SDN : make devis-sdn — zone, VNets et sous-réseaux dérivés du seed (D-43/44, P30)
Ajouter un tenant implique 1 zone EVPN + 6 VNets + 6 sous-réseaux sur le cluster. Aucun générateur ne les produisait : dernière lacune dans un dépôt où tout dérive. 26 objets pour les deux tenants, VNI = 1000 + index×10 + zone, sous-réseau et passerelle par les mêmes fonctions que l'inventaire. Nommage, en deux temps. D'abord VRF0017 / v1174, alignés sur ce que le cluster portait — réflexe inverse du bon : cette convention venait d'une création à la main et ne disait pas de quel tenant il s'agissait. Forme retenue : t<index> pour la zone, t<index><zone abrégée> pour le VNet (t17, t17serv). C'est le préfixe que le pare-feu Proxmox utilisait déjà (t17-cli-metrique), donc un seul schéma dans tout le dépôt. Abréviation = 4 premières lettres du libellé, accents retirés. Pas de tiret entre index et zone, contrairement aux IPSets : t245-serv ferait 9 caractères, t245serv en fait 8 — la forme reste uniforme jusqu'au dernier index. Contrainte cadrante : zones ET VNets sont limités à 8 caractères par Proxmox. sdn-evpn.md annonçait chez17-services-infra (21) : il aurait été refusé à l'application. P30 refuse tout dépassement et toute collision de nom, de VNI ou de sous-réseau. Éprouvé aux bornes et par sabotage. Vérification la plus forte : avant renommage, la dérivation reproduisait à l'identique les deux zones créées à la main — nom, VNI de VRF, MTU, contrôleur. voute.py saisir : le pendant de la génération. On génère un secret dont le dépôt est la source, on saisit celui dont un tiers est la source — inventer une clé d'API OPNsense donnerait une valeur refusée à la première requête, avec P18 au vert sur une voûte inutilisable. Sans écho, double confirmation, rien sur la ligne de commande. Reste ouvert : aucun nœud de sortie déclaré. Le devis émet un marqueur, pas une valeur plausible. Deux points à trancher — le nœud de sortie route selon sa propre table (défaut actuel : 192.168.11.254, pas la frontière), et l'entrée n'est pas redondante puisqu'elle dépend d'une route statique d'OPNsense vers un seul nœud. D-45 : l'affinité de VM attend Proxmox 9 (cluster en 8.4.19), tenue à la main. 30 preuves OK. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
22ef279464
commit
3b1d9b6660
11 changed files with 499 additions and 4 deletions
66
CHANGELOG.md
66
CHANGELOG.md
|
|
@ -1,5 +1,71 @@
|
|||
# CHANGELOG — Set-OPS
|
||||
|
||||
## 2026-08-03 (suite 17) — le plan de données existe (`make devis-sdn`)
|
||||
|
||||
Ajouter un tenant n'ajoute pas qu'un plan : cela implique **1 zone EVPN + 6 VNets +
|
||||
6 sous-réseaux** sur le cluster. Aucun générateur ne les produisait — c'était la
|
||||
dernière lacune dans un dépôt où tout dérive du seed.
|
||||
|
||||
`make devis-sdn` les émet, tenant par tenant. **26 objets** pour les deux tenants,
|
||||
tous dérivés : le VNI est `1000 + index×10 + zone`, le sous-réseau et la passerelle
|
||||
viennent des mêmes fonctions que l'inventaire.
|
||||
|
||||
### Le nommage, en deux temps
|
||||
|
||||
Première version : `VRF0017` / `v1174`, alignés sur ce que le cluster portait déjà.
|
||||
C'était le réflexe inverse du bon — cette convention venait d'une création à la main,
|
||||
ne disait pas de quel tenant il s'agissait, et `chez174` demandait d'ouvrir la table
|
||||
des catégories pour être lu.
|
||||
|
||||
Forme retenue : **`t<index>`** pour la zone, **`t<index><zone abrégée>`** pour le
|
||||
VNet — `t17`, `t17serv`, `t17obse`. C'est le préfixe que le **pare-feu Proxmox
|
||||
utilisait déjà** (`t17-cli-metrique`, `t17-flotte`) : un seul schéma se lit dans tout
|
||||
le dépôt. L'abréviation vient des 4 premières lettres du libellé, accents retirés,
|
||||
donc dérivée.
|
||||
|
||||
Pas de tiret entre l'index et la zone, contrairement aux IPSets : `t245-serv` ferait
|
||||
9 caractères. Sans séparateur, `t245serv` en fait 8 — la forme reste **uniforme
|
||||
jusqu'au dernier index de la fédération**.
|
||||
|
||||
### La contrainte qui a tout cadré
|
||||
|
||||
Zones et VNets sont **limités à 8 caractères** par Proxmox — l'identifiant sert de
|
||||
base aux noms de bridge, veth et tap. Le tableau de `sdn-evpn.md` annonçait
|
||||
`chez17-services-infra`, soit 21 : il aurait été **refusé à l'application**. **P30**
|
||||
refuse désormais tout dépassement, sur les deux objets, et toute collision de nom, de
|
||||
VNI ou de sous-réseau entre tenants.
|
||||
|
||||
Éprouvé aux bornes (`t1serv` 6, `t17serv` 7, `t245serv` 8) et par sabotage : deux
|
||||
libellés partageant leurs 4 premières lettres produisent le même VNet, et la garde
|
||||
l'attrape.
|
||||
|
||||
### Vérification la plus forte disponible
|
||||
|
||||
Avant renommage, la dérivation **reproduisait à l'identique** les deux zones déjà
|
||||
créées à la main — nom, VNI de VRF, MTU, contrôleur. La dérivation retombait sur ce
|
||||
qu'un humain avait posé.
|
||||
|
||||
### `voute.py saisir` : le pendant de la génération
|
||||
|
||||
On **génère** un secret dont le dépôt est la source ; on **saisit** celui dont un tiers
|
||||
est la source. Inventer une clé d'API OPNsense donnerait une valeur syntaxiquement
|
||||
correcte, refusée à la première requête — et P18 au vert sur une voûte inutilisable.
|
||||
|
||||
Saisie sans écho, double confirmation, rien sur la ligne de commande. Éprouvée sur une
|
||||
voûte jetable : écrit, préserve l'existant, refuse d'écraser sans `--remplacer`.
|
||||
|
||||
### Ce qui reste ouvert
|
||||
|
||||
Aucune zone ne déclare de **nœud de sortie**, et le devis émet un marqueur plutôt
|
||||
qu'une valeur plausible. Deux points à trancher avant :
|
||||
|
||||
- le nœud de sortie route selon **sa propre table** ; la passerelle par défaut des
|
||||
trois hyperviseurs est `192.168.11.254`, pas la frontière ;
|
||||
- l'**entrée n'est pas redondante** : elle dépend d'une route statique d'OPNsense vers
|
||||
**un** nœud. Deux nœuds de sortie ne donnent aucune redondance entrante.
|
||||
|
||||
**D-43**, **D-44**, **D-45**, **AFF-112**. 30 preuves OK.
|
||||
|
||||
## 2026-08-03 (suite 16) — la directive d'authentification est gardée (P29)
|
||||
|
||||
Une règle qu'aucune garde ne vérifie finit par ne plus être vraie : c'est exactement ce
|
||||
|
|
|
|||
7
Makefile
7
Makefile
|
|
@ -362,6 +362,13 @@ devis-proxmox-pools: ansible-runtime ## Devis des pools Proxmox (un par tenant),
|
|||
devis-proxmox-pools-verifier:
|
||||
python3 scripts/devis_proxmox_pools.py --verifier
|
||||
|
||||
.PHONY: devis-sdn devis-sdn-verifier
|
||||
devis-sdn: ansible-runtime ## Devis SDN EVPN (zone + VNets + sous-reseaux par tenant), derive du seed
|
||||
python3 scripts/devis_sdn.py $(if $(JSON),--json,)
|
||||
|
||||
devis-sdn-verifier:
|
||||
python3 scripts/devis_sdn.py --verifier
|
||||
|
||||
devis-opnsense-verifier:
|
||||
python3 scripts/devis_opnsense.py --verifier
|
||||
|
||||
|
|
|
|||
|
|
@ -160,6 +160,7 @@ Légende du statut : ✅ prouvée · 🟡 partielle · ❌ fausse · ⚪ invéri
|
|||
| AFF-109 | « Ce qui appartient à l'hébergeur (cluster, fabric, frontière) vit dans son dépôt, jamais recopié chez un tenant » (`docs/decisions-architecture.md` D-35) | structurelle | **P27** (`scripts/prouver.py`) | ✅ | Le chemin se dérive du symlink `underlay.yml` : l'hébergeur n'est toujours pas déclaré (D-17). Sautée si aucun underlay n'est monté. |
|
||||
| AFF-110 | « Chaque tenant a son pool Proxmox, dérivé, sans collision de nom ni de VMID » (`docs/decisions-architecture.md` D-37) | structurelle | **P28** (`scripts/devis_proxmox_pools.py --verifier`) | ✅ | Le nom dérive du dossier d'instance et de l'`index`, déjà unique par P21 — aucun registre de plus. Le devis ne lit pas le cluster : il affirme la cohérence du **plan**, pas l'état appliqué. |
|
||||
| AFF-111 | « Chaque rôle `serveur_*` déclare sa position d'authentification, et la déclaration est cohérente avec son code » (`docs/authentification.md`, D-38 à D-41) | structurelle | **P29** (`scripts/prouver.py`) | ✅ | 23 rôles déclarés. La preuve refuse l'oubli **et** le mensonge : une déclaration `web-sso natif` sans variable `<rôle>_oidc`, ou `ldap-direct` sans variable `<rôle>_ldap`, échoue. Les indices sont **nommés** — une première version cherchait les mots « ldap »/« oidc » et le mot LDAP dans un commentaire suffisait à valider un mensonge. Les 2 portées `interne-sans-auth` (Prometheus, Loki) sont des lacunes **comptées, pas masquées**. |
|
||||
| AFF-112 | « Ajouter un tenant implique une zone EVPN, ses VNets et ses sous-réseaux, tous dérivés du seed » (`docs/sdn-evpn.md` §2) | structurelle | **P30** (`scripts/devis_sdn.py --verifier`) | ✅ | 2 zones, 12 VNets, 12 sous-réseaux. La dérivation **reproduit à l'identique** les deux zones déjà présentes sur le cluster (nom, VNI de VRF, MTU, contrôleur) — vérifié par lecture de l'API. La garde refuse un identifiant de VNet de plus de 8 caractères, limite Proxmox qui aurait fait échouer le devis à l'application. Les **nœuds de sortie manquants** sont signalés, pas inventés. |
|
||||
| AFF-106 | « Le panneau couvre le schéma du plan : aucun champ n'oblige à éditer le YAML » (`docs/intrants-base-gui-conception.md`) | fonctionnelle | **P19** (`scripts/couverture_gui.py`) | 🟡 | Vrai des registres du plan. **Exceptions assumées** : `reseaux` et `hotes` de l'underlay (listes de tables) restent hors du panneau ; les ports physiques et le nœud de sortie EVPN se nomment à la main. |
|
||||
|
||||
**Non enregistré volontairement.** Les devis eux-mêmes (`make devis-reseau`,
|
||||
|
|
@ -401,6 +402,7 @@ Chaque affirmation ✅ est soit **rejouée** par une preuve de `make prouver`
|
|||
| AFF-109 | **P27** (propriété des intrants : hébergeur et tenant séparés) |
|
||||
| AFF-110 | **P28** (pools Proxmox : un par tenant) |
|
||||
| AFF-111 | **P29** (authentification : chaque rôle déclare sa position) |
|
||||
| AFF-112 | **P30** (SDN EVPN : zones, VNets et sous-réseaux dérivés) |
|
||||
|
||||
**Structurelles/doctrinales, hors harnais récurrent** (vérifiées à l'audit) : AFF-005
|
||||
(`make`=aide), AFF-014 (ciblage `groupe:&hotes_actifs`), AFF-024 (`instancier-appliquer`,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
> [`docs/audit/affirmations.md`](affirmations.md).
|
||||
|
||||
- **Instance** : `instance` — inventaire `instance/inventories/principal/hosts.yml`
|
||||
- **Verdict** : ✅ CONFORME (29 OK · 0 echec · 0 saute)
|
||||
- **Verdict** : ✅ CONFORME (30 OK · 0 echec · 0 saute)
|
||||
|
||||
## Preuves
|
||||
|
||||
|
|
@ -39,9 +39,10 @@
|
|||
| P24 | Frontiere nord/sud : acces d'administration declare | AFF-104 | ✅ OK | CONFORME : frontiere nord/sud, 26 regles, 2 routes, admin=192.168.254.2/32,192.168.255.0/24,192.168.255.2/32. |
|
||||
| P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 2 tenant(s), 36 groupe(s), 56 regle(s). |
|
||||
| P26 | Integrations universelles : aucun hote laisse de cote | AFF-108 | ✅ OK | 14 hote(s) x 3 integration(s) universelle(s) : aucune lacune, aucune recopie (1 exemption(s) derivee(s) du service rendu). |
|
||||
| P27 | Propriete des intrants : hebergeur et tenant separes | AFF-109 | ✅ OK | 7 cle(s) de cluster chez l'hebergeur, aucune recopiee dans les group_vars du tenant. |
|
||||
| P27 | Propriete des intrants : hebergeur et tenant separes | AFF-109 | ✅ OK | 8 cle(s) de cluster chez l'hebergeur, aucune recopiee dans les group_vars du tenant. |
|
||||
| P28 | Pools Proxmox : un par tenant, sans collision | AFF-110 | ✅ OK | CONFORME : 2 pool(s) Proxmox, 28 VM placee(s), aucun nom ni VMID en collision. |
|
||||
| P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 23 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 12, 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 ; /!\ aucun noeud de sortie declare (VRF sans chemin vers l'exterieur). |
|
||||
|
||||
## Couverture des affirmations ✅ du registre
|
||||
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ Ce que je re-découvre sinon. **Consulter avant de concevoir un nouveau mécanis
|
|||
| Multi-instance | un dépôt par écosystème ; l'active = symlink `instance/`, les autres **découvertes par convention** (dossiers frères, aucun registre) | active : symlink `instance/` ; découverte : `scripts/instances.py` / `devis_reseau.py` (glob `../*/plan/nomenclature.yml` avec `index`) ; garde-fou collision : preuve **P21** | `multi-instances.md` |
|
||||
| Exposition → edge | app expose un FQDN public servi par un edge | `plan/domaines.yml` + `expose` (applications) | `bindings-conception.md` §4 |
|
||||
| Authentification | web → Keycloak ; LDAP source unique ; secours par `sudo`, formulaire local non annoncé | `<rôle>_connexion_locale: false` (grafana, forgejo, nextcloud) ; garde de version Forgejo ≥ 10 | `authentification.md` |
|
||||
| SDN EVPN | ajouter un tenant implique **1 zone + 6 VNets + 6 sous-réseaux**, tous dérivés du seed | `scripts/devis_sdn.py` (`make devis-sdn`) ; nommage dérivé du tenant (`CHEZ17`, `chez174`), ≤ 8 caractères ; garde **P30** | `sdn-evpn.md` §2 |
|
||||
| Pools Proxmox | un pool par tenant : les noms courts de VM sont **volontairement identiques** d'un tenant à l'autre (même fonction, même nom), et seule la console Proxmox en souffrait | `scripts/devis_proxmox_pools.py` (`make devis-proxmox-pools`) ; nom dérivé de l'`index` ; garde de collision = preuve **P28** | `decisions-architecture.md` D-37 |
|
||||
| Frontière nord/sud | les flux `pair: externe` — **sautés** par le pare-feu d'hôte — sont la politique de bordure | `scripts/devis_opnsense.py` (`make devis-opnsense`) ; garde d'accès admin = preuve **P24** | `frontiere-opnsense.md` |
|
||||
|
||||
|
|
|
|||
|
|
@ -86,6 +86,28 @@ L'assistant demande « Configurer la voûte de secrets maintenant ». Si `oui` :
|
|||
- Ces secrets ne transitent **jamais** par le GUI ni par aucun fichier en clair ;
|
||||
le GUI n'en affiche que les **noms** (panneau « Intrants »).
|
||||
|
||||
### Deux façons de remplir la voûte, et il ne faut pas les confondre
|
||||
|
||||
| | Qui est la source | Comment |
|
||||
|---|---|---|
|
||||
| **Générer** | le **dépôt** — Ansible configure les deux côtés depuis la même variable (ex. `vault_nextcloud_oidc` : le client Keycloak le déclare, le rôle Nextcloud le lit) | valeur aléatoire, 32 octets |
|
||||
| **Saisir** | un **tiers** — le secret existe déjà ailleurs et ne s'invente pas (clé d'API OPNsense, jeton Proxmox) | `python3 scripts/voute.py saisir <clés>` |
|
||||
|
||||
```bash
|
||||
ANSIBLE_VAULT_PASSWORD_FILE=~/.config/setops-vault-pass \
|
||||
python3 scripts/voute.py saisir vault_opnsense_api_key vault_opnsense_api_secret
|
||||
```
|
||||
|
||||
Saisie **sans écho**, double confirmation, rien sur la ligne de commande — donc ni
|
||||
dans l'historique du shell, ni dans la liste des processus. Rien n'est écrit en clair
|
||||
sur disque : la voûte est déchiffrée en mémoire, complétée, reparsée et re-déchiffrée
|
||||
pour contrôle **avant** d'être posée. Une clé déjà renseignée est ignorée, sauf
|
||||
`--remplacer`.
|
||||
|
||||
> **Pourquoi la distinction compte.** Inventer une clé d'API OPNsense produirait une
|
||||
> valeur syntaxiquement correcte, refusée à la première requête — et **P18 passerait
|
||||
> au vert** sur une voûte inutilisable. Pire qu'une absence : un faux confort.
|
||||
|
||||
### `proxmox.vault.yml` n'est plus lue (retirée le 2026-08-03)
|
||||
|
||||
Les playbooks ne chargent plus ce fichier. Tolérée « en compatibilité », elle était
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ sont les seules vérifiables.
|
|||
| **D-05** | **Un seul commutateur route** ; les autres restent en L2 pur | sans MLAG, dupliquer les SVI créerait autant de conflits d'adresses que de zones | `underlay.yml.example` | P23 |
|
||||
| **D-06** | Les réseaux déclarent leur **fabric** ; un devis ne parle que de la sienne | le stockage jumbo vit sur ses propres commutateurs — un devis est une configuration, pas un inventaire | `sdn-evpn.md` §4 | P23 |
|
||||
| **D-07** | **Pas d'ACL** sur cette fabric (`acl_inter_tenant: false`) | le matériel ne sait pas lier une ACL à un SVI ; des règles jamais liées auraient l'air d'isoler | `frontiere-opnsense.md` §1 | — |
|
||||
| **D-43** | Les objets SDN d'un tenant (zone, VNets, sous-réseaux) sont **dérivés du seed** et émis par `make devis-sdn` | ajouter un tenant implique 13 objets de cluster ; les créer à la main dans un dépôt où tout dérive était la dernière lacune | `sdn-evpn.md` §2 | P30 |
|
||||
| **D-44** | Le **nommage SDN dérive du tenant** : zone `<PRÉFIXE><index>`, VNet `<préfixe><index><zone>` | se lit sans table de correspondance ; les zones faites à la main (`VRF0017`) sont remplacées tant qu'elles sont vides. Seule contrainte conservée : **8 caractères**, imposée par Proxmox | `sdn-evpn.md` §2 | P30 |
|
||||
| **D-08** | Le **routage tenant passe au SDN EVPN** — une zone par tenant | une zone EVPN est un VRF : l'isolation devient structurelle, pas réglementaire | `sdn-evpn.md` | P23 (MTU) |
|
||||
| **D-09** | Le **filtrage inter-zone d'un tenant** se fait au même niveau (EVPN) | là où le routage a lieu | `sdn-evpn.md` §3 | — |
|
||||
| **D-31** | Le filtrage est-ouest est appliqué **deux fois** : hyperviseur **puis** nftables d'hôte | défense en profondeur ; les deux dérivent du même registre par les mêmes fonctions, donc ne peuvent pas diverger | `sdn-evpn.md` §3 | P25 |
|
||||
|
|
@ -53,6 +55,7 @@ sont les seules vérifiables.
|
|||
| **D-35** | Le **cluster Proxmox** appartient à l'hébergeur ; seuls le **golden template** et les **défauts de placement** restent au tenant | recopié chez chaque tenant, l'inventaire du cluster avait déjà divergé — deux listes de stockages contradictoires pour le même matériel | `config-proxmox.md` | P27 |
|
||||
| **D-36** | Le panneau **nomme le propriétaire** de chaque section d'intrants | éditer une section « hébergeur » vaut pour tous ses tenants ; l'écran ne le disait pas | `scripts/inventory_gui.py` (`INTRANTS_SCHEMA`) | — |
|
||||
| **D-37** | Chaque tenant a son **pool Proxmox** ; les noms courts de VM restent **identiques** d'un tenant à l'autre | 11 serveurs sur 14 sont homonymes — c'est la preuve que la nomenclature est un gabarit ; le coût est humain (la console affiche le nom), et le pool le corrige sans rien renommer | `devis_proxmox_pools.py` | P28 |
|
||||
| **D-45** | L'**affinité de VM** (garder un groupe sur le même hyperviseur) attend **Proxmox 9** ; tenue à la main d'ici là | les *resource affinity rules* n'existent qu'en 9 ; en 8.4 seuls les groupes HA épinglent à des **nœuds**, pas des VM entre elles. Sans ressource HA déclarée, rien ne déplace ni ne sépare les VM — le sujet ne devient réel qu'en activant la HA | `sdn-evpn.md` | — |
|
||||
| **D-18** | Chaque tenant a un **responsable désigné** | sans lui, « qui peut décider de déménager cette organisation ? » se pose au pire moment | `migration-tenant.md` §3 | — |
|
||||
|
||||
| **D-38** | Toute authentification **web** passe par Keycloak ; LDAP est la **source unique** des comptes | une identité, un mot de passe ; aucun service ne tient son propre répertoire d'humains | `authentification.md` §1-2 | — |
|
||||
|
|
|
|||
|
|
@ -30,11 +30,34 @@ Vérifiée sur les deux tenants fédérés, elle ne demande **aucun changement d
|
|||
| Objet Proxmox SDN | Vient de | Exemple (Chezlepro, zone Services-infra) |
|
||||
|---|---|---|
|
||||
| **zone** (un VRF) | le tenant | `CHEZ17` |
|
||||
| **VNet** | la zone de sécurité | `chez17-services-infra` |
|
||||
| **VNet** | la zone de sécurité | `chez174` |
|
||||
| **tag** (VNI) | `vlan_de(index, zone)` | `1174` |
|
||||
| **subnet** | `sous_reseau_de(index, zone)` | `10.27.19.0/24` |
|
||||
| **gateway** | `passerelle_de(index, zone)` | `10.27.19.1` |
|
||||
|
||||
> **Rectification du 2026-08-03.** Ce tableau annonçait `chez17-services-infra`, qui
|
||||
> aurait été **refusé à l'application** : zones et VNets sont limités à **8 caractères**
|
||||
> par Proxmox — l'identifiant sert de base aux noms de bridge, veth et tap. Message
|
||||
> amont : *« zone ID … can't be more length than 8 characters »*.
|
||||
>
|
||||
> Le nommage dérive du tenant, comme tout le reste : `<PRÉFIXE><index>` pour la zone,
|
||||
> `<préfixe><index><zone>` pour le VNet. Le préfixe vient de `devis_reseau.prefixe()` —
|
||||
> la même fonction que le devis des commutateurs, donc un seul endroit fabrique le nom
|
||||
> court d'un tenant. Éprouvé jusqu'au pire cas de la fédération : `COOP245` = 7,
|
||||
> `coop2459` = 8. **P30** refuse tout dépassement, sur les deux objets.
|
||||
>
|
||||
> **Les zones créées à la main (`VRF0011`, `VRF0017`) sont remplacées.** Ce nommage ne
|
||||
> disait ni de quel tenant il s'agissait, ni rien qu'on puisse relier au plan : il
|
||||
> fallait une table de correspondance pour le lire. Le remplacement se fait **pendant
|
||||
> que les zones sont vides** — les supprimer ne débranche rien. Avec des VM attachées,
|
||||
> ce serait une migration ; le devis émet donc une §0 qui les retire d'abord.
|
||||
|
||||
**`make devis-sdn`** émet ces objets, tenant par tenant : la zone, ses six VNets, ses six
|
||||
sous-réseaux, puis `pvesh set /cluster/sdn` pour pousser. Non destructif, à relire.
|
||||
Vérification la plus forte disponible : la dérivation **reproduit à l'identique** les deux
|
||||
zones déjà présentes sur le cluster — même nom, même VNI de VRF, même MTU, même
|
||||
contrôleur.
|
||||
|
||||
Le seed `index` reste la source unique. Le `.1` ne change pas d'adresse, il **change de
|
||||
porteur** : du SVI d'un commutateur vers la passerelle **anycast** du VNet, présente sur
|
||||
chaque hyperviseur — donc plus proche de la VM, et sans point unique de défaillance.
|
||||
|
|
@ -141,7 +164,7 @@ Lecture seule par l'API Proxmox. **Le plan de contrôle existe, le plan de donn
|
|||
| Contrôleur | `EVPN0017`, ASN 65000 |
|
||||
| Zones | `VRF0011`, `VRF0017` — un VRF par tenant, VNI = index, MTU 1450 |
|
||||
| VNets | **aucun** |
|
||||
| Nœuds de sortie | **aucun** |
|
||||
| Nœuds de sortie | **aucun** — un VRF sans sortie n'a aucun chemin vers la frontière |
|
||||
|
||||
### Deux blocages à lever avant d'aller plus loin
|
||||
|
||||
|
|
|
|||
282
scripts/devis_sdn.py
Normal file
282
scripts/devis_sdn.py
Normal file
|
|
@ -0,0 +1,282 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Devis SDN EVPN — le plan de donnees d'un tenant.
|
||||
|
||||
Ajouter un tenant n'ajoute pas qu'un plan et un inventaire : cela implique, sur le
|
||||
cluster, UNE ZONE EVPN (le VRF du tenant), UN VNET par zone de securite, et UN
|
||||
SOUS-RESEAU par VNet avec sa passerelle anycast. Sans ces objets, aucune VM ne peut
|
||||
etre branchee — le plan de controle existe, le plan de donnees non.
|
||||
|
||||
Tout DERIVE du seed `index` (cf. docs/sdn-evpn.md §2) : le VNI est
|
||||
`1000 + index*10 + zone`, le sous-reseau et la passerelle viennent des memes
|
||||
fonctions que l'inventaire. Rien n'est saisi deux fois.
|
||||
|
||||
NOMMAGE — aligne sur le prefixe `t<index>` DEJA utilise par le pare-feu Proxmox
|
||||
(`t17-cli-metrique`, `t17-flotte`) : un seul schema se lit dans tout le depot.
|
||||
- zone : `t<index>` -> t17, t11
|
||||
- VNet : `t<index><zone>` -> t17fron, t17iden, t17donn,
|
||||
t17serv, t17obse, t17appl
|
||||
|
||||
L'abreviation de zone vient des 4 premieres lettres du libelle de la nomenclature,
|
||||
sans accent : elle DIT la zone, la ou un simple chiffre demandait la table des
|
||||
categories. `chez174` n'etait pas lisible ; `t17serv` l'est.
|
||||
|
||||
Pas de tiret entre l'index et la zone, contrairement aux IPSets : `t245-serv` ferait
|
||||
9 caracteres. Sans separateur, `t245serv` en fait 8 — la forme reste UNIFORME jusqu'au
|
||||
dernier index de la federation, ce qu'un separateur qui saute selon la taille de
|
||||
l'index ne permettrait pas.
|
||||
|
||||
Le cluster portait `VRF0011` / `VRF0017`, herites d'une creation a la main. Ce
|
||||
nommage-la ne dit ni de quel tenant il s'agit, ni rien qu'on puisse relier au plan :
|
||||
il fallait un tableau de correspondance pour le lire. On le remplace pendant que les
|
||||
zones sont VIDES — le cout est nul aujourd'hui, ce serait une migration une fois des
|
||||
VM branchees dessus.
|
||||
|
||||
LA SEULE CONTRAINTE QUI SURVIT, parce qu'elle vient du produit et non d'une habitude :
|
||||
zones ET VNets sont **limites a 8 caracteres** (l'identifiant sert de base aux noms de
|
||||
bridge, veth, tap ; message amont : « zone ID ... can't be more length than 8
|
||||
characters »). Le nommage retenu tient : `CHEZ245` = 7, `chez2456` = 8 au pire cas de
|
||||
la federation. P30 refuse tout depassement — la faute ne se decouvrirait qu'a
|
||||
l'application.
|
||||
|
||||
CE QUI APPARTIENT A L'HEBERGEUR : le controleur EVPN, son ASN et les noeuds de
|
||||
sortie sont des objets de CLUSTER (D-35). Ils se lisent dans
|
||||
`<depot hebergeur>/proxmox-hebergeur.yml`, jamais chez le tenant.
|
||||
|
||||
NON destructif : ce script n'ecrit rien sur le cluster.
|
||||
|
||||
Usage :
|
||||
python3 scripts/devis_sdn.py # devis lisible
|
||||
python3 scripts/devis_sdn.py --json # meme contenu, pour l'API
|
||||
python3 scripts/devis_sdn.py --verifier # garde : aucune collision, noms valides
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import unicodedata
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
RACINE = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(RACINE / "scripts"))
|
||||
|
||||
from inventory_rules import passerelle_de, sous_reseau_de, vlan_de # noqa: E402
|
||||
from devis_reseau import decouvrir # noqa: E402
|
||||
import underlay as underlay_mod # noqa: E402
|
||||
|
||||
|
||||
# Limite Proxmox, pour les zones ET les VNets : l'identifiant sert de base aux noms
|
||||
# de bridge, veth et tap, auxquels le produit ajoute prefixes et suffixes.
|
||||
LONGUEUR_MAX_ID = 8
|
||||
MARQUEUR_SORTIE = "<NOEUDS-DE-SORTIE-A-DESIGNER>"
|
||||
# Nommages precedents, a retirer : VRF00xx (cree a la main) puis CHEZ17/TECH11
|
||||
# (premiere derivation, remplacee — les chiffres de zone n'etaient pas lisibles).
|
||||
ANCIEN_NOMMAGE = ("VRF0011", "VRF0017", "CHEZ17", "TECH11")
|
||||
|
||||
|
||||
def zone_de(index: int) -> str:
|
||||
"""Zone EVPN : `t<index>` — meme prefixe que les IPSets du pare-feu Proxmox."""
|
||||
return f"t{int(index)}"
|
||||
|
||||
|
||||
def abreger(libelle: str, taille: int = 4) -> str:
|
||||
"""4 premieres lettres du libelle, sans accent ni ponctuation, en minuscules.
|
||||
|
||||
Mecanique, donc derivee : « Services-infra » -> `serv`, « Observabilite » ->
|
||||
`obse`. Deux libelles partageant leurs 4 premieres lettres produiraient le meme
|
||||
VNet — la garde de collision l'attrape et exige de renommer la categorie, plutot
|
||||
que d'inventer un suffixe illisible.
|
||||
"""
|
||||
plat = unicodedata.normalize("NFKD", str(libelle))
|
||||
plat = "".join(c for c in plat if not unicodedata.combining(c))
|
||||
plat = "".join(c for c in plat if c.isalnum())
|
||||
return plat.lower()[:taille] or "zone"
|
||||
|
||||
|
||||
def vnet_de(index: int, libelle: str) -> str:
|
||||
"""VNet : `t<index><zone abregee>` — t17serv, t17obse. Minuscules : cet
|
||||
identifiant devient une base de nom d'interface."""
|
||||
return f"{zone_de(index)}{abreger(libelle)}"
|
||||
|
||||
|
||||
def sdn_hebergeur() -> dict:
|
||||
"""Controleur, ASN et noeuds de sortie — propriete de l'HEBERGEUR (D-35).
|
||||
|
||||
Trouve par derivation du symlink `underlay.yml`, qui designe deja l'hebergeur :
|
||||
rien de nouveau n'est declare (D-17). Vide si aucun hebergeur n'est monte.
|
||||
"""
|
||||
lien = Path(os.environ.get("SETOPS_UNDERLAY") or (RACINE / "underlay.yml"))
|
||||
if not lien.exists():
|
||||
return {}
|
||||
fichier = lien.resolve().parent / "proxmox-hebergeur.yml"
|
||||
if not fichier.is_file():
|
||||
return {}
|
||||
data = yaml.safe_load(fichier.read_text(encoding="utf-8")) or {}
|
||||
return data.get("proxmox_sdn") or {}
|
||||
|
||||
|
||||
def construire(tenants: list[tuple[str, str, dict]]) -> dict:
|
||||
sdn = sdn_hebergeur()
|
||||
u = underlay_mod.charger()
|
||||
mtu = underlay_mod.mtu_overlay(u)
|
||||
blocs = []
|
||||
for nom_instance, _prefixe, nomenclature in tenants:
|
||||
index = int(nomenclature["index"])
|
||||
vnets = []
|
||||
for cle, cat in sorted((nomenclature.get("categories") or {}).items(),
|
||||
key=lambda x: int(x[0])):
|
||||
z = int(cle)
|
||||
vnets.append({
|
||||
"vnet": vnet_de(index, cat.get("libelle", f"zone{z}")),
|
||||
"libelle": str(cat.get("libelle", f"zone-{z}")),
|
||||
"tag": vlan_de(index, z),
|
||||
"sous_reseau": sous_reseau_de(index, z),
|
||||
"passerelle": passerelle_de(index, z),
|
||||
})
|
||||
blocs.append({
|
||||
"tenant": nom_instance,
|
||||
"index": index,
|
||||
"zone": zone_de(index),
|
||||
"vrf_vxlan": index,
|
||||
"mtu": mtu,
|
||||
"vnets": vnets,
|
||||
})
|
||||
return {
|
||||
"controleur": sdn.get("controleur") or "<CONTROLEUR-EVPN-A-DECLARER>",
|
||||
"asn": sdn.get("asn"),
|
||||
"noeuds_de_sortie": sdn.get("noeuds_de_sortie") or [],
|
||||
"anciennes": list(ANCIEN_NOMMAGE),
|
||||
"zones": blocs,
|
||||
}
|
||||
|
||||
|
||||
def rendre(devis: dict) -> str:
|
||||
sortie = devis["noeuds_de_sortie"]
|
||||
out = [
|
||||
"# Devis SDN EVPN — une zone, ses VNets, ses sous-reseaux, par tenant",
|
||||
"#",
|
||||
"# Tout derive du seed `index` : VNI = 1000 + index*10 + zone ; le sous-reseau et",
|
||||
"# la passerelle viennent des memes fonctions que l'inventaire.",
|
||||
"#",
|
||||
"# Le `.1` ne change pas d'adresse, il change de PORTEUR : du SVI d'un commutateur",
|
||||
"# vers la passerelle anycast du VNet, presente sur chaque hyperviseur.",
|
||||
"",
|
||||
f"# Controleur (hebergeur) : {devis['controleur']}"
|
||||
+ (f" ASN {devis['asn']}" if devis.get("asn") else ""),
|
||||
"",
|
||||
]
|
||||
if devis.get("anciennes"):
|
||||
out += [
|
||||
"## 0. Retirer l'ancien nommage (fait a la main, remplace)",
|
||||
"#",
|
||||
"# Ces zones ne portent AUCUN VNet : les supprimer ne debranche rien. C'est",
|
||||
"# precisement pourquoi on renomme maintenant — avec des VM attachees, ce",
|
||||
"# serait une migration. Verifier l'absence de VNet avant de supprimer :",
|
||||
"# pvesh get /cluster/sdn/vnets --output-format json",
|
||||
]
|
||||
for z in devis["anciennes"]:
|
||||
out.append(f"pvesh delete /cluster/sdn/zones/{z}")
|
||||
out.append("")
|
||||
for b in devis["zones"]:
|
||||
out += [
|
||||
f"## {b['tenant']} — zone `{b['zone']}` (index {b['index']}, VNI de VRF {b['vrf_vxlan']})",
|
||||
"",
|
||||
"### 1. La zone (le VRF du tenant)",
|
||||
f"pvesh create /cluster/sdn/zones --zone {b['zone']} --type evpn \\",
|
||||
f" --controller {devis['controleur']} --vrf-vxlan {b['vrf_vxlan']} \\",
|
||||
f" --mtu {b['mtu']} --ipam pve \\",
|
||||
f" --exitnodes {','.join(sortie) if sortie else MARQUEUR_SORTIE}",
|
||||
]
|
||||
if not sortie:
|
||||
out += [
|
||||
"# /!\\ AUCUN noeud de sortie declare chez l'hebergeur.",
|
||||
"# Un VRF sans sortie n'a AUCUN chemin vers l'exterieur : ni frontiere,",
|
||||
"# ni inter-tenant, ni Internet. Le trafic intra-tenant fonctionnerait,",
|
||||
"# le reste echouerait sans message clair. Declarer",
|
||||
"# `proxmox_sdn.noeuds_de_sortie` dans proxmox-hebergeur.yml.",
|
||||
]
|
||||
out += ["", "### 2. Les VNets (une par zone de securite)"]
|
||||
for v in b["vnets"]:
|
||||
out.append(f"pvesh create /cluster/sdn/vnets --vnet {v['vnet']} "
|
||||
f"--zone {b['zone']} --tag {v['tag']} # {v['libelle']}")
|
||||
out += ["", "### 3. Les sous-reseaux (passerelle anycast sur chaque hyperviseur)"]
|
||||
for v in b["vnets"]:
|
||||
out.append(f"pvesh create /cluster/sdn/vnets/{v['vnet']}/subnets "
|
||||
f"--subnet {v['sous_reseau']} --type subnet "
|
||||
f"--gateway {v['passerelle']} --snat 0")
|
||||
out += ["", "# VNet tag/VNI sous-reseau passerelle zone"]
|
||||
for v in b["vnets"]:
|
||||
out.append(f"# {v['vnet']:<8} {v['tag']:<8} {v['sous_reseau']:<18} "
|
||||
f"{v['passerelle']:<17} {v['libelle']}")
|
||||
out.append("")
|
||||
out += [
|
||||
"## 4. Appliquer",
|
||||
"# Les objets ci-dessus ne sont que DECLARES tant que la configuration SDN n'est",
|
||||
"# pas poussee sur les noeuds. Rien ne change avant cette ligne.",
|
||||
"pvesh set /cluster/sdn",
|
||||
"",
|
||||
"## Ce que ce devis ne fait pas",
|
||||
"# Il ne branche aucune VM : le `bridge` d'une VM se regle au clonage, et vient du",
|
||||
"# plan (proxmox_clone_pont). Il ne lit pas non plus le cluster — il dit l'etat",
|
||||
"# cible, pas l'ecart. Les commandes echouent si l'objet existe deja, ce qui est",
|
||||
"# le comportement voulu pour un devis qu'on relit avant d'appliquer.",
|
||||
]
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument("--json", action="store_true")
|
||||
ap.add_argument("--verifier", action="store_true")
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
devis = construire(decouvrir())
|
||||
if args.verifier:
|
||||
zones = devis["zones"]
|
||||
if not zones:
|
||||
print("erreur: aucun tenant federe decouvert.", file=sys.stderr)
|
||||
return 2
|
||||
fautes: list[str] = []
|
||||
vus_zone: dict[str, str] = {}
|
||||
vus_vnet: dict[str, str] = {}
|
||||
vus_reseau: dict[str, str] = {}
|
||||
for b in zones:
|
||||
if len(b["zone"]) > LONGUEUR_MAX_ID:
|
||||
fautes.append(f"zone {b['zone']} : {len(b['zone'])} caracteres "
|
||||
f"(max {LONGUEUR_MAX_ID})")
|
||||
if b["zone"] in vus_zone:
|
||||
fautes.append(f"zone {b['zone']} revendiquee par {vus_zone[b['zone']]} "
|
||||
f"et {b['tenant']}")
|
||||
vus_zone[b["zone"]] = b["tenant"]
|
||||
for v in b["vnets"]:
|
||||
# La limite de 8 caracteres est la faute la plus couteuse : Proxmox
|
||||
# refuse l'objet, et un devis invalide se decouvre a l'application.
|
||||
if len(v["vnet"]) > LONGUEUR_MAX_ID:
|
||||
fautes.append(f"VNet {v['vnet']} : {len(v['vnet'])} caracteres "
|
||||
f"(max {LONGUEUR_MAX_ID})")
|
||||
if v["vnet"] in vus_vnet:
|
||||
fautes.append(f"VNet {v['vnet']} revendique par {vus_vnet[v['vnet']]} "
|
||||
f"et {b['tenant']}")
|
||||
vus_vnet[v["vnet"]] = b["tenant"]
|
||||
if v["sous_reseau"] in vus_reseau:
|
||||
fautes.append(f"sous-reseau {v['sous_reseau']} revendique par "
|
||||
f"{vus_reseau[v['sous_reseau']]} et {b['tenant']}")
|
||||
vus_reseau[v["sous_reseau"]] = b["tenant"]
|
||||
if fautes:
|
||||
print("erreur: " + " ; ".join(fautes[:6]), file=sys.stderr)
|
||||
return 2
|
||||
total_v = sum(len(b["vnets"]) for b in zones)
|
||||
manque = "" if devis["noeuds_de_sortie"] else \
|
||||
" ; /!\\ aucun noeud de sortie declare (VRF sans chemin vers l'exterieur)"
|
||||
print(f"CONFORME : SDN EVPN, {len(zones)} zone(s), {total_v} VNet(s), "
|
||||
f"{total_v} sous-reseau(x), aucune collision{manque}.")
|
||||
return 0
|
||||
print(json.dumps(devis, indent=2, ensure_ascii=False) if args.json else rendre(devis))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main(sys.argv[1:]))
|
||||
|
|
@ -470,6 +470,8 @@ PREUVES: list[dict] = [
|
|||
"cmds": [[sys.executable, "scripts/devis_proxmox_pools.py", "--verifier"]]},
|
||||
{"id": "P29", "titre": "Authentification : chaque role declare sa position", "refs": ["AFF-111"],
|
||||
"func": preuve_authentification},
|
||||
{"id": "P30", "titre": "SDN EVPN : zones, VNets et sous-reseaux derives", "refs": ["AFF-112"],
|
||||
"cmds": [[sys.executable, "scripts/devis_sdn.py", "--verifier"]]},
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ Usage :
|
|||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import getpass
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
|
@ -154,6 +155,84 @@ def cles_voute_reelle() -> set[str] | None:
|
|||
return None
|
||||
|
||||
|
||||
def _voute_reelle() -> Path | None:
|
||||
for nom in ("principal", "production", "lab"):
|
||||
p = INSTANCE / "inventories" / nom / "group_vars" / "all" / "vault.yml"
|
||||
if p.is_file():
|
||||
return p
|
||||
return None
|
||||
|
||||
|
||||
def saisir(noms: list[str], remplacer: bool = False) -> int:
|
||||
"""Ecrit dans la voute des secrets EMIS PAR UN TIERS, saisis sans echo.
|
||||
|
||||
Pendant de la generation, et sa contrepartie exacte : on GENERE un secret dont
|
||||
le depot est la source (Ansible configure les deux cotes depuis la meme
|
||||
variable) ; on SAISIT un secret dont un tiers est la source. Inventer une cle
|
||||
d'API OPNsense donnerait une valeur syntaxiquement correcte et refusee a la
|
||||
premiere requete — pire qu'une absence, puisque la garde passerait au vert.
|
||||
|
||||
Rien n'est affiche, rien n'est ecrit en clair sur disque, rien ne passe par la
|
||||
ligne de commande (donc ni par l'historique du shell, ni par la liste des
|
||||
processus). Le resultat est reparse et redechiffre avant d'etre pose.
|
||||
"""
|
||||
from ansible.parsing.vault import VaultLib, VaultSecret
|
||||
|
||||
cible = _voute_reelle()
|
||||
if cible is None:
|
||||
print("erreur: aucune voute group_vars/all/vault.yml dans cette instance.",
|
||||
file=sys.stderr)
|
||||
return 2
|
||||
fichier_mdp = os.environ.get("ANSIBLE_VAULT_PASSWORD_FILE")
|
||||
if not fichier_mdp or not Path(os.path.expanduser(fichier_mdp)).is_file():
|
||||
print("erreur: ANSIBLE_VAULT_PASSWORD_FILE absent ou introuvable.", file=sys.stderr)
|
||||
return 2
|
||||
mdp = Path(os.path.expanduser(fichier_mdp)).read_bytes().strip()
|
||||
vault = VaultLib([("default", VaultSecret(mdp))])
|
||||
|
||||
brut = cible.read_bytes()
|
||||
texte = (vault.decrypt(brut) if brut.startswith(b"$ANSIBLE_VAULT") else brut).decode("utf-8")
|
||||
data = yaml.safe_load(texte) or {}
|
||||
|
||||
a_poser: dict[str, str] = {}
|
||||
for nom in noms:
|
||||
if str(data.get(nom, "")).strip() and not remplacer:
|
||||
print(f" {nom} : deja renseigne, ignore (--remplacer pour ecraser).")
|
||||
continue
|
||||
while True:
|
||||
v = getpass.getpass(f" {nom} : ")
|
||||
if not v.strip():
|
||||
print(" valeur vide refusee.")
|
||||
continue
|
||||
if getpass.getpass(f" {nom} (confirmer) : ") != v:
|
||||
print(" les deux saisies different, on recommence.")
|
||||
continue
|
||||
break
|
||||
a_poser[nom] = v
|
||||
if not a_poser:
|
||||
print("Rien a ecrire.")
|
||||
return 0
|
||||
|
||||
nouveau = (texte.rstrip("\n") + "\n\n"
|
||||
+ "# --- Saisis a la main (secrets emis par un tiers) ---\n"
|
||||
+ yaml.safe_dump(a_poser, default_flow_style=False,
|
||||
allow_unicode=True, sort_keys=True))
|
||||
attendu = {**data, **a_poser}
|
||||
if yaml.safe_load(nouveau) != attendu:
|
||||
print("erreur: le YAML fusionne ne reproduit pas l'union attendue.", file=sys.stderr)
|
||||
return 2
|
||||
chiffre = vault.encrypt(nouveau.encode("utf-8"))
|
||||
if not chiffre.endswith(b"\n"):
|
||||
chiffre += b"\n"
|
||||
if yaml.safe_load(vault.decrypt(chiffre).decode()) != attendu:
|
||||
print("erreur: aller-retour de chiffrement infidele, rien n'a ete ecrit.", file=sys.stderr)
|
||||
return 2
|
||||
cible.write_bytes(chiffre)
|
||||
print(f"Ecrit et verifie : {len(a_poser)} secret(s) -> {cible} "
|
||||
f"({len(attendu)} cle(s) au total).")
|
||||
return 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Couverture du gabarit de voute Set-OPS.")
|
||||
sub = parser.add_subparsers(dest="commande", required=True)
|
||||
|
|
@ -161,8 +240,15 @@ def main() -> int:
|
|||
pv = sub.add_parser("verifier", help="Le gabarit couvre-t-il les secrets exiges ?")
|
||||
pv.add_argument("--strict", action="store_true",
|
||||
help="Echouer aussi si le gabarit porte des cles inutiles.")
|
||||
ps = sub.add_parser("saisir", help="Saisir sans echo des secrets emis par un tiers.")
|
||||
ps.add_argument("noms", nargs="+", help="Noms des cles a saisir (ex. vault_opnsense_api_key).")
|
||||
ps.add_argument("--remplacer", action="store_true",
|
||||
help="Ecraser une cle deja renseignee.")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.commande == "saisir":
|
||||
return saisir(args.noms, args.remplacer)
|
||||
|
||||
g = gabarit()
|
||||
if not g:
|
||||
print("Aucun gabarit vault.yml.example : rien a verifier.")
|
||||
|
|
|
|||
Loading…
Reference in a new issue