feat: provision register + k3s kubeconfig #8
@@ -49,6 +49,9 @@ _labctl() {
|
|||||||
"app k3s list")
|
"app k3s list")
|
||||||
COMPREPLY=($(compgen -W "--user -h --help" -- "$cur"))
|
COMPREPLY=($(compgen -W "--user -h --help" -- "$cur"))
|
||||||
return ;;
|
return ;;
|
||||||
|
"app k3s kubeconfig")
|
||||||
|
COMPREPLY=($(compgen -W "--user --context --print -h --help" -- "$cur"))
|
||||||
|
return ;;
|
||||||
"init bastion")
|
"init bastion")
|
||||||
COMPREPLY=($(compgen -W "standalone -h --help" -- "$cur"))
|
COMPREPLY=($(compgen -W "standalone -h --help" -- "$cur"))
|
||||||
return ;;
|
return ;;
|
||||||
@@ -67,6 +70,9 @@ _labctl() {
|
|||||||
"provision forget")
|
"provision forget")
|
||||||
COMPREPLY=($(compgen -W "-h --help" -- "$cur"))
|
COMPREPLY=($(compgen -W "-h --help" -- "$cur"))
|
||||||
return ;;
|
return ;;
|
||||||
|
"provision register")
|
||||||
|
COMPREPLY=($(compgen -W "--role --ip -h --help" -- "$cur"))
|
||||||
|
return ;;
|
||||||
"provision logs")
|
"provision logs")
|
||||||
COMPREPLY=($(compgen -W "-f --follow -h --help" -- "$cur"))
|
COMPREPLY=($(compgen -W "-f --follow -h --help" -- "$cur"))
|
||||||
return ;;
|
return ;;
|
||||||
@@ -89,7 +95,7 @@ _labctl() {
|
|||||||
COMPREPLY=($(compgen -W "deploy status -h --help" -- "$cur"))
|
COMPREPLY=($(compgen -W "deploy status -h --help" -- "$cur"))
|
||||||
return ;;
|
return ;;
|
||||||
"app k3s")
|
"app k3s")
|
||||||
COMPREPLY=($(compgen -W "install health list -h --help" -- "$cur"))
|
COMPREPLY=($(compgen -W "install health list kubeconfig -h --help" -- "$cur"))
|
||||||
return ;;
|
return ;;
|
||||||
"version")
|
"version")
|
||||||
COMPREPLY=($(compgen -W "-h --help" -- "$cur"))
|
COMPREPLY=($(compgen -W "-h --help" -- "$cur"))
|
||||||
@@ -98,7 +104,7 @@ _labctl() {
|
|||||||
COMPREPLY=($(compgen -W "bastion -h --help" -- "$cur"))
|
COMPREPLY=($(compgen -W "bastion -h --help" -- "$cur"))
|
||||||
return ;;
|
return ;;
|
||||||
"provision")
|
"provision")
|
||||||
COMPREPLY=($(compgen -W "list install reprovision debug forget logs makeiso -h --help" -- "$cur"))
|
COMPREPLY=($(compgen -W "list install reprovision debug forget register logs makeiso -h --help" -- "$cur"))
|
||||||
return ;;
|
return ;;
|
||||||
"config")
|
"config")
|
||||||
COMPREPLY=($(compgen -W "list get set path -h --help" -- "$cur"))
|
COMPREPLY=($(compgen -W "list get set path -h --help" -- "$cur"))
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ complete -c labctl -n "__labctl_using_cmd provision" -a install -d 'Queue a disc
|
|||||||
complete -c labctl -n "__labctl_using_cmd provision" -a reprovision -d 'Queue install + SSH reboot into PXE (target: hostname, MAC, or IP)'
|
complete -c labctl -n "__labctl_using_cmd provision" -a reprovision -d 'Queue install + SSH reboot into PXE (target: hostname, MAC, or IP)'
|
||||||
complete -c labctl -n "__labctl_using_cmd provision" -a debug -d 'PXE boot into Fedora rescue mode for debugging (target: hostname, MAC, or IP)'
|
complete -c labctl -n "__labctl_using_cmd provision" -a debug -d 'PXE boot into Fedora rescue mode for debugging (target: hostname, MAC, or IP)'
|
||||||
complete -c labctl -n "__labctl_using_cmd provision" -a forget -d 'Remove a machine from bastion state'
|
complete -c labctl -n "__labctl_using_cmd provision" -a forget -d 'Remove a machine from bastion state'
|
||||||
|
complete -c labctl -n "__labctl_using_cmd provision" -a register -d 'Register an already-installed machine (e.g. after state loss)'
|
||||||
complete -c labctl -n "__labctl_using_cmd provision" -a logs -d 'Show provisioning logs for a machine (hostname, MAC, or IP)'
|
complete -c labctl -n "__labctl_using_cmd provision" -a logs -d 'Show provisioning logs for a machine (hostname, MAC, or IP)'
|
||||||
complete -c labctl -n "__labctl_using_cmd provision" -a makeiso -d 'Generate a UEFI-bootable iPXE ISO for network provisioning'
|
complete -c labctl -n "__labctl_using_cmd provision" -a makeiso -d 'Generate a UEFI-bootable iPXE ISO for network provisioning'
|
||||||
|
|
||||||
@@ -140,6 +141,10 @@ complete -c labctl -n "__labctl_in_cmd provision reprovision" -l disk -d 'Target
|
|||||||
# provision debug options
|
# provision debug options
|
||||||
complete -c labctl -n "__labctl_in_cmd provision debug" -l pxe-boot -d 'Boot installed system via PXE (kernel+initrd from network, root from NVMe)'
|
complete -c labctl -n "__labctl_in_cmd provision debug" -l pxe-boot -d 'Boot installed system via PXE (kernel+initrd from network, root from NVMe)'
|
||||||
|
|
||||||
|
# provision register options
|
||||||
|
complete -c labctl -n "__labctl_in_cmd provision register" -l role -d 'Machine role' -xa 'vanilla worker infra labcontroller'
|
||||||
|
complete -c labctl -n "__labctl_in_cmd provision register" -l ip -d 'Machine IP address' -x
|
||||||
|
|
||||||
# provision logs options
|
# provision logs options
|
||||||
complete -c labctl -n "__labctl_in_cmd provision logs" -s f -l follow -d 'Follow log output in real-time'
|
complete -c labctl -n "__labctl_in_cmd provision logs" -s f -l follow -d 'Follow log output in real-time'
|
||||||
|
|
||||||
@@ -179,6 +184,7 @@ complete -c labctl -n "__labctl_in_cmd app labcontroller status" -l user -d 'SSH
|
|||||||
complete -c labctl -n "__labctl_using_cmd app k3s" -a install -d 'Install k3s on a target machine (hostname, IP, or MAC)'
|
complete -c labctl -n "__labctl_using_cmd app k3s" -a install -d 'Install k3s on a target machine (hostname, IP, or MAC)'
|
||||||
complete -c labctl -n "__labctl_using_cmd app k3s" -a health -d 'Check k3s health (all hosts if no target given)'
|
complete -c labctl -n "__labctl_using_cmd app k3s" -a health -d 'Check k3s health (all hosts if no target given)'
|
||||||
complete -c labctl -n "__labctl_using_cmd app k3s" -a list -d 'List installed machines and their k3s status'
|
complete -c labctl -n "__labctl_using_cmd app k3s" -a list -d 'List installed machines and their k3s status'
|
||||||
|
complete -c labctl -n "__labctl_using_cmd app k3s" -a kubeconfig -d 'Fetch kubeconfig from a target and merge into ~/.kube/config'
|
||||||
|
|
||||||
# app k3s install options
|
# app k3s install options
|
||||||
complete -c labctl -n "__labctl_in_cmd app k3s install" -l role -d 'k3s role: infra (server) or worker (agent)' -x
|
complete -c labctl -n "__labctl_in_cmd app k3s install" -l role -d 'k3s role: infra (server) or worker (agent)' -x
|
||||||
@@ -192,3 +198,8 @@ complete -c labctl -n "__labctl_in_cmd app k3s health" -l user -d 'SSH user' -x
|
|||||||
# app k3s list options
|
# app k3s list options
|
||||||
complete -c labctl -n "__labctl_in_cmd app k3s list" -l user -d 'SSH user' -x
|
complete -c labctl -n "__labctl_in_cmd app k3s list" -l user -d 'SSH user' -x
|
||||||
|
|
||||||
|
# app k3s kubeconfig options
|
||||||
|
complete -c labctl -n "__labctl_in_cmd app k3s kubeconfig" -l user -d 'SSH user' -x
|
||||||
|
complete -c labctl -n "__labctl_in_cmd app k3s kubeconfig" -l context -d 'Context name (defaults to hostname)' -x
|
||||||
|
complete -c labctl -n "__labctl_in_cmd app k3s kubeconfig" -l print -d 'Print kubeconfig to stdout instead of merging'
|
||||||
|
|
||||||
|
|||||||
@@ -294,6 +294,21 @@ export async function startBastion(overrides: Partial<BastionConfig> = {}): Prom
|
|||||||
return { status: "ok", data: { mac } };
|
return { status: "ok", data: { mac } };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
labdConn.onCommand("command-register", async (msg) => {
|
||||||
|
if (msg.type !== "command-register") throw new Error("unexpected");
|
||||||
|
const mac = msg.mac.toLowerCase();
|
||||||
|
state.update((s) => {
|
||||||
|
s.installed[mac] = {
|
||||||
|
hostname: msg.hostname,
|
||||||
|
role: msg.role,
|
||||||
|
ip: msg.ip,
|
||||||
|
installed_at: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
logger.info(`MACHINE REGISTERED: ${mac} -> ${msg.hostname} (${msg.role}) ip=${msg.ip}`);
|
||||||
|
return { status: "ok", data: { mac, hostname: msg.hostname } };
|
||||||
|
});
|
||||||
|
|
||||||
labdConn.onCommand("command-role-update", async (msg) => {
|
labdConn.onCommand("command-role-update", async (msg) => {
|
||||||
if (msg.type !== "command-role-update") throw new Error("unexpected");
|
if (msg.type !== "command-role-update") throw new Error("unexpected");
|
||||||
const mac = msg.mac.toLowerCase();
|
const mac = msg.mac.toLowerCase();
|
||||||
|
|||||||
@@ -315,6 +315,50 @@ export function registerApiRoutes(
|
|||||||
return reply.send({ status: "ok", mac, new: isNew });
|
return reply.send({ status: "ok", mac, new: isNew });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Register an already-installed machine (e.g. re-add after state loss)
|
||||||
|
app.post<{
|
||||||
|
Body: {
|
||||||
|
mac?: string;
|
||||||
|
hostname?: string;
|
||||||
|
role?: string;
|
||||||
|
ip?: string;
|
||||||
|
};
|
||||||
|
}>("/api/register", async (request, reply) => {
|
||||||
|
const { mac: rawMac, hostname, role, ip } = request.body ?? {};
|
||||||
|
const mac = (rawMac ?? "").toLowerCase().replace(/-/g, ":");
|
||||||
|
|
||||||
|
if (mac === "") {
|
||||||
|
return reply.status(400).send({ error: "mac is required" });
|
||||||
|
}
|
||||||
|
if (!hostname) {
|
||||||
|
return reply.status(400).send({ error: "hostname is required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const validRole = role ?? "worker";
|
||||||
|
if (!(SUPPORTED_ROLES as readonly string[]).includes(validRole)) {
|
||||||
|
return reply.status(400).send({ error: `invalid role: '${validRole}'. Supported: ${SUPPORTED_ROLES.join(", ")}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
state.update((s) => {
|
||||||
|
s.installed[mac] = {
|
||||||
|
hostname,
|
||||||
|
role: validRole,
|
||||||
|
ip: ip ?? "",
|
||||||
|
installed_at: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(`MACHINE REGISTERED: ${mac} -> hostname=${hostname} role=${validRole} ip=${ip ?? ""}`);
|
||||||
|
|
||||||
|
return reply.send({
|
||||||
|
status: "registered",
|
||||||
|
mac,
|
||||||
|
hostname,
|
||||||
|
role: validRole,
|
||||||
|
ip: ip ?? "",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Update a machine's role (e.g. promote infra -> labcontroller)
|
// Update a machine's role (e.g. promote infra -> labcontroller)
|
||||||
app.post<{
|
app.post<{
|
||||||
Body: {
|
Body: {
|
||||||
|
|||||||
@@ -165,6 +165,7 @@ export class BastionConnection {
|
|||||||
case "command-forget":
|
case "command-forget":
|
||||||
case "command-role-update":
|
case "command-role-update":
|
||||||
case "command-debug":
|
case "command-debug":
|
||||||
|
case "command-register":
|
||||||
void this.handleCommand(msg);
|
void this.handleCommand(msg);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,6 +94,12 @@ export class LabdClient {
|
|||||||
return this.request("POST", "/api/machines/install", { body: opts });
|
return this.request("POST", "/api/machines/install", { body: opts });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async registerMachine(opts: {
|
||||||
|
mac: string; hostname: string; role?: string; ip?: string;
|
||||||
|
}): Promise<{ status: string; data?: unknown; error?: string }> {
|
||||||
|
return this.request("POST", "/api/machines/register", { body: opts });
|
||||||
|
}
|
||||||
|
|
||||||
async debugMachine(mac: string, opts?: { pxeBoot?: boolean }): Promise<{ status: string; data?: { mac: string; hostname: string }; error?: string }> {
|
async debugMachine(mac: string, opts?: { pxeBoot?: boolean }): Promise<{ status: string; data?: { mac: string; hostname: string }; error?: string }> {
|
||||||
return this.request("POST", "/api/machines/debug", { body: { mac, pxeBoot: opts?.pxeBoot } });
|
return this.request("POST", "/api/machines/debug", { body: { mac, pxeBoot: opts?.pxeBoot } });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
// CLI command: labctl app k3s install/health <target>
|
// CLI command: labctl app k3s install/health <target>
|
||||||
// Install or check k3s on a target machine via SSH.
|
// Install or check k3s on a target machine via SSH.
|
||||||
|
|
||||||
import { existsSync } from "node:fs";
|
import { existsSync, writeFileSync, mkdirSync } from "node:fs";
|
||||||
import { homedir } from "node:os";
|
import { homedir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
|
import { execSync } from "node:child_process";
|
||||||
import type { Command } from "commander";
|
import type { Command } from "commander";
|
||||||
import type { BastionState } from "@lab/shared";
|
import type { BastionState } from "@lab/shared";
|
||||||
import { K3sModule, sshExec } from "@lab/modules";
|
import { K3sModule, sshExec } from "@lab/modules";
|
||||||
@@ -400,4 +401,88 @@ export function registerAppCommand(program: Command): void {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
k3sCmd
|
||||||
|
.command("kubeconfig <target>")
|
||||||
|
.description("Fetch kubeconfig from a target and merge into ~/.kube/config")
|
||||||
|
.option("--user <user>", "SSH user", "root")
|
||||||
|
.option("--context <name>", "Context name (defaults to hostname)")
|
||||||
|
.option("--print", "Print kubeconfig to stdout instead of merging")
|
||||||
|
.action(async (target: string, opts: {
|
||||||
|
user: string;
|
||||||
|
context?: string;
|
||||||
|
print?: boolean;
|
||||||
|
}) => {
|
||||||
|
const state = await fetchState();
|
||||||
|
const resolved = resolveTarget(target, state);
|
||||||
|
|
||||||
|
if (!resolved) {
|
||||||
|
console.error(`Cannot resolve target: ${target}`);
|
||||||
|
console.error("Provide an IP address, hostname, or MAC of an installed machine.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sshKey = findSshKey();
|
||||||
|
|
||||||
|
// Fetch kubeconfig via SSH
|
||||||
|
let raw: string;
|
||||||
|
try {
|
||||||
|
const result = await sshExec(resolved.ip, opts.user, "cat /etc/rancher/k3s/k3s.yaml", {
|
||||||
|
...(sshKey ? { keyPath: sshKey } : {}),
|
||||||
|
timeoutMs: 10_000,
|
||||||
|
});
|
||||||
|
raw = result.stdout;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Failed to fetch kubeconfig: ${err instanceof Error ? err.message : String(err)}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const contextName = opts.context ?? resolved.hostname;
|
||||||
|
|
||||||
|
// Rewrite: replace 127.0.0.1 with actual IP, rename cluster/user/context
|
||||||
|
const rewritten = raw
|
||||||
|
.replace(/server:\s*https:\/\/127\.0\.0\.1:/, `server: https://${resolved.ip}:`)
|
||||||
|
.replace(/name:\s*default/g, `name: ${contextName}`)
|
||||||
|
.replace(/cluster:\s*default/g, `cluster: ${contextName}`)
|
||||||
|
.replace(/user:\s*default/g, `user: ${contextName}`)
|
||||||
|
.replace(/current-context:\s*default/, `current-context: ${contextName}`);
|
||||||
|
|
||||||
|
if (opts.print) {
|
||||||
|
process.stdout.write(rewritten);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge into ~/.kube/config using kubectl
|
||||||
|
const kubeDir = join(homedir(), ".kube");
|
||||||
|
mkdirSync(kubeDir, { recursive: true });
|
||||||
|
const mainConfig = join(kubeDir, "config");
|
||||||
|
const tmpFile = join(kubeDir, `.labctl-${contextName}.tmp`);
|
||||||
|
|
||||||
|
writeFileSync(tmpFile, rewritten, { mode: 0o600 });
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (existsSync(mainConfig)) {
|
||||||
|
const merged = execSync(
|
||||||
|
`KUBECONFIG="${mainConfig}:${tmpFile}" kubectl config view --flatten`,
|
||||||
|
{ encoding: "utf-8" },
|
||||||
|
);
|
||||||
|
writeFileSync(mainConfig, merged, { mode: 0o600 });
|
||||||
|
} else {
|
||||||
|
writeFileSync(mainConfig, rewritten, { mode: 0o600 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set current context
|
||||||
|
execSync(`kubectl config use-context ${contextName}`, { stdio: "pipe" });
|
||||||
|
|
||||||
|
console.log(`Merged kubeconfig for ${contextName} (${resolved.ip})`);
|
||||||
|
console.log(`Context set to: ${contextName}`);
|
||||||
|
console.log(`\nSwitch contexts: kubectl config use-context <name>`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Failed to merge kubeconfig: ${err instanceof Error ? err.message : String(err)}`);
|
||||||
|
console.error(`Standalone config saved at: ${tmpFile}`);
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
try { const { unlinkSync } = await import("node:fs"); unlinkSync(tmpFile); } catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
37
bastion/src/cli/src/commands/register.ts
Normal file
37
bastion/src/cli/src/commands/register.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
// CLI command: provision register
|
||||||
|
// Register an already-installed machine that is missing from bastion state.
|
||||||
|
|
||||||
|
import { Command, Option } from "commander";
|
||||||
|
import { SUPPORTED_ROLES } from "@lab/shared";
|
||||||
|
import { getLabdClient } from "../api/config.js";
|
||||||
|
|
||||||
|
export function registerRegisterCommand(parent: Command): void {
|
||||||
|
parent
|
||||||
|
.command("register <mac> <hostname>")
|
||||||
|
.description("Register an already-installed machine (e.g. after state loss)")
|
||||||
|
.addOption(new Option("--role <role>", "Machine role").choices([...SUPPORTED_ROLES]).default("worker"))
|
||||||
|
.option("--ip <address>", "Machine IP address")
|
||||||
|
.action(async (mac: string, hostname: string, opts: {
|
||||||
|
role: string;
|
||||||
|
ip?: string;
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
const result = await getLabdClient().registerMachine({
|
||||||
|
mac,
|
||||||
|
hostname,
|
||||||
|
role: opts.role,
|
||||||
|
...(opts.ip ? { ip: opts.ip } : {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
console.error(`Failed: ${result.error}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Registered ${mac} as ${hostname} (role=${opts.role}${opts.ip ? `, ip=${opts.ip}` : ""})`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Failed: ${err instanceof Error ? err.message : String(err)}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// CLI entry point for lab-bastion.
|
// CLI entry point for lab-bastion.
|
||||||
// Commands:
|
// Commands:
|
||||||
// init bastion standalone start/stop/status
|
// init bastion standalone start/stop/status
|
||||||
// provision list/install/reprovision/forget
|
// provision list/install/reprovision/forget/register
|
||||||
|
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import { Command, Option } from "commander";
|
import { Command, Option } from "commander";
|
||||||
@@ -16,6 +16,7 @@ import { registerListCommand } from "./commands/list.js";
|
|||||||
import { registerReprovisionCommand } from "./commands/reprovision.js";
|
import { registerReprovisionCommand } from "./commands/reprovision.js";
|
||||||
import { registerDebugCommand } from "./commands/debug.js";
|
import { registerDebugCommand } from "./commands/debug.js";
|
||||||
import { registerForgetCommand } from "./commands/forget.js";
|
import { registerForgetCommand } from "./commands/forget.js";
|
||||||
|
import { registerRegisterCommand } from "./commands/register.js";
|
||||||
import { registerLogsCommand } from "./commands/logs.js";
|
import { registerLogsCommand } from "./commands/logs.js";
|
||||||
import { registerMakeIsoCommand } from "./commands/makeiso.js";
|
import { registerMakeIsoCommand } from "./commands/makeiso.js";
|
||||||
import { registerConfigCommand } from "./commands/config.js";
|
import { registerConfigCommand } from "./commands/config.js";
|
||||||
@@ -98,6 +99,7 @@ export function createProgram(): Command {
|
|||||||
registerReprovisionCommand(provisionCmd);
|
registerReprovisionCommand(provisionCmd);
|
||||||
registerDebugCommand(provisionCmd);
|
registerDebugCommand(provisionCmd);
|
||||||
registerForgetCommand(provisionCmd);
|
registerForgetCommand(provisionCmd);
|
||||||
|
registerRegisterCommand(provisionCmd);
|
||||||
registerLogsCommand(provisionCmd);
|
registerLogsCommand(provisionCmd);
|
||||||
registerMakeIsoCommand(provisionCmd);
|
registerMakeIsoCommand(provisionCmd);
|
||||||
|
|
||||||
|
|||||||
@@ -172,6 +172,43 @@ export function registerBastionRoutes(app: FastifyInstance, db: DbClient): void
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Register an already-installed machine — route to correct bastion (or single bastion)
|
||||||
|
app.post<{
|
||||||
|
Body: { mac?: string; hostname?: string; role?: string; ip?: string };
|
||||||
|
}>("/api/machines/register", async (request, reply) => {
|
||||||
|
const { mac, hostname, role, ip } = request.body ?? {};
|
||||||
|
if (!mac || !hostname) {
|
||||||
|
return reply.code(400).send({ error: "mac and hostname are required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = mac.toLowerCase().replace(/-/g, ":");
|
||||||
|
|
||||||
|
// Find bastion that knows this MAC, or use single connected bastion
|
||||||
|
const bastion = bastionRegistry.findBastionByMac(normalized);
|
||||||
|
const target = bastion ?? (bastionRegistry.getAll().length === 1 ? bastionRegistry.getAll()[0] : null);
|
||||||
|
|
||||||
|
if (!target) {
|
||||||
|
const all = bastionRegistry.getAll();
|
||||||
|
if (all.length === 0) {
|
||||||
|
return reply.code(503).send({ error: "No bastions connected" });
|
||||||
|
}
|
||||||
|
return reply.code(404).send({ error: `MAC ${normalized} not found on any bastion and multiple bastions connected` });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await sendCommand(target.bastionId, {
|
||||||
|
type: "command-register",
|
||||||
|
mac: normalized,
|
||||||
|
hostname,
|
||||||
|
role: role ?? "worker",
|
||||||
|
ip: ip ?? "",
|
||||||
|
});
|
||||||
|
return reply.code(result.status === "ok" ? 200 : 500).send(result);
|
||||||
|
} catch (err) {
|
||||||
|
return reply.code(500).send({ error: err instanceof Error ? err.message : String(err) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Queue debug/rescue mode — route to correct bastion by MAC
|
// Queue debug/rescue mode — route to correct bastion by MAC
|
||||||
app.post<{
|
app.post<{
|
||||||
Body: { mac?: string; pxeBoot?: boolean };
|
Body: { mac?: string; pxeBoot?: boolean };
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ export type LabdBastionMessage =
|
|||||||
| { type: "command-forget"; requestId: string; mac: string }
|
| { type: "command-forget"; requestId: string; mac: string }
|
||||||
| { type: "command-role-update"; requestId: string; mac: string; role: string }
|
| { type: "command-role-update"; requestId: string; mac: string; role: string }
|
||||||
| { type: "command-debug"; requestId: string; mac: string; pxeBoot?: boolean }
|
| { type: "command-debug"; requestId: string; mac: string; pxeBoot?: boolean }
|
||||||
|
| { type: "command-register"; requestId: string; mac: string; hostname: string; role: string; ip: string }
|
||||||
| { type: "server-shutdown"; reconnectAfter: number };
|
| { type: "server-shutdown"; reconnectAfter: number };
|
||||||
|
|
||||||
export type BastionMessageType = BastionMessage["type"];
|
export type BastionMessageType = BastionMessage["type"];
|
||||||
@@ -126,7 +127,7 @@ const BASTION_MESSAGE_TYPES = new Set<string>([
|
|||||||
|
|
||||||
const LABD_BASTION_MESSAGE_TYPES = new Set<string>([
|
const LABD_BASTION_MESSAGE_TYPES = new Set<string>([
|
||||||
"bastion-enrolled", "bastion-heartbeat-ack", "command-install",
|
"bastion-enrolled", "bastion-heartbeat-ack", "command-install",
|
||||||
"command-forget", "command-role-update", "command-debug", "server-shutdown",
|
"command-forget", "command-role-update", "command-debug", "command-register", "server-shutdown",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export function isBastionMessage(msg: unknown): msg is BastionMessage {
|
export function isBastionMessage(msg: unknown): msg is BastionMessage {
|
||||||
|
|||||||
Reference in New Issue
Block a user