#!/usr/bin/env bash
# status.sh
# Author: Nils Knieling and Contributors- https://github.com/Cyclenerd/static_status
# Simple Bash script to generate a status page.
ME=$(basename "$0")
BASE_PATH=$(dirname "$0")
MY_TIMESTAMP=$(date -u "+%s")
MY_LASTRUN_TIME="0"
BE_LOUD="no"
BE_QUIET="no"
################################################################################
# Usage
################################################################################
function usage {
returnCode="$1"
echo -e "Usage: $ME [OPTION]:
OPTION is one of the following:
\\t silent no output from faulty connections to stout (default: $BE_QUIET)
\\t loud output from successful and faulty connections to stout (default: $BE_LOUD)
\\t debug displays all variables
\\t help displays help (this message)"
exit "$returnCode"
}
case "$1" in
"")
# called without arguments
;;
"silent")
BE_QUIET="yes"
;;
"loud")
BE_LOUD="yes"
;;
"debug")
ONLY_OUTPUT_DEBUG_VARIABLES="yes"
;;
"h" | "help" | "-h" | "-help" | "-?" | *)
usage 0
;;
esac
################################################################################
#### Configuration Section
################################################################################
# Tip: You can also outsource configuration to an extra configuration file.
# Just create a file named 'config' at the location of this script.
# You can find an example here:
# https://github.com/Cyclenerd/static_status/blob/master/config-example
# You can also pass a configuration file with the variable MY_STATUS_CONFIG.
# if a config file has been specified with MY_STATUS_CONFIG=myfile use this one, otherwise default to config
if [[ -z "$MY_STATUS_CONFIG" ]]; then
MY_STATUS_CONFIG="$BASE_PATH/config"
fi
if [ -e "$MY_STATUS_CONFIG" ]; then
if [[ "$BE_LOUD" = "yes" ]] || [[ "$BE_QUIET" = "no" ]]; then
echo "using config from file: $MY_STATUS_CONFIG"
fi
# ignore SC1090
# shellcheck source=/dev/null
source "$MY_STATUS_CONFIG"
fi
# Tip: You can tweak curl parameters via .curlrc config file.
# The default curl config file is checked for in the following places in this order:
# 1. "$CURL_HOME/.curlrc"
# 2. "$HOME/.curlrc"
#
# ~~~ Example .curlrc file ~~~
# # this is a comment
# # change the useragent string
# -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:97.0) Gecko/20100101 Firefox/97.0"
# # ok if certification validation fails
# --insecure
# ~~~ End of example file ~~~
# Title for the status page
MY_STATUS_TITLE=${MY_STATUS_TITLE:-"Status Page"}
# Link for the homepage button
MY_HOMEPAGE_URL=${MY_HOMEPAGE_URL:-"https://github.com/Cyclenerd/static_status"}
# Text for the homepage button
MY_HOMEPAGE_TITLE=${MY_HOMEPAGE_TITLE:-"Homepage"}
# Auto refresh interval in seconds 0 is no refresh
MY_AUTOREFRESH=${MY_AUTOREFRESH:-"0"}
# Shortcut to place the configuration file in a folder.
# Save it without / at the end.
MY_STATUS_CONFIG_DIR=${MY_STATUS_CONFIG_DIR:-"$HOME/status"}
# List with the configuration. What do we want to monitor?
MY_HOSTNAME_FILE=${MY_HOSTNAME_FILE:-"$MY_STATUS_CONFIG_DIR/status_hostname_list.txt"}
# Where should the HTML status page be stored?
MY_STATUS_HTML=${MY_STATUS_HTML:-"$HOME/status_index.html"}
# Where should the SVG status icon be stored?
MY_STATUS_ICON=${MY_STATUS_ICON-"$HOME/status.svg"}
# Icon colors
MY_STATUS_ICON_COLOR_SUCCESS=${MY_STATUS_ICON_COLOR_SUCCESS:-"lime"}
MY_STATUS_ICON_COLOR_WARNING=${MY_STATUS_ICON_COLOR_WARNING:-"orange"}
MY_STATUS_ICON_COLOR_DANGER=${MY_STATUS_ICON_COLOR_DANGER:-"red"}
# Where should the JSON status page be stored? Set to "" to disable JSON output
MY_STATUS_JSON=${MY_STATUS_JSON-"$HOME/status.json"}
# Text file in which you can place a status message.
# If the file exists and has a content, all errors on the status page are overwritten.
MY_MAINTENANCE_TEXT_FILE=${MY_MAINTENANCE_TEXT_FILE:-"$MY_STATUS_CONFIG_DIR/status_maintenance_text.txt"}
# Text file in which you can place notifications of past maintenance work.
MY_PAST_MAINTENANCE_TEXT_FILE=${MY_PAST_MAINTENANCE_TEXT_FILE:-"$MY_STATUS_CONFIG_DIR/status_past_maintenance_text.txt"}
# Duration we wait for response (nc, curl and traceroute).
MY_TIMEOUT=${MY_TIMEOUT:-"2"}
# Duration we wait for response (only ping).
MY_PING_TIMEOUT=${MY_PING_TIMEOUT:-"4"}
MY_PING_COUNT=${MY_PING_COUNT:-"2"}
# Duration we wait for response (only script)
MY_SCRIPT_TIMEOUT=${MY_SCRIPT_TIMEOUT:-20}
# Route to host
MY_TRACEROUTE_HOST=${MY_TRACEROUTE_HOST-"1.1.1.1"} # Cloudflare DNS
# Sets the number of probe packets per hop
MY_TRACEROUTE_NQUERIES=${MY_TRACEROUTE_NQUERIES:-"1"}
# Location for the status files. Please do not edit created files.
MY_HOSTNAME_STATUS_OK=${MY_HOSTNAME_STATUS_OK:-"$MY_STATUS_CONFIG_DIR/status_hostname_ok.txt"}
MY_HOSTNAME_STATUS_DOWN=${MY_HOSTNAME_STATUS_DOWN:-"$MY_STATUS_CONFIG_DIR/status_hostname_down.txt"}
MY_HOSTNAME_STATUS_LASTRUN=${MY_HOSTNAME_STATUS_LASTRUN:-"$MY_STATUS_CONFIG_DIR/status_hostname_last.txt"}
MY_HOSTNAME_STATUS_DEGRADE=${MY_HOSTNAME_STATUS_DEGRADE:-"$MY_STATUS_CONFIG_DIR/status_hostname_degrade.txt"}
MY_HOSTNAME_STATUS_LASTRUN_DEGRADE=${MY_HOSTNAME_STATUS_LASTRUN_DEGRADE:-"$MY_STATUS_CONFIG_DIR/status_hostname_last_degrade.txt"}
MY_HOSTNAME_STATUS_HISTORY=${MY_HOSTNAME_STATUS_HISTORY:-"$MY_STATUS_CONFIG_DIR/status_hostname_history.txt"}
MY_HOSTNAME_STATUS_HISTORY_TEMP_SORT=${MY_HOSTNAME_STATUS_HISTORY_TEMP_SORT:-"/tmp/status_hostname_history_sort.txt"}
# Minimum downtime in seconds to display in past incidents
MY_MIN_DOWN_TIME=${MY_MIN_DOWN_TIME:-"60"}
# CSS Stylesheet for the status page
MY_STATUS_STYLESHEET=${MY_STATUS_STYLESHEET:-"https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap.min.css"}
# FontAwesome for the status page
MY_STATUS_FONTAWESOME=${MY_STATUS_FONTAWESOME:-"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css"}
# A footer
MY_STATUS_FOOTER=${MY_STATUS_FOOTER:-'Powered by static_status'}
# Lock file to prevent duplicate execution.
# If this file exists, status.sh script is terminated.
# If something has gone wrong and the file has not been deleted automatically, you can delete it.
MY_STATUS_LOCKFILE=${MY_STATUS_LOCKFILE:-"/tmp/STATUS_SH_IS_RUNNING.lock"}
# Date format for the web page.
# UTC (`-u`) is the default.
# Example: 2021-12-23 12:34:55 UTC
MY_DATE_TIME=${MY_DATE_TIME:-$(date -u "+%Y-%m-%d %H:%M:%S %Z")}
# Can be changed. Example with system time:
# MY_DATE_TIME=$(date "+%Y-%m-%d %H:%M:%S")
# Avoid semicolons.
# More details can be found in `man date`.
# Hook to call when a hostname status changes
# The hook script must be executable and receives three arguments:
# 1. New status, either up, down or degraded
# 2. Command used
# 3. Hostname
MY_HOOK_STATUS=${MY_HOOK_STATUS:-""}
################################################################################
#### END Configuration Section
################################################################################
################################################################################
# Helper
################################################################################
# debug_variables() print all script global variables to ease debugging
debug_variables() {
echo "USER: $USER"
echo "SHELL: $SHELL"
echo "BASH_VERSION: $BASH_VERSION"
echo
echo "MY_COMMANDS:"
for MY_COMMAND in "${MY_COMMANDS[@]}"; do
echo " $MY_COMMAND"
done
echo
echo "MY_TIMEOUT: $MY_TIMEOUT"
echo "MY_STATUS_CONFIG: $MY_STATUS_CONFIG"
echo "MY_STATUS_CONFIG_DIR: $MY_STATUS_CONFIG_DIR"
echo "MY_HOSTNAME_FILE: $MY_HOSTNAME_FILE"
echo "MY_HOSTNAME_STATUS_OK: $MY_HOSTNAME_STATUS_OK"
echo "MY_HOSTNAME_STATUS_DOWN: $MY_HOSTNAME_STATUS_DOWN"
echo "MY_HOSTNAME_STATUS_LASTRUN: $MY_HOSTNAME_STATUS_LASTRUN"
echo "MY_HOSTNAME_STATUS_DEGRADE: $MY_HOSTNAME_STATUS_DEGRADE"
echo "MY_HOSTNAME_STATUS_LASTRUN_DEGRADE: $MY_HOSTNAME_STATUS_LASTRUN_DEGRADE"
echo "MY_HOSTNAME_STATUS_HISTORY: $MY_HOSTNAME_STATUS_HISTORY"
echo
echo "MY_STATUS_HTML: $MY_STATUS_HTML"
echo "MY_STATUS_ICON: $MY_STATUS_ICON"
echo "MY_STATUS_JSON: $MY_STATUS_JSON"
echo "MY_MAINTENANCE_TEXT_FILE: $MY_MAINTENANCE_TEXT_FILE"
echo "MY_PAST_MAINTENANCE_TEXT_FILE: $MY_PAST_MAINTENANCE_TEXT_FILE"
echo "MY_HOMEPAGE_URL: $MY_HOMEPAGE_URL"
echo "MY_HOMEPAGE_TITLE: $MY_HOMEPAGE_TITLE"
echo "MY_STATUS_TITLE: $MY_STATUS_TITLE"
echo "MY_STATUS_STYLESHEET: $MY_STATUS_STYLESHEET"
echo "MY_STATUS_FOOTER: $MY_STATUS_FOOTER"
echo
echo "MY_STATUS_LOCKFILE: $MY_STATUS_LOCKFILE"
echo
echo "MY_TIMEOUT: $MY_TIMEOUT"
echo "MY_PING_TIMEOUT: $MY_PING_TIMEOUT"
echo "MY_PING_COUNT: $MY_PING_COUNT"
echo "MY_SCRIPT_TIMEOUT: $MY_SCRIPT_TIMEOUT"
echo "MY_TRACEROUTE_HOST: $MY_TRACEROUTE_HOST"
echo "MY_TRACEROUTE_NQUERIES: $MY_TRACEROUTE_NQUERIES"
echo
echo "MY_TIMESTAMP: $MY_TIMESTAMP"
echo "MY_DATE_TIME: $MY_DATE_TIME"
echo "MY_LASTRUN_TIME: $MY_LASTRUN_TIME"
echo
echo "MY_HOOK_STATUS: $MY_HOOK_STATUS"
}
# command_exists() tells if a given command exists.
function command_exists() {
command -v "$1" >/dev/null 2>&1
}
# check_bash() check if current shell is bash
function check_bash() {
if [[ "$0" == *"bash" ]]; then
exit_with_failure "Your current shell is $0"
fi
}
# check_command() check if command exists and exit if not exists
function check_command() {
if ! command_exists "$1"; then
exit_with_failure "Command '$1' not found"
fi
}
# check_config() check if the configuration file is readable
function check_config() {
if [ ! -r "$1" ]; then
exit_with_failure "Can not read required configuration file '$1'"
fi
}
# check_file() check if the file exists if not create the file
function check_file() {
if [ ! -f "$1" ]; then
if ! echo > "$1"; then
exit_with_failure "Can not create file '$1'"
fi
fi
if [ ! -w "$1" ]; then
exit_with_failure "Can not write file '$1'"
fi
}
# exit_with_failure() outputs a message before exiting the script.
function exit_with_failure() {
echo
echo "FAILURE: $1"
echo
debug_variables
echo
del_lock
exit 1
}
# echo_warning() outputs a warning message.
function echo_warning() {
echo
echo "WARNING: $1, will attempt to continue..."
echo
}
# echo_do_not_edit() outputs a "do not edit" message to write to a file
function echo_do_not_edit() {
echo "#"
echo "# !!! Do not edit this file !!!"
echo "#"
echo "# To reset everything, delete the files:"
echo "# $MY_HOSTNAME_STATUS_OK"
echo "# $MY_HOSTNAME_STATUS_DOWN"
echo "# $MY_HOSTNAME_STATUS_LASTRUN"
echo "# $MY_HOSTNAME_STATUS_DEGRADE"
echo "# $MY_HOSTNAME_STATUS_LASTRUN_DEGRADE"
echo "# $MY_HOSTNAME_STATUS_HISTORY"
echo "#"
}
# set_lock() sets lock file
function set_lock() {
if ! echo "$MY_DATE_TIME" > "$MY_STATUS_LOCKFILE"; then
exit_with_failure "Can not create lock file '$MY_STATUS_LOCKFILE'"
fi
}
# del_lock() deletes lock file
function del_lock() {
rm "$MY_STATUS_LOCKFILE" &> /dev/null
}
# check_lock() checks lock file and exit if the file exists
function check_lock() {
if [ -f "$MY_STATUS_LOCKFILE" ]; then
exit_with_failure "$ME is already running. Please wait... In case of problems simply delete the file: '$MY_STATUS_LOCKFILE'"
fi
}
# port_to_name() outputs name of well-known ports
# https://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers#Well-known_ports
function port_to_name() {
case "$1" in
32[0-9][0-9])
MY_PORT_NAME="SAP Dispatcher"
;;
33[0-9][0-9])
MY_PORT_NAME="SAP Gateway"
;;
80[0-9][0-9])
MY_PORT_NAME="SAP ICM HTTP"
;;
443[0-9][0-9])
MY_PORT_NAME="SAP ICM HTTPS"
;;
36[0-9][0-9])
MY_PORT_NAME="SAP Message Server"
;;
5[0-9][0-9]00)
MY_PORT_NAME="SAP J2EE HTTP"
;;
5[0-9][0-9]01)
MY_PORT_NAME="SAP J2EE HTTPS"
;;
5[0-9][0-9]04)
MY_PORT_NAME="SAP P4"
;;
5[0-9][0-9]08)
MY_PORT_NAME="SAP Telnet"
;;
*)
MY_SERVICE_NAME=$(awk '$2 ~ /^'"$1"'\// {print $1; exit}' "/etc/services" 2> /dev/null)
if [ -n "$MY_SERVICE_NAME" ]; then
MY_PORT_NAME=$(echo "$MY_SERVICE_NAME" | awk '{print toupper($0)}')
else
MY_PORT_NAME="Port $1"
fi
;;
esac
printf "%s" "$MY_PORT_NAME"
}
# get_lastrun_time()
function get_lastrun_time() {
while IFS=';' read -r MY_LASTRUN_COMMAND MY_LASTRUN_TIMESTAMP || [[ -n "$MY_LASTRUN_COMMAND" ]]; do
if [[ "$MY_LASTRUN_COMMAND" = "timestamp" ]]; then
if [ "$MY_LASTRUN_TIMESTAMP" -ge "0" ]; then
MY_LASTRUN_TIME="$((MY_TIMESTAMP-MY_LASTRUN_TIMESTAMP))"
else
MY_LASTRUN_TIME="0"
fi
fi
done <"$MY_HOSTNAME_STATUS_LASTRUN"
}
# check_downtime() check whether a failure has already been documented
# and determine the duration
function check_downtime() {
MY_COMMAND="$1"
MY_HOSTNAME="$2"
MY_PORT="$3"
MY_DOWN_TIME="0"
while IFS=';' read -r MY_DOWN_COMMAND MY_DOWN_HOSTNAME MY_DOWN_PORT MY_PREVIOUS_DOWN_TIME || [[ -n "$MY_DOWN_COMMAND" ]]; do
if [[ "$MY_DOWN_COMMAND" = "ping" ]] ||
[[ "$MY_DOWN_COMMAND" = "ping6" ]] ||
[[ "$MY_DOWN_COMMAND" = "nc" ]] ||
[[ "$MY_DOWN_COMMAND" = "grep" ]] ||
[[ "$MY_DOWN_COMMAND" = "traceroute" ]] ||
[[ "$MY_DOWN_COMMAND" = "curl" ]] ||
[[ "$MY_DOWN_COMMAND" = "http-status" ]] ||
[[ "$MY_DOWN_COMMAND" = "script" ]]; then
if [[ "$MY_DOWN_HOSTNAME" = "$MY_HOSTNAME" ]]; then
if [[ "$MY_DOWN_PORT" = "$MY_PORT" ]]; then
MY_DOWN_TIME="$((MY_PREVIOUS_DOWN_TIME+MY_LASTRUN_TIME))"
break # Skip entire rest of loop.
fi
fi
fi
done <"$MY_HOSTNAME_STATUS_LASTRUN" # MY_HOSTNAME_STATUS_DOWN is copied to MY_HOSTNAME_STATUS_LASTRUN
}
# check_degradetime() check whether a degradation has already been documented
# and determine the duration
function check_degradetime() {
MY_COMMAND="$1"
MY_HOSTNAME="$2"
MY_DEGRADE_TIME="0"
while IFS=';' read -r MY_DEGRADE_COMMAND MY_DEGRADE_HOSTNAME MY_PREVIOUS_DEGRADE_TIME || [[ -n "$MY_DEGRADE_COMMAND" ]]; do
if [[ "$MY_DEGRADE_COMMAND" = "script" ]]; then
if [[ "$MY_DEGRADE_HOSTNAME" = "$MY_HOSTNAME" ]]; then
MY_DEGRADE_TIME="$((MY_PREVIOUS_DEGRADE_TIME+MY_LASTRUN_TIME))"
break # Skip entire rest of loop.
fi
fi
done <"$MY_HOSTNAME_STATUS_LASTRUN_DEGRADE" # MY_HOSTNAME_STATUS_DEGRADE is copied to MY_HOSTNAME_STATUS_LASTRUN_DEGRADE
}
# save_downtime()
function save_downtime() {
MY_COMMAND="$1"
MY_HOSTNAME="$2"
MY_PORT="$3"
MY_DOWN_TIME="$4"
printf "\\n%s;%s;%s;%s" "$MY_COMMAND" "$MY_HOSTNAME" "$MY_PORT" "$MY_DOWN_TIME" >> "$MY_HOSTNAME_STATUS_DOWN"
if [[ "$BE_LOUD" = "yes" ]] || [[ "$BE_QUIET" = "no" ]]; then
printf "\\n%-5s %-4s %s" "DOWN:" "$MY_COMMAND" "$MY_HOSTNAME"
if [[ $MY_COMMAND == "nc" ]]; then
printf " %s" "$(port_to_name "$MY_PORT")"
fi
if [[ $MY_COMMAND == "grep" ]]; then
printf " %s" "$MY_PORT"
fi
if [[ $MY_COMMAND == "http-status" ]]; then
printf " %s" "$MY_PORT"
fi
fi
if [[ -x "${MY_HOOK_STATUS}" ]] && ! grep -E "^${MY_COMMAND};${MY_HOSTNAME}" "${MY_HOSTNAME_STATUS_LASTRUN}" &> /dev/null; then
${MY_HOOK_STATUS} "down" "${MY_COMMAND}" "${MY_HOSTNAME}" &> /dev/null
fi
}
# save_degradetime()
function save_degradetime() {
MY_COMMAND="$1"
MY_HOSTNAME="$2"
MY_DEGRADE_TIME="$3"
printf "\\n%s;%s;%s" "$MY_COMMAND" "$MY_HOSTNAME" "$MY_DEGRADE_TIME" >> "$MY_HOSTNAME_STATUS_DEGRADE"
if [[ "$BE_LOUD" = "yes" ]] || [[ "$BE_QUIET" = "no" ]]; then
printf "\\n%-5s %-4s %s" "DEGRADED:" "$MY_COMMAND" "$MY_HOSTNAME"
fi
if [[ -x "${MY_HOOK_STATUS}" ]] && ! grep -E "^${MY_COMMAND};${MY_HOSTNAME}" "${MY_HOSTNAME_STATUS_LASTRUN_DEGRADE}" &>/dev/null; then
${MY_HOOK_STATUS} "degraded" "${MY_COMMAND}" "${MY_HOSTNAME}" &> /dev/null
fi
}
# save_availability()
function save_availability() {
MY_COMMAND="$1"
MY_HOSTNAME="$2"
MY_PORT="$3"
printf "\\n%s;%s;%s" "$MY_COMMAND" "$MY_HOSTNAME" "$MY_PORT" >> "$MY_HOSTNAME_STATUS_OK"
if [[ "$BE_LOUD" = "yes" ]]; then
printf "\\n%-5s %-4s %s" "UP:" "$MY_COMMAND" "$MY_HOSTNAME"
if [[ $MY_COMMAND == "nc" ]]; then
printf " %s" "$(port_to_name "$MY_PORT")"
fi
if [[ $MY_COMMAND == "grep" ]]; then
printf " %s" "$MY_PORT"
fi
if [[ $MY_COMMAND == "http-status" ]]; then
printf " %s" "$MY_PORT"
fi
fi
if [[ -x "${MY_HOOK_STATUS}" ]]; then
if grep -E "^${MY_COMMAND};${MY_HOSTNAME}" "${MY_HOSTNAME_STATUS_LASTRUN}" &>/dev/null || grep -E "^${MY_COMMAND};${MY_HOSTNAME}" "${MY_HOSTNAME_STATUS_LASTRUN_DEGRADE}" &>/dev/null; then
${MY_HOOK_STATUS} "up" "${MY_COMMAND}" "${MY_HOSTNAME}" &> /dev/null
fi
fi
}
# save_history()
function save_history() {
MY_COMMAND="$1"
MY_HOSTNAME="$2"
MY_PORT="$3"
MY_DOWN_TIME="$4"
MY_DATE_TIME="$5"
if cp "$MY_HOSTNAME_STATUS_HISTORY" "$MY_HOSTNAME_STATUS_HISTORY_TEMP_SORT" &> /dev/null; then
printf "\\n%s;%s;%s;%s;%s" "$MY_COMMAND" "$MY_HOSTNAME" "$MY_PORT" "$MY_DOWN_TIME" "$MY_DATE_TIME" > "$MY_HOSTNAME_STATUS_HISTORY"
cat "$MY_HOSTNAME_STATUS_HISTORY_TEMP_SORT" >> "$MY_HOSTNAME_STATUS_HISTORY"
rm "$MY_HOSTNAME_STATUS_HISTORY_TEMP_SORT" &> /dev/null
else
exit_with_failure "Can not copy file '$MY_HOSTNAME_STATUS_HISTORY' to '$MY_HOSTNAME_STATUS_HISTORY_TEMP_SORT'"
fi
if [[ "$BE_LOUD" = "yes" ]]; then
printf "\\n%-5s %-4s %s %s sec" "HIST:" "$MY_COMMAND" "$MY_HOSTNAME" "$MY_DOWN_TIME"
if [[ $MY_COMMAND == "nc" ]]; then
printf " %s" "$(port_to_name "$MY_PORT")"
fi
if [[ $MY_COMMAND == "grep" ]]; then
printf " %s" "$MY_PORT"
fi
if [[ $MY_COMMAND == "http-status" ]]; then
printf " %s" "$MY_PORT"
fi
fi
}
################################################################################
# HTML
################################################################################
function page_header() {
# check for autorefresh
if [ "$MY_AUTOREFRESH" -gt 0 ]
then
MY_AUTOREFRESH_TEXT=""
else
MY_AUTOREFRESH_TEXT=""
fi
cat > "$MY_STATUS_HTML" << EOF