Files

74 lines
2.7 KiB
Bash
Raw Permalink Normal View History

#!/usr/bin/env bash
# netvm-fleet.sh — operator fleet control over the tailnet.
# Run on any tailnet-connected host (laptop, VM) as the operator user.
# netvm-fleet.sh topology # egress per node, fleet-wide
# netvm-fleet.sh up <node> # bring a node's egress up
# netvm-fleet.sh down <node> # bring it down
# netvm-fleet.sh ssh <node> # shell on the node
# Node names are tailnet hostnames (see NODES.md).
# Prereqs: targets provisioned via netvm-provision-edge.sh; this user's SSH
# key accepted on targets. Lifecycle runs through the sudoers allowlist
# (sudo -n), so it is audit-logged and never blanket root.
set -euo pipefail
REPO="${NETVM_REPO:-$HOME/Projects/NetVM}"
OPERATOR_USER="${OPERATOR_USER:-$(whoami)}"
nodes() {
awk -F'|' '/^\|/ && $2 !~ /node/ && $2 !~ /---/ { n=$2; gsub(/^ +| +$/, "", n); if (n != "") print n }' "$REPO/NODES.md"
}
tail_ip() {
NODE="$1" python3 -c "
import json, os, subprocess, sys
node = os.environ['NODE']
st = json.loads(subprocess.run(['tailscale','status','--json'], capture_output=True, text=True).stdout)
cands = list(st.get('Peer', {}).values()) + [st.get('Self', {})]
for p in cands:
names = {p.get('HostName',''), p.get('DNSName','').split('.')[0]}
if node in names and p.get('TailscaleIPs'):
print(p['TailscaleIPs'][0]); sys.exit(0)
sys.exit(1)
"
}
run_on() { # <node> <remote-command...>
local node="$1"; shift
local ip
ip=$(tail_ip "$node") || { echo "unknown tailnet node: $node"; exit 1; }
ssh -o BatchMode=yes -o ConnectTimeout=15 "${OPERATOR_USER}@${ip}" "$@"
}
cmd="${1:?usage: netvm-fleet.sh topology|up <node>|down <node>|ssh <node>|exec <node> -- <cmd>|cdp <node>|accounts <node>}"
case "$cmd" in
topology)
for n in $(nodes); do
printf '== %s ==\n' "$n"
run_on "$n" 'sudo -n $HOME/Projects/NetVM/bin/netvm-topology.sh' 2>&1 || echo "unreachable"
done
;;
up|down)
node="${2:?usage: netvm-fleet.sh $cmd <node>}"
run_on "$node" "sudo -n \$HOME/Projects/NetVM/bin/netvm-node-${cmd}.sh ${node}"
;;
ssh)
node="${2:?usage: netvm-fleet.sh ssh <node>}"
ip=$(tail_ip "$node") || { echo "unknown tailnet node: $node"; exit 1; }
exec ssh "${OPERATOR_USER}@${ip}"
;;
exec)
node="${2:?usage: netvm-fleet.sh exec <node> -- <cmd> [args...]}"
shift 2
[ "${1:-}" = "--" ] && shift
run_on "$node" "sudo -n \$HOME/Projects/NetVM/bin/netvm-exec.sh ${node} -- $*"
;;
cdp)
node="${2:?usage: netvm-fleet.sh cdp <node>}"
run_on "$node" "\$HOME/Projects/NetVM/bin/netvm-cdp.sh ${node}"
;;
accounts)
node="${2:?usage: netvm-fleet.sh accounts <node>}"
run_on "$node" "\$HOME/Projects/NetVM/bin/netvm-accounts.sh"
;;
*) echo "unknown command: $cmd"; exit 1 ;;
esac