Compare commits

..

2 commits

Author SHA1 Message Date
663c785458 readme: agregar referencias en mi sitio 2023-08-26 17:44:09 -03:00
bc33fd38cb WIP 2023-07-20 15:25:07 -03:00
6 changed files with 129 additions and 21 deletions

View file

@ -4,6 +4,8 @@
una API para correr cosas aisladas en una VM [firecracker] que inicia rápido a partir de una imágen de contenedor OCI. inspirado en [Fly Machines] y otras cosas.
[notas y referencias sobre firecracker](https://nulo.ar/Firecracker.html) en mi sitio
[firecracker]: <https://github.com/firecracker-microvm/firecracker>
[Fly Machines]: <https://fly.io/docs/reference/machines/>

3
server/.gitignore vendored
View file

@ -3,3 +3,6 @@ container-baby.js
# cache, will move later
cache/
# jailer
jailer/

View file

@ -1,11 +1,13 @@
import { delay } from "nanodelay";
import { nanoid } from "nanoid";
import { customAlphabet as nanoidCustomAlphabet, nanoid } from "nanoid";
import which from "which";
import { spawn, execFile as _execFile } from "node:child_process";
import { createServer, request as _request } from "node:http";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { basename, join } from "node:path";
import { downloadImage, parseImageRef } from "./container-baby.js";
import { promisify } from "node:util";
import { access, chmod, chown, link, mkdir } from "node:fs/promises";
const execFile = promisify(_execFile);
const server = createServer(listener);
@ -55,52 +57,115 @@ async function runVm(image) {
* @prop {boolean} is_read_only
*/
/**
* gets absolute path to firecracker executable
* @returns {Promise<string>}
*/
async function getFirecrackerExe() {
const firecrackerExe = await which("firecracker", { nothrow: true });
console.debug(
`[fireactions] Using firecracker executable '${firecrackerExe}'`
);
return firecrackerExe;
}
const jailerDir = "./jailer";
const [jailerUid, jailerGid] = [65534, 65534];
// we need to use a custom alphabet because jailer is picky
const jailerNanoid = nanoidCustomAlphabet(
"0123456789abcdefghijklmnopqrstuvwxyz-",
64
);
class FirecrackerInstance {
proc;
vmid;
socketPath;
constructor() {
const vmid = nanoid();
this.socketPath = join(tmpdir(), `firecracker-${vmid}.sock`);
proc;
/**
* @param {{ firecrackerExe: string }} param0
* @private
*/
constructor({ firecrackerExe }) {
this.vmid = jailerNanoid();
this.socketPath = join(this.chrootPath, `firecracker.sock`);
console.debug(this.socketPath);
// TODO: jailer
this.proc = spawn(
"firecracker",
"jailer",
[
...["--api-sock", this.socketPath],
...["--level", "Debug"],
...["--log-path", "/dev/stderr"],
"--show-level",
"--show-log-origin",
...["--id", this.vmid],
...["--exec-file", firecrackerExe],
...["--uid", "" + jailerUid],
...["--gid", "" + jailerGid],
...["--chroot-base-dir", jailerDir],
"--",
...["--api-sock", "/firecracker.sock"],
// ...["--level", "Debug"],
// ...["--log-path", "/stderr.log"],
// "--show-level",
// "--show-log-origin",
],
{
stdio: "inherit",
}
);
console.debug(
`[fireactions] Started firecracker with jailer at ${this.chrootPath}`
);
}
get chrootPath() {
return join(jailerDir, "firecracker", this.vmid, "root");
}
/**
* @param {string} path path to file to hardlink to root of chroot
* @param {number} mod mod inside the chroot
*/
async hardlinkToChroot(path, mod = 0o600) {
const chrootPath = join(this.chrootPath, basename(path));
let accesed = false;
try {
await access(chrootPath);
accesed = true;
} catch {}
if (accesed)
throw new Error("file with same name already exists inside chroot");
await link(path, chrootPath);
await chown(chrootPath, jailerUid, jailerGid);
await chmod(chrootPath, mod);
}
/**
* @param [opts] {{ drives?: Drive[] }}
*/
static async run(opts) {
const self = new FirecrackerInstance();
await mkdir(jailerDir, { recursive: true });
const self = new FirecrackerInstance({
firecrackerExe: await getFirecrackerExe(),
});
await mkdir(self.chrootPath, { recursive: true });
// TODO: retry until success
await delay(15);
const { ifname, hostAddr, guestAddr } = await createNetworkInterface();
await self.hardlinkToChroot("../vmlinux.bin", 0o400);
await self.request("PUT", "/boot-source", {
kernel_image_path: "../vmlinux.bin",
kernel_image_path: "/vmlinux.bin",
boot_args: `console=ttyS0 reboot=k panic=1 pci=off fireactions_ip=${guestAddr}:${hostAddr}`,
});
await self.request("PUT", "/drives/rootfs", {
drive_id: "rootfs",
path_on_host: "../rootfs.ext4",
is_root_device: true,
// TODO: readonly
self.attachDrive({
drive_id: "rootfs",
path_on_host: "/rootfs.ext4",
is_root_device: true,
is_read_only: false,
});
if (opts?.drives) {
for (const drive of opts.drives) {
await self.request("PUT", "/drives/" + drive.drive_id, drive);
await self.attachDrive(drive);
}
}
await self.request("PUT", "/network-interfaces/eth0", {
@ -119,6 +184,18 @@ class FirecrackerInstance {
});
}
/**
* @param {Drive} drive
*/
async attachDrive(drive) {
const perms = drive.is_read_only ? 0o400 : 0o600;
await this.hardlinkToChroot(drive.path_on_host, perms);
await this.request("PUT", "/drives/" + drive.drive_id, {
...drive,
path_on_host: "/" + basename(drive.path_on_host),
});
}
/**
* @param {string} method
* @param {string} path

View file

@ -5,7 +5,7 @@
"description": "",
"main": "index.js",
"scripts": {
"start": "esbuild --bundle index.js --platform=node --sourcemap > build.cjs && sudo node --enable-source-maps build.cjs"
"start": "esbuild --bundle index.js --platform=node --sourcemap > build.cjs && sudo env PATH=\"$PATH:$HOME/.local/bin\" node --enable-source-maps build.cjs"
},
"keywords": [],
"author": "",
@ -15,12 +15,14 @@
"@types/gunzip-maybe": "^1.4.0",
"@types/node": "^20.2.5",
"@types/tar-stream": "^2.2.2",
"@types/which": "^3.0.0",
"undici": "^5.22.1"
},
"dependencies": {
"gunzip-maybe": "^1.4.2",
"nanodelay": "^2.0.2",
"nanoid": "^4.0.2",
"tar-stream": "^3.0.0"
"tar-stream": "^3.0.0",
"which": "^3.0.1"
}
}

View file

@ -17,6 +17,9 @@ dependencies:
tar-stream:
specifier: ^3.0.0
version: 3.0.0
which:
specifier: ^3.0.1
version: 3.0.1
devDependencies:
'@tsconfig/node16':
@ -31,6 +34,9 @@ devDependencies:
'@types/tar-stream':
specifier: ^2.2.2
version: 2.2.2
'@types/which':
specifier: ^3.0.0
version: 3.0.0
undici:
specifier: ^5.22.1
version: 5.22.1
@ -57,6 +63,10 @@ packages:
'@types/node': 20.2.5
dev: true
/@types/which@3.0.0:
resolution: {integrity: sha512-ASCxdbsrwNfSMXALlC3Decif9rwDMu+80KGp5zI2RLRotfMsTv7fHL8W8VDp24wymzDyIFudhUeSCugrgRFfHQ==}
dev: true
/abort-controller@3.0.0:
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
engines: {node: '>=6.5'}
@ -170,6 +180,10 @@ packages:
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
dev: false
/isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
dev: false
/nanodelay@2.0.2:
resolution: {integrity: sha512-6AS5aCSXsjoxq2Jr9CdaAeT60yoYDOTp6po9ziqeOeY6vf6uTEHYSqWql6EFILrM3fEfXgkZ4KqE9L0rTm/wlA==}
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
@ -301,6 +315,14 @@ packages:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
dev: false
/which@3.0.1:
resolution: {integrity: sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
hasBin: true
dependencies:
isexe: 2.0.0
dev: false
/wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
dev: false

View file

@ -2,6 +2,8 @@
// TODO: sandboxear mkquashfs
// TODO: fijarse como hace firecracker-containerd
// TODO: fijarse como hace [ignite](https://github.com/weaveworks/ignite)
// TODO: fijarse como hace [thi](https://github.com/thi-startup/init)
// TODO: fijarse como hace [firebuild](https://combust-labs.github.io/firebuild-docs/)
import gunzip from "gunzip-maybe";
import { spawn } from "node:child_process";