Files
smartvm/ts/classes.imagemanager.ts
T

269 lines
8.5 KiB
TypeScript
Raw Permalink Normal View History

2026-02-08 21:47:33 +00:00
import * as plugins from './plugins.js';
import type { TFirecrackerArch } from './interfaces/index.js';
import { SmartVMError } from './interfaces/index.js';
type TShellExecResult = Awaited<ReturnType<InstanceType<typeof plugins.smartshell.Smartshell>['execSpawn']>>;
function getErrorMessage(err: unknown): string {
return err instanceof Error ? err.message : String(err);
}
2026-02-08 21:47:33 +00:00
/**
* Helper to check if a file or directory exists.
*/
async function pathExists(filePath: string): Promise<boolean> {
try {
await plugins.fs.promises.access(filePath);
return true;
} catch {
return false;
}
}
/**
* Manages Firecracker binaries, kernel images, and rootfs images.
* Downloads and caches them in a local data directory.
*/
export class ImageManager {
private dataDir: string;
private arch: TFirecrackerArch;
private shell: InstanceType<typeof plugins.smartshell.Smartshell>;
2026-02-08 21:47:33 +00:00
constructor(dataDir: string, arch: TFirecrackerArch = 'x86_64') {
this.dataDir = dataDir;
this.arch = arch;
this.shell = new plugins.smartshell.Smartshell({ executor: 'bash' });
}
private async runChecked(command: string, args: string[]): Promise<TShellExecResult> {
const result = await this.shell.execSpawn(command, args, { silent: true });
if (result.exitCode !== 0) {
const output = (result.stderr || result.stdout || '').trim();
throw new Error(`${command} ${args.join(' ')} exited with code ${result.exitCode}${output ? `: ${output}` : ''}`);
}
return result;
2026-02-08 21:47:33 +00:00
}
/**
* Ensure all required directories exist.
*/
public async ensureDirectories(): Promise<void> {
const dirs = [
this.getBinDir(),
this.getKernelsDir(),
this.getRootfsDir(),
this.getSocketsDir(),
];
for (const dir of dirs) {
await plugins.fs.promises.mkdir(dir, { recursive: true });
}
}
/** Base directory for firecracker binaries. */
public getBinDir(): string {
return plugins.path.join(this.dataDir, 'bin');
}
/** Directory for kernel images. */
public getKernelsDir(): string {
return plugins.path.join(this.dataDir, 'kernels');
}
/** Directory for rootfs images. */
public getRootfsDir(): string {
return plugins.path.join(this.dataDir, 'rootfs');
}
/** Directory for Unix sockets. */
public getSocketsDir(): string {
return plugins.path.join(this.dataDir, 'sockets');
}
/**
* Get the path to the firecracker binary for a given version.
*/
public getFirecrackerPath(version: string): string {
return plugins.path.join(this.getBinDir(), version, 'firecracker');
}
/**
* Get the path to the jailer binary for a given version.
*/
public getJailerPath(version: string): string {
return plugins.path.join(this.getBinDir(), version, 'jailer');
}
/**
* Check if a Firecracker binary for the given version exists.
*/
public async hasBinary(version: string): Promise<boolean> {
const binPath = this.getFirecrackerPath(version);
return pathExists(binPath);
}
/**
* Query the GitHub API for the latest Firecracker release version tag.
*/
public async getLatestVersion(): Promise<string> {
try {
const result = await this.runChecked('curl', [
'-fsSLI',
'-o',
'/dev/null',
'-w',
'%{url_effective}',
'https://github.com/firecracker-microvm/firecracker/releases/latest',
]);
const match = result.stdout.trim().match(/\/releases\/tag\/(v\d+\.\d+\.\d+)$/);
if (!match) {
throw new Error(`Could not parse latest Firecracker release URL: ${result.stdout.trim()}`);
}
return match[1];
2026-02-08 21:47:33 +00:00
} catch (err) {
throw new SmartVMError(
`Failed to fetch latest Firecracker version: ${getErrorMessage(err)}`,
2026-02-08 21:47:33 +00:00
'VERSION_FETCH_FAILED',
);
}
}
/**
* Download and extract the Firecracker binary for a given version.
*/
public async downloadFirecracker(version: string): Promise<string> {
const targetDir = plugins.path.join(this.getBinDir(), version);
await plugins.fs.promises.mkdir(targetDir, { recursive: true });
// Firecracker release tarball naming:
// firecracker-v1.5.0-x86_64.tgz containing release-v1.5.0-x86_64/firecracker-v1.5.0-x86_64
const tag = version.startsWith('v') ? version : `v${version}`;
const archiveName = `firecracker-${tag}-${this.arch}.tgz`;
const downloadUrl = `https://github.com/firecracker-microvm/firecracker/releases/download/${tag}/${archiveName}`;
const archivePath = plugins.path.join(targetDir, archiveName);
try {
// Download the archive
await this.runChecked('curl', ['-fSL', '-o', archivePath, downloadUrl]);
2026-02-08 21:47:33 +00:00
// Extract the archive
await this.runChecked('tar', ['-xzf', archivePath, '-C', targetDir]);
2026-02-08 21:47:33 +00:00
// Firecracker archives contain a directory like release-v1.5.0-x86_64/
// with binaries named like firecracker-v1.5.0-x86_64
const extractedDir = plugins.path.join(targetDir, `release-${tag}-${this.arch}`);
const firecrackerSrc = plugins.path.join(extractedDir, `firecracker-${tag}-${this.arch}`);
const jailerSrc = plugins.path.join(extractedDir, `jailer-${tag}-${this.arch}`);
const firecrackerDst = this.getFirecrackerPath(version);
const jailerDst = this.getJailerPath(version);
// Move binaries to expected paths
await plugins.fs.promises.rename(firecrackerSrc, firecrackerDst);
2026-02-08 21:47:33 +00:00
if (await pathExists(jailerSrc)) {
await plugins.fs.promises.rename(jailerSrc, jailerDst);
2026-02-08 21:47:33 +00:00
}
// Make executable
await plugins.fs.promises.chmod(firecrackerDst, 0o755);
if (await pathExists(jailerDst)) {
await plugins.fs.promises.chmod(jailerDst, 0o755);
}
2026-02-08 21:47:33 +00:00
// Clean up
await plugins.fs.promises.rm(archivePath, { force: true });
await plugins.fs.promises.rm(extractedDir, { recursive: true, force: true });
2026-02-08 21:47:33 +00:00
return firecrackerDst;
} catch (err) {
throw new SmartVMError(
`Failed to download Firecracker ${version}: ${getErrorMessage(err)}`,
2026-02-08 21:47:33 +00:00
'DOWNLOAD_FAILED',
);
}
}
/**
* Download a kernel image from a URL.
*/
public async downloadKernel(url: string, name: string): Promise<string> {
const kernelsDir = this.getKernelsDir();
await plugins.fs.promises.mkdir(kernelsDir, { recursive: true });
const kernelPath = plugins.path.join(kernelsDir, name);
try {
await this.runChecked('curl', ['-fSL', '-o', kernelPath, url]);
2026-02-08 21:47:33 +00:00
return kernelPath;
} catch (err) {
throw new SmartVMError(
`Failed to download kernel from ${url}: ${getErrorMessage(err)}`,
2026-02-08 21:47:33 +00:00
'DOWNLOAD_FAILED',
);
}
}
/**
* Download a rootfs image from a URL.
*/
public async downloadRootfs(url: string, name: string): Promise<string> {
const rootfsDir = this.getRootfsDir();
await plugins.fs.promises.mkdir(rootfsDir, { recursive: true });
const rootfsPath = plugins.path.join(rootfsDir, name);
try {
await this.runChecked('curl', ['-fSL', '-o', rootfsPath, url]);
2026-02-08 21:47:33 +00:00
return rootfsPath;
} catch (err) {
throw new SmartVMError(
`Failed to download rootfs from ${url}: ${getErrorMessage(err)}`,
2026-02-08 21:47:33 +00:00
'DOWNLOAD_FAILED',
);
}
}
/**
* Create a blank ext4 rootfs image.
*/
public async createBlankRootfs(name: string, sizeMib: number): Promise<string> {
const rootfsDir = this.getRootfsDir();
await plugins.fs.promises.mkdir(rootfsDir, { recursive: true });
const rootfsPath = plugins.path.join(rootfsDir, name);
try {
await this.runChecked('dd', ['if=/dev/zero', `of=${rootfsPath}`, 'bs=1M', `count=${sizeMib}`]);
await this.runChecked('mkfs.ext4', [rootfsPath]);
2026-02-08 21:47:33 +00:00
return rootfsPath;
} catch (err) {
throw new SmartVMError(
`Failed to create blank rootfs: ${getErrorMessage(err)}`,
2026-02-08 21:47:33 +00:00
'ROOTFS_CREATE_FAILED',
);
}
}
/**
* Clone a rootfs image for a specific VM.
*/
public async cloneRootfs(sourcePath: string, targetName: string): Promise<string> {
const rootfsDir = this.getRootfsDir();
await plugins.fs.promises.mkdir(rootfsDir, { recursive: true });
const targetPath = plugins.path.join(rootfsDir, targetName);
try {
await plugins.fs.promises.copyFile(sourcePath, targetPath);
2026-02-08 21:47:33 +00:00
return targetPath;
} catch (err) {
throw new SmartVMError(
`Failed to clone rootfs: ${getErrorMessage(err)}`,
2026-02-08 21:47:33 +00:00
'ROOTFS_CLONE_FAILED',
);
}
}
/**
* Generate a unique socket path for a VM.
*/
public getSocketPath(vmId: string): string {
return plugins.path.join(this.getSocketsDir(), `${vmId}.sock`);
}
}