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

Refactor main logic and add utility functions for ISO download and drive image creation

+203 -161
+2 -1
deno.json
··· 5 5 "imports": { 6 6 "@cliffy/command": "jsr:@cliffy/command@^1.0.0-rc.8", 7 7 "@std/assert": "jsr:@std/assert@1", 8 - "chalk": "npm:chalk@^5.6.2" 8 + "chalk": "npm:chalk@^5.6.2", 9 + "lodash": "npm:lodash@^4.17.21" 9 10 } 10 11 }
+7 -2
deno.lock
··· 9 9 "jsr:@std/fmt@~1.0.2": "1.0.8", 10 10 "jsr:@std/internal@^1.0.12": "1.0.12", 11 11 "jsr:@std/text@~1.0.7": "1.0.16", 12 - "npm:chalk@^5.6.2": "5.6.2" 12 + "npm:chalk@^5.6.2": "5.6.2", 13 + "npm:lodash@^4.17.21": "4.17.21" 13 14 }, 14 15 "jsr": { 15 16 "@cliffy/command@1.0.0-rc.8": { ··· 56 57 "npm": { 57 58 "chalk@5.6.2": { 58 59 "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==" 60 + }, 61 + "lodash@4.17.21": { 62 + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" 59 63 } 60 64 }, 61 65 "workspace": { 62 66 "dependencies": [ 63 67 "jsr:@cliffy/command@^1.0.0-rc.8", 64 68 "jsr:@std/assert@1", 65 - "npm:chalk@^5.6.2" 69 + "npm:chalk@^5.6.2", 70 + "npm:lodash@^4.17.21" 66 71 ] 67 72 } 68 73 }
+14 -158
main.ts
··· 1 1 #!/usr/bin/env -S deno run --allow-run --allow-read --allow-env 2 2 3 3 import { Command } from "@cliffy/command"; 4 - import chalk from "chalk"; 5 - 6 - const DEFAULT_VERSION = "6.4.2"; 7 - 8 - interface Options { 9 - output?: string; 10 - cpu: string; 11 - cpus: number; 12 - memory: string; 13 - drive?: string; 14 - diskFormat: string; 15 - size: string; 16 - } 17 - 18 - async function downloadIso(url: string, outputPath?: string): Promise<string> { 19 - const filename = url.split("/").pop()!; 20 - outputPath = outputPath ?? filename; 21 - 22 - if (await Deno.stat(outputPath).catch(() => false)) { 23 - console.log( 24 - chalk.yellowBright( 25 - `File ${outputPath} already exists, skipping download.`, 26 - ), 27 - ); 28 - return outputPath; 29 - } 30 - 31 - const cmd = new Deno.Command("curl", { 32 - args: ["-L", "-o", outputPath, url], 33 - stdin: "inherit", 34 - stdout: "inherit", 35 - stderr: "inherit", 36 - }); 37 - 38 - const status = await cmd.spawn().status; 39 - if (!status.success) { 40 - console.error(chalk.redBright("Failed to download ISO image.")); 41 - Deno.exit(status.code); 42 - } 43 - 44 - console.log(chalk.greenBright(`Downloaded ISO to ${outputPath}`)); 45 - return outputPath; 46 - } 47 - 48 - function constructDownloadUrl(version: string): string { 49 - return `https://mirror-master.dragonflybsd.org/iso-images/dfly-x86_64-${version}_REL.iso`; 50 - } 51 - 52 - async function runQemu(isoPath: string, options: Options): Promise<void> { 53 - const cmd = new Deno.Command("qemu-system-x86_64", { 54 - args: [ 55 - "-enable-kvm", 56 - "-cpu", 57 - options.cpu, 58 - "-m", 59 - options.memory, 60 - "-smp", 61 - options.cpus.toString(), 62 - "-cdrom", 63 - isoPath, 64 - "-netdev", 65 - "user,id=net0,hostfwd=tcp::2222-:22", 66 - "-device", 67 - "e1000,netdev=net0", 68 - "-display", 69 - "none", 70 - "-vga", 71 - "none", 72 - "-monitor", 73 - "none", 74 - "-chardev", 75 - "stdio,id=con0,signal=off", 76 - "-serial", 77 - "chardev:con0", 78 - ...(options.drive 79 - ? [ 80 - "-drive", 81 - `file=${options.drive},format=${options.diskFormat},if=virtio`, 82 - ] 83 - : []), 84 - ], 85 - stdin: "inherit", 86 - stdout: "inherit", 87 - stderr: "inherit", 88 - }); 89 - 90 - const status = await cmd.spawn().status; 91 - 92 - if (!status.success) { 93 - Deno.exit(status.code); 94 - } 95 - } 96 - 97 - function handleInput(input?: string): string { 98 - if (!input) { 99 - console.log( 100 - `No ISO path provided, defaulting to ${chalk.cyan("DragonflyBSD")} ${ 101 - chalk.cyan(DEFAULT_VERSION) 102 - }...`, 103 - ); 104 - return constructDownloadUrl(DEFAULT_VERSION); 105 - } 106 - 107 - const versionRegex = /^\d{1,2}\.\d{1,2}$/; 108 - 109 - if (versionRegex.test(input)) { 110 - console.log( 111 - chalk.blueBright( 112 - `Detected version ${chalk.cyan(input)}, constructing download URL...`, 113 - ), 114 - ); 115 - return constructDownloadUrl(input); 116 - } 117 - 118 - return input; 119 - } 120 - 121 - async function createDriveImageIfNeeded( 122 - { 123 - drive: path, 124 - diskFormat: format, 125 - size, 126 - }: Options, 127 - ): Promise<void> { 128 - if (await Deno.stat(path!).catch(() => false)) { 129 - console.log( 130 - chalk.yellowBright( 131 - `Drive image ${path} already exists, skipping creation.`, 132 - ), 133 - ); 134 - return; 135 - } 136 - 137 - const cmd = new Deno.Command("qemu-img", { 138 - args: ["create", "-f", format, path!, size!], 139 - stdin: "inherit", 140 - stdout: "inherit", 141 - stderr: "inherit", 142 - }); 143 - 144 - const status = await cmd.spawn().status; 145 - if (!status.success) { 146 - console.error(chalk.redBright("Failed to create drive image.")); 147 - Deno.exit(status.code); 148 - } 149 - 150 - console.log(chalk.greenBright(`Created drive image at ${path}`)); 151 - } 4 + import { 5 + createDriveImageIfNeeded, 6 + downloadIso, 7 + handleInput, 8 + Options, 9 + runQemu, 10 + } from "./utils.ts"; 152 11 153 12 if (import.meta.main) { 154 13 await new Command() ··· 201 60 ) 202 61 .action(async (options: Options, input?: string) => { 203 62 const resolvedInput = handleInput(input); 204 - let isoPath = resolvedInput; 63 + let isoPath: string | null = resolvedInput; 205 64 206 65 if ( 207 66 resolvedInput.startsWith("https://") || 208 67 resolvedInput.startsWith("http://") 209 68 ) { 210 - isoPath = await downloadIso(resolvedInput, options.output); 69 + isoPath = await downloadIso(resolvedInput, options); 211 70 } 212 71 213 72 if (options.drive) { 214 73 await createDriveImageIfNeeded(options); 215 74 } 216 75 217 - await runQemu(isoPath, { 218 - cpu: options.cpu, 219 - memory: options.memory, 220 - cpus: options.cpus, 221 - drive: options.drive, 222 - diskFormat: options.diskFormat, 223 - size: options.size, 224 - }); 76 + if (!input && options.drive) { 77 + isoPath = null; 78 + } 79 + 80 + await runQemu(isoPath, options); 225 81 }) 226 82 .parse(Deno.args); 227 83 }
+180
utils.ts
··· 1 + import chalk from "chalk"; 2 + import _ from "lodash"; 3 + 4 + const DEFAULT_VERSION = "6.4.2"; 5 + 6 + export interface Options { 7 + output?: string; 8 + cpu: string; 9 + cpus: number; 10 + memory: string; 11 + drive?: string; 12 + diskFormat: string; 13 + size: string; 14 + } 15 + 16 + async function du(path: string): Promise<number> { 17 + const cmd = new Deno.Command("du", { 18 + args: [path], 19 + stdout: "piped", 20 + stderr: "inherit", 21 + }); 22 + 23 + const { stdout } = await cmd.spawn().output(); 24 + const output = new TextDecoder().decode(stdout).trim(); 25 + const size = parseInt(output.split("\t")[0], 10); 26 + return size; 27 + } 28 + 29 + export async function downloadIso( 30 + url: string, 31 + options: Options, 32 + ): Promise<string | null> { 33 + const filename = url.split("/").pop()!; 34 + const outputPath = options.output ?? filename; 35 + 36 + if (options.drive && await Deno.stat(options.drive).catch(() => false)) { 37 + const driveSize = await du(options.drive); 38 + if (driveSize > 10) { 39 + console.log( 40 + chalk.yellowBright( 41 + `Drive image ${options.drive} is not empty (size: ${driveSize} KB), skipping ISO download to avoid overwriting existing data.`, 42 + ), 43 + ); 44 + return null; 45 + } 46 + } 47 + 48 + if (await Deno.stat(outputPath).catch(() => false)) { 49 + console.log( 50 + chalk.yellowBright( 51 + `File ${outputPath} already exists, skipping download.`, 52 + ), 53 + ); 54 + return outputPath; 55 + } 56 + 57 + const cmd = new Deno.Command("curl", { 58 + args: ["-L", "-o", outputPath, url], 59 + stdin: "inherit", 60 + stdout: "inherit", 61 + stderr: "inherit", 62 + }); 63 + 64 + const status = await cmd.spawn().status; 65 + if (!status.success) { 66 + console.error(chalk.redBright("Failed to download ISO image.")); 67 + Deno.exit(status.code); 68 + } 69 + 70 + console.log(chalk.greenBright(`Downloaded ISO to ${outputPath}`)); 71 + return outputPath; 72 + } 73 + 74 + export function constructDownloadUrl(version: string): string { 75 + return `https://mirror-master.dragonflybsd.org/iso-images/dfly-x86_64-${version}_REL.iso`; 76 + } 77 + 78 + export async function runQemu( 79 + isoPath: string | null, 80 + options: Options, 81 + ): Promise<void> { 82 + const cmd = new Deno.Command("qemu-system-x86_64", { 83 + args: [ 84 + "-enable-kvm", 85 + "-cpu", 86 + options.cpu, 87 + "-m", 88 + options.memory, 89 + "-smp", 90 + options.cpus.toString(), 91 + options.cpus.toString(), 92 + ..._.compact([isoPath && "-cdrom", isoPath]), 93 + "-netdev", 94 + "user,id=net0,hostfwd=tcp::2222-:22", 95 + "-device", 96 + "e1000,netdev=net0", 97 + "-display", 98 + "none", 99 + "-vga", 100 + "none", 101 + "-monitor", 102 + "none", 103 + "-chardev", 104 + "stdio,id=con0,signal=off", 105 + "-serial", 106 + "chardev:con0", 107 + ..._.compact( 108 + options.drive && [ 109 + "-drive", 110 + `file=${options.drive},format=${options.diskFormat},if=virtio`, 111 + ], 112 + ), 113 + ], 114 + stdin: "inherit", 115 + stdout: "inherit", 116 + stderr: "inherit", 117 + }); 118 + 119 + const status = await cmd.spawn().status; 120 + 121 + if (!status.success) { 122 + Deno.exit(status.code); 123 + } 124 + } 125 + 126 + export function handleInput(input?: string): string { 127 + if (!input) { 128 + console.log( 129 + `No ISO path provided, defaulting to ${chalk.cyan("DragonflyBSD")} ${ 130 + chalk.cyan(DEFAULT_VERSION) 131 + }...`, 132 + ); 133 + return constructDownloadUrl(DEFAULT_VERSION); 134 + } 135 + 136 + const versionRegex = /^\d{1,2}\.\d{1,2}$/; 137 + 138 + if (versionRegex.test(input)) { 139 + console.log( 140 + chalk.blueBright( 141 + `Detected version ${chalk.cyan(input)}, constructing download URL...`, 142 + ), 143 + ); 144 + return constructDownloadUrl(input); 145 + } 146 + 147 + return input; 148 + } 149 + 150 + export async function createDriveImageIfNeeded( 151 + { 152 + drive: path, 153 + diskFormat: format, 154 + size, 155 + }: Options, 156 + ): Promise<void> { 157 + if (await Deno.stat(path!).catch(() => false)) { 158 + console.log( 159 + chalk.yellowBright( 160 + `Drive image ${path} already exists, skipping creation.`, 161 + ), 162 + ); 163 + return; 164 + } 165 + 166 + const cmd = new Deno.Command("qemu-img", { 167 + args: ["create", "-f", format, path!, size!], 168 + stdin: "inherit", 169 + stdout: "inherit", 170 + stderr: "inherit", 171 + }); 172 + 173 + const status = await cmd.spawn().status; 174 + if (!status.success) { 175 + console.error(chalk.redBright("Failed to create drive image.")); 176 + Deno.exit(status.code); 177 + } 178 + 179 + console.log(chalk.greenBright(`Created drive image at ${path}`)); 180 + }