@ghalex/kvbox-server 0.1.3 → 0.1.4
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 +132 -72
- package/dist/kvbox-server.js.map +1 -1
- package/package.json +1 -1
package/dist/kvbox-server.js
CHANGED
|
@@ -278,9 +278,16 @@ import { rm as rm3, writeFile as writeFile5 } from "node:fs/promises";
|
|
|
278
278
|
import { createAdaptorServer } from "@hono/node-server";
|
|
279
279
|
|
|
280
280
|
// src/core/state.ts
|
|
281
|
+
import { randomBytes } from "node:crypto";
|
|
281
282
|
import { mkdir, readFile as readFile2, readdir, writeFile as writeFile2 } from "node:fs/promises";
|
|
282
283
|
import { z as z2 } from "zod";
|
|
283
284
|
var MachineMetaSchema = z2.object({
|
|
285
|
+
/**
|
|
286
|
+
* Host-unique, minted at `kvbox new` (fresh or restored) and kept for the box's life; the tap is
|
|
287
|
+
* `kv-<id>`. Absent only on a box that predates ids and is still running on its `kv-<name>` tap:
|
|
288
|
+
* `migrateBoxIds` gives stopped ones theirs at server start, a running one gets it when it next starts.
|
|
289
|
+
*/
|
|
290
|
+
id: z2.string().optional(),
|
|
284
291
|
name: z2.string(),
|
|
285
292
|
/** Part of the box's identity: machines/<owner>/<name>. See refs.ts. */
|
|
286
293
|
owner: z2.string(),
|
|
@@ -303,6 +310,12 @@ var MachineMetaSchema = z2.object({
|
|
|
303
310
|
/** `kvbox pause`: Firecracker is alive but the VM is frozen (standby). Absent: not paused. */
|
|
304
311
|
paused: z2.boolean().optional()
|
|
305
312
|
});
|
|
313
|
+
function newBoxId(taken) {
|
|
314
|
+
for (; ; ) {
|
|
315
|
+
const id = randomBytes(3).toString("hex");
|
|
316
|
+
if (!taken.has(id)) return id;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
306
319
|
var SnapshotMetaSchema = z2.object({
|
|
307
320
|
name: z2.string(),
|
|
308
321
|
owner: z2.string(),
|
|
@@ -377,57 +390,6 @@ function pidAlive(pid) {
|
|
|
377
390
|
import { existsSync as existsSync2 } from "node:fs";
|
|
378
391
|
import { mkdir as mkdir2, readdir as readdir2, readFile as readFile3, rename, writeFile as writeFile3 } from "node:fs/promises";
|
|
379
392
|
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
393
|
|
|
432
394
|
// src/core/db.ts
|
|
433
395
|
import { chmodSync, closeSync, mkdirSync, openSync } from "node:fs";
|
|
@@ -509,6 +471,91 @@ function usageEventsFor(boxId, since, p = paths()) {
|
|
|
509
471
|
const rows = openDb(p).prepare("select * from usage_events where box_id = ? and at >= ? order by at, id").all(boxId, since);
|
|
510
472
|
return rows.map(fromRaw);
|
|
511
473
|
}
|
|
474
|
+
function rekeyUsageEvents(from, to, p = paths()) {
|
|
475
|
+
return Number(openDb(p).prepare("update usage_events set box_id = ? where box_id = ?").run(to, from).changes);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// src/core/migrate.ts
|
|
479
|
+
async function migrateOwnerLayout(owner, p = paths()) {
|
|
480
|
+
const done = { boxes: [], snapshots: [], conflicts: [] };
|
|
481
|
+
for (const name of await oldDepthEntries(p.machines)) {
|
|
482
|
+
const from = join2(p.machines, name);
|
|
483
|
+
const to = p.machine(owner, name).dir;
|
|
484
|
+
if (existsSync2(to)) {
|
|
485
|
+
done.conflicts.push(`machines/${name}`);
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
await patchJson(join2(from, "meta.json"), (meta) => ({
|
|
489
|
+
...meta,
|
|
490
|
+
owner,
|
|
491
|
+
// a restored box names its snapshot: that moved under the same owner
|
|
492
|
+
source: typeof meta["source"] === "string" && meta["source"].startsWith("snapshot:") && !meta["source"].includes("/") ? `snapshot:${owner}/${meta["source"].slice("snapshot:".length)}` : meta["source"]
|
|
493
|
+
}));
|
|
494
|
+
await mkdir2(p.ownerMachines(owner), { recursive: true, mode: 448 });
|
|
495
|
+
await rename(from, to);
|
|
496
|
+
done.boxes.push(name);
|
|
497
|
+
}
|
|
498
|
+
for (const name of await oldDepthEntries(p.snapshots)) {
|
|
499
|
+
const from = join2(p.snapshots, name);
|
|
500
|
+
const to = p.snapshot(owner, name).dir;
|
|
501
|
+
if (existsSync2(to)) {
|
|
502
|
+
done.conflicts.push(`snapshots/${name}`);
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
await patchJson(join2(from, "meta.json"), (meta) => ({
|
|
506
|
+
...meta,
|
|
507
|
+
owner,
|
|
508
|
+
source: typeof meta["source"] === "string" && !meta["source"].includes("/") ? `${owner}/${meta["source"]}` : meta["source"]
|
|
509
|
+
}));
|
|
510
|
+
await mkdir2(p.ownerSnapshots(owner), { recursive: true, mode: 448 });
|
|
511
|
+
await rename(from, to);
|
|
512
|
+
done.snapshots.push(name);
|
|
513
|
+
}
|
|
514
|
+
return done;
|
|
515
|
+
}
|
|
516
|
+
async function oldDepthEntries(root) {
|
|
517
|
+
let entries;
|
|
518
|
+
try {
|
|
519
|
+
entries = await readdir2(root, { withFileTypes: true });
|
|
520
|
+
} catch (err) {
|
|
521
|
+
if (err.code === "ENOENT") return [];
|
|
522
|
+
throw err;
|
|
523
|
+
}
|
|
524
|
+
return entries.filter((e) => e.isDirectory() && existsSync2(join2(root, e.name, "meta.json"))).map((e) => e.name).sort();
|
|
525
|
+
}
|
|
526
|
+
async function patchJson(file, patch) {
|
|
527
|
+
const raw = JSON.parse(await readFile3(file, "utf8"));
|
|
528
|
+
await writeFile3(file, JSON.stringify(patch(raw), null, 2) + "\n");
|
|
529
|
+
}
|
|
530
|
+
async function migrateBoxIds(p = paths()) {
|
|
531
|
+
const metas = [];
|
|
532
|
+
for (const ref of await listMachineRefs(p)) {
|
|
533
|
+
try {
|
|
534
|
+
metas.push(await readMachineMeta(ref, p));
|
|
535
|
+
} catch {
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
const taken = new Set(metas.flatMap((m) => m.id === void 0 ? [] : [m.id]));
|
|
540
|
+
const done = [];
|
|
541
|
+
for (const meta of metas) {
|
|
542
|
+
if (meta.id !== void 0) {
|
|
543
|
+
rekeyUsageEvents(refKey(meta), meta.id, p);
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
if (pidAlive(meta.pid)) continue;
|
|
547
|
+
await adoptBoxId(meta, taken, p);
|
|
548
|
+
done.push(refKey(meta));
|
|
549
|
+
}
|
|
550
|
+
return done;
|
|
551
|
+
}
|
|
552
|
+
async function adoptBoxId(meta, taken, p = paths()) {
|
|
553
|
+
if (meta.id !== void 0) return meta;
|
|
554
|
+
const withId2 = { ...meta, id: newBoxId(taken) };
|
|
555
|
+
await writeMachineMeta(withId2, p);
|
|
556
|
+
rekeyUsageEvents(refKey(meta), withId2.id, p);
|
|
557
|
+
return withId2;
|
|
558
|
+
}
|
|
512
559
|
|
|
513
560
|
// src/core/machines.ts
|
|
514
561
|
import { spawn } from "node:child_process";
|
|
@@ -752,6 +799,12 @@ var NAME_RE = /^[a-z0-9][a-z0-9-]{0,11}$/;
|
|
|
752
799
|
var AUTO_PREFIX = "box";
|
|
753
800
|
var GUEST_CID = 3;
|
|
754
801
|
var OVERLAY_REL = "overlay.ext4";
|
|
802
|
+
var tapKey = (meta) => meta.id ?? meta.name;
|
|
803
|
+
var usageKey = (meta) => meta.id ?? refKey(meta);
|
|
804
|
+
var idsInUse = (metas) => new Set(metas.flatMap((m) => m.id === void 0 ? [] : [m.id]));
|
|
805
|
+
async function withId(meta, p) {
|
|
806
|
+
return meta.id !== void 0 ? meta : adoptBoxId(meta, idsInUse(await readAllMeta(p)), p);
|
|
807
|
+
}
|
|
755
808
|
function diskStats(disk) {
|
|
756
809
|
try {
|
|
757
810
|
const st = statSync(disk);
|
|
@@ -773,7 +826,7 @@ function diskGibOf(meta, p) {
|
|
|
773
826
|
}
|
|
774
827
|
function recordState(meta, state, by, p, at = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
775
828
|
try {
|
|
776
|
-
insertUsageEvent({ boxId:
|
|
829
|
+
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
830
|
} catch (err) {
|
|
778
831
|
console.error(`warning: usage ledger: ${err.message}`);
|
|
779
832
|
}
|
|
@@ -792,12 +845,13 @@ function toMachine(meta, p = paths()) {
|
|
|
792
845
|
const alive = pidAlive(meta.pid);
|
|
793
846
|
if (!alive && meta.pid !== null) {
|
|
794
847
|
try {
|
|
795
|
-
closeOpenRun(
|
|
848
|
+
closeOpenRun(usageKey(meta), (/* @__PURE__ */ new Date()).toISOString(), p);
|
|
796
849
|
} catch (err) {
|
|
797
850
|
console.error(`warning: usage ledger: ${err.message}`);
|
|
798
851
|
}
|
|
799
852
|
}
|
|
800
853
|
return {
|
|
854
|
+
id: meta.id,
|
|
801
855
|
name: meta.name,
|
|
802
856
|
owner: meta.owner,
|
|
803
857
|
ip: meta.ip,
|
|
@@ -1014,6 +1068,7 @@ async function createMachine(req, owner, p = paths()) {
|
|
|
1014
1068
|
const pubkeyB64 = await readPubkeyB64(p);
|
|
1015
1069
|
const existing = await readAllMeta(p);
|
|
1016
1070
|
const name = pickName(req.name, new Set(existing.filter((m2) => m2.owner === owner).map((m2) => m2.name)));
|
|
1071
|
+
const id = newBoxId(idsInUse(existing));
|
|
1017
1072
|
const ip = nextFreeIp(cfg.subnet, [...existing.map((m2) => m2.ip), cfg.gateway]);
|
|
1018
1073
|
const mac = macFor(ip);
|
|
1019
1074
|
const m = p.machine(owner, name);
|
|
@@ -1024,16 +1079,16 @@ async function createMachine(req, owner, p = paths()) {
|
|
|
1024
1079
|
let tapCreated = false;
|
|
1025
1080
|
try {
|
|
1026
1081
|
await createOverlay(m.overlay, size.diskGib);
|
|
1027
|
-
const tap = await createTap(
|
|
1082
|
+
const tap = await createTap(id, cfg);
|
|
1028
1083
|
tapCreated = true;
|
|
1029
|
-
const spec = { name, owner, ip, mac, vcpu: size.vcpu, memMib: size.memMib, base: base.version };
|
|
1084
|
+
const spec = { id, name, owner, ip, mac, vcpu: size.vcpu, memMib: size.memMib, base: base.version };
|
|
1030
1085
|
const pid = await launch(spec, tap, cfg, pubkeyB64, p);
|
|
1031
1086
|
const meta = { ...spec, pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), source: "base" };
|
|
1032
1087
|
await writeMachineMeta(meta, p);
|
|
1033
1088
|
recordState(meta, "running", "new", p, meta.createdAt);
|
|
1034
1089
|
return toMachine(meta, p);
|
|
1035
1090
|
} catch (err) {
|
|
1036
|
-
if (tapCreated) await deleteTap(
|
|
1091
|
+
if (tapCreated) await deleteTap(id, cfg).catch(() => void 0);
|
|
1037
1092
|
await rm(m.dir, { recursive: true, force: true });
|
|
1038
1093
|
throw err;
|
|
1039
1094
|
}
|
|
@@ -1062,6 +1117,7 @@ async function restoreMachine(fromText, owner, req, cfg, p) {
|
|
|
1062
1117
|
await assertRoomFor(snap.vcpu, snap.memMib, req.force ?? false, p);
|
|
1063
1118
|
const existing = await readAllMeta(p);
|
|
1064
1119
|
const name = pickName(req.name, new Set(existing.filter((m2) => m2.owner === owner).map((m2) => m2.name)));
|
|
1120
|
+
const id = newBoxId(idsInUse(existing));
|
|
1065
1121
|
const ip = nextFreeIp(cfg.subnet, [...existing.map((m2) => m2.ip), cfg.gateway]);
|
|
1066
1122
|
const mac = macFor(ip);
|
|
1067
1123
|
const m = p.machine(owner, name);
|
|
@@ -1073,23 +1129,23 @@ async function restoreMachine(fromText, owner, req, cfg, p) {
|
|
|
1073
1129
|
try {
|
|
1074
1130
|
await run(["cp", "--sparse=always", "--reflink=auto", s.overlay, m.overlay]);
|
|
1075
1131
|
await link(s.mem, m.memBase);
|
|
1076
|
-
const tap = await createTap(
|
|
1132
|
+
const tap = await createTap(id, cfg, { detached: true });
|
|
1077
1133
|
tapCreated = true;
|
|
1078
|
-
const spec = { name, owner, ip, mac, vcpu: snap.vcpu, memMib: snap.memMib, base: snap.base };
|
|
1134
|
+
const spec = { id, name, owner, ip, mac, vcpu: snap.vcpu, memMib: snap.memMib, base: snap.base };
|
|
1079
1135
|
pid = await loadSnapshot(spec, { vmstate: s.vmstate, mem: s.mem, label: shown(from) }, tap, cfg, p);
|
|
1080
1136
|
await resumeVm(m.sock);
|
|
1081
1137
|
await introduce(spec, m.vsock, cfg, {
|
|
1082
1138
|
silent: `nothing answered on the restored box's control channel for 3 s; ${tooOldToRestore(shown(from))}`,
|
|
1083
1139
|
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
1140
|
});
|
|
1085
|
-
await attachTap(
|
|
1141
|
+
await attachTap(id, cfg);
|
|
1086
1142
|
const meta = { ...spec, pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), source: `snapshot:${refKey(from)}`, memSnapshots: 1 };
|
|
1087
1143
|
await writeMachineMeta(meta, p);
|
|
1088
1144
|
recordState(meta, "running", "new", p, meta.createdAt);
|
|
1089
1145
|
return toMachine(meta, p);
|
|
1090
1146
|
} catch (err) {
|
|
1091
1147
|
if (pid !== void 0) killQuietly(pid);
|
|
1092
|
-
if (tapCreated) await deleteTap(
|
|
1148
|
+
if (tapCreated) await deleteTap(id, cfg).catch(() => void 0);
|
|
1093
1149
|
await rm(m.dir, { recursive: true, force: true });
|
|
1094
1150
|
throw err;
|
|
1095
1151
|
}
|
|
@@ -1147,7 +1203,7 @@ async function waitDead(pid, graceMs) {
|
|
|
1147
1203
|
}
|
|
1148
1204
|
}
|
|
1149
1205
|
async function cleanupProcess(meta, cfg, p) {
|
|
1150
|
-
await deleteTap(meta
|
|
1206
|
+
await deleteTap(tapKey(meta), cfg);
|
|
1151
1207
|
const m = p.machine(meta.owner, meta.name);
|
|
1152
1208
|
await rm(m.sock, { force: true });
|
|
1153
1209
|
await rm(m.vsock, { force: true });
|
|
@@ -1166,7 +1222,7 @@ async function stopMachine(ref, p = paths()) {
|
|
|
1166
1222
|
return toMachine(stopped, p);
|
|
1167
1223
|
}
|
|
1168
1224
|
async function startMachine(ref, p = paths()) {
|
|
1169
|
-
const meta = await readMeta(ref, p);
|
|
1225
|
+
const meta = await withId(await readMeta(ref, p), p);
|
|
1170
1226
|
const cfg = await loadConfig(p);
|
|
1171
1227
|
if (pidAlive(meta.pid)) throw new KvboxError(`box ${shown(ref)} is already ${meta.paused ? "up, in standby (kvbox resume " + shown(ref) + ")" : "running"}`);
|
|
1172
1228
|
if (isHibernated(meta, p)) {
|
|
@@ -1175,7 +1231,7 @@ async function startMachine(ref, p = paths()) {
|
|
|
1175
1231
|
assertAssets(cfg);
|
|
1176
1232
|
const pubkeyB64 = await readPubkeyB64(p);
|
|
1177
1233
|
await ensureHostNetwork(cfg);
|
|
1178
|
-
const tap = await createTap(
|
|
1234
|
+
const tap = await createTap(meta.id, cfg);
|
|
1179
1235
|
try {
|
|
1180
1236
|
const pid = await launch(meta, tap, cfg, pubkeyB64, p);
|
|
1181
1237
|
const started = { ...meta, pid };
|
|
@@ -1183,7 +1239,7 @@ async function startMachine(ref, p = paths()) {
|
|
|
1183
1239
|
recordState(started, "running", "start", p);
|
|
1184
1240
|
return toMachine(started, p);
|
|
1185
1241
|
} catch (err) {
|
|
1186
|
-
await deleteTap(
|
|
1242
|
+
await deleteTap(meta.id, cfg).catch(() => void 0);
|
|
1187
1243
|
throw err;
|
|
1188
1244
|
}
|
|
1189
1245
|
}
|
|
@@ -1250,7 +1306,7 @@ async function hibernateMachine(ref, p = paths()) {
|
|
|
1250
1306
|
return toMachine(hibernated, p);
|
|
1251
1307
|
}
|
|
1252
1308
|
async function wakeMachine(ref, p = paths()) {
|
|
1253
|
-
const meta = await readMeta(ref, p);
|
|
1309
|
+
const meta = await withId(await readMeta(ref, p), p);
|
|
1254
1310
|
const cfg = await loadConfig(p);
|
|
1255
1311
|
const status = statusOf(meta, p);
|
|
1256
1312
|
if (status === "running" || status === "standby") throw new KvboxError(`${stateMessage(ref, status)}, not hibernated`);
|
|
@@ -1261,20 +1317,20 @@ async function wakeMachine(ref, p = paths()) {
|
|
|
1261
1317
|
}
|
|
1262
1318
|
const m = p.machine(ref.owner, ref.name);
|
|
1263
1319
|
await ensureHostNetwork(cfg);
|
|
1264
|
-
const tap = await createTap(
|
|
1320
|
+
const tap = await createTap(meta.id, cfg, { detached: true });
|
|
1265
1321
|
let pid;
|
|
1266
1322
|
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 };
|
|
1323
|
+
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
1324
|
pid = await loadSnapshot(spec, { vmstate: m.hibernateVmstate, mem: m.hibernateMem, label: `box ${shown(ref)}'s memory image` }, tap, cfg, p);
|
|
1269
1325
|
await resumeVm(m.sock);
|
|
1270
1326
|
await introduce(spec, m.vsock, cfg, {
|
|
1271
1327
|
silent: `nothing answered on the box's control channel for 3 s; it stays hibernated, try kvbox wake ${shown(ref)} again`,
|
|
1272
1328
|
noAnswer: `the box stays hibernated. kvbox-agent inside it took the request but gave no answer; try kvbox wake ${shown(ref)} again`
|
|
1273
1329
|
});
|
|
1274
|
-
await attachTap(
|
|
1330
|
+
await attachTap(meta.id, cfg);
|
|
1275
1331
|
} catch (err) {
|
|
1276
1332
|
if (pid !== void 0) killQuietly(pid);
|
|
1277
|
-
await deleteTap(
|
|
1333
|
+
await deleteTap(meta.id, cfg).catch(() => void 0);
|
|
1278
1334
|
throw err;
|
|
1279
1335
|
}
|
|
1280
1336
|
await rm(m.memBase, { force: true });
|
|
@@ -1404,7 +1460,7 @@ async function machineUsage(ref, since, p = paths()) {
|
|
|
1404
1460
|
const box = await getMachine(ref, p);
|
|
1405
1461
|
const cfg = await loadConfig(p);
|
|
1406
1462
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1407
|
-
const boxId = refKey(ref);
|
|
1463
|
+
const boxId = box.id ?? refKey(ref);
|
|
1408
1464
|
let events = usageEventsFor(boxId, box.createdAt, p);
|
|
1409
1465
|
if (events.length === 0) {
|
|
1410
1466
|
insertUsageEvent(
|
|
@@ -1419,7 +1475,7 @@ async function machineUsage(ref, since, p = paths()) {
|
|
|
1419
1475
|
async function seedUsageLedger(p = paths()) {
|
|
1420
1476
|
let seeded = 0;
|
|
1421
1477
|
for (const box of await listMachines(null, p)) {
|
|
1422
|
-
const boxId = `${box.owner}/${box.name}`;
|
|
1478
|
+
const boxId = box.id ?? `${box.owner}/${box.name}`;
|
|
1423
1479
|
if (usageEventsFor(boxId, box.createdAt, p).length > 0) continue;
|
|
1424
1480
|
insertUsageEvent(
|
|
1425
1481
|
{ 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 +1502,7 @@ function versionOf(srcFileUrl, injected) {
|
|
|
1446
1502
|
}
|
|
1447
1503
|
|
|
1448
1504
|
// src/version.ts
|
|
1449
|
-
var VERSION = versionOf(import.meta.url, true ? "0.1.
|
|
1505
|
+
var VERSION = versionOf(import.meta.url, true ? "0.1.4" : void 0);
|
|
1450
1506
|
|
|
1451
1507
|
// src/caller.ts
|
|
1452
1508
|
function resolveCaller() {
|
|
@@ -1473,6 +1529,8 @@ import { z as z3 } from "zod";
|
|
|
1473
1529
|
var SizeSchema = z3.enum(SIZE_NAMES);
|
|
1474
1530
|
var BoxStatusSchema = z3.enum(["running", "standby", "hibernated", "stopped"]);
|
|
1475
1531
|
var MachineSchema = z3.object({
|
|
1532
|
+
/** Host-unique short id (the tap is `kv-<id>`). Missing only on a box made before ids that has not restarted since. */
|
|
1533
|
+
id: z3.string().optional(),
|
|
1476
1534
|
name: z3.string(),
|
|
1477
1535
|
/** Who the box belongs to; names are unique per owner. A bare name is local to the host, `user@control-plane` later. */
|
|
1478
1536
|
owner: z3.string(),
|
|
@@ -1780,6 +1838,8 @@ async function listen(opts = {}) {
|
|
|
1780
1838
|
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
1839
|
}
|
|
1782
1840
|
for (const c of moved.conflicts) console.error(`warning: ${c} left in place: ${cfg.owner} already has one with that name`);
|
|
1841
|
+
const withIds = await migrateBoxIds(p);
|
|
1842
|
+
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
1843
|
const seeded = await seedUsageLedger(p);
|
|
1784
1844
|
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
1845
|
const socketPath = opts.socketPath ?? p.serverSock;
|