@daemonitor/plugins 1.1.3 → 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.
|
@@ -21,9 +21,39 @@ const OID = {
|
|
|
21
21
|
supplyLevel: "1.3.6.1.2.1.43.11.1.1.9", // prtMarkerSuppliesLevel (walk)
|
|
22
22
|
};
|
|
23
23
|
const PRINTER_STATUS = { "1": "other", "2": "unknown", "3": "idle", "4": "printing", "5": "warmup" };
|
|
24
|
+
// Generic SYSTEM / IF-MIB OIDs (switches, APs, anything SNMP-capable).
|
|
25
|
+
const SYS = {
|
|
26
|
+
descr: "1.3.6.1.2.1.1.1.0", // sysDescr
|
|
27
|
+
uptime: "1.3.6.1.2.1.1.3.0", // sysUpTime (timeticks, 1/100s)
|
|
28
|
+
name: "1.3.6.1.2.1.1.5.0", // sysName
|
|
29
|
+
ifOper: "1.3.6.1.2.1.2.2.1.8", // ifOperStatus (walk): 1=up 2=down
|
|
30
|
+
ifAdmin: "1.3.6.1.2.1.2.2.1.7", // ifAdminStatus (walk): 1=up (enabled)
|
|
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
|
+
};
|
|
24
45
|
function slug(s) {
|
|
25
46
|
return s.replace(/[^a-zA-Z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
26
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
|
+
}
|
|
27
57
|
async function snmpGet(t, oid, timeout = 8000) {
|
|
28
58
|
try {
|
|
29
59
|
const { stdout } = await pexec("snmpget", ["-v", t.version || "2c", "-c", t.community || "public", "-Ovq", "-t", "2", "-r", "1", t.ip, oid], { timeout });
|
|
@@ -86,6 +116,114 @@ async function collectPrinter(t) {
|
|
|
86
116
|
warnPct,
|
|
87
117
|
};
|
|
88
118
|
}
|
|
119
|
+
// Generic SNMP device (switch/AP/…): identity, uptime, and enabled-port link
|
|
120
|
+
// state so a down uplink or dead switch surfaces. Reachability via SNMP is more
|
|
121
|
+
// reliable than ICMP — many devices drop ping but answer SNMP.
|
|
122
|
+
async function collectDevice(t) {
|
|
123
|
+
const [descr, name, uptimeRaw, oper, admin] = await Promise.all([
|
|
124
|
+
snmpGet(t, SYS.descr),
|
|
125
|
+
snmpGet(t, SYS.name),
|
|
126
|
+
snmpGet(t, SYS.uptime),
|
|
127
|
+
snmpWalk(t, SYS.ifOper),
|
|
128
|
+
snmpWalk(t, SYS.ifAdmin),
|
|
129
|
+
]);
|
|
130
|
+
if (descr == null && name == null && !oper.length) {
|
|
131
|
+
return { name: t.name, ip: t.ip, kind: t.kind || "device", reachable: false };
|
|
132
|
+
}
|
|
133
|
+
// Only count ADMIN-enabled ports; an enabled port that's operationally down is
|
|
134
|
+
// a real link-down (ignore the many disabled ports on a big switch).
|
|
135
|
+
let portsTotal = 0;
|
|
136
|
+
let portsUp = 0;
|
|
137
|
+
for (let i = 0; i < oper.length; i++) {
|
|
138
|
+
if (Number(admin[i]) === 1) {
|
|
139
|
+
portsTotal++;
|
|
140
|
+
if (Number(oper[i]) === 1)
|
|
141
|
+
portsUp++;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// sysUpTime timeticks → seconds
|
|
145
|
+
const upSecs = uptimeRaw != null ? Math.round((Number(String(uptimeRaw).replace(/[^0-9]/g, "")) || 0) / 100) : undefined;
|
|
146
|
+
return {
|
|
147
|
+
name: t.name,
|
|
148
|
+
ip: t.ip,
|
|
149
|
+
kind: t.kind || "device",
|
|
150
|
+
reachable: true,
|
|
151
|
+
sysDescr: descr || undefined,
|
|
152
|
+
sysName: name || undefined,
|
|
153
|
+
uptimeSecs: upSecs,
|
|
154
|
+
portsUp,
|
|
155
|
+
portsTotal,
|
|
156
|
+
};
|
|
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
|
+
}
|
|
89
227
|
export function createSnmpPlugin() {
|
|
90
228
|
let refreshTimer = null;
|
|
91
229
|
const targetsOf = (plugin) => {
|
|
@@ -97,7 +235,9 @@ export function createSnmpPlugin() {
|
|
|
97
235
|
const refreshFn = async (plugin) => {
|
|
98
236
|
await Promise.all(targetsOf(plugin).map(async (t) => {
|
|
99
237
|
try {
|
|
100
|
-
const data = t.kind === "
|
|
238
|
+
const data = t.kind === "qnap" ? await collectQnap(t)
|
|
239
|
+
: t.kind === "printer" || !t.kind ? await collectPrinter(t)
|
|
240
|
+
: await collectDevice(t);
|
|
101
241
|
await plugin.send(data, `snmp-${slug(t.name || t.ip)}`);
|
|
102
242
|
}
|
|
103
243
|
catch (e) {
|