CH-J ToolsSerververwaltung über SSH
Menü
Veröffentlichte Quellen

CH-J Tools

Durchsuchen Sie Verzeichnisse und Dateien einer bestimmten Anwendungsausgabe.

Quellen als ZIP herunterladen
CH-J / Server Manager 406 Dateien
core-20261024T135205Z-7c487d719de2 Hinzugefügt: · Erstellt:
inventory.js Hinzugefügt: Datum nicht erfasst 27,7 KB · 490 Zeilen
Code Datei herunterladen
1"use strict";
3// Builds the chj.disks model from the output of remoteDisks.sh "inspect". The device graph comes from sysfs
4// (holders/slaves), so layered stacks such as disk -> partition -> LUKS -> LVM PV -> VG -> LV -> ext4 are
5// represented without assuming that a partition directly contains a file system. lsblk/blkid/sfdisk/LVM JSON only
6// enrich nodes; without them (BusyBox, no root) the model is marked partial instead of guessing.
7const text = (value, limit = 200) => String(value ?? "").replace(/[\x00-\x1f\x7f]/g, " ").trim().slice(0, limit);
8const int = (value) => { const result = Number(value); return value === "" || value === null || value === undefined || !Number.isFinite(result) ? null : result; };
9const lines = (value) => String(value || "").split(/\r?\n/).filter(Boolean);
10const MiB = 1048576;
12const GPT_TYPES = Object.freeze({
13 "0FC63DAF-8483-4772-8E79-3D69D8477DE4": "linux",
14 "4F68BCE3-E8CD-4DB1-96E7-FBCAF984B709": "linux-root-x86-64",
15 "B921B045-1DF0-41C3-AF44-4C6F280D3FAE": "linux-root-arm64",
16 "BC13C2FF-59E6-4262-A352-B275FD6F7172": "linux-extended-boot",
17 "933AC7E1-2EB4-4F13-B844-0E14E2AEF915": "linux-home",
18 "3B8F8425-20E0-4F3B-907F-1A25A76F98E8": "linux-server-data",
19 "4D21B016-B534-45C2-A9FB-5C16E091FD2D": "linux-var",
20 "0657FD6D-A4AB-43C4-84E5-0933C84B4F4F": "swap",
21 "E6D6D379-F507-44C2-A23C-238F2A3DF928": "lvm",
22 "A19D880F-05FC-4D3B-A006-743F0F84911E": "raid",
23 "CA7D7CCB-63ED-4C53-861C-1742536059CC": "luks",
24 "C12A7328-F81F-11D2-BA4B-00A0C93EC93B": "esp",
25 "21686148-6449-6E6F-744E-656564454649": "bios-boot",
26 "EBD0A0A2-B9E5-4433-87C0-68B6B72699C7": "basic-data",
27 "E3C9E316-0B5C-4DB8-817D-F92DF00215AE": "microsoft-reserved",
28 "DE94BBA4-06D1-4D40-A16A-BFD50179D6AC": "windows-recovery",
29 "6A898CC3-1DD2-11B2-99A6-080020736631": "zfs",
30 "516E7CBA-6ECF-11D6-8FF8-00022D09712B": "freebsd-zfs"
31});
32const DOS_TYPES = Object.freeze({ "83": "linux", "82": "swap", "8e": "lvm", "fd": "raid", "ef": "esp", "7": "basic-data", "b": "fat32", "c": "fat32", "e": "fat16", "5": "extended", "f": "extended", "85": "extended", "ee": "gpt-protective" });
33// Partition types CH-J writes (GPT GUID / MBR type) for new partitions and type flags.
34const PARTITION_TYPE_CODES = Object.freeze({
35 linux: { gpt: "0FC63DAF-8483-4772-8E79-3D69D8477DE4", dos: "83" },
36 swap: { gpt: "0657FD6D-A4AB-43C4-84E5-0933C84B4F4F", dos: "82" },
37 lvm: { gpt: "E6D6D379-F507-44C2-A23C-238F2A3DF928", dos: "8e" },
38 raid: { gpt: "A19D880F-05FC-4D3B-A006-743F0F84911E", dos: "fd" },
39 esp: { gpt: "C12A7328-F81F-11D2-BA4B-00A0C93EC93B", dos: "ef" },
40 "bios-boot": { gpt: "21686148-6449-6E6F-744E-656564454649", dos: null },
41 "basic-data": { gpt: "EBD0A0A2-B9E5-4433-87C0-68B6B72699C7", dos: "7" },
42 fat32: { gpt: "EBD0A0A2-B9E5-4433-87C0-68B6B72699C7", dos: "c" }
43});
44const SYSTEM_MOUNTS = /^(\/|\/boot(\/.*)?|\/efi|\/usr(\/.*)?|\/var(\/.*)?|\/home|\/opt|\/srv|\/root|\/tmp|\/nix|\/snap(\/.*)?)$/;
45const VIRTUAL_MODEL = /QEMU|VBOX|VirtualBox|VMware|Virtual disk|Virtual HD|Msft Virtual|Xen|Hyper-V|BHYVE|Red Hat VirtIO/i;
47function splitSections(stdout) {
48 const sections = {};
49 const pattern = /\x1eCHJ_SECTION=([a-z]+)\r?\n/g;
50 let current = null; let start = 0; let match;
51 while ((match = pattern.exec(stdout))) {
52 if (current) sections[current] = stdout.slice(start, match.index);
53 current = match[1]; start = pattern.lastIndex;
54 }
55 if (current) sections[current] = stdout.slice(start);
56 return sections;
58const keyValues = (value) => Object.fromEntries(lines(value).map((line) => [line.slice(0, line.indexOf("=")), line.slice(line.indexOf("=") + 1)]).filter(([key]) => key));
59const parseJson = (value) => { try { return JSON.parse(value); } catch { return null; } };
60const unescapeMount = (value) => String(value).replace(/\\(\d{3})/g, (_m, octal) => String.fromCharCode(Number.parseInt(octal, 8)));
62function parseSysblock(section) {
63 return lines(section).map((line) => {
64 const f = line.split("|");
65 return {
66 kname: text(f[0], 64), devno: text(f[1], 16), sectors512: int(f[2]), readOnly: f[3] === "1", removable: f[4] === "1",
67 partition: f[5] === "-" ? null : int(f[5]), start512: f[6] === "-" ? null : int(f[6]), parent: f[7] === "-" ? null : text(f[7], 64),
68 rotational: f[8] === "" ? null : f[8] === "1", logicalSectorSize: int(f[9]) || 512, physicalSectorSize: int(f[10]) || 512,
69 model: text(f[11], 80), vendor: text(f[12], 40), serial: text(f[13], 80), wwid: text(f[14], 120),
70 holders: String(f[15] || "").split(",").filter(Boolean).map((name) => text(name, 64)),
71 slaves: String(f[16] || "").split(",").filter(Boolean).map((name) => text(name, 64)),
72 dmName: text(f[17], 128), dmUuid: text(f[18], 160), loopBacking: text(f[19], 200), mdLevel: text(f[20], 16),
73 sysPath: text(f[21], 200), discardMax: int(f[22])
74 };
75 }).filter((entry) => /^[A-Za-z0-9._:+-]{1,64}$/.test(entry.kname));
78function parseBlkid(section) {
79 const result = new Map();
80 for (const block of String(section || "").split(/\n\s*\n/)) {
81 const values = keyValues(block);
82 if (values.DEVNAME) result.set(values.DEVNAME, values);
83 }
84 return result;
87function parseMountinfo(section) {
88 return lines(section).map((line) => {
89 const [left, right = ""] = line.split(" - ");
90 const fields = left.split(" "); const tail = right.split(" ");
91 return { devno: fields[2], root: unescapeMount(fields[3] || ""), mountpoint: unescapeMount(fields[4] || ""), options: fields[5] || "", fstype: tail[0] || "", source: unescapeMount(tail[1] || "") };
92 }).filter((entry) => entry.mountpoint);
95function parseDf(section) {
96 const result = new Map();
97 for (const line of lines(section).slice(1)) {
98 const match = line.match(/^(\S+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)%\s+(.+)$/);
99 if (match) result.set(match[6], { size: Number(match[2]) * 1024, used: Number(match[3]) * 1024, available: Number(match[4]) * 1024 });
100 }
101 return result;
104function parseSwaps(section) {
105 return lines(section).slice(1).map((line) => {
106 const [name, type, size, used, priority] = line.trim().split(/\s+/);
107 return { path: unescapeMount(name), type, size: Number(size) * 1024, used: Number(used) * 1024, priority: Number(priority) };
108 });
111function parsePartitionTables(section) {
112 const tables = new Map();
113 const pattern = /@@disk (\S+) (\S+)\n([\s\S]*?)\n@@end/g;
114 let match;
115 while ((match = pattern.exec(String(section || "")))) {
116 const json = parseJson(match[3]);
117 if (json?.partitiontable) tables.set(match[1], { ...json.partitiontable, hash: /^[a-f0-9]{64}$/.test(match[2]) ? match[2] : null });
118 }
119 return tables;
122function parseLvm(section) {
123 const parts = {};
124 for (const block of String(section || "").split(/^@@/m).filter(Boolean)) {
125 const name = block.slice(0, block.indexOf("\n")).trim();
126 parts[name] = parseJson(block.slice(block.indexOf("\n") + 1))?.report?.[0] || {};
127 }
128 const n = (value) => int(String(value ?? "").trim());
129 return {
130 available: Boolean(section),
131 pvs: (parts.pvs?.pv || []).map((pv) => ({ path: text(pv.pv_name), vg: text(pv.vg_name, 128), size: n(pv.pv_size), free: n(pv.pv_free), deviceSize: n(pv.dev_size), uuid: text(pv.pv_uuid, 64), attr: text(pv.pv_attr, 16) })),
132 vgs: (parts.vgs?.vg || []).map((vg) => ({ name: text(vg.vg_name, 128), uuid: text(vg.vg_uuid, 64), size: n(vg.vg_size), free: n(vg.vg_free), extentSize: n(vg.vg_extent_size), freeExtents: n(vg.vg_free_count), pvCount: n(vg.pv_count), lvCount: n(vg.lv_count), attr: text(vg.vg_attr, 16) })),
133 lvs: (parts.lvs?.lv || []).filter((lv) => !String(lv.lv_name).startsWith("[")).map((lv) => {
134 const attr = text(lv.lv_attr, 16);
135 return {
136 name: text(lv.lv_name, 128), vg: text(lv.vg_name, 128), uuid: text(lv.lv_uuid, 64), size: n(lv.lv_size), attr, path: text(lv.lv_path), dmPath: text(lv.lv_dm_path),
137 pool: text(lv.pool_lv, 128), dataPercent: n(lv.data_percent), metadataPercent: n(lv.metadata_percent), segtype: text(lv.segtype, 32), origin: text(lv.origin, 128),
138 // lv_attr[0]: t = thin pool, V = thin volume, s/S = snapshot, o = origin; lv_attr[4]: a = active.
139 thinPool: attr[0] === "t", thinVolume: attr[0] === "V", snapshot: /^[sS]/.test(attr), active: attr[4] === "a",
140 pveGuest: (String(lv.lv_name).match(/^(?:vm|base)-(\d+)-(?:disk|cloudinit|state|tpmstate)/) || [])[1] || null
141 };
142 })
143 };
146function parseCrypt(section) {
147 const result = new Map();
148 for (const block of String(section || "").split(/^@@map /m).filter(Boolean)) {
149 const name = block.slice(0, block.indexOf("\n")).trim();
150 const values = {};
151 for (const line of lines(block.slice(block.indexOf("\n") + 1))) {
152 const index = line.indexOf(":");
153 if (index > 0) values[line.slice(0, index).trim().toLowerCase()] = text(line.slice(index + 1), 120);
154 }
155 result.set(name, { type: values.type || null, cipher: values.cipher || null, keySize: values.keysize || null, keyLocation: values["key location"] || null, device: values.device || null, mode: values.mode || null, sectorSize: int(values["sector size"]) });
156 }
157 return result;
160function parseFsinfo(section) {
161 const result = new Map();
162 for (const block of String(section || "").split(/^@@fs /m).filter(Boolean)) {
163 const [device, type] = block.slice(0, block.indexOf("\n")).trim().split(/\s+/);
164 const body = block.slice(block.indexOf("\n") + 1);
165 if (/^ext[234]$/.test(type)) {
166 const value = (key) => int((body.match(new RegExp(`^${key}:\\s*(\\d+)`, "m")) || [])[1]);
167 const blocks = value("Block count"); const blockSize = value("Block size"); const free = value("Free blocks");
168 if (blocks && blockSize) result.set(device, { fsSize: blocks * blockSize, fsUsed: free === null ? null : (blocks - free) * blockSize, blockSize, state: text((body.match(/^Filesystem state:\s*(.+)$/m) || [])[1], 40) });
169 } else if (type === "xfs") {
170 const data = body.match(/^data\s*=.*?bsize=(\d+)\s+blocks=(\d+)/m);
171 if (data) result.set(device, { fsSize: Number(data[1]) * Number(data[2]), blockSize: Number(data[1]) });
172 }
173 }
174 return result;
177function parseBtrfs(section) {
178 const filesystems = [];
179 let current = null;
180 for (const line of lines(section)) {
181 const label = line.match(/^Label:\s*(?:'(.*)'|none)\s+uuid:\s*([0-9a-f-]+)/);
182 if (label) { current = { label: label[1] || null, uuid: label[2], devices: [] }; filesystems.push(current); continue; }
183 const device = line.match(/^\s*devid\s+(\d+)\s+size\s+(\d+)\s+used\s+(\d+)\s+path\s+(\S+)/);
184 if (device && current) current.devices.push({ devid: Number(device[1]), size: Number(device[2]), used: Number(device[3]), path: device[4] });
185 }
186 return filesystems;
189function parseZpool(section) {
190 const pools = [];
191 let current = null;
192 for (const line of lines(section)) {
193 const fields = line.split("\t");
194 if (fields[0]) { current = { name: text(fields[0], 64), size: int(fields[1]), allocated: int(fields[2]), free: int(fields[3]), health: text(fields[9] || fields[fields.length - 1], 16), devices: [] }; pools.push(current); }
195 else if (current && fields[1] && fields[1].startsWith("/dev/")) current.devices.push(text(fields[1], 200));
196 }
197 return pools;
200function parseLsblk(section) {
201 const json = parseJson(section);
202 const map = new Map();
203 for (const entry of json?.blockdevices || []) if (entry.kname || entry.name) map.set(entry.kname || entry.name, entry);
204 return map;
207function mediaType(entry) {
208 if (/^loop/.test(entry.kname)) return "loop";
209 if (/^nvme/.test(entry.kname)) return VIRTUAL_MODEL.test(entry.model) ? "virtual" : "nvme";
210 if (/^(vd|xvd)/.test(entry.kname) || VIRTUAL_MODEL.test(entry.model) || VIRTUAL_MODEL.test(entry.vendor)) return "virtual";
211 if (entry.rotational === true) return "hdd";
212 if (entry.rotational === false) return "ssd";
213 return "unknown";
215function transport(entry, lsblk) {
216 if (lsblk?.tran) return text(lsblk.tran, 16);
217 const path = entry.sysPath;
218 if (/^nvme/.test(entry.kname)) return "nvme";
219 if (/\/virtio\d+\/block\//.test(path)) return "virtio";
220 if (/\/virtio\d+\/host\d+\//.test(path)) return "virtio-scsi";
221 if (/\/usb\d+\//.test(path)) return "usb";
222 if (/\/ata\d+\//.test(path)) return "sata";
223 if (/\/host\d+\/target/.test(path)) return "scsi";
224 if (/^mmcblk/.test(entry.kname)) return "mmc";
225 return null;
228function nodeKind(entry) {
229 if (entry.partition !== null) return "part";
230 if (/^dm-/.test(entry.kname)) {
231 if (/^CRYPT-/.test(entry.dmUuid)) return "crypt";
232 if (/^LVM-/.test(entry.dmUuid)) return "lvm";
233 return "dm";
234 }
235 if (/^md\d/.test(entry.kname)) return "md";
236 if (/^loop/.test(entry.kname)) return "loop";
237 return "disk";
240// Unescape a device-mapper LVM name ("vg--data-lv" -> vg "vg-data", lv "lv").
241function splitDmLvName(name) {
242 const match = String(name).match(/^((?:[^-]|--)+)-((?:[^-]|--)+)$/);
243 return match ? { vg: match[1].replace(/--/g, "-"), lv: match[2].replace(/--/g, "-") } : null;
246function buildInventory(stdout) {
247 const sections = splitSections(stdout);
248 const meta = keyValues(sections.meta);
249 const tools = keyValues(sections.tools);
250 const versions = keyValues(sections.versions);
251 const privileged = meta.uid === "0";
252 const sys = parseSysblock(sections.sysblock);
253 const lsblk = parseLsblk(sections.lsblk);
254 const blkid = parseBlkid(sections.blkid);
255 const mounts = parseMountinfo(sections.mountinfo);
256 const df = parseDf(sections.df);
257 const swaps = parseSwaps(sections.swaps);
258 const tables = parsePartitionTables(sections.pt);
259 const lvm = parseLvm(sections.lvm);
260 const crypt = parseCrypt(sections.crypt);
261 const fsinfo = parseFsinfo(sections.fsinfo);
262 const btrfs = parseBtrfs(sections.btrfs);
263 const zpools = parseZpool(sections.zpool);
264 const mdstat = String(sections.mdstat || "");
266 const nodes = new Map();
267 for (const entry of sys) {
268 if (/^(ram|zram|fd)\d*$/.test(entry.kname)) continue;
269 // Unused loop devices (size 0) and read-only snap images are not disks anyone manages here.
270 if (/^loop\d+$/.test(entry.kname) && (!entry.sectors512 || entry.readOnly || !entry.loopBacking)) continue;
271 if (/^sr\d+$/.test(entry.kname)) continue;
272 const ls = lsblk.get(entry.kname) || {};
273 const path = entry.dmName && /^dm-/.test(entry.kname) ? `/dev/mapper/${entry.dmName}` : `/dev/${entry.kname}`;
274 const probe = blkid.get(`/dev/${entry.kname}`) || blkid.get(path) || {};
275 const kind = nodeKind(entry);
276 const fstype = text(probe.TYPE || ls.fstype || "", 32) || null;
277 const node = {
278 kname: entry.kname, path, kind, devno: entry.devno,
279 size: entry.sectors512 === null ? null : entry.sectors512 * 512,
280 readOnly: entry.readOnly, parent: entry.parent, partition: entry.partition,
281 start512: entry.start512, holders: entry.holders, slaves: entry.slaves,
282 fstype, fsVersion: text(probe.VERSION || ls.fsver || "", 16) || null,
283 label: text(probe.LABEL ?? ls.label ?? "", 64) || null,
284 uuid: text(probe.UUID ?? ls.uuid ?? "", 64) || null,
285 partuuid: text(probe.PARTUUID ?? ls.partuuid ?? "", 64) || null,
286 partlabel: text(probe.PARTLABEL ?? ls.partlabel ?? "", 72) || null,
287 parttype: text(ls.parttype ?? "", 40) || null,
288 dmName: entry.dmName || null, dmUuid: entry.dmUuid || null,
289 loopBacking: entry.loopBacking || null,
290 mdLevel: entry.mdLevel || null,
291 mountpoints: [], usage: null, fsSize: null, fsUsed: null, swapActive: false,
292 lvm: null, crypt: null, zfsPool: null, btrfs: null, _sys: entry, _lsblk: ls
293 };
294 nodes.set(entry.kname, node);
295 }
296 const byDevno = new Map([...nodes.values()].map((node) => [node.devno, node]));
297 const byPath = new Map();
298 for (const node of nodes.values()) {
299 byPath.set(node.path, node); byPath.set(`/dev/${node.kname}`, node);
300 if (node.dmName) byPath.set(`/dev/mapper/${node.dmName}`, node);
301 }
303 // Mounts: by device number first (exact), by source path otherwise (Btrfs uses anonymous device numbers).
304 for (const mount of mounts) {
305 const node = byDevno.get(mount.devno) || byPath.get(mount.source);
306 if (!node) continue;
307 if (!node.mountpoints.includes(mount.mountpoint)) node.mountpoints.push(mount.mountpoint);
308 node.mountedFstype ||= mount.fstype;
309 node.mountedReadOnly = /(^|,)ro(,|$)/.test(mount.options);
310 }
311 for (const swap of swaps) { const node = byPath.get(swap.path); if (node) { node.swapActive = true; node.swapUsed = swap.used; } }
312 for (const node of nodes.values()) {
313 const mountpoint = node.mountpoints.find((point) => df.has(point));
314 if (mountpoint) { node.usage = { ...df.get(mountpoint), source: "df", mountpoint }; }
315 const info = fsinfo.get(node.path) || fsinfo.get(`/dev/${node.kname}`);
316 if (info) {
317 node.fsSize = info.fsSize; node.fsState = info.state || null;
318 if (!node.usage && info.fsUsed !== null && info.fsUsed !== undefined) node.usage = { size: info.fsSize, used: info.fsUsed, available: info.fsSize - info.fsUsed, source: "superblock" };
319 }
320 if (!node.fsSize && node.usage && node.fstype === "xfs") node.fsSize = null;
321 }
322 for (const fs of btrfs) for (const device of fs.devices) {
323 const node = byPath.get(device.path);
324 if (node) { node.btrfs = { uuid: fs.uuid, devid: device.devid, devices: fs.devices.length, allocated: device.used }; node.fsSize = device.size; }
325 }
326 for (const pool of zpools) for (const device of pool.devices) { const node = byPath.get(device); if (node) node.zfsPool = pool.name; }
328 // LVM: privileged JSON when available, otherwise the device-mapper name.
329 const pvByPath = new Map(lvm.pvs.map((pv) => [pv.path, pv]));
330 for (const node of nodes.values()) {
331 const pv = pvByPath.get(node.path) || pvByPath.get(`/dev/${node.kname}`);
332 if (pv) node.pv = pv;
333 if (node.kind === "lvm") {
334 const lv = lvm.lvs.find((entry) => entry.dmPath === node.path || `/dev/mapper/${node.dmName}` === entry.dmPath);
335 const names = splitDmLvName(node.dmName);
336 node.lvm = lv ? { ...lv } : names ? { name: names.lv, vg: names.vg, size: node.size, attr: null, thinPool: /-tpool$|_tdata$|_tmeta$/.test(names.lv), thinVolume: false, partial: true } : null;
337 if (node.lvm) node.displayPath = `/dev/${node.lvm.vg}/${node.lvm.name}`;
338 }
339 if (node.kind === "crypt") {
340 const status = crypt.get(node.dmName);
341 const type = (String(node.dmUuid).match(/^CRYPT-([A-Z0-9]+)-/) || [])[1] || null;
342 node.crypt = { type: status?.type || type, cipher: status?.cipher || null, keyLocation: status?.keyLocation || null, mode: status?.mode || null, open: true };
343 }
344 }
346 // Graph edges. Partitions hang below their disk; dm/md below their slaves.
347 for (const node of nodes.values()) {
348 node.parents = node.kind === "part" ? [node.parent].filter((name) => nodes.has(name)) : node.slaves.filter((name) => nodes.has(name));
349 node.children = node.holders.filter((name) => nodes.has(name));
350 }
351 const descendants = (node, seen = new Set()) => {
352 for (const name of node.children) if (!seen.has(name)) { seen.add(name); descendants(nodes.get(name), seen); }
353 return seen;
354 };
355 for (const node of nodes.values()) {
356 node.inUseReasons = [];
357 if (node.mountpoints.length) node.inUseReasons.push({ reason: "mounted", detail: node.mountpoints.join(", ") });
358 if (node.swapActive) node.inUseReasons.push({ reason: "swap" });
359 if (node.children.length) node.inUseReasons.push({ reason: "holders", detail: node.children.map((name) => nodes.get(name).displayPath || nodes.get(name).path).join(", ") });
360 if (node.fstype === "zfs_member") node.inUseReasons.push({ reason: "zfs", detail: node.zfsPool || "" });
361 if (node.fstype === "linux_raid_member" && !node.children.length && /\b(md\d+)\b/.test(mdstat)) node.inUseReasons.push({ reason: "raid-member" });
362 node.inUse = node.inUseReasons.length > 0;
363 const stack = [node, ...[...descendants(node)].map((name) => nodes.get(name))];
364 node.systemRoles = [...new Set(stack.flatMap((item) => [
365 ...item.mountpoints.filter((point) => SYSTEM_MOUNTS.test(point)).map((point) => `mount:${point}`),
366 ...(item.swapActive ? ["swap"] : [])
367 ]))];
368 }
370 const disks = [];
371 for (const node of nodes.values()) {
372 if (!["disk", "loop"].includes(node.kind) || !node.size) continue;
373 const entry = node._sys;
374 const table = tables.get(node.kname) || null;
375 const lbs = Number(table?.sectorsize) || entry.logicalSectorSize || 512;
376 const sectors = Math.floor(node.size / lbs);
377 const align = Math.max(1, Math.floor(MiB / lbs));
378 const partNodes = [...nodes.values()].filter((item) => item.kind === "part" && item.parent === node.kname);
379 let label = table?.label || text(node._lsblk.pttype || "", 8) || null;
380 if (!label && partNodes.length) label = "unknown";
381 const partitions = [];
382 if (table) {
383 for (const part of [...table.partitions || []].sort((a, b) => a.start - b.start)) {
384 const number = Number((String(part.node).match(/(\d+)$/) || [])[1]);
385 const kernelNode = partNodes.find((item) => item.partition === number) || null;
386 partitions.push(describePartition(label, number, part.start, part.size, lbs, part, kernelNode));
387 }
388 } else {
389 for (const part of partNodes.sort((a, b) => a.start512 - b.start512)) {
390 const factor = lbs / 512;
391 partitions.push(describePartition(label, part.partition, Math.round(part.start512 / factor), Math.round(part.size / lbs), lbs, { type: part.parttype, uuid: part.partuuid, name: part.partlabel }, part));
392 }
393 }
394 // Usable area of the table; GPT keeps 33 sectors (header + 128 entries) at both ends.
395 const gptTail = Math.ceil(16384 / lbs) + 1;
396 let firstUsable = label === "gpt" ? Number(table?.firstlba) || gptTail + 1 : align;
397 if (label === "gpt" && firstUsable < align) firstUsable = Math.min(align, firstUsable);
398 const lastUsableNow = label === "gpt" ? Number(table?.lastlba) || sectors - gptTail - 1 : Math.min(sectors - 1, 0xffffffff);
399 const lastUsable = label === "gpt" ? sectors - gptTail - 1 : lastUsableNow;
400 const gptBackupMisplaced = label === "gpt" && Boolean(table?.lastlba) && lastUsableNow < lastUsable;
401 const mbrLimited = label === "dos" && sectors - 1 > 0xffffffff;
402 const segments = label ? buildSegments(partitions, firstUsable, lastUsable, align, lbs, lastUsableNow) : [];
403 const whole = !partitions.length ? node : null;
404 const allRoles = [...new Set([node, ...partitions.map((part) => part.kernel && nodes.get(part.kernel))].filter(Boolean).flatMap((item) => item.systemRoles))];
405 disks.push({
406 name: node.kname, path: node.path, kind: node.kind,
407 model: entry.model || null, vendor: entry.vendor || null, serial: entry.serial || text(node._lsblk.serial || "", 80) || null, wwid: entry.wwid || text(node._lsblk.wwn || "", 120) || null,
408 transport: transport(entry, node._lsblk), mediaType: mediaType(entry), rotational: entry.rotational,
409 discard: entry.discardMax > 0, readOnly: node.readOnly, removable: entry.removable, loopBacking: node.loopBacking,
410 size: node.size, sectors, logicalSectorSize: lbs, physicalSectorSize: entry.physicalSectorSize, alignSectors: align,
411 table: label ? {
412 type: label, id: text(table?.id || node._lsblk.ptuuid || "", 64) || null, firstUsable, lastUsable, lastUsableNow,
413 hash: table?.hash || null, source: table ? "sfdisk" : "sysfs", gptBackupMisplaced, mbrLimited, maxPrimary: label === "dos" ? 4 : 128
414 } : null,
415 partitions, segments,
416 wholeDevice: whole ? { kname: whole.kname, fstype: whole.fstype } : null,
417 systemRoles: allRoles, system: allRoles.length > 0,
418 inUse: node.inUse || partitions.some((part) => part.inUse)
419 });
420 }
422 const pve = meta.pve === "1";
423 for (const disk of disks) {
424 const stackKnames = new Set([disk.name, ...disk.partitions.map((part) => part.kernel).filter(Boolean)]);
425 for (const kname of [...stackKnames]) for (const child of descendantsOf(nodes, kname)) stackKnames.add(child);
426 const stackNodes = [...stackKnames].map((name) => nodes.get(name)).filter(Boolean);
427 disk.layers = { luks: stackNodes.some((item) => item.fstype === "crypto_LUKS"), lvm: stackNodes.some((item) => item.fstype === "LVM2_member" || item.kind === "lvm"), raid: stackNodes.some((item) => item.fstype === "linux_raid_member" || item.kind === "md"), zfs: stackNodes.some((item) => item.fstype === "zfs_member") };
428 // Proxmox host storage (LVM/ZFS behind guests or the host) is never a target of destructive changes.
429 disk.protected = pve && (disk.layers.lvm || disk.layers.zfs || disk.system) ? "pve-host-storage" : null;
430 }
432 const publicNodes = [...nodes.values()].map(({ _sys, _lsblk, holders, slaves, ...rest }) => rest);
433 return {
434 platform: "linux", supported: true, privileged,
435 os: text(meta.os, 120), osId: text(meta.osId, 40), osLike: text(meta.osLike, 80), kernel: text(meta.kernel, 80),
436 systemd: meta.systemd === "1", pve, selinux: meta.selinux ? text(meta.selinux, 20) : null,
437 tools: Object.fromEntries(Object.entries(tools).map(([key, value]) => [text(key, 32), text(value, 120)])),
438 versions: Object.fromEntries(Object.entries(versions).map(([key, value]) => [text(key, 32), text(value, 120)])),
439 sources: { lsblk: lsblk.size > 0, blkid: blkid.size > 0, sfdisk: tables.size > 0, lvm: lvm.available, crypt: crypt.size > 0 },
440 partial: !privileged,
441 disks: disks.sort((a, b) => a.name.localeCompare(b.name, "en", { numeric: true })),
442 nodes: publicNodes,
443 lvm: { available: lvm.available, pvs: lvm.pvs, vgs: lvm.vgs, lvs: lvm.lvs },
444 btrfs, zpools,
445 remoteTime: int(meta.time)
446 };
448 function describePartition(tableType, number, start, size, lbs, part, kernelNode) {
449 const typeCode = text(part.type || "", 40).toUpperCase();
450 const typeName = tableType === "gpt" ? GPT_TYPES[typeCode] || null : DOS_TYPES[typeCode.toLowerCase().replace(/^0+(?=.)/, "")] || null;
451 const extended = typeName === "extended";
452 return {
453 number, start, size, end: start + size - 1, bytes: size * lbs,
454 type: typeCode || null, typeName, uuid: text(part.uuid || kernelNode?.partuuid || "", 64) || null, name: text(part.name || kernelNode?.partlabel || "", 72) || null,
455 attrs: text(part.attrs || "", 160) || null, bootable: part.bootable === true,
456 extended, logical: tableType === "dos" && number > 4,
457 kernel: kernelNode?.kname || null, path: kernelNode?.path || null,
458 kernelMatches: kernelNode ? Math.round(kernelNode.size / lbs) === size || extended : false,
459 fstype: kernelNode?.fstype || null, label: kernelNode?.label || null, fsUuid: kernelNode?.uuid || null,
460 mountpoints: kernelNode?.mountpoints || [], usage: kernelNode?.usage || null, fsSize: kernelNode?.fsSize || null,
461 inUse: Boolean(kernelNode?.inUse), inUseReasons: kernelNode?.inUseReasons || [], systemRoles: kernelNode?.systemRoles || [],
462 children: kernelNode?.children || []
463 };
464 }
467function descendantsOf(nodes, kname, seen = new Set()) {
468 for (const child of nodes.get(kname)?.children || []) if (!seen.has(child)) { seen.add(child); descendantsOf(nodes, child, seen); }
469 return seen;
472// Ordered partitions and unallocated gaps (>= one alignment unit) between firstUsable and lastUsable.
473function buildSegments(partitions, firstUsable, lastUsable, align, lbs, lastUsableNow = lastUsable) {
474 const top = partitions.filter((part) => !part.logical).sort((a, b) => a.start - b.start);
475 const segments = [];
476 let cursor = firstUsable;
477 const pushFree = (from, to) => {
478 if (to - from + 1 < align) return;
479 segments.push({ kind: "free", start: from, size: to - from + 1, end: to, bytes: (to - from + 1) * lbs, needsGptRelocation: to > lastUsableNow });
480 };
481 for (const part of top) {
482 if (part.start > cursor) pushFree(cursor, part.start - 1);
483 segments.push({ kind: "partition", number: part.number, start: part.start, size: part.size, end: part.end, bytes: part.bytes });
484 cursor = Math.max(cursor, part.end + 1);
485 }
486 if (cursor <= lastUsable) pushFree(cursor, lastUsable);
487 return segments;
490module.exports = { DOS_TYPES, GPT_TYPES, MiB, PARTITION_TYPE_CODES, SYSTEM_MOUNTS, buildInventory, buildSegments, parseBtrfs, parseLvm, parseMountinfo, splitDmLvName, splitSections };
SHA-25660f0e11482a6b76774419ed3a7471872c0d587f50880933ade67b7fdbaf14b65

CH-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 Archivs476ed8aef0f1e96a5acdf223127b6692acdf4492eeaf5d0816b6e2a634fbbda6
Dateien