Ferramentas do usuário

Ferramentas do site


proxmox:prune-individual

Diferenças

Aqui você vê as diferenças entre duas revisões dessa página.

Link para esta página de comparações

Ambos lados da revisão anteriorRevisão anterior
Próxima revisão
Revisão anterior
proxmox:prune-individual [2026/09/27 18:10] – adminproxmox:prune-individual [2026/09/29 13:22] (atual) – admin
Linha 1: Linha 1:
-<!doctype html> +====== PBS — Prune Individual por VM/CT ======
-<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><div class="wrap nav"><div class="brand">PBS <b>PRUNE</b> INDIVIDUAL</div><div class="links"> +
-<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> +
-</div></div></nav>+
  
-<header class="wrap hero"> +Script para configurar políticas de retenção individuais por VM ou CT no Proxmox Backup Server.
-<div class="eyebrow">Proxmox Backup Server · documentação técnica</div> +
-<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> +
-<div class="badges"><span class="badge hot">v1.0.0 · 25/09/2026</span><span class="badge">Bash</span><span class="badge">PBS</span><span class="badge">MIT</span></div> +
-</header>+
  
-<main class="wrap"> +Exemplo:
-<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> +
-<div class="grid"> +
-<div class="card"><div class="num">01 · CONEXÃO</div><h3>Valida o ambiente</h3><p>Host, porta, datastore, namespace, token e fingerprint.</p></div> +
-<div class="card"><div class="num">02 · RETENÇÃO</div><h3>Escolhe o grupo</h3><p>VM/ID ou CT/ID com seu próprio keep-last.</p></div> +
-<div class="card"><div class="num">03 · SEGURANÇA</div><h3>Simula antes</h3><p>Dry-run permite revisar o que será mantido ou removido.</p></div> +
-</div> +
-</section>+
  
-<section id="estrutura"> +<code> 
-<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> +vm/100   7 
-<div class="flow"> +vm/101   15 
-<div class="step"><b>Setup</b><br><span class="small">entrada e validação</span></div><div class="arrow">→</div> +ct/200   30 
-<div class="step"><b>Config</b><br><span class="small">PBS + agendamento</span></div><div class="arrow">→</div> +</code>
-<div class="step"><b>Groups</b><br><span class="small">VM/CT + keep-last</span></div><div class="arrow">→</div> +
-<div class="step"><b>Runner</b><br><span class="small">execução real</span></div><div class="arrow">→</div> +
-<div class="step"><b>Log</b><br><span class="small">histórico</span></div> +
-</div> +
-<div class="grid2"> +
-<div class="card"><h3>Setup interativo</h3><p>Executa o fluxo de pré-requisitos, conexão, grupos, dry-run, agendamento e instalação.</p></div> +
-<div class="card"><h3>Executor</h3><p>O <code>pbs-prune</code> lê a configuração e processa os grupos configurados, com lock, timeout e log.</p></div> +
-</div> +
-</section>+
  
-<section id="uso"> +Cada grupo pode possuir seu próprio ''keep-last''.
-<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 +===== Pré-requisitos =====
-./pbs-prune-setup.sh --help+
  
-# remover a instalação local +Antes de executar:
-./pbs-prune-setup.sh --uninstall</pre> +
-<div class="callout"><strong>Importante:</strong> o espaço no datastore só é efetivamente liberado quando o Garbage Collection do datastore for executado.</div> +
-</section>+
  
-<section id="arquivos"> +  * Proxmox Backup Server acessível; 
-<h2>O que fica instalado</h2> +  * ''proxmox-backup-client'' instalado; 
-<p class="sub">A instalação separa código, configuração, segredo, política de retenção, agendamento e logs.</p> +  * acesso ''root'' ao servidor onde o script será instalado; 
-<table> +  * um usuário existente no PBS; 
-<thead><tr><th>Arquivo</th><th>Função</th></tr></thead> +  * API Token associado a esse usuário; 
-<tbody> +  * permissão no datastore que será utilizado.
-<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> +O API Token sempre pertence a um usuário existente.
-<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> +Exemplos:
-<h2>Comandos do executor</h2> +
-<div class="grid"> +
-<div class="card"><h3><code>pbs-prune --dry-run</code></h3><p>Simula a operação sem aplicar o prune.</p></div> +
-<div class="card"><h3><code>pbs-prune</code></h3><p>Executa o prune configurado.</p></div> +
-<div class="card"><h3><code>pbs-prune --list</code></h3><p>Mostra os grupos configurados.</p></div> +
-</div> +
-</section>+
  
-<section> +<code> 
-<h2>Cuidados de segurança</h2> +root@pam!prune 
-<div class="grid2"> +backup@pbs!prune 
-<div class="card"><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></div> +</code>
-<div class="card"><h3>Execução controlada</h3><p>O runner usa <code>flock</code> para evitar concorrência e <code>timeout</code> para limitar a chamada ao cliente PBS.</p></div> +
-</div> +
-</section>+
  
-<section id="script"> +A permissão deve ser aplicada no datastore utilizado:
-<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+<code bash> 
 +proxmox-backup-manager acl update /datastore/backup DatastoreAdmin --auth-id 'backup@pbs!prune' 
 +</code>
  
-readonly VERSION=&quot;1.0.0&quot; +Substitua ''backup'' pelo nome do seu datastore e utilize o usuário/token criado no seu ambiente.
-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 +===== Download =====
-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 +Baixe o script diretamente:
-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;; } +[[https://cursoproxmox.com.br/prune/pbs-prune-setup.sh|pbs-prune-setup.sh]]
-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() { +===== Instalação =====
-  printf &#x27;%s%s&#x27; &quot;$C_B&quot; &quot;$C_C&quot; +
-  cat &lt;&lt;&#x27;EOF&#x27;+
  
-  ╔════════════════════════════════════════════════════════╗ +No PBS:
-  ║   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; } +<code bash> 
-trap on_interrupt INT+chmod +x /root/pbs-prune-setup.sh 
 +/root/pbs-prune-setup.sh 
 +</code>
  
-# ask VAR &quot;pergunta&quot; &quot;padrão&quot; [validador] +O assistente solicitará:
-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() { +  * endereço do PBS; 
-  local __var=$1 __prompt=$2 __ans +  * datastore; 
-  while :; do +  * namespace, se utilizado; 
-    read -rsp &quot;  ${__prompt}: &quot; __ans || die &quot;Entrada encerrada.&quot;; printf &#x27;\n&#x27; +  * API Token; 
-    [[ -n $__ans ]] &amp;&amp; { printf -v &quot;$__var&quot; &#x27;%s&#x27; &quot;$__ans&quot;; return 0; } +  * VMs/CTs; 
-    warn &quot;Campo obrigatório.&quot; +  * quantidade de backups a manter; 
-  done +  * agendamento.
-}+
  
-# confirm &quot;pergunta&quot; [S|N]  -&gt; 0 = sim +Antes da instalação é possível realizar um ''dry-run''.
-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 +===== Estrutura criada =====
-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 +Após a instalação:
-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 )) || { 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()       { [[ -z $1 || $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()     { [[ $1 =~ ^([01]?[0-9]|2[0-3]):[0-5][0-9]$ ]] || { warn &quot;Horário no formato HH:MM (ex.: 03:30).&quot;; return 1; }; } +
-v_hours()    { [[ $1 =~ ^([1-9]|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 +<code> 
-repo() { printf &#x27;%s@%s:%s:%s&#x27; &quot;$AUTH_ID&quot; &quot;$PBS_HOST&quot; &quot;$PBS_PORT&quot; &quot;$DATASTORE&quot;; } +/usr/local/sbin/ 
-ns_args() { [[ -n $NAMESPACE ]] &amp;&amp; printf -- &#x27;--ns\n%s\n&#x27; &quot;$NAMESPACE&quot;; }+└── pbs-prune
  
-pbc() { +/etc/pbs-prune/ 
-  local -a ns; mapfile -t ns &lt; &lt;(ns_args) +├── pbs-prune.conf 
-  PBS_REPOSITORY=&quot;$(repo)&quot; PBS_PASSWORD=&quot;$SECRET&quot; PBS_FINGERPRINT=&quot;$FINGERPRINT&quot; \ +├── groups.conf 
-    timeout 60 proxmox-backup-client &quot;$@&quot; &quot;${ns[@]}&quot; &lt;/dev/null +└── secret
-}+
  
-fetch_fingerprint() { +/etc/cron.d/ 
-  timeout 8 openssl s_client -connect &quot;${PBS_HOST}:${PBS_PORT}&quot; -servername &quot;$PBS_HOST&quot; &lt;/dev/null 2&gt;/dev/null \ +└── pbs-prune
-    | openssl x509 -noout -fingerprint -sha256 2&gt;/dev/null | cut -d= -f2 +
-}+
  
-is_local_host() { +/etc/logrotate.d/ 
-  [[ $1 == 127.0.0.1 || $1 == localhost || $1 == &quot;$(hostname)&quot; || $1 == &quot;$(hostname -f 2&gt;/dev/null)&quot; ]] &amp;&amp; return 0 +└── pbs-prune
-  hostname -I 2&gt;/dev/null | tr &#x27; &#x27; &#x27;\n&#x27; | grep -qxF &quot;$1&quot; +
-}+
  
-# ------------------------------------------------------------------ pré-checagem +/var/log/ 
-preflight() { +└── pbs-prune.log 
-  step &quot;Verificando pré-requisitos&quot; +</code>
-  [[ $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 +Arquivos principais:
-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 +^ Arquivo ^ Função ^ 
-    info &quot;Datastores disponíveis:&quot; +| ''/usr/local/sbin/pbs-prune'' | Executor do prune | 
-    proxmox-backup-manager datastore list 2&gt;/dev/null | sed &#x27;s/^/     /&#x27; +| ''/etc/pbs-prune/pbs-prune.conf'' | Configuração | 
-  fi +| ''/etc/pbs-prune/groups.conf'' | Retenção por VM/CT | 
-  ask DATASTORE &quot;Nome do datastore&quot; &quot;$DATASTORE&quot; v_name +| ''/etc/pbs-prune/secret'' | Secret do API Token | 
-  ask NAMESPACE &quot;Namespace (Enter = raiz)&quot; &quot;$NAMESPACE&quot; v_ns+| ''/etc/cron.d/pbs-prune'' | Agendamento | 
 +| ''/var/log/pbs-prune.log'' | Log das execuções |
  
-  tip &quot;Recomendado: token dedicado, ex.: root@pam!prune&quot; +===== Configuração das VMs/CTs =====
-  ask AUTH_ID &quot;Auth-ID do token (usuario@realm!token)&quot; &quot;$AUTH_ID&quot; v_authid+
  
-  setup_token +Arquivo:
-  setup_fingerprint +
-  test_connection || step_connection +
-}+
  
-setup_token() { +<code> 
-  local user=${AUTH_ID%%!*} tok=${AUTH_ID#*!} opt out acl_path+/etc/pbs-prune/groups.conf 
 +</code>
  
-  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 +Exemplo:
-    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() { +<code> 
-  local fp +vm/100 7 
-  info &quot;Obtendo o fingerprint do certificado de ${PBS_HOST}:${PBS_PORT}...&quot; +vm/101 15 
-  fp=$(fetch_fingerprint || true) +ct/200 30 
-  if [[ -n $fp ]]; then +</code>
-    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() { +Formato:
-  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 +<code> 
-print_groups() { +GRUPO KEEP-LAST 
-  local i +</code>
-  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; } +===== Comandos =====
-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 +Simular:
-  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() { +<code bash> 
-  step &quot;2/5 · VMs/CTs e retenção&quot; +pbs-prune --dry-run 
-  if (( ${#AVAILABLE[@]} )); then +</code>
-    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 +Executar:
-  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 +<code bash> 
-    if idx=$(group_index &quot;$group&quot;); then +pbs-prune 
-      warn &quot;$group já está na lista (keep-last ${G_KEEP[$idx]}).&quot; +</code>
-      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 +Listar grupos:
-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 +<code bash> 
-step_schedule() { +pbs-prune --list 
-  step &quot;4/5 · Agendamento&quot; +</code>
-  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 +Acompanhar log:
-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; +<code bash>
-[[ -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=(); [[ -n ${NAMESPACE:-} ]] &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} · $((SECONDS - start))s =====&quot; +
- +
-(( errors == 0 )) +
-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 +
-  -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 $CONF_FILE &amp;&amp; -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> +
-<div class="actions"> +
-<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> +
-</div> +
-<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 tail -f /var/log/pbs-prune.log
-&lt;/code&gt; +</code>
- +
-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} +
-  [[ ${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 &gt;= 1 &amp;&amp; $1 &lt;= 65535 )) || { 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()       { [[ -z $1 || $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()     { [[ $1 =~ ^([01]?[0-9]|2[0-3]):[0-5][0-9]$ ]] || { warn &quot;Horário no formato HH:MM (ex.: 03:30).&quot;; return 1; }; } +
-v_hours()    { [[ $1 =~ ^([1-9]|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 [[ ${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=$((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 +
-  -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=(); [[ -n ${NAMESPACE:-} ]] &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} · $((SECONDS - start))s =====&quot; +
- +
-(( errors == 0 )) +
-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 +===== Garbage Collection =====
-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() { +O prune remove as referências dos snapshots.
-  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() { +O espaço físico no datastore será efetivamente recuperado após a execução do **Garbage Collection (GC)**.
-  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() { +Por isso, recomenda-se executar na ordem:
-  step_connection +
-  step_groups +
-  step_dryrun +
-  step_schedule +
-  step_review_install +
-  finish +
-}+
  
-# ------------------------------------------------------------------ main +<code> 
-case &quot;${1:-}&quot; in +Backup → Prune Individual → Garbage Collection 
-  -h|--help)    sed -n &#x27;2,32p&#x27; &quot;$0&quot; | sed &#x27;s/^# \{0,1\}//&#x27;; exit 0 ;; +</code>
-  -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 +===== Desinstalação =====
-preflight +
-if [[ -f $CONF_FILE &amp;&amp; -f $GROUPS_FILE ]]; then existing_menu; else full_flow; fi +
-&lt;/code&gt;+
  
-===== Licença =====+<code bash> 
 +/root/pbs-prune-setup.sh --uninstall 
 +</code>
  
-&lt;code&gt; +A desinstalação remove o executor, configurações, cron e logrotate.
-MIT +
-&lt;/code&gt; +
-</pre> +
-</section> +
-</main>+
  
-<footer><div class="wrap">PBS Prune Individual · Projeto Empresa Livre · v1.0.0 · Licença MIT</div></footer> +O API Token criado no PBS não é removido automaticamente.
-</body> +
-</html>+
proxmox/prune-individual.1790543451.txt.gz · Última modificação: por admin

Donate Powered by PHP Valid HTML5 Valid CSS Driven by DokuWiki