"use strict"; // chj.disks: inventory model, operation planner, auto-expand suggestions and Core service contracts. // Synthetic inspect output only; nothing here talks to a server. Real-device tests: test/disksVmIntegration.test.js. const test = require("node:test"); const assert = require("node:assert/strict"); const { buildInventory } = require("../src/main/disks/inventory"); const { planOperations, suggestions } = require("../src/main/disks/planner"); const { DiskService, smartSummary } = require("../src/main/disks/diskService"); const { inspectOutput, GiB, MiB, sectors } = require("./fixtures/disks/inspectFixture"); const inv = (spec) => buildInventory(inspectOutput(spec)); const names = (plan) => plan.steps.map((step) => step.name); const lines = (plan) => plan.planText.split("\n"); const code = (expected) => (error) => { assert.equal(error.code, expected, error.message); return true; }; const rootDisk = (extra = {}) => ({ name: "vda", sectors: sectors(20 * GiB), table: "gpt", partitions: [ { n: 1, start: 2048, size: sectors(512 * MiB), fs: "vfat", mount: "/boot/efi", used: 6 * MiB, type: "C12A7328-F81F-11D2-BA4B-00A0C93EC93B" }, { n: 2, start: 2048 + sectors(512 * MiB), size: sectors(10 * GiB), fs: "ext4", mount: "/", used: 3 * GiB } ], ...extra }); test("empty GPT disk: new partition is aligned, formatted and verified", () => { const inventory = inv({ disks: [{ name: "vdb", sectors: sectors(8 * GiB), table: "gpt", partitions: [] }] }); const disk = inventory.disks[0]; assert.equal(disk.segments.length, 1); assert.equal(disk.segments[0].kind, "free"); const plan = planOperations(inventory, [{ type: "create-partition", disk: "vdb", start: 34, size: "max", fs: "ext4", label: "data" }]); assert.deepEqual(names(plan), ["verify-disk", "backup-table", "create-part", "mkfs", "verify-part"]); const create = plan.steps.find((step) => step.name === "create-part"); assert.equal(create.args[1], "1"); assert.equal(create.args[2], "2048"); assert.equal(create.args[4], "0FC63DAF-8483-4772-8E79-3D69D8477DE4"); assert.equal(plan.steps.find((step) => step.name === "mkfs").args.join(" "), "/dev/vdb1 ext4 data"); assert.equal(plan.dataLoss, false); assert.equal(plan.after[0].segments[0].created, true); }); test("disk with several partitions: delete one, then grow its neighbour into the freed space", () => { const inventory = inv({ disks: [{ name: "sdb", sectors: sectors(10 * GiB), table: "gpt", partitions: [ { n: 1, start: 2048, size: sectors(2 * GiB), fs: "ext4", used: 100 * MiB, fsSize: 2 * GiB }, { n: 2, start: 2048 + sectors(2 * GiB), size: sectors(2 * GiB), fs: "ext4", used: 0, fsSize: 2 * GiB } ] }] }); assert.throws(() => planOperations(inventory, [{ type: "resize-partition", disk: "sdb", number: 1, size: "max" }]), code("DISKS_NO_FREE_SPACE")); const plan = planOperations(inventory, [{ type: "delete-partition", disk: "sdb", number: 2 }, { type: "resize-partition", disk: "sdb", number: 1, size: "max" }]); assert.deepEqual(names(plan), ["verify-disk", "backup-table", "require-free", "delete-part", "resize-part", "grow-fs", "verify-part"]); assert.equal(plan.dataLoss, true); const resize = plan.steps.find((step) => step.name === "resize-part"); assert.equal(Number(resize.args[4]), sectors(10 * GiB) - 34 - 2048 + 1, "grows to the last usable GPT sector (inclusive)"); }); test("ext4 with adjacent free space: online growth of the mounted root is suggested", () => { const inventory = inv({ disks: [rootDisk()] }); const [suggestion] = inventory && suggestions(inventory); assert.equal(suggestion.kind, "partition"); assert.equal(suggestion.available, true); assert.equal(suggestion.target, "/dev/vda2"); assert.ok(suggestion.freeBytes > 9 * GiB); assert.equal(suggestion.mayRequireReboot, true, "a partition in use may need a reboot if the kernel refuses"); const plan = planOperations(inventory, suggestion.operations); assert.deepEqual(names(plan), ["verify-disk", "backup-table", "resize-part", "grow-fs", "verify-part"]); assert.equal(plan.steps.find((step) => step.name === "grow-fs").args.join(" "), "/dev/vda2 ext4"); assert.equal(plan.dataLoss, false); }); test("ext4 without adjacent free space: no suggestion and growth is refused", () => { 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 }] })] }); assert.equal(suggestions(inventory).filter((entry) => entry.target === "/dev/vda2").length, 0); assert.throws(() => planOperations(inventory, [{ type: "resize-partition", disk: "vda", number: 2, size: "max" }]), code("DISKS_NO_FREE_SPACE")); }); test("offline ext4 shrink: check and shrink the file system before the partition, then verify", () => { 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 }] }; const plan = planOperations(inv({ disks: [base] }), [{ type: "resize-partition", disk: "sdb", number: 1, size: sectors(4 * GiB) }]); assert.deepEqual(names(plan), ["verify-disk", "backup-table", "shrink-fs", "resize-part", "verify-fs", "verify-part"]); assert.equal(plan.steps.find((step) => step.name === "shrink-fs").args[2], String(4 * GiB)); assert.ok(plan.destructive && plan.dataLoss); assert.deepEqual(plan.offline, ["/dev/sdb1"]); assert.throws(() => planOperations(inv({ disks: [base] }), [{ type: "resize-partition", disk: "sdb", number: 1, size: sectors(1 * GiB) }]), code("DISKS_SIZE_TOO_SMALL")); const mounted = { ...base, partitions: [{ ...base.partitions[0], mount: "/srv/data" }] }; assert.throws(() => planOperations(inv({ disks: [mounted] }), [{ type: "resize-partition", disk: "sdb", number: 1, size: sectors(4 * GiB) }]), code("DISKS_SHRINK_REQUIRES_UNMOUNT")); // A queued unmount makes the shrink possible within the same plan. const withUnmount = { ...base, partitions: [{ ...base.partitions[0], mount: "/mnt/data" }] }; const queued = planOperations(inv({ disks: [withUnmount] }), [{ type: "unmount", device: "/dev/sdb1" }, { type: "resize-partition", disk: "sdb", number: 1, size: sectors(4 * GiB) }]); assert.deepEqual(names(queued).slice(2, 4), ["unmount", "shrink-fs"]); }); test("the mounted system root is never shrunk online and never unmounted", () => { const inventory = inv({ disks: [rootDisk({ sectors: sectors(11 * GiB) })] }); 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)); assert.throws(() => planOperations(inventory, [{ type: "unmount", device: "/dev/vda2" }]), code("DISKS_SYSTEM_MOUNT")); assert.throws(() => planOperations(inventory, [{ type: "delete-partition", disk: "vda", number: 2 }]), code("DISKS_DEVICE_IN_USE")); assert.throws(() => planOperations(inventory, [{ type: "format", device: "/dev/vda2", fs: "xfs" }]), code("DISKS_DEVICE_IN_USE")); assert.throws(() => planOperations(inventory, [{ type: "create-table", disk: "vda", label: "gpt" }]), code("DISKS_DEVICE_IN_USE")); assert.deepEqual(inventory.disks[0].systemRoles.sort(), ["mount:/", "mount:/boot/efi"]); }); test("XFS: online growth only while mounted, shrinking is refused with an explanation", () => { 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 }] }); const plan = planOperations(inv({ disks: [disk("/data")] }), [{ type: "resize-partition", disk: "sdc", number: 1, size: "max" }]); assert.deepEqual(names(plan).slice(2, 4), ["resize-part", "grow-fs"]); assert.throws(() => planOperations(inv({ disks: [disk(undefined)] }), [{ type: "resize-partition", disk: "sdc", number: 1, size: "max" }]), code("DISKS_XFS_GROW_NEEDS_MOUNT")); 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)); }); const lvmSpec = ({ thin = false, lvs = 1 } = {}) => ({ disks: [{ name: "sda", sectors: sectors(40 * GiB), table: "gpt", partitions: [{ n: 1, start: 2048, size: sectors(20 * GiB), fs: "LVM2_member", holders: ["dm-0"] }] }], dm: [{ name: "dm-0", bytes: 19 * GiB, dmName: "vg0-root", dmUuid: "LVM-abc", slaves: ["sda1"], fs: "ext4", mount: "/", used: 2 * GiB, fsSize: 19 * GiB }], lvm: { 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--" }], 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-" }], 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" }] } }); test("LVM on a partition: the layered model and partition -> PV -> LV -> ext4 expansion", () => { const inventory = inv(lvmSpec()); const root = inventory.nodes.find((node) => node.kname === "dm-0"); assert.equal(root.displayPath, "/dev/vg0/root"); assert.equal(root.lvm.vg, "vg0"); assert.deepEqual(root.parents, ["sda1"]); assert.deepEqual(inventory.disks[0].systemRoles, ["mount:/"], "system role found through the LVM layer"); const auto = suggestions(inventory).find((entry) => entry.kind === "partition"); assert.equal(auto.available, true); const plan = planOperations(inventory, auto.operations); assert.deepEqual(names(plan), ["verify-disk", "verify-size", "backup-table", "resize-part", "pv-resize", "lv-extend", "grow-fs", "verify-part"]); assert.match(plan.planText, /lv-extend vg0\/root max/); assert.ok(suggestions(inventory).some((entry) => entry.kind === "lv" && entry.available), "free VG space is offered too"); }); test("LVM thin volumes are read-only and LV shrinking needs the advanced confirmation", () => { assert.throws(() => planOperations(inv(lvmSpec({ thin: true })), [{ type: "lv-extend", lv: "vg0/root", size: "max" }]), code("DISKS_LV_THIN_UNSUPPORTED")); assert.throws(() => planOperations(inv(lvmSpec()), [{ type: "lv-reduce", lv: "vg0/root", size: 10 * GiB }]), code("DISKS_ADVANCED_CONFIRMATION_REQUIRED")); assert.throws(() => planOperations(inv(lvmSpec()), [{ type: "lv-reduce", lv: "vg0/root", size: 10 * GiB, advanced: true }]), code("DISKS_SHRINK_REQUIRES_UNMOUNT")); }); const luksSpec = (keyLocation, inner = "ext4") => ({ disks: [{ name: "sdd", sectors: sectors(10 * GiB), table: "gpt", partitions: [{ n: 1, start: 2048, size: sectors(4 * GiB), fs: "crypto_LUKS", holders: ["dm-1"] }] }], dm: inner === "ext4" ? [{ 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 }] : [{ name: "dm-1", bytes: 4 * GiB - 16 * MiB, dmName: "secret", dmUuid: "CRYPT-LUKS2-0123-secret", slaves: ["sdd1"], holders: ["dm-2"], fs: "LVM2_member" }, { 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 }], crypt: { secret: { keyLocation, device: "/dev/sdd1" } }, lvm: inner === "ext4" ? undefined : { 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) }], vgs: [{ vg_name: "vgs", vg_size: String(4 * GiB - 16 * MiB), vg_free: String(1 * GiB - 16 * MiB), vg_extent_size: String(4 * MiB) }], 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" }] } }); test("LUKS with ext4: resize order disk -> partition -> LUKS mapping -> file system; never with a keyring key", () => { const plan = planOperations(inv(luksSpec("dm-crypt")), [{ type: "resize-partition", disk: "sdd", number: 1, size: "max" }]); assert.deepEqual(names(plan), ["verify-disk", "backup-table", "resize-part", "luks-resize", "grow-fs", "verify-part"]); assert.match(plan.planText, /luks-resize secret\ngrow-fs \/dev\/mapper\/secret ext4/); const blocked = suggestions(inv(luksSpec("keyring"))).find((entry) => entry.kind === "partition"); assert.equal(blocked.available, false); assert.equal(blocked.blocked.code, "DISKS_LUKS_PASSPHRASE_REQUIRED"); assert.throws(() => planOperations(inv(luksSpec("dm-crypt")), [{ type: "resize-partition", disk: "sdd", number: 1, size: sectors(2 * GiB) }]), code("DISKS_LUKS_SHRINK_UNSUPPORTED")); const unverified = suggestions(inv(luksSpec(""))).find((entry) => entry.kind === "partition"); assert.equal(unverified.blocked.code, "DISKS_LUKS_UNVERIFIED", "unverified configurations stay display-only"); }); test("LUKS with LVM inside: disk -> partition -> LUKS -> PV -> LV -> ext4", () => { const plan = planOperations(inv(luksSpec("dm-crypt", "lvm")), [{ type: "resize-partition", disk: "sdd", number: 1, size: "max", auto: true }]); assert.deepEqual(names(plan).filter((name) => !name.startsWith("verify") && name !== "backup-table"), ["resize-part", "luks-resize", "pv-resize", "lv-extend", "grow-fs"]); assert.match(plan.planText, /pv-resize \/dev\/mapper\/secret\nlv-extend vgs\/data max\ngrow-fs \/dev\/mapper\/vgs-data ext4/); }); test("virtual disk enlarged on the hypervisor: the misplaced GPT backup header is relocated first", () => { 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 }] }); const inventory = inv({ disks: [disk] }); assert.equal(inventory.disks[0].table.gptBackupMisplaced, true); const [suggestion] = suggestions(inventory); assert.equal(suggestion.available, true); assert.ok(Math.abs(suggestion.freeBytes - 40 * GiB) < 2 * MiB, "about 40 GiB found"); const plan = planOperations(inventory, suggestion.operations); assert.deepEqual(names(plan), ["verify-disk", "backup-table", "gpt-relocate", "resize-part", "grow-fs", "verify-part"]); }); test("unprivileged inventory is partial and cannot plan; suggestions are blocked, not faked", () => { const inventory = inv({ uid: 1000, disks: [rootDisk()] }); assert.equal(inventory.privileged, false); assert.equal(inventory.partial, true); assert.equal(inventory.disks[0].table.source, "sysfs"); assert.throws(() => planOperations(inventory, [{ type: "grow-fs", device: "/dev/vda2" }]), code("DISKS_PRIVILEGE_REQUIRED")); assert.equal(suggestions(inventory)[0].blocked.code, "DISKS_PRIVILEGE_REQUIRED"); }); test("missing tools are reported, never installed", () => { const inventory = inv({ disks: [rootDisk()], withoutTools: ["resize2fs"] }); assert.equal(suggestions(inventory)[0].blocked.code, "DISKS_TOOL_MISSING"); 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")); }); test("Proxmox host storage, partition moves and unsafe input are refused", () => { const pve = inv({ ...lvmSpec(), pve: true }); assert.equal(pve.disks[0].protected, "pve-host-storage"); assert.throws(() => planOperations(pve, [{ type: "create-table", disk: "sda", label: "gpt" }]), code("DISKS_PROTECTED_DEVICE")); const plain = inv({ disks: [rootDisk()] }); assert.throws(() => planOperations(plain, [{ type: "move-partition", disk: "vda", number: 2 }]), code("DISKS_OPERATION_DISABLED")); assert.throws(() => planOperations(plain, [{ type: "grow-fs", device: "/dev/vda2; reboot" }]), code("DISKS_DEVICE_NOT_FOUND")); assert.throws(() => planOperations(plain, [{ type: "shell", command: "rm -rf /" }]), code("DISKS_ARGUMENTS_INVALID")); 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 }] }] }); assert.throws(() => planOperations(free, [{ type: "set-label", device: "/dev/vdb1", label: "x$(id)" }]), code("DISKS_LABEL_INVALID")); assert.throws(() => planOperations(free, [{ type: "mount", device: "/dev/vdb1", mountpoint: "/etc" }]), code("DISKS_MOUNTPOINT_INVALID")); assert.throws(() => planOperations(free, [{ type: "mount", device: "/dev/vdb1", mountpoint: "/mnt/../etc" }]), code("DISKS_MOUNTPOINT_INVALID")); const mount = planOperations(free, [{ type: "mount", device: "/dev/vdb1", mountpoint: "/mnt/data" }]); assert.equal(lines(mount).at(-1), "mount /dev/vdb1 /mnt/data"); }); test("MBR: primary partitions only, boot flag and the 2 TiB limit", () => { 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 }] }] }); 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" }]); assert.match(plan.planText, /set-boot \/dev\/sde 1/); assert.match(plan.planText, /create-part \/dev\/sde 2 \d+ \d+ 82 -/); assert.throws(() => planOperations(inventory, [{ type: "set-name", disk: "sde", number: 1, name: "x" }]), code("DISKS_NAME_UNSUPPORTED")); }); test("SMART: unavailable on virtual disks is reported as such", () => { const result = smartSummary({ smartctl: JSON.stringify({ smartctl: { messages: [{ string: "/dev/sda: Unable to detect device type", severity: "error" }], exit_status: 1 } }) }); assert.equal(result.available, false); assert.match(result.reason, /Unable to detect/); 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 } }) }); assert.equal(nvme.passed, true); assert.equal(nvme.percentageUsed, 3); assert.equal(nvme.temperatureC, 41); }); // ---------- Core service contracts with a fake executor ---------- function fakeService({ platform = { os: "linux" }, outputs = [] } = {}) { const calls = []; const record = { sessionId: "s", profileId: "p", host: "h", port: 22, username: "ai", state: "connected" }; const sessionManager = { _requireConnected: () => record, _platform: async () => platform }; const executor = { probe: async () => ({ mode: "root" }), hasCachedPassword: () => false, run: async (_id, options) => { calls.push(options); const next = outputs.shift(); if (typeof next === "function") return next(options); return { stdout: next ?? "" }; } }; return { service: new DiskService({ sessionManager, privilegedExecutor: executor }), calls, record }; } test("Windows and other non-Linux servers: no Linux tool is started", async () => { const { service, calls } = fakeService({ platform: { os: "windows", caption: "Windows Server 2025" } }); const result = await service.inspect("s"); assert.equal(result.supported, false); assert.equal(result.reason, "WINDOWS"); assert.equal(calls.length, 0); await assert.rejects(() => service.plan("s", { operations: [{ type: "grow-fs", device: "/dev/sda1" }] }), code("DISKS_PLATFORM_UNSUPPORTED")); await assert.rejects(() => service.smart("s", { device: "/dev/sda" }), code("DISKS_PLATFORM_UNSUPPORTED")); }); test("apply: the layout is re-read; a changed layout is refused (stale plan) and plans are single-use", async () => { const before = inspectOutput({ disks: [rootDisk()] }); const changed = inspectOutput({ disks: [rootDisk({ hash: "b".repeat(64) })] }); const { service, calls } = fakeService({ outputs: [before, changed] }); const plan = await service.plan("s", { operations: [{ type: "resize-partition", disk: "vda", number: 2, size: "max" }] }); await assert.rejects(() => service.apply("s", { planId: plan.planId }), code("DISKS_CONFIRMATION_REQUIRED")); await assert.rejects(() => service.apply("s", { planId: plan.planId, confirm: true }), code("DISKS_PLAN_STALE")); assert.ok(!calls.some((call) => call.args[0] === "job-start"), "no job was started"); await assert.rejects(() => service.apply("s", { planId: plan.planId, confirm: true }), code("DISKS_PLAN_EXPIRED")); }); test("apply: an unchanged layout starts exactly one remote job with the reviewed steps", async () => { const output = inspectOutput({ disks: [rootDisk()] }); const { service, calls, record } = fakeService({ outputs: [output, output, () => ({ stdout: "\x1eCHJ_SECTION=job\nid=x\nstate=running\nrunner=systemd\n" })] }); const plan = await service.plan("s", { operations: [{ type: "resize-partition", disk: "vda", number: 2, size: "max" }] }); const result = await service.apply("s", { planId: plan.planId, confirm: true }); assert.equal(result.state, "running"); assert.match(result.jobId, /^[a-f0-9-]{36}$/); const start = calls.find((call) => call.args[0] === "job-start"); assert.deepEqual(start.args, ["job-start", result.jobId]); assert.equal(start.privileged, true); assert.equal(start.frames[0].tag, "PLAN"); assert.equal(start.frames[0].data.trim(), plan.planText); assert.equal(start.frames[1].tag, "HELPER"); assert.match(start.frames[1].data, /chj\.disks/); // A second server (identity change) can never use the plan of another server. const other = await service.plan("s", { operations: [{ type: "resize-partition", disk: "vda", number: 2, size: "max" }] }).catch(() => null); if (other) { record.host = "other"; await assert.rejects(() => service.apply("s", { planId: other.planId, confirm: true }), code("DISKS_PLAN_SERVER_CHANGED")); } }); test("concurrent apply on the same session is refused", async () => { const output = inspectOutput({ disks: [rootDisk()] }); let release; const slow = () => new Promise((resolve) => { release = () => resolve({ stdout: output }); }); const { service } = fakeService({ outputs: [output, output, slow] }); const first = await service.plan("s", { operations: [{ type: "resize-partition", disk: "vda", number: 2, size: "max" }] }); const second = await service.plan("s", { operations: [{ type: "resize-partition", disk: "vda", number: 2, size: "max" }] }); const running = service.apply("s", { planId: first.planId, confirm: true }); await new Promise((resolve) => setImmediate(resolve)); await assert.rejects(() => service.apply("s", { planId: second.planId, confirm: true }), code("DISKS_BUSY")); release(); await running.catch(() => {}); }); test("job status: runner gone without a result is 'unknown' and never restarted", async () => { 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"; const { service, calls } = fakeService({ outputs: [status] }); const result = await service.jobStatus("s", { jobId: "abcdef12-0000", offset: 0 }); assert.equal(result.outcome, "unknown"); assert.deepEqual(result.steps, [{ index: 1, total: 5, status: "done", step: "" }]); assert.equal(result.log, "line\n"); assert.equal(calls.length, 1); assert.deepEqual(calls[0].args, ["job-status", "abcdef12-0000", "0"]); await assert.rejects(() => service.jobStatus("s", { jobId: "../../etc" }), code("DISKS_ARGUMENTS_INVALID")); }); test("boot partitions of the running system disk are never deleted or formatted", () => { 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) }] })] }); assert.throws(() => planOperations(inventory, [{ type: "delete-partition", disk: "vda", number: 14 }]), code("DISKS_SYSTEM_PARTITION")); assert.throws(() => planOperations(inventory, [{ type: "format", device: "/dev/vda15", fs: "vfat" }]), code("DISKS_SYSTEM_PARTITION")); });