@daemonitor/plugins 1.1.4 → 1.1.7
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.
|
@@ -29,14 +29,49 @@ const SYS = {
|
|
|
29
29
|
ifOper: "1.3.6.1.2.1.2.2.1.8", // ifOperStatus (walk): 1=up 2=down
|
|
30
30
|
ifAdmin: "1.3.6.1.2.1.2.2.1.7", // ifAdminStatus (walk): 1=up (enabled)
|
|
31
31
|
};
|
|
32
|
+
// QNAP enterprise MIB (NAS-MIB, 1.3.6.1.4.1.24681.1.2). A full walk of this
|
|
33
|
+
// subtree times out on QTS, so everything here is fetched by exact OID.
|
|
34
|
+
const QNAP = {
|
|
35
|
+
cpuUsage: "1.3.6.1.4.1.24681.1.2.1.0", // systemCPU-Usage e.g. "3.5 %"
|
|
36
|
+
cpuTemp: "1.3.6.1.4.1.24681.1.2.5.0", // cpu-Temperature e.g. "67 C/152 F"
|
|
37
|
+
sysTemp: "1.3.6.1.4.1.24681.1.2.6.0", // systemTemperature
|
|
38
|
+
model: "1.3.6.1.4.1.24681.1.2.12.0", // e.g. "TS-963X"
|
|
39
|
+
hdNumber: "1.3.6.1.4.1.24681.1.2.10.0", // installed disk count
|
|
40
|
+
hdDescr: "1.3.6.1.4.1.24681.1.2.11.1.2.", // + index
|
|
41
|
+
hdTemp: "1.3.6.1.4.1.24681.1.2.11.1.3.",
|
|
42
|
+
hdStatus: "1.3.6.1.4.1.24681.1.2.11.1.4.", // 0 = ready/ok
|
|
43
|
+
hdSmart: "1.3.6.1.4.1.24681.1.2.11.1.7.", // "GOOD" / "Warning"
|
|
44
|
+
};
|
|
32
45
|
function slug(s) {
|
|
33
46
|
return s.replace(/[^a-zA-Z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
34
47
|
}
|
|
48
|
+
// QNAP reports temps as "67 C/152 F" and percentages as "3.5 %" — pull the number.
|
|
49
|
+
function parseC(s) {
|
|
50
|
+
const m = /(-?\d+(?:\.\d+)?)\s*C/.exec(s || "");
|
|
51
|
+
return m ? Math.round(Number(m[1])) : undefined;
|
|
52
|
+
}
|
|
53
|
+
function parseNum(s) {
|
|
54
|
+
const m = /(-?\d+(?:\.\d+)?)/.exec(s || "");
|
|
55
|
+
return m ? Number(m[1]) : undefined;
|
|
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
|
+
}
|
|
35
68
|
async function snmpGet(t, oid, timeout = 8000) {
|
|
36
69
|
try {
|
|
37
70
|
const { stdout } = await pexec("snmpget", ["-v", t.version || "2c", "-c", t.community || "public", "-Ovq", "-t", "2", "-r", "1", t.ip, oid], { timeout });
|
|
38
|
-
const
|
|
39
|
-
|
|
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);
|
|
40
75
|
}
|
|
41
76
|
catch {
|
|
42
77
|
return null;
|
|
@@ -45,7 +80,7 @@ async function snmpGet(t, oid, timeout = 8000) {
|
|
|
45
80
|
async function snmpWalk(t, oid, timeout = 10000) {
|
|
46
81
|
try {
|
|
47
82
|
const { stdout } = await pexec("snmpwalk", ["-v", t.version || "2c", "-c", t.community || "public", "-Oqv", "-t", "2", "-r", "1", t.ip, oid], { timeout });
|
|
48
|
-
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);
|
|
49
84
|
}
|
|
50
85
|
catch {
|
|
51
86
|
return [];
|
|
@@ -133,6 +168,85 @@ async function collectDevice(t) {
|
|
|
133
168
|
portsTotal,
|
|
134
169
|
};
|
|
135
170
|
}
|
|
171
|
+
// Walk that keeps each row's index (numeric OID suffix) — needed to resolve the
|
|
172
|
+
// hrStorage table entries for the data volumes.
|
|
173
|
+
async function snmpWalkIndexed(t, oid, timeout = 10000) {
|
|
174
|
+
try {
|
|
175
|
+
const { stdout } = await pexec("snmpwalk", ["-v", t.version || "2c", "-c", t.community || "public", "-On", "-t", "2", "-r", "1", t.ip, oid], { timeout });
|
|
176
|
+
return stdout.split("\n").map((l) => {
|
|
177
|
+
const m = /^(\S+)\s*=\s*[^:]+:\s*(.*)$/.exec(l.trim());
|
|
178
|
+
if (!m)
|
|
179
|
+
return null;
|
|
180
|
+
// snmpwalk -On emits a leading dot (".1.3.6…") the bare OID lacks.
|
|
181
|
+
const clean = m[1].replace(/^\./, "");
|
|
182
|
+
const idx = clean.startsWith(oid) ? clean.slice(oid.length).replace(/^\./, "") : clean;
|
|
183
|
+
return { idx, val: m[2].trim().replace(/^"|"$/g, "") };
|
|
184
|
+
}).filter(Boolean);
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return [];
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// QNAP NAS: CPU load, cpu/system temps, per-disk health + temp, and data-volume
|
|
191
|
+
// fullness. Uses the QNAP enterprise MIB by exact OID (walk times out) plus
|
|
192
|
+
// HOST-RESOURCES for CPU load and volume sizes.
|
|
193
|
+
async function collectQnap(t) {
|
|
194
|
+
const tempWarn = t.tempWarnC ?? 70;
|
|
195
|
+
const volWarn = t.volumeWarnPct ?? 90;
|
|
196
|
+
const [model, cpuTemp, sysTemp, hdNumRaw, cpuLoads, storDescrs] = await Promise.all([
|
|
197
|
+
snmpGet(t, QNAP.model),
|
|
198
|
+
snmpGet(t, QNAP.cpuTemp),
|
|
199
|
+
snmpGet(t, QNAP.sysTemp),
|
|
200
|
+
snmpGet(t, QNAP.hdNumber),
|
|
201
|
+
snmpWalk(t, "1.3.6.1.2.1.25.3.3.1.2"), // hrProcessorLoad (per core)
|
|
202
|
+
snmpWalkIndexed(t, "1.3.6.1.2.1.25.2.3.1.3"), // hrStorageDescr
|
|
203
|
+
]);
|
|
204
|
+
if (model == null && cpuTemp == null && !cpuLoads.length) {
|
|
205
|
+
return { name: t.name, ip: t.ip, kind: "qnap", reachable: false };
|
|
206
|
+
}
|
|
207
|
+
// CPU: average per-core load.
|
|
208
|
+
const cores = cpuLoads.map(Number).filter((n) => !isNaN(n));
|
|
209
|
+
const cpuPct = cores.length ? Math.round(cores.reduce((a, b) => a + b, 0) / cores.length) : undefined;
|
|
210
|
+
// Disks — iterate 1..hdNumber (bounded).
|
|
211
|
+
const hdN = Math.min(Number(hdNumRaw) || 0, 24);
|
|
212
|
+
const disks = [];
|
|
213
|
+
for (let i = 1; i <= hdN; i++) {
|
|
214
|
+
const [st, tp, sm, ds] = await Promise.all([
|
|
215
|
+
snmpGet(t, QNAP.hdStatus + i), snmpGet(t, QNAP.hdTemp + i), snmpGet(t, QNAP.hdSmart + i), snmpGet(t, QNAP.hdDescr + i),
|
|
216
|
+
]);
|
|
217
|
+
const status = st != null ? Number(st) : undefined;
|
|
218
|
+
const tempC = parseC(tp);
|
|
219
|
+
// Empty bay: no readable temp and a non-ready (negative/absent, e.g. -5)
|
|
220
|
+
// status. Skip it — an empty slot isn't a failed disk.
|
|
221
|
+
if (tempC == null && (status == null || status < 0))
|
|
222
|
+
continue;
|
|
223
|
+
disks.push({ slot: i, descr: ds || undefined, tempC, status, smart: sm && sm !== "--" ? sm : undefined });
|
|
224
|
+
}
|
|
225
|
+
// A present disk is bad only on a real error status, an explicitly-bad SMART
|
|
226
|
+
// verdict, or overheating — not a blank "--" or an empty bay.
|
|
227
|
+
const badDisks = disks.filter((d) => (d.status != null && d.status !== 0) ||
|
|
228
|
+
(typeof d.smart === "string" && /warn|abnormal|fail|error|bad|caution/i.test(d.smart)) ||
|
|
229
|
+
(d.tempC != null && d.tempC >= tempWarn));
|
|
230
|
+
// Volumes — the CACHEDEVn_DATA mounts from hrStorage.
|
|
231
|
+
const volumes = [];
|
|
232
|
+
for (const row of storDescrs.filter((r) => /CACHEDEV\d+_DATA$/.test(r.val))) {
|
|
233
|
+
const [units, size, used] = await Promise.all([
|
|
234
|
+
snmpGet(t, `1.3.6.1.2.1.25.2.3.1.4.${row.idx}`), snmpGet(t, `1.3.6.1.2.1.25.2.3.1.5.${row.idx}`), snmpGet(t, `1.3.6.1.2.1.25.2.3.1.6.${row.idx}`),
|
|
235
|
+
]);
|
|
236
|
+
const u = parseNum(units) || 1, sz = parseNum(size) || 0, us = parseNum(used) || 0;
|
|
237
|
+
if (sz > 0)
|
|
238
|
+
volumes.push({ mount: row.val, totalGB: Math.round((sz * u) / 1e9), usedGB: Math.round((us * u) / 1e9), pct: Math.round((us / sz) * 100) });
|
|
239
|
+
}
|
|
240
|
+
const fullVols = volumes.filter((v) => v.pct >= volWarn);
|
|
241
|
+
return {
|
|
242
|
+
name: t.name, ip: t.ip, kind: "qnap", reachable: true,
|
|
243
|
+
model: model || undefined,
|
|
244
|
+
cpuPct, cpuTempC: parseC(cpuTemp), sysTempC: parseC(sysTemp),
|
|
245
|
+
disks, badDisks: badDisks.map((d) => ({ slot: d.slot, status: d.status, smart: d.smart, tempC: d.tempC })),
|
|
246
|
+
volumes, fullVolumes: fullVols,
|
|
247
|
+
tempWarnC: tempWarn, volumeWarnPct: volWarn,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
136
250
|
export function createSnmpPlugin() {
|
|
137
251
|
let refreshTimer = null;
|
|
138
252
|
const targetsOf = (plugin) => {
|
|
@@ -144,7 +258,9 @@ export function createSnmpPlugin() {
|
|
|
144
258
|
const refreshFn = async (plugin) => {
|
|
145
259
|
await Promise.all(targetsOf(plugin).map(async (t) => {
|
|
146
260
|
try {
|
|
147
|
-
const data = t.kind === "
|
|
261
|
+
const data = t.kind === "qnap" ? await collectQnap(t)
|
|
262
|
+
: t.kind === "printer" || !t.kind ? await collectPrinter(t)
|
|
263
|
+
: await collectDevice(t);
|
|
148
264
|
await plugin.send(data, `snmp-${slug(t.name || t.ip)}`);
|
|
149
265
|
}
|
|
150
266
|
catch (e) {
|