Set-OPS-Public/playbooks/maintenance/genome_pousser.yml
Daniel Allaire e8907285d5 publier : les releases voyagent jusqu'aux deux forges ; wiki publie sur la forge du site
Le colis du genome ne portait que la branche : les etiquettes de release
n'arrivaient jamais sur la forge du site. Il les porte desormais, le runner les
pousse sans forcer, et la forge est relue par son API. publier.py pousse aussi
les etiquettes vers eregion. Le wiki, jamais publie sur la forge du site, l'est.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 19:04:31 -04:00

346 lines
16 KiB
YAML

---
# LE RUNNER DU SITE POUSSE LE GENOME SUR LA FORGE DU SITE.
#
# POURQUOI CE DETOUR, ET POURQUOI IL EST BON (2026-08-26).
#
# La forge du genome vit DANS le site, sur un reseau que le poste de l'exploitant ne route
# pas — et c'est voulu : le poste n'a de patte que sur l'administration, la frontiere
# police le reste. Il ne peut donc pas pousser lui-meme.
#
# On ne perce pas un chemin pour lui. Le RUNNER existe exactement pour ca : il est dans le
# site, il a le genome, il a une identite. On lui PORTE les commits dans un `git bundle` —
# un fichier, verifiable, qui ne demande aucun reseau — et c'est LUI qui pousse.
#
# Mesure du 2026-08-26 : la forge du site etait restee quatre commits en arriere du poste,
# sans que rien ne le signale. Le runner clonait donc un moteur perime — dont, ce jour-la,
# le correctif qui desarme le pare-feu Proxmox. Un ecosysteme qui se reproduit depuis une
# forge en retard reproduit ses defauts.
#
# CE QUI EST POUSSE SE DERIVE, il ne s'ecrit pas : `serveur_ops_depots` (au plan du site)
# dit ce que le runner porte. On pousse ceux dont une copie locale existe a cote du
# moteur, et rien d'autre.
#
# make genome-pousser tous les depots portes par le runner
# make genome-pousser DEPOT=<nom> un seul (le nom du depot sur la forge)
#
# L'ECRITURE S'AUTHENTIFIE, LA LECTURE NON. `serveur_ops` clone en anonyme, par doctrine.
# Pousser est un autre acte : le runner s'y presente avec SA cle — celle que `serveur_ops`
# lui a generee — declaree en cle de depot, en ecriture, sur ces depots-la seulement.
# `url` reste en HTTPS anonyme ; seul `pushurl` passe par SSH.
- name: Le runner du site pousse le genome sur la forge du site
hosts: serveur_ops_site
become: true
become_user: "{{ serveur_ops_utilisateur | default('setops') }}"
gather_facts: false
vars:
genome_racine: "{{ serveur_ops_racine | default('/opt/setops') }}"
genome_depot_demande: "{{ lookup('env', 'SETOPS_DEPOT') | default('', true) }}"
# Le dossier qui contient le moteur ET ses depots freres, sur le CONTROLEUR.
genome_freres: "{{ playbook_dir | dirname | dirname | dirname }}"
# Le dossier du moteur, tel que le plan le nomme — jamais ecrit en dur.
genome_moteur: "{{ (serveur_ops_depots | selectattr('role', 'eq', 'moteur') | first).dest }}"
tasks:
- name: Exiger de savoir quoi pousser
ansible.builtin.assert:
that:
- (serveur_ops_depots | default([])) | length > 0
fail_msg: >-
Ce poste ne déclare aucun dépôt (`serveur_ops_depots`). Rien à pousser — et
probablement rien à piloter non plus : voir le plan de cette machine.
# Ce que le runner porte, restreint a ce dont une copie existe sur le controleur.
# Un depot declare mais absent d'ici n'est pas une faute : il vit ailleurs.
- name: Voir lesquels de ces dépôts existent sur le contrôleur
ansible.builtin.stat:
path: "{{ genome_freres }}/{{ item.dest }}/.git"
delegate_to: localhost
become: false
loop: "{{ serveur_ops_depots }}"
loop_control:
label: "{{ item.dest }}"
register: genome_locaux
- name: Retenir ceux qu'on peut effectivement porter
ansible.builtin.set_fact:
genome_a_pousser: >-
{{ genome_locaux.results
| selectattr('stat.exists')
| map(attribute='item') | list
| selectattr('depot', 'equalto', genome_depot_demande)
| list
if genome_depot_demande | length > 0
else genome_locaux.results | selectattr('stat.exists')
| map(attribute='item') | list }}
- name: Dire ce qui va être poussé
ansible.builtin.debug:
msg: >-
{{ genome_a_pousser | map(attribute='depot') | list
| ternary(genome_a_pousser | map(attribute='depot') | list,
'aucun dépôt à pousser') }}
- name: Refuser un DEPOT= qui ne désigne rien
ansible.builtin.assert:
that: genome_a_pousser | length > 0
fail_msg: >-
Aucun dépôt à pousser. {{ 'Le dépôt `' ~ genome_depot_demande ~ '` n est pas
porté par ce runner, ou n a pas de copie locale à côté du moteur.'
if genome_depot_demande | length > 0 else
'Aucun des dépôts déclarés n a de copie locale à côté du moteur.' }}
# LA CLE DU RUNNER DOIT ETRE AUTORISEE SUR CE QU'IL POUSSE — sinon la forge refuse,
# depot par depot, et l'exploitant doit aller cliquer. La cible s'en charge : la
# declaration est idempotente (422 = deja la), et sa portee est exactement ce qu'on
# pousse, jamais plus large.
- name: Lire la clé publique du runner
ansible.builtin.slurp:
src: "{{ genome_racine }}/.ssh/id_{{ serveur_ops_cle_type | default('ed25519') }}.pub"
register: genome_cle
- name: Autoriser cette clé en écriture sur les dépôts poussés
ansible.builtin.uri:
url: >-
https://forge.{{ domaine_interne }}/api/v1/repos/{{ serveur_ops_forge_organisation
| default('genome') }}/{{ item.depot }}/keys
method: POST
url_username: forgejo-admin
url_password: "{{ vault_forgejo_admin }}"
force_basic_auth: true
validate_certs: false
body_format: json
body:
title: "runner du site — {{ inventory_hostname }}"
key: "{{ genome_cle.content | b64decode | trim }}"
read_only: false
status_code: [201, 422]
loop: "{{ genome_a_pousser }}"
loop_control:
label: "{{ item.depot }}"
register: genome_cles
changed_when: false
no_log: true
- name: Un endroit pour fabriquer les colis, sur le contrôleur
ansible.builtin.tempfile:
state: directory
suffix: -genome
delegate_to: localhost
become: false
register: genome_atelier
- name: Un endroit pour déposer les colis
ansible.builtin.file:
path: "{{ genome_racine }}/.bundles"
state: directory
owner: "{{ serveur_ops_utilisateur | default('setops') }}"
group: "{{ serveur_ops_utilisateur | default('setops') }}"
mode: "0700"
# LA LOGIQUE GIT VIT DANS `scripts/genome_colis.py`, comme partout ici : le playbook
# orchestre, le script fait. Il voyage AVEC le moteur — le runner en a donc une copie
# dès qu'il l'a cloné, et rien de spécial n'est déposé sur la machine.
- name: Emballer chaque dépôt sur le contrôleur
# `argv` ET NON `cmd` : le dossier des dépôts frères peut contenir des espaces —
# c'est le cas ici (« Espace Chezlepro »). Une chaîne `cmd` est découpée sur les
# blancs, et python recevait « /home/…/Espace » comme nom de fichier.
ansible.builtin.command:
argv:
- python3
- "{{ genome_freres }}/{{ genome_moteur }}/scripts/genome_colis.py"
- emballer
- "{{ genome_freres }}/{{ item.dest }}"
- "{{ genome_atelier.path }}/{{ item.depot }}.bundle"
- --branche
- "{{ item.branche | default('main') }}"
delegate_to: localhost
become: false
loop: "{{ genome_a_pousser }}"
loop_control:
label: "{{ item.dest }}"
register: genome_emballe
changed_when: true
- name: Porter les colis jusqu'au runner
ansible.builtin.copy:
src: "{{ genome_atelier.path }}/{{ item.depot }}.bundle"
dest: "{{ genome_racine }}/.bundles/{{ item.depot }}.bundle"
owner: "{{ serveur_ops_utilisateur | default('setops') }}"
group: "{{ serveur_ops_utilisateur | default('setops') }}"
mode: "0600"
loop: "{{ genome_a_pousser }}"
loop_control:
label: "{{ item.depot }}"
# L'OUTIL VOYAGE AVEC LE COLIS, il n'est pas supposé déjà là (2026-08-26).
#
# Première version : on lançait `scripts/genome_colis.py` depuis le moteur cloné sur
# le runner. Circulaire — le runner ne reçoit cette version du script qu'APRÈS la
# poussée qu'elle sert à faire. Et ça se reproduirait à chaque évolution de l'outil :
# le runner en retard ne saurait jamais rattraper son retard.
#
# C'est le contrôleur qui détient la version qui fait autorité. Il la porte.
- name: Porter l'outil lui-même jusqu'au runner
ansible.builtin.copy:
src: "{{ genome_freres }}/{{ genome_moteur }}/scripts/genome_colis.py"
dest: "{{ genome_racine }}/.bundles/genome_colis.py"
owner: "{{ serveur_ops_utilisateur | default('setops') }}"
group: "{{ serveur_ops_utilisateur | default('setops') }}"
mode: "0700"
- name: Le runner vérifie, avance en avance rapide, et pousse
ansible.builtin.command:
argv:
- python3
- "{{ genome_racine }}/.bundles/genome_colis.py"
- recevoir
- "{{ genome_racine }}/{{ item.dest }}"
- "{{ genome_racine }}/.bundles/{{ item.depot }}.bundle"
- "git@forge.{{ domaine_interne }}:{{ serveur_ops_forge_organisation | default('genome') }}/{{ item.depot }}.git"
- --cle
- "{{ genome_racine }}/.ssh/id_{{ serveur_ops_cle_type | default('ed25519') }}"
- --branche
- "{{ item.branche | default('main') }}"
loop: "{{ genome_a_pousser }}"
loop_control:
label: "{{ item.depot }}"
register: genome_recu
# `changed_when: false` ET C'EST VOULU. Dans une boucle, ni le registre (pas encore
# defini) ni `stdout` nu ne sont lisibles ici — les deux ont ete essayes, les deux
# ont leve. Plutot qu'une condition qui ment ou qui casse, on ne pretend rien : ce
# qui a change est dit par le VERDICT, plus bas, qui compare la forge au runner et
# nomme la transition. La verite vient de la forge, pas d'un drapeau de tache.
changed_when: false
- name: Demander à la forge ce qu'elle porte maintenant
# On interroge son API plutôt que `git ls-remote` : c'est la forge elle-même qui
# répond, et la lecture est anonyme — aucun justificatif n'entre ici.
ansible.builtin.uri:
url: >-
https://forge.{{ domaine_interne }}/api/v1/repos/{{ serveur_ops_forge_organisation
| default('genome') }}/{{ item.depot }}/branches/{{ item.branche | default('main') }}
validate_certs: false
status_code: [200]
loop: "{{ genome_a_pousser }}"
loop_control:
label: "{{ item.depot }}"
register: genome_forge
# LE PREMIER CONTACT AVEC LA FORGE RATE PARFOIS (mesure du 2026-08-26 : une fois sur
# six, poignee TLS expiree, puis cinq reponses de suite). Ce n'est pas le chemin —
# il est prouve — c'est l'acceptation TLS apres un temps d'inactivite. On reessaie
# plutot que de rendre un verdict sur un alea.
retries: 4
delay: 3
until: genome_forge is succeeded
changed_when: false
- name: La forge porte-t-elle exactement ce que le runner porte ?
ansible.builtin.assert:
that:
- genome_forge.results[idx].json.commit.id
== (genome_recu.results[idx].stdout | from_json).apres
fail_msg: >-
{{ item.depot }} : la forge porte
{{ genome_forge.results[idx].json.commit.id[:8] | default('rien') }} alors que
le runner porte
{{ (genome_recu.results[idx].stdout | from_json).apres[:8] }}. La poussée n'a pas
abouti — ne pas cloner depuis cette forge tant que ce n'est pas réglé.
success_msg: >-
{{ item.depot }} :
{{ (genome_recu.results[idx].stdout | from_json).avant[:7] }} →
{{ (genome_recu.results[idx].stdout | from_json).apres[:7] }}, et la forge le porte.
loop: "{{ genome_a_pousser }}"
loop_control:
label: "{{ item.depot }}"
index_var: idx
# LES RELEASES AUSSI (2026-10-07). Le colis ne portait que la branche : `v2026.08.21` et
# `v2026.10.07` manquaient a la forge du site. Il porte desormais les etiquettes ; on
# demande a la forge si elle a TOUTES celles du poste. 404 = aucune etiquette sur ce
# depot, ce qui est juste quand le poste n'en a pas non plus.
- name: Demander à la forge les étiquettes qu'elle porte
ansible.builtin.uri:
url: >-
https://forge.{{ domaine_interne }}/api/v1/repos/{{ serveur_ops_forge_organisation
| default('genome') }}/{{ item.depot }}/git/refs/tags
validate_certs: false
status_code: [200, 404]
loop: "{{ genome_a_pousser }}"
loop_control:
label: "{{ item.depot }}"
register: genome_forge_etiquettes
retries: 4
delay: 3
until: genome_forge_etiquettes is succeeded
changed_when: false
- name: La forge porte-t-elle toutes les releases du poste ?
vars:
genome_etiquettes_poste: "{{ (genome_emballe.results[idx].stdout | from_json).etiquettes | default([]) }}"
genome_etiquettes_forge: >-
{{ (genome_forge_etiquettes.results[idx].json | default([]) if genome_forge_etiquettes.results[idx].status == 200 else [])
| map(attribute='ref') | map('regex_replace', '^refs/tags/', '') | list }}
ansible.builtin.assert:
that:
- genome_etiquettes_poste | difference(genome_etiquettes_forge) | length == 0
fail_msg: >-
{{ item.depot }} : la forge n'a pas
{{ genome_etiquettes_poste | difference(genome_etiquettes_forge) | join(', ') }}.
success_msg: >-
{{ item.depot }} : {{ genome_etiquettes_poste | length }} etiquette(s) du poste, toutes sur la forge.
loop: "{{ genome_a_pousser }}"
loop_control:
label: "{{ item.depot }}"
index_var: idx
# LE RUNNER DU SITE AVANCE SON PROPRE CLONE, ET SA CONSOLE NE LE SAIT PAS (2026-09-20).
#
# C'est LUI qui pousse, donc il doit d'abord recevoir : ce playbook fait avancer le
# moteur du site sans qu'aucun role ne passe. `serveur_ops` redemarre bien la console
# quand le clone avance, mais il ne verra jamais cet avancement-ci — le clone sera
# deja a jour quand il passera. Le site garderait donc une console servant l'ancien
# code, exactement le defaut corrige le meme jour chez les locataires.
#
# `python3 scripts/inventory_gui.py` lit son code au demarrage, et seulement la. Ce
# qui a avance doit etre relu, et personne d'autre que ce playbook ne sait qu'il a
# avance : le verdict ci-dessus nomme la transition, il suffit de l'ecouter.
- name: Retenir ce que le colis du moteur a fait bouger
ansible.builtin.set_fact:
genome_moteur_colis: >-
{{ (genome_recu.results | default([]))
| selectattr('item.dest', 'equalto', genome_moteur)
| map(attribute='stdout') | list }}
- name: Voir si ce runner sert une console
ansible.builtin.stat:
path: /etc/systemd/system/setops-gui.service
register: genome_console_unite
- name: Redémarrer la console du site quand son moteur a avancé
ansible.builtin.systemd:
name: setops-gui.service
state: restarted
daemon_reload: true
become: true
become_user: root
when:
- genome_console_unite.stat.exists
- genome_moteur_colis | length > 0
- (genome_moteur_colis[0] | from_json).avant
!= (genome_moteur_colis[0] | from_json).apres
# Le genome n'a pas a trainer en double sur la machine : ce qui compte est dans les
# depots et sur la forge.
- name: Retirer les colis
ansible.builtin.file:
path: "{{ genome_racine }}/.bundles"
state: absent
- name: Retirer l'atelier du contrôleur
ansible.builtin.file:
path: "{{ genome_atelier.path }}"
state: absent
delegate_to: localhost
become: false