@ghalex/kvbox-server 0.0.17 → 0.1.1
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 +197 -44
- package/dist/kvbox-server.js.map +1 -1
- package/package.json +1 -1
package/dist/kvbox-server.js
CHANGED
|
@@ -66,6 +66,10 @@ function paths(home = kvboxHome()) {
|
|
|
66
66
|
* The next snapshot is a diff of the pages dirtied since, merged onto this. Absent after a boot.
|
|
67
67
|
*/
|
|
68
68
|
memBase: join(dir, "mem.base"),
|
|
69
|
+
/** `kvbox hibernate`: the box's memory image and VM state while it is hibernated; gone again after `wake` or `stop`. */
|
|
70
|
+
hibernateDir: join(dir, "hibernate"),
|
|
71
|
+
hibernateVmstate: join(dir, "hibernate", "vmstate"),
|
|
72
|
+
hibernateMem: join(dir, "hibernate", "mem"),
|
|
69
73
|
sock: join(dir, "fc.sock"),
|
|
70
74
|
/** Firecracker's vsock endpoint: the host connects here to reach kvbox-agent inside the box (core/agent.ts). */
|
|
71
75
|
vsock: join(dir, "vsock.sock"),
|
|
@@ -295,7 +299,9 @@ var MachineMetaSchema = z2.object({
|
|
|
295
299
|
* each one, so when this is > 0 the next diff is only standalone merged onto `mem.base`; if that file
|
|
296
300
|
* is missing the lineage is broken and the next snapshot has to be a full dump. Absent: 0.
|
|
297
301
|
*/
|
|
298
|
-
memSnapshots: z2.number().int().optional()
|
|
302
|
+
memSnapshots: z2.number().int().optional(),
|
|
303
|
+
/** `kvbox pause`: Firecracker is alive but the VM is frozen (standby). Absent: not paused. */
|
|
304
|
+
paused: z2.boolean().optional()
|
|
299
305
|
});
|
|
300
306
|
var SnapshotMetaSchema = z2.object({
|
|
301
307
|
name: z2.string(),
|
|
@@ -474,6 +480,7 @@ function migrate(db) {
|
|
|
474
480
|
}
|
|
475
481
|
}
|
|
476
482
|
}
|
|
483
|
+
var LIVE_STATES = ["running", "standby"];
|
|
477
484
|
function fromRaw(r) {
|
|
478
485
|
return { boxId: r.box_id, owner: r.owner, name: r.name, at: r.at, state: r.state, vcpu: r.vcpu, memMib: r.mem_mib, diskGib: r.disk_gib, by: r.by };
|
|
479
486
|
}
|
|
@@ -488,13 +495,13 @@ function insertRaw(e, db) {
|
|
|
488
495
|
}
|
|
489
496
|
function insertUsageEvent(e, p = paths()) {
|
|
490
497
|
const db = openDb(p);
|
|
491
|
-
if (e.state === "running") closeOpenRun(e.boxId, e.at, p);
|
|
498
|
+
if (e.state === "running" && e.by !== "resume") closeOpenRun(e.boxId, e.at, p);
|
|
492
499
|
insertRaw(e, db);
|
|
493
500
|
}
|
|
494
501
|
function closeOpenRun(boxId, at, p = paths()) {
|
|
495
502
|
const db = openDb(p);
|
|
496
503
|
const last = lastEvent(boxId, db);
|
|
497
|
-
if (last === void 0 || last.state
|
|
504
|
+
if (last === void 0 || !LIVE_STATES.includes(last.state)) return false;
|
|
498
505
|
insertRaw({ ...last, at, state: "stopped", by: "server" }, db);
|
|
499
506
|
return true;
|
|
500
507
|
}
|
|
@@ -771,6 +778,16 @@ function recordState(meta, state, by, p, at = (/* @__PURE__ */ new Date()).toISO
|
|
|
771
778
|
console.error(`warning: usage ledger: ${err.message}`);
|
|
772
779
|
}
|
|
773
780
|
}
|
|
781
|
+
function isHibernated(meta, p) {
|
|
782
|
+
return !pidAlive(meta.pid) && existsSync3(p.machine(meta.owner, meta.name).hibernateVmstate);
|
|
783
|
+
}
|
|
784
|
+
function statusOf(meta, p = paths()) {
|
|
785
|
+
if (pidAlive(meta.pid)) return meta.paused ? "standby" : "running";
|
|
786
|
+
return isHibernated(meta, p) ? "hibernated" : "stopped";
|
|
787
|
+
}
|
|
788
|
+
function stateMessage(ref, status) {
|
|
789
|
+
return `box ${shown(ref)} is ${status === "standby" ? "in standby" : status}`;
|
|
790
|
+
}
|
|
774
791
|
function toMachine(meta, p = paths()) {
|
|
775
792
|
const alive = pidAlive(meta.pid);
|
|
776
793
|
if (!alive && meta.pid !== null) {
|
|
@@ -792,7 +809,7 @@ function toMachine(meta, p = paths()) {
|
|
|
792
809
|
base: meta.base ?? null,
|
|
793
810
|
createdAt: meta.createdAt,
|
|
794
811
|
source: meta.source,
|
|
795
|
-
status:
|
|
812
|
+
status: statusOf(meta, p)
|
|
796
813
|
};
|
|
797
814
|
}
|
|
798
815
|
async function readAllMeta(p) {
|
|
@@ -936,27 +953,26 @@ async function launch(spec, tap, cfg, pubkeyB64, p) {
|
|
|
936
953
|
throw err;
|
|
937
954
|
}
|
|
938
955
|
}
|
|
939
|
-
async function loadSnapshot(spec,
|
|
956
|
+
async function loadSnapshot(spec, image, tap, cfg, p) {
|
|
940
957
|
const m = p.machine(spec.owner, spec.name);
|
|
941
|
-
const s = p.snapshot(snapshot.owner, snapshot.name);
|
|
942
958
|
const pid = await spawnFirecracker(spec, cfg, p);
|
|
943
959
|
try {
|
|
944
960
|
const fc = new FirecrackerClient(m.sock);
|
|
945
961
|
try {
|
|
946
962
|
try {
|
|
947
963
|
await fc.snapshotLoad({
|
|
948
|
-
snapshotPath:
|
|
949
|
-
memFilePath:
|
|
964
|
+
snapshotPath: image.vmstate,
|
|
965
|
+
memFilePath: image.mem,
|
|
950
966
|
tap: { ifaceId: "eth0", hostDevName: tap },
|
|
951
967
|
vsockPath: m.vsock,
|
|
952
968
|
trackDirtyPages: true,
|
|
953
969
|
resume: false
|
|
954
970
|
});
|
|
955
971
|
} catch (err) {
|
|
956
|
-
if (/Unknown Vsock Device/.test(err.message)) throw new KvboxError(tooOldToRestore(
|
|
972
|
+
if (/Unknown Vsock Device/.test(err.message)) throw new KvboxError(tooOldToRestore(image.label));
|
|
957
973
|
throw err;
|
|
958
974
|
}
|
|
959
|
-
if (await namesOverlayAbsolutely(
|
|
975
|
+
if (await namesOverlayAbsolutely(image.vmstate)) await fc.patchDrivePath("overlay", m.overlay);
|
|
960
976
|
} finally {
|
|
961
977
|
await fc.close();
|
|
962
978
|
}
|
|
@@ -1060,14 +1076,12 @@ async function restoreMachine(fromText, owner, req, cfg, p) {
|
|
|
1060
1076
|
const tap = await createTap(name, cfg, { detached: true });
|
|
1061
1077
|
tapCreated = true;
|
|
1062
1078
|
const spec = { name, owner, ip, mac, vcpu: snap.vcpu, memMib: snap.memMib, base: snap.base };
|
|
1063
|
-
pid = await loadSnapshot(spec, from, tap, cfg, p);
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
}
|
|
1070
|
-
await introduce(spec, m.vsock, cfg, shown(from));
|
|
1079
|
+
pid = await loadSnapshot(spec, { vmstate: s.vmstate, mem: s.mem, label: shown(from) }, tap, cfg, p);
|
|
1080
|
+
await resumeVm(m.sock);
|
|
1081
|
+
await introduce(spec, m.vsock, cfg, {
|
|
1082
|
+
silent: `nothing answered on the restored box's control channel for 3 s; ${tooOldToRestore(shown(from))}`,
|
|
1083
|
+
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
|
+
});
|
|
1071
1085
|
await attachTap(name, cfg);
|
|
1072
1086
|
const meta = { ...spec, pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), source: `snapshot:${refKey(from)}`, memSnapshots: 1 };
|
|
1073
1087
|
await writeMachineMeta(meta, p);
|
|
@@ -1081,7 +1095,7 @@ async function restoreMachine(fromText, owner, req, cfg, p) {
|
|
|
1081
1095
|
}
|
|
1082
1096
|
}
|
|
1083
1097
|
var AGENT_REPLY_MS = 2e4;
|
|
1084
|
-
async function introduce(spec, vsock, cfg,
|
|
1098
|
+
async function introduce(spec, vsock, cfg, failure) {
|
|
1085
1099
|
const id = { name: spec.name, ip: spec.ip, prefixLen: prefixLenOf(cfg.subnet), gateway: cfg.gateway, mac: spec.mac, time: Math.floor(Date.now() / 1e3) };
|
|
1086
1100
|
const deadline = Date.now() + 3e3;
|
|
1087
1101
|
for (; ; ) {
|
|
@@ -1096,31 +1110,43 @@ async function introduce(spec, vsock, cfg, from) {
|
|
|
1096
1110
|
continue;
|
|
1097
1111
|
}
|
|
1098
1112
|
if (transient) throw new KvboxError(`${msg}
|
|
1099
|
-
|
|
1113
|
+
${failure.silent}`);
|
|
1100
1114
|
throw new KvboxError(`${msg}
|
|
1101
|
-
|
|
1115
|
+
${failure.noAnswer}`);
|
|
1102
1116
|
}
|
|
1103
1117
|
}
|
|
1104
1118
|
}
|
|
1119
|
+
async function resumeVm(sock) {
|
|
1120
|
+
const fc = new FirecrackerClient(sock);
|
|
1121
|
+
try {
|
|
1122
|
+
await fc.resume();
|
|
1123
|
+
} finally {
|
|
1124
|
+
await fc.close();
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1105
1127
|
async function shutdown(meta, cfg, p, graceMs) {
|
|
1106
1128
|
const pid = meta.pid;
|
|
1107
1129
|
if (pidAlive(pid)) {
|
|
1108
1130
|
const fc = new FirecrackerClient(p.machine(meta.owner, meta.name).sock);
|
|
1109
1131
|
try {
|
|
1132
|
+
if (meta.paused) await fc.resume();
|
|
1110
1133
|
await fc.sendCtrlAltDel();
|
|
1111
1134
|
} catch {
|
|
1112
1135
|
process.kill(pid, "SIGTERM");
|
|
1113
1136
|
} finally {
|
|
1114
1137
|
await fc.close();
|
|
1115
1138
|
}
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1139
|
+
await waitDead(pid, graceMs);
|
|
1140
|
+
}
|
|
1141
|
+
await cleanupProcess(meta, cfg, p);
|
|
1142
|
+
}
|
|
1143
|
+
async function waitDead(pid, graceMs) {
|
|
1144
|
+
if (!await waitFor(() => !pidAlive(pid), graceMs, 100)) {
|
|
1145
|
+
killQuietly(pid);
|
|
1146
|
+
await waitFor(() => !pidAlive(pid), 1e3, 50);
|
|
1123
1147
|
}
|
|
1148
|
+
}
|
|
1149
|
+
async function cleanupProcess(meta, cfg, p) {
|
|
1124
1150
|
await deleteTap(meta.name, cfg);
|
|
1125
1151
|
const m = p.machine(meta.owner, meta.name);
|
|
1126
1152
|
await rm(m.sock, { force: true });
|
|
@@ -1129,10 +1155,12 @@ async function shutdown(meta, cfg, p, graceMs) {
|
|
|
1129
1155
|
async function stopMachine(ref, p = paths()) {
|
|
1130
1156
|
const meta = await readMeta(ref, p);
|
|
1131
1157
|
const cfg = await loadConfig(p);
|
|
1132
|
-
|
|
1158
|
+
const m = p.machine(ref.owner, ref.name);
|
|
1159
|
+
if (!pidAlive(meta.pid) && !isHibernated(meta, p)) throw new KvboxError(`box ${shown(ref)} is already stopped`);
|
|
1133
1160
|
await shutdown(meta, cfg, p, 1e4);
|
|
1134
|
-
await rm(
|
|
1135
|
-
|
|
1161
|
+
await rm(m.hibernateDir, { recursive: true, force: true });
|
|
1162
|
+
await rm(m.memBase, { force: true });
|
|
1163
|
+
const stopped = { ...meta, pid: null, memSnapshots: void 0, paused: void 0 };
|
|
1136
1164
|
await writeMachineMeta(stopped, p);
|
|
1137
1165
|
recordState(stopped, "stopped", "stop", p);
|
|
1138
1166
|
return toMachine(stopped, p);
|
|
@@ -1140,7 +1168,10 @@ async function stopMachine(ref, p = paths()) {
|
|
|
1140
1168
|
async function startMachine(ref, p = paths()) {
|
|
1141
1169
|
const meta = await readMeta(ref, p);
|
|
1142
1170
|
const cfg = await loadConfig(p);
|
|
1143
|
-
if (pidAlive(meta.pid)) throw new KvboxError(`box ${shown(ref)} is already running`);
|
|
1171
|
+
if (pidAlive(meta.pid)) throw new KvboxError(`box ${shown(ref)} is already ${meta.paused ? "up, in standby (kvbox resume " + shown(ref) + ")" : "running"}`);
|
|
1172
|
+
if (isHibernated(meta, p)) {
|
|
1173
|
+
throw new KvboxError(`${stateMessage(ref, "hibernated")}: kvbox wake ${shown(ref)} continues where it left off; to boot fresh instead, kvbox stop ${shown(ref)} first (discards its memory)`);
|
|
1174
|
+
}
|
|
1144
1175
|
assertAssets(cfg);
|
|
1145
1176
|
const pubkeyB64 = await readPubkeyB64(p);
|
|
1146
1177
|
await ensureHostNetwork(cfg);
|
|
@@ -1156,21 +1187,125 @@ async function startMachine(ref, p = paths()) {
|
|
|
1156
1187
|
throw err;
|
|
1157
1188
|
}
|
|
1158
1189
|
}
|
|
1190
|
+
async function pauseMachine(ref, p = paths()) {
|
|
1191
|
+
const meta = await readMeta(ref, p);
|
|
1192
|
+
const status = statusOf(meta, p);
|
|
1193
|
+
if (status === "standby") throw new KvboxError(`${stateMessage(ref, status)} already`);
|
|
1194
|
+
if (status === "hibernated") throw new KvboxError(`${stateMessage(ref, status)}; kvbox wake ${shown(ref)} brings it back`);
|
|
1195
|
+
if (status === "stopped") throw new KvboxError(`${stateMessage(ref, status)}; only a running box can be paused`);
|
|
1196
|
+
const fc = new FirecrackerClient(p.machine(ref.owner, ref.name).sock);
|
|
1197
|
+
try {
|
|
1198
|
+
await fc.pause();
|
|
1199
|
+
} finally {
|
|
1200
|
+
await fc.close();
|
|
1201
|
+
}
|
|
1202
|
+
const paused = { ...meta, paused: true };
|
|
1203
|
+
await writeMachineMeta(paused, p);
|
|
1204
|
+
recordState(paused, "standby", "pause", p);
|
|
1205
|
+
return toMachine(paused, p);
|
|
1206
|
+
}
|
|
1207
|
+
async function resumeMachine(ref, p = paths()) {
|
|
1208
|
+
const meta = await readMeta(ref, p);
|
|
1209
|
+
const status = statusOf(meta, p);
|
|
1210
|
+
if (status === "running") throw new KvboxError(`${stateMessage(ref, status)} already`);
|
|
1211
|
+
if (status === "hibernated") throw new KvboxError(`${stateMessage(ref, status)}, not in standby; kvbox wake ${shown(ref)} brings it back`);
|
|
1212
|
+
if (status === "stopped") throw new KvboxError(`${stateMessage(ref, status)}, not in standby; kvbox start ${shown(ref)} boots it`);
|
|
1213
|
+
await resumeVm(p.machine(ref.owner, ref.name).sock);
|
|
1214
|
+
const running = { ...meta, paused: void 0 };
|
|
1215
|
+
await writeMachineMeta(running, p);
|
|
1216
|
+
recordState(running, "running", "resume", p);
|
|
1217
|
+
return toMachine(running, p);
|
|
1218
|
+
}
|
|
1219
|
+
async function hibernateMachine(ref, p = paths()) {
|
|
1220
|
+
const meta = await readMeta(ref, p);
|
|
1221
|
+
const cfg = await loadConfig(p);
|
|
1222
|
+
const status = statusOf(meta, p);
|
|
1223
|
+
if (status === "hibernated") throw new KvboxError(`${stateMessage(ref, status)} already`);
|
|
1224
|
+
if (status === "stopped") throw new KvboxError(`${stateMessage(ref, status)}; only a running box can be hibernated (there is no memory to keep)`);
|
|
1225
|
+
if (meta.base === void 0) throw new KvboxError(`box ${shown(ref)} was made before overlays and runs on its own disk copy; it cannot be hibernated`);
|
|
1226
|
+
const m = p.machine(ref.owner, ref.name);
|
|
1227
|
+
await rm(m.hibernateDir, { recursive: true, force: true });
|
|
1228
|
+
await mkdir3(m.hibernateDir, { recursive: true, mode: 448 });
|
|
1229
|
+
const fc = new FirecrackerClient(m.sock);
|
|
1230
|
+
try {
|
|
1231
|
+
try {
|
|
1232
|
+
await fc.pause();
|
|
1233
|
+
await fc.snapshotCreate(m.hibernateVmstate, m.hibernateMem, "Full");
|
|
1234
|
+
} catch (err) {
|
|
1235
|
+
await rm(m.hibernateDir, { recursive: true, force: true });
|
|
1236
|
+
if (!meta.paused) await fc.resume().catch(() => void 0);
|
|
1237
|
+
throw err;
|
|
1238
|
+
}
|
|
1239
|
+
} finally {
|
|
1240
|
+
await fc.close();
|
|
1241
|
+
}
|
|
1242
|
+
killQuietly(meta.pid);
|
|
1243
|
+
await waitFor(() => !pidAlive(meta.pid), 5e3, 50);
|
|
1244
|
+
await cleanupProcess(meta, cfg, p);
|
|
1245
|
+
await run(["fallocate", "--dig-holes", m.hibernateMem]);
|
|
1246
|
+
await rm(m.memBase, { force: true });
|
|
1247
|
+
const hibernated = { ...meta, pid: null, memSnapshots: void 0, paused: void 0 };
|
|
1248
|
+
await writeMachineMeta(hibernated, p);
|
|
1249
|
+
recordState(hibernated, "hibernated", "hibernate", p);
|
|
1250
|
+
return toMachine(hibernated, p);
|
|
1251
|
+
}
|
|
1252
|
+
async function wakeMachine(ref, p = paths()) {
|
|
1253
|
+
const meta = await readMeta(ref, p);
|
|
1254
|
+
const cfg = await loadConfig(p);
|
|
1255
|
+
const status = statusOf(meta, p);
|
|
1256
|
+
if (status === "running" || status === "standby") throw new KvboxError(`${stateMessage(ref, status)}, not hibernated`);
|
|
1257
|
+
if (status === "stopped") throw new KvboxError(`${stateMessage(ref, status)}, not hibernated; kvbox start ${shown(ref)} boots it`);
|
|
1258
|
+
assertAssets(cfg);
|
|
1259
|
+
if (meta.base !== void 0 && !existsSync3(p.baseImage(meta.base))) {
|
|
1260
|
+
throw new KvboxError(`box ${shown(ref)} stacks on base image ${meta.base}, which is gone from ${p.assets}`);
|
|
1261
|
+
}
|
|
1262
|
+
const m = p.machine(ref.owner, ref.name);
|
|
1263
|
+
await ensureHostNetwork(cfg);
|
|
1264
|
+
const tap = await createTap(ref.name, cfg, { detached: true });
|
|
1265
|
+
let pid;
|
|
1266
|
+
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 };
|
|
1268
|
+
pid = await loadSnapshot(spec, { vmstate: m.hibernateVmstate, mem: m.hibernateMem, label: `box ${shown(ref)}'s memory image` }, tap, cfg, p);
|
|
1269
|
+
await resumeVm(m.sock);
|
|
1270
|
+
await introduce(spec, m.vsock, cfg, {
|
|
1271
|
+
silent: `nothing answered on the box's control channel for 3 s; it stays hibernated, try kvbox wake ${shown(ref)} again`,
|
|
1272
|
+
noAnswer: `the box stays hibernated. kvbox-agent inside it took the request but gave no answer; try kvbox wake ${shown(ref)} again`
|
|
1273
|
+
});
|
|
1274
|
+
await attachTap(ref.name, cfg);
|
|
1275
|
+
} catch (err) {
|
|
1276
|
+
if (pid !== void 0) killQuietly(pid);
|
|
1277
|
+
await deleteTap(ref.name, cfg).catch(() => void 0);
|
|
1278
|
+
throw err;
|
|
1279
|
+
}
|
|
1280
|
+
await rm(m.memBase, { force: true });
|
|
1281
|
+
await link(m.hibernateMem, m.memBase);
|
|
1282
|
+
await rm(m.hibernateDir, { recursive: true, force: true });
|
|
1283
|
+
const running = { ...meta, pid, memSnapshots: 1, paused: void 0 };
|
|
1284
|
+
await writeMachineMeta(running, p);
|
|
1285
|
+
recordState(running, "running", "wake", p);
|
|
1286
|
+
return toMachine(running, p);
|
|
1287
|
+
}
|
|
1159
1288
|
async function applyAtNextBoot(meta, changes, by, cfg, p, whileStopped) {
|
|
1160
1289
|
const wasRunning = pidAlive(meta.pid);
|
|
1161
1290
|
if (wasRunning) {
|
|
1162
1291
|
await shutdown(meta, cfg, p, 1e4);
|
|
1163
1292
|
await rm(p.machine(meta.owner, meta.name).memBase, { force: true });
|
|
1164
1293
|
}
|
|
1165
|
-
const updated = { ...meta, ...changes, pid: null, memSnapshots: void 0 };
|
|
1294
|
+
const updated = { ...meta, ...changes, pid: null, memSnapshots: void 0, paused: void 0 };
|
|
1166
1295
|
await writeMachineMeta(updated, p);
|
|
1167
1296
|
if (whileStopped) await whileStopped();
|
|
1168
1297
|
recordState(updated, "stopped", by, p);
|
|
1169
1298
|
return wasRunning ? startMachine(meta, p) : toMachine(updated, p);
|
|
1170
1299
|
}
|
|
1300
|
+
function assertNotHibernated(meta, p) {
|
|
1301
|
+
if (isHibernated(meta, p)) {
|
|
1302
|
+
throw new KvboxError(`${stateMessage(meta, "hibernated")}; its memory image was made with its current shape and base: kvbox wake ${shown(meta)} first`);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1171
1305
|
async function updateMachine(ref, p = paths()) {
|
|
1172
1306
|
const meta = await readMeta(ref, p);
|
|
1173
1307
|
const cfg = await loadConfig(p);
|
|
1308
|
+
assertNotHibernated(meta, p);
|
|
1174
1309
|
if (meta.base === void 0) {
|
|
1175
1310
|
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`);
|
|
1176
1311
|
}
|
|
@@ -1183,6 +1318,7 @@ async function updateMachine(ref, p = paths()) {
|
|
|
1183
1318
|
async function rescaleMachine(ref, req, p = paths()) {
|
|
1184
1319
|
const meta = await readMeta(ref, p);
|
|
1185
1320
|
const cfg = await loadConfig(p);
|
|
1321
|
+
assertNotHibernated(meta, p);
|
|
1186
1322
|
const disk = ownDisk(meta, p);
|
|
1187
1323
|
const currentDiskGib = Math.round(statSync(disk).size / 1024 ** 3);
|
|
1188
1324
|
const preset = req.size !== void 0 && isSizeName(req.size) ? SIZES[req.size] : void 0;
|
|
@@ -1229,7 +1365,7 @@ function computeUsage(events, w) {
|
|
|
1229
1365
|
const sorted = [...events].sort((a, b) => a.at < b.at ? -1 : a.at > b.at ? 1 : 0);
|
|
1230
1366
|
const from = Date.parse(w.from);
|
|
1231
1367
|
const to = Date.parse(w.to);
|
|
1232
|
-
const ms = { running: 0, stopped: 0 };
|
|
1368
|
+
const ms = { running: 0, standby: 0, hibernated: 0, stopped: 0 };
|
|
1233
1369
|
let vcpuHours = 0;
|
|
1234
1370
|
let ramGibHours = 0;
|
|
1235
1371
|
let diskGibHours = 0;
|
|
@@ -1240,14 +1376,17 @@ function computeUsage(events, w) {
|
|
|
1240
1376
|
if (end <= start) return;
|
|
1241
1377
|
const h = (end - start) / HOUR_MS;
|
|
1242
1378
|
ms[e.state] += end - start;
|
|
1243
|
-
if (e.state === "running")
|
|
1244
|
-
|
|
1245
|
-
ramGibHours += h * e.memMib / 1024;
|
|
1246
|
-
}
|
|
1379
|
+
if (e.state === "running") vcpuHours += h * e.vcpu;
|
|
1380
|
+
if (e.state === "running" || e.state === "standby") ramGibHours += h * e.memMib / 1024;
|
|
1247
1381
|
diskGibHours += h * e.diskGib;
|
|
1248
1382
|
});
|
|
1249
1383
|
const totals = {
|
|
1250
|
-
seconds: {
|
|
1384
|
+
seconds: {
|
|
1385
|
+
running: Math.round(ms.running / 1e3),
|
|
1386
|
+
standby: Math.round(ms.standby / 1e3),
|
|
1387
|
+
hibernated: Math.round(ms.hibernated / 1e3),
|
|
1388
|
+
stopped: Math.round(ms.stopped / 1e3)
|
|
1389
|
+
},
|
|
1251
1390
|
vcpuHours: round(vcpuHours, 3),
|
|
1252
1391
|
ramGibHours: round(ramGibHours, 3),
|
|
1253
1392
|
diskGibHours: round(diskGibHours, 3),
|
|
@@ -1307,7 +1446,7 @@ function versionOf(srcFileUrl, injected) {
|
|
|
1307
1446
|
}
|
|
1308
1447
|
|
|
1309
1448
|
// src/version.ts
|
|
1310
|
-
var VERSION = versionOf(import.meta.url, true ? "0.
|
|
1449
|
+
var VERSION = versionOf(import.meta.url, true ? "0.1.1" : void 0);
|
|
1311
1450
|
|
|
1312
1451
|
// src/caller.ts
|
|
1313
1452
|
function resolveCaller() {
|
|
@@ -1332,6 +1471,7 @@ import { Hono as Hono2 } from "hono";
|
|
|
1332
1471
|
// ../shared/src/api.ts
|
|
1333
1472
|
import { z as z3 } from "zod";
|
|
1334
1473
|
var SizeSchema = z3.enum(SIZE_NAMES);
|
|
1474
|
+
var BoxStatusSchema = z3.enum(["running", "standby", "hibernated", "stopped"]);
|
|
1335
1475
|
var MachineSchema = z3.object({
|
|
1336
1476
|
name: z3.string(),
|
|
1337
1477
|
/** Who the box belongs to; names are unique per owner. A bare name is local to the host, `user@control-plane` later. */
|
|
@@ -1350,7 +1490,7 @@ var MachineSchema = z3.object({
|
|
|
1350
1490
|
createdAt: z3.string(),
|
|
1351
1491
|
source: z3.string(),
|
|
1352
1492
|
// "base" or "snapshot:<name>"
|
|
1353
|
-
status:
|
|
1493
|
+
status: BoxStatusSchema
|
|
1354
1494
|
});
|
|
1355
1495
|
var CreateMachineRequest = z3.object({
|
|
1356
1496
|
name: z3.string().optional(),
|
|
@@ -1402,9 +1542,15 @@ var HealthSchema = z3.object({
|
|
|
1402
1542
|
var MachineUsageSchema = z3.object({
|
|
1403
1543
|
name: z3.string(),
|
|
1404
1544
|
owner: z3.string(),
|
|
1405
|
-
status:
|
|
1545
|
+
status: BoxStatusSchema,
|
|
1406
1546
|
window: z3.object({ from: z3.string(), to: z3.string() }),
|
|
1407
|
-
|
|
1547
|
+
/** standby and hibernated default to 0 so a newer CLI reads an older server's answer. */
|
|
1548
|
+
seconds: z3.object({
|
|
1549
|
+
running: z3.number().int(),
|
|
1550
|
+
standby: z3.number().int().default(0),
|
|
1551
|
+
hibernated: z3.number().int().default(0),
|
|
1552
|
+
stopped: z3.number().int()
|
|
1553
|
+
}),
|
|
1408
1554
|
vcpuHours: z3.number(),
|
|
1409
1555
|
ramGibHours: z3.number(),
|
|
1410
1556
|
diskGibHours: z3.number(),
|
|
@@ -1427,6 +1573,10 @@ function machineRoutes() {
|
|
|
1427
1573
|
});
|
|
1428
1574
|
r.post("/:name/stop", async (c) => c.json(await stopMachine(parseRef(c.req.param("name"), c.get("owner")))));
|
|
1429
1575
|
r.post("/:name/start", async (c) => c.json(await startMachine(parseRef(c.req.param("name"), c.get("owner")))));
|
|
1576
|
+
r.post("/:name/pause", async (c) => c.json(await pauseMachine(parseRef(c.req.param("name"), c.get("owner")))));
|
|
1577
|
+
r.post("/:name/resume", async (c) => c.json(await resumeMachine(parseRef(c.req.param("name"), c.get("owner")))));
|
|
1578
|
+
r.post("/:name/hibernate", async (c) => c.json(await hibernateMachine(parseRef(c.req.param("name"), c.get("owner")))));
|
|
1579
|
+
r.post("/:name/wake", async (c) => c.json(await wakeMachine(parseRef(c.req.param("name"), c.get("owner")))));
|
|
1430
1580
|
r.post("/:name/update", async (c) => c.json(await updateMachine(parseRef(c.req.param("name"), c.get("owner")))));
|
|
1431
1581
|
r.post("/:name/rescale", async (c) => {
|
|
1432
1582
|
const body = RescaleMachineRequest.parse(await c.req.json().catch(() => ({})));
|
|
@@ -1504,6 +1654,9 @@ async function saveSnapshot(req, owner, p = paths()) {
|
|
|
1504
1654
|
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)`);
|
|
1505
1655
|
}
|
|
1506
1656
|
if (!pidAlive(box.pid)) {
|
|
1657
|
+
if (existsSync4(p.machine(box.owner, box.name).hibernateVmstate)) {
|
|
1658
|
+
throw new KvboxError(`box ${req.box} is hibernated; a snapshot freezes a running box (kvbox wake ${req.box} first)`);
|
|
1659
|
+
}
|
|
1507
1660
|
throw new KvboxError(`box ${req.box} is not running; a snapshot freezes a running box (kvbox start ${req.box} first)`);
|
|
1508
1661
|
}
|
|
1509
1662
|
const s = p.snapshot(owner, req.name);
|
|
@@ -1526,7 +1679,7 @@ async function saveSnapshot(req, owner, p = paths()) {
|
|
|
1526
1679
|
} catch (err) {
|
|
1527
1680
|
failure = err;
|
|
1528
1681
|
} finally {
|
|
1529
|
-
await fc.resume();
|
|
1682
|
+
if (!box.paused) await fc.resume();
|
|
1530
1683
|
}
|
|
1531
1684
|
if (lineageTouched) {
|
|
1532
1685
|
await writeMachineMeta({ ...box, memSnapshots: since + 1 }, p);
|