@daemonitor/plugins 1.1.9 → 1.1.11
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.
|
@@ -204,20 +204,20 @@ async function snmpWalkIndexed(t, oid, timeout = 10000) {
|
|
|
204
204
|
async function collectQnap(t) {
|
|
205
205
|
const tempWarn = t.tempWarnC ?? 70;
|
|
206
206
|
const volWarn = t.volumeWarnPct ?? 90;
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
snmpGet(t, QNAP.memTotal),
|
|
215
|
-
snmpGet(t, QNAP.memFree),
|
|
216
|
-
snmpWalk(t, "1.3.6.1.4.1.24681.1.2.15.1.3"), // SystemFanSpeed table
|
|
207
|
+
// The light scalar gets can run together, but the WALKS must be SERIALIZED:
|
|
208
|
+
// QNAP's SNMP agent silently drops rows (and whole tables) under concurrent
|
|
209
|
+
// sessions, producing partial reads that flip a failing NAS back to a false
|
|
210
|
+
// "healthy". 5-min cadence — sequential latency is fine.
|
|
211
|
+
const [model, cpuTemp, sysTemp, memTotal, memFree] = await Promise.all([
|
|
212
|
+
snmpGet(t, QNAP.model), snmpGet(t, QNAP.cpuTemp), snmpGet(t, QNAP.sysTemp),
|
|
213
|
+
snmpGet(t, QNAP.memTotal), snmpGet(t, QNAP.memFree),
|
|
217
214
|
]);
|
|
218
|
-
if (model == null && cpuTemp == null
|
|
215
|
+
if (model == null && cpuTemp == null) {
|
|
219
216
|
return { name: t.name, ip: t.ip, kind: "qnap", reachable: false };
|
|
220
217
|
}
|
|
218
|
+
const cpuLoads = await snmpWalk(t, "1.3.6.1.2.1.25.3.3.1.2"); // hrProcessorLoad (per core)
|
|
219
|
+
const storDescrs = await snmpWalkIndexed(t, "1.3.6.1.2.1.25.2.3.1.3"); // hrStorageDescr
|
|
220
|
+
const fanSpeeds = await snmpWalk(t, "1.3.6.1.4.1.24681.1.2.15.1.3"); // SystemFanSpeed table
|
|
221
221
|
// CPU: average per-core load.
|
|
222
222
|
const cores = cpuLoads.map(Number).filter((n) => !isNaN(n));
|
|
223
223
|
const cpuPct = cores.length ? Math.round(cores.reduce((a, b) => a + b, 0) / cores.length) : undefined;
|
|
@@ -227,26 +227,46 @@ async function collectQnap(t) {
|
|
|
227
227
|
? Math.round(((memTotalMB - memFreeMB) / memTotalMB) * 100) : undefined;
|
|
228
228
|
// Fans — each entry looks like "1835 RPM".
|
|
229
229
|
const fans = fanSpeeds.map((f) => parseNum(f)).filter((n) => n != null && n > 0);
|
|
230
|
-
// Disks —
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
// "
|
|
235
|
-
|
|
230
|
+
// Disks — walk each column of the disk table ONCE (6 small walks) rather than
|
|
231
|
+
// 6 gets × N bays. QNAP's SNMP agent is flaky under concurrent-get bursts and
|
|
232
|
+
// silently drops rows (partial reads that hid disks); a per-column walk is a
|
|
233
|
+
// single session each — far gentler and returns every populated row atomically.
|
|
234
|
+
// Presence keys on MODEL/CAPACITY ("--" = empty bay), so a FAILING disk
|
|
235
|
+
// (unreadable temp, negative status) is still reported, not mistaken for empty.
|
|
236
|
+
const DT = "1.3.6.1.4.1.24681.1.2.11.1."; // disk table columns: 2=descr 3=temp 4=status 5=model 6=capacity 7=smart
|
|
237
|
+
// Serialized (not Promise.all) — concurrent column walks are what make the
|
|
238
|
+
// agent drop rows.
|
|
239
|
+
const wStatus = await snmpWalkIndexed(t, DT + "4");
|
|
240
|
+
const wTemp = await snmpWalkIndexed(t, DT + "3");
|
|
241
|
+
const wSmart = await snmpWalkIndexed(t, DT + "7");
|
|
242
|
+
const wDescr = await snmpWalkIndexed(t, DT + "2");
|
|
243
|
+
const wModel = await snmpWalkIndexed(t, DT + "5");
|
|
244
|
+
const wCap = await snmpWalkIndexed(t, DT + "6");
|
|
245
|
+
const col = (rows) => {
|
|
246
|
+
const m = {};
|
|
247
|
+
for (const r of rows)
|
|
248
|
+
m[r.idx] = r.val;
|
|
249
|
+
return m;
|
|
250
|
+
};
|
|
251
|
+
const cSt = col(wStatus), cTp = col(wTemp), cSm = col(wSmart), cDs = col(wDescr), cMd = col(wModel), cCap = col(wCap);
|
|
252
|
+
const slots = (Object.keys(cMd).length ? Object.keys(cMd) : Object.keys(cDs))
|
|
253
|
+
.sort((a, b) => Number(a) - Number(b));
|
|
236
254
|
const disks = [];
|
|
237
|
-
for (
|
|
238
|
-
const [
|
|
239
|
-
|
|
240
|
-
snmpGet(t, QNAP.hdDescr + i), snmpGet(t, QNAP.hdModel + i), snmpGet(t, QNAP.hdCapacity + i),
|
|
241
|
-
]);
|
|
242
|
-
const model = md && md !== "--" ? md : undefined;
|
|
243
|
-
const capacity = cap && cap !== "--" ? cap : undefined;
|
|
255
|
+
for (const i of slots) {
|
|
256
|
+
const model = cMd[i] && cMd[i] !== "--" ? cMd[i] : undefined;
|
|
257
|
+
const capacity = cCap[i] && cCap[i] !== "--" ? cCap[i] : undefined;
|
|
244
258
|
// Only a truly empty bay (no model AND no capacity) is skipped.
|
|
245
259
|
if (!model && !capacity)
|
|
246
260
|
continue;
|
|
247
|
-
const status =
|
|
248
|
-
const tempC = parseC(
|
|
249
|
-
disks.push({ slot: i, descr:
|
|
261
|
+
const status = cSt[i] != null ? Number(cSt[i]) : undefined;
|
|
262
|
+
const tempC = parseC(cTp[i] || null);
|
|
263
|
+
disks.push({ slot: Number(i), descr: cDs[i] || undefined, model, capacity, tempC, status, smart: cSm[i] && cSm[i] !== "--" ? cSm[i] : undefined });
|
|
264
|
+
}
|
|
265
|
+
// A reachable QNAP always has ≥1 disk. Zero means the disk-table walks came
|
|
266
|
+
// back empty (agent timed out) — treat as an INCOMPLETE read, not a clean
|
|
267
|
+
// bill of health, so a real disk error isn't falsely cleared to green.
|
|
268
|
+
if (!disks.length) {
|
|
269
|
+
return { name: t.name, ip: t.ip, kind: "qnap", reachable: true, error: "SNMP read incomplete — disk table empty" };
|
|
250
270
|
}
|
|
251
271
|
// A present disk is bad only on a real error status, an explicitly-bad SMART
|
|
252
272
|
// verdict, or overheating — not a blank "--" or an empty bay.
|