repertoires partages : un seul mode par repertoire ; le depot de binaires du site sert de nouveau
/var/lib/setops etait tenu par quatre roles (0750 contre 0755) : apt-cacher-ng ne le traversait plus, le depot rendait 403 et chaque runner reconstruit allait chercher 500 Mo sur Internet. /etc/setops (0700 contre 0755) et /srv/restic au site portaient le meme desaccord. test_repertoires_partages.py le refuse. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
c1408be9bb
commit
7a2f179f39
9 changed files with 173 additions and 5 deletions
34
CHANGELOG.md
34
CHANGELOG.md
|
|
@ -1,5 +1,39 @@
|
|||
# CHANGELOG — Set-OPS
|
||||
|
||||
## 2026-10-01 (58) — Le dépôt de binaires du site rendait 403 : quatre rôles se disputaient un répertoire
|
||||
|
||||
**Le symptôme** (reconstruction de Technolibre lancée par l'exploitant, 63 min au lieu de
|
||||
~43) : `deployer-tout` restait 20 min sur « Télécharger le tarball Nextcloud dans le cache du
|
||||
contrôleur » — 281 Mo depuis download.nextcloud.com à 130–230 Ko/s. Le runner, né à vide,
|
||||
devait les prendre au dépôt de binaires du SITE ; l'étape qui l'y cherche (`failed_when:
|
||||
false`, « le dépôt est une commodité ») avait rendu « ok » sur un **403**.
|
||||
|
||||
**La cause** : le dépôt existe et est complet depuis le 2026-09-12 (Forgejo, Keycloak,
|
||||
Nextcloud 34.0.2, oauth2-proxy), `LocalDirs` est en place — mais apt-cacher-ng, qui ne tourne
|
||||
pas en root, ne traversait plus `/var/lib/setops` (0750). QUATRE rôles tiennent ce
|
||||
répertoire : `common_packages`, `client_journal`, `serveur_ops_site` en 0750, et
|
||||
`serveur_artefacts` en 0755. Le dernier déployé gagnait ; mes déploiements au site du matin
|
||||
l'avaient refermé (06:16). Le piège était documenté dans `serveur_artefacts` — pas chez les
|
||||
trois autres.
|
||||
|
||||
**Correction** : 0755 partout (le répertoire ne tient que des états de sondes, déjà lisibles) ;
|
||||
appliqué à `site-cache-01` : les quatre archives répondent 200, à la taille exacte, depuis une
|
||||
VM de locataire.
|
||||
|
||||
**La garde, `test_repertoires_partages.py`** : un répertoire dont plusieurs rôles imposent le
|
||||
mode n'en a qu'un — chemins Jinja RÉSOLUS depuis les défauts (celui de `serveur_artefacts`
|
||||
s'écrit `{{ … | dirname }}`, une lecture littérale ne l'aurait pas vu). À sa première
|
||||
exécution, elle a trouvé deux autres désaccords :
|
||||
- `/etc/setops` : 0700 (`client_backup`, `serveur_backup`) contre 0755 (`client_sante`,
|
||||
`serveur_icinga`). Il tient des secrets, tous ses lecteurs tournent en root : **0700**.
|
||||
- `/srv/restic` sur `site-backup-01` : `serveur_backup` y est appliqué deux fois — en
|
||||
dépendance de `serveur_backup_site` (racine `/srv/restic/site`) et par son propre groupe
|
||||
(défaut `/srv/restic`, 0700 au compte `restic`). Joué seul, ce second passage refermait le
|
||||
parent des dépôts de tous les locataires (sshd `StrictModes`, mesuré le 2026-09-01). Dormant
|
||||
— un déploiement complet du site rejoue `serveur_backup_site` après. L'inventaire du site
|
||||
donne désormais au groupe la racine de la dépendance, LUE dans son `meta/main.yml`. Le test
|
||||
admet ce cas : une dépendance qui surcharge le chemin n'est pas un désaccord.
|
||||
|
||||
## 2026-10-01 (57) — Aucune exception : la soumission n'est ouverte que là où elle existe
|
||||
|
||||
J'avais présenté `site-mon-01:465/587` (« sans service ») comme une exception tolérable.
|
||||
|
|
|
|||
1
Makefile
1
Makefile
|
|
@ -322,6 +322,7 @@ test: ## Lance les tests unitaires (derivation de nomenclature et d'inventaire)
|
|||
python3 scripts/tests/test_proxmox_fw_externe.py
|
||||
python3 scripts/tests/test_sonde_tcp.py
|
||||
python3 scripts/tests/test_restauration.py
|
||||
python3 scripts/tests/test_repertoires_partages.py
|
||||
|
||||
# LA SONDE QUI RAPPORTE CE QUI DISTINGUE (2026-08-28). Trois faux diagnostics en une
|
||||
# journee, tous dus a l'instrument : `curl` et `bash /dev/tcp` ecrasent « la machine
|
||||
|
|
|
|||
|
|
@ -225,11 +225,16 @@
|
|||
# perdre ferait passer une cicatrice ancienne pour une perte en cours.
|
||||
- name: Assurer le repertoire d etat des sondes
|
||||
ansible.builtin.file:
|
||||
# 0755, COMME PARTOUT AILLEURS (2026-10-01) : quatre roles tenaient ce repertoire, trois
|
||||
# en 0750 et `serveur_artefacts` en 0755 — apt-cacher-ng doit le traverser pour servir
|
||||
# le depot de binaires du site. Le dernier deploye gagnait : au site, le 2026-10-01, il
|
||||
# rendait 403, et chaque runner allait chercher 500 Mo sur Internet. Le repertoire ne
|
||||
# tient que des etats de sondes, deja lisibles. Garde : test_repertoires_partages.py.
|
||||
path: /var/lib/setops
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0750"
|
||||
mode: "0755"
|
||||
|
||||
- name: Deposer la sonde d expedition des journaux
|
||||
ansible.builtin.template:
|
||||
|
|
|
|||
|
|
@ -27,11 +27,14 @@
|
|||
|
||||
- name: Assurer le répertoire des secrets Set-OPS
|
||||
ansible.builtin.file:
|
||||
# 0700, COMME `client_backup` ET `serveur_backup` (2026-10-01) : ce repertoire tient des
|
||||
# secrets, et tous ses lecteurs tournent en root. Ce role etait seul a l'ouvrir en 0755,
|
||||
# et le dernier deploye gagnait. Garde : test_repertoires_partages.py.
|
||||
path: /etc/setops
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
mode: "0700"
|
||||
|
||||
- name: Déposer le mot de passe d'API Icinga
|
||||
ansible.builtin.copy:
|
||||
|
|
|
|||
|
|
@ -147,11 +147,16 @@
|
|||
|
||||
- name: Assurer le repertoire d etat des sondes
|
||||
ansible.builtin.file:
|
||||
# 0755, COMME PARTOUT AILLEURS (2026-10-01) : quatre roles tenaient ce repertoire, trois
|
||||
# en 0750 et `serveur_artefacts` en 0755 — apt-cacher-ng doit le traverser pour servir
|
||||
# le depot de binaires du site. Le dernier deploye gagnait : au site, le 2026-10-01, il
|
||||
# rendait 403, et chaque runner allait chercher 500 Mo sur Internet. Le repertoire ne
|
||||
# tient que des etats de sondes, deja lisibles. Garde : test_repertoires_partages.py.
|
||||
path: /var/lib/setops
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0750"
|
||||
mode: "0755"
|
||||
|
||||
- name: Deposer la sonde des correctifs de securite
|
||||
ansible.builtin.template:
|
||||
|
|
|
|||
|
|
@ -451,7 +451,7 @@
|
|||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
mode: "0700" # comme client_sante et client_backup (test_repertoires_partages.py)
|
||||
delegate_to: "{{ groups['serveur_backup'] | first }}"
|
||||
|
||||
- name: Deposer l'AC sur le depot de sauvegarde
|
||||
|
|
|
|||
|
|
@ -156,11 +156,16 @@
|
|||
# consigne l'ecart ; la sonde « fabric » le lit. Voir `templates/conformite-fabric.sh.j2`.
|
||||
- name: Assurer le repertoire d'etat
|
||||
ansible.builtin.file:
|
||||
# 0755, COMME PARTOUT AILLEURS (2026-10-01) : quatre roles tenaient ce repertoire, trois
|
||||
# en 0750 et `serveur_artefacts` en 0755 — apt-cacher-ng doit le traverser pour servir
|
||||
# le depot de binaires du site. Le dernier deploye gagnait : au site, le 2026-10-01, il
|
||||
# rendait 403, et chaque runner allait chercher 500 Mo sur Internet. Le repertoire ne
|
||||
# tient que des etats de sondes, deja lisibles. Garde : test_repertoires_partages.py.
|
||||
path: /var/lib/setops
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0750"
|
||||
mode: "0755"
|
||||
|
||||
- name: Deposer le constat de conformite de la fabric
|
||||
ansible.builtin.template:
|
||||
|
|
|
|||
|
|
@ -687,6 +687,15 @@ def inventaire() -> dict:
|
|||
"nftables_baseline_ruleset_genere": str(
|
||||
(U.chemin().resolve().parent / "flux-genere" / f"{nom}.nft")
|
||||
if U.chemin() else ""),
|
||||
# LA RACINE DE `serveur_backup` SUR LA MACHINE DU DEPOT DU SITE (2026-10-01). Elle y
|
||||
# est appliquee DEUX fois : en dependance de `serveur_backup_site`, qui lui passe
|
||||
# `/srv/restic/site`, et par son propre groupe (le temoin des depots dans Icinga),
|
||||
# qui reprenait le defaut `/srv/restic` — en 0700 au compte `restic`. Joue seul, ce
|
||||
# second passage refermait le parent des depots de tous les locataires : sshd
|
||||
# refuse alors leurs comptes (`StrictModes`, mesure le 2026-09-01). La valeur est
|
||||
# LUE dans la dependance, pas recopiee : `test_repertoires_partages.py`.
|
||||
**({"serveur_backup_racine": _racine_backup_du_site()}
|
||||
if "serveur_backup_site" in _groupes_de(nom) else {}),
|
||||
# La liste que la sonde `connectivite` teste, ecrite a cote du ruleset et pour
|
||||
# la meme raison (2026-09-30) : au site, le defaut pointait hors du depot, et la
|
||||
# sonde aurait tourne sans liste.
|
||||
|
|
@ -997,6 +1006,15 @@ def inventaire() -> dict:
|
|||
return out
|
||||
|
||||
|
||||
def _racine_backup_du_site() -> str:
|
||||
"""La racine que `serveur_backup_site` passe a sa dependance `serveur_backup`."""
|
||||
meta = Path(__file__).resolve().parent.parent / "roles" / "serveur_backup_site" / "meta" / "main.yml"
|
||||
for dep in (yaml.safe_load(meta.read_text(encoding="utf-8")) or {}).get("dependencies") or []:
|
||||
if isinstance(dep, dict) and dep.get("role") == "serveur_backup" and dep.get("serveur_backup_racine"):
|
||||
return str(dep["serveur_backup_racine"])
|
||||
raise SystemExit("serveur_backup_site/meta/main.yml ne passe plus `serveur_backup_racine` a sa dependance.")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument("--list", action="store_true")
|
||||
|
|
|
|||
97
scripts/tests/test_repertoires_partages.py
Normal file
97
scripts/tests/test_repertoires_partages.py
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Un repertoire tenu par plusieurs roles a UN seul mode — sinon le dernier deploye gagne.
|
||||
|
||||
POURQUOI (2026-10-01). Quatre roles tenaient `/var/lib/setops` : trois en 0750, un en 0755
|
||||
(`serveur_artefacts`, dont apt-cacher-ng doit traverser le chemin pour servir le depot de
|
||||
binaires). Chaque deploiement basculait le mode selon l'ordre des roles. Au site, le
|
||||
2026-10-01, le repertoire etait en 0750 : le depot rendait 403, et chaque runner reconstruit
|
||||
allait chercher 500 Mo sur Internet — une reconstruction prenait 70 min au lieu de 43.
|
||||
`/etc/setops` portait le meme desaccord (0700 contre 0755).
|
||||
|
||||
On releve chaque tache `file: state=directory` qui impose un `mode`, chemins Jinja RESOLUS
|
||||
depuis les defauts du role (le chemin de `serveur_artefacts` est ecrit `{{ ... | dirname }}`
|
||||
— une lecture litterale ne l'aurait pas vu), et on exige un seul mode par chemin.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import collections
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import jinja2
|
||||
import yaml
|
||||
|
||||
RACINE = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def defauts(role: str) -> dict:
|
||||
f = RACINE / "roles" / role / "defaults" / "main.yml"
|
||||
try:
|
||||
return yaml.safe_load(f.read_text(encoding="utf-8")) or {} if f.is_file() else {}
|
||||
except yaml.YAMLError:
|
||||
return {}
|
||||
|
||||
|
||||
def resoudre(chemin: str, role: str) -> str | None:
|
||||
if "{{" not in chemin:
|
||||
return chemin
|
||||
env = jinja2.Environment(undefined=jinja2.StrictUndefined)
|
||||
env.filters["dirname"] = os.path.dirname
|
||||
env.filters["basename"] = os.path.basename
|
||||
try:
|
||||
r = env.from_string(chemin).render(**defauts(role))
|
||||
return r if r.startswith("/") and "{{" not in r else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def main() -> int:
|
||||
vus: dict[str, set] = collections.defaultdict(set)
|
||||
|
||||
def parcourir(taches, role):
|
||||
for t in taches or []:
|
||||
if not isinstance(t, dict):
|
||||
continue
|
||||
for k in ("block", "rescue", "always"):
|
||||
if k in t:
|
||||
parcourir(t[k], role)
|
||||
m = t.get("ansible.builtin.file") or t.get("file")
|
||||
if isinstance(m, dict) and m.get("state") == "directory" and "mode" in m:
|
||||
p = resoudre(str(m.get("path", "")), role)
|
||||
if p:
|
||||
vus[p.rstrip("/")].add((str(m["mode"]), role))
|
||||
|
||||
for f in sorted((RACINE / "roles").glob("*/tasks/*.yml")):
|
||||
try:
|
||||
parcourir(yaml.safe_load(f.read_text(encoding="utf-8")), f.parts[-3])
|
||||
except yaml.YAMLError:
|
||||
continue
|
||||
# UNE DEPENDANCE QUI SURCHARGE LE CHEMIN N'EST PAS UN DESACCORD. `serveur_backup_site`
|
||||
# passe `serveur_backup_racine: /srv/restic/site` a sa dependance `serveur_backup` (et
|
||||
# l'inventaire du site l'applique aussi au groupe) : la ou les deux roles cohabitent, ils
|
||||
# ne tiennent pas le meme repertoire. Le defaut de `serveur_backup` ne vaut que seul.
|
||||
surcharges = set()
|
||||
for meta in sorted((RACINE / "roles").glob("*/meta/main.yml")):
|
||||
try:
|
||||
deps = (yaml.safe_load(meta.read_text(encoding="utf-8")) or {}).get("dependencies") or []
|
||||
except yaml.YAMLError:
|
||||
continue
|
||||
for d in deps:
|
||||
if isinstance(d, dict) and d.get("role") and len(d) > 1:
|
||||
surcharges.add(frozenset({meta.parts[-3], d["role"]}))
|
||||
conflits = {p: s for p, s in vus.items()
|
||||
if len({m for m, _ in s}) > 1 and frozenset(r for _, r in s) not in surcharges}
|
||||
for p, s in sorted(conflits.items()):
|
||||
print(f"ECHEC {p} : " + ", ".join(f"{r}={m}" for m, r in sorted(s)))
|
||||
temoin = vus.get("/var/lib/setops", set())
|
||||
print(("OK " if {r for _, r in temoin} >= {"serveur_artefacts", "common_packages"} else "ECHEC ")
|
||||
+ f"/var/lib/setops vu chez {len(temoin)} role(s), chemin Jinja de serveur_artefacts compris")
|
||||
if conflits or not {r for _, r in temoin} >= {"serveur_artefacts", "common_packages"}:
|
||||
return 1
|
||||
print(f"OK {len(vus)} repertoire(s) a mode impose, aucun desaccord entre roles.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Loading…
Reference in a new issue