[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
|
# Main loop
|
||||||
# ──────────────────────────────────────────────
|
# ──────────────────────────────────────────────
|
||||||
@@ -1087,8 +1251,22 @@ def run(cfg: dict):
|
|||||||
pubsub = r.pubsub()
|
pubsub = r.pubsub()
|
||||||
pubsub.subscribe("dns.commands")
|
pubsub.subscribe("dns.commands")
|
||||||
log.info("Subscribed to dns.commands — waiting for messages…")
|
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
|
# Announce reconnection so the backend can flush the pending queue
|
||||||
import socket as _socket
|
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()}))
|
r.publish("agent.ready", json.dumps({"service": "dns", "host": _socket.gethostname()}))
|
||||||
log.info(f"Published agent.ready for service 'dns'")
|
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")
|
@router.post("/sync")
|
||||||
def sync_endpoint(
|
def sync_endpoint(
|
||||||
zone: str | None = None,
|
zone: str | None = None,
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export default function DNSManager({ apiBase, token, onUnauthorized }) {
|
|||||||
const [rawSaving, setRawSaving] = useState(false);
|
const [rawSaving, setRawSaving] = useState(false);
|
||||||
const [rawLoading, setRawLoading] = 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 [copied, setCopied] = useState(false);
|
||||||
const [confirmZone, setConfirmZone] = useState(null);
|
const [confirmZone, setConfirmZone] = useState(null);
|
||||||
const [confirmRec, setConfirmRec] = 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) => {
|
const loadSnapshots = async (zone) => {
|
||||||
setSnapshotModal(true);
|
setSnapshotModal(true);
|
||||||
setSnapLoading(true);
|
setSnapLoading(true);
|
||||||
|
|||||||
Reference in New Issue
Block a user