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