A simple, zero-configuration script to quickly boot FreeBSD ISO images using QEMU
at main 129 lines 3.4 kB view raw
1import { createId } from "@paralleldrive/cuid2"; 2import { Data, Effect } from "effect"; 3import type { DeleteResult, InsertResult } from "kysely"; 4import { VOLUME_DIR } from "./constants.ts"; 5import { ctx } from "./context.ts"; 6import type { Image, Volume } from "./db.ts"; 7 8export class VolumeError extends Data.TaggedError("VolumeError")<{ 9 message?: unknown; 10}> {} 11 12export const listVolumes = () => 13 Effect.tryPromise({ 14 try: () => ctx.db.selectFrom("volumes").selectAll().execute(), 15 catch: (error) => 16 new VolumeError({ 17 message: error instanceof Error ? error.message : String(error), 18 }), 19 }); 20 21export const getVolume = ( 22 id: string, 23): Effect.Effect<Volume | undefined, VolumeError, never> => 24 Effect.tryPromise({ 25 try: () => 26 ctx.db 27 .selectFrom("volumes") 28 .selectAll() 29 .where((eb) => 30 eb.or([ 31 eb("name", "=", id), 32 eb("id", "=", id), 33 eb("path", "=", id), 34 ]) 35 ) 36 .executeTakeFirst(), 37 catch: (error) => 38 new VolumeError({ 39 message: error instanceof Error ? error.message : String(error), 40 }), 41 }); 42 43export const saveVolume = ( 44 volume: Volume, 45): Effect.Effect<InsertResult[], VolumeError, never> => 46 Effect.tryPromise({ 47 try: () => 48 ctx.db.insertInto("volumes") 49 .values(volume) 50 .execute(), 51 catch: (error) => 52 new VolumeError({ 53 message: error instanceof Error ? error.message : String(error), 54 }), 55 }); 56 57export const deleteVolume = ( 58 id: string, 59): Effect.Effect<DeleteResult[], VolumeError, never> => 60 Effect.tryPromise({ 61 try: () => 62 ctx.db.deleteFrom("volumes").where((eb) => 63 eb.or([ 64 eb("name", "=", id), 65 eb("id", "=", id), 66 ]) 67 ).execute(), 68 catch: (error) => 69 new VolumeError({ 70 message: error instanceof Error ? error.message : String(error), 71 }), 72 }); 73 74export const createVolume = ( 75 name: string, 76 baseImage: Image, 77 size?: string, 78): Effect.Effect<Volume, VolumeError, never> => 79 Effect.tryPromise({ 80 try: async () => { 81 const path = `${VOLUME_DIR}/${name}.qcow2`; 82 83 if (!(await Deno.stat(path).catch(() => false))) { 84 await Deno.mkdir(VOLUME_DIR, { recursive: true }); 85 const qemu = new Deno.Command("qemu-img", { 86 args: [ 87 "create", 88 "-F", 89 "raw", 90 "-f", 91 "qcow2", 92 "-b", 93 baseImage.path, 94 path, 95 ...(size ? [size] : []), 96 ], 97 stdout: "inherit", 98 stderr: "inherit", 99 }) 100 .spawn(); 101 const status = await qemu.status; 102 if (!status.success) { 103 throw new Error( 104 `Failed to create volume: qemu-img exited with code ${status.code}`, 105 ); 106 } 107 } 108 109 ctx.db.insertInto("volumes").values({ 110 id: createId(), 111 name, 112 path, 113 baseImageId: baseImage.id, 114 }).execute(); 115 const volume = await ctx.db 116 .selectFrom("volumes") 117 .selectAll() 118 .where("name", "=", name) 119 .executeTakeFirst(); 120 if (!volume) { 121 throw new Error("Failed to create volume"); 122 } 123 return volume; 124 }, 125 catch: (error) => 126 new VolumeError({ 127 message: error instanceof Error ? error.message : String(error), 128 }), 129 });