propagation: single registry drives all node references
New bin/netvm-registry.py parses NODES.md (node -> cdp_port); every consumer reads from it instead of hardcoding: - onboard-driver.py: CDP_PORTS dict -> registry lookup (new nodes work) - muse-signin.py: hardcoded 9410 -> --node/--cdp-port args - muse-chat-api.py: hardcoded ACCOUNTS -> registry-built - agent-health.sh: hardcoded muse/pip blocks -> loop over all active nodes (646 and opm now get health coverage too) NODES.md: record opm node (9440).
This commit is contained in:
@@ -11,3 +11,4 @@ Unified naming: node == agent == profile == API account.
|
|||||||
- 2026-10-03: Renamed smoke->muse, phone-test->pip for unified naming.
|
- 2026-10-03: Renamed smoke->muse, phone-test->pip for unified naming.
|
||||||
Profiles preserved, sessions persisted (muse). WireGuard identities renamed.
|
Profiles preserved, sessions persisted (muse). WireGuard identities renamed.
|
||||||
| 646 | warp-646 | 104.28.195.181 | 9430 | active | 646 (phone_otp, first Meta Account option) |
|
| 646 | warp-646 | 104.28.195.181 | 9430 | active | 646 (phone_otp, first Meta Account option) |
|
||||||
|
| opm | warp-opm | 104.28.195.181 | 9440 | active | opm (yourfriendnico@proton.me, email_otp, Nico Parada) |
|
||||||
|
|||||||
+15
-16
@@ -49,28 +49,27 @@ restart_browser() {
|
|||||||
# Main
|
# Main
|
||||||
echo "=== Health check $(date -Iseconds) ===" >> "$LOG"
|
echo "=== Health check $(date -Iseconds) ===" >> "$LOG"
|
||||||
|
|
||||||
# muse (node muse, CDP 9410)
|
check_one() {
|
||||||
if ! check_agent "muse" "muse" "9410"; then
|
local agent=$1
|
||||||
restart_browser "muse" "9410"
|
local cdp_port=$2
|
||||||
|
# node == agent == profile (unified naming)
|
||||||
|
if ! check_agent "$agent" "$agent" "$cdp_port"; then
|
||||||
|
restart_browser "$agent" "$cdp_port"
|
||||||
sleep 5
|
sleep 5
|
||||||
if ! check_agent "muse" "muse" "9410"; then
|
if ! check_agent "$agent" "$agent" "$cdp_port"; then
|
||||||
echo "$(date -Iseconds) muse: CRITICAL - still down after restart" >> "$LOG"
|
echo "$(date -Iseconds) $agent: CRITICAL - still down after restart" >> "$LOG"
|
||||||
# TODO: Alert operator (e.g., via board post or email)
|
# TODO: Alert operator (e.g., via board post or email)
|
||||||
else
|
else
|
||||||
echo "$(date -Iseconds) muse: RECOVERED after restart" >> "$LOG"
|
echo "$(date -Iseconds) $agent: RECOVERED after restart" >> "$LOG"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# pip (node pip, CDP 9420)
|
# Every active node in the NODES.md registry gets checked — new nodes
|
||||||
if ! check_agent "pip" "pip" "9420"; then
|
# propagate automatically, no per-node blocks to add.
|
||||||
restart_browser "pip" "9420"
|
"$NETVM_BIN/netvm-registry.py" 2>/dev/null | while IFS=: read -r node port; do
|
||||||
sleep 5
|
[ -n "$node" ] && [ -n "$port" ] && check_one "$node" "$port"
|
||||||
if ! check_agent "pip" "pip" "9420"; then
|
done
|
||||||
echo "$(date -Iseconds) pip: CRITICAL - still down after restart" >> "$LOG"
|
|
||||||
else
|
|
||||||
echo "$(date -Iseconds) pip: RECOVERED after restart" >> "$LOG"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Trim log (keep last 1000 lines)
|
# Trim log (keep last 1000 lines)
|
||||||
tail -1000 "$LOG" > "$LOG.tmp" && mv "$LOG.tmp" "$LOG"
|
tail -1000 "$LOG" > "$LOG.tmp" && mv "$LOG.tmp" "$LOG"
|
||||||
|
|||||||
+17
-7
@@ -16,14 +16,24 @@ Usage:
|
|||||||
muse-chat-api.py --account <agent> wait [timeout]
|
muse-chat-api.py --account <agent> wait [timeout]
|
||||||
muse-chat-api.py --account <agent> approvals # check pending approvals
|
muse-chat-api.py --account <agent> approvals # check pending approvals
|
||||||
"""
|
"""
|
||||||
import json, urllib.request, websocket, time, sys, argparse
|
import json, urllib.request, websocket, time, sys, argparse, importlib.util
|
||||||
|
|
||||||
ACCOUNTS = {
|
|
||||||
"muse": ("muse", "http://127.0.0.1:9410/json/list"),
|
def _load_accounts():
|
||||||
"pip": ("pip", "http://127.0.0.1:9420/json/list"),
|
"""Build the accounts table from the NODES.md registry — new nodes
|
||||||
"646": ("646", "http://127.0.0.1:9430/json/list"),
|
propagate automatically instead of being hardcoded here."""
|
||||||
"opm": ("opm", "http://127.0.0.1:9440/json/list"),
|
path = "/home/super/Projects/NetVM/bin/netvm-registry.py"
|
||||||
}
|
spec = importlib.util.spec_from_file_location("netvm_registry", path)
|
||||||
|
mod = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(mod)
|
||||||
|
accounts = {}
|
||||||
|
for node, rec in mod.load().items():
|
||||||
|
accounts[node] = (node, "http://127.0.0.1:%d/json/list" %
|
||||||
|
rec["cdp_port"])
|
||||||
|
return accounts
|
||||||
|
|
||||||
|
|
||||||
|
ACCOUNTS = _load_accounts()
|
||||||
|
|
||||||
# IPs we trust for auto-approval (our infrastructure)
|
# IPs we trust for auto-approval (our infrastructure)
|
||||||
TRUSTED_IPS = {
|
TRUSTED_IPS = {
|
||||||
|
|||||||
+29
-5
@@ -16,12 +16,25 @@ The operator then obtains the OTP (via chat with user) and re-runs:
|
|||||||
This keeps credentials out of the automation — the OTP is provided
|
This keeps credentials out of the automation — the OTP is provided
|
||||||
transiently via the operator, never stored.
|
transiently via the operator, never stored.
|
||||||
"""
|
"""
|
||||||
import json, urllib.request, websocket, time, sys, argparse
|
import json, urllib.request, websocket, time, sys, argparse, importlib.util
|
||||||
|
|
||||||
CDP_URL = "http://127.0.0.1:9410/json/list"
|
CDP_PORT_DEFAULT = 9410
|
||||||
|
|
||||||
def get_page():
|
|
||||||
with urllib.request.urlopen(CDP_URL, timeout=5) as r:
|
def _registry_port(node):
|
||||||
|
try:
|
||||||
|
path = "/home/super/Projects/NetVM/bin/netvm-registry.py"
|
||||||
|
spec = importlib.util.spec_from_file_location("netvm_registry", path)
|
||||||
|
mod = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(mod)
|
||||||
|
return mod.port_for(node)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_page(port):
|
||||||
|
cdp_url = "http://127.0.0.1:%s/json/list" % port
|
||||||
|
with urllib.request.urlopen(cdp_url, timeout=5) as r:
|
||||||
ts = json.load(r)
|
ts = json.load(r)
|
||||||
pages = [t for t in ts if t.get('type') == 'page']
|
pages = [t for t in ts if t.get('type') == 'page']
|
||||||
if not pages:
|
if not pages:
|
||||||
@@ -41,9 +54,20 @@ def main():
|
|||||||
p = argparse.ArgumentParser()
|
p = argparse.ArgumentParser()
|
||||||
p.add_argument('--email', required=True)
|
p.add_argument('--email', required=True)
|
||||||
p.add_argument('--otp', default=None)
|
p.add_argument('--otp', default=None)
|
||||||
|
p.add_argument('--node', default=None,
|
||||||
|
help='node name: CDP port comes from the NODES.md registry')
|
||||||
|
p.add_argument('--cdp-port', default=None,
|
||||||
|
help='explicit CDP port (overrides --node lookup)')
|
||||||
args = p.parse_args()
|
args = p.parse_args()
|
||||||
|
|
||||||
page = get_page()
|
if args.cdp_port:
|
||||||
|
port = args.cdp_port
|
||||||
|
elif args.node:
|
||||||
|
port = _registry_port(args.node) or CDP_PORT_DEFAULT
|
||||||
|
else:
|
||||||
|
port = CDP_PORT_DEFAULT
|
||||||
|
|
||||||
|
page = get_page(port)
|
||||||
ws = websocket.create_connection(page['webSocketDebuggerUrl'], timeout=15)
|
ws = websocket.create_connection(page['webSocketDebuggerUrl'], timeout=15)
|
||||||
|
|
||||||
# Step 1: Check if already logged in
|
# Step 1: Check if already logged in
|
||||||
|
|||||||
Executable
+77
@@ -0,0 +1,77 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
netvm-registry.py — single source of truth for node -> CDP port mapping.
|
||||||
|
|
||||||
|
Parses NODES.md (the fleet registry). Every script that needs a node's
|
||||||
|
CDP port reads it from here instead of hardcoding — new nodes propagate
|
||||||
|
automatically.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
from netvm_registry import port_for, load
|
||||||
|
port_for("muse") # -> 9410 (int), None if unknown
|
||||||
|
|
||||||
|
CLI:
|
||||||
|
netvm-registry.py # prints "node:port" lines for active nodes
|
||||||
|
netvm-registry.py <node> # prints just the port (for bash)
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
REGISTRY = os.path.join(os.path.dirname(os.path.dirname(
|
||||||
|
os.path.abspath(__file__))), "NODES.md")
|
||||||
|
|
||||||
|
|
||||||
|
def load(registry_path=None):
|
||||||
|
"""Return {node: {netns, egress_ip, cdp_port, status, agent}}."""
|
||||||
|
path = registry_path or REGISTRY
|
||||||
|
nodes = {}
|
||||||
|
try:
|
||||||
|
with open(path) as f:
|
||||||
|
for line in f:
|
||||||
|
line = line.rstrip("\n")
|
||||||
|
if not line.startswith("|"):
|
||||||
|
continue
|
||||||
|
cells = [c.strip() for c in line.strip("|").split("|")]
|
||||||
|
if len(cells) < 6:
|
||||||
|
continue
|
||||||
|
node, netns, egress, port, status, agent = cells[:6]
|
||||||
|
if node in ("node", "") or not re.fullmatch(r"[a-z0-9-]+",
|
||||||
|
node):
|
||||||
|
continue
|
||||||
|
if node.startswith("-"):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
port_n = int(port)
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
nodes[node] = {"netns": netns, "egress_ip": egress,
|
||||||
|
"cdp_port": port_n, "status": status,
|
||||||
|
"agent": agent}
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
return nodes
|
||||||
|
|
||||||
|
|
||||||
|
def port_for(node, registry_path=None):
|
||||||
|
"""CDP port for a node, or None if the node isn't registered."""
|
||||||
|
rec = load(registry_path).get(node)
|
||||||
|
return rec["cdp_port"] if rec else None
|
||||||
|
|
||||||
|
|
||||||
|
def active_nodes(registry_path=None):
|
||||||
|
"""{node: port} for nodes with status == active."""
|
||||||
|
return {n: r["cdp_port"] for n, r in load(registry_path).items()
|
||||||
|
if r["status"] == "active"}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) == 2:
|
||||||
|
port = port_for(sys.argv[1])
|
||||||
|
if port is None:
|
||||||
|
print("unknown node: %s" % sys.argv[1], file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
print(port)
|
||||||
|
else:
|
||||||
|
for node, port in sorted(active_nodes().items()):
|
||||||
|
print("%s:%d" % (node, port))
|
||||||
+13
-4
@@ -16,13 +16,21 @@ VM->bl SSH stdin pipe.
|
|||||||
Part of the cred onboarding module (front-door repo, docs/CRED-MODULE.md).
|
Part of the cred onboarding module (front-door repo, docs/CRED-MODULE.md).
|
||||||
"""
|
"""
|
||||||
import argparse
|
import argparse
|
||||||
|
import importlib.util
|
||||||
import json
|
import json
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
|
||||||
SIGNIN = "/home/super/Projects/NetVM/bin/muse-signin.py"
|
SIGNIN = "/home/super/Projects/NetVM/bin/muse-signin.py"
|
||||||
CDP_PORTS = {"muse": "9410", "pip": "9420"}
|
|
||||||
|
|
||||||
|
def _load_registry():
|
||||||
|
path = "/home/super/Projects/NetVM/bin/netvm-registry.py"
|
||||||
|
spec = importlib.util.spec_from_file_location("netvm_registry", path)
|
||||||
|
mod = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(mod)
|
||||||
|
return mod
|
||||||
|
|
||||||
|
|
||||||
def cdp_ok(port):
|
def cdp_ok(port):
|
||||||
@@ -51,9 +59,10 @@ def main():
|
|||||||
print("ERROR: unsupported service/id_type "
|
print("ERROR: unsupported service/id_type "
|
||||||
"(muse+email only for now)", file=sys.stderr)
|
"(muse+email only for now)", file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
port = CDP_PORTS.get(args.node)
|
port = _load_registry().port_for(args.node)
|
||||||
if not port:
|
if not port:
|
||||||
print("ERROR: unknown node", file=sys.stderr)
|
print("ERROR: unknown node '%s' (not in NODES.md registry)"
|
||||||
|
% args.node, file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
if args.dry_run:
|
if args.dry_run:
|
||||||
@@ -69,7 +78,7 @@ def main():
|
|||||||
print("ERROR: no identifier on stdin", file=sys.stderr)
|
print("ERROR: no identifier on stdin", file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
cmd = [sys.executable, SIGNIN, "--email", identifier]
|
cmd = [sys.executable, SIGNIN, "--node", args.node, "--email", identifier]
|
||||||
if args.step == "submit":
|
if args.step == "submit":
|
||||||
if not code:
|
if not code:
|
||||||
print("ERROR: no code on stdin", file=sys.stderr)
|
print("ERROR: no code on stdin", file=sys.stderr)
|
||||||
|
|||||||
Reference in New Issue
Block a user