Set-OPS-Public/playbooks/maintenance/genome_pousser.yml
Daniel Allaire ee5aad5f41 genome_pousser : la simulation dit ce que la poussee ferait, puis s'arrete
En --check, la sonde de l'organisation (une lecture uri) etait sautee et la
tache suivante echouait sur un statut absent, cache par no_log. La suite
(colis, reception, poussee) est faite de command et d'ecritures que --check
saute aussi : le verdict n'aurait rien prouve.

Les deux sondes de la forge tournent desormais en simulation. En --check, le
playbook lit la branche de la forge, la compare au poste (rev-list
--left-right), nomme pour chaque depot ce que la poussee ferait, et s'arrete
avant la premiere ecriture. Temoin : quatre cas nommes contre des clones
jetables ; a jour partout contre les vrais depots, aux deux sites.

CHANGELOG 150.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 04:11:52 -04:00

532 lines
24 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)
# make genome-pousser ARGS=--check ce que la poussee ferait, depot par depot, sans rien ecrire
#
# 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.
# UNE FORGE NEUVE, UN RUNNER NEUF (2026-10-13, premier site construit depuis zero). Le
# runner du site ne fabrique sa cle qu'APRES avoir clone le genome, et il ne peut cloner
# que ce que cette commande a pousse — qui exige sa cle. Personne n'allait au bout. On
# fabrique donc la cle ici si elle manque, A L'IDENTIQUE de `serveur_ops` (meme commande,
# meme commentaire, `creates:`) : le role la retrouvera et ne la refera pas.
- name: Assurer le répertoire SSH du runner
ansible.builtin.file:
path: "{{ genome_racine }}/.ssh"
state: directory
mode: "0700"
- name: Fabriquer la clé du runner si elle n'existe pas encore
ansible.builtin.command:
cmd: >-
ssh-keygen -t {{ serveur_ops_cle_type | default('ed25519') }} -N ''
-C "setops@{{ inventory_hostname }}.{{ domaine_interne }}"
-f {{ genome_racine }}/.ssh/id_{{ serveur_ops_cle_type | default('ed25519') }}
creates: "{{ genome_racine }}/.ssh/id_{{ serveur_ops_cle_type | default('ed25519') }}"
# L'ORGANISATION ET SES DEPOTS N'ETAIENT CREES PAR PERSONNE : au site de Chezlepro, une
# fois, a la main. Meme forme qu'elle : organisation PUBLIQUE ; le moteur et les modeles
# PUBLICS (le runner les clone en anonyme) ; la carte du site et les plans des
# locataires PRIVES (`serveur_ops_roles_prives`, lus en SSH par cle de deploiement).
- name: L'organisation du génome existe-t-elle ?
ansible.builtin.uri:
url: "https://forge.{{ domaine_interne }}/api/v1/orgs/{{ serveur_ops_forge_organisation | default('genome') }}"
validate_certs: false
status_code: [200, 404]
# UNE LECTURE, DONC AUSSI EN SIMULATION : sans elle, `--check` sautait la sonde et la
# tache suivante echouait sur un statut absent (2026-10-14).
check_mode: false
register: genome_org
- name: Créer l'organisation du génome
ansible.builtin.uri:
url: "https://forge.{{ domaine_interne }}/api/v1/orgs"
method: POST
url_username: forgejo-admin
url_password: "{{ vault_forgejo_admin }}"
force_basic_auth: true
validate_certs: false
body_format: json
body:
username: "{{ serveur_ops_forge_organisation | default('genome') }}"
visibility: public
status_code: 201
no_log: true
when: genome_org.status == 404
- name: Quels dépôts poussés existent déjà sur la forge ?
ansible.builtin.uri:
url: "https://forge.{{ domaine_interne }}/api/v1/repos/{{ serveur_ops_forge_organisation | default('genome') }}/{{ item.depot }}"
url_username: forgejo-admin
url_password: "{{ vault_forgejo_admin }}"
force_basic_auth: true
validate_certs: false
status_code: [200, 404]
loop: "{{ genome_a_pousser }}"
loop_control:
label: "{{ item.depot }}"
no_log: true
check_mode: false
register: genome_depots_forge
# LA SIMULATION DIT CE QUE LA POUSSEE FERAIT, PUIS S'ARRETE (2026-10-14).
#
# Tout ce qui suit est `uri` en ecriture ou `command` : `--check` le saute, et le verdict
# final comparait alors une forge intacte a des colis jamais recus. On ne lit donc que la
# forge et le poste, on compare, et on rend la main avant la premiere ecriture.
- name: Simulation — la branche que porte la forge
ansible.builtin.uri:
url: >-
https://forge.{{ domaine_interne }}/api/v1/repos/{{ serveur_ops_forge_organisation
| default('genome') }}/{{ item.item.depot }}/branches/{{ item.item.branche | default('main') }}
url_username: forgejo-admin
url_password: "{{ vault_forgejo_admin }}"
force_basic_auth: true
validate_certs: false
status_code: [200, 404]
loop: "{{ genome_depots_forge.results }}"
loop_control:
label: "{{ item.item.depot }}"
register: genome_sim_forge
retries: 4
delay: 3
until: genome_sim_forge is succeeded
check_mode: false
no_log: true
when:
- ansible_check_mode
- item.status == 200
# `--left-right` : a gauche ce que la forge porte et que le poste n'a pas (le runner
# refusera, `--ff-only`), a droite ce qui serait pousse. rc 128 : le poste ne connait
# pas le commit de la forge — l'avance rapide est impossible a juger d'ici.
- name: Simulation — l'écart entre la forge et le poste
ansible.builtin.command:
argv:
- git
- -C
- "{{ genome_freres }}/{{ item.item.item.dest }}"
- rev-list
- --count
- --left-right
- "{{ item.json.commit.id }}...refs/heads/{{ item.item.item.branche | default('main') }}"
delegate_to: localhost
become: false
loop: "{{ genome_sim_forge.results }}"
loop_control:
label: "{{ item.item.item.depot }}"
register: genome_sim_ecart
check_mode: false
changed_when: false
failed_when: false
when:
- ansible_check_mode
- item.status | default(0) == 200
- name: Simulation — ce que la poussée ferait
vars:
genome_sim_lignes: >-
{%- set out = [] -%}
{%- if genome_org.status == 404 -%}
{%- set _ = out.append('organisation ' ~ (serveur_ops_forge_organisation | default('genome')) ~ ' : à créer') -%}
{%- endif -%}
{%- for r in genome_sim_ecart.results -%}
{%- set d = r.item.item.item.depot -%}
{%- if r.item.item.status == 404 -%}
{%- set _ = out.append(d ~ ' : dépôt à créer, puis tout poussé') -%}
{%- elif r.item.status | default(0) == 404 -%}
{%- set _ = out.append(d ~ ' : dépôt vide, tout poussé') -%}
{%- elif r.rc != 0 -%}
{%- set _ = out.append(d ~ ' : la forge porte ' ~ r.item.json.commit.id[:7]
~ ', inconnu du poste — avance rapide impossible à juger d ici') -%}
{%- else -%}
{%- set lr = r.stdout.split() | map('int') | list -%}
{%- if lr[0] > 0 -%}
{%- set _ = out.append(d ~ ' : la forge a ' ~ lr[0] ~ ' commit(s) absent(s) du poste — le runner REFUSERA (--ff-only)') -%}
{%- elif lr[1] == 0 -%}
{%- set _ = out.append(d ~ ' : à jour (' ~ r.item.json.commit.id[:7] ~ ')') -%}
{%- else -%}
{%- set _ = out.append(d ~ ' : ' ~ r.item.json.commit.id[:7] ~ ' → ' ~ lr[1] ~ ' commit(s) en avance rapide') -%}
{%- endif -%}
{%- endif -%}
{%- endfor -%}
{{ out }}
ansible.builtin.debug:
msg: "{{ genome_sim_lignes }}"
when: ansible_check_mode
- name: Simulation — s'arrêter avant la première écriture
ansible.builtin.meta: end_host
when: ansible_check_mode
- name: Créer les dépôts qui manquent
ansible.builtin.uri:
url: "https://forge.{{ domaine_interne }}/api/v1/orgs/{{ serveur_ops_forge_organisation | default('genome') }}/repos"
method: POST
url_username: forgejo-admin
url_password: "{{ vault_forgejo_admin }}"
force_basic_auth: true
validate_certs: false
body_format: json
body:
name: "{{ item.item.depot }}"
private: "{{ item.item.role in (serveur_ops_roles_prives | default(['instance', 'tenant', 'hebergeur'])) }}"
default_branch: "{{ item.item.branche | default('main') }}"
auto_init: false
status_code: 201
loop: "{{ genome_depots_forge.results }}"
loop_control:
label: "{{ item.item.depot }}"
no_log: true
when: item.status == 404
- 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. Lecture authentifiée depuis le 2026-10-11 (dépôts privés).
ansible.builtin.uri:
url: >-
https://forge.{{ domaine_interne }}/api/v1/repos/{{ serveur_ops_forge_organisation
| default('genome') }}/{{ item.depot }}/branches/{{ item.branche | default('main') }}
# AUTHENTIFIE (2026-10-11) : les depots des locataires et du site deviennent prives, et
# l'API anonyme y repondrait 404 — un « absent » qui n'en serait pas un.
url_username: forgejo-admin
url_password: "{{ vault_forgejo_admin }}"
force_basic_auth: true
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
# AUTHENTIFIE (2026-10-11) : les depots des locataires et du site deviennent prives, et
# l'API anonyme y repondrait 404 — un « absent » qui n'en serait pas un.
url_username: forgejo-admin
url_password: "{{ vault_forgejo_admin }}"
force_basic_auth: true
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