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.
Files changed (32) hide show
  1. package/.env.example +7 -0
  2. package/package.json +28 -0
  3. package/scripts/generate-firecracker-client.ts +12 -0
  4. package/src/api/config/VmmNodeManagerConfig.ts +54 -0
  5. package/src/api/domain/DriveConfig.ts +6 -0
  6. package/src/api/domain/KinoticMicrovmLaunchResult.ts +7 -0
  7. package/src/api/domain/MachineConfig.ts +4 -0
  8. package/src/api/domain/MmdsConfig.ts +6 -0
  9. package/src/api/domain/NetworkInterfaceConfig.ts +5 -0
  10. package/src/api/domain/VmmConfig.ts +19 -0
  11. package/src/api/domain/VmmType.ts +4 -0
  12. package/src/api/services/KinoticMicrovmLauncherService.ts +118 -0
  13. package/src/api/services/VmmManagerService.ts +205 -0
  14. package/src/index.ts +14 -0
  15. package/src/internal/api/firecracker/generated/client/client.gen.ts +305 -0
  16. package/src/internal/api/firecracker/generated/client/index.ts +25 -0
  17. package/src/internal/api/firecracker/generated/client/types.gen.ts +241 -0
  18. package/src/internal/api/firecracker/generated/client/utils.gen.ts +332 -0
  19. package/src/internal/api/firecracker/generated/client.gen.ts +16 -0
  20. package/src/internal/api/firecracker/generated/core/auth.gen.ts +42 -0
  21. package/src/internal/api/firecracker/generated/core/bodySerializer.gen.ts +100 -0
  22. package/src/internal/api/firecracker/generated/core/params.gen.ts +176 -0
  23. package/src/internal/api/firecracker/generated/core/pathSerializer.gen.ts +181 -0
  24. package/src/internal/api/firecracker/generated/core/queryKeySerializer.gen.ts +136 -0
  25. package/src/internal/api/firecracker/generated/core/serverSentEvents.gen.ts +266 -0
  26. package/src/internal/api/firecracker/generated/core/types.gen.ts +118 -0
  27. package/src/internal/api/firecracker/generated/core/utils.gen.ts +143 -0
  28. package/src/internal/api/firecracker/generated/index.ts +4 -0
  29. package/src/internal/api/firecracker/generated/sdk.gen.ts +441 -0
  30. package/src/internal/api/firecracker/generated/types.gen.ts +1811 -0
  31. package/src/internal/api/services/BootstrapService.ts +41 -0
  32. 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,6 @@
1
+ export class DriveConfig {
2
+ public driveId!: string;
3
+ public pathOnHost!: string;
4
+ public isRootDevice!: boolean;
5
+ public isReadOnly!: boolean;
6
+ }
@@ -0,0 +1,7 @@
1
+ export interface KinoticMicrovmLaunchResult {
2
+ vmIndex: number;
3
+ tapIp: string;
4
+ guestIp: string;
5
+ overlayPath: string;
6
+ tapName: string;
7
+ }
@@ -0,0 +1,4 @@
1
+ export class MachineConfig {
2
+ public vcpuCount!: number;
3
+ public memSizeMib!: number;
4
+ }
@@ -0,0 +1,6 @@
1
+ export class MmdsConfig {
2
+ public networkInterfaces!: string[];
3
+ public data!: Record<string, unknown>;
4
+ public ipv4Address?: string;
5
+ public imdsCompat?: boolean;
6
+ }
@@ -0,0 +1,5 @@
1
+ export class NetworkInterfaceConfig {
2
+ public ifaceId!: string;
3
+ public guestMac?: string;
4
+ public hostDevName!: string;
5
+ }
@@ -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,4 @@
1
+ export enum VmmType{
2
+ FIRECRACKER = 'FIRECRACKER',
3
+ CLOUD_HYPERVISOR = 'CLOUD_HYPERVISOR',
4
+ }
@@ -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.')