LA DERNIERE DEPENDANCE QUI SORTAIT PAR ELLE-MEME. Les collections Ansible suivent
depuis longtemps l idiome de ce depot : le CONTROLEUR telecharge une fois puis
pousse par SSH, aucun flux nouveau depuis la cible, et l artefact devient
deployable hors ligne. pip sortait encore vers PyPI, en HTTPS et par nom.
Un tenant n a NI DNS SORTANT NI 443 vers l Internet, et c est voulu. Mesure sur
ops-01, premiere machine de la reconstruction de Chezlepro :
ERROR: Could not find a version that satisfies the requirement ansible-core
Echec temporaire dans la resolution du nom
PAS LE PAQUET DEBIAN : trixie propose ansible-core 2.19.4, hors de la plage
epinglee. Y aller demanderait de deplacer l epinglage vers une version majeure aux
changements de gabarits connus, trois jours apres l avoir pose pour cause de
portabilite.
--no-index EST LE POINT, PAS UNE OPTIMISATION. Sans lui, pip resterait capable de
sortir vers PyPI le jour ou le depot local serait incomplet : la reussite
dependrait d un flux que ce tenant n a pas le droit d avoir, et une lignee
autonome deviendrait une lignee qui a l air autonome.
LE CONTROLEUR ET LA CIBLE DOIVENT PARTAGER LEUR PYTHON, et ca ne se devine pas.
pyyaml porte du C compile ; une roue cp313 posee sur un python 3.12 ne s installe
pas, et l echec accuserait le depot local au lieu de l ecart entre deux machines.
Mesure et dit ici, ou on peut encore le nommer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
522 lines
24 KiB
YAML
522 lines
24 KiB
YAML
---
|
|
# UN POSTE QUI NE SAIT PAS SUR QUOI IL TRAVAILLE N'A RIEN À PILOTER.
|
|
#
|
|
# On exige explicitement ce qu'il pilote, plutôt que de retomber sur un défaut : le
|
|
# défaut nommait patient 0, et un écosystème distrait aurait cloné le génome d'un autre
|
|
# sans qu'aucune erreur ne le signale.
|
|
#
|
|
# DEUX FORMES DE RUNNER, ET UNE SEULE ÉTAIT PRÉVUE (2026-08-25).
|
|
#
|
|
# Un runner de TENANT pilote un écosystème : il monte son plan par le lien `instance`,
|
|
# détient sa voûte, et configure ses services. Il lui faut donc un dépôt `role: instance`
|
|
# et `serveur_ops_instance`.
|
|
#
|
|
# Un runner de SITE ne pilote aucun écosystème — il MATÉRIALISE le terrain que d'autres
|
|
# occuperont : VNets, VM, routes, flux. Son propre inventaire est DYNAMIQUE
|
|
# (`site_inventaire.py`), il n'a pas de lien `instance` à monter, et les dépôts de
|
|
# tenants qu'il porte sont marqués `role: tenant` précisément pour dire qu'il ne les
|
|
# pilote pas. Ce qu'il pilote, c'est la CARTE de l'hébergeur : `role: hebergeur` et
|
|
# `serveur_ops_underlay`.
|
|
#
|
|
# L'assertion n'admettait que la première forme. Le plan du site déclarait pourtant tout
|
|
# ce qu'il fallait, correctement et avec ses raisons écrites — et le rôle le refusait au
|
|
# nom d'une exigence qui ne le concernait pas.
|
|
#
|
|
# On exige donc que le runner pilote QUELQUE CHOSE, sans prescrire quoi.
|
|
- name: Exiger les intrants du poste d'exploitation
|
|
ansible.builtin.assert:
|
|
that:
|
|
- domaine_interne | default('') | length > 0
|
|
- (serveur_ops_depots | selectattr('role', 'eq', 'moteur') | list | length) == 1
|
|
- >-
|
|
((serveur_ops_depots | selectattr('role', 'eq', 'instance') | list | length) >= 1
|
|
and serveur_ops_instance | default('') | length > 0)
|
|
or
|
|
((serveur_ops_depots | selectattr('role', 'eq', 'hebergeur') | list | length) >= 1
|
|
and serveur_ops_underlay | default('') | length > 0)
|
|
- not (serveur_ops_forge_externe | bool) or (serveur_ops_forge_amont | length > 0)
|
|
fail_msg: >-
|
|
serveur_ops exige `domaine_interne`, exactement un dépôt `role: moteur`, et de
|
|
savoir sur quoi il travaille — SOIT un dépôt `role: instance` avec
|
|
`serveur_ops_instance` (runner de TENANT : il pilote un écosystème), SOIT un dépôt
|
|
`role: hebergeur` avec `serveur_ops_underlay` (runner de SITE : il matérialise le
|
|
terrain, et son inventaire est dynamique). Et si `serveur_ops_forge_externe` est
|
|
levé — écosystème au premier âge, qui lit son génome chez son hôte —
|
|
`serveur_ops_forge_amont` doit nommer cette forge.
|
|
|
|
- name: Installer l'outillage du poste
|
|
ansible.builtin.apt:
|
|
name: "{{ serveur_ops_paquets }}"
|
|
state: present
|
|
update_cache: true
|
|
when: not ansible_check_mode
|
|
|
|
- name: Créer l'utilisateur d'exploitation
|
|
ansible.builtin.user:
|
|
name: "{{ serveur_ops_utilisateur }}"
|
|
home: "{{ serveur_ops_racine }}"
|
|
shell: /bin/bash
|
|
create_home: true
|
|
system: false
|
|
|
|
- name: Poser les droits de la racine d'exploitation
|
|
ansible.builtin.file:
|
|
path: "{{ serveur_ops_racine }}"
|
|
state: directory
|
|
owner: "{{ serveur_ops_utilisateur }}"
|
|
group: "{{ serveur_ops_utilisateur }}"
|
|
mode: "0750"
|
|
|
|
# L'ENVIRONNEMENT PYTHON EST ISOLÉ, PAS SYSTÈME. Debian gère ansible en paquet, mais la
|
|
# version qu'il propose suit son propre calendrier : reconstruire un écosystème avec un
|
|
# ansible plus récent que celui qui l'a construit, c'est changer la recette sans le dire.
|
|
# Le venv permet d'épingler exactement la famille utilisée par le mainteneur.
|
|
- name: Créer l'environnement Python isolé
|
|
ansible.builtin.command:
|
|
cmd: "python3 -m venv {{ serveur_ops_venv }}"
|
|
creates: "{{ serveur_ops_venv }}/bin/python"
|
|
become: true
|
|
become_user: "{{ serveur_ops_utilisateur }}"
|
|
|
|
# LES BIBLIOTHEQUES DU CONTROLEUR SE DECLARENT, ELLES AUSSI (2026-08-27). Cette liste
|
|
# etait ecrite en dur — `ansible-core` et `pyyaml`, rien de plus — alors que les modules
|
|
# Proxmox du moteur exigent `proxmoxer` sur la machine qui les execute. Le runner du SITE
|
|
# a donc echoue a sa premiere materialisation de VM, sur une bibliotheque que le poste du
|
|
# mainteneur possedait par son paquet systeme sans que personne ne l'ait declaree.
|
|
# `requirements-python.txt` est desormais la source, au meme titre que `requirements.yml`
|
|
# pour les collections. Voir la preuve P51.
|
|
- name: Deposer la liste des bibliotheques Python du controleur
|
|
ansible.builtin.copy:
|
|
src: "{{ playbook_dir }}/../../requirements-python.txt"
|
|
dest: "{{ serveur_ops_racine }}/requirements-python.txt"
|
|
owner: "{{ serveur_ops_utilisateur }}"
|
|
group: "{{ serveur_ops_utilisateur }}"
|
|
mode: "0644"
|
|
|
|
# --- LA ROUE HORS-LIGNE ------------------------------------------------------
|
|
#
|
|
# MEME IDIOME QUE LES COLLECTIONS, quelques lignes plus bas : le CONTROLEUR telecharge
|
|
# une fois dans son cache, puis pousse par SSH. La cible n'ouvre aucun flux, et
|
|
# l'artefact devient deployable hors ligne.
|
|
|
|
- name: Preparer le cache des roues Python sur le controleur
|
|
ansible.builtin.file:
|
|
path: "{{ serveur_ops_cache_roues }}"
|
|
state: directory
|
|
mode: "0755"
|
|
delegate_to: localhost
|
|
become: false
|
|
run_once: true
|
|
|
|
# LE CONTROLEUR ET LA CIBLE DOIVENT PARTAGER LEUR PYTHON, et ca ne se devine pas.
|
|
#
|
|
# `pip download` rend les roues du systeme qui telecharge. `ansible-core`, `proxmoxer` et
|
|
# `requests` sont universelles (`py3-none-any`), mais `pyyaml` porte du C compile : une
|
|
# roue `cp313` posee sur un python 3.12 ne s'installe pas. L'echec serait « No matching
|
|
# distribution found » a l'installation — un message qui accuse le depot local alors que
|
|
# le coupable est l'ecart entre deux machines. On le mesure ici, ou on peut encore le dire.
|
|
- name: Lire le Python du controleur
|
|
ansible.builtin.command:
|
|
argv: ["python3", "-c", "import sys; print('%d.%d' % sys.version_info[:2])"]
|
|
delegate_to: localhost
|
|
become: false
|
|
run_once: true
|
|
changed_when: false
|
|
register: serveur_ops_python_controleur
|
|
|
|
- name: Exiger que le controleur et la cible partagent leur version de Python
|
|
ansible.builtin.assert:
|
|
that:
|
|
- serveur_ops_python_controleur.stdout | trim
|
|
== ansible_python_version.split('.')[0:2] | join('.')
|
|
fail_msg: >-
|
|
Le controleur tourne Python {{ serveur_ops_python_controleur.stdout | trim }} et
|
|
{{ inventory_hostname }} tourne Python
|
|
{{ ansible_python_version.split('.')[0:2] | join('.') }}. Les roues telechargees ne
|
|
s'installeraient pas sur la cible, et l'echec accuserait le depot local au lieu de
|
|
cet ecart. Materialiser depuis un controleur de meme famille — pendant une
|
|
insemination, c'est le runner du SITE, qui porte le meme Debian que la machine
|
|
qu'il amorce.
|
|
|
|
# `argv` ET NON `cmd` : le chemin du controleur peut contenir une ESPACE (« Espace
|
|
# Chezlepro/… » chez le mainteneur). Meme piege que pour ansible-galaxy le 2026-08-23.
|
|
- name: Telecharger les roues sur le controleur (une seule fois par empreinte)
|
|
ansible.builtin.command:
|
|
argv:
|
|
- "python3"
|
|
- "-m"
|
|
- "pip"
|
|
- "download"
|
|
- "--dest"
|
|
- "{{ serveur_ops_cache_roues }}"
|
|
- "--requirement"
|
|
- "{{ serveur_ops_roues_source }}"
|
|
- "{{ serveur_ops_ansible }}"
|
|
- "pyyaml"
|
|
delegate_to: localhost
|
|
become: false
|
|
run_once: true
|
|
register: serveur_ops_roues_telechargees
|
|
changed_when: "'Saved' in serveur_ops_roues_telechargees.stdout"
|
|
# LE GARDE-FOU SUIT LE RESULTAT, PAS LE GESTE (lecon du 2026-08-27) : on saute quand le
|
|
# cache PORTE deja des roues, pas quand une commande a deja tourne un jour.
|
|
when: not ansible_check_mode
|
|
|
|
- name: Deposer les roues sur le runner
|
|
ansible.builtin.copy:
|
|
src: "{{ serveur_ops_cache_roues }}/"
|
|
dest: "{{ serveur_ops_roues_depot }}/"
|
|
owner: "{{ serveur_ops_utilisateur }}"
|
|
group: "{{ serveur_ops_utilisateur }}"
|
|
mode: "0644"
|
|
directory_mode: "0755"
|
|
when: not ansible_check_mode
|
|
|
|
# `--no-index` EST LE POINT, PAS UNE OPTIMISATION. Sans lui, pip resterait capable de
|
|
# sortir vers PyPI le jour ou le depot local serait incomplet — et la reussite dependrait
|
|
# alors d'un flux que ce tenant n'a pas le droit d'avoir. Un repli silencieux vers
|
|
# l'Internet transformerait une lignee autonome en lignee qui a l'air autonome.
|
|
- name: Installer Ansible dans l'environnement isolé (hors ligne)
|
|
ansible.builtin.pip:
|
|
name:
|
|
- "{{ serveur_ops_ansible }}"
|
|
- "pyyaml"
|
|
virtualenv: "{{ serveur_ops_venv }}"
|
|
state: present
|
|
extra_args: "--no-index --find-links {{ serveur_ops_roues_depot }}"
|
|
become: true
|
|
become_user: "{{ serveur_ops_utilisateur }}"
|
|
when: not ansible_check_mode
|
|
register: serveur_ops_pip
|
|
retries: 3
|
|
delay: 6
|
|
until: serveur_ops_pip is succeeded
|
|
|
|
# TACHE SEPAREE : dans `ansible.builtin.pip`, `name` et `requirements` sont MUTUELLEMENT
|
|
# EXCLUSIFS. Les fusionner ne produit pas une erreur parlante — le module ignore l'un des
|
|
# deux, et la bibliotheque manquante ne se revele qu'au premier module qui en depend.
|
|
- name: Installer les bibliotheques Python du controleur (source declaree, hors ligne)
|
|
ansible.builtin.pip:
|
|
requirements: "{{ serveur_ops_racine }}/requirements-python.txt"
|
|
virtualenv: "{{ serveur_ops_venv }}"
|
|
state: present
|
|
extra_args: "--no-index --find-links {{ serveur_ops_roues_depot }}"
|
|
become: true
|
|
become_user: "{{ serveur_ops_utilisateur }}"
|
|
when: not ansible_check_mode
|
|
register: serveur_ops_pip_controleur
|
|
retries: 3
|
|
delay: 6
|
|
until: serveur_ops_pip_controleur is succeeded
|
|
|
|
# L'OUTILLAGE DOIT ETRE SUR LE CHEMIN DE CELUI QUI L'EXPLOITE (2026-08-26).
|
|
#
|
|
# Ansible vit dans un venv isole — c'est voulu, il est epingle. Mais rien ne le mettait sur
|
|
# le PATH du compte d'exploitation : `make instancier` y echouait sur
|
|
# « [Errno 2] No such file or directory: 'ansible-inventory' », un message qui accuse un
|
|
# fichier manquant alors que le fichier est la, deux dossiers plus loin.
|
|
#
|
|
# Mesure du 2026-08-26, en eprouvant la sequence depuis le runner du site. Un exploitant y
|
|
# serait tombe exactement de la meme facon — et c'est precisement ce qu'un outil « utilisable
|
|
# sans IA » ne doit pas faire.
|
|
#
|
|
# Un fragment `profile.d` plutot qu'un `.bashrc` : il vaut pour toute session du compte, y
|
|
# compris `sudo -iu setops`, et se retire d'un seul geste.
|
|
- name: Mettre l'outillage du poste sur le chemin du compte d'exploitation
|
|
ansible.builtin.copy:
|
|
dest: /etc/profile.d/50-setops-venv.sh
|
|
content: |
|
|
# GÉNÉRÉ par Set-OPS (rôle serveur_ops). NE PAS éditer à la main.
|
|
# Ansible vit dans un venv épinglé ; sans cette ligne, `make` ne le trouve pas.
|
|
if [ "$(id -un)" = "{{ serveur_ops_utilisateur }}" ]; then
|
|
PATH="{{ serveur_ops_venv }}/bin:$PATH"
|
|
export PATH
|
|
fi
|
|
owner: root
|
|
group: root
|
|
mode: "0644"
|
|
|
|
# LA CONFIANCE, AVANT LE CLONE — et limitee a une seule url.
|
|
#
|
|
# Voir defaults/main.yml : on ne pose pas cette racine dans le magasin systeme. `git
|
|
# config http.<url>.sslCAInfo` ne l'applique qu'aux echanges avec CETTE forge.
|
|
- name: Deposer la racine de la forge amont
|
|
ansible.builtin.copy:
|
|
src: "{{ serveur_ops_forge_amont_ac }}"
|
|
dest: "{{ serveur_ops_forge_amont_ac_depot }}"
|
|
owner: "{{ serveur_ops_utilisateur }}"
|
|
group: "{{ serveur_ops_utilisateur }}"
|
|
mode: "0644"
|
|
when:
|
|
- serveur_ops_forge_amont_ac | length > 0
|
|
- not ansible_check_mode
|
|
|
|
- name: Limiter la confiance a la forge amont, et a elle seule
|
|
community.general.git_config:
|
|
name: "http.{{ serveur_ops_forge_url }}/.sslCAInfo"
|
|
scope: global
|
|
value: "{{ serveur_ops_forge_amont_ac_depot }}"
|
|
become: true
|
|
become_user: "{{ serveur_ops_utilisateur }}"
|
|
when:
|
|
- serveur_ops_forge_amont_ac | length > 0
|
|
- not ansible_check_mode
|
|
|
|
# --- LE GÉNOME, DEPUIS SA PROPRE FORGE ---------------------------------------
|
|
#
|
|
# Cloné en anonyme : les dépôts du génome sont lisibles sur la forge de l'écosystème, et
|
|
# le poste n'a donc AUCUN justificatif à détenir pour se reconstruire. C'est délibéré —
|
|
# un secret de moins sur la machine qui en concentre déjà beaucoup.
|
|
#
|
|
# La confiance TLS vient de `client_pki` quand la forge est celle de CET écosystème
|
|
# (racine step-ca dans le magasin système). Quand elle est celle d'un voisin, elle vient
|
|
# de la tâche ci-dessus — limitée à cette seule url. Sans l'une ou l'autre, `git clone`
|
|
# refuse le certificat, et c'est bien qu'il refuse.
|
|
- name: Cloner le génome depuis la forge de l'écosystème
|
|
ansible.builtin.git:
|
|
repo: "{{ serveur_ops_forge_url }}/{{ serveur_ops_forge_organisation }}/{{ item.depot }}.git"
|
|
dest: "{{ serveur_ops_racine }}/{{ item.dest }}"
|
|
version: "{{ item.branche | default('main') }}"
|
|
update: true
|
|
force: false
|
|
loop: "{{ serveur_ops_depots }}"
|
|
loop_control:
|
|
label: "{{ item.depot }} → {{ item.dest }}"
|
|
become: true
|
|
become_user: "{{ serveur_ops_utilisateur }}"
|
|
when: not ansible_check_mode
|
|
register: serveur_ops_clone
|
|
retries: 3
|
|
delay: 6
|
|
until: serveur_ops_clone is succeeded
|
|
|
|
# --- LES COLLECTIONS, SANS DEPENDRE DE GALAXY ---------------------------------
|
|
#
|
|
# UN POSTE D'EXPLOITATION QUI APPELLE galaxy.ansible.com POUR SE CONSTRUIRE N'EST PAS
|
|
# SOUVERAIN. Mesure du 2026-08-23, premier deploiement : `ansible-galaxy collection
|
|
# install` a echoue depuis l'overlay de patient 0 —
|
|
# `SSL: UNEXPECTED_EOF_WHILE_READING` — parce que la frontiere ne laisse pas passer ce
|
|
# flux, et c'est tres bien ainsi. Ouvrir une regle vers un serveur americain pour que
|
|
# l'ecosysteme sache se reconstruire aurait ete la mauvaise reponse.
|
|
#
|
|
# MEME IDIOME QUE serveur_forgejo DEVANT SON PROPRE TIERS : le CONTROLEUR telecharge une
|
|
# fois, dans son cache, puis pousse par SSH. Aucun flux nouveau depuis la cible, et
|
|
# l'artefact devient deployable hors ligne — un poste peut se reconstruire sans internet
|
|
# du tout, ce qui est le sens meme d'une lignee autonome.
|
|
- name: Preparer le cache des collections sur le controleur
|
|
ansible.builtin.file:
|
|
path: "{{ serveur_ops_cache_collections }}"
|
|
state: directory
|
|
mode: "0755"
|
|
delegate_to: localhost
|
|
become: false
|
|
run_once: true
|
|
|
|
# `argv` ET NON `cmd` : le chemin du controleur peut contenir une ESPACE (« Espace
|
|
# Chezlepro/… » chez le mainteneur), et `cmd` la lit comme un separateur d'arguments.
|
|
# Constate le 2026-08-23 : ansible-galaxy recevait « /home/…/Espace » puis
|
|
# « Chezlepro/… » comme deux arguments, et refusait — « positional collection_name arg
|
|
# and --requirements-file are mutually exclusive ». Le message ne parlait pas du tout du
|
|
# vrai probleme.
|
|
- name: Telecharger les collections dans le cache du controleur (une seule fois)
|
|
ansible.builtin.command:
|
|
argv:
|
|
- "ansible-galaxy"
|
|
- "collection"
|
|
- "download"
|
|
- "-r"
|
|
- "{{ serveur_ops_requirements_controleur }}"
|
|
- "-p"
|
|
- "{{ serveur_ops_cache_collections }}"
|
|
creates: "{{ serveur_ops_cache_collections }}/requirements.yml"
|
|
delegate_to: localhost
|
|
become: false
|
|
run_once: true
|
|
when: not ansible_check_mode
|
|
|
|
- name: Deposer les collections sur le poste
|
|
register: serveur_ops_depot_collections
|
|
ansible.builtin.copy:
|
|
src: "{{ serveur_ops_cache_collections }}/"
|
|
dest: "{{ serveur_ops_racine }}/.collections-hors-ligne/"
|
|
owner: "{{ serveur_ops_utilisateur }}"
|
|
group: "{{ serveur_ops_utilisateur }}"
|
|
mode: "0644"
|
|
directory_mode: "0755"
|
|
when: not ansible_check_mode
|
|
|
|
# `chdir` OBLIGATOIRE : le `requirements.yml` produit par `collection download` nomme les
|
|
# archives en RELATIF (`community-postgresql-4.2.0.tar.gz`), et ansible-galaxy les cherche
|
|
# depuis le repertoire COURANT, pas depuis celui du fichier. Sans chdir : « Could not find
|
|
# community-postgresql-4.2.0.tar.gz » alors que l'archive est bien la, a cote.
|
|
# LE GARDE-FOU DOIT SUIVRE LE RESULTAT, PAS LE GESTE (2026-08-27). La condition
|
|
# d'installation ne regardait que le DEPOT des archives. Le jour ou l'on a corrige le
|
|
# CHEMIN d'installation, les archives etaient inchangees : le role a donc conclu qu'il
|
|
# n'y avait rien a faire, et les collections sont restees a l'ancien endroit. Le meme
|
|
# piege qu'en 2026-08-24, deplace d'un cran. On mesure desormais la DESTINATION.
|
|
- name: Voir si les collections sont deja la ou le moteur les cherche
|
|
ansible.builtin.stat:
|
|
path: >-
|
|
{{ serveur_ops_racine }}/{{ (serveur_ops_depots | selectattr('role', 'eq', 'moteur')
|
|
| first).dest }}/.ansible/collections/ansible_collections/community/general/MANIFEST.json
|
|
register: serveur_ops_collections_en_place
|
|
become: true
|
|
become_user: "{{ serveur_ops_utilisateur }}"
|
|
|
|
- name: Installer les collections depuis le depot local (hors ligne)
|
|
ansible.builtin.command:
|
|
cmd: >-
|
|
{{ serveur_ops_venv }}/bin/ansible-galaxy collection install
|
|
-r requirements.yml --force
|
|
-p {{ serveur_ops_racine }}/{{ (serveur_ops_depots | selectattr('role', 'eq', 'moteur')
|
|
| first).dest }}/.ansible/collections
|
|
chdir: "{{ serveur_ops_racine }}/.collections-hors-ligne"
|
|
# INSTALLER LA OU LE MOTEUR REGARDE (2026-08-27). Le `Makefile` pose
|
|
# `export ANSIBLE_HOME ?= $(CURDIR)/.ansible` : sous `make`, Ansible ne cherche donc ses
|
|
# collections QUE dans `<moteur>/.ansible/collections`. Ce role les deposait dans
|
|
# `<racine>/.ansible/collections` — a cote, jamais lu. Le runner du SITE echouait ainsi
|
|
# sur `couldn't resolve module/action community.general.proxmox_pool` alors que la
|
|
# collection etait bel et bien installee, complete, et visible par `ansible-doc`.
|
|
#
|
|
# POURQUOI LE MAINTENEUR NE VOYAIT RIEN : sur son poste, les collections viennent du
|
|
# paquet systeme (`/usr/lib/python3/dist-packages/ansible_collections`), toujours dans
|
|
# le chemin de recherche quel que soit `ANSIBLE_HOME`. Le reglage du Makefile n'y a
|
|
# jamais eu de consequence. Sur un runner, ou rien n'est installe par la distribution,
|
|
# il decide de tout. Encore un ecart qu'on ne voit qu'en portant le moteur ailleurs.
|
|
#
|
|
# `--force` PARCE QU'UN POSTE PEUT ETRE EN AVANCE, PAS SEULEMENT EN RETARD (2026-08-27).
|
|
# Sans lui, `ansible-galaxy` laisse en place une collection deja installee dans une
|
|
# version SUPERIEURE a celle demandee : il ne retrograde pas. Le runner du SITE, monte
|
|
# avant que `requirements.yml` n'epingle ses versions, portait `community.general` 13.3.0
|
|
# — une version d'ou les modules Proxmox ont ete RETIRES. La premiere materialisation de
|
|
# VM depuis le runner a echoue la-dessus. Le depot doit faire autorite dans les DEUX
|
|
# sens : ce que `requirements.yml` declare est ce qui est installe, ni plus haut ni plus
|
|
# bas. Voir la preuve P51.
|
|
#
|
|
# LE GARDE-FOU SUIVAIT LA MAUVAISE CHOSE (2026-08-24). Il etait `creates: …/community/general` :
|
|
# une collection AJOUTEE a requirements.yml n'aurait jamais ete installee, puisque le
|
|
# repertoire temoin existait deja. Le runner serait reste en retard sur le moteur, en
|
|
# silence. On suit desormais le DEPOT des archives : si les collections deposees
|
|
# changent, on reinstalle.
|
|
changed_when: true
|
|
become: true
|
|
become_user: "{{ serveur_ops_utilisateur }}"
|
|
# DEUX `when` SUR UNE MEME TACHE, C'EST UN SEUL — LE DERNIER (2026-08-24). En ajoutant
|
|
# la condition sur le depot, j'ai laisse celle du check-mode juste en dessous : YAML
|
|
# garde la derniere cle et jette l'autre, en silence. `ansible-lint` l'a vu ; personne
|
|
# d'autre ne l'aurait vu. Les conditions vont donc dans UNE liste.
|
|
when:
|
|
- serveur_ops_depot_collections is changed
|
|
or not serveur_ops_collections_en_place.stat.exists
|
|
- not ansible_check_mode
|
|
|
|
# LES DEUX SYMLINKS (D-80). `instance` dit QUEL tenant on pilote. Le poste en pose un par
|
|
# défaut ; l'exploitant le bascule comme sur le poste du mainteneur.
|
|
#
|
|
# UN RUNNER DE SITE N'EN A PAS (2026-08-26). Il ne pilote aucun écosystème — il matérialise
|
|
# le terrain — et son propre inventaire est DYNAMIQUE (`site_inventaire.py`). Son plan
|
|
# déclare donc `serveur_ops_instance: ""`, et c'est une réponse, pas un oubli.
|
|
#
|
|
# Sans la garde ci-dessous, la chaîne rendait `{{ racine }}/` + `""` : le lien `instance`
|
|
# pointait sur `/opt/setops/` lui-même — un répertoire qui n'est l'écosystème de personne.
|
|
# Mesuré le 2026-08-26 : `instance -> /opt/setops/`. Un lien qui existe et ne désigne rien
|
|
# est pire qu'un lien absent : tout ce qui le suit croit avoir trouvé une instance.
|
|
- name: Désigner l'instance pilotée par défaut
|
|
ansible.builtin.file:
|
|
src: "{{ serveur_ops_racine }}/{{ serveur_ops_instance }}"
|
|
dest: >-
|
|
{{ serveur_ops_racine }}/{{ (serveur_ops_depots | selectattr('role', 'eq', 'moteur') | first).dest }}/instance
|
|
state: link
|
|
owner: "{{ serveur_ops_utilisateur }}"
|
|
group: "{{ serveur_ops_utilisateur }}"
|
|
force: true
|
|
when:
|
|
- serveur_ops_instance | default('') | trim | length > 0
|
|
- not ansible_check_mode
|
|
|
|
# Et s'il n'y en a pas, le lien ne doit pas SURVIVRE d'un passage precedent : un runner
|
|
# qu'on reconfigure en runner de site garderait sinon un lien perime vers un tenant.
|
|
- name: Retirer le lien `instance` quand le poste ne pilote aucun écosystème
|
|
ansible.builtin.file:
|
|
path: >-
|
|
{{ serveur_ops_racine }}/{{ (serveur_ops_depots | selectattr('role', 'eq', 'moteur') | first).dest }}/instance
|
|
state: absent
|
|
when:
|
|
- serveur_ops_instance | default('') | trim | length == 0
|
|
- not ansible_check_mode
|
|
|
|
# LE SECOND SYMLINK. Sans lui, le poste sait configurer des machines qui existent ; il ne
|
|
# sait pas en creer, faute de savoir sur quelle fabric les poser. C'est la difference
|
|
# entre exploiter et engendrer.
|
|
- name: Désigner la fabric sur laquelle l'écosystème repose
|
|
ansible.builtin.file:
|
|
src: "{{ serveur_ops_racine }}/{{ serveur_ops_underlay }}/underlay.yml"
|
|
dest: >-
|
|
{{ serveur_ops_racine }}/{{ (serveur_ops_depots | selectattr('role', 'eq', 'moteur') | first).dest }}/underlay.yml
|
|
state: link
|
|
owner: "{{ serveur_ops_utilisateur }}"
|
|
group: "{{ serveur_ops_utilisateur }}"
|
|
force: true
|
|
when:
|
|
- serveur_ops_underlay | length > 0
|
|
- not ansible_check_mode
|
|
|
|
# --- LA CLÉ DU POSTE ---------------------------------------------------------
|
|
#
|
|
# Le poste se fabrique sa propre paire, distincte de celle du mainteneur. Deux
|
|
# exploitants, deux clés : on peut révoquer l'un sans couper l'autre — et l'accès du
|
|
# poste se lit dans les `authorized_keys` de la flotte, au lieu de se confondre avec
|
|
# celui d'un humain.
|
|
#
|
|
# `ssh-keygen` plutôt que `community.crypto.openssh_keypair` : le dépôt ne requiert que
|
|
# `community.postgresql` et `community.general`, et fabriquer une paire de clés ne
|
|
# justifie pas d'imposer une collection de plus à tout écosystème qui déploie un poste.
|
|
- name: Assurer le répertoire SSH du poste
|
|
ansible.builtin.file:
|
|
path: "{{ serveur_ops_racine }}/.ssh"
|
|
state: directory
|
|
owner: "{{ serveur_ops_utilisateur }}"
|
|
group: "{{ serveur_ops_utilisateur }}"
|
|
mode: "0700"
|
|
|
|
- name: Fabriquer la clé SSH du poste
|
|
ansible.builtin.command:
|
|
cmd: >-
|
|
ssh-keygen -t {{ serveur_ops_cle_type }} -N ''
|
|
-C "setops@{{ inventory_hostname }}.{{ domaine_interne }}"
|
|
-f {{ serveur_ops_racine }}/.ssh/id_{{ serveur_ops_cle_type }}
|
|
creates: "{{ serveur_ops_racine }}/.ssh/id_{{ serveur_ops_cle_type }}"
|
|
become: true
|
|
become_user: "{{ serveur_ops_utilisateur }}"
|
|
when:
|
|
- serveur_ops_cle_generer | bool
|
|
- not ansible_check_mode
|
|
register: serveur_ops_cle
|
|
|
|
- name: Relire la clé publique du poste
|
|
ansible.builtin.slurp:
|
|
src: "{{ serveur_ops_racine }}/.ssh/id_{{ serveur_ops_cle_type }}.pub"
|
|
register: serveur_ops_pub
|
|
when:
|
|
- serveur_ops_cle_generer | bool
|
|
- not ansible_check_mode
|
|
|
|
# ÉCRIRE, PUIS RELIRE (D-68) — et rendre le résultat EXPLOITABLE. Cette clé ne sert à
|
|
# rien tant qu'elle n'est pas autorisée sur la flotte : on l'affiche pour que l'exploitant
|
|
# la reprenne dans les intrants, au lieu de la laisser dormir sur le disque.
|
|
- name: La clé publique à autoriser sur la flotte
|
|
ansible.builtin.debug:
|
|
msg:
|
|
- "Clé publique du poste d'exploitation — à ajouter aux intrants SSH du plan :"
|
|
- "{{ serveur_ops_pub.content | b64decode | trim }}"
|
|
when:
|
|
- serveur_ops_cle_generer | bool
|
|
- not ansible_check_mode
|
|
|
|
- name: Déposer le mode d'emploi sur le poste
|
|
ansible.builtin.template:
|
|
src: LISEZ-MOI.md.j2
|
|
dest: "{{ serveur_ops_racine }}/LISEZ-MOI.md"
|
|
owner: "{{ serveur_ops_utilisateur }}"
|
|
group: "{{ serveur_ops_utilisateur }}"
|
|
mode: "0644"
|