netssh multi opens one pane per host in a tmux window or zellij tab. Hosts come from names, tag:TAG (NetBox and static hosts), cluster:NAME (NetBox virtualization clusters; devices by default, --kind vm|all), and groups from the config (@name, nested, with cycle detection). Every pane runs netssh HOST, so lookup, rules, and jump hosts apply. - tmux: a new window inside tmux, else a new session that is attached; tiled layout, pane borders with the host and a [SYNC] marker. - zellij: a generated grid layout; a new tab inside zellij, else a new session. - --sync broadcasts typing to all panes; netssh sync toggles it in the current tmux window or zellij tab. - TUI: Space marks hosts, Ctrl+A marks all, Enter opens them together. - Configurator: multiplexer, panes per window, groups. NetBox SearchAll gains a cluster filter (name to cluster IDs per instance), and cache entries keep the host's cluster for completion and an offline fallback. Panes skip the 24-hour background refresh (checked once by multi) and get HOME/XDG paths but no NETSSH_* values, which would expose tokens in the process list. testenv/tests/15-multi.sh checks real tmux and zellij sessions, including a broadcast command arriving on all three cluster nodes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
52 lines
1.7 KiB
Go
52 lines
1.7 KiB
Go
package netbox
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
)
|
|
|
|
func TestSearchAllByCluster(t *testing.T) {
|
|
var deviceQuery, vmQuery string
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
switch r.URL.Path {
|
|
case "/api/virtualization/clusters/":
|
|
if r.URL.Query().Get("name__ie") == "PVE" {
|
|
fmt.Fprint(w, `{"count":2,"results":[{"id":3,"name":"pve"},{"id":4,"name":"pve"}]}`)
|
|
return
|
|
}
|
|
fmt.Fprint(w, `{"count":0,"results":[]}`)
|
|
case "/api/dcim/devices/":
|
|
deviceQuery = r.URL.RawQuery
|
|
fmt.Fprint(w, `{"count":1,"results":[{"id":1,"name":"node1","cluster":{"id":3,"name":"pve"}}]}`)
|
|
case "/api/virtualization/virtual-machines/":
|
|
vmQuery = r.URL.RawQuery
|
|
fmt.Fprint(w, `{"count":1,"results":[{"id":2,"name":"vm1","cluster":{"id":4,"name":"pve"}}]}`)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
c := NewClient(srv.URL, "t", 1)
|
|
|
|
hosts, err := c.SearchAll(context.Background(), "", SearchOptions{Cluster: "PVE", Kind: "device"})
|
|
if err != nil || len(hosts) != 1 || hosts[0].Name != "node1" || hosts[0].Cluster != "pve" {
|
|
t.Fatalf("got %+v %v", hosts, err)
|
|
}
|
|
if want := "name__ic=&limit=50&offset=0&cluster_id=3&cluster_id=4"; deviceQuery != want {
|
|
t.Errorf("device query %q, want %q", deviceQuery, want)
|
|
}
|
|
if vmQuery != "" {
|
|
t.Error("VMs queried for kind device")
|
|
}
|
|
all, err := c.SearchAll(context.Background(), "", SearchOptions{Cluster: "PVE"})
|
|
if err != nil || len(all) != 2 {
|
|
t.Fatalf("all kinds: %+v %v", all, err)
|
|
}
|
|
none, err := c.SearchAll(context.Background(), "", SearchOptions{Cluster: "missing"})
|
|
if err != nil || len(none) != 0 {
|
|
t.Fatalf("unknown cluster: %+v %v", none, err)
|
|
}
|
|
}
|