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

+112
View File
@@ -16,6 +16,20 @@
<span id="demoBadge" class="demo-badge" hidden data-i18n="demo">DÉMO</span>
</div>
<!-- the two replications: cluster to cluster (API), node to node (system files) -->
<div id="modeSw" class="seg mode-sw" role="radiogroup" data-i18n-aria="modeAriaSw">
<button type="button" role="radio" data-mode="cluster" data-i18n-title="modeClusterTitle">
<!-- Lucide "layers" (ISC license) -->
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83Z"/><path d="m22 17.65-9.17 4.16a2 2 0 0 1-1.66 0L2 17.65"/><path d="m22 12.65-9.17 4.16a2 2 0 0 1-1.66 0L2 12.65"/></svg>
<span data-i18n="modeCluster">Clusters (API)</span>
</button>
<button type="button" role="radio" data-mode="node" data-i18n-title="modeNodeTitle">
<!-- Lucide "hard-drive" (ISC license) -->
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M22 12H2"/><path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/><path d="M6 16h.01M10 16h.01"/></svg>
<span data-i18n="modeNode">Nœuds (fichiers)</span>
</button>
</div>
<div class="search">
<svg class="ico" viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>
<input id="q" type="search" autocomplete="off" spellcheck="false" data-i18n-ph="searchPh" data-i18n-aria="searchPh">
@@ -56,6 +70,11 @@
<svg viewBox="0 0 24 24" aria-hidden="true"><rect width="20" height="8" x="2" y="2" rx="2"/><rect width="20" height="8" x="2" y="14" rx="2"/><path d="M6 6h.01M6 18h.01"/></svg>
<span data-i18n="viewClusters">Clusters</span>
</button>
<button type="button" role="radio" data-view="nodes" hidden>
<!-- Lucide "server" (ISC license) -->
<svg viewBox="0 0 24 24" aria-hidden="true"><rect width="20" height="8" x="2" y="2" rx="2"/><rect width="20" height="8" x="2" y="14" rx="2"/><path d="M6 6h.01M6 18h.01"/></svg>
<span data-i18n="viewNodes">Nœuds et profils</span>
</button>
<button type="button" role="radio" data-view="runs">
<!-- Lucide "history" (ISC license) -->
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M12 7v5l4 2"/></svg>
@@ -67,6 +86,7 @@
<span data-i18n="viewAudit">Audit</span>
</button>
</div>
<select id="profileSel" data-diff data-node-only data-i18n-aria="profileAria"></select>
<select id="kindSel" data-diff data-i18n-aria="kindAria"></select>
<select id="statusSel" data-diff data-i18n-aria="statusAria"></select>
<select id="runModeSel" data-runs data-i18n-aria="modeAria" hidden></select>
@@ -132,6 +152,20 @@
<div id="clusterList" class="clist"></div>
</section>
<!-- Nodes (SSH access) and node replication profiles -->
<section id="nodesView" class="cl-view" hidden>
<div class="cl-tools">
<p class="muted small" data-i18n="nodesHelp"></p>
<button id="addProfile" class="btn tool primary" type="button" data-admin-only data-i18n="addProfile">+ Nouveau profil</button>
<button id="discoverBtn" class="btn tool" type="button" data-admin-only data-i18n="discover">Découvrir les nœuds</button>
<button id="testNodes" class="btn tool" type="button" data-admin-only data-i18n="testAll">Tout tester</button>
</div>
<h3 class="sec-title" data-i18n="profilesTitle">Profils de réplication</h3>
<div id="profileList" class="clist"></div>
<h3 class="sec-title" data-i18n="nodesTitle">Nœuds</h3>
<div id="nodeList" class="clist"></div>
</section>
<!-- Execution journal -->
<section id="runsView" hidden>
<div class="table runs">
@@ -297,6 +331,84 @@
</form>
</dialog>
<!-- SSH access of a node (administrators) -->
<dialog id="nodeDlg" aria-labelledby="nodeTitle">
<form class="dlg" id="nodeForm" method="dialog">
<header>
<h2 id="nodeTitle"></h2>
<button class="icon-btn" type="button" data-close data-i18n-aria="close"><svg viewBox="0 0 24 24"><path d="M6 6l12 12M18 6 6 18"/></svg></button>
</header>
<div class="dlg-body">
<p class="muted small" data-i18n="nodeHelp"></p>
<div class="field-grid">
<label for="nHost" data-i18n="nHost">Adresse SSH</label>
<input id="nHost" class="inp mono" autocomplete="off" spellcheck="false">
<label for="nPort" data-i18n="cSSHPort">Port SSH</label>
<input id="nPort" class="inp mono" type="number" min="1" max="65535" placeholder="22">
<label for="nPassword" data-i18n="nPassword">Mot de passe root</label>
<input id="nPassword" class="inp mono" type="password" autocomplete="new-password" spellcheck="false">
<span></span>
<label class="switch-row"><input id="nClear" type="checkbox" class="switch"><span data-i18n="nClear">Utiliser le mot de passe du cluster</span></label>
<span class="lbl" data-i18n="cSSHKey">Clé d'hôte SSH</span>
<span id="nKey" class="mono small muted"></span>
</div>
<p id="nErr" class="login-err" role="alert" hidden></p>
</div>
<footer>
<button id="nForget" class="btn ghost" type="button" data-i18n="forgetKey">Oublier la clé</button>
<span class="sp"></span>
<button class="btn ghost" type="button" data-close data-i18n="cancel">Annuler</button>
<button class="btn primary" type="submit" data-i18n="save">Enregistrer</button>
</footer>
</form>
</dialog>
<!-- Node replication profile (administrators) -->
<dialog id="profileDlg" class="wide" aria-labelledby="profileTitle">
<form class="dlg" id="profileForm" method="dialog">
<header>
<h2 id="profileTitle"></h2>
<button class="icon-btn" type="button" data-close data-i18n-aria="close"><svg viewBox="0 0 24 24"><path d="M6 6l12 12M18 6 6 18"/></svg></button>
</header>
<div class="dlg-body">
<div class="field-grid">
<label for="pName" data-i18n="cName">Nom</label>
<input id="pName" class="inp" required autocomplete="off" spellcheck="false">
<label for="pMaster" data-i18n="pMaster">Nœud maître</label>
<select id="pMaster" class="field" required></select>
</div>
<div class="sec-head mt"><h3 class="lbl" data-i18n="pTargets">Nœuds cibles</h3></div>
<div id="pTargets" class="chip-field"></div>
<div class="sec-head mt"><h3 class="lbl" data-i18n="pItems">Éléments répliqués</h3></div>
<div id="pItems" class="chip-field"></div>
<p class="muted small mt-s" data-i18n="pItemsHelp"></p>
<label class="switch-row mt-s"><input id="pPrune" type="checkbox" class="switch"><span data-i18n="pPrune">Supprimer sur les cibles ce que le maître n'a pas</span></label>
<p class="muted small" data-i18n="pPruneHelp"></p>
<div class="sec-head mt"><h3 class="lbl" data-i18n="polTitle">Politiques par champ</h3></div>
<p class="muted small" data-i18n="pPolHelp"></p>
<div class="erow-head pol"><span data-i18n="colKind">Type</span><span data-i18n="colField">Champ</span><span data-i18n="colPolicy">Politique</span><span></span></div>
<div id="pPolRows" class="erows"></div>
<div class="toolbar"><button id="pPolAdd" class="btn tool" type="button" data-i18n="addRow">+ Ajouter</button></div>
<div class="sec-head mt"><h3 class="lbl" data-i18n="pProtect">Lignes protégées des fichiers</h3></div>
<p class="muted small" data-i18n="pProtectHelp"></p>
<div id="pProtect" class="erows"></div>
<div class="sec-head mt"><h3 class="lbl" data-i18n="pMapping">Mapping par cible</h3></div>
<p class="muted small" data-i18n="pMappingHelp"></p>
<div id="pMapRows" class="erows"></div>
<p id="pErr" class="login-err" role="alert" hidden></p>
</div>
<footer>
<button id="pDelete" class="btn danger" type="button" data-i18n="delete">Supprimer</button>
<span class="sp"></span>
<button class="btn ghost" type="button" data-close data-i18n="cancel">Annuler</button>
<button class="btn primary" type="submit" data-i18n="save">Enregistrer</button>
</footer>
</form>
</dialog>
<dialog id="settingsDlg" class="settings" aria-labelledby="settingsTitle">
<div class="dlg">
<header>