@ghalex/kvbox-server 0.0.15 → 0.0.16
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 +300 -152
- package/dist/kvbox-server.js.map +1 -1
- package/package.json +1 -1
package/dist/kvbox-server.js
CHANGED
|
@@ -47,31 +47,40 @@ function paths(home = kvboxHome()) {
|
|
|
47
47
|
baseImage: (version) => join(home, "assets", `rootfs-${version}.ext4`),
|
|
48
48
|
machines: join(home, "machines"),
|
|
49
49
|
snapshots: join(home, "snapshots"),
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
50
|
+
/** Boxes and snapshots are filed by owner: machines/<owner>/<name>. The owner is part of a box's identity. */
|
|
51
|
+
ownerMachines: (owner) => join(home, "machines", owner),
|
|
52
|
+
ownerSnapshots: (owner) => join(home, "snapshots", owner),
|
|
53
|
+
machine: (owner, name) => {
|
|
54
|
+
const dir = join(home, "machines", owner, name);
|
|
55
|
+
return {
|
|
56
|
+
dir,
|
|
57
|
+
meta: join(dir, "meta.json"),
|
|
58
|
+
/** The box's writable layer, stacked on the read-only base image inside the guest. */
|
|
59
|
+
overlay: join(dir, "overlay.ext4"),
|
|
60
|
+
/** Boxes made before overlays: a full copy of the base image. Kept bootable, never created anymore. */
|
|
61
|
+
disk: join(dir, "disk.ext4"),
|
|
62
|
+
/**
|
|
63
|
+
* The box's memory as of its last snapshot (a hard link to that snapshot's `mem`, never modified).
|
|
64
|
+
* The next snapshot is a diff of the pages dirtied since, merged onto this. Absent after a boot.
|
|
65
|
+
*/
|
|
66
|
+
memBase: join(dir, "mem.base"),
|
|
67
|
+
sock: join(dir, "fc.sock"),
|
|
68
|
+
/** Firecracker's vsock endpoint: the host connects here to reach kvbox-agent inside the box (core/agent.ts). */
|
|
69
|
+
vsock: join(dir, "vsock.sock"),
|
|
70
|
+
log: join(dir, "fc.log")
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
snapshot: (owner, name) => {
|
|
74
|
+
const dir = join(home, "snapshots", owner, name);
|
|
75
|
+
return {
|
|
76
|
+
dir,
|
|
77
|
+
meta: join(dir, "meta.json"),
|
|
78
|
+
vmstate: join(dir, "vmstate"),
|
|
79
|
+
mem: join(dir, "mem"),
|
|
80
|
+
/** Copy of the box's overlay at the moment of the snapshot; the base image is referenced by version in meta.json. */
|
|
81
|
+
overlay: join(dir, "overlay.ext4")
|
|
82
|
+
};
|
|
83
|
+
}
|
|
75
84
|
};
|
|
76
85
|
}
|
|
77
86
|
|
|
@@ -91,6 +100,36 @@ function isSizeName(value) {
|
|
|
91
100
|
return value in SIZES;
|
|
92
101
|
}
|
|
93
102
|
|
|
103
|
+
// src/core/refs.ts
|
|
104
|
+
import { userInfo } from "os";
|
|
105
|
+
function shown(ref) {
|
|
106
|
+
return ref.display ?? ref.name;
|
|
107
|
+
}
|
|
108
|
+
var LOCAL_OWNER_RE = /^[A-Za-z0-9_][A-Za-z0-9._-]{0,63}$/;
|
|
109
|
+
var OWNER_RE = /^[A-Za-z0-9_][A-Za-z0-9._-]{0,63}(@[A-Za-z0-9.-]{1,128})?$/;
|
|
110
|
+
function localOwner() {
|
|
111
|
+
try {
|
|
112
|
+
const name = userInfo().username;
|
|
113
|
+
if (LOCAL_OWNER_RE.test(name)) return name;
|
|
114
|
+
} catch {
|
|
115
|
+
}
|
|
116
|
+
const env = process.env["USER"];
|
|
117
|
+
return env !== void 0 && LOCAL_OWNER_RE.test(env) ? env : "kvbox";
|
|
118
|
+
}
|
|
119
|
+
function parseRef(text, caller) {
|
|
120
|
+
const slash = text.indexOf("/");
|
|
121
|
+
if (slash === -1) return { owner: caller, name: text, display: text };
|
|
122
|
+
const owner = text.slice(0, slash);
|
|
123
|
+
const name = text.slice(slash + 1);
|
|
124
|
+
if (!OWNER_RE.test(owner) || name.length === 0 || name.includes("/")) {
|
|
125
|
+
throw new KvboxError(`invalid box reference "${text}": use <name> or <owner>/<name>`);
|
|
126
|
+
}
|
|
127
|
+
return { owner, name, display: owner === caller ? name : text };
|
|
128
|
+
}
|
|
129
|
+
function refKey(ref) {
|
|
130
|
+
return `${ref.owner}/${ref.name}`;
|
|
131
|
+
}
|
|
132
|
+
|
|
94
133
|
// src/core/config.ts
|
|
95
134
|
var ConfigSchema = z.object({
|
|
96
135
|
kernel: z.string(),
|
|
@@ -101,14 +140,17 @@ var ConfigSchema = z.object({
|
|
|
101
140
|
bridge: z.string().default("kvbox0"),
|
|
102
141
|
subnet: z.string().default("172.30.0.0/24"),
|
|
103
142
|
gateway: z.string().default("172.30.0.1"),
|
|
104
|
-
defaultSize: z.enum(SIZE_NAMES).default(DEFAULT_SIZE)
|
|
143
|
+
defaultSize: z.enum(SIZE_NAMES).default(DEFAULT_SIZE),
|
|
144
|
+
/** Who boxes made over the local socket belong to: the Unix user running the server unless set here. No `@`, no `/`. */
|
|
145
|
+
owner: z.string().regex(LOCAL_OWNER_RE, "owner: letters, digits, '.', '_', '-'; no '@' or '/'").default(localOwner)
|
|
105
146
|
});
|
|
106
147
|
function defaultConfig(p = paths()) {
|
|
107
148
|
return ConfigSchema.parse({
|
|
108
149
|
kernel: p.kernel,
|
|
109
150
|
rootfs: p.rootfs,
|
|
110
151
|
firecracker: p.firecracker,
|
|
111
|
-
snapshotEditor: p.snapshotEditor
|
|
152
|
+
snapshotEditor: p.snapshotEditor,
|
|
153
|
+
owner: localOwner()
|
|
112
154
|
});
|
|
113
155
|
}
|
|
114
156
|
async function loadConfig(p = paths()) {
|
|
@@ -219,7 +261,7 @@ function macFor(ip) {
|
|
|
219
261
|
}
|
|
220
262
|
|
|
221
263
|
// src/listen.ts
|
|
222
|
-
import { rm as rm3, writeFile as
|
|
264
|
+
import { rm as rm3, writeFile as writeFile5 } from "fs/promises";
|
|
223
265
|
import { createAdaptorServer } from "@hono/node-server";
|
|
224
266
|
|
|
225
267
|
// src/core/state.ts
|
|
@@ -227,6 +269,8 @@ import { mkdir, readFile as readFile2, readdir, writeFile as writeFile2 } from "
|
|
|
227
269
|
import { z as z2 } from "zod";
|
|
228
270
|
var MachineMetaSchema = z2.object({
|
|
229
271
|
name: z2.string(),
|
|
272
|
+
/** Part of the box's identity: machines/<owner>/<name>. See refs.ts. */
|
|
273
|
+
owner: z2.string(),
|
|
230
274
|
ip: z2.string(),
|
|
231
275
|
mac: z2.string(),
|
|
232
276
|
pid: z2.number().int().nullable(),
|
|
@@ -246,6 +290,8 @@ var MachineMetaSchema = z2.object({
|
|
|
246
290
|
});
|
|
247
291
|
var SnapshotMetaSchema = z2.object({
|
|
248
292
|
name: z2.string(),
|
|
293
|
+
owner: z2.string(),
|
|
294
|
+
/** The box it was taken from, as owner/name. */
|
|
249
295
|
source: z2.string(),
|
|
250
296
|
base: z2.string(),
|
|
251
297
|
vcpu: z2.number().int(),
|
|
@@ -271,25 +317,36 @@ async function listDirs(dir) {
|
|
|
271
317
|
throw err;
|
|
272
318
|
}
|
|
273
319
|
}
|
|
274
|
-
async function readMachineMeta(
|
|
275
|
-
return readJson(p.machine(name).meta, MachineMetaSchema);
|
|
320
|
+
async function readMachineMeta(ref, p = paths()) {
|
|
321
|
+
return readJson(p.machine(ref.owner, ref.name).meta, MachineMetaSchema);
|
|
276
322
|
}
|
|
277
323
|
async function writeMachineMeta(meta, p = paths()) {
|
|
278
|
-
|
|
279
|
-
await
|
|
324
|
+
const m = p.machine(meta.owner, meta.name);
|
|
325
|
+
await mkdir(m.dir, { recursive: true });
|
|
326
|
+
await writeFile2(m.meta, JSON.stringify(meta, null, 2) + "\n");
|
|
280
327
|
}
|
|
281
|
-
async function
|
|
282
|
-
return
|
|
328
|
+
async function listMachineRefs(p = paths()) {
|
|
329
|
+
return listRefs(p.machines);
|
|
283
330
|
}
|
|
284
|
-
async function readSnapshotMeta(
|
|
285
|
-
return readJson(p.snapshot(name).meta, SnapshotMetaSchema);
|
|
331
|
+
async function readSnapshotMeta(ref, p = paths()) {
|
|
332
|
+
return readJson(p.snapshot(ref.owner, ref.name).meta, SnapshotMetaSchema);
|
|
286
333
|
}
|
|
287
334
|
async function writeSnapshotMeta(meta, p = paths()) {
|
|
288
|
-
|
|
289
|
-
await
|
|
335
|
+
const s = p.snapshot(meta.owner, meta.name);
|
|
336
|
+
await mkdir(s.dir, { recursive: true });
|
|
337
|
+
await writeFile2(s.meta, JSON.stringify(meta, null, 2) + "\n");
|
|
338
|
+
}
|
|
339
|
+
async function listSnapshotRefs(p = paths()) {
|
|
340
|
+
return listRefs(p.snapshots);
|
|
290
341
|
}
|
|
291
|
-
async function
|
|
292
|
-
|
|
342
|
+
async function listRefs(root) {
|
|
343
|
+
const out = [];
|
|
344
|
+
for (const owner of await listDirs(root)) {
|
|
345
|
+
for (const name of await listDirs(`${root}/${owner}`)) {
|
|
346
|
+
out.push({ owner, name });
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return out;
|
|
293
350
|
}
|
|
294
351
|
function pidAlive(pid) {
|
|
295
352
|
if (pid === null || pid <= 0) return false;
|
|
@@ -301,6 +358,62 @@ function pidAlive(pid) {
|
|
|
301
358
|
}
|
|
302
359
|
}
|
|
303
360
|
|
|
361
|
+
// src/core/migrate.ts
|
|
362
|
+
import { existsSync as existsSync2 } from "fs";
|
|
363
|
+
import { mkdir as mkdir2, readdir as readdir2, readFile as readFile3, rename, writeFile as writeFile3 } from "fs/promises";
|
|
364
|
+
import { join as join2 } from "path";
|
|
365
|
+
async function migrateOwnerLayout(owner, p = paths()) {
|
|
366
|
+
const done = { boxes: [], snapshots: [], conflicts: [] };
|
|
367
|
+
for (const name of await oldDepthEntries(p.machines)) {
|
|
368
|
+
const from = join2(p.machines, name);
|
|
369
|
+
const to = p.machine(owner, name).dir;
|
|
370
|
+
if (existsSync2(to)) {
|
|
371
|
+
done.conflicts.push(`machines/${name}`);
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
await patchJson(join2(from, "meta.json"), (meta) => ({
|
|
375
|
+
...meta,
|
|
376
|
+
owner,
|
|
377
|
+
// a restored box names its snapshot: that moved under the same owner
|
|
378
|
+
source: typeof meta["source"] === "string" && meta["source"].startsWith("snapshot:") && !meta["source"].includes("/") ? `snapshot:${owner}/${meta["source"].slice("snapshot:".length)}` : meta["source"]
|
|
379
|
+
}));
|
|
380
|
+
await mkdir2(p.ownerMachines(owner), { recursive: true, mode: 448 });
|
|
381
|
+
await rename(from, to);
|
|
382
|
+
done.boxes.push(name);
|
|
383
|
+
}
|
|
384
|
+
for (const name of await oldDepthEntries(p.snapshots)) {
|
|
385
|
+
const from = join2(p.snapshots, name);
|
|
386
|
+
const to = p.snapshot(owner, name).dir;
|
|
387
|
+
if (existsSync2(to)) {
|
|
388
|
+
done.conflicts.push(`snapshots/${name}`);
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
await patchJson(join2(from, "meta.json"), (meta) => ({
|
|
392
|
+
...meta,
|
|
393
|
+
owner,
|
|
394
|
+
source: typeof meta["source"] === "string" && !meta["source"].includes("/") ? `${owner}/${meta["source"]}` : meta["source"]
|
|
395
|
+
}));
|
|
396
|
+
await mkdir2(p.ownerSnapshots(owner), { recursive: true, mode: 448 });
|
|
397
|
+
await rename(from, to);
|
|
398
|
+
done.snapshots.push(name);
|
|
399
|
+
}
|
|
400
|
+
return done;
|
|
401
|
+
}
|
|
402
|
+
async function oldDepthEntries(root) {
|
|
403
|
+
let entries;
|
|
404
|
+
try {
|
|
405
|
+
entries = await readdir2(root, { withFileTypes: true });
|
|
406
|
+
} catch (err) {
|
|
407
|
+
if (err.code === "ENOENT") return [];
|
|
408
|
+
throw err;
|
|
409
|
+
}
|
|
410
|
+
return entries.filter((e) => e.isDirectory() && existsSync2(join2(root, e.name, "meta.json"))).map((e) => e.name).sort();
|
|
411
|
+
}
|
|
412
|
+
async function patchJson(file, patch) {
|
|
413
|
+
const raw = JSON.parse(await readFile3(file, "utf8"));
|
|
414
|
+
await writeFile3(file, JSON.stringify(patch(raw), null, 2) + "\n");
|
|
415
|
+
}
|
|
416
|
+
|
|
304
417
|
// src/app.ts
|
|
305
418
|
import { Hono as Hono4 } from "hono";
|
|
306
419
|
|
|
@@ -317,12 +430,21 @@ function versionOf(srcFileUrl, injected) {
|
|
|
317
430
|
}
|
|
318
431
|
|
|
319
432
|
// src/version.ts
|
|
320
|
-
var VERSION = versionOf(import.meta.url, true ? "0.0.
|
|
433
|
+
var VERSION = versionOf(import.meta.url, true ? "0.0.16" : void 0);
|
|
434
|
+
|
|
435
|
+
// src/caller.ts
|
|
436
|
+
function resolveCaller() {
|
|
437
|
+
return async (c, next) => {
|
|
438
|
+
const cfg = await loadConfig();
|
|
439
|
+
c.set("owner", cfg.owner);
|
|
440
|
+
await next();
|
|
441
|
+
};
|
|
442
|
+
}
|
|
321
443
|
|
|
322
444
|
// src/core/machines.ts
|
|
323
445
|
import { spawn } from "child_process";
|
|
324
|
-
import { closeSync, existsSync as
|
|
325
|
-
import { link, mkdir as
|
|
446
|
+
import { closeSync, existsSync as existsSync3, openSync, statSync } from "fs";
|
|
447
|
+
import { link, mkdir as mkdir3, readFile as readFile4, realpath, rm, truncate, writeFile as writeFile4 } from "fs/promises";
|
|
326
448
|
import { basename } from "path";
|
|
327
449
|
import { setTimeout as sleep } from "timers/promises";
|
|
328
450
|
|
|
@@ -385,9 +507,9 @@ function hostResources() {
|
|
|
385
507
|
}
|
|
386
508
|
async function runningReservations(p = paths()) {
|
|
387
509
|
const out = [];
|
|
388
|
-
for (const
|
|
510
|
+
for (const ref of await listMachineRefs(p)) {
|
|
389
511
|
try {
|
|
390
|
-
const meta = await readMachineMeta(
|
|
512
|
+
const meta = await readMachineMeta(ref, p);
|
|
391
513
|
if (pidAlive(meta.pid)) out.push({ name: meta.name, vcpu: meta.vcpu, memMib: meta.memMib });
|
|
392
514
|
} catch {
|
|
393
515
|
}
|
|
@@ -570,13 +692,14 @@ function diskStats(disk) {
|
|
|
570
692
|
}
|
|
571
693
|
}
|
|
572
694
|
function ownDisk(meta, p) {
|
|
573
|
-
const m = p.machine(meta.name);
|
|
695
|
+
const m = p.machine(meta.owner, meta.name);
|
|
574
696
|
return meta.base === void 0 ? m.disk : m.overlay;
|
|
575
697
|
}
|
|
576
698
|
function toMachine(meta, p = paths()) {
|
|
577
699
|
const alive = pidAlive(meta.pid);
|
|
578
700
|
return {
|
|
579
701
|
name: meta.name,
|
|
702
|
+
owner: meta.owner,
|
|
580
703
|
ip: meta.ip,
|
|
581
704
|
mac: meta.mac,
|
|
582
705
|
pid: alive ? meta.pid : null,
|
|
@@ -591,17 +714,17 @@ function toMachine(meta, p = paths()) {
|
|
|
591
714
|
}
|
|
592
715
|
async function readAllMeta(p) {
|
|
593
716
|
const metas = [];
|
|
594
|
-
for (const
|
|
717
|
+
for (const ref of await listMachineRefs(p)) {
|
|
595
718
|
try {
|
|
596
|
-
metas.push(await readMachineMeta(
|
|
719
|
+
metas.push(await readMachineMeta(ref, p));
|
|
597
720
|
} catch (err) {
|
|
598
|
-
console.error(`warning: skipping machines/${
|
|
721
|
+
console.error(`warning: skipping machines/${refKey(ref)}: ${err.message}`);
|
|
599
722
|
}
|
|
600
723
|
}
|
|
601
724
|
return metas;
|
|
602
725
|
}
|
|
603
|
-
async function listMachines(p = paths()) {
|
|
604
|
-
return (await readAllMeta(p)).map((m) => toMachine(m, p));
|
|
726
|
+
async function listMachines(owner, p = paths()) {
|
|
727
|
+
return (await readAllMeta(p)).filter((m) => owner === null || m.owner === owner).map((m) => toMachine(m, p));
|
|
605
728
|
}
|
|
606
729
|
function pickName(requested, taken) {
|
|
607
730
|
if (requested !== void 0) {
|
|
@@ -636,7 +759,7 @@ async function currentBase(cfg, p) {
|
|
|
636
759
|
function baseFileFor(meta, p) {
|
|
637
760
|
if (meta.base === void 0) return void 0;
|
|
638
761
|
const file = p.baseImage(meta.base);
|
|
639
|
-
if (!
|
|
762
|
+
if (!existsSync3(file)) throw new KvboxError(`box ${meta.name} runs on base image ${meta.base}, which is gone from ${p.assets}`);
|
|
640
763
|
return file;
|
|
641
764
|
}
|
|
642
765
|
var overlayCapable = /* @__PURE__ */ new Map();
|
|
@@ -650,7 +773,7 @@ async function assertOverlayCapable(baseFile, version) {
|
|
|
650
773
|
if (!ok) throw new KvboxError(`base image ${version} is too old for this server (no /sbin/overlay-init); rerun install-server.sh to get a newer one`);
|
|
651
774
|
}
|
|
652
775
|
async function createOverlay(file, gib) {
|
|
653
|
-
await
|
|
776
|
+
await writeFile4(file, "", { mode: 384 });
|
|
654
777
|
await truncate(file, gib * 1024 ** 3);
|
|
655
778
|
await run(["mkfs.ext4", "-q", "-F", "-m", "0", "-L", "kvbox-box", "-E", "lazy_itable_init=1,lazy_journal_init=1", file]);
|
|
656
779
|
}
|
|
@@ -664,19 +787,19 @@ async function waitFor(pred, timeoutMs, stepMs = 50) {
|
|
|
664
787
|
}
|
|
665
788
|
async function tailOf(file, lines = 5) {
|
|
666
789
|
try {
|
|
667
|
-
return (await
|
|
790
|
+
return (await readFile4(file, "utf8")).trim().split("\n").slice(-lines).join("\n");
|
|
668
791
|
} catch {
|
|
669
792
|
return "";
|
|
670
793
|
}
|
|
671
794
|
}
|
|
672
|
-
async function spawnFirecracker(
|
|
673
|
-
const m = p.machine(name);
|
|
795
|
+
async function spawnFirecracker(ref, cfg, p) {
|
|
796
|
+
const m = p.machine(ref.owner, ref.name);
|
|
674
797
|
await rm(m.sock, { force: true });
|
|
675
798
|
await rm(m.vsock, { force: true });
|
|
676
799
|
const logFd = openSync(m.log, "a");
|
|
677
800
|
let child;
|
|
678
801
|
try {
|
|
679
|
-
child = spawn(cfg.firecracker, ["--api-sock", m.sock, "--id", name, "--level", "Error"], {
|
|
802
|
+
child = spawn(cfg.firecracker, ["--api-sock", m.sock, "--id", ref.name, "--level", "Error"], {
|
|
680
803
|
cwd: m.dir,
|
|
681
804
|
detached: true,
|
|
682
805
|
stdio: ["ignore", logFd, logFd]
|
|
@@ -687,7 +810,7 @@ async function spawnFirecracker(name, cfg, p) {
|
|
|
687
810
|
child.unref();
|
|
688
811
|
const pid = child.pid;
|
|
689
812
|
if (pid === void 0) throw new KvboxError("could not start firecracker");
|
|
690
|
-
if (!await waitFor(() =>
|
|
813
|
+
if (!await waitFor(() => existsSync3(m.sock), 3e3)) {
|
|
691
814
|
killQuietly(pid);
|
|
692
815
|
throw new KvboxError(`firecracker did not open its API socket; last log lines:
|
|
693
816
|
${await tailOf(m.log)}`);
|
|
@@ -701,9 +824,9 @@ function killQuietly(pid) {
|
|
|
701
824
|
}
|
|
702
825
|
}
|
|
703
826
|
async function launch(spec, tap, cfg, pubkeyB64, p) {
|
|
704
|
-
const m = p.machine(spec.name);
|
|
827
|
+
const m = p.machine(spec.owner, spec.name);
|
|
705
828
|
const baseFile = baseFileFor(spec, p);
|
|
706
|
-
const pid = await spawnFirecracker(spec
|
|
829
|
+
const pid = await spawnFirecracker(spec, cfg, p);
|
|
707
830
|
try {
|
|
708
831
|
const fc = new FirecrackerClient(m.sock);
|
|
709
832
|
try {
|
|
@@ -727,10 +850,10 @@ async function launch(spec, tap, cfg, pubkeyB64, p) {
|
|
|
727
850
|
throw err;
|
|
728
851
|
}
|
|
729
852
|
}
|
|
730
|
-
async function loadSnapshot(spec,
|
|
731
|
-
const m = p.machine(spec.name);
|
|
732
|
-
const s = p.snapshot(
|
|
733
|
-
const pid = await spawnFirecracker(spec
|
|
853
|
+
async function loadSnapshot(spec, snapshot, tap, cfg, p) {
|
|
854
|
+
const m = p.machine(spec.owner, spec.name);
|
|
855
|
+
const s = p.snapshot(snapshot.owner, snapshot.name);
|
|
856
|
+
const pid = await spawnFirecracker(spec, cfg, p);
|
|
734
857
|
try {
|
|
735
858
|
const fc = new FirecrackerClient(m.sock);
|
|
736
859
|
try {
|
|
@@ -744,7 +867,7 @@ async function loadSnapshot(spec, snapshotName, tap, cfg, p) {
|
|
|
744
867
|
resume: false
|
|
745
868
|
});
|
|
746
869
|
} catch (err) {
|
|
747
|
-
if (/Unknown Vsock Device/.test(err.message)) throw new KvboxError(tooOldToRestore(
|
|
870
|
+
if (/Unknown Vsock Device/.test(err.message)) throw new KvboxError(tooOldToRestore(shown(snapshot)));
|
|
748
871
|
throw err;
|
|
749
872
|
}
|
|
750
873
|
if (await namesOverlayAbsolutely(s.vmstate)) await fc.patchDrivePath("overlay", m.overlay);
|
|
@@ -764,23 +887,23 @@ async function currentBaseVersion(p = paths()) {
|
|
|
764
887
|
return (await currentBase(await loadConfig(p), p)).version;
|
|
765
888
|
}
|
|
766
889
|
async function namesOverlayAbsolutely(vmstate) {
|
|
767
|
-
return (await
|
|
890
|
+
return (await readFile4(vmstate)).includes(`/${OVERLAY_REL}`);
|
|
768
891
|
}
|
|
769
892
|
async function readPubkeyB64(p) {
|
|
770
893
|
try {
|
|
771
|
-
return Buffer.from((await
|
|
894
|
+
return Buffer.from((await readFile4(`${p.sshKey}.pub`, "utf8")).trim()).toString("base64");
|
|
772
895
|
} catch {
|
|
773
896
|
throw new KvboxError(`SSH key missing at ${p.sshKey}.pub; run install-server.sh`);
|
|
774
897
|
}
|
|
775
898
|
}
|
|
776
899
|
function assertAssets(cfg) {
|
|
777
900
|
for (const [what, file] of [["kernel", cfg.kernel], ["base image", cfg.rootfs], ["firecracker", cfg.firecracker]]) {
|
|
778
|
-
if (!
|
|
901
|
+
if (!existsSync3(file)) throw new KvboxError(`${what} missing at ${file}; run install-server.sh`);
|
|
779
902
|
}
|
|
780
903
|
}
|
|
781
|
-
async function createMachine(req, p = paths()) {
|
|
904
|
+
async function createMachine(req, owner, p = paths()) {
|
|
782
905
|
const cfg = await loadConfig(p);
|
|
783
|
-
if (req.from !== void 0) return restoreMachine(req.from, req, cfg, p);
|
|
906
|
+
if (req.from !== void 0) return restoreMachine(req.from, owner, req, cfg, p);
|
|
784
907
|
const sizeName = req.size ?? cfg.defaultSize;
|
|
785
908
|
if (!isSizeName(sizeName)) throw new KvboxError(`unknown size "${sizeName}"; use one of ${Object.keys(SIZES).join(", ")}`);
|
|
786
909
|
const size = SIZES[sizeName];
|
|
@@ -788,20 +911,20 @@ async function createMachine(req, p = paths()) {
|
|
|
788
911
|
await assertRoomFor(size.vcpu, size.memMib, req.force ?? false, p);
|
|
789
912
|
const pubkeyB64 = await readPubkeyB64(p);
|
|
790
913
|
const existing = await readAllMeta(p);
|
|
791
|
-
const name = pickName(req.name, new Set(existing.map((m2) => m2.name)));
|
|
914
|
+
const name = pickName(req.name, new Set(existing.filter((m2) => m2.owner === owner).map((m2) => m2.name)));
|
|
792
915
|
const ip = nextFreeIp(cfg.subnet, [...existing.map((m2) => m2.ip), cfg.gateway]);
|
|
793
916
|
const mac = macFor(ip);
|
|
794
|
-
const m = p.machine(name);
|
|
917
|
+
const m = p.machine(owner, name);
|
|
795
918
|
const base = await currentBase(cfg, p);
|
|
796
919
|
await assertOverlayCapable(base.file, base.version);
|
|
797
920
|
await ensureHostNetwork(cfg);
|
|
798
|
-
await
|
|
921
|
+
await mkdir3(m.dir, { recursive: true, mode: 448 });
|
|
799
922
|
let tapCreated = false;
|
|
800
923
|
try {
|
|
801
924
|
await createOverlay(m.overlay, size.diskGib);
|
|
802
925
|
const tap = await createTap(name, cfg);
|
|
803
926
|
tapCreated = true;
|
|
804
|
-
const spec = { name, ip, mac, vcpu: size.vcpu, memMib: size.memMib, base: base.version };
|
|
927
|
+
const spec = { name, owner, ip, mac, vcpu: size.vcpu, memMib: size.memMib, base: base.version };
|
|
805
928
|
const pid = await launch(spec, tap, cfg, pubkeyB64, p);
|
|
806
929
|
const meta = { ...spec, pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), source: "base" };
|
|
807
930
|
await writeMachineMeta(meta, p);
|
|
@@ -812,35 +935,36 @@ async function createMachine(req, p = paths()) {
|
|
|
812
935
|
throw err;
|
|
813
936
|
}
|
|
814
937
|
}
|
|
815
|
-
async function readSnap(
|
|
938
|
+
async function readSnap(ref, p) {
|
|
816
939
|
try {
|
|
817
|
-
return await readSnapshotMeta(
|
|
940
|
+
return await readSnapshotMeta(ref, p);
|
|
818
941
|
} catch (err) {
|
|
819
|
-
if (err.code === "ENOENT") throw new NotFoundError(`snapshot ${
|
|
942
|
+
if (err.code === "ENOENT") throw new NotFoundError(`snapshot ${shown(ref)}`);
|
|
820
943
|
throw err;
|
|
821
944
|
}
|
|
822
945
|
}
|
|
823
946
|
function prefixLenOf(subnet) {
|
|
824
947
|
return Number(subnet.split("/")[1]);
|
|
825
948
|
}
|
|
826
|
-
async function restoreMachine(
|
|
949
|
+
async function restoreMachine(fromText, owner, req, cfg, p) {
|
|
950
|
+
const from = parseRef(fromText, owner);
|
|
827
951
|
const snap = await readSnap(from, p);
|
|
828
952
|
if (req.size !== void 0) {
|
|
829
|
-
throw new KvboxError(`--size cannot be combined with --from: a restored box has the snapshot's size (${from}: ${snap.vcpu} vCPU, ${snap.memMib} MiB)`);
|
|
953
|
+
throw new KvboxError(`--size cannot be combined with --from: a restored box has the snapshot's size (${shown(from)}: ${snap.vcpu} vCPU, ${snap.memMib} MiB)`);
|
|
830
954
|
}
|
|
831
955
|
assertAssets(cfg);
|
|
832
|
-
if (!
|
|
833
|
-
throw new KvboxError(`snapshot ${from} stacks on base image ${snap.base}, which is gone from ${p.assets}`);
|
|
956
|
+
if (!existsSync3(p.baseImage(snap.base))) {
|
|
957
|
+
throw new KvboxError(`snapshot ${shown(from)} stacks on base image ${snap.base}, which is gone from ${p.assets}`);
|
|
834
958
|
}
|
|
835
959
|
await assertRoomFor(snap.vcpu, snap.memMib, req.force ?? false, p);
|
|
836
960
|
const existing = await readAllMeta(p);
|
|
837
|
-
const name = pickName(req.name, new Set(existing.map((m2) => m2.name)));
|
|
961
|
+
const name = pickName(req.name, new Set(existing.filter((m2) => m2.owner === owner).map((m2) => m2.name)));
|
|
838
962
|
const ip = nextFreeIp(cfg.subnet, [...existing.map((m2) => m2.ip), cfg.gateway]);
|
|
839
963
|
const mac = macFor(ip);
|
|
840
|
-
const m = p.machine(name);
|
|
841
|
-
const s = p.snapshot(from);
|
|
964
|
+
const m = p.machine(owner, name);
|
|
965
|
+
const s = p.snapshot(from.owner, from.name);
|
|
842
966
|
await ensureHostNetwork(cfg);
|
|
843
|
-
await
|
|
967
|
+
await mkdir3(m.dir, { recursive: true, mode: 448 });
|
|
844
968
|
let tapCreated = false;
|
|
845
969
|
let pid;
|
|
846
970
|
try {
|
|
@@ -848,7 +972,7 @@ async function restoreMachine(from, req, cfg, p) {
|
|
|
848
972
|
await link(s.mem, m.memBase);
|
|
849
973
|
const tap = await createTap(name, cfg, { detached: true });
|
|
850
974
|
tapCreated = true;
|
|
851
|
-
const spec = { name, ip, mac, vcpu: snap.vcpu, memMib: snap.memMib, base: snap.base };
|
|
975
|
+
const spec = { name, owner, ip, mac, vcpu: snap.vcpu, memMib: snap.memMib, base: snap.base };
|
|
852
976
|
pid = await loadSnapshot(spec, from, tap, cfg, p);
|
|
853
977
|
const fc = new FirecrackerClient(m.sock);
|
|
854
978
|
try {
|
|
@@ -856,9 +980,9 @@ async function restoreMachine(from, req, cfg, p) {
|
|
|
856
980
|
} finally {
|
|
857
981
|
await fc.close();
|
|
858
982
|
}
|
|
859
|
-
await introduce(spec, m.vsock, cfg, from);
|
|
983
|
+
await introduce(spec, m.vsock, cfg, shown(from));
|
|
860
984
|
await attachTap(name, cfg);
|
|
861
|
-
const meta = { ...spec, pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), source: `snapshot:${from}`, memSnapshots: 1 };
|
|
985
|
+
const meta = { ...spec, pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), source: `snapshot:${refKey(from)}`, memSnapshots: 1 };
|
|
862
986
|
await writeMachineMeta(meta, p);
|
|
863
987
|
return toMachine(meta, p);
|
|
864
988
|
} catch (err) {
|
|
@@ -893,7 +1017,7 @@ async function introduce(spec, vsock, cfg, from) {
|
|
|
893
1017
|
async function shutdown(meta, cfg, p, graceMs) {
|
|
894
1018
|
const pid = meta.pid;
|
|
895
1019
|
if (pidAlive(pid)) {
|
|
896
|
-
const fc = new FirecrackerClient(p.machine(meta.name).sock);
|
|
1020
|
+
const fc = new FirecrackerClient(p.machine(meta.owner, meta.name).sock);
|
|
897
1021
|
try {
|
|
898
1022
|
await fc.sendCtrlAltDel();
|
|
899
1023
|
} catch {
|
|
@@ -910,34 +1034,35 @@ async function shutdown(meta, cfg, p, graceMs) {
|
|
|
910
1034
|
}
|
|
911
1035
|
}
|
|
912
1036
|
await deleteTap(meta.name, cfg);
|
|
913
|
-
|
|
914
|
-
await rm(
|
|
1037
|
+
const m = p.machine(meta.owner, meta.name);
|
|
1038
|
+
await rm(m.sock, { force: true });
|
|
1039
|
+
await rm(m.vsock, { force: true });
|
|
915
1040
|
}
|
|
916
|
-
async function stopMachine(
|
|
917
|
-
const meta = await readMeta(
|
|
1041
|
+
async function stopMachine(ref, p = paths()) {
|
|
1042
|
+
const meta = await readMeta(ref, p);
|
|
918
1043
|
const cfg = await loadConfig(p);
|
|
919
|
-
if (!pidAlive(meta.pid)) throw new KvboxError(`box ${
|
|
1044
|
+
if (!pidAlive(meta.pid)) throw new KvboxError(`box ${shown(ref)} is already stopped`);
|
|
920
1045
|
await shutdown(meta, cfg, p, 1e4);
|
|
921
|
-
await rm(p.machine(name).memBase, { force: true });
|
|
1046
|
+
await rm(p.machine(ref.owner, ref.name).memBase, { force: true });
|
|
922
1047
|
const stopped = { ...meta, pid: null, memSnapshots: void 0 };
|
|
923
1048
|
await writeMachineMeta(stopped, p);
|
|
924
1049
|
return toMachine(stopped, p);
|
|
925
1050
|
}
|
|
926
|
-
async function startMachine(
|
|
927
|
-
const meta = await readMeta(
|
|
1051
|
+
async function startMachine(ref, p = paths()) {
|
|
1052
|
+
const meta = await readMeta(ref, p);
|
|
928
1053
|
const cfg = await loadConfig(p);
|
|
929
|
-
if (pidAlive(meta.pid)) throw new KvboxError(`box ${
|
|
1054
|
+
if (pidAlive(meta.pid)) throw new KvboxError(`box ${shown(ref)} is already running`);
|
|
930
1055
|
assertAssets(cfg);
|
|
931
1056
|
const pubkeyB64 = await readPubkeyB64(p);
|
|
932
1057
|
await ensureHostNetwork(cfg);
|
|
933
|
-
const tap = await createTap(name, cfg);
|
|
1058
|
+
const tap = await createTap(ref.name, cfg);
|
|
934
1059
|
try {
|
|
935
1060
|
const pid = await launch(meta, tap, cfg, pubkeyB64, p);
|
|
936
1061
|
const started = { ...meta, pid };
|
|
937
1062
|
await writeMachineMeta(started, p);
|
|
938
1063
|
return toMachine(started, p);
|
|
939
1064
|
} catch (err) {
|
|
940
|
-
await deleteTap(name, cfg).catch(() => void 0);
|
|
1065
|
+
await deleteTap(ref.name, cfg).catch(() => void 0);
|
|
941
1066
|
throw err;
|
|
942
1067
|
}
|
|
943
1068
|
}
|
|
@@ -945,18 +1070,18 @@ async function applyAtNextBoot(meta, changes, cfg, p, whileStopped) {
|
|
|
945
1070
|
const wasRunning = pidAlive(meta.pid);
|
|
946
1071
|
if (wasRunning) {
|
|
947
1072
|
await shutdown(meta, cfg, p, 1e4);
|
|
948
|
-
await rm(p.machine(meta.name).memBase, { force: true });
|
|
1073
|
+
await rm(p.machine(meta.owner, meta.name).memBase, { force: true });
|
|
949
1074
|
}
|
|
950
1075
|
const updated = { ...meta, ...changes, pid: null, memSnapshots: void 0 };
|
|
951
1076
|
await writeMachineMeta(updated, p);
|
|
952
1077
|
if (whileStopped) await whileStopped();
|
|
953
|
-
return wasRunning ? startMachine(meta
|
|
1078
|
+
return wasRunning ? startMachine(meta, p) : toMachine(updated, p);
|
|
954
1079
|
}
|
|
955
|
-
async function updateMachine(
|
|
956
|
-
const meta = await readMeta(
|
|
1080
|
+
async function updateMachine(ref, p = paths()) {
|
|
1081
|
+
const meta = await readMeta(ref, p);
|
|
957
1082
|
const cfg = await loadConfig(p);
|
|
958
1083
|
if (meta.base === void 0) {
|
|
959
|
-
throw new KvboxError(`box ${
|
|
1084
|
+
throw new KvboxError(`box ${shown(ref)} was made before overlays and runs on its own disk copy; it cannot be moved to another base image`);
|
|
960
1085
|
}
|
|
961
1086
|
assertAssets(cfg);
|
|
962
1087
|
const base = await currentBase(cfg, p);
|
|
@@ -964,8 +1089,8 @@ async function updateMachine(name, p = paths()) {
|
|
|
964
1089
|
await assertOverlayCapable(base.file, base.version);
|
|
965
1090
|
return applyAtNextBoot(meta, { base: base.version }, cfg, p);
|
|
966
1091
|
}
|
|
967
|
-
async function rescaleMachine(
|
|
968
|
-
const meta = await readMeta(
|
|
1092
|
+
async function rescaleMachine(ref, req, p = paths()) {
|
|
1093
|
+
const meta = await readMeta(ref, p);
|
|
969
1094
|
const cfg = await loadConfig(p);
|
|
970
1095
|
const disk = ownDisk(meta, p);
|
|
971
1096
|
const currentDiskGib = Math.round(statSync(disk).size / 1024 ** 3);
|
|
@@ -976,7 +1101,7 @@ async function rescaleMachine(name, req, p = paths()) {
|
|
|
976
1101
|
diskGib: req.diskGib ?? preset?.diskGib ?? currentDiskGib
|
|
977
1102
|
};
|
|
978
1103
|
if (target.diskGib < currentDiskGib) {
|
|
979
|
-
throw new KvboxError(`box ${
|
|
1104
|
+
throw new KvboxError(`box ${shown(ref)} has a ${currentDiskGib} GB disk and a disk cannot shrink; ${target.diskGib} GB is smaller`);
|
|
980
1105
|
}
|
|
981
1106
|
const changes = {};
|
|
982
1107
|
if (target.vcpu !== meta.vcpu) changes.vcpu = target.vcpu;
|
|
@@ -990,19 +1115,19 @@ async function growDisk(file, gib) {
|
|
|
990
1115
|
await run(["e2fsck", "-f", "-p", file], { okExitCodes: [1] });
|
|
991
1116
|
await run(["resize2fs", file]);
|
|
992
1117
|
}
|
|
993
|
-
async function readMeta(
|
|
1118
|
+
async function readMeta(ref, p) {
|
|
994
1119
|
try {
|
|
995
|
-
return await readMachineMeta(
|
|
1120
|
+
return await readMachineMeta(ref, p);
|
|
996
1121
|
} catch (err) {
|
|
997
|
-
if (err.code === "ENOENT") throw new NotFoundError(`box ${
|
|
1122
|
+
if (err.code === "ENOENT") throw new NotFoundError(`box ${shown(ref)}`);
|
|
998
1123
|
throw err;
|
|
999
1124
|
}
|
|
1000
1125
|
}
|
|
1001
|
-
async function destroyMachine(
|
|
1002
|
-
const meta = await readMeta(
|
|
1126
|
+
async function destroyMachine(ref, p = paths()) {
|
|
1127
|
+
const meta = await readMeta(ref, p);
|
|
1003
1128
|
const cfg = await loadConfig(p);
|
|
1004
1129
|
await shutdown(meta, cfg, p, 2e3);
|
|
1005
|
-
await rm(p.machine(name).dir, { recursive: true, force: true });
|
|
1130
|
+
await rm(p.machine(ref.owner, ref.name).dir, { recursive: true, force: true });
|
|
1006
1131
|
}
|
|
1007
1132
|
|
|
1008
1133
|
// src/routes/capacity.ts
|
|
@@ -1021,6 +1146,8 @@ import { z as z3 } from "zod";
|
|
|
1021
1146
|
var SizeSchema = z3.enum(SIZE_NAMES);
|
|
1022
1147
|
var MachineSchema = z3.object({
|
|
1023
1148
|
name: z3.string(),
|
|
1149
|
+
/** Who the box belongs to; names are unique per owner. A bare name is local to the host, `user@control-plane` later. */
|
|
1150
|
+
owner: z3.string(),
|
|
1024
1151
|
ip: z3.string(),
|
|
1025
1152
|
mac: z3.string(),
|
|
1026
1153
|
pid: z3.number().int().nullable(),
|
|
@@ -1039,6 +1166,8 @@ var MachineSchema = z3.object({
|
|
|
1039
1166
|
});
|
|
1040
1167
|
var CreateMachineRequest = z3.object({
|
|
1041
1168
|
name: z3.string().optional(),
|
|
1169
|
+
/** Ignored over the local socket (the caller is the host owner); the control plane sets it over HTTPS. */
|
|
1170
|
+
owner: z3.string().optional(),
|
|
1042
1171
|
size: SizeSchema.optional(),
|
|
1043
1172
|
from: z3.string().optional(),
|
|
1044
1173
|
force: z3.boolean().optional()
|
|
@@ -1051,8 +1180,9 @@ var RescaleMachineRequest = z3.object({
|
|
|
1051
1180
|
});
|
|
1052
1181
|
var SnapshotSchema = z3.object({
|
|
1053
1182
|
name: z3.string(),
|
|
1183
|
+
owner: z3.string(),
|
|
1054
1184
|
source: z3.string(),
|
|
1055
|
-
// the box it was taken from
|
|
1185
|
+
// the box it was taken from, as owner/name
|
|
1056
1186
|
base: z3.string(),
|
|
1057
1187
|
// base image version the overlay stacks on; a restore needs the same one
|
|
1058
1188
|
vcpu: z3.number().int(),
|
|
@@ -1077,7 +1207,9 @@ var HealthSchema = z3.object({
|
|
|
1077
1207
|
version: z3.string(),
|
|
1078
1208
|
pid: z3.number().int(),
|
|
1079
1209
|
/** Base image version new boxes get (null when the host has no usable base image yet); absent from older servers. */
|
|
1080
|
-
base: z3.string().nullable().optional()
|
|
1210
|
+
base: z3.string().nullable().optional(),
|
|
1211
|
+
/** Owner that boxes made over the local socket get (the Unix user running the server); absent from older servers. */
|
|
1212
|
+
owner: z3.string().optional()
|
|
1081
1213
|
});
|
|
1082
1214
|
var ApiErrorSchema = z3.object({
|
|
1083
1215
|
error: z3.string()
|
|
@@ -1086,20 +1218,20 @@ var ApiErrorSchema = z3.object({
|
|
|
1086
1218
|
// src/routes/machines.ts
|
|
1087
1219
|
function machineRoutes() {
|
|
1088
1220
|
const r = new Hono2();
|
|
1089
|
-
r.get("/", async (c) => c.json(await listMachines()));
|
|
1221
|
+
r.get("/", async (c) => c.json(await listMachines(c.get("owner"))));
|
|
1090
1222
|
r.post("/", async (c) => {
|
|
1091
1223
|
const body = CreateMachineRequest.parse(await c.req.json().catch(() => ({})));
|
|
1092
|
-
return c.json(await createMachine(body), 201);
|
|
1224
|
+
return c.json(await createMachine(body, c.get("owner")), 201);
|
|
1093
1225
|
});
|
|
1094
|
-
r.post("/:name/stop", async (c) => c.json(await stopMachine(c.req.param("name"))));
|
|
1095
|
-
r.post("/:name/start", async (c) => c.json(await startMachine(c.req.param("name"))));
|
|
1096
|
-
r.post("/:name/update", async (c) => c.json(await updateMachine(c.req.param("name"))));
|
|
1226
|
+
r.post("/:name/stop", async (c) => c.json(await stopMachine(parseRef(c.req.param("name"), c.get("owner")))));
|
|
1227
|
+
r.post("/:name/start", async (c) => c.json(await startMachine(parseRef(c.req.param("name"), c.get("owner")))));
|
|
1228
|
+
r.post("/:name/update", async (c) => c.json(await updateMachine(parseRef(c.req.param("name"), c.get("owner")))));
|
|
1097
1229
|
r.post("/:name/rescale", async (c) => {
|
|
1098
1230
|
const body = RescaleMachineRequest.parse(await c.req.json().catch(() => ({})));
|
|
1099
|
-
return c.json(await rescaleMachine(c.req.param("name"), body));
|
|
1231
|
+
return c.json(await rescaleMachine(parseRef(c.req.param("name"), c.get("owner")), body));
|
|
1100
1232
|
});
|
|
1101
1233
|
r.delete("/:name", async (c) => {
|
|
1102
|
-
await destroyMachine(c.req.param("name"));
|
|
1234
|
+
await destroyMachine(parseRef(c.req.param("name"), c.get("owner")));
|
|
1103
1235
|
return c.body(null, 204);
|
|
1104
1236
|
});
|
|
1105
1237
|
return r;
|
|
@@ -1109,12 +1241,13 @@ function machineRoutes() {
|
|
|
1109
1241
|
import { Hono as Hono3 } from "hono";
|
|
1110
1242
|
|
|
1111
1243
|
// src/core/snapshots.ts
|
|
1112
|
-
import { existsSync as
|
|
1113
|
-
import { link as link2, mkdir as
|
|
1244
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1245
|
+
import { link as link2, mkdir as mkdir4, readdir as readdir3, rename as rename2, rm as rm2, stat } from "fs/promises";
|
|
1114
1246
|
var NAME_RE2 = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
|
1115
1247
|
function toSnapshot(meta) {
|
|
1116
1248
|
return {
|
|
1117
1249
|
name: meta.name,
|
|
1250
|
+
owner: meta.owner,
|
|
1118
1251
|
source: meta.source,
|
|
1119
1252
|
base: meta.base,
|
|
1120
1253
|
vcpu: meta.vcpu,
|
|
@@ -1123,54 +1256,55 @@ function toSnapshot(meta) {
|
|
|
1123
1256
|
createdAt: meta.createdAt
|
|
1124
1257
|
};
|
|
1125
1258
|
}
|
|
1126
|
-
async function listSnapshots(p = paths()) {
|
|
1259
|
+
async function listSnapshots(owner, p = paths()) {
|
|
1127
1260
|
const out = [];
|
|
1128
|
-
for (const
|
|
1261
|
+
for (const ref of await listSnapshotRefs(p)) {
|
|
1262
|
+
if (owner !== null && ref.owner !== owner) continue;
|
|
1129
1263
|
try {
|
|
1130
|
-
out.push(toSnapshot(await readSnapshotMeta(
|
|
1264
|
+
out.push(toSnapshot(await readSnapshotMeta(ref, p)));
|
|
1131
1265
|
} catch (err) {
|
|
1132
|
-
console.error(`warning: skipping snapshots/${
|
|
1266
|
+
console.error(`warning: skipping snapshots/${refKey(ref)}: ${err.message}`);
|
|
1133
1267
|
}
|
|
1134
1268
|
}
|
|
1135
1269
|
return out;
|
|
1136
1270
|
}
|
|
1137
1271
|
async function usedBytes(dir) {
|
|
1138
1272
|
let total = 0;
|
|
1139
|
-
for (const entry of await
|
|
1273
|
+
for (const entry of await readdir3(dir)) {
|
|
1140
1274
|
total += (await stat(`${dir}/${entry}`)).blocks * 512;
|
|
1141
1275
|
}
|
|
1142
1276
|
return total;
|
|
1143
1277
|
}
|
|
1144
|
-
async function readBox(
|
|
1278
|
+
async function readBox(ref, p) {
|
|
1145
1279
|
try {
|
|
1146
|
-
return await readMachineMeta(
|
|
1280
|
+
return await readMachineMeta(ref, p);
|
|
1147
1281
|
} catch (err) {
|
|
1148
|
-
if (err.code === "ENOENT") throw new NotFoundError(`box ${
|
|
1282
|
+
if (err.code === "ENOENT") throw new NotFoundError(`box ${shown(ref)}`);
|
|
1149
1283
|
throw err;
|
|
1150
1284
|
}
|
|
1151
1285
|
}
|
|
1152
|
-
async function saveSnapshot(req, p = paths()) {
|
|
1286
|
+
async function saveSnapshot(req, owner, p = paths()) {
|
|
1153
1287
|
if (!NAME_RE2.test(req.name)) {
|
|
1154
1288
|
throw new KvboxError(`invalid snapshot name "${req.name}": 1-64 chars, lowercase letters, digits, '.', '_' and '-', starting with a letter or digit`);
|
|
1155
1289
|
}
|
|
1156
|
-
const box = await readBox(req.box, p);
|
|
1290
|
+
const box = await readBox(parseRef(req.box, owner), p);
|
|
1157
1291
|
if (box.base === void 0) {
|
|
1158
1292
|
throw new KvboxError(`box ${req.box} was made before overlays and runs on its own disk copy; snapshots need a box made by this version of kvbox (kvbox new)`);
|
|
1159
1293
|
}
|
|
1160
1294
|
if (!pidAlive(box.pid)) {
|
|
1161
1295
|
throw new KvboxError(`box ${req.box} is not running; a snapshot freezes a running box (kvbox start ${req.box} first)`);
|
|
1162
1296
|
}
|
|
1163
|
-
const s = p.snapshot(req.name);
|
|
1164
|
-
if (
|
|
1297
|
+
const s = p.snapshot(owner, req.name);
|
|
1298
|
+
if (existsSync4(s.dir)) throw new KvboxError(`snapshot ${req.name} already exists`);
|
|
1165
1299
|
const cfg = await loadConfig(p);
|
|
1166
|
-
const m = p.machine(box.name);
|
|
1167
|
-
await
|
|
1300
|
+
const m = p.machine(box.owner, box.name);
|
|
1301
|
+
await mkdir4(s.dir, { recursive: true, mode: 448 });
|
|
1168
1302
|
const fc = new FirecrackerClient(m.sock);
|
|
1169
1303
|
let lineageTouched = false;
|
|
1170
1304
|
try {
|
|
1171
1305
|
const tracked = (await fc.machineConfigInfo()).track_dirty_pages;
|
|
1172
1306
|
const since = box.memSnapshots ?? 0;
|
|
1173
|
-
const mode = tracked && (since === 0 ||
|
|
1307
|
+
const mode = tracked && (since === 0 || existsSync4(m.memBase)) ? "Diff" : "Full";
|
|
1174
1308
|
let failure;
|
|
1175
1309
|
await fc.pause();
|
|
1176
1310
|
try {
|
|
@@ -1197,7 +1331,8 @@ async function saveSnapshot(req, p = paths()) {
|
|
|
1197
1331
|
}
|
|
1198
1332
|
const meta = {
|
|
1199
1333
|
name: req.name,
|
|
1200
|
-
|
|
1334
|
+
owner,
|
|
1335
|
+
source: refKey(box),
|
|
1201
1336
|
base: box.base,
|
|
1202
1337
|
vcpu: box.vcpu,
|
|
1203
1338
|
memMib: box.memMib,
|
|
@@ -1215,27 +1350,28 @@ async function saveSnapshot(req, p = paths()) {
|
|
|
1215
1350
|
}
|
|
1216
1351
|
}
|
|
1217
1352
|
async function mergeOntoPrevious(previous, diffFile, editor) {
|
|
1218
|
-
if (!
|
|
1353
|
+
if (!existsSync4(editor)) throw new KvboxError(`snapshot-editor missing at ${editor}; rerun install-server.sh`);
|
|
1219
1354
|
const merged = `${diffFile}.merged`;
|
|
1220
1355
|
await run(["cp", "--sparse=always", "--reflink=auto", previous, merged]);
|
|
1221
1356
|
await run([editor, "edit-memory", "rebase", "--memory-path", merged, "--diff-path", diffFile]);
|
|
1222
|
-
await
|
|
1357
|
+
await rename2(merged, diffFile);
|
|
1223
1358
|
}
|
|
1224
|
-
async function deleteSnapshot(
|
|
1225
|
-
|
|
1226
|
-
|
|
1359
|
+
async function deleteSnapshot(ref, p = paths()) {
|
|
1360
|
+
const dir = p.snapshot(ref.owner, ref.name).dir;
|
|
1361
|
+
if (!existsSync4(dir)) throw new NotFoundError(`snapshot ${shown(ref)}`);
|
|
1362
|
+
await rm2(dir, { recursive: true, force: true });
|
|
1227
1363
|
}
|
|
1228
1364
|
|
|
1229
1365
|
// src/routes/snapshots.ts
|
|
1230
1366
|
function snapshotRoutes() {
|
|
1231
1367
|
const r = new Hono3();
|
|
1232
|
-
r.get("/", async (c) => c.json(await listSnapshots()));
|
|
1368
|
+
r.get("/", async (c) => c.json(await listSnapshots(c.get("owner"))));
|
|
1233
1369
|
r.post("/", async (c) => {
|
|
1234
1370
|
const body = CreateSnapshotRequest.parse(await c.req.json());
|
|
1235
|
-
return c.json(await saveSnapshot(body), 201);
|
|
1371
|
+
return c.json(await saveSnapshot(body, c.get("owner")), 201);
|
|
1236
1372
|
});
|
|
1237
1373
|
r.delete("/:name", async (c) => {
|
|
1238
|
-
await deleteSnapshot(c.req.param("name"));
|
|
1374
|
+
await deleteSnapshot(parseRef(c.req.param("name"), c.get("owner")));
|
|
1239
1375
|
return c.body(null, 204);
|
|
1240
1376
|
});
|
|
1241
1377
|
return r;
|
|
@@ -1244,9 +1380,16 @@ function snapshotRoutes() {
|
|
|
1244
1380
|
// src/app.ts
|
|
1245
1381
|
function createApp() {
|
|
1246
1382
|
const app = new Hono4();
|
|
1383
|
+
app.use(resolveCaller());
|
|
1247
1384
|
app.get(
|
|
1248
1385
|
"/health",
|
|
1249
|
-
async (c) => c.json({
|
|
1386
|
+
async (c) => c.json({
|
|
1387
|
+
ok: true,
|
|
1388
|
+
version: VERSION,
|
|
1389
|
+
pid: process.pid,
|
|
1390
|
+
base: await currentBaseVersion().catch(() => null),
|
|
1391
|
+
owner: (await loadConfig()).owner
|
|
1392
|
+
})
|
|
1250
1393
|
);
|
|
1251
1394
|
app.route("/machines", machineRoutes());
|
|
1252
1395
|
app.route("/snapshots", snapshotRoutes());
|
|
@@ -1266,7 +1409,12 @@ function createApp() {
|
|
|
1266
1409
|
// src/listen.ts
|
|
1267
1410
|
async function listen(opts = {}) {
|
|
1268
1411
|
const p = await ensureLayout(paths());
|
|
1269
|
-
await loadConfig(p);
|
|
1412
|
+
const cfg = await loadConfig(p);
|
|
1413
|
+
const moved = await migrateOwnerLayout(cfg.owner, p);
|
|
1414
|
+
if (moved.boxes.length + moved.snapshots.length > 0) {
|
|
1415
|
+
console.error(`boxes and snapshots now belong to an owner: moved ${moved.boxes.length} box(es) and ${moved.snapshots.length} snapshot(s) under ${cfg.owner}/`);
|
|
1416
|
+
}
|
|
1417
|
+
for (const c of moved.conflicts) console.error(`warning: ${c} left in place: ${cfg.owner} already has one with that name`);
|
|
1270
1418
|
const socketPath = opts.socketPath ?? p.serverSock;
|
|
1271
1419
|
if (Buffer.byteLength(socketPath) > 100) {
|
|
1272
1420
|
throw new KvboxError(`socket path too long for a Unix socket (${socketPath}); set KVBOX_HOME to a shorter path`);
|
|
@@ -1277,7 +1425,7 @@ async function listen(opts = {}) {
|
|
|
1277
1425
|
server.once("error", reject);
|
|
1278
1426
|
server.listen(socketPath, () => resolve());
|
|
1279
1427
|
});
|
|
1280
|
-
await
|
|
1428
|
+
await writeFile5(p.serverPid, `${process.pid}
|
|
1281
1429
|
`);
|
|
1282
1430
|
const shutdown2 = async () => {
|
|
1283
1431
|
server.close();
|