package setup import ( "errors" "fmt" "os" "path/filepath" "strconv" "strings" "github.com/charmbracelet/huh" "git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/config" "git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/netbox" ) // 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 strategiesRaw := strings.Join(cfg.Resolver.Strategies, ", ") subnets := strings.Join(cfg.Resolver.ManagementSubnets, ", ") interfaceName := cfg.Resolver.InterfaceName cacheTTL := strconv.Itoa(cfg.Cache.TTL) if strategiesRaw == "" { strategiesRaw = "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.NewInput(). Title("Resolver strategies"). Description("Comma-separated, in priority order. First match wins.\nAvailable: primary_ip, management_subnet, interface_name"). Placeholder("primary_ip, management_subnet"). Value(&strategiesRaw). Validate(validateStrategies), 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 } strategies := parseStrategies(strategiesRaw) tokenVersion := netbox.TokenVersion(token) if tokenVersion == 1 { fmt.Fprintln(os.Stderr, "\nHinweis: Du verwendest einen Legacy-Token (v1). Erstelle in NetBox einen v2-Token (beginnt mit nbt_) für bessere Kompatibilität.") fmt.Fprintln(os.Stderr, " NetBox → Admin → API Tokens → Add Token") } 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), TokenVersion: tokenVersion, }, 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) } var knownStrategies = map[string]bool{ "primary_ip": true, "management_subnet": true, "interface_name": true, } func validateStrategies(s string) error { if strings.TrimSpace(s) == "" { return errors.New("at least one strategy is required") } for _, name := range parseStrategies(s) { if !knownStrategies[name] { return fmt.Errorf("unknown strategy %q — available: primary_ip, management_subnet, interface_name", name) } } return nil } func parseStrategies(s string) []string { var out []string for _, part := range strings.Split(s, ",") { if name := strings.TrimSpace(part); name != "" { out = append(out, name) } } return 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)) fmt.Fprintf(&b, " token_version: %d\n", cfg.NetBox.TokenVersion) 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 }