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 emdelete-report.tsv. - Em
exec: remove cada snapshot listado, registra emdeleted.tsve rodagarbage-collectao 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
plannão altera nada;execremove 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.