feat: netssh multi — several hosts side by side in tmux or zellij
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>
This commit is contained in:
1 parent
dbb99bdded
commit
a31aab4110
24 files changed
+1754
-40
No files matched your search
@@ -0,0 +1,74 @@
|
||||
# netssh multi with real tmux and zellij: selectors against NetBox, panes,
|
||||
# broadcast typing reaching every host, netssh sync, zellij layouts and tabs.
|
||||
config <<'EOF'
|
||||
resolver:
|
||||
strategies: [management_subnet, primary_ip]
|
||||
management_subnets: [10.231.1.0/24]
|
||||
ssh:
|
||||
default_user: ops
|
||||
rules:
|
||||
- match: {kind: vm, instance: fsn}
|
||||
proxy_jump: ops@10.231.0.30
|
||||
groups:
|
||||
pve: ["cluster:pve-fsn1"]
|
||||
mixed: ["@pve", "lab/lab-db1", "tag:web"]
|
||||
EOF
|
||||
check "refresh" netssh cache refresh
|
||||
nodes="fsn/fsn1-pve1.hoster.test fsn/fsn1-pve2.hoster.test fsn/fsn1-pve3.hoster.test"
|
||||
expect_out "cluster: selects the devices" "# 3 hosts: $nodes" netssh multi --dry-run --tmux cluster:pve-fsn1
|
||||
expect_out "--kind vm selects the VMs" "# 2 hosts: fsn/web1 fsn/web2" netssh multi --dry-run --tmux --kind vm cluster:pve-fsn1
|
||||
expect_out "groups, tags, and hosts combined" "# 7 hosts: $nodes lab/lab-db1 fsn/web1 fsn/web2 lab/web1" netssh multi --dry-run --tmux @mixed
|
||||
expect_fail "unknown cluster" "matches no hosts" netssh multi --dry-run --tmux cluster:nope
|
||||
expect_out "completion offers clusters" "cluster:pve-fsn1" netssh __complete multi cluster:
|
||||
expect_out "completion offers groups" "@pve" netssh __complete multi @
|
||||
|
||||
export TMUX_TMPDIR="$HOME/tmux"
|
||||
mkdir -p "$TMUX_TMPDIR" "$HOME/.config/zellij"
|
||||
# No first-start tips or release notes on top of the panes.
|
||||
printf 'show_startup_tips false\nshow_release_notes false\n' >"$HOME/.config/zellij/config.kdl"
|
||||
envs=(-e HOME="$HOME" -e XDG_CONFIG_HOME="$XDG_CONFIG_HOME" -e XDG_CACHE_HOME="$XDG_CACHE_HOME")
|
||||
hosts_of() { tmux list-panes -t "$1" -F '#{@netssh_host}' 2>/dev/null | sort | tr '\n' ' '; }
|
||||
# wait_for retries a command, so checks must re-evaluate on every call.
|
||||
panes_in() { [ "$(tmux list-panes -t "$1" 2>/dev/null | wc -l)" = "$2" ]; }
|
||||
sync_is() { [ "$(tmux show-window-options -t "$1" -v synchronize-panes 2>/dev/null)" = "$2" ]; }
|
||||
prompts() { [ "$(tmux capture-pane -p -t z | grep -o 'fsn1-pve[123]:~\$' | sort -u | wc -l)" = 3 ]; }
|
||||
remote_file() { for n in 1 2 3; do netssh "fsn/fsn1-pve$n.hoster.test" test -f "$1" || return 1; done; }
|
||||
|
||||
# Inside tmux: a new window in the current session, broadcast from the start.
|
||||
tmux new-session -d -s outer -x 200 -y 60 "${envs[@]}" bash
|
||||
tmux send-keys -t outer "netssh multi --sync @pve" Enter
|
||||
if wait_for 20 panes_in outer:pve 3; then ok "tmux: three panes in a new window"; else bad "tmux: three panes in a new window" "$(tmux list-windows -a)"; fi
|
||||
expect_eq "tmux: panes are labelled" "$nodes " hosts_of outer:pve
|
||||
expect_eq "tmux: broadcast is on" "on" tmux show-window-options -t outer:pve -v synchronize-panes
|
||||
sleep 2
|
||||
marker="/tmp/netssh-multi-$$"
|
||||
tmux send-keys -t outer:pve "touch $marker" Enter
|
||||
if wait_for 15 remote_file "$marker"; then ok "tmux: one command reached all three hosts"; else bad "tmux: one command reached all three hosts" "$(tmux capture-pane -p -t outer:pve)"; fi
|
||||
|
||||
tmux new-window -t outer -n ctl "${envs[@]}" bash
|
||||
tmux send-keys -t outer:ctl "netssh sync" Enter
|
||||
if wait_for 10 sync_is outer:ctl on; then ok "netssh sync toggles broadcast"; else bad "netssh sync toggles broadcast" "$(tmux capture-pane -p -t outer:ctl)"; fi
|
||||
|
||||
# Outside tmux: a new session, attached.
|
||||
tmux send-keys -t outer:ctl "netssh sync; env -u TMUX netssh multi --kind vm cluster:pve-fsn1" Enter
|
||||
if wait_for 20 panes_in netssh-pve-fsn1 2; then ok "tmux: new session outside tmux"; else bad "tmux: new session outside tmux" "$(tmux list-sessions)"; fi
|
||||
expect_eq "tmux: VMs through the bastion" "fsn/web1 fsn/web2 " hosts_of netssh-pve-fsn1
|
||||
tmux kill-server
|
||||
|
||||
# zellij outside a session: netssh starts zellij with a grid layout.
|
||||
tmux new-session -d -s z -x 200 -y 60 "${envs[@]}" bash
|
||||
tmux send-keys -t z "netssh multi --zellij @pve" Enter
|
||||
zscreen() { tmux capture-pane -p -t z; }
|
||||
if wait_for 30 prompts; then ok "zellij: three panes with remote shells"; else bad "zellij: three panes with remote shells" "$(zscreen)"; fi
|
||||
expect_out "zellij: pane titles" "fsn/fsn1-pve2.hoster.test" zscreen
|
||||
zsession() { zellij list-sessions --short --no-formatting 2>/dev/null | head -1 | grep .; }
|
||||
if wait_for 10 zsession; then ok "zellij: session is running"; else bad "zellij: session is running" "$(zellij list-sessions 2>&1)"; fi
|
||||
session=$(zsession)
|
||||
|
||||
# zellij inside that session: netssh multi adds a tab.
|
||||
ZELLIJ_SESSION_NAME=$session ZELLIJ=0 netssh multi --kind vm cluster:pve-fsn1 >/dev/null 2>&1
|
||||
expect_out "zellij: new tab inside a session" "pve-fsn1" zellij --session "$session" action query-tab-names
|
||||
expect_out "zellij: tab layout runs netssh for the VMs" "fsn/web2" zellij --session "$session" action dump-layout
|
||||
zellij kill-all-sessions --yes >/dev/null 2>&1
|
||||
tmux kill-server 2>/dev/null
|
||||
true
|
||||
Reference in new issue
Block a user