import * as plugins from './plugins.js'; import type { TFirecrackerArch } from './interfaces/index.js'; import { SmartVMError } from './interfaces/index.js'; type TShellExecResult = Awaited['execSpawn']>>; function getErrorMessage(err: unknown): string { return err instanceof Error ? err.message : String(err); } /** * Helper to check if a file or directory exists. */ async function pathExists(filePath: string): Promise { 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; 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 { 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; } /** * Ensure all required directories exist. */ public async ensureDirectories(): Promise { 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 { const binPath = this.getFirecrackerPath(version); return pathExists(binPath); } /** * Query the GitHub API for the latest Firecracker release version tag. */ public async getLatestVersion(): Promise { 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]; } catch (err) { throw new SmartVMError( `Failed to fetch latest Firecracker version: ${getErrorMessage(err)}`, 'VERSION_FETCH_FAILED', ); } } /** * Download and extract the Firecracker binary for a given version. */ public async downloadFirecracker(version: string): Promise { 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]); // Extract the archive await this.runChecked('tar', ['-xzf', archivePath, '-C', targetDir]); // 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); if (await pathExists(jailerSrc)) { await plugins.fs.promises.rename(jailerSrc, jailerDst); } // Make executable await plugins.fs.promises.chmod(firecrackerDst, 0o755); if (await pathExists(jailerDst)) { await plugins.fs.promises.chmod(jailerDst, 0o755); } // Clean up await plugins.fs.promises.rm(archivePath, { force: true }); await plugins.fs.promises.rm(extractedDir, { recursive: true, force: true }); return firecrackerDst; } catch (err) { throw new SmartVMError( `Failed to download Firecracker ${version}: ${getErrorMessage(err)}`, 'DOWNLOAD_FAILED', ); } } /** * Download a kernel image from a URL. */ public async downloadKernel(url: string, name: string): Promise { 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]); return kernelPath; } catch (err) { throw new SmartVMError( `Failed to download kernel from ${url}: ${getErrorMessage(err)}`, 'DOWNLOAD_FAILED', ); } } /** * Download a rootfs image from a URL. */ public async downloadRootfs(url: string, name: string): Promise { 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]); return rootfsPath; } catch (err) { throw new SmartVMError( `Failed to download rootfs from ${url}: ${getErrorMessage(err)}`, 'DOWNLOAD_FAILED', ); } } /** * Create a blank ext4 rootfs image. */ public async createBlankRootfs(name: string, sizeMib: number): Promise { 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]); return rootfsPath; } catch (err) { throw new SmartVMError( `Failed to create blank rootfs: ${getErrorMessage(err)}`, 'ROOTFS_CREATE_FAILED', ); } } /** * Clone a rootfs image for a specific VM. */ public async cloneRootfs(sourcePath: string, targetName: string): Promise { 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); return targetPath; } catch (err) { throw new SmartVMError( `Failed to clone rootfs: ${getErrorMessage(err)}`, 'ROOTFS_CLONE_FAILED', ); } } /** * Generate a unique socket path for a VM. */ public getSocketPath(vmId: string): string { return plugins.path.join(this.getSocketsDir(), `${vmId}.sock`); } }