Automação de limpeza de backups antigos (>30 dias) no Proxmox Backup Server 3.3.x

Problema: acúmulo de backups antigos no PBS ocupando espaço desnecessário.
Ação: criação de um processo que varre todos os datastores e namespaces, identifica snapshots com mais de 30 dias, gera relatórios diários e executa exclusão segura seguida de garbage-collect.
Agendamento: via cron na madrugada, com opção de testes manuais.
Resultado (1ª execução): foram encontrados itens muito antigos (até 10–11 meses), liberando ≈640 GB.


1) Pré-requisitos (uma vez)

# utilitários e pastas de log
mkdir -p /var/log/pbs-prune

Requisitos: bash, CLIs do PBS (proxmox-backup-manager, proxmox-backup-client), usuário root no PBS.


2) Script principal — varredura e deleção

Arquivo: /root/pbs-prune-older-than-30d.sh

O que faz (resumo):

  • Descobre datastores e namespaces via CLI.
  • Em plan: lista snapshots > 30 dias e grava em delete-report.tsv.
  • Em exec: remove cada snapshot listado, registra em deleted.tsv e roda garbage-collect ao final de cada datastore.

Crie o arquivo e cole o conteúdo:

nano /root/pbs-prune-older-than-30d.sh
#!/usr/bin/env bash
set -euo pipefail

# --- Parâmetros ---
CUTOFF_DAYS="${CUTOFF_DAYS:-30}"
MODE="${1:-plan}"             # plan (só planeja) | exec (executa)
DAY_UTC="$(date -u +%F)"
RUN_DIR="/var/log/pbs-prune/${DAY_UTC}"
REPORT="${RUN_DIR}/delete-report.tsv"
DELETED="${RUN_DIR}/deleted.tsv"

log()   { printf '[%s] %s\n' "$(date -u +%FT%TZ)" "$*" ; }
ensure() { mkdir -p "${RUN_DIR}"; }

# --- Descobrir datastores e namespaces ---
list_datastores() {
  proxmox-backup-manager datastore list --output-format json | jq -r '.[].name'
}
list_namespaces() {
  local ds="$1"
  echo "root"
  proxmox-backup-manager namespace list "${ds}" --output-format json | jq -r '.[].ns' || true
}

# --- Escrever cabeçalhos dos relatórios ---
init_reports() {
  echo -e "store\tns\ttype/id\tgroup\tepoch\tiso8601" > "${REPORT}"
  echo -e "store\tns\ttype/id\tiso8601\tstatus\texit_code\tstderr" > "${DELETED}"
}

# --- Planejar (varrer >30d e popular REPORT) ---
plan_store_ns() {
  local store="$1" ns="$2"
  local ns_arg=()
  [ "${ns}" != "root" ] && ns_arg=(--ns "${ns}")

  proxmox-backup-client \
    ${ns_arg+"${ns_arg[@]}"} \
    snapshots --repository "localhost:${store}" --output-format json \
  | jq -r --arg now "$(date -u +%s)" --arg cutoff "$(( CUTOFF_DAYS*24*3600 ))" '
      .[] | .snapshots[]? | select((.timestamp | tonumber) <= ((($now|tonumber)-($cutoff|tonumber))))
      | "\(.store)\t\(.ns // "root")\t\(.backup-type + "/" + .backup-id)\t\(.backup-id)\t\(.timestamp)\t\(.time)"
    ' >> "${REPORT}" || true
}

# --- Executar deleção e registrar ---
exec_delete_row() {
  local store="$1" ns="$2" type_id="$3" iso="$4"
  local ns_arg=()
  [ "${ns}" != "root" ] && ns_arg=(--ns "${ns}")
  local btype="${type_id%%/*}"
  local bid="${type_id#*/}"
  local rc=0 err=""

  set +e
  proxmox-backup-manager \
    "${btype}-remove" \
    --repository "localhost:${store}" \
    ${ns_arg+"${ns_arg[@]}"} \
    --backup-id "${bid}" \
    --backup-time "${iso}" 2> >(err=$(cat); typeset -p err >/dev/null)
  rc=$?
  set -e

  printf "%s\t%s\t%s\t%s\t%s\t%s\n" "${store}" "${ns}" "${type_id}" "${iso}" "$([ $rc -eq 0 ] && echo OK || echo FAILED)" "${rc}" \
  >> "${DELETED}"

  return $rc
}

garbage_collect_store() {
  local store="$1"
  proxmox-backup-manager datastore garbage-collect "${store}" >/dev/null 2>&1 || true
}

main() {
  ensure
  init_reports
  log "Início | cutoff=${CUTOFF_DAYS}d ($(date -u +%s)) | modo=${MODE}"

  while read -r store; do
    [ -z "${store}" ] && continue
    log "Datastore: ${store}"
    while read -r ns; do
      [ -z "${ns}" ] && continue
      log "  Namespace: ${ns}"
      if [ "${MODE}" = "plan" ]; then
        plan_store_ns "${store}" "${ns}"
      else
        [ -s "${REPORT}" ] || plan_store_ns "${store}" "${ns}"

        awk -F'\t' -v s="${store}" -v n="${ns}" 'NR>1 && $1==s && $2==n {print}' "${REPORT}" \
        | while IFS=$'\t' read -r s ns typeid group epoch iso; do
            exec_delete_row "${s}" "${ns}" "${typeid}" "${iso}" || true
          done
      fi
    done < <(list_namespaces "${store}")
    [ "${MODE}" = "exec" ] && garbage_collect_store "${store}"
  done < <(list_datastores)

  if [ "${MODE}" = "exec" ]; then
    dels=$(awk -F'\t' 'NR>1 && $5=="OK"{c++} END{print c+0}' "${DELETED}")
    fails=$(awk -F'\t' 'NR>1 && $5=="FAILED"{c++} END{print c+0}' "${DELETED}")
    log "Fim | report=$( [ -s "${REPORT}" ] && echo 1 || echo 0 ) | plan=$( [ "${MODE}" = "plan" ] && echo 1 || echo 0 ) | modo=${MODE} | dir=${RUN_DIR}"
    log "Totais do dia: itens (del+fail): $(( (dels+0)+(fails+0) )) | falhas: ${fails}"
  else
    log "Fim | report=$( [ -s "${REPORT}" ] && echo 1 || echo 0 ) | plan=1 | modo=${MODE} | dir=${RUN_DIR}"
  fi
}
main "$@"

Permissão de execução:

chmod +x /root/pbs-prune-older-than-30d.sh

3) Wrapper com guarda (pré-prune / pós-forget-lock)

Arquivo: /root/pbs-post-exec-checks.sh

O que faz:

  • Checa janela segura (evita concorrer com Verify).
  • Permite pular a guarda com FORCE_SKIP_GUARD=1 (testes).
  • Chama o script principal em modo exec.

Crie o arquivo e cole o conteúdo:

nano /root/pbs-post-exec-checks.sh
#!/usr/bin/env bash
set -euo pipefail

DAY_UTC="$(date -u +%F)"
RUN_DIR="/var/log/pbs-prune/${DAY_UTC}"
DELETED="${RUN_DIR}/deleted.tsv"

log(){ printf '[%s] %s\n' "$(date -u +%FT%TZ)" "$*"; }

guard_window() {
  if proxmox-backup-manager task list --all | grep -q "verificationjob"; then
    echo "warn"
  else
    echo "ok"
  fi
}

log "Início do wrapper (pré-prune + pós-forget-lock)"

if [ "${FORCE_SKIP_GUARD:-0}" != "1" ]; then
  status="$(guard_window)"
  if [ "${status}" != "ok" ]; then
    log "[WARN] Guard: ainda há tarefas listadas; abortando para não competir."
    exit 0
  fi
else
  log "Guard pulado por FORCE_SKIP_GUARD=1"
fi

log "Executando prune: /root/pbs-prune-older-than-30d.sh exec"
/root/pbs-prune-older-than-30d.sh exec || true

if [ -f "${DELETED}" ]; then
  log "Analisando falhas de forget por lock (rc=255) em ${DELETED}"
fi

log "Fim do wrapper."

Permissão:

chmod +x /root/pbs-post-exec-checks.sh

4) Agendar no cron (04:30, horário do sistema)

Arquivo: /etc/cron.d/pbs-pre-prune

cat >/etc/cron.d/pbs-pre-prune <<'EOF'
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# roda às 04:30 (horário do sistema)
30 4 * * * root /root/pbs-post-exec-checks.sh >> /var/log/pbs-prune/pre-prune.log 2>&1
EOF

chmod 0644 /etc/cron.d/pbs-pre-prune

5) Testes manuais rápidos

Planejar (não exclui):

D="$(date -u +%F)"
[ -d "/var/log/pbs-prune/$D" ] && mv "/var/log/pbs-prune/$D" "/var/log/pbs-prune/${D}.bak.$(date -u +%H%M%S)"
mkdir -p "/var/log/pbs-prune/$D"

/root/pbs-prune-older-than-30d.sh plan

Contar candidatos (>30d) de hoje:

R="/var/log/pbs-prune/$D/delete-report.tsv"
[ -f "$R" ] && awk -F'\t' 'NR>1{tot++} END{print tot+0}' "$R" || echo 0

Ver os 100 primeiros:

[ -f "$R" ] && column -t -s$'\t' "$R" | sed -n '1,101p'

Executar agora (pulando a guarda) e acompanhar:

FORCE_SKIP_GUARD=1 /root/pbs-post-exec-checks.sh
tail -f /var/log/pbs-prune/pre-prune.log

6) Auditoria e validação

Deletados hoje (totais):

D="$(date -u +%F)"; DEL="/var/log/pbs-prune/$D/deleted.tsv"
awk -F'\t' 'NR>1{tot++; if($5=="FAILED") fail++} END{printf "Deletados: %d | Falhas: %d\n", tot+0, fail+0}' "$DEL"

Por datastore/namespace:

awk -F'\t' 'NR>1{k=$1" | "($2?$2:"root"); c[k]++} END{for(k in c) printf "%5d  %s\n", c[k], k | "sort -nr"}' "$DEL"

Linhas legíveis:

column -t -s$'\t' "$DEL" | head -50

Rodar GC manual (opcional, por datastore):

proxmox-backup-manager datastore garbage-collect NOME_DO_DATASTORE

7) Observações

  • plan não altera nada; exec remove e roda GC.
  • Evite concorrência com Verify ou jobs pesados.
  • Logs do dia: /var/log/pbs-prune/YYYY-MM-DD/
    delete-report.tsv (candidatos) • deleted.tsv (resultado)
  • Valide espaço liberado no Dashboard do PBS e/ou filesystem.

8) TL;DR

mkdir -p /var/log/pbs-prune
nano /root/pbs-prune-older-than-30d.sh && chmod +x /root/pbs-prune-older-than-30d.sh
nano /root/pbs-post-exec-checks.sh   && chmod +x /root/pbs-post-exec-checks.sh
cat >/etc/cron.d/pbs-pre-prune <<'EOF'
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
30 4 * * * root /root/pbs-post-exec-checks.sh >> /var/log/pbs-prune/pre-prune.log 2>&1
EOF
chmod 0644 /etc/cron.d/pbs-pre-prune

Resultado esperado: na primeira execução completa, snapshots antigos (>30d) são removidos e o garbage-collect consolida o espaço. No meu caso inicial, a economia foi de ≈640 GB.

Você pode gostar...

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *