This commit is contained in:
2026-02-08 21:47:33 +00:00
commit d8b5e8a6c0
22 changed files with 11080 additions and 0 deletions

8
ts/00_commitinfo_data.ts Normal file
View File

@@ -0,0 +1,8 @@
/**
* autocreated commitance data during CI/CD
*/
export const commitinfo = {
name: '@push.rocks/smartvm',
version: '1.0.0',
description: 'A TypeScript module wrapping Amazon Firecracker VMM for managing lightweight microVMs',
};

View File

@@ -0,0 +1,141 @@
import * as plugins from './plugins.js';
import type { IFirecrackerProcessOptions } from './interfaces/index.js';
import { SmartVMError } from './interfaces/index.js';
import { SocketClient } from './classes.socketclient.js';
/**
* Manages a single Firecracker child process, including startup, readiness polling, and shutdown.
*/
export class FirecrackerProcess {
private options: IFirecrackerProcessOptions;
private streaming: any | null = null;
private shell: InstanceType<typeof plugins.smartshell.Smartshell>;
private smartExitInstance: InstanceType<typeof plugins.smartexit.SmartExit> | null = null;
public socketClient: SocketClient;
constructor(options: IFirecrackerProcessOptions) {
this.options = options;
this.shell = new plugins.smartshell.Smartshell({ executor: 'bash' });
this.socketClient = new SocketClient({ socketPath: options.socketPath });
}
/**
* Start the Firecracker process and wait for the API socket to become ready.
*/
public async start(): Promise<void> {
// Remove any stale socket file
if (plugins.fs.existsSync(this.options.socketPath)) {
plugins.fs.unlinkSync(this.options.socketPath);
}
// Build the command
let cmd = `${this.options.binaryPath} --api-sock ${this.options.socketPath}`;
if (this.options.logLevel) {
cmd += ` --level ${this.options.logLevel}`;
}
// Spawn the process
this.streaming = await this.shell.execStreaming(cmd, true);
// Register with smartexit for automatic cleanup
if (this.streaming?.childProcess) {
this.smartExitInstance = new plugins.smartexit.SmartExit({ silent: true });
this.smartExitInstance.addProcess(this.streaming.childProcess);
}
// Wait for the socket file to appear
const socketReady = await this.waitForSocket(10000);
if (!socketReady) {
await this.stop();
throw new SmartVMError(
'Firecracker socket did not become ready within timeout',
'SOCKET_TIMEOUT',
);
}
// Wait for the API to be responsive
const apiReady = await this.socketClient.isReady(5000);
if (!apiReady) {
await this.stop();
throw new SmartVMError(
'Firecracker API did not become responsive within timeout',
'API_TIMEOUT',
);
}
}
/**
* Poll for the socket file to appear on disk.
*/
private async waitForSocket(timeoutMs: number): Promise<boolean> {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
if (plugins.fs.existsSync(this.options.socketPath)) {
return true;
}
await plugins.smartdelay.delayFor(100);
}
return false;
}
/**
* Gracefully stop the Firecracker process with SIGTERM, then SIGKILL after timeout.
*/
public async stop(): Promise<void> {
if (!this.streaming) return;
try {
// Try graceful termination first
await this.streaming.terminate();
// Wait up to 5 seconds for the process to exit
const exitPromise = Promise.race([
this.streaming.finalPromise,
plugins.smartdelay.delayFor(5000),
]);
await exitPromise;
} catch {
// If termination fails, force kill
try {
await this.streaming.kill();
} catch {
// Process may already be dead
}
}
this.streaming = null;
}
/**
* Clean up the socket file.
*/
public async cleanup(): Promise<void> {
await this.stop();
// Remove the socket file
if (plugins.fs.existsSync(this.options.socketPath)) {
plugins.fs.unlinkSync(this.options.socketPath);
}
}
/**
* Check if the process is currently running.
*/
public isRunning(): boolean {
if (!this.streaming?.childProcess) return false;
try {
// Sending signal 0 tests if process exists without actually sending a signal
process.kill(this.streaming.childProcess.pid, 0);
return true;
} catch {
return false;
}
}
/**
* Get the child process PID.
*/
public getPid(): number | null {
return this.streaming?.childProcess?.pid ?? null;
}
}

244
ts/classes.imagemanager.ts Normal file
View File

@@ -0,0 +1,244 @@
import * as plugins from './plugins.js';
import type { TFirecrackerArch } from './interfaces/index.js';
import { SmartVMError } from './interfaces/index.js';
/**
* Helper to check if a file or directory exists.
*/
async function pathExists(filePath: string): Promise<boolean> {
try {
await plugins.fs.promises.access(filePath);
return true;
} catch {
return false;
}
}
/**
* Manages Firecracker binaries, kernel images, and rootfs images.
* Downloads and caches them in a local data directory.
*/
export class ImageManager {
private dataDir: string;
private arch: TFirecrackerArch;
constructor(dataDir: string, arch: TFirecrackerArch = 'x86_64') {
this.dataDir = dataDir;
this.arch = arch;
}
/**
* Ensure all required directories exist.
*/
public async ensureDirectories(): Promise<void> {
const dirs = [
this.getBinDir(),
this.getKernelsDir(),
this.getRootfsDir(),
this.getSocketsDir(),
];
for (const dir of dirs) {
await plugins.fs.promises.mkdir(dir, { recursive: true });
}
}
/** Base directory for firecracker binaries. */
public getBinDir(): string {
return plugins.path.join(this.dataDir, 'bin');
}
/** Directory for kernel images. */
public getKernelsDir(): string {
return plugins.path.join(this.dataDir, 'kernels');
}
/** Directory for rootfs images. */
public getRootfsDir(): string {
return plugins.path.join(this.dataDir, 'rootfs');
}
/** Directory for Unix sockets. */
public getSocketsDir(): string {
return plugins.path.join(this.dataDir, 'sockets');
}
/**
* Get the path to the firecracker binary for a given version.
*/
public getFirecrackerPath(version: string): string {
return plugins.path.join(this.getBinDir(), version, 'firecracker');
}
/**
* Get the path to the jailer binary for a given version.
*/
public getJailerPath(version: string): string {
return plugins.path.join(this.getBinDir(), version, 'jailer');
}
/**
* Check if a Firecracker binary for the given version exists.
*/
public async hasBinary(version: string): Promise<boolean> {
const binPath = this.getFirecrackerPath(version);
return pathExists(binPath);
}
/**
* Query the GitHub API for the latest Firecracker release version tag.
*/
public async getLatestVersion(): Promise<string> {
try {
const response = await plugins.SmartRequest.create()
.url('https://api.github.com/repos/firecracker-microvm/firecracker/releases/latest')
.get();
const data = await response.json() as { tag_name: string };
return data.tag_name;
} catch (err) {
throw new SmartVMError(
`Failed to fetch latest Firecracker version: ${(err as Error).message}`,
'VERSION_FETCH_FAILED',
);
}
}
/**
* Download and extract the Firecracker binary for a given version.
*/
public async downloadFirecracker(version: string): Promise<string> {
const targetDir = plugins.path.join(this.getBinDir(), version);
await plugins.fs.promises.mkdir(targetDir, { recursive: true });
// Firecracker release tarball naming:
// firecracker-v1.5.0-x86_64.tgz containing release-v1.5.0-x86_64/firecracker-v1.5.0-x86_64
const tag = version.startsWith('v') ? version : `v${version}`;
const archiveName = `firecracker-${tag}-${this.arch}.tgz`;
const downloadUrl = `https://github.com/firecracker-microvm/firecracker/releases/download/${tag}/${archiveName}`;
const archivePath = plugins.path.join(targetDir, archiveName);
try {
// Download the archive
const shell = new plugins.smartshell.Smartshell({ executor: 'bash' });
await shell.exec(`curl -fSL -o "${archivePath}" "${downloadUrl}"`);
// Extract the archive
await shell.exec(`tar -xzf "${archivePath}" -C "${targetDir}"`);
// Firecracker archives contain a directory like release-v1.5.0-x86_64/
// with binaries named like firecracker-v1.5.0-x86_64
const extractedDir = plugins.path.join(targetDir, `release-${tag}-${this.arch}`);
const firecrackerSrc = plugins.path.join(extractedDir, `firecracker-${tag}-${this.arch}`);
const jailerSrc = plugins.path.join(extractedDir, `jailer-${tag}-${this.arch}`);
const firecrackerDst = this.getFirecrackerPath(version);
const jailerDst = this.getJailerPath(version);
// Move binaries to expected paths
await shell.exec(`mv "${firecrackerSrc}" "${firecrackerDst}"`);
if (await pathExists(jailerSrc)) {
await shell.exec(`mv "${jailerSrc}" "${jailerDst}"`);
}
// Make executable
await shell.exec(`chmod +x "${firecrackerDst}"`);
// Clean up
await shell.exec(`rm -rf "${archivePath}" "${extractedDir}"`);
return firecrackerDst;
} catch (err) {
throw new SmartVMError(
`Failed to download Firecracker ${version}: ${(err as Error).message}`,
'DOWNLOAD_FAILED',
);
}
}
/**
* Download a kernel image from a URL.
*/
public async downloadKernel(url: string, name: string): Promise<string> {
const kernelsDir = this.getKernelsDir();
await plugins.fs.promises.mkdir(kernelsDir, { recursive: true });
const kernelPath = plugins.path.join(kernelsDir, name);
try {
const shell = new plugins.smartshell.Smartshell({ executor: 'bash' });
await shell.exec(`curl -fSL -o "${kernelPath}" "${url}"`);
return kernelPath;
} catch (err) {
throw new SmartVMError(
`Failed to download kernel from ${url}: ${(err as Error).message}`,
'DOWNLOAD_FAILED',
);
}
}
/**
* Download a rootfs image from a URL.
*/
public async downloadRootfs(url: string, name: string): Promise<string> {
const rootfsDir = this.getRootfsDir();
await plugins.fs.promises.mkdir(rootfsDir, { recursive: true });
const rootfsPath = plugins.path.join(rootfsDir, name);
try {
const shell = new plugins.smartshell.Smartshell({ executor: 'bash' });
await shell.exec(`curl -fSL -o "${rootfsPath}" "${url}"`);
return rootfsPath;
} catch (err) {
throw new SmartVMError(
`Failed to download rootfs from ${url}: ${(err as Error).message}`,
'DOWNLOAD_FAILED',
);
}
}
/**
* Create a blank ext4 rootfs image.
*/
public async createBlankRootfs(name: string, sizeMib: number): Promise<string> {
const rootfsDir = this.getRootfsDir();
await plugins.fs.promises.mkdir(rootfsDir, { recursive: true });
const rootfsPath = plugins.path.join(rootfsDir, name);
try {
const shell = new plugins.smartshell.Smartshell({ executor: 'bash' });
await shell.exec(`dd if=/dev/zero of="${rootfsPath}" bs=1M count=${sizeMib}`);
await shell.exec(`mkfs.ext4 "${rootfsPath}"`);
return rootfsPath;
} catch (err) {
throw new SmartVMError(
`Failed to create blank rootfs: ${(err as Error).message}`,
'ROOTFS_CREATE_FAILED',
);
}
}
/**
* Clone a rootfs image for a specific VM.
*/
public async cloneRootfs(sourcePath: string, targetName: string): Promise<string> {
const rootfsDir = this.getRootfsDir();
await plugins.fs.promises.mkdir(rootfsDir, { recursive: true });
const targetPath = plugins.path.join(rootfsDir, targetName);
try {
const shell = new plugins.smartshell.Smartshell({ executor: 'bash' });
await shell.exec(`cp "${sourcePath}" "${targetPath}"`);
return targetPath;
} catch (err) {
throw new SmartVMError(
`Failed to clone rootfs: ${(err as Error).message}`,
'ROOTFS_CLONE_FAILED',
);
}
}
/**
* Generate a unique socket path for a VM.
*/
public getSocketPath(vmId: string): string {
return plugins.path.join(this.getSocketsDir(), `${vmId}.sock`);
}
}

367
ts/classes.microvm.ts Normal file
View File

@@ -0,0 +1,367 @@
import * as plugins from './plugins.js';
import type {
TVMState,
IMicroVMConfig,
ISnapshotCreateParams,
ISnapshotLoadParams,
ITapDevice,
} from './interfaces/index.js';
import { SmartVMError } from './interfaces/index.js';
import { VMConfig } from './classes.vmconfig.js';
import { SocketClient } from './classes.socketclient.js';
import { FirecrackerProcess } from './classes.firecrackerprocess.js';
import { NetworkManager } from './classes.networkmanager.js';
/**
* Represents a single Firecracker MicroVM with full lifecycle management.
* State machine: created → configuring → running → paused → stopped
*/
export class MicroVM {
public readonly id: string;
public state: TVMState = 'created';
private vmConfig: VMConfig;
private process: FirecrackerProcess | null = null;
private socketClient: SocketClient | null = null;
private networkManager: NetworkManager;
private binaryPath: string;
private socketPath: string;
private tapDevices: ITapDevice[] = [];
constructor(
id: string,
config: IMicroVMConfig,
binaryPath: string,
socketPath: string,
networkManager: NetworkManager,
) {
this.id = id;
this.vmConfig = new VMConfig(config);
this.binaryPath = binaryPath;
this.socketPath = socketPath;
this.networkManager = networkManager;
}
/**
* Assert that the VM is in one of the expected states.
*/
private assertState(expected: TVMState[], operation: string): void {
if (!expected.includes(this.state)) {
throw new SmartVMError(
`Cannot ${operation}: VM is in state '${this.state}', expected one of [${expected.join(', ')}]`,
'INVALID_STATE',
);
}
}
/**
* Start the MicroVM.
* Validates config, starts the Firecracker process, applies pre-boot configuration, and boots the VM.
*/
public async start(): Promise<void> {
this.assertState(['created'], 'start');
// Validate configuration
const validation = this.vmConfig.validate();
if (!validation.valid) {
throw new SmartVMError(
`Invalid VM configuration: ${validation.errors.join('; ')}`,
'INVALID_CONFIG',
);
}
this.state = 'configuring';
try {
// Start the Firecracker process
this.process = new FirecrackerProcess({
binaryPath: this.binaryPath,
socketPath: this.socketPath,
});
await this.process.start();
this.socketClient = this.process.socketClient;
// Apply pre-boot configuration in order
// 1. Logger (optional, must be first)
const loggerPayload = this.vmConfig.toLoggerPayload();
if (loggerPayload) {
await this.apiPut('/logger', loggerPayload);
}
// 2. Metrics (optional)
const metricsPayload = this.vmConfig.toMetricsPayload();
if (metricsPayload) {
await this.apiPut('/metrics', metricsPayload);
}
// 3. Machine config
await this.apiPut('/machine-config', this.vmConfig.toMachineConfigPayload());
// 4. Boot source
await this.apiPut('/boot-source', this.vmConfig.toBootSourcePayload());
// 5. Drives
if (this.vmConfig.config.drives) {
for (const drive of this.vmConfig.config.drives) {
const payload = this.vmConfig.toDrivePayload(drive);
await this.apiPut(`/drives/${drive.driveId}`, payload);
}
}
// 6. Network interfaces
if (this.vmConfig.config.networkInterfaces) {
for (const iface of this.vmConfig.config.networkInterfaces) {
// Create TAP device if hostDevName not manually specified
if (!iface.hostDevName) {
const tap = await this.networkManager.createTapDevice(this.id, iface.ifaceId);
this.tapDevices.push(tap);
iface.hostDevName = tap.tapName;
if (!iface.guestMac) {
iface.guestMac = tap.mac;
}
}
const payload = this.vmConfig.toNetworkInterfacePayload(iface);
await this.apiPut(`/network-interfaces/${iface.ifaceId}`, payload);
}
}
// 7. Vsock (optional)
const vsockPayload = this.vmConfig.toVsockPayload();
if (vsockPayload) {
await this.apiPut('/vsock', vsockPayload);
}
// 8. Balloon (optional)
const balloonPayload = this.vmConfig.toBalloonPayload();
if (balloonPayload) {
await this.apiPut('/balloon', balloonPayload);
}
// 9. MMDS config (optional)
const mmdsPayload = this.vmConfig.toMmdsConfigPayload();
if (mmdsPayload) {
await this.apiPut('/mmds/config', mmdsPayload);
}
// Boot the VM
await this.apiPut('/actions', { action_type: 'InstanceStart' });
this.state = 'running';
} catch (err) {
this.state = 'error';
// Clean up on failure
await this.cleanup();
if (err instanceof SmartVMError) {
throw err;
}
throw new SmartVMError(
`Failed to start VM ${this.id}: ${err.message}`,
'START_FAILED',
);
}
}
/**
* Pause the running VM.
*/
public async pause(): Promise<void> {
this.assertState(['running'], 'pause');
await this.apiPatch('/vm', { state: 'Paused' });
this.state = 'paused';
}
/**
* Resume a paused VM.
*/
public async resume(): Promise<void> {
this.assertState(['paused'], 'resume');
await this.apiPatch('/vm', { state: 'Resumed' });
this.state = 'running';
}
/**
* Stop the VM.
* Sends SendCtrlAltDel first for graceful shutdown, then kills the process if needed.
*/
public async stop(): Promise<void> {
this.assertState(['running', 'paused'], 'stop');
try {
// Try graceful shutdown via SendCtrlAltDel
await this.apiPut('/actions', { action_type: 'SendCtrlAltDel' });
// Wait a bit for the VM to shut down
await plugins.smartdelay.delayFor(2000);
} catch {
// SendCtrlAltDel may fail if the VM is already stopping
}
// Force stop the process
if (this.process) {
await this.process.stop();
}
this.state = 'stopped';
}
/**
* Create a snapshot of the VM.
*/
public async createSnapshot(params: ISnapshotCreateParams): Promise<void> {
this.assertState(['paused'], 'createSnapshot');
await this.apiPut('/snapshot/create', {
snapshot_path: params.snapshotPath,
mem_file_path: params.memFilePath,
snapshot_type: params.snapshotType || 'Full',
});
}
/**
* Load a snapshot into the VM.
*/
public async loadSnapshot(params: ISnapshotLoadParams): Promise<void> {
this.assertState(['created', 'configuring'], 'loadSnapshot');
await this.apiPut('/snapshot/load', {
snapshot_path: params.snapshotPath,
mem_file_path: params.memFilePath,
enable_diff_snapshots: params.enableDiffSnapshots ?? false,
resume_vm: params.resumeVm ?? false,
});
this.state = params.resumeVm ? 'running' : 'paused';
}
/**
* Set MMDS metadata.
*/
public async setMetadata(data: Record<string, any>): Promise<void> {
this.assertState(['running', 'paused'], 'setMetadata');
await this.apiPut('/mmds', data);
}
/**
* Get MMDS metadata.
*/
public async getMetadata(): Promise<any> {
this.assertState(['running', 'paused'], 'getMetadata');
const response = await this.socketClient!.get('/mmds');
return response.body;
}
/**
* Update a drive on the running VM (hot-plug path update).
*/
public async updateDrive(driveId: string, pathOnHost: string): Promise<void> {
this.assertState(['running', 'paused'], 'updateDrive');
await this.apiPatch(`/drives/${driveId}`, {
drive_id: driveId,
path_on_host: pathOnHost,
});
}
/**
* Update a network interface rate limiter on the running VM.
*/
public async updateNetworkInterface(
ifaceId: string,
update: Record<string, any>,
): Promise<void> {
this.assertState(['running', 'paused'], 'updateNetworkInterface');
await this.apiPatch(`/network-interfaces/${ifaceId}`, update);
}
/**
* Update balloon device on the running VM.
*/
public async updateBalloon(amountMib: number): Promise<void> {
this.assertState(['running', 'paused'], 'updateBalloon');
await this.apiPatch('/balloon', { amount_mib: amountMib });
}
/**
* Get VM instance info.
*/
public async getInfo(): Promise<any> {
const response = await this.socketClient!.get('/');
return response.body;
}
/**
* Get Firecracker version info.
*/
public async getVersion(): Promise<any> {
const response = await this.socketClient!.get('/version');
return response.body;
}
/**
* Get the TAP devices associated with this VM.
*/
public getTapDevices(): ITapDevice[] {
return [...this.tapDevices];
}
/**
* Get the VMConfig instance for payload inspection.
*/
public getVMConfig(): VMConfig {
return this.vmConfig;
}
/**
* Full cleanup: stop process, remove socket, remove TAP devices.
*/
public async cleanup(): Promise<void> {
// Stop process
if (this.process) {
await this.process.cleanup();
this.process = null;
}
// Remove TAP devices
for (const tap of this.tapDevices) {
await this.networkManager.removeTapDevice(tap.tapName);
}
this.tapDevices = [];
this.socketClient = null;
if (this.state !== 'error') {
this.state = 'stopped';
}
}
/**
* Helper: PUT request with error handling.
*/
private async apiPut(path: string, body: Record<string, any>): Promise<void> {
if (!this.socketClient) {
throw new SmartVMError('Socket client not initialized', 'NO_CLIENT');
}
const response = await this.socketClient.put(path, body);
if (!response.ok) {
throw new SmartVMError(
`API PUT ${path} failed with status ${response.statusCode}: ${JSON.stringify(response.body)}`,
'API_ERROR',
response.statusCode,
response.body,
);
}
}
/**
* Helper: PATCH request with error handling.
*/
private async apiPatch(path: string, body: Record<string, any>): Promise<void> {
if (!this.socketClient) {
throw new SmartVMError('Socket client not initialized', 'NO_CLIENT');
}
const response = await this.socketClient.patch(path, body);
if (!response.ok) {
throw new SmartVMError(
`API PATCH ${path} failed with status ${response.statusCode}: ${JSON.stringify(response.body)}`,
'API_ERROR',
response.statusCode,
response.body,
);
}
}
}

View File

@@ -0,0 +1,237 @@
import * as plugins from './plugins.js';
import type { INetworkManagerOptions, ITapDevice } from './interfaces/index.js';
import { SmartVMError } from './interfaces/index.js';
/**
* Manages host networking for Firecracker VMs.
* Creates TAP devices, Linux bridges, and configures NAT for VM internet access.
*/
export class NetworkManager {
private bridgeName: string;
private subnetBase: string;
private subnetCidr: number;
private gatewayIp: string;
private subnetMask: string;
private nextIpOctet: number;
private activeTaps: Map<string, ITapDevice> = new Map();
private bridgeCreated: boolean = false;
private shell: InstanceType<typeof plugins.smartshell.Smartshell>;
constructor(options: INetworkManagerOptions = {}) {
this.bridgeName = options.bridgeName || 'svbr0';
const subnet = options.subnet || '172.30.0.0/24';
// Parse the subnet
const [baseIp, cidrStr] = subnet.split('/');
this.subnetBase = baseIp;
this.subnetCidr = parseInt(cidrStr, 10);
this.subnetMask = this.cidrToSubnetMask(this.subnetCidr);
// Gateway is .1 in the subnet
const parts = this.subnetBase.split('.').map(Number);
parts[3] = 1;
this.gatewayIp = parts.join('.');
// VMs start at .2
this.nextIpOctet = 2;
this.shell = new plugins.smartshell.Smartshell({ executor: 'bash' });
}
/**
* Convert a CIDR prefix length to a dotted-decimal subnet mask.
*/
private cidrToSubnetMask(cidr: number): string {
const mask = (0xffffffff << (32 - cidr)) >>> 0;
return [
(mask >>> 24) & 0xff,
(mask >>> 16) & 0xff,
(mask >>> 8) & 0xff,
mask & 0xff,
].join('.');
}
/**
* Allocate the next available IP address in the subnet.
*/
public allocateIp(): string {
const parts = this.subnetBase.split('.').map(Number);
parts[3] = this.nextIpOctet;
this.nextIpOctet++;
return parts.join('.');
}
/**
* Generate a deterministic locally-administered MAC address.
*/
public generateMac(vmId: string, ifaceId: string): string {
// Create a simple hash from vmId + ifaceId for deterministic MAC generation
const input = `${vmId}:${ifaceId}`;
let hash = 0;
for (let i = 0; i < input.length; i++) {
const char = input.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32-bit integer
}
// Ensure hash is positive
const h = Math.abs(hash);
// Generate MAC octets from hash, using locally-administered prefix (02:xx:xx:xx:xx:xx)
const mac = [
0x02,
(h >> 0) & 0xff,
(h >> 8) & 0xff,
(h >> 16) & 0xff,
(h >> 24) & 0xff,
((h >> 4) ^ (h >> 12)) & 0xff,
];
return mac.map((b) => b.toString(16).padStart(2, '0')).join(':');
}
/**
* Generate a TAP device name that fits within IFNAMSIZ (15 chars).
* Format: sv<4charVmId><ifaceId truncated>
*/
public generateTapName(vmId: string, ifaceId: string): string {
const vmPart = vmId.replace(/-/g, '').substring(0, 4);
const ifacePart = ifaceId.substring(0, 6);
const tapName = `sv${vmPart}${ifacePart}`;
// Ensure max 15 chars (Linux IFNAMSIZ)
return tapName.substring(0, 15);
}
/**
* Ensure the Linux bridge is created and configured.
*/
public async ensureBridge(): Promise<void> {
if (this.bridgeCreated) return;
try {
// Check if bridge already exists
const result = await this.shell.exec(`ip link show ${this.bridgeName} 2>/dev/null`);
if (result.exitCode !== 0) {
// Create bridge
await this.shell.exec(`ip link add ${this.bridgeName} type bridge`);
await this.shell.exec(`ip addr add ${this.gatewayIp}/${this.subnetCidr} dev ${this.bridgeName}`);
await this.shell.exec(`ip link set ${this.bridgeName} up`);
}
// Enable IP forwarding
await this.shell.exec('sysctl -w net.ipv4.ip_forward=1');
// Set up NAT masquerade (idempotent with -C check)
const checkResult = await this.shell.exec(
`iptables -t nat -C POSTROUTING -s ${this.subnetBase}/${this.subnetCidr} -o $(ip route | grep default | awk '{print $5}' | head -1) -j MASQUERADE 2>/dev/null`,
);
if (checkResult.exitCode !== 0) {
await this.shell.exec(
`iptables -t nat -A POSTROUTING -s ${this.subnetBase}/${this.subnetCidr} -o $(ip route | grep default | awk '{print $5}' | head -1) -j MASQUERADE`,
);
}
this.bridgeCreated = true;
} catch (err) {
throw new SmartVMError(
`Failed to set up network bridge: ${err.message}`,
'BRIDGE_SETUP_FAILED',
);
}
}
/**
* Create a TAP device for a VM and attach it to the bridge.
*/
public async createTapDevice(vmId: string, ifaceId: string): Promise<ITapDevice> {
await this.ensureBridge();
const tapName = this.generateTapName(vmId, ifaceId);
const guestIp = this.allocateIp();
const mac = this.generateMac(vmId, ifaceId);
try {
// Create TAP device
await this.shell.exec(`ip tuntap add dev ${tapName} mode tap`);
// Attach to bridge
await this.shell.exec(`ip link set ${tapName} master ${this.bridgeName}`);
// Bring TAP device up
await this.shell.exec(`ip link set ${tapName} up`);
const tap: ITapDevice = {
tapName,
guestIp,
gatewayIp: this.gatewayIp,
subnetMask: this.subnetMask,
mac,
};
this.activeTaps.set(tapName, tap);
return tap;
} catch (err) {
throw new SmartVMError(
`Failed to create TAP device ${tapName}: ${err.message}`,
'TAP_CREATE_FAILED',
);
}
}
/**
* Remove a TAP device and free its resources.
*/
public async removeTapDevice(tapName: string): Promise<void> {
try {
await this.shell.exec(`ip link del ${tapName} 2>/dev/null`);
this.activeTaps.delete(tapName);
} catch {
// Device may already be gone
}
}
/**
* Generate kernel boot args for guest networking.
* Returns the `ip=` parameter for the kernel command line.
*/
public getGuestNetworkBootArgs(tap: ITapDevice): string {
// Format: ip=<client-ip>:<server-ip>:<gw-ip>:<netmask>::<device>:off
return `ip=${tap.guestIp}::${tap.gatewayIp}:${tap.subnetMask}::eth0:off`;
}
/**
* Get all active TAP devices.
*/
public getActiveTaps(): ITapDevice[] {
return Array.from(this.activeTaps.values());
}
/**
* Clean up all TAP devices and the bridge.
*/
public async cleanup(): Promise<void> {
// Remove all TAP devices
for (const tapName of this.activeTaps.keys()) {
await this.removeTapDevice(tapName);
}
// Remove bridge if we created it
if (this.bridgeCreated) {
try {
await this.shell.exec(`ip link set ${this.bridgeName} down 2>/dev/null`);
await this.shell.exec(`ip link del ${this.bridgeName} 2>/dev/null`);
} catch {
// Bridge may already be gone
}
// Remove NAT rule
try {
await this.shell.exec(
`iptables -t nat -D POSTROUTING -s ${this.subnetBase}/${this.subnetCidr} -o $(ip route | grep default | awk '{print $5}' | head -1) -j MASQUERADE 2>/dev/null`,
);
} catch {
// Rule may not exist
}
this.bridgeCreated = false;
}
}
}

182
ts/classes.smartvm.ts Normal file
View File

@@ -0,0 +1,182 @@
import * as plugins from './plugins.js';
import type { ISmartVMOptions, IMicroVMConfig } from './interfaces/index.js';
import { SmartVMError } from './interfaces/index.js';
import { ImageManager } from './classes.imagemanager.js';
import { NetworkManager } from './classes.networkmanager.js';
import { MicroVM } from './classes.microvm.js';
/**
* Top-level orchestrator for creating and managing Firecracker MicroVMs.
*/
export class SmartVM {
private options: ISmartVMOptions;
public imageManager: ImageManager;
public networkManager: NetworkManager;
private activeVMs: Map<string, MicroVM> = new Map();
private smartExitInstance: InstanceType<typeof plugins.smartexit.SmartExit>;
private firecrackerVersion: string | null = null;
private firecrackerBinaryPath: string | null = null;
constructor(options: ISmartVMOptions = {}) {
this.options = {
dataDir: options.dataDir || '/tmp/.smartvm',
arch: options.arch || 'x86_64',
bridgeName: options.bridgeName || 'svbr0',
subnet: options.subnet || '172.30.0.0/24',
...options,
};
this.imageManager = new ImageManager(this.options.dataDir!, this.options.arch);
this.networkManager = new NetworkManager({
bridgeName: this.options.bridgeName,
subnet: this.options.subnet,
});
// If a custom binary path is provided, use it directly
if (this.options.firecrackerBinaryPath) {
this.firecrackerBinaryPath = this.options.firecrackerBinaryPath;
}
// Register global cleanup
this.smartExitInstance = new plugins.smartexit.SmartExit({ silent: true });
this.smartExitInstance.addCleanupFunction(async () => {
await this.cleanup();
});
}
/**
* Ensure the Firecracker binary is available.
* Downloads it if not present.
*/
public async ensureBinary(): Promise<string> {
// If custom binary path is set, just verify it exists
if (this.firecrackerBinaryPath) {
try {
await plugins.fs.promises.access(this.firecrackerBinaryPath);
return this.firecrackerBinaryPath;
} catch {
// File doesn't exist, fall through to error
}
throw new SmartVMError(
`Firecracker binary not found at ${this.firecrackerBinaryPath}`,
'BINARY_NOT_FOUND',
);
}
// Ensure data directories exist
await this.imageManager.ensureDirectories();
// Determine version
let version = this.options.firecrackerVersion;
if (!version) {
version = await this.imageManager.getLatestVersion();
}
this.firecrackerVersion = version;
// Check if binary exists
if (await this.imageManager.hasBinary(version)) {
this.firecrackerBinaryPath = this.imageManager.getFirecrackerPath(version);
return this.firecrackerBinaryPath;
}
// Download the binary
this.firecrackerBinaryPath = await this.imageManager.downloadFirecracker(version);
return this.firecrackerBinaryPath;
}
/**
* Create a new MicroVM with the given configuration.
* Returns the MicroVM instance (not yet started).
*/
public async createVM(config: IMicroVMConfig): Promise<MicroVM> {
// Ensure binary is available
if (!this.firecrackerBinaryPath) {
await this.ensureBinary();
}
// Generate VM ID if not provided
const vmId = config.id || plugins.smartunique.uuid4();
// Generate socket path
const socketPath = this.imageManager.getSocketPath(vmId);
// Create MicroVM instance
const vm = new MicroVM(
vmId,
config,
this.firecrackerBinaryPath!,
socketPath,
this.networkManager,
);
// Register in active VMs
this.activeVMs.set(vmId, vm);
return vm;
}
/**
* Get an active VM by ID.
*/
public getVM(id: string): MicroVM | undefined {
return this.activeVMs.get(id);
}
/**
* List all active VM IDs.
*/
public listVMs(): string[] {
return Array.from(this.activeVMs.keys());
}
/**
* Get the number of active VMs.
*/
public get vmCount(): number {
return this.activeVMs.size;
}
/**
* Stop all running VMs.
*/
public async stopAll(): Promise<void> {
const stopPromises: Promise<void>[] = [];
for (const vm of this.activeVMs.values()) {
if (vm.state === 'running' || vm.state === 'paused') {
stopPromises.push(
vm.stop().catch((err) => {
console.error(`Failed to stop VM ${vm.id}: ${err.message}`);
}),
);
}
}
await Promise.all(stopPromises);
}
/**
* Clean up all VMs and networking resources.
*/
public async cleanup(): Promise<void> {
// Clean up all VMs
const cleanupPromises: Promise<void>[] = [];
for (const vm of this.activeVMs.values()) {
cleanupPromises.push(
vm.cleanup().catch((err) => {
console.error(`Failed to clean up VM ${vm.id}: ${err.message}`);
}),
);
}
await Promise.all(cleanupPromises);
this.activeVMs.clear();
// Clean up networking
await this.networkManager.cleanup();
}
/**
* Remove a VM from the active list (after it's been cleaned up).
*/
public removeVM(id: string): boolean {
return this.activeVMs.delete(id);
}
}

135
ts/classes.socketclient.ts Normal file
View File

@@ -0,0 +1,135 @@
import * as plugins from './plugins.js';
import type { ISocketClientOptions, IApiResponse } from './interfaces/index.js';
import { SmartVMError } from './interfaces/index.js';
/**
* HTTP client that communicates with Firecracker over a Unix domain socket.
* Uses @push.rocks/smartrequest with the `http://unix:<socket>:<path>` URL format.
*/
export class SocketClient {
private socketPath: string;
constructor(options: ISocketClientOptions) {
this.socketPath = options.socketPath;
}
/**
* Build the Unix socket URL for a given API path.
*/
private buildUrl(apiPath: string): string {
return `http://unix:${this.socketPath}:${apiPath}`;
}
/**
* Perform a GET request.
*/
public async get<T = any>(apiPath: string): Promise<IApiResponse<T>> {
const url = this.buildUrl(apiPath);
try {
const response = await plugins.SmartRequest.create()
.url(url)
.get();
const statusCode = response.status;
let body: T;
try {
body = await response.json() as T;
} catch {
body = undefined as any;
}
return {
statusCode,
body,
ok: response.ok,
};
} catch (err) {
throw new SmartVMError(
`GET ${apiPath} failed: ${(err as Error).message}`,
'SOCKET_REQUEST_FAILED',
);
}
}
/**
* Perform a PUT request with a JSON body.
*/
public async put<T = any>(apiPath: string, body?: Record<string, any>): Promise<IApiResponse<T>> {
const url = this.buildUrl(apiPath);
try {
let request = plugins.SmartRequest.create().url(url);
if (body !== undefined) {
request = request.json(body);
}
const response = await request.put();
const statusCode = response.status;
let responseBody: T;
try {
responseBody = await response.json() as T;
} catch {
responseBody = undefined as any;
}
return {
statusCode,
body: responseBody,
ok: response.ok,
};
} catch (err) {
throw new SmartVMError(
`PUT ${apiPath} failed: ${(err as Error).message}`,
'SOCKET_REQUEST_FAILED',
);
}
}
/**
* Perform a PATCH request with a JSON body.
*/
public async patch<T = any>(apiPath: string, body?: Record<string, any>): Promise<IApiResponse<T>> {
const url = this.buildUrl(apiPath);
try {
let request = plugins.SmartRequest.create().url(url);
if (body !== undefined) {
request = request.json(body);
}
const response = await request.patch();
const statusCode = response.status;
let responseBody: T;
try {
responseBody = await response.json() as T;
} catch {
responseBody = undefined as any;
}
return {
statusCode,
body: responseBody,
ok: response.ok,
};
} catch (err) {
throw new SmartVMError(
`PATCH ${apiPath} failed: ${(err as Error).message}`,
'SOCKET_REQUEST_FAILED',
);
}
}
/**
* Check if the Firecracker API socket is ready by polling GET /.
*/
public async isReady(timeoutMs: number = 5000): Promise<boolean> {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
try {
const response = await this.get('/');
if (response.ok || response.statusCode === 200 || response.statusCode === 400) {
return true;
}
} catch {
// Socket not ready yet
}
await plugins.smartdelay.delayFor(100);
}
return false;
}
}

251
ts/classes.vmconfig.ts Normal file
View File

@@ -0,0 +1,251 @@
import type {
IMicroVMConfig,
IDriveConfig,
INetworkInterfaceConfig,
IRateLimiter,
} from './interfaces/index.js';
/**
* Transforms a camelCase IMicroVMConfig into snake_case Firecracker API payloads.
*/
export class VMConfig {
public config: IMicroVMConfig;
constructor(config: IMicroVMConfig) {
this.config = config;
}
/**
* Validate the configuration for required fields and constraints.
*/
public validate(): { valid: boolean; errors: string[] } {
const errors: string[] = [];
if (!this.config.bootSource) {
errors.push('bootSource is required');
} else if (!this.config.bootSource.kernelImagePath) {
errors.push('bootSource.kernelImagePath is required');
}
if (!this.config.machineConfig) {
errors.push('machineConfig is required');
} else {
if (!this.config.machineConfig.vcpuCount || this.config.machineConfig.vcpuCount < 1) {
errors.push('machineConfig.vcpuCount must be at least 1');
}
if (this.config.machineConfig.vcpuCount > 32) {
errors.push('machineConfig.vcpuCount must be at most 32');
}
if (!this.config.machineConfig.memSizeMib || this.config.machineConfig.memSizeMib < 1) {
errors.push('machineConfig.memSizeMib must be at least 1');
}
}
if (this.config.drives) {
const rootDrives = this.config.drives.filter((d) => d.isRootDevice);
if (rootDrives.length > 1) {
errors.push('Only one root drive is allowed');
}
for (const drive of this.config.drives) {
if (!drive.driveId) {
errors.push('Each drive must have a driveId');
}
if (!drive.pathOnHost) {
errors.push(`Drive ${drive.driveId}: pathOnHost is required`);
}
}
}
if (this.config.vsock) {
if (this.config.vsock.guestCid < 3) {
errors.push('vsock.guestCid must be >= 3');
}
if (!this.config.vsock.udsPath) {
errors.push('vsock.udsPath is required');
}
}
return { valid: errors.length === 0, errors };
}
/**
* Generate the boot source PUT payload.
*/
public toBootSourcePayload(): Record<string, any> {
const bs = this.config.bootSource;
const payload: Record<string, any> = {
kernel_image_path: bs.kernelImagePath,
};
if (bs.bootArgs !== undefined) {
payload.boot_args = bs.bootArgs;
}
if (bs.initrdPath !== undefined) {
payload.initrd_path = bs.initrdPath;
}
return payload;
}
/**
* Generate the machine config PUT payload.
*/
public toMachineConfigPayload(): Record<string, any> {
const mc = this.config.machineConfig;
const payload: Record<string, any> = {
vcpu_count: mc.vcpuCount,
mem_size_mib: mc.memSizeMib,
};
if (mc.smt !== undefined) {
payload.smt = mc.smt;
}
if (mc.cpuTemplate !== undefined) {
payload.cpu_template = mc.cpuTemplate;
}
if (mc.trackDirtyPages !== undefined) {
payload.track_dirty_pages = mc.trackDirtyPages;
}
return payload;
}
/**
* Generate a drive PUT payload.
*/
public toDrivePayload(drive: IDriveConfig): Record<string, any> {
const payload: Record<string, any> = {
drive_id: drive.driveId,
path_on_host: drive.pathOnHost,
is_root_device: drive.isRootDevice,
is_read_only: drive.isReadOnly ?? false,
};
if (drive.partUuid !== undefined) {
payload.partuuid = drive.partUuid;
}
if (drive.cacheType !== undefined) {
payload.cache_type = drive.cacheType;
}
if (drive.rateLimiter) {
payload.rate_limiter = this.toRateLimiterPayload(drive.rateLimiter);
}
if (drive.ioEngine !== undefined) {
payload.io_engine = drive.ioEngine;
}
return payload;
}
/**
* Generate a network interface PUT payload.
*/
public toNetworkInterfacePayload(iface: INetworkInterfaceConfig): Record<string, any> {
const payload: Record<string, any> = {
iface_id: iface.ifaceId,
};
if (iface.hostDevName !== undefined) {
payload.host_dev_name = iface.hostDevName;
}
if (iface.guestMac !== undefined) {
payload.guest_mac = iface.guestMac;
}
if (iface.rxRateLimiter) {
payload.rx_rate_limiter = this.toRateLimiterPayload(iface.rxRateLimiter);
}
if (iface.txRateLimiter) {
payload.tx_rate_limiter = this.toRateLimiterPayload(iface.txRateLimiter);
}
return payload;
}
/**
* Generate the vsock PUT payload.
*/
public toVsockPayload(): Record<string, any> | null {
if (!this.config.vsock) return null;
return {
guest_cid: this.config.vsock.guestCid,
uds_path: this.config.vsock.udsPath,
};
}
/**
* Generate the balloon PUT payload.
*/
public toBalloonPayload(): Record<string, any> | null {
if (!this.config.balloon) return null;
const payload: Record<string, any> = {
amount_mib: this.config.balloon.amountMib,
deflate_on_oom: this.config.balloon.deflateOnOom,
};
if (this.config.balloon.statsPollingIntervalS !== undefined) {
payload.stats_polling_interval_s = this.config.balloon.statsPollingIntervalS;
}
return payload;
}
/**
* Generate the MMDS config PUT payload.
*/
public toMmdsConfigPayload(): Record<string, any> | null {
if (!this.config.mmds) return null;
const payload: Record<string, any> = {
network_interfaces: this.config.mmds.networkInterfaces,
};
if (this.config.mmds.version !== undefined) {
payload.version = this.config.mmds.version;
}
return payload;
}
/**
* Generate the logger PUT payload.
*/
public toLoggerPayload(): Record<string, any> | null {
if (!this.config.logger) return null;
const payload: Record<string, any> = {
log_path: this.config.logger.logPath,
};
if (this.config.logger.level !== undefined) {
payload.level = this.config.logger.level;
}
if (this.config.logger.showLevel !== undefined) {
payload.show_level = this.config.logger.showLevel;
}
if (this.config.logger.showLogOrigin !== undefined) {
payload.show_log_origin = this.config.logger.showLogOrigin;
}
return payload;
}
/**
* Generate the metrics PUT payload.
*/
public toMetricsPayload(): Record<string, any> | null {
if (!this.config.metrics) return null;
return {
metrics_path: this.config.metrics.metricsPath,
};
}
/**
* Convert a rate limiter config to a Firecracker API payload.
*/
private toRateLimiterPayload(rl: IRateLimiter): Record<string, any> {
const payload: Record<string, any> = {};
if (rl.bandwidth) {
payload.bandwidth = {
size: rl.bandwidth.size,
refill_time: rl.bandwidth.refillTime,
};
if (rl.bandwidth.oneTimeBurst !== undefined) {
payload.bandwidth.one_time_burst = rl.bandwidth.oneTimeBurst;
}
}
if (rl.ops) {
payload.ops = {
size: rl.ops.size,
refill_time: rl.ops.refillTime,
};
if (rl.ops.oneTimeBurst !== undefined) {
payload.ops.one_time_burst = rl.ops.oneTimeBurst;
}
}
return payload;
}
}

8
ts/index.ts Normal file
View File

@@ -0,0 +1,8 @@
export * from './interfaces/index.js';
export { VMConfig } from './classes.vmconfig.js';
export { SocketClient } from './classes.socketclient.js';
export { ImageManager } from './classes.imagemanager.js';
export { FirecrackerProcess } from './classes.firecrackerprocess.js';
export { NetworkManager } from './classes.networkmanager.js';
export { MicroVM } from './classes.microvm.js';
export { SmartVM } from './classes.smartvm.js';

48
ts/interfaces/api.ts Normal file
View File

@@ -0,0 +1,48 @@
/**
* Options for the SocketClient.
*/
export interface ISocketClientOptions {
/** Path to the Firecracker Unix domain socket. */
socketPath: string;
}
/**
* Standardized API response from the socket client.
*/
export interface IApiResponse<T = any> {
/** HTTP status code. */
statusCode: number;
/** Parsed response body. */
body: T;
/** Whether the request was successful (2xx). */
ok: boolean;
}
/**
* Options for spawning a Firecracker process.
*/
export interface IFirecrackerProcessOptions {
/** Path to the firecracker binary. */
binaryPath: string;
/** Path for the API Unix domain socket. */
socketPath: string;
/** Log level for Firecracker. */
logLevel?: string;
}
/**
* Custom error class for SmartVM operations.
*/
export class SmartVMError extends Error {
public code: string;
public statusCode?: number;
public details?: any;
constructor(message: string, code: string, statusCode?: number, details?: any) {
super(message);
this.name = 'SmartVMError';
this.code = code;
this.statusCode = statusCode;
this.details = details;
}
}

24
ts/interfaces/common.ts Normal file
View File

@@ -0,0 +1,24 @@
/**
* State machine states for a MicroVM lifecycle.
*/
export type TVMState = 'created' | 'configuring' | 'running' | 'paused' | 'stopped' | 'error';
/**
* Supported Firecracker architectures.
*/
export type TFirecrackerArch = 'x86_64' | 'aarch64';
/**
* Disk cache types supported by Firecracker.
*/
export type TCacheType = 'Unsafe' | 'Writeback';
/**
* Snapshot types for creating snapshots.
*/
export type TSnapshotType = 'Full' | 'Diff';
/**
* Log levels for Firecracker logger.
*/
export type TLogLevel = 'Error' | 'Warning' | 'Info' | 'Debug';

235
ts/interfaces/config.ts Normal file
View File

@@ -0,0 +1,235 @@
import type { TFirecrackerArch, TCacheType, TSnapshotType, TLogLevel } from './common.js';
/**
* Top-level options for the SmartVM orchestrator.
*/
export interface ISmartVMOptions {
/** Directory for storing binaries, kernels, rootfs images, and sockets. Defaults to /tmp/.smartvm */
dataDir?: string;
/** Firecracker version to use. Defaults to latest. */
firecrackerVersion?: string;
/** Target architecture. Defaults to x86_64. */
arch?: TFirecrackerArch;
/** Custom path to firecracker binary (overrides version-based lookup). */
firecrackerBinaryPath?: string;
/** Network bridge name. Defaults to 'svbr0'. */
bridgeName?: string;
/** Network subnet in CIDR notation. Defaults to '172.30.0.0/24'. */
subnet?: string;
}
/**
* Firecracker boot source configuration.
*/
export interface IBootSource {
/** Path to the kernel image on the host. */
kernelImagePath: string;
/** Kernel boot arguments. */
bootArgs?: string;
/** Path to initrd image (optional). */
initrdPath?: string;
}
/**
* Machine hardware configuration.
*/
export interface IMachineConfig {
/** Number of vCPUs (1-32). */
vcpuCount: number;
/** Memory size in MiB. */
memSizeMib: number;
/** Enable SMT (simultaneous multi-threading). Defaults to false. */
smt?: boolean;
/** Enable CPU template for security (C3, T2, T2S, T2CL, T2A, V1N1, None). */
cpuTemplate?: string;
/** Whether to track dirty pages for incremental snapshots. */
trackDirtyPages?: boolean;
}
/**
* Rate limiter configuration for drives and network interfaces.
*/
export interface IRateLimiter {
/** Bandwidth limit. */
bandwidth?: {
size: number;
oneTimeBurst?: number;
refillTime: number;
};
/** Operations per second limit. */
ops?: {
size: number;
oneTimeBurst?: number;
refillTime: number;
};
}
/**
* Block device (drive) configuration.
*/
export interface IDriveConfig {
/** Unique drive identifier. */
driveId: string;
/** Path to the disk image on the host. */
pathOnHost: string;
/** Whether this is the root device. */
isRootDevice: boolean;
/** Whether the drive is read-only. */
isReadOnly?: boolean;
/** Partition UUID (optional). */
partUuid?: string;
/** Cache type (Unsafe or Writeback). */
cacheType?: TCacheType;
/** Rate limiter for the drive. */
rateLimiter?: IRateLimiter;
/** Path to a file that backs the device for I/O. */
ioEngine?: string;
}
/**
* Network interface configuration.
*/
export interface INetworkInterfaceConfig {
/** Unique interface identifier (e.g., 'eth0'). */
ifaceId: string;
/** TAP device name on the host. Automatically set by NetworkManager if not provided. */
hostDevName?: string;
/** Guest MAC address. Automatically generated if not provided. */
guestMac?: string;
/** Rate limiter for RX traffic. */
rxRateLimiter?: IRateLimiter;
/** Rate limiter for TX traffic. */
txRateLimiter?: IRateLimiter;
}
/**
* Vsock device configuration.
*/
export interface IVsockConfig {
/** Guest CID (Context Identifier). Must be >= 3. */
guestCid: number;
/** Path to the Unix domain socket on the host. */
udsPath: string;
}
/**
* Balloon device configuration for dynamic memory management.
*/
export interface IBalloonConfig {
/** Target balloon size in MiB. */
amountMib: number;
/** Whether to deflate on OOM. */
deflateOnOom: boolean;
/** Polling interval for balloon stats in seconds. */
statsPollingIntervalS?: number;
}
/**
* MMDS (Microvm Metadata Service) configuration.
*/
export interface IMmdsConfig {
/** MMDS version (V1 or V2). */
version?: 'V1' | 'V2';
/** Network interfaces that MMDS traffic is allowed on. */
networkInterfaces: string[];
}
/**
* Logger configuration for Firecracker.
*/
export interface ILoggerConfig {
/** Path to the log file. */
logPath: string;
/** Log level. */
level?: TLogLevel;
/** Whether to show log origin (file, line). */
showLevel?: boolean;
/** Whether to show log level. */
showLogOrigin?: boolean;
}
/**
* Metrics configuration for Firecracker.
*/
export interface IMetricsConfig {
/** Path to the metrics file (FIFO). */
metricsPath: string;
}
/**
* Snapshot creation parameters.
*/
export interface ISnapshotCreateParams {
/** Path to save the snapshot file. */
snapshotPath: string;
/** Path to save the memory file. */
memFilePath: string;
/** Snapshot type (Full or Diff). */
snapshotType?: TSnapshotType;
}
/**
* Snapshot loading parameters.
*/
export interface ISnapshotLoadParams {
/** Path to the snapshot file. */
snapshotPath: string;
/** Path to the memory file. */
memFilePath: string;
/** Whether to enable diff snapshots after loading. */
enableDiffSnapshots?: boolean;
/** Whether to resume the VM after loading. */
resumeVm?: boolean;
}
/**
* Complete MicroVM configuration combining all sub-configs.
*/
export interface IMicroVMConfig {
/** Unique VM identifier. Auto-generated if not provided. */
id?: string;
/** Boot source configuration (required). */
bootSource: IBootSource;
/** Machine hardware configuration (required). */
machineConfig: IMachineConfig;
/** Block devices. */
drives?: IDriveConfig[];
/** Network interfaces. */
networkInterfaces?: INetworkInterfaceConfig[];
/** Vsock device. */
vsock?: IVsockConfig;
/** Balloon device. */
balloon?: IBalloonConfig;
/** MMDS configuration. */
mmds?: IMmdsConfig;
/** Logger configuration. */
logger?: ILoggerConfig;
/** Metrics configuration. */
metrics?: IMetricsConfig;
}
/**
* Options for the NetworkManager.
*/
export interface INetworkManagerOptions {
/** Bridge device name. Defaults to 'svbr0'. */
bridgeName?: string;
/** Subnet in CIDR notation. Defaults to '172.30.0.0/24'. */
subnet?: string;
}
/**
* Represents a TAP device created by the NetworkManager.
*/
export interface ITapDevice {
/** TAP device name on the host. */
tapName: string;
/** IP address assigned to the guest. */
guestIp: string;
/** Gateway IP (bridge IP). */
gatewayIp: string;
/** Subnet mask. */
subnetMask: string;
/** MAC address for the guest. */
mac: string;
}

3
ts/interfaces/index.ts Normal file
View File

@@ -0,0 +1,3 @@
export * from './common.js';
export * from './config.js';
export * from './api.js';

21
ts/plugins.ts Normal file
View File

@@ -0,0 +1,21 @@
// node native
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
export { fs, path, os };
// @push.rocks scope
import * as smartdelay from '@push.rocks/smartdelay';
import * as smartexit from '@push.rocks/smartexit';
import { SmartRequest } from '@push.rocks/smartrequest';
import * as smartshell from '@push.rocks/smartshell';
import * as smartunique from '@push.rocks/smartunique';
export {
smartdelay,
smartexit,
SmartRequest,
smartshell,
smartunique,
};