@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.
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daemonitor/plugins",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "description": "daemonitor plugins",
5
5
  "types": "types/index.d.ts",
6
6
  "main": "dist/index.js",