diff options
author | Yuqian Yang <crupest@crupest.life> | 2025-06-13 15:41:02 +0800 |
---|---|---|
committer | Yuqian Yang <crupest@crupest.life> | 2025-06-13 17:29:39 +0800 |
commit | f0d4e0d641d4c5b2ce6bd1ad447944c8958d5f1d (patch) | |
tree | cddd6a6833165892e2c0d67d7b7b68e4b4f71f31 | |
parent | 7b09e420ede2f9c92f5eaff28508ec10255414b7 (diff) | |
download | crupest-f0d4e0d641d4c5b2ce6bd1ad447944c8958d5f1d.tar.gz crupest-f0d4e0d641d4c5b2ce6bd1ad447944c8958d5f1d.tar.bz2 crupest-f0d4e0d641d4c5b2ce6bd1ad447944c8958d5f1d.zip |
deno: add manage-vm.
-rw-r--r-- | deno/deno.json | 3 | ||||
-rw-r--r-- | deno/deno.lock | 88 | ||||
-rw-r--r-- | deno/tools/deno.json | 3 | ||||
-rw-r--r-- | deno/tools/manage-vm.ts | 147 |
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(); +} |