A simple, powerful CLI tool to spin up OpenIndiana virtual machines with QEMU
1import { parseFlags } from "@cliffy/flags";
2import _ from "@es-toolkit/es-toolkit/compat";
3import { LOGS_DIR } from "../constants.ts";
4import type { VirtualMachine } from "../db.ts";
5import { getInstanceState, updateInstanceState } from "../state.ts";
6
7export default async function (name: string, detach: boolean = false) {
8 let vm = await getInstanceState(name);
9 if (!vm) {
10 console.error(
11 `Virtual machine with name or ID ${name} not found.`,
12 );
13 Deno.exit(1);
14 }
15
16 console.log(`Starting virtual machine ${vm.name} (ID: ${vm.id})...`);
17
18 vm = mergeFlags(vm);
19
20 const qemuArgs = [
21 ..._.compact([vm.bridge && "qemu-system-x86_64"]),
22 ...Deno.build.os === "linux" ? ["-enable-kvm"] : [],
23 "-cpu",
24 vm.cpu,
25 "-m",
26 vm.memory,
27 "-smp",
28 vm.cpus.toString(),
29 ..._.compact([vm.isoPath && "-cdrom", vm.isoPath]),
30 "-netdev",
31 vm.bridge
32 ? `bridge,id=net0,br=${vm.bridge}`
33 : "user,id=net0,hostfwd=tcp::2222-:22",
34 "-device",
35 `e1000,netdev=net0,mac=${vm.macAddress}`,
36 "-device",
37 "ahci,id=ahci0",
38 "-nographic",
39 "-monitor",
40 "none",
41 "-chardev",
42 "stdio,id=con0,signal=off",
43 "-serial",
44 "chardev:con0",
45 ..._.compact(
46 vm.drivePath && [
47 "-drive",
48 `file=${vm.drivePath},format=${vm.diskFormat},if=none,id=disk0`,
49 "-device",
50 "ide-hd,drive=disk0,bus=ahci0.0",
51 ],
52 ),
53 ];
54
55 if (detach) {
56 await Deno.mkdir(LOGS_DIR, { recursive: true });
57 const logPath = `${LOGS_DIR}/${vm.name}.log`;
58
59 const fullCommand = vm.bridge
60 ? `sudo qemu-system-x86_64 ${
61 qemuArgs.slice(1).join(" ")
62 } >> "${logPath}" 2>&1 & echo $!`
63 : `qemu-system-x86_64 ${
64 qemuArgs.join(" ")
65 } >> "${logPath}" 2>&1 & echo $!`;
66
67 const cmd = new Deno.Command("sh", {
68 args: ["-c", fullCommand],
69 stdin: "null",
70 stdout: "piped",
71 });
72
73 const { stdout } = await cmd.spawn().output();
74 const qemuPid = parseInt(new TextDecoder().decode(stdout).trim(), 10);
75
76 await updateInstanceState(name, "RUNNING", qemuPid);
77
78 console.log(
79 `Virtual machine ${vm.name} started in background (PID: ${qemuPid})`,
80 );
81 console.log(`Logs will be written to: ${logPath}`);
82
83 // Exit successfully while keeping VM running in background
84 Deno.exit(0);
85 } else {
86 const cmd = new Deno.Command(vm.bridge ? "sudo" : "qemu-system-x86_64", {
87 args: qemuArgs,
88 stdin: "inherit",
89 stdout: "inherit",
90 stderr: "inherit",
91 });
92
93 const child = cmd.spawn();
94 await updateInstanceState(name, "RUNNING", child.pid);
95
96 const status = await child.status;
97
98 await updateInstanceState(name, "STOPPED", child.pid);
99
100 if (!status.success) {
101 Deno.exit(status.code);
102 }
103 }
104}
105
106function mergeFlags(vm: VirtualMachine): VirtualMachine {
107 const { flags } = parseFlags(Deno.args);
108 return {
109 ...vm,
110 memory: flags.memory ? String(flags.memory) : vm.memory,
111 cpus: flags.cpus ? Number(flags.cpus) : vm.cpus,
112 cpu: flags.cpu ? String(flags.cpu) : vm.cpu,
113 diskFormat: flags.diskFormat ? String(flags.diskFormat) : vm.diskFormat,
114 portForward: flags.portForward ? String(flags.portForward) : vm.portForward,
115 drivePath: flags.image ? String(flags.image) : vm.drivePath,
116 bridge: flags.bridge ? String(flags.bridge) : vm.bridge,
117 diskSize: flags.size ? String(flags.size) : vm.diskSize,
118 };
119}