GNU/Linux >> LINUX-Kenntnisse >  >> Linux

Lernen Sie die Bash-Fehlerbehandlung anhand eines Beispiels kennen

In diesem Artikel stelle ich ein paar Tricks zur Behandlung von Fehlerbedingungen vor – einige fallen streng genommen nicht unter die Kategorie der Fehlerbehandlung (eine reaktive Art, mit dem Unerwarteten umzugehen), aber auch einige Techniken, um Fehler zu vermeiden, bevor sie passieren.

Fallstudie:Einfaches Skript, das einen Hardwarebericht von mehreren Hosts herunterlädt und in eine Datenbank einfügt.

Angenommen, Sie haben einen cron job auf jedem Ihrer Linux-Systeme, und Sie haben ein Skript, um die Hardwareinformationen von jedem zu erfassen:

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

Wenn alles gut geht, sammeln Sie Ihre Dateien parallel, weil Sie nicht mehr als zehn Systeme haben. Sie können es sich leisten, alle gleichzeitig per ssh zu erreichen und dann die Hardwaredetails von jedem anzuzeigen.

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"
}

Hier sind einige Möglichkeiten, warum etwas schief gelaufen ist:

  • Ihr Bericht wurde nicht ausgeführt, weil der Server ausgefallen war
  • Sie konnten das Verzeichnis, in dem die Dateien gespeichert werden müssen, nicht erstellen
  • Die Tools, die Sie zum Ausführen des Skripts benötigen, fehlen
  • Sie können den Bericht nicht abrufen, weil Ihr Remote-Computer abgestürzt ist
  • Mindestens einer der Berichte ist beschädigt

Die aktuelle Version des Skripts hat ein Problem – es wird von Anfang bis Ende ausgeführt, Fehler hin oder her:

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

Als Nächstes demonstriere ich ein paar Dinge, um Ihr Skript robuster zu machen und in manchen Fällen nach einem Fehler wiederhergestellt zu werden.

Die nukleare Option:Hart scheitern, schnell scheitern

Der richtige Umgang mit Fehlern besteht darin, mithilfe von Rückgabecodes zu prüfen, ob das Programm erfolgreich beendet wurde oder nicht. Es klingt offensichtlich, aber Rückgabecodes, eine Ganzzahl, die in bash $? gespeichert ist oder $! Variable, haben manchmal eine breitere Bedeutung. Auf der Manpage von Bash erfahren Sie:

Für die Zwecke der Shell war ein Befehl erfolgreich, der mit einem Exit-
Status von Null endet. Ein Exit-Status von Null zeigt einen Erfolg an.
Ein Exit-Status ungleich Null zeigt einen Fehler an. Wenn ein Befehl
mit einem fatalen Signal N beendet wird, verwendet Bash den Wert 128+N als
Ausgangsstatus.

Wie üblich sollten Sie immer die Handbuchseiten der Skripte lesen, die Sie aufrufen, um zu sehen, welche Konventionen für jedes von ihnen gelten. Wenn Sie mit einer Sprache wie Java oder Python programmiert haben, sind Sie höchstwahrscheinlich mit ihren Ausnahmen, unterschiedlichen Bedeutungen und der Tatsache vertraut, dass nicht alle auf die gleiche Weise behandelt werden.

Wenn Sie set -o errexit hinzufügen zu Ihrem Skript hinzufügen, wird es von diesem Punkt an die Ausführung abbrechen, wenn ein Befehl mit einem Code != 0 vorhanden ist . Aber errexit wird nicht verwendet, wenn Funktionen innerhalb eines if ausgeführt werden Bedingung. Anstatt mir also diese Ausnahme zu merken, führe ich lieber eine explizite Fehlerbehandlung durch.

Sehen Sie sich Version 2 des Skripts an. Es ist etwas besser:

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

Folgendes hat sich geändert:

  • Zeile 11 und 12, ich aktiviere die Fehlerverfolgung und füge eine "Falle" hinzu, um dem Benutzer mitzuteilen, dass ein Fehler aufgetreten ist und Turbulenzen bevorstehen. Vielleicht möchten Sie Ihr Skript stattdessen hier beenden, ich zeige Ihnen, warum das vielleicht nicht das Beste ist.
  • Zeile 20, wenn das Verzeichnis nicht existiert, versuchen Sie es in Zeile 21 zu erstellen. Wenn die Verzeichniserstellung fehlschlägt, beenden Sie es mit einem Fehler.
  • In Zeile 27, nachdem ich jeden Hintergrundjob ausgeführt habe, erfasse ich die PID und verknüpfe sie mit der Maschine (1:1-Beziehung).
  • In den Zeilen 33-35 warte ich auf den scp Aufgabe beenden, Rückgabecode abrufen und bei Fehler abbrechen.
  • In Zeile 37 überprüfe ich, ob die Datei geparst werden konnte, andernfalls breche ich mit einem Fehler ab.

Wie sieht nun die Fehlerbehandlung aus?

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

Wie Sie sehen können, ist diese Version besser darin, Fehler zu erkennen, aber sie ist sehr unversöhnlich. Außerdem werden nicht alle Fehler erkannt, oder?

Wenn Sie nicht weiterkommen und sich wünschen, Sie hätten einen Alarm

Der Code sieht besser aus, außer dass manchmal die scp könnte auf einem Server hängen bleiben (beim Versuch, eine Datei zu kopieren), weil der Server zu beschäftigt ist, um zu antworten, oder einfach in einem schlechten Zustand ist.

Ein weiteres Beispiel ist der Versuch, über NFS auf ein Verzeichnis zuzugreifen, in dem $HOME wird von einem NFS-Server gemountet:

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

Und Sie stellen Stunden später fest, dass der NFS-Mount-Punkt veraltet ist und Ihr Skript hängen bleibt.

Ein Timeout ist die Lösung. Und GNU Timeout kommt zur Rettung:

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

Hier versuchen Sie regelmäßig den Prozess nach 10,0 Sekunden nach dem Start zu beenden (TERM-Signal). Wenn es nach 20,0 Sekunden immer noch läuft, senden Sie ein KILL-Signal (kill -9 ). Prüfen Sie im Zweifelsfall, welche Signale in Ihrem System unterstützt werden (kill -l , zum Beispiel).

Wenn dies aus meinem Dialog nicht klar hervorgeht, sehen Sie sich das Skript an, um mehr Klarheit zu erhalten.

/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

Zurück zum ursprünglichen Skript, um ein paar weitere Optionen hinzuzufügen, und Sie haben 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

Was sind die Änderungen?:

  • Überprüfen Sie zwischen den Zeilen 16-22, ob alle erforderlichen Abhängigkeitstools vorhanden sind. Wenn es nicht ausgeführt werden kann, dann „Houston, wir haben ein Problem.“
  • Eine remote_copy erstellt Funktion, die ein Timeout verwendet, um sicherzustellen, dass scp endet spätestens bei 45.0s – Zeile 33.
  • Verbindungs-Timeout von 5 Sekunden anstelle des TCP-Standardwerts hinzugefügt – Zeile 37.
  • Neuversuch zu scp hinzugefügt in Zeile 38 – 3 Versuche, die jeweils 1 Sekunde warten.

Es gibt andere Möglichkeiten, es erneut zu versuchen, wenn ein Fehler auftritt.

Warten auf das Ende der Welt – wie und wann es erneut versucht werden sollte

Sie haben bemerkt, dass dem scp ein Wiederholungsversuch hinzugefügt wurde Befehl. Aber das wiederholt sich nur bei fehlgeschlagenen Verbindungen, was ist, wenn der Befehl mitten in der Kopie fehlschlägt?

Manchmal möchten Sie einfach scheitern, weil es nur sehr wenige Chancen gibt, sich von einem Problem zu erholen. Ein System, das beispielsweise Hardware-Fixes erfordert, oder Sie können einfach in einen eingeschränkten Modus zurückkehren – was bedeutet, dass Sie Ihre Systemarbeit ohne die aktualisierten Daten fortsetzen können. In diesen Fällen macht es keinen Sinn, ewig zu warten, sondern nur eine bestimmte Zeit.

Hier sind die Änderungen an remote_copy , um es kurz zu halten (vierte Version):

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

Wie sieht es jetzt aus? In diesem Durchlauf habe ich ein System ausgefallen (mac-pro-1-1) und ein System ohne die Datei (macmini2). Sie können sehen, dass die Kopie vom Server dmaf5 funktioniert sofort, aber für die anderen beiden gibt es einen Wiederholungsversuch für eine zufällige Zeit zwischen 1 und 60 Sekunden, bevor sie beendet werden:

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

Wenn ich versage, muss ich das alles noch einmal machen? Verwenden eines Checkpoints

Angenommen, die Remote-Kopie ist der teuerste Vorgang dieses gesamten Skripts und Sie sind bereit oder in der Lage, dieses Skript erneut auszuführen, vielleicht mit cron oder zweimal am Tag von Hand, um sicherzustellen, dass Sie die Dateien abholen, wenn ein oder mehrere Systeme ausfallen.

Sie könnten für den Tag einen kleinen „Status-Cache“ erstellen, in dem Sie nur die erfolgreichen Verarbeitungsvorgänge pro Maschine aufzeichnen. Wenn sich dort ein System befindet, suchen Sie für diesen Tag nicht erneut nach.

Einige Programme wie Ansible tun etwas Ähnliches und ermöglichen es Ihnen, ein Playbook nach einem Fehler auf einer begrenzten Anzahl von Computern erneut zu versuchen (--limit @/home/user/site.retry ).

Eine neue Version (Version fünf) des Skripts enthält Code zum Aufzeichnen des Status der Kopie (Zeile 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 }

Haben Sie die Falle in Zeile 22 bemerkt? Wenn das Skript unterbrochen (beendet) wird, möchte ich sicherstellen, dass der gesamte Cache ungültig wird.

Fügen Sie dann diese neue Hilfslogik zu remote_copy hinzu Funktion (Zeile 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 }

Beim ersten Start wird eine neue neue Nachricht für das Cache-Verzeichnis ausgegeben:

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

Wenn Sie es erneut ausführen, weiß das Skript, dass dma5f ist einsatzbereit, Sie müssen den Kopiervorgang nicht wiederholen:

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

Stellen Sie sich vor, wie sich dies beschleunigt, wenn Sie mehr Maschinen haben, die nicht erneut besucht werden sollten.

Krümel zurücklassen:Was protokollieren, wie protokollieren und ausführliche Ausgabe

Wenn Sie wie ich sind, mag ich ein bisschen Kontext, mit dem ich korrelieren kann, wenn etwas schief geht. Das echo Anweisungen im Skript sind nett, aber was wäre, wenn Sie ihnen einen Zeitstempel hinzufügen könnten.

Wenn Sie logger verwenden , können Sie die Ausgabe auf journalctl speichern zur späteren Überprüfung (sogar Aggregation mit anderen Tools da draußen). Das Beste daran ist, dass Sie die Leistungsfähigkeit von journalctl zeigen sofort.

Anstatt also einfach echo zu machen , können Sie auch einen Aufruf zu logger hinzufügen so mit einer neuen Bash-Funktion namens ‘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
}

Sie können sehen, dass Sie separate Felder als Teil der Nachricht speichern können, z. B. die Priorität, das Skript, das die Nachricht erstellt hat usw.

Wie ist das nützlich? Nun, Sie könnten get die Nachrichten zwischen 13:26 Uhr und 13:27 Uhr nur Fehler (priority=0 ) und nur für unser Skript (collect_data_from_servers.v6.sh ) wie folgt, Ausgabe im JSON-Format:

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"
}

Da es sich um strukturierte Daten handelt, können andere Protokollsammler alle Ihre Computer durchsuchen, Ihre Skriptprotokolle zusammenfassen und dann haben Sie nicht nur Daten, sondern auch die Informationen.

Sie können sich die gesamte sechste Version des Skripts ansehen.

Seien Sie nicht so eifrig, Ihre Daten zu ersetzen, bis Sie sie überprüft haben.


Wenn Sie von Anfang an bemerkt haben, dass ich immer wieder eine beschädigte JSON-Datei kopiert habe:

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

Das lässt sich leicht verhindern. Kopieren Sie die Datei an einen temporären Speicherort. Wenn die Datei beschädigt ist, versuchen Sie nicht, die vorherige Version zu ersetzen (und lassen Sie die fehlerhafte zur Überprüfung. Zeilen 99–107 der siebten Version des Skripts):

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
}

Wählen Sie die richtigen Tools für die Aufgabe aus und bereiten Sie Ihren Code ab der ersten Zeile vor

Ein sehr wichtiger Aspekt der Fehlerbehandlung ist die richtige Codierung. Wenn Sie eine schlechte Logik in Ihrem Code haben, wird es durch keine noch so große Fehlerbehandlung besser. Um dies kurz und bash-bezogen zu halten, gebe ich Ihnen im Folgenden ein paar Hinweise.

Sie sollten IMMER auf Fehlersyntax prüfen, bevor Sie Ihr Skript ausführen:

bash -n $my_bash_script.sh

Ernsthaft. Er sollte genauso automatisch ablaufen wie jeder andere Test.

Lesen Sie die Bash-Manpage und machen Sie sich mit den wichtigsten Optionen vertraut, wie:

set -xv
my_complicated_instruction1
my_complicated_instruction2
my_complicated_instruction3
set +xv

Verwenden Sie ShellCheck, um Ihre Bash-Skripte zu überprüfen

Es ist sehr leicht, einfache Probleme zu übersehen, wenn Ihre Skripts groß werden. ShellCheck ist eines dieser Tools, das Sie vor Fehlern bewahrt.

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
...

Wenn Sie sich fragen, die endgültige Version des Skripts, nachdem Sie ShellCheck bestanden haben, ist hier. Blitzsauber.

Sie haben etwas bei den scp-Prozessen im Hintergrund bemerkt

Sie haben wahrscheinlich bemerkt, dass, wenn Sie das Skript beenden, einige gegabelte Prozesse zurückbleiben. Das ist nicht gut und einer der Gründe, warum ich es vorziehe, Tools wie Ansible oder Parallel zu verwenden, um diese Art von Aufgabe auf mehreren Hosts zu erledigen, und die Frameworks die richtige Bereinigung für mich erledigen lassen. Sie können natürlich weiteren Code hinzufügen, um diese Situation zu handhaben.

Dieses Bash-Skript könnte möglicherweise eine Fork-Bombe erstellen. Es hat keine Kontrolle darüber, wie viele Prozesse gleichzeitig erzeugt werden, was in einer realen Produktionsumgebung ein großes Problem darstellt. Außerdem gibt es eine Grenze dafür, wie viele gleichzeitige SSH-Sitzungen Sie haben können (geschweige denn Bandbreite verbrauchen). Wiederum habe ich dieses fiktive Beispiel in Bash geschrieben, um Ihnen zu zeigen, wie Sie ein Programm immer verbessern können, um Fehler besser zu behandeln.

Fassen wir zusammen

[ Jetzt herunterladen:Eine Anleitung für Systemadministratoren zum Bash-Skripting. ]

1.  Sie müssen den Rückgabecode Ihrer Befehle überprüfen. Das könnte bedeuten, dass Sie sich entscheiden, es erneut zu versuchen, bis sich ein vorübergehender Zustand verbessert, oder das gesamte Skript kurzschließen.
2. Apropos vorübergehende Bedingungen, Sie müssen nicht bei Null anfangen. Sie können den Status erfolgreicher Aufgaben speichern und es ab diesem Zeitpunkt erneut versuchen.
3. Bash 'Trap' ist dein Freund. Verwenden Sie es zur Bereinigung und Fehlerbehandlung.
4. Gehen Sie beim Herunterladen von Daten aus einer beliebigen Quelle davon aus, dass diese beschädigt sind. Überschreiben Sie niemals Ihren guten Datensatz mit neuen Daten, bis Sie einige Integritätsprüfungen durchgeführt haben.
5. Nutzen Sie die Vorteile von journalctl und benutzerdefinierten Feldern. Sie können anspruchsvolle Suchen nach Problemen durchführen und diese Daten sogar an Protokollaggregatoren senden.
6. Sie können den Status von Hintergrundaufgaben (einschließlich Sub-Shells) überprüfen. Denken Sie daran, die PID zu speichern und darauf zu warten.
7. Und schließlich:Verwenden Sie einen Bash-Flusenhelfer wie ShellCheck. Sie können es auf Ihrem bevorzugten Editor (wie VIM oder PyCharm) installieren. Sie werden überrascht sein, wie viele Fehler in Bash-Skripten unentdeckt bleiben...

Wenn Ihnen dieser Inhalt gefallen hat oder Sie ihn erweitern möchten, kontaktieren Sie das Team unter [email protected].


Linux
  1. Die besten Ressourcen zum Erlernen der Bash-Skripterstellung?

  2. Anpassen der Bash Shell:Fett/Farbe Der Befehl?

  3. Bash-Skriptfehler:Integer-Ausdruck erwartet?

  4. Beheben Sie den Fehler „Der angegebene Hostname ist ungültig“.

  5. Bash-Skript – Beispiel „Hello World“.

Bash Shebang

Fehlerbehebung „Bash:Command Not Found“-Fehler in Linux

Lernen Sie Multithreading-Bash-Skripte mit GNU Parallel

Bash ignoriert Fehler für einen bestimmten Befehl

So deklarieren Sie ein 2D-Array in Bash

Fehler in einem Bash-Skript auslösen