@ghalex/kvbox-server 0.1.3 → 0.2.0
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 +167 -73
- package/dist/kvbox-server.js.map +1 -1
- package/package.json +1 -1
package/dist/kvbox-server.js
CHANGED
|
@@ -35,7 +35,8 @@ function paths(home = kvboxHome()) {
|
|
|
35
35
|
serverSock: join(home, "server.sock"),
|
|
36
36
|
serverPid: join(home, "server.pid"),
|
|
37
37
|
serverLog: join(home, "server.log"),
|
|
38
|
-
|
|
38
|
+
/** The CLI's login to the control plane (`kvbox login`): url, bearer token, user. Mode 600. */
|
|
39
|
+
cli: join(home, "cli.json"),
|
|
39
40
|
ssh: join(home, "ssh"),
|
|
40
41
|
sshKey: join(home, "ssh", "id_ed25519"),
|
|
41
42
|
bin: join(home, "bin"),
|
|
@@ -278,9 +279,16 @@ import { rm as rm3, writeFile as writeFile5 } from "node:fs/promises";
|
|
|
278
279
|
import { createAdaptorServer } from "@hono/node-server";
|
|
279
280
|
|
|
280
281
|
// src/core/state.ts
|
|
282
|
+
import { randomBytes } from "node:crypto";
|
|
281
283
|
import { mkdir, readFile as readFile2, readdir, writeFile as writeFile2 } from "node:fs/promises";
|
|
282
284
|
import { z as z2 } from "zod";
|
|
283
285
|
var MachineMetaSchema = z2.object({
|
|
286
|
+
/**
|
|
287
|
+
* Host-unique, minted at `kvbox new` (fresh or restored) and kept for the box's life; the tap is
|
|
288
|
+
* `kv-<id>`. Absent only on a box that predates ids and is still running on its `kv-<name>` tap:
|
|
289
|
+
* `migrateBoxIds` gives stopped ones theirs at server start, a running one gets it when it next starts.
|
|
290
|
+
*/
|
|
291
|
+
id: z2.string().optional(),
|
|
284
292
|
name: z2.string(),
|
|
285
293
|
/** Part of the box's identity: machines/<owner>/<name>. See refs.ts. */
|
|
286
294
|
owner: z2.string(),
|
|
@@ -303,6 +311,12 @@ var MachineMetaSchema = z2.object({
|
|
|
303
311
|
/** `kvbox pause`: Firecracker is alive but the VM is frozen (standby). Absent: not paused. */
|
|
304
312
|
paused: z2.boolean().optional()
|
|
305
313
|
});
|
|
314
|
+
function newBoxId(taken) {
|
|
315
|
+
for (; ; ) {
|
|
316
|
+
const id = randomBytes(3).toString("hex");
|
|
317
|
+
if (!taken.has(id)) return id;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
306
320
|
var SnapshotMetaSchema = z2.object({
|
|
307
321
|
name: z2.string(),
|
|
308
322
|
owner: z2.string(),
|
|
@@ -377,57 +391,6 @@ function pidAlive(pid) {
|
|
|
377
391
|
import { existsSync as existsSync2 } from "node:fs";
|
|
378
392
|
import { mkdir as mkdir2, readdir as readdir2, readFile as readFile3, rename, writeFile as writeFile3 } from "node:fs/promises";
|
|
379
393
|
import { join as join2 } from "node:path";
|
|
380
|
-
async function migrateOwnerLayout(owner, p = paths()) {
|
|
381
|
-
const done = { boxes: [], snapshots: [], conflicts: [] };
|
|
382
|
-
for (const name of await oldDepthEntries(p.machines)) {
|
|
383
|
-
const from = join2(p.machines, name);
|
|
384
|
-
const to = p.machine(owner, name).dir;
|
|
385
|
-
if (existsSync2(to)) {
|
|
386
|
-
done.conflicts.push(`machines/${name}`);
|
|
387
|
-
continue;
|
|
388
|
-
}
|
|
389
|
-
await patchJson(join2(from, "meta.json"), (meta) => ({
|
|
390
|
-
...meta,
|
|
391
|
-
owner,
|
|
392
|
-
// a restored box names its snapshot: that moved under the same owner
|
|
393
|
-
source: typeof meta["source"] === "string" && meta["source"].startsWith("snapshot:") && !meta["source"].includes("/") ? `snapshot:${owner}/${meta["source"].slice("snapshot:".length)}` : meta["source"]
|
|
394
|
-
}));
|
|
395
|
-
await mkdir2(p.ownerMachines(owner), { recursive: true, mode: 448 });
|
|
396
|
-
await rename(from, to);
|
|
397
|
-
done.boxes.push(name);
|
|
398
|
-
}
|
|
399
|
-
for (const name of await oldDepthEntries(p.snapshots)) {
|
|
400
|
-
const from = join2(p.snapshots, name);
|
|
401
|
-
const to = p.snapshot(owner, name).dir;
|
|
402
|
-
if (existsSync2(to)) {
|
|
403
|
-
done.conflicts.push(`snapshots/${name}`);
|
|
404
|
-
continue;
|
|
405
|
-
}
|
|
406
|
-
await patchJson(join2(from, "meta.json"), (meta) => ({
|
|
407
|
-
...meta,
|
|
408
|
-
owner,
|
|
409
|
-
source: typeof meta["source"] === "string" && !meta["source"].includes("/") ? `${owner}/${meta["source"]}` : meta["source"]
|
|
410
|
-
}));
|
|
411
|
-
await mkdir2(p.ownerSnapshots(owner), { recursive: true, mode: 448 });
|
|
412
|
-
await rename(from, to);
|
|
413
|
-
done.snapshots.push(name);
|
|
414
|
-
}
|
|
415
|
-
return done;
|
|
416
|
-
}
|
|
417
|
-
async function oldDepthEntries(root) {
|
|
418
|
-
let entries;
|
|
419
|
-
try {
|
|
420
|
-
entries = await readdir2(root, { withFileTypes: true });
|
|
421
|
-
} catch (err) {
|
|
422
|
-
if (err.code === "ENOENT") return [];
|
|
423
|
-
throw err;
|
|
424
|
-
}
|
|
425
|
-
return entries.filter((e) => e.isDirectory() && existsSync2(join2(root, e.name, "meta.json"))).map((e) => e.name).sort();
|
|
426
|
-
}
|
|
427
|
-
async function patchJson(file, patch) {
|
|
428
|
-
const raw = JSON.parse(await readFile3(file, "utf8"));
|
|
429
|
-
await writeFile3(file, JSON.stringify(patch(raw), null, 2) + "\n");
|
|
430
|
-
}
|
|
431
394
|
|
|
432
395
|
// src/core/db.ts
|
|
433
396
|
import { chmodSync, closeSync, mkdirSync, openSync } from "node:fs";
|
|
@@ -509,6 +472,91 @@ function usageEventsFor(boxId, since, p = paths()) {
|
|
|
509
472
|
const rows = openDb(p).prepare("select * from usage_events where box_id = ? and at >= ? order by at, id").all(boxId, since);
|
|
510
473
|
return rows.map(fromRaw);
|
|
511
474
|
}
|
|
475
|
+
function rekeyUsageEvents(from, to, p = paths()) {
|
|
476
|
+
return Number(openDb(p).prepare("update usage_events set box_id = ? where box_id = ?").run(to, from).changes);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// src/core/migrate.ts
|
|
480
|
+
async function migrateOwnerLayout(owner, p = paths()) {
|
|
481
|
+
const done = { boxes: [], snapshots: [], conflicts: [] };
|
|
482
|
+
for (const name of await oldDepthEntries(p.machines)) {
|
|
483
|
+
const from = join2(p.machines, name);
|
|
484
|
+
const to = p.machine(owner, name).dir;
|
|
485
|
+
if (existsSync2(to)) {
|
|
486
|
+
done.conflicts.push(`machines/${name}`);
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
await patchJson(join2(from, "meta.json"), (meta) => ({
|
|
490
|
+
...meta,
|
|
491
|
+
owner,
|
|
492
|
+
// a restored box names its snapshot: that moved under the same owner
|
|
493
|
+
source: typeof meta["source"] === "string" && meta["source"].startsWith("snapshot:") && !meta["source"].includes("/") ? `snapshot:${owner}/${meta["source"].slice("snapshot:".length)}` : meta["source"]
|
|
494
|
+
}));
|
|
495
|
+
await mkdir2(p.ownerMachines(owner), { recursive: true, mode: 448 });
|
|
496
|
+
await rename(from, to);
|
|
497
|
+
done.boxes.push(name);
|
|
498
|
+
}
|
|
499
|
+
for (const name of await oldDepthEntries(p.snapshots)) {
|
|
500
|
+
const from = join2(p.snapshots, name);
|
|
501
|
+
const to = p.snapshot(owner, name).dir;
|
|
502
|
+
if (existsSync2(to)) {
|
|
503
|
+
done.conflicts.push(`snapshots/${name}`);
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
await patchJson(join2(from, "meta.json"), (meta) => ({
|
|
507
|
+
...meta,
|
|
508
|
+
owner,
|
|
509
|
+
source: typeof meta["source"] === "string" && !meta["source"].includes("/") ? `${owner}/${meta["source"]}` : meta["source"]
|
|
510
|
+
}));
|
|
511
|
+
await mkdir2(p.ownerSnapshots(owner), { recursive: true, mode: 448 });
|
|
512
|
+
await rename(from, to);
|
|
513
|
+
done.snapshots.push(name);
|
|
514
|
+
}
|
|
515
|
+
return done;
|
|
516
|
+
}
|
|
517
|
+
async function oldDepthEntries(root) {
|
|
518
|
+
let entries;
|
|
519
|
+
try {
|
|
520
|
+
entries = await readdir2(root, { withFileTypes: true });
|
|
521
|
+
} catch (err) {
|
|
522
|
+
if (err.code === "ENOENT") return [];
|
|
523
|
+
throw err;
|
|
524
|
+
}
|
|
525
|
+
return entries.filter((e) => e.isDirectory() && existsSync2(join2(root, e.name, "meta.json"))).map((e) => e.name).sort();
|
|
526
|
+
}
|
|
527
|
+
async function patchJson(file, patch) {
|
|
528
|
+
const raw = JSON.parse(await readFile3(file, "utf8"));
|
|
529
|
+
await writeFile3(file, JSON.stringify(patch(raw), null, 2) + "\n");
|
|
530
|
+
}
|
|
531
|
+
async function migrateBoxIds(p = paths()) {
|
|
532
|
+
const metas = [];
|
|
533
|
+
for (const ref of await listMachineRefs(p)) {
|
|
534
|
+
try {
|
|
535
|
+
metas.push(await readMachineMeta(ref, p));
|
|
536
|
+
} catch {
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
const taken = new Set(metas.flatMap((m) => m.id === void 0 ? [] : [m.id]));
|
|
541
|
+
const done = [];
|
|
542
|
+
for (const meta of metas) {
|
|
543
|
+
if (meta.id !== void 0) {
|
|
544
|
+
rekeyUsageEvents(refKey(meta), meta.id, p);
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
if (pidAlive(meta.pid)) continue;
|
|
548
|
+
await adoptBoxId(meta, taken, p);
|
|
549
|
+
done.push(refKey(meta));
|
|
550
|
+
}
|
|
551
|
+
return done;
|
|
552
|
+
}
|
|
553
|
+
async function adoptBoxId(meta, taken, p = paths()) {
|
|
554
|
+
if (meta.id !== void 0) return meta;
|
|
555
|
+
const withId2 = { ...meta, id: newBoxId(taken) };
|
|
556
|
+
await writeMachineMeta(withId2, p);
|
|
557
|
+
rekeyUsageEvents(refKey(meta), withId2.id, p);
|
|
558
|
+
return withId2;
|
|
559
|
+
}
|
|
512
560
|
|
|
513
561
|
// src/core/machines.ts
|
|
514
562
|
import { spawn } from "node:child_process";
|
|
@@ -752,6 +800,12 @@ var NAME_RE = /^[a-z0-9][a-z0-9-]{0,11}$/;
|
|
|
752
800
|
var AUTO_PREFIX = "box";
|
|
753
801
|
var GUEST_CID = 3;
|
|
754
802
|
var OVERLAY_REL = "overlay.ext4";
|
|
803
|
+
var tapKey = (meta) => meta.id ?? meta.name;
|
|
804
|
+
var usageKey = (meta) => meta.id ?? refKey(meta);
|
|
805
|
+
var idsInUse = (metas) => new Set(metas.flatMap((m) => m.id === void 0 ? [] : [m.id]));
|
|
806
|
+
async function withId(meta, p) {
|
|
807
|
+
return meta.id !== void 0 ? meta : adoptBoxId(meta, idsInUse(await readAllMeta(p)), p);
|
|
808
|
+
}
|
|
755
809
|
function diskStats(disk) {
|
|
756
810
|
try {
|
|
757
811
|
const st = statSync(disk);
|
|
@@ -773,7 +827,7 @@ function diskGibOf(meta, p) {
|
|
|
773
827
|
}
|
|
774
828
|
function recordState(meta, state, by, p, at = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
775
829
|
try {
|
|
776
|
-
insertUsageEvent({ boxId:
|
|
830
|
+
insertUsageEvent({ boxId: usageKey(meta), owner: meta.owner, name: meta.name, at, state, by, vcpu: meta.vcpu, memMib: meta.memMib, diskGib: diskGibOf(meta, p) }, p);
|
|
777
831
|
} catch (err) {
|
|
778
832
|
console.error(`warning: usage ledger: ${err.message}`);
|
|
779
833
|
}
|
|
@@ -792,12 +846,13 @@ function toMachine(meta, p = paths()) {
|
|
|
792
846
|
const alive = pidAlive(meta.pid);
|
|
793
847
|
if (!alive && meta.pid !== null) {
|
|
794
848
|
try {
|
|
795
|
-
closeOpenRun(
|
|
849
|
+
closeOpenRun(usageKey(meta), (/* @__PURE__ */ new Date()).toISOString(), p);
|
|
796
850
|
} catch (err) {
|
|
797
851
|
console.error(`warning: usage ledger: ${err.message}`);
|
|
798
852
|
}
|
|
799
853
|
}
|
|
800
854
|
return {
|
|
855
|
+
id: meta.id,
|
|
801
856
|
name: meta.name,
|
|
802
857
|
owner: meta.owner,
|
|
803
858
|
ip: meta.ip,
|
|
@@ -1014,6 +1069,7 @@ async function createMachine(req, owner, p = paths()) {
|
|
|
1014
1069
|
const pubkeyB64 = await readPubkeyB64(p);
|
|
1015
1070
|
const existing = await readAllMeta(p);
|
|
1016
1071
|
const name = pickName(req.name, new Set(existing.filter((m2) => m2.owner === owner).map((m2) => m2.name)));
|
|
1072
|
+
const id = newBoxId(idsInUse(existing));
|
|
1017
1073
|
const ip = nextFreeIp(cfg.subnet, [...existing.map((m2) => m2.ip), cfg.gateway]);
|
|
1018
1074
|
const mac = macFor(ip);
|
|
1019
1075
|
const m = p.machine(owner, name);
|
|
@@ -1024,16 +1080,16 @@ async function createMachine(req, owner, p = paths()) {
|
|
|
1024
1080
|
let tapCreated = false;
|
|
1025
1081
|
try {
|
|
1026
1082
|
await createOverlay(m.overlay, size.diskGib);
|
|
1027
|
-
const tap = await createTap(
|
|
1083
|
+
const tap = await createTap(id, cfg);
|
|
1028
1084
|
tapCreated = true;
|
|
1029
|
-
const spec = { name, owner, ip, mac, vcpu: size.vcpu, memMib: size.memMib, base: base.version };
|
|
1085
|
+
const spec = { id, name, owner, ip, mac, vcpu: size.vcpu, memMib: size.memMib, base: base.version };
|
|
1030
1086
|
const pid = await launch(spec, tap, cfg, pubkeyB64, p);
|
|
1031
1087
|
const meta = { ...spec, pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), source: "base" };
|
|
1032
1088
|
await writeMachineMeta(meta, p);
|
|
1033
1089
|
recordState(meta, "running", "new", p, meta.createdAt);
|
|
1034
1090
|
return toMachine(meta, p);
|
|
1035
1091
|
} catch (err) {
|
|
1036
|
-
if (tapCreated) await deleteTap(
|
|
1092
|
+
if (tapCreated) await deleteTap(id, cfg).catch(() => void 0);
|
|
1037
1093
|
await rm(m.dir, { recursive: true, force: true });
|
|
1038
1094
|
throw err;
|
|
1039
1095
|
}
|
|
@@ -1062,6 +1118,7 @@ async function restoreMachine(fromText, owner, req, cfg, p) {
|
|
|
1062
1118
|
await assertRoomFor(snap.vcpu, snap.memMib, req.force ?? false, p);
|
|
1063
1119
|
const existing = await readAllMeta(p);
|
|
1064
1120
|
const name = pickName(req.name, new Set(existing.filter((m2) => m2.owner === owner).map((m2) => m2.name)));
|
|
1121
|
+
const id = newBoxId(idsInUse(existing));
|
|
1065
1122
|
const ip = nextFreeIp(cfg.subnet, [...existing.map((m2) => m2.ip), cfg.gateway]);
|
|
1066
1123
|
const mac = macFor(ip);
|
|
1067
1124
|
const m = p.machine(owner, name);
|
|
@@ -1073,23 +1130,23 @@ async function restoreMachine(fromText, owner, req, cfg, p) {
|
|
|
1073
1130
|
try {
|
|
1074
1131
|
await run(["cp", "--sparse=always", "--reflink=auto", s.overlay, m.overlay]);
|
|
1075
1132
|
await link(s.mem, m.memBase);
|
|
1076
|
-
const tap = await createTap(
|
|
1133
|
+
const tap = await createTap(id, cfg, { detached: true });
|
|
1077
1134
|
tapCreated = true;
|
|
1078
|
-
const spec = { name, owner, ip, mac, vcpu: snap.vcpu, memMib: snap.memMib, base: snap.base };
|
|
1135
|
+
const spec = { id, name, owner, ip, mac, vcpu: snap.vcpu, memMib: snap.memMib, base: snap.base };
|
|
1079
1136
|
pid = await loadSnapshot(spec, { vmstate: s.vmstate, mem: s.mem, label: shown(from) }, tap, cfg, p);
|
|
1080
1137
|
await resumeVm(m.sock);
|
|
1081
1138
|
await introduce(spec, m.vsock, cfg, {
|
|
1082
1139
|
silent: `nothing answered on the restored box's control channel for 3 s; ${tooOldToRestore(shown(from))}`,
|
|
1083
1140
|
noAnswer: "the restored box was discarded. kvbox-agent inside it took the request but gave no answer; try again, and if it keeps happening look at the box's journal (kvbox exec <box> -- sudo journalctl -u kvbox-agent) on a box started from the same snapshot."
|
|
1084
1141
|
});
|
|
1085
|
-
await attachTap(
|
|
1142
|
+
await attachTap(id, cfg);
|
|
1086
1143
|
const meta = { ...spec, pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), source: `snapshot:${refKey(from)}`, memSnapshots: 1 };
|
|
1087
1144
|
await writeMachineMeta(meta, p);
|
|
1088
1145
|
recordState(meta, "running", "new", p, meta.createdAt);
|
|
1089
1146
|
return toMachine(meta, p);
|
|
1090
1147
|
} catch (err) {
|
|
1091
1148
|
if (pid !== void 0) killQuietly(pid);
|
|
1092
|
-
if (tapCreated) await deleteTap(
|
|
1149
|
+
if (tapCreated) await deleteTap(id, cfg).catch(() => void 0);
|
|
1093
1150
|
await rm(m.dir, { recursive: true, force: true });
|
|
1094
1151
|
throw err;
|
|
1095
1152
|
}
|
|
@@ -1147,7 +1204,7 @@ async function waitDead(pid, graceMs) {
|
|
|
1147
1204
|
}
|
|
1148
1205
|
}
|
|
1149
1206
|
async function cleanupProcess(meta, cfg, p) {
|
|
1150
|
-
await deleteTap(meta
|
|
1207
|
+
await deleteTap(tapKey(meta), cfg);
|
|
1151
1208
|
const m = p.machine(meta.owner, meta.name);
|
|
1152
1209
|
await rm(m.sock, { force: true });
|
|
1153
1210
|
await rm(m.vsock, { force: true });
|
|
@@ -1166,7 +1223,7 @@ async function stopMachine(ref, p = paths()) {
|
|
|
1166
1223
|
return toMachine(stopped, p);
|
|
1167
1224
|
}
|
|
1168
1225
|
async function startMachine(ref, p = paths()) {
|
|
1169
|
-
const meta = await readMeta(ref, p);
|
|
1226
|
+
const meta = await withId(await readMeta(ref, p), p);
|
|
1170
1227
|
const cfg = await loadConfig(p);
|
|
1171
1228
|
if (pidAlive(meta.pid)) throw new KvboxError(`box ${shown(ref)} is already ${meta.paused ? "up, in standby (kvbox resume " + shown(ref) + ")" : "running"}`);
|
|
1172
1229
|
if (isHibernated(meta, p)) {
|
|
@@ -1175,7 +1232,7 @@ async function startMachine(ref, p = paths()) {
|
|
|
1175
1232
|
assertAssets(cfg);
|
|
1176
1233
|
const pubkeyB64 = await readPubkeyB64(p);
|
|
1177
1234
|
await ensureHostNetwork(cfg);
|
|
1178
|
-
const tap = await createTap(
|
|
1235
|
+
const tap = await createTap(meta.id, cfg);
|
|
1179
1236
|
try {
|
|
1180
1237
|
const pid = await launch(meta, tap, cfg, pubkeyB64, p);
|
|
1181
1238
|
const started = { ...meta, pid };
|
|
@@ -1183,7 +1240,7 @@ async function startMachine(ref, p = paths()) {
|
|
|
1183
1240
|
recordState(started, "running", "start", p);
|
|
1184
1241
|
return toMachine(started, p);
|
|
1185
1242
|
} catch (err) {
|
|
1186
|
-
await deleteTap(
|
|
1243
|
+
await deleteTap(meta.id, cfg).catch(() => void 0);
|
|
1187
1244
|
throw err;
|
|
1188
1245
|
}
|
|
1189
1246
|
}
|
|
@@ -1250,7 +1307,7 @@ async function hibernateMachine(ref, p = paths()) {
|
|
|
1250
1307
|
return toMachine(hibernated, p);
|
|
1251
1308
|
}
|
|
1252
1309
|
async function wakeMachine(ref, p = paths()) {
|
|
1253
|
-
const meta = await readMeta(ref, p);
|
|
1310
|
+
const meta = await withId(await readMeta(ref, p), p);
|
|
1254
1311
|
const cfg = await loadConfig(p);
|
|
1255
1312
|
const status = statusOf(meta, p);
|
|
1256
1313
|
if (status === "running" || status === "standby") throw new KvboxError(`${stateMessage(ref, status)}, not hibernated`);
|
|
@@ -1261,20 +1318,20 @@ async function wakeMachine(ref, p = paths()) {
|
|
|
1261
1318
|
}
|
|
1262
1319
|
const m = p.machine(ref.owner, ref.name);
|
|
1263
1320
|
await ensureHostNetwork(cfg);
|
|
1264
|
-
const tap = await createTap(
|
|
1321
|
+
const tap = await createTap(meta.id, cfg, { detached: true });
|
|
1265
1322
|
let pid;
|
|
1266
1323
|
try {
|
|
1267
|
-
const spec = { name: meta.name, owner: meta.owner, ip: meta.ip, mac: meta.mac, vcpu: meta.vcpu, memMib: meta.memMib, base: meta.base };
|
|
1324
|
+
const spec = { id: meta.id, name: meta.name, owner: meta.owner, ip: meta.ip, mac: meta.mac, vcpu: meta.vcpu, memMib: meta.memMib, base: meta.base };
|
|
1268
1325
|
pid = await loadSnapshot(spec, { vmstate: m.hibernateVmstate, mem: m.hibernateMem, label: `box ${shown(ref)}'s memory image` }, tap, cfg, p);
|
|
1269
1326
|
await resumeVm(m.sock);
|
|
1270
1327
|
await introduce(spec, m.vsock, cfg, {
|
|
1271
1328
|
silent: `nothing answered on the box's control channel for 3 s; it stays hibernated, try kvbox wake ${shown(ref)} again`,
|
|
1272
1329
|
noAnswer: `the box stays hibernated. kvbox-agent inside it took the request but gave no answer; try kvbox wake ${shown(ref)} again`
|
|
1273
1330
|
});
|
|
1274
|
-
await attachTap(
|
|
1331
|
+
await attachTap(meta.id, cfg);
|
|
1275
1332
|
} catch (err) {
|
|
1276
1333
|
if (pid !== void 0) killQuietly(pid);
|
|
1277
|
-
await deleteTap(
|
|
1334
|
+
await deleteTap(meta.id, cfg).catch(() => void 0);
|
|
1278
1335
|
throw err;
|
|
1279
1336
|
}
|
|
1280
1337
|
await rm(m.memBase, { force: true });
|
|
@@ -1404,7 +1461,7 @@ async function machineUsage(ref, since, p = paths()) {
|
|
|
1404
1461
|
const box = await getMachine(ref, p);
|
|
1405
1462
|
const cfg = await loadConfig(p);
|
|
1406
1463
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1407
|
-
const boxId = refKey(ref);
|
|
1464
|
+
const boxId = box.id ?? refKey(ref);
|
|
1408
1465
|
let events = usageEventsFor(boxId, box.createdAt, p);
|
|
1409
1466
|
if (events.length === 0) {
|
|
1410
1467
|
insertUsageEvent(
|
|
@@ -1419,7 +1476,7 @@ async function machineUsage(ref, since, p = paths()) {
|
|
|
1419
1476
|
async function seedUsageLedger(p = paths()) {
|
|
1420
1477
|
let seeded = 0;
|
|
1421
1478
|
for (const box of await listMachines(null, p)) {
|
|
1422
|
-
const boxId = `${box.owner}/${box.name}`;
|
|
1479
|
+
const boxId = box.id ?? `${box.owner}/${box.name}`;
|
|
1423
1480
|
if (usageEventsFor(boxId, box.createdAt, p).length > 0) continue;
|
|
1424
1481
|
insertUsageEvent(
|
|
1425
1482
|
{ boxId, owner: box.owner, name: box.name, at: (/* @__PURE__ */ new Date()).toISOString(), state: box.status, by: "upgrade", vcpu: box.vcpu, memMib: box.memMib, diskGib: Math.round(box.diskBytes / GIB) },
|
|
@@ -1446,7 +1503,7 @@ function versionOf(srcFileUrl, injected) {
|
|
|
1446
1503
|
}
|
|
1447
1504
|
|
|
1448
1505
|
// src/version.ts
|
|
1449
|
-
var VERSION = versionOf(import.meta.url, true ? "0.
|
|
1506
|
+
var VERSION = versionOf(import.meta.url, true ? "0.2.0" : void 0);
|
|
1450
1507
|
|
|
1451
1508
|
// src/caller.ts
|
|
1452
1509
|
function resolveCaller() {
|
|
@@ -1473,6 +1530,8 @@ import { z as z3 } from "zod";
|
|
|
1473
1530
|
var SizeSchema = z3.enum(SIZE_NAMES);
|
|
1474
1531
|
var BoxStatusSchema = z3.enum(["running", "standby", "hibernated", "stopped"]);
|
|
1475
1532
|
var MachineSchema = z3.object({
|
|
1533
|
+
/** Host-unique short id (the tap is `kv-<id>`). Missing only on a box made before ids that has not restarted since. */
|
|
1534
|
+
id: z3.string().optional(),
|
|
1476
1535
|
name: z3.string(),
|
|
1477
1536
|
/** Who the box belongs to; names are unique per owner. A bare name is local to the host, `user@control-plane` later. */
|
|
1478
1537
|
owner: z3.string(),
|
|
@@ -1562,6 +1621,39 @@ var MachineUsageSchema = z3.object({
|
|
|
1562
1621
|
var ApiErrorSchema = z3.object({
|
|
1563
1622
|
error: z3.string()
|
|
1564
1623
|
});
|
|
1624
|
+
var CloudUserSchema = z3.object({
|
|
1625
|
+
name: z3.string(),
|
|
1626
|
+
role: z3.enum(["admin", "user"]),
|
|
1627
|
+
createdAt: z3.string()
|
|
1628
|
+
});
|
|
1629
|
+
var LoginRequestSchema = z3.object({
|
|
1630
|
+
name: z3.string().min(1),
|
|
1631
|
+
password: z3.string().min(1),
|
|
1632
|
+
publicKey: z3.string().optional()
|
|
1633
|
+
});
|
|
1634
|
+
var LoginResponseSchema = z3.object({
|
|
1635
|
+
token: z3.string(),
|
|
1636
|
+
user: CloudUserSchema
|
|
1637
|
+
});
|
|
1638
|
+
var HostJoinRequestSchema = z3.object({
|
|
1639
|
+
token: z3.string().min(1),
|
|
1640
|
+
name: z3.string().min(1),
|
|
1641
|
+
url: z3.string().url(),
|
|
1642
|
+
fingerprint: z3.string().regex(/^([0-9A-F]{2}:){31}[0-9A-F]{2}$/),
|
|
1643
|
+
publicAddress: z3.string().min(1)
|
|
1644
|
+
});
|
|
1645
|
+
var HostJoinResponseSchema = z3.object({
|
|
1646
|
+
hostSecret: z3.string(),
|
|
1647
|
+
controlPlane: z3.object({ name: z3.string() })
|
|
1648
|
+
});
|
|
1649
|
+
var CloudHostSchema = z3.object({
|
|
1650
|
+
name: z3.string(),
|
|
1651
|
+
url: z3.string(),
|
|
1652
|
+
publicAddress: z3.string(),
|
|
1653
|
+
joinedAt: z3.string(),
|
|
1654
|
+
lastSeenAt: z3.string().nullable(),
|
|
1655
|
+
capacity: CapacitySchema.nullable()
|
|
1656
|
+
});
|
|
1565
1657
|
|
|
1566
1658
|
// src/routes/machines.ts
|
|
1567
1659
|
function machineRoutes() {
|
|
@@ -1780,6 +1872,8 @@ async function listen(opts = {}) {
|
|
|
1780
1872
|
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}/`);
|
|
1781
1873
|
}
|
|
1782
1874
|
for (const c of moved.conflicts) console.error(`warning: ${c} left in place: ${cfg.owner} already has one with that name`);
|
|
1875
|
+
const withIds = await migrateBoxIds(p);
|
|
1876
|
+
if (withIds.length > 0) console.error(`boxes now have an id of their own: ${withIds.length} box(es) made before this version got theirs (${withIds.join(", ")})`);
|
|
1783
1877
|
const seeded = await seedUsageLedger(p);
|
|
1784
1878
|
if (seeded > 0) console.error(`usage: started keeping time for ${seeded} box(es) made before this version; kvbox vm usage counts from now for them`);
|
|
1785
1879
|
const socketPath = opts.socketPath ?? p.serverSock;
|