diff options
Diffstat (limited to 'node.d')
-rw-r--r-- | node.d/Makefile.am | 26 | ||||
-rw-r--r-- | node.d/README.md | 0 | ||||
-rwxr-xr-x | node.d/named.node.js | 586 | ||||
-rw-r--r-- | node.d/node_modules/asn1.js | 20 | ||||
-rw-r--r-- | node.d/node_modules/ber/errors.js | 13 | ||||
-rw-r--r-- | node.d/node_modules/ber/index.js | 27 | ||||
-rw-r--r-- | node.d/node_modules/ber/reader.js | 261 | ||||
-rw-r--r-- | node.d/node_modules/ber/types.js | 36 | ||||
-rw-r--r-- | node.d/node_modules/ber/writer.js | 316 | ||||
-rw-r--r-- | node.d/node_modules/extend.js | 87 | ||||
-rw-r--r-- | node.d/node_modules/net-snmp.js | 1466 | ||||
-rwxr-xr-x | node.d/node_modules/netdata.js | 612 | ||||
-rw-r--r-- | node.d/node_modules/pixl-xml.js | 606 | ||||
-rwxr-xr-x | node.d/sma_webbox.node.js | 237 | ||||
-rwxr-xr-x | node.d/snmp.node.js | 436 |
15 files changed, 4729 insertions, 0 deletions
diff --git a/node.d/Makefile.am b/node.d/Makefile.am new file mode 100644 index 000000000..ae7eeac52 --- /dev/null +++ b/node.d/Makefile.am @@ -0,0 +1,26 @@ +MAINTAINERCLEANFILES= $(srcdir)/Makefile.in + +dist_node_SCRIPTS = \ + README.md \ + named.node.js \ + sma_webbox.node.js \ + snmp.node.js \ + $(NULL) + +nodemodulesdir=$(nodedir)/node_modules +dist_nodemodules_DATA = \ + node_modules/netdata.js \ + node_modules/extend.js \ + node_modules/pixl-xml.js \ + node_modules/net-snmp.js \ + node_modules/asn1.js \ + $(NULL) + +nodemodulesberdir=$(nodedir)/node_modules/ber +dist_nodemodulesber_DATA = \ + node_modules/ber/index.js \ + node_modules/ber/errors.js \ + node_modules/ber/reader.js \ + node_modules/ber/types.js \ + node_modules/ber/writer.js \ + $(NULL) diff --git a/node.d/README.md b/node.d/README.md new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/node.d/README.md diff --git a/node.d/named.node.js b/node.d/named.node.js new file mode 100755 index 000000000..c2b15eae7 --- /dev/null +++ b/node.d/named.node.js @@ -0,0 +1,586 @@ +'use strict'; + +// collect statistics from bind (named) v9.10+ +// +// bind statistics documentation at: +// http://jpmens.net/2013/03/18/json-in-bind-9-s-statistics-server/ +// https://ftp.isc.org/isc/bind/9.10.3/doc/arm/Bv9ARM.ch06.html#statistics + +// example configuration in /etc/netdata/named.conf +// the module supports auto-detection if bind is running in localhost + +/* +{ + "enable_autodetect": true, + "update_every": 5, + "servers": [ + { + "name": "bind1", + "url": "http://127.0.0.1:8888/json/v1/server", + "update_every": 1 + }, + { + "name": "bind2", + "url": "http://10.0.0.1:8888/xml/v3/server", + "update_every": 2 + } + ] +} +*/ + +// the following is the bind named.conf configuration required + +/* +statistics-channels { + inet 127.0.0.1 port 8888 allow { 127.0.0.1; }; +}; +*/ + +var url = require('url'); +var http = require('http'); +var XML = require('pixl-xml'); +var netdata = require('netdata'); + +if(netdata.options.DEBUG === true) netdata.debug('loaded ' + __filename + ' plugin'); + +var named = { + name: __filename, + enable_autodetect: true, + update_every: 1, + base_priority: 60000, + charts: {}, + + chartFromMembersCreate: function(service, obj, id, title_suffix, units, family, context, type, priority, algorithm, multiplier, divisor) { + var chart = { + id: id, // the unique id of the chart + name: '', // the unique name of the chart + title: service.name + ' ' + title_suffix, // the title of the chart + units: units, // the units of the chart dimensions + family: family, // the family of the chart + context: context, // the context of the chart + type: type, // the type of the chart + priority: priority, // the priority relative to others in the same family + update_every: service.update_every, // the expected update frequency of the chart + dimensions: {} + } + + var found = 0; + for(var x in obj) { + if(typeof(obj[x]) !== 'undefined' && obj[x] !== 0) { + found++; + chart.dimensions[x] = { + id: x, // the unique id of the dimension + name: x, // the name of the dimension + algorithm: algorithm, // the id of the netdata algorithm + multiplier: multiplier, // the multiplier + divisor: divisor, // the divisor + hidden: false // is hidden (boolean) + } + } + } + + if(found === false) + return null; + + chart = service.chart(id, chart); + this.charts[id] = chart; + return chart; + }, + + chartFromMembers: function(service, obj, id_suffix, title_suffix, units, family, context, type, priority, algorithm, multiplier, divisor) { + var id = 'named_' + service.name + '.' + id_suffix; + var chart = this.charts[id]; + + if(typeof chart === 'undefined') { + chart = this.chartFromMembersCreate(service, obj, id, title_suffix, units, family, context, type, priority, algorithm, multiplier, divisor); + if(chart === null) return false; + } + else { + // check if we need to re-generate the chart + for(var x in obj) { + if(typeof(chart.dimensions[x]) === 'undefined') { + chart = this.chartFromMembersCreate(service, obj, id, title_suffix, units, family, context, type, priority, algorithm, multiplier, divisor); + if(chart === null) return false; + break; + } + } + } + + var found = 0; + service.begin(chart); + for(var x in obj) { + if(typeof(chart.dimensions[x]) !== 'undefined') { + found++; + service.set(x, obj[x]); + } + } + service.end(); + + if(found > 0) return true; + return false; + }, + + // an index to map values to different charts + lookups: { + nsstats: {}, + resolver_stats: {}, + numfetch: {} + }, + + // transform the XML response of bind + // to the JSON response of bind + xml2js: function(service, data_xml) { + var d = XML.parse(data_xml); + if(d === null) return null; + + var data = {}; + var len = d.server.counters.length; + while(len--) { + var a = d.server.counters[len]; + if(typeof a.counter === 'undefined') continue; + if(a.type === 'opcode') a.type = 'opcodes'; + else if(a.type === 'qtype') a.type = 'qtypes'; + else if(a.type === 'nsstat') a.type = 'nsstats'; + var aa = data[a.type] = {}; + var alen = 0 + var alen2 = a.counter.length; + while(alen < alen2) { + aa[a.counter[alen].name] = parseInt(a.counter[alen]._Data); + alen++; + } + } + + data.views = {}; + var vlen = d.views.view.length; + while(vlen--) { + var vname = d.views.view[vlen].name; + data.views[vname] = { resolver: {} }; + var len = d.views.view[vlen].counters.length; + while(len--) { + var a = d.views.view[vlen].counters[len]; + if(typeof a.counter === 'undefined') continue; + if(a.type === 'resstats') a.type = 'stats'; + else if(a.type === 'resqtype') a.type = 'qtypes'; + else if(a.type === 'adbstat') a.type = 'adb'; + var aa = data.views[vname].resolver[a.type] = {}; + var alen = 0; + var alen2 = a.counter.length; + while(alen < alen2) { + aa[a.counter[alen].name] = parseInt(a.counter[alen]._Data); + alen++; + } + } + } + + return data; + }, + + processResponse: function(service, data) { + if(data !== null) { + var r; + + // parse XML or JSON + // pepending on the URL given + if(service.request.path.match(/^\/xml/) !== null) + r = named.xml2js(service, data); + else + r = JSON.parse(data); + + if(typeof r === 'undefined' || r === null) { + netdata.serviceError(service, "Cannot parse these data: " + data); + return; + } + + if(service.added !== true) + service.commit(); + + if(typeof r.nsstats !== 'undefined') { + // we split the nsstats object to several others + var global_requests = {}, global_requests_enable = false; + var global_failures = {}, global_failures_enable = false; + var global_failures_detail = {}, global_failures_detail_enable = false; + var global_updates = {}, global_updates_enable = false; + var protocol_queries = {}, protocol_queries_enable = false; + var global_queries = {}, global_queries_enable = false; + var global_queries_success = {}, global_queries_success_enable = false; + var default_enable = false; + var RecursClients = 0; + + // RecursClients is an absolute value + if(typeof r.nsstats['RecursClients'] !== 'undefined') { + RecursClients = r.nsstats['RecursClients']; + delete r.nsstats['RecursClients']; + } + + for( var x in r.nsstats ) { + // we maintain an index of the values found + // mapping them to objects splitted + + var look = named.lookups.nsstats[x]; + if(typeof look === 'undefined') { + // a new value, not found in the index + // index it: + if(x === 'Requestv4') { + named.lookups.nsstats[x] = { + name: 'IPv4', + type: 'global_requests' + }; + } + else if(x === 'Requestv6') { + named.lookups.nsstats[x] = { + name: 'IPv6', + type: 'global_requests' + }; + } + else if(x === 'QryFailure') { + named.lookups.nsstats[x] = { + name: 'failures', + type: 'global_failures' + }; + } + else if(x === 'QryUDP') { + named.lookups.nsstats[x] = { + name: 'UDP', + type: 'protocol_queries' + }; + } + else if(x === 'QryTCP') { + named.lookups.nsstats[x] = { + name: 'TCP', + type: 'protocol_queries' + }; + } + else if(x === 'QrySuccess') { + named.lookups.nsstats[x] = { + name: 'queries', + type: 'global_queries_success' + }; + } + else if(x.match(/QryRej$/) !== null) { + named.lookups.nsstats[x] = { + name: x, + type: 'global_failures_detail' + }; + } + else if(x.match(/^Qry/) !== null) { + named.lookups.nsstats[x] = { + name: x, + type: 'global_queries' + }; + } + else if(x.match(/^Update/) !== null) { + named.lookups.nsstats[x] = { + name: x, + type: 'global_updates' + }; + } + else { + // values not mapped, will remain + // in the default map + named.lookups.nsstats[x] = { + name: x, + type: 'default' + }; + } + + look = named.lookups.nsstats[x]; + // netdata.error('lookup nsstats value: ' + x + ' >>> ' + named.lookups.nsstats[x].type); + } + + switch(look.type) { + case 'global_requests': global_requests[look.name] = r.nsstats[x]; delete r.nsstats[x]; global_requests_enable = true; break; + case 'global_queries': global_queries[look.name] = r.nsstats[x]; delete r.nsstats[x]; global_queries_enable = true; break; + case 'global_queries_success': global_queries_success[look.name] = r.nsstats[x]; delete r.nsstats[x]; global_queries_success_enable = true; break; + case 'global_updates': global_updates[look.name] = r.nsstats[x]; delete r.nsstats[x]; global_updates_enable = true; break; + case 'protocol_queries': protocol_queries[look.name] = r.nsstats[x]; delete r.nsstats[x]; protocol_queries_enable = true; break; + case 'global_failures': global_failures[look.name] = r.nsstats[x]; delete r.nsstats[x]; global_failures_enable = true; break; + case 'global_failures_detail': global_failures_detail[look.name] = r.nsstats[x]; delete r.nsstats[x]; global_failures_detail_enable = true; break; + default: default_enable = true; break; + } + } + + if(global_requests_enable == true) + service.module.chartFromMembers(service, global_requests, 'received_requests', 'Bind, Global Received Requests by IP version', 'requests/s', 'requests', 'named.requests', netdata.chartTypes.stacked, named.base_priority + 1, netdata.chartAlgorithms.incremental, 1, 1); + + if(global_queries_success_enable == true) + service.module.chartFromMembers(service, global_queries_success, 'global_queries_success', 'Bind, Global Successful Queries', 'queries/s', 'queries', 'named.queries.succcess', netdata.chartTypes.line, named.base_priority + 2, netdata.chartAlgorithms.incremental, 1, 1); + + if(protocol_queries_enable == true) + service.module.chartFromMembers(service, protocol_queries, 'protocols_queries', 'Bind, Global Queries by IP Protocol', 'queries/s', 'queries', 'named.protocol.queries', netdata.chartTypes.stacked, named.base_priority + 3, netdata.chartAlgorithms.incremental, 1, 1); + + if(global_queries_enable == true) + service.module.chartFromMembers(service, global_queries, 'global_queries', 'Bind, Global Queries Analysis', 'queries/s', 'queries', 'named.global.queries', netdata.chartTypes.stacked, named.base_priority + 4, netdata.chartAlgorithms.incremental, 1, 1); + + if(global_updates_enable == true) + service.module.chartFromMembers(service, global_updates, 'received_updates', 'Bind, Global Received Updates', 'updates/s', 'updates', 'named.global.updates', netdata.chartTypes.stacked, named.base_priority + 5, netdata.chartAlgorithms.incremental, 1, 1); + + if(global_failures_enable == true) + service.module.chartFromMembers(service, global_failures, 'query_failures', 'Bind, Global Query Failures', 'failures/s', 'failures', 'named.global.failures', netdata.chartTypes.line, named.base_priority + 6, netdata.chartAlgorithms.incremental, 1, 1); + + if(global_failures_detail_enable == true) + service.module.chartFromMembers(service, global_failures_detail, 'query_failures_detail', 'Bind, Global Query Failures Analysis', 'failures/s', 'failures', 'named.global.failures.detail', netdata.chartTypes.stacked, named.base_priority + 7, netdata.chartAlgorithms.incremental, 1, 1); + + if(default_enable === true) + service.module.chartFromMembers(service, r.nsstats, 'nsstats', 'Bind, Other Global Server Statistics', 'operations/s', 'other', 'named.nsstats', netdata.chartTypes.line, named.base_priority + 8, netdata.chartAlgorithms.incremental, 1, 1); + + // RecursClients chart + { + var id = 'named_' + service.name + '.recursive_clients'; + var chart = named.charts[id]; + + if(typeof chart === 'undefined') { + chart = { + id: id, // the unique id of the chart + name: '', // the unique name of the chart + title: service.name + ' Bind, Current Recursive Clients', // the title of the chart + units: 'clients', // the units of the chart dimensions + family: 'clients', // the family of the chart + context: 'named.recursive.clients', // the context of the chart + type: netdata.chartTypes.line, // the type of the chart + priority: named.base_priority + 1, // the priority relative to others in the same family + update_every: service.update_every, // the expected update frequency of the chart + dimensions: { + 'clients': { + id: 'clients', // the unique id of the dimension + name: '', // the name of the dimension + algorithm: netdata.chartAlgorithms.absolute,// the id of the netdata algorithm + multiplier: 1, // the multiplier + divisor: 1, // the divisor + hidden: false // is hidden (boolean) + } + } + }; + + chart = service.chart(id, chart); + named.charts[id] = chart; + } + + service.begin(chart); + service.set('clients', RecursClients); + service.end(); + } + } + + if(typeof r.opcodes !== 'undefined') + service.module.chartFromMembers(service, r.opcodes, 'in_opcodes', 'Bind, Global Incoming Requests by OpCode', 'requests/s', 'requests', 'named.in.opcodes', netdata.chartTypes.stacked, named.base_priority + 9, netdata.chartAlgorithms.incremental, 1, 1); + + if(typeof r.qtypes !== 'undefined') + service.module.chartFromMembers(service, r.qtypes, 'in_qtypes', 'Bind, Global Incoming Requests by Query Type', 'requests/s', 'requests', 'named.in.qtypes', netdata.chartTypes.stacked, named.base_priority + 10, netdata.chartAlgorithms.incremental, 1, 1); + + if(typeof r.sockstats !== 'undefined') + service.module.chartFromMembers(service, r.sockstats, 'in_sockstats', 'Bind, Global Socket Statistics', 'operations/s', 'sockets', 'named.in.sockstats', netdata.chartTypes.line, named.base_priority + 11, netdata.chartAlgorithms.incremental, 1, 1); + + if(typeof r.views !== 'undefined') { + for( var x in r.views ) { + var resolver = r.views[x].resolver; + + if(typeof resolver !== 'undefined') { + if(typeof resolver.stats !== 'undefined') { + var NumFetch = 0; + var key = service.name + '.' + x; + var default_enable = false; + var rtt = {}, rtt_enable = false; + + // NumFetch is an absolute value + if(typeof resolver.stats['NumFetch'] !== 'undefined') { + named.lookups.numfetch[key] = true; + NumFetch = resolver.stats['NumFetch']; + delete resolver.stats['NumFetch']; + } + if(typeof resolver.stats['BucketSize'] !== 'undefined') { + delete resolver.stats['BucketSize']; + } + + // split the QryRTT* from the main chart + for( var y in resolver.stats ) { + // we maintain an index of the values found + // mapping them to objects splitted + + var look = named.lookups.resolver_stats[y]; + if(typeof look === 'undefined') { + if(y.match(/^QryRTT/) !== null) { + named.lookups.resolver_stats[y] = { + name: y, + type: 'rtt' + }; + } + else { + named.lookups.resolver_stats[y] = { + name: y, + type: 'default' + }; + } + + look = named.lookups.resolver_stats[y]; + // netdata.error('lookup resolver stats value: ' + y + ' >>> ' + look.type); + } + + switch(look.type) { + case 'rtt': rtt[look.name] = resolver.stats[y]; delete resolver.stats[y]; rtt_enable = true; break; + default: default_enable = true; break; + } + } + + if(rtt_enable) + service.module.chartFromMembers(service, rtt, 'view_resolver_rtt_' + x, 'Bind, ' + x + ' View, Resolver Round Trip Timings', 'queries/s', 'view_' + x, 'named.resolver.rtt', netdata.chartTypes.stacked, named.base_priority + 12, netdata.chartAlgorithms.incremental, 1, 1); + + if(default_enable) + service.module.chartFromMembers(service, resolver.stats, 'view_resolver_stats_' + x, 'Bind, ' + x + ' View, Resolver Statistics', 'operations/s', 'view_' + x, 'named.resolver.stats', netdata.chartTypes.line, named.base_priority + 13, netdata.chartAlgorithms.incremental, 1, 1); + + // NumFetch chart + if(typeof named.lookups.numfetch[key] !== 'undefined') { + var id = 'named_' + service.name + '.view_resolver_numfetch_' + x; + var chart = named.charts[id]; + + if(typeof chart === 'undefined') { + chart = { + id: id, // the unique id of the chart + name: '', // the unique name of the chart + title: service.name + ' Bind, ' + x + ' View, Resolver Active Queries', // the title of the chart + units: 'queries', // the units of the chart dimensions + family: 'view_' + x, // the family of the chart + context: 'named.resolver.active.queries', // the context of the chart + type: netdata.chartTypes.line, // the type of the chart + priority: named.base_priority + 1001, // the priority relative to others in the same family + update_every: service.update_every, // the expected update frequency of the chart + dimensions: { + 'queries': { + id: 'queries', // the unique id of the dimension + name: '', // the name of the dimension + algorithm: netdata.chartAlgorithms.absolute,// the id of the netdata algorithm + multiplier: 1, // the multiplier + divisor: 1, // the divisor + hidden: false // is hidden (boolean) + } + } + }; + + chart = service.chart(id, chart); + named.charts[id] = chart; + } + + service.begin(chart); + service.set('queries', NumFetch); + service.end(); + } + } + } + + if(typeof resolver.qtypes !== 'undefined') + service.module.chartFromMembers(service, resolver.qtypes, 'view_resolver_qtypes_' + x, 'Bind, ' + x + ' View, Requests by Query Type', 'requests/s', 'view_' + x, 'named.resolver.qtypes', netdata.chartTypes.stacked, named.base_priority + 14, netdata.chartAlgorithms.incremental, 1, 1); + + //if(typeof resolver.cache !== 'undefined') + // service.module.chartFromMembers(service, resolver.cache, 'view_resolver_cache_' + x, 'Bind, ' + x + ' View, Cache Entries', 'entries', 'view_' + x, 'named.resolver.cache', netdata.chartTypes.stacked, named.base_priority + 15, netdata.chartAlgorithms.absolute, 1, 1); + + if(typeof resolver.cachestats['CacheHits'] !== 'undefined' && resolver.cachestats['CacheHits'] > 0) { + var id = 'named_' + service.name + '.view_resolver_cachehits_' + x; + var chart = named.charts[id]; + + if(typeof chart === 'undefined') { + chart = { + id: id, // the unique id of the chart + name: '', // the unique name of the chart + title: service.name + ' Bind, ' + x + ' View, Resolver Cache Hits', // the title of the chart + units: 'operations/s', // the units of the chart dimensions + family: 'view_' + x, // the family of the chart + context: 'named.resolver.cache.hits', // the context of the chart + type: netdata.chartTypes.area, // the type of the chart + priority: named.base_priority + 1100, // the priority relative to others in the same family + update_every: service.update_every, // the expected update frequency of the chart + dimensions: { + 'CacheHits': { + id: 'CacheHits', // the unique id of the dimension + name: 'hits', // the name of the dimension + algorithm: netdata.chartAlgorithms.incremental,// the id of the netdata algorithm + multiplier: 1, // the multiplier + divisor: 1, // the divisor + hidden: false // is hidden (boolean) + }, + 'CacheMisses': { + id: 'CacheMisses', // the unique id of the dimension + name: 'misses', // the name of the dimension + algorithm: netdata.chartAlgorithms.incremental,// the id of the netdata algorithm + multiplier: -1, // the multiplier + divisor: 1, // the divisor + hidden: false // is hidden (boolean) + } + } + }; + + chart = service.chart(id, chart); + named.charts[id] = chart; + } + + service.begin(chart); + service.set('CacheHits', resolver.cachestats['CacheHits']); + service.set('CacheMisses', resolver.cachestats['CacheMisses']); + service.end(); + } + + // this is wrong, it contains many types of info: + // 1. CacheHits, CacheMisses - incremental (added above) + // 2. QueryHits, QueryMisses - incremental + // 3. DeleteLRU, DeleteTTL - incremental + // 4. CacheNodes, CacheBuckets - absolute + // 5. TreeMemTotal, TreeMemInUse - absolute + // 6. HeapMemMax, HeapMemTotal, HeapMemInUse - absolute + //if(typeof resolver.cachestats !== 'undefined') + // service.module.chartFromMembers(service, resolver.cachestats, 'view_resolver_cachestats_' + x, 'Bind, ' + x + ' View, Cache Statistics', 'requests/s', 'view_' + x, 'named.resolver.cache.stats', netdata.chartTypes.line, named.base_priority + 1001, netdata.chartAlgorithms.incremental, 1, 1); + + //if(typeof resolver.adb !== 'undefined') + // service.module.chartFromMembers(service, resolver.adb, 'view_resolver_adb_' + x, 'Bind, ' + x + ' View, ADB Statistics', 'entries', 'view_' + x, 'named.resolver.adb', netdata.chartTypes.line, named.base_priority + 1002, netdata.chartAlgorithms.absolute, 1, 1); + } + } + } + }, + + // module.serviceExecute() + // this function is called only from this module + // its purpose is to prepare the request and call + // netdata.serviceExecute() + serviceExecute: function(name, a_url, update_every) { + if(netdata.options.DEBUG === true) netdata.debug(this.name + ': ' + name + ': url: ' + a_url + ', update_every: ' + update_every); + var service = netdata.service({ + name: name, + request: netdata.requestFromURL(a_url), + update_every: update_every, + module: this + }); + + service.execute(this.processResponse); + }, + + configure: function(config) { + var added = 0; + + if(this.enable_autodetect === true) { + this.serviceExecute('local', 'http://localhost:8888/json/v1/server', this.update_every); + added++; + } + + if(typeof(config.servers) !== 'undefined') { + var len = config.servers.length; + while(len--) { + if(typeof config.servers[len].update_every === 'undefined') + config.servers[len].update_every = this.update_every; + + this.serviceExecute(config.servers[len].name, config.servers[len].url, config.servers[len].update_every); + added++; + } + } + + return added; + }, + + // module.update() + // this is called repeatidly to collect data, by calling + // netdata.serviceExecute() + update: function(service, callback) { + service.execute(function(serv, data) { + service.module.processResponse(serv, data); + callback(); + }); + }, +}; + +module.exports = named; diff --git a/node.d/node_modules/asn1.js b/node.d/node_modules/asn1.js new file mode 100644 index 000000000..d1766e7a6 --- /dev/null +++ b/node.d/node_modules/asn1.js @@ -0,0 +1,20 @@ +// Copyright 2011 Mark Cavage <mcavage@gmail.com> All rights reserved. + +// If you have no idea what ASN.1 or BER is, see this: +// ftp://ftp.rsa.com/pub/pkcs/ascii/layman.asc + +var Ber = require('./ber/index'); + + + +///--- Exported API + +module.exports = { + + Ber: Ber, + + BerReader: Ber.Reader, + + BerWriter: Ber.Writer + +}; diff --git a/node.d/node_modules/ber/errors.js b/node.d/node_modules/ber/errors.js new file mode 100644 index 000000000..ff21d4fab --- /dev/null +++ b/node.d/node_modules/ber/errors.js @@ -0,0 +1,13 @@ +// Copyright 2011 Mark Cavage <mcavage@gmail.com> All rights reserved. + + +module.exports = { + + newInvalidAsn1Error: function(msg) { + var e = new Error(); + e.name = 'InvalidAsn1Error'; + e.message = msg || ''; + return e; + } + +}; diff --git a/node.d/node_modules/ber/index.js b/node.d/node_modules/ber/index.js new file mode 100644 index 000000000..4fb90aea9 --- /dev/null +++ b/node.d/node_modules/ber/index.js @@ -0,0 +1,27 @@ +// Copyright 2011 Mark Cavage <mcavage@gmail.com> All rights reserved. + +var errors = require('./errors'); +var types = require('./types'); + +var Reader = require('./reader'); +var Writer = require('./writer'); + + +///--- Exports + +module.exports = { + + Reader: Reader, + + Writer: Writer + +}; + +for (var t in types) { + if (types.hasOwnProperty(t)) + module.exports[t] = types[t]; +} +for (var e in errors) { + if (errors.hasOwnProperty(e)) + module.exports[e] = errors[e]; +} diff --git a/node.d/node_modules/ber/reader.js b/node.d/node_modules/ber/reader.js new file mode 100644 index 000000000..0a00e98e3 --- /dev/null +++ b/node.d/node_modules/ber/reader.js @@ -0,0 +1,261 @@ +// Copyright 2011 Mark Cavage <mcavage@gmail.com> All rights reserved. + +var assert = require('assert'); + +var ASN1 = require('./types'); +var errors = require('./errors'); + + +///--- Globals + +var newInvalidAsn1Error = errors.newInvalidAsn1Error; + + + +///--- API + +function Reader(data) { + if (!data || !Buffer.isBuffer(data)) + throw new TypeError('data must be a node Buffer'); + + this._buf = data; + this._size = data.length; + + // These hold the "current" state + this._len = 0; + this._offset = 0; +} + +Object.defineProperty(Reader.prototype, 'length', { + enumerable: true, + get: function () { return (this._len); } +}); + +Object.defineProperty(Reader.prototype, 'offset', { + enumerable: true, + get: function () { return (this._offset); } +}); + +Object.defineProperty(Reader.prototype, 'remain', { + get: function () { return (this._size - this._offset); } +}); + +Object.defineProperty(Reader.prototype, 'buffer', { + get: function () { return (this._buf.slice(this._offset)); } +}); + + +/** + * Reads a single byte and advances offset; you can pass in `true` to make this + * a "peek" operation (i.e., get the byte, but don't advance the offset). + * + * @param {Boolean} peek true means don't move offset. + * @return {Number} the next byte, null if not enough data. + */ +Reader.prototype.readByte = function(peek) { + if (this._size - this._offset < 1) + return null; + + var b = this._buf[this._offset] & 0xff; + + if (!peek) + this._offset += 1; + + return b; +}; + + +Reader.prototype.peek = function() { + return this.readByte(true); +}; + + +/** + * Reads a (potentially) variable length off the BER buffer. This call is + * not really meant to be called directly, as callers have to manipulate + * the internal buffer afterwards. + * + * As a result of this call, you can call `Reader.length`, until the + * next thing called that does a readLength. + * + * @return {Number} the amount of offset to advance the buffer. + * @throws {InvalidAsn1Error} on bad ASN.1 + */ +Reader.prototype.readLength = function(offset) { + if (offset === undefined) + offset = this._offset; + + if (offset >= this._size) + return null; + + var lenB = this._buf[offset++] & 0xff; + if (lenB === null) + return null; + + if ((lenB & 0x80) == 0x80) { + lenB &= 0x7f; + + if (lenB == 0) + throw newInvalidAsn1Error('Indefinite length not supported'); + + if (lenB > 4) + throw newInvalidAsn1Error('encoding too long'); + + if (this._size - offset < lenB) + return null; + + this._len = 0; + for (var i = 0; i < lenB; i++) + this._len = (this._len << 8) + (this._buf[offset++] & 0xff); + + } else { + // Wasn't a variable length + this._len = lenB; + } + + return offset; +}; + + +/** + * Parses the next sequence in this BER buffer. + * + * To get the length of the sequence, call `Reader.length`. + * + * @return {Number} the sequence's tag. + */ +Reader.prototype.readSequence = function(tag) { + var seq = this.peek(); + if (seq === null) + return null; + if (tag !== undefined && tag !== seq) + throw newInvalidAsn1Error('Expected 0x' + tag.toString(16) + + ': got 0x' + seq.toString(16)); + + var o = this.readLength(this._offset + 1); // stored in `length` + if (o === null) + return null; + + this._offset = o; + return seq; +}; + + +Reader.prototype.readInt = function() { + return this._readTag(ASN1.Integer); +}; + + +Reader.prototype.readBoolean = function() { + return (this._readTag(ASN1.Boolean) === 0 ? false : true); +}; + + +Reader.prototype.readEnumeration = function() { + return this._readTag(ASN1.Enumeration); +}; + + +Reader.prototype.readString = function(tag, retbuf) { + if (!tag) + tag = ASN1.OctetString; + + var b = this.peek(); + if (b === null) + return null; + + if (b !== tag) + throw newInvalidAsn1Error('Expected 0x' + tag.toString(16) + + ': got 0x' + b.toString(16)); + + var o = this.readLength(this._offset + 1); // stored in `length` + + if (o === null) + return null; + + if (this.length > this._size - o) + return null; + + this._offset = o; + + if (this.length === 0) + return retbuf ? new Buffer(0) : ''; + + var str = this._buf.slice(this._offset, this._offset + this.length); + this._offset += this.length; + + return retbuf ? str : str.toString('utf8'); +}; + +Reader.prototype.readOID = function(tag) { + if (!tag) + tag = ASN1.OID; + + var b = this.readString(tag, true); + if (b === null) + return null; + + var values = []; + var value = 0; + + for (var i = 0; i < b.length; i++) { + var byte = b[i] & 0xff; + + value <<= 7; + value += byte & 0x7f; + if ((byte & 0x80) == 0) { + values.push(value); + value = 0; + } + } + + value = values.shift(); + values.unshift(value % 40); + values.unshift((value / 40) >> 0); + + return values.join('.'); +}; + + +Reader.prototype._readTag = function(tag) { + assert.ok(tag !== undefined); + + var b = this.peek(); + + if (b === null) + return null; + + if (b !== tag) + throw newInvalidAsn1Error('Expected 0x' + tag.toString(16) + + ': got 0x' + b.toString(16)); + + var o = this.readLength(this._offset + 1); // stored in `length` + if (o === null) + return null; + + if (this.length > 4) + throw newInvalidAsn1Error('Integer too long: ' + this.length); + + if (this.length > this._size - o) + return null; + this._offset = o; + + var fb = this._buf[this._offset]; + var value = 0; + + for (var i = 0; i < this.length; i++) { + value <<= 8; + value |= (this._buf[this._offset++] & 0xff); + } + + if ((fb & 0x80) == 0x80 && i !== 4) + value -= (1 << (i * 8)); + + return value >> 0; +}; + + + +///--- Exported API + +module.exports = Reader; diff --git a/node.d/node_modules/ber/types.js b/node.d/node_modules/ber/types.js new file mode 100644 index 000000000..8aea00013 --- /dev/null +++ b/node.d/node_modules/ber/types.js @@ -0,0 +1,36 @@ +// Copyright 2011 Mark Cavage <mcavage@gmail.com> All rights reserved. + + +module.exports = { + EOC: 0, + Boolean: 1, + Integer: 2, + BitString: 3, + OctetString: 4, + Null: 5, + OID: 6, + ObjectDescriptor: 7, + External: 8, + Real: 9, // float + Enumeration: 10, + PDV: 11, + Utf8String: 12, + RelativeOID: 13, + Sequence: 16, + Set: 17, + NumericString: 18, + PrintableString: 19, + T61String: 20, + VideotexString: 21, + IA5String: 22, + UTCTime: 23, + GeneralizedTime: 24, + GraphicString: 25, + VisibleString: 26, + GeneralString: 28, + UniversalString: 29, + CharacterString: 30, + BMPString: 31, + Constructor: 32, + Context: 128 +}; diff --git a/node.d/node_modules/ber/writer.js b/node.d/node_modules/ber/writer.js new file mode 100644 index 000000000..d9d99af68 --- /dev/null +++ b/node.d/node_modules/ber/writer.js @@ -0,0 +1,316 @@ +// Copyright 2011 Mark Cavage <mcavage@gmail.com> All rights reserved. + +var assert = require('assert'); +var ASN1 = require('./types'); +var errors = require('./errors'); + + +///--- Globals + +var newInvalidAsn1Error = errors.newInvalidAsn1Error; + +var DEFAULT_OPTS = { + size: 1024, + growthFactor: 8 +}; + + +///--- Helpers + +function merge(from, to) { + assert.ok(from); + assert.equal(typeof(from), 'object'); + assert.ok(to); + assert.equal(typeof(to), 'object'); + + var keys = Object.getOwnPropertyNames(from); + keys.forEach(function(key) { + if (to[key]) + return; + + var value = Object.getOwnPropertyDescriptor(from, key); + Object.defineProperty(to, key, value); + }); + + return to; +} + + + +///--- API + +function Writer(options) { + options = merge(DEFAULT_OPTS, options || {}); + + this._buf = new Buffer(options.size || 1024); + this._size = this._buf.length; + this._offset = 0; + this._options = options; + + // A list of offsets in the buffer where we need to insert + // sequence tag/len pairs. + this._seq = []; +} + +Object.defineProperty(Writer.prototype, 'buffer', { + get: function () { + if (this._seq.length) + throw new InvalidAsn1Error(this._seq.length + ' unended sequence(s)'); + + return (this._buf.slice(0, this._offset)); + } +}); + +Writer.prototype.writeByte = function(b) { + if (typeof(b) !== 'number') + throw new TypeError('argument must be a Number'); + + this._ensure(1); + this._buf[this._offset++] = b; +}; + + +Writer.prototype.writeInt = function(i, tag) { + if (typeof(i) !== 'number') + throw new TypeError('argument must be a Number'); + if (typeof(tag) !== 'number') + tag = ASN1.Integer; + + var sz = 4; + + while ((((i & 0xff800000) === 0) || ((i & 0xff800000) === 0xff800000 >> 0)) && + (sz > 1)) { + sz--; + i <<= 8; + } + + if (sz > 4) + throw new InvalidAsn1Error('BER ints cannot be > 0xffffffff'); + + this._ensure(2 + sz); + this._buf[this._offset++] = tag; + this._buf[this._offset++] = sz; + + while (sz-- > 0) { + this._buf[this._offset++] = ((i & 0xff000000) >>> 24); + i <<= 8; + } + +}; + + +Writer.prototype.writeNull = function() { + this.writeByte(ASN1.Null); + this.writeByte(0x00); +}; + + +Writer.prototype.writeEnumeration = function(i, tag) { + if (typeof(i) !== 'number') + throw new TypeError('argument must be a Number'); + if (typeof(tag) !== 'number') + tag = ASN1.Enumeration; + + return this.writeInt(i, tag); +}; + + +Writer.prototype.writeBoolean = function(b, tag) { + if (typeof(b) !== 'boolean') + throw new TypeError('argument must be a Boolean'); + if (typeof(tag) !== 'number') + tag = ASN1.Boolean; + + this._ensure(3); + this._buf[this._offset++] = tag; + this._buf[this._offset++] = 0x01; + this._buf[this._offset++] = b ? 0xff : 0x00; +}; + + +Writer.prototype.writeString = function(s, tag) { + if (typeof(s) !== 'string') + throw new TypeError('argument must be a string (was: ' + typeof(s) + ')'); + if (typeof(tag) !== 'number') + tag = ASN1.OctetString; + + var len = Buffer.byteLength(s); + this.writeByte(tag); + this.writeLength(len); + if (len) { + this._ensure(len); + this._buf.write(s, this._offset); + this._offset += len; + } +}; + + +Writer.prototype.writeBuffer = function(buf, tag) { + if (typeof(tag) !== 'number') + throw new TypeError('tag must be a number'); + if (!Buffer.isBuffer(buf)) + throw new TypeError('argument must be a buffer'); + + this.writeByte(tag); + this.writeLength(buf.length); + this._ensure(buf.length); + buf.copy(this._buf, this._offset, 0, buf.length); + this._offset += buf.length; +}; + + +Writer.prototype.writeStringArray = function(strings) { + if ((!strings instanceof Array)) + throw new TypeError('argument must be an Array[String]'); + + var self = this; + strings.forEach(function(s) { + self.writeString(s); + }); +}; + +// This is really to solve DER cases, but whatever for now +Writer.prototype.writeOID = function(s, tag) { + if (typeof(s) !== 'string') + throw new TypeError('argument must be a string'); + if (typeof(tag) !== 'number') + tag = ASN1.OID; + + if (!/^([0-9]+\.){3,}[0-9]+$/.test(s)) + throw new Error('argument is not a valid OID string'); + + function encodeOctet(bytes, octet) { + if (octet < 128) { + bytes.push(octet); + } else if (octet < 16384) { + bytes.push((octet >>> 7) | 0x80); + bytes.push(octet & 0x7F); + } else if (octet < 2097152) { + bytes.push((octet >>> 14) | 0x80); + bytes.push(((octet >>> 7) | 0x80) & 0xFF); + bytes.push(octet & 0x7F); + } else if (octet < 268435456) { + bytes.push((octet >>> 21) | 0x80); + bytes.push(((octet >>> 14) | 0x80) & 0xFF); + bytes.push(((octet >>> 7) | 0x80) & 0xFF); + bytes.push(octet & 0x7F); + } else { + bytes.push(((octet >>> 28) | 0x80) & 0xFF); + bytes.push(((octet >>> 21) | 0x80) & 0xFF); + bytes.push(((octet >>> 14) | 0x80) & 0xFF); + bytes.push(((octet >>> 7) | 0x80) & 0xFF); + bytes.push(octet & 0x7F); + } + } + + var tmp = s.split('.'); + var bytes = []; + bytes.push(parseInt(tmp[0], 10) * 40 + parseInt(tmp[1], 10)); + tmp.slice(2).forEach(function(b) { + encodeOctet(bytes, parseInt(b, 10)); + }); + + var self = this; + this._ensure(2 + bytes.length); + this.writeByte(tag); + this.writeLength(bytes.length); + bytes.forEach(function(b) { + self.writeByte(b); + }); +}; + + +Writer.prototype.writeLength = function(len) { + if (typeof(len) !== 'number') + throw new TypeError('argument must be a Number'); + + this._ensure(4); + + if (len <= 0x7f) { + this._buf[this._offset++] = len; + } else if (len <= 0xff) { + this._buf[this._offset++] = 0x81; + this._buf[this._offset++] = len; + } else if (len <= 0xffff) { + this._buf[this._offset++] = 0x82; + this._buf[this._offset++] = len >> 8; + this._buf[this._offset++] = len; + } else if (len <= 0xffffff) { + this._buf[this._offset++] = 0x83; + this._buf[this._offset++] = len >> 16; + this._buf[this._offset++] = len >> 8; + this._buf[this._offset++] = len; + } else { + throw new InvalidAsn1ERror('Length too long (> 4 bytes)'); + } +}; + +Writer.prototype.startSequence = function(tag) { + if (typeof(tag) !== 'number') + tag = ASN1.Sequence | ASN1.Constructor; + + this.writeByte(tag); + this._seq.push(this._offset); + this._ensure(3); + this._offset += 3; +}; + + +Writer.prototype.endSequence = function() { + var seq = this._seq.pop(); + var start = seq + 3; + var len = this._offset - start; + + if (len <= 0x7f) { + this._shift(start, len, -2); + this._buf[seq] = len; + } else if (len <= 0xff) { + this._shift(start, len, -1); + this._buf[seq] = 0x81; + this._buf[seq + 1] = len; + } else if (len <= 0xffff) { + this._buf[seq] = 0x82; + this._buf[seq + 1] = len >> 8; + this._buf[seq + 2] = len; + } else if (len <= 0xffffff) { + this._shift(start, len, 1); + this._buf[seq] = 0x83; + this._buf[seq + 1] = len >> 16; + this._buf[seq + 2] = len >> 8; + this._buf[seq + 3] = len; + } else { + throw new InvalidAsn1Error('Sequence too long'); + } +}; + + +Writer.prototype._shift = function(start, len, shift) { + assert.ok(start !== undefined); + assert.ok(len !== undefined); + assert.ok(shift); + + this._buf.copy(this._buf, start + shift, start, start + len); + this._offset += shift; +}; + +Writer.prototype._ensure = function(len) { + assert.ok(len); + + if (this._size - this._offset < len) { + var sz = this._size * this._options.growthFactor; + if (sz - this._offset < len) + sz += len; + + var buf = new Buffer(sz); + + this._buf.copy(buf, 0, 0, this._offset); + this._buf = buf; + this._size = sz; + } +}; + + + +///--- Exported API + +module.exports = Writer; diff --git a/node.d/node_modules/extend.js b/node.d/node_modules/extend.js new file mode 100644 index 000000000..0fdd8be22 --- /dev/null +++ b/node.d/node_modules/extend.js @@ -0,0 +1,87 @@ +// https://github.com/justmoon/node-extend + +'use strict'; + +var hasOwn = Object.prototype.hasOwnProperty; +var toStr = Object.prototype.toString; + +var isArray = function isArray(arr) { + if (typeof Array.isArray === 'function') { + return Array.isArray(arr); + } + + return toStr.call(arr) === '[object Array]'; +}; + +var isPlainObject = function isPlainObject(obj) { + if (!obj || toStr.call(obj) !== '[object Object]') { + return false; + } + + var hasOwnConstructor = hasOwn.call(obj, 'constructor'); + var hasIsPrototypeOf = obj.constructor && obj.constructor.prototype && hasOwn.call(obj.constructor.prototype, 'isPrototypeOf'); + // Not own constructor property must be Object + if (obj.constructor && !hasOwnConstructor && !hasIsPrototypeOf) { + return false; + } + + // Own properties are enumerated firstly, so to speed up, + // if last one is own, then all properties are own. + var key; + for (key in obj) { /**/ } + + return typeof key === 'undefined' || hasOwn.call(obj, key); +}; + +module.exports = function extend() { + var options, name, src, copy, copyIsArray, clone; + var target = arguments[0]; + var i = 1; + var length = arguments.length; + var deep = false; + + // Handle a deep copy situation + if (typeof target === 'boolean') { + deep = target; + target = arguments[1] || {}; + // skip the boolean and the target + i = 2; + } else if ((typeof target !== 'object' && typeof target !== 'function') || target == null) { + target = {}; + } + + for (; i < length; ++i) { + options = arguments[i]; + // Only deal with non-null/undefined values + if (options != null) { + // Extend the base object + for (name in options) { + src = target[name]; + copy = options[name]; + + // Prevent never-ending loop + if (target !== copy) { + // Recurse if we're merging plain objects or arrays + if (deep && copy && (isPlainObject(copy) || (copyIsArray = isArray(copy)))) { + if (copyIsArray) { + copyIsArray = false; + clone = src && isArray(src) ? src : []; + } else { + clone = src && isPlainObject(src) ? src : {}; + } + + // Never move original objects, clone them + target[name] = extend(deep, clone, copy); + + // Don't bring in undefined values + } else if (typeof copy !== 'undefined') { + target[name] = copy; + } + } + } + } + } + + // Return the modified object + return target; +}; diff --git a/node.d/node_modules/net-snmp.js b/node.d/node_modules/net-snmp.js new file mode 100644 index 000000000..6fbd4e721 --- /dev/null +++ b/node.d/node_modules/net-snmp.js @@ -0,0 +1,1466 @@ + +// Copyright 2013 Stephen Vickers <stephen.vickers.sv@gmail.com> + +var ber = require ("asn1").Ber; +var dgram = require ("dgram"); +var events = require ("events"); +var util = require ("util"); + +/***************************************************************************** + ** Constants + **/ + +function _expandConstantObject (object) { + var keys = []; + for (key in object) + keys.push (key); + for (var i = 0; i < keys.length; i++) + object[object[keys[i]]] = parseInt (keys[i]); +} + +var ErrorStatus = { + 0: "NoError", + 1: "TooBig", + 2: "NoSuchName", + 3: "BadValue", + 4: "ReadOnly", + 5: "GeneralError", + 6: "NoAccess", + 7: "WrongType", + 8: "WrongLength", + 9: "WrongEncoding", + 10: "WrongValue", + 11: "NoCreation", + 12: "InconsistentValue", + 13: "ResourceUnavailable", + 14: "CommitFailed", + 15: "UndoFailed", + 16: "AuthorizationError", + 17: "NotWritable", + 18: "InconsistentName" +}; + +_expandConstantObject (ErrorStatus); + +var ObjectType = { + 1: "Boolean", + 2: "Integer", + 4: "OctetString", + 5: "Null", + 6: "OID", + 64: "IpAddress", + 65: "Counter", + 66: "Gauge", + 67: "TimeTicks", + 68: "Opaque", + 70: "Counter64", + 128: "NoSuchObject", + 129: "NoSuchInstance", + 130: "EndOfMibView" +}; + +_expandConstantObject (ObjectType); + +ObjectType.Integer32 = ObjectType.Integer; +ObjectType.Counter32 = ObjectType.Counter; +ObjectType.Gauge32 = ObjectType.Gauge; +ObjectType.Unsigned32 = ObjectType.Gauge32; + +var PduType = { + 160: "GetRequest", + 161: "GetNextRequest", + 162: "GetResponse", + 163: "SetRequest", + 164: "Trap", + 165: "GetBulkRequest", + 166: "InformRequest", + 167: "TrapV2", + 168: "Report" +}; + +_expandConstantObject (PduType); + +var TrapType = { + 0: "ColdStart", + 1: "WarmStart", + 2: "LinkDown", + 3: "LinkUp", + 4: "AuthenticationFailure", + 5: "EgpNeighborLoss", + 6: "EnterpriseSpecific" +}; + +_expandConstantObject (TrapType); + +var Version1 = 0; +var Version2c = 1; + +/***************************************************************************** + ** Exception class definitions + **/ + +function ResponseInvalidError (message) { + this.name = "ResponseInvalidError"; + this.message = message; + Error.captureStackTrace(this, ResponseInvalidError); +} +util.inherits (ResponseInvalidError, Error); + +function RequestInvalidError (message) { + this.name = "RequestInvalidError"; + this.message = message; + Error.captureStackTrace(this, RequestInvalidError); +} +util.inherits (RequestInvalidError, Error); + +function RequestFailedError (message, status) { + this.name = "RequestFailedError"; + this.message = message; + this.status = status; + Error.captureStackTrace(this, RequestFailedError); +} +util.inherits (RequestFailedError, Error); + +function RequestTimedOutError (message) { + this.name = "RequestTimedOutError"; + this.message = message; + Error.captureStackTrace(this, RequestTimedOutError); +} +util.inherits (RequestTimedOutError, Error); + +/***************************************************************************** + ** OID and varbind helper functions + **/ + +function isVarbindError (varbind) { + if (varbind.type == ObjectType.NoSuchObject + || varbind.type == ObjectType.NoSuchInstance + || varbind.type == ObjectType.EndOfMibView) + return true; + else + return false; +} + +function varbindError (varbind) { + return (ObjectType[varbind.type] || "NotAnError") + ": " + varbind.oid; +} + +function oidFollowsOid (oidString, nextString) { + var oid = {str: oidString, len: oidString.length, idx: 0}; + var next = {str: nextString, len: nextString.length, idx: 0}; + var dotCharCode = ".".charCodeAt (0); + + function getNumber (item) { + var n = 0; + if (item.idx >= item.len) + return null; + while (item.idx < item.len) { + var charCode = item.str.charCodeAt (item.idx++); + if (charCode == dotCharCode) + return n; + n = (n ? (n * 10) : n) + (charCode - 48); + } + return n; + } + + while (1) { + var oidNumber = getNumber (oid); + var nextNumber = getNumber (next); + + if (oidNumber !== null) { + if (nextNumber !== null) { + if (nextNumber > oidNumber) { + return true; + } else if (nextNumber < oidNumber) { + return false; + } + } else { + return true; + } + } else { + return true; + } + } +} + +function oidInSubtree (oidString, nextString) { + var oid = oidString.split ("."); + var next = nextString.split ("."); + + if (oid.length > next.length) + return false; + + for (var i = 0; i < oid.length; i++) { + if (next[i] != oid[i]) + return false; + } + + return true; +} + +/** + ** Some SNMP agents produce integers on the wire such as 00 ff ff ff ff. + ** The ASN.1 BER parser we use throws an error when parsing this, which we + ** believe is correct. So, we decided not to bother the "asn1" developer(s) + ** with this, instead opting to work around it here. + ** + ** If an integer is 5 bytes in length we check if the first byte is 0, and if so + ** simply drop it and parse it like it was a 4 byte integer, otherwise throw + ** an error since the integer is too large. + **/ + +function readInt (buffer) { + return readUint (buffer, true); +} + +function readUint (buffer, isSigned) { + buffer.readByte (); + var length = buffer.readByte (); + + if (length > 5) { + throw new RangeError ("Integer too long '" + length + "'"); + } else if (length == 5) { + if (buffer.readByte () !== 0) + throw new RangeError ("Integer too long '" + length + "'"); + length = 4; + } + + value = 0, signedBitSet = false; + + for (var i = 0; i < length; i++) { + value *= 256; + value += buffer.readByte (); + + if (isSigned && i <= 0) { + if ((value & 0x80) == 0x80) + signedBitSet = true; + } + } + + if (signedBitSet) + value -= (1 << (i * 8)); + + return value; +} + +function readUint64 (buffer) { + var value = buffer.readString (ObjectType.Counter64, true); + + if (value.length > 8) + throw new RequestInvalidError ("64 bit unsigned integer too long '" + + value.length + "'") + + return value; +} + +function readVarbinds (buffer, varbinds) { + buffer.readSequence (); + + while (1) { + buffer.readSequence (); + var oid = buffer.readOID (); + var type = buffer.peek (); + + if (type == null) + break; + + var value; + + if (type == ObjectType.Boolean) { + value = buffer.readBoolean (); + } else if (type == ObjectType.Integer) { + value = readInt (buffer); + } else if (type == ObjectType.OctetString) { + value = buffer.readString (null, true); + } else if (type == ObjectType.Null) { + buffer.readByte (); + buffer.readByte (); + value = null; + } else if (type == ObjectType.OID) { + value = buffer.readOID (); + } else if (type == ObjectType.IpAddress) { + var bytes = buffer.readString (ObjectType.IpAddress, true); + if (bytes.length != 4) + throw new ResponseInvalidError ("Length '" + bytes.length + + "' of IP address '" + bytes.toString ("hex") + + "' is not 4"); + value = bytes[0] + "." + bytes[1] + "." + bytes[2] + "." + bytes[3]; + } else if (type == ObjectType.Counter) { + value = readUint (buffer); + } else if (type == ObjectType.Gauge) { + value = readUint (buffer); + } else if (type == ObjectType.TimeTicks) { + value = readUint (buffer); + } else if (type == ObjectType.Opaque) { + value = buffer.readString (ObjectType.Opaque, true); + } else if (type == ObjectType.Counter64) { + value = readUint64 (buffer); + } else if (type == ObjectType.NoSuchObject) { + buffer.readByte (); + buffer.readByte (); + value = null; + } else if (type == ObjectType.NoSuchInstance) { + buffer.readByte (); + buffer.readByte (); + value = null; + } else if (type == ObjectType.EndOfMibView) { + buffer.readByte (); + buffer.readByte (); + value = null; + } else { + throw new ResponseInvalidError ("Unknown type '" + type + + "' in response"); + } + + varbinds.push ({ + oid: oid, + type: type, + value: value + }); + } +} + +function writeUint (buffer, type, value) { + var b = new Buffer (4); + b.writeUInt32BE (value, 0); + buffer.writeBuffer (b, type); +} + +function writeUint64 (buffer, value) { + if (value.length > 8) + throw new RequestInvalidError ("64 bit unsigned integer too long '" + + value.length + "'") + buffer.writeBuffer (value, ObjectType.Counter64); +} + +function writeVarbinds (buffer, varbinds) { + buffer.startSequence (); + for (var i = 0; i < varbinds.length; i++) { + buffer.startSequence (); + buffer.writeOID (varbinds[i].oid); + + if (varbinds[i].type && varbinds[i].hasOwnProperty("value")) { + var type = varbinds[i].type; + var value = varbinds[i].value; + + if (type == ObjectType.Boolean) { + buffer.writeBoolean (value ? true : false); + } else if (type == ObjectType.Integer) { // also Integer32 + buffer.writeInt (value); + } else if (type == ObjectType.OctetString) { + if (typeof value == "string") + buffer.writeString (value); + else + buffer.writeBuffer (value, ObjectType.OctetString); + } else if (type == ObjectType.Null) { + buffer.writeNull (); + } else if (type == ObjectType.OID) { + buffer.writeOID (value); + } else if (type == ObjectType.IpAddress) { + var bytes = value.split ("."); + if (bytes.length != 4) + throw new RequestInvalidError ("Invalid IP address '" + + value + "'"); + buffer.writeBuffer (new Buffer (bytes), 64); + } else if (type == ObjectType.Counter) { // also Counter32 + writeUint (buffer, ObjectType.Counter, value); + } else if (type == ObjectType.Gauge) { // also Gauge32 & Unsigned32 + writeUint (buffer, ObjectType.Gauge, value); + } else if (type == ObjectType.TimeTicks) { + writeUint (buffer, ObjectType.TimeTicks, value); + } else if (type == ObjectType.Opaque) { + buffer.writeBuffer (value, ObjectType.Opaque); + } else if (type == ObjectType.Counter64) { + writeUint64 (buffer, value); + } else { + throw new RequestInvalidError ("Unknown type '" + type + + "' in request"); + } + } else { + buffer.writeNull (); + } + + buffer.endSequence (); + }; + buffer.endSequence (); +} + +/***************************************************************************** + ** PDU class definitions + **/ + +var SimplePdu = function (id, varbinds, options) { + this.id = id; + this.varbinds = varbinds; + this.options = options || {}; +}; + +SimplePdu.prototype.toBuffer = function (buffer) { + buffer.startSequence (this.type); + + buffer.writeInt (this.id); + buffer.writeInt ((this.type == PduType.GetBulkRequest) + ? (this.options.nonRepeaters || 0) + : 0); + buffer.writeInt ((this.type == PduType.GetBulkRequest) + ? (this.options.maxRepetitions || 0) + : 0); + + writeVarbinds (buffer, this.varbinds); + + buffer.endSequence (); +}; + +var GetBulkRequestPdu = function () { + this.type = PduType.GetBulkRequest; + GetBulkRequestPdu.super_.apply (this, arguments); +}; + +util.inherits (GetBulkRequestPdu, SimplePdu); + +var GetNextRequestPdu = function () { + this.type = PduType.GetNextRequest; + GetNextRequestPdu.super_.apply (this, arguments); +}; + +util.inherits (GetNextRequestPdu, SimplePdu); + +var GetResponsePdu = function (buffer) { + this.type = PduType.GetResponse; + + buffer.readSequence (this.type); + + this.id = buffer.readInt (); + + this.errorStatus = buffer.readInt (); + this.errorIndex = buffer.readInt (); + + this.varbinds = []; + + readVarbinds (buffer, this.varbinds); +}; + +var GetRequestPdu = function () { + this.type = PduType.GetRequest; + GetRequestPdu.super_.apply (this, arguments); +}; + +util.inherits (GetRequestPdu, SimplePdu); + +var InformRequestPdu = function () { + this.type = PduType.InformRequest; + InformRequestPdu.super_.apply (this, arguments); +}; + +util.inherits (InformRequestPdu, SimplePdu); + +var SetRequestPdu = function () { + this.type = PduType.SetRequest; + SetRequestPdu.super_.apply (this, arguments); +}; + +util.inherits (SetRequestPdu, SimplePdu); + +var TrapPdu = function (typeOrOid, varbinds, options) { + this.type = PduType.Trap; + + this.agentAddr = options.agentAddr || "127.0.0.1"; + this.upTime = options.upTime; + + if (typeof typeOrOid == "string") { + this.generic = TrapType.EnterpriseSpecific; + this.specific = parseInt (typeOrOid.match (/\.(\d+)$/)[1]); + this.enterprise = typeOrOid.replace (/\.(\d+)$/, ""); + } else { + this.generic = typeOrOid; + this.specific = 0; + this.enterprise = "1.3.6.1.4.1"; + } + + this.varbinds = varbinds; +}; + +TrapPdu.prototype.toBuffer = function (buffer) { + buffer.startSequence (this.type); + + buffer.writeOID (this.enterprise); + buffer.writeBuffer (new Buffer (this.agentAddr.split (".")), + ObjectType.IpAddress); + buffer.writeInt (this.generic); + buffer.writeInt (this.specific); + writeUint (buffer, ObjectType.TimeTicks, + this.upTime || Math.floor (process.uptime () * 100)); + + writeVarbinds (buffer, this.varbinds); + + buffer.endSequence (); +}; + +var TrapV2Pdu = function () { + this.type = PduType.TrapV2; + TrapV2Pdu.super_.apply (this, arguments); +}; + +util.inherits (TrapV2Pdu, SimplePdu); + +/***************************************************************************** + ** Message class definitions + **/ + +var RequestMessage = function (version, community, pdu) { + this.version = version; + this.community = community; + this.pdu = pdu; +}; + +RequestMessage.prototype.toBuffer = function () { + if (this.buffer) + return this.buffer; + + var writer = new ber.Writer (); + + writer.startSequence (); + + writer.writeInt (this.version); + writer.writeString (this.community); + + this.pdu.toBuffer (writer); + + writer.endSequence (); + + this.buffer = writer.buffer; + + return this.buffer; +}; + +var ResponseMessage = function (buffer) { + var reader = new ber.Reader (buffer); + + reader.readSequence (); + + this.version = reader.readInt (); + this.community = reader.readString (); + + var type = reader.peek (); + + if (type == PduType.GetResponse) { + this.pdu = new GetResponsePdu (reader); + } else { + throw new ResponseInvalidError ("Unknown PDU type '" + type + + "' in response"); + } +} + +/***************************************************************************** + ** Session class definition + **/ + +var Session = function (target, community, options) { + this.target = target || "127.0.0.1"; + this.community = community || "public"; + + this.version = (options && options.version) + ? options.version + : Version1; + + this.transport = (options && options.transport) + ? options.transport + : "udp4"; + this.port = (options && options.port ) + ? options.port + : 161; + this.trapPort = (options && options.trapPort ) + ? options.trapPort + : 162; + + this.retries = (options && (options.retries || options.retries == 0)) + ? options.retries + : 1; + this.timeout = (options && options.timeout) + ? options.timeout + : 5000; + + this.sourceAddress = (options && options.sourceAddress ) + ? options.sourceAddress + : undefined; + this.sourcePort = (options && options.sourcePort ) + ? parseInt(options.sourcePort) + : undefined; + + this.reqs = {}; + this.reqCount = 0; + + this.dgram = dgram.createSocket (this.transport); + this.dgram.unref(); + + var me = this; + this.dgram.on ("message", me.onMsg.bind (me)); + this.dgram.on ("close", me.onClose.bind (me)); + this.dgram.on ("error", me.onError.bind (me)); + + if (this.sourceAddress || this.sourcePort) + req.dgram.bind (this.sourcePort, this.sourceAddress); +}; + +util.inherits (Session, events.EventEmitter); + +Session.prototype.close = function () { + this.dgram.close (); + return this; +} + +Session.prototype.cancelRequests = function (error) { + for (id in this.reqs) { + var req = this.reqs[id]; + this.unregisterRequest (req.id); + req.responseCb (error); + } +} + +function _generateId () { + return Math.floor (Math.random () + Math.random () * 10000000) +} + +Session.prototype.get = function (oids, responseCb) { + function feedCb (req, message) { + var pdu = message.pdu; + var varbinds = []; + + if (req.message.pdu.varbinds.length != pdu.varbinds.length) { + req.responseCb (new ResponseInvalidError ("Requested OIDs do not " + + "match response OIDs")); + } else { + for (var i = 0; i < req.message.pdu.varbinds.length; i++) { + if (req.message.pdu.varbinds[i].oid != pdu.varbinds[i].oid) { + req.responseCb (new ResponseInvalidError ("OID '" + + req.message.pdu.varbinds[i].oid + + "' in request at positiion '" + i + "' does not " + + "match OID '" + pdu.varbinds[i].oid + "' in response " + + "at position '" + i + "'")); + return; + } else { + varbinds.push (pdu.varbinds[i]); + } + } + + req.responseCb (null, varbinds); + } + }; + + var pduVarbinds = []; + + for (var i = 0; i < oids.length; i++) { + var varbind = { + oid: oids[i] + }; + pduVarbinds.push (varbind); + } + + this.simpleGet (GetRequestPdu, feedCb, pduVarbinds, responseCb); + + return this; +}; + +Session.prototype.getBulk = function () { + var oids, nonRepeaters, maxRepetitions, responseCb; + + if (arguments.length >= 4) { + oids = arguments[0]; + nonRepeaters = arguments[1]; + maxRepetitions = arguments[2]; + responseCb = arguments[3]; + } else if (arguments.length >= 3) { + oids = arguments[0]; + nonRepeaters = arguments[1]; + maxRepetitions = 10; + responseCb = arguments[2]; + } else { + oids = arguments[0]; + nonRepeaters = 0; + maxRepetitions = 10; + responseCb = arguments[1]; + } + + function feedCb (req, message) { + var pdu = message.pdu; + var varbinds = []; + var i = 0; + + // first walk through and grab non-repeaters + if (pdu.varbinds.length < nonRepeaters) { + req.responseCb (new ResponseInvalidError ("Varbind count in " + + "response '" + pdu.varbinds.length + "' is less than " + + "non-repeaters '" + nonRepeaters + "' in request")); + } else { + for ( ; i < nonRepeaters; i++) { + if (isVarbindError (pdu.varbinds[i])) { + varbinds.push (pdu.varbinds[i]); + } else if (! oidFollowsOid (req.message.pdu.varbinds[i].oid, + pdu.varbinds[i].oid)) { + req.responseCb (new ResponseInvalidError ("OID '" + + req.message.pdu.varbinds[i].oid + "' in request at " + + "positiion '" + i + "' does not precede " + + "OID '" + pdu.varbinds[i].oid + "' in response " + + "at position '" + i + "'")); + return; + } else { + varbinds.push (pdu.varbinds[i]); + } + } + } + + var repeaters = req.message.pdu.varbinds.length - nonRepeaters; + + // secondly walk through and grab repeaters + if (pdu.varbinds.length % (repeaters)) { + req.responseCb (new ResponseInvalidError ("Varbind count in " + + "response '" + pdu.varbinds.length + "' is not a " + + "multiple of repeaters '" + repeaters + + "' plus non-repeaters '" + nonRepeaters + "' in request")); + } else { + while (i < pdu.varbinds.length) { + for (var j = 0; j < repeaters; j++, i++) { + var reqIndex = nonRepeaters + j; + var respIndex = i; + + if (isVarbindError (pdu.varbinds[respIndex])) { + if (! varbinds[reqIndex]) + varbinds[reqIndex] = []; + varbinds[reqIndex].push (pdu.varbinds[respIndex]); + } else if (! oidFollowsOid ( + req.message.pdu.varbinds[reqIndex].oid, + pdu.varbinds[respIndex].oid)) { + req.responseCb (new ResponseInvalidError ("OID '" + + req.message.pdu.varbinds[reqIndex].oid + + "' in request at positiion '" + (reqIndex) + + "' does not precede OID '" + + pdu.varbinds[respIndex].oid + + "' in response at position '" + (respIndex) + "'")); + return; + } else { + if (! varbinds[reqIndex]) + varbinds[reqIndex] = []; + varbinds[reqIndex].push (pdu.varbinds[respIndex]); + } + } + } + } + + req.responseCb (null, varbinds); + }; + + var pduVarbinds = []; + + for (var i = 0; i < oids.length; i++) { + var varbind = { + oid: oids[i] + }; + pduVarbinds.push (varbind); + } + + var options = { + nonRepeaters: nonRepeaters, + maxRepetitions: maxRepetitions + }; + + this.simpleGet (GetBulkRequestPdu, feedCb, pduVarbinds, responseCb, + options); + + return this; +}; + +Session.prototype.getNext = function (oids, responseCb) { + function feedCb (req, message) { + var pdu = message.pdu; + var varbinds = []; + + if (req.message.pdu.varbinds.length != pdu.varbinds.length) { + req.responseCb (new ResponseInvalidError ("Requested OIDs do not " + + "match response OIDs")); + } else { + for (var i = 0; i < req.message.pdu.varbinds.length; i++) { + if (isVarbindError (pdu.varbinds[i])) { + varbinds.push (pdu.varbinds[i]); + } else if (! oidFollowsOid (req.message.pdu.varbinds[i].oid, + pdu.varbinds[i].oid)) { + req.responseCb (new ResponseInvalidError ("OID '" + + req.message.pdu.varbinds[i].oid + "' in request at " + + "positiion '" + i + "' does not precede " + + "OID '" + pdu.varbinds[i].oid + "' in response " + + "at position '" + i + "'")); + return; + } else { + varbinds.push (pdu.varbinds[i]); + } + } + + req.responseCb (null, varbinds); + } + }; + + var pduVarbinds = []; + + for (var i = 0; i < oids.length; i++) { + var varbind = { + oid: oids[i] + }; + pduVarbinds.push (varbind); + } + + this.simpleGet (GetNextRequestPdu, feedCb, pduVarbinds, responseCb); + + return this; +}; + +Session.prototype.inform = function () { + var typeOrOid = arguments[0];; + var varbinds, options = {}, responseCb; + + /** + ** Support the following signatures: + ** + ** typeOrOid, varbinds, options, callback + ** typeOrOid, varbinds, callback + ** typeOrOid, options, callback + ** typeOrOid, callback + **/ + if (arguments.length >= 4) { + varbinds = arguments[1]; + options = arguments[2]; + responseCb = arguments[3]; + } else if (arguments.length >= 3) { + if (arguments[1].constructor != Array) { + varbinds = []; + options = arguments[1]; + responseCb = arguments[2]; + } else { + varbinds = arguments[1]; + responseCb = arguments[2]; + } + } else { + varbinds = []; + responseCb = arguments[1]; + } + + function feedCb (req, message) { + var pdu = message.pdu; + var varbinds = []; + + if (req.message.pdu.varbinds.length != pdu.varbinds.length) { + req.responseCb (new ResponseInvalidError ("Inform OIDs do not " + + "match response OIDs")); + } else { + for (var i = 0; i < req.message.pdu.varbinds.length; i++) { + if (req.message.pdu.varbinds[i].oid != pdu.varbinds[i].oid) { + req.responseCb (new ResponseInvalidError ("OID '" + + req.message.pdu.varbinds[i].oid + + "' in inform at positiion '" + i + "' does not " + + "match OID '" + pdu.varbinds[i].oid + "' in response " + + "at position '" + i + "'")); + return; + } else { + varbinds.push (pdu.varbinds[i]); + } + } + + req.responseCb (null, varbinds); + } + }; + + if (typeof typeOrOid != "string") + typeOrOid = "1.3.6.1.6.3.1.1.5." + (typeOrOid + 1); + + var pduVarbinds = [ + { + oid: "1.3.6.1.2.1.1.3.0", + type: ObjectType.TimeTicks, + value: options.upTime || Math.floor (process.uptime () * 100) + }, + { + oid: "1.3.6.1.6.3.1.1.4.1.0", + type: ObjectType.OID, + value: typeOrOid + } + ]; + + for (var i = 0; i < varbinds.length; i++) { + var varbind = { + oid: varbinds[i].oid, + type: varbinds[i].type, + value: varbinds[i].value + }; + pduVarbinds.push (varbind); + } + + options.port = this.trapPort; + + this.simpleGet (InformRequestPdu, feedCb, pduVarbinds, responseCb, options); + + return this; +}; + +Session.prototype.onClose = function () { + this.cancelRequests (new Error ("Socket forcibly closed")); + this.emit ("close"); +}; + +Session.prototype.onError = function (error) { + this.emit (error); +}; + +Session.prototype.onMsg = function (buffer, remote) { + try { + var message = new ResponseMessage (buffer); + + var req = this.unregisterRequest (message.pdu.id); + if (! req) + return; + + try { + if (message.version != req.message.version) { + req.responseCb (new ResponseInvalidError ("Version in request '" + + req.message.version + "' does not match version in " + + "response '" + message.version)); + } else if (message.community != req.message.community) { + req.responseCb (new ResponseInvalidError ("Community '" + + req.message.community + "' in request does not match " + + "community '" + message.community + "' in response")); + } else if (message.pdu.type == PduType.GetResponse) { + req.onResponse (req, message); + } else { + req.responseCb (new ResponseInvalidError ("Unknown PDU type '" + + message.pdu.type + "' in response")); + } + } catch (error) { + req.responseCb (error); + } + } catch (error) { + this.emit("error", error); + } +}; + +Session.prototype.onSimpleGetResponse = function (req, message) { + var pdu = message.pdu; + + if (pdu.errorStatus > 0) { + var statusString = ErrorStatus[pdu.errorStatus] + || ErrorStatus.GeneralError; + var statusCode = ErrorStatus[statusString] + || ErrorStatus[ErrorStatus.GeneralError]; + + if (pdu.errorIndex <= 0 || pdu.errorIndex > pdu.varbinds.length) { + req.responseCb (new RequestFailedError (statusString, statusCode)); + } else { + var oid = pdu.varbinds[pdu.errorIndex - 1].oid; + var error = new RequestFailedError (statusString + ": " + oid, + statusCode); + req.responseCb (error); + } + } else { + req.feedCb (req, message); + } +}; + +Session.prototype.registerRequest = function (req) { + if (! this.reqs[req.id]) { + this.reqs[req.id] = req; + if (this.reqCount <= 0) + this.dgram.ref(); + this.reqCount++; + } + var me = this; + req.timer = setTimeout (function () { + if (req.retries-- > 0) { + me.send (req); + } else { + me.unregisterRequest (req.id); + req.responseCb (new RequestTimedOutError ( + "Request timed out")); + } + }, req.timeout); +}; + +Session.prototype.send = function (req, noWait) { + try { + var me = this; + + var buffer = req.message.toBuffer (); + + this.dgram.send (buffer, 0, buffer.length, req.port, this.target, + function (error, bytes) { + if (error) { + req.responseCb (error); + } else { + if (noWait) { + req.responseCb (null); + } else { + me.registerRequest (req); + } + } + }); + } catch (error) { + req.responseCb (error); + } + + return this; +}; + +Session.prototype.set = function (varbinds, responseCb) { + function feedCb (req, message) { + var pdu = message.pdu; + var varbinds = []; + + if (req.message.pdu.varbinds.length != pdu.varbinds.length) { + req.responseCb (new ResponseInvalidError ("Requested OIDs do not " + + "match response OIDs")); + } else { + for (var i = 0; i < req.message.pdu.varbinds.length; i++) { + if (req.message.pdu.varbinds[i].oid != pdu.varbinds[i].oid) { + req.responseCb (new ResponseInvalidError ("OID '" + + req.message.pdu.varbinds[i].oid + + "' in request at positiion '" + i + "' does not " + + "match OID '" + pdu.varbinds[i].oid + "' in response " + + "at position '" + i + "'")); + return; + } else { + varbinds.push (pdu.varbinds[i]); + } + } + + req.responseCb (null, varbinds); + } + }; + + var pduVarbinds = []; + + for (var i = 0; i < varbinds.length; i++) { + var varbind = { + oid: varbinds[i].oid, + type: varbinds[i].type, + value: varbinds[i].value + }; + pduVarbinds.push (varbind); + } + + this.simpleGet (SetRequestPdu, feedCb, pduVarbinds, responseCb); + + return this; +}; + +Session.prototype.simpleGet = function (pduClass, feedCb, varbinds, + responseCb, options) { + var req = {} + + try { + var id = _generateId (); + var pdu = new pduClass (id, varbinds, options); + var message = new RequestMessage (this.version, this.community, pdu); + + req = { + id: id, + message: message, + responseCb: responseCb, + retries: this.retries, + timeout: this.timeout, + onResponse: this.onSimpleGetResponse, + feedCb: feedCb, + port: (options && options.port) ? options.port : this.port + }; + + this.send (req); + } catch (error) { + if (req.responseCb) + req.responseCb (error); + } +}; + +function subtreeCb (req, varbinds) { + var done = 0; + + for (var i = varbinds.length; i > 0; i--) { + if (! oidInSubtree (req.baseOid, varbinds[i - 1].oid)) { + done = 1; + varbinds.pop (); + } + } + + if (varbinds.length > 0) + req.feedCb (varbinds); + + if (done) + return true; +} + +Session.prototype.subtree = function () { + var me = this; + var oid = arguments[0]; + var maxRepetitions, feedCb, doneCb; + + if (arguments.length < 4) { + maxRepetitions = 20; + feedCb = arguments[1]; + doneCb = arguments[2]; + } else { + maxRepetitions = arguments[1]; + feedCb = arguments[2]; + doneCb = arguments[3]; + } + + var req = { + feedCb: feedCb, + doneCb: doneCb, + maxRepetitions: maxRepetitions, + baseOid: oid + }; + + this.walk (oid, maxRepetitions, subtreeCb.bind (me, req), doneCb); + + return this; +} + +function tableColumnsResponseCb (req, error) { + if (error) { + req.responseCb (error); + } else if (req.error) { + req.responseCb (req.error); + } else { + if (req.columns.length > 0) { + var column = req.columns.pop (); + var me = this; + this.subtree (req.rowOid + column, req.maxRepetitions, + tableColumnsFeedCb.bind (me, req), + tableColumnsResponseCb.bind (me, req)); + } else { + req.responseCb (null, req.table); + } + } +} + +function tableColumnsFeedCb (req, varbinds) { + for (var i = 0; i < varbinds.length; i++) { + if (isVarbindError (varbinds[i])) { + req.error = new RequestFailedError (varbindError (varbind[i])); + return true; + } + + var oid = varbinds[i].oid.replace (req.rowOid, "") + if (oid && oid != varbinds[i].oid) { + var match = oid.match (/^(\d+)\.(.+)$/); + if (match && match[1] > 0) { + if (! req.table[match[2]]) + req.table[match[2]] = {}; + req.table[match[2]][match[1]] = varbinds[i].value; + } + } + } +} + +Session.prototype.tableColumns = function () { + var me = this; + + var oid = arguments[0]; + var columns = arguments[1]; + var maxRepetitions, responseCb; + + if (arguments.length < 4) { + responseCb = arguments[2]; + maxRepetitions = 20; + } else { + maxRepetitions = arguments[2]; + responseCb = arguments[3]; + } + + var req = { + responseCb: responseCb, + maxRepetitions: maxRepetitions, + baseOid: oid, + rowOid: oid + ".1.", + columns: columns.slice(0), + table: {} + }; + + if (req.columns.length > 0) { + var column = req.columns.pop (); + this.subtree (req.rowOid + column, maxRepetitions, + tableColumnsFeedCb.bind (me, req), + tableColumnsResponseCb.bind (me, req)); + } + + return this; +} + +function tableResponseCb (req, error) { + if (error) + req.responseCb (error); + else if (req.error) + req.responseCb (req.error); + else + req.responseCb (null, req.table); +} + +function tableFeedCb (req, varbinds) { + for (var i = 0; i < varbinds.length; i++) { + if (isVarbindError (varbinds[i])) { + req.error = new RequestFailedError (varbindError (varbind[i])); + return true; + } + + var oid = varbinds[i].oid.replace (req.rowOid, "") + if (oid && oid != varbinds[i].oid) { + var match = oid.match (/^(\d+)\.(.+)$/); + if (match && match[1] > 0) { + if (! req.table[match[2]]) + req.table[match[2]] = {}; + req.table[match[2]][match[1]] = varbinds[i].value; + } + } + } +} + +Session.prototype.table = function () { + var me = this; + + var oid = arguments[0]; + var maxRepetitions, responseCb; + + if (arguments.length < 3) { + responseCb = arguments[1]; + maxRepetitions = 20; + } else { + maxRepetitions = arguments[1]; + responseCb = arguments[2]; + } + + var req = { + responseCb: responseCb, + maxRepetitions: maxRepetitions, + baseOid: oid, + rowOid: oid + ".1.", + table: {} + }; + + this.subtree (oid, maxRepetitions, tableFeedCb.bind (me, req), + tableResponseCb.bind (me, req)); + + return this; +} + +Session.prototype.trap = function () { + var req = {}; + + try { + var typeOrOid = arguments[0]; + var varbinds, options = {}, responseCb; + + /** + ** Support the following signatures: + ** + ** typeOrOid, varbinds, options, callback + ** typeOrOid, varbinds, agentAddr, callback + ** typeOrOid, varbinds, callback + ** typeOrOid, agentAddr, callback + ** typeOrOid, options, callback + ** typeOrOid, callback + **/ + if (arguments.length >= 4) { + varbinds = arguments[1]; + if (typeof arguments[2] == "string") { + options.agentAddr = arguments[2]; + } else if (arguments[2].constructor != Array) { + options = arguments[2]; + } + responseCb = arguments[3]; + } else if (arguments.length >= 3) { + if (typeof arguments[1] == "string") { + varbinds = []; + options.agentAddr = arguments[1]; + } else if (arguments[1].constructor != Array) { + varbinds = []; + options = arguments[1]; + } else { + varbinds = arguments[1]; + agentAddr = null; + } + responseCb = arguments[2]; + } else { + varbinds = []; + responseCb = arguments[1]; + } + + var pdu, pduVarbinds = []; + + for (var i = 0; i < varbinds.length; i++) { + var varbind = { + oid: varbinds[i].oid, + type: varbinds[i].type, + value: varbinds[i].value + }; + pduVarbinds.push (varbind); + } + + var id = _generateId (); + + if (this.version == Version2c) { + if (typeof typeOrOid != "string") + typeOrOid = "1.3.6.1.6.3.1.1.5." + (typeOrOid + 1); + + pduVarbinds.unshift ( + { + oid: "1.3.6.1.2.1.1.3.0", + type: ObjectType.TimeTicks, + value: options.upTime || Math.floor (process.uptime () * 100) + }, + { + oid: "1.3.6.1.6.3.1.1.4.1.0", + type: ObjectType.OID, + value: typeOrOid + } + ); + + pdu = new TrapV2Pdu (id, pduVarbinds, options); + } else { + pdu = new TrapPdu (typeOrOid, pduVarbinds, options); + } + + var message = new RequestMessage (this.version, this.community, pdu); + + req = { + id: id, + message: message, + responseCb: responseCb, + port: this.trapPort + }; + + this.send (req, true); + } catch (error) { + if (req.responseCb) + req.responseCb (error); + } + + return this; +}; + +Session.prototype.unregisterRequest = function (id) { + var req = this.reqs[id]; + if (req) { + delete this.reqs[id]; + clearTimeout (req.timer); + delete req.timer; + this.reqCount--; + if (this.reqCount <= 0) + this.dgram.unref(); + return req; + } else { + return null; + } +}; + +function walkCb (req, error, varbinds) { + var done = 0; + var oid; + + if (error) { + if (error instanceof RequestFailedError) { + if (error.status != ErrorStatus.NoSuchName) { + req.doneCb (error); + return; + } else { + // signal the version 1 walk code below that it should stop + done = 1; + } + } else { + req.doneCb (error); + return; + } + } + + if (this.version == Version2c) { + for (var i = varbinds[0].length; i > 0; i--) { + if (varbinds[0][i - 1].type == ObjectType.EndOfMibView) { + varbinds[0].pop (); + done = 1; + } + } + if (req.feedCb (varbinds[0])) + done = 1; + if (! done) + oid = varbinds[0][varbinds[0].length - 1].oid; + } else { + if (! done) { + if (req.feedCb (varbinds)) { + done = 1; + } else { + oid = varbinds[0].oid; + } + } + } + + if (done) + req.doneCb (null); + else + this.walk (oid, req.maxRepetitions, req.feedCb, req.doneCb, + req.baseOid); +} + +Session.prototype.walk = function () { + var me = this; + var oid = arguments[0]; + var maxRepetitions, feedCb, doneCb, baseOid; + + if (arguments.length < 4) { + maxRepetitions = 20; + feedCb = arguments[1]; + doneCb = arguments[2]; + } else { + maxRepetitions = arguments[1]; + feedCb = arguments[2]; + doneCb = arguments[3]; + } + + var req = { + maxRepetitions: maxRepetitions, + feedCb: feedCb, + doneCb: doneCb + }; + + if (this.version == Version2c) + this.getBulk ([oid], 0, maxRepetitions, + walkCb.bind (me, req)); + else + this.getNext ([oid], walkCb.bind (me, req)); + + return this; +} + +/***************************************************************************** + ** Exports + **/ + +exports.Session = Session; + +exports.createSession = function (target, community, version, options) { + return new Session (target, community, version, options); +}; + +exports.isVarbindError = isVarbindError; +exports.varbindError = varbindError; + +exports.Version1 = Version1; +exports.Version2c = Version2c; + +exports.ErrorStatus = ErrorStatus; +exports.TrapType = TrapType; +exports.ObjectType = ObjectType; + +exports.ResponseInvalidError = ResponseInvalidError; +exports.RequestInvalidError = RequestInvalidError; +exports.RequestFailedError = RequestFailedError; +exports.RequestTimedOutError = RequestTimedOutError; + +/** + ** We've added this for testing. + **/ +exports.ObjectParser = { + readInt: readInt, + readUint: readUint +}; diff --git a/node.d/node_modules/netdata.js b/node.d/node_modules/netdata.js new file mode 100755 index 000000000..9834534ee --- /dev/null +++ b/node.d/node_modules/netdata.js @@ -0,0 +1,612 @@ +'use strict'; + +var url = require('url'); +var http = require('http'); +var util = require('util'); + +/* +var netdata = require('netdata'); + +var example_chart = { + id: 'id', // the unique id of the chart + name: 'name', // the name of the chart + title: 'title', // the title of the chart + units: 'units', // the units of the chart dimensions + family: 'family', // the family of the chart + context: 'context', // the context of the chart + type: netdata.chartTypes.line, // the type of the chart + priority: 0, // the priority relative to others in the same family + update_every: 1, // the expected update frequency of the chart + dimensions: { + 'dim1': { + id: 'dim1', // the unique id of the dimension + name: 'name', // the name of the dimension + algorithm: netdata.chartAlgorithms.absolute, // the id of the netdata algorithm + multiplier: 1, // the multiplier + divisor: 1, // the divisor + hidden: false, // is hidden (boolean) + }, + 'dim2': { + id: 'dim2', // the unique id of the dimension + name: 'name', // the name of the dimension + algorithm: 'absolute', // the id of the netdata algorithm + multiplier: 1, // the multiplier + divisor: 1, // the divisor + hidden: false, // is hidden (boolean) + } + // add as many dimensions as needed + } +}; +*/ + +var netdata = { + options: { + filename: __filename, + DEBUG: false, + update_every: 1, + }, + + chartAlgorithms: { + incremental: 'incremental', + absolute: 'absolute', + percentage_of_absolute_row: 'percentage-of-absolute-row', + percentage_of_incremental_row: 'percentage-of-incremental-row' + }, + + chartTypes: { + line: 'line', + area: 'area', + stacked: 'stacked' + }, + + services: new Array(), + modules_configuring: 0, + charts: {}, + + + processors: { + http: { + name: 'http', + + process: function(service, callback) { + if(netdata.options.DEBUG === true) netdata.debug(service.module.name + ': ' + service.name + ': making ' + this.name + ' request: ' + netdata.stringify(service.request)); + + var req = http.request(service.request, function(response) { + if(netdata.options.DEBUG === true) netdata.debug(service.module.name + ': ' + service.name + ': got server response...'); + + var end = false; + var data = ''; + response.setEncoding('utf8'); + + if(response.statusCode !== 200) { + if(end === false) { + service.error('Got HTTP code ' + response.statusCode + ', failed to get data.'); + end = true; + callback(null); + } + } + + response.on('data', function(chunk) { + if(end === false) data += chunk; + }); + + response.on('error', function() { + if(end === false) { + service.error(': Read error, failed to get data.'); + end = true; + callback(null); + } + }); + + response.on('end', function() { + if(end === false) { + if(netdata.options.DEBUG === true) netdata.debug(service.module.name + ': ' + service.name + ': read completed.'); + end = true; + callback(data); + } + }); + }); + + req.on('error', function(e) { + service.error('Failed to make request: ' + netdata.stringify(service.request) + ', message: ' + e.message); + callback(null); + }); + + // write data to request body + if(typeof service.postData !== 'undefined' && service.request.method === 'POST') { + if(netdata.options.DEBUG === true) netdata.debug(service.module.name + ': ' + service.name + ': posting data: ' + service.postData); + req.write(service.postData); + } + + req.end(); + } + } + }, + + stringify: function(obj) { + return util.inspect(obj, {depth: 10}); + }, + + // show debug info, if debug is enabled + debug: function(msg) { + if(this.options.DEBUG === true) { + var now = new Date(); + console.error(now.toString() + ': ' + netdata.options.filename + ': DEBUG: ' + ((typeof(msg) === 'object')?netdata.stringify(msg):msg).toString()); + } + }, + + // log an error + error: function(msg) { + var now = new Date(); + console.error(now.toString() + ': ' + netdata.options.filename + ': ERROR: ' + ((typeof(msg) === 'object')?netdata.stringify(msg):msg).toString()); + }, + + // send data to netdata + send: function(msg) { + console.log(msg.toString()); + }, + + service: function(service) { + if(typeof service === 'undefined') + service = {}; + + var now = new Date().getTime(); + + service._current_chart = null; // the current chart we work on + service._queue = ''; // data to be sent to netdata + + service.error_reported = false; // error log flood control + + service.added = false; // added to netdata.services + service.enabled = true; + service.updates = 0; + service.running = false; + service.started = 0; + service.ended = 0; + + if(typeof service.module === 'undefined') { + service.module = { name: 'not-defined-module' }; + service.error('Attempted to create service without a module.'); + service.enabled = false; + } + + if(typeof service.name === 'undefined') { + service.name = 'unnamed@' + service.module.name + '/' + now; + } + + if(typeof service.processor === 'undefined') + service.processor = netdata.processors.http; + + if(typeof service.update_every === 'undefined') + service.update_every = service.module.update_every; + + if(typeof service.update_every === 'undefined') + service.update_every = netdata.options.update_every; + + if(service.update_every < netdata.options.update_every) + service.update_every = netdata.options.update_every; + + // align the runs + service.next_run = now - (now % (service.update_every * 1000)); + + service.commit = function() { + if(this.added !== true) { + this.added = true; + + var now = new Date().getTime(); + while( this.next_run < now ) + this.next_run += (this.update_every * 1000); + + netdata.services.push(this); + if(netdata.options.DEBUG === true) netdata.debug(this.module.name + ': ' + this.name + ': service committed.'); + } + }; + + service.execute = function(callback) { + if(service.enabled === false) { + callback(null); + return; + } + + this.module.active++; + this.running = true; + this.started = new Date().getTime(); + this.updates++; + + if(netdata.options.DEBUG === true) + netdata.debug(this.module.name + ': ' + this.name + ': making ' + this.processor.name + ' request: ' + netdata.stringify(this)); + + this.processor.process(this, function(response) { + service.ended = new Date().getTime(); + service.duration = service.ended - service.started; + + if(typeof response === 'undefined') + response = null; + + if(response !== null) + service.errorClear(); + + if(netdata.options.DEBUG === true) + netdata.debug(service.module.name + ': ' + service.name + ': processing ' + service.processor.name + ' response (received in ' + (service.ended - service.started).toString() + ' ms)'); + + callback(service, response); + + service.running = false; + service.module.active--; + if(service.module.active < 0) { + service.module.active = 0; + if(netdata.options.DEBUG === true) netdata.debug(service.module.name + ': active module counter below zero.'); + } + + if(service.module.active === 0) { + // check if we run under configure + if(service.module.configure_callback !== null) { + if(netdata.options.DEBUG === true) netdata.debug(service.module.name + ': configuration finish callback called from processResponse().'); + var ccallback = service.module.configure_callback; + service.module.configure_callback = null; + ccallback(); + } + } + }); + }; + + service.update = function() { + if(netdata.options.DEBUG === true) netdata.debug(this.module.name + ': ' + this.name + ': starting data collection...'); + + this.module.update(this, function() { + if(netdata.options.DEBUG === true) netdata.debug(service.module.name + ': ' + service.name + ': data collection ended in ' + service.duration.toString() + ' ms.'); + }); + }; + + service.error = function(message) { + if(this.error_reported === false) { + netdata.error(this.module.name + ': ' + this.name + ': ' + message); + this.error_reported = true; + } + else if(netdata.options.DEBUG === true) + netdata.debug(this.module.name + ': ' + this.name + ': ' + message); + }; + + service.errorClear = function() { + this.error_reported = false; + }; + + service.queue = function(txt) { + this._queue += txt + '\n'; + }; + + service._send_chart_to_netdata = function(chart) { + // internal function to send a chart to netdata + this.queue('CHART "' + chart.id + '" "' + chart.name + '" "' + chart.title + '" "' + chart.units + '" "' + chart.family + '" "' + chart.context + '" "' + chart.type + '" ' + chart.priority.toString() + ' ' + chart.update_every.toString()); + + for(var dim in chart.dimensions) { + var d = chart.dimensions[dim]; + + this.queue('DIMENSION "' + d.id + '" "' + d.name + '" "' + d.algorithm + '" ' + d.multiplier.toString() + ' ' + d.divisor.toString() + ' ' + ((d.hidden === true)?'hidden':'').toString()); + d._created = true; + d._updated = false; + } + + chart._created = true; + chart._updated = false; + }; + + // begin data collection for a chart + service.begin = function(chart) { + if(this._current_chart !== null && this._current_chart !== chart) { + this.error('Called begin() for chart ' + chart.id + ' while chart ' + this._current_chart.id + ' is still open. Closing it.'); + this.end(); + } + + if(typeof(chart.id) === 'undefined' || netdata.charts[chart.id] != chart) { + this.error('Called begin() for chart ' + chart.id + ' that is not mine. Where did you find it? Ignoring it.'); + return false; + } + + if(netdata.options.DEBUG === true) netdata.debug('setting current chart to ' + chart.id); + this._current_chart = chart; + this._current_chart._began = true; + + if(this._current_chart._dimensions_count !== 0) { + if(this._current_chart._created === false || this._current_chart._updated === true) + this._send_chart_to_netdata(this._current_chart); + + var now = this.ended; + this.queue('BEGIN ' + this._current_chart.id + ' ' + ((this._current_chart._last_updated > 0)?((now - this._current_chart._last_updated) * 1000):'').toString()); + } + // else this.error('Called begin() for chart ' + chart.id + ' which is empty.'); + + this._current_chart._last_updated = now; + this._current_chart._began = true; + this._current_chart._counter++; + + return true; + }; + + // set a collected value for a chart + // we do most things on the first value we attempt to set + service.set = function(dimension, value) { + if(this._current_chart === null) { + this.error('Called set(' + dimension + ', ' + value + ') without an open chart.'); + return false; + } + + if(typeof(this._current_chart.dimensions[dimension]) === 'undefined') { + this.error('Called set(' + dimension + ', ' + value + ') but dimension "' + dimension + '" does not exist in chart "' + this._current_chart.id + '".'); + return false; + } + + if(typeof value === 'undefined' || value === null) + return false; + + if(this._current_chart._dimensions_count !== 0) + this.queue('SET ' + dimension + ' = ' + value); + + return true; + }; + + // end data collection for the current chart - after calling begin() + service.end = function() { + if(this._current_chart !== null && this._current_chart._began === false) { + this.error('Called end() without an open chart.'); + return false; + } + + if(this._current_chart._dimensions_count !== 0) { + this.queue('END'); + netdata.send(this._queue); + } + + this._queue = ''; + this._current_chart._began = false; + if(netdata.options.DEBUG === true) netdata.debug('sent chart ' + this._current_chart.id); + this._current_chart = null; + return true; + }; + + // discard the collected values for the current chart - after calling begin() + service.flush = function() { + if(this._current_chart === null || this._current_chart._began === false) { + this.error('Called flush() without an open chart.'); + return false; + } + + this._queue = ''; + this._current_chart._began = false; + this._current_chart = null; + return true; + }; + + // create a netdata chart + service.chart = function(id, chart) { + if(typeof(netdata.charts[id]) === 'undefined') { + netdata.charts[id] = { + _created: false, + _updated: true, + _began: false, + _counter: 0, + _last_updated: 0, + _dimensions_count: 0, + id: id, + name: id, + title: 'untitled chart', + units: 'a unit', + family: '', + context: '', + type: netdata.chartTypes.line, + priority: 50000, + update_every: netdata.options.update_every, + dimensions: {} + }; + } + + var c = netdata.charts[id]; + + if(typeof(chart.name) !== 'undefined' && chart.name !== c.name) { + if(netdata.options.DEBUG === true) netdata.debug('chart ' + id + ' updated its name'); + c.name = chart.name; + c._updated = true; + } + + if(typeof(chart.title) !== 'undefined' && chart.title !== c.title) { + if(netdata.options.DEBUG === true) netdata.debug('chart ' + id + ' updated its title'); + c.title = chart.title; + c._updated = true; + } + + if(typeof(chart.units) !== 'undefined' && chart.units !== c.units) { + if(netdata.options.DEBUG === true) netdata.debug('chart ' + id + ' updated its units'); + c.units = chart.units; + c._updated = true; + } + + if(typeof(chart.family) !== 'undefined' && chart.family !== c.family) { + if(netdata.options.DEBUG === true) netdata.debug('chart ' + id + ' updated its family'); + c.family = chart.family; + c._updated = true; + } + + if(typeof(chart.context) !== 'undefined' && chart.context !== c.context) { + if(netdata.options.DEBUG === true) netdata.debug('chart ' + id + ' updated its context'); + c.context = chart.context; + c._updated = true; + } + + if(typeof(chart.type) !== 'undefined' && chart.type !== c.type) { + if(netdata.options.DEBUG === true) netdata.debug('chart ' + id + ' updated its type'); + c.type = chart.type; + c._updated = true; + } + + if(typeof(chart.priority) !== 'undefined' && chart.priority !== c.priority) { + if(netdata.options.DEBUG === true) netdata.debug('chart ' + id + ' updated its priority'); + c.priority = chart.priority; + c._updated = true; + } + + if(typeof(chart.update_every) !== 'undefined' && chart.update_every !== c.update_every) { + if(netdata.options.DEBUG === true) netdata.debug('chart ' + id + ' updated its update_every from ' + c.update_every + ' to ' + chart.update_every); + c.update_every = chart.update_every; + c._updated = true; + } + + if(typeof(chart.dimensions) !== 'undefined') { + for(var x in chart.dimensions) { + if(typeof(c.dimensions[x]) === 'undefined') { + c._dimensions_count++; + + c.dimensions[x] = { + _created: false, + _updated: false, + id: x, // the unique id of the dimension + name: x, // the name of the dimension + algorithm: netdata.chartAlgorithms.absolute, // the id of the netdata algorithm + multiplier: 1, // the multiplier + divisor: 1, // the divisor + hidden: false, // is hidden (boolean) + }; + + if(netdata.options.DEBUG === true) netdata.debug('chart ' + id + ' created dimension ' + x); + c._updated = true; + } + + var dim = chart.dimensions[x]; + var d = c.dimensions[x]; + + if(typeof(dim.name) !== 'undefined' && d.name !== dim.name) { + if(netdata.options.DEBUG === true) netdata.debug('chart ' + id + ', dimension ' + x + ' updated its name'); + d.name = dim.name; + d._updated = true; + } + + if(typeof(dim.algorithm) !== 'undefined' && d.algorithm !== dim.algorithm) { + if(netdata.options.DEBUG === true) netdata.debug('chart ' + id + ', dimension ' + x + ' updated its algorithm from ' + d.algorithm + ' to ' + dim.algorithm); + d.algorithm = dim.algorithm; + d._updated = true; + } + + if(typeof(dim.multiplier) !== 'undefined' && d.multiplier !== dim.multiplier) { + if(netdata.options.DEBUG === true) netdata.debug('chart ' + id + ', dimension ' + x + ' updated its multiplier'); + d.multiplier = dim.multiplier; + d._updated = true; + } + + if(typeof(dim.divisor) !== 'undefined' && d.divisor !== dim.divisor) { + if(netdata.options.DEBUG === true) netdata.debug('chart ' + id + ', dimension ' + x + ' updated its divisor'); + d.divisor = dim.divisor; + d._updated = true; + } + + if(typeof(dim.hidden) !== 'undefined' && d.hidden !== dim.hidden) { + if(netdata.options.DEBUG === true) netdata.debug('chart ' + id + ', dimension ' + x + ' updated its hidden status'); + d.hidden = dim.hidden; + d._updated = true; + } + + if(d._updated) c._updated = true; + } + } + + //if(netdata.options.DEBUG === true) netdata.debug(netdata.charts); + return netdata.charts[id]; + }; + + return service; + }, + + runAllServices: function() { + if(netdata.options.DEBUG === true) netdata.debug('runAllServices()'); + + var now = new Date().getTime(); + var len = netdata.services.length; + while(len--) { + var service = netdata.services[len]; + + if(service.enabled === false || service.running === true) continue; + if(now <= service.next_run) continue; + + service.update(); + + now = new Date().getTime(); + while(service.next_run < now) + service.next_run += (service.update_every * 1000); + } + + // 1/10th of update_every in pause + setTimeout(netdata.runAllServices, netdata.options.update_every * 100); + }, + + start: function() { + if(netdata.options.DEBUG === true) this.debug('started, services: ' + netdata.stringify(this.services)); + + if(this.services.length === 0) { + this.disableNodePlugin(); + process.exit(1); + } + else this.runAllServices(); + }, + + // disable the whole node.js plugin + disableNodePlugin: function() { + this.send('DISABLE'); + process.exit(1); + }, + + requestFromParams: function(protocol, hostname, port, path, method) { + return { + protocol: protocol, + hostname: hostname, + port: port, + path: path, + //family: 4, + method: method, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Connection': 'keep-alive' + }, + agent: new http.Agent({ + keepAlive: true, + keepAliveMsecs: netdata.options.update_every * 1000, + maxSockets: 2, // it must be 2 to work + maxFreeSockets: 1 + }) + }; + }, + + requestFromURL: function(a_url) { + var u = url.parse(a_url); + return netdata.requestFromParams(u.protocol, u.hostname, u.port, u.path, 'GET'); + }, + + configure: function(module, config, callback) { + if(netdata.options.DEBUG === true) this.debug(module.name + ': configuring (update_every: ' + this.options.update_every + ')...'); + + module.active = 0; + module.update_every = this.options.update_every; + + if(typeof config.update_every !== 'undefined') + module.update_every = config.update_every; + + module.enable_autodetect = (config.enable_autodetect)?true:false; + + if(typeof(callback) === 'function') + module.configure_callback = callback; + else + module.configure_callback = null; + + var added = module.configure(config); + + if(netdata.options.DEBUG === true) this.debug(module.name + ': configured, reporting ' + added + ' eligible services.'); + + if(module.configure_callback !== null && added === 0) { + if(netdata.options.DEBUG === true) this.debug(module.name + ': configuration finish callback called from configure().'); + module.configure_callback = null; + callback(); + } + + return added; + } +}; + +if(netdata.options.DEBUG === true) netdata.debug('loaded netdata from: ' + __filename); +module.exports = netdata; diff --git a/node.d/node_modules/pixl-xml.js b/node.d/node_modules/pixl-xml.js new file mode 100644 index 000000000..481acbaeb --- /dev/null +++ b/node.d/node_modules/pixl-xml.js @@ -0,0 +1,606 @@ +/* + JavaScript XML Library + Plus a bunch of object utility functions + + Usage: + var XML = require('pixl-xml'); + var myxmlstring = '<?xml version="1.0"?><Document>' + + '<Simple>Hello</Simple>' + + '<Node Key="Value">Content</Node>' + + '</Document>'; + + var tree = XML.parse( myxmlstring, { preserveAttributes: true }); + console.log( tree ); + + tree.Simple = "Hello2"; + tree.Node._Attribs.Key = "Value2"; + tree.Node._Data = "Content2"; + tree.New = "I added this"; + + console.log( XML.stringify( tree, 'Document' ) ); + + Copyright (c) 2004 - 2015 Joseph Huckaby + Released under the MIT License + This version is for Node.JS, converted in 2012. +*/ + +var fs = require('fs'); + +var indent_string = "\t"; +var xml_header = '<?xml version="1.0"?>'; +var sort_args = null; +var re_valid_tag_name = /^\w[\w\-\:]*$/; + +var XML = exports.XML = function XML(args) { + // class constructor for XML parser class + // pass in args hash or text to parse + if (!args) args = ''; + if (isa_hash(args)) { + for (var key in args) this[key] = args[key]; + } + else this.text = args || ''; + + // stringify buffers + if (this.text instanceof Buffer) { + this.text = this.text.toString(); + } + + if (!this.text.match(/^\s*</)) { + // try as file path + var file = this.text; + this.text = fs.readFileSync(file, { encoding: 'utf8' }); + if (!this.text) throw new Error("File not found: " + file); + } + + this.tree = {}; + this.errors = []; + this.piNodeList = []; + this.dtdNodeList = []; + this.documentNodeName = ''; + + if (this.lowerCase) { + this.attribsKey = this.attribsKey.toLowerCase(); + this.dataKey = this.dataKey.toLowerCase(); + } + + this.patTag.lastIndex = 0; + if (this.text) this.parse(); +} + +XML.prototype.preserveAttributes = false; +XML.prototype.lowerCase = false; + +XML.prototype.patTag = /([^<]*?)<([^>]+)>/g; +XML.prototype.patSpecialTag = /^\s*([\!\?])/; +XML.prototype.patPITag = /^\s*\?/; +XML.prototype.patCommentTag = /^\s*\!--/; +XML.prototype.patDTDTag = /^\s*\!DOCTYPE/; +XML.prototype.patCDATATag = /^\s*\!\s*\[\s*CDATA/; +XML.prototype.patStandardTag = /^\s*(\/?)([\w\-\:\.]+)\s*(.*)$/; +XML.prototype.patSelfClosing = /\/\s*$/; +XML.prototype.patAttrib = new RegExp("([\\w\\-\\:\\.]+)\\s*=\\s*([\\\"\\'])([^\\2]*?)\\2", "g"); +XML.prototype.patPINode = /^\s*\?\s*([\w\-\:]+)\s*(.*)$/; +XML.prototype.patEndComment = /--$/; +XML.prototype.patNextClose = /([^>]*?)>/g; +XML.prototype.patExternalDTDNode = new RegExp("^\\s*\\!DOCTYPE\\s+([\\w\\-\\:]+)\\s+(SYSTEM|PUBLIC)\\s+\\\"([^\\\"]+)\\\""); +XML.prototype.patInlineDTDNode = /^\s*\!DOCTYPE\s+([\w\-\:]+)\s+\[/; +XML.prototype.patEndDTD = /\]$/; +XML.prototype.patDTDNode = /^\s*\!DOCTYPE\s+([\w\-\:]+)\s+\[(.*)\]/; +XML.prototype.patEndCDATA = /\]\]$/; +XML.prototype.patCDATANode = /^\s*\!\s*\[\s*CDATA\s*\[([^]*)\]\]/; + +XML.prototype.attribsKey = '_Attribs'; +XML.prototype.dataKey = '_Data'; + +XML.prototype.parse = function(branch, name) { + // parse text into XML tree, recurse for nested nodes + if (!branch) branch = this.tree; + if (!name) name = null; + var foundClosing = false; + var matches = null; + + // match each tag, plus preceding text + while ( matches = this.patTag.exec(this.text) ) { + var before = matches[1]; + var tag = matches[2]; + + // text leading up to tag = content of parent node + if (before.match(/\S/)) { + if (typeof(branch[this.dataKey]) != 'undefined') branch[this.dataKey] += ' '; else branch[this.dataKey] = ''; + branch[this.dataKey] += trim(decode_entities(before)); + } + + // parse based on tag type + if (tag.match(this.patSpecialTag)) { + // special tag + if (tag.match(this.patPITag)) tag = this.parsePINode(tag); + else if (tag.match(this.patCommentTag)) tag = this.parseCommentNode(tag); + else if (tag.match(this.patDTDTag)) tag = this.parseDTDNode(tag); + else if (tag.match(this.patCDATATag)) { + tag = this.parseCDATANode(tag); + if (typeof(branch[this.dataKey]) != 'undefined') branch[this.dataKey] += ' '; else branch[this.dataKey] = ''; + branch[this.dataKey] += trim(decode_entities(tag)); + } // cdata + else { + this.throwParseError( "Malformed special tag", tag ); + break; + } // error + + if (tag == null) break; + continue; + } // special tag + else { + // Tag is standard, so parse name and attributes (if any) + var matches = tag.match(this.patStandardTag); + if (!matches) { + this.throwParseError( "Malformed tag", tag ); + break; + } + + var closing = matches[1]; + var nodeName = this.lowerCase ? matches[2].toLowerCase() : matches[2]; + var attribsRaw = matches[3]; + + // If this is a closing tag, make sure it matches its opening tag + if (closing) { + if (nodeName == (name || '')) { + foundClosing = 1; + break; + } + else { + this.throwParseError( "Mismatched closing tag (expected </" + name + ">)", tag ); + break; + } + } // closing tag + else { + // Not a closing tag, so parse attributes into hash. If tag + // is self-closing, no recursive parsing is needed. + var selfClosing = !!attribsRaw.match(this.patSelfClosing); + var leaf = {}; + var attribs = leaf; + + // preserve attributes means they go into a sub-hash named "_Attribs" + // the XML composer honors this for restoring the tree back into XML + if (this.preserveAttributes) { + leaf[this.attribsKey] = {}; + attribs = leaf[this.attribsKey]; + } + + // parse attributes + this.patAttrib.lastIndex = 0; + while ( matches = this.patAttrib.exec(attribsRaw) ) { + var key = this.lowerCase ? matches[1].toLowerCase() : matches[1]; + attribs[ key ] = decode_entities( matches[3] ); + } // foreach attrib + + // if no attribs found, but we created the _Attribs subhash, clean it up now + if (this.preserveAttributes && !num_keys(attribs)) { + delete leaf[this.attribsKey]; + } + + // Recurse for nested nodes + if (!selfClosing) { + this.parse( leaf, nodeName ); + if (this.error()) break; + } + + // Compress into simple node if text only + var num_leaf_keys = num_keys(leaf); + if ((typeof(leaf[this.dataKey]) != 'undefined') && (num_leaf_keys == 1)) { + leaf = leaf[this.dataKey]; + } + else if (!num_leaf_keys) { + leaf = ''; + } + + // Add leaf to parent branch + if (typeof(branch[nodeName]) != 'undefined') { + if (isa_array(branch[nodeName])) { + branch[nodeName].push( leaf ); + } + else { + var temp = branch[nodeName]; + branch[nodeName] = [ temp, leaf ]; + } + } + else { + branch[nodeName] = leaf; + } + + if (this.error() || (branch == this.tree)) break; + } // not closing + } // standard tag + } // main reg exp + + // Make sure we found the closing tag + if (name && !foundClosing) { + this.throwParseError( "Missing closing tag (expected </" + name + ">)", name ); + } + + // If we are the master node, finish parsing and setup our doc node + if (branch == this.tree) { + if (typeof(this.tree[this.dataKey]) != 'undefined') delete this.tree[this.dataKey]; + + if (num_keys(this.tree) > 1) { + this.throwParseError( 'Only one top-level node is allowed in document', first_key(this.tree) ); + return; + } + + this.documentNodeName = first_key(this.tree); + if (this.documentNodeName) { + this.tree = this.tree[this.documentNodeName]; + } + } +}; + +XML.prototype.throwParseError = function(key, tag) { + // log error and locate current line number in source XML document + var parsedSource = this.text.substring(0, this.patTag.lastIndex); + var eolMatch = parsedSource.match(/\n/g); + var lineNum = (eolMatch ? eolMatch.length : 0) + 1; + lineNum -= tag.match(/\n/) ? tag.match(/\n/g).length : 0; + + this.errors.push({ + type: 'Parse', + key: key, + text: '<' + tag + '>', + line: lineNum + }); + + // Throw actual error (must wrap parse in try/catch) + throw new Error( this.getLastError() ); +}; + +XML.prototype.error = function() { + // return number of errors + return this.errors.length; +}; + +XML.prototype.getError = function(error) { + // get formatted error + var text = ''; + if (!error) return ''; + + text = (error.type || 'General') + ' Error'; + if (error.code) text += ' ' + error.code; + text += ': ' + error.key; + + if (error.line) text += ' on line ' + error.line; + if (error.text) text += ': ' + error.text; + + return text; +}; + +XML.prototype.getLastError = function() { + // Get most recently thrown error in plain text format + if (!this.error()) return ''; + return this.getError( this.errors[this.errors.length - 1] ); +}; + +XML.prototype.parsePINode = function(tag) { + // Parse Processor Instruction Node, e.g. <?xml version="1.0"?> + if (!tag.match(this.patPINode)) { + this.throwParseError( "Malformed processor instruction", tag ); + return null; + } + + this.piNodeList.push( tag ); + return tag; +}; + +XML.prototype.parseCommentNode = function(tag) { + // Parse Comment Node, e.g. <!-- hello --> + var matches = null; + this.patNextClose.lastIndex = this.patTag.lastIndex; + + while (!tag.match(this.patEndComment)) { + if (matches = this.patNextClose.exec(this.text)) { + tag += '>' + matches[1]; + } + else { + this.throwParseError( "Unclosed comment tag", tag ); + return null; + } + } + + this.patTag.lastIndex = this.patNextClose.lastIndex; + return tag; +}; + +XML.prototype.parseDTDNode = function(tag) { + // Parse Document Type Descriptor Node, e.g. <!DOCTYPE ... > + var matches = null; + + if (tag.match(this.patExternalDTDNode)) { + // tag is external, and thus self-closing + this.dtdNodeList.push( tag ); + } + else if (tag.match(this.patInlineDTDNode)) { + // Tag is inline, so check for nested nodes. + this.patNextClose.lastIndex = this.patTag.lastIndex; + + while (!tag.match(this.patEndDTD)) { + if (matches = this.patNextClose.exec(this.text)) { + tag += '>' + matches[1]; + } + else { + this.throwParseError( "Unclosed DTD tag", tag ); + return null; + } + } + + this.patTag.lastIndex = this.patNextClose.lastIndex; + + // Make sure complete tag is well-formed, and push onto DTD stack. + if (tag.match(this.patDTDNode)) { + this.dtdNodeList.push( tag ); + } + else { + this.throwParseError( "Malformed DTD tag", tag ); + return null; + } + } + else { + this.throwParseError( "Malformed DTD tag", tag ); + return null; + } + + return tag; +}; + +XML.prototype.parseCDATANode = function(tag) { + // Parse CDATA Node, e.g. <![CDATA[Brooks & Shields]]> + var matches = null; + this.patNextClose.lastIndex = this.patTag.lastIndex; + + while (!tag.match(this.patEndCDATA)) { + if (matches = this.patNextClose.exec(this.text)) { + tag += '>' + matches[1]; + } + else { + this.throwParseError( "Unclosed CDATA tag", tag ); + return null; + } + } + + this.patTag.lastIndex = this.patNextClose.lastIndex; + + if (matches = tag.match(this.patCDATANode)) { + return matches[1]; + } + else { + this.throwParseError( "Malformed CDATA tag", tag ); + return null; + } +}; + +XML.prototype.getTree = function() { + // get reference to parsed XML tree + return this.tree; +}; + +XML.prototype.compose = function() { + // compose tree back into XML + var raw = compose_xml( this.tree, this.documentNodeName ); + var body = raw.substring( raw.indexOf("\n") + 1, raw.length ); + var xml = ''; + + if (this.piNodeList.length) { + for (var idx = 0, len = this.piNodeList.length; idx < len; idx++) { + xml += '<' + this.piNodeList[idx] + '>' + "\n"; + } + } + else { + xml += xml_header + "\n"; + } + + if (this.dtdNodeList.length) { + for (var idx = 0, len = this.dtdNodeList.length; idx < len; idx++) { + xml += '<' + this.dtdNodeList[idx] + '>' + "\n"; + } + } + + xml += body; + return xml; +}; + +// +// Static Utility Functions: +// + +var parse_xml = exports.parse = function parse_xml(text, opts) { + // turn text into XML tree quickly + if (!opts) opts = {}; + opts.text = text; + var parser = new XML(opts); + return parser.error() ? parser.getLastError() : parser.getTree(); +}; + +var trim = exports.trim = function trim(text) { + // strip whitespace from beginning and end of string + if (text == null) return ''; + + if (text && text.replace) { + text = text.replace(/^\s+/, ""); + text = text.replace(/\s+$/, ""); + } + + return text; +}; + +var encode_entities = exports.encodeEntities = function encode_entities(text) { + // Simple entitize exports.for = function for composing XML + if (text == null) return ''; + + if (text && text.replace) { + text = text.replace(/\&/g, "&"); // MUST BE FIRST + text = text.replace(/</g, "<"); + text = text.replace(/>/g, ">"); + } + + return text; +}; + +var encode_attrib_entities = exports.encodeAttribEntities = function encode_attrib_entities(text) { + // Simple entitize exports.for = function for composing XML attributes + if (text == null) return ''; + + if (text && text.replace) { + text = text.replace(/\&/g, "&"); // MUST BE FIRST + text = text.replace(/</g, "<"); + text = text.replace(/>/g, ">"); + text = text.replace(/\"/g, """); + text = text.replace(/\'/g, "'"); + } + + return text; +}; + +var decode_entities = exports.decodeEntities = function decode_entities(text) { + // Decode XML entities into raw ASCII + if (text == null) return ''; + + if (text && text.replace && text.match(/\&/)) { + text = text.replace(/\<\;/g, "<"); + text = text.replace(/\>\;/g, ">"); + text = text.replace(/\"\;/g, '"'); + text = text.replace(/\&apos\;/g, "'"); + text = text.replace(/\&\;/g, "&"); // MUST BE LAST + } + + return text; +}; + +var compose_xml = exports.stringify = function compose_xml(node, name, indent) { + // Compose node into XML including attributes + // Recurse for child nodes + var xml = ""; + + // If this is the root node, set the indent to 0 + // and setup the XML header (PI node) + if (!indent) { + indent = 0; + xml = xml_header + "\n"; + + if (!name) { + // no name provided, assume content is wrapped in it + name = first_key(node); + node = node[name]; + } + } + + // Setup the indent text + var indent_text = ""; + for (var k = 0; k < indent; k++) indent_text += indent_string; + + if ((typeof(node) == 'object') && (node != null)) { + // node is object -- now see if it is an array or hash + if (!node.length) { // what about zero-length array? + // node is hash + xml += indent_text + "<" + name; + + var num_keys = 0; + var has_attribs = 0; + for (var key in node) num_keys++; // there must be a better way... + + if (node["_Attribs"]) { + has_attribs = 1; + var sorted_keys = hash_keys_to_array(node["_Attribs"]).sort(); + for (var idx = 0, len = sorted_keys.length; idx < len; idx++) { + var key = sorted_keys[idx]; + xml += " " + key + "=\"" + encode_attrib_entities(node["_Attribs"][key]) + "\""; + } + } // has attribs + + if (num_keys > has_attribs) { + // has child elements + xml += ">"; + + if (node["_Data"]) { + // simple text child node + xml += encode_entities(node["_Data"]) + "</" + name + ">\n"; + } // just text + else { + xml += "\n"; + + var sorted_keys = hash_keys_to_array(node).sort(); + for (var idx = 0, len = sorted_keys.length; idx < len; idx++) { + var key = sorted_keys[idx]; + if ((key != "_Attribs") && key.match(re_valid_tag_name)) { + // recurse for node, with incremented indent value + xml += compose_xml( node[key], key, indent + 1 ); + } // not _Attribs key + } // foreach key + + xml += indent_text + "</" + name + ">\n"; + } // real children + } + else { + // no child elements, so self-close + xml += "/>\n"; + } + } // standard node + else { + // node is array + for (var idx = 0; idx < node.length; idx++) { + // recurse for node in array with same indent + xml += compose_xml( node[idx], name, indent ); + } + } // array of nodes + } // complex node + else { + // node is simple string + xml += indent_text + "<" + name + ">" + encode_entities(node) + "</" + name + ">\n"; + } // simple text node + + return xml; +}; + +var always_array = exports.alwaysArray = function always_array(obj, key) { + // if object is not array, return array containing object + // if key is passed, work like XMLalwaysarray() instead + if (key) { + if ((typeof(obj[key]) != 'object') || (typeof(obj[key].length) == 'undefined')) { + var temp = obj[key]; + delete obj[key]; + obj[key] = new Array(); + obj[key][0] = temp; + } + return null; + } + else { + if ((typeof(obj) != 'object') || (typeof(obj.length) == 'undefined')) { return [ obj ]; } + else return obj; + } +}; + +var hash_keys_to_array = exports.hashKeysToArray = function hash_keys_to_array(hash) { + // convert hash keys to array (discard values) + var array = []; + for (var key in hash) array.push(key); + return array; +}; + +var isa_hash = exports.isaHash = function isa_hash(arg) { + // determine if arg is a hash + return( !!arg && (typeof(arg) == 'object') && (typeof(arg.length) == 'undefined') ); +}; + +var isa_array = exports.isaArray = function isa_array(arg) { + // determine if arg is an array or is array-like + if (typeof(arg) == 'array') return true; + return( !!arg && (typeof(arg) == 'object') && (typeof(arg.length) != 'undefined') ); +}; + +var first_key = exports.firstKey = function first_key(hash) { + // return first key from hash (unordered) + for (var key in hash) return key; + return null; // no keys in hash +}; + +var num_keys = exports.numKeys = function num_keys(hash) { + // count the number of keys in a hash + var count = 0; + for (var a in hash) count++; + return count; +}; diff --git a/node.d/sma_webbox.node.js b/node.d/sma_webbox.node.js new file mode 100755 index 000000000..5ed1c55a7 --- /dev/null +++ b/node.d/sma_webbox.node.js @@ -0,0 +1,237 @@ +'use strict'; + +// This program will connect to one or more SMA Sunny Webboxes +// to get the Solar Power Generated (current, today, total). + +// example configuration in /etc/netdata/sma_webbox.conf +/* +{ + "enable_autodetect": false, + "update_every": 5, + "servers": [ + { + "name": "plant1", + "hostname": "10.0.1.1", + "update_every": 10 + }, + { + "name": "plant2", + "hostname": "10.0.2.1", + "update_every": 15 + } + ] +} +*/ + +var url = require('url'); +var http = require('http'); +var netdata = require('netdata'); + +if(netdata.options.DEBUG === true) netdata.debug('loaded ' + __filename + ' plugin'); + +var webbox = { + name: __filename, + enable_autodetect: true, + update_every: 1, + base_priority: 60000, + charts: {}, + + processResponse: function(service, data) { + if(data !== null) { + var r = JSON.parse(data); + + var d = { + 'GriPwr': { + unit: null, + value: null + }, + 'GriEgyTdy': { + unit: null, + value: null + }, + 'GriEgyTot': { + unit: null, + value: null + } + }; + + // parse the webbox response + // and put it in our d object + var found = 0; + var len = r.result.overview.length; + while(len--) { + var e = r.result.overview[len]; + if(typeof(d[e.meta]) !== 'undefined') { + found++; + d[e.meta].value = e.value; + d[e.meta].unit = e.unit; + } + } + + // add the service + if(found > 0 && service.added !== true) + service.commit(); + + // Grid Current Power Chart + if(d['GriPwr'].value !== null) { + var id = 'sma_webbox_' + service.name + '.current'; + var chart = webbox.charts[id]; + + if(typeof chart === 'undefined') { + chart = { + id: id, // the unique id of the chart + name: '', // the unique name of the chart + title: service.name + ' Current Grid Power', // the title of the chart + units: d['GriPwr'].unit, // the units of the chart dimensions + family: 'now', // the family of the chart + context: 'sma_webbox.grid.power', // the context of the chart + type: netdata.chartTypes.area, // the type of the chart + priority: webbox.base_priority + 1, // the priority relative to others in the same family + update_every: service.update_every, // the expected update frequency of the chart + dimensions: { + 'GriPwr': { + id: 'GriPwr', // the unique id of the dimension + name: 'power', // the name of the dimension + algorithm: netdata.chartAlgorithms.absolute,// the id of the netdata algorithm + multiplier: 1, // the multiplier + divisor: 1, // the divisor + hidden: false // is hidden (boolean) + } + } + }; + + chart = service.chart(id, chart); + webbox.charts[id] = chart; + } + + service.begin(chart); + service.set('GriPwr', Math.round(d['GriPwr'].value)); + service.end(); + } + + if(d['GriEgyTdy'].value !== null) { + var id = 'sma_webbox_' + service.name + '.today'; + var chart = webbox.charts[id]; + + if(typeof chart === 'undefined') { + chart = { + id: id, // the unique id of the chart + name: '', // the unique name of the chart + title: service.name + ' Today Grid Power', // the title of the chart + units: d['GriEgyTdy'].unit, // the units of the chart dimensions + family: 'today', // the family of the chart + context: 'sma_webbox.grid.power.today', // the context of the chart + type: netdata.chartTypes.area, // the type of the chart + priority: webbox.base_priority + 2, // the priority relative to others in the same family + update_every: service.update_every, // the expected update frequency of the chart + dimensions: { + 'GriEgyTdy': { + id: 'GriEgyTdy', // the unique id of the dimension + name: 'power', // the name of the dimension + algorithm: netdata.chartAlgorithms.absolute,// the id of the netdata algorithm + multiplier: 1, // the multiplier + divisor: 1000, // the divisor + hidden: false // is hidden (boolean) + } + } + }; + + chart = service.chart(id, chart); + webbox.charts[id] = chart; + } + + service.begin(chart); + service.set('GriEgyTdy', Math.round(d['GriEgyTdy'].value * 1000)); + service.end(); + } + + if(d['GriEgyTot'].value !== null) { + var id = 'sma_webbox_' + service.name + '.total'; + var chart = webbox.charts[id]; + + if(typeof chart === 'undefined') { + chart = { + id: id, // the unique id of the chart + name: '', // the unique name of the chart + title: service.name + ' Total Grid Power', // the title of the chart + units: d['GriEgyTot'].unit, // the units of the chart dimensions + family: 'total', // the family of the chart + context: 'sma_webbox.grid.power.total', // the context of the chart + type: netdata.chartTypes.area, // the type of the chart + priority: webbox.base_priority + 3, // the priority relative to others in the same family + update_every: service.update_every, // the expected update frequency of the chart + dimensions: { + 'GriEgyTot': { + id: 'GriEgyTot', // the unique id of the dimension + name: 'power', // the name of the dimension + algorithm: netdata.chartAlgorithms.absolute,// the id of the netdata algorithm + multiplier: 1, // the multiplier + divisor: 1000, // the divisor + hidden: false // is hidden (boolean) + } + } + }; + + chart = service.chart(id, chart); + webbox.charts[id] = chart; + } + + service.begin(chart); + service.set('GriEgyTot', Math.round(d['GriEgyTot'].value * 1000)); + service.end(); + } + } + }, + + // module.serviceExecute() + // this function is called only from this module + // its purpose is to prepare the request and call + // netdata.serviceExecute() + serviceExecute: function(name, hostname, update_every) { + if(netdata.options.DEBUG === true) netdata.debug(this.name + ': ' + name + ': hostname: ' + hostname + ', update_every: ' + update_every); + + var service = netdata.service({ + name: name, + request: netdata.requestFromURL('http://' + hostname + '/rpc'), + update_every: update_every, + module: this + }); + service.postData = 'RPC={"proc":"GetPlantOverview","format":"JSON","version":"1.0","id":"1"}'; + service.request.method = 'POST'; + service.request.headers['Content-Length'] = service.postData.length; + + service.execute(this.processResponse); + }, + + configure: function(config) { + var added = 0; + + if(typeof(config.servers) !== 'undefined') { + var len = config.servers.length; + while(len--) { + if(typeof config.servers[len].update_every === 'undefined') + config.servers[len].update_every = this.update_every; + + if(config.servers[len].update_every < 5) + config.servers[len].update_every = 5; + + this.serviceExecute(config.servers[len].name, config.servers[len].hostname, config.servers[len].update_every); + added++; + } + } + + return added; + }, + + // module.update() + // this is called repeatidly to collect data, by calling + // netdata.serviceExecute() + update: function(service, callback) { + service.execute(function(serv, data) { + service.module.processResponse(serv, data); + callback(); + }); + }, +}; + +module.exports = webbox; diff --git a/node.d/snmp.node.js b/node.d/snmp.node.js new file mode 100755 index 000000000..21615f623 --- /dev/null +++ b/node.d/snmp.node.js @@ -0,0 +1,436 @@ +'use strict'; + +// This program will connect to one or more SNMP Agents + +// example configuration in /etc/netdata/snmp.conf +/* +{ + "enable_autodetect": false, + "update_every": 5, + "max_request_size": 50, + "servers": [ + { + "hostname": "10.11.12.8", + "community": "public", + "update_every": 10, + "max_request_size": 50, + "options": { "timeout": 10000 }, + "charts": { + "snmp_switch.bandwidth_port1": { + "title": "Switch Bandwidth for port 1", + "units": "kilobits/s", + "type": "area", + "priority": 1, + "dimensions": { + "in": { + "oid": ".1.3.6.1.2.1.2.2.1.10.1", + "algorithm": "incremental", + "multiplier": 8, + "divisor": 1024 + }, + "out": { + "oid": ".1.3.6.1.2.1.2.2.1.16.1", + "algorithm": "incremental", + "multiplier": -8, + "divisor": 1024 + } + } + }, + "snmp_switch.bandwidth_port2": { + "title": "Switch Bandwidth for port 2", + "units": "kilobits/s", + "type": "area", + "priority": 1, + "dimensions": { + "in": { + "oid": ".1.3.6.1.2.1.2.2.1.10.2", + "algorithm": "incremental", + "multiplier": 8, + "divisor": 1024 + }, + "out": { + "oid": ".1.3.6.1.2.1.2.2.1.16.2", + "algorithm": "incremental", + "multiplier": -8, + "divisor": 1024 + } + } + } + } + } + ] +} +*/ + +// You can also give ranges of charts like the following. +// This will append 1-24 to id, title, oid (on each dimension) +// so that 24 charts will be created. +/* +{ + "enable_autodetect": false, + "update_every": 10, + "max_request_size": 50, + "servers": [ + { + "hostname": "10.11.12.8", + "community": "public", + "update_every": 10, + "max_request_size": 50, + "options": { "timeout": 20000 }, + "charts": { + "snmp_switch.bandwidth_port": { + "title": "Switch Bandwidth for port ", + "units": "kilobits/s", + "type": "area", + "priority": 1, + "multiply_range": [ 1, 24 ], + "dimensions": { + "in": { + "oid": ".1.3.6.1.2.1.2.2.1.10.", + "algorithm": "incremental", + "multiplier": 8, + "divisor": 1024 + }, + "out": { + "oid": ".1.3.6.1.2.1.2.2.1.16.", + "algorithm": "incremental", + "multiplier": -8, + "divisor": 1024 + } + } + } + } + } + ] +} +*/ + +var net_snmp = require('net-snmp'); +var extend = require('extend'); +var netdata = require('netdata'); + +if(netdata.options.DEBUG === true) netdata.debug('loaded ' + __filename + ' plugin'); + +netdata.processors.snmp = { + name: 'snmp', + + fixoid: function(oid) { + if(typeof oid !== 'string') + return oid; + + if(oid.charAt(0) === '.') + return oid.substring(1, oid.length); + + return oid; + }, + + prepare: function(service) { + if(typeof service.snmp_oids === 'undefined' || service.snmp_oids === null || service.snmp_oids.length === 0) { + // this is the first time we see this service + + if(netdata.options.DEBUG === true) + netdata.debug(service.module.name + ': ' + service.name + ': preparing ' + this.name + ' OIDs'); + + // build an index of all OIDs + service.snmp_oids_index = {}; + for(var c in service.request.charts) { + // for each chart + + if(netdata.options.DEBUG === true) + netdata.debug(service.module.name + ': ' + service.name + ': indexing ' + this.name + ' chart: ' + c); + + if(typeof service.request.charts[c].titleoid !== 'undefined') { + service.snmp_oids_index[this.fixoid(service.request.charts[c].titleoid)] = { + type: 'title', + link: service.request.charts[c] + }; + } + + for(var d in service.request.charts[c].dimensions) { + // for each dimension in the chart + + var oid = this.fixoid(service.request.charts[c].dimensions[d].oid); + var oidname = this.fixoid(service.request.charts[c].dimensions[d].oidname); + + if(netdata.options.DEBUG === true) + netdata.debug(service.module.name + ': ' + service.name + ': indexing ' + this.name + ' chart: ' + c + ', dimension: ' + d + ', OID: ' + oid + ", OID name: " + oidname); + + // link it to the point we need to set the value to + service.snmp_oids_index[oid] = { + type: 'value', + link: service.request.charts[c].dimensions[d] + }; + + if(typeof oidname !== 'undefined') + service.snmp_oids_index[oidname] = { + type: 'name', + link: service.request.charts[c].dimensions[d] + }; + + // and set the value to null + service.request.charts[c].dimensions[d].value = null; + } + } + + if(netdata.options.DEBUG === true) + netdata.debug(service.module.name + ': ' + service.name + ': indexed ' + this.name + ' OIDs: ' + netdata.stringify(service.snmp_oids_index)); + + // now create the array of OIDs needed by net-snmp + service.snmp_oids = new Array(); + for(var o in service.snmp_oids_index) + service.snmp_oids.push(o); + + if(netdata.options.DEBUG === true) + netdata.debug(service.module.name + ': ' + service.name + ': final list of ' + this.name + ' OIDs: ' + netdata.stringify(service.snmp_oids)); + + service.snmp_oids_cleaned = 0; + } + else if(service.snmp_oids_cleaned === 0) { + service.snmp_oids_cleaned = 1; + + // the second time, keep only values + service.snmp_oids = new Array(); + for(var o in service.snmp_oids_index) + if(service.snmp_oids_index[o].type === 'value') + service.snmp_oids.push(o); + } + }, + + getdata: function(service, index, ok, failed, callback) { + var that = this; + + if(index >= service.snmp_oids.length) { + callback((ok > 0)?{ ok: ok, failed: failed }:null); + return; + } + + var slice; + if(service.snmp_oids.length <= service.request.max_request_size) { + slice = service.snmp_oids; + index = service.snmp_oids.length; + } + else if(service.snmp_oids.length - index <= service.request.max_request_size) { + slice = service.snmp_oids.slice(index, service.snmp_oids.length); + index = service.snmp_oids.length; + } + else { + slice = service.snmp_oids.slice(index, index + service.request.max_request_size); + index += service.request.max_request_size; + } + + if(netdata.options.DEBUG === true) + netdata.debug(service.module.name + ': ' + service.name + ': making ' + slice.length + ' entries request, max is: ' + service.request.max_request_size); + + service.snmp_session.get(slice, function(error, varbinds) { + if(error) { + service.error('Received error = ' + netdata.stringify(error) + ' varbinds = ' + netdata.stringify(varbinds)); + + // make all values null + var len = slice.length; + while(len--) + service.snmp_oids_index[slice[len]].value = null; + } + else { + if(netdata.options.DEBUG === true) + netdata.debug(service.module.name + ': ' + service.name + ': got valid ' + service.module.name + ' response: ' + netdata.stringify(varbinds)); + + for(var i = 0; i < varbinds.length; i++) { + var value = null; + + if(net_snmp.isVarbindError(varbinds[i])) { + if(netdata.options.DEBUG === true) + netdata.debug(service.module.name + ': ' + service.name + ': failed ' + service.module.name + ' get for OIDs ' + varbinds[i].oid); + + service.error('OID ' + varbinds[i].oid + ' gave error: ' + snmp.varbindError(varbinds[i])); + value = null; + failed++; + } + else { + if(netdata.options.DEBUG === true) + netdata.debug(service.module.name + ': ' + service.name + ': found ' + service.module.name + ' value of OIDs ' + varbinds[i].oid + " = " + varbinds[i].value); + + value = varbinds[i].value; + ok++; + } + + if(value !== null) { + switch(service.snmp_oids_index[varbinds[i].oid].type) { + case 'title': service.snmp_oids_index[varbinds[i].oid].link.title += ' ' + value; break; + case 'name' : service.snmp_oids_index[varbinds[i].oid].link.name = value; break; + case 'value': service.snmp_oids_index[varbinds[i].oid].link.value = value; break; + } + } + } + + if(netdata.options.DEBUG === true) + netdata.debug(service.module.name + ': ' + service.name + ': finished ' + service.module.name + ' with ' + ok + ' successful and ' + failed + ' failed values'); + } + that.getdata(service, index, ok, failed, callback); + }); + }, + + process: function(service, callback) { + this.prepare(service); + + if(service.snmp_oids.length === 0) { + // no OIDs found for this service + + if(netdata.options.DEBUG === true) + service.error('no OIDs to process.'); + + callback(null); + return; + } + + if(typeof service.snmp_session === 'undefined' || service.snmp_session === null) { + // no SNMP session has been created for this service + // the SNMP session is just the initialization of NET-SNMP + + if(netdata.options.DEBUG === true) + netdata.debug(service.module.name + ': ' + service.name + ': opening ' + this.name + ' session on ' + service.request.hostname + ' community ' + service.request.community + ' options ' + netdata.stringify(service.request.options)); + + // create the SNMP session + service.snmp_session = net_snmp.createSession (service.request.hostname, service.request.community, service.request.options); + + if(netdata.options.DEBUG === true) + netdata.debug(service.module.name + ': ' + service.name + ': got ' + this.name + ' session: ' + netdata.stringify(service.snmp_session)); + + // if we later need traps, this is how to do it: + //service.snmp_session.trap(net_snmp.TrapType.LinkDown, function(error) { + // if(error) console.error('trap error: ' + netdata.stringify(error)); + //}); + } + + // do it, get the SNMP values for the sessions we need + this.getdata(service, 0, 0, 0, callback); + } +}; + +var snmp = { + name: __filename, + enable_autodetect: true, + update_every: 1, + base_priority: 50000, + + charts: {}, + + processResponse: function(service, data) { + if(data !== null) { + if(service.added !== true) + service.commit(); + + for(var c in service.request.charts) { + var chart = snmp.charts[c]; + + if(typeof chart === 'undefined') { + chart = service.chart(c, service.request.charts[c]); + snmp.charts[c] = chart; + } + + service.begin(chart); + + for( var d in service.request.charts[c].dimensions ) + if(service.request.charts[c].dimensions[d].value !== null) + service.set(d, service.request.charts[c].dimensions[d].value); + + service.end(); + } + } + }, + + // module.serviceExecute() + // this function is called only from this module + // its purpose is to prepare the request and call + // netdata.serviceExecute() + serviceExecute: function(conf) { + if(netdata.options.DEBUG === true) + netdata.debug(this.name + ': snmp hostname: ' + conf.hostname + ', update_every: ' + conf.update_every); + + var service = netdata.service({ + name: conf.hostname, + request: conf, + update_every: conf.update_every, + module: this, + processor: netdata.processors.snmp + }); + + // multiply the charts, if required + for(var c in service.request.charts) { + if(netdata.options.DEBUG === true) + netdata.debug(this.name + ': snmp hostname: ' + conf.hostname + ', examining chart: ' + c); + + if(typeof service.request.charts[c].update_every === 'undefined') + service.request.charts[c].update_every = service.update_every; + + if(typeof service.request.charts[c].multiply_range !== 'undefined') { + var from = service.request.charts[c].multiply_range[0]; + var to = service.request.charts[c].multiply_range[1]; + var prio = service.request.charts[c].priority || 1; + + if(prio < snmp.base_priority) prio += snmp.base_priority; + + while(from <= to) { + var id = c + from.toString(); + var chart = extend(true, {}, service.request.charts[c]); + chart.title += from.toString(); + + if(typeof chart.titleoid !== 'undefined') + chart.titleoid += from.toString(); + + chart.priority = prio++; + for(var d in chart.dimensions) { + chart.dimensions[d].oid += from.toString(); + + if(typeof chart.dimensions[d].oidname !== 'undefined') + chart.dimensions[d].oidname += from.toString(); + } + service.request.charts[id] = chart; + from++; + } + + delete service.request.charts[c]; + } + else { + if(service.request.charts[c].priority < snmp.base_priority) + service.request.charts[c].priority += snmp.base_priority; + } + } + + service.execute(this.processResponse); + }, + + configure: function(config) { + var added = 0; + + if(typeof config.max_request_size === 'undefined') + config.max_request_size = 50; + + if(typeof(config.servers) !== 'undefined') { + var len = config.servers.length; + while(len--) { + if(typeof config.servers[len].update_every === 'undefined') + config.servers[len].update_every = this.update_every; + + if(typeof config.servers[len].max_request_size === 'undefined') + config.servers[len].max_request_size = config.max_request_size; + + this.serviceExecute(config.servers[len]); + added++; + } + } + + return added; + }, + + // module.update() + // this is called repeatidly to collect data, by calling + // service.execute() + update: function(service, callback) { + service.execute(function(serv, data) { + service.module.processResponse(serv, data); + callback(); + }); + }, +}; + +module.exports = snmp; |