Apprendre la gestion des erreurs Bash par exemple

[*]

Dans cet article, je présente quelques astuces pour gérer les conditions d'erreur. Certaines ne relèvent strictement pas de la catégorie de la gestion des erreurs (un moyen réactif de gérer les imprévus), mais également certaines techniques pour éviter les erreurs avant qu'elles ne se produisent.

Étude de cas : script simple qui télécharge un rapport matériel à partir de plusieurs hôtes et l'insère dans une base de données.

Dites que vous avez un cron travail sur chacun de vos systèmes Linux, et vous avez un script à collecter les informations matérielles de chacun :

#!/bin/bash
# Script to collect the status of lshw output from home servers
# Dependencies:
# * LSHW: http://ezix.org/project/wiki/HardwareLiSter
# * JQ: http://stedolan.github.io/jq/
#
# On each machine you can run something like this from cron (Don't know CRON, no worries: https://crontab-generator.org/)
# 0 0 * * * /usr/sbin/lshw -json -quiet > /var/log/lshw-dump.json
# Author: Jose Vicente Nunez
#
declare -a servers=(
dmaf5
)

DATADIR="$HOME/Documents/lshw-dump"

/usr/bin/mkdir -p -v "$DATADIR"
for server in ${servers[*]}; do
    echo "Visiting: $server"
    /usr/bin/scp -o logLevel=Error ${server}:/var/log/lshw-dump.json ${DATADIR}/lshw-$server-dump.json &
done
wait
for lshw in $(/usr/bin/find $DATADIR -type f -name 'lshw-*-dump.json'); do
    /usr/bin/jq '.["product","vendor", "configuration"]' $lshw
done

Si tout se passe bien, alors vous collectez vos fichiers en parallèle car vous n'avez pas plus de dix systèmes. Vous pouvez vous permettre de les connecter à tous en même temps, puis d'afficher les détails matériels de chacun.

Visiting: dmaf5
lshw-dump.json                                                                                         100%   54KB 136.9MB/s   00:00    
"DMAF5 (Default string)"
"BESSTAR TECH LIMITED"
{
  "boot": "normal",
  "chassis": "desktop",
  "family": "Default string",
  "sku": "Default string",
  "uuid": "00020003-0004-0005-0006-000700080009"
}

Voici quelques-unes des raisons pour lesquelles les choses se sont mal passées :

  • Votre rapport n'a pas été exécuté car le serveur était en panne
  • Vous n'avez pas pu créer le répertoire où les fichiers doivent être enregistrés
  • Les outils dont vous avez besoin pour exécuter le script sont manquants
  • Vous ne pouvez pas collecter le rapport car votre machine distante est tombée en panne
  • Un ou plusieurs rapports sont corrompus

La version actuelle du script a un problème : elle s'exécutera du début à la fin, erreurs ou non :

./collect_data_from_servers.sh 
Visiting: macmini2
Visiting: mac-pro-1-1
Visiting: dmaf5
lshw-dump.json                                                                                         100%   54KB  48.8MB/s   00:00    
scp: /var/log/lshw-dump.json: No such file or directory
scp: /var/log/lshw-dump.json: No such file or directory
parse error: Expected separator between values at line 3, column 9

Ensuite, je démontre quelques éléments pour rendre votre script plus robuste et, dans certains cas, récupérer d'un échec.

L'option nucléaire : échouer dur, échouer vite

La bonne façon de gérer les erreurs est de vérifier si le programme s'est terminé avec succès ou non, en utilisant des codes de retour. Cela semble évident mais les codes de retour, un nombre entier stocké dans bash $? ou $! variable, ont parfois un sens plus large. le page de manuel bash vous dit:

Pour les besoins du shell, une commande qui se termine avec une sortie zéro
statut a réussi. Un état de sortie de zéro indique le succès.
Un état de sortie différent de zéro indique un échec. Lorsqu'une commande
se termine sur un signal fatal N, bash utilise la valeur de 128+N comme
l'état de sortie.

Comme d'habitude, vous devriez toujours lire la page de manuel des scripts que vous appelez, pour voir quelles sont les conventions pour chacun d'eux. Si vous avez programmé avec un langage comme Java ou Python, vous connaissez probablement leurs exceptions, leurs différentes significations et la façon dont elles ne sont pas toutes gérées de la même manière.

Si vous ajoutez un ensemble -o errexit à votre script, à partir de ce moment-là, il abandonnera l'exécution si une commande existe avec un code != 0. Mais errexit n'est pas utilisé lors de l'exécution de fonctions à l'intérieur d'un if condition, donc au lieu de me souvenir de cette exception, je fais plutôt une gestion explicite des erreurs.

Jeter un coup d'œil à version deux du script. C'est un peu mieux :

1 #!/bin/bash
2 # Script to collect the status of lshw output from home servers
3 # Dependencies:
4 # * LSHW: http://ezix.org/project/wiki/HardwareLiSter
5 # * JQ: http://stedolan.github.io/jq/
6 #
7 # On each machine you can run something like this from cron (Don't know CRON, no worries: https://crontab-generator.org/        ) 
8 # 0 0 * * * /usr/sbin/lshw -json -quiet > /var/log/lshw-dump.json
9   Author: Jose Vicente Nunez
10 #
11 set -o errtrace # Enable the err trap, code will get called when an error is detected
12 trap "echo ERROR: There was an error in ${FUNCNAME-main context}, details to follow" ERR
13 declare -a servers=(
14 macmini2
15 mac-pro-1-1
16 dmaf5
17 )
18  
19 DATADIR="$HOME/Documents/lshw-dump"
20 if [ ! -d "$DATADIR" ]; then 
21    /usr/bin/mkdir -p -v "$DATADIR"|| "FATAL: Failed to create $DATADIR" && exit 100
22 fi 
23 declare -A server_pid
24 for server in ${servers[*]}; do
25    echo "Visiting: $server"
26    /usr/bin/scp -o logLevel=Error ${server}:/var/log/lshw-dump.json ${DATADIR}/lshw-$server-dump.json &
27   server_pid[$server]=$! # Save the PID of the scp  of a given server for later
28 done
29 # Iterate through all the servers and:
30 # Wait for the return code of each
31 # Check the exit code from each scp
32 for server in ${!server_pid[*]}; do
33    wait ${server_pid[$server]}
34    test $? -ne 0 && echo "ERROR: Copy from $server had problems, will not continue" && exit 100
35 done
36 for lshw in $(/usr/bin/find $DATADIR -type f -name 'lshw-*-dump.json'); do
37    /usr/bin/jq '.["product","vendor", "configuration"]' $lshw
38 done

Voici ce qui a changé :

  • Lignes 11 et 12, j'active la trace d'erreur et j'ai ajouté un « piège » pour indiquer à l'utilisateur qu'il y a eu une erreur et qu'il y a des turbulences à venir. Vous voudrez peut-être tuer votre script ici à la place, je vais vous montrer pourquoi ce n'est peut-être pas le meilleur.
  • Ligne 20, si le répertoire n'existe pas, essayez de le créer à la ligne 21. Si la création du répertoire échoue, quittez avec une erreur.
  • À la ligne 27, après avoir exécuté chaque tâche d'arrière-plan, je capture le PID et l'associe à la machine (relation 1:1).
  • Sur les lignes 33-35, j'attends le scp tâche à terminer, obtenez le code de retour, et s'il s'agit d'une erreur, abandonnez.
  • A la ligne 37, je vérifie que le fichier a pu être analysé, sinon je sors avec une erreur.

Alors, à quoi ressemble la gestion des erreurs maintenant ?

Visiting: macmini2
Visiting: mac-pro-1-1
Visiting: dmaf5
lshw-dump.json                                                                                         100%   54KB 146.1MB/s   00:00    
scp: /var/log/lshw-dump.json: No such file or directory
ERROR: There was an error in main context, details to follow
ERROR: Copy from mac-pro-1-1 had problems, will not continue
scp: /var/log/lshw-dump.json: No such file or directory

Comme vous pouvez le voir, cette version est meilleure pour détecter les erreurs mais elle est très impitoyable. De plus, il ne détecte pas toutes les erreurs, n'est-ce pas ?

Quand tu es coincé et que tu souhaites avoir une alarme

Le code a l'air mieux, sauf que parfois le scp pourrait rester bloqué sur un serveur (en essayant de copier un fichier) parce que le serveur est trop occupé pour répondre ou simplement dans un mauvais état.

Un autre exemple consiste à essayer d'accéder à un répertoire via NFS où $HOME est monté depuis un serveur NFS :

/usr/bin/find $HOME -type f -name '*.csv' -print -fprint /tmp/report.txt

Et vous découvrez des heures plus tard que le point de montage NFS est périmé et que votre script est bloqué.

Un délai d'attente est la solution. Et, Délai d'expiration GNU vient à la rescousse :

/usr/bin/timeout --kill-after 20.0s 10.0s /usr/bin/find $HOME -type f -name '*.csv' -print -fprint /tmp/report.txt

Ici, vous essayez de tuer régulièrement (signal TERM) le processus correctement après 10,0 secondes après son démarrage. S'il fonctionne toujours après 20,0 secondes, envoyez un signal KILL (kill -9). En cas de doute, vérifiez quels signaux sont pris en charge dans votre système (kill -l, par example).

Si ce n'est pas clair dans ma boîte de dialogue, regardez le script pour plus de clarté.

/usr/bin/time /usr/bin/timeout --kill-after=10.0s 20.0s /usr/bin/sleep 60s
real    0m20.003s
user    0m0.000s
sys     0m0.003s

Revenez au script d'origine pour ajouter quelques options supplémentaires et vous avez version trois:

 1 #!/bin/bash
  2 # Script to collect the status of lshw output from home servers
  3 # Dependencies:
  4 # * Open SSH: http://www.openssh.com/portable.html
  5 # * LSHW: http://ezix.org/project/wiki/HardwareLiSter
  6 # * JQ: http://stedolan.github.io/jq/
  7 # * timeout: https://www.gnu.org/software/coreutils/
  8 #
  9 # On each machine you can run something like this from cron (Don't know CRON, no worries: https://crontab-generator.org/)
 10 # 0 0 * * * /usr/sbin/lshw -json -quiet > /var/log/lshw-dump.json
 11 # Author: Jose Vicente Nunez
 12 #
 13 set -o errtrace # Enable the err trap, code will get called when an error is detected
 14 trap "echo ERROR: There was an error in ${FUNCNAME-main context}, details to follow" ERR
 15 
 16 declare -a dependencies=(/usr/bin/timeout /usr/bin/ssh /usr/bin/jq)
 17 for dependency in ${dependencies[@]}; do
 18     if [ ! -x $dependency ]; then
 19         echo "ERROR: Missing $dependency"
 20         exit 100
 21     fi
 22 done
 23 
 24 declare -a servers=(
 25 macmini2
 26 mac-pro-1-1
 27 dmaf5
 28 )
 29 
 30 function remote_copy {
 31     local server=$1
 32     echo "Visiting: $server"
 33     /usr/bin/timeout --kill-after 25.0s 20.0s 
 34         /usr/bin/scp 
 35             -o BatchMode=yes 
 36             -o logLevel=Error 
 37             -o ConnectTimeout=5 
 38             -o ConnectionAttempts=3 
 39             ${server}:/var/log/lshw-dump.json ${DATADIR}/lshw-$server-dump.json
 40     return $?
 41 }
 42 
 43 DATADIR="$HOME/Documents/lshw-dump"
 44 if [ ! -d "$DATADIR" ]; then
 45     /usr/bin/mkdir -p -v "$DATADIR"|| "FATAL: Failed to create $DATADIR" && exit 100
 46 fi
 47 declare -A server_pid
 48 for server in ${servers[*]}; do
 49     remote_copy $server &
 50     server_pid[$server]=$! # Save the PID of the scp  of a given server for later
 51 done
 52 # Iterate through all the servers and:
 53 # Wait for the return code of each
 54 # Check the exit code from each scp
 55 for server in ${!server_pid[*]}; do
 56     wait ${server_pid[$server]}
 57     test $? -ne 0 && echo "ERROR: Copy from $server had problems, will not continue" && exit 100
 58 done
 59 for lshw in $(/usr/bin/find $DATADIR -type f -name 'lshw-*-dump.json'); do
 60     /usr/bin/jq '.["product","vendor", "configuration"]' $lshw
 61 done

Quels sont les changements ? :

  • Entre les lignes 16-22, vérifiez si tous les outils de dépendance requis sont présents. S'il ne peut pas s'exécuter, alors 'Houston nous avons un problème.'
  • A créé un remote_copy fonction, qui utilise un délai d'attente pour s'assurer que le scp se termine au plus tard en 45,0 s—ligne 33.
  • Ajout d'un délai de connexion de 5 secondes au lieu du TCP par défaut—ligne 37.
  • Ajout d'une nouvelle tentative à scp à la ligne 38—3 tentatives qui attendent 1 seconde entre chacune.

Il existe d'autres façons de réessayer en cas d'erreur.

Attendre la fin du monde-comment et quand réessayer

Vous avez remarqué qu'il y a une nouvelle tentative ajoutée à la scp commander. Mais cela ne réessaye que pour les connexions ayant échoué, et si la commande échoue au milieu de la copie ?

Parfois, vous voulez simplement échouer parce qu'il y a très peu de chances de récupérer d'un problème. Un système qui nécessite des correctifs matériels, par exemple, ou vous pouvez simplement revenir à un mode dégradé, ce qui signifie que vous pouvez continuer à travailler sur votre système sans les données mises à jour. Dans ces cas, cela n'a aucun sens d'attendre indéfiniment, mais seulement pendant un laps de temps spécifique.

Voici les modifications apportées au remote_copy, pour garder ce bref (version quatre):

#!/bin/bash
# Omitted code for clarity...
declare REMOTE_FILE="/var/log/lshw-dump.json"
declare MAX_RETRIES=3

# Blah blah blah...

function remote_copy {
    local server=$1
    local retries=$2
    local now=1
    status=0
    while [ $now -le $retries ]; do
        echo "INFO: Trying to copy file from: $server, attempt=$now"
        /usr/bin/timeout --kill-after 25.0s 20.0s 
            /usr/bin/scp 
                -o BatchMode=yes 
                -o logLevel=Error 
                -o ConnectTimeout=5 
                -o ConnectionAttempts=3 
                ${server}:$REMOTE_FILE ${DATADIR}/lshw-$server-dump.json
        status=$?
        if [ $status -ne 0 ]; then
            sleep_time=$(((RANDOM % 60)+ 1))
            echo "WARNING: Copy failed for $server:$REMOTE_FILE. Waiting '${sleep_time} seconds' before re-trying..."
            /usr/bin/sleep ${sleep_time}s
        else
            break # All good, no point on waiting...
        fi
        ((now=now+1))
    done
    return $status
}

DATADIR="$HOME/Documents/lshw-dump"
if [ ! -d "$DATADIR" ]; then
    /usr/bin/mkdir -p -v "$DATADIR"|| "FATAL: Failed to create $DATADIR" && exit 100
fi
declare -A server_pid
for server in ${servers[*]}; do
    remote_copy $server $MAX_RETRIES &
    server_pid[$server]=$! # Save the PID of the scp  of a given server for later
done

# Iterate through all the servers and:
# Wait for the return code of each
# Check the exit code from each scp
for server in ${!server_pid[*]}; do
    wait ${server_pid[$server]}
    test $? -ne 0 && echo "ERROR: Copy from $server had problems, will not continue" && exit 100
done

# Blah blah blah, process the files you just copied...

A quoi ça ressemble maintenant ? Dans cette exécution, j'ai un système en panne (mac-pro-1-1) et un système sans le fichier (macmini2). Vous pouvez voir que la copie du serveur dmaf5 fonctionne tout de suite, mais pour les deux autres, il y a une nouvelle tentative pour un temps aléatoire compris entre 1 et 60 secondes avant de quitter :

INFO: Trying to copy file from: macmini2, attempt=1
INFO: Trying to copy file from: mac-pro-1-1, attempt=1
INFO: Trying to copy file from: dmaf5, attempt=1
scp: /var/log/lshw-dump.json: No such file or directory
ERROR: There was an error in main context, details to follow
WARNING: Copy failed for macmini2:/var/log/lshw-dump.json. Waiting '60 seconds' before re-trying...
ssh: connect to host mac-pro-1-1 port 22: No route to host
ERROR: There was an error in main context, details to follow
WARNING: Copy failed for mac-pro-1-1:/var/log/lshw-dump.json. Waiting '32 seconds' before re-trying...
INFO: Trying to copy file from: mac-pro-1-1, attempt=2
ssh: connect to host mac-pro-1-1 port 22: No route to host
ERROR: There was an error in main context, details to follow
WARNING: Copy failed for mac-pro-1-1:/var/log/lshw-dump.json. Waiting '18 seconds' before re-trying...
INFO: Trying to copy file from: macmini2, attempt=2
scp: /var/log/lshw-dump.json: No such file or directory
ERROR: There was an error in main context, details to follow
WARNING: Copy failed for macmini2:/var/log/lshw-dump.json. Waiting '3 seconds' before re-trying...
INFO: Trying to copy file from: macmini2, attempt=3
scp: /var/log/lshw-dump.json: No such file or directory
ERROR: There was an error in main context, details to follow
WARNING: Copy failed for macmini2:/var/log/lshw-dump.json. Waiting '6 seconds' before re-trying...
INFO: Trying to copy file from: mac-pro-1-1, attempt=3
ssh: connect to host mac-pro-1-1 port 22: No route to host
ERROR: There was an error in main context, details to follow
WARNING: Copy failed for mac-pro-1-1:/var/log/lshw-dump.json. Waiting '47 seconds' before re-trying...
ERROR: There was an error in main context, details to follow
ERROR: Copy from mac-pro-1-1 had problems, will not continue

Si j'échoue, dois-je recommencer ? Utiliser un point de contrôle

Supposons que la copie distante soit l'opération la plus coûteuse de tout ce script et que vous vouliez ou puissiez réexécuter ce script, peut-être en utilisant cron ou le faire à la main deux fois par jour pour vous assurer de récupérer les fichiers si un ou plusieurs systèmes sont en panne.

Vous pourriez, pour la journée, créer un petit « cache d'état », où vous n'enregistrez que les opérations de traitement réussies par machine. Si un système est présent, ne vous embêtez pas à vérifier à nouveau pour ce jour-là.

Certains programmes, comme Ansible, faire quelque chose de similaire et vous permettre de réessayer un playbook sur un nombre limité de machines après un échec (--limit @/home/user/site.retry).

Une nouvelle version (version cinq) du script a du code pour enregistrer l'état de la copie (lignes 15-33):

15 declare SCRIPT_NAME=$(/usr/bin/basename $BASH_SOURCE)|| exit 100
16 declare YYYYMMDD=$(/usr/bin/date +%Y%m%d)|| exit 100
17 declare CACHE_DIR="/tmp/$SCRIPT_NAME/$YYYYMMDD"
18 # Logic to clean up the cache dir on daily basis is not shown here
19 if [ ! -d "$CACHE_DIR" ]; then
20   /usr/bin/mkdir -p -v "$CACHE_DIR"|| exit 100
21 fi
22 trap "/bin/rm -rf $CACHE_DIR" INT KILL
23
24 function check_previous_run {
25  local machine=$1
26  test -f $CACHE_DIR/$machine && return 0|| return 1
27 }
28
29 function mark_previous_run {
30    machine=$1
31    /usr/bin/touch $CACHE_DIR/$machine
32    return $?
33 }

Avez-vous remarqué le piège sur la ligne 22? Si le script est interrompu (tué), je veux m'assurer que tout le cache est invalidé.

Et puis, ajoutez cette nouvelle logique d'aide dans le remote_copy fonction (lignes 52-81):

52 function remote_copy {
53    local server=$1
54    check_previous_run $server
55    test $? -eq 0 && echo "INFO: $1 ran successfully before. Not doing again" && return 0
56    local retries=$2
57    local now=1
58    status=0
59    while [ $now -le $retries ]; do
60        echo "INFO: Trying to copy file from: $server, attempt=$now"
61        /usr/bin/timeout --kill-after 25.0s 20.0s 
62            /usr/bin/scp 
63                -o BatchMode=yes 
64                -o logLevel=Error 
65                -o ConnectTimeout=5 
66               -o ConnectionAttempts=3 
67                ${server}:$REMOTE_FILE ${DATADIR}/lshw-$server-dump.json
68        status=$?
69        if [ $status -ne 0 ]; then
70            sleep_time=$(((RANDOM % 60)+ 1))
71            echo "WARNING: Copy failed for $server:$REMOTE_FILE. Waiting '${sleep_time} seconds' before re-trying..."
72            /usr/bin/sleep ${sleep_time}s
73        else
74            break # All good, no point on waiting...
75        fi
76        ((now=now+1))
77    done
78    test $status -eq 0 && mark_previous_run $server
79    test $? -ne 0 && status=1
80    return $status
81 }

La première fois qu'il s'exécute, un nouveau message pour le répertoire de cache est imprimé :

./collect_data_from_servers.v5.sh
/usr/bin/mkdir: created directory '/tmp/collect_data_from_servers.v5.sh'
/usr/bin/mkdir: created directory '/tmp/collect_data_from_servers.v5.sh/20210612'
ERROR: There was an error in main context, details to follow
INFO: Trying to copy file from: macmini2, attempt=1
ERROR: There was an error in main context, details to follow

Si vous l'exécutez à nouveau, le script sait que dma5f c'est bon, pas besoin de réessayer la copie :

./collect_data_from_servers.v5.sh
INFO: dmaf5 ran successfully before. Not doing again
ERROR: There was an error in main context, details to follow
INFO: Trying to copy file from: macmini2, attempt=1
ERROR: There was an error in main context, details to follow
INFO: Trying to copy file from: mac-pro-1-1, attempt=1

Imaginez à quel point cela s'accélère lorsque vous avez plus de machines qui ne devraient pas être revisitées.

Laisser les miettes de côté : que consigner, comment consigner et sortie détaillée

Si vous êtes comme moi, j'aime un peu de contexte pour mettre en corrélation quand quelque chose ne va pas. le echo les déclarations sur le script sont sympas, mais si vous pouviez leur ajouter un horodatage.

Si tu utilises logger, vous pouvez enregistrer la sortie sur journalctl pour un examen ultérieur (même l'agrégation avec d'autres outils là-bas). La meilleure partie est que vous montrez la puissance de journalctl tout de suite.

Alors au lieu de simplement faire echo, vous pouvez également ajouter un appel à logger comme ça en utilisant une nouvelle fonction bash appelée 'message' :

SCRIPT_NAME=$(/usr/bin/basename $BASH_SOURCE)|| exit 100
FULL_PATH=$(/usr/bin/realpath ${BASH_SOURCE[0]})|| exit 100
set -o errtrace # Enable the err trap, code will get called when an error is detected
trap "echo ERROR: There was an error in ${FUNCNAME[0]-main context}, details to follow" ERR
declare CACHE_DIR="/tmp/$SCRIPT_NAME/$YYYYMMDD"

function message {
    message="$1"
    func_name="${2-unknown}"
    priority=6
    if [ -z "$2" ]; then
        echo "INFO:" $message
    else
        echo "ERROR:" $message
        priority=0
    fi
    /usr/bin/logger --journald<<EOF
MESSAGE_ID=$SCRIPT_NAME
MESSAGE=$message
PRIORITY=$priority
CODE_FILE=$FULL_PATH
CODE_FUNC=$func_name
EOF
}

Vous pouvez voir que vous pouvez stocker des champs séparés dans le cadre du message, comme la priorité, le script qui a produit le message, etc.

Alors en quoi est-ce utile ? Eh bien, vous pourriez get les messages entre 13:26 et 13:27, uniquement les erreurs (priority=0) et uniquement pour notre script (collect_data_from_servers.v6.sh) comme ceci, sortie au format JSON :

journalctl --since 13:26 --until 13:27 --output json-pretty PRIORITY=0 MESSAGE_ID=collect_data_from_servers.v6.sh
{
        "_BOOT_ID" : "dfcda9a1a1cd406ebd88a339bec96fb6",
        "_AUDIT_LOGINUID" : "1000",
        "SYSLOG_IDENTIFIER" : "logger",
        "PRIORITY" : "0",
        "_TRANSPORT" : "journal",
        "_SELINUX_CONTEXT" : "unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023",
        "__REALTIME_TIMESTAMP" : "1623518797641880",
        "_AUDIT_SESSION" : "3",
        "_GID" : "1000",
        "MESSAGE_ID" : "collect_data_from_servers.v6.sh",
        "MESSAGE" : "Copy failed for macmini2:/var/log/lshw-dump.json. Waiting '45 seconds' before re-trying...",
        "_CAP_EFFECTIVE" : "0",
        "CODE_FUNC" : "remote_copy",
        "_MACHINE_ID" : "60d7a3f69b674aaebb600c0e82e01d05",
        "_COMM" : "logger",
        "CODE_FILE" : "/home/josevnz/BashError/collect_data_from_servers.v6.sh",
        "_PID" : "41832",
        "__MONOTONIC_TIMESTAMP" : "25928272252",
        "_HOSTNAME" : "dmaf5",
        "_SOURCE_REALTIME_TIMESTAMP" : "1623518797641843",
        "__CURSOR" : "s=97bb6295795a4560ad6fdedd8143df97;i=1f826;b=dfcda9a1a1cd406ebd88a339bec96fb6;m=60972097c;t=5c494ed383898;x=921c71966b8943e3",
        "_UID" : "1000"
}

Étant donné qu'il s'agit de données structurées, d'autres collecteurs de journaux peuvent parcourir toutes vos machines, agréger vos journaux de script, et vous disposez alors non seulement des données, mais également des informations.

Vous pouvez jeter un oeil à l'ensemble version six du scénario.

Ne soyez pas si impatient de remplacer vos données avant de les avoir vérifiées.

Si vous l'avez remarqué dès le début, j'ai copié un fichier JSON corrompu encore et encore :

Parse error: Expected separator between values at line 4, column 11
ERROR parsing '/home/josevnz/Documents/lshw-dump/lshw-dmaf5-dump.json'

C'est facile à éviter. Copiez le fichier dans un emplacement temporaire et si le fichier est corrompu, n'essayez pas de remplacer la version précédente (et laissez la mauvaise pour inspection. lignes 99-107 de la version sept du script):

function remote_copy {
    local server=$1
    check_previous_run $server
    test $? -eq 0 && message "$1 ran successfully before. Not doing again" && return 0
    local retries=$2
    local now=1
    status=0
    while [ $now -le $retries ]; do
        message "Trying to copy file from: $server, attempt=$now"
        /usr/bin/timeout --kill-after 25.0s 20.0s 
            /usr/bin/scp 
                -o BatchMode=yes 
                -o logLevel=Error 
                -o ConnectTimeout=5 
                -o ConnectionAttempts=3 
                ${server}:$REMOTE_FILE ${DATADIR}/lshw-$server-dump.json.$$
        status=$?
        if [ $status -ne 0 ]; then
            sleep_time=$(((RANDOM % 60)+ 1))
            message "Copy failed for $server:$REMOTE_FILE. Waiting '${sleep_time} seconds' before re-trying..." ${FUNCNAME[0]}
            /usr/bin/sleep ${sleep_time}s
        else
            break # All good, no point on waiting...
        fi
        ((now=now+1))
    done
    if [ $status -eq 0 ]; then
        /usr/bin/jq '.' ${DATADIR}/lshw-$server-dump.json.$$ > /dev/null 2>&1
        status=$?
        if [ $status -eq 0 ]; then
            /usr/bin/mv -v -f ${DATADIR}/lshw-$server-dump.json.$$ ${DATADIR}/lshw-$server-dump.json && mark_previous_run $server
            test $? -ne 0 && status=1
        else
            message "${DATADIR}/lshw-$server-dump.json.$$ Is corrupted. Leaving for inspection..." ${FUNCNAME[0]}
        fi
    fi
    return $status
}

Choisissez les bons outils pour la tâche et préparez votre code dès la première ligne

Un aspect très important de la gestion des erreurs est le codage approprié. Si vous avez une mauvaise logique dans votre code, aucune quantité de gestion des erreurs ne l'améliorera. Pour que cela reste bref et lié au bash, je vais vous donner ci-dessous quelques conseils.

Vous devez TOUJOURS vérifier la syntaxe des erreurs avant d'exécuter votre script :

bash -n $my_bash_script.sh

Sérieusement. Il devrait être aussi automatique que d'effectuer n'importe quel autre test.

Lisez la page de manuel bash et familiarisez-vous avec les options incontournables, telles que :

set -xv
my_complicated_instruction1
my_complicated_instruction2
my_complicated_instruction3
set +xv

Utilisez ShellCheck pour vérifier vos scripts bash

Il est très facile de passer à côté de problèmes simples lorsque vos scripts commencent à devenir volumineux. ShellCheck est l'un de ces outils qui vous évite de faire des erreurs.

shellcheck collect_data_from_servers.v7.sh

In collect_data_from_servers.v7.sh line 15:
for dependency in ${dependencies[@]}; do
                  ^----------------^ SC2068: Double quote array expansions to avoid re-splitting elements.


In collect_data_from_servers.v7.sh line 16:
    if [ ! -x $dependency ]; then
              ^---------^ SC2086: Double quote to prevent globbing and word splitting.

Did you mean: 
    if [ ! -x "$dependency" ]; then
...

Si vous vous demandez, la version finale du script, après avoir passé ShellCheck est là. Vraiment propre.

Vous avez remarqué quelque chose avec les processus scp en arrière-plan

Vous avez probablement remarqué que si vous tuez le script, il laisse derrière lui des processus fourchus. Ce n'est pas bon et c'est l'une des raisons pour lesquelles je préfère utiliser des outils comme Ansible ou Parallel pour gérer ce type de tâche sur plusieurs hôtes, laisser les frameworks faire le bon nettoyage pour moi. Vous pouvez, bien sûr, ajouter plus de code pour gérer cette situation.

Ce script bash pourrait potentiellement créer une bombe à fourche. Il n'a aucun contrôle sur le nombre de processus à générer en même temps, ce qui est un gros problème dans un environnement de production réel. En outre, il existe une limite au nombre de sessions ssh simultanées que vous pouvez avoir (et encore moins consommer de la bande passante). Encore une fois, j'ai écrit cet exemple fictif dans bash pour vous montrer comment vous pouvez toujours améliorer un programme pour mieux gérer les erreurs.

Résumons

1. Vous devez vérifier le code retour de vos commandes. Cela pourrait signifier décider de réessayer jusqu'à ce qu'une condition transitoire s'améliore ou de court-circuiter l'ensemble du script.
2. En parlant de conditions transitoires, vous n'avez pas besoin de repartir de zéro. Vous pouvez enregistrer l'état des tâches réussies, puis réessayer à partir de ce point.
3. Bash « piège » est votre ami. Utilisez-le pour le nettoyage et la gestion des erreurs.
4. Lorsque vous téléchargez des données à partir de n'importe quelle source, supposez qu'elles sont corrompues. N'écrasez jamais votre bon ensemble de données avec de nouvelles données avant d'avoir effectué des vérifications d'intégrité.
5. Tirez parti de journalctl et des champs personnalisés. Vous pouvez effectuer des recherches sophistiquées à la recherche de problèmes et même envoyer ces données aux agrégateurs de journaux.
6. Vous pouvez vérifier l'état des tâches en arrière-plan (y compris les sous-shells). N'oubliez pas de sauvegarder le PID et d'attendre.
7. Et enfin : utilisez un assistant de charpie Bash comme ShellCheck. Vous pouvez l'installer sur votre éditeur préféré (comme VIM ou PyCharm). Vous serez surpris du nombre d'erreurs non détectées sur les scripts Bash...

Si vous avez apprécié ce contenu ou si vous souhaitez le développer, contactez l'équipe à [email protected]

Artículos de interés

Subir