diff --git a/code/linux/linux-mint/alpi-fiche.sh b/code/linux/linux-mint/alpi-fiche.sh new file mode 100755 index 0000000..e62ce05 --- /dev/null +++ b/code/linux/linux-mint/alpi-fiche.sh @@ -0,0 +1,669 @@ +#!/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 diff --git a/code/linux/linux-mint/install.sh b/code/linux/linux-mint/install.sh index c67e8bc..30cf719 100644 --- a/code/linux/linux-mint/install.sh +++ b/code/linux/linux-mint/install.sh @@ -1,4 +1,26 @@ #!/bin/bash + +################################################## +# Fiche matérielle pour le serveur de l'install party +# +# Placée au début, et non à la fin : si un disque faiblit, autant le savoir +# avant d'avoir passé une heure à installer dessus — c'est le moment où l'on +# peut encore en parler à son propriétaire. Et si l'installation s'arrête en +# route, on a tout de même la fiche. +# +# Le relevé est le script voisin du dépôt, lu et versionné comme celui-ci : +# rien n'est téléchargé ni exécuté depuis le réseau. S'il n'est pas là, on +# passe — la machine d'un particulier qui rejoue ce script chez elle ne doit +# rien avoir à faire de tout ceci. +# +# C'est la seconde passe, celle que « sudo » relance, qui l'exécute : +# dmidecode et smartctl demandent les droits d'administration. +################################################## + +if [ "$(id -u)" = "0" ] && [ -x "$(dirname "$0")/alpi-fiche.sh" ]; then + "$(dirname "$0")/alpi-fiche.sh" || true +fi + # Script de post-installation pour Linux Mint 22.3 # testé le 25/03/2026 diff --git a/docs/guides/linux-mint-depuis-windows.md b/docs/guides/linux-mint-depuis-windows.md index 9987f2c..cb736ee 100644 --- a/docs/guides/linux-mint-depuis-windows.md +++ b/docs/guides/linux-mint-depuis-windows.md @@ -369,6 +369,32 @@ les vérifications qui le nécessitent, puis passe en revue : Il finit par deux listes : *à régler avant d'installer*, et *à savoir*. +### Relever la fiche de la machine + +En install party, un second script complète l'état des lieux : il relève la **fiche +matérielle** — modèle, processeur, mémoire et ses connecteurs, disques et leur santé — +et l'envoie au serveur de l'association. + +```bash +wget http://10.0.0.1/alpi-fiche.sh && chmod +x alpi-fiche.sh && sudo ./alpi-fiche.sh --check +``` + +Lancé depuis la session live, il rend en plus un verdict en trois états : **bon pour +l'installation**, **possible avec des réserves**, ou **ne pas installer en l'état** — un +disque qui se déclare défaillant, des secteurs réalloués, moins de 2 Go de mémoire, un +processeur 32 bits. C'est le dernier moment où l'on peut encore renoncer et en parler au +propriétaire. + +Le même script, relancé après l'installation (sans `--check`), enregistre ce qui a +finalement été installé. Les deux relevés se comparent : un disque qui allait bien le +matin et se plaint le soir, cela se voit. + +!!! warning "Dites-le à la personne dont vous installez la machine" + La fiche part sur le réseau local, vers le serveur de l'association, et n'en sort + pas. Elle ne contient ni fichiers, ni historique, ni nom d'utilisateur — mais elle + contient le **nom de la machine**, souvent un prénom, et l'**adresse matérielle** de + la carte réseau. C'est son ordinateur : cela se demande, cela ne se suppose pas. + !!! danger "BitLocker : ne touchez à rien" Si le script annonce une partition chiffrée par BitLocker, **arrêtez-vous là**. Le chiffrement doit être suspendu depuis Windows — *Paramètres → Confidentialité et @@ -574,22 +600,29 @@ où [le serveur d'Alpinux](../technique/serveur-install-party.md) le sert et met tout ce que la machine téléchargera ensuite : ```bash -wget http://10.0.0.1/install.sh +wget http://10.0.0.1/install.sh http://10.0.0.1/alpi-fiche.sh ``` **ou depuis Internet :** ```bash -wget https://gitea.alpinux.org/alpinux.cedrica5l/alpinux-wiki/raw/branch/main/code/linux/linux-mint/install.sh +wget https://gitea.alpinux.org/alpinux.cedrica5l/alpinux-wiki/raw/branch/main/code/linux/linux-mint/install.sh \ + https://gitea.alpinux.org/alpinux.cedrica5l/alpinux-wiki/raw/branch/main/code/linux/linux-mint/alpi-fiche.sh ``` Puis, dans les deux cas : ```bash -chmod +x install.sh +chmod +x install.sh alpi-fiche.sh ./install.sh ``` +!!! note "Pourquoi deux fichiers" + `install.sh` commence par relever la fiche matérielle, et il ne le fait **que s'il + trouve `alpi-fiche.sh` à côté de lui** — il ne va rien chercher sur le réseau de + lui-même. Téléchargé seul, il s'installera très bien, simplement sans relevé. En + install party, prenez les deux. + Le mot de passe est demandé au passage (`alpinux73` en Install Party) : les caractères ne s'affichent pas, c'est normal.