tangled
alpha
login
or
join now
tsiry-sandratraina.com
/
vmx
1
fork
atom
A Docker-like CLI and HTTP API for managing headless VMs
1
fork
atom
overview
issues
pulls
pipelines
format
tsiry-sandratraina.com
4 months ago
1fc72372
5eea478a
+59
-57
2 changed files
expand all
collapse all
unified
split
src
subcommands
run.ts
start.ts
+13
-13
src/subcommands/run.ts
···
22
22
pulledImg
23
23
? Effect.succeed(pulledImg)
24
24
: Effect.fail(new PullImageError({ cause: "Failed to pull image" }))
25
25
-
)
25
25
+
),
26
26
);
27
27
-
})
27
27
+
}),
28
28
);
29
29
30
30
const createVolumeIfNeeded = (
31
31
-
image: Image
31
31
+
image: Image,
32
32
): Effect.Effect<[Image, Volume?], Error, never> =>
33
33
parseFlags(Deno.args).flags.volume
34
34
? Effect.gen(function* () {
35
35
-
const volumeName = parseFlags(Deno.args).flags.volume as string;
36
36
-
const volume = yield* getVolume(volumeName);
37
37
-
if (volume) {
38
38
-
return [image, volume];
39
39
-
}
40
40
-
const newVolume = yield* createVolume(volumeName, image);
41
41
-
return [image, newVolume];
42
42
-
})
35
35
+
const volumeName = parseFlags(Deno.args).flags.volume as string;
36
36
+
const volume = yield* getVolume(volumeName);
37
37
+
if (volume) {
38
38
+
return [image, volume];
39
39
+
}
40
40
+
const newVolume = yield* createVolume(volumeName, image);
41
41
+
return [image, newVolume];
42
42
+
})
43
43
: Effect.succeed([image]);
44
44
45
45
const runImage = ([image, volume]: [Image, Volume?]) =>
···
73
73
console.error(`Failed to run image: ${error.cause} ${image}`);
74
74
Deno.exit(1);
75
75
})
76
76
-
)
77
77
-
)
76
76
+
),
77
77
+
),
78
78
);
79
79
}
80
80
+46
-44
src/subcommands/start.ts
···
17
17
}> {}
18
18
19
19
export class VmAlreadyRunningError extends Data.TaggedError(
20
20
-
"VmAlreadyRunningError"
20
20
+
"VmAlreadyRunningError",
21
21
)<{
22
22
name: string;
23
23
}> {}
···
31
31
getInstanceState(name),
32
32
Effect.flatMap((vm) =>
33
33
vm ? Effect.succeed(vm) : Effect.fail(new VmNotFoundError({ name }))
34
34
-
)
34
34
+
),
35
35
);
36
36
37
37
const logStarting = (vm: VirtualMachine) =>
···
44
44
export const setupFirmware = () => setupFirmwareFilesIfNeeded();
45
45
46
46
export const buildQemuArgs = (vm: VirtualMachine, firmwareArgs: string[]) => {
47
47
-
const qemu =
48
48
-
Deno.build.arch === "aarch64"
49
49
-
? "qemu-system-aarch64"
50
50
-
: "qemu-system-x86_64";
47
47
+
const qemu = Deno.build.arch === "aarch64"
48
48
+
? "qemu-system-aarch64"
49
49
+
: "qemu-system-x86_64";
51
50
52
51
let coreosArgs: string[] = Effect.runSync(setupCoreOSArgs(vm.drivePath));
53
52
···
84
83
vm.drivePath && [
85
84
"-drive",
86
85
`file=${vm.drivePath},format=${vm.diskFormat},if=virtio`,
87
87
-
]
86
86
+
],
88
87
),
89
88
...coreosArgs,
90
89
...(vm.volume ? [] : ["-snapshot"]),
···
100
99
export const startDetachedQemu = (
101
100
name: string,
102
101
vm: VirtualMachine,
103
103
-
qemuArgs: string[]
102
102
+
qemuArgs: string[],
104
103
) => {
105
105
-
const qemu =
106
106
-
Deno.build.arch === "aarch64"
107
107
-
? "qemu-system-aarch64"
108
108
-
: "qemu-system-x86_64";
104
104
+
const qemu = Deno.build.arch === "aarch64"
105
105
+
? "qemu-system-aarch64"
106
106
+
: "qemu-system-x86_64";
109
107
110
108
const logPath = `${LOGS_DIR}/${vm.name}.log`;
111
109
112
110
const fullCommand = vm.bridge
113
113
-
? `sudo ${qemu} ${qemuArgs
111
111
+
? `sudo ${qemu} ${
112
112
+
qemuArgs
114
113
.slice(1)
115
115
-
.join(" ")} >> "${logPath}" 2>&1 & echo $!`
114
114
+
.join(" ")
115
115
+
} >> "${logPath}" 2>&1 & echo $!`
116
116
: `${qemu} ${qemuArgs.join(" ")} >> "${logPath}" 2>&1 & echo $!`;
117
117
118
118
return Effect.tryPromise({
···
143
143
Effect.flatMap(({ qemuPid, logPath }) =>
144
144
pipe(
145
145
updateInstanceState(name, "RUNNING", qemuPid),
146
146
-
Effect.map(() => ({ vm, qemuPid, logPath }))
146
146
+
Effect.map(() => ({ vm, qemuPid, logPath })),
147
147
)
148
148
-
)
148
148
+
),
149
149
);
150
150
};
151
151
···
160
160
}) =>
161
161
Effect.sync(() => {
162
162
console.log(
163
163
-
`Virtual machine ${vm.name} started in background (PID: ${qemuPid})`
163
163
+
`Virtual machine ${vm.name} started in background (PID: ${qemuPid})`,
164
164
);
165
165
console.log(`Logs will be written to: ${logPath}`);
166
166
});
···
168
168
const startInteractiveQemu = (
169
169
name: string,
170
170
vm: VirtualMachine,
171
171
-
qemuArgs: string[]
171
171
+
qemuArgs: string[],
172
172
) => {
173
173
-
const qemu =
174
174
-
Deno.build.arch === "aarch64"
175
175
-
? "qemu-system-aarch64"
176
176
-
: "qemu-system-x86_64";
173
173
+
const qemu = Deno.build.arch === "aarch64"
174
174
+
? "qemu-system-aarch64"
175
175
+
: "qemu-system-x86_64";
177
176
178
177
return Effect.tryPromise({
179
178
try: async () => {
···
209
208
});
210
209
211
210
export const createVolumeIfNeeded = (
212
212
-
vm: VirtualMachine
211
211
+
vm: VirtualMachine,
213
212
): Effect.Effect<[VirtualMachine, Volume?], Error, never> =>
214
213
Effect.gen(function* () {
215
214
const { flags } = parseFlags(Deno.args);
···
224
223
225
224
if (!vm.drivePath) {
226
225
throw new Error(
227
227
-
`Cannot create volume: Virtual machine ${vm.name} has no drivePath defined.`
226
226
+
`Cannot create volume: Virtual machine ${vm.name} has no drivePath defined.`,
228
227
);
229
228
}
230
229
···
267
266
diskFormat: volume ? "qcow2" : vm.diskFormat,
268
267
volume: volume?.path,
269
268
},
270
270
-
firmwareArgs
269
269
+
firmwareArgs,
271
270
)
272
271
),
273
272
Effect.flatMap((qemuArgs) =>
···
277
276
startDetachedQemu(name, { ...vm, volume: volume?.path }, qemuArgs)
278
277
),
279
278
Effect.tap(logDetachedSuccess),
280
280
-
Effect.map(() => 0) // Exit code 0
279
279
+
Effect.map(() => 0), // Exit code 0
281
280
)
282
282
-
)
281
281
+
),
283
282
)
284
283
),
285
285
-
Effect.catchAll(handleError)
284
284
+
Effect.catchAll(handleError),
286
285
);
287
286
288
287
const startInteractiveEffect = (name: string) =>
···
303
302
diskFormat: volume ? "qcow2" : vm.diskFormat,
304
303
volume: volume?.path,
305
304
},
306
306
-
firmwareArgs
305
305
+
firmwareArgs,
307
306
)
308
307
),
309
308
Effect.flatMap((qemuArgs) =>
310
309
startInteractiveQemu(name, { ...vm, volume: volume?.path }, qemuArgs)
311
310
),
312
312
-
Effect.map((status) => (status.success ? 0 : status.code || 1))
311
311
+
Effect.map((status) => (status.success ? 0 : status.code || 1)),
313
312
)
314
313
),
315
315
-
Effect.catchAll(handleError)
314
314
+
Effect.catchAll(handleError),
316
315
);
317
316
318
317
export default async function (name: string, detach: boolean = false) {
319
318
const exitCode = await Effect.runPromise(
320
320
-
detach ? startDetachedEffect(name) : startInteractiveEffect(name)
319
319
+
detach ? startDetachedEffect(name) : startInteractiveEffect(name),
321
320
);
322
321
323
322
if (detach) {
···
331
330
const { flags } = parseFlags(Deno.args);
332
331
return {
333
332
...vm,
334
334
-
memory:
335
335
-
flags.memory || flags.m ? String(flags.memory || flags.m) : vm.memory,
333
333
+
memory: flags.memory || flags.m
334
334
+
? String(flags.memory || flags.m)
335
335
+
: vm.memory,
336
336
cpus: flags.cpus || flags.C ? Number(flags.cpus || flags.C) : vm.cpus,
337
337
cpu: flags.cpu || flags.c ? String(flags.cpu || flags.c) : vm.cpu,
338
338
diskFormat: flags.diskFormat ? String(flags.diskFormat) : vm.diskFormat,
339
339
-
portForward:
340
340
-
flags.portForward || flags.p
341
341
-
? String(flags.portForward || flags.p)
342
342
-
: vm.portForward,
343
343
-
drivePath:
344
344
-
flags.image || flags.i ? String(flags.image || flags.i) : vm.drivePath,
345
345
-
bridge:
346
346
-
flags.bridge || flags.b ? String(flags.bridge || flags.b) : vm.bridge,
347
347
-
diskSize:
348
348
-
flags.size || flags.s ? String(flags.size || flags.s) : vm.diskSize,
339
339
+
portForward: flags.portForward || flags.p
340
340
+
? String(flags.portForward || flags.p)
341
341
+
: vm.portForward,
342
342
+
drivePath: flags.image || flags.i
343
343
+
? String(flags.image || flags.i)
344
344
+
: vm.drivePath,
345
345
+
bridge: flags.bridge || flags.b
346
346
+
? String(flags.bridge || flags.b)
347
347
+
: vm.bridge,
348
348
+
diskSize: flags.size || flags.s
349
349
+
? String(flags.size || flags.s)
350
350
+
: vm.diskSize,
349
351
};
350
352
}