CH-J Toolsspráva serverů přes SSH
Menu
Vydané zdroje

CH-J Tools

Procházejte adresáře a soubory konkrétního vydání aplikace.

Stáhnout zdroje ZIP
CH-J / Server Manager 406 souborů
core-20261024T135205Z-7c487d719de2 Přidáno: · Sestaveno:
disksPlanner.test.js Přidáno: Datum není evidováno 24,1 KB · 283 řádků
Kód Stáhnout soubor
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.
5const test = require("node:test");
6const assert = require("node:assert/strict");
7const { buildInventory } = require("../src/main/disks/inventory");
8const { planOperations, suggestions } = require("../src/main/disks/planner");
9const { DiskService, smartSummary } = require("../src/main/disks/diskService");
10const { inspectOutput, GiB, MiB, sectors } = require("./fixtures/disks/inspectFixture");
12const inv = (spec) => buildInventory(inspectOutput(spec));
13const names = (plan) => plan.steps.map((step) => step.name);
14const lines = (plan) => plan.planText.split("\n");
15const code = (expected) => (error) => { assert.equal(error.code, expected, error.message); return true; };
16const 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 });
21test("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});
34test("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});
47test("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});
59test("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});
65test("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});
80test("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});
90test("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});
98const 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});
108test("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});
121test("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});
127const 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});
141test("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});
152test("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});
158test("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});
168test("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});
175test("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});
181test("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});
197test("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});
204test("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 ----------
212function 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 };
223test("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});
231test("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});
242test("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});
257test("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});
270test("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});
279test("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-2566f1964ee2709ca3c2255a7a076080bb2778a8d0f632dc0d8c92827d3be844fb6

CH-J Proprietary Software License 1.14 · Zdroje jsou zveřejněny pod CH-J Proprietary Software License 1.14. Jejich dostupnost nemění licenční podmínky ani neposkytuje další oprávnění.

SHA-256 archivu476ed8aef0f1e96a5acdf223127b6692acdf4492eeaf5d0816b6e2a634fbbda6
Soubory