Compare commits

..

No commits in common. "main" and "doc-messagerie" have entirely different histories.

34 changed files with 85 additions and 1194 deletions

View file

@ -1,702 +0,0 @@
#!/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
# Bash ne compte les caractères — et non les octets — que si la locale le dit.
# C'est ce qui aligne les colonnes du tableau des disques.
export LC_ALL="${LC_ALL:-C.UTF-8}"
# 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=""
# Remplir une colonne à la bonne largeur.
#
# « printf %-10s » compte les octets, pas les caractères : « mécanique » en
# pèse dix pour neuf signes, et la colonne suivante glisse d'un cran. Bash,
# lui, sait compter les caractères quand la locale est en UTF-8 — on lui
# laisse ce travail, et l'on force la locale plutôt que d'espérer.
colonne() {
local texte="$1" largeur="$2" cote="${3:-gauche}" n
n=${#texte}
if [ "$n" -gt "$largeur" ]; then
printf '%s' "${texte:0:$largeur}"
return
fi
local blancs=""
while [ "$n" -lt "$largeur" ]; do blancs="$blancs "; n=$((n + 1)); done
if [ "$cote" = droite ]; then printf '%s%s' "$blancs" "$texte"
else printf '%s%s' "$texte" "$blancs"; fi
}
# --- 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$(
colonne "$nom" 7; printf ' '
colonne "${modele:-—}" 20; printf ' '
colonne "$([ "$rotatif" = 1 ] && echo mécanique || echo SSD)" 10; printf ' '
colonne "$taille_go Go" 10 droite; printf ' '
colonne "${sante:-—}" 8; printf ' '
colonne "$usure_dite" 16; printf ' '
printf '%s' "$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' "$D"
colonne "Disque" 7; printf ' '
colonne "Modèle" 20; printf ' '
colonne "Type" 10; printf ' '
colonne "Taille" 10 droite; printf ' '
colonne "SMART" 8; printf ' '
colonne "Marche / usure" 16; printf ' '
printf 'Secteurs R/P/I%s\n' "$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

View file

@ -1,27 +1,5 @@
#!/bin/bash #!/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 # Script de post-installation pour Linux Mint 22.3
# testé le 25/03/2026 # testé le 25/03/2026
# #
@ -249,7 +227,7 @@ install_logiciels() {
DEBIAN_FRONTEND=noninteractive apt install -y ttf-mscorefonts-installer || abort "problème lors de l'installation des polices" DEBIAN_FRONTEND=noninteractive apt install -y ttf-mscorefonts-installer || abort "problème lors de l'installation des polices"
# mise à jour du système # mise à jour du système
echo -e "==> Mise à jour du système\n" echo "==> Mise à jour du système\n"
apt update || abort "impossible de récupérer les informations des dépôts" apt update || abort "impossible de récupérer les informations des dépôts"
apt full-upgrade -y || abort "erreur lors de la mise à jour des paquets" apt full-upgrade -y || abort "erreur lors de la mise à jour des paquets"
@ -311,7 +289,7 @@ install_logiciels() {
# Mises à jour # # Mises à jour #
################ ################
mises_a_jour() { mises_a_jour() {
echo -e "==> Configuration de dconf\n" echo -e "Configuraiton de dconf\n"
# Configuration de dconf pour permettre de modifier certaines configurations # Configuration de dconf pour permettre de modifier certaines configurations
# par défaut # par défaut
mkdir -p /etc/dconf/profile mkdir -p /etc/dconf/profile
@ -359,32 +337,11 @@ configuration_apt() {
# GRUB : sélection automatique de la dernière entrée utilisée # # GRUB : sélection automatique de la dernière entrée utilisée #
############################################################### ###############################################################
modif_grub() { modif_grub() {
# Rien à faire si c'est déjà en place.
#
# update-grub lance os-prober, qui part sonder toutes les partitions de la
# machine à la recherche d'autres systèmes, puis régénère la configuration
# entière : vingt à trente secondes qu'on ne rattrape pas, et qu'on payait
# jusqu'ici à chaque exécution, y compris quand il n'y avait rien à
# changer. Sur une install party de quinze machines, cela se voit.
if grep -q '^GRUB_DEFAULT=saved' /etc/default/grub \
&& grep -q '^GRUB_SAVEDEFAULT=true' /etc/default/grub; then
echo -e "==> GRUB est déjà configuré, rien à changer\n"
return 0
fi
echo -e "==> Modification de GRUB\n" echo -e "==> Modification de GRUB\n"
# On commente la ligne d'origine, puis on ajoute les nôtres si elles # On comment la ligne "GRUB_DEFAULT=0" et on ajoute les "bonnes" options
# manquent. Les ajouter plutôt que les insérer règle deux choses : le sed -i '/^GRUB_DEFAULT=0/ i#GRUB_DEFAULT=0\
# fichier ne se retrouve plus avec des lignes indentées — GRUB les accepte, GRUB_DEFAULT=saved\
# mais elles trompent la lecture quand on vient déboguer —, et la fonction GRUB_SAVEDEFAULT=true' /etc/default/grub
# fait toujours son travail lorsque « GRUB_DEFAULT=0 » a disparu, ce qui
# arrive dès qu'un autre outil est passé avant nous.
sed -i 's/^GRUB_DEFAULT=0/#&/' /etc/default/grub
grep -q '^GRUB_DEFAULT=saved' /etc/default/grub \
|| echo 'GRUB_DEFAULT=saved' >> /etc/default/grub
grep -q '^GRUB_SAVEDEFAULT=true' /etc/default/grub \
|| echo 'GRUB_SAVEDEFAULT=true' >> /etc/default/grub
# On met à jour GRUB # On met à jour GRUB
update-grub update-grub
} }

View file

@ -58,7 +58,6 @@ Lors de nos **Install Party**, les bénévoles d'**Alpinux** installent Linux av
## 🧰 Après l'installation ## 🧰 Après l'installation
- 🩹 [**Trousse de premiers secours Linux Mint**](../guides/linux-mint-trousse.md) — les réflexes utiles des premiers jours. - 🩹 [**Trousse de premiers secours Linux Mint**](../guides/linux-mint-trousse.md) — les réflexes utiles des premiers jours.
- 🖧 [**Le serveur qu'on emporte**](../technique/serveur-install-party.md) — le réseau de la salle, le cache de paquets, et ce qu'un bénévole doit en savoir.
- 🗣️ [**Ateliers et rencontres**](evenements.md) — pour progresser et échanger avec la communauté. - 🗣️ [**Ateliers et rencontres**](evenements.md) — pour progresser et échanger avec la communauté.
--- ---

View file

@ -17,7 +17,7 @@ demandé.
Rendez-vous sur la page d'accueil de la messagerie de l'association. Rendez-vous sur la page d'accueil de la messagerie de l'association.
![La page d'accueil : votre adresse, la liste, et c'est tout](../assets/messagerie/abonnement-01-accueil.png) <!-- capture : page d'accueil publique, formulaire d'inscription vide -->
Indiquez votre adresse de courriel, choisissez la liste qui vous intéresse, puis Indiquez votre adresse de courriel, choisissez la liste qui vous intéresse, puis
**Continuer**. **Continuer**.
@ -30,7 +30,7 @@ faire une chose, une seule :
Objet : **le code affiché**, recopié tel quel Objet : **le code affiché**, recopié tel quel
Corps du message : ce que vous voulez, il n'est pas lu Corps du message : ce que vous voulez, il n'est pas lu
![Le code à recopier, l'adresse où écrire, et l'heure limite](../assets/messagerie/abonnement-02-code.png) <!-- capture : écran du code, avec le code et l'heure limite -->
Un bouton **Ouvrir mon logiciel de messagerie** prépare le message pour vous. Il ne Un bouton **Ouvrir mon logiciel de messagerie** prépare le message pour vous. Il ne
l'envoie pas : c'est à vous de le faire. l'envoie pas : c'est à vous de le faire.
@ -41,7 +41,7 @@ s'efface d'elle-même — sans aucune conséquence, vous pouvez recommencer.
Dans la minute qui suit votre envoi, vous recevez une réponse qui confirme Dans la minute qui suit votre envoi, vous recevez une réponse qui confirme
l'inscription, rappelle la liste, et porte déjà le lien pour partir. l'inscription, rappelle la liste, et porte déjà le lien pour partir.
![La réponse reçue une fois le code envoyé](../assets/messagerie/abonnement-03-confirmation.png) <!-- capture : le message de confirmation reçu, adresse du destinataire masquée -->
### Pourquoi est-ce à moi d'écrire ? ### Pourquoi est-ce à moi d'écrire ?
@ -86,14 +86,14 @@ C'est ce second emplacement qui fait apparaître un bouton **Se désabonner** en
du message dans les logiciels modernes — Gmail, Outlook, Thunderbird, Apple Mail. Un du message dans les logiciels modernes — Gmail, Outlook, Thunderbird, Apple Mail. Un
clic suffit. clic suffit.
![Le bouton « Se désabonner » qu'affiche votre logiciel de messagerie](../assets/messagerie/abonnement-04-bouton-desabonner.png) <!-- capture : un message reçu, bouton « Se désabonner » du client de messagerie -->
La page qui s'ouvre vous laisse choisir : La page qui s'ouvre vous laisse choisir :
- quitter **cette liste seulement** ; - quitter **cette liste seulement** ;
- ou quitter **toutes les listes** de l'association d'un coup. - ou quitter **toutes les listes** de l'association d'un coup.
![Quitter cette liste, ou toutes](../assets/messagerie/abonnement-05-desabonnement.png) <!-- capture : page de désabonnement, adresse masquée -->
Aucune justification n'est demandée. Le retrait est immédiat, et il n'y a personne à Aucune justification n'est demandée. Le retrait est immédiat, et il n'y a personne à
qui écrire. qui écrire.
@ -109,17 +109,16 @@ qui écrire.
Les listes ouvertes au public ont des **archives** consultables sans compte. C'est Les listes ouvertes au public ont des **archives** consultables sans compte. C'est
utile avant de s'abonner, pour voir à quoi on s'engage. utile avant de s'abonner, pour voir à quoi on s'engage.
![Les envois passés d'une liste publique](../assets/messagerie/abonnement-06-archives.png) <!-- capture : index des archives, liste des envois par date -->
Seuls y figurent les messages **réellement envoyés** : ni les brouillons, ni les Seuls y figurent les messages **réellement envoyés** : ni les brouillons, ni les
envois annulés. envois annulés.
!!! info "Aucune adresse de courriel n'apparaît dans les archives" !!! info "Aucune adresse de courriel n'apparaît dans les archives"
Les adresses citées dans le corps d'un message — signature, contact d'un Les adresses citées dans le corps d'un message — signature, contact d'un
événement, adresse d'un partenaire — y sont masquées : `s****@a****.xxx`. On voit événement, adresse d'un partenaire — y sont remplacées par `[adresse masquée]`.
qu'un contact existait, sans pouvoir le reconstituer ni même savoir chez quel Leurs titulaires n'ont pas consenti à une publication sur le web, où un robot les
fournisseur il est hébergé. Leurs titulaires n'ont pas consenti à une publication ramasserait pour en faire du spam.
sur le web, où un robot les ramasserait pour en faire du spam.
--- ---

View file

@ -25,7 +25,7 @@ automate le distribue en lissant le débit pendant que vous faites autre chose.
Menu **Listes → Campagnes**, puis **Nouvelle campagne**. Menu **Listes → Campagnes**, puis **Nouvelle campagne**.
![La rédaction, avec l'aperçu de ce qui partira](../assets/messagerie/gestionnaire-01-redaction.png) <!-- capture : formulaire de rédaction, aperçu à côté -->
Choisissez la liste destinataire, écrivez l'objet et le corps. Choisissez la liste destinataire, écrivez l'objet et le corps.
@ -66,7 +66,7 @@ Deux choses valent le coup d'œil :
Bouton **Mettre en file d'envoi…**. Une confirmation vous est demandée, avec le Bouton **Mettre en file d'envoi…**. Une confirmation vous est demandée, avec le
nombre de destinataires. nombre de destinataires.
![Le dernier écran avant que la file se remplisse](../assets/messagerie/gestionnaire-02-confirmation.png) <!-- capture : écran de confirmation avant mise en file -->
L'envoi se fait ensuite tout seul, une minute sur l'autre, **sous un plafond horaire** L'envoi se fait ensuite tout seul, une minute sur l'autre, **sous un plafond horaire**
fixé pour ne pas saturer le serveur. Une liste de quelques centaines de personnes fixé pour ne pas saturer le serveur. Une liste de quelques centaines de personnes
@ -78,7 +78,7 @@ Vous pouvez fermer la page : l'envoi continue.
La fiche de la campagne affiche la progression : envoyés, en attente, échecs. La fiche de la campagne affiche la progression : envoyés, en attente, échecs.
![L'envoi en cours : remis, en attente, échecs](../assets/messagerie/gestionnaire-03-progression.png) <!-- capture : fiche de campagne en cours d'envoi, barre de progression -->
Si le compteur semble figé, c'est probablement le plafond horaire : l'envoi reprend Si le compteur semble figé, c'est probablement le plafond horaire : l'envoi reprend
de lui-même à l'heure suivante. de lui-même à l'heure suivante.
@ -89,10 +89,6 @@ de lui-même à l'heure suivante.
est déjà distribué ne revient pas. est déjà distribué ne revient pas.
- **Dupliquer en brouillon** recrée une copie modifiable d'un envoi passé — pratique - **Dupliquer en brouillon** recrée une copie modifiable d'un envoi passé — pratique
pour une lettre dont la structure ne change pas d'un mois sur l'autre. pour une lettre dont la structure ne change pas d'un mois sur l'autre.
- **Supprimer ce brouillon…** efface un brouillon abandonné. Une confirmation est
demandée, et il n'y a pas de corbeille. Seul un brouillon se supprime : une
campagne partie reste, puisque des gens l'ont reçue et qu'elle figure dans les
archives.
--- ---
@ -100,7 +96,7 @@ de lui-même à l'heure suivante.
Menu **Listes → Listes de diffusion**, puis le formulaire de création. Menu **Listes → Listes de diffusion**, puis le formulaire de création.
![Créer une liste : le nom et la description suffisent](../assets/messagerie/gestionnaire-04-creer-liste.png) <!-- capture : formulaire de création d'une liste -->
Seuls **le nom** et **la description** sont demandés. Le reste est automatique : Seuls **le nom** et **la description** sont demandés. Le reste est automatique :
@ -122,7 +118,7 @@ cela désoriente les destinataires.
Menu **Listes → Listes de diffusion**, puis la liste voulue. Menu **Listes → Listes de diffusion**, puis la liste voulue.
![La fiche d'une liste, adresses masquées par défaut](../assets/messagerie/gestionnaire-05-fiche-liste.png) <!-- capture : fiche de liste, compteurs et tableau des abonnés, adresses masquées -->
Quatre compteurs résument l'état : destinataires joignables, inscrits au total, Quatre compteurs résument l'état : destinataires joignables, inscrits au total,
désabonnés, adresses mortes. désabonnés, adresses mortes.
@ -158,7 +154,7 @@ Deux moyens, dans le volet **Ajouter des abonnés** :
Sur la fiche de la liste, cochez **Liste publique** et enregistrez. Sur la fiche de la liste, cochez **Liste publique** et enregistrez.
![Ouvrir une liste à l'inscription libre](../assets/messagerie/gestionnaire-06-liste-publique.png) <!-- capture : case « Liste publique » cochée -->
Trois conséquences, à peser avant de cocher : Trois conséquences, à peser avant de cocher :
@ -174,7 +170,7 @@ dans le journal de l'outil.
Quand une liste est publique, la fiche affiche les **demandes d'inscription en Quand une liste est publique, la fiche affiche les **demandes d'inscription en
attente de confirmation**, avec leur heure d'expiration. attente de confirmation**, avec leur heure d'expiration.
![Les demandes en attente de confirmation](../assets/messagerie/gestionnaire-07-demandes.png) <!-- capture : tableau des demandes en attente, adresses masquées -->
Ces adresses **ne sont pas inscrites** et ne le seront que si leurs titulaires Ces adresses **ne sont pas inscrites** et ne le seront que si leurs titulaires
confirment depuis leur propre boîte. Il n'y a rien à faire : celles qui restent confirment depuis leur propre boîte. Il n'y a rien à faire : celles qui restent
@ -193,7 +189,7 @@ L'écran comporte deux tableaux, et la différence entre les deux est importante
Ces adresses ont été retirées de **toutes** les listes : le serveur d'en face a Ces adresses ont été retirées de **toutes** les listes : le serveur d'en face a
répondu que la boîte n'existait pas, ou la personne nous a signalés comme spam. répondu que la boîte n'existait pas, ou la personne nous a signalés comme spam.
![Les rejets définitifs, avec le texte exact du refus](../assets/messagerie/gestionnaire-08-rejets.png) <!-- capture : tableau des rejets définitifs, adresses masquées -->
La colonne de droite affiche **le texte exact du refus**, extrait du rapport reçu — La colonne de droite affiche **le texte exact du refus**, extrait du rapport reçu —
« utilisateur inconnu », « boîte pleine », le nom d'une liste noire. Le rapport « utilisateur inconnu », « boîte pleine », le nom d'une liste noire. Le rapport
@ -210,7 +206,7 @@ Le message a été refusé non pas parce que la boîte a disparu, mais parce qu'
opérateur refuse *notre* serveur — un grand fournisseur de messagerie qui nous a mis opérateur refuse *notre* serveur — un grand fournisseur de messagerie qui nous a mis
sur une liste d'attente, ou un serveur qui interroge une liste noire. sur une liste d'attente, ou un serveur qui interroge une liste noire.
![Les blocages d'opérateur : ces adresses restent inscrites](../assets/messagerie/gestionnaire-09-blocages.png) <!-- capture : tableau des blocages d'opérateur -->
La distinction est vitale. Une part notable des abonnés d'une association tient ses La distinction est vitale. Une part notable des abonnés d'une association tient ses
adresses chez deux ou trois grands fournisseurs : les traiter comme mortes parce que adresses chez deux ou trois grands fournisseurs : les traiter comme mortes parce que
@ -230,7 +226,7 @@ Les grands opérateurs de messagerie envoient chaque jour un rapport disant **qu
adresses IP ont émis du courrier au nom du domaine de l'association**, et si ces adresses IP ont émis du courrier au nom du domaine de l'association**, et si ces
envois étaient correctement signés. L'outil les dépouille tout seul. envois étaient correctement signés. L'outil les dépouille tout seul.
![Qui émet au nom du domaine, et si c'est aligné](../assets/messagerie/gestionnaire-10-postmaster.png) <!-- capture : tableau de bord postmaster, colonnes IP / messages / alignés -->
Deux lectures valent le détour : Deux lectures valent le détour :
@ -249,7 +245,7 @@ Le bouton **Marquer comme lu** le retire de la liste une fois traité.
*Réservé aux administrateurs.* Menu **Configuration → Droits**. *Réservé aux administrateurs.* Menu **Configuration → Droits**.
![Les trois rôles, cumulables](../assets/messagerie/gestionnaire-11-droits.png) <!-- capture : écran des droits, noms anonymisés -->
Trois rôles indépendants, cumulables : Trois rôles indépendants, cumulables :

View file

@ -22,7 +22,7 @@ répond sans connaître la vôtre.
Menu **Messagerie → Mes messages**. Menu **Messagerie → Mes messages**.
![La boîte de réception ; les messages non lus sont en gras](../assets/messagerie/membre-01-boite.png) <!-- capture : boîte de réception, noms des correspondants anonymisés -->
Le nombre de messages non lus apparaît à côté du mot **Messagerie** dans le menu : Le nombre de messages non lus apparaît à côté du mot **Messagerie** dans le menu :
vous le voyez donc depuis n'importe quel écran, sans avoir à aller voir. vous le voyez donc depuis n'importe quel écran, sans avoir à aller voir.
@ -35,7 +35,7 @@ Un message s'ouvre d'un clic sur son objet. Il est marqué lu à ce moment-là.
Menu **Messagerie → Écrire**. Menu **Messagerie → Écrire**.
![Le destinataire se choisit dans l'annuaire, sans connaître son adresse](../assets/messagerie/membre-02-ecrire.png) <!-- capture : formulaire d'écriture, annuaire déroulé, noms anonymisés -->
Choisissez le destinataire dans la liste proposée : c'est l'annuaire des membres. Choisissez le destinataire dans la liste proposée : c'est l'annuaire des membres.
**Vous n'avez pas besoin de son adresse** — vous ne la verrez pas, et il ne verra pas **Vous n'avez pas besoin de son adresse** — vous ne la verrez pas, et il ne verra pas
@ -58,7 +58,7 @@ inutile — autant s'écrire directement.
Menu **Configuration → Mon profil**. Menu **Configuration → Mon profil**.
![Les avis par courriel et le résumé quotidien se règlent ici](../assets/messagerie/membre-03-profil.png) <!-- capture : préférences, adresse de l'utilisateur masquée -->
Vous y choisissez : Vous y choisissez :

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

View file

@ -1,23 +0,0 @@
# wiki.alpinux.org
Documentation publique Alpinux — MkDocs Material.
## Déploiement
```bash
cd org.alpinux.owni/wiki
mkdocs build --strict
rsync -rlcz --delete site/ alpinux.org:/var/www/wiki.alpinux.org/web/
```
## Développement local
```bash
pip install mkdocs-material
mkdocs serve # http://localhost:8000
```
## Notes
- Pas de service systemd — site statique servi directement par Apache
- Les docs publiques sont dans `wiki/docs/technique/`

View file

@ -104,6 +104,10 @@ Clic droit sur **Démarrer** → *Terminal (administrateur)*, puis :
powercfg /hibernate off powercfg /hibernate off
``` ```
!!! tip "Le dernier arrêt de Windows"
Au moment d'éteindre Windows pour insérer la clé USB, maintenez **Maj (Shift)**
enfoncée en cliquant sur *Démarrer → Arrêter*. Cela force un arrêt complet.
### Gestion de l'alimentation de la carte Wi-Fi ### Gestion de l'alimentation de la carte Wi-Fi
Certaines cartes Wi-Fi (puces Realtek ou Broadcom notamment) restent dans l'état de basse Certaines cartes Wi-Fi (puces Realtek ou Broadcom notamment) restent dans l'état de basse
@ -184,8 +188,6 @@ Linux. Il faut le basculer sur **AHCI**, dans la section *Storage*, *SATA Config
conserver Windows, faites le changement avec un bénévole. Si Windows est de toute conserver Windows, faites le changement avec un bénévole. Si Windows est de toute
façon remplacé, la bascule est sans conséquence. façon remplacé, la bascule est sans conséquence.
--- ---
## 2. Préparer son support d'installation ## 2. Préparer son support d'installation
@ -244,11 +246,8 @@ Avec [Ventoy](ventoy.md), la préparation tient en un copier-coller :
### Branchements ### Branchements
- Insérez la clé Ventoy dans un **port USB de la machine**, en évitant les hubs. - Insérez la clé Ventoy dans un **port USB de la machine**, en évitant les hubs.
- **Branchez un câble Ethernet.** Le script install.sh de Alpinux effectue des tests. - **Branchez un câble Ethernet.** Mint pourra télécharger mises à jour, codecs et paquets
de langue pendant l'installation — cela évite beaucoup de réglages ensuite.
!!! tip "Le dernier arrêt de Windows - Comment arrêter Windows pour démarrer sur la clé USB"
Au moment d'éteindre Windows pour insérer la clé USB, maintenez **Maj (Shift)**
enfoncée en cliquant sur *Démarrer → Arrêter*. Cela force un arrêt complet.
### Accéder au menu de démarrage ### Accéder au menu de démarrage
@ -342,27 +341,18 @@ fatigué. On ne le découvre pas au milieu du partitionnement.
En session live, taper les commandes une à une est fastidieux. Un script d'Alpinux fait En session live, taper les commandes une à une est fastidieux. Un script d'Alpinux fait
l'inventaire et rend un verdict : l'inventaire et rend un verdict :
**Sur le réseau de l'install party**, où les scripts sont servis par
[le serveur qu'Alpinux emporte](../technique/serveur-install-party.md) :
```bash ```bash
wget http://alpicache.lan/verif-disque.sh && chmod +x verif-disque.sh && ./verif-disque.sh wget https://gitea.alpinux.org/alpinux.cedrica5l/alpinux-wiki/raw/branch/main/code/linux/linux-mint/verif-disque.sh
```
!!! tip "Si le nom ne répond pas"
Le serveur répond aussi à `alpicache`, `apt-proxy` et `10.0.0.1` — cette
dernière adresse fonctionne même quand la résolution de noms n'est pas encore
en place :
`wget http://10.0.0.1/verif-disque.sh`
**Ailleurs, depuis Internet :**
```bash
wget https://gitea.alpinux.org/alpinux.cedrica5l/alpinux.scripts/raw/branch/stable/poste/verif-disque.sh
chmod +x verif-disque.sh chmod +x verif-disque.sh
./verif-disque.sh ./verif-disque.sh
``` ```
Sur le réseau de l'install party, plus court :
```bash
wget http://10.0.0.1/verif-disque.sh && chmod +x verif-disque.sh && ./verif-disque.sh
```
Il **ne modifie rien** — il lit, il affiche, il conseille. Il demande le mot de passe pour Il **ne modifie rien** — il lit, il affiche, il conseille. Il demande le mot de passe pour
les vérifications qui le nécessitent, puis passe en revue : les vérifications qui le nécessitent, puis passe en revue :
@ -378,40 +368,6 @@ les vérifications qui le nécessitent, puis passe en revue :
Il finit par deux listes : *à régler avant d'installer*, et *à savoir*. 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://alpicache.lan/alpi-fiche.sh && chmod +x alpi-fiche.sh && sudo ./alpi-fiche.sh --check
```
Hors réseau de l'install party, il se prend sur Internet :
```bash
wget https://gitea.alpinux.org/alpinux.cedrica5l/alpinux.scripts/raw/branch/stable/poste/alpi-fiche.sh
```
Il n'aura alors aucun serveur à qui remettre la fiche : il l'affiche, et c'est tout.
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" !!! danger "BitLocker : ne touchez à rien"
Si le script annonce une partition chiffrée par BitLocker, **arrêtez-vous là**. Le 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 chiffrement doit être suspendu depuis Windows — *Paramètres → Confidentialité et
@ -612,34 +568,25 @@ Le système est installé ; quelques réglages le rendent réellement utilisable
### Le script de post-installation Alpinux ### Le script de post-installation Alpinux
Il automatise ce qui manque presque toujours après une installation standard. Ouvrez un Il automatise ce qui manque presque toujours après une installation standard. Ouvrez un
terminal (`Ctrl + Alt + T`) et récupérez-le — **sur le réseau local de l'Install Party**, terminal (`Ctrl + Alt + T`) et récupérez-le — **sur le réseau local de l'Install Party** :
où [le serveur d'Alpinux](../technique/serveur-install-party.md) le sert et met en cache
tout ce que la machine téléchargera ensuite :
```bash ```bash
wget http://alpicache.lan/install.sh http://alpicache.lan/alpi-fiche.sh wget http://10.0.0.1/install.sh
``` ```
**ou depuis Internet :** **ou depuis Internet :**
```bash ```bash
wget https://gitea.alpinux.org/alpinux.cedrica5l/alpinux.scripts/raw/branch/stable/poste/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.scripts/raw/branch/stable/poste/alpi-fiche.sh
``` ```
Puis, dans les deux cas : Puis, dans les deux cas :
```bash ```bash
chmod +x install.sh alpi-fiche.sh chmod +x install.sh
./install.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 Le mot de passe est demandé au passage (`alpinux73` en Install Party) : les caractères ne
s'affichent pas, c'est normal. s'affichent pas, c'est normal.

View file

@ -109,31 +109,16 @@ Les **Préférences** s'organisent en trois onglets :
!!! tip "Le piège du `/boot` plein" !!! tip "Le piège du `/boot` plein"
Les anciens noyaux s'accumulent et finissent par saturer la partition de démarrage, Les anciens noyaux s'accumulent et finissent par saturer la partition de démarrage,
ce qui bloque toute nouvelle mise à jour. L'onglet *Automatisation* permet de ce qui bloque toute nouvelle mise à jour. L'onglet *Automatisation* permet de
programmer leur suppression ; à défaut, passez de temps en temps par programmer leur suppression ; à défaut, retirez-les de temps en temps depuis
**Affichage → Noyaux Linux**, qui les liste et permet de retirer les plus anciens. l'*Analyseur de noyaux* du gestionnaire.
!!! warning "Changer de noyau n'est pas anodin"
La même fenêtre *Noyaux Linux* s'ouvre sur un avertissement qu'il vaut la peine de
lire : un noyau plus récent peut faire disparaître le Wi-Fi, l'accélération graphique,
ou donner un écran noir au démarrage — surtout avec des pilotes propriétaires.
La sortie de secours tient en quatre gestes, à connaître **avant** d'en installer un :
redémarrer, maintenir **Maj** pour afficher le menu GRUB, choisir *Advanced options*
puis un noyau antérieur, et désinstaller le plus récent une fois revenu.
### Activer le pare-feu ### Activer le pare-feu
Le pare-feu est installé, mais **désactivé par défaut**. Pour un ordinateur portable qui Le pare-feu est installé, mais **désactivé par défaut**. Pour un ordinateur portable qui
se connecte à des réseaux publics, il vaut mieux l'allumer : se connecte à des réseaux publics, il vaut mieux l'allumer :
**Paramètres du système → Pare-feu** (famille *Administration*). L'ouverture demande votre **Paramètres du système → Pare-feu** (famille *Administration*), puis basculez **État** sur *Activé*. Le profil *Public* convient :
mot de passe, puis il suffit de basculer **État** sur *Activé*. il bloque les connexions entrantes et laisse passer tout ce que vous initiez.
Le profil proposé par défaut, **Domestique**, convient : les deux lignes en dessous
annoncent la règle appliquée — *Entrant : Refuser*, *Sortant : Autoriser*. Autrement dit,
rien ne rentre sans y avoir été invité, tout ce que vous lancez sort normalement. Les
profils *Bureau* et *Public* servent à mémoriser d'autres jeux de règles ; sans besoin
précis, n'y touchez pas.
### Vérifier ses pilotes ### Vérifier ses pilotes

View file

@ -181,57 +181,6 @@ Une exécution réussie encadre le build par une ligne `Deploy started` et une l
--- ---
## Ce que le build publie pour alpinux.org
Chaque build écrit deux fichiers à la racine du site, avec pour chaque page son titre,
son adresse et la date du commit qui a touché son contenu :
| Fichier | Ce qu'il porte | Qui le lit |
|---|---|---|
| `derniers-articles.json` | Les vingt dernières pages créées ou modifiées | L'accueil d'alpinux.org, section « Le wiki, fraîchement mis à jour » |
| `toutes-les-pages.json` | Toutes les pages, sans limite, avec leur rubrique | La page des mises à jour d'alpinux.org |
Le second est la liste entière, le premier son début : un seul parcours de l'historique
git les produit tous les deux, et ils portent la même date de génération.
Ils sont produits par le hook `hooks/derniers_articles.py`, déclaré dans `mkdocs.yml`.
Trois choses à savoir :
- **Un déplacement n'est pas une mise à jour.** Les dates viennent de `git log` avec
suivi des renommages : déplacer `guides/truc.md` ne fait pas remonter l'article en
tête de liste, seule une modification de son texte le fait.
- **Seules les pages de la navigation sont annoncées.** Une page absente de `nav` —
typiquement la redirection laissée derrière un article déplacé — n'apparaît jamais
dans la liste.
- **Le hook n'échoue jamais.** Sans historique git, il n'écrit rien et le build
continue ; alpinux.org masque alors simplement ce qu'il ne peut pas remplir. Ses
messages sont pour cela de niveau *info* et jamais *warning* : le déploiement
construit avec `--strict`, où un seul avertissement suffit à interrompre le build,
et donc la publication.
Le même hook pose l'en-tête `Access-Control-Allow-Origin` sur ces deux fichiers, et
sur eux seuls, au moyen d'un `.htaccess` déposé à la racine du site construit. Sans
lui, le navigateur refuserait à alpinux.org le droit de lire une réponse venue de
wiki.alpinux.org.
!!! warning "Deux conditions"
**Le clone doit avoir son historique.** Un clone `--depth 1` ne porte qu'un
commit : toutes les pages sembleraient modifiées le même jour. Le hook le détecte
et le signale dans le journal du build, sans rien casser.
**Un `.htaccess` posé à la main sur le serveur est effacé au déploiement suivant**,
puisque le staging est recopié avec `rsync --delete`. Pour ajouter des règles
Apache durables, placez-les dans `docs/.htaccess` : MkDocs le recopie dans le site
construit, et le hook **ajoute** son bloc à la suite au lieu de le remplacer.
```bash
# vérifier ce que le dernier build a publié
curl -s https://wiki.alpinux.org/derniers-articles.json | head -20
curl -s https://wiki.alpinux.org/toutes-les-pages.json | head -20
```
---
## En cas d'erreur de build ## En cas d'erreur de build
Si `mkdocs build` échoue, le site en ligne **n'est pas modifié** — l'ancien contenu reste en place. Lisez le message d'erreur : il indique généralement le fichier et la ligne problématiques. Si `mkdocs build` échoue, le site en ligne **n'est pas modifié** — l'ancien contenu reste en place. Lisez le message d'erreur : il indique généralement le fichier et la ligne problématiques.
@ -263,6 +212,4 @@ Ouvrez [http://localhost:8000](http://localhost:8000) — MkDocs recharge automa
| Vérifier le build avant de pousser | `mkdocs build --strict -d /tmp/wiki-build` | | Vérifier le build avant de pousser | `mkdocs build --strict -d /tmp/wiki-build` |
| Déployer à la main (webhook en panne) | `deploy-wiki.sh`, sur le serveur | | Déployer à la main (webhook en panne) | `deploy-wiki.sh`, sur le serveur |
| Voir le journal des déploiements | `tail -20 /var/log/wiki-deploy.log` | | Voir le journal des déploiements | `tail -20 /var/log/wiki-deploy.log` |
| Vérifier la liste envoyée à l'accueil | `curl -s https://wiki.alpinux.org/derniers-articles.json` |
| Vérifier l'inventaire complet publié | `curl -s https://wiki.alpinux.org/toutes-les-pages.json` |
| Régénérer les fichiers du logo (poste local) | `python3 scripts/build-assets.py` | | Régénérer les fichiers du logo (poste local) | `python3 scripts/build-assets.py` |

View file

@ -1,169 +0,0 @@
---
description: Le serveur qu'Alpinux emporte en install party — partage de connexion, réseau local, cache de paquets APT, page de santé, et ce qu'un bénévole doit en savoir sur place.
---
# Le serveur qu'on emporte en install party
Quinze ordinateurs qui s'installent le même matin, c'est quinze fois les mêmes
cinq cents mégaoctets tirés par la ligne de la salle — quand il y en a une. Alpinux
apporte donc sa propre machine, **alpicache**, qui partage l'accès à Internet, sert un
réseau local aux postes en cours d'installation, et **garde en cache les paquets déjà
téléchargés**.
!!! note "Pour qui est cette page"
Pour les bénévoles qui installent, et pour qui prépare la salle. La configuration
complète de la machine — interfaces, services, dépannage détaillé — vit dans son
propre dépôt, `alpinux.alpicache`, et non ici.
---
## Le réseau de la salle
```mermaid
flowchart LR
A[Prise murale<br/>ou box de la salle] --> B[alpicache<br/>10.0.0.1]
B --> C[Switch]
C --> D[Poste 1]
C --> E[Poste 2]
C --> F[Poste n]
```
La machine tient les deux bouts : d'un côté elle demande un accès à la salle, de l'autre
elle distribue un réseau en **10.0.0.x** aux postes branchés sur le switch. Les rôles ne
sont pas figés à l'avance : elle désigne comme sortie la première prise où on lui répond,
et sert le réseau local sur l'autre.
!!! warning "Les postes se branchent sur le switch, jamais sur la box"
Un poste branché directement sur la box de la salle reçoit une adresse de la salle :
il ne voit plus le cache, ne trouve plus les scripts, et fait tomber les compteurs à
zéro. C'est l'erreur la plus fréquente, et elle ne se voit pas tout de suite.
---
## Ce que la machine offre, une fois branchée
| Adresse | À quoi ça sert |
| --- | --- |
| **<http://alpicache.lan/>** | La page de santé : réseau, disques, machines servies, cache |
| **`http://apt-proxy:3142`** | Le cache de paquets, à déclarer comme mandataire |
| **`http://10.0.0.1/`** | Les scripts d'Alpinux, servis sur place — dont `verif-disque.sh` et `install.sh` |
La page de santé se rafraîchit seule ; c'est le tableau de bord de la journée. Quatre
cartes à surveiller :
| Carte | Ce qu'on veut y voir |
| --- | --- |
| **Réseau** | une sortie *et* un réseau local nommés, « Internet joignable » |
| **Disques** | le disque du cache présent, avec de la place |
| **Machines servies** | le nombre grandit à mesure que les postes arrivent |
| **Cache de paquets** | « Économisé » qui monte au fil de la matinée |
**« Machines servies » qui reste à zéro** alors que des postes sont branchés, c'est le
signe que le réseau local ne distribue pas d'adresses : prévenez la personne qui tient la
machine plutôt que de chercher sur le poste du participant.
---
## Faire passer une installation par le cache
C'est là que tout se joue, et le geste n'est pas le même selon le système.
### Debian
L'installateur demande un mandataire à l'écran **« Configurer le miroir de l'archive
Debian »**. Saisissez-y :
```
http://apt-proxy:3142
```
C'est **le seul moment** où on peut le faire, et c'est celui qui compte : toute
l'installation passe alors par le cache.
### Linux Mint et Ubuntu
Leur installateur ne demande pas de mandataire : l'installation elle-même sortira sur
Internet. Le gain arrive juste après — sur les mises à jour et les logiciels ajoutés,
c'est-à-dire sur l'essentiel du volume.
Une fois la machine redémarrée sur son nouveau système :
```bash
sudo apt install auto-apt-proxy
```
Ce paquet cherche un cache sur le réseau local et s'en sert quand il en trouve un — sans
rien à configurer, et sans gêner la machine une fois rentrée chez son propriétaire.
!!! tip "Le script de post-installation le fait déjà"
Le [script d'Alpinux](../guides/linux-mint-depuis-windows.md#le-script-de-post-installation-alpinux)
installe `auto-apt-proxy` parmi ses réglages : si vous le lancez, c'est fait.
---
## Ce qui coince, et la parade
**La salle n'a pas de réseau.** Un téléphone Android en partage de connexion USB fait
l'affaire : branché sur la machine, il devient la sortie. Prévoyez le câble.
**Le disque du cache n'est pas vu.** Il doit être branché **avant** d'allumer la machine.
Après coup, le cache démarre sans rien dire et ne garde rien — la carte *Disques* de la
page de santé le montre tout de suite.
**Un poste n'obtient pas d'adresse.** Vérifiez d'abord le câble et le switch, puis la
carte *Machines servies*. Une adresse en `169.254.x.x` signifie que le poste n'a reçu
aucune réponse : il s'en est inventé une.
**`apt` renvoie des « 503 Connection closed » en série.** Le cache accepte la requête mais
ne sait pas où aller la chercher — c'est arrivé le 24/09/2026, et une installation Mint
s'y est arrêtée. Le piège est qu'**interroger le cache directement répond parfaitement** :
les deux chemins ne suivent pas la même route à l'intérieur du serveur. Ne cherchez donc
pas sur la machine du participant : prévenez la personne qui tient le serveur.
---
## Pour préparer une install party
La veille, la machine s'allume chez quelqu'un, sur une ligne rapide, et l'on garnit le
cache avec ce qu'on va installer — si la version de Mint a changé depuis la dernière
sortie, autant la tirer maintenant.
!!! warning "Le cache ne sert que ce qu'il connaît"
Le pré-chargement vise des versions nommées, et une version absente de la liste ne
profite à personne. Le 24/09/2026, la première Mint 22.3 de la soirée a tiré **1,3 Go
d'Internet pour 82 Mo servis par le cache** : le pré-chargeur couvrait encore les
versions précédentes.
Avant chaque sortie, vérifiez donc que la liste des versions pré-chargées correspond à
celle que vous allez réellement installer. C'est par le **nom de code** que le
pré-chargement les désigne, et un décalage d'un cran suffit à garnir le cache de la
mauvaise version :
| Version | Nom de code |
|---|---|
| Mint 22 | `wilma` |
| Mint 22.1 | `xia` |
| Mint 22.2 | `zara` |
| Mint 22.3 | `zena` |
Pour mémoire, la série précédente : `vanessa` (21), `vera` (21.1), `victoria` (21.2),
`virginia` (21.3).
Sur place, l'ordre de branchement n'est pas une superstition : le disque externe d'abord,
puis le câble Internet **seul**, puis la machine, et seulement ensuite le switch et les
postes. Brancher la sortie seule d'abord, c'est ôter à la machine toute occasion de se
tromper de rôle.
La procédure complète — ce qu'on emporte, les vérifications, le dépannage, la fiche à
cocher pour chaque participant et l'affiche à scotcher près du switch — est dans le dépôt
`alpinux.alpicache`. Demandez-la à un membre du bureau avant une sortie.
---
## Voir aussi
- [Install Party](../alpinux/install-party.md) — les dates, et comment y participer
- [Installer Linux Mint depuis Windows](../guides/linux-mint-depuis-windows.md) — le guide
suivi sur place
- [Préparation d'un serveur Debian 13](serveur-debian-13.md) — pour la machine, la base
système est la même

View file

@ -1,15 +1,8 @@
"""Publie ce que le wiki a écrit ou repris, pour qui l'affiche ailleurs. """Publie `derniers-articles.json` : ce que le wiki a écrit ou repris récemment.
Deux fichiers, écrits à chaque build à côté des pages et servis avec l'en-tête CORS La page d'accueil d'alpinux.org lit ce fichier pour afficher « Le wiki, fraîchement
sans lequel un autre domaine ne peut pas les lire : mis à jour ». Il est donc écrit à chaque build, à côté des pages, et servi avec
l'en-tête CORS sans lequel un autre domaine ne peut pas le lire.
- `derniers-articles.json` — les vingt dernières pages touchées, que la page d'accueil
d'alpinux.org lit pour sa section « Le wiki, fraîchement mis à jour » ;
- `toutes-les-pages.json` — le même inventaire, sans limite de nombre et avec la
rubrique de chaque page, pour la page des mises à jour d'alpinux.org.
Les deux sortent du même calcul : le second est la liste entière, le premier son
début.
Les dates viennent de git, pas du système de fichiers : un `git clone` repose tous les Les dates viennent de git, pas du système de fichiers : un `git clone` repose tous les
fichiers à la même seconde, et une page recopiée depuis Obsidian aurait l'air neuve. fichiers à la même seconde, et une page recopiée depuis Obsidian aurait l'air neuve.
@ -27,11 +20,10 @@ from urllib.parse import urljoin
from mkdocs.plugins import log from mkdocs.plugins import log
FICHIER_RECENTS = "derniers-articles.json" FICHIER = "derniers-articles.json"
FICHIER_TOUTES = "toutes-les-pages.json"
# Ce que le premier fichier porte au plus. L'accueil n'en affiche que six, mais le # Ce que le fichier porte au plus. L'accueil n'en affiche que six, mais le fichier
# fichier sert aussi à qui voudrait en faire autre chose. Le second ne plafonne pas. # sert aussi à qui voudrait en faire autre chose.
NB_MAX = 20 NB_MAX = 20
# En deçà, un article est annoncé comme « publié » plutôt que « mis à jour ». # En deçà, un article est annoncé comme « publié » plutôt que « mis à jour ».
@ -42,24 +34,10 @@ EXCLUS = {"index.md"}
_pages: dict[str, dict[str, str]] = {} _pages: dict[str, dict[str, str]] = {}
_dans_la_nav: set[str] = set() _dans_la_nav: set[str] = set()
_rubriques: dict[str, str] = {}
def _rubrique_par_page(items, rubrique=""):
"""Associe chaque page de la navigation au titre de la section qui la porte.
On garde la section de premier niveau — « Guides », « Contribuer » — plutôt que la
plus proche : c'est l'échelle à laquelle un lecteur range les pages.
"""
for item in items:
if item.is_section:
yield from _rubrique_par_page(item.children, rubrique or item.title)
elif item.is_page:
yield item.file.src_uri, rubrique
def on_nav(nav, config, files): def on_nav(nav, config, files):
"""Retient les pages de la navigation, et sous quelle rubrique elles vivent. """Retient les pages de la navigation.
Ce qui en est absent — les redirections laissées derrière un article déplacé, par Ce qui en est absent — les redirections laissées derrière un article déplacé, par
exemple — n'a pas à être annoncé comme une nouveauté. exemple — n'a pas à être annoncé comme une nouveauté.
@ -67,9 +45,6 @@ def on_nav(nav, config, files):
_dans_la_nav.clear() _dans_la_nav.clear()
_dans_la_nav.update(page.file.src_uri for page in nav.pages) _dans_la_nav.update(page.file.src_uri for page in nav.pages)
_rubriques.clear()
_rubriques.update(_rubrique_par_page(nav.items))
def on_page_context(context, page, config, nav): def on_page_context(context, page, config, nav):
"""Retient le titre et l'URL de chaque page rendue.""" """Retient le titre et l'URL de chaque page rendue."""
@ -131,30 +106,24 @@ def _historique(depot: Path, dossier: str) -> tuple[dict, dict]:
return modifiees, creees return modifiees, creees
def _autoriser_la_lecture_croisee(site_dir: Path, fichiers: list[str]) -> None: def _autoriser_la_lecture_croisee(site_dir: Path) -> None:
"""Pose l'en-tête CORS sur les seuls fichiers qui en ont besoin. """Pose l'en-tête CORS sur le seul fichier qui en a besoin.
Ils sont lus depuis alpinux.org : sans cet en-tête, le navigateur refuse la Le fichier est lu depuis alpinux.org : sans cet en-tête, le navigateur refuse la
réponse et la section reste masquée. Le bloc s'ajoute à un `.htaccess` déjà là réponse et la section reste masquée. Le bloc s'ajoute à un `.htaccess` déjà là
plutôt que de le remplacer — on ne sait pas ce qu'il porte d'autre — et n'est plutôt que de le remplacer — on ne sait pas ce qu'il porte d'autre.
écrit que pour les fichiers qui n'y figurent pas encore.
""" """
fichier = site_dir / ".htaccess" bloc = (
existant = fichier.read_text(encoding="utf-8") if fichier.exists() else "" "<IfModule mod_headers.c>\n"
f' <Files "{FICHIER}">\n'
manquants = [nom for nom in fichiers if nom not in existant]
if not manquants:
return
bloc = "<IfModule mod_headers.c>\n"
for nom in manquants:
bloc += (
f' <Files "{nom}">\n'
' Header set Access-Control-Allow-Origin "*"\n' ' Header set Access-Control-Allow-Origin "*"\n'
" </Files>\n" " </Files>\n"
"</IfModule>\n"
) )
bloc += "</IfModule>\n" fichier = site_dir / ".htaccess"
existant = fichier.read_text(encoding="utf-8") if fichier.exists() else ""
if FICHIER in existant:
return
separateur = "\n" if existant and not existant.endswith("\n") else "" separateur = "\n" if existant and not existant.endswith("\n") else ""
fichier.write_text(existant + separateur + bloc, encoding="utf-8") fichier.write_text(existant + separateur + bloc, encoding="utf-8")
@ -175,7 +144,7 @@ def on_post_build(config):
log.info("derniers-articles : clone superficiel, les dates ne sont pas fiables") log.info("derniers-articles : clone superficiel, les dates ne sont pas fiables")
seuil = datetime.now(timezone.utc) - timedelta(days=JOURS_NOUVEAU) seuil = datetime.now(timezone.utc) - timedelta(days=JOURS_NOUVEAU)
pages = [] articles = []
for chemin, modifiee in modifiees.items(): for chemin, modifiee in modifiees.items():
prefixe = docs_dir.name + "/" prefixe = docs_dir.name + "/"
@ -193,43 +162,30 @@ def on_post_build(config):
except ValueError: except ValueError:
nouveau = False nouveau = False
pages.append({ articles.append({
"titre": page["titre"], "titre": page["titre"],
"url": page["url"], "url": page["url"],
"date": creee if nouveau else modifiee, "date": creee if nouveau else modifiee,
"nouveau": nouveau, "nouveau": nouveau,
"rubrique": _rubriques.get(src_uri, ""),
}) })
if not pages: if not articles:
log.info("derniers-articles : aucune page datée, fichier non écrit") log.info("derniers-articles : aucune page datée, fichier non écrit")
return return
pages.sort(key=lambda page: page["date"], reverse=True) articles.sort(key=lambda article: article["date"], reverse=True)
articles = articles[:NB_MAX]
genere = datetime.now(timezone.utc).replace(microsecond=0).isoformat() genere = datetime.now(timezone.utc).replace(microsecond=0).isoformat()
contenu = json.dumps({"genere": genere, "articles": articles},
# L'accueil lit ce fichier depuis longtemps : il garde ses quatre clés, sans la ensure_ascii=False, indent=2) + "\n"
# rubrique dont il n'a que faire.
recents = [
{clef: page[clef] for clef in ("titre", "url", "date", "nouveau")}
for page in pages[:NB_MAX]
]
ecrits = {
FICHIER_RECENTS: {"genere": genere, "articles": recents},
FICHIER_TOUTES: {"genere": genere, "pages": pages},
}
try: try:
for nom, contenu in ecrits.items(): (site_dir / FICHIER).write_text(contenu, encoding="utf-8")
texte = json.dumps(contenu, ensure_ascii=False, indent=2) + "\n" _autoriser_la_lecture_croisee(site_dir)
(site_dir / nom).write_text(texte, encoding="utf-8")
_autoriser_la_lecture_croisee(site_dir, list(ecrits))
except OSError as erreur: except OSError as erreur:
log.info("derniers-articles : écriture impossible (%s), fichier non écrit", log.info("derniers-articles : écriture impossible (%s), fichier non écrit",
erreur) erreur)
return return
log.info("derniers-articles : %d page(s) publiée(s) dans %s, %d dans %s", log.info("derniers-articles : %d article(s) publié(s) dans %s", len(articles), FICHIER)
len(pages), FICHIER_TOUTES, len(recents), FICHIER_RECENTS)

View file

@ -119,7 +119,6 @@ nav:
- technique/matrix.md - technique/matrix.md
- technique/arduino-ide.md - technique/arduino-ide.md
- technique/serveur-debian-13.md - technique/serveur-debian-13.md
- technique/serveur-install-party.md
- technique/bigbluebutton.md - technique/bigbluebutton.md
- technique/ateliers.md - technique/ateliers.md
- Communication: - Communication: