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,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user