aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYuqian Yang <crupest@crupest.life>2025-06-13 15:41:02 +0800
committerYuqian Yang <crupest@crupest.life>2025-06-13 17:29:39 +0800
commitf0d4e0d641d4c5b2ce6bd1ad447944c8958d5f1d (patch)
treecddd6a6833165892e2c0d67d7b7b68e4b4f71f31
parent7b09e420ede2f9c92f5eaff28508ec10255414b7 (diff)
downloadcrupest-f0d4e0d641d4c5b2ce6bd1ad447944c8958d5f1d.tar.gz
crupest-f0d4e0d641d4c5b2ce6bd1ad447944c8958d5f1d.tar.bz2
crupest-f0d4e0d641d4c5b2ce6bd1ad447944c8958d5f1d.zip
deno: add manage-vm.
-rw-r--r--deno/deno.json3
-rw-r--r--deno/deno.lock88
-rw-r--r--deno/tools/deno.json3
-rw-r--r--deno/tools/manage-vm.ts147
4 files changed, 234 insertions, 7 deletions
diff --git a/deno/deno.json b/deno/deno.json
index 569f97f..0c603a0 100644
--- a/deno/deno.json
+++ b/deno/deno.json
@@ -13,6 +13,7 @@
"@std/path": "jsr:@std/path@^1.1.0",
"@std/testing": "jsr:@std/testing@^1.0.13",
"@std/dotenv": "jsr:@std/dotenv@^0.225.5",
- "@std/fs": "jsr:@std/fs@^1.0.18"
+ "@std/fs": "jsr:@std/fs@^1.0.18",
+ "@types/yargs": "npm:@types/yargs@^17.0.33"
}
}
diff --git a/deno/deno.lock b/deno/deno.lock
index 357b31f..2f3ac13 100644
--- a/deno/deno.lock
+++ b/deno/deno.lock
@@ -36,10 +36,12 @@
"npm:@types/lodash@*": "4.17.17",
"npm:@types/mustache@*": "4.2.6",
"npm:@types/node@*": "22.15.15",
+ "npm:@types/yargs@^17.0.33": "17.0.33",
"npm:email-addresses@5": "5.0.0",
"npm:hono@^4.7.11": "4.7.11",
"npm:kysely@~0.28.2": "0.28.2",
"npm:mustache@^4.2.0": "4.2.0",
+ "npm:yargs@18": "18.0.0",
"npm:zod@^3.25.48": "3.25.51"
},
"jsr": {
@@ -1183,12 +1185,41 @@
"undici-types"
]
},
+ "@types/yargs-parser@21.0.3": {
+ "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="
+ },
+ "@types/yargs@17.0.33": {
+ "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==",
+ "dependencies": [
+ "@types/yargs-parser"
+ ]
+ },
+ "ansi-regex@6.1.0": {
+ "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="
+ },
+ "ansi-styles@6.2.1": {
+ "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="
+ },
"bowser@2.11.0": {
"integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA=="
},
+ "cliui@9.0.1": {
+ "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==",
+ "dependencies": [
+ "string-width",
+ "strip-ansi",
+ "wrap-ansi"
+ ]
+ },
"email-addresses@5.0.0": {
"integrity": "sha512-4OIPYlA6JXqtVn8zpHpGiI7vE6EQOAg16aGnDMIAlZVinnoZ8208tW1hAbjWydgN/4PLTT9q+O1K6AH/vALJGw=="
},
+ "emoji-regex@10.4.0": {
+ "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="
+ },
+ "escalade@3.2.0": {
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="
+ },
"fast-xml-parser@4.4.1": {
"integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==",
"dependencies": [
@@ -1196,6 +1227,12 @@
],
"bin": true
},
+ "get-caller-file@2.0.5": {
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="
+ },
+ "get-east-asian-width@1.3.0": {
+ "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ=="
+ },
"hono@4.7.11": {
"integrity": "sha512-rv0JMwC0KALbbmwJDEnxvQCeJh+xbS3KEWW5PC9cMJ08Ur9xgatI0HmtgYZfOdOSOeYsp5LO2cOhdI8cLEbDEQ=="
},
@@ -1206,6 +1243,20 @@
"integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==",
"bin": true
},
+ "string-width@7.2.0": {
+ "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
+ "dependencies": [
+ "emoji-regex",
+ "get-east-asian-width",
+ "strip-ansi"
+ ]
+ },
+ "strip-ansi@7.1.0": {
+ "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+ "dependencies": [
+ "ansi-regex"
+ ]
+ },
"strnum@1.1.2": {
"integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA=="
},
@@ -1219,6 +1270,31 @@
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
"bin": true
},
+ "wrap-ansi@9.0.0": {
+ "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==",
+ "dependencies": [
+ "ansi-styles",
+ "string-width",
+ "strip-ansi"
+ ]
+ },
+ "y18n@5.0.8": {
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="
+ },
+ "yargs-parser@22.0.0": {
+ "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="
+ },
+ "yargs@18.0.0": {
+ "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==",
+ "dependencies": [
+ "cliui",
+ "escalade",
+ "get-caller-file",
+ "string-width",
+ "y18n",
+ "yargs-parser"
+ ]
+ },
"zod@3.25.51": {
"integrity": "sha512-TQSnBldh+XSGL+opiSIq0575wvDPqu09AqWe1F7JhUMKY+M91/aGlK4MhpVNO7MgYfHcVCB1ffwAUTJzllKJqg=="
}
@@ -1228,11 +1304,14 @@
"jsr:@std/cli@^1.0.19",
"jsr:@std/collections@^1.1.1",
"jsr:@std/csv@^1.0.6",
+ "jsr:@std/dotenv@~0.225.5",
"jsr:@std/encoding@^1.0.10",
"jsr:@std/expect@^1.0.16",
+ "jsr:@std/fs@^1.0.18",
"jsr:@std/io@~0.225.2",
"jsr:@std/path@^1.1.0",
- "jsr:@std/testing@^1.0.13"
+ "jsr:@std/testing@^1.0.13",
+ "npm:@types/yargs@^17.0.33"
],
"members": {
"mail-relay": {
@@ -1248,11 +1327,10 @@
"npm:zod@^3.25.48"
]
},
- "service-manager": {
+ "tools": {
"dependencies": [
- "jsr:@std/dotenv@~0.225.5",
- "jsr:@std/fs@^1.0.18",
- "npm:mustache@^4.2.0"
+ "npm:mustache@^4.2.0",
+ "npm:yargs@18"
]
}
}
diff --git a/deno/tools/deno.json b/deno/tools/deno.json
index 355046a..cda933f 100644
--- a/deno/tools/deno.json
+++ b/deno/tools/deno.json
@@ -3,6 +3,7 @@
"tasks": {
},
"imports": {
- "mustache": "npm:mustache@^4.2.0"
+ "mustache": "npm:mustache@^4.2.0",
+ "yargs": "npm:yargs@^18.0.0"
}
}
diff --git a/deno/tools/manage-vm.ts b/deno/tools/manage-vm.ts
new file mode 100644
index 0000000..a1388b1
--- /dev/null
+++ b/deno/tools/manage-vm.ts
@@ -0,0 +1,147 @@
+import os from "node:os"
+import { join } from "@std/path";
+// @ts-types="npm:@types/yargs"
+import yargs from "yargs";
+
+
+type ArchAliasMap = { [name: string]: string[] };
+const arches = {
+ x86_64: ["x86_64", "amd64"],
+ i386: ["i386", "x86", "i686"],
+} as const satisfies ArchAliasMap;
+type Arch = keyof typeof arches;
+type GeneralArch = (typeof arches)[Arch][number];
+
+function normalizeArch(generalName: GeneralArch): Arch {
+ for (const [name, aliases] of Object.entries(
+ arches as ArchAliasMap,
+ )) {
+ if (aliases.includes(generalName)) return name as Arch;
+ }
+ throw Error("Unknown architecture name.");
+}
+
+interface GeneralVmSetup {
+ name?: string[];
+ arch: GeneralArch;
+ disk: string;
+ sshForwardPort: number;
+ kvm?: boolean;
+}
+
+interface VmSetup {
+ arch: Arch;
+ disk: string;
+ sshForwardPort: number;
+ kvm: boolean;
+}
+
+const MY_VMS: GeneralVmSetup[] = [
+ {
+ name: ["hurd", ...arches.i386.map((a) => `hurd-${a}`)],
+ arch: "i386",
+ disk: join(os.homedir(), "vms/hurd-i386.qcow2"),
+ sshForwardPort: 3222,
+ },
+ {
+ name: [...arches.x86_64.map((a) => `hurd-${a}`)],
+ arch: "x86_64",
+ disk: join(os.homedir(), "vms/hurd-x86_64.qcow2"),
+ sshForwardPort: 3223,
+ },
+];
+
+function normalizeVmSetup(generalSetup: GeneralVmSetup): VmSetup {
+ const { arch, disk, sshForwardPort, kvm } = generalSetup;
+ return {
+ arch: normalizeArch(arch),
+ disk,
+ sshForwardPort,
+ kvm: kvm ?? Deno.build.os === "linux",
+ };
+}
+
+function resolveVmSetup(
+ name: string,
+ vms: GeneralVmSetup[],
+): VmSetup | undefined {
+ const setup = vms.find((vm) => vm.name?.includes(name));
+ return setup == null ? undefined : normalizeVmSetup(setup);
+}
+
+const qemuBinPrefix = "qemu-system" as const;
+
+const qemuBinSuffix = {
+ x86_64: "x86_64",
+ i386: "x86_64",
+} as const;
+
+function getQemuBin(arch: Arch): string {
+ return `${qemuBinPrefix}-${qemuBinSuffix[arch]}`;
+}
+
+function getLinuxHostArgs(kvm: boolean): string[] {
+ return kvm ? ["-enable-kvm"] : [];
+}
+
+function getMachineArgs(arch: Arch): string[] {
+ const is64 = arch === "x86_64";
+ const machineArgs = is64 ? ["-machine", "q35"] : [];
+ const memory = is64 ? 8 : 4;
+ return [...machineArgs, "-m", `${memory}G`];
+}
+
+function getNetworkArgs(sshForwardPort: number): string[] {
+ return ["-net", "nic", "-net", `user,hostfwd=tcp::${sshForwardPort}-:22`];
+}
+
+function getDisplayArgs(): string[] {
+ return ["-vga", "vmware"];
+}
+
+function getDiskArgs(disk: string): string[] {
+ return ["-drive", `cache=writeback,file=${disk}`];
+}
+
+function createQemuArgs(setup: VmSetup): string[] {
+ const { arch, disk, sshForwardPort } = setup;
+ return [
+ getQemuBin(arch),
+ ...getLinuxHostArgs(setup.kvm),
+ ...getMachineArgs(arch),
+ ...getDisplayArgs(),
+ ...getNetworkArgs(sshForwardPort),
+ ...getDiskArgs(disk),
+ ];
+}
+
+if (import.meta.main) {
+ await yargs(Deno.args)
+ .scriptName("manage-vm")
+ .command({
+ command: "gen <name>",
+ describe: "generate cli command to run the vm",
+ builder: (builder) => {
+ return builder
+ .positional("name", {
+ describe: "name of the vm to run",
+ type: "string",
+ })
+ .demandOption("name")
+ .strict();
+ },
+ handler: (argv) => {
+ const vm = resolveVmSetup(argv.name, MY_VMS);
+ if (vm == null) {
+ console.error(`No vm called ${argv.name} is found.`);
+ Deno.exit(-1);
+ }
+ const cli = createQemuArgs(vm);
+ console.log(`${cli.join(" ")}`);
+ },
+ })
+ .demandCommand(1, "One command must be specified.")
+ .help()
+ .strict()
+ .parse();
+}