@daemonitor/plugins 1.1.4 → 1.1.5

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,9 +29,31 @@ 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
+ }
35
57
  async function snmpGet(t, oid, timeout = 8000) {
36
58
  try {
37
59
  const { stdout } = await pexec("snmpget", ["-v", t.version || "2c", "-c", t.community || "public", "-Ovq", "-t", "2", "-r", "1", t.ip, oid], { timeout });
@@ -133,6 +155,75 @@ async function collectDevice(t) {
133
155
  portsTotal,
134
156
  };
135
157
  }
158
+ // Walk that keeps each row's index (numeric OID suffix) — needed to resolve the
159
+ // hrStorage table entries for the data volumes.
160
+ async function snmpWalkIndexed(t, oid, timeout = 10000) {
161
+ try {
162
+ const { stdout } = await pexec("snmpwalk", ["-v", t.version || "2c", "-c", t.community || "public", "-On", "-t", "2", "-r", "1", t.ip, oid], { timeout });
163
+ return stdout.split("\n").map((l) => {
164
+ const m = /^(\S+)\s*=\s*[^:]+:\s*(.*)$/.exec(l.trim());
165
+ if (!m)
166
+ return null;
167
+ const idx = m[1].startsWith(oid) ? m[1].slice(oid.length).replace(/^\./, "") : m[1];
168
+ return { idx, val: m[2].trim().replace(/^"|"$/g, "") };
169
+ }).filter(Boolean);
170
+ }
171
+ catch {
172
+ return [];
173
+ }
174
+ }
175
+ // QNAP NAS: CPU load, cpu/system temps, per-disk health + temp, and data-volume
176
+ // fullness. Uses the QNAP enterprise MIB by exact OID (walk times out) plus
177
+ // HOST-RESOURCES for CPU load and volume sizes.
178
+ async function collectQnap(t) {
179
+ const tempWarn = t.tempWarnC ?? 70;
180
+ const volWarn = t.volumeWarnPct ?? 90;
181
+ const [model, cpuTemp, sysTemp, hdNumRaw, cpuLoads, storDescrs] = await Promise.all([
182
+ snmpGet(t, QNAP.model),
183
+ snmpGet(t, QNAP.cpuTemp),
184
+ snmpGet(t, QNAP.sysTemp),
185
+ snmpGet(t, QNAP.hdNumber),
186
+ snmpWalk(t, "1.3.6.1.2.1.25.3.3.1.2"), // hrProcessorLoad (per core)
187
+ snmpWalkIndexed(t, "1.3.6.1.2.1.25.2.3.1.3"), // hrStorageDescr
188
+ ]);
189
+ if (model == null && cpuTemp == null && !cpuLoads.length) {
190
+ return { name: t.name, ip: t.ip, kind: "qnap", reachable: false };
191
+ }
192
+ // CPU: average per-core load.
193
+ const cores = cpuLoads.map(Number).filter((n) => !isNaN(n));
194
+ const cpuPct = cores.length ? Math.round(cores.reduce((a, b) => a + b, 0) / cores.length) : undefined;
195
+ // Disks — iterate 1..hdNumber (bounded).
196
+ const hdN = Math.min(Number(hdNumRaw) || 0, 24);
197
+ const disks = [];
198
+ for (let i = 1; i <= hdN; i++) {
199
+ const [st, tp, sm, ds] = await Promise.all([
200
+ snmpGet(t, QNAP.hdStatus + i), snmpGet(t, QNAP.hdTemp + i), snmpGet(t, QNAP.hdSmart + i), snmpGet(t, QNAP.hdDescr + i),
201
+ ]);
202
+ if (st == null && tp == null)
203
+ continue;
204
+ disks.push({ slot: i, descr: ds || undefined, tempC: parseC(tp), status: st != null ? Number(st) : undefined, smart: sm || undefined });
205
+ }
206
+ const badDisks = disks.filter((d) => (d.status != null && d.status !== 0) || (d.smart && !/good|normal/i.test(d.smart)) || (d.tempC != null && d.tempC >= tempWarn));
207
+ // Volumes — the CACHEDEVn_DATA mounts from hrStorage.
208
+ const volumes = [];
209
+ for (const row of storDescrs.filter((r) => /CACHEDEV\d+_DATA$/.test(r.val))) {
210
+ const [units, size, used] = await Promise.all([
211
+ 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}`),
212
+ ]);
213
+ const u = parseNum(units) || 1, sz = parseNum(size) || 0, us = parseNum(used) || 0;
214
+ if (sz > 0)
215
+ volumes.push({ mount: row.val, totalGB: Math.round((sz * u) / 1e9), usedGB: Math.round((us * u) / 1e9), pct: Math.round((us / sz) * 100) });
216
+ }
217
+ const fullVols = volumes.filter((v) => v.pct >= volWarn);
218
+ return {
219
+ name: t.name, ip: t.ip, kind: "qnap", reachable: true,
220
+ model: model || undefined,
221
+ cpuPct, cpuTempC: parseC(cpuTemp), sysTempC: parseC(sysTemp),
222
+ disks, badDisks: badDisks.map((d) => ({ slot: d.slot, status: d.status, smart: d.smart, tempC: d.tempC })),
223
+ volumes, fullVolumes: fullVols,
224
+ tempWarnC: tempWarn, volumeWarnPct: volWarn,
225
+ };
226
+ }
136
227
  export function createSnmpPlugin() {
137
228
  let refreshTimer = null;
138
229
  const targetsOf = (plugin) => {
@@ -144,7 +235,9 @@ export function createSnmpPlugin() {
144
235
  const refreshFn = async (plugin) => {
145
236
  await Promise.all(targetsOf(plugin).map(async (t) => {
146
237
  try {
147
- const data = t.kind === "printer" || !t.kind ? await collectPrinter(t) : await collectDevice(t);
238
+ const data = t.kind === "qnap" ? await collectQnap(t)
239
+ : t.kind === "printer" || !t.kind ? await collectPrinter(t)
240
+ : await collectDevice(t);
148
241
  await plugin.send(data, `snmp-${slug(t.name || t.ip)}`);
149
242
  }
150
243
  catch (e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daemonitor/plugins",
3
- "version": "1.1.4",
3
+ "version": "1.1.5",
4
4
  "description": "daemonitor plugins",
5
5
  "types": "types/index.d.ts",
6
6
  "main": "dist/index.js",