Ferramentas do usuário

Ferramentas do site


proxmox:prune-individual

Essa é uma revisão anterior do documento!


<!doctype html> <html lang=“pt-BR”> <head> <meta charset=“utf-8”> <meta name=“viewport” content=“width=device-width,initial-scale=1”> <title>PBS Prune Individual · Estrutura, Uso e Script</title> <meta name=“description” content=“Documentação do PBS Prune Individual para Proxmox Backup Server: estrutura, funcionamento, uso e script completo em DokuWiki.”> <style> :root{–bg:#07090d;–panel:#0d121a;–panel2:#111923;–line:#202b39;–text:#e9eef5;–muted:#9ba8b8;–accent:#f2810f;–accent2:#2fd8bd;–danger:#ff6b6b;–code:#080b10} *{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;background:radial-gradient(circle at 15% 0%,#1a1209 0,transparent 28%),radial-gradient(circle at 90% 10%,#09201d 0,transparent 25%),var(–bg);color:var(–text);font:15px/1.65 Inter,system-ui,-apple-system,Segoe UI,sans-serif} a{color:inherit}.wrap{max-width:1120px;margin:auto;padding:0 22px} nav{position:sticky;top:0;z-index:10;background:rgba(7,9,13,.86);backdrop-filter:blur(14px);border-bottom:1px solid var(–line)} .nav{height:62px;display:flex;align-items:center;justify-content:space-between;gap:18px}.brand{font-weight:800;letter-spacing:.02em}.brand b{color:var(–accent)}.links{display:flex;gap:18px;color:var(–muted);font-size:13px}.links a{text-decoration:none}.links a:hover{color:#fff} .hero{padding:78px 0 52px}.eyebrow{color:var(–accent);font-weight:800;text-transform:uppercase;letter-spacing:.14em;font-size:12px}h1{font-size:clamp(34px,6vw,64px);line-height:1.02;margin:12px 0 18px;max-width:850px}.lead{font-size:19px;color:var(–muted);max-width:780px} .badges{display:flex;gap:9px;flex-wrap:wrap;margin-top:25px}.badge{border:1px solid var(–line);background:rgba(255,255,255,.025);padding:7px 11px;border-radius:999px;color:#cbd5e1;font-size:12px}.badge.hot{border-color:#6d3d13;color:#ffb66d} section{padding:38px 0}h2{font-size:27px;margin:0 0 10px}h3{margin:0 0 8px}.sub{color:var(–muted);max-width:760px} .grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin-top:22px}.grid2{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-top:22px} .card{background:linear-gradient(180deg,rgba(255,255,255,.035),rgba(255,255,255,.018));border:1px solid var(–line);border-radius:14px;padding:20px}.card .num{font:700 12px ui-monospace,SFMono-Regular,monospace;color:var(–accent);margin-bottom:12px}.card p{color:var(–muted);margin:7px 0 0} .flow{display:flex;gap:0;align-items:stretch;margin-top:24px;overflow:auto;padding-bottom:4px}.step{min-width:155px;padding:16px;background:var(–panel);border:1px solid var(–line)}.step:first-child{border-radius:12px 0 0 12px}.step:last-child{border-radius:0 12px 12px 0}.arrow{display:flex;align-items:center;padding:0 8px;color:var(–accent);background:var(–panel)} pre{margin:18px 0 0;background:var(–code);border:1px solid var(–line);border-radius:12px;padding:18px;overflow:auto;color:#d7e0ea;font:12.5px/1.6 “JetBrains Mono”,ui-monospace,SFMono-Regular,monospace;max-height:620px}.terminal{border-left:3px solid var(–accent)} table{width:100%;border-collapse:collapse;margin-top:18px;background:var(–panel);border:1px solid var(–line);border-radius:12px;overflow:hidden}th,td{padding:12px 14px;text-align:left;border-bottom:1px solid var(–line)}th{color:#fff;font-size:12px;text-transform:uppercase;letter-spacing:.06em}td{color:var(–muted);font-family:ui-monospace,SFMono-Regular,monospace;font-size:12px} .callout{margin-top:22px;padding:18px 20px;border:1px solid #6d3d13;background:rgba(242,129,15,.06);border-radius:12px}.callout strong{color:#ffb66d} .actions{display:flex;gap:10px;flex-wrap:wrap;margin-top:20px}.btn{display:inline-block;text-decoration:none;padding:11px 15px;border-radius:9px;border:1px solid var(–line);background:var(–panel2);font-weight:700;font-size:13px}.btn.primary{background:var(–accent);border-color:var(–accent);color:#090b0f}.btn:hover{transform:translateY(-1px)} .small{font-size:12px;color:var(–muted)}footer{border-top:1px solid var(–line);padding:32px 0 50px;color:var(–muted);font-size:12px} @media(max-width:800px){.links{display:none}.grid,.grid2{grid-template-columns:1fr}.hero{padding-top:52px}h1{font-size:42px}.flow{display:grid;gap:8px}.step,.step:first-child,.step:last-child{border-radius:10px}.arrow{display:none}} </style> </head> <body> <nav>

PBS <b>PRUNE</b> INDIVIDUAL

<a href=“#estrutura”>Estrutura</a><a href=“#uso”>Como usar</a><a href=“#arquivos”>Arquivos</a><a href=“#script”>Script</a><a href=“#wiki”>DokuWiki</a>

</nav>

<header class=“wrap hero”>

Proxmox Backup Server · documentação técnica

<h1>Prune por VM/CT, sem transformar uma tarefa simples em uma operação manual.</h1> <p class=“lead”>O PBS Prune Individual é um assistente em Bash que organiza conexão, retenção, simulação, agendamento e execução de prune por grupo de backup.</p>

v1.0.0 · 25/09/2026BashPBSMIT

</header>

<main class=“wrap”> <section> <h2>O que ele resolve</h2><p class=“sub”>A ferramenta parte de uma necessidade operacional: definir retenção diferente para VMs/CTs específicos e transformar essa configuração em uma execução repetível.</p>

01 · CONEXÃO

<h3>Valida o ambiente</h3><p>Host, porta, datastore, namespace, token e fingerprint.</p>

02 · RETENÇÃO

<h3>Escolhe o grupo</h3><p>VM/ID ou CT/ID com seu próprio keep-last.</p>

03 · SEGURANÇA

<h3>Simula antes</h3><p>Dry-run permite revisar o que será mantido ou removido.</p>

</section>

<section id=“estrutura”> <h2>Estrutura do projeto</h2><p class=“sub”>O setup é um assistente. Depois da configuração, ele deixa um executor separado para as execuções recorrentes.</p>

<b>Setup</b><br>entrada e validação

→

<b>Config</b><br>PBS + agendamento

→

<b>Groups</b><br>VM/CT + keep-last

→

<b>Runner</b><br>execução real

→

<b>Log</b><br>histórico

<h3>Setup interativo</h3><p>Executa o fluxo de pré-requisitos, conexão, grupos, dry-run, agendamento e instalação.</p>

<h3>Executor</h3><p>O

pbs-prune

lê a configuração e processa os grupos configurados, com lock, timeout e log.</p>

</section>

<section id=“uso”> <h2>Como usar</h2> <p class=“sub”>O ponto de entrada é simples. O assistente conduz a configuração e pode ser executado novamente para alterar a instalação existente.</p> <pre class=“terminal”># executar o assistente ./pbs-prune-setup.sh

# ajuda ./pbs-prune-setup.sh –help

# remover a instalação local ./pbs-prune-setup.sh –uninstall</pre>

<strong>Importante:</strong> o espaço no datastore só é efetivamente liberado quando o Garbage Collection do datastore for executado.

</section>

<section id=“arquivos”> <h2>O que fica instalado</h2> <p class=“sub”>A instalação separa código, configuração, segredo, política de retenção, agendamento e logs.</p> <table> <thead><tr><th>Arquivo</th><th>Função</th></tr></thead> <tbody> <tr><td>/usr/local/sbin/pbs-prune</td><td>Executor</td></tr> <tr><td>/etc/pbs-prune/pbs-prune.conf</td><td>Conexão e agendamento</td></tr> <tr><td>/etc/pbs-prune/groups.conf</td><td>Grupos + keep-last</td></tr> <tr><td>/etc/pbs-prune/secret</td><td>Secret do token</td></tr> <tr><td>/etc/cron.d/pbs-prune</td><td>Agendamento</td></tr> <tr><td>/etc/logrotate.d/pbs-prune</td><td>Rotação do log</td></tr> <tr><td>/var/log/pbs-prune.log</td><td>Log das execuções</td></tr> </tbody></table> </section>

<section> <h2>Exemplo de política</h2> <p class=“sub”>O arquivo de grupos permite definir retenção individual por grupo.</p> <pre>vm/100 7 ct/200 14</pre> <p class=“small”>Neste exemplo, a VM 100 mantém os últimos 7 backups e o CT 200 mantém os últimos 14.</p> </section>

<section> <h2>Comandos do executor</h2>

<h3>

pbs-prune --dry-run

</h3><p>Simula a operação sem aplicar o prune.</p>

<h3>

pbs-prune

</h3><p>Executa o prune configurado.</p>

<h3>

pbs-prune --list

</h3><p>Mostra os grupos configurados.</p>

</section>

<section> <h2>Cuidados de segurança</h2>

<h3>Secret separado</h3><p>O token fica em arquivo próprio com permissão 600. Não coloque credenciais reais no código.</p>

<h3>Execução controlada</h3><p>O runner usa

flock

para evitar concorrência e

timeout

para limitar a chamada ao cliente PBS.</p>

</section>

<section id=“script”> <h2>Script completo</h2> <p class=“sub”>Abaixo está a versão disponibilizada na documentação. O conteúdo é o arquivo Bash integral utilizado como base desta página.</p> <pre>#!/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 &quot;grupo keep-last&quot; (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=&quot;1.0.0&quot; readonly APP=&quot;pbs-prune&quot; readonly PREFIX=&quot;${PBS_PRUNE_PREFIX:-}&quot; # só para testes; em produção fica vazio readonly CONF_DIR=&quot;${PREFIX}/etc/${APP}&quot; readonly CONF_FILE=&quot;${CONF_DIR}/${APP}.conf&quot; readonly GROUPS_FILE=&quot;${CONF_DIR}/groups.conf&quot; readonly SECRET_FILE=&quot;${CONF_DIR}/secret&quot; readonly RUNNER=&quot;${PREFIX}/usr/local/sbin/${APP}&quot; readonly CRON_FILE=&quot;${PREFIX}/etc/cron.d/${APP}&quot; readonly LOGROTATE_FILE=&quot;${PREFIX}/etc/logrotate.d/${APP}&quot; readonly LOG_FILE=&quot;${PREFIX}/var/log/${APP}.log&quot; readonly LOCK_FILE=&quot;${PREFIX}/run/${APP}.lock&quot;

# —————————————————————— estado PBS_HOST=&quot;&quot; PBS_PORT=&quot;8007&quot; DATASTORE=&quot;&quot; NAMESPACE=&quot;&quot; AUTH_ID=&quot;root@pam!prune&quot; SECRET=&quot;&quot; FINGERPRINT=&quot;&quot; CRON_EXPR=&quot;&quot; CRON_DESC=&quot;&quot; G_NAMES=() G_KEEP=() AVAILABLE=() IS_LOCAL_PBS=0

# —————————————————————— interface if -t 1; then

C_0=$&#x27;\e[0m&#x27; C_B=$&#x27;\e[1m&#x27; C_DIM=$&#x27;\e[2m&#x27; C_R=$&#x27;\e[31m&#x27; C_G=$&#x27;\e[32m&#x27; C_Y=$&#x27;\e[33m&#x27; C_C=$&#x27;\e[36m&#x27;

else

C_0=&quot;&quot; C_B=&quot;&quot; C_DIM=&quot;&quot; C_R=&quot;&quot; C_G=&quot;&quot; C_Y=&quot;&quot; C_C=&quot;&quot;

fi

hr() { printf &#x27;%s%s%s\n&#x27; &quot;$C_DIM&quot; &quot;────────────────────────────────────────────────────────────&quot; &quot;$C_0&quot;; } info() { printf &#x27; %s➜%s %s\n&#x27; &quot;$C_C&quot; &quot;$C_0&quot; &quot;$*&quot;; } ok() { printf &#x27; %s✔%s %s\n&#x27; &quot;$C_G&quot; &quot;$C_0&quot; &quot;$*&quot;; } warn() { printf &#x27; %s⚠%s %s\n&#x27; &quot;$C_Y&quot; &quot;$C_0&quot; &quot;$*&quot; &gt;&amp;2; } fail() { printf &#x27; %s✖%s %s\n&#x27; &quot;$C_R&quot; &quot;$C_0&quot; &quot;$*&quot; &gt;&amp;2; } die() { fail &quot;$*&quot;; exit 1; } tip() { printf &#x27; %s%s%s\n&#x27; &quot;$C_DIM&quot; &quot;$*&quot; &quot;$C_0&quot;; } step() { printf &#x27;\n%s%s▌ %s%s\n&#x27; &quot;$C_B&quot; &quot;$C_C&quot; &quot;$1&quot; &quot;$C_0&quot;; hr; }

banner() {

printf &#x27;%s%s&#x27; &quot;$C_B&quot; &quot;$C_C&quot;
cat &lt;&lt;&#x27;EOF&#x27;
╔════════════════════════════════════════════════════════╗
║   PBS Prune Individual — Assistente de configuração    ║
║   Retenção por VM/CT no Proxmox Backup Server          ║
╚════════════════════════════════════════════════════════╝

EOF

printf &#x27;%s  %sv%s · Projeto Empresa Livre%s\n&#x27; &quot;$C_0&quot; &quot;$C_DIM&quot; &quot;$VERSION&quot; &quot;$C_0&quot;

}

on_interrupt() { stty echo 2&gt;/dev/null; printf &#x27;\n&#x27;; warn &quot;Cancelado pelo usuário. Nenhuma alteração pendente foi aplicada.&quot;; exit 130; } trap on_interrupt INT

# ask VAR &quot;pergunta&quot; &quot;padrão&quot; [validador] ask() {

local __var=$1 __prompt=$2 __def=${3-} __val=${4:-v_nonempty} __ans
while :; do
  read -rp &quot;  ${__prompt}${__def:+ ${C_DIM}[${__def}]${C_0}}: &quot; __ans || die &quot;Entrada encerrada.&quot;
  __ans=${__ans:-$__def}
  if &quot;$__val&quot; &quot;$__ans&quot;; then printf -v &quot;$__var&quot; &#x27;%s&#x27; &quot;$__ans&quot;; return 0; fi
done

}

ask_secret() {

local __var=$1 __prompt=$2 __ans
while :; do
  read -rsp &quot;  ${__prompt}: &quot; __ans || die &quot;Entrada encerrada.&quot;; printf &#x27;\n&#x27;
  [[ -n $__ans ]] &amp;&amp; { printf -v &quot;$__var&quot; &#x27;%s&#x27; &quot;$__ans&quot;; return 0; }
  warn &quot;Campo obrigatório.&quot;
done

}

# confirm &quot;pergunta&quot; [S|N] -&gt; 0 = sim confirm() {

local def=${2:-N} ans hint
[[ $def == S ]] &amp;&amp; hint=&quot;S/n&quot; || hint=&quot;s/N&quot;
read -rp &quot;  $1 ${C_DIM}[${hint}]${C_0}: &quot; ans || die &quot;Entrada encerrada.&quot;
ans=${ans:-$def}
[[ ${ans,,} =~ ^(s|sim|y|yes)$ ]]

}

# choose VAR &quot;título&quot; &quot;opção 1&quot; &quot;opção 2&quot; … -&gt; VAR recebe o número escolhido choose() {

local __var=$1 __title=$2; shift 2
local __i=1 __opt __ans
printf &#x27;  %s%s%s\n&#x27; &quot;$C_B&quot; &quot;$__title&quot; &quot;$C_0&quot;
for __opt in &quot;$@&quot;; do printf &#x27;   %s%2d)%s %s\n&#x27; &quot;$C_C&quot; &quot;$__i&quot; &quot;$C_0&quot; &quot;$__opt&quot;; __i=$((__i+1)); done
while :; do
  read -rp &quot;  Opção: &quot; __ans || die &quot;Entrada encerrada.&quot;
  if [[ $__ans =~ ^[0-9]+$ ]] &amp;&amp; (( __ans &gt;= 1 &amp;&amp; __ans &lt;= $# )); then printf -v &quot;$__var&quot; &#x27;%s&#x27; &quot;$__ans&quot;; return 0; fi
  warn &quot;Escolha um número entre 1 e $#.&quot;
done

}

# —————————————————————— validadores v_nonempty() { -n $1 || { warn &quot;Campo obrigatório.&quot;; return 1; }; } v_any() { return 0; } v_host() { $1 =~ ^[A-Za-z0-9._-]+$ || { warn &quot;Host/IP inválido (use nome DNS ou IPv4).&quot;; return 1; }; } v_port() { $1 =~ ^[0-9]+$ &amp;&amp; 1) || { warn &quot;Porta inválida (1-65535).&quot;; return 1; }; } v_name() { $1 =~ ^[A-Za-z0-9._-]+$ || { warn &quot;Use apenas letras, números, ponto, hífen e sublinhado.&quot;; return 1; }; } v_ns() { | $1 =~ ^[A-Za-z0-9._/-]+$ || { warn &quot;Namespace inválido (ex.: producao ou clientes/acme).&quot;; return 1; }; } v_posint() { $1 =~ ^[1-9][0-9]*$ || { warn &quot;Informe um inteiro maior ou igual a 1.&quot;; return 1; }; } v_time() { 2[0-3]):[0-5][0-9]$ || { warn &quot;Horário no formato HH:MM (ex.: 03:30).&quot;; return 1; }; } v_hours() { 1[0-9]|2[0-3])$ || { warn &quot;Informe de 1 a 23 horas.&quot;; return 1; }; } v_authid() {

local re=&#x27;^[A-Za-z0-9._-]+@[A-Za-z0-9._-]+![A-Za-z0-9._-]+$&#x27;
[[ $1 =~ $re ]] || { warn &quot;Formato: usuario@realm!token  (ex.: root@pam!prune)&quot;; return 1; }

} v_fp() {

local re=&#x27;^([0-9A-Fa-f]{2}:){31}[0-9A-Fa-f]{2}$&#x27;
[[ $1 =~ $re ]] || { warn &quot;Fingerprint SHA-256 inválido (32 pares hex separados por &#x27;:&#x27;).&quot;; return 1; }

} v_cron() {

local -a f; read -ra f &lt;&lt;&lt;&quot;$1&quot;
(( ${#f[@]} == 5 )) &amp;&amp; [[ $1 =~ ^[0-9A-Za-z*/,\ -]+$ ]] \
  || { warn &quot;Expressão cron precisa de 5 campos (min hora dia mês dia-semana).&quot;; return 1; }

}

# —————————————————————— helpers PBS repo() { printf &#x27;%s@%s:%s:%s&#x27; &quot;$AUTH_ID&quot; &quot;$PBS_HOST&quot; &quot;$PBS_PORT&quot; &quot;$DATASTORE&quot;; } ns_args() { -n $NAMESPACE &amp;&amp; printf – &#x27;–ns\n%s\n&#x27; &quot;$NAMESPACE&quot;; }

pbc() {

local -a ns; mapfile -t ns &lt; &lt;(ns_args)
PBS_REPOSITORY=&quot;$(repo)&quot; PBS_PASSWORD=&quot;$SECRET&quot; PBS_FINGERPRINT=&quot;$FINGERPRINT&quot; \
  timeout 60 proxmox-backup-client &quot;$@&quot; &quot;${ns[@]}&quot; &lt;/dev/null

}

fetch_fingerprint() {

timeout 8 openssl s_client -connect &quot;${PBS_HOST}:${PBS_PORT}&quot; -servername &quot;$PBS_HOST&quot; &lt;/dev/null 2&gt;/dev/null \
  | openssl x509 -noout -fingerprint -sha256 2&gt;/dev/null | cut -d= -f2

}

is_local_host() {

[[ $1 == 127.0.0.1 || $1 == localhost || $1 == &quot;$(hostname)&quot; || $1 == &quot;$(hostname -f 2&gt;/dev/null)&quot; ]] &amp;&amp; return 0
hostname -I 2&gt;/dev/null | tr &#x27; &#x27; &#x27;\n&#x27; | grep -qxF &quot;$1&quot;

}

# —————————————————————— pré-checagem preflight() {

step &quot;Verificando pré-requisitos&quot;
[[ $EUID -eq 0 ]] || die &quot;Execute como root.&quot;
local c missing=0
for c in proxmox-backup-client openssl timeout flock; do
  if command -v &quot;$c&quot; &gt;/dev/null 2&gt;&amp;1; then ok &quot;$c&quot;; else fail &quot;$c não encontrado&quot;; missing=1; fi
done
(( missing )) &amp;&amp; die &quot;Instale os pacotes faltando (proxmox-backup-client vem no PVE/PBS; em Debian puro use o repositório do PBS).&quot;
[[ -d ${PREFIX}/etc/cron.d ]] || die &quot;Diretório /etc/cron.d não existe — o cron está instalado?&quot;
ok &quot;cron&quot;
command -v proxmox-backup-manager &gt;/dev/null 2&gt;&amp;1 &amp;&amp; { IS_LOCAL_PBS=1; ok &quot;Rodando no próprio PBS — criação automática de token disponível&quot;; }
return 0

}

# —————————————————————— 1. conexão step_connection() {

step &quot;1/5 · Conexão com o Proxmox Backup Server&quot;
local def_host=&quot;&quot;; (( IS_LOCAL_PBS )) &amp;&amp; def_host=&quot;127.0.0.1&quot;
ask PBS_HOST &quot;IP ou hostname do PBS&quot; &quot;${PBS_HOST:-$def_host}&quot; v_host
ask PBS_PORT &quot;Porta da API&quot; &quot;$PBS_PORT&quot; v_port
if (( IS_LOCAL_PBS )) &amp;&amp; is_local_host &quot;$PBS_HOST&quot;; then
  info &quot;Datastores disponíveis:&quot;
  proxmox-backup-manager datastore list 2&gt;/dev/null | sed &#x27;s/^/     /&#x27;
fi
ask DATASTORE &quot;Nome do datastore&quot; &quot;$DATASTORE&quot; v_name
ask NAMESPACE &quot;Namespace (Enter = raiz)&quot; &quot;$NAMESPACE&quot; v_ns
tip &quot;Recomendado: token dedicado, ex.: root@pam!prune&quot;
ask AUTH_ID &quot;Auth-ID do token (usuario@realm!token)&quot; &quot;$AUTH_ID&quot; 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 )) &amp;&amp; is_local_host &quot;$PBS_HOST&quot; &amp;&amp; confirm &quot;Criar/ajustar o token e a permissão automaticamente?&quot; S; then
  if proxmox-backup-manager user list-tokens &quot;$user&quot; --output-format json 2&gt;/dev/null | grep -qF &quot;\&quot;${AUTH_ID}\&quot;&quot;; then
    warn &quot;O token ${AUTH_ID} já existe.&quot;
    choose opt &quot;O que deseja fazer?&quot; &quot;Usar o token existente (vou digitar o secret)&quot; &quot;Regenerar o token (o secret atual deixa de funcionar)&quot;
    if (( opt == 2 )); then
      proxmox-backup-manager user delete-token &quot;$user&quot; &quot;$tok&quot; &gt;/dev/null 2&gt;&amp;1 || die &quot;Falha ao remover o token antigo.&quot;
    else
      ask_secret SECRET &quot;Secret do token&quot;
    fi
  fi
  if [[ -z $SECRET ]]; then
    out=$(proxmox-backup-manager user generate-token &quot;$user&quot; &quot;$tok&quot; --output-format json 2&gt;&amp;1) \
      || die &quot;Falha ao gerar o token: $out&quot;
    SECRET=$(sed -n &#x27;s/.*&quot;value&quot;[[:space:]]*:[[:space:]]*&quot;\([^&quot;]*\)&quot;.*/\1/p&#x27; &lt;&lt;&lt;&quot;$out&quot;)
    [[ -n $SECRET ]] || die &quot;Não consegui ler o secret gerado: $out&quot;
    ok &quot;Token ${AUTH_ID} criado&quot;
  fi
  acl_path=&quot;/datastore/${DATASTORE}${NAMESPACE:+/$NAMESPACE}&quot;
  if proxmox-backup-manager acl update &quot;$acl_path&quot; DatastoreAdmin --auth-id &quot;$AUTH_ID&quot; &gt;/dev/null 2&gt;&amp;1; then
    ok &quot;Permissão DatastoreAdmin em ${acl_path}&quot;
  else
    warn &quot;Não consegui aplicar a ACL. Aplique manualmente:&quot;
    tip &quot;proxmox-backup-manager acl update ${acl_path} DatastoreAdmin --auth-id &#x27;${AUTH_ID}&#x27;&quot;
  fi
else
  if [[ -n $SECRET ]] &amp;&amp; confirm &quot;Manter o secret já configurado?&quot; S; then return 0; fi
  tip &quot;O token precisa da role DatastoreAdmin em /datastore/${DATASTORE} (prune de grupos de outros donos).&quot;
  ask_secret SECRET &quot;Secret do token&quot;
fi

}

setup_fingerprint() {

local fp
info &quot;Obtendo o fingerprint do certificado de ${PBS_HOST}:${PBS_PORT}...&quot;
fp=$(fetch_fingerprint || true)
if [[ -n $fp ]]; then
  printf &#x27;     %s%s%s\n&#x27; &quot;$C_B&quot; &quot;$fp&quot; &quot;$C_0&quot;
  tip &quot;Confira em: PBS → Painel → Mostrar Fingerprint (ou: proxmox-backup-manager cert info)&quot;
  if confirm &quot;O fingerprint confere?&quot; S; then FINGERPRINT=$fp; return 0; fi
else
  warn &quot;Não foi possível ler o certificado automaticamente.&quot;
fi
ask FINGERPRINT &quot;Cole o fingerprint SHA-256&quot; &quot;$FINGERPRINT&quot; v_fp

}

test_connection() {

local out
info &quot;Testando conexão em $(repo)${NAMESPACE:+ (ns: $NAMESPACE)}...&quot;
if out=$(pbc list --output-format text 2&gt;&amp;1); then
  mapfile -t AVAILABLE &lt; &lt;(grep -oE &#x27;\b(vm|ct|host)/[A-Za-z0-9._-]+&#x27; &lt;&lt;&lt;&quot;$out&quot; | sort -u -V)
  ok &quot;Conectado — ${#AVAILABLE[@]} grupo(s) de backup encontrados&quot;
  return 0
fi
fail &quot;Falha na conexão:&quot;
sed &#x27;s/^/     /&#x27; &lt;&lt;&lt;&quot;$out&quot; | tail -n 5 &gt;&amp;2
confirm &quot;Revisar os dados de conexão?&quot; S &amp;&amp; return 1
confirm &quot;Continuar mesmo assim?&quot; N || die &quot;Abortado.&quot;
AVAILABLE=()
return 0

}

# —————————————————————— 2. grupos print_groups() {

local i
if (( ${#G_NAMES[@]} == 0 )); then tip &quot;(nenhum grupo configurado)&quot;; return; fi
printf &#x27;  %s%-4s %-18s %s%s\n&#x27; &quot;$C_B&quot; &quot;#&quot; &quot;GRUPO&quot; &quot;MANTER (keep-last)&quot; &quot;$C_0&quot;
for i in &quot;${!G_NAMES[@]}&quot;; do
  printf &#x27;  %-4s %-18s %s\n&#x27; &quot;$((i+1))&quot; &quot;${G_NAMES[$i]}&quot; &quot;${G_KEEP[$i]}&quot;
done

}

group_index() { local i; for i in &quot;${!G_NAMES[@]}&quot;; do $1&quot &amp;&amp; { echo &quot;$i&quot;; return 0; }; done; return 1; } in_available() { printf &#x27;%s\n&#x27; &quot;${AVAILABLE[@]}&quot; | grep -qxF &quot;$1&quot;; }

resolve_group() { # entrada: &quot;100&quot;, &quot;vm/100&quot;, &quot;ct/200&quot; -&gt; saída: grupo

local input=$1 opt
if [[ $input =~ ^(vm|ct|host)/[A-Za-z0-9._-]+$ ]]; then echo &quot;$input&quot;; return 0; fi
[[ $input =~ ^[0-9]+$ ]] || { warn &quot;Informe o ID (ex.: 100) ou o grupo (ex.: ct/200).&quot;; return 1; }
local has_vm=0 has_ct=0
in_available &quot;vm/$input&quot; &amp;&amp; has_vm=1
in_available &quot;ct/$input&quot; &amp;&amp; has_ct=1
if (( has_vm &amp;&amp; has_ct )); then
  choose opt &quot;Existem vm/$input e ct/$input. Qual?&quot; &quot;vm/$input (QEMU)&quot; &quot;ct/$input (LXC)&quot; &gt;&amp;2
  (( opt == 1 )) &amp;&amp; echo &quot;vm/$input&quot; || echo &quot;ct/$input&quot;
elif (( has_vm )); then echo &quot;vm/$input&quot;
elif (( has_ct )); then echo &quot;ct/$input&quot;
else
  warn &quot;ID $input não encontrado no datastore${NAMESPACE:+ / ns $NAMESPACE}.&quot;
  choose opt &quot;Adicionar mesmo assim como:&quot; &quot;vm/$input (QEMU)&quot; &quot;ct/$input (LXC)&quot; &quot;Cancelar&quot; &gt;&amp;2
  case $opt in 1) echo &quot;vm/$input&quot;;; 2) echo &quot;ct/$input&quot;;; *) return 1;; esac
fi

}

step_groups() {

step &quot;2/5 · VMs/CTs e retenção&quot;
if (( ${#AVAILABLE[@]} )); then
  info &quot;Grupos encontrados no datastore:&quot;
  if command -v column &gt;/dev/null 2&gt;&amp;1; then
    printf &#x27;%s\n&#x27; &quot;${AVAILABLE[@]}&quot; | column -c 70 | sed &#x27;s/^/     /&#x27;
  else
    printf &#x27;     %s\n&#x27; &quot;${AVAILABLE[@]}&quot;
  fi
fi
tip &quot;Digite o ID (100) ou o grupo (ct/200). Enter vazio finaliza.&quot;
local input group keep idx opt n
while :; do
  printf &#x27;\n&#x27;
  read -rp &quot;  ID da VM/CT: &quot; input || die &quot;Entrada encerrada.&quot;
  input=${input// /}
  if [[ -z $input ]]; then
    if (( ${#G_NAMES[@]} == 0 )); then warn &quot;Adicione pelo menos um grupo.&quot;; continue; fi
    printf &#x27;\n&#x27;; print_groups; printf &#x27;\n&#x27;
    choose opt &quot;Lista concluída?&quot; &quot;Sim, continuar&quot; &quot;Adicionar mais&quot; &quot;Remover um item&quot; &quot;Limpar a lista&quot;
    case $opt in
      1) break ;;
      2) continue ;;
      3) ask n &quot;Número do item a remover&quot; &quot;&quot; v_posint
         if (( n &lt;= ${#G_NAMES[@]} )); then
           ok &quot;Removido ${G_NAMES[$((n-1))]}&quot;
           unset &#x27;G_NAMES[n-1]&#x27; &#x27;G_KEEP[n-1]&#x27;; G_NAMES=(&quot;${G_NAMES[@]}&quot;); G_KEEP=(&quot;${G_KEEP[@]}&quot;)
         else warn &quot;Item inexistente.&quot;; fi ;;
      4) G_NAMES=(); G_KEEP=(); ok &quot;Lista limpa.&quot; ;;
    esac
    continue
  fi
  group=$(resolve_group &quot;$input&quot;) || continue
  if idx=$(group_index &quot;$group&quot;); then
    warn &quot;$group já está na lista (keep-last ${G_KEEP[$idx]}).&quot;
    confirm &quot;Alterar a retenção?&quot; S || continue
    ask keep &quot;Quantos backups manter de $group&quot; &quot;${G_KEEP[$idx]}&quot; v_posint
    G_KEEP[$idx]=$keep
  else
    ask keep &quot;Quantos backups manter de $group&quot; &quot;7&quot; v_posint
    G_NAMES+=(&quot;$group&quot;); G_KEEP+=(&quot;$keep&quot;)
  fi
  ok &quot;$group → manter os últimos $keep&quot;
done

}

# —————————————————————— 3. simulação step_dryrun() {

step &quot;3/5 · Simulação (dry-run)&quot;
confirm &quot;Simular agora o que seria mantido/removido?&quot; S || { tip &quot;Simulação pulada.&quot;; return 0; }
local i out kept removed
printf &#x27;  %s%-18s %-10s %-9s %s%s\n&#x27; &quot;$C_B&quot; &quot;GRUPO&quot; &quot;KEEP-LAST&quot; &quot;MANTÉM&quot; &quot;REMOVE&quot; &quot;$C_0&quot;
for i in &quot;${!G_NAMES[@]}&quot;; do
  if out=$(pbc prune &quot;${G_NAMES[$i]}&quot; --keep-last &quot;${G_KEEP[$i]}&quot; --dry-run --output-format json 2&gt;&amp;1); then
    kept=$(grep -oE &#x27;&quot;keep&quot;:[[:space:]]*true&#x27; &lt;&lt;&lt;&quot;$out&quot; | wc -l)
    removed=$(grep -oE &#x27;&quot;keep&quot;:[[:space:]]*false&#x27; &lt;&lt;&lt;&quot;$out&quot; | wc -l)
    printf &#x27;  %-18s %-10s %-9s %s%s%s\n&#x27; &quot;${G_NAMES[$i]}&quot; &quot;${G_KEEP[$i]}&quot; &quot;$kept&quot; \
      &quot;$( (( removed &gt; 0 )) &amp;&amp; echo &quot;$C_Y&quot; )&quot; &quot;$removed&quot; &quot;$C_0&quot;
  else
    printf &#x27;  %-18s %-10s %s%s%s\n&#x27; &quot;${G_NAMES[$i]}&quot; &quot;${G_KEEP[$i]}&quot; &quot;$C_R&quot; &quot;erro: $(tail -n1 &lt;&lt;&lt;&quot;$out&quot;)&quot; &quot;$C_0&quot;
  fi
done
printf &#x27;\n&#x27;
confirm &quot;Resultado OK? (N volta para editar a lista)&quot; S || { step_groups; step_dryrun; }

}

# —————————————————————— 4. agendamento step_schedule() {

step &quot;4/5 · Agendamento&quot;
tip &quot;Agende DEPOIS da janela de backup e ANTES do Garbage Collection do datastore.&quot;
local opt t h m d dname
local -a days=(&quot;Domingo&quot; &quot;Segunda&quot; &quot;Terça&quot; &quot;Quarta&quot; &quot;Quinta&quot; &quot;Sexta&quot; &quot;Sábado&quot;)
choose opt &quot;Frequência:&quot; &quot;Diário&quot; &quot;Semanal&quot; &quot;A cada N horas&quot; &quot;Expressão cron personalizada&quot; &quot;Não agendar (só executar manualmente)&quot;
case $opt in
  1) ask t &quot;Horário (HH:MM)&quot; &quot;03:30&quot; v_time; h=$((10#${t%%:*})); m=$((10#${t##*:}))
     CRON_EXPR=&quot;$m $h * * *&quot;; CRON_DESC=&quot;Diário às $(printf &#x27;%02d:%02d&#x27; &quot;$h&quot; &quot;$m&quot;)&quot; ;;
  2) choose d &quot;Dia da semana:&quot; &quot;${days[@]}&quot;; d=$((d-1)); dname=${days[$d]}
     ask t &quot;Horário (HH:MM)&quot; &quot;03:30&quot; v_time; h=$((10#${t%%:*})); m=$((10#${t##*:}))
     CRON_EXPR=&quot;$m $h * * $d&quot;; CRON_DESC=&quot;Toda ${dname} às $(printf &#x27;%02d:%02d&#x27; &quot;$h&quot; &quot;$m&quot;)&quot; ;;
  3) ask h &quot;Intervalo em horas&quot; &quot;6&quot; v_hours
     CRON_EXPR=&quot;15 */$h * * *&quot;; CRON_DESC=&quot;A cada $h hora(s), no minuto 15&quot; ;;
  4) ask CRON_EXPR &quot;Expressão (min hora dia mês dia-semana)&quot; &quot;${CRON_EXPR:-30 3 * * *}&quot; v_cron
     CRON_DESC=&quot;Personalizado&quot; ;;
  5) CRON_EXPR=&quot;&quot;; CRON_DESC=&quot;Sem agendamento&quot; ;;
esac
ok &quot;${CRON_DESC}${CRON_EXPR:+  (${CRON_EXPR})}&quot;

}

# —————————————————————— 5. instalação write_runner() {

mkdir -p &quot;$(dirname &quot;$RUNNER&quot;)&quot;
cat &gt;&quot;$RUNNER&quot; &lt;&lt;&#x27;RUNNER_EOF&#x27;

#!/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=&quot;${PBS_PRUNE_CONF:-@CONF_FILE@}&quot; -r $CONF || { echo &quot;Configuração não encontrada: $CONF&quot; &gt;&amp;2; exit 2; } # shellcheck source=/dev/null source &quot;$CONF&quot;

DRY=() case &quot;${1:-}&quot; in

  1. n|–dry-run) DRY=(–dry-run) ;;
  2. l|–list) grep -vE &#x27;^\s*(#|$)&#x27; &quot;$GROUPS_FILE&quot;; exit 0 ;;
  3. h|–help) echo &quot;Uso: $(basename &quot;$0&quot;) [–dry-run] [–list]&quot;; exit 0 ;;

&quot;&quot;) ;;

  • ) echo &quot;Opção inválida: $1&quot; &gt;&amp;2; exit 2 ;;

esac

exec 9&gt;&quot;$LOCK_FILE&quot; flock -n 9 || { echo &quot;Já existe uma execução em andamento.&quot; &gt;&amp;2; exit 3; }

log() {

local line; line=&quot;$(date &#x27;+%F %T&#x27;) $*&quot;
printf &#x27;%s\n&#x27; &quot;$line&quot; &gt;&gt;&quot;$LOG_FILE&quot;
[[ -t 1 ]] &amp;&amp; printf &#x27;%s\n&#x27; &quot;$line&quot;
logger -t pbs-prune -- &quot;$*&quot; 2&gt;/dev/null
return 0

}

export PBS_REPOSITORY=&quot;${AUTH_ID}@${PBS_HOST}:${PBS_PORT}:${DATASTORE}&quot; export PBS_FINGERPRINT=&quot;$FINGERPRINT&quot; PBS_PASSWORD=&quot;$(&lt;&quot;$SECRET_FILE&quot;)&quot; || { log &quot;ERRO: não foi possível ler $SECRET_FILE&quot;; exit 2; } export PBS_PASSWORD

NS=(); -} &amp;&amp; NS=(–ns &quot;$NAMESPACE&quot;)

total=0 errors=0 removed_total=0 start=$SECONDS log &quot;===== início${DRY:+ (dry-run)} · ${PBS_REPOSITORY}${NAMESPACE:+ · ns=$NAMESPACE} =====&quot; while read -r group keep _; do

[[ -z ${group:-} || $group == \#* ]] &amp;&amp; continue
total=$((total + 1))
if out=$(timeout 600 proxmox-backup-client prune &quot;$group&quot; &quot;${NS[@]}&quot; --keep-last &quot;$keep&quot; \
           &quot;${DRY[@]}&quot; --output-format json &lt;/dev/null 2&gt;&amp;1); then
  removed=$(grep -oE &#x27;&quot;keep&quot;:[[:space:]]*false&#x27; &lt;&lt;&lt;&quot;$out&quot; | wc -l)
  removed_total=$((removed_total + removed))
  log &quot;OK    ${group}  keep-last=${keep}  removidos=${removed}&quot;
else
  errors=$((errors + 1))
  log &quot;ERRO  ${group}  keep-last=${keep}&quot;
  while IFS= read -r l; do log &quot;      $l&quot;; done &lt;&lt;&lt;&quot;$out&quot;
fi

done &lt;&quot;$GROUPS_FILE&quot; log &quot;===== fim · grupos=${total} erros=${errors} snapshots removidos=${removed_total} · $2)s =====&quot;

3) RUNNER_EOF

sed -i &quot;s|@CONF_FILE@|${CONF_FILE}|&quot; &quot;$RUNNER&quot;
chmod 750 &quot;$RUNNER&quot;

}

write_config() {

install -d -m 700 &quot;$CONF_DIR&quot;
umask 077
{
  echo &quot;# Gerado por pbs-prune-setup v${VERSION} em $(date &#x27;+%F %T&#x27;). Rode o setup novamente para alterar.&quot;
  printf &#x27;PBS_HOST=%q\nPBS_PORT=%q\nDATASTORE=%q\nNAMESPACE=%q\nAUTH_ID=%q\nFINGERPRINT=%q\n&#x27; \
    &quot;$PBS_HOST&quot; &quot;$PBS_PORT&quot; &quot;$DATASTORE&quot; &quot;$NAMESPACE&quot; &quot;$AUTH_ID&quot; &quot;$FINGERPRINT&quot;
  printf &#x27;CRON_EXPR=%q\nCRON_DESC=%q\n&#x27; &quot;$CRON_EXPR&quot; &quot;$CRON_DESC&quot;
  printf &#x27;SECRET_FILE=%q\nGROUPS_FILE=%q\nLOG_FILE=%q\nLOCK_FILE=%q\n&#x27; \
    &quot;$SECRET_FILE&quot; &quot;$GROUPS_FILE&quot; &quot;$LOG_FILE&quot; &quot;$LOCK_FILE&quot;
} &gt;&quot;$CONF_FILE&quot;
printf &#x27;%s&#x27; &quot;$SECRET&quot; &gt;&quot;$SECRET_FILE&quot;
chmod 600 &quot;$CONF_FILE&quot; &quot;$SECRET_FILE&quot;
umask 022

}

write_groups() {

local i
install -d -m 700 &quot;$CONF_DIR&quot;
{
  echo &quot;# grupo      keep-last   (vm/ID = QEMU · ct/ID = LXC)&quot;
  echo &quot;# Pode editar à mão; uma linha por grupo.&quot;
  for i in &quot;${!G_NAMES[@]}&quot;; do printf &#x27;%-12s %s\n&#x27; &quot;${G_NAMES[$i]}&quot; &quot;${G_KEEP[$i]}&quot;; done
} &gt;&quot;$GROUPS_FILE&quot;
chmod 600 &quot;$GROUPS_FILE&quot;

}

write_cron() {

if [[ -z $CRON_EXPR ]]; then rm -f &quot;$CRON_FILE&quot;; return 0; fi
mkdir -p &quot;$(dirname &quot;$CRON_FILE&quot;)&quot; &quot;$(dirname &quot;$LOG_FILE&quot;)&quot; &quot;$(dirname &quot;$LOCK_FILE&quot;)&quot;
cat &gt;&quot;$CRON_FILE&quot; &lt;&lt;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} &gt;/dev/null 2&gt;&amp;1 EOF

chmod 644 &quot;$CRON_FILE&quot;

}

write_logrotate() {

mkdir -p &quot;$(dirname &quot;$LOGROTATE_FILE&quot;)&quot;
cat &gt;&quot;$LOGROTATE_FILE&quot; &lt;&lt;EOF

${LOG_FILE} {

  monthly
  rotate 12
  compress
  missingok
  notifempty

} EOF }

step_review_install() {

step &quot;5/5 · Revisão e instalação&quot;
printf &#x27;  %-14s %s\n&#x27; &quot;Repositório:&quot; &quot;$(repo)&quot; &quot;Namespace:&quot; &quot;${NAMESPACE:-(raiz)}&quot; \
  &quot;Fingerprint:&quot; &quot;${FINGERPRINT:0:23}…&quot; &quot;Agendamento:&quot; &quot;${CRON_DESC}${CRON_EXPR:+ (${CRON_EXPR})}&quot;
printf &#x27;\n&#x27;; print_groups; printf &#x27;\n&#x27;
confirm &quot;Instalar com essas configurações?&quot; S || die &quot;Instalação cancelada. Nada foi alterado.&quot;
write_config;    ok &quot;Configuração     ${CONF_FILE}&quot;
write_groups;    ok &quot;Lista de grupos  ${GROUPS_FILE}&quot;
ok &quot;Secret           ${SECRET_FILE} (600)&quot;
write_runner;    ok &quot;Executor         ${RUNNER}&quot;
write_cron;      [[ -n $CRON_EXPR ]] &amp;&amp; ok &quot;Agendamento      ${CRON_FILE}&quot; || tip &quot;Sem agendamento instalado.&quot;
write_logrotate; ok &quot;Logrotate        ${LOGROTATE_FILE}&quot;

}

finish() {

printf &#x27;\n&#x27;
if confirm &quot;Executar o prune agora (valendo)?&quot; N; then
  &quot;$RUNNER&quot; &amp;&amp; ok &quot;Execução concluída.&quot; || warn &quot;Execução terminou com erros — veja o log.&quot;
fi
step &quot;Pronto&quot;
printf &#x27;  %-34s %s\n&#x27; \
  &quot;pbs-prune --dry-run&quot; &quot;simular&quot; \
  &quot;pbs-prune&quot; &quot;executar agora&quot; \
  &quot;pbs-prune --list&quot; &quot;ver grupos configurados&quot; \
  &quot;tail -f ${LOG_FILE}&quot; &quot;acompanhar o log&quot; \
  &quot;$(basename &quot;$0&quot;)&quot; &quot;editar lista / agendamento / desinstalar&quot;
tip &quot;Lembrete: o espaço só é liberado quando o Garbage Collection do datastore rodar.&quot;
printf &#x27;\n&#x27;

}

# —————————————————————— carregar / desinstalar load_existing() {

# shellcheck source=/dev/null
source &quot;$CONF_FILE&quot;
[[ -r $SECRET_FILE ]] &amp;&amp; SECRET=$(&lt;&quot;$SECRET_FILE&quot;)
G_NAMES=() G_KEEP=()
local g k
while read -r g k _; do
  [[ -z ${g:-} || $g == \#* ]] &amp;&amp; continue
  G_NAMES+=(&quot;$g&quot;); G_KEEP+=(&quot;$k&quot;)
done &lt;&quot;$GROUPS_FILE&quot;

}

uninstall() {

step &quot;Desinstalação&quot;
[[ $EUID -eq 0 ]] || die &quot;Execute como root.&quot;
confirm &quot;Remover executor, configuração, agendamento e logrotate?&quot; N || die &quot;Cancelado.&quot;
rm -f &quot;$RUNNER&quot; &quot;$CRON_FILE&quot; &quot;$LOGROTATE_FILE&quot; &amp;&amp; ok &quot;Executor, cron e logrotate removidos&quot;
rm -rf &quot;$CONF_DIR&quot; &amp;&amp; ok &quot;Configuração e secret removidos&quot;
tip &quot;O log ${LOG_FILE} foi mantido. O token no PBS não foi removido:&quot;
tip &quot;proxmox-backup-manager user delete-token &lt;usuario@realm&gt; &lt;token&gt;&quot;

}

existing_menu() {

local opt
load_existing
step &quot;Instalação existente encontrada&quot;
printf &#x27;  %-14s %s\n&#x27; &quot;Repositório:&quot; &quot;$(repo)&quot; &quot;Agendamento:&quot; &quot;${CRON_DESC:-—}${CRON_EXPR:+ (${CRON_EXPR})}&quot;
printf &#x27;\n&#x27;; print_groups; printf &#x27;\n&#x27;
choose opt &quot;O que deseja fazer?&quot; \
  &quot;Editar a lista de VMs/CTs&quot; &quot;Alterar o agendamento&quot; &quot;Reconfigurar tudo&quot; \
  &quot;Simular agora (dry-run)&quot; &quot;Desinstalar&quot; &quot;Sair&quot;
case $opt in
  1) test_connection || true; step_groups; step_dryrun; write_groups; ok &quot;Lista atualizada: ${GROUPS_FILE}&quot; ;;
  2) step_schedule; write_config; write_cron; ok &quot;Agendamento atualizado&quot; ;;
  3) full_flow; return ;;
  4) &quot;$RUNNER&quot; --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 &quot;${1:-}&quot; in

  1. h|–help) sed -n &#x27;2,32p&#x27; &quot;$0&quot; | sed &#x27;s/^# \{0,1\}&#x27;; exit 0 ;; -v|–version) echo &quot;pbs-prune-setup $VERSION&quot;; exit 0 ;; –uninstall) banner; uninstall; exit 0 ;; &quot;&quot;) ;; *) die &quot;Opção inválida: $1 (use –help)&quot; ;; esac banner preflight if -f $GROUPS_FILE; then existing_menu; else full_flow; fi</pre> </section> <section id=“wiki”> <h2>Versão para DokuWiki</h2> <p class=“sub”>A página também pode ser publicada diretamente no DokuWiki. O arquivo abaixo contém a documentação e o script em bloco Bash.</p>

    <a class=“btn primary” href=“pbs-prune-individual.txt” download>↓ Baixar página DokuWiki</a> <a class=“btn” href=“https://wiki.projetoempresalivre.com.br/doku.php?id=proxmox:prune-individual” target=“_blank” rel=“noopener”>Abrir na Wiki</a>

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

    PBS Prune Individual · Projeto Empresa Livre · v1.0.0 · Licença MIT

    </footer> </body> </html>

1)
$1 &gt;= 1 &amp;&amp; $1 &lt;= 65535
2) , 16)
SECONDS - start
3) , 17)
errors == 0
4)
i+1)); done while :; do read -rp &quot; Opção: &quot; ans || die &quot;Entrada encerrada.&quot;
  if [[ $__ans =~ ^[0-9]+$ ]] &amp;&amp; (( __ans &gt;= 1 &amp;&amp; __ans &lt;= $# )); then printf -v &quot;$__var&quot; &#x27;%s&#x27; &quot;$__ans&quot;; return 0; fi
  warn &quot;Escolha um número entre 1 e $#.&quot;
done
} # —————————————————————— validadores v_nonempty() { -n $1 || { warn &quot;Campo obrigatório.&quot;; return 1; }; } v_any() { return 0; } v_host() { $1 =~ ^[A-Za-z0-9._-]+$ || { warn &quot;Host/IP inválido (use nome DNS ou IPv4).&quot;; return 1; }; } v_port() { $1 =~ ^[0-9]+$ &amp;&amp; (( $1 &gt;= 1 &amp;&amp; $1 &lt;= 65535
5)
${#f[@]} == 5
6)
missing
7) , 8)
IS_LOCAL_PBS
9) , 12)
10#${t##*:}
10)
d-1
11)
10#${t%%:*}
13)
total + 1
14)
removed_total + removed
15)
errors + 1
proxmox/prune-individual.1790543451.txt.gz · Última modificação: por admin

Donate Powered by PHP Valid HTML5 Valid CSS Driven by DokuWiki