#!/usr/bin/env python ''' Nagios plug-in to pull the Dell service tag and check it against Dell's web site to see how many days remain. By default it issues a warning when there is less than thirty days remaining and critical when there is less than ten days remaining. These values can be adjusted using the command line, see --help. Version: 2.1.2 Created: 2009-02-12 Author: Erinn Looney-Triggs Revised: 2010-07-19 Revised by: Erinn Looney-Triggs, Justin Ellison, Harald Jensas, Jim Browne ''' #============================================================================= # TODO: omreport md enclosures, cap the threads, tests, more I suppose # # Revision history: # # 2010-07-19 2.1.2: Patch to again fix Dell's web page changes, thanks # to Jim Browne http://blog.jbrowne.com/ as well as a patch to work against # OM 5.3 # # 2010-04-13 2.1.1: Change to deal with Dell's change to their website # dropping the warranty extension field. # # 2009-12-17 2.1: Change format back to % to be compatible with python 2.4 # and older. # # 2009-11-16 2.0: Fix formatting issues, change some variable names, fix # a file open exception issue, Dell changed the interface so updated to # work with that, new option --short for short output. # # 2009-08-07 1.9: Add smbios as a way to get the serial number. # Move away from old string formatting to new string formatting. # # 2009-08-04 1.8: Improved the parsing of Dell's website, output is now much # more complete (read larger) and includes all warranties. Thresholds are # measured against the warranty with the greatest number of days remaining. # This fixes the bug with doubled or even tripled warranty days being # reported. # # 2009-07-24 1.7: SNMP support, DRAC - Remote Access Controller, CMC - # Chassis Management Controller and MD/PV Disk Enclosure support. # # 2009-07-09 1.6: Threads! # # 2009-06-25 1.5: Changed optparse to handle multiple serial numbers. Changed # the rest of the program to be able to handle multiple serial numbers. Added # a de-duper for serial numbers just in case you get two of the same from # the command line or as is the case with Dell blades, two of the same # from omreport. So this ought to handle blades, though I don't have # any to test against. # # 2009-06-05 1.4 Changed optparse to display %default in help output. Pretty # up the help output with instead of variable names. Add description # top optparse. Will now use prefer omreport to dmidecode for systems # that have omreport installed and in $PATH. Note, that you do not have to be # root to run omreport and get the service tag. # # 2009-05-29 1.3 Display output for all warranties for a system. Add up the # number of days left to give an accurate count of the time remaining. Fix # basic check for Dell's database being down. Fixed regex to be non-greedy. # Start and end dates for warranty now takes all warranties into account. # Date output is now yyyy-mm-dd because that is more international. # # 2009-05-28 1.2 Added service tag to output for nagios. Fixed some typos. # Added command-line option for specifying a serial number. This gets # rid of the sudo dependency as well as the newer python dependency # allowing it to run on older RHEL distros. justin@techadvise.com # # 2009-05-27 1.1 Fixed string conversions to do int comparisons properly. # Remove import csv as I am not using that yet. Add a license to the file. # # License: # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # #============================================================================= import os import sys __version__ = '2.1.2' #Nagios exit codes in English UNKNOWN = 3 CRITICAL = 2 WARNING = 1 OK = 0 def extract_serial_number(): '''Extracts the serial number from the localhost using (in order of precedence) omreport, libsmbios, or dmidecode. This function takes no arguments but expects omreport, libsmbios or dmidecode to exist and also expects dmidecode to accept -s system-serial-number (RHEL5 or later). ''' dmidecode = which('dmidecode') libsmbios = False omreport = which('omreport') serial_numbers = [] #Test for the libsmbios module try: import libsmbios_c except ImportError: pass #Module does not exist, move on else: libsmbios = True if omreport: import subprocess import re try: process = subprocess.Popen([omreport, "chassis", "info", "-fmt", "xml"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) except OSError: print 'Error:', sys.exc_info, 'exiting!' sys.exit(WARNING) text = process.stdout.read() pattern = '''(\S+)''' regex = re.compile(pattern, re.X) serial_numbers = regex.findall(text) elif libsmbios: #You have to be root to extract the serial number via this method if os.geteuid() != 0: print ('%s must be run as root in order to access ' 'libsmbios, exiting!') % sys.argv[0] sys.exit(WARNING) serial_numbers.append(libsmbios_c.system_info.get_service_tag()) elif dmidecode: import subprocess #Gather the information from dmidecode sudo = which('sudo') if not sudo: print 'Sudo is not available, exiting!' sys.exit(WARNING) try: process = subprocess.Popen([sudo, dmidecode, "-s", "system-serial-number"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) except OSError: print 'Error:', sys.exc_info, 'exiting!' sys.exit(WARNING) serial_numbers.append(process.stdout.read().strip()) else: print ('Omreport, libsmbios and dmidecode are not available in ' '$PATH, exiting!') sys.exit(WARNING) return serial_numbers def extract_serial_number_snmp( hostname, community_string='public', mtk_installed=False ): ''' Extracts the serial number from the a remote host using SNMP. This function takes the following arguments: hostname, community, and mtk. The mtk argument will make the plug-in read the SNMP community string from /etc/mtk.conf. (/etc/mtk.conf is used by the mtk-nagios plugin. (mtk-nagios plug-in: http://www.hpccommunity.org/sysmgmt/) ''' def run_snmp_command(snmp_cmd, cmdline, hostname, community_string, encl_id=None): ''' Runs the command specified in snmp_cmd and collects the output, the output is then sanitized to be passed back to the requester. ''' import subprocess if encl_id: cmdline = cmdline % (snmp_cmd, community_string, hostname, encl_id) else: cmdline = cmdline % (snmp_cmd, community_string, hostname) try: p = subprocess.Popen(cmdline, shell=True, stdout = subprocess.PIPE, stderr = subprocess.STDOUT) except OSError: print 'Error:', sys.exc_info, 'exiting!' sys.exit(WARNING) #This is where we sanitize the output gathered. output = p.stdout.read() #Things we don't want in the strings: replacement_strings = {'"':'', 'STRING: ':'', 'INTEGER: ':'' } #Strip them out: for old, new in replacement_strings.iteritems(): output = output.replace(old, new).strip() #This output should be clean now. return output serial_numbers = [] snmpget = which('snmpget') snmpgetnext = which('snmpgetnext') snmpwalk = which('snmpwalk') #Test that we actually have snmpget, snmpgetnext and snmpwalk installed if not snmpget: print 'Unable to locate snmpget, exiting!' sys.exit(UNKNOWN) if not snmpgetnext: print 'Unable to locate snmpgetnext, exiting!' sys.exit(UNKNOWN) if not snmpwalk: print 'Unable to locate snmpwalk, exiting!' sys.exit(UNKNOWN) # Get SNMP community string from /etc/mtk.conf if mtk_installed: mtk_conf_file = '/etc/mtk.conf' if os.path.isfile(mtk_conf_file): try: conf_file = open(mtk_conf_file, 'r') except IOError: print 'Unable to open %s, exiting!' % (mtk_conf_file) sys.exit(UNKNOWN) #Iterate over the file and search for the community_string for line in conf_file: token = line.split('=') if token[0] == 'community_string': community_string = token[1].strip() conf_file.close() else: print ('The %s file does not exist, ' 'exiting!') % (mtk_conf_file) sys.exit(UNKNOWN) #SnmpGetNext - Get next OID in Dell tree to decide device type cmdline_snmpgetnext = ('%s -v1 -c %s %s ' 'SNMPv2-SMI::enterprises.674') #SnmpWalk - Get storage enclosure ID's cmdline_get_stor_encl = ('%s -v1 -Ov -c %s %s ' 'SNMPv2-SMI::enterprises.674.' '10893.1.20.130.3.1.1') #SnmpGet - Get storage enclosure type's cmdline_get_stor_encl_type = ('%s -v1 -Ov -c %s %s ' 'SNMPv2-SMI::enterprises.674.' '10893.1.20.130.3.1.16.%s') #SnmpGet - Get storage enclosure serial number cmdline_get_stor_encl_serial = ('%s -v1 -Ov -c %s %s ' 'SNMPv2-SMI::enterprises.674.' '10893.1.20.130.3.1.8.%s') #SnmpGet - Get server serial number (OMSA) cmdline_get_server_serial = ('%s -v1 -Ov -c %s %s ' 'SNMPv2-SMI::enterprises.674.' '10892.1.300.10.1.11.1') #SnmpGet - Get server/blade chassis serial number (RAC) cmdline_get_rac_serial = ('%s -v1 -Ov -c %s %s ' 'SNMPv2-SMI::enterprises.674.' '10892.2.1.1.11.0') #SnmpGet - Get PowerConnect switch serial number cmdline_get_pc_serial = ('%s -v1 -Ov -c %s %s ' 'SNMPv2-SMI::enterprises.674.' '10895.3000.1.2.100.8.1.4.1') #Figure out device type OMSA on Server, DRAC/CMC or PowerConnect Switch snmp_out = run_snmp_command(snmpgetnext, cmdline_snmpgetnext, hostname, community_string) if snmp_out.find('SNMPv2-SMI::enterprises.674.10892.1.') != -1: sys_type = 'omsa' #OMSA answered. elif snmp_out.find('SNMPv2-SMI::enterprises.674.10892.2.') != -1: sys_type = 'RAC' #Blade CMC or Server DRAC answered. elif snmp_out.find('SNMPv2-SMI::enterprises.674.10895.') != -1: sys_type = 'powerconnect' #PowerConnect switch answered. else: print ('snmpgetnext Failed: %s System ' 'type or system unknown!') % (snmp_out) sys.exit(WARNING) #System is server with OMSA, will check for External DAS enclosure #and get service tag. if sys_type == 'omsa': #Is External DAS Storage Enclosure connected #TODO: get rid of the split() snmp_out = run_snmp_command(snmpwalk, cmdline_get_stor_encl, hostname, community_string).split('\n') for encl_id in snmp_out: #For backwards compatibility with OM 5.3 if not encl_id: continue #Get enclosure type. # 1: Internal # 2: DellTM PowerVaultTM 200S (PowerVault 201S) # 3: Dell PowerVault 210S (PowerVault 211S) # 4: Dell PowerVault 220S (PowerVault 221S) # 5: Dell PowerVault 660F # 6: Dell PowerVault 224F # 7: Dell PowerVault 660F/PowerVault 224F # 8: Dell MD1000 # 9: Dell MD1120 encl_type = run_snmp_command(snmpget, cmdline_get_stor_encl_type, hostname, community_string, encl_id) if encl_type != '1': #Enclosure type 1 is integrated backplane. #Get storage enclosure Service Tag. encl_serial_number = run_snmp_command(snmpget, cmdline_get_stor_encl_serial, hostname, community_string, encl_id) serial_numbers.append(encl_serial_number) #Get system Service Tag. serial_number = run_snmp_command(snmpget, cmdline_get_server_serial, hostname, community_string) # Get DRAC/CMC or PowerConnect Service Tag. elif sys_type == 'RAC': serial_number = run_snmp_command(snmpget, cmdline_get_rac_serial, hostname, community_string) elif sys_type == 'powerconnect': serial_number = run_snmp_command(snmpget, cmdline_get_pc_serial, hostname, community_string) serial_numbers.append(serial_number) return serial_numbers def get_warranty(serial_numbers): ''' Obtains the warranty information from Dell's website. This function expects a list containing one or more serial numbers to be checked against Dell's database. ''' import re import thread import time import urllib2 thread_id = 0 result_list = [] list_write_mutex = thread.allocate_lock() exit_mutexes = [0] * len(serial_numbers) #The URL to Dell's site dell_url = ('http://support.dell.com/support/topics/global.aspx/support/' + 'my_systems_info/details?c=us&l=en&s=gen&ServiceTag=') pattern = r"""( #Capture )""" #Closing tag regex = re.compile(pattern, re.X) def fetch_result(thread_id, serial_number, dell_url, regex): '''Opens a connection to Dell's website and fetches the output of the page using the regex that is passed in. This function expects to be used in a threaded setup as such it requires a thread id. In addition the serial number to be used needs to be passed as well as the url and the regex to pull the info. ''' #Basic check of the serial number. if len( serial_number ) != 7 and len( serial_number ) != 5: print 'Invalid serial number: %s exiting!' % (serial_number) sys.exit(WARNING) #Build the full URL full_url = dell_url + serial_number #Try to open the page, exit on failure try: response = urllib2.urlopen(full_url) except URLError, error: if hasattr(error, 'reason'): print ('Unable to open URL: ' '%s exiting! %s') % (full_url, error.reason) sys.exit(UNKNOWN) elif hasattr(error, 'code'): print ('The server is unable to fulfill ' 'the request, error: %s') % (error.code) sys.exit(UNKNOWN) list_write_mutex.acquire() #Acquire our lock to write to the list result_list.append((regex.findall(response.read()), serial_number)) list_write_mutex.release() #Release the lock exit_mutexes[thread_id] = 1 #Communicate that this thread is done thread.exit() #Not necessary, but pretty # Remove duplicates: if len( serial_numbers ) > 1: serial_numbers = dict.keys(dict.fromkeys(serial_numbers)) for serial_number in serial_numbers: thread.start_new(fetch_result, (thread_id, serial_number, dell_url, regex)) thread_id += 1 #Check that all threads have exited while 0 in exit_mutexes: time.sleep(.05) #Give the CPU a break return result_list def parse_exit(result_list, short_output=False): '''This parses the results from the get_warranty() function and outputs the appropriate information. ''' import datetime import re critical = 0 warning = 0 def i8n_date(date): ''' Simple function that takes a North American style date string seperated by '/'s and converts it to ISO standard date format of yyy-mm-dd and returns it. ''' month, day, year = date.split('/') return datetime.date(int(year), int(month), int(day)) def parse_table(table): '''Takes an HTML string of a table and returns a list of lists of the contents of the table. ''' results = [] row_pattern = """(.*?)""" table_pattern = """(.*?)""" row_regex = re.compile(row_pattern) table_regex = re.compile(table_pattern) #Pull the rows out rows = row_regex.findall(table) for row in rows: row_list = [] tables = table_regex.findall(row) #Clean the tables for table in tables: row_list.append(re.sub(r'<[^>]*?>', '', table)) results.append(row_list) return results for result in result_list: days = [] serial_number = result[1] #We start to build our output line full_line = r'%s: Service Tag: %s' if len(result[0]) == 0: print "Dell's database appears to be down." sys.exit(WARNING) for match in result[0]: #Because there can be multiple warranties for one system we get #them all warranties = parse_table(match) #Remove the header lines. header = warranties.pop(0) notice_present = (header[2].find("Warranty") >= 0) for entry in warranties: if notice_present: (description, provider, notice, start_date, end_date, days_left) = entry[0:6] else: (description, provider, start_date, end_date, days_left) = entry[0:5] #Convert the dates to international standard start_date = str(i8n_date(start_date)) end_date = str(i8n_date(end_date)) if short_output: full_line = full_line + ', End: ' + end_date \ + ', Days left: ' + days_left else: full_line = full_line + ' Warranty: ' + description \ + ', Provider: ' + provider + ', Start: ' + start_date \ + ', End: ' + end_date + ', Days left: ' + days_left days.append(int(days_left)) #Put the days remaining in ascending order days.sort() if days[-1] < options.critical_days: state = 'CRITICAL' critical += 1 elif days[-1] < options.warning_days: state = 'WARNING' warning += 1 else: state = 'OK' print full_line % (state, serial_number), if critical: sys.exit(CRITICAL) elif warning: sys.exit(WARNING) else: sys.exit(OK) return None #Should never get here def sigalarm_handler(signum, frame): ''' Handler for an alarm situation. ''' print ('%s timed out after %s seconds, ' 'signum:%s, frame: %s') % (sys.argv[0], options.timeout, signum, frame) sys.exit(CRITICAL) def which(program): '''This is the equivalent of the 'which' BASH built-in with a check to make sure the program that is found is executable. ''' def is_exe(file_path): '''Tests that a file exists and is executable. ''' return os.path.exists(file_path) and os.access(file_path, os.X_OK) file_path = os.path.split(program)[0] if file_path: if is_exe(program): return program else: for path in os.environ["PATH"].split(os.pathsep): exe_file = os.path.join(path, program) if is_exe(exe_file): return exe_file return None if __name__ == '__main__': import optparse import signal parser = optparse.OptionParser(description='''Nagios plug-in to pull the Dell service tag and check it against Dell's web site to see how many days remain. By default it issues a warning when there is less than thirty days remaining and critical when there is less than ten days remaining. These values can be adjusted using the command line, see --help. ''', prog="check_dell_warranty", version="%prog Version: 2.1.2") parser.add_option('-C', '--community', action='store', dest='community_string', type='string',default='public', help=('SNMP Community String to use. ' '(Default: %default)')) parser.add_option('-c', '--critical', dest='critical_days', default=10, help=('Number of days under which to return critical ' '(Default: %default)'), type='int', metavar='') parser.add_option('-H', '--hostname', action='store', type='string', dest='hostname', help='Specify hostname for SNMP') parser.add_option('--mtk', action='store_true', dest='mtk_installed', default=False, help=('Get SNMP Community String from /etc/mtk.conf if ' 'mtk-nagios plugin is installed. NOTE: This option ' 'will make the mtk.conf community string take ' 'precedence over anything entered at the ' 'command line (Default: %default)')) parser.add_option('-s', '--serial-number', dest='serial_number', help=('Dell Service Tag of system, to enter more than ' 'one use multiple flags (Default: auto-detected)'), action='append', metavar='') parser.add_option('-S', '--short', dest='short_output', action='store_true', default = False, help=('Display short output: only the status, ' 'service tag, end date and days left for each ' 'warranty.')) parser.add_option('-t', '--timeout', dest='timeout', default=10, help=('Set the timeout for the program to run ' '(Default: %default seconds)'), type='int', metavar='') parser.add_option('-w', '--warning', dest='warning_days', default=30, help=('Number of days under which to return a warning ' '(Default: %default)'), type='int', metavar='' ) (options, args) = parser.parse_args() signal.signal(signal.SIGALRM, sigalarm_handler) signal.alarm(options.timeout) if options.serial_number: SERIAL_NUMBERS = options.serial_number elif options.hostname or options.mtk_installed: SERIAL_NUMBERS = extract_serial_number_snmp(options.hostname, options.community_string, options.mtk_installed) else: SERIAL_NUMBERS = extract_serial_number() RESULT = get_warranty(SERIAL_NUMBERS) signal.alarm(0) parse_exit(RESULT, options.short_output)