lib/arborist/monitor/snmp.rb
changeset 8 e0b7c95a154f
parent 7 4548e58c8c66
child 14 d5cb8bd33170
--- 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'