144 lines
3.6 KiB
JavaScript
144 lines
3.6 KiB
JavaScript
import { delay } from "nanodelay";
|
|
import { nanoid } from "nanoid";
|
|
import { ChildProcess, execFile, spawn } from "node:child_process";
|
|
import { writeFile } from "node:fs/promises";
|
|
import { createServer, request as _request, IncomingMessage } from "node:http";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { promisify } from "node:util";
|
|
|
|
const server = createServer(listener);
|
|
|
|
/**
|
|
* @param {import("node:http").IncomingMessage} req
|
|
* @param {import("node:http").ServerResponse<import("node:http").IncomingMessage>} res
|
|
*/
|
|
async function listener(req, res) {
|
|
const url = getUrl(req);
|
|
if (url.pathname == "/run") {
|
|
if (req.method == "POST") {
|
|
await runVm();
|
|
} else res.writeHead(405).end("wrong method");
|
|
} else res.writeHead(404).end("not found");
|
|
}
|
|
|
|
async function runVm() {
|
|
try {
|
|
await FirecrackerInstance.run();
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
}
|
|
|
|
class FirecrackerInstance {
|
|
proc;
|
|
socketPath;
|
|
constructor() {
|
|
const vmid = nanoid();
|
|
this.socketPath = join(tmpdir(), `firecracker-${vmid}.sock`);
|
|
console.debug(this.socketPath);
|
|
// TODO: jailer
|
|
this.proc = spawn(
|
|
"firecracker",
|
|
[
|
|
...["--api-sock", this.socketPath],
|
|
...["--level", "Debug"],
|
|
...["--log-path", "/dev/stderr"],
|
|
"--show-level",
|
|
"--show-log-origin",
|
|
],
|
|
{
|
|
stdio: "inherit",
|
|
}
|
|
);
|
|
}
|
|
static async run() {
|
|
const self = new FirecrackerInstance();
|
|
|
|
await self.request("PUT", "/boot-source", {
|
|
kernel_image_path: "../vmlinux.bin",
|
|
boot_args: "console=ttyS0 reboot=k panic=1 pci=off",
|
|
});
|
|
await self.request("PUT", "/drives/rootfs", {
|
|
drive_id: "rootfs",
|
|
path_on_host: "../rootfs.ext4",
|
|
is_root_device: true,
|
|
is_read_only: false,
|
|
});
|
|
|
|
// API requests are handled asynchronously, it is important the configuration is
|
|
// set, before `InstanceStart`.
|
|
await delay(15);
|
|
|
|
await self.request("PUT", "/actions", {
|
|
action_type: "InstanceStart",
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @param {string} method
|
|
* @param {string} path
|
|
* @param {object} body
|
|
*/
|
|
async request(method, path, body) {
|
|
const jsonBody = JSON.stringify(body);
|
|
const { response, body: respBody } = await request(
|
|
{
|
|
method,
|
|
path,
|
|
socketPath: this.socketPath,
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"Content-Length": jsonBody.length,
|
|
},
|
|
},
|
|
jsonBody
|
|
);
|
|
// @ts-ignore
|
|
if (response.statusCode > 299)
|
|
throw new Error(
|
|
`Status code: ${response.statusCode} / Body: ${respBody}`,
|
|
{
|
|
cause: `${method} ${path} ${JSON.stringify(body, null, 4)}`,
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {import("http").IncomingMessage} req
|
|
*/
|
|
function getUrl(req) {
|
|
// prettier-ignore
|
|
const urlString = /** @type {string} */(req.url);
|
|
const url = new URL(urlString, `http://${req.headers.host}`);
|
|
return url;
|
|
}
|
|
|
|
/**
|
|
* @param {string | import("url").URL | import("http").RequestOptions} opts
|
|
* @param {string} [body]
|
|
* @returns {Promise<{ response: import("node:http").IncomingMessage, body: string }>}
|
|
*/
|
|
function request(opts, body) {
|
|
return new Promise((resolve, reject) => {
|
|
const req = _request(opts, (res) => {
|
|
let respBody = "";
|
|
res.on("data", (data) => {
|
|
respBody += data;
|
|
});
|
|
res.on("end", () => {
|
|
resolve({ response: res, body: respBody });
|
|
});
|
|
});
|
|
req.on("error", reject);
|
|
if (body)
|
|
req.write(body, (error) => {
|
|
if (error) reject(error);
|
|
});
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
server.listen("8080");
|