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>
149 lines
3.0 KiB
Go
149 lines
3.0 KiB
Go
package nodesync
|
|
|
|
import (
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// Line is one line of a unified diff: Op is ' ' (context), '-' (target only)
|
|
// or '+' (written by the replication). A hunk starts with Op '@' and Text
|
|
// "@@ -a,b +c,d @@".
|
|
type Line struct {
|
|
Op string `json:"op"`
|
|
Text string `json:"text"`
|
|
}
|
|
|
|
const diffContext = 3
|
|
|
|
// maxCells bounds the LCS table; beyond, the diff shows every line replaced.
|
|
const maxCells = 4_000_000
|
|
|
|
// Diff compares the current content of a file with the content to write.
|
|
func Diff(oldText, newText []byte) []Line {
|
|
a, b := lines(oldText), lines(newText)
|
|
ops := editScript(a, b)
|
|
// group the changes into hunks with their context
|
|
var out []Line
|
|
n := len(ops)
|
|
i := 0
|
|
for i < n {
|
|
if ops[i].Op == " " {
|
|
i++
|
|
continue
|
|
}
|
|
start := i - diffContext
|
|
if start < 0 {
|
|
start = 0
|
|
}
|
|
end := i
|
|
for end < n {
|
|
if ops[end].Op != " " {
|
|
end++
|
|
continue
|
|
}
|
|
// a run of context: the hunk ends when it is longer than 2*context
|
|
j := end
|
|
for j < n && ops[j].Op == " " {
|
|
j++
|
|
}
|
|
if j == n || j-end > 2*diffContext {
|
|
end += diffContext
|
|
if end > n {
|
|
end = n
|
|
}
|
|
break
|
|
}
|
|
end = j
|
|
}
|
|
oa, ob := ops[start].A, ops[start].B
|
|
ca, cb := 0, 0
|
|
for _, o := range ops[start:end] {
|
|
if o.Op != "+" {
|
|
ca++
|
|
}
|
|
if o.Op != "-" {
|
|
cb++
|
|
}
|
|
}
|
|
out = append(out, Line{Op: "@", Text: hunkHeader(oa, ca, ob, cb)})
|
|
for _, o := range ops[start:end] {
|
|
out = append(out, Line{Op: o.Op, Text: o.Text})
|
|
}
|
|
i = end
|
|
}
|
|
return out
|
|
}
|
|
|
|
type editOp struct {
|
|
Op string
|
|
Text string
|
|
A, B int // line numbers (1-based) where this op starts in a and b
|
|
}
|
|
|
|
func editScript(a, b []string) []editOp {
|
|
var ops []editOp
|
|
if len(a)*len(b) > maxCells {
|
|
for i, l := range a {
|
|
ops = append(ops, editOp{"-", l, i + 1, 1})
|
|
}
|
|
for i, l := range b {
|
|
ops = append(ops, editOp{"+", l, len(a) + 1, i + 1})
|
|
}
|
|
return ops
|
|
}
|
|
// lcs[i][j]: length of the LCS of a[i:] and b[j:]
|
|
lcs := make([][]int, len(a)+1)
|
|
for i := range lcs {
|
|
lcs[i] = make([]int, len(b)+1)
|
|
}
|
|
for i := len(a) - 1; i >= 0; i-- {
|
|
for j := len(b) - 1; j >= 0; j-- {
|
|
if a[i] == b[j] {
|
|
lcs[i][j] = lcs[i+1][j+1] + 1
|
|
} else if lcs[i+1][j] >= lcs[i][j+1] {
|
|
lcs[i][j] = lcs[i+1][j]
|
|
} else {
|
|
lcs[i][j] = lcs[i][j+1]
|
|
}
|
|
}
|
|
}
|
|
i, j := 0, 0
|
|
for i < len(a) || j < len(b) {
|
|
switch {
|
|
case i < len(a) && j < len(b) && a[i] == b[j]:
|
|
ops = append(ops, editOp{" ", a[i], i + 1, j + 1})
|
|
i++
|
|
j++
|
|
case j < len(b) && (i == len(a) || lcs[i][j+1] > lcs[i+1][j]):
|
|
ops = append(ops, editOp{"+", b[j], i + 1, j + 1})
|
|
j++
|
|
default:
|
|
ops = append(ops, editOp{"-", a[i], i + 1, j + 1})
|
|
i++
|
|
}
|
|
}
|
|
return ops
|
|
}
|
|
|
|
func lines(b []byte) []string {
|
|
s := string(b)
|
|
if s == "" {
|
|
return nil
|
|
}
|
|
s = strings.TrimSuffix(s, "\n")
|
|
return strings.Split(s, "\n")
|
|
}
|
|
|
|
func hunkHeader(a, ca, b, cb int) string {
|
|
r := func(start, n int) string {
|
|
if n == 0 {
|
|
start--
|
|
}
|
|
if n == 1 {
|
|
return strconv.Itoa(start)
|
|
}
|
|
return strconv.Itoa(start) + "," + strconv.Itoa(n)
|
|
}
|
|
return "@@ -" + r(a, ca) + " +" + r(b, cb) + " @@"
|
|
}
|