Files
vps/ansible/roles/healthcheck/templates/health-common.sh.j2
T

96 lines
3.4 KiB
Django/Jinja

#!/usr/bin/env bash
# Shared contract: one JSON object per run in /var/lib/vps-health/latest.json.
# Status values: ok, warning, critical, unknown. Exit codes: 0 healthy/warning,
# 2 critical, 3 unknown. Check detail must never include secret values.
set -uo pipefail
RESULT_DIR='{{ healthcheck_state_dir }}'
LOG_DIR='{{ healthcheck_log_dir }}'
HOST_NAME="$(hostname -f 2>/dev/null || hostname)"
CHECK_NAME='{{ healthcheck_profile }}'
STATUS=ok
EXIT_CODE=0
DETAILS=()
record() {
local severity="$1" message="$2"
DETAILS+=("${severity}:${message}")
case "$severity" in
critical) STATUS=critical; EXIT_CODE=2 ;;
unknown) [[ "$STATUS" != critical ]] && STATUS=unknown; [[ "$EXIT_CODE" -eq 0 ]] && EXIT_CODE=3 ;;
warning) [[ "$STATUS" == ok ]] && STATUS=warning ;;
esac
}
require_command() {
command -v "$1" >/dev/null 2>&1 || record unknown "missing_command:$1"
}
compose_ps() {
docker compose --project-directory '{{ compose_project_dir }}' ps --all 2>&1
}
check_compose() {
local output
output="$(compose_ps)" || { record critical 'compose_ps_failed'; return; }
if grep -qiE 'Exited|Restarting|[[:space:]]Dead[[:space:]]' <<<"$output"; then
record critical 'compose_unhealthy_container'
else
record ok 'compose_ok'
fi
}
check_backup_freshness() {
local pattern="$1" newest now age
newest="$(find {{ compose_project_dir | quote }} -type f -path "$pattern" -printf '%T@\n' 2>/dev/null | sort -nr | head -n1)"
if [[ -z "$newest" ]]; then
record warning 'backup_not_found'
return
fi
now="$(date +%s)"
age="$(( now - ${newest%.*} ))"
if (( age > {{ healthcheck_backup_max_age_hours }} * 3600 )); then
record critical 'backup_stale'
else
record ok 'backup_fresh'
fi
}
check_https() {
local url="$1" expected="$2" code
code="$(curl --silent --show-error --output /dev/null --write-out '%{http_code}' --max-time 20 "$url" 2>/dev/null)" || {
record critical 'https_request_failed'; return;
}
[[ "$code" =~ $expected ]] && record ok "https_${code}" || record critical "https_${code}"
}
check_tls_days() {
local host="$1" port="$2" expires epoch remaining
expires="$(timeout 20 openssl s_client -connect "${host}:${port}" -servername "$host" </dev/null 2>/dev/null | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2-)" || {
record unknown 'tls_read_failed'; return;
}
epoch="$(date -d "$expires" +%s 2>/dev/null)" || { record unknown 'tls_date_parse_failed'; return; }
remaining="$(( (epoch - $(date +%s)) / 86400 ))"
if (( remaining < 0 )); then record critical 'tls_expired'
elif (( remaining < {{ healthcheck_tls_warn_days }} )); then record warning 'tls_near_expiry'
else record ok 'tls_valid'; fi
}
emit_result() {
local tmp detail_json
tmp="$(mktemp "${RESULT_DIR}/latest.json.XXXXXX")"
detail_json="$(printf '%s\n' "${DETAILS[@]:-unknown:no_details}" | python3 -c 'import json,sys; print(json.dumps([line.rstrip() for line in sys.stdin if line.strip()]))')"
python3 - "$tmp" "$HOST_NAME" "$CHECK_NAME" "$STATUS" "$EXIT_CODE" "$detail_json" <<'PY'
import json, sys
path, host, check, status, code, details = sys.argv[1:]
with open(path, 'w', encoding='utf-8') as f:
json.dump({'schema': 1, 'host': host, 'check': check, 'status': status,
'exit_code': int(code), 'details': json.loads(details)}, f,
sort_keys=True, separators=(',', ':'))
f.write('\n')
PY
chmod 0640 "$tmp"
mv "$tmp" "${RESULT_DIR}/latest.json"
cat "${RESULT_DIR}/latest.json"
}