[SOLVED] PBS-Datastore Monitor

plurgi95

Member
Aug 22, 2023
16
3
8
Hallo,

Ich hab mir mal gedacht ich teil mein Script mit euch.

Ich übe zzt. Die Shell Script entwicklung in meiner ausbildung und dachte mir ich baue für meine Firma ein Monitoring Script welches den Aktuellen Speicherplatz des Backup Datastores Täglich via Cronjob an die Ticket E-Mail schickt.

Aktuell versuche ich in die Status mail die Prognose von Proxmox mit einzubringen
Sprich eine Trend-Analyse für "Geschätzt Voll"

Leider scheitere ich daran noch aktuell ein wenig, Aktuell bekomme ich ständig die info


Code:
Trend & Prognose:
-----------------
Wachstum:     Speicher wächst aktuell nicht.
Voraussichtlich voll in: Unbekannt (zu wenig Daten)

Dass Script hänge ich in nem Code-Block an, Vielleicht hat ja jemand ne idee wie ich die Prognose dierekt mit einem der von der Proxmox API Bereitgestellten GET Parameter,
Die Aktuelle lösung bringt leider kein Erfolg.

Bash:
#!/usr/bin/env bash
# pbs-datastore-monitor.sh
# Überwacht den Speicherplatz eines PBS Datastores über die lokale API.
# Beinhaltet automatische 14-Tage Log-Rotation und Fehlerberichterstattung.
#
# Usage:
#   ./pbs-datastore-monitor.sh -d <Datastore> -t <To-Mail> [-f <From-Mail>] [-w <Threshold>] [-r]

set -euo pipefail
export LC_ALL=C
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"

# Standardwerte
DATASTORE="local"
NOTIFY_MAIL="ticket@"
MAIL_FROM="pve-cluster@"
WARN_THRESHOLD_PERCENT=20
MODE="check"
UNIT="pbs" # Basis 1000 wie PBS GUI

# Argumente parsen
while [[ "$#" -gt 0 ]]; do
    case $1 in
        -d|--datastore) DATASTORE="$2"; shift ;;
        -t|--to)        NOTIFY_MAIL="$2"; shift ;;
        -f|--from)      MAIL_FROM="$2"; shift ;;
        -w|--warn)      WARN_THRESHOLD_PERCENT="$2"; shift ;;
        -r|--report)    MODE="report" ;;
        -h|--help)
            echo "Usage: $0 -d <Datastore> -t <To-Mail> [-f <From-Mail>] [-w <Warn-Threshold-%>] [-r|--report]"
            exit 0
            ;;
        *) echo "Unbekannter Parameter: $1" >&2; exit 1 ;;
    esac
    shift
done

# Frühe Validierung (bevor Dateien angelegt werden)
if [ -z "${DATASTORE}" ] || [ -z "${NOTIFY_MAIL}" ]; then
  echo "Fehler: -d <Datastore> und -t <To-Mail> sind zwingend erforderlich." >&2
  exit 1
fi

STATE_DIR="/var/lib/pbs-space-monitor"
STATE_FILE="${STATE_DIR}/state_${DATASTORE}.txt"
LOG_FILE="${STATE_DIR}/monitor_${DATASTORE}.log"
LOG_CYCLE_FILE="${STATE_DIR}/cycle_${DATASTORE}.txt"
PIVOT_HOST="$(hostname -s)"

mkdir -p "${STATE_DIR}"

# --- LOGGING FUNKTION ---
log() {
  local level="$1"
  local msg="$2"
  local ts="$(date '+%Y-%m-%d %H:%M:%S')"
  local log_line="${ts} [${level}] ${msg}"
 
  # Ins Logfile schreiben
  echo "${log_line}" >> "${LOG_FILE}"
 
  # Auf die Konsole ausgeben
  if [ "${level}" = "ERROR" ]; then
    echo "${log_line}" >&2
  else
    echo "${log_line}"
  fi
}

# --- MAIL FUNKTION ---
send_mail() {
  local subject="$1"
  local body="$2"
  if command -v sendmail >/dev/null 2>&1; then
    local sendmail_cmd=(sendmail)
    [ -n "${MAIL_FROM}" ] && sendmail_cmd+=("-f" "${MAIL_FROM}")
    sendmail_cmd+=("${NOTIFY_MAIL}")
    if ! {
      [ -n "${MAIL_FROM}" ] && echo "From: ${MAIL_FROM}"
      echo "To: ${NOTIFY_MAIL}"
      echo "Subject: ${subject}"
      echo ""
      echo -e "${body}"
    } | "${sendmail_cmd[@]}"; then
       log "ERROR" "E-Mail Versand via sendmail fehlgeschlagen."
       exit 1
    fi
  elif command -v mail >/dev/null 2>&1; then
    if [ -n "${MAIL_FROM}" ]; then
      if ! echo -e "${body}" | mail -a "From: ${MAIL_FROM}" -s "${subject}" "${NOTIFY_MAIL}"; then
         log "ERROR" "E-Mail Versand via mail fehlgeschlagen."
         exit 1
      fi
    else
      if ! echo -e "${body}" | mail -s "${subject}" "${NOTIFY_MAIL}"; then
         log "ERROR" "E-Mail Versand via mail fehlgeschlagen."
         exit 1
      fi
    fi
  else
    log "ERROR" "Weder sendmail noch mail verfügbar."
    exit 1
  fi
}

# --- 14 TAGE LOG ROTATION ---
current_time=$(date +%s)
if [ -f "${LOG_CYCLE_FILE}" ]; then
  cycle_start=$(cat "${LOG_CYCLE_FILE}")
else
  cycle_start=${current_time}
  echo "${cycle_start}" > "${LOG_CYCLE_FILE}"
fi

# 14 Tage in Sekunden = 1209600
if [ $(( current_time - cycle_start )) -ge 1209600 ]; then
  log "INFO" "14-Tage Log-Zyklus erreicht. Werte Log aus..."
 
  if [ -f "${LOG_FILE}" ] && grep -q "\[ERROR\]" "${LOG_FILE}"; then
    log "INFO" "Es wurden Fehler im Log gefunden. Sende Logdatei per E-Mail an ${NOTIFY_MAIL}."
    log_content=$(cat "${LOG_FILE}")
    subj="PBS Datastore '${DATASTORE}' - 14-Tage Fehler-Log [${PIVOT_HOST}]"
    mail_body="Im Überwachungszeitraum der letzten 14 Tage sind bei der Ausführung des Skripts Fehler aufgetreten.\nHier ist das komplette Logfile der letzten 14 Tage:\n\n${log_content}"
    
    send_mail "${subj}" "${mail_body}"
  else
    log "INFO" "Keine Fehler im Überwachungszeitraum aufgetreten."
  fi
 
  # Logfile leeren und neuen Zyklus starten
  > "${LOG_FILE}"
  echo "${current_time}" > "${LOG_CYCLE_FILE}"
  log "INFO" "Logdatei zurückgesetzt. Neuer 14-Tage Zyklus gestartet."
fi

log "INFO" "--- Starte Überprüfung für Datastore '${DATASTORE}' ---"

# Voraussetzungen prüfen
command_exists() { command -v "$1" >/dev/null 2>&1; }
if ! command_exists proxmox-backup-debug; then
  log "ERROR" "proxmox-backup-debug fehlt. Skript muss als root auf PBS laufen."
  exit 1
fi
if ! command_exists jq || ! command_exists awk; then
  log "ERROR" "jq oder awk fehlt. Bitte installieren."
  exit 1
fi

# Einheit umwandeln: Basis 1000 (Exakt wie die PBS GUI rechnet!)
bytes_to_human() {
  local b="$1"
  awk -v b="$b" 'BEGIN {
    if (b >= 1000000000000) printf "%.2f TB", b/1000000000000
    else if (b >= 1000000000) printf "%.2f GB", b/1000000000
    else if (b >= 1000000)    printf "%.2f MB", b/1000000
    else if (b <= -1000000000) printf "%.2f GB", b/1000000000
    else                       printf "%d B",   b
  }'
}

# 1. Daten von der API abrufen
if ! json_out="$(proxmox-backup-debug api get /status/datastore-usage --output-format json 2>/dev/null)"; then
  log "ERROR" "Abrufen der Datastore-Daten von der API fehlgeschlagen."
  exit 1
fi

ds_json="$(printf '%s' "${json_out}" | jq -r --arg ds "${DATASTORE}" '.[] | select(.store == $ds)')"
if [ -z "${ds_json}" ]; then
  log "ERROR" "Datastore '${DATASTORE}' nicht in der API gefunden."
  exit 1
fi

# Rohe Byte-Werte aus der API lesen
used="$(printf '%s' "${ds_json}"  | jq -r '.used // empty')"
avail="$(printf '%s' "${ds_json}" | jq -r '.avail // empty')"

if [ -z "${avail}" ] || [ -z "${used}" ]; then
  log "ERROR" "API lieferte unvollständige Daten (used/avail)."
  exit 1
fi

# WICHTIG: Die PBS GUI ignoriert den "total" Wert der API (da dieser die ext4-Root-Reserve enthält).
# Stattdessen berechnet die GUI die Gesamtgröße dynamisch aus den nutzbaren Werten:
total=$(( used + avail ))

# 2. Prozentwerte berechnen
avail_percent_int="$(( avail * 100 / total ))"
avail_percent_fmt="$(awk -v a="${avail}" -v t="${total}" 'BEGIN { printf "%.2f", a*100/t }')"
used_percent_fmt="$(awk -v u="${used}" -v t="${total}" 'BEGIN { printf "%.2f", u*100/t }')"

# 3. Trend-Analyse
growth_per_day_bytes=0
days_until_full="Unbekannt (zu wenig Daten)"
trend_string="Speicher wächst aktuell nicht."

if [ -f "${STATE_FILE}" ]; then
  read -r last_time last_used < "${STATE_FILE}"
  time_diff=$(( current_time - last_time ))
  used_diff=$(( used - last_used ))

  if [ "${time_diff}" -ge 3600 ]; then
    growth_per_day_bytes=$(awk -v diff="${used_diff}" -v t="${time_diff}" 'BEGIN { printf "%d", (diff / t) * 86400 }')
    if [ "${growth_per_day_bytes}" -gt 0 ]; then
      days_left=$(awk -v a="${avail}" -v g="${growth_per_day_bytes}" 'BEGIN { printf "%d", a / g }')
      days_until_full="${days_left} Tagen"
      trend_string="$(bytes_to_human "${growth_per_day_bytes}") pro Tag"
    elif [ "${growth_per_day_bytes}" -lt 0 ]; then
      trend_string="Speicher wird freigegeben ($(bytes_to_human "${growth_per_day_bytes}") pro Tag)"
      days_until_full="N/A (Speicher sinkt)"
    fi
  fi
fi

if [ ! -f "${STATE_FILE}" ] || [ "${time_diff:-0}" -ge 86400 ]; then
  echo "${current_time} ${used}" > "${STATE_FILE}"
fi

# 4. Ausgabe formatieren
total_human="$(bytes_to_human "${total}")"
used_human="$(bytes_to_human "${used}")"
avail_human="$(bytes_to_human "${avail}")"

log "INFO" "Status: Gesamt ${total_human} | Belegt ${used_human} (${used_percent_fmt}%) | Frei ${avail_human} (${avail_percent_fmt}%)"
log "INFO" "Trend: Wachstum ${trend_string} | Voll in: ${days_until_full}"

body_text=$(printf \
'Server:    %s
Datastore: %s

Speicherübersicht:
------------------
Gesamt:       %s
Belegt:       %s (%s%%)
Verfügbar:    %s (%s%%)

Trend & Prognose:
-----------------
Wachstum:     %s
Voraussichtlich voll in: %s
' \
  "${PIVOT_HOST}" "${DATASTORE}" \
  "${total_human}" \
  "${used_human}" "${used_percent_fmt}" \
  "${avail_human}" "${avail_percent_fmt}" \
  "${trend_string}" "${days_until_full}")

# 5. Senden
if [ "${MODE}" = "report" ]; then
  log "INFO" "Report-Modus aktiv. Sende Status-E-Mail."
  subject="Status: PBS Datastore '${DATASTORE}' - ${avail_percent_fmt}% frei [${PIVOT_HOST}]"
  send_mail "${subject}" "Täglicher Status-Bericht für Datastore Speicherplatz:\n\n${body_text}"
  log "INFO" "Prüfung abgeschlossen."
  exit 0
fi

if [ "${avail_percent_int}" -lt "${WARN_THRESHOLD_PERCENT}" ]; then
  log "WARNING" "Schwellenwert unterschritten (${avail_percent_fmt}% < ${WARN_THRESHOLD_PERCENT}%). Sende Warn-E-Mail."
  subject="WARNUNG: PBS Datastore '${DATASTORE}' - Nur noch ${avail_percent_fmt}% frei [${PIVOT_HOST}]"
  warn_header="ACHTUNG: Der verfügbare Speicherplatz liegt unter dem konfigurierten Schwellenwert von ${WARN_THRESHOLD_PERCENT}%!\n\n"
  send_mail "${subject}" "${warn_header}${body_text}"
else
  log "INFO" "Speicherplatz ausreichend. Keine Aktion erforderlich."
fi

log "INFO" "Prüfung erfolgreich abgeschlossen."
exit 0


Ich bedanke mich schonmal im Vorraus für jede hilfe.
#LearningByDoing

PS: für Dieses Script wurde Kaum bis keine KI genutzt.
Bedeutet ich nutze VS-Code zum Programmieren welches die automatische einrückung für mich vornimmt, aber nicht beim Code Schreiben allgemein hilf..
Ich lerne durch meine Fehler und suche mir fast alles durch Tutorials zusammen.

MFG
 
Hi,

I answer in english since my german is quite bad :-)

Looking at your script, I can see the issue with the trend analysis. There are a few bugs:
The core problem: The time_diff variable is used in the if [ ! -f "${STATE_FILE}" ] check outside the block where it's defined, causing it to always re-write the state file and never accumulate enough data. So, the state file gets overwritten too easily (every 24h resets the baseline).
Secondary issue: The PBS API actually provides a estimated-full field directly — you don't need to calculate it yourself!
Here's the fixed and improved version of the relevant sections:

Bash:
# 3. Trend-Analyse
growth_per_day_bytes=0
days_until_full="Unbekannt (zu wenig Daten)"
trend_string="Speicher wächst aktuell nicht."

# Erst: Schau ob die PBS-API selbst eine Schätzung liefert
api_estimated_full="$(printf '%s' "${ds_json}" | jq -r '."estimated-full" // empty')"

if [ -n "${api_estimated_full}" ] && [ "${api_estimated_full}" != "null" ]; then
  # PBS liefert Unix-Timestamp → in lesbares Datum umwandeln
  full_date="$(date -d "@${api_estimated_full}" '+%d.%m.%Y')"
  days_left=$(( (api_estimated_full - current_time) / 86400 ))
  days_until_full="${days_left} Tagen (ca. ${full_date})"
  log "INFO" "PBS API liefert estimated-full: ${full_date}"
fi

# Eigene Trend-Berechnung als Fallback / Ergänzung
time_diff=0
if [ -f "${STATE_FILE}" ]; then
  read -r last_time last_used < "${STATE_FILE}"
  time_diff=$(( current_time - last_time ))
  used_diff=$(( used - last_used ))

  if [ "${time_diff}" -ge 3600 ] && [ "${used_diff}" -ne 0 ]; then
    growth_per_day_bytes=$(awk \
      -v diff="${used_diff}" \
      -v t="${time_diff}" \
      'BEGIN { printf "%d", (diff / t) * 86400 }')

    if [ "${growth_per_day_bytes}" -gt 0 ]; then
      trend_string="$(bytes_to_human "${growth_per_day_bytes}") pro Tag"
      # Nur eigene Berechnung nutzen wenn API keinen Wert geliefert hat
      if [ -z "${api_estimated_full}" ] || [ "${api_estimated_full}" = "null" ]; then
        days_left=$(awk \
          -v a="${avail}" \
          -vg="${growth_per_day_bytes}" \
          'BEGIN { printf "%d", a / g }')
        days_until_full="${days_left} days (own calculation)"
      fi
    elif [ "${growth_per_day_bytes}" -lt 0 ]; then
      trend_string="Storage is released ($(bytes_to_human "${growth_per_day_bytes}") per day)"
      days_until_full="N/A (memory drops)"
    fi
  fi
fi

# Update state file — ONLY if none exists yet OR
# the last measurement is older than 24h.
# IMPORTANT: Don't overwrite every run, otherwise there will never be a diff!
if [ ! -f "${STATE_FILE}" ] || [ "${time_diff}" -ge 86400]; then
  echo "${current_time} ${used}" > "${STATE_FILE}"
  log "INFO" "State file updated (base for next trend comparison)."
fi
 
  • Like
Reactions: plurgi95
Hi,

I answer in english since my german is quite bad :-)

Looking at your script, I can see the issue with the trend analysis. There are a few bugs:
The core problem: The time_diff variable is used in the if [ ! -f "${STATE_FILE}" ] check outside the block where it's defined, causing it to always re-write the state file and never accumulate enough data. So, the state file gets overwritten too easily (every 24h resets the baseline).
Secondary issue: The PBS API actually provides a estimated-full field directly — you don't need to calculate it yourself!
Here's the fixed and improved version of the relevant sections:
Thanks ill Test this :)
 
Here's the fixed and improved version of the relevant sections:
I Tryed your Fix

And Got an Error,
But i Fixed it now with Your Fix Itsselv.

The Problem was a Wrong Fieldname in the JSON

Bash:
# 3. Trend-Analyse
trend_string="Speicher wächst aktuell nicht (laut API)."
days_until_full="Nicht absehbar (kein oder negatives Wachstum)"

# Schau ob die PBS-API selbst eine Schätzung liefert (Feld heißt 'estimated-full-date')
api_estimated_full="$(printf '%s' "${ds_json}" | jq -r '."estimated-full-date" // empty')"

if [ -n "${api_estimated_full}" ] && [ "${api_estimated_full}" != "null" ] && [ "${api_estimated_full}" -gt 0 ]; then
  # PBS liefert Unix-Timestamp → in lesbares Datum umwandeln
  full_date="$(date -d "@${api_estimated_full}" '+%d.%m.%Y')"
  days_left=$(( (api_estimated_full - current_time) / 86400 ))
 
  # Verhindern, dass negative Tage angezeigt werden, falls der Zeitpunkt schon erreicht ist
  if [ "${days_left}" -lt 0 ]; then days_left=0; fi
 
  days_until_full="In ${days_left} Tagen (ca. ${full_date})"
  trend_string="Siehe Vorhersage."
  log "INFO" "PBS API liefert estimated-full-date: ${full_date}"
else
  log "INFO" "PBS API liefert kein estimated-full-date (wächst nicht / zu wenig Daten)."
fi

# Update state file — NUR wenn noch keines existiert ODER die letzte Messung > 24h her ist.
time_diff=0
if [ -f "${STATE_FILE}" ]; then
  read -r last_time last_used < "${STATE_FILE}"
  time_diff=$(( current_time - last_time ))
fi

# ACHTUNG: Syntax-Fehler bei 86400] behoben (Leerzeichen fehlte)
if [ ! -f "${STATE_FILE}" ] || [ "${time_diff}" -ge 86400 ]; then
  echo "${current_time} ${used}" > "${STATE_FILE}"
  log "INFO" "State file aktualisiert."
fi

# 4. Ausgabe formatieren
total_human="$(bytes_to_human "${total}")"
used_human="$(bytes_to_human "${used}")"
avail_human="$(bytes_to_human "${avail}")"

E-Mail Now has Following informations:

Täglicher Status-Bericht für Datastore Speicherplatz:

Server: pbs01
Datastore: local

Speicherübersicht:
------------------
Gesamt: 22.71 TB
Belegt: 4.55 TB (20.02%)
Verfügbar: 18.16 TB (79.98%)

Trend & Prognose:
-----------------
Wachstum: Siehe Prognose
Voraussichtlich voll in: 113 Tagen (ca. 02.08.2026)

Thanks for your help
 
  • Like
Reactions: psalkiewicz