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>
78 lines
3.2 KiB
Go
78 lines
3.2 KiB
Go
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"))
|
|
}
|
|
}
|