feat: search multiple NetBox instances with isolated host identities
Release / release (push) Successful in 5m16s
Release / release (push) Successful in 5m16s
This commit is contained in:
1 parent
a2a6dd7591
commit
7f53b90266
26 files changed
+1299
-115
No files matched your search
@@ -0,0 +1,32 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"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"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/cache"
|
||||
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/config"
|
||||
)
|
||||
|
||||
func prepareInstancesTest(t *testing.T, officeURL, labURL string) *config.Config {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
t.Setenv("HOME", t.TempDir())
|
||||
t.Setenv("XDG_CONFIG_HOME", dir)
|
||||
t.Setenv("XDG_CACHE_HOME", t.TempDir())
|
||||
t.Setenv("NETSSH_NETBOX_URL", "")
|
||||
t.Setenv("NETSSH_NETBOX_TOKEN", "")
|
||||
t.Setenv("NETSSH_CACHE_PATH", filepath.Join(t.TempDir(), "hosts.json"))
|
||||
t.Setenv("NETSSH_SHELL_ALIAS", "")
|
||||
data := fmt.Sprintf("netboxes:\n - name: office\n url: %q\n token: office-token\n - name: lab\n url: %q\n token: lab-token\nresolver:\n strategies: [primary_ip]\n", officeURL, labURL)
|
||||
if err := os.WriteFile(filepath.Join(dir, "netssh.yaml"), []byte(data), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
func multiHost(name, ip string) connectionTestHost {
|
||||
return connectionTestHost{ID: 1, Name: name, PrimaryIP4: &struct {
|
||||
Address string `json:"address"`
|
||||
}{Address: ip + "/24"}}
|
||||
}
|
||||
|
||||
func TestRefreshStoresSameHostnameFromBothInstances(t *testing.T) {
|
||||
office := connectionTestServer(t, []connectionTestHost{multiHost("web", "10.0.0.1")}, nil)
|
||||
lab := connectionTestServer(t, []connectionTestHost{multiHost("web", "10.0.0.2")}, nil)
|
||||
cfg := prepareInstancesTest(t, office.URL, lab.URL)
|
||||
cmd := rootCmd()
|
||||
cmd.SetArgs([]string{"cache", "refresh"})
|
||||
if err := cmd.Execute(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c := cache.New(cfg.CachePath(), cfg.Cache.TTL)
|
||||
if err := c.Load(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for name, ip := range map[string]string{"office/web": "10.0.0.1", "lab/web": "10.0.0.2"} {
|
||||
got, fresh := c.Get(name)
|
||||
if !fresh || got.IP != ip {
|
||||
t.Fatalf("cache source collision: %+v, fresh=%v", got, fresh)
|
||||
}
|
||||
}
|
||||
if len(c.All()) != 2 || c.NeedsRefresh(time.Hour) {
|
||||
t.Fatal("full refresh is incomplete")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefreshFailureAndPerInstanceRefresh(t *testing.T) {
|
||||
office := connectionTestServer(t, []connectionTestHost{multiHost("web", "10.0.0.1")}, nil)
|
||||
failed := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Error(w, "offline", 503) }))
|
||||
defer failed.Close()
|
||||
cfg := prepareInstancesTest(t, office.URL, failed.URL)
|
||||
c := cache.New(cfg.CachePath(), cfg.Cache.TTL)
|
||||
c.Upsert(cache.Entry{Name: "lab/old", Kind: "device", IP: "10.0.0.2"})
|
||||
if err := c.Save(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cmd := rootCmd()
|
||||
cmd.SetArgs([]string{"cache", "refresh"})
|
||||
if err := cmd.Execute(); err == nil {
|
||||
t.Fatal("failed instance marked as full refresh")
|
||||
}
|
||||
if err := c.Load(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(c.All()) != 1 || !c.NeedsRefresh(time.Hour) {
|
||||
t.Fatal("partial refresh changed cache or full-refresh stamp")
|
||||
}
|
||||
cmd = rootCmd()
|
||||
cmd.SetArgs([]string{"cache", "refresh", "--instance", "office"})
|
||||
if err := cmd.Execute(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := c.Load(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(c.All()) != 2 || !c.NeedsRefresh(time.Hour) {
|
||||
t.Fatal("per-instance refresh lost other source or updated full stamp")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalNotesCLIKeepsInstancesSeparate(t *testing.T) {
|
||||
cfg := prepareInstancesTest(t, "https://office.example", "https://lab.example")
|
||||
c := cache.New(cfg.CachePath(), cfg.Cache.TTL)
|
||||
for _, name := range []string{"office/web", "lab/web"} {
|
||||
c.Upsert(cache.Entry{Name: name, Kind: "device", IP: "10.0.0.1"})
|
||||
}
|
||||
if err := c.Save(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
run := func(args ...string) (string, error) {
|
||||
var out bytes.Buffer
|
||||
cmd := rootCmd()
|
||||
cmd.SetArgs(append([]string{"notes"}, args...))
|
||||
cmd.SetOut(&out)
|
||||
cmd.SetErr(&out)
|
||||
err := cmd.Execute()
|
||||
return out.String(), err
|
||||
}
|
||||
for _, source := range []string{"office", "lab"} {
|
||||
if _, err := run("set", source+"/web", source+" note"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
for _, source := range []string{"office", "lab"} {
|
||||
got, err := run("show", source+"/web")
|
||||
if err != nil || got != source+" note" {
|
||||
t.Fatalf("CLI notes crossed sources: %q, %v", got, err)
|
||||
}
|
||||
}
|
||||
if _, err := run("set", "web", "wrong note"); err == nil {
|
||||
t.Fatal("ambiguous note target accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiInstanceWrapperHelper(t *testing.T) {
|
||||
if target := os.Getenv("NETSSH_TEST_MULTI_TARGET"); target != "" {
|
||||
runSSHWrapper([]string{target})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrapperRoutesQualifiedTargetsAndRejectsAmbiguity(t *testing.T) {
|
||||
office := connectionTestServer(t, []connectionTestHost{multiHost("web", "10.0.0.1")}, nil)
|
||||
lab := connectionTestServer(t, []connectionTestHost{multiHost("web", "10.0.0.2")}, nil)
|
||||
cfg := prepareInstancesTest(t, office.URL, lab.URL)
|
||||
c := cache.New(cfg.CachePath(), 3600)
|
||||
if err := c.SetRefreshed(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bin := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(bin, "ssh"), []byte("#!/bin/sh\nprintf '%s\\n' \"$@\"\n"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for target, want := range map[string]string{"office/web": "10.0.0.1", "lab/web": "10.0.0.2", "web": "ambiguous"} {
|
||||
cmd := exec.Command(os.Args[0], "-test.run=^TestMultiInstanceWrapperHelper$")
|
||||
cmd.Env = append(os.Environ(), "NETSSH_TEST_MULTI_TARGET="+target, "PATH="+bin+":"+os.Getenv("PATH"))
|
||||
out, err := cmd.CombinedOutput()
|
||||
if want == "ambiguous" {
|
||||
if err == nil || !strings.Contains(string(out), "2 exact matches") {
|
||||
t.Fatalf("ambiguous connection accepted: %s, %v", out, err)
|
||||
}
|
||||
} else if err != nil || !strings.Contains(string(out), want+"\n") {
|
||||
t.Fatalf("wrong SSH destination for %s: %s, %v", target, out, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestQualifiedShortcutDoesNotMatchOtherInstance(t *testing.T) {
|
||||
cfg := config.ShortcutsConfig{Domains: []string{".example.com"}, StripHyphens: true}
|
||||
normalize := normalizeTarget(cfg, true)
|
||||
if normalize("office/web-01.example.com") != normalize("office/web01") {
|
||||
t.Fatal("qualified shortcut does not normalize")
|
||||
}
|
||||
if normalize("office/web01") == normalize("lab/web01") {
|
||||
t.Fatal("qualified shortcut lost instance")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefreshCachesIndependentDevicesAndVMs(t *testing.T) {
|
||||
office := connectionTestServer(t, []connectionTestHost{multiHost("router", "10.1.0.1")}, []connectionTestHost{multiHost("app", "10.1.0.2")})
|
||||
lab := connectionTestServer(t, []connectionTestHost{multiHost("switch", "10.2.0.1")}, []connectionTestHost{multiHost("test-vm", "10.2.0.2")})
|
||||
cfg := prepareInstancesTest(t, office.URL, lab.URL)
|
||||
cmd := rootCmd()
|
||||
cmd.SetArgs([]string{"cache", "refresh"})
|
||||
if err := cmd.Execute(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c := cache.New(cfg.CachePath(), cfg.Cache.TTL)
|
||||
if err := c.Load(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := map[string]struct{ kind, ip string }{
|
||||
"office/router": {"device", "10.1.0.1"}, "office/app": {"vm", "10.1.0.2"},
|
||||
"lab/switch": {"device", "10.2.0.1"}, "lab/test-vm": {"vm", "10.2.0.2"},
|
||||
}
|
||||
if len(c.All()) != len(want) {
|
||||
t.Fatalf("missing inventory entries: %+v", c.All())
|
||||
}
|
||||
for name, entry := range want {
|
||||
got, fresh := c.Get(name)
|
||||
if !fresh || got.Kind != entry.kind || got.IP != entry.ip {
|
||||
t.Fatalf("wrong cached host %s: %+v", name, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
+63
-37
@@ -23,7 +23,6 @@ import (
|
||||
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/notes"
|
||||
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/resolver"
|
||||
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/setup"
|
||||
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/shortcuts"
|
||||
internalssh "git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/ssh"
|
||||
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/tui"
|
||||
)
|
||||
@@ -66,7 +65,7 @@ func loadConfigOrSetup() *config.Config {
|
||||
if err != nil {
|
||||
fatalf("config: %v", err)
|
||||
}
|
||||
if cfg.NetBox.URL == "" {
|
||||
if !cfg.HasNetBox() {
|
||||
fmt.Fprintln(os.Stderr, "No configuration found. Starting setup…")
|
||||
if err := setup.RunWizard(cfg); err != nil {
|
||||
fatalf("setup: %v", err)
|
||||
@@ -103,12 +102,15 @@ func runSSHWrapper(args []string) {
|
||||
parsed.DestIdx += 2
|
||||
}
|
||||
|
||||
c := cache.New(cfg.Cache.Path, cfg.Cache.TTL)
|
||||
c := cache.New(cfg.CachePath(), cfg.Cache.TTL)
|
||||
_ = c.Load()
|
||||
maybeBackgroundRefresh(cfg, c)
|
||||
|
||||
source, _ := netbox.SplitTarget(parsed.Host)
|
||||
useCache := len(cfg.NetBoxes) == 0 || source != ""
|
||||
|
||||
// Cache hit with a fresh TTL — connect directly without querying NetBox.
|
||||
if entry, fresh := c.Get(parsed.Host); fresh {
|
||||
if entry, fresh := c.Get(parsed.Host); fresh && useCache {
|
||||
c.MarkUsed(parsed.Host)
|
||||
_ = c.Save()
|
||||
connect(entry.IP, parsed, args)
|
||||
@@ -119,8 +121,8 @@ func runSSHWrapper(args []string) {
|
||||
// If a stale match is found, use the canonical name for the NetBox re-fetch below.
|
||||
lookupHost := parsed.Host
|
||||
shortcutsEnabled := len(cfg.Shortcuts.Domains) > 0 || cfg.Shortcuts.StripHyphens
|
||||
if cached, _ := c.Get(lookupHost); shortcutsEnabled && cached.Name == "" {
|
||||
normalize := shortcuts.MakeNormalizer(cfg.Shortcuts)
|
||||
if cached, _ := c.Get(lookupHost); shortcutsEnabled && cached.Name == "" && useCache {
|
||||
normalize := normalizeTarget(cfg.Shortcuts, source != "")
|
||||
entry, found, fresh, err := c.GetByShortcut(lookupHost, normalize)
|
||||
if err != nil {
|
||||
fatalf("%v", err)
|
||||
@@ -137,19 +139,35 @@ func runSSHWrapper(args []string) {
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.NetBox.URL == "" {
|
||||
if !cfg.HasNetBox() {
|
||||
fatalf("netbox.url is not configured (~/.config/netssh.yaml)")
|
||||
}
|
||||
nbClient := netbox.NewClient(cfg.NetBox.URL, cfg.NetBox.Token, cfg.NetBox.TokenVersion)
|
||||
nbClient := newNetBoxClient(cfg)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
target, err := findSSHHost(ctx, nbClient, lookupHost, cfg.Shortcuts.Domains)
|
||||
if errors.Is(err, netbox.ErrHostNotFound) && !useCache && shortcutsEnabled {
|
||||
entry, found, _, cacheErr := c.GetByShortcut(parsed.Host, normalizeTarget(cfg.Shortcuts, false))
|
||||
if cacheErr != nil {
|
||||
fatalf("%v", cacheErr)
|
||||
}
|
||||
if found {
|
||||
target, err = findSSHHost(ctx, nbClient, entry.Name, cfg.Shortcuts.Domains)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
fatalf("NetBox: %v", err)
|
||||
}
|
||||
|
||||
if cached, fresh := c.Get(target.Target()); fresh && cached.Kind == target.Kind {
|
||||
c.MarkUsed(target.Target())
|
||||
_ = c.Save()
|
||||
connect(cached.IP, parsed, args)
|
||||
return
|
||||
}
|
||||
|
||||
chain, err := resolver.New(cfg.Resolver)
|
||||
if err != nil {
|
||||
fatalf("resolver: %v", err)
|
||||
@@ -160,8 +178,8 @@ func runSSHWrapper(args []string) {
|
||||
fatalf("IP resolution for %q: %v", parsed.Host, err)
|
||||
}
|
||||
|
||||
c.Upsert(cache.Entry{Name: target.Name, IP: ip, Kind: target.Kind, Tags: target.Tags})
|
||||
c.MarkUsed(target.Name)
|
||||
c.Upsert(cache.Entry{Name: target.Target(), IP: ip, Kind: target.Kind, Tags: target.Tags})
|
||||
c.MarkUsed(target.Target())
|
||||
_ = c.Save()
|
||||
|
||||
connect(ip, parsed, args)
|
||||
@@ -198,13 +216,13 @@ func connect(ip string, parsed *internalssh.ParsedArgs, originalArgs []string) {
|
||||
func runTUI() {
|
||||
cfg := loadConfigOrSetup()
|
||||
|
||||
c := cache.New(cfg.Cache.Path, cfg.Cache.TTL)
|
||||
c := cache.New(cfg.CachePath(), cfg.Cache.TTL)
|
||||
_ = c.Load()
|
||||
maybeBackgroundRefresh(cfg, c)
|
||||
|
||||
var nbClient *netbox.Client
|
||||
if cfg.NetBox.URL != "" {
|
||||
nbClient = netbox.NewClient(cfg.NetBox.URL, cfg.NetBox.Token, cfg.NetBox.TokenVersion)
|
||||
if cfg.HasNetBox() {
|
||||
nbClient = newNetBoxClient(cfg)
|
||||
}
|
||||
|
||||
m := tui.New(nbClient, c, cfg.SSH.DefaultUser, cfg.SSH.DefaultPort)
|
||||
@@ -245,7 +263,7 @@ func runTUI() {
|
||||
if err != nil {
|
||||
fatalf("IP resolution: %v", err)
|
||||
}
|
||||
c.Upsert(cache.Entry{Name: target.Name, IP: host.IP, Kind: target.Kind, Tags: target.Tags})
|
||||
c.Upsert(cache.Entry{Name: target.Target(), IP: host.IP, Kind: target.Kind, Tags: target.Tags})
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "Connecting to %s (%s)…\n", host.Name, host.IP)
|
||||
|
||||
@@ -290,7 +308,7 @@ func rootCmd() *cobra.Command {
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
}
|
||||
c := cache.New(cfg.Cache.Path, cfg.Cache.TTL)
|
||||
c := cache.New(cfg.CachePath(), cfg.Cache.TTL)
|
||||
_ = c.Load()
|
||||
|
||||
var completions []cobra.Completion
|
||||
@@ -403,9 +421,9 @@ func configureCmd() *cobra.Command {
|
||||
Use: "configure",
|
||||
Short: "Interactively configure netssh",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cfg, _ := config.Load()
|
||||
if cfg == nil {
|
||||
cfg = &config.Config{}
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return setup.RunWizard(cfg)
|
||||
},
|
||||
@@ -422,7 +440,7 @@ func searchCmd() *cobra.Command {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c := cache.New(cfg.Cache.Path, cfg.Cache.TTL)
|
||||
c := cache.New(cfg.CachePath(), cfg.Cache.TTL)
|
||||
if err := c.Load(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -456,7 +474,7 @@ func cacheListCmd() *cobra.Command {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c := cache.New(cfg.Cache.Path, cfg.Cache.TTL)
|
||||
c := cache.New(cfg.CachePath(), cfg.Cache.TTL)
|
||||
if err := c.Load(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -482,7 +500,7 @@ func cacheClearCmd() *cobra.Command {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c := cache.New(cfg.Cache.Path, cfg.Cache.TTL)
|
||||
c := cache.New(cfg.CachePath(), cfg.Cache.TTL)
|
||||
c.Clear()
|
||||
if err := c.Save(); err != nil {
|
||||
return err
|
||||
@@ -494,7 +512,7 @@ func cacheClearCmd() *cobra.Command {
|
||||
}
|
||||
|
||||
func cacheRefreshCmd() *cobra.Command {
|
||||
var filterTag, filterKind string
|
||||
var filterTag, filterKind, filterInstance string
|
||||
cmd := &cobra.Command{
|
||||
Use: "refresh",
|
||||
Short: "Re-fetch all known hosts from NetBox",
|
||||
@@ -503,21 +521,28 @@ func cacheRefreshCmd() *cobra.Command {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cfg.NetBox.URL == "" {
|
||||
if !cfg.HasNetBox() {
|
||||
return fmt.Errorf("netbox.url is not configured")
|
||||
}
|
||||
|
||||
nbClient := netbox.NewClient(cfg.NetBox.URL, cfg.NetBox.Token, cfg.NetBox.TokenVersion)
|
||||
nbClient := newNetBoxClient(cfg)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
opts := netbox.SearchOptions{Tag: filterTag, Kind: filterKind}
|
||||
entries, err := nbClient.SearchAll(ctx, "", opts)
|
||||
query := ""
|
||||
if filterInstance != "" {
|
||||
if !nbClient.MultipleSources() {
|
||||
return fmt.Errorf("--instance requires named netboxes configuration")
|
||||
}
|
||||
query = filterInstance + "/"
|
||||
}
|
||||
entries, err := nbClient.SearchAll(ctx, query, opts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("NetBox: %w", err)
|
||||
}
|
||||
|
||||
c := cache.New(cfg.Cache.Path, cfg.Cache.TTL)
|
||||
c := cache.New(cfg.CachePath(), cfg.Cache.TTL)
|
||||
_ = c.Load()
|
||||
|
||||
chain, err := resolver.New(cfg.Resolver)
|
||||
@@ -527,12 +552,12 @@ func cacheRefreshCmd() *cobra.Command {
|
||||
written := 0
|
||||
nameCounts := make(map[string]int)
|
||||
for _, e := range entries {
|
||||
nameCounts[strings.ToLower(e.Name)]++
|
||||
nameCounts[strings.ToLower(e.Target())]++
|
||||
}
|
||||
for i := range entries {
|
||||
e := &entries[i]
|
||||
if nameCounts[strings.ToLower(e.Name)] > 1 {
|
||||
c.Delete(e.Name)
|
||||
if nameCounts[strings.ToLower(e.Target())] > 1 {
|
||||
c.Delete(e.Target())
|
||||
fmt.Fprintf(os.Stderr, "warning: skipping ambiguous hostname %q\n", e.Name)
|
||||
continue
|
||||
}
|
||||
@@ -544,18 +569,18 @@ func cacheRefreshCmd() *cobra.Command {
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
c.Delete(e.Name)
|
||||
c.Delete(e.Target())
|
||||
fmt.Fprintf(os.Stderr, "warning: skipping %s: %v\n", e.Name, err)
|
||||
continue
|
||||
}
|
||||
c.Upsert(cache.Entry{Name: e.Name, IP: ip, Kind: e.Kind, Tags: e.Tags})
|
||||
c.Upsert(cache.Entry{Name: e.Target(), IP: ip, Kind: e.Kind, Tags: e.Tags})
|
||||
written++
|
||||
}
|
||||
|
||||
if err := c.Save(); err != nil {
|
||||
return err
|
||||
}
|
||||
if filterTag == "" && filterKind == "" {
|
||||
if filterTag == "" && filterKind == "" && filterInstance == "" {
|
||||
if err := c.SetRefreshed(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "warning: could not update refresh timestamp: %v\n", err)
|
||||
}
|
||||
@@ -571,6 +596,7 @@ func cacheRefreshCmd() *cobra.Command {
|
||||
}
|
||||
cmd.Flags().StringVar(&filterTag, "tag", "", "Filter by NetBox tag slug (e.g. prod)")
|
||||
cmd.Flags().StringVar(&filterKind, "kind", "", "Filter by kind: device or vm")
|
||||
cmd.Flags().StringVar(&filterInstance, "instance", "", "Refresh only this named NetBox instance")
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -599,7 +625,7 @@ Or use in a script:
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c := cache.New(cfg.Cache.Path, cfg.Cache.TTL)
|
||||
c := cache.New(cfg.CachePath(), cfg.Cache.TTL)
|
||||
if err := c.Load(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -644,7 +670,7 @@ func shellInitCmd() *cobra.Command {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
c := cache.New(cfg.Cache.Path, cfg.Cache.TTL)
|
||||
c := cache.New(cfg.CachePath(), cfg.Cache.TTL)
|
||||
_ = c.Load()
|
||||
|
||||
// Regenerate alias files from the current cache every shell start
|
||||
@@ -705,8 +731,8 @@ After installation, reload your profile or open a new shell.`,
|
||||
|
||||
// Generate the aliases file immediately so it is ready on the next shell start.
|
||||
cfg, cfgErr := config.Load()
|
||||
if cfgErr == nil && cfg.NetBox.URL != "" {
|
||||
c := cache.New(cfg.Cache.Path, cfg.Cache.TTL)
|
||||
if cfgErr == nil && cfg.HasNetBox() {
|
||||
c := cache.New(cfg.CachePath(), cfg.Cache.TTL)
|
||||
if _ = c.Load(); true {
|
||||
if entries := buildAliasEntries(c, *cfg); len(entries) > 0 {
|
||||
if err := hook.WriteAliasFiles(entries); err != nil {
|
||||
@@ -758,7 +784,7 @@ func hookUninstallCmd() *cobra.Command {
|
||||
// maybeBackgroundRefresh starts a background `netssh cache refresh` if the cache
|
||||
// has not been fully refreshed within the last 24 hours.
|
||||
func maybeBackgroundRefresh(cfg *config.Config, c *cache.Cache) {
|
||||
if cfg.NetBox.URL == "" {
|
||||
if !cfg.HasNetBox() {
|
||||
return
|
||||
}
|
||||
if !c.NeedsRefresh(24 * time.Hour) {
|
||||
|
||||
+17
-7
@@ -34,11 +34,11 @@ func notesCmd() *cobra.Command {
|
||||
return err
|
||||
}
|
||||
var client *netbox.Client
|
||||
if cfg.NetBox.URL != "" {
|
||||
client = netbox.NewClient(cfg.NetBox.URL, cfg.NetBox.Token, cfg.NetBox.TokenVersion)
|
||||
if cfg.HasNetBox() {
|
||||
client = newNetBoxClient(cfg)
|
||||
}
|
||||
host := netbox.HostEntry{Name: args[0], Kind: kind}
|
||||
c := cache.New(cfg.Cache.Path, cfg.Cache.TTL)
|
||||
c := cache.New(cfg.CachePath(), cfg.Cache.TTL)
|
||||
if err := c.Load(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -46,13 +46,23 @@ func notesCmd() *cobra.Command {
|
||||
if !remote {
|
||||
cached = c.All()
|
||||
}
|
||||
query := host.Name
|
||||
source, rawName := netbox.SplitTarget(query)
|
||||
var matches []cache.Entry
|
||||
for _, entry := range cached {
|
||||
if strings.EqualFold(entry.Name, host.Name) && (kind == "" || kind == entry.Kind) {
|
||||
host.Name = entry.Name
|
||||
host.Kind = entry.Kind
|
||||
break
|
||||
_, entryName := netbox.SplitTarget(entry.Name)
|
||||
if (kind == "" || kind == entry.Kind) && (strings.EqualFold(entry.Name, query) || (source == "" && strings.EqualFold(entryName, rawName))) {
|
||||
matches = append(matches, entry)
|
||||
}
|
||||
}
|
||||
if len(matches) > 1 {
|
||||
return fmt.Errorf("ambiguous hostname %q; use instance/hostname", query)
|
||||
}
|
||||
if len(matches) == 1 {
|
||||
host.Name = matches[0].Name
|
||||
host.Kind = matches[0].Kind
|
||||
}
|
||||
|
||||
if !remote && host.Kind == "" {
|
||||
return fmt.Errorf("host is not cached; specify --kind device or --kind vm")
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user