diff --git a/bastion/src/bastion/src/routes/api.ts b/bastion/src/bastion/src/routes/api.ts index fd7c393..7aab036 100644 --- a/bastion/src/bastion/src/routes/api.ts +++ b/bastion/src/bastion/src/routes/api.ts @@ -359,6 +359,23 @@ export function registerApiRoutes( }); }); + // Simple machine state query (used by ks-auto for ISO boot dispatch) + app.get<{ + Params: { mac: string }; + }>("/api/machine-state/:mac", async (request, reply) => { + const mac = request.params.mac.toLowerCase().replace(/-/g, ":"); + const currentState = state.load(); + + if (currentState.debug[mac]) return reply.send("debug"); + if (currentState.install_queue[mac]) { + const progress = currentState.install_queue[mac].progress; + return reply.send(progress ? "installing" : "queued"); + } + if (currentState.installed[mac]) return reply.send("installed"); + if (currentState.discovered[mac]) return reply.send("discovered"); + return reply.send("unknown"); + }); + // Update a machine's role (e.g. promote infra -> labcontroller) app.post<{ Body: { diff --git a/bastion/src/bastion/src/routes/boot-iso.ts b/bastion/src/bastion/src/routes/boot-iso.ts index 1a5a5f4..ba16d04 100644 --- a/bastion/src/bastion/src/routes/boot-iso.ts +++ b/bastion/src/bastion/src/routes/boot-iso.ts @@ -137,7 +137,7 @@ function generateIso(config: BastionConfig, outputPath: string): void { "# Map iPXE arch names to Fedora mirror paths (arm64 -> aarch64)", "set fedarch ${buildarch}", "iseq ${buildarch} arm64 && set fedarch aarch64 ||", - `kernel file:/vmlinuz-\${buildarch} inst.ks=${bastionUrl}/discover.ks inst.repo=${FEDORA_MIRROR_BASE}/${config.fedoraVersion}/Everything/\${fedarch}/os inst.text || goto no_kernel`, + `kernel file:/vmlinuz-\${buildarch} inst.ks=${bastionUrl}/ks-auto inst.repo=${FEDORA_MIRROR_BASE}/${config.fedoraVersion}/Everything/\${fedarch}/os inst.text || goto no_kernel`, `initrd file:/initrd-\${buildarch} || goto no_kernel`, "boot || shell", "", diff --git a/bastion/src/bastion/src/routes/kickstart.ts b/bastion/src/bastion/src/routes/kickstart.ts index 49ca90a..db4dab6 100644 --- a/bastion/src/bastion/src/routes/kickstart.ts +++ b/bastion/src/bastion/src/routes/kickstart.ts @@ -41,6 +41,136 @@ export function registerKickstartRoutes( return reply.type("text/plain").send(ks); }); + // Auto-detecting kickstart for ISO boot (no-network machines like R1 ARM). + // %pre detects MAC, queries bastion state, writes dynamic kickstart to /tmp. + // Main body %include's it — so Anaconda gets either discover or install content. + app.get("/ks-auto", async (_request, reply) => { + const bastionUrl = `http://${config.serverIp}:${config.httpPort}`; + + const ks = `# Lab Bastion -- Auto-detect kickstart (ISO boot) +# %pre detects MAC, queries bastion state, writes /tmp/dynamic.ks. +# Main body %include's it to get either discovery reboot or full install. + +%pre --erroronfail --log=/tmp/ks-auto.log +#!/bin/bash +set -x + +# -- Detect MAC address -- +MAC=$(ip link show | awk '/ether/ && !/00:00:00:00/ {print $2; exit}') +echo "Detected MAC: $MAC" + +# -- Wait for network (Linux drivers may take a moment) -- +for i in $(seq 1 30); do + if curl -sf "${bastionUrl}/healthz" >/dev/null 2>&1; then + echo "Bastion reachable at ${bastionUrl}" + break + fi + echo "Waiting for network... ($i/30)" + sleep 2 +done + +# -- Query bastion for machine state -- +STATE=$(curl -sf "${bastionUrl}/api/machine-state/$MAC" 2>/dev/null || echo "unknown") +echo "Machine state: $STATE" + +case "$STATE" in + queued|installing) + echo "=== Machine queued for install. Fetching install kickstart... ===" + curl -sf "${bastionUrl}/ks?mac=$MAC" > /tmp/dynamic.ks + if [ -s /tmp/dynamic.ks ]; then + echo "Install kickstart downloaded ($(wc -l < /tmp/dynamic.ks) lines)" + else + echo "ERROR: Failed to download install kickstart" + exit 1 + fi + ;; + + debug) + echo "=== Debug mode ===" + curl -sf "${bastionUrl}/debug.ks?mac=$MAC" > /tmp/dynamic.ks 2>/dev/null + if [ ! -s /tmp/dynamic.ks ]; then + echo "rescue" > /tmp/dynamic.ks + fi + ;; + + *) + echo "=== Running hardware discovery ===" + # Collect hardware info + PRODUCT=$(cat /sys/class/dmi/id/product_name 2>/dev/null || echo "unknown") + BOARD=$(cat /sys/class/dmi/id/board_name 2>/dev/null || echo "unknown") + SERIAL=$(cat /sys/class/dmi/id/product_serial 2>/dev/null || echo "unknown") + MANUFACTURER=$(cat /sys/class/dmi/id/sys_vendor 2>/dev/null || echo "unknown") + CPUMODEL=$(grep -m1 'model name' /proc/cpuinfo | cut -d: -f2 | sed 's/^ //') + CPUCORES=$(grep -c '^processor' /proc/cpuinfo) + MEMGB=$(awk '/MemTotal/ {printf "%d", $2/1024/1024}' /proc/meminfo) + ARCHTYPE=$(uname -m) + + DISKS_JSON=$(lsblk -Jb -o NAME,SIZE,TYPE,MODEL 2>/dev/null | python3 -c " +import sys, json +data = json.load(sys.stdin) +disks = [d for d in data.get('blockdevices', []) if d.get('type') == 'disk'] +result = [] +for d in disks: + size_gb = round(int(d.get('size', 0)) / 1073741824, 1) + result.append({'name': d.get('name', '?'), 'size_gb': size_gb, 'model': (d.get('model') or 'unknown').strip()}) +print(json.dumps(result)) +" 2>/dev/null || echo '[]') + + NICS_JSON=$(ip -j link show 2>/dev/null | python3 -c " +import sys, json +nics = json.load(sys.stdin) +result = [] +for n in nics: + if n.get('link_type') == 'loopback': continue + result.append({'name': n.get('ifname', '?'), 'mac': n.get('address', '?'), 'state': n.get('operstate', '?')}) +print(json.dumps(result)) +" 2>/dev/null || echo '[]') + + PAYLOAD=$(python3 -c " +import json +print(json.dumps({ + 'mac': '$MAC', 'product': '$PRODUCT', 'board': '$BOARD', 'serial': '$SERIAL', + 'manufacturer': '$MANUFACTURER', 'cpu_model': '$CPUMODEL', + 'cpu_cores': int('$CPUCORES' or 0), 'memory_gb': int('$MEMGB' or 0), + 'arch': '$ARCHTYPE', 'disks': $DISKS_JSON, 'nics': $NICS_JSON +})) +") + + curl -sf -X POST "${bastionUrl}/api/discover" \\ + -H "Content-Type: application/json" \\ + -d "$PAYLOAD" || true + + echo "" + echo "=== Discovery complete ===" + echo "Machine MAC: $MAC" + echo "Queue for install: labctl provision install $MAC --role infra" + echo "Then reboot to start installation." + echo "" + + # Write a minimal kickstart that just reboots + cat > /tmp/dynamic.ks << 'DISCOVER_KS' +# Discovery mode -- reboot to allow install queue +reboot +DISCOVER_KS + + # Force reboot now (don't wait for Anaconda) + sleep 3 + echo 1 > /proc/sys/kernel/sysrq + echo b > /proc/sysrq-trigger + sleep 5 + reboot -f + ;; +esac + +%end + +# Include the dynamically chosen kickstart +%include /tmp/dynamic.ks +`; + + return reply.type("text/plain").send(ks); + }); + // Ubuntu autoinstall user-data (cloud-init) app.get<{ Params: { mac: string } }>("/autoinstall/:mac/user-data", async (request, reply) => { const mac = request.params.mac.toLowerCase().replace(/-/g, ":");