9334003c9e
- TokenVersion() distinguishes nbt_-prefixed v2 tokens from legacy v1 - 403 errors now say "check token permissions" + v1 hint if applicable - Setup wizard prints a note after saving if a v1 token was entered Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
182 lines
4.8 KiB
Go
182 lines
4.8 KiB
Go
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
|
|
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
|
|
}
|
|
|
|
if netbox.TokenVersion(token) == 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),
|
|
},
|
|
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
|
|
}
|