feat(metal): phone home API for nixos-anywhere orchestration

This commit is contained in:
Khue Doan
2025-10-02 21:44:53 +07:00
parent 1f62010c66
commit 3294e7adb4
3 changed files with 137 additions and 0 deletions

View File

@@ -17,4 +17,14 @@
}
];
};
metal2 = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
disko.nixosModules.disko
./configuration.nix
{
networking.hostName = "metal2";
}
];
};
}

View File

@@ -17,4 +17,31 @@
btop
];
};
systemd.services."phone-home" =
let
reportScript = pkgs.writeShellScript "phone-home.sh" ''
set -euo pipefail
sleep 30
iface=$(${pkgs.iproute2}/bin/ip route show default | ${pkgs.coreutils}/bin/cut -d ' ' -f 5)
mac=$(cat /sys/class/net/$iface/address)
ip=$(${pkgs.iproute2}/bin/ip route show default | ${pkgs.coreutils}/bin/cut -d ' ' -f 9)
${pkgs.curl}/bin/curl -sf -X POST "http://192.168.1.15:5000/report" \
--data-urlencode "mac=$mac" \
--data-urlencode "ip=$ip"
'';
in
{
description = "Report IP address after PXE boot";
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
serviceConfig = {
Type = "oneshot";
ExecStart = [ reportScript ];
};
wantedBy = [ "multi-user.target" ];
};
}

100
metal/server.py Normal file
View File

@@ -0,0 +1,100 @@
from http.server import BaseHTTPRequestHandler, HTTPServer
import urllib.parse
import subprocess
import threading
import sys
import signal
# TODO clean this shit up, split this to a JSON or some shit
HOST_MAP = {
"bc:24:11:d0:28:34": ".#metal1",
"bc:24:11:0d:2f:20": ".#metal2",
}
installed = set()
processes = {}
lock = threading.Lock()
server = None # HTTPServer instance
pxe_process = None # PXE server process
class Handler(BaseHTTPRequestHandler):
def do_POST(self):
length = int(self.headers['Content-Length'])
body = self.rfile.read(length).decode()
data = urllib.parse.parse_qs(body)
mac = data.get("mac", [""])[0]
ip = data.get("ip", [""])[0]
print(f"Got phone-home from {mac} at {ip}")
if mac in HOST_MAP:
with lock:
if mac not in installed and mac not in processes:
flake = HOST_MAP[mac]
print(f"→ Starting nixos-anywhere for {flake} on {ip}")
command = [
"nixos-anywhere",
"--flake", flake,
"--target-host", f"root@{ip}"
]
print("Command:", command)
proc = subprocess.Popen(command)
processes[mac] = proc
threading.Thread(target=monitor_install, args=(mac,)).start()
else:
print(f"{mac} already installed or in progress, ignoring")
else:
print("Unknown MAC, ignoring")
self.send_response(200)
self.end_headers()
def monitor_install(mac):
proc = processes[mac]
proc.wait()
with lock:
installed.add(mac)
del processes[mac]
print(f"Installed servers: {installed}")
print(f"Pending servers: {set(HOST_MAP.keys()) - installed}")
if not (set(HOST_MAP.keys()) - installed):
print("All servers installed, shutting down.")
stop_server()
def stop_server():
global server, pxe_process
if server:
server.shutdown()
print("HTTP server shutdown complete.")
if pxe_process:
print("Stopping PXE server...")
pxe_process.send_signal(signal.SIGINT)
pxe_process.wait()
print("PXE server stopped.")
sys.exit(0)
if __name__ == "__main__":
print("Servers to install:", HOST_MAP)
# Start PXE server with interactive sudo
print("Starting PXE server (sudo will prompt)...")
pxe_process = subprocess.Popen(
["sudo", "nix", "run", ".#nixosPxeServer"],
stdin=sys.stdin, stdout=sys.stdout, stderr=sys.stderr
)
# Start HTTP server
server = HTTPServer(("0.0.0.0", 5000), Handler)
try:
server.serve_forever()
except KeyboardInterrupt:
stop_server()