--- # LE CACHE NE DOIT PAS DÉPENDRE DE LUI-MÊME POUR SE RÉPARER. # # La première version faisait `update_cache: true` à chaque passage. Sur l'hôte qui PORTE # le cache, cet `apt update` passe par le cache lui-même — et en mode hors ligne, il est # refusé. Le rôle qui devait remettre le service en ligne ne pouvait donc plus s'exécuter : # # « Failed to update apt cache after 5 retries » (mesuré le 2026-08-23, après # l'épreuve hors ligne : il a fallu réparer la machine à la main) # # Un service dont la remise en route exige qu'il fonctionne déjà est un piège. On ne # rafraîchit donc l'index qu'à la PREMIÈRE installation, quand aucun cache n'existe encore # et qu'`apt` va forcément à l'amont. - name: Le cache est-il déjà installé ? ansible.builtin.stat: path: /usr/sbin/apt-cacher-ng register: serveur_artefacts_present - name: Installer le cache d'artefacts ansible.builtin.apt: name: "{{ serveur_artefacts_paquets }}" state: present update_cache: "{{ not serveur_artefacts_present.stat.exists }}" when: not ansible_check_mode - name: Assurer le répertoire de cache ansible.builtin.file: path: "{{ serveur_artefacts_cache }}" state: directory owner: apt-cacher-ng group: apt-cacher-ng mode: "0755" when: not ansible_check_mode # apt-cacher-ng lit TOUS les `*.conf` de son répertoire, par ordre alphabétique, et le # dernier lu gagne. Le préfixe `zzz-` place donc notre configuration après celle du # paquet Debian et celle de debconf, sans avoir à les éditer — on ajoute, on ne remplace # pas. Une valeur posée ici l'emporte, et `apt purge` rend la machine à son état d'origine. # --- LE FILET : NE PAS POSER UN AMONT INJOIGNABLE ---------------------------- # # CE CACHE EST UN MAILLON DONT DEPEND SA PROPRE RECONSTRUCTION (mesure du 2026-08-31). # # Toute la flotte du tenant prend ses paquets ici. Si ce cache pointe vers un amont mort, # `apt` echoue sur les QUINZE machines — donc le socle echoue, donc le deploiement # n'atteint jamais la couche qui poserait le bon amont. Le correctif se retrouve dans la # couche que la panne empeche d'atteindre, et il faut une main pour en sortir. # # C'est exactement ce qui est arrive : l'amont pointait sur le cache de patient 0, eteint # la veille pour liberer de la RAM. `apt` rendait `503 Connection timeout` EN CITANT # L'ADRESSE DU CACHE LOCAL, jamais celle de l'amont manquant — la panne accusait le # maillon visible. # # On mesure donc AVANT d'ecrire. Un amont injoignable est refuse : mieux vaut un cache # qui garde sa configuration precedente et un message clair, qu'un cache qu'on vient de # rendre inutilisable pour toute la flotte. - name: L'amont declare repond-il ? ansible.builtin.wait_for: host: "{{ serveur_artefacts_amont | urlsplit('hostname') }}" port: "{{ serveur_artefacts_amont | urlsplit('port') | default(3142, true) | int }}" timeout: 10 register: serveur_artefacts_amont_sonde changed_when: false # `ignore_errors` ET NON `failed_when: false` — LA DIFFERENCE EST TOUTE LA GARDE. # # `failed_when: false` REECRIT le verdict : la tache n'est plus jamais `failed`, donc # `is not failed` est toujours vrai, donc l'assertion qui suit ne peut PAS tirer. Ecrite # ainsi, elle a declare « joignable » un amont mesure MUET la seconde d'avant # (2026-08-31). Une garde qui ne peut pas echouer ne garde rien. # # `ignore_errors` laisse le verdict intact et se contente de ne pas arreter le play : # c'est ce qu'il faut pour SONDER puis DECIDER. ignore_errors: true when: - serveur_artefacts_amont | length > 0 - not ansible_check_mode - name: Refuser un amont injoignable plutot que de rendre ce cache inutilisable ansible.builtin.assert: that: - serveur_artefacts_amont_sonde is not failed fail_msg: >- L'amont declare ({{ serveur_artefacts_amont }}) ne repond pas. Le poser rendrait ce cache — et donc `apt` sur TOUTE la flotte de cet ecosysteme — inutilisable, et le deploiement ne pourrait plus atteindre la couche qui le corrigerait. Verifier que la machine d'amont tourne, ou vider `serveur_artefacts_amont` pour aller directement chez Debian (c'est une emancipation : voir docs/filiation-emancipation.md). success_msg: "Amont {{ serveur_artefacts_amont }} joignable." when: - serveur_artefacts_amont | length > 0 - not ansible_check_mode # LE REPERTOIRE AVANT LA DIRECTIVE QUI LE PUBLIE. `LocalDirs` pointant sur un chemin # absent, apt-cacher-ng ne refuse pas de demarrer : il rend 503 sur ce prefixe, et le # message parle de « File or directory not found » — ce qui accuse le fichier demande, # pas la racine manquante. # LE PARENT AUSSI, ET C'EST LUI QUI A MORDU (mesure du 2026-09-12). # # `file: state=directory` cree les parents manquants avec les droits par defaut — ici # `0750 root:root`. apt-cacher-ng ne tourne PAS en root : il n'a pas pu traverser # `/var/lib/setops` et rendait 403 sur chaque fichier du depot, alors que le repertoire # final etait bien en 0755 et les fichiers bien la. # # 403 ACCUSE LE FICHIER DEMANDE, pas le repertoire qui le contient — et encore moins le # GRAND-PARENT. On a donc cherche du cote des motifs de fichiers d'apt-cacher-ng, qui # n'y etaient pour rien. Un chemin se traverse en entier ; un droit se verifie de meme. - name: Assurer la racine d'état de Set-OPS, traversable par les services ansible.builtin.file: path: "{{ serveur_artefacts_directs_dir | dirname }}" state: directory owner: root group: root mode: "0755" when: serveur_artefacts_directs_actif | bool - name: Assurer le répertoire du dépôt de binaires directs ansible.builtin.file: path: "{{ serveur_artefacts_directs_dir }}" state: directory owner: root group: root mode: "0755" when: serveur_artefacts_directs_actif | bool - name: Déployer la configuration Set-OPS ansible.builtin.template: src: zzz-setops.conf.j2 dest: /etc/apt-cacher-ng/zzz-setops.conf owner: root group: root mode: "0644" notify: Redémarrer le cache d'artefacts - name: Activer et démarrer le cache d'artefacts ansible.builtin.systemd: name: "{{ serveur_artefacts_service }}" enabled: true state: started when: not ansible_check_mode - name: Appliquer les redémarrages avant de vérifier ansible.builtin.meta: flush_handlers # ÉCRIRE, PUIS RELIRE (D-68). Un service « active » qui n'écoute pas sur le port attendu # est un vert sur un périmètre vide : les clients le découvriraient à leur premier # `apt update`, c'est-à-dire au pire moment. - name: Le cache écoute-t-il vraiment ? ansible.builtin.wait_for: host: "127.0.0.1" port: "{{ serveur_artefacts_port }}" timeout: 30 when: not ansible_check_mode # --- Sonde de supervision (docs/supervision-conception.md) -------------------------- # Le role qui possede la verite depose sa propre sonde ; le porteur (`client_sante`) la # fait tourner et pousse le verdict, sans savoir ce qu'elle mesure. - name: Assurer le repertoire des sondes de supervision ansible.builtin.file: path: /usr/local/lib/setops/sondes state: directory owner: root group: root mode: "0755" - name: Deposer la sonde « cache-apt » ansible.builtin.template: src: sonde-cache-apt.sh.j2 dest: /usr/local/lib/setops/sondes/cache-apt.sh owner: root group: root mode: "0750" - name: Deposer la sonde « cache-apt-volume » ansible.builtin.template: src: sonde-cache-apt-volume.sh.j2 dest: /usr/local/lib/setops/sondes/cache-apt-volume.sh owner: root group: root mode: "0750" # --- REMPLIR LE DEPOT DE BINAIRES DIRECTS -------------------------------------------- # # CETTE MACHINE EST LA SEULE A SORTIR, et c'est le meme principe que pour apt : un seul # telechargement pour tout le site, y compris pour les ecosystemes qui naitront demain. # # ON LIT LES DEFAUTS DES ROLES CONSOMMATEURS, on ne recopie pas leurs versions. Sans ca, # la version vivrait a deux endroits et le depot finirait par servir, en silence, un # artefact que plus personne ne demande. # # `include_vars` SANS `name:` — DELIBEREMENT. Enfermees dans un dictionnaire, les valeurs # qui se citent entre elles ne se resolvent plus : `serveur_forgejo_url` reference # `serveur_forgejo_version`. Le depot connait deja cette marche, payee sur le devis des # certificats ; on ne la repaye pas. - name: Lire ce que les rôles consommateurs déclarent télécharger ansible.builtin.include_vars: dir: "{{ role_path }}/../{{ item }}/defaults" loop: "{{ serveur_artefacts_directs_roles }}" loop_control: label: "{{ item }}" when: serveur_artefacts_directs_actif | bool # `force: false` ET UN `dest` NOMME PAR LA VERSION : une publication ne change pas sous # le meme nom, donc un fichier deja present est le bon. Une nouvelle version porte un # autre nom et se telecharge d'elle-meme — le depot se met a jour en suivant les roles, # sans qu'on ait rien a purger. # # LES REPRISES SONT OBLIGATOIRES, PAS PRUDENTIELLES. `packages.smallstep.com` a deja fait # echouer un deploiement de quarante minutes en pendant au premier essai et en repondant # en 0,48 s au second ; codeberg.org n'a aucune raison d'etre plus fiable. - name: Tenir les binaires directs à la disposition des écosystèmes ansible.builtin.get_url: url: "{{ item.url }}" dest: "{{ serveur_artefacts_directs_dir }}/{{ item.nom }}" mode: "0644" force: false timeout: 60 loop: "{{ serveur_artefacts_directs }}" loop_control: label: "{{ item.nom }}" register: serveur_artefacts_directs_tires retries: 5 delay: 6 until: serveur_artefacts_directs_tires is succeeded # UN AMONT MUET NE DOIT PAS ARRETER LE SITE. Ce depot est une COMMODITE : sans lui, # chaque runner sort lui-meme, comme avant. Le faire echouer rendrait le site # indeployable le jour ou codeberg.org tousse — on aurait echange une sortie Internet # contre une dependance a Internet, ce qui est exactement l'inverse du but. failed_when: false when: - serveur_artefacts_directs_actif | bool - not ansible_check_mode # ON LE DIT, FORT. Un depot a moitie rempli sert a moitie, et le runner qui complete en # sortant sur Internet le fait sans que rien ne le signale. - name: Dire ce que le dépôt de binaires ne tient PAS ansible.builtin.debug: msg: >- {{ (serveur_artefacts_directs_tires.results | default([]) | selectattr('failed', 'defined') | selectattr('failed') | map(attribute='item.nom') | list) | join(', ') }} — absent(s) du depot. Les runners qui en ont besoin sortiront sur Internet. when: - serveur_artefacts_directs_actif | bool - not ansible_check_mode - (serveur_artefacts_directs_tires.results | default([]) | selectattr('failed', 'defined') | selectattr('failed') | list | length) > 0