A 07-03 va néixer copia.sh: copiava /srv/veloz/dades amb rsync --link-dest, comprimia, verificava amb sha256sum i aplicava una retenció senzilla. N'hi havia prou per no perdre les dades del dia, però no per dormir tranquil. Aquest projecte el porta a la seva versió definitiva i digna de confiança: perfils configurables sense tocar el codi, manifest verificable, retenció amb promoció a setmanal i mensual, còpia remota, xifratge opcional, esborrat segur i —el més important— una prova de restauració automàtica que s'executa sola cada setmana. El principi que ordena tot el projecte: una còpia de seguretat no verificada no existeix.

Contingut

  1. Inventari: què es copia, amb quin RPO i RTO
  2. La regla 3-2-1 aplicada a Veloz Envíos
  3. Disseny: perfils en fitxers, no en el codi
  4. Fase 1: la còpia incremental que es restaura com a completa
  5. Fase 2: manifest i verificació
  6. Fase 3: retenció amb promoció
  7. Fase 4: restauració i la seva prova automàtica
  8. Còpia remota i xifratge
  9. Espai en disc: comprovar abans de començar
  10. Observabilitat i codis de sortida
  11. Posada en producció i runbook

  1. Inventari: què es copia, amb quin RPO i RTO

L'inventari s'escriu abans que el codi, i es revisa cada trimestre:

Conjunt Camí Mida RPO RTO Per què
Dades operatives /srv/veloz/dades 6 GB 1 dia 1 h Reconstruir-les exigiria demanar els CSV als clients
Històric /srv/veloz/dades/historic 40 GB 1 mes 1 dia Immutable; només canvia en tancar el mes
Configuració ~/veloz-ops/etc, /etc/veloz 2 MB 1 dia 15 min Petit i crític: sense ell no arrenca res
Sistema operatiu No es copia: es reinstal·la des de la recepta

RPO és quantes dades acceptem perdre; RTO, quant triguem a tornar. Les dues columnes són les que decideixen la freqüència i el disseny, i l'última fila és la més important: copiar el sistema sencer multiplica el cost i no millora cap dels dos números.

  1. La regla 3-2-1 aplicada a Veloz Envíos

Tres còpies, dos mitjans, una a fora (07-03):

Còpia On Com Freqüència
1 (les dades vives) srv-veloz-01:/srv/veloz/dades
2 srv-veloz-01:/copies rsync --link-dest Diària, 03:15
3 srv-veloz-03:/copies/srv-veloz-01 rsync -e ssh Diària, després de la còpia 2
A fora Emmagatzematge extern tar + gpg -c mensual Mensual, dia 1

La còpia 2 al mateix disc que les dades no compleix la regla per si sola —un disc que mor se les endú totes dues— però és la que dóna un RTO d'una hora, perquè restaurar de local és instantani. La 3 cobreix la mort del servidor; la de fora, l'incendi i l'esborrat maliciós.

  1. Disseny: perfils en fitxers, no en el codi

L'error de 07-03 era tenir els camins dins de l'script: afegir un conjunt exigia editar codi, i editar codi exigeix revisió i desplegament. La versió definitiva llegeix perfils d'etc/copia.d/*.conf:

# etc/copia.d/10-dades.conf
NOM=dades
ORIGEN=/srv/veloz/dades/
DESTI=/copies
EXCLUSIONS=/tmp/*:*.swp:historic/
RETENCIO_DIARIES=7; RETENCIO_SETMANALS=4; RETENCIO_MENSUALS=6
VERIFICA_GLOB='*.csv'

El prefix numèric fixa l'ordre. Es carreguen en bucle, cadascun en una subshell perquè les variables d'un perfil no contaminin el següent (01-04):

for perfil in "$ETC/copia.d"/*.conf; do
  [[ -r $perfil ]] || continue
  ( set -a; . "$perfil"; set +a
    copia_perfil ) || fallades+=("$(basename "$perfil")")
done

set -a marca per a exportació tot el que es defineixi a continuació, i la subshell aïlla. L'alternativa —carregar-ho tot en el mateix àmbit— provoca el bug clàssic: el perfil 20 hereta la RETENCIO_MENSUALS del 10 perquè es va oblidar de definir-la. Amb la subshell, cada perfil arrenca dels valors per defecte de l'script, que és la precedència de 05-06: defecte < fitxer < entorn < opcions.

  1. Fase 1: la còpia incremental que es restaura com a completa

copia_perfil() {
  local avui previa enllac=() excl
  avui=$(date +%F)
  previa=$(ultima_correcta "$DESTI/$NOM")
  [[ -n $previa ]] && enllac=(--link-dest="$DESTI/$NOM/$previa")
  excl=$(mktemp); trap 'rm -f "$excl"' RETURN
  printf '%s\n' ${EXCLUSIONS//:/ } > "$excl"
  rsync -a --delete --exclude-from="$excl" "${enllac[@]}" ${SIMULACIO:+--dry-run} \
        "$ORIGEN" "$DESTI/$NOM/$avui.parcial/" || return 1
  mv "$DESTI/$NOM/$avui.parcial" "$DESTI/$NOM/$avui"
}

Quatre decisions que valen per tot el projecte. --link-dest (07-03) enllaça dur el que no ha canviat: la còpia d'avui ocupa el que ocupen els fitxers nous, però conté l'arbre complet, així que restaurar és copiar un directori i no reconstruir una cadena —amb el requisit que origen i destí dels enllaços siguin al mateix sistema de fitxers—. I enllac=() és un array (04-03), no una cadena: el primer dia no hi ha còpia prèvia, l'array queda buit i l'opció desapareix de l'ordre; si fos una cadena buida entre cometes, rsync rebria un argument buit i fallaria.

${SIMULACIO:+--dry-run} (03-06) afegeix l'opció només si la variable té valor. El mode simulació és obligatori aquí perquè hi ha un --delete pel mig: abans de la primera execució real d'un perfil nou sempre s'executa amb --simula. I el sufix .parcial amb mv final és l'escriptura atòmica de 08-03: mentre rsync treballa, el directori té un nom que ultima_correcta ignora; només el mv —atòmic dins del mateix sistema de fitxers— el converteix en còpia vàlida. Si el servidor s'apaga a mitges, queda brossa identificable, mai una còpia incompleta que sembli bona.

  1. Fase 2: manifest i verificació

Un directori amb fitxers no demostra res. El manifest és la prova:

escriu_manifest() {
  local dir=$1 m="$1/MANIFEST"
  {
    printf '# copia %s de %s\norigen\t%s\n' "$NOM" "$(date -Is)" "$ORIGEN"
    printf 'fitxers\t%s\n' "$(find "$dir" -type f ! -name MANIFEST | wc -l)"
    printf 'bytes\t%s\n' "$(du -sb "$dir" | cut -f1)"
    (cd "$dir" && find . -name "$VERIFICA_GLOB" -print0 | xargs -0 -r sha256sum)
  } > "$m.parcial" && mv "$m.parcial" "$m"
}

verifica() {
  local dir=$1 esperats reals
  [[ -r $dir/MANIFEST ]] || { veloz_log_error "sense manifest: $dir"; return 1; }
  esperats=$(awk -F'\t' '$1=="fitxers" {print $2}' "$dir/MANIFEST")
  reals=$(find "$dir" -type f ! -name MANIFEST | wc -l)
  (( esperats == reals )) || { veloz_log_error "recompte: $esperats vs $reals"; return 1; }
  (cd "$dir" && grep -E '^[0-9a-f]{64} ' MANIFEST | sha256sum -c --quiet) || return 1
  veloz_log_info "verificat: $dir ($reals fitxers)"
}

Es verifiquen tres coses, de la més barata a la més cara: que el manifest existeixi, que el recompte coincideixi i que les sumes dels fitxers clau quadrin. Només es calculen sumes de VERIFICA_GLOB (els CSV) i no dels 40 GB de l'històric: verificar-ho tot cada nit costaria mitja hora i no aporta res, perquè els fitxers enllaçats amb --link-dest són les mateixes dades ja verificades ahir. El sha256sum -c --quiet retorna un codi diferent de zero si alguna cosa no quadra, i això n'hi ha prou per marcar el perfil com a fallit.

  1. Fase 3: retenció amb promoció

La retenció no és «esborrar el que és vell», és decidir quina còpia es converteix en setmanal i quina en mensual abans que caduqui com a diària.

aplica_retencio() {
  local base=$1 d
  # Promocio: la copia del dilluns passa a setmanals; la del dia 1, a mensuals.
  for d in "$base/diaries"/*; do
    [[ -d $d ]] || continue
    local f=${d##*/}
    [[ $(date -d "$f" +%u) == 1 ]] && enllaca_si_falta "$d" "$base/setmanals/$f"
    [[ $(date -d "$f" +%d) == 01 ]] && enllaca_si_falta "$d" "$base/mensuals/$f"
  done
  poda "$base/diaries" "$RETENCIO_DIARIES"
  poda "$base/setmanals" "$RETENCIO_SETMANALS"
  poda "$base/mensuals" "$RETENCIO_MENSUALS"
}

poda() {  # $1 = directori, $2 = quantes conservar
  local dir=$1 conservar=$2 copies=() sobren
  mapfile -t copies < <(find "$dir" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | sort)
  (( ${#copies[@]} > conservar + 3 )) && { veloz_log_error "$dir: massa copies, revisa"; return 1; }
  sobren=$(( ${#copies[@]} - conservar ))
  for ((i = 0; i < sobren; i++)); do
    veloz_log_info "esborrant ${copies[i]}"
    [[ $SIMULACIO == si ]] || rm -rf "${dir:?dir buit}/${copies[i]:?copia buida}"
  done
}

La promoció fa servir enllaços durs (cp -al, dins d'enllaca_si_falta), no còpies: la setmanal del 3 d'agost comparteix dades amb la diària del mateix dia, així que promocionar costa zero bytes, i quan la diària s'esborri, la setmanal continuarà contenint les dades completes. És la mateixa propietat de --link-dest aplicada a la retenció. Hi ha a més dues salvaguardes de 08-03. La primera, ${dir:?} i ${copies[i]:?}: si per un error de configuració dir quedés buida, rm -rf "/${copies[i]}" esborraria des de l'arrel; amb :? l'script mor abans d'executar res. La segona, l'avís quan hi ha moltes més còpies de les esperades: significa que l'script fa dies que no poda i alguna cosa va malament; esborrar quinze directoris de cop sense avisar és justament el que no volem que faci tot sol.

  1. Fase 4: restauració i la seva prova automàtica

El subcomandament restaura mai escriu sobre l'original:

restaura() {  # $1 = perfil, $2 = data o "ultima", $3 = desti
  local base=$ARREL/$1 data=$2 desti=${3:-$(mktemp -d /tmp/restaura-XXXXXX)}
  [[ $data == ultima ]] && data=$(ultima_correcta "$base/diaries")
  local origen=$base/diaries/$data
  [[ -d $origen ]] || veloz_morir 3 "no existeix la copia $1/$data"
  verifica "$origen" || veloz_morir 4 "copia corrupta, no es restaura"
  rsync -a "$origen/" "$desti/"
  printf 'Restaurat %s (%s) a %s\n' "$1" "$data" "$desti"
}

Es verifica abans de restaurar: restaurar una còpia corrupta sobre dades bones converteix un incident en un desastre. I el destí per defecte és un temporal, de manera que cal un acte deliberat per escriure a /srv. La prova de restauració automàtica és la peça que separa aquest projecte d'un simple script de còpia:

prova_restauracio() {
  local tmp; tmp=$(mktemp -d); trap 'rm -rf "$tmp"' RETURN
  restaura dades ultima "$tmp" >/dev/null || return 1
  local orig=/srv/veloz/dades/enviaments.csv rest=$tmp/enviaments.csv
  [[ -f $rest ]] || { veloz_log_error "prova: falta enviaments.csv"; return 1; }
  cmp -s "$orig" "$rest" || { veloz_log_error "prova FALLIDA: el CSV difereix"; return 1; }
  veloz_log_info "prova de restauracio OK ($(wc -l < "$rest") linies)"
}

Corre cada diumenge amb el seu propi timer i compara el CSV recuperat amb l'original mitjançant cmp -s. Pot donar un fals negatiu si el fitxer va canviar entre la còpia i la prova; per això es compara contra el fitxer tal com estava —o s'accepta el fals positiu i s'investiga— però mai de la vida es desactiva la prova perquè deixi d'avisar. Una còpia de seguretat no verificada no existeix; una de verificada però mai restaurada, tampoc.

  1. Còpia remota i xifratge

sincronitza_remot() {
  rsync -a --delete -e 'ssh -o BatchMode=yes' \
        "$ARREL/" "copia@srv-veloz-03:/copies/$(hostname -s)/"
}
xifra_mensual() {  # $1 = tar mensual
  gpg --batch --yes --passphrase-file "$CLAU" -c --cipher-algo AES256 "$1" && shred -u "$1"
}

BatchMode=yes (07-06) fa que SSH falli en lloc d'esperar una contrasenya que ningú no teclejarà a les tres de la matinada. Al remot es fa servir una clau restringida amb command= a authorized_keys, el mínim privilegi de 08-03: aquesta clau només pot rebre un rsync, no obrir una sessió. La clau de xifratge, per la seva banda, viu en un fitxer amb permisos 600 fora del repositori i fora de la mateixa còpia. Xifrar la còpia i guardar-hi la clau a dins és l'acudit més vell de l'ofici; i una clau que només és al servidor incendiat tampoc no serveix: la còpia de la frase de pas va al gestor de contrasenyes de l'empresa, i el runbook diu qui la pot recuperar.

  1. Espai en disc: comprovar abans de començar

Una còpia que omple el disc tomba el servei que pretenia protegir.

hi_ha_espai() {  # $1 = origen, $2 = desti
  local necessita disponible
  necessita=$(du -sb --exclude=historic "$1" | cut -f1)
  disponible=$(df -PB1 "$2" | awk 'NR==2 {print $4}')
  (( disponible > necessita * 12 / 10 ))   # 20 % de marge
}

Es demana un 20 % de marge perquè --link-dest fa que el consum real sigui molt menor que la mida de l'origen, però el marge cobreix el dia que algú reescriu el CSV sencer. Si no hi cap: no s'executa el perfil, es registra un error, s'avisa i es continua amb el perfil següent. El que no es fa mai és esborrar còpies antigues per fer lloc —això converteix un problema d'espai en una pèrdua d'historial—.

  1. Observabilitat i codis de sortida

Cada perfil produeix el seu propi codi, i l'script agrega:

Codi Significat Què fa l'operador
0 Tots els perfils bé i verificats Res
1 Un perfil ha fallat, la resta bé Revisar aquest perfil
3 / 4 Còpia no trobada / verificació fallida Comprovar la data; si és corrupció, restaurar de srv-veloz-03
5 Sense espai Ampliar disc o revisar retenció

El bucle de perfils no avorta a la primera fallada: acumula a l'array fallades i continua, perquè que el perfil de registres falli no és cap raó per quedar-se sense còpia de les dades. Al final, un resum (07-04):

resum() {
  veloz_log_info "copia acabada: ${#perfils[@]} perfils, ${#fallades[@]} fallades, ${SECONDS}s"
  (( ${#fallades[@]} == 0 )) || avisa_webhook "Copia amb fallades a $(hostname -s): ${fallades[*]}"
}

SECONDS (04-06) dóna la durada sense restar marques de temps, i la seva tendència és una dada valuosa: si la còpia passa de tres a trenta minuts, alguna cosa ha canviat encara que el codi de sortida sigui 0.

  1. Posada en producció i runbook

[Timer]                          # systemd/veloz-copia.timer
OnCalendar=*-*-* 03:15
RandomizedDelaySec=300
Persistent=true

Persistent=true (07-05) recupera l'execució si el servidor estava apagat a les 03:15, que és justament quan més falta fa la còpia. El RandomizedDelaySec evita que els tres servidors colpegin srv-veloz-03 al mateix segon. El servei executa copia.sh --tots amb flock (07-02) perquè dues execucions no se solapin mai.

El runbook (docs/runbook.md) conté l'única seqüència que importa a les quatre de la matinada, amb ordres copiables: comprovar l'última còpia correcta, verificar-la, restaurar a un temporal, comparar, i només llavors moure a producció. Un runbook que no s'ha executat mai és ficció: es prova al simulacre trimestral.

Errors Habituals i Consells

  • Confondre còpia amb còpia de seguretat. Un rsync --delete a un disc muntat al mateix servidor replica el rm -rf accidental en segons. Cal retenció, i la retenció és la que converteix la còpia en còpia de seguretat.
  • --link-dest entre sistemes de fitxers diferents. No falla: fa còpies completes en silenci fins a omplir el disc. I sense .parcial + mv, un tall de llum deixa un directori incomplet que la retenció compta com a bo i que el dia de la restauració descobreixes buit.
  • rm -rf "$dir/$copia" sense :?. El dia que una variable quedi buida, l'script esborra des de l'arrel. És la línia més perillosa de tot el toolkit.
  • Verificar la còpia amb la mateixa còpia. Desar el manifest només dins de la còpia detecta corrupció de dades, no un esborrat del conjunt: per això el recompte i la mida també es registren al log central.
  • Consell: apunta al calendari una restauració real cada trimestre, amb cronòmetre. Si l'RTO mesurat supera l'RTO promès, el disseny està malament, no l'operador.

Exercicis

  1. Còpia de base de dades. Afegeix un perfil postgres que faci servir pg_dump -Fc a un fitxer abans de la còpia, amb la contrasenya a ~/.pgpass (600) i verificació amb pg_restore -l.
  2. Informe setmanal de l'estat de les còpies. Un subcomandament informe que recorri tots els perfils i produeixi una taula amb: última còpia correcta, antiguitat en dies, mida, nombre de còpies per nivell i resultat de l'última verificació, en text i JSON.
  3. Restauració parcial. Estén restaura amb --fitxer CAMI per recuperar un sol fitxer, buscant en quines còpies existeix i en quina va canviar per última vegada.

Solucions

1. La clau és que el bolcat es fa abans del rsync i es verifica al moment:

pre_copia_postgres() {
  local out=$ORIGEN/dump-$(date +%F).dump
  pg_dump -Fc -f "$out.parcial" veloz || return 1
  pg_restore -l "$out.parcial" >/dev/null || { rm -f "$out.parcial"; return 1; }
  mv "$out.parcial" "$out"
  find "$ORIGEN" -name 'dump-*.dump' -mtime +7 -delete
}

pg_dump -Fc dóna una còpia consistent encara que la base estigui en ús; copiar-ne els fitxers en calent donaria una foto inconsistent (07-03). pg_restore -l llista el contingut sense restaurar: és la verificació més barata possible que el bolcat no està truncat. S'enganxa amb un PRE_ORDRE al .conf del perfil, invocat si està definit.

2. L'informe es recolza en els manifests ja escrits, així que no cal recalcular res:

informe() {
  printf '%-12s %-12s %5s %8s %s\n' PERFIL ULTIMA DIES MIDA ESTAT
  for base in "$ARREL"/*; do
    local n=${base##*/} u; u=$(ultima_correcta "$base/diaries")
    printf '%-12s %-12s %5d %8s %s\n' "$n" "${u:-cap}" \
      "$(( ( $(date +%s) - $(date -d "$u" +%s) ) / 86400 ))" "$(du -sh "$base" | cut -f1)" \
      "$(verifica "$base/diaries/$u" >/dev/null 2>&1 && echo OK || echo FALLADA)"
  done
}

La columna de dies és la que de debò es mira: una còpia «correcta» de fa nou dies és una alerta, encara que verifiqui bé.

3. Recórrer les còpies de més nova a més antiga amb find "$base" -path "*/$CAMI", i com que els fitxers no modificats comparteixen inode gràcies a --link-dest, comparar el número d'inode (stat -c %i) entre còpies consecutives identifica exactament en quina va canviar, sense llegir ni un sol byte del contingut.

Conclusió

copia.sh ja no és un script de còpia: és un sistema. Els perfils d'etc/copia.d/*.conf permeten afegir un conjunt sense tocar codi; rsync --link-dest dóna còpies que es generen com a incrementals i es restauren com a completes; el patró .parcial + mv garanteix que mai existeixi una còpia a mitges que sembli bona; el manifest amb recompte, mida i sumes converteix «hi ha fitxers» en «hi ha aquests fitxers i són aquests»; la retenció promociona amb enllaços durs abans de podar i esborra amb ${var:?} com a xarxa de seguretat; i la prova de restauració setmanal respon sola, cada diumenge, a l'única pregunta que importa: es pot recuperar? Al voltant, el que ja havies après: subshells per aïllar perfils (01-04), arrays per a opcions opcionals (04-03), mktemp i trap (05-02), escriptura atòmica i mínim privilegi (08-03), BatchMode a SSH (07-06), timer amb Persistent=true (07-05) i avís només quan hi ha fallades (07-04).

A 09-04 passem de protegir les dades a vigilar el servei. Construirem monitor-xarxa.sh: definir de manera mesurable què significa «la xarxa va bé», un catàleg d'objectius en fitxer, comprovacions que retornen 0/1/2 a l'estil dels connectors clàssics, execució en paral·lel perquè tot és espera de xarxa, reintents amb retrocés, alertes només en les transicions i un històric TSV del qual surt la disponibilitat percentual.

Curs de Programació en Bash

Mòdul 1: Introducció a Bash

Mòdul 2: Ordres Bàsiques de Bash

Mòdul 3: Fonaments de Scripting

Mòdul 4: Scripting Intermedi

Mòdul 5: Tècniques Avançades de Scripting

Mòdul 6: Treballar amb Eines Externes

Mòdul 7: Automatització i Programació

Mòdul 8: Bones Pràctiques i Optimització

Mòdul 9: Projectes del Món Real

© Copyright 2026. Tots els drets reservats