feat: interactive setup wizard for first-run and netssh configure
Release / release (push) Successful in 1m36s
Release / release (push) Successful in 1m36s
- Auto-detects missing config (netbox.url empty) and launches wizard - `netssh configure` re-runs the wizard anytime to change settings - 3-page huh form: NetBox connection, SSH defaults, resolver & cache - Saves to ~/.config/netssh.yaml (permissions 0600) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,15 @@ type SSHConfig struct {
|
||||
DefaultUser string `mapstructure:"default_user"`
|
||||
}
|
||||
|
||||
// Path returns the canonical config file path.
|
||||
func Path() string {
|
||||
configDir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
configDir = filepath.Join(os.Getenv("HOME"), ".config")
|
||||
}
|
||||
return filepath.Join(configDir, "netssh.yaml")
|
||||
}
|
||||
|
||||
func Load() (*Config, error) {
|
||||
v := viper.New()
|
||||
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/huh"
|
||||
|
||||
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/config"
|
||||
)
|
||||
|
||||
// RunWizard runs the interactive setup form, pre-filled with any existing cfg values.
|
||||
// It saves the result to the config file on success.
|
||||
func RunWizard(cfg *config.Config) error {
|
||||
url := cfg.NetBox.URL
|
||||
token := cfg.NetBox.Token
|
||||
defaultUser := cfg.SSH.DefaultUser
|
||||
strategies := cfg.Resolver.Strategies
|
||||
subnets := strings.Join(cfg.Resolver.ManagementSubnets, ", ")
|
||||
interfaceName := cfg.Resolver.InterfaceName
|
||||
cacheTTL := strconv.Itoa(cfg.Cache.TTL)
|
||||
|
||||
if len(strategies) == 0 {
|
||||
strategies = []string{"primary_ip"}
|
||||
}
|
||||
if cacheTTL == "0" {
|
||||
cacheTTL = "3600"
|
||||
}
|
||||
|
||||
form := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewInput().
|
||||
Title("NetBox URL").
|
||||
Description("e.g. https://netbox.example.com").
|
||||
Placeholder("https://").
|
||||
Value(&url).
|
||||
Validate(func(s string) error {
|
||||
if strings.TrimSpace(s) == "" {
|
||||
return errors.New("required")
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
huh.NewInput().
|
||||
Title("NetBox API token").
|
||||
EchoMode(huh.EchoModePassword).
|
||||
Value(&token).
|
||||
Validate(func(s string) error {
|
||||
if strings.TrimSpace(s) == "" {
|
||||
return errors.New("required")
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
).Title("NetBox connection"),
|
||||
|
||||
huh.NewGroup(
|
||||
huh.NewInput().
|
||||
Title("Default SSH user").
|
||||
Description("Leave empty to use your system user ($USER).").
|
||||
Value(&defaultUser),
|
||||
).Title("SSH defaults"),
|
||||
|
||||
huh.NewGroup(
|
||||
huh.NewMultiSelect[string]().
|
||||
Title("Resolver strategies").
|
||||
Description("Order matters: first match wins.").
|
||||
Options(
|
||||
huh.NewOption("primary_ip — NetBox primary IPv4/IPv6", "primary_ip"),
|
||||
huh.NewOption("management_subnet — first IP inside a subnet", "management_subnet"),
|
||||
huh.NewOption("interface_name — IP on a named interface", "interface_name"),
|
||||
).
|
||||
Value(&strategies),
|
||||
huh.NewInput().
|
||||
Title("Management subnets").
|
||||
Description("Comma-separated CIDRs, e.g. 10.0.0.0/8, 192.168.0.0/16\nOnly used when management_subnet strategy is active.").
|
||||
Value(&subnets),
|
||||
huh.NewInput().
|
||||
Title("Interface name").
|
||||
Description("Only used when interface_name strategy is active.").
|
||||
Placeholder("eth0").
|
||||
Value(&interfaceName),
|
||||
huh.NewInput().
|
||||
Title("Cache TTL (seconds)").
|
||||
Value(&cacheTTL).
|
||||
Validate(func(s string) error {
|
||||
if _, err := strconv.Atoi(s); err != nil {
|
||||
return errors.New("must be a number")
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
).Title("Resolver & cache"),
|
||||
)
|
||||
|
||||
if err := form.Run(); err != nil {
|
||||
if errors.Is(err, huh.ErrUserAborted) {
|
||||
fmt.Fprintln(os.Stderr, "Setup cancelled.")
|
||||
os.Exit(0)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
ttl, _ := strconv.Atoi(cacheTTL)
|
||||
|
||||
var subnetList []string
|
||||
for _, s := range strings.Split(subnets, ",") {
|
||||
if s = strings.TrimSpace(s); s != "" {
|
||||
subnetList = append(subnetList, s)
|
||||
}
|
||||
}
|
||||
|
||||
out := config.Config{
|
||||
NetBox: config.NetBoxConfig{
|
||||
URL: strings.TrimRight(strings.TrimSpace(url), "/"),
|
||||
Token: strings.TrimSpace(token),
|
||||
},
|
||||
SSH: config.SSHConfig{
|
||||
DefaultUser: strings.TrimSpace(defaultUser),
|
||||
},
|
||||
Resolver: config.ResolverConfig{
|
||||
Strategies: strategies,
|
||||
ManagementSubnets: subnetList,
|
||||
InterfaceName: strings.TrimSpace(interfaceName),
|
||||
},
|
||||
Cache: config.CacheConfig{
|
||||
TTL: ttl,
|
||||
},
|
||||
}
|
||||
|
||||
return save(out)
|
||||
}
|
||||
|
||||
func save(cfg config.Config) error {
|
||||
path := config.Path()
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
||||
return fmt.Errorf("creating config dir: %w", err)
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString("netbox:\n")
|
||||
b.WriteString(fmt.Sprintf(" url: %q\n", cfg.NetBox.URL))
|
||||
b.WriteString(fmt.Sprintf(" token: %q\n", cfg.NetBox.Token))
|
||||
|
||||
b.WriteString("\nresolver:\n")
|
||||
b.WriteString(" strategies:\n")
|
||||
for _, s := range cfg.Resolver.Strategies {
|
||||
fmt.Fprintf(&b, " - %s\n", s)
|
||||
}
|
||||
if len(cfg.Resolver.ManagementSubnets) > 0 {
|
||||
b.WriteString(" management_subnets:\n")
|
||||
for _, s := range cfg.Resolver.ManagementSubnets {
|
||||
fmt.Fprintf(&b, " - %s\n", s)
|
||||
}
|
||||
}
|
||||
if cfg.Resolver.InterfaceName != "" {
|
||||
fmt.Fprintf(&b, " interface_name: %q\n", cfg.Resolver.InterfaceName)
|
||||
}
|
||||
|
||||
b.WriteString("\ncache:\n")
|
||||
fmt.Fprintf(&b, " ttl: %d\n", cfg.Cache.TTL)
|
||||
|
||||
b.WriteString("\nssh:\n")
|
||||
if cfg.SSH.DefaultUser != "" {
|
||||
fmt.Fprintf(&b, " default_user: %q\n", cfg.SSH.DefaultUser)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(path, []byte(b.String()), 0o600); err != nil {
|
||||
return fmt.Errorf("writing config: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("\nConfig saved → %s\n", path)
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user