@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
- const [model, cpuTemp, sysTemp, hdNumRaw, cpuLoads, storDescrs, memTotal, memFree, fanSpeeds] = await Promise.all([
208
- snmpGet(t, QNAP.model),
209
- snmpGet(t, QNAP.cpuTemp),
210
- snmpGet(t, QNAP.sysTemp),
211
- snmpGet(t, QNAP.hdNumber),
212
- snmpWalk(t, "1.3.6.1.2.1.25.3.3.1.2"), // hrProcessorLoad (per core)
213
- snmpWalkIndexed(t, "1.3.6.1.2.1.25.2.3.1.3"), // hrStorageDescr
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 && !cpuLoads.length) {
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 — iterate 1..hdNumber (bounded). Presence is judged by MODEL/CAPACITY:
231
- // an empty bay reports model "--", but a FAILING disk can report an unreadable
232
- // temp ("-- C/-- F") + negative status yet still advertise a real model. The old
233
- // temp+status heuristic silently skipped those bad disks (e.g. an HDD in SMART
234
- // "Warning" with status -9) — so the NAS looked healthy while QTS flagged it.
235
- const hdN = Math.min(Number(hdNumRaw) || 0, 24);
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 (let i = 1; i <= hdN; i++) {
238
- const [st, tp, sm, ds, md, cap] = await Promise.all([
239
- snmpGet(t, QNAP.hdStatus + i), snmpGet(t, QNAP.hdTemp + i), snmpGet(t, QNAP.hdSmart + i),
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 = st != null ? Number(st) : undefined;
248
- const tempC = parseC(tp);
249
- disks.push({ slot: i, descr: ds || undefined, model, capacity, tempC, status, smart: sm && sm !== "--" ? sm : undefined });
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daemonitor/plugins",
3
- "version": "1.1.9",
3
+ "version": "1.1.11",
4
4
  "description": "daemonitor plugins",
5
5
  "types": "types/index.d.ts",
6
6
  "main": "dist/index.js",