feat(base-images): add managed base image bundles with cache retention, hosted manifests, and opt-in integration boot testing
This commit is contained in:
+467
-1
@@ -1,11 +1,31 @@
|
||||
import * as fs from 'fs';
|
||||
import * as crypto from 'crypto';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import {
|
||||
BaseImageManager,
|
||||
VMConfig,
|
||||
SocketClient,
|
||||
NetworkManager,
|
||||
MicroVM,
|
||||
SmartVM,
|
||||
SmartVMError,
|
||||
} from '../ts/index.js';
|
||||
import type { IMicroVMConfig } from '../ts/index.js';
|
||||
import type { IBaseImageBundle, IBaseImageHostedManifest, IMicroVMConfig } from '../ts/index.js';
|
||||
|
||||
async function getRejectedError(promise: Promise<unknown>): Promise<unknown> {
|
||||
try {
|
||||
await promise;
|
||||
} catch (err) {
|
||||
return err;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function sha256Buffer(buffer: Buffer): string {
|
||||
return crypto.createHash('sha256').update(buffer).digest('hex');
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// VMConfig Tests
|
||||
@@ -87,6 +107,33 @@ tap.test('VMConfig - validate() should fail for multiple root drives', async ()
|
||||
expect(result.valid).toBeFalse();
|
||||
});
|
||||
|
||||
tap.test('VMConfig - validate() should fail for invalid vsock guestCid', async () => {
|
||||
const vmConfig = new VMConfig({
|
||||
bootSource: { kernelImagePath: '/vmlinux' },
|
||||
machineConfig: { vcpuCount: 1, memSizeMib: 128 },
|
||||
vsock: { guestCid: 2, udsPath: '/tmp/vsock.sock' },
|
||||
});
|
||||
const result = vmConfig.validate();
|
||||
expect(result.valid).toBeFalse();
|
||||
expect(result.errors).toContain('vsock.guestCid must be >= 3');
|
||||
});
|
||||
|
||||
tap.test('VMConfig - constructor should not retain caller references', async () => {
|
||||
const config: IMicroVMConfig = {
|
||||
...sampleConfig,
|
||||
networkInterfaces: [{ ifaceId: 'eth0', guestMac: '02:00:00:00:00:01' }],
|
||||
mmds: { version: 'V2', networkInterfaces: ['eth0'] },
|
||||
};
|
||||
const vmConfig = new VMConfig(config);
|
||||
|
||||
config.networkInterfaces![0].guestMac = '02:00:00:00:00:02';
|
||||
config.mmds!.networkInterfaces.push('eth1');
|
||||
|
||||
expect(vmConfig.toNetworkInterfacePayload(vmConfig.config.networkInterfaces![0]).guest_mac)
|
||||
.toEqual('02:00:00:00:00:01');
|
||||
expect(vmConfig.toMmdsConfigPayload()!.network_interfaces).toEqual(['eth0']);
|
||||
});
|
||||
|
||||
tap.test('VMConfig - toBootSourcePayload() should generate correct snake_case', async () => {
|
||||
const vmConfig = new VMConfig(sampleConfig);
|
||||
const payload = vmConfig.toBootSourcePayload();
|
||||
@@ -144,6 +191,65 @@ tap.test('VMConfig - toBalloonPayload() should generate correct payload', async
|
||||
expect(payload!.stats_polling_interval_s).toEqual(5);
|
||||
});
|
||||
|
||||
tap.test('VMConfig - toVsockPayload() should generate correct payload', async () => {
|
||||
const config: IMicroVMConfig = {
|
||||
...sampleConfig,
|
||||
vsock: { guestCid: 3, udsPath: '/tmp/vsock.sock' },
|
||||
};
|
||||
const vmConfig = new VMConfig(config);
|
||||
const payload = vmConfig.toVsockPayload();
|
||||
expect(payload).not.toBeNull();
|
||||
expect(payload!.guest_cid).toEqual(3);
|
||||
expect(payload!.uds_path).toEqual('/tmp/vsock.sock');
|
||||
});
|
||||
|
||||
tap.test('VMConfig - toMmdsConfigPayload() should generate correct payload', async () => {
|
||||
const config: IMicroVMConfig = {
|
||||
...sampleConfig,
|
||||
mmds: { version: 'V2', networkInterfaces: ['eth0'] },
|
||||
};
|
||||
const vmConfig = new VMConfig(config);
|
||||
const payload = vmConfig.toMmdsConfigPayload();
|
||||
expect(payload).not.toBeNull();
|
||||
expect(payload!.version).toEqual('V2');
|
||||
expect(payload!.network_interfaces).toEqual(['eth0']);
|
||||
});
|
||||
|
||||
tap.test('VMConfig - toMetricsPayload() should generate correct payload', async () => {
|
||||
const config: IMicroVMConfig = {
|
||||
...sampleConfig,
|
||||
metrics: { metricsPath: '/tmp/firecracker-metrics.fifo' },
|
||||
};
|
||||
const vmConfig = new VMConfig(config);
|
||||
const payload = vmConfig.toMetricsPayload();
|
||||
expect(payload).not.toBeNull();
|
||||
expect(payload!.metrics_path).toEqual('/tmp/firecracker-metrics.fifo');
|
||||
});
|
||||
|
||||
tap.test('VMConfig - toDrivePayload() should include rate limiter payloads', async () => {
|
||||
const vmConfig = new VMConfig(sampleConfig);
|
||||
const payload = vmConfig.toDrivePayload({
|
||||
driveId: 'data',
|
||||
pathOnHost: '/path/to/data.ext4',
|
||||
isRootDevice: false,
|
||||
rateLimiter: {
|
||||
bandwidth: { size: 1000, refillTime: 2000, oneTimeBurst: 3000 },
|
||||
ops: { size: 10, refillTime: 20, oneTimeBurst: 30 },
|
||||
},
|
||||
});
|
||||
|
||||
expect(payload.rate_limiter.bandwidth).toEqual({
|
||||
size: 1000,
|
||||
refill_time: 2000,
|
||||
one_time_burst: 3000,
|
||||
});
|
||||
expect(payload.rate_limiter.ops).toEqual({
|
||||
size: 10,
|
||||
refill_time: 20,
|
||||
one_time_burst: 30,
|
||||
});
|
||||
});
|
||||
|
||||
tap.test('VMConfig - toLoggerPayload() should generate correct payload', async () => {
|
||||
const config: IMicroVMConfig = {
|
||||
...sampleConfig,
|
||||
@@ -167,6 +273,269 @@ tap.test('SocketClient - URL construction', async () => {
|
||||
expect(client).toBeTruthy();
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// BaseImageManager Tests
|
||||
// ============================================================
|
||||
|
||||
tap.test('BaseImageManager - instantiation with defaults', async () => {
|
||||
const manager = new BaseImageManager();
|
||||
expect(manager.getCacheDir()).toEqual(path.join(os.tmpdir(), '.smartvm', 'base-images'));
|
||||
expect(manager.getMaxStoredBaseImages()).toEqual(2);
|
||||
});
|
||||
|
||||
tap.test('BaseImageManager - rejects invalid maxStoredBaseImages', async () => {
|
||||
let error: unknown;
|
||||
try {
|
||||
new BaseImageManager({ maxStoredBaseImages: 0 });
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
|
||||
expect(error).toBeInstanceOf(SmartVMError);
|
||||
expect((error as SmartVMError).code).toEqual('INVALID_BASE_IMAGE_CACHE_LIMIT');
|
||||
});
|
||||
|
||||
tap.test('BaseImageManager - pruneBaseImageCache() should evict old bundles', async () => {
|
||||
const cacheDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'smartvm-base-image-test-'));
|
||||
const manager = new BaseImageManager({ cacheDir, maxStoredBaseImages: 2 });
|
||||
const originalWarn = console.warn;
|
||||
const warnings: string[] = [];
|
||||
console.warn = (message?: any) => {
|
||||
warnings.push(String(message));
|
||||
};
|
||||
|
||||
const createManifest = async (bundleId: string, lastAccessedAt: string) => {
|
||||
const bundleDir = path.join(cacheDir, bundleId);
|
||||
await fs.promises.mkdir(bundleDir, { recursive: true });
|
||||
const bundle: IBaseImageBundle = {
|
||||
preset: 'lts',
|
||||
arch: 'x86_64',
|
||||
ciVersion: 'v1.7',
|
||||
firecrackerVersion: 'v1.7.0',
|
||||
bundleId,
|
||||
bundleDir,
|
||||
kernelImagePath: path.join(bundleDir, 'vmlinux'),
|
||||
rootfsPath: path.join(bundleDir, 'rootfs.ext4'),
|
||||
rootfsType: 'ext4',
|
||||
rootfsIsReadOnly: false,
|
||||
bootArgs: 'console=ttyS0 reboot=k panic=1 pci=off',
|
||||
source: {
|
||||
bucketUrl: 'https://s3.amazonaws.com/spec.ccfc.min',
|
||||
kernelKey: 'kernel',
|
||||
rootfsKey: 'rootfs',
|
||||
},
|
||||
createdAt: lastAccessedAt,
|
||||
lastAccessedAt,
|
||||
};
|
||||
await fs.promises.writeFile(path.join(bundleDir, 'manifest.json'), `${JSON.stringify(bundle, null, 2)}\n`);
|
||||
};
|
||||
|
||||
try {
|
||||
await createManifest('old', '2024-01-01T00:00:00.000Z');
|
||||
await createManifest('middle', '2024-01-02T00:00:00.000Z');
|
||||
await createManifest('new', '2024-01-03T00:00:00.000Z');
|
||||
|
||||
const evicted = await manager.pruneBaseImageCache('new');
|
||||
expect(evicted).toEqual(['old']);
|
||||
expect(warnings.length).toEqual(1);
|
||||
expect(warnings[0]).toInclude('Evicting old');
|
||||
expect(fs.existsSync(path.join(cacheDir, 'old'))).toBeFalse();
|
||||
expect(fs.existsSync(path.join(cacheDir, 'middle'))).toBeTrue();
|
||||
expect(fs.existsSync(path.join(cacheDir, 'new'))).toBeTrue();
|
||||
} finally {
|
||||
console.warn = originalWarn;
|
||||
await fs.promises.rm(cacheDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('BaseImageManager - ensureBaseImage() should copy hosted manifest artifacts', async () => {
|
||||
const workDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'smartvm-hosted-image-test-'));
|
||||
const cacheDir = path.join(workDir, 'cache');
|
||||
const assetsDir = path.join(workDir, 'assets');
|
||||
await fs.promises.mkdir(assetsDir, { recursive: true });
|
||||
|
||||
const kernelBuffer = Buffer.from('fake-kernel');
|
||||
const rootfsBuffer = Buffer.from('fake-rootfs');
|
||||
const kernelPath = path.join(assetsDir, 'vmlinux-test');
|
||||
const rootfsPath = path.join(assetsDir, 'rootfs-test.ext4');
|
||||
await fs.promises.writeFile(kernelPath, kernelBuffer);
|
||||
await fs.promises.writeFile(rootfsPath, rootfsBuffer);
|
||||
|
||||
const manifest: IBaseImageHostedManifest = {
|
||||
schemaVersion: 1,
|
||||
bundleId: 'smartvm-minimal-test',
|
||||
name: 'SmartVM minimal test bundle',
|
||||
arch: 'x86_64',
|
||||
firecrackerVersion: 'v1.15.1',
|
||||
rootfsType: 'ext4',
|
||||
rootfsIsReadOnly: false,
|
||||
bootArgs: 'console=ttyS0 reboot=k panic=1 pci=off',
|
||||
kernel: {
|
||||
path: kernelPath,
|
||||
fileName: 'vmlinux',
|
||||
sha256: sha256Buffer(kernelBuffer),
|
||||
sizeBytes: kernelBuffer.length,
|
||||
},
|
||||
rootfs: {
|
||||
path: rootfsPath,
|
||||
fileName: 'rootfs.ext4',
|
||||
sha256: sha256Buffer(rootfsBuffer),
|
||||
sizeBytes: rootfsBuffer.length,
|
||||
},
|
||||
};
|
||||
const manifestPath = path.join(workDir, 'manifest.json');
|
||||
await fs.promises.writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
||||
|
||||
try {
|
||||
const manager = new BaseImageManager({ cacheDir });
|
||||
const bundle = await manager.ensureBaseImage({ manifestPath });
|
||||
|
||||
expect(bundle.preset).toEqual('hosted');
|
||||
expect(bundle.bundleId).toEqual('smartvm-minimal-test');
|
||||
expect(bundle.firecrackerVersion).toEqual('v1.15.1');
|
||||
expect(bundle.source.type).toEqual('hosted-manifest');
|
||||
expect(bundle.source.manifestPath).toEqual(manifestPath);
|
||||
expect(fs.existsSync(bundle.kernelImagePath)).toBeTrue();
|
||||
expect(fs.existsSync(bundle.rootfsPath)).toBeTrue();
|
||||
expect(bundle.checksums!.kernelSha256).toEqual(sha256Buffer(kernelBuffer));
|
||||
expect(bundle.checksums!.rootfsSha256).toEqual(sha256Buffer(rootfsBuffer));
|
||||
} finally {
|
||||
await fs.promises.rm(workDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('BaseImageManager - ensureBaseImage() should redownload corrupted cached artifacts', async () => {
|
||||
const workDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'smartvm-hosted-image-cache-test-'));
|
||||
const cacheDir = path.join(workDir, 'cache');
|
||||
const assetsDir = path.join(workDir, 'assets');
|
||||
await fs.promises.mkdir(assetsDir, { recursive: true });
|
||||
|
||||
const kernelBuffer = Buffer.from('fresh-kernel');
|
||||
const rootfsBuffer = Buffer.from('fresh-rootfs');
|
||||
const kernelPath = path.join(assetsDir, 'vmlinux-test');
|
||||
const rootfsPath = path.join(assetsDir, 'rootfs-test.ext4');
|
||||
await fs.promises.writeFile(kernelPath, kernelBuffer);
|
||||
await fs.promises.writeFile(rootfsPath, rootfsBuffer);
|
||||
|
||||
const manifest: IBaseImageHostedManifest = {
|
||||
schemaVersion: 1,
|
||||
bundleId: 'smartvm-corruption-test',
|
||||
arch: 'x86_64',
|
||||
firecrackerVersion: 'v1.15.1',
|
||||
rootfsType: 'ext4',
|
||||
kernel: {
|
||||
path: kernelPath,
|
||||
fileName: 'vmlinux',
|
||||
sha256: sha256Buffer(kernelBuffer),
|
||||
sizeBytes: kernelBuffer.length,
|
||||
},
|
||||
rootfs: {
|
||||
path: rootfsPath,
|
||||
fileName: 'rootfs.ext4',
|
||||
sha256: sha256Buffer(rootfsBuffer),
|
||||
sizeBytes: rootfsBuffer.length,
|
||||
},
|
||||
};
|
||||
const manifestPath = path.join(workDir, 'manifest.json');
|
||||
await fs.promises.writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
||||
|
||||
try {
|
||||
const manager = new BaseImageManager({ cacheDir });
|
||||
const firstBundle = await manager.ensureBaseImage({ manifestPath });
|
||||
await fs.promises.writeFile(firstBundle.kernelImagePath, 'tampered-kernel');
|
||||
|
||||
const secondBundle = await manager.ensureBaseImage({ manifestPath });
|
||||
expect(await fs.promises.readFile(secondBundle.kernelImagePath, 'utf8')).toEqual('fresh-kernel');
|
||||
expect(secondBundle.checksums!.kernelSha256).toEqual(sha256Buffer(kernelBuffer));
|
||||
} finally {
|
||||
await fs.promises.rm(workDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('BaseImageManager - ensureBaseImage() should reject hosted manifest arch mismatch', async () => {
|
||||
const workDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'smartvm-hosted-image-invalid-test-'));
|
||||
const manifestPath = path.join(workDir, 'manifest.json');
|
||||
const manifest: IBaseImageHostedManifest = {
|
||||
schemaVersion: 1,
|
||||
bundleId: 'smartvm-invalid-arch-test',
|
||||
arch: 'aarch64',
|
||||
firecrackerVersion: 'v1.15.1',
|
||||
rootfsType: 'ext4',
|
||||
kernel: { path: path.join(workDir, 'vmlinux') },
|
||||
rootfs: { path: path.join(workDir, 'rootfs.ext4') },
|
||||
};
|
||||
await fs.promises.writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
||||
|
||||
try {
|
||||
const manager = new BaseImageManager({ cacheDir: path.join(workDir, 'cache'), arch: 'x86_64' });
|
||||
const error = await getRejectedError(manager.ensureBaseImage({ manifestPath }));
|
||||
expect(error).toBeInstanceOf(SmartVMError);
|
||||
expect((error as SmartVMError).code).toEqual('INVALID_BASE_IMAGE_MANIFEST');
|
||||
} finally {
|
||||
await fs.promises.rm(workDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('BaseImageManager - ensureBaseImage() should reject hosted manifest fileName traversal', async () => {
|
||||
const workDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'smartvm-hosted-image-filename-test-'));
|
||||
const manifestPath = path.join(workDir, 'manifest.json');
|
||||
const manifest: IBaseImageHostedManifest = {
|
||||
schemaVersion: 1,
|
||||
bundleId: 'smartvm-invalid-filename-test',
|
||||
arch: 'x86_64',
|
||||
firecrackerVersion: 'v1.15.1',
|
||||
rootfsType: 'ext4',
|
||||
kernel: { path: path.join(workDir, 'vmlinux'), fileName: '../vmlinux' },
|
||||
rootfs: { path: path.join(workDir, 'rootfs.ext4'), fileName: 'rootfs.ext4' },
|
||||
};
|
||||
await fs.promises.writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
||||
|
||||
try {
|
||||
const manager = new BaseImageManager({ cacheDir: path.join(workDir, 'cache') });
|
||||
const error = await getRejectedError(manager.ensureBaseImage({ manifestPath }));
|
||||
expect(error).toBeInstanceOf(SmartVMError);
|
||||
expect((error as SmartVMError).code).toEqual('INVALID_BASE_IMAGE_MANIFEST');
|
||||
} finally {
|
||||
await fs.promises.rm(workDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('BaseImageManager - ensureBaseImage() should reject hosted URL artifacts without sha256', async () => {
|
||||
const workDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'smartvm-hosted-image-url-test-'));
|
||||
const manifestPath = path.join(workDir, 'manifest.json');
|
||||
const manifest: IBaseImageHostedManifest = {
|
||||
schemaVersion: 1,
|
||||
bundleId: 'smartvm-invalid-url-test',
|
||||
arch: 'x86_64',
|
||||
firecrackerVersion: 'v1.15.1',
|
||||
rootfsType: 'ext4',
|
||||
kernel: { url: 'https://example.com/vmlinux' },
|
||||
rootfs: { path: path.join(workDir, 'rootfs.ext4') },
|
||||
};
|
||||
await fs.promises.writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
||||
|
||||
try {
|
||||
const manager = new BaseImageManager({ cacheDir: path.join(workDir, 'cache') });
|
||||
const error = await getRejectedError(manager.ensureBaseImage({ manifestPath }));
|
||||
expect(error).toBeInstanceOf(SmartVMError);
|
||||
expect((error as SmartVMError).code).toEqual('INVALID_BASE_IMAGE_MANIFEST');
|
||||
} finally {
|
||||
await fs.promises.rm(workDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('BaseImageManager - hosted preset should require a manifest', async () => {
|
||||
const workDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'smartvm-hosted-image-missing-test-'));
|
||||
try {
|
||||
const manager = new BaseImageManager({ cacheDir: path.join(workDir, 'cache') });
|
||||
const error = await getRejectedError(manager.ensureBaseImage({ preset: 'hosted' }));
|
||||
expect(error).toBeInstanceOf(SmartVMError);
|
||||
expect((error as SmartVMError).code).toEqual('BASE_IMAGE_MANIFEST_FAILED');
|
||||
} finally {
|
||||
await fs.promises.rm(workDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// NetworkManager Tests
|
||||
// ============================================================
|
||||
@@ -181,6 +550,65 @@ tap.test('NetworkManager - allocateIp() should allocate sequential IPs', async (
|
||||
expect(ip3).toEqual('172.30.0.4');
|
||||
});
|
||||
|
||||
tap.test('NetworkManager - allocateIp() should normalize non-network CIDR input', async () => {
|
||||
const nm = new NetworkManager({ subnet: '10.20.30.17/29' });
|
||||
expect(nm.allocateIp()).toEqual('10.20.30.18');
|
||||
expect(nm.allocateIp()).toEqual('10.20.30.19');
|
||||
});
|
||||
|
||||
tap.test('NetworkManager - allocateIp() should fail when subnet is exhausted', async () => {
|
||||
const nm = new NetworkManager({ subnet: '192.168.100.0/30' });
|
||||
expect(nm.allocateIp()).toEqual('192.168.100.2');
|
||||
|
||||
let error: unknown;
|
||||
try {
|
||||
nm.allocateIp();
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
|
||||
expect(error).toBeInstanceOf(SmartVMError);
|
||||
expect((error as SmartVMError).code).toEqual('IP_EXHAUSTED');
|
||||
});
|
||||
|
||||
tap.test('NetworkManager - constructor should reject invalid subnet input', async () => {
|
||||
let error: unknown;
|
||||
try {
|
||||
new NetworkManager({ subnet: '10.0.0.0/31' });
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
|
||||
expect(error).toBeInstanceOf(SmartVMError);
|
||||
expect((error as SmartVMError).code).toEqual('INVALID_SUBNET');
|
||||
});
|
||||
|
||||
tap.test('NetworkManager - constructor should reject malformed IPv4 octets', async () => {
|
||||
for (const subnet of ['10..0.1/24', '10.0x10.0.1/24', '10.0.0.1 /24']) {
|
||||
let error: unknown;
|
||||
try {
|
||||
new NetworkManager({ subnet });
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
|
||||
expect(error).toBeInstanceOf(SmartVMError);
|
||||
expect((error as SmartVMError).code).toEqual('INVALID_SUBNET');
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('NetworkManager - constructor should reject invalid bridge names', async () => {
|
||||
let error: unknown;
|
||||
try {
|
||||
new NetworkManager({ bridgeName: 'bad bridge' });
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
|
||||
expect(error).toBeInstanceOf(SmartVMError);
|
||||
expect((error as SmartVMError).code).toEqual('INVALID_INTERFACE_NAME');
|
||||
});
|
||||
|
||||
tap.test('NetworkManager - generateMac() should generate locally-administered MACs', async () => {
|
||||
const nm = new NetworkManager();
|
||||
const mac1 = nm.generateMac('vm1', 'eth0');
|
||||
@@ -228,6 +656,28 @@ tap.test('NetworkManager - getGuestNetworkBootArgs() should format correctly', a
|
||||
expect(bootArgs).toEqual('ip=172.30.0.2::172.30.0.1:255.255.255.0::eth0:off');
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// MicroVM Tests
|
||||
// ============================================================
|
||||
|
||||
tap.test('MicroVM - invalid lifecycle calls should throw SmartVMError', async () => {
|
||||
const vm = new MicroVM(
|
||||
'lifecycle-vm',
|
||||
sampleConfig,
|
||||
'/bin/false',
|
||||
'/tmp/smartvm-lifecycle.sock',
|
||||
new NetworkManager(),
|
||||
);
|
||||
|
||||
const pauseError = await getRejectedError(vm.pause());
|
||||
expect(pauseError).toBeInstanceOf(SmartVMError);
|
||||
expect((pauseError as SmartVMError).code).toEqual('INVALID_STATE');
|
||||
|
||||
const infoError = await getRejectedError(vm.getInfo());
|
||||
expect(infoError).toBeInstanceOf(SmartVMError);
|
||||
expect((infoError as SmartVMError).code).toEqual('NO_CLIENT');
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// SmartVM Tests
|
||||
// ============================================================
|
||||
@@ -236,6 +686,7 @@ tap.test('SmartVM - instantiation with defaults', async () => {
|
||||
const smartvm = new SmartVM();
|
||||
expect(smartvm).toBeTruthy();
|
||||
expect(smartvm.imageManager).toBeTruthy();
|
||||
expect(smartvm.baseImageManager).toBeTruthy();
|
||||
expect(smartvm.networkManager).toBeTruthy();
|
||||
expect(smartvm.vmCount).toEqual(0);
|
||||
expect(smartvm.listVMs()).toHaveLength(0);
|
||||
@@ -251,4 +702,19 @@ tap.test('SmartVM - instantiation with custom options', async () => {
|
||||
expect(smartvm).toBeTruthy();
|
||||
});
|
||||
|
||||
tap.test('SmartVM - createVM() should track created VMs', async () => {
|
||||
const smartvm = new SmartVM({
|
||||
dataDir: '/tmp/smartvm-test',
|
||||
firecrackerBinaryPath: '/bin/false',
|
||||
});
|
||||
const vm = await smartvm.createVM(sampleConfig);
|
||||
|
||||
expect(smartvm.vmCount).toEqual(1);
|
||||
expect(smartvm.getVM(vm.id)).toEqual(vm);
|
||||
expect(smartvm.listVMs()).toEqual([vm.id]);
|
||||
|
||||
await smartvm.cleanup();
|
||||
expect(smartvm.vmCount).toEqual(0);
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
|
||||
Reference in New Issue
Block a user