Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a61694bd01 | |||
| c868d07d29 |
@@ -1,5 +1,13 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-05-01 - 1.3.0 - feat(runtime)
|
||||||
|
stage VM runtime artifacts and writable drives in per-VM ephemeral storage by default
|
||||||
|
|
||||||
|
- default runtime files to /dev/shm/.smartvm/runtime when available, with per-VM socket and drive staging paths
|
||||||
|
- copy writable drives into per-VM runtime storage before boot and remove them during cleanup, with per-drive and global opt-out controls
|
||||||
|
- prefer squashfs rootfs images over ext4 when resolving Firecracker CI base images
|
||||||
|
- add tests and documentation for ephemeral drive staging and runtime directory defaults
|
||||||
|
|
||||||
## 2026-05-01 - 1.2.0 - feat(base-images)
|
## 2026-05-01 - 1.2.0 - feat(base-images)
|
||||||
add managed base image bundles with cache retention, hosted manifests, and opt-in integration boot testing
|
add managed base image bundles with cache retention, hosted manifests, and opt-in integration boot testing
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@push.rocks/smartvm",
|
"name": "@push.rocks/smartvm",
|
||||||
"version": "1.2.0",
|
"version": "1.3.0",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "A TypeScript module wrapping Amazon Firecracker VMM for managing lightweight microVMs",
|
"description": "A TypeScript module wrapping Amazon Firecracker VMM for managing lightweight microVMs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
- `BaseImageManager` downloads Firecracker CI demo artifacts or hosted project manifests into `/tmp/.smartvm/base-images` by default
|
- `BaseImageManager` downloads Firecracker CI demo artifacts or hosted project manifests into `/tmp/.smartvm/base-images` by default
|
||||||
- Base image cache keeps 2 bundles by default and warns before evicting older bundles
|
- Base image cache keeps 2 bundles by default and warns before evicting older bundles
|
||||||
- Hosted manifest examples live in `assets/base-images/`
|
- Hosted manifest examples live in `assets/base-images/`
|
||||||
|
- VM runtime files default to `/dev/shm/.smartvm/runtime` when available
|
||||||
|
- Writable drives are staged into per-VM runtime storage by default and removed during cleanup; use `ephemeral: false` only for explicit persistence
|
||||||
|
|
||||||
## Key API Patterns
|
## Key API Patterns
|
||||||
- SmartRequest: `SmartRequest.create().url(...).json(body).put()` — response has `.status`, `.ok`, `.json()`
|
- SmartRequest: `SmartRequest.create().url(...).json(body).put()` — response has `.status`, `.ok`, `.json()`
|
||||||
|
|||||||
@@ -257,9 +257,22 @@ const clonePath = await imageManager.cloneRootfs(rootfsPath, 'ubuntu-clone.ext4'
|
|||||||
bin/<version>/jailer
|
bin/<version>/jailer
|
||||||
kernels/<name>
|
kernels/<name>
|
||||||
rootfs/<name>
|
rootfs/<name>
|
||||||
sockets/<vmId>.sock
|
|
||||||
|
/dev/shm/.smartvm/runtime/<vmId>/
|
||||||
|
firecracker.sock
|
||||||
|
drives/<driveId>-<sourceName>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Runtime files default to `/dev/shm/.smartvm/runtime` on Linux when available, so sockets and per-VM writable drive copies live on tmpfs and are removed during cleanup. Set `runtimeDir` to override this.
|
||||||
|
|
||||||
|
**Ephemeral drives:**
|
||||||
|
|
||||||
|
- Writable drives are copied into the per-VM runtime directory before boot by default
|
||||||
|
- Firecracker receives the staged tmpfs path, so guest writes do not modify cached/base rootfs files
|
||||||
|
- Per-VM staged drive files are deleted by `vm.cleanup()` / `smartvm.cleanup()`
|
||||||
|
- Read-only drives are not copied unless `ephemeral: true` is set on the drive
|
||||||
|
- Set `ephemeral: false` on a drive, or `ephemeralWritableDrives: false` on `SmartVM`, only when persistence is explicitly required
|
||||||
|
|
||||||
### `BaseImageManager` — Base Images
|
### `BaseImageManager` — Base Images
|
||||||
|
|
||||||
Downloads known base image bundles into a `/tmp` cache for integration tests and quick local smoke tests. The default preset is `latest`; `lts` maps to a pinned Firecracker CI train (`v1.7`) for a stable fallback. Hosted project-owned manifests are also supported for pinned Alpine/BusyBox-style bundles.
|
Downloads known base image bundles into a `/tmp` cache for integration tests and quick local smoke tests. The default preset is `latest`; `lts` maps to a pinned Firecracker CI train (`v1.7`) for a stable fallback. Hosted project-owned manifests are also supported for pinned Alpine/BusyBox-style bundles.
|
||||||
@@ -304,6 +317,7 @@ Example configuration:
|
|||||||
```typescript
|
```typescript
|
||||||
const smartvm = new SmartVM({
|
const smartvm = new SmartVM({
|
||||||
baseImageCacheDir: '/tmp/.smartvm/base-images',
|
baseImageCacheDir: '/tmp/.smartvm/base-images',
|
||||||
|
runtimeDir: '/dev/shm/.smartvm/runtime',
|
||||||
maxStoredBaseImages: 4,
|
maxStoredBaseImages: 4,
|
||||||
baseImageManifestUrl: 'https://assets.example.com/push.rocks/smartvm/base-images/smartvm-minimal-v1/x86_64/manifest.json',
|
baseImageManifestUrl: 'https://assets.example.com/push.rocks/smartvm/base-images/smartvm-minimal-v1/x86_64/manifest.json',
|
||||||
});
|
});
|
||||||
@@ -542,6 +556,7 @@ All configuration interfaces are fully exported for type-safe usage:
|
|||||||
```typescript
|
```typescript
|
||||||
import type {
|
import type {
|
||||||
ISmartVMOptions,
|
ISmartVMOptions,
|
||||||
|
IMicroVMRuntimeOptions,
|
||||||
IMicroVMConfig,
|
IMicroVMConfig,
|
||||||
IBootSource,
|
IBootSource,
|
||||||
IMachineConfig,
|
IMachineConfig,
|
||||||
|
|||||||
@@ -678,6 +678,84 @@ tap.test('MicroVM - invalid lifecycle calls should throw SmartVMError', async ()
|
|||||||
expect((infoError as SmartVMError).code).toEqual('NO_CLIENT');
|
expect((infoError as SmartVMError).code).toEqual('NO_CLIENT');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
tap.test('MicroVM - start() should stage writable drives ephemerally and clean them up on failure', async () => {
|
||||||
|
const workDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'smartvm-ephemeral-drive-test-'));
|
||||||
|
const runtimeDir = path.join(workDir, 'runtime');
|
||||||
|
const sourceRootfs = path.join(workDir, 'rootfs.ext4');
|
||||||
|
await fs.promises.writeFile(sourceRootfs, 'persistent-rootfs');
|
||||||
|
|
||||||
|
const config: IMicroVMConfig = {
|
||||||
|
...sampleConfig,
|
||||||
|
id: 'ephemeral-vm',
|
||||||
|
drives: [
|
||||||
|
{
|
||||||
|
driveId: 'rootfs',
|
||||||
|
pathOnHost: sourceRootfs,
|
||||||
|
isRootDevice: true,
|
||||||
|
isReadOnly: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
networkInterfaces: [],
|
||||||
|
};
|
||||||
|
const vm = new MicroVM(
|
||||||
|
'ephemeral-vm',
|
||||||
|
config,
|
||||||
|
'/bin/false',
|
||||||
|
path.join(runtimeDir, 'ephemeral-vm', 'firecracker.sock'),
|
||||||
|
new NetworkManager(),
|
||||||
|
{ runtimeDir },
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const error = await getRejectedError(vm.start());
|
||||||
|
expect(error).toBeInstanceOf(SmartVMError);
|
||||||
|
expect(fs.existsSync(path.join(runtimeDir, 'ephemeral-vm'))).toBeFalse();
|
||||||
|
expect(await fs.promises.readFile(sourceRootfs, 'utf8')).toEqual('persistent-rootfs');
|
||||||
|
expect(vm.getVMConfig().config.drives![0].pathOnHost).not.toEqual(sourceRootfs);
|
||||||
|
} finally {
|
||||||
|
await fs.promises.rm(workDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('MicroVM - start() should honor per-drive ephemeral opt-out', async () => {
|
||||||
|
const workDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'smartvm-persistent-drive-test-'));
|
||||||
|
const runtimeDir = path.join(workDir, 'runtime');
|
||||||
|
const sourceRootfs = path.join(workDir, 'rootfs.ext4');
|
||||||
|
await fs.promises.writeFile(sourceRootfs, 'persistent-rootfs');
|
||||||
|
|
||||||
|
const config: IMicroVMConfig = {
|
||||||
|
...sampleConfig,
|
||||||
|
id: 'persistent-vm',
|
||||||
|
drives: [
|
||||||
|
{
|
||||||
|
driveId: 'rootfs',
|
||||||
|
pathOnHost: sourceRootfs,
|
||||||
|
isRootDevice: true,
|
||||||
|
isReadOnly: false,
|
||||||
|
ephemeral: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
networkInterfaces: [],
|
||||||
|
};
|
||||||
|
const vm = new MicroVM(
|
||||||
|
'persistent-vm',
|
||||||
|
config,
|
||||||
|
'/bin/false',
|
||||||
|
path.join(runtimeDir, 'persistent-vm', 'firecracker.sock'),
|
||||||
|
new NetworkManager(),
|
||||||
|
{ runtimeDir },
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const error = await getRejectedError(vm.start());
|
||||||
|
expect(error).toBeInstanceOf(SmartVMError);
|
||||||
|
expect(vm.getVMConfig().config.drives![0].pathOnHost).toEqual(sourceRootfs);
|
||||||
|
expect(fs.existsSync(path.join(runtimeDir, 'persistent-vm'))).toBeFalse();
|
||||||
|
} finally {
|
||||||
|
await fs.promises.rm(workDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// SmartVM Tests
|
// SmartVM Tests
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -688,6 +766,9 @@ tap.test('SmartVM - instantiation with defaults', async () => {
|
|||||||
expect(smartvm.imageManager).toBeTruthy();
|
expect(smartvm.imageManager).toBeTruthy();
|
||||||
expect(smartvm.baseImageManager).toBeTruthy();
|
expect(smartvm.baseImageManager).toBeTruthy();
|
||||||
expect(smartvm.networkManager).toBeTruthy();
|
expect(smartvm.networkManager).toBeTruthy();
|
||||||
|
if (fs.existsSync('/dev/shm')) {
|
||||||
|
expect(smartvm.getRuntimeDir()).toEqual('/dev/shm/.smartvm/runtime');
|
||||||
|
}
|
||||||
expect(smartvm.vmCount).toEqual(0);
|
expect(smartvm.vmCount).toEqual(0);
|
||||||
expect(smartvm.listVMs()).toHaveLength(0);
|
expect(smartvm.listVMs()).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartvm',
|
name: '@push.rocks/smartvm',
|
||||||
version: '1.2.0',
|
version: '1.3.0',
|
||||||
description: 'A TypeScript module wrapping Amazon Firecracker VMM for managing lightweight microVMs'
|
description: 'A TypeScript module wrapping Amazon Firecracker VMM for managing lightweight microVMs'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -485,14 +485,14 @@ export class BaseImageManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private selectRootfsKey(keys: string[]): string {
|
private selectRootfsKey(keys: string[]): string {
|
||||||
const ext4Keys = keys.filter((key) => /\/ubuntu-[^/]+\.ext4$/.test(key));
|
|
||||||
if (ext4Keys.length > 0) {
|
|
||||||
return ext4Keys.sort((a, b) => a.localeCompare(b, undefined, { numeric: true })).at(-1)!;
|
|
||||||
}
|
|
||||||
const squashfsKeys = keys.filter((key) => /\/ubuntu-[^/]+\.squashfs$/.test(key));
|
const squashfsKeys = keys.filter((key) => /\/ubuntu-[^/]+\.squashfs$/.test(key));
|
||||||
if (squashfsKeys.length > 0) {
|
if (squashfsKeys.length > 0) {
|
||||||
return squashfsKeys.sort((a, b) => a.localeCompare(b, undefined, { numeric: true })).at(-1)!;
|
return squashfsKeys.sort((a, b) => a.localeCompare(b, undefined, { numeric: true })).at(-1)!;
|
||||||
}
|
}
|
||||||
|
const ext4Keys = keys.filter((key) => /\/ubuntu-[^/]+\.ext4$/.test(key));
|
||||||
|
if (ext4Keys.length > 0) {
|
||||||
|
return ext4Keys.sort((a, b) => a.localeCompare(b, undefined, { numeric: true })).at(-1)!;
|
||||||
|
}
|
||||||
throw new SmartVMError('No suitable Firecracker CI rootfs image found', 'BASE_IMAGE_RESOLVE_FAILED');
|
throw new SmartVMError('No suitable Firecracker CI rootfs image found', 'BASE_IMAGE_RESOLVE_FAILED');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ import * as plugins from './plugins.js';
|
|||||||
import type {
|
import type {
|
||||||
TVMState,
|
TVMState,
|
||||||
IMicroVMConfig,
|
IMicroVMConfig,
|
||||||
|
IMicroVMRuntimeOptions,
|
||||||
ISnapshotCreateParams,
|
ISnapshotCreateParams,
|
||||||
ISnapshotLoadParams,
|
ISnapshotLoadParams,
|
||||||
|
IDriveConfig,
|
||||||
ITapDevice,
|
ITapDevice,
|
||||||
} from './interfaces/index.js';
|
} from './interfaces/index.js';
|
||||||
import { SmartVMError } from './interfaces/index.js';
|
import { SmartVMError } from './interfaces/index.js';
|
||||||
@@ -26,6 +28,9 @@ export class MicroVM {
|
|||||||
private networkManager: NetworkManager;
|
private networkManager: NetworkManager;
|
||||||
private binaryPath: string;
|
private binaryPath: string;
|
||||||
private socketPath: string;
|
private socketPath: string;
|
||||||
|
private runtimeDir: string;
|
||||||
|
private ephemeralWritableDrives: boolean;
|
||||||
|
private vmRuntimeDir: string | null = null;
|
||||||
private tapDevices: ITapDevice[] = [];
|
private tapDevices: ITapDevice[] = [];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -34,12 +39,15 @@ export class MicroVM {
|
|||||||
binaryPath: string,
|
binaryPath: string,
|
||||||
socketPath: string,
|
socketPath: string,
|
||||||
networkManager: NetworkManager,
|
networkManager: NetworkManager,
|
||||||
|
runtimeOptions: IMicroVMRuntimeOptions = {},
|
||||||
) {
|
) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.vmConfig = new VMConfig(config);
|
this.vmConfig = new VMConfig(config);
|
||||||
this.binaryPath = binaryPath;
|
this.binaryPath = binaryPath;
|
||||||
this.socketPath = socketPath;
|
this.socketPath = socketPath;
|
||||||
this.networkManager = networkManager;
|
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';
|
this.state = 'configuring';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
await this.ensureVMRuntimeDir();
|
||||||
|
await this.prepareEphemeralDrives();
|
||||||
|
|
||||||
// Start the Firecracker process
|
// Start the Firecracker process
|
||||||
this.process = new FirecrackerProcess({
|
this.process = new FirecrackerProcess({
|
||||||
binaryPath: this.binaryPath,
|
binaryPath: this.binaryPath,
|
||||||
@@ -318,6 +329,13 @@ export class MicroVM {
|
|||||||
return this.vmConfig;
|
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.
|
* Full cleanup: stop process, remove socket, remove TAP devices.
|
||||||
*/
|
*/
|
||||||
@@ -334,12 +352,69 @@ export class MicroVM {
|
|||||||
}
|
}
|
||||||
this.tapDevices = [];
|
this.tapDevices = [];
|
||||||
|
|
||||||
|
if (this.vmRuntimeDir) {
|
||||||
|
await plugins.fs.promises.rm(this.vmRuntimeDir, { recursive: true, force: true });
|
||||||
|
this.vmRuntimeDir = null;
|
||||||
|
}
|
||||||
|
|
||||||
this.socketClient = null;
|
this.socketClient = null;
|
||||||
if (this.state !== 'error') {
|
if (this.state !== 'error') {
|
||||||
this.state = 'stopped';
|
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.
|
* Helper: PUT request with error handling.
|
||||||
*/
|
*/
|
||||||
|
|||||||
+33
-2
@@ -26,6 +26,8 @@ export class SmartVM {
|
|||||||
constructor(options: ISmartVMOptions = {}) {
|
constructor(options: ISmartVMOptions = {}) {
|
||||||
this.options = {
|
this.options = {
|
||||||
dataDir: options.dataDir || '/tmp/.smartvm',
|
dataDir: options.dataDir || '/tmp/.smartvm',
|
||||||
|
runtimeDir: options.runtimeDir || this.getDefaultRuntimeDir(),
|
||||||
|
ephemeralWritableDrives: options.ephemeralWritableDrives ?? true,
|
||||||
arch: options.arch || 'x86_64',
|
arch: options.arch || 'x86_64',
|
||||||
bridgeName: options.bridgeName || 'svbr0',
|
bridgeName: options.bridgeName || 'svbr0',
|
||||||
subnet: options.subnet || '172.30.0.0/24',
|
subnet: options.subnet || '172.30.0.0/24',
|
||||||
@@ -57,6 +59,30 @@ export class SmartVM {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getDefaultRuntimeDir(): string {
|
||||||
|
const tmpfsDir = '/dev/shm';
|
||||||
|
try {
|
||||||
|
if (plugins.fs.existsSync(tmpfsDir) && plugins.fs.statSync(tmpfsDir).isDirectory()) {
|
||||||
|
return plugins.path.join(tmpfsDir, '.smartvm', 'runtime');
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Fall back to os.tmpdir() below.
|
||||||
|
}
|
||||||
|
return plugins.path.join(plugins.os.tmpdir(), '.smartvm', 'runtime');
|
||||||
|
}
|
||||||
|
|
||||||
|
public getRuntimeDir(): string {
|
||||||
|
return this.options.runtimeDir!;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sanitizePathPart(value: string): string {
|
||||||
|
const sanitized = value.replace(/[^a-zA-Z0-9._-]/g, '_');
|
||||||
|
if (!sanitized || sanitized === '.' || sanitized === '..') {
|
||||||
|
return 'item';
|
||||||
|
}
|
||||||
|
return sanitized;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure the Firecracker binary is available.
|
* Ensure the Firecracker binary is available.
|
||||||
* Downloads it if not present.
|
* Downloads it if not present.
|
||||||
@@ -110,8 +136,9 @@ export class SmartVM {
|
|||||||
// Generate VM ID if not provided
|
// Generate VM ID if not provided
|
||||||
const vmId = config.id || plugins.smartunique.uuid4();
|
const vmId = config.id || plugins.smartunique.uuid4();
|
||||||
|
|
||||||
// Generate socket path
|
// Keep per-VM runtime artifacts in tmpfs by default.
|
||||||
const socketPath = this.imageManager.getSocketPath(vmId);
|
const vmRuntimeDir = plugins.path.join(this.options.runtimeDir!, this.sanitizePathPart(vmId));
|
||||||
|
const socketPath = plugins.path.join(vmRuntimeDir, 'firecracker.sock');
|
||||||
|
|
||||||
// Create MicroVM instance
|
// Create MicroVM instance
|
||||||
const vm = new MicroVM(
|
const vm = new MicroVM(
|
||||||
@@ -120,6 +147,10 @@ export class SmartVM {
|
|||||||
this.firecrackerBinaryPath!,
|
this.firecrackerBinaryPath!,
|
||||||
socketPath,
|
socketPath,
|
||||||
this.networkManager,
|
this.networkManager,
|
||||||
|
{
|
||||||
|
runtimeDir: this.options.runtimeDir,
|
||||||
|
ephemeralWritableDrives: this.options.ephemeralWritableDrives,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Register in active VMs
|
// Register in active VMs
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export class VMConfig {
|
|||||||
drives: config.drives?.map((drive) => ({
|
drives: config.drives?.map((drive) => ({
|
||||||
...drive,
|
...drive,
|
||||||
rateLimiter: drive.rateLimiter ? this.cloneRateLimiter(drive.rateLimiter) : undefined,
|
rateLimiter: drive.rateLimiter ? this.cloneRateLimiter(drive.rateLimiter) : undefined,
|
||||||
|
ephemeral: drive.ephemeral,
|
||||||
})),
|
})),
|
||||||
networkInterfaces: config.networkInterfaces?.map((iface) => ({
|
networkInterfaces: config.networkInterfaces?.map((iface) => ({
|
||||||
...iface,
|
...iface,
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ import type { TFirecrackerArch, TCacheType, TSnapshotType, TLogLevel } from './c
|
|||||||
export interface ISmartVMOptions {
|
export interface ISmartVMOptions {
|
||||||
/** Directory for storing binaries, kernels, rootfs images, and sockets. Defaults to /tmp/.smartvm */
|
/** Directory for storing binaries, kernels, rootfs images, and sockets. Defaults to /tmp/.smartvm */
|
||||||
dataDir?: string;
|
dataDir?: string;
|
||||||
|
/** Directory for VM sockets and ephemeral per-VM files. Defaults to /dev/shm/.smartvm/runtime on Linux when available. */
|
||||||
|
runtimeDir?: string;
|
||||||
|
/** Copy writable drives into the VM runtime directory before boot and delete them on cleanup. Defaults to true. */
|
||||||
|
ephemeralWritableDrives?: boolean;
|
||||||
/** Firecracker version to use. Defaults to latest. */
|
/** Firecracker version to use. Defaults to latest. */
|
||||||
firecrackerVersion?: string;
|
firecrackerVersion?: string;
|
||||||
/** Target architecture. Defaults to x86_64. */
|
/** Target architecture. Defaults to x86_64. */
|
||||||
@@ -139,6 +143,16 @@ export interface IBaseImageBundle {
|
|||||||
lastAccessedAt: string;
|
lastAccessedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runtime behavior for a MicroVM instance.
|
||||||
|
*/
|
||||||
|
export interface IMicroVMRuntimeOptions {
|
||||||
|
/** Directory for VM sockets and ephemeral per-VM files. */
|
||||||
|
runtimeDir?: string;
|
||||||
|
/** Copy writable drives into runtimeDir before boot and delete them on cleanup. Defaults to true. */
|
||||||
|
ephemeralWritableDrives?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Firecracker boot source configuration.
|
* Firecracker boot source configuration.
|
||||||
*/
|
*/
|
||||||
@@ -205,6 +219,8 @@ export interface IDriveConfig {
|
|||||||
rateLimiter?: IRateLimiter;
|
rateLimiter?: IRateLimiter;
|
||||||
/** Path to a file that backs the device for I/O. */
|
/** Path to a file that backs the device for I/O. */
|
||||||
ioEngine?: string;
|
ioEngine?: string;
|
||||||
|
/** Whether this drive should be staged into per-VM ephemeral storage. Defaults to true for writable drives. */
|
||||||
|
ephemeral?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user