@gapit/node-red-contrib-gapit-snmp
Version:
A Node-RED node which gets SNMP OIDs based on the gapit code JSON schema.
802 lines (728 loc) • 38.5 kB
JavaScript
module.exports = function (RED) {
"use strict";
var snmp = require("net-snmp");
var crypto = require("crypto");
var sessions = {};
function varbindsParseCounter64Buffers(varbinds, convertToNumbers=true) {
// Counter64 is not directly supported by net-snmp,
// but returned as a buffer (array). This function
// converts any Counter64 buffers in a varbinds object
// to a Number. The returned buffer is variable length
// (as big as needed to represent the number), while a
// BigInt is 8 bytes, and the function used expects this
// exact buffer length, so the buffer needs to be
// padded before it can be read as a BigInt.
//
// Based on code from
// https://discourse.nodered.org/t/snmp-access-problem/45990/9
//
// If convertToNumbers is set, the BigInt is converted
// to Number (if possible).
for (const varbind of varbinds) {
// ignore other data types
if(varbind.type !== snmp.ObjectType.Counter64) continue;
let inputBuffer = varbind.value;
// Counter64 is unsigned, but the underlying ASN.1
// integer in SNMP is signed, so a "full" Counter64
// will be represented by 9 bytes. Remove the leading
// (sign) byte in this case.
if (inputBuffer.length == 9) {
inputBuffer = inputBuffer.slice(1);
}
// pad and parse buffer
let padBuffer = Buffer.alloc(8 - inputBuffer.length);
let bigBuffer = Buffer.concat([padBuffer, inputBuffer]);
varbind.bufferValue = varbind.value
let big = bigBuffer.readBigUInt64BE();
if (convertToNumbers && big < Number.MAX_SAFE_INTEGER) {
varbind.value = Number(big);
}
else {
varbind.value = big;
}
}
}
function getGapitCodeResultsStructure(gapit_code) {
// Create a copy of gapit_code for storing results.
//
// Remove keys which should be runtime data, which
// may be present in older JSON files.
const group_remove_keys = ["next_read"];
const member_remove_keys = ["value"];
// deep copy using JSON stringify/parse
var gapit_results = JSON.parse(JSON.stringify(gapit_code));
for (const [groups_key, groups] of Object.entries(gapit_results)) {
for (var group_idx = 0; group_idx < groups.length; group_idx++) {
// remove specified group keys
for (const group_key of group_remove_keys) {
if (group_key in groups[group_idx]) {
delete groups[group_idx][group_key];
}
}
for (var member_idx = 0; member_idx < groups[group_idx]["group"].length; member_idx++) {
// remove specified member keys
for (const member_key of member_remove_keys) {
if (member_key in groups[group_idx]["group"][member_idx]) {
delete groups[group_idx]["group"][member_idx][member_key];
}
}
}
}
};
return gapit_results;
}
class Scaling {
constructor(name, convertBigintToNumber=true) {
// set use_scaling to requested scaling
var scaling_func = "_scaling_" + name;
if (this[scaling_func] === undefined) {
console.warn("Could not find scaling function '" + scaling_func + "', falling back to 'general'");
scaling_func = "_scaling_general";
}
else {
console.debug("Found scaling function '" + scaling_func + "'");
}
this.use_scaling = this[scaling_func];
var scaling_init_func = "_init_scaling_" + name;
if (this[scaling_init_func] !== undefined) {
console.debug("Calling init for scaling '" + name + "'")
this[scaling_init_func]();
}
this.convertBigintToNumber = convertBigintToNumber;
}
_init_scaling_schleifenbauer() {
this.registers = Object();
// each register field_name set needs its own set of registers
// these are lazy initialized when the first register of a
// field_name is used
}
factorToDivisorInt(factor) {
// turn a factor into a divisor,
// _if_ it turns out an integer
// otherwise, return 1
let divisor = 1 / factor;
if (Math.ceil(divisor) != divisor) {
// floating-point arithmetic handling
// 1/0.0001 == 10000
// 1/0.00001 == 99999.99999... - ceil to get the expected 100000
if (String(Math.ceil(divisor)).endsWith('000')) {
console.debug('Divisor must be ceil\'ed, but assumed safe');
divisor = Math.ceil(divisor);
}
else {
divisor = 1;
}
}
return divisor;
}
simple_scaling(value, scaling_factor, unit, field_name) {
if ((typeof value !== "number" && typeof value !== "bigint") || typeof scaling_factor !== "number" || scaling_factor == 1) {
if (scaling_factor == 1) {
console.debug("scaling_factor == 1, returning unchanged value");
}
else {
console.debug("Value or scaling_factor is not a number, returning unchanged value");
}
return value;
}
if (typeof value === "number") {
// cast to string with 8 decimals, and convert back to number
// this is to avoid numbers like 49.900000000000006 (from 499 * 0.1)
var result = Number((value * scaling_factor).toFixed(8));
console.debug(`Applied scaling to value ${value} with factor ${scaling_factor}, for result ${result}`);
return result;
}
else if (typeof value === "bigint") {
if (scaling_factor < 1) {
// a BigInt can't be multiplied with a fractional number,
// so flip (1/n) the scaling_factor and divide instead
let divisor = this.factorToDivisorInt(scaling_factor);
if (divisor == 1) {
console.error(`Factor ${scaling_factor} cannot be turned into an (integer) divisor for use with BigInt, returning unchanged value`);
return value;
}
var result = value / BigInt(divisor);
}
else {
// scaling_factor > 1
var result = value * BigInt(scaling_factor);
}
if (this.convertBigintToNumber && result < Number.MAX_SAFE_INTEGER) {
result = Number(result);
}
console.debug(`Applied scaling to value ${value} with factor ${scaling_factor}, for result ${result}`);
return result;
}
}
_scaling_general(value, scaling_factor, unit, field_name) {
console.debug(`Applying scaling to value ${value} with scaling factor ${scaling_factor}`);
return this.simple_scaling(value, scaling_factor, unit, field_name);
}
_scaling_schleifenbauer(value, scaling_factor, unit, field_name) {
console.debug(`Decoding Schleifenbauer with value ${value} and scaling factor ${scaling_factor}`);
var result = this.simple_scaling(value, scaling_factor, unit, field_name);
if (unit.startsWith("register")) {
// this is a register1/2/3 field
// only the last word of the field names should be different
// (e.g. "Active Total 1", "Active Total 2")
// find common field name ("Active Total" in above example)
var common_field_name = field_name.split(" ").slice(0, -1).join(" ")
//console.log("common_field_name: " + common_field_name);
// set up registers for common field name if missing
if (! (common_field_name in this.registers)) {
console.log(`initializing registers for ${common_field_name}`);
this.registers[common_field_name] = {
"register1": -1,
"register2": -1,
"register3": -1
}
}
if (unit != "register4")
// register 1/2/3, persist value for later sum
//
// if a "register5" or "registerBob" exists in gapit code,
// it would also be persisted, but it won't be used to
// calculate the sum anyway
this.registers[common_field_name][unit] = result;
else {
// unit == register4
// if registers 1 through 3 are set (not -1), return sum
if (this.registers[common_field_name]["register1"] != -1 &&
this.registers[common_field_name]["register2"] != -1 &&
this.registers[common_field_name]["register3"] != -1) {
console.debug(`All registers set for '${common_field_name}', calculating total`);
result = this.registers[common_field_name]["register1"] +
this.registers[common_field_name]["register2"] +
this.registers[common_field_name]["register3"]
// reset registers
this.registers[common_field_name]["register1"] = -1;
this.registers[common_field_name]["register2"] = -1;
this.registers[common_field_name]["register3"] = -1;
}
else {
console.debug(`One or more registers was not set for '${common_field_name}', cannot calculate total`);
// reset registers
this.registers[common_field_name]["register1"] = -1;
this.registers[common_field_name]["register2"] = -1;
this.registers[common_field_name]["register3"] = -1;
// set result to an invalid value as well
result = -1
}
}
}
return result;
}
}
function GapitSnmpNode(config) {
RED.nodes.createNode(this, config);
this.blockTuningSteps = 10;
this.config = config;
this.multi_device_separator = ";";
this.version = snmp.Version[config.version];
this.tagname_device_name = config.tagname_device_name.trim();
this.tagvalue_device_name = config.tagvalue_device_name.trim();
// split device_name from config into an array
this.device_names = this.tagvalue_device_name.split(this.multi_device_separator);
// split minion_ids from config into an array
// splitting an empty string yields an array with
// one empty string, so check length of string
if (config.minion_ids.trim().length > 0) {
this.minion_ids = config.minion_ids.trim().split(this.multi_device_separator);
}
else {
this.minion_ids = Array();
}
// parse custom tags JSON if present in config
if (config.custom_tags) {
this.custom_tags = JSON.parse(config.custom_tags);
}
else {
this.custom_tags = Object();
}
// parse Gapit code JSON if present in config
if (config.gapit_code) {
config.gapit_code = JSON.parse(config.gapit_code);
}
this.scaling = config.scaling;
this.timeout = Number(config.timeout || 5) * 1000;
// set up mapping from device_name to minion_id
this.device_name_to_minion_id = Object();
if (this.minion_ids.length > 0) {
if (this.minion_ids.length != this.device_names.length) {
this.error("'Device name' and 'Minion IDs' must contain the same number of items");
}
for (const [i, devname] of Object.entries(this.device_names)) {
this.device_name_to_minion_id[devname] = this.minion_ids[i];
}
}
else {
// single device, no minion id
// minion_id = -1 to indicate that minion id replacement
// should not be performed on OIDs.
this.device_name_to_minion_id[this.device_names[0]] = -1;
}
// add db tags from config to node
this.db_tags = {}
for (const [key, val] of Object.entries(config)) {
if (key.startsWith("tagname_") && key != "tagname_device_name") {
var tag_name = key.substr("tagname_".length);
var tagvalue_key = "tagvalue_" + tag_name
// console.info("Found tag " + tag_name + ", looking for " + tagvalue_key)
if (tagvalue_key in config) {
console.debug("Adding tag " + config[key] + ": " + config[tagvalue_key])
this.db_tags[config[key]] = config[tagvalue_key];
}
else {
console.warn("Could not find matching " + tagvalue_key + " for " + key);
}
}
}
// add custom tags for all minions, or when no minions are
// specified. minion-specific tags must be processed later,
// e.g. in gapit-results-to-influx-batch node.
for (const [root_key, root_val] of Object.entries(this.custom_tags)) {
if (this.minion_ids.length == 0) {
// no minions, add tags from root
console.debug("Adding custom tag " + root_key + ": " + root_val)
this.db_tags[root_key] = root_val;
}
else if (root_key == "all-minion-tags") {
for (const [minion_key, minion_val] of Object.entries(root_val)) {
console.debug("Adding custom (all-minion) tag " + minion_key + ": " + minion_val)
this.db_tags[minion_key] = minion_val;
}
}
}
this.scaler = new Scaling(this.scaling, this.config.convert_counter64_bigint_to_number);
var node = this;
// get context
var nodeContext = node.context();
// initialize nonexistent_oids in context
console.info("initializing nonexistent_oids in context (set to empty Array)")
nodeContext.set("nonexistent_oids", Array());
this.getSession = function () {
// check if session already exists, return if it does
if (node.sessionKey !== undefined && node.sessionKey in sessions) {
console.debug(`Found existing session for ${node.sessionKey}`);
return sessions[node.sessionKey];
}
let host = node.config.host;
let sessionKey;
if (node.version === snmp.Version3) {
// include (a hash of) all v3 options to create a unique session key
let v3OptionsHash = crypto.createHash('sha256').update(
node.config.security_level +
node.config.auth_protocol +
node.credentials.auth_key +
node.config.priv_protocol +
node.credentials.priv_key).digest('hex').slice(-8)
sessionKey = host + ":" + node.credentials.username + ":" + node.version + ":" + v3OptionsHash;
}
else { // not SNMPv3
sessionKey = host + ":" + node.config.community + ":" + node.version;
}
var port = 161;
if (host.indexOf(":") !== -1) {
port = host.split(":")[1];
host = host.split(":")[0];
}
console.info(`Creating session for ${sessionKey}`);
let options = { port:port, version:node.version, timeout:(node.timeout || 5000) }
if (node.version === snmp.Version3) {
if (node.config.context.trim() != "") {
options.context = node.config.context.trim();
}
let user = node.createV3UserObject();
sessions[sessionKey] = snmp.createV3Session(host, user, options);
}
else {
sessions[sessionKey] = snmp.createSession(host, node.config.community, options);
}
sessions[sessionKey].on("error", function (error) {
console.log ("Session error: " + error.toString());
node.closeSession();
})
node.sessionKey = sessionKey;
return sessions[sessionKey];
}
this.createV3UserObject = function() {
// set up user object for SNMPv3
// empty object if other version
let user = {}
if (node.version == snmp.Version3) {
user.name = node.credentials.username;
user.level = snmp.SecurityLevel[node.config.security_level];
if (user.level == snmp.SecurityLevel.authNoPriv || user.level == snmp.SecurityLevel.authPriv) {
user.authProtocol = snmp.AuthProtocols[node.config.auth_protocol];
user.authKey = node.credentials.auth_key;
}
if (user.level == snmp.SecurityLevel.authPriv) {
user.privProtocol = snmp.PrivProtocols[node.config.priv_protocol];
user.privKey = node.credentials.priv_key;
}
}
return user;
}
this.closeSession = function () {
if (node.sessionKey !== undefined && node.sessionKey in sessions) {
console.debug(`Closing session ${node.sessionKey}`);
sessions[node.sessionKey].close();
delete sessions[node.sessionKey];
}
}
this.processVarbinds = function (msg, varbinds) {
// parse Counter64 values in varbinds
varbindsParseCounter64Buffers(varbinds, node.config.convert_counter64_bigint_to_number);
// get result structure
var gapit_results = getGapitCodeResultsStructure(msg.gapit_code);
var varbinds_to_delete = Array();
for (var i = 0; i < varbinds.length; i++) {
if (snmp.isVarbindError(varbinds[i])) {
if (varbinds[i].type == snmp.ObjectType.NoSuchInstance ||
varbinds[i].type == snmp.ObjectType.NoSuchObject) {
// example code uses snmp.ErrorStatus.NoSuchInstance,
// but it is actually snmp.ObjectType.NoSuchInstance
node.warn("OID '" + varbinds[i]["oid"] + "' is not present")
// remove varbinds with these errors, instead of throwing an error
// build list of indexes to delete after iteration is complete
varbinds_to_delete.push(i);
// add to context "nonexistent_oids" array if not already there,
// so the OID can be skipped in the next query
node.addNonExistentOid(varbinds[i]["oid"]);
}
else {
node.error("OID/varbind error: " + snmp.varbindError(varbinds[i]), msg);
}
}
else {
if (varbinds[i].type == 4) { varbinds[i].value = varbinds[i].value.toString(); }
varbinds[i].tstr = snmp.ObjectType[varbinds[i].type];
//node.log(varbinds[i].oid + "|" + varbinds[i].tstr + "|" + varbinds[i].value);
}
}
node.persistNonExistentOids();
// reverse the list of varbinds to delete,
// to delete starting at the end of the array
varbinds_to_delete.reverse().forEach(function(i) {
varbinds.splice(i, 1);
});
var oid_value_map = Object();
for (var i = 0; i < varbinds.length; i++) {
oid_value_map[varbinds[i]["oid"]] = varbinds[i]["value"];
}
// map result values into gapit_results
// also, optionally remove items with no value
for (const [groups_key, groups] of Object.entries(gapit_results)) {
for (var group_idx = 0; group_idx < groups.length; group_idx++) {
// iterate array in reverse, to enable deletion
for (var member_idx = groups[group_idx]["group"].length - 1; member_idx >= 0 ; member_idx--) {
var oid = groups[group_idx]["group"][member_idx]["address"];
if (oid in oid_value_map) {
groups[group_idx]["group"][member_idx]["value"] = oid_value_map[oid];
}
else if (node.config.remove_novalue_items_from_gapit_results) {
groups[group_idx]["group"].splice(member_idx, 1);
//node.warn("should delete this");
}
}
// apply scaling
// for certain scaling methods (e.g. Schleifenbauer), the scaling
// needs to be applied in the defined gapit_code order, hence a
// separate loop for scaling.
for (var member_idx = 0; member_idx < groups[group_idx]["group"].length ; member_idx++) {
if(("value" in groups[group_idx]["group"][member_idx])
&& groups[group_idx]["group"][member_idx]["byte_type"] != "STR") {
// value is set, and not a string, apply scaling
groups[group_idx]["group"][member_idx]["value"] =
node.scaler.use_scaling(groups[group_idx]["group"][member_idx]["value"],
groups[group_idx]["group"][member_idx]["scaling_factor"],
groups[group_idx]["group"][member_idx]["unit"],
groups[group_idx]["group"][member_idx]["description"]);
}
}
}
};
msg.db_tags = node.db_tags;
msg.custom_tags = node.custom_tags;
msg.tagname_device_name = node.tagname_device_name;
msg.oid_value_map = oid_value_map;
msg.gapit_results = gapit_results;
node.send(msg);
}
this.readNonExistentOids = function () {
// get nonexistent_oids from context
node.nonexistent_oids = nodeContext.get("nonexistent_oids");
// flag to keep track of changes to nonexistent_oids
node.nonexistent_oids_modified = false;
}
this.addNonExistentOid = function (oid) {
if (node.config.skip_nonexistent_oids) {
if (! node.nonexistent_oids.includes(oid)) {
node.nonexistent_oids.push(oid);
node.nonexistent_oids_modified = true;
}
}
}
this.isNonExistentOid = function (oid) {
if (node.config.skip_nonexistent_oids) {
return node.nonexistent_oids.includes(oid);
}
else {
// skip_nonexistent_oids is disabled
return false;
}
}
this.persistNonExistentOids = function () {
// if modified, save nonexistent_oids to context
if (node.config.skip_nonexistent_oids) {
if (node.nonexistent_oids_modified) {
nodeContext.set("nonexistent_oids", node.nonexistent_oids);
}
}
}
this.expandGapitCodeForMinions = function (msg) {
// expand config to support querying for multiple minions.
// modify gapit_config to have device_name as key, in place of "objects",
// with one copy of config per device_name.
//
for (const device_name of node.device_names) {
// in case of empty device names (;;)
if(device_name.length > 0) {
console.debug(`Copying gapit_code["objects"] to gapit_code[${device_name}]`);
msg.gapit_code[device_name] = JSON.parse(JSON.stringify(msg.gapit_code["objects"]));
var minion_id = node.device_name_to_minion_id[device_name];
if (minion_id != -1) {
// miniond_id == -1 means minion_ids was not specified in node config
console.debug(`Replacing minion ID in OIDs for "${device_name}", id ${minion_id}`);
var groups = msg.gapit_code[device_name];
for (var group_idx = 0; group_idx < groups.length; group_idx++) {
for (var member_idx = 0; member_idx < groups[group_idx]["group"].length; member_idx++) {
var oid = groups[group_idx]["group"][member_idx]["address"];
oid = oid.replace("x", minion_id);
groups[group_idx]["group"][member_idx]["address"] = oid;
}
}
}
}
}
// remove original "objects" from gapit_code, leaving only
// device_name keys. even if tagvalue_device_name is required,
// it is still possible (with a warning) to deploy a flow
// without it, so first verify that there is more than one
// key present.
if (Object.keys(msg.gapit_code).length > 1) {
console.debug("Removing original gapit_code['objects']");
delete msg.gapit_code["objects"];
}
}
this.getNextRead = function (msg) {
var next_read = nodeContext.get("next_read");
// initialize next_read (set 0) if not present
if (next_read === undefined) {
console.debug("no next_read in context, initializing variable");
next_read = Object();
for (const [groups_key, groups] of Object.entries(msg.gapit_code)) {
next_read[groups_key] = Object();
for (var group_idx = 0; group_idx < groups.length; group_idx++) {
var group_name = groups[group_idx]["group_name"];
// console.log(`setting next_read for ${group_name}`);
next_read[groups_key][group_name] = 0;
}
}
}
return next_read;
}
this.skipGroupThisRead = function (msg, groups_key, group_idx) {
// check if this group should be skipped for this read,
// according to next_read
var next_read = node.getNextRead(msg);
var group_name = msg.gapit_code[groups_key][group_idx]["group_name"];
var read_priority = msg.gapit_code[groups_key][group_idx]["read_priority"];
var ts = Math.trunc(new Date().valueOf() / 1000);
if (ts < next_read[groups_key][group_name] || read_priority == "n") {
if (ts < next_read[groups_key][group_name]) {
var next_read_time = new Date(next_read[groups_key][group_name] * 1000).toLocaleTimeString();
console.debug(`Skipping group '${group_name}' until next_read time (${next_read_time})`);
}
else if (groups[group_idx]["read_priority"] == "n") {
console.debug(`Skipping group '${group_name}', read_priority == "n"`);
}
return true;
}
// default
return false;
}
this.setNextReadForGroup = function (msg, groups_key, group_idx) {
// set next_read
var next_read = node.getNextRead(msg);
var group_name = msg.gapit_code[groups_key][group_idx]["group_name"];
var read_priority = msg.gapit_code[groups_key][group_idx]["read_priority"];
var ts = Math.trunc(new Date().valueOf() / 1000);
next_read[groups_key][group_name] = ts + read_priority;
// save next_read to context
nodeContext.set("next_read", next_read);
}
this.getOidsToQuery = function (msg) {
var oids = Array()
for (const [groups_key, groups] of Object.entries(msg.gapit_code)) {
for (var group_idx = 0; group_idx < groups.length; group_idx++) {
var group_name = groups[group_idx]["group_name"];
if (node.skipGroupThisRead(msg, groups_key, group_idx)) {
continue;
}
node.setNextReadForGroup(msg, groups_key, group_idx);
console.info("Getting OIDs from group '" + group_name + "'");
for (var member_idx = 0; member_idx < groups[group_idx]["group"].length; member_idx++) {
var oid = groups[group_idx]["group"][member_idx]["address"];
if (node.isNonExistentOid(oid)) {
// oid does not exist, skip
continue;
}
// duplicate OIDs kill the SNMP request
if (oids.includes(oid)) {
// already in Array, skip
continue;
}
oids.push(oid);
}
}
};
return oids;
}
this.tuneSnmpBlockSize = function (host, community, oids, msg, blockSize) {
node.getSession().get(oids.slice(0, blockSize), function (error, varbinds) {
if (error) {
// error object has .name, .message and, optionally, .status
// error.status is only set for RequestFailed, so check
// that it's this error before checking the value of .status
if ((error.name == "RequestFailedError") && (error.status == snmp.ErrorStatus.TooBig)) {
// Adjust blockSize down one level, try again.
blockSize = blockSize - node.blockTuningSteps;
node.warn(`Still tooBig, trying again with ${blockSize} OIDs`);
node.tuneSnmpBlockSize(host, community, oids, msg, blockSize);
}
else if ((error.name == "RequestFailedError") && (error.status == snmp.ErrorStatus.NoSuchName)) {
// As soon as tooBig is no longer triggered, this error
// may occur.
//
// SNMPv1 NoSuchName
// A single "missing" OID causes an SNMPv1 query to fail,
// query OIDs one by one as a workaround
node.warn("SNMPv1 NoSuchName, will query all OIDs individually");
node.getBlockSize = 1;
node.snmpGet(host, community, oids, msg);
}
else {
node.error("Request error: " + error.toString(), msg);
}
}
else {
console.info(`Found working block size to work around SNMP tooBig error: ${blockSize} OIDs`);
node.getBlockSize = blockSize;
node.snmpGet(host, community, oids, msg);
}
});
}
this.snmpGet = function (host, community, oids, msg) {
let blockSize = 0;
if (node.getBlockSize !== undefined) {
blockSize = node.getBlockSize;
console.debug(`Querying OIDs in blocks of ${blockSize}, total number of OIDs is ${oids.length}`);
// For testing, remove always-failing (in simulator) oid from list.
// let simFailingOidIndex = oids.indexOf("1.3.6.1.4.1.534.1.9.7.0.1.2.1.2.1.2.1.2.1.2.1.2.1.2.1.2.1.2.1.2.1.2.1.2.1.2.1.2.1.2.1.2.1.2.1.2.1.2")
// if (simFailingOidIndex != -1) {
// oids.splice(simFailingOidIndex, 1);
// }
}
else {
blockSize = oids.length;
console.debug("Querying all OIDs in a single request");
}
msg.varbinds = Array();
msg.totalBlockResponseCount = 0;
for (let blockStart = 0; blockStart < oids.length; blockStart=blockStart+blockSize) {
let oidBlock = oids.slice(blockStart, blockStart+blockSize);
node.getSession().get(oidBlock, function (error, blockVarbinds) {
msg.totalBlockResponseCount += oidBlock.length;
if (error) {
// error object has .name, .message and, optionally, .status
// error.status is only set for RequestFailed, so check
// that it's this error before checking the value of .status
if ((error.name == "RequestFailedError") && (error.status == snmp.ErrorStatus.NoSuchName)) {
// SNMPv1 NoSuchName
if (blockSize > 1) {
// A single "missing" OID causes an SNMPv1 query to fail,
// query OIDs one by one as a workaround
node.warn("SNMPv1 NoSuchName, will query all OIDs individually");
node.getBlockSize = 1;
node.snmpGet(host, community, oids, msg);
}
else {
node.warn(`SNMPv1 single-OID query: OID '${oidBlock[0]}' is not present`);
node.addNonExistentOid(oidBlock[0]);
}
}
else if ((error.name == "RequestFailedError") && (error.status == snmp.ErrorStatus.TooBig)) {
// SNMP tooBig -- too many OIDs for a single request
// The request must be broken into several smaller requests.
// Set an initial bulk size, then let tuneSnmpBlockSize()
// handle further adjustments.
let blockSize = oids.length - node.blockTuningSteps - (oids.length % node.blockTuningSteps);
node.warn(`SNMP tooBig error, divide into multiple requests for blocks of OIDS, first attempt: ${blockSize} OIDs`);
node.tuneSnmpBlockSize(host, community, oids, msg, blockSize);
}
else {
node.error("Request error: " + error.toString(), msg);
}
}
else {
// add results for this block to msg.varbinds
msg.varbinds.push(... blockVarbinds);
}
// if all queries have returned, and at least one had data, process
if (msg.totalBlockResponseCount === oids.length && msg.varbinds.length > 0) {
if (blockSize != oids.length) {
console.debug("Got results for all OID block queries, processing");
// clear 1-block if set (SNMPv1 NoSuchName)
if (node.getBlockSize !== undefined && node.getBlockSize == 1) {
console.debug("Clearing get block size of 1, assumed set for SNMPv1 NoSuchName");
delete(node.getBlockSize);
}
}
node.processVarbinds(msg, msg.varbinds);
}
});
}
}
this.on("input", function (msg) {
node.readNonExistentOids();
// deep copy gapit_code, so this variable can be modified without affecting config object
msg.gapit_code = JSON.parse(JSON.stringify(node.config.gapit_code || msg.gapit_code));
// if multiple minions are specified, verify that the
// number of device names matches the number of minions
if (node.minion_ids.length > 0 && node.minion_ids.length != node.device_names.length) {
node.error("'Device name' and 'Minion IDs' must contain the same number of items");
return;
}
node.expandGapitCodeForMinions(msg);
// build list of OIDs
var oids = node.getOidsToQuery(msg)
if (oids.length > 0) {
node.snmpGet(node.config.host, node.config.community, oids, msg);
}
else {
node.warn("No oid(s) to search for");
}
});
this.on("close", function() {
node.closeSession();
});
}
RED.nodes.registerType("gapit-snmp", GapitSnmpNode, {
credentials: {
username: {type:"text"},
auth_key: {type:"password"},
priv_key: {type:"password"}
}
});
};