import * as plugins from './plugins.js'; import type { TFirecrackerArch } from './interfaces/index.js'; import { SmartVMError } from './interfaces/index.js'; /** * 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; constructor(dataDir: string, arch: TFirecrackerArch = 'x86_64') { this.dataDir = dataDir; this.arch = arch; } /** * 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 response = await plugins.SmartRequest.create() .url('https://api.github.com/repos/firecracker-microvm/firecracker/releases/latest') .get(); const data = await response.json() as { tag_name: string }; return data.tag_name; } catch (err) { throw new SmartVMError( `Failed to fetch latest Firecracker version: ${(err as Error).message}`, '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 const shell = new plugins.smartshell.Smartshell({ executor: 'bash' }); await shell.exec(`curl -fSL -o "${archivePath}" "${downloadUrl}"`); // Extract the archive await shell.exec(`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 shell.exec(`mv "${firecrackerSrc}" "${firecrackerDst}"`); if (await pathExists(jailerSrc)) { await shell.exec(`mv "${jailerSrc}" "${jailerDst}"`); } // Make executable await shell.exec(`chmod +x "${firecrackerDst}"`); // Clean up await shell.exec(`rm -rf "${archivePath}" "${extractedDir}"`); return firecrackerDst; } catch (err) { throw new SmartVMError( `Failed to download Firecracker ${version}: ${(err as Error).message}`, '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 { const shell = new plugins.smartshell.Smartshell({ executor: 'bash' }); await shell.exec(`curl -fSL -o "${kernelPath}" "${url}"`); return kernelPath; } catch (err) { throw new SmartVMError( `Failed to download kernel from ${url}: ${(err as Error).message}`, '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 { const shell = new plugins.smartshell.Smartshell({ executor: 'bash' }); await shell.exec(`curl -fSL -o "${rootfsPath}" "${url}"`); return rootfsPath; } catch (err) { throw new SmartVMError( `Failed to download rootfs from ${url}: ${(err as Error).message}`, '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 { const shell = new plugins.smartshell.Smartshell({ executor: 'bash' }); await shell.exec(`dd if=/dev/zero of="${rootfsPath}" bs=1M count=${sizeMib}`); await shell.exec(`mkfs.ext4 "${rootfsPath}"`); return rootfsPath; } catch (err) { throw new SmartVMError( `Failed to create blank rootfs: ${(err as Error).message}`, '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 { const shell = new plugins.smartshell.Smartshell({ executor: 'bash' }); await shell.exec(`cp "${sourcePath}" "${targetPath}"`); return targetPath; } catch (err) { throw new SmartVMError( `Failed to clone rootfs: ${(err as Error).message}`, 'ROOTFS_CLONE_FAILED', ); } } /** * Generate a unique socket path for a VM. */ public getSocketPath(vmId: string): string { return plugins.path.join(this.getSocketsDir(), `${vmId}.sock`); } }