A simple, powerful CLI tool to spin up OpenIndiana virtual machines with QEMU
1#!/usr/bin/env -S deno run --allow-run --allow-read --allow-env
2
3import { Command } from "@cliffy/command";
4import pkg from "./deno.json" with { type: "json" };
5import { createBridgeNetworkIfNeeded } from "./src/network.ts";
6import inspect from "./src/subcommands/inspect.ts";
7import logs from "./src/subcommands/logs.ts";
8import ps from "./src/subcommands/ps.ts";
9import restart from "./src/subcommands/restart.ts";
10import rm from "./src/subcommands/rm.ts";
11import start from "./src/subcommands/start.ts";
12import stop from "./src/subcommands/stop.ts";
13import {
14 createDriveImageIfNeeded,
15 downloadIso,
16 emptyDiskImage,
17 handleInput,
18 type Options,
19 runQemu,
20} from "./src/utils.ts";
21
22if (import.meta.main) {
23 await new Command()
24 .name("openindiana-up")
25 .version(pkg.version)
26 .description("Start a OpenIndiana virtual machine using QEMU")
27 .arguments(
28 "[path-or-url-to-iso-or-version:string]",
29 )
30 .option("-o, --output <path:string>", "Output path for downloaded ISO")
31 .option("-c, --cpu <type:string>", "Type of CPU to emulate", {
32 default: "host",
33 })
34 .option("-C, --cpus <number:number>", "Number of CPU cores", {
35 default: 2,
36 })
37 .option("-m, --memory <size:string>", "Amount of memory for the VM", {
38 default: "2G",
39 })
40 .option("-i, --image <path:string>", "Path to VM disk image")
41 .option(
42 "--disk-format <format:string>",
43 "Disk image format (e.g., qcow2, raw)",
44 {
45 default: "raw",
46 },
47 )
48 .option(
49 "--size <size:string>",
50 "Size of the VM disk image to create if it does not exist (e.g., 20G)",
51 {
52 default: "20G",
53 },
54 )
55 .option(
56 "-b, --bridge <name:string>",
57 "Name of the network bridge to use for networking (e.g., br0)",
58 )
59 .option(
60 "-d, --detach",
61 "Run VM in the background and print VM name",
62 )
63 .option(
64 "-p, --port-forward <mappings:string>",
65 "Port forwarding rules in the format hostPort:guestPort (comma-separated for multiple)",
66 )
67 .example(
68 "Default usage",
69 "openindiana-up",
70 )
71 .example(
72 "Specific version",
73 "openindiana-up 20251026",
74 )
75 .example(
76 "Local ISO file",
77 "openindiana-up /path/to/openindiana.iso",
78 )
79 .example(
80 "Download URL",
81 "openindiana-up https://dlc.openindiana.org/isos/hipster/20251026/OI-hipster-text-20251026.iso",
82 )
83 .example(
84 "List running VMs",
85 "openindiana-up ps",
86 )
87 .example(
88 "List all VMs",
89 "openindiana-up ps --all",
90 )
91 .example(
92 "Start a VM",
93 "openindiana-up start my-vm",
94 )
95 .example(
96 "Stop a VM",
97 "openindiana-up stop my-vm",
98 )
99 .example(
100 "Inspect a VM",
101 "openindiana-up inspect my-vm",
102 )
103 .example(
104 "Remove a VM",
105 "openindiana-up rm my-vm",
106 )
107 .action(async (options: Options, input?: string) => {
108 const resolvedInput = handleInput(input);
109 let isoPath: string | null = resolvedInput;
110
111 if (
112 resolvedInput.startsWith("https://") ||
113 resolvedInput.startsWith("http://")
114 ) {
115 isoPath = await downloadIso(resolvedInput, options);
116 }
117
118 if (options.image) {
119 await createDriveImageIfNeeded(options);
120 }
121
122 if (!input && options.image && !await emptyDiskImage(options.image)) {
123 isoPath = null;
124 }
125
126 if (options.bridge) {
127 await createBridgeNetworkIfNeeded(options.bridge);
128 }
129
130 await runQemu(isoPath, options);
131 })
132 .command("ps", "List all virtual machines")
133 .option("--all, -a", "Show all virtual machines, including stopped ones")
134 .action(async (options: { all?: unknown }) => {
135 await ps(Boolean(options.all));
136 })
137 .command("start", "Start a virtual machine")
138 .arguments("<vm-name:string>")
139 .option("-c, --cpu <type:string>", "Type of CPU to emulate", {
140 default: "host",
141 })
142 .option("-C, --cpus <number:number>", "Number of CPU cores", {
143 default: 2,
144 })
145 .option("-m, --memory <size:string>", "Amount of memory for the VM", {
146 default: "2G",
147 })
148 .option("-i, --image <path:string>", "Path to VM disk image")
149 .option(
150 "--disk-format <format:string>",
151 "Disk image format (e.g., qcow2, raw)",
152 {
153 default: "raw",
154 },
155 )
156 .option(
157 "--size <size:string>",
158 "Size of the VM disk image to create if it doesn't exist (e.g., 20G)",
159 {
160 default: "20G",
161 },
162 )
163 .option(
164 "-b, --bridge <name:string>",
165 "Name of the network bridge to use for networking (e.g., br0)",
166 )
167 .option(
168 "-d, --detach",
169 "Run VM in the background and print VM name",
170 )
171 .option(
172 "-p, --port-forward <mappings:string>",
173 "Port forwarding rules in the format hostPort:guestPort (comma-separated for multiple)",
174 )
175 .action(async (options: unknown, vmName: string) => {
176 await start(vmName, Boolean((options as { detach: boolean }).detach));
177 })
178 .command("stop", "Stop a virtual machine")
179 .arguments("<vm-name:string>")
180 .action(async (_options: unknown, vmName: string) => {
181 await stop(vmName);
182 })
183 .command("inspect", "Inspect a virtual machine")
184 .arguments("<vm-name:string>")
185 .action(async (_options: unknown, vmName: string) => {
186 await inspect(vmName);
187 })
188 .command("rm", "Remove a virtual machine")
189 .arguments("<vm-name:string>")
190 .action(async (_options: unknown, vmName: string) => {
191 await rm(vmName);
192 })
193 .command("logs", "View logs of a virtual machine")
194 .option("--follow, -f", "Follow log output")
195 .arguments("<vm-name:string>")
196 .action(async (options: unknown, vmName: string) => {
197 await logs(vmName, Boolean((options as { follow: boolean }).follow));
198 })
199 .command("restart", "Restart a virtual machine")
200 .arguments("<vm-name:string>")
201 .action(async (_options: unknown, vmName: string) => {
202 await restart(vmName);
203 })
204 .parse(Deno.args);
205}