@daemonitor/plugins 1.1.2 → 1.1.4
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.
package/dist/plugins/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import { createWebsitePlugin } from "./monitoring/WebsitePlugin";
|
|
|
7
7
|
import { createWordpressPlugin } from "./monitoring/WordpressPlugin";
|
|
8
8
|
import { createLogWatchPlugin } from "./monitoring/LogWatchPlugin";
|
|
9
9
|
import { createNetScanPlugin } from "./monitoring/NetScanPlugin";
|
|
10
|
+
import { createSnmpPlugin } from "./monitoring/SnmpPlugin";
|
|
10
11
|
// In future, implement other plugin factory functions using the same pattern
|
|
11
12
|
// import { createOSPlugin } from "./monitoring/OSPlugin"
|
|
12
13
|
// import { createMongoDBPlugin } from "./monitoring/MongoDBPlugin"
|
|
@@ -25,6 +26,7 @@ export default {
|
|
|
25
26
|
Wordpress: createWordpressPlugin,
|
|
26
27
|
Logwatch: createLogWatchPlugin,
|
|
27
28
|
Netscan: createNetScanPlugin,
|
|
29
|
+
Snmp: createSnmpPlugin,
|
|
28
30
|
// CloudflarePlugin: createCloudflarePlugin,
|
|
29
31
|
// EwelinkPlugin: createEwelinkPlugin,
|
|
30
32
|
// MongoDBPlugin: createMongoDBPlugin,
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { createMonitoringPlugin } from "../../lib/MonitoringPlugin";
|
|
4
|
+
// SNMP monitor. First target class: printers (standard Printer-MIB) — model,
|
|
5
|
+
// serial, lifetime page count, and per-supply toner/consumable levels. Shells
|
|
6
|
+
// out to net-snmp's snmpget/snmpwalk (add `snmp` to the client image) rather
|
|
7
|
+
// than pulling an npm SNMP stack, matching the other CLI-backed plugins.
|
|
8
|
+
//
|
|
9
|
+
// One client_state per target (type "snmp-printer", uid snmp-<slug>). Severity
|
|
10
|
+
// is decided server-side (clientstate/update.put.ts): a supply at/under its
|
|
11
|
+
// warn threshold → warning; an error/offline printer status → error.
|
|
12
|
+
const pexec = promisify(execFile);
|
|
13
|
+
// Printer-MIB / HOST-RESOURCES OIDs
|
|
14
|
+
const OID = {
|
|
15
|
+
model: "1.3.6.1.2.1.25.3.2.1.3.1", // hrDeviceDescr
|
|
16
|
+
serial: "1.3.6.1.2.1.43.5.1.1.17.1", // prtGeneralSerialNumber
|
|
17
|
+
pages: "1.3.6.1.2.1.43.10.2.1.4.1.1", // prtMarkerLifeCount
|
|
18
|
+
status: "1.3.6.1.2.1.25.3.5.1.1.1", // hrPrinterStatus (3=idle,4=printing,5=warmup)
|
|
19
|
+
supplyDesc: "1.3.6.1.2.1.43.11.1.1.6", // prtMarkerSuppliesDescription (walk)
|
|
20
|
+
supplyMax: "1.3.6.1.2.1.43.11.1.1.8", // prtMarkerSuppliesMaxCapacity (walk)
|
|
21
|
+
supplyLevel: "1.3.6.1.2.1.43.11.1.1.9", // prtMarkerSuppliesLevel (walk)
|
|
22
|
+
};
|
|
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
|
+
function slug(s) {
|
|
33
|
+
return s.replace(/[^a-zA-Z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
34
|
+
}
|
|
35
|
+
async function snmpGet(t, oid, timeout = 8000) {
|
|
36
|
+
try {
|
|
37
|
+
const { stdout } = await pexec("snmpget", ["-v", t.version || "2c", "-c", t.community || "public", "-Ovq", "-t", "2", "-r", "1", t.ip, oid], { timeout });
|
|
38
|
+
const v = stdout.trim().replace(/^"|"$/g, "");
|
|
39
|
+
return v && !/No Such|no response|Timeout/i.test(v) ? v : null;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function snmpWalk(t, oid, timeout = 10000) {
|
|
46
|
+
try {
|
|
47
|
+
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);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async function collectPrinter(t) {
|
|
55
|
+
const warnPct = t.tonerWarnPct ?? 10;
|
|
56
|
+
const [model, serial, pagesRaw, statusRaw, descs, maxes, levels] = await Promise.all([
|
|
57
|
+
snmpGet(t, OID.model),
|
|
58
|
+
snmpGet(t, OID.serial),
|
|
59
|
+
snmpGet(t, OID.pages),
|
|
60
|
+
snmpGet(t, OID.status),
|
|
61
|
+
snmpWalk(t, OID.supplyDesc),
|
|
62
|
+
snmpWalk(t, OID.supplyMax),
|
|
63
|
+
snmpWalk(t, OID.supplyLevel),
|
|
64
|
+
]);
|
|
65
|
+
// No response at all → the printer is unreachable / SNMP off.
|
|
66
|
+
if (model == null && serial == null && pagesRaw == null && !descs.length) {
|
|
67
|
+
return { name: t.name, ip: t.ip, kind: "printer", reachable: false };
|
|
68
|
+
}
|
|
69
|
+
// Pair descriptions with levels. prtMarkerSuppliesLevel semantics: -2 =
|
|
70
|
+
// unknown, -3 = "some remaining" (a level the device won't quantify); else a
|
|
71
|
+
// count out of MaxCapacity (which can itself be -2 = unknown).
|
|
72
|
+
const supplies = descs.map((desc, i) => {
|
|
73
|
+
const max = Number(maxes[i]);
|
|
74
|
+
const lvl = Number(levels[i]);
|
|
75
|
+
let pct = null;
|
|
76
|
+
if (lvl === -3)
|
|
77
|
+
pct = 100; // "ok / some remaining"
|
|
78
|
+
else if (lvl >= 0 && max > 0)
|
|
79
|
+
pct = Math.round((lvl / max) * 100);
|
|
80
|
+
return { name: desc, level: lvl, max, pct };
|
|
81
|
+
});
|
|
82
|
+
const low = supplies.filter((s) => s.pct != null && s.pct <= warnPct);
|
|
83
|
+
return {
|
|
84
|
+
name: t.name,
|
|
85
|
+
ip: t.ip,
|
|
86
|
+
kind: "printer",
|
|
87
|
+
reachable: true,
|
|
88
|
+
model: model || undefined,
|
|
89
|
+
serial: serial || undefined,
|
|
90
|
+
pages: pagesRaw != null ? Number(pagesRaw) : undefined,
|
|
91
|
+
status: statusRaw != null ? PRINTER_STATUS[statusRaw] || statusRaw : undefined,
|
|
92
|
+
supplies,
|
|
93
|
+
lowSupplies: low.map((s) => ({ name: s.name, pct: s.pct })),
|
|
94
|
+
warnPct,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
// Generic SNMP device (switch/AP/…): identity, uptime, and enabled-port link
|
|
98
|
+
// state so a down uplink or dead switch surfaces. Reachability via SNMP is more
|
|
99
|
+
// reliable than ICMP — many devices drop ping but answer SNMP.
|
|
100
|
+
async function collectDevice(t) {
|
|
101
|
+
const [descr, name, uptimeRaw, oper, admin] = await Promise.all([
|
|
102
|
+
snmpGet(t, SYS.descr),
|
|
103
|
+
snmpGet(t, SYS.name),
|
|
104
|
+
snmpGet(t, SYS.uptime),
|
|
105
|
+
snmpWalk(t, SYS.ifOper),
|
|
106
|
+
snmpWalk(t, SYS.ifAdmin),
|
|
107
|
+
]);
|
|
108
|
+
if (descr == null && name == null && !oper.length) {
|
|
109
|
+
return { name: t.name, ip: t.ip, kind: t.kind || "device", reachable: false };
|
|
110
|
+
}
|
|
111
|
+
// Only count ADMIN-enabled ports; an enabled port that's operationally down is
|
|
112
|
+
// a real link-down (ignore the many disabled ports on a big switch).
|
|
113
|
+
let portsTotal = 0;
|
|
114
|
+
let portsUp = 0;
|
|
115
|
+
for (let i = 0; i < oper.length; i++) {
|
|
116
|
+
if (Number(admin[i]) === 1) {
|
|
117
|
+
portsTotal++;
|
|
118
|
+
if (Number(oper[i]) === 1)
|
|
119
|
+
portsUp++;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// sysUpTime timeticks → seconds
|
|
123
|
+
const upSecs = uptimeRaw != null ? Math.round((Number(String(uptimeRaw).replace(/[^0-9]/g, "")) || 0) / 100) : undefined;
|
|
124
|
+
return {
|
|
125
|
+
name: t.name,
|
|
126
|
+
ip: t.ip,
|
|
127
|
+
kind: t.kind || "device",
|
|
128
|
+
reachable: true,
|
|
129
|
+
sysDescr: descr || undefined,
|
|
130
|
+
sysName: name || undefined,
|
|
131
|
+
uptimeSecs: upSecs,
|
|
132
|
+
portsUp,
|
|
133
|
+
portsTotal,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
export function createSnmpPlugin() {
|
|
137
|
+
let refreshTimer = null;
|
|
138
|
+
const targetsOf = (plugin) => {
|
|
139
|
+
const cfg = (plugin.config || {});
|
|
140
|
+
return (Array.isArray(cfg.targets) ? cfg.targets : [])
|
|
141
|
+
.filter((t) => t?.ip)
|
|
142
|
+
.map((t) => ({ ...t, community: t.community || cfg.community || "public" }));
|
|
143
|
+
};
|
|
144
|
+
const refreshFn = async (plugin) => {
|
|
145
|
+
await Promise.all(targetsOf(plugin).map(async (t) => {
|
|
146
|
+
try {
|
|
147
|
+
const data = t.kind === "printer" || !t.kind ? await collectPrinter(t) : await collectDevice(t);
|
|
148
|
+
await plugin.send(data, `snmp-${slug(t.name || t.ip)}`);
|
|
149
|
+
}
|
|
150
|
+
catch (e) {
|
|
151
|
+
await plugin.send({ name: t.name, ip: t.ip, kind: t.kind || "printer", error: String(e?.message || e).slice(0, 160) }, `snmp-${slug(t.name || t.ip)}`);
|
|
152
|
+
}
|
|
153
|
+
}));
|
|
154
|
+
};
|
|
155
|
+
const monitorFn = async (plugin) => {
|
|
156
|
+
await refreshFn(plugin);
|
|
157
|
+
refreshTimer = setInterval(() => refreshFn(plugin), plugin.config?.refreshInterval || 300000);
|
|
158
|
+
};
|
|
159
|
+
const teardownFn = async () => {
|
|
160
|
+
if (refreshTimer)
|
|
161
|
+
clearInterval(refreshTimer);
|
|
162
|
+
refreshTimer = null;
|
|
163
|
+
};
|
|
164
|
+
return createMonitoringPlugin("snmp", "snmp", "SNMP monitor (printers: model, pages, toner levels)", async () => { }, monitorFn, refreshFn, teardownFn);
|
|
165
|
+
}
|
|
166
|
+
export default createSnmpPlugin;
|