#!/bin/sh
#
# Local puppet daemon activity checker.
#
# $Id: check_puppet 382 2012-07-27 15:36:07Z alexey $

# Puppet client and Puppet master daemon pidfiles.
: ${PUPPETD_PIDFILE:="/var/run/puppet/agent.pid"}
: ${PUPPET_MASTERD_PIDFILE:="/var/run/puppet/master.pid"}

# Where puppetd and puppetmasterd store state associated
# with the running configuration.
: ${PUPPETD_STATE:="/var/puppet/state/state.yaml"}

# Timeouts for state mtime in seconds.
: ${PUPPETD_STATE_WARNING:=3600}
: ${PUPPETD_STATE_CRITICAL:=36000}

# The default checks triggers. The most popular puppet installation is
# client-only instance. So puppet master checks disabled by default.
: ${CHECK_PUPPETD:="yes"}
: ${CHECK_PUPPET_MASTERD:="no"}

# Prefix output string with one of the following keywords:
# OK, WARNING, CRITICAL, UNKNOWN.
: ${OUTPUT_PREFIXED:="no"}

# Configurable options ends here.

# The option string. See getopt(3) for details.
OPTIONS="hvcCmMp:P:s:e:E:"

# Exit codes.
STATE_OK=0
STATE_WARNING=1
STATE_CRITICAL=2
STATE_UNKNOWN=3
STATE_DEPENDENT=4

# usage() and version() are helper functions to work with help2man program.
#
# Outputs program usage instructions.
usage() 
{
	_me=$(basename $0)
	cat << EOF
${_me} observes for Puppet daemon activity by checking the pid file against the
process running on the system for both Puppet master daemon and Puppet client.
In the client mode local configuration freshness is also being checked.

Usage: ${_me} [-h | -v] [OPTIONS]

Options:
   -h                Print this help and then exit immediately.
   -v                Print version and exit.
   -c                Enable puppet client checking mode. (default)
   -C                Do not check puppetd.
   -m                Enable puppet master daemon checking mode.
   -M                Do not check puppet master daemon. (default)
   -p path-to-pid    Running puppet client pid file. You should ensure that
		     user running this plugin have sufficient permissions to
		     access this file. Default is /var/run/puppet/agent.pid
   -P path-to-pid    Puppet master daemon pid file. Default pid file path is
                     /var/run/puppet/master.pid
   -s path-to-yaml   Puppet client local configuration YAML. By default puppet
                     stores it's state in /var/puppet/state/state.yaml
   -e                Configuration expiration warning delay.
   -E                Configuration expiration critical delay.

Report bugs to <alexey@renatasystems.org>
EOF
	# Return exitcode given as $1 argument.
	[ $# -eq 1 ] && exit $1

	# Or exit with OK state.
	exit 0
}

# Output version and exit.
version() 
{
	cat <<EOF
$(basename $0) 1.3

Written by Alexey V. Degtyarev
EOF
	exit 0
}

# Helper function to determine given argument either `yes' or `no'.
check_yesno() 
{
	# Accepts exaclty one argument.
	[ $# -eq 1 ] || return 2

	case $1 in
		[Yy][Ee][Ss]|[Tt][Rr][Uu][Ee]|[Oo][Nn]|1) return 0;;
		[Nn][Oo]|[Ff][Aa][Ll][Ss][Ee]|[Oo][Ff][Ff]|0) return 1;;
		*) return 2;
	esac

	# Should not be here ever.
	return 0
}

# Parse given options with builtin getopts function.
parse_options()
{
	while getopts ${OPTIONS} option $@; do
		case ${option} in
			h) usage ${STATE_UNKNOWN};;
			v) version;;
			c) CHECK_PUPPETD="yes";;
			C) CHECK_PUPPETD="no";;
			m) CHECK_PUPPET_MASTERD="yes";;
			M) CHECK_PUPPET_MASTERD="no";;
			p) PUPPETD_PIDFILE=${OPTARG};;
			P) PUPPET_MASTERD_PIDFILE=${OPTARG};;
			s) PUPPETD_STATE=${OPTARG};;
			e) PUPPETD_STATE_WARNING=${OPTARG};;
			E) PUPPETD_STATE_CRITICAL=${OPTARG};;
			?) echo; usage ${STATE_UNKNOWN};;
		esac
	done

	return 0
}


check_options()
{
	# Options -C and -M should not be given together.
	( check_yesno ${CHECK_PUPPETD} || \
	check_yesno ${CHECK_PUPPET_MASTERD} ) || \
	{ echo "ERROR: Neither -c nor -m option specified. Nothing to do."; \
		return 1; }

	# Check that levels are not conflicts each others.
	[ ${PUPPETD_STATE_WARNING} -gt ${PUPPETD_STATE_CRITICAL} ] && \
	{ echo "ERROR: WARNING level must not exceed CRITICAL one."; \
		return 1; }

	return 0
}

# Checks that:
# 1. pid file given in $1 exists;
# 2. pid number specified in pidfile is a real process identificator, i.e.
# process with such number is really exists;
# 3. this process is running puppetd or puppet masterd application with ruby
# interpreter;
#
# Return codes:
# 0: puppetd found running (and set variable name in $2 to pid number);
# 1: Empty pid file name given;
# 2: Can't read the pidfile (i.e. no such file or no access);
# 3: Can't find puppet process running with pid from ${PUPPETD_PIDFILE};
# 4: Process running with pid ${PUPPETD_PIDFILE} is not a puppet daemon;
check_process()
{
	# Check the pid file existence.
	[ ! -z $1 ] || return 1
	[ ! -f $1 -o ! -r $1 ] && return 2

	# Take the process name and arguments from process tree.
	# Linux users need to use some other command.
	_procname="`pgrep -F $1 -l -f`"
	if [ $? -ne 0 ]; then
		return 3
	fi

	_variable=$2

# 14659 /usr/local/bin/ruby18 /usr/local/bin/puppetd --rundir /var/run/puppet
	set -- ${_procname}

	# Take the basename of interpreter running puppetd instance we got from
	# process list.
	_running_interpreter=${2##*/}

	# Preserve the pid
	_pid=$1

	# And take the interpreter from puppetd executable.
	read _interpreter < $3
	case "${_interpreter}" in
		# strip #!
		\#!*) _interpreter=${_interpreter#\#!}
		set -- ${_interpreter}
		_interpreter=${_interpreter##*/}
		;;
		*) _interpreter="/nonexistent"
		;;
	esac

	# Compare two interpretators from ps and from puppetd executable.
	if [ ${_running_interpreter} != ${_interpreter} ]; then
		return 4
	fi

	eval ${_variable}=${_pid}

	return 0
}

# Checks:
# 1. state file ${PUPPETD_STATE} existence.
# 2. state file mtime against ${PUPPETD_STATE_WARNING} and
# ${PUPPETD_STATE_CRITICAL}
#
# Returns:
# 0: State file OK.
# 1: Empty name for state file given.
# 2: State file was not found (or no access).
# 3: State file's mtime is older than the WARNING point, but is newer than the
# CRITICAL one.
# 4: State file's mtime is older than CRITICAL timepoint.
check_activity()
{
	# Check the state file existence.
	[ ! -z ${PUPPETD_STATE} ] || return 1
	[ ! -r ${PUPPETD_STATE} ] && return 2

	# Take file states.
	eval `stat -s ${PUPPETD_STATE}`

	local _now=`date +%s`
	local _diff=`echo ${_now} - ${st_mtime} |bc`
	PUPPETD_STATE_MTIME=${st_mtime}

	# State mtime is less than PUPPETD_STATE_WARNING?
	if [ ${_diff} -lt ${PUPPETD_STATE_WARNING} ]; then
		return 0
	# State is WARN < state < CRIT ?
	elif [ ${_diff} -gt ${PUPPETD_STATE_WARNING} -a \
		${_diff} -lt ${PUPPETD_STATE_CRITICAL} ]; then
		return 3
	# Critical state.
	elif [ ${_diff} -gt ${PUPPETD_STATE_CRITICAL} ]; then
		return 4
	fi

	return 0
}

# Sets global state information. If the given state code number is less than
# existing one (i.e. already set before) - ignore new code silently. This make
# possible to get the worst code error seen while script goes through several
# check points.
set_state()
{
	[ $# -ne 1 ] && return 1

	[ $1 -lt ${STATE} ] || STATE=$1

	return 0
}

# Checks puppet daemon process status and process the exit code returned.
# The ${OUTPUT_STR} status variable will contain some status information.
check_puppetd()
{
	check_process ${PUPPETD_PIDFILE} PUPPETD_PID
	case $? in
		# Pid file and process seems to be OK.
		0)
		_str="puppet client is running as pid: ${PUPPETD_PID}"
		set_state ${STATE_OK};;
		
		# Exit code 1 means unreachable pid file.
		1)
		_str="cannot found puppetd pid: UNKNOWN"
		set_state ${STATE_UNKNOWN};;

		# Codes 2 and 3 telling us puppet daemon is down.
		2|3)
		_str="puppet client is not running: CRITICAL"
		set_state ${STATE_CRITICAL};;

		# Should never been reached.
		*)
		_str="plugin error: UNKNOWN"
		set_state ${STATE_UNKNOWN};;
	esac
	
	# Concat nonempty output or set the output string.
	[ -z "${OUTPUT_STR}" ] && OUTPUT_STR=${_str} || \
		OUTPUT_STR="${OUTPUT_STR}, ${_str}"

	return 0
}

# Checks puppetmaster daemon process status and process the exit code returned.
# The ${OUTPUT_STR} status variable will contain some status information.
check_puppet_masterd()
{
	check_process ${PUPPET_MASTERD_PIDFILE} PUPPET_MASTERD_PID
	case $? in
		# Pid file and process seems to be OK.
		0)
		_str="puppet master is running as pid: ${PUPPET_MASTERD_PID}"
		set_state ${STATE_OK};;
		
		# Exit code 1 means unreachable pid file.
		1)
		_str="cannot found puppet master pid: UNKNOWN"
		set_state ${STATE_UNKNOWN};;

		# Codes 2 and 3 telling us puppet daemon is down.
		2|3)
		_str="puppet master is not running: CRITICAL"
		set_state ${STATE_CRITICAL};;

		# Should never been reached.
		*)
		_str="plugin error: UNKNOWN"
		set_state ${STATE_UNKNOWN};;
	esac
	
	# Concat nonempty output or set the output string.
	[ -z "${OUTPUT_STR}" ] && OUTPUT_STR=${_str} || \
		OUTPUT_STR="${OUTPUT_STR}, ${_str}"

	return 0
}

# The main routine.
#
# Checks process status and configuration file state and outputs status string
# exiting with corresponding status code.
main()
{
	# Set the default values.
	STATE=${STATE_OK}
	PUPPETD_PID=-1

	# Check the client daemon.
	check_yesno ${CHECK_PUPPETD} && check_puppetd

	# Check the master daemon.
	check_yesno ${CHECK_PUPPET_MASTERD} && check_puppet_masterd

	# This will check the configuration file freshness and set the
	# appropriate status.
	check_yesno ${CHECK_PUPPETD} && 
	{ check_activity 
	case $? in
		# Configuration freshness OK.
		0) 
		_time=`date -j -f %s ${PUPPETD_STATE_MTIME} +"%H:%M, %d %b %Y"`
		_str="configuration last recieved at ${_time}"
		set_state ${STATE_OK};;

		# Empty name for state file given?
		1)
		_str="cannot found state file: UNKNOWN"
		set_state ${STATE_UNKNOWN};;

		# Unreachable configuration filename.
		2)
		_str="YAML state was not yet recieved: UNKNOWN"
		set_state ${STATE_UNKNOWN};;

		# Configuration expiring in WARNING state.
		3)
		_now=`date +%s`;
		_time=`echo ${_now}-${PUPPETD_STATE_MTIME} |bc`
		_str="config overdue in ${_time} seconds: WARNING"
		set_state ${STATE_WARNING};;

		# Configuration too old.
		4)
		_now=`date +%s`;
		_time=`date -j -f %s ${PUPPETD_STATE_MTIME} +"%H:%M, %d %b %Y"`
		_str="configuration expired at ${_time}: CRITICAL"
		set_state ${STATE_CRITICAL};;
	esac

	# Concat nonempty output or set the output string.
	[ -z "${OUTPUT_STR}" ] && OUTPUT_STR=${_str} || \
		OUTPUT_STR="${OUTPUT_STR}, ${_str}"
	}

	# Select preferable status prefix.
	if check_yesno ${OUTPUT_PREFIXED}; then
		case ${STATE} in
			${STATE_OK}) _status_str="OK:";;
			${STATE_WARNING}) _status_str="WARNING:";;
			${STATE_CRITICAL}) _status_str="CRITICAL:";;
			${STATE_UNKNOWN}) _status_str="UNKNOWN:";;
			*) _status_str="ERROR:";;
		esac
		_status_str="${_status_str} ${OUTPUT_STR}."
	else
		_status_str="${OUTPUT_STR}."
	fi

	# Output status, set exit code and exit.
	echo ${_status_str}
	return ${STATE}
}

# Parse and check command line options.
parse_options $* && check_options || return ${STATE_UNKNOWN}

main

exit $?
