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

+18 -9
View File
@@ -17,18 +17,27 @@ import (
// sshServer answers the commands pve-confsync runs on a node: pveversion, the
// read of priv/shadow.cfg and its locked write (adapter password).
type sshServer struct {
ln net.Listener
key ssh.PublicKey
ln net.Listener
key ssh.PublicKey
exec func(cmd string, in io.Reader, out, errw io.Writer) int
}
func startSSH(s *Server) *sshServer {
return newSSH(func(user, pass string) bool {
s.mu.Lock()
defer s.mu.Unlock()
return user == "root" && pass == s.Password
}, s.exec)
}
// newSSH starts an SSH server accepting the password check ok and running
// the commands with exec.
func newSSH(ok func(user, pass string) bool, exec func(cmd string, in io.Reader, out, errw io.Writer) int) *sshServer {
_, priv, _ := ed25519.GenerateKey(rand.Reader)
signer, _ := ssh.NewSignerFromKey(priv)
cfg := &ssh.ServerConfig{
PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
s.mu.Lock()
defer s.mu.Unlock()
if c.User() == "root" && string(pass) == s.Password {
if ok(c.User(), string(pass)) {
return nil, nil
}
return nil, fmt.Errorf("denied")
@@ -39,14 +48,14 @@ func startSSH(s *Server) *sshServer {
if err != nil {
panic(err)
}
srv := &sshServer{ln: ln, key: signer.PublicKey()}
srv := &sshServer{ln: ln, key: signer.PublicKey(), exec: exec}
go func() {
for {
nc, err := ln.Accept()
if err != nil {
return
}
go s.serveSSH(nc, cfg)
go srv.serve(nc, cfg)
}
}()
return srv
@@ -65,7 +74,7 @@ func (s *Server) SSHHostKey() string {
return strings.TrimSpace(string(ssh.MarshalAuthorizedKey(s.ssh.key)))
}
func (s *Server) serveSSH(nc net.Conn, cfg *ssh.ServerConfig) {
func (x *sshServer) serve(nc net.Conn, cfg *ssh.ServerConfig) {
_, chans, reqs, err := ssh.NewServerConn(nc, cfg)
if err != nil {
nc.Close()
@@ -91,7 +100,7 @@ func (s *Server) serveSSH(nc net.Conn, cfg *ssh.ServerConfig) {
var p struct{ Cmd string }
_ = ssh.Unmarshal(req.Payload, &p)
_ = req.Reply(true, nil)
status := s.exec(p.Cmd, ch, ch, ch.Stderr())
status := x.exec(p.Cmd, ch, ch, ch.Stderr())
b := make([]byte, 4)
binary.BigEndian.PutUint32(b, uint32(status))
_, _ = ch.SendRequest("exit-status", false, b)