Plan2026/03-decision-monitoring-pbs.md

21 KiB

Decision: Monitoring especifico de PBS (punto 1.3)

El problema

PBS no tiene exporter Prometheus nativo. Las metricas hay que extraerlas de alguna forma. Evaluamos 3 vias para alimentar VictoriaMetrics/Grafana con datos de PBS.


Via A: PBS metricas nativas a InfluxDB

Que es

PBS tiene soporte nativo para enviar metricas a InfluxDB. Se configura desde la GUI o CLI sin instalar nada en el PBS.

Como funciona

PBS ----(push nativo)----> InfluxDB ----(datasource)----> Grafana
                                                              ^
VictoriaMetrics ---------(datasource)---------------------+

Grafana lee de dos backends: VictoriaMetrics (infra) e InfluxDB (PBS).

Configuracion

# En cada PBS, añadir metric server
proxmox-backup-manager metric-server add influxdb mi-influxdb \
    --server <ip-influxdb> \
    --port 8089 \
    --protocol udp

Metricas que envia PBS nativamente

  • Uso de CPU, RAM, IO del host PBS
  • Espacio total/usado/disponible por datastore
  • Estado de GC (running, last-run)
  • Trafico de red
  • Estadisticas de chunks (deduplication ratio)

Lo que NO envia nativamente

  • Estado de sync jobs (exito/fallo, ultimo run, bytes transferidos)
  • Estado de verify jobs (errores encontrados, ultimo run)
  • Ultimo backup por datastore/cliente
  • Quota vs uso real por cliente
  • Lista de snapshots y su antiguedad
  • Estado de maintenance mode

Veredicto

Insuficiente como solucion unica. Util como complemento rapido para metricas basicas de espacio, pero no cubre lo operacionalmente critico.


Via B: natrontech/pbs-exporter + script complementario (RECOMENDADA)

Que es

Combinacion de un exporter comunitario maduro en Go (natrontech/pbs-exporter) que cubre datastores, snapshots y host, complementado con un script ligero via textfile collector para las metricas de jobs (GC, verify, sync) que el exporter no cubre.

pbs-exporter (natrontech)

Repositorio: https://github.com/natrontech/pbs-exporter Lenguaje: Go | Licencia: GPL-3.0 | Version: v0.8.0 (dic 2025) | Stars: 45 Testado con: PBS 3.x

Metricas que expone

# === SISTEMA PBS ===
pbs_up                                          # 1 si PBS responde, 0 si no
pbs_version{version="3.3-1"}                    # Version de PBS

# === ALMACENAMIENTO POR DATASTORE ===
pbs_available{datastore="cliente1"}             # Bytes disponibles
pbs_size{datastore="cliente1"}                  # Bytes totales
pbs_used{datastore="cliente1"}                  # Bytes usados

# === SNAPSHOTS ===
pbs_snapshot_count{datastore="cliente1"}        # Total snapshots
pbs_snapshot_vm_count{id="101",datastore=".."}  # Snapshots por VM
pbs_snapshot_vm_last_timestamp{id="101",...}     # Timestamp ultimo backup por VM
pbs_snapshot_vm_last_verify{id="101",...}        # Estado verificacion ultimo backup

# === HOST ===
pbs_host_cpu_usage                              # Uso CPU
pbs_host_memory_free / _total / _used           # RAM
pbs_host_swap_free / _total / _used             # Swap
pbs_host_disk_available / _total / _used        # Disco
pbs_host_uptime                                 # Uptime en segundos
pbs_host_io_wait                                # IO wait
pbs_host_load1 / _load5 / _load15              # Load averages

# === SUSCRIPCION ===
pbs_host_subscription_status                    # Estado licencia
pbs_host_subscription_due_timestamp_seconds     # Expiracion

Modo multi-target (clave para escalar)

Un solo pbs-exporter puede monitorizar multiples PBS:

# vmagent scrape config
- job_name: 'pbs'
  metrics_path: /metrics
  params:
    module: [default]
  static_configs:
    - targets:
      - https://pbs-server1:8007
      - https://pbs-server2:8007
      - https://pbs-server3:8007
  relabel_configs:
    - source_labels: [__address__]
      target_label: __param_target
    - source_labels: [__param_target]
      target_label: instance
    - target_label: __address__
      replacement: pbs-exporter:10019  # un solo exporter central

Esto significa: 1 instancia de pbs-exporter para N servidores PBS. No hay que instalar nada en cada PBS.

Despliegue

Opcion 1: Docker (recomendado para centralizacion)

services:
  pbs-exporter:
    image: ghcr.io/natrontech/pbs-exporter:latest
    container_name: pbs-exporter
    restart: unless-stopped
    environment:
      - PBS_API_TOKEN_NAME=pbs-exporter
      - PBS_API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
      - PBS_USERNAME=monitoring@pbs
      - PBS_INSECURE=true
    ports:
      - "10019:10019"

Opcion 2: Binario + systemd (en cada PBS si no se usa multi-target)

# Crear usuario dedicado en PBS (minimos privilegios)
proxmox-backup-manager user create monitoring@pbs
proxmox-backup-manager user generate-token monitoring@pbs pbs-exporter
# Dar solo permisos Audit
proxmox-backup-manager acl update / Audit --auth-id monitoring@pbs

# Descargar binario
wget https://github.com/natrontech/pbs-exporter/releases/download/v0.8.0/pbs-exporter_linux_amd64
chmod +x pbs-exporter_linux_amd64
mv pbs-exporter_linux_amd64 /usr/local/bin/pbs-exporter

# Systemd unit
cat > /etc/systemd/system/pbs-exporter.service << 'EOF'
[Unit]
Description=PBS Prometheus Exporter
After=network.target proxmox-backup.service

[Service]
Type=simple
EnvironmentFile=/etc/default/pbs-exporter
ExecStart=/usr/local/bin/pbs-exporter
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
EOF

# Configuracion
cat > /etc/default/pbs-exporter << 'EOF'
PBS_API_TOKEN_NAME=pbs-exporter
PBS_API_TOKEN=<token-generado>
PBS_USERNAME=monitoring@pbs
PBS_ENDPOINT=https://localhost:8007
PBS_INSECURE=true
PBS_LISTEN_ADDRESS=:10019
EOF

systemctl enable --now pbs-exporter

Lo que NO cubre pbs-exporter

  • Estado de GC jobs (ultimo run, duracion, espacio liberado)
  • Estado de sync jobs (exito/fallo, ultimo run, bytes, remote)
  • Estado de verify jobs (como job programado, no como verificacion de snapshot)
  • Maintenance mode
  • Quota/reservation ZFS por datastore

Script complementario para jobs (textfile collector)

Solo cubre lo que pbs-exporter no tiene. ~40 lineas.

#!/bin/bash
# /usr/local/sbin/pbs-jobs-metrics.sh
# Complementa pbs-exporter con metricas de GC, verify y sync jobs
# Cron: */10 * * * * /usr/local/sbin/pbs-jobs-metrics.sh

OUTPUT="/var/lib/node_exporter/textfile/pbs_jobs.prom"
TMPFILE="${OUTPUT}.tmp"
PBS_API="https://localhost:8007/api2/json"

# Autenticacion con API token (sin password)
TOKEN_NAME="monitoring@pbs!pbs-exporter"
TOKEN_VALUE="<mismo-token-que-pbs-exporter>"
AUTH_HEADER="Authorization: PBSAPIToken=${TOKEN_NAME}:${TOKEN_VALUE}"

api_get() {
    curl -sk -H "$AUTH_HEADER" "${PBS_API}${1}"
}

> "$TMPFILE"

# GC jobs
api_get "/admin/gc" | jq -r '.data[] | @base64' | while read row; do
    _jq() { echo "$row" | base64 -d | jq -r "$1 // 0"; }
    store=$(_jq '.store')
    last=$(_jq '."last-run-endtime"')
    state=$(_jq '."last-run-state"')
    ok=0; [ "$state" = "ok" ] && ok=1
    echo "pbs_gc_last_run_timestamp{datastore=\"${store}\"} ${last}" >> "$TMPFILE"
    echo "pbs_gc_last_status{datastore=\"${store}\"} ${ok}" >> "$TMPFILE"
done

# Verify jobs
api_get "/admin/verify" | jq -r '.data[] | @base64' | while read row; do
    _jq() { echo "$row" | base64 -d | jq -r "$1 // 0"; }
    store=$(_jq '.store')
    last=$(_jq '."last-run-endtime"')
    state=$(_jq '."last-run-state"')
    ok=0; [ "$state" = "ok" ] && ok=1
    echo "pbs_verify_last_run_timestamp{datastore=\"${store}\"} ${last}" >> "$TMPFILE"
    echo "pbs_verify_last_status{datastore=\"${store}\"} ${ok}" >> "$TMPFILE"
done

# Sync jobs
api_get "/admin/sync" | jq -r '.data[] | @base64' | while read row; do
    _jq() { echo "$row" | base64 -d | jq -r "$1 // 0"; }
    store=$(_jq '.store')
    remote=$(_jq '.remote')
    last=$(_jq '."last-run-endtime"')
    state=$(_jq '."last-run-state"')
    ok=0; [ "$state" = "ok" ] && ok=1
    echo "pbs_sync_last_run_timestamp{datastore=\"${store}\",remote=\"${remote}\"} ${last}" >> "$TMPFILE"
    echo "pbs_sync_last_status{datastore=\"${store}\",remote=\"${remote}\"} ${ok}" >> "$TMPFILE"
done

mv "$TMPFILE" "$OUTPUT"

Arquitectura resultante

vmagent scrape
    │
    ├── :9100   node_exporter      (sistema + ZFS quotas)
    │             └── textfile/pbs_jobs.prom  (GC/verify/sync jobs via script)
    │
    └── :10019  pbs-exporter       (datastores, snapshots, ultimo backup por VM, host)
                  └── multi-target: puede cubrir N servidores PBS

Un unico backend: VictoriaMetrics. Sin InfluxDB. Todo en PromQL/MetricsQL.

Ventajas de esta combinacion

  • pbs-exporter hace el 80% del trabajo pesado (datastores, snapshots por VM, host)
  • Script de 40 lineas cubre el 20% restante (jobs)
  • Mismo token de API para ambos (un solo usuario monitoring@pbs)
  • Multi-target: un pbs-exporter central para todos los PBS
  • Si el exporter crece y añade metricas de jobs en el futuro, se elimina el script

Desventajas

  • Dos fuentes de metricas PBS (exporter + textfile), pero ambas en el mismo backend
  • El script tiene resolucion de cron (10 min), el exporter es real-time
  • Depende de un proyecto comunitario (45 stars, pero activo y en Go)

Via C: Script completo custom (textfile collector) - DESCARTADA como principal

La Via B original del documento anterior. Sigue siendo valida como fallback si pbs-exporter deja de mantenerse o tiene algun problema, pero no tiene sentido reescribir en bash lo que ya existe en Go con mejor rendimiento y multi-target.

Se mantiene el script completo en la seccion de referencia por si fuera necesario en el futuro.

Script completo de referencia (click para expandir)
#!/bin/bash
# /usr/local/sbin/pbs-metrics-exporter.sh
# Version completa: genera TODAS las metricas PBS
# Solo usar si pbs-exporter de natrontech no esta disponible

OUTPUT="/var/lib/node_exporter/textfile/pbs_metrics.prom"
TMPFILE="${OUTPUT}.tmp"
PBS_API="https://localhost:8007/api2/json"
PBS_USER="root@pam"
PBS_PASS="$(cat /etc/pbs-monitoring-password)"

TICKET_DATA=$(curl -sk -d "username=${PBS_USER}&password=${PBS_PASS}" \
    "${PBS_API}/access/ticket")
TICKET=$(echo "$TICKET_DATA" | jq -r '.data.ticket')
CSRF=$(echo "$TICKET_DATA" | jq -r '.data.CSRFPreventionToken')

api_get() {
    curl -sk -b "PBSAuthCookie=${TICKET}" -H "CSRFPreventionToken: ${CSRF}" \
        "${PBS_API}${1}"
}

> "$TMPFILE"

# Datastore usage
api_get "/status/datastore-usage" | jq -r '.data[] | @base64' | while read row; do
    _jq() { echo "$row" | base64 -d | jq -r "$1 // 0"; }
    name=$(_jq '.store')
    echo "pbs_datastore_size_bytes{datastore=\"${name}\"} $(_jq '.total')" >> "$TMPFILE"
    echo "pbs_datastore_used_bytes{datastore=\"${name}\"} $(_jq '.used')" >> "$TMPFILE"
    echo "pbs_datastore_available_bytes{datastore=\"${name}\"} $(_jq '.avail')" >> "$TMPFILE"
done

# GC jobs
api_get "/admin/gc" | jq -r '.data[] | @base64' | while read row; do
    _jq() { echo "$row" | base64 -d | jq -r "$1 // 0"; }
    store=$(_jq '.store'); last=$(_jq '."last-run-endtime"')
    state=$(_jq '."last-run-state"'); ok=0; [ "$state" = "ok" ] && ok=1
    echo "pbs_gc_last_run_timestamp{datastore=\"${store}\"} ${last}" >> "$TMPFILE"
    echo "pbs_gc_last_status{datastore=\"${store}\"} ${ok}" >> "$TMPFILE"
done

# Verify jobs
api_get "/admin/verify" | jq -r '.data[] | @base64' | while read row; do
    _jq() { echo "$row" | base64 -d | jq -r "$1 // 0"; }
    store=$(_jq '.store'); last=$(_jq '."last-run-endtime"')
    state=$(_jq '."last-run-state"'); ok=0; [ "$state" = "ok" ] && ok=1
    echo "pbs_verify_last_run_timestamp{datastore=\"${store}\"} ${last}" >> "$TMPFILE"
    echo "pbs_verify_last_status{datastore=\"${store}\"} ${ok}" >> "$TMPFILE"
done

# Sync jobs
api_get "/admin/sync" | jq -r '.data[] | @base64' | while read row; do
    _jq() { echo "$row" | base64 -d | jq -r "$1 // 0"; }
    store=$(_jq '.store'); remote=$(_jq '.remote')
    last=$(_jq '."last-run-endtime"')
    state=$(_jq '."last-run-state"'); ok=0; [ "$state" = "ok" ] && ok=1
    echo "pbs_sync_last_run_timestamp{datastore=\"${store}\",remote=\"${remote}\"} ${last}" >> "$TMPFILE"
    echo "pbs_sync_last_status{datastore=\"${store}\",remote=\"${remote}\"} ${ok}" >> "$TMPFILE"
done

mv "$TMPFILE" "$OUTPUT"

Comparativa final actualizada

Aspecto A: InfluxDB nativo B: pbs-exporter + script (RECOMENDADA) C: Script completo custom
Esfuerzo de setup 5 minutos 30 min exporter + 1h script 2 horas
Datastores (espacio) Si Si (exporter) Si
Snapshots por VM No Si (exporter) Posible (mas script)
Ultimo backup por VM No Si (exporter) Posible (mas script)
GC jobs Basico Si (script) Si
Verify jobs No Si (script) Si
Sync jobs No Si (script) Si
Host metrics Si Si (exporter) Manual
Multi-target (1 instancia para N PBS) N/A Si (exporter) No
Backend InfluxDB (extra) VictoriaMetrics (unificado) VictoriaMetrics (unificado)
Dependencias InfluxDB Binario Go + bash/curl/jq bash/curl/jq
Resolucion ~30s Real-time (exporter) + 10min (script) 5-15 min
Mantenimiento Nulo Bajo Medio
Alertas PromQL No Si Si

Recomendacion final

Despliegue en fases

Fase 1 (dia 1): pbs-exporter centralizado

  1. Crear usuario monitoring@pbs + API token en cada PBS (Ansible playbook)
  2. Desplegar un pbs-exporter en Docker (VM de monitoring) en modo multi-target
  3. Configurar vmagent para scrapear todos los PBS via multi-target
  4. Dashboard Grafana basico: datastores, espacio, ultimo backup por VM

Fase 2 (dia 2-3): script complementario de jobs

  1. Desplegar pbs-jobs-metrics.sh en cada PBS via Ansible
  2. Configurar cron cada 10 minutos
  3. node_exporter ya esta instalado (del punto 1.2), solo añadir textfile path
  4. Dashboard Grafana: añadir paneles de GC, verify, sync jobs

Fase 3 (semana 2): alertas

  1. Configurar alertas en Grafana o vmalert:
    • pbs_snapshot_vm_last_timestamp > 48h sin backup
    • pbs_gc_last_status != 1
    • pbs_verify_last_status != 1
    • pbs_sync_last_status != 1
    • pbs_used / pbs_size > 0.90
  2. Notificaciones: Telegram para criticas, email para warnings

Fase 4 (mes 2): capacity planning

  1. Dashboard de tendencias de crecimiento por datastore
  2. Prediccion de llenado con predict_linear() de PromQL
  3. Deteccion de quotas sobredimensionadas

Dashboards PBS para Grafana

Dashboard 1: Vista global PBS (para el NOC / pantalla grande)

┌─────────────────────────────────────────────────────────┐
│  PBS GLOBAL STATUS                                       │
├──────────────┬──────────────┬──────────────┬────────────┤
│  Datastores  │  Espacio     │  Jobs OK     │  Alertas   │
│     42       │  78% usado   │   39/42      │    3       │
├──────────────┴──────────────┴──────────────┴────────────┤
│                                                          │
│  [Tabla] Datastores con problemas                        │
│  - cliente7:  verify fallido hace 5 dias                 │
│  - cliente12: sync fallido, ultimo ok hace 45 dias       │
│  - cliente23: espacio > 90% quota                        │
│                                                          │
├──────────────────────────────────────────────────────────┤
│  [Barras] Top 10 datastores por ocupacion                │
│  ████████████████████░░░░ cliente3  82%                  │
│  ███████████████████░░░░░ cliente1  78%                  │
│  ...                                                     │
├──────────────────────────────────────────────────────────┤
│  [Timeline] Jobs ejecutados hoy                          │
│  09:00 GC cliente1 OK (12min)                            │
│  09:30 GC cliente2 OK (8min)                             │
│  10:00 Verify cliente5 OK (45min)                        │
│  10:30 Verify cliente8 FAIL (errors: 2)                  │
└──────────────────────────────────────────────────────────┘

Dashboard 2: Detalle por datastore (para operaciones)

┌─────────────────────────────────────────────────────────┐
│  DATASTORE: cliente1          Status: ACTIVE             │
├──────────────┬──────────────┬──────────────┬────────────┤
│  Espacio     │  Quota       │  Dedup ratio │  Snapshots │
│  322 GB      │  600 GB      │  1.42x       │    45      │
├──────────────┴──────────────┴──────────────┴────────────┤
│  [Grafico linea] Espacio usado ultimos 90 dias           │
│  (tendencia de crecimiento + prediccion de llenado)      │
├──────────────────────────────────────────────────────────┤
│  JOBS                                                    │
│  GC:     ultimo 2h ago, duro 12min, libero 5.2GB    [OK]│
│  Verify: ultimo 1d ago, 45 snaps ok, 0 errores      [OK]│
│  Sync:   ultimo 25d ago, desde pbs3343, 10.7GB       [OK]│
│  Proximo sync: dia 15 a las 09:23                        │
├──────────────────────────────────────────────────────────┤
│  [Tabla] Snapshots recientes                             │
│  vm/101  2026-03-12  12.3GB  verified                    │
│  vm/101  2026-03-11  12.1GB  verified                    │
│  vm/102  2026-03-12   8.7GB  outdated                    │
└──────────────────────────────────────────────────────────┘

Dashboard 3: Capacidad y planificacion (para management)

┌─────────────────────────────────────────────────────────┐
│  CAPACITY PLANNING                                       │
├──────────────────────────────────────────────────────────┤
│  Pool9: 85TB usados / 120TB total (71%)                  │
│  [Grafico] Tendencia 12 meses + prediccion               │
│  "Al ritmo actual, pool9 se llena en 8 meses"            │
├──────────────────────────────────────────────────────────┤
│  [Tabla] Clientes por crecimiento mensual                │
│  cliente3:  +15GB/mes  (el que mas crece)                │
│  cliente1:  +12GB/mes                                    │
│  cliente7:   +8GB/mes                                    │
├──────────────────────────────────────────────────────────┤
│  [Tabla] Clientes con quota sobredimensionada            │
│  cliente15: usa 20GB de 600GB quota (3%)                 │
│  cliente22: usa 45GB de 600GB quota (7.5%)               │
│  "Oportunidad de reclamar 1.1TB de quota"                │
├──────────────────────────────────────────────────────────┤
│  Resumen comercial                                       │
│  Clientes activos: 40                                    │
│  GB comerciales vendidos: 8,200 GB                       │
│  GB reales usados: 28,500 GB (ratio 3.5x real)           │
│  GB quota asignada: 49,200 GB (ratio 6x)                 │
└──────────────────────────────────────────────────────────┘