package main import ( "sort" "strings" "git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/cache" "git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/config" "git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/netbox" "git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/shortcuts" ) // newCache opens the host cache with the config's static hosts. func newCache(cfg *config.Config) *cache.Cache { return cache.New(cfg.CachePath(), cfg.Cache.TTL).WithStatic(cfg.Hosts) } func newNetBoxClient(cfg *config.Config) *netbox.Client { if len(cfg.NetBoxes) > 0 { sources := make([]netbox.Source, 0, len(cfg.NetBoxes)) for _, source := range cfg.NetBoxes { sources = append(sources, netbox.Source{Name: source.Name, URL: source.URL, Token: source.Token, TokenVersion: source.TokenVersion}) } return netbox.NewMultiClient(sources) } if cfg.NetBox.URL == "" { return nil } return netbox.NewClient(cfg.NetBox.URL, cfg.NetBox.Token, cfg.NetBox.TokenVersion) } func normalizeTarget(cfg config.ShortcutsConfig, qualified bool) func(string) string { return func(target string) string { source, name := netbox.SplitTarget(target) name = shortcuts.Normalize(name, cfg) if qualified && source != "" { return source + "/" + name } return name } } // hostCompletions returns the cached hosts matching toComplete in a form that // starts with the typed text, because bash and fish only accept such // candidates. Without an instance prefix, a hostname that is unique across // all instances is offered bare ("web01"), so it completes like with a single // NetBox; connecting to it still checks every instance for ambiguity. // Names present in several instances stay qualified ("prod/web01"). // Hosts whose full name does not match are offered by their configured // shortcut instead ("fsnb" → "fsnbx1" for fsn1-bx1.example.com). func hostCompletions(entries []cache.Entry, toComplete string, sc config.ShortcutsConfig) []string { prefix := strings.ToLower(toComplete) qualifiedInput := strings.Contains(prefix, "/") instances := make(map[string]int) for _, e := range entries { _, name := netbox.SplitTarget(e.Name) instances[strings.ToLower(name)]++ } var unmatched []cache.Entry var names []string for _, e := range entries { source, name := netbox.SplitTarget(e.Name) target := strings.ToLower(e.Name) switch { case source != "" && !qualifiedInput && strings.HasPrefix(strings.ToLower(name), prefix): if instances[strings.ToLower(name)] == 1 { names = append(names, name) } else { names = append(names, e.Name) } case strings.HasPrefix(target, prefix): names = append(names, e.Name) default: unmatched = append(unmatched, e) } } if sc.Enabled() { names = append(names, shortcutCompletions(entries, unmatched, prefix, sc)...) } sort.Strings(names) return names } // shortcutCompletions offers the shortcut of each unmatched host when it starts // with prefix and selects exactly one host, qualified by instance if needed. func shortcutCompletions(all, unmatched []cache.Entry, prefix string, sc config.ShortcutsConfig) []string { bare := make(map[string]int) qualified := make(map[string]int) short := func(e cache.Entry) (string, string) { source, name := netbox.SplitTarget(e.Name) s := shortcuts.Normalize(name, sc) if source == "" { return s, s } return s, strings.ToLower(source) + "/" + s } for _, e := range all { b, q := short(e) bare[b]++ qualified[q]++ } seen := make(map[string]bool) var out []string for _, e := range unmatched { b, q := short(e) candidate := "" switch { case strings.Contains(prefix, "/"): if qualified[q] == 1 && strings.HasPrefix(q, prefix) { candidate = q } case !strings.HasPrefix(b, prefix): case bare[b] == 1: candidate = b case q != b && qualified[q] == 1: candidate = q // like full names: zsh accepts it, bash/fish ignore it } if candidate != "" && !seen[candidate] { seen[candidate] = true out = append(out, candidate) } } return out }