diff options
| -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(); +} | 
