From 272ad6d80ad83b36258a62c8b9230aa11f788394 Mon Sep 17 00:00:00 2001 From: Michel Date: Tue, 21 Apr 2026 17:14:16 +0200 Subject: [PATCH] Mise en place d'un journal de connexions --- .env | 16 +++- app/Monitor_Meudon.py | 15 ++- app/Monitor_Saclay.py | 11 ++- app/Monitor_connexions.py | 183 ++++++++++++++++++++++++++++++++++++ app/surveillance_releves.py | 48 +++++++--- requirements.txt | Bin 2932 -> 3000 bytes 6 files changed, 250 insertions(+), 23 deletions(-) create mode 100644 app/Monitor_connexions.py diff --git a/.env b/.env index 3695dc8..be60b3f 100644 --- a/.env +++ b/.env @@ -3,6 +3,11 @@ DB_HOST=162.19.78.131 DB_USER=sondes DB_PASS=TX.)-U1!zq5Axdk4 DB_NAME=Sondes + +DB_USER2=journal_connexions +DB_PASS2=%t!RRa6sj4KY9dY%zTT +DB_NAME2=Acces + AUTH_USERS=[{"user":"Michel","pass":"210462"}] # MQTT @@ -12,14 +17,17 @@ MQTT_PASS=3J@bjYP0 MQTT_PORT=1883 # Synology Chat -SYNO_CHAT_WEBHOOK_MONITOR_SACLAY=https://mj91.fr/chat/webapi/entry.cgi?api=SYNO.Chat.External&method=incoming&version=2&token=QpLWAZEqIW1EOBHkfDkmr1LqC3P3J1SASWfqpchZdd1xPY7xGbYerS4lCADJnPrm -SYNO_CHAT_WEBHOOK_MONITOR_MEUDON=https://mj91.fr/chat/webapi/entry.cgi?api=SYNO.Chat.External&method=incoming&version=2&token=QpLWAZEqIW1EOBHkfDkmr1LqC3P3J1SASWfqpchZdd1xPY7xGbYerS4lCADJnPrm +SYNO_CHAT_WEBHOOK_MONITOR_SACLAY=https://mj91.fr/webapi/entry.cgi?api=SYNO.Chat.External&method=incoming&version=2&token=zLaQnf5tD2BTsu6N7RQZvUyKipQSDSiXqV57VhWUfblQksL9K8NH22imxEtKas4m +SYNO_CHAT_WEBHOOK_MONITOR_MEUDON=https://mj91.fr/webapi/entry.cgi?api=SYNO.Chat.External&method=incoming&version=2&token=3bU0z4cG15CyCxXvz76voErQq8c1SLsms8kxsH6DvUQhGVQ9w5zqLvZ0GLVsLONP -SYNO_CHAT_WEBHOOK_GYRO_SACLAY=https://mj91.fr/chat/webapi/entry.cgi?api=SYNO.Chat.External&method=incoming&version=2&token=NUqdEGbtmTBBGIN29z0AVZUeQnyZ8tskyXGpkOFqHwqOAXB8quvHxlEcewKX3Xnq -SYNO_CHAT_WEBHOOK_GYRO_MEUDON=https://mj91.fr/chat/webapi/entry.cgi?api=SYNO.Chat.External&method=incoming&version=2&token=NUqdEGbtmTBBGIN29z0AVZUeQnyZ8tskyXGpkOFqHwqOAXB8quvHxlEcewKX3Xnq +SYNO_CHAT_WEBHOOK_GYRO_SACLAY=https://mj91.fr/webapi/entry.cgi?api=SYNO.Chat.External&method=incoming&version=2&token=M0cJHOhtqhEWFEpdd8XTcmZHcTVq9ItDDCodvRq8MJHAJVbn9UTRZf1SxZXRn8mr +SYNO_CHAT_WEBHOOK_GYRO_MEUDON=https://mj91.fr/webapi/entry.cgi?api=SYNO.Chat.External&method=incoming&version=2&token=yllI91DMC75XqFDwMW798GraUTLVv5Hb4wGGGmd65fHm4wbQIghlb01cgYPkZLtd + +SYNO_CHAT_WEBHOOK_CONNEXIONS=https://mj91.fr/webapi/entry.cgi?api=SYNO.Chat.External&method=incoming&version=2&token=R585242twVz04qmzukxbtSTMe7p0GgdroKtO8opBglDx3VLtaLwJhYb93btH6Hya SYNO_CHAT_BOTNAME_MONITOR="Injection données dans tables" SYNO_CHAT_BOTNAME_GYRO="Gestion Gyro" +SYNO_CHAT_BOTNAME_CONNEXIONS="Journal Connexions" SYNO_CHAT_TIMEOUT=10 SYNO_CHAT_VERIFY_SSL=true diff --git a/app/Monitor_Meudon.py b/app/Monitor_Meudon.py index 3ab8a78..066b268 100644 --- a/app/Monitor_Meudon.py +++ b/app/Monitor_Meudon.py @@ -391,15 +391,22 @@ def depassement_depuis_30min(site: str, sonde: str, seuil: float) -> bool: # ========= Synology Chat ========= def send_synology_chat(message: str, *, username: str | None = None) -> bool: webhook = ( + _env_str(f"SYNO_CHAT_WEBHOOK_MONITOR_{SITE}") or + _env_str(f"SYNO_CHAT_WEBHOOK_MONITOR_{SITE.upper()}") or + _env_str("SYNO_CHAT_WEBHOOK_MONITOR") or _env_str(f"SYNO_CHAT_WEBHOOK_{SITE}") or - _env_str(f"SYNO_CHAT_WEBHOOK_MONITOR") or + _env_str(f"SYNO_CHAT_WEBHOOK_{SITE.upper()}") or _env_str("SYNO_CHAT_WEBHOOK") ) if not webhook: log.info("Synology Chat non configuré.") return False - botname = username or _env_str("SYNO_CHAT_BOTNAME") + botname = ( + username + or _env_str("SYNO_CHAT_BOTNAME_MONITOR") + or _env_str("SYNO_CHAT_BOTNAME") + ) timeout = int(_env_str("SYNO_CHAT_TIMEOUT", "10")) verify_ssl = _env_bool("SYNO_CHAT_VERIFY_SSL", True) @@ -407,7 +414,9 @@ def send_synology_chat(message: str, *, username: str | None = None) -> bool: if botname: chat_payload["username"] = botname - form_data = {"payload": json.dumps(chat_payload, ensure_ascii=False)} + form_data = { + "payload": json.dumps(chat_payload, ensure_ascii=False) + } try: response = requests.post( diff --git a/app/Monitor_Saclay.py b/app/Monitor_Saclay.py index e835069..5ed0f3c 100644 --- a/app/Monitor_Saclay.py +++ b/app/Monitor_Saclay.py @@ -307,15 +307,22 @@ def depassement_depuis_30min(site: str, sonde: str, seuil: float) -> bool: # ========= Synology Chat ========= def send_synology_chat(message: str, *, username: str | None = None) -> bool: webhook = ( + _env_str(f"SYNO_CHAT_WEBHOOK_MONITOR_{SITE}") or + _env_str(f"SYNO_CHAT_WEBHOOK_MONITOR_{SITE.upper()}") or + _env_str("SYNO_CHAT_WEBHOOK_MONITOR") or _env_str(f"SYNO_CHAT_WEBHOOK_{SITE}") or - _env_str(f"SYNO_CHAT_WEBHOOK_MONITOR_SACLAY") or + _env_str(f"SYNO_CHAT_WEBHOOK_{SITE.upper()}") or _env_str("SYNO_CHAT_WEBHOOK") ) if not webhook: log.info("Synology Chat non configuré.") return False - botname = username or _env_str("SYNO_CHAT_BOTNAME") + botname = ( + username + or _env_str("SYNO_CHAT_BOTNAME_MONITOR") + or _env_str("SYNO_CHAT_BOTNAME") + ) timeout = int(_env_str("SYNO_CHAT_TIMEOUT", "10")) verify_ssl = _env_bool("SYNO_CHAT_VERIFY_SSL", True) diff --git a/app/Monitor_connexions.py b/app/Monitor_connexions.py new file mode 100644 index 0000000..277bd7a --- /dev/null +++ b/app/Monitor_connexions.py @@ -0,0 +1,183 @@ +import os +import time +import logging +from typing import Optional +import json +import pymysql +import requests +from dotenv import load_dotenv + +load_dotenv() + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s | %(levelname)s | %(message)s" +) +log = logging.getLogger("journal_connexions") + + +def env_str(name: str, default: Optional[str] = None) -> Optional[str]: + value = os.getenv(name, default) + if value is None: + return None + value = value.strip() + return value if value else default + + +DB_CONFIG = { + "host": env_str("DB_HOST"), + "port": int(env_str("DB_PORT", "3306")), + "user": env_str("DB_USER2"), + "password": env_str("DB_PASS2"), + "database": env_str("DB_NAME2", "Acces"), + "charset": "utf8mb4", + "cursorclass": pymysql.cursors.DictCursor, + "autocommit": True, +} + +SYNO_CHAT_WEBHOOK = env_str("SYNO_CHAT_WEBHOOK_CONNEXIONS") +SYNO_CHAT_BOTNAME = env_str("SYNO_CHAT_BOTNAME_CONNEXIONS", "Journal Connexions") +POLL_INTERVAL = int(env_str("POLL_INTERVAL", "10")) + + +def get_connection(): + return pymysql.connect(**DB_CONFIG) + + +def format_message(row: dict) -> str: + return ( + f"[Connexion MySQL]\n" + f"Utilisateur : {row.get('NomUtilisateur', '')}\n" + f"Poste : {row.get('PosteClient', '')}\n" + f"Tableur : {row.get('TableurSource', '')}\n" + f"Windows : {row.get('UtilisateurWindows', '')}\n" + f"Site : {row.get('SiteDemande', '')}\n" + f"Service : {row.get('ServiceDemande', '')}\n" + f"DSN : {row.get('DSN', '')}\n" + f"BDD : {row.get('BDD', '')}\n" + f"Statut : {row.get('Statut', '')}\n" + f"Motif : {row.get('Motif', '')}\n" + f"Heure : {row.get('DateHeure', '')}\n" + f"Session : {row.get('SessionID', '')}" + ) + + +def send_synology_chat(message: str) -> None: + if not SYNO_CHAT_WEBHOOK: + raise RuntimeError("SYNO_CHAT_WEBHOOK_CONNEXIONS non configuré") + + syno_payload = { + "text": message + } + + response = requests.post( + SYNO_CHAT_WEBHOOK, + data={"payload": json.dumps(syno_payload, ensure_ascii=False)}, + timeout=10 + ) + + log.info("Synology Chat HTTP=%s body=%s", response.status_code, response.text) + + response.raise_for_status() + + body = response.json() + if not body.get("success", False): + raise RuntimeError(f"Synology Chat erreur: {body}") + + +def fetch_pending_rows(conn) -> list[dict]: + sql = """ + SELECT + Id_Journal, + DateHeure, + NomUtilisateur, + PosteClient, + TableurSource, + UtilisateurWindows, + SiteDemande, + ServiceDemande, + DSN, + BDD, + Statut, + Motif, + SessionID + FROM `Acces`.`JournalConnexions` + WHERE NotificationEnvoyee = 0 + ORDER BY DateHeure ASC, Id_Journal ASC + LIMIT 50 + """ + print("SQL fetch_pending_rows =") + print(sql) + + with conn.cursor() as cur: + cur.execute(sql) + return cur.fetchall() + + +def mark_sent(conn, row_id: int) -> None: + sql = """ + UPDATE Acces.JournalConnexions + SET NotificationEnvoyee = 1, + DateNotification = NOW(), + ErreurNotification = NULL + WHERE Id_Journal = %s + """ + with conn.cursor() as cur: + cur.execute(sql, (row_id,)) + + +def mark_error(conn, row_id: int, error_msg: str) -> None: + sql = """ + UPDATE `Acces`.`JournalConnexions` + SET ErreurNotification = %s + WHERE Id_Journal = %s + """ + with conn.cursor() as cur: + cur.execute(sql, (error_msg[:255], row_id)) + + +def process_once() -> None: + with get_connection() as conn: + rows = fetch_pending_rows(conn) + + if not rows: + log.info("Aucune nouvelle connexion à notifier.") + return + + log.info("%s connexion(s) à notifier.", len(rows)) + + for row in rows: + row_id = row["Id_Journal"] + try: + message = format_message(row) + send_synology_chat(message) + mark_sent(conn, row_id) + log.info("Notification envoyée pour Id_Journal=%s", row_id) + + except Exception as exc: + log.exception("Erreur d'envoi pour Id_Journal=%s", row_id) + try: + mark_error(conn, row_id, str(exc)) + except Exception: + log.exception("Impossible d'écrire ErreurNotification pour Id_Journal=%s", row_id) + + +def main(): + missing = [k for k, v in DB_CONFIG.items() if v is None and k != "port"] + if missing: + raise RuntimeError(f"Variables d'environnement manquantes : {', '.join(missing)}") + + if not SYNO_CHAT_WEBHOOK: + raise RuntimeError("SYNO_CHAT_WEBHOOK_CONNEXIONS manquant") + + log.info("Surveillance de JournalConnexions démarrée.") + while True: + try: + process_once() + except Exception: + log.exception("Erreur générale dans la boucle de surveillance") + time.sleep(POLL_INTERVAL) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/app/surveillance_releves.py b/app/surveillance_releves.py index 8f3b48f..95398d8 100644 --- a/app/surveillance_releves.py +++ b/app/surveillance_releves.py @@ -57,10 +57,12 @@ TABLES_SET = set(TABLES) DELAI_MINUTES = 15 RAPPEL_HEURES = 6 -SYNOLOGY_CHAT_WEBHOOK_URL = os.getenv("SYNOLOGY_CHAT_WEBHOOK_URL", "").strip() -SYNOLOGY_CHAT_VERIFY_SSL = os.getenv("SYNOLOGY_CHAT_VERIFY_SSL", "true").strip().lower() in ( - "1", "true", "yes", "on" -) +def _env_str(name: str, default: str = "") -> str: + return (os.getenv(name, default) or "").strip() + +def _env_bool(name: str, default: bool) -> bool: + value = _env_str(name, "1" if default else "0").lower() + return value in ("1", "true", "yes", "on") # ============================================================ @@ -201,19 +203,33 @@ def should_send_alert(site: str) -> bool: # NOTIFICATIONS # ============================================================ -def envoyer_chat(titre: str, message: str) -> None: - if not SYNOLOGY_CHAT_WEBHOOK_URL: - logging.warning("Webhook Synology Chat non configuré : notification Chat ignorée.") +def envoyer_chat(site: str, titre: str, message: str) -> None: + webhook = ( + _env_str(f"SYNO_CHAT_WEBHOOK_MONITOR_{site}") or + _env_str(f"SYNO_CHAT_WEBHOOK_MONITOR_{site.upper()}") or + _env_str("SYNO_CHAT_WEBHOOK_MONITOR") or + _env_str(f"SYNO_CHAT_WEBHOOK_{site}") or + _env_str(f"SYNO_CHAT_WEBHOOK_{site.upper()}") or + _env_str("SYNO_CHAT_WEBHOOK") + ) + + if not webhook: + logging.warning(f"Webhook Synology Chat monitor non configuré pour {site} : notification Chat ignorée.") return + verify_ssl = _env_bool("SYNO_CHAT_VERIFY_SSL", True) + botname = _env_str("SYNO_CHAT_BOTNAME_MONITOR", "Injection données dans tables") + texte = f"{titre}\n{message}" - payload = {"text": texte} + payload: dict[str, str] = {"text": texte} + if botname: + payload["username"] = botname response = requests.post( - SYNOLOGY_CHAT_WEBHOOK_URL, + webhook, data={"payload": json.dumps(payload, ensure_ascii=False)}, - timeout=10, - verify=SYNOLOGY_CHAT_VERIFY_SSL, + timeout=int(_env_str("SYNO_CHAT_TIMEOUT", "10")), + verify=verify_ssl, ) response.raise_for_status() @@ -224,10 +240,10 @@ def envoyer_chat(titre: str, message: str) -> None: except ValueError: pass - logging.info("💬 Notification Synology Chat envoyée.") + logging.info("💬 Notification Synology Chat envoyée pour %s.", site) -def envoyer_notifications(sujet: str, message: str) -> None: +def envoyer_notifications(site: str, sujet: str, message: str) -> None: """ Envoie mail + chat. Lève une erreur si au moins un des deux canaux échoue. @@ -241,7 +257,7 @@ def envoyer_notifications(sujet: str, message: str) -> None: erreurs.append(f"mail: {e}") try: - envoyer_chat(sujet, message) + envoyer_chat(site, sujet, message) except Exception as e: erreurs.append(f"chat: {e}") @@ -293,6 +309,7 @@ def traiter_table(cursor, table: str, limite: datetime, if should_send_alert(table): try: envoyer_notifications( + table, f"⚠️ ALERTE : erreur SQL sur {table}", f"Erreur SQL détectée sur la table {table}.\n\nDétail :\n{e}" ) @@ -312,6 +329,7 @@ def traiter_table(cursor, table: str, limite: datetime, logging.warning(f"⚠️ {table} en défaut (dernier relevé : {last_update})") try: envoyer_notifications( + table, f"⚠️ ALERTE : {table} absence de relevés", f"Pas de relevés depuis plus de {DELAI_MINUTES} min.\nDernier relevé : {last_update}" ) @@ -339,6 +357,7 @@ def traiter_table(cursor, table: str, limite: datetime, message = f"✅ {table} : relevés à nouveau reçus (dernier : {last_update}). Situation normale." try: envoyer_notifications( + table, f"✅ OK : {table} relevés reçus", message ) @@ -381,6 +400,7 @@ def main() -> None: logging.error(f"MySQL KO : {e}") try: envoyer_notifications( + "GLOBAL", "⚠️ ALERTE : Base MySQL inaccessible", "Connexion MySQL impossible : la surveillance des relevés ne peut pas s’exécuter." ) diff --git a/requirements.txt b/requirements.txt index b9424ed5a9c15e868a1909d9beb16be31c73658c..9c76af0f596aeea5978315b8c34aff018f9f21f3 100644 GIT binary patch delta 34 ocmew&wnKbF2{&&6LnT8l5EnBPGUQA)