feat(runtime): stage VM runtime artifacts and writable drives in per-VM ephemeral storage by default

This commit is contained in:
2026-05-01 15:28:06 +00:00
parent 9cdb8571a4
commit c868d07d29
10 changed files with 237 additions and 8 deletions
+75
View File
@@ -2,8 +2,10 @@ import * as plugins from './plugins.js';
import type {
TVMState,
IMicroVMConfig,
IMicroVMRuntimeOptions,
ISnapshotCreateParams,
ISnapshotLoadParams,
IDriveConfig,
ITapDevice,
} from './interfaces/index.js';
import { SmartVMError } from './interfaces/index.js';
@@ -26,6 +28,9 @@ export class MicroVM {
private networkManager: NetworkManager;
private binaryPath: string;
private socketPath: string;
private runtimeDir: string;
private ephemeralWritableDrives: boolean;
private vmRuntimeDir: string | null = null;
private tapDevices: ITapDevice[] = [];
constructor(
@@ -34,12 +39,15 @@ export class MicroVM {
binaryPath: string,
socketPath: string,
networkManager: NetworkManager,
runtimeOptions: IMicroVMRuntimeOptions = {},
) {
this.id = id;
this.vmConfig = new VMConfig(config);
this.binaryPath = binaryPath;
this.socketPath = socketPath;
this.networkManager = networkManager;
this.runtimeDir = runtimeOptions.runtimeDir || plugins.path.join(plugins.os.tmpdir(), '.smartvm', 'runtime');
this.ephemeralWritableDrives = runtimeOptions.ephemeralWritableDrives ?? true;
}
/**
@@ -83,6 +91,9 @@ export class MicroVM {
this.state = 'configuring';
try {
await this.ensureVMRuntimeDir();
await this.prepareEphemeralDrives();
// Start the Firecracker process
this.process = new FirecrackerProcess({
binaryPath: this.binaryPath,
@@ -318,6 +329,13 @@ export class MicroVM {
return this.vmConfig;
}
/**
* Get the per-VM runtime directory if it has been created.
*/
public getRuntimeDir(): string | null {
return this.vmRuntimeDir;
}
/**
* Full cleanup: stop process, remove socket, remove TAP devices.
*/
@@ -334,12 +352,69 @@ export class MicroVM {
}
this.tapDevices = [];
if (this.vmRuntimeDir) {
await plugins.fs.promises.rm(this.vmRuntimeDir, { recursive: true, force: true });
this.vmRuntimeDir = null;
}
this.socketClient = null;
if (this.state !== 'error') {
this.state = 'stopped';
}
}
private shouldStageDrive(drive: IDriveConfig): boolean {
if (!this.ephemeralWritableDrives) {
return false;
}
if (drive.ephemeral === false) {
return false;
}
if (drive.isReadOnly === true && drive.ephemeral !== true) {
return false;
}
return true;
}
private async ensureVMRuntimeDir(): Promise<string> {
if (!this.vmRuntimeDir) {
this.vmRuntimeDir = plugins.path.join(this.runtimeDir, this.sanitizePathPart(this.id));
}
await plugins.fs.promises.mkdir(this.vmRuntimeDir, { recursive: true });
return this.vmRuntimeDir;
}
private async prepareEphemeralDrives(): Promise<void> {
const drives = this.vmConfig.config.drives || [];
for (const drive of drives) {
if (!this.shouldStageDrive(drive)) {
continue;
}
const vmRuntimeDir = await this.ensureVMRuntimeDir();
const drivesDir = plugins.path.join(vmRuntimeDir, 'drives');
await plugins.fs.promises.mkdir(drivesDir, { recursive: true });
const sourcePath = drive.pathOnHost;
const sourceFileName = plugins.path.basename(sourcePath) || 'drive.img';
const stagedPath = plugins.path.join(
drivesDir,
`${this.sanitizePathPart(drive.driveId)}-${this.sanitizePathPart(sourceFileName)}`,
);
await plugins.fs.promises.copyFile(sourcePath, stagedPath);
drive.pathOnHost = stagedPath;
}
}
private sanitizePathPart(value: string): string {
const sanitized = value.replace(/[^a-zA-Z0-9._-]/g, '_');
if (!sanitized || sanitized === '.' || sanitized === '..') {
return 'item';
}
return sanitized;
}
/**
* Helper: PUT request with error handling.
*/