diff --git a/db/knex_migrations/2024-04-26-0000-snmp-monitor.js b/db/knex_migrations/2024-04-26-0000-snmp-monitor.js new file mode 100644 index 000000000..8b93ecd49 --- /dev/null +++ b/db/knex_migrations/2024-04-26-0000-snmp-monitor.js @@ -0,0 +1,10 @@ +exports.up = function (knex) { + return knex.schema + .alterTable("monitor", function (table) { + table.string("snmp_community_string", 255).defaultTo("public"); // Add community_string column + table.string("snmp_oid").notNullable(); // Add oid column + table.enum("snmp_version", ["1", "2c", "3"]).defaultTo("2c"); // Add snmp_version column with enum values + table.float("snmp_control_value").notNullable(); // Add control_value column as float + table.string("snmp_condition").notNullable(); // Add oid column + }); +}; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index a6ef4f036..7728a0cfe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -72,6 +72,7 @@ "redbean-node": "~0.3.0", "redis": "~4.5.1", "semver": "~7.5.4", + "snmp-native": "^1.2.0", "socket.io": "~4.6.1", "socket.io-client": "~4.6.1", "socks-proxy-agent": "6.1.1", @@ -12609,6 +12610,11 @@ "npm": ">= 3.0.0" } }, + "node_modules/snmp-native": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/snmp-native/-/snmp-native-1.2.0.tgz", + "integrity": "sha512-JIyuLX3bQmuAI4gHztHSQd3M/M2hqgLhiHBZYEk8YnYRJ2ooxqwON4gUQfgp/WCZVDca4tIX3vFJgv6lz5iY+g==" + }, "node_modules/socket.io": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.6.2.tgz", diff --git a/package.json b/package.json index 567efa1b5..7ee8afb41 100644 --- a/package.json +++ b/package.json @@ -137,6 +137,7 @@ "redbean-node": "~0.3.0", "redis": "~4.5.1", "semver": "~7.5.4", + "snmp-native": "^1.2.0", "socket.io": "~4.6.1", "socket.io-client": "~4.6.1", "socks-proxy-agent": "6.1.1", diff --git a/server/monitor-types/snmp.js b/server/monitor-types/snmp.js new file mode 100644 index 000000000..4ddc88230 --- /dev/null +++ b/server/monitor-types/snmp.js @@ -0,0 +1,69 @@ +const { MonitorType } = require("./monitor-type"); +const { UP, DOWN } = require("../../src/util"); +const snmp = require("snmp-native"); + +class SNMPMonitorType extends MonitorType { + name = "snmp"; + + /** + * Checks the SNMP value against the condition and control value. + * @param {object} monitor The monitor object associated with the check. + * @param {object} heartbeat The heartbeat object to update. + * @param {object} _server Unused server object. + */ + async check(monitor, heartbeat, _server) { + try { + const session = new snmp.Session({ host: monitor.ipAddress, community: monitor.snmpCommunityString, version: monitor.snmpVersion }); + + session.get({ oid: monitor.snmpOid }, (err, varbinds) => { + if (err) { + heartbeat.status = DOWN; + heartbeat.msg = `Error: ${err.message}`; + return; + } + + // Assuming only one varbind is returned + const value = varbinds[0].value; + + // Convert value to appropriate type based on SNMP type (assuming it's integer or string for simplicity) + const numericValue = parseInt(value); + const stringValue = value.toString(); + + // Check against condition and control value + switch (monitor.snmpCondition) { + case '>': + heartbeat.status = numericValue > monitor.snmpControlValue ? UP : DOWN; + break; + case '>=': + heartbeat.status = numericValue >= monitor.snmpControlValue ? UP : DOWN; + break; + case '<': + heartbeat.status = numericValue < monitor.snmpControlValue ? UP : DOWN; + break; + case '<=': + heartbeat.status = numericValue <= monitor.snmpControlValue ? UP : DOWN; + break; + case '==': + heartbeat.status = value === monitor.snmpControlValue ? UP : DOWN; + break; + case 'contains': + heartbeat.status = stringValue.includes(monitor.snmpControlValue) ? UP : DOWN; + break; + default: + heartbeat.status = DOWN; + heartbeat.msg = `Invalid condition: ${monitor.snmpCondition}`; + } + + session.close(); + }); + } catch (err) { + heartbeat.status = DOWN; + heartbeat.msg = `Error: ${err.message}`; + } + } + +} + +module.exports = { + SNMPMonitorType, +}; diff --git a/server/uptime-kuma-server.js b/server/uptime-kuma-server.js index bcf497b58..db7b47970 100644 --- a/server/uptime-kuma-server.js +++ b/server/uptime-kuma-server.js @@ -113,6 +113,7 @@ class UptimeKumaServer { UptimeKumaServer.monitorTypeList["tailscale-ping"] = new TailscalePing(); UptimeKumaServer.monitorTypeList["dns"] = new DnsMonitorType(); UptimeKumaServer.monitorTypeList["mqtt"] = new MqttMonitorType(); + UptimeKumaServer.monitorTypeList["snmp"] = new SNMPMonitorType(); // Allow all CORS origins (polling) in development let cors = undefined; @@ -516,3 +517,4 @@ const { RealBrowserMonitorType } = require("./monitor-types/real-browser-monitor const { TailscalePing } = require("./monitor-types/tailscale-ping"); const { DnsMonitorType } = require("./monitor-types/dns"); const { MqttMonitorType } = require("./monitor-types/mqtt"); +const { SNMPMonitorType } = require("./monitor-types/snmp"); diff --git a/src/pages/.EditMonitor.vue.swp b/src/pages/.EditMonitor.vue.swp new file mode 100644 index 000000000..638a932fe Binary files /dev/null and b/src/pages/.EditMonitor.vue.swp differ diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index e9f3ac832..d082d3e2f 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -24,6 +24,9 @@ + @@ -100,19 +103,19 @@ -
+
-
+
-
+
@@ -159,12 +162,7 @@
- +
@@ -198,22 +196,7 @@
- +
@@ -246,646 +229,639 @@ - -
+ +
- -
+ +
- - - - - - -
- - -
- - - -
-
- - -
-
- - - - - - - - - - - - - - - - -
- - -
- -
- - -
- {{ $t("retriesDescription") }} -
-
- -
- - -
- - -
- - -
- -
- - -
- -

{{ $t("Advanced") }}

- -
- - -
-
-
- -
- - -
- -
- - -
- {{ $t("upsideDownModeDescription") }} -
-
- -
- - -
- {{ $t("gamedigGuessPortDescription") }} -
-
- - -
- - -
- - - - - -
- - -
- - -
- - -
- -
-
-
-
+
+ + +
- -

{{ $t("Notifications") }}

-

+ + + + + + +

+ + +
+ + + +
+
+ + +
+
+ + + + + + + + + + + + + + + + +
+ + +
+ +
+ + +
+ {{ $t("retriesDescription") }} +
+
+ +
+ + +
+ + +
+ + +
+ +
+ + +
+ +

{{ $t("Advanced") }}

+ +
+ + +
+
+
+ +
+ + +
+ +
+ + +
+ {{ $t("upsideDownModeDescription") }} +
+
+ +
+ + +
+ {{ $t("gamedigGuessPortDescription") }} +
+
+ + +
+ + +
+ + + + + +
+ + +
+ + +
+ + +
+ +
+ +
+
+ +
+
+ + +

{{ $t("Notifications") }}

+

+ {{ $t("Not available, please setup.") }} +

+ +
+ + + + + {{ $t("Default") }} +
+ + + + +
+

{{ $t("Proxy") }}

+

{{ $t("Not available, please setup.") }}

-
- +
+ + +
-
- -
-

{{ $t("Proxy") }}

-

- {{ $t("Not available, please setup.") }} -

- -
- - + + + - + + @@ -926,7 +902,7 @@ const monitorDefaults = { packetSize: 56, expiryNotification: false, maxredirects: 10, - accepted_statuscodes: [ "200-299" ], + accepted_statuscodes: ["200-299"], dns_resolve_type: "A", dns_resolve_server: "1.1.1.1", docker_container: "", @@ -947,7 +923,10 @@ const monitorDefaults = { kafkaProducerSsl: false, kafkaProducerAllowAutoTopicCreation: false, gamedigGivenPortOnly: true, - remote_browser: null + remote_browser: null, + port: 161, + communityString: 'public', + oid: '1.3.6.1.2.1.1.1.0', }; export default { @@ -996,7 +975,7 @@ export default { ipRegex() { // Allow to test with simple dns server with port (127.0.0.1:5300) - if (! isDev) { + if (!isDev) { return this.ipRegexPattern; } return null; @@ -1053,15 +1032,15 @@ export default { }, protoServicePlaceholder() { - return this.$t("Example:", [ "Health" ]); + return this.$t("Example:", ["Health"]); }, protoMethodPlaceholder() { - return this.$t("Example:", [ "check" ]); + return this.$t("Example:", ["check"]); }, protoBufDataPlaceholder() { - return this.$t("Example:", [ ` + return this.$t("Example:", [` syntax = "proto3"; package grpc.health.v1; @@ -1088,7 +1067,7 @@ message HealthCheckResponse { }, bodyPlaceholder() { if (this.monitor && this.monitor.httpBodyEncoding && this.monitor.httpBodyEncoding === "xml") { - return this.$t("Example:", [ ` + return this.$t("Example:", [` @@ -1097,16 +1076,16 @@ message HealthCheckResponse { ` ]); } if (this.monitor && this.monitor.httpBodyEncoding === "form") { - return this.$t("Example:", [ "key1=value1&key2=value2" ]); + return this.$t("Example:", ["key1=value1&key2=value2"]); } - return this.$t("Example:", [ ` + return this.$t("Example:", [` { "key": "value" }` ]); }, headersPlaceholder() { - return this.$t("Example:", [ ` + return this.$t("Example:", [` { "HeaderName": "HeaderValue" }` ]); @@ -1131,8 +1110,8 @@ message HealthCheckResponse { // Only groups, not itself, not a decendant result = result.filter( monitor => monitor.type === "group" && - monitor.id !== this.monitor.id && - !this.monitor.childrenIDs?.includes(monitor.id) + monitor.id !== this.monitor.id && + !this.monitor.childrenIDs?.includes(monitor.id) ); // Filter result by active state, weight and alphabetical @@ -1251,7 +1230,7 @@ message HealthCheckResponse { "monitor.type"() { if (this.monitor.type === "push") { - if (! this.monitor.pushToken) { + if (!this.monitor.pushToken) { // ideally this would require checking if the generated token is already used // it's very unlikely to get a collision though (62^32 ~ 2.27265788 * 10^57 unique tokens) this.monitor.pushToken = genSecret(pushTokenLength); @@ -1259,7 +1238,7 @@ message HealthCheckResponse { } // Set default port for DNS if not already defined - if (! this.monitor.port || this.monitor.port === "53" || this.monitor.port === "1812") { + if (!this.monitor.port || this.monitor.port === "53" || this.monitor.port === "1812") { if (this.monitor.type === "dns") { this.monitor.port = "53"; } else if (this.monitor.type === "radius") { @@ -1399,7 +1378,7 @@ message HealthCheckResponse { this.monitor.pathName = undefined; this.monitor.screenshot = undefined; - this.monitor.name = this.$t("cloneOf", [ this.monitor.name ]); + this.monitor.name = this.$t("cloneOf", [this.monitor.name]); this.$refs.tagsManager.newTags = this.monitor.tags.map((monitorTag) => { return { id: monitorTag.tag_id, @@ -1486,7 +1465,7 @@ message HealthCheckResponse { this.monitor.body = JSON.stringify(JSON.parse(this.monitor.body), null, 4); } - const monitorTypesWithEncodingAllowed = [ "http", "keyword", "json-query" ]; + const monitorTypesWithEncodingAllowed = ["http", "keyword", "json-query"]; if (this.monitor.type && !monitorTypesWithEncodingAllowed.includes(this.monitor.type)) { this.monitor.httpBodyEncoding = null; } @@ -1564,7 +1543,7 @@ message HealthCheckResponse { async startParentGroupMonitor() { await sleep(2000); - await this.$root.getSocket().emit("resumeMonitor", this.monitor.parent, () => {}); + await this.$root.getSocket().emit("resumeMonitor", this.monitor.parent, () => { }); }, /** @@ -1610,7 +1589,7 @@ message HealthCheckResponse { // Clamp timeout clampTimeout(timeout) { // limit to 80% of interval, narrowly avoiding epsilon bug - const maxTimeout = ~~(this.monitor.interval * 8 ) / 10; + const maxTimeout = ~~(this.monitor.interval * 8) / 10; const clamped = Math.max(0, Math.min(timeout, maxTimeout)); // 0 will be treated as 80% of interval @@ -1630,9 +1609,9 @@ message HealthCheckResponse { +textarea { + min-height: 200px; +} + \ No newline at end of file