A simple command-line tool to start NetBSD virtual machines using QEMU with sensible defaults.
1import { parseFlags } from "@cliffy/flags";
2import { Effect, pipe } from "effect";
3import type { Image, Volume } from "../db.ts";
4import { getImage } from "../images.ts";
5import { createBridgeNetworkIfNeeded } from "../network.ts";
6import { pullImage, PullImageError, setupOrasBinary } from "../oras.ts";
7import { type Options, runQemu, validateImage } from "../utils.ts";
8import { createVolume, getVolume } from "../volumes.ts";
9
10const pullImageOnMissing = (
11 name: string,
12): Effect.Effect<Image, Error, never> =>
13 pipe(
14 getImage(name),
15 Effect.flatMap((img) => {
16 if (img) {
17 return Effect.succeed(img);
18 }
19 console.log(`Image ${name} not found locally`);
20 return pipe(
21 pullImage(name),
22 Effect.flatMap(() => getImage(name)),
23 Effect.flatMap((pulledImg) =>
24 pulledImg ? Effect.succeed(pulledImg) : Effect.fail(
25 new PullImageError({ cause: "Failed to pull image" }),
26 )
27 ),
28 );
29 }),
30 );
31
32const createVolumeIfNeeded = (
33 image: Image,
34): Effect.Effect<[Image, Volume?], Error, never> =>
35 parseFlags(Deno.args).flags.volume
36 ? Effect.gen(function* () {
37 const volumeName = parseFlags(Deno.args).flags.volume as string;
38 const volume = yield* getVolume(volumeName);
39 if (volume) {
40 return [image, volume];
41 }
42 const newVolume = yield* createVolume(volumeName, image);
43 return [image, newVolume];
44 })
45 : Effect.succeed([image]);
46
47const runImage = ([image, volume]: [Image, Volume?]) =>
48 Effect.gen(function* () {
49 console.log(`Running image ${image.repository}...`);
50 const options = mergeFlags(image);
51 if (options.bridge) {
52 yield* createBridgeNetworkIfNeeded(options.bridge);
53 }
54
55 if (volume) {
56 options.image = volume.path;
57 options.install = true;
58 options.diskFormat = "qcow2";
59 }
60
61 yield* runQemu(null, options);
62 });
63
64export default async function (
65 image: string,
66): Promise<void> {
67 await Effect.runPromise(
68 pipe(
69 Effect.promise(() => setupOrasBinary()),
70 Effect.tap(() => validateImage(image)),
71 Effect.flatMap(() => pullImageOnMissing(image)),
72 Effect.flatMap(createVolumeIfNeeded),
73 Effect.flatMap(runImage),
74 Effect.catchAll((error) =>
75 Effect.sync(() => {
76 console.error(`Failed to run image: ${error.cause} ${image}`);
77 Deno.exit(1);
78 })
79 ),
80 ),
81 );
82}
83
84function mergeFlags(image: Image): Options {
85 const { flags } = parseFlags(Deno.args);
86 return {
87 cpu: (flags.cpu || flags.c) ? (flags.cpu || flags.c) : "host",
88 cpus: (flags.cpus || flags.C) ? (flags.cpus || flags.C) : 2,
89 memory: (flags.memory || flags.m) ? (flags.memory || flags.m) : "2G",
90 image: image.path,
91 bridge: flags.bridge || flags.b,
92 portForward: flags.portForward || flags.p,
93 detach: flags.detach || flags.d,
94 install: false,
95 diskFormat: image.format,
96 volume: flags.volume || flags.v,
97 };
98}