@ghalex/kvbox-server 0.0.2 → 0.0.3
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/dist/kvbox-server.js +435 -55
- package/dist/kvbox-server.js.map +1 -1
- package/package.json +3 -2
- package/scripts/net-setup.sh +143 -0
package/dist/kvbox-server.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
+
import { spawnSync } from "child_process";
|
|
4
5
|
import { Command } from "commander";
|
|
5
6
|
|
|
6
7
|
// ../shared/src/errors.ts
|
|
@@ -18,14 +19,12 @@ var NotImplementedError = class extends KvboxError {
|
|
|
18
19
|
this.name = "NotImplementedError";
|
|
19
20
|
}
|
|
20
21
|
};
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
import { mkdir, readFile, readdir, writeFile } from "fs/promises";
|
|
28
|
-
import { z } from "zod";
|
|
22
|
+
var NotFoundError = class extends KvboxError {
|
|
23
|
+
constructor(what) {
|
|
24
|
+
super(`${what}: not found`, 1);
|
|
25
|
+
this.name = "NotFoundError";
|
|
26
|
+
}
|
|
27
|
+
};
|
|
29
28
|
|
|
30
29
|
// ../shared/src/paths.ts
|
|
31
30
|
import { homedir } from "os";
|
|
@@ -67,34 +66,9 @@ function paths(home = kvboxHome()) {
|
|
|
67
66
|
};
|
|
68
67
|
}
|
|
69
68
|
|
|
70
|
-
// src/core/state.ts
|
|
71
|
-
var MachineMetaSchema = z.object({
|
|
72
|
-
name: z.string(),
|
|
73
|
-
ip: z.string(),
|
|
74
|
-
mac: z.string(),
|
|
75
|
-
pid: z.number().int(),
|
|
76
|
-
vcpu: z.number().int(),
|
|
77
|
-
memMib: z.number().int(),
|
|
78
|
-
createdAt: z.string(),
|
|
79
|
-
source: z.string()
|
|
80
|
-
});
|
|
81
|
-
var SnapshotMetaSchema = z.object({
|
|
82
|
-
name: z.string(),
|
|
83
|
-
source: z.string(),
|
|
84
|
-
vcpu: z.number().int(),
|
|
85
|
-
memMib: z.number().int(),
|
|
86
|
-
createdAt: z.string()
|
|
87
|
-
});
|
|
88
|
-
async function ensureLayout(p = paths()) {
|
|
89
|
-
for (const dir of [p.home, p.ssh, p.bin, p.assets, p.machines, p.snapshots]) {
|
|
90
|
-
await mkdir(dir, { recursive: true, mode: 448 });
|
|
91
|
-
}
|
|
92
|
-
return p;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
69
|
// src/core/config.ts
|
|
96
|
-
import { readFile
|
|
97
|
-
import { z
|
|
70
|
+
import { readFile, writeFile } from "fs/promises";
|
|
71
|
+
import { z } from "zod";
|
|
98
72
|
|
|
99
73
|
// ../shared/src/sizes.ts
|
|
100
74
|
var SIZES = {
|
|
@@ -104,16 +78,19 @@ var SIZES = {
|
|
|
104
78
|
};
|
|
105
79
|
var SIZE_NAMES = Object.keys(SIZES);
|
|
106
80
|
var DEFAULT_SIZE = "s";
|
|
81
|
+
function isSizeName(value) {
|
|
82
|
+
return value in SIZES;
|
|
83
|
+
}
|
|
107
84
|
|
|
108
85
|
// src/core/config.ts
|
|
109
|
-
var ConfigSchema =
|
|
110
|
-
kernel:
|
|
111
|
-
rootfs:
|
|
112
|
-
firecracker:
|
|
113
|
-
bridge:
|
|
114
|
-
subnet:
|
|
115
|
-
gateway:
|
|
116
|
-
defaultSize:
|
|
86
|
+
var ConfigSchema = z.object({
|
|
87
|
+
kernel: z.string(),
|
|
88
|
+
rootfs: z.string(),
|
|
89
|
+
firecracker: z.string(),
|
|
90
|
+
bridge: z.string().default("kvbox0"),
|
|
91
|
+
subnet: z.string().default("172.30.0.0/24"),
|
|
92
|
+
gateway: z.string().default("172.30.0.1"),
|
|
93
|
+
defaultSize: z.enum(SIZE_NAMES).default(DEFAULT_SIZE)
|
|
117
94
|
});
|
|
118
95
|
function defaultConfig(p = paths()) {
|
|
119
96
|
return ConfigSchema.parse({
|
|
@@ -124,18 +101,168 @@ function defaultConfig(p = paths()) {
|
|
|
124
101
|
}
|
|
125
102
|
async function loadConfig(p = paths()) {
|
|
126
103
|
try {
|
|
127
|
-
const raw = await
|
|
104
|
+
const raw = await readFile(p.config, "utf8");
|
|
128
105
|
return ConfigSchema.parse(JSON.parse(raw));
|
|
129
106
|
} catch (err) {
|
|
130
107
|
if (err.code === "ENOENT") {
|
|
131
108
|
const cfg = defaultConfig(p);
|
|
132
|
-
await
|
|
109
|
+
await writeFile(p.config, JSON.stringify(cfg, null, 2) + "\n");
|
|
133
110
|
return cfg;
|
|
134
111
|
}
|
|
135
112
|
throw err;
|
|
136
113
|
}
|
|
137
114
|
}
|
|
138
115
|
|
|
116
|
+
// src/core/network.ts
|
|
117
|
+
import { existsSync } from "fs";
|
|
118
|
+
import { fileURLToPath } from "url";
|
|
119
|
+
|
|
120
|
+
// src/core/exec.ts
|
|
121
|
+
import { execFile } from "child_process";
|
|
122
|
+
import { promisify } from "util";
|
|
123
|
+
var execFileAsync = promisify(execFile);
|
|
124
|
+
async function run(argv, opts = {}) {
|
|
125
|
+
const [cmd, ...args] = opts.sudo ? ["sudo", "-n", ...argv] : argv;
|
|
126
|
+
if (!cmd) throw new KvboxError("run: empty argv");
|
|
127
|
+
if (process.env["KVBOX_DEBUG"]) console.error(`[exec] ${[cmd, ...args].join(" ")}`);
|
|
128
|
+
try {
|
|
129
|
+
const { stdout, stderr } = await execFileAsync(cmd, args, {
|
|
130
|
+
cwd: opts.cwd,
|
|
131
|
+
env: opts.env ?? process.env,
|
|
132
|
+
timeout: opts.timeoutMs ?? 3e4,
|
|
133
|
+
maxBuffer: 8 * 1024 * 1024
|
|
134
|
+
});
|
|
135
|
+
return { stdout, stderr };
|
|
136
|
+
} catch (err) {
|
|
137
|
+
const e = err;
|
|
138
|
+
const detail = (e.stderr ?? e.message ?? "").trim();
|
|
139
|
+
throw new KvboxError(`${[cmd, ...args].join(" ")} failed${detail ? `: ${detail}` : ""}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// src/core/network.ts
|
|
144
|
+
var TAP_PREFIX = "kv-";
|
|
145
|
+
var PRIVILEGED_SCRIPT = "/usr/local/lib/kvbox/net-setup.sh";
|
|
146
|
+
function tapName(box) {
|
|
147
|
+
return `${TAP_PREFIX}${box}`.slice(0, 15);
|
|
148
|
+
}
|
|
149
|
+
function netSetupScript() {
|
|
150
|
+
const override = process.env["KVBOX_NET_SETUP"];
|
|
151
|
+
if (override) return override;
|
|
152
|
+
if (existsSync(PRIVILEGED_SCRIPT)) return PRIVILEGED_SCRIPT;
|
|
153
|
+
for (const rel of ["../scripts/net-setup.sh", "../../scripts/net-setup.sh"]) {
|
|
154
|
+
const candidate = fileURLToPath(new URL(rel, import.meta.url));
|
|
155
|
+
if (existsSync(candidate)) return candidate;
|
|
156
|
+
}
|
|
157
|
+
throw new KvboxError("net-setup.sh not found next to kvbox-server");
|
|
158
|
+
}
|
|
159
|
+
function bridgeExists(bridge) {
|
|
160
|
+
return existsSync(`/sys/class/net/${bridge}`);
|
|
161
|
+
}
|
|
162
|
+
function netRun(cfg, args) {
|
|
163
|
+
return run([netSetupScript(), ...args], {
|
|
164
|
+
env: { ...process.env, KVBOX_BRIDGE: cfg.bridge, KVBOX_SUBNET: cfg.subnet, KVBOX_GATEWAY: cfg.gateway }
|
|
165
|
+
}).catch((err) => {
|
|
166
|
+
throw new KvboxError(
|
|
167
|
+
`${err.message}
|
|
168
|
+
(root is needed for host networking; install-server.sh sets up a sudoers rule for ${PRIVILEGED_SCRIPT})`
|
|
169
|
+
);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
async function ensureHostNetwork(cfg) {
|
|
173
|
+
if (bridgeExists(cfg.bridge)) return "present";
|
|
174
|
+
await netRun(cfg, ["up"]);
|
|
175
|
+
return "created";
|
|
176
|
+
}
|
|
177
|
+
async function createTap(box, cfg) {
|
|
178
|
+
const tap = tapName(box);
|
|
179
|
+
await netRun(cfg, ["tap", "add", tap]);
|
|
180
|
+
return tap;
|
|
181
|
+
}
|
|
182
|
+
async function deleteTap(box, cfg) {
|
|
183
|
+
await netRun(cfg, ["tap", "del", tapName(box)]);
|
|
184
|
+
}
|
|
185
|
+
var ipToInt = (ip) => ip.split(".").reduce((acc, o) => (acc << 8) + Number(o), 0) >>> 0;
|
|
186
|
+
var intToIp = (n) => [24, 16, 8, 0].map((s) => n >>> s & 255).join(".");
|
|
187
|
+
function nextFreeIp(subnet, inUse) {
|
|
188
|
+
const [base, lenStr] = subnet.split("/");
|
|
189
|
+
const len = Number(lenStr);
|
|
190
|
+
if (!base || !Number.isInteger(len) || len < 8 || len > 30) throw new KvboxError(`bad subnet: ${subnet}`);
|
|
191
|
+
const mask = len === 0 ? 0 : ~0 << 32 - len >>> 0;
|
|
192
|
+
const network = (ipToInt(base) & mask) >>> 0;
|
|
193
|
+
const broadcast = (network | ~mask >>> 0) >>> 0;
|
|
194
|
+
const used = new Set(inUse.map(ipToInt));
|
|
195
|
+
for (let n = network + 2; n < broadcast; n++) {
|
|
196
|
+
if (!used.has(n)) return intToIp(n);
|
|
197
|
+
}
|
|
198
|
+
throw new KvboxError(`no free IP left in ${subnet}`);
|
|
199
|
+
}
|
|
200
|
+
function macFor(ip) {
|
|
201
|
+
const hex = ip.split(".").map((o) => Number(o).toString(16).padStart(2, "0").toUpperCase());
|
|
202
|
+
return ["06", "00", ...hex].join(":");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// src/listen.ts
|
|
206
|
+
import { rm as rm2, writeFile as writeFile3 } from "fs/promises";
|
|
207
|
+
import { createAdaptorServer } from "@hono/node-server";
|
|
208
|
+
|
|
209
|
+
// src/core/state.ts
|
|
210
|
+
import { mkdir, readFile as readFile2, readdir, writeFile as writeFile2 } from "fs/promises";
|
|
211
|
+
import { z as z2 } from "zod";
|
|
212
|
+
var MachineMetaSchema = z2.object({
|
|
213
|
+
name: z2.string(),
|
|
214
|
+
ip: z2.string(),
|
|
215
|
+
mac: z2.string(),
|
|
216
|
+
pid: z2.number().int(),
|
|
217
|
+
vcpu: z2.number().int(),
|
|
218
|
+
memMib: z2.number().int(),
|
|
219
|
+
createdAt: z2.string(),
|
|
220
|
+
source: z2.string()
|
|
221
|
+
});
|
|
222
|
+
var SnapshotMetaSchema = z2.object({
|
|
223
|
+
name: z2.string(),
|
|
224
|
+
source: z2.string(),
|
|
225
|
+
vcpu: z2.number().int(),
|
|
226
|
+
memMib: z2.number().int(),
|
|
227
|
+
createdAt: z2.string()
|
|
228
|
+
});
|
|
229
|
+
async function ensureLayout(p = paths()) {
|
|
230
|
+
for (const dir of [p.home, p.ssh, p.bin, p.assets, p.machines, p.snapshots]) {
|
|
231
|
+
await mkdir(dir, { recursive: true, mode: 448 });
|
|
232
|
+
}
|
|
233
|
+
return p;
|
|
234
|
+
}
|
|
235
|
+
async function readJson(file, schema) {
|
|
236
|
+
return schema.parse(JSON.parse(await readFile2(file, "utf8")));
|
|
237
|
+
}
|
|
238
|
+
async function listDirs(dir) {
|
|
239
|
+
try {
|
|
240
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
241
|
+
return entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
242
|
+
} catch (err) {
|
|
243
|
+
if (err.code === "ENOENT") return [];
|
|
244
|
+
throw err;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
async function readMachineMeta(name, p = paths()) {
|
|
248
|
+
return readJson(p.machine(name).meta, MachineMetaSchema);
|
|
249
|
+
}
|
|
250
|
+
async function writeMachineMeta(meta, p = paths()) {
|
|
251
|
+
await mkdir(p.machine(meta.name).dir, { recursive: true });
|
|
252
|
+
await writeFile2(p.machine(meta.name).meta, JSON.stringify(meta, null, 2) + "\n");
|
|
253
|
+
}
|
|
254
|
+
async function listMachineNames(p = paths()) {
|
|
255
|
+
return listDirs(p.machines);
|
|
256
|
+
}
|
|
257
|
+
function pidAlive(pid) {
|
|
258
|
+
try {
|
|
259
|
+
process.kill(pid, 0);
|
|
260
|
+
return true;
|
|
261
|
+
} catch (err) {
|
|
262
|
+
return err.code === "EPERM";
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
139
266
|
// src/app.ts
|
|
140
267
|
import { Hono as Hono4 } from "hono";
|
|
141
268
|
|
|
@@ -152,7 +279,7 @@ function versionOf(srcFileUrl, injected) {
|
|
|
152
279
|
}
|
|
153
280
|
|
|
154
281
|
// src/version.ts
|
|
155
|
-
var VERSION = versionOf(import.meta.url, true ? "0.0.
|
|
282
|
+
var VERSION = versionOf(import.meta.url, true ? "0.0.3" : void 0);
|
|
156
283
|
|
|
157
284
|
// src/routes/capacity.ts
|
|
158
285
|
import { Hono } from "hono";
|
|
@@ -221,14 +348,250 @@ var ApiErrorSchema = z3.object({
|
|
|
221
348
|
});
|
|
222
349
|
|
|
223
350
|
// src/core/machines.ts
|
|
224
|
-
|
|
225
|
-
|
|
351
|
+
import { spawn } from "child_process";
|
|
352
|
+
import { closeSync, existsSync as existsSync2, openSync } from "fs";
|
|
353
|
+
import { mkdir as mkdir2, readFile as readFile3, rm } from "fs/promises";
|
|
354
|
+
import { setTimeout as sleep } from "timers/promises";
|
|
355
|
+
|
|
356
|
+
// src/core/firecracker.ts
|
|
357
|
+
import { Agent, request } from "undici";
|
|
358
|
+
var FirecrackerClient = class {
|
|
359
|
+
constructor(socketPath) {
|
|
360
|
+
this.socketPath = socketPath;
|
|
361
|
+
this.agent = new Agent({ connect: { socketPath } });
|
|
362
|
+
}
|
|
363
|
+
socketPath;
|
|
364
|
+
agent;
|
|
365
|
+
async call(method, path, body) {
|
|
366
|
+
const res = await request(`http://localhost${path}`, {
|
|
367
|
+
dispatcher: this.agent,
|
|
368
|
+
method,
|
|
369
|
+
headers: body === void 0 ? {} : { "content-type": "application/json" },
|
|
370
|
+
body: body === void 0 ? null : JSON.stringify(body)
|
|
371
|
+
});
|
|
372
|
+
const text = await res.body.text();
|
|
373
|
+
if (res.statusCode >= 300) {
|
|
374
|
+
throw new KvboxError(`firecracker ${method} ${path} -> ${res.statusCode}: ${text}`);
|
|
375
|
+
}
|
|
376
|
+
return text ? JSON.parse(text) : void 0;
|
|
377
|
+
}
|
|
378
|
+
bootSource(kernelImagePath, bootArgs2) {
|
|
379
|
+
return this.call("PUT", "/boot-source", { kernel_image_path: kernelImagePath, boot_args: bootArgs2 });
|
|
380
|
+
}
|
|
381
|
+
rootDrive(pathOnHost) {
|
|
382
|
+
return this.call("PUT", "/drives/rootfs", {
|
|
383
|
+
drive_id: "rootfs",
|
|
384
|
+
path_on_host: pathOnHost,
|
|
385
|
+
is_root_device: true,
|
|
386
|
+
is_read_only: false
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
machineConfig(vcpuCount, memSizeMib) {
|
|
390
|
+
return this.call("PUT", "/machine-config", { vcpu_count: vcpuCount, mem_size_mib: memSizeMib });
|
|
391
|
+
}
|
|
392
|
+
networkInterface(ifaceId, hostDevName, guestMac) {
|
|
393
|
+
return this.call("PUT", `/network-interfaces/${ifaceId}`, {
|
|
394
|
+
iface_id: ifaceId,
|
|
395
|
+
host_dev_name: hostDevName,
|
|
396
|
+
guest_mac: guestMac
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
start() {
|
|
400
|
+
return this.call("PUT", "/actions", { action_type: "InstanceStart" });
|
|
401
|
+
}
|
|
402
|
+
pause() {
|
|
403
|
+
return this.call("PATCH", "/vm", { state: "Paused" });
|
|
404
|
+
}
|
|
405
|
+
resume() {
|
|
406
|
+
return this.call("PATCH", "/vm", { state: "Resumed" });
|
|
407
|
+
}
|
|
408
|
+
snapshotCreate(snapshotPath, memFilePath) {
|
|
409
|
+
return this.call("PUT", "/snapshot/create", {
|
|
410
|
+
snapshot_type: "Full",
|
|
411
|
+
snapshot_path: snapshotPath,
|
|
412
|
+
mem_file_path: memFilePath
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
snapshotLoad(snapshotPath, memFilePath, resume = true) {
|
|
416
|
+
return this.call("PUT", "/snapshot/load", {
|
|
417
|
+
snapshot_path: snapshotPath,
|
|
418
|
+
mem_backend: { backend_type: "File", backend_path: memFilePath },
|
|
419
|
+
resume_vm: resume
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
info() {
|
|
423
|
+
return this.call("GET", "/");
|
|
424
|
+
}
|
|
425
|
+
async close() {
|
|
426
|
+
await this.agent.close();
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
// src/core/machines.ts
|
|
431
|
+
var NAME_RE = /^[a-z0-9][a-z0-9-]{0,11}$/;
|
|
432
|
+
var AUTO_PREFIX = "box";
|
|
433
|
+
function toMachine(meta) {
|
|
434
|
+
const alive = pidAlive(meta.pid);
|
|
435
|
+
return {
|
|
436
|
+
name: meta.name,
|
|
437
|
+
ip: meta.ip,
|
|
438
|
+
mac: meta.mac,
|
|
439
|
+
pid: alive ? meta.pid : null,
|
|
440
|
+
vcpu: meta.vcpu,
|
|
441
|
+
memMib: meta.memMib,
|
|
442
|
+
createdAt: meta.createdAt,
|
|
443
|
+
source: meta.source,
|
|
444
|
+
status: alive ? "running" : "stopped"
|
|
445
|
+
};
|
|
226
446
|
}
|
|
227
|
-
async function
|
|
228
|
-
|
|
447
|
+
async function readAllMeta(p) {
|
|
448
|
+
const metas = [];
|
|
449
|
+
for (const name of await listMachineNames(p)) {
|
|
450
|
+
try {
|
|
451
|
+
metas.push(await readMachineMeta(name, p));
|
|
452
|
+
} catch (err) {
|
|
453
|
+
console.error(`warning: skipping machines/${name}: ${err.message}`);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return metas;
|
|
229
457
|
}
|
|
230
|
-
async function
|
|
231
|
-
|
|
458
|
+
async function listMachines(p = paths()) {
|
|
459
|
+
return (await readAllMeta(p)).map(toMachine);
|
|
460
|
+
}
|
|
461
|
+
async function getMachine(name, p = paths()) {
|
|
462
|
+
try {
|
|
463
|
+
return toMachine(await readMachineMeta(name, p));
|
|
464
|
+
} catch (err) {
|
|
465
|
+
if (err.code === "ENOENT") throw new NotFoundError(`box ${name}`);
|
|
466
|
+
throw err;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
function pickName(requested, taken) {
|
|
470
|
+
if (requested !== void 0) {
|
|
471
|
+
if (!NAME_RE.test(requested)) {
|
|
472
|
+
throw new KvboxError(`invalid box name "${requested}": 1-12 chars, lowercase letters, digits and '-', starting with a letter or digit`);
|
|
473
|
+
}
|
|
474
|
+
if (taken.has(requested)) throw new KvboxError(`box ${requested} already exists`);
|
|
475
|
+
return requested;
|
|
476
|
+
}
|
|
477
|
+
for (let i = 1; ; i++) {
|
|
478
|
+
const candidate = `${AUTO_PREFIX}${i}`;
|
|
479
|
+
if (!taken.has(candidate)) return candidate;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
function bootArgs(name, ip, cfg, pubkeyB64) {
|
|
483
|
+
const args = `console=ttyS0 reboot=k panic=1 ip=${ip}::${cfg.gateway}:255.255.255.0:${name}:eth0:off kvbox.name=${name} kvbox.sshkey=${pubkeyB64}`;
|
|
484
|
+
return process.arch === "arm64" ? `keep_bootcon ${args}` : args;
|
|
485
|
+
}
|
|
486
|
+
async function waitFor(pred, timeoutMs, stepMs = 50) {
|
|
487
|
+
const deadline = Date.now() + timeoutMs;
|
|
488
|
+
while (Date.now() < deadline) {
|
|
489
|
+
if (pred()) return true;
|
|
490
|
+
await sleep(stepMs);
|
|
491
|
+
}
|
|
492
|
+
return pred();
|
|
493
|
+
}
|
|
494
|
+
async function tailOf(file, lines = 5) {
|
|
495
|
+
try {
|
|
496
|
+
return (await readFile3(file, "utf8")).trim().split("\n").slice(-lines).join("\n");
|
|
497
|
+
} catch {
|
|
498
|
+
return "";
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
async function createMachine(req, p = paths()) {
|
|
502
|
+
if (req.from !== void 0) throw new NotImplementedError("new --from <snapshot> (#13)");
|
|
503
|
+
const cfg = await loadConfig(p);
|
|
504
|
+
const sizeName = req.size ?? cfg.defaultSize;
|
|
505
|
+
if (!isSizeName(sizeName)) throw new KvboxError(`unknown size "${sizeName}"; use one of ${Object.keys(SIZES).join(", ")}`);
|
|
506
|
+
const size = SIZES[sizeName];
|
|
507
|
+
for (const [what, file] of [["kernel", cfg.kernel], ["base image", cfg.rootfs], ["firecracker", cfg.firecracker]]) {
|
|
508
|
+
if (!existsSync2(file)) throw new KvboxError(`${what} missing at ${file}; run install-server.sh`);
|
|
509
|
+
}
|
|
510
|
+
let pubkeyB64;
|
|
511
|
+
try {
|
|
512
|
+
pubkeyB64 = Buffer.from((await readFile3(`${p.sshKey}.pub`, "utf8")).trim()).toString("base64");
|
|
513
|
+
} catch {
|
|
514
|
+
throw new KvboxError(`SSH key missing at ${p.sshKey}.pub; run install-server.sh`);
|
|
515
|
+
}
|
|
516
|
+
const existing = await readAllMeta(p);
|
|
517
|
+
const name = pickName(req.name, new Set(existing.map((m2) => m2.name)));
|
|
518
|
+
const ip = nextFreeIp(cfg.subnet, [...existing.map((m2) => m2.ip), cfg.gateway]);
|
|
519
|
+
const mac = macFor(ip);
|
|
520
|
+
const m = p.machine(name);
|
|
521
|
+
await ensureHostNetwork(cfg);
|
|
522
|
+
await mkdir2(m.dir, { recursive: true, mode: 448 });
|
|
523
|
+
let tapCreated = false;
|
|
524
|
+
let fcPid;
|
|
525
|
+
try {
|
|
526
|
+
await run(["cp", "--reflink=auto", "--sparse=always", cfg.rootfs, m.disk], { timeoutMs: 12e4 });
|
|
527
|
+
const tap = await createTap(name, cfg);
|
|
528
|
+
tapCreated = true;
|
|
529
|
+
await rm(m.sock, { force: true });
|
|
530
|
+
const logFd = openSync(m.log, "a");
|
|
531
|
+
let child;
|
|
532
|
+
try {
|
|
533
|
+
child = spawn(cfg.firecracker, ["--api-sock", m.sock, "--id", name, "--level", "Error"], {
|
|
534
|
+
detached: true,
|
|
535
|
+
stdio: ["ignore", logFd, logFd]
|
|
536
|
+
});
|
|
537
|
+
} finally {
|
|
538
|
+
closeSync(logFd);
|
|
539
|
+
}
|
|
540
|
+
child.unref();
|
|
541
|
+
fcPid = child.pid;
|
|
542
|
+
if (fcPid === void 0) throw new KvboxError("could not start firecracker");
|
|
543
|
+
if (!await waitFor(() => existsSync2(m.sock), 3e3)) {
|
|
544
|
+
throw new KvboxError(`firecracker did not open its API socket; last log lines:
|
|
545
|
+
${await tailOf(m.log)}`);
|
|
546
|
+
}
|
|
547
|
+
const fc = new FirecrackerClient(m.sock);
|
|
548
|
+
try {
|
|
549
|
+
await fc.bootSource(cfg.kernel, bootArgs(name, ip, cfg, pubkeyB64));
|
|
550
|
+
await fc.rootDrive(m.disk);
|
|
551
|
+
await fc.machineConfig(size.vcpu, size.memMib);
|
|
552
|
+
await fc.networkInterface("eth0", tap, mac);
|
|
553
|
+
await fc.start();
|
|
554
|
+
} finally {
|
|
555
|
+
await fc.close();
|
|
556
|
+
}
|
|
557
|
+
const meta = {
|
|
558
|
+
name,
|
|
559
|
+
ip,
|
|
560
|
+
mac,
|
|
561
|
+
pid: fcPid,
|
|
562
|
+
vcpu: size.vcpu,
|
|
563
|
+
memMib: size.memMib,
|
|
564
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
565
|
+
source: "base"
|
|
566
|
+
};
|
|
567
|
+
await writeMachineMeta(meta, p);
|
|
568
|
+
return toMachine(meta);
|
|
569
|
+
} catch (err) {
|
|
570
|
+
if (fcPid !== void 0) {
|
|
571
|
+
try {
|
|
572
|
+
process.kill(fcPid, "SIGKILL");
|
|
573
|
+
} catch {
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
if (tapCreated) await deleteTap(name, cfg).catch(() => void 0);
|
|
577
|
+
await rm(m.dir, { recursive: true, force: true });
|
|
578
|
+
throw err;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
async function destroyMachine(name, p = paths()) {
|
|
582
|
+
const machine = await getMachine(name, p);
|
|
583
|
+
const cfg = await loadConfig(p);
|
|
584
|
+
if (machine.pid !== null) {
|
|
585
|
+
process.kill(machine.pid, "SIGTERM");
|
|
586
|
+
if (!await waitFor(() => !pidAlive(machine.pid), 2e3, 100)) {
|
|
587
|
+
try {
|
|
588
|
+
process.kill(machine.pid, "SIGKILL");
|
|
589
|
+
} catch {
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
await deleteTap(name, cfg);
|
|
594
|
+
await rm(p.machine(name).dir, { recursive: true, force: true });
|
|
232
595
|
}
|
|
233
596
|
|
|
234
597
|
// src/routes/machines.ts
|
|
@@ -284,6 +647,7 @@ function createApp() {
|
|
|
284
647
|
app.route("/capacity", capacityRoutes());
|
|
285
648
|
app.notFound((c) => c.json({ error: `no route: ${c.req.method} ${c.req.path}` }, 404));
|
|
286
649
|
app.onError((err, c) => {
|
|
650
|
+
if (err instanceof NotFoundError) return c.json({ error: err.message }, 404);
|
|
287
651
|
if (err instanceof KvboxError) {
|
|
288
652
|
return c.json({ error: err.message }, err.exitCode === 2 ? 501 : 400);
|
|
289
653
|
}
|
|
@@ -301,7 +665,7 @@ async function listen(opts = {}) {
|
|
|
301
665
|
if (Buffer.byteLength(socketPath) > 100) {
|
|
302
666
|
throw new KvboxError(`socket path too long for a Unix socket (${socketPath}); set KVBOX_HOME to a shorter path`);
|
|
303
667
|
}
|
|
304
|
-
await
|
|
668
|
+
await rm2(socketPath, { force: true });
|
|
305
669
|
const server = createAdaptorServer({ fetch: createApp().fetch });
|
|
306
670
|
await new Promise((resolve, reject) => {
|
|
307
671
|
server.once("error", reject);
|
|
@@ -311,8 +675,8 @@ async function listen(opts = {}) {
|
|
|
311
675
|
`);
|
|
312
676
|
const shutdown = async () => {
|
|
313
677
|
server.close();
|
|
314
|
-
await
|
|
315
|
-
await
|
|
678
|
+
await rm2(socketPath, { force: true });
|
|
679
|
+
await rm2(p.serverPid, { force: true });
|
|
316
680
|
process.exit(0);
|
|
317
681
|
};
|
|
318
682
|
process.once("SIGINT", () => void shutdown());
|
|
@@ -325,6 +689,22 @@ var program = new Command("kvbox-server").description("kvbox host daemon: owns F
|
|
|
325
689
|
program.command("start", { isDefault: true }).description("serve the API on the local Unix socket (foreground)").option("--socket <path>", "socket to listen on (default: $KVBOX_HOME/server.sock)").action(async (opts) => {
|
|
326
690
|
const { socketPath } = await listen(opts.socket ? { socketPath: opts.socket } : {});
|
|
327
691
|
console.log(`kvbox-server ${VERSION} listening on ${socketPath} (pid ${process.pid})`);
|
|
692
|
+
try {
|
|
693
|
+
const cfg = await loadConfig(paths());
|
|
694
|
+
const state = await ensureHostNetwork(cfg);
|
|
695
|
+
console.log(`host network: bridge ${cfg.bridge} ${state}`);
|
|
696
|
+
} catch (err) {
|
|
697
|
+
console.error(`warning: host network not ready (${err.message}); run: kvbox-server net up`);
|
|
698
|
+
}
|
|
699
|
+
});
|
|
700
|
+
program.command("net <action>").description("host network: up | down | status (bridge, forwarding, NAT; uses sudo)").action(async (action) => {
|
|
701
|
+
if (!["up", "down", "status"].includes(action)) throw new KvboxError("net: expected up, down or status");
|
|
702
|
+
const cfg = await loadConfig(paths());
|
|
703
|
+
const res = spawnSync(netSetupScript(), [action], {
|
|
704
|
+
stdio: "inherit",
|
|
705
|
+
env: { ...process.env, KVBOX_BRIDGE: cfg.bridge, KVBOX_SUBNET: cfg.subnet, KVBOX_GATEWAY: cfg.gateway }
|
|
706
|
+
});
|
|
707
|
+
process.exit(res.status ?? 1);
|
|
328
708
|
});
|
|
329
709
|
try {
|
|
330
710
|
await program.parseAsync(process.argv);
|
package/dist/kvbox-server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../../shared/src/errors.ts","../src/listen.ts","../src/core/state.ts","../../shared/src/paths.ts","../src/core/config.ts","../../shared/src/sizes.ts","../src/app.ts","../../shared/src/version.ts","../src/version.ts","../src/routes/capacity.ts","../src/core/capacity.ts","../src/routes/machines.ts","../../shared/src/api.ts","../src/core/machines.ts","../src/routes/snapshots.ts","../src/core/snapshots.ts"],"sourcesContent":["/**\n * kvbox-server: the host daemon. `kvbox-server` (or `kvbox-server start`) serves\n * the kvbox API on ~/.kvbox/server.sock in the foreground; the CLI's\n * `kvbox server start` spawns it detached. M2 adds `register`.\n */\nimport { Command } from \"commander\";\nimport { KvboxError } from \"@kvbox/shared/errors\";\nimport { listen } from \"./listen.js\";\nimport { VERSION } from \"./version.js\";\n\nconst program = new Command(\"kvbox-server\")\n .description(\"kvbox host daemon: owns Firecracker and ~/.kvbox, serves the kvbox API\")\n .version(VERSION, \"-v, --version\")\n .showHelpAfterError();\n\nprogram\n .command(\"start\", { isDefault: true })\n .description(\"serve the API on the local Unix socket (foreground)\")\n .option(\"--socket <path>\", \"socket to listen on (default: $KVBOX_HOME/server.sock)\")\n .action(async (opts: { socket?: string }) => {\n const { socketPath } = await listen(opts.socket ? { socketPath: opts.socket } : {});\n console.log(`kvbox-server ${VERSION} listening on ${socketPath} (pid ${process.pid})`);\n });\n\ntry {\n await program.parseAsync(process.argv);\n} catch (err) {\n if (err instanceof KvboxError) {\n console.error(`error: ${err.message}`);\n process.exit(err.exitCode);\n }\n throw err;\n}\n","/** Errors with an exit code; the entry point prints `message` and exits with `exitCode`. */\nexport class KvboxError extends Error {\n constructor(\n message: string,\n public readonly exitCode: number = 1,\n ) {\n super(message);\n this.name = \"KvboxError\";\n }\n}\n\nexport class NotImplementedError extends KvboxError {\n constructor(what: string) {\n super(`${what}: not implemented yet`, 2);\n this.name = \"NotImplementedError\";\n }\n}\n","/** Bind the app to the local Unix socket and write the pid file. */\nimport { rm, writeFile } from \"node:fs/promises\";\nimport { createAdaptorServer } from \"@hono/node-server\";\nimport { ensureLayout } from \"./core/state.js\";\nimport { loadConfig } from \"./core/config.js\";\nimport { KvboxError } from \"@kvbox/shared/errors\";\nimport { paths } from \"@kvbox/shared/paths\";\nimport { createApp } from \"./app.js\";\n\nexport interface ListenOptions {\n socketPath?: string;\n}\n\nexport async function listen(opts: ListenOptions = {}) {\n const p = await ensureLayout(paths());\n await loadConfig(p);\n const socketPath = opts.socketPath ?? p.serverSock;\n // Linux limits a Unix socket path (sun_path) to 108 bytes; bind() fails with EINVAL past it.\n if (Buffer.byteLength(socketPath) > 100) {\n throw new KvboxError(`socket path too long for a Unix socket (${socketPath}); set KVBOX_HOME to a shorter path`);\n }\n\n // A stale socket file from a crashed server blocks bind(); the pid file tells\n // `server status` whether anything is actually alive.\n await rm(socketPath, { force: true });\n\n const server = createAdaptorServer({ fetch: createApp().fetch });\n await new Promise<void>((resolve, reject) => {\n server.once(\"error\", reject);\n server.listen(socketPath, () => resolve());\n });\n await writeFile(p.serverPid, `${process.pid}\\n`);\n\n const shutdown = async () => {\n server.close();\n await rm(socketPath, { force: true });\n await rm(p.serverPid, { force: true });\n process.exit(0);\n };\n process.once(\"SIGINT\", () => void shutdown());\n process.once(\"SIGTERM\", () => void shutdown());\n\n return { socketPath, server };\n}\n","/**\n * The `~/.kvbox` layout and the per-box / per-snapshot `meta.json` files.\n * The server is the only writer; every read is validated with zod.\n */\nimport { mkdir, readFile, readdir, writeFile } from \"node:fs/promises\";\nimport { z } from \"zod\";\nimport { paths, type Paths } from \"@kvbox/shared/paths\";\n\nexport const MachineMetaSchema = z.object({\n name: z.string(),\n ip: z.string(),\n mac: z.string(),\n pid: z.number().int(),\n vcpu: z.number().int(),\n memMib: z.number().int(),\n createdAt: z.string(),\n source: z.string(),\n});\nexport type MachineMeta = z.infer<typeof MachineMetaSchema>;\n\nexport const SnapshotMetaSchema = z.object({\n name: z.string(),\n source: z.string(),\n vcpu: z.number().int(),\n memMib: z.number().int(),\n createdAt: z.string(),\n});\nexport type SnapshotMeta = z.infer<typeof SnapshotMetaSchema>;\n\n/** Create every directory of the layout. Idempotent; called by `kvbox server start`. */\nexport async function ensureLayout(p: Paths = paths()): Promise<Paths> {\n for (const dir of [p.home, p.ssh, p.bin, p.assets, p.machines, p.snapshots]) {\n await mkdir(dir, { recursive: true, mode: 0o700 });\n }\n return p;\n}\n\nasync function readJson<T>(file: string, schema: z.ZodType<T>): Promise<T> {\n return schema.parse(JSON.parse(await readFile(file, \"utf8\")));\n}\n\nasync function listDirs(dir: string): Promise<string[]> {\n try {\n const entries = await readdir(dir, { withFileTypes: true });\n return entries.filter((e) => e.isDirectory()).map((e) => e.name);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return [];\n throw err;\n }\n}\n\nexport async function readMachineMeta(name: string, p: Paths = paths()) {\n return readJson(p.machine(name).meta, MachineMetaSchema);\n}\n\nexport async function writeMachineMeta(meta: MachineMeta, p: Paths = paths()) {\n await mkdir(p.machine(meta.name).dir, { recursive: true });\n await writeFile(p.machine(meta.name).meta, JSON.stringify(meta, null, 2) + \"\\n\");\n}\n\nexport async function listMachineNames(p: Paths = paths()) {\n return listDirs(p.machines);\n}\n\nexport async function readSnapshotMeta(name: string, p: Paths = paths()) {\n return readJson(p.snapshot(name).meta, SnapshotMetaSchema);\n}\n\nexport async function writeSnapshotMeta(meta: SnapshotMeta, p: Paths = paths()) {\n await mkdir(p.snapshot(meta.name).dir, { recursive: true });\n await writeFile(p.snapshot(meta.name).meta, JSON.stringify(meta, null, 2) + \"\\n\");\n}\n\nexport async function listSnapshotNames(p: Paths = paths()) {\n return listDirs(p.snapshots);\n}\n\n/** True when a pid is alive (signal 0). */\nexport function pidAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (err) {\n return (err as NodeJS.ErrnoException).code === \"EPERM\";\n }\n}\n","/**\n * Host state layout. The filesystem is the database and `~/.kvbox` is its root.\n * This module is dependency-free so both the CLI (socket path, hosts.json)\n * and core (everything else) can use it without crossing the layer boundary.\n *\n * `KVBOX_HOME` overrides the root, mainly for tests.\n */\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\nexport function kvboxHome(): string {\n return process.env[\"KVBOX_HOME\"] ?? join(homedir(), \".kvbox\");\n}\n\nexport function paths(home: string = kvboxHome()) {\n return {\n home,\n config: join(home, \"config.json\"),\n serverSock: join(home, \"server.sock\"),\n serverPid: join(home, \"server.pid\"),\n serverLog: join(home, \"server.log\"),\n hosts: join(home, \"hosts.json\"),\n ssh: join(home, \"ssh\"),\n sshKey: join(home, \"ssh\", \"id_ed25519\"),\n bin: join(home, \"bin\"),\n firecracker: join(home, \"bin\", \"firecracker\"),\n assets: join(home, \"assets\"),\n kernel: join(home, \"assets\", \"vmlinux\"),\n rootfs: join(home, \"assets\", \"rootfs.ext4\"),\n machines: join(home, \"machines\"),\n snapshots: join(home, \"snapshots\"),\n machine: (name: string) => ({\n dir: join(home, \"machines\", name),\n meta: join(home, \"machines\", name, \"meta.json\"),\n disk: join(home, \"machines\", name, \"disk.ext4\"),\n sock: join(home, \"machines\", name, \"fc.sock\"),\n log: join(home, \"machines\", name, \"fc.log\"),\n }),\n snapshot: (name: string) => ({\n dir: join(home, \"snapshots\", name),\n meta: join(home, \"snapshots\", name, \"meta.json\"),\n vmstate: join(home, \"snapshots\", name, \"vmstate\"),\n mem: join(home, \"snapshots\", name, \"mem\"),\n disk: join(home, \"snapshots\", name, \"disk.ext4\"),\n }),\n } as const;\n}\n\nexport type Paths = ReturnType<typeof paths>;\n","/** `~/.kvbox/config.json`: asset paths, subnet, default size. Written with defaults on first run. */\nimport { readFile, writeFile } from \"node:fs/promises\";\nimport { z } from \"zod\";\nimport { paths, type Paths } from \"@kvbox/shared/paths\";\nimport { DEFAULT_SIZE, SIZE_NAMES } from \"@kvbox/shared/sizes\";\n\nexport const ConfigSchema = z.object({\n kernel: z.string(),\n rootfs: z.string(),\n firecracker: z.string(),\n bridge: z.string().default(\"zbr0\"),\n subnet: z.string().default(\"172.30.0.0/24\"),\n gateway: z.string().default(\"172.30.0.1\"),\n defaultSize: z.enum(SIZE_NAMES as [string, ...string[]]).default(DEFAULT_SIZE),\n});\nexport type Config = z.infer<typeof ConfigSchema>;\n\nexport function defaultConfig(p: Paths = paths()): Config {\n return ConfigSchema.parse({\n kernel: p.kernel,\n rootfs: p.rootfs,\n firecracker: p.firecracker,\n });\n}\n\nexport async function loadConfig(p: Paths = paths()): Promise<Config> {\n try {\n const raw = await readFile(p.config, \"utf8\");\n return ConfigSchema.parse(JSON.parse(raw));\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n const cfg = defaultConfig(p);\n await writeFile(p.config, JSON.stringify(cfg, null, 2) + \"\\n\");\n return cfg;\n }\n throw err;\n }\n}\n","/** Fixed box sizes. Firecracker backs guest memory lazily, so a box only costs what the guest touches. */\nexport const SIZES = {\n s: { vcpu: 2, memMib: 4096 },\n m: { vcpu: 4, memMib: 8192 },\n l: { vcpu: 8, memMib: 16384 },\n} as const;\n\nexport type SizeName = keyof typeof SIZES;\nexport const SIZE_NAMES = Object.keys(SIZES) as SizeName[];\nexport const DEFAULT_SIZE: SizeName = \"s\";\n\nexport function isSizeName(value: string): value is SizeName {\n return value in SIZES;\n}\n","/**\n * The hono app behind `kvbox-server`. Served over `~/.kvbox/server.sock`\n * in M1; the same app goes behind HTTPS in M2.\n */\nimport { Hono } from \"hono\";\nimport { KvboxError } from \"@kvbox/shared/errors\";\nimport { VERSION } from \"./version.js\";\nimport { capacityRoutes } from \"./routes/capacity.js\";\nimport { machineRoutes } from \"./routes/machines.js\";\nimport { snapshotRoutes } from \"./routes/snapshots.js\";\n\nexport function createApp() {\n const app = new Hono();\n\n app.get(\"/health\", (c) => c.json({ ok: true as const, version: VERSION, pid: process.pid }));\n app.route(\"/machines\", machineRoutes());\n app.route(\"/snapshots\", snapshotRoutes());\n app.route(\"/capacity\", capacityRoutes());\n\n app.notFound((c) => c.json({ error: `no route: ${c.req.method} ${c.req.path}` }, 404));\n app.onError((err, c) => {\n if (err instanceof KvboxError) {\n return c.json({ error: err.message }, err.exitCode === 2 ? 501 : 400);\n }\n console.error(err);\n return c.json({ error: err.message }, 500);\n });\n\n return app;\n}\n\nexport type App = ReturnType<typeof createApp>;\n","/**\n * Each binary's version comes from its own package.json: tsup injects\n * `__KVBOX_VERSION__` at build time; the dev path (tsx) reads the file.\n * Usage in a package: `versionOf(import.meta.url)` from its `src/version.ts`.\n */\nimport { createRequire } from \"node:module\";\n\nexport function versionOf(srcFileUrl: string, injected?: string): string {\n if (typeof injected === \"string\") return injected;\n try {\n const require = createRequire(srcFileUrl);\n return (require(\"../package.json\") as { version: string }).version;\n } catch {\n return \"0.0.0-dev\";\n }\n}\n","import { versionOf } from \"@kvbox/shared/version\";\n\ndeclare const __KVBOX_VERSION__: string | undefined;\n\nexport const VERSION: string = versionOf(import.meta.url, typeof __KVBOX_VERSION__ === \"string\" ? __KVBOX_VERSION__ : undefined);\n","import { Hono } from \"hono\";\nimport { capacity } from \"../core/capacity.js\";\n\nexport function capacityRoutes() {\n const r = new Hono();\n r.get(\"/\", async (c) => c.json(await capacity()));\n return r;\n}\n","/**\n * The capacity guard: `kvbox new` refuses when reserved RAM of running boxes\n * plus the new one exceeds host RAM minus 2 GB (hard), or reserved vCPUs exceed\n * 2x host cores (soft). `--force` skips it.\n */\nimport { NotImplementedError } from \"@kvbox/shared/errors\";\nimport type { Capacity } from \"@kvbox/shared/api\";\n\nexport const MEM_HEADROOM_MIB = 2048;\nexport const VCPU_OVERSUBSCRIPTION = 2;\n\nexport async function capacity(): Promise<Capacity> {\n throw new NotImplementedError(\"capacity.capacity\");\n}\n\nexport async function assertRoomFor(_vcpu: number, _memMib: number, _force: boolean): Promise<void> {\n throw new NotImplementedError(\"capacity.assertRoomFor\");\n}\n","import { Hono } from \"hono\";\nimport { CreateMachineRequest } from \"@kvbox/shared/api\";\nimport { createMachine, destroyMachine, listMachines } from \"../core/machines.js\";\n\nexport function machineRoutes() {\n const r = new Hono();\n r.get(\"/\", async (c) => c.json(await listMachines()));\n r.post(\"/\", async (c) => {\n const body = CreateMachineRequest.parse(await c.req.json().catch(() => ({})));\n return c.json(await createMachine(body), 201);\n });\n r.delete(\"/:name\", async (c) => {\n await destroyMachine(c.req.param(\"name\"));\n return c.body(null, 204);\n });\n return r;\n}\n","/**\n * Wire shapes of the kvbox HTTP API, shared by the client (src/cli) and the\n * server (src/server). Validated with zod on both sides because the client\n * may be talking to an older or newer server.\n */\nimport { z } from \"zod\";\nimport { SIZE_NAMES } from \"./sizes.js\";\n\nexport const SizeSchema = z.enum(SIZE_NAMES as [string, ...string[]]);\n\nexport const MachineSchema = z.object({\n name: z.string(),\n ip: z.string(),\n mac: z.string(),\n pid: z.number().int().nullable(),\n vcpu: z.number().int(),\n memMib: z.number().int(),\n createdAt: z.string(),\n source: z.string(), // \"base\" or \"snapshot:<name>\"\n status: z.enum([\"running\", \"stopped\"]),\n});\nexport type Machine = z.infer<typeof MachineSchema>;\n\nexport const CreateMachineRequest = z.object({\n name: z.string().optional(),\n size: SizeSchema.optional(),\n from: z.string().optional(),\n force: z.boolean().optional(),\n});\nexport type CreateMachineRequest = z.infer<typeof CreateMachineRequest>;\n\nexport const SnapshotSchema = z.object({\n name: z.string(),\n source: z.string(),\n vcpu: z.number().int(),\n memMib: z.number().int(),\n createdAt: z.string(),\n});\nexport type Snapshot = z.infer<typeof SnapshotSchema>;\n\nexport const CreateSnapshotRequest = z.object({\n box: z.string(),\n name: z.string(),\n});\nexport type CreateSnapshotRequest = z.infer<typeof CreateSnapshotRequest>;\n\nexport const CapacitySchema = z.object({\n hostMemMib: z.number().int(),\n hostCores: z.number().int(),\n reservedMemMib: z.number().int(),\n reservedVcpu: z.number().int(),\n running: z.number().int(),\n});\nexport type Capacity = z.infer<typeof CapacitySchema>;\n\nexport const HealthSchema = z.object({\n ok: z.literal(true),\n version: z.string(),\n pid: z.number().int(),\n});\nexport type Health = z.infer<typeof HealthSchema>;\n\nexport const ApiErrorSchema = z.object({\n error: z.string(),\n});\n","/** Create, list and destroy boxes. See architecture \"Process model\". */\nimport { NotImplementedError } from \"@kvbox/shared/errors\";\nimport type { CreateMachineRequest, Machine } from \"@kvbox/shared/api\";\n\nexport async function listMachines(): Promise<Machine[]> {\n throw new NotImplementedError(\"machines.listMachines\");\n}\n\nexport async function createMachine(_req: CreateMachineRequest): Promise<Machine> {\n throw new NotImplementedError(\"machines.createMachine\");\n}\n\nexport async function destroyMachine(_name: string): Promise<void> {\n throw new NotImplementedError(\"machines.destroyMachine\");\n}\n","import { Hono } from \"hono\";\nimport { CreateSnapshotRequest } from \"@kvbox/shared/api\";\nimport { deleteSnapshot, listSnapshots, saveSnapshot } from \"../core/snapshots.js\";\n\nexport function snapshotRoutes() {\n const r = new Hono();\n r.get(\"/\", async (c) => c.json(await listSnapshots()));\n r.post(\"/\", async (c) => {\n const body = CreateSnapshotRequest.parse(await c.req.json());\n return c.json(await saveSnapshot(body), 201);\n });\n r.delete(\"/:name\", async (c) => {\n await deleteSnapshot(c.req.param(\"name\"));\n return c.body(null, 204);\n });\n return r;\n}\n","/** Full snapshots (vmstate + mem + disk copy) and their listing. */\nimport { NotImplementedError } from \"@kvbox/shared/errors\";\nimport type { CreateSnapshotRequest, Snapshot } from \"@kvbox/shared/api\";\n\nexport async function listSnapshots(): Promise<Snapshot[]> {\n throw new NotImplementedError(\"snapshots.listSnapshots\");\n}\n\nexport async function saveSnapshot(_req: CreateSnapshotRequest): Promise<Snapshot> {\n throw new NotImplementedError(\"snapshots.saveSnapshot\");\n}\n\nexport async function deleteSnapshot(_name: string): Promise<void> {\n throw new NotImplementedError(\"snapshots.deleteSnapshot\");\n}\n"],"mappings":";;;AAKA,SAAS,eAAe;;;ACJjB,IAAM,aAAN,cAAyB,MAAM;AAAA,EACpC,YACE,SACgB,WAAmB,GACnC;AACA,UAAM,OAAO;AAFG;AAGhB,SAAK,OAAO;AAAA,EACd;AAAA,EAJkB;AAKpB;AAEO,IAAM,sBAAN,cAAkC,WAAW;AAAA,EAClD,YAAY,MAAc;AACxB,UAAM,GAAG,IAAI,yBAAyB,CAAC;AACvC,SAAK,OAAO;AAAA,EACd;AACF;;;ACfA,SAAS,IAAI,aAAAA,kBAAiB;AAC9B,SAAS,2BAA2B;;;ACEpC,SAAS,OAAO,UAAU,SAAS,iBAAiB;AACpD,SAAS,SAAS;;;ACElB,SAAS,eAAe;AACxB,SAAS,YAAY;AAEd,SAAS,YAAoB;AAClC,SAAO,QAAQ,IAAI,YAAY,KAAK,KAAK,QAAQ,GAAG,QAAQ;AAC9D;AAEO,SAAS,MAAM,OAAe,UAAU,GAAG;AAChD,SAAO;AAAA,IACL;AAAA,IACA,QAAQ,KAAK,MAAM,aAAa;AAAA,IAChC,YAAY,KAAK,MAAM,aAAa;AAAA,IACpC,WAAW,KAAK,MAAM,YAAY;AAAA,IAClC,WAAW,KAAK,MAAM,YAAY;AAAA,IAClC,OAAO,KAAK,MAAM,YAAY;AAAA,IAC9B,KAAK,KAAK,MAAM,KAAK;AAAA,IACrB,QAAQ,KAAK,MAAM,OAAO,YAAY;AAAA,IACtC,KAAK,KAAK,MAAM,KAAK;AAAA,IACrB,aAAa,KAAK,MAAM,OAAO,aAAa;AAAA,IAC5C,QAAQ,KAAK,MAAM,QAAQ;AAAA,IAC3B,QAAQ,KAAK,MAAM,UAAU,SAAS;AAAA,IACtC,QAAQ,KAAK,MAAM,UAAU,aAAa;AAAA,IAC1C,UAAU,KAAK,MAAM,UAAU;AAAA,IAC/B,WAAW,KAAK,MAAM,WAAW;AAAA,IACjC,SAAS,CAAC,UAAkB;AAAA,MAC1B,KAAK,KAAK,MAAM,YAAY,IAAI;AAAA,MAChC,MAAM,KAAK,MAAM,YAAY,MAAM,WAAW;AAAA,MAC9C,MAAM,KAAK,MAAM,YAAY,MAAM,WAAW;AAAA,MAC9C,MAAM,KAAK,MAAM,YAAY,MAAM,SAAS;AAAA,MAC5C,KAAK,KAAK,MAAM,YAAY,MAAM,QAAQ;AAAA,IAC5C;AAAA,IACA,UAAU,CAAC,UAAkB;AAAA,MAC3B,KAAK,KAAK,MAAM,aAAa,IAAI;AAAA,MACjC,MAAM,KAAK,MAAM,aAAa,MAAM,WAAW;AAAA,MAC/C,SAAS,KAAK,MAAM,aAAa,MAAM,SAAS;AAAA,MAChD,KAAK,KAAK,MAAM,aAAa,MAAM,KAAK;AAAA,MACxC,MAAM,KAAK,MAAM,aAAa,MAAM,WAAW;AAAA,IACjD;AAAA,EACF;AACF;;;ADtCO,IAAM,oBAAoB,EAAE,OAAO;AAAA,EACxC,MAAM,EAAE,OAAO;AAAA,EACf,IAAI,EAAE,OAAO;AAAA,EACb,KAAK,EAAE,OAAO;AAAA,EACd,KAAK,EAAE,OAAO,EAAE,IAAI;AAAA,EACpB,MAAM,EAAE,OAAO,EAAE,IAAI;AAAA,EACrB,QAAQ,EAAE,OAAO,EAAE,IAAI;AAAA,EACvB,WAAW,EAAE,OAAO;AAAA,EACpB,QAAQ,EAAE,OAAO;AACnB,CAAC;AAGM,IAAM,qBAAqB,EAAE,OAAO;AAAA,EACzC,MAAM,EAAE,OAAO;AAAA,EACf,QAAQ,EAAE,OAAO;AAAA,EACjB,MAAM,EAAE,OAAO,EAAE,IAAI;AAAA,EACrB,QAAQ,EAAE,OAAO,EAAE,IAAI;AAAA,EACvB,WAAW,EAAE,OAAO;AACtB,CAAC;AAID,eAAsB,aAAa,IAAW,MAAM,GAAmB;AACrE,aAAW,OAAO,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,SAAS,GAAG;AAC3E,UAAM,MAAM,KAAK,EAAE,WAAW,MAAM,MAAM,IAAM,CAAC;AAAA,EACnD;AACA,SAAO;AACT;;;AElCA,SAAS,YAAAC,WAAU,aAAAC,kBAAiB;AACpC,SAAS,KAAAC,UAAS;;;ACDX,IAAM,QAAQ;AAAA,EACnB,GAAG,EAAE,MAAM,GAAG,QAAQ,KAAK;AAAA,EAC3B,GAAG,EAAE,MAAM,GAAG,QAAQ,KAAK;AAAA,EAC3B,GAAG,EAAE,MAAM,GAAG,QAAQ,MAAM;AAC9B;AAGO,IAAM,aAAa,OAAO,KAAK,KAAK;AACpC,IAAM,eAAyB;;;ADH/B,IAAM,eAAeC,GAAE,OAAO;AAAA,EACnC,QAAQA,GAAE,OAAO;AAAA,EACjB,QAAQA,GAAE,OAAO;AAAA,EACjB,aAAaA,GAAE,OAAO;AAAA,EACtB,QAAQA,GAAE,OAAO,EAAE,QAAQ,MAAM;AAAA,EACjC,QAAQA,GAAE,OAAO,EAAE,QAAQ,eAAe;AAAA,EAC1C,SAASA,GAAE,OAAO,EAAE,QAAQ,YAAY;AAAA,EACxC,aAAaA,GAAE,KAAK,UAAmC,EAAE,QAAQ,YAAY;AAC/E,CAAC;AAGM,SAAS,cAAc,IAAW,MAAM,GAAW;AACxD,SAAO,aAAa,MAAM;AAAA,IACxB,QAAQ,EAAE;AAAA,IACV,QAAQ,EAAE;AAAA,IACV,aAAa,EAAE;AAAA,EACjB,CAAC;AACH;AAEA,eAAsB,WAAW,IAAW,MAAM,GAAoB;AACpE,MAAI;AACF,UAAM,MAAM,MAAMC,UAAS,EAAE,QAAQ,MAAM;AAC3C,WAAO,aAAa,MAAM,KAAK,MAAM,GAAG,CAAC;AAAA,EAC3C,SAAS,KAAK;AACZ,QAAK,IAA8B,SAAS,UAAU;AACpD,YAAM,MAAM,cAAc,CAAC;AAC3B,YAAMC,WAAU,EAAE,QAAQ,KAAK,UAAU,KAAK,MAAM,CAAC,IAAI,IAAI;AAC7D,aAAO;AAAA,IACT;AACA,UAAM;AAAA,EACR;AACF;;;AEjCA,SAAS,QAAAC,aAAY;;;ACCrB,SAAS,qBAAqB;AAEvB,SAAS,UAAU,YAAoB,UAA2B;AACvE,MAAI,OAAO,aAAa,SAAU,QAAO;AACzC,MAAI;AACF,UAAMC,WAAU,cAAc,UAAU;AACxC,WAAQA,SAAQ,iBAAiB,EAA0B;AAAA,EAC7D,QAAQ;AACN,WAAO;AAAA,EACT;AACF;;;ACXO,IAAM,UAAkB,UAAU,YAAY,KAAK,OAAwC,UAAoB,MAAS;;;ACJ/H,SAAS,YAAY;;;ACWrB,eAAsB,WAA8B;AAClD,QAAM,IAAI,oBAAoB,mBAAmB;AACnD;;;ADVO,SAAS,iBAAiB;AAC/B,QAAM,IAAI,IAAI,KAAK;AACnB,IAAE,IAAI,KAAK,OAAO,MAAM,EAAE,KAAK,MAAM,SAAS,CAAC,CAAC;AAChD,SAAO;AACT;;;AEPA,SAAS,QAAAC,aAAY;;;ACKrB,SAAS,KAAAC,UAAS;AAGX,IAAM,aAAaC,GAAE,KAAK,UAAmC;AAE7D,IAAM,gBAAgBA,GAAE,OAAO;AAAA,EACpC,MAAMA,GAAE,OAAO;AAAA,EACf,IAAIA,GAAE,OAAO;AAAA,EACb,KAAKA,GAAE,OAAO;AAAA,EACd,KAAKA,GAAE,OAAO,EAAE,IAAI,EAAE,SAAS;AAAA,EAC/B,MAAMA,GAAE,OAAO,EAAE,IAAI;AAAA,EACrB,QAAQA,GAAE,OAAO,EAAE,IAAI;AAAA,EACvB,WAAWA,GAAE,OAAO;AAAA,EACpB,QAAQA,GAAE,OAAO;AAAA;AAAA,EACjB,QAAQA,GAAE,KAAK,CAAC,WAAW,SAAS,CAAC;AACvC,CAAC;AAGM,IAAM,uBAAuBA,GAAE,OAAO;AAAA,EAC3C,MAAMA,GAAE,OAAO,EAAE,SAAS;AAAA,EAC1B,MAAM,WAAW,SAAS;AAAA,EAC1B,MAAMA,GAAE,OAAO,EAAE,SAAS;AAAA,EAC1B,OAAOA,GAAE,QAAQ,EAAE,SAAS;AAC9B,CAAC;AAGM,IAAM,iBAAiBA,GAAE,OAAO;AAAA,EACrC,MAAMA,GAAE,OAAO;AAAA,EACf,QAAQA,GAAE,OAAO;AAAA,EACjB,MAAMA,GAAE,OAAO,EAAE,IAAI;AAAA,EACrB,QAAQA,GAAE,OAAO,EAAE,IAAI;AAAA,EACvB,WAAWA,GAAE,OAAO;AACtB,CAAC;AAGM,IAAM,wBAAwBA,GAAE,OAAO;AAAA,EAC5C,KAAKA,GAAE,OAAO;AAAA,EACd,MAAMA,GAAE,OAAO;AACjB,CAAC;AAGM,IAAM,iBAAiBA,GAAE,OAAO;AAAA,EACrC,YAAYA,GAAE,OAAO,EAAE,IAAI;AAAA,EAC3B,WAAWA,GAAE,OAAO,EAAE,IAAI;AAAA,EAC1B,gBAAgBA,GAAE,OAAO,EAAE,IAAI;AAAA,EAC/B,cAAcA,GAAE,OAAO,EAAE,IAAI;AAAA,EAC7B,SAASA,GAAE,OAAO,EAAE,IAAI;AAC1B,CAAC;AAGM,IAAM,eAAeA,GAAE,OAAO;AAAA,EACnC,IAAIA,GAAE,QAAQ,IAAI;AAAA,EAClB,SAASA,GAAE,OAAO;AAAA,EAClB,KAAKA,GAAE,OAAO,EAAE,IAAI;AACtB,CAAC;AAGM,IAAM,iBAAiBA,GAAE,OAAO;AAAA,EACrC,OAAOA,GAAE,OAAO;AAClB,CAAC;;;AC5DD,eAAsB,eAAmC;AACvD,QAAM,IAAI,oBAAoB,uBAAuB;AACvD;AAEA,eAAsB,cAAc,MAA8C;AAChF,QAAM,IAAI,oBAAoB,wBAAwB;AACxD;AAEA,eAAsB,eAAe,OAA8B;AACjE,QAAM,IAAI,oBAAoB,yBAAyB;AACzD;;;AFVO,SAAS,gBAAgB;AAC9B,QAAM,IAAI,IAAIC,MAAK;AACnB,IAAE,IAAI,KAAK,OAAO,MAAM,EAAE,KAAK,MAAM,aAAa,CAAC,CAAC;AACpD,IAAE,KAAK,KAAK,OAAO,MAAM;AACvB,UAAM,OAAO,qBAAqB,MAAM,MAAM,EAAE,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC,EAAE,CAAC;AAC5E,WAAO,EAAE,KAAK,MAAM,cAAc,IAAI,GAAG,GAAG;AAAA,EAC9C,CAAC;AACD,IAAE,OAAO,UAAU,OAAO,MAAM;AAC9B,UAAM,eAAe,EAAE,IAAI,MAAM,MAAM,CAAC;AACxC,WAAO,EAAE,KAAK,MAAM,GAAG;AAAA,EACzB,CAAC;AACD,SAAO;AACT;;;AGhBA,SAAS,QAAAC,aAAY;;;ACIrB,eAAsB,gBAAqC;AACzD,QAAM,IAAI,oBAAoB,yBAAyB;AACzD;AAEA,eAAsB,aAAa,MAAgD;AACjF,QAAM,IAAI,oBAAoB,wBAAwB;AACxD;AAEA,eAAsB,eAAe,OAA8B;AACjE,QAAM,IAAI,oBAAoB,0BAA0B;AAC1D;;;ADVO,SAAS,iBAAiB;AAC/B,QAAM,IAAI,IAAIC,MAAK;AACnB,IAAE,IAAI,KAAK,OAAO,MAAM,EAAE,KAAK,MAAM,cAAc,CAAC,CAAC;AACrD,IAAE,KAAK,KAAK,OAAO,MAAM;AACvB,UAAM,OAAO,sBAAsB,MAAM,MAAM,EAAE,IAAI,KAAK,CAAC;AAC3D,WAAO,EAAE,KAAK,MAAM,aAAa,IAAI,GAAG,GAAG;AAAA,EAC7C,CAAC;AACD,IAAE,OAAO,UAAU,OAAO,MAAM;AAC9B,UAAM,eAAe,EAAE,IAAI,MAAM,MAAM,CAAC;AACxC,WAAO,EAAE,KAAK,MAAM,GAAG;AAAA,EACzB,CAAC;AACD,SAAO;AACT;;;ARLO,SAAS,YAAY;AAC1B,QAAM,MAAM,IAAIC,MAAK;AAErB,MAAI,IAAI,WAAW,CAAC,MAAM,EAAE,KAAK,EAAE,IAAI,MAAe,SAAS,SAAS,KAAK,QAAQ,IAAI,CAAC,CAAC;AAC3F,MAAI,MAAM,aAAa,cAAc,CAAC;AACtC,MAAI,MAAM,cAAc,eAAe,CAAC;AACxC,MAAI,MAAM,aAAa,eAAe,CAAC;AAEvC,MAAI,SAAS,CAAC,MAAM,EAAE,KAAK,EAAE,OAAO,aAAa,EAAE,IAAI,MAAM,IAAI,EAAE,IAAI,IAAI,GAAG,GAAG,GAAG,CAAC;AACrF,MAAI,QAAQ,CAAC,KAAK,MAAM;AACtB,QAAI,eAAe,YAAY;AAC7B,aAAO,EAAE,KAAK,EAAE,OAAO,IAAI,QAAQ,GAAG,IAAI,aAAa,IAAI,MAAM,GAAG;AAAA,IACtE;AACA,YAAQ,MAAM,GAAG;AACjB,WAAO,EAAE,KAAK,EAAE,OAAO,IAAI,QAAQ,GAAG,GAAG;AAAA,EAC3C,CAAC;AAED,SAAO;AACT;;;ALhBA,eAAsB,OAAO,OAAsB,CAAC,GAAG;AACrD,QAAM,IAAI,MAAM,aAAa,MAAM,CAAC;AACpC,QAAM,WAAW,CAAC;AAClB,QAAM,aAAa,KAAK,cAAc,EAAE;AAExC,MAAI,OAAO,WAAW,UAAU,IAAI,KAAK;AACvC,UAAM,IAAI,WAAW,2CAA2C,UAAU,qCAAqC;AAAA,EACjH;AAIA,QAAM,GAAG,YAAY,EAAE,OAAO,KAAK,CAAC;AAEpC,QAAM,SAAS,oBAAoB,EAAE,OAAO,UAAU,EAAE,MAAM,CAAC;AAC/D,QAAM,IAAI,QAAc,CAAC,SAAS,WAAW;AAC3C,WAAO,KAAK,SAAS,MAAM;AAC3B,WAAO,OAAO,YAAY,MAAM,QAAQ,CAAC;AAAA,EAC3C,CAAC;AACD,QAAMC,WAAU,EAAE,WAAW,GAAG,QAAQ,GAAG;AAAA,CAAI;AAE/C,QAAM,WAAW,YAAY;AAC3B,WAAO,MAAM;AACb,UAAM,GAAG,YAAY,EAAE,OAAO,KAAK,CAAC;AACpC,UAAM,GAAG,EAAE,WAAW,EAAE,OAAO,KAAK,CAAC;AACrC,YAAQ,KAAK,CAAC;AAAA,EAChB;AACA,UAAQ,KAAK,UAAU,MAAM,KAAK,SAAS,CAAC;AAC5C,UAAQ,KAAK,WAAW,MAAM,KAAK,SAAS,CAAC;AAE7C,SAAO,EAAE,YAAY,OAAO;AAC9B;;;AFjCA,IAAM,UAAU,IAAI,QAAQ,cAAc,EACvC,YAAY,wEAAwE,EACpF,QAAQ,SAAS,eAAe,EAChC,mBAAmB;AAEtB,QACG,QAAQ,SAAS,EAAE,WAAW,KAAK,CAAC,EACpC,YAAY,qDAAqD,EACjE,OAAO,mBAAmB,wDAAwD,EAClF,OAAO,OAAO,SAA8B;AAC3C,QAAM,EAAE,WAAW,IAAI,MAAM,OAAO,KAAK,SAAS,EAAE,YAAY,KAAK,OAAO,IAAI,CAAC,CAAC;AAClF,UAAQ,IAAI,gBAAgB,OAAO,iBAAiB,UAAU,SAAS,QAAQ,GAAG,GAAG;AACvF,CAAC;AAEH,IAAI;AACF,QAAM,QAAQ,WAAW,QAAQ,IAAI;AACvC,SAAS,KAAK;AACZ,MAAI,eAAe,YAAY;AAC7B,YAAQ,MAAM,UAAU,IAAI,OAAO,EAAE;AACrC,YAAQ,KAAK,IAAI,QAAQ;AAAA,EAC3B;AACA,QAAM;AACR;","names":["writeFile","readFile","writeFile","z","z","readFile","writeFile","Hono","require","Hono","z","z","Hono","Hono","Hono","Hono","writeFile"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../../shared/src/errors.ts","../../shared/src/paths.ts","../src/core/config.ts","../../shared/src/sizes.ts","../src/core/network.ts","../src/core/exec.ts","../src/listen.ts","../src/core/state.ts","../src/app.ts","../../shared/src/version.ts","../src/version.ts","../src/routes/capacity.ts","../src/core/capacity.ts","../src/routes/machines.ts","../../shared/src/api.ts","../src/core/machines.ts","../src/core/firecracker.ts","../src/routes/snapshots.ts","../src/core/snapshots.ts"],"sourcesContent":["/**\n * kvbox-server: the host daemon. `kvbox-server` (or `kvbox-server start`) serves\n * the kvbox API on ~/.kvbox/server.sock in the foreground; the CLI's\n * `kvbox server start` spawns it detached. M2 adds `register`.\n */\nimport { spawnSync } from \"node:child_process\";\nimport { Command } from \"commander\";\nimport { KvboxError } from \"@kvbox/shared/errors\";\nimport { paths } from \"@kvbox/shared/paths\";\nimport { loadConfig } from \"./core/config.js\";\nimport { ensureHostNetwork, netSetupScript } from \"./core/network.js\";\nimport { listen } from \"./listen.js\";\nimport { VERSION } from \"./version.js\";\n\nconst program = new Command(\"kvbox-server\")\n .description(\"kvbox host daemon: owns Firecracker and ~/.kvbox, serves the kvbox API\")\n .version(VERSION, \"-v, --version\")\n .showHelpAfterError();\n\nprogram\n .command(\"start\", { isDefault: true })\n .description(\"serve the API on the local Unix socket (foreground)\")\n .option(\"--socket <path>\", \"socket to listen on (default: $KVBOX_HOME/server.sock)\")\n .action(async (opts: { socket?: string }) => {\n const { socketPath } = await listen(opts.socket ? { socketPath: opts.socket } : {});\n console.log(`kvbox-server ${VERSION} listening on ${socketPath} (pid ${process.pid})`);\n // The bridge vanishes on host reboot; recreate it. Not fatal: ls/status must still work.\n try {\n const cfg = await loadConfig(paths());\n const state = await ensureHostNetwork(cfg);\n console.log(`host network: bridge ${cfg.bridge} ${state}`);\n } catch (err) {\n console.error(`warning: host network not ready (${(err as Error).message}); run: kvbox-server net up`);\n }\n });\n\nprogram\n .command(\"net <action>\")\n .description(\"host network: up | down | status (bridge, forwarding, NAT; uses sudo)\")\n .action(async (action: string) => {\n if (![\"up\", \"down\", \"status\"].includes(action)) throw new KvboxError(\"net: expected up, down or status\");\n const cfg = await loadConfig(paths());\n const res = spawnSync(netSetupScript(), [action], {\n stdio: \"inherit\",\n env: { ...process.env, KVBOX_BRIDGE: cfg.bridge, KVBOX_SUBNET: cfg.subnet, KVBOX_GATEWAY: cfg.gateway },\n });\n process.exit(res.status ?? 1);\n });\n\ntry {\n await program.parseAsync(process.argv);\n} catch (err) {\n if (err instanceof KvboxError) {\n console.error(`error: ${err.message}`);\n process.exit(err.exitCode);\n }\n throw err;\n}\n","/** Errors with an exit code; the entry point prints `message` and exits with `exitCode`. */\nexport class KvboxError extends Error {\n constructor(\n message: string,\n public readonly exitCode: number = 1,\n ) {\n super(message);\n this.name = \"KvboxError\";\n }\n}\n\nexport class NotImplementedError extends KvboxError {\n constructor(what: string) {\n super(`${what}: not implemented yet`, 2);\n this.name = \"NotImplementedError\";\n }\n}\n\n/** The named box/snapshot/host does not exist: HTTP 404, exit 1. */\nexport class NotFoundError extends KvboxError {\n constructor(what: string) {\n super(`${what}: not found`, 1);\n this.name = \"NotFoundError\";\n }\n}\n","/**\n * Host state layout. The filesystem is the database and `~/.kvbox` is its root.\n * This module is dependency-free so both the CLI (socket path, hosts.json)\n * and core (everything else) can use it without crossing the layer boundary.\n *\n * `KVBOX_HOME` overrides the root, mainly for tests.\n */\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\nexport function kvboxHome(): string {\n return process.env[\"KVBOX_HOME\"] ?? join(homedir(), \".kvbox\");\n}\n\nexport function paths(home: string = kvboxHome()) {\n return {\n home,\n config: join(home, \"config.json\"),\n serverSock: join(home, \"server.sock\"),\n serverPid: join(home, \"server.pid\"),\n serverLog: join(home, \"server.log\"),\n hosts: join(home, \"hosts.json\"),\n ssh: join(home, \"ssh\"),\n sshKey: join(home, \"ssh\", \"id_ed25519\"),\n bin: join(home, \"bin\"),\n firecracker: join(home, \"bin\", \"firecracker\"),\n assets: join(home, \"assets\"),\n kernel: join(home, \"assets\", \"vmlinux\"),\n rootfs: join(home, \"assets\", \"rootfs.ext4\"),\n machines: join(home, \"machines\"),\n snapshots: join(home, \"snapshots\"),\n machine: (name: string) => ({\n dir: join(home, \"machines\", name),\n meta: join(home, \"machines\", name, \"meta.json\"),\n disk: join(home, \"machines\", name, \"disk.ext4\"),\n sock: join(home, \"machines\", name, \"fc.sock\"),\n log: join(home, \"machines\", name, \"fc.log\"),\n }),\n snapshot: (name: string) => ({\n dir: join(home, \"snapshots\", name),\n meta: join(home, \"snapshots\", name, \"meta.json\"),\n vmstate: join(home, \"snapshots\", name, \"vmstate\"),\n mem: join(home, \"snapshots\", name, \"mem\"),\n disk: join(home, \"snapshots\", name, \"disk.ext4\"),\n }),\n } as const;\n}\n\nexport type Paths = ReturnType<typeof paths>;\n","/** `~/.kvbox/config.json`: asset paths, subnet, default size. Written with defaults on first run. */\nimport { readFile, writeFile } from \"node:fs/promises\";\nimport { z } from \"zod\";\nimport { paths, type Paths } from \"@kvbox/shared/paths\";\nimport { DEFAULT_SIZE, SIZE_NAMES } from \"@kvbox/shared/sizes\";\n\nexport const ConfigSchema = z.object({\n kernel: z.string(),\n rootfs: z.string(),\n firecracker: z.string(),\n bridge: z.string().default(\"kvbox0\"),\n subnet: z.string().default(\"172.30.0.0/24\"),\n gateway: z.string().default(\"172.30.0.1\"),\n defaultSize: z.enum(SIZE_NAMES as [string, ...string[]]).default(DEFAULT_SIZE),\n});\nexport type Config = z.infer<typeof ConfigSchema>;\n\nexport function defaultConfig(p: Paths = paths()): Config {\n return ConfigSchema.parse({\n kernel: p.kernel,\n rootfs: p.rootfs,\n firecracker: p.firecracker,\n });\n}\n\nexport async function loadConfig(p: Paths = paths()): Promise<Config> {\n try {\n const raw = await readFile(p.config, \"utf8\");\n return ConfigSchema.parse(JSON.parse(raw));\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n const cfg = defaultConfig(p);\n await writeFile(p.config, JSON.stringify(cfg, null, 2) + \"\\n\");\n return cfg;\n }\n throw err;\n }\n}\n","/** Fixed box sizes. Firecracker backs guest memory lazily, so a box only costs what the guest touches. */\nexport const SIZES = {\n s: { vcpu: 2, memMib: 4096 },\n m: { vcpu: 4, memMib: 8192 },\n l: { vcpu: 8, memMib: 16384 },\n} as const;\n\nexport type SizeName = keyof typeof SIZES;\nexport const SIZE_NAMES = Object.keys(SIZES) as SizeName[];\nexport const DEFAULT_SIZE: SizeName = \"s\";\n\nexport function isSizeName(value: string): value is SizeName {\n return value in SIZES;\n}\n","/**\n * Host networking: bridge `kvbox0`, one tap `kv-<box>` per box, IP allocation\n * inside the subnet. Everything that needs root goes through one script,\n * `net-setup.sh`, which re-execs itself with `sudo -n` when run from here.\n * `install-server.sh` installs a root-owned copy at PRIVILEGED_SCRIPT plus a\n * sudoers rule for exactly that path; the package copy is the dev fallback.\n */\nimport { existsSync } from \"node:fs\";\nimport { fileURLToPath } from \"node:url\";\nimport { KvboxError } from \"@kvbox/shared/errors\";\nimport type { Config } from \"./config.js\";\nimport { run } from \"./exec.js\";\n\nexport const TAP_PREFIX = \"kv-\";\nexport const PRIVILEGED_SCRIPT = \"/usr/local/lib/kvbox/net-setup.sh\";\n\ntype NetConfig = Pick<Config, \"bridge\" | \"subnet\" | \"gateway\">;\n\nexport function tapName(box: string): string {\n // Linux interface names are limited to 15 characters.\n return `${TAP_PREFIX}${box}`.slice(0, 15);\n}\n\n/** The script to run: KVBOX_NET_SETUP override, the root-owned install, else the package copy (dist/ or src/core/). */\nexport function netSetupScript(): string {\n const override = process.env[\"KVBOX_NET_SETUP\"];\n if (override) return override;\n if (existsSync(PRIVILEGED_SCRIPT)) return PRIVILEGED_SCRIPT;\n for (const rel of [\"../scripts/net-setup.sh\", \"../../scripts/net-setup.sh\"]) {\n const candidate = fileURLToPath(new URL(rel, import.meta.url));\n if (existsSync(candidate)) return candidate;\n }\n throw new KvboxError(\"net-setup.sh not found next to kvbox-server\");\n}\n\nexport function bridgeExists(bridge: string): boolean {\n return existsSync(`/sys/class/net/${bridge}`);\n}\n\nfunction netRun(cfg: NetConfig, args: string[]) {\n return run([netSetupScript(), ...args], {\n env: { ...process.env, KVBOX_BRIDGE: cfg.bridge, KVBOX_SUBNET: cfg.subnet, KVBOX_GATEWAY: cfg.gateway },\n }).catch((err: Error) => {\n throw new KvboxError(\n `${err.message}\\n (root is needed for host networking; install-server.sh sets up a sudoers rule for ${PRIVILEGED_SCRIPT})`,\n );\n });\n}\n\n/** Run `net-setup.sh up` when the bridge is missing, which is the case after every host reboot. */\nexport async function ensureHostNetwork(cfg: NetConfig): Promise<\"present\" | \"created\"> {\n if (bridgeExists(cfg.bridge)) return \"present\";\n await netRun(cfg, [\"up\"]);\n return \"created\";\n}\n\n/** Create the box's tap (owned by this user, plugged into the bridge) and return its name. */\nexport async function createTap(box: string, cfg: NetConfig): Promise<string> {\n const tap = tapName(box);\n await netRun(cfg, [\"tap\", \"add\", tap]);\n return tap;\n}\n\nexport async function deleteTap(box: string, cfg: NetConfig): Promise<void> {\n await netRun(cfg, [\"tap\", \"del\", tapName(box)]);\n}\n\n// ── addresses ────────────────────────────────────────────────────────────────\nconst ipToInt = (ip: string) => ip.split(\".\").reduce((acc, o) => (acc << 8) + Number(o), 0) >>> 0;\nconst intToIp = (n: number) => [24, 16, 8, 0].map((s) => (n >>> s) & 255).join(\".\");\n\n/** Lowest free host address in `subnet` (a.b.c.d/len), skipping network, gateway (.1) and broadcast. */\nexport function nextFreeIp(subnet: string, inUse: readonly string[]): string {\n const [base, lenStr] = subnet.split(\"/\");\n const len = Number(lenStr);\n if (!base || !Number.isInteger(len) || len < 8 || len > 30) throw new KvboxError(`bad subnet: ${subnet}`);\n const mask = len === 0 ? 0 : (~0 << (32 - len)) >>> 0;\n const network = (ipToInt(base) & mask) >>> 0;\n const broadcast = (network | (~mask >>> 0)) >>> 0;\n const used = new Set(inUse.map(ipToInt));\n for (let n = network + 2; n < broadcast; n++) {\n if (!used.has(n)) return intToIp(n);\n }\n throw new KvboxError(`no free IP left in ${subnet}`);\n}\n\n/** Locally administered MAC derived from the IP: 06:00:<a>:<b>:<c>:<d>, unique per box and readable. */\nexport function macFor(ip: string): string {\n const hex = ip.split(\".\").map((o) => Number(o).toString(16).padStart(2, \"0\").toUpperCase());\n return [\"06\", \"00\", ...hex].join(\":\");\n}\n","/**\n * The one shell-out helper. Every `firecracker`, `ip`, `ssh`, `cp` call goes\n * through here: argv arrays only (never a shell string), the command is logged,\n * and stderr ends up in the thrown error.\n */\nimport { execFile } from \"node:child_process\";\nimport { promisify } from \"node:util\";\nimport { KvboxError } from \"@kvbox/shared/errors\";\n\nconst execFileAsync = promisify(execFile);\n\nexport interface RunOptions {\n sudo?: boolean;\n cwd?: string;\n timeoutMs?: number;\n env?: NodeJS.ProcessEnv;\n}\n\nexport interface RunResult {\n stdout: string;\n stderr: string;\n}\n\nexport async function run(argv: readonly string[], opts: RunOptions = {}): Promise<RunResult> {\n const [cmd, ...args] = opts.sudo ? [\"sudo\", \"-n\", ...argv] : argv;\n if (!cmd) throw new KvboxError(\"run: empty argv\");\n if (process.env[\"KVBOX_DEBUG\"]) console.error(`[exec] ${[cmd, ...args].join(\" \")}`);\n try {\n const { stdout, stderr } = await execFileAsync(cmd, args, {\n cwd: opts.cwd,\n env: opts.env ?? process.env,\n timeout: opts.timeoutMs ?? 30_000,\n maxBuffer: 8 * 1024 * 1024,\n });\n return { stdout, stderr };\n } catch (err) {\n const e = err as NodeJS.ErrnoException & { stderr?: string; code?: number | string };\n const detail = (e.stderr ?? e.message ?? \"\").trim();\n throw new KvboxError(`${[cmd, ...args].join(\" \")} failed${detail ? `: ${detail}` : \"\"}`);\n }\n}\n","/** Bind the app to the local Unix socket and write the pid file. */\nimport { rm, writeFile } from \"node:fs/promises\";\nimport { createAdaptorServer } from \"@hono/node-server\";\nimport { ensureLayout } from \"./core/state.js\";\nimport { loadConfig } from \"./core/config.js\";\nimport { KvboxError } from \"@kvbox/shared/errors\";\nimport { paths } from \"@kvbox/shared/paths\";\nimport { createApp } from \"./app.js\";\n\nexport interface ListenOptions {\n socketPath?: string;\n}\n\nexport async function listen(opts: ListenOptions = {}) {\n const p = await ensureLayout(paths());\n await loadConfig(p);\n const socketPath = opts.socketPath ?? p.serverSock;\n // Linux limits a Unix socket path (sun_path) to 108 bytes; bind() fails with EINVAL past it.\n if (Buffer.byteLength(socketPath) > 100) {\n throw new KvboxError(`socket path too long for a Unix socket (${socketPath}); set KVBOX_HOME to a shorter path`);\n }\n\n // A stale socket file from a crashed server blocks bind(); the pid file tells\n // `server status` whether anything is actually alive.\n await rm(socketPath, { force: true });\n\n const server = createAdaptorServer({ fetch: createApp().fetch });\n await new Promise<void>((resolve, reject) => {\n server.once(\"error\", reject);\n server.listen(socketPath, () => resolve());\n });\n await writeFile(p.serverPid, `${process.pid}\\n`);\n\n const shutdown = async () => {\n server.close();\n await rm(socketPath, { force: true });\n await rm(p.serverPid, { force: true });\n process.exit(0);\n };\n process.once(\"SIGINT\", () => void shutdown());\n process.once(\"SIGTERM\", () => void shutdown());\n\n return { socketPath, server };\n}\n","/**\n * The `~/.kvbox` layout and the per-box / per-snapshot `meta.json` files.\n * The server is the only writer; every read is validated with zod.\n */\nimport { mkdir, readFile, readdir, writeFile } from \"node:fs/promises\";\nimport { z } from \"zod\";\nimport { paths, type Paths } from \"@kvbox/shared/paths\";\n\nexport const MachineMetaSchema = z.object({\n name: z.string(),\n ip: z.string(),\n mac: z.string(),\n pid: z.number().int(),\n vcpu: z.number().int(),\n memMib: z.number().int(),\n createdAt: z.string(),\n source: z.string(),\n});\nexport type MachineMeta = z.infer<typeof MachineMetaSchema>;\n\nexport const SnapshotMetaSchema = z.object({\n name: z.string(),\n source: z.string(),\n vcpu: z.number().int(),\n memMib: z.number().int(),\n createdAt: z.string(),\n});\nexport type SnapshotMeta = z.infer<typeof SnapshotMetaSchema>;\n\n/** Create every directory of the layout. Idempotent; called by `kvbox server start`. */\nexport async function ensureLayout(p: Paths = paths()): Promise<Paths> {\n for (const dir of [p.home, p.ssh, p.bin, p.assets, p.machines, p.snapshots]) {\n await mkdir(dir, { recursive: true, mode: 0o700 });\n }\n return p;\n}\n\nasync function readJson<T>(file: string, schema: z.ZodType<T>): Promise<T> {\n return schema.parse(JSON.parse(await readFile(file, \"utf8\")));\n}\n\nasync function listDirs(dir: string): Promise<string[]> {\n try {\n const entries = await readdir(dir, { withFileTypes: true });\n return entries.filter((e) => e.isDirectory()).map((e) => e.name);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return [];\n throw err;\n }\n}\n\nexport async function readMachineMeta(name: string, p: Paths = paths()) {\n return readJson(p.machine(name).meta, MachineMetaSchema);\n}\n\nexport async function writeMachineMeta(meta: MachineMeta, p: Paths = paths()) {\n await mkdir(p.machine(meta.name).dir, { recursive: true });\n await writeFile(p.machine(meta.name).meta, JSON.stringify(meta, null, 2) + \"\\n\");\n}\n\nexport async function listMachineNames(p: Paths = paths()) {\n return listDirs(p.machines);\n}\n\nexport async function readSnapshotMeta(name: string, p: Paths = paths()) {\n return readJson(p.snapshot(name).meta, SnapshotMetaSchema);\n}\n\nexport async function writeSnapshotMeta(meta: SnapshotMeta, p: Paths = paths()) {\n await mkdir(p.snapshot(meta.name).dir, { recursive: true });\n await writeFile(p.snapshot(meta.name).meta, JSON.stringify(meta, null, 2) + \"\\n\");\n}\n\nexport async function listSnapshotNames(p: Paths = paths()) {\n return listDirs(p.snapshots);\n}\n\n/** True when a pid is alive (signal 0). */\nexport function pidAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (err) {\n return (err as NodeJS.ErrnoException).code === \"EPERM\";\n }\n}\n","/**\n * The hono app behind `kvbox-server`. Served over `~/.kvbox/server.sock`\n * in M1; the same app goes behind HTTPS in M2.\n */\nimport { Hono } from \"hono\";\nimport { KvboxError, NotFoundError } from \"@kvbox/shared/errors\";\nimport { VERSION } from \"./version.js\";\nimport { capacityRoutes } from \"./routes/capacity.js\";\nimport { machineRoutes } from \"./routes/machines.js\";\nimport { snapshotRoutes } from \"./routes/snapshots.js\";\n\nexport function createApp() {\n const app = new Hono();\n\n app.get(\"/health\", (c) => c.json({ ok: true as const, version: VERSION, pid: process.pid }));\n app.route(\"/machines\", machineRoutes());\n app.route(\"/snapshots\", snapshotRoutes());\n app.route(\"/capacity\", capacityRoutes());\n\n app.notFound((c) => c.json({ error: `no route: ${c.req.method} ${c.req.path}` }, 404));\n app.onError((err, c) => {\n if (err instanceof NotFoundError) return c.json({ error: err.message }, 404);\n if (err instanceof KvboxError) {\n return c.json({ error: err.message }, err.exitCode === 2 ? 501 : 400);\n }\n console.error(err);\n return c.json({ error: err.message }, 500);\n });\n\n return app;\n}\n\nexport type App = ReturnType<typeof createApp>;\n","/**\n * Each binary's version comes from its own package.json: tsup injects\n * `__KVBOX_VERSION__` at build time; the dev path (tsx) reads the file.\n * Usage in a package: `versionOf(import.meta.url)` from its `src/version.ts`.\n */\nimport { createRequire } from \"node:module\";\n\nexport function versionOf(srcFileUrl: string, injected?: string): string {\n if (typeof injected === \"string\") return injected;\n try {\n const require = createRequire(srcFileUrl);\n return (require(\"../package.json\") as { version: string }).version;\n } catch {\n return \"0.0.0-dev\";\n }\n}\n","import { versionOf } from \"@kvbox/shared/version\";\n\ndeclare const __KVBOX_VERSION__: string | undefined;\n\nexport const VERSION: string = versionOf(import.meta.url, typeof __KVBOX_VERSION__ === \"string\" ? __KVBOX_VERSION__ : undefined);\n","import { Hono } from \"hono\";\nimport { capacity } from \"../core/capacity.js\";\n\nexport function capacityRoutes() {\n const r = new Hono();\n r.get(\"/\", async (c) => c.json(await capacity()));\n return r;\n}\n","/**\n * The capacity guard: `kvbox new` refuses when reserved RAM of running boxes\n * plus the new one exceeds host RAM minus 2 GB (hard), or reserved vCPUs exceed\n * 2x host cores (soft). `--force` skips it.\n */\nimport { NotImplementedError } from \"@kvbox/shared/errors\";\nimport type { Capacity } from \"@kvbox/shared/api\";\n\nexport const MEM_HEADROOM_MIB = 2048;\nexport const VCPU_OVERSUBSCRIPTION = 2;\n\nexport async function capacity(): Promise<Capacity> {\n throw new NotImplementedError(\"capacity.capacity\");\n}\n\nexport async function assertRoomFor(_vcpu: number, _memMib: number, _force: boolean): Promise<void> {\n throw new NotImplementedError(\"capacity.assertRoomFor\");\n}\n","import { Hono } from \"hono\";\nimport { CreateMachineRequest } from \"@kvbox/shared/api\";\nimport { createMachine, destroyMachine, listMachines } from \"../core/machines.js\";\n\nexport function machineRoutes() {\n const r = new Hono();\n r.get(\"/\", async (c) => c.json(await listMachines()));\n r.post(\"/\", async (c) => {\n const body = CreateMachineRequest.parse(await c.req.json().catch(() => ({})));\n return c.json(await createMachine(body), 201);\n });\n r.delete(\"/:name\", async (c) => {\n await destroyMachine(c.req.param(\"name\"));\n return c.body(null, 204);\n });\n return r;\n}\n","/**\n * Wire shapes of the kvbox HTTP API, shared by the client (src/cli) and the\n * server (src/server). Validated with zod on both sides because the client\n * may be talking to an older or newer server.\n */\nimport { z } from \"zod\";\nimport { SIZE_NAMES } from \"./sizes.js\";\n\nexport const SizeSchema = z.enum(SIZE_NAMES as [string, ...string[]]);\n\nexport const MachineSchema = z.object({\n name: z.string(),\n ip: z.string(),\n mac: z.string(),\n pid: z.number().int().nullable(),\n vcpu: z.number().int(),\n memMib: z.number().int(),\n createdAt: z.string(),\n source: z.string(), // \"base\" or \"snapshot:<name>\"\n status: z.enum([\"running\", \"stopped\"]),\n});\nexport type Machine = z.infer<typeof MachineSchema>;\n\nexport const CreateMachineRequest = z.object({\n name: z.string().optional(),\n size: SizeSchema.optional(),\n from: z.string().optional(),\n force: z.boolean().optional(),\n});\nexport type CreateMachineRequest = z.infer<typeof CreateMachineRequest>;\n\nexport const SnapshotSchema = z.object({\n name: z.string(),\n source: z.string(),\n vcpu: z.number().int(),\n memMib: z.number().int(),\n createdAt: z.string(),\n});\nexport type Snapshot = z.infer<typeof SnapshotSchema>;\n\nexport const CreateSnapshotRequest = z.object({\n box: z.string(),\n name: z.string(),\n});\nexport type CreateSnapshotRequest = z.infer<typeof CreateSnapshotRequest>;\n\nexport const CapacitySchema = z.object({\n hostMemMib: z.number().int(),\n hostCores: z.number().int(),\n reservedMemMib: z.number().int(),\n reservedVcpu: z.number().int(),\n running: z.number().int(),\n});\nexport type Capacity = z.infer<typeof CapacitySchema>;\n\nexport const HealthSchema = z.object({\n ok: z.literal(true),\n version: z.string(),\n pid: z.number().int(),\n});\nexport type Health = z.infer<typeof HealthSchema>;\n\nexport const ApiErrorSchema = z.object({\n error: z.string(),\n});\n","/**\n * Create, list and destroy boxes: what spikes #4 and #6 did by hand.\n * See architecture \"Process model\". The server is the only writer of ~/.kvbox/machines.\n */\nimport { spawn } from \"node:child_process\";\nimport { closeSync, existsSync, openSync } from \"node:fs\";\nimport { mkdir, readFile, rm } from \"node:fs/promises\";\nimport { setTimeout as sleep } from \"node:timers/promises\";\nimport { KvboxError, NotFoundError, NotImplementedError } from \"@kvbox/shared/errors\";\nimport type { CreateMachineRequest, Machine } from \"@kvbox/shared/api\";\nimport { paths, type Paths } from \"@kvbox/shared/paths\";\nimport { SIZES, isSizeName } from \"@kvbox/shared/sizes\";\nimport { loadConfig, type Config } from \"./config.js\";\nimport { run } from \"./exec.js\";\nimport { FirecrackerClient } from \"./firecracker.js\";\nimport { createTap, deleteTap, ensureHostNetwork, macFor, nextFreeIp } from \"./network.js\";\nimport { listMachineNames, pidAlive, readMachineMeta, writeMachineMeta, type MachineMeta } from \"./state.js\";\n\n/** Box names double as hostnames and tap suffixes (kv- + name ≤ 15 chars). */\nconst NAME_RE = /^[a-z0-9][a-z0-9-]{0,11}$/;\nconst AUTO_PREFIX = \"box\";\n\nfunction toMachine(meta: MachineMeta): Machine {\n const alive = pidAlive(meta.pid);\n return {\n name: meta.name,\n ip: meta.ip,\n mac: meta.mac,\n pid: alive ? meta.pid : null,\n vcpu: meta.vcpu,\n memMib: meta.memMib,\n createdAt: meta.createdAt,\n source: meta.source,\n status: alive ? \"running\" : \"stopped\",\n };\n}\n\nasync function readAllMeta(p: Paths): Promise<MachineMeta[]> {\n const metas: MachineMeta[] = [];\n for (const name of await listMachineNames(p)) {\n try {\n metas.push(await readMachineMeta(name, p));\n } catch (err) {\n console.error(`warning: skipping machines/${name}: ${(err as Error).message}`);\n }\n }\n return metas;\n}\n\nexport async function listMachines(p: Paths = paths()): Promise<Machine[]> {\n return (await readAllMeta(p)).map(toMachine);\n}\n\nexport async function getMachine(name: string, p: Paths = paths()): Promise<Machine> {\n try {\n return toMachine(await readMachineMeta(name, p));\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") throw new NotFoundError(`box ${name}`);\n throw err;\n }\n}\n\nfunction pickName(requested: string | undefined, taken: Set<string>): string {\n if (requested !== undefined) {\n if (!NAME_RE.test(requested)) {\n throw new KvboxError(`invalid box name \"${requested}\": 1-12 chars, lowercase letters, digits and '-', starting with a letter or digit`);\n }\n if (taken.has(requested)) throw new KvboxError(`box ${requested} already exists`);\n return requested;\n }\n for (let i = 1; ; i++) {\n const candidate = `${AUTO_PREFIX}${i}`;\n if (!taken.has(candidate)) return candidate;\n }\n}\n\nfunction bootArgs(name: string, ip: string, cfg: Config, pubkeyB64: string): string {\n // Firecracker appends `pci=off root=/dev/vda rw` itself. ip= is read by the guest kernel,\n // kvbox.name= / kvbox.sshkey= by kvbox-init on first boot (the kvbox-images contract).\n const args = `console=ttyS0 reboot=k panic=1 ip=${ip}::${cfg.gateway}:255.255.255.0:${name}:eth0:off kvbox.name=${name} kvbox.sshkey=${pubkeyB64}`;\n return process.arch === \"arm64\" ? `keep_bootcon ${args}` : args;\n}\n\nasync function waitFor(pred: () => boolean, timeoutMs: number, stepMs = 50): Promise<boolean> {\n const deadline = Date.now() + timeoutMs;\n while (Date.now() < deadline) {\n if (pred()) return true;\n await sleep(stepMs);\n }\n return pred();\n}\n\nasync function tailOf(file: string, lines = 5): Promise<string> {\n try {\n return (await readFile(file, \"utf8\")).trim().split(\"\\n\").slice(-lines).join(\"\\n\");\n } catch {\n return \"\";\n }\n}\n\nexport async function createMachine(req: CreateMachineRequest, p: Paths = paths()): Promise<Machine> {\n if (req.from !== undefined) throw new NotImplementedError(\"new --from <snapshot> (#13)\");\n const cfg = await loadConfig(p);\n const sizeName = req.size ?? cfg.defaultSize;\n if (!isSizeName(sizeName)) throw new KvboxError(`unknown size \"${sizeName}\"; use one of ${Object.keys(SIZES).join(\", \")}`);\n const size = SIZES[sizeName];\n\n for (const [what, file] of [[\"kernel\", cfg.kernel], [\"base image\", cfg.rootfs], [\"firecracker\", cfg.firecracker]] as const) {\n if (!existsSync(file)) throw new KvboxError(`${what} missing at ${file}; run install-server.sh`);\n }\n let pubkeyB64: string;\n try {\n pubkeyB64 = Buffer.from((await readFile(`${p.sshKey}.pub`, \"utf8\")).trim()).toString(\"base64\");\n } catch {\n throw new KvboxError(`SSH key missing at ${p.sshKey}.pub; run install-server.sh`);\n }\n\n const existing = await readAllMeta(p);\n const name = pickName(req.name, new Set(existing.map((m) => m.name)));\n const ip = nextFreeIp(cfg.subnet, [...existing.map((m) => m.ip), cfg.gateway]);\n const mac = macFor(ip);\n const m = p.machine(name);\n\n await ensureHostNetwork(cfg);\n await mkdir(m.dir, { recursive: true, mode: 0o700 });\n let tapCreated = false;\n let fcPid: number | undefined;\n try {\n // 1. the box's own disk: instant on btrfs/xfs (reflink), a sparse copy elsewhere\n await run([\"cp\", \"--reflink=auto\", \"--sparse=always\", cfg.rootfs, m.disk], { timeoutMs: 120_000 });\n // 2. its network cable\n const tap = await createTap(name, cfg);\n tapCreated = true;\n // 3. Firecracker, detached so it outlives the server; stdio to fc.log\n await rm(m.sock, { force: true });\n const logFd = openSync(m.log, \"a\");\n let child;\n try {\n child = spawn(cfg.firecracker, [\"--api-sock\", m.sock, \"--id\", name, \"--level\", \"Error\"], {\n detached: true,\n stdio: [\"ignore\", logFd, logFd],\n });\n } finally {\n closeSync(logFd);\n }\n child.unref();\n fcPid = child.pid;\n if (fcPid === undefined) throw new KvboxError(\"could not start firecracker\");\n if (!(await waitFor(() => existsSync(m.sock), 3000))) {\n throw new KvboxError(`firecracker did not open its API socket; last log lines:\\n${await tailOf(m.log)}`);\n }\n // 4. configure and start over its socket\n const fc = new FirecrackerClient(m.sock);\n try {\n await fc.bootSource(cfg.kernel, bootArgs(name, ip, cfg, pubkeyB64));\n await fc.rootDrive(m.disk);\n await fc.machineConfig(size.vcpu, size.memMib);\n await fc.networkInterface(\"eth0\", tap, mac);\n await fc.start();\n } finally {\n await fc.close();\n }\n const meta: MachineMeta = {\n name,\n ip,\n mac,\n pid: fcPid,\n vcpu: size.vcpu,\n memMib: size.memMib,\n createdAt: new Date().toISOString(),\n source: \"base\",\n };\n await writeMachineMeta(meta, p);\n return toMachine(meta);\n } catch (err) {\n // leave nothing half-made behind\n if (fcPid !== undefined) {\n try {\n process.kill(fcPid, \"SIGKILL\");\n } catch {\n /* already gone */\n }\n }\n if (tapCreated) await deleteTap(name, cfg).catch(() => undefined);\n await rm(m.dir, { recursive: true, force: true });\n throw err;\n }\n}\n\nexport async function destroyMachine(name: string, p: Paths = paths()): Promise<void> {\n const machine = await getMachine(name, p);\n const cfg = await loadConfig(p);\n if (machine.pid !== null) {\n process.kill(machine.pid, \"SIGTERM\");\n if (!(await waitFor(() => !pidAlive(machine.pid!), 2000, 100))) {\n try {\n process.kill(machine.pid, \"SIGKILL\");\n } catch {\n /* exited meanwhile */\n }\n }\n }\n await deleteTap(name, cfg);\n await rm(p.machine(name).dir, { recursive: true, force: true });\n}\n","/**\n * Client for one Firecracker process's API socket. Ten-odd endpoints, all\n * PUT/PATCH with JSON bodies, so a thin wrapper over undici beats an SDK.\n */\nimport { Agent, request } from \"undici\";\nimport { KvboxError } from \"@kvbox/shared/errors\";\n\nexport class FirecrackerClient {\n private readonly agent: Agent;\n\n constructor(readonly socketPath: string) {\n this.agent = new Agent({ connect: { socketPath } });\n }\n\n private async call(method: \"GET\" | \"PUT\" | \"PATCH\", path: string, body?: unknown): Promise<unknown> {\n const res = await request(`http://localhost${path}`, {\n dispatcher: this.agent,\n method,\n headers: body === undefined ? {} : { \"content-type\": \"application/json\" },\n body: body === undefined ? null : JSON.stringify(body),\n });\n const text = await res.body.text();\n if (res.statusCode >= 300) {\n throw new KvboxError(`firecracker ${method} ${path} -> ${res.statusCode}: ${text}`);\n }\n return text ? JSON.parse(text) : undefined;\n }\n\n bootSource(kernelImagePath: string, bootArgs: string) {\n return this.call(\"PUT\", \"/boot-source\", { kernel_image_path: kernelImagePath, boot_args: bootArgs });\n }\n\n rootDrive(pathOnHost: string) {\n return this.call(\"PUT\", \"/drives/rootfs\", {\n drive_id: \"rootfs\",\n path_on_host: pathOnHost,\n is_root_device: true,\n is_read_only: false,\n });\n }\n\n machineConfig(vcpuCount: number, memSizeMib: number) {\n return this.call(\"PUT\", \"/machine-config\", { vcpu_count: vcpuCount, mem_size_mib: memSizeMib });\n }\n\n networkInterface(ifaceId: string, hostDevName: string, guestMac: string) {\n return this.call(\"PUT\", `/network-interfaces/${ifaceId}`, {\n iface_id: ifaceId,\n host_dev_name: hostDevName,\n guest_mac: guestMac,\n });\n }\n\n start() {\n return this.call(\"PUT\", \"/actions\", { action_type: \"InstanceStart\" });\n }\n\n pause() {\n return this.call(\"PATCH\", \"/vm\", { state: \"Paused\" });\n }\n\n resume() {\n return this.call(\"PATCH\", \"/vm\", { state: \"Resumed\" });\n }\n\n snapshotCreate(snapshotPath: string, memFilePath: string) {\n return this.call(\"PUT\", \"/snapshot/create\", {\n snapshot_type: \"Full\",\n snapshot_path: snapshotPath,\n mem_file_path: memFilePath,\n });\n }\n\n snapshotLoad(snapshotPath: string, memFilePath: string, resume = true) {\n return this.call(\"PUT\", \"/snapshot/load\", {\n snapshot_path: snapshotPath,\n mem_backend: { backend_type: \"File\", backend_path: memFilePath },\n resume_vm: resume,\n });\n }\n\n info() {\n return this.call(\"GET\", \"/\");\n }\n\n async close() {\n await this.agent.close();\n }\n}\n","import { Hono } from \"hono\";\nimport { CreateSnapshotRequest } from \"@kvbox/shared/api\";\nimport { deleteSnapshot, listSnapshots, saveSnapshot } from \"../core/snapshots.js\";\n\nexport function snapshotRoutes() {\n const r = new Hono();\n r.get(\"/\", async (c) => c.json(await listSnapshots()));\n r.post(\"/\", async (c) => {\n const body = CreateSnapshotRequest.parse(await c.req.json());\n return c.json(await saveSnapshot(body), 201);\n });\n r.delete(\"/:name\", async (c) => {\n await deleteSnapshot(c.req.param(\"name\"));\n return c.body(null, 204);\n });\n return r;\n}\n","/** Full snapshots (vmstate + mem + disk copy) and their listing. */\nimport { NotImplementedError } from \"@kvbox/shared/errors\";\nimport type { CreateSnapshotRequest, Snapshot } from \"@kvbox/shared/api\";\n\nexport async function listSnapshots(): Promise<Snapshot[]> {\n throw new NotImplementedError(\"snapshots.listSnapshots\");\n}\n\nexport async function saveSnapshot(_req: CreateSnapshotRequest): Promise<Snapshot> {\n throw new NotImplementedError(\"snapshots.saveSnapshot\");\n}\n\nexport async function deleteSnapshot(_name: string): Promise<void> {\n throw new NotImplementedError(\"snapshots.deleteSnapshot\");\n}\n"],"mappings":"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","names":["rm","writeFile","readFile","writeFile","z","z","readFile","writeFile","Hono","require","Hono","z","z","existsSync","mkdir","readFile","bootArgs","readFile","existsSync","m","mkdir","Hono","Hono","Hono","Hono","rm","writeFile"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ghalex/kvbox-server",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "kvbox host daemon: owns Firecracker microVMs on a Linux host and serves the kvbox API. Managed with the @ghalex/kvbox CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
"kvbox-server": "./dist/kvbox-server.js"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
|
-
"dist"
|
|
11
|
+
"dist",
|
|
12
|
+
"scripts"
|
|
12
13
|
],
|
|
13
14
|
"publishConfig": {
|
|
14
15
|
"access": "public"
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Host networking for kvbox: bridge kvbox0 (172.30.0.1/24), IP forwarding, NAT to the
|
|
3
|
+
# default route, and FORWARD accept rules for the bridge (Docker sets FORWARD's policy
|
|
4
|
+
# to DROP, so NAT alone is not enough). Needs root; re-execs itself with sudo.
|
|
5
|
+
# Idempotent. The bridge and the rules vanish on host reboot, so this runs from
|
|
6
|
+
# install-server.sh once and from kvbox-server at start whenever the bridge is missing.
|
|
7
|
+
#
|
|
8
|
+
# net-setup.sh [up] create/repair everything
|
|
9
|
+
# net-setup.sh down remove the rules and the bridge (detaches any taps still on it)
|
|
10
|
+
# net-setup.sh status show what is in place; exit 1 if anything is missing
|
|
11
|
+
# net-setup.sh tap add <name> create tap <name> owned by the calling user, plugged into the bridge
|
|
12
|
+
# net-setup.sh tap del <name> delete it
|
|
13
|
+
#
|
|
14
|
+
# KVBOX_BRIDGE (default kvbox0) KVBOX_SUBNET (default 172.30.0.0/24)
|
|
15
|
+
# KVBOX_GATEWAY (default 172.30.0.1) the host's address on the bridge
|
|
16
|
+
set -euo pipefail
|
|
17
|
+
|
|
18
|
+
BRIDGE="${KVBOX_BRIDGE:-kvbox0}"
|
|
19
|
+
SUBNET="${KVBOX_SUBNET:-172.30.0.0/24}"
|
|
20
|
+
GATEWAY="${KVBOX_GATEWAY:-172.30.0.1}"
|
|
21
|
+
PREFIX="${SUBNET#*/}"
|
|
22
|
+
TAG="kvbox:$BRIDGE" # iptables comment; `down` finds our rules by it
|
|
23
|
+
action="${1:-up}"
|
|
24
|
+
tap_op="${2:-}"; tap_name="${3:-}"
|
|
25
|
+
|
|
26
|
+
if [[ -t 1 ]]; then GREEN=$'\033[0;32m'; RED=$'\033[0;31m'; DIM=$'\033[2m'; NC=$'\033[0m'; else GREEN=""; RED=""; DIM=""; NC=""; fi
|
|
27
|
+
ok() { printf '%s✓%s %s\n' "$GREEN" "$NC" "$*"; }
|
|
28
|
+
skip() { printf '%s–%s %s\n' "$DIM" "$NC" "$*"; }
|
|
29
|
+
fail() { printf '%s✗%s %s\n' "$RED" "$NC" "$*" >&2; exit 1; }
|
|
30
|
+
|
|
31
|
+
case "$action" in
|
|
32
|
+
up|down|status) ;;
|
|
33
|
+
tap) [[ "$tap_op" =~ ^(add|del)$ && "$tap_name" =~ ^[a-z0-9][a-z0-9._-]{0,14}$ ]] || fail "usage: $0 tap add|del <name> (name: up to 15 chars, [a-z0-9._-])" ;;
|
|
34
|
+
*) fail "usage: $0 [up|down|status|tap add|del <name>]" ;;
|
|
35
|
+
esac
|
|
36
|
+
for bin in ip iptables sysctl; do command -v "$bin" >/dev/null 2>&1 || fail "$bin not found"; done
|
|
37
|
+
if [[ $EUID -ne 0 ]]; then
|
|
38
|
+
# From a terminal sudo may prompt; from kvbox-server (no tty) it must not hang.
|
|
39
|
+
# KVBOX_TAP_USER tells the root side who should own a new tap (Firecracker runs as that user).
|
|
40
|
+
export KVBOX_TAP_USER="${KVBOX_TAP_USER:-$(id -un)}"
|
|
41
|
+
if [[ -t 0 ]]; then exec sudo --preserve-env=KVBOX_TAP_USER,KVBOX_BRIDGE,KVBOX_SUBNET,KVBOX_GATEWAY "$0" "$@"; fi
|
|
42
|
+
exec sudo -n --preserve-env=KVBOX_TAP_USER,KVBOX_BRIDGE,KVBOX_SUBNET,KVBOX_GATEWAY "$0" "$@"
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
# ── rule set ─────────────────────────────────────────────────────────────────
|
|
46
|
+
# RULE_DESC[i] is what we tell the user; RULE_SPEC[i] is "<table> <chain> <rule...>".
|
|
47
|
+
# Added with -I (ahead of Docker's chains), checked with -C, found again by the comment tag.
|
|
48
|
+
RULE_DESC=(
|
|
49
|
+
"NAT: boxes reach the internet using this host's address"
|
|
50
|
+
"firewall: traffic from boxes may leave through $BRIDGE"
|
|
51
|
+
"firewall: replies may come back to boxes"
|
|
52
|
+
)
|
|
53
|
+
RULE_SPEC=(
|
|
54
|
+
"nat POSTROUTING -s $SUBNET ! -o $BRIDGE -m comment --comment $TAG -j MASQUERADE"
|
|
55
|
+
"filter FORWARD -i $BRIDGE -m comment --comment $TAG -j ACCEPT"
|
|
56
|
+
"filter FORWARD -o $BRIDGE -m conntrack --ctstate RELATED,ESTABLISHED -m comment --comment $TAG -j ACCEPT"
|
|
57
|
+
)
|
|
58
|
+
has_rule() { local t=$1 c=$2; shift 2; iptables -t "$t" -C "$c" "$@" 2>/dev/null; }
|
|
59
|
+
add_rule() { local t=$1 c=$2; shift 2; iptables -t "$t" -I "$c" 1 "$@"; }
|
|
60
|
+
del_rule() { local t=$1 c=$2; shift 2; while iptables -t "$t" -C "$c" "$@" 2>/dev/null; do iptables -t "$t" -D "$c" "$@"; done; }
|
|
61
|
+
|
|
62
|
+
bridge_exists() { [[ -d "/sys/class/net/$BRIDGE" ]]; }
|
|
63
|
+
taps_on_bridge() { local t out=""; for t in "/sys/class/net/$BRIDGE/brif"/*; do [[ -e "$t" ]] && out+="${t##*/} "; done; printf '%s' "$out"; }
|
|
64
|
+
bridge_has_ip() { ip -4 -o addr show dev "$BRIDGE" 2>/dev/null | grep -q " inet $GATEWAY/$PREFIX "; }
|
|
65
|
+
# Administrative UP flag only: an empty bridge reports operstate "down" (NO-CARRIER) until a tap joins.
|
|
66
|
+
bridge_is_up() { ip link show "$BRIDGE" 2>/dev/null | head -1 | grep -q "[<,]UP[,>]"; }
|
|
67
|
+
|
|
68
|
+
# ── up ───────────────────────────────────────────────────────────────────────
|
|
69
|
+
do_up() {
|
|
70
|
+
if bridge_exists; then skip "bridge $BRIDGE already exists"; else ip link add "$BRIDGE" type bridge; ok "created bridge $BRIDGE (the virtual switch boxes plug into)"; fi
|
|
71
|
+
if bridge_has_ip; then skip "$BRIDGE already has address $GATEWAY"; else ip addr add "$GATEWAY/$PREFIX" dev "$BRIDGE"; ok "gave this host address $GATEWAY on $BRIDGE (boxes use it as their gateway)"; fi
|
|
72
|
+
if bridge_is_up; then skip "$BRIDGE is already up"; else ip link set "$BRIDGE" up; ok "brought $BRIDGE up"; fi
|
|
73
|
+
if [[ "$(sysctl -n net.ipv4.ip_forward)" == 1 ]]; then skip "IP forwarding already enabled"; else sysctl -q -w net.ipv4.ip_forward=1; ok "enabled IP forwarding (this host routes for the boxes)"; fi
|
|
74
|
+
local i
|
|
75
|
+
for i in "${!RULE_SPEC[@]}"; do
|
|
76
|
+
# shellcheck disable=SC2086 # the rule is a word list by design
|
|
77
|
+
if has_rule ${RULE_SPEC[$i]}; then skip "${RULE_DESC[$i]} (already set)"; else add_rule ${RULE_SPEC[$i]}; ok "${RULE_DESC[$i]}"; fi
|
|
78
|
+
done
|
|
79
|
+
echo
|
|
80
|
+
ok "host network ready: boxes get addresses in $SUBNET on $BRIDGE, gateway $GATEWAY, internet via NAT"
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# ── down ─────────────────────────────────────────────────────────────────────
|
|
84
|
+
do_down() {
|
|
85
|
+
local i
|
|
86
|
+
for i in "${!RULE_SPEC[@]}"; do
|
|
87
|
+
# shellcheck disable=SC2086
|
|
88
|
+
if has_rule ${RULE_SPEC[$i]}; then del_rule ${RULE_SPEC[$i]}; ok "removed: ${RULE_DESC[$i]}"; else skip "not set: ${RULE_DESC[$i]}"; fi
|
|
89
|
+
done
|
|
90
|
+
if bridge_exists; then
|
|
91
|
+
local taps; taps="$(taps_on_bridge)"
|
|
92
|
+
[[ -z "$taps" ]] || printf '! detaching taps still on %s: %s\n' "$BRIDGE" "$taps"
|
|
93
|
+
ip link del "$BRIDGE"; ok "deleted bridge $BRIDGE"
|
|
94
|
+
else
|
|
95
|
+
skip "bridge $BRIDGE does not exist"
|
|
96
|
+
fi
|
|
97
|
+
# IP forwarding is left as is: other software (Docker) relies on it too.
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# ── status ───────────────────────────────────────────────────────────────────
|
|
101
|
+
do_status() {
|
|
102
|
+
local missing=0 i
|
|
103
|
+
bad() { printf '%s✗%s %s\n' "$RED" "$NC" "$*"; missing=1; }
|
|
104
|
+
if bridge_exists && bridge_has_ip && bridge_is_up; then ok "bridge $BRIDGE is up with address $GATEWAY/$PREFIX"
|
|
105
|
+
elif ! bridge_exists; then bad "bridge $BRIDGE does not exist"
|
|
106
|
+
elif ! bridge_has_ip; then bad "bridge $BRIDGE has no address $GATEWAY"
|
|
107
|
+
else bad "bridge $BRIDGE is down"; fi
|
|
108
|
+
if [[ "$(sysctl -n net.ipv4.ip_forward)" == 1 ]]; then ok "IP forwarding enabled"; else bad "IP forwarding disabled"; fi
|
|
109
|
+
for i in "${!RULE_SPEC[@]}"; do
|
|
110
|
+
# shellcheck disable=SC2086
|
|
111
|
+
if has_rule ${RULE_SPEC[$i]}; then ok "${RULE_DESC[$i]}"; else bad "missing: ${RULE_DESC[$i]}"; fi
|
|
112
|
+
done
|
|
113
|
+
local taps; taps="$(taps_on_bridge)"
|
|
114
|
+
if [[ -z "$taps" ]]; then skip "no boxes attached to $BRIDGE yet"; else ok "boxes attached to $BRIDGE: $taps"; fi
|
|
115
|
+
if [[ $missing -eq 0 ]]; then echo; ok "host network is ready"; else echo; printf '%s✗%s host network incomplete; run: kvbox-server net up\n' "$RED" "$NC"; fi
|
|
116
|
+
return $missing
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
# ── tap add|del ──────────────────────────────────────────────────────────────
|
|
120
|
+
# One tap per box: a virtual cable with one end in the VM and the other plugged into the bridge.
|
|
121
|
+
do_tap_add() {
|
|
122
|
+
local tap=$1 owner="${KVBOX_TAP_USER:-${SUDO_USER:-root}}"
|
|
123
|
+
bridge_exists || fail "bridge $BRIDGE does not exist; run: kvbox-server net up"
|
|
124
|
+
if [[ -d "/sys/class/net/$tap" ]]; then
|
|
125
|
+
skip "tap $tap already exists"
|
|
126
|
+
else
|
|
127
|
+
ip tuntap add dev "$tap" mode tap user "$owner"
|
|
128
|
+
ok "created tap $tap (owner $owner)"
|
|
129
|
+
fi
|
|
130
|
+
ip link set "$tap" master "$BRIDGE" up
|
|
131
|
+
ok "tap $tap plugged into $BRIDGE"
|
|
132
|
+
}
|
|
133
|
+
do_tap_del() {
|
|
134
|
+
local tap=$1
|
|
135
|
+
if [[ -d "/sys/class/net/$tap" ]]; then ip link del "$tap"; ok "deleted tap $tap"; else skip "tap $tap does not exist"; fi
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
case "$action" in
|
|
139
|
+
up) do_up ;;
|
|
140
|
+
down) do_down ;;
|
|
141
|
+
status) do_status ;;
|
|
142
|
+
tap) if [[ "$tap_op" == add ]]; then do_tap_add "$tap_name"; else do_tap_del "$tap_name"; fi ;;
|
|
143
|
+
esac
|