runner : roues et collections prises au site, plus a PyPI ni a Galaxy
verrou-runner.yml fige les 15 roues et les 3 collections qui tournent sur le runner de Technolibre, avec URL amont et somme SHA-256. Le cache du runner tenait deux versions de six bibliotheques selon le jour du montage. - serveur_artefacts depose ces 18 fichiers au depot de binaires du site. - serveur_ops les prend au site d'abord, somme verifiee ; repli sur Internet dit en clair ; plus aucun pip download ni collection download ; seuls les fichiers du verrou sont deposes sur le runner. - ansible-core epingle a 2.18.19 (la version deployee). - P70 exige le verrou au site ; test_verrou_runner.py dans make test. AGENTS.md et CLAUDE.md : version allegee par l'operateur. 94 preuves. CHANGELOG (121). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
279e583701
commit
4ebca13058
11 changed files with 816 additions and 715 deletions
48
CHANGELOG.md
48
CHANGELOG.md
|
|
@ -1,5 +1,53 @@
|
|||
# CHANGELOG — Set-OPS
|
||||
|
||||
## 2026-10-09 (121) — Le runner prend ses roues et ses collections au site, plus à PyPI ni à Galaxy
|
||||
|
||||
**94 preuves.** Les deux dernières dépendances Internet d'une reconstruction, mesurées le
|
||||
2026-10-05, se ferment comme l'app store de Nextcloud (entrée (117)) : **le site fournit
|
||||
tous les artefacts**.
|
||||
|
||||
### Pourquoi un verrou
|
||||
|
||||
`serveur_ops` laissait `pip download` et `ansible-galaxy collection download` résoudre les
|
||||
versions au moment du montage. Mesuré sur le runner de Technolibre : le cache tenait **deux
|
||||
versions de six bibliothèques** (`cryptography` 50.0.1 et 50.0.2, `urllib3` 2.7.0 et
|
||||
2.8.0…) : PyPI avait bougé entre l'insémination et le montage.
|
||||
On ne peut pas déposer au site « ce que PyPI répondra demain ». D'où `verrou-runner.yml`, à
|
||||
la racine : les **15 roues** (Python 3.13) et les **3 collections** qui tournent aujourd'hui
|
||||
sur ce runner, chacune avec son URL amont et sa somme SHA-256. `ansible-core` passe de
|
||||
`>=2.18,<2.19` à `==2.18.19`, la version déjà déployée : aucun changement de version sur
|
||||
la flotte.
|
||||
|
||||
### Ce qui change
|
||||
|
||||
- **`serveur_artefacts`** lit le verrou et dépose les 18 fichiers au dépôt de binaires du
|
||||
site (`/setops-binaires`), somme vérifiée au téléchargement.
|
||||
- **`serveur_ops`** les prend **au site d'abord**, somme vérifiée ; un fichier absent ou
|
||||
dont la somme diffère est pris sur Internet **en le disant** (« absentes du dépôt du
|
||||
site … : prises sur Internet »), et sa somme est encore vérifiée. Plus aucun
|
||||
`pip download` ni `collection download`. Seuls les fichiers du verrou sont déposés sur
|
||||
le runner ; les roues d'anciennes versions sont retirées. Les collections s'installent
|
||||
depuis un `requirements.yml` généré du verrou.
|
||||
- **P70** exige les fichiers du verrou au dépôt de binaires, et que la tâche du site lise
|
||||
le verrou. **`test_verrou_runner.py`** (dans `make test`) : URL et somme bien formées, une
|
||||
seule version par bibliothèque, roues compilées pour le Python cible, épinglages de
|
||||
`requirements-python.txt` et de `serveur_ops_ansible` égaux au verrou, collections
|
||||
égales à `requirements.yml`, plus aucun téléchargement résolu dans `serveur_ops`.
|
||||
|
||||
### Témoins
|
||||
|
||||
Trois, chacun échoue comme attendu puis repasse une fois remis : l'ancien `serveur_ops`
|
||||
(« download » encore présent) ; un verrou à deux `urllib3` (« une seule version par
|
||||
bibliothèque … {'urllib3': ['2.7.0', '2.8.0']} ») ; P70 quand le site ne lit plus le
|
||||
verrou. Le deuxième a d'abord fait planter le test : une somme fictive de zéros est lue
|
||||
par YAML comme un entier ; le test convertit désormais en texte.
|
||||
|
||||
### Pas encore fait
|
||||
|
||||
Rien n'est déployé. Il reste : `serveur_artefacts` au site (simulé d'abord), vérifier les
|
||||
18 sommes servies depuis un runner de locataire, puis reconstruire les deux locataires sur
|
||||
ce commit pour prouver qu'aucun montage ne sort vers PyPI ni Galaxy.
|
||||
|
||||
## 2026-10-09 (120) — Chezlepro reconstruite sur `3afb756` : les deux locataires sur le même commit
|
||||
|
||||
**94 preuves.** `make reconstruire-locataire TENANT=OPS-Chezlepro` (44 min, journal
|
||||
|
|
|
|||
21
CLAUDE.md
21
CLAUDE.md
|
|
@ -4,24 +4,3 @@
|
|||
|
||||
`AGENTS.md` est la **source d'autorité unique** de ce dépôt. Claude Code doit le lire
|
||||
et le respecter intégralement avant toute modification.
|
||||
|
||||
En cas de contradiction entre `CLAUDE.md` et `AGENTS.md`, **suivre `AGENTS.md`**.
|
||||
|
||||
Ce fichier est volontairement mince : il ne redéclare pas la doctrine (template, SSH,
|
||||
pare-feu, cloud-init, handlers, Makefile, groupes…). Toute cette doctrine vit dans
|
||||
`AGENTS.md`, pour éviter la duplication et les dérives qu'elle provoque.
|
||||
|
||||
## Les cinq règles absolues
|
||||
|
||||
1. **Un seul agent IA** travaille dans ce dépôt à la fois (Codex *ou* Claude, jamais les deux).
|
||||
2. **`instance/inventories/*/hosts.yml` est un artefact GÉNÉRÉ** depuis le plan
|
||||
(`instance/plan/`). On l'édite jamais à la main : on édite le plan, puis
|
||||
`make instancier` → `make instancier-appliquer`.
|
||||
3. **Aucun secret en clair** (mots de passe, clés privées, tokens, certificats) — Vault
|
||||
ou stockage hors dépôt uniquement.
|
||||
4. **Toute action destructive ou risquée exige une confirmation explicite** (ex.
|
||||
`CONFIRMER=true`, `confirm_destructive_action: true`). Sans elle, refuser l'exécution.
|
||||
5. **Rien n'est « prêt » sans validation.** Au minimum `--syntax-check` du playbook
|
||||
touché, `ansible-lint` si disponible, et l'entrée `CHANGELOG.md` correspondante.
|
||||
|
||||
Le détail de chacune de ces règles, et tout le reste, est dans `AGENTS.md`.
|
||||
|
|
|
|||
1
Makefile
1
Makefile
|
|
@ -321,6 +321,7 @@ test: ## Lance les tests unitaires (derivation de nomenclature et d'inventaire)
|
|||
python3 scripts/tests/test_genome_colis.py
|
||||
python3 scripts/tests/test_verifier_depot.py
|
||||
python3 scripts/tests/test_apps_nextcloud.py
|
||||
python3 scripts/tests/test_verrou_runner.py
|
||||
python3 scripts/tests/test_adressage_derive.py
|
||||
python3 scripts/tests/test_gui_intrants.py
|
||||
python3 scripts/tests/test_runbooks.py
|
||||
|
|
|
|||
|
|
@ -19,9 +19,13 @@ quarante minutes sur la **première** machine.
|
|||
| | |
|
||||
|---|---|
|
||||
| **dépôts apt** | ce rôle — un cache, servi à la flotte |
|
||||
| **téléchargements directs** (Forgejo, Keycloak, Nextcloud, oauth2-proxy, collections) | le **cache du contrôleur** : il télécharge une fois et pousse par SSH |
|
||||
| **téléchargements directs** (Forgejo, Keycloak, Nextcloud et ses applications, oauth2-proxy) | ce rôle — le **dépôt de binaires** (`/var/lib/setops/artefacts-directs`, servi sous `/setops-binaires` sur le même port) |
|
||||
| **ce que le runner installe** (roues Python, collections Ansible, `verrou-runner.yml`) | ce rôle — le même dépôt de binaires |
|
||||
|
||||
Deux mécanismes parce que ce sont deux problèmes : on ne sert pas un dépôt apt par `scp`.
|
||||
Les rôles consommateurs prennent ces fichiers au dépôt du site d'abord, vérifient leur somme
|
||||
(ou leur signature), et ne sortent sur Internet qu'en repli, en le disant. Depuis le
|
||||
2026-10-09, une reconstruction ne demande plus rien à PyPI, Galaxy ni à l'app store de
|
||||
Nextcloud quand le dépôt est rempli.
|
||||
|
||||
## Ce qu'il ne couvre pas encore
|
||||
|
||||
|
|
|
|||
|
|
@ -199,6 +199,14 @@
|
|||
label: "{{ item }}"
|
||||
when: serveur_artefacts_directs_actif | bool
|
||||
|
||||
# CE QUE LE RUNNER INSTALLE (2026-10-09) : ses roues Python et ses collections, verrouillees
|
||||
# a l'octet dans `verrou-runner.yml`, a la racine du moteur. Lues, pas recopiees.
|
||||
- name: Lire le verrou du runner (roues Python, collections Ansible)
|
||||
ansible.builtin.include_vars:
|
||||
file: "{{ role_path }}/../../verrou-runner.yml"
|
||||
name: serveur_artefacts_verrou_runner
|
||||
when: serveur_artefacts_directs_actif | bool
|
||||
|
||||
# `force: false` ET UN `dest` NOMME PAR LA VERSION : une publication ne change pas sous
|
||||
# le meme nom, donc un fichier deja present est le bon. Une nouvelle version porte un
|
||||
# autre nom et se telecharge d'elle-meme — le depot se met a jour en suivant les roles,
|
||||
|
|
@ -211,12 +219,18 @@
|
|||
ansible.builtin.get_url:
|
||||
url: "{{ item.url }}"
|
||||
dest: "{{ serveur_artefacts_directs_dir }}/{{ item.nom }}"
|
||||
checksum: "{{ ('sha256:' ~ item.sha256) if item.sha256 is defined else omit }}"
|
||||
mode: "0644"
|
||||
force: false
|
||||
timeout: 60
|
||||
# Les applications de Nextcloud (2026-10-08) : declarees par `serveur_nextcloud`, lues
|
||||
# telles quelles — meme `nom`, meme `url`, rien de recopie ici.
|
||||
loop: "{{ serveur_artefacts_directs + (serveur_nextcloud_apps | default([])) }}"
|
||||
# Et le verrou du runner (2026-10-09). Un artefact qui declare sa somme est verifie ici :
|
||||
# le site ne tient pas un fichier altere en chemin.
|
||||
loop: >-
|
||||
{{ serveur_artefacts_directs + (serveur_nextcloud_apps | default([]))
|
||||
+ (serveur_artefacts_verrou_runner.python | default([]))
|
||||
+ (serveur_artefacts_verrou_runner.collections | default([])) }}
|
||||
loop_control:
|
||||
label: "{{ item.nom }}"
|
||||
register: serveur_artefacts_directs_tires
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ serveur_ops_paquets:
|
|||
# Ansible est ÉPINGLÉ sur la même famille que le poste du mainteneur (core 2.18) : un
|
||||
# écosystème qui se reconstruit avec une version différente de celle qui l'a construit ne
|
||||
# reproduit pas la même chose, et l'écart ne se voit qu'au premier échec.
|
||||
serveur_ops_ansible: "ansible-core>=2.18,<2.19"
|
||||
serveur_ops_ansible: "ansible-core==2.18.19"
|
||||
|
||||
# --- D'OÙ VIENT LE GÉNOME ----------------------------------------------------
|
||||
#
|
||||
|
|
|
|||
|
|
@ -108,80 +108,124 @@
|
|||
become: false
|
||||
run_once: true
|
||||
|
||||
# LE CONTROLEUR ET LA CIBLE DOIVENT PARTAGER LEUR PYTHON, et ca ne se devine pas.
|
||||
# LE VERROU DU RUNNER (2026-10-09) — `verrou-runner.yml`, a la racine du moteur.
|
||||
#
|
||||
# `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
|
||||
# LIRE UNE VERSION N'EST PAS CHANGER, ET `--check` RENDAIT LA GARDE MUETTE (2026-09-14).
|
||||
#
|
||||
# Sans `check_mode: false`, cette commande est sautee en mode idempotent : la garde qui
|
||||
# suit compare alors contre du VIDE et refuse.
|
||||
#
|
||||
# Le controleur tourne Python et site-ops-01 tourne Python 3.13
|
||||
#
|
||||
# L'espace apres « Python » est tout le diagnostic — il n'y avait rien a comparer. Un
|
||||
# essai a blanc rendait donc un echec sur le runner, alors que le deploiement reel passe.
|
||||
check_mode: false
|
||||
register: serveur_ops_python_controleur
|
||||
# Les roues ne sont plus resolues par `pip download` contre PyPI, a chaque passage : elles
|
||||
# sont ECRITES, une par une, avec leur somme, et le SITE les tient dans son depot de
|
||||
# binaires. Le runner de Technolibre portait deux versions de six bibliotheques — PyPI avait
|
||||
# bouge entre l'insemination et le montage. Desormais, ce qui s'installe est ce que le
|
||||
# verrou nomme, et rien d'autre.
|
||||
- name: Lire le verrou du runner (roues Python, collections Ansible)
|
||||
ansible.builtin.include_vars:
|
||||
file: "{{ playbook_dir }}/../../verrou-runner.yml"
|
||||
name: serveur_ops_verrou
|
||||
|
||||
- name: Exiger que le controleur et la cible partagent leur version de Python
|
||||
# LA CIBLE DOIT PORTER LE PYTHON DES ROUES VERROUILLEES. `pyyaml`, `cffi`, `markupsafe`...
|
||||
# portent du C compile pour un Python donne : une roue `cp313` posee sur un 3.12 ne
|
||||
# s'installe pas, et l'echec accuserait le depot local. Le controleur, lui, ne compile plus
|
||||
# rien : il ne fait que transporter des fichiers ecrits.
|
||||
- name: Exiger que la cible porte le Python des roues verrouillees
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- serveur_ops_python_controleur.stdout | trim
|
||||
== ansible_python_version.split('.')[0:2] | join('.')
|
||||
- ansible_python_version.split('.')[0:2] | join('.') == serveur_ops_verrou.python_cible | string
|
||||
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.
|
||||
{{ inventory_hostname }} tourne Python {{ ansible_python_version }} ; les roues de
|
||||
`verrou-runner.yml` sont faites pour Python {{ serveur_ops_verrou.python_cible }}.
|
||||
Monter le verrou (nouvelles roues, nouvelles sommes), ou garder la cible sur ce Python.
|
||||
|
||||
# `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"
|
||||
# LE DEPOT DU SITE D'ABORD, L'AMONT ENSUITE — la somme verifiee dans les deux cas.
|
||||
# `failed_when: false` : le depot est une COMMODITE ; sans lui, on retombe sur l'amont, et on
|
||||
# le dit (tache suivante).
|
||||
- name: Prendre les roues verrouillees au depot du site
|
||||
ansible.builtin.get_url:
|
||||
url: "{{ setops_depot_binaires }}/{{ item.nom }}"
|
||||
dest: "{{ serveur_ops_cache_roues }}/{{ item.nom }}"
|
||||
checksum: "sha256:{{ item.sha256 }}"
|
||||
mode: "0644"
|
||||
timeout: 30
|
||||
loop: "{{ serveur_ops_verrou.python }}"
|
||||
loop_control:
|
||||
label: "{{ item.nom }}"
|
||||
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
|
||||
failed_when: false
|
||||
when:
|
||||
- setops_depot_binaires | default('') | length > 0
|
||||
- not ansible_check_mode
|
||||
|
||||
- name: Deposer les roues sur le runner
|
||||
- name: Les roues verrouillees sont-elles dans le cache du controleur ?
|
||||
ansible.builtin.stat:
|
||||
path: "{{ serveur_ops_cache_roues }}/{{ item.nom }}"
|
||||
checksum_algorithm: sha256
|
||||
loop: "{{ serveur_ops_verrou.python }}"
|
||||
loop_control:
|
||||
label: "{{ item.nom }}"
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
run_once: true
|
||||
register: serveur_ops_roues_cache
|
||||
|
||||
# ON LE DIT, FORT : une reconstruction qui sort sur Internet doit se voir.
|
||||
- name: Dire les roues absentes du depot du site (prises en amont)
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
{{ serveur_ops_roues_cache.results | rejectattr('stat.exists') | map(attribute='item.nom') | list }}
|
||||
— absentes du depot du site ({{ setops_depot_binaires | default('aucun depot declare') }}) : prises sur Internet.
|
||||
run_once: true
|
||||
when: serveur_ops_roues_cache.results | rejectattr('stat.exists') | list | length > 0
|
||||
|
||||
- name: Prendre en amont les roues que le site n'a pas (repli)
|
||||
ansible.builtin.get_url:
|
||||
url: "{{ item.item.url }}"
|
||||
dest: "{{ serveur_ops_cache_roues }}/{{ item.item.nom }}"
|
||||
checksum: "sha256:{{ item.item.sha256 }}"
|
||||
mode: "0644"
|
||||
timeout: 60
|
||||
# ABSENTE OU ALTEREE : `pip --no-index` ne verifie pas les sommes, un fichier present mais
|
||||
# different passerait. On le reprend, et `get_url` le verifie.
|
||||
loop: "{{ serveur_ops_roues_cache.results }}"
|
||||
loop_control:
|
||||
label: "{{ item.item.nom }}"
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
run_once: true
|
||||
when:
|
||||
- not ansible_check_mode
|
||||
- not item.stat.exists or item.stat.checksum != item.item.sha256
|
||||
|
||||
# LES ROUES DU VERROU, ET ELLES SEULES. Copier le cache entier deposait aussi les versions
|
||||
# d'hier (deux `cryptography`, deux `urllib3`...) ; `--no-index` choisissait alors la plus
|
||||
# haute, que personne n'avait choisie.
|
||||
- name: Deposer sur le runner les roues du verrou
|
||||
ansible.builtin.copy:
|
||||
src: "{{ serveur_ops_cache_roues }}/"
|
||||
dest: "{{ serveur_ops_roues_depot }}/"
|
||||
src: "{{ serveur_ops_cache_roues }}/{{ item.nom }}"
|
||||
dest: "{{ serveur_ops_roues_depot }}/{{ item.nom }}"
|
||||
owner: "{{ serveur_ops_utilisateur }}"
|
||||
group: "{{ serveur_ops_utilisateur }}"
|
||||
mode: "0644"
|
||||
directory_mode: "0755"
|
||||
loop: "{{ serveur_ops_verrou.python }}"
|
||||
loop_control:
|
||||
label: "{{ item.nom }}"
|
||||
when: not ansible_check_mode
|
||||
|
||||
- name: Relever les roues deposees sur le runner
|
||||
ansible.builtin.find:
|
||||
paths: "{{ serveur_ops_roues_depot }}"
|
||||
patterns: "*.whl"
|
||||
register: serveur_ops_roues_deposees
|
||||
|
||||
- name: Retirer du runner les roues que le verrou ne nomme pas
|
||||
ansible.builtin.file:
|
||||
path: "{{ item.path }}"
|
||||
state: absent
|
||||
loop: >-
|
||||
{{ serveur_ops_roues_deposees.files
|
||||
| rejectattr('path', 'search', '/(' ~ (serveur_ops_verrou.python | map(attribute='nom')
|
||||
| map('regex_escape') | join('|')) ~ ')$') | list }}
|
||||
loop_control:
|
||||
label: "{{ item.path | basename }}"
|
||||
|
||||
# `--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
|
||||
|
|
@ -336,48 +380,95 @@
|
|||
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"
|
||||
# LES COLLECTIONS DU VERROU (2026-10-09), meme chemin que les roues : le depot du site
|
||||
# d'abord, Galaxy en repli dit, la somme verifiee. `collection download` sortait sur Galaxy
|
||||
# a chaque runner neuf.
|
||||
- name: Prendre les collections verrouillees au depot du site
|
||||
ansible.builtin.get_url:
|
||||
url: "{{ setops_depot_binaires }}/{{ item.nom }}"
|
||||
dest: "{{ serveur_ops_cache_collections }}/{{ item.nom }}"
|
||||
checksum: "sha256:{{ item.sha256 }}"
|
||||
mode: "0644"
|
||||
timeout: 30
|
||||
loop: "{{ serveur_ops_verrou.collections }}"
|
||||
loop_control:
|
||||
label: "{{ item.nom }}"
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
run_once: true
|
||||
when: not ansible_check_mode
|
||||
failed_when: false
|
||||
when:
|
||||
- setops_depot_binaires | default('') | length > 0
|
||||
- not ansible_check_mode
|
||||
|
||||
- name: Deposer les collections sur le poste
|
||||
- name: Les collections verrouillees sont-elles dans le cache du controleur ?
|
||||
ansible.builtin.stat:
|
||||
path: "{{ serveur_ops_cache_collections }}/{{ item.nom }}"
|
||||
checksum_algorithm: sha256
|
||||
loop: "{{ serveur_ops_verrou.collections }}"
|
||||
loop_control:
|
||||
label: "{{ item.nom }}"
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
run_once: true
|
||||
register: serveur_ops_collections_cache
|
||||
|
||||
- name: Dire les collections absentes du depot du site (prises sur Galaxy)
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
{{ serveur_ops_collections_cache.results | rejectattr('stat.exists') | map(attribute='item.nom') | list }}
|
||||
— absentes du depot du site : prises sur Internet.
|
||||
run_once: true
|
||||
when: serveur_ops_collections_cache.results | rejectattr('stat.exists') | list | length > 0
|
||||
|
||||
- name: Prendre sur Galaxy les collections que le site n'a pas (repli)
|
||||
ansible.builtin.get_url:
|
||||
url: "{{ item.item.url }}"
|
||||
dest: "{{ serveur_ops_cache_collections }}/{{ item.item.nom }}"
|
||||
checksum: "sha256:{{ item.item.sha256 }}"
|
||||
mode: "0644"
|
||||
timeout: 60
|
||||
loop: "{{ serveur_ops_collections_cache.results }}"
|
||||
loop_control:
|
||||
label: "{{ item.item.nom }}"
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
run_once: true
|
||||
when:
|
||||
- not ansible_check_mode
|
||||
- not item.stat.exists or item.stat.checksum != item.item.sha256
|
||||
|
||||
- name: Deposer sur le poste les collections du verrou
|
||||
register: serveur_ops_depot_collections
|
||||
ansible.builtin.copy:
|
||||
src: "{{ serveur_ops_cache_collections }}/"
|
||||
dest: "{{ serveur_ops_racine }}/.collections-hors-ligne/"
|
||||
src: "{{ serveur_ops_cache_collections }}/{{ item.nom }}"
|
||||
dest: "{{ serveur_ops_racine }}/.collections-hors-ligne/{{ item.nom }}"
|
||||
owner: "{{ serveur_ops_utilisateur }}"
|
||||
group: "{{ serveur_ops_utilisateur }}"
|
||||
mode: "0644"
|
||||
directory_mode: "0755"
|
||||
loop: "{{ serveur_ops_verrou.collections }}"
|
||||
loop_control:
|
||||
label: "{{ item.nom }}"
|
||||
when: not ansible_check_mode
|
||||
|
||||
# Le meme format que celui qu'ecrivait `collection download` : des archives en RELATIF,
|
||||
# que l'installation lit depuis ce dossier (`chdir`).
|
||||
- name: Ecrire la liste hors ligne des collections du verrou
|
||||
register: serveur_ops_depot_collections_liste
|
||||
ansible.builtin.copy:
|
||||
dest: "{{ serveur_ops_racine }}/.collections-hors-ligne/requirements.yml"
|
||||
owner: "{{ serveur_ops_utilisateur }}"
|
||||
group: "{{ serveur_ops_utilisateur }}"
|
||||
mode: "0644"
|
||||
content: |
|
||||
# Gere par Set-OPS (role serveur_ops), d'apres verrou-runner.yml. Ne pas editer.
|
||||
collections:
|
||||
{% for c in serveur_ops_verrou.collections %}
|
||||
- name: {{ c.nom }}
|
||||
version: {{ c.nom | regex_replace('^.*-([0-9][^-]*)[.]tar[.]gz$', '\\1') }}
|
||||
{% endfor %}
|
||||
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: >-
|
||||
|
|
@ -431,6 +522,7 @@
|
|||
# d'autre ne l'aurait vu. Les conditions vont donc dans UNE liste.
|
||||
when:
|
||||
- serveur_ops_depot_collections is changed
|
||||
or serveur_ops_depot_collections_liste is changed
|
||||
or not serveur_ops_collections_en_place.stat.exists
|
||||
- not ansible_check_mode
|
||||
|
||||
|
|
|
|||
|
|
@ -3898,6 +3898,14 @@ def preuve_depot_binaires_complet() -> tuple[bool, str]:
|
|||
tenus |= set(noms_de[modele])
|
||||
|
||||
attendus: dict[str, str] = {}
|
||||
# LE VERROU DU RUNNER (2026-10-09) : ses roues et ses collections, que `serveur_ops` va
|
||||
# chercher. Tenues seulement si le site lit ce verrou.
|
||||
verrou = _yaml.safe_load((RACINE / "verrou-runner.yml").read_text(encoding="utf-8")) or {}
|
||||
noms_verrou = [str(e["nom"]) for k in ("python", "collections") for e in verrou.get(k) or []]
|
||||
for nom in noms_verrou:
|
||||
attendus[nom] = "serveur_ops (verrou-runner.yml)"
|
||||
if "verrou-runner.yml" in taches_site:
|
||||
tenus |= set(noms_verrou)
|
||||
for role in sorted(p.name for p in roles_dir.iterdir() if p.is_dir()):
|
||||
for tf in sorted((roles_dir / role / "tasks").glob("*.yml")) if (roles_dir / role / "tasks").is_dir() else []:
|
||||
texte = tf.read_text(encoding="utf-8")
|
||||
|
|
|
|||
82
scripts/tests/test_verrou_runner.py
Normal file
82
scripts/tests/test_verrou_runner.py
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
#!/usr/bin/env python3
|
||||
"""`verrou-runner.yml` : ce que le runner installe, une version par bibliotheque, coherent avec
|
||||
les epingles — et plus aucune resolution contre PyPI ou Galaxy dans les taches.
|
||||
|
||||
POURQUOI (2026-10-09). Le runner de Technolibre portait deux versions de six bibliotheques :
|
||||
`pip download` resolvait contre PyPI a chaque passage, et PyPI avait bouge entre
|
||||
l'insemination et le montage. Le verrou nomme chaque fichier ; ce test garde qu'il reste un
|
||||
verrou (une version par bibliotheque), que ses roues vont au Python declare, et que les
|
||||
epingles ecrites ailleurs disent la meme chose que lui.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
RACINE = Path(__file__).resolve().parents[2]
|
||||
ECHECS: list[str] = []
|
||||
|
||||
|
||||
def verifier(cond: bool, msg: str) -> None:
|
||||
print(("OK " if cond else "ECHEC ") + msg)
|
||||
if not cond:
|
||||
ECHECS.append(msg)
|
||||
|
||||
|
||||
def norme(nom: str) -> str:
|
||||
return re.sub(r"[-_.]+", "_", nom).lower()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
v = yaml.safe_load((RACINE / "verrou-runner.yml").read_text())
|
||||
roues, colls = v["python"], v["collections"]
|
||||
for e in roues + colls:
|
||||
verifier(e["url"].endswith("/" + e["nom"]) and re.fullmatch(r"[0-9a-f]{64}", str(e["sha256"])) is not None,
|
||||
f"{e['nom']} : url qui le nomme, sha256 bien formee")
|
||||
versions: dict[str, set[str]] = {}
|
||||
for e in roues:
|
||||
dist, ver = e["nom"].split("-")[0], e["nom"].split("-")[1]
|
||||
versions.setdefault(norme(dist), set()).add(ver)
|
||||
doubles = {d: sorted(vs) for d, vs in versions.items() if len(vs) > 1}
|
||||
verifier(not doubles, f"une seule version par bibliotheque ({len(versions)} bibliotheques){' : ' + str(doubles) if doubles else ''}")
|
||||
cible = "cp" + str(v["python_cible"]).replace(".", "")
|
||||
hors = [e["nom"] for e in roues if e["nom"].split("-")[2].startswith("cp") and "abi3" not in e["nom"]
|
||||
and e["nom"].split("-")[2] != cible]
|
||||
verifier(not hors, f"les roues compilees vont a Python {v['python_cible']} ({cible}){' : ' + str(hors) if hors else ''}")
|
||||
|
||||
epingles = {}
|
||||
for l in (RACINE / "requirements-python.txt").read_text().splitlines():
|
||||
m = re.match(r"^\s*([A-Za-z0-9_.-]+)==([^\s#]+)", l)
|
||||
if m:
|
||||
epingles[norme(m.group(1))] = m.group(2)
|
||||
defauts = yaml.safe_load((RACINE / "roles/serveur_ops/defaults/main.yml").read_text())
|
||||
m = re.fullmatch(r"\s*([A-Za-z0-9_.-]+)==(\S+)\s*", defauts["serveur_ops_ansible"])
|
||||
verifier(m is not None, f"serveur_ops_ansible est une epingle exacte ({defauts['serveur_ops_ansible']})")
|
||||
if m:
|
||||
epingles[norme(m.group(1))] = m.group(2)
|
||||
for dist, ver in sorted(epingles.items()):
|
||||
verifier(versions.get(dist) == {ver}, f"{dist}=={ver} epingle, et c'est la version du verrou ({sorted(versions.get(dist, []))})")
|
||||
|
||||
req = yaml.safe_load((RACINE / "requirements.yml").read_text())["collections"]
|
||||
attendus = sorted(f"{c['name'].replace('.', '-')}-{c['version']}.tar.gz" for c in req)
|
||||
verifier(sorted(e["nom"] for e in colls) == attendus, f"les collections du verrou sont celles de requirements.yml ({attendus})")
|
||||
|
||||
taches = [l for l in (RACINE / "roles/serveur_ops/tasks/main.yml").read_text().splitlines()
|
||||
if not l.lstrip().startswith("#")]
|
||||
texte = "\n".join(taches)
|
||||
verifier('"download"' not in texte and "collection download" not in texte,
|
||||
"plus aucune resolution contre PyPI ou Galaxy dans serveur_ops (pip / collection download)")
|
||||
verifier("verrou-runner.yml" in (RACINE / "roles/serveur_artefacts/tasks/main.yml").read_text(),
|
||||
"le site tient le verrou du runner")
|
||||
if ECHECS:
|
||||
print(f"\n{len(ECHECS)} echec(s).")
|
||||
return 1
|
||||
print("\nLe verrou nomme chaque fichier du runner, une version par bibliotheque, et le site le tient.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
79
verrou-runner.yml
Normal file
79
verrou-runner.yml
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
---
|
||||
# CE QUE LE RUNNER INSTALLE, A L'OCTET PRES — tenu par le site, verifie a la reception.
|
||||
#
|
||||
# POURQUOI (2026-10-09). A chaque reconstruction, le runner sortait sur Internet : `pip
|
||||
# download` interrogeait PyPI, et un runner neuf tirait ses collections de Galaxy. Seules
|
||||
# deux bibliotheques etaient epinglees ; `ansible-core` etait borne (>=2.18,<2.19), le reste
|
||||
# libre. Mesure sur le runner de Technolibre : son depot de roues portait DEUX versions de
|
||||
# six bibliotheques (cryptography 50.0.1 et 50.0.2, urllib3 2.7.0 et 2.8.0...) — PyPI avait
|
||||
# bouge entre l'insemination et le montage, et personne n'avait choisi celle qui tournait.
|
||||
#
|
||||
# CE VERROU EST CE QUI TOURNE : les quinze roues installees dans le venv de ce runner (pip
|
||||
# freeze), et les trois collections qu'il a recues, avec leurs sommes, releves le
|
||||
# 2026-10-09. Chaque URL a ete telechargee depuis le poste et rend exactement sa somme.
|
||||
#
|
||||
# QUI LE LIT : `serveur_artefacts` (le site tient ces fichiers dans son depot de binaires),
|
||||
# `serveur_ops` (le runner les prend au site, ne depose QUE ceux-ci, installe hors ligne),
|
||||
# P70 (le site tient tout ce que le runner va chercher), `test_verrou_runner.py`.
|
||||
#
|
||||
# MONTER UNE VERSION est un geste delibere : remplacer la ligne (nom, url, sha256), et les
|
||||
# epingles de `requirements-python.txt`, `serveur_ops_ansible` et `requirements.yml`, que
|
||||
# le test confronte a ce verrou.
|
||||
# Le Python pour lequel les roues sont faites (`cp313`) ; la cible doit le porter.
|
||||
python_cible: "3.13"
|
||||
python:
|
||||
- nom: ansible_core-2.18.19-py3-none-any.whl
|
||||
url: https://files.pythonhosted.org/packages/py3/a/ansible-core/ansible_core-2.18.19-py3-none-any.whl
|
||||
sha256: 00f53c10d97ce001f6e49857820901b3e8d2f234cb9f5fd9fdf8092c14109fa0
|
||||
- nom: certifi-2026.7.22-py3-none-any.whl
|
||||
url: https://files.pythonhosted.org/packages/py3/c/certifi/certifi-2026.7.22-py3-none-any.whl
|
||||
sha256: 62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775
|
||||
- nom: cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
|
||||
url: https://files.pythonhosted.org/packages/cp313/c/cffi/cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
|
||||
sha256: a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2
|
||||
- nom: charset_normalizer-3.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl
|
||||
url: https://files.pythonhosted.org/packages/cp313/c/charset-normalizer/charset_normalizer-3.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl # yamllint disable-line rule:line-length
|
||||
sha256: 7218e8f32b0956cfcd048fd42d9d5779809745ca1d86113ca56f66e7ae1549c4
|
||||
- nom: cryptography-50.0.2-cp311-abi3-manylinux_2_34_x86_64.whl
|
||||
url: https://files.pythonhosted.org/packages/cp311/c/cryptography/cryptography-50.0.2-cp311-abi3-manylinux_2_34_x86_64.whl
|
||||
sha256: 9dab55f57c74c3cad24c323bacbbd04be4705ba6eb0d92e920b1fc4837ed5079
|
||||
- nom: idna-3.20-py3-none-any.whl
|
||||
url: https://files.pythonhosted.org/packages/py3/i/idna/idna-3.20-py3-none-any.whl
|
||||
sha256: ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c
|
||||
- nom: jinja2-3.1.6-py3-none-any.whl
|
||||
url: https://files.pythonhosted.org/packages/py3/j/jinja2/jinja2-3.1.6-py3-none-any.whl
|
||||
sha256: 85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67
|
||||
- nom: markupsafe-3.0.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl
|
||||
url: https://files.pythonhosted.org/packages/cp313/m/markupsafe/markupsafe-3.0.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl # yamllint disable-line rule:line-length
|
||||
sha256: 434139499bb20b502ed3baa1f169e618f924a97e7a777fea1a49446d80106cf6
|
||||
- nom: packaging-26.3-py3-none-any.whl
|
||||
url: https://files.pythonhosted.org/packages/py3/p/packaging/packaging-26.3-py3-none-any.whl
|
||||
sha256: d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c
|
||||
- nom: proxmoxer-2.2.0-py3-none-any.whl
|
||||
url: https://files.pythonhosted.org/packages/py3/p/proxmoxer/proxmoxer-2.2.0-py3-none-any.whl
|
||||
sha256: 7e4431fd38e1a9321eedf6d860ddc373fc2b29ce1845fa8b072d7580d99f6b90
|
||||
- nom: pycparser-3.1-py3-none-any.whl
|
||||
url: https://files.pythonhosted.org/packages/py3/p/pycparser/pycparser-3.1-py3-none-any.whl
|
||||
sha256: f09d358c840bd147b79e55f2bc494f18ea869dc897f5852a8f5766b74f787882
|
||||
- nom: pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl
|
||||
url: https://files.pythonhosted.org/packages/cp313/p/pyyaml/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl
|
||||
sha256: 0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6
|
||||
- nom: requests-2.32.3-py3-none-any.whl
|
||||
url: https://files.pythonhosted.org/packages/py3/r/requests/requests-2.32.3-py3-none-any.whl
|
||||
sha256: 70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6
|
||||
- nom: resolvelib-1.0.1-py2.py3-none-any.whl
|
||||
url: https://files.pythonhosted.org/packages/py2.py3/r/resolvelib/resolvelib-1.0.1-py2.py3-none-any.whl
|
||||
sha256: d2da45d1a8dfee81bdd591647783e340ef3bcb104b54c383f70d422ef5cc7dbf
|
||||
- nom: urllib3-2.8.0-py3-none-any.whl
|
||||
url: https://files.pythonhosted.org/packages/py3/u/urllib3/urllib3-2.8.0-py3-none-any.whl
|
||||
sha256: 0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3
|
||||
collections:
|
||||
- nom: ansible-posix-1.6.2.tar.gz
|
||||
url: https://galaxy.ansible.com/api/v3/plugin/ansible/content/published/collections/artifacts/ansible-posix-1.6.2.tar.gz
|
||||
sha256: 3c6b4d4f326cb42490b75644179e809ed09f3040ddaf0763bd6ea9e0f2ef8c47
|
||||
- nom: community-general-10.3.0.tar.gz
|
||||
url: https://galaxy.ansible.com/api/v3/plugin/ansible/content/published/collections/artifacts/community-general-10.3.0.tar.gz
|
||||
sha256: f1df87cca61c7c22aa1d66fbb3864185da71d0141f825a1385b8c4b7a635b129
|
||||
- nom: community-postgresql-3.10.2.tar.gz
|
||||
url: https://galaxy.ansible.com/api/v3/plugin/ansible/content/published/collections/artifacts/community-postgresql-3.10.2.tar.gz
|
||||
sha256: f0ad8976e502d28c901b8bbeb5a4a85a82f86e28fac245c4caa3d60a618e14e2
|
||||
Loading…
Reference in a new issue