Faire voyager le verdict avec la fiche, au lieu de le refaire au serveur
Sur une machine réelle, un HP 250 G4 affichait devant son propriétaire « NE PAS INSTALLER EN L'ÉTAT — 976 secteurs en attente », et apparaissait « sain » sur le tableau de bord. La fiche ne transportait que le compteur de secteurs réalloués, nul sur ce disque, et le serveur rejugeait là-dessus. Deux juges, deux verdicts, et c'est le rassurant qu'on lisait. Le verdict part désormais avec la fiche — champs « verdict » et « motifs », ces derniers débarrassés de leurs couleurs — et le serveur les affiche sans rien recalculer. Un seul juge, celui qui a vu le disque. Les trois compteurs SMART cessent d'être confondus : les secteurs réalloués disent l'usure et se comptent, ceux en attente et les illisibles disent une panne en cours, et un seul suffit à déclencher l'alerte. Ce ne sont pas des secteurs fatigués mais des données que le disque n'arrive plus à relire. Les deux nouveaux compteurs voyagent aussi dans la fiche. Le relevé se chronomètre enfin, sur la collecte seule : la pause du verdict attend quelqu'un devant l'écran, la mesurer reviendrait à chronométrer la personne. Constat, correctifs et version de référence : session alpicache. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BE4rvHVETRoWnYNDTGssdo
This commit is contained in:
parent
7795ad3158
commit
790874682b
1 changed files with 219 additions and 12 deletions
|
|
@ -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
|
||||
case "$attribut" in
|
||||
Reallocated_Sector_Ct)
|
||||
libelle="secteur(s) déjà remplacé(s)"
|
||||
if [ "$valeur" -ge 50 ]; then
|
||||
note 2 " ${R}✗${Z} /dev/$nom : $valeur $attribut — disque à remplacer."
|
||||
note 2 " ${R}✗${Z} /dev/$nom : ${B}$valeur $libelle${Z} — disque à remplacer."
|
||||
else
|
||||
note 1 " ${O}!${Z} /dev/$nom : $valeur $attribut — à surveiller."
|
||||
fi
|
||||
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.
|
||||
if [ "$rotatif" = "1" ]; then
|
||||
age=""
|
||||
# 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]*) ;;
|
||||
*) annees=$(( heures / 8760 ))
|
||||
[ "$annees" -gt 0 ] && age=" de $annees an$([ "$annees" -gt 1 ] && echo s)"
|
||||
[ "$annees" = "0" ] && age=" ($heures h de marche)" ;;
|
||||
*) marche="$heures h"
|
||||
if [ "$heures" -ge 8760 ]; then
|
||||
marche="$marche, soit $(( heures / 8760 )),$(( (heures % 8760) * 10 / 8760 )) an de marche"
|
||||
fi ;;
|
||||
esac
|
||||
note 1 " ${O}!${Z} /dev/$nom est un ${B}disque mécanique${Z}$age.
|
||||
|
||||
if [ "$rotatif" = "1" ]; then
|
||||
# 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]*) ;;
|
||||
*) 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 "$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")"
|
||||
|
|
|
|||
Loading…
Reference in a new issue