@glassmkr/crucible 0.10.2 → 0.10.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/collect/__tests__/c1-c6.test.d.ts +1 -0
- package/dist/collect/__tests__/c1-c6.test.js +160 -0
- package/dist/collect/__tests__/c1-c6.test.js.map +1 -0
- package/dist/collect/edac.d.ts +2 -0
- package/dist/collect/edac.js +104 -0
- package/dist/collect/edac.js.map +1 -0
- package/dist/collect/hardware-raid.d.ts +2 -0
- package/dist/collect/hardware-raid.js +152 -0
- package/dist/collect/hardware-raid.js.map +1 -0
- package/dist/collect/psi.d.ts +20 -0
- package/dist/collect/psi.js +90 -0
- package/dist/collect/psi.js.map +1 -0
- package/dist/collect/reboot-evidence.d.ts +2 -0
- package/dist/collect/reboot-evidence.js +109 -0
- package/dist/collect/reboot-evidence.js.map +1 -0
- package/dist/collect/security.js +23 -0
- package/dist/collect/security.js.map +1 -1
- package/dist/collect/vmstat.d.ts +22 -0
- package/dist/collect/vmstat.js +94 -0
- package/dist/collect/vmstat.js.map +1 -0
- package/dist/collect/zfs.js +94 -0
- package/dist/collect/zfs.js.map +1 -1
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/types.d.ts +117 -0
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// Tests for C1-C6 collectors (v0.10.4, 2026-05-19).
|
|
2
|
+
//
|
|
3
|
+
// Most collectors hit real filesystem paths (/sys/devices/system/edac/,
|
|
4
|
+
// /proc/pressure/, /proc/vmstat, /var/crash/, etc.) so we can't fully
|
|
5
|
+
// integration-test them outside a Linux host with the expected
|
|
6
|
+
// capabilities. These tests cover the pure parser functions and the
|
|
7
|
+
// capability-gate behavior (collectors return null on hosts where the
|
|
8
|
+
// surface is absent).
|
|
9
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
10
|
+
import { __test_only as psiTest } from "../psi.js";
|
|
11
|
+
import { __test_only as vmstatTest, collectVmstat } from "../vmstat.js";
|
|
12
|
+
import { parseZpoolStatus } from "../zfs.js";
|
|
13
|
+
describe("C2 PSI: parsePsiLine", () => {
|
|
14
|
+
it("parses a complete PSI line", () => {
|
|
15
|
+
const r = psiTest.parsePsiLine("some avg10=0.06 avg60=0.04 avg300=0.05 total=12345");
|
|
16
|
+
expect(r).toEqual({ avg10: 0.06, avg60: 0.04, avg300: 0.05, total: 12345 });
|
|
17
|
+
});
|
|
18
|
+
it("returns null on malformed input", () => {
|
|
19
|
+
expect(psiTest.parsePsiLine("some avg10=0.06")).toBeNull();
|
|
20
|
+
expect(psiTest.parsePsiLine("garbage")).toBeNull();
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
describe("C2 PSI: parsePsiFile", () => {
|
|
24
|
+
it("captures both some and full lines (memory/io shape)", () => {
|
|
25
|
+
const out = psiTest.parsePsiFile("some avg10=1.0 avg60=2.0 avg300=3.0 total=100\nfull avg10=0.5 avg60=1.0 avg300=1.5 total=50\n");
|
|
26
|
+
expect(out?.some.avg10).toBe(1.0);
|
|
27
|
+
expect(out?.full?.avg60).toBe(1.0);
|
|
28
|
+
});
|
|
29
|
+
it("works when only 'some' is present (cpu shape)", () => {
|
|
30
|
+
const out = psiTest.parsePsiFile("some avg10=0.0 avg60=0.0 avg300=0.0 total=0\n");
|
|
31
|
+
expect(out?.some.avg10).toBe(0.0);
|
|
32
|
+
expect(out?.full).toBeUndefined();
|
|
33
|
+
});
|
|
34
|
+
it("returns null on empty input", () => {
|
|
35
|
+
expect(psiTest.parsePsiFile("")).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
describe("C3 vmstat: collectVmstat delta tracking", () => {
|
|
39
|
+
// Note: vmstat reads real /proc/vmstat on the test runner; on a
|
|
40
|
+
// dev macOS host the file doesn't exist so collectVmstat returns
|
|
41
|
+
// null cleanly. This is sufficient evidence for the capability
|
|
42
|
+
// gate; behavioral delta tests live in the e2e tier.
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
vmstatTest.resetForTests();
|
|
45
|
+
});
|
|
46
|
+
it("returns null on a host without /proc/vmstat", () => {
|
|
47
|
+
// On macOS / non-Linux, readProcFile returns null and the
|
|
48
|
+
// collector returns null cleanly.
|
|
49
|
+
const r = collectVmstat();
|
|
50
|
+
// We accept either: null (non-Linux) or a snapshot with null
|
|
51
|
+
// rates (Linux but first call). Both prove the capability gate
|
|
52
|
+
// shape.
|
|
53
|
+
if (r !== null) {
|
|
54
|
+
expect(r.pswpin_rate).toBeNull();
|
|
55
|
+
expect(r.pswpout_rate).toBeNull();
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
describe("C6 ZFS: parseZpoolStatus extended vdev fields", () => {
|
|
60
|
+
it("parses a raidz2 pool with a mirrored SLOG and L2ARC", () => {
|
|
61
|
+
const status = [
|
|
62
|
+
" pool: tank",
|
|
63
|
+
" state: ONLINE",
|
|
64
|
+
" scan: scrub repaired 0B in 00:00:30 with 0 errors on Sun Oct 1 02:30:00 2026",
|
|
65
|
+
"config:",
|
|
66
|
+
"",
|
|
67
|
+
"\tNAME STATE READ WRITE CKSUM",
|
|
68
|
+
"\ttank ONLINE 0 0 0",
|
|
69
|
+
"\t raidz2-0 ONLINE 0 0 0",
|
|
70
|
+
"\t sda ONLINE 0 0 0",
|
|
71
|
+
"\t sdb ONLINE 0 0 0",
|
|
72
|
+
"\t sdc ONLINE 0 0 0",
|
|
73
|
+
"\t sdd ONLINE 0 0 0",
|
|
74
|
+
"logs",
|
|
75
|
+
"\t mirror-1 ONLINE 0 0 0",
|
|
76
|
+
"\t nvme0n1p2 ONLINE 0 0 0",
|
|
77
|
+
"\t nvme1n1p2 ONLINE 0 0 0",
|
|
78
|
+
"cache",
|
|
79
|
+
"\t nvme0n1p3 ONLINE 0 0 0",
|
|
80
|
+
"",
|
|
81
|
+
"errors: No known data errors",
|
|
82
|
+
].join("\n");
|
|
83
|
+
const pools = parseZpoolStatus(status);
|
|
84
|
+
expect(pools).toHaveLength(1);
|
|
85
|
+
const p = pools[0];
|
|
86
|
+
expect(p.name).toBe("tank");
|
|
87
|
+
expect(p.state).toBe("ONLINE");
|
|
88
|
+
expect(p.vdevs).toHaveLength(1);
|
|
89
|
+
expect(p.vdevs[0].name).toBe("raidz2-0");
|
|
90
|
+
expect(p.vdevs[0].redundancy_class).toBe("raidz2");
|
|
91
|
+
expect(p.vdevs[0].degraded_disks_count).toBe(0);
|
|
92
|
+
expect(p.slog_vdevs).toHaveLength(1);
|
|
93
|
+
expect(p.slog_vdevs[0].name).toBe("mirror-1");
|
|
94
|
+
expect(p.l2arc_vdevs).toHaveLength(1);
|
|
95
|
+
expect(p.l2arc_vdevs[0].name).toBe("nvme0n1p3");
|
|
96
|
+
});
|
|
97
|
+
it("classifies raidz1 / raidz3 / mirror correctly", () => {
|
|
98
|
+
const status = [
|
|
99
|
+
" pool: a",
|
|
100
|
+
" state: ONLINE",
|
|
101
|
+
"config:",
|
|
102
|
+
"\tNAME STATE",
|
|
103
|
+
"\ta ONLINE",
|
|
104
|
+
"\t raidz1-0 ONLINE",
|
|
105
|
+
"\t s1 ONLINE",
|
|
106
|
+
"\t s2 ONLINE",
|
|
107
|
+
" pool: b",
|
|
108
|
+
" state: ONLINE",
|
|
109
|
+
"config:",
|
|
110
|
+
"\tNAME STATE",
|
|
111
|
+
"\tb ONLINE",
|
|
112
|
+
"\t raidz3-0 ONLINE",
|
|
113
|
+
"\t s1 ONLINE",
|
|
114
|
+
" pool: c",
|
|
115
|
+
" state: ONLINE",
|
|
116
|
+
"config:",
|
|
117
|
+
"\tNAME STATE",
|
|
118
|
+
"\tc ONLINE",
|
|
119
|
+
"\t mirror-0 ONLINE",
|
|
120
|
+
"\t s1 ONLINE",
|
|
121
|
+
].join("\n");
|
|
122
|
+
const pools = parseZpoolStatus(status);
|
|
123
|
+
expect(pools.map((p) => p.vdevs[0]?.redundancy_class)).toEqual([
|
|
124
|
+
"raidz1",
|
|
125
|
+
"raidz3",
|
|
126
|
+
"mirror",
|
|
127
|
+
]);
|
|
128
|
+
});
|
|
129
|
+
it("counts degraded child devices on a DEGRADED vdev", () => {
|
|
130
|
+
const status = [
|
|
131
|
+
" pool: tank",
|
|
132
|
+
" state: DEGRADED",
|
|
133
|
+
"config:",
|
|
134
|
+
"\tNAME STATE",
|
|
135
|
+
"\ttank DEGRADED",
|
|
136
|
+
"\t raidz2-0 DEGRADED",
|
|
137
|
+
"\t sda ONLINE",
|
|
138
|
+
"\t sdb FAULTED",
|
|
139
|
+
"\t sdc ONLINE",
|
|
140
|
+
"\t sdd ONLINE",
|
|
141
|
+
"",
|
|
142
|
+
"errors: No known data errors",
|
|
143
|
+
].join("\n");
|
|
144
|
+
const pools = parseZpoolStatus(status);
|
|
145
|
+
expect(pools[0].vdevs[0].degraded_disks_count).toBe(1);
|
|
146
|
+
});
|
|
147
|
+
it("treats a single-device top-level vdev as stripe (no redundancy)", () => {
|
|
148
|
+
const status = [
|
|
149
|
+
" pool: scratch",
|
|
150
|
+
" state: ONLINE",
|
|
151
|
+
"config:",
|
|
152
|
+
"\tNAME STATE",
|
|
153
|
+
"\tscratch ONLINE",
|
|
154
|
+
"\t sda ONLINE",
|
|
155
|
+
].join("\n");
|
|
156
|
+
const pools = parseZpoolStatus(status);
|
|
157
|
+
expect(pools[0].vdevs[0].redundancy_class).toBe("stripe");
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
//# sourceMappingURL=c1-c6.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"c1-c6.test.js","sourceRoot":"","sources":["../../../src/collect/__tests__/c1-c6.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// EDAC (Error Detection and Correction) counter collection.
|
|
2
|
+
//
|
|
3
|
+
// Per CC_SPEC_FORGE_FOLLOWUP_C1_C6_ACTIVATION_2026-05-19.md (C1).
|
|
4
|
+
//
|
|
5
|
+
// Kernel surface: /sys/devices/system/edac/mc/mcN/{ce_count,ue_count}
|
|
6
|
+
// ce_count = correctable errors (memory controller saw and fixed)
|
|
7
|
+
// ue_count = uncorrectable errors (memory error couldn't be fixed;
|
|
8
|
+
// kernel may have killed the process or panicked)
|
|
9
|
+
//
|
|
10
|
+
// Per-DIMM granularity: /sys/devices/system/edac/mc/mcN/dimmM/
|
|
11
|
+
// dimm_ce_count, dimm_ue_count, dimm_label, dimm_location, size
|
|
12
|
+
//
|
|
13
|
+
// Capability gate: if no `mcN` directories exist under
|
|
14
|
+
// /sys/devices/system/edac/mc/, EDAC is not loaded (no memory
|
|
15
|
+
// controller driver compiled in or no DIMMs reporting) and this
|
|
16
|
+
// collector returns null. The dashboard ecc_errors evaluator falls
|
|
17
|
+
// back to its IPMI-SEL path.
|
|
18
|
+
//
|
|
19
|
+
// On hosts where EDAC is loaded but counters stay at zero, this
|
|
20
|
+
// collector emits zero counters — that's the correct baseline.
|
|
21
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
22
|
+
import { join } from "node:path";
|
|
23
|
+
const EDAC_ROOT = "/sys/devices/system/edac/mc";
|
|
24
|
+
function readUint(path) {
|
|
25
|
+
try {
|
|
26
|
+
const s = readFileSync(path, "utf8").trim();
|
|
27
|
+
if (!/^\d+$/.test(s))
|
|
28
|
+
return null;
|
|
29
|
+
return parseInt(s, 10);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function readString(path) {
|
|
36
|
+
try {
|
|
37
|
+
return readFileSync(path, "utf8").trim();
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function listMcDirs() {
|
|
44
|
+
try {
|
|
45
|
+
return readdirSync(EDAC_ROOT)
|
|
46
|
+
.filter((n) => /^mc\d+$/.test(n))
|
|
47
|
+
.map((n) => join(EDAC_ROOT, n));
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function listDimmDirs(mcPath) {
|
|
54
|
+
try {
|
|
55
|
+
return readdirSync(mcPath)
|
|
56
|
+
.filter((n) => /^dimm\d+$/.test(n) || /^rank\d+$/.test(n))
|
|
57
|
+
.map((n) => join(mcPath, n));
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function collectDimm(dimmPath) {
|
|
64
|
+
const ce = readUint(join(dimmPath, "dimm_ce_count"));
|
|
65
|
+
const ue = readUint(join(dimmPath, "dimm_ue_count"));
|
|
66
|
+
if (ce === null && ue === null)
|
|
67
|
+
return null;
|
|
68
|
+
const label = readString(join(dimmPath, "dimm_label"));
|
|
69
|
+
const location = readString(join(dimmPath, "dimm_location"));
|
|
70
|
+
const sizeStr = readString(join(dimmPath, "size"));
|
|
71
|
+
const sizeMb = sizeStr && /^\d+$/.test(sizeStr) ? parseInt(sizeStr, 10) : null;
|
|
72
|
+
return {
|
|
73
|
+
label: label ?? "",
|
|
74
|
+
location: location ?? "",
|
|
75
|
+
size_mb: sizeMb,
|
|
76
|
+
ce_count: ce ?? 0,
|
|
77
|
+
ue_count: ue ?? 0,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
export function collectEdac() {
|
|
81
|
+
const mcDirs = listMcDirs();
|
|
82
|
+
if (mcDirs.length === 0)
|
|
83
|
+
return null;
|
|
84
|
+
let totalCe = 0;
|
|
85
|
+
let totalUe = 0;
|
|
86
|
+
const dimms = [];
|
|
87
|
+
for (const mc of mcDirs) {
|
|
88
|
+
const ce = readUint(join(mc, "ce_count")) ?? 0;
|
|
89
|
+
const ue = readUint(join(mc, "ue_count")) ?? 0;
|
|
90
|
+
totalCe += ce;
|
|
91
|
+
totalUe += ue;
|
|
92
|
+
for (const dimm of listDimmDirs(mc)) {
|
|
93
|
+
const d = collectDimm(dimm);
|
|
94
|
+
if (d)
|
|
95
|
+
dimms.push(d);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
edac_corrected_total: totalCe,
|
|
100
|
+
edac_uncorrected_total: totalUe,
|
|
101
|
+
dimms,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=edac.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"edac.js","sourceRoot":"","sources":["../../src/collect/edac.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// Hardware RAID controller scraping.
|
|
2
|
+
//
|
|
3
|
+
// Per CC_SPEC_FORGE_FOLLOWUP_C1_C6_ACTIVATION_2026-05-19.md (C5).
|
|
4
|
+
//
|
|
5
|
+
// Detects four vendor CLIs:
|
|
6
|
+
// - perccli (Dell PERC)
|
|
7
|
+
// - ssacli (HPE Smart Array)
|
|
8
|
+
// - storcli (LSI / Broadcom MegaRAID)
|
|
9
|
+
// - arcconf (Adaptec)
|
|
10
|
+
//
|
|
11
|
+
// For each installed CLI, queries the controller state and returns
|
|
12
|
+
// a normalized HardwareRaidController. The vendor-specific output
|
|
13
|
+
// formats vary considerably; this module's parsers are intentionally
|
|
14
|
+
// conservative — they extract a state string ("Optimal", "Degraded",
|
|
15
|
+
// etc.) plus an optional degraded_disks counter when the vendor output
|
|
16
|
+
// makes it easy to find. The dashboard's raid_degraded evaluator pages
|
|
17
|
+
// on any state != "Optimal"; that's the contract.
|
|
18
|
+
//
|
|
19
|
+
// Implementation scope (2026-05-19): perccli + storcli parsers are
|
|
20
|
+
// best-effort because the validation fleet has no hardware RAID
|
|
21
|
+
// controllers to verify against. Real parsing precision lands in
|
|
22
|
+
// follow-up PRs as customers with each vendor surface. The framework
|
|
23
|
+
// here ensures:
|
|
24
|
+
// - empty controllers[] on hosts without any vendor CLI (capability
|
|
25
|
+
// gate; dashboard rule no-ops),
|
|
26
|
+
// - empty controllers[] on hosts with the CLI but no controllers
|
|
27
|
+
// present (rare configurations).
|
|
28
|
+
//
|
|
29
|
+
// The dashboard's mdadm path is unaffected by this module.
|
|
30
|
+
import { run } from "../lib/exec.js";
|
|
31
|
+
async function hasCli(name) {
|
|
32
|
+
const out = await run("which", [name], 2000);
|
|
33
|
+
return !!(out && out.trim());
|
|
34
|
+
}
|
|
35
|
+
async function scrapePerccli() {
|
|
36
|
+
// perccli /c0 show all J — JSON output for controller 0.
|
|
37
|
+
// Multi-controller hosts are rare; query c0 only and let follow-ups
|
|
38
|
+
// expand if a customer surfaces multi-controller hardware.
|
|
39
|
+
const raw = await run("perccli", ["/c0", "show", "all", "J"], 10000);
|
|
40
|
+
if (!raw)
|
|
41
|
+
return [];
|
|
42
|
+
try {
|
|
43
|
+
const obj = JSON.parse(raw);
|
|
44
|
+
const ctrlList = obj?.Controllers ?? [];
|
|
45
|
+
return ctrlList.map((c) => ({
|
|
46
|
+
vendor: "dell",
|
|
47
|
+
controller_id: String(c?.["Command Status"]?.Controller ?? "0"),
|
|
48
|
+
state: String(c?.["Response Data"]?.["Status"]?.["Controller Status"] ?? "Unknown"),
|
|
49
|
+
degraded_disks: null,
|
|
50
|
+
raw_summary: null,
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Output wasn't JSON (older perccli, or controller missing).
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function scrapeStorcli() {
|
|
59
|
+
const raw = await run("storcli", ["/call", "show", "all", "J"], 10000);
|
|
60
|
+
if (!raw)
|
|
61
|
+
return [];
|
|
62
|
+
try {
|
|
63
|
+
const obj = JSON.parse(raw);
|
|
64
|
+
const ctrlList = obj?.Controllers ?? [];
|
|
65
|
+
return ctrlList.map((c) => ({
|
|
66
|
+
vendor: "lsi",
|
|
67
|
+
controller_id: String(c?.["Command Status"]?.Controller ?? "?"),
|
|
68
|
+
state: String(c?.["Response Data"]?.["Status"]?.["Controller Status"] ?? "Unknown"),
|
|
69
|
+
degraded_disks: null,
|
|
70
|
+
raw_summary: null,
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function scrapeSsacli() {
|
|
78
|
+
// ssacli ctrl all show — text format. Conservative: extract one
|
|
79
|
+
// line per "in slot X" entry; status reported on a "Controller Status"
|
|
80
|
+
// line. Real parsing lands when an HPE customer surfaces.
|
|
81
|
+
const raw = await run("ssacli", ["ctrl", "all", "show", "status"], 10000);
|
|
82
|
+
if (!raw)
|
|
83
|
+
return [];
|
|
84
|
+
const lines = raw.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
85
|
+
const controllers = [];
|
|
86
|
+
let pending = null;
|
|
87
|
+
for (const line of lines) {
|
|
88
|
+
const slotMatch = line.match(/in Slot (\S+)/);
|
|
89
|
+
if (slotMatch) {
|
|
90
|
+
if (pending)
|
|
91
|
+
controllers.push(pendingToController(pending));
|
|
92
|
+
pending = { slot: slotMatch[1], status: "Unknown" };
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const statusMatch = line.match(/Controller Status:\s*(.+)/);
|
|
96
|
+
if (statusMatch && pending)
|
|
97
|
+
pending.status = statusMatch[1].trim();
|
|
98
|
+
}
|
|
99
|
+
if (pending)
|
|
100
|
+
controllers.push(pendingToController(pending));
|
|
101
|
+
return controllers;
|
|
102
|
+
}
|
|
103
|
+
function pendingToController(p) {
|
|
104
|
+
return {
|
|
105
|
+
vendor: "hpe",
|
|
106
|
+
controller_id: p.slot,
|
|
107
|
+
state: p.status,
|
|
108
|
+
degraded_disks: null,
|
|
109
|
+
raw_summary: null,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
async function scrapeArcconf() {
|
|
113
|
+
// arcconf has no JSON mode. Best-effort: detect controllers via
|
|
114
|
+
// `arcconf list` and surface a placeholder state. Real parser
|
|
115
|
+
// for Adaptec lands when a customer surfaces.
|
|
116
|
+
const raw = await run("arcconf", ["list"], 10000);
|
|
117
|
+
if (!raw)
|
|
118
|
+
return [];
|
|
119
|
+
const controllers = [];
|
|
120
|
+
for (const line of raw.split("\n")) {
|
|
121
|
+
const m = line.match(/Controller (\d+):/i);
|
|
122
|
+
if (m) {
|
|
123
|
+
controllers.push({
|
|
124
|
+
vendor: "adaptec",
|
|
125
|
+
controller_id: m[1],
|
|
126
|
+
state: "Unknown",
|
|
127
|
+
degraded_disks: null,
|
|
128
|
+
raw_summary: "arcconf parsing pending — surface a customer with Adaptec hardware",
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return controllers;
|
|
133
|
+
}
|
|
134
|
+
export async function collectHardwareRaid() {
|
|
135
|
+
const hasPerccli = await hasCli("perccli");
|
|
136
|
+
const hasStorcli = await hasCli("storcli");
|
|
137
|
+
const hasSsacli = await hasCli("ssacli");
|
|
138
|
+
const hasArcconf = await hasCli("arcconf");
|
|
139
|
+
if (!hasPerccli && !hasStorcli && !hasSsacli && !hasArcconf)
|
|
140
|
+
return null;
|
|
141
|
+
const controllers = [];
|
|
142
|
+
if (hasPerccli)
|
|
143
|
+
controllers.push(...(await scrapePerccli()));
|
|
144
|
+
if (hasStorcli)
|
|
145
|
+
controllers.push(...(await scrapeStorcli()));
|
|
146
|
+
if (hasSsacli)
|
|
147
|
+
controllers.push(...(await scrapeSsacli()));
|
|
148
|
+
if (hasArcconf)
|
|
149
|
+
controllers.push(...(await scrapeArcconf()));
|
|
150
|
+
return { controllers };
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=hardware-raid.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hardware-raid.js","sourceRoot":"","sources":["../../src/collect/hardware-raid.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { PsiResource, PsiSnapshot } from "../lib/types.js";
|
|
2
|
+
declare function parsePsiLine(line: string): PsiResource | null;
|
|
3
|
+
interface PsiFile {
|
|
4
|
+
some: PsiResource;
|
|
5
|
+
/** Only present for memory + io; cpu has no "full" line in current kernels. */
|
|
6
|
+
full?: PsiResource;
|
|
7
|
+
}
|
|
8
|
+
declare function parsePsiFile(contents: string): PsiFile | null;
|
|
9
|
+
/**
|
|
10
|
+
* Collect PSI for all three resources. Returns null if /proc/pressure/
|
|
11
|
+
* is unavailable (older kernel or PSI disabled). Per resource: returns
|
|
12
|
+
* undefined if that specific file is unreadable (rare; usually all
|
|
13
|
+
* three present or none).
|
|
14
|
+
*/
|
|
15
|
+
export declare function collectPsi(): PsiSnapshot | null;
|
|
16
|
+
export declare const __test_only: {
|
|
17
|
+
parsePsiLine: typeof parsePsiLine;
|
|
18
|
+
parsePsiFile: typeof parsePsiFile;
|
|
19
|
+
};
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// PSI (Pressure Stall Information) collection.
|
|
2
|
+
//
|
|
3
|
+
// Reads /proc/pressure/{cpu,memory,io}. The kernel exposes this on
|
|
4
|
+
// 4.20+ when PSI is compiled in (most modern distros). Older kernels
|
|
5
|
+
// or those built without `CONFIG_PSI=y` return an empty result; the
|
|
6
|
+
// snapshot then omits the `psi` field entirely, and the dashboard
|
|
7
|
+
// alert evaluator's capability gates treat that as `available: false`.
|
|
8
|
+
//
|
|
9
|
+
// Per CC_SPEC_FORGE_FOLLOWUP_C1_C6_ACTIVATION_2026-05-19.md (C2).
|
|
10
|
+
//
|
|
11
|
+
// File format (all three resources share the same shape):
|
|
12
|
+
//
|
|
13
|
+
// some avg10=0.06 avg60=0.04 avg300=0.05 total=12345
|
|
14
|
+
// full avg10=0.00 avg60=0.00 avg300=0.00 total=678
|
|
15
|
+
//
|
|
16
|
+
// "some" = % of time at least one task was stalled on the resource.
|
|
17
|
+
// "full" = % of time ALL non-idle tasks were stalled (only present
|
|
18
|
+
// for memory and io; cpu has only "some" per kernel docs).
|
|
19
|
+
// "total" = cumulative microseconds since boot.
|
|
20
|
+
import { readProcFile } from "../lib/parse.js";
|
|
21
|
+
function parsePsiLine(line) {
|
|
22
|
+
// Expect: "some avg10=N avg60=N avg300=N total=N"
|
|
23
|
+
const parts = line.trim().split(/\s+/);
|
|
24
|
+
if (parts.length < 5)
|
|
25
|
+
return null;
|
|
26
|
+
const out = {};
|
|
27
|
+
for (const part of parts.slice(1)) {
|
|
28
|
+
const [k, v] = part.split("=");
|
|
29
|
+
if (k === "avg10")
|
|
30
|
+
out.avg10 = parseFloat(v);
|
|
31
|
+
else if (k === "avg60")
|
|
32
|
+
out.avg60 = parseFloat(v);
|
|
33
|
+
else if (k === "avg300")
|
|
34
|
+
out.avg300 = parseFloat(v);
|
|
35
|
+
else if (k === "total")
|
|
36
|
+
out.total = parseInt(v, 10);
|
|
37
|
+
}
|
|
38
|
+
if (out.avg10 === undefined ||
|
|
39
|
+
out.avg60 === undefined ||
|
|
40
|
+
out.avg300 === undefined ||
|
|
41
|
+
out.total === undefined) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
function parsePsiFile(contents) {
|
|
47
|
+
let some = null;
|
|
48
|
+
let full = null;
|
|
49
|
+
for (const line of contents.split("\n")) {
|
|
50
|
+
if (line.startsWith("some "))
|
|
51
|
+
some = parsePsiLine(line);
|
|
52
|
+
else if (line.startsWith("full "))
|
|
53
|
+
full = parsePsiLine(line);
|
|
54
|
+
}
|
|
55
|
+
if (!some)
|
|
56
|
+
return null;
|
|
57
|
+
return full ? { some, full } : { some };
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Collect PSI for all three resources. Returns null if /proc/pressure/
|
|
61
|
+
* is unavailable (older kernel or PSI disabled). Per resource: returns
|
|
62
|
+
* undefined if that specific file is unreadable (rare; usually all
|
|
63
|
+
* three present or none).
|
|
64
|
+
*/
|
|
65
|
+
export function collectPsi() {
|
|
66
|
+
const cpu = readProcFile("/proc/pressure/cpu");
|
|
67
|
+
const memory = readProcFile("/proc/pressure/memory");
|
|
68
|
+
const io = readProcFile("/proc/pressure/io");
|
|
69
|
+
if (!cpu && !memory && !io)
|
|
70
|
+
return null;
|
|
71
|
+
const out = {};
|
|
72
|
+
if (cpu) {
|
|
73
|
+
const parsed = parsePsiFile(cpu);
|
|
74
|
+
if (parsed)
|
|
75
|
+
out.cpu = parsed;
|
|
76
|
+
}
|
|
77
|
+
if (memory) {
|
|
78
|
+
const parsed = parsePsiFile(memory);
|
|
79
|
+
if (parsed)
|
|
80
|
+
out.memory = parsed;
|
|
81
|
+
}
|
|
82
|
+
if (io) {
|
|
83
|
+
const parsed = parsePsiFile(io);
|
|
84
|
+
if (parsed)
|
|
85
|
+
out.io = parsed;
|
|
86
|
+
}
|
|
87
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
88
|
+
}
|
|
89
|
+
export const __test_only = { parsePsiLine, parsePsiFile };
|
|
90
|
+
//# sourceMappingURL=psi.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"psi.js","sourceRoot":"","sources":["../../src/collect/psi.ts"],"names":[],"mappings":"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"}
|