Ferramentas do usuário

Ferramentas do site


proxmox:prune-individual

Essa é uma revisão anterior do documento!


PBS Prune Individual

Visão geral

O PBS Prune Individual é um assistente em Bash para configurar retenção de backups por VM/CT no Proxmox Backup Server.

O PBS trabalha com prune por datastore/namespace. Este assistente permite selecionar grupos específicos, definir o keep-last, realizar uma simulação antes da execução e instalar o processo para execução recorrente.

O que o PBS Prune faz

Coleta e valida os dados de conexão com o PBS.

Pode criar automaticamente o API Token e aplicar a permissão necessária quando executado no próprio PBS, utilizando um usuário existente.

Lista os grupos de backup disponíveis.

Permite configurar retenção individual para VM/CT.

Executa dry-run antes do prune real.

Instala o executor, configuração, cron e logrotate.

Mantém logs das execuções.

Utiliza lock para evitar duas execuções simultâneas.

Fluxo de funcionamento

Pré-requisitos
      |
      v
Conexão com PBS
      |
      v
VMs / CTs + keep-last
      |
      v
Dry-run
      |
      v
Agendamento
      |
      v
Revisão
      |
      v
Instalação
      |
      v
Prune automático

Pré-requisitos

Antes de executar o assistente, é necessário preparar o acesso ao Proxmox Backup Server.

O ambiente deve possuir:

acesso ao Proxmox Backup Server;

um usuário existente no PBS que será utilizado como proprietário do API Token;

um API Token criado nesse usuário;

permissão do API Token no datastore que será utilizado pelo prune;

proxmox-backup-client

instalado;

openssl

instalado;

timeout

disponível;

flock

disponível;

cron

instalado e funcionando.

Usuário da API

O API Token sempre pertence a um usuário existente no Proxmox Backup Server.

Antes de configurar o PBS Prune Individual, defina qual usuário será utilizado para criar o API Token.

Exemplos:

root@pam

ou:

backup@pbs

O usuário pode ser um usuário administrativo existente, como

root@pam

, ou um usuário dedicado ao serviço, como

backup@pbs

.

Importante: primeiro deve existir o usuário. O API Token será criado dentro desse usuário.

API Token

Depois de definir o usuário, crie um API Token vinculado a ele.

O formato do Auth-ID é:

USUARIO@REALM!NOME_DO_TOKEN

Exemplo utilizando

root@pam

:

root@pam!prune

Exemplo utilizando um usuário dedicado:

backup@pbs!prune

Nesse exemplo:

backup@pbs

é o usuário existente;

prune

é o nome do API Token.

O secret apresentado pelo PBS durante a criação do token será utilizado pelo script para autenticação.

Nunca publique o secret do API Token em documentação, scripts públicos, prints ou repositórios.

Permissão no Datastore

Depois de criar o API Token, identifique o datastore onde estão os backups que serão processados.

Por exemplo:

backup

O caminho da ACL será:

/datastore/backup

Para o PBS Prune Individual, o token deve possuir a permissão necessária no datastore.

Exemplo:

proxmox-backup-manager acl update /datastore/backup DatastoreAdmin --auth-id 'backup@pbs!prune'

Nesse exemplo:

backup@pbs

→ usuário existente;

prune

→ API Token;

/datastore/backup

→ datastore;

DatastoreAdmin

→ permissão atribuída ao token.

Importante: a permissão deve ser aplicada ao datastore onde estão os backups que serão processados pelo script.

Resumo do pré-requisito

O fluxo de autenticação é:

Usuário existente no PBS
        |
        v
API Token criado nesse usuário
        |
        v
Permissão no datastore
        |
        v
PBS Prune Individual

Requisitos técnicos

O script realiza uma pré-checagem antes da configuração.

São verificados:

execução como root;

proxmox-backup-client;

openssl;

timeout;

flock;

cron.

Quando executado diretamente no próprio PBS, o script também detecta a presença do

proxmox-backup-manager

e pode oferecer a criação/ajuste do token e da permissão automaticamente.

Como usar

Executar o assistente

./pbs-prune-setup.sh

O assistente conduz a configuração em etapas:

conexão com o Proxmox Backup Server;

seleção das VMs/CTs;

definição do keep-last;

dry-run;

agendamento;

revisão;

instalação.

Ver ajuda

./pbs-prune-setup.sh --help

Desinstalar

./pbs-prune-setup.sh --uninstall

A desinstalação remove o executor, configuração, agendamento e logrotate locais.

O token existente no PBS não é removido automaticamente.

Configuração de retenção

O arquivo:

/etc/pbs-prune/groups.conf

possui uma linha para cada grupo configurado.

Exemplo:

vm/100 7
ct/200 14

Neste exemplo:

vm/100

mantém os últimos 7 backups.

ct/200

mantém os últimos 14 backups.

Também é possível informar somente o ID durante o assistente:

100

O script identifica se o grupo é VM ou CT. Caso existam os dois com o mesmo ID, o usuário escolhe qual deseja configurar.

Dry-run

Antes da execução real, o assistente pode executar uma simulação.

O objetivo é permitir a conferência da retenção antes de remover snapshots.

Depois da instalação:

pbs-prune --dry-run

Execução

Para executar o prune configurado:

pbs-prune

Para visualizar os grupos configurados:

pbs-prune --list

Para acompanhar o log:

tail -f /var/log/pbs-prune.log

Agendamento

O assistente permite:

diário;

semanal;

a cada N horas;

expressão cron personalizada;

execução somente manual.

A recomendação incorporada ao assistente é executar o prune:

depois da janela de backup;

antes do Garbage Collection do datastore.

Arquivos instalados

Arquivo Função
/usr/local/sbin/pbs-prune
Executor
/etc/pbs-prune/pbs-prune.conf
Configuração de conexão e agendamento
/etc/pbs-prune/groups.conf
Grupos e retenção
/etc/pbs-prune/secret
Secret do token
/etc/cron.d/pbs-prune
Agendamento
/etc/logrotate.d/pbs-prune
Rotação do log
/var/log/pbs-prune.log
Log de execução

Estrutura lógica

pbs-prune-setup.sh
        |
        +-- coleta e validação
        |
        +-- conexão PBS
        |
        +-- configuração dos grupos
        |
        +-- dry-run
        |
        +-- agendamento
        |
        +-- instalação
                |
                +-- pbs-prune
                +-- pbs-prune.conf
                +-- groups.conf
                +-- secret
                +-- cron
                +-- logrotate
                +-- log

Segurança

O secret do token é armazenado em arquivo separado do código.

O arquivo é criado com permissão restrita.

O executor também utiliza:

flock

para impedir execuções concorrentes;

timeout

para limitar a execução do cliente PBS;

log das execuções;

fingerprint SHA-256 do certificado do PBS.

Nunca publique tokens, secrets ou credenciais reais no código-fonte.

Garbage Collection

O prune define quais snapshots deixam de ser mantidos.

O espaço do datastore é efetivamente liberado quando o Garbage Collection do datastore for executado.

Licença

O script informa em seu cabeçalho:

Licença : MIT

Script completo

O conteúdo abaixo é o script completo

pbs-prune-setup.sh

.

#!/usr/bin/env bash
# ==============================================================================
#  pbs-prune-setup — Assistente de prune individual por VM/CT no Proxmox Backup Server
# ------------------------------------------------------------------------------
#  Autor   : Caio Monteiro
#  Site    : www.projetoempresalivre.com.br
#  Versão  : 1.0.0 (25/09/2026)
#  Licença : MIT
#
#  O PBS só faz prune por datastore/namespace. Este assistente:
#    1. coleta e valida os dados de conexão (host, datastore, token, fingerprint)
#    2. cria o token de API e a permissão automaticamente (quando roda no próprio PBS)
#    3. lista os grupos existentes e deixa você escolher VM/CT + quantos manter (keep-last)
#    4. simula (dry-run) antes de aplicar
#    5. instala o executor, a config, o agendamento (cron) e o logrotate
#
#  Uso:
#    pbs-prune-setup.sh              assistente interativo
#    pbs-prune-setup.sh --uninstall  remove tudo que foi instalado
#    pbs-prune-setup.sh --help
#
#  Arquivos instalados:
#    /usr/local/sbin/pbs-prune          executor (roda pelo cron ou manualmente)
#    /etc/pbs-prune/pbs-prune.conf      conexão e agendamento
#    /etc/pbs-prune/groups.conf         lista "grupo keep-last"  (ex.: vm/100 7)
#    /etc/pbs-prune/secret              secret do token (600)
#    /etc/cron.d/pbs-prune              agendamento
#    /etc/logrotate.d/pbs-prune         rotação do log
#    /var/log/pbs-prune.log             log de execução
# ==============================================================================
 
set -uo pipefail
 
readonly VERSION="1.0.0"
readonly APP="pbs-prune"
readonly PREFIX="${PBS_PRUNE_PREFIX:-}"        # só para testes; em produção fica vazio
readonly CONF_DIR="${PREFIX}/etc/${APP}"
readonly CONF_FILE="${CONF_DIR}/${APP}.conf"
readonly GROUPS_FILE="${CONF_DIR}/groups.conf"
readonly SECRET_FILE="${CONF_DIR}/secret"
readonly RUNNER="${PREFIX}/usr/local/sbin/${APP}"
readonly CRON_FILE="${PREFIX}/etc/cron.d/${APP}"
readonly LOGROTATE_FILE="${PREFIX}/etc/logrotate.d/${APP}"
readonly LOG_FILE="${PREFIX}/var/log/${APP}.log"
readonly LOCK_FILE="${PREFIX}/run/${APP}.lock"
 
------------------------------------------------------------------ estado
 
PBS_HOST="" PBS_PORT="8007" DATASTORE="" NAMESPACE="" AUTH_ID="root@pam!prune"
SECRET="" FINGERPRINT="" CRON_EXPR="" CRON_DESC=""
G_NAMES=() G_KEEP=() AVAILABLE=()
IS_LOCAL_PBS=0
 
------------------------------------------------------------------ interface
 
if [[ -t 1 ]]; then
C_0=$'\e[0m' C_B=$'\e[1m' C_DIM=$'\e[2m' C_R=$'\e[31m' C_G=$'\e[32m' C_Y=$'\e[33m' C_C=$'\e[36m'
else
C_0="" C_B="" C_DIM="" C_R="" C_G="" C_Y="" C_C=""
fi
 
hr()   { printf '%s%s%s\n' "$C_DIM" "────────────────────────────────────────────────────────────" "$C_0"; }
info() { printf '  %s➜%s %s\n' "$C_C" "$C_0" "$"; }
ok()   { printf '  %s✔%s %s\n' "$C_G" "$C_0" "$"; }
warn() { printf '  %s⚠%s %s\n' "$C_Y" "$C_0" "$" >&2; }
fail() { printf '  %s✖%s %s\n' "$C_R" "$C_0" "$" >&2; }
die()  { fail "$"; exit 1; }
tip()  { printf '  %s%s%s\n' "$C_DIM" "$" "$C_0"; }
step() { printf '\n%s%s▌ %s%s\n' "$C_B" "$C_C" "$1" "$C_0"; hr; }
 
banner() {
printf '%s%s' "$C_B" "$C_C"
cat <<'EOF'
 
╔════════════════════════════════════════════════════════╗
║   PBS Prune Individual — Assistente de configuração    ║
║   Retenção por VM/CT no Proxmox Backup Server          ║
╚════════════════════════════════════════════════════════╝
EOF
printf '%s  %sv%s · Projeto Empresa Livre%s\n' "$C_0" "$C_DIM" "$VERSION" "$C_0"
}
 
on_interrupt() { stty echo 2>/dev/null; printf '\n'; warn "Cancelado pelo usuário. Nenhuma alteração pendente foi aplicada."; exit 130; }
trap on_interrupt INT
 
ask VAR "pergunta" "padrão" [validador]
 
ask() {
local __var=$1 __prompt=$2 __def=${3-} __val=${4:-v_nonempty} __ans
while :; do
read -rp "  ${__prompt}${__def:+ ${C_DIM}[${__def}]${C_0}}: " __ans || die "Entrada encerrada."
__ans=${__ans:-$__def}
if "$__val" "$__ans"; then printf -v "$__var" '%s' "$__ans"; return 0; fi
done
}
 
ask_secret() {
local __var=$1 __prompt=$2 __ans
while :; do
read -rsp "  ${__prompt}: " __ans || die "Entrada encerrada."; printf '\n'
[[ -n $__ans ]] && { printf -v "$__var" '%s' "$__ans"; return 0; }
warn "Campo obrigatório."
done
}
 
confirm "pergunta" [S|N]  -> 0 = sim
 
confirm() {
local def=${2:-N} ans hint
[[ $def == S ]] && hint="S/n" || hint="s/N"
read -rp "  $1 ${C_DIM}[${hint}]${C_0}: " ans || die "Entrada encerrada."
ans=${ans:-$def}
[[ ${ans,,} =~ ^(s|sim|y|yes)$ ]]
}
 
choose VAR "título" "opção 1" "opção 2" ...   -> VAR recebe o número escolhido
 
choose() {
local __var=$1 __title=$2; shift 2
local __i=1 __opt __ans
printf '  %s%s%s\n' "$C_B" "$__title" "$C_0"
for __opt in "$@"; do printf '   %s%2d)%s %s\n' "$C_C" "$__i" "$C_0" "$__opt"; __i=$((__i+1)); done
while :; do
read -rp "  Opção: " __ans || die "Entrada encerrada."
if [[ $__ans =~ ^[0-9]+$ ]] && (( __ans >= 1 && __ans <= $# )); then printf -v "$__var" '%s' "$__ans"; return 0; fi
warn "Escolha um número entre 1 e $#."
done
}
 
------------------------------------------------------------------ validadores
 
v_nonempty() { [[ -n $1 ]] || { warn "Campo obrigatório."; return 1; }; }
v_any()      { return 0; }
v_host()     { [[ $1 =~ ^[A-Za-z0-9.-]+$ ]] || { warn "Host/IP inválido (use nome DNS ou IPv4)."; return 1; }; }
v_port()     { [[ $1 =~ ^[0-9]+$ ]] && (( $1 >= 1 && $1 <= 65535 )) || { warn "Porta inválida (1-65535)."; return 1; }; }
v_name()     { [[ $1 =~ ^[A-Za-z0-9.-]+$ ]] || { warn "Use apenas letras, números, ponto, hífen e sublinhado."; return 1; }; }
v_ns()       { [[ -z $1 || $1 =~ ^[A-Za-z0-9./-]+$ ]] || { warn "Namespace inválido (ex.: producao ou clientes/acme)."; return 1; }; }
v_posint()   { [[ $1 =~ ^[1-9][0-9]*$ ]] || { warn "Informe um inteiro maior ou igual a 1."; return 1; }; }
v_time()     { [[ $1 =~ ^([01]?[0-9]|2[0-3]):[0-5][0-9]$ ]] || { warn "Horário no formato HH (ex.: 03:30)."; return 1; }; }
v_hours()    { [[ $1 =~ ^([1-9]|1[0-9]|2[0-3])$ ]] || { warn "Informe de 1 a 23 horas."; return 1; }; }
v_authid() {
local re='^[A-Za-z0-9.-]+@[A-Za-z0-9.-]+![A-Za-z0-9.-]+$'
[[ $1 =~ $re ]] || { warn "Formato: usuario@realm!token  (ex.: root@pam!prune)"; return 1; }
}
v_fp() {
local re='^([0-9A-Fa-f]{2}:){31}[0-9A-Fa-f]{2}$'
[[ $1 =~ $re ]] || { warn "Fingerprint SHA-256 inválido (32 pares hex separados por ':')."; return 1; }
}
v_cron() {
local -a f; read -ra f <<<"$1"
(( ${#f[@]} == 5 )) && [[ $1 =~ ^[0-9A-Za-z*/,\ -]+$ ]] 
|| { warn "Expressão cron precisa de 5 campos (min hora dia mês dia-semana)."; return 1; }
}
 
------------------------------------------------------------------ helpers PBS
 
repo() { printf '%s@%s:%s:%s' "$AUTH_ID" "$PBS_HOST" "$PBS_PORT" "$DATASTORE"; }
ns_args() { [[ -n $NAMESPACE ]] && printf -- '--ns\n%s\n' "$NAMESPACE"; }
 
pbc() {
local -a ns; mapfile -t ns < <(ns_args)
PBS_REPOSITORY="$(repo)" PBS_PASSWORD="$SECRET" PBS_FINGERPRINT="$FINGERPRINT" 
timeout 60 proxmox-backup-client "$@" "${ns[@]}" </dev/null
}
 
fetch_fingerprint() {
timeout 8 openssl s_client -connect "${PBS_HOST}:${PBS_PORT}" -servername "$PBS_HOST" </dev/null 2>/dev/null 
| openssl x509 -noout -fingerprint -sha256 2>/dev/null | cut -d= -f2
}
 
is_local_host() {
[[ $1 == 127.0.0.1 || $1 == localhost || $1 == "$(hostname)" || $1 == "$(hostname -f 2>/dev/null)" ]] && return 0
hostname -I 2>/dev/null | tr ' ' '\n' | grep -qxF "$1"
}
 
------------------------------------------------------------------ pré-checagem
 
preflight() {
step "Verificando pré-requisitos"
[[ $EUID -eq 0 ]] || die "Execute como root."
local c missing=0
for c in proxmox-backup-client openssl timeout flock; do
if command -v "$c" >/dev/null 2>&1; then ok "$c"; else fail "$c não encontrado"; missing=1; fi
done
(( missing )) && die "Instale os pacotes faltando (proxmox-backup-client vem no PVE/PBS; em Debian puro use o repositório do PBS)."
[[ -d ${PREFIX}/etc/cron.d ]] || die "Diretório /etc/cron.d não existe — o cron está instalado?"
ok "cron"
command -v proxmox-backup-manager >/dev/null 2>&1 && { IS_LOCAL_PBS=1; ok "Rodando no próprio PBS — criação automática de token disponível"; }
return 0
}
 
------------------------------------------------------------------ 1. conexão
 
step_connection() {
step "1/5 · Conexão com o Proxmox Backup Server"
local def_host=""; (( IS_LOCAL_PBS )) && def_host="127.0.0.1"
ask PBS_HOST "IP ou hostname do PBS" "${PBS_HOST:-$def_host}" v_host
ask PBS_PORT "Porta da API" "$PBS_PORT" v_port
 
if (( IS_LOCAL_PBS )) && is_local_host "$PBS_HOST"; then
info "Datastores disponíveis:"
proxmox-backup-manager datastore list 2>/dev/null | sed 's/^/     /'
fi
ask DATASTORE "Nome do datastore" "$DATASTORE" v_name
ask NAMESPACE "Namespace (Enter = raiz)" "$NAMESPACE" v_ns
 
tip "Recomendado: token dedicado, ex.: root@pam!prune"
ask AUTH_ID "Auth-ID do token (usuario@realm!token)" "$AUTH_ID" v_authid
 
setup_token
setup_fingerprint
test_connection || step_connection
}
 
setup_token() {
local user=${AUTH_ID%%!} tok=${AUTH_ID#!} opt out acl_path
 
if (( IS_LOCAL_PBS )) && is_local_host "$PBS_HOST" && confirm "Criar/ajustar o token e a permissão automaticamente?" S; then
if proxmox-backup-manager user list-tokens "$user" --output-format json 2>/dev/null | grep -qF ""${AUTH_ID}""; then
warn "O token ${AUTH_ID} já existe."
choose opt "O que deseja fazer?" "Usar o token existente (vou digitar o secret)" "Regenerar o token (o secret atual deixa de funcionar)"
if (( opt == 2 )); then
proxmox-backup-manager user delete-token "$user" "$tok" >/dev/null 2>&1 || die "Falha ao remover o token antigo."
else
ask_secret SECRET "Secret do token"
fi
fi
if [[ -z $SECRET ]]; then
out=$(proxmox-backup-manager user generate-token "$user" "$tok" --output-format json 2>&1) 
|| die "Falha ao gerar o token: $out"
SECRET=$(sed -n 's/."value"[[:space:]]:[[:space:]]"([^"])".*/\1/p' <<<"$out")
[[ -n $SECRET ]] || die "Não consegui ler o secret gerado: $out"
ok "Token ${AUTH_ID} criado"
fi
acl_path="/datastore/${DATASTORE}${NAMESPACE:+/$NAMESPACE}"
if proxmox-backup-manager acl update "$acl_path" DatastoreAdmin --auth-id "$AUTH_ID" >/dev/null 2>&1; then
ok "Permissão DatastoreAdmin em ${acl_path}"
else
warn "Não consegui aplicar a ACL. Aplique manualmente:"
tip "proxmox-backup-manager acl update ${acl_path} DatastoreAdmin --auth-id '${AUTH_ID}'"
fi
else
if [[ -n $SECRET ]] && confirm "Manter o secret já configurado?" S; then return 0; fi
tip "O token precisa da role DatastoreAdmin em /datastore/${DATASTORE} (prune de grupos de outros donos)."
ask_secret SECRET "Secret do token"
fi
}
 
setup_fingerprint() {
local fp
info "Obtendo o fingerprint do certificado de ${PBS_HOST}:${PBS_PORT}..."
fp=$(fetch_fingerprint || true)
if [[ -n $fp ]]; then
printf '     %s%s%s\n' "$C_B" "$fp" "$C_0"
tip "Confira em: PBS → Painel → Mostrar Fingerprint (ou: proxmox-backup-manager cert info)"
if confirm "O fingerprint confere?" S; then FINGERPRINT=$fp; return 0; fi
else
warn "Não foi possível ler o certificado automaticamente."
fi
ask FINGERPRINT "Cole o fingerprint SHA-256" "$FINGERPRINT" v_fp
}
 
test_connection() {
local out
info "Testando conexão em $(repo)${NAMESPACE:+ (ns: $NAMESPACE)}..."
if out=$(pbc list --output-format text 2>&1); then
mapfile -t AVAILABLE < <(grep -oE '\b(vm|ct|host)/[A-Za-z0-9._-]+' <<<"$out" | sort -u -V)
ok "Conectado — ${#AVAILABLE[@]} grupo(s) de backup encontrados"
return 0
fi
fail "Falha na conexão:"
sed 's/^/     /' <<<"$out" | tail -n 5 >&2
confirm "Revisar os dados de conexão?" S && return 1
confirm "Continuar mesmo assim?" N || die "Abortado."
AVAILABLE=()
return 0
}
 
------------------------------------------------------------------ 2. grupos
 
print_groups() {
local i
if (( ${#G_NAMES[@]} == 0 )); then tip "(nenhum grupo configurado)"; return; fi
printf '  %s%-4s %-18s %s%s\n' "$C_B" "#" "GRUPO" "MANTER (keep-last)" "$C_0"
for i in "${!G_NAMES[@]}"; do
printf '  %-4s %-18s %s\n' "$((i+1))" "${G_NAMES[$i]}" "${G_KEEP[$i]}"
done
}
 
group_index() { local i; for i in "${!G_NAMES[@]}"; do [[ ${G_NAMES[$i]} == "$1" ]] && { echo "$i"; return 0; }; done; return 1; }
in_available() { printf '%s\n' "${AVAILABLE[@]}" | grep -qxF "$1"; }
 
resolve_group() {   # entrada: "100", "vm/100", "ct/200"  -> saída: grupo
local input=$1 opt
if [[ $input =~ ^(vm|ct|host)/[A-Za-z0-9._-]+$ ]]; then echo "$input"; return 0; fi
[[ $input =~ ^[0-9]+$ ]] || { warn "Informe o ID (ex.: 100) ou o grupo (ex.: ct/200)."; return 1; }
local has_vm=0 has_ct=0
in_available "vm/$input" && has_vm=1
in_available "ct/$input" && has_ct=1
if (( has_vm && has_ct )); then
choose opt "Existem vm/$input e ct/$input. Qual?" "vm/$input (QEMU)" "ct/$input (LXC)" >&2
(( opt == 1 )) && echo "vm/$input" || echo "ct/$input"
elif (( has_vm )); then echo "vm/$input"
elif (( has_ct )); then echo "ct/$input"
else
warn "ID $input não encontrado no datastore${NAMESPACE:+ / ns $NAMESPACE}."
choose opt "Adicionar mesmo assim como:" "vm/$input (QEMU)" "ct/$input (LXC)" "Cancelar" >&2
case $opt in 1) echo "vm/$input";; 2) echo "ct/$input";; *) return 1;; esac
fi
}
 
step_groups() {
step "2/5 · VMs/CTs e retenção"
if (( ${#AVAILABLE[@]} )); then
info "Grupos encontrados no datastore:"
if command -v column >/dev/null 2>&1; then
printf '%s\n' "${AVAILABLE[@]}" | column -c 70 | sed 's/^/     /'
else
printf '     %s\n' "${AVAILABLE[@]}"
fi
fi
tip "Digite o ID (100) ou o grupo (ct/200). Enter vazio finaliza."
 
local input group keep idx opt n
while :; do
printf '\n'
read -rp "  ID da VM/CT: " input || die "Entrada encerrada."
input=${input// /}
if [[ -z $input ]]; then
if (( ${#G_NAMES[@]} == 0 )); then warn "Adicione pelo menos um grupo."; continue; fi
printf '\n'; print_groups; printf '\n'
choose opt "Lista concluída?" "Sim, continuar" "Adicionar mais" "Remover um item" "Limpar a lista"
case $opt in
1) break ;;
2) continue ;;
3) ask n "Número do item a remover" "" v_posint
if (( n <= ${#G_NAMES[@]} )); then
ok "Removido ${G_NAMES[$((n-1))]}"
unset 'G_NAMES[n-1]' 'G_KEEP[n-1]'; G_NAMES=("${G_NAMES[@]}"); G_KEEP=("${G_KEEP[@]}")
else warn "Item inexistente."; fi ;;
4) G_NAMES=(); G_KEEP=(); ok "Lista limpa." ;;
esac
continue
fi
 
group=$(resolve_group "$input") || continue
if idx=$(group_index "$group"); then
  warn "$group já está na lista (keep-last ${G_KEEP[$idx]})."
  confirm "Alterar a retenção?" S || continue
  ask keep "Quantos backups manter de $group" "${G_KEEP[$idx]}" v_posint
  G_KEEP[$idx]=$keep
else
  ask keep "Quantos backups manter de $group" "7" v_posint
  G_NAMES+=("$group"); G_KEEP+=("$keep")
fi
ok "$group → manter os últimos $keep"
 
done
}
 
------------------------------------------------------------------ 3. simulação
 
step_dryrun() {
step "3/5 · Simulação (dry-run)"
confirm "Simular agora o que seria mantido/removido?" S || { tip "Simulação pulada."; return 0; }
local i out kept removed
printf '  %s%-18s %-10s %-9s %s%s\n' "$C_B" "GRUPO" "KEEP-LAST" "MANTÉM" "REMOVE" "$C_0"
for i in "${!G_NAMES[@]}"; do
if out=$(pbc prune "${G_NAMES[$i]}" --keep-last "${G_KEEP[$i]}" --dry-run --output-format json 2>&1); then
kept=$(grep -oE '"keep":[[:space:]]*true' <<<"$out" | wc -l)
removed=$(grep -oE '"keep":[[:space:]]*false' <<<"$out" | wc -l)
printf '  %-18s %-10s %-9s %s%s%s\n' "${G_NAMES[$i]}" "${G_KEEP[$i]}" "$kept" 
"$( (( removed > 0 )) && echo "$C_Y" )" "$removed" "$C_0"
else
printf '  %-18s %-10s %s%s%s\n' "${G_NAMES[$i]}" "${G_KEEP[$i]}" "$C_R" "erro: $(tail -n1 <<<"$out")" "$C_0"
fi
done
printf '\n'
confirm "Resultado OK? (N volta para editar a lista)" S || { step_groups; step_dryrun; }
}
 
------------------------------------------------------------------ 4. agendamento
 
step_schedule() {
step "4/5 · Agendamento"
tip "Agende DEPOIS da janela de backup e ANTES do Garbage Collection do datastore."
local opt t h m d dname
local -a days=("Domingo" "Segunda" "Terça" "Quarta" "Quinta" "Sexta" "Sábado")
choose opt "Frequência:" "Diário" "Semanal" "A cada N horas" "Expressão cron personalizada" "Não agendar (só executar manualmente)"
case $opt in
1) ask t "Horário (HH)" "03:30" v_time; h=$((10#${t%%:})); m=$((10#${t##:}))
CRON_EXPR="$m $h * * "; CRON_DESC="Diário às $(printf '%02d:%02d' "$h" "$m")" ;;
2) choose d "Dia da semana:" "${days[@]}"; d=$((d-1)); dname=${days[$d]}
ask t "Horário (HH)" "03:30" v_time; h=$((10#${t%%:})); m=$((10#${t##*:}))
CRON_EXPR="$m $h * * $d"; CRON_DESC="Toda ${dname} às $(printf '%02d:%02d' "$h" "$m")" ;;
3) ask h "Intervalo em horas" "6" v_hours
CRON_EXPR="15 */$h * * *"; CRON_DESC="A cada $h hora(s), no minuto 15" ;;
4) ask CRON_EXPR "Expressão (min hora dia mês dia-semana)" "${CRON_EXPR:-30 3 * * *}" v_cron
CRON_DESC="Personalizado" ;;
5) CRON_EXPR=""; CRON_DESC="Sem agendamento" ;;
esac
ok "${CRON_DESC}${CRON_EXPR:+  (${CRON_EXPR})}"
}
 
------------------------------------------------------------------ 5. instalação
 
write_runner() {
mkdir -p "$(dirname "$RUNNER")"
cat >"$RUNNER" <<'RUNNER_EOF'
#!/usr/bin/env bash
 
pbs-prune — executor de prune individual por VM/CT (gerado pelo pbs-prune-setup)
 
Uso: pbs-prune [--dry-run] [--list]
 
set -uo pipefail
 
CONF="${PBS_PRUNE_CONF:-@CONF_FILE@}"
[[ -r $CONF ]] || { echo "Configuração não encontrada: $CONF" >&2; exit 2; }
 
shellcheck source=/dev/null
 
source "$CONF"
 
DRY=()
case "${1:-}" in
-n|--dry-run) DRY=(--dry-run) ;;
-l|--list)    grep -vE '^\s*(#|$)' "$GROUPS_FILE"; exit 0 ;;
-h|--help)    echo "Uso: $(basename "$0") [--dry-run] [--list]"; exit 0 ;;
"") ;;
*) echo "Opção inválida: $1" >&2; exit 2 ;;
esac
 
exec 9>"$LOCK_FILE"
flock -n 9 || { echo "Já existe uma execução em andamento." >&2; exit 3; }
 
log() {
local line; line="$(date '+%F %T') $"
printf '%s\n' "$line" >>"$LOG_FILE"
[[ -t 1 ]] && printf '%s\n' "$line"
logger -t pbs-prune -- "$" 2>/dev/null
return 0
}
 
export PBS_REPOSITORY="${AUTH_ID}@${PBS_HOST}:${PBS_PORT}:${DATASTORE}"
export PBS_FINGERPRINT="$FINGERPRINT"
PBS_PASSWORD="$(<"$SECRET_FILE")" || { log "ERRO: não foi possível ler $SECRET_FILE"; exit 2; }
export PBS_PASSWORD
 
NS=(); [[ -n ${NAMESPACE:-} ]] && NS=(--ns "$NAMESPACE")
 
total=0 errors=0 removed_total=0 start=$SECONDS
log "===== início${DRY:+ (dry-run)} · ${PBS_REPOSITORY}${NAMESPACE:+ · ns=$NAMESPACE} ====="
while read -r group keep _; do
[[ -z ${group:-} || $group == #* ]] && continue
total=$((total + 1))
if out=$(timeout 600 proxmox-backup-client prune "$group" "${NS[@]}" --keep-last "$keep" 
"${DRY[@]}" --output-format json </dev/null 2>&1); then
removed=$(grep -oE '"keep":[[:space:]]*false' <<<"$out" | wc -l)
removed_total=$((removed_total + removed))
log "OK    ${group}  keep-last=${keep}  removidos=${removed}"
else
errors=$((errors + 1))
log "ERRO  ${group}  keep-last=${keep}"
while IFS= read -r l; do log "      $l"; done <<<"$out"
fi
done <"$GROUPS_FILE"
log "===== fim · grupos=${total} erros=${errors} snapshots removidos=${removed_total} · $((SECONDS - start))s ====="
 
(( errors == 0 ))
RUNNER_EOF
sed -i "s|@CONF_FILE@|${CONF_FILE}|" "$RUNNER"
chmod 750 "$RUNNER"
}
 
write_config() {
install -d -m 700 "$CONF_DIR"
umask 077
{
echo "# Gerado por pbs-prune-setup v${VERSION} em $(date '+%F %T'). Rode o setup novamente para alterar."
printf 'PBS_HOST=%q\nPBS_PORT=%q\nDATASTORE=%q\nNAMESPACE=%q\nAUTH_ID=%q\nFINGERPRINT=%q\n' 
"$PBS_HOST" "$PBS_PORT" "$DATASTORE" "$NAMESPACE" "$AUTH_ID" "$FINGERPRINT"
printf 'CRON_EXPR=%q\nCRON_DESC=%q\n' "$CRON_EXPR" "$CRON_DESC"
printf 'SECRET_FILE=%q\nGROUPS_FILE=%q\nLOG_FILE=%q\nLOCK_FILE=%q\n' 
"$SECRET_FILE" "$GROUPS_FILE" "$LOG_FILE" "$LOCK_FILE"
} >"$CONF_FILE"
printf '%s' "$SECRET" >"$SECRET_FILE"
chmod 600 "$CONF_FILE" "$SECRET_FILE"
umask 022
}
 
write_groups() {
local i
install -d -m 700 "$CONF_DIR"
{
echo "# grupo      keep-last   (vm/ID = QEMU · ct/ID = LXC)"
echo "# Pode editar à mão; uma linha por grupo."
for i in "${!G_NAMES[@]}"; do printf '%-12s %s\n' "${G_NAMES[$i]}" "${G_KEEP[$i]}"; done
} >"$GROUPS_FILE"
chmod 600 "$GROUPS_FILE"
}
 
write_cron() {
if [[ -z $CRON_EXPR ]]; then rm -f "$CRON_FILE"; return 0; fi
mkdir -p "$(dirname "$CRON_FILE")" "$(dirname "$LOG_FILE")" "$(dirname "$LOCK_FILE")"
cat >"$CRON_FILE" <<EOF
 
pbs-prune — ${CRON_DESC} (gerado por pbs-prune-setup)
 
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
${CRON_EXPR} root ${RUNNER} >/dev/null 2>&1
EOF
chmod 644 "$CRON_FILE"
}
 
write_logrotate() {
mkdir -p "$(dirname "$LOGROTATE_FILE")"
cat >"$LOGROTATE_FILE" <<EOF
${LOG_FILE} {
monthly
rotate 12
compress
missingok
notifempty
}
EOF
}
 
step_review_install() {
step "5/5 · Revisão e instalação"
printf '  %-14s %s\n' "Repositório:" "$(repo)" "Namespace:" "${NAMESPACE:-(raiz)}" 
"Fingerprint:" "${FINGERPRINT:0:23}…" "Agendamento:" "${CRON_DESC}${CRON_EXPR:+ (${CRON_EXPR})}"
printf '\n'; print_groups; printf '\n'
confirm "Instalar com essas configurações?" S || die "Instalação cancelada. Nada foi alterado."
 
write_config;    ok "Configuração     ${CONF_FILE}"
write_groups;    ok "Lista de grupos  ${GROUPS_FILE}"
ok "Secret           ${SECRET_FILE} (600)"
write_runner;    ok "Executor         ${RUNNER}"
write_cron;      [[ -n $CRON_EXPR ]] && ok "Agendamento      ${CRON_FILE}" || tip "Sem agendamento instalado."
write_logrotate; ok "Logrotate        ${LOGROTATE_FILE}"
}
 
finish() {
printf '\n'
if confirm "Executar o prune agora (valendo)?" N; then
"$RUNNER" && ok "Execução concluída." || warn "Execução terminou com erros — veja o log."
fi
step "Pronto"
printf '  %-34s %s\n' 
"pbs-prune --dry-run" "simular" 
"pbs-prune" "executar agora" 
"pbs-prune --list" "ver grupos configurados" 
"tail -f ${LOG_FILE}" "acompanhar o log" 
"$(basename "$0")" "editar lista / agendamento / desinstalar"
tip "Lembrete: o espaço só é liberado quando o Garbage Collection do datastore rodar."
printf '\n'
}
 
------------------------------------------------------------------ carregar / desinstalar
 
load_existing() {
 
shellcheck source=/dev/null
 
source "$CONF_FILE"
[[ -r $SECRET_FILE ]] && SECRET=$(<"$SECRET_FILE")
G_NAMES=() G_KEEP=()
local g k
while read -r g k _; do
[[ -z ${g:-} || $g == #* ]] && continue
G_NAMES+=("$g"); G_KEEP+=("$k")
done <"$GROUPS_FILE"
}
 
uninstall() {
step "Desinstalação"
[[ $EUID -eq 0 ]] || die "Execute como root."
confirm "Remover executor, configuração, agendamento e logrotate?" N || die "Cancelado."
rm -f "$RUNNER" "$CRON_FILE" "$LOGROTATE_FILE" && ok "Executor, cron e logrotate removidos"
rm -rf "$CONF_DIR" && ok "Configuração e secret removidos"
tip "O log ${LOG_FILE} foi mantido. O token no PBS não foi removido:"
tip "proxmox-backup-manager user delete-token usuario@realm <token>"
}
 
existing_menu() {
local opt
load_existing
step "Instalação existente encontrada"
printf '  %-14s %s\n' "Repositório:" "$(repo)" "Agendamento:" "${CRON_DESC:-—}${CRON_EXPR:+ (${CRON_EXPR})}"
printf '\n'; print_groups; printf '\n'
choose opt "O que deseja fazer?" 
"Editar a lista de VMs/CTs" "Alterar o agendamento" "Reconfigurar tudo" 
"Simular agora (dry-run)" "Desinstalar" "Sair"
case $opt in
1) test_connection || true; step_groups; step_dryrun; write_groups; ok "Lista atualizada: ${GROUPS_FILE}" ;;
2) step_schedule; write_config; write_cron; ok "Agendamento atualizado" ;;
3) full_flow; return ;;
4) "$RUNNER" --dry-run ;;
5) uninstall; exit 0 ;;
6) exit 0 ;;
esac
finish
}
 
full_flow() {
step_connection
step_groups
step_dryrun
step_schedule
step_review_install
finish
}
 
------------------------------------------------------------------ main
 
case "${1:-}" in
-h|--help)    sed -n '2,32p' "$0" | sed 's/^# {0,1}//'; exit 0 ;;
-v|--version) echo "pbs-prune-setup $VERSION"; exit 0 ;;
--uninstall)  banner; uninstall; exit 0 ;;
"") ;;
*) die "Opção inválida: $1 (use --help)" ;;
esac
 
banner
preflight
if [[ -f $CONF_FILE && -f $GROUPS_FILE ]]; then existing_menu; else full_flow; fi

Referência

Download

O arquivo do script pode ser disponibilizado separadamente na própria página da Wiki usando a função de mídia do DokuWiki.

Para uma página de documentação, recomenda-se manter:

esta página para documentação;

o arquivo

pbs-prune-setup.sh

como download;

o código completo acima como referência.

Comandos rápidos

# Assistente
./pbs-prune-setup.sh
 
Ajuda
 
./pbs-prune-setup.sh --help
 
Dry-run
 
pbs-prune --dry-run
 
Executar
 
pbs-prune
 
Listar grupos
 
pbs-prune --list
 
Log
 
tail -f /var/log/pbs-prune.log
 
Desinstalar
 
./pbs-prune-setup.sh --uninstall
proxmox/prune-individual.1790698386.txt.gz · Última modificação: por admin

Donate Powered by PHP Valid HTML5 Valid CSS Driven by DokuWiki