A simple, zero-configuration script to quickly boot FreeBSD ISO images using QEMU

Add database support and virtual machine management commands

- Introduced database functionality with SQLite for managing virtual machines.
- Added commands for starting, stopping, inspecting, and listing virtual machines.
- Implemented state management for virtual machines, including saving and updating their status.
- Enhanced the utility functions to generate random MAC addresses and handle disk images.
- Organized code structure by creating separate files for constants, context, and database operations.

+503 -12
+9 -2
deno.json
··· 5 5 "imports": { 6 6 "@cliffy/command": "jsr:@cliffy/command@^1.0.0-rc.8", 7 7 "@cliffy/flags": "jsr:@cliffy/flags@^1.0.0-rc.8", 8 + "@cliffy/table": "jsr:@cliffy/table@^1.0.0-rc.8", 9 + "@db/sqlite": "jsr:@db/sqlite@^0.12.0", 10 + "@paralleldrive/cuid2": "npm:@paralleldrive/cuid2@^3.0.4", 11 + "@soapbox/kysely-deno-sqlite": "jsr:@soapbox/kysely-deno-sqlite@^2.2.0", 8 12 "@std/assert": "jsr:@std/assert@1", 9 13 "chalk": "npm:chalk@^5.6.2", 10 - "lodash": "npm:lodash@^4.17.21" 14 + "dayjs": "npm:dayjs@^1.11.19", 15 + "kysely": "npm:kysely@0.27.6", 16 + "lodash": "npm:lodash@^4.17.21", 17 + "moniker": "npm:moniker@^0.1.2" 11 18 } 12 - } 19 + }
+105 -6
deno.lock
··· 6 6 "jsr:@cliffy/flags@^1.0.0-rc.8": "1.0.0-rc.8", 7 7 "jsr:@cliffy/internal@1.0.0-rc.8": "1.0.0-rc.8", 8 8 "jsr:@cliffy/table@1.0.0-rc.8": "1.0.0-rc.8", 9 + "jsr:@cliffy/table@^1.0.0-rc.8": "1.0.0-rc.8", 10 + "jsr:@db/sqlite@0.12": "0.12.0", 11 + "jsr:@denosaurs/plug@1": "1.1.0", 12 + "jsr:@soapbox/kysely-deno-sqlite@^2.2.0": "2.2.0", 13 + "jsr:@std/assert@0.217": "0.217.0", 9 14 "jsr:@std/assert@1": "1.0.15", 15 + "jsr:@std/encoding@1": "1.0.10", 16 + "jsr:@std/fmt@1": "1.0.8", 10 17 "jsr:@std/fmt@~1.0.2": "1.0.8", 18 + "jsr:@std/fs@1": "1.0.19", 19 + "jsr:@std/internal@^1.0.10": "1.0.12", 11 20 "jsr:@std/internal@^1.0.12": "1.0.12", 21 + "jsr:@std/internal@^1.0.9": "1.0.12", 22 + "jsr:@std/path@0.217": "0.217.0", 23 + "jsr:@std/path@1": "1.1.2", 24 + "jsr:@std/path@^1.1.1": "1.1.2", 12 25 "jsr:@std/text@~1.0.7": "1.0.15", 26 + "npm:@paralleldrive/cuid2@^3.0.4": "3.0.4", 13 27 "npm:chalk@^5.6.2": "5.6.2", 14 - "npm:lodash@^4.17.21": "4.17.21" 28 + "npm:dayjs@^1.11.19": "1.11.19", 29 + "npm:kysely@0.27.6": "0.27.6", 30 + "npm:kysely@~0.27.2": "0.27.6", 31 + "npm:lodash@^4.17.21": "4.17.21", 32 + "npm:moniker@~0.1.2": "0.1.2" 15 33 }, 16 34 "jsr": { 17 35 "@cliffy/command@1.0.0-rc.8": { ··· 19 37 "dependencies": [ 20 38 "jsr:@cliffy/flags@1.0.0-rc.8", 21 39 "jsr:@cliffy/internal", 22 - "jsr:@cliffy/table", 23 - "jsr:@std/fmt", 40 + "jsr:@cliffy/table@1.0.0-rc.8", 41 + "jsr:@std/fmt@~1.0.2", 24 42 "jsr:@std/text" 25 43 ] 26 44 }, ··· 36 54 "@cliffy/table@1.0.0-rc.8": { 37 55 "integrity": "8bbcdc2ba5e0061b4b13810a24e6f5c6ab19c09f0cce9eb691ccd76c7c6c9db5", 38 56 "dependencies": [ 39 - "jsr:@std/fmt" 57 + "jsr:@std/fmt@~1.0.2" 40 58 ] 41 59 }, 60 + "@db/sqlite@0.12.0": { 61 + "integrity": "dd1ef7f621ad50fc1e073a1c3609c4470bd51edc0994139c5bf9851de7a6d85f", 62 + "dependencies": [ 63 + "jsr:@denosaurs/plug", 64 + "jsr:@std/path@0.217" 65 + ] 66 + }, 67 + "@denosaurs/plug@1.1.0": { 68 + "integrity": "eb2f0b7546c7bca2000d8b0282c54d50d91cf6d75cb26a80df25a6de8c4bc044", 69 + "dependencies": [ 70 + "jsr:@std/encoding", 71 + "jsr:@std/fmt@1", 72 + "jsr:@std/fs", 73 + "jsr:@std/path@1" 74 + ] 75 + }, 76 + "@soapbox/kysely-deno-sqlite@2.2.0": { 77 + "integrity": "668ec94600bc4b4d7bd618dd7ca65d4ef30ee61c46ffcb379b6f45203c08517a", 78 + "dependencies": [ 79 + "npm:kysely@~0.27.2" 80 + ] 81 + }, 82 + "@std/assert@0.217.0": { 83 + "integrity": "c98e279362ca6982d5285c3b89517b757c1e3477ee9f14eb2fdf80a45aaa9642" 84 + }, 42 85 "@std/assert@1.0.15": { 43 86 "integrity": "d64018e951dbdfab9777335ecdb000c0b4e3df036984083be219ce5941e4703b", 44 87 "dependencies": [ 45 - "jsr:@std/internal" 88 + "jsr:@std/internal@^1.0.12" 46 89 ] 90 + }, 91 + "@std/encoding@1.0.10": { 92 + "integrity": "8783c6384a2d13abd5e9e87a7ae0520a30e9f56aeeaa3bdf910a3eaaf5c811a1" 47 93 }, 48 94 "@std/fmt@1.0.8": { 49 95 "integrity": "71e1fc498787e4434d213647a6e43e794af4fd393ef8f52062246e06f7e372b7" 50 96 }, 97 + "@std/fs@1.0.19": { 98 + "integrity": "051968c2b1eae4d2ea9f79a08a3845740ef6af10356aff43d3e2ef11ed09fb06", 99 + "dependencies": [ 100 + "jsr:@std/internal@^1.0.9", 101 + "jsr:@std/path@^1.1.1" 102 + ] 103 + }, 51 104 "@std/internal@1.0.12": { 52 105 "integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027" 53 106 }, 107 + "@std/path@0.217.0": { 108 + "integrity": "1217cc25534bca9a2f672d7fe7c6f356e4027df400c0e85c0ef3e4343bc67d11", 109 + "dependencies": [ 110 + "jsr:@std/assert@0.217" 111 + ] 112 + }, 113 + "@std/path@1.1.2": { 114 + "integrity": "c0b13b97dfe06546d5e16bf3966b1cadf92e1cc83e56ba5476ad8b498d9e3038", 115 + "dependencies": [ 116 + "jsr:@std/internal@^1.0.10" 117 + ] 118 + }, 54 119 "@std/text@1.0.15": { 55 120 "integrity": "91f5cc1e12779a3d95f1be34e763f9c28a75a078b7360e6fcaef0d8d9b1e3e7f" 56 121 } 57 122 }, 58 123 "npm": { 124 + "@noble/hashes@2.0.1": { 125 + "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==" 126 + }, 127 + "@paralleldrive/cuid2@3.0.4": { 128 + "integrity": "sha512-sM6M2PWrByOEpN2QYAdulhEbSZmChwj0e52u4hpwB7u4PznFiNAavtE6m7O8tWUlzX+jT2eKKtc5/ZgX+IHrtg==", 129 + "dependencies": [ 130 + "@noble/hashes", 131 + "bignumber.js", 132 + "error-causes" 133 + ], 134 + "bin": true 135 + }, 136 + "bignumber.js@9.3.1": { 137 + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==" 138 + }, 59 139 "chalk@5.6.2": { 60 140 "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==" 61 141 }, 142 + "dayjs@1.11.19": { 143 + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==" 144 + }, 145 + "error-causes@3.0.2": { 146 + "integrity": "sha512-i0B8zq1dHL6mM85FGoxaJnVtx6LD5nL2v0hlpGdntg5FOSyzQ46c9lmz5qx0xRS2+PWHGOHcYxGIBC5Le2dRMw==" 147 + }, 148 + "kysely@0.27.6": { 149 + "integrity": "sha512-FIyV/64EkKhJmjgC0g2hygpBv5RNWVPyNCqSAD7eTCv6eFWNIi4PN1UvdSJGicN/o35bnevgis4Y0UDC0qi8jQ==" 150 + }, 62 151 "lodash@4.17.21": { 63 152 "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" 153 + }, 154 + "moniker@0.1.2": { 155 + "integrity": "sha512-Uj9iV0QYr6281G+o0TvqhKwHHWB2Q/qUTT4LPQ3qDGc0r8cbMuqQjRXPZuVZ+gcL7APx+iQgE8lcfWPrj1LsLA==" 64 156 } 65 157 }, 66 158 "workspace": { 67 159 "dependencies": [ 68 160 "jsr:@cliffy/command@^1.0.0-rc.8", 69 161 "jsr:@cliffy/flags@^1.0.0-rc.8", 162 + "jsr:@cliffy/table@^1.0.0-rc.8", 163 + "jsr:@db/sqlite@0.12", 164 + "jsr:@soapbox/kysely-deno-sqlite@^2.2.0", 70 165 "jsr:@std/assert@1", 166 + "npm:@paralleldrive/cuid2@^3.0.4", 71 167 "npm:chalk@^5.6.2", 72 - "npm:lodash@^4.17.21" 168 + "npm:dayjs@^1.11.19", 169 + "npm:kysely@0.27.6", 170 + "npm:lodash@^4.17.21", 171 + "npm:moniker@~0.1.2" 73 172 ] 74 173 } 75 174 }
+45 -1
main.ts
··· 2 2 3 3 import { Command } from "@cliffy/command"; 4 4 import { createBridgeNetworkIfNeeded } from "./src/network.ts"; 5 + import inspect from "./src/subcommands/inspect.ts"; 6 + import ps from "./src/subcommands/ps.ts"; 7 + import start from "./src/subcommands/start.ts"; 8 + import stop from "./src/subcommands/stop.ts"; 5 9 import { 6 10 createDriveImageIfNeeded, 7 11 downloadIso, 8 12 emptyDiskImage, 9 13 handleInput, 10 - Options, 14 + type Options, 11 15 runQemu, 12 16 } from "./src/utils.ts"; 13 17 ··· 64 68 "Download URL", 65 69 "freebsd-up https://download.freebsd.org/ftp/releases/ISO-IMAGES/14.3/FreeBSD-14.3-RELEASE-amd64-disc1.iso", 66 70 ) 71 + .example( 72 + "List running VMs", 73 + "freebsd-up ps", 74 + ) 75 + .example( 76 + "List all VMs", 77 + "freebsd-up ps --all", 78 + ) 79 + .example( 80 + "Start a VM", 81 + "freebsd-up start my-vm", 82 + ) 83 + .example( 84 + "Stop a VM", 85 + "freebsd-up stop my-vm", 86 + ) 87 + .example( 88 + "Inspect a VM", 89 + "freebsd-up inspect my-vm", 90 + ) 67 91 .action(async (options: Options, input?: string) => { 68 92 const resolvedInput = handleInput(input); 69 93 let isoPath: string | null = resolvedInput; ··· 88 112 } 89 113 90 114 await runQemu(isoPath, options); 115 + }) 116 + .command("ps", "List all virtual machines") 117 + .option("--all, -a", "Show all virtual machines, including stopped ones") 118 + .action(async (options: { all: boolean }) => { 119 + await ps(options.all); 120 + }) 121 + .command("start", "Start a virtual machine") 122 + .arguments("<vm-name:string>") 123 + .action(async (_options: unknown, vmName: string) => { 124 + await start(vmName); 125 + }) 126 + .command("stop", "Stop a virtual machine") 127 + .arguments("<vm-name:string>") 128 + .action(async (_options: unknown, vmName: string) => { 129 + await stop(vmName); 130 + }) 131 + .command("inspect", "Inspect a virtual machine") 132 + .arguments("<vm-name:string>") 133 + .action(async (_options: unknown, vmName: string) => { 134 + await inspect(vmName); 91 135 }) 92 136 .parse(Deno.args); 93 137 }
+2
src/constants.ts
··· 1 + export const CONFIG_DIR = `${Deno.env.get("HOME")}/.freebsd-up`; 2 + export const DB_PATH = `${CONFIG_DIR}/state.sqlite`;
+11
src/context.ts
··· 1 + import { DB_PATH } from "./constants.ts"; 2 + import { createDb, migrateToLatest } from "./db.ts"; 3 + 4 + export const db = createDb(DB_PATH); 5 + await migrateToLatest(db); 6 + 7 + export const ctx = { 8 + db, 9 + }; 10 + 11 + export type Context = typeof ctx;
+96
src/db.ts
··· 1 + import { Database as Sqlite } from "@db/sqlite"; 2 + import { DenoSqlite3Dialect } from "@soapbox/kysely-deno-sqlite"; 3 + import { 4 + Kysely, 5 + type Migration, 6 + type MigrationProvider, 7 + Migrator, 8 + sql, 9 + } from "kysely"; 10 + import { CONFIG_DIR } from "./constants.ts"; 11 + import type { STATUS } from "./types.ts"; 12 + 13 + export const createDb = (location: string): Database => { 14 + Deno.mkdirSync(CONFIG_DIR, { recursive: true }); 15 + return new Kysely<DatabaseSchema>({ 16 + dialect: new DenoSqlite3Dialect({ 17 + database: new Sqlite(location), 18 + }), 19 + }); 20 + }; 21 + 22 + export type DatabaseSchema = { 23 + virtual_machines: VirtualMachine; 24 + }; 25 + 26 + export type VirtualMachine = { 27 + id: string; 28 + name: string; 29 + bridge?: string; 30 + macAddress: string; 31 + memory: string; 32 + cpus: number; 33 + cpu: string; 34 + diskSize: string; 35 + drivePath?: string; 36 + diskFormat: string; 37 + isoPath?: string; 38 + version: string; 39 + status: STATUS; 40 + pid: number; 41 + createdAt?: string; 42 + updatedAt?: string; 43 + }; 44 + 45 + const migrations: Record<string, Migration> = {}; 46 + 47 + const migrationProvider: MigrationProvider = { 48 + // deno-lint-ignore require-await 49 + async getMigrations() { 50 + return migrations; 51 + }, 52 + }; 53 + 54 + migrations["001"] = { 55 + async up(db: Kysely<unknown>): Promise<void> { 56 + await db.schema 57 + .createTable("virtual_machines") 58 + .addColumn("id", "varchar", (col) => col.primaryKey()) 59 + .addColumn("name", "varchar", (col) => col.notNull().unique()) 60 + .addColumn("bridge", "varchar") 61 + .addColumn("macAddress", "varchar", (col) => col.notNull().unique()) 62 + .addColumn("memory", "varchar", (col) => col.notNull()) 63 + .addColumn("cpus", "integer", (col) => col.notNull()) 64 + .addColumn("cpu", "varchar", (col) => col.notNull()) 65 + .addColumn("diskSize", "varchar", (col) => col.notNull()) 66 + .addColumn("drivePath", "varchar") 67 + .addColumn("version", "varchar", (col) => col.notNull()) 68 + .addColumn("diskFormat", "varchar") 69 + .addColumn("isoPath", "varchar") 70 + .addColumn("status", "varchar", (col) => col.notNull()) 71 + .addColumn("pid", "integer", (col) => col.notNull().unique()) 72 + .addColumn( 73 + "createdAt", 74 + "varchar", 75 + (col) => col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`), 76 + ) 77 + .addColumn( 78 + "updatedAt", 79 + "varchar", 80 + (col) => col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`), 81 + ) 82 + .execute(); 83 + }, 84 + 85 + async down(db: Kysely<unknown>): Promise<void> { 86 + await db.schema.dropTable("virtual_machines").execute(); 87 + }, 88 + }; 89 + 90 + export const migrateToLatest = async (db: Database) => { 91 + const migrator = new Migrator({ db, provider: migrationProvider }); 92 + const { error } = await migrator.migrateToLatest(); 93 + if (error) throw error; 94 + }; 95 + 96 + export type Database = Kysely<DatabaseSchema>;
+52
src/state.ts
··· 1 + import { ctx } from "./context.ts"; 2 + import type { VirtualMachine } from "./db.ts"; 3 + import type { STATUS } from "./types.ts"; 4 + 5 + export async function saveInstanceState(vm: VirtualMachine) { 6 + await ctx.db.insertInto("virtual_machines") 7 + .values(vm) 8 + .execute(); 9 + } 10 + 11 + export async function updateInstanceState( 12 + name: string, 13 + status: STATUS, 14 + pid?: number, 15 + ) { 16 + await ctx.db.updateTable("virtual_machines") 17 + .set({ status, pid }) 18 + .where((eb) => 19 + eb.or([ 20 + eb("name", "=", name), 21 + eb("id", "=", name), 22 + ]) 23 + ) 24 + .execute(); 25 + } 26 + 27 + export async function removeInstanceState(name: string) { 28 + await ctx.db.deleteFrom("virtual_machines") 29 + .where((eb) => 30 + eb.or([ 31 + eb("name", "=", name), 32 + eb("id", "=", name), 33 + ]) 34 + ) 35 + .execute(); 36 + } 37 + 38 + export async function getInstanceState( 39 + name: string, 40 + ): Promise<VirtualMachine | undefined> { 41 + const vm = await ctx.db.selectFrom("virtual_machines") 42 + .selectAll() 43 + .where((eb) => 44 + eb.or([ 45 + eb("name", "=", name), 46 + eb("id", "=", name), 47 + ]) 48 + ) 49 + .executeTakeFirst(); 50 + 51 + return vm; 52 + }
+13
src/subcommands/inspect.ts
··· 1 + import { getInstanceState } from "../state.ts"; 2 + 3 + export default async function (name: string) { 4 + const vm = await getInstanceState(name); 5 + if (!vm) { 6 + console.error( 7 + `Virtual machine with name or ID ${name} not found.`, 8 + ); 9 + Deno.exit(1); 10 + } 11 + 12 + console.log(vm); 13 + }
+39
src/subcommands/ps.ts
··· 1 + import { Table } from "@cliffy/table"; 2 + import dayjs from "dayjs"; 3 + import relativeTime from "dayjs/plugin/relativeTime.js"; 4 + import utc from "dayjs/plugin/utc.js"; 5 + import { ctx } from "../context.ts"; 6 + 7 + dayjs.extend(relativeTime); 8 + dayjs.extend(utc); 9 + 10 + export default async function (all: boolean) { 11 + const results = await ctx.db.selectFrom("virtual_machines") 12 + .selectAll() 13 + .where((eb) => { 14 + if (all) { 15 + return eb("id", "!=", ""); 16 + } 17 + return eb("status", "=", "RUNNING"); 18 + }) 19 + .execute(); 20 + 21 + const table: Table = new Table( 22 + ["NAME", "VCPU", "MEMORY", "STATUS", "PID", "BRIDGE", "MAC", "CREATED"], 23 + ); 24 + 25 + for (const vm of results) { 26 + table.push([ 27 + vm.name, 28 + vm.cpus.toString(), 29 + vm.memory, 30 + vm.status, 31 + vm.pid?.toString() ?? "-", 32 + vm.bridge ?? "-", 33 + vm.macAddress, 34 + dayjs.utc(vm.createdAt).local().fromNow(), 35 + ]); 36 + } 37 + 38 + console.log(table.padding(2).toString()); 39 + }
+63
src/subcommands/start.ts
··· 1 + import _ from "lodash"; 2 + import { getInstanceState, updateInstanceState } from "../state.ts"; 3 + 4 + export default async function (name: string) { 5 + const vm = await getInstanceState(name); 6 + if (!vm) { 7 + console.error( 8 + `Virtual machine with name or ID ${name} not found.`, 9 + ); 10 + Deno.exit(1); 11 + } 12 + 13 + console.log(`Starting virtual machine ${vm.name} (ID: ${vm.id})...`); 14 + 15 + const cmd = new Deno.Command(vm.bridge ? "sudo" : "qemu-system-x86_64", { 16 + args: [ 17 + ..._.compact([vm.bridge && "qemu-system-x86_64"]), 18 + "-enable-kvm", 19 + "-cpu", 20 + vm.cpu, 21 + "-m", 22 + vm.memory, 23 + "-smp", 24 + vm.cpus.toString(), 25 + ..._.compact([vm.isoPath && "-cdrom", vm.isoPath]), 26 + "-netdev", 27 + vm.bridge 28 + ? `bridge,id=net0,br=${vm.bridge}` 29 + : "user,id=net0,hostfwd=tcp::2222-:22", 30 + "-device", 31 + `e1000,netdev=net0,mac=${vm.macAddress}`, 32 + "-display", 33 + "none", 34 + "-vga", 35 + "none", 36 + "-monitor", 37 + "none", 38 + "-chardev", 39 + "stdio,id=con0,signal=off", 40 + "-serial", 41 + "chardev:con0", 42 + ..._.compact( 43 + vm.drivePath && [ 44 + "-drive", 45 + `file=${vm.drivePath},format=${vm.diskFormat},if=virtio`, 46 + ], 47 + ), 48 + ], 49 + stdin: "inherit", 50 + stdout: "inherit", 51 + stderr: "inherit", 52 + }).spawn(); 53 + 54 + await updateInstanceState(name, "RUNNING", cmd.pid); 55 + 56 + const status = await cmd.status; 57 + 58 + await updateInstanceState(name, "STOPPED", cmd.pid); 59 + 60 + if (!status.success) { 61 + Deno.exit(status.code); 62 + } 63 + }
+43
src/subcommands/stop.ts
··· 1 + import chalk from "chalk"; 2 + import _ from "lodash"; 3 + import { getInstanceState, updateInstanceState } from "../state.ts"; 4 + 5 + export default async function (name: string) { 6 + const vm = await getInstanceState(name); 7 + if (!vm) { 8 + console.error( 9 + `Virtual machine with name or ID ${chalk.greenBright(name)} not found.`, 10 + ); 11 + Deno.exit(1); 12 + } 13 + 14 + console.log( 15 + `Stopping virtual machine ${chalk.greenBright(vm.name)} (ID: ${ 16 + chalk.greenBright(vm.id) 17 + })...`, 18 + ); 19 + 20 + const cmd = new Deno.Command(vm.bridge ? "sudo" : "kill", { 21 + args: [ 22 + ..._.compact([vm.bridge && "kill"]), 23 + "-TERM", 24 + vm.pid.toString(), 25 + ], 26 + stdin: "inherit", 27 + stdout: "inherit", 28 + stderr: "inherit", 29 + }); 30 + 31 + const status = await cmd.spawn().status; 32 + 33 + if (!status.success) { 34 + console.error( 35 + `Failed to stop virtual machine ${chalk.greenBright(vm.name)}.`, 36 + ); 37 + Deno.exit(status.code); 38 + } 39 + 40 + await updateInstanceState(vm.name, "STOPPED"); 41 + 42 + console.log(`Virtual machine ${chalk.greenBright(vm.name)} stopped.`); 43 + }
+1
src/types.ts
··· 1 + export type STATUS = "RUNNING" | "STOPPED";
+24 -3
src/utils.ts
··· 1 + import { createId } from "@paralleldrive/cuid2"; 1 2 import chalk from "chalk"; 2 3 import _ from "lodash"; 4 + import Moniker from "moniker"; 3 5 import { generateRandomMacAddress } from "./network.ts"; 6 + import { saveInstanceState } from "./state.ts"; 4 7 5 8 const DEFAULT_VERSION = "14.3-RELEASE"; 6 9 ··· 11 14 memory: string; 12 15 drive?: string; 13 16 diskFormat: string; 14 - size?: string; 17 + size: string; 15 18 bridge?: string; 16 19 } 17 20 ··· 92 95 isoPath: string | null, 93 96 options: Options, 94 97 ): Promise<void> { 98 + const macAddress = generateRandomMacAddress(); 95 99 const cmd = new Deno.Command(options.bridge ? "sudo" : "qemu-system-x86_64", { 96 100 args: [ 97 101 ..._.compact([options.bridge && "qemu-system-x86_64"]), ··· 108 112 ? `bridge,id=net0,br=${options.bridge}` 109 113 : "user,id=net0,hostfwd=tcp::2222-:22", 110 114 "-device", 111 - `e1000,netdev=net0,mac=${generateRandomMacAddress()}`, 115 + `e1000,netdev=net0,mac=${macAddress}`, 112 116 "-nographic", 113 117 "-monitor", 114 118 "none", ··· 126 130 stdin: "inherit", 127 131 stdout: "inherit", 128 132 stderr: "inherit", 133 + }).spawn(); 134 + 135 + await saveInstanceState({ 136 + id: createId(), 137 + name: Moniker.choose(), 138 + bridge: options.bridge, 139 + macAddress, 140 + memory: options.memory, 141 + cpus: options.cpus, 142 + cpu: options.cpu, 143 + diskSize: options.size, 144 + diskFormat: options.diskFormat, 145 + isoPath: isoPath ? Deno.realPathSync(isoPath) : undefined, 146 + drivePath: options.drive ? Deno.realPathSync(options.drive) : undefined, 147 + version: DEFAULT_VERSION, 148 + status: "RUNNING", 149 + pid: cmd.pid, 129 150 }); 130 151 131 - const status = await cmd.spawn().status; 152 + const status = await cmd.status; 132 153 133 154 if (!status.success) { 134 155 Deno.exit(status.code);