initial
This commit is contained in:
8
ts/00_commitinfo_data.ts
Normal file
8
ts/00_commitinfo_data.ts
Normal 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',
|
||||
};
|
||||
141
ts/classes.firecrackerprocess.ts
Normal file
141
ts/classes.firecrackerprocess.ts
Normal 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
244
ts/classes.imagemanager.ts
Normal 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
367
ts/classes.microvm.ts
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
237
ts/classes.networkmanager.ts
Normal file
237
ts/classes.networkmanager.ts
Normal 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
182
ts/classes.smartvm.ts
Normal 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
135
ts/classes.socketclient.ts
Normal 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
251
ts/classes.vmconfig.ts
Normal 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
8
ts/index.ts
Normal 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
48
ts/interfaces/api.ts
Normal 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
24
ts/interfaces/common.ts
Normal 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
235
ts/interfaces/config.ts
Normal 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
3
ts/interfaces/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './common.js';
|
||||
export * from './config.js';
|
||||
export * from './api.js';
|
||||
21
ts/plugins.ts
Normal file
21
ts/plugins.ts
Normal 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,
|
||||
};
|
||||
Reference in New Issue
Block a user