#!/bin/bash # ====================================================================== # # ICINGA PASSIVE CLIENT # # Run all passive checks and send response to Icinga Endpoint # # Requirements # - curl # - jq # ---------------------------------------------------------------------- # ah = axel.hahn@iml.unibe.ch # 2021-03-.. init # 2022-01-11 v0.7 ah shellcheck # 2022-02-16 v0.8 ah add --cfg param # 2022-03-04 v0.9 ah abort on http 5xx error # 2022-03-14 v0.10 ah less output and add _elog to run as a service # ====================================================================== _product="ICINGA PASSIVE CLIENT" _version="0.10" _license="GNU GPL 3.0" _copyright='(c) 2020 Institute for Medical Education * University of Bern' typeset -i debug=0 # source config ... . "$( dirname $0 )/inc_getconfig.sh" # where to find check scripts ... first directory wins # dir_plugins="/opt/imlmonitor/client/plugins/ /usr/lib64/nagios/plugins" # dir_cfg="/etc/icinga2-passive-client" # dir_data="/var/tmp/icinga2-passive-client" # dir_logs="/var/log/icinga2-passive-client" logfile="${dir_logs}/execution.log" ch="$( dirname $0 )/inc/confighandler.sh" myHost=$(hostname -f) # for loop mode only: max. random sleep time typeset -i sleeptime=30 typeset -i _rc_all=0 # ---------------------------------------------------------------------- # # FUNCTIONS # # ---------------------------------------------------------------------- # .................................................................. # # helper to make http base setup for host actions function _initHttp(){ # see inc_functions _initHttpWithConfigfile "${dir_cfg}/api-icinga2.cfg" if [ $debug -ne 0 ]; then http.setDebug 1 fi } # ...................................................................... # # find first place of the check script in the known plugin dirs # see ${dir_plugins} in checks.cfg # param string name of the check script without path # function findCheckScript(){ local _script=$1 for mydir in ${dir_plugins} do if [ -x "${mydir}/${_script}" ]; then echo "${mydir}/${_script}" fi done | head -1 } # helper used function in loopChecks # get a snapshot of a few files function _getFileSnapshot(){ ls -l $(dirname $0)/* ${dir_cfg}/* } # ...................................................................... # # Loop over executing all checks # no params # function loopChecks(){ # TODO-MEMORY-CHECK # echo ${myHost} | egrep "^(kvm4|icinga)" # echo ${myHost} | egrep "^(monitortest)" # if [ $? -ne 0 ]; then # echo "HARD EXIT - DO NOT EXECUTE ANY CHECK ON $myHost" # exit 1 # fi local lockfile lockfile="${dir_data}/loop.pid" local snapShotStart snapShotStart=${dir_data}/$(basename $0)-start.fingerprint local snapShotCurrent snapShotCurrent=${dir_data}/$(basename $0)-last.fingerprint if [ -f "${lockfile}" ]; then local lockpid lockpid=$(cat "${lockfile}" | cut -f 2 -d "-" | cut -f 4 -d " " | grep "[0-9]") ps -f --pid "$lockpid" | grep "$(basename $0)" | grep loop >/dev/null if [ $? -eq 0 ]; then _elog "ABORT: Loop seems to run already. See process with PID $lockpid" _elog $( ps -f --pid "$lockpid" ) exit 0 fi fi _log "---------- starting in a permanent loop" echo "Serviceloop started $(date) - process id $$" > "${lockfile}" if [ $? -ne 0 ]; then _elog "ABORT: Lock file is not writable ${lockfile}." _elog $( ls -l "${lockfile}" ); exit 1 fi _getFileSnapshot>"${snapShotStart}" if [ $? -ne 0 ]; then _elog "ABORT: Snapshot file is not writable ${snapShotStart}." _elog $( ls -l "${snapShotStart}" ) exit 1 fi while true; do # typeset -i local iSleep=$(($RANDOM%$sleeptime)) # sleep minimum is half of $sleeptime typeset -i local iSleep=$(($RANDOM%$sleeptime/2+$sleeptime/2)) _log "sleeping $iSleep sec ..." sleep $iSleep _log "______________________________________________________________________" _log "" _getFileSnapshot>$snapShotCurrent if [ $? -ne 0 ]; then _elog "ABORT: Snapshot file is not writable ${snapShotCurrent}." _elog $( ls -l "${snapShotCurrent}" ) exit 1 fi diff $snapShotStart $snapShotCurrent >/dev/null if [ $? -ne 0 ]; then _elog "ABORT: Files were updated / overwritten. The loop must be restarted.\n`diff $snapShotStart $snapShotCurrent`" exit 1 fi icingaHostMustExist processAllChecks done } # ...................................................................... # # execute all defined checks one by one # no params # function processAllChecks(){ # loop over all defined checks typeset -i local iChecksTotal iChecksTotal=$(getChecks | wc -l) typeset -i local iCounter iCounter=0 _rc_all=0 typeset -i local iLoopStart iLoopStart=$(_getUnixTs) _log "" _log "------ looping over all checks" for myconfig in $(getChecks) do iCounter=$iCounter+1 _log "--- processing [$iCounter of $iChecksTotal] $myconfig" processCheck "$myconfig" _log "" done typeset -i local iLoopEnd iLoopEnd=$(_getUnixTs) typeset -i local iLoopTime iLoopTime=$iLoopEnd-$iLoopStart _log "------ loop done - needed $iLoopTime sec - rc=$_rc_all" } # ...................................................................... # # parse a config file and set global vars: # checkName # checkCommand # checkInterval # param string full path of a config file # function _parseCheckConfig(){ local _myconfig="$1" if [ ! -r "$_myconfig" ]; then _elog "ERROR: config file is not readable [$_myconfig]" exit 1 fi # EXAMPLE a config contains ... # checkname=check_cronstatus # command=check_cronstatus -param1 -param2 # interval=60 checkName=$(cat $_myconfig | grep ^checkname= | cut -f 2 -d "=") checkCommand=$(cat $_myconfig | grep ^command= | cut -f 2 -d "=") checkInterval=$(cat $_myconfig | grep ^interval= | cut -f 2 -d "=") } # actions for icinga host # param string action; "get" only function icingaHost(){ local _logPrefix="${myHost} :: API |" local _apiRequest=objects/hosts/${myHost} local _localCache=${dir_data}/host_${myHost}_deployed-at-icinga.txt typeset -i local _iRefreshCache=120 local sAction=$1 _initHttp case $sAction in 'get') # update after caching was added in http-component # http.responseImport $_localCache # typeset -i local iAgeLastGet=`http.getRequestAge` # _log "${_logPrefix} INFO: cache is $iAgeLastGet sec old ... TTL is _iRefreshCache=3600 sec" # if [ $iAgeLastGet -eq 0 -o $iAgeLastGet -gt $_iRefreshCache ]; then # _log "${_logPrefix} INFO: request to Icinga GET $_apiRequest" # _getApiObject $_apiRequest $_localCache # fi # new: http.setCacheTtl $_iRefreshCache http.setCacheFile $_localCache http.makeRequest GET $_apiRequest if http.isServerError >/dev/null; then _elog "CRITICAL ERROR: Icinga2 API request failed with a server error GET $_apiRequest" exit 1 fi # set return code of GET action http.isOk >/dev/null ;; *) _log "ERROR: unknown action parameter $sAction" esac } # for check on the beginning of the script: # execute a check only if the host exists on icinga2 # global string myHost # global string dir_data function icingaHostMustExist(){ _log "check if the host [${myHost}] exists on Icinga ..." icingaHost get if [ $? -ne 0 ]; then _echo $( http.getResponse ) if [ "$(http.getStatuscode)" = "000" ]; then _elog "ERROR: Unable to reach the Icinga node. Stopping script current monitoring actions." exit 1 fi _elog "ERROR: host object for ${myHost} is not available on Icinga service (yet) - Status: $(http.getStatuscode)" _echo _echo "ABORTING" _echo _echo "To run checks ..." _echo "- you must create the host on director (check director-cli.sh --hr)" _echo "- the director must deploy the host to icinga daemon" _echo rm -f "${dir_data}"/service__check* 2>/dev/null exit 1 fi _log "OK, found." } # ...................................................................... # # process a single check # param string name of config file # param string name of config file # function processCheck(){ local _myconfig=$1 local _force=$2 typeset -i local iCheckStart iCheckStart=$(_getUnixTs) _parseCheckConfig "${_myconfig}" local _logPrefix="${checkName} |" _log "${_logPrefix} INFO: every ${checkInterval} sec: ${checkCommand}" local _outfile=${dir_data}/service__check__${checkName}__output.txt local _response=${dir_data}/service__check__${checkName}__icinga_response.txt typeset -i local _rc=0 _initHttp # --- check last run ... if never or > $interval then execute doRun=0 if [ ! -f "$_outfile" ]; then _log "${_logPrefix} INFO: Never executed before" doRun=1 else # typeset -i iAgeLastRun=$(($(date +%s) - $(date +%s -r "$_outfile"))) typeset -i iAgeLastRun iAgeLastRun=$(_getFileAge "$_outfile") _log "${_logPrefix} INFO: last run was $iAgeLastRun sec ago ... vs Interval = $checkInterval ... sleeptime = $sleeptime" iAgeLastRun=$iAgeLastRun+$sleeptime if [ $iAgeLastRun -gt $checkInterval ]; then doRun=1 fi if [ ! -z "$_force" ]; then doRun=1 _log "${_logPrefix} INFO: forced execution by given param " fi fi if [ $doRun -ne 0 ]; then myscript=$(echo "$checkCommand" | cut -f 1 -d " ") myFullscript=$(findCheckScript "$myscript") if [ -z "$myFullscript" ]; then _log "${_logPrefix} ERROR: $myscript was not found in any plugin dir" else myparams=$( echo $checkCommand | grep " " | cut -f 2- -d " " ) # # --- this executes the check plugin ... # _log "${_logPrefix} starting $myFullscript $myparams" typeset -i local iTsStart=`date +%s` # $myFullscript $myparams | tee $_outfile eval $myFullscript $myparams > $_outfile rc=$? if [ ! -w $_outfile ]; then _elog "${_logPrefix} ERROR: output file $_outfile is not writable." _elog "${_logPrefix} $( ls -ld ${dir_data} $_outfile )" exit 1 fi typeset -i local iTsEnd=`date +%s` # outPerfdata=`grep '|' $_outfile | cut -f 2 -d '|'` outPerfdata=`grep '|' $_outfile | rev | cut -f 1 -d '|' | rev` _echo _echo -------- check output: _echo $( cat $_outfile ) _echo # echo -------- extracted performance data: # echo $outPerfdata # echo _log "${_logPrefix} check command finished with returncode $rc" _rc=$_rc+$rc # # --- send check result to Icinga # fields of the object # https://icinga.com/docs/icinga2/latest/doc/12-icinga2-api/#process-check-result export CFGSTORAGE="${checkName}output" outputAsText="$(cat $_outfile)" # outputAsJson="$(jq -nR --arg data """${outputAsText}""" '$data')" commandAsJson="$(jq -nR --arg data """${myFullscript} $myparams""" '$data')" ( $ch --set check_source \"${myHost}\" $ch --set check_command "${commandAsJson}" $ch --set exit_status $rc # $ch --set plugin_output "${outputAsJson}" $ch --setfile plugin_output "${_outfile}" $ch --set performance_data "\"${outPerfdata}\"" $ch --set ttl $checkInterval $ch --set execution_start $iTsStart $ch --set execution_end $iTsEnd ) 2>/dev/null # $ch --json data=`$ch --json 2>/dev/null` slot="`_getName4Svcathost ${checkName} | sed 's# #%20#g'`" _log "${_logPrefix} starting POST of data to monitoring server" _echo POST actions/process-check-result?service=${myHost}!${slot} "$data" _APIcall POST actions/process-check-result?service=${myHost}!${slot} "$data" http.responseExport "$_response" if [ ! -w "$_response" ]; then _elog "${_logPrefix} ERROR: responsefile $_response is not writable." _elog "${_logPrefix} $( ls -ld ${dir_data} $_response )" exit 1 fi # --- check if data were sent successfully # fgrep "HTTP/1.1 200" ${_response} >/dev/null # _testHttpOk ${_response} >/dev/null http.isOk >/dev/null if [ $? -eq 0 ]; then _log "${_logPrefix} rc=$rc - OK, response was sent to Icinga" else _elog "${_logPrefix} rc=$rc - WARNING: the check response was NOT sent to Icinga" _rc=$_rc+1 _echo _echo For Debugging: _echo "$( $ch --show --json )" _log "$( $ch --show --json )" fi $ch --flush 2>/dev/null fi else _log "${_logPrefix} SKIP execution." fi # add current result to global returncode _rc_all=$_rc_all+$_rc typeset -i local iCheckEnd=`_getUnixTs` typeset -i local iCheckTime=$iCheckEnd-$iCheckStart _log "${_logPrefix} finished after $iCheckTime sec with returncode $_rc" test $_rc -eq 0 || (_echo; _echo " >>> Check ${checkName} was not OK. See Output block above!"; _echo; _echo) } # ---------------------------------------------------------------------- # help # ---------------------------------------------------------------------- # show help text function showHelp(){ self=`basename $0` cat <<EOH INTRODUCTION $_product v$_version Handle and execute icinga passive checks. With this client can run a single check, all checks or make a permanent loop. A new local check will be added to Icinga while running it the first time. GENERAL PARAMETERS --cfg CONFIGFILE load a costom config file; default: ./inc_getconfig.sh This must be the 1st parameter to be processed. --help or -h or -? show this help and abort. --version or -v show the version abd abort SERVICE ACTIONS --list get a list of local config files --loop Start to check all passive checks in a permanent loop. It makes a random sleep if $sleeptime sec between all loops. This shuffles the access time of all clients making requests to the icinga server. Multiple starts will be detected. This parameter is the optimal choice for a cronjob. --runonce Start to check all passive checks once. This method respects the interval per check. Only outdated checks will be executet. This is a second choice for a cronjob if the runtime of all checks is much shorter than your cronjob interval to prevent multiple processes. Multiple starts will NOT be detected. --run CONFIGFILE Run a check by pointing the config file (see --list) This execution ignores the interval and forces the execution. CONFIG Config file /etc/icinga2-passive-client/client.cfg It sets other used pathes. Config files for checks are in ${dir_cfg}/checks/ DEBUGGING The Output of check and results from Icinga are in ${dir_data}. A log of all performed executed check runs by $self are in $logfile. BTW: do not forget to add a log rotation for it. EOH } function showVersion(){ echo "$_license" echo "$_copyright" echo } # ---------------------------------------------------------------------- # # MAIN # # ---------------------------------------------------------------------- . "$( dirname $0 )/inc_functions.sh" _echo " ______________________________________________________________________________________ _______ __ |_ _|.----.|__|.-----.-----.---.-. _| |_ | __|| || | _ | _ | |_______||____||__||__|__|___ |___._| |_____| ______ __ ______ _____ __ __ | __ \.---.-.-----.-----.|__|.--.--.-----. | | |_|__|.-----.-----.| |_ | __/| _ |__ --|__ --|| || | | -__| | ---| | || -__| || _| |___| |___._|_____|_____||__| \___/|_____| |______|_______|__||_____|__|__||____| v${_version} $_license .. $_copyright ______________________________________________________________________________________ " _elog "Starting $_product $_version" if [ "$1" = "--cfg" ] && [ -n "$2" ]; then echo "INFO: loading custom config [$2]..." . "${2}" shift 2 else . "$( dirname $0 )/inc_getconfig.sh" fi . "$( dirname $0 )/inc/rest-api-client.sh" if [ $# -eq 0 ]; then showHelp exit 0 fi if [ -z "${dir_cfg}" ]; then echo ERROR: $_product is not installed/ configured yet on this machine. exit 1 fi icingaHostMustExist touch ${logfile} while [ $# -gt 0 ]; do case "$1" in '--help' | '-h' | '-?') showHelp exit 0 ;; '--version' | '-v') showVersion exit 0 ;; '--list') getChecks ;; '--loop') loopChecks ;; '--runonce') processAllChecks ;; '--run') processCheck "$2" "force" shift 1 ;; *) echo "ERROR: unknown parameter detected." exit 2 esac shift 1 done echo # remark: # $_rc_all is a collected status code in the loop of all actions # if it is 0 then all checks were OK and have been sent to Icinga # echo exit with status code $_rc_all # exit $_rc_all exit 0 # ----------------------------------------------------------------------