From 4b62583937141faf4d9b6478a91b7923e9e66866 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 14 Aug 2026 18:02:32 +0000 Subject: [PATCH] Gate watchdogs on integration configuration --- app_new/health/routes.py | 29 ++++++++++-- app_new/outlook/dashboard.py | 6 +-- app_new/yeastar/routes.py | 4 +- docker-compose.override.yml | 1 + docker/supervisord.conf | 16 +++---- watchdog_gate.py | 91 ++++++++++++++++++++++++++++++++++++ 6 files changed, 129 insertions(+), 18 deletions(-) create mode 100644 watchdog_gate.py diff --git a/app_new/health/routes.py b/app_new/health/routes.py index a6aa13c..d856572 100644 --- a/app_new/health/routes.py +++ b/app_new/health/routes.py @@ -50,6 +50,21 @@ def _check_database(): return {'connected': False, 'error': str(e)} +def _check_integration_watchdog(process_name, integration, start_time_threshold=None): + """Un watchdog non configure est volontairement arrete et reste sain.""" + try: + from watchdog_gate import integration_is_configured + + if not integration_is_configured(integration): + return {'running': False, 'enabled': False} + except Exception as exc: + return {'running': False, 'enabled': False, 'configuration_error': str(exc)} + + status = _check_watchdog(process_name, start_time_threshold=start_time_threshold) + status['enabled'] = True + return status + + @health_bp.route('/') def health(): """Endpoint public de healthcheck.""" @@ -58,14 +73,18 @@ def health(): # Le watchdog pronote met parfois plus de temps a demarrer (connexion ENT) watchdogs = { 'flask': _check_watchdog('gunicorn'), - 'gmao_watchdog': _check_watchdog('gmao_watchdog.py'), - 'ent_watchdog': _check_watchdog('ent_watchdog.py'), - 'pronote_watchdog': _check_watchdog('pronote_watchdog.py', start_time_threshold=WATCHDOG_STARTUP_GRACE_SECONDS), - 'watchdog_dnd': _check_watchdog('watchdog_dnd.py'), + 'gmao_watchdog': _check_integration_watchdog('gmao_watchdog.py', 'outlook'), + 'ent_watchdog': _check_integration_watchdog('ent_watchdog.py', 'ent'), + 'pronote_watchdog': _check_integration_watchdog( + 'pronote_watchdog.py', + 'pronote', + start_time_threshold=WATCHDOG_STARTUP_GRACE_SECONDS, + ), + 'watchdog_dnd': _check_integration_watchdog('watchdog_dnd.py', 'yeastar'), } all_watchdogs_ok = all( - w.get('running') or w.get('starting') + w.get('enabled') is False or w.get('running') or w.get('starting') for w in watchdogs.values() ) healthy = db_status.get('connected') and all_watchdogs_ok and not any(w.get('starting') for w in watchdogs.values()) diff --git a/app_new/outlook/dashboard.py b/app_new/outlook/dashboard.py index add1bff..0af38fe 100644 --- a/app_new/outlook/dashboard.py +++ b/app_new/outlook/dashboard.py @@ -722,13 +722,13 @@ def api_save_auto_interpret(): if gmao_context.auto_interpret_enabled: # Relancer GMAO watchdog gmao_script = '/app/gmao_watchdog.py' - subprocess.Popen(['python3', gmao_script], + subprocess.Popen(['python3', '/app/watchdog_gate.py', 'outlook', gmao_script], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) # Relancer ENT watchdog ent_script = '/app/ent_watchdog.py' - subprocess.Popen(['python3', ent_script], + subprocess.Popen(['python3', '/app/watchdog_gate.py', 'ent', ent_script], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) @@ -913,7 +913,7 @@ def api_pronote_watchdog_restart(): # Démarrer le nouveau subprocess.Popen( - ['python3', 'pronote_watchdog.py'], + ['python3', '/app/watchdog_gate.py', 'pronote', '/app/pronote_watchdog.py'], cwd=workdir, stdout=open('/tmp/pronote_watchdog.out', 'a'), stderr=open('/tmp/pronote_watchdog.err', 'a') diff --git a/app_new/yeastar/routes.py b/app_new/yeastar/routes.py index d1acc5d..a20693c 100644 --- a/app_new/yeastar/routes.py +++ b/app_new/yeastar/routes.py @@ -227,7 +227,7 @@ def dnd_config_page(): pass if config.is_enabled: script_path = '/app/watchdog_dnd.py' - subprocess.Popen(['python3', script_path], + subprocess.Popen(['python3', '/app/watchdog_gate.py', 'yeastar', script_path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) flash("Configuration DND sauvegardée", "success") @@ -330,7 +330,7 @@ def api_watchdog_restart(): # Relancer script_path = '/app/watchdog_dnd.py' - subprocess.Popen(['python3', script_path], + subprocess.Popen(['python3', '/app/watchdog_gate.py', 'yeastar', script_path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 3d65ac4..b83d90b 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -13,6 +13,7 @@ services: - ./ent_watchdog.py:/app/ent_watchdog.py - ./pronote_watchdog.py:/app/pronote_watchdog.py - ./watchdog_dnd.py:/app/watchdog_dnd.py + - ./watchdog_gate.py:/app/watchdog_gate.py:ro - ./run_app_new.py:/app/run_app_new.py - ./wsgi.py:/app/wsgi.py - ./VERSION:/app/VERSION:ro diff --git a/docker/supervisord.conf b/docker/supervisord.conf index 0fe27ae..09f1b88 100644 --- a/docker/supervisord.conf +++ b/docker/supervisord.conf @@ -17,36 +17,36 @@ stderr_logfile=/dev/fd/2 stderr_logfile_maxbytes=0 [program:gmao_watchdog] -command=python3 /app/gmao_watchdog.py +command=python3 /app/watchdog_gate.py outlook /app/gmao_watchdog.py environment=DOCKER_GMAO_WORKDIR="/app",DOCKER_GMAO_ENVFILE="/app/.env.docker",PYTHONPATH="/app",HOME="/home/gmao" directory=/app user=gmao autostart=true -autorestart=true +autorestart=false stdout_logfile=/dev/fd/1 stdout_logfile_maxbytes=0 stderr_logfile=/dev/fd/2 stderr_logfile_maxbytes=0 [program:ent_watchdog] -command=python3 /app/ent_watchdog.py +command=python3 /app/watchdog_gate.py ent /app/ent_watchdog.py environment=DOCKER_GMAO_WORKDIR="/app",DOCKER_GMAO_ENVFILE="/app/.env.docker",PYTHONPATH="/app",HOME="/home/gmao" directory=/app user=gmao autostart=true -autorestart=true +autorestart=false stdout_logfile=/dev/fd/1 stdout_logfile_maxbytes=0 stderr_logfile=/dev/fd/2 stderr_logfile_maxbytes=0 [program:pronote_watchdog] -command=python3 /app/pronote_watchdog.py +command=python3 /app/watchdog_gate.py pronote /app/pronote_watchdog.py environment=DOCKER_GMAO_WORKDIR="/app",DOCKER_GMAO_ENVFILE="/app/.env.docker",PYTHONPATH="/app",HOME="/home/gmao" directory=/app user=gmao autostart=true -autorestart=true +autorestart=false stdout_logfile=/dev/fd/1 stdout_logfile_maxbytes=0 stderr_logfile=/dev/fd/2 @@ -65,12 +65,12 @@ stderr_logfile=/dev/fd/2 stderr_logfile_maxbytes=0 [program:watchdog_dnd] -command=python3 /app/watchdog_dnd.py +command=python3 /app/watchdog_gate.py yeastar /app/watchdog_dnd.py environment=PYTHONPATH="/app",HOME="/home/gmao" directory=/app user=gmao autostart=true -autorestart=true +autorestart=false stdout_logfile=/dev/fd/1 stdout_logfile_maxbytes=0 stderr_logfile=/dev/fd/2 diff --git a/watchdog_gate.py b/watchdog_gate.py new file mode 100644 index 0000000..11df9db --- /dev/null +++ b/watchdog_gate.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +"""Start an integration watchdog only when its configuration is usable.""" + +import os +import sys + + +def integration_is_configured(name): + import pymysql + from sqlalchemy.engine import make_url + + database_url = make_url(os.environ['DATABASE_URL']) + connection = pymysql.connect( + host=database_url.host, + port=database_url.port or 3306, + user=database_url.username, + password=database_url.password, + database=database_url.database, + connect_timeout=5, + ) + checks = { + "outlook": """ + SELECT EXISTS(SELECT 1 FROM gmao_context WHERE auto_interpret_enabled = 1) + AND EXISTS(SELECT 1 FROM outlook_accounts WHERE is_active = 1) + """, + "ent": """ + SELECT EXISTS(SELECT 1 FROM gmao_context WHERE auto_interpret_enabled = 1) + AND EXISTS(SELECT 1 FROM ent_credentials WHERE is_active = 1) + """, + "pronote": """ + SELECT EXISTS(SELECT 1 FROM gmao_context WHERE pronote_sync_enabled = 1) + AND EXISTS( + SELECT 1 FROM pronote_session + WHERE session_token IS NOT NULL AND session_token != '' + ) + """, + "yeastar": """ + SELECT username_encrypted, password_encrypted + FROM yeastar_config + WHERE is_active = 1 AND base_url != '' + AND EXISTS(SELECT 1 FROM yeastar_dnd_config WHERE is_enabled = 1) + LIMIT 1 + """, + } + if name not in checks: + raise ValueError(f"Integration inconnue: {name}") + try: + with connection.cursor() as cursor: + cursor.execute(checks[name]) + result = cursor.fetchone() + if name != "yeastar": + return bool(result and result[0]) + if not result: + return False + from cryptography.fernet import Fernet + + key = os.environ.get('YEASTAR_ENCRYPTION_KEY') or os.environ.get( + 'OUTLOOK_ENCRYPTION_KEY' + ) + if not key: + return False + try: + cipher = Fernet(key.encode()) + return bool(cipher.decrypt(result[0]) and cipher.decrypt(result[1])) + except Exception: + return False + finally: + connection.close() + + +def main(): + if len(sys.argv) != 3: + raise SystemExit("usage: watchdog_gate.py INTEGRATION SCRIPT") + + integration, script = sys.argv[1:] + try: + configured = integration_is_configured(integration) + except Exception as exc: + print(f"[{integration}] watchdog non lance: configuration illisible ({exc})") + return 0 + + if not configured: + print(f"[{integration}] watchdog non lance: integration non configuree") + return 0 + + print(f"[{integration}] configuration valide, lancement de {script}") + os.execv(sys.executable, [sys.executable, script]) + + +if __name__ == "__main__": + raise SystemExit(main())