Els teus scripts ja funcionen. Però «funcionar» és el que fan al migdia, amb el teu terminal obert, les teves variables carregades i tu mirant la sortida. Producció és una altra cosa: són les 4:20 de la matinada, no hi ha ningú al davant, el PATH és el pobre de cron, el disc està més ple que ahir, la xarxa va estranya i l'única prova del que va passar serà el que el teu script hagi deixat escrit. Aquesta lliçó tanca el mòdul amb la llista de comprovació que salva aquella distància i amb el projecte que la demostra: desplegar.sh, l'script de desplegament de Tramontana Reserves, amb validació, còpia prèvia, canvi atòmic i reversió automàtica si l'aplicació no respon. És l'examen final del mòdul, i fa servir absolutament tot el que has après.

Contingut

  1. La llista de comprovació de producció
  2. Estructura canònica i main "$@"
  3. Configuració: la precedència, implementada
  4. Secrets
  5. Registre: què, com i on
  6. Sortida per a humans i per a màquines
  7. Bloqueig, temps límit i solapament
  8. Notificacions: silenci si tot va bé
  9. Proves, versionatge i documentació
  10. Quan deixar Bash
  11. Projecte: desplegar.sh

  1. La llista de comprovació de producció

Requisit Per què
Rutes absolutes, sense dependre del directori de treball Cron arrenca a $HOME amb un PATH mínim
Mode estricte i trap de neteja Ningú no recollirà els temporals per tu
Idempotent i amb --dry-run Per poder rellançar-lo després d'una fallada sense pensar-hi
Bloqueig i temps límit Dos alhora, o un de penjat, fan més mal que cap
Registre amb data, decisions i durada És l'única prova del que va passar
Codis de sortida documentats Qui el crida ha de poder actuar sense llegir-ne la sortida
Sense secrets al codi ni a la línia d'ordres ps els ensenya a qualsevol
Silenci si tot va bé Una alerta que sona diàriament deixa de llegir-se
shellcheck net i provat en un entorn de proves Una fallada de sintaxi a les 4:20 no l'arregla ningú
Versionat a git, amb --help i un README D'aquí a un any no recordaràs per què

Cap no és opcional en un script que corre sol: els deu junts són la diferència entre una automatització i una bomba de rellotgeria.

  1. Estructura canònica i main "$@"

La plantilla completa, evolució de la de 04-01. Copia-la tal qual:

#!/usr/bin/env bash
#
# nom.sh - Què fa, en una línia.
# Autor : Nom <correu>   Data: 2026-08-18   Versió: 1.0.0
# Ús    : nom.sh [-h] [-v] [-q] [-n] <argument>
# Sortida: 0 ok | 2 ús incorrecte | 69 servei no disponible | 75 ja en marxa

set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib/comuns.sh
source "${SCRIPT_DIR}/lib/comuns.sh"

# --- Constants -----------------------------------------------------------
readonly VERSIO_SCRIPT="1.0.0"
readonly BLOQUEIG="/var/lock/nom.lock"
LLINDAR=80                # configuració: 1. valor per defecte

# --- Funcions ------------------------------------------------------------
us() { ... };  validar() { ... };  feina() { ... }

# --- Punt d'entrada ------------------------------------------------------
main() {
    carregar_config          # 2. fitxer
    analitzar_opcions "$@"   # 4. arguments (l'entorn, 3, va amb les constants)
    validar; feina
}
main "$@"        # <- l'ÚLTIMA línia del fitxer

main al final permet que el fitxer es llegeixi de dalt a baix com un text: primer les constants, després les peces, i al final el resum del que fa el programa. Com que Bash necessita que les funcions estiguin definides abans de cridar-les, main "$@" ha de ser l'última línia, i posar-la allà protegeix a més d'una fallada real: si el fitxer es trunca en copiar-lo, Bash no executarà un script a mitges, perquè la crida a main haurà desaparegut amb la resta. I va amb cometes, sempre, pel que saps de 04-03: és l'única manera que els arguments arribin intactes.

  1. Configuració: la precedència, implementada

La regla de 04-03 —arguments > entorn > fitxer > defectes— s'implementa aplicant-la en aquell ordre, de la més feble a la més forta:

LLINDAR_DISC=80                                    # 1. defecte
carregar_config() {                                # 2. fitxer
    local f="${CONFIG:-/etc/tramontana/desplegar.conf}" clau valor
    [[ -r $f ]] || return 0
    while IFS='=' read -r clau valor; do
        clau="${clau// /}"
        [[ -z $clau || $clau == \#* ]] && continue
        case "$clau" in
            llindar_disc) LLINDAR_DISC="$valor" ;;
            url_salut)    URL_SALUT="$valor" ;;
            *) log "clau desconeguda a $f: $clau" ;;
        esac
    done < "$f"
}
LLINDAR_DISC="${TRAMONTANA_LLINDAR_DISC:-$LLINDAR_DISC}"   # 3. entorn
# 4. arguments: el bucle de getopts, que s'executa després

${VAR:-$ACTUAL} pren el valor de l'entorn si existeix i, si no, conserva el que ja hi hagués: aquell petit truc és tot el mecanisme. I registra les claus desconegudes en comptes d'ignorar-les, perquè un llindar_dsic mal escrit que no diu res és una nit perduda.

  1. Secrets

Tres regles, en ordre d'importància:

  • Mai a l'script. Un fitxer versionat a git amb una contrasenya a dins està compromès per sempre, encara que l'esborris: queda a l'historial.
  • Mai a la línia d'ordres. mysql -p"$CLAU" és visible a ps aux per a qualsevol usuari de la màquina mentre duri l'ordre. Sí en un fitxer amb permisos 600, propietat de l'usuari del servei, o en una variable d'entorn del gestor de serveis.
llegir_secret() {                   # llegir_secret RUTA -> imprimeix el secret
    local f="$1" mode
    [[ -r $f ]] || morir 78 "no puc llegir el fitxer de secrets: $f"
    mode=$(stat -c '%a' "$f")       # 600 o 640, mai llegible per altres
    [[ $mode == 600 || $mode == 640 ]] || morir 78 "permisos insegurs ($mode)"
    < "$f" tr -d '\n'
}

Un mínim digne: res de contrasenyes al codi i verificació activa dels permisos. La gestió seriosa —pass, un gestor de secrets, certificats TLS— és matèria de 06-05.

  1. Registre: què, com i on

Un script de producció registra cinc coses: que ha començat (amb la seva versió i els seus paràmetres), cada decisió que pren, cada canvi que fa al sistema, que ha acabat, i quant ha trigat. Amb això es reconstrueix qualsevol incident.

readonly LOG="/var/log/tramontana/desplegament.log"
registrar() {                       # registrar NIVELL MISSATGE...
    local nivell="$1" linia; shift
    linia="$(date '+%Y-%m-%dT%H:%M:%S%z') [$nivell] ${0##*/}[$$]: $*"
    printf '%s\n' "$linia" >> "$LOG"
    (( TRAMONTANA_SILENCIOS )) || printf '%s\n' "$linia" >&2
}
# I així queda el registre:
2026-08-18T14:02:11+0200 [INFO] desplegar.sh[4471]: inici v1.0.0, versio=3.3.0
2026-08-18T14:02:19+0200 [WARN] desplegar.sh[4471]: salut KO, revertint
2026-08-18T14:02:23+0200 [INFO] desplegar.sh[4471]: fi codi=69 durada=12s

Tres detalls del format. La data ISO 8601 amb zona horària (%Y-%m-%dT%H:%M:%S%z) s'ordena alfabèticament igual que cronològicament i no és ambigua en el canvi d'hora. El PID entre claudàtors permet separar dues execucions entrellaçades. I el missatge va alhora al fitxer i a stderr, perquè serveixi igual a cron que a mà.

La durada es mesura amb SECONDS, la variable que Bash incrementa sola: SECONDS=0 en començar i $SECONDS en acabar. I una nota de frontera: al Mòdul 5 substituirem aquest fitxer pel journal, amb logger -t tramontana -p daemon.info com a pont i journalctl per consultar-lo (05-06), més logrotate perquè el fitxer no creixi sense fi.

  1. Sortida per a humans i per a màquines

Un script útil serveix dos públics: la Marta vol llegir, la monitoració vol analitzar. La solució no és triar, és oferir totes dues:

case "$FORMAT" in
    text) LC_ALL=C printf '%-10s %s\n' "Estat:" "$estat" ;;
    json) LC_ALL=C printf '{"estat":"%s","versio":"%s","durada":%d}\n' \
              "$estat" "$versio" "$SECONDS" ;;
esac

Amb --quiet se suprimeix tot el que és informatiu i només queden els errors —imprescindible per a cron, que envia per correu qualsevol sortida— i amb --json la sortida és una sola línia analitzable. Si el teu JSON comença a tenir estructures imbricades, aquell és el senyal de l'apartat 10: en Bash no hi ha manera decent d'escapar cometes dins de valors arbitraris.

  1. Bloqueig, temps límit i solapament

Ja tens el flock sobre el descriptor 9 de 04-06. Falta l'altra meitat: què passa si una execució triga més que l'interval entre execucions. La còpia nocturna es llança a les 04:20; si un dia triga 25 hores, la de demà trobarà el bloqueig. Tres respostes, en ordre de preferència:

  • Abandonar amb un codi propi (75) i un avís. És el correcte per a còpies i informes: la de demà ja ho farà.
  • Esperar amb flock -w 300 9, si el solapament és breu i esperar és acceptable. I mai matar l'anterior des de la nova: si cal això, el problema és l'interval, no el bloqueig.

A l'execució cal posar-li sostre, perquè un procés penjat no falla: es queda allà, ocupant el bloqueig per sempre.

# 20 minuts de límit; -k 30 envia SIGKILL si als 30 s no ha mort.
timeout -k 30 20m tar -czf "$arxiu" -C "$TEMPORAL" . \
    || morir 73 "la compressió ha superat el temps límit"

timeout retorna 124 quan talla per temps, cosa que permet distingir-ho d'una fallada normal. És l'aplicació directa de la regla SIGTERM → esperar → SIGKILL de 03-06.

  1. Notificacions: silenci si tot va bé

El principi és curt i s'incompleix sempre: un script que avisa quan tot va bé ensenya la gent a ignorar els seus avisos. Si la Marta rep cada matí un correu de «còpia correcta», a la tercera setmana l'arxiva sense llegir, i el dia que hi posi «còpia FALLIDA» tampoc no el llegirà. La regla operativa: notifica només quan hi hagi alguna cosa a fer, i que el missatge digui què ha passat, quin impacte té i què s'espera de qui el llegeix.

notificar() {                       # notificar ASSUMPTE COS
    (( NOTIFICAR )) || return 0
    printf '%s\n\nServidor: %s\nRegistre: %s\n' "$2" "$(hostname)" "$LOG" \
        | mail -s "[Tramontana] $1" "$DESTINATARI"
}
esperar_salut || { notificar "Desplegament revertit" \
    "La versió ${versio} no ha respost al 8080; s'ha restaurat ${previ}."; }

Perquè el silenci no es confongui amb «l'script no s'ha executat», la contrapartida és una marca d'èxit: l'última línia del registre amb la data d'avui, o un fitxer ULTIM_EXIT amb la marca de temps. Comprovar «la marca és d'avui?» és trivial i detecta el 90 % dels problemes, inclòs el pitjor: que la tasca hagi deixat d'executar-se.

  1. Proves, versionatge i documentació

Abans que un script toqui producció:

  1. shellcheck sense avisos i bash -n, tots dos obligatoris a CI: find scripts -name '*.sh' -exec shellcheck {} +.
  2. --dry-run complet contra el sistema real, llegint la sortida sencera: és l'última oportunitat de veure el que passarà. I en un entorn de proves amb les mateixes dades i rutes, amb instantània de la VM abans de cada prova destructiva.
  3. El camí de fallada, no només el d'èxit: treu permisos al destí, apunta la URL de salut a un port tancat, interromp amb Ctrl+C a mitges. Si només ho has provat quan tot va bé, no ho has provat.
  4. Amb l'entorn de cron, que és diferent del teu: env -i /home/operador/scripts/desplegar.sh -n 3.3.0 /tmp/app.tgz reprodueix la pobresa de variables que tindrà a les 4:20.

El versionatge no és opcional: ~/scripts és un repositori git, amb un commit per canvi i un missatge que digui per què. Desplegar l'script és aleshores git pull al servidor —o millor, copiar-lo amb rsync des d'un repositori central, sense deixar credencials de git al servidor—. I la documentació mínima són tres coses: la capçalera amb ús i codis de sortida, un --help que funcioni sempre, i un README.md a la carpeta que digui quin script fa què i quin crida quin. Res més; res menys.

  1. Quan deixar Bash

Bash és excel·lent per orquestrar ordres. Deixa de ser-ho quan apareix qualsevol d'aquests senyals:

Senyal Cap a on
Més de ~300 línies, o un main que no cap en una pantalla Python
Estructures de dades imbricades, JSON més enllà de llegir un camp Python (json), o jq si és només consulta
Càlcul numèric, dates complicades, informes Python
Concurrència real amb coordinació entre tasques Python, Go
Configurar màquines, i més d'una Ansible (07-06)
Dependències entre passos, estat entre execucions Una eina d'orquestració

El senyal més honest: si estàs escrivint eval, o escapant cometes dins de cometes dins de cometes, ja has passat el límit. Per a Tramontana la frontera natural és Ansible: en el moment en què el desplegament s'hagi de fer en tres servidors alhora, desplegar.sh es converteix en un rol d'Ansible, i aquest mòdul haurà complert la seva funció, que era ensenyar-te exactament què fa aquell rol per sota.

  1. Projecte: desplegar.sh

L'examen final del mòdul. Valida, comprova precondicions, copia abans de tocar, extreu, verifica, canvia l'enllaç de manera atòmica, comprova amb curl i reverteix sol si l'aplicació no respon.

#!/usr/bin/env bash
#
# desplegar.sh - Desplegament de Tramontana Reserves amb reversió automàtica.
# Autor : Operador de sistemes <operador@srv-tramontana>  Data: 2026-08-18
# Ús    : desplegar.sh [-h] [-v] [-q] [-n] [-t SEG] <versio> <paquet.tar.gz>
# Sortida: 0 ok | 2 ús | 65 paquet invàlid | 66 origen il·legible | 73 no es
#          pot escriure | 69 l'app no ha respost (revertit) | 75 ja en marxa

set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib/comuns.sh
source "${SCRIPT_DIR}/lib/comuns.sh"
readonly VERSIO_SCRIPT="1.0.0"
readonly BASE="/opt/tramontana"; readonly RELEASES="${BASE}/releases"
readonly BLOQUEIG="/var/lock/tramontana-desplegament.lock"
readonly LOG="/var/log/tramontana/desplegament.log"
URL_SALUT="${TRAMONTANA_URL_SALUT:-http://10.0.2.15:8080/salut}"
ESPERA=60; DRY_RUN=0
versio=""; paquet=""; previ=""; enllac_canviat=0
# registrar() i us() són les de l'apartat 5 i de 04-03, sense canvis.
revertir() {                      # desfà el canvi d'enllaç si es va fer
    (( enllac_canviat )) || return 0
    registrar WARN "revertint al release $previ"
    ( cd "$BASE" && ln -sfn "releases/${previ}" app )
    enllac_canviat=0
}
netejar() {
    local codi=$?
    (( codi != 0 )) && revertir
    registrar INFO "fi codi=${codi} durada=${SECONDS}s"
    exit "$codi"
}
trap netejar EXIT
trap 'registrar ERROR "interromput per senyal"; exit 130' INT TERM
validar() {
    [[ $versio =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || morir 2 "versió invàlida: $versio"
    [[ -r $paquet ]] || morir 66 "no puc llegir el paquet: $paquet"
    # tar -tzf abans d'extreure: convenció del curs des de 02-04.
    tar -tzf "$paquet" >/dev/null 2>&1 || morir 65 "el paquet no és un tar.gz vàlid"
    [[ -w $RELEASES ]] || morir 73 "no puc escriure a $RELEASES"
    requereix_comanda tar curl timeout || morir 69 "falten dependències"
}
esperar_salut() {                 # 0 si respon 200 abans de $ESPERA segons
    local fi=$(( SECONDS + ESPERA )) codi
    while (( SECONDS < fi )); do
        codi=$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 "$URL_SALUT" || true)
        [[ $codi == 200 ]] && return 0
        sleep 3
    done
    return 1
}
main() {
    SECONDS=0
    while getopts ":hvqnt:" o; do
        case "$o" in
            h)  us; exit 0 ;;             n) DRY_RUN=1 ;;   t) ESPERA="$OPTARG" ;;
            v)  TRAMONTANA_VERBOSE=1 ;;   q) TRAMONTANA_VERBOSE=0 ;;
            \?) us >&2; morir 2 "opció desconeguda: -$OPTARG" ;;
            :)  morir 2 "l'opció -$OPTARG necessita un valor" ;;
        esac
    done
    shift $(( OPTIND - 1 ))
    [[ $# -eq 2 ]] || { us >&2; morir 2 "calen <versio> i <paquet>"; }
    versio="$1"; paquet="$2"; validar
    exec 9>"$BLOQUEIG" || morir 73 "no puc obrir $BLOQUEIG"
    flock -n 9 || morir 75 "ja hi ha un altre desplegament en marxa"
    previ=$(basename "$(readlink -f "${BASE}/app")")
    registrar INFO "inici v${VERSIO_SCRIPT}, versio=${versio}, previ=${previ}"
    [[ $versio == "$previ" ]] && { registrar INFO "ja desplegada; res a fer"; exit 0; }

    if (( DRY_RUN )); then
        registrar INFO "SIMULACIÓ: extrauria $paquet a ${RELEASES}/${versio}"
        registrar INFO "SIMULACIÓ: app -> releases/${versio}, salut a $URL_SALUT"
        exit 0
    fi
    "${SCRIPT_DIR}/copia_tramontana.sh" -q >/dev/null \
        || morir 73 "la còpia prèvia ha fallat; abandono sense tocar res"
    registrar INFO "còpia prèvia completada"
    mkdir -p "${RELEASES}/${versio}"           # idempotent
    rm -rf -- "${RELEASES:?}/${versio:?}"/*
    timeout -k 30 10m tar -xzf "$paquet" -C "${RELEASES}/${versio}"
    [[ -s "${RELEASES}/${versio}/app.jar" ]] || morir 65 "el release no porta app.jar"
    registrar INFO "release ${versio} extret ($(formatar_bytes \
        "$(du -sb "${RELEASES}/${versio}" | cut -f1)"))"
    # Canvi atòmic: ln -sfn amb destí relatiu, com mana el Mòdul 2.
    ( cd "$BASE" && ln -sfn "releases/${versio}" app )
    enllac_canviat=1
    registrar INFO "enllaç app -> releases/${versio}"
    if esperar_salut; then
        enllac_canviat=0                       # confirmat: no cal revertir
        registrar INFO "salut OK a ${URL_SALUT}"
        printf '%s\n' "$versio"                # stdout: la versió activa
        exit 0
    fi
    registrar ERROR "la versió ${versio} no ha respost en ${ESPERA}s"
    exit 69                                    # el trap EXIT farà la reversió
}
main "$@"
operador@srv-tramontana:~$ ~/scripts/desplegar.sh 3.3.0 /tmp/app-3.3.0.tar.gz
2026-08-18T14:02:11+0200 [INFO] desplegar.sh[4471]: inici v1.0.0, versio=3.3.0, previ=3.2.1
2026-08-18T14:02:14+0200 [INFO] desplegar.sh[4471]: còpia prèvia completada
2026-08-18T14:02:16+0200 [INFO] desplegar.sh[4471]: release 3.3.0 extret (99.2 MiB)
2026-08-18T14:02:16+0200 [INFO] desplegar.sh[4471]: enllaç app -> releases/3.3.0
2026-08-18T14:03:16+0200 [ERROR] desplegar.sh[4471]: la versió 3.3.0 no ha respost en 60s
2026-08-18T14:03:16+0200 [WARN] desplegar.sh[4471]: revertint al release 3.2.1
2026-08-18T14:03:16+0200 [INFO] desplegar.sh[4471]: fi codi=69 durada=65s
operador@srv-tramontana:~$ readlink /opt/tramontana/app
releases/3.2.1

El servei va continuar funcionant. Això és el que aquest mòdul ha construït: la 3.3.0 no va arrencar, l'script ho va detectar, va revertir a l'enllaç anterior en el mateix segon, ho va registrar tot i va retornar un codi (69) que diu exactament què va passar. I amb -n hauries vist abans el que anava a intentar, sense tocar res.

Repassa on viu cada lliçó: el shebang i l'exit explícit són de 04-01; ${VAR:-}, les matrius i printf de 04-02; getopts, us(), stdout enfront de stderr i els codis de sortida de 04-03; [[ =~ ]], case i el while d'espera de 04-04; SCRIPT_DIR, la biblioteca i morir() de 04-05; i set -euo pipefail, els trap, el bloqueig, la idempotència i timeout de 04-06. Res d'això no és nou: és tot l'anterior, junt.

Errors Comuns i Consells

  • Rutes relatives. Funcionen des del teu terminal i fallen des de cron: absolutes sempre, o derivades de SCRIPT_DIR. I no suposis el PATH, que a cron és /usr/bin:/bin; si fas servir alguna cosa de /usr/local/bin, ruta completa o PATH= explícit al principi.
  • Posar main "$@" sense cometes o al mig del fitxer. El primer trenca els arguments amb espais; el segon falla perquè les funcions encara no estan definides. I no notifiquis l'èxit: ensenya a ignorar les notificacions. Silenci si tot va bé, i una marca d'èxit comprovable.
  • Registrar només el final. Sense les decisions intermèdies no pots reconstruir res; registra també el que vas decidir no fer. I posa sempre temps límit: un procés penjat no falla, es queda allà amb el bloqueig agafat fins que algú el mata.
  • Desplegar sense --dry-run previ. Trenta segons de lectura contra mitja hora de restauració.
  • Consell: guarda ~/scripts a git des d'avui encara que només siguis tu, perquè git log respon a «des de quan fa això?» millor que la teva memòria. I fes que tot script imprimeixi la seva versió a la primera línia del registre: quan dos servidors es comportin diferent, serà el primer que compararàs.
  • Consell: el millor moment per escriure el README de la carpeta és avui, quan encara recordes per què existeix cada script.

Exercicis

Exercici 1. Afegeix a desplegar.sh una opció --json que, en acabar, imprimeixi per stdout una línia amb versio, previ, estat (desplegat o revertit), codi i durada. Ha de funcionar també quan hi ha reversió, sense duplicar codi.

Exercici 2. Escriu ~/scripts/comprovar_tasques.sh, que verifiqui que les tres tasques nocturnes (cron-copia.log, cron-purga.log i el desplegament) han deixat marca d'avui i notifiqui només si en falta alguna, aplicant el principi de silenci si tot va bé.

Exercici 3. En Luis vol posar aquesta línia a /etc/cron.d/tramontana: 20 4 * * * operador cd ~/scripts && ./desplegar.sh $VERSIO /tmp/app.tar.gz. Troba-hi els cinc problemes i reescriu-la.

Solucions

Solució 1. La clau és no duplicar: el punt únic de sortida ja existeix —el trap netejar EXIT— i és allà on s'ha d'imprimir el resum.

FORMAT="text"; estat="desconegut"
# Al bucle d'opcions, que passa a manual per ser una opció llarga:
#   --json) FORMAT="json"; shift ;;
resum() {                         # resum CODI
    (( $1 == 0 )) && estat="desplegat" || estat="revertit"
    case "$FORMAT" in
        json) LC_ALL=C printf \
            '{"versio":"%s","previ":"%s","estat":"%s","codi":%d,"durada":%d}\n' \
            "$versio" "$previ" "$estat" "$1" "$SECONDS" ;;
        *) printf '%s\n' "$versio" ;;
    esac
}

# A netejar(), just abans de l'exit: resum "$codi"
operador@srv-tramontana:~$ ~/scripts/desplegar.sh --json 3.3.0 /tmp/app.tgz 2>/dev/null
{"versio":"3.3.0","previ":"3.2.1","estat":"revertit","codi":69,"durada":65}

El 2>/dev/null deixa només la línia JSON, perquè el registre va a stderr: la separació de canals de 04-03 donant el seu fruit. I com que l'exit 0 del camí d'èxit també dispara l'EXIT, s'elimina el printf '%s\n' "$versio" del cos de main per no imprimir-lo dues vegades.

Solució 2.

#!/usr/bin/env bash
# comprovar_tasques.sh - Verifica que les tasques nocturnes han deixat marca d'avui.
# Autor : Operador de sistemes <operador@srv-tramontana>  Data: 2026-08-18
# Ús    : comprovar_tasques.sh [-v]   Sortida: 0 tot al dia | 1 en falta alguna
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/lib/comuns.sh"   # shellcheck source=lib/comuns.sh
readonly DESTINATARI="[email protected]"
declare -A TASQUES=( [copia]="/var/log/tramontana/cron-copia.log"
    [purga]="/var/log/tramontana/cron-purga.log"
    [desplegament]="/var/log/tramontana/desplegament.log" )
avui=$(date '+%Y-%m-%d'); falten=()
for tasca in "${!TASQUES[@]}"; do
    fitxer="${TASQUES[$tasca]}"
    # -s: existeix i no és buit. tail -1 evita llegir un registre de milers de línies.
    if [[ -s $fitxer ]] && [[ $(tail -1 "$fitxer") == "$avui"* ]]; then
        log "$tasca: al dia"
    else falten+=("$tasca"); fi
done
(( ${#falten[@]} == 0 )) && exit 0      # silenci si tot va bé
printf "Sense marca d'avui (%s): %s\n" "$avui" "${falten[*]}" >&2
printf "Les tasques %s no han deixat registre d'avui a %s.\nRevisa cron i el disc.\n" \
    "${falten[*]}" "$(hostname)" | mail -s "[Tramontana] Tasques sense executar" "$DESTINATARI"
exit 1

operador@srv-tramontana:~$ ~/scripts/comprovar_tasques.sh; echo "codi: $?"
codi: 0
operador@srv-tramontana:~$ ~/scripts/comprovar_tasques.sh -v; echo "codi: $?"
[2026-08-18 08:05:01] copia: al dia
[2026-08-18 08:05:01] purga: al dia
Sense marca d'avui (2026-08-18): desplegament
codi: 1

Sense -v i amb tot correcte l'script no imprimeix res i retorna 0: cron no envia correu i la Marta no rep soroll. El correu només s'envia al camí de fallada, dient què passa i què cal revisar, com demana la convenció del curs.

Solució 3. Els cinc problemes:

  1. ~ no s'expandeix en un crontab de sistema. El camp es passa a sh -c amb un entorn mínim i HOME pot no estar definida: rutes absolutes.
  2. $VERSIO és buida. Cron no hereta el teu entorn, així que l'script rebria un argument on n'esperava dos i la validació ho rebutjaria. Sort que sí.
  3. Falta redirigir la sortida. Sense >> registre 2>&1, qualsevol missatge es converteix en un correu i qualsevol error es perd si el correu no està configurat.
  4. Sense SHELL ni PATH declarats al principi del fitxer, i amb l'hora mal triada: les 04:20 eviten la franja del canvi horari i el solapament amb altres tasques.
  5. Desplegar automàticament per cron és una mala idea de fons. Un desplegament l'aprova la Marta; el que ha de córrer sol és la còpia.
# /etc/cron.d/tramontana
SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin
MAILTO=""
# Còpia diària. 04:20 per no solapar-se amb la rotació de registres.
20 4 * * * operador /home/operador/scripts/copia_tramontana.sh -q \
    >> /var/log/tramontana/cron-copia.log 2>&1
# Comprovació que les tasques nocturnes han deixat marca. Només avisa si falla.
5 8 * * * operador /home/operador/scripts/comprovar_tasques.sh \
    >> /var/log/tramontana/cron-copia.log 2>&1

Rutes absolutes, PATH explícit, MAILTO="" perquè l'script ja escriu el seu propi registre i només notifica quan cal, i el desplegament fora de cron: es llança a mà quan la Marta l'aprova. Al Mòdul 5 veurem que un timer de systemd fa tot això amb millor registre i millor control de solapament.

Conclusió

Has tancat el mòdul, i amb ell la distància entre «a mi em funciona» i «corre sol».

  • Tens una llista de comprovació de producció de deu punts i una plantilla canònica amb main "$@" com a última línia, que fa que l'script es llegeixi de dalt a baix i no s'executi a mitges si es trunca. Implementes la precedència de configuració —arguments > entorn > fitxer > defectes— de debò, registrant fins i tot les claus desconegudes.
  • Mantens els secrets fora del codi i de la línia d'ordres, i verifiques els permisos del fitxer que els guarda.
  • Registres inici, decisions, canvis, fi i durada, amb data ISO i PID, a fitxer i a stderr, sabent que a 05-06 això passarà al journal.
  • Produeixes sortida per a humans i per a màquines, amb --quiet i --json. Controles el solapament amb flock, poses sostre amb timeout sabent que 124 significa «s'ha esgotat el temps», i apliques el silenci si tot va bé amb una marca d'èxit comprovable com a contrapartida.
  • Proves amb shellcheck, bash -n, --dry-run, entorn de proves, env -i i el camí de fallada; versiones a git i documentes amb capçalera, --help i README.
  • Reconeixes quan deixar Bash —300 línies, JSON imbricat, concurrència, eval— i cap a on anar. I desplegar.sh existeix: valida, bloqueja, copia abans de tocar, extreu amb temps límit, canvia l'enllaç de manera atòmica, comprova la salut i reverteix sol quan la 3.3.0 no arrenca, deixant el servei dret.

A /home/operador/scripts/ hi ha ara una caixa d'eines real: revisio_salut.sh, informe_reserves.sh, resum_reserves.sh, purgar_releases.sh, copia_tramontana.sh, comprovar_tasques.sh, desplegar.sh, la biblioteca lib/comuns.sh i el seu test_comuns.sh, tots amb la mateixa estructura, els mateixos codis de sortida i la mateixa disciplina d'errors. Això ja no és «saber Bash»: és saber automatitzar.

Però fixa't en el que ha anat apareixent entre línies i hem anat ajornant. copia_tramontana.sh escriu a /var/log/tramontana/ i ningú no rota aquell fitxer. desplegar.sh canvia l'enllaç però no reinicia l'aplicació, perquè encara no saps gestionar serveis. La còpia va a /srv/tramontana/backups sense que hàgim parlat de quant disc hi ha ni de què passa quan s'acabi. El grup tramontana continua sense crear-se formalment, svc-tramontana existeix per art de màgia, i sudo l'has fet servir sense preguntar-te qui decideix què hi pots fer. Al Mòdul 5: Administració del Sistema tot això deixa de ser màgia: crearàs usuaris i grups de debò, configuraràs sudo i els permisos especials, instal·laràs i fixaràs versions de paquets, gestionaràs discos i sistemes de fitxers, convertiràs els teus scripts en serveis i timers de systemd amb arrencada, dependències i reinici automàtic, enviaràs els teus registres al journal amb rotació, mesuraràs el rendiment del servidor, i tancaràs amb una estratègia de còpia i restauració digna d'aquest nom. Els scripts que has escrit seran les peces; el Mòdul 5 és el sistema que les sosté. Actualitza la instantània de la teva VM i ens veiem allà.

Curs de Linux: De Principiant a Administrador de Sistemes

Mòdul 1: Introducció a Linux

Mòdul 2: Comandes Bàsiques de Linux

Mòdul 3: Habilitats Avançades en la Línia de Comandes

Mòdul 4: Scripting en Shell

Mòdul 5: Administració del Sistema

Mòdul 6: Xarxes i Seguretat

Mòdul 7: Temes Avançats

Mòdul 8: Projectes Pràctics

© Copyright 2026. Tots els drets reservats