A Docker-like CLI and HTTP API for managing headless VMs
1import chalk from "chalk";
2import { Effect } from "effect";
3import { BridgeSetupError, NetworkError } from "./errors.ts";
4
5export const setupQemuBridge = (bridgeName: string) =>
6 Effect.tryPromise({
7 try: async () => {
8 const bridgeConfPath = "/etc/qemu/bridge.conf";
9 const bridgeConfContent = await Deno.readTextFile(bridgeConfPath).catch(
10 () => "",
11 );
12 if (bridgeConfContent.includes(`allow ${bridgeName}`)) {
13 console.log(
14 chalk.greenBright(
15 `QEMU bridge configuration for ${bridgeName} already exists.`,
16 ),
17 );
18 return;
19 }
20
21 console.log(
22 chalk.blueBright(
23 `Adding QEMU bridge configuration for ${bridgeName}...`,
24 ),
25 );
26
27 const cmd = new Deno.Command("sudo", {
28 args: [
29 "sh",
30 "-c",
31 `mkdir -p /etc/qemu && echo "allow ${bridgeName}" >> ${bridgeConfPath}`,
32 ],
33 stdin: "inherit",
34 stdout: "inherit",
35 stderr: "inherit",
36 });
37 const status = await cmd.spawn().status;
38
39 if (!status.success) {
40 console.error(
41 chalk.redBright(
42 `Failed to add QEMU bridge configuration for ${bridgeName}.`,
43 ),
44 );
45 Deno.exit(status.code);
46 }
47
48 console.log(
49 chalk.greenBright(
50 `QEMU bridge configuration for ${bridgeName} added successfully.`,
51 ),
52 );
53 },
54 catch: (error) => new BridgeSetupError({ cause: error }),
55 });
56
57export const createBridgeNetworkIfNeeded = (bridgeName: string) =>
58 Effect.tryPromise({
59 try: async () => {
60 const bridgeExistsCmd = new Deno.Command("ip", {
61 args: ["link", "show", bridgeName],
62 stdout: "null",
63 stderr: "null",
64 });
65
66 const bridgeExistsStatus = await bridgeExistsCmd.spawn().status;
67 if (bridgeExistsStatus.success) {
68 console.log(
69 chalk.greenBright(`Network bridge ${bridgeName} already exists.`),
70 );
71 await setupQemuBridge(bridgeName);
72 return;
73 }
74
75 console.log(chalk.blueBright(`Creating network bridge ${bridgeName}...`));
76 const createBridgeCmd = new Deno.Command("sudo", {
77 args: ["ip", "link", "add", bridgeName, "type", "bridge"],
78 stdin: "inherit",
79 stdout: "inherit",
80 stderr: "inherit",
81 });
82
83 let status = await createBridgeCmd.spawn().status;
84 if (!status.success) {
85 console.error(
86 chalk.redBright(`Failed to create network bridge ${bridgeName}.`),
87 );
88 Deno.exit(status.code);
89 }
90
91 const bringUpBridgeCmd = new Deno.Command("sudo", {
92 args: ["ip", "link", "set", "dev", bridgeName, "up"],
93 stdin: "inherit",
94 stdout: "inherit",
95 stderr: "inherit",
96 });
97 status = await bringUpBridgeCmd.spawn().status;
98 if (!status.success) {
99 console.error(
100 chalk.redBright(`Failed to bring up network bridge ${bridgeName}.`),
101 );
102 Deno.exit(status.code);
103 }
104
105 console.log(
106 chalk.greenBright(`Network bridge ${bridgeName} created and up.`),
107 );
108
109 await setupQemuBridge(bridgeName);
110 },
111 catch: (error) => new NetworkError({ cause: error }),
112 });
113
114export const generateRandomMacAddress = () =>
115 Effect.sync(() => {
116 const hexDigits = "0123456789ABCDEF";
117 let macAddress = "52:54:00";
118
119 for (let i = 0; i < 3; i++) {
120 macAddress += ":";
121 for (let j = 0; j < 2; j++) {
122 macAddress += hexDigits.charAt(
123 Math.floor(Math.random() * hexDigits.length),
124 );
125 }
126 }
127
128 return macAddress;
129 });