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

+77
View File
@@ -0,0 +1,77 @@
package nodesync
import (
"os"
"os/exec"
"os/user"
"path/filepath"
"strings"
"testing"
)
// TestPrepareScriptForReal runs the prepare script and restore.sh with the
// local shell in a temporary folder (every path prefixed with it).
func TestPrepareScriptForReal(t *testing.T) {
if _, err := exec.LookPath("sha256sum"); err != nil {
t.Skip("sha256sum not installed")
}
root := t.TempDir()
u, _ := user.Current()
g, _ := user.LookupGroupId(u.Gid)
owner := u.Username + ":" + g.Name
write := func(p, s string) {
if err := os.MkdirAll(filepath.Dir(root+p), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(root+p, []byte(s), 0o644); err != nil {
t.Fatal(err)
}
}
read := func(p string) string {
b, err := os.ReadFile(root + p)
if err != nil {
return "<absent>"
}
return string(b)
}
write("/etc/sysctl.d/90-net.conf", "old\n")
write("/etc/sysctl.d/99-local.conf", "local\n")
write("/etc/network/interfaces", "auto lo\n")
plan := Plan{Changes: []Change{
{Item: "sysctl", ID: "/etc/sysctl.d/90-net.conf", Op: OpUpdate, Hash: hash([]byte("old\n")), Mode: "640", Owner: owner, Content: []byte("new\n")},
{Item: "multipath", ID: "/etc/multipath.conf", Op: OpCreate, Hash: "absent", Mode: "644", Owner: owner, Content: []byte("defaults {}\n")},
{Item: "sysctl", ID: "/etc/sysctl.d/99-local.conf", Op: OpDelete, Hash: hash([]byte("local\n"))},
{Item: "network", ID: "vmbr0", Op: OpUpdate},
}}
run := func(script string, in []byte) (string, error) {
cmd := exec.Command("sh", "-c", script)
cmd.Stdin = strings.NewReader(string(in))
out, err := cmd.CombinedOutput()
return string(out), err
}
const dir = "/var/lib/pve-confsync/backup/test"
if out, err := run(prepareScriptIn(dir, root), prepareInput(plan, true, false)); err != nil || strings.TrimSpace(out) != "ok" {
t.Fatalf("prepare: %v %s", err, out)
}
if read("/etc/sysctl.d/90-net.conf") != "new\n" || read("/etc/multipath.conf") != "defaults {}\n" || read("/etc/sysctl.d/99-local.conf") != "<absent>" {
t.Fatalf("writes: %q %q %q", read("/etc/sysctl.d/90-net.conf"), read("/etc/multipath.conf"), read("/etc/sysctl.d/99-local.conf"))
}
if fi, _ := os.Stat(root + "/etc/sysctl.d/90-net.conf"); fi.Mode().Perm() != 0o640 {
t.Errorf("mode %v", fi.Mode())
}
if m, _ := filepath.Glob(root + "/etc/sysctl.d/*.confsync.*"); len(m) > 0 {
t.Errorf("temporary files left: %v", m)
}
// a second run with the same plan: the files changed, nothing is written
if out, err := run(prepareScriptIn(dir+"2", root), prepareInput(plan, false, false)); err == nil || !strings.Contains(out, "changed since the dry-run") {
t.Errorf("stale check: %v %s", err, out)
}
write("/etc/network/interfaces.new", "pending\n")
if out, err := run("sh '"+root+dir+"/restore.sh'", nil); err != nil {
t.Fatalf("restore: %v %s", err, out)
}
if read("/etc/sysctl.d/90-net.conf") != "old\n" || read("/etc/multipath.conf") != "<absent>" || read("/etc/sysctl.d/99-local.conf") != "local\n" ||
read("/etc/network/interfaces") != "auto lo\n" || read("/etc/network/interfaces.new") != "<absent>" {
t.Errorf("restore: %q %q %q %q", read("/etc/sysctl.d/90-net.conf"), read("/etc/multipath.conf"), read("/etc/sysctl.d/99-local.conf"), read("/etc/network/interfaces.new"))
}
}