|
|
|
@@ -1,190 +1,341 @@
|
|
|
|
|
# @push.rocks/smartvm
|
|
|
|
|
|
|
|
|
|
A TypeScript module that wraps Amazon's [Firecracker VMM](https://firecracker-microvm.github.io/) to create, configure, and manage lightweight microVMs with a clean, type-safe API.
|
|
|
|
|
Run Firecracker microVMs from TypeScript without hand-rolling process management, Unix-socket HTTP calls, TAP devices, bridge setup, image caching, and cleanup. `@push.rocks/smartvm` gives you a typed orchestration layer for building tiny, fast, disk-light VM workflows on Linux/KVM.
|
|
|
|
|
|
|
|
|
|
## Issue Reporting and Security
|
|
|
|
|
|
|
|
|
|
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
|
|
|
|
|
|
|
|
|
|
## What It Does
|
|
|
|
|
|
|
|
|
|
`smartvm` wraps the operational parts of Amazon Firecracker:
|
|
|
|
|
|
|
|
|
|
- Downloads and caches Firecracker binaries.
|
|
|
|
|
- Resolves bootable Firecracker CI base-image bundles with `latest` and `lts` presets.
|
|
|
|
|
- Supports project-owned hosted base-image manifests with SHA256 verification.
|
|
|
|
|
- Creates and controls microVMs through Firecracker's HTTP-over-Unix-socket API.
|
|
|
|
|
- Converts TypeScript camelCase config into Firecracker's snake_case payloads.
|
|
|
|
|
- Creates TAP devices, a Linux bridge, static guest IP assignments, and NAT rules.
|
|
|
|
|
- Defaults VM runtime artifacts to tmpfs via `/dev/shm/.smartvm/runtime` when available.
|
|
|
|
|
- Stages writable drives into per-VM ephemeral storage by default so guest writes do not touch cached rootfs files.
|
|
|
|
|
- Cleans up Firecracker processes, sockets, TAPs, bridges, NAT rules, and staged drive copies.
|
|
|
|
|
|
|
|
|
|
The design goal is close to a Cloudflare Workers/Deno-style filesystem model, adapted to Firecracker: immutable root image, explicit writable scratch, no accidental persistent state, and persistence only when you opt in.
|
|
|
|
|
|
|
|
|
|
## Install
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
pnpm install @push.rocks/smartvm
|
|
|
|
|
pnpm add @push.rocks/smartvm
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
> ⚡ **Prerequisites**: Firecracker requires a Linux host with KVM support (`/dev/kvm`). Networking features (TAP devices, bridges, NAT) require root privileges.
|
|
|
|
|
## Runtime Requirements
|
|
|
|
|
|
|
|
|
|
Runtime host requirements:
|
|
|
|
|
Firecracker is a Linux/KVM technology. The package is TypeScript, but the runtime host must provide the VM substrate.
|
|
|
|
|
|
|
|
|
|
- Linux with `/dev/kvm` available to the running process
|
|
|
|
|
- A Firecracker binary downloaded by `ensureBinary()` or supplied through `firecrackerBinaryPath`
|
|
|
|
|
- Root privileges for automatic bridge, TAP, IP forwarding, and iptables NAT setup
|
|
|
|
|
- Host tools available for networking: `ip`, `sysctl`, and `iptables`
|
|
|
|
|
- IPv4 CIDR subnets with prefix length `1-30`; the bridge uses the first usable address as gateway and guests start at the second usable address
|
|
|
|
|
| Requirement | Why it matters |
|
|
|
|
|
|---|---|
|
|
|
|
|
| Linux with `/dev/kvm` | Firecracker needs KVM acceleration. |
|
|
|
|
|
| Firecracker binary | Downloaded by `ensureBinary()` or supplied through `firecrackerBinaryPath`. |
|
|
|
|
|
| Root privileges for networking | TAP devices, bridges, IP forwarding, and iptables NAT require elevated privileges. |
|
|
|
|
|
| Host tools: `curl`, `tar`, `ip`, `sysctl`, `iptables` | Used for binary/image downloads and network setup. |
|
|
|
|
|
| Enough tmpfs memory | Writable VM drives are copied into `/dev/shm` by default when available. |
|
|
|
|
|
|
|
|
|
|
## Quick Start
|
|
|
|
|
|
|
|
|
|
This is the happy path: let `smartvm` download Firecracker, resolve a known-good base image, boot it, and clean it up.
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
import { SmartVM } from '@push.rocks/smartvm';
|
|
|
|
|
|
|
|
|
|
// 1. Create the orchestrator
|
|
|
|
|
const smartvm = new SmartVM({
|
|
|
|
|
dataDir: '/opt/smartvm', // where binaries, kernels, rootfs are cached
|
|
|
|
|
firecrackerVersion: 'v1.7.0', // or omit for latest
|
|
|
|
|
arch: 'x86_64',
|
|
|
|
|
// Optional. Defaults are intentionally disk-light.
|
|
|
|
|
dataDir: '/tmp/.smartvm',
|
|
|
|
|
runtimeDir: '/dev/shm/.smartvm/runtime',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 2. Download Firecracker if not already present
|
|
|
|
|
await smartvm.ensureBinary();
|
|
|
|
|
const baseImage = await smartvm.ensureBaseImage({ preset: 'latest' });
|
|
|
|
|
|
|
|
|
|
// 3. Create a MicroVM
|
|
|
|
|
const vm = await smartvm.createVM({
|
|
|
|
|
id: 'hello-firecracker',
|
|
|
|
|
bootSource: {
|
|
|
|
|
kernelImagePath: '/opt/smartvm/kernels/vmlinux',
|
|
|
|
|
bootArgs: 'console=ttyS0 reboot=k panic=1 pci=off',
|
|
|
|
|
kernelImagePath: baseImage.kernelImagePath,
|
|
|
|
|
bootArgs: baseImage.bootArgs,
|
|
|
|
|
},
|
|
|
|
|
machineConfig: {
|
|
|
|
|
vcpuCount: 2,
|
|
|
|
|
vcpuCount: 1,
|
|
|
|
|
memSizeMib: 256,
|
|
|
|
|
},
|
|
|
|
|
drives: [
|
|
|
|
|
{
|
|
|
|
|
driveId: 'rootfs',
|
|
|
|
|
pathOnHost: '/opt/smartvm/rootfs/ubuntu.ext4',
|
|
|
|
|
pathOnHost: baseImage.rootfsPath,
|
|
|
|
|
isRootDevice: true,
|
|
|
|
|
isReadOnly: false,
|
|
|
|
|
isReadOnly: baseImage.rootfsIsReadOnly,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
networkInterfaces: [
|
|
|
|
|
{ ifaceId: 'eth0' }, // TAP device and MAC auto-generated
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 4. Start it 🚀
|
|
|
|
|
await vm.start();
|
|
|
|
|
|
|
|
|
|
// 5. Inspect
|
|
|
|
|
console.log(vm.state); // 'running'
|
|
|
|
|
console.log(await vm.getInfo()); // Firecracker instance info
|
|
|
|
|
|
|
|
|
|
// 6. Pause / Resume
|
|
|
|
|
await vm.pause(); // state → 'paused'
|
|
|
|
|
await vm.resume(); // state → 'running'
|
|
|
|
|
|
|
|
|
|
// 7. Stop and clean up
|
|
|
|
|
await vm.stop();
|
|
|
|
|
await vm.cleanup();
|
|
|
|
|
await smartvm.cleanup();
|
|
|
|
|
try {
|
|
|
|
|
await vm.start();
|
|
|
|
|
console.log(vm.state); // "running"
|
|
|
|
|
console.log(await vm.getVersion());
|
|
|
|
|
console.log(await vm.getInfo());
|
|
|
|
|
} finally {
|
|
|
|
|
if (vm.state === 'running' || vm.state === 'paused') {
|
|
|
|
|
await vm.stop();
|
|
|
|
|
}
|
|
|
|
|
await vm.cleanup();
|
|
|
|
|
await smartvm.cleanup();
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Architecture Overview
|
|
|
|
|
## Disk-Light Runtime Model
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
┌─────────────────────────────────────────────┐
|
|
|
|
|
│ SmartVM │ ← Top-level orchestrator
|
|
|
|
|
│ ┌──────────────┐ ┌────────────────────┐ │
|
|
|
|
|
│ │ ImageManager │ │ NetworkManager │ │
|
|
|
|
|
│ │ (binaries, │ │ (TAP, bridge, │ │
|
|
|
|
|
│ │ kernels, │ │ NAT, IP alloc) │ │
|
|
|
|
|
│ │ rootfs) │ │ │ │
|
|
|
|
|
│ └──────────────┘ └────────────────────┘ │
|
|
|
|
|
│ │
|
|
|
|
|
│ ┌─────────── MicroVM ────────────────┐ │
|
|
|
|
|
│ │ state: created → configuring → │ │
|
|
|
|
|
│ │ running → paused → stopped │ │
|
|
|
|
|
│ │ │ │
|
|
|
|
|
│ │ ┌──────────────────────────────┐ │ │
|
|
|
|
|
│ │ │ FirecrackerProcess │ │ │
|
|
|
|
|
│ │ │ (child process management) │ │ │
|
|
|
|
|
│ │ └──────────────────────────────┘ │ │
|
|
|
|
|
│ │ ┌──────────────────────────────┐ │ │
|
|
|
|
|
│ │ │ SocketClient │ │ │
|
|
|
|
|
│ │ │ (HTTP over Unix socket) │ │ │
|
|
|
|
|
│ │ └──────────────────────────────┘ │ │
|
|
|
|
|
│ │ ┌──────────────────────────────┐ │ │
|
|
|
|
|
│ │ │ VMConfig │ │ │
|
|
|
|
|
│ │ │ (camelCase → snake_case) │ │ │
|
|
|
|
|
│ │ └──────────────────────────────┘ │ │
|
|
|
|
|
│ └────────────────────────────────────┘ │
|
|
|
|
|
└─────────────────────────────────────────────┘
|
|
|
|
|
By default, `smartvm` treats VMs as ephemeral execution units.
|
|
|
|
|
|
|
|
|
|
| Path | Default | Persistence model |
|
|
|
|
|
|---|---|---|
|
|
|
|
|
| Firecracker binaries | `/tmp/.smartvm/bin` | Cached for reuse. |
|
|
|
|
|
| Base images | `/tmp/.smartvm/base-images` | Cached, retention-limited, verified before reuse. |
|
|
|
|
|
| VM sockets | `/dev/shm/.smartvm/runtime/<vmId>/firecracker.sock` | Per-VM tmpfs, deleted on cleanup. |
|
|
|
|
|
| Writable drives | `/dev/shm/.smartvm/runtime/<vmId>/drives/*` | Per-VM tmpfs copy, deleted on cleanup. |
|
|
|
|
|
| Read-only drives | Original path | Not copied unless `ephemeral: true`. |
|
|
|
|
|
|
|
|
|
|
Writable drives are staged into the VM runtime directory before boot. Firecracker receives the staged path, so guest writes do not modify cached base images or source rootfs files.
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
const vm = await smartvm.createVM({
|
|
|
|
|
bootSource: { kernelImagePath: baseImage.kernelImagePath, bootArgs: baseImage.bootArgs },
|
|
|
|
|
machineConfig: { vcpuCount: 1, memSizeMib: 256 },
|
|
|
|
|
drives: [
|
|
|
|
|
{
|
|
|
|
|
driveId: 'rootfs',
|
|
|
|
|
pathOnHost: baseImage.rootfsPath,
|
|
|
|
|
isRootDevice: true,
|
|
|
|
|
isReadOnly: false,
|
|
|
|
|
// Default for writable drives: true
|
|
|
|
|
ephemeral: true,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Firecracker exposes a REST API over a Unix domain socket. This module handles all the plumbing: spawning the process, waiting for the socket, translating your TypeScript config into Firecracker's snake_case API payloads, managing TAP devices, and tearing everything down on exit.
|
|
|
|
|
Opt into persistence only when that is the point:
|
|
|
|
|
|
|
|
|
|
## API Reference
|
|
|
|
|
```typescript
|
|
|
|
|
const persistentVm = await smartvm.createVM({
|
|
|
|
|
bootSource: { kernelImagePath: '/images/vmlinux', bootArgs: 'console=ttyS0 reboot=k panic=1 pci=off' },
|
|
|
|
|
machineConfig: { vcpuCount: 2, memSizeMib: 512 },
|
|
|
|
|
drives: [
|
|
|
|
|
{
|
|
|
|
|
driveId: 'state',
|
|
|
|
|
pathOnHost: '/var/lib/my-vm/state.ext4',
|
|
|
|
|
isRootDevice: true,
|
|
|
|
|
isReadOnly: false,
|
|
|
|
|
ephemeral: false,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### `SmartVM` — The Orchestrator
|
|
|
|
|
You can also disable writable-drive staging globally:
|
|
|
|
|
|
|
|
|
|
The entry point for everything. Manages binary downloads, VM creation, and global cleanup.
|
|
|
|
|
```typescript
|
|
|
|
|
const smartvm = new SmartVM({
|
|
|
|
|
ephemeralWritableDrives: false,
|
|
|
|
|
});
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Best practice for high-volume VM starts:
|
|
|
|
|
|
|
|
|
|
- Prefer `squashfs` or another read-only root filesystem.
|
|
|
|
|
- Put mutable scratch data on tmpfs-backed writable drives.
|
|
|
|
|
- Keep shared assets read-only by default.
|
|
|
|
|
- Use external services, object storage, databases, or explicit persistent drives for durable state.
|
|
|
|
|
- Use a dedicated `runtimeDir` on a real tmpfs if `/dev/shm` is too small or unavailable.
|
|
|
|
|
|
|
|
|
|
## Architecture
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
SmartVM
|
|
|
|
|
ImageManager downloads/caches Firecracker binaries and manual images
|
|
|
|
|
BaseImageManager resolves known-good base-image bundles
|
|
|
|
|
NetworkManager creates TAP devices, bridge, NAT, and static guest network data
|
|
|
|
|
MicroVM
|
|
|
|
|
FirecrackerProcess starts/stops the VMM process
|
|
|
|
|
SocketClient talks HTTP over the Firecracker Unix socket
|
|
|
|
|
VMConfig validates and transforms TypeScript config
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Firecracker exposes a REST API over a Unix domain socket. `smartvm` starts the child process, waits for readiness, sends pre-boot config in the right order, starts the instance, and tears down host resources when you are done.
|
|
|
|
|
|
|
|
|
|
## SmartVM
|
|
|
|
|
|
|
|
|
|
`SmartVM` is the top-level orchestrator.
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
import { SmartVM } from '@push.rocks/smartvm';
|
|
|
|
|
import type { ISmartVMOptions } from '@push.rocks/smartvm';
|
|
|
|
|
|
|
|
|
|
const smartvm = new SmartVM({
|
|
|
|
|
dataDir: '/tmp/.smartvm', // default: /tmp/.smartvm
|
|
|
|
|
firecrackerVersion: 'v1.7.0', // default: latest from GitHub
|
|
|
|
|
arch: 'x86_64', // default: x86_64 (also: aarch64)
|
|
|
|
|
firecrackerBinaryPath: '/usr/bin/firecracker', // optional: skip download
|
|
|
|
|
baseImageCacheDir: '/tmp/.smartvm/base-images', // default: /tmp/.smartvm/base-images
|
|
|
|
|
maxStoredBaseImages: 2, // default: keep at most 2 cached base image bundles
|
|
|
|
|
bridgeName: 'svbr0', // default: svbr0
|
|
|
|
|
subnet: '172.30.0.0/24', // default: 172.30.0.0/24
|
|
|
|
|
});
|
|
|
|
|
const options: ISmartVMOptions = {
|
|
|
|
|
dataDir: '/tmp/.smartvm',
|
|
|
|
|
runtimeDir: '/dev/shm/.smartvm/runtime',
|
|
|
|
|
ephemeralWritableDrives: true,
|
|
|
|
|
firecrackerVersion: 'v1.7.0',
|
|
|
|
|
arch: 'x86_64',
|
|
|
|
|
firecrackerBinaryPath: '/usr/bin/firecracker',
|
|
|
|
|
bridgeName: 'svbr0',
|
|
|
|
|
subnet: '172.30.0.0/24',
|
|
|
|
|
baseImageCacheDir: '/tmp/.smartvm/base-images',
|
|
|
|
|
maxStoredBaseImages: 2,
|
|
|
|
|
baseImageManifestUrl: 'https://assets.example.com/smartvm/manifest.json',
|
|
|
|
|
baseImageManifestPath: './assets/base-images/local.manifest.json',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const smartvm = new SmartVM(options);
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
| Method | Description |
|
|
|
|
|
| API | Description |
|
|
|
|
|
|---|---|
|
|
|
|
|
| `ensureBinary()` | Downloads Firecracker from GitHub if not cached. Returns path to binary. |
|
|
|
|
|
| `ensureBaseImage(options)` | Downloads/caches a Firecracker CI base image bundle. Defaults to the `latest` preset. |
|
|
|
|
|
| `createVM(config)` | Creates a `MicroVM` instance (not yet started). Returns the VM. |
|
|
|
|
|
| `getVM(id)` | Look up an active VM by ID. |
|
|
|
|
|
| `listVMs()` | Returns an array of active VM IDs. |
|
|
|
|
|
| `vmCount` | Number of active VMs. |
|
|
|
|
|
| `stopAll()` | Stops all running/paused VMs in parallel. |
|
|
|
|
|
| `cleanup()` | Stops all VMs, removes TAP devices and bridge. |
|
|
|
|
|
| `ensureBinary()` | Ensures the Firecracker binary exists and returns its path. |
|
|
|
|
|
| `ensureBaseImage(options)` | Resolves/downloads a base-image bundle and returns kernel/rootfs paths plus boot args. |
|
|
|
|
|
| `createVM(config)` | Creates a `MicroVM` instance. It does not boot until `vm.start()`. |
|
|
|
|
|
| `getRuntimeDir()` | Returns the active runtime directory used for per-VM tmpfs artifacts. |
|
|
|
|
|
| `getVM(id)` | Looks up an active VM by ID. |
|
|
|
|
|
| `listVMs()` | Lists active VM IDs. |
|
|
|
|
|
| `vmCount` | Number of tracked VMs. |
|
|
|
|
|
| `removeVM(id)` | Removes a VM from the internal tracking map. |
|
|
|
|
|
| `stopAll()` | Stops every running or paused VM. |
|
|
|
|
|
| `cleanup()` | Cleans up all tracked VMs and networking resources. |
|
|
|
|
|
|
|
|
|
|
### `MicroVM` — VM Lifecycle
|
|
|
|
|
## Base Images
|
|
|
|
|
|
|
|
|
|
Each VM follows a strict state machine: **created → configuring → running → paused → stopped**.
|
|
|
|
|
`BaseImageManager` gives you fast bootable image discovery without committing giant rootfs files to git.
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
const baseImage = await smartvm.ensureBaseImage(); // preset: "latest"
|
|
|
|
|
const ltsBaseImage = await smartvm.ensureBaseImage({ preset: 'lts' });
|
|
|
|
|
const freshBaseImage = await smartvm.ensureBaseImage({ preset: 'latest', forceDownload: true });
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Presets:
|
|
|
|
|
|
|
|
|
|
| Preset | Behavior |
|
|
|
|
|
|---|---|
|
|
|
|
|
| `latest` | Resolves the latest Firecracker release and matching CI demo artifacts. |
|
|
|
|
|
| `lts` | Uses the pinned Firecracker CI train `v1.7` / Firecracker `v1.7.0`. |
|
|
|
|
|
| `hosted` | Uses a project-owned manifest. Requires `manifestUrl`, `manifestPath`, or manager-level hosted manifest options. |
|
|
|
|
|
|
|
|
|
|
The resolver prefers read-only `squashfs` rootfs artifacts when Firecracker CI exposes them, falling back to `ext4` when needed.
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
import { BaseImageManager } from '@push.rocks/smartvm';
|
|
|
|
|
|
|
|
|
|
const baseImageManager = new BaseImageManager({
|
|
|
|
|
arch: 'x86_64',
|
|
|
|
|
cacheDir: '/tmp/.smartvm/base-images',
|
|
|
|
|
maxStoredBaseImages: 4,
|
|
|
|
|
hostedManifestPath: './assets/base-images/smartvm-minimal.manifest.json',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
console.log(baseImageManager.getCacheDir());
|
|
|
|
|
console.log(baseImageManager.getMaxStoredBaseImages());
|
|
|
|
|
|
|
|
|
|
const hosted = await baseImageManager.ensureBaseImage({ preset: 'hosted' });
|
|
|
|
|
const evictedBundleIds = await baseImageManager.pruneBaseImageCache(hosted.bundleId);
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
`IBaseImageBundle` contains:
|
|
|
|
|
|
|
|
|
|
- `kernelImagePath`
|
|
|
|
|
- `rootfsPath`
|
|
|
|
|
- `rootfsType`
|
|
|
|
|
- `rootfsIsReadOnly`
|
|
|
|
|
- `bootArgs`
|
|
|
|
|
- `firecrackerVersion`
|
|
|
|
|
- `checksums`
|
|
|
|
|
- `sizes`
|
|
|
|
|
- source metadata
|
|
|
|
|
|
|
|
|
|
Cache behavior:
|
|
|
|
|
|
|
|
|
|
- Default cache directory: `/tmp/.smartvm/base-images`
|
|
|
|
|
- Default retention: `2` bundles
|
|
|
|
|
- Older bundles are evicted with a `console.warn` when retention is exceeded
|
|
|
|
|
- Cached artifacts are checked for size and SHA256 before reuse
|
|
|
|
|
- Hosted URL artifacts require SHA256 hashes
|
|
|
|
|
- Hosted local-path artifacts may omit SHA256, but hashes are still recorded in the cached manifest
|
|
|
|
|
|
|
|
|
|
Hosted manifest example:
|
|
|
|
|
|
|
|
|
|
The repository ships an example at `assets/base-images/smartvm-minimal.manifest.example.json`.
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
"schemaVersion": 1,
|
|
|
|
|
"bundleId": "smartvm-minimal-v1-x86_64",
|
|
|
|
|
"name": "SmartVM minimal x86_64 bundle",
|
|
|
|
|
"arch": "x86_64",
|
|
|
|
|
"firecrackerVersion": "v1.15.1",
|
|
|
|
|
"rootfsType": "squashfs",
|
|
|
|
|
"rootfsIsReadOnly": true,
|
|
|
|
|
"bootArgs": "console=ttyS0 reboot=k panic=1 pci=off ro rootfstype=squashfs",
|
|
|
|
|
"kernel": {
|
|
|
|
|
"url": "https://assets.example.com/smartvm/vmlinux",
|
|
|
|
|
"fileName": "vmlinux",
|
|
|
|
|
"sha256": "0000000000000000000000000000000000000000000000000000000000000000",
|
|
|
|
|
"sizeBytes": 12345678
|
|
|
|
|
},
|
|
|
|
|
"rootfs": {
|
|
|
|
|
"url": "https://assets.example.com/smartvm/rootfs.squashfs",
|
|
|
|
|
"fileName": "rootfs.squashfs",
|
|
|
|
|
"sha256": "0000000000000000000000000000000000000000000000000000000000000000",
|
|
|
|
|
"sizeBytes": 12345678
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## MicroVM Lifecycle
|
|
|
|
|
|
|
|
|
|
`MicroVM` is a single Firecracker instance with a strict state machine:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
created -> configuring -> running -> paused -> stopped
|
|
|
|
|
\-> error
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
const vm = await smartvm.createVM({
|
|
|
|
|
id: 'my-vm', // optional, auto-generated UUID if omitted
|
|
|
|
|
id: 'api-worker-1',
|
|
|
|
|
bootSource: {
|
|
|
|
|
kernelImagePath: '/path/to/vmlinux',
|
|
|
|
|
bootArgs: 'console=ttyS0 reboot=k panic=1',
|
|
|
|
|
initrdPath: '/path/to/initrd', // optional
|
|
|
|
|
kernelImagePath: baseImage.kernelImagePath,
|
|
|
|
|
bootArgs: baseImage.bootArgs,
|
|
|
|
|
},
|
|
|
|
|
machineConfig: {
|
|
|
|
|
vcpuCount: 4,
|
|
|
|
|
vcpuCount: 2,
|
|
|
|
|
memSizeMib: 512,
|
|
|
|
|
smt: false,
|
|
|
|
|
cpuTemplate: 'T2', // optional: C3, T2, T2S, T2CL, T2A, V1N1
|
|
|
|
|
cpuTemplate: 'T2',
|
|
|
|
|
trackDirtyPages: true,
|
|
|
|
|
},
|
|
|
|
|
drives: [
|
|
|
|
|
{
|
|
|
|
|
driveId: 'rootfs',
|
|
|
|
|
pathOnHost: '/path/to/rootfs.ext4',
|
|
|
|
|
pathOnHost: baseImage.rootfsPath,
|
|
|
|
|
isRootDevice: true,
|
|
|
|
|
isReadOnly: false,
|
|
|
|
|
cacheType: 'Unsafe', // or 'Writeback'
|
|
|
|
|
isReadOnly: baseImage.rootfsIsReadOnly,
|
|
|
|
|
cacheType: 'Unsafe',
|
|
|
|
|
ephemeral: true,
|
|
|
|
|
rateLimiter: {
|
|
|
|
|
bandwidth: { size: 100_000_000, refillTime: 1_000_000_000 },
|
|
|
|
|
ops: { size: 1000, refillTime: 1_000_000_000 },
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
networkInterfaces: [
|
|
|
|
|
{
|
|
|
|
|
ifaceId: 'eth0',
|
|
|
|
|
// hostDevName and guestMac auto-generated if omitted
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
networkInterfaces: [{ ifaceId: 'eth0' }],
|
|
|
|
|
vsock: {
|
|
|
|
|
guestCid: 3,
|
|
|
|
|
udsPath: '/tmp/vsock.sock',
|
|
|
|
|
udsPath: '/dev/shm/api-worker-1.vsock',
|
|
|
|
|
},
|
|
|
|
|
balloon: {
|
|
|
|
|
amountMib: 128,
|
|
|
|
@@ -195,276 +346,184 @@ const vm = await smartvm.createVM({
|
|
|
|
|
version: 'V2',
|
|
|
|
|
networkInterfaces: ['eth0'],
|
|
|
|
|
},
|
|
|
|
|
logger: {
|
|
|
|
|
logPath: '/tmp/firecracker.log',
|
|
|
|
|
level: 'Debug',
|
|
|
|
|
showLogOrigin: true,
|
|
|
|
|
},
|
|
|
|
|
metrics: {
|
|
|
|
|
metricsPath: '/tmp/firecracker-metrics.fifo',
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await vm.start();
|
|
|
|
|
await vm.pause();
|
|
|
|
|
await vm.resume();
|
|
|
|
|
await vm.stop();
|
|
|
|
|
await vm.cleanup();
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
| Method | Valid States | Description |
|
|
|
|
|
| API | Valid state | Description |
|
|
|
|
|
|---|---|---|
|
|
|
|
|
| `start()` | `created` | Spawns Firecracker, applies config, boots the VM |
|
|
|
|
|
| `pause()` | `running` | Pauses VM execution |
|
|
|
|
|
| `resume()` | `paused` | Resumes a paused VM |
|
|
|
|
|
| `stop()` | `running`, `paused` | Graceful shutdown (Ctrl+Alt+Del), then force kill |
|
|
|
|
|
| `cleanup()` | any | Full cleanup: kill process, remove socket, remove TAPs |
|
|
|
|
|
| `getInfo()` | any (after start) | Returns Firecracker instance info |
|
|
|
|
|
| `getVersion()` | any (after start) | Returns Firecracker version |
|
|
|
|
|
| `createSnapshot(params)` | `paused` | Create a VM snapshot |
|
|
|
|
|
| `loadSnapshot(params)` | `created`, `configuring` | Load a VM from snapshot |
|
|
|
|
|
| `setMetadata(data)` | `running`, `paused` | Set MMDS metadata |
|
|
|
|
|
| `getMetadata()` | `running`, `paused` | Get MMDS metadata |
|
|
|
|
|
| `updateDrive(id, path)` | `running`, `paused` | Hot-update a drive path |
|
|
|
|
|
| `updateBalloon(mib)` | `running`, `paused` | Resize the balloon device |
|
|
|
|
|
| `getTapDevices()` | any | Returns TAP devices associated with this VM |
|
|
|
|
|
| `start()` | `created` | Stages ephemeral drives, starts Firecracker, applies config, boots the VM. |
|
|
|
|
|
| `pause()` | `running` | Pauses execution. |
|
|
|
|
|
| `resume()` | `paused` | Resumes execution. |
|
|
|
|
|
| `stop()` | `running`, `paused` | Sends Ctrl+Alt+Del, waits briefly, then stops the process. |
|
|
|
|
|
| `cleanup()` | any | Stops process, deletes sockets/runtime dir, removes auto-created TAPs. |
|
|
|
|
|
| `getInfo()` | after start | Returns Firecracker instance info. |
|
|
|
|
|
| `getVersion()` | after start | Returns Firecracker version info. |
|
|
|
|
|
| `setMetadata(data)` | `running`, `paused` | Writes MMDS metadata. |
|
|
|
|
|
| `getMetadata()` | `running`, `paused` | Reads MMDS metadata. |
|
|
|
|
|
| `updateDrive(id, path)` | `running`, `paused` | Hot-updates a drive path. |
|
|
|
|
|
| `updateNetworkInterface(id, update)` | `running`, `paused` | Updates network interface config such as rate limiters. |
|
|
|
|
|
| `updateBalloon(mib)` | `running`, `paused` | Resizes the balloon device. |
|
|
|
|
|
| `createSnapshot(params)` | `paused` | Creates a Firecracker snapshot. |
|
|
|
|
|
| `loadSnapshot(params)` | `created`, `configuring` | Low-level Firecracker snapshot-load call; requires an initialized socket client. |
|
|
|
|
|
| `getTapDevices()` | any | Returns TAP devices created automatically by this VM. |
|
|
|
|
|
| `getVMConfig()` | any | Returns the internal `VMConfig` instance. |
|
|
|
|
|
| `getRuntimeDir()` | any | Returns the per-VM runtime directory after it has been created. |
|
|
|
|
|
|
|
|
|
|
### `ImageManager` — Binary & Image Management
|
|
|
|
|
Snapshot caveat: diff snapshots require dirty-page tracking to be enabled before boot through `machineConfig.trackDirtyPages`. Snapshot restore is currently exposed as a low-level API; full restore orchestration should be built around the Firecracker process/config lifecycle intentionally.
|
|
|
|
|
|
|
|
|
|
Handles downloading and caching Firecracker binaries, kernels, and rootfs images.
|
|
|
|
|
## Networking
|
|
|
|
|
|
|
|
|
|
`NetworkManager` creates host-side networking primitives. It does not run DHCP inside the guest. Your guest image must configure its interface itself, or you must pass static `ip=` kernel boot arguments.
|
|
|
|
|
|
|
|
|
|
Automatic mode:
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
const imageManager = smartvm.imageManager;
|
|
|
|
|
|
|
|
|
|
// Auto-download the latest Firecracker release
|
|
|
|
|
const version = await imageManager.getLatestVersion(); // e.g. 'v1.7.0'
|
|
|
|
|
const binaryPath = await imageManager.downloadFirecracker(version);
|
|
|
|
|
|
|
|
|
|
// Download kernel and rootfs images
|
|
|
|
|
const kernelPath = await imageManager.downloadKernel(
|
|
|
|
|
'https://example.com/vmlinux-5.10',
|
|
|
|
|
'vmlinux-5.10',
|
|
|
|
|
);
|
|
|
|
|
const rootfsPath = await imageManager.downloadRootfs(
|
|
|
|
|
'https://example.com/ubuntu-22.04.ext4',
|
|
|
|
|
'ubuntu-22.04.ext4',
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Create a blank rootfs or clone an existing one
|
|
|
|
|
const blankPath = await imageManager.createBlankRootfs('scratch.ext4', 1024);
|
|
|
|
|
const clonePath = await imageManager.cloneRootfs(rootfsPath, 'ubuntu-clone.ext4');
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**Data directory layout:**
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
/tmp/.smartvm/
|
|
|
|
|
bin/<version>/firecracker
|
|
|
|
|
bin/<version>/jailer
|
|
|
|
|
kernels/<name>
|
|
|
|
|
rootfs/<name>
|
|
|
|
|
sockets/<vmId>.sock
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### `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.
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
const baseImage = await smartvm.ensureBaseImage(); // same as { preset: 'latest' }
|
|
|
|
|
|
|
|
|
|
const ltsBaseImage = await smartvm.ensureBaseImage({ preset: 'lts' });
|
|
|
|
|
|
|
|
|
|
const hostedBaseImage = await smartvm.ensureBaseImage({
|
|
|
|
|
manifestUrl: 'https://assets.example.com/push.rocks/smartvm/base-images/smartvm-minimal-v1/x86_64/manifest.json',
|
|
|
|
|
const vm = await smartvm.createVM({
|
|
|
|
|
bootSource: { kernelImagePath: baseImage.kernelImagePath, bootArgs: baseImage.bootArgs },
|
|
|
|
|
machineConfig: { vcpuCount: 1, memSizeMib: 256 },
|
|
|
|
|
drives: [{ driveId: 'rootfs', pathOnHost: baseImage.rootfsPath, isRootDevice: true, isReadOnly: baseImage.rootfsIsReadOnly }],
|
|
|
|
|
networkInterfaces: [{ ifaceId: 'eth0' }],
|
|
|
|
|
});
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Static-kernel-args mode:
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
const tap = await smartvm.networkManager.createTapDevice('net-vm', 'eth0');
|
|
|
|
|
|
|
|
|
|
const vm = await smartvm.createVM({
|
|
|
|
|
id: 'net-vm',
|
|
|
|
|
bootSource: {
|
|
|
|
|
kernelImagePath: baseImage.kernelImagePath,
|
|
|
|
|
bootArgs: baseImage.bootArgs,
|
|
|
|
|
bootArgs: `${baseImage.bootArgs} ${smartvm.networkManager.getGuestNetworkBootArgs(tap)}`,
|
|
|
|
|
},
|
|
|
|
|
machineConfig: { vcpuCount: 1, memSizeMib: 256 },
|
|
|
|
|
drives: [
|
|
|
|
|
drives: [{ driveId: 'rootfs', pathOnHost: baseImage.rootfsPath, isRootDevice: true, isReadOnly: baseImage.rootfsIsReadOnly }],
|
|
|
|
|
networkInterfaces: [
|
|
|
|
|
{
|
|
|
|
|
driveId: 'rootfs',
|
|
|
|
|
pathOnHost: baseImage.rootfsPath,
|
|
|
|
|
isRootDevice: true,
|
|
|
|
|
isReadOnly: baseImage.rootfsIsReadOnly,
|
|
|
|
|
ifaceId: 'eth0',
|
|
|
|
|
hostDevName: tap.tapName,
|
|
|
|
|
guestMac: tap.mac,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**Cache behavior:**
|
|
|
|
|
Networking behavior:
|
|
|
|
|
|
|
|
|
|
- Default cache directory: `/tmp/.smartvm/base-images`
|
|
|
|
|
- Default retention: at most `2` base image bundles
|
|
|
|
|
- Configure retention with `maxStoredBaseImages`
|
|
|
|
|
- Configure location with `baseImageCacheDir`
|
|
|
|
|
- When a new download causes the retention limit to be exceeded, older bundles are removed and a console warning is emitted
|
|
|
|
|
- Downloaded bundles include a local `manifest.json` with source URLs/keys, file paths, sizes, and computed SHA256 hashes
|
|
|
|
|
- Default bridge: `svbr0`
|
|
|
|
|
- Default subnet: `172.30.0.0/24`
|
|
|
|
|
- Subnet input is normalized to the network address
|
|
|
|
|
- Prefix length must be `1-30`
|
|
|
|
|
- Gateway uses the first usable address
|
|
|
|
|
- Guest IP allocation starts at the second usable address
|
|
|
|
|
- Allocation is sequential and not reused within the same `NetworkManager` instance
|
|
|
|
|
- MAC addresses are deterministic and locally administered (`02:xx:xx:xx:xx:xx`)
|
|
|
|
|
- TAP names are capped to Linux's 15-character IFNAMSIZ limit
|
|
|
|
|
- NAT masquerade uses the host default route interface
|
|
|
|
|
- Use a dedicated bridge name; `cleanup()` tears down the bridge configured by this manager
|
|
|
|
|
|
|
|
|
|
Example configuration:
|
|
|
|
|
## ImageManager
|
|
|
|
|
|
|
|
|
|
`ImageManager` is the lower-level helper for Firecracker binaries and manually managed kernel/rootfs files.
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
const smartvm = new SmartVM({
|
|
|
|
|
baseImageCacheDir: '/tmp/.smartvm/base-images',
|
|
|
|
|
maxStoredBaseImages: 4,
|
|
|
|
|
baseImageManifestUrl: 'https://assets.example.com/push.rocks/smartvm/base-images/smartvm-minimal-v1/x86_64/manifest.json',
|
|
|
|
|
});
|
|
|
|
|
const imageManager = smartvm.imageManager;
|
|
|
|
|
|
|
|
|
|
await imageManager.ensureDirectories();
|
|
|
|
|
|
|
|
|
|
const latest = await imageManager.getLatestVersion();
|
|
|
|
|
const firecrackerPath = await imageManager.downloadFirecracker(latest);
|
|
|
|
|
|
|
|
|
|
const kernelPath = await imageManager.downloadKernel(
|
|
|
|
|
'https://example.com/vmlinux',
|
|
|
|
|
'vmlinux',
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const rootfsPath = await imageManager.downloadRootfs(
|
|
|
|
|
'https://example.com/rootfs.ext4',
|
|
|
|
|
'rootfs.ext4',
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const blankRootfs = await imageManager.createBlankRootfs('scratch.ext4', 1024);
|
|
|
|
|
const clonedRootfs = await imageManager.cloneRootfs(rootfsPath, 'vm-rootfs.ext4');
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Hosted manifest format examples live in `assets/base-images/`. Hosted URL artifacts require SHA256 hashes; `smartvm` verifies them during download before returning the bundle paths.
|
|
|
|
|
Useful path helpers:
|
|
|
|
|
|
|
|
|
|
### `NetworkManager` — Host Networking
|
|
|
|
|
- `getBinDir()`
|
|
|
|
|
- `getKernelsDir()`
|
|
|
|
|
- `getRootfsDir()`
|
|
|
|
|
- `getSocketsDir()`
|
|
|
|
|
- `getFirecrackerPath(version)`
|
|
|
|
|
- `getJailerPath(version)`
|
|
|
|
|
- `getSocketPath(vmId)`
|
|
|
|
|
|
|
|
|
|
Automatically manages TAP devices, a Linux bridge, and iptables NAT masquerade rules so VMs get internet access out of the box.
|
|
|
|
|
Note: `SmartVM.createVM()` uses `runtimeDir/<vmId>/firecracker.sock` for new VM sockets by default. `ImageManager.getSocketPath()` remains available for lower-level/custom flows.
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
const networkManager = smartvm.networkManager;
|
|
|
|
|
## VMConfig
|
|
|
|
|
|
|
|
|
|
// Manually create a TAP device (usually handled by MicroVM.start())
|
|
|
|
|
const tap = await networkManager.createTapDevice('vm-id', 'eth0');
|
|
|
|
|
console.log(tap);
|
|
|
|
|
// {
|
|
|
|
|
// tapName: 'svvmideth0',
|
|
|
|
|
// guestIp: '172.30.0.2',
|
|
|
|
|
// gatewayIp: '172.30.0.1',
|
|
|
|
|
// subnetMask: '255.255.255.0',
|
|
|
|
|
// mac: '02:a3:b1:c4:d2:e5'
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// Generate kernel boot args for the guest
|
|
|
|
|
const bootArgs = networkManager.getGuestNetworkBootArgs(tap);
|
|
|
|
|
// 'ip=172.30.0.2::172.30.0.1:255.255.255.0::eth0:off'
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**Networking architecture:**
|
|
|
|
|
- Creates a Linux bridge (default: `svbr0`) with gateway at the first usable subnet address
|
|
|
|
|
- Each VM gets a TAP device attached to the bridge
|
|
|
|
|
- Sequential IP allocation from the second usable subnet address onwards
|
|
|
|
|
- Subnet input is normalized to the network address and allocation fails with `IP_EXHAUSTED` when no guest addresses remain
|
|
|
|
|
- iptables NAT masquerade for outbound internet
|
|
|
|
|
- Deterministic MAC generation (`02:xx:xx:xx:xx:xx` locally-administered)
|
|
|
|
|
- TAP names fit Linux's 15-char IFNAMSIZ limit
|
|
|
|
|
|
|
|
|
|
### `VMConfig` — Config Transformer
|
|
|
|
|
|
|
|
|
|
Converts your camelCase TypeScript config into Firecracker's snake_case API payloads. Also validates configuration before boot.
|
|
|
|
|
`VMConfig` validates `IMicroVMConfig` and transforms it into Firecracker API payloads.
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
import { VMConfig } from '@push.rocks/smartvm';
|
|
|
|
|
|
|
|
|
|
const vmConfig = new VMConfig({
|
|
|
|
|
bootSource: { kernelImagePath: '/path/to/vmlinux' },
|
|
|
|
|
bootSource: { kernelImagePath: '/images/vmlinux', bootArgs: 'console=ttyS0 reboot=k panic=1 pci=off' },
|
|
|
|
|
machineConfig: { vcpuCount: 2, memSizeMib: 256 },
|
|
|
|
|
drives: [{ driveId: 'rootfs', pathOnHost: '/images/rootfs.ext4', isRootDevice: true }],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Validate
|
|
|
|
|
const result = vmConfig.validate();
|
|
|
|
|
// { valid: true, errors: [] }
|
|
|
|
|
const validation = vmConfig.validate();
|
|
|
|
|
if (!validation.valid) {
|
|
|
|
|
throw new Error(validation.errors.join('; '));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Generate API payloads
|
|
|
|
|
vmConfig.toBootSourcePayload();
|
|
|
|
|
// { kernel_image_path: '/path/to/vmlinux' }
|
|
|
|
|
|
|
|
|
|
vmConfig.toMachineConfigPayload();
|
|
|
|
|
// { vcpu_count: 2, mem_size_mib: 256 }
|
|
|
|
|
console.log(vmConfig.toBootSourcePayload());
|
|
|
|
|
console.log(vmConfig.toMachineConfigPayload());
|
|
|
|
|
console.log(vmConfig.toDrivePayload(vmConfig.config.drives![0]));
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### `SocketClient` — Low-Level HTTP Client
|
|
|
|
|
The constructor clones caller-provided config, so internal normalization does not mutate your original object.
|
|
|
|
|
|
|
|
|
|
Direct HTTP-over-Unix-socket communication with Firecracker. You typically don't need this directly — `MicroVM` handles it — but it's available if you want raw API access.
|
|
|
|
|
## SocketClient
|
|
|
|
|
|
|
|
|
|
`SocketClient` is the raw Firecracker API client. Most users should go through `MicroVM`, but the low-level client is exported for tooling and diagnostics.
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
import { SocketClient } from '@push.rocks/smartvm';
|
|
|
|
|
|
|
|
|
|
const client = new SocketClient({ socketPath: '/tmp/firecracker.sock' });
|
|
|
|
|
const client = new SocketClient({ socketPath: '/dev/shm/.smartvm/runtime/vm/firecracker.sock' });
|
|
|
|
|
|
|
|
|
|
const info = await client.get('/');
|
|
|
|
|
const putResult = await client.put('/machine-config', { vcpu_count: 2, mem_size_mib: 256 });
|
|
|
|
|
const patchResult = await client.patch('/vm', { state: 'Paused' });
|
|
|
|
|
const version = await client.get('/version');
|
|
|
|
|
const machineConfig = await client.put('/machine-config', {
|
|
|
|
|
vcpu_count: 1,
|
|
|
|
|
mem_size_mib: 256,
|
|
|
|
|
});
|
|
|
|
|
const paused = await client.patch('/vm', { state: 'Paused' });
|
|
|
|
|
|
|
|
|
|
// Check if socket is alive (polls with timeout)
|
|
|
|
|
const ready = await client.isReady(5000);
|
|
|
|
|
console.log(version.ok, version.statusCode, version.body);
|
|
|
|
|
console.log(machineConfig.ok, machineConfig.statusCode, machineConfig.body);
|
|
|
|
|
console.log(paused.ok, paused.statusCode, paused.body);
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### `SmartVMError` — Error Handling
|
|
|
|
|
`SocketClient` returns `{ ok, statusCode, body }`. Non-2xx responses do not become `API_ERROR` until higher-level `MicroVM` helpers validate them.
|
|
|
|
|
|
|
|
|
|
All errors thrown by this module are `SmartVMError` instances with structured error codes.
|
|
|
|
|
## Metadata Service
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
import { SmartVMError } from '@push.rocks/smartvm';
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await vm.start();
|
|
|
|
|
} catch (err) {
|
|
|
|
|
if (err instanceof SmartVMError) {
|
|
|
|
|
console.log(err.code); // 'INVALID_CONFIG', 'SOCKET_TIMEOUT', 'API_ERROR', etc.
|
|
|
|
|
console.log(err.statusCode); // HTTP status from Firecracker (if applicable)
|
|
|
|
|
console.log(err.details); // Raw error body from Firecracker (if applicable)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**Error codes:**
|
|
|
|
|
|
|
|
|
|
| Code | Description |
|
|
|
|
|
|---|---|
|
|
|
|
|
| `INVALID_STATE` | Operation not valid for current VM state |
|
|
|
|
|
| `INVALID_CONFIG` | Config validation failed |
|
|
|
|
|
| `SOCKET_TIMEOUT` | Firecracker socket didn't become ready |
|
|
|
|
|
| `API_TIMEOUT` | Firecracker API didn't respond in time |
|
|
|
|
|
| `SOCKET_REQUEST_FAILED` | HTTP request to socket failed |
|
|
|
|
|
| `API_ERROR` | Firecracker returned a non-2xx response |
|
|
|
|
|
| `BINARY_NOT_FOUND` | Firecracker binary not at expected path |
|
|
|
|
|
| `DOWNLOAD_FAILED` | Failed to download binary/kernel/rootfs |
|
|
|
|
|
| `VERSION_FETCH_FAILED` | Couldn't query GitHub for latest version |
|
|
|
|
|
| `BASE_IMAGE_RESOLVE_FAILED` | Failed to resolve Firecracker CI base image artifacts |
|
|
|
|
|
| `BASE_IMAGE_MANIFEST_FAILED` | Failed to load or use a hosted base image manifest |
|
|
|
|
|
| `BASE_IMAGE_PREPARE_FAILED` | Failed to download or prepare a base image bundle |
|
|
|
|
|
| `INVALID_BASE_IMAGE_MANIFEST` | Hosted base image manifest is invalid |
|
|
|
|
|
| `INVALID_BASE_IMAGE_CACHE_LIMIT` | Base image cache retention limit is invalid |
|
|
|
|
|
| `INVALID_SUBNET` | Subnet is not a supported IPv4 CIDR range |
|
|
|
|
|
| `INVALID_INTERFACE_NAME` | Bridge or TAP interface name is invalid |
|
|
|
|
|
| `IP_EXHAUSTED` | No guest IP addresses remain in the configured subnet |
|
|
|
|
|
| `BRIDGE_SETUP_FAILED` | Failed to create network bridge |
|
|
|
|
|
| `TAP_CREATE_FAILED` | Failed to create TAP device |
|
|
|
|
|
| `ROOTFS_CREATE_FAILED` | Failed to create blank rootfs |
|
|
|
|
|
| `ROOTFS_CLONE_FAILED` | Failed to clone rootfs image |
|
|
|
|
|
| `START_FAILED` | VM start sequence failed |
|
|
|
|
|
| `NO_CLIENT` | Socket client not initialized |
|
|
|
|
|
|
|
|
|
|
## Snapshots
|
|
|
|
|
|
|
|
|
|
Create and restore VM snapshots for fast cold-start or live migration:
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// Pause first (required for snapshots)
|
|
|
|
|
await vm.pause();
|
|
|
|
|
|
|
|
|
|
// Create a snapshot
|
|
|
|
|
await vm.createSnapshot({
|
|
|
|
|
snapshotPath: '/tmp/snapshot.bin',
|
|
|
|
|
memFilePath: '/tmp/snapshot-mem.bin',
|
|
|
|
|
snapshotType: 'Full', // or 'Diff' for incremental
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Later: restore from snapshot
|
|
|
|
|
const freshVm = await smartvm.createVM({
|
|
|
|
|
bootSource: { kernelImagePath: '/path/to/vmlinux' },
|
|
|
|
|
machineConfig: { vcpuCount: 2, memSizeMib: 256 },
|
|
|
|
|
});
|
|
|
|
|
await freshVm.loadSnapshot({
|
|
|
|
|
snapshotPath: '/tmp/snapshot.bin',
|
|
|
|
|
memFilePath: '/tmp/snapshot-mem.bin',
|
|
|
|
|
resumeVm: true,
|
|
|
|
|
});
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## MMDS (Metadata Service)
|
|
|
|
|
|
|
|
|
|
Pass metadata to your guest VM via Firecracker's Microvm Metadata Service:
|
|
|
|
|
Firecracker MMDS lets the host pass structured metadata to a running VM.
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
const vm = await smartvm.createVM({
|
|
|
|
|
bootSource: { /* ... */ },
|
|
|
|
|
machineConfig: { /* ... */ },
|
|
|
|
|
bootSource: { kernelImagePath: baseImage.kernelImagePath, bootArgs: baseImage.bootArgs },
|
|
|
|
|
machineConfig: { vcpuCount: 1, memSizeMib: 256 },
|
|
|
|
|
drives: [{ driveId: 'rootfs', pathOnHost: baseImage.rootfsPath, isRootDevice: true, isReadOnly: baseImage.rootfsIsReadOnly }],
|
|
|
|
|
networkInterfaces: [{ ifaceId: 'eth0' }],
|
|
|
|
|
mmds: {
|
|
|
|
|
version: 'V2',
|
|
|
|
@@ -474,74 +533,114 @@ const vm = await smartvm.createVM({
|
|
|
|
|
|
|
|
|
|
await vm.start();
|
|
|
|
|
|
|
|
|
|
// Set metadata from host
|
|
|
|
|
await vm.setMetadata({
|
|
|
|
|
instance: { id: 'my-instance', region: 'eu-central-1' },
|
|
|
|
|
secrets: { apiKey: 'sk-...' },
|
|
|
|
|
instance: { id: 'api-worker-1', region: 'local' },
|
|
|
|
|
config: { mode: 'ephemeral' },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Guest can access via: curl http://169.254.169.254/latest/meta-data/
|
|
|
|
|
const data = await vm.getMetadata();
|
|
|
|
|
console.log(await vm.getMetadata());
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Graceful Cleanup
|
|
|
|
|
## Error Handling
|
|
|
|
|
|
|
|
|
|
The module registers cleanup handlers via `@push.rocks/smartexit` so resources are released even if your process crashes:
|
|
|
|
|
|
|
|
|
|
- 🔌 Firecracker child processes are killed
|
|
|
|
|
- 🧹 Unix socket files are removed
|
|
|
|
|
- 🌐 TAP devices are deleted
|
|
|
|
|
- 🌉 Bridge and NAT rules are torn down
|
|
|
|
|
|
|
|
|
|
You can also trigger cleanup manually:
|
|
|
|
|
All package-level failures use `SmartVMError` with structured codes.
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// Stop one VM
|
|
|
|
|
await vm.stop();
|
|
|
|
|
await vm.cleanup();
|
|
|
|
|
import { SmartVMError } from '@push.rocks/smartvm';
|
|
|
|
|
|
|
|
|
|
// Stop all VMs and clean everything
|
|
|
|
|
await smartvm.stopAll();
|
|
|
|
|
await smartvm.cleanup();
|
|
|
|
|
try {
|
|
|
|
|
await vm.start();
|
|
|
|
|
} catch (err) {
|
|
|
|
|
if (err instanceof SmartVMError) {
|
|
|
|
|
console.error(err.code);
|
|
|
|
|
console.error(err.statusCode);
|
|
|
|
|
console.error(err.details);
|
|
|
|
|
}
|
|
|
|
|
throw err;
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
| Code | Meaning |
|
|
|
|
|
|---|---|
|
|
|
|
|
| `INVALID_STATE` | Operation is invalid for the current VM state. |
|
|
|
|
|
| `INVALID_CONFIG` | VM configuration failed validation. |
|
|
|
|
|
| `SOCKET_TIMEOUT` | Firecracker did not create its socket in time. |
|
|
|
|
|
| `API_TIMEOUT` | Firecracker API readiness check timed out. |
|
|
|
|
|
| `SOCKET_REQUEST_FAILED` | Unix-socket HTTP request failed. |
|
|
|
|
|
| `API_ERROR` | Firecracker returned a non-2xx response through a high-level VM call. |
|
|
|
|
|
| `BINARY_NOT_FOUND` | Custom Firecracker binary path does not exist. |
|
|
|
|
|
| `DOWNLOAD_FAILED` | Binary, kernel, or rootfs download failed. |
|
|
|
|
|
| `VERSION_FETCH_FAILED` | Latest Firecracker version lookup failed. |
|
|
|
|
|
| `BASE_IMAGE_RESOLVE_FAILED` | Firecracker CI base-image artifact resolution failed. |
|
|
|
|
|
| `BASE_IMAGE_MANIFEST_FAILED` | Hosted manifest could not be loaded or used. |
|
|
|
|
|
| `BASE_IMAGE_PREPARE_FAILED` | Base-image download/copy/verification failed. |
|
|
|
|
|
| `INVALID_BASE_IMAGE_MANIFEST` | Hosted manifest schema or artifact metadata is invalid. |
|
|
|
|
|
| `INVALID_BASE_IMAGE_CACHE_LIMIT` | Base-image retention limit is invalid. |
|
|
|
|
|
| `INVALID_SUBNET` | Subnet is not a supported IPv4 CIDR. |
|
|
|
|
|
| `INVALID_INTERFACE_NAME` | Bridge or TAP name is invalid. |
|
|
|
|
|
| `IP_EXHAUSTED` | No guest IPs remain in the configured subnet. |
|
|
|
|
|
| `BRIDGE_SETUP_FAILED` | Bridge/NAT setup failed. |
|
|
|
|
|
| `TAP_CREATE_FAILED` | TAP creation failed. |
|
|
|
|
|
| `ROOTFS_CREATE_FAILED` | Blank rootfs creation failed. |
|
|
|
|
|
| `ROOTFS_CLONE_FAILED` | Rootfs clone failed. |
|
|
|
|
|
| `START_FAILED` | VM start sequence failed. |
|
|
|
|
|
| `NO_CLIENT` | Socket client is not initialized. |
|
|
|
|
|
|
|
|
|
|
## Testing
|
|
|
|
|
|
|
|
|
|
The default test suite is unit-level and safe to run without KVM or root privileges:
|
|
|
|
|
Default tests are safe on machines without KVM or root privileges:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
pnpm test
|
|
|
|
|
pnpm run build
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
These tests cover config validation, Firecracker payload generation, lifecycle guard errors, VM tracking, and subnet/IP allocation. They do not boot a real microVM.
|
|
|
|
|
The default suite covers config validation, payload generation, lifecycle guards, base-image cache behavior, hosted manifest validation, VM tracking, ephemeral drive staging, and subnet/IP behavior.
|
|
|
|
|
|
|
|
|
|
Real Firecracker boot testing should be run on a Linux/KVM host with the runtime requirements above. At minimum, verify `ensureBinary()`, `createVM()`, `start()`, `getInfo()`, `stop()`, and `cleanup()` against a known-good kernel and rootfs image before relying on a new host setup.
|
|
|
|
|
|
|
|
|
|
An opt-in integration test scaffold is included and skipped by default:
|
|
|
|
|
Opt into real Firecracker boot tests on a Linux/KVM host:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
SMARTVM_RUN_INTEGRATION=true pnpm test
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Accepted truthy values for `SMARTVM_RUN_INTEGRATION`: `1`, `true`, `yes`.
|
|
|
|
|
|
|
|
|
|
Useful integration-test environment variables:
|
|
|
|
|
|
|
|
|
|
- `SMARTVM_BASE_IMAGE_PRESET`: `latest` or `lts` (default: `latest`)
|
|
|
|
|
- `SMARTVM_BASE_IMAGE_MANIFEST_URL`: use a hosted/project-owned base image manifest instead of a preset
|
|
|
|
|
- `SMARTVM_BASE_IMAGE_MANIFEST_PATH`: use a local base image manifest instead of a preset
|
|
|
|
|
- `SMARTVM_BASE_IMAGE_CACHE_DIR`: override `/tmp/.smartvm/base-images`
|
|
|
|
|
- `SMARTVM_MAX_STORED_BASE_IMAGES`: override the default retention limit of `2`
|
|
|
|
|
- `SMARTVM_FIRECRACKER_VERSION`: override the Firecracker binary version; otherwise the base image's recommended version is used
|
|
|
|
|
- `SMARTVM_ARCH`: `x86_64` or `aarch64`; defaults from the host architecture
|
|
|
|
|
- `SMARTVM_INTEGRATION_DATA_DIR`: override the Firecracker binary/socket data directory
|
|
|
|
|
| Variable | Purpose |
|
|
|
|
|
|---|---|
|
|
|
|
|
| `SMARTVM_BASE_IMAGE_PRESET` | `latest` or `lts`; default is `latest`. |
|
|
|
|
|
| `SMARTVM_BASE_IMAGE_MANIFEST_URL` | Hosted/project-owned base-image manifest URL. |
|
|
|
|
|
| `SMARTVM_BASE_IMAGE_MANIFEST_PATH` | Local hosted manifest path. |
|
|
|
|
|
| `SMARTVM_BASE_IMAGE_CACHE_DIR` | Override `/tmp/.smartvm/base-images`. |
|
|
|
|
|
| `SMARTVM_MAX_STORED_BASE_IMAGES` | Override default retention of `2`. |
|
|
|
|
|
| `SMARTVM_FIRECRACKER_VERSION` | Override the Firecracker binary version. |
|
|
|
|
|
| `SMARTVM_ARCH` | `x86_64` or `aarch64`; defaults from host architecture. |
|
|
|
|
|
| `SMARTVM_INTEGRATION_DATA_DIR` | Override the Firecracker binary data directory used by integration tests. |
|
|
|
|
|
|
|
|
|
|
## TypeScript Interfaces
|
|
|
|
|
## TypeScript Surface
|
|
|
|
|
|
|
|
|
|
All configuration interfaces are fully exported for type-safe usage:
|
|
|
|
|
Main exports:
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
export {
|
|
|
|
|
SmartVM,
|
|
|
|
|
MicroVM,
|
|
|
|
|
NetworkManager,
|
|
|
|
|
FirecrackerProcess,
|
|
|
|
|
BaseImageManager,
|
|
|
|
|
ImageManager,
|
|
|
|
|
SocketClient,
|
|
|
|
|
VMConfig,
|
|
|
|
|
};
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Important exported types:
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
import type {
|
|
|
|
|
ISmartVMOptions,
|
|
|
|
|
IMicroVMRuntimeOptions,
|
|
|
|
|
IMicroVMConfig,
|
|
|
|
|
IBootSource,
|
|
|
|
|
IMachineConfig,
|
|
|
|
|