vmsan 0.1.0-alpha.24 → 0.1.0-alpha.25
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/README.md +24 -46
- package/dist/_chunks/connect.mjs +0 -1
- package/dist/_chunks/context.mjs +105 -174
- package/dist/_chunks/create.mjs +107 -113
- package/dist/_chunks/download.mjs +1 -2
- package/dist/_chunks/errors.mjs +2 -2
- package/dist/_chunks/exec.mjs +1 -2
- package/dist/_chunks/list.mjs +2 -1
- package/dist/_chunks/network.mjs +2 -1
- package/dist/_chunks/remove.mjs +2 -1
- package/dist/_chunks/start.mjs +2 -1
- package/dist/_chunks/stop.mjs +2 -1
- package/dist/_chunks/summary.mjs +1 -1
- package/dist/_chunks/timeout-extender.mjs +1 -1
- package/dist/_chunks/upload.mjs +1 -2
- package/dist/_chunks/vm-context.mjs +242 -3
- package/dist/index.d.mts +11 -7
- package/dist/index.mjs +3 -4
- package/package.json +4 -4
- package/dist/_chunks/vm-state.mjs +0 -242
package/README.md
CHANGED
|
@@ -29,26 +29,30 @@ Create, manage, and connect to isolated [Firecracker](https://github.com/firecra
|
|
|
29
29
|
- 📂 **File transfer** — upload and download without SSH
|
|
30
30
|
- 🐳 **Docker images** — build rootfs from any OCI image with `--from-image`
|
|
31
31
|
- 🏃 **Command execution** — run commands with streaming output, env injection, and sudo
|
|
32
|
-
- 🧩 **Multiple runtimes** — `base`, `node22`, `python3.13`
|
|
32
|
+
- 🧩 **Multiple runtimes** — `base`, `node22`, `node24`, `python3.13`
|
|
33
33
|
- 📸 **VM snapshots** — save and restore VM state
|
|
34
34
|
- 📊 **JSON output** — `--json` flag for scripting and automation
|
|
35
35
|
|
|
36
36
|
## 📋 Prerequisites
|
|
37
37
|
|
|
38
38
|
- Linux (x86_64 or aarch64) with KVM support
|
|
39
|
-
- [Bun](https://bun.sh) >= 1.2
|
|
40
|
-
- [Go](https://go.dev) >= 1.22 (to build the in-VM agent)
|
|
41
39
|
- Root/sudo access (required for TAP device networking and jailer)
|
|
42
|
-
-
|
|
40
|
+
- Docker (for building runtime images and `--from-image`)
|
|
43
41
|
|
|
44
42
|
## 🚀 Install
|
|
45
43
|
|
|
46
|
-
### 1. Install Firecracker, kernel, and rootfs
|
|
47
|
-
|
|
48
44
|
```bash
|
|
49
45
|
curl -fsSL https://vmsan.dev/install | bash
|
|
50
46
|
```
|
|
51
47
|
|
|
48
|
+
This downloads and installs everything into `~/.vmsan/`:
|
|
49
|
+
|
|
50
|
+
- Firecracker + Jailer (latest release)
|
|
51
|
+
- Linux kernel (vmlinux 6.1)
|
|
52
|
+
- Ubuntu 24.04 rootfs (converted from squashfs to ext4)
|
|
53
|
+
- vmsan CLI + in-VM agent
|
|
54
|
+
- Runtime images (node22, node24, python3.13)
|
|
55
|
+
|
|
52
56
|
<details>
|
|
53
57
|
<summary>Uninstall</summary>
|
|
54
58
|
|
|
@@ -58,53 +62,27 @@ curl -fsSL https://vmsan.dev/install | bash -s -- --uninstall
|
|
|
58
62
|
|
|
59
63
|
</details>
|
|
60
64
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
- Firecracker + Jailer (latest release)
|
|
64
|
-
- Linux kernel (vmlinux 6.1)
|
|
65
|
-
- Ubuntu 24.04 rootfs (converted from squashfs to ext4)
|
|
66
|
-
|
|
67
|
-
### 2. Install vmsan CLI
|
|
68
|
-
|
|
69
|
-
<!-- automd:pm-install -->
|
|
70
|
-
|
|
71
|
-
```sh
|
|
72
|
-
# ✨ Auto-detect
|
|
73
|
-
npx nypm install vmsan
|
|
74
|
-
|
|
75
|
-
# npm
|
|
76
|
-
npm install vmsan
|
|
77
|
-
|
|
78
|
-
# yarn
|
|
79
|
-
yarn add vmsan
|
|
80
|
-
|
|
81
|
-
# pnpm
|
|
82
|
-
pnpm add vmsan
|
|
83
|
-
|
|
84
|
-
# bun
|
|
85
|
-
bun install vmsan
|
|
86
|
-
|
|
87
|
-
# deno
|
|
88
|
-
deno install npm:vmsan
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
<!-- /automd -->
|
|
65
|
+
<details>
|
|
66
|
+
<summary>Development setup</summary>
|
|
92
67
|
|
|
93
|
-
|
|
68
|
+
If you want to build from source:
|
|
94
69
|
|
|
95
70
|
```bash
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
cd ..
|
|
99
|
-
```
|
|
71
|
+
# Install dependencies
|
|
72
|
+
bun install
|
|
100
73
|
|
|
101
|
-
|
|
74
|
+
# Build the in-VM agent
|
|
75
|
+
cd agent && make install && cd ..
|
|
102
76
|
|
|
103
|
-
|
|
104
|
-
bun
|
|
77
|
+
# Build the CLI
|
|
78
|
+
bun run build
|
|
79
|
+
|
|
80
|
+
# Link local build
|
|
81
|
+
mkdir -p ~/.vmsan/bin
|
|
82
|
+
ln -sf "$(pwd)/dist/bin/cli.mjs" ~/.vmsan/bin/vmsan
|
|
105
83
|
```
|
|
106
84
|
|
|
107
|
-
|
|
85
|
+
</details>
|
|
108
86
|
|
|
109
87
|
## 📖 Usage
|
|
110
88
|
|
package/dist/_chunks/connect.mjs
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { n as vmsanPaths } from "./paths.mjs";
|
|
2
2
|
import { l as handleCommandError } from "./errors.mjs";
|
|
3
3
|
import { t as createCommandLogger } from "./logger.mjs";
|
|
4
|
-
import "./vm-state.mjs";
|
|
5
4
|
import { n as waitForAgent, t as resolveVmState } from "./vm-context.mjs";
|
|
6
5
|
import { t as ShellSession } from "./shell.mjs";
|
|
7
6
|
import { consola } from "consola";
|
package/dist/_chunks/context.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { n as vmsanPaths } from "./paths.mjs";
|
|
2
|
-
import { A as vmNotRunningError, B as invalidDomainPatternError, D as snapshotNotFoundError, F as invalidCidrOctetError, G as invalidNetworkPolicyError, H as invalidImageRefEmptyError, I as invalidCidrPrefixError, J as mutuallyExclusiveFlagsError, K as invalidPortError, L as invalidDiskSizeFormatError, P as invalidCidrFormatError, R as invalidDiskSizeRangeError, T as chrootNotFoundError, U as invalidImageRefTagError, W as invalidIntegerFlagError, X as portConflictError, Z as VmsanError, a as cloudflareNotConfiguredError, c as cloudflaredStartFailedError, d as missingBinaryError, f as noExt4RootfsError, h as noRootfsDirError, i as cloudflareNoZoneError, j as vmNotStoppedError, k as vmNotFoundError, m as noKernelError, n as cloudflareConfigNotFoundError, o as cloudflareTunnelNoIdError, p as noKernelDirError, q as invalidRuntimeError, r as cloudflareNoAccountsError, s as cloudflaredNotFoundError, v as lockTimeoutError, x as defaultInterfaceNotFoundError, y as socketTimeoutError, z as invalidDomainError } from "./errors.mjs";
|
|
3
|
-
import { c as
|
|
2
|
+
import { A as vmNotRunningError, B as invalidDomainPatternError, D as snapshotNotFoundError, F as invalidCidrOctetError, G as invalidNetworkPolicyError, H as invalidImageRefEmptyError, I as invalidCidrPrefixError, J as mutuallyExclusiveFlagsError, K as invalidPortError, L as invalidDiskSizeFormatError, P as invalidCidrFormatError, R as invalidDiskSizeRangeError, T as chrootNotFoundError, U as invalidImageRefTagError, W as invalidIntegerFlagError, X as portConflictError, Z as VmsanError, a as cloudflareNotConfiguredError, c as cloudflaredStartFailedError, d as missingBinaryError, f as noExt4RootfsError, h as noRootfsDirError, i as cloudflareNoZoneError, j as vmNotStoppedError, k as vmNotFoundError, m as noKernelError, n as cloudflareConfigNotFoundError, o as cloudflareTunnelNoIdError, p as noKernelDirError, q as invalidRuntimeError, r as cloudflareNoAccountsError, s as cloudflaredNotFoundError, u as SetupError, v as lockTimeoutError, x as defaultInterfaceNotFoundError, y as socketTimeoutError, z as invalidDomainError } from "./errors.mjs";
|
|
3
|
+
import { c as mkdirSecure, d as sleepSync, g as writeSecure, h as toError, n as waitForAgent, o as generateVmId, r as FileVmStateStore, u as safeKill } from "./vm-context.mjs";
|
|
4
4
|
import { t as FirecrackerClient } from "./firecracker.mjs";
|
|
5
5
|
import { t as spawnTimeoutKiller } from "./timeout-killer.mjs";
|
|
6
|
+
import { t as AgentClient } from "./agent.mjs";
|
|
6
7
|
import { createHooks } from "hookable";
|
|
7
8
|
import { dirname, join } from "node:path";
|
|
8
9
|
import { execFileSync, execSync, spawn } from "node:child_process";
|
|
@@ -1202,137 +1203,7 @@ var FileLock = class {
|
|
|
1202
1203
|
}
|
|
1203
1204
|
};
|
|
1204
1205
|
/**
|
|
1205
|
-
* Template generators for the node22-demo runtime welcome page.
|
|
1206
|
-
* All functions are pure and return string content ready to write to files.
|
|
1207
|
-
*/
|
|
1208
|
-
function generateWelcomeHtml(vmId, ports) {
|
|
1209
|
-
ports.map((p) => `<li>${p}</li>`).join("\n ");
|
|
1210
|
-
return `<!DOCTYPE html>
|
|
1211
|
-
<html lang="en">
|
|
1212
|
-
<head>
|
|
1213
|
-
<meta charset="utf-8">
|
|
1214
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1215
|
-
<title>vmsan VM ${vmId}</title>
|
|
1216
|
-
<style>
|
|
1217
|
-
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
1218
|
-
body {
|
|
1219
|
-
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
1220
|
-
background: #0f172a;
|
|
1221
|
-
color: #e2e8f0;
|
|
1222
|
-
min-height: 100vh;
|
|
1223
|
-
display: flex;
|
|
1224
|
-
align-items: center;
|
|
1225
|
-
justify-content: center;
|
|
1226
|
-
padding: 2rem;
|
|
1227
|
-
}
|
|
1228
|
-
.container { max-width: 640px; width: 100%; }
|
|
1229
|
-
.header { text-align: center; margin-bottom: 2rem; }
|
|
1230
|
-
.logo {
|
|
1231
|
-
font-size: 2.5rem;
|
|
1232
|
-
font-weight: 800;
|
|
1233
|
-
background: linear-gradient(135deg, #f97316, #ef4444);
|
|
1234
|
-
-webkit-background-clip: text;
|
|
1235
|
-
-webkit-text-fill-color: transparent;
|
|
1236
|
-
background-clip: text;
|
|
1237
|
-
}
|
|
1238
|
-
.subtitle { color: #94a3b8; margin-top: 0.5rem; font-size: 1.1rem; }
|
|
1239
|
-
.card {
|
|
1240
|
-
background: #1e293b;
|
|
1241
|
-
border: 1px solid #334155;
|
|
1242
|
-
border-radius: 12px;
|
|
1243
|
-
padding: 1.5rem;
|
|
1244
|
-
margin-bottom: 1.25rem;
|
|
1245
|
-
}
|
|
1246
|
-
.card h2 { font-size: 1rem; color: #f97316; margin-bottom: 0.75rem; }
|
|
1247
|
-
.info-row { display: flex; justify-content: space-between; padding: 0.35rem 0; }
|
|
1248
|
-
.info-label { color: #94a3b8; }
|
|
1249
|
-
.info-value { font-family: monospace; color: #e2e8f0; }
|
|
1250
|
-
ul { list-style: none; }
|
|
1251
|
-
ul li { padding: 0.25rem 0; }
|
|
1252
|
-
code {
|
|
1253
|
-
background: #0f172a;
|
|
1254
|
-
border: 1px solid #334155;
|
|
1255
|
-
border-radius: 6px;
|
|
1256
|
-
padding: 0.2rem 0.5rem;
|
|
1257
|
-
font-size: 0.875rem;
|
|
1258
|
-
color: #f97316;
|
|
1259
|
-
}
|
|
1260
|
-
.steps li { padding: 0.5rem 0; color: #cbd5e1; }
|
|
1261
|
-
.steps li strong { color: #e2e8f0; }
|
|
1262
|
-
.footer { text-align: center; color: #475569; font-size: 0.85rem; margin-top: 1.5rem; }
|
|
1263
|
-
</style>
|
|
1264
|
-
</head>
|
|
1265
|
-
<body>
|
|
1266
|
-
<div class="container">
|
|
1267
|
-
<div class="header">
|
|
1268
|
-
<div class="logo">vmsan</div>
|
|
1269
|
-
<div class="subtitle">Your microVM is running</div>
|
|
1270
|
-
</div>
|
|
1271
|
-
<div class="card">
|
|
1272
|
-
<h2>VM Info</h2>
|
|
1273
|
-
<div class="info-row">
|
|
1274
|
-
<span class="info-label">VM ID</span>
|
|
1275
|
-
<span class="info-value">${vmId}</span>
|
|
1276
|
-
</div>
|
|
1277
|
-
<div class="info-row">
|
|
1278
|
-
<span class="info-label">Runtime</span>
|
|
1279
|
-
<span class="info-value">node22-demo</span>
|
|
1280
|
-
</div>
|
|
1281
|
-
<div class="info-row">
|
|
1282
|
-
<span class="info-label">Published Ports</span>
|
|
1283
|
-
<span class="info-value">${ports.join(", ")}</span>
|
|
1284
|
-
</div>
|
|
1285
|
-
</div>
|
|
1286
|
-
<div class="card">
|
|
1287
|
-
<h2>Next Steps</h2>
|
|
1288
|
-
<ul class="steps">
|
|
1289
|
-
<li><strong>Connect to the VM:</strong> <code>vmsan connect ${vmId}</code></li>
|
|
1290
|
-
<li><strong>Deploy your app:</strong> Replace this page by stopping the welcome service and running your own server on the published port(s).</li>
|
|
1291
|
-
<li><strong>Stop this page:</strong> <code>systemctl stop vmsan-welcome</code></li>
|
|
1292
|
-
</ul>
|
|
1293
|
-
</div>
|
|
1294
|
-
<div class="footer">Powered by vmsan · Firecracker microVMs</div>
|
|
1295
|
-
</div>
|
|
1296
|
-
</body>
|
|
1297
|
-
</html>`;
|
|
1298
|
-
}
|
|
1299
|
-
function generateWelcomeServer(ports) {
|
|
1300
|
-
return `"use strict";
|
|
1301
|
-
const http = require("node:http");
|
|
1302
|
-
const fs = require("node:fs");
|
|
1303
|
-
const path = require("node:path");
|
|
1304
|
-
|
|
1305
|
-
const html = fs.readFileSync(path.join(__dirname, "index.html"), "utf-8");
|
|
1306
|
-
|
|
1307
|
-
const server = http.createServer((req, res) => {
|
|
1308
|
-
res.writeHead(200, {
|
|
1309
|
-
"Content-Type": "text/html; charset=utf-8",
|
|
1310
|
-
"Cache-Control": "no-cache",
|
|
1311
|
-
});
|
|
1312
|
-
res.end(html);
|
|
1313
|
-
});
|
|
1314
|
-
|
|
1315
|
-
${ports.map((p) => `server.listen(${p}, "0.0.0.0", () => console.log("vmsan-welcome listening on 0.0.0.0:${p}"));`).join("\n")}
|
|
1316
|
-
`;
|
|
1317
|
-
}
|
|
1318
|
-
function generateWelcomeService(ports) {
|
|
1319
|
-
return `[Unit]
|
|
1320
|
-
Description=${`vmsan welcome page on port(s) ${ports.join(", ")}`}
|
|
1321
|
-
After=network.target
|
|
1322
|
-
|
|
1323
|
-
[Service]
|
|
1324
|
-
Type=simple
|
|
1325
|
-
ExecStart=/usr/local/bin/node /opt/vmsan/welcome/server.js
|
|
1326
|
-
Restart=on-failure
|
|
1327
|
-
RestartSec=2
|
|
1328
|
-
|
|
1329
|
-
[Install]
|
|
1330
|
-
WantedBy=multi-user.target
|
|
1331
|
-
`;
|
|
1332
|
-
}
|
|
1333
|
-
/**
|
|
1334
1206
|
* Template generators for the vmsan-agent systemd service.
|
|
1335
|
-
* Follows the same pattern as welcome-page.ts.
|
|
1336
1207
|
*/
|
|
1337
1208
|
function generateAgentService() {
|
|
1338
1209
|
return `[Unit]
|
|
@@ -1416,19 +1287,10 @@ var Jailer = class {
|
|
|
1416
1287
|
try {
|
|
1417
1288
|
execSync(`sudo mount -o loop "${paths.rootfsPath}" "${tmpMount}"`, { stdio: "pipe" });
|
|
1418
1289
|
execSync(`rm -f "${tmpMount}/etc/resolv.conf" && ln -s /proc/net/pnp "${tmpMount}/etc/resolv.conf"`, { stdio: "pipe" });
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
writeFileSync(join(welcomeDir, "index.html"), generateWelcomeHtml(welcomeVmId, welcomePorts));
|
|
1424
|
-
writeFileSync(join(welcomeDir, "server.js"), generateWelcomeServer(welcomePorts));
|
|
1425
|
-
const systemdDir = join(tmpMount, "etc", "systemd", "system");
|
|
1426
|
-
mkdirSync(systemdDir, { recursive: true });
|
|
1427
|
-
writeFileSync(join(systemdDir, "vmsan-welcome.service"), generateWelcomeService(welcomePorts));
|
|
1428
|
-
const wantsDir = join(systemdDir, "multi-user.target.wants");
|
|
1429
|
-
mkdirSync(wantsDir, { recursive: true });
|
|
1430
|
-
execSync(`ln -sf /etc/systemd/system/vmsan-welcome.service "${join(wantsDir, "vmsan-welcome.service")}"`, { stdio: "pipe" });
|
|
1431
|
-
}
|
|
1290
|
+
writeFileSync(join(tmpMount, "etc", "hostname"), `${this.vmId}\n`);
|
|
1291
|
+
const hostsPath = join(tmpMount, "etc", "hosts");
|
|
1292
|
+
const hostsContent = existsSync(hostsPath) ? readFileSync(hostsPath, "utf-8") : "";
|
|
1293
|
+
if (!hostsContent.includes(this.vmId)) writeFileSync(hostsPath, `${hostsContent.trimEnd()}\n127.0.1.1 ${this.vmId}\n`);
|
|
1432
1294
|
if (config.agent) {
|
|
1433
1295
|
const agentDst = join(tmpMount, "usr", "local", "bin", "vmsan-agent");
|
|
1434
1296
|
mkdirSync(join(tmpMount, "usr", "local", "bin"), { recursive: true });
|
|
@@ -1445,7 +1307,19 @@ var Jailer = class {
|
|
|
1445
1307
|
execSync(`ln -sf /etc/systemd/system/vmsan-agent.service "${join(wantsDir, "vmsan-agent.service")}"`, { stdio: "pipe" });
|
|
1446
1308
|
}
|
|
1447
1309
|
const ubuntuHome = join(tmpMount, "home", "ubuntu");
|
|
1448
|
-
|
|
1310
|
+
const rootfsPasswd = join(tmpMount, "etc", "passwd");
|
|
1311
|
+
if (existsSync(ubuntuHome) && existsSync(rootfsPasswd)) {
|
|
1312
|
+
const ubuntuEntry = readFileSync(rootfsPasswd, "utf-8").split("\n").find((l) => l.startsWith("ubuntu:"));
|
|
1313
|
+
if (ubuntuEntry) {
|
|
1314
|
+
const fields = ubuntuEntry.split(":");
|
|
1315
|
+
if (fields.length >= 4 && /^\d+$/.test(fields[2]) && /^\d+$/.test(fields[3])) execFileSync("sudo", [
|
|
1316
|
+
"chown",
|
|
1317
|
+
"-R",
|
|
1318
|
+
`${fields[2]}:${fields[3]}`,
|
|
1319
|
+
ubuntuHome
|
|
1320
|
+
], { stdio: "pipe" });
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1449
1323
|
execSync(`sudo umount "${tmpMount}"`, { stdio: "pipe" });
|
|
1450
1324
|
} catch {
|
|
1451
1325
|
try {
|
|
@@ -1509,6 +1383,34 @@ function findKernel(baseDir) {
|
|
|
1509
1383
|
if (files.length === 0) throw noKernelError();
|
|
1510
1384
|
return join(kernelDir, files.sort().at(-1));
|
|
1511
1385
|
}
|
|
1386
|
+
const RUNTIME_ROOTFS_MAP = {
|
|
1387
|
+
node22: "node22.ext4",
|
|
1388
|
+
node24: "node24.ext4",
|
|
1389
|
+
"python3.13": "python3.13.ext4"
|
|
1390
|
+
};
|
|
1391
|
+
const BASE_ROOTFS_FILENAMES = ["ubuntu-24.04.ext4"];
|
|
1392
|
+
function findRuntimeRootfs(runtime, baseDir) {
|
|
1393
|
+
const filename = RUNTIME_ROOTFS_MAP[runtime];
|
|
1394
|
+
const rootfsPath = join(baseDir, "rootfs", filename);
|
|
1395
|
+
if (!existsSync(rootfsPath)) throw new SetupError("ERR_SETUP_NO_EXT4_ROOTFS", {
|
|
1396
|
+
message: `Runtime "${runtime}" rootfs not found at ${rootfsPath}`,
|
|
1397
|
+
fix: "Run \"curl -fsSL https://vmsan.dev/install | bash\" to build runtime images."
|
|
1398
|
+
});
|
|
1399
|
+
return rootfsPath;
|
|
1400
|
+
}
|
|
1401
|
+
function findBaseRootfs(baseDir) {
|
|
1402
|
+
const rootfsDir = join(baseDir, "rootfs");
|
|
1403
|
+
if (!existsSync(rootfsDir)) throw noRootfsDirError();
|
|
1404
|
+
for (const filename of BASE_ROOTFS_FILENAMES) {
|
|
1405
|
+
const rootfsPath = join(rootfsDir, filename);
|
|
1406
|
+
if (existsSync(rootfsPath)) return rootfsPath;
|
|
1407
|
+
}
|
|
1408
|
+
const files = readdirSync(rootfsDir).filter((fileName) => fileName.endsWith(".ext4"));
|
|
1409
|
+
const runtimeFilenames = new Set(Object.values(RUNTIME_ROOTFS_MAP));
|
|
1410
|
+
const baseFiles = files.filter((fileName) => !runtimeFilenames.has(fileName));
|
|
1411
|
+
if (baseFiles.length === 0) throw noExt4RootfsError();
|
|
1412
|
+
return join(rootfsDir, baseFiles.sort().at(-1));
|
|
1413
|
+
}
|
|
1512
1414
|
function findRootfs(baseDir) {
|
|
1513
1415
|
const rootfsDir = join(baseDir, "rootfs");
|
|
1514
1416
|
if (!existsSync(rootfsDir)) throw noRootfsDirError();
|
|
@@ -1651,16 +1553,18 @@ const APT_PACKAGES = [
|
|
|
1651
1553
|
"findutils",
|
|
1652
1554
|
"git",
|
|
1653
1555
|
"gzip",
|
|
1556
|
+
"iptables",
|
|
1654
1557
|
"iputils-ping",
|
|
1655
1558
|
"libicu-dev",
|
|
1656
1559
|
"libjpeg-dev",
|
|
1657
1560
|
"libpng-dev",
|
|
1658
1561
|
"ncurses-base",
|
|
1659
1562
|
"libssl-dev",
|
|
1660
|
-
"openssh-server",
|
|
1661
1563
|
"openssl",
|
|
1662
1564
|
"procps",
|
|
1663
1565
|
"sudo",
|
|
1566
|
+
"systemd",
|
|
1567
|
+
"systemd-sysv",
|
|
1664
1568
|
"tar",
|
|
1665
1569
|
"unzip",
|
|
1666
1570
|
"debianutils",
|
|
@@ -1673,12 +1577,12 @@ const DNF_PACKAGES = [
|
|
|
1673
1577
|
"findutils",
|
|
1674
1578
|
"git",
|
|
1675
1579
|
"gzip",
|
|
1580
|
+
"iptables",
|
|
1676
1581
|
"iputils",
|
|
1677
1582
|
"libicu",
|
|
1678
1583
|
"libjpeg",
|
|
1679
1584
|
"libpng",
|
|
1680
1585
|
"ncurses-libs",
|
|
1681
|
-
"openssh-server",
|
|
1682
1586
|
"openssl",
|
|
1683
1587
|
"openssl-libs",
|
|
1684
1588
|
"procps",
|
|
@@ -1696,13 +1600,13 @@ const APK_PACKAGES = [
|
|
|
1696
1600
|
"findutils",
|
|
1697
1601
|
"git",
|
|
1698
1602
|
"gzip",
|
|
1603
|
+
"iptables",
|
|
1699
1604
|
"iputils",
|
|
1700
1605
|
"icu-libs",
|
|
1701
1606
|
"libjpeg-turbo",
|
|
1702
1607
|
"libpng",
|
|
1703
1608
|
"ncurses-libs",
|
|
1704
1609
|
"openrc",
|
|
1705
|
-
"openssh",
|
|
1706
1610
|
"openssl",
|
|
1707
1611
|
"procps",
|
|
1708
1612
|
"sudo",
|
|
@@ -1711,7 +1615,8 @@ const APK_PACKAGES = [
|
|
|
1711
1615
|
"whois",
|
|
1712
1616
|
"zstd"
|
|
1713
1617
|
];
|
|
1714
|
-
function generateDockerfile(baseImage) {
|
|
1618
|
+
function generateDockerfile(baseImage, minimal = false) {
|
|
1619
|
+
if (minimal) return `FROM ${baseImage}\n`;
|
|
1715
1620
|
return `FROM ${baseImage}
|
|
1716
1621
|
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/*`}; \\
|
|
1717
1622
|
elif command -v dnf >/dev/null 2>&1; then ${`dnf install -y ${DNF_PACKAGES.join(" ")} && dnf clean all`}; \\
|
|
@@ -1725,13 +1630,22 @@ RUN if command -v apk >/dev/null 2>&1; then \\
|
|
|
1725
1630
|
fi; \\
|
|
1726
1631
|
echo 'ubuntu ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/ubuntu; \\
|
|
1727
1632
|
chmod 440 /etc/sudoers.d/ubuntu; \\
|
|
1728
|
-
|
|
1729
|
-
RUN
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1633
|
+
chown -R ubuntu:ubuntu /home/ubuntu
|
|
1634
|
+
RUN EXTRA=""; \\
|
|
1635
|
+
if command -v npm >/dev/null 2>&1; then \\
|
|
1636
|
+
su -c 'mkdir -p /home/ubuntu/.npm-global && npm config set prefix /home/ubuntu/.npm-global' ubuntu; \\
|
|
1637
|
+
EXTRA="\${EXTRA}export PATH=\\"/home/ubuntu/.npm-global/bin:\\$PATH\\"\\n"; \\
|
|
1733
1638
|
fi; \\
|
|
1734
|
-
if command -v
|
|
1639
|
+
if command -v pip3 >/dev/null 2>&1 || command -v pip >/dev/null 2>&1; then \\
|
|
1640
|
+
EXTRA="\${EXTRA}export PATH=\\"/home/ubuntu/.local/bin:\\$PATH\\"\\n"; \\
|
|
1641
|
+
fi; \\
|
|
1642
|
+
if [ -n "$EXTRA" ]; then \\
|
|
1643
|
+
printf '%b' "$EXTRA" >> /home/ubuntu/.profile; \\
|
|
1644
|
+
{ printf '%b' "$EXTRA"; cat /home/ubuntu/.bashrc; } > /home/ubuntu/.bashrc.tmp \\
|
|
1645
|
+
&& mv /home/ubuntu/.bashrc.tmp /home/ubuntu/.bashrc; \\
|
|
1646
|
+
fi; \\
|
|
1647
|
+
chown -R ubuntu:ubuntu /home/ubuntu
|
|
1648
|
+
RUN if command -v rc-update >/dev/null 2>&1; then \\
|
|
1735
1649
|
rc-update add devfs sysinit 2>/dev/null || true; \\
|
|
1736
1650
|
rc-update add mdev sysinit 2>/dev/null || true; \\
|
|
1737
1651
|
rc-update add hwdrivers sysinit 2>/dev/null || true; \\
|
|
@@ -1740,10 +1654,8 @@ RUN ssh-keygen -A 2>/dev/null || true; \\
|
|
|
1740
1654
|
rc-update add hostname boot 2>/dev/null || true; \\
|
|
1741
1655
|
rc-update add bootmisc boot 2>/dev/null || true; \\
|
|
1742
1656
|
rc-update add networking boot 2>/dev/null || true; \\
|
|
1743
|
-
rc-update add sshd default 2>/dev/null || true; \\
|
|
1744
1657
|
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; \\
|
|
1745
|
-
fi
|
|
1746
|
-
if command -v systemctl >/dev/null 2>&1; then systemctl enable sshd 2>/dev/null || systemctl enable ssh 2>/dev/null || true; fi
|
|
1658
|
+
fi
|
|
1747
1659
|
`;
|
|
1748
1660
|
}
|
|
1749
1661
|
function verifyDocker() {
|
|
@@ -1753,7 +1665,7 @@ function verifyDocker() {
|
|
|
1753
1665
|
throw dockerUnavailableError();
|
|
1754
1666
|
}
|
|
1755
1667
|
}
|
|
1756
|
-
function buildImageRootfs(imageRef, cacheDir) {
|
|
1668
|
+
function buildImageRootfs(imageRef, cacheDir, minimal = false) {
|
|
1757
1669
|
const ext4Path = join(cacheDir, "rootfs.ext4");
|
|
1758
1670
|
verifyDocker();
|
|
1759
1671
|
const buildTag = `vmsan-rootfs-${imageRef.name.replace(/[^a-z0-9._-]/gi, "-")}:${imageRef.tag}`;
|
|
@@ -1762,7 +1674,7 @@ function buildImageRootfs(imageRef, cacheDir) {
|
|
|
1762
1674
|
mkdirSync(cacheDir, { recursive: true });
|
|
1763
1675
|
try {
|
|
1764
1676
|
consola.start(`Building image from ${imageRef.full}...`);
|
|
1765
|
-
execSync(`docker build -t "${buildTag}" -f - . <<'DOCKERFILE'\n${generateDockerfile(imageRef.full)}\nDOCKERFILE`, {
|
|
1677
|
+
execSync(`docker build -t "${buildTag}" -f - . <<'DOCKERFILE'\n${generateDockerfile(imageRef.full, minimal)}\nDOCKERFILE`, {
|
|
1766
1678
|
stdio: "pipe",
|
|
1767
1679
|
shell: "/bin/bash"
|
|
1768
1680
|
});
|
|
@@ -1800,19 +1712,20 @@ function buildImageRootfs(imageRef, cacheDir) {
|
|
|
1800
1712
|
} catch {}
|
|
1801
1713
|
}
|
|
1802
1714
|
}
|
|
1803
|
-
function resolveImageRootfs(imageRef, registryDir) {
|
|
1804
|
-
const
|
|
1715
|
+
function resolveImageRootfs(imageRef, registryDir, minimal = false) {
|
|
1716
|
+
const cacheSuffix = minimal ? "-minimal" : "";
|
|
1717
|
+
const cacheDir = join(registryDir, `${imageRef.cacheKey}${cacheSuffix}`);
|
|
1805
1718
|
const ext4Path = join(cacheDir, "rootfs.ext4");
|
|
1806
1719
|
if (existsSync(ext4Path)) {
|
|
1807
1720
|
consola.info(`Using cached rootfs for ${imageRef.full}`);
|
|
1808
1721
|
return ext4Path;
|
|
1809
1722
|
}
|
|
1810
|
-
return buildImageRootfs(imageRef, cacheDir);
|
|
1723
|
+
return buildImageRootfs(imageRef, cacheDir, minimal);
|
|
1811
1724
|
}
|
|
1812
1725
|
const VALID_RUNTIMES = [
|
|
1813
1726
|
"base",
|
|
1814
1727
|
"node22",
|
|
1815
|
-
"
|
|
1728
|
+
"node24",
|
|
1816
1729
|
"python3.13"
|
|
1817
1730
|
];
|
|
1818
1731
|
const VALID_NETWORK_POLICIES = [
|
|
@@ -2042,11 +1955,12 @@ var VMService = class {
|
|
|
2042
1955
|
const kernelPath = opts.kernelPath ?? findKernel(paths.baseDir);
|
|
2043
1956
|
logger.debug(`Kernel resolved: ${kernelPath}`);
|
|
2044
1957
|
let rootfsPath;
|
|
2045
|
-
if (opts.fromImage) rootfsPath = resolveImageRootfs(opts.fromImage, paths.registryDir);
|
|
2046
|
-
else
|
|
1958
|
+
if (opts.fromImage) rootfsPath = resolveImageRootfs(opts.fromImage, paths.registryDir, true);
|
|
1959
|
+
else if (runtime !== "base" && !opts.rootfsPath) rootfsPath = findRuntimeRootfs(runtime, paths.baseDir);
|
|
1960
|
+
else rootfsPath = opts.rootfsPath ?? findBaseRootfs(paths.baseDir);
|
|
2047
1961
|
logger.debug(`Rootfs resolved: ${rootfsPath}`);
|
|
2048
1962
|
const netnsName = opts.disableNetns ? void 0 : `vmsan-${vmId}`;
|
|
2049
|
-
const agentToken = existsSync(paths.agentBin) ? randomBytes(32).toString("hex") : null;
|
|
1963
|
+
const agentToken = !opts.fromImage && existsSync(paths.agentBin) ? randomBytes(32).toString("hex") : null;
|
|
2050
1964
|
log.start(`Creating VM ${vmId}...`);
|
|
2051
1965
|
const { net } = new FileLock(join(paths.vmsDir, ".slot-lock"), "slot-alloc").run(() => {
|
|
2052
1966
|
const slot = this.store.allocateNetworkSlot();
|
|
@@ -2100,10 +2014,6 @@ var VMService = class {
|
|
|
2100
2014
|
memFile: join(paths.snapshotsDir, snapshotId, "mem_file")
|
|
2101
2015
|
} : void 0;
|
|
2102
2016
|
const jailer = new Jailer(vmId, paths.jailerBaseDir);
|
|
2103
|
-
const welcomePage = runtime === "node22-demo" && ports.length > 0 ? {
|
|
2104
|
-
vmId,
|
|
2105
|
-
ports
|
|
2106
|
-
} : void 0;
|
|
2107
2017
|
const agentConfig = agentToken ? {
|
|
2108
2018
|
binaryPath: paths.agentBin,
|
|
2109
2019
|
token: agentToken,
|
|
@@ -2115,7 +2025,6 @@ var VMService = class {
|
|
|
2115
2025
|
rootfsSrc: rootfsPath,
|
|
2116
2026
|
diskSizeGb,
|
|
2117
2027
|
snapshot: snapshotConfig,
|
|
2118
|
-
welcomePage,
|
|
2119
2028
|
agent: agentConfig
|
|
2120
2029
|
});
|
|
2121
2030
|
chrootDir = jailerPaths.chrootDir;
|
|
@@ -2167,6 +2076,7 @@ var VMService = class {
|
|
|
2167
2076
|
timeoutMs,
|
|
2168
2077
|
stateFile: join(paths.vmsDir, `${vmId}.json`)
|
|
2169
2078
|
});
|
|
2079
|
+
if (agentToken && opts.ports?.length) await this.setupLocalhostPortForwarding(netCfg.guestIp, paths.agentPort, agentToken, opts.ports);
|
|
2170
2080
|
const finalState = this.store.load(vmId);
|
|
2171
2081
|
await hooks.callHook("vm:afterCreate", finalState);
|
|
2172
2082
|
return {
|
|
@@ -2321,6 +2231,7 @@ var VMService = class {
|
|
|
2321
2231
|
pid
|
|
2322
2232
|
});
|
|
2323
2233
|
log.success(`VM ${vmId} is running (PID: ${pid || "unknown"})`);
|
|
2234
|
+
if (state.agentToken && state.network.publishedPorts?.length) await this.setupLocalhostPortForwarding(state.network.guestIp, state.agentPort, state.agentToken, state.network.publishedPorts);
|
|
2324
2235
|
const finalState = this.store.load(vmId);
|
|
2325
2236
|
await hooks.callHook("vm:afterStart", finalState);
|
|
2326
2237
|
return {
|
|
@@ -2515,6 +2426,26 @@ var VMService = class {
|
|
|
2515
2426
|
await vm.configure(vcpus, memMib);
|
|
2516
2427
|
await vm.addNetwork("eth0", netCfg.tapDevice, netCfg.macAddress);
|
|
2517
2428
|
}
|
|
2429
|
+
/**
|
|
2430
|
+
* Set up iptables DNAT rules inside the VM so that traffic arriving on
|
|
2431
|
+
* published ports is forwarded to 127.0.0.1. Many services bind to
|
|
2432
|
+
* localhost only; this lets the Cloudflare tunnel (which connects to the
|
|
2433
|
+
* guest IP) reach them.
|
|
2434
|
+
*/
|
|
2435
|
+
async setupLocalhostPortForwarding(guestIp, agentPort, agentToken, ports) {
|
|
2436
|
+
try {
|
|
2437
|
+
await waitForAgent(guestIp, agentPort);
|
|
2438
|
+
const agent = new AgentClient(`http://${guestIp}:${agentPort}`, agentToken);
|
|
2439
|
+
const iptablesRules = ports.map((p) => `sudo iptables-legacy -t nat -A PREROUTING -i eth0 -p tcp --dport ${p} -j DNAT --to-destination 127.0.0.1:${p}`).join(" && ");
|
|
2440
|
+
await agent.runCommand({
|
|
2441
|
+
cmd: "/bin/bash",
|
|
2442
|
+
args: ["-c", `sudo sysctl -w net.ipv4.conf.all.route_localnet=1 && ${iptablesRules}`]
|
|
2443
|
+
});
|
|
2444
|
+
this.logger.debug(`Localhost port forwarding set up for ports: ${ports.join(", ")}`);
|
|
2445
|
+
} catch (err) {
|
|
2446
|
+
this.logger.warn(`Failed to set up localhost port forwarding: ${toError(err).message}`);
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2518
2449
|
markAsError(vmId, error) {
|
|
2519
2450
|
try {
|
|
2520
2451
|
this.store.update(vmId, {
|
|
@@ -2961,4 +2892,4 @@ async function createVmsan(options) {
|
|
|
2961
2892
|
for (const plugin of allPlugins) await plugin.setup(ctx);
|
|
2962
2893
|
return vmsan;
|
|
2963
2894
|
}
|
|
2964
|
-
export { findKernel as A,
|
|
2895
|
+
export { findKernel as A, NetworkManager as B, resolveImageRootfs as C, killOrphanVmProcess as D, cleanupNetwork as E, validateEnvironment as F, createSilentLogger as H, waitForSocket as I, Jailer as L, findRuntimeRootfs as M, getVmJailerPid as N, markVmAsError as O, getVmPid as P, detectCgroupVersion as R, validatePublishedPortsAvailable as S, cleanupChroot as T, createDefaultLogger as V, parseNetworkPolicy as _, resolveTunnelHostnames as a, parseVcpuCount as b, VMService as c, parseBandwidth as d, parseCidrList as f, parseMemoryMib as g, parseImageReference as h, CloudflareService as i, findRootfs as j, assertSnapshotExists as k, compileSeccompFilter as l, parseDomains as m, cloudflarePlugin as n, PidFile as o, parseDiskSizeGb as p, cleanupCloudflareResources as r, definePlugin as s, createVmsan as t, ensureSeccompFilter as u, parsePublishedPorts as v, buildInitialVmState as w, validateCidr as x, parseRuntime as y, FileLock as z };
|