SNMP Initial Commits

This commit introduces a new SNMP monitor feature to the application, allowing users to monitor devices using SNMP (Simple Network Management Protocol).
This commit is contained in:
Matt Visnovsky 2024-04-26 19:05:56 -06:00
parent bab427f715
commit d92003e172
7 changed files with 714 additions and 647 deletions

View file

@ -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
});
};

6
package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -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,
};

View file

@ -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");

Binary file not shown.

View file

@ -24,6 +24,9 @@
<option value="ping">
Ping
</option>
<option value="snmp">
SNMP
</option>
<option value="keyword">
HTTP(s) - {{ $t("Keyword") }}
</option>
@ -159,12 +162,7 @@
<div v-if="remoteBrowsersToggle">
<label for="remote-browser" class="form-label">{{ $t("Remote Browser") }}</label>
<ActionSelect
v-model="monitor.remote_browser"
:options="remoteBrowsersOptions"
icon="plus"
:action="() => $refs.remoteBrowserDialog.show()"
/>
<ActionSelect v-model="monitor.remote_browser" :options="remoteBrowsersOptions" icon="plus" :action="() => $refs.remoteBrowserDialog.show()" />
</div>
</div>
@ -198,22 +196,7 @@
<!-- Kafka Brokers List -->
<div class="my-3">
<label for="kafkaProducerBrokers" class="form-label">{{ $t("Kafka Brokers") }}</label>
<VueMultiselect
id="kafkaProducerBrokers"
v-model="monitor.kafkaProducerBrokers"
:multiple="true"
:options="[]"
:placeholder="$t('Enter the list of brokers')"
:tag-placeholder="$t('Press Enter to add broker')"
:max-height="500"
:taggable="true"
:show-no-options="false"
:close-on-select="false"
:clear-on-select="false"
:preserve-search="false"
:preselect-first="false"
@tag="addKafkaProducerBroker"
></VueMultiselect>
<VueMultiselect id="kafkaProducerBrokers" v-model="monitor.kafkaProducerBrokers" :multiple="true" :options="[]" :placeholder="$t('Enter the list of brokers')" :tag-placeholder="$t('Press Enter to add broker')" :max-height="500" :taggable="true" :show-no-options="false" :close-on-select="false" :clear-on-select="false" :preserve-search="false" :preselect-first="false" @tag="addKafkaProducerBroker"></VueMultiselect>
</div>
<!-- Kafka Topic Name -->
@ -246,19 +229,65 @@
</template>
<!-- Hostname -->
<!-- TCP Port / Ping / DNS / Steam / MQTT / Radius / Tailscale Ping only -->
<div v-if="monitor.type === 'port' || monitor.type === 'ping' || monitor.type === 'dns' || monitor.type === 'steam' || monitor.type === 'gamedig' ||monitor.type === 'mqtt' || monitor.type === 'radius' || monitor.type === 'tailscale-ping'" class="my-3">
<!-- TCP Port / Ping / DNS / Steam / MQTT / Radius / Tailscale Ping / SNMP only -->
<div v-if="monitor.type === 'port' || monitor.type === 'ping' || monitor.type === 'dns' || monitor.type === 'steam' || monitor.type === 'gamedig' || monitor.type === 'mqtt' || monitor.type === 'radius' || monitor.type === 'tailscale-ping' || monitor.type === 'snmp'" class="my-3">
<label for="hostname" class="form-label">{{ $t("Hostname") }}</label>
<input id="hostname" v-model="monitor.hostname" type="text" class="form-control" :pattern="`${monitor.type === 'mqtt' ? mqttIpOrHostnameRegexPattern : ipOrHostnameRegexPattern}`" required>
</div>
<!-- Port -->
<!-- For TCP Port / Steam / MQTT / Radius Type -->
<div v-if="monitor.type === 'port' || monitor.type === 'steam' || monitor.type === 'gamedig' || monitor.type === 'mqtt' || monitor.type === 'radius'" class="my-3">
<!-- For TCP Port / Steam / MQTT / Radius Type / SNMP -->
<div v-if="monitor.type === 'port' || monitor.type === 'steam' || monitor.type === 'gamedig' || monitor.type === 'mqtt' || monitor.type === 'radius' || monitor.type === 'snmp'" class="my-3">
<label for="port" class="form-label">{{ $t("Port") }}</label>
<input id="port" v-model="monitor.port" type="number" class="form-control" required min="0" max="65535" step="1">
</div>
<!-- SNMP Monitor Type -->
<div v-if="monitor.type === 'snmp'" class="my-3">
<label for="snmp_community_string" class="form-label">{{ $t("Community String") }}</label>
<input id="snmp_community_string" v-model="monitor.snmpCommunityString" type="text" class="form-control" required>
</div>
<div v-if="monitor.type === 'snmp'" class="my-3">
<label for="snmp_oid" class="form-label">{{ $t("OID (Object Identifier)") }}</label>
<input id="snmp_oid" v-model="monitor.snmpOid" type="text" class="form-control" required>
</div>
<div v-if="monitor.type === 'snmp'" class="my-3">
<div class="d-flex align-items-start">
<div>
<label for="snmp_condition" class="form-label">{{ $t("Condition") }}</label>
<select id="snmp_condition" v-model="monitor.snmpCondition" class="form-select me-3">
<option value=">">></option>
<option value=">=">>=</option>
<option value="<"><</option>
<option value="<="><=</option>
<option value="==">==</option>
<option value="contains">contains</option>
</select>
</div>
<div>
<label for="snmp_control_value" class="form-label">{{ $t("Control Value") }}</label>
<input id="snmp_control_value" v-model="monitor.snmpControlValue" type="number" class="form-control" required step=".01">
</div>
</div>
</div>
<div v-if="monitor.type === 'snmp'" class="my-3">
<label for="snmp_version" class="form-label">{{ $t("SNMP Version") }}</label>
<select id="snmp_version" v-model="monitor.snmpVersion" class="form-select">
<option value="1">
SNMPv1
</option>
<option value="2c">
SNMPv2c
</option>
<option value="3">
SNMPv3
</option>
</select>
</div>
<!-- DNS Resolver Server -->
<!-- For DNS Type -->
<template v-if="monitor.type === 'dns'">
@ -283,19 +312,7 @@
<label for="dns_resolve_type" class="form-label">{{ $t("Resource Record Type") }}</label>
<!-- :allow-empty="false" is not working, set a default value instead https://github.com/shentao/vue-multiselect/issues/336 -->
<VueMultiselect
id="dns_resolve_type"
v-model="monitor.dns_resolve_type"
:options="dnsresolvetypeOptions"
:multiple="false"
:close-on-select="true"
:clear-on-select="false"
:preserve-search="false"
:placeholder="$t('Pick a RR-Type...')"
:preselect-first="false"
:max-height="500"
:taggable="false"
></VueMultiselect>
<VueMultiselect id="dns_resolve_type" v-model="monitor.dns_resolve_type" :options="dnsresolvetypeOptions" :multiple="false" :close-on-select="true" :clear-on-select="false" :preserve-search="false" :placeholder="$t('Pick a RR-Type...')" :preselect-first="false" :max-height="500" :taggable="false"></VueMultiselect>
<div class="form-text">
{{ $t("rrtypeDescription") }}
@ -315,16 +332,7 @@
<div v-if="monitor.type === 'docker'" class="my-3">
<div class="mb-3">
<label for="docker-host" class="form-label">{{ $t("Docker Host") }}</label>
<ActionSelect
id="docker-host"
v-model="monitor.docker_host"
:action-aria-label="$t('openModalTo', $t('Setup Docker Host'))"
:options="dockerHostOptionsList"
:disabled="$root.dockerHostList == null || $root.dockerHostList.length === 0"
:icon="'plus'"
:action="() => $refs.dockerHostDialog.show()"
:required="true"
/>
<ActionSelect id="docker-host" v-model="monitor.docker_host" :action-aria-label="$t('openModalTo', $t('Setup Docker Host'))" :options="dockerHostOptionsList" :disabled="$root.dockerHostList == null || $root.dockerHostList.length === 0" :icon="'plus'" :action="() => $refs.dockerHostDialog.show()" :required="true" />
</div>
</div>
@ -528,19 +536,7 @@
<div class="my-3">
<label for="acceptedStatusCodes" class="form-label">{{ $t("Accepted Status Codes") }}</label>
<VueMultiselect
id="acceptedStatusCodes"
v-model="monitor.accepted_statuscodes"
:options="acceptedStatusCodeOptions"
:multiple="true"
:close-on-select="false"
:clear-on-select="false"
:preserve-search="true"
:placeholder="$t('Pick Accepted Status Codes...')"
:preselect-first="false"
:max-height="600"
:taggable="true"
></VueMultiselect>
<VueMultiselect id="acceptedStatusCodes" v-model="monitor.accepted_statuscodes" :options="acceptedStatusCodeOptions" :multiple="true" :close-on-select="false" :clear-on-select="false" :preserve-search="true" :placeholder="$t('Pick Accepted Status Codes...')" :preselect-first="false" :max-height="600" :taggable="true"></VueMultiselect>
<div class="form-text">
{{ $t("acceptedStatusCodesDescription") }}
@ -551,15 +547,7 @@
<!-- Parent Monitor -->
<div class="my-3">
<label for="monitorGroupSelector" class="form-label">{{ $t("Monitor Group") }}</label>
<ActionSelect
id="monitorGroupSelector"
v-model="monitor.parent"
:action-aria-label="$t('openModalTo', 'setup a new monitor group')"
:options="parentMonitorOptionsList"
:disabled="sortedGroupMonitorList.length === 0 && draftGroupName == null"
:icon="'plus'"
:action="() => $refs.createGroupDialog.show()"
/>
<ActionSelect id="monitorGroupSelector" v-model="monitor.parent" :action-aria-label="$t('openModalTo', 'setup a new monitor group')" :options="parentMonitorOptionsList" :disabled="sortedGroupMonitorList.length === 0 && draftGroupName == null" :icon="'plus'" :action="() => $refs.createGroupDialog.show()" />
</div>
<!-- Description -->
@ -633,19 +621,7 @@
<label class="form-label" for="kafkaProducerSaslMechanism">
{{ $t("Mechanism") }}
</label>
<VueMultiselect
id="kafkaProducerSaslMechanism"
v-model="monitor.kafkaProducerSaslOptions.mechanism"
:options="kafkaSaslMechanismOptions"
:multiple="false"
:clear-on-select="false"
:preserve-search="false"
:placeholder="$t('Pick a SASL Mechanism...')"
:preselect-first="false"
:max-height="500"
:allow-empty="false"
:taggable="false"
></VueMultiselect>
<VueMultiselect id="kafkaProducerSaslMechanism" v-model="monitor.kafkaProducerSaslOptions.mechanism" :options="kafkaSaslMechanismOptions" :multiple="false" :clear-on-select="false" :preserve-search="false" :placeholder="$t('Pick a SASL Mechanism...')" :preselect-first="false" :max-height="500" :allow-empty="false" :taggable="false"></VueMultiselect>
</div>
<div v-if="monitor.kafkaProducerSaslOptions.mechanism !== 'None'">
<div v-if="monitor.kafkaProducerSaslOptions.mechanism !== 'aws'" class="my-3">
@ -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 {