feat: TypeScript bastion rewrite (initial scaffold)
Full rewrite of the bash bastion.sh into a TypeScript application: - Fastify HTTP server with typed routes (dispatch, kickstart, API) - Commander CLI (serve, install, list, reprovision) - Kickstart templates as TypeScript template literals (no more heredoc hell) - dnsmasq management via execa subprocess - Merged machine list view (hardware + install info in one table) - Containerized via podman-compose (Dockerfile + docker-compose.yml) - All partition logic preserved (LVM, reprovision detection, role-based) Not yet tested end-to-end — needs VM validation before replacing bash version. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
118
bastion/src/templates/discover.ks.ts
Normal file
118
bastion/src/templates/discover.ks.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
// Discovery kickstart template.
|
||||
// Boots Fedora installer, collects hardware info, POSTs to bastion, reboots.
|
||||
// Never touches the disk.
|
||||
|
||||
export interface DiscoverKickstartParams {
|
||||
serverIp: string;
|
||||
httpPort: number;
|
||||
}
|
||||
|
||||
export function renderDiscoverKickstart(params: DiscoverKickstartParams): string {
|
||||
const bastionUrl = `http://${params.serverIp}:${params.httpPort}`;
|
||||
|
||||
return `# Lab Bastion -- Discovery Mode
|
||||
# Collects hardware inventory and reboots. Does NOT install anything.
|
||||
|
||||
%pre --erroronfail --log=/tmp/discover.log
|
||||
#!/bin/bash
|
||||
set -x
|
||||
|
||||
# -- Collect hardware info from /proc, /sys, and available tools --
|
||||
|
||||
MAC=$(ip link show | awk '/ether/ && !/00:00:00:00/ {print $2; exit}')
|
||||
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)
|
||||
|
||||
# Disk info
|
||||
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 '[]')
|
||||
|
||||
# Network interfaces
|
||||
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 '[]')
|
||||
|
||||
# -- Build and POST discovery payload --
|
||||
|
||||
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
|
||||
}))
|
||||
")
|
||||
|
||||
# POST to bastion
|
||||
BASTION_URL="${bastionUrl}/api/discover"
|
||||
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -sf -X POST "$BASTION_URL" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d "$PAYLOAD" || true
|
||||
else
|
||||
python3 -c "
|
||||
import urllib.request
|
||||
req = urllib.request.Request('$BASTION_URL',
|
||||
data=b'''$PAYLOAD''',
|
||||
headers={'Content-Type': 'application/json'})
|
||||
try:
|
||||
urllib.request.urlopen(req, timeout=10)
|
||||
except Exception as e:
|
||||
print(f'POST failed: {e}')
|
||||
"
|
||||
fi
|
||||
|
||||
# -- Reboot -- do NOT let Anaconda proceed --
|
||||
echo ""
|
||||
echo "=== Discovery complete, rebooting ==="
|
||||
echo ""
|
||||
sleep 3
|
||||
echo 1 > /proc/sys/kernel/sysrq
|
||||
echo b > /proc/sysrq-trigger
|
||||
sleep 5
|
||||
reboot -f
|
||||
|
||||
%end
|
||||
|
||||
# Anaconda should never get here, but just in case:
|
||||
reboot
|
||||
`;
|
||||
}
|
||||
Reference in New Issue
Block a user