]> git.ipfire.org Git - thirdparty/knot-resolver.git/commitdiff
manager: files watchdog: watchdog created specifically for TLS certificate files
authorAleš Mrázek <ales.mrazek@nic.cz>
Mon, 25 Nov 2024 11:51:38 +0000 (12:51 +0100)
committerVladimír Čunát <vladimir.cunat@nic.cz>
Tue, 3 Dec 2024 07:53:33 +0000 (08:53 +0100)
- on_modified: the command is delayed to avoid sending too many
- on_deleted: files watching is stopped rescheduled (replaced file)

python/knot_resolver/manager/files/watchdog.py

index 9cb644a680f643ab5a8450c8ddc8079d01c6a2e5..d365542c7697b9faf4e494b5b0e09bc680453e56 100644 (file)
@@ -1,6 +1,9 @@
 import importlib
 import logging
+import os
+import time
 from pathlib import Path
+from threading import Timer
 from typing import List, Optional, Union
 
 from knot_resolver.controller.registered_workers import command_registered_workers
@@ -16,73 +19,120 @@ if importlib.util.find_spec("watchdog"):
 logger = logging.getLogger(__name__)
 
 
-def files_to_watch(config: KresConfig) -> List[Path]:
+def tls_cert_paths(config: KresConfig) -> List[str]:
     files: List[Optional[File]] = [
         config.network.tls.cert_file,
         config.network.tls.key_file,
     ]
-    return [file.to_path() for file in files if file is not None]
+    return [str(file) for file in files if file is not None]
 
 
 if _watchdog:
     from watchdog.events import (
+        DirDeletedEvent,
         DirModifiedEvent,
+        FileDeletedEvent,
         FileModifiedEvent,
         FileSystemEventHandler,
     )
     from watchdog.observers import Observer
 
-    _files_watchdog: Optional["FilesWatchDog"] = None
+    _tls_cert_watchdog: Optional["TLSCertWatchDog"] = None
 
-    class CertificatesEventHandler(FileSystemEventHandler):
+    class TLSCertEventHandler(FileSystemEventHandler):
+        def __init__(self, cmd: str, delay: int = 5) -> None:
+            self._delay = delay
+            self._timer: Optional[Timer] = None
+            self._cmd = cmd
 
-        def __init__(self, config: KresConfig) -> None:
-            self._config = config
-            self._command = f"net.tls('{config.network.tls.cert_file}', '{config.network.tls.key_file}')"
+        def _reload_cmd(self) -> None:
+            logger.info("Reloading TLS certificate files for the all workers")
+            if compat.asyncio.is_event_loop_running():
+                compat.asyncio.create_task(command_registered_workers(self._cmd))
+            else:
+                compat.asyncio.run(command_registered_workers(self._cmd))
 
-        # def on_any_event(self, event: FileSystemEvent) -> None:
-        #     pass
+        def on_deleted(self, event: Union[DirDeletedEvent, FileDeletedEvent]) -> None:
+            path = str(event.src_path)
+            logger.info(f"Stopped watching '{path}', because it was deleted")
 
-        # def on_created(self, event: Union[DirCreatedEvent, FileCreatedEvent]) -> None:
-        #     pass
+            # do not send command when the file was deleted
+            if self._timer and self._timer.is_alive():
+                self._timer.cancel()
+                self._timer.join()
 
-        # def on_deleted(self, event: Union[DirDeletedEvent, FileDeletedEvent]) -> None:
-        #     pass
+            if _tls_cert_watchdog:
+                _tls_cert_watchdog.reschedule()
 
         def on_modified(self, event: Union[DirModifiedEvent, FileModifiedEvent]) -> None:
-            if compat.asyncio.is_event_loop_running():
-                compat.asyncio.create_task(command_registered_workers(self._command))
-            else:
-                compat.asyncio.run(command_registered_workers(self._command))
-
-        # def on_closed(self, event: FileClosedEvent) -> None:
-        #     pass
-
-    class FilesWatchDog:
-        def __init__(self, config: KresConfig, files: List[Path]) -> None:
+            path = str(event.src_path)
+            logger.info(f"TLS certificate file '{path}' has been modified")
+
+            # skipping if command was already triggered
+            if self._timer and self._timer.is_alive():
+                logger.info(f"Skipping '{path}', reload file already triggered")
+                return
+            # start a new timer
+            self._timer = Timer(self._delay, self._reload_cmd)
+            self._timer.start()
+            logger.info("Delayed reload of TLS certificate files has started")
+
+    class TLSCertWatchDog:
+        def __init__(self, cert_file: Path, key_file: Path) -> None:
             self._observer = Observer()
-            for file in files:
-                self._observer.schedule(CertificatesEventHandler(config), str(file), recursive=False)
-                logger.info(f"Watching '{file}. file")
+            self._cert_file = cert_file
+            self._key_file = key_file
+            self._cmd = f"net.tls('{cert_file}', '{key_file}')"
+
+        def schedule(self) -> None:
+            event_handler = TLSCertEventHandler(self._cmd)
+            logger.info("Schedule watching of TLS certificate files")
+            self._observer.schedule(
+                event_handler,
+                str(self._cert_file),
+                recursive=False,
+            )
+            self._observer.schedule(
+                event_handler,
+                str(self._key_file),
+                recursive=False,
+            )
+
+        def reschedule(self) -> None:
+            self._observer.unschedule_all()
+
+            # wait for files creation
+            while not (os.path.exists(self._cert_file) and os.path.exists(self._key_file)):
+                if os.path.exists(self._cert_file):
+                    logger.error(f"Cannot start watching TLS cert file, '{self._cert_file}' is missing.")
+                if os.path.exists(self._key_file):
+                    logger.error(f"Cannot start watching TLS cert key file, '{self._key_file}' is missing.")
+                time.sleep(1)
+            self.schedule()
 
         def start(self) -> None:
-            if self._observer:
-                self._observer.start()
+            self._observer.start()
 
         def stop(self) -> None:
-            if self._observer:
-                self._observer.stop()
-                self._observer.join()
+            self._observer.stop()
+            self._observer.join()
+
+    @only_on_real_changes_update(tls_cert_paths)
+    async def _init_tls_cert_watchdog(config: KresConfig) -> None:
+        global _tls_cert_watchdog
+        if _tls_cert_watchdog:
+            _tls_cert_watchdog.stop()
 
-    @only_on_real_changes_update(files_to_watch)
-    async def _init_files_watchdog(config: KresConfig) -> None:
-        global _files_watchdog
-        if _files_watchdog is None:
-            logger.info("Starting files WatchDog")
-            _files_watchdog = FilesWatchDog(config, files_to_watch(config))
-            _files_watchdog.start()
+        if config.network.tls.cert_file and config.network.tls.key_file:
+            logger.info("Starting TLS certificate files WatchDog")
+            _tls_cert_watchdog = TLSCertWatchDog(
+                config.network.tls.cert_file.to_path(), config.network.tls.key_file.to_path()
+            )
+            _tls_cert_watchdog.schedule()
+            _tls_cert_watchdog.start()
 
 
 async def init_files_watchdog(config_store: ConfigStore) -> None:
     if _watchdog:
-        await config_store.register_on_change_callback(_init_files_watchdog)
+        # watchdog for TLS certificate files
+        await config_store.register_on_change_callback(_init_tls_cert_watchdog)