initial
This commit is contained in:
244
ts/classes.imagemanager.ts
Normal file
244
ts/classes.imagemanager.ts
Normal 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`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user