A simple command-line tool to start NetBSD virtual machines using QEMU with sensible defaults.
at main 158 lines 4.0 kB view raw
1import { Data, Effect } from "effect"; 2import type { Context } from "hono"; 3import { 4 type CommandError, 5 StopCommandError, 6 VmNotFoundError, 7} from "../subcommands/stop.ts"; 8import { VmAlreadyRunningError } from "../subcommands/start.ts"; 9import { 10 MachineParamsSchema, 11 NewMachineSchema, 12 NewVolumeSchema, 13} from "../types.ts"; 14import type { Image, Volume } from "../db.ts"; 15import { createVolume, getVolume } from "../volumes.ts"; 16import { ImageNotFoundError, RemoveRunningVmError } from "./machines.ts"; 17 18export const parseQueryParams = (c: Context) => Effect.succeed(c.req.query()); 19 20export const parseParams = (c: Context) => Effect.succeed(c.req.param()); 21 22export const presentation = (c: Context) => 23 Effect.flatMap((data) => Effect.succeed(c.json(data))); 24 25export class ParseRequestError extends Data.TaggedError("ParseRequestError")<{ 26 cause?: unknown; 27 message: string; 28}> {} 29 30export const handleError = ( 31 error: 32 | VmNotFoundError 33 | StopCommandError 34 | CommandError 35 | ParseRequestError 36 | VmAlreadyRunningError 37 | ImageNotFoundError 38 | RemoveRunningVmError 39 | Error, 40 c: Context, 41) => 42 Effect.sync(() => { 43 if (error instanceof VmNotFoundError) { 44 return c.json( 45 { message: "VM not found", code: "VM_NOT_FOUND" }, 46 404, 47 ); 48 } 49 if (error instanceof StopCommandError) { 50 return c.json( 51 { 52 message: error.message || 53 `Failed to stop VM ${error.vmName}`, 54 code: "STOP_COMMAND_ERROR", 55 }, 56 500, 57 ); 58 } 59 60 if (error instanceof ParseRequestError) { 61 return c.json( 62 { 63 message: error.message || "Failed to parse request body", 64 code: "PARSE_BODY_ERROR", 65 }, 66 400, 67 ); 68 } 69 70 if (error instanceof VmAlreadyRunningError) { 71 return c.json( 72 { 73 message: `VM ${error.name} is already running`, 74 code: "VM_ALREADY_RUNNING", 75 }, 76 400, 77 ); 78 } 79 80 if (error instanceof ImageNotFoundError) { 81 return c.json( 82 { 83 message: `Image ${error.id} not found`, 84 code: "IMAGE_NOT_FOUND", 85 }, 86 404, 87 ); 88 } 89 90 if (error instanceof RemoveRunningVmError) { 91 return c.json( 92 { 93 message: 94 `Cannot remove running VM with ID ${error.id}. Please stop it first.`, 95 code: "REMOVE_RUNNING_VM_ERROR", 96 }, 97 400, 98 ); 99 } 100 101 return c.json( 102 { message: error instanceof Error ? error.message : String(error) }, 103 500, 104 ); 105 }); 106 107export const parseStartRequest = (c: Context) => 108 Effect.tryPromise({ 109 try: async () => { 110 const body = await c.req.json(); 111 return MachineParamsSchema.parse(body); 112 }, 113 catch: (error) => 114 new ParseRequestError({ 115 cause: error, 116 message: error instanceof Error ? error.message : String(error), 117 }), 118 }); 119 120export const parseCreateMachineRequest = (c: Context) => 121 Effect.tryPromise({ 122 try: async () => { 123 const body = await c.req.json(); 124 return NewMachineSchema.parse(body); 125 }, 126 catch: (error) => 127 new ParseRequestError({ 128 cause: error, 129 message: error instanceof Error ? error.message : String(error), 130 }), 131 }); 132 133export const createVolumeIfNeeded = ( 134 image: Image, 135 volumeName: string, 136 size?: string, 137): Effect.Effect<Volume, Error, never> => 138 Effect.gen(function* () { 139 const volume = yield* getVolume(volumeName); 140 if (volume) { 141 return volume; 142 } 143 144 return yield* createVolume(volumeName, image, size); 145 }); 146 147export const parseCreateVolumeRequest = (c: Context) => 148 Effect.tryPromise({ 149 try: async () => { 150 const body = await c.req.json(); 151 return NewVolumeSchema.parse(body); 152 }, 153 catch: (error) => 154 new ParseRequestError({ 155 cause: error, 156 message: error instanceof Error ? error.message : String(error), 157 }), 158 });