Réplication de nœuds : réseau et DNS par l'API, multipath, chrony, sysctl par SSH

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>
This commit is contained in:
cedricandClaude Opus 5.5 committed 2026-10-23 20:29:55 +02:00
1 parent dc1a7ff40e
commit 1e667ce8b9
24 files changed
+4973 -65

No files matched your search

+31
View File
@@ -104,6 +104,37 @@ Champs (schémas : [VlanPlugin.pm](https://github.com/proxmox/pve-network/blob/c
**À valider sur un vrai cluster** : script d'écriture du shadow, forme réelle de
`/access/users?full=1`, mise à jour d'un utilisateur avec champs vidés.
## Réplication de nœuds (vérifié le 2026-10-23, pve-manager f643ce4b, pve-common master)
| Méthode | Chemin | Usage |
|---|---|---|
| GET | `/nodes/{node}/network` ([Network.pm](https://github.com/proxmox/pve-manager/blob/f643ce4bceb99e8b23094cd04075405281eb3f27/PVE/API2/Network.pm)) | interfaces, lues dans `interfaces.new` s'il existe ; `lo` retiré |
| POST | `/nodes/{node}/network` | création (`iface`, `type` + champs) dans `interfaces.new` |
| PUT | `/nodes/{node}/network/{iface}` | mise à jour (`type` obligatoire, `delete=`) |
| DELETE | `/nodes/{node}/network/{iface}` | suppression (prune) |
| PUT | `/nodes/{node}/network` | renomme `interfaces.new` puis `ifreload -a` dans une tâche (UPID) |
| DELETE | `/nodes/{node}/network` | annule les changements en attente (supprime `interfaces.new`) |
| GET / PUT | `/nodes/{node}/dns` ([Nodes.pm](https://github.com/proxmox/pve-manager/blob/f643ce4bceb99e8b23094cd04075405281eb3f27/PVE/API2/Nodes.pm)) | `search` (obligatoire au PUT), `dns1` à `dns3` ; le PUT réécrit tous les serveurs |
Points d'attention :
- champs écrits (`$confdesc`) : `type`, `comments`, `comments6`, `autostart`, `bridge_vlan_aware`,
`bridge_vids`, `bridge_ports`, `ovs_*`, `slaves`, `bond_mode`, `bond-primary`,
`bond_xmit_hash_policy`, `vlan-raw-device`, `vlan-id`, `gateway`, `gateway6`, `cidr`, `cidr6`, `mtu`.
Le GET renvoie aussi `address`/`netmask` (forme canonique : `cidr`), `bridge_stp`,
`bridge_fd`, `bond_miimon` (écrits par défaut par PVE), `options` (lignes inconnues de
l'API, jamais écrites), `active`, `exists`, `families`, `method`, `priority` : non répliqués.
- le PUT d'une interface recalcule `method` et `families` à partir des paramètres reçus :
sans `address`/`cidr`, une interface statique devient `manual`. pve-confsync envoie donc
toute la configuration de l'interface à chaque mise à jour.
- `interfaces.d/*` n'est pas lu par l'API ([Interfaces.pm](https://github.com/proxmox/pve-common/blob/master/src/PVE/Network/Interfaces.pm)) : répliqué en fichier par SSH.
Scripts SSH (première ligne `# confsync:<étape>`) : lecture (`stat`, `base64 -w0`),
préparation (contrôle SHA-256, `cp -p` vers la sauvegarde, `restore.sh`, écriture par
`mktemp` + `mv`), restauration, `systemd-run --on-active=120s` / `systemctl stop <unité>.timer`,
rechargements. Le script de préparation est testé tel quel avec le shell local
(`internal/nodesync/script_test.go`) ; **le reste est à valider sur les clusters de labo.**
## Droits
Accès en `root@pam` (API + SSH, même mot de passe), sans TFA. La référence (primaire)
+117
View File
@@ -0,0 +1,117 @@
# Proposition : réplication de fichiers système nœud à nœud (SSH)
Statut : validée par Cédric le 2026-10-23 (liste v1, API dès que possible, identifiants
SSH hérités du cluster avec surcharge par nœud), puis implémentée. Écart avec la
proposition : `/etc/network/interfaces` passe par l'API PVE (`/nodes/{node}/network`) et
non par fichier, et le DNS est ajouté par l'API (`/nodes/{node}/dns`). Le fonctionnement
réel est décrit dans le README (« Réplication de nœuds »).
Deuxième mode de réplication, séparé de la réplication cluster à cluster (API) :
un **nœud maître** et un ou plusieurs **nœuds cibles** (même cluster ou non),
fichiers lus et écrits en SSH root. Même moteur : lecture, normalisation, diff,
plan, apply, vérification.
Constat dans le code actuel : l'accès SSH est défini **par cluster, sur un seul
hôte** (`SSHHost`, `internal/config/config.go:33`). Il faut le passer **par
nœud** : nœuds découverts par l'API (`GET /cluster/status`, champ `ip`), identifiants
root hérités du cluster et surchargeables par nœud, clé d'hôte épinglée par nœud
(même TOFU que maintenant).
## 1. Fichiers candidats
Légende : 🟢 v1 recommandée, 🟡 v1 possible avec garde-fous, 🔴 exclu.
| Fichier | Intérêt | Risques | Propre au nœud (mapper / exclure) | Rechargement |
|---|---|---|---|---|
| 🟢 `/etc/network/interfaces` | bonds, VLAN, bridges, MTU identiques partout | **perte d'accès au nœud** ; noms de NIC différents d'un nœud à l'autre | `address`, `gateway`, `hwaddress`, IP de management, éventuellement noms de NIC (`nic1` maître ↔ `enp65s0f0` cible) | `ifreload -a` |
| 🟢 `/etc/network/interfaces.d/*` | idem | **`interfaces.d/sdn` est généré par le SDN PVE** | exclure `sdn` d'office | `ifreload -a` |
| 🟢 `/etc/multipath.conf` | `defaults`, `blacklist`, `devices` identiques sur tout le cluster | erreur de syntaxe = chemins perdus | rien en général | `multipathd reconfigure` |
| 🟢 `/etc/multipath/wwids` | mêmes LUN vues par tous les nœuds | ajouter un WWID local (disque système) par erreur | mode additif seulement, jamais de suppression | `multipathd reconfigure` |
| 🟡 `/etc/multipath/bindings` | alias `mpathX` identiques sur tous les nœuds (important si LVM partagé référence les noms) | renommer un alias en service | additif seulement, conflit d'alias = bloquant | `multipathd reconfigure` |
| 🟢 `/etc/chrony/chrony.conf` (+ `sources.d`) | mêmes sources NTP | faible | rien | `systemctl restart chrony` |
| 🟡 `/etc/systemd/timesyncd.conf` | si chrony absent | faible | rien | `systemctl restart systemd-timesyncd` |
| 🟢 `/etc/sysctl.d/*.conf` | tuning réseau / mémoire homogène | réglage inadapté au matériel | rien ; ne jamais toucher les fichiers des paquets (`/usr/lib/sysctl.d`) | `sysctl --system` |
| 🟡 `/etc/modprobe.d/*.conf` | options ZFS (`zfs_arc_max`), blacklist de modules | effet au reboot seulement ; ARC dépend de la RAM | `zfs_arc_max` à surcharger par nœud | `update-initramfs -u`, **redémarrage requis** (signalé, jamais fait) |
| 🟡 `/etc/apt/sources.list.d/*.sources` | mêmes dépôts (enterprise / no-subscription / ceph) | dépôt enterprise sans abonnement sur la cible | rien | aucun (pas d'`apt update` automatique) |
| 🟡 `/etc/lvm/lvm.conf` | `global_filter` pour iSCSI / multipath | **filtre faux = volumes invisibles** | rien en général | aucun ; vérification `pvs` avant / après |
| 🟡 `/etc/iscsi/iscsid.conf` | timeouts, `node.startup` | **contient les secrets CHAP** | secrets chiffrés au repos, jamais affichés en clair dans le diff | `systemctl restart iscsid` (signalé) |
| 🟡 `/etc/vzdump.conf` | défauts de sauvegarde par nœud | faible | `tmpdir` éventuellement | aucun |
| 🟡 `/etc/postfix/main.cf` | relais SMTP des notifications | faible | `myhostname` | `postfix reload` |
| 🔵 `/etc/resolv.conf` | DNS identiques | écrasé par DHCP / resolvconf | rien | aucun. **Existe aussi en API** (`/nodes/{node}/dns`), à préférer : à vérifier dans l'API viewer |
| 🔵 `/etc/hosts` | résolution des nœuds entre eux | ligne du nœud lui-même ; corosync dépend du nom | ligne `127.0.1.1` et la ligne du nœud cible | aucun. **Existe aussi en API** (`/nodes/{node}/hosts`), à vérifier |
| 🔴 `/etc/pve/*` (dont `corosync.conf`) | déjà partagé par pmxcfs dans un cluster | casser le cluster | | |
| 🔴 `/etc/corosync/*`, `/etc/hostname`, `/etc/machine-id`, clés SSH d'hôte, `/etc/iscsi/initiatorname.iscsi` | | identité unique du nœud | | |
| 🔴 `/etc/frr/*` | | géré par le SDN EVPN (hors périmètre) | | |
| 🔴 `/etc/kernel/cmdline`, `/etc/default/grub` | | démarrage du nœud | | v2 éventuellement |
🔵 = je propose de passer par l'API PVE plutôt que par le fichier (déjà le modèle du reste de l'outil).
**Ma recommandation v1 :** `interfaces` (+ `interfaces.d` hors `sdn`), `multipath.conf`, `wwids`, `chrony.conf`, `sysctl.d`. Le reste en v1.1 une fois le mécanisme éprouvé.
## 2. Modèle
**Profil de réplication** : un nœud maître, des nœuds cibles, une liste de fichiers, et par fichier des règles.
**Règles par fichier**, deux niveaux :
- **Adaptateur structuré** pour `interfaces` (stanzas `iface`) et `multipath.conf` (sections) : politique par champ comme pour l'API (sync / ignorer / surcharger par cible). Exemple : `iface vmbr0 / address` = surcharger, valeur par cible.
- **Mode texte** pour les autres : diff ligne à ligne, avec des **lignes protégées** (regex) conservées telles quelles sur la cible, et des **variables** (`{{hostname}}`, `{{ip}}`, valeurs saisies par cible).
- **Mapping de noms de NIC** par cible (`eno1` → `enp1s0f0`), appliqué avant le diff.
**Déroulé :**
1. **Lecture** : `cat` + `sha256sum` + propriétaire et droits, sur le maître et chaque cible.
2. **Diff** : diff unifié par fichier et par cible, après application des règles. Statut : à jour / écarts / bloquant (ex. NIC du maître absente sur la cible, alias multipath en conflit).
3. **Dry-run** (par défaut) : rien n'est écrit, on affiche le fichier final exact et la commande de rechargement.
4. **Apply**, après **confirmation explicite** (comme le SDN), **un nœud à la fois**, arrêt au premier échec :
- le hash de la cible doit être celui lu au plan (sinon : modifié entre-temps, on refuse) ;
- **sauvegarde** sur le nœud (`/var/lib/pve-confsync/backup/<chemin>.<horodatage>`) et en base (SQLite, historique) ;
- **validation** avant écriture quand l'outil existe : `multipath -t` sur un fichier temporaire, contrôle de syntaxe ifupdown2 (`ifup --no-act`, option exacte à vérifier) ;
- écriture **atomique** (fichier temporaire + `mv`), propriétaire et droits conservés ;
- **rechargement** (colonne de droite du tableau), seulement si coché ;
- **vérification** : relecture du hash, et pour le réseau, reconnexion SSH.
5. **Filet de sécurité réseau** : avant `ifreload -a`, on programme sur le nœud une restauration automatique (`systemd-run --on-active=120s` qui remet la sauvegarde et relance `ifreload -a`). Si l'outil se reconnecte en SSH dans le délai, il annule la restauration. Sinon le nœud revient seul à l'ancienne config.
6. **Suppression** : jamais par défaut. Un fichier absent du maître n'est pas supprimé sur la cible sauf option explicite (cohérent avec le mode additif du reste).
**Journal et audit** : chaque exécution (profil, nœud, fichier, hash avant / après, sauvegarde, sortie du rechargement) dans le journal existant, avec un type « Nœuds ».
**Droits** : opérateur = dry-run, admin = apply (le SSH root est le droit le plus fort de l'outil).
## 3. Maquette de l'interface
Un sélecteur en tête sépare les deux modes. Chaque mode garde les onglets Écarts / Profils (ou Clusters) / Journal.
```
┌──────────────────────────────────────────────────────────────────────┐
│ pve-confsync [ Clusters (API) | Nœuds (fichiers) ] 👤 ⏻ │
├──────────────────────────────────────────────────────────────────────┤
│ [Écarts] [Profils] [Journal] Profil: [Réseau stockage ▾] 2 écarts │
│ [Dry-run global] [Appliquer]│
├────────────────────────────┬─────────────────────────────────────────┤
│ MAÎTRE pve1 (lab-a) │ pve2 · /etc/network/interfaces ÉCARTS │
│ │ │
│ CIBLES │ auto bond0 │
│ ┌────────────────────────┐ │ iface bond0 inet manual │
│ │ pve2 (lab-a) ÉCARTS │ │ - bond-mode active-backup │
│ │ interfaces ✎ 3 │ │ + bond-mode 802.3ad │
│ │ multipath.conf ✓ │ │ + bond-xmit-hash-policy layer3+4 │
│ │ chrony.conf ✓ │ │ iface vmbr0 inet static │
│ └────────────────────────┘ │ address 10.0.0.12/24 🔒 surchargé │
│ ┌────────────────────────┐ │ bridge-ports bond0 │
│ │ pve3 (lab-a) À JOUR │ │ │
│ └────────────────────────┘ │ Après écriture : ifreload -a ☑ │
│ ┌────────────────────────┐ │ Restauration auto si perte SSH : 120 s │
│ │ pve7 (lab-b) BLOQUANT │ │ │
│ │ interfaces NIC eno2 │ │ [Dry-run] [Appliquer sur pve2] │
│ │ absente, à mapper │ │ │
│ └────────────────────────┘ │ │
└────────────────────────────┴─────────────────────────────────────────┘
```
Onglet **Profils** : maître, cibles (nœuds découverts par cluster, cases à cocher), fichiers (cases parmi la liste du §1), règles par fichier (champs à surcharger, lignes protégées, mapping de NIC par cible).
Fenêtre de **confirmation** d'apply : liste des nœuds dans l'ordre, fichiers, commandes de rechargement, avertissements (« redémarrage requis », « accès réseau »), saisie du nom du nœud pour confirmer quand `interfaces` est concerné.
## Questions ouvertes
1. Liste v1 : celle recommandée ci-dessus, ou plus large ?
2. `resolv.conf` et `hosts` par l'API plutôt que par fichier : d'accord ?
3. Identifiants SSH : hérités du cluster avec surcharge par nœud, d'accord ?