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

+59
View File
@@ -8,9 +8,11 @@ import (
"context"
"crypto/sha256"
"encoding/hex"
"strings"
"pve-confsync/internal/adapter"
"pve-confsync/internal/model"
"pve-confsync/internal/nodesync"
"pve-confsync/internal/pve"
"pve-confsync/internal/pve/pvetest"
"pve-confsync/internal/store"
@@ -104,6 +106,45 @@ func Start(ctx context.Context, st *store.Store) (*Clusters, error) {
b.Shadow["alice"] = ref.Shadow["alice"]
b.Shadow["bob"] = ref.Shadow["bob"]
// nodes of the node replication: pve1 (master) and pve2 in pve-rennes,
// pve-a1 in site-a with other NIC names
n1, n2, a1 := ref.AddNode("pve1"), ref.AddNode("pve2"), a.AddNode("pve-a1")
n1.Net = map[string]map[string]any{
"eno1": {"type": "eth", "mtu": "9000"},
"eno2": {"type": "eth", "mtu": "9000"},
"bond0": {"type": "bond", "slaves": "eno1 eno2", "bond_mode": "802.3ad", "bond_xmit_hash_policy": "layer3+4", "mtu": "9000"},
"vmbr0": {"type": "bridge", "bridge_ports": "bond0", "bridge_vlan_aware": "1", "cidr": "10.0.0.11/24", "gateway": "10.0.0.1", "autostart": "1"},
"bond0.50": {"type": "vlan", "mtu": "9000"},
"vmbr1": {"type": "bridge", "bridge_ports": "bond0.50", "cidr": "10.5.0.11/24", "comments": "stockage iSCSI", "autostart": "1"},
}
n2.Net = map[string]map[string]any{
"eno1": {"type": "eth", "mtu": "9000"},
"eno2": {"type": "eth", "mtu": "9000"},
"bond0": {"type": "bond", "slaves": "eno1 eno2", "bond_mode": "active-backup", "mtu": "9000"},
"vmbr0": {"type": "bridge", "bridge_ports": "bond0", "cidr": "10.0.0.12/24", "gateway": "10.0.0.1", "autostart": "1"},
}
a1.Net = map[string]map[string]any{
"enp65s0f0": {"type": "eth"},
"enp65s0f1": {"type": "eth"},
"bond0": {"type": "bond", "slaves": "enp65s0f0 enp65s0f1", "bond_mode": "802.3ad", "bond_xmit_hash_policy": "layer3+4"},
"vmbr0": {"type": "bridge", "bridge_ports": "bond0", "bridge_vlan_aware": "1", "cidr": "192.168.10.21/24", "gateway": "192.168.10.1", "autostart": "1"},
}
n1.DNS = map[string]string{"search": "rennes.lab", "dns1": "10.0.0.53", "dns2": "10.0.0.54"}
n2.DNS = map[string]string{"search": "rennes.lab", "dns1": "10.0.0.53"}
a1.DNS = map[string]string{"search": "rennes.lab", "dns1": "10.0.0.53", "dns2": "10.0.0.54"}
mp := "defaults {\n user_friendly_names yes\n find_multipaths no\n}\nblacklist {\n devnode \"^sda\"\n}\ndevices {\n device {\n vendor \"NETAPP\"\n product \"LUN.*\"\n path_grouping_policy group_by_prio\n no_path_retry queue\n }\n}\n"
n1.Files["/etc/multipath.conf"] = pvetest.FakeFile{Content: mp}
n2.Files["/etc/multipath.conf"] = pvetest.FakeFile{Content: strings.Replace(mp, "no_path_retry queue", "no_path_retry 12", 1)}
n1.Files["/etc/multipath/wwids"] = pvetest.FakeFile{Content: "# Multipath wwids, Version : 1.0\n/3600a098038303053453f463045747a41/\n/3600a098038303053453f463045747a42/\n"}
n2.Files["/etc/multipath/wwids"] = pvetest.FakeFile{Content: "# Multipath wwids, Version : 1.0\n/3600a098038303053453f463045747a41/\n"}
n1.Files["/etc/chrony/chrony.conf"] = pvetest.FakeFile{Content: "server ntp1.rennes.lab iburst\nserver ntp2.rennes.lab iburst\ndriftfile /var/lib/chrony/chrony.drift\nmakestep 1.0 3\n"}
n2.Files["/etc/chrony/chrony.conf"] = pvetest.FakeFile{Content: "pool 2.debian.pool.ntp.org iburst\ndriftfile /var/lib/chrony/chrony.drift\nmakestep 1.0 3\n"}
a1.Files["/etc/chrony/chrony.conf"] = n1.Files["/etc/chrony/chrony.conf"]
n1.Files["/etc/sysctl.d/90-network.conf"] = pvetest.FakeFile{Content: "net.core.rmem_max = 16777216\nnet.core.wmem_max = 16777216\n"}
a1.Files["/etc/sysctl.d/90-network.conf"] = n1.Files["/etc/sysctl.d/90-network.conf"]
a1.Files["/etc/sysctl.d/99-local.conf"] = pvetest.FakeFile{Content: "vm.swappiness = 10\n"}
a1.Files["/etc/multipath.conf"] = n1.Files["/etc/multipath.conf"]
// legacy: PVE 8, refused by the version check
old := pvetest.New("8.4.1")
@@ -140,6 +181,24 @@ func Start(ctx context.Context, st *store.Store) (*Clusters, error) {
st.SaveSettings(ctx, store.Settings{Kinds: model.Kinds,
Policies: map[model.Kind]map[string]adapter.Policy{model.KindDatacenter: {"keyboard": adapter.Sync}}}),
}
node := func(cluster string, n *pvetest.Node) error {
host, port := n.SSHAddr()
if err := st.SaveNode(ctx, cluster, n.Name, host, port, "", false); err != nil {
return err
}
return st.PinNode(ctx, cluster, n.Name, n.SSHHostKey(), nil)
}
steps = append(steps,
st.SetNodes(ctx, "pve-rennes", []store.Discovered{{Name: "pve1", IP: "127.0.0.1"}, {Name: "pve2", IP: "127.0.0.1"}}),
st.SetNodes(ctx, "site-a", []store.Discovered{{Name: "pve-a1", IP: "127.0.0.1"}}),
node("pve-rennes", n1), node("pve-rennes", n2), node("site-a", a1),
st.SaveProfile(ctx, "", nodesync.Profile{Name: "réseau et stockage", Master: "pve-rennes/pve1",
Targets: []string{"pve-rennes/pve2", "site-a/pve-a1"},
Items: []string{"network", "dns", "multipath", "wwids", "chrony", "sysctl"},
Mapping: map[string]nodesync.TargetMap{
"pve-rennes/pve2": {Values: map[string]map[string]string{"network/vmbr1": {"cidr": "10.5.0.12/24"}}},
"site-a/pve-a1": {NICs: map[string]string{"eno1": "enp65s0f0", "eno2": "enp65s0f1"}},
}}))
for _, u := range []struct{ name, role string }{{"admin", store.RoleAdmin}, {"operateur", store.RoleOperator}, {"lecteur", store.RoleViewer}} {
steps = append(steps, st.SaveUser(ctx, u.name, u.role, Password))
}