#!/bin/bash # Relève la fiche d'une machine, et l'envoie au serveur de l'install party. # # Ce script vit dans le dépôt du wiki, à côté d'install.sh : il est lu et # versionné comme lui, et c'est de là qu'il faut le prendre. # # wget https://gitea.alpinux.org/alpinux.cedrica5l/alpinux-wiki/raw/branch/main/code/linux/linux-mint/alpi-fiche.sh # chmod +x alpi-fiche.sh # sudo ./alpi-fiche.sh --check # depuis la session live, avant d'installer # sudo ./alpi-fiche.sh # après l'installation # # Sur le réseau de l'install party, plus court : # # wget http://10.0.0.1/alpi-fiche.sh && chmod +x alpi-fiche.sh && sudo ./alpi-fiche.sh --check # # Pourquoi la garder sur le serveur plutôt que sur le poste : trois semaines # plus tard, quand quelqu'un rappelle parce que « ça rame », on n'a plus la # machine sous la main. Savoir qu'elle a huit ans, quatre gigaoctets de # mémoire et un disque mécanique qui commence à réallouer des secteurs répond # à la question avant de l'avoir posée. # # Ce qui est envoyé : le modèle, le processeur, la mémoire, les disques et # leur santé, le système installé. # # Deux de ces champs touchent à la personne, et il faut le dire plutôt que de # prétendre le contraire — la première version de cet en-tête annonçait « rien # qui touche à la personne », ce qui était faux : # # · le NOM DE LA MACHINE, choisi pendant l'installation, contient très # souvent un prénom ; # · l'ADRESSE MATÉRIELLE de la carte réseau identifie durablement l'appareil. # # Les deux servent : le nom pour s'y retrouver, l'adresse pour rapprocher la # fiche du bail DHCP. Mais ils appartiennent à la personne dont on installe # l'ordinateur — il faut le lui dire. Ne sont en revanche jamais envoyés : son # nom d'utilisateur, ses fichiers, son historique, ni rien de ce qu'elle a # installé. Le numéro de série est tronqué à quatre caractères. # # alpi-fiche relève et envoie # alpi-fiche --montrer relève et affiche, sans rien envoyer # alpi-fiche --check relève, envoie, et rend un verdict # # Le mode « --check » est fait pour tourner **depuis la clé USB, avant # d'installer** : c'est le seul moment où l'on peut encore renoncer. Un disque # qui se déclare défaillant, on ne passe pas une heure à installer dessus — on # le dit à son propriétaire et on cherche une autre solution. 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à. PATH="$PATH:/usr/sbin:/sbin" SERVEUR="${ALPI_SERVEUR:-http://alpicache.lan}" MONTRER=0 CHECK=0 case "${1:-}" in --montrer) MONTRER=1 ;; --check) CHECK=1 ;; esac if [ -t 1 ]; then V=$'\e[32m'; O=$'\e[33m'; R=$'\e[31m'; B=$'\e[1m'; D=$'\e[2m'; Z=$'\e[0m' else V=""; O=""; R=""; B=""; D=""; Z="" fi # Ce dont le relevé a besoin, et qu'il installe lui-même plutôt que de rendre # un verdict à trous. # # « état des disques inconnu » n'aide personne : c'est précisément la réponse # qu'on était venu chercher. Les deux paquets pèsent moins de deux mégaoctets, # ils sont dans le cache, et l'installation prend deux secondes sur le réseau # de l'install party. On ne le fait qu'en root, qu'avec apt, et sans rien # demander d'autre. completer_outils() { [ "$(id -u)" = "0" ] || return 0 command -v apt-get >/dev/null 2>&1 || return 0 local manquants="" command -v smartctl >/dev/null 2>&1 || manquants="$manquants smartmontools" command -v dmidecode >/dev/null 2>&1 || manquants="$manquants dmidecode" [ -z "$manquants" ] && return 0 echo "==> Installation de ce qui manque au relevé :$manquants" # Par le cache s'il répond, directement sinon. « -qq » pour ne pas noyer # l'écran : ce n'est pas l'objet de la manœuvre. local proxy="" if command -v wget >/dev/null 2>&1 \ && wget -q --spider --timeout=3 "$SERVEUR/" 2>/dev/null; then proxy="-o Acquire::http::Proxy=http://apt-proxy:3142" fi DEBIAN_FRONTEND=noninteractive apt-get $proxy -qq -y install $manquants \ >/dev/null 2>&1 \ || echo " (échec — le relevé se fera sans)" # Ce qui vient d'arriver est dans /usr/sbin, que le PATH d'un shell # ordinaire ne contient pas toujours. PATH="$PATH:/usr/sbin:/sbin" } completer_outils lire_dmi() { cat "/sys/class/dmi/id/$1" 2>/dev/null | tr -d '\n' | sed 's/^ *//; s/ *$//' } echapper() { # De quoi écrire du JSON sans rien installer : guillemets, antislashs et # sauts de ligne, et c'est tout ce qui peut nous arriver ici. printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g' | tr -d '\n\r' } champ() { printf '"%s":"%s"' "$1" "$(echapper "${2:-}")"; } # --- La machine ------------------------------------------------------------ MODELE="$(lire_dmi product_name)" MARQUE="$(lire_dmi sys_vendor)" SERIE="$(lire_dmi product_serial)" # Le numéro de série identifie un objet, pas une personne, mais il n'a rien à # faire dans un fichier qu'on partage : on n'en garde que les quatre derniers # caractères, assez pour distinguer deux machines du même modèle. SERIE="${SERIE: -4}" BIOS="$(lire_dmi bios_date)" PROCESSEUR="$(awk -F': ' '/^model name/{print $2; exit}' /proc/cpuinfo)" COEURS="$(nproc 2>/dev/null || echo '?')" MEMOIRE_KO="$(awk '/^MemTotal/{print $2}' /proc/meminfo)" SYSTEME="" if [ -r /etc/os-release ]; then SYSTEME="$(. /etc/os-release; echo "${PRETTY_NAME:-$NAME $VERSION}")" fi NOYAU="$(uname -r)" ARCHITECTURE="$(uname -m)" # Sommes-nous sur la clé USB, ou sur le système installé ? # # La distinction n'est pas cosmétique : les deux relevés répondent à deux # questions différentes. Celui de la session live dit « peut-on installer sur # cette machine ? » et se fait avant d'effacer quoi que ce soit. Celui d'après # dit « qu'avons-nous installé, et sur quoi ». Les garder séparés permet de # comparer — un disque qui allait bien le matin et qui se plaint le soir, cela # se voit. # # Le signe qui ne trompe pas est la racine : une session live la monte en # overlay au-dessus d'un squashfs, ce qu'aucun système installé ne fait. S'y # ajoute « boot=casper » ou « boot=live » sur la ligne de commande du noyau. # # L'existence de /cdrom ou de /rofs, elle, ne prouve rien : l'installateur de # Mint laisse un /cdrom vide derrière lui, et la première version de ce script # a donc déclaré « session live » une machine parfaitement installée. On ne les # retient que s'ils sont réellement montés. MOMENT="post-installation" if findmnt -n -o FSTYPE / 2>/dev/null | grep -qE '^(overlay|aufs|squashfs)$' \ || grep -qE '(^| )boot=(casper|live)( |$)' /proc/cmdline 2>/dev/null \ || mountpoint -q /rofs 2>/dev/null \ || { mountpoint -q /cdrom 2>/dev/null \ && findmnt -n -o FSTYPE /cdrom 2>/dev/null | grep -qE '^(iso9660|squashfs)$'; }; then MOMENT="live" fi # --- La mémoire, barrette par barrette ------------------------------------ # # La question qu'on pose vraiment devant une machine lente : peut-on en # ajouter ? Elle demande trois réponses — combien de connecteurs, combien # d'occupés, et de quel type. Un slot libre en DDR3 ne se remplit pas avec ce # qu'on a dans le tiroir. # # dmidecode lit cela dans la table du BIOS. Il est présent par défaut sur # Debian, Ubuntu et Mint ; s'il manque, on ne prétend rien plutôt que de # deviner. BARRETTES="[" SLOTS_TOTAL=0 SLOTS_PRIS=0 TYPE_MEMOIRE="" VITESSE_MEMOIRE="" if command -v dmidecode >/dev/null 2>&1 && [ "$(id -u)" = "0" ]; then premier=1 # Chaque bloc « Memory Device » décrit un connecteur, occupé ou non. while IFS= read -r bloc; do emplacement="$(printf '%s' "$bloc" | sed -n 's/.*|Locator:\([^|]*\).*/\1/p' | sed 's/^ *//; s/ *$//')" taille="$(printf '%s' "$bloc" | sed -n 's/.*|Size:\([^|]*\).*/\1/p' | sed 's/^ *//; s/ *$//')" type_="$(printf '%s' "$bloc" | sed -n 's/.*|Type:\([^|]*\).*/\1/p' | sed 's/^ *//; s/ *$//')" vitesse="$(printf '%s' "$bloc" | sed -n 's/.*|Speed:\([^|]*\).*/\1/p' | sed 's/^ *//; s/ *$//')" forme="$(printf '%s' "$bloc" | sed -n 's/.*|Form Factor:\([^|]*\).*/\1/p' | sed 's/^ *//; s/ *$//')" reference="$(printf '%s' "$bloc" | sed -n 's/.*|Part Number:\([^|]*\).*/\1/p' | sed 's/^ *//; s/ *$//')" # La vitesse configurée est celle à laquelle la barrette travaille # vraiment ; « Speed » n'est que ce dont elle serait capable. L'écart # entre les deux explique bien des machines qui « rament » sans raison # apparente : une DDR4-3200 bridée à 2133 par la carte mère, ou deux # barrettes dépareillées qui s'alignent sur la plus lente. vitesse_reelle="$(printf '%s' "$bloc" | sed -n 's/.*|Configured Memory Speed:\([^|]*\).*/\1/p' | sed 's/^ *//; s/ *$//')" fabricant="$(printf '%s' "$bloc" | sed -n 's/.*|Manufacturer:\([^|]*\).*/\1/p' | sed 's/^ *//; s/ *$//')" rang="$(printf '%s' "$bloc" | sed -n 's/.*|Rank:\([^|]*\).*/\1/p' | sed 's/^ *//; s/ *$//')" tension="$(printf '%s' "$bloc" | sed -n 's/.*|Configured Voltage:\([^|]*\).*/\1/p' | sed 's/^ *//; s/ *$//')" largeur="$(printf '%s' "$bloc" | sed -n 's/.*|Data Width:\([^|]*\).*/\1/p' | sed 's/^ *//; s/ *$//')" detail="$(printf '%s' "$bloc" | sed -n 's/.*|Type Detail:\([^|]*\).*/\1/p' | sed 's/^ *//; s/ *$//')" [ -z "$emplacement" ] && continue SLOTS_TOTAL=$(( SLOTS_TOTAL + 1 )) case "$taille" in *"No Module"*|""|"Unknown") taille="" ;; *) SLOTS_PRIS=$(( SLOTS_PRIS + 1 )) [ -z "$TYPE_MEMOIRE" ] && [ "$type_" != "Unknown" ] && TYPE_MEMOIRE="$type_" # Deux barrettes de vitesses différentes s'alignent sur la plus # lente : c'est celle-là qu'il faut retenir pour la machine. if [ -n "$vitesse_reelle" ] && [ "$vitesse_reelle" != "Unknown" ]; then nombre="${vitesse_reelle%% *}" case "$nombre" in ''|*[!0-9]*) ;; *) if [ -z "$VITESSE_MEMOIRE" ] || [ "$nombre" -lt "$VITESSE_MEMOIRE" ]; then VITESSE_MEMOIRE="$nombre" fi ;; esac fi ;; esac [ "$premier" = 0 ] && BARRETTES="$BARRETTES," premier=0 BARRETTES="$BARRETTES{$(champ emplacement "$emplacement"),$(champ taille "$taille"),\ $(champ type "$type_"),$(champ vitesse "$vitesse"),$(champ vitesse_reelle "$vitesse_reelle"),\ $(champ forme "$forme"),$(champ reference "$reference"),$(champ fabricant "$fabricant"),\ $(champ rang "$rang"),$(champ tension "$tension"),$(champ largeur "$largeur"),\ $(champ detail "$detail")}" done < <(dmidecode -t memory 2>/dev/null \ | awk '/^Memory Device$/{bloc=""; dedans=1; next} /^$/{if (dedans && bloc != "") print bloc; dedans=0; bloc=""} dedans {gsub(/^[ \t]+/, ""); bloc = bloc "|" $0} END{if (dedans && bloc != "") print bloc}') fi BARRETTES="$BARRETTES]" TABLE_DISQUES="" # --- Les disques ----------------------------------------------------------- # # lsblk donne la taille et le type ; le drapeau « rotational » du noyau # distingue un disque mécanique d'un SSD sans rien installer. C'est la # distinction qui compte : une machine lente avec un disque mécanique de dix # ans ne s'arrangera pas avec un système plus léger. DISQUES="[" premier=1 for chemin in /sys/block/*; do nom="$(basename "$chemin")" case "$nom" in loop*|ram*|sr*|zram*|dm-*) continue ;; esac [ -r "$chemin/size" ] || continue taille=$(( $(cat "$chemin/size") * 512 )) [ "$taille" -lt 1000000000 ] && continue # moins d'un Go : pas un disque rotatif="$(cat "$chemin/queue/rotational" 2>/dev/null || echo '?')" modele="$(cat "$chemin/device/model" 2>/dev/null | sed 's/ *$//')" # 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 pending "$pending"),$(champ illisibles "$illisibles"),$(champ heures "$heures"),\ $(champ usure "$usure"),$(champ ecrit "$ecrit")}" done DISQUES="$DISQUES]" # L'occupation, telle qu'elle est maintenant. OCCUPATION="[" premier=1 while read -r point taille utilise dispo pourcent; do [ "$premier" = 0 ] && OCCUPATION="$OCCUPATION," premier=0 OCCUPATION="$OCCUPATION{$(champ point "$point"),\"taille\":$taille,\ \"utilise\":$utilise,$(champ pourcent "$pourcent")}" done < <(df -B1 --output=target,size,used,avail,pcent 2>/dev/null \ | awk 'NR>1 && $2+0 > 1000000000 {print $1, $2, $3, $4, $5}') OCCUPATION="$OCCUPATION]" # --- Le rapport ------------------------------------------------------------ # L'adresse matérielle de l'interface par laquelle on sort — celle que le # serveur a vue en donnant le bail, donc celle qui permettra de rapprocher # cette fiche de la machine servie. « ip -o link » a failli nous donner # l'adresse de diffusion, qui est la même pour tout le monde. INTERFACE="$(ip -o -4 route show default 2>/dev/null | awk '{print $5; exit}')" [ -z "$INTERFACE" ] && INTERFACE="$(ls /sys/class/net 2>/dev/null \ | grep -v '^lo$' | head -1)" MAC="$(cat "/sys/class/net/$INTERFACE/address" 2>/dev/null)" RAPPORT="{$(champ machine "$(hostname)"),$(champ mac "$MAC"),\ $(champ marque "$MARQUE"),$(champ modele "$MODELE"),$(champ serie "$SERIE"),\ $(champ bios "$BIOS"),$(champ processeur "$PROCESSEUR"),\"coeurs\":${COEURS:-0},\ \"memoire_ko\":${MEMOIRE_KO:-0},$(champ systeme "$SYSTEME"),$(champ noyau "$NOYAU"),\ $(champ architecture "$ARCHITECTURE"),$(champ moment "$MOMENT"),\ $(champ type_memoire "$TYPE_MEMOIRE"),\ $(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" else printf '%s\n' "$RAPPORT" fi exit 0 fi [ "$CHECK" = 1 ] && printf " %sLa fiche est envoyée à alpicache…%s\n\n" "$D" "$Z" echo "Relevé : $([ "$MOMENT" = live ] && echo "session live, avant installation" \ || echo "système installé")" echo "Machine : $MARQUE $MODELE" echo "Processeur : $PROCESSEUR ($COEURS cœurs)" if [ "$SLOTS_TOTAL" -gt 0 ]; then echo "Mémoire : $(( MEMOIRE_KO / 1024 )) Mo ${TYPE_MEMOIRE:+$TYPE_MEMOIRE }\ ${VITESSE_MEMOIRE:+à $VITESSE_MEMOIRE MT/s }· \ $SLOTS_PRIS connecteur(s) occupé(s) sur $SLOTS_TOTAL" else echo "Mémoire : $(( MEMOIRE_KO / 1024 )) Mo" fi echo "Système : $SYSTEME" echo MOTIFS="" # --- Le verdict ------------------------------------------------------------ # # Trois réponses possibles, et une seule question : peut-on installer sur cette # machine ? Les seuils viennent de ce qu'on rencontre en install party, pas # d'un idéal — une machine de dix ans avec 4 Go et un disque sain fait très # bien l'affaire, et c'est souvent celle qu'on nous apporte. # Le verdict sort toujours, live ou non. C'est l'information qu'on est venu # chercher : la première version ne le rendait qu'en session live, et le # relevé d'après installation défilait sans rien dire de l'état des disques — # précisément ce dont on voulait parler au propriétaire. [ "$MONTRER" = 1 ] || CHECK=1 if [ "$CHECK" = 1 ]; then GRAVITE=0 # 0 rien à signaler · 1 à surveiller · 2 ne pas installer REMARQUES="" 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. for chemin in /sys/block/*; do nom="$(basename "$chemin")" case "$nom" in loop*|ram*|sr*|zram*|dm-*) continue ;; esac [ -r "$chemin/size" ] || continue taille=$(( $(cat "$chemin/size") * 512 )) [ "$taille" -lt 1000000000 ] && continue if ! command -v smartctl >/dev/null 2>&1; then note 1 " ${O}?${Z} état des disques inconnu — smartmontools n'est pas installé. ${D}sudo apt install -y smartmontools${Z} ${D}(il passera par le cache)${Z}" break fi sortie="$(smartctl -H -A "/dev/$nom" 2>/dev/null)" 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 : ${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 # 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}$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." fi done # La mémoire, le processeur. MEMOIRE_MO=$(( MEMOIRE_KO / 1024 )) if [ "$MEMOIRE_MO" -lt 2048 ]; then note 2 " ${R}✗${Z} ${B}${MEMOIRE_MO} Mo de mémoire${Z} — trop peu pour Mint. ${D}Envisager une version légère, ou ajouter de la mémoire.${Z}" elif [ "$MEMOIRE_MO" -lt 4096 ]; then libres=$(( SLOTS_TOTAL - SLOTS_PRIS )) note 1 " ${O}!${Z} ${MEMOIRE_MO} Mo de mémoire — Mint tournera, mais sans aisance.$( [ "$libres" -gt 0 ] && printf '\n %s' "${D}$libres connecteur(s) ${TYPE_MEMOIRE} libre(s) : la machine est extensible.${Z}")" fi if [ "$ARCHITECTURE" != "x86_64" ] && [ "$ARCHITECTURE" != "aarch64" ]; then note 2 " ${R}✗${Z} processeur ${ARCHITECTURE} — les versions 64 bits ne s'installeront pas." fi echo case "$GRAVITE" in 0) printf " %s%s BON POUR L'INSTALLATION %s\n" "$B$V" "✓" "$Z" ;; 1) printf " %s%s INSTALLATION POSSIBLE, AVEC DES RÉSERVES %s\n" "$B$O" "!" "$Z" ;; 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 # suit la gravité, et l'on n'attend jamais indéfiniment : le script tourne # aussi sans personne devant l'écran, appelé par install.sh. if [ -t 0 ]; then case "$GRAVITE" in 2) printf " %sEn parler au propriétaire AVANT de toucher à quoi que ce soit.%s\n" "$D" "$Z" printf " %sEntrée pour continuer malgré tout%s " "$B" "$Z" read -r -t "${ALPI_PAUSE_ALERTE:-120}" _ || echo ;; 1) printf " %sEntrée pour continuer (reprise automatique dans %ss)%s " \ "$D" "${ALPI_PAUSE:-20}" "$Z" read -r -t "${ALPI_PAUSE:-20}" _ || echo ;; *) sleep "${ALPI_PAUSE_OK:-3}" ;; esac echo 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")" elif command -v wget >/dev/null 2>&1; then reponse="$(printf '%s' "$RAPPORT" | wget -qO- --timeout=15 \ --header='Content-Type: application/json' --post-file=- "$SERVEUR/fiche")" else echo "Ni curl ni wget : la fiche n'a pas pu être envoyée." >&2 exit 1 fi if [ -n "$reponse" ]; then echo "$reponse" else echo "Le serveur n'a pas répondu — la fiche n'est pas enregistrée." >&2 exit 1 fi