Tot el que has construït passa en un sol servidor. Però en producció n'hi ha tres —srv-veloz-01, srv-veloz-02 i srv-veloz-03—, i comprovar l'estat de la flota entrant a mà a cadascun no és automatització: és el mateix d'abans amb més finestres de terminal. ssh és l'eina que converteix un script local en una ordre que s'executa en qualsevol màquina, i amb ella el toolkit fa l'últim salt del mòdul. Veuràs SSH des del punt de vista d'un script —bastant diferent del d'una persona escrivint en un terminal—, com autenticar-se sense que ningú teclegi una contrasenya i sense obrir cap forat, i com recórrer una flota en paral·lel sense que un servidor caigut tombi el recorregut.

Contingut

  1. ssh des d'un script: quin shell obtens i quin codi retorna
  2. Autenticació per clau
  3. Limitar el que una clau automatitzada pot fer
  4. ~/.ssh/config i la reutilització de connexió
  5. Les opcions que un script necessita
  6. known_hosts ben gestionat
  7. Blocs remots amb here-document
  8. Enviar scripts i transferir fitxers
  9. Recórrer la flota, en sèrie i en paral·lel
  10. sudo remot i quan Bash deixa de ser la resposta
  11. Aplicació: neix flota.sh

  1. ssh des d'un script: quin shell obtens i quin codi retorna

Sense ordre, ssh obre una sessió interactiva. Amb ordre (ssh veloz@srv-veloz-02 'uptime') passa una cosa diferent i molt important: s'executa un shell no interactiu i no d'inici de sessió, exactament la mateixa situació que cron (07-01).

Sessió interactiva ssh host ssh host ordre
Llegeix ~/.bash_profile o ~/.bashrc? No (llevat de BASH_ENV, rar)
Àlies i funcions del teu perfil? / PATH Sí / el del teu perfil No / el mínim del sistema
Hi ha TTY? No, llevat de -t

Comprova-ho amb ssh veloz@srv-veloz-02 'echo "$PATH"': respon /usr/local/bin:/usr/bin:/bin, i no el que veus en entrar a mà. La conseqüència és la mateixa que amb cron, i la solució també: camins absoluts sempre, i si necessites configuració, carrega-la explícitament. Un ssh srv-veloz-02 'estat-servei.sh' fallarà amb «command not found» encara que l'script funcioni perfectament quan hi entres a mà. I com que no hi ha TTY, una ordre remota que pregunti alguna cosa es queda penjada o falla: s'aplica la propietat 1 de 07-02, res interactiu, ara també a l'altre costat del cable. ssh retorna el codi de sortida de l'ordre remota, cosa magnífica: tota la teva lògica d'errors de 05-03 viatja per la xarxa. Però hi ha una excepció crítica: el 255 és el codi que ssh fa servir per als seus propis errors —host inabastable, autenticació rebutjada, clau desconeguda—. Distingir-lo és el que separa «el servidor està caigut» de «el servei del servidor està caigut», dues incidències molt diferents:

sortida=$(ssh -n -o BatchMode=yes veloz@srv-veloz-02 'estat-servei.sh' 2>&1) && codi=0 || codi=$?
case "$codi" in
    0)   veloz_log_info  "srv-veloz-02: OK" ;;
    255) veloz_log_error "srv-veloz-02: INACCESSIBLE (${sortida%%$'\n'*})" ;;
    *)   veloz_log_warn  "srv-veloz-02: ha fallat amb codi $codi" ;;
esac

L'ambigüitat és teòrica però real: si la teva ordre remota retornés 255 pel seu compte, no ho podries distingir; per això els codis convencionals de 05-03 es queden per sota de 128.

  1. Autenticació per clau

Un script no pot teclejar una contrasenya; l'autenticació per clau pública ho resol i a més és més segura.

ssh-keygen -t ed25519 -C "veloz-ops a srv-veloz-01" -f ~/.ssh/id_veloz_ops
ssh-copy-id -i ~/.ssh/id_veloz_ops.pub veloz@srv-veloz-02

-t ed25519 tria l'algorisme recomanat avui: claus curtes, ràpides i sòlides (RSA de 4096 bits val si el destí és antic). -f dona un nom propi, per no barrejar la clau d'automatització amb la teva personal; es generen id_veloz_ops (privada, mai no surt de la màquina) i id_veloz_ops.pub (pública, la que es copia). ssh-copy-id l'afegeix al ~/.ssh/authorized_keys del destí i arregla els permisos, cosa important perquè SSH és estricte amb ells i falla en silenci si no li agraden: ~/.ssh a 700, authorized_keys i la clau privada a 600, i ~ sense escriptura per a grup ni altres —si no, qualsevol podria reemplaçar .ssh—. La frase de pas és la decisió incòmoda: una clau protegida és més segura, però algú l'ha d'escriure. ssh-agent desa la clau desxifrada a memòria durant la teva sessió (eval "$(ssh-agent)" i ssh-add ~/.ssh/id_veloz_ops), cosa perfecta per a la feina interactiva i inútil per a una tasca de cron a les 03:00, quan no hi ha ni sessió ni agent. Les tasques automàtiques fan servir claus sense frase de pas, i cal ser honest sobre el risc: qui aconsegueixi aquell fitxer té accés a tots els destins on està autoritzada. Com que el risc no es pot evitar, s'acota: permisos 600 i propietari correcte, una clau diferent per finalitat per revocar-la sense afectar la resta, i restriccions al destí.

  1. Limitar el que una clau automatitzada pot fer

Una entrada d'authorized_keys admet opcions davant de la clau que limiten el que s'hi pot fer. És la diferència entre una clau que dona accés total al servidor i una que només serveix per al que vas dir:

from="10.0.0.11",command="/home/veloz/veloz-ops/bin/despatxador-remot.sh",no-port-forwarding,no-agent-forwarding,no-pty ssh-ed25519 AAAAC3Nz... veloz-ops

from= impedeix fer servir la clau des d'una altra adreça; no-port-forwarding impedeix fer servir el servidor com a túnel cap a la xarxa interna; no-agent-forwarding impedeix aprofitar el teu agent per saltar a altres màquines; no-pty nega el terminal interactiu; i command= obliga a executar sempre aquella ordre, sigui quina sigui la que demani el client. Aquesta última és la més potent i la que més s'oblida: encara que algú robi la clau, l'única cosa que pot fer és llançar aquell script. L'ordre que el client va demanar queda a $SSH_ORIGINAL_COMMAND, cosa que permet un petit despatxador que només accepti el que està previst:

# despatxador-remot.sh — unica ordre permesa per la clau d'operacions
case "${SSH_ORIGINAL_COMMAND:-}" in
    estat) exec /home/veloz/veloz-ops/bin/estat-servei.sh ;;
    copia) exec /home/veloz/veloz-ops/bin/copia.sh ;;
    *)     printf 'ordre no permesa\n' >&2; exit 64 ;;
esac

El case amb llista tancada (04-05) és el correcte: enumera el que està permès en lloc de filtrar el que és perillós. I mai no passis $SSH_ORIGINAL_COMMAND a eval ni a un shell; seria regalar l'execució arbitrària que acabes d'evitar (08-03).

  1. ~/.ssh/config i la reutilització de connexió

Repetir usuari, port i clau a cada crida és soroll; ~/.ssh/config (permisos 600) ho centralitza.

Host srv-veloz-*
    User veloz
    IdentityFile ~/.ssh/id_veloz_ops
    IdentitiesOnly yes
    ConnectTimeout 5
    ServerAliveInterval 15
    ControlMaster auto
    ControlPath ~/.ssh/cm-%r@%h:%p
    ControlPersist 60
Host srv-veloz-03
    HostName 10.0.2.33
    ProxyJump bastion.veloz.example

Host srv-veloz-* s'aplica a tots els que casin amb el patró, i els blocs més específics s'hi sumen. IdentitiesOnly yes evita que SSH ofereixi totes les teves claus abans de la correcta, cosa que pot provocar «massa intents d'autenticació»; ServerAliveInterval detecta una connexió morta en lloc de deixar-la penjada; i ProxyJump salta a través d'una màquina intermèdia sense túnels a mà. ControlMaster és la joia per als bucles. Amb auto, la primera connexió a un host obre un sòcol de control a ControlPath i les següents reutilitzen aquella connexió en lloc de negociar de nou; ControlPersist 60 manté el sòcol un minut després de l'última. Establir una sessió SSH costa entre 200 i 500 ms, així que un bucle amb deu ordres sobre tres servidors passa de diversos segons a gairebé res:

executar time ssh srv-veloz-02 true dues vegades seguides dona 0m0,412s la primera i 0m0,038s la segona, deu vegades més ràpida perquè no s'ha tornat a autenticar. Un avís: si el sòcol queda orfe, ssh -O exit srv-veloz-02 el tanca.

  1. Les opcions que un script necessita

Opció Què fa Per què és imprescindible
-n Entrada estàndard des de /dev/null Mira més avall: és **l'**error clàssic
-o BatchMode=yes No pregunta mai; falla en lloc seu Sense ella es penja demanant contrasenya
-o ConnectTimeout=5 Màxim per connectar Un host caigut no pot penjar la tasca
-o StrictHostKeyChecking=accept-new Accepta hosts nous, rebutja canvis Apartat 6

El problema del -n mereix el seu propi paràgraf, perquè desconcerta tothom el primer cop. A while read -r host; do ssh "$host" 'uptime'; done < servidors.txt només es processa el primer servidor, i la raó és que ssh llegeix l'entrada estàndard per enviar-la a l'ordre remota. Dins del bucle, l'entrada estàndard és servidors.txt, així que el primer ssh s'empassa la resta del fitxer i el read de la segona volta no hi troba res: el bucle acaba després d'un sol servidor, sense error i sense explicació. La solució és ssh -n, un idioma que cal memoritzar igual que el while IFS= read -r de 04-01. L'alternativa és fer servir el descriptor 3 (done < servidors.txt 3<&0 i read -u 3), però -n és més simple. I compte: només es posa -n quan no vols alimentar l'ordre remota; si li estàs passant dades per una canonada, no hi ha de ser.

  1. known_hosts ben gestionat

La primera vegada que et connectes a una màquina, SSH pregunta si n'acceptes l'empremta. Aquella pregunta penja un script, i la temptació és -o StrictHostKeyChecking=no, cosa que és una mala idea: desactiva la comprovació que impedeix que un atacant es faci passar pel teu servidor. Que sigui comú no la fa correcta. Les dues maneres correctes:

# a) Acceptar hosts nous, pero seguir rebutjant canvis de clau
ssh -o StrictHostKeyChecking=accept-new -n veloz@srv-veloz-02 'uptime'
# b) Registrar les claus per endavant, en l'aprovisionament
ssh-keyscan -t ed25519 srv-veloz-01 srv-veloz-02 srv-veloz-03 >> ~/.ssh/known_hosts

accept-new és el punt mitjà raonable: no pregunta la primera vegada, però si la clau d'un host canvia —senyal d'un atac o d'una reinstal·lació— falla sorollosament, que és justament el que vols. La (b) és més rigorosa i l'adequada per a un entorn seriós: les claus es recullen un cop, es verifiquen i es distribueixen amb la resta de la configuració. Quan reinstal·lis un servidor i la clau canviï legítimament, ssh-keygen -R srv-veloz-02 esborra l'entrada antiga.

  1. Blocs remots amb here-document

Per a diverses ordres remotes, ficar-les en una cadena amb ; es torna il·legible; la manera neta fa servir un here-document (05-05) alimentant un shell remot.

ssh -o BatchMode=yes veloz@srv-veloz-02 bash -s <<'EOF'
set -euo pipefail
cd /srv/veloz/dades
printf 'srv-veloz-02: %s enviaments, %s lliures\n' "$(wc -l < enviaments.csv)" "$(df -h --output=avail /srv|tail -1)"
EOF

bash -s li diu al Bash remot que llegeixi l'script de la seva entrada estàndard, i el here-document l'hi proporciona. Fixa't que aquí no s'hi posa -n: l'entrada estàndard és precisament el canal per on viatja l'script. I les cometes simples del delimitador són la part crítica d'aquest idioma, perquè decideixen on s'expandeixen les variables. Amb <<'EOF' el text viatja literal i $var s'expandeix al destí; amb <<EOF sense cometes, el Bash local expandeix abans d'enviar. Compara-ho:

data=2026-08-03
ssh srv-veloz-02 bash -s <<'EOF'
echo "remot: $(hostname), data: $data"      # -> remot: srv-veloz-02, data:
EOF
ssh srv-veloz-02 bash -s <<EOF
echo "remot: \$(hostname), data: $data"     # -> remot: srv-veloz-02, data: 2026-08-03
EOF

En el primer cas, $(hostname) s'executa al destí (correcte) i $data surt buida perquè aquella variable no existeix allà. En el segon, $data l'expandeix el shell local abans d'enviar i \$(hostname) va escapada per executar-se al destí. Barrejar els dos mons produeix errors desconcertants, així que la recomanació és fer servir sempre <<'EOF' i passar els valors locals com a arguments, més segur i més llegible:

ssh srv-veloz-02 bash -s -- "$data" "$llindar" <<'EOF'
set -euo pipefail
data="$1"; llindar="$2"
awk -F, -v f="$data" -v u="$llindar" '$2 ~ f && $5 == "incidencia" { c++ }
    END { print (c + 0 >= u ? "ALERTA" : "OK"), c + 0 }' /srv/veloz/dades/enviaments.csv
EOF

Els arguments després de -- arriben a l'script remot com a $1, $2… Així no hi ha cap expansió local que s'hi coli, i un valor amb espais o cometes no pot alterar el significat del codi remot: la mateixa defensa contra la injecció que aplicaves amb awk -v (06-01) i jq --arg (06-05).

  1. Enviar scripts i transferir fitxers

Quan la feina remota passa de vint línies deixa de tenir sentit incrustar-la.

# a) Enviar per l'entrada estandard, sense deixar rastre al desti
ssh -o BatchMode=yes veloz@srv-veloz-02 'bash -s' -- --dry-run < ~/veloz-ops/bin/comprova-node.sh
# b) Copiar, executar i netejar conservant el codi de sortida
scp -q ~/veloz-ops/bin/comprova-node.sh veloz@srv-veloz-02:/tmp/ && ssh -n veloz@srv-veloz-02 \
    'bash /tmp/comprova-node.sh; rc=$?; rm -f /tmp/comprova-node.sh; exit $rc'

L'opció (a) és elegant i la que convé per defecte: no toca el disc remot, així que no cal netejar res ni queda una versió antiga oblidada. La (b) cal quan l'script ha de persistir; fixa't que desa el codi de sortida abans d'esborrar, per no perdre'l amb el rm. Per a fitxers, scp està bé per a un de sol, però per a directoris rsync és superior en tot —transfereix només el que ha canviat, preserva permisos, permet excloure, es reprèn i accepta --link-dest—:

rsync -az --delete -e 'ssh -o BatchMode=yes -o ConnectTimeout=5' /copies/diaries/ srv-veloz-02:/copies/n01/

-e fixa el transport amb totes les opcions de la connexió, i -z sí que té sentit aquí perquè va per xarxa (recorda de 07-03 que en local no aportava res). Això completa la regla 3-2-1: la còpia que surt del servidor. I per fer-ho bé, la clau de la còpia remota es restringeix al destí amb command="rrsync -wo /copies", un embolcall que només permet a rsync escriure sota aquell camí: l'apartat 3 aplicat al cas més freqüent.

  1. Recórrer la flota, en sèrie i en paral·lel

El requisit central: que un servidor caigut no avorti el recorregut. Amb set -e actiu, un ssh fallit mataria l'script al primer host inaccessible i perdries la informació dels altres dos. L'idioma ordre && codi=0 || codi=$? és el que neutralitza set -e per a aquella ordre i captura el codi alhora (05-03), i a partir d'aquí un case com el de l'apartat 1 distingeix el 255 de la resta. Tres servidors en sèrie, a dos segons cadascun, són sis segons; amb trenta, un minut. En paral·lel, el temps és el del més lent. Reprenent 05-02, amb un advertiment important: si diversos processos escriuen alhora a la mateixa sortida, les línies s'entrebarregen, així que cada branca escriu al seu propi fitxer i es consolida al final. Quatre decisions fan que el recorregut paral·lel de l'apartat 11 funcioni. Cada branca de fons escriu el seu codi i la seva sortida en fitxers separats dins d'un mktemp -d, perquè una subshell no pot retornar valors al pare per variables. El wait || true espera totes i evita que el codi diferent de zero d'una branca avorti l'script. La consolidació recorre la llista de servidors en el seu ordre original, així que l'informe surt determinista encara que els nodes responguin en qualsevol ordre —un informe l'ordre del qual canvia cada dia és incomparable—. I un trap … EXIT neteja el temporal. Per a flotes grans convé limitar la concurrència: xargs -P 10 (05-01) és la manera més simple de no passar de deu connexions simultànies.

  1. sudo remot i quan Bash deixa de ser la resposta

Si l'ordre remota necessita privilegis, el problema és que sudo voldrà una contrasenya i no hi ha cap terminal on escriure-la. La manera correcta és autoritzar una ordre concreta sense contrasenya al destí, amb una línia veloz ALL=(root) NOPASSWD: /usr/bin/systemctl restart veloz-api a /etc/sudoers.d/veloz-ops. Amb això, ssh -n srv-veloz-02 'sudo /usr/bin/systemctl restart veloz-api' funciona sense preguntar. Camí absolut, sense comodins i per a un usuari concret: un NOPASSWD: ALL equival a donar la contrasenya de root a qui robi la clau. L'alternativa sudo -S, que llegeix la contrasenya de l'entrada estàndard, obliga a enviar-la per una canonada, que és justament el que intentàvem evitar. I aquí apareix requiretty: algunes configuracions de sudoers exigeixen un terminal, cosa que fa fallar tot sudo remot no interactiu amb «sorry, you must have a tty to run sudo»; s'arregla amb Defaults:veloz !requiretty al destí. Forçar el TTY amb ssh -t és la solució bruta, perquè a més barreja stdout i stderr i desordena la sortida. Cal saber reconèixer el límit. Amb tres servidors i comprovacions simples, flota.sh és perfecte: no hi ha res per instal·lar, és transparent i l'entén qualsevol. Però si comences a necessitar plantilles de configuració, gestió de paquets, control que l'estat desitjat es compleix, inventaris dinàmics o execució sobre cinquanta màquines, estàs reescrivint malament el que Ansible ja fa bé —i per SSH, sense agent, amb el mateix model mental—; pssh/parallel-ssh és l'esglaó intermedi per llançar la mateixa ordre en molts hosts. El senyal d'alarma és clar: quan el teu script de Bash comença a portar a dins una llista de paquets per instal·lar i plantilles de fitxers de configuració, ha deixat de ser un script d'operacions.

  1. Aplicació: neix flota.sh

L'últim script del toolkit executa estat-servei.sh als tres servidors en paral·lel i consolida el resultat.

#!/usr/bin/env bash
# flota.sh — Estat consolidat de la flota. QUAN: cada 15 min (temporitzador).
# Codis: 0 tota la flota OK | 1 algun node amb problema | 2 algun node inaccessible
set -euo pipefail
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin; export LC_ALL=C
readonly BASE="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
. "$BASE/lib/comu.sh"; [[ -r $BASE/etc/veloz-ops.conf ]] && . "$BASE/etc/veloz-ops.conf"
VELOZ_COMPONENT=flota
readonly -a FLOTA=(${VELOZ_FLOTA:-srv-veloz-01 srv-veloz-02 srv-veloz-03})
readonly REMOT=/home/veloz/veloz-ops/bin/estat-servei.sh
readonly -a SSH_OPC=(-n -o BatchMode=yes -o ConnectTimeout=5 -o ControlMaster=auto
                     -o StrictHostKeyChecking=accept-new -o ControlPersist=30
                     -o ControlPath="$HOME/.ssh/cm-%r@%h:%p")
estat_text() { case "$1" in 0) printf OK ;; 255) printf INACCESSIBLE ;; *) printf "FALLADA($1)" ;; esac; }

main() {
    veloz_requereix ssh; local tmp host cod pitjor=0
    tmp=$(mktemp -d "${TMPDIR:-/tmp}/flota.XXXXXX"); trap 'rm -rf "$tmp"' EXIT
    for host in "${FLOTA[@]}"; do
        { local sor c; sor=$(timeout 60s ssh "${SSH_OPC[@]}" "$host" "$REMOT" 2>&1) && c=0 || c=$?
          printf '%s\n' "$c" > "$tmp/$host.cod"; printf '%s\n' "$sor" > "$tmp/$host.sor"; } &
    done
    wait || true
    printf '%-14s %-12s %s\n' NODE ESTAT DETALL
    for host in "${FLOTA[@]}"; do
        cod=$(< "$tmp/$host.cod"); printf '%-14s %-12s %s\n' "$host" \
            "$(estat_text "$cod")" "$(head -1 "$tmp/$host.sor")"
        case "$cod" in
            0)   veloz_log_info  "$host: OK" ;;
            255) veloz_log_error "$host: inaccessible"; pitjor=2 ;;
            *)   veloz_log_warn  "$host: codi $cod"; (( pitjor < 1 )) && pitjor=1 ;;
        esac
    done
    veloz_log_info "FI, pitjor estat: $pitjor"; return "$pitjor"
}
main "$@"
NODE           ESTAT        DETALL
srv-veloz-01   OK           veloz-api sana, 14 enviaments a la cua
srv-veloz-03   INACCESSIBLE ssh: connect to host srv-veloz-03 port 22: timed out

Repassa les peces: les opcions de SSH agrupades en un array per no repetir-les ni perdre l'entrecometat; ControlMaster per reutilitzar la connexió; timeout 60s a més de ConnectTimeout, perquè el primer cobreix l'ordre remota penjada i el segon només la connexió; branques en paral·lel escrivint a fitxers separats; consolidació en ordre fix; i codis que distingeixen els tres desenllaços. Com que retorna 0/1/2, flota.sh encaixa com una comprovació més de vigilant.sh (07-04) o com un .service amb el seu temporitzador (07-05).

Errors Habituals i Consells

  • Oblidar el -n en un bucle. L'ssh s'empassa el fitxer d'entrada i només es processa el primer host, sense cap error visible.
  • Suposar que el destí llegeix el .bashrc, o fer servir StrictHostKeyChecking=no. El primer dona «command not found» tot i funcionar en entrar a mà (camins absoluts); el segon desactiva l'única defensa contra la suplantació (fes servir accept-new o ssh-keyscan).
  • Sense BatchMode=yes o sense ConnectTimeout. El primer penja l'script esperant una contrasenya que ningú no escriurà; el segon deixa que un host caigut bloquegi el recorregut durant minuts.
  • Confondre el 255 amb una fallada de l'aplicació. «El servidor no respon» i «el servei del servidor falla» són incidències diferents.
  • Delimitador de here-document sense cometes, o diverses branques paral·leles escrivint a la mateixa sortida. El primer expandeix les variables al lloc equivocat; el segon entrebarreja les línies (un fitxer per branca i consolidar). I NOPASSWD: ALL converteix una clau robada en accés total: autoritza ordres concretes amb camí absolut.
  • Consell: prova sempre ssh -n host 'ordre' a mà abans de ficar-ho en un bucle, i afegeix-hi -v si alguna cosa no quadra: la traça d'ssh -v diu exactament quina clau es va oferir, què va rebutjar el servidor i quin fitxer de configuració es va aplicar.

Exercicis

Exercici 1. Aquest bucle només processa el primer servidor i, quan un està caigut, avorta. Troba els quatre problemes i reescriu-lo.

set -e
while read -r host; do ssh "$host" "uptime; df -h /"; done < ~/veloz-ops/etc/flota.txt

Exercici 2. Escriu veloz_remot per a lib/comu.sh: executa una ordre en un host remot amb totes les opcions d'automatització, distingeix inaccessible de fallada d'aplicació amb codis diferents, i limita la durada total.

Solucions

Solució 1. Els quatre problemes: (a) falta el -n, així que el primer ssh consumeix el fitxer i el bucle acaba; (b) set -e avorta tan bon punt un host falla, i es perden els altres; (c) sense BatchMode ni ConnectTimeout, un host caigut penja el recorregut o demana contrasenya; (d) sense IFS= read -r i sense filtrar línies buides o comentades, un fitxer d'inventari normal trenca el bucle.

while IFS= read -r host; do
    [[ -z $host || $host == \#* ]] && continue
    if sortida=$(ssh -n -o BatchMode=yes -o ConnectTimeout=5 "$host" 'uptime; df -h /' 2>&1); then
        printf '=== %s ===\n%s\n' "$host" "$sortida"
    else
        printf '=== %s === ERROR (%s): %s\n' "$host" "$?" "${sortida%%$'\n'*}"
    fi
done < ~/veloz-ops/etc/flota.txt

L'if al voltant de l'ssh és el que permet conviure amb set -e (05-03): una fallada dins de la condició d'un if no avorta l'script. I la sortida es captura en una variable per etiquetar-la amb el nom del host, en lloc de deixar que tres servidors escriguin entrebarrejats.

Solució 2.

# veloz_remot <host> <ordre...>
# Codis: 0 ok | 69 host inaccessible | 124 s'ha exhaurit el temps | altre = de l'ordre remota
veloz_remot() {
    local host="${1:?falta el host}" limit="${VELOZ_SSH_LIMIT:-60}" sortida codi; shift
    sortida=$(timeout "${limit}s" ssh -n -o BatchMode=yes -o ConnectTimeout=5 \
        -o StrictHostKeyChecking=accept-new -o ControlMaster=auto \
        -o ControlPath="$HOME/.ssh/cm-%r@%h:%p" "$host" "$@" 2>&1) && codi=0 || codi=$?
    case "$codi" in
        0)   printf '%s\n' "$sortida"; return 0 ;;
        124) veloz_log_error "$host: exhaurits els ${limit}s"; return 124 ;;
        255) veloz_log_error "$host: inaccessible (${sortida%%$'\n'*})"; return 69 ;;
        *)   veloz_log_warn "$host: l ordre ha fallat amb codi $codi"
             printf '%s\n' "$sortida" >&2; return "$codi" ;;
    esac
}

El 255 es tradueix a 69 (EX_UNAVAILABLE), el mateix codi que feia servir veloz_api_get a 06-05 per a «el servei no està disponible»: així qui hi crida tracta igual una API caiguda i un servidor caigut, que operativament és el mateix. La resta de codis es propaguen tal qual, de manera que la lògica d'errors de l'script remot arriba intacta al local. I la sortida correcta va a stdout mentre els diagnòstics van a stderr (02-04), així que dades=$(veloz_remot srv-veloz-02 cat /etc/os-release) captura només el que és útil.

Conclusió

ssh host ordre et dona un shell no interactiu i sense perfil: el mateix problema d'entorn que cron, amb la mateixa solució —camins absoluts i configuració carregada explícitament— i sense res interactiu a l'altre costat. Retorna el codi de l'ordre remota, llevat del 255, que són els errors del mateix ssh i s'han de distingir sempre. L'autenticació és per clau (ssh-keygen -t ed25519, ssh-copy-id, permisos 700/600 estrictes); en automatització va sense frase de pas, i com que el risc no es pot eliminar s'acota: una clau per finalitat i restriccions a authorized_keys amb command=, from=, no-port-forwarding i no-pty. Centralitza el que és repetitiu a ~/.ssh/config amb User, IdentityFile, IdentitiesOnly, ProxyJump i sobretot ControlMaster/ControlPersist, que reutilitza la connexió i fa un bucle deu vegades més ràpid. Les quatre opcions imprescindibles: -n —perquè ssh llegeix l'entrada estàndard i dins d'un while read s'empassa el fitxer, processant un sol host sense donar cap error—, -o BatchMode=yes perquè falli en lloc de preguntar, -o ConnectTimeout perquè un host caigut no bloquegi el recorregut, i StrictHostKeyChecking=accept-new (o ssh-keyscan per endavant) en lloc del perillós no. Per a blocs remots, ssh host bash -s <<'EOF' amb cometes al delimitador i els valors locals passats com a arguments després de --: és el que decideix on s'expandeixen les variables i el que impedeix que una dada alteri el codi. Envia scripts per l'entrada estàndard abans que copiar-los, fes servir rsync -az -e ssh en lloc de scp per a directoris —cosa que a més completa la regla 3-2-1 de 07-03—, recorre la flota capturant el codi de cada host perquè un de caigut no avorti els altres, paral·lelitza amb & i wait escrivint a fitxers separats i consolidant en ordre fix, autoritza sudo remot per ordres concretes —mai NOPASSWD: ALL— i reconeix el límit: quan apareguin plantilles i inventaris, Ansible fa millor aquella feina.

Amb flota.sh es tanca el Mòdul 7 i el toolkit de Veloz Envíos. Ja no espera les teves ordres: informe-diari.sh publica el seu informe cada matí encara que el servidor passés la nit apagat, copia.sh copia, verifica, reté i replica fora del servidor, vigilant.sh comprova vuit coses cada cinc minuts i avisa un sol cop per incidència, i flota.sh consulta els tres servidors en paral·lel. I aquí apareix el problema següent, que és d'una altra naturalesa: això ja són uns quants centenars de línies repartides en cinc scripts i una llibreria, escrites al llarg de set mòduls, que executen tasques privilegiades, manegen claus SSH i toquen dades de clients. Funciona, però és mantenible? És segur? Com saps que un canvi no trenca res? El Mòdul 8 hi respon: codi llegible (08-01), optimització del que de debò importa (08-02), seguretat (08-03), control de versions amb Git (08-04), anàlisi estàtica amb ShellCheck i shfmt (08-05), proves automatitzades amb Bats (08-06) i portabilitat enfront de POSIX (08-07). El toolkit ja funciona sol; toca convertir-lo en programari del qual fiar-se.

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