depots perimes : ce qui n'existe qu'ici ne se detruit pas depuis ici
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>
This commit is contained in:
parent
4188cf5b6f
commit
7ef7ca196a
4 changed files with 212 additions and 0 deletions
48
CHANGELOG.md
48
CHANGELOG.md
|
|
@ -1,5 +1,53 @@
|
|||
# CHANGELOG — Set-OPS
|
||||
|
||||
## 2026-09-20 (9) — Ce qui n'existe qu'ici ne se détruit pas depuis ici
|
||||
|
||||
**83 preuves, `make test` à 0 échec, 133 cibles documentées.** Le runner de
|
||||
Chezlepro-locataire portait encore le clone de `SITE-Chezlepro` — la carte de son
|
||||
hébergeur — longtemps après que son plan ait cessé de la déclarer. `serveur_ops` clone ce
|
||||
qui est déclaré ; il ne retirait rien.
|
||||
|
||||
### Pourquoi ce retrait n'est pas dans le rôle
|
||||
|
||||
Un rôle qui efface des dossiers à chaque passage est une grenade dégoupillée : une faute
|
||||
de frappe dans `serveur_ops_depots` suffirait à perdre du travail local. Le retrait est
|
||||
donc un geste **séparé**, qui **regarde** par défaut et n'efface que sur `CONFIRMER=true`
|
||||
— comme `raser` et `site-raser`.
|
||||
|
||||
`make depots-perimes` nomme ce qu'un runner porte et que son plan ne déclare plus, puis
|
||||
dit ce qu'une confirmation emporterait. **Trois choses ne sont jamais retirées**, même
|
||||
confirmées : ce qui n'est pas un dépôt git, ce qui porte des modifications non validées,
|
||||
et ce qui porte des commits qu'aucun distant ne porte.
|
||||
|
||||
### La garde a servi au premier essai
|
||||
|
||||
Le relevé a nommé **deux** dossiers non déclarés : `SITE-Chezlepro`, et `venv` —
|
||||
l'environnement Python du runner. `venv` a été écarté parce que ce n'est pas un dépôt
|
||||
git. **Une version naïve de ce geste aurait effacé l'environnement d'exécution du
|
||||
runner**, et l'aurait fait en se disant satisfaite.
|
||||
|
||||
C'est la raison d'être de la première des trois règles : un dossier qu'on ne sait pas
|
||||
lire n'est pas un dossier périmé, c'est un dossier qu'on ne comprend pas. Le doute se
|
||||
résout en s'abstenant, pas en effaçant.
|
||||
|
||||
### Écrire, puis relire — ici : regarder, puis vérifier
|
||||
|
||||
Le geste relit 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. Mesure après coup — le
|
||||
runner du locataire ne porte plus que `Set-OPS-public` et `OPS-Chezlepro`, ce que son plan
|
||||
déclare, et sa console rend `portee=tenant`, `configurer=True`, `materialiser=False`,
|
||||
`fabric=False` : même le pouvoir de LIRE la fabric est tombé avec la carte, ce qui est
|
||||
juste — il n'y a plus de miroir à consulter.
|
||||
|
||||
### Ce qui a été vérifié
|
||||
|
||||
`--syntax-check` sur le playbook, `python3 scripts/runbooks.py verifier` (0 écart,
|
||||
la cible neuve est portée par le runbook « Filiation »), `make test` à 0 échec. Le geste
|
||||
a été passé **deux fois** sur le runner réel : une fois pour regarder — rien touché,
|
||||
`changed=0` — puis une fois confirmé, `changed=1`, avec sa relecture.
|
||||
|
||||
**P02 reste en échec**, pour la raison antérieure consignée à l'entrée `(6)`.
|
||||
|
||||
## 2026-09-20 (8) — Un pouvoir se mesure sur ce qu'on peut ouvrir, pas sur ce qu'on peut lire
|
||||
|
||||
**83 preuves, `make test` à 0 échec (21 tests neufs aujourd'hui).** La vue Assistants a
|
||||
|
|
|
|||
10
Makefile
10
Makefile
|
|
@ -1255,6 +1255,16 @@ genome-pousser: ## Le runner du site pousse le genome sur la forge du site — D
|
|||
SETOPS_DEPOT="$(DEPOT)" ansible-playbook -i scripts/site_inventaire.py \
|
||||
playbooks/maintenance/genome_pousser.yml "$${supp[@]}" $(ARGS)
|
||||
|
||||
.PHONY: depots-perimes
|
||||
depots-perimes: ansible-runtime _instance-requise ## Depots qu'un runner ne declare plus : regarde, et retire avec CONFIRMER=true
|
||||
@# DESTRUCTIF SUR CONFIRMATION SEULEMENT. Sans `CONFIRMER=true`, ce geste REGARDE :
|
||||
@# il nomme ce que le runner porte et que son plan ne declare plus, et dit ce qu'un
|
||||
@# retrait emporterait. Trois choses ne sont jamais retirees, meme confirmees : ce
|
||||
@# qui n'est pas un depot git, ce qui porte des modifications non validees, et ce
|
||||
@# qui porte des commits qu'aucun distant ne porte.
|
||||
CONFIRMER="$(CONFIRMER)" ansible-playbook -i $(INVENTAIRE_PRODUCTION) \
|
||||
playbooks/maintenance/depots_perimes.yml $(if $(HOTE),--limit $(HOTE),) $(ARGS)
|
||||
|
||||
.PHONY: site-creer
|
||||
forge-amorcer: ## Amorce la forge d'un SITE neuf avec le genome — CONFIRMER=true
|
||||
@# LE MAILLON QUI N'EXISTAIT PAS (2026-09-12, premiere reconstruction du site depuis
|
||||
|
|
|
|||
|
|
@ -632,6 +632,14 @@ runbooks:
|
|||
pourquoi: >-
|
||||
Le SITE amorce le runner d'un locataire, SANS ses secrets. Trois murs connus :
|
||||
apt, pip, et le genome lui-meme.
|
||||
- cible: depots-perimes
|
||||
nature: destructif
|
||||
fixes: {CONFIRMER: "true"}
|
||||
pourquoi: >-
|
||||
Un depot raye du plan reste sur le disque du runner, qui garde de quoi lire un
|
||||
ecosysteme qu'il ne declare plus. Trois choses ne sont jamais retirees : ce qui
|
||||
n'est pas un depot git, ce qui porte des modifications non validees, et ce qui
|
||||
porte des commits qu'aucun distant ne porte.
|
||||
- cible: emancipation-prouver
|
||||
nature: mesure
|
||||
variables: [SERVICE, HOTE]
|
||||
|
|
|
|||
146
playbooks/maintenance/depots_perimes.yml
Normal file
146
playbooks/maintenance/depots_perimes.yml
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
---
|
||||
# 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
|
||||
Loading…
Reference in a new issue