#!/bin/bash
# check_ipmi_sensor: Nagios/Icinga plugin to check IPMI sensors
#
# Copyright (C) 2009-2010 Thomas-Krenn.AG (written by Werner Fischer),
# additional contributors see changelog.txt
#
# 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 <http://www.gnu.org/licenses/>.
#
################################################################################
# The following guides provide helpful information if you want to extend this
# script:
#   http://tldp.org/LDP/abs/html/ (Advanced Bash-Scripting Guide)
#   http://www.gnu.org/manual/gawk/ (Gawk: Effective AWK Programming)
#   http://de.wikibooks.org/wiki/Awk (awk Wikibook, in German)
#   http://nagios.sourceforge.net/docs/3_0/customobjectvars.html (hints on
#                  custom object variables)
#   http://nagiosplug.sourceforge.net/developer-guidelines.html (plug-in
#                  development guidelines)
#   http://nagios.sourceforge.net/docs/3_0/pluginapi.html (plugin API)
################################################################################

################################################################################
# possible future enhancements for this plugin:
# * error handling for creation of SDR cache file
# * search/query for other locations of ipmitool in case it is not in /usr/bin/
#   (e.g. in /bin, /usr/bin, /usr/local/bin) - e.g. using
#   IPMITOOL=$(which ipmitool)
# * find an alternative to 'shopt -u nocasematch' as this option is only
#   available in bash-3.1-alpha1 or newer, see
#   http://tiswww.case.edu/php/chet/bash/CHANGES
# * if $ipmioutput contains "error" maybe add query if server is switched on
#   -> error message could be more precise then 
# * option to change performance data output to omit quotes and to use
#   underscores instead, e.g. to return Fan_1=5432 instead of 'Fan 1'=5432 - 
#   this should allow Zenoss (http://community.zenoss.org/) users to use this
#   plugin (a Zenoss user reported that zenoss has issues with creating graphs
#   for data sources with quotes)
# * check whether there is a possibility to notice a failed power supply when a
#   'PS Redundancy' sensor is reported as ok with 'Redundancy Lost', see this
#   posting on the ipmitool mailinglist:
#   http://sourceforge.net/mailarchive/message.php?msg_name=4BBD4F0E.2050909%40spiritual-machines.org
# * validate if an option to execute different ipmitool queries instead of
#   'ipmitool sdr', e.g. 'ipmitool sunoem sbled get SERVICE' (with additional
#   info on how the output should look like) would be useful 
################################################################################

################################################################################
# set ipmitool path
IPMITOOL="/usr/bin/ipmitool"

################################################################################
# set text variables
version_text="check_ipmi_sensor version 1.3 20110111
Copyright (C) 2009-2010 Thomas-Krenn.AG (written by Werner Fischer)"
usage_text="Usage: check_ipmi_sensor -H <hostname> [-I lan|lanplus] [-L <privilege level>]
                         -U <username> -P <password>|-f <password_file>
                         -T <sensor type>|-t <SDR type> [-x <sensor name>]
                         [-v 1|2|3] [-h] [-V]"
help_text="Options:
  -H <hostname>
         hostname or IP of the IPMI interface
         if given as \"localhost\", username/password are not needed
  [-I lan|lanplus]
         lan ...... uses unencrypted lan interface
         lanplus .. uses encrypted lanplus interface (default)
  [-L CALLBACK|USER|OPERATOR|ADMINISTRATOR]
         force IPMI session privilege level,
         can be CALLBACK, USER, OPERATOR, ADMINISTRATOR,
         default is ADMINISTRATOR,
         for a higher security it is recommended to configure a dedicated IPMI
         user with limited privileges (USER privilege level) and to use this
         option with the parameter '-L USER'
  -U <username>
         IPMI username
  -P <password>
         IPMI password
  -f <password_file>
         file containing the IPMI password,
         in this way the password will not be visible in the process list,
         limit access to this file, e.g. by 'chmod 640', 'chown root.nagios'
  -T <sensor type>
         limit sensors to query based on IPMI sensor type
         examples for IPMI sensor type are 'Fan', 'Temperature', 'Voltage', ...
         see chapter '42.2 Sensor Type Codes and Data' of the IPMI 2.0 spec or
         'ipmitool sdr type list' for a full list of possible sensor types,
         the available types depend on the particular server and the
         available sensors there, see 'ipmitool sdr elist all' for a list
         containing all sensor and locator records of your server;
         if you want to limit sensors to query based on IPMI SDR type,
         use the -t option instead
  -t <SDR type>
         limit sensors to query based on IPMI SDR type
         SDR types can be (taken from 'man ipmitool'):
           all ...... All SDR records (Sensor and Locator)
           full ..... Full Sensor Record
           compact .. Compact Sensor Record
           event .... Event-Only Sensor Record
           mcloc .... Management Controller Locator Record
           fru ...... FRU Locator Record
           generic .. Generic SDR records
         Hint: use 'all' to query all of your sensors with a single query
         For more details see (in German):
           http://www.thomas-krenn.com/de/wiki/Ipmitool_zur_Sensorabfrage_von_Servern_nutzen#ipmitool_sdr_type_TYPE
           http://www.thomas-krenn.com/de/wiki/Ipmitool_zur_Sensorabfrage_von_Servern_nutzen#ipmitool_sdr_list.7Celist
         if you want to limit sensors to query based on IPMI sensor type,
         use the -T option instead
  [-x <sensor name>]
  	 exclude sensor matching <sensor name>. Usefull for cases, where unused
         sensors can't be deleted from SDR and are reported in a non-OK state.
  	 Option can be specified multiple times.
  [-v 1|2|3]
         be verbose
         (no -v) .. single line output
         -v 1 ..... single line output with additional details for warnings
         -v 2 ..... multi line output, also with additional details for warnings
         -v 3 ..... debugging output, followed by normal multi line output
  [-h]
         show this help
  [-V]
         show version information

This plugin checks all IPMI sensors of a given type of a server remotely (or
locally if \"-H localhost\" is used.
You can use the sensor type (-T) or the SDR type (-t) to limit the sensors you
wand to query. As soon as at least one of the monitored sensors has a status
other than 'ok' or 'ns' the plugin reports a warning.
For a given server the plugin can be used in one of the following ways:
  - one service check for a given server (using '-t all')
  - multiple service checks (configured with an individual service check for
    each sensor type (-T) or SDR type (-t) that you want to monitor)

Examples: the following examples show the use of different sensor types for an
          Intel SR2500 server system. As multiple sensor types are available in
          this server you can configure multiple service checks, executing the
          plugin with the following parameters (in this example 192.168.1.211 is
          the IP of the IPMI BMC of the server, 'monitor' the username, and
          'relation' the password):
check_ipmi_sensor -H 192.168.1.211 -L USER -U monitor -P relation -T Temperature
check_ipmi_sensor -H 192.168.1.211 -L USER -U monitor -P relation -T Voltage
check_ipmi_sensor -H 192.168.1.211 -L USER -U monitor -P relation -T Current
check_ipmi_sensor -H 192.168.1.211 -L USER -U monitor -P relation -T Fan
check_ipmi_sensor -H 192.168.1.211 -L USER -U monitor -P relation -T 'Physical Security'
... and so on.

Further information about this plugin and more example can be found on the
Thomas Krenn Wiki (currently only in German):
http://www.thomas-krenn.com/de/wiki/IPMI_Sensor_Monitoring_Plugin

Send email to the IPMI-plugin-user mailing list if you have questions regarding
use of this software, to submit patches, or suggest improvements.
The mailing list is available at http://lists.thomas-krenn.com/
"
abort_text=""
################################################################################
# read parameters 
while getopts "I:H:U:P:f:T:t:L:v:x:hV" option
do
	case $option in
		I)	IPMI_INTERFACE=$OPTARG;;
		H)	IPMI_IP=$OPTARG;;
		U)	IPMI_USER=$OPTARG;;
		P)	IPMI_PASSWORD=$OPTARG;;
		f)	IPMI_PASSWORD_FILE=$OPTARG;;
		T)	IPMI_SENSOR_TYPE=$OPTARG;;
		t)	IPMI_SDR_TYPE=$OPTARG;;
		L)	IPMI_PRIVLVL=$OPTARG;;
		v)	VERBOSITY=$OPTARG;;
		x)	if [ -z "$IPMI_XLIST" ]; then
				IPMI_XLIST="$OPTARG"
			else
				IPMI_XLIST="${IPMI_XLIST};$OPTARG"
			fi
			;;
		h)	echo "$version_text"
			echo
			echo "$usage_text"
			echo
			echo "$help_text"
		  	exit 0;;
		V)	echo "$version_text"
		  	exit 0;;
		\?)	echo "$usage_text"
		  	exit 3;;
	esac
done

################################################################################
# verify if all mandatory parameters are set
if [ -z "$IPMI_IP" ]; then abort_text="$abort_text IPMI-IP"; fi
if [ "$IPMI_IP" != "localhost" ]; then
	# credentials are only neccessary for remote logins
	if [ -z "$IPMI_USER" ]; then abort_text="$abort_text IPMI-USER"; fi
	if [ -z "$IPMI_PASSWORD" ]; then
		if [ -z "$IPMI_PASSWORD_FILE" ]; then abort_text="$abort_text IPMI-PASSWORD or IPMI-PASSWORD-FILE"; fi
	fi
fi
if [ -z "$IPMI_SENSOR_TYPE" ]; then 
	if [ -z "$IPMI_SDR_TYPE" ]; then abort_text="$abort_text IPMI-SENSOR-TYPE or IPMI-SDR-TYPE"; fi
fi
if [ -n "$abort_text" ]; then
	echo "Error:$abort_text missing."
	echo "$usage_text"
	exit 3
fi

################################################################################
# initialize various variables
IPMI_SDR_CACHE_FILE="/tmp/ipmi-sdr-cache-$IPMI_IP"
if [ "$IPMI_IP" == "localhost" ]; then
	BASECOMMAND="sudo $IPMITOOL"
else
	if [ "$IPMI_INTERFACE" == "lan" ]; then
		BASECOMMAND="$IPMITOOL -I lan"
	else
		BASECOMMAND="$IPMITOOL -I lanplus"
	fi
	if [ -n "$IPMI_PASSWORD_FILE" ]; then
		BASECOMMAND="$BASECOMMAND -H $IPMI_IP -U $IPMI_USER -f $IPMI_PASSWORD_FILE"
	else
		BASECOMMAND="$BASECOMMAND -H $IPMI_IP -U $IPMI_USER -P $IPMI_PASSWORD"
	fi
fi
if [ -n "$IPMI_PRIVLVL" ]; then BASECOMMAND="$BASECOMMAND -L $IPMI_PRIVLVL"; fi
CREATE_SDR_CACHE="$BASECOMMAND sdr dump $IPMI_SDR_CACHE_FILE"
shopt -s nocasematch
if [ -n "$IPMI_SDR_TYPE" ]; then
	GET_STATUS="$BASECOMMAND -S $IPMI_SDR_CACHE_FILE sdr elist \"$IPMI_SDR_TYPE\""
else
	# for backward compatibility to check_ipmi_sensor version 1.1 and older
	# treat "-T full" like "-t full"
	if [[ $IPMI_SENSOR_TYPE = "full" ]]
	then
		GET_STATUS="$BASECOMMAND -S $IPMI_SDR_CACHE_FILE sdr elist full"
	else
		GET_STATUS="$BASECOMMAND -S $IPMI_SDR_CACHE_FILE sdr type \"$IPMI_SENSOR_TYPE\""
	fi
fi
shopt -u nocasematch

################################################################################
# check if IPMI_SDR_CACHE_FILE exists
if [ ! -e $IPMI_SDR_CACHE_FILE ]
then
	# create IPMI_SDR_CACHE_FILE
	$CREATE_SDR_CACHE &> /dev/null
fi

################################################################################
# execute $GET_STATUS
# * uses old-style backquote so the backslash retains its literal meaning except
#   when followed by ‘$’, ‘`’, or ‘\’
#   see http://www.gnu.org/software/bash/manual/bashref.html#Command-Substitution
ipmioutput=`eval $GET_STATUS 2>&1`
returncode=$?

################################################################################
# print debug output when verbosity is set to 3 (-v 3)
if [ "$VERBOSITY" = "3" ]
then
	echo "------------- begin of debug output (-v 3 is set): ------------"
	echo "  script was executed with the following parameters:"
	echo "    $0 $@"
	echo "  ipmitool was executed with the following parameters:"
	echo "    $GET_STATUS"
	echo "  ipmitool return code: $returncode"
	echo "  output of ipmitool:"
	echo "$ipmioutput"
	echo "--------------------- end of debug output ---------------------"
fi

errorstring="Error"
shopt -s nocasematch
if [[ "$ipmioutput" =~ "${errorstring}" ]]
then
  if [[ "${errorstring}" =~ ^"Fatal IO Err" ]]
  then
	checkhint="check BMC availability/username/password"
	echo "ipmitool output contains \"$errorstring\" - $checkhint"
	exit 2
  fi
fi
shopt -u nocasematch

################################################################################
# generate main output
if [ $returncode != 0 ]
then
	echo "Execution of ipmitool failed with return code $returncode."
	echo "ipmitool was executed with the following parameters:"
        echo "$GET_STATUS"
	echo "Please check hostname, username, password, and sensor or SDR type."
	exit 3
else
	if [ -n "$IPMI_SDR_TYPE" ]; then
		echo -n "SDR type '$IPMI_SDR_TYPE' Status: ";
	else
		echo -n "sensor type '$IPMI_SENSOR_TYPE' Status: ";
        fi
	echo "$ipmioutput" | gawk -v verbosity=$VERBOSITY -v xlist="$IPMI_XLIST" -F '|' '
################################################################################
# * BEGIN rule is executed once only, before the first input record is read
#   see http://www.gnu.org/manual/gawk/html_node/Using-BEGIN_002fEND.html
# * we initialize variables here
BEGIN {
	EXIT=0
	number_of_numerical_records=0
	w_sensors=""
	split(xlist,xl_array,";")
}

################################################################################
# * the empty pattern below is used to match every input record
#   see http://www.gnu.org/manual/gawk/html_node/Empty.html
# * we fill the following arrays with data here:
#   - arrays containing all sensors:  
#     - sensor_id[] .......... contains the name of the sensor, e.g. "Fan 1"
#     - sensor_status[] ...... contains the status of the sensor, e.g. "ok"
#     - sensor_reading[] ..... contains the sensor reading , e.g. "5719 RPM"
#   - arrays containing only numerical sensors (for performance data)
#     - n_record_id[] ...... contains the name of the sensor, e.g. "Fan 1"
#     - n_record_value[] ... contains the numerical reading, e.g. "5719"
{
	########################################################################
	# Remove extra spaces
	gsub(/ +$/,"",$1)
	gsub(/^ +/,"",$3)
	gsub(/ +$/,"",$3)
	gsub(/^ +/,"",$5)
	gsub(/ +$/,"",$5)

	sensor_id[NR]=$1
	sensor_status[NR]=$3
	sensor_reading[NR]=$5

	for (ind in xl_array)
	{
		if (sensor_id[NR] == xl_array[ind])
		{
			next
		}
	}
	########################################################################
	# * set EXIT variable to 1 if a sensor is not "ok" or "ns"
	# * also build contents of w_sensors variable (sensors with status not
	#   ok) in this case
	if (sensor_status[NR] != "ok" && sensor_status[NR] != "ns")
	{
		EXIT=1
		if (verbosity>0)
		{
			if (w_sensors == "")
				w_sensors=sensor_id[NR]" = "sensor_status[NR]" ("sensor_reading[NR]")"
			else
				w_sensors=w_sensors", "sensor_id[NR]" = "sensor_status[NR]" ("sensor_reading[NR]")"
		}
		else
		{
			if (w_sensors == "")
				w_sensors=sensor_id[NR]" = "sensor_status[NR]
			else
				w_sensors=w_sensors", "sensor_id[NR]" = "sensor_status[NR]
		}
	}

	########################################################################
	# * split the fifth field (containing e.g. "5719 RPM") in an array
	#   called sensor_reading_array[], containing e.g. the values
	#     sensor_reading_array[1] = 5719
	#     sensor_reading_array[2] = RPM
	# * see http://www.gnu.org/manual/gawk/html_node/String-Functions.html
	split($5,sensor_reading_array," ")
	
	########################################################################
	# * use match() and regex to determine whether there is a numerical
	#   value or not
	# * set "IGNORECASE = 1" so the regex is not case sensitive
	# * see http://www.gnu.org/manual/gawk/html_node/Regexp-Operators.html
	#       http://www.gnu.org/manual/gawk/html_node/Case_002dsensitivity.html
	IGNORECASE = 1
	isnumber=match(sensor_reading_array[2],"degrees|volts|RPM|Amps|Watts")
	IGNORECASE = 0
	if (isnumber != "0")
	{
		number_of_numerical_records++
		n_record_id[number_of_numerical_records]=sensor_id[NR]
		n_record_value[number_of_numerical_records]=sensor_reading_array[1]
	}
}

################################################################################
# * END rule is executed once only, after all the input is read
#   see http://www.gnu.org/manual/gawk/html_node/Using-BEGIN_002fEND.html
# * we print the data which has been collected above in this part below
END {
	########################################################################
	# * build perfdata string (variable pstring) using quotes
	#   see http://www.gnu.org/manual/gawk/html_node/Quoting.html
	while(j<number_of_numerical_records) {
		j++
		pstring=pstring"\47"n_record_id[j]"\47="n_record_value[j]" "
	}

	########################################################################
	# * print status message (first text output line)
	if (EXIT==0)
	{
		if (number_of_numerical_records>0)
			print "OK | "pstring
		else
			print "OK"
	}
	else
	{
		if (number_of_numerical_records>0)
			print "Warning ["w_sensors"] | "pstring
		else
			print "Warning ["w_sensors"]"
	}

	########################################################################
	# * print additional text lines (multi-line output) for verbosity > 1
	if (verbosity>1)
	{
		while(i<FNR)
		{
			i++
			print sensor_id[i],"=",sensor_reading[i],"(Status:",sensor_status[i]")"
		}
	}
	exit EXIT
}
'
fi
