vmm-node-manager 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +7 -0
- package/package.json +28 -0
- package/scripts/generate-firecracker-client.ts +12 -0
- package/src/api/config/VmmNodeManagerConfig.ts +54 -0
- package/src/api/domain/DriveConfig.ts +6 -0
- package/src/api/domain/KinoticMicrovmLaunchResult.ts +7 -0
- package/src/api/domain/MachineConfig.ts +4 -0
- package/src/api/domain/MmdsConfig.ts +6 -0
- package/src/api/domain/NetworkInterfaceConfig.ts +5 -0
- package/src/api/domain/VmmConfig.ts +19 -0
- package/src/api/domain/VmmType.ts +4 -0
- package/src/api/services/KinoticMicrovmLauncherService.ts +118 -0
- package/src/api/services/VmmManagerService.ts +205 -0
- package/src/index.ts +14 -0
- package/src/internal/api/firecracker/generated/client/client.gen.ts +305 -0
- package/src/internal/api/firecracker/generated/client/index.ts +25 -0
- package/src/internal/api/firecracker/generated/client/types.gen.ts +241 -0
- package/src/internal/api/firecracker/generated/client/utils.gen.ts +332 -0
- package/src/internal/api/firecracker/generated/client.gen.ts +16 -0
- package/src/internal/api/firecracker/generated/core/auth.gen.ts +42 -0
- package/src/internal/api/firecracker/generated/core/bodySerializer.gen.ts +100 -0
- package/src/internal/api/firecracker/generated/core/params.gen.ts +176 -0
- package/src/internal/api/firecracker/generated/core/pathSerializer.gen.ts +181 -0
- package/src/internal/api/firecracker/generated/core/queryKeySerializer.gen.ts +136 -0
- package/src/internal/api/firecracker/generated/core/serverSentEvents.gen.ts +266 -0
- package/src/internal/api/firecracker/generated/core/types.gen.ts +118 -0
- package/src/internal/api/firecracker/generated/core/utils.gen.ts +143 -0
- package/src/internal/api/firecracker/generated/index.ts +4 -0
- package/src/internal/api/firecracker/generated/sdk.gen.ts +441 -0
- package/src/internal/api/firecracker/generated/types.gen.ts +1811 -0
- package/src/internal/api/services/BootstrapService.ts +41 -0
- package/tsconfig.json +11 -0
package/.env.example
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# VmmNodeManagerConfig – Bun loads .env natively. Copy to .env and set paths.
|
|
2
|
+
# For custom file: bun --env-file=.env.custom run src/index.ts
|
|
3
|
+
|
|
4
|
+
VMM_KERNEL_IMAGE_PATH=./build/vmlinux
|
|
5
|
+
VMM_ROOTFS_PATH=./build/alpine-rootfs.img
|
|
6
|
+
VMM_MASTER_OVERLAY_PATH=./build/alpine-overlay.ext4
|
|
7
|
+
VMM_OVERLAY_OUTPUT_DIR=./build/overlays
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vmm-node-manager",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "VMM Manager standalone application",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"start": "bun run src/index.ts",
|
|
9
|
+
"dev": "DEBUG=continuum:* bun run --watch src/index.ts",
|
|
10
|
+
"type-check": "tsc --noEmit",
|
|
11
|
+
"generate:firecracker-client": "bun run scripts/generate-firecracker-client.ts"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@mindignited/continuum-client": "3.0.0-beta.2",
|
|
15
|
+
"zod": "^3.24.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@hey-api/openapi-ts": "^0.90.3"
|
|
19
|
+
},
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"typescript": ">=4.5.0"
|
|
22
|
+
},
|
|
23
|
+
"peerDependenciesMeta": {
|
|
24
|
+
"typescript": {
|
|
25
|
+
"optional": true
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createClient } from '@hey-api/openapi-ts';
|
|
2
|
+
|
|
3
|
+
const FIRECRACKER_OPENAPI_URL = 'https://raw.githubusercontent.com/firecracker-microvm/firecracker/main/src/firecracker/swagger/firecracker.yaml';
|
|
4
|
+
const OUTPUT_DIR = 'src/internal/api/firecracker/generated';
|
|
5
|
+
|
|
6
|
+
await createClient({
|
|
7
|
+
input: FIRECRACKER_OPENAPI_URL,
|
|
8
|
+
output: OUTPUT_DIR,
|
|
9
|
+
plugins: ['@hey-api/client-fetch'],
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
console.log(`Firecracker API client generated successfully in ${OUTPUT_DIR}`);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { z, type ZodIssue } from 'zod'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Global config for the vmm-node-manager. Loaded once at process start from environment via Bun's native .env.
|
|
5
|
+
* Extensible for other vmm-node-manager settings later.
|
|
6
|
+
*
|
|
7
|
+
* Env vars: VMM_CONTINUUM_HOST, VMM_KERNEL_IMAGE_PATH, VMM_ROOTFS_PATH, VMM_MASTER_OVERLAY_PATH, VMM_OVERLAY_OUTPUT_DIR
|
|
8
|
+
* Bun loads .env, .env.local, .env.{NODE_ENV} automatically. For custom: bun --env-file=.env.custom run ...
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const VmmNodeManagerConfigSchema = z.object({
|
|
12
|
+
continuumHost: z
|
|
13
|
+
.string({ required_error: 'VMM_CONTINUUM_HOST must be set' })
|
|
14
|
+
.min(1, 'VMM_CONTINUUM_HOST must be non-empty'),
|
|
15
|
+
kernelImagePath: z
|
|
16
|
+
.string({ required_error: 'VMM_KERNEL_IMAGE_PATH must be set' })
|
|
17
|
+
.min(1, 'VMM_KERNEL_IMAGE_PATH must be non-empty'),
|
|
18
|
+
rootfsPath: z
|
|
19
|
+
.string({ required_error: 'VMM_ROOTFS_PATH must be set' })
|
|
20
|
+
.min(1, 'VMM_ROOTFS_PATH must be non-empty'),
|
|
21
|
+
masterOverlayPath: z
|
|
22
|
+
.string({ required_error: 'VMM_MASTER_OVERLAY_PATH must be set' })
|
|
23
|
+
.min(1, 'VMM_MASTER_OVERLAY_PATH must be non-empty'),
|
|
24
|
+
overlayOutputDir: z
|
|
25
|
+
.string({ required_error: 'VMM_OVERLAY_OUTPUT_DIR must be set' })
|
|
26
|
+
.min(1, 'VMM_OVERLAY_OUTPUT_DIR must be non-empty'),
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
export type VmmNodeManagerConfig = z.infer<typeof VmmNodeManagerConfigSchema>
|
|
30
|
+
|
|
31
|
+
function loadVmmNodeManagerConfig(): VmmNodeManagerConfig {
|
|
32
|
+
const env = typeof Bun !== 'undefined' ? Bun.env : process.env
|
|
33
|
+
const raw = {
|
|
34
|
+
continuumHost: env.VMM_CONTINUUM_HOST,
|
|
35
|
+
kernelImagePath: env.VMM_KERNEL_IMAGE_PATH,
|
|
36
|
+
rootfsPath: env.VMM_ROOTFS_PATH,
|
|
37
|
+
masterOverlayPath: env.VMM_MASTER_OVERLAY_PATH,
|
|
38
|
+
overlayOutputDir: env.VMM_OVERLAY_OUTPUT_DIR,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const result = VmmNodeManagerConfigSchema.safeParse(raw)
|
|
42
|
+
if (!result.success) {
|
|
43
|
+
const msg = result.error.issues
|
|
44
|
+
.map((i: ZodIssue) => ` - ${i.path.join('.')}: ${i.message}`)
|
|
45
|
+
.join('\n')
|
|
46
|
+
throw new Error(
|
|
47
|
+
`VmmNodeManagerConfig: validation failed:\n${msg}\nSet them in .env or the environment. See .env.example.`,
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
return result.data
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Loaded once when this module is first imported. Throws if required env vars are missing. */
|
|
54
|
+
export const vmmNodeManagerConfig: VmmNodeManagerConfig = loadVmmNodeManagerConfig()
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type {VmmType} from './VmmType.js'
|
|
2
|
+
import type {DriveConfig} from './DriveConfig.js'
|
|
3
|
+
import type {NetworkInterfaceConfig} from './NetworkInterfaceConfig.js'
|
|
4
|
+
import type {MachineConfig} from './MachineConfig.js'
|
|
5
|
+
import type {MmdsConfig} from './MmdsConfig.js'
|
|
6
|
+
|
|
7
|
+
export class VmmConfig {
|
|
8
|
+
|
|
9
|
+
/** Used to derive the API socket path: /tmp/firecracker-{id}.socket */
|
|
10
|
+
public id!: string | number;
|
|
11
|
+
public vmmType!: VmmType;
|
|
12
|
+
public kernelImagePath!: string;
|
|
13
|
+
public bootArgs?: string;
|
|
14
|
+
public drives!: DriveConfig[];
|
|
15
|
+
public networkInterfaces!: NetworkInterfaceConfig[];
|
|
16
|
+
public machineConfig!: MachineConfig;
|
|
17
|
+
public mmdsConfig?: MmdsConfig;
|
|
18
|
+
|
|
19
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import {copyFileSync, mkdirSync} from 'node:fs'
|
|
2
|
+
import {join} from 'node:path'
|
|
3
|
+
import {vmmNodeManagerConfig} from '../config/VmmNodeManagerConfig.js'
|
|
4
|
+
import type {KinoticMicrovmLaunchResult} from '../domain/KinoticMicrovmLaunchResult.js'
|
|
5
|
+
import {VmmType} from '../domain/VmmType.js'
|
|
6
|
+
import type {VmmConfig} from '../domain/VmmConfig.js'
|
|
7
|
+
import type {VmmManagerService} from './VmmManagerService.js'
|
|
8
|
+
import {Publish} from '@mindignited/continuum-client'
|
|
9
|
+
|
|
10
|
+
const NETWORK_BASE = '172.16'
|
|
11
|
+
const SUBNET_SIZE = 4
|
|
12
|
+
const NETMASK = '255.255.255.252'
|
|
13
|
+
const CIDR = 30
|
|
14
|
+
|
|
15
|
+
@Publish('kinotic.vmm-node-manager')
|
|
16
|
+
export class KinoticMicrovmLauncherService {
|
|
17
|
+
private readonly paths = vmmNodeManagerConfig
|
|
18
|
+
|
|
19
|
+
constructor(private readonly vmmManager: VmmManagerService) {}
|
|
20
|
+
|
|
21
|
+
async launch(vmIndex: number): Promise<KinoticMicrovmLaunchResult> {
|
|
22
|
+
const tapName = `tap${vmIndex}`
|
|
23
|
+
const {tapIp, guestIp} = this.computeIps(SUBNET_SIZE, vmIndex)
|
|
24
|
+
|
|
25
|
+
// 1. Create tap
|
|
26
|
+
await this.execIpCommand(['tuntap', 'add', tapName, 'mode', 'tap'])
|
|
27
|
+
await this.execIpCommand(['addr', 'add', `${tapIp}/${CIDR}`, 'dev', tapName])
|
|
28
|
+
await this.execIpCommand(['link', 'set', tapName, 'up'])
|
|
29
|
+
|
|
30
|
+
// 2. Copy overlay
|
|
31
|
+
mkdirSync(this.paths.overlayOutputDir, {recursive: true})
|
|
32
|
+
const overlayPath = join(this.paths.overlayOutputDir, `overlay-${vmIndex}.ext4`)
|
|
33
|
+
const masterFile = Bun.file(this.paths.masterOverlayPath)
|
|
34
|
+
if (!(await masterFile.exists())) {
|
|
35
|
+
throw new Error(`Master overlay not found at: ${this.paths.masterOverlayPath}`)
|
|
36
|
+
}
|
|
37
|
+
copyFileSync(this.paths.masterOverlayPath, overlayPath)
|
|
38
|
+
|
|
39
|
+
// 3. Build bootArgs
|
|
40
|
+
const bootArgs = `console=ttyS0 reboot=k panic=1 pci=off overlay_root=vdb init=/sbin/overlay-init.sh ip=${guestIp}::${tapIp}:${NETMASK}::eth0:off loglevel=7 earlyprintk=serial`
|
|
41
|
+
|
|
42
|
+
// 4. Build VmmConfig
|
|
43
|
+
const vmmConfig: VmmConfig = {
|
|
44
|
+
id: vmIndex,
|
|
45
|
+
vmmType: VmmType.FIRECRACKER,
|
|
46
|
+
kernelImagePath: this.paths.kernelImagePath,
|
|
47
|
+
bootArgs,
|
|
48
|
+
drives: [
|
|
49
|
+
{
|
|
50
|
+
driveId: 'rootfs',
|
|
51
|
+
pathOnHost: this.paths.rootfsPath,
|
|
52
|
+
isRootDevice: true,
|
|
53
|
+
isReadOnly: true,
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
driveId: 'overlayfs',
|
|
57
|
+
pathOnHost: overlayPath,
|
|
58
|
+
isRootDevice: false,
|
|
59
|
+
isReadOnly: false,
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
networkInterfaces: [{ifaceId: 'eth0', hostDevName: tapName}],
|
|
63
|
+
machineConfig: {vcpuCount: 1, memSizeMib: 128},
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 5. Launch via VmmManagerService
|
|
67
|
+
await this.vmmManager.launchVmm(vmmConfig)
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
vmIndex,
|
|
71
|
+
tapIp,
|
|
72
|
+
guestIp,
|
|
73
|
+
overlayPath,
|
|
74
|
+
tapName,
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Stops the VM (sends SendCtrlAltDel to request shutdown) and removes the tap device.
|
|
80
|
+
* Does not delete the overlay file.
|
|
81
|
+
*/
|
|
82
|
+
async stop(vmIndex: number): Promise<void> {
|
|
83
|
+
await this.vmmManager.stopVmm(vmIndex)
|
|
84
|
+
await this.execIpCommand(['link', 'delete', `tap${vmIndex}`])
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Sequential subnet allocation for tap and guest IPs. Each VM gets a /30 (4 addresses):
|
|
89
|
+
* network, tap (host), guest (VM), broadcast. For VM index O (0-based) and A = subnetSize
|
|
90
|
+
* (e.g. 4 for /30), we place this VM's /30 at offset (A*O) in the 172.16.0.0/16 range.
|
|
91
|
+
* Tap = 2nd address (offset A*O+1), guest = 3rd (offset A*O+2). The offset is encoded as
|
|
92
|
+
* 3rd octet = floor(offset/256), 4th = offset%256. Example: O=0 → tap 172.16.0.1, guest 172.16.0.2;
|
|
93
|
+
* O=999 → tap 172.16.15.157, guest 172.16.15.158.
|
|
94
|
+
*/
|
|
95
|
+
private computeIps(subnetSize: number, vmIndex: number): {tapIp: string; guestIp: string} {
|
|
96
|
+
const tapThird = Math.floor((subnetSize * vmIndex + 1) / 256)
|
|
97
|
+
const tapFourth = (subnetSize * vmIndex + 1) % 256
|
|
98
|
+
const guestThird = Math.floor((subnetSize * vmIndex + 2) / 256)
|
|
99
|
+
const guestFourth = (subnetSize * vmIndex + 2) % 256
|
|
100
|
+
return {
|
|
101
|
+
tapIp: `${NETWORK_BASE}.${tapThird}.${tapFourth}`,
|
|
102
|
+
guestIp: `${NETWORK_BASE}.${guestThird}.${guestFourth}`,
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private async execIpCommand(args: string[]): Promise<void> {
|
|
107
|
+
const proc = Bun.spawn({
|
|
108
|
+
cmd: ['ip', ...args],
|
|
109
|
+
stdout: 'pipe',
|
|
110
|
+
stderr: 'pipe',
|
|
111
|
+
})
|
|
112
|
+
const exitCode = await proc.exited
|
|
113
|
+
if (exitCode !== 0) {
|
|
114
|
+
const stderr = await new Response(proc.stderr).text()
|
|
115
|
+
throw new Error(`ip ${args.join(' ')} failed (exit ${exitCode}): ${stderr}`)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import {Publish} from '@mindignited/continuum-client'
|
|
2
|
+
import type {VmmConfig} from '../domain/VmmConfig.js'
|
|
3
|
+
import {VmmType} from '../domain/VmmType.js'
|
|
4
|
+
import {createClient} from '@/internal/api/firecracker/generated/client/index.js'
|
|
5
|
+
import {
|
|
6
|
+
putGuestBootSource,
|
|
7
|
+
putGuestDriveById,
|
|
8
|
+
putGuestNetworkInterfaceById,
|
|
9
|
+
putMachineConfiguration,
|
|
10
|
+
putMmdsConfig,
|
|
11
|
+
putMmds,
|
|
12
|
+
createSyncAction,
|
|
13
|
+
} from '@/internal/api/firecracker/generated/sdk.gen.js'
|
|
14
|
+
|
|
15
|
+
const FIRECRACKER_BIN = 'firecracker'
|
|
16
|
+
|
|
17
|
+
@Publish('kinotic.vmm-node-manager')
|
|
18
|
+
export class VmmManagerService {
|
|
19
|
+
|
|
20
|
+
public constructor() {
|
|
21
|
+
console.log('VmmManagerService constructed')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public async launchVmm(vmmConfig: VmmConfig): Promise<void> {
|
|
25
|
+
// Validate VMM type
|
|
26
|
+
if (vmmConfig.vmmType !== VmmType.FIRECRACKER) {
|
|
27
|
+
throw new Error(`Unsupported VMM type: ${vmmConfig.vmmType}. Only ${VmmType.FIRECRACKER} is supported.`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Validate required fields
|
|
31
|
+
if (vmmConfig.id === undefined || vmmConfig.id === null || vmmConfig.id === '') {
|
|
32
|
+
throw new Error('id is required');
|
|
33
|
+
}
|
|
34
|
+
if (!vmmConfig.kernelImagePath) {
|
|
35
|
+
throw new Error('kernelImagePath is required');
|
|
36
|
+
}
|
|
37
|
+
if (!vmmConfig.drives || vmmConfig.drives.length === 0) {
|
|
38
|
+
throw new Error('At least one drive is required');
|
|
39
|
+
}
|
|
40
|
+
if (!vmmConfig.networkInterfaces || vmmConfig.networkInterfaces.length === 0) {
|
|
41
|
+
throw new Error('At least one network interface is required');
|
|
42
|
+
}
|
|
43
|
+
if (!vmmConfig.machineConfig) {
|
|
44
|
+
throw new Error('machineConfig is required');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Validate file paths exist
|
|
48
|
+
const kernelFile = Bun.file(vmmConfig.kernelImagePath);
|
|
49
|
+
if (!(await kernelFile.exists())) {
|
|
50
|
+
throw new Error(`Kernel image not found at: ${vmmConfig.kernelImagePath}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
for (const drive of vmmConfig.drives) {
|
|
54
|
+
const driveFile = Bun.file(drive.pathOnHost);
|
|
55
|
+
if (!(await driveFile.exists())) {
|
|
56
|
+
throw new Error(`Drive file not found at: ${drive.pathOnHost}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const socketPath = this.socketPathFor(vmmConfig.id);
|
|
61
|
+
|
|
62
|
+
// Spawn Firecracker as a detached background process; it stays running after vmm-node-manager exits.
|
|
63
|
+
Bun.spawn({
|
|
64
|
+
cmd: [FIRECRACKER_BIN],
|
|
65
|
+
env: { ...process.env, API_SOCKET: socketPath },
|
|
66
|
+
detached: true,
|
|
67
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Wait for the API socket to appear before configuring the VM.
|
|
71
|
+
const socketDeadline = Date.now() + 5000;
|
|
72
|
+
while (!(await Bun.file(socketPath).exists())) {
|
|
73
|
+
if (Date.now() > socketDeadline) {
|
|
74
|
+
throw new Error(`Firecracker socket did not appear at ${socketPath} within 5s`);
|
|
75
|
+
}
|
|
76
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Use a dedicated client for this VM's socket to avoid contention with concurrent launch/stop.
|
|
80
|
+
const fc = createClient({ baseUrl: `http://unix:${socketPath}:` });
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
// 1. Configure boot source
|
|
84
|
+
await putGuestBootSource({
|
|
85
|
+
client: fc,
|
|
86
|
+
body: {
|
|
87
|
+
kernel_image_path: vmmConfig.kernelImagePath,
|
|
88
|
+
boot_args: vmmConfig.bootArgs,
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// 2. Configure drives
|
|
93
|
+
for (const drive of vmmConfig.drives) {
|
|
94
|
+
await putGuestDriveById({
|
|
95
|
+
client: fc,
|
|
96
|
+
path: {
|
|
97
|
+
drive_id: drive.driveId,
|
|
98
|
+
},
|
|
99
|
+
body: {
|
|
100
|
+
drive_id: drive.driveId,
|
|
101
|
+
path_on_host: drive.pathOnHost,
|
|
102
|
+
is_root_device: drive.isRootDevice,
|
|
103
|
+
is_read_only: drive.isReadOnly,
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 3. Configure network interfaces
|
|
109
|
+
for (const iface of vmmConfig.networkInterfaces) {
|
|
110
|
+
await putGuestNetworkInterfaceById({
|
|
111
|
+
client: fc,
|
|
112
|
+
path: {
|
|
113
|
+
iface_id: iface.ifaceId,
|
|
114
|
+
},
|
|
115
|
+
body: {
|
|
116
|
+
iface_id: iface.ifaceId,
|
|
117
|
+
guest_mac: iface.guestMac,
|
|
118
|
+
host_dev_name: iface.hostDevName,
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 4. Configure machine config
|
|
124
|
+
await putMachineConfiguration({
|
|
125
|
+
client: fc,
|
|
126
|
+
body: {
|
|
127
|
+
vcpu_count: vmmConfig.machineConfig.vcpuCount,
|
|
128
|
+
mem_size_mib: vmmConfig.machineConfig.memSizeMib,
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// 5. Configure MMDS if provided
|
|
133
|
+
if (vmmConfig.mmdsConfig) {
|
|
134
|
+
await putMmdsConfig({
|
|
135
|
+
client: fc,
|
|
136
|
+
body: {
|
|
137
|
+
version: 'V2', // Always use V2, V1 is deprecated
|
|
138
|
+
network_interfaces: vmmConfig.mmdsConfig.networkInterfaces,
|
|
139
|
+
ipv4_address: vmmConfig.mmdsConfig.ipv4Address,
|
|
140
|
+
imds_compat: vmmConfig.mmdsConfig.imdsCompat,
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Set MMDS data
|
|
145
|
+
await putMmds({
|
|
146
|
+
client: fc,
|
|
147
|
+
body: vmmConfig.mmdsConfig.data,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 6. Start VM
|
|
152
|
+
await createSyncAction({
|
|
153
|
+
client: fc,
|
|
154
|
+
body: {
|
|
155
|
+
action_type: 'InstanceStart',
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
} catch (error) {
|
|
159
|
+
// Provide more context about which step failed
|
|
160
|
+
let stepContext = '';
|
|
161
|
+
if (error instanceof Error) {
|
|
162
|
+
const message = error.message.toLowerCase();
|
|
163
|
+
if (message.includes('boot') || message.includes('kernel')) {
|
|
164
|
+
stepContext = ' (boot source configuration)';
|
|
165
|
+
} else if (message.includes('drive')) {
|
|
166
|
+
stepContext = ' (drive configuration)';
|
|
167
|
+
} else if (message.includes('network') || message.includes('interface')) {
|
|
168
|
+
stepContext = ' (network interface configuration)';
|
|
169
|
+
} else if (message.includes('machine') || message.includes('config')) {
|
|
170
|
+
stepContext = ' (machine configuration)';
|
|
171
|
+
} else if (message.includes('mmds')) {
|
|
172
|
+
stepContext = ' (MMDS configuration)';
|
|
173
|
+
} else if (message.includes('action') || message.includes('start')) {
|
|
174
|
+
stepContext = ' (VM start)';
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
179
|
+
throw new Error(`Failed to launch Firecracker VM${stepContext}: ${errorMessage}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Sends SendCtrlAltDel to the VM to request a graceful shutdown.
|
|
185
|
+
* The socket path is derived from the id: /tmp/firecracker-{id}.socket.
|
|
186
|
+
*/
|
|
187
|
+
public async stopVmm(id: string | number): Promise<void> {
|
|
188
|
+
const sp = this.socketPathFor(id);
|
|
189
|
+
if (!(await Bun.file(sp).exists())) {
|
|
190
|
+
throw new Error(`Firecracker socket not found at ${sp}. Is the VM running?`);
|
|
191
|
+
}
|
|
192
|
+
const fc = createClient({ baseUrl: `http://unix:${sp}:` });
|
|
193
|
+
await createSyncAction({
|
|
194
|
+
client: fc,
|
|
195
|
+
body: {
|
|
196
|
+
action_type: 'SendCtrlAltDel',
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private socketPathFor(id: string | number): string {
|
|
202
|
+
return `/tmp/firecracker-${id}.socket`
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import 'reflect-metadata'
|
|
2
|
+
import {BootstrapService} from './internal/api/services/BootstrapService.js'
|
|
3
|
+
|
|
4
|
+
export {vmmNodeManagerConfig, type VmmNodeManagerConfig} from './api/config/VmmNodeManagerConfig.js'
|
|
5
|
+
export type {KinoticMicrovmLaunchResult} from './api/domain/KinoticMicrovmLaunchResult.js'
|
|
6
|
+
export {KinoticMicrovmLauncherService} from './api/services/KinoticMicrovmLauncherService.js'
|
|
7
|
+
|
|
8
|
+
const bootstrapService = new BootstrapService()
|
|
9
|
+
|
|
10
|
+
console.log('Starting VMM Node Manager bootstrap process...')
|
|
11
|
+
|
|
12
|
+
await bootstrapService.bootstrap()
|
|
13
|
+
|
|
14
|
+
console.log('VMM Node Manager bootstrap process completed.')
|