From aca63011ea7db23a86705ca906752dad9ebc9fab Mon Sep 17 00:00:00 2001 From: Daniel Allaire Date: Fri, 28 Aug 2026 15:37:09 -0400 Subject: [PATCH] =?UTF-8?q?roues=20:=20pip=20par=20le=20meme=20chemin=20qu?= =?UTF-8?q?e=20les=20collections=20=E2=80=94=20le=20controleur=20telecharg?= =?UTF-8?q?e,=20la=20cible=20n=20installe=20rien=20du=20dehors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on --- roles/serveur_ops/defaults/main.yml | 29 ++++++++++ roles/serveur_ops/tasks/main.yml | 89 ++++++++++++++++++++++++++++- 2 files changed, 116 insertions(+), 2 deletions(-) diff --git a/roles/serveur_ops/defaults/main.yml b/roles/serveur_ops/defaults/main.yml index a437e84..040c4cc 100644 --- a/roles/serveur_ops/defaults/main.yml +++ b/roles/serveur_ops/defaults/main.yml @@ -153,3 +153,32 @@ serveur_ops_cache_collections: >- {{ (setops_cache_artefacts | default(lookup('env', 'HOME') + '/.cache/setops')) }}/collections-{{ lookup('file', serveur_ops_requirements_controleur) | hash('sha1') | truncate(12, true, '') }} serveur_ops_requirements_controleur: "{{ playbook_dir }}/../../requirements.yml" + +# --- LA ROUE HORS-LIGNE : PYTHON PAR LE MEME CHEMIN QUE LES COLLECTIONS ------- +# +# LA DERNIERE DEPENDANCE QUI SORTAIT PAR ELLE-MEME (2026-08-28). 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, ce qui est le sens meme d'une lignee autonome ». `pip`, lui, +# sortait encore vers PyPI, en HTTPS et par nom de domaine. +# +# 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<2.19 +# Echec temporaire dans la resolution du nom +# +# POURQUOI 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. Ce n'est pas le moment, et ce n'est pas ce probleme-ci. +# +# L'EMPREINTE DES SOURCES ENTRE DANS LE CHEMIN, comme pour les collections : changer une +# ligne de `requirements-python.txt` ou l'epinglage d'ansible-core cree un cache neuf, +# donc un telechargement. Les anciens restent et documentent ce qu'exigeaient les +# versions precedentes. +serveur_ops_roues_source: "{{ playbook_dir }}/../../requirements-python.txt" +serveur_ops_cache_roues: >- + {{ (setops_cache_artefacts | default(lookup('env', 'HOME') + '/.cache/setops')) }}/roues-{{ + (lookup('file', serveur_ops_roues_source) ~ serveur_ops_ansible) | hash('sha1') | truncate(12, true, '') }} +serveur_ops_roues_depot: "{{ serveur_ops_racine }}/.roues-hors-ligne" diff --git a/roles/serveur_ops/tasks/main.yml b/roles/serveur_ops/tasks/main.yml index cdbdba9..c1d9c62 100644 --- a/roles/serveur_ops/tasks/main.yml +++ b/roles/serveur_ops/tasks/main.yml @@ -93,13 +93,97 @@ group: "{{ serveur_ops_utilisateur }}" mode: "0644" -- name: Installer Ansible dans l'environnement isolé +# --- 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 @@ -111,11 +195,12 @@ # 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) +- 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