[IMP] Remove jnl file if error in log file

This commit is contained in:
2026-06-30 23:15:57 +02:00
parent 8eb8a65348
commit 192b7ad713
3 changed files with 232 additions and 1 deletions
+178
View File
@@ -1053,6 +1053,170 @@ HANDLERS = {
}
# ──────────────────────────────────────────────
# BIND journal sync watcher
# ──────────────────────────────────────────────
def _get_redis():
"""Retourne la connexion Redis globale si disponible."""
import builtins
return getattr(builtins, "_netadmin_redis", None)
def fix_journal_out_of_sync(zone_name: str, cfg: dict) -> bool:
"""
Supprime le fichier .jnl désynchronisé et recharge la zone.
Appelé automatiquement quand BIND log "journal out of sync with zone".
Retourne True si la correction a réussi.
"""
zones_dir = cfg.get("zones_dir", "/etc/bind/zones")
rndc_cmd = cfg.get("rndc_cmd", "rndc")
# Chercher le .jnl dans zones_dir et ses parents courants
search_dirs = list({
zones_dir,
str(Path(zones_dir).parent), # /etc/bind
zones_dir.replace("/zones", "/master"), # /etc/bind/master
zones_dir.replace("/zones", "/pri"), # /etc/bind/pri
})
deleted = None
for d in search_dirs:
for jnl in [
Path(d) / f"db.{zone_name}.jnl",
Path(d) / f"{zone_name}.jnl",
Path(d) / f"db.{zone_name}.db.jnl",
]:
if jnl.exists():
try:
jnl.unlink()
deleted = jnl
log.info(f"Journal supprimé : {jnl}")
break
except Exception as e:
log.error(f"Impossible de supprimer {jnl}: {e}")
return False
if deleted:
break
if not deleted:
# Dernier recours : find dans tout zones_dir
try:
result = subprocess.run(
["find", zones_dir, "-name", f"*{zone_name}*.jnl"],
capture_output=True, text=True, timeout=5
)
for line in result.stdout.strip().splitlines():
p = Path(line.strip())
if p.exists():
p.unlink()
deleted = p
log.info(f"Journal supprimé (find) : {p}")
break
except Exception:
pass
if not deleted:
log.warning(f"Fichier .jnl introuvable pour '{zone_name}'")
# Recharger BIND
try:
result = subprocess.run(
[rndc_cmd, "reload", zone_name],
capture_output=True, text=True, timeout=15
)
if result.returncode == 0:
log.info(f"Zone '{zone_name}' rechargée après correction du journal")
else:
subprocess.run([rndc_cmd, "reconfig"], capture_output=True, timeout=15)
log.warning(f"rndc reload {zone_name} a échoué, reconfig lancé")
# Publier une notification sur Redis
try:
import datetime
_notif = {
"type": "journal_fixed",
"zone": zone_name,
"jnl_file": str(deleted) if deleted else "inconnu",
"timestamp": datetime.datetime.now().isoformat(),
"message": f"Journal BIND désynchronisé corrigé automatiquement pour {zone_name}",
}
_r = _get_redis()
if _r:
_r.publish("dns.notifications", json.dumps(_notif))
_r.lpush("dns.notifications.history", json.dumps(_notif))
_r.ltrim("dns.notifications.history", 0, 49) # garder 50 max
except Exception as ne:
log.warning(f"Impossible de publier la notification: {ne}")
return True
except Exception as e:
log.error(f"Erreur rndc après suppression du journal: {e}")
return False
def watch_bind_logs(cfg: dict, stop_event):
"""
Thread qui surveille les logs BIND et corrige automatiquement
les erreurs "journal out of sync with zone".
"""
import time
bind_log = cfg.get("bind_log", "/var/log/bind/default")
check_interval = cfg.get("journal_watch_interval", 30) # secondes
log.info(f"Journal watcher démarré — surveillance de {bind_log} toutes les {check_interval}s")
# Regex pour détecter l'erreur
jnl_re = re.compile(
r"zone\s+([^/]+)/IN.*journal out of sync with zone",
re.IGNORECASE
)
last_pos = 0
last_size = 0
while not stop_event.is_set():
try:
log_path = Path(bind_log)
if not log_path.exists():
stop_event.wait(check_interval)
continue
current_size = log_path.stat().st_size
# Log rotaté (fichier plus petit qu'avant) → recommencer du début
if current_size < last_size:
last_pos = 0
last_size = current_size
if current_size == last_pos:
stop_event.wait(check_interval)
continue
# Lire les nouvelles lignes depuis la dernière position
with open(bind_log, "r", errors="replace") as f:
f.seek(last_pos)
new_lines = f.read()
last_pos = f.tell()
for line in new_lines.splitlines():
m = jnl_re.search(line)
if m:
zone_name = m.group(1).strip().rstrip(".")
log.warning(f"Journal out of sync détecté pour '{zone_name}' — correction automatique…")
fix_journal_out_of_sync(zone_name, cfg)
except Exception as e:
log.error(f"Journal watcher error: {e}")
stop_event.wait(check_interval)
log.info("Journal watcher arrêté")
# ──────────────────────────────────────────────
# Main loop
# ──────────────────────────────────────────────
@@ -1087,8 +1251,22 @@ def run(cfg: dict):
pubsub = r.pubsub()
pubsub.subscribe("dns.commands")
log.info("Subscribed to dns.commands — waiting for messages…")
# Start BIND journal sync watcher thread
import threading
stop_event = threading.Event()
watcher_thread = threading.Thread(
target=watch_bind_logs,
args=(cfg, stop_event),
daemon=True,
name="journal-watcher"
)
watcher_thread.start()
# Announce reconnection so the backend can flush the pending queue
import socket as _socket
# Rendre la connexion Redis accessible au journal watcher
import builtins
builtins._netadmin_redis = r
r.publish("agent.ready", json.dumps({"service": "dns", "host": _socket.gethostname()}))
log.info(f"Published agent.ready for service 'dns'")
+31
View File
@@ -939,6 +939,37 @@ def rollback_zone(
}
@router.get("/notifications")
def get_notifications(user: dict = Depends(get_current_user)):
"""Retourne les dernières notifications de l'agent DNS."""
try:
from broker import get_redis
r = get_redis()
items = r.lrange("dns.notifications.history", 0, 49)
import json as _json
notifications = []
for item in items:
try:
notifications.append(_json.loads(item))
except Exception:
pass
return {"notifications": notifications}
except Exception as e:
return {"notifications": [], "error": str(e)}
@router.delete("/notifications")
def clear_notifications(user: dict = Depends(get_current_user)):
"""Efface l'historique des notifications."""
try:
from broker import get_redis
r = get_redis()
r.delete("dns.notifications.history")
return {"success": True}
except Exception as e:
raise HTTPException(500, str(e))
@router.post("/sync")
def sync_endpoint(
zone: str | None = None,
+23 -1
View File
@@ -42,7 +42,8 @@ export default function DNSManager({ apiBase, token, onUnauthorized }) {
const [rawContent, setRawContent] = useState("");
const [rawSaving, setRawSaving] = useState(false);
const [rawLoading, setRawLoading] = useState(false);
const [rawError, setRawError] = useState(null);
const [rawError, setRawError] = useState(null);
const [notifications, setNotifications] = useState([]);
const [copied, setCopied] = useState(false);
const [confirmZone, setConfirmZone] = useState(null);
const [confirmRec, setConfirmRec] = useState(null);
@@ -125,6 +126,27 @@ export default function DNSManager({ apiBase, token, onUnauthorized }) {
}
};
const loadNotifications = useCallback(async () => {
try {
const data = await api("/dns/notifications");
setNotifications(data.notifications || []);
} catch(e) { /* silencieux */ }
}, [api]);
const clearNotifications = async () => {
try {
await api("/dns/notifications", { method: "DELETE" });
setNotifications([]);
} catch(e) { setError(e.message); }
};
// Poll notifications toutes les 30s
useEffect(() => {
loadNotifications();
const timer = setInterval(loadNotifications, 30000);
return () => clearInterval(timer);
}, [loadNotifications]);
const loadSnapshots = async (zone) => {
setSnapshotModal(true);
setSnapLoading(true);