[IMP] Remove jnl file if error in log file
This commit is contained in:
@@ -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'")
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user