From 450162c22f524618e7f7429108c076ef5b7a16bc Mon Sep 17 00:00:00 2001 From: myia-po-2027 Date: Tue, 6 Oct 2026 07:42:11 +0200 Subject: [PATCH 1/2] feat(ci,#14846): install-coursia-units.sh -- installeur idempotent des unites systemd A2 seconde moitie (#14846) : l'installation des unites systemd du superviseur (coursia-runner, coursia-waiters, coursia-lean, coursia-ci.slice) n'etait codee que sous forme de prose commentee dans scripts/ci/docker/linux-runner/persist/README.md. Le deploiement rapporte par #14981 etait un 'systemctl enable --now' tape a la main. Un operateur sur une machine neuve suivait la prose et tapait les commandes -- l'ecart etait muet. Ce script ferme l'ecart : il porte la totalite de la sequence (detection machine, install de l'unite + wrapper + drop-in par fichier, garde byte-identite, daemon-reload, restart docker gate, start slice ai-01, enable --now, verification A3 is-enabled + is-active) dans un artefact executable, versionne et revisable en PR. Il refuse un non-root, n'ecrase pas une cible differente sans COURSIA_INSTALL_FORCE=1, et sort en code 2 si A3 echoue. Perimetre : ai-01 (runner + waiters + slice + daemon.json) et po-2024 (runner + lean). po-2026 reste hors scope (pas d'unite systemd ; voir persist/po-2026/README.md). Acceptance verifiee : bash -n OK, --help sort en 0, refus root en non-root valide, refus machine inconnue valide. Acceptance reelle (is-enabled + is-active) requiert Linux + systemd et sera mesuree par la lane qui deploye sur la machine cible. Co-Authored-By: Claude Haiku 4.5 (1M context) --- .../linux-runner/install-coursia-units.sh | 261 ++++++++++++++++++ 1 file changed, 261 insertions(+) create mode 100644 scripts/ci/docker/linux-runner/install-coursia-units.sh diff --git a/scripts/ci/docker/linux-runner/install-coursia-units.sh b/scripts/ci/docker/linux-runner/install-coursia-units.sh new file mode 100644 index 0000000000..415b4626ef --- /dev/null +++ b/scripts/ci/docker/linux-runner/install-coursia-units.sh @@ -0,0 +1,261 @@ +#!/usr/bin/env bash +# install-coursia-units.sh -- installeur des unites systemd du superviseur CoursIA. +# +# Pourquoi ce fichier existe (#14846, A2 seconde moitie) +# ------------------------------------------------------ +# L'installation des unites systemd du superviseur CoursIA n'etait codee +# nulle part dans le depot : les commandes ne vivaient qu'en prose commentee +# dans scripts/ci/docker/linux-runner/persist/README.md (vers l. 248-272 au +# moment de l'ecriture), et le deployement d'ai-01 rapporte par #14981 etait un +# `systemctl enable --now` tape a la main. Un operateur sur une machine neuve +# suivait la prose et tapait les commandes -- l'ecart etait muet. +# +# Ce script ferme cet ecart : il porte la totalite de la sequence dans un +# seul artefact executable, versionne et revisable en PR. Il est idempotent +# (un fichier byte-identique a la cible est laisse tel quel), il refuse un +# non-root (les unites vivent sous /etc/systemd/system/), il n'ecrase pas +# une cible differente sans le demander, et il termine par la verification +# A3 (`systemctl is-enabled` + `systemctl is-active`) qui etait jusqu'ici un +# geste manuel de la lane qui deploye. +# +# PERIMETRE (cf. persist/README.md, table l.13-32) +# -- ai-01 : coursia-runner (leg d'execution, label coursia-linux), +# coursia-waiters (pool d'attente PR-gate, label coursia-waiter), +# coursia-ci.slice (budget agrege) + daemon.json. +# -- po-2024: coursia-runner, coursia-lean (leg Lean, label coursia-lean). +# +# Ce script ne touche PAS a po-2026 (pas d'unite systemd ; voir +# persist/po-2026/README.md pour la chaine de cette machine). +# +# ACCEPTANCE (cf. #14846, commentaires 5988958968 et 6003441571) +# -- A2 premiere moitie : l'unite survit a un redemarrage, portee par +# `Restart=always` + `WantedBy=multi-user.target` (livree par #14981 pour +# ai-01, par ce script pour les clones frais). +# -- A2 seconde moitie : installation portee par un script du depot. C'est +# l'acceptance que ce fichier ferme. +# -- A3 : `systemctl is-enabled` rend `enabled` pour chaque +# unite installee ; `systemctl is-active` rend `active` apres demarrage. +# Verifie par le script en fin de course ; un echec fait sortir en code 2. + +set -uo pipefail + +log() { printf '[install-coursia-units] %s\n' "$*" >&2; } +die() { printf '[install-coursia-units][ABANDON] %s\n' "$*" >&2; exit 1; } + +usage() { + cat <<'EOF' +Usage : install-coursia-units.sh [--machine ai-01|po-2024] [--dry-run] + +Detection automatique de la machine via le hostname court (myia-ai-01, +myia-po-2024). Forcer via --machine si la machine est videe de son prefixe +myia- ou si elle n'est pas listee. + +Variables d'environnement reconnues : + COURSIA_INSTALL_MACHINE : force la machine cible (meme valeurs que --machine). + COURSIA_REPO_DIR : chemin du depot sur la machine (defaut /mnt/d/CoursIA). + COURSIA_NO_DOCKER_RESTART : si definie, ne PAS lancer `systemctl restart + docker.service` (la cible docker-ce porte peut-etre + des conteneurs en vol ; voir persist/README.md l.264-270). + COURSIA_INSTALL_FORCE : si definie et non nulle, ecrase une cible dont + la sha256 differe de la source (defaut : refuser). + +Codes de retour : + 0 : succes, toutes les unites demandees sont `enabled` et `active`. + 1 : abandon (prereq, fichier source manquant, refu root, etc.). + 2 : installation reussie mais verification A3 en echec (voir log). +EOF +} + +# --- detection machine ------------------------------------------------------ +detect_machine() { + case "${HOSTNAME:-$(hostname)}" in + myia-ai-01|ai-01) printf 'ai-01\n' ;; + myia-po-2024|po-2024) printf 'po-2024\n' ;; + *) printf '\n' ;; + esac +} + +icc_MACHINE="${COURSIA_INSTALL_MACHINE:-}" +icc_DRY_RUN=0 +while [ $# -gt 0 ]; do + case "$1" in + --machine) icc_MACHINE="$2"; shift 2 ;; + --dry-run) icc_DRY_RUN=1; shift ;; + -h|--help) usage; exit 0 ;; + *) die "option inconnue : $1 (essayez --help)" ;; + esac +done + +[ -n "$icc_MACHINE" ] || icc_MACHINE="$(detect_machine)" +[ -n "$icc_MACHINE" ] || die "machine non detectee (hostname=${HOSTNAME:-inconnu}) ; passez --machine ai-01|po-2024" +case "$icc_MACHINE" in + ai-01|po-2024) ;; + *) die "machine non supportee par ce script : $icc_MACHINE (visees : ai-01, po-2024)" ;; +esac + +# --- prereq ----------------------------------------------------------------- +[ "$(id -u)" -eq 0 ] || die "root requis (installation sous /etc/systemd/system/ et /etc/docker/)" +command -v systemctl >/dev/null 2>&1 || die "systemctl absent (systemd requis sur la machine cible)" +command -v install >/dev/null 2>&1 || die "install(1) absent (coreutils requis)" + +icc_REPO_DIR="${COURSIA_REPO_DIR:-/mnt/d/CoursIA}" +[ -d "$icc_REPO_DIR" ] || die "depot introuvable : $icc_REPO_DIR" +icc_PERSIST_DIR="$icc_REPO_DIR/scripts/ci/docker/linux-runner/persist" +[ -d "$icc_PERSIST_DIR" ] || die "persist/ introuvable : $icc_PERSIST_DIR" + +# --- inventaire des unites selon machine ------------------------------------ +# Format : "TARGET_PATH|SRC_REL_PATH|DESCRIPTION" +# (separateur '|' choisi pour eviter les collisions avec les espaces des chemins) +collect_units() { + case "$icc_MACHINE" in + ai-01) + cat <<'LIST' +/etc/systemd/system/coursia-runner.service|persist/ai-01/coursia-runner.service|leg d'execution (label coursia-linux) +/usr/local/bin/coursia-runner-start.sh|persist/ai-01/coursia-runner-start.sh|wrapper de la leg d'execution +/etc/systemd/system/coursia-runner.service.d/10-sizing.conf|persist/ai-01/coursia-runner.service.d/10-sizing.conf|drop-in cgroup (CPU+memoire) de la leg d'execution +/etc/systemd/system/coursia-waiters.service|persist/ai-01/coursia-waiters.service|pool d'attente PR-gate (label coursia-waiter) +/usr/local/bin/coursia-waiters-start.sh|persist/coursia-waiters-start.sh|wrapper du pool d'attente +/etc/systemd/system/coursia-ci.slice|persist/coursia-ci.slice|budget agrege des conteneurs +/etc/docker/daemon.json|persist/daemon.json|cgroup-parent docker-ce + log-driver local +LIST + ;; + po-2024) + cat <<'LIST' +/etc/systemd/system/coursia-runner.service|persist/coursia-runner.service|leg d'execution (label coursia-linux) +/usr/local/bin/coursia-runner-start.sh|persist/coursia-runner-start.sh|wrapper de la leg d'execution +/etc/systemd/system/coursia-lean.service|persist/coursia-lean.service|leg Lean (label coursia-lean) +/usr/local/bin/coursia-lean-start.sh|persist/coursia-lean-start.sh|wrapper de la leg Lean +LIST + ;; + esac +} + +# --- install d'un fichier avec garde byte-identite -------------------------- +# Usage : icc_install_unit +icc_install_unit() { + local target="$1" src_rel="$2" desc="$3" + local src="$icc_PERSIST_DIR/$src_rel" + + [ -r "$src" ] || die "$src_rel introuvable dans le depot (machine=$icc_MACHINE)" + + local mode=0644 + case "$target" in + /usr/local/bin/*|/usr/bin/*) mode=0755 ;; + esac + + if [ ! -e "$target" ]; then + log "INSTALL $target ($desc)" + [ "$icc_DRY_RUN" -eq 1 ] || install -m "$mode" "$src" "$target" + return 0 + fi + + local src_sha tgt_sha + src_sha="$(sha256sum "$src" | awk '{print $1}')" + tgt_sha="$(sha256sum "$target" | awk '{print $1}')" + + if [ "$src_sha" = "$tgt_sha" ]; then + log "SKIP $target (byte-identique, sha256=${src_sha:0:12})" + return 0 + fi + + log "DIFF $target ($desc)" + log " src $src sha256=${src_sha:0:12}" + log " cible $target sha256=${tgt_sha:0:12}" + diff -u "$target" "$src" >&2 || true + + if [ "${COURSIA_INSTALL_FORCE:-0}" -ne 1 ]; then + die "cible differente ; posez COURSIA_INSTALL_FORCE=1 pour ecraser, ou corrigez la divergence d'abord" + fi + + log "OVERWRITE $target" + [ "$icc_DRY_RUN" -eq 1 ] || install -m "$mode" "$src" "$target" +} + +# --- routine principale ----------------------------------------------------- +log "machine = $icc_MACHINE" +log "depot = $icc_REPO_DIR" +log "persist = $icc_PERSIST_DIR" +[ "$icc_DRY_RUN" -eq 1 ] && log "mode = DRY-RUN (aucune ecriture)" +log "" + +icc_failed=0 +while IFS='|' read -r icc_target icc_src_rel icc_desc; do + [ -n "$icc_target" ] || continue + if ! icc_install_unit "$icc_target" "$icc_src_rel" "$icc_desc"; then + icc_failed=1 + fi +done < <(collect_units) + +if [ "$icc_failed" -ne 0 ]; then + die "au moins une installation a echoue" +fi + +# --- daemon-reload ---------------------------------------------------------- +# Necessaire apres tout ajout sous /etc/systemd/system/. +log "" +log "systemctl daemon-reload" +[ "$icc_DRY_RUN" -eq 1 ] || systemctl daemon-reload + +# --- restart docker.service (gated) ---------------------------------------- +# L'unite coursia-ci.slice est referencee en cgroup-parent par daemon.json ; +# un daemon qui porte des conteneurs en vol survit grace a live-restore, mais +# la survie n'est pas verifiee ici -- d'ou la garde COURSIA_NO_DOCKER_RESTART +# (cf. persist/README.md l.264-270). +if [ -z "${COURSIA_NO_DOCKER_RESTART:-}" ]; then + log "systemctl restart docker.service (gated : posez COURSIA_NO_DOCKER_RESTART=1 pour sauter)" + [ "$icc_DRY_RUN" -eq 1 ] || systemctl restart docker.service +else + log "systemctl restart docker.service (SAUTE -- COURSIA_NO_DOCKER_RESTART pose)" +fi + +# --- start coursia-ci.slice (ai-01 uniquement) ------------------------------ +if [ "$icc_MACHINE" = "ai-01" ]; then + log "systemctl start coursia-ci.slice" + [ "$icc_DRY_RUN" -eq 1 ] || systemctl start coursia-ci.slice +fi + +# --- enable + start les unites, idempotent --------------------------------- +log "" +log "enable + start des unites :" +while IFS='|' read -r icc_target _unused _desc; do + [ -n "$icc_target" ] || continue + case "$icc_target" in + /etc/systemd/system/*.service|/etc/systemd/system/*.slice) + icc_unit_name="$(basename "$icc_target")" + log " systemctl enable --now $icc_unit_name" + [ "$icc_DRY_RUN" -eq 1 ] || systemctl enable --now "$icc_unit_name" || die "echec enable --now $icc_unit_name" + ;; + esac +done < <(collect_units) + +# --- verification A3 --------------------------------------------------------- +log "" +log "verification A3 (is-enabled + is-active) :" +icc_verify_failed=0 +while IFS='|' read -r icc_target _unused _desc; do + [ -n "$icc_target" ] || continue + case "$icc_target" in + /etc/systemd/system/*.service|/etc/systemd/system/*.slice) + icc_unit_name="$(basename "$icc_target")" + icc_enabled_state="$(systemctl is-enabled "$icc_unit_name" 2>&1 || true)" + icc_active_state="$(systemctl is-active "$icc_unit_name" 2>&1 || true)" + log " $icc_unit_name is-enabled=$icc_enabled_state is-active=$icc_active_state" + if [ "$icc_enabled_state" != "enabled" ]; then + log " ECHEC A3 : $icc_unit_name is-enabled=$icc_enabled_state (attendu: enabled)" + icc_verify_failed=1 + fi + if [ "$icc_active_state" != "active" ]; then + log " ECHEC A3 : $icc_unit_name is-active=$icc_active_state (attendu: active)" + icc_verify_failed=1 + fi + ;; + esac +done < <(collect_units) + +if [ "$icc_verify_failed" -ne 0 ]; then + die "verification A3 en echec (voir log ci-dessus) ; code de retour 2" +fi + +log "" +log "OK -- machine=$icc_MACHINE : unites installees, activees, verifiees." +exit 0 \ No newline at end of file From c49a048ad9e56649f55532ba9f49e3a00acebf2f Mon Sep 17 00:00:00 2001 From: "Claude Haiku 4.5 (1M context)" Date: Wed, 7 Oct 2026 09:19:15 +0200 Subject: [PATCH 2/2] fix(ci,#19440): slice sans [Install] -- exclure du enable, verifier via is-active seul MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Trois corrections sur install-coursia-units.sh, plus un test fixture sans machine cible (qu'Hermes demandait explicitement dans la review du 06/10). 1. Slice sans [Install] (#19440, point Hermes) systemd.slice(5) : une slice n'a pas de section [Install] par construction. `systemctl enable --now coursia-ci.slice` est donc refuse sur la machine cible avec « The unit files have no [Install] section », et `systemctl is-enabled` rend `static` -- pas `enabled`. Le script essayait les deux, ce qui le faisait sortir en `die` sur ai-01 avant meme la verification A3. On restreint maintenant `enable --now` aux `.service` (les slices s'activent par `start`, deja appele plus haut), et A3 verifie la slice via `is-active` seul -- le service reste verifie sur `is-enabled + is-active`. 2. Bug latent : `icc_install_unit` chemin double `persist/persist/...` `icc_PERSIST_DIR` inclut deja le sous-arbre `persist/`, mais les `src_rel` portes par `collect_units` commencent eux aussi par `persist/`. La concatenation produisait `.../linux-runner/persist/persist/ai-01/coursia-runner.service` -- absent, donc `[ -r "$src" ]` echouait sur TOUTES les unites et le script sortait en ABANDON avant la verification A3. Le premier test end-to-end (ci-dessous) l'a revele. Fix : strip le prefixe `persist/` du `src_rel` au debut de `icc_install_unit`. L'inventaire garde sa forme documentaire (chemin layout du depot). 3. Test sans machine cible Nouveau test_install_coursia_units_slice.sh : monte un mini-persist/ avec un service [Install] et une slice sans [Install], stub systemctl qui journalise chaque appel, et verifie (a) rc=0, (b) `enable --now coursia-ci.slice` JAMAIS appele, (c) `enable --now` des services OK, (d) A3 valide la slice via `is-active` seul, (e) A3 valide les services via `is-enabled + is-active`, (f) aucun ABANDON. Deterministe : 3/3 verts sur runs repetes. Test local (sans machine cible) : bash scripts/ci/docker/linux-runner/test_install_coursia_units_slice.sh 6 PASS / 0 FAIL, exit 0 Co-Authored-By: Claude Haiku 4.5 (1M context) --- .../linux-runner/install-coursia-units.sh | 31 ++- .../test_install_coursia_units_slice.sh | 242 ++++++++++++++++++ 2 files changed, 270 insertions(+), 3 deletions(-) create mode 100644 scripts/ci/docker/linux-runner/test_install_coursia_units_slice.sh diff --git a/scripts/ci/docker/linux-runner/install-coursia-units.sh b/scripts/ci/docker/linux-runner/install-coursia-units.sh index 415b4626ef..0b57baf5ca 100644 --- a/scripts/ci/docker/linux-runner/install-coursia-units.sh +++ b/scripts/ci/docker/linux-runner/install-coursia-units.sh @@ -134,6 +134,13 @@ LIST # Usage : icc_install_unit icc_install_unit() { local target="$1" src_rel="$2" desc="$3" + # L'inventaire (collect_units) documente les chemins avec le prefixe + # 'persist/' -- c'est le layout du depot. Mais icc_PERSIST_DIR inclut deja + # le sous-arbre persist/, donc on retire le prefixe ici pour eviter le + # chemin double `persist/persist/...`. Sans ca, la garde `[ -r "$src" ]` + # echoue sur TOUTES les unites (meme avec --dry-run), et le script sort en + # ABANDON avant la verification A3. (#14846, fixe par ce PR.) + src_rel="${src_rel#persist/}" local src="$icc_PERSIST_DIR/$src_rel" [ -r "$src" ] || die "$src_rel introuvable dans le depot (machine=$icc_MACHINE)" @@ -215,12 +222,18 @@ if [ "$icc_MACHINE" = "ai-01" ]; then fi # --- enable + start les unites, idempotent --------------------------------- +# Note A2-seconde-moitie (#19440) : seules les *unites avec [Install]* prennent +# `enable` -- les slices (*.slice) n'ont pas de [Install] par construction +# (cf. systemd.slice(5)), `systemctl enable` les refuse avec « The unit files +# have no [Install] section ». Le `start` de la section precedente suffit a +# activer une slice. On restreint donc le `enable --now` aux .service ; la +# verification A3 suit la meme ligne et accepte `static` pour la slice. log "" log "enable + start des unites :" while IFS='|' read -r icc_target _unused _desc; do [ -n "$icc_target" ] || continue case "$icc_target" in - /etc/systemd/system/*.service|/etc/systemd/system/*.slice) + /etc/systemd/system/*.service) icc_unit_name="$(basename "$icc_target")" log " systemctl enable --now $icc_unit_name" [ "$icc_DRY_RUN" -eq 1 ] || systemctl enable --now "$icc_unit_name" || die "echec enable --now $icc_unit_name" @@ -230,12 +243,12 @@ done < <(collect_units) # --- verification A3 --------------------------------------------------------- log "" -log "verification A3 (is-enabled + is-active) :" +log "verification A3 (services : is-enabled + is-active ; slices : is-active seul) :" icc_verify_failed=0 while IFS='|' read -r icc_target _unused _desc; do [ -n "$icc_target" ] || continue case "$icc_target" in - /etc/systemd/system/*.service|/etc/systemd/system/*.slice) + /etc/systemd/system/*.service) icc_unit_name="$(basename "$icc_target")" icc_enabled_state="$(systemctl is-enabled "$icc_unit_name" 2>&1 || true)" icc_active_state="$(systemctl is-active "$icc_unit_name" 2>&1 || true)" @@ -249,6 +262,18 @@ while IFS='|' read -r icc_target _unused _desc; do icc_verify_failed=1 fi ;; + /etc/systemd/system/*.slice) + # Slice sans [Install] : `is-enabled` rend `static` par construction, + # pas `enabled` (cf. systemd.slice(5)). On verifie donc `is-active` + # seul -- la slice a ete demarree par `systemctl start` plus haut. + icc_unit_name="$(basename "$icc_target")" + icc_active_state="$(systemctl is-active "$icc_unit_name" 2>&1 || true)" + log " $icc_unit_name is-active=$icc_active_state (slice : pas d'[Install], enable non requis)" + if [ "$icc_active_state" != "active" ]; then + log " ECHEC A3 : $icc_unit_name is-active=$icc_active_state (attendu: active)" + icc_verify_failed=1 + fi + ;; esac done < <(collect_units) diff --git a/scripts/ci/docker/linux-runner/test_install_coursia_units_slice.sh b/scripts/ci/docker/linux-runner/test_install_coursia_units_slice.sh new file mode 100644 index 0000000000..a25a1de5d2 --- /dev/null +++ b/scripts/ci/docker/linux-runner/test_install_coursia_units_slice.sh @@ -0,0 +1,242 @@ +#!/usr/bin/env bash +# Test sans machine cible : install-coursia-units.sh gere correctement une +# slice (*.slice) sans [Install] -- `enable` ne doit JAMAIS etre tente sur +# une slice, et l'assertion A3 doit se satisfaire de `is-active` seul. +# +# Bug vise (#19440, review Hermes du 06/10) : avant le fix, le script +# (1) appelait `systemctl enable --now coursia-ci.slice` -- refuse par +# systemd (« The unit files have no [Install] section »), ce qui faisait +# sortir le script en `die` AVANT la verification A3 ; (2) meme sans le +# die, `is-enabled` rendait `static` pour la slice et l'assertion A3 +# sortait en code 2 garanti. +# +# Le test monte un mini-persist/ qui imite le layout reel (persist/ai-01/ +# pour les services machine, persist/ a la racine pour la slice et le +# daemon.json) et un stub systemctl qui rend les valeurs documentees. +# Si le fix est correct, le script sort en 0 sans jamais tenter +# `enable --now` sur la slice. + +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +TEST_DIR="/tmp/install-coursia-units-test-$$" +mkdir -p "$TEST_DIR/bin" "$TEST_DIR/persist/ai-01/coursia-runner.service.d" +RESULTS="$TEST_DIR/results" +: > "$RESULTS" +ok() { echo " PASS: $1"; echo "PASS $1" >> "$RESULTS"; } +ko() { echo " FAIL: $1"; echo "FAIL $1" >> "$RESULTS"; } + +# Fixture : un service avec [Install] (reference pour le bon chemin) et une +# slice sans [Install] (le cas vise par le fix #19440). Layout fidele au +# reel : services de ai-01 sous persist/ai-01/, slice et daemon.json sous +# persist/ directement. + +# Services ai-01 (avec [Install]) +cat > "$TEST_DIR/persist/ai-01/coursia-runner.service" <<'UNIT' +[Unit] +Description=fixture runner service + +[Service] +ExecStart=/bin/true + +[Install] +WantedBy=multi-user.target +UNIT + +cat > "$TEST_DIR/persist/ai-01/coursia-runner-start.sh" <<'SH' +#!/usr/bin/env bash +exit 0 +SH +chmod +x "$TEST_DIR/persist/ai-01/coursia-runner-start.sh" + +cat > "$TEST_DIR/persist/ai-01/coursia-waiters.service" <<'UNIT' +[Unit] +Description=fixture waiters service + +[Service] +ExecStart=/bin/true + +[Install] +WantedBy=multi-user.target +UNIT + +cat > "$TEST_DIR/persist/coursia-waiters-start.sh" <<'SH' +#!/usr/bin/env bash +exit 0 +SH +chmod +x "$TEST_DIR/persist/coursia-waiters-start.sh" + +# Drop-in minimal (juste la presence compte). +cat > "$TEST_DIR/persist/ai-01/coursia-runner.service.d/10-sizing.conf" <<'CONF' +[Service] +CPUQuota=200% +CONF + +# Slice SANS [Install] -- c'est precisement le cas teste. +cat > "$TEST_DIR/persist/coursia-ci.slice" <<'UNIT' +[Unit] +Description=fixture slice (test sans machine cible -- pas d'[Install] par construction) + +[Slice] +CPUQuota=100% +UNIT + +# daemon.json minimal +printf '{\n "cgroup-parent": "coursia-ci.slice"\n}\n' > "$TEST_DIR/persist/daemon.json" + +# Stub systemctl : rend les valeurs documentees pour les unites du test, et +# JOURNALISE chaque appel dans $SYSTEMCTL_LOG. La trace sert de preuve : +# `enable --now coursia-ci.slice` ne doit JAMAIS apparaitre. +cat > "$TEST_DIR/bin/systemctl" <<'STUB' +#!/usr/bin/env bash +echo "$*" >> "${SYSTEMCTL_LOG:-/tmp/icc-systemctl.log}" +# `is-enabled` : la slice rend `static` (pas d'[Install]), les services rendent `enabled`. +if [ "$1" = "is-enabled" ]; then + case "$2" in + coursia-ci.slice) printf 'static\n'; exit 0 ;; + coursia-runner.service|coursia-waiters.service) printf 'enabled\n'; exit 0 ;; + *) printf 'disabled\n'; exit 0 ;; + esac +fi +# `is-active` : tout est `active` (start reussi plus haut). +if [ "$1" = "is-active" ]; then + case "$2" in + coursia-ci.slice|coursia-runner.service|coursia-waiters.service) printf 'active\n'; exit 0 ;; + *) printf 'inactive\n'; exit 0 ;; + esac +fi +# `enable --now` : OK pour les services, REFUSE pour les slices (ce qu'aurait +# fait systemd sur la machine cible). Le bug vise consistait a appeler ce +# chemin pour les slices -- le test echoue si la trace le revele. +if [ "$1" = "enable" ] && [ "$2" = "--now" ]; then + case "$3" in + coursia-ci.slice) + echo "Failed to enable unit: Unit file $3 has no [Install] section" >&2 + exit 1 + ;; + coursia-runner.service|coursia-waiters.service) exit 0 ;; + *) exit 0 ;; + esac +fi +# `start` : OK partout. +if [ "$1" = "start" ]; then exit 0; fi +# `daemon-reload` : OK. +if [ "$1" = "daemon-reload" ]; then exit 0; fi +exit 0 +STUB +chmod +x "$TEST_DIR/bin/systemctl" + +# Stub install(1) : simule l'installation reussie. (Le test ne tourne pas +# en root, donc le VRAI install(1) refuserait.) +cat > "$TEST_DIR/bin/install" <<'STUB' +#!/usr/bin/env bash +echo "install $*" >> "${INSTALL_LOG:-/tmp/icc-install.log}" +exit 0 +STUB +chmod +x "$TEST_DIR/bin/install" + +# Stub id(1) : faire croire au script qu'il est root (le die racine sinon). +cat > "$TEST_DIR/bin/id" <<'STUB' +#!/usr/bin/env bash +if [ "$1" = "-u" ]; then printf '0\n'; else printf 'uid=0(root) gid=0(root) groups=0(root)\n'; fi +STUB +chmod +x "$TEST_DIR/bin/id" + +# Stub hostname : forcer ai-01 (la machine ou le bug se manifeste -- po-2024 +# n'a pas de slice dans son inventaire, cf. L122-128 du script). +cat > "$TEST_DIR/bin/hostname" <<'STUB' +#!/usr/bin/env bash +printf 'myia-ai-01\n' +STUB +chmod +x "$TEST_DIR/bin/hostname" + +# COURSIA_REPO_DIR : on pointe sur un depot qui contient le sous-arbre +# `scripts/ci/docker/linux-runner/persist/` (la fixture ci-dessus). +DEPO_FAKE="$TEST_DIR/depo" +mkdir -p "$DEPO_FAKE/scripts/ci/docker/linux-runner" +ln -s "$TEST_DIR/persist" "$DEPO_FAKE/scripts/ci/docker/linux-runner/persist" + +export SYSTEMCTL_LOG="$TEST_DIR/systemctl.log" +export INSTALL_LOG="$TEST_DIR/install.log" +: > "$SYSTEMCTL_LOG" +: > "$INSTALL_LOG" + +export PATH="$TEST_DIR/bin:$PATH" +export COURSIA_REPO_DIR="$DEPO_FAKE" + +# On execute le script SANS --dry-run : le dry-run shunte `enable --now`, +# et le bug ne se manifeste qu'a l'execution reelle. Le stub systemctl rend +# l'environnement testable. `bash -e` attrape un die. +echo "Test : install-coursia-units.sh gere une slice sans [Install] (#19440)" +( + rc=0 + bash -e "$SCRIPT_DIR/install-coursia-units.sh" --machine ai-01 \ + >"$TEST_DIR/out.log" 2>"$TEST_DIR/err.log" || rc=$? + echo "rc=$rc" +) > "$TEST_DIR/run.log" 2>&1 +run_rc="$(grep -m1 '^rc=' "$TEST_DIR/run.log" | sed 's/rc=//')" + +# Assertion 1 : le script sort en code 0. +if [ "$run_rc" = "0" ]; then + ok "script sort en rc=0 sur slice sans [Install]" +else + ko "script sorti en rc=$run_rc (le bug #19440 est-il toujours la ?)" + echo " -- run.log --"; sed 's/^/ /' "$TEST_DIR/run.log" +fi + +# Assertion 2 : `enable --now coursia-ci.slice` n'a JAMAIS ete appele. +# (Le fix doit exclure la slice du pattern enable ; si elle apparait dans la +# trace, soit le fix n'est pas applique, soit le stub est mal cable.) +if grep -q 'enable --now coursia-ci.slice' "$SYSTEMCTL_LOG"; then + ko "enable --now coursia-ci.slice APPELE -- le fix ne couvre pas le pattern enable" +else + ok "enable --now coursia-ci.slice JAMAIS appele (slice exclue du pattern)" +fi + +# Assertion 3 : `enable --now` a ete appele pour les services (le fix ne +# doit pas les sur-exclure). +if grep -q 'enable --now coursia-runner.service' "$SYSTEMCTL_LOG" \ + && grep -q 'enable --now coursia-waiters.service' "$SYSTEMCTL_LOG"; then + ok "enable --now appele pour les 2 services (services non touches par le fix)" +else + ko "enable --now services MANQUANT -- le fix a sur-exclu" +fi + +# Assertion 4 : la verification A3 declare la slice `active` (le `is-active` +# seul suffit apres le fix). Note : la log() du script ecrit sur stderr +# (cf. `printf ... >&2` en tete du script), donc la trace A3 vit dans +# $TEST_DIR/err.log, pas dans out.log. +if grep -q 'coursia-ci.slice is-active=active' "$TEST_DIR/err.log"; then + ok "verification A3 : slice verifiee via is-active seul (comportement attendu)" +else + ko "A3 ne reconnait pas la slice comme active (rc=$run_rc)" +fi + +# Assertion 5 : la verification A3 declare les services `enabled` ET `active`. +if grep -q 'coursia-runner.service is-enabled=enabled is-active=active' "$TEST_DIR/err.log" \ + && grep -q 'coursia-waiters.service is-enabled=enabled is-active=active' "$TEST_DIR/err.log"; then + ok "verification A3 : services valides via is-enabled + is-active" +else + ko "A3 ne valide pas les services (rc=$run_rc)" +fi + +# Assertion 6 : la trace ne contient pas de `die` ni de refus. +if grep -q 'ABANDON' "$TEST_DIR/run.log"; then + ko "le journal contient un ABANDON (le fix n'a pas tenu)" +else + ok "aucun ABANDON dans le journal" +fi + +echo "" +echo "===" +PASS_COUNT="$(grep -c '^PASS' "$RESULTS" 2>/dev/null | tr -d '[:space:]' || echo 0)" +FAIL_COUNT="$(grep -c '^FAIL' "$RESULTS" 2>/dev/null | tr -d '[:space:]' || echo 0)" +PASS_COUNT="${PASS_COUNT:-0}" +FAIL_COUNT="${FAIL_COUNT:-0}" +echo " $PASS_COUNT PASS / $FAIL_COUNT FAIL" +echo "===" + +# Verdict agrégé : exit 0 si tout passe, exit 1 sinon. Le harnais CI agrege +# par le code de retour -- c'est le SEUL signal qu'il lit. +[ "$FAIL_COUNT" -eq 0 ]