Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
.envrc
.vscode/
caphcli
/*.yaml
57 changes: 54 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,28 @@ Depending on the command, these environment variables are needed.
- One of `HETZNER_SSH_PUB_PATH` or `HETZNER_SSH_PUB` for the SSH public key.
- One of `HETZNER_SSH_PRIV_PATH` or `HETZNER_SSH_PRIV` for the SSH private key.

## Common Usage

If you have Go installed, the easiest way is to run the code like this:

```console
go run github.com/syself/caphcli@latest -h
```

If you have new Hetzner Baremetal (Robot) Server, then create a HetznerBareMetalHost YAML file:

```console
go run github.com/syself/caphcli@latest create-host-yaml 1234567 1234567.yaml
```

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

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
```

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

## CLI Help
Expand All @@ -27,6 +49,7 @@ Usage:
Available Commands:
check-bm-servers 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

Flags:
Expand All @@ -45,15 +68,14 @@ HetznerBareMetalHost objects and then talks directly to Hetzner Robot plus the
target server.

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

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

Flags:
--file string Path to a local YAML file containing HetznerBareMetalHost objects (required)
--force Skip the destructive-action confirmation prompt
-h, --help help for check-bm-servers
--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")
Expand All @@ -71,4 +93,33 @@ Flags:
--timeout-wait-rescue duration Timeout for waiting until rescue SSH is reachable (default 6m0s)
```

### `caphcli create-host-yaml --help`

```text
Generate a HetznerBareMetalHost YAML file for one Hetzner Robot server.

The command talks directly to Hetzner Robot, ensures rescue SSH access, reboots
the target server into rescue once, inspects the available disks, and writes a
YAML file to the requested output path. Progress and confirmation prompts go to stderr.

Usage:
caphcli create-host-yaml SERVER_ID OUTPUT_FILE [flags]

Examples:
caphcli create-host-yaml 1751550 host.yaml
caphcli create-host-yaml --force --name bm-e2e-1751550 1751550 host.yaml

Flags:
--force Skip the reboot confirmation prompt
-h, --help help for create-host-yaml
--name string metadata.name for the generated HetznerBareMetalHost (default: bm-SERVER_ID)
--poll-interval duration Polling interval while waiting for rescue SSH (default 10s)
--timeout-activate-rescue duration Timeout for activating rescue boot (default 45s)
--timeout-ensure-ssh-key duration Timeout for ensuring SSH key in Robot (default 1m0s)
--timeout-fetch-server duration Timeout for fetching server details from Robot (default 30s)
--timeout-load-input duration Timeout for env loading + initial validation (default 30s)
--timeout-reboot-rescue duration Timeout for requesting reboot to rescue (default 45s)
--timeout-wait-rescue duration Timeout for waiting until rescue SSH is reachable (default 6m0s)
```

<!-- readmegen:cli-help:end -->
15 changes: 6 additions & 9 deletions internal/cmd/check_bm_servers.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package cmd

import (
"context"
"errors"
"fmt"
"os"

Expand All @@ -17,23 +16,22 @@ func newCheckBMServersCommand() *cobra.Command {
cfg.Output = os.Stdout

cmd := &cobra.Command{
Use: "check-bm-servers",
Use: "check-bm-servers 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 \
--file test/e2e/data/infrastructure-hetzner/v1beta1/bases/hetznerbaremetalhosts.yaml \
test/e2e/data/infrastructure-hetzner/v1beta1/bases/hetznerbaremetalhosts.yaml \
--name bm-e2e-1731561`,
RunE: func(_ *cobra.Command, _ []string) error {
if cfg.HbmhYAMLFile == "" {
return errors.New("--file is required")
}
Args: cobra.ExactArgs(1),
RunE: func(_ *cobra.Command, args []string) error {
cfg.HbmhYAMLFile = args[0]

if _, err := os.Stat(cfg.HbmhYAMLFile); err != nil {
return fmt.Errorf("check --file: %w", err)
return fmt.Errorf("check FILE: %w", err)
}

if err := provisioncheck.Run(context.Background(), cfg); err != nil {
Expand All @@ -45,7 +43,6 @@ target server.`,
}

flags := cmd.Flags()
flags.StringVar(&cfg.HbmhYAMLFile, "file", "", "Path to a local YAML file containing HetznerBareMetalHost objects (required)")
flags.StringVar(&cfg.Name, "name", "", "HetznerBareMetalHost metadata.name. Optional if YAML contains exactly one host")
flags.StringVar(&cfg.ImagePath, "image-path", provisioncheck.DefaultUbuntu2404ImagePath, "Installimage IMAGE path for operating system inside the Hetzner rescue system")
flags.BoolVar(&cfg.Force, "force", false, "Skip the destructive-action confirmation prompt")
Expand Down
76 changes: 76 additions & 0 deletions internal/cmd/createhostyaml.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package cmd

import (
"context"
"fmt"
"os"
"strconv"

"github.com/spf13/cobra"

"github.com/syself/caphcli/internal/createhostyaml"
"github.com/syself/caphcli/internal/provisioncheck"
)

func newCreateHostYAMLCommand() *cobra.Command {
cfg := createhostyaml.DefaultConfig()
cfg.Input = os.Stdin
cfg.LogOutput = os.Stderr

cmd := &cobra.Command{
Use: "create-host-yaml SERVER_ID OUTPUT_FILE",
Short: "Generate a HetznerBareMetalHost YAML file for one Robot server",
Long: `Generate a HetznerBareMetalHost YAML file for one Hetzner Robot server.

The command talks directly to Hetzner Robot, ensures rescue SSH access, reboots
the target server into rescue once, inspects the available disks, and writes a
YAML file to the requested output path. Progress and confirmation prompts go to stderr.`,
Example: ` caphcli create-host-yaml 1751550 host.yaml
caphcli create-host-yaml --force --name bm-e2e-1751550 1751550 host.yaml`,
Args: cobra.ExactArgs(2),
RunE: func(_ *cobra.Command, args []string) error {
serverID, err := strconv.Atoi(args[0])
if err != nil {
return fmt.Errorf("parse SERVER_ID %q: %w", args[0], err)
}
cfg.ServerID = serverID
outputFile := args[1]

f, err := os.Create(outputFile)
if err != nil {
return fmt.Errorf("create output file %q: %w", outputFile, err)
}
defer func() {
if f != nil {
_ = f.Close()
}
}()
cfg.Output = f

if err := createhostyaml.Run(context.Background(), cfg); err != nil {
return fmt.Errorf("caphcli create-host-yaml failed for server %d: %w", cfg.ServerID, err)
}

if err := f.Close(); err != nil {
return fmt.Errorf("close output file %q: %w", outputFile, err)
}
f = nil
_, _ = fmt.Fprintf(cfg.LogOutput, "✓ created %s\n", outputFile)

return nil
},
}

flags := cmd.Flags()
flags.BoolVar(&cfg.Force, "force", false, "Skip the reboot confirmation prompt")
flags.StringVar(&cfg.Name, "name", "", "metadata.name for the generated HetznerBareMetalHost (default: bm-SERVER_ID)")
flags.DurationVar(&cfg.PollInterval, "poll-interval", provisioncheck.DefaultPollInterval, "Polling interval while waiting for rescue SSH")
flags.DurationVar(&cfg.Timeouts.LoadInput, "timeout-load-input", provisioncheck.DefaultLoadInputTimeout, "Timeout for env loading + initial validation")
flags.DurationVar(&cfg.Timeouts.EnsureSSHKey, "timeout-ensure-ssh-key", provisioncheck.DefaultEnsureSSHKeyTimeout, "Timeout for ensuring SSH key in Robot")
flags.DurationVar(&cfg.Timeouts.FetchServerDetails, "timeout-fetch-server", provisioncheck.DefaultFetchServerDetailsTimeout, "Timeout for fetching server details from Robot")
flags.DurationVar(&cfg.Timeouts.ActivateRescue, "timeout-activate-rescue", provisioncheck.DefaultActivateRescueTimeout, "Timeout for activating rescue boot")
flags.DurationVar(&cfg.Timeouts.RebootToRescue, "timeout-reboot-rescue", provisioncheck.DefaultRebootToRescueTimeout, "Timeout for requesting reboot to rescue")
flags.DurationVar(&cfg.Timeouts.WaitForRescue, "timeout-wait-rescue", provisioncheck.DefaultWaitForRescueTimeout, "Timeout for waiting until rescue SSH is reachable")

return cmd
}
1 change: 1 addition & 0 deletions internal/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ func NewRootCommand() *cobra.Command {
}

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

return rootCmd
}
Loading
Loading