--- - name: Résoudre la base IcingaDB depuis le registre (rôle partagé) ansible.builtin.include_role: name: resoudre_base vars: resoudre_base_groupe: "{{ serveur_icingaweb2_icinga_groupe }}" # ON RECOPIE TOUT DE SUITE, PARCE QU'UN SECOND APPEL ECRASE LE PREMIER (2026-09-15). # # `resoudre_base` rend ses resultats dans des faits PARTAGES — `resoudre_base_entree`, # `resoudre_base_db_host`... Ce role l'appelle DEUX fois : une pour la base du MOTEUR # (icingadb, en lecture) et une pour celle des COMPTES (icingaweb2). Le second appel # remplace les faits du premier, et les gabarits rendus APRES les deux lisaient donc la # seconde base partout. # # CE QUE CA A DONNE, mesure sur la machine : # # [icingadb] dbname = "icingaweb2" <- la base des comptes # [icingaweb_db] dbname = "icingaweb2" # # La console cherchait `icingadb_schema` dans la base des comptes, ne l'y trouvait pas, et # rendait une trace PHP a chaque page. Les 66 tables du moteur etaient intactes a cote. # # UN ROLE PARTAGE REND DES FAITS PARTAGES : celui qui l'appelle deux fois doit nommer ses # resultats avant de le rappeler. C'est la meme lecon que « une liste qui suit une autre », # appliquee au temps plutot qu'a l'espace. - name: Adopter la base du MOTEUR avant de résoudre celle des comptes ansible.builtin.set_fact: serveur_icingaweb2_moteur_host: "{{ resoudre_base_db_host }}" serveur_icingaweb2_moteur_port: "{{ resoudre_base_db_port }}" serveur_icingaweb2_moteur_base: "{{ resoudre_base_entree.base }}" serveur_icingaweb2_moteur_utilisateur: "{{ resoudre_base_entree.proprietaire }}" serveur_icingaweb2_moteur_motdepasse: "{{ resoudre_base_db_password }}" no_log: true # --- MODE `db` : LA BASE DES COMPTES, ET LE COMPTE D'AMORCAGE (2026-09-15) ------------ # # TOUT EN HAUT, ET C'EST L'ORDRE QUI L'EXIGE. `resources.ini` nomme cette base ; il est # rendu quelques taches plus bas. Pose apres lui — la ou vivait l'ancien vestibule — ce # bloc laissait le gabarit lire des variables qui n'existaient pas encore, et l'echec # etait CENSURE par `no_log` : « changed: true » et rien d'autre. Une tache qui rend des # secrets ne peut pas dire ce qui lui manque ; c'est a l'ordre de ne pas la mettre dans # cette situation. # # Icinga Web 2 gere ses comptes NATIVEMENT. La premiere ecriture posait un `auth_basic` # nginx devant le backend `external` — ca marchait, et ca reinventait une page de # connexion devant une application qui en a une, en privant l'exploitant de la gestion # des comptes dans l'interface. Un vestibule n'a de sens que devant une application qui # ne sait pas s'authentifier. - name: Résoudre la base des comptes de la console (rôle partagé) ansible.builtin.include_role: name: resoudre_base vars: resoudre_base_groupe: "icingaweb2" when: serveur_icingaweb2_auth == 'db' - name: Adopter la base des comptes pour Icinga Web 2 ansible.builtin.set_fact: serveur_icingaweb2_db_host: "{{ resoudre_base_db_host }}" serveur_icingaweb2_db_port: "{{ resoudre_base_db_port }}" serveur_icingaweb2_db_base: "{{ resoudre_base_entree.base }}" serveur_icingaweb2_db_utilisateur: "{{ resoudre_base_entree.proprietaire }}" serveur_icingaweb2_db_motdepasse: "{{ resoudre_base_db_password }}" no_log: true when: serveur_icingaweb2_auth == 'db' - name: Exiger un mot de passe pour le compte d'amorçage ansible.builtin.assert: that: - serveur_icingaweb2_admin_motdepasse | length > 0 fail_msg: >- `serveur_icingaweb2_auth: db` mais `serveur_icingaweb2_admin_motdepasse` est vide. Une console qui montre l'état de toute une fabric ne s'ouvre pas avec un mot de passe deviné : renseigner la clé dans la voûte de cet écosystème. when: serveur_icingaweb2_auth == 'db' # LE SCHEMA NE SE CHARGE QU'UNE FOIS. Le rejouer sur une base deja peuplee echouerait sur # les objets existants — on lit donc d'abord si la table des comptes est la. - name: La base des comptes est-elle déjà en place ? community.postgresql.postgresql_query: login_host: "{{ serveur_icingaweb2_db_host }}" login_user: "{{ serveur_icingaweb2_db_utilisateur }}" login_password: "{{ serveur_icingaweb2_db_motdepasse }}" login_db: "{{ serveur_icingaweb2_db_base }}" ssl_mode: "{{ serveur_icingaweb2_db_sslmode | default('prefer', true) or 'prefer' }}" ca_cert: "{{ serveur_icingaweb2_db_sslrootcert if serveur_icingaweb2_db_sslmode | default('') else omit }}" query: "SELECT to_regclass('public.icingaweb_user') IS NOT NULL AS presente" register: serveur_icingaweb2_schema_etat changed_when: false no_log: true when: - serveur_icingaweb2_auth == 'db' - not ansible_check_mode - name: Charger le schéma des comptes community.postgresql.postgresql_script: login_host: "{{ serveur_icingaweb2_db_host }}" login_user: "{{ serveur_icingaweb2_db_utilisateur }}" login_password: "{{ serveur_icingaweb2_db_motdepasse }}" login_db: "{{ serveur_icingaweb2_db_base }}" ssl_mode: "{{ serveur_icingaweb2_db_sslmode | default('prefer', true) or 'prefer' }}" ca_cert: "{{ serveur_icingaweb2_db_sslrootcert if serveur_icingaweb2_db_sslmode | default('') else omit }}" path: "{{ serveur_icingaweb2_schema }}" no_log: true when: - serveur_icingaweb2_auth == 'db' - not ansible_check_mode - not (serveur_icingaweb2_schema_etat.query_result[0].presente | default(false)) # LE HACHAGE EST CELUI QUE L'APPLICATION VERIFIE, PAS UN QU'ON CHOISIT. `password_hash()` # de PHP est exactement la fonction qu'Icinga Web 2 appelle a la connexion ; produire un # hachage d'un autre outil donnerait une base valide et une connexion impossible. - name: Hacher le mot de passe du compte d'amorçage ansible.builtin.command: argv: - php - -r - 'echo password_hash($argv[1], PASSWORD_DEFAULT);' - "{{ serveur_icingaweb2_admin_motdepasse }}" register: serveur_icingaweb2_hachage changed_when: false no_log: true when: - serveur_icingaweb2_auth == 'db' - not ansible_check_mode # CREE S'IL MANQUE, JAMAIS ECRASE. Un mot de passe change dans l'interface appartient a # celui qui l'a change : le moteur ne le reprend pas au deploiement suivant. Il ne pose # que le compte qui permet d'entrer la premiere fois. - name: Poser le compte d'amorçage s'il n'existe pas community.postgresql.postgresql_query: login_host: "{{ serveur_icingaweb2_db_host }}" login_user: "{{ serveur_icingaweb2_db_utilisateur }}" login_password: "{{ serveur_icingaweb2_db_motdepasse }}" login_db: "{{ serveur_icingaweb2_db_base }}" ssl_mode: "{{ serveur_icingaweb2_db_sslmode | default('prefer', true) or 'prefer' }}" ca_cert: "{{ serveur_icingaweb2_db_sslrootcert if serveur_icingaweb2_db_sslmode | default('') else omit }}" query: >- INSERT INTO icingaweb_user (name, active, password_hash) VALUES (%s, 1, %s) ON CONFLICT (name) DO NOTHING positional_args: - "{{ serveur_icingaweb2_admin_utilisateur }}" - "{{ serveur_icingaweb2_hachage.stdout }}" no_log: true when: - serveur_icingaweb2_auth == 'db' - not ansible_check_mode # ON NE RESOUT PAS UN ANNUAIRE QUAND ON N'EN A PAS (2026-09-14). # # `resoudre_annuaire` etait appele SANS CONDITION. Au site — qui n'a ni LDAP ni Keycloak — # il tombait sur un plan sans application d'annuaire : # # object of type 'NoneType' has no len() # # Un message qui ne nomme ni l'annuaire, ni le role qui le demandait, ni la raison. Le # mode `locale` existe precisement pour se passer d'annuaire : il n'a rien a resoudre. - name: Résoudre l'annuaire (rôle partagé) ansible.builtin.include_role: name: resoudre_annuaire vars: # SON compte, pas celui de l'administrateur de l'annuaire (2026-09-13). resoudre_annuaire_bind_cn: "icingaweb2" resoudre_annuaire_secret: "vault_ldap_bind_icingaweb2" # LES MODES NOMMES, PAS UNE NEGATION. `!= 'locale'` etait juste tant que `locale` # etait le seul mode sans annuaire ; renomme en `db`, la garde a cesse de garder et # `resoudre_annuaire` s'est remis a exiger un secret de liaison qui n'existe pas. # Une condition qui dit ce qu'elle VEUT survit a un renommage ; une qui dit ce qu'elle # refuse, non. when: serveur_icingaweb2_auth in ('ldap', 'external') - name: Adopter la connexion annuaire pour Icinga Web 2 ansible.builtin.set_fact: serveur_icingaweb2_ldap_url: "{{ resoudre_annuaire_uri }}" serveur_icingaweb2_ldap_port: "{{ resoudre_annuaire_port }}" serveur_icingaweb2_ldap_base: "{{ resoudre_annuaire_base_dn }}" serveur_icingaweb2_ldap_users_dn: "{{ resoudre_annuaire_users_dn }}" serveur_icingaweb2_ldap_bind_dn: "{{ resoudre_annuaire_bind_dn }}" serveur_icingaweb2_ldap_bind_password: "{{ resoudre_annuaire_bind_password }}" no_log: true # LES MODES NOMMES, PAS UNE NEGATION. `!= 'locale'` etait juste tant que `locale` # etait le seul mode sans annuaire ; renomme en `db`, la garde a cesse de garder et # `resoudre_annuaire` s'est remis a exiger un secret de liaison qui n'existe pas. # Une condition qui dit ce qu'elle VEUT survit a un renommage ; une qui dit ce qu'elle # refuse, non. when: serveur_icingaweb2_auth in ('ldap', 'external') # LE CACHE DU CONTROLEUR D'ABORD, LE DEPOT DISTANT POUR LE RESTE (2026-09-03). # # Les depots tiers sont en HTTPS, et `client_artefacts` pose # `Acquire::https::Proxy "DIRECT"` — ils CONTOURNENT donc le cache du site et sortent sur # Internet a chaque construction de VM. `make cacher-paquets` les tire une fois, versions # epinglees et empreintes verifiees ; ce role les depose depuis ce cache. - name: Poser les paquets tiers depuis le cache du controleur ansible.builtin.include_role: name: paquets_tiers vars: paquets_tiers_noms: "{{ serveur_icingaweb2_paquets }}" - name: Installer Icinga Web 2 + module IcingaDB + PHP-FPM + nginx ansible.builtin.apt: # CE QUE LE CACHE N'A PAS FOURNI, ET RIEN D'AUTRE. Les dependances Debian passent # par le cache du site en HTTP ; seuls les paquets tiers non caches exigent Internet. # Reinstaller ce que le cache vient de poser ferait un `update_cache` inutile — et # hors ligne, il echouerait APRES un travail deja fait. name: "{{ serveur_icingaweb2_paquets | difference((paquets_tiers_disponibles | default({})).keys() | list) }}" state: present update_cache: true when: (serveur_icingaweb2_paquets | difference((paquets_tiers_disponibles | default({})).keys() | list)) | length > 0 - name: Ajouter www-data au groupe icingaweb2 (lecture de la config) ansible.builtin.user: name: www-data groups: icingaweb2 append: true notify: Redémarrer php-fpm - name: Assurer le répertoire du module IcingaDB ansible.builtin.file: path: "{{ serveur_icingaweb2_config_dir }}/modules/icingadb" state: directory owner: root group: icingaweb2 mode: "0750" - name: Déployer la configuration icingaweb2 (.ini) ansible.builtin.template: src: "{{ item.s }}" dest: "{{ serveur_icingaweb2_config_dir }}/{{ item.d }}" owner: root group: icingaweb2 mode: "0640" no_log: "{{ item.secret | default(false) }}" loop: - { s: config.ini.j2, d: config.ini } - { s: resources.ini.j2, d: resources.ini, secret: true } - { s: authentication.ini.j2, d: authentication.ini } - { s: groups.ini.j2, d: groups.ini } - { s: roles.ini.j2, d: roles.ini } - { s: icingadb-config.ini.j2, d: modules/icingadb/config.ini } - { s: icingadb-redis.ini.j2, d: modules/icingadb/redis.ini } loop_control: label: "{{ item.d }}" notify: Redémarrer php-fpm - name: Activer les modules icingaweb2 (IcingaDB, BPM) ansible.builtin.command: "icingacli module enable {{ item }}" args: creates: "{{ serveur_icingaweb2_config_dir }}/enabledModules/{{ item }}" loop: "{{ serveur_icingaweb2_modules }}" when: not ansible_check_mode - name: Assurer le répertoire des processus BPM (éditable via l'UI) ansible.builtin.file: path: "{{ serveur_icingaweb2_config_dir }}/modules/businessprocess/processes" state: directory owner: root group: icingaweb2 mode: "2770" when: "'businessprocess' in serveur_icingaweb2_modules" - name: Semer les processus métier BPM (IaC, optionnel) ansible.builtin.copy: content: "{{ item.value }}" dest: "{{ serveur_icingaweb2_config_dir }}/modules/businessprocess/processes/{{ item.key }}.conf" owner: root group: icingaweb2 mode: "0660" loop: "{{ serveur_icingaweb2_bpm_processes | dict2items }}" loop_control: label: "{{ item.key }}" when: serveur_icingaweb2_bpm_processes | length > 0 - name: Déployer le vhost nginx local d'Icinga Web 2 ansible.builtin.template: src: nginx.conf.j2 dest: /etc/nginx/sites-available/icingaweb2.conf owner: root group: root mode: "0644" notify: Recharger nginx - name: Activer le vhost Icinga Web 2 ansible.builtin.file: src: /etc/nginx/sites-available/icingaweb2.conf dest: /etc/nginx/sites-enabled/icingaweb2.conf state: link notify: Recharger nginx - name: Retirer le site nginx par défaut ansible.builtin.file: path: /etc/nginx/sites-enabled/default state: absent notify: Recharger nginx - name: Activer et démarrer PHP-FPM et nginx ansible.builtin.systemd: name: "{{ item }}" enabled: true state: started loop: - "{{ serveur_icingaweb2_php_fpm_service }}" - "{{ serveur_icingaweb2_nginx_service }}" when: not ansible_check_mode # LE ROLE QUI POSSEDE LA VERITE DEPOSE SA PROPRE SONDE. `client_sante` la fait tourner et # pousse le resultat ; les greffons standard, eux, sont poses par le porteur. # # LA SUPERVISION SURVEILLE SA PROPRE VITRINE. Sans cette sonde, une console morte laisse # tous les verdicts au vert — le moteur va bien, les sondes poussent, et personne ne peut # plus rien lire. - name: Assurer le repertoire des sondes de supervision ansible.builtin.file: path: /usr/local/lib/setops/sondes state: directory owner: root group: root mode: "0755" - name: Deposer la sonde « vigie » ansible.builtin.template: src: sonde-vigie.sh.j2 dest: /usr/local/lib/setops/sondes/vigie.sh owner: root group: root mode: "0750"