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>
14 KiB
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.
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/statusde 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 unrestore.sh, écrit de façon atomique (fichier temporaire puismv, 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 relancerestore.shetifreload -ade 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 lemappingde 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.bridged'une zone VLAN sans mapping), champ immuable modifié (type de zone, vnet d'un subnet). - Apply SDN :
POST /cluster/sdn/lock→ écritures aveclock-token→PUT /cluster/sdn(commit + rechargement réseau, suivi de la tâche). Toute erreur avant le commit déclenchePOST /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, commepveum passwd). - Vérification : relecture de la cible, nouveau plan qui doit être vide, et aucune zone en
errorsur les nœuds (/nodes/{node}/sdn/zones).
Démarrage
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 (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 royaumepve, 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-Confsyncexigé 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
- Moteur export / diff / apply (options datacenter, tags, SDN) ; la CLI a été retirée.
- 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. - 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
- 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 dePUT /cluster/optionsavecdelete. - 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).