diff --git a/poste/alpi-fiche.sh b/poste/alpi-fiche.sh index c457ccc..e62ce05 100755 --- a/poste/alpi-fiche.sh +++ b/poste/alpi-fiche.sh @@ -47,6 +47,18 @@ set -u +# Le temps que met la collecte, en millisecondes. +# +# Une machine n'est relevée qu'une fois quand elle n'a pas connu de session +# live : il n'y a alors pas d'écart entre deux passages à mesurer. Cette durée- +# ci existe toujours, et elle se compare — elle dit ce que la machine met à +# s'interroger elle-même : lire le SMART de ses disques, réveiller ceux qui +# dorment, énumérer ses barrettes. Un plateau qui rame se voit là. +# +# On arrête le chronomètre à la fin de la collecte, avant le verdict : celui-ci +# attend une touche, et l'on mesurerait la personne devant l'écran. +DEBUT_NS="$(date +%s%N 2>/dev/null || echo 0)" + # smartctl et dmidecode vivent dans /usr/sbin, que le PATH d'un shell ordinaire # ne contient pas toujours — un « command -v » y échouait alors que le binaire # était là. @@ -233,6 +245,8 @@ $(champ detail "$detail")}" fi BARRETTES="$BARRETTES]" +TABLE_DISQUES="" + # --- Les disques ----------------------------------------------------------- # # lsblk donne la taille et le type ; le drapeau « rotational » du noyau @@ -254,21 +268,81 @@ for chemin in /sys/block/*; do # L'état SMART, s'il est lisible. Sans smartctl on ne prétend rien. sante="" reallocations="" + pending="" + illisibles="" heures="" + usure="" + ecrit="" if command -v smartctl >/dev/null 2>&1; then sortie="$(smartctl -H -A "/dev/$nom" 2>/dev/null)" sante="$(printf '%s' "$sortie" | grep -iE 'overall-health|SMART Health' \ | sed 's/.*: *//' | head -1)" reallocations="$(printf '%s' "$sortie" \ | awk '/Reallocated_Sector_Ct|Reallocated_Event/{print $10; exit}')" + # Les secteurs que le disque n'arrive plus à lire. Ils manquaient à la + # fiche, et c'est ce qui a fait qu'alpicache annonçait « sain » un + # disque déclaré à remplacer : il ne les avait jamais reçus. + pending="$(printf '%s' "$sortie" | awk '$2 == "Current_Pending_Sector"{print $10; exit}')" + illisibles="$(printf '%s' "$sortie" | awk '$2 == "Offline_Uncorrectable"{print $10; exit}')" heures="$(printf '%s' "$sortie" | awk '/Power_On_Hours/{print $10; exit}')" + + # L'usure d'un SSD, en pour cent de vie consommée. + # + # Un disque mécanique s'use en tournant, un SSD en écrivant : les + # heures de marche ne disent rien de son état. Chaque constructeur a + # son attribut, et deux conventions coexistent — les uns comptent ce + # qui est consommé, les autres ce qui reste. On ramène tout à + # « consommé », qui est le sens qu'on veut lire. NVMe donne directement + # « Percentage Used » ; pour les SATA, la colonne VALUE part de 100 et + # descend. + usure="$(printf '%s' "$sortie" | awk '/Percentage Used:/{gsub(/[^0-9]/,"",$3); print $3; exit}')" + if [ -z "$usure" ]; then + usure="$(printf '%s' "$sortie" | awk ' + /Media_Wearout_Indicator|Wear_Leveling_Count|SSD_Life_Left|Percent_Lifetime_Remain/ { + if ($4 ~ /^[0-9]+$/ && $4 <= 100) { print 100 - $4; exit } + }')" + fi + # Ce que le disque a écrit depuis sa mise en service : l'autre façon de + # juger un SSD, dont les modèles courants tiennent 100 à 600 To. + ecrit="$(printf '%s' "$sortie" | awk ' + /Total_LBAs_Written|Host_Writes_32MiB|Total_Host_Writes/ { + if ($10 ~ /^[0-9]+$/) { print $10; exit } + } + /Data Units Written:/ { gsub(/[^0-9]/,"",$4); print $4; exit }')" fi + # Les mêmes chiffres, mis en table pour être lus par quelqu'un. Le verdict + # ne dit que ce qui cloche ; on veut aussi voir ce qui va, et sur quoi le + # verdict se fonde. + taille_go="$(awk -v o="$taille" 'BEGIN{printf "%.1f", o/1000000000}' | tr . ,)" + secteurs="—" + for valeur in "${pending:-0}" "${illisibles:-0}" "${reallocations:-0}"; do + case "$valeur" in ''|*[!0-9]*) continue ;; esac + [ "$valeur" -gt 0 ] && secteurs="!" + done + [ "$secteurs" = "!" ] && secteurs="$(printf '%s/%s/%s' \ + "${reallocations:-0}" "${pending:-0}" "${illisibles:-0}")" + usure_dite="—" + if [ "$rotatif" = 1 ]; then + case "$heures" in ''|*[!0-9]*) ;; *) + usure_dite="$heures h" + [ "$heures" -ge 8760 ] && usure_dite="$usure_dite ($(( heures / 8760 )),$(( (heures % 8760) * 10 / 8760 )) an)" ;; + esac + else + case "$usure" in ''|*[!0-9]*) ;; *) usure_dite="usé à $usure %" ;; esac + fi + TABLE_DISQUES="$TABLE_DISQUES$(printf '%-7s %-20s %-10s %9s %-8s %-7s %s' \ + "$nom" "$(printf '%.20s' "${modele:-—}")" \ + "$([ "$rotatif" = 1 ] && echo mécanique || echo SSD)" \ + "$taille_go Go" "${sante:-—}" "${heures:-—}" "$secteurs")"$'\n' + [ "$premier" = 0 ] && DISQUES="$DISQUES," premier=0 DISQUES="$DISQUES{$(champ nom "$nom"),$(champ modele "$modele"),\ \"octets\":$taille,$(champ type "$([ "$rotatif" = 1 ] && echo mécanique || echo SSD)"),\ -$(champ sante "$sante"),$(champ reallocations "$reallocations"),$(champ heures "$heures")}" +$(champ sante "$sante"),$(champ reallocations "$reallocations"),\ +$(champ pending "$pending"),$(champ illisibles "$illisibles"),$(champ heures "$heures"),\ +$(champ usure "$usure"),$(champ ecrit "$ecrit")}" done DISQUES="$DISQUES]" @@ -305,6 +379,19 @@ $(champ vitesse_memoire "$VITESSE_MEMOIRE"),\ \"slots_total\":$SLOTS_TOTAL,\"slots_pris\":$SLOTS_PRIS,\"barrettes\":$BARRETTES,\ \"disques\":$DISQUES,\"occupation\":$OCCUPATION}" +DUREE_MS=0 +case "$DEBUT_NS" in + ''|*[!0-9]*) ;; + 0) ;; + *) FIN_NS="$(date +%s%N 2>/dev/null || echo 0)" + case "$FIN_NS" in + ''|*[!0-9]*|0) ;; + *) DUREE_MS=$(( (FIN_NS - DEBUT_NS) / 1000000 )) ;; + esac ;; +esac +[ "$DUREE_MS" -lt 0 ] && DUREE_MS=0 +RAPPORT="${RAPPORT%\}},\"duree_releve_ms\":$DUREE_MS}" + if [ "$MONTRER" = 1 ]; then if command -v python3 >/dev/null 2>&1; then printf '%s' "$RAPPORT" | python3 -m json.tool 2>/dev/null || printf '%s\n' "$RAPPORT" @@ -329,6 +416,8 @@ fi echo "Système : $SYSTEME" echo +MOTIFS="" + # --- Le verdict ------------------------------------------------------------ # # Trois réponses possibles, et une seule question : peut-on installer sur cette @@ -349,6 +438,17 @@ if [ "$CHECK" = 1 ]; then note() { [ "$1" -gt "$GRAVITE" ] && GRAVITE="$1" REMARQUES="$REMARQUES$2"$'\n' + # Le même motif, débarrassé de ses couleurs et de ses retours à la + # ligne, pour qu'alpicache dise exactement ce qui a été dit ici. + if [ "$1" -ge 1 ]; then + # Une note tient sur plusieurs lignes à l'écran ; le motif, lui, + # est d'un seul tenant — il ira dans une case de tableau. + nu="$(printf '%s' "$2" | tr '\n' ' ' | sed -e 's/\x1b\[[0-9;]*m//g' \ + -e 's/[[:space:]]\{1,\}/ /g' \ + -e 's/^ *[!?✗·] *//' \ + -e 's/^ *//' -e 's/ *$//')" + [ -n "$nu" ] && MOTIFS="$MOTIFS$nu"$'\n' + fi } # Les disques. C'est pour eux qu'on fait ce contrôle. @@ -369,34 +469,111 @@ if [ "$CHECK" = 1 ]; then if printf '%s' "$sortie" | grep -qiE 'overall-health.*(FAILED|FAILING)'; then note 2 " ${R}✗${Z} ${B}/dev/$nom se déclare défaillant${Z} — ne pas installer dessus." fi + # Les trois attributs ne disent pas la même chose, et les confondre + # revient à sous-estimer les deux qui comptent. + # + # Reallocated_Sector_Ct des secteurs déjà remplacés par la réserve + # du disque. Quelques-uns sont de l'usure + # ordinaire ; c'est leur nombre qui parle. + # Current_Pending_Sector des secteurs que le disque n'arrive plus à + # lire et n'a pas encore pu déplacer. Ce + # qu'ils contenaient est en train d'être + # perdu. Un seul suffit à s'inquiéter. + # Offline_Uncorrectable des secteurs définitivement illisibles. + # + # Les deux derniers ne sont pas de l'usure mais une panne en cours : + # on ne les mesure pas à la même aune que le premier. for attribut in Reallocated_Sector_Ct Current_Pending_Sector Offline_Uncorrectable; do valeur="$(printf '%s' "$sortie" | awk -v a="$attribut" '$2 == a {print $10; exit}')" case "$valeur" in ''|*[!0-9]*) continue ;; esac [ "$valeur" = "0" ] && continue - if [ "$valeur" -ge 50 ]; then - note 2 " ${R}✗${Z} /dev/$nom : $valeur $attribut — disque à remplacer." - else - note 1 " ${O}!${Z} /dev/$nom : $valeur $attribut — à surveiller." - fi + case "$attribut" in + Reallocated_Sector_Ct) + libelle="secteur(s) déjà remplacé(s)" + if [ "$valeur" -ge 50 ]; then + note 2 " ${R}✗${Z} /dev/$nom : ${B}$valeur $libelle${Z} — disque à remplacer." + else + note 1 " ${O}!${Z} /dev/$nom : $valeur $libelle — à surveiller." + fi ;; + Current_Pending_Sector) + note 2 " ${R}✗${Z} /dev/$nom : ${B}$valeur secteur(s) illisible(s)${Z} en attente. + ${R}Ce ne sont pas des secteurs usés mais des données que le disque + n'arrive plus à relire : le disque est en train de lâcher.${Z} + ${D}Sauvegarder ce qui doit l'être avant toute autre chose, et + ne rien installer dessus — l'installation ne tiendrait pas.${Z}" ;; + Offline_Uncorrectable) + note 2 " ${R}✗${Z} /dev/$nom : ${B}$valeur secteur(s) définitivement perdu(s)${Z}. + ${R}Le disque a renoncé à les relire. À remplacer.${Z}" ;; + esac done heures="$(printf '%s' "$sortie" | awk '/Power_On_Hours/{print $10; exit}')" rotatif="$(cat "$chemin/queue/rotational" 2>/dev/null)" + + # L'usure, recalculée ici : cette boucle est distincte de celle du + # relevé et ne voit pas ses variables. + usure="$(printf '%s' "$sortie" | awk '/Percentage Used:/{gsub(/[^0-9]/,"",$3); print $3; exit}')" + if [ -z "$usure" ]; then + usure="$(printf '%s' "$sortie" | awk ' + /Media_Wearout_Indicator|Wear_Leveling_Count|SSD_Life_Left|Percent_Lifetime_Remain/ { + if ($4 ~ /^[0-9]+$/ && $4 <= 100) { print 100 - $4; exit } + }')" + fi # Un disque mécanique se signale toujours, et pas seulement quand il # est vieux. C'est le premier frein d'une machine par ailleurs # correcte : remplacer un plateau par un SSD fait plus pour elle que # n'importe quel réglage, et c'est le genre de conseil qu'on donne # pendant qu'on a la machine ouverte devant soi. + # Les heures de marche, dites en heures et en années de fonctionnement. + # « 477 j 15 h » se lit mal : on croit à un disque épuisé alors que + # c'est une année et quelques mois de marche, ce qui est peu. + marche="" + case "$heures" in + ''|*[!0-9]*) ;; + *) marche="$heures h" + if [ "$heures" -ge 8760 ]; then + marche="$marche, soit $(( heures / 8760 )),$(( (heures % 8760) * 10 / 8760 )) an de marche" + fi ;; + esac + if [ "$rotatif" = "1" ]; then - age="" + # Un disque mécanique se fatigue en tournant. Les constructeurs + # annoncent 300 000 à 600 000 heures de MTBF, mais le retour du + # terrain est plus sobre : au-delà de 30 000 heures — trois ans et + # demi de marche continue — les pannes deviennent sensiblement plus + # fréquentes, et à 45 000 on est dans le sursis. + gravite_disque=1 + supplement="" case "$heures" in ''|*[!0-9]*) ;; - *) annees=$(( heures / 8760 )) - [ "$annees" -gt 0 ] && age=" de $annees an$([ "$annees" -gt 1 ] && echo s)" - [ "$annees" = "0" ] && age=" ($heures h de marche)" ;; + *) if [ "$heures" -ge 45000 ]; then + gravite_disque=2 + supplement=" + ${R}Au-delà de 45 000 heures : sauvegarder avant toute chose.${Z}" + elif [ "$heures" -ge 30000 ]; then + supplement=" + ${O}Plus de 30 000 heures de marche : il a fait son temps.${Z}" + fi ;; esac - note 1 " ${O}!${Z} /dev/$nom est un ${B}disque mécanique${Z}$age. + note "$gravite_disque" " ${O}!${Z} /dev/$nom est un ${B}disque mécanique${Z}${marche:+ · $marche}. ${D}Le remplacer par un SSD ferait plus pour cette machine que - tout le reste réuni — démarrage, ouverture des programmes, réactivité.${Z}" + tout le reste réuni — démarrage, ouverture des programmes, réactivité.${Z}$supplement" + else + # Un SSD ne s'use pas en tournant mais en écrivant. Les heures de + # marche ne disent rien de son état : c'est l'usure qu'on regarde. + case "$usure" in + ''|*[!0-9]*) + [ -n "$marche" ] && note 0 " ${V}·${Z} /dev/$nom est un SSD · $marche. + ${D}Usure non déclarée par ce modèle.${Z}" ;; + *) if [ "$usure" -ge 90 ]; then + note 2 " ${R}✗${Z} /dev/$nom : SSD usé à ${B}${usure} %${Z} — il approche de sa fin. + ${R}Sauvegarder, et prévoir son remplacement.${Z}" + elif [ "$usure" -ge 70 ]; then + note 1 " ${O}!${Z} /dev/$nom : SSD usé à ${usure} %${marche:+ · $marche}. + ${D}Encore utilisable, mais il faudra y penser.${Z}" + else + note 0 " ${V}·${Z} /dev/$nom : SSD usé à ${usure} % seulement${marche:+ · $marche}." + fi ;; + esac fi if [ "$taille" -lt 32000000000 ]; then note 1 " ${O}!${Z} /dev/$nom ne fait que $(( taille / 1000000000 )) Go — c'est juste." @@ -424,6 +601,13 @@ if [ "$CHECK" = 1 ]; then 2) printf " %s%s NE PAS INSTALLER EN L'ÉTAT %s\n" "$B$R" "✗" "$Z" ;; esac echo + if [ -n "$TABLE_DISQUES" ]; then + printf " %s%-7s %-20s %-10s %9s %-8s %-7s %s%s\n" "$D" \ + "Disque" "Modèle" "Type" "Taille" "SMART" "Heures" "Secteurs R/P/I" "$Z" + printf '%s' "$TABLE_DISQUES" | sed "s/^/ /" + printf " %sR : remplacés · P : illisibles en attente · I : perdus%s\n" "$D" "$Z" + echo + fi [ -n "$REMARQUES" ] && printf "%s\n" "$REMARQUES" # Une pause, sans quoi tout cela défile et personne ne le lit. Sa durée @@ -443,6 +627,29 @@ if [ "$CHECK" = 1 ]; then fi fi +# Ce que l'on vient de dire à la personne part avec la fiche. +# +# Sans cela, alpicache rejugeait à partir des seuls champs reçus — et ne voyait +# pas les secteurs en attente, qui ne lui étaient pas envoyés. Il affichait +# « sain » un disque que ce script venait de déclarer à remplacer. Deux juges, +# deux verdicts : il n'y en a plus qu'un, et c'est celui-ci. +VERDICT="ok" +case "${GRAVITE:-0}" in + 2) VERDICT="alerte" ;; + 1) VERDICT="attention" ;; +esac +MOTIFS_JSON="[" +premier=1 +while IFS= read -r ligne; do + [ -n "$ligne" ] || continue + [ "$premier" = 0 ] && MOTIFS_JSON="$MOTIFS_JSON," + premier=0 + MOTIFS_JSON="$MOTIFS_JSON$(printf '%s' "$ligne" \ + | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/^/"/' -e 's/$/"/')" +done <<< "${MOTIFS:-}" +MOTIFS_JSON="$MOTIFS_JSON]" +RAPPORT="${RAPPORT%\}},$(champ verdict "$VERDICT"),\"motifs\":$MOTIFS_JSON}" + if command -v curl >/dev/null 2>&1; then reponse="$(printf '%s' "$RAPPORT" | curl -s --max-time 15 \ -H 'Content-Type: application/json' --data-binary @- "$SERVEUR/fiche")"