Dimensionner les ressources VM depuis les logiciels hébergés

Les cœurs/RAM/disque d'une VM sont estimés depuis l'empreinte des rôles
hébergés (roles/<rôle>/meta/empreinte.yml) sommée au socle SE, au lieu
d'hériter des specs du golden template. Le générateur écrit
proxmox_coeurs/memoire/disque_taille ; le clonage les passe à Proxmox
(omit si absent → aucune régression). Override par hôte dans le plan.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-06-30 10:07:04 -04:00
parent c77ec20e4a
commit 5d30a3a608
20 changed files with 293 additions and 0 deletions

View file

@ -325,6 +325,8 @@ 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)" ); \
@ -363,6 +365,8 @@ 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)" \

View file

@ -0,0 +1,102 @@
# 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é.

View file

@ -121,6 +121,8 @@
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 }}"

View file

@ -0,0 +1,8 @@
---
# 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

View file

@ -0,0 +1,8 @@
---
# 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

View file

@ -0,0 +1,8 @@
---
# 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

View file

@ -0,0 +1,8 @@
---
# 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

View file

@ -0,0 +1,8 @@
---
# 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

View file

@ -0,0 +1,8 @@
---
# 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

View file

@ -0,0 +1,8 @@
---
# 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

View file

@ -0,0 +1,8 @@
---
# 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

View file

@ -0,0 +1,8 @@
---
# 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

View file

@ -0,0 +1,8 @@
---
# 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

View file

@ -0,0 +1,8 @@
---
# 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

View file

@ -0,0 +1,8 @@
---
# 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

View file

@ -0,0 +1,8 @@
---
# 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

View file

@ -30,10 +30,12 @@ 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"
@ -75,6 +77,12 @@ 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 [])

View file

@ -157,6 +157,8 @@ 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),
)

View file

@ -385,6 +385,75 @@ 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 {}

View file

@ -69,6 +69,8 @@ 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