diff -r 4548e58c8c66 -r e0b7c95a154f lib/arborist/monitor/snmp.rb --- a/lib/arborist/monitor/snmp.rb Wed Aug 30 13:55:02 2017 -0700 +++ b/lib/arborist/monitor/snmp.rb Wed Apr 04 11:00:35 2018 -0700 @@ -1,109 +1,133 @@ # -*- ruby -*- # vim: set noet nosta sw=4 ts=4 : #encoding: utf-8 -# + +require 'arborist/monitor' unless defined?( Arborist::Monitor ) +require 'net-snmp2' + # SNMP checks for Arborist. Requires an SNMP agent to be installed -# on target machine, and the various "pieces" enabled. For your platform. +# on target machine, and the various "pieces" enabled for your platform. # # For example, for disk monitoring with Net-SNMP, you'll want to set # 'includeAllDisks' in the snmpd.conf. bsnmpd on FreeBSD benefits from # the 'bsnmp-ucd' package. Etc. # - -require 'loggability' -require 'arborist/monitor' unless defined?( Arborist::Monitor ) -require 'snmp' - -using Arborist::TimeRefinements +module Arborist::Monitor::SNMP + using Arborist::TimeRefinements + extend Configurability, Loggability -# Shared SNMP monitor logic. -# -module Arborist::Monitor::SNMP - extend Loggability - log_to :arborist - - # The version of this library. - VERSION = '0.3.1' - - # Global defaults for instances of this monitor - # - DEFAULT_OPTIONS = { - timeout: 2, - retries: 1, - community: 'public', - port: 161 - } + # Loggability API + log_to :arborist_snmp # Always request the node addresses and any config. USED_PROPERTIES = [ :addresses, :config ].freeze - ### Return the properties used by this monitor. - def self::node_properties - return USED_PROPERTIES + # The OID that returns the system environment. + IDENTIFICATION_OID = '1.3.6.1.2.1.1.1.0' + + # Global defaults for instances of this monitor + # + configurability( 'arborist.snmp' ) do + setting :timeout, default: 2 + setting :retries, default: 1 + setting :community, default: 'public' + setting :version, default: '2c' + setting :port, default: 161 + + # How many hosts to check simultaneously + setting :batchsize, default: 25 end + # Indicate to FFI that we're using threads. + Net::SNMP.thread_safe = true + + + # The system type, as advertised. + attr_reader :system + + # The mapping of addresses back to node identifiers. + attr_reader :identifiers + + # The results hash that is sent back to the manager. + attr_reader :results + ### Connect to the SNMP daemon and yield. ### def run( nodes ) - self.log.debug "Got nodes to SNMP check: %p" % [ nodes ] - opts = Arborist::Monitor::SNMP::DEFAULT_OPTIONS # Create mapping of addresses back to node identifiers, # and retain any custom (overrides) config per node. # @identifiers = {} @results = {} - nodes.each_pair do |(identifier, props)| next unless props.key?( 'addresses' ) address = props[ 'addresses' ].first - @identifiers[ address ] = [ identifier, props['config'] ] + self.identifiers[ address ] = [ identifier, props['config'] ] end # Perform the work! # - threads = [] - @identifiers.keys.each do |host| - thr = Thread.new do - Thread.current.abort_on_exception = true + mainstart = Time.now + threads = ThreadGroup.new + batchcount = nodes.size / Arborist::Monitor::SNMP.batchsize + self.log.debug "Starting SNMP run for %d nodes" % [ nodes.size ] - config = @identifiers[host].last || {} - opts = { - host: host, - port: config[ 'port' ] || opts[ :port ], - community: config[ 'community' ] || opts[ :community ], - timeout: config[ 'timeout' ] || opts[ :timeout ], - retries: config[ 'retries' ] || opts[ :retries ] - } + self.identifiers.keys.each_slice( Arborist::Monitor::SNMP.batchsize ).each_with_index do |slice, batch| + slicestart = Time.now + self.log.debug " %d hosts (batch %d of %d)" % [ + slice.size, + batch + 1, + batchcount + 1 + ] + + slice.each do |host| + thr = Thread.new do + config = self.identifiers[ host ].last || {} + opts = { + peername: host, + port: config[ 'port' ] || Arborist::Monitor::SNMP.port, + version: config[ 'version' ] || Arborist::Monitor::SNMP.version, + community: config[ 'community' ] || Arborist::Monitor::SNMP.community, + timeout: config[ 'timeout' ] || Arborist::Monitor::SNMP.timeout, + retries: config[ 'retries' ] || Arborist::Monitor::SNMP.retries + } - begin - SNMP::Manager.open( opts ) do |snmp| - yield( snmp, host ) + snmp = Net::SNMP::Session.open( opts ) + begin + @system = snmp.get( IDENTIFICATION_OID ).varbinds.first.value + yield( host, snmp ) + + rescue Net::SNMP::TimeoutError, Net::SNMP::Error => err + self.log.error "%s: %s %s" % [ host, err.message, snmp.error_message ] + self.results[ host ] = { + error: "%s" % [ snmp.error_message ] + } + rescue => err + self.results[ host ] = { + error: "Uncaught exception. (%s: %s)" % [ err.class.name, err.message ] + } + ensure + snmp.close end - rescue SNMP::RequestTimeout - @results[ host ] = { - error: "Host is not responding to SNMP requests." - } - rescue StandardError => err - @results[ host ] = { - error: "Network is not accessible. (%s: %s)" % [ err.class.name, err.message ] - } end + + threads.add( thr ) end - threads << thr + + # Wait for thread completions + threads.list.map( &:join ) + self.log.debug " finished after %0.1f seconds." % [ Time.now - slicestart ] end - - # Wait for thread completion - threads.map( &:join ) + self.log.debug "Completed SNMP run for %d nodes after %0.1f seconds." % [ nodes.size, Time.now - mainstart ] # Map everything back to identifier -> attribute(s), and send to the manager. # - reply = @results.each_with_object({}) do |(address, results), hash| - identifier = @identifiers[ address ] or next + reply = self.results.each_with_object({}) do |(address, results), hash| + identifier = self.identifiers[ address ] or next hash[ identifier.first ] = results end - self.log.debug "Sending to manager: %p" % [ reply ] return reply ensure @@ -113,9 +137,8 @@ end # Arborist::Monitor::SNMP +require 'arborist/monitor/snmp/cpu' require 'arborist/monitor/snmp/disk' -require 'arborist/monitor/snmp/load' +require 'arborist/monitor/snmp/process' require 'arborist/monitor/snmp/memory' -require 'arborist/monitor/snmp/process' -require 'arborist/monitor/snmp/swap'