lib/arborist/monitor/snmp/disk.rb
changeset 4 e6eb11b1e00d
child 8 e0b7c95a154f
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/arborist/monitor/snmp/disk.rb	Wed Sep 07 15:23:42 2016 -0700
@@ -0,0 +1,167 @@
+# -*- ruby -*-
+# vim: set noet nosta sw=4 ts=4 :
+
+require 'arborist/monitor/snmp' unless defined?( Arborist::Monitor::SNMP )
+
+# SNMP Disk capacity checks.
+# Returns all mounts with their current usage percentage in a "mount" attribute.
+#
+class Arborist::Monitor::SNMP::Disk
+	include Arborist::Monitor::SNMP
+
+	extend Loggability
+	log_to :arborist
+
+	# The OID that returns the system environment.
+	IDENTIFICATION_OID = '1.3.6.1.2.1.1.1.0'
+
+	# For net-snmp systems, ignore mount types that match
+	# this regular expression.  This includes null/union mounts
+	# and NFS, currently.
+	STORAGE_IGNORE = %r{25.3.9.(?:2|14)$}
+
+	# The OID that matches a local windows hard disk.  Anything else
+	# is a remote (SMB) mount.
+	WINDOWS_DEVICE = '1.3.6.1.2.1.25.2.1.4'
+
+	# OIDS required to pull disk information from net-snmp.
+	#
+	STORAGE_NET_SNMP = [
+		'1.3.6.1.4.1.2021.9.1.2', # paths
+		'1.3.6.1.2.1.25.3.8.1.4', # types
+		'1.3.6.1.4.1.2021.9.1.9'  # percents
+	]
+
+	# OIDS required to pull disk information from Windows.
+	#
+	STORAGE_WINDOWS = [
+		'1.3.6.1.2.1.25.2.3.1.2', # types
+		'1.3.6.1.2.1.25.2.3.1.3', # paths
+		'1.3.6.1.2.1.25.2.3.1.5', # totalsize
+		'1.3.6.1.2.1.25.2.3.1.6'  # usedsize
+	]
+
+	# Global defaults for instances of this monitor
+	#
+	DEFAULT_OPTIONS = {
+		error_at: 95, # in percent full
+		include:  [], # if non-empty, only these paths are included in checks
+		exclude:  []  # paths to exclude from checks
+	}
+
+
+	### This monitor is complex enough to require creating an instance from the caller.
+	### Provide a friendlier error message the class was provided to exec() directly.
+	###
+	def self::run( nodes )
+		return new.run( nodes )
+	end
+
+
+	### Create a new instance of this monitor.
+	###
+	def initialize( options=DEFAULT_OPTIONS )
+		options = DEFAULT_OPTIONS.merge( options || {} )
+		%i[ include exclude ].each do |opt|
+			options[ opt ] = Array( options[opt] )
+		end
+
+		options.each do |name, value|
+			self.public_send( "#{name.to_s}=", value )
+		end
+	end
+
+	# Set an error if mount points are above this percentage.
+	attr_accessor :error_at
+
+	# Only check these specific mount points.
+	attr_accessor :include
+
+	# Exclude these mount points (array of paths) from checks.
+	attr_accessor :exclude
+
+
+	### Perform the monitoring checks.
+	###
+	def run( nodes )
+		super do |snmp, host|
+			self.gather_disks( snmp, host )
+		end
+	end
+
+
+	#########
+	protected
+	#########
+
+	### Collect mount point usage for +host+ from an existing (and open)
+	#### +snmp+ connection.
+	###
+	def gather_disks( snmp, host )
+		self.log.debug "Getting disk information for %s" % [ host ]
+		errors  = []
+		results = {}
+		mounts  = self.get_disk_percentages( snmp )
+		config  = @identifiers[ host ].last || {}
+
+		includes = config[ 'include' ] || self.include
+		excludes = config[ 'exclude' ] || self.exclude
+
+		mounts.each_pair do |path, percentage|
+			next if excludes.include?( path )
+			next if ! includes.empty? && ! includes.include?( path )
+			if percentage >= ( config[ 'error_at' ] || self.error_at )
+				errors << "%s at %d%% capacity" % [ path, percentage ]
+			end
+		end
+
+		results[ :mounts ] = mounts
+		results[ :error ] = errors.join( ', ' ) unless errors.empty?
+
+		@results[ host ] = results
+	end
+
+
+	### Given a SNMP object, return a hash of:
+	###
+	###    device path => percentage full
+	###
+	def get_disk_percentages( snmp )
+
+		# Does this look like a windows system, or a net-snmp based one?
+		system_type = snmp.get( SNMP::ObjectId.new( IDENTIFICATION_OID ) ).varbind_list.first.value
+		disks = {}
+
+		# Windows has it's own MIBs.
+		#
+		if system_type =~ /windows/i
+			snmp.walk( STORAGE_WINDOWS ) do |list|
+				next unless list[0].value.to_s == WINDOWS_DEVICE
+				disks[ list[1].value.to_s ] = ( list[3].value.to_f / list[2].value.to_f ) * 100
+			end
+			return disks
+		end
+
+		# Everything else.
+		#
+		snmp.walk( STORAGE_NET_SNMP ) do |list|
+			mount = list[0].value.to_s
+			next if mount == 'noSuchInstance'
+
+			next if list[2].value.to_s == 'noSuchInstance'
+			used = list[2].value.to_i
+
+			unless list[1].value.to_s == 'noSuchInstance'
+				typeoid = list[1].value.join('.').to_s
+				next if typeoid =~ STORAGE_IGNORE
+			end
+			next if mount =~ /\/(?:dev|proc)$/
+
+			disks[ mount ] = used
+		end
+
+		return disks
+	end
+
+end # class Arborist::Monitor::SNMP::Disk
+