d127a3b957
Release / release (push) Successful in 49s
- **Strategies**: Add resolver strategy input validation and parsing in setup wizard. Support comma-separated input with known strategy mapping. - **Client**: Extend Search and SearchAll to include kind and tag filters. Add pagination for full cache refresh handling large datasets. - **Cache**: Introduce `RecentlyUsed` and `MarkUsed`. Persist `LastUsed` timestamps for entries. - **TUI**: Add recent hosts view, tag/kind filters, and inline editor for user/port override. - **Tests**: Comprehensive unit tests for new features, including strategy validation, cache behavior, and client filtering. - **Docs**: Update README with new TUI features and cache subcommands.
211 lines
5.6 KiB
Go
211 lines
5.6 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
|
|
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
|
|
}
|