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
|
@@ -1,329 +0,0 @@
|
|
|
1
|
-
import { B as policyConflictError } from "./errors.mjs";
|
|
2
|
-
import { a as parseDuration } from "./vm-state.mjs";
|
|
3
|
-
import { t as assertSnapshotExists } from "./environment.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
|
-
import { dirname, join } from "node:path";
|
|
6
|
-
import { execFileSync, execSync } from "node:child_process";
|
|
7
|
-
import { consola } from "consola";
|
|
8
|
-
import { copyFileSync, existsSync, mkdirSync, statSync, writeFileSync } from "node:fs";
|
|
9
|
-
function parseCreateInput(args, paths) {
|
|
10
|
-
const vcpus = parseVcpuCount(args.vcpus);
|
|
11
|
-
const memMib = parseMemoryMib(args.memory);
|
|
12
|
-
const runtime = parseRuntime(args.runtime);
|
|
13
|
-
const networkPolicy = parseNetworkPolicy(args["network-policy"]);
|
|
14
|
-
const ports = parsePublishedPorts(args["publish-port"]);
|
|
15
|
-
const domains = parseDomains(args["allowed-domain"]);
|
|
16
|
-
const allowedCidrs = parseCidrList(args["allowed-cidr"]);
|
|
17
|
-
const deniedCidrs = parseCidrList(args["denied-cidr"]);
|
|
18
|
-
const timeoutMs = typeof args.timeout === "string" ? parseDuration(args.timeout) : null;
|
|
19
|
-
const snapshotId = typeof args.snapshot === "string" ? args.snapshot : null;
|
|
20
|
-
const diskSizeGb = parseDiskSizeGb(args.disk);
|
|
21
|
-
validatePublishedPortsAvailable(ports, paths);
|
|
22
|
-
for (const cidr of allowedCidrs) validateCidr(cidr);
|
|
23
|
-
for (const cidr of deniedCidrs) validateCidr(cidr);
|
|
24
|
-
if (networkPolicy === "deny-all" && (domains.length || allowedCidrs.length || deniedCidrs.length)) throw policyConflictError();
|
|
25
|
-
if (snapshotId) assertSnapshotExists(snapshotId, paths);
|
|
26
|
-
return {
|
|
27
|
-
vcpus,
|
|
28
|
-
memMib,
|
|
29
|
-
runtime,
|
|
30
|
-
networkPolicy: networkPolicy === "allow-all" && (domains.length > 0 || allowedCidrs.length > 0 || deniedCidrs.length > 0) ? "custom" : networkPolicy,
|
|
31
|
-
ports,
|
|
32
|
-
domains,
|
|
33
|
-
allowedCidrs,
|
|
34
|
-
deniedCidrs,
|
|
35
|
-
timeoutMs,
|
|
36
|
-
snapshotId,
|
|
37
|
-
diskSizeGb
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
function buildInitialVmState(input) {
|
|
41
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
42
|
-
return {
|
|
43
|
-
id: input.vmId,
|
|
44
|
-
project: input.project,
|
|
45
|
-
runtime: input.runtime,
|
|
46
|
-
diskSizeGb: input.diskSizeGb,
|
|
47
|
-
status: "creating",
|
|
48
|
-
pid: null,
|
|
49
|
-
apiSocket: "",
|
|
50
|
-
chrootDir: "",
|
|
51
|
-
kernel: input.kernelPath,
|
|
52
|
-
rootfs: input.rootfsPath,
|
|
53
|
-
vcpuCount: input.vcpus,
|
|
54
|
-
memSizeMib: input.memMib,
|
|
55
|
-
network: {
|
|
56
|
-
tapDevice: input.tapDevice,
|
|
57
|
-
hostIp: input.hostIp,
|
|
58
|
-
guestIp: input.guestIp,
|
|
59
|
-
subnetMask: input.subnetMask,
|
|
60
|
-
macAddress: input.macAddress,
|
|
61
|
-
networkPolicy: input.networkPolicy,
|
|
62
|
-
allowedDomains: input.domains,
|
|
63
|
-
allowedCidrs: input.allowedCidrs,
|
|
64
|
-
deniedCidrs: input.deniedCidrs,
|
|
65
|
-
publishedPorts: input.ports,
|
|
66
|
-
tunnelHostname: null,
|
|
67
|
-
tunnelHostnames: [],
|
|
68
|
-
bandwidthMbit: input.bandwidthMbit,
|
|
69
|
-
netnsName: input.netnsName
|
|
70
|
-
},
|
|
71
|
-
snapshot: input.snapshotId,
|
|
72
|
-
timeoutMs: input.timeoutMs,
|
|
73
|
-
timeoutAt: input.timeoutMs ? new Date(Date.now() + input.timeoutMs).toISOString() : null,
|
|
74
|
-
createdAt: now,
|
|
75
|
-
error: null,
|
|
76
|
-
agentToken: input.agentToken,
|
|
77
|
-
agentPort: input.agentPort
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
function buildCreateSummaryLines(input) {
|
|
81
|
-
return [
|
|
82
|
-
`VM Created: ${input.vmId}`,
|
|
83
|
-
"",
|
|
84
|
-
" Status: running",
|
|
85
|
-
` PID: ${input.pid || "unknown"}`,
|
|
86
|
-
` vCPUs: ${input.vcpus}`,
|
|
87
|
-
` Memory: ${input.memMib} MiB`,
|
|
88
|
-
` Runtime: ${input.runtime}`,
|
|
89
|
-
` Disk: ${input.diskSizeGb} GB`,
|
|
90
|
-
...input.project ? [` Project: ${input.project}`] : [],
|
|
91
|
-
"",
|
|
92
|
-
" Network:",
|
|
93
|
-
` TAP: ${input.tapDevice}`,
|
|
94
|
-
` Host: ${input.hostIp}`,
|
|
95
|
-
` Guest: ${input.guestIp}`,
|
|
96
|
-
` MAC: ${input.macAddress}`,
|
|
97
|
-
` Policy: ${input.networkPolicy}`,
|
|
98
|
-
...input.domains.length > 0 ? [` Domains: ${input.domains.join(", ")}`] : [],
|
|
99
|
-
...input.allowedCidrs.length > 0 ? [` Allowed CIDRs: ${input.allowedCidrs.join(", ")}`] : [],
|
|
100
|
-
...input.deniedCidrs.length > 0 ? [` Denied CIDRs: ${input.deniedCidrs.join(", ")}`] : [],
|
|
101
|
-
...input.ports.length > 0 ? [` Ports: ${input.ports.join(", ")}`] : [],
|
|
102
|
-
"",
|
|
103
|
-
` Kernel: ${input.kernelPath}`,
|
|
104
|
-
` Rootfs: ${input.rootfsPath}`,
|
|
105
|
-
...input.snapshotId ? [` Snapshot: ${input.snapshotId}`] : [],
|
|
106
|
-
...input.timeout ? [` Timeout: ${input.timeout}`] : [],
|
|
107
|
-
"",
|
|
108
|
-
` Socket: ${input.socketPath}`,
|
|
109
|
-
` Chroot: ${input.chrootDir}`,
|
|
110
|
-
` State: ${input.stateFilePath}`
|
|
111
|
-
];
|
|
112
|
-
}
|
|
113
|
-
const VALID_ARCHES = ["x86_64", "aarch64"];
|
|
114
|
-
const MAX_FILTER_SIZE = 1048576;
|
|
115
|
-
/**
|
|
116
|
-
* Compile a Firecracker seccomp JSON filter to BPF using seccompiler-bin.
|
|
117
|
-
* Falls back to using the JSON filter directly if seccompiler-bin is not available.
|
|
118
|
-
*/
|
|
119
|
-
function compileSeccompFilter(jsonPath, outputPath, arch) {
|
|
120
|
-
const targetArch = arch ?? "x86_64";
|
|
121
|
-
if (!VALID_ARCHES.includes(targetArch)) throw new Error(`unsupported seccomp arch: ${targetArch} (allowed: ${VALID_ARCHES.join(", ")})`);
|
|
122
|
-
const stat = statSync(jsonPath);
|
|
123
|
-
if (stat.size > MAX_FILTER_SIZE) throw new Error(`seccomp filter too large: ${stat.size} bytes (max ${MAX_FILTER_SIZE})`);
|
|
124
|
-
mkdirSync(dirname(outputPath), { recursive: true });
|
|
125
|
-
execFileSync("seccompiler-bin", [
|
|
126
|
-
"--input-file",
|
|
127
|
-
jsonPath,
|
|
128
|
-
"--target-arch",
|
|
129
|
-
targetArch,
|
|
130
|
-
"--output-file",
|
|
131
|
-
outputPath
|
|
132
|
-
], { stdio: "pipe" });
|
|
133
|
-
}
|
|
134
|
-
/**
|
|
135
|
-
* Ensure a seccomp filter is available for Firecracker.
|
|
136
|
-
*
|
|
137
|
-
* 1. If a compiled BPF exists at paths.seccompDir/default.bpf, return it.
|
|
138
|
-
* 2. If the JSON source exists, try to compile it; return BPF path on success.
|
|
139
|
-
* 3. If compilation fails (seccompiler-bin not installed), return null
|
|
140
|
-
* (Firecracker requires compiled BPF, not raw JSON).
|
|
141
|
-
* 4. If no filter source exists at all, return null.
|
|
142
|
-
*/
|
|
143
|
-
function ensureSeccompFilter(paths) {
|
|
144
|
-
const bpfPath = join(paths.seccompDir, "default.bpf");
|
|
145
|
-
if (existsSync(bpfPath)) {
|
|
146
|
-
consola.debug(`seccomp: using compiled BPF filter at ${bpfPath}`);
|
|
147
|
-
return bpfPath;
|
|
148
|
-
}
|
|
149
|
-
const bundledJson = join(dirname(dirname(__dirname)), "seccomp", "default.json");
|
|
150
|
-
const userJson = paths.seccompFilter;
|
|
151
|
-
let sourceJson = null;
|
|
152
|
-
try {
|
|
153
|
-
const mode = statSync(userJson).mode;
|
|
154
|
-
if (mode & 18) consola.warn(`seccomp: filter at ${userJson} is group/world writable (mode ${(mode & 511).toString(8)}); consider restricting permissions`);
|
|
155
|
-
consola.debug(`seccomp: using user filter at ${userJson}`);
|
|
156
|
-
sourceJson = userJson;
|
|
157
|
-
} catch {}
|
|
158
|
-
if (!sourceJson && existsSync(bundledJson)) {
|
|
159
|
-
mkdirSync(paths.seccompDir, { recursive: true });
|
|
160
|
-
copyFileSync(bundledJson, userJson);
|
|
161
|
-
consola.debug(`seccomp: copied bundled filter to ${userJson}`);
|
|
162
|
-
sourceJson = userJson;
|
|
163
|
-
}
|
|
164
|
-
if (!sourceJson) return null;
|
|
165
|
-
try {
|
|
166
|
-
compileSeccompFilter(sourceJson, bpfPath);
|
|
167
|
-
consola.debug(`seccomp: compiled BPF filter at ${bpfPath}`);
|
|
168
|
-
return bpfPath;
|
|
169
|
-
} catch {
|
|
170
|
-
consola.warn("seccomp: BPF compilation failed (seccompiler-bin not available?); seccomp filtering disabled. Install seccompiler-bin for seccomp support.");
|
|
171
|
-
return null;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
function dockerUnavailableError() {
|
|
175
|
-
return /* @__PURE__ */ new Error("Docker is not available. Install Docker and ensure the daemon is running.");
|
|
176
|
-
}
|
|
177
|
-
const APT_PACKAGES = [
|
|
178
|
-
"bind9-utils",
|
|
179
|
-
"bzip2",
|
|
180
|
-
"findutils",
|
|
181
|
-
"git",
|
|
182
|
-
"gzip",
|
|
183
|
-
"iputils-ping",
|
|
184
|
-
"libicu-dev",
|
|
185
|
-
"libjpeg-dev",
|
|
186
|
-
"libpng-dev",
|
|
187
|
-
"ncurses-base",
|
|
188
|
-
"libssl-dev",
|
|
189
|
-
"openssh-server",
|
|
190
|
-
"openssl",
|
|
191
|
-
"procps",
|
|
192
|
-
"tar",
|
|
193
|
-
"unzip",
|
|
194
|
-
"debianutils",
|
|
195
|
-
"whois",
|
|
196
|
-
"zstd"
|
|
197
|
-
];
|
|
198
|
-
const DNF_PACKAGES = [
|
|
199
|
-
"bind-utils",
|
|
200
|
-
"bzip2",
|
|
201
|
-
"findutils",
|
|
202
|
-
"git",
|
|
203
|
-
"gzip",
|
|
204
|
-
"iputils",
|
|
205
|
-
"libicu",
|
|
206
|
-
"libjpeg",
|
|
207
|
-
"libpng",
|
|
208
|
-
"ncurses-libs",
|
|
209
|
-
"openssh-server",
|
|
210
|
-
"openssl",
|
|
211
|
-
"openssl-libs",
|
|
212
|
-
"procps",
|
|
213
|
-
"tar",
|
|
214
|
-
"unzip",
|
|
215
|
-
"which",
|
|
216
|
-
"whois",
|
|
217
|
-
"zstd"
|
|
218
|
-
];
|
|
219
|
-
const APK_PACKAGES = [
|
|
220
|
-
"bind-tools",
|
|
221
|
-
"bzip2",
|
|
222
|
-
"findutils",
|
|
223
|
-
"git",
|
|
224
|
-
"gzip",
|
|
225
|
-
"iputils",
|
|
226
|
-
"icu-libs",
|
|
227
|
-
"libjpeg-turbo",
|
|
228
|
-
"libpng",
|
|
229
|
-
"ncurses-libs",
|
|
230
|
-
"openrc",
|
|
231
|
-
"openssh",
|
|
232
|
-
"openssl",
|
|
233
|
-
"procps",
|
|
234
|
-
"tar",
|
|
235
|
-
"unzip",
|
|
236
|
-
"whois",
|
|
237
|
-
"zstd"
|
|
238
|
-
];
|
|
239
|
-
function generateDockerfile(baseImage) {
|
|
240
|
-
return `FROM ${baseImage}
|
|
241
|
-
RUN if command -v apt-get >/dev/null 2>&1; then ${`apt-get update && apt-get install -y --no-install-recommends ${APT_PACKAGES.join(" ")} && rm -rf /var/lib/apt/lists/*`}; \\
|
|
242
|
-
elif command -v dnf >/dev/null 2>&1; then ${`dnf install -y ${DNF_PACKAGES.join(" ")} && dnf clean all`}; \\
|
|
243
|
-
elif command -v yum >/dev/null 2>&1; then ${`yum install -y ${DNF_PACKAGES.join(" ")} && yum clean all`}; \\
|
|
244
|
-
elif command -v apk >/dev/null 2>&1; then ${`apk add --no-cache ${APK_PACKAGES.join(" ")}`}; \\
|
|
245
|
-
fi
|
|
246
|
-
RUN ssh-keygen -A 2>/dev/null || true; \\
|
|
247
|
-
mkdir -p /root/.ssh && chmod 700 /root/.ssh; \\
|
|
248
|
-
if [ -f /etc/ssh/sshd_config ]; then \\
|
|
249
|
-
sed -i 's/^#*PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config; \\
|
|
250
|
-
fi; \\
|
|
251
|
-
if command -v rc-update >/dev/null 2>&1; then \\
|
|
252
|
-
rc-update add devfs sysinit 2>/dev/null || true; \\
|
|
253
|
-
rc-update add mdev sysinit 2>/dev/null || true; \\
|
|
254
|
-
rc-update add hwdrivers sysinit 2>/dev/null || true; \\
|
|
255
|
-
rc-update add modules boot 2>/dev/null || true; \\
|
|
256
|
-
rc-update add sysctl boot 2>/dev/null || true; \\
|
|
257
|
-
rc-update add hostname boot 2>/dev/null || true; \\
|
|
258
|
-
rc-update add bootmisc boot 2>/dev/null || true; \\
|
|
259
|
-
rc-update add networking boot 2>/dev/null || true; \\
|
|
260
|
-
rc-update add sshd default 2>/dev/null || true; \\
|
|
261
|
-
printf '%s\\n' '::sysinit:/sbin/openrc sysinit' '::sysinit:/sbin/openrc boot' '::wait:/sbin/openrc default' '::shutdown:/sbin/openrc shutdown' 'ttyS0::respawn:/sbin/getty 115200 ttyS0' > /etc/inittab; \\
|
|
262
|
-
fi; \\
|
|
263
|
-
if command -v systemctl >/dev/null 2>&1; then systemctl enable sshd 2>/dev/null || systemctl enable ssh 2>/dev/null || true; fi
|
|
264
|
-
`;
|
|
265
|
-
}
|
|
266
|
-
function verifyDocker() {
|
|
267
|
-
try {
|
|
268
|
-
execSync("docker info", { stdio: "pipe" });
|
|
269
|
-
} catch {
|
|
270
|
-
throw dockerUnavailableError();
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
function buildImageRootfs(imageRef, cacheDir) {
|
|
274
|
-
const ext4Path = join(cacheDir, "rootfs.ext4");
|
|
275
|
-
verifyDocker();
|
|
276
|
-
const buildTag = `vmsan-rootfs-${imageRef.name.replace(/[^a-z0-9._-]/gi, "-")}:${imageRef.tag}`;
|
|
277
|
-
const containerName = `vmsan-export-${Date.now()}`;
|
|
278
|
-
const tmpTar = join(cacheDir, "rootfs.tar");
|
|
279
|
-
mkdirSync(cacheDir, { recursive: true });
|
|
280
|
-
try {
|
|
281
|
-
consola.start(`Building image from ${imageRef.full}...`);
|
|
282
|
-
execSync(`docker build -t "${buildTag}" -f - . <<'DOCKERFILE'\n${generateDockerfile(imageRef.full)}\nDOCKERFILE`, {
|
|
283
|
-
stdio: "pipe",
|
|
284
|
-
shell: "/bin/bash"
|
|
285
|
-
});
|
|
286
|
-
consola.start("Exporting filesystem...");
|
|
287
|
-
execSync(`docker create --name "${containerName}" "${buildTag}"`, { stdio: "pipe" });
|
|
288
|
-
execSync(`docker export "${containerName}" -o "${tmpTar}"`, { stdio: "pipe" });
|
|
289
|
-
consola.start("Converting to ext4...");
|
|
290
|
-
const tarSizeOutput = execSync(`stat -c %s "${tmpTar}"`, { encoding: "utf-8" }).trim();
|
|
291
|
-
const tarMb = Number(tarSizeOutput) / 1024 / 1024;
|
|
292
|
-
const imageSizeMb = Math.max(1024, Math.ceil(tarMb + 512));
|
|
293
|
-
execSync(`dd if=/dev/zero of="${ext4Path}" bs=1M count=${imageSizeMb} 2>/dev/null`, { stdio: "pipe" });
|
|
294
|
-
execSync(`mkfs.ext4 -q "${ext4Path}"`, { stdio: "pipe" });
|
|
295
|
-
execSync(`tune2fs -m 0 "${ext4Path}"`, { stdio: "pipe" });
|
|
296
|
-
const tmpMount = join(cacheDir, "mnt");
|
|
297
|
-
mkdirSync(tmpMount, { recursive: true });
|
|
298
|
-
execSync(`mount -o loop "${ext4Path}" "${tmpMount}"`, { stdio: "pipe" });
|
|
299
|
-
try {
|
|
300
|
-
execSync(`tar -xf "${tmpTar}" -C "${tmpMount}"`, { stdio: "pipe" });
|
|
301
|
-
} finally {
|
|
302
|
-
execSync(`umount "${tmpMount}"`, { stdio: "pipe" });
|
|
303
|
-
execSync(`rm -rf "${tmpMount}"`, { stdio: "pipe" });
|
|
304
|
-
}
|
|
305
|
-
writeFileSync(join(cacheDir, "metadata.json"), JSON.stringify({
|
|
306
|
-
image: imageRef.full,
|
|
307
|
-
builtAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
308
|
-
}, null, 2));
|
|
309
|
-
consola.success(`Rootfs built from ${imageRef.full} (${imageSizeMb} MB)`);
|
|
310
|
-
return ext4Path;
|
|
311
|
-
} finally {
|
|
312
|
-
try {
|
|
313
|
-
execSync(`docker rm -f "${containerName}" 2>/dev/null`, { stdio: "pipe" });
|
|
314
|
-
} catch {}
|
|
315
|
-
try {
|
|
316
|
-
execSync(`rm -f "${tmpTar}"`, { stdio: "pipe" });
|
|
317
|
-
} catch {}
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
function resolveImageRootfs(imageRef, registryDir) {
|
|
321
|
-
const cacheDir = join(registryDir, imageRef.cacheKey);
|
|
322
|
-
const ext4Path = join(cacheDir, "rootfs.ext4");
|
|
323
|
-
if (existsSync(ext4Path)) {
|
|
324
|
-
consola.info(`Using cached rootfs for ${imageRef.full}`);
|
|
325
|
-
return ext4Path;
|
|
326
|
-
}
|
|
327
|
-
return buildImageRootfs(imageRef, cacheDir);
|
|
328
|
-
}
|
|
329
|
-
export { buildInitialVmState as a, buildCreateSummaryLines as i, compileSeccompFilter as n, parseCreateInput as o, ensureSeccompFilter as r, resolveImageRootfs as t };
|
package/dist/_chunks/vm.mjs
DELETED
|
@@ -1,208 +0,0 @@
|
|
|
1
|
-
import { H as VmsanError, S as vmNotStoppedError, b as vmNotFoundError, u as lockTimeoutError, x as vmNotRunningError } from "./errors.mjs";
|
|
2
|
-
import { o as safeKill, t as FileVmStateStore } from "./vm-state.mjs";
|
|
3
|
-
import { a as getVmPid, c as NetworkManager, i as getVmJailerPid } from "./environment.mjs";
|
|
4
|
-
import { dirname, join } from "node:path";
|
|
5
|
-
import { mkdirSync, rmSync } from "node:fs";
|
|
6
|
-
import { lock, lockSync } from "proper-lockfile";
|
|
7
|
-
const STALE_MS = 3e5;
|
|
8
|
-
const RETRY_MS = 50;
|
|
9
|
-
const MAX_RETRIES = 600;
|
|
10
|
-
const WAIT_ARRAY = new Int32Array(new SharedArrayBuffer(4));
|
|
11
|
-
var FileLock = class {
|
|
12
|
-
stale;
|
|
13
|
-
retries;
|
|
14
|
-
realpath;
|
|
15
|
-
constructor(path, name, options) {
|
|
16
|
-
this.path = path;
|
|
17
|
-
this.name = name;
|
|
18
|
-
this.stale = options?.stale ?? STALE_MS;
|
|
19
|
-
this.retries = options?.retries ?? MAX_RETRIES;
|
|
20
|
-
this.realpath = options?.realpath ?? false;
|
|
21
|
-
}
|
|
22
|
-
run(fn) {
|
|
23
|
-
mkdirSync(dirname(this.path), { recursive: true });
|
|
24
|
-
const syncOpts = {
|
|
25
|
-
stale: this.stale,
|
|
26
|
-
realpath: this.realpath
|
|
27
|
-
};
|
|
28
|
-
let release;
|
|
29
|
-
for (let attempt = 0;; attempt++) try {
|
|
30
|
-
release = lockSync(this.path, syncOpts);
|
|
31
|
-
break;
|
|
32
|
-
} catch (error) {
|
|
33
|
-
if (error.code !== "ELOCKED") throw error;
|
|
34
|
-
if (attempt >= this.retries) throw lockTimeoutError(this.name);
|
|
35
|
-
Atomics.wait(WAIT_ARRAY, 0, 0, RETRY_MS);
|
|
36
|
-
}
|
|
37
|
-
try {
|
|
38
|
-
return fn();
|
|
39
|
-
} finally {
|
|
40
|
-
release();
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
async runAsync(fn) {
|
|
44
|
-
mkdirSync(dirname(this.path), { recursive: true });
|
|
45
|
-
const asyncOpts = {
|
|
46
|
-
stale: this.stale,
|
|
47
|
-
realpath: this.realpath,
|
|
48
|
-
retries: {
|
|
49
|
-
retries: this.retries,
|
|
50
|
-
minTimeout: RETRY_MS,
|
|
51
|
-
maxTimeout: RETRY_MS,
|
|
52
|
-
factor: 1
|
|
53
|
-
}
|
|
54
|
-
};
|
|
55
|
-
let release;
|
|
56
|
-
try {
|
|
57
|
-
release = await lock(this.path, asyncOpts);
|
|
58
|
-
} catch (error) {
|
|
59
|
-
if (error.code === "ELOCKED") throw lockTimeoutError(this.name);
|
|
60
|
-
throw error;
|
|
61
|
-
}
|
|
62
|
-
try {
|
|
63
|
-
return await fn();
|
|
64
|
-
} finally {
|
|
65
|
-
await release();
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
};
|
|
69
|
-
var VMService = class {
|
|
70
|
-
store;
|
|
71
|
-
constructor(paths) {
|
|
72
|
-
this.paths = paths;
|
|
73
|
-
this.store = new FileVmStateStore(paths.vmsDir);
|
|
74
|
-
}
|
|
75
|
-
list() {
|
|
76
|
-
return this.store.list().sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
77
|
-
}
|
|
78
|
-
get(vmId) {
|
|
79
|
-
return this.store.load(vmId);
|
|
80
|
-
}
|
|
81
|
-
async stop(vmId) {
|
|
82
|
-
const state = this.store.load(vmId);
|
|
83
|
-
if (!state) return {
|
|
84
|
-
vmId,
|
|
85
|
-
success: false,
|
|
86
|
-
error: vmNotFoundError(vmId)
|
|
87
|
-
};
|
|
88
|
-
if (state.status === "stopped") return {
|
|
89
|
-
vmId,
|
|
90
|
-
success: true,
|
|
91
|
-
alreadyStopped: true
|
|
92
|
-
};
|
|
93
|
-
try {
|
|
94
|
-
if (state.pid) safeKill(state.pid, "SIGKILL");
|
|
95
|
-
const orphanPid = getVmPid(vmId);
|
|
96
|
-
if (orphanPid) safeKill(orphanPid, "SIGKILL");
|
|
97
|
-
const orphanJailerPid = getVmJailerPid(vmId);
|
|
98
|
-
if (orphanJailerPid) safeKill(orphanJailerPid, "SIGKILL");
|
|
99
|
-
if (state.network) try {
|
|
100
|
-
NetworkManager.fromVmNetwork(state.network).teardown();
|
|
101
|
-
} catch {}
|
|
102
|
-
this.store.update(vmId, {
|
|
103
|
-
status: "stopped",
|
|
104
|
-
pid: null
|
|
105
|
-
});
|
|
106
|
-
return {
|
|
107
|
-
vmId,
|
|
108
|
-
success: true
|
|
109
|
-
};
|
|
110
|
-
} catch (err) {
|
|
111
|
-
return {
|
|
112
|
-
vmId,
|
|
113
|
-
success: false,
|
|
114
|
-
error: err instanceof VmsanError ? err : void 0
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
async updateNetworkPolicy(vmId, policy, domains, allowedCidrs, deniedCidrs) {
|
|
119
|
-
return new FileLock(join(this.paths.vmsDir, `${vmId}.json`), `update-policy-${vmId}`).run(() => {
|
|
120
|
-
const state = this.store.load(vmId);
|
|
121
|
-
if (!state) return {
|
|
122
|
-
vmId,
|
|
123
|
-
success: false,
|
|
124
|
-
previousPolicy: "",
|
|
125
|
-
newPolicy: policy,
|
|
126
|
-
error: vmNotFoundError(vmId)
|
|
127
|
-
};
|
|
128
|
-
if (state.status !== "running") return {
|
|
129
|
-
vmId,
|
|
130
|
-
success: false,
|
|
131
|
-
previousPolicy: state.network.networkPolicy,
|
|
132
|
-
newPolicy: policy,
|
|
133
|
-
error: vmNotRunningError(vmId)
|
|
134
|
-
};
|
|
135
|
-
const previousPolicy = state.network.networkPolicy;
|
|
136
|
-
try {
|
|
137
|
-
NetworkManager.fromVmNetwork(state.network).updatePolicy(policy, domains, allowedCidrs, deniedCidrs);
|
|
138
|
-
this.store.update(vmId, { network: {
|
|
139
|
-
...state.network,
|
|
140
|
-
networkPolicy: policy,
|
|
141
|
-
allowedDomains: domains,
|
|
142
|
-
allowedCidrs,
|
|
143
|
-
deniedCidrs
|
|
144
|
-
} });
|
|
145
|
-
return {
|
|
146
|
-
vmId,
|
|
147
|
-
success: true,
|
|
148
|
-
previousPolicy,
|
|
149
|
-
newPolicy: policy
|
|
150
|
-
};
|
|
151
|
-
} catch (err) {
|
|
152
|
-
return {
|
|
153
|
-
vmId,
|
|
154
|
-
success: false,
|
|
155
|
-
previousPolicy,
|
|
156
|
-
newPolicy: policy,
|
|
157
|
-
error: err instanceof VmsanError ? err : void 0
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
async remove(vmId, opts) {
|
|
163
|
-
const state = this.store.load(vmId);
|
|
164
|
-
if (!state) return {
|
|
165
|
-
vmId,
|
|
166
|
-
success: false,
|
|
167
|
-
error: vmNotFoundError(vmId)
|
|
168
|
-
};
|
|
169
|
-
try {
|
|
170
|
-
if (state.status !== "stopped") {
|
|
171
|
-
if (!opts?.force) return {
|
|
172
|
-
vmId,
|
|
173
|
-
success: false,
|
|
174
|
-
error: vmNotStoppedError(vmId, state.status)
|
|
175
|
-
};
|
|
176
|
-
const stopResult = await this.stop(vmId);
|
|
177
|
-
if (!stopResult.success) return stopResult;
|
|
178
|
-
}
|
|
179
|
-
if (state.chrootDir) {
|
|
180
|
-
const vmJailerDir = dirname(state.chrootDir);
|
|
181
|
-
try {
|
|
182
|
-
rmSync(state.chrootDir, {
|
|
183
|
-
recursive: true,
|
|
184
|
-
force: true
|
|
185
|
-
});
|
|
186
|
-
} catch {}
|
|
187
|
-
try {
|
|
188
|
-
rmSync(vmJailerDir, {
|
|
189
|
-
recursive: true,
|
|
190
|
-
force: true
|
|
191
|
-
});
|
|
192
|
-
} catch {}
|
|
193
|
-
}
|
|
194
|
-
this.store.delete(vmId);
|
|
195
|
-
return {
|
|
196
|
-
vmId,
|
|
197
|
-
success: true
|
|
198
|
-
};
|
|
199
|
-
} catch (err) {
|
|
200
|
-
return {
|
|
201
|
-
vmId,
|
|
202
|
-
success: false,
|
|
203
|
-
error: err instanceof VmsanError ? err : void 0
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
};
|
|
208
|
-
export { FileLock as n, VMService as t };
|