From 626918ff943083c808c154085d8e009af814d543 Mon Sep 17 00:00:00 2001 From: Antigravity Agent Date: Sat, 3 Oct 2026 00:16:42 -0400 Subject: [PATCH] NetVM: fleet control script (netvm-fleet.sh) for operators --- NODES.md | 2 +- README.md | 1 + bin/netvm-fleet.sh | 59 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 1 deletion(-) create mode 100755 bin/netvm-fleet.sh diff --git a/NODES.md b/NODES.md index 5884f34..0eb3d98 100644 --- a/NODES.md +++ b/NODES.md @@ -5,4 +5,4 @@ designed choice — record the reason. | node | netns | warp identity | egress IP | tail IP | shared with / reason | |------|-------|---------------|-----------|---------|----------------------| -| laptop (node zero) | — | — | — | — | orchestrator + first node, not yet provisioned | +| tp | warp-tp | — | — | — | laptop; orchestrator + first node; identity pending (netvm-new-identity.sh tp) | diff --git a/README.md b/README.md index c36edae..94f639f 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,7 @@ Tear down: sudo bin/netvm-node-down.sh . - bin/netvm-node-down.sh — tear a node's egress down. - bin/netvm-topology.sh — print the live topology table. - `bin/netvm-provision-edge.sh` — prepare an edge device (sudoers allowlist, deps, /etc/netvm); run on the node, once. +- `bin/netvm-fleet.sh` — operator fleet control over the tailnet (topology/up/down/ssh per node). - NODES.md — the registry: node -> netns -> Warp identity -> egress IP. ## Verification checklist diff --git a/bin/netvm-fleet.sh b/bin/netvm-fleet.sh new file mode 100755 index 0000000..4424313 --- /dev/null +++ b/bin/netvm-fleet.sh @@ -0,0 +1,59 @@ +#!/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 }" +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}" + ;; + *) echo "unknown command: $cmd"; exit 1 ;; +esac