Veröffentlichte Quellen
Quellen als ZIP herunterladen
CH-J Tools
Durchsuchen Sie Verzeichnisse und Dateien einer bestimmten Anwendungsausgabe.
CH-J / Server Manager
406 Dateien
core-20261024T135205Z-7c487d719de2
Hinzugefügt: · Erstellt:
1
"use strict";3
// chj.disks: inventory model, operation planner, auto-expand suggestions and Core service contracts.4
// Synthetic inspect output only; nothing here talks to a server. Real-device tests: test/disksVmIntegration.test.js.5
const test = require("node:test");6
const assert = require("node:assert/strict");7
const { buildInventory } = require("../src/main/disks/inventory");8
const { planOperations, suggestions } = require("../src/main/disks/planner");9
const { DiskService, smartSummary } = require("../src/main/disks/diskService");10
const { inspectOutput, GiB, MiB, sectors } = require("./fixtures/disks/inspectFixture");12
const inv = (spec) => buildInventory(inspectOutput(spec));13
const names = (plan) => plan.steps.map((step) => step.name);14
const lines = (plan) => plan.planText.split("\n");15
const code = (expected) => (error) => { assert.equal(error.code, expected, error.message); return true; };16
const rootDisk = (extra = {}) => ({ name: "vda", sectors: sectors(20 * GiB), table: "gpt", partitions: [17
{ n: 1, start: 2048, size: sectors(512 * MiB), fs: "vfat", mount: "/boot/efi", used: 6 * MiB, type: "C12A7328-F81F-11D2-BA4B-00A0C93EC93B" },18
{ n: 2, start: 2048 + sectors(512 * MiB), size: sectors(10 * GiB), fs: "ext4", mount: "/", used: 3 * GiB }19
], ...extra });21
test("empty GPT disk: new partition is aligned, formatted and verified", () => {22
const inventory = inv({ disks: [{ name: "vdb", sectors: sectors(8 * GiB), table: "gpt", partitions: [] }] });23
const disk = inventory.disks[0];24
assert.equal(disk.segments.length, 1); assert.equal(disk.segments[0].kind, "free");25
const plan = planOperations(inventory, [{ type: "create-partition", disk: "vdb", start: 34, size: "max", fs: "ext4", label: "data" }]);26
assert.deepEqual(names(plan), ["verify-disk", "backup-table", "create-part", "mkfs", "verify-part"]);27
const create = plan.steps.find((step) => step.name === "create-part");28
assert.equal(create.args[1], "1"); assert.equal(create.args[2], "2048"); assert.equal(create.args[4], "0FC63DAF-8483-4772-8E79-3D69D8477DE4");29
assert.equal(plan.steps.find((step) => step.name === "mkfs").args.join(" "), "/dev/vdb1 ext4 data");30
assert.equal(plan.dataLoss, false);31
assert.equal(plan.after[0].segments[0].created, true);32
});34
test("disk with several partitions: delete one, then grow its neighbour into the freed space", () => {35
const inventory = inv({ disks: [{ name: "sdb", sectors: sectors(10 * GiB), table: "gpt", partitions: [36
{ n: 1, start: 2048, size: sectors(2 * GiB), fs: "ext4", used: 100 * MiB, fsSize: 2 * GiB },37
{ n: 2, start: 2048 + sectors(2 * GiB), size: sectors(2 * GiB), fs: "ext4", used: 0, fsSize: 2 * GiB }38
] }] });39
assert.throws(() => planOperations(inventory, [{ type: "resize-partition", disk: "sdb", number: 1, size: "max" }]), code("DISKS_NO_FREE_SPACE"));40
const plan = planOperations(inventory, [{ type: "delete-partition", disk: "sdb", number: 2 }, { type: "resize-partition", disk: "sdb", number: 1, size: "max" }]);41
assert.deepEqual(names(plan), ["verify-disk", "backup-table", "require-free", "delete-part", "resize-part", "grow-fs", "verify-part"]);42
assert.equal(plan.dataLoss, true);43
const resize = plan.steps.find((step) => step.name === "resize-part");44
assert.equal(Number(resize.args[4]), sectors(10 * GiB) - 34 - 2048 + 1, "grows to the last usable GPT sector (inclusive)");45
});47
test("ext4 with adjacent free space: online growth of the mounted root is suggested", () => {48
const inventory = inv({ disks: [rootDisk()] });49
const [suggestion] = inventory && suggestions(inventory);50
assert.equal(suggestion.kind, "partition"); assert.equal(suggestion.available, true); assert.equal(suggestion.target, "/dev/vda2");51
assert.ok(suggestion.freeBytes > 9 * GiB);52
assert.equal(suggestion.mayRequireReboot, true, "a partition in use may need a reboot if the kernel refuses");53
const plan = planOperations(inventory, suggestion.operations);54
assert.deepEqual(names(plan), ["verify-disk", "backup-table", "resize-part", "grow-fs", "verify-part"]);55
assert.equal(plan.steps.find((step) => step.name === "grow-fs").args.join(" "), "/dev/vda2 ext4");56
assert.equal(plan.dataLoss, false);57
});59
test("ext4 without adjacent free space: no suggestion and growth is refused", () => {60
const inventory = inv({ disks: [rootDisk({ partitions: [...rootDisk().partitions, { n: 3, start: 2048 + sectors(512 * MiB + 10 * GiB), size: sectors(9 * GiB), fs: "swap", swap: true }] })] });61
assert.equal(suggestions(inventory).filter((entry) => entry.target === "/dev/vda2").length, 0);62
assert.throws(() => planOperations(inventory, [{ type: "resize-partition", disk: "vda", number: 2, size: "max" }]), code("DISKS_NO_FREE_SPACE"));63
});65
test("offline ext4 shrink: check and shrink the file system before the partition, then verify", () => {66
const base = { name: "sdb", sectors: sectors(10 * GiB), table: "gpt", partitions: [{ n: 1, start: 2048, size: sectors(8 * GiB), fs: "ext4", used: 1 * GiB, fsSize: 8 * GiB }] };67
const plan = planOperations(inv({ disks: [base] }), [{ type: "resize-partition", disk: "sdb", number: 1, size: sectors(4 * GiB) }]);68
assert.deepEqual(names(plan), ["verify-disk", "backup-table", "shrink-fs", "resize-part", "verify-fs", "verify-part"]);69
assert.equal(plan.steps.find((step) => step.name === "shrink-fs").args[2], String(4 * GiB));70
assert.ok(plan.destructive && plan.dataLoss); assert.deepEqual(plan.offline, ["/dev/sdb1"]);71
assert.throws(() => planOperations(inv({ disks: [base] }), [{ type: "resize-partition", disk: "sdb", number: 1, size: sectors(1 * GiB) }]), code("DISKS_SIZE_TOO_SMALL"));72
const mounted = { ...base, partitions: [{ ...base.partitions[0], mount: "/srv/data" }] };73
assert.throws(() => planOperations(inv({ disks: [mounted] }), [{ type: "resize-partition", disk: "sdb", number: 1, size: sectors(4 * GiB) }]), code("DISKS_SHRINK_REQUIRES_UNMOUNT"));74
// A queued unmount makes the shrink possible within the same plan.75
const withUnmount = { ...base, partitions: [{ ...base.partitions[0], mount: "/mnt/data" }] };76
const queued = planOperations(inv({ disks: [withUnmount] }), [{ type: "unmount", device: "/dev/sdb1" }, { type: "resize-partition", disk: "sdb", number: 1, size: sectors(4 * GiB) }]);77
assert.deepEqual(names(queued).slice(2, 4), ["unmount", "shrink-fs"]);78
});80
test("the mounted system root is never shrunk online and never unmounted", () => {81
const inventory = inv({ disks: [rootDisk({ sectors: sectors(11 * GiB) })] });82
assert.throws(() => planOperations(inventory, [{ type: "resize-partition", disk: "vda", number: 2, size: sectors(6 * GiB) }]), (error) => error.code === "DISKS_SHRINK_REQUIRES_UNMOUNT" && /rescue/.test(error.message));83
assert.throws(() => planOperations(inventory, [{ type: "unmount", device: "/dev/vda2" }]), code("DISKS_SYSTEM_MOUNT"));84
assert.throws(() => planOperations(inventory, [{ type: "delete-partition", disk: "vda", number: 2 }]), code("DISKS_DEVICE_IN_USE"));85
assert.throws(() => planOperations(inventory, [{ type: "format", device: "/dev/vda2", fs: "xfs" }]), code("DISKS_DEVICE_IN_USE"));86
assert.throws(() => planOperations(inventory, [{ type: "create-table", disk: "vda", label: "gpt" }]), code("DISKS_DEVICE_IN_USE"));87
assert.deepEqual(inventory.disks[0].systemRoles.sort(), ["mount:/", "mount:/boot/efi"]);88
});90
test("XFS: online growth only while mounted, shrinking is refused with an explanation", () => {91
const disk = (mount) => ({ name: "sdc", sectors: sectors(10 * GiB), table: "gpt", partitions: [{ n: 1, start: 2048, size: sectors(4 * GiB), fs: "xfs", mount, used: 100 * MiB, fsSize: 4 * GiB }] });92
const plan = planOperations(inv({ disks: [disk("/data")] }), [{ type: "resize-partition", disk: "sdc", number: 1, size: "max" }]);93
assert.deepEqual(names(plan).slice(2, 4), ["resize-part", "grow-fs"]);94
assert.throws(() => planOperations(inv({ disks: [disk(undefined)] }), [{ type: "resize-partition", disk: "sdc", number: 1, size: "max" }]), code("DISKS_XFS_GROW_NEEDS_MOUNT"));95
assert.throws(() => planOperations(inv({ disks: [disk(undefined)] }), [{ type: "resize-partition", disk: "sdc", number: 1, size: sectors(2 * GiB) }]), (error) => error.code === "DISKS_XFS_SHRINK_UNSUPPORTED" && /migrate/.test(error.message));96
});98
const lvmSpec = ({ thin = false, lvs = 1 } = {}) => ({99
disks: [{ name: "sda", sectors: sectors(40 * GiB), table: "gpt", partitions: [{ n: 1, start: 2048, size: sectors(20 * GiB), fs: "LVM2_member", holders: ["dm-0"] }] }],100
dm: [{ name: "dm-0", bytes: 19 * GiB, dmName: "vg0-root", dmUuid: "LVM-abc", slaves: ["sda1"], fs: "ext4", mount: "/", used: 2 * GiB, fsSize: 19 * GiB }],101
lvm: {102
pvs: [{ pv_name: "/dev/sda1", vg_name: "vg0", pv_size: String(20 * GiB), pv_free: String(1 * GiB), dev_size: String(20 * GiB), pv_uuid: "pv", pv_attr: "a--" }],103
vgs: [{ vg_name: "vg0", vg_uuid: "vg", vg_size: String(20 * GiB), vg_free: String(1 * GiB), vg_extent_size: String(4 * MiB), vg_free_count: "256", pv_count: "1", lv_count: String(lvs), vg_attr: "wz--n-" }],104
lvs: [{ lv_name: "root", vg_name: "vg0", lv_uuid: "lv", lv_size: String(19 * GiB), lv_attr: thin ? "Vwi-aotz--" : "-wi-ao----", lv_path: "/dev/vg0/root", lv_dm_path: "/dev/mapper/vg0-root", pool_lv: thin ? "pool" : "", segtype: thin ? "thin" : "linear" }]105
}106
});108
test("LVM on a partition: the layered model and partition -> PV -> LV -> ext4 expansion", () => {109
const inventory = inv(lvmSpec());110
const root = inventory.nodes.find((node) => node.kname === "dm-0");111
assert.equal(root.displayPath, "/dev/vg0/root"); assert.equal(root.lvm.vg, "vg0"); assert.deepEqual(root.parents, ["sda1"]);112
assert.deepEqual(inventory.disks[0].systemRoles, ["mount:/"], "system role found through the LVM layer");113
const auto = suggestions(inventory).find((entry) => entry.kind === "partition");114
assert.equal(auto.available, true);115
const plan = planOperations(inventory, auto.operations);116
assert.deepEqual(names(plan), ["verify-disk", "verify-size", "backup-table", "resize-part", "pv-resize", "lv-extend", "grow-fs", "verify-part"]);117
assert.match(plan.planText, /lv-extend vg0\/root max/);118
assert.ok(suggestions(inventory).some((entry) => entry.kind === "lv" && entry.available), "free VG space is offered too");119
});121
test("LVM thin volumes are read-only and LV shrinking needs the advanced confirmation", () => {122
assert.throws(() => planOperations(inv(lvmSpec({ thin: true })), [{ type: "lv-extend", lv: "vg0/root", size: "max" }]), code("DISKS_LV_THIN_UNSUPPORTED"));123
assert.throws(() => planOperations(inv(lvmSpec()), [{ type: "lv-reduce", lv: "vg0/root", size: 10 * GiB }]), code("DISKS_ADVANCED_CONFIRMATION_REQUIRED"));124
assert.throws(() => planOperations(inv(lvmSpec()), [{ type: "lv-reduce", lv: "vg0/root", size: 10 * GiB, advanced: true }]), code("DISKS_SHRINK_REQUIRES_UNMOUNT"));125
});127
const luksSpec = (keyLocation, inner = "ext4") => ({128
disks: [{ name: "sdd", sectors: sectors(10 * GiB), table: "gpt", partitions: [{ n: 1, start: 2048, size: sectors(4 * GiB), fs: "crypto_LUKS", holders: ["dm-1"] }] }],129
dm: inner === "ext4"130
? [{ name: "dm-1", bytes: 4 * GiB - 16 * MiB, dmName: "secret", dmUuid: "CRYPT-LUKS2-0123-secret", slaves: ["sdd1"], fs: "ext4", mount: "/srv/secret", used: 10 * MiB, fsSize: 4 * GiB - 16 * MiB }]131
: [{ name: "dm-1", bytes: 4 * GiB - 16 * MiB, dmName: "secret", dmUuid: "CRYPT-LUKS2-0123-secret", slaves: ["sdd1"], holders: ["dm-2"], fs: "LVM2_member" },132
{ name: "dm-2", bytes: 3 * GiB, dmName: "vgs-data", dmUuid: "LVM-x", slaves: ["dm-1"], fs: "ext4", mount: "/srv/data", used: 1 * GiB, fsSize: 3 * GiB }],133
crypt: { secret: { keyLocation, device: "/dev/sdd1" } },134
lvm: inner === "ext4" ? undefined : {135
pvs: [{ pv_name: "/dev/mapper/secret", vg_name: "vgs", pv_size: String(4 * GiB - 16 * MiB), pv_free: String(1 * GiB - 16 * MiB), dev_size: String(4 * GiB - 16 * MiB) }],136
vgs: [{ vg_name: "vgs", vg_size: String(4 * GiB - 16 * MiB), vg_free: String(1 * GiB - 16 * MiB), vg_extent_size: String(4 * MiB) }],137
lvs: [{ lv_name: "data", vg_name: "vgs", lv_size: String(3 * GiB), lv_attr: "-wi-ao----", lv_path: "/dev/vgs/data", lv_dm_path: "/dev/mapper/vgs-data", segtype: "linear" }]138
}139
});141
test("LUKS with ext4: resize order disk -> partition -> LUKS mapping -> file system; never with a keyring key", () => {142
const plan = planOperations(inv(luksSpec("dm-crypt")), [{ type: "resize-partition", disk: "sdd", number: 1, size: "max" }]);143
assert.deepEqual(names(plan), ["verify-disk", "backup-table", "resize-part", "luks-resize", "grow-fs", "verify-part"]);144
assert.match(plan.planText, /luks-resize secret\ngrow-fs \/dev\/mapper\/secret ext4/);145
const blocked = suggestions(inv(luksSpec("keyring"))).find((entry) => entry.kind === "partition");146
assert.equal(blocked.available, false); assert.equal(blocked.blocked.code, "DISKS_LUKS_PASSPHRASE_REQUIRED");147
assert.throws(() => planOperations(inv(luksSpec("dm-crypt")), [{ type: "resize-partition", disk: "sdd", number: 1, size: sectors(2 * GiB) }]), code("DISKS_LUKS_SHRINK_UNSUPPORTED"));148
const unverified = suggestions(inv(luksSpec(""))).find((entry) => entry.kind === "partition");149
assert.equal(unverified.blocked.code, "DISKS_LUKS_UNVERIFIED", "unverified configurations stay display-only");150
});152
test("LUKS with LVM inside: disk -> partition -> LUKS -> PV -> LV -> ext4", () => {153
const plan = planOperations(inv(luksSpec("dm-crypt", "lvm")), [{ type: "resize-partition", disk: "sdd", number: 1, size: "max", auto: true }]);154
assert.deepEqual(names(plan).filter((name) => !name.startsWith("verify") && name !== "backup-table"), ["resize-part", "luks-resize", "pv-resize", "lv-extend", "grow-fs"]);155
assert.match(plan.planText, /pv-resize \/dev\/mapper\/secret\nlv-extend vgs\/data max\ngrow-fs \/dev\/mapper\/vgs-data ext4/);156
});158
test("virtual disk enlarged on the hypervisor: the misplaced GPT backup header is relocated first", () => {159
const disk = rootDisk({ sectors: sectors(80 * GiB), lastlba: sectors(40 * GiB) - 34, partitions: [rootDisk().partitions[0], { ...rootDisk().partitions[1], size: sectors(40 * GiB) - 34 - 2048 - sectors(512 * MiB) - 2048 }] });160
const inventory = inv({ disks: [disk] });161
assert.equal(inventory.disks[0].table.gptBackupMisplaced, true);162
const [suggestion] = suggestions(inventory);163
assert.equal(suggestion.available, true); assert.ok(Math.abs(suggestion.freeBytes - 40 * GiB) < 2 * MiB, "about 40 GiB found");164
const plan = planOperations(inventory, suggestion.operations);165
assert.deepEqual(names(plan), ["verify-disk", "backup-table", "gpt-relocate", "resize-part", "grow-fs", "verify-part"]);166
});168
test("unprivileged inventory is partial and cannot plan; suggestions are blocked, not faked", () => {169
const inventory = inv({ uid: 1000, disks: [rootDisk()] });170
assert.equal(inventory.privileged, false); assert.equal(inventory.partial, true); assert.equal(inventory.disks[0].table.source, "sysfs");171
assert.throws(() => planOperations(inventory, [{ type: "grow-fs", device: "/dev/vda2" }]), code("DISKS_PRIVILEGE_REQUIRED"));172
assert.equal(suggestions(inventory)[0].blocked.code, "DISKS_PRIVILEGE_REQUIRED");173
});175
test("missing tools are reported, never installed", () => {176
const inventory = inv({ disks: [rootDisk()], withoutTools: ["resize2fs"] });177
assert.equal(suggestions(inventory)[0].blocked.code, "DISKS_TOOL_MISSING");178
assert.throws(() => planOperations(inv({ disks: [{ name: "vdb", sectors: sectors(GiB), table: "gpt", partitions: [] }], withoutTools: ["mkfs.xfs"] }), [{ type: "create-partition", disk: "vdb", start: 2048, size: "max", fs: "xfs" }]), code("DISKS_TOOL_MISSING"));179
});181
test("Proxmox host storage, partition moves and unsafe input are refused", () => {182
const pve = inv({ ...lvmSpec(), pve: true });183
assert.equal(pve.disks[0].protected, "pve-host-storage");184
assert.throws(() => planOperations(pve, [{ type: "create-table", disk: "sda", label: "gpt" }]), code("DISKS_PROTECTED_DEVICE"));185
const plain = inv({ disks: [rootDisk()] });186
assert.throws(() => planOperations(plain, [{ type: "move-partition", disk: "vda", number: 2 }]), code("DISKS_OPERATION_DISABLED"));187
assert.throws(() => planOperations(plain, [{ type: "grow-fs", device: "/dev/vda2; reboot" }]), code("DISKS_DEVICE_NOT_FOUND"));188
assert.throws(() => planOperations(plain, [{ type: "shell", command: "rm -rf /" }]), code("DISKS_ARGUMENTS_INVALID"));189
const free = inv({ disks: [{ name: "vdb", sectors: sectors(4 * GiB), table: "gpt", partitions: [{ n: 1, start: 2048, size: sectors(GiB), fs: "ext4", used: 0, fsSize: GiB }] }] });190
assert.throws(() => planOperations(free, [{ type: "set-label", device: "/dev/vdb1", label: "x$(id)" }]), code("DISKS_LABEL_INVALID"));191
assert.throws(() => planOperations(free, [{ type: "mount", device: "/dev/vdb1", mountpoint: "/etc" }]), code("DISKS_MOUNTPOINT_INVALID"));192
assert.throws(() => planOperations(free, [{ type: "mount", device: "/dev/vdb1", mountpoint: "/mnt/../etc" }]), code("DISKS_MOUNTPOINT_INVALID"));193
const mount = planOperations(free, [{ type: "mount", device: "/dev/vdb1", mountpoint: "/mnt/data" }]);194
assert.equal(lines(mount).at(-1), "mount /dev/vdb1 /mnt/data");195
});197
test("MBR: primary partitions only, boot flag and the 2 TiB limit", () => {198
const inventory = inv({ disks: [{ name: "sde", sectors: sectors(8 * GiB), table: "dos", partitions: [{ n: 1, start: 2048, size: sectors(GiB), fs: "ext4", used: 0, fsSize: GiB, bootable: false }] }] });199
const plan = planOperations(inventory, [{ type: "set-flag", disk: "sde", number: 1, flag: "boot", enabled: true }, { type: "create-partition", disk: "sde", start: 2048 + sectors(GiB), size: sectors(GiB), fs: "swap" }]);200
assert.match(plan.planText, /set-boot \/dev\/sde 1/); assert.match(plan.planText, /create-part \/dev\/sde 2 \d+ \d+ 82 -/);201
assert.throws(() => planOperations(inventory, [{ type: "set-name", disk: "sde", number: 1, name: "x" }]), code("DISKS_NAME_UNSUPPORTED"));202
});204
test("SMART: unavailable on virtual disks is reported as such", () => {205
const result = smartSummary({ smartctl: JSON.stringify({ smartctl: { messages: [{ string: "/dev/sda: Unable to detect device type", severity: "error" }], exit_status: 1 } }) });206
assert.equal(result.available, false); assert.match(result.reason, /Unable to detect/);207
const nvme = smartSummary({ smartctl: JSON.stringify({ model_name: "NVMe X", smart_status: { passed: true }, temperature: { current: 41 }, nvme_smart_health_information_log: { percentage_used: 3, media_errors: 0, critical_warning: 0, available_spare: 100 } }) });208
assert.equal(nvme.passed, true); assert.equal(nvme.percentageUsed, 3); assert.equal(nvme.temperatureC, 41);209
});211
// ---------- Core service contracts with a fake executor ----------212
function fakeService({ platform = { os: "linux" }, outputs = [] } = {}) {213
const calls = [];214
const record = { sessionId: "s", profileId: "p", host: "h", port: 22, username: "ai", state: "connected" };215
const sessionManager = { _requireConnected: () => record, _platform: async () => platform };216
const executor = {217
probe: async () => ({ mode: "root" }), hasCachedPassword: () => false,218
run: async (_id, options) => { calls.push(options); const next = outputs.shift(); if (typeof next === "function") return next(options); return { stdout: next ?? "" }; }219
};220
return { service: new DiskService({ sessionManager, privilegedExecutor: executor }), calls, record };221
}223
test("Windows and other non-Linux servers: no Linux tool is started", async () => {224
const { service, calls } = fakeService({ platform: { os: "windows", caption: "Windows Server 2025" } });225
const result = await service.inspect("s");226
assert.equal(result.supported, false); assert.equal(result.reason, "WINDOWS"); assert.equal(calls.length, 0);227
await assert.rejects(() => service.plan("s", { operations: [{ type: "grow-fs", device: "/dev/sda1" }] }), code("DISKS_PLATFORM_UNSUPPORTED"));228
await assert.rejects(() => service.smart("s", { device: "/dev/sda" }), code("DISKS_PLATFORM_UNSUPPORTED"));229
});231
test("apply: the layout is re-read; a changed layout is refused (stale plan) and plans are single-use", async () => {232
const before = inspectOutput({ disks: [rootDisk()] });233
const changed = inspectOutput({ disks: [rootDisk({ hash: "b".repeat(64) })] });234
const { service, calls } = fakeService({ outputs: [before, changed] });235
const plan = await service.plan("s", { operations: [{ type: "resize-partition", disk: "vda", number: 2, size: "max" }] });236
await assert.rejects(() => service.apply("s", { planId: plan.planId }), code("DISKS_CONFIRMATION_REQUIRED"));237
await assert.rejects(() => service.apply("s", { planId: plan.planId, confirm: true }), code("DISKS_PLAN_STALE"));238
assert.ok(!calls.some((call) => call.args[0] === "job-start"), "no job was started");239
await assert.rejects(() => service.apply("s", { planId: plan.planId, confirm: true }), code("DISKS_PLAN_EXPIRED"));240
});242
test("apply: an unchanged layout starts exactly one remote job with the reviewed steps", async () => {243
const output = inspectOutput({ disks: [rootDisk()] });244
const { service, calls, record } = fakeService({ outputs: [output, output, () => ({ stdout: "\x1eCHJ_SECTION=job\nid=x\nstate=running\nrunner=systemd\n" })] });245
const plan = await service.plan("s", { operations: [{ type: "resize-partition", disk: "vda", number: 2, size: "max" }] });246
const result = await service.apply("s", { planId: plan.planId, confirm: true });247
assert.equal(result.state, "running"); assert.match(result.jobId, /^[a-f0-9-]{36}$/);248
const start = calls.find((call) => call.args[0] === "job-start");249
assert.deepEqual(start.args, ["job-start", result.jobId]); assert.equal(start.privileged, true);250
assert.equal(start.frames[0].tag, "PLAN"); assert.equal(start.frames[0].data.trim(), plan.planText);251
assert.equal(start.frames[1].tag, "HELPER"); assert.match(start.frames[1].data, /chj\.disks/);252
// A second server (identity change) can never use the plan of another server.253
const other = await service.plan("s", { operations: [{ type: "resize-partition", disk: "vda", number: 2, size: "max" }] }).catch(() => null);254
if (other) { record.host = "other"; await assert.rejects(() => service.apply("s", { planId: other.planId, confirm: true }), code("DISKS_PLAN_SERVER_CHANGED")); }255
});257
test("concurrent apply on the same session is refused", async () => {258
const output = inspectOutput({ disks: [rootDisk()] });259
let release;260
const slow = () => new Promise((resolve) => { release = () => resolve({ stdout: output }); });261
const { service } = fakeService({ outputs: [output, output, slow] });262
const first = await service.plan("s", { operations: [{ type: "resize-partition", disk: "vda", number: 2, size: "max" }] });263
const second = await service.plan("s", { operations: [{ type: "resize-partition", disk: "vda", number: 2, size: "max" }] });264
const running = service.apply("s", { planId: first.planId, confirm: true });265
await new Promise((resolve) => setImmediate(resolve));266
await assert.rejects(() => service.apply("s", { planId: second.planId, confirm: true }), code("DISKS_BUSY"));267
release(); await running.catch(() => {});268
});270
test("job status: runner gone without a result is 'unknown' and never restarted", async () => {271
const status = "\x1eCHJ_SECTION=job\nid=abcdef12-0000\nstate=running\nstep=2\ntotal=5\nalive=0\nlogSize=40\n\x1eCHJ_SECTION=plan\nverify-disk /dev/vda 1 none\n\x1eCHJ_SECTION=log\n\x1eSTEP 1|5|done|\nline\n";272
const { service, calls } = fakeService({ outputs: [status] });273
const result = await service.jobStatus("s", { jobId: "abcdef12-0000", offset: 0 });274
assert.equal(result.outcome, "unknown"); assert.deepEqual(result.steps, [{ index: 1, total: 5, status: "done", step: "" }]);275
assert.equal(result.log, "line\n"); assert.equal(calls.length, 1); assert.deepEqual(calls[0].args, ["job-status", "abcdef12-0000", "0"]);276
await assert.rejects(() => service.jobStatus("s", { jobId: "../../etc" }), code("DISKS_ARGUMENTS_INVALID"));277
});279
test("boot partitions of the running system disk are never deleted or formatted", () => {280
const inventory = inv({ disks: [rootDisk({ partitions: [{ n: 14, start: 34 + 2014, size: 2048, type: "21686148-6449-6E6F-744E-656564454649" }, { ...rootDisk().partitions[0], n: 15, start: 4096, mount: undefined }, { ...rootDisk().partitions[1], start: 4096 + sectors(512 * MiB) }] })] });281
assert.throws(() => planOperations(inventory, [{ type: "delete-partition", disk: "vda", number: 14 }]), code("DISKS_SYSTEM_PARTITION"));282
assert.throws(() => planOperations(inventory, [{ type: "format", device: "/dev/vda15", fs: "vfat" }]), code("DISKS_SYSTEM_PARTITION"));283
});SHA-256
6f1964ee2709ca3c2255a7a076080bb2778a8d0f632dc0d8c92827d3be844fb6CH-J Proprietary Software License 1.14 · Die Quellen werden unter der CH-J Proprietary Software License 1.14 bereitgestellt. Ihre Verfügbarkeit ändert die Lizenzbedingungen nicht und gewährt keine zusätzlichen Rechte.
SHA-256 des Archivs
476ed8aef0f1e96a5acdf223127b6692acdf4492eeaf5d0816b6e2a634fbbda6