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/create.mjs
CHANGED
|
@@ -1,19 +1,16 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { t as handleCommandError
|
|
1
|
+
import "./paths.mjs";
|
|
2
|
+
import { t as handleCommandError } from "./errors.mjs";
|
|
3
3
|
import { i as initVmsanLogger, n as createScopedLogger, t as createCommandLogger } from "./logger.mjs";
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
7
|
-
import
|
|
4
|
+
import "./vm-state.mjs";
|
|
5
|
+
import { t as createVmsan } from "./context.mjs";
|
|
6
|
+
import "./firecracker.mjs";
|
|
7
|
+
import "./timeout-killer.mjs";
|
|
8
|
+
import { n as waitForAgent } from "./vm-context.mjs";
|
|
8
9
|
import { t as ShellSession } from "./shell.mjs";
|
|
9
10
|
import { a as parseImageReference, t as parseBandwidth } from "./validation.mjs";
|
|
10
|
-
import {
|
|
11
|
-
import { t as waitForAgent } from "./connect.mjs";
|
|
11
|
+
import { n as parseCreateInput, t as buildCreateSummaryLines } from "./summary.mjs";
|
|
12
12
|
import { join } from "node:path";
|
|
13
|
-
import { spawn } from "node:child_process";
|
|
14
13
|
import { consola } from "consola";
|
|
15
|
-
import { existsSync } from "node:fs";
|
|
16
|
-
import { randomBytes } from "node:crypto";
|
|
17
14
|
import { defineCommand } from "citty";
|
|
18
15
|
const createCommandArgs = {
|
|
19
16
|
vcpus: {
|
|
@@ -118,13 +115,6 @@ const createCommandArgs = {
|
|
|
118
115
|
}
|
|
119
116
|
};
|
|
120
117
|
const RUNTIME_DEFAULT_IMAGES = { "node22-demo": "node:22" };
|
|
121
|
-
function createLifecycleState() {
|
|
122
|
-
return {
|
|
123
|
-
networkConfig: void 0,
|
|
124
|
-
vmId: void 0,
|
|
125
|
-
chrootDir: void 0
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
118
|
const createCommand = defineCommand({
|
|
129
119
|
meta: {
|
|
130
120
|
name: "create",
|
|
@@ -135,161 +125,45 @@ const createCommand = defineCommand({
|
|
|
135
125
|
const commandArgs = args;
|
|
136
126
|
if (commandArgs.silent) initVmsanLogger("silent");
|
|
137
127
|
const cmdLog = createCommandLogger("create");
|
|
138
|
-
const lifecycle = createLifecycleState();
|
|
139
|
-
const paths = vmsanPaths();
|
|
140
|
-
const store = new FileVmStateStore(paths.vmsDir);
|
|
141
128
|
try {
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
if (commandArgs["from-image"] && commandArgs.rootfs) throw mutuallyExclusiveFlagsError("--from-image", "--rootfs");
|
|
145
|
-
const parsedInput = parseCreateInput(commandArgs, paths);
|
|
129
|
+
const vmsan = await createVmsan();
|
|
130
|
+
const parsedInput = parseCreateInput(commandArgs, vmsan.paths);
|
|
146
131
|
const defaultImage = RUNTIME_DEFAULT_IMAGES[parsedInput.runtime];
|
|
147
132
|
if (defaultImage && !commandArgs["from-image"] && !commandArgs.rootfs) {
|
|
148
133
|
commandArgs["from-image"] = defaultImage;
|
|
149
134
|
consola.info(`Runtime "${parsedInput.runtime}" auto-selected image: ${defaultImage}`);
|
|
150
135
|
}
|
|
151
136
|
if (parsedInput.runtime === "node22-demo" && parsedInput.ports.length === 0) consola.warn("Runtime node22-demo serves a welcome page, but no --publish-port was specified. The page won't be accessible externally.");
|
|
152
|
-
const kernelPath = typeof commandArgs.kernel === "string" ? commandArgs.kernel : findKernel(baseDir);
|
|
153
|
-
consola.debug(`Kernel resolved: ${kernelPath}`);
|
|
154
|
-
let rootfsPath;
|
|
155
|
-
if (typeof commandArgs["from-image"] === "string") rootfsPath = resolveImageRootfs(parseImageReference(commandArgs["from-image"]), paths.registryDir);
|
|
156
|
-
else rootfsPath = typeof commandArgs.rootfs === "string" ? commandArgs.rootfs : findRootfs(baseDir);
|
|
157
|
-
consola.debug(`Rootfs resolved: ${rootfsPath}`);
|
|
158
137
|
if (parsedInput.domains.length > 0) consola.warn("Domain filtering is DNS-based best effort. Direct-IP and DoH traffic may bypass allow-lists.");
|
|
159
138
|
const bandwidthMbit = parseBandwidth(commandArgs.bandwidth);
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
const slot = store.allocateNetworkSlot();
|
|
163
|
-
consola.debug(`Network slot allocated: ${slot}`);
|
|
164
|
-
const netnsName = commandArgs["no-netns"] ? void 0 : `vmsan-${lifecycle.vmId}`;
|
|
165
|
-
const net = new NetworkManager(slot, parsedInput.networkPolicy, parsedInput.domains, parsedInput.allowedCidrs, parsedInput.deniedCidrs, parsedInput.ports, bandwidthMbit, netnsName);
|
|
166
|
-
lifecycle.networkConfig = net.config;
|
|
167
|
-
log.start(`Creating VM ${lifecycle.vmId}...`);
|
|
168
|
-
const agentToken = existsSync(paths.agentBin) ? randomBytes(32).toString("hex") : null;
|
|
169
|
-
const state = buildInitialVmState({
|
|
170
|
-
vmId: lifecycle.vmId,
|
|
171
|
-
project: commandArgs.project || "",
|
|
172
|
-
runtime: parsedInput.runtime,
|
|
173
|
-
diskSizeGb: parsedInput.diskSizeGb,
|
|
174
|
-
kernelPath,
|
|
175
|
-
rootfsPath,
|
|
139
|
+
const fromImage = commandArgs["from-image"] ? parseImageReference(commandArgs["from-image"]) : void 0;
|
|
140
|
+
const result = await vmsan.create({
|
|
176
141
|
vcpus: parsedInput.vcpus,
|
|
177
142
|
memMib: parsedInput.memMib,
|
|
143
|
+
diskSizeGb: parsedInput.diskSizeGb,
|
|
144
|
+
kernelPath: typeof commandArgs.kernel === "string" ? commandArgs.kernel : void 0,
|
|
145
|
+
rootfsPath: typeof commandArgs.rootfs === "string" ? commandArgs.rootfs : void 0,
|
|
146
|
+
fromImage,
|
|
147
|
+
project: commandArgs.project || "",
|
|
148
|
+
runtime: parsedInput.runtime,
|
|
178
149
|
networkPolicy: parsedInput.networkPolicy,
|
|
179
150
|
domains: parsedInput.domains,
|
|
180
151
|
allowedCidrs: parsedInput.allowedCidrs,
|
|
181
152
|
deniedCidrs: parsedInput.deniedCidrs,
|
|
182
153
|
ports: parsedInput.ports,
|
|
183
|
-
tapDevice: lifecycle.networkConfig.tapDevice,
|
|
184
|
-
hostIp: lifecycle.networkConfig.hostIp,
|
|
185
|
-
guestIp: lifecycle.networkConfig.guestIp,
|
|
186
|
-
subnetMask: lifecycle.networkConfig.subnetMask,
|
|
187
|
-
macAddress: lifecycle.networkConfig.macAddress,
|
|
188
|
-
snapshotId: parsedInput.snapshotId,
|
|
189
|
-
timeoutMs: parsedInput.timeoutMs,
|
|
190
|
-
agentToken,
|
|
191
|
-
agentPort: paths.agentPort,
|
|
192
154
|
bandwidthMbit,
|
|
193
|
-
|
|
155
|
+
disableNetns: commandArgs["no-netns"],
|
|
156
|
+
disableSeccomp: commandArgs["no-seccomp"],
|
|
157
|
+
disablePidNs: commandArgs["no-pid-ns"],
|
|
158
|
+
disableCgroup: commandArgs["no-cgroup"],
|
|
159
|
+
timeoutMs: parsedInput.timeoutMs ?? void 0,
|
|
160
|
+
snapshotId: parsedInput.snapshotId ?? void 0
|
|
194
161
|
});
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
await net.setup();
|
|
198
|
-
log.success(`Network: TAP ${lifecycle.networkConfig.tapDevice}, Host ${lifecycle.networkConfig.hostIp}, Guest ${lifecycle.networkConfig.guestIp}`);
|
|
199
|
-
log.start("Preparing chroot...");
|
|
200
|
-
const snapshotConfig = parsedInput.snapshotId ? {
|
|
201
|
-
snapshotFile: join(paths.snapshotsDir, parsedInput.snapshotId, "snapshot_file"),
|
|
202
|
-
memFile: join(paths.snapshotsDir, parsedInput.snapshotId, "mem_file")
|
|
203
|
-
} : void 0;
|
|
204
|
-
const jailer = new Jailer(lifecycle.vmId, paths.jailerBaseDir);
|
|
205
|
-
const welcomePage = parsedInput.runtime === "node22-demo" && parsedInput.ports.length > 0 ? {
|
|
206
|
-
vmId: lifecycle.vmId,
|
|
207
|
-
ports: parsedInput.ports
|
|
208
|
-
} : void 0;
|
|
209
|
-
const agentConfig = agentToken ? {
|
|
210
|
-
binaryPath: paths.agentBin,
|
|
211
|
-
token: agentToken,
|
|
212
|
-
port: paths.agentPort,
|
|
213
|
-
vmId: lifecycle.vmId
|
|
214
|
-
} : void 0;
|
|
215
|
-
const jailerPaths = jailer.prepare({
|
|
216
|
-
kernelSrc: kernelPath,
|
|
217
|
-
rootfsSrc: rootfsPath,
|
|
218
|
-
diskSizeGb: parsedInput.diskSizeGb,
|
|
219
|
-
snapshot: snapshotConfig,
|
|
220
|
-
welcomePage,
|
|
221
|
-
agent: agentConfig
|
|
222
|
-
});
|
|
223
|
-
lifecycle.chrootDir = jailerPaths.chrootDir;
|
|
224
|
-
store.update(lifecycle.vmId, {
|
|
225
|
-
chrootDir: jailerPaths.chrootDir,
|
|
226
|
-
apiSocket: jailerPaths.socketPath
|
|
227
|
-
});
|
|
228
|
-
consola.debug(`Jailer chroot: ${jailerPaths.chrootDir}`);
|
|
229
|
-
consola.debug(`API socket path: ${jailerPaths.socketPath}`);
|
|
230
|
-
log.start("Spawning Firecracker via jailer...");
|
|
231
|
-
const firecrackerBin = join(baseDir, "bin", "firecracker");
|
|
232
|
-
const jailerBin = join(baseDir, "bin", "jailer");
|
|
233
|
-
const seccompFilter = commandArgs["no-seccomp"] ? void 0 : ensureSeccompFilter(paths);
|
|
234
|
-
if (seccompFilter) consola.debug(`Seccomp filter: ${seccompFilter}`);
|
|
235
|
-
const cgroup = commandArgs["no-cgroup"] ? void 0 : {
|
|
236
|
-
cpuQuotaUs: parsedInput.vcpus * 1e5,
|
|
237
|
-
cpuPeriodUs: 1e5,
|
|
238
|
-
memoryBytes: parsedInput.memMib * 1024 * 1024
|
|
239
|
-
};
|
|
240
|
-
jailer.spawn({
|
|
241
|
-
firecrackerBin,
|
|
242
|
-
jailerBin,
|
|
243
|
-
chrootBase: jailerPaths.chrootBase,
|
|
244
|
-
seccompFilter: seccompFilter ?? void 0,
|
|
245
|
-
newPidNs: !commandArgs["no-pid-ns"],
|
|
246
|
-
cgroup,
|
|
247
|
-
netns: netnsName
|
|
248
|
-
});
|
|
249
|
-
log.start("Waiting for API socket...");
|
|
250
|
-
await waitForSocket(jailerPaths.socketPath, 5e3);
|
|
251
|
-
log.success("API socket ready");
|
|
252
|
-
const vm = new FirecrackerClient(jailerPaths.socketPath);
|
|
253
|
-
if (parsedInput.snapshotId) {
|
|
254
|
-
log.start("Restoring from snapshot...");
|
|
255
|
-
await vm.loadSnapshot("snapshot/snapshot_file", "snapshot/mem_file");
|
|
256
|
-
await vm.resume();
|
|
257
|
-
log.success("Snapshot restored and VM resumed");
|
|
258
|
-
} else {
|
|
259
|
-
const bootArgs = NetworkManager.bootArgs(slot);
|
|
260
|
-
consola.debug(`Boot args: ${bootArgs}`);
|
|
261
|
-
await vm.boot("kernel/vmlinux", bootArgs);
|
|
262
|
-
await vm.addDrive("rootfs", "rootfs/rootfs.ext4", true, false);
|
|
263
|
-
await vm.configure(parsedInput.vcpus, parsedInput.memMib);
|
|
264
|
-
await vm.addNetwork("eth0", lifecycle.networkConfig.tapDevice, lifecycle.networkConfig.macAddress);
|
|
265
|
-
log.start("Starting VM...");
|
|
266
|
-
await vm.start();
|
|
267
|
-
}
|
|
268
|
-
const pid = getVmPid(lifecycle.vmId);
|
|
269
|
-
consola.debug(`Firecracker PID: ${pid ?? "unknown"}`);
|
|
270
|
-
store.update(lifecycle.vmId, {
|
|
271
|
-
status: "running",
|
|
272
|
-
pid
|
|
273
|
-
});
|
|
274
|
-
log.success(`VM ${lifecycle.vmId} is running (PID: ${pid || "unknown"})`);
|
|
275
|
-
if (parsedInput.timeoutMs && pid) {
|
|
276
|
-
const stateFile = join(paths.vmsDir, `${lifecycle.vmId}.json`);
|
|
277
|
-
spawn("bash", ["-c", [
|
|
278
|
-
`sleep ${Math.ceil(parsedInput.timeoutMs / 1e3)}`,
|
|
279
|
-
`STATE=$(cat "${stateFile}" 2>/dev/null) || exit 0`,
|
|
280
|
-
`echo "$STATE" | grep -q '"status":"running"' || exit 0`,
|
|
281
|
-
`echo "$STATE" | grep -q '"pid":${pid}' || exit 0`,
|
|
282
|
-
`[ -d /proc/${pid} ] || exit 0`,
|
|
283
|
-
`grep -q "${lifecycle.vmId}" /proc/${pid}/cmdline 2>/dev/null || exit 0`,
|
|
284
|
-
`kill ${pid} 2>/dev/null`
|
|
285
|
-
].join(" && ")], {
|
|
286
|
-
detached: true,
|
|
287
|
-
stdio: "ignore"
|
|
288
|
-
}).unref();
|
|
289
|
-
}
|
|
162
|
+
const log = createScopedLogger(result.vmId);
|
|
163
|
+
const state = result.state;
|
|
290
164
|
const summaryLines = buildCreateSummaryLines({
|
|
291
|
-
vmId:
|
|
292
|
-
pid,
|
|
165
|
+
vmId: result.vmId,
|
|
166
|
+
pid: result.pid,
|
|
293
167
|
vcpus: parsedInput.vcpus,
|
|
294
168
|
memMib: parsedInput.memMib,
|
|
295
169
|
runtime: parsedInput.runtime,
|
|
@@ -300,50 +174,44 @@ const createCommand = defineCommand({
|
|
|
300
174
|
allowedCidrs: parsedInput.allowedCidrs,
|
|
301
175
|
deniedCidrs: parsedInput.deniedCidrs,
|
|
302
176
|
ports: parsedInput.ports,
|
|
303
|
-
kernelPath,
|
|
304
|
-
rootfsPath,
|
|
177
|
+
kernelPath: state.kernel,
|
|
178
|
+
rootfsPath: state.rootfs,
|
|
305
179
|
snapshotId: parsedInput.snapshotId,
|
|
306
180
|
timeout: commandArgs.timeout,
|
|
307
|
-
socketPath:
|
|
308
|
-
chrootDir:
|
|
309
|
-
tapDevice:
|
|
310
|
-
hostIp:
|
|
311
|
-
guestIp:
|
|
312
|
-
macAddress:
|
|
313
|
-
stateFilePath: join(paths.vmsDir, `${
|
|
181
|
+
socketPath: state.apiSocket,
|
|
182
|
+
chrootDir: state.chrootDir,
|
|
183
|
+
tapDevice: state.network.tapDevice,
|
|
184
|
+
hostIp: state.network.hostIp,
|
|
185
|
+
guestIp: state.network.guestIp,
|
|
186
|
+
macAddress: state.network.macAddress,
|
|
187
|
+
stateFilePath: join(vmsan.paths.vmsDir, `${result.vmId}.json`)
|
|
314
188
|
});
|
|
315
189
|
log.box(summaryLines.join("\n"));
|
|
316
190
|
cmdLog.set({
|
|
317
|
-
vmId:
|
|
191
|
+
vmId: result.vmId,
|
|
318
192
|
vcpus: parsedInput.vcpus,
|
|
319
193
|
memMib: parsedInput.memMib,
|
|
320
194
|
runtime: parsedInput.runtime,
|
|
321
195
|
networkPolicy: parsedInput.networkPolicy,
|
|
322
|
-
guestIp:
|
|
323
|
-
pid,
|
|
324
|
-
kernel:
|
|
325
|
-
rootfs:
|
|
196
|
+
guestIp: state.network.guestIp,
|
|
197
|
+
pid: result.pid,
|
|
198
|
+
kernel: state.kernel,
|
|
199
|
+
rootfs: state.rootfs,
|
|
326
200
|
diskSizeGb: parsedInput.diskSizeGb
|
|
327
201
|
});
|
|
328
202
|
cmdLog.emit();
|
|
329
|
-
if (commandArgs.connect &&
|
|
203
|
+
if (commandArgs.connect && state.agentToken) {
|
|
330
204
|
log.start("Waiting for agent to become ready...");
|
|
331
|
-
await waitForAgent(
|
|
205
|
+
await waitForAgent(state.network.guestIp, vmsan.paths.agentPort);
|
|
332
206
|
log.success("Agent is ready. Connecting via PTY shell...");
|
|
333
207
|
const shell = new ShellSession({
|
|
334
|
-
host:
|
|
335
|
-
port: paths.agentPort,
|
|
336
|
-
token: agentToken
|
|
208
|
+
host: state.network.guestIp,
|
|
209
|
+
port: vmsan.paths.agentPort,
|
|
210
|
+
token: state.agentToken
|
|
337
211
|
});
|
|
338
|
-
if (!(await shell.connect()).sessionDestroyed && shell.sessionId) process.stderr.write(`\n[2mResume this session with:\n vmsan connect ${
|
|
212
|
+
if (!(await shell.connect()).sessionDestroyed && shell.sessionId) process.stderr.write(`\n[2mResume this session with:\n vmsan connect ${result.vmId} --session ${shell.sessionId}[0m\n`);
|
|
339
213
|
}
|
|
340
214
|
} catch (error) {
|
|
341
|
-
if (lifecycle.vmId) {
|
|
342
|
-
killOrphanVmProcess(lifecycle.vmId);
|
|
343
|
-
markVmAsError(lifecycle.vmId, error, paths);
|
|
344
|
-
}
|
|
345
|
-
cleanupNetwork(lifecycle.networkConfig);
|
|
346
|
-
cleanupChroot(lifecycle.chrootDir);
|
|
347
215
|
handleCommandError(error, cmdLog);
|
|
348
216
|
process.exitCode = 1;
|
|
349
217
|
}
|
|
@@ -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 {
|
|
7
|
-
import {
|
|
6
|
+
import { n as waitForAgent, t as resolveVmState } from "./vm-context.mjs";
|
|
7
|
+
import { basename, join, resolve } from "node:path";
|
|
8
|
+
import { existsSync, mkdirSync, statSync, writeFileSync } from "node:fs";
|
|
8
9
|
import { consola } from "consola";
|
|
9
|
-
import { writeFileSync } from "node:fs";
|
|
10
10
|
import { defineCommand } from "citty";
|
|
11
11
|
const downloadCommand = defineCommand({
|
|
12
12
|
meta: {
|
|
@@ -29,23 +29,8 @@ const downloadCommand = defineCommand({
|
|
|
29
29
|
const cmdLog = createCommandLogger("download");
|
|
30
30
|
const paths = vmsanPaths();
|
|
31
31
|
try {
|
|
32
|
-
const state =
|
|
33
|
-
if (!state) throw vmNotFoundError(args.vmId);
|
|
34
|
-
if (state.status !== "running") {
|
|
35
|
-
consola.error(`VM ${args.vmId} is not running (status: ${state.status})`);
|
|
36
|
-
cmdLog.emit();
|
|
37
|
-
process.exitCode = 1;
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
if (!state.agentToken) {
|
|
41
|
-
consola.error("VM has no agent token. Cannot download files without the agent.");
|
|
42
|
-
cmdLog.emit();
|
|
43
|
-
process.exitCode = 1;
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
32
|
+
const { state, guestIp, port } = resolveVmState(args.vmId, paths);
|
|
46
33
|
const log = consola.withTag(args.vmId);
|
|
47
|
-
const guestIp = state.network.guestIp;
|
|
48
|
-
const port = state.agentPort || paths.agentPort;
|
|
49
34
|
consola.debug(`Agent endpoint: ${guestIp}:${port}`);
|
|
50
35
|
log.start("Waiting for agent...");
|
|
51
36
|
await waitForAgent(guestIp, port);
|
|
@@ -66,7 +51,14 @@ const downloadCommand = defineCommand({
|
|
|
66
51
|
process.exitCode = 1;
|
|
67
52
|
return;
|
|
68
53
|
}
|
|
69
|
-
|
|
54
|
+
let localPath;
|
|
55
|
+
if (args.dest) {
|
|
56
|
+
const resolved = resolve(args.dest);
|
|
57
|
+
if (args.dest.endsWith("/") || existsSync(resolved) && statSync(resolved).isDirectory()) {
|
|
58
|
+
mkdirSync(resolved, { recursive: true });
|
|
59
|
+
localPath = join(resolved, basename(remotePath));
|
|
60
|
+
} else localPath = resolved;
|
|
61
|
+
} else localPath = resolve(basename(remotePath));
|
|
70
62
|
writeFileSync(localPath, data);
|
|
71
63
|
log.success(`Downloaded to ${localPath} (${data.length} bytes)`);
|
|
72
64
|
cmdLog.set({
|
package/dist/_chunks/errors.mjs
CHANGED
|
@@ -122,10 +122,16 @@ const chrootNotFoundError = (vmId) => new VmError("ERR_VM_CHROOT_NOT_FOUND", {
|
|
|
122
122
|
fix: "The VM must be recreated with 'vmsan create'."
|
|
123
123
|
});
|
|
124
124
|
const networkSlotsExhaustedError = () => new VmError("ERR_VM_NETWORK_SLOTS_EXHAUSTED", { message: "No available network slots (max 255 VMs)" });
|
|
125
|
-
const vmNotRunningError = (vmId) => new VmError("ERR_VM_NOT_RUNNING", {
|
|
125
|
+
const vmNotRunningError = (vmId, currentStatus) => new VmError("ERR_VM_NOT_RUNNING", {
|
|
126
126
|
vmId,
|
|
127
|
-
message: `VM ${vmId} is not running`,
|
|
128
|
-
fix: "The VM must be running
|
|
127
|
+
message: currentStatus ? `VM ${vmId} is not running (current status: ${currentStatus})` : `VM ${vmId} is not running`,
|
|
128
|
+
fix: "The VM must be running. Start it with 'vmsan start <vm-id>'."
|
|
129
|
+
});
|
|
130
|
+
const vmNoAgentTokenError = (vmId) => new VmError("ERR_VM_NO_AGENT_TOKEN", {
|
|
131
|
+
vmId,
|
|
132
|
+
message: `VM ${vmId} has no agent token`,
|
|
133
|
+
why: "The vmsan-agent binary was not found at ~/.vmsan/bin/vmsan-agent when this VM was created.",
|
|
134
|
+
fix: "Install the agent binary into ~/.vmsan/bin/vmsan-agent and recreate the VM with 'vmsan create'."
|
|
129
135
|
});
|
|
130
136
|
const snapshotNotFoundError = (snapshotId) => new VmError("ERR_VM_SNAPSHOT_NOT_FOUND", { message: `Snapshot not found: ${snapshotId}` });
|
|
131
137
|
var FirecrackerApiError = class extends VmsanError {
|
|
@@ -197,7 +203,7 @@ var SetupError = class extends VmsanError {
|
|
|
197
203
|
this.name = "SetupError";
|
|
198
204
|
}
|
|
199
205
|
};
|
|
200
|
-
const INSTALL_FIX = `Run the install script to set up all dependencies:\n\ncurl -fsSL https://
|
|
206
|
+
const INSTALL_FIX = `Run the install script to set up all dependencies:\n\ncurl -fsSL https://vmsan.dev/install | bash`;
|
|
201
207
|
const missingBinaryError = (binary, path) => new SetupError("ERR_SETUP_MISSING_BINARY", {
|
|
202
208
|
message: `${binary} not found at ${path}`,
|
|
203
209
|
fix: INSTALL_FIX
|
|
@@ -229,4 +235,4 @@ function handleCommandError(error, cmdLog) {
|
|
|
229
235
|
if (error.link) consola.log(` More: ${error.link}`);
|
|
230
236
|
} else consola.error(error instanceof Error ? error.message : String(error));
|
|
231
237
|
}
|
|
232
|
-
export {
|
|
238
|
+
export { invalidDiskSizeRangeError as A, mutuallyExclusiveFlagsError as B, vmNotStoppedError as C, invalidCidrOctetError as D, invalidCidrFormatError as E, invalidImageRefTagError as F, portConflictError as H, invalidIntegerFlagError as I, invalidNetworkPolicyError as L, invalidDomainPatternError as M, invalidDurationError as N, invalidCidrPrefixError as O, invalidImageRefEmptyError as P, invalidPortError as R, vmNotRunningError as S, ValidationError as T, VmsanError as U, policyConflictError as V, chrootNotFoundError as _, noKernelDirError as a, vmNoAgentTokenError as b, TimeoutError as c, socketTimeoutError as d, NetworkError as f, VmError as g, firecrackerApiError as h, noExt4RootfsError as i, invalidDomainError as j, invalidDiskSizeFormatError as k, agentTimeoutError as l, FirecrackerApiError as m, SetupError as n, noKernelError as o, defaultInterfaceNotFoundError as p, missingBinaryError as r, noRootfsDirError as s, handleCommandError as t, lockTimeoutError as u, networkSlotsExhaustedError as v, vmStateNotFoundError as w, vmNotFoundError as x, snapshotNotFoundError as y, invalidRuntimeError as z };
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { n as vmsanPaths } from "./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 "./timeout-killer.mjs";
|
|
6
|
+
import { t as AgentClient } from "./agent.mjs";
|
|
7
|
+
import { t as TimeoutExtender } from "./timeout-extender.mjs";
|
|
8
|
+
import { n as waitForAgent, t as resolveVmState } from "./vm-context.mjs";
|
|
9
|
+
import { t as ShellSession } from "./shell.mjs";
|
|
10
|
+
import { consola } from "consola";
|
|
11
|
+
import { defineCommand } from "citty";
|
|
12
|
+
import { isatty } from "node:tty";
|
|
13
|
+
function shellEscape(s) {
|
|
14
|
+
if (/^[a-zA-Z0-9._\-/=:@]+$/.test(s)) return s;
|
|
15
|
+
return "'" + s.replace(/'/g, "'\\''") + "'";
|
|
16
|
+
}
|
|
17
|
+
function parseEnvFlags(targetCommand) {
|
|
18
|
+
const env = {};
|
|
19
|
+
const argv = process.argv;
|
|
20
|
+
let positionalCount = 0;
|
|
21
|
+
for (let i = 2; i < argv.length; i++) {
|
|
22
|
+
const arg = argv[i];
|
|
23
|
+
if (arg === "--") break;
|
|
24
|
+
if (!arg.startsWith("-")) {
|
|
25
|
+
positionalCount++;
|
|
26
|
+
if (positionalCount >= 3) break;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
let value;
|
|
30
|
+
if (arg === "--env" || arg === "-e") {
|
|
31
|
+
value = argv[i + 1];
|
|
32
|
+
if (value !== void 0) i++;
|
|
33
|
+
} else if (arg.startsWith("--env=")) value = arg.slice(6);
|
|
34
|
+
else if (arg.startsWith("-e=")) value = arg.slice(3);
|
|
35
|
+
if (value !== void 0) {
|
|
36
|
+
const eqIdx = value.indexOf("=");
|
|
37
|
+
if (eqIdx > 0) env[value.slice(0, eqIdx)] = value.slice(eqIdx + 1);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return env;
|
|
41
|
+
}
|
|
42
|
+
function buildVmsanPrompt(vmId) {
|
|
43
|
+
return `\\[\\033[1;32m\\]vmsan:${vmId.slice(0, 8)}\\[\\033[0m\\]:\\[\\033[1;34m\\]\\w\\[\\033[0m\\]\\$ `;
|
|
44
|
+
}
|
|
45
|
+
const execCommand = defineCommand({
|
|
46
|
+
meta: {
|
|
47
|
+
name: "exec",
|
|
48
|
+
description: "Execute a command inside a running VM"
|
|
49
|
+
},
|
|
50
|
+
args: {
|
|
51
|
+
vmId: {
|
|
52
|
+
type: "positional",
|
|
53
|
+
description: "VM ID, followed by command and arguments",
|
|
54
|
+
required: true
|
|
55
|
+
},
|
|
56
|
+
sudo: {
|
|
57
|
+
type: "boolean",
|
|
58
|
+
default: false,
|
|
59
|
+
description: "Run with extended privileges (sudo)"
|
|
60
|
+
},
|
|
61
|
+
interactive: {
|
|
62
|
+
type: "boolean",
|
|
63
|
+
alias: "i",
|
|
64
|
+
default: false,
|
|
65
|
+
description: "Interactive shell mode (PTY)"
|
|
66
|
+
},
|
|
67
|
+
"no-extend-timeout": {
|
|
68
|
+
type: "boolean",
|
|
69
|
+
default: false,
|
|
70
|
+
description: "Skip timeout extension (interactive only)"
|
|
71
|
+
},
|
|
72
|
+
tty: {
|
|
73
|
+
type: "boolean",
|
|
74
|
+
alias: "t",
|
|
75
|
+
default: false,
|
|
76
|
+
description: "Allocate a pseudo-TTY (accepted for compatibility)"
|
|
77
|
+
},
|
|
78
|
+
workdir: {
|
|
79
|
+
type: "string",
|
|
80
|
+
alias: "w",
|
|
81
|
+
description: "Working directory inside the VM"
|
|
82
|
+
},
|
|
83
|
+
env: {
|
|
84
|
+
type: "string",
|
|
85
|
+
alias: "e",
|
|
86
|
+
description: "Environment variable (KEY=VAL), repeatable"
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
async run({ args }) {
|
|
90
|
+
const cmdLog = createCommandLogger("exec");
|
|
91
|
+
const paths = vmsanPaths();
|
|
92
|
+
try {
|
|
93
|
+
const command = args._[1];
|
|
94
|
+
const commandArgs = args._.slice(2);
|
|
95
|
+
if (!command) {
|
|
96
|
+
consola.error("No command provided. Usage: vmsan exec <vm_id> <command> [...args]");
|
|
97
|
+
cmdLog.emit();
|
|
98
|
+
process.exitCode = 1;
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (args.interactive && !isatty(1)) {
|
|
102
|
+
consola.error("--interactive requires a terminal (TTY).");
|
|
103
|
+
process.exitCode = 1;
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const envVars = parseEnvFlags(command);
|
|
107
|
+
const { state, guestIp, port, store } = resolveVmState(args.vmId, paths);
|
|
108
|
+
consola.debug(`Agent endpoint: ${guestIp}:${port}`);
|
|
109
|
+
consola.debug(`Waiting for agent on ${guestIp}:${port}...`);
|
|
110
|
+
await waitForAgent(guestIp, port);
|
|
111
|
+
if (args.interactive) {
|
|
112
|
+
const parts = [];
|
|
113
|
+
parts.push(`export PS1=${shellEscape(buildVmsanPrompt(args.vmId))} TERM=xterm-256color &&`);
|
|
114
|
+
if (args.workdir) parts.push(`cd ${shellEscape(args.workdir)} &&`);
|
|
115
|
+
for (const [key, val] of Object.entries(envVars)) parts.push(`${key}=${shellEscape(val)}`);
|
|
116
|
+
parts.push(shellEscape(command));
|
|
117
|
+
for (const a of commandArgs) parts.push(shellEscape(a));
|
|
118
|
+
const injectedCmd = "clear; " + parts.join(" ") + "; exit $?\n";
|
|
119
|
+
let extender = null;
|
|
120
|
+
if (!args["no-extend-timeout"] && state.timeoutMs) {
|
|
121
|
+
extender = new TimeoutExtender({
|
|
122
|
+
vmId: args.vmId,
|
|
123
|
+
store,
|
|
124
|
+
paths
|
|
125
|
+
});
|
|
126
|
+
extender.start();
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
await new ShellSession({
|
|
130
|
+
host: guestIp,
|
|
131
|
+
port,
|
|
132
|
+
token: state.agentToken,
|
|
133
|
+
initialCommand: injectedCmd,
|
|
134
|
+
user: args.sudo ? "root" : void 0
|
|
135
|
+
}).connect();
|
|
136
|
+
} finally {
|
|
137
|
+
extender?.stop();
|
|
138
|
+
}
|
|
139
|
+
cmdLog.set({
|
|
140
|
+
vmId: args.vmId,
|
|
141
|
+
mode: "interactive",
|
|
142
|
+
command,
|
|
143
|
+
args: commandArgs,
|
|
144
|
+
...args["no-extend-timeout"] && { noExtendTimeout: true }
|
|
145
|
+
});
|
|
146
|
+
cmdLog.emit();
|
|
147
|
+
} else {
|
|
148
|
+
consola.debug(`exec: ${command} ${commandArgs.join(" ")}`);
|
|
149
|
+
const agent = new AgentClient(`http://${guestIp}:${port}`, state.agentToken);
|
|
150
|
+
const params = {
|
|
151
|
+
cmd: command,
|
|
152
|
+
args: commandArgs.length > 0 ? commandArgs : void 0,
|
|
153
|
+
cwd: args.workdir || void 0,
|
|
154
|
+
env: Object.keys(envVars).length > 0 ? envVars : void 0,
|
|
155
|
+
user: args.sudo ? "root" : void 0
|
|
156
|
+
};
|
|
157
|
+
const ac = new AbortController();
|
|
158
|
+
const onSignal = () => ac.abort();
|
|
159
|
+
process.on("SIGINT", onSignal);
|
|
160
|
+
process.on("SIGTERM", onSignal);
|
|
161
|
+
try {
|
|
162
|
+
const result = await (await agent.exec(params, {
|
|
163
|
+
signal: ac.signal,
|
|
164
|
+
onStdout: (line) => process.stdout.write(line + "\n"),
|
|
165
|
+
onStderr: (line) => process.stderr.write(line + "\n")
|
|
166
|
+
})).wait();
|
|
167
|
+
process.exitCode = result.exitCode;
|
|
168
|
+
if (result.timedOut) consola.error("Command timed out.");
|
|
169
|
+
} catch (err) {
|
|
170
|
+
if (!ac.signal.aborted) throw err;
|
|
171
|
+
process.exitCode = 130;
|
|
172
|
+
} finally {
|
|
173
|
+
process.removeListener("SIGINT", onSignal);
|
|
174
|
+
process.removeListener("SIGTERM", onSignal);
|
|
175
|
+
}
|
|
176
|
+
cmdLog.set({
|
|
177
|
+
vmId: args.vmId,
|
|
178
|
+
mode: "non-interactive",
|
|
179
|
+
command,
|
|
180
|
+
args: commandArgs
|
|
181
|
+
});
|
|
182
|
+
cmdLog.emit();
|
|
183
|
+
}
|
|
184
|
+
} catch (error) {
|
|
185
|
+
handleCommandError(error, cmdLog);
|
|
186
|
+
process.exitCode = 1;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
export { execCommand as default };
|