#!/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 # bring a node's egress up # netvm-fleet.sh down # bring it down # netvm-fleet.sh ssh # 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() { # 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 |down |ssh |exec -- |cdp |accounts }" 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 }" run_on "$node" "sudo -n \$HOME/Projects/NetVM/bin/netvm-node-${cmd}.sh ${node}" ;; ssh) node="${2:?usage: netvm-fleet.sh ssh }" ip=$(tail_ip "$node") || { echo "unknown tailnet node: $node"; exit 1; } exec ssh "${OPERATOR_USER}@${ip}" ;; exec) node="${2:?usage: netvm-fleet.sh exec -- [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 }" run_on "$node" "\$HOME/Projects/NetVM/bin/netvm-cdp.sh ${node}" ;; accounts) node="${2:?usage: netvm-fleet.sh accounts }" run_on "$node" "\$HOME/Projects/NetVM/bin/netvm-accounts.sh" ;; *) echo "unknown command: $cmd"; exit 1 ;; esac