Dotfiles/setup/modules/FreeipaAnsible/image/ansipa-opnsense-monitor.sh

125 lines
6.9 KiB
Bash
Executable File

#!/bin/sh
# ansipa_opnsense — CheckMK local check for an OPNsense firewall.
#
# Install: copy to /usr/local/lib/check_mk_agent/local/ansipa_opnsense, chmod 755
# (ansipa-opnsense-checkmk-install.sh does this for you).
#
# ── Why this is /bin/sh and not bash ────────────────────────────────────────
# OPNsense is FreeBSD. There is no bash in the base system, no GNU grep (so no
# `grep -P`), and no `mapfile`. Everything here is POSIX sh + awk/sed, unlike
# the sibling ansipa-pve/pbs-monitor.sh scripts which target Debian and may use
# bashisms. Do not "modernise" this file with [[ ]] or grep -o -P; it will
# silently stop producing output on the firewall.
#
# Emits (CheckMK local-check format: "<state> <service> <perfdata> <text>"):
# Ansipa_OPN_Gateways per-gateway up/down from the gateway status API
# Ansipa_OPN_States pf state-table usage against the configured hard limit
# Ansipa_OPN_Firmware pending firmware/package updates
# Ansipa_OPN_CARP CARP interface health (skipped cleanly if unused)
#
# CPU / RAM / disk / interface counters are already covered by the stock
# FreeBSD agent checks — deliberately not duplicated here.
PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin
export PATH
# ── Gateways ────────────────────────────────────────────────────────────────
# `configctl interface gateways status` returns a JSON blob. There is no jq in
# the OPNsense base system, so the objects are split onto separate lines and
# read with sed. We count anything not explicitly Online/none as degraded.
if command -v configctl >/dev/null 2>&1; then
_gw=$(configctl interface gateways status 2>/dev/null)
if [ -z "$_gw" ]; then
echo "3 Ansipa_OPN_Gateways - could not query gateway status"
else
# One JSON object per line, then pull name + status_translated per line.
_parsed=$(printf '%s' "$_gw" | sed 's/},[[:space:]]*{/}\
{/g')
_total=$(echo "$_parsed" | grep -c '"name"' 2>/dev/null || echo 0)
_downlist=$(echo "$_parsed" \
| sed -n 's/.*"name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*"status_translated"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1=\2/p' \
| grep -v '=Online$' | tr '\n' ' ')
_down=$(printf '%s' "$_downlist" | wc -w | tr -d ' ')
if [ "${_total:-0}" -eq 0 ]; then
echo "3 Ansipa_OPN_Gateways - no gateways found in status output"
elif [ "${_down:-0}" -gt 0 ]; then
echo "2 Ansipa_OPN_Gateways gateways_total=${_total};; gateways_down=${_down};1;1 ${_down}/${_total} gateway(s) not online: ${_downlist}"
else
echo "0 Ansipa_OPN_Gateways gateways_total=${_total};; gateways_down=0;1;1 all ${_total} gateway(s) online"
fi
fi
else
echo "3 Ansipa_OPN_Gateways - configctl not found (not an OPNsense host?)"
fi
# ── pf state table ──────────────────────────────────────────────────────────
# A firewall that hits its state limit drops new connections while looking
# perfectly healthy on CPU/RAM, so this is the check that actually catches it.
if command -v pfctl >/dev/null 2>&1; then
_cur=$(pfctl -si 2>/dev/null | awk '/current entries/ {print $3; exit}')
_lim=$(pfctl -sm 2>/dev/null | awk '/^states/ {print $4; exit}')
if [ -z "$_cur" ] || [ -z "$_lim" ] || [ "${_lim:-0}" -eq 0 ] 2>/dev/null; then
echo "3 Ansipa_OPN_States - could not read pf state table counters"
else
_pct=$(( _cur * 100 / _lim ))
_perf="states=${_cur};$(( _lim * 80 / 100 ));$(( _lim * 90 / 100 ));0;${_lim} states_pct=${_pct}%;80;90"
if [ "$_pct" -ge 90 ]; then
echo "2 Ansipa_OPN_States ${_perf} pf states ${_cur}/${_lim} (${_pct}%) — near the hard limit"
elif [ "$_pct" -ge 80 ]; then
echo "1 Ansipa_OPN_States ${_perf} pf states ${_cur}/${_lim} (${_pct}%)"
else
echo "0 Ansipa_OPN_States ${_perf} pf states ${_cur}/${_lim} (${_pct}%)"
fi
fi
else
echo "3 Ansipa_OPN_States - pfctl not found"
fi
# ── Firmware / package updates ──────────────────────────────────────────────
# Read the cached firmware status rather than triggering a check: this runs
# every agent poll, and hitting the mirrors that often is antisocial and slow.
# `configctl firmware check` refreshes the cache; a cron job on the firewall
# should do that once a day (the installer sets one up).
_fw_status=/tmp/pkg_upgrade.progress
if command -v opnsense-version >/dev/null 2>&1; then
_pending=""
if [ -r /var/cache/opnsense-update/changelog/firmware.json ]; then
_pending=$(sed -n 's/.*"new_packages_count"[[:space:]]*:[[:space:]]*"\{0,1\}\([0-9]*\).*/\1/p' \
/var/cache/opnsense-update/changelog/firmware.json 2>/dev/null | head -1)
fi
if [ -z "$_pending" ] && command -v pkg >/dev/null 2>&1; then
# Offline count from the local package DB — no network access.
_pending=$(pkg version -vIL= 2>/dev/null | wc -l | tr -d ' ')
fi
_ver=$(opnsense-version 2>/dev/null | head -1)
if [ -z "$_pending" ]; then
echo "3 Ansipa_OPN_Firmware - could not determine pending update count (${_ver})"
elif [ "$_pending" -gt 0 ] 2>/dev/null; then
echo "1 Ansipa_OPN_Firmware pending_updates=${_pending};1;;0; ${_pending} update(s) pending — ${_ver}"
else
echo "0 Ansipa_OPN_Firmware pending_updates=0;1;;0; up to date — ${_ver}"
fi
else
echo "3 Ansipa_OPN_Firmware - opnsense-version not found"
fi
unset _fw_status
# ── CARP (high availability) ────────────────────────────────────────────────
# Only meaningful on an HA pair. On a standalone firewall there are no carp
# interfaces and we stay silent rather than emitting a permanent UNKNOWN.
if command -v ifconfig >/dev/null 2>&1; then
_carp=$(ifconfig 2>/dev/null | grep -c ' carp:' 2>/dev/null || echo 0)
if [ "${_carp:-0}" -gt 0 ]; then
_demote=$(sysctl -n net.inet.carp.demotion 2>/dev/null || echo 0)
_init=$(ifconfig 2>/dev/null | grep ' carp:' | grep -c 'INIT' || echo 0)
if [ "${_init:-0}" -gt 0 ]; then
echo "2 Ansipa_OPN_CARP carp_ifaces=${_carp};; carp_demotion=${_demote};; ${_init} CARP interface(s) in INIT state"
elif [ "${_demote:-0}" -ne 0 ]; then
echo "1 Ansipa_OPN_CARP carp_ifaces=${_carp};; carp_demotion=${_demote};; CARP demotion factor is ${_demote} (not 0)"
else
echo "0 Ansipa_OPN_CARP carp_ifaces=${_carp};; carp_demotion=0;; ${_carp} CARP interface(s) healthy"
fi
fi
fi