Deuxième mode de réplication, séparé de la réplication cluster vers cluster
(sélecteur « Clusters (API) | Nœuds (fichiers) » en haut de page) : un profil
réplique la configuration système d'un nœud maître vers des nœuds cibles.
- réseau par /nodes/{node}/network (interfaces.new puis rechargement), adresses
et passerelles propres à chaque nœud, mapping des noms d'interfaces par cible ;
DNS par /nodes/{node}/dns ; multipath.conf, wwids (ajout seul), chrony,
sysctl.d, interfaces.d (hors sdn) par SSH
- découverte des nœuds (/cluster/status), accès SSH par nœud (mot de passe du
cluster ou propre), clé d'hôte épinglée, contrôle du nom du nœud
- dry-run avec diff des fichiers ; apply (admin) avec contrôle SHA-256,
sauvegarde et restore.sh sur le nœud, écriture atomique, rechargements,
restauration si un rechargement échoue, timer systemd de restauration si le
nœud est injoignable après le rechargement du réseau, confirmation tapée
- vue Nœuds et profils, journal séparé par mode, démo avec nœuds simulés
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
217 lines
14 KiB
Markdown
217 lines
14 KiB
Markdown
# pve-confsync
|
|
|
|
Réplication d'une partie de la configuration d'un cluster Proxmox VE 9.x **primaire**
|
|
vers une ou plusieurs **cibles**. Sens unique primaire → cibles. Pas de VM, pas de disques.
|
|
Tout se fait depuis l'interface web : plus de CLI ni de fichier YAML.
|
|
|
|
**État : phases 0 à 2, version « full web ».** Ajout des clusters (un par un ou en lot),
|
|
choix du primaire, éléments répliqués et cibles choisis dans des menus, écarts par cible,
|
|
dry-run et réplication par objet, par cible ou global, journal, audit.
|
|
Testé contre un faux serveur PVE (API + SSH) ; **pas encore testé sur un vrai cluster.**
|
|
|
|
## Périmètre
|
|
|
|
| Type (`kind`) | Accès | Politique par défaut |
|
|
|---|---|---|
|
|
| `datacenter-options` | API `/cluster/options` | `tag-style`, `registered-tags`, `user-tag-access`, `next-id` en `sync`, le reste ignoré ; `webauthn`, `u2f`, `http_proxy` jamais répliqués |
|
|
| `pve-group` | API `/access/groups` | `sync` |
|
|
| `pve-role` | API `/access/roles` | `sync` (rôles intégrés ignorés) |
|
|
| `pve-user` | API `/access/users` | `sync`, comptes du royaume `pve` uniquement |
|
|
| `pve-password` | SSH `/etc/pve/priv/shadow.cfg` | `sync` (haché recopié tel quel, jamais affiché) |
|
|
| `sdn-zone` (vlan, vxlan) | API `/cluster/sdn/zones` | `nodes`, `bridge`, `peers`, `fabric`, `dns`, `reversedns`, `dnszone` en `override` (mapping de la cible) ; le reste en `sync` |
|
|
| `sdn-vnet` | API `/cluster/sdn/vnets` | `sync` |
|
|
| `sdn-subnet` | API `/cluster/sdn/vnets/{vnet}/subnets` | `sync` |
|
|
|
|
Les comptes du realm pve et leurs mots de passe forment un seul élément à répliquer ;
|
|
ils exigent leurs groupes, que le menu « Éléments répliqués » coche avec eux. Un parent absent de la cible et non répliqué
|
|
est une erreur bloquante.
|
|
|
|
Hors périmètre : EVPN (et les zones simple, QinQ, faucet, avec leurs VNets, ignorées
|
|
avec un avertissement), firewall, storage, pools, ACL, jobs, comptes `@pam` et autres royaumes.
|
|
|
|
Détail des routes, schémas et accès, avec les sources : [docs/api-pve.md](docs/api-pve.md).
|
|
|
|
## Réplication de nœuds
|
|
|
|
Deuxième mode, séparé du premier dans l'interface (sélecteur « Clusters (API) | Nœuds
|
|
(fichiers) » en haut de page) : un **profil** réplique la configuration système d'un
|
|
**nœud maître** vers des **nœuds cibles**, du même cluster ou d'un autre.
|
|
|
|
| Élément | Accès | Rechargement | Particularités |
|
|
|---|---|---|---|
|
|
| `network` | API `/nodes/{node}/network` (écrit `interfaces.new`) | `PUT /nodes/{node}/network` (ifreload -a) | `cidr`, `cidr6`, `gateway`, `gateway6` en `override` : chaque nœud garde ses adresses ; mapping des noms d'interfaces par cible (eno1=enp65s0f0, VLAN suivis) ; interface physique absente = bloquant ; options que l'API n'écrit pas (bond-miimon, post-up…) signalées, non répliquées |
|
|
| `dns` | API `/nodes/{node}/dns` | aucun | domaine de recherche obligatoire pour l'API |
|
|
| `multipath` | SSH `/etc/multipath.conf` | `multipathd reconfigure` | |
|
|
| `wwids` | SSH `/etc/multipath/wwids` | `multipathd reconfigure` | ajout seul des WWID du maître |
|
|
| `chrony` | SSH `/etc/chrony/chrony.conf`, `sources.d/*.sources` | `systemctl restart chrony` | |
|
|
| `sysctl` | SSH `/etc/sysctl.d/*.conf` | `sysctl --system` | |
|
|
| `interfaces-d` | SSH `/etc/network/interfaces.d/*` | `ifreload -a` | `interfaces.d/sdn` (généré par le SDN) jamais touché |
|
|
|
|
Jamais répliqués : `/etc/pve`, corosync, hostname, machine-id, clés SSH, IQN iSCSI, `/etc/frr`.
|
|
|
|
- **Accès** : « Découvrir les nœuds » lit `/cluster/status` de chaque cluster (adresse de
|
|
chaque nœud). SSH en root sur chaque nœud, avec le mot de passe de son cluster ou le sien ;
|
|
clé d'hôte épinglée au premier test, qui vérifie aussi que le nœud répond bien sous son nom.
|
|
- **Règles** : politiques par champ (réseau, DNS), lignes protégées par expression régulière
|
|
pour les fichiers (gardées de la cible), valeurs propres à une cible (`vmbr1 cidr=…`).
|
|
- **Apply** (administrateurs), un nœud après l'autre, arrêt au premier échec : un script SSH
|
|
vérifie que chaque fichier est celui du dry-run (SHA-256), sauvegarde dans
|
|
`/var/lib/pve-confsync/backup/<date>/` avec un `restore.sh`, écrit de façon atomique
|
|
(fichier temporaire puis `mv`, droits et propriétaire gardés) ; l'API écrit les interfaces
|
|
et le DNS ; les rechargements suivent. Un rechargement en échec restaure les sauvegardes.
|
|
- **Filet réseau** : avant de recharger le réseau, un timer systemd est armé sur le nœud
|
|
(`pve-confsync-restore-<date>`) : sans nouvelle connexion SSH et API dans les 120 s, le nœud
|
|
relance `restore.sh` et `ifreload -a` de lui-même. Confirmation tapée (nom du nœud, ou du
|
|
profil en global) dès qu'un plan recharge le réseau. Refus si la cible a déjà des
|
|
changements réseau en attente (`interfaces.new`).
|
|
|
|
## Fonctionnement
|
|
|
|
```
|
|
lecture source -> normalisation -> diff -> plan -> apply -> vérification
|
|
```
|
|
|
|
- **Lecture** : le primaire est lu en configuration SDN *appliquée* (`running=1`) ; les
|
|
cibles en configuration courante. Chaque cluster est refusé si un nœud en ligne n'est
|
|
pas en PVE 9.x (`/nodes/{node}/version`).
|
|
- **Normalisation** : objets JSON génériques → champs canoniques (chaînes, listes triées,
|
|
property strings à clés triées). Les champs calculés par PVE sont retirés.
|
|
- **Politiques** : `sync` (copie du primaire), `ignore`, `override` (valeur fournie
|
|
uniquement par le `mapping` de la cible). Le mapping d'une cible est prioritaire sur le
|
|
primaire. Résolution : politique de la cible → politique globale → défaut de l'adaptateur.
|
|
- **Plan** : création / mise à jour (champs modifiés, champs à supprimer) ; suppression
|
|
d'objets uniquement avec `prune` (option explicite), dépendants d'abord. Erreurs bloquantes :
|
|
champ obligatoire absent à la création (ex. `bridge` d'une zone VLAN sans mapping),
|
|
champ immuable modifié (type de zone, vnet d'un subnet).
|
|
- **Apply SDN** : `POST /cluster/sdn/lock` → écritures avec `lock-token` → `PUT /cluster/sdn`
|
|
(commit + rechargement réseau, suivi de la tâche). Toute erreur avant le commit déclenche
|
|
`POST /cluster/sdn/rollback`. Le verrou est refusé si la cible a déjà des changements SDN
|
|
en attente : on ne mélange pas nos modifications avec celles de quelqu'un d'autre.
|
|
- **Apply comptes** : API pour groupes, rôles et comptes ; mots de passe par SSH (script
|
|
perl sous `lock_shadow_config`, comme `pveum passwd`).
|
|
- **Vérification** : relecture de la cible, nouveau plan qui doit être vide, et aucune zone en
|
|
`error` sur les nœuds (`/nodes/{node}/sdn/zones`).
|
|
|
|
## Démarrage
|
|
|
|
```sh
|
|
go run ./cmd/pve-confsync -demo # démo : faux clusters PVE, http://localhost:8080
|
|
docker compose -f docker-compose.demo.yml up -d --build # même démo en conteneur
|
|
docker compose up -d # production : données dans le volume /data
|
|
```
|
|
|
|
La clé AES-256 des mots de passe en base est créée au premier démarrage dans
|
|
`/data/secret.key` (à sauvegarder : sans elle, les mots de passe enregistrés sont perdus).
|
|
En production, on peut la garder hors du volume en secret docker (`CONFSYNC_SECRET_KEY_FILE`,
|
|
voir les commentaires de `docker-compose.yml`). Port publié : `CONFSYNC_PORT` (`.env`, 8080 par défaut).
|
|
|
|
Le mode démo démarre un primaire PVE 9.1, deux cibles avec des écarts (dont un mapping
|
|
manquant, bloquant) et un cluster PVE 8 refusé. Comptes `admin`, `operateur` et `lecteur`,
|
|
mot de passe `confsync-demo`.
|
|
|
|
**Base vide** : la page de connexion propose de créer le premier administrateur
|
|
(12 caractères minimum). Tant que ce n'est pas fait, **le premier qui atteint l'instance
|
|
devient administrateur** : ne pas l'exposer avant. Alternative : `CONFSYNC_ADMIN_USER` et
|
|
`CONFSYNC_ADMIN_PASSWORD` / `_FILE` créent ce compte au démarrage.
|
|
|
|
## Interface web
|
|
|
|
- **Clusters** : ajout d'un cluster (URL `https://…:8006`, mot de passe root) ou d'une
|
|
liste (« nom URL » par ligne, même mot de passe), test (API, version 9.x, SSH), rôle
|
|
primaire / cible / désactivé. Un seul primaire, jamais écrit.
|
|
- **Éléments répliqués** et **Cibles** : menus de la vue Écarts ; les cibles décochées
|
|
sont exclues du global (choix mémorisé dans le navigateur).
|
|
- **Écarts** : une carte par cluster (version, compteurs à créer / modifier / erreurs /
|
|
hors primaire), puis les objets de la cible choisie ; le détail d'un objet montre pour
|
|
chaque champ la politique, la valeur du primaire, le mapping, la valeur actuelle et
|
|
la valeur après réplication. Les hachés sont masqués (`<masqué xxxxxx>`).
|
|
- **Dry-run / Répliquer** par objet (avec ses parents à créer), par cible ou global.
|
|
Dans le dry-run, chaque élément peut être exclu de l'exécution (« Ne pas répliquer ») ;
|
|
le dry-run est alors relancé sans lui, sauf ce dont les autres ont besoin.
|
|
La réplication passe toujours par un dry-run : le serveur recalcule le plan et refuse
|
|
s'il a changé depuis (empreinte du plan). Avec des changements SDN, il faut taper le
|
|
nom de la cible (celui du primaire en global). Une seule réplication à la fois.
|
|
- **Journal** des dry-runs et réplications et **Audit** (connexions, actions,
|
|
modifications ; admin).
|
|
- **Paramètres** : sélection, politiques, mapping par cible, suppression (désactivée par
|
|
défaut), utilisateurs locaux, compte, apparence.
|
|
|
|
Rôles : `admin` (tout), `operator` (dry-run et réplication), `viewer` (lecture).
|
|
Les comptes locaux restent disponibles même avec OIDC. Toutes les variables sont reprises,
|
|
commentées, dans [`.env.example`](.env.example) (`cp .env.example .env`).
|
|
|
|
| Variable | Défaut | Rôle |
|
|
|---|---|---|
|
|
| `LISTEN` | `:8080` | adresse d'écoute |
|
|
| `DATA_DIR` | `/data` | base SQLite, `secret.key`, `session.key` |
|
|
| `CONFSYNC_SECRET_KEY` / `_FILE` | `DATA_DIR/secret.key` (créée) | clé AES-256 (hex ou base64) des mots de passe en base |
|
|
| `CONFSYNC_ADMIN_USER`, `CONFSYNC_ADMIN_PASSWORD` / `_FILE` | `admin`, vide | premier administrateur sans passer par la page web |
|
|
| `AUTH_LOCAL` | `true` | comptes locaux |
|
|
| `OIDC_ISSUER`, `OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET` / `_FILE`, `OIDC_REDIRECT_URL` | | OIDC (code + PKCE) ; `OIDC_REDIRECT_URL` complète, par ex. `https://confsync.example.org/auth/callback` |
|
|
| `OIDC_SCOPES`, `OIDC_GROUPS_CLAIM` | `openid profile email`, `groups` | ajouter le scope qui fournit les groupes chez l'IdP |
|
|
| `OIDC_ADMIN_GROUP`, `OIDC_OPERATOR_GROUP`, `OIDC_DEFAULT_ROLE` | `viewer` | rôle d'après les groupes |
|
|
| `SESSION_TTL` | `12h` | durée des sessions |
|
|
| `CONFSYNC_DEMO` | `false` | mode démo |
|
|
|
|
## Sécurité
|
|
|
|
- **Accès PVE en `root@pam`** (API par ticket, SSH avec le même mot de passe) : nécessaire
|
|
pour les mots de passe du royaume `pve`, absents de l'API. Le compte root ne doit pas
|
|
avoir de TFA (refusé). Le primaire n'est jamais écrit.
|
|
- **Épinglage au premier test** : empreinte SHA-256 du certificat TLS (mode par défaut,
|
|
adapté aux certificats auto-signés PVE) et clé d'hôte SSH. Une clé SSH différente
|
|
ensuite bloque l'accès ; l'accepter demande une action explicite (« faire confiance à la
|
|
nouvelle clé »). Changer l'URL ou le nœud SSH oublie les empreintes. Autres modes TLS :
|
|
CA (PEM), CA système, `insecure` (labo).
|
|
- **Mots de passe root** chiffrés en base (AES-256-GCM, clé hors base), jamais renvoyés au
|
|
navigateur ni journalisés. Les hachés du shadow sont masqués dans l'UI, le journal et
|
|
les réponses JSON.
|
|
- Interface : mots de passe PBKDF2-SHA256 (600 000 itérations), cookie de session signé
|
|
(HttpOnly, SameSite=Lax, Secure derrière HTTPS) invalidé au changement de mot de passe,
|
|
en-tête `X-Confsync` exigé sur les écritures (CSRF), CSP sans script ni style en ligne.
|
|
|
|
## Arborescence
|
|
|
|
```
|
|
cmd/pve-confsync/ serveur web (seul point d'entrée)
|
|
internal/service/ analyse pour l'UI : lecture parallèle, plans, lignes de diff par champ
|
|
internal/store/ SQLite : clusters (mots de passe chiffrés, rôles, empreintes), utilisateurs, journal, audit
|
|
internal/server/ HTTP : auth locale + OIDC, rôles, API, jobs de synchronisation
|
|
internal/demo/ faux clusters du mode démo
|
|
web/ UI embarquée (index.html, app.js, style.css, login.html)
|
|
internal/pve/ client API : ticket root@pam, TLS épinglé, form-encoding, version, tâches
|
|
internal/sshx/ SSH root, clé d'hôte épinglée
|
|
internal/pve/pvetest/ faux serveur PVE 9.x (API + SSH) pour les tests
|
|
internal/model/ objets génériques, valeurs canoniques
|
|
internal/adapter/ un adaptateur par type : routes, champs calculés, locaux, immuables
|
|
internal/config/ configuration en mémoire : clusters, politiques, mappings, sélection
|
|
internal/engine/ lecture, plan, apply (verrou SDN, rollback), vérification
|
|
internal/nodesync/ réplication de nœuds : réseau et DNS (API), fichiers (SSH), diff, sauvegarde, restauration
|
|
docs/api-pve.md routes et schémas vérifiés dans les sources PVE
|
|
```
|
|
|
|
Tests : `go test ./...` (bout en bout contre le faux serveur : réplication SDN et comptes
|
|
avec mots de passe par SSH, mapping manquant, parent absent, rollback sur erreur, refus de
|
|
PVE 8, prune et ordre de suppression ; sélection et dépendances ; chiffrement, migration
|
|
de schéma, premier administrateur ; serveur depuis une base vide : création de l'admin,
|
|
ajout des clusters, épinglage, primaire, réplication globale, primaire jamais écrit).
|
|
|
|
## Phasage
|
|
|
|
0. Moteur export / diff / apply (options datacenter, tags, SDN) ; la CLI a été retirée.
|
|
1. UI lecture seule : clusters, diff visuel par cible. Ergonomie reprise de Logstream
|
|
(`web/index.html`, `style.css`, `app.js`, `login.html`, auth locale + OIDC, audit) ;
|
|
SQLite (clusters, profils, mappings, historique, audit), secrets chiffrés.
|
|
2. UI d'action : Dry-run / Répliquer (par objet, par cible, global), journal ; gestion
|
|
des clusters en web, primaire / cibles, comptes et mots de passe du royaume pve. ← ici
|
|
3. Planification, détection de dérive en lecture seule, notifications.
|
|
|
|
## Reste à faire
|
|
|
|
- Valider sur un vrai cluster : écriture du shadow par SSH, forme de `/access/users?full=1`, forme réelle des réponses (`nodes`, `dhcp-range`,
|
|
booléens), comportement de `PUT /cluster/options` avec `delete`.
|
|
- Exploiter `GET /cluster/sdn/dry-run` (diff frr / interfaces, PVE ≥ 9.2 environ) pour
|
|
enrichir le dry-run SDN.
|
|
- Rechargement en échec après commit : aujourd'hui signalé, pas d'annulation automatique
|
|
(la configuration appliquée est déjà la nouvelle).
|