feat(bastion): first-class aarch64 support in the network-PXE path #17
@@ -313,27 +313,31 @@ export function registerApiRoutes(
|
|||||||
// classification already on the record wins (see classifyOnboard).
|
// classification already on the record wins (see classifyOnboard).
|
||||||
const onboarding = classifyOnboard({
|
const onboarding = classifyOnboard({
|
||||||
mac,
|
mac,
|
||||||
manufacturer: data.manufacturer ?? "unknown",
|
manufacturer: data.manufacturer ?? existing?.manufacturer ?? "unknown",
|
||||||
product: data.product ?? "unknown",
|
product: data.product ?? existing?.product ?? "unknown",
|
||||||
board: data.board ?? "unknown",
|
board: data.board ?? existing?.board ?? "unknown",
|
||||||
...(existing?.onboard !== undefined ? { onboard: existing.onboard } : {}),
|
...(existing?.onboard !== undefined ? { onboard: existing.onboard } : {}),
|
||||||
...(existing?.vendor_os !== undefined ? { vendor_os: existing.vendor_os } : {}),
|
...(existing?.vendor_os !== undefined ? { vendor_os: existing.vendor_os } : {}),
|
||||||
});
|
});
|
||||||
const rootDevice = data.root_device ?? existing?.root_device;
|
const rootDevice = data.root_device ?? existing?.root_device;
|
||||||
const rootArgs = data.root_args ?? existing?.root_args;
|
const rootArgs = data.root_args ?? existing?.root_args;
|
||||||
|
|
||||||
|
// Absent fields keep whatever we already knew. Reporters are not all the full
|
||||||
|
// discovery kickstart: the rescue-shell probe posts only a root device, and
|
||||||
|
// blanking a machine's hardware inventory as a side effect of that would be
|
||||||
|
// silent data loss.
|
||||||
const hwInfo: HardwareInfo = {
|
const hwInfo: HardwareInfo = {
|
||||||
mac,
|
mac,
|
||||||
product: data.product ?? "unknown",
|
product: data.product ?? existing?.product ?? "unknown",
|
||||||
board: data.board ?? "unknown",
|
board: data.board ?? existing?.board ?? "unknown",
|
||||||
serial: data.serial ?? "unknown",
|
serial: data.serial ?? existing?.serial ?? "unknown",
|
||||||
manufacturer: data.manufacturer ?? "unknown",
|
manufacturer: data.manufacturer ?? existing?.manufacturer ?? "unknown",
|
||||||
cpu_model: data.cpu_model ?? "unknown",
|
cpu_model: data.cpu_model ?? existing?.cpu_model ?? "unknown",
|
||||||
cpu_cores: data.cpu_cores ?? 0,
|
cpu_cores: data.cpu_cores ?? existing?.cpu_cores ?? 0,
|
||||||
memory_gb: data.memory_gb ?? 0,
|
memory_gb: data.memory_gb ?? existing?.memory_gb ?? 0,
|
||||||
arch: data.arch ?? "unknown",
|
arch: data.arch ?? existing?.arch ?? "unknown",
|
||||||
disks: data.disks ?? [],
|
disks: data.disks ?? existing?.disks ?? [],
|
||||||
nics: data.nics ?? [],
|
nics: data.nics ?? existing?.nics ?? [],
|
||||||
first_seen: existing?.first_seen ?? now,
|
first_seen: existing?.first_seen ?? now,
|
||||||
last_seen: now,
|
last_seen: now,
|
||||||
onboard: onboarding.onboard,
|
onboard: onboarding.onboard,
|
||||||
|
|||||||
@@ -92,10 +92,58 @@ curl -sf -X POST "http://${config.serverIp}:${config.httpPort}/api/progress" \\
|
|||||||
-H "Content-Type: application/json" \\
|
-H "Content-Type: application/json" \\
|
||||||
-d "{\\"mac\\":\\"$MAC_ADDR\\",\\"stage\\":\\"debug-ready\\",\\"detail\\":\\"nc $IP_ADDR 2323\\"}" 2>/dev/null || true
|
-d "{\\"mac\\":\\"$MAC_ADDR\\",\\"stage\\":\\"debug-ready\\",\\"detail\\":\\"nc $IP_ADDR 2323\\"}" 2>/dev/null || true
|
||||||
|
|
||||||
|
# --- Find the installed root filesystem and report it ---
|
||||||
|
# This is what 'labctl provision debug --pxe-boot' needs. The rescue image cannot
|
||||||
|
# report it by itself: %pre/%post do not run in rescue mode, so it happens here.
|
||||||
|
vgchange -ay >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
ROOT_DEVICE=""
|
||||||
|
ROOT_ARGS=""
|
||||||
|
PROBE_MNT=/tmp/lab-rootprobe
|
||||||
|
mkdir -p "$PROBE_MNT"
|
||||||
|
|
||||||
|
# Candidates: every LVM logical volume plus every non-LVM partition with a filesystem.
|
||||||
|
for CAND in $(lvs --noheadings -o lv_path 2>/dev/null) \\
|
||||||
|
$(blkid -o device 2>/dev/null | grep -v '^/dev/mapper/'); do
|
||||||
|
[ -b "$CAND" ] || continue
|
||||||
|
mount -o ro "$CAND" "$PROBE_MNT" >/dev/null 2>&1 || continue
|
||||||
|
# A root filesystem has both of these; /boot and /home do not.
|
||||||
|
if [ -f "$PROBE_MNT/etc/fstab" ] && [ -d "$PROBE_MNT/usr" ]; then
|
||||||
|
ROOT_DEVICE="$CAND"
|
||||||
|
PRETTY=$(. "$PROBE_MNT/etc/os-release" 2>/dev/null && echo "$PRETTY_NAME")
|
||||||
|
echo " found root: $CAND \${PRETTY:+($PRETTY)}"
|
||||||
|
if [ "$(lsblk -no TYPE "$CAND" 2>/dev/null | head -1)" = "lvm" ]; then
|
||||||
|
VGLV=$(lvs --noheadings -o vg_name,lv_name "$CAND" 2>/dev/null | awk '{print $1"/"$2}')
|
||||||
|
[ -n "$VGLV" ] && ROOT_ARGS="rd.lvm.lv=$VGLV"
|
||||||
|
# Swap comes from fstab here — /proc/swaps is the rescue image's, not the host's.
|
||||||
|
SWLV=$(awk '$3=="swap" && $1 ~ /^\\/dev\\// {print $1; exit}' "$PROBE_MNT/etc/fstab" 2>/dev/null)
|
||||||
|
if [ -n "$SWLV" ]; then
|
||||||
|
SWVGLV=$(lvs --noheadings -o vg_name,lv_name "$SWLV" 2>/dev/null | awk '{print $1"/"$2}')
|
||||||
|
[ -n "$SWVGLV" ] && [ "$SWVGLV" != "$VGLV" ] && ROOT_ARGS="$ROOT_ARGS rd.lvm.lv=$SWVGLV"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
umount "$PROBE_MNT" >/dev/null 2>&1 || true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
umount "$PROBE_MNT" >/dev/null 2>&1 || true
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -n "$ROOT_DEVICE" ]; then
|
||||||
|
curl -sf -X POST "http://${config.serverIp}:${config.httpPort}/api/discover" \\
|
||||||
|
-H "Content-Type: application/json" \\
|
||||||
|
-d "{\\"mac\\":\\"$MAC_ADDR\\",\\"root_device\\":\\"$ROOT_DEVICE\\",\\"root_args\\":\\"$ROOT_ARGS\\"}" 2>/dev/null \\
|
||||||
|
&& echo " reported to bastion — 'labctl provision debug --pxe-boot' will work now"
|
||||||
|
else
|
||||||
|
echo " no root filesystem found — --pxe-boot cannot be used on this machine"
|
||||||
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "=== Debug environment ready ==="
|
echo "=== Debug environment ready ==="
|
||||||
echo " nc $IP_ADDR 2323 (remote shell)"
|
echo " nc $IP_ADDR 2323 (remote shell)"
|
||||||
echo " ssh root@$IP_ADDR (password: debug)"
|
echo " ssh root@$IP_ADDR (password: debug)"
|
||||||
|
if [ -n "$ROOT_DEVICE" ]; then
|
||||||
|
echo " root: $ROOT_DEVICE $ROOT_ARGS"
|
||||||
|
fi
|
||||||
echo "==============================="
|
echo "==============================="
|
||||||
`;
|
`;
|
||||||
return reply.type("text/plain").send(script);
|
return reply.type("text/plain").send(script);
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ export class LabdClient {
|
|||||||
memory_gb?: number; arch?: string;
|
memory_gb?: number; arch?: string;
|
||||||
disks?: Array<{ name: string; size_gb: number; model: string }>;
|
disks?: Array<{ name: string; size_gb: number; model: string }>;
|
||||||
nics?: Array<{ name: string; mac: string; state: string }>;
|
nics?: Array<{ name: string; mac: string; state: string }>;
|
||||||
|
root_device?: string; root_args?: string;
|
||||||
}): Promise<{ status: string; error?: string }> {
|
}): Promise<{ status: string; error?: string }> {
|
||||||
return this.request("POST", "/api/machines/discover", { body: data });
|
return this.request("POST", "/api/machines/discover", { body: data });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { join } from "node:path";
|
|||||||
import { Command } from "commander";
|
import { Command } from "commander";
|
||||||
import type { BastionState } from "@lab/shared";
|
import type { BastionState } from "@lab/shared";
|
||||||
import { getLabdClient } from "../api/config.js";
|
import { getLabdClient } from "../api/config.js";
|
||||||
|
import { ROOT_DEVICE_PROBE, parseRootProbe } from "../utils/hardware-probe.js";
|
||||||
|
|
||||||
/** Resolve a target (hostname, MAC, or IP) to {mac, hostname, ip} from state. */
|
/** Resolve a target (hostname, MAC, or IP) to {mac, hostname, ip} from state. */
|
||||||
function resolveTarget(
|
function resolveTarget(
|
||||||
@@ -44,6 +45,54 @@ function resolveTarget(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The local admin account to SSH as (root is not usable — it has no key here). */
|
||||||
|
function sshUser(): string {
|
||||||
|
const adminUser = process.env["SUDO_USER"] ?? process.env["USER"] ?? "";
|
||||||
|
return adminUser === "root" ? "" : adminUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Common ssh arguments, ending with user@host. Null when there is no usable user. */
|
||||||
|
function sshBaseArgs(ip: string): string[] | null {
|
||||||
|
const user = sshUser();
|
||||||
|
if (user === "") return null;
|
||||||
|
|
||||||
|
const sudoUser = process.env["SUDO_USER"];
|
||||||
|
const realHome = sudoUser !== undefined ? join("/home", sudoUser) : homedir();
|
||||||
|
const sshKey = ["id_ed25519", "id_rsa", "id_ecdsa"]
|
||||||
|
.map((name) => join(realHome, ".ssh", name))
|
||||||
|
.find((k) => existsSync(k));
|
||||||
|
|
||||||
|
return [
|
||||||
|
"-o", "StrictHostKeyChecking=no",
|
||||||
|
"-o", "UserKnownHostsFile=/dev/null",
|
||||||
|
"-o", "ConnectTimeout=10",
|
||||||
|
...(sshKey !== undefined ? ["-i", sshKey] : []),
|
||||||
|
`${user}@${ip}`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a shell script on the target as root and return its stdout, or null.
|
||||||
|
*
|
||||||
|
* The script goes over stdin rather than the command line so it can contain quotes
|
||||||
|
* without a second round of shell escaping. `sudo -n` fails fast instead of hanging on
|
||||||
|
* a password prompt that would then eat the script.
|
||||||
|
*/
|
||||||
|
function sshCapture(ip: string, script: string): string | null {
|
||||||
|
const base = sshBaseArgs(ip);
|
||||||
|
if (base === null) return null;
|
||||||
|
try {
|
||||||
|
return execFileSync("ssh", [...base, "sudo", "-n", "sh", "-s"], {
|
||||||
|
input: script,
|
||||||
|
encoding: "utf-8",
|
||||||
|
stdio: ["pipe", "pipe", "pipe"],
|
||||||
|
timeout: 30_000,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function registerDebugCommand(parent: Command): void {
|
export function registerDebugCommand(parent: Command): void {
|
||||||
parent
|
parent
|
||||||
.command("debug <target>")
|
.command("debug <target>")
|
||||||
@@ -71,6 +120,31 @@ export function registerDebugCommand(parent: Command): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { mac, hostname, ip } = resolved;
|
const { mac, hostname, ip } = resolved;
|
||||||
|
|
||||||
|
// --pxe-boot needs a root= for the installed system. If the machine is still
|
||||||
|
// reachable, observe it now rather than assuming a disk layout: a wrong root=
|
||||||
|
// leaves the machine unbootable. If it isn't reachable, dispatch falls back to
|
||||||
|
// rescue and the operator reports the real one from there.
|
||||||
|
if (opts.pxeBoot === true && ip !== "") {
|
||||||
|
const known = state.installed[mac]?.root_device ?? state.discovered[mac]?.root_device;
|
||||||
|
if (known === undefined || known === "") {
|
||||||
|
console.log(`No root device recorded for ${hostname}. Probing over SSH...`);
|
||||||
|
const probe = sshCapture(ip, ROOT_DEVICE_PROBE);
|
||||||
|
const root = probe === null ? {} : parseRootProbe(probe);
|
||||||
|
if (root.root_device !== undefined) {
|
||||||
|
console.log(` root=${root.root_device}${root.root_args ? ` ${root.root_args}` : ""}`);
|
||||||
|
try {
|
||||||
|
await client.discoverMachine({ mac, ...root });
|
||||||
|
} catch (err) {
|
||||||
|
console.error(` Could not record it: ${err instanceof Error ? err.message : String(err)}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(" Probe failed. Booting rescue instead; report the root device with:");
|
||||||
|
console.log(" curl http://<bastion>:8080/debug-setup.sh | bash");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`Queuing debug mode for ${hostname} (${mac})...`);
|
console.log(`Queuing debug mode for ${hostname} (${mac})...`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -86,32 +160,15 @@ export function registerDebugCommand(parent: Command): void {
|
|||||||
|
|
||||||
// Try SSH reboot into PXE
|
// Try SSH reboot into PXE
|
||||||
if (ip !== "") {
|
if (ip !== "") {
|
||||||
const adminUser = process.env["SUDO_USER"] ?? process.env["USER"] ?? "";
|
const base = sshBaseArgs(ip);
|
||||||
const effectiveUser = adminUser === "root" ? "" : adminUser;
|
if (base !== null) {
|
||||||
|
console.log(`\nAttempting SSH reboot into PXE (${sshUser()}@${ip})...`);
|
||||||
if (effectiveUser !== "") {
|
|
||||||
console.log(`\nAttempting SSH reboot into PXE (${effectiveUser}@${ip})...`);
|
|
||||||
|
|
||||||
const sudoUser = process.env["SUDO_USER"];
|
|
||||||
const realHome = sudoUser !== undefined ? join("/home", sudoUser) : homedir();
|
|
||||||
const keyPaths = [
|
|
||||||
join(realHome, ".ssh", "id_ed25519"),
|
|
||||||
join(realHome, ".ssh", "id_rsa"),
|
|
||||||
join(realHome, ".ssh", "id_ecdsa"),
|
|
||||||
];
|
|
||||||
const sshKey = keyPaths.find(k => existsSync(k));
|
|
||||||
|
|
||||||
const sshArgs = [
|
|
||||||
"-o", "StrictHostKeyChecking=no",
|
|
||||||
"-o", "UserKnownHostsFile=/dev/null",
|
|
||||||
"-o", "ConnectTimeout=10",
|
|
||||||
...(sshKey !== undefined ? ["-i", sshKey] : []),
|
|
||||||
`${effectiveUser}@${ip}`,
|
|
||||||
'PXE_ENTRY=$(sudo efibootmgr | grep -iE "pxe|network|ipv4" | head -1 | grep -oP "Boot\\K[0-9A-F]+"); if [ -n "$PXE_ENTRY" ]; then sudo efibootmgr --bootnext "$PXE_ENTRY" && echo "PXE set as next boot" && sudo reboot; else echo "No PXE boot entry found, rebooting anyway..." && sudo reboot; fi',
|
|
||||||
];
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
execFileSync("ssh", sshArgs, { stdio: "inherit" });
|
execFileSync("ssh", [
|
||||||
|
...base,
|
||||||
|
'PXE_ENTRY=$(sudo efibootmgr | grep -iE "pxe|network|ipv4" | head -1 | grep -oP "Boot\\K[0-9A-F]+"); if [ -n "$PXE_ENTRY" ]; then sudo efibootmgr --bootnext "$PXE_ENTRY" && echo "PXE set as next boot" && sudo reboot; else echo "No PXE boot entry found, rebooting anyway..." && sudo reboot; fi',
|
||||||
|
], { stdio: "inherit" });
|
||||||
} catch {
|
} catch {
|
||||||
// SSH connection closing during reboot is expected
|
// SSH connection closing during reboot is expected
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import type { Command } from "commander";
|
import type { Command } from "commander";
|
||||||
import { sshExec } from "@lab/modules";
|
import { sshExec } from "@lab/modules";
|
||||||
import { getLabdClient } from "../api/config.js";
|
import { getLabdClient } from "../api/config.js";
|
||||||
|
import { ROOT_DEVICE_PROBE } from "../utils/hardware-probe.js";
|
||||||
|
|
||||||
const BOLD = "\x1b[1m";
|
const BOLD = "\x1b[1m";
|
||||||
const GREEN = "\x1b[0;32m";
|
const GREEN = "\x1b[0;32m";
|
||||||
@@ -24,7 +25,9 @@ const HW_COLLECT_SCRIPT = [
|
|||||||
'N=$(grep -c "^processor" /proc/cpuinfo 2>/dev/null || echo 0)',
|
'N=$(grep -c "^processor" /proc/cpuinfo 2>/dev/null || echo 0)',
|
||||||
'R=$(awk "/MemTotal/ {printf \\"%d\\", \\$2/1024/1024}" /proc/meminfo 2>/dev/null || echo 0)',
|
'R=$(awk "/MemTotal/ {printf \\"%d\\", \\$2/1024/1024}" /proc/meminfo 2>/dev/null || echo 0)',
|
||||||
'A=$(uname -m)',
|
'A=$(uname -m)',
|
||||||
'printf \'{"product":"%s","board":"%s","serial":"%s","manufacturer":"%s","cpu_model":"%s","cpu_cores":%s,"memory_gb":%s,"arch":"%s"}\\n\' "$P" "$B" "$S" "$M" "$C" "$N" "$R" "$A"',
|
// Root filesystem, so --pxe-boot has a root= to use instead of assuming our layout.
|
||||||
|
ROOT_DEVICE_PROBE,
|
||||||
|
'printf \'{"product":"%s","board":"%s","serial":"%s","manufacturer":"%s","cpu_model":"%s","cpu_cores":%s,"memory_gb":%s,"arch":"%s","root_device":"%s","root_args":"%s"}\\n\' "$P" "$B" "$S" "$M" "$C" "$N" "$R" "$A" "$RD" "$RA"',
|
||||||
].join("; ");
|
].join("; ");
|
||||||
|
|
||||||
export function registerRecheckCommand(parent: Command): void {
|
export function registerRecheckCommand(parent: Command): void {
|
||||||
@@ -81,7 +84,8 @@ export function registerRecheckCommand(parent: Command): void {
|
|||||||
const cpu = hwData.cpu_model || "?";
|
const cpu = hwData.cpu_model || "?";
|
||||||
const cores = hwData.cpu_cores || "?";
|
const cores = hwData.cpu_cores || "?";
|
||||||
const mem = hwData.memory_gb || "?";
|
const mem = hwData.memory_gb || "?";
|
||||||
console.log(`${GREEN}OK${RESET} ${DIM}${cpu}, ${cores} cores, ${mem}GB${RESET}`);
|
const root = hwData.root_device ? `, root=${hwData.root_device}` : "";
|
||||||
|
console.log(`${GREEN}OK${RESET} ${DIM}${cpu}, ${cores} cores, ${mem}GB${root}${RESET}`);
|
||||||
updated++;
|
updated++;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(`${RED}FAIL${RESET} ${DIM}${err instanceof Error ? err.message : String(err)}${RESET}`);
|
console.log(`${RED}FAIL${RESET} ${DIM}${err instanceof Error ? err.message : String(err)}${RESET}`);
|
||||||
|
|||||||
59
bastion/src/cli/src/utils/hardware-probe.ts
Normal file
59
bastion/src/cli/src/utils/hardware-probe.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
// Shell snippets for observing a machine's hardware over SSH.
|
||||||
|
//
|
||||||
|
// Pure shell + awk, no Python: these run on whatever the target happens to be,
|
||||||
|
// including a minimal rescue environment.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Report the root filesystem and any dracut arguments needed to assemble it.
|
||||||
|
*
|
||||||
|
* Emits two lines:
|
||||||
|
* ROOT_DEVICE=<device>
|
||||||
|
* ROOT_ARGS=<args>
|
||||||
|
*
|
||||||
|
* Used by `--pxe-boot`, which boots the installed system with a kernel and initrd from
|
||||||
|
* the network. Getting root= wrong there leaves the machine unbootable, so this observes
|
||||||
|
* the machine rather than assuming our Fedora LVM layout.
|
||||||
|
*
|
||||||
|
* Device form is chosen for stability across reboots: LVM logical volumes keep their
|
||||||
|
* /dev/mapper path, anything else is reported by UUID, which survives device renumbering.
|
||||||
|
*/
|
||||||
|
export const ROOT_DEVICE_PROBE = [
|
||||||
|
'RD=$(findmnt -no SOURCE / 2>/dev/null | head -1)',
|
||||||
|
'RA=""',
|
||||||
|
'RT=$(lsblk -no TYPE "$RD" 2>/dev/null | head -1)',
|
||||||
|
'if [ "$RT" = "lvm" ]; then',
|
||||||
|
' VGLV=$(lvs --noheadings -o vg_name,lv_name "$RD" 2>/dev/null | awk \'{print $1"/"$2}\')',
|
||||||
|
' [ -n "$VGLV" ] && RA="rd.lvm.lv=$VGLV"',
|
||||||
|
// Swap must be assembled too or resume= stalls the boot waiting for it.
|
||||||
|
' SW=$(awk \'NR>1 {print $1; exit}\' /proc/swaps 2>/dev/null)',
|
||||||
|
' if [ -n "$SW" ] && [ "$(lsblk -no TYPE "$SW" 2>/dev/null | head -1)" = "lvm" ]; then',
|
||||||
|
' SWVGLV=$(lvs --noheadings -o vg_name,lv_name "$SW" 2>/dev/null | awk \'{print $1"/"$2}\')',
|
||||||
|
' [ -n "$SWVGLV" ] && [ "$SWVGLV" != "$VGLV" ] && RA="$RA rd.lvm.lv=$SWVGLV"',
|
||||||
|
' fi',
|
||||||
|
'elif [ -n "$RD" ]; then',
|
||||||
|
' U=$(findmnt -no UUID / 2>/dev/null | head -1)',
|
||||||
|
' [ -n "$U" ] && RD="UUID=$U"',
|
||||||
|
'fi',
|
||||||
|
'printf \'ROOT_DEVICE=%s\\nROOT_ARGS=%s\\n\' "$RD" "$RA"',
|
||||||
|
].join("; ");
|
||||||
|
|
||||||
|
export interface RootInfo {
|
||||||
|
root_device?: string;
|
||||||
|
root_args?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parse the ROOT_DEVICE/ROOT_ARGS lines emitted by ROOT_DEVICE_PROBE. */
|
||||||
|
export function parseRootProbe(stdout: string): RootInfo {
|
||||||
|
const out: RootInfo = {};
|
||||||
|
for (const line of stdout.split("\n")) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (trimmed.startsWith("ROOT_DEVICE=")) {
|
||||||
|
const v = trimmed.slice("ROOT_DEVICE=".length).trim();
|
||||||
|
if (v !== "") out.root_device = v;
|
||||||
|
} else if (trimmed.startsWith("ROOT_ARGS=")) {
|
||||||
|
const v = trimmed.slice("ROOT_ARGS=".length).trim();
|
||||||
|
if (v !== "") out.root_args = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
@@ -299,6 +299,7 @@ export function registerBastionRoutes(app: FastifyInstance, db: DbClient): void
|
|||||||
memory_gb?: number; arch?: string;
|
memory_gb?: number; arch?: string;
|
||||||
disks?: Array<{ name: string; size_gb: number; model: string }>;
|
disks?: Array<{ name: string; size_gb: number; model: string }>;
|
||||||
nics?: Array<{ name: string; mac: string; state: string }>;
|
nics?: Array<{ name: string; mac: string; state: string }>;
|
||||||
|
root_device?: string; root_args?: string;
|
||||||
};
|
};
|
||||||
}>("/api/machines/discover", async (request, reply) => {
|
}>("/api/machines/discover", async (request, reply) => {
|
||||||
const data = request.body ?? {};
|
const data = request.body ?? {};
|
||||||
|
|||||||
Reference in New Issue
Block a user