#!/bin/bash # Relève la fiche d'une machine, et l'envoie au serveur de l'install party. # # À récupérer depuis le dépôt du wiki, comme les autres scripts d'Alpinux : # # 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é, et rien d'autre : le modèle et la marque, les quatre # derniers caractères du numéro de série, la date du BIOS, le processeur, la # mémoire et le détail de ses connecteurs, les disques avec leur santé SMART, # l'occupation des systèmes de fichiers, le système installé, l'architecture, # l'adresse matérielle de l'interface réseau et le nom de la machine. # # Ni nom d'utilisateur, ni fichiers, ni historique, ni adresse IP publique. # # Deux réserves à connaître avant de s'en servir, parce qu'elles concernent # des gens et pas des machines : # # - le **nom de la machine** est celui choisi pendant l'installation, et il # contient souvent un prénom (« portable-jean ») ; # - l'**adresse matérielle** identifie durablement la carte réseau ; elle # sert ici à rapprocher la fiche du bail DHCP, rien de plus. # # Le relevé part en clair sur le réseau local de la salle, vers le serveur de # l'association, et n'en sort pas. Prévenez-en la personne dont vous installez # la machine : c'est la sienne. # # 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 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 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. La première version de ce test a # donc déclaré « session live » une machine parfaitement installée — constaté # le 24/09/2026 sur un HP 250 G4. Ce n'est pas anodin : la durée d'une # installation se mesure entre le relevé d'avant et celui d'après, et deux # relevés « avant » ne se soustraient pas. On ne retient donc ces répertoires # que s'ils sont réellement montés, et montés depuis un support en lecture # seule. 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]" # --- 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="" heures="" 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}')" heures="$(printf '%s' "$sortie" | awk '/Power_On_Hours/{print $10; exit}')" fi [ "$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")}" 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}" 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 # --- 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. # Depuis une session live, le verdict est tout l'intérêt de l'exercice : on # le rend sans qu'on ait à le demander. [ "$MOMENT" = "live" ] && 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' } # 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 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 done heures="$(printf '%s' "$sortie" | awk '/Power_On_Hours/{print $10; exit}')" rotatif="$(cat "$chemin/queue/rotational" 2>/dev/null)" case "$heures" in ''|*[!0-9]*) ;; *) if [ "$rotatif" = "1" ] && [ "$heures" -gt 43800 ]; then note 1 " ${O}!${Z} /dev/$nom : disque mécanique de $(( heures / 8760 )) ans. ${D}Un SSD changerait tout sur cette machine.${Z}" fi ;; esac 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 [ -n "$REMARQUES" ] && printf "%s\n" "$REMARQUES" [ "$GRAVITE" = 2 ] && printf " %sEn parler au propriétaire avant de toucher à quoi que ce soit.%s\n\n" "$D" "$Z" fi 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