site : le premier site construit depuis zero, et ce que le moteur supposait deja en place
- cloner_vm_debian / supprimer_vm_debian suivent SETOPS_UNDERLAY (le clonage visait le cluster du lien underlay.yml) ; sept cibles du Makefile aussi pour la voute. - site_machines.py transmet au clone la cle, le domaine et le resolveur du plan du site. - La machine qui porte le cache recoit artefacts_amorcage vide ; le socle retire alors le proxy herite du gabarit. P79 n'admet ce vide que sur la machine qui porte le cache. - serveur_icingaweb2 : la base et le compte d'amorcage apres l'installation des paquets. - genome_pousser : cle du runner, organisation genome et depots crees s'ils manquent. - common_packages : la lecture apt-daily s'execute en simulation (check_mode: false). 95 preuves (make verifier), make test passe. CHANGELOG 146. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
f9e45b1676
commit
810670cd30
11 changed files with 290 additions and 95 deletions
45
CHANGELOG.md
45
CHANGELOG.md
|
|
@ -1,3 +1,48 @@
|
|||
## 2026-10-13 (146) — Le premier site construit depuis zéro : ce que le moteur supposait déjà là
|
||||
|
||||
**95 preuves.** `make verifier` passe (site de Chezlepro), `make test` passe. Témoins : l'inventaire
|
||||
du site de Chezlepro ne change que pour sa propre machine de cache ; P79 signale toujours une
|
||||
valeur d'amorçage vide sur une machine qui ne porte pas le cache, et une valeur absente sur celle
|
||||
qui le porte.
|
||||
|
||||
Les neuf machines du site de TechnoLibre sont nées du gabarit 9006 (`maurice`, `nvme` et
|
||||
`miroir`, ~30 s chacune) et le site se déploie couche par couche. Aucun site n'avait jamais été
|
||||
construit depuis zéro — celui de Chezlepro ne l'a jamais été — et chaque arrêt a montré une
|
||||
chose que le moteur supposait **déjà en place** :
|
||||
|
||||
- **Le clonage visait le cluster du lien `underlay.yml`**, pas celui de `SETOPS_UNDERLAY` :
|
||||
les VM de TechnoLibre seraient nées chez Chezlepro, avec ses jetons. `cloner_vm_debian` et
|
||||
`supprimer_vm_debian` suivent la variable ; **sept cibles** du Makefile aussi pour la voûte
|
||||
(`site-appliquer`, `site-deployer-tout`, `genome-pousser`, `genome-etat`, `forge-cles`,
|
||||
`forge-comptes`, `cloner-vm`).
|
||||
- **`site-creer` ne transmettait ni la clé, ni le domaine, ni le résolveur du plan** : les VM
|
||||
ne portaient que la clé du gabarit (celle de Chezlepro), fermées à Ansible. `site_machines.py`
|
||||
les tire du plan, et refuse si la clé publique manque.
|
||||
- **Le cache s'amorçait sur lui-même**, et toutes les machines sur un cache déployé plus tard.
|
||||
La machine qui porte le cache reçoit `artefacts_amorcage` vide ; le socle retire alors le
|
||||
fichier hérité du gabarit (qui visait `10.0.33.21`, une adresse d'avant le renumérotage) et
|
||||
elle va droit chez Debian. Déployée seule d'abord ; prouvée : HTTP 200 depuis une autre zone.
|
||||
- **Les règles nftables du site n'avaient jamais été générées** pour ce site : le pare-feu
|
||||
serait retombé sur son gabarit générique. Générées, relues, `nft -c` sur deux machines.
|
||||
- **La PKI voulait restaurer son état d'un dépôt qui n'existait pas encore.** Le refus est
|
||||
juste (conclure « rien à restaurer » faute de joindre le dépôt ferait naître neuf ce qui
|
||||
attend ailleurs) : le dépôt de sauvegarde est déployé d'abord, il répond « rien
|
||||
d'antérieur », et l'autorité naît neuve parce qu'il l'a dit. L'ordre des couches reste à
|
||||
corriger pour une première naissance.
|
||||
- **`serveur_icingaweb2` chargeait son schéma avant d'installer le paquet qui le fournit.**
|
||||
Le bloc base et compte d'amorçage vient après les paquets.
|
||||
- **Une forge neuve n'avait ni organisation ni dépôts**, et le runner ne fabriquait sa clé
|
||||
qu'après avoir cloné. `genome_pousser` crée la clé si elle manque (à l'identique du rôle),
|
||||
l'organisation `genome` (publique) et les dépôts manquants (moteur et modèles publics,
|
||||
carte du site et plans privés) — la forme de la forge de Chezlepro, faite à la main.
|
||||
- **La simulation échouait sur une lecture** (`systemctl is-active` sautée en `--check`, donc
|
||||
sans `rc`) : `check_mode: false`.
|
||||
|
||||
**Voûte du site de TechnoLibre** : les secrets TSIG du DNS public, identiques à ceux du site de
|
||||
Chezlepro (les primaires n'en connaissent qu'un par relation). **Plan** : la clé d'hôte de la
|
||||
forge, relevée à la source. Un lien entre les deux sites est prévu ; d'ici là, le DNS public
|
||||
de ce site ne reçoit pas les zones.
|
||||
|
||||
## 2026-10-13 (145) — La face publique d'un site sans adresse fixe : le relais
|
||||
|
||||
**95 preuves.** `make verifier` passe (site de Chezlepro), `make test` passe. Témoin : le devis de
|
||||
|
|
|
|||
15
Makefile
15
Makefile
|
|
@ -1315,7 +1315,7 @@ site-deployer-tout: ansible-runtime ## Deploie TOUT le site dans l'ordre des cou
|
|||
python3 scripts/verifier_genome_a_jour.py || exit 2; \
|
||||
python3 scripts/orchestrer.py verifier; \
|
||||
python3 scripts/orchestrer.py ecrire; \
|
||||
voute="$$(dirname "$$(readlink -f underlay.yml)")/underlay.vault.yml"; \
|
||||
voute="$$(dirname "$$(readlink -f "$${SETOPS_UNDERLAY:-underlay.yml}")")/underlay.vault.yml"; \
|
||||
supp=(); [[ -f "$$voute" ]] && supp+=( -e "@$$voute" ); \
|
||||
mode="$$([[ -n "$(MODE_CHECK)" ]] && printf -- '--check --diff' || true)"; \
|
||||
ansible-playbook -i scripts/site_inventaire.py playbooks/site.yml \
|
||||
|
|
@ -1371,7 +1371,7 @@ gabarit-etat: ## Le gabarit porte-t-il ce que le SITE declare ? — ne corrige r
|
|||
.PHONY: genome-etat
|
||||
genome-etat: ## Ce que la forge du site porte, face au poste — ne corrige rien, regarde
|
||||
@set -e; \
|
||||
voute="$$(dirname "$$(readlink -f underlay.yml)")/underlay.vault.yml"; \
|
||||
voute="$$(dirname "$$(readlink -f "$${SETOPS_UNDERLAY:-underlay.yml}")")/underlay.vault.yml"; \
|
||||
supp=(); [[ -f "$$voute" ]] && supp+=( -e "@$$voute" ); \
|
||||
ansible-playbook -i scripts/site_inventaire.py \
|
||||
playbooks/maintenance/genome_etat.yml "$${supp[@]}" $(ARGS)
|
||||
|
|
@ -1386,7 +1386,7 @@ publier: ## Publie le genome d'un geste : eregion PUIS la forge du site, et le v
|
|||
.PHONY: publier
|
||||
genome-pousser: ## Le runner du site pousse le genome sur la forge du site — DEPOT=<nom> pour un seul
|
||||
@set -e; \
|
||||
voute="$$(dirname "$$(readlink -f underlay.yml)")/underlay.vault.yml"; \
|
||||
voute="$$(dirname "$$(readlink -f "$${SETOPS_UNDERLAY:-underlay.yml}")")/underlay.vault.yml"; \
|
||||
supp=(); [[ -f "$$voute" ]] && supp+=( -e "@$$voute" ); \
|
||||
SETOPS_DEPOT="$(DEPOT)" ansible-playbook -i scripts/site_inventaire.py \
|
||||
playbooks/maintenance/genome_pousser.yml "$${supp[@]}" $(ARGS)
|
||||
|
|
@ -1394,7 +1394,7 @@ genome-pousser: ## Le runner du site pousse le genome sur la forge du site — D
|
|||
.PHONY: forge-cles
|
||||
forge-cles: ## Le site enregistre sur le depot de chaque locataire la cle de lecture que sa face publie — CONFIRMER=true pour ecrire
|
||||
@set -e; \
|
||||
voute="$$(dirname "$$(readlink -f underlay.yml)")/underlay.vault.yml"; \
|
||||
voute="$$(dirname "$$(readlink -f "$${SETOPS_UNDERLAY:-underlay.yml}")")/underlay.vault.yml"; \
|
||||
supp=(); [[ -f "$$voute" ]] && supp+=( -e "@$$voute" ); \
|
||||
CONFIRMER="$(CONFIRMER)" ansible-playbook -i scripts/site_inventaire.py \
|
||||
playbooks/maintenance/forge_cles.yml "$${supp[@]}" $(ARGS)
|
||||
|
|
@ -1402,7 +1402,7 @@ forge-cles: ## Le site enregistre sur le depot de chaque locataire la cle de lec
|
|||
.PHONY: forge-comptes
|
||||
forge-comptes: ## Les comptes de la forge du site : un par mainteneur (plan du site), et le lecteur de la console — CONFIRMER=true pour ecrire
|
||||
@set -e; \
|
||||
voute="$$(dirname "$$(readlink -f underlay.yml)")/underlay.vault.yml"; \
|
||||
voute="$$(dirname "$$(readlink -f "$${SETOPS_UNDERLAY:-underlay.yml}")")/underlay.vault.yml"; \
|
||||
supp=(); [[ -f "$$voute" ]] && supp+=( -e "@$$voute" ); \
|
||||
CONFIRMER="$(CONFIRMER)" ansible-playbook -i scripts/site_inventaire.py \
|
||||
playbooks/maintenance/forge_comptes.yml "$${supp[@]}" $(ARGS)
|
||||
|
|
@ -1482,6 +1482,9 @@ site-creer: ## Cree les machines du site depuis l'underlay — CONFIRMER=true
|
|||
NOEUD_PROXMOX="$$SETOPS_NOEUD" \
|
||||
VMID_MODELE="$${SETOPS_VMID_MODELE:-$(VMID_MODELE)}" \
|
||||
CLONE_COMPLET="$$SETOPS_CLONE_COMPLET" \
|
||||
CLE_SSH_PUBLIQUE="$${SETOPS_CLE_PUBLIQUE:-$(CLE_SSH_PUBLIQUE)}" \
|
||||
DNS="$${SETOPS_DNS:-$(DNS)}" \
|
||||
DOMAINE_RECHERCHE="$${SETOPS_DOMAINE:-$(DOMAINE_RECHERCHE)}" \
|
||||
FORMAT_DISQUE="$(FORMAT_DISQUE)"; \
|
||||
printf '=== %s : %s s ===\n' "$$m" "$$(( $$(date +%s) - t0 ))"; \
|
||||
done; \
|
||||
|
|
@ -1711,7 +1714,7 @@ cloner-vm: ansible-runtime ## Clone une VM depuis le gabarit dore — HOTE=<nom>
|
|||
done; \
|
||||
done; \
|
||||
if [[ -z "$$vault_file" && -L underlay.yml ]]; then \
|
||||
vault_file="$$(dirname "$$(readlink -f underlay.yml)")/underlay.vault.yml"; \
|
||||
vault_file="$$(dirname "$$(readlink -f "$${SETOPS_UNDERLAY:-underlay.yml}")")/underlay.vault.yml"; \
|
||||
fi; \
|
||||
if [[ -n "$$vault_file" && -f "$$vault_file" ]]; then \
|
||||
read -r premiere_ligne < "$$vault_file" || true; \
|
||||
|
|
|
|||
|
|
@ -79,6 +79,18 @@
|
|||
- artefacts_amorcage is defined
|
||||
- artefacts_amorcage | string | length > 0
|
||||
|
||||
# LA MACHINE QUI PORTE LE CACHE VA DROIT CHEZ DEBIAN (2026-10-13). L'inventaire lui
|
||||
# donne `artefacts_amorcage` VIDE (et non absent) : le fichier herite du gabarit vise
|
||||
# un cache qui n'existe pas encore — le sien — ou une adresse perimee. Absent, il
|
||||
# survivrait ; vide, on le retire.
|
||||
- name: Retirer l'amorcage sur la machine qui porte le cache
|
||||
ansible.builtin.file:
|
||||
path: /etc/apt/apt.conf.d/00-setops-amorcage-artefacts
|
||||
state: absent
|
||||
when:
|
||||
- artefacts_amorcage is defined
|
||||
- artefacts_amorcage | string | length == 0
|
||||
|
||||
# RETIRER CE QUI DESIGNE UN CACHE QUI N'EXISTE PLUS (2026-09-10).
|
||||
#
|
||||
# `client_artefacts` ecrit `00-setops-artefacts`, qui trie APRES le plancher et gagne.
|
||||
|
|
|
|||
|
|
@ -95,6 +95,87 @@
|
|||
# depot par depot, et l'exploitant doit aller cliquer. La cible s'en charge : la
|
||||
# declaration est idempotente (422 = deja la), et sa portee est exactement ce qu'on
|
||||
# pousse, jamais plus large.
|
||||
# UNE FORGE NEUVE, UN RUNNER NEUF (2026-10-13, premier site construit depuis zero). Le
|
||||
# runner du site ne fabrique sa cle qu'APRES avoir clone le genome, et il ne peut cloner
|
||||
# que ce que cette commande a pousse — qui exige sa cle. Personne n'allait au bout. On
|
||||
# fabrique donc la cle ici si elle manque, A L'IDENTIQUE de `serveur_ops` (meme commande,
|
||||
# meme commentaire, `creates:`) : le role la retrouvera et ne la refera pas.
|
||||
- name: Assurer le répertoire SSH du runner
|
||||
ansible.builtin.file:
|
||||
path: "{{ genome_racine }}/.ssh"
|
||||
state: directory
|
||||
mode: "0700"
|
||||
|
||||
- name: Fabriquer la clé du runner si elle n'existe pas encore
|
||||
ansible.builtin.command:
|
||||
cmd: >-
|
||||
ssh-keygen -t {{ serveur_ops_cle_type | default('ed25519') }} -N ''
|
||||
-C "setops@{{ inventory_hostname }}.{{ domaine_interne }}"
|
||||
-f {{ genome_racine }}/.ssh/id_{{ serveur_ops_cle_type | default('ed25519') }}
|
||||
creates: "{{ genome_racine }}/.ssh/id_{{ serveur_ops_cle_type | default('ed25519') }}"
|
||||
|
||||
# L'ORGANISATION ET SES DEPOTS N'ETAIENT CREES PAR PERSONNE : au site de Chezlepro, une
|
||||
# fois, a la main. Meme forme qu'elle : organisation PUBLIQUE ; le moteur et les modeles
|
||||
# PUBLICS (le runner les clone en anonyme) ; la carte du site et les plans des
|
||||
# locataires PRIVES (`serveur_ops_roles_prives`, lus en SSH par cle de deploiement).
|
||||
- name: L'organisation du génome existe-t-elle ?
|
||||
ansible.builtin.uri:
|
||||
url: "https://forge.{{ domaine_interne }}/api/v1/orgs/{{ serveur_ops_forge_organisation | default('genome') }}"
|
||||
validate_certs: false
|
||||
status_code: [200, 404]
|
||||
register: genome_org
|
||||
|
||||
- name: Créer l'organisation du génome
|
||||
ansible.builtin.uri:
|
||||
url: "https://forge.{{ domaine_interne }}/api/v1/orgs"
|
||||
method: POST
|
||||
url_username: forgejo-admin
|
||||
url_password: "{{ vault_forgejo_admin }}"
|
||||
force_basic_auth: true
|
||||
validate_certs: false
|
||||
body_format: json
|
||||
body:
|
||||
username: "{{ serveur_ops_forge_organisation | default('genome') }}"
|
||||
visibility: public
|
||||
status_code: 201
|
||||
no_log: true
|
||||
when: genome_org.status == 404
|
||||
|
||||
- name: Quels dépôts poussés existent déjà sur la forge ?
|
||||
ansible.builtin.uri:
|
||||
url: "https://forge.{{ domaine_interne }}/api/v1/repos/{{ serveur_ops_forge_organisation | default('genome') }}/{{ item.depot }}"
|
||||
url_username: forgejo-admin
|
||||
url_password: "{{ vault_forgejo_admin }}"
|
||||
force_basic_auth: true
|
||||
validate_certs: false
|
||||
status_code: [200, 404]
|
||||
loop: "{{ genome_a_pousser }}"
|
||||
loop_control:
|
||||
label: "{{ item.depot }}"
|
||||
no_log: true
|
||||
register: genome_depots_forge
|
||||
|
||||
- name: Créer les dépôts qui manquent
|
||||
ansible.builtin.uri:
|
||||
url: "https://forge.{{ domaine_interne }}/api/v1/orgs/{{ serveur_ops_forge_organisation | default('genome') }}/repos"
|
||||
method: POST
|
||||
url_username: forgejo-admin
|
||||
url_password: "{{ vault_forgejo_admin }}"
|
||||
force_basic_auth: true
|
||||
validate_certs: false
|
||||
body_format: json
|
||||
body:
|
||||
name: "{{ item.item.depot }}"
|
||||
private: "{{ item.item.role in (serveur_ops_roles_prives | default(['instance', 'tenant', 'hebergeur'])) }}"
|
||||
default_branch: "{{ item.item.branche | default('main') }}"
|
||||
auto_init: false
|
||||
status_code: 201
|
||||
loop: "{{ genome_depots_forge.results }}"
|
||||
loop_control:
|
||||
label: "{{ item.item.depot }}"
|
||||
no_log: true
|
||||
when: item.status == 404
|
||||
|
||||
- name: Lire la clé publique du runner
|
||||
ansible.builtin.slurp:
|
||||
src: "{{ genome_racine }}/.ssh/id_{{ serveur_ops_cle_type | default('ed25519') }}.pub"
|
||||
|
|
|
|||
|
|
@ -21,13 +21,17 @@
|
|||
# vit dans le depot de l'hebergeur, que le symlink `underlay.yml` designe deja
|
||||
# (D-14/D-17) : on en DERIVE le chemin plutot que de redeclarer l'hebergeur. Absent
|
||||
# (depot sans underlay monte), le `stat` echoue simplement et rien n'est charge.
|
||||
setops_proxmox_hebergeur: "{{ (playbook_dir ~ '/../../underlay.yml') | realpath | dirname }}/proxmox-hebergeur.yml"
|
||||
# LA CARTE DESIGNEE (2026-10-13) : `SETOPS_UNDERLAY` quand il est pose, le symlink sinon.
|
||||
# Lire le symlink seul aurait clone les machines du site de TechnoLibre sur le cluster
|
||||
# de Chezlepro, avec ses jetons — l'inventaire, lui, suivait deja la variable.
|
||||
setops_carte_underlay: "{{ lookup('ansible.builtin.env', 'SETOPS_UNDERLAY') | default(playbook_dir ~ '/../../underlay.yml', true) }}"
|
||||
setops_proxmox_hebergeur: "{{ setops_carte_underlay | realpath | dirname }}/proxmox-hebergeur.yml"
|
||||
# LA VOUTE DE L'UNDERLAY — les secrets du MONDE PHYSIQUE (jeton d'API du cluster).
|
||||
# Meme derivation que le fichier ci-dessus : le symlink `underlay.yml` designe deja
|
||||
# l'hebergeur, on n'a rien a redeclarer. Depuis le 2026-08-22 ces secrets ne vivent
|
||||
# plus dans la voute d'un tenant : recopies chez chacun, ils ne pouvaient plus etre
|
||||
# revoques isolement (docs/frontiere-physique-virtuel.md).
|
||||
setops_voute_underlay: "{{ (playbook_dir ~ '/../../underlay.yml') | realpath | dirname }}/underlay.vault.yml"
|
||||
setops_voute_underlay: "{{ setops_carte_underlay | realpath | dirname }}/underlay.vault.yml"
|
||||
|
||||
tasks:
|
||||
- name: Verifier les fichiers de variables Proxmox optionnels
|
||||
|
|
|
|||
|
|
@ -22,7 +22,11 @@
|
|||
# vit dans le depot de l'hebergeur, que le symlink `underlay.yml` designe deja
|
||||
# (D-14/D-17) : on en DERIVE le chemin plutot que de redeclarer l'hebergeur. Absent
|
||||
# (depot sans underlay monte), le `stat` echoue simplement et rien n'est charge.
|
||||
setops_proxmox_hebergeur: "{{ (playbook_dir ~ '/../../underlay.yml') | realpath | dirname }}/proxmox-hebergeur.yml"
|
||||
# LA CARTE DESIGNEE (2026-10-13) : `SETOPS_UNDERLAY` quand il est pose, le symlink sinon.
|
||||
# Lire le symlink seul aurait clone les machines du site de TechnoLibre sur le cluster
|
||||
# de Chezlepro, avec ses jetons — l'inventaire, lui, suivait deja la variable.
|
||||
setops_carte_underlay: "{{ lookup('ansible.builtin.env', 'SETOPS_UNDERLAY') | default(playbook_dir ~ '/../../underlay.yml', true) }}"
|
||||
setops_proxmox_hebergeur: "{{ setops_carte_underlay | realpath | dirname }}/proxmox-hebergeur.yml"
|
||||
# cibles_vmid : liste d'entiers, passée en -e (sinon dérivée des hôtes actifs).
|
||||
cibles_vmid: "{{ groups['hotes_actifs'] | map('extract', hostvars, 'proxmox_vmid') | map('int') | list }}"
|
||||
|
||||
|
|
|
|||
|
|
@ -54,10 +54,14 @@
|
|||
|
||||
# UN PASSAGE DEJA PARTI se termine avant nos operations apt, plutot que de se les disputer.
|
||||
# `is-active` rend 0 si l'une au moins des unites tourne.
|
||||
# `check_mode: false` : une LECTURE. Sautee en simulation, elle n'avait jamais de `rc`, la
|
||||
# condition `until` ne se remplissait pas, et `--check` echouait la apres 30 essais sur
|
||||
# chaque machine (mesure le 2026-10-13, premiere simulation du site de TechnoLibre).
|
||||
- name: Attendre la fin d'un passage apt automatique deja en cours
|
||||
ansible.builtin.command:
|
||||
argv: ["systemctl", "is-active", "--quiet", "apt-daily.service", "apt-daily-upgrade.service"]
|
||||
register: common_packages_passage
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
until: common_packages_passage.rc != 0
|
||||
|
|
|
|||
|
|
@ -79,89 +79,6 @@
|
|||
passe deviné : renseigner la clé dans la voûte de cet écosystème.
|
||||
when: serveur_icingaweb2_auth == 'db'
|
||||
|
||||
# LE CLIENT POSTGRESQL DES MODULES `community.postgresql`, SUR CETTE MACHINE (2026-10-03).
|
||||
#
|
||||
# Les requetes ci-dessous s'executent sur l'hote de la vigie, pas sur le serveur de base.
|
||||
# Au site, les deux sont la meme machine : `serveur_postgresql` y avait deja pose
|
||||
# `python3-psycopg2`, et l'absence ne se voyait pas. Chez un locataire, la vigie vit sur
|
||||
# `mon-01` et la base sur `data-sql-01` — premier passage apres que la base est devenue
|
||||
# commune a tous les modes : `No module named 'psycopg2'`, masque par `no_log`.
|
||||
- name: Installer le client PostgreSQL de Python (modules community.postgresql)
|
||||
ansible.builtin.apt:
|
||||
name: python3-psycopg2
|
||||
state: present
|
||||
|
||||
# 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 de la console 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:
|
||||
- not ansible_check_mode
|
||||
|
||||
- name: Charger le schéma de la console (comptes, préférences, migrations)
|
||||
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:
|
||||
- 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 —
|
||||
|
|
@ -259,6 +176,94 @@
|
|||
| map('regex_replace', '^(?:Purg|Remv) (\\S+).*$', '\\1') | list)
|
||||
is subset(serveur_icingaweb2_apache_restes)
|
||||
|
||||
# LA BASE ET LE COMPTE D'AMORCAGE APRES LES PAQUETS (2026-10-13). Ce bloc venait AVANT
|
||||
# l'installation : il lit `/usr/share/icingaweb2/schema/pgsql.schema.sql` et hache par `php`,
|
||||
# deux choses que les paquets apportent. Invisible la ou la console etait deja installee ;
|
||||
# sur la premiere machine neuve (site de TechnoLibre), le schema n'existait pas encore.
|
||||
|
||||
# LE CLIENT POSTGRESQL DES MODULES `community.postgresql`, SUR CETTE MACHINE (2026-10-03).
|
||||
#
|
||||
# Les requetes ci-dessous s'executent sur l'hote de la vigie, pas sur le serveur de base.
|
||||
# Au site, les deux sont la meme machine : `serveur_postgresql` y avait deja pose
|
||||
# `python3-psycopg2`, et l'absence ne se voyait pas. Chez un locataire, la vigie vit sur
|
||||
# `mon-01` et la base sur `data-sql-01` — premier passage apres que la base est devenue
|
||||
# commune a tous les modes : `No module named 'psycopg2'`, masque par `no_log`.
|
||||
- name: Installer le client PostgreSQL de Python (modules community.postgresql)
|
||||
ansible.builtin.apt:
|
||||
name: python3-psycopg2
|
||||
state: present
|
||||
|
||||
# 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 de la console 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:
|
||||
- not ansible_check_mode
|
||||
|
||||
- name: Charger le schéma de la console (comptes, préférences, migrations)
|
||||
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:
|
||||
- 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
|
||||
|
||||
|
||||
- name: Ajouter www-data au groupe icingaweb2 (lecture de la config)
|
||||
ansible.builtin.user:
|
||||
name: www-data
|
||||
|
|
|
|||
|
|
@ -4079,10 +4079,19 @@ def _repli_hotes_du_site(enfants: dict, hv: dict) -> list[str]:
|
|||
fautes.append(f"rechargement : {h} renouvelle son certificat sans recharger "
|
||||
f"nginx — le nouveau reste sur disque, l'ancien est servi")
|
||||
if membres("serveur_artefacts"):
|
||||
porteurs = set(membres("serveur_artefacts"))
|
||||
for h, v in sorted(hv.items()):
|
||||
if (v or {}).get("dns_amorcage") and not (v or {}).get("artefacts_amorcage"):
|
||||
fautes.append(f"jumeaux : {h} recoit `dns_amorcage` sans "
|
||||
f"`artefacts_amorcage` — le gabarit garde son ancien proxy apt")
|
||||
v = v or {}
|
||||
if not v.get("dns_amorcage") or v.get("artefacts_amorcage"):
|
||||
continue
|
||||
# VIDE SUR LA MACHINE QUI PORTE LE CACHE, ET SEULEMENT LA (2026-10-13). Elle ne
|
||||
# peut pas s'amorcer sur un cache qu'elle n'a pas encore : le socle RETIRE alors
|
||||
# le fichier du gabarit (`artefacts_amorcage` defini et vide). Absent, ou vide
|
||||
# ailleurs, c'est toujours le repli silencieux que cette garde existe pour voir.
|
||||
if h in porteurs and "artefacts_amorcage" in v:
|
||||
continue
|
||||
fautes.append(f"jumeaux : {h} recoit `dns_amorcage` sans "
|
||||
f"`artefacts_amorcage` — le gabarit garde son ancien proxy apt")
|
||||
return fautes
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -978,6 +978,16 @@ def inventaire() -> dict:
|
|||
if _frontiere_metriques else [])}
|
||||
if (_fabric_metriques and "serveur_prometheus" in _groupes_de(nom)) else {}),
|
||||
**communes,
|
||||
# LE CACHE NE S'AMORCE PAS SUR LUI-MEME (2026-10-13). Toute machine du site
|
||||
# recevait `artefacts_amorcage` = le cache — y compris le cache, et des le
|
||||
# socle, alors qu'il n'est deploye qu'a la couche `services`. Le premier `apt`
|
||||
# d'un site construit depuis zero aurait echoue partout ; celui de Chezlepro
|
||||
# n'a jamais ete reconstruit, son cache existait deja. VIDE ici, et non absent :
|
||||
# le socle RETIRE alors le fichier du gabarit (qui vise `10.0.33.21`, une adresse
|
||||
# d'avant le renumerotage) et la machine va droit chez Debian, ce que la
|
||||
# frontiere permet au cache du site.
|
||||
**({"artefacts_amorcage": ""}
|
||||
if _cache_site and str(srv.get("ip")) == _cache_site else {}),
|
||||
# En dernier : ce qu'une machine declare d'elle-meme prime sur ce que le
|
||||
# plan declare pour toutes.
|
||||
**(srv.get("variables") or {}),
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ validee, et `make prouver` s'en assure avant.
|
|||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import shlex
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
|
@ -174,6 +175,23 @@ def parametres(nom: str) -> dict[str, str]:
|
|||
# un autre stockage sans que tout le site bascule.
|
||||
p["SETOPS_STOCKAGE"] = str(m.get("stockage") or mat.get("stockage") or "")
|
||||
p["SETOPS_CLONE_COMPLET"] = "true" if mat.get("clone_complet", True) else "false"
|
||||
# CE QUE LE PLAN DU SITE DECLARE DEJA, TRANSMIS AU CLONE (2026-10-13). `site-creer` ne
|
||||
# passait ni cle, ni domaine, ni resolveur : la VM recevait ceux du GABARIT, ne au site de
|
||||
# Chezlepro — sa cle (`royaume chezlepro`) et `searchdomain: chezlepro.ca`. Au site de
|
||||
# TechnoLibre, dont le plan dit `cle_ssh: …technolibre`, les neuf VM seraient nees
|
||||
# FERMEES a Ansible. La cle manque : refus, plutot qu'une VM qu'on ne peut plus joindre.
|
||||
intr = U.lire_plan_site("10-intrants.yml") or {}
|
||||
cle = str(intr.get("cle_ssh") or "").strip()
|
||||
if cle:
|
||||
pub = Path(os.path.expanduser(cle + ".pub"))
|
||||
if not pub.is_file():
|
||||
raise SystemExit(f"machine '{nom}': `cle_ssh: {cle}` au plan du site, mais "
|
||||
f"{pub} est absente — la VM naitrait sans cle qu'Ansible possede")
|
||||
p["SETOPS_CLE_PUBLIQUE"] = str(pub)
|
||||
if str(intr.get("domaine_interne") or "").strip():
|
||||
p["SETOPS_DOMAINE"] = str(intr["domaine_interne"]).strip()
|
||||
if str(intr.get("dns_amorcage") or "").strip():
|
||||
p["SETOPS_DNS"] = str(intr["dns_amorcage"]).strip()
|
||||
return p
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue