Compare commits
No commits in common. "9f7bb229f30da462da733797d751736bc00e93c5" and "c77ec20e4a1da349b2dd80baa9f069ff123e1765" have entirely different histories.
9f7bb229f3
...
c77ec20e4a
26 changed files with 1 additions and 1087 deletions
41
CHANGELOG.md
41
CHANGELOG.md
|
|
@ -1,45 +1,6 @@
|
|||
# CHANGELOG — Set-OPS
|
||||
|
||||
## 2026-06-30
|
||||
|
||||
### Ajouté
|
||||
- **Panneau « Intrants de base » dans le GUI.** Un bouton ⚙ Intrants ouvre une fenêtre
|
||||
unique pour saisir les valeurs communes à tout l'écosystème, avec une distinction
|
||||
visible entre **constantes** (valeur unique, non surchargeable) et **défauts**
|
||||
(valeurs proposées, surchargeables dans les instances). Les secrets ne sont **jamais**
|
||||
saisis ni affichés ici (garde-fou `INTRANTS_CLES_INTERDITES` + filtrage par schéma) :
|
||||
ils restent dans le Vault Ansible, édités en CLI ; le panneau les liste seulement à
|
||||
titre informatif. Un changement de `domaine_interne` (clé de voûte) demande une
|
||||
confirmation explicite ; un `domaine_interne` vide est refusé côté serveur. La
|
||||
nomenclature reste en lecture seule (modifiable dans le plan). Voir
|
||||
`docs/intrants-communs.md` et `docs/intrants-base-gui-conception.md`.
|
||||
- **Source unique d'identité partagée.** `domaine_interne` et `chezlepro_timezone`
|
||||
vivent désormais dans `inventories/partage/intrants-identite.yml`, référencé par
|
||||
symlink depuis chaque environnement (`group_vars/all/10-intrants.yml`) — fin de la
|
||||
duplication lab/production.
|
||||
- **Info-bulles d'aide sur les champs du GUI.** Survoler la description d'un champ à
|
||||
saisir affiche une bulle avec des instructions sommaires.
|
||||
|
||||
## 2026-06-28
|
||||
|
||||
### Ajouté
|
||||
- **Document de présentation de l'écosystème Chezlepro** (`docs/ecosysteme-chezlepro.md`).
|
||||
Description vulgarisée à destination client/partenaire : les piliers de l'écosystème
|
||||
souverain, le modèle reproductible (plan → génération → clonage → conformité), et un
|
||||
inventaire des **mesures de renforcement** réellement en place (SSH durci, fail2ban,
|
||||
sysctl, nftables, AppArmor, auditd, mises à jour automatiques, gestion des secrets,
|
||||
garde-fous destructifs, sécurité par l'architecture). Honnête sur le statut
|
||||
« défini/validé vs déployé ».
|
||||
|
||||
### Ajouté
|
||||
- **Dimensionnement dérivé des ressources VM.** Les cœurs/RAM/disque d'une VM sont
|
||||
désormais **estimés depuis les logiciels hébergés + le socle SE**, au lieu d'hériter
|
||||
des specs du golden template (CPU/RAM identiques pour toutes les VM auparavant).
|
||||
Chaque rôle déclare son empreinte (`roles/<rôle>/meta/empreinte.yml`) ; le
|
||||
générateur somme par hôte (marge + arrondis) et écrit `proxmox_coeurs`/
|
||||
`proxmox_memoire`/`proxmox_disque_taille` ; le clonage passe `cores`/`memory` à
|
||||
Proxmox (`omit` si absent → aucune régression). Override par hôte possible dans le
|
||||
plan (`serveurs.yml`). Voir `docs/dimensionnement-ressources.md`.
|
||||
## 2026-06-25
|
||||
|
||||
### Corrigé
|
||||
- **`make instancier-appliquer FORCE=1` n'honorait pas `--force`.** La recette Makefile lançait `instancier.py appliquer` sans relayer `FORCE` ; le message « Utilise FORCE=1 » était donc trompeur (un changement de plan intentionnel restait bloqué). La recette passe désormais `$(if $(FORCE),--force)`. Trouvé en dogfooding.
|
||||
|
|
|
|||
4
Makefile
4
Makefile
|
|
@ -325,8 +325,6 @@ cloner-vm: ansible-runtime
|
|||
[[ -n "$(FORMAT_DISQUE)" ]] && extra_vars+=( -e proxmox_clone_format="$(FORMAT_DISQUE)" ); \
|
||||
[[ -n "$(CLONE_COMPLET)" ]] && extra_vars+=( -e proxmox_clone_complet="$(CLONE_COMPLET)" ); \
|
||||
[[ -n "$(TAILLE_DISQUE)" ]] && extra_vars+=( -e proxmox_clone_taille_disque="$(TAILLE_DISQUE)" ); \
|
||||
[[ -n "$(COEURS)" ]] && extra_vars+=( -e proxmox_clone_coeurs="$(COEURS)" ); \
|
||||
[[ -n "$(MEMOIRE)" ]] && extra_vars+=( -e proxmox_clone_memoire="$(MEMOIRE)" ); \
|
||||
[[ -n "$(DISQUE_PROXMOX)" ]] && extra_vars+=( -e proxmox_clone_disque="$(DISQUE_PROXMOX)" ); \
|
||||
[[ -n "$(DNS)" ]] && extra_vars+=( -e proxmox_clone_dns="$(DNS)" ); \
|
||||
[[ -n "$(CIUSER)" ]] && extra_vars+=( -e proxmox_clone_ciuser="$(CIUSER)" ); \
|
||||
|
|
@ -365,8 +363,6 @@ creer-vm: _instance-requise
|
|||
VLAN="$$SETOPS_VLAN" \
|
||||
STOCKAGE_PROXMOX="$${SETOPS_STOCKAGE:-$(STOCKAGE_PROXMOX)}" \
|
||||
TAILLE_DISQUE="$${SETOPS_DISQUE:-$(TAILLE_DISQUE)}" \
|
||||
COEURS="$${SETOPS_COEURS:-$(COEURS)}" \
|
||||
MEMOIRE="$${SETOPS_MEMOIRE:-$(MEMOIRE)}" \
|
||||
NOEUD_PROXMOX="$${SETOPS_NOEUD:-$(NOEUD_PROXMOX)}" \
|
||||
VMID_MODELE="$(VMID_MODELE)" \
|
||||
FORMAT_DISQUE="$(FORMAT_DISQUE)" \
|
||||
|
|
|
|||
|
|
@ -1,102 +0,0 @@
|
|||
# Dimensionnement dérivé des ressources VM
|
||||
|
||||
Ce document décrit comment Set-OPS **estime les ressources d'une VM** (cœurs, RAM,
|
||||
disque) à partir des **propriétés des logiciels** qu'elle héberge et du **socle SE**,
|
||||
plutôt que de laisser chaque clone hériter aveuglément des specs du golden template.
|
||||
|
||||
> Décision actée le 2026-06-25 : extension assumée de la *modélisation* du plan de
|
||||
> contrôle (cf. `positionnement.md`). C'est le prolongement naturel de la dérivation
|
||||
> réseau (`nomenclature.yml`) — du dimensionnement, pas une fonction de type NetBox.
|
||||
|
||||
## Principe
|
||||
|
||||
```
|
||||
ressources(VM) = socle_SE + Σ empreinte(logiciel hébergé) → marge → arrondi
|
||||
```
|
||||
|
||||
Précédence (du plus fort au plus faible) :
|
||||
1. **Valeur explicite par hôte** dans `serveurs.yml` (`coeurs` / `memoire` / `disque`) — gagne toujours.
|
||||
2. **Valeur dérivée** (socle + empreintes + marge + arrondi) sinon.
|
||||
|
||||
Comme Ansible/Proxmox sont idempotents, ajuster une empreinte puis régénérer ne fait
|
||||
que recalculer la cible ; aucune dérive silencieuse.
|
||||
|
||||
## Où vit chaque donnée
|
||||
|
||||
| Donnée | Emplacement | Remarque |
|
||||
| --- | --- | --- |
|
||||
| Empreinte d'un logiciel | `roles/<groupe>/meta/empreinte.yml` | Propriété du logiciel, voyage avec le rôle. Fichier *pur données* (parsable sans Jinja). |
|
||||
| Groupes sans rôle dédié | `EMPREINTES_SANS_ROLE` dans `scripts/inventory_rules.py` | p. ex. `serveur_web_frontal`, `serveur_web_dorsal`. |
|
||||
| Repli ultime | `EMPREINTE_DEFAUT` | groupe inconnu : empreinte minimale. |
|
||||
| Socle SE | `SOCLE_SE` dans `inventory_rules.py` | coût de base Debian durci. |
|
||||
| Override par hôte | `serveurs.yml` (`coeurs`/`memoire`/`disque`) | déjà supporté par le générateur (`PLACEMENT`). |
|
||||
| Politique (marges, paliers, plafonds) | constantes en tête de `inventory_rules.py` | |
|
||||
|
||||
Format d'une empreinte (`roles/<rôle>/meta/empreinte.yml`) :
|
||||
```yaml
|
||||
setops_empreinte: { coeurs: 2, memoire_mo: 2048, disque_go: 20 }
|
||||
```
|
||||
|
||||
## Règles d'agrégation (valeurs en vigueur)
|
||||
|
||||
- **Socle SE** : 1 cœur / 512 Mo / 8 Go.
|
||||
- **RAM** : `(socle + Σ rôles) × 1.20`, arrondie au palier **512 Mo**, **min 1024 Mo**.
|
||||
- **Disque** : `(socle + Σ rôles) × 1.25`, arrondi au palier **5 Go**, **min 12 Go**,
|
||||
émis sous la forme `"<n>G"`.
|
||||
- **Cœurs** : `max(socle, Σ rôles)`, entier, **min 1**, **plafond 8** (CPU *burstable* :
|
||||
ni additif strict, ni marge).
|
||||
|
||||
Seuls les **groupes de service** de l'hôte (issus de `applications.yml`) entrent dans
|
||||
la somme. Le socle (`serveur_debian`/`serveur_durci`) et les groupes d'état sont exclus.
|
||||
|
||||
## Chaîne technique
|
||||
|
||||
1. `scripts/inventory_rules.py` — `deriver_ressources(groupes_service, racine_roles)`
|
||||
lit les empreintes et calcule.
|
||||
2. `scripts/instancier.py` — pour chaque hôte, écrit `proxmox_coeurs`,
|
||||
`proxmox_memoire`, `proxmox_disque_taille` dans l'inventaire généré
|
||||
(via `setdefault` : un override `serveurs.yml` prime).
|
||||
3. `scripts/inventory_host.py` — `parametres-proxmox` émet `SETOPS_COEURS` /
|
||||
`SETOPS_MEMOIRE` / `SETOPS_DISQUE` lus par `make creer-vm`.
|
||||
4. `Makefile` (`creer-vm` → `cloner-vm`) relaie vers `proxmox_clone_coeurs` /
|
||||
`proxmox_clone_memoire`.
|
||||
5. `playbooks/proxmox/cloner_vm_debian.yml` — passe `cores`/`memory` à `proxmox_kvm`
|
||||
(avec `omit` si absent : aucune régression, on garde alors les specs du template).
|
||||
|
||||
## Empreintes actuelles
|
||||
|
||||
| Rôle | cœurs | RAM (Mo) | disque (Go) |
|
||||
| --- | --- | --- | --- |
|
||||
| serveur_step_ca | 1 | 256 | 2 |
|
||||
| serveur_powerdns | 1 | 512 | 2 |
|
||||
| serveur_sendmail | 1 | 512 | 5 |
|
||||
| serveur_nginx | 1 | 512 | 3 |
|
||||
| serveur_openldap | 1 | 512 | 3 |
|
||||
| serveur_keycloak | 2 | 1536 | 5 |
|
||||
| serveur_postgresql | 2 | 2048 | 20 |
|
||||
| serveur_redis | 1 | 512 | 2 |
|
||||
| serveur_forgejo | 1 | 1024 | 20 |
|
||||
| serveur_prometheus | 1 | 1024 | 20 |
|
||||
| serveur_loki | 1 | 1024 | 20 |
|
||||
| serveur_grafana | 1 | 512 | 2 |
|
||||
| serveur_icinga | 2 | 1024 | 10 |
|
||||
| serveur_web_frontal (repli) | 1 | 512 | 5 |
|
||||
| serveur_web_dorsal (repli) | 1 | 1024 | 10 |
|
||||
|
||||
## Ajuster
|
||||
|
||||
- **Une empreinte de logiciel** : éditer `roles/<rôle>/meta/empreinte.yml`.
|
||||
- **Un hôte précis** : poser `coeurs`/`memoire`/`disque` sur le serveur dans
|
||||
`serveurs.yml` (l'override prime sur la dérivation).
|
||||
- **La politique** (marges, paliers, socle, plafond) : constantes en tête de
|
||||
`inventory_rules.py`.
|
||||
|
||||
Puis : `make instancier` (voir le diff), `make instancier-appliquer` (FORCE=1 si
|
||||
changement intentionnel).
|
||||
|
||||
## Limite connue
|
||||
|
||||
Les ressources s'appliquent **à la création de la VM** (clonage). Modifier une
|
||||
empreinte n'**ajuste pas** automatiquement une VM déjà existante : il faut un
|
||||
re-clonage ou un redimensionnement Proxmox manuel. Le **plan**, lui, reste la source
|
||||
de vérité et se régénère à volonté.
|
||||
|
|
@ -1,149 +0,0 @@
|
|||
# L'écosystème numérique Chezlepro
|
||||
|
||||
*Document de présentation — souveraineté et sécurité*
|
||||
|
||||
## En une phrase
|
||||
|
||||
Chezlepro est un **écosystème numérique souverain** : une infrastructure interne
|
||||
complète — identité, confiance, nommage, données, communication, observabilité,
|
||||
applications — hébergée sur sa propre grappe Proxmox, **sans dépendance à un service
|
||||
en nuage tiers**, entièrement décrite en code et reconstructible à volonté.
|
||||
|
||||
Le moteur qui la construit et l'entretient s'appelle **Set-OPS**. Il est *libre*,
|
||||
réutilisable par n'importe quel hébergeur, et **s'exploite entièrement à la main** :
|
||||
la documentation, les commandes `make` et l'interface locale suffisent. L'outil
|
||||
libère de la dépendance aux géants du numérique ; il ne la remplace pas par une
|
||||
dépendance à une intelligence artificielle.
|
||||
|
||||
---
|
||||
|
||||
## Pourquoi un écosystème, et pas seulement des serveurs
|
||||
|
||||
La plupart des organisations louent leur identité, leurs courriels, leurs noms de
|
||||
domaine et leur confiance numérique à quelques fournisseurs étrangers. Chaque brique
|
||||
louée est une dépendance, un coût récurrent et une perte de contrôle.
|
||||
|
||||
Chezlepro renverse cette logique : **chaque pilier de l'infrastructure est hébergé
|
||||
et maîtrisé en interne**, et les piliers sont reliés entre eux comme un tout
|
||||
cohérent.
|
||||
|
||||
| Pilier | Ce qu'il apporte |
|
||||
| --- | --- |
|
||||
| **Identité** | Annuaire interne (LDAP) et authentification unique (SSO) : un seul compte, partout. |
|
||||
| **Confiance** | Autorité de certification interne (PKI/ACME) : les serveurs se reconnaissent par certificats émis localement, sans acheter de confiance à l'extérieur. |
|
||||
| **Nommage** | DNS interne : des noms de machines stables et dérivables, plutôt que des adresses IP fragiles. |
|
||||
| **Données** | Bases relationnelles et cache, avec un registre des connexions applicatives. |
|
||||
| **Communication** | Relais courriel interne pour les alertes, notifications et réinitialisations. |
|
||||
| **Observabilité** | Métriques, journaux centralisés, tableaux de bord et supervision active. |
|
||||
| **Applicatif** | Services internes (forge logicielle, collaboration documentaire) derrière une couche web sécurisée. |
|
||||
|
||||
Tous ces services sont **libres** (OpenLDAP, Keycloak, step-ca, PowerDNS, PostgreSQL,
|
||||
Redis, NGINX, Prometheus, Loki, Grafana, Icinga, Forgejo, Sendmail…). Aucun verrou
|
||||
propriétaire, aucune licence captive.
|
||||
|
||||
---
|
||||
|
||||
## Comment c'est construit : reproductible par conception
|
||||
|
||||
Rien n'est configuré « à la main » sur un serveur. Tout part d'un **plan déclaratif**
|
||||
— quelques fichiers lisibles qui décrivent l'état voulu de l'infrastructure
|
||||
(les serveurs, les applications, les bases de données, les domaines). À partir de ce
|
||||
plan :
|
||||
|
||||
1. l'inventaire des machines est **généré** automatiquement ;
|
||||
2. chaque VM est **clonée** depuis un modèle Debian de référence (« golden template ») durci une fois pour toutes ;
|
||||
3. sa configuration réelle est **appliquée par Ansible**, de façon idempotente (on peut la relancer sans effet de bord).
|
||||
|
||||
**Conséquence concrète pour la résilience** : si une machine est perdue, elle se
|
||||
reconstruit à l'identique depuis le plan et le code. L'infrastructure n'est pas un
|
||||
objet fragile patiemment bricolé — c'est un artefact reproductible.
|
||||
|
||||
> **Honnêteté sur le statut.** Une grande partie de l'écosystème est aujourd'hui
|
||||
> *définie, codée et validée* (vérification de syntaxe, analyse statique) mais n'a pas
|
||||
> encore été éprouvée sur des machines de production réelles. Le socle Debian durci et
|
||||
> son modèle sont la fondation établie ; les services applicatifs sont prêts en tant
|
||||
> que code et se déploient progressivement. Nous ne présentons jamais un service non
|
||||
> déployé comme « en production ».
|
||||
|
||||
---
|
||||
|
||||
## Mesures de renforcement (durcissement)
|
||||
|
||||
La sécurité n'est pas une couche ajoutée après coup : elle est **intégrée dès le
|
||||
socle commun** de chaque machine, puis renforcée par un groupe de durcissement
|
||||
appliqué aux serveurs. Voici les mesures réellement en place dans le moteur.
|
||||
|
||||
### 1. Accès SSH verrouillé
|
||||
|
||||
L'accès distant est la porte d'entrée la plus exposée ; elle est fermée par défaut à
|
||||
tout ce qui n'est pas strictement nécessaire.
|
||||
|
||||
- **Authentification par clé uniquement** — le mot de passe est désactivé (`PasswordAuthentication no`, `AuthenticationMethods publickey`).
|
||||
- **Connexion root interdite** (`PermitRootLogin no`), pas de mots de passe vides, pas d'authentification par hôte.
|
||||
- **Aucune redirection** : X11, agent, ports TCP, tunnels, sockets — tout est désactivé.
|
||||
- **Sessions limitées** : déconnexion automatique des sessions inactives, nombre de tentatives plafonné (`MaxAuthTries 3`), délai de grâce court (`LoginGraceTime 20s`), sessions simultanées limitées.
|
||||
- **Garde-fou opérationnel** : toute modification SSH est validée (`sshd -t`) *avant* d'être rechargée, et l'authentification par mot de passe n'est jamais coupée avant d'avoir confirmé que l'accès par clé fonctionne — pour ne jamais se verrouiller dehors.
|
||||
|
||||
### 2. Protection contre le brute-force
|
||||
|
||||
- **fail2ban** surveille les tentatives de connexion SSH et bannit automatiquement les adresses fautives (5 échecs en 10 min → bannissement d'1 h).
|
||||
|
||||
### 3. Durcissement du noyau (sysctl)
|
||||
|
||||
Une trentaine de paramètres noyau resserrent le comportement du système et du réseau, notamment :
|
||||
|
||||
- **Réseau** : rejet des redirections et du routage par la source ICMP/IP, protection anti-spoofing (`rp_filter`), cookies SYN contre les inondations, journalisation des paquets « martiens », rejet des broadcasts ICMP.
|
||||
- **Noyau** : ASLR activé (`randomize_va_space`), restriction de `dmesg` et des pointeurs noyau, blocage de Ctrl-Alt-Suppr, restriction de `perf_event`.
|
||||
- **Système de fichiers** : protection des liens durs/symboliques, des FIFO et fichiers réguliers, interdiction des core dumps SUID.
|
||||
|
||||
### 4. Pare-feu prêt, activé au bon moment
|
||||
|
||||
- **nftables** est installé et préparé sur le socle, mais **volontairement désactivé dans le modèle**. Il est activé sur les serveurs finaux avec des règles adaptées à leur rôle (politique par défaut « tout refuser » en entrée et en transit). Principe : on n'active jamais un pare-feu générique sans connaître la fonction réelle de la machine, pour ne pas couper l'accès par accident.
|
||||
|
||||
### 5. Confinement et intégrité
|
||||
|
||||
- **AppArmor** : confinement des applications par profils.
|
||||
- **auditd** : journalisation d'audit du système avec un jeu de règles Set-OPS.
|
||||
- **debsums / needrestart / apt-listchanges** : vérification de l'intégrité des paquets et détection des services à redémarrer après mise à jour.
|
||||
|
||||
### 6. Mises à jour de sécurité automatiques
|
||||
|
||||
- **unattended-upgrades** applique automatiquement les correctifs de sécurité Debian. Le redémarrage automatique reste désactivé par défaut (décision laissée à l'opérateur), et les dépendances inutilisées sont nettoyées.
|
||||
|
||||
### 7. Journalisation maîtrisée
|
||||
|
||||
- **journald** est borné (rétention et taille plafonnées) pour garder des traces utiles sans saturer le disque. Les journaux peuvent ensuite être centralisés vers Loki.
|
||||
|
||||
### 8. Politique de mots de passe et gestion des secrets
|
||||
|
||||
- **libpam-pwquality** impose une qualité minimale des mots de passe locaux.
|
||||
- **Aucun secret n'est stocké en clair dans le code.** Mots de passe, clés privées, jetons et certificats sont gérés hors dépôt (Ansible Vault, fichiers locaux non versionnés, gestionnaire de secrets). Le modèle de VM ne contient ni données de clone, ni secret, ni clé privée.
|
||||
|
||||
### 9. Garde-fous sur les actions destructives
|
||||
|
||||
- Toute opération risquée (modification bloquante de SSH, activation d'un pare-feu, suppression d'utilisateurs ou de paquets critiques, formatage, purge de données, redémarrage massif, opérations de stockage…) **exige une confirmation explicite**. Sans elle, l'exécution est refusée.
|
||||
|
||||
### 10. Sécurité par l'architecture
|
||||
|
||||
Au-delà des réglages machine, l'architecture elle-même est une mesure de sécurité :
|
||||
|
||||
- **Tout service exposé en HTTP(S) passe par un point d'entrée unique** (NGINX en mandataire inverse, terminaison TLS, WAF) plutôt que d'être exposé directement.
|
||||
- **Authentification centralisée** : les services à accès humain se branchent au SSO, ce qui évite la multiplication des comptes et des mots de passe dispersés.
|
||||
- **Certificats internes** : les services se font confiance via la PKI interne, sans exposer de secrets à des autorités externes.
|
||||
- **Dépendances déclarées** : un service ne se déploie pas si ses prérequis (DNS, PKI…) ne sont pas réellement actifs — ce qui évite les états incohérents.
|
||||
- **Source unique de vérité** : l'état voulu vit dans des registres lisibles, versionnés par Git, qui sert de filet en cas d'erreur.
|
||||
|
||||
---
|
||||
|
||||
## Souveraineté jusqu'au bout
|
||||
|
||||
Trois principes résument la posture de Chezlepro :
|
||||
|
||||
1. **Tout est libre.** Aucune brique propriétaire, aucune licence captive.
|
||||
2. **Aucune dépendance externe pour les fonctions vitales** — confiance, identité, nom, communication sont hébergées et maîtrisées en interne.
|
||||
3. **Exploitable sans IA, par un humain.** La documentation, `make` et l'interface locale forment l'interface complète. L'intelligence artificielle n'assiste éventuellement que le mainteneur de l'outil — jamais l'utilisateur final, qui reste pleinement autonome.
|
||||
|
||||
---
|
||||
|
||||
*Détails techniques et runbooks : voir `AGENTS.md` (autorité du dépôt),
|
||||
`docs/catalogue-services.md`, `docs/plan-et-generation.md`, `docs/positionnement.md`.*
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
# Note de conception — panneau « Intrants de base » du GUI
|
||||
|
||||
But : saisir depuis **un endroit unique** les [intrants communs](intrants-communs.md)
|
||||
de l'écosystème, en distinguant **constantes** et **défauts surchargeables**.
|
||||
|
||||
> Extension assumée du plan de contrôle (gelé — cf. [`positionnement.md`](positionnement.md)),
|
||||
> au même titre que le [dimensionnement](dimensionnement-ressources.md). Décidée le
|
||||
> 2026-06-26.
|
||||
|
||||
## 1. Décisions cadre (validées)
|
||||
|
||||
1. **Secrets : hors périmètre.** Le GUI n'affiche ni ne stocke aucun secret. Les
|
||||
`vault_*` et tokens Proxmox restent édités via Ansible Vault en ligne de commande.
|
||||
Le panneau peut, au plus, afficher une **liste de rappel en lecture seule** des
|
||||
secrets attendus (sans valeur).
|
||||
2. **Nomenclature : lecture seule** dans le panneau. L'édition de `supernet`/VLAN/
|
||||
catégories reste dans `plan/nomenclature.yml` (autorité unique du plan réseau).
|
||||
3. **Conception avant code** (cette note).
|
||||
|
||||
## 2. Modèle : constante vs défaut surchargeable
|
||||
|
||||
- **Constante** — une seule valeur, pas de surcharge. Éditée *uniquement* au panneau ;
|
||||
apparaît en **lecture seule** dans les formulaires d'hôte (contexte).
|
||||
- **Défaut surchargeable** — valeur globale qui **ressurgit** comme défaut là où
|
||||
l'intrant réapparaît (hôte / groupe). Le formulaire d'hôte la montre **pré-remplie
|
||||
et marquée « hérité »** ; toute saisie locale devient une **surcharge** (badge
|
||||
« surchargé » + bouton « rétablir le défaut »).
|
||||
|
||||
Classification de départ : voir [`intrants-communs.md` §2](intrants-communs.md).
|
||||
Résumé : *Constantes* = `domaine_interne`, nomenclature, accès Proxmox, golden
|
||||
template, secrets. *Défauts* = timezone, nœud/stockage/pont Proxmox, DNS internes,
|
||||
politiques de durcissement, relais SMTP, `ciuser`/compte `ansible`.
|
||||
|
||||
## 3. Persistance — fichiers cibles, sans perte de commentaires
|
||||
|
||||
Contrainte : pas de dépendance non-stdlib (souveraineté) → pas de round-trip YAML
|
||||
préservant les commentaires (ruamel). Solution : **fichiers possédés par le GUI**,
|
||||
réécrits en bloc avec un **en-tête généré** (comme `hosts.genere.yml`), à côté des
|
||||
fichiers tenus à la main.
|
||||
|
||||
| Domaine | Fichier possédé par le GUI | Tenu à la main (coexiste) |
|
||||
| --- | --- | --- |
|
||||
| Identité + politiques (défauts) | `group_vars/all/10-intrants.yml` | `group_vars/all/00-instance.yml` |
|
||||
| Proxmox (constantes + défauts) | `group_vars/proxmox/10-intrants.yml` | `group_vars/proxmox/00-base.yml` |
|
||||
| Template/durcissement (défauts) | `group_vars/modeles_vm/10-intrants.yml` | `group_vars/modeles_vm/00-base.yml` |
|
||||
|
||||
> Migration légère : convertir `group_vars/all.yml` → répertoire `group_vars/all/`
|
||||
> (Ansible le supporte nativement). Idem `proxmox.yml`, `modeles_vm.yml`. Les valeurs
|
||||
> existantes sont scindées : commentaires/contexte dans `00-*`, valeurs pilotées par
|
||||
> le GUI dans `10-intrants.yml`. La précédence Ansible reste identique.
|
||||
|
||||
Les **surcharges par hôte** continuent de vivre dans `plan/serveurs.yml` (déjà
|
||||
supporté) ; par groupe, dans le `group_vars/<groupe>/` correspondant.
|
||||
|
||||
## 4. Mécanique d'héritage (côté générateur)
|
||||
|
||||
- Le panneau écrit les **défauts** dans les `10-intrants.yml`.
|
||||
- `instancier.py` / les formulaires lisent ces défauts pour **pré-remplir** et
|
||||
marquer « hérité » ; une valeur présente dans `serveurs.yml` (hôte) **prime** et
|
||||
s'affiche « surchargé » (même logique `setdefault` que le dimensionnement).
|
||||
- Les **constantes** ne sont jamais réémises par hôte : un seul point de vérité.
|
||||
|
||||
## 5. Écran (UI)
|
||||
|
||||
- Bouton **« Intrants de base »** dans l'en-tête du GUI → panneau modal/plein écran.
|
||||
- Sections repliables par domaine : **Identité**, **Proxmox**, **Politiques de
|
||||
durcissement**, **Services centraux**, **Nomenclature (lecture seule)**,
|
||||
**Secrets attendus (lecture seule)**.
|
||||
- Chaque champ porte un badge **Constante** / **Défaut**, et l'info-bulle d'aide
|
||||
(réutilise le mécanisme `AIDES` déjà en place).
|
||||
- Bandeau d'avertissement sur `domaine_interne` (changement à fort impact : re-dérive
|
||||
zones, FQDN, base DN…) → confirmation explicite.
|
||||
|
||||
## 6. Garde-fous
|
||||
|
||||
- `--syntax-check` + `ansible-inventory --list` doivent rester verts après écriture.
|
||||
- Discipline **diff-vide** : `make instancier` montre l'impact, application via FORCE
|
||||
si intentionnel.
|
||||
- Aucune valeur secrète n'entre dans un fichier écrit par le GUI (vérifié par un test).
|
||||
|
||||
## 7. Périmètre proposé (MVP → suite)
|
||||
|
||||
- **MVP** : `domaine_interne` (constante, avec garde-fou), `chezlepro_timezone`
|
||||
(défaut), accès Proxmox (constantes), golden template (constantes), placement
|
||||
Proxmox par défaut (nœud/stockage/pont), + nomenclature et secrets en lecture seule.
|
||||
- **Suite** : politiques de durcissement (défauts par groupe), DNS internes, relais
|
||||
SMTP, endpoints services centraux.
|
||||
|
||||
## 8. Points ouverts à confirmer
|
||||
- OK pour la **migration `group_vars/*.yml` → répertoires** (§3) ? (alternative :
|
||||
réécrire les fichiers existants en bloc, au prix des commentaires).
|
||||
- Le panneau affiche-t-il la **liste de rappel des secrets attendus** (lecture seule),
|
||||
ou on n'en parle pas du tout dans le GUI ?
|
||||
- Périmètre MVP (§7) suffisant pour une première itération ?
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
# Intrants communs de l'écosystème
|
||||
|
||||
Recensement des **intrants communs** : les valeurs partagées par tout l'écosystème
|
||||
(par opposition aux valeurs propres à un seul hôte). Objectif : les saisir **une
|
||||
fois**, depuis un endroit unique, puis les laisser se **dériver** ou se **propager**.
|
||||
|
||||
> Concept lié : [`meta-classe.md`](meta-classe.md) (une définition instancie la flotte)
|
||||
> et [`dimensionnement-ressources.md`](dimensionnement-ressources.md).
|
||||
|
||||
## 1. Recensement par domaine
|
||||
|
||||
### A. Identité de l'instance — `group_vars/<env>/all.yml`
|
||||
- **`domaine_interne`** — domaine DNS interne (ex. `chezlepro.internal`). **Keystone** :
|
||||
zones DNS, FQDN, base DN LDAP, expéditeurs courriel, URL AC en dérivent.
|
||||
- `chezlepro_timezone` — fuseau (ex. `America/Toronto`).
|
||||
- `setops_plan_dir` — chemin du plan.
|
||||
|
||||
### B. Réseau & nomenclature — `plan/nomenclature.yml`
|
||||
- `supernet`, `cidr_hote`, `reservations`, `categories` (VLAN/sous-réseau/passerelle),
|
||||
`fonctions` (catégorie + service).
|
||||
|
||||
### C. Hyperviseur Proxmox — `group_vars/proxmox.yml` (+ `proxmox.vault.yml`)
|
||||
- `proxmox_api_host`, `proxmox_api_user`, `proxmox_api_port`, `proxmox_validate_certs`.
|
||||
- 🔒 `proxmox_api_token_id`, `proxmox_api_token_secret` *(Vault)*.
|
||||
- Golden template : `proxmox_clone_vmid_modele`, `proxmox_clone_source_nom`.
|
||||
- Placement par défaut : `proxmox_clone_noeud`, `proxmox_clone_stockage`,
|
||||
`proxmox_clone_pont`, format, complet, timeout, disque, interface, démarrer.
|
||||
|
||||
### D. Identité initiale des VM — `group_vars/modeles_vm.yml`, `creer-vm`
|
||||
- **Clé publique SSH** (cloud-init) — accès admin de toute la flotte.
|
||||
- Compte technique `ansible` (`sudo_ansible_admin_user`) + sudo NOPASSWD, `ciuser`.
|
||||
- Politiques SSH communes (port, password auth off, root login off, grace/auth tries).
|
||||
|
||||
### E. Socle durci commun — `group_vars/modeles_vm.yml`
|
||||
nftables baseline · fail2ban SSH · auditd · AppArmor · sysctl · unattended-upgrades ·
|
||||
journald (rétention) · core_dumps · systemd_ssh_auto.
|
||||
|
||||
### F. Endpoints des services centraux (les rôles `client_*` en dérivent)
|
||||
- DNS interne (`client_dns_nameservers`, zone = `domaine_interne`).
|
||||
- AC/PKI (`client_pki_ca_url` → infra-pki, provisioner).
|
||||
- IdM/LDAP (`client_ldap_uri` → idm, base DN dérivé).
|
||||
- Relais courriel (`client_smtp_relais` → infra-mail, port 25).
|
||||
- Métriques (node_exporter `:9100`).
|
||||
- Journaux (`client_journal_loki_url` → obs).
|
||||
- Temps (NTP) : **implicite** (pool Debian par défaut — aucun intrant configuré).
|
||||
|
||||
### G. Dépôts APT tiers communs
|
||||
- Smallstep (step-cli) — `client_pki` ↔ `serveur_step_ca`.
|
||||
- Grafana (alloy/loki/grafana) — `client_journal` ↔ `serveur_loki`/`serveur_grafana`.
|
||||
|
||||
### H. Secrets Vault communs 🔒 — `proxmox.vault.yml` + vault d'instance
|
||||
`proxmox_api_token_id/secret`, `vault_step_ca_password`, `vault_step_ca_fingerprint`,
|
||||
`vault_step_ca_provisioner_password`, `vault_openldap_admin`, `vault_ldap_sssd`,
|
||||
`vault_keycloak_admin`, `vault_postgresql_keycloak`, `vault_bd_keycloak`,
|
||||
`vault_forgejo_admin`, `vault_forgejo_secret_key`, `vault_forgejo_internal_token`,
|
||||
`vault_bd_forgejo`, `vault_grafana_admin`, `vault_redis`, `vault_bd_icingadb`.
|
||||
Mot de passe du Vault lui-même : saisi au déploiement.
|
||||
|
||||
### I. Exposition publique — `plan/domaines.yml`
|
||||
domaines publics, `edge`, autorité DNS, FQDN exposés.
|
||||
|
||||
## 2. Classification proposée : constante vs défaut surchargeable
|
||||
|
||||
> **Proposition à valider.** « Constante » = une seule valeur, pas de surcharge
|
||||
> (diverger casserait le modèle). « Défaut surchargeable » = valeur globale qui
|
||||
> ressurgit comme défaut là où l'intrant réapparaît (hôte / groupe / application).
|
||||
|
||||
| Intrant | Classe | Surcharge où ? |
|
||||
| --- | --- | --- |
|
||||
| `domaine_interne` | **Constante** | — |
|
||||
| Nomenclature (supernet, CIDR, catégories, fonctions) | **Constante** | — (le plan réseau est la loi) |
|
||||
| Accès Proxmox (API host/user/port/token) | **Constante** | — (un seul cluster) |
|
||||
| Golden template (vmid_modele, source_nom) | **Constante** | — |
|
||||
| Secrets Vault | **Constante** 🔒 | — (gérés à part, jamais en clair) |
|
||||
| `chezlepro_timezone` | Défaut | par hôte (rare) |
|
||||
| `proxmox_clone_noeud` / `stockage` / `pont` | Défaut | par hôte (`serveurs.yml`) |
|
||||
| DNS internes (`client_dns_nameservers`) | Défaut | par hôte / groupe |
|
||||
| Politiques durcissement (SSH, nftables, fail2ban, journald…) | Défaut | par hôte / groupe |
|
||||
| Relais SMTP, TLS internes | Défaut | par hôte / groupe |
|
||||
| `ciuser`, compte `ansible` | Défaut | rarement surchargé |
|
||||
|
||||
## 3. Saisie unique dans le GUI (cible)
|
||||
|
||||
Un panneau « Intrants de base » dans le GUI :
|
||||
- édite les **constantes** et les **défauts** au même endroit ;
|
||||
- pour un **défaut**, la valeur globale apparaît pré-remplie (et signalée comme
|
||||
« héritée ») là où l'intrant réapparaît dans le plan, surchargeable sur place ;
|
||||
- les **secrets** ne sont **jamais** stockés en clair par le GUI (cf. §H) — il en
|
||||
gère au plus les *références*, l'édition réelle restant côté Vault.
|
||||
|
||||
Persistance : les fichiers existants (`group_vars/all.yml`, `proxmox.yml`,
|
||||
`modeles_vm.yml`, `plan/nomenclature.yml`). À détailler dans la note de conception
|
||||
de la fonctionnalité GUI.
|
||||
|
||||
## 4. Incohérences repérées (à corriger)
|
||||
- `chezlepro_timezone` défini en **lab** seulement, absent de **production**.
|
||||
- `group_vars/serveur_debian.yml` **référencé** (commentaire de prod `all.yml`) mais
|
||||
**absent** des deux environnements.
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
# La méta-classe — une définition, tout l'écosystème
|
||||
|
||||
Set-OPS se comporte comme une **méta-classe** : on n'écrit pas onze serveurs, on
|
||||
écrit *une* **définition d'écosystème** (le plan déclaratif) ; l'instanciation
|
||||
engendre la flotte entière, cohérente. Le réseau, la taille des VM, les groupes, les
|
||||
liens de bases (DSN) et l'exposition DNS sont **dérivés** — jamais saisis deux fois.
|
||||
|
||||
> Concept acté dans [`positionnement.md`](positionnement.md) : « la définition
|
||||
> instancie toute la flotte ». La dérivation des ressources est décrite dans
|
||||
> [`dimensionnement-ressources.md`](dimensionnement-ressources.md).
|
||||
|
||||
## Schéma
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph DEF["① DÉFINITION — la méta-classe (le plan déclaratif)"]
|
||||
direction LR
|
||||
NOM["nomenclature.yml<br/>catégories · VLAN · fonctions"]
|
||||
SRV["serveurs.yml<br/>VM = fonction + état + overrides"]
|
||||
APP["applications.yml<br/>app → rôle · hôte · expose"]
|
||||
BD["bases-donnees.yml<br/>serveurs_bd · DSN · portée"]
|
||||
DOM["domaines.yml<br/>domaines publics · edge"]
|
||||
EMP["roles/*/meta/empreinte.yml<br/>empreinte ressources"]
|
||||
TPL[("golden template<br/>Debian 13 durci")]
|
||||
end
|
||||
|
||||
subgraph GEN["② DÉRIVATION — make instancier (le constructeur)"]
|
||||
direction LR
|
||||
R1["réseau<br/>VMID · VLAN · IP · gw"]
|
||||
R2["ressources<br/>cœurs · RAM · disque"]
|
||||
R3["groupes<br/>socle + services + état"]
|
||||
R4["liens<br/>DSN + exposition DNS"]
|
||||
end
|
||||
|
||||
subgraph INST["③ INSTANCES — la flotte (inventory hosts.yml)"]
|
||||
direction LR
|
||||
INV[["hosts.yml"]]
|
||||
VMS(("11 VM<br/>infra-pki · dns · mail · edge<br/>idm · data · obs · mon · forge · web"))
|
||||
end
|
||||
|
||||
ECO["④ ÉCOSYSTÈME souverain en service<br/>PKI · DNS · IdM/SSO · Données · Observabilité · Forge · Web"]
|
||||
|
||||
DEF --> GEN --> INST
|
||||
TPL -. "clone + cloud-init" .-> VMS
|
||||
INV --> VMS
|
||||
INST -- "make deployer · rôles par groupe" --> ECO
|
||||
```
|
||||
|
||||
## Le même schéma en texte
|
||||
|
||||
```
|
||||
① DÉFINITION — la « méta-classe » (instance/plan/ + roles/*/meta/empreinte.yml)
|
||||
nomenclature · serveurs · applications · bases-données · domaines
|
||||
⟦ golden template Debian 13 durci ⟧
|
||||
│ make instancier
|
||||
▼
|
||||
② DÉRIVATION — le constructeur (instancier.py · inventory_rules · filter_plugins)
|
||||
réseau (VMID·VLAN·IP·gw) · ressources (cœurs·RAM·disque) · groupes · DSN+DNS
|
||||
│ = instanciation
|
||||
▼
|
||||
③ INSTANCES — la flotte (inventory hosts.yml : 11 VM cohérentes)
|
||||
infra-pki·dns·mail·edge · idm · data · obs · mon · forge · web
|
||||
▲ clone du golden template + identité cloud-init
|
||||
│ make deployer (rôles Ansible par groupe)
|
||||
▼
|
||||
④ ÉCOSYSTÈME souverain en service
|
||||
PKI · DNS interne · IdM+SSO · Bases · Observabilité · Forge · Web
|
||||
|
||||
1 définition ───────────────▶ N VM : réseau, taille, groupes, DSN, DNS
|
||||
(édite le plan) tout dérivé, rien saisi deux fois.
|
||||
```
|
||||
|
||||
## Pourquoi c'est une *méta*-classe
|
||||
|
||||
Une classe ordinaire instancie **un** objet. Ici, **une** définition instancie un
|
||||
**ensemble cohérent** d'objets (VM + services + liens), avec leurs invariants garantis
|
||||
par dérivation plutôt que par discipline humaine. Éditer le plan, c'est faire évoluer
|
||||
la classe ; régénérer, c'est ré-instancier — de façon idempotente.
|
||||
|
|
@ -121,8 +121,6 @@
|
|||
vmid: "{{ proxmox_clone_vmid_modele | int }}"
|
||||
newid: "{{ proxmox_clone_vmid | int }}"
|
||||
name: "{{ proxmox_clone_nom }}"
|
||||
cores: "{{ proxmox_clone_coeurs | int if proxmox_clone_coeurs is defined and proxmox_clone_coeurs | string | length > 0 else omit }}"
|
||||
memory: "{{ proxmox_clone_memoire | int if proxmox_clone_memoire is defined and proxmox_clone_memoire | string | length > 0 else omit }}"
|
||||
storage: "{{ proxmox_clone_stockage | default(omit, true) }}"
|
||||
format: "{{ proxmox_clone_format | default(omit, true) }}"
|
||||
full: "{{ proxmox_clone_complet | default(true) | bool }}"
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
# Empreinte ressources du logiciel — dimensionnement VM derive (scripts/instancier.py).
|
||||
# Sommee au socle SE + marge pour estimer coeurs/RAM/disque de la VM hote.
|
||||
# Override possible par hote dans le plan (serveurs.yml: coeurs/memoire/disque).
|
||||
setops_empreinte:
|
||||
coeurs: 1
|
||||
memoire_mo: 1024
|
||||
disque_go: 20
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
# Empreinte ressources du logiciel — dimensionnement VM derive (scripts/instancier.py).
|
||||
# Sommee au socle SE + marge pour estimer coeurs/RAM/disque de la VM hote.
|
||||
# Override possible par hote dans le plan (serveurs.yml: coeurs/memoire/disque).
|
||||
setops_empreinte:
|
||||
coeurs: 1
|
||||
memoire_mo: 512
|
||||
disque_go: 2
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
# Empreinte ressources du logiciel — dimensionnement VM derive (scripts/instancier.py).
|
||||
# Sommee au socle SE + marge pour estimer coeurs/RAM/disque de la VM hote.
|
||||
# Override possible par hote dans le plan (serveurs.yml: coeurs/memoire/disque).
|
||||
setops_empreinte:
|
||||
coeurs: 2
|
||||
memoire_mo: 1024
|
||||
disque_go: 10
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
# Empreinte ressources du logiciel — dimensionnement VM derive (scripts/instancier.py).
|
||||
# Sommee au socle SE + marge pour estimer coeurs/RAM/disque de la VM hote.
|
||||
# Override possible par hote dans le plan (serveurs.yml: coeurs/memoire/disque).
|
||||
setops_empreinte:
|
||||
coeurs: 2
|
||||
memoire_mo: 1536
|
||||
disque_go: 5
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
# Empreinte ressources du logiciel — dimensionnement VM derive (scripts/instancier.py).
|
||||
# Sommee au socle SE + marge pour estimer coeurs/RAM/disque de la VM hote.
|
||||
# Override possible par hote dans le plan (serveurs.yml: coeurs/memoire/disque).
|
||||
setops_empreinte:
|
||||
coeurs: 1
|
||||
memoire_mo: 1024
|
||||
disque_go: 20
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
# Empreinte ressources du logiciel — dimensionnement VM derive (scripts/instancier.py).
|
||||
# Sommee au socle SE + marge pour estimer coeurs/RAM/disque de la VM hote.
|
||||
# Override possible par hote dans le plan (serveurs.yml: coeurs/memoire/disque).
|
||||
setops_empreinte:
|
||||
coeurs: 1
|
||||
memoire_mo: 512
|
||||
disque_go: 3
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
# Empreinte ressources du logiciel — dimensionnement VM derive (scripts/instancier.py).
|
||||
# Sommee au socle SE + marge pour estimer coeurs/RAM/disque de la VM hote.
|
||||
# Override possible par hote dans le plan (serveurs.yml: coeurs/memoire/disque).
|
||||
setops_empreinte:
|
||||
coeurs: 1
|
||||
memoire_mo: 512
|
||||
disque_go: 3
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
# Empreinte ressources du logiciel — dimensionnement VM derive (scripts/instancier.py).
|
||||
# Sommee au socle SE + marge pour estimer coeurs/RAM/disque de la VM hote.
|
||||
# Override possible par hote dans le plan (serveurs.yml: coeurs/memoire/disque).
|
||||
setops_empreinte:
|
||||
coeurs: 2
|
||||
memoire_mo: 2048
|
||||
disque_go: 20
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
# Empreinte ressources du logiciel — dimensionnement VM derive (scripts/instancier.py).
|
||||
# Sommee au socle SE + marge pour estimer coeurs/RAM/disque de la VM hote.
|
||||
# Override possible par hote dans le plan (serveurs.yml: coeurs/memoire/disque).
|
||||
setops_empreinte:
|
||||
coeurs: 1
|
||||
memoire_mo: 512
|
||||
disque_go: 2
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
# Empreinte ressources du logiciel — dimensionnement VM derive (scripts/instancier.py).
|
||||
# Sommee au socle SE + marge pour estimer coeurs/RAM/disque de la VM hote.
|
||||
# Override possible par hote dans le plan (serveurs.yml: coeurs/memoire/disque).
|
||||
setops_empreinte:
|
||||
coeurs: 1
|
||||
memoire_mo: 1024
|
||||
disque_go: 20
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
# Empreinte ressources du logiciel — dimensionnement VM derive (scripts/instancier.py).
|
||||
# Sommee au socle SE + marge pour estimer coeurs/RAM/disque de la VM hote.
|
||||
# Override possible par hote dans le plan (serveurs.yml: coeurs/memoire/disque).
|
||||
setops_empreinte:
|
||||
coeurs: 1
|
||||
memoire_mo: 512
|
||||
disque_go: 2
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
# Empreinte ressources du logiciel — dimensionnement VM derive (scripts/instancier.py).
|
||||
# Sommee au socle SE + marge pour estimer coeurs/RAM/disque de la VM hote.
|
||||
# Override possible par hote dans le plan (serveurs.yml: coeurs/memoire/disque).
|
||||
setops_empreinte:
|
||||
coeurs: 1
|
||||
memoire_mo: 512
|
||||
disque_go: 5
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
# Empreinte ressources du logiciel — dimensionnement VM derive (scripts/instancier.py).
|
||||
# Sommee au socle SE + marge pour estimer coeurs/RAM/disque de la VM hote.
|
||||
# Override possible par hote dans le plan (serveurs.yml: coeurs/memoire/disque).
|
||||
setops_empreinte:
|
||||
coeurs: 1
|
||||
memoire_mo: 256
|
||||
disque_go: 2
|
||||
|
|
@ -30,12 +30,10 @@ from inventory_rules import (
|
|||
charger_nomenclature,
|
||||
charger_serveurs,
|
||||
deriver_nomenclature,
|
||||
deriver_ressources,
|
||||
fonction_seq,
|
||||
)
|
||||
|
||||
RACINE = Path(__file__).resolve().parents[1]
|
||||
ROLES = RACINE / "roles"
|
||||
INSTANCE = Path(os.environ.get("SETOPS_INSTANCE") or (RACINE / "instance"))
|
||||
INVENTAIRE = INSTANCE / "inventories/production/hosts.yml"
|
||||
GENERE = INSTANCE / "inventories/production/hosts.genere.yml"
|
||||
|
|
@ -77,12 +75,6 @@ def generer() -> dict:
|
|||
for cle_srv, cle_var in PLACEMENT:
|
||||
if str(srv.get(cle_srv, "")).strip():
|
||||
hostvars[cle_var] = srv[cle_srv]
|
||||
# Ressources derivees des logiciels (groupes de service) + socle SE.
|
||||
# setdefault : une valeur explicite du plan (PLACEMENT ci-dessus) gagne.
|
||||
ressources = deriver_ressources(sorted(services_par_hote.get(nom, set())), ROLES)
|
||||
hostvars.setdefault("proxmox_coeurs", ressources["coeurs"])
|
||||
hostvars.setdefault("proxmox_memoire", ressources["memoire"])
|
||||
hostvars.setdefault("proxmox_disque_taille", ressources["disque"])
|
||||
etat = "hotes_actifs" if srv.get("etat") == "actif" else "hotes_planifies"
|
||||
children[etat]["hosts"][nom] = hostvars
|
||||
groupes = set(GROUPES_SOCLE) | services_par_hote.get(nom, set()) | set(srv.get("integrations") or [])
|
||||
|
|
|
|||
|
|
@ -48,42 +48,6 @@ FICHIER_APPLICATIONS = INSTANCE / "plan/applications.yml"
|
|||
FICHIER_DOMAINES = INSTANCE / "plan/domaines.yml"
|
||||
FICHIER_SERVEURS = INSTANCE / "plan/serveurs.yml"
|
||||
|
||||
# Intrants de base (panneau GUI). Cibles d'ecriture possedees par le GUI.
|
||||
# Identite : source UNIQUE partagee par tous les envs (symlink depuis chaque
|
||||
# group_vars/all/). Proxmox : fichier lab (le clonage le lit en dur), cles gerees
|
||||
# mises a jour, les autres cles preservees. Les SECRETS n'y entrent jamais.
|
||||
INTRANTS_IDENTITE = INSTANCE / "inventories/partage/intrants-identite.yml"
|
||||
INTRANTS_PROXMOX = INSTANCE / "inventories/lab/group_vars/proxmox.yml"
|
||||
FICHIERS_INTRANTS = {"identite": INTRANTS_IDENTITE, "proxmox": INTRANTS_PROXMOX}
|
||||
|
||||
# (variable, fichier, classe, section, libelle, type)
|
||||
INTRANTS_SCHEMA = [
|
||||
("domaine_interne", "identite", "constante", "Identité", "Domaine DNS interne", "str"),
|
||||
("chezlepro_timezone", "identite", "defaut", "Identité", "Fuseau horaire", "str"),
|
||||
("proxmox_api_host", "proxmox", "constante", "Proxmox", "Hôte API Proxmox", "str"),
|
||||
("proxmox_api_user", "proxmox", "constante", "Proxmox", "Utilisateur API", "str"),
|
||||
("proxmox_api_port", "proxmox", "constante", "Proxmox", "Port API", "str"),
|
||||
("proxmox_validate_certs", "proxmox", "constante", "Proxmox", "Valider les certificats TLS", "bool"),
|
||||
("proxmox_clone_source_nom", "proxmox", "constante", "Proxmox", "Nom du golden template", "str"),
|
||||
("proxmox_clone_vmid_modele", "proxmox", "constante", "Proxmox", "VMID du golden template", "int"),
|
||||
("proxmox_clone_noeud", "proxmox", "defaut", "Proxmox", "Nœud Proxmox (défaut)", "str"),
|
||||
("proxmox_clone_stockage", "proxmox", "defaut", "Proxmox", "Stockage (défaut)", "str"),
|
||||
("proxmox_clone_pont", "proxmox", "defaut", "Proxmox", "Pont réseau (défaut)", "str"),
|
||||
]
|
||||
# Cles secretes : interdites a l'ecriture par le GUI (garde-fou).
|
||||
INTRANTS_CLES_INTERDITES = {
|
||||
"proxmox_api_token_id", "proxmox_api_token_secret", "ansible_become_pass", "ansible_password",
|
||||
}
|
||||
# Rappel en lecture seule (jamais de valeur) : secrets a fournir via Ansible Vault.
|
||||
SECRETS_ATTENDUS = [
|
||||
"proxmox_api_token_id", "proxmox_api_token_secret",
|
||||
"vault_step_ca_password", "vault_step_ca_fingerprint", "vault_step_ca_provisioner_password",
|
||||
"vault_openldap_admin", "vault_ldap_sssd", "vault_keycloak_admin",
|
||||
"vault_postgresql_keycloak", "vault_bd_keycloak", "vault_forgejo_admin",
|
||||
"vault_forgejo_secret_key", "vault_forgejo_internal_token", "vault_bd_forgejo",
|
||||
"vault_grafana_admin", "vault_redis", "vault_bd_icingadb",
|
||||
]
|
||||
|
||||
# Garde-fous des executions (verifier / deployer) depuis l'interface.
|
||||
JETON = secrets.token_urlsafe(18)
|
||||
VERROU = threading.Lock()
|
||||
|
|
@ -246,86 +210,9 @@ def inventaire_api(path: Path) -> dict:
|
|||
"domaines": charger_domaines(FICHIER_DOMAINES),
|
||||
"serveurs": serveurs_reconcilies(data),
|
||||
"hotes": liste_hotes(data),
|
||||
"intrants": intrants_api(),
|
||||
}
|
||||
|
||||
|
||||
def _lire_yaml_dict(path: Path) -> dict:
|
||||
if not path.exists():
|
||||
return {}
|
||||
with path.open("r", encoding="utf-8") as fichier:
|
||||
data = yaml.safe_load(fichier) or {}
|
||||
return data if isinstance(data, dict) else {}
|
||||
|
||||
|
||||
def intrants_courants() -> dict:
|
||||
cache: dict = {}
|
||||
valeurs: dict = {}
|
||||
for variable, fcle, *_ in INTRANTS_SCHEMA:
|
||||
if fcle not in cache:
|
||||
cache[fcle] = _lire_yaml_dict(FICHIERS_INTRANTS[fcle])
|
||||
v = cache[fcle].get(variable, "")
|
||||
valeurs[variable] = "" if v is None else v
|
||||
return valeurs
|
||||
|
||||
|
||||
def intrants_api() -> dict:
|
||||
schema = [{"variable": v, "fichier": f, "classe": c, "section": s,
|
||||
"libelle": l, "type": t} for (v, f, c, s, l, t) in INTRANTS_SCHEMA]
|
||||
return {
|
||||
"schema": schema,
|
||||
"valeurs": intrants_courants(),
|
||||
"secrets_attendus": SECRETS_ATTENDUS,
|
||||
"fichiers": {k: os.path.relpath(v, RACINE) for k, v in FICHIERS_INTRANTS.items()},
|
||||
}
|
||||
|
||||
|
||||
def _coercer_intrant(typ: str, brut: object) -> object:
|
||||
s = str(brut).strip()
|
||||
if typ == "bool":
|
||||
return s.lower() in ("true", "1", "oui", "yes", "on")
|
||||
if typ == "int":
|
||||
return int(s) if s.lstrip("-").isdigit() else s
|
||||
return s
|
||||
|
||||
|
||||
def _ecrire_intrants_fichier(path: Path, valeurs: dict, entete: str) -> None:
|
||||
interdits = sorted(set(valeurs) & INTRANTS_CLES_INTERDITES)
|
||||
if interdits:
|
||||
raise ValueError(f"Refus d'ecrire des cles sensibles via le GUI: {', '.join(interdits)}")
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with path.open("w", encoding="utf-8") as fichier:
|
||||
fichier.write(entete)
|
||||
yaml.safe_dump(valeurs, fichier, default_flow_style=False, sort_keys=True, allow_unicode=True)
|
||||
|
||||
|
||||
def ecrire_intrants(payload: dict) -> None:
|
||||
valeurs = payload.get("valeurs", {}) or {}
|
||||
if not str(valeurs.get("domaine_interne", "")).strip():
|
||||
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}
|
||||
fichier_de = {v: f for (v, f, c, s, l, t) in INTRANTS_SCHEMA}
|
||||
par_fichier: dict = {}
|
||||
for variable, valeur in valeurs.items():
|
||||
if variable not in fichier_de:
|
||||
continue
|
||||
par_fichier.setdefault(fichier_de[variable], {})[variable] = _coercer_intrant(types[variable], valeur)
|
||||
|
||||
if "identite" in par_fichier:
|
||||
_ecrire_intrants_fichier(
|
||||
INTRANTS_IDENTITE, par_fichier["identite"],
|
||||
"# Intrants d'IDENTITE de l'instance — SOURCE UNIQUE, partagee par tous les envs.\n"
|
||||
"# Edite par le panneau « Intrants de base » du GUI (make inventaire-ui).\n"
|
||||
"# Reference via symlink depuis inventories/<env>/group_vars/all/10-intrants.yml.\n---\n")
|
||||
if "proxmox" in par_fichier:
|
||||
fusion = _lire_yaml_dict(INTRANTS_PROXMOX) # preserve les cles non gerees
|
||||
fusion.update(par_fichier["proxmox"])
|
||||
_ecrire_intrants_fichier(
|
||||
INTRANTS_PROXMOX, fusion,
|
||||
"# Parametres Proxmox non sensibles (les secrets vont dans proxmox.vault.yml).\n"
|
||||
"# Cles d'intrants editees par le panneau « Intrants de base » du GUI.\n---\n")
|
||||
|
||||
|
||||
def ecrire_bases(path: Path, registre: dict) -> None:
|
||||
entete = (
|
||||
"# Registre des bases de donnees Set-OPS (serveurs_bd + bases_donnees).\n"
|
||||
|
|
@ -622,16 +509,6 @@ HTML = r"""<!doctype html>
|
|||
.champ { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
|
||||
.champ > span { color: var(--muted); font-size: 11px; font-weight: 700; }
|
||||
.champ .unite { color: var(--faint); font-weight: 600; }
|
||||
.champ > span.a-aide { cursor: help; border-bottom: 1px dotted var(--ligne-forte); width: fit-content; }
|
||||
#infobulle {
|
||||
position: fixed; z-index: 200; max-width: 260px; pointer-events: none;
|
||||
background: var(--surface-3); color: var(--texte);
|
||||
border: 1px solid var(--ligne-forte); border-radius: var(--rayon-sm);
|
||||
padding: 7px 10px; font-size: 12px; font-weight: 600; line-height: 1.4;
|
||||
box-shadow: var(--ombre); opacity: 0; transform: translateY(2px);
|
||||
transition: opacity .12s ease, transform .12s ease;
|
||||
}
|
||||
#infobulle.on { opacity: 1; transform: none; }
|
||||
.groupes-liste { display: grid; gap: 7px; }
|
||||
.groupe { display: block; border: 1px solid var(--ligne); border-radius: var(--rayon-sm); padding: 8px 10px; background: var(--surface-2); cursor: pointer; transition: border-color .12s, background .12s; }
|
||||
.groupe:hover { border-color: var(--ligne-forte); }
|
||||
|
|
@ -692,18 +569,6 @@ HTML = r"""<!doctype html>
|
|||
.modale-titre { font-weight: 800; font-size: 15px; }
|
||||
.modale-texte { color: var(--muted); font-size: 13px; line-height: 1.5; }
|
||||
.modale-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 4px; }
|
||||
.modale-boite.large { width: min(760px, 94vw); max-height: 88vh; overflow: auto; }
|
||||
.intrants-corps { display: grid; gap: 16px; }
|
||||
.intr-section { display: grid; gap: 9px; }
|
||||
.intr-tete { font-weight: 800; font-size: 13px; color: var(--bleu); border-bottom: 1px solid var(--ligne); padding-bottom: 5px; }
|
||||
.intr-grille { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 11px; }
|
||||
.badge-classe { display: inline-block; font-size: 9.5px; font-weight: 800; letter-spacing: .04em; text-transform: uppercase; padding: 1px 6px; border-radius: 999px; margin-left: 6px; vertical-align: middle; }
|
||||
.badge-classe.const { background: color-mix(in srgb, var(--ambre) 20%, transparent); color: var(--ambre); }
|
||||
.badge-classe.def { 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 b { color: var(--texte); font-weight: 700; }
|
||||
.intr-secrets { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
|
||||
.intr-secrets code { font-size: 11px; background: var(--surface-2); border: 1px solid var(--ligne); border-radius: var(--rayon-sm); padding: 1px 6px; color: var(--muted); }
|
||||
.console { position: fixed; left: 0; right: 0; bottom: 0; height: 44vh; background: var(--surface); border-top: 1px solid var(--ligne-forte); box-shadow: 0 -16px 50px -18px rgba(0,0,0,.8); z-index: 40; display: flex; flex-direction: column; }
|
||||
.console[hidden] { display: none; }
|
||||
.console-tete { display: flex; align-items: center; gap: 12px; padding: 9px 16px; border-bottom: 1px solid var(--ligne); background: var(--surface-2); }
|
||||
|
|
@ -735,7 +600,6 @@ HTML = r"""<!doctype html>
|
|||
</div>
|
||||
<span class="recherche"><input id="filtre" type="search" placeholder="Filtrer…" oninput="filtrer(this.value)"></span>
|
||||
<button type="button" class="fantome" onclick="ajouterHote()">+ Hôte</button>
|
||||
<button type="button" class="fantome" onclick="ouvrirIntrants()">⚙ Intrants</button>
|
||||
<button type="button" class="fantome" onclick="charger()">Recharger</button>
|
||||
<button type="button" class="primaire" id="btn-sauver" onclick="sauvegarder()">Sauvegarder</button>
|
||||
</div>
|
||||
|
|
@ -764,20 +628,6 @@ HTML = r"""<!doctype html>
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modale" id="intrants" hidden>
|
||||
<div class="modale-boite large">
|
||||
<div class="modale-titre">⚙ Intrants de base de l'écosystème</div>
|
||||
<div class="modale-texte">Saisis ici les valeurs communes à tout l'écosystème, depuis un endroit unique.
|
||||
Les <b>constantes</b> sont une valeur unique non surchargeable ; les <b>défauts</b> resurgissent
|
||||
comme valeurs proposées là où ils réapparaissent dans les instances déclarées.
|
||||
Les secrets ne sont jamais saisis ici : ils restent dans le Vault Ansible (CLI).</div>
|
||||
<div class="intrants-corps" id="intrants-corps"></div>
|
||||
<div class="modale-actions">
|
||||
<button type="button" onclick="fermerIntrants()">Fermer</button>
|
||||
<button type="button" class="primaire" onclick="enregistrerIntrants()">Enregistrer</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="console" id="console" hidden>
|
||||
<div class="console-tete">
|
||||
<span class="console-titre" id="console-titre"></span>
|
||||
|
|
@ -813,7 +663,6 @@ HTML = r"""<!doctype html>
|
|||
let verifie = {};
|
||||
let motVault = '';
|
||||
let modaleResolveur = null;
|
||||
let intrants = null;
|
||||
|
||||
const ONGLETS = {
|
||||
reseau: [
|
||||
|
|
@ -848,7 +697,6 @@ HTML = r"""<!doctype html>
|
|||
dependances = data.dependances || {};
|
||||
nomenclature = data.nomenclature || {};
|
||||
chaine = data.chaine || {};
|
||||
intrants = data.intrants || null;
|
||||
chargerBases(data);
|
||||
chargerApplications(data);
|
||||
chargerServeurs(data);
|
||||
|
|
@ -1533,107 +1381,6 @@ HTML = r"""<!doctype html>
|
|||
if (r) r(null);
|
||||
}
|
||||
|
||||
function ouvrirIntrants() {
|
||||
if (!intrants) { message('Intrants indisponibles (rechargez).', 'erreur'); return; }
|
||||
rendreIntrants();
|
||||
document.getElementById('intrants').hidden = false;
|
||||
}
|
||||
function fermerIntrants() { document.getElementById('intrants').hidden = true; }
|
||||
|
||||
function rendreIntrants() {
|
||||
const corps = document.getElementById('intrants-corps');
|
||||
const schema = intrants.schema || [];
|
||||
const valeurs = intrants.valeurs || {};
|
||||
const sections = [];
|
||||
const parSection = {};
|
||||
schema.forEach(ch => {
|
||||
if (!parSection[ch.section]) { parSection[ch.section] = []; sections.push(ch.section); }
|
||||
parSection[ch.section].push(ch);
|
||||
});
|
||||
let html = '';
|
||||
sections.forEach(sec => {
|
||||
html += '<div class="intr-section"><div class="intr-tete">' + echapper(sec) + '</div><div class="intr-grille">';
|
||||
parSection[sec].forEach(ch => {
|
||||
const v = valeurs[ch.variable];
|
||||
const badge = ch.classe === 'constante'
|
||||
? '<span class="badge-classe const" title="Valeur unique, non surchargeable">Constante</span>'
|
||||
: '<span class="badge-classe def" title="Valeur par défaut, surchargeable dans les instances">Défaut</span>';
|
||||
const lib = echapper(ch.libelle) + badge;
|
||||
if (ch.type === 'bool') {
|
||||
const coche = (v === true || v === 'true' || v === 'True') ? ' checked' : '';
|
||||
html += '<label class="champ"><span>' + lib + '</span>'
|
||||
+ '<input type="checkbox" data-intrant="' + echapper(ch.variable) + '" data-type="bool"' + coche + '></label>';
|
||||
} else {
|
||||
const t = ch.type === 'int' ? 'number' : 'text';
|
||||
html += '<label class="champ"><span>' + lib + '</span>'
|
||||
+ '<input type="' + t + '" data-intrant="' + echapper(ch.variable) + '" data-type="' + echapper(ch.type) + '"'
|
||||
+ ' value="' + echapper(v == null ? '' : v) + '"></label>';
|
||||
}
|
||||
});
|
||||
html += '</div></div>';
|
||||
});
|
||||
// Nomenclature en lecture seule (lue ailleurs : vue Nomenclature)
|
||||
const n = nomenclature || {};
|
||||
if (Object.keys(n).length) {
|
||||
html += '<div class="intr-section"><div class="intr-tete">Nomenclature (lecture seule)</div>'
|
||||
+ '<div class="intr-ro">';
|
||||
if (n.supernet) html += 'Supernet : <b>' + echapper(n.supernet) + '</b><br>';
|
||||
if (n.domaine || n.domaine_interne) html += 'Domaine : <b>' + echapper(n.domaine || n.domaine_interne) + '</b><br>';
|
||||
const nbCat = n.categories ? Object.keys(n.categories).length : 0;
|
||||
const nbFon = n.fonctions ? Object.keys(n.fonctions).length : 0;
|
||||
html += nbCat + ' catégorie(s), ' + nbFon + ' fonction(s) déclarée(s).';
|
||||
html += '<br><span style="font-style:italic">Modifiable dans le plan (nomenclature.yml), pas ici.</span>';
|
||||
html += '</div></div>';
|
||||
}
|
||||
// Secrets attendus : informatif, jamais saisis ici
|
||||
const secrets = intrants.secrets_attendus || [];
|
||||
if (secrets.length) {
|
||||
html += '<div class="intr-section"><div class="intr-tete">Secrets attendus (Vault — CLI)</div>'
|
||||
+ '<div class="intr-ro">Ces clés ne sont jamais saisies ni stockées ici. Édite-les via <b>ansible-vault</b>.'
|
||||
+ '<div class="intr-secrets">'
|
||||
+ secrets.map(s => '<code>' + echapper(s) + '</code>').join('')
|
||||
+ '</div></div></div>';
|
||||
}
|
||||
const f = intrants.fichiers || {};
|
||||
const chemins = Object.values(f).map(x => echapper(x)).join(', ');
|
||||
if (chemins) html += '<div class="intr-ro" style="margin-top:2px">Écrit dans : <b>' + chemins + '</b></div>';
|
||||
corps.innerHTML = html;
|
||||
}
|
||||
|
||||
async function enregistrerIntrants() {
|
||||
const valeurs = {};
|
||||
document.querySelectorAll('#intrants-corps [data-intrant]').forEach(el => {
|
||||
const k = el.getAttribute('data-intrant');
|
||||
valeurs[k] = el.type === 'checkbox' ? el.checked : el.value;
|
||||
});
|
||||
const ancienDomaine = (intrants.valeurs || {}).domaine_interne;
|
||||
const nouveauDomaine = valeurs.domaine_interne;
|
||||
if (nouveauDomaine !== undefined && nouveauDomaine !== ancienDomaine) {
|
||||
if (!confirm('Changer le domaine interne (« ' + (ancienDomaine || '∅') + ' » → « ' + (nouveauDomaine || '∅')
|
||||
+ ' ») impacte tout l\'écosystème (DNS, certificats, noms d\'hôtes). Continuer ?')) return;
|
||||
}
|
||||
try {
|
||||
const reponse = await fetch('/api/intrants', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json', 'X-Jeton': JETON},
|
||||
body: JSON.stringify({valeurs}),
|
||||
});
|
||||
if (!reponse.ok) {
|
||||
let detail = '';
|
||||
try { detail = (await reponse.json()).erreur || ''; } catch (e) { detail = await reponse.text(); }
|
||||
message('Échec de l\'enregistrement des intrants : ' + detail, 'erreur');
|
||||
return;
|
||||
}
|
||||
const data = await reponse.json();
|
||||
intrants = data.intrants || intrants;
|
||||
nomenclature = data.nomenclature || nomenclature;
|
||||
fermerIntrants();
|
||||
message('Intrants de base enregistrés.', 'ok');
|
||||
} catch (e) {
|
||||
message('Erreur réseau : ' + e, 'erreur');
|
||||
}
|
||||
}
|
||||
|
||||
async function lancer(mode, index) {
|
||||
const hote = hotes[index] ? hotes[index].nom : '';
|
||||
if (!hote) { message('Nommez l\'hôte d\'abord.', 'erreur'); return; }
|
||||
|
|
@ -1703,85 +1450,6 @@ HTML = r"""<!doctype html>
|
|||
if ((e.ctrlKey || e.metaKey) && e.key === 's') { e.preventDefault(); if (!occupe) sauvegarder(); }
|
||||
});
|
||||
|
||||
// ------ Info-bulles d'aide au survol de la description d'un champ ------
|
||||
(function () {
|
||||
const AIDES = {
|
||||
'Adresse IP': "Adresse IPv4 de la VM sur son VLAN. Ex. 10.10.12.11. Souvent dérivée — clic « Proposer ».",
|
||||
'CIDR': "Masque réseau en bits. 24 = /24 (255.255.255.0).",
|
||||
'Passerelle': "Routeur de sortie du VLAN. En général le .1 du sous-réseau.",
|
||||
'VLAN': "Identifiant 802.1Q (1–4094). Dérivé de la fonction via la nomenclature.",
|
||||
'Pont': "Pont réseau Proxmox. Ex. vmbr0.",
|
||||
'DNS': "Résolveur DNS de la VM. En général le DNS interne de l'écosystème.",
|
||||
'Utilisateur Ansible': "Compte technique utilisé par Ansible. Par défaut : ansible.",
|
||||
'VMID': "Identifiant unique Proxmox. Dérivé (catégorie + service + rang).",
|
||||
'Nœud': "Hôte Proxmox cible. Ex. pve-01.",
|
||||
'Stockage': "Pool de stockage Proxmox du disque. Ex. local-zfs.",
|
||||
'Disque': "Taille du disque système. Ex. 32G. Dérivée des logiciels hébergés + socle.",
|
||||
'Mémoire': "RAM en Mo. Dérivée des logiciels hébergés + socle (avec marge).",
|
||||
'Cœurs': "vCPU alloués. Dérivés des logiciels hébergés (maximum, pas additif).",
|
||||
'Nom': "Identifiant unique du serveur de base, sans espace.",
|
||||
'Type': "Moteur de la base. Ex. postgres, redis.",
|
||||
'Hôte': "VM qui héberge ce service (nom du serveur).",
|
||||
'Hôte (VM)': "VM cible où l'application est déployée.",
|
||||
'Port': "Port d'écoute du service.",
|
||||
'Groupe Ansible': "Groupe/rôle Set-OPS appliqué. Ex. serveur_postgresql.",
|
||||
'Groupe (rôle)': "Rôle Set-OPS appliqué à l'application.",
|
||||
'Identifiant': "Clé courte et unique, sans espace.",
|
||||
'Portée': "À qui la base est offerte : groupe, hôte ou application.",
|
||||
'Consommateur': "L'entité (groupe / hôte / application) qui utilise la base.",
|
||||
'Usage': "Étiquette libre. Ex. principale.",
|
||||
'Serveur': "Serveur de base de données qui héberge cette base.",
|
||||
'Base': "Nom de la base de données.",
|
||||
'Propriétaire': "Rôle SQL propriétaire de la base.",
|
||||
'Secret (Vault)': "Nom de la variable Ansible Vault contenant le mot de passe.",
|
||||
'Requiert (applis)': "Applications prérequises, séparées par des virgules.",
|
||||
'Expose (FQDN)': "Nom(s) public(s) exposé(s). Ex. forge.exemple.ca.",
|
||||
'Serveur (fonction-NN)': "Nom = fonction + rang. Ex. web-frontal-03.",
|
||||
};
|
||||
const bulle = document.createElement('div');
|
||||
bulle.id = 'infobulle';
|
||||
bulle.setAttribute('role', 'tooltip');
|
||||
document.body.appendChild(bulle);
|
||||
|
||||
function libelleDe(el) {
|
||||
const n = el.firstChild;
|
||||
return ((n && n.nodeType === 3 ? n.textContent : el.textContent) || '').trim();
|
||||
}
|
||||
function aideDe(el) {
|
||||
if (!el || !el.matches || !el.matches('.champ > span')) return null;
|
||||
return AIDES[libelleDe(el)] || null;
|
||||
}
|
||||
function placer(el) {
|
||||
const r = el.getBoundingClientRect();
|
||||
bulle.style.left = Math.max(8, Math.min(r.left, innerWidth - bulle.offsetWidth - 8)) + 'px';
|
||||
let haut = r.top - bulle.offsetHeight - 8;
|
||||
if (haut < 8) haut = r.bottom + 8; // bascule sous le champ si pas la place
|
||||
bulle.style.top = haut + 'px';
|
||||
}
|
||||
document.addEventListener('mouseover', e => {
|
||||
const aide = aideDe(e.target);
|
||||
if (!aide) return;
|
||||
bulle.textContent = aide;
|
||||
bulle.classList.add('on');
|
||||
placer(e.target);
|
||||
});
|
||||
document.addEventListener('mouseout', e => {
|
||||
if (e.target && e.target.matches && e.target.matches('.champ > span')) bulle.classList.remove('on');
|
||||
});
|
||||
|
||||
// Affordance : souligné pointillé sur les descriptions qui ont une aide.
|
||||
function marquer(racine) {
|
||||
if (!racine.querySelectorAll) return;
|
||||
racine.querySelectorAll('.champ > span').forEach(s => {
|
||||
if (AIDES[libelleDe(s)]) s.classList.add('a-aide');
|
||||
});
|
||||
}
|
||||
new MutationObserver(muts => muts.forEach(m =>
|
||||
m.addedNodes.forEach(n => { if (n.nodeType === 1) marquer(n); })
|
||||
)).observe(document.body, { childList: true, subtree: true });
|
||||
marquer(document);
|
||||
})();
|
||||
|
||||
charger().catch(err => message(err.message, 'erreur'));
|
||||
</script>
|
||||
</body>
|
||||
|
|
@ -1916,9 +1584,6 @@ class Gestionnaire(BaseHTTPRequestHandler):
|
|||
valider_serveurs(donnees, charger_nomenclature(FICHIER_NOMENCLATURE))
|
||||
ecrire_serveurs(FICHIER_SERVEURS, donnees)
|
||||
self.repondre_json(200, inventaire_api(self.inventaire))
|
||||
elif chemin == "/api/intrants":
|
||||
ecrire_intrants(donnees)
|
||||
self.repondre_json(200, inventaire_api(self.inventaire))
|
||||
elif chemin == "/api/instancier":
|
||||
resultat = subprocess.run(
|
||||
["python3", str(RACINE / "scripts/instancier.py"), "appliquer"],
|
||||
|
|
|
|||
|
|
@ -157,8 +157,6 @@ CHAMPS_PROXMOX: tuple[tuple[str, str, bool], ...] = (
|
|||
("SETOPS_STOCKAGE", "proxmox_stockage", False),
|
||||
("SETOPS_DISQUE", "proxmox_disque_taille", False),
|
||||
("SETOPS_NOEUD", "proxmox_noeud", False),
|
||||
("SETOPS_COEURS", "proxmox_coeurs", False),
|
||||
("SETOPS_MEMOIRE", "proxmox_memoire", False),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -385,75 +385,6 @@ def reconcilier_serveur(nom: str, srv: dict, hote_inventaire: dict | None, nomen
|
|||
"absent_inventaire": hote_inventaire is None}
|
||||
|
||||
|
||||
# --- Dimensionnement derive des ressources VM -------------------------------
|
||||
# ressources(VM) = socle_SE + somme(empreinte des logiciels) -> marge -> arrondi.
|
||||
# Les empreintes vivent dans roles/<groupe>/meta/empreinte.yml (propriete du
|
||||
# logiciel). Les groupes sans role dedie ont un repli ci-dessous. Le plan peut
|
||||
# tout surcharger par hote (serveurs.yml: coeurs/memoire/disque).
|
||||
|
||||
SOCLE_SE = {"coeurs": 1, "memoire_mo": 512, "disque_go": 8}
|
||||
EMPREINTE_DEFAUT = {"coeurs": 1, "memoire_mo": 512, "disque_go": 5}
|
||||
EMPREINTES_SANS_ROLE = {
|
||||
"serveur_web_frontal": {"coeurs": 1, "memoire_mo": 512, "disque_go": 5},
|
||||
"serveur_web_dorsal": {"coeurs": 1, "memoire_mo": 1024, "disque_go": 10},
|
||||
}
|
||||
MARGE_MEMOIRE = 1.20
|
||||
MARGE_DISQUE = 1.25
|
||||
PALIER_MEMOIRE_MO = 512
|
||||
MIN_MEMOIRE_MO = 1024
|
||||
PALIER_DISQUE_GO = 5
|
||||
MIN_DISQUE_GO = 12
|
||||
MIN_COEURS = 1
|
||||
MAX_COEURS = 8
|
||||
CHAMPS_EMPREINTE = ("coeurs", "memoire_mo", "disque_go")
|
||||
|
||||
|
||||
def charger_empreinte_role(racine_roles: Path, groupe: str) -> dict | None:
|
||||
"""Lit roles/<groupe>/meta/empreinte.yml -> empreinte normalisee, ou None."""
|
||||
fichier = racine_roles / groupe / "meta" / "empreinte.yml"
|
||||
if not fichier.exists():
|
||||
return None
|
||||
with fichier.open("r", encoding="utf-8") as fh:
|
||||
data = yaml.safe_load(fh) or {}
|
||||
emp = (data or {}).get("setops_empreinte") or {}
|
||||
if not isinstance(emp, dict):
|
||||
raise ValueError(f"{fichier}: 'setops_empreinte' doit etre une table.")
|
||||
return {cle: int(emp.get(cle, 0)) for cle in CHAMPS_EMPREINTE}
|
||||
|
||||
|
||||
def empreinte_groupe(racine_roles: Path, groupe: str) -> dict:
|
||||
"""Empreinte d'un groupe de service : role > repli connu > defaut."""
|
||||
return (charger_empreinte_role(racine_roles, groupe)
|
||||
or EMPREINTES_SANS_ROLE.get(groupe)
|
||||
or EMPREINTE_DEFAUT)
|
||||
|
||||
|
||||
def _arrondi_superieur(valeur: float, palier: int) -> int:
|
||||
import math
|
||||
return int(math.ceil(valeur / palier) * palier)
|
||||
|
||||
|
||||
def deriver_ressources(groupes_service, racine_roles: Path, socle: dict | None = None) -> dict:
|
||||
"""Estime coeurs/memoire(Mo)/disque(Go) d'une VM depuis ses logiciels + le SE.
|
||||
|
||||
`groupes_service` : groupes applicatifs portes par l'hote (hors socle/etat).
|
||||
RAM et disque additifs (contraintes dures) ; coeurs en max (burstable).
|
||||
"""
|
||||
socle = socle or SOCLE_SE
|
||||
coeurs_somme = 0
|
||||
memoire = socle["memoire_mo"]
|
||||
disque = socle["disque_go"]
|
||||
for groupe in groupes_service:
|
||||
emp = empreinte_groupe(racine_roles, groupe)
|
||||
coeurs_somme += emp["coeurs"]
|
||||
memoire += emp["memoire_mo"]
|
||||
disque += emp["disque_go"]
|
||||
coeurs = max(MIN_COEURS, min(MAX_COEURS, max(socle["coeurs"], coeurs_somme)))
|
||||
memoire = max(MIN_MEMOIRE_MO, _arrondi_superieur(memoire * MARGE_MEMOIRE, PALIER_MEMOIRE_MO))
|
||||
disque = max(MIN_DISQUE_GO, _arrondi_superieur(disque * MARGE_DISQUE, PALIER_DISQUE_GO))
|
||||
return {"coeurs": coeurs, "memoire": memoire, "disque": f"{disque}G"}
|
||||
|
||||
|
||||
def charger_dependances(path: Path | None) -> dict:
|
||||
if not path:
|
||||
return {}
|
||||
|
|
|
|||
|
|
@ -69,8 +69,6 @@ def test_hote_complet_avec_noeud() -> None:
|
|||
"SETOPS_STOCKAGE='stockage-a'",
|
||||
"SETOPS_DISQUE='160G'",
|
||||
"SETOPS_NOEUD='noeud-a'",
|
||||
"SETOPS_COEURS='4'",
|
||||
"SETOPS_MEMOIRE='2048'",
|
||||
}
|
||||
assert set(lignes) == attendu, lignes
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue