@daemonitor/plugins 1.1.5 → 1.1.8
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.
|
@@ -54,11 +54,24 @@ function parseNum(s) {
|
|
|
54
54
|
const m = /(-?\d+(?:\.\d+)?)/.exec(s || "");
|
|
55
55
|
return m ? Number(m[1]) : undefined;
|
|
56
56
|
}
|
|
57
|
+
// net-snmp renders octet strings that contain a NUL/non-printable byte (e.g. HP's
|
|
58
|
+
// NUL-terminated "Black Cartridge 81A HP CF281A\0") as space-separated hex.
|
|
59
|
+
// Decode that back to text, and strip control chars from any value.
|
|
60
|
+
function decodeSnmp(s) {
|
|
61
|
+
const t = s.trim();
|
|
62
|
+
if (/^([0-9A-Fa-f]{2})(\s+[0-9A-Fa-f]{2})+\s*$/.test(t)) {
|
|
63
|
+
const bytes = t.split(/\s+/).map((h) => parseInt(h, 16));
|
|
64
|
+
return Buffer.from(bytes).toString("latin1").replace(/[\x00-\x1f\x7f]+/g, "").trim();
|
|
65
|
+
}
|
|
66
|
+
return t.replace(/[\x00-\x1f\x7f]+/g, "").trim();
|
|
67
|
+
}
|
|
57
68
|
async function snmpGet(t, oid, timeout = 8000) {
|
|
58
69
|
try {
|
|
59
70
|
const { stdout } = await pexec("snmpget", ["-v", t.version || "2c", "-c", t.community || "public", "-Ovq", "-t", "2", "-r", "1", t.ip, oid], { timeout });
|
|
60
|
-
const
|
|
61
|
-
|
|
71
|
+
const raw = stdout.trim().replace(/^"|"$/g, "");
|
|
72
|
+
if (!raw || /No Such|no response|Timeout/i.test(raw))
|
|
73
|
+
return null;
|
|
74
|
+
return decodeSnmp(raw);
|
|
62
75
|
}
|
|
63
76
|
catch {
|
|
64
77
|
return null;
|
|
@@ -67,7 +80,7 @@ async function snmpGet(t, oid, timeout = 8000) {
|
|
|
67
80
|
async function snmpWalk(t, oid, timeout = 10000) {
|
|
68
81
|
try {
|
|
69
82
|
const { stdout } = await pexec("snmpwalk", ["-v", t.version || "2c", "-c", t.community || "public", "-Oqv", "-t", "2", "-r", "1", t.ip, oid], { timeout });
|
|
70
|
-
return stdout.split("\n").map((l) => l.trim().replace(/^"|"$/g, "")).filter(Boolean);
|
|
83
|
+
return stdout.split("\n").map((l) => decodeSnmp(l.trim().replace(/^"|"$/g, ""))).filter(Boolean);
|
|
71
84
|
}
|
|
72
85
|
catch {
|
|
73
86
|
return [];
|
|
@@ -75,32 +88,38 @@ async function snmpWalk(t, oid, timeout = 10000) {
|
|
|
75
88
|
}
|
|
76
89
|
async function collectPrinter(t) {
|
|
77
90
|
const warnPct = t.tonerWarnPct ?? 10;
|
|
78
|
-
|
|
91
|
+
// Walk LEVELS (clean numeric values, one per row) to get the supply indices,
|
|
92
|
+
// then fetch each supply's description/max by that index — a single get whose
|
|
93
|
+
// (possibly multi-line hex) value decodeSnmp handles, so a long description
|
|
94
|
+
// can't be split into phantom supplies and misalign the levels.
|
|
95
|
+
const [model, serial, pagesRaw, statusRaw, levelRows] = await Promise.all([
|
|
79
96
|
snmpGet(t, OID.model),
|
|
80
97
|
snmpGet(t, OID.serial),
|
|
81
98
|
snmpGet(t, OID.pages),
|
|
82
99
|
snmpGet(t, OID.status),
|
|
83
|
-
|
|
84
|
-
snmpWalk(t, OID.supplyMax),
|
|
85
|
-
snmpWalk(t, OID.supplyLevel),
|
|
100
|
+
snmpWalkIndexed(t, OID.supplyLevel),
|
|
86
101
|
]);
|
|
87
102
|
// No response at all → the printer is unreachable / SNMP off.
|
|
88
|
-
if (model == null && serial == null && pagesRaw == null && !
|
|
103
|
+
if (model == null && serial == null && pagesRaw == null && !levelRows.length) {
|
|
89
104
|
return { name: t.name, ip: t.ip, kind: "printer", reachable: false };
|
|
90
105
|
}
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
const supplies =
|
|
95
|
-
const
|
|
96
|
-
|
|
106
|
+
// prtMarkerSuppliesLevel semantics: -2 = unknown, -3 = "some remaining" (a
|
|
107
|
+
// level the device won't quantify); else a count out of MaxCapacity (which can
|
|
108
|
+
// itself be -2 = unknown).
|
|
109
|
+
const supplies = await Promise.all(levelRows.map(async (row) => {
|
|
110
|
+
const [descRaw, maxRaw] = await Promise.all([
|
|
111
|
+
snmpGet(t, `${OID.supplyDesc}.${row.idx}`),
|
|
112
|
+
snmpGet(t, `${OID.supplyMax}.${row.idx}`),
|
|
113
|
+
]);
|
|
114
|
+
const lvl = Number(row.val);
|
|
115
|
+
const max = Number(maxRaw);
|
|
97
116
|
let pct = null;
|
|
98
117
|
if (lvl === -3)
|
|
99
|
-
pct = 100;
|
|
118
|
+
pct = 100;
|
|
100
119
|
else if (lvl >= 0 && max > 0)
|
|
101
120
|
pct = Math.round((lvl / max) * 100);
|
|
102
|
-
return { name:
|
|
103
|
-
});
|
|
121
|
+
return { name: descRaw || "supply", level: lvl, max, pct };
|
|
122
|
+
}));
|
|
104
123
|
const low = supplies.filter((s) => s.pct != null && s.pct <= warnPct);
|
|
105
124
|
return {
|
|
106
125
|
name: t.name,
|
|
@@ -164,7 +183,9 @@ async function snmpWalkIndexed(t, oid, timeout = 10000) {
|
|
|
164
183
|
const m = /^(\S+)\s*=\s*[^:]+:\s*(.*)$/.exec(l.trim());
|
|
165
184
|
if (!m)
|
|
166
185
|
return null;
|
|
167
|
-
|
|
186
|
+
// snmpwalk -On emits a leading dot (".1.3.6…") the bare OID lacks.
|
|
187
|
+
const clean = m[1].replace(/^\./, "");
|
|
188
|
+
const idx = clean.startsWith(oid) ? clean.slice(oid.length).replace(/^\./, "") : clean;
|
|
168
189
|
return { idx, val: m[2].trim().replace(/^"|"$/g, "") };
|
|
169
190
|
}).filter(Boolean);
|
|
170
191
|
}
|
|
@@ -199,11 +220,19 @@ async function collectQnap(t) {
|
|
|
199
220
|
const [st, tp, sm, ds] = await Promise.all([
|
|
200
221
|
snmpGet(t, QNAP.hdStatus + i), snmpGet(t, QNAP.hdTemp + i), snmpGet(t, QNAP.hdSmart + i), snmpGet(t, QNAP.hdDescr + i),
|
|
201
222
|
]);
|
|
202
|
-
|
|
223
|
+
const status = st != null ? Number(st) : undefined;
|
|
224
|
+
const tempC = parseC(tp);
|
|
225
|
+
// Empty bay: no readable temp and a non-ready (negative/absent, e.g. -5)
|
|
226
|
+
// status. Skip it — an empty slot isn't a failed disk.
|
|
227
|
+
if (tempC == null && (status == null || status < 0))
|
|
203
228
|
continue;
|
|
204
|
-
disks.push({ slot: i, descr: ds || undefined, tempC
|
|
229
|
+
disks.push({ slot: i, descr: ds || undefined, tempC, status, smart: sm && sm !== "--" ? sm : undefined });
|
|
205
230
|
}
|
|
206
|
-
|
|
231
|
+
// A present disk is bad only on a real error status, an explicitly-bad SMART
|
|
232
|
+
// verdict, or overheating — not a blank "--" or an empty bay.
|
|
233
|
+
const badDisks = disks.filter((d) => (d.status != null && d.status !== 0) ||
|
|
234
|
+
(typeof d.smart === "string" && /warn|abnormal|fail|error|bad|caution/i.test(d.smart)) ||
|
|
235
|
+
(d.tempC != null && d.tempC >= tempWarn));
|
|
207
236
|
// Volumes — the CACHEDEVn_DATA mounts from hrStorage.
|
|
208
237
|
const volumes = [];
|
|
209
238
|
for (const row of storDescrs.filter((r) => /CACHEDEV\d+_DATA$/.test(r.val))) {
|