Files
fireclaw/src/firecracker-vm.ts

165 lines
3.6 KiB
TypeScript

/**
* Shared Firecracker VM lifecycle helpers.
* Used by vm.ts, snapshot.ts, and agent-manager.ts.
*/
import { spawn, type ChildProcess } from "node:child_process";
import { existsSync, unlinkSync, mkdirSync } from "node:fs";
import { CONFIG } from "./config.js";
import * as api from "./firecracker-api.js";
import {
ensureBridge,
ensureNat,
createTap,
deleteTap,
macFromOctet,
} from "./network.js";
export interface BootOptions {
socketPath: string;
kernelPath?: string;
rootfsPath: string;
extraDrives?: { id: string; path: string; readOnly?: boolean }[];
tapDevice: string;
ip: string;
octet: number;
vcpu?: number;
mem?: number;
}
/**
* Wait for a Firecracker API socket to appear.
*/
export function waitForSocket(
socketPath: string,
timeoutMs = 5_000
): Promise<void> {
return new Promise((resolve, reject) => {
const deadline = Date.now() + timeoutMs;
const check = () => {
if (existsSync(socketPath)) {
setTimeout(resolve, 200);
return;
}
if (Date.now() > deadline) {
reject(new Error("Firecracker socket did not appear"));
return;
}
setTimeout(check, 50);
};
check();
});
}
/**
* Set up network for a VM: ensure bridge, NAT, and create tap device.
* Cleans stale tap first.
*/
export function setupNetwork(tapDevice: string) {
ensureBridge();
ensureNat();
deleteTap(tapDevice);
createTap(tapDevice);
}
/**
* Spawn a Firecracker process and wait for the API socket.
*/
export async function spawnFirecracker(
socketPath: string,
opts?: { detached?: boolean }
): Promise<ChildProcess> {
// Clean stale socket
try {
unlinkSync(socketPath);
} catch {}
mkdirSync(CONFIG.socketDir, { recursive: true });
const proc = spawn(
CONFIG.firecrackerBin,
["--api-sock", socketPath],
{
stdio: "pipe",
detached: opts?.detached ?? false,
}
);
if (opts?.detached) proc.unref();
await waitForSocket(socketPath);
return proc;
}
/**
* Configure and start a Firecracker VM via its API.
*/
export async function bootVM(opts: BootOptions) {
const kernel = opts.kernelPath ?? CONFIG.kernelPath;
const vcpu = opts.vcpu ?? CONFIG.vm.vcpuCount;
const mem = opts.mem ?? CONFIG.vm.memSizeMib;
const bootArgs = [
"console=ttyS0",
"reboot=k",
"panic=1",
"pci=off",
"root=/dev/vda",
"rw",
`ip=${opts.ip}::${CONFIG.bridge.gateway}:${CONFIG.bridge.netmask}::eth0:off`,
].join(" ");
await api.putBootSource(opts.socketPath, kernel, bootArgs);
await api.putDrive(opts.socketPath, "rootfs", opts.rootfsPath);
if (opts.extraDrives) {
for (const drive of opts.extraDrives) {
await api.putDrive(
opts.socketPath,
drive.id,
drive.path,
drive.readOnly ?? false,
false
);
}
}
await api.putNetworkInterface(
opts.socketPath,
"eth0",
opts.tapDevice,
macFromOctet(opts.octet)
);
await api.putMachineConfig(opts.socketPath, vcpu, mem);
await api.startInstance(opts.socketPath);
}
/**
* Kill a Firecracker process and clean up its socket.
*/
export async function killFirecracker(
proc: ChildProcess | null,
socketPath: string,
signal: NodeJS.Signals = "SIGTERM"
) {
if (proc && !proc.killed) {
proc.kill(signal);
await new Promise<void>((resolve) => {
const timer = setTimeout(() => {
if (proc && !proc.killed) {
proc.kill("SIGKILL");
}
resolve();
}, 2_000);
proc.on("exit", () => {
clearTimeout(timer);
resolve();
});
});
}
try {
unlinkSync(socketPath);
} catch {}
}