--- - name: Exiger les intrants du poste d'exploitation ansible.builtin.assert: that: - domaine_interne | default('') | length > 0 - serveur_ops_depots | length > 0 fail_msg: >- serveur_ops exige `domaine_interne` et au moins un dépôt dans `serveur_ops_depots` (le moteur au minimum). - 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 }}" - name: Installer Ansible dans l'environnement isolé ansible.builtin.pip: name: - "{{ serveur_ops_ansible }}" - "pyyaml" virtualenv: "{{ serveur_ops_venv }}" state: present 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 # --- 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` (racine step-ca dans le magasin système). Sans # lui, `git clone` refuse le certificat de la forge — 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 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. - 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 -p {{ serveur_ops_racine }}/.ansible/collections chdir: "{{ serveur_ops_racine }}/.collections-hors-ligne" creates: "{{ serveur_ops_racine }}/.ansible/collections/ansible_collections/community/general" become: true become_user: "{{ serveur_ops_utilisateur }}" when: 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. - 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: 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"