test: add a local Docker test environment for all features
testenv/ starts two real NetBox 4.7 instances (v2 and v1 token), SSH targets on public, mgmt, and prod networks, a jump host, an IPv6-only host, a static host, and a client with ssh, sftp, scp, tmux, zellij, bash, zsh, and fish. A seed script fills NetBox through the REST API. 141 end-to-end checks cover cache and refresh filters, connecting, all resolver strategies, multiple instances, shortcuts and rewrites, SSH options and rules, static hosts, sftp/scp incl. IPv6 and jump hosts, tunnels, local notes and NetBox comments, completion and shell hooks, the TUI (driven through tmux), the configurator, and uninstall. Local only: testenv/up.sh, test.sh, shell.sh, down.sh. Not part of CI/CD. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
55e7a5106b
commit
dbb99bdded
34 files changed
+1215
No files matched your search
@@ -0,0 +1,19 @@
|
||||
# Cache refresh across both NetBox instances, filters, list, search, clear.
|
||||
config <<'EOF'
|
||||
resolver:
|
||||
strategies: [management_subnet, primary_ip]
|
||||
management_subnets: [10.231.1.0/24]
|
||||
EOF
|
||||
|
||||
expect_out "refresh both instances" "9 entries written" netssh cache refresh
|
||||
expect_out "list shows mgmt address" "fsn/fsn1-pve1.hoster.test 10.231.1.11" netssh cache list
|
||||
expect_out "list shows lab VM" "lab/web1 10.231.1.42 vm" netssh cache list
|
||||
expect_out "IPv6-only host" "fsn/v6-host fd00:30::60" netssh cache list
|
||||
expect_out "refresh one instance" "2 entries written" netssh cache refresh --instance lab
|
||||
expect_out "refresh by tag" "3 entries written" netssh cache refresh --tag pve
|
||||
expect_out "refresh by kind" "3 entries written" netssh cache refresh --kind vm
|
||||
expect_eq "search" "fsn/fsn1-pve1.hoster.test
|
||||
fsn/fsn1-pve2.hoster.test
|
||||
fsn/fsn1-pve3.hoster.test" bash -c 'netssh search fsn1 | sort'
|
||||
check "clear" netssh cache clear
|
||||
expect_eq "empty after clear" "" bash -c 'netssh search fsn'
|
||||
@@ -0,0 +1,29 @@
|
||||
# Connecting with the resolved address, default user and port, overrides.
|
||||
config <<'EOF'
|
||||
resolver:
|
||||
strategies: [management_subnet, primary_ip]
|
||||
management_subnets: [10.231.1.0/24]
|
||||
ssh:
|
||||
default_user: ops
|
||||
EOF
|
||||
|
||||
expect_eq "connect via NetBox" "fsn1-pve1" netssh fsn/fsn1-pve1.hoster.test hostname
|
||||
expect_eq "unique name without instance" "fsn1-pve2" netssh fsn1-pve2.hoster.test hostname
|
||||
expect_eq "default user" "ops" netssh fsn/fsn1-pve1.hoster.test whoami
|
||||
expect_eq "user@ wins" "admin" netssh admin@fsn/fsn1-pve1.hoster.test whoami
|
||||
expect_eq "-l wins" "admin" netssh -l admin fsn/fsn1-pve1.hoster.test whoami
|
||||
expect_eq "remote command with arguments" "a b" netssh fsn/fsn1-pve1.hoster.test echo a b
|
||||
expect_eq "dry run" "ssh -l ops 10.231.1.11" netssh --dry-run fsn/fsn1-pve1.hoster.test
|
||||
|
||||
config <<'EOF'
|
||||
resolver:
|
||||
strategies: [management_subnet, primary_ip]
|
||||
management_subnets: [10.231.1.0/24]
|
||||
ssh:
|
||||
default_user: ops
|
||||
default_port: 2222
|
||||
EOF
|
||||
expect_out "default port" " 2222" netssh fsn/fsn1-pve1.hoster.test 'echo $SSH_CONNECTION'
|
||||
expect_out "-p wins over default port" " 22" netssh -p 22 fsn/fsn1-pve1.hoster.test 'echo $SSH_CONNECTION'
|
||||
expect_eq "dry run with port" "ssh -p 2222 -l ops 10.231.1.11" netssh --dry-run fsn/fsn1-pve1.hoster.test
|
||||
expect_eq "version" "netssh" bash -c 'netssh --version | cut -d" " -f1'
|
||||
@@ -0,0 +1,63 @@
|
||||
# Every resolver strategy against real NetBox data. cache.ttl 0 makes every
|
||||
# connection ask NetBox, so each config change takes effect immediately.
|
||||
jump=' - match: {instance: fsn}
|
||||
proxy_jump: ops@10.231.0.30
|
||||
- match: {name: "bastion*"}
|
||||
proxy_jump: none'
|
||||
|
||||
config <<EOF
|
||||
cache: {ttl: 0}
|
||||
resolver: {strategies: [primary_ip]}
|
||||
ssh:
|
||||
default_user: ops
|
||||
rules:
|
||||
$jump
|
||||
EOF
|
||||
expect_eq "primary_ip uses prod address via bastion" "ssh -l ops -J ops@10.231.0.30 10.231.2.11" netssh --dry-run fsn/fsn1-pve1.hoster.test
|
||||
expect_eq "primary_ip connects through the jump host" "fsn1-pve1" netssh fsn/fsn1-pve1.hoster.test hostname
|
||||
expect_eq "bastion rule disables the jump" "ssh -l ops 10.231.0.30" netssh --dry-run fsn/bastion.hoster.test
|
||||
expect_eq "IPv6 primary address" "ssh -l ops -J ops@10.231.0.30 fd00:30::60" netssh --dry-run fsn/v6-host
|
||||
|
||||
config <<'EOF'
|
||||
cache: {ttl: 0}
|
||||
resolver:
|
||||
strategies: [management_subnet, primary_ip]
|
||||
management_subnets: [10.231.1.0/24]
|
||||
ssh: {default_user: ops}
|
||||
EOF
|
||||
expect_eq "management_subnet" "ssh -l ops 10.231.1.12" netssh --dry-run fsn/fsn1-pve2.hoster.test
|
||||
expect_eq "management_subnet falls back to primary_ip" "ssh -l ops 10.231.2.21" netssh --dry-run fsn/web1
|
||||
|
||||
config <<'EOF'
|
||||
cache: {ttl: 0}
|
||||
resolver:
|
||||
strategies: [interface_name, primary_ip]
|
||||
interface_name: mgmt0
|
||||
ssh: {default_user: ops}
|
||||
EOF
|
||||
expect_eq "interface_name" "ssh -l ops 10.231.1.13" netssh --dry-run fsn/fsn1-pve3.hoster.test
|
||||
expect_eq "interface_name connects" "fsn1-pve3" netssh fsn/fsn1-pve3.hoster.test hostname
|
||||
expect_eq "interface_name falls back for VMs" "ssh -l ops 10.231.2.22" netssh --dry-run fsn/web2
|
||||
|
||||
# eth0 holds the prod address, which sorts after mgmt: catches an ignored filter.
|
||||
sed -i 's/interface_name: mgmt0/interface_name: eth0/' "$XDG_CONFIG_HOME/netssh.yaml"
|
||||
expect_eq "interface_name picks the named interface" "ssh -l ops 10.231.2.13" netssh --dry-run fsn/fsn1-pve3.hoster.test
|
||||
|
||||
config <<'EOF'
|
||||
cache: {ttl: 0}
|
||||
resolver:
|
||||
strategies: [hostname]
|
||||
hostname_domain: example.org
|
||||
ssh: {default_user: ops}
|
||||
EOF
|
||||
expect_eq "hostname strategy" "ssh -l ops fsn1-pve1.hoster.test" netssh --dry-run fsn/fsn1-pve1.hoster.test
|
||||
expect_eq "hostname connects via DNS" "fsn1-pve1" netssh fsn/fsn1-pve1.hoster.test hostname
|
||||
expect_eq "hostname domain for names without a dot" "ssh -l ops lab-db1.example.org" netssh --dry-run lab/lab-db1
|
||||
|
||||
config <<'EOF'
|
||||
cache: {ttl: 0}
|
||||
resolver:
|
||||
strategies: [interface_name]
|
||||
interface_name: mgmt0
|
||||
EOF
|
||||
expect_fail "no strategy finds an address" "no strategy resolved an IP" netssh --dry-run fsn/web1
|
||||
@@ -0,0 +1,20 @@
|
||||
# Two NetBox instances: qualified names, ambiguity, an unreachable instance.
|
||||
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
|
||||
EOF
|
||||
expect_fail "same name in both instances is ambiguous" "found 2 exact matches" netssh --dry-run web1
|
||||
expect_eq "fsn/web1 through the bastion" "web1" netssh fsn/web1 hostname
|
||||
expect_eq "lab/web1 directly" "lab-web1" netssh lab/web1 hostname
|
||||
expect_eq "lab uses its v1 token" "lab-db1" netssh lab/lab-db1 hostname
|
||||
|
||||
# lab unreachable: qualified fsn targets still work, unqualified ones need every instance.
|
||||
sed -i 's#http://10.231.0.21:8080#http://10.231.0.21:9#' "$XDG_CONFIG_HOME/netssh.yaml"
|
||||
expect_eq "qualified target with another instance down" "fsn1-pve1" netssh fsn/fsn1-pve1.hoster.test hostname
|
||||
expect_fail "unqualified target needs every instance" "10.231.0.21:9" netssh --dry-run fsn1-pve1.hoster.test
|
||||
@@ -0,0 +1,31 @@
|
||||
# Domains, hyphen folding, rewrite rules, alias --test, completion, aliases.
|
||||
config <<'EOF'
|
||||
resolver:
|
||||
strategies: [management_subnet, primary_ip]
|
||||
management_subnets: [10.231.1.0/24]
|
||||
ssh: {default_user: ops}
|
||||
shortcuts:
|
||||
domains: [.hoster.test]
|
||||
rewrites:
|
||||
- pattern: '^([a-z]+)[0-9]+-'
|
||||
replace: '$1'
|
||||
EOF
|
||||
check "refresh" netssh cache refresh
|
||||
expect_eq "domain expansion via NetBox" "fsn1-pve1" netssh fsn/fsn1-pve1 hostname
|
||||
expect_eq "rewrite shortcut, qualified" "fsn1-pve2" netssh fsn/fsnpve2 hostname
|
||||
expect_eq "rewrite shortcut, unqualified" "fsn1-pve3" netssh fsnpve3 hostname
|
||||
expect_out "alias --test" "fsn1-pve1.hoster.test → fsnpve1" netssh alias --test fsn1-pve1.hoster.test
|
||||
expect_out "completion offers shortcuts" "fsnpve1" netssh __complete fsnp
|
||||
expect_out "aliases use the shortcut" "alias fsn_fsnpve1='netssh " netssh alias --shell bash
|
||||
expect_fail "unknown shortcut" "host not found" netssh --dry-run fsnpve9
|
||||
|
||||
config <<'EOF'
|
||||
resolver:
|
||||
strategies: [management_subnet, primary_ip]
|
||||
management_subnets: [10.231.1.0/24]
|
||||
shortcuts:
|
||||
domains: [.hoster.test]
|
||||
strip_hyphens: true
|
||||
EOF
|
||||
check "refresh" netssh cache refresh
|
||||
expect_eq "hyphen folding" "ssh 10.231.1.12" netssh --dry-run fsn/fsn1pve2
|
||||
@@ -0,0 +1,34 @@
|
||||
# Jump hosts, rules, host key checking, connection target, options, extra args.
|
||||
known="$HOME/known_hosts"
|
||||
config <<EOF
|
||||
cache: {ttl: 0}
|
||||
resolver: {strategies: [primary_ip]}
|
||||
ssh:
|
||||
default_user: ops
|
||||
options:
|
||||
UserKnownHostsFile: $known
|
||||
ServerAliveInterval: "30"
|
||||
extra_args: ["-T"]
|
||||
rules:
|
||||
- match: {tag: web}
|
||||
proxy_jump: ops@10.231.0.30
|
||||
- match: {tag: pve}
|
||||
proxy_jump: ops@10.231.0.30
|
||||
host_key_checking: strict
|
||||
- match: {name: "fsn1-pve2*"}
|
||||
host_key_checking: accept-new
|
||||
- match: {name: "fsn1-pve3*"}
|
||||
host_key_checking: "off"
|
||||
target: name
|
||||
EOF
|
||||
expect_eq "rule by tag adds jump host" "ssh -T -l ops -J ops@10.231.0.30 -o serveraliveinterval=30 -o userknownhostsfile=$known 10.231.2.21" netssh --dry-run fsn/web1
|
||||
expect_eq "connect through rule jump" "web1" netssh fsn/web1 hostname
|
||||
expect_out "--no-jump removes it" "-o ProxyJump=none" netssh --dry-run --no-jump fsn/web1
|
||||
expect_out "--jump overrides it" "-J ops@10.231.0.30" netssh --dry-run --jump ops@10.231.0.30 fsn/v6-host
|
||||
expect_fail "strict rejects an unknown host key" "Host key verification failed" netssh fsn/fsn1-pve1.hoster.test hostname
|
||||
expect_eq "later rule accept-new wins" "fsn1-pve2" netssh fsn/fsn1-pve2.hoster.test hostname
|
||||
expect_out "accept-new stored the key" "10.231.2.12" cat "$known"
|
||||
expect_out "off prints a notice" "host key checking is disabled" netssh fsn/fsn1-pve3.hoster.test hostname
|
||||
expect_out "target name keeps the address" "-o HostName=10.231.2.13 -o HostKeyAlias=fsn1-pve3.hoster.test" netssh --dry-run fsn/fsn1-pve3.hoster.test
|
||||
expect_out "target name destination" " fsn1-pve3.hoster.test" netssh --dry-run fsn/fsn1-pve3.hoster.test
|
||||
expect_out "user -o wins over config" "-o ServerAliveInterval=5" netssh --dry-run -o ServerAliveInterval=5 fsn/web1
|
||||
@@ -0,0 +1,25 @@
|
||||
# Static hosts without any NetBox: config, CLI, own user/port/options, rules.
|
||||
config_only <<'EOF'
|
||||
ssh:
|
||||
default_user: ops
|
||||
rules:
|
||||
- match: {kind: static, tag: legacy}
|
||||
options: {ServerAliveInterval: "15"}
|
||||
hosts:
|
||||
- name: legacy-box
|
||||
address: 10.231.0.50
|
||||
user: admin
|
||||
tags: [legacy]
|
||||
EOF
|
||||
expect_eq "connect to a static host" "legacy-box" netssh legacy-box hostname
|
||||
expect_eq "static user" "admin" netssh legacy-box whoami
|
||||
expect_out "rule by kind and tag" "-o serveraliveinterval=15" netssh --dry-run legacy-box
|
||||
expect_eq "-l still wins" "ops" netssh -l ops legacy-box whoami
|
||||
check "hosts add" netssh hosts add box2 10.231.0.50 --port 2222 --tag lab
|
||||
expect_eq "hosts list" "box2 10.231.0.50 2222 lab" bash -c 'netssh hosts list | grep "^box2" | tr -s " "'
|
||||
expect_out "static port" " 2222" netssh box2 'echo $SSH_CONNECTION'
|
||||
expect_out "completion" "box2" netssh __complete bo
|
||||
expect_fail "duplicate" "already exists" netssh hosts add box2 10.0.0.1
|
||||
check "hosts remove" netssh hosts remove box2
|
||||
expect_fail "removed host is gone" "no NetBox is configured" netssh --dry-run box2
|
||||
expect_eq "sftp to a static host" "Remote working directory: /home/admin" bash -c 'echo pwd | netssh sftp -b - legacy-box | grep Remote'
|
||||
@@ -0,0 +1,24 @@
|
||||
# sftp and scp: upload, download, jump host, IPv6, errors.
|
||||
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
|
||||
EOF
|
||||
echo "payload $$" >"$HOME/up.txt"
|
||||
check "scp upload" netssh scp "$HOME/up.txt" fsn/fsn1-pve1.hoster.test:/tmp/up.txt
|
||||
expect_eq "uploaded content" "payload $$" netssh fsn/fsn1-pve1.hoster.test cat /tmp/up.txt
|
||||
check "scp download" netssh scp fsn/fsn1-pve1.hoster.test:/tmp/up.txt "$HOME/down.txt"
|
||||
expect_eq "downloaded content" "payload $$" cat "$HOME/down.txt"
|
||||
check "scp to a VM through the bastion" netssh scp "$HOME/up.txt" fsn/web1:/tmp/up.txt
|
||||
expect_eq "VM content" "payload $$" netssh fsn/web1 cat /tmp/up.txt
|
||||
check "scp to IPv6 host" netssh scp "$HOME/up.txt" fsn/v6-host:/tmp/v6.txt
|
||||
expect_eq "IPv6 content" "payload $$" netssh fsn/v6-host cat /tmp/v6.txt
|
||||
expect_out "sftp starts in the path" "Remote working directory: /tmp" bash -c 'echo pwd | netssh sftp -b - fsn/fsn1-pve2.hoster.test:/tmp'
|
||||
expect_out "sftp to IPv6 host" "v6.txt" bash -c 'echo "ls /tmp" | netssh sftp -b - fsn/v6-host'
|
||||
expect_out "sftp through the bastion" "up.txt" bash -c 'echo "ls /tmp" | netssh sftp -b - fsn/web1'
|
||||
expect_fail "two remote hosts" "not supported" netssh scp fsn/web1:/a fsn/web2:/b
|
||||
@@ -0,0 +1,40 @@
|
||||
# Port forwards: named, ad hoc, --forward with a shell, port in use.
|
||||
config <<'EOF'
|
||||
resolver:
|
||||
strategies: [management_subnet, primary_ip]
|
||||
management_subnets: [10.231.1.0/24]
|
||||
ssh:
|
||||
default_user: ops
|
||||
forwards:
|
||||
web: {local: "18081:127.0.0.1:8081"}
|
||||
rules:
|
||||
- match: {kind: vm, instance: fsn}
|
||||
proxy_jump: ops@10.231.0.30
|
||||
EOF
|
||||
tunnel_test() { # NAME PORT WANT ARGS...
|
||||
local name=$1 port=$2 want=$3 pid out
|
||||
shift 3
|
||||
"$@" >"$HOME/tunnel.log" 2>&1 &
|
||||
pid=$!
|
||||
if wait_for 15 curl -sf "http://127.0.0.1:$port/"; then
|
||||
out=$(curl -s "http://127.0.0.1:$port/")
|
||||
[[ $out == *"$want"* ]] && ok "$name" || bad "$name" "curl: $out"
|
||||
else
|
||||
bad "$name" "tunnel did not open: $(cat "$HOME/tunnel.log")"
|
||||
fi
|
||||
kill "$pid" 2>/dev/null
|
||||
wait "$pid" 2>/dev/null
|
||||
}
|
||||
tunnel_test "named forward" 18081 "hello from fsn1-pve1" netssh tunnel fsn/fsn1-pve1.hoster.test web
|
||||
expect_out "tunnel notice" "forwarding -L 18081:127.0.0.1:8081 via fsn1-pve1.hoster.test" cat "$HOME/tunnel.log"
|
||||
tunnel_test "ad hoc spec through the bastion" 18082 "hello from web1" netssh tunnel fsn/web1 18082:127.0.0.1:8081
|
||||
tunnel_test "--forward with a remote command" 18081 "hello from fsn1-pve2" netssh --forward web fsn/fsn1-pve2.hoster.test sleep 30
|
||||
|
||||
netssh tunnel fsn/fsn1-pve1.hoster.test web >/dev/null 2>&1 &
|
||||
holder=$!
|
||||
wait_for 15 curl -sf http://127.0.0.1:18081/
|
||||
expect_fail "port in use fails instead of hanging" "Address in use" timeout 20 netssh tunnel fsn/fsn1-pve2.hoster.test web
|
||||
kill "$holder"; wait "$holder" 2>/dev/null
|
||||
|
||||
expect_fail "unknown forward lists the configured ones" "available: web" netssh tunnel fsn/fsn1-pve1.hoster.test db
|
||||
expect_fail "tunnel without forward" "forwards for fsn/fsn1-pve1.hoster.test: web" netssh tunnel fsn/fsn1-pve1.hoster.test
|
||||
@@ -0,0 +1,22 @@
|
||||
# Local notes and NetBox comments (v2 token on fsn, v1 token on lab).
|
||||
config <<'EOF'
|
||||
resolver:
|
||||
strategies: [management_subnet, primary_ip]
|
||||
management_subnets: [10.231.1.0/24]
|
||||
EOF
|
||||
check "refresh" netssh cache refresh
|
||||
host=fsn/fsn1-pve1.hoster.test
|
||||
check "set local note" netssh notes set $host "first line"
|
||||
check "append local note" netssh notes append $host "second line"
|
||||
expect_eq "show local note" "first line
|
||||
second line" netssh notes show $host
|
||||
expect_eq "note from stdin" "piped" bash -c "echo piped | netssh notes set $host && netssh notes show $host"
|
||||
|
||||
expect_eq "read NetBox comments (v2 token)" "Proxmox node 1" netssh notes show $host --netbox
|
||||
check "append NetBox comments" netssh notes append $host --netbox "checked by netssh test"
|
||||
expect_out "NetBox has the new line" "checked by netssh test" curl -s -H "Authorization: Bearer $FSN_TOKEN" "$FSN_URL/api/dcim/devices/?name=fsn1-pve1.hoster.test"
|
||||
check "restore NetBox comments" netssh notes set $host --netbox "Proxmox node 1"
|
||||
|
||||
check "set NetBox comments (v1 token)" netssh notes set lab/lab-db1 --netbox "lab note"
|
||||
expect_eq "read them back" "lab note" netssh notes show lab/lab-db1 --netbox
|
||||
check "clear them" netssh notes set lab/lab-db1 --netbox ""
|
||||
@@ -0,0 +1,32 @@
|
||||
# Completion, aliases, hook install for bash, zsh, fish, and the command alias.
|
||||
config <<'EOF'
|
||||
resolver:
|
||||
strategies: [management_subnet, primary_ip]
|
||||
management_subnets: [10.231.1.0/24]
|
||||
ssh:
|
||||
default_user: ops
|
||||
forwards:
|
||||
web: {local: "18081:127.0.0.1:8081"}
|
||||
shortcuts:
|
||||
domains: [.hoster.test]
|
||||
shell:
|
||||
alias: j
|
||||
EOF
|
||||
check "refresh" netssh cache refresh
|
||||
expect_out "complete hosts" "fsn/fsn1-pve2.hoster.test" netssh __complete fsn/fsn1
|
||||
expect_out "complete scp host:" "fsn/fsn1-pve1.hoster.test:" netssh __complete scp a.txt fsn/fsn1-pve1
|
||||
expect_out "complete forwards after the host" "web" netssh __complete tunnel fsn/fsn1-pve1.hoster.test ""
|
||||
expect_out "complete --forward" "web" netssh __complete --forward ""
|
||||
expect_out "fish aliases" "alias fsn_fsn1-pve1 " netssh alias --shell fish
|
||||
|
||||
check "hook install bash" netssh hook install --shell bash
|
||||
# Interactive bash without a terminal warns about job control on stderr.
|
||||
expect_eq "bash loads host aliases" "fsn1-pve1" sh -c "bash -ic 'fsn_fsn1-pve1 hostname' 2>/dev/null </dev/null"
|
||||
expect_eq "bash command alias" "fsn1-pve2" sh -c "bash -ic 'j fsn/fsn1-pve2.hoster.test hostname' 2>/dev/null </dev/null"
|
||||
check "hook install zsh" netssh hook install --shell zsh
|
||||
expect_eq "zsh loads host aliases" "fsn1-pve3" zsh -ic 'fsn_fsn1-pve3 hostname' </dev/null
|
||||
mkdir -p "$XDG_CONFIG_HOME/fish"
|
||||
check "hook install fish" netssh hook install --shell fish
|
||||
expect_eq "fish loads host aliases" "fsn1-pve1" fish -c 'fsn_fsn1-pve1 hostname' </dev/null
|
||||
check "completion install bash" netssh completion install --shell bash
|
||||
expect_out "bash completes hosts" "fsn/fsn1-pve2.hoster.test" bash -ic '. ~/.local/share/bash-completion/completions/netssh; COMP_WORDS=(netssh fsn/fsn1-pve2); COMP_CWORD=1; COMP_LINE="netssh fsn/fsn1-pve2"; COMP_POINT=${#COMP_LINE}; __start_netssh netssh; printf "%s\n" "${COMPREPLY[@]}"' </dev/null
|
||||
@@ -0,0 +1,64 @@
|
||||
# The interactive TUI, driven through tmux: search, connect, sftp, tunnel, edit.
|
||||
config <<'EOF'
|
||||
resolver:
|
||||
strategies: [management_subnet, primary_ip]
|
||||
management_subnets: [10.231.1.0/24]
|
||||
ssh:
|
||||
default_user: ops
|
||||
forwards:
|
||||
web: {local: "18085:127.0.0.1:8081"}
|
||||
EOF
|
||||
check "refresh" netssh cache refresh
|
||||
export TMUX_TMPDIR="$HOME/tmux"
|
||||
mkdir -p "$TMUX_TMPDIR"
|
||||
|
||||
screen() { tmux capture-pane -p -t tui; }
|
||||
shows() { screen | grep -qF -- "$1"; }
|
||||
tui() { # start netssh in a fresh tmux session
|
||||
tmux kill-server 2>/dev/null
|
||||
tmux new-session -d -s tui -x 160 -y 40 -e HOME="$HOME" -e XDG_CONFIG_HOME="$XDG_CONFIG_HOME" -e XDG_CACHE_HOME="$XDG_CACHE_HOME" netssh
|
||||
wait_for 10 shows "Search hostname"
|
||||
}
|
||||
step() { # NAME TEXT — wait until the pane shows TEXT
|
||||
if wait_for 15 shows "$2"; then ok "$1"; else bad "$1" "$(screen)"; fi
|
||||
}
|
||||
|
||||
tui && ok "TUI starts" || bad "TUI starts" "$(screen)"
|
||||
tmux send-keys -t tui "fsn1-pve2"
|
||||
step "search shows the host" "fsn/fsn1-pve2.hoster.test"
|
||||
tmux send-keys -t tui Enter
|
||||
sleep 1
|
||||
tmux send-keys -t tui "hostname" Enter
|
||||
step "enter connects" "fsn1-pve2"
|
||||
tmux send-keys -t tui "exit" Enter
|
||||
|
||||
tui
|
||||
tmux send-keys -t tui "fsn1-pve3"
|
||||
wait_for 10 shows "fsn/fsn1-pve3.hoster.test"
|
||||
tmux send-keys -t tui C-o
|
||||
step "ctrl+o opens sftp" "sftp>"
|
||||
tmux send-keys -t tui "pwd" Enter
|
||||
step "sftp works" "Remote working directory: /home/ops"
|
||||
tmux send-keys -t tui "bye" Enter
|
||||
|
||||
tui
|
||||
tmux send-keys -t tui "fsn1-pve1"
|
||||
wait_for 10 shows "fsn/fsn1-pve1.hoster.test"
|
||||
tmux send-keys -t tui C-t
|
||||
step "ctrl+t lists configured forwards" "forwards: web"
|
||||
tmux send-keys -t tui "web" Enter
|
||||
step "tunnel notice" "forwarding -L 18085:127.0.0.1:8081"
|
||||
expect_out "tunnel from the TUI works" "hello from fsn1-pve1" curl -s --retry 10 --retry-connrefused --retry-delay 1 http://127.0.0.1:18085/
|
||||
tmux send-keys -t tui C-c
|
||||
|
||||
tui
|
||||
tmux send-keys -t tui "fsn1-pve1"
|
||||
wait_for 10 shows "fsn/fsn1-pve1.hoster.test"
|
||||
tmux send-keys -t tui C-e
|
||||
step "ctrl+e opens the editor" "Connect as: ops@fsn/fsn1-pve1.hoster.test:22"
|
||||
tmux send-keys -t tui C-u "admin@fsn/fsn1-pve1.hoster.test:22" Enter
|
||||
sleep 1
|
||||
tmux send-keys -t tui "whoami" Enter
|
||||
step "edited user is used" "admin"
|
||||
tmux send-keys -t tui "exit" Enter
|
||||
tmux kill-server 2>/dev/null
|
||||
@@ -0,0 +1,9 @@
|
||||
# The configurator in huh's accessible mode: without NetBox, add a static host,
|
||||
# save, then connect. Menu numbers follow the order in internal/setup.
|
||||
config_only </dev/null
|
||||
rm -f "$XDG_CONFIG_HOME/netssh.yaml"
|
||||
# no NetBox, static hosts, add, name, address, user, port, tags, back, save
|
||||
out=$(python3 /tests/drive.py 2 4 1 legacy-box 10.231.0.50 admin "" legacy 5 8)
|
||||
[[ $out == *"Config saved"* ]] && ok "configure saves" || bad "configure saves" "$out"
|
||||
expect_out "config has the host" 'address: "10.231.0.50"' cat "$XDG_CONFIG_HOME/netssh.yaml"
|
||||
expect_eq "connect to the configured host" "admin" netssh legacy-box whoami
|
||||
@@ -0,0 +1,12 @@
|
||||
# Uninstall in dry-run mode lists everything netssh created.
|
||||
config <<'EOF'
|
||||
resolver: {strategies: [primary_ip]}
|
||||
EOF
|
||||
check "refresh" netssh cache refresh
|
||||
check "hook install" netssh hook install --shell bash
|
||||
expect_out "lists the profile" ".bashrc" netssh uninstall --dry-run
|
||||
expect_out "lists the config" "netssh.yaml" netssh uninstall --dry-run
|
||||
expect_out "lists the cache" ".cache/netssh" netssh uninstall --dry-run
|
||||
out=$(netssh uninstall --dry-run --keep-config 2>&1)
|
||||
[[ $out != *"netssh.yaml"* ]] && ok "--keep-config keeps the config" || bad "--keep-config keeps the config" "$out"
|
||||
expect_out "nothing was removed" "netssh shell-init" cat "$HOME/.bashrc"
|
||||
@@ -0,0 +1,32 @@
|
||||
"""Drive `netssh configure` in huh's accessible mode (TERM=dumb): one answer
|
||||
per prompt, sent after a short pause so each form reads its own line."""
|
||||
import os
|
||||
import pty
|
||||
import select
|
||||
import sys
|
||||
import time
|
||||
|
||||
answers = sys.argv[1:]
|
||||
pid, fd = pty.fork()
|
||||
if pid == 0:
|
||||
os.execvpe("netssh", ["netssh", "configure"], dict(os.environ, TERM="dumb"))
|
||||
out = b""
|
||||
|
||||
|
||||
def drain(seconds):
|
||||
global out
|
||||
end = time.time() + seconds
|
||||
while time.time() < end:
|
||||
ready, _, _ = select.select([fd], [], [], 0.05)
|
||||
if ready:
|
||||
try:
|
||||
out += os.read(fd, 4096)
|
||||
except OSError:
|
||||
return
|
||||
|
||||
|
||||
for answer in answers:
|
||||
drain(0.5)
|
||||
os.write(fd, (answer + "\n").encode())
|
||||
drain(2)
|
||||
sys.stdout.write(out.decode(errors="replace"))
|
||||
@@ -0,0 +1,78 @@
|
||||
# Helpers for the netssh test environment. Sourced by run.sh, the tests, and
|
||||
# shell.sh inside the client container.
|
||||
|
||||
# shellcheck disable=SC1091
|
||||
. /state/env
|
||||
RESULTS=${RESULTS:-/tmp/netssh-results}
|
||||
|
||||
# new_home gives each test file its own netssh config, cache, notes, and rc files.
|
||||
new_home() {
|
||||
HOME=$(mktemp -d /tmp/home.XXXXXX)
|
||||
export HOME XDG_CONFIG_HOME="$HOME/.config" XDG_CACHE_HOME="$HOME/.cache" SHELL=/bin/bash
|
||||
mkdir -p "$XDG_CONFIG_HOME" "$XDG_CACHE_HOME"
|
||||
unset TMUX ZELLIJ NETSSH_NETBOX_URL NETSSH_NETBOX_TOKEN
|
||||
cd "$HOME" || exit 1
|
||||
}
|
||||
|
||||
# base_config prints both NetBox instances followed by stdin.
|
||||
base_config() { cat /state/netssh.yaml -; }
|
||||
|
||||
# config writes the netssh config: both NetBox instances plus stdin.
|
||||
config() { base_config >"$XDG_CONFIG_HOME/netssh.yaml"; }
|
||||
|
||||
# config_only writes stdin as the whole config (no NetBox).
|
||||
config_only() { cat >"$XDG_CONFIG_HOME/netssh.yaml"; }
|
||||
|
||||
ok() {
|
||||
echo "ok $CURRENT: $1" >>"$RESULTS"
|
||||
printf ' \033[32mok\033[0m %s\n' "$1"
|
||||
}
|
||||
bad() {
|
||||
echo "FAIL $CURRENT: $1" >>"$RESULTS"
|
||||
printf ' \033[31mFAIL\033[0m %s\n' "$1"
|
||||
printf '%s\n' "$2" | sed 's/^/ /'
|
||||
}
|
||||
|
||||
# check NAME CMD... — CMD must succeed.
|
||||
check() {
|
||||
local name=$1 out
|
||||
shift
|
||||
if out=$("$@" 2>&1); then ok "$name"; else bad "$name" "exit $?: $out"; fi
|
||||
}
|
||||
|
||||
# expect_out NAME WANT CMD... — CMD must succeed and print WANT (substring).
|
||||
expect_out() {
|
||||
local name=$1 want=$2 out rc
|
||||
shift 2
|
||||
out=$("$@" 2>&1) && rc=0 || rc=$?
|
||||
if [ "$rc" = 0 ] && [[ $out == *"$want"* ]]; then ok "$name"; else bad "$name" "want: $want
|
||||
got (exit $rc): $out"; fi
|
||||
}
|
||||
|
||||
# expect_eq NAME WANT CMD... — CMD must succeed and print exactly WANT.
|
||||
expect_eq() {
|
||||
local name=$1 want=$2 out rc
|
||||
shift 2
|
||||
out=$("$@" 2>&1) && rc=0 || rc=$?
|
||||
if [ "$rc" = 0 ] && [ "$out" = "$want" ]; then ok "$name"; else bad "$name" "want: $want
|
||||
got (exit $rc): $out"; fi
|
||||
}
|
||||
|
||||
# expect_fail NAME WANT CMD... — CMD must fail and print WANT (substring).
|
||||
expect_fail() {
|
||||
local name=$1 want=$2 out rc
|
||||
shift 2
|
||||
out=$("$@" 2>&1) && rc=0 || rc=$?
|
||||
if [ "$rc" != 0 ] && [[ $out == *"$want"* ]]; then ok "$name"; else bad "$name" "want failure with: $want
|
||||
got (exit $rc): $out"; fi
|
||||
}
|
||||
|
||||
# wait_for TIMEOUT CMD... — retry CMD every 0.2 s until it succeeds.
|
||||
wait_for() {
|
||||
local deadline=$((SECONDS + $1))
|
||||
shift
|
||||
until "$@" >/dev/null 2>&1; do
|
||||
[ $SECONDS -ge $deadline ] && return 1
|
||||
sleep 0.2
|
||||
done
|
||||
}
|
||||
Executable
+26
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env bash
|
||||
# Runs every tests/NN-*.sh (or those matching $1) in a fresh netssh home and
|
||||
# prints a summary. Exit code 1 if any check failed.
|
||||
set -u
|
||||
export RESULTS=/tmp/netssh-results
|
||||
: >"$RESULTS"
|
||||
filter=${1:-}
|
||||
for file in /tests/[0-9][0-9]-*.sh; do
|
||||
name=$(basename "$file" .sh)
|
||||
[[ -n $filter && $name != *"$filter"* ]] && continue
|
||||
echo "== $name"
|
||||
(
|
||||
export CURRENT=$name
|
||||
. /tests/lib.sh
|
||||
new_home
|
||||
. "$file"
|
||||
)
|
||||
done
|
||||
passed=$(grep -c '^ok ' "$RESULTS" || true)
|
||||
failed=$(grep -c '^FAIL ' "$RESULTS" || true)
|
||||
echo
|
||||
echo "passed: $passed, failed: $failed"
|
||||
if [ "$failed" -gt 0 ]; then
|
||||
grep '^FAIL ' "$RESULTS" | sed 's/^/ /'
|
||||
exit 1
|
||||
fi
|
||||
Reference in new issue
Block a user