A simple command-line tool to start NetBSD virtual machines using QEMU with sensible defaults.
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 });