A convenient CLI tool to quickly spin up DragonflyBSD virtual machines using QEMU with sensible defaults.

Refactor network setup functions to use Effect for improved error handling and control flow

+144 -123
+24 -21
main.ts
··· 104 104 "dflybsd-up inspect my-vm", 105 105 ) 106 106 .action(async (options: Options, input?: string) => { 107 - const resolvedInput = handleInput(input); 108 - let isoPath: string | null = resolvedInput; 107 + const program = Effect.gen(function* () { 108 + const resolvedInput = handleInput(input); 109 + let isoPath: string | null = resolvedInput; 109 110 110 - if ( 111 - resolvedInput.startsWith("https://") || 112 - resolvedInput.startsWith("http://") 113 - ) { 114 - isoPath = await Effect.runPromise(downloadIso(resolvedInput, options)); 115 - } 111 + if ( 112 + resolvedInput.startsWith("https://") || 113 + resolvedInput.startsWith("http://") 114 + ) { 115 + isoPath = yield* downloadIso(resolvedInput, options); 116 + } 116 117 117 - if (options.image) { 118 - await Effect.runPromise(createDriveImageIfNeeded(options)); 119 - } 118 + if (options.image) { 119 + yield* createDriveImageIfNeeded(options); 120 + } 120 121 121 - if ( 122 - !input && options.image && 123 - !await Effect.runPromise(emptyDiskImage(options.image)) 124 - ) { 125 - isoPath = null; 126 - } 122 + if ( 123 + !input && options.image && 124 + !(yield* emptyDiskImage(options.image)) 125 + ) { 126 + isoPath = null; 127 + } 127 128 128 - if (options.bridge) { 129 - await createBridgeNetworkIfNeeded(options.bridge); 130 - } 129 + if (options.bridge) { 130 + yield* createBridgeNetworkIfNeeded(options.bridge); 131 + } 132 + yield* runQemu(isoPath, options); 133 + }); 131 134 132 - await Effect.runPromise(runQemu(isoPath, options)); 135 + await Effect.runPromise(program); 133 136 }) 134 137 .command("ps", "List all virtual machines") 135 138 .option("--all, -a", "Show all virtual machines, including stopped ones")
+120 -102
src/network.ts
··· 1 1 import chalk from "chalk"; 2 + import { Data, Effect } from "effect"; 2 3 3 - export async function setupQemuBridge(bridgeName: string): Promise<void> { 4 - const bridgeConfPath = "/etc/qemu/bridge.conf"; 5 - const bridgeConfContent = await Deno.readTextFile(bridgeConfPath).catch( 6 - () => "", 7 - ); 8 - if (bridgeConfContent.includes(`allow ${bridgeName}`)) { 9 - console.log( 10 - chalk.greenBright( 11 - `QEMU bridge configuration for ${bridgeName} already exists.`, 12 - ), 13 - ); 14 - return; 15 - } 4 + export class NetworkError extends Data.TaggedError("NetworkError")<{ 5 + cause?: unknown; 6 + }> {} 16 7 17 - console.log( 18 - chalk.blueBright( 19 - `Adding QEMU bridge configuration for ${bridgeName}...`, 20 - ), 21 - ); 8 + export class BridgeSetupError extends Data.TaggedError("BridgeSetupError")<{ 9 + cause?: unknown; 10 + }> {} 22 11 23 - const cmd = new Deno.Command("sudo", { 24 - args: [ 25 - "sh", 26 - "-c", 27 - `mkdir -p /etc/qemu && echo "allow ${bridgeName}" >> ${bridgeConfPath}`, 28 - ], 29 - stdin: "inherit", 30 - stdout: "inherit", 31 - stderr: "inherit", 32 - }); 33 - const status = await cmd.spawn().status; 12 + export const setupQemuBridge = (bridgeName: string) => 13 + Effect.tryPromise({ 14 + try: async () => { 15 + const bridgeConfPath = "/etc/qemu/bridge.conf"; 16 + const bridgeConfContent = await Deno.readTextFile(bridgeConfPath).catch( 17 + () => "", 18 + ); 19 + if (bridgeConfContent.includes(`allow ${bridgeName}`)) { 20 + console.log( 21 + chalk.greenBright( 22 + `QEMU bridge configuration for ${bridgeName} already exists.`, 23 + ), 24 + ); 25 + return; 26 + } 34 27 35 - if (!status.success) { 36 - console.error( 37 - chalk.redBright( 38 - `Failed to add QEMU bridge configuration for ${bridgeName}.`, 39 - ), 40 - ); 41 - Deno.exit(status.code); 42 - } 28 + console.log( 29 + chalk.blueBright( 30 + `Adding QEMU bridge configuration for ${bridgeName}...`, 31 + ), 32 + ); 43 33 44 - console.log( 45 - chalk.greenBright( 46 - `QEMU bridge configuration for ${bridgeName} added successfully.`, 47 - ), 48 - ); 49 - } 34 + const cmd = new Deno.Command("sudo", { 35 + args: [ 36 + "sh", 37 + "-c", 38 + `mkdir -p /etc/qemu && echo "allow ${bridgeName}" >> ${bridgeConfPath}`, 39 + ], 40 + stdin: "inherit", 41 + stdout: "inherit", 42 + stderr: "inherit", 43 + }); 44 + const status = await cmd.spawn().status; 50 45 51 - export async function createBridgeNetworkIfNeeded( 52 - bridgeName: string, 53 - ): Promise<void> { 54 - const bridgeExistsCmd = new Deno.Command("ip", { 55 - args: ["link", "show", bridgeName], 56 - stdout: "null", 57 - stderr: "null", 58 - }); 46 + if (!status.success) { 47 + console.error( 48 + chalk.redBright( 49 + `Failed to add QEMU bridge configuration for ${bridgeName}.`, 50 + ), 51 + ); 52 + Deno.exit(status.code); 53 + } 59 54 60 - const bridgeExistsStatus = await bridgeExistsCmd.spawn().status; 61 - if (bridgeExistsStatus.success) { 62 - console.log( 63 - chalk.greenBright(`Network bridge ${bridgeName} already exists.`), 64 - ); 65 - await setupQemuBridge(bridgeName); 66 - return; 67 - } 68 - 69 - console.log(chalk.blueBright(`Creating network bridge ${bridgeName}...`)); 70 - const createBridgeCmd = new Deno.Command("sudo", { 71 - args: ["ip", "link", "add", bridgeName, "type", "bridge"], 72 - stdin: "inherit", 73 - stdout: "inherit", 74 - stderr: "inherit", 55 + console.log( 56 + chalk.greenBright( 57 + `QEMU bridge configuration for ${bridgeName} added successfully.`, 58 + ), 59 + ); 60 + }, 61 + catch: (error) => new BridgeSetupError({ cause: error }), 75 62 }); 76 63 77 - let status = await createBridgeCmd.spawn().status; 78 - if (!status.success) { 79 - console.error( 80 - chalk.redBright(`Failed to create network bridge ${bridgeName}.`), 81 - ); 82 - Deno.exit(status.code); 83 - } 64 + export const createBridgeNetworkIfNeeded = ( 65 + bridgeName: string, 66 + ) => 67 + Effect.tryPromise({ 68 + try: async () => { 69 + const bridgeExistsCmd = new Deno.Command("ip", { 70 + args: ["link", "show", bridgeName], 71 + stdout: "null", 72 + stderr: "null", 73 + }); 84 74 85 - const bringUpBridgeCmd = new Deno.Command("sudo", { 86 - args: ["ip", "link", "set", "dev", bridgeName, "up"], 87 - stdin: "inherit", 88 - stdout: "inherit", 89 - stderr: "inherit", 90 - }); 91 - status = await bringUpBridgeCmd.spawn().status; 92 - if (!status.success) { 93 - console.error( 94 - chalk.redBright(`Failed to bring up network bridge ${bridgeName}.`), 95 - ); 96 - Deno.exit(status.code); 97 - } 75 + const bridgeExistsStatus = await bridgeExistsCmd.spawn().status; 76 + if (bridgeExistsStatus.success) { 77 + console.log( 78 + chalk.greenBright(`Network bridge ${bridgeName} already exists.`), 79 + ); 80 + await setupQemuBridge(bridgeName); 81 + return; 82 + } 98 83 99 - console.log( 100 - chalk.greenBright(`Network bridge ${bridgeName} created and up.`), 101 - ); 84 + console.log(chalk.blueBright(`Creating network bridge ${bridgeName}...`)); 85 + const createBridgeCmd = new Deno.Command("sudo", { 86 + args: ["ip", "link", "add", bridgeName, "type", "bridge"], 87 + stdin: "inherit", 88 + stdout: "inherit", 89 + stderr: "inherit", 90 + }); 102 91 103 - await setupQemuBridge(bridgeName); 104 - } 92 + let status = await createBridgeCmd.spawn().status; 93 + if (!status.success) { 94 + console.error( 95 + chalk.redBright(`Failed to create network bridge ${bridgeName}.`), 96 + ); 97 + Deno.exit(status.code); 98 + } 105 99 106 - export function generateRandomMacAddress(): string { 107 - const hexDigits = "0123456789ABCDEF"; 108 - let macAddress = "52:54:00"; 100 + const bringUpBridgeCmd = new Deno.Command("sudo", { 101 + args: ["ip", "link", "set", "dev", bridgeName, "up"], 102 + stdin: "inherit", 103 + stdout: "inherit", 104 + stderr: "inherit", 105 + }); 106 + status = await bringUpBridgeCmd.spawn().status; 107 + if (!status.success) { 108 + console.error( 109 + chalk.redBright(`Failed to bring up network bridge ${bridgeName}.`), 110 + ); 111 + Deno.exit(status.code); 112 + } 109 113 110 - for (let i = 0; i < 3; i++) { 111 - macAddress += ":"; 112 - for (let j = 0; j < 2; j++) { 113 - macAddress += hexDigits.charAt( 114 - Math.floor(Math.random() * hexDigits.length), 114 + console.log( 115 + chalk.greenBright(`Network bridge ${bridgeName} created and up.`), 115 116 ); 117 + 118 + await setupQemuBridge(bridgeName); 119 + }, 120 + catch: (error) => new NetworkError({ cause: error }), 121 + }); 122 + 123 + export const generateRandomMacAddress = () => 124 + Effect.sync(() => { 125 + const hexDigits = "0123456789ABCDEF"; 126 + let macAddress = "52:54:00"; 127 + 128 + for (let i = 0; i < 3; i++) { 129 + macAddress += ":"; 130 + for (let j = 0; j < 2; j++) { 131 + macAddress += hexDigits.charAt( 132 + Math.floor(Math.random() * hexDigits.length), 133 + ); 134 + } 116 135 } 117 - } 118 136 119 - return macAddress; 120 - } 137 + return macAddress; 138 + });