vmsan 0.1.0-alpha.2 → 0.1.0-alpha.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +190 -21
- package/README.md +91 -47
- package/dist/_chunks/agent.mjs +231 -4
- package/dist/_chunks/connect.mjs +53 -11
- package/dist/_chunks/context.mjs +2380 -0
- package/dist/_chunks/create.mjs +48 -180
- package/dist/_chunks/download.mjs +14 -22
- package/dist/_chunks/errors.mjs +11 -5
- package/dist/_chunks/exec.mjs +190 -0
- package/dist/_chunks/list.mjs +60 -54
- package/dist/_chunks/network.mjs +6 -5
- package/dist/_chunks/remove.mjs +9 -8
- package/dist/_chunks/shell.mjs +2 -0
- package/dist/_chunks/start.mjs +16 -165
- package/dist/_chunks/stop.mjs +8 -7
- package/dist/_chunks/summary.mjs +69 -0
- package/dist/_chunks/timeout-extender.mjs +66 -0
- package/dist/_chunks/timeout-killer.mjs +33 -0
- package/dist/_chunks/upload.mjs +5 -20
- package/dist/_chunks/validation.mjs +1 -1
- package/dist/_chunks/vm-context.mjs +34 -0
- package/dist/_chunks/vm-state.mjs +56 -24
- package/dist/bin/cli.mjs +16 -2
- package/dist/index.d.mts +660 -366
- package/dist/index.mjs +35 -8
- package/package.json +7 -6
- package/dist/_chunks/cleanup.mjs +0 -328
- package/dist/_chunks/connect2.mjs +0 -72
- package/dist/_chunks/environment.mjs +0 -1064
- package/dist/_chunks/image-rootfs.mjs +0 -329
- package/dist/_chunks/vm.mjs +0 -208
package/dist/_chunks/list.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import
|
|
2
|
-
import "./errors.mjs";
|
|
1
|
+
import "./paths.mjs";
|
|
2
|
+
import { t as handleCommandError } from "./errors.mjs";
|
|
3
3
|
import { r as getOutputMode, t as createCommandLogger } from "./logger.mjs";
|
|
4
|
-
import {
|
|
5
|
-
import "./
|
|
6
|
-
import
|
|
4
|
+
import { d as timeRemaining, l as table, u as timeAgo } from "./vm-state.mjs";
|
|
5
|
+
import { t as createVmsan } from "./context.mjs";
|
|
6
|
+
import "./firecracker.mjs";
|
|
7
|
+
import "./timeout-killer.mjs";
|
|
7
8
|
import { consola } from "consola";
|
|
8
9
|
import { defineCommand } from "citty";
|
|
9
10
|
const STATUS_COLORS = {
|
|
@@ -21,63 +22,68 @@ const listCommand = defineCommand({
|
|
|
21
22
|
name: "list",
|
|
22
23
|
description: "List all VMs"
|
|
23
24
|
},
|
|
24
|
-
run() {
|
|
25
|
+
async run() {
|
|
25
26
|
const cmdLog = createCommandLogger("list");
|
|
26
27
|
const log = consola.withTag("list");
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
try {
|
|
29
|
+
const vms = (await createVmsan()).list();
|
|
30
|
+
if (vms.length === 0) {
|
|
31
|
+
if (getOutputMode() === "json") cmdLog.set({
|
|
32
|
+
count: 0,
|
|
33
|
+
vms: []
|
|
34
|
+
});
|
|
35
|
+
else {
|
|
36
|
+
log.log("No VMs found.");
|
|
37
|
+
cmdLog.set({ count: 0 });
|
|
38
|
+
}
|
|
39
|
+
cmdLog.emit();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const statuses = {};
|
|
43
|
+
for (const vm of vms) statuses[vm.status] = (statuses[vm.status] ?? 0) + 1;
|
|
44
|
+
consola.debug(`Found ${vms.length} VM(s): ${Object.entries(statuses).map(([s, n]) => `${n} ${s}`).join(", ")}`);
|
|
29
45
|
if (getOutputMode() === "json") cmdLog.set({
|
|
30
|
-
count:
|
|
31
|
-
|
|
46
|
+
count: vms.length,
|
|
47
|
+
statuses,
|
|
48
|
+
vms: vms.map((vm) => ({
|
|
49
|
+
id: vm.id,
|
|
50
|
+
status: vm.status,
|
|
51
|
+
createdAt: vm.createdAt,
|
|
52
|
+
memSizeMib: vm.memSizeMib,
|
|
53
|
+
vcpuCount: vm.vcpuCount,
|
|
54
|
+
runtime: vm.runtime,
|
|
55
|
+
timeoutAt: vm.timeoutAt ?? null,
|
|
56
|
+
snapshot: vm.snapshot ?? null
|
|
57
|
+
}))
|
|
32
58
|
});
|
|
33
59
|
else {
|
|
34
|
-
|
|
35
|
-
|
|
60
|
+
const output = table({
|
|
61
|
+
rows: vms,
|
|
62
|
+
columns: {
|
|
63
|
+
ID: { value: (vm) => vm.id },
|
|
64
|
+
STATUS: {
|
|
65
|
+
value: (vm) => vm.status,
|
|
66
|
+
color: (vm) => colorStatus(vm.status)
|
|
67
|
+
},
|
|
68
|
+
CREATED: { value: (vm) => timeAgo(vm.createdAt) },
|
|
69
|
+
MEMORY: { value: (vm) => `${vm.memSizeMib} MiB` },
|
|
70
|
+
VCPUS: { value: (vm) => vm.vcpuCount },
|
|
71
|
+
RUNTIME: { value: (vm) => vm.runtime },
|
|
72
|
+
TIMEOUT: { value: (vm) => vm.timeoutAt ? timeRemaining(vm.timeoutAt) : "-" },
|
|
73
|
+
SNAPSHOT: { value: (vm) => vm.snapshot ?? "-" }
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
log.log(output);
|
|
77
|
+
cmdLog.set({
|
|
78
|
+
count: vms.length,
|
|
79
|
+
statuses
|
|
80
|
+
});
|
|
36
81
|
}
|
|
37
82
|
cmdLog.emit();
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
for (const vm of vms) statuses[vm.status] = (statuses[vm.status] ?? 0) + 1;
|
|
42
|
-
consola.debug(`Found ${vms.length} VM(s): ${Object.entries(statuses).map(([s, n]) => `${n} ${s}`).join(", ")}`);
|
|
43
|
-
if (getOutputMode() === "json") cmdLog.set({
|
|
44
|
-
count: vms.length,
|
|
45
|
-
statuses,
|
|
46
|
-
vms: vms.map((vm) => ({
|
|
47
|
-
id: vm.id,
|
|
48
|
-
status: vm.status,
|
|
49
|
-
createdAt: vm.createdAt,
|
|
50
|
-
memSizeMib: vm.memSizeMib,
|
|
51
|
-
vcpuCount: vm.vcpuCount,
|
|
52
|
-
runtime: vm.runtime,
|
|
53
|
-
timeoutAt: vm.timeoutAt ?? null,
|
|
54
|
-
snapshot: vm.snapshot ?? null
|
|
55
|
-
}))
|
|
56
|
-
});
|
|
57
|
-
else {
|
|
58
|
-
const output = table({
|
|
59
|
-
rows: vms,
|
|
60
|
-
columns: {
|
|
61
|
-
ID: { value: (vm) => vm.id },
|
|
62
|
-
STATUS: {
|
|
63
|
-
value: (vm) => vm.status,
|
|
64
|
-
color: (vm) => colorStatus(vm.status)
|
|
65
|
-
},
|
|
66
|
-
CREATED: { value: (vm) => timeAgo(vm.createdAt) },
|
|
67
|
-
MEMORY: { value: (vm) => `${vm.memSizeMib} MiB` },
|
|
68
|
-
VCPUS: { value: (vm) => vm.vcpuCount },
|
|
69
|
-
RUNTIME: { value: (vm) => vm.runtime },
|
|
70
|
-
TIMEOUT: { value: (vm) => vm.timeoutAt ? timeRemaining(vm.timeoutAt) : "-" },
|
|
71
|
-
SNAPSHOT: { value: (vm) => vm.snapshot ?? "-" }
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
log.log(output);
|
|
75
|
-
cmdLog.set({
|
|
76
|
-
count: vms.length,
|
|
77
|
-
statuses
|
|
78
|
-
});
|
|
83
|
+
} catch (error) {
|
|
84
|
+
handleCommandError(error, cmdLog);
|
|
85
|
+
process.exitCode = 1;
|
|
79
86
|
}
|
|
80
|
-
cmdLog.emit();
|
|
81
87
|
}
|
|
82
88
|
});
|
|
83
89
|
export { listCommand as default };
|
package/dist/_chunks/network.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import "./paths.mjs";
|
|
2
|
+
import { V as policyConflictError, t as handleCommandError } from "./errors.mjs";
|
|
3
3
|
import { r as getOutputMode, t as createCommandLogger } from "./logger.mjs";
|
|
4
4
|
import "./vm-state.mjs";
|
|
5
|
-
import "./
|
|
6
|
-
import
|
|
5
|
+
import { t as createVmsan } from "./context.mjs";
|
|
6
|
+
import "./firecracker.mjs";
|
|
7
|
+
import "./timeout-killer.mjs";
|
|
7
8
|
import { d as validateCidr, i as parseDomains, n as parseCidrList, s as parseNetworkPolicy } from "./validation.mjs";
|
|
8
9
|
import { consola } from "consola";
|
|
9
10
|
import { defineCommand } from "citty";
|
|
@@ -46,7 +47,7 @@ const networkCommand = defineCommand({
|
|
|
46
47
|
for (const cidr of allowedCidrs) validateCidr(cidr);
|
|
47
48
|
for (const cidr of deniedCidrs) validateCidr(cidr);
|
|
48
49
|
if (policy === "deny-all" && (domains.length || allowedCidrs.length || deniedCidrs.length)) throw policyConflictError();
|
|
49
|
-
const result = await
|
|
50
|
+
const result = await (await createVmsan()).updateNetworkPolicy(args.vmId, policy, domains, allowedCidrs, deniedCidrs);
|
|
50
51
|
if (!result.success) {
|
|
51
52
|
if (result.error) throw result.error;
|
|
52
53
|
consola.error(`Failed to update network policy for ${args.vmId}`);
|
package/dist/_chunks/remove.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
import "./paths.mjs";
|
|
2
2
|
import "./errors.mjs";
|
|
3
3
|
import { t as createCommandLogger } from "./logger.mjs";
|
|
4
4
|
import "./vm-state.mjs";
|
|
5
|
-
import "./
|
|
6
|
-
import
|
|
5
|
+
import { t as createVmsan } from "./context.mjs";
|
|
6
|
+
import "./firecracker.mjs";
|
|
7
|
+
import "./timeout-killer.mjs";
|
|
7
8
|
import { consola } from "consola";
|
|
8
9
|
import { defineCommand } from "citty";
|
|
9
10
|
const removeCommand = defineCommand({
|
|
@@ -34,9 +35,9 @@ const removeCommand = defineCommand({
|
|
|
34
35
|
process.exitCode = 1;
|
|
35
36
|
return;
|
|
36
37
|
}
|
|
37
|
-
const
|
|
38
|
+
const vmsan = await createVmsan();
|
|
38
39
|
const missing = [];
|
|
39
|
-
for (const id of vmIds) if (!
|
|
40
|
+
for (const id of vmIds) if (!vmsan.get(id)) missing.push(id);
|
|
40
41
|
if (missing.length > 0) {
|
|
41
42
|
consola.error(`VM(s) not found: ${missing.join(", ")}`);
|
|
42
43
|
cmdLog.emit();
|
|
@@ -45,7 +46,7 @@ const removeCommand = defineCommand({
|
|
|
45
46
|
}
|
|
46
47
|
if (!args.force) {
|
|
47
48
|
const running = [];
|
|
48
|
-
for (const id of vmIds) if (
|
|
49
|
+
for (const id of vmIds) if (vmsan.get(id).status !== "stopped") running.push(id);
|
|
49
50
|
if (running.length > 0) {
|
|
50
51
|
consola.error(`Cannot remove running VM(s): ${running.join(", ")}. Stop them first or use --force (-f).`);
|
|
51
52
|
cmdLog.emit();
|
|
@@ -57,10 +58,10 @@ const removeCommand = defineCommand({
|
|
|
57
58
|
let hasErrors = false;
|
|
58
59
|
for (const id of vmIds) {
|
|
59
60
|
const log = consola.withTag(id);
|
|
60
|
-
const vm =
|
|
61
|
+
const vm = vmsan.get(id);
|
|
61
62
|
consola.debug(`VM ${id} status=${vm.status}, force=${args.force}`);
|
|
62
63
|
log.start(`Removing ${id}...`);
|
|
63
|
-
const result = await
|
|
64
|
+
const result = await vmsan.remove(id, { force: args.force });
|
|
64
65
|
if (result.success) {
|
|
65
66
|
log.success(`Removed ${id}`);
|
|
66
67
|
results.push({
|
package/dist/_chunks/shell.mjs
CHANGED
|
@@ -100,6 +100,7 @@ var ShellSession = class {
|
|
|
100
100
|
process.stdin.resume();
|
|
101
101
|
this.ws.send(serializeReady());
|
|
102
102
|
if (process.stdout.columns && process.stdout.rows) this.ws.send(serializeResize(process.stdout.columns, process.stdout.rows));
|
|
103
|
+
if (this.opts.initialCommand) this.ws.send(serializeData(Buffer.from(this.opts.initialCommand)));
|
|
103
104
|
process.stdin.on("data", onStdinData);
|
|
104
105
|
process.stdout.on("resize", onResize);
|
|
105
106
|
});
|
|
@@ -136,6 +137,7 @@ var ShellSession = class {
|
|
|
136
137
|
const url = new URL(`${proto}://${this.opts.host}:${this.opts.port}/ws/shell`);
|
|
137
138
|
url.searchParams.set("token", this.opts.token);
|
|
138
139
|
if (this.opts.shell) url.searchParams.set("shell", this.opts.shell);
|
|
140
|
+
if (this.opts.user) url.searchParams.set("user", this.opts.user);
|
|
139
141
|
return url.toString();
|
|
140
142
|
}
|
|
141
143
|
};
|
package/dist/_chunks/start.mjs
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import { t as
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import { dirname, join } from "node:path";
|
|
9
|
-
import { consola } from "consola";
|
|
10
|
-
import { existsSync, rmSync, unlinkSync } from "node:fs";
|
|
1
|
+
import "./paths.mjs";
|
|
2
|
+
import { t as handleCommandError } from "./errors.mjs";
|
|
3
|
+
import { t as createCommandLogger } from "./logger.mjs";
|
|
4
|
+
import "./vm-state.mjs";
|
|
5
|
+
import { t as createVmsan } from "./context.mjs";
|
|
6
|
+
import "./firecracker.mjs";
|
|
7
|
+
import "./timeout-killer.mjs";
|
|
11
8
|
import { defineCommand } from "citty";
|
|
12
9
|
const startCommand = defineCommand({
|
|
13
10
|
meta: {
|
|
@@ -21,169 +18,23 @@ const startCommand = defineCommand({
|
|
|
21
18
|
} },
|
|
22
19
|
async run({ args }) {
|
|
23
20
|
const cmdLog = createCommandLogger("start");
|
|
24
|
-
const paths = vmsanPaths();
|
|
25
|
-
const lifecycle = {
|
|
26
|
-
vmId: void 0,
|
|
27
|
-
networkConfig: void 0
|
|
28
|
-
};
|
|
29
|
-
const store = new FileVmStateStore(paths.vmsDir);
|
|
30
21
|
try {
|
|
31
22
|
const vmId = args.vmId;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (!state.chrootDir || !existsSync(state.chrootDir)) throw chrootNotFoundError(vmId);
|
|
39
|
-
const baseDir = paths.baseDir;
|
|
40
|
-
validateEnvironment(baseDir);
|
|
41
|
-
log.start(`Starting VM ${vmId}...`);
|
|
42
|
-
const mgr = NetworkManager.fromVmNetwork(state.network);
|
|
43
|
-
const networkConfig = mgr.config;
|
|
44
|
-
lifecycle.networkConfig = networkConfig;
|
|
45
|
-
consola.debug(`Reconstructed network config: slot=${networkConfig.slot}, tap=${networkConfig.tapDevice}, host=${networkConfig.hostIp}, guest=${networkConfig.guestIp}`);
|
|
46
|
-
log.start("Setting up networking...");
|
|
47
|
-
await mgr.setup();
|
|
48
|
-
log.success(`Network: TAP ${networkConfig.tapDevice}, Host ${networkConfig.hostIp}, Guest ${networkConfig.guestIp}`);
|
|
49
|
-
const vmRootCandidates = Array.from(new Set([
|
|
50
|
-
join(state.chrootDir, "root"),
|
|
51
|
-
state.chrootDir,
|
|
52
|
-
dirname(dirname(state.apiSocket))
|
|
53
|
-
]));
|
|
54
|
-
for (const rootDir of vmRootCandidates) {
|
|
55
|
-
const staleFirecrackerBin = join(rootDir, "firecracker");
|
|
56
|
-
if (existsSync(staleFirecrackerBin)) unlinkSync(staleFirecrackerBin);
|
|
57
|
-
rmSync(join(rootDir, "firecracker.pid"), { force: true });
|
|
23
|
+
const result = await (await createVmsan()).start(vmId);
|
|
24
|
+
if (!result.success) {
|
|
25
|
+
if (result.error) throw result.error;
|
|
26
|
+
process.exitCode = 1;
|
|
27
|
+
cmdLog.emit();
|
|
28
|
+
return;
|
|
58
29
|
}
|
|
59
|
-
const socketPath = state.apiSocket;
|
|
60
|
-
if (existsSync(socketPath)) unlinkSync(socketPath);
|
|
61
|
-
const removeStaleDevTrees = () => {
|
|
62
|
-
for (const rootDir of vmRootCandidates) {
|
|
63
|
-
const devDir = join(rootDir, "dev");
|
|
64
|
-
if (existsSync(devDir)) rmSync(devDir, {
|
|
65
|
-
recursive: true,
|
|
66
|
-
force: true
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
};
|
|
70
|
-
const removeStaleDeviceNodes = () => {
|
|
71
|
-
const staleNodes = [
|
|
72
|
-
"dev/net/tun",
|
|
73
|
-
"dev/kvm",
|
|
74
|
-
"dev/userfaultfd",
|
|
75
|
-
"dev/urandom"
|
|
76
|
-
];
|
|
77
|
-
for (const rootDir of vmRootCandidates) for (const rel of staleNodes) {
|
|
78
|
-
const nodePath = join(rootDir, rel);
|
|
79
|
-
if (existsSync(nodePath)) rmSync(nodePath, {
|
|
80
|
-
recursive: true,
|
|
81
|
-
force: true
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
};
|
|
85
|
-
const firecrackerBin = join(baseDir, "bin", "firecracker");
|
|
86
|
-
const jailerBin = join(baseDir, "bin", "jailer");
|
|
87
|
-
const jailer = new Jailer(vmId, paths.jailerBaseDir);
|
|
88
|
-
let socketReady = false;
|
|
89
|
-
const errorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
90
|
-
const logAttemptError = (attempt, error) => {
|
|
91
|
-
const message = errorMessage(error);
|
|
92
|
-
consola.error(`${startTag} ${attempt} failed: ${message}`);
|
|
93
|
-
};
|
|
94
|
-
consola.debug(`Stale file cleanup: checked ${vmRootCandidates.length} root candidates`);
|
|
95
|
-
const logDiagnostics = () => {
|
|
96
|
-
const socketExists = existsSync(socketPath);
|
|
97
|
-
const devState = vmRootCandidates.map((rootDir) => `${join(rootDir, "dev")}=${existsSync(join(rootDir, "dev"))}`).join(", ");
|
|
98
|
-
const firecrackerPid = getVmPid(vmId);
|
|
99
|
-
const jailerPid = getVmJailerPid(vmId);
|
|
100
|
-
log.error(`${startTag} diagnostics: socketExists=${socketExists}; firecrackerPid=${firecrackerPid ?? "none"}; jailerPid=${jailerPid ?? "none"}; devDirs=[${devState}]`);
|
|
101
|
-
};
|
|
102
|
-
const isRecoverableStartError = (message) => {
|
|
103
|
-
if (message.includes("Timeout waiting for API socket")) return true;
|
|
104
|
-
if (message.includes("mknod inside the jail") && message.includes("File exists")) return true;
|
|
105
|
-
if (message.includes("MknodDev(") && message.includes("os error 17")) return true;
|
|
106
|
-
return false;
|
|
107
|
-
};
|
|
108
|
-
const cgroup = {
|
|
109
|
-
cpuQuotaUs: state.vcpuCount * 1e5,
|
|
110
|
-
cpuPeriodUs: 1e5,
|
|
111
|
-
memoryBytes: state.memSizeMib * 1024 * 1024
|
|
112
|
-
};
|
|
113
|
-
const spawnAndWait = async (timeoutMs) => {
|
|
114
|
-
log.start("Spawning Firecracker via jailer...");
|
|
115
|
-
consola.debug(`Jailer spawn: firecracker=${firecrackerBin}, jailer=${jailerBin}, chrootBase=${jailer.paths.chrootBase}`);
|
|
116
|
-
jailer.spawn({
|
|
117
|
-
firecrackerBin,
|
|
118
|
-
jailerBin,
|
|
119
|
-
chrootBase: jailer.paths.chrootBase,
|
|
120
|
-
newPidNs: true,
|
|
121
|
-
cgroup,
|
|
122
|
-
netns: state.network.netnsName
|
|
123
|
-
});
|
|
124
|
-
log.start("Waiting for API socket...");
|
|
125
|
-
await waitForSocket(socketPath, timeoutMs);
|
|
126
|
-
};
|
|
127
|
-
try {
|
|
128
|
-
removeStaleDeviceNodes();
|
|
129
|
-
await spawnAndWait(1e4);
|
|
130
|
-
socketReady = true;
|
|
131
|
-
} catch (firstStartError) {
|
|
132
|
-
const message = errorMessage(firstStartError);
|
|
133
|
-
if (!isRecoverableStartError(message)) {
|
|
134
|
-
logAttemptError("initial attempt", firstStartError);
|
|
135
|
-
logDiagnostics();
|
|
136
|
-
throw firstStartError;
|
|
137
|
-
}
|
|
138
|
-
logAttemptError("initial attempt", firstStartError);
|
|
139
|
-
killOrphanVmProcess(vmId);
|
|
140
|
-
if (existsSync(socketPath)) unlinkSync(socketPath);
|
|
141
|
-
removeStaleDeviceNodes();
|
|
142
|
-
removeStaleDevTrees();
|
|
143
|
-
for (const rootDir of vmRootCandidates) {
|
|
144
|
-
const staleFirecrackerBin = join(rootDir, "firecracker");
|
|
145
|
-
if (existsSync(staleFirecrackerBin)) unlinkSync(staleFirecrackerBin);
|
|
146
|
-
rmSync(join(rootDir, "firecracker.pid"), { force: true });
|
|
147
|
-
}
|
|
148
|
-
try {
|
|
149
|
-
await spawnAndWait(15e3);
|
|
150
|
-
socketReady = true;
|
|
151
|
-
} catch (retryError) {
|
|
152
|
-
logAttemptError("retry attempt", retryError);
|
|
153
|
-
logDiagnostics();
|
|
154
|
-
throw new Error(`${startTag} retry failed after cleanup. First error: ${message}. Retry error: ${errorMessage(retryError)}`);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
if (!socketReady) throw new Error(`Timeout waiting for API socket at ${socketPath}`);
|
|
158
|
-
log.success("API socket ready");
|
|
159
|
-
const vm = new FirecrackerClient(socketPath);
|
|
160
|
-
const bootArgs = NetworkManager.bootArgs(networkConfig.slot);
|
|
161
|
-
consola.debug(`Boot args: ${bootArgs}`);
|
|
162
|
-
await vm.boot("kernel/vmlinux", bootArgs);
|
|
163
|
-
await vm.addDrive("rootfs", "rootfs/rootfs.ext4", true, false);
|
|
164
|
-
await vm.configure(state.vcpuCount, state.memSizeMib);
|
|
165
|
-
await vm.addNetwork("eth0", networkConfig.tapDevice, networkConfig.macAddress);
|
|
166
|
-
log.start("Starting VM...");
|
|
167
|
-
await vm.start();
|
|
168
|
-
const pid = getVmPid(vmId);
|
|
169
|
-
store.update(vmId, {
|
|
170
|
-
status: "running",
|
|
171
|
-
pid
|
|
172
|
-
});
|
|
173
|
-
log.success(`VM ${vmId} is running (PID: ${pid || "unknown"})`);
|
|
174
30
|
cmdLog.set({
|
|
175
31
|
vmId,
|
|
176
|
-
pid,
|
|
177
|
-
guestIp:
|
|
178
|
-
networking:
|
|
32
|
+
pid: result.pid,
|
|
33
|
+
guestIp: result.state?.network.guestIp,
|
|
34
|
+
networking: result.state?.network.networkPolicy
|
|
179
35
|
});
|
|
180
36
|
cmdLog.emit();
|
|
181
37
|
} catch (error) {
|
|
182
|
-
if (lifecycle.vmId) {
|
|
183
|
-
killOrphanVmProcess(lifecycle.vmId);
|
|
184
|
-
markVmAsError(lifecycle.vmId, error, paths);
|
|
185
|
-
}
|
|
186
|
-
cleanupNetwork(lifecycle.networkConfig);
|
|
187
38
|
handleCommandError(error, cmdLog);
|
|
188
39
|
process.exitCode = 1;
|
|
189
40
|
}
|
package/dist/_chunks/stop.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
import "./paths.mjs";
|
|
2
2
|
import "./errors.mjs";
|
|
3
3
|
import { t as createCommandLogger } from "./logger.mjs";
|
|
4
4
|
import "./vm-state.mjs";
|
|
5
|
-
import "./
|
|
6
|
-
import
|
|
5
|
+
import { t as createVmsan } from "./context.mjs";
|
|
6
|
+
import "./firecracker.mjs";
|
|
7
|
+
import "./timeout-killer.mjs";
|
|
7
8
|
import { consola } from "consola";
|
|
8
9
|
import { defineCommand } from "citty";
|
|
9
10
|
const stopCommand = defineCommand({
|
|
@@ -26,9 +27,9 @@ const stopCommand = defineCommand({
|
|
|
26
27
|
process.exitCode = 1;
|
|
27
28
|
return;
|
|
28
29
|
}
|
|
29
|
-
const
|
|
30
|
+
const vmsan = await createVmsan();
|
|
30
31
|
const missing = [];
|
|
31
|
-
for (const id of vmIds) if (!
|
|
32
|
+
for (const id of vmIds) if (!vmsan.get(id)) missing.push(id);
|
|
32
33
|
if (missing.length > 0) {
|
|
33
34
|
consola.error(`VM(s) not found: ${missing.join(", ")}`);
|
|
34
35
|
cmdLog.emit();
|
|
@@ -39,10 +40,10 @@ const stopCommand = defineCommand({
|
|
|
39
40
|
let hasErrors = false;
|
|
40
41
|
for (const id of vmIds) {
|
|
41
42
|
const log = consola.withTag(id);
|
|
42
|
-
const vm =
|
|
43
|
+
const vm = vmsan.get(id);
|
|
43
44
|
consola.debug(`VM ${id} current status: ${vm?.status ?? "unknown"}`);
|
|
44
45
|
log.start(`Stopping ${id}...`);
|
|
45
|
-
const result = await
|
|
46
|
+
const result = await vmsan.stop(id);
|
|
46
47
|
if (result.alreadyStopped) {
|
|
47
48
|
log.warn(`${id} is already stopped`);
|
|
48
49
|
results.push({
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { V as policyConflictError } from "./errors.mjs";
|
|
2
|
+
import { s as parseDuration } from "./vm-state.mjs";
|
|
3
|
+
import { d as assertSnapshotExists } from "./context.mjs";
|
|
4
|
+
import { c as parsePublishedPorts, d as validateCidr, f as validatePublishedPortsAvailable, i as parseDomains, l as parseRuntime, n as parseCidrList, o as parseMemoryMib, r as parseDiskSizeGb, s as parseNetworkPolicy, u as parseVcpuCount } from "./validation.mjs";
|
|
5
|
+
function parseCreateInput(args, paths) {
|
|
6
|
+
const vcpus = parseVcpuCount(args.vcpus);
|
|
7
|
+
const memMib = parseMemoryMib(args.memory);
|
|
8
|
+
const runtime = parseRuntime(args.runtime);
|
|
9
|
+
const networkPolicy = parseNetworkPolicy(args["network-policy"]);
|
|
10
|
+
const ports = parsePublishedPorts(args["publish-port"]);
|
|
11
|
+
const domains = parseDomains(args["allowed-domain"]);
|
|
12
|
+
const allowedCidrs = parseCidrList(args["allowed-cidr"]);
|
|
13
|
+
const deniedCidrs = parseCidrList(args["denied-cidr"]);
|
|
14
|
+
const timeoutMs = typeof args.timeout === "string" ? parseDuration(args.timeout) : null;
|
|
15
|
+
const snapshotId = typeof args.snapshot === "string" ? args.snapshot : null;
|
|
16
|
+
const diskSizeGb = parseDiskSizeGb(args.disk);
|
|
17
|
+
validatePublishedPortsAvailable(ports, paths);
|
|
18
|
+
for (const cidr of allowedCidrs) validateCidr(cidr);
|
|
19
|
+
for (const cidr of deniedCidrs) validateCidr(cidr);
|
|
20
|
+
if (networkPolicy === "deny-all" && (domains.length || allowedCidrs.length || deniedCidrs.length)) throw policyConflictError();
|
|
21
|
+
if (snapshotId) assertSnapshotExists(snapshotId, paths);
|
|
22
|
+
return {
|
|
23
|
+
vcpus,
|
|
24
|
+
memMib,
|
|
25
|
+
runtime,
|
|
26
|
+
networkPolicy: networkPolicy === "allow-all" && (domains.length > 0 || allowedCidrs.length > 0 || deniedCidrs.length > 0) ? "custom" : networkPolicy,
|
|
27
|
+
ports,
|
|
28
|
+
domains,
|
|
29
|
+
allowedCidrs,
|
|
30
|
+
deniedCidrs,
|
|
31
|
+
timeoutMs,
|
|
32
|
+
snapshotId,
|
|
33
|
+
diskSizeGb
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function buildCreateSummaryLines(input) {
|
|
37
|
+
return [
|
|
38
|
+
`VM Created: ${input.vmId}`,
|
|
39
|
+
"",
|
|
40
|
+
" Status: running",
|
|
41
|
+
` PID: ${input.pid || "unknown"}`,
|
|
42
|
+
` vCPUs: ${input.vcpus}`,
|
|
43
|
+
` Memory: ${input.memMib} MiB`,
|
|
44
|
+
` Runtime: ${input.runtime}`,
|
|
45
|
+
` Disk: ${input.diskSizeGb} GB`,
|
|
46
|
+
...input.project ? [` Project: ${input.project}`] : [],
|
|
47
|
+
"",
|
|
48
|
+
" Network:",
|
|
49
|
+
` TAP: ${input.tapDevice}`,
|
|
50
|
+
` Host: ${input.hostIp}`,
|
|
51
|
+
` Guest: ${input.guestIp}`,
|
|
52
|
+
` MAC: ${input.macAddress}`,
|
|
53
|
+
` Policy: ${input.networkPolicy}`,
|
|
54
|
+
...input.domains.length > 0 ? [` Domains: ${input.domains.join(", ")}`] : [],
|
|
55
|
+
...input.allowedCidrs.length > 0 ? [` Allowed CIDRs: ${input.allowedCidrs.join(", ")}`] : [],
|
|
56
|
+
...input.deniedCidrs.length > 0 ? [` Denied CIDRs: ${input.deniedCidrs.join(", ")}`] : [],
|
|
57
|
+
...input.ports.length > 0 ? [` Ports: ${input.ports.join(", ")}`] : [],
|
|
58
|
+
"",
|
|
59
|
+
` Kernel: ${input.kernelPath}`,
|
|
60
|
+
` Rootfs: ${input.rootfsPath}`,
|
|
61
|
+
...input.snapshotId ? [` Snapshot: ${input.snapshotId}`] : [],
|
|
62
|
+
...input.timeout ? [` Timeout: ${input.timeout}`] : [],
|
|
63
|
+
"",
|
|
64
|
+
` Socket: ${input.socketPath}`,
|
|
65
|
+
` Chroot: ${input.chrootDir}`,
|
|
66
|
+
` State: ${input.stateFilePath}`
|
|
67
|
+
];
|
|
68
|
+
}
|
|
69
|
+
export { parseCreateInput as n, buildCreateSummaryLines as t };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { c as safeKill } from "./vm-state.mjs";
|
|
2
|
+
import { t as spawnTimeoutKiller } from "./timeout-killer.mjs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
const DEFAULT_INTERVAL_MS = 300 * 1e3;
|
|
5
|
+
var TimeoutExtender = class {
|
|
6
|
+
_timer = null;
|
|
7
|
+
_previousKillerPid = null;
|
|
8
|
+
_vmId;
|
|
9
|
+
_store;
|
|
10
|
+
_paths;
|
|
11
|
+
_intervalMs;
|
|
12
|
+
_signal;
|
|
13
|
+
constructor(opts) {
|
|
14
|
+
this._vmId = opts.vmId;
|
|
15
|
+
this._store = opts.store;
|
|
16
|
+
this._paths = opts.paths;
|
|
17
|
+
this._intervalMs = opts.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
18
|
+
this._signal = opts.signal;
|
|
19
|
+
}
|
|
20
|
+
start() {
|
|
21
|
+
if (this._timer) return;
|
|
22
|
+
if (this._signal) {
|
|
23
|
+
this._signal.addEventListener("abort", () => this.stop(), { once: true });
|
|
24
|
+
if (this._signal.aborted) {
|
|
25
|
+
this.stop();
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
this._extendSafe();
|
|
30
|
+
this._timer = setInterval(() => this._extendSafe(), this._intervalMs);
|
|
31
|
+
}
|
|
32
|
+
stop() {
|
|
33
|
+
if (this._timer) {
|
|
34
|
+
clearInterval(this._timer);
|
|
35
|
+
this._timer = null;
|
|
36
|
+
}
|
|
37
|
+
if (this._previousKillerPid !== null) {
|
|
38
|
+
safeKill(this._previousKillerPid);
|
|
39
|
+
this._previousKillerPid = null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
_extendSafe() {
|
|
43
|
+
try {
|
|
44
|
+
this._extend();
|
|
45
|
+
} catch {
|
|
46
|
+
this.stop();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
_extend() {
|
|
50
|
+
const state = this._store.load(this._vmId);
|
|
51
|
+
if (!state || state.status !== "running" || !state.timeoutMs) return;
|
|
52
|
+
const timeoutAt = new Date(Date.now() + state.timeoutMs).toISOString();
|
|
53
|
+
this._store.update(this._vmId, { timeoutAt });
|
|
54
|
+
if (this._previousKillerPid !== null) {
|
|
55
|
+
safeKill(this._previousKillerPid);
|
|
56
|
+
this._previousKillerPid = null;
|
|
57
|
+
}
|
|
58
|
+
if (state.pid) this._previousKillerPid = spawnTimeoutKiller({
|
|
59
|
+
vmId: this._vmId,
|
|
60
|
+
pid: state.pid,
|
|
61
|
+
timeoutMs: state.timeoutMs,
|
|
62
|
+
stateFile: join(this._paths.vmsDir, `${this._vmId}.json`)
|
|
63
|
+
}).pid ?? null;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
export { TimeoutExtender as t };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
/**
|
|
3
|
+
* Spawn a detached bash process that kills the VM after timeout.
|
|
4
|
+
* The process sleeps for the timeout duration, then verifies the VM
|
|
5
|
+
* is still running with the expected PID before sending SIGTERM.
|
|
6
|
+
*/
|
|
7
|
+
function spawnTimeoutKiller(opts) {
|
|
8
|
+
const { vmId, pid, timeoutMs, stateFile } = opts;
|
|
9
|
+
const timeoutSec = String(Math.ceil(timeoutMs / 1e3));
|
|
10
|
+
const killer = spawn("bash", [
|
|
11
|
+
"-c",
|
|
12
|
+
[
|
|
13
|
+
"sleep \"$1\"",
|
|
14
|
+
"STATE=$(cat -- \"$2\" 2>/dev/null) || exit 0",
|
|
15
|
+
"echo \"$STATE\" | grep -q '\"status\":\"running\"' || exit 0",
|
|
16
|
+
"echo \"$STATE\" | grep -q '\"pid\":'\"$3\" || exit 0",
|
|
17
|
+
"[ -d \"/proc/$3\" ] || exit 0",
|
|
18
|
+
"grep -aq -- \"$4\" \"/proc/$3/cmdline\" 2>/dev/null || exit 0",
|
|
19
|
+
"kill -- \"$3\" 2>/dev/null"
|
|
20
|
+
].join(" && "),
|
|
21
|
+
"bash",
|
|
22
|
+
timeoutSec,
|
|
23
|
+
stateFile,
|
|
24
|
+
String(pid),
|
|
25
|
+
vmId
|
|
26
|
+
], {
|
|
27
|
+
detached: true,
|
|
28
|
+
stdio: "ignore"
|
|
29
|
+
});
|
|
30
|
+
killer.unref();
|
|
31
|
+
return killer;
|
|
32
|
+
}
|
|
33
|
+
export { spawnTimeoutKiller as t };
|
package/dist/_chunks/upload.mjs
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { n as vmsanPaths } from "./paths.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { t as handleCommandError } from "./errors.mjs";
|
|
3
3
|
import { t as createCommandLogger } from "./logger.mjs";
|
|
4
|
+
import "./vm-state.mjs";
|
|
4
5
|
import { t as AgentClient } from "./agent.mjs";
|
|
5
|
-
import { t as
|
|
6
|
-
import { t as waitForAgent } from "./connect.mjs";
|
|
6
|
+
import { n as waitForAgent, t as resolveVmState } from "./vm-context.mjs";
|
|
7
7
|
import { basename } from "node:path";
|
|
8
|
-
import { consola } from "consola";
|
|
9
8
|
import { readFileSync } from "node:fs";
|
|
9
|
+
import { consola } from "consola";
|
|
10
10
|
import { defineCommand } from "citty";
|
|
11
11
|
const uploadCommand = defineCommand({
|
|
12
12
|
meta: {
|
|
@@ -30,23 +30,8 @@ const uploadCommand = defineCommand({
|
|
|
30
30
|
const cmdLog = createCommandLogger("upload");
|
|
31
31
|
const paths = vmsanPaths();
|
|
32
32
|
try {
|
|
33
|
-
const state =
|
|
34
|
-
if (!state) throw vmNotFoundError(args.vmId);
|
|
35
|
-
if (state.status !== "running") {
|
|
36
|
-
consola.error(`VM ${args.vmId} is not running (status: ${state.status})`);
|
|
37
|
-
cmdLog.emit();
|
|
38
|
-
process.exitCode = 1;
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
if (!state.agentToken) {
|
|
42
|
-
consola.error("VM has no agent token. Cannot upload files without the agent.");
|
|
43
|
-
cmdLog.emit();
|
|
44
|
-
process.exitCode = 1;
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
33
|
+
const { state, guestIp, port } = resolveVmState(args.vmId, paths);
|
|
47
34
|
const log = consola.withTag(args.vmId);
|
|
48
|
-
const guestIp = state.network.guestIp;
|
|
49
|
-
const port = state.agentPort || paths.agentPort;
|
|
50
35
|
consola.debug(`Agent endpoint: ${guestIp}:${port}`);
|
|
51
36
|
log.start("Waiting for agent...");
|
|
52
37
|
await waitForAgent(guestIp, port);
|