Veröffentlichte Quellen
Quellen als ZIP herunterladen
CH-J Tools
Durchsuchen Sie Verzeichnisse und Dateien einer bestimmten Anwendungsausgabe.
CH-J / Server Manager
406 Dateien
core-20261024T135205Z-7c487d719de2
Hinzugefügt: · Erstellt:
1
"use strict";3
const { SessionError } = require("../sessions/sessionManager");4
const { 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, then8
// grow partition 2" works. Changing a partition boundary and resizing the content are separate steps, ordered by9
// the content type and its layers (disk -> partition -> LUKS -> LVM -> file system). Nothing here touches a server.10
const MAX_OPERATIONS = 32;11
const AUTO_MIN_BYTES = 16 * MiB;12
const FILESYSTEMS = Object.freeze(["ext4", "xfs", "btrfs", "vfat", "ntfs", "swap"]);13
const MKFS_TOOL = Object.freeze({ ext4: "mkfs.ext4", xfs: "mkfs.xfs", btrfs: "mkfs.btrfs", vfat: "mkfs.vfat", ntfs: "mkfs.ntfs", swap: "mkswap" });14
const MIN_FS_BYTES = Object.freeze({ ext4: 16 * MiB, xfs: 300 * MiB, btrfs: 256 * MiB, vfat: 33 * MiB, ntfs: 16 * MiB, swap: MiB });15
const LABEL_LIMITS = Object.freeze({ ext2: 16, ext3: 16, ext4: 16, xfs: 12, btrfs: 36, vfat: 11, ntfs: 32, swap: 15 });16
const MOUNTABLE = new Set(["ext2", "ext3", "ext4", "xfs", "btrfs", "vfat", "ntfs", "exfat"]);17
const 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"]);18
const FLAGS = Object.freeze({ gpt: ["legacy_boot", "esp", "bios-boot", "lvm", "raid", "swap", "basic-data"], dos: ["boot", "esp", "lvm", "raid", "swap"] });19
const LABEL_PATTERN = /^[A-Za-z0-9._-]{1,36}$/;20
const MOUNTPOINT_PATTERN = /^\/(mnt|media)\/[A-Za-z0-9._-]{1,64}(\/[A-Za-z0-9._-]{1,64}){0,3}$/;22
function fail(code, message, details) { return new SessionError(`DISKS_${code}`, message, details); }23
const clone = (value) => JSON.parse(JSON.stringify(value));24
const isExt = (fstype) => /^ext[234]$/.test(String(fstype || ""));25
function partPath(diskName, number) { return `/dev/${diskName}${/\d$/.test(diskName) ? "p" : ""}${number}`; }27
// ---------- simulation state ----------28
function 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;33
}34
function 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);37
}38
function 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;42
}43
function 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;47
}48
function 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;52
}53
function partNode(sim, disk, part) {54
return (part.kernel && sim.nodes.get(part.kernel)) || sim.byPath.get(partPath(disk.name, part.number)) || null;55
}56
function 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;65
}66
function 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.`);69
}70
function 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);74
}75
const alignUp = (value, align) => Math.ceil(value / align) * align;76
const 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).78
function 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;81
}83
// ---------- plan context ----------84
function 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: [] };86
}87
function 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 });91
}92
function 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);96
}97
function 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);101
}102
function 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 });104
}105
function 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;113
}114
// Boot partitions of the disk that runs the system are not "in use" by a mount, but deleting or formatting them115
// makes the server unbootable. They are treated like mounted system partitions.116
function 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.`);118
}119
function 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.`);122
}124
// ---------- content chains (grow outward from the partition, shrink inward) ----------125
function 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;179
}181
function 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", root189
? `${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
}225
}227
function 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;232
}233
function 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;238
}239
function 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);257
}259
// ---------- operation handlers ----------260
const 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
};524
function 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.`);527
}528
function 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;534
}535
function 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 });540
}542
function 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;552
}554
function 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
};565
}567
function 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
};611
}613
// ---------- automatic expansion ----------614
function 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;657
}659
module.exports = { FILESYSTEMS, LABEL_LIMITS, MAX_OPERATIONS, MOUNTPOINT_PATTERN, OPERATION_TYPES, partPath, planOperations, suggestions };SHA-256
145faf4fc4b3238d3a91bc2188dbcece5bce569c4e79f588712dc9f0fd8b0f17CH-J Proprietary Software License 1.14 · Die Quellen werden unter der CH-J Proprietary Software License 1.14 bereitgestellt. Ihre Verfügbarkeit ändert die Lizenzbedingungen nicht und gewährt keine zusätzlichen Rechte.
SHA-256 des Archivs
476ed8aef0f1e96a5acdf223127b6692acdf4492eeaf5d0816b6e2a634fbbda6