GNU/Linux >> Tutoriels Linux >  >> Linux

Apprendre la gestion des erreurs Bash par exemple

Dans cet article, je présente quelques astuces pour gérer les conditions d'erreur :certaines n'entrent strictement pas dans la catégorie de la gestion des erreurs (une manière réactive de gérer les imprévus), mais également certaines techniques pour éviter les erreurs avant qu'elles ne surviennent.

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

Dites que vous avez un cron sur chacun de vos systèmes Linux, et vous disposez d'un script pour 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 d'accéder à tous en ssh 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 possibilités de pourquoi 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 dans lequel 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 ordinateur distant a planté
  • Un ou plusieurs des rapports sont corrompus

La version actuelle du script a un problème—Il 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, à certains moments, récupérer d'un échec.

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

La bonne façon de gérer les erreurs consiste à vérifier si le programme s'est terminé avec succès ou non, à l'aide de 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. La page de manuel bash vous indique :

Pour les besoins du shell, une commande qui se termine avec un état de sortie
zéro a réussi. Un état de sortie de zéro indique un 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 128+N comme
é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 le fait qu'ils ne sont pas tous traités de la même manière.

Si vous ajoutez set -o errexit à votre script, à partir de ce moment, 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 dans un if condition, donc au lieu de me souvenir de cette exception, je fais plutôt une gestion explicite des erreurs.

Jetez un œil à la version 2 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 le suivi des erreurs et j'ajoute 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 en 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 parsé, 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 ?

Lorsque vous êtes bloqué et que vous souhaitez avoir une alarme

Le code est plus beau, sauf que parfois le scp peut rester bloqué sur un serveur (en essayant de copier un fichier) car le serveur est trop occupé pour répondre ou simplement en 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 obsolète et que votre script est bloqué.

Un délai d'attente est la solution. Et, GNU timeout 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 bien 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 compatibles avec votre système (kill -l , par exemple).

Si ce n'est pas clair dans ma boîte de dialogue, alors 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 obtenez la version 3 :

 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".
  • Créé une remote_copy fonction, qui utilise un délai d'attente pour s'assurer que le scp se termine au plus tard en 45.0s—ligne 33.
  • Ajout d'un délai de connexion de 5 secondes au lieu de la valeur TCP par défaut :ligne 37.
  • Ajout d'une nouvelle tentative à scp sur la ligne 38—3 tentatives qui attendent 1 seconde entre chacune.

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

En attendant la fin du monde - comment et quand réessayer

Vous avez remarqué qu'une nouvelle tentative a été ajoutée au scp commande. Mais qui réessaie uniquement pour les connexions ayant échoué, que se passe-t-il 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 vous remettre d'un problème. Un système qui nécessite des correctifs matériels, par exemple, ou vous pouvez simplement revenir en 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 certain temps.

Voici les modifications apportées au remote_copy , pour être bref (version 4) :

#!/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...

À 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 pendant un temps aléatoire 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 tout recommencer ? Utiliser un point de contrôle

Supposons que la copie à distance soit l'opération la plus coûteuse de tout ce script et que vous souhaitiez ou puissiez réexécuter ce script, peut-être en utilisant cron ou le faire à la main deux fois pendant la journée 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 enregistrez uniquement les opérations de traitement réussies par machine. Si un système s'y trouve, ne vous embêtez pas à vérifier à nouveau ce jour-là.

Certains programmes, comme Ansible, font quelque chose de similaire et vous permettent 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 contient 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'assistance 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 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 le réexécutez, 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 comment cela s'accélère lorsque vous avez plus de machines qui ne doivent pas être revisitées.

Laisser des miettes derrière :quoi enregistrer, comment enregistrer et sortie détaillée

Si vous êtes comme moi, j'aime un peu de contexte pour établir une corrélation avec quand quelque chose ne va pas. L'echo les déclarations sur le script sont sympas mais que se passerait-il si vous pouviez leur ajouter un horodatage.

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

Ainsi, au lieu de simplement faire echo , vous pouvez également ajouter un appel à logger comme ceci 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 distincts dans le cadre du message, comme la priorité, le script qui a produit le message, etc.

Alors, comment est-ce utile? Eh bien, vous pourriez get les messages entre 13h26 et 13h27, 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"
}

Comme 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 non seulement des données, mais également des informations.

Vous pouvez consulter l'intégralité de la version 6 du script.

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


Si vous avez remarqué dès le début, je copie sans cesse un fichier JSON corrompu :

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 à prévenir. 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 7 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 ce soit court et lié à 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. Cela devrait être aussi automatique que n'importe quel autre test.

Lisez la page de manuel de bash et familiarisez-vous avec les options indispensables, 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 à prendre de l'ampleur. 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 posez la question, la version finale du script, après avoir passé ShellCheck est ici. Parfaitement 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, laissant 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 fork. 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 (sans parler de la consommation de bande passante). Encore une fois, j'ai écrit cet exemple fictif en bash pour vous montrer comment vous pouvez toujours améliorer un programme pour mieux gérer les erreurs.

Récapitulons

[ Télécharger maintenant :Un guide de l'administrateur système sur les scripts Bash. ]

1.  Vous devez vérifier le code de retour de vos commandes. Cela pourrait impliquer de décider de réessayer jusqu'à ce qu'une condition transitoire s'améliore ou de court-circuiter tout le 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 moment.
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é quelques 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 à des 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 de l'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 souhaitez le développer, contactez l'équipe à [email protected].


Linux
  1. Gestion des erreurs dans les scripts Bash

  2. La variable Curl Outfile ne fonctionne pas dans le script Bash ?

  3. Erreur de script bash :expression entière attendue ?

  4. Bash :erreur de syntaxe près du jeton inattendu `} ?

  5. Résoudre l'erreur de nom d'hôte fourni non valide

Bash Shebang

Dépannage de l'erreur "Bash :Commande introuvable" sous Linux

Apprenez les scripts Bash multi-threading avec GNU Parallel

Bash ignorant l'erreur pour une commande particulière

Comment déclarer un tableau 2D dans bash

Augmenter l'erreur dans un script Bash