#!/bin/bash # /usr/lib/check_mk_agent/local/300/check_portainer set -u CONFIG="/etc/check_mk/portainer.conf" OK=0 WARN=1 CRIT=2 UNKNOWN=3 # # --------------------------------------------------------------------------- # Configuration # --------------------------------------------------------------------------- # if [[ ! -r "$CONFIG" ]]; then printf '%s\n' "${UNKNOWN} \"Portainer\" - Configuration file ${CONFIG} missing or unreadable" exit 0 fi # shellcheck disable=SC1090 source "$CONFIG" PORTAINER_URL="${PORTAINER_URL:-https://127.0.0.1:9443}" PORTAINER_TOKEN="${PORTAINER_TOKEN:-}" PORTAINER_INSECURE="${PORTAINER_INSECURE:-yes}" ALERT_STOPPED_CONTAINERS="${ALERT_STOPPED_CONTAINERS:-no}" CHECK_UPDATES="${CHECK_UPDATES:-yes}" PORTAINER_RELEASE_API="${PORTAINER_RELEASE_API:-https://api.github.com/repos/portainer/portainer/releases/latest}" # # --------------------------------------------------------------------------- # Requirements # --------------------------------------------------------------------------- # if ! command -v curl >/dev/null 2>&1; then printf '%s\n' "${UNKNOWN} \"Portainer\" - curl is not installed" exit 0 fi if ! command -v jq >/dev/null 2>&1; then printf '%s\n' "${UNKNOWN} \"Portainer\" - jq is not installed" exit 0 fi if ! command -v sort >/dev/null 2>&1; then printf '%s\n' "${UNKNOWN} \"Portainer\" - sort is not installed" exit 0 fi if [[ -z "$PORTAINER_TOKEN" ]]; then printf '%s\n' "${UNKNOWN} \"Portainer\" - PORTAINER_TOKEN is not configured" exit 0 fi # # --------------------------------------------------------------------------- # curl configuration # --------------------------------------------------------------------------- # CURL_OPTS=( --silent --show-error --connect-timeout 3 --max-time 10 ) if [[ "$PORTAINER_INSECURE" == "yes" ]]; then CURL_OPTS+=(--insecure) fi AUTH_HEADER="X-API-Key: ${PORTAINER_TOKEN}" # # --------------------------------------------------------------------------- # Portainer health / version # --------------------------------------------------------------------------- # STATUS_RESPONSE="$( curl \ "${CURL_OPTS[@]}" \ "${PORTAINER_URL%/}/api/status" \ 2>/dev/null )" CURL_RC=$? if [[ $CURL_RC -ne 0 || -z "$STATUS_RESPONSE" ]]; then printf '%s\n' "${CRIT} \"Portainer\" - API unavailable at ${PORTAINER_URL}" exit 0 fi if ! printf '%s' "$STATUS_RESPONSE" | jq -e . >/dev/null 2>&1; then printf '%s\n' "${UNKNOWN} \"Portainer\" - Status API returned invalid JSON" exit 0 fi VERSION="$( printf '%s' "$STATUS_RESPONSE" | jq -r '.Version // .version // empty' )" if [[ -n "$VERSION" ]]; then printf '%s\n' "${OK} \"Portainer\" - API reachable, version ${VERSION}" else printf '%s\n' "${OK} \"Portainer\" - API reachable" fi # # --------------------------------------------------------------------------- # Portainer update check # --------------------------------------------------------------------------- # if [[ "$CHECK_UPDATES" == "yes" ]]; then if [[ -z "$VERSION" ]]; then printf '%s\n' \ "${UNKNOWN} \"Portainer Update\" - Installed Portainer version could not be determined" else RELEASE_RESPONSE="$( curl \ "${CURL_OPTS[@]}" \ -H "Accept: application/vnd.github+json" \ -H "User-Agent: checkmk-portainer-local-check" \ "$PORTAINER_RELEASE_API" \ 2>/dev/null )" CURL_RC=$? if [[ $CURL_RC -ne 0 || -z "$RELEASE_RESPONSE" ]]; then printf '%s\n' \ "${UNKNOWN} \"Portainer Update\" - Unable to retrieve latest release information" elif ! printf '%s' "$RELEASE_RESPONSE" | jq -e . >/dev/null 2>&1; then printf '%s\n' \ "${UNKNOWN} \"Portainer Update\" - Release API returned invalid JSON" else LATEST_VERSION="$( printf '%s' "$RELEASE_RESPONSE" | jq -r '.tag_name // empty' )" INSTALLED_VERSION="${VERSION#v}" LATEST_VERSION="${LATEST_VERSION#v}" if [[ -z "$LATEST_VERSION" ]]; then printf '%s\n' \ "${UNKNOWN} \"Portainer Update\" - Latest release version could not be determined" else # # Strip known suffixes before numeric comparison. # # Examples: # 2.36.0 # 2.36.0-sts # 2.36.0-lts # INSTALLED_COMPARE="${INSTALLED_VERSION%%-*}" LATEST_COMPARE="${LATEST_VERSION%%-*}" if [[ ! "$INSTALLED_COMPARE" =~ ^[0-9]+([.][0-9]+)*$ ]]; then printf '%s\n' \ "${UNKNOWN} \"Portainer Update\" - Unable to parse installed version ${INSTALLED_VERSION}" elif [[ ! "$LATEST_COMPARE" =~ ^[0-9]+([.][0-9]+)*$ ]]; then printf '%s\n' \ "${UNKNOWN} \"Portainer Update\" - Unable to parse latest version ${LATEST_VERSION}" else NEWEST="$( printf '%s\n%s\n' \ "$INSTALLED_COMPARE" \ "$LATEST_COMPARE" | sort -V | tail -n 1 )" if [[ "$INSTALLED_COMPARE" == "$LATEST_COMPARE" ]]; then printf '%s\n' \ "${OK} \"Portainer Update\" - Current: installed ${INSTALLED_VERSION}, latest ${LATEST_VERSION}" elif [[ "$NEWEST" == "$LATEST_COMPARE" ]]; then printf '%s\n' \ "${WARN} \"Portainer Update\" - Update available: installed ${INSTALLED_VERSION}, latest ${LATEST_VERSION}" else printf '%s\n' \ "${OK} \"Portainer Update\" - Installed ${INSTALLED_VERSION} is newer than latest stable ${LATEST_VERSION}" fi fi fi fi fi fi # # --------------------------------------------------------------------------- # Endpoints # --------------------------------------------------------------------------- # ENDPOINTS_RESPONSE="$( curl \ "${CURL_OPTS[@]}" \ -H "$AUTH_HEADER" \ "${PORTAINER_URL%/}/api/endpoints" \ 2>/dev/null )" CURL_RC=$? if [[ $CURL_RC -ne 0 || -z "$ENDPOINTS_RESPONSE" ]]; then printf '%s\n' "${CRIT} \"Portainer Endpoints\" - Unable to retrieve endpoints" exit 0 fi if ! printf '%s' "$ENDPOINTS_RESPONSE" | jq -e 'type == "array"' >/dev/null 2>&1; then printf '%s\n' "${UNKNOWN} \"Portainer Endpoints\" - Endpoint API returned unexpected data" exit 0 fi ENDPOINT_COUNT="$( printf '%s' "$ENDPOINTS_RESPONSE" | jq 'length' )" UP_ENDPOINTS=0 DOWN_ENDPOINTS=0 while IFS= read -r ENDPOINT; do ENDPOINT_ID="$( printf '%s' "$ENDPOINT" | jq -r '.Id // .ID // empty' )" ENDPOINT_NAME="$( printf '%s' "$ENDPOINT" | jq -r '.Name // "Unknown"' )" ENDPOINT_STATUS="$( printf '%s' "$ENDPOINT" | jq -r '.Status // 0' )" ENDPOINT_NAME="${ENDPOINT_NAME//\"/\'}" # # Portainer endpoint states: # 1 = UP # 2 = DOWN # if [[ "$ENDPOINT_STATUS" == "1" ]]; then UP_ENDPOINTS=$((UP_ENDPOINTS + 1)) printf '%s\n' \ "${OK} \"Portainer Endpoint ${ENDPOINT_NAME}\" - Endpoint is UP" else DOWN_ENDPOINTS=$((DOWN_ENDPOINTS + 1)) printf '%s\n' \ "${CRIT} \"Portainer Endpoint ${ENDPOINT_NAME}\" - Endpoint is DOWN, status ${ENDPOINT_STATUS}" fi # # ----------------------------------------------------------------------- # Containers for this endpoint # ----------------------------------------------------------------------- # if [[ -z "$ENDPOINT_ID" ]]; then printf '%s\n' \ "${UNKNOWN} \"Docker Containers ${ENDPOINT_NAME}\" - Endpoint ID missing" continue fi CONTAINERS_RESPONSE="$( curl \ "${CURL_OPTS[@]}" \ -H "$AUTH_HEADER" \ "${PORTAINER_URL%/}/api/endpoints/${ENDPOINT_ID}/docker/containers/json?all=1" \ 2>/dev/null )" CURL_RC=$? if [[ $CURL_RC -ne 0 || -z "$CONTAINERS_RESPONSE" ]]; then printf '%s\n' \ "${UNKNOWN} \"Docker Containers ${ENDPOINT_NAME}\" - Unable to retrieve containers" continue fi if ! printf '%s' "$CONTAINERS_RESPONSE" | jq -e 'type == "array"' >/dev/null 2>&1; then printf '%s\n' \ "${UNKNOWN} \"Docker Containers ${ENDPOINT_NAME}\" - Container API returned unexpected data" continue fi TOTAL_CONTAINERS="$( printf '%s' "$CONTAINERS_RESPONSE" | jq 'length' )" RUNNING_CONTAINERS="$( printf '%s' "$CONTAINERS_RESPONSE" | jq '[.[] | select(.State == "running")] | length' )" STOPPED_CONTAINERS="$( printf '%s' "$CONTAINERS_RESPONSE" | jq '[.[] | select(.State != "running")] | length' )" UNHEALTHY_CONTAINERS="$( printf '%s' "$CONTAINERS_RESPONSE" | jq ' [ .[] | select( ((.Status // "") | test("\\(unhealthy\\)")) ) ] | length ' )" PROBLEM_LIST="$( printf '%s' "$CONTAINERS_RESPONSE" | jq -r ' .[] | select( (.State != "running") or ((.Status // "") | test("\\(unhealthy\\)")) ) | ( ( (.Names[0] // .Id // "unknown") | sub("^/"; "") ) + "=" + (.Status // .State // "unknown") ) ' | paste -sd ';' - )" STATE=$OK if [[ "$UNHEALTHY_CONTAINERS" -gt 0 ]]; then STATE=$CRIT elif [[ "$ALERT_STOPPED_CONTAINERS" == "yes" && "$STOPPED_CONTAINERS" -gt 0 ]]; then STATE=$WARN fi PERFDATA="total=${TOTAL_CONTAINERS};;;0|running=${RUNNING_CONTAINERS};;;0|stopped=${STOPPED_CONTAINERS};;;0|unhealthy=${UNHEALTHY_CONTAINERS};;;0" DETAILS="${RUNNING_CONTAINERS}/${TOTAL_CONTAINERS} running" if [[ "$UNHEALTHY_CONTAINERS" -gt 0 ]]; then DETAILS="${DETAILS}, ${UNHEALTHY_CONTAINERS} unhealthy" fi if [[ "$STOPPED_CONTAINERS" -gt 0 ]]; then DETAILS="${DETAILS}, ${STOPPED_CONTAINERS} stopped" fi if [[ -n "$PROBLEM_LIST" ]]; then DETAILS="${DETAILS}: ${PROBLEM_LIST}" fi printf '%s\n' \ "${STATE} \"Docker Containers ${ENDPOINT_NAME}\" ${PERFDATA} ${DETAILS}" done < <( printf '%s' "$ENDPOINTS_RESPONSE" | jq -c '.[]' ) # # --------------------------------------------------------------------------- # Endpoint summary # --------------------------------------------------------------------------- # ENDPOINT_STATE=$OK if [[ "$DOWN_ENDPOINTS" -gt 0 ]]; then ENDPOINT_STATE=$CRIT fi printf '%s\n' \ "${ENDPOINT_STATE} \"Portainer Endpoints\" total=${ENDPOINT_COUNT};;;0|up=${UP_ENDPOINTS};;;0|down=${DOWN_ENDPOINTS};;;0 ${UP_ENDPOINTS}/${ENDPOINT_COUNT} endpoints UP" exit 0