A convenient CLI tool to quickly spin up DragonflyBSD virtual machines using QEMU with sensible defaults.
at main 170 lines 4.6 kB view raw
1import _ from "@es-toolkit/es-toolkit/compat"; 2import chalk from "chalk"; 3import { Effect, pipe } from "effect"; 4import { LOGS_DIR } from "../constants.ts"; 5import type { VirtualMachine } from "../db.ts"; 6import { getInstanceStateOrFail, updateInstanceState } from "../state.ts"; 7import { safeKillQemu, setupNATNetworkArgs } from "../utils.ts"; 8 9const killQemuProcess = (vm: VirtualMachine) => 10 pipe( 11 safeKillQemu(vm.pid, Boolean(vm.bridge)), 12 Effect.flatMap((success) => { 13 if (!success) { 14 return Effect.fail( 15 new Error(`Failed to stop virtual machine ${vm.name}`), 16 ); 17 } 18 return Effect.succeed(vm); 19 }), 20 ); 21 22const markInstanceAsStopped = (vm: VirtualMachine) => 23 updateInstanceState(vm.id, "STOPPED"); 24 25const waitForDelay = (ms: number) => 26 Effect.tryPromise({ 27 try: () => new Promise((resolve) => setTimeout(resolve, ms)), 28 catch: () => new Error("Sleep failed"), 29 }); 30 31const createLogsDirectory = () => 32 Effect.tryPromise({ 33 try: () => Deno.mkdir(LOGS_DIR, { recursive: true }), 34 catch: (cause) => new Error(`Failed to create logs directory: ${cause}`), 35 }); 36 37const buildQemuArgs = (vm: VirtualMachine) => [ 38 ..._.compact([vm.bridge && "qemu-system-x86_64"]), 39 ...Deno.build.os === "linux" ? ["-enable-kvm"] : [], 40 "-cpu", 41 vm.cpu, 42 "-m", 43 vm.memory, 44 "-smp", 45 vm.cpus.toString(), 46 ..._.compact([vm.isoPath && "-cdrom", vm.isoPath]), 47 "-netdev", 48 vm.bridge 49 ? `bridge,id=net0,br=${vm.bridge}` 50 : setupNATNetworkArgs(vm.portForward), 51 "-device", 52 `e1000,netdev=net0,mac=${vm.macAddress}`, 53 "-display", 54 "none", 55 "-vga", 56 "none", 57 "-monitor", 58 "none", 59 "-chardev", 60 "stdio,id=con0,signal=off", 61 "-serial", 62 "chardev:con0", 63 ..._.compact( 64 vm.drivePath && [ 65 "-drive", 66 `file=${vm.drivePath},format=${vm.diskFormat},if=virtio`, 67 ], 68 ), 69]; 70 71const buildQemuCommand = (vm: VirtualMachine, logPath: string) => { 72 const qemuArgs = buildQemuArgs(vm); 73 return vm.bridge 74 ? `sudo qemu-system-x86_64 ${ 75 qemuArgs.slice(1).join(" ") 76 } >> "${logPath}" 2>&1 & echo $!` 77 : `qemu-system-x86_64 ${qemuArgs.join(" ")} >> "${logPath}" 2>&1 & echo $!`; 78}; 79 80const startQemuProcess = (fullCommand: string) => 81 Effect.tryPromise({ 82 try: async () => { 83 const cmd = new Deno.Command("sh", { 84 args: ["-c", fullCommand], 85 stdin: "null", 86 stdout: "piped", 87 }); 88 89 const { stdout } = await cmd.spawn().output(); 90 return parseInt(new TextDecoder().decode(stdout).trim(), 10); 91 }, 92 catch: (cause) => new Error(`Failed to start QEMU: ${cause}`), 93 }); 94 95const markInstanceAsRunning = (vm: VirtualMachine, qemuPid: number) => 96 updateInstanceState(vm.id, "RUNNING", qemuPid); 97 98const logRestartSuccess = ( 99 vm: VirtualMachine, 100 qemuPid: number, 101 logPath: string, 102) => 103 Effect.sync(() => { 104 console.log( 105 `${chalk.greenBright(vm.name)} restarted with PID ${ 106 chalk.greenBright(qemuPid) 107 }.`, 108 ); 109 console.log( 110 `Logs are being written to ${chalk.blueBright(logPath)}`, 111 ); 112 }); 113 114const startVirtualMachineProcess = (vm: VirtualMachine) => { 115 const logPath = `${LOGS_DIR}/${vm.name}.log`; 116 const fullCommand = buildQemuCommand(vm, logPath); 117 118 return pipe( 119 startQemuProcess(fullCommand), 120 Effect.flatMap((qemuPid) => 121 pipe( 122 waitForDelay(2000), 123 Effect.flatMap(() => markInstanceAsRunning(vm, qemuPid)), 124 Effect.flatMap(() => logRestartSuccess(vm, qemuPid, logPath)), 125 ) 126 ), 127 ); 128}; 129 130const restartVirtualMachine = (name: string) => 131 pipe( 132 getInstanceStateOrFail(name), 133 Effect.flatMap((vm) => 134 pipe( 135 killQemuProcess(vm), 136 Effect.flatMap(() => markInstanceAsStopped(vm)), 137 Effect.flatMap(() => waitForDelay(2000)), 138 Effect.flatMap(() => createLogsDirectory()), 139 Effect.flatMap(() => startVirtualMachineProcess(vm)), 140 ) 141 ), 142 ); 143 144export default async function (name: string) { 145 const program = pipe( 146 restartVirtualMachine(name), 147 Effect.catchTags({ 148 InstanceNotFoundError: (_error) => 149 Effect.sync(() => { 150 console.error( 151 `Virtual machine with name or ID ${ 152 chalk.greenBright(name) 153 } not found.`, 154 ); 155 Deno.exit(1); 156 }), 157 }), 158 Effect.catchAll((error) => 159 Effect.sync(() => { 160 console.error(`Error: ${error.message}`); 161 Deno.exit(1); 162 }) 163 ), 164 ); 165 166 await Effect.runPromise(program); 167 168 await new Promise((resolve) => setTimeout(resolve, 2000)); 169 Deno.exit(0); 170}