serveur_ops : les depots prives se lisent en SSH, ou arrivent en colis
Mesure sur le terrain : chaque runner lit son depot en SSH avec sa cle de deploiement, et la forge lui refuse les autres, alors meme que les depots sont publics. Le role ne clone plus en HTTPS que les depots publics ; les depots instance, tenant et hebergeur passent en SSH, cle d'hote epinglee, commande SSH posee dans leur config git. Avant l'armement, le controleur les apporte en git bundle. La cle d'hote de la forge est declaree au plan du site et offerte par la fiche. genome_etat et genome_pousser lisent l'API authentifies ; la sonde genome-servi compte les publics ; forge_amorcer cree prives les depots des ecosystemes et du site. make forge-etancheite mesure le tout. Les depots restent publics. 95 preuves. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
9af39b1b60
commit
d7717a4f98
18 changed files with 485 additions and 17 deletions
46
CHANGELOG.md
46
CHANGELOG.md
|
|
@ -1,5 +1,51 @@
|
|||
# CHANGELOG — Set-OPS
|
||||
|
||||
## 2026-10-11 (133) — Chaque runner lit son seul dépôt : mesuré, puis écrit dans le rôle
|
||||
|
||||
**95 preuves.** `make verifier` passe. `test_forge_cles.py` couvre aussi le classement des
|
||||
mesures : un refus réseau n'est jamais pris pour une protection. Les dépôts restent publics.
|
||||
|
||||
### Mesuré sur le terrain (étape 1 du chantier)
|
||||
|
||||
`make forge-cles CONFIRMER=true` a enregistré les deux clés de lecture (HTTP 201), et un
|
||||
second passage ne trouve rien à faire. Le pare-feu de `site-forge-01` a été simulé puis
|
||||
appliqué. Daniel a appliqué la frontière : une règle créée, rien de retiré. Depuis chaque
|
||||
runner, en SSH, avec la clé d'hôte de la forge épinglée (lue sur sa machine par l'accès
|
||||
d'administration), le runner lit son propre dépôt, et la forge refuse les trois autres :
|
||||
celui de l'autre locataire, celui du site, et le moteur. Ce refus vaut **alors que les dépôts
|
||||
sont encore publics** : une clé de déploiement ne vaut que pour le dépôt qui l'a enregistrée.
|
||||
Le moteur reste donc cloné en HTTPS sans identifiant.
|
||||
|
||||
Une première mesure accusait la forge à tort (« No ED25519 host key is known ») : le fichier
|
||||
`known_hosts` temporaire, créé par root en mode 600, était illisible pour `setops`. C'était
|
||||
l'instrument, pas la forge.
|
||||
|
||||
### Ce que le rôle sait faire maintenant
|
||||
|
||||
- **Dépôts privés en SSH.** `serveur_ops` ne clone plus en HTTPS que les dépôts publics. Les
|
||||
dépôts `instance`, `tenant` et `hebergeur` passent en SSH, avec la clé du runner et la clé
|
||||
d'hôte épinglée. La commande SSH est posée dans leur configuration git : la console, la
|
||||
garde du génome et `make monter` la reprennent sans rien savoir de tout ceci.
|
||||
- **Le colis d'insémination.** Quand le runner ne lit pas encore un dépôt privé (à
|
||||
l'insémination, avant que l'armement lui remette sa clé), le contrôleur, qui porte ce dépôt,
|
||||
le lui apporte en `git bundle`. Le runner le clone ou avance en avance rapide, puis vise la
|
||||
forge en SSH. Le rôle dit quels dépôts sont venus par colis.
|
||||
- **La clé d'hôte de la forge** est déclarée dans le plan du site (`forge_ssh_cle_hote`),
|
||||
exposée au runner du site, offerte aux locataires par leur fiche, et copiée dans leurs
|
||||
variables, comparée à l'original par `make site-intrants`. Les deux fiches ont été
|
||||
redéposées.
|
||||
- **Les lecteurs anonymes.** `genome_etat.yml` et `genome_pousser.yml` lisent l'API de la forge
|
||||
avec le compte d'administration de la voûte du site : en anonyme, un dépôt privé
|
||||
répondrait 404. La sonde `genome-servi` ne compte plus que les dépôts publics (au moins le
|
||||
moteur). `forge_amorcer.py` crée privés les dépôts des écosystèmes et du site.
|
||||
- **`make forge-etancheite`**, lancé du poste : sans identifiant, chaque dépôt privé doit
|
||||
exiger une authentification et chaque dépôt public répondre ; depuis chaque runner, il lit
|
||||
son dépôt et pas les autres. Lancée avant la bascule, elle compte 7 écarts, comme attendu.
|
||||
|
||||
**Limite.** Le colis n'a pas encore servi sur le terrain ; la bascule n'est pas faite. Les
|
||||
étapes suivantes : déployer le rôle sur les trois runners (dépôts encore publics), éprouver le
|
||||
colis sur un runner, basculer, mesurer, puis reconstruire Technolibre.
|
||||
|
||||
## 2026-10-11 (132) — Chaque locataire publie la clé qui lira son dépôt ; la forge du site peut l'entendre
|
||||
|
||||
**95 preuves.** `make verifier` passe. P95 est nouvelle, et `test_forge_cles.py` épingle ce
|
||||
|
|
|
|||
4
Makefile
4
Makefile
|
|
@ -1388,6 +1388,10 @@ forge-cles: ## Le site enregistre sur le depot de chaque locataire la cle de lec
|
|||
CONFIRMER="$(CONFIRMER)" ansible-playbook -i scripts/site_inventaire.py \
|
||||
playbooks/maintenance/forge_cles.yml "$${supp[@]}" $(ARGS)
|
||||
|
||||
.PHONY: forge-etancheite
|
||||
forge-etancheite: ansible-runtime ## La forge du site laisse-t-elle chaque runner lire ses depots, et personne d'autre ? (mesure, aucune ecriture)
|
||||
python3 scripts/forge_etancheite.py
|
||||
|
||||
.PHONY: depots-perimes
|
||||
depots-perimes: ansible-runtime _instance-requise ## Depots qu'un runner ne declare plus : regarde, et retire avec CONFIRMER=true
|
||||
@# DESTRUCTIF SUR CONFIRMATION SEULEMENT. Sans `CONFIRMER=true`, ce geste REGARDE :
|
||||
|
|
|
|||
|
|
@ -43,8 +43,8 @@
|
|||
| P28 | Pools Proxmox : un par tenant, sans collision | AFF-110 | ✅ OK | CONFORME : 3 pool(s) Proxmox, 35 VM placee(s), aucun nom ni VMID en collision. |
|
||||
| P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 34 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 23, socle-identite 2, web-sso 5) ; 2 lacune(s) nommee(s) : serveur_loki, serv |
|
||||
| P30 | SDN EVPN : zones, VNets et sous-reseaux derives | AFF-112 | ✅ OK | CONFORME : SDN EVPN, 2 zone(s), 12 VNet(s), 12 sous-reseau(x), aucune collision. |
|
||||
| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 79 scripts expliques et atteignables, 152 cibles make documentees, 69 roles avec README. |
|
||||
| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 37 exigence(s) de role, toutes satisfaites (149 cle(s) declaree(s) par l'instance). |
|
||||
| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 80 scripts expliques et atteignables, 153 cibles make documentees, 69 roles avec README. |
|
||||
| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 38 exigence(s) de role, toutes satisfaites (150 cle(s) declaree(s) par l'instance). |
|
||||
| P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 44 revendication(s) de port, aucune collision entre roles co-localises (35 groupes). |
|
||||
| P34 | Chaque document declare son lecteur | — | ✅ OK | 51 document(s) declarent leur lecteur (48 genere(s) exempte(s)). |
|
||||
| P35 | Toute application exigeant une base en a une au plan | — | ✅ OK | 4 application(s) exigeant une base l'ont toutes (4 entree(s) au registre). |
|
||||
|
|
@ -53,7 +53,7 @@
|
|||
| P38 | Catalogue des services : la carte dit ce que le moteur fait | — | ✅ OK | Catalogue a jour : 42 role(s) serveur/client tous nommes, 42 groupe(s) cite(s) en table existent tous. |
|
||||
| P39 | Glossaire : tout mot employe est enseigne | — | ✅ OK | Glossaire complet : 81 terme(s) du jargon expliques, 15 lien(s) valides, 97 page(s) de wiki toutes atteignables. |
|
||||
| P40 | Parente : l'ecosysteme sait de quoi il descend | — | ✅ OK | Parente coherente : 4 depot(s), tous retrouves, tous porteurs d'un remote. |
|
||||
| P41 | Resolution d'instance : une seule, partagee | — | ✅ OK | Resolution unique : 75 script(s) passent par `inventory_rules`, 3 exemption(s) nommee(s). |
|
||||
| P41 | Resolution d'instance : une seule, partagee | — | ✅ OK | Resolution unique : 76 script(s) passent par `inventory_rules`, 3 exemption(s) nommee(s). |
|
||||
| P42 | L'edge porte les noms qu'il publie | — | ✅ OK | 3 edge(s) emettent un certificat portant les noms publies (OPS-Chezlepro-lab/principal, OPS-Chezlepro/principal, OPS-Technolibre/principal). |
|
||||
| P43 | Frontiere : le devis voit les machines du site | — | ✅ OK | Devis de la frontiere : 9 machine(s) du plan retrouvees, 175 regle(s) du site. |
|
||||
| P44 | Integrations : le serveur avant ses clients | — | ✅ OK | 5 integration(s) appliquent leur serveur avant leurs clients. |
|
||||
|
|
@ -85,7 +85,7 @@
|
|||
| P70 | Depot de binaires : il tient tout ce que les roles vont chercher | — | ✅ OK | 27 artefact(s) direct(s) tenus par le depot du site. |
|
||||
| P71 | Pool du site : le genome ne nait pas chez un tenant | — | ✅ OK | `site-creer` nomme `--pool-site` ; le pool du genome ne peut plus etre celui d'un tenant. |
|
||||
| P72 | Annuaire : aucun service ne se lie avec le compte du maitre | — | ✅ OK | 5 role(s) consultent l'annuaire, chacun avec SON compte de service ; seul `amorcage_acces` garde celui d'administration, et il provisionne au lieu de consommer. |
|
||||
| P73 | Le locataire designe les services de son site REEL | — | ✅ OK | CONFORME : 10 intrant(s) du locataire concordent avec ce que le site expose (1 non declare(s), donc derive(s) ou non utilise(s)). |
|
||||
| P73 | Le locataire designe les services de son site REEL | — | ✅ OK | CONFORME : 11 intrant(s) du locataire concordent avec ce que le site expose (1 non declare(s), donc derive(s) ou non utilise(s)). |
|
||||
| P74 | Gabarit dore : une seule declaration, au plan du site | — | ✅ OK | Gabarit declare une seule fois : VMID 9006 « modeleSetOPS-minimal ». |
|
||||
| P75 | Les parametres de clonage traversent les trois maillons | — | ✅ OK | 14 parametre(s) de clonage, tous emis par l'inventaire. |
|
||||
| P76 | Tout gabarit de role se rend vraiment | — | ✅ OK | 189 gabarits de role : tous se rendent. |
|
||||
|
|
@ -95,7 +95,7 @@
|
|||
| P80 | Remise au client : inscrite, nommee, et son second temps a l'heure | — | ✅ OK | Aucun ecosysteme remis a un client : rien a tenir. |
|
||||
| P81 | La console dit sa portee, et ne sert pas un inventaire vide en silence | — | ✅ OK | Portee `poste` derivee des voutes portees, source d'inventaire `instance/inventories/principal/hosts.yml`, et 15 route(s) POST exigent toutes un pouvoir. |
|
||||
| P82 | DNS public : les zones publiees sont servies, signees avant d'etre exposees | — | ✅ OK | 2 zone(s) publique(s) declaree(s), toutes servies par le site ; 2 replication(s) declaree(s) des deux cotes ; transfert ouvert a la cle seule ; aucune expositio |
|
||||
| P83 | Assistants : le registre des runbooks ne prend pas de retard sur le Makefile | — | ✅ OK | 17 runbooks, 146 etapes, 151 cibles documentees : chacune portee par un assistant ou exemptee avec son motif. |
|
||||
| P83 | Assistants : le registre des runbooks ne prend pas de retard sur le Makefile | — | ✅ OK | 17 runbooks, 147 etapes, 152 cibles documentees : chacune portee par un assistant ou exemptee avec son motif. |
|
||||
| P84 | La fiche du site dit exactement ce que chaque locataire porte | — | ✅ OK | 2 fiche(s) de site : chacune dit exactement ce que son locataire porte (copies, racine, inventaire genere). |
|
||||
| P85 | La face reseau du locataire dit exactement ce que le site en tire | — | ✅ OK | 2 face(s) reseau : chacune dit exactement ce que le site en tire (decouverte, devis Proxmox, frontiere, DNS public, sauvegarde). |
|
||||
| P86 | Machine par machine, le locataire et Proxmox admettent les memes entrees | — | ✅ OK | 2 locataire(s) : machine par machine et port par port, nftables et Proxmox admettent les memes sources. |
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
> **Pour qui :** le mainteneur qui décide de ce chantier puis le conduit, et quiconque se demande
|
||||
> pourquoi un runner ne lit que ses propres dépôts.
|
||||
|
||||
Préparé le 2026-10-11. **Rien n'est changé tant que les décisions D1 à D3 ne sont pas prises.**
|
||||
Préparé le 2026-10-11. Décisions D1 à D3 prises le même jour par Daniel : les trois recommandations.
|
||||
|
||||
## 1. Le constat
|
||||
|
||||
|
|
@ -42,6 +42,22 @@ en font partie.
|
|||
face réseau ; le site l'enregistre sur le dépôt de ce locataire, avec ses propres
|
||||
identifiants d'administration de la forge (`vault_forgejo_admin`, déjà dans sa voûte).
|
||||
|
||||
### Éprouvé le 2026-10-11 (étape 1)
|
||||
|
||||
Clés de lecture enregistrées par `make forge-cles`, flux posé sur `site-forge-01` et à la
|
||||
frontière. Depuis chaque runner, `git ls-remote` en SSH, clé d'hôte de la forge épinglée (lue
|
||||
sur sa machine par l'accès d'administration) :
|
||||
|
||||
| Depuis | son dépôt | l'autre locataire | `site-chezlepro` | `set-ops-public` |
|
||||
|---|---|---|---|---|
|
||||
| runner de Technolibre | lu (`0172dd8`) | refusé | refusé | refusé |
|
||||
| runner de Chezlepro | lu (`74d5567`) | refusé | refusé | refusé |
|
||||
|
||||
Le refus vient de la forge (« Deploy Key … is not authorized to read »), **alors que les dépôts
|
||||
sont encore publics** : une clé de déploiement ne vaut que pour le dépôt qui l'a enregistrée.
|
||||
Conséquence : le moteur, public, continue de se cloner en HTTPS sans identifiant ; seuls les
|
||||
dépôts privés passent par SSH.
|
||||
|
||||
## 3. Le nœud : l'insémination
|
||||
|
||||
Le site amorce le runner d'un locataire **avant** que celui-ci ait reçu sa clé stable (elle
|
||||
|
|
@ -78,6 +94,10 @@ privés, avant de reprendre le lot 3b.
|
|||
|
||||
## 5. Ce qui change dans le code
|
||||
|
||||
État au 2026-10-11 : tout ce tableau est écrit, sauf la bascule elle-même. La clé d'hôte de
|
||||
la forge est déclarée dans le plan du site (`forge_ssh_cle_hote`), offerte par la fiche
|
||||
(`serveur_ops_forge_cle_hote`) et copiée par chaque locataire. Le moteur reste cloné en HTTPS.
|
||||
|
||||
| Pièce | Changement |
|
||||
|---|---|
|
||||
| `serveur_ops_depots` | un drapeau `prive: true` sur les dépôts `instance`, `tenant` et `hebergeur` |
|
||||
|
|
@ -88,7 +108,7 @@ privés, avant de reprendre le lot 3b.
|
|||
| `forge_amorcer.py` | crée les dépôts privés comme privés |
|
||||
| flux | `serveur_forge_site` : entrée SSH depuis les runners des locataires hébergés (paire nouvelle, résolue depuis les faces) ; `serveur_ops` : sortie correspondante |
|
||||
| `genome_etat.yml` | lit la forge par SSH ou authentifié : l'API anonyme répondrait 404 pour un dépôt privé |
|
||||
| `sonde-genome-servi` | compte les dépôts de l'organisation authentifié, sinon elle ne verrait plus que les deux publics |
|
||||
| `sonde-genome-servi` | ne compte plus que les dépôts publics (au moins le moteur) : c'est ce qu'un écosystème neuf clone sans rien détenir ; les privés sont suivis par `genome-etat`, `forge-cles` et les runners |
|
||||
|
||||
Inchangés : `sonde-genome-a-jour` (son repère ne compare que `set-ops-public`), le poste (il
|
||||
pousse déjà en SSH), la console (`etat_forge` lit `origin`, qui deviendra l'URL SSH).
|
||||
|
|
|
|||
|
|
@ -225,6 +225,13 @@ runbooks:
|
|||
pourquoi: >-
|
||||
Enregistrer sur le depot de chaque locataire la cle de lecture que sa face publie, et
|
||||
retirer celle qui ne l'est plus. Sans elle, son runner ne lit plus son depot prive.
|
||||
- cible: forge-etancheite
|
||||
nature: mesure
|
||||
portee: poste
|
||||
pourquoi: >-
|
||||
Sans identifiant, chaque depot prive exige une authentification et le moteur
|
||||
repond ; chaque runner de locataire lit son depot et pas celui d'un autre. Du
|
||||
poste, qui voit le site et les runners.
|
||||
- cible: site-verifier
|
||||
nature: mesure
|
||||
pourquoi: "Le playbook du site correspond-il encore aux couches declarees ?"
|
||||
|
|
|
|||
|
|
@ -61,6 +61,11 @@
|
|||
url: >-
|
||||
https://forge.{{ domaine_interne }}/api/v1/repos/{{ serveur_ops_forge_organisation
|
||||
| default('genome') }}/{{ item.depot }}/branches/{{ item.branche | default('main') }}
|
||||
# AUTHENTIFIE (2026-10-11) : les depots des locataires et du site deviennent prives, et
|
||||
# l'API anonyme y repondrait 404 — un « absent » qui n'en serait pas un.
|
||||
url_username: forgejo-admin
|
||||
url_password: "{{ vault_forgejo_admin }}"
|
||||
force_basic_auth: true
|
||||
validate_certs: false
|
||||
status_code: [200, 404]
|
||||
loop: "{{ genome_compares }}"
|
||||
|
|
|
|||
|
|
@ -216,11 +216,16 @@
|
|||
|
||||
- name: Demander à la forge ce qu'elle porte maintenant
|
||||
# On interroge son API plutôt que `git ls-remote` : c'est la forge elle-même qui
|
||||
# répond, et la lecture est anonyme — aucun justificatif n'entre ici.
|
||||
# répond. Lecture authentifiée depuis le 2026-10-11 (dépôts privés).
|
||||
ansible.builtin.uri:
|
||||
url: >-
|
||||
https://forge.{{ domaine_interne }}/api/v1/repos/{{ serveur_ops_forge_organisation
|
||||
| default('genome') }}/{{ item.depot }}/branches/{{ item.branche | default('main') }}
|
||||
# AUTHENTIFIE (2026-10-11) : les depots des locataires et du site deviennent prives, et
|
||||
# l'API anonyme y repondrait 404 — un « absent » qui n'en serait pas un.
|
||||
url_username: forgejo-admin
|
||||
url_password: "{{ vault_forgejo_admin }}"
|
||||
force_basic_auth: true
|
||||
validate_certs: false
|
||||
status_code: [200]
|
||||
loop: "{{ genome_a_pousser }}"
|
||||
|
|
@ -265,6 +270,11 @@
|
|||
url: >-
|
||||
https://forge.{{ domaine_interne }}/api/v1/repos/{{ serveur_ops_forge_organisation
|
||||
| default('genome') }}/{{ item.depot }}/git/refs/tags
|
||||
# AUTHENTIFIE (2026-10-11) : les depots des locataires et du site deviennent prives, et
|
||||
# l'API anonyme y repondrait 404 — un « absent » qui n'en serait pas un.
|
||||
url_username: forgejo-admin
|
||||
url_password: "{{ vault_forgejo_admin }}"
|
||||
force_basic_auth: true
|
||||
validate_certs: false
|
||||
status_code: [200, 404]
|
||||
loop: "{{ genome_a_pousser }}"
|
||||
|
|
|
|||
|
|
@ -12,7 +12,10 @@ serveur_forge_site_port: "{{ serveur_forgejo_port | default(443) }}"
|
|||
# `serveur_ops_depots` nomme des depots sous ce proprietaire, pas ailleurs.
|
||||
serveur_forge_site_organisation: "genome"
|
||||
|
||||
# COMBIEN DE DEPOTS AU MINIMUM. Un site sert au moins le moteur et sa propre carte ; en
|
||||
# dessous de deux, la forge n'a pas de quoi faire naitre un ecosysteme. On ne fixe pas un
|
||||
# compte exact : le nombre de locataires change, la sonde ne doit pas changer avec.
|
||||
serveur_forge_site_sonde_depots_minimum: 2
|
||||
# COMBIEN DE DEPOTS PUBLICS AU MINIMUM. La sonde lit l'API SANS identifiant : elle voit ce
|
||||
# qu'un ecosysteme neuf peut cloner sans rien detenir. Depuis le 2026-10-11, la carte du
|
||||
# site et les plans des locataires sont PRIVES (`docs/depots-prives-conception.md`) : il
|
||||
# reste au moins le moteur. Les depots prives se surveillent ailleurs — `make genome-etat`,
|
||||
# `make forge-cles`, et chaque runner qui les tire. On ne fixe pas un compte exact : le
|
||||
# nombre de depots change, la sonde ne doit pas changer avec.
|
||||
serveur_forge_site_sonde_depots_minimum: 1
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ nombre=${lecture%% *}
|
|||
vides=${lecture#*|}
|
||||
|
||||
if (( nombre < MINIMUM )); then
|
||||
echo "La forge du genome ne porte que ${nombre} depot(s) pour « ${ORG} » (minimum ${MINIMUM}) : un ecosysteme neuf n'a rien a cloner."
|
||||
echo "La forge du genome ne sert que ${nombre} depot(s) public(s) pour « ${ORG} » (minimum ${MINIMUM}) : un ecosysteme neuf n'a rien a cloner."
|
||||
exit 2
|
||||
fi
|
||||
|
||||
|
|
|
|||
|
|
@ -101,6 +101,29 @@ serveur_ops_forge_amont_ac_depot: "{{ serveur_ops_racine }}/.ac-amont.crt"
|
|||
serveur_ops_depots:
|
||||
- { depot: "set-ops-public", dest: "Set-OPS-public", role: "moteur" }
|
||||
|
||||
# --- LES DEPOTS PRIVES (2026-10-11, `docs/depots-prives-conception.md`) --------
|
||||
#
|
||||
# Le plan d'un ecosysteme et la carte du site ne regardent que leur runner — et le runner du
|
||||
# site. Sur la forge du site, ils sont PRIVES : chaque runner les lit en SSH, avec SA cle,
|
||||
# enregistree comme cle de deploiement en lecture sur son seul depot (`make forge-cles`).
|
||||
# Le moteur et les modeles restent publics, et se clonent comme avant, en HTTPS anonyme :
|
||||
# une cle de deploiement ne vaut que pour le depot qui l'a enregistree (mesure du jour).
|
||||
serveur_ops_roles_prives: ["instance", "tenant", "hebergeur"]
|
||||
# La cle d'hote SSH de la forge, epinglee : le runner sait a qui il parle avant de presenter
|
||||
# sa cle. Au site, elle vient de son plan (`forge_ssh_cle_hote`) ; chez un locataire, de sa
|
||||
# fiche (`serveur_ops_forge_cle_hote`, copie verifiee par `make site-intrants`).
|
||||
serveur_ops_forge_cle_hote: ""
|
||||
serveur_ops_forge_ssh_hote: "{{ serveur_ops_forge_url | urlsplit('hostname') }}"
|
||||
serveur_ops_forge_known_hosts: "{{ serveur_ops_racine }}/.ssh/known_hosts_forge"
|
||||
serveur_ops_cle_privee: "{{ serveur_ops_racine }}/.ssh/id_{{ serveur_ops_cle_type }}"
|
||||
# Ou le CONTROLEUR fabrique ses colis avant de les porter (jamais dans un depot).
|
||||
serveur_ops_colis_local: "{{ lookup('env', 'TMPDIR') | default('/tmp', true) }}"
|
||||
# La commande SSH des depots prives, posee dans LEUR config git (`core.sshCommand`) : la
|
||||
# console, la garde du genome et `make monter` la reprennent sans rien savoir de tout ceci.
|
||||
serveur_ops_ssh_prive: >-
|
||||
ssh -i {{ serveur_ops_cle_privee }} -o BatchMode=yes -o IdentitiesOnly=yes
|
||||
-o StrictHostKeyChecking=yes -o UserKnownHostsFile={{ serveur_ops_forge_known_hosts }}
|
||||
|
||||
# --- LES DEUX SYMLINKS (D-80) ------------------------------------------------
|
||||
#
|
||||
# `instance` dit QUEL tenant on pilote.
|
||||
|
|
|
|||
|
|
@ -349,7 +349,8 @@
|
|||
version: "{{ item.branche | default('main') }}"
|
||||
update: true
|
||||
force: false
|
||||
loop: "{{ serveur_ops_depots }}"
|
||||
# LES DEPOTS PUBLICS SEULEMENT (2026-10-11). Les prives suivent, plus bas, en SSH.
|
||||
loop: "{{ serveur_ops_depots | rejectattr('role', 'in', serveur_ops_roles_prives) | list }}"
|
||||
loop_control:
|
||||
label: "{{ item.depot }} → {{ item.dest }}"
|
||||
become: true
|
||||
|
|
@ -360,6 +361,162 @@
|
|||
delay: 6
|
||||
until: serveur_ops_clone is succeeded
|
||||
|
||||
# --- LES DEPOTS PRIVES : EN SSH, OU EN COLIS TANT QUE LE RUNNER NE PEUT PAS ---------------
|
||||
#
|
||||
# (2026-10-11, `docs/depots-prives-conception.md`.) Le plan d'un ecosysteme et la carte du
|
||||
# site sont prives sur la forge du site. Le runner les lit en SSH, avec SA cle, que le site a
|
||||
# enregistree comme cle de deploiement sur ce seul depot (`make forge-cles`).
|
||||
#
|
||||
# MAIS PAS ENCORE A L'INSEMINATION. Le site amorce le runner d'un locataire AVANT l'armement,
|
||||
# donc avant que la voute lui remette sa cle stable : la forge ne le connait pas. Le
|
||||
# controleur qui joue ce role — le runner du site, ou le poste — porte deja ce depot, frere
|
||||
# du moteur ; il le lui apporte en COLIS (`git bundle`). Le runner pointe ensuite `origin`
|
||||
# vers la forge en SSH : une fois arme, il tire lui-meme. Rien n'est devine : si le
|
||||
# controleur n'a pas le depot, on refuse.
|
||||
- name: Exiger la cle d'hote de la forge pour lire un depot prive
|
||||
ansible.builtin.assert:
|
||||
that: serveur_ops_forge_cle_hote | length > 0
|
||||
fail_msg: >-
|
||||
Ce runner lit des depots prives ({{ serveur_ops_depots
|
||||
| selectattr('role', 'in', serveur_ops_roles_prives) | map(attribute='depot') | join(', ') }}),
|
||||
mais `serveur_ops_forge_cle_hote` est vide. Au site : `forge_ssh_cle_hote` dans son plan.
|
||||
Chez un locataire : la copier depuis sa fiche (`make site-intrants`).
|
||||
when: serveur_ops_depots | selectattr('role', 'in', serveur_ops_roles_prives) | list | length > 0
|
||||
|
||||
- name: Epingler la cle d'hote de la forge
|
||||
ansible.builtin.copy:
|
||||
content: "{{ serveur_ops_forge_ssh_hote }} {{ serveur_ops_forge_cle_hote | trim }}\n"
|
||||
dest: "{{ serveur_ops_forge_known_hosts }}"
|
||||
owner: "{{ serveur_ops_utilisateur }}"
|
||||
group: "{{ serveur_ops_utilisateur }}"
|
||||
mode: "0644"
|
||||
when: serveur_ops_forge_cle_hote | length > 0
|
||||
|
||||
- name: Le runner lit-il deja chaque depot prive en SSH ?
|
||||
ansible.builtin.command:
|
||||
argv: [git, ls-remote, "git@{{ serveur_ops_forge_ssh_hote }}:{{ serveur_ops_forge_organisation }}/{{ item.depot }}.git", HEAD]
|
||||
environment:
|
||||
GIT_SSH_COMMAND: "{{ serveur_ops_ssh_prive }}"
|
||||
become: true
|
||||
become_user: "{{ serveur_ops_utilisateur }}"
|
||||
loop: "{{ serveur_ops_depots | selectattr('role', 'in', serveur_ops_roles_prives) | list }}"
|
||||
loop_control:
|
||||
label: "{{ item.depot }}"
|
||||
register: serveur_ops_prives_ssh
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
check_mode: false
|
||||
|
||||
- name: Tirer en SSH les depots prives que le runner peut lire
|
||||
ansible.builtin.git:
|
||||
repo: "git@{{ serveur_ops_forge_ssh_hote }}:{{ serveur_ops_forge_organisation }}/{{ item.item.depot }}.git"
|
||||
dest: "{{ serveur_ops_racine }}/{{ item.item.dest }}"
|
||||
version: "{{ item.item.branche | default('main') }}"
|
||||
key_file: "{{ serveur_ops_cle_privee }}"
|
||||
ssh_opts: "-o StrictHostKeyChecking=yes -o UserKnownHostsFile={{ serveur_ops_forge_known_hosts }} -o IdentitiesOnly=yes"
|
||||
accept_hostkey: false
|
||||
update: true
|
||||
force: false
|
||||
loop: "{{ serveur_ops_prives_ssh.results | selectattr('rc', 'equalto', 0) | list }}"
|
||||
loop_control:
|
||||
label: "{{ item.item.depot }} (SSH)"
|
||||
become: true
|
||||
become_user: "{{ serveur_ops_utilisateur }}"
|
||||
when: not ansible_check_mode
|
||||
register: serveur_ops_prives_tires
|
||||
|
||||
- name: Faire le colis des depots prives que le runner ne lit pas encore (controleur)
|
||||
ansible.builtin.command:
|
||||
argv: [git, -C, "{{ serveur_ops_freres }}/{{ item.item.dest }}", bundle, create,
|
||||
"{{ serveur_ops_colis_local }}/{{ item.item.depot }}.bundle",
|
||||
"refs/heads/{{ item.item.branche | default('main') }}"]
|
||||
loop: "{{ serveur_ops_prives_ssh.results | rejectattr('rc', 'equalto', 0) | list }}"
|
||||
loop_control:
|
||||
label: "{{ item.item.depot }} (colis)"
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
changed_when: false
|
||||
when: not ansible_check_mode
|
||||
vars:
|
||||
serveur_ops_freres: "{{ role_path | dirname | dirname | dirname }}"
|
||||
register: serveur_ops_colis_faits
|
||||
|
||||
- name: Assurer le dossier des colis sur le runner
|
||||
ansible.builtin.file:
|
||||
path: "{{ serveur_ops_racine }}/.colis"
|
||||
state: directory
|
||||
owner: "{{ serveur_ops_utilisateur }}"
|
||||
group: "{{ serveur_ops_utilisateur }}"
|
||||
mode: "0700"
|
||||
when:
|
||||
- not ansible_check_mode
|
||||
- serveur_ops_prives_ssh.results | rejectattr('rc', 'equalto', 0) | list | length > 0
|
||||
|
||||
- name: Porter les colis au runner
|
||||
ansible.builtin.copy:
|
||||
src: "{{ serveur_ops_colis_local }}/{{ item.item.depot }}.bundle"
|
||||
dest: "{{ serveur_ops_racine }}/.colis/{{ item.item.depot }}.bundle"
|
||||
owner: "{{ serveur_ops_utilisateur }}"
|
||||
group: "{{ serveur_ops_utilisateur }}"
|
||||
mode: "0600"
|
||||
loop: "{{ serveur_ops_prives_ssh.results | rejectattr('rc', 'equalto', 0) | list }}"
|
||||
loop_control:
|
||||
label: "{{ item.item.depot }} (colis)"
|
||||
when: not ansible_check_mode
|
||||
|
||||
- name: Recevoir chaque colis (clone neuf, ou avance rapide), puis viser la forge en SSH
|
||||
ansible.builtin.shell: |
|
||||
set -eu
|
||||
d="{{ serveur_ops_racine }}/{{ item.item.dest }}"
|
||||
c="{{ serveur_ops_racine }}/.colis/{{ item.item.depot }}.bundle"
|
||||
b="{{ item.item.branche | default('main') }}"
|
||||
git bundle verify -q "$c"
|
||||
if [ ! -d "$d/.git" ]; then
|
||||
git clone -q --branch "$b" "$c" "$d"
|
||||
echo "CLONE"
|
||||
else
|
||||
avant=$(git -C "$d" rev-parse HEAD)
|
||||
git -C "$d" fetch -q "$c" "refs/heads/$b:refs/remotes/colis/$b"
|
||||
git -C "$d" merge -q --ff-only "colis/$b"
|
||||
[ "$avant" = "$(git -C "$d" rev-parse HEAD)" ] || echo "AVANCE"
|
||||
fi
|
||||
git -C "$d" remote set-url origin "git@{{ serveur_ops_forge_ssh_hote }}:{{ serveur_ops_forge_organisation }}/{{ item.item.depot }}.git"
|
||||
git -C "$d" fetch -q origin 2>/dev/null && git -C "$d" branch -q --set-upstream-to "origin/$b" || true
|
||||
rm -f "$c"
|
||||
args:
|
||||
executable: /bin/bash
|
||||
loop: "{{ serveur_ops_prives_ssh.results | rejectattr('rc', 'equalto', 0) | list }}"
|
||||
loop_control:
|
||||
label: "{{ item.item.depot }} (colis)"
|
||||
become: true
|
||||
become_user: "{{ serveur_ops_utilisateur }}"
|
||||
when: not ansible_check_mode
|
||||
register: serveur_ops_colis_recus
|
||||
changed_when: "'CLONE' in serveur_ops_colis_recus.stdout or 'AVANCE' in serveur_ops_colis_recus.stdout"
|
||||
|
||||
- name: Dire quels depots prives sont venus par colis
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
{{ item.item.depot }} : le runner ne le lit pas encore en SSH ({{ (item.stderr | default('')
|
||||
| trim).splitlines()[-1:] | join('') }}). Apporte en colis par le controleur. S'il est
|
||||
arme et que ca dure : `make forge-cles` au site, et la cle de sa voute.
|
||||
loop: "{{ serveur_ops_prives_ssh.results | rejectattr('rc', 'equalto', 0) | list }}"
|
||||
loop_control:
|
||||
label: "{{ item.item.depot }}"
|
||||
|
||||
- name: Poser la commande SSH de la forge dans chaque depot prive
|
||||
community.general.git_config:
|
||||
name: core.sshCommand
|
||||
scope: local
|
||||
repo: "{{ serveur_ops_racine }}/{{ item.dest }}"
|
||||
value: "{{ serveur_ops_ssh_prive | trim }}"
|
||||
loop: "{{ serveur_ops_depots | selectattr('role', 'in', serveur_ops_roles_prives) | list }}"
|
||||
loop_control:
|
||||
label: "{{ item.depot }}"
|
||||
become: true
|
||||
become_user: "{{ serveur_ops_utilisateur }}"
|
||||
when: not ansible_check_mode
|
||||
|
||||
# CE QUI A AVANCE, ET CE QUE CA OBLIGE A RELIRE (2026-09-20).
|
||||
#
|
||||
# SEUL LE MOTEUR PORTE LE CODE DE LA CONSOLE. Un plan qui avance ne change pas
|
||||
|
|
|
|||
|
|
@ -46,6 +46,12 @@ sys.path.insert(0, str(RACINE / "scripts"))
|
|||
import underlay as underlay_mod # noqa: E402
|
||||
|
||||
|
||||
# LES DEPOTS QUI NAISSENT PRIVES (2026-10-11, `docs/depots-prives-conception.md`) : le plan
|
||||
# d'un ecosysteme et la carte du site ne regardent que leur runner et celui du site. La MEME
|
||||
# liste que `serveur_ops_roles_prives` du role, qui les clone en SSH.
|
||||
ROLES_PRIVES = {"instance", "tenant", "hebergeur"}
|
||||
|
||||
|
||||
def _depots_declares() -> list[dict]:
|
||||
"""`serveur_ops_depots` du plan du SITE — la source que le runner lit."""
|
||||
import yaml
|
||||
|
|
@ -128,8 +134,10 @@ def main() -> int:
|
|||
f"le runner clonerait ce qui est la et echouerait sur le reste, plus tard.")
|
||||
return 2
|
||||
|
||||
prives = {str(d["depot"]) for d in depots if str(d.get("role")) in ROLES_PRIVES}
|
||||
if not a.confirmer:
|
||||
print(f"\n{len(prets)} depot(s) seraient crees puis pousses.\n"
|
||||
print(f"\n{len(prets)} depot(s) seraient crees puis pousses, dont prives : "
|
||||
f"{', '.join(sorted(prives)) or 'aucun'}.\n"
|
||||
f"Relancer avec : make forge-amorcer CONFIRMER=true")
|
||||
return 0
|
||||
|
||||
|
|
@ -206,7 +214,12 @@ def main() -> int:
|
|||
st, _ = _api(base, f"/repos/{a.organisation}/{nom}", auth=auth)
|
||||
if st == 404:
|
||||
st, rep = _api(base, f"/orgs/{a.organisation}/repos", "POST",
|
||||
{"name": nom, "private": False}, auth)
|
||||
{"name": nom, "private": nom in prives}, auth)
|
||||
# PRIVE OU PUBLIC SELON CE QU'IL PORTE (2026-10-11). Le moteur et les modeles
|
||||
# restent publics ; le plan d'un ecosysteme et la carte du site naissent prives,
|
||||
# et leurs runners les clonent en SSH (`roles/serveur_ops`, colis compris). Le
|
||||
# recit de 2026-09-12 ci-dessous reste vrai pour les depots publics :
|
||||
#
|
||||
# PUBLIC DANS LE PERIMETRE DE LA FORGE, ET C'EST VOULU (2026-09-12).
|
||||
#
|
||||
# Cree en `private` au premier essai — par reflexe. Le runner clone
|
||||
|
|
|
|||
|
|
@ -159,7 +159,9 @@ def main(argv: list[str] | None = None) -> int:
|
|||
for v in voulues:
|
||||
etat = ("a ajouter" if v in ajouter else "refus" if any(v["locataire"] in r for r in refus)
|
||||
else "deja enregistree")
|
||||
print(f" {v['locataire']:<18} {v['depot']:<18} {v['cle'][:32]}… {etat}")
|
||||
# LA FIN DE LA CLE, PAS SON DEBUT : toutes les ed25519 commencent par les memes 32
|
||||
# caracteres, et deux lignes differentes s'affichaient identiques (2026-10-11).
|
||||
print(f" {v['locataire']:<18} {v['depot']:<18} …{v['cle'][-16:]} {etat}")
|
||||
for x in retirer:
|
||||
print(f" {x['locataire']:<18} {x['depot']:<18} cle perimee #{x['id']} a retirer")
|
||||
for r in refus:
|
||||
|
|
|
|||
150
scripts/forge_etancheite.py
Normal file
150
scripts/forge_etancheite.py
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
#!/usr/bin/env python3
|
||||
"""La forge du site laisse-t-elle chaque runner lire ses depots, et personne d'autre ?
|
||||
|
||||
POURQUOI (2026-10-11, `docs/depots-prives-conception.md`). Le plan d'un ecosysteme et la carte
|
||||
du site sont prives sur la forge du site ; chaque runner de locataire lit le sien avec une cle
|
||||
de deploiement en lecture seule. Deux mesures, depuis deux endroits :
|
||||
|
||||
SANS IDENTIFIANT, depuis ce poste (HTTPS) :
|
||||
- un depot PRIVE doit EXIGER une authentification. Un refus RESEAU ne compte pas : il ne
|
||||
dit rien de la forge, seulement du chemin (une sonde rapporte son errno) ;
|
||||
- un depot PUBLIC (le moteur, les modeles) doit repondre : un ecosysteme neuf le clone
|
||||
sans rien detenir.
|
||||
DEPUIS CHAQUE RUNNER DE LOCATAIRE (SSH, sa cle, la cle d'hote epinglee) :
|
||||
- il lit SON depot ;
|
||||
- la forge lui REFUSE les autres depots prives (« not authorized »).
|
||||
|
||||
Le resultat se lit ligne par ligne ; le code de sortie vaut 1 au moindre ecart.
|
||||
|
||||
Usage :
|
||||
python3 scripts/forge_etancheite.py (make forge-etancheite)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
RACINE = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(RACINE / "scripts"))
|
||||
ROLES_PRIVES = {"instance", "tenant", "hebergeur"}
|
||||
|
||||
|
||||
def classer_https(rc: int, sortie: str) -> str:
|
||||
"""'lu', 'authentification', ou 'reseau' — ce que dit un `git ls-remote` anonyme."""
|
||||
if rc == 0:
|
||||
return "lu"
|
||||
s = sortie.lower()
|
||||
if any(m in s for m in ("could not read username", "terminal prompts disabled",
|
||||
"authentication failed", "401", "403")):
|
||||
return "authentification"
|
||||
return "reseau"
|
||||
|
||||
|
||||
def classer_ssh(rc: int, sortie: str) -> str:
|
||||
"""'lu', 'refuse' (la forge a juge), ou 'reseau' (personne n'a juge)."""
|
||||
if rc == 0:
|
||||
return "lu"
|
||||
s = sortie.lower()
|
||||
if "not authorized" in s or "permission denied" in s or "does not exist" in s:
|
||||
return "refuse"
|
||||
return "reseau"
|
||||
|
||||
|
||||
def _site_et_depots():
|
||||
import underlay as underlay_mod
|
||||
plan = underlay_mod.lire_plan_site("serveurs.yml") or {}
|
||||
depots = []
|
||||
for srv in (plan.get("serveurs") or {}).values():
|
||||
d = (srv.get("variables") or {}).get("serveur_ops_depots")
|
||||
if d:
|
||||
depots = list(d)
|
||||
dom = str((underlay_mod.lire_plan_site("10-intrants.yml") or {}).get("domaine_interne") or "")
|
||||
racine_ac = (underlay_mod.chemin() or Path()).resolve().parent / "ac-racine-site.crt"
|
||||
return dom, depots, racine_ac
|
||||
|
||||
|
||||
def mesurer_https(dom: str, depots: list[dict], racine_ac: Path) -> list[str]:
|
||||
ecarts = []
|
||||
env = dict(os.environ, GIT_TERMINAL_PROMPT="0", GIT_ASKPASS="/bin/false")
|
||||
if racine_ac.is_file():
|
||||
env["GIT_SSL_CAINFO"] = str(racine_ac)
|
||||
print(f"Sans identifiant, depuis ce poste (https://forge.{dom}) :")
|
||||
for d in depots:
|
||||
nom, prive = str(d["depot"]), str(d.get("role")) in ROLES_PRIVES
|
||||
r = subprocess.run(["git", "ls-remote", f"https://forge.{dom}/genome/{nom}.git", "HEAD"],
|
||||
capture_output=True, text=True, timeout=30, env=env)
|
||||
verdict = classer_https(r.returncode, r.stdout + r.stderr)
|
||||
attendu = "authentification" if prive else "lu"
|
||||
ok = verdict == attendu
|
||||
print(f" {'OK ' if ok else 'ECART'} {nom:<18} {'prive ' if prive else 'public'} -> {verdict}"
|
||||
+ ("" if ok else f" (attendu : {attendu})"))
|
||||
if not ok:
|
||||
ecarts.append(f"{nom} : {verdict}, attendu {attendu}")
|
||||
return ecarts
|
||||
|
||||
|
||||
def mesurer_runners(depots: list[dict]) -> list[str]:
|
||||
import contexte as _ctx
|
||||
import yaml
|
||||
prives = [str(d["depot"]) for d in depots if str(d.get("role")) in ROLES_PRIVES]
|
||||
ecarts = []
|
||||
for site, loc in _ctx.couples_hebergeur():
|
||||
face = loc.face_publiee() or {}
|
||||
if face.get("hebergeur") != site.nom:
|
||||
continue
|
||||
sien = str((face.get("genome") or {}).get("depot") or "")
|
||||
runners = sorted((face.get("groupes") or {}).get("serveur_ops_tenant") or [])
|
||||
gv = yaml.safe_load((loc.dossier_inventaire() / "group_vars" / "serveur_ops.yml")
|
||||
.read_text(encoding="utf-8")) or {}
|
||||
dest = next((str(x.get("dest")) for x in gv.get("serveur_ops_depots") or []
|
||||
if x.get("role") == "instance"), "")
|
||||
if not (sien and runners and dest):
|
||||
ecarts.append(f"{loc.nom} : face sans depot, sans runner, ou plan sans `role: instance`")
|
||||
continue
|
||||
print(f"\nDepuis le runner de {loc.nom} ({runners[0]}), par l'URL de son origin :")
|
||||
# LA COMMANDE SSH DU DEPOT LUI-MEME (`core.sshCommand`, posee par serveur_ops) : on
|
||||
# mesure ce que le runner fait vraiment, pas une commande refaite a cote.
|
||||
script = (f"cd /opt/setops/{dest} && base=$(git remote get-url origin) && base=${{base%/*}} && "
|
||||
f"for n in {' '.join(prives)}; do "
|
||||
"if out=$(git ls-remote \"$base/$n.git\" HEAD 2>&1 >/dev/null); then echo \"MESURE $n lu\"; "
|
||||
"else echo \"MESURE $n $(echo $out | tr '\\n' ' ')\"; fi; done")
|
||||
r = subprocess.run(["ansible", "-i", str(loc.inventaire()), runners[0], "-b",
|
||||
"--become-user", "setops", "-m", "shell", "-a", script],
|
||||
capture_output=True, text=True, timeout=300, stdin=subprocess.DEVNULL)
|
||||
mesures = {}
|
||||
for ligne in r.stdout.splitlines():
|
||||
if ligne.startswith("MESURE "):
|
||||
_m, n, reste = ligne.split(" ", 2)
|
||||
mesures[n] = ("lu", "") if reste.strip() == "lu" else (classer_ssh(1, reste), reste.strip())
|
||||
if not mesures:
|
||||
ecarts.append(f"{loc.nom} : aucune mesure rendue par {runners[0]} ({(r.stderr or r.stdout)[-160:]})")
|
||||
print(f" ECART aucune mesure rendue")
|
||||
continue
|
||||
for n in prives:
|
||||
verdict, detail = mesures.get(n, ("absent", ""))
|
||||
attendu = "lu" if n == sien else "refuse"
|
||||
ok = verdict == attendu
|
||||
print(f" {'OK ' if ok else 'ECART'} {n:<18} -> {verdict}"
|
||||
+ ("" if ok else f" (attendu : {attendu}) {detail[:90]}"))
|
||||
if not ok:
|
||||
ecarts.append(f"{loc.nom} -> {n} : {verdict}, attendu {attendu}")
|
||||
return ecarts
|
||||
|
||||
|
||||
def main() -> int:
|
||||
dom, depots, racine_ac = _site_et_depots()
|
||||
if not dom or not depots:
|
||||
print("REFUS : aucun site monte, ou son plan ne declare pas `serveur_ops_depots`.")
|
||||
return 2
|
||||
ecarts = mesurer_https(dom, depots, racine_ac) + mesurer_runners(depots)
|
||||
if ecarts:
|
||||
print(f"\nNON ETANCHE : {len(ecarts)} ecart(s).")
|
||||
return 1
|
||||
print("\nETANCHE : chaque runner lit son depot, aucun celui d'un autre ; le public reste public.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -4465,6 +4465,7 @@ let selMachine = null;
|
|||
['artefacts_amorcage', 'Cache d\'artefacts', 'paquets apt'],
|
||||
['setops_depot_binaires', 'Dépôt des roues et collections', 'ce que le runner installe sans Internet'],
|
||||
['serveur_ops_forge_amont', 'Forge du génome', 'le moteur que mon runner suit'],
|
||||
['serveur_ops_forge_cle_hote', 'Clé d\'hôte SSH de la forge', 'épinglée par mon runner pour lire mon dépôt privé'],
|
||||
['client_backup_cible', 'Dépôt de sauvegarde', 'reçoit mon chiffré ; la vérification reste chez moi'],
|
||||
['dns_public_site', 'DNS public secondaire', ''],
|
||||
['dns_public_nom', 'Nom du DNS public', ''],
|
||||
|
|
|
|||
|
|
@ -152,6 +152,9 @@ def contrat() -> dict:
|
|||
"serveur_ops_forge_amont": f"https://{forge}" if forge else "",
|
||||
"forge_amont_ac": str((underlay_mod.chemin() or Path()).resolve().parent
|
||||
/ "ac-racine-site.crt"),
|
||||
# ET A QUI L'ON PARLE EN SSH, QUAND LE DEPOT EST PRIVE (2026-10-11) : la cle d'hote de
|
||||
# la forge, que le runner epingle avant de presenter sa cle de deploiement.
|
||||
"serveur_ops_forge_cle_hote": str(intrants.get("forge_ssh_cle_hote") or ""),
|
||||
# OU DEPOSER SON ETAT — par NOM : le depot sert plusieurs locataires et peut bouger
|
||||
"client_backup_cible": _nom_expose("serveur_backup_site", domaine),
|
||||
# QUI TIRE SES ZONES PUBLIQUES — le seul serveur a qui le primaire notifie, et le
|
||||
|
|
@ -183,6 +186,7 @@ OU_LE_LOCATAIRE_LE_DIT = {
|
|||
"artefacts_amorcage": ("all/10-intrants.yml", "artefacts_amorcage"),
|
||||
"setops_depot_binaires": ("all/10-intrants.yml", "setops_depot_binaires"),
|
||||
"serveur_ops_forge_amont": ("serveur_ops.yml", "serveur_ops_forge_amont"),
|
||||
"serveur_ops_forge_cle_hote": ("serveur_ops.yml", "serveur_ops_forge_cle_hote"),
|
||||
"client_backup_cible": ("client_backup.yml", "client_backup_cible"),
|
||||
"dns_public_site": ("all/10-intrants.yml", "dns_public_site"),
|
||||
"ip_publique_site": ("all/10-intrants.yml", "ip_publique_site"),
|
||||
|
|
|
|||
|
|
@ -435,6 +435,10 @@ def inventaire() -> dict:
|
|||
for cle in ("domaine_interne", "organisation", "dns_amorcage"):
|
||||
if intrants.get(cle):
|
||||
communes[cle] = str(intrants[cle])
|
||||
# LA CLE D'HOTE DE LA FORGE DU SITE (2026-10-11) : le runner du site l'epingle pour lire
|
||||
# en SSH les depots prives, comme ceux des locataires l'epinglent depuis leur fiche.
|
||||
if intrants.get("forge_ssh_cle_hote"):
|
||||
communes["serveur_ops_forge_cle_hote"] = str(intrants["forge_ssh_cle_hote"])
|
||||
# L'identite de sauvegarde DU SITE (moitie publique). Le depot du site l'autorise
|
||||
# pour ses propres machines ; les locataires ont chacun la leur, ailleurs.
|
||||
if intrants.get("backup_pubkey"):
|
||||
|
|
|
|||
|
|
@ -53,5 +53,24 @@ class TestPlan(unittest.TestCase):
|
|||
self.assertEqual((a, r, len(x)), ([], [], 1))
|
||||
|
||||
|
||||
class TestClassement(unittest.TestCase):
|
||||
"""`forge_etancheite` : un refus RESEAU n'est jamais lu comme une protection."""
|
||||
|
||||
def test_https(self):
|
||||
import forge_etancheite as fe
|
||||
self.assertEqual(fe.classer_https(0, ""), "lu")
|
||||
self.assertEqual(fe.classer_https(128, "fatal: could not read Username for 'https://x'"),
|
||||
"authentification")
|
||||
self.assertEqual(fe.classer_https(128, "fatal: unable to access: Connection timed out"), "reseau")
|
||||
|
||||
def test_ssh(self):
|
||||
import forge_etancheite as fe
|
||||
self.assertEqual(fe.classer_ssh(0, ""), "lu")
|
||||
self.assertEqual(fe.classer_ssh(1, "Forgejo: Deploy Key: 4 is not authorized to read genome/x"),
|
||||
"refuse")
|
||||
self.assertEqual(fe.classer_ssh(1, "ssh: connect to host 10.37.33.11 port 22: No route to host"),
|
||||
"reseau")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
|
|
|
|||
Loading…
Reference in a new issue