A convenient CLI tool to quickly spin up DragonflyBSD virtual machines using QEMU with sensible defaults.
at main 219 lines 6.4 kB view raw
1import { Hono } from "hono"; 2import { Data, Effect, pipe } from "effect"; 3import { 4 createVolumeIfNeeded, 5 handleError, 6 parseCreateMachineRequest, 7 parseParams, 8 parseQueryParams, 9 parseStartRequest, 10 presentation, 11} from "./utils.ts"; 12import { DEFAULT_VERSION, getInstanceState, LOGS_DIR } from "../mod.ts"; 13import { 14 listInstances, 15 removeInstanceState, 16 saveInstanceState, 17 updateInstanceState, 18} from "../state.ts"; 19import { findVm, killProcess, updateToStopped } from "../subcommands/stop.ts"; 20import { 21 buildDetachedCommand, 22 buildQemuArgs, 23 createLogsDir, 24 failIfVMRunning, 25 startDetachedQemu, 26} from "../subcommands/start.ts"; 27import type { NewMachine } from "../types.ts"; 28import { createId } from "@paralleldrive/cuid2"; 29import { generateRandomMacAddress } from "../network.ts"; 30import Moniker from "moniker"; 31import { getImage } from "../images.ts"; 32 33export class ImageNotFoundError extends Data.TaggedError("ImageNotFoundError")<{ 34 id: string; 35}> {} 36 37export class RemoveRunningVmError extends Data.TaggedError( 38 "RemoveRunningVmError", 39)<{ 40 id: string; 41}> {} 42 43const app = new Hono(); 44 45app.get("/", (c) => 46 Effect.runPromise( 47 pipe( 48 parseQueryParams(c), 49 Effect.flatMap((params) => 50 listInstances( 51 params.all === "true" || params.all === "1", 52 ) 53 ), 54 presentation(c), 55 ), 56 )); 57 58app.post("/", (c) => 59 Effect.runPromise( 60 pipe( 61 parseCreateMachineRequest(c), 62 Effect.flatMap((params: NewMachine) => 63 Effect.gen(function* () { 64 const image = yield* getImage(params.image); 65 if (!image) { 66 return yield* Effect.fail( 67 new ImageNotFoundError({ id: params.image }), 68 ); 69 } 70 71 const volume = params.volume 72 ? yield* createVolumeIfNeeded(image, params.volume) 73 : undefined; 74 75 const macAddress = yield* generateRandomMacAddress(); 76 const id = createId(); 77 yield* saveInstanceState({ 78 id, 79 name: Moniker.choose(), 80 bridge: params.bridge, 81 macAddress, 82 memory: params.memory || "2G", 83 cpus: params.cpus || 8, 84 cpu: params.cpu || "host", 85 diskSize: "20G", 86 diskFormat: volume ? "qcow2" : "raw", 87 portForward: params.portForward 88 ? params.portForward.join(",") 89 : undefined, 90 drivePath: volume ? volume.path : image.path, 91 version: image.tag ?? DEFAULT_VERSION, 92 status: "STOPPED", 93 pid: 0, 94 }); 95 96 const createdVm = yield* findVm(id); 97 return createdVm; 98 }) 99 ), 100 presentation(c), 101 Effect.catchAll((error) => handleError(error, c)), 102 ), 103 )); 104 105app.get("/:id", (c) => 106 Effect.runPromise( 107 pipe( 108 parseParams(c), 109 Effect.flatMap(({ id }) => getInstanceState(id)), 110 presentation(c), 111 ), 112 )); 113 114app.delete("/:id", (c) => 115 Effect.runPromise( 116 pipe( 117 parseParams(c), 118 Effect.flatMap(({ id }) => findVm(id)), 119 Effect.flatMap((vm) => 120 vm.status === "RUNNING" 121 ? Effect.fail(new RemoveRunningVmError({ id: vm.id })) 122 : Effect.succeed(vm) 123 ), 124 Effect.flatMap((vm) => 125 Effect.gen(function* () { 126 yield* removeInstanceState(vm.id); 127 return vm; 128 }) 129 ), 130 presentation(c), 131 Effect.catchAll((error) => handleError(error, c)), 132 ), 133 )); 134 135app.post("/:id/start", (c) => 136 Effect.runPromise( 137 pipe( 138 Effect.all([parseParams(c), parseStartRequest(c)]), 139 Effect.flatMap(( 140 [{ id }, startRequest], 141 ) => Effect.all([findVm(id), Effect.succeed(startRequest)])), 142 Effect.flatMap(([vm, startRequest]) => 143 Effect.gen(function* () { 144 yield* failIfVMRunning(vm); 145 const mergedVm = { 146 ...vm, 147 cpu: String(startRequest.cpu ?? vm.cpu), 148 cpus: startRequest.cpus ?? vm.cpus, 149 memory: startRequest.memory ?? vm.memory, 150 portForward: startRequest.portForward 151 ? startRequest.portForward.join(",") 152 : vm.portForward, 153 }; 154 const qemuArgs = buildQemuArgs(mergedVm); 155 const logPath = `${LOGS_DIR}/${vm.name}.log`; 156 const fullCommand = buildDetachedCommand(vm, qemuArgs, logPath); 157 158 yield* createLogsDir(); 159 const qemuPid = yield* startDetachedQemu(fullCommand); 160 yield* updateInstanceState(vm.id, "RUNNING", qemuPid); 161 return { ...vm, status: "RUNNING" }; 162 }) 163 ), 164 presentation(c), 165 Effect.catchAll((error) => handleError(error, c)), 166 ), 167 )); 168 169app.post("/:id/stop", (c) => 170 Effect.runPromise( 171 pipe( 172 parseParams(c), 173 Effect.flatMap(({ id }) => findVm(id)), 174 Effect.flatMap(killProcess), 175 Effect.flatMap(updateToStopped), 176 presentation(c), 177 Effect.catchAll((error) => handleError(error, c)), 178 ), 179 )); 180 181app.post("/:id/restart", (c) => 182 Effect.runPromise( 183 pipe( 184 parseParams(c), 185 Effect.flatMap(({ id }) => findVm(id)), 186 Effect.flatMap(killProcess), 187 Effect.flatMap(updateToStopped), 188 Effect.flatMap(() => Effect.all([parseParams(c), parseStartRequest(c)])), 189 Effect.flatMap(( 190 [{ id }, startRequest], 191 ) => Effect.all([findVm(id), Effect.succeed(startRequest)])), 192 Effect.flatMap(([vm, startRequest]) => 193 Effect.gen(function* () { 194 yield* failIfVMRunning(vm); 195 const mergedVm = { 196 ...vm, 197 cpu: String(startRequest.cpu ?? vm.cpu), 198 cpus: startRequest.cpus ?? vm.cpus, 199 memory: startRequest.memory ?? vm.memory, 200 portForward: startRequest.portForward 201 ? startRequest.portForward.join(",") 202 : vm.portForward, 203 }; 204 const qemuArgs = buildQemuArgs(mergedVm); 205 const logPath = `${LOGS_DIR}/${vm.name}.log`; 206 const fullCommand = buildDetachedCommand(vm, qemuArgs, logPath); 207 208 yield* createLogsDir(); 209 const qemuPid = yield* startDetachedQemu(fullCommand); 210 yield* updateInstanceState(vm.id, "RUNNING", qemuPid); 211 return { ...vm, status: "RUNNING" }; 212 }) 213 ), 214 presentation(c), 215 Effect.catchAll((error) => handleError(error, c)), 216 ), 217 )); 218 219export default app;