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

+100
View File
@@ -0,0 +1,100 @@
// Package nodesync replicates the system configuration of one PVE node (the
// master) to other nodes (the targets), of the same cluster or not. It is the
// second replication mode of pve-confsync, next to the cluster to cluster one
// (package engine). What the PVE API covers goes through the API: the network
// interfaces (/nodes/{node}/network, written in interfaces.new then reloaded)
// and the DNS servers (/nodes/{node}/dns). The other files go through SSH as
// root on the node: multipath, chrony, sysctl, interfaces.d. Same pipeline as
// the engine: read, normalize, diff, plan, apply with backup, reload, verify.
package nodesync
import "fmt"
// ItemKind tells how an item is read and written.
type ItemKind string
const (
KindNetwork ItemKind = "network" // API /nodes/{node}/network
KindDNS ItemKind = "dns" // API /nodes/{node}/dns
KindFile ItemKind = "file" // files over SSH
)
// Item is one replicable element.
type Item struct {
ID string `json:"id"`
Kind ItemKind `json:"kind"`
// Files: paths or glob patterns read on the nodes, and paths never touched.
Paths []string `json:"paths,omitempty"`
Exclude []string `json:"exclude,omitempty"`
// Glob items hold several files: a file only on the target is kept, or
// deleted with the prune option. Single file items are never deleted.
Glob bool `json:"glob,omitempty"`
// Additive files (multipath wwids) only get the lines of the master they
// miss: nothing is removed from the target.
Additive bool `json:"additive,omitempty"`
// Reload is the command run on the node after a write (empty: none).
Reload string `json:"reload,omitempty"`
// Network items reload the network of the node: typed confirmation,
// automatic restore when the node is unreachable afterwards.
Network bool `json:"network,omitempty"`
}
// Items is the catalog, in apply order (reloads follow this order, the
// network last).
var Items = []Item{
{ID: "sysctl", Kind: KindFile, Paths: []string{"/etc/sysctl.d/*.conf"}, Glob: true, Reload: "sysctl --system"},
{ID: "chrony", Kind: KindFile, Paths: []string{"/etc/chrony/chrony.conf", "/etc/chrony/sources.d/*.sources"}, Glob: true,
Reload: "systemctl restart chrony"},
{ID: "multipath", Kind: KindFile, Paths: []string{"/etc/multipath.conf"}, Reload: "multipathd reconfigure"},
{ID: "wwids", Kind: KindFile, Paths: []string{"/etc/multipath/wwids"}, Additive: true, Reload: "multipathd reconfigure"},
{ID: "dns", Kind: KindDNS},
// interfaces.d/sdn is generated by the SDN of PVE: never replicated
{ID: "interfaces-d", Kind: KindFile, Paths: []string{"/etc/network/interfaces.d/*"}, Exclude: []string{"/etc/network/interfaces.d/sdn"},
Glob: true, Reload: "ifreload -a", Network: true},
{ID: "network", Kind: KindNetwork, Network: true},
}
// ItemByID returns an item of the catalog.
func ItemByID(id string) (Item, error) {
for _, it := range Items {
if it.ID == id {
return it, nil
}
}
return Item{}, fmt.Errorf("unknown item %q", id)
}
func itemRank(id string) int {
for i, it := range Items {
if it.ID == id {
return i
}
}
return len(Items)
}
// Policy of a field of an API item (network, dns).
type Policy string
const (
Sync Policy = "sync" // copied from the master
Ignore Policy = "ignore" // never written
Override Policy = "override" // only written from the target mapping
)
// defaultPolicy: the addresses and gateways are those of each node; they are
// only written from the mapping of a target.
func defaultPolicy(item, field string) Policy {
if item == "network" {
switch field {
case "cidr", "cidr6", "gateway", "gateway6":
return Override
}
}
return Sync
}
// DefaultPolicies lists the fields whose default is not "sync", for the UI.
var DefaultPolicies = map[string]map[string]Policy{
"network": {"cidr": Override, "cidr6": Override, "gateway": Override, "gateway6": Override},
}