This commit is contained in:
2026-02-08 21:47:33 +00:00
commit d8b5e8a6c0
22 changed files with 11080 additions and 0 deletions

244
ts/classes.imagemanager.ts Normal file
View File

@@ -0,0 +1,244 @@
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<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;
constructor(dataDir: string, arch: TFirecrackerArch = 'x86_64') {
this.dataDir = dataDir;
this.arch = arch;
}
/**
* 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 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<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
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<string> {
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<string> {
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<string> {
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<string> {
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`);
}
}