Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,16 @@ go run github.com/syself/caphcli@latest create-host-yaml 1234567 1234567.yaml

This will create a HetznerBareMetalHost YAML file: `1234567.yaml`

The generated host starts with `spec.maintenanceMode: true`, and the command prints a hint to run `check-bm-server` next.

After that you can check if the rescue system is reachable reliably:

```console
go run github.com/syself/caphcli@latest check-bm-servers 1234567.yaml
go run github.com/syself/caphcli@latest check-bm-server 1234567.yaml
```

`check-bm-server` refuses to run unless `spec.maintenanceMode` is `true`. After a successful check it prints a hint to disable maintenance mode again.

<!-- readmegen:cli-help:start -->

## CLI Help
Expand All @@ -47,7 +51,7 @@ Usage:
caphcli [command]

Available Commands:
check-bm-servers Validate rescue and provisioning reliability for one bare-metal server
check-bm-server Validate rescue and provisioning reliability for one bare-metal server
completion Generate the autocompletion script for the specified shell
create-host-yaml Generate a HetznerBareMetalHost YAML file for one Robot server
help Help about any command
Expand All @@ -58,7 +62,7 @@ Flags:
Use "caphcli [command] --help" for more information about a command.
```

### `caphcli check-bm-servers --help`
### `caphcli check-bm-server --help`

```text
Validate rescue and provisioning reliability for one HetznerBareMetalHost from a local YAML file.
Expand All @@ -68,16 +72,16 @@ HetznerBareMetalHost objects and then talks directly to Hetzner Robot plus the
target server.

Usage:
caphcli check-bm-servers FILE [flags]
caphcli check-bm-server FILE [flags]

Examples:
caphcli check-bm-servers \
caphcli check-bm-server \
test/e2e/data/infrastructure-hetzner/v1beta1/bases/hetznerbaremetalhosts.yaml \
--name bm-e2e-1731561

Flags:
--force Skip the destructive-action confirmation prompt
-h, --help help for check-bm-servers
-h, --help help for check-bm-server
--image-path string Installimage IMAGE path for operating system inside the Hetzner rescue system (default "/root/.oldroot/nfs/images/Ubuntu-2404-noble-amd64-base.tar.gz")
--name string HetznerBareMetalHost metadata.name. Optional if YAML contains exactly one host
--poll-interval duration Polling interval for wait steps (default 10s)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,20 @@ import (
"github.com/syself/caphcli/internal/provisioncheck"
)

func newCheckBMServersCommand() *cobra.Command {
func newCheckBMServerCommand() *cobra.Command {
cfg := provisioncheck.DefaultConfig()
cfg.Input = os.Stdin
cfg.Output = os.Stdout

cmd := &cobra.Command{
Use: "check-bm-servers FILE",
Use: "check-bm-server FILE",
Short: "Validate rescue and provisioning reliability for one bare-metal server",
Long: `Validate rescue and provisioning reliability for one HetznerBareMetalHost from a local YAML file.

The command does not talk to Kubernetes. It reads one local YAML file containing
HetznerBareMetalHost objects and then talks directly to Hetzner Robot plus the
target server.`,
Example: ` caphcli check-bm-servers \
Example: ` caphcli check-bm-server \
test/e2e/data/infrastructure-hetzner/v1beta1/bases/hetznerbaremetalhosts.yaml \
--name bm-e2e-1731561`,
Args: cobra.ExactArgs(1),
Expand All @@ -35,7 +35,7 @@ target server.`,
}

if err := provisioncheck.Run(context.Background(), cfg); err != nil {
return fmt.Errorf("caphcli check-bm-servers failed for %q: %w", cfg.Name, err)
return fmt.Errorf("caphcli check-bm-server failed for %q: %w", cfg.Name, err)
}

return nil
Expand Down
1 change: 1 addition & 0 deletions internal/cmd/createhostyaml.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ YAML file to the requested output path. Progress and confirmation prompts go to
}
f = nil
_, _ = fmt.Fprintf(cfg.LogOutput, "✓ created %s\n", outputFile)
_, _ = fmt.Fprintf(cfg.LogOutput, "Hint: run `caphcli check-bm-server %s` next.\n", outputFile)

return nil
},
Expand Down
2 changes: 1 addition & 1 deletion internal/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func NewRootCommand() *cobra.Command {
SilenceErrors: false,
}

rootCmd.AddCommand(newCheckBMServersCommand())
rootCmd.AddCommand(newCheckBMServerCommand())
rootCmd.AddCommand(newCreateHostYAMLCommand())

return rootCmd
Expand Down
2 changes: 1 addition & 1 deletion internal/createhostyaml/createhostyaml.go
Original file line number Diff line number Diff line change
Expand Up @@ -586,7 +586,7 @@ func renderTemplate(server *models.Server, name string, disks []disk) string {
}
fmt.Fprintf(&b, " # wwn: %q\n", disk.WWN)
}
b.WriteString(" maintenanceMode: false\n")
b.WriteString(" maintenanceMode: true\n")
fmt.Fprintf(&b, " description: %q\n", defaultDescription(server))
return b.String()
}
Expand Down
2 changes: 1 addition & 1 deletion internal/createhostyaml/createhostyaml_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ func TestRenderTemplate(t *testing.T) {
`serverID: 1751550 # Robot name: ci-box-1751550, IP: 144.76.74.13`,
`wwn: "0x0001"`,
`# wwn: "0x0002"`,
`maintenanceMode: false`,
`maintenanceMode: true`,
`description: "ci-box-1751550"`,
}

Expand Down
35 changes: 23 additions & 12 deletions internal/provisioncheck/provisioncheck.go
Original file line number Diff line number Diff line change
Expand Up @@ -241,14 +241,6 @@ func Run(ctx context.Context, cfg Config) error {

r := newRunner(cfg)

// Load all local inputs first so parse and credential errors fail before any
// Robot API call or reboot on the target machine.
creds, err := loadEnvCredentials()
if err != nil {
return err
}
r.creds = creds

hosts, err := loadHostsFromHBMHYAMLFile(cfg.HbmhYAMLFile)
if err != nil {
return err
Expand All @@ -265,6 +257,14 @@ func Run(ctx context.Context, cfg Config) error {
}
r.host = host

// Load credentials only after the selected host has passed local manifest
// validation, including the maintenance-mode safety gate.
creds, err := loadEnvCredentials()
if err != nil {
return err
}
r.creds = creds

// Ask for confirmation only after we know the exact host and WWNs that will
// be wiped by the provisioning loop.
if err := r.confirmDestructiveAction(); err != nil {
Expand Down Expand Up @@ -363,6 +363,7 @@ func (r *runner) run(ctx context.Context) error {

_, _ = fmt.Fprintln(r.out)
r.logf("all checks passed: machine %q (serverID=%d) completed two rescue+install+boot cycles", r.host.Name, r.host.Spec.ServerID)
_, _ = fmt.Fprintf(r.out, "Hint: set spec.maintenanceMode back to false in %s now.\n", r.cfg.HbmhYAMLFile)
return nil
}

Expand Down Expand Up @@ -792,8 +793,8 @@ func selectHost(hosts []infrav1.HetznerBareMetalHost, name string) (infrav1.Hetz
if name != "" {
for _, host := range hosts {
if host.Name == name {
if host.Spec.RootDeviceHints == nil {
return infrav1.HetznerBareMetalHost{}, fmt.Errorf("host %q has no spec.rootDeviceHints", host.Name)
if err := validateHostForProvisionCheck(host); err != nil {
return infrav1.HetznerBareMetalHost{}, err
}
return host, nil
}
Expand All @@ -816,12 +817,22 @@ func selectHost(hosts []infrav1.HetznerBareMetalHost, name string) (infrav1.Hetz
}

host := hosts[0]
if host.Spec.RootDeviceHints == nil {
return infrav1.HetznerBareMetalHost{}, fmt.Errorf("host %q has no spec.rootDeviceHints", host.Name)
if err := validateHostForProvisionCheck(host); err != nil {
return infrav1.HetznerBareMetalHost{}, err
}
return host, nil
}

func validateHostForProvisionCheck(host infrav1.HetznerBareMetalHost) error {
if host.Spec.RootDeviceHints == nil {
return fmt.Errorf("host %q has no spec.rootDeviceHints", host.Name)
}
if host.Spec.MaintenanceMode == nil || !*host.Spec.MaintenanceMode {
return fmt.Errorf("host %q must set spec.maintenanceMode: true before running check-bm-server", host.Name)
}
return nil
}

func listHostNames(hosts []infrav1.HetznerBareMetalHost) []string {
names := make([]string, 0, len(hosts))
for _, host := range hosts {
Expand Down
66 changes: 66 additions & 0 deletions internal/provisioncheck/provisioncheck_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ package provisioncheck
import (
"os"
"path/filepath"
"strings"
"testing"

infrav1 "github.com/syself/cluster-api-provider-hetzner/api/v1beta1"
)

func TestLoadHostsFromHBMHYAMLFile(t *testing.T) {
Expand Down Expand Up @@ -102,3 +105,66 @@ items:
})
}
}

func TestSelectHostRequiresMaintenanceMode(t *testing.T) {
t.Parallel()

trueValue := true
falseValue := false

tests := []struct {
name string
host infrav1.HetznerBareMetalHost
wantErr string
}{
{
name: "maintenance mode unset",
host: infrav1.HetznerBareMetalHost{
Spec: infrav1.HetznerBareMetalHostSpec{
RootDeviceHints: &infrav1.RootDeviceHints{WWN: "0x1"},
},
},
wantErr: `must set spec.maintenanceMode: true`,
},
{
name: "maintenance mode false",
host: infrav1.HetznerBareMetalHost{
Spec: infrav1.HetznerBareMetalHostSpec{
RootDeviceHints: &infrav1.RootDeviceHints{WWN: "0x1"},
MaintenanceMode: &falseValue,
},
},
wantErr: `must set spec.maintenanceMode: true`,
},
{
name: "maintenance mode true",
host: infrav1.HetznerBareMetalHost{
Spec: infrav1.HetznerBareMetalHostSpec{
RootDeviceHints: &infrav1.RootDeviceHints{WWN: "0x1"},
MaintenanceMode: &trueValue,
},
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

tt.host.Name = tt.name
_, err := selectHost([]infrav1.HetznerBareMetalHost{tt.host}, "")
if tt.wantErr == "" {
if err != nil {
t.Fatalf("selectHost() error = %v", err)
}
return
}
if err == nil {
t.Fatalf("selectHost() error = nil, want substring %q", tt.wantErr)
}
if !strings.Contains(err.Error(), tt.wantErr) {
t.Fatalf("selectHost() error = %q, want substring %q", err.Error(), tt.wantErr)
}
})
}
}
4 changes: 2 additions & 2 deletions internal/tools/readmegen/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const generatedSectionTemplate = `## CLI Help
{{ROOT_HELP}}
` + "```" + `

### ` + "`caphcli check-bm-servers --help`" + `
### ` + "`caphcli check-bm-server --help`" + `

` + "```text" + `
{{CHECK_HELP}}
Expand All @@ -42,7 +42,7 @@ func main() {
fail(err)
}

checkHelp, err := renderHelp("check-bm-servers")
checkHelp, err := renderHelp("check-bm-server")
if err != nil {
fail(err)
}
Expand Down
Loading