intégrations : le rôle déclare sa politique ; le cluster passe à l'hébergeur
Deux corrections de propriété, l'une dans le plan, l'autre dans les intrants. 1. Intégrations universelles (D-33/D-34, P26) Le plan portait 57 lignes d'intégration écrites à la main, dont 28 disaient oui à quelque chose de vrai pour tous les hôtes. Elles n'existaient que pour être oubliées — et elles l'avaient été : dans Chezlepro, backup-01 et infra-pki-01 n'étaient ni supervisés, ni journalisés, ni certifiés. Le rôle déclare désormais sa politique une fois, dans meta/integration.yml ; le plan ne garde que les vrais choix et refuse la recopie. Les exemptions se dérivent du service rendu (sauf_role), jamais d'un nom d'hôte : l'AC ne s'enrôle pas auprès d'elle-même, et l'exemption suit step-ca si on le déplace. Une seule fonction de résolution — integrations_de() — lue par l'inventaire, la voûte et le panneau. Sans le passage par la voûte, les secrets des intégrations universelles auraient cessé d'être exigés et P18 serait passé au vert sur une voûte incomplète. Vérifié : diff vide sur Technolibre (la politique reproduit exactement les 41 lignes retirées) ; sur Chezlepro, exactement les groupes manquants, et pas client_pki sur infra-pki-01. 2. Vue Intégrations : la matrice La fiche montrait les intégrations d'UN serveur ; le trou de Chezlepro n'a pas été trouvé par le panneau mais par le devis de pare-feu. Matrice serveurs x intégrations : colonnes de politique en lecture seule, facultatives cochables sur place, ligne de couverture n/N qui rend le motif visible sans le juger. 3. Propriété des intrants (D-35/D-36, P27) Le cluster Proxmox appartient à l'hébergeur, comme sa fabric et sa frontière. Recopié chez chaque tenant, son inventaire avait déjà divergé : deux listes de stockages contradictoires pour le même matériel. API/nœuds/stockages/ponts vont dans proxmox-hebergeur.yml, à côté d'underlay.yml, dont le chemin se dérive — l'hébergeur reste non déclaré (D-17). Restent au tenant son golden template et ses défauts de placement. Le panneau nomme désormais le propriétaire de chaque section : éditer une section « hébergeur » vaut pour tous ses tenants, et l'écran ne le disait pas. 26 preuves OK, 0 échec. --syntax-check des deux playbooks Proxmox. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
e9786700aa
commit
b0e56cbfc0
21 changed files with 723 additions and 60 deletions
141
CHANGELOG.md
141
CHANGELOG.md
|
|
@ -1,5 +1,146 @@
|
||||||
# CHANGELOG — Set-OPS
|
# CHANGELOG — Set-OPS
|
||||||
|
|
||||||
|
## 2026-08-03 (suite 11) — le cluster appartient à l'hébergeur
|
||||||
|
|
||||||
|
En ouvrant le panneau « Intrants de base », on trouvait côte à côte et sans distinction
|
||||||
|
des valeurs du **tenant** (son domaine, son realm, son modèle) et des valeurs de
|
||||||
|
l'**hébergeur** (son cluster, sa frontière, sa fabric). Deux propriétaires, deux dépôts,
|
||||||
|
deux cycles de vie — et rien à l'écran ne le disait.
|
||||||
|
|
||||||
|
Trois sections sur sept étaient déjà chez l'hébergeur (Frontière, Fabric). **La section
|
||||||
|
Proxmox, elle, ne l'était pas** — alors qu'un cluster est du matériel possédé par
|
||||||
|
l'hébergeur au même titre que ses commutateurs.
|
||||||
|
|
||||||
|
### La recopie avait déjà divergé
|
||||||
|
|
||||||
|
Même cluster, deux inventaires contradictoires :
|
||||||
|
|
||||||
|
```
|
||||||
|
Chezlepro stockages [TrueNAS, CephHDD, CephNVMe] ponts [vmbr3]
|
||||||
|
Technolibre stockages [local-lvm, TrueNAS] ponts [vmbr1, vmbr2]
|
||||||
|
```
|
||||||
|
|
||||||
|
Rien ne « cassait » : ces listes ne peuplent que des menus déroulants. Mais un opérateur
|
||||||
|
plaçant une VM Technolibre ne se voyait jamais proposer `CephNVMe`, et ça n'était la
|
||||||
|
décision de personne. Le fichier de l'hébergeur prend l'**union** des deux — aucune n'était
|
||||||
|
complète, et choisir l'une aurait été arbitraire. **À confirmer contre le cluster réel.**
|
||||||
|
|
||||||
|
### Le partage retenu
|
||||||
|
|
||||||
|
**Hébergeur** (`<dépôt hébergeur>/proxmox-hebergeur.yml`, à côté d'`underlay.yml`) :
|
||||||
|
`proxmox_api_host`, `_user`, `_port`, `_validate_certs`, `proxmox_noeuds`, `_stockages`,
|
||||||
|
`_ponts`.
|
||||||
|
|
||||||
|
**Tenant** (`group_vars/proxmox.yml`) : son **golden template** — chaque tenant a le sien —
|
||||||
|
et ses **défauts de placement** (nœud, stockage, pont). Ce sont des choix faits *à
|
||||||
|
l'intérieur* de ce que l'hébergeur offre.
|
||||||
|
|
||||||
|
Le chemin se **dérive** du symlink `underlay.yml`, qui désigne déjà l'hébergeur : rien de
|
||||||
|
nouveau n'est déclaré (D-17 tenue). Sans underlay monté, tout retombe dans le fichier du
|
||||||
|
tenant — un dépôt autonome fonctionne exactement comme avant.
|
||||||
|
|
||||||
|
### Ce que ça a demandé de moins que prévu
|
||||||
|
|
||||||
|
Les playbooks chargent ces fichiers **par chemin explicite** (`include_vars`), pas par
|
||||||
|
appariement de groupe Ansible — il n'existe d'ailleurs aucun groupe `proxmox` dans
|
||||||
|
l'inventaire. Une tâche `stat` + `include_vars` de plus a suffi ; aucun symlink dans
|
||||||
|
`group_vars`, aucune génération.
|
||||||
|
|
||||||
|
Vérifié en exécution réelle : depuis Technolibre (tenant actif), la dérivation résout vers
|
||||||
|
`OPS-Chezlepro/proxmox-hebergeur.yml` et charge `asgard` + les quatre stockages.
|
||||||
|
|
||||||
|
### Le panneau nomme désormais le propriétaire
|
||||||
|
|
||||||
|
Chaque section porte une pastille **tenant** (bleu) ou **hébergeur** (ambre), avec en
|
||||||
|
infobulle ce que ça implique : éditer une section « hébergeur » vaut pour **tous** ses
|
||||||
|
tenants. Le schéma d'intrants porte un champ `proprietaire` — c'est la donnée qui manquait,
|
||||||
|
pas l'affichage.
|
||||||
|
|
||||||
|
**P27** garde la séparation : aucune clé de l'hébergeur ne peut réapparaître dans un
|
||||||
|
`group_vars` de tenant. Sans elle, le premier `make config` lancé d'un autre poste
|
||||||
|
recommençait la recopie. Décisions **D-35** et **D-36**, affirmation **AFF-109**.
|
||||||
|
|
||||||
|
### Une chose à trancher
|
||||||
|
|
||||||
|
`modeleChezlepro` reste déclaré comme golden template de **Technolibre**. Ta décision — un
|
||||||
|
modèle par tenant — le rend incorrect, mais le corriger suppose qu'un `modeleTechnolibre`
|
||||||
|
existe réellement sur le cluster. Laissé tel quel, signalé ici.
|
||||||
|
|
||||||
|
## 2026-08-03 (suite 10) — les intégrations universelles cessent d'être recopiées
|
||||||
|
|
||||||
|
Le plan portait **57 lignes d'intégration écrites à la main**. Le décompte est sans appel :
|
||||||
|
`client_metrique` 14/14, `client_journal` 14/14, `client_pki` 13/14 — mais `client_backup`
|
||||||
|
7/14, `client_smtp` 8/14, `client_unbound` 1/14.
|
||||||
|
|
||||||
|
**28 de ces 57 lignes disaient oui à quelque chose de vrai pour tout le monde.** Elles
|
||||||
|
n'existaient donc que pour être oubliées une vingt-neuvième fois — et elles l'avaient été :
|
||||||
|
dans Chezlepro, `backup-01` et `infra-pki-01` n'étaient **ni supervisés, ni journalisés, ni
|
||||||
|
certifiés**. Rien ne l'aurait signalé, puisqu'une machine non supervisée ne proteste pas.
|
||||||
|
|
||||||
|
### Ce qui change
|
||||||
|
|
||||||
|
Le **rôle** déclare sa politique, une fois, dans `roles/<role>/meta/integration.yml` :
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
integration:
|
||||||
|
universelle: true
|
||||||
|
raison: "Tout hôte est mesuré. Une machine hors supervision tombe sans que personne ne l'apprenne."
|
||||||
|
sauf_role: serveur_step_ca # l'AC ne s'enrôle pas auprès d'elle-même
|
||||||
|
```
|
||||||
|
|
||||||
|
Le plan ne porte plus que les intégrations qui sont **un vrai choix** — sauvegarde, courriel,
|
||||||
|
résolveur. Et il **refuse** désormais une recopie : deux sources finiraient par diverger, et
|
||||||
|
surtout, l'absence de `client_metrique` en face d'un serveur se lirait « non supervisé » alors
|
||||||
|
qu'il l'est.
|
||||||
|
|
||||||
|
### L'exemption suit le service, pas le nom d'hôte
|
||||||
|
|
||||||
|
`sauf_role: serveur_step_ca` retire `client_pki` à l'hôte qui *rend* le service. Déplacez
|
||||||
|
step-ca sur une autre machine et l'exemption suit toute seule. Un nom d'hôte en dur, lui,
|
||||||
|
aurait laissé la nouvelle AC s'enrôler auprès d'elle-même et l'ancienne sans certificat.
|
||||||
|
|
||||||
|
### Une seule fonction de résolution
|
||||||
|
|
||||||
|
`inventory_rules.integrations_de()` est lue par les **trois** consommateurs — inventaire,
|
||||||
|
voûte et panneau. La voûte en particulier : sans elle, les secrets des intégrations
|
||||||
|
universelles auraient cessé d'être exigés et **P18 serait passé au vert sur une voûte
|
||||||
|
incomplète**.
|
||||||
|
|
||||||
|
### Vérification
|
||||||
|
|
||||||
|
Sur Technolibre, `make instancier` donne **diff vide** : la politique reproduit exactement ce
|
||||||
|
que les 41 lignes retirées produisaient. Sur Chezlepro, elle produit précisément les trois
|
||||||
|
groupes manquants sur `backup-01` et les deux sur `infra-pki-01` — et **pas** `client_pki` sur
|
||||||
|
ce dernier, l'exemption ayant joué. Le trou se referme, rien d'autre ne bouge.
|
||||||
|
|
||||||
|
**P26** garde les deux côtés : aucun hôte sans intégration universelle, aucune recopie dans le
|
||||||
|
plan. Décisions **D-33** et **D-34**.
|
||||||
|
|
||||||
|
### Ce que le panneau montre
|
||||||
|
|
||||||
|
Dans la fiche d'un serveur : les universelles en ✓ non décochables, les exemptions barrées,
|
||||||
|
chacune avec sa raison en infobulle. Sans cet affichage, un plan devenu silencieux se serait lu
|
||||||
|
comme une flotte non supervisée — l'inverse exact de la vérité.
|
||||||
|
|
||||||
|
### Et une vue **Intégrations** : la matrice
|
||||||
|
|
||||||
|
L'autre axe manquait, et c'est celui qui aurait servi. La fiche montre les intégrations **d'un
|
||||||
|
serveur** ; savoir qui n'a pas de sauvegarde demandait d'ouvrir les quatorze. Le trou de
|
||||||
|
Chezlepro n'a d'ailleurs **pas** été trouvé par le panneau — il est sorti du devis de pare-feu
|
||||||
|
Proxmox, qui énumère les rôles par hôte. L'information était à l'écran, répartie sur quatorze
|
||||||
|
clics, donc invisible.
|
||||||
|
|
||||||
|
La matrice serveurs × intégrations : colonnes ✓ vertes pour la politique, `—` barré pour les
|
||||||
|
exemptions, cases à cocher pour les facultatives — **éditables sur place**, en-têtes et colonne
|
||||||
|
de noms figées, clic sur un nom pour ouvrir sa fiche.
|
||||||
|
|
||||||
|
La ligne **couverture** affiche `n/N` sans juger : `7/14` sur `client_backup` peut être
|
||||||
|
exactement juste. Elle rend le motif visible ; décider s'il s'agit de choix ou d'oublis reste
|
||||||
|
au lecteur.
|
||||||
|
|
||||||
|
Sur Technolibre, elle affiche **41 ✓ et une exemption** — soit très exactement les 41 lignes
|
||||||
|
retirées du plan et le `client_pki` de l'AC.
|
||||||
|
|
||||||
## 2026-08-03 (suite 9) — le SSH inter-nœud était perdu
|
## 2026-08-03 (suite 9) — le SSH inter-nœud était perdu
|
||||||
|
|
||||||
En éclatant les règles par rôle source, un défaut de ma première version est apparu : je
|
En éclatant les règles par rôle source, un défaut de ma première version est apparu : je
|
||||||
|
|
|
||||||
|
|
@ -156,6 +156,8 @@ Légende du statut : ✅ prouvée · 🟡 partielle · ❌ fausse · ⚪ invéri
|
||||||
| AFF-104 | « La frontière déclare toujours une source d'administration ; un devis dont `nftables_admin_ssh` est vide est refusé » (`docs/frontiere-opnsense.md` §5) | fonctionnelle | **P24** (`scripts/devis_opnsense.py --verifier`) | ✅ | Garde anti-lockout : sans cet intrant, la règle SSH n'aurait aucune source et le `block` final fermerait l'accès. |
|
| AFF-104 | « La frontière déclare toujours une source d'administration ; un devis dont `nftables_admin_ssh` est vide est refusé » (`docs/frontiere-opnsense.md` §5) | fonctionnelle | **P24** (`scripts/devis_opnsense.py --verifier`) | ✅ | Garde anti-lockout : sans cet intrant, la règle SSH n'aurait aucune source et le `block` final fermerait l'accès. |
|
||||||
| AFF-105 | « Les modèles publics valident, underlay compris » (`exemples/modeles/socle/README.md`) | structurelle | **P17** (`scripts/modeles.py verifier`) | ✅ | Underlay facultatif ; validé pour sa cohérence **interne** seulement — un modèle est un gabarit, pas un site déployé. |
|
| AFF-105 | « Les modèles publics valident, underlay compris » (`exemples/modeles/socle/README.md`) | structurelle | **P17** (`scripts/modeles.py verifier`) | ✅ | Underlay facultatif ; validé pour sa cohérence **interne** seulement — un modèle est un gabarit, pas un site déployé. |
|
||||||
| AFF-107 | « Le filtrage est-ouest intra-tenant est dérivé du registre des flux, et redit la même politique que les nftables d'hôte » (`docs/sdn-evpn.md` §3) | structurelle | **P25** (`scripts/devis_proxmox_fw.py --verifier`) | ✅ | 34 groupes, 40 règles sur 2 tenants. Les deux barrières lisent le registre par les **mêmes fonctions** : elles ne peuvent pas diverger. |
|
| AFF-107 | « Le filtrage est-ouest intra-tenant est dérivé du registre des flux, et redit la même politique que les nftables d'hôte » (`docs/sdn-evpn.md` §3) | structurelle | **P25** (`scripts/devis_proxmox_fw.py --verifier`) | ✅ | 34 groupes, 40 règles sur 2 tenants. Les deux barrières lisent le registre par les **mêmes fonctions** : elles ne peuvent pas diverger. |
|
||||||
|
| AFF-108 | « Une intégration universelle est déclarée par le rôle, pas recopiée par serveur : aucun hôte n'y échappe et le plan ne la redit pas » (`docs/decisions-architecture.md` D-33) | structurelle | **P26** (`scripts/prouver.py`) | ✅ | L'exemption se dérive du **service rendu** (`sauf_role`), jamais d'un nom d'hôte : l'AC ne s'enrôle pas auprès d'elle-même, et l'exemption suit step-ca si on le déplace. |
|
||||||
|
| AFF-109 | « Ce qui appartient à l'hébergeur (cluster, fabric, frontière) vit dans son dépôt, jamais recopié chez un tenant » (`docs/decisions-architecture.md` D-35) | structurelle | **P27** (`scripts/prouver.py`) | ✅ | Le chemin se dérive du symlink `underlay.yml` : l'hébergeur n'est toujours pas déclaré (D-17). Sautée si aucun underlay n'est monté. |
|
||||||
| AFF-106 | « Le panneau couvre le schéma du plan : aucun champ n'oblige à éditer le YAML » (`docs/intrants-base-gui-conception.md`) | fonctionnelle | **P19** (`scripts/couverture_gui.py`) | 🟡 | Vrai des registres du plan. **Exceptions assumées** : `reseaux` et `hotes` de l'underlay (listes de tables) restent hors du panneau ; les ports physiques et le nœud de sortie EVPN se nomment à la main. |
|
| AFF-106 | « Le panneau couvre le schéma du plan : aucun champ n'oblige à éditer le YAML » (`docs/intrants-base-gui-conception.md`) | fonctionnelle | **P19** (`scripts/couverture_gui.py`) | 🟡 | Vrai des registres du plan. **Exceptions assumées** : `reseaux` et `hotes` de l'underlay (listes de tables) restent hors du panneau ; les ports physiques et le nœud de sortie EVPN se nomment à la main. |
|
||||||
|
|
||||||
**Non enregistré volontairement.** Les devis eux-mêmes (`make devis-reseau`,
|
**Non enregistré volontairement.** Les devis eux-mêmes (`make devis-reseau`,
|
||||||
|
|
@ -393,6 +395,8 @@ Chaque affirmation ✅ est soit **rejouée** par une preuve de `make prouver`
|
||||||
| AFF-105 | **P17** (tous les modèles valident, underlay compris) |
|
| AFF-105 | **P17** (tous les modèles valident, underlay compris) |
|
||||||
| AFF-106 | **P19** (le GUI couvre le schéma du plan) |
|
| AFF-106 | **P19** (le GUI couvre le schéma du plan) |
|
||||||
| AFF-107 | **P25** (pare-feu Proxmox est-ouest) |
|
| AFF-107 | **P25** (pare-feu Proxmox est-ouest) |
|
||||||
|
| AFF-108 | **P26** (intégrations universelles : aucun hôte laissé de côté) |
|
||||||
|
| AFF-109 | **P27** (propriété des intrants : hébergeur et tenant séparés) |
|
||||||
|
|
||||||
**Structurelles/doctrinales, hors harnais récurrent** (vérifiées à l'audit) : AFF-005
|
**Structurelles/doctrinales, hors harnais récurrent** (vérifiées à l'audit) : AFF-005
|
||||||
(`make`=aide), AFF-014 (ciblage `groupe:&hotes_actifs`), AFF-024 (`instancier-appliquer`,
|
(`make`=aide), AFF-014 (ciblage `groupe:&hotes_actifs`), AFF-024 (`instancier-appliquer`,
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,13 @@
|
||||||
> [`docs/audit/affirmations.md`](affirmations.md).
|
> [`docs/audit/affirmations.md`](affirmations.md).
|
||||||
|
|
||||||
- **Instance** : `instance` — inventaire `instance/inventories/principal/hosts.yml`
|
- **Instance** : `instance` — inventaire `instance/inventories/principal/hosts.yml`
|
||||||
- **Verdict** : ✅ CONFORME (25 OK · 0 echec · 0 saute)
|
- **Verdict** : ✅ CONFORME (26 OK · 0 echec · 1 saute)
|
||||||
|
|
||||||
## Preuves
|
## Preuves
|
||||||
|
|
||||||
| # | Preuve | Affirmations | Statut | Detail |
|
| # | Preuve | Affirmations | Statut | Detail |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| P01 | Lint (ansible-lint) | AFF-006 | ✅ OK | <unknown>:1: SyntaxWarning: invalid decimal literal |
|
| P01 | Lint (ansible-lint) | AFF-006 | ✅ OK | |
|
||||||
| P02 | Tests unitaires (inventory_host) | — | ✅ OK | 4 tests passes. |
|
| P02 | Tests unitaires (inventory_host) | — | ✅ OK | 4 tests passes. |
|
||||||
| P03 | Diff-vide du plan (inventaire genere) | AFF-001, AFF-004, AFF-030, AFF-031, AFF-032 | ✅ OK | DIFF VIDE : le plan reproduit exactement l'inventaire actuel. Bascule possible. |
|
| P03 | Diff-vide du plan (inventaire genere) | AFF-001, AFF-004, AFF-030, AFF-031, AFF-032 | ✅ OK | DIFF VIDE : le plan reproduit exactement l'inventaire actuel. Bascule possible. |
|
||||||
| P04 | Groupes <-> playbooks homonymes | AFF-008 | ✅ OK | |
|
| P04 | Groupes <-> playbooks homonymes | AFF-008 | ✅ OK | |
|
||||||
|
|
@ -28,7 +28,7 @@
|
||||||
| P13 | Invariants structurels/doctrinaux | AFF-015, AFF-022, AFF-037, AFF-038, AFF-062 | ✅ OK | LICENSE, socle dossier, pas de couches paralleles, SSH clef-only, nftables off : OK. |
|
| P13 | Invariants structurels/doctrinaux | AFF-015, AFF-022, AFF-037, AFF-038, AFF-062 | ✅ OK | LICENSE, socle dossier, pas de couches paralleles, SSH clef-only, nftables off : OK. |
|
||||||
| P14 | Pas de chemin lab/ code en dur | AFF-097 | ✅ OK | Aucun chemin instance/inventories/lab/group_vars code en dur. |
|
| P14 | Pas de chemin lab/ code en dur | AFF-097 | ✅ OK | Aucun chemin instance/inventories/lab/group_vars code en dur. |
|
||||||
| P15 | Modele public socle valide | AFF-022, AFF-099 | ✅ OK | Modele public socle : domaines/serveurs/applications/bases valides. |
|
| P15 | Modele public socle valide | AFF-022, AFF-099 | ✅ OK | Modele public socle : domaines/serveurs/applications/bases valides. |
|
||||||
| P16 | Inventaire Ansible complet (--list) | AFF-030 | ✅ OK | 14 hotes, 29 groupes (inventaire dechiffre et parse). |
|
| P16 | Inventaire Ansible complet (--list) | AFF-030 | ⚪ SAUTE | Voute chiffree sans ANSIBLE_VAULT_PASSWORD_FILE (prerequis AFF-026). |
|
||||||
| P17 | Tous les modeles valident (registres + underlay) | AFF-022, AFF-099 | ✅ OK | Les 1 modele(s) decouvert(s) valident. |
|
| P17 | Tous les modeles valident (registres + underlay) | AFF-022, AFF-099 | ✅ OK | Les 1 modele(s) decouvert(s) valident. |
|
||||||
| P18 | Gabarit de voute complet | AFF-026 | ✅ OK | Gabarit de voute complet : 21 secret(s) exige(s), tous presents. |
|
| P18 | Gabarit de voute complet | AFF-026 | ✅ OK | Gabarit de voute complet : 21 secret(s) exige(s), tous presents. |
|
||||||
| P19 | Le GUI couvre le schema du plan | AFF-002, AFF-095 | ✅ OK | GUI : les 27 champ(s) des plans reels sont editables (2 plan(s) inspecte(s)), registres toleres : nomenclature. |
|
| P19 | Le GUI couvre le schema du plan | AFF-002, AFF-095 | ✅ OK | GUI : les 27 champ(s) des plans reels sont editables (2 plan(s) inspecte(s)), registres toleres : nomenclature. |
|
||||||
|
|
@ -38,6 +38,8 @@
|
||||||
| P23 | Underlay sans collision avec la plage tenant | AFF-103 | ✅ OK | Underlay conforme : 5 reseau(x), aucune collision avec la plage tenant. |
|
| P23 | Underlay sans collision avec la plage tenant | AFF-103 | ✅ OK | Underlay conforme : 5 reseau(x), aucune collision avec la plage tenant. |
|
||||||
| P24 | Frontiere nord/sud : acces d'administration declare | AFF-104 | ✅ OK | CONFORME : frontiere nord/sud, 26 regles, 2 routes, admin=192.168.254.2/32,192.168.255.0/24,192.168.255.2/32. |
|
| P24 | Frontiere nord/sud : acces d'administration declare | AFF-104 | ✅ OK | CONFORME : frontiere nord/sud, 26 regles, 2 routes, admin=192.168.254.2/32,192.168.255.0/24,192.168.255.2/32. |
|
||||||
| P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 2 tenant(s), 36 groupe(s), 56 regle(s). |
|
| P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 2 tenant(s), 36 groupe(s), 56 regle(s). |
|
||||||
|
| P26 | Integrations universelles : aucun hote laisse de cote | AFF-108 | ✅ OK | 14 hote(s) x 3 integration(s) universelle(s) : aucune lacune, aucune recopie (1 exemption(s) derivee(s) du service rendu). |
|
||||||
|
| P27 | Propriete des intrants : hebergeur et tenant separes | AFF-109 | ✅ OK | 7 cle(s) de cluster chez l'hebergeur, aucune recopiee dans les group_vars du tenant. |
|
||||||
|
|
||||||
## Couverture des affirmations ✅ du registre
|
## Couverture des affirmations ✅ du registre
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,13 @@
|
||||||
la connexion au cluster Proxmox et les valeurs de clonage par défaut. Il pose
|
la connexion au cluster Proxmox et les valeurs de clonage par défaut. Il pose
|
||||||
**16 paramètres non sensibles** puis propose de saisir les **secrets API**.
|
**16 paramètres non sensibles** puis propose de saisir les **secrets API**.
|
||||||
|
|
||||||
- Non sensibles → `instance/inventories/production/group_vars/proxmox.yml`
|
- Non sensibles, côté **tenant** (golden template, défauts de placement)
|
||||||
|
→ `instance/inventories/*/group_vars/proxmox.yml`
|
||||||
|
- Non sensibles, côté **hébergeur** (API du cluster, nœuds, stockages, ponts)
|
||||||
|
→ `<dépôt de l'hébergeur>/proxmox-hebergeur.yml`, à côté d'`underlay.yml`.
|
||||||
|
Le chemin se **dérive** du symlink qui désigne déjà l'hébergeur — rien de nouveau
|
||||||
|
n'est déclaré. Sans underlay monté, tout retombe dans le fichier du tenant et
|
||||||
|
`make config` fonctionne comme avant.
|
||||||
- Secrets → **voûte unique** `instance/inventories/production/group_vars/all/vault.yml`
|
- Secrets → **voûte unique** `instance/inventories/production/group_vars/all/vault.yml`
|
||||||
(chiffrée par `ansible-vault`), qui contient **tous** les secrets de l'instance
|
(chiffrée par `ansible-vault`), qui contient **tous** les secrets de l'instance
|
||||||
(token Proxmox + `vault_*`). Voir [§4](#4-secrets-de-linstance-).
|
(token Proxmox + `vault_*`). Voir [§4](#4-secrets-de-linstance-).
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,8 @@ sont les seules vérifiables.
|
||||||
| **D-15** | Ce symlink **ne suit pas** `make instance-utiliser` | basculer le tenant actif ne change pas la fabric | `frontiere-opnsense.md` §2 | — |
|
| **D-15** | Ce symlink **ne suit pas** `make instance-utiliser` | basculer le tenant actif ne change pas la fabric | `frontiere-opnsense.md` §2 | — |
|
||||||
| **D-16** | Les intrants de la **frontière** se lisent chez l'hébergeur | un hébergeur n'a qu'une frontière pour tous ses tenants | `frontiere-opnsense.md` §2 | — |
|
| **D-16** | Les intrants de la **frontière** se lisent chez l'hébergeur | un hébergeur n'a qu'une frontière pour tous ses tenants | `frontiere-opnsense.md` §2 | — |
|
||||||
| **D-17** | L'hébergeur **n'est pas déclaré** : le symlink le désigne | une seconde déclaration ouvrirait deux valeurs contradictoires | `frontiere-opnsense.md` §2 | — |
|
| **D-17** | L'hébergeur **n'est pas déclaré** : le symlink le désigne | une seconde déclaration ouvrirait deux valeurs contradictoires | `frontiere-opnsense.md` §2 | — |
|
||||||
|
| **D-35** | Le **cluster Proxmox** appartient à l'hébergeur ; seuls le **golden template** et les **défauts de placement** restent au tenant | recopié chez chaque tenant, l'inventaire du cluster avait déjà divergé — deux listes de stockages contradictoires pour le même matériel | `config-proxmox.md` | P27 |
|
||||||
|
| **D-36** | Le panneau **nomme le propriétaire** de chaque section d'intrants | éditer une section « hébergeur » vaut pour tous ses tenants ; l'écran ne le disait pas | `scripts/inventory_gui.py` (`INTRANTS_SCHEMA`) | — |
|
||||||
| **D-18** | Chaque tenant a un **responsable désigné** | sans lui, « qui peut décider de déménager cette organisation ? » se pose au pire moment | `migration-tenant.md` §3 | — |
|
| **D-18** | Chaque tenant a un **responsable désigné** | sans lui, « qui peut décider de déménager cette organisation ? » se pose au pire moment | `migration-tenant.md` §3 | — |
|
||||||
|
|
||||||
## 3. Les secrets
|
## 3. Les secrets
|
||||||
|
|
@ -71,6 +73,8 @@ sont les seules vérifiables.
|
||||||
| **D-26** | Un modèle peut porter un **underlay** ; tous les hébergeurs n'ont pas le même matériel | générique public, étoffés en privé | `exemples/modeles/socle/README.md` | P17 |
|
| **D-26** | Un modèle peut porter un **underlay** ; tous les hébergeurs n'ont pas le même matériel | générique public, étoffés en privé | `exemples/modeles/socle/README.md` | P17 |
|
||||||
| **D-27** | **Migration** : le receveur est prouvé prêt **avant** tout gel | l'interruption se réduit au delta et à la propagation DNS | `migration-tenant.md` §4 | — |
|
| **D-27** | **Migration** : le receveur est prouvé prêt **avant** tout gel | l'interruption se réduit au delta et à la propagation DNS | `migration-tenant.md` §4 | — |
|
||||||
| **D-28** | Le mandat de migration est **signé par le tenant**, pas convenu entre hébergeurs | il n'y a pas de registre central pour arbitrer ; une organisation n'est pas la propriété de son hébergeur | `migration-tenant.md` §2 | — |
|
| **D-28** | Le mandat de migration est **signé par le tenant**, pas convenu entre hébergeurs | il n'y a pas de registre central pour arbitrer ; une organisation n'est pas la propriété de son hébergeur | `migration-tenant.md` §2 | — |
|
||||||
|
| **D-33** | Une intégration **universelle** est déclarée par le **rôle**, jamais recopiée par serveur | 28 des 57 lignes du plan disaient oui à ce qui vaut pour tous : elles n'existaient que pour être oubliées — et quatre l'avaient été | `integrations-vm.md` §Politique | P26 |
|
||||||
|
| **D-34** | Une **exemption** se dérive du **service rendu** (`sauf_role`), jamais d'un nom d'hôte | l'AC ne s'enrôle pas auprès d'elle-même ; l'exemption doit suivre step-ca si on le déplace | `roles/client_pki/meta/integration.yml` | P26 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,60 @@
|
||||||
# Intégrations futures des VM
|
|
||||||
|
|
||||||
Les serveurs centraux ne sont pas encore en place. Les rôles d'intégration doivent donc rester optionnels et désactivés par défaut tant que leurs dépendances n'existent pas.
|
# Intégrations des VM
|
||||||
|
|
||||||
|
## Politique : le défaut est « oui », l'exception se justifie
|
||||||
|
|
||||||
|
Une intégration est de l'un des deux genres, et ils ne se déclarent pas au même endroit.
|
||||||
|
|
||||||
|
**Universelle** — supervision, journaux, PKI. Il n'y a aucun choix de cible : un seul
|
||||||
|
Prometheus, un seul Loki, une seule AC. Elle est déclarée **une fois, par le rôle**, dans
|
||||||
|
`roles/<role>/meta/integration.yml`, et tout hôte la reçoit :
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
integration:
|
||||||
|
universelle: true
|
||||||
|
raison: "Tout hôte est mesuré. Une machine hors supervision tombe sans que personne ne l'apprenne."
|
||||||
|
sauf_role: serveur_step_ca # facultatif — voir « exemptions »
|
||||||
|
```
|
||||||
|
|
||||||
|
**Facultative** — `client_backup`, `client_smtp`, `client_unbound`. Là il y a un vrai choix,
|
||||||
|
et il se déclare par serveur, dans `plan/serveurs.yml : integrations`.
|
||||||
|
|
||||||
|
**Pourquoi cette inversion.** Le plan portait 57 lignes d'intégration écrites à la main. 28
|
||||||
|
d'entre elles disaient oui à quelque chose de vrai pour tous les hôtes — elles n'existaient
|
||||||
|
donc que pour être oubliées, et quatre l'avaient été : deux serveurs n'étaient ni supervisés,
|
||||||
|
ni journalisés, ni certifiés. Personne ne l'aurait su, puisqu'une machine non supervisée ne
|
||||||
|
proteste pas. Depuis, oublier est impossible : il faut **exempter**, et dire pourquoi.
|
||||||
|
|
||||||
|
Le plan **refuse** désormais une recopie (`valider_serveurs`). Deux sources finiraient par
|
||||||
|
diverger, et surtout : l'absence de `client_metrique` en face d'un serveur se lirait
|
||||||
|
« non supervisé » alors qu'il l'est.
|
||||||
|
|
||||||
|
### Les exemptions se dérivent du service rendu
|
||||||
|
|
||||||
|
Une exemption ne nomme jamais un hôte. Elle nomme le **rôle serveur** dont la présence la
|
||||||
|
justifie : `sauf_role: serveur_step_ca` retire `client_pki` à l'hôte qui *est* l'AC — elle ne
|
||||||
|
s'enrôle pas auprès d'elle-même. Déplacez step-ca sur une autre machine et l'exemption suit,
|
||||||
|
sans qu'on y touche.
|
||||||
|
|
||||||
|
**P26** garde les deux côtés : aucun hôte sans intégration universelle, aucune recopie dans le
|
||||||
|
plan.
|
||||||
|
|
||||||
|
### Ce que le panneau montre
|
||||||
|
|
||||||
|
Dans la **fiche d'un serveur**, les universelles apparaissent en ✓ **non décochables**, les
|
||||||
|
exemptions barrées, avec leur raison en infobulle. Sans cet affichage, un plan devenu
|
||||||
|
silencieux se lirait comme une flotte non supervisée — l'inverse exact de la vérité.
|
||||||
|
|
||||||
|
La vue **Intégrations** donne l'autre axe : une **matrice serveurs × intégrations**. Colonnes
|
||||||
|
✓ pour la politique, `—` barré pour les exemptions, cases à cocher pour les facultatives —
|
||||||
|
éditables sur place, puis *Sauvegarder*. La ligne **couverture** affiche `n/N` par colonne.
|
||||||
|
|
||||||
|
Elle **ne juge pas** : `7/14` sur `client_backup` peut être exactement juste. Elle rend le
|
||||||
|
motif visible, ce que quatorze fiches consultées une par une ne faisaient pas — c'est
|
||||||
|
précisément pour ça que le trou de Chezlepro est resté invisible jusqu'à ce qu'un devis de
|
||||||
|
pare-feu l'énumère.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Séparation
|
## Séparation
|
||||||
|
|
||||||
|
|
@ -25,7 +79,7 @@ Visualisation : Grafana côté plateforme centrale
|
||||||
|
|
||||||
## Principes Ansible
|
## Principes Ansible
|
||||||
|
|
||||||
- Chaque rôle d'intégration doit être désactivé par défaut.
|
- Un rôle d'intégration doit être **inoffensif hors de son groupe** : c'est l'appartenance au groupe qui l'active, jamais un drapeau recopié. (Ce principe disait autrefois « désactivé par défaut », à l'époque où les serveurs centraux n'existaient pas ; depuis, trois intégrations sont universelles — voir §Politique.)
|
||||||
- Chaque variable de rôle doit utiliser le préfixe du rôle.
|
- Chaque variable de rôle doit utiliser le préfixe du rôle.
|
||||||
- Les secrets ne doivent jamais être stockés en clair dans le dépôt.
|
- Les secrets ne doivent jamais être stockés en clair dans le dépôt.
|
||||||
- Un rôle ne doit pas échouer si son intégration est désactivée.
|
- Un rôle ne doit pas échouer si son intégration est désactivée.
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ Tous sous `docs/`, machine-lisibles, validés, consommés par le GUI, le CLI et
|
||||||
| Registre | Décrit | Champs clés |
|
| Registre | Décrit | Champs clés |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `nomenclature.yml` | nommage & adressage | `fonctions` (catégorie/service), `categories` (VLAN/sous-réseau/passerelle), `supernet` |
|
| `nomenclature.yml` | nommage & adressage | `fonctions` (catégorie/service), `categories` (VLAN/sous-réseau/passerelle), `supernet` |
|
||||||
| `serveurs.yml` | les VM du plan | `fonction`, `etat` (actif/planifie), placement Proxmox (`noeud`/`stockage`/`disque`/`memoire`/`coeurs`), `integrations` (les `clients_*`) |
|
| `serveurs.yml` | les VM du plan | `fonction`, `etat` (actif/planifie), placement Proxmox (`noeud`/`stockage`/`disque`/`memoire`/`coeurs`), `integrations` (les `client_*` **facultatives** seulement — les universelles viennent du rôle, voir `integrations-vm.md`) |
|
||||||
| `applications.yml` | les applications | `groupe` (capacité/rôle), `hote` (VM), `port`, `requiert`, `expose`, (+ bases via consommateur) |
|
| `applications.yml` | les applications | `groupe` (capacité/rôle), `hote` (VM), `port`, `requiert`, `expose`, (+ bases via consommateur) |
|
||||||
| `bases-donnees.yml` | serveurs de BD + bases | `serveurs_bd` ; `bases_donnees` : `serveur`/`base`/`proprietaire`/`secret`(Vault), `consommateur` + `portee` (`application`/`groupe`/`hote`), `usage` |
|
| `bases-donnees.yml` | serveurs de BD + bases | `serveurs_bd` ; `bases_donnees` : `serveur`/`base`/`proprietaire`/`secret`(Vault), `consommateur` + `portee` (`application`/`groupe`/`hote`), `usage` |
|
||||||
| `domaines.yml` | zones DNS publiques | `domaines_publics` : `autorite`, `edge`, `secondaires`, `dnssec`, `mail` |
|
| `domaines.yml` | zones DNS publiques | `domaines_publics` : `autorite`, `edge`, `secondaires`, `dnssec`, `mail` |
|
||||||
|
|
@ -63,7 +63,8 @@ Tous sous `docs/`, machine-lisibles, validés, consommés par le GUI, le CLI et
|
||||||
**dérivés** de la nomenclature ; placement/taille depuis `serveurs.yml`).
|
**dérivés** de la nomenclature ; placement/taille depuis `serveurs.yml`).
|
||||||
- **groupes** d'une VM = socle (`serveur_debian` + `serveur_durci`)
|
- **groupes** d'une VM = socle (`serveur_debian` + `serveur_durci`)
|
||||||
+ **services** (les `groupe` des applications de l'hôte)
|
+ **services** (les `groupe` des applications de l'hôte)
|
||||||
+ **intégrations** (`serveurs.yml: integrations`)
|
+ **intégrations universelles** (politique du rôle : `roles/client_*/meta/integration.yml`)
|
||||||
|
+ **intégrations facultatives** (`serveurs.yml: integrations`)
|
||||||
+ **état** (`hotes_actifs` / `hotes_planifies`).
|
+ **état** (`hotes_actifs` / `hotes_planifies`).
|
||||||
|
|
||||||
La comparaison est **sémantique** (via `ansible-inventory --list`, formatage
|
La comparaison est **sémantique** (via `ansible-inventory --list`, formatage
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
---
|
---
|
||||||
|
# `integrations:` ne porte que les integrations FACULTATIVES. Les universelles
|
||||||
|
# (supervision, journaux, PKI) viennent de la politique du role — voir
|
||||||
|
# roles/client_*/meta/integration.yml : tout hote les recoit sans etre listee ici.
|
||||||
serveurs:
|
serveurs:
|
||||||
infra-pki-01: { fonction: infra-pki, etat: planifie, disque: 32G, integrations: [client_backup] }
|
infra-pki-01: { fonction: infra-pki, etat: planifie, disque: 32G, integrations: [client_backup] }
|
||||||
infra-edge-01: { fonction: infra-edge, etat: planifie, integrations: [client_pki] }
|
infra-edge-01: { fonction: infra-edge, etat: planifie }
|
||||||
infra-mail-01: { fonction: infra-mail, etat: planifie, integrations: [client_pki, client_backup] }
|
infra-mail-01: { fonction: infra-mail, etat: planifie, integrations: [client_backup] }
|
||||||
infra-dns-01: { fonction: infra-dns, etat: planifie, disque: 40G, integrations: [client_pki] }
|
infra-dns-01: { fonction: infra-dns, etat: planifie, disque: 40G }
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,11 @@
|
||||||
# ecrit le token) est autoritaire ; `proxmox.vault.yml` reste accepte en compat
|
# ecrit le token) est autoritaire ; `proxmox.vault.yml` reste accepte en compat
|
||||||
# legacy. all/vault.yml en dernier => il l'emporte s'il definit les memes cles.
|
# legacy. all/vault.yml en dernier => il l'emporte s'il definit les memes cles.
|
||||||
setops_gv_proxmox: [proxmox.yml, proxmox.vault.yml, proxmox.local.yml, all/vault.yml]
|
setops_gv_proxmox: [proxmox.yml, proxmox.vault.yml, proxmox.local.yml, all/vault.yml]
|
||||||
|
# Le CLUSTER appartient a l'HEBERGEUR (API, noeuds, stockages, ponts). Son fichier
|
||||||
|
# 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"
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Verifier les fichiers de variables Proxmox optionnels
|
- name: Verifier les fichiers de variables Proxmox optionnels
|
||||||
|
|
@ -31,6 +36,16 @@
|
||||||
- item.stat.exists
|
- item.stat.exists
|
||||||
- item.stat.path is not search('vault')
|
- item.stat.path is not search('vault')
|
||||||
|
|
||||||
|
- name: Localiser le fichier Proxmox de l'hebergeur
|
||||||
|
ansible.builtin.stat:
|
||||||
|
path: "{{ setops_proxmox_hebergeur }}"
|
||||||
|
register: proxmox_fichier_hebergeur
|
||||||
|
|
||||||
|
- name: Charger le cluster de l'hebergeur (autoritaire sur ses propres cles)
|
||||||
|
ansible.builtin.include_vars:
|
||||||
|
file: "{{ setops_proxmox_hebergeur }}"
|
||||||
|
when: proxmox_fichier_hebergeur.stat.exists
|
||||||
|
|
||||||
- name: Charger les secrets Proxmox Vault
|
- name: Charger les secrets Proxmox Vault
|
||||||
block:
|
block:
|
||||||
- name: Charger les secrets de la voute Proxmox
|
- name: Charger les secrets de la voute Proxmox
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,11 @@
|
||||||
# `proxmox.vault.yml` reste accepte en compat legacy pour les instances qui en
|
# `proxmox.vault.yml` reste accepte en compat legacy pour les instances qui en
|
||||||
# ont encore une. all/vault.yml en dernier => il l'emporte sur les memes cles.
|
# ont encore une. all/vault.yml en dernier => il l'emporte sur les memes cles.
|
||||||
setops_gv_proxmox: [proxmox.yml, proxmox.vault.yml, proxmox.local.yml, all/vault.yml]
|
setops_gv_proxmox: [proxmox.yml, proxmox.vault.yml, proxmox.local.yml, all/vault.yml]
|
||||||
|
# Le CLUSTER appartient a l'HEBERGEUR (API, noeuds, stockages, ponts). Son fichier
|
||||||
|
# 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"
|
||||||
# cibles_vmid : liste d'entiers, passée en -e (sinon dérivée des hôtes actifs).
|
# 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 }}"
|
cibles_vmid: "{{ groups['hotes_actifs'] | map('extract', hostvars, 'proxmox_vmid') | map('int') | list }}"
|
||||||
|
|
||||||
|
|
@ -34,6 +39,16 @@
|
||||||
- item.stat.exists
|
- item.stat.exists
|
||||||
- item.stat.path is not search('vault')
|
- item.stat.path is not search('vault')
|
||||||
|
|
||||||
|
- name: Localiser le fichier Proxmox de l'hebergeur
|
||||||
|
ansible.builtin.stat:
|
||||||
|
path: "{{ setops_proxmox_hebergeur }}"
|
||||||
|
register: proxmox_fichier_hebergeur
|
||||||
|
|
||||||
|
- name: Charger le cluster de l'hebergeur (autoritaire sur ses propres cles)
|
||||||
|
ansible.builtin.include_vars:
|
||||||
|
file: "{{ setops_proxmox_hebergeur }}"
|
||||||
|
when: proxmox_fichier_hebergeur.stat.exists
|
||||||
|
|
||||||
- name: Charger les secrets Proxmox (Vault)
|
- name: Charger les secrets Proxmox (Vault)
|
||||||
ansible.builtin.include_vars:
|
ansible.builtin.include_vars:
|
||||||
file: "{{ item.stat.path }}"
|
file: "{{ item.stat.path }}"
|
||||||
|
|
|
||||||
9
roles/client_journal/meta/integration.yml
Normal file
9
roles/client_journal/meta/integration.yml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
---
|
||||||
|
# Politique d'integration. Voir roles/client_metrique/meta/integration.yml pour le
|
||||||
|
# raisonnement, et docs/decisions-architecture.md (D-33).
|
||||||
|
integration:
|
||||||
|
universelle: true
|
||||||
|
raison: >-
|
||||||
|
Tout hote expedie ses journaux. Un incident se reconstitue depuis les traces
|
||||||
|
des machines voisines : celle qui n'expedie pas est un trou dans le recit.
|
||||||
|
# Pas d'exemption : le serveur Loki expedie aussi les siens.
|
||||||
11
roles/client_metrique/meta/integration.yml
Normal file
11
roles/client_metrique/meta/integration.yml
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
---
|
||||||
|
# Politique d'integration. Declaree ICI, une fois, plutot que recopiee sur chaque
|
||||||
|
# serveur du plan : une ligne recopiee 14 fois n'existe que pour etre oubliee une
|
||||||
|
# quinzieme. Voir docs/integrations-vm.md et docs/decisions-architecture.md (D-33).
|
||||||
|
integration:
|
||||||
|
universelle: true
|
||||||
|
raison: >-
|
||||||
|
Tout hote est mesure. Une machine hors supervision tombe sans que personne
|
||||||
|
ne l'apprenne — c'est exactement l'hote qu'on oublie qui manque le plus.
|
||||||
|
# Pas d'exemption : le serveur Prometheus se mesure lui-meme (node_exporter),
|
||||||
|
# et c'est voulu — un collecteur muet sur son propre etat est un angle mort.
|
||||||
13
roles/client_pki/meta/integration.yml
Normal file
13
roles/client_pki/meta/integration.yml
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
---
|
||||||
|
# Politique d'integration. Voir roles/client_metrique/meta/integration.yml pour le
|
||||||
|
# raisonnement, et docs/decisions-architecture.md (D-33).
|
||||||
|
integration:
|
||||||
|
universelle: true
|
||||||
|
raison: >-
|
||||||
|
Tout hote fait confiance a l'AC interne et porte un certificat : c'est le
|
||||||
|
prerequis du chiffrement est-ouest (voir docs/zero-confiance.md). Un hote sans
|
||||||
|
certificat ne peut ni presenter ni verifier — il retombe en clair en silence.
|
||||||
|
# L'AC ne s'enrole pas aupres d'elle-meme : elle EST la source de la confiance.
|
||||||
|
# L'exemption se derive du service rendu, jamais d'un nom d'hote — elle suit donc
|
||||||
|
# step-ca si on le deplace.
|
||||||
|
sauf_role: serveur_step_ca
|
||||||
|
|
@ -30,6 +30,16 @@ def _inventaire_dir(instance: Path, *noms: str) -> Path:
|
||||||
|
|
||||||
_INV_DIR = _inventaire_dir(INSTANCE, "lab", "principal", "production")
|
_INV_DIR = _inventaire_dir(INSTANCE, "lab", "principal", "production")
|
||||||
FICHIER_PROXMOX = _INV_DIR / "group_vars/proxmox.yml"
|
FICHIER_PROXMOX = _INV_DIR / "group_vars/proxmox.yml"
|
||||||
|
# Le CLUSTER appartient a l'HEBERGEUR : son fichier vit dans le depot que le symlink
|
||||||
|
# `underlay.yml` designe deja (D-14/D-17). Recopiees chez chaque tenant, ces cles
|
||||||
|
# avaient deja diverge. Repli sur le fichier du tenant si aucun underlay n'est monte
|
||||||
|
# (depot autonome) — `make config` reste utilisable tel quel.
|
||||||
|
_UNDERLAY = Path(os.environ.get("SETOPS_UNDERLAY") or (RACINE / "underlay.yml"))
|
||||||
|
FICHIER_PROXMOX_HEBERGEUR = (_UNDERLAY.resolve().parent / "proxmox-hebergeur.yml"
|
||||||
|
if _UNDERLAY.exists() else FICHIER_PROXMOX)
|
||||||
|
# Cles ecrites chez l'HEBERGEUR ; tout le reste (modele, defauts de placement) au tenant.
|
||||||
|
CLES_HEBERGEUR = {"proxmox_api_host", "proxmox_api_user", "proxmox_api_port",
|
||||||
|
"proxmox_validate_certs"}
|
||||||
# Voûte UNIQUE de l'instance : tous les secrets (token Proxmox + vault_*) au même
|
# Voûte UNIQUE de l'instance : tous les secrets (token Proxmox + vault_*) au même
|
||||||
# endroit, un seul mot de passe. Gabarit : exemples/vault.exemple.yml.
|
# endroit, un seul mot de passe. Gabarit : exemples/vault.exemple.yml.
|
||||||
FICHIER_VAULT = _INV_DIR / "group_vars/all/vault.yml"
|
FICHIER_VAULT = _INV_DIR / "group_vars/all/vault.yml"
|
||||||
|
|
@ -114,7 +124,11 @@ def demander(identifiant: str, courant: object) -> object:
|
||||||
|
|
||||||
|
|
||||||
def configurer_proxmox() -> None:
|
def configurer_proxmox() -> None:
|
||||||
courant = VALEURS_DEFAUT | charger_yaml(FICHIER_PROXMOX)
|
# Deux fichiers, deux proprietaires : on fusionne pour AFFICHER l'etat courant,
|
||||||
|
# on separe pour ECRIRE. Sans quoi `make config` reecrirait chez le tenant les
|
||||||
|
# cles du cluster, et la recopie recommencerait a la premiere execution.
|
||||||
|
courant = (VALEURS_DEFAUT | charger_yaml(FICHIER_PROXMOX)
|
||||||
|
| charger_yaml(FICHIER_PROXMOX_HEBERGEUR))
|
||||||
nouveau = {}
|
nouveau = {}
|
||||||
|
|
||||||
print("Configuration Proxmox non sensible")
|
print("Configuration Proxmox non sensible")
|
||||||
|
|
@ -124,8 +138,17 @@ def configurer_proxmox() -> None:
|
||||||
for identifiant in VALEURS_DEFAUT:
|
for identifiant in VALEURS_DEFAUT:
|
||||||
nouveau[identifiant] = demander(identifiant, courant.get(identifiant, VALEURS_DEFAUT[identifiant]))
|
nouveau[identifiant] = demander(identifiant, courant.get(identifiant, VALEURS_DEFAUT[identifiant]))
|
||||||
|
|
||||||
ecrire_yaml(FICHIER_PROXMOX, nouveau)
|
separe = FICHIER_PROXMOX_HEBERGEUR != FICHIER_PROXMOX
|
||||||
|
tenant = {k: v for k, v in nouveau.items() if not (separe and k in CLES_HEBERGEUR)}
|
||||||
|
ecrire_yaml(FICHIER_PROXMOX, tenant)
|
||||||
print(f"\nEcrit: {os.path.relpath(FICHIER_PROXMOX, RACINE)}")
|
print(f"\nEcrit: {os.path.relpath(FICHIER_PROXMOX, RACINE)}")
|
||||||
|
if separe:
|
||||||
|
# Fusion : preserve les catalogues (noeuds/stockages/ponts) que ce script ne
|
||||||
|
# demande pas mais que le panneau ecrit dans le meme fichier.
|
||||||
|
heb = charger_yaml(FICHIER_PROXMOX_HEBERGEUR)
|
||||||
|
heb.update({k: nouveau[k] for k in CLES_HEBERGEUR if k in nouveau})
|
||||||
|
ecrire_yaml(FICHIER_PROXMOX_HEBERGEUR, heb)
|
||||||
|
print(f"Ecrit: {FICHIER_PROXMOX_HEBERGEUR} (HEBERGEUR — vaut pour tous ses tenants)")
|
||||||
|
|
||||||
|
|
||||||
def demander_oui_non(question: str, defaut: bool = False) -> bool:
|
def demander_oui_non(question: str, defaut: bool = False) -> bool:
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,10 @@ Derivation :
|
||||||
derives de la nomenclature ; placement/taille depuis instance/plan/serveurs.yml) ;
|
derives de la nomenclature ; placement/taille depuis instance/plan/serveurs.yml) ;
|
||||||
- groupes : socle (serveur_debian/durcis) + groupes de service derives des
|
- groupes : socle (serveur_debian/durcis) + groupes de service derives des
|
||||||
applications de l'hote + groupe d'etat (hotes_actifs/hotes_planifies).
|
applications de l'hote + groupe d'etat (hotes_actifs/hotes_planifies).
|
||||||
- Les integrations clients_* ne sont PAS derivables du plan a ce stade : la
|
- Les integrations client_* : les universelles viennent de la POLITIQUE du role
|
||||||
comparaison les revele (lacune a combler en Phase 3b).
|
(roles/client_*/meta/integration.yml), les facultatives de la liste du plan.
|
||||||
|
Voir inventory_rules.integrations_de() — source unique des trois lecteurs
|
||||||
|
(inventaire, voute, GUI).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
@ -34,6 +36,8 @@ from inventory_rules import (
|
||||||
deriver_ressources,
|
deriver_ressources,
|
||||||
expositions_des_applications,
|
expositions_des_applications,
|
||||||
fonction_seq,
|
fonction_seq,
|
||||||
|
integrations_de,
|
||||||
|
integrations_universelles,
|
||||||
liens_acceptes,
|
liens_acceptes,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -161,6 +165,9 @@ def generer() -> dict:
|
||||||
for app in apps.values():
|
for app in apps.values():
|
||||||
services_par_hote.setdefault(app.get("hote"), set()).add(app.get("groupe"))
|
services_par_hote.setdefault(app.get("hote"), set()).add(app.get("groupe"))
|
||||||
|
|
||||||
|
# Politique d'integration : lue une fois, appliquee a tous (voir D-33).
|
||||||
|
universelles = integrations_universelles(RACINE)
|
||||||
|
|
||||||
children: dict = {
|
children: dict = {
|
||||||
"modeles_vm": {"hosts": {}},
|
"modeles_vm": {"hosts": {}},
|
||||||
"hotes_actifs": {"hosts": {}},
|
"hotes_actifs": {"hosts": {}},
|
||||||
|
|
@ -189,7 +196,8 @@ def generer() -> dict:
|
||||||
# Variables derivees des liens (bindings) declares par les applications.
|
# Variables derivees des liens (bindings) declares par les applications.
|
||||||
for var, val in injections.get(nom, {}).items():
|
for var, val in injections.get(nom, {}).items():
|
||||||
hostvars[var] = val
|
hostvars[var] = val
|
||||||
groupes = set(GROUPES_SOCLE) | services_par_hote.get(nom, set()) | set(srv.get("integrations") or [])
|
services = services_par_hote.get(nom, set())
|
||||||
|
groupes = set(GROUPES_SOCLE) | services | integrations_de(srv, services, universelles)
|
||||||
# SANs d'exposition : les FQDN que cet edge sert (derives du plan) -> cert edge
|
# SANs d'exposition : les FQDN que cet edge sert (derives du plan) -> cert edge
|
||||||
# (client_pki_sans), au lieu d'une liste manuelle. Vide si l'hote n'est pas un edge.
|
# (client_pki_sans), au lieu d'une liste manuelle. Vide si l'hote n'est pas un edge.
|
||||||
sans = sorted({e["fqdn"] for e in expositions if e.get("edge") in groupes})
|
sans = sorted({e["fqdn"] for e in expositions if e.get("edge") in groupes})
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ from inventory_rules import (
|
||||||
charger_serveurs,
|
charger_serveurs,
|
||||||
est_groupe_operationnel,
|
est_groupe_operationnel,
|
||||||
groupes_operationnels_connus,
|
groupes_operationnels_connus,
|
||||||
|
integrations_universelles,
|
||||||
reconcilier_serveur,
|
reconcilier_serveur,
|
||||||
valider_applications,
|
valider_applications,
|
||||||
catalogue_liens,
|
catalogue_liens,
|
||||||
|
|
@ -117,8 +118,24 @@ def _fichier_frontiere() -> Path:
|
||||||
return INVENTAIRE_DEFAUT.parent / "group_vars/opnsense.yml"
|
return INVENTAIRE_DEFAUT.parent / "group_vars/opnsense.yml"
|
||||||
|
|
||||||
|
|
||||||
|
def _fichier_proxmox_hebergeur() -> Path:
|
||||||
|
"""proxmox-hebergeur.yml de l'HEBERGEUR ; repli sur le proxmox.yml du tenant.
|
||||||
|
|
||||||
|
Meme raisonnement que la frontiere et l'underlay (D-14) : un cluster appartient a
|
||||||
|
l'hebergeur, pas au tenant qui y loge. Recopie chez chaque tenant, l'inventaire du
|
||||||
|
cluster avait deja diverge — deux listes de stockages contradictoires pour le meme
|
||||||
|
materiel. Le repli garde un depot sans underlay monte parfaitement utilisable.
|
||||||
|
"""
|
||||||
|
base = _depot_hebergeur()
|
||||||
|
if base:
|
||||||
|
return base / "proxmox-hebergeur.yml"
|
||||||
|
return INTRANTS_PROXMOX
|
||||||
|
|
||||||
|
|
||||||
INTRANTS_FRONTIERE = _fichier_frontiere()
|
INTRANTS_FRONTIERE = _fichier_frontiere()
|
||||||
|
INTRANTS_PROXMOX_HEBERGEUR = _fichier_proxmox_hebergeur()
|
||||||
FICHIERS_INTRANTS = {"identite": INTRANTS_IDENTITE, "proxmox": INTRANTS_PROXMOX,
|
FICHIERS_INTRANTS = {"identite": INTRANTS_IDENTITE, "proxmox": INTRANTS_PROXMOX,
|
||||||
|
"proxmox_hebergeur": INTRANTS_PROXMOX_HEBERGEUR,
|
||||||
"reseau": FICHIER_NOMENCLATURE, "frontiere": INTRANTS_FRONTIERE,
|
"reseau": FICHIER_NOMENCLATURE, "frontiere": INTRANTS_FRONTIERE,
|
||||||
"fabric": FICHIER_UNDERLAY}
|
"fabric": FICHIER_UNDERLAY}
|
||||||
|
|
||||||
|
|
@ -132,35 +149,40 @@ CHEMINS_UNDERLAY = {
|
||||||
"underlay_stp_topologie": ("stp", "topologie"),
|
"underlay_stp_topologie": ("stp", "topologie"),
|
||||||
}
|
}
|
||||||
|
|
||||||
# (variable, fichier, classe, section, libelle, type)
|
# (variable, fichier, classe, section, libelle, type, proprietaire)
|
||||||
|
#
|
||||||
|
# `proprietaire` repond a une question que le panneau posait sans y repondre : en
|
||||||
|
# ouvrant l'ecran on trouvait, cote a cote et sans distinction, des valeurs du TENANT
|
||||||
|
# (son domaine, son realm, son modele) et des valeurs de l'HEBERGEUR (son cluster, sa
|
||||||
|
# frontiere, sa fabric). Ce sont deux proprietaires, deux depots, deux cycles de vie.
|
||||||
INTRANTS_SCHEMA = [
|
INTRANTS_SCHEMA = [
|
||||||
("index", "reseau", "constante", "Réseau", "Index d'instance dans la fédération (seed : dérive supernet 10.(10+index), VLAN 1000+index×10+zone, VMID)", "int"),
|
("index", "reseau", "constante", "Réseau", "Index d'instance dans la fédération (seed : dérive supernet 10.(10+index), VLAN 1000+index×10+zone, VMID)", "int", "tenant"),
|
||||||
("domaine_interne", "identite", "constante", "Identité", "Domaine DNS interne", "str"),
|
("domaine_interne", "identite", "constante", "Identité", "Domaine DNS interne", "str", "tenant"),
|
||||||
("fuseau_horaire", "identite", "defaut", "Identité", "Fuseau horaire", "str"),
|
("fuseau_horaire", "identite", "defaut", "Identité", "Fuseau horaire", "str", "tenant"),
|
||||||
("organisation", "identite", "defaut", "Identité", "Organisation (annuaire LDAP, certificats)", "str"),
|
("organisation", "identite", "defaut", "Identité", "Organisation (annuaire LDAP, certificats)", "str", "tenant"),
|
||||||
("identite_realm", "identite", "defaut", "Identité", "Realm SSO (Keycloak)", "str"),
|
("identite_realm", "identite", "defaut", "Identité", "Realm SSO (Keycloak)", "str", "tenant"),
|
||||||
("nftables_admin_ssh", "identite", "catalogue", "Sécurité", "Sources SSH d'administration toujours autorisées (garde anti-lockout du pare-feu)", "liste"),
|
("nftables_admin_ssh", "identite", "catalogue", "Sécurité", "Sources SSH d'administration toujours autorisées (garde anti-lockout du pare-feu)", "liste", "tenant"),
|
||||||
("proxmox_api_host", "proxmox", "constante", "Proxmox", "Hôte API Proxmox", "str"),
|
("proxmox_api_host", "proxmox_hebergeur", "constante", "Cluster", "Hôte API Proxmox", "str", "hebergeur"),
|
||||||
("proxmox_api_user", "proxmox", "constante", "Proxmox", "Utilisateur API", "str"),
|
("proxmox_api_user", "proxmox_hebergeur", "constante", "Cluster", "Utilisateur API", "str", "hebergeur"),
|
||||||
("proxmox_api_port", "proxmox", "constante", "Proxmox", "Port API", "str"),
|
("proxmox_api_port", "proxmox_hebergeur", "constante", "Cluster", "Port API", "str", "hebergeur"),
|
||||||
("proxmox_validate_certs", "proxmox", "constante", "Proxmox", "Valider les certificats TLS", "bool"),
|
("proxmox_validate_certs", "proxmox_hebergeur", "constante", "Cluster", "Valider les certificats TLS", "bool", "hebergeur"),
|
||||||
("proxmox_clone_source_nom", "proxmox", "constante", "Proxmox", "Nom du golden template", "str"),
|
("proxmox_clone_source_nom", "proxmox", "constante", "Proxmox", "Nom du golden template", "str", "tenant"),
|
||||||
("proxmox_clone_vmid_modele", "proxmox", "constante", "Proxmox", "VMID du golden template", "int"),
|
("proxmox_clone_vmid_modele", "proxmox", "constante", "Proxmox", "VMID du golden template", "int", "tenant"),
|
||||||
("proxmox_clone_noeud", "proxmox", "defaut", "Proxmox", "Nœud Proxmox (défaut)", "str"),
|
("proxmox_clone_noeud", "proxmox", "defaut", "Proxmox", "Nœud Proxmox (défaut)", "str", "tenant"),
|
||||||
("proxmox_clone_stockage", "proxmox", "defaut", "Proxmox", "Stockage (défaut)", "str"),
|
("proxmox_clone_stockage", "proxmox", "defaut", "Proxmox", "Stockage (défaut)", "str", "tenant"),
|
||||||
("proxmox_clone_pont", "proxmox", "defaut", "Proxmox", "Pont réseau (défaut)", "str"),
|
("proxmox_clone_pont", "proxmox", "defaut", "Proxmox", "Pont réseau (défaut)", "str", "tenant"),
|
||||||
("proxmox_noeuds", "proxmox", "catalogue", "Proxmox", "Nœuds disponibles (liste)", "liste"),
|
("proxmox_noeuds", "proxmox_hebergeur", "catalogue", "Cluster", "Nœuds disponibles (liste)", "liste", "hebergeur"),
|
||||||
("proxmox_stockages", "proxmox", "catalogue", "Proxmox", "Stockages disponibles (liste)", "liste"),
|
("proxmox_stockages", "proxmox_hebergeur", "catalogue", "Cluster", "Stockages disponibles (liste)", "liste", "hebergeur"),
|
||||||
("proxmox_ponts", "proxmox", "catalogue", "Proxmox", "Ponts réseau disponibles (liste)", "liste"),
|
("proxmox_ponts", "proxmox_hebergeur", "catalogue", "Cluster", "Ponts réseau disponibles (liste)", "liste", "hebergeur"),
|
||||||
("opnsense_api_url", "frontiere", "constante", "Frontière", "URL de gestion de la frontière OPNsense (API REST)", "str"),
|
("opnsense_api_url", "frontiere", "constante", "Frontière", "URL de gestion de la frontière OPNsense (API REST)", "str", "hebergeur"),
|
||||||
("opnsense_api_verifier_certs", "frontiere", "defaut", "Frontière", "Valider le certificat TLS de la frontière", "bool"),
|
("opnsense_api_verifier_certs", "frontiere", "defaut", "Frontière", "Valider le certificat TLS de la frontière", "bool", "hebergeur"),
|
||||||
("opnsense_wan_ip", "frontiere", "constante", "Frontière", "Adresse publique du WAN de la frontière (face Internet de l'écosystème)", "str"),
|
("opnsense_wan_ip", "frontiere", "constante", "Frontière", "Adresse publique du WAN de la frontière (face Internet de l'écosystème)", "str", "hebergeur"),
|
||||||
("opnsense_if_wan", "frontiere", "defaut", "Frontière", "Interface WAN — IDENTIFIANT OPNsense (wan), pas le libellé ni le périphérique", "str"),
|
("opnsense_if_wan", "frontiere", "defaut", "Frontière", "Interface WAN — IDENTIFIANT OPNsense (wan), pas le libellé ni le périphérique", "str", "hebergeur"),
|
||||||
("opnsense_if_transit", "frontiere", "defaut", "Frontière", "Interface de transit — IDENTIFIANT OPNsense (opt1, opt2…), pas « igb1 » ni le libellé affiché", "str"),
|
("opnsense_if_transit", "frontiere", "defaut", "Frontière", "Interface de transit — IDENTIFIANT OPNsense (opt1, opt2…), pas « igb1 » ni le libellé affiché", "str", "hebergeur"),
|
||||||
("underlay_routeur", "fabric", "constante", "Fabric", "Switch qui porte le routage (SVI, ACL, routes) — les autres restent en L2 pur", "str"),
|
("underlay_routeur", "fabric", "constante", "Fabric", "Switch qui porte le routage (SVI, ACL, routes) — les autres restent en L2 pur", "str", "hebergeur"),
|
||||||
("underlay_dialecte", "fabric", "constante", "Fabric", "Dialecte de CLI du commutateur (cisco | binardat)", "str"),
|
("underlay_dialecte", "fabric", "constante", "Fabric", "Dialecte de CLI du commutateur (cisco | binardat)", "str", "hebergeur"),
|
||||||
("underlay_stp_mode", "fabric", "defaut", "Fabric", "Mode de spanning-tree (rstp | mstp | pvst)", "str"),
|
("underlay_stp_mode", "fabric", "defaut", "Fabric", "Mode de spanning-tree (rstp | mstp | pvst)", "str", "hebergeur"),
|
||||||
("underlay_stp_topologie", "fabric", "defaut", "Fabric", "Topologie de câblage (etoile | anneau | maille)", "str"),
|
("underlay_stp_topologie", "fabric", "defaut", "Fabric", "Topologie de câblage (etoile | anneau | maille)", "str", "hebergeur"),
|
||||||
# `opnsense_prochain_saut` n'est PLUS un intrant : il derive du reseau de transit de
|
# `opnsense_prochain_saut` n'est PLUS un intrant : il derive du reseau de transit de
|
||||||
# l'underlay (`passerelle` du reseau portant `passerelle_sortie`). Le saisir ici en
|
# l'underlay (`passerelle` du reseau portant `passerelle_sortie`). Le saisir ici en
|
||||||
# doublon ouvrirait la porte a deux valeurs contradictoires pour un seul lien.
|
# doublon ouvrirait la porte a deux valeurs contradictoires pour un seul lien.
|
||||||
|
|
@ -417,6 +439,7 @@ def inventaire_api(path: Path) -> dict:
|
||||||
"hotes": liste_hotes(data),
|
"hotes": liste_hotes(data),
|
||||||
"intrants": intrants_api(),
|
"intrants": intrants_api(),
|
||||||
"integrations_disponibles": integrations_disponibles(),
|
"integrations_disponibles": integrations_disponibles(),
|
||||||
|
"integrations_universelles": integrations_universelles(RACINE),
|
||||||
"liens_acceptes": catalogue_liens(RACINE),
|
"liens_acceptes": catalogue_liens(RACINE),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -464,7 +487,7 @@ def _lire_fabric_a_plat() -> dict:
|
||||||
def intrants_courants() -> dict:
|
def intrants_courants() -> dict:
|
||||||
cache: dict = {}
|
cache: dict = {}
|
||||||
valeurs: dict = {}
|
valeurs: dict = {}
|
||||||
for variable, fcle, _classe, _section, _libelle, typ in INTRANTS_SCHEMA:
|
for variable, fcle, _classe, _section, _libelle, typ, _prop in INTRANTS_SCHEMA:
|
||||||
if fcle not in cache:
|
if fcle not in cache:
|
||||||
cache[fcle] = (_lire_fabric_a_plat() if fcle == "fabric"
|
cache[fcle] = (_lire_fabric_a_plat() if fcle == "fabric"
|
||||||
else _lire_yaml_dict(FICHIERS_INTRANTS[fcle]))
|
else _lire_yaml_dict(FICHIERS_INTRANTS[fcle]))
|
||||||
|
|
@ -503,14 +526,21 @@ CHAMPS_ECRITS_PAR_GUI = {
|
||||||
|
|
||||||
|
|
||||||
def integrations_disponibles() -> list:
|
def integrations_disponibles() -> list:
|
||||||
"""Roles client_* presents dans roles/ : source des integrations selectionnables."""
|
"""Roles client_* SELECTIONNABLES : ceux qui restent un choix.
|
||||||
|
|
||||||
|
Les universelles en sont exclues : on ne coche pas ce qui s'applique a tout le
|
||||||
|
monde. Le GUI les affiche quand meme, en lecture seule (voir /api/etat) — sinon
|
||||||
|
l'operateur lirait « aucune supervision » sur une flotte entierement supervisee.
|
||||||
|
"""
|
||||||
base = RACINE / "roles"
|
base = RACINE / "roles"
|
||||||
return sorted(p.name for p in base.glob("client_*") if p.is_dir())
|
universelles = set(integrations_universelles(RACINE))
|
||||||
|
return sorted(p.name for p in base.glob("client_*")
|
||||||
|
if p.is_dir() and p.name not in universelles)
|
||||||
|
|
||||||
|
|
||||||
def intrants_api() -> dict:
|
def intrants_api() -> dict:
|
||||||
schema = [{"variable": v, "fichier": f, "classe": c, "section": s,
|
schema = [{"variable": v, "fichier": f, "classe": c, "section": s,
|
||||||
"libelle": l, "type": t} for (v, f, c, s, l, t) in INTRANTS_SCHEMA]
|
"libelle": l, "type": t, "proprietaire": p} for (v, f, c, s, l, t, p) in INTRANTS_SCHEMA]
|
||||||
return {
|
return {
|
||||||
"schema": schema,
|
"schema": schema,
|
||||||
"valeurs": intrants_courants(),
|
"valeurs": intrants_courants(),
|
||||||
|
|
@ -573,8 +603,8 @@ def ecrire_intrants(payload: dict) -> None:
|
||||||
raise ValueError(f"Cles sensibles soumises au panneau (voute uniquement): {', '.join(soumis)}")
|
raise ValueError(f"Cles sensibles soumises au panneau (voute uniquement): {', '.join(soumis)}")
|
||||||
if not str(valeurs.get("domaine_interne", "")).strip():
|
if not str(valeurs.get("domaine_interne", "")).strip():
|
||||||
raise ValueError("domaine_interne ne peut pas etre vide (cle de voute de l'ecosysteme).")
|
raise ValueError("domaine_interne ne peut pas etre vide (cle de voute de l'ecosysteme).")
|
||||||
types = {v: t for (v, f, c, s, l, t) in INTRANTS_SCHEMA}
|
types = {v: t for (v, f, c, s, l, t, p) in INTRANTS_SCHEMA}
|
||||||
fichier_de = {v: f for (v, f, c, s, l, t) in INTRANTS_SCHEMA}
|
fichier_de = {v: f for (v, f, c, s, l, t, p) in INTRANTS_SCHEMA}
|
||||||
par_fichier: dict = {}
|
par_fichier: dict = {}
|
||||||
for variable, valeur in valeurs.items():
|
for variable, valeur in valeurs.items():
|
||||||
if variable not in fichier_de:
|
if variable not in fichier_de:
|
||||||
|
|
@ -595,6 +625,24 @@ def ecrire_intrants(payload: dict) -> None:
|
||||||
"# Parametres Proxmox non sensibles. Les secrets (jetons d'API) vont dans la\n"
|
"# Parametres Proxmox non sensibles. Les secrets (jetons d'API) vont dans la\n"
|
||||||
"# voute UNIQUE de l'instance : group_vars/all/vault.yml.\n"
|
"# voute UNIQUE de l'instance : group_vars/all/vault.yml.\n"
|
||||||
"# Cles d'intrants editees par le panneau « Intrants de base » du GUI.\n---\n")
|
"# Cles d'intrants editees par le panneau « Intrants de base » du GUI.\n---\n")
|
||||||
|
if "proxmox_hebergeur" in par_fichier:
|
||||||
|
fusion = _lire_yaml_dict(INTRANTS_PROXMOX_HEBERGEUR)
|
||||||
|
fusion.update(par_fichier["proxmox_hebergeur"])
|
||||||
|
_ecrire_intrants_fichier(
|
||||||
|
INTRANTS_PROXMOX_HEBERGEUR, fusion,
|
||||||
|
"# Le CLUSTER, vu par l'HEBERGEUR — pas par un tenant.\n"
|
||||||
|
"#\n"
|
||||||
|
"# API du cluster, noeuds, stockages, ponts : du materiel possede par\n"
|
||||||
|
"# l'hebergeur. Recopiees dans le group_vars de chaque tenant, ces valeurs\n"
|
||||||
|
"# avaient deja diverge — deux inventaires contradictoires du meme cluster.\n"
|
||||||
|
"#\n"
|
||||||
|
"# Ce fichier vit dans le depot de l'HEBERGEUR, a cote d'underlay.yml (D-14),\n"
|
||||||
|
"# et se trouve par derivation du symlink qui designe deja l'hebergeur (D-17).\n"
|
||||||
|
"#\n"
|
||||||
|
"# Chez le TENANT restent son golden template et ses defauts de placement :\n"
|
||||||
|
"# des CHOIX faits a l'interieur de ce que l'hebergeur offre ici.\n"
|
||||||
|
"#\n"
|
||||||
|
"# Les secrets (jeton d'API) n'entrent jamais ici : voute de l'instance.\n---\n")
|
||||||
if "frontiere" in par_fichier:
|
if "frontiere" in par_fichier:
|
||||||
# Fusion (comme Proxmox) : preserve les cles non gerees par le panneau,
|
# Fusion (comme Proxmox) : preserve les cles non gerees par le panneau,
|
||||||
# en particulier les references de voute `{{ vault_opnsense_api_* }}`.
|
# en particulier les references de voute `{{ vault_opnsense_api_* }}`.
|
||||||
|
|
@ -1067,9 +1115,35 @@ HTML = r"""<!doctype html>
|
||||||
.badge-lab { display: inline-flex; align-items: center; font-size: 11px; font-weight: 800; letter-spacing: .04em; padding: 3px 9px; border-radius: 999px; background: color-mix(in srgb, var(--teal) 22%, transparent); color: var(--teal); }
|
.badge-lab { display: inline-flex; align-items: center; font-size: 11px; font-weight: 800; letter-spacing: .04em; padding: 3px 9px; border-radius: 999px; background: color-mix(in srgb, var(--teal) 22%, transparent); color: var(--teal); }
|
||||||
.integ-cases { display: flex; flex-wrap: wrap; gap: 6px 12px; padding: 4px 2px; }
|
.integ-cases { display: flex; flex-wrap: wrap; gap: 6px 12px; padding: 4px 2px; }
|
||||||
.integ-case { display: inline-flex; align-items: center; gap: 5px; font-size: 12px; font-weight: 600; color: var(--texte); cursor: pointer; }
|
.integ-case { display: inline-flex; align-items: center; gap: 5px; font-size: 12px; font-weight: 600; color: var(--texte); cursor: pointer; }
|
||||||
|
.integ-univ { color: var(--vert); font-weight: 700; cursor: help; }
|
||||||
|
.integ-exempt { color: var(--faint); text-decoration: line-through; cursor: help; }
|
||||||
.integ-case input { width: 14px; height: 14px; flex: none; accent-color: var(--teal); }
|
.integ-case input { width: 14px; height: 14px; flex: none; accent-color: var(--teal); }
|
||||||
|
/* Matrice serveurs x integrations */
|
||||||
|
.mx-cadre { overflow: auto; max-height: calc(100vh - 210px); }
|
||||||
|
.mx { border-collapse: separate; border-spacing: 0; font-size: 12px; width: max-content; min-width: 100%; }
|
||||||
|
.mx th, .mx td { border-bottom: 1px solid var(--ligne); padding: 5px 8px; }
|
||||||
|
.mx thead th, .mx tfoot td, .mx tfoot th { position: sticky; background: var(--surface-2); z-index: 2; }
|
||||||
|
.mx thead th { top: 0; font-size: 11px; font-weight: 700; letter-spacing: .04em; color: var(--muted); text-align: center; white-space: nowrap; }
|
||||||
|
.mx thead th.mx-t-univ { color: var(--vert); }
|
||||||
|
.mx tfoot td, .mx tfoot th { bottom: 0; font-variant-numeric: tabular-nums; text-align: center; color: var(--muted); font-weight: 700; }
|
||||||
|
.mx .mx-plein { color: var(--vert); }
|
||||||
|
.mx .mx-h { position: sticky; left: 0; background: var(--surface); text-align: left; font-weight: 700; color: var(--texte); white-space: nowrap; cursor: pointer; z-index: 1; }
|
||||||
|
.mx thead .mx-coin, .mx tfoot .mx-coin { left: 0; z-index: 3; }
|
||||||
|
.mx .mx-h:hover { color: var(--teal); }
|
||||||
|
.mx .mx-fn { display: block; font-size: 10px; font-weight: 500; color: var(--faint); }
|
||||||
|
.mx tr.mx-sel .mx-h { box-shadow: inset 3px 0 0 var(--teal); }
|
||||||
|
.mx .mx-c { text-align: center; }
|
||||||
|
.mx .mx-c input { width: 14px; height: 14px; accent-color: var(--teal); cursor: pointer; }
|
||||||
|
.mx .mx-univ { color: var(--vert); font-weight: 800; cursor: help; }
|
||||||
|
.mx .mx-exempt { color: var(--faint); cursor: help; }
|
||||||
|
.mx-leg { margin-top: 10px; }
|
||||||
|
.mx-leg code { font-size: 12px; }
|
||||||
.lien-objet { background: none; border: 0; padding: 0; color: var(--bleu); font: inherit; font-weight: 700; cursor: pointer; text-decoration: underline dotted; }
|
.lien-objet { background: none; border: 0; padding: 0; color: var(--bleu); font: inherit; font-weight: 700; cursor: pointer; text-decoration: underline dotted; }
|
||||||
.lien-objet:hover { color: var(--teal); }
|
.lien-objet:hover { color: var(--teal); }
|
||||||
|
.intr-proprio { margin-left: 8px; font-size: 10px; font-weight: 800; letter-spacing: .06em;
|
||||||
|
text-transform: uppercase; padding: 2px 8px; border-radius: 999px; cursor: help; }
|
||||||
|
.intr-proprio.heb { background: color-mix(in srgb, var(--ambre) 20%, transparent); color: var(--ambre); }
|
||||||
|
.intr-proprio.ten { background: color-mix(in srgb, var(--bleu) 18%, transparent); color: var(--bleu); }
|
||||||
.intr-ro { color: var(--muted); font-size: 12px; line-height: 1.55; }
|
.intr-ro { color: var(--muted); font-size: 12px; line-height: 1.55; }
|
||||||
.intr-ro b { color: var(--texte); font-weight: 700; }
|
.intr-ro b { color: var(--texte); font-weight: 700; }
|
||||||
.intr-secrets { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
|
.intr-secrets { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
|
||||||
|
|
@ -1136,6 +1210,7 @@ HTML = r"""<!doctype html>
|
||||||
<button type="button" id="btn-vue-applications" onclick="setVue('applications')">Applications</button>
|
<button type="button" id="btn-vue-applications" onclick="setVue('applications')">Applications</button>
|
||||||
<button type="button" id="btn-vue-bases" onclick="setVue('bases')">Bases</button>
|
<button type="button" id="btn-vue-bases" onclick="setVue('bases')">Bases</button>
|
||||||
<button type="button" id="btn-vue-domaines" onclick="setVue('domaines')">Domaines</button>
|
<button type="button" id="btn-vue-domaines" onclick="setVue('domaines')">Domaines</button>
|
||||||
|
<button type="button" id="btn-vue-integrations" onclick="setVue('integrations')">Intégrations</button>
|
||||||
<button type="button" id="btn-vue-flux" onclick="setVue('flux')">Flux</button>
|
<button type="button" id="btn-vue-flux" onclick="setVue('flux')">Flux</button>
|
||||||
<button type="button" id="btn-vue-couches" onclick="setVue('couches')">Couches</button>
|
<button type="button" id="btn-vue-couches" onclick="setVue('couches')">Couches</button>
|
||||||
<button type="button" id="btn-vue-reseau" onclick="setVue('reseau')">Réseau</button>
|
<button type="button" id="btn-vue-reseau" onclick="setVue('reseau')">Réseau</button>
|
||||||
|
|
@ -1226,6 +1301,7 @@ HTML = r"""<!doctype html>
|
||||||
let modaleResolveur = null;
|
let modaleResolveur = null;
|
||||||
let intrants = null;
|
let intrants = null;
|
||||||
let integrationsDispo = [];
|
let integrationsDispo = [];
|
||||||
|
let integrationsUniv = {};
|
||||||
let liensAcceptes = {};
|
let liensAcceptes = {};
|
||||||
let sondes = {}; // état RÉEL par hôte : joignable (SSH) ou non — distinct du plan
|
let sondes = {}; // état RÉEL par hôte : joignable (SSH) ou non — distinct du plan
|
||||||
let chronoConsole = null;
|
let chronoConsole = null;
|
||||||
|
|
@ -1274,6 +1350,7 @@ HTML = r"""<!doctype html>
|
||||||
couchesRegistre = data.couches || [];
|
couchesRegistre = data.couches || [];
|
||||||
intrants = data.intrants || null;
|
intrants = data.intrants || null;
|
||||||
integrationsDispo = data.integrations_disponibles || [];
|
integrationsDispo = data.integrations_disponibles || [];
|
||||||
|
integrationsUniv = data.integrations_universelles || {};
|
||||||
liensAcceptes = data.liens_acceptes || {};
|
liensAcceptes = data.liens_acceptes || {};
|
||||||
chargerBases(data);
|
chargerBases(data);
|
||||||
chargerApplications(data);
|
chargerApplications(data);
|
||||||
|
|
@ -1319,8 +1396,8 @@ HTML = r"""<!doctype html>
|
||||||
function majIndicateurSauvegarde() {
|
function majIndicateurSauvegarde() {
|
||||||
const b = document.getElementById('btn-sauver');
|
const b = document.getElementById('btn-sauver');
|
||||||
if (!b) return;
|
if (!b) return;
|
||||||
const editable = vuePrincipale === 'serveurs' || vuePrincipale === 'applications' || vuePrincipale === 'bases' || vuePrincipale === 'domaines';
|
const editable = vuePrincipale === 'serveurs' || vuePrincipale === 'integrations' || vuePrincipale === 'applications' || vuePrincipale === 'bases' || vuePrincipale === 'domaines';
|
||||||
const sale = (vuePrincipale === 'serveurs' && serveursModifie)
|
const sale = ((vuePrincipale === 'serveurs' || vuePrincipale === 'integrations') && serveursModifie)
|
||||||
|| (vuePrincipale === 'applications' && applicationsModifie)
|
|| (vuePrincipale === 'applications' && applicationsModifie)
|
||||||
|| (vuePrincipale === 'bases' && basesModifie)
|
|| (vuePrincipale === 'bases' && basesModifie)
|
||||||
|| (vuePrincipale === 'domaines' && domainesModifie);
|
|| (vuePrincipale === 'domaines' && domainesModifie);
|
||||||
|
|
@ -1358,6 +1435,7 @@ HTML = r"""<!doctype html>
|
||||||
document.getElementById('btn-vue-applications').classList.toggle('on', v === 'applications');
|
document.getElementById('btn-vue-applications').classList.toggle('on', v === 'applications');
|
||||||
document.getElementById('btn-vue-bases').classList.toggle('on', v === 'bases');
|
document.getElementById('btn-vue-bases').classList.toggle('on', v === 'bases');
|
||||||
document.getElementById('btn-vue-domaines').classList.toggle('on', v === 'domaines');
|
document.getElementById('btn-vue-domaines').classList.toggle('on', v === 'domaines');
|
||||||
|
document.getElementById('btn-vue-integrations').classList.toggle('on', v === 'integrations');
|
||||||
document.getElementById('btn-vue-flux').classList.toggle('on', v === 'flux');
|
document.getElementById('btn-vue-flux').classList.toggle('on', v === 'flux');
|
||||||
document.getElementById('btn-vue-couches').classList.toggle('on', v === 'couches');
|
document.getElementById('btn-vue-couches').classList.toggle('on', v === 'couches');
|
||||||
document.getElementById('btn-vue-reseau').classList.toggle('on', v === 'reseau');
|
document.getElementById('btn-vue-reseau').classList.toggle('on', v === 'reseau');
|
||||||
|
|
@ -1383,6 +1461,7 @@ HTML = r"""<!doctype html>
|
||||||
if (vuePrincipale === 'applications') { dessinerApplications(); return; }
|
if (vuePrincipale === 'applications') { dessinerApplications(); return; }
|
||||||
if (vuePrincipale === 'bases') { dessinerBases(); return; }
|
if (vuePrincipale === 'bases') { dessinerBases(); return; }
|
||||||
if (vuePrincipale === 'domaines') { dessinerDomaines(); return; }
|
if (vuePrincipale === 'domaines') { dessinerDomaines(); return; }
|
||||||
|
if (vuePrincipale === 'integrations') { dessinerIntegrations(); return; }
|
||||||
if (vuePrincipale === 'flux') { dessinerFlux(); return; }
|
if (vuePrincipale === 'flux') { dessinerFlux(); return; }
|
||||||
if (vuePrincipale === 'couches') { dessinerCouches(); return; }
|
if (vuePrincipale === 'couches') { dessinerCouches(); return; }
|
||||||
if (vuePrincipale === 'reseau') { dessinerReseau(); return; }
|
if (vuePrincipale === 'reseau') { dessinerReseau(); return; }
|
||||||
|
|
@ -1547,6 +1626,100 @@ HTML = r"""<!doctype html>
|
||||||
<div class="couches-pile">${blocs}</div></div>`;
|
<div class="couches-pile">${blocs}</div></div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Matrice serveurs x integrations -------------------------------------
|
||||||
|
// Pourquoi cette vue : la fiche de detail montre les integrations d'UN serveur.
|
||||||
|
// Savoir qui n'a pas de sauvegarde demandait donc d'ouvrir chaque fiche — et un
|
||||||
|
// trou (deux serveurs sans supervision) est reste invisible jusqu'a ce qu'un
|
||||||
|
// devis de pare-feu l'enumere. Ici il se lit dans une colonne.
|
||||||
|
function servicesRendus(nom) {
|
||||||
|
return new Set(applications.filter(a => a.hote === nom).map(a => a.groupe));
|
||||||
|
}
|
||||||
|
function estExempt(role, rendus) {
|
||||||
|
const p = integrationsUniv[role];
|
||||||
|
return !!(p && p.sauf_role && rendus.has(p.sauf_role));
|
||||||
|
}
|
||||||
|
function basculerIntegrationDe(i, role, actif) {
|
||||||
|
const s = serveurs[i]; if (!s) return;
|
||||||
|
const set = new Set(listeDepuisTexte(s.integrations));
|
||||||
|
if (actif) set.add(role); else set.delete(role);
|
||||||
|
s.integrations = Array.from(set).join(', ');
|
||||||
|
marquerServeursModifie(); dessinerIntegrations();
|
||||||
|
}
|
||||||
|
|
||||||
|
function dessinerIntegrations() {
|
||||||
|
const cible = document.getElementById('grilles');
|
||||||
|
const univ = Object.keys(integrationsUniv).sort();
|
||||||
|
// Colonnes facultatives : celles proposees + toute valeur deja presente dans le
|
||||||
|
// plan (un role retire de roles/ ne doit pas faire disparaitre sa colonne).
|
||||||
|
const dansPlan = new Set();
|
||||||
|
serveurs.forEach(s => listeDepuisTexte(s.integrations).forEach(r => dansPlan.add(r)));
|
||||||
|
const opt = [...new Set([...integrationsDispo, ...dansPlan])].filter(r => !integrationsUniv[r]).sort();
|
||||||
|
const colonnes = [...univ, ...opt];
|
||||||
|
const lignes = serveurs.map((s, i) => [s, i]).filter(([s]) => correspondServeur(s));
|
||||||
|
if (!colonnes.length) { cible.innerHTML = '<div class="vide" style="padding:14px">Aucun rôle client_* dans roles/.</div>'; return; }
|
||||||
|
if (!lignes.length) { cible.innerHTML = '<div class="vide" style="padding:14px">Aucun serveur ne correspond au filtre.</div>'; return; }
|
||||||
|
|
||||||
|
const couvert = {}; colonnes.forEach(r => couvert[r] = 0);
|
||||||
|
const corps = lignes.map(([s, i]) => {
|
||||||
|
const rendus = servicesRendus(s.nom);
|
||||||
|
const set = new Set(listeDepuisTexte(s.integrations));
|
||||||
|
const cellules = colonnes.map(role => {
|
||||||
|
if (integrationsUniv[role]) {
|
||||||
|
if (estExempt(role, rendus)) {
|
||||||
|
return `<td class="mx-c mx-exempt" title="Exempté : cet hôte rend ${echapper(integrationsUniv[role].sauf_role)} — il ne s'intègre pas à lui-même">—</td>`;
|
||||||
|
}
|
||||||
|
couvert[role]++;
|
||||||
|
return `<td class="mx-c mx-univ" title="${echapper(integrationsUniv[role].raison || '')}">✓</td>`;
|
||||||
|
}
|
||||||
|
if (set.has(role)) couvert[role]++;
|
||||||
|
return `<td class="mx-c"><input type="checkbox" ${set.has(role) ? 'checked' : ''}`
|
||||||
|
+ ` onchange="basculerIntegrationDe(${i}, '${echapper(role)}', this.checked)"`
|
||||||
|
+ ` title="${echapper(role)} sur ${echapper(s.nom)}"></td>`;
|
||||||
|
}).join('');
|
||||||
|
const sel = i === selServeur ? ' class="mx-sel"' : '';
|
||||||
|
return `<tr${sel}><th class="mx-h" onclick="setVue('serveurs'); choisirServeur(${i})" title="Ouvrir la fiche de ${echapper(s.nom)}">`
|
||||||
|
+ `${echapper(s.nom || '(sans nom)')}<span class="mx-fn">${echapper(s.fonction || '')}</span></th>${cellules}</tr>`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
const n = lignes.length;
|
||||||
|
const tetes = colonnes.map(role => {
|
||||||
|
const u = !!integrationsUniv[role];
|
||||||
|
return `<th class="mx-t${u ? ' mx-t-univ' : ''}" title="${u ? 'Politique du rôle — s\'applique à tout hôte' : 'Facultative — un choix, serveur par serveur'}">`
|
||||||
|
+ `<span>${echapper(role.replace(/^client_/, ''))}</span></th>`;
|
||||||
|
}).join('');
|
||||||
|
const pieds = colonnes.map(role => {
|
||||||
|
const c = couvert[role];
|
||||||
|
const cls = integrationsUniv[role] ? 'mx-p mx-univ' : (c === n ? 'mx-p mx-plein' : 'mx-p');
|
||||||
|
return `<td class="${cls}">${c}/${n}</td>`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
cible.innerHTML = `<div class="section-grille">
|
||||||
|
<div class="section-tete">Intégrations — matrice <span class="compte">${n} × ${colonnes.length}</span>
|
||||||
|
<span class="hint-dep" style="margin-left:auto">Les colonnes ✓ sont la politique des rôles ; les cases sont les choix, éditables ici</span></div>
|
||||||
|
<div class="mx-cadre"><table class="mx">
|
||||||
|
<thead><tr><th class="mx-t mx-coin">serveur</th>${tetes}</tr></thead>
|
||||||
|
<tbody>${corps}</tbody>
|
||||||
|
<tfoot><tr><th class="mx-h mx-coin">couverture</th>${pieds}</tr></tfoot>
|
||||||
|
</table></div></div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function legendeIntegrations() {
|
||||||
|
const univ = Object.entries(integrationsUniv).sort();
|
||||||
|
const lignes = univ.map(([role, p]) => `<div class="mx-leg"><span class="mx-univ">✓</span> <code>${echapper(role)}</code>
|
||||||
|
<div class="hint-dep">${echapper(p.raison || '')}${p.sauf_role ? ` <b>Exemption</b> : l'hôte qui rend <code>${echapper(p.sauf_role)}</code>.` : ''}</div></div>`).join('');
|
||||||
|
document.getElementById('detail').innerHTML = `<div class="detail">
|
||||||
|
<div class="section-tete">Deux genres d'intégration</div>
|
||||||
|
<div class="hint-dep"><b>Universelle</b> — aucun choix de cible : un seul Prometheus, un seul Loki, une seule AC.
|
||||||
|
Déclarée <b>une fois par le rôle</b> (<code>roles/<rôle>/meta/integration.yml</code>), elle s'applique à tout hôte
|
||||||
|
et ne se décoche pas. Le plan <b>refuse</b> de la redire : deux sources finiraient par diverger.</div>
|
||||||
|
${lignes}
|
||||||
|
<div class="hint-dep" style="margin-top:12px"><b>Facultative</b> — un vrai choix, serveur par serveur, écrit dans
|
||||||
|
<code>plan/serveurs.yml</code>. Coche dans la matrice, puis <b>Sauvegarder</b>.</div>
|
||||||
|
<div class="hint-dep" style="margin-top:12px">La ligne <b>couverture</b> ne juge pas : une colonne à <code>7/14</code>
|
||||||
|
peut être exactement juste. Elle rend le motif <em>visible</em> — c'est au lecteur de savoir si les sept manquants
|
||||||
|
sont des décisions ou des oublis.</div></div>`;
|
||||||
|
}
|
||||||
|
|
||||||
function basesDeApp(app, hoteNom) {
|
function basesDeApp(app, hoteNom) {
|
||||||
return bdApplis.filter(a => {
|
return bdApplis.filter(a => {
|
||||||
const p = a.portee || 'groupe';
|
const p = a.portee || 'groupe';
|
||||||
|
|
@ -2036,6 +2209,16 @@ HTML = r"""<!doctype html>
|
||||||
const cases = tousRoles.length
|
const cases = tousRoles.length
|
||||||
? tousRoles.map(role => `<label class="integ-case" title="${echapper(role)}"><input type="checkbox" ${set.has(role) ? 'checked' : ''} onchange="basculerIntegration('${echapper(role)}', this.checked)"><span>${echapper(role.replace(/^client_/, ''))}</span></label>`).join('')
|
? tousRoles.map(role => `<label class="integ-case" title="${echapper(role)}"><input type="checkbox" ${set.has(role) ? 'checked' : ''} onchange="basculerIntegration('${echapper(role)}', this.checked)"><span>${echapper(role.replace(/^client_/, ''))}</span></label>`).join('')
|
||||||
: '<span class="dsn">aucun rôle client_* disponible</span>';
|
: '<span class="dsn">aucun rôle client_* disponible</span>';
|
||||||
|
// Integrations universelles : montrees, jamais cochables. Sans cet affichage,
|
||||||
|
// un plan silencieux se lirait « cet hote n'est pas supervise » — l'inverse
|
||||||
|
// exact de la politique. L'exemption `sauf_role` s'evalue ici comme au moteur.
|
||||||
|
const rendus = new Set(applications.filter(a => a.hote === s.nom).map(a => a.groupe));
|
||||||
|
const univ = Object.entries(integrationsUniv)
|
||||||
|
.filter(([role, p]) => !(p.sauf_role && rendus.has(p.sauf_role)))
|
||||||
|
.map(([role, p]) => `<span class="integ-case integ-univ" title="${echapper(p.raison || '')}">✓ ${echapper(role.replace(/^client_/, ''))}</span>`).join('');
|
||||||
|
const exempts = Object.entries(integrationsUniv)
|
||||||
|
.filter(([role, p]) => p.sauf_role && rendus.has(p.sauf_role))
|
||||||
|
.map(([role, p]) => `<span class="integ-case integ-exempt" title="Exempté : cet hôte rend le service ${echapper(p.sauf_role)}">— ${echapper(role.replace(/^client_/, ''))}</span>`).join('');
|
||||||
const info = infoFonction(s.fonction);
|
const info = infoFonction(s.fonction);
|
||||||
const zoneHint = info ? `<span class="zone-hint" title="Dérivé de la nomenclature">${echapper(info.libelle)} · VLAN ${info.vlan}</span>` : '';
|
const zoneHint = info ? `<span class="zone-hint" title="Dérivé de la nomenclature">${echapper(info.libelle)} · VLAN ${info.vlan}</span>` : '';
|
||||||
const h = hotes.find(x => x.nom === s.nom);
|
const h = hotes.find(x => x.nom === s.nom);
|
||||||
|
|
@ -2083,7 +2266,7 @@ HTML = r"""<!doctype html>
|
||||||
<label class="champ"><span>Disque</span><input data-v="disque" value="${echapper(s.disque)}" placeholder="16G" oninput="definirServeur('disque', this.value)"></label>
|
<label class="champ"><span>Disque</span><input data-v="disque" value="${echapper(s.disque)}" placeholder="16G" oninput="definirServeur('disque', this.value)"></label>
|
||||||
<label class="champ"><span>Mémoire</span><input data-v="memoire" type="number" value="${echapper(s.memoire)}" placeholder="2048" oninput="definirServeur('memoire', this.value)"></label>
|
<label class="champ"><span>Mémoire</span><input data-v="memoire" type="number" value="${echapper(s.memoire)}" placeholder="2048" oninput="definirServeur('memoire', this.value)"></label>
|
||||||
<label class="champ"><span>Cœurs</span><input data-v="coeurs" type="number" value="${echapper(s.coeurs)}" placeholder="2" oninput="definirServeur('coeurs', this.value)"></label>
|
<label class="champ"><span>Cœurs</span><input data-v="coeurs" type="number" value="${echapper(s.coeurs)}" placeholder="2" oninput="definirServeur('coeurs', this.value)"></label>
|
||||||
<label class="champ" style="grid-column:1/-1"><span>Intégrations (rôles client)</span><div class="integ-cases">${cases}</div></label>
|
<label class="champ" style="grid-column:1/-1"><span>Intégrations (rôles client)</span><div class="integ-cases">${univ}${exempts}${cases}</div><span class="dsn" style="font-size:11px">Les ✓ sont la politique des rôles : elles s'appliquent à tout hôte et ne se décochent pas.</span></label>
|
||||||
</div>
|
</div>
|
||||||
<div class="section-tete" style="margin:12px 2px 2px">Applications ici</div>${appsHtml}
|
<div class="section-tete" style="margin:12px 2px 2px">Applications ici</div>${appsHtml}
|
||||||
<div class="section-tete" style="margin:12px 2px 2px">Bases ici</div>${bdHtml}
|
<div class="section-tete" style="margin:12px 2px 2px">Bases ici</div>${bdHtml}
|
||||||
|
|
@ -2177,6 +2360,7 @@ HTML = r"""<!doctype html>
|
||||||
function dessinerDetail() {
|
function dessinerDetail() {
|
||||||
// Le panneau de droite reflète la sélection de la VUE COURANTE.
|
// Le panneau de droite reflète la sélection de la VUE COURANTE.
|
||||||
if (vuePrincipale === 'flux' || vuePrincipale === 'couches' || vuePrincipale === 'reseau') return legendeLecture();
|
if (vuePrincipale === 'flux' || vuePrincipale === 'couches' || vuePrincipale === 'reseau') return legendeLecture();
|
||||||
|
if (vuePrincipale === 'integrations') return legendeIntegrations();
|
||||||
if (vuePrincipale === 'applications') return inspecteurApplication();
|
if (vuePrincipale === 'applications') return inspecteurApplication();
|
||||||
if (vuePrincipale === 'bases') return inspecteurBase();
|
if (vuePrincipale === 'bases') return inspecteurBase();
|
||||||
if (vuePrincipale === 'domaines') return inspecteurDomaine();
|
if (vuePrincipale === 'domaines') return inspecteurDomaine();
|
||||||
|
|
@ -2287,13 +2471,22 @@ HTML = r"""<!doctype html>
|
||||||
const valeurs = intrants.valeurs || {};
|
const valeurs = intrants.valeurs || {};
|
||||||
const sections = [];
|
const sections = [];
|
||||||
const parSection = {};
|
const parSection = {};
|
||||||
|
const proprioDe = {};
|
||||||
schema.forEach(ch => {
|
schema.forEach(ch => {
|
||||||
if (!parSection[ch.section]) { parSection[ch.section] = []; sections.push(ch.section); }
|
if (!parSection[ch.section]) { parSection[ch.section] = []; sections.push(ch.section); }
|
||||||
parSection[ch.section].push(ch);
|
parSection[ch.section].push(ch);
|
||||||
|
proprioDe[ch.section] = ch.proprietaire || 'tenant';
|
||||||
});
|
});
|
||||||
let html = '';
|
let html = '';
|
||||||
sections.forEach(sec => {
|
sections.forEach(sec => {
|
||||||
html += '<div class="intr-section"><div class="intr-tete">' + echapper(sec) + '</div><div class="intr-grille">';
|
// Le panneau melangeait deux proprietaires sans le dire. Un tenant et son
|
||||||
|
// hebergeur n'ont ni le meme depot, ni le meme cycle de vie : editer une
|
||||||
|
// section « hébergeur » touche TOUS les tenants du cluster.
|
||||||
|
const heb = proprioDe[sec] === 'hebergeur';
|
||||||
|
const chip = heb
|
||||||
|
? '<span class="intr-proprio heb" title="Propriété de l\'HÉBERGEUR — écrit hors du dépôt du tenant ; la modification vaut pour tous ses tenants">hébergeur</span>'
|
||||||
|
: '<span class="intr-proprio ten" title="Propriété du TENANT — écrit dans son dépôt ; n\'affecte que lui">tenant</span>';
|
||||||
|
html += '<div class="intr-section"><div class="intr-tete">' + echapper(sec) + chip + '</div><div class="intr-grille">';
|
||||||
parSection[sec].forEach(ch => {
|
parSection[sec].forEach(ch => {
|
||||||
const v = valeurs[ch.variable];
|
const v = valeurs[ch.variable];
|
||||||
let badge;
|
let badge;
|
||||||
|
|
@ -2448,7 +2641,7 @@ HTML = r"""<!doctype html>
|
||||||
|
|
||||||
function sauvegarderVue() {
|
function sauvegarderVue() {
|
||||||
if (occupe) return;
|
if (occupe) return;
|
||||||
if (vuePrincipale === 'serveurs') sauvegarderServeurs();
|
if (vuePrincipale === 'serveurs' || vuePrincipale === 'integrations') sauvegarderServeurs();
|
||||||
else if (vuePrincipale === 'applications') sauvegarderApplications();
|
else if (vuePrincipale === 'applications') sauvegarderApplications();
|
||||||
else if (vuePrincipale === 'bases') sauvegarderBases();
|
else if (vuePrincipale === 'bases') sauvegarderBases();
|
||||||
else if (vuePrincipale === 'domaines') sauvegarderDomaines();
|
else if (vuePrincipale === 'domaines') sauvegarderDomaines();
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,51 @@ def catalogue_liens(racine: Path | None = None) -> dict:
|
||||||
return catalogue
|
return catalogue
|
||||||
|
|
||||||
|
|
||||||
|
def integrations_universelles(racine: Path | None = None) -> dict[str, dict]:
|
||||||
|
"""{role: {raison, sauf_role}} — les integrations declarees universelles.
|
||||||
|
|
||||||
|
Une integration universelle n'est PAS recopiee sur chaque serveur du plan : le
|
||||||
|
role la declare une fois dans roles/<role>/meta/integration.yml, et tout hote
|
||||||
|
la recoit. Le plan ne porte plus que les integrations reellement facultatives
|
||||||
|
(client_backup, client_smtp, client_unbound) et les exemptions.
|
||||||
|
|
||||||
|
Pourquoi : recopiee 14 fois, la meme ligne finit par manquer une quinzieme —
|
||||||
|
et un hote non supervise, non journalise ou sans certificat ne proteste pas.
|
||||||
|
Le defaut est desormais « oui », et c'est l'exception qui doit se justifier.
|
||||||
|
"""
|
||||||
|
base = (racine or RACINE_DEPOT) / "roles"
|
||||||
|
if not base.is_dir():
|
||||||
|
return {}
|
||||||
|
politiques = {}
|
||||||
|
for meta in sorted(base.glob("client_*/meta/integration.yml")):
|
||||||
|
data = yaml.safe_load(meta.read_text(encoding="utf-8")) or {}
|
||||||
|
politique = data.get("integration") or {}
|
||||||
|
if politique.get("universelle"):
|
||||||
|
politiques[meta.parents[1].name] = {
|
||||||
|
"raison": str(politique.get("raison", "")).strip(),
|
||||||
|
"sauf_role": politique.get("sauf_role") or None,
|
||||||
|
}
|
||||||
|
return politiques
|
||||||
|
|
||||||
|
|
||||||
|
def integrations_de(srv: dict, services_hote: set[str] | None = None,
|
||||||
|
universelles: dict[str, dict] | None = None,
|
||||||
|
racine: Path | None = None) -> set[str]:
|
||||||
|
"""Integrations effectives d'un serveur : les universelles + celles du plan.
|
||||||
|
|
||||||
|
`services_hote` = groupes de service que cet hote rend (derives des
|
||||||
|
applications). Il sert aux exemptions : `sauf_role` retire l'integration a
|
||||||
|
l'hote qui rend le service lui-meme — l'AC ne s'enrole pas aupres d'elle-meme.
|
||||||
|
L'exemption suit donc le service, jamais un nom d'hote.
|
||||||
|
"""
|
||||||
|
if universelles is None:
|
||||||
|
universelles = integrations_universelles(racine)
|
||||||
|
rendus = services_hote or set()
|
||||||
|
effectives = {role for role, p in universelles.items()
|
||||||
|
if not (p["sauf_role"] and p["sauf_role"] in rendus)}
|
||||||
|
return effectives | set(srv.get("integrations") or [])
|
||||||
|
|
||||||
|
|
||||||
def est_groupe_operationnel(groupe: str) -> bool:
|
def est_groupe_operationnel(groupe: str) -> bool:
|
||||||
return groupe.startswith(GROUPES_OPERATIONNELS_PREFIXES)
|
return groupe.startswith(GROUPES_OPERATIONNELS_PREFIXES)
|
||||||
|
|
||||||
|
|
@ -466,6 +511,7 @@ def valider_serveurs(registre: dict, nomenclature: dict | None = None) -> None:
|
||||||
fonctions = (nomenclature or {}).get("fonctions") or {}
|
fonctions = (nomenclature or {}).get("fonctions") or {}
|
||||||
if not isinstance(serveurs, dict):
|
if not isinstance(serveurs, dict):
|
||||||
raise ValueError("serveurs doit etre une table.")
|
raise ValueError("serveurs doit etre une table.")
|
||||||
|
universelles = integrations_universelles()
|
||||||
for nom, srv in serveurs.items():
|
for nom, srv in serveurs.items():
|
||||||
if not isinstance(srv, dict):
|
if not isinstance(srv, dict):
|
||||||
raise ValueError(f"Serveur '{nom}': table attendue.")
|
raise ValueError(f"Serveur '{nom}': table attendue.")
|
||||||
|
|
@ -480,6 +526,16 @@ def valider_serveurs(registre: dict, nomenclature: dict | None = None) -> None:
|
||||||
integrations = srv.get("integrations")
|
integrations = srv.get("integrations")
|
||||||
if integrations is not None and not isinstance(integrations, list):
|
if integrations is not None and not isinstance(integrations, list):
|
||||||
raise ValueError(f"Serveur '{nom}': 'integrations' doit etre une liste.")
|
raise ValueError(f"Serveur '{nom}': 'integrations' doit etre une liste.")
|
||||||
|
# Une integration universelle recopiee dans le plan a deux torts : elle
|
||||||
|
# laisse croire que son ABSENCE chez un voisin signifie « non integre »,
|
||||||
|
# et elle offre une valeur a oublier. La politique du role est la seule
|
||||||
|
# source ; on refuse la copie plutot que de la tolerer silencieusement.
|
||||||
|
redites = sorted(set(integrations or []) & set(universelles))
|
||||||
|
if redites:
|
||||||
|
raise ValueError(
|
||||||
|
f"Serveur '{nom}': integration(s) {', '.join(redites)} universelle(s) — "
|
||||||
|
f"declaree(s) dans roles/<role>/meta/integration.yml, elles s'appliquent "
|
||||||
|
f"a tout hote. Retirer ces lignes du plan.")
|
||||||
|
|
||||||
|
|
||||||
def reconcilier_serveur(nom: str, srv: dict, hote_inventaire: dict | None, nomenclature: dict) -> dict:
|
def reconcilier_serveur(nom: str, srv: dict, hote_inventaire: dict | None, nomenclature: dict) -> dict:
|
||||||
|
|
|
||||||
|
|
@ -163,6 +163,84 @@ def preuve_modele_socle() -> tuple[bool, str]:
|
||||||
return True, "Modele public socle : domaines/serveurs/applications/bases valides."
|
return True, "Modele public socle : domaines/serveurs/applications/bases valides."
|
||||||
|
|
||||||
|
|
||||||
|
def preuve_propriete_des_intrants() -> tuple[bool, str]:
|
||||||
|
"""Aucune cle de l'HEBERGEUR ne subsiste dans un group_vars de tenant.
|
||||||
|
|
||||||
|
Un cluster, une fabric et une frontiere appartiennent a l'hebergeur : recopies
|
||||||
|
chez chaque tenant, ils avaient deja diverge (deux listes de stockages
|
||||||
|
contradictoires pour le meme materiel). La garde attrape la rechute — un
|
||||||
|
`make config` d'un autre poste, une reprise a la main — avant qu'elle ne
|
||||||
|
reintroduise deux valeurs pour un seul objet.
|
||||||
|
|
||||||
|
Portee : l'instance liee. Un depot sans hebergeur monte (aucun `underlay.yml`)
|
||||||
|
ecrit legitimement tout au meme endroit : la preuve se saute alors.
|
||||||
|
"""
|
||||||
|
sys.path.insert(0, str(RACINE / "scripts"))
|
||||||
|
import yaml as _yaml
|
||||||
|
underlay = Path(os.environ.get("SETOPS_UNDERLAY") or (RACINE / "underlay.yml"))
|
||||||
|
if not underlay.exists():
|
||||||
|
return True, "Aucun hebergeur monte (pas d'underlay.yml) : separation sans objet."
|
||||||
|
hebergeur = underlay.resolve().parent
|
||||||
|
instance = Path(os.environ.get("SETOPS_INSTANCE") or (RACINE / "instance"))
|
||||||
|
if instance.resolve() == hebergeur.resolve():
|
||||||
|
cles = {} # l'hebergeur est aussi tenant : son propre depot porte les deux
|
||||||
|
else:
|
||||||
|
fichier_heb = hebergeur / "proxmox-hebergeur.yml"
|
||||||
|
if not fichier_heb.is_file():
|
||||||
|
return False, f"Fichier du cluster absent chez l'hebergeur : {fichier_heb}"
|
||||||
|
cles = _yaml.safe_load(fichier_heb.read_text(encoding="utf-8")) or {}
|
||||||
|
fautes = []
|
||||||
|
for gv in sorted((instance / "inventories").glob("*/group_vars/*.yml")):
|
||||||
|
data = _yaml.safe_load(gv.read_text(encoding="utf-8")) or {}
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
continue
|
||||||
|
redites = sorted(set(data) & set(cles))
|
||||||
|
if redites:
|
||||||
|
fautes.append(f"{gv.parent.name}/{gv.name}: {', '.join(redites)}")
|
||||||
|
if fautes:
|
||||||
|
return False, "cle(s) de l'hebergeur recopiee(s) chez le tenant — " + " ; ".join(fautes[:4])
|
||||||
|
return True, (f"{len(cles)} cle(s) de cluster chez l'hebergeur, aucune recopiee dans les "
|
||||||
|
f"group_vars du tenant.")
|
||||||
|
|
||||||
|
|
||||||
|
def preuve_integrations_universelles() -> tuple[bool, str]:
|
||||||
|
"""Aucun hote n'echappe a une integration universelle, et le plan ne la redit pas.
|
||||||
|
|
||||||
|
C'est la garde de l'inversion du defaut (D-33). Elle attrape les deux rechutes
|
||||||
|
possibles : un hote sans supervision/journal/certificat, et une recopie dans le
|
||||||
|
plan — qui reintroduirait deux sources et donc, tot ou tard, un ecart.
|
||||||
|
"""
|
||||||
|
sys.path.insert(0, str(RACINE / "scripts"))
|
||||||
|
from inventory_rules import (charger_applications, charger_serveurs,
|
||||||
|
integrations_de, integrations_universelles)
|
||||||
|
universelles = integrations_universelles(RACINE)
|
||||||
|
if not universelles:
|
||||||
|
return False, "Aucune integration universelle declaree : la politique a disparu."
|
||||||
|
instance = Path(os.environ.get("SETOPS_INSTANCE") or (RACINE / "instance"))
|
||||||
|
serveurs = (charger_serveurs(instance / "plan/serveurs.yml").get("serveurs") or {})
|
||||||
|
apps = (charger_applications(instance / "plan/applications.yml").get("applications") or {})
|
||||||
|
services: dict[str, set[str]] = {}
|
||||||
|
for app in apps.values():
|
||||||
|
services.setdefault(app.get("hote"), set()).add(app.get("groupe"))
|
||||||
|
fautes, exemptes = [], 0
|
||||||
|
for nom, srv in serveurs.items():
|
||||||
|
rendus = services.get(nom, set())
|
||||||
|
obtenues = integrations_de(srv, rendus, universelles)
|
||||||
|
for role, politique in universelles.items():
|
||||||
|
exempt = politique["sauf_role"] and politique["sauf_role"] in rendus
|
||||||
|
if exempt:
|
||||||
|
exemptes += 1
|
||||||
|
elif role not in obtenues:
|
||||||
|
fautes.append(f"{nom} sans {role}")
|
||||||
|
redites = sorted(set(srv.get("integrations") or []) & set(universelles))
|
||||||
|
if redites:
|
||||||
|
fautes.append(f"{nom} redit {', '.join(redites)} (politique du role)")
|
||||||
|
if fautes:
|
||||||
|
return False, "; ".join(fautes[:6])
|
||||||
|
return True, (f"{len(serveurs)} hote(s) x {len(universelles)} integration(s) universelle(s) : "
|
||||||
|
f"aucune lacune, aucune recopie ({exemptes} exemption(s) derivee(s) du service rendu).")
|
||||||
|
|
||||||
|
|
||||||
def preuve_nomenclature_derivee() -> tuple[bool, str]:
|
def preuve_nomenclature_derivee() -> tuple[bool, str]:
|
||||||
"""Aucune nomenclature ne STOCKE d'adressage : tout se derive du seed `index`.
|
"""Aucune nomenclature ne STOCKE d'adressage : tout se derive du seed `index`.
|
||||||
|
|
||||||
|
|
@ -296,6 +374,10 @@ PREUVES: list[dict] = [
|
||||||
"cmds": [[sys.executable, "scripts/devis_proxmox_fw.py", "--verifier"]]},
|
"cmds": [[sys.executable, "scripts/devis_proxmox_fw.py", "--verifier"]]},
|
||||||
{"id": "P24", "titre": "Frontiere nord/sud : acces d'administration declare", "refs": ["AFF-104"],
|
{"id": "P24", "titre": "Frontiere nord/sud : acces d'administration declare", "refs": ["AFF-104"],
|
||||||
"cmds": [[sys.executable, "scripts/devis_opnsense.py", "--verifier"]]},
|
"cmds": [[sys.executable, "scripts/devis_opnsense.py", "--verifier"]]},
|
||||||
|
{"id": "P26", "titre": "Integrations universelles : aucun hote laisse de cote", "refs": ["AFF-108"],
|
||||||
|
"func": preuve_integrations_universelles},
|
||||||
|
{"id": "P27", "titre": "Propriete des intrants : hebergeur et tenant separes", "refs": ["AFF-109"],
|
||||||
|
"func": preuve_propriete_des_intrants},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ from inventory_rules import (
|
||||||
charger_nomenclature,
|
charger_nomenclature,
|
||||||
charger_serveurs,
|
charger_serveurs,
|
||||||
fonction_seq,
|
fonction_seq,
|
||||||
|
integrations_universelles,
|
||||||
reconcilier_serveur,
|
reconcilier_serveur,
|
||||||
valider_serveurs,
|
valider_serveurs,
|
||||||
)
|
)
|
||||||
|
|
@ -67,6 +68,9 @@ def hotes_inventaire() -> dict:
|
||||||
|
|
||||||
def bootstrap() -> dict:
|
def bootstrap() -> dict:
|
||||||
serveurs = {}
|
serveurs = {}
|
||||||
|
# Les universelles ne se recopient pas dans le plan (D-33) : les reinscrire ici
|
||||||
|
# reconstituerait, a chaque bootstrap, les lignes qu'on vient de retirer.
|
||||||
|
universelles = set(integrations_universelles())
|
||||||
for nom, h in hotes_inventaire().items():
|
for nom, h in hotes_inventaire().items():
|
||||||
fonction, _ = fonction_seq(nom)
|
fonction, _ = fonction_seq(nom)
|
||||||
srv = {"fonction": fonction, "etat": h.get("etat", "planifie")}
|
srv = {"fonction": fonction, "etat": h.get("etat", "planifie")}
|
||||||
|
|
@ -74,7 +78,8 @@ def bootstrap() -> dict:
|
||||||
v = str(h.get(cle_src, "")).strip()
|
v = str(h.get(cle_src, "")).strip()
|
||||||
if v:
|
if v:
|
||||||
srv[cle_dst] = int(v) if v.isdigit() else v
|
srv[cle_dst] = int(v) if v.isdigit() else v
|
||||||
integrations = sorted(g for g in (h.get("groupes") or []) if g.startswith("client_"))
|
integrations = sorted(g for g in (h.get("groupes") or [])
|
||||||
|
if g.startswith("client_") and g not in universelles)
|
||||||
if integrations:
|
if integrations:
|
||||||
srv["integrations"] = integrations
|
srv["integrations"] = integrations
|
||||||
serveurs[nom] = srv
|
serveurs[nom] = srv
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,8 @@ from pathlib import Path
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
from inventory_rules import charger_applications, charger_bases_donnees, charger_serveurs
|
from inventory_rules import (charger_applications, charger_bases_donnees, charger_serveurs,
|
||||||
|
integrations_universelles)
|
||||||
|
|
||||||
RACINE = Path(__file__).resolve().parents[1]
|
RACINE = Path(__file__).resolve().parents[1]
|
||||||
INSTANCE = Path(os.environ.get("SETOPS_INSTANCE") or (RACINE / "instance"))
|
INSTANCE = Path(os.environ.get("SETOPS_INSTANCE") or (RACINE / "instance"))
|
||||||
|
|
@ -67,8 +68,15 @@ def groupes_du_plan() -> set[str]:
|
||||||
apps = charger_applications(INSTANCE / "plan/applications.yml")
|
apps = charger_applications(INSTANCE / "plan/applications.yml")
|
||||||
srv = charger_serveurs(INSTANCE / "plan/serveurs.yml")
|
srv = charger_serveurs(INSTANCE / "plan/serveurs.yml")
|
||||||
groupes = {a.get("groupe") for a in (apps.get("applications") or {}).values() if a.get("groupe")}
|
groupes = {a.get("groupe") for a in (apps.get("applications") or {}).values() if a.get("groupe")}
|
||||||
for s in (srv.get("serveurs") or {}).values():
|
serveurs = (srv.get("serveurs") or {})
|
||||||
|
for s in serveurs.values():
|
||||||
groupes.update(s.get("integrations") or [])
|
groupes.update(s.get("integrations") or [])
|
||||||
|
# Les integrations universelles ne figurent plus dans le plan (D-33) : sans
|
||||||
|
# cette ligne, leurs secrets cesseraient d'etre exiges — et P18 passerait au
|
||||||
|
# vert sur une voute incomplete. L'exemption `sauf_role` ne retire le role qu'a
|
||||||
|
# UN hote : le groupe existe des qu'il reste un serveur, donc pas de filtrage.
|
||||||
|
if serveurs:
|
||||||
|
groupes.update(integrations_universelles())
|
||||||
return groupes
|
return groupes
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ Une bonne console d'exploitation obéit à trois règles :
|
||||||
|
|
||||||
| Vues **éditables** (le plan) | Vues **dérivées** (lecture seule) |
|
| Vues **éditables** (le plan) | Vues **dérivées** (lecture seule) |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Serveurs · Applications · Bases · Domaines · **Intrants** | Flux · Couches · **Réseau** (flotte + devis) |
|
| Serveurs · **Intégrations** · Applications · Bases · Domaines · **Intrants** | Flux · Couches · **Réseau** (flotte + devis) |
|
||||||
|
|
||||||
Le flux d'exploitation, de bout en bout :
|
Le flux d'exploitation, de bout en bout :
|
||||||
|
|
||||||
|
|
@ -73,6 +73,16 @@ un pointeur seulement) et le **DSN dérivé** (secret masqué).
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
**Intégrations** *(éditable)* — la **matrice serveurs × intégrations**. Les colonnes ✓ vertes
|
||||||
|
sont la **politique des rôles** : elles s'appliquent à tout hôte et ne se décochent pas ; un `—`
|
||||||
|
barré signale une **exemption dérivée du service rendu** (l'AC ne s'enrôle pas auprès d'elle-même).
|
||||||
|
Les autres colonnes sont de vrais choix, cochables ici. La ligne **couverture** ne juge pas — elle
|
||||||
|
rend le motif visible : c'est au lecteur de savoir si les manquants sont des décisions ou des oublis.
|
||||||
|
|
||||||
|
> Cette vue existe parce que la fiche de détail ne montrait les intégrations que **d'un serveur à
|
||||||
|
> la fois**. Deux serveurs sans supervision ni journaux sont restés invisibles jusqu'à ce qu'un
|
||||||
|
> devis de pare-feu les énumère : l'information était à l'écran, répartie sur quatorze clics.
|
||||||
|
|
||||||
**Flux** *(dérivée)* — la **matrice d'audit** : la source de nftables *et* la justification lisible
|
**Flux** *(dérivée)* — la **matrice d'audit** : la source de nftables *et* la justification lisible
|
||||||
de chaque flux (sens ingress/egress, chiffrement).
|
de chaque flux (sens ingress/egress, chiffrement).
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue