Extract shared VM lifecycle helpers into firecracker-vm.ts
This commit is contained in:
164
src/firecracker-vm.ts
Normal file
164
src/firecracker-vm.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* 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 {}
|
||||
}
|
||||
Reference in New Issue
Block a user