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:
planner.js Přidáno: Datum není evidováno 44,4 KB · 659 řádků
Kód Stáhnout soubor
1"use strict";
3const { SessionError } = require("../sessions/sessionManager");
4const { MiB, PARTITION_TYPE_CODES, SYSTEM_MOUNTS, buildSegments } = require("./inventory");
6// Turns a queue of user operations (GParted-style) into a validated, simulated plan of whitelisted remote steps.
7// Each operation is checked against the layout produced by the operations before it, so "delete partition 3, then
8// grow partition 2" works. Changing a partition boundary and resizing the content are separate steps, ordered by
9// the content type and its layers (disk -> partition -> LUKS -> LVM -> file system). Nothing here touches a server.
10const MAX_OPERATIONS = 32;
11const AUTO_MIN_BYTES = 16 * MiB;
12const FILESYSTEMS = Object.freeze(["ext4", "xfs", "btrfs", "vfat", "ntfs", "swap"]);
13const MKFS_TOOL = Object.freeze({ ext4: "mkfs.ext4", xfs: "mkfs.xfs", btrfs: "mkfs.btrfs", vfat: "mkfs.vfat", ntfs: "mkfs.ntfs", swap: "mkswap" });
14const MIN_FS_BYTES = Object.freeze({ ext4: 16 * MiB, xfs: 300 * MiB, btrfs: 256 * MiB, vfat: 33 * MiB, ntfs: 16 * MiB, swap: MiB });
15const LABEL_LIMITS = Object.freeze({ ext2: 16, ext3: 16, ext4: 16, xfs: 12, btrfs: 36, vfat: 11, ntfs: 32, swap: 15 });
16const MOUNTABLE = new Set(["ext2", "ext3", "ext4", "xfs", "btrfs", "vfat", "ntfs", "exfat"]);
17const OPERATION_TYPES = new Set(["create-table", "create-partition", "delete-partition", "resize-partition", "format", "set-label", "set-name", "set-flag", "check", "grow-fs", "mount", "unmount", "lv-extend", "lv-reduce", "pv-resize", "move-partition", "copy-partition"]);
18const FLAGS = Object.freeze({ gpt: ["legacy_boot", "esp", "bios-boot", "lvm", "raid", "swap", "basic-data"], dos: ["boot", "esp", "lvm", "raid", "swap"] });
19const LABEL_PATTERN = /^[A-Za-z0-9._-]{1,36}$/;
20const MOUNTPOINT_PATTERN = /^\/(mnt|media)\/[A-Za-z0-9._-]{1,64}(\/[A-Za-z0-9._-]{1,64}){0,3}$/;
22function fail(code, message, details) { return new SessionError(`DISKS_${code}`, message, details); }
23const clone = (value) => JSON.parse(JSON.stringify(value));
24const isExt = (fstype) => /^ext[234]$/.test(String(fstype || ""));
25function partPath(diskName, number) { return `/dev/${diskName}${/\d$/.test(diskName) ? "p" : ""}${number}`; }
27// ---------- simulation state ----------
28function createSim(inventory) {
29 const sim = { inventory, disks: new Map(), nodes: new Map(), byPath: new Map(), lvm: clone(inventory.lvm || { pvs: [], vgs: [], lvs: [] }) };
30 for (const disk of inventory.disks) sim.disks.set(disk.name, clone(disk));
31 for (const node of inventory.nodes) addNode(sim, clone(node));
32 return sim;
34function addNode(sim, node) {
35 sim.nodes.set(node.kname, node);
36 for (const path of [node.path, `/dev/${node.kname}`, node.displayPath].filter(Boolean)) sim.byPath.set(path, node);
38function nodeFor(sim, device) {
39 const node = sim.byPath.get(String(device || ""));
40 if (!node) throw fail("DEVICE_NOT_FOUND", `${device} was not found in the current inventory.`);
41 return node;
43function diskFor(sim, name) {
44 const disk = sim.disks.get(String(name || "").replace(/^\/dev\//, ""));
45 if (!disk) throw fail("DEVICE_NOT_FOUND", `Disk ${name} was not found in the current inventory.`);
46 return disk;
48function partFor(disk, number) {
49 const part = disk.partitions.find((entry) => entry.number === Number(number));
50 if (!part) throw fail("DEVICE_NOT_FOUND", `Partition ${number} does not exist on ${disk.path}.`);
51 return part;
53function partNode(sim, disk, part) {
54 return (part.kernel && sim.nodes.get(part.kernel)) || sim.byPath.get(partPath(disk.name, part.number)) || null;
56function inUse(sim, node) {
57 if (!node) return [];
58 const reasons = [];
59 if (node.mountpoints?.length) reasons.push(`mounted at ${node.mountpoints.join(", ")}`);
60 if (node.swapActive) reasons.push("active swap");
61 if (node.children?.length) reasons.push(`used by ${node.children.map((name) => sim.nodes.get(name)?.displayPath || sim.nodes.get(name)?.path || name).join(", ")}`);
62 if (node.fstype === "zfs_member") reasons.push("ZFS pool member");
63 if (node.fstype === "linux_raid_member") reasons.push("RAID member");
64 return reasons;
66function requireFree(sim, node, code = "DEVICE_IN_USE") {
67 const reasons = inUse(sim, node);
68 if (reasons.length) throw fail(code, `${node.displayPath || node.path} is in use (${reasons.join("; ")}). CH-J never unmounts, deactivates or closes it automatically.`);
70function refreshSegments(disk) {
71 if (!disk.table) { disk.segments = []; return; }
72 disk.partitions.sort((a, b) => a.start - b.start);
73 disk.segments = buildSegments(disk.partitions, disk.table.firstUsable, disk.table.lastUsable, disk.alignSectors, disk.logicalSectorSize, disk.table.lastUsableNow);
75const alignUp = (value, align) => Math.ceil(value / align) * align;
76const alignDown = (value, align) => Math.floor(value / align) * align;
77// The free space directly after a partition (up to the next partition or the end of the usable area).
78function boundaryAfter(disk, part) {
79 const next = disk.partitions.filter((entry) => !entry.logical && entry.start > part.start).sort((a, b) => a.start - b.start)[0];
80 return next ? next.start - 1 : disk.table.lastUsable;
83// ---------- plan context ----------
84function createContext(sim) {
85 return { sim, steps: [], warnings: [], notes: [], verifyDisks: new Set(), backupDisks: new Set(), verifySizes: new Map(), relocated: new Set(), dataLoss: false, offline: new Set(), mayReboot: false, operations: [] };
87function step(ctx, name, args, info = {}) {
88 const values = args.map((value) => String(value));
89 for (const value of values) if (!/^[A-Za-z0-9._/:+,=@-]{1,160}$/.test(value)) throw fail("ARGUMENTS_INVALID", `Invalid step argument for ${name}.`);
90 ctx.steps.push({ name, args: values, operation: ctx.currentOperation, kind: info.kind || "change", destructive: info.destructive === true });
92function touchDisk(ctx, disk, { backup = false } = {}) {
93 if (disk.protected && backup) throw fail("PROTECTED_DEVICE", `${disk.path} holds Proxmox host storage; CH-J does not change its partition table.`);
94 ctx.verifyDisks.add(disk.name);
95 if (backup) ctx.backupDisks.add(disk.name);
97function verifyDevice(ctx, sim, node) {
98 if (node.kind === "part") { const disk = sim.disks.get(node.parent); if (disk) touchDisk(ctx, disk); return; }
99 const original = sim.inventory.nodes.find((entry) => entry.kname === node.kname);
100 if (original && !ctx.verifySizes.has(original.path)) ctx.verifySizes.set(original.path, original.size);
102function needTool(sim, tool, purpose) {
103 if (!sim.inventory.tools[tool]) throw fail("TOOL_MISSING", `${tool} is required for ${purpose} but is not installed on the server. CH-J does not install packages.`, { tool });
105function diskOfNode(sim, node) {
106 let current = node;
107 for (let depth = 0; current && depth < 16; depth += 1) {
108 if (current.kind === "part") return sim.disks.get(current.parent) || null;
109 if (current.kind === "disk" || current.kind === "loop") return sim.disks.get(current.kname) || null;
110 current = sim.nodes.get(current.parents?.[0]);
111 }
112 return null;
114// Boot partitions of the disk that runs the system are not "in use" by a mount, but deleting or formatting them
115// makes the server unbootable. They are treated like mounted system partitions.
116function assertNotBootPartition(disk, part) {
117 if (disk?.system && ["bios-boot", "esp"].includes(part?.typeName)) throw fail("SYSTEM_PARTITION", `${part.path || `Partition ${part.number}`} is the ${part.typeName === "esp" ? "EFI system" : "BIOS boot"} partition of the disk that runs the system; CH-J does not delete or format it.`);
119function assertNotProtected(sim, node, action) {
120 const disk = diskOfNode(sim, node);
121 if (disk?.protected || node?.lvm?.pveGuest) throw fail("PROTECTED_DEVICE", `${node.displayPath || node.path} belongs to Proxmox host or guest storage; CH-J does not ${action} it.`);
124// ---------- content chains (grow outward from the partition, shrink inward) ----------
125function growContent(sim, ctx, node, newBytes, options = {}) {
126 if (!node) return;
127 const fstype = node.fstype;
128 const path = node.path;
129 if (!fstype) { node.size = newBytes; return; }
130 if (isExt(fstype)) {
131 needTool(sim, "resize2fs", "growing ext file systems");
132 step(ctx, "grow-fs", [path, fstype], { kind: "filesystem" });
133 } else if (fstype === "xfs") {
134 needTool(sim, "xfs_growfs", "growing XFS");
135 if (!node.mountpoints?.length) throw fail("XFS_GROW_NEEDS_MOUNT", `XFS can only be grown while mounted; mount ${path} first (add a mount operation before this one).`);
136 step(ctx, "grow-fs", [path, fstype], { kind: "filesystem" });
137 } else if (fstype === "btrfs") {
138 needTool(sim, "btrfs", "growing Btrfs");
139 if (!node.mountpoints?.length) throw fail("BTRFS_NEEDS_MOUNT", `Btrfs is resized while mounted; mount ${path} first.`);
140 step(ctx, "grow-fs", [path, fstype], { kind: "filesystem" });
141 } else if (fstype === "ntfs") {
142 needTool(sim, "ntfsresize", "growing NTFS");
143 requireFree(sim, node, "NTFS_MOUNTED");
144 step(ctx, "grow-fs", [path, fstype], { kind: "filesystem" });
145 } else if (fstype === "swap") {
146 needTool(sim, "mkswap", "resizing swap");
147 if (node.swapActive) throw fail("SWAP_ACTIVE", `${path} is active swap; CH-J never disables swap automatically. Deactivate it yourself first.`);
148 step(ctx, "grow-fs", [path, fstype], { kind: "filesystem" });
149 } else if (fstype === "crypto_LUKS") {
150 const mapping = sim.nodes.get(node.children?.[0]);
151 if (!mapping) { ctx.notes.push({ code: "LUKS_CLOSED", device: path }); node.size = newBytes; return; }
152 needTool(sim, "cryptsetup", "resizing LUKS mappings");
153 if (/keyring/i.test(mapping.crypt?.keyLocation || "")) throw fail("LUKS_PASSPHRASE_REQUIRED", `The LUKS2 volume key of ${mapping.path} is held in the kernel keyring, so cryptsetup needs the passphrase to resize it. CH-J never handles LUKS secrets; resize the mapping manually.`);
154 if (!mapping.crypt?.keyLocation) throw fail("LUKS_UNVERIFIED", `The key location of ${mapping.path} could not be verified; only display and diagnostics are available.`);
155 step(ctx, "luks-resize", [mapping.dmName], { kind: "luks" });
156 const headerBytes = Math.max(0, (node.size || 0) - (mapping.size || 0));
157 mapping.size = newBytes - headerBytes;
158 growContent(sim, ctx, mapping, mapping.size, options);
159 } else if (fstype === "LVM2_member") {
160 needTool(sim, "pvresize", "resizing LVM physical volumes");
161 step(ctx, "pv-resize", [path], { kind: "lvm" });
162 const pv = sim.lvm.pvs.find((entry) => entry.path === path || entry.path === `/dev/${node.kname}`);
163 const vg = pv && sim.lvm.vgs.find((entry) => entry.name === pv.vg);
164 const growth = newBytes - (pv?.size ?? node.size ?? newBytes);
165 if (pv) { pv.size = newBytes; pv.free = (pv.free || 0) + Math.max(0, growth); }
166 if (vg) vg.free = (vg.free || 0) + Math.max(0, growth);
167 if (options.autoLv && vg) {
168 const lv = autoLv(sim, vg);
169 if (lv) extendLv(sim, ctx, lv, "max");
170 else ctx.notes.push({ code: "VG_FREE_LEFT", vg: vg.name });
171 }
172 } else if (fstype === "vfat") {
173 throw fail("FAT_RESIZE_UNSUPPORTED", "Resizing FAT is not implemented (fatresize is not reliable enough for unattended use).");
174 } else {
175 throw fail("RESIZE_UNSUPPORTED", `Resizing content of type ${fstype} is not supported.`);
176 }
177 node.fsSize = newBytes;
178 node.size = newBytes;
181function shrinkContent(sim, ctx, node, newBytes) {
182 if (!node || !node.fstype) return;
183 const fstype = node.fstype; const path = node.path;
184 if (isExt(fstype)) {
185 needTool(sim, "resize2fs", "shrinking ext file systems"); needTool(sim, "e2fsck", "checking ext file systems");
186 if (inUse(sim, node).length) {
187 const root = node.mountpoints?.some((point) => SYSTEM_MOUNTS.test(point));
188 throw fail("SHRINK_REQUIRES_UNMOUNT", root
189 ? `${path} is mounted at ${node.mountpoints.join(", ")}, which the running system needs. Shrinking ext4 is only possible offline: boot a rescue/live system and shrink it there.`
190 : `ext file systems are shrunk only while unmounted; unmount ${path} first (CH-J never unmounts automatically).`);
191 }
192 const used = node.usage?.used ?? null;
193 if (used === null) throw fail("USAGE_UNKNOWN", `The usage of ${path} could not be determined; shrinking is refused.`);
194 const reserve = Math.max(used / 10, 128 * MiB);
195 if (newBytes < used + reserve) throw fail("SIZE_TOO_SMALL", `The new size must keep the used ${Math.ceil(used / MiB)} MiB plus a reserve of ${Math.ceil(reserve / MiB)} MiB.`, { minimumBytes: Math.ceil(used + reserve) });
196 step(ctx, "shrink-fs", [path, fstype, newBytes], { kind: "filesystem", destructive: true });
197 ctx.offline.add(path);
198 } else if (fstype === "btrfs") {
199 needTool(sim, "btrfs", "shrinking Btrfs");
200 if (!node.mountpoints?.length) throw fail("BTRFS_NEEDS_MOUNT", `Btrfs is resized while mounted; mount ${path} first.`);
201 if ((node.btrfs?.devices || 1) > 1) throw fail("BTRFS_MULTI_DEVICE", "Shrinking a multi-device Btrfs is not supported.");
202 const used = node.btrfs?.allocated ?? node.usage?.used ?? null;
203 if (used === null) throw fail("USAGE_UNKNOWN", `The allocation of ${path} could not be determined; shrinking is refused.`);
204 const reserve = Math.max(used / 10, 256 * MiB);
205 if (newBytes < used + reserve) throw fail("SIZE_TOO_SMALL", `The new size must keep the allocated ${Math.ceil(used / MiB)} MiB plus a reserve of ${Math.ceil(reserve / MiB)} MiB.`, { minimumBytes: Math.ceil(used + reserve) });
206 step(ctx, "shrink-fs", [path, fstype, newBytes], { kind: "filesystem", destructive: true });
207 } else if (fstype === "ntfs") {
208 needTool(sim, "ntfsresize", "shrinking NTFS");
209 requireFree(sim, node, "NTFS_MOUNTED");
210 step(ctx, "shrink-fs", [path, fstype, newBytes], { kind: "filesystem", destructive: true });
211 } else if (fstype === "swap") {
212 if (node.swapActive) throw fail("SWAP_ACTIVE", `${path} is active swap; CH-J never disables swap automatically.`);
213 needTool(sim, "mkswap", "resizing swap");
214 } else if (fstype === "xfs") {
215 throw fail("XFS_SHRINK_UNSUPPORTED", "XFS cannot be shrunk: the standard XFS tools only grow file systems. To get a smaller volume, create a new, smaller file system and migrate the data (for example with xfsdump/xfsrestore or rsync).");
216 } else if (fstype === "vfat") {
217 throw fail("FAT_RESIZE_UNSUPPORTED", "Resizing FAT is not implemented.");
218 } else if (fstype === "crypto_LUKS") {
219 throw fail("LUKS_SHRINK_UNSUPPORTED", "Shrinking LUKS (and the layers inside it) is never done automatically by CH-J.");
220 } else if (fstype === "LVM2_member") {
221 throw fail("PV_SHRINK_UNSUPPORTED", "Shrinking an LVM physical volume is not automated; move extents and use pvresize manually.");
222 } else {
223 throw fail("RESIZE_UNSUPPORTED", `Shrinking content of type ${fstype} is not supported.`);
224 }
227function autoLv(sim, vg) {
228 const candidates = sim.lvm.lvs.filter((lv) => lv.vg === vg.name && !lv.thinPool && !lv.thinVolume && !lv.snapshot && !lv.pveGuest);
229 const withFs = candidates.filter((lv) => { const node = sim.byPath.get(lv.dmPath) || sim.byPath.get(lv.path); return node && ["ext2", "ext3", "ext4", "xfs", "btrfs"].includes(node.fstype); });
230 if (withFs.length === 1) return withFs[0];
231 return withFs.find((lv) => (sim.byPath.get(lv.dmPath) || sim.byPath.get(lv.path))?.mountpoints?.includes("/")) || null;
233function lvFor(sim, name) {
234 const [vgName, lvName] = String(name || "").split("/");
235 const lv = sim.lvm.lvs.find((entry) => entry.vg === vgName && entry.name === lvName);
236 if (!lv || !/^[A-Za-z0-9._+-]{1,64}\/[A-Za-z0-9._+-]{1,64}$/.test(String(name))) throw fail("DEVICE_NOT_FOUND", `Logical volume ${name} was not found.`);
237 return lv;
239function extendLv(sim, ctx, lv, size) {
240 needTool(sim, "lvextend", "extending logical volumes");
241 if (lv.thinPool || lv.thinVolume) throw fail("LV_THIN_UNSUPPORTED", "Thin pools and thin volumes are shown read-only: their virtual size, pool data and allocation need manual planning.");
242 if (lv.snapshot) throw fail("LV_SNAPSHOT_UNSUPPORTED", "Snapshots are not resized by CH-J.");
243 if (lv.pveGuest) throw fail("PROTECTED_DEVICE", `This logical volume is a disk of Proxmox guest ${lv.pveGuest}; CH-J does not change it.`);
244 const vg = sim.lvm.vgs.find((entry) => entry.name === lv.vg);
245 const node = sim.byPath.get(lv.dmPath) || sim.byPath.get(lv.path);
246 const extent = vg?.extentSize || 4 * MiB;
247 let target;
248 if (size === "max") target = lv.size + Math.floor((vg?.free || 0) / extent) * extent;
249 else target = Math.floor(Number(size) / extent) * extent;
250 if (!Number.isFinite(target) || target <= lv.size) throw fail(size === "max" ? "NO_FREE_SPACE" : "SIZE_INVALID", size === "max" ? `Volume group ${lv.vg} has no free extents.` : "The new size must be larger than the current size.");
251 if (target - lv.size > (vg?.free || 0)) throw fail("NO_FREE_SPACE", `Volume group ${lv.vg} does not have enough free space.`);
252 if (node) verifyDevice(ctx, sim, node);
253 step(ctx, "lv-extend", [`${lv.vg}/${lv.name}`, size === "max" ? "max" : target], { kind: "lvm" });
254 if (vg) vg.free -= target - lv.size;
255 lv.size = target;
256 if (node) growContent(sim, ctx, node, target);
259// ---------- operation handlers ----------
260const HANDLERS = {
261 "create-table"(sim, ctx, op) {
262 const disk = diskFor(sim, op.disk);
263 if (!["gpt", "dos"].includes(op.label)) throw fail("ARGUMENTS_INVALID", "The partition table must be gpt or dos (MBR).");
264 if (disk.protected) throw fail("PROTECTED_DEVICE", `${disk.path} holds Proxmox host storage.`);
265 const node = sim.nodes.get(disk.name);
266 requireFree(sim, node);
267 for (const part of disk.partitions) requireFree(sim, partNode(sim, disk, part));
268 needTool(sim, "sfdisk", "writing partition tables");
269 touchDisk(ctx, disk, { backup: true });
270 step(ctx, "create-table", [disk.path, op.label], { kind: "partition", destructive: true });
271 ctx.dataLoss = true;
272 const lbs = disk.logicalSectorSize; const tail = Math.ceil(16384 / lbs) + 1;
273 disk.partitions = [];
274 disk.table = { type: op.label, id: null, firstUsable: disk.alignSectors, lastUsable: op.label === "gpt" ? disk.sectors - tail - 1 : Math.min(disk.sectors - 1, 0xffffffff), hash: null, source: "plan", gptBackupMisplaced: false, maxPrimary: op.label === "dos" ? 4 : 128 };
275 disk.table.lastUsableNow = disk.table.lastUsable;
276 disk.wholeDevice = null; node.fstype = null;
277 refreshSegments(disk);
278 },
279 "create-partition"(sim, ctx, op) {
280 const disk = diskFor(sim, op.disk);
281 if (!disk.table || !["gpt", "dos"].includes(disk.table.type)) throw fail("NO_PARTITION_TABLE", `${disk.path} has no supported partition table; create one first.`);
282 if (disk.protected) throw fail("PROTECTED_DEVICE", `${disk.path} holds Proxmox host storage.`);
283 needTool(sim, "sfdisk", "creating partitions");
284 const fs = op.fs || "none";
285 if (fs !== "none" && !FILESYSTEMS.includes(fs)) throw fail("ARGUMENTS_INVALID", "Unsupported file system.");
286 if (fs !== "none") needTool(sim, MKFS_TOOL[fs], `creating ${fs}`);
287 const align = disk.alignSectors;
288 const start = alignUp(Math.max(Number(op.start), disk.table.firstUsable), align);
289 const segment = disk.segments.find((entry) => entry.kind === "free" && start >= entry.start && start <= entry.end);
290 if (!segment) throw fail("NO_FREE_SPACE", "The requested start is not inside unallocated space.");
291 let size = op.size === "max" ? segment.end - start + 1 : Number(op.size);
292 if (!Number.isSafeInteger(size) || size < align) throw fail("SIZE_INVALID", "The partition must be at least 1 MiB.");
293 if (start + size - 1 > segment.end) throw fail("NO_FREE_SPACE", "The partition does not fit into the unallocated space.");
294 if (start + size - 1 !== segment.end) size = alignDown(size, align);
295 if (fs !== "none" && size * disk.logicalSectorSize < MIN_FS_BYTES[fs]) throw fail("SIZE_TOO_SMALL", `${fs} needs at least ${Math.ceil(MIN_FS_BYTES[fs] / MiB)} MiB.`);
296 if (disk.table.type === "dos" && start + size - 1 > 0xffffffff) throw fail("MBR_LIMIT", "MBR cannot address space beyond 2 TiB; use GPT.");
297 const used = new Set(disk.partitions.map((part) => part.number));
298 const max = disk.table.type === "dos" ? 4 : 128;
299 let number = 1; while (used.has(number) && number <= max) number += 1;
300 if (number > max) throw fail("PARTITION_LIMIT", disk.table.type === "dos" ? "All four MBR primary partitions are used; CH-J does not create extended/logical partitions." : "The GPT has no free entry.");
301 const flag = op.typeFlag === "esp" && fs === "vfat" ? "esp" : fs === "swap" ? "swap" : fs === "vfat" ? "fat32" : fs === "ntfs" ? "basic-data" : "linux";
302 const type = PARTITION_TYPE_CODES[flag][disk.table.type];
303 const name = op.name ? String(op.name) : "-";
304 if (name !== "-" && (disk.table.type !== "gpt" || !LABEL_PATTERN.test(name))) throw fail("ARGUMENTS_INVALID", "Partition names (GPT only) may contain A-Z, a-z, 0-9, dot, dash and underscore (max. 36).");
305 const label = op.label ? String(op.label) : "-";
306 if (label !== "-" && fs !== "none") validateLabel(fs, label);
307 if (disk.table.gptBackupMisplaced && start + size - 1 > disk.table.lastUsableNow) relocate(ctx, disk);
308 touchDisk(ctx, disk, { backup: true });
309 step(ctx, "create-part", [disk.path, number, start, size, type, name], { kind: "partition" });
310 const path = partPath(disk.name, number);
311 const kname = path.slice(5);
312 if (fs !== "none") step(ctx, "mkfs", [path, fs, label === "-" ? "-" : fs === "vfat" ? label.toUpperCase() : label], { kind: "filesystem", destructive: true });
313 const part = { number, start, size, end: start + size - 1, bytes: size * disk.logicalSectorSize, type, typeName: flag, name: name === "-" ? null : name, kernel: kname, path, fstype: fs === "none" ? null : fs, label: label === "-" ? null : label, mountpoints: [], usage: null, inUse: false, inUseReasons: [], systemRoles: [], children: [], created: true };
314 disk.partitions.push(part);
315 addNode(sim, { kname, path, kind: "part", parent: disk.name, partition: number, size: part.bytes, fstype: part.fstype, label: part.label, mountpoints: [], children: [], parents: [disk.name], swapActive: false, synthetic: true });
316 refreshSegments(disk);
317 ctx.steps.at(-1).created = path;
318 },
319 "delete-partition"(sim, ctx, op) {
320 const disk = diskFor(sim, op.disk); const part = partFor(disk, op.number);
321 if (part.extended || part.logical) throw fail("EXTENDED_UNSUPPORTED", "Extended and logical MBR partitions are shown read-only.");
322 assertNotBootPartition(disk, part);
323 const node = partNode(sim, disk, part);
324 requireFree(sim, node);
325 needTool(sim, "sfdisk", "deleting partitions");
326 touchDisk(ctx, disk, { backup: true });
327 if (node) step(ctx, "require-free", [node.path], { kind: "check" });
328 step(ctx, "delete-part", [disk.path, part.number, part.start, part.size], { kind: "partition", destructive: true });
329 ctx.dataLoss = true;
330 disk.partitions = disk.partitions.filter((entry) => entry !== part);
331 if (node) { sim.nodes.delete(node.kname); sim.byPath.delete(node.path); }
332 refreshSegments(disk);
333 },
334 "resize-partition"(sim, ctx, op) {
335 const disk = diskFor(sim, op.disk); const part = partFor(disk, op.number);
336 if (!disk.table || !["gpt", "dos"].includes(disk.table.type)) throw fail("NO_PARTITION_TABLE", "Unsupported partition table.");
337 if (part.extended || part.logical) throw fail("EXTENDED_UNSUPPORTED", "Extended and logical MBR partitions are shown read-only.");
338 needTool(sim, "sfdisk", "resizing partitions");
339 if (!sim.inventory.tools.partx && !sim.inventory.tools.blockdev && !sim.inventory.tools.partprobe) throw fail("TOOL_MISSING", "partx, blockdev or partprobe is required to inform the kernel.");
340 const node = partNode(sim, disk, part);
341 const boundary = boundaryAfter(disk, part);
342 const lbs = disk.logicalSectorSize;
343 let size;
344 if (op.size === "max") size = boundary - part.start + 1;
345 else {
346 size = Number(op.size);
347 if (!Number.isSafeInteger(size) || size < disk.alignSectors) throw fail("SIZE_INVALID", "Invalid partition size.");
348 if (part.start + size - 1 !== boundary) size = Math.max(disk.alignSectors, alignDown(part.start + size, disk.alignSectors) - part.start);
349 }
350 if (part.start + size - 1 > boundary) throw fail("NO_FREE_SPACE", "There is not enough unallocated space directly after the partition.");
351 if (disk.table.type === "dos" && part.start + size - 1 > 0xffffffff) throw fail("MBR_LIMIT", "MBR cannot address space beyond 2 TiB.");
352 if (size === part.size) throw fail(op.size === "max" ? "NO_FREE_SPACE" : "NO_CHANGE", op.size === "max" ? "There is no unallocated space directly after the partition." : "The size is unchanged.");
353 const newBytes = size * lbs;
354 if (size > part.size) {
355 if (disk.table.gptBackupMisplaced && part.start + size - 1 > disk.table.lastUsableNow) relocate(ctx, disk);
356 touchDisk(ctx, disk, { backup: true });
357 if (node && inUse(sim, node).length) ctx.mayReboot = true;
358 step(ctx, "resize-part", [disk.path, part.number, part.start, part.size, size], { kind: "partition" });
359 part.size = size; part.end = part.start + size - 1; part.bytes = newBytes;
360 if (node) node.size = newBytes;
361 growContent(sim, ctx, node, newBytes, { autoLv: op.auto === true });
362 } else {
363 if (disk.protected) throw fail("PROTECTED_DEVICE", `${disk.path} holds Proxmox host storage; shrinking is not offered.`);
364 if (node) shrinkContent(sim, ctx, node, newBytes);
365 touchDisk(ctx, disk, { backup: true });
366 step(ctx, "resize-part", [disk.path, part.number, part.start, part.size, size], { kind: "partition", destructive: true });
367 ctx.dataLoss = true;
368 part.size = size; part.end = part.start + size - 1; part.bytes = newBytes;
369 if (node) {
370 node.size = newBytes;
371 if (node.fstype === "swap") step(ctx, "grow-fs", [node.path, "swap"], { kind: "filesystem" });
372 if (node.fstype) node.fsSize = newBytes;
373 if (isExt(node.fstype)) step(ctx, "verify-fs", [node.path, node.fstype], { kind: "verify" });
374 }
375 }
376 refreshSegments(disk);
377 },
378 format(sim, ctx, op) {
379 const node = nodeFor(sim, op.device);
380 const fs = String(op.fs || "");
381 if (!FILESYSTEMS.includes(fs)) throw fail("ARGUMENTS_INVALID", "Unsupported file system.");
382 needTool(sim, MKFS_TOOL[fs], `creating ${fs}`);
383 if (["disk", "loop"].includes(node.kind) && sim.disks.get(node.kname)?.table) throw fail("HAS_PARTITION_TABLE", "This disk has a partition table; format a partition instead.");
384 if (!["part", "lvm", "disk", "loop"].includes(node.kind)) throw fail("FORMAT_UNSUPPORTED", "Only partitions, logical volumes and unpartitioned disks can be formatted.");
385 if (node.lvm?.thinPool) throw fail("LV_THIN_UNSUPPORTED", "Thin pools cannot be formatted.");
386 assertNotProtected(sim, node, "format");
387 if (node.kind === "part") { const disk = sim.disks.get(node.parent); assertNotBootPartition(disk, disk?.partitions.find((entry) => entry.number === node.partition)); }
388 requireFree(sim, node);
389 if ((node.size || 0) < MIN_FS_BYTES[fs]) throw fail("SIZE_TOO_SMALL", `${fs} needs at least ${Math.ceil(MIN_FS_BYTES[fs] / MiB)} MiB.`);
390 const label = op.label ? String(op.label) : "-";
391 if (label !== "-") validateLabel(fs, label);
392 verifyDevice(ctx, sim, node);
393 step(ctx, "mkfs", [node.path, fs, label === "-" ? "-" : fs === "vfat" ? label.toUpperCase() : label], { kind: "filesystem", destructive: true });
394 ctx.dataLoss = true;
395 Object.assign(node, { fstype: fs, label: label === "-" ? null : label, usage: null, fsSize: node.size, uuid: null });
396 syncPart(sim, node);
397 },
398 "set-label"(sim, ctx, op) {
399 const node = nodeFor(sim, op.device);
400 const fstype = node.fstype;
401 const tool = { ext2: "e2label", ext3: "e2label", ext4: "e2label", xfs: "xfs_admin", btrfs: "btrfs", vfat: "fatlabel", ntfs: "ntfslabel", swap: "swaplabel" }[fstype];
402 if (!tool) throw fail("LABEL_UNSUPPORTED", `Labels of ${fstype || "this device"} cannot be changed.`);
403 needTool(sim, tool, "changing labels");
404 const label = op.label ? String(op.label) : "-";
405 if (label !== "-") validateLabel(fstype, label);
406 if (label === "-" && fstype === "vfat") throw fail("ARGUMENTS_INVALID", "Enter a FAT label.");
407 if (["xfs", "vfat", "ntfs"].includes(fstype)) requireFree(sim, node, "LABEL_REQUIRES_UNMOUNT");
408 verifyDevice(ctx, sim, node);
409 step(ctx, "set-label", [node.path, fstype, label === "-" ? "-" : fstype === "vfat" ? label.toUpperCase() : label], { kind: "filesystem" });
410 node.label = label === "-" ? null : label; syncPart(sim, node);
411 },
412 "set-name"(sim, ctx, op) {
413 const disk = diskFor(sim, op.disk); const part = partFor(disk, op.number);
414 if (disk.table?.type !== "gpt") throw fail("NAME_UNSUPPORTED", "Partition names exist only in GPT.");
415 const name = op.name ? String(op.name) : "-";
416 if (name !== "-" && !LABEL_PATTERN.test(name)) throw fail("ARGUMENTS_INVALID", "Partition names may contain A-Z, a-z, 0-9, dot, dash and underscore (max. 36).");
417 needTool(sim, "sfdisk", "naming partitions");
418 touchDisk(ctx, disk, { backup: true });
419 step(ctx, "set-name", [disk.path, part.number, name], { kind: "partition" });
420 part.name = name === "-" ? null : name;
421 },
422 "set-flag"(sim, ctx, op) {
423 const disk = diskFor(sim, op.disk); const part = partFor(disk, op.number);
424 const type = disk.table?.type;
425 if (!type || !FLAGS[type]?.includes(op.flag)) throw fail("FLAG_UNSUPPORTED", `The flag ${op.flag} is not supported for this partition table.`);
426 if (part.extended || part.logical) throw fail("EXTENDED_UNSUPPORTED", "Extended and logical MBR partitions are shown read-only.");
427 const node = partNode(sim, disk, part);
428 if ((node?.systemRoles || part.systemRoles || []).length && op.flag !== "boot" && op.flag !== "legacy_boot") throw fail("SYSTEM_PARTITION", "The partition is used by the running system; its type is not changed by CH-J.");
429 needTool(sim, "sfdisk", "changing partition flags");
430 touchDisk(ctx, disk, { backup: true });
431 const enabled = op.enabled === true;
432 if (op.flag === "boot") {
433 const list = new Set(disk.partitions.filter((entry) => entry.bootable).map((entry) => entry.number));
434 if (enabled) list.add(part.number); else list.delete(part.number);
435 step(ctx, "set-boot", [disk.path, list.size ? [...list].sort((a, b) => a - b).join(",") : "-"], { kind: "partition" });
436 part.bootable = enabled;
437 } else if (op.flag === "legacy_boot") {
438 const attrs = new Set(String(part.attrs || "").split(/[\s,]+/).filter(Boolean));
439 if (enabled) attrs.add("LegacyBIOSBootable"); else attrs.delete("LegacyBIOSBootable");
440 const value = [...attrs].join(",") || "-";
441 if (!/^[A-Za-z0-9,:-]{1,120}$/.test(value)) throw fail("FLAG_UNSUPPORTED", "The existing GPT attributes cannot be represented safely.");
442 step(ctx, "set-attrs", [disk.path, part.number, value], { kind: "partition" });
443 part.attrs = value === "-" ? null : value;
444 } else {
445 const target = enabled ? op.flag : "linux";
446 if (!enabled && part.typeName !== op.flag) throw fail("NO_CHANGE", "The partition does not have this type.");
447 const code = PARTITION_TYPE_CODES[target]?.[type];
448 if (!code) throw fail("FLAG_UNSUPPORTED", "This type is not available for the partition table.");
449 step(ctx, "set-type", [disk.path, part.number, code], { kind: "partition" });
450 part.type = code; part.typeName = target;
451 }
452 },
453 check(sim, ctx, op) {
454 const node = nodeFor(sim, op.device);
455 const fstype = node.fstype; const repair = op.repair === true;
456 const supported = { ext2: "e2fsck", ext3: "e2fsck", ext4: "e2fsck", xfs: "xfs_repair", btrfs: repair ? null : "btrfs", vfat: "fsck.vfat" }[fstype];
457 if (!supported) throw fail("CHECK_UNSUPPORTED", repair && fstype === "btrfs" ? "btrfs check --repair is not offered: it can make damage worse. Use btrfs scrub or expert recovery." : `Checking ${fstype || "this device"} is not supported.`);
458 needTool(sim, supported, "file system checks");
459 requireFree(sim, node, "CHECK_REQUIRES_UNMOUNT");
460 verifyDevice(ctx, sim, node);
461 step(ctx, "fsck", [node.path, fstype, repair ? "repair" : "check"], { kind: "check", destructive: repair });
462 },
463 "grow-fs"(sim, ctx, op) {
464 const node = nodeFor(sim, op.device);
465 if (!["ext2", "ext3", "ext4", "xfs", "btrfs", "ntfs", "swap"].includes(node.fstype)) throw fail("RESIZE_UNSUPPORTED", "This content cannot be grown.");
466 if (node.fsSize && node.size && node.size - node.fsSize < MiB) throw fail("NO_CHANGE", "The file system already fills the device.");
467 verifyDevice(ctx, sim, node);
468 growContent(sim, ctx, node, node.size);
469 },
470 mount(sim, ctx, op) {
471 const node = nodeFor(sim, op.device);
472 if (!MOUNTABLE.has(node.fstype)) throw fail("MOUNT_UNSUPPORTED", "This device does not contain a mountable file system.");
473 if (node.mountpoints?.length) throw fail("ALREADY_MOUNTED", `${node.path} is already mounted.`);
474 const mountpoint = String(op.mountpoint || "");
475 if (!MOUNTPOINT_PATTERN.test(mountpoint) || mountpoint.includes("..")) throw fail("MOUNTPOINT_INVALID", "Mount points must be below /mnt or /media (letters, digits, dot, dash, underscore).");
476 for (const other of sim.nodes.values()) if (other.mountpoints?.includes(mountpoint)) throw fail("MOUNTPOINT_INVALID", `${mountpoint} is already in use.`);
477 step(ctx, "mount", [node.path, mountpoint], { kind: "mount" });
478 node.mountpoints = [mountpoint]; syncPart(sim, node);
479 },
480 unmount(sim, ctx, op) {
481 const node = nodeFor(sim, op.device);
482 if (!node.mountpoints?.length) throw fail("NOT_MOUNTED", `${node.path} is not mounted.`);
483 const system = node.mountpoints.find((point) => SYSTEM_MOUNTS.test(point));
484 if (system) throw fail("SYSTEM_MOUNT", `${system} is needed by the running system; CH-J never unmounts it.`);
485 step(ctx, "unmount", [node.path], { kind: "mount" });
486 node.mountpoints = []; node.usage = node.usage && { ...node.usage, source: "superblock" }; syncPart(sim, node);
487 },
488 "lv-extend"(sim, ctx, op) {
489 const lv = lvFor(sim, op.lv);
490 extendLv(sim, ctx, lv, op.size === "max" ? "max" : Number(op.size));
491 },
492 "lv-reduce"(sim, ctx, op) {
493 if (op.advanced !== true) throw fail("ADVANCED_CONFIRMATION_REQUIRED", "Shrinking a logical volume is an advanced operation and must be confirmed explicitly.");
494 const lv = lvFor(sim, op.lv);
495 needTool(sim, "lvreduce", "reducing logical volumes");
496 if (lv.thinPool || lv.thinVolume || lv.snapshot || lv.pveGuest) throw fail("LV_THIN_UNSUPPORTED", "Thin, snapshot and Proxmox guest volumes are not reduced by CH-J.");
497 const node = sim.byPath.get(lv.dmPath) || sim.byPath.get(lv.path);
498 if (!node) throw fail("DEVICE_NOT_FOUND", "The logical volume is not active.");
499 if (!isExt(node.fstype)) throw fail(node.fstype === "xfs" ? "XFS_SHRINK_UNSUPPORTED" : "LV_SHRINK_UNSUPPORTED", node.fstype === "xfs" ? "XFS cannot be shrunk; migrate the data to a new, smaller volume instead." : "Only logical volumes with an unmounted ext file system can be reduced.");
500 const vg = sim.lvm.vgs.find((entry) => entry.name === lv.vg);
501 const extent = vg?.extentSize || 4 * MiB;
502 const target = Math.floor(Number(op.size) / extent) * extent;
503 if (!Number.isSafeInteger(target) || target <= 0 || target >= lv.size) throw fail("SIZE_INVALID", "The new size must be smaller than the current size.");
504 verifyDevice(ctx, sim, node);
505 shrinkContent(sim, ctx, node, target);
506 step(ctx, "lv-reduce", [`${lv.vg}/${lv.name}`, target], { kind: "lvm", destructive: true });
507 step(ctx, "verify-fs", [node.path, node.fstype], { kind: "verify" });
508 ctx.dataLoss = true;
509 if (vg) vg.free += lv.size - target;
510 lv.size = target; node.size = target; node.fsSize = target;
511 },
512 "pv-resize"(sim, ctx, op) {
513 const node = nodeFor(sim, op.device);
514 if (node.fstype !== "LVM2_member") throw fail("NOT_A_PV", `${node.path} is not an LVM physical volume.`);
515 const pv = sim.lvm.pvs.find((entry) => entry.path === node.path || entry.path === `/dev/${node.kname}`);
516 if (pv && pv.deviceSize - pv.size < MiB) throw fail("NO_CHANGE", "The physical volume already uses the whole device.");
517 verifyDevice(ctx, sim, node);
518 growContent(sim, ctx, node, node.size, { autoLv: op.auto === true });
519 },
520 "move-partition"() { throw fail("OPERATION_DISABLED", "Moving partitions is prepared in the architecture but disabled until a reliable, tested mechanism exists."); },
521 "copy-partition"() { throw fail("OPERATION_DISABLED", "Copying partitions is prepared in the architecture but disabled until a reliable, tested mechanism exists."); }
522};
524function validateLabel(fstype, label) {
525 const limit = LABEL_LIMITS[fstype] || 16;
526 if (!LABEL_PATTERN.test(label) || label.length > limit) throw fail("LABEL_INVALID", `Labels of ${fstype} may have at most ${limit} characters: A-Z, a-z, 0-9, dot, dash and underscore.`);
528function relocate(ctx, disk) {
529 if (ctx.relocated.has(disk.name)) return;
530 ctx.relocated.add(disk.name);
531 touchDisk(ctx, disk, { backup: true });
532 step(ctx, "gpt-relocate", [disk.path], { kind: "partition" });
533 disk.table.lastUsableNow = disk.table.lastUsable; disk.table.gptBackupMisplaced = false;
535function syncPart(sim, node) {
536 if (node.kind !== "part") return;
537 const disk = sim.disks.get(node.parent);
538 const part = disk?.partitions.find((entry) => entry.number === node.partition || entry.kernel === node.kname);
539 if (part) Object.assign(part, { fstype: node.fstype, label: node.label, mountpoints: node.mountpoints, usage: node.usage });
542function normalizeOperation(input) {
543 if (!input || typeof input !== "object") throw fail("ARGUMENTS_INVALID", "Invalid operation.");
544 const type = String(input.type || "");
545 if (!OPERATION_TYPES.has(type)) throw fail("ARGUMENTS_INVALID", `Unknown operation ${type}.`);
546 const op = { type };
547 for (const key of ["disk", "device", "label", "name", "fs", "flag", "mountpoint", "lv", "typeFlag"]) if (input[key] !== undefined && input[key] !== null && input[key] !== "") op[key] = String(input[key]).slice(0, 200);
548 for (const key of ["number", "start"]) if (input[key] !== undefined) op[key] = Number(input[key]);
549 if (input.size !== undefined) op.size = input.size === "max" ? "max" : Number(input.size);
550 for (const key of ["enabled", "repair", "auto", "advanced"]) if (input[key] !== undefined) op[key] = input[key] === true;
551 return op;
554function diskView(disk, sim) {
555 return {
556 name: disk.name, path: disk.path, size: disk.size, sectors: disk.sectors, logicalSectorSize: disk.logicalSectorSize,
557 table: disk.table ? { type: disk.table.type } : null,
558 segments: (disk.segments || []).map((segment) => {
559 if (segment.kind !== "partition") return { ...segment };
560 const part = disk.partitions.find((entry) => entry.number === segment.number);
561 const node = part && partNode(sim, disk, part);
562 return { ...segment, fstype: node?.fstype ?? part?.fstype ?? null, label: node?.label ?? part?.label ?? null, path: part?.path || null, mountpoints: node?.mountpoints || [], created: part?.created === true };
563 })
564 };
567function planOperations(inventory, operations, options = {}) {
568 if (!inventory?.supported) throw fail("PLATFORM_UNSUPPORTED", "Disk changes are supported only on Linux servers.");
569 if (!inventory.privileged) throw fail("PRIVILEGE_REQUIRED", "Planning changes needs the privileged (root/sudo) inventory.");
570 if (!Array.isArray(operations) || !operations.length || operations.length > MAX_OPERATIONS) throw fail("ARGUMENTS_INVALID", `Plan 1 to ${MAX_OPERATIONS} operations.`);
571 const sim = createSim(inventory);
572 const before = new Map([...sim.disks].map(([name, disk]) => [name, diskView(disk, sim)]));
573 const ctx = createContext(sim);
574 const normalized = operations.map((operation) => normalizeOperation(operation));
575 normalized.forEach((op, index) => {
576 ctx.currentOperation = index;
577 try { HANDLERS[op.type](sim, ctx, op); }
578 catch (error) { error.details = { ...(error.details || {}), operation: index }; throw error; }
579 });
580 if (!ctx.steps.length) throw fail("NO_CHANGE", "The plan has no steps.");
581 // Guard steps run first: the job refuses to touch anything if the layout differs from this plan.
582 const guards = [];
583 for (const name of ctx.verifyDisks) {
584 const disk = inventory.disks.find((entry) => entry.name === name);
585 guards.push({ name: "verify-disk", args: [disk.path, String(disk.sectors), disk.table?.hash || "none"], operation: null, kind: "verify", destructive: false });
586 }
587 for (const [path, size] of ctx.verifySizes) guards.push({ name: "verify-size", args: [path, String(size)], operation: null, kind: "verify", destructive: false });
588 for (const name of ctx.backupDisks) guards.push({ name: "backup-table", args: [inventory.disks.find((entry) => entry.name === name).path], operation: null, kind: "backup", destructive: false });
589 // Final verification of every partition the plan created or resized.
590 const finals = [];
591 for (const entry of ctx.steps) {
592 if (entry.name === "resize-part") finals.push({ name: "verify-part", args: [entry.args[0], entry.args[1], entry.args[2], entry.args[4]], operation: entry.operation, kind: "verify", destructive: false });
593 if (entry.name === "create-part") finals.push({ name: "verify-part", args: [entry.args[0], entry.args[1], entry.args[2], entry.args[3]], operation: entry.operation, kind: "verify", destructive: false });
594 }
595 const steps = [...guards, ...ctx.steps, ...finals];
596 const touched = new Set([...ctx.verifyDisks]);
597 return {
598 operations: normalized,
599 steps,
600 planText: steps.map((entry) => [entry.name, ...entry.args].join(" ")).join("\n"),
601 before: [...touched].map((name) => before.get(name)).filter(Boolean),
602 after: [...touched].map((name) => diskView(sim.disks.get(name), sim)),
603 dataLoss: ctx.dataLoss,
604 destructive: steps.some((entry) => entry.destructive),
605 offline: [...ctx.offline],
606 mayRequireReboot: ctx.mayReboot,
607 backupDisks: [...ctx.backupDisks].map((name) => inventory.disks.find((entry) => entry.name === name).path),
608 notes: ctx.notes,
609 title: String(options.title || normalized.map((op) => op.type).join(", ")).slice(0, 200)
610 };
613// ---------- automatic expansion ----------
614function suggestions(inventory) {
615 if (!inventory?.supported) return [];
616 const result = [];
617 const seen = new Set();
618 const attempt = (id, kind, target, freeBytes, operations) => {
619 if (seen.has(id)) return;
620 seen.add(id);
621 const entry = { id, kind, target, freeBytes, operations, available: false, blocked: null, steps: [] };
622 if (!inventory.privileged) entry.blocked = { code: "DISKS_PRIVILEGE_REQUIRED", message: "Load the privileged inventory (sudo) to evaluate this expansion." };
623 else {
624 try {
625 const plan = planOperations(inventory, operations, { title: `auto-expand ${target}` });
626 Object.assign(entry, { available: true, steps: plan.steps.map((item) => item.name), mayRequireReboot: plan.mayRequireReboot, notes: plan.notes });
627 } catch (error) { entry.blocked = { code: error.code || "DISKS_FAILED", message: error.message }; }
628 }
629 result.push(entry);
630 };
631 for (const disk of inventory.disks) {
632 if (!disk.table || !["gpt", "dos"].includes(disk.table.type)) continue;
633 for (const part of disk.partitions) {
634 if (part.extended || part.logical) continue;
635 const next = disk.segments[disk.segments.findIndex((segment) => segment.kind === "partition" && segment.number === part.number) + 1];
636 if (!next || next.kind !== "free" || next.bytes < AUTO_MIN_BYTES) continue;
637 if (part.fstype && ["linux_raid_member", "zfs_member", "vfat"].includes(part.fstype)) continue;
638 attempt(`partition:${disk.name}:${part.number}`, "partition", part.path || partPath(disk.name, part.number), next.bytes, [{ type: "resize-partition", disk: disk.name, number: part.number, size: "max", auto: true }]);
639 }
640 }
641 for (const node of inventory.nodes) {
642 if (!node.fsSize || !node.size || !["ext2", "ext3", "ext4", "xfs", "btrfs"].includes(node.fstype)) continue;
643 if (node.size - node.fsSize < AUTO_MIN_BYTES) continue;
644 attempt(`filesystem:${node.kname}`, "filesystem", node.displayPath || node.path, node.size - node.fsSize, [{ type: "grow-fs", device: node.path }]);
645 }
646 for (const pv of inventory.lvm?.pvs || []) {
647 if (!pv.deviceSize || pv.deviceSize - pv.size < AUTO_MIN_BYTES) continue;
648 attempt(`pv:${pv.path}`, "pv", pv.path, pv.deviceSize - pv.size, [{ type: "pv-resize", device: pv.path, auto: true }]);
649 }
650 for (const vg of inventory.lvm?.vgs || []) {
651 if (!vg.free || vg.free < AUTO_MIN_BYTES) continue;
652 const sim = createSim(inventory);
653 const lv = autoLv(sim, sim.lvm.vgs.find((entry) => entry.name === vg.name));
654 if (lv) attempt(`lv:${lv.vg}/${lv.name}`, "lv", `${lv.vg}/${lv.name}`, vg.free, [{ type: "lv-extend", lv: `${lv.vg}/${lv.name}`, size: "max" }]);
655 }
656 return result;
659module.exports = { FILESYSTEMS, LABEL_LIMITS, MAX_OPERATIONS, MOUNTPOINT_PATTERN, OPERATION_TYPES, partPath, planOperations, suggestions };
SHA-256145faf4fc4b3238d3a91bc2188dbcece5bce569c4e79f588712dc9f0fd8b0f17

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