245 lines
7.7 KiB
TypeScript
245 lines
7.7 KiB
TypeScript
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`);
|
|
}
|
|
}
|