serveur_ops clone ce que le plan declare et ne retire rien : le runner de Chezlepro-locataire portait encore le clone de SITE-Chezlepro, la carte de son hebergeur, longtemps apres que son plan ait cesse de la declarer. Le retrait n'entre pas dans le role. Un role qui efface des dossiers a chaque passage est une grenade degoupillee : une faute de frappe dans serveur_ops_depots suffirait a perdre du travail local. C'est donc un geste separe, qui regarde par defaut et n'efface que sur CONFIRMER=true. Trois choses ne sont jamais retirees, meme confirmees : ce qui n'est pas un depot git, ce qui porte des modifications non validees, ce qui porte des commits qu'aucun distant ne porte. La garde a servi au premier essai : le releve a nomme SITE-Chezlepro et venv, et venv a ete ecarte parce que ce n'est pas un depot git. Une version naive aurait efface l'environnement Python du runner en se disant satisfaite. Passe deux fois sur le runner reel : regarder (changed=0), puis confirmer (changed=1) avec relecture. Le runner ne porte plus que Set-OPS-public et OPS-Chezlepro ; sa console rend portee=tenant, materialiser=False, fabric=False — le pouvoir de LIRE la fabric est tombe avec la carte, et c'est juste. Valide : syntax-check du playbook, runbooks.py verifier a 0 ecart (la cible neuve est portee par le runbook Filiation), make test a 0 echec. P02 reste en echec pour la raison anterieure. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
146 lines
6.1 KiB
YAML
146 lines
6.1 KiB
YAML
---
|
|
# LES DEPOTS D'UN RUNNER SUIVENT SA DECLARATION — DANS LES DEUX SENS.
|
|
#
|
|
# `serveur_ops` clone ce que `serveur_ops_depots` declare. Il ne retire rien : un depot
|
|
# raye du plan reste sur le disque, et le runner garde de quoi lire un ecosysteme qu'il
|
|
# ne declare plus. Mesure du 2026-09-20 : le runner de Chezlepro-LOCATAIRE portait encore
|
|
# le clone de `SITE-Chezlepro` — la carte de son hebergeur — apres que son plan ait cesse
|
|
# de la declarer.
|
|
#
|
|
# POURQUOI CE N'EST PAS DANS LE ROLE. Retirer un depot est DESTRUCTIF, et la regle du
|
|
# depot est qu'une action destructive s'annonce et se confirme. Un role qui efface des
|
|
# dossiers a chaque passage serait une grenade degoupillee : il suffirait d'une faute de
|
|
# frappe dans `serveur_ops_depots` pour perdre un travail local. On en fait donc un geste
|
|
# separe, qui REGARDE par defaut et n'efface que sur `CONFIRMER=true`.
|
|
#
|
|
# CE QUI NE SERA JAMAIS EFFACE, meme avec la confirmation :
|
|
# - un dossier qui n'est pas un depot git (on ne sait pas ce que c'est) ;
|
|
# - un depot qui porte des modifications non validees ;
|
|
# - un depot qui porte des commits qu'aucun distant ne porte.
|
|
# Dans ces trois cas le playbook le DIT et passe. Ce qui n'existe qu'ici ne se detruit
|
|
# pas depuis ici.
|
|
#
|
|
# make depots-perimes regarde, n'efface rien
|
|
# make depots-perimes CONFIRMER=true retire ceux qui ne portent rien d'unique
|
|
|
|
- name: Les dépôts qu'un runner ne déclare plus
|
|
hosts: "{{ cible | default('serveur_ops') }}"
|
|
become: true
|
|
become_user: "{{ serveur_ops_utilisateur | default('setops') }}"
|
|
gather_facts: false
|
|
|
|
vars:
|
|
racine: "{{ serveur_ops_racine | default('/opt/setops') }}"
|
|
confirme: "{{ (lookup('env', 'CONFIRMER') | default('', true)) == 'true' }}"
|
|
|
|
tasks:
|
|
- name: Exiger de savoir ce que ce runner déclare
|
|
ansible.builtin.assert:
|
|
that:
|
|
- (serveur_ops_depots | default([])) | length > 0
|
|
fail_msg: >-
|
|
Cet hôte ne déclare aucun dépôt (`serveur_ops_depots`). Sans la liste de ce
|
|
qu'il DOIT porter, on ne peut pas dire ce qui est périmé — et surtout pas
|
|
l'effacer.
|
|
|
|
- name: Lister ce que le runner porte
|
|
ansible.builtin.find:
|
|
paths: "{{ racine }}"
|
|
file_type: directory
|
|
recurse: false
|
|
hidden: false
|
|
register: presents
|
|
|
|
- name: Retenir ceux que le plan ne déclare plus
|
|
ansible.builtin.set_fact:
|
|
perimes: >-
|
|
{{ presents.files | map(attribute='path') | map('basename') | list
|
|
| difference(serveur_ops_depots | map(attribute='dest') | list) }}
|
|
|
|
- name: Le dire quand il n'y a rien à retirer
|
|
ansible.builtin.debug:
|
|
msg: >-
|
|
Ce runner ne porte que ce qu'il déclare
|
|
({{ serveur_ops_depots | map(attribute='dest') | join(', ') }}).
|
|
when: perimes | length == 0
|
|
|
|
# ÉCRIRE, PUIS RELIRE — ici : REGARDER avant de détruire. Trois questions par dépôt, et
|
|
# la réponse décide. `git status` et `git log` ne modifient rien.
|
|
- name: Regarder ce que chaque dépôt périmé porte d'unique
|
|
ansible.builtin.shell:
|
|
cmd: |
|
|
set -u
|
|
d={{ (racine ~ '/' ~ item) | quote }}
|
|
if [ ! -d "$d/.git" ]; then echo '{"git": false}'; exit 0; fi
|
|
sale=$(git -C "$d" status --porcelain 2>/dev/null | head -c 2000)
|
|
# Des commits qu'AUCUN distant ne porte : ce qui n'existe qu'ici.
|
|
seuls=$(git -C "$d" log --branches --not --remotes --oneline 2>/dev/null | head -c 2000)
|
|
printf '{"git": true, "sale": %s, "seuls": %s}\n' \
|
|
"$([ -n "$sale" ] && echo true || echo false)" \
|
|
"$([ -n "$seuls" ] && echo true || echo false)"
|
|
executable: /bin/bash
|
|
loop: "{{ perimes }}"
|
|
register: examen
|
|
changed_when: false
|
|
|
|
- name: Nommer ce qui est retenu, et pourquoi
|
|
ansible.builtin.debug:
|
|
msg: >-
|
|
{{ item.item }} : {{
|
|
'ce n_est pas un dépôt git — on ne sait pas ce que c_est, on n_y touche pas'
|
|
if not (item.stdout | from_json).git else
|
|
'des modifications non validées vivent ici' if (item.stdout | from_json).sale else
|
|
'des commits qu_aucun distant ne porte vivent ici' if (item.stdout | from_json).seuls else
|
|
'ne porte rien d_unique — retirable'
|
|
}}
|
|
loop: "{{ examen.results }}"
|
|
loop_control:
|
|
label: "{{ item.item }}"
|
|
|
|
- name: Retenir ceux qu'on peut retirer sans rien perdre
|
|
ansible.builtin.set_fact:
|
|
retirables: >-
|
|
{{ examen.results
|
|
| selectattr('stdout')
|
|
| selectattr('stdout', 'search', '"git": true')
|
|
| rejectattr('stdout', 'search', '"sale": true')
|
|
| rejectattr('stdout', 'search', '"seuls": true')
|
|
| map(attribute='item') | list }}
|
|
|
|
- name: Dire ce qu'une confirmation retirerait
|
|
ansible.builtin.debug:
|
|
msg: >-
|
|
{{ retirables | length }} dépôt(s) retirable(s) : {{ retirables | join(', ') }}.
|
|
Rien n'a été touché — relancer avec CONFIRMER=true pour les retirer.
|
|
when:
|
|
- retirables | length > 0
|
|
- not (confirme | bool)
|
|
|
|
- name: Retirer les dépôts que ce runner ne déclare plus
|
|
ansible.builtin.file:
|
|
path: "{{ racine }}/{{ item }}"
|
|
state: absent
|
|
loop: "{{ retirables }}"
|
|
when:
|
|
- confirme | bool
|
|
- not ansible_check_mode
|
|
|
|
# RELIRE APRÈS AVOIR ÉCRIT. Un `state: absent` qui se dit satisfait sans que le
|
|
# dossier ait disparu est exactement le genre de succès qui ment.
|
|
- name: Vérifier qu'ils ont bien disparu
|
|
ansible.builtin.stat:
|
|
path: "{{ racine }}/{{ item }}"
|
|
loop: "{{ retirables }}"
|
|
register: apres
|
|
when: confirme | bool
|
|
|
|
- name: Refuser un retrait qui n'a pas eu lieu
|
|
ansible.builtin.assert:
|
|
that:
|
|
- not (item.stat.exists | default(false))
|
|
fail_msg: "{{ item.item }} est toujours là après le retrait."
|
|
success_msg: "{{ item.item }} : retiré."
|
|
loop: "{{ apres.results | default([]) }}"
|
|
loop_control:
|
|
label: "{{ item.item | default('') }}"
|
|
when: confirme | bool
|