From 1d473674527c8b337db88321514fa336da445a86 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Ale=C5=A1=20Mr=C3=A1zek?= Date: Tue, 4 Mar 2025 15:13:20 +0100 Subject: [PATCH] manager: files: watchdog: added RPZ files Separate timer for each command. --- NEWS | 4 + .../knot_resolver/manager/files/watchdog.py | 94 ++++++++++---- python/knot_resolver/manager/server.py | 18 +++ tests/packaging/interactive/rpz_watchdog.sh | 121 ++++++++++++++++++ tests/packaging/interactive/watchdog.sh | 2 +- 5 files changed, 214 insertions(+), 25 deletions(-) create mode 100755 tests/packaging/interactive/rpz_watchdog.sh diff --git a/NEWS b/NEWS index 13fd8f428..b53ed68d4 100644 --- a/NEWS +++ b/NEWS @@ -6,6 +6,10 @@ Bugfixes - /management/unix-socket: revert to absolute path (#926, !1664) - fix `tags` when used in /local-data/rules/*/records (!1670) +Improvements +------------ +- /local-data/rpz/*/watchdog: new configuration to enable watchdog for RPZ files (!1665) + Knot Resolver 6.0.11 (2025-02-26) ================================= diff --git a/python/knot_resolver/manager/files/watchdog.py b/python/knot_resolver/manager/files/watchdog.py index b235ca192..e74abec9f 100644 --- a/python/knot_resolver/manager/files/watchdog.py +++ b/python/knot_resolver/manager/files/watchdog.py @@ -2,27 +2,29 @@ import logging from pathlib import Path from threading import Timer from typing import Any, Dict, List, Optional +from urllib.parse import quote from knot_resolver.constants import WATCHDOG_LIB from knot_resolver.controller.registered_workers import command_registered_workers from knot_resolver.datamodel import KresConfig from knot_resolver.manager.config_store import ConfigStore, only_on_real_changes_update from knot_resolver.utils import compat +from knot_resolver.utils.requests import SocketDesc, request logger = logging.getLogger(__name__) +FilesToWatch = Dict[Path, Optional[str]] -def tls_cert_files_config(config: KresConfig) -> List[Any]: + +def watched_files_config(config: KresConfig) -> List[Any]: return [ config.network.tls.files_watchdog, config.network.tls.cert_file, config.network.tls.key_file, + config.local_data.rpz, ] -FilesToWatch = Dict[Path, str] - - if WATCHDOG_LIB: from watchdog.events import ( FileSystemEvent, @@ -31,58 +33,96 @@ if WATCHDOG_LIB: from watchdog.observers import Observer class FilesWatchdogEventHandler(FileSystemEventHandler): - def __init__(self, files: FilesToWatch) -> None: + def __init__(self, files: FilesToWatch, config: KresConfig) -> None: self._files = files - self._timer: Optional[Timer] = None + self._config = config + self._policy_timer: Optional[Timer] = None + self._timers: Dict[str, Timer] = {} + + def _trigger(self, cmd: Optional[str]) -> None: + def policy_reload() -> None: + management = self._config.management + socket = SocketDesc( + f'http+unix://{quote(str(management.unix_socket), safe="")}/', + 'Key "/management/unix-socket" in validated configuration', + ) + if management.interface: + socket = SocketDesc( + f"http://{management.interface.addr}:{management.interface.port}", + 'Key "/management/interface" in validated configuration', + ) + + response = request(socket, "POST", "renew") + if response.status != 200: + logger.error(f"Failed to reload policy rules: {response.body}") + logger.info("Reloading policy rules has finished") + + if not cmd: + # skipping if reload was already triggered + if self._policy_timer and self._policy_timer.is_alive(): + logger.info("Skipping reloading policy rules, it was already triggered") + return + # start a 5sec timer + logger.info("Delayed policy rules reload has started") + self._policy_timer = Timer(5, policy_reload) + self._policy_timer.start() + return - def _reload(self, cmd: str) -> None: def command() -> None: if compat.asyncio.is_event_loop_running(): compat.asyncio.create_task(command_registered_workers(cmd)) else: compat.asyncio.run(command_registered_workers(cmd)) - logger.info("Reloading of TLS certificate files has finished") + logger.info(f"Sending '{cmd}' command to reload watched files has finished") - # skipping if reload was already triggered - if self._timer and self._timer.is_alive(): - logger.info("Skipping TLS certificate files reloading, reload command was already triggered") + # skipping if command was already triggered + if cmd in self._timers and self._timers[cmd].is_alive(): + logger.info(f"Skipping sending '{cmd}' command, it was already triggered") return # start a 5sec timer - logger.info("Delayed reload of TLS certificate files has started") - self._timer = Timer(5, command) - self._timer.start() + logger.info(f"Delayed send of '{cmd}' command has started") + self._timers[cmd] = Timer(5, command) + self._timers[cmd].start() def on_created(self, event: FileSystemEvent) -> None: src_path = Path(str(event.src_path)) if src_path in self._files.keys(): logger.info(f"Watched file '{src_path}' has been created") - self._reload(self._files[src_path]) + self._trigger(self._files[src_path]) def on_deleted(self, event: FileSystemEvent) -> None: src_path = Path(str(event.src_path)) if src_path in self._files.keys(): logger.warning(f"Watched file '{src_path}' has been deleted") - if self._timer: - self._timer.cancel() + cmd = self._files[src_path] + if cmd in self._timers: + self._timers[cmd].cancel() for file in self._files.keys(): if file.parent == src_path: logger.warning(f"Watched directory '{src_path}' has been deleted") - if self._timer: - self._timer.cancel() + cmd = self._files[file] + if cmd in self._timers: + self._timers[cmd].cancel() + + def on_moved(self, event: FileSystemEvent) -> None: + src_path = Path(str(event.src_path)) + if src_path in self._files.keys(): + logger.info(f"Watched file '{src_path}' has been moved") + self._trigger(self._files[src_path]) def on_modified(self, event: FileSystemEvent) -> None: src_path = Path(str(event.src_path)) if src_path in self._files.keys(): logger.info(f"Watched file '{src_path}' has been modified") - self._reload(self._files[src_path]) + self._trigger(self._files[src_path]) _files_watchdog: Optional["FilesWatchdog"] = None class FilesWatchdog: - def __init__(self, files_to_watch: FilesToWatch) -> None: + def __init__(self, files_to_watch: FilesToWatch, config: KresConfig) -> None: self._observer = Observer() - event_handler = FilesWatchdogEventHandler(files_to_watch) + event_handler = FilesWatchdogEventHandler(files_to_watch, config) dirs_to_watch: List[Path] = [] for file in files_to_watch.keys(): if file.parent not in dirs_to_watch: @@ -104,7 +144,7 @@ if WATCHDOG_LIB: self._observer.join() -@only_on_real_changes_update(tls_cert_files_config) +@only_on_real_changes_update(watched_files_config) async def _init_files_watchdog(config: KresConfig) -> None: if WATCHDOG_LIB: global _files_watchdog @@ -119,9 +159,15 @@ async def _init_files_watchdog(config: KresConfig) -> None: files_to_watch[config.network.tls.cert_file.to_path()] = net_tls files_to_watch[config.network.tls.key_file.to_path()] = net_tls + # local-data.rpz + if config.local_data.rpz: + for rpz in config.local_data.rpz: + if rpz.watchdog: + files_to_watch[rpz.file.to_path()] = None + if files_to_watch: logger.info("Initializing files watchdog") - _files_watchdog = FilesWatchdog(files_to_watch) + _files_watchdog = FilesWatchdog(files_to_watch, config) _files_watchdog.start() diff --git a/python/knot_resolver/manager/server.py b/python/knot_resolver/manager/server.py index 879ce80e4..c9b2e6fa1 100644 --- a/python/knot_resolver/manager/server.py +++ b/python/knot_resolver/manager/server.py @@ -138,6 +138,14 @@ class Server: logger.error(f"Reloading of the configuration file failed: {e}") logger.error("Configuration have NOT been changed.") + async def _renew_config(self) -> None: + try: + await self.config_store.renew() + logger.info("Configuration successfully renewed") + except KresManagerException as e: + logger.error(f"Renewing the configuration failed: {e}") + logger.error("Configuration have NOT been renewed.") + async def sigint_handler(self) -> None: logger.info("Received SIGINT, triggering graceful shutdown") self.trigger_shutdown(0) @@ -325,6 +333,15 @@ class Server: await self._reload_config() return web.Response(text="Reloading...") + async def _handler_renew(self, _request: web.Request) -> web.Response: + """ + Route handler for renewing the configuration + """ + + logger.info("Renewing configuration event triggered...") + await self._renew_config() + return web.Response(text="Renewing configuration...") + async def _handler_processes(self, request: web.Request) -> web.Response: """ Route handler for listing PIDs of subprocesses @@ -359,6 +376,7 @@ class Server: web.patch(r"/v1/config{path:.*}", self._handler_config_query), web.post("/stop", self._handler_stop), web.post("/reload", self._handler_reload), + web.post("/renew", self._handler_renew), web.get("/schema", self._handler_schema), web.get("/schema/ui", self._handle_view_schema), web.get("/metrics", self._handler_metrics), diff --git a/tests/packaging/interactive/rpz_watchdog.sh b/tests/packaging/interactive/rpz_watchdog.sh new file mode 100755 index 000000000..910178fab --- /dev/null +++ b/tests/packaging/interactive/rpz_watchdog.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash + +set -e + +gitroot=$(git rev-parse --show-toplevel) +rpz_file=$gitroot/example.rpz + +rpz_example=$(cat <> $rpz_file + +rpz_conf=$(cat <> /etc/knot-resolver/config.yaml + +function count_errors(){ + echo "$(journalctl -u knot-resolver.service | grep -c error)" +} + +function count_reloads(){ + echo "$(journalctl -u knot-resolver.service | grep -c "Reloading policy rules has finished")" +} + +# test that RPZ watchdog +# {{ + +err_count=$(count_errors) +rel_count=$(count_reloads) + +# reload config with RPZ configured without watchdog turned on +kresctl reload +sleep 1 +if [ $(count_errors) -ne $err_count ] || [ $(count_reloads) -ne $rel_count ]; then + echo "RPZ file watchdog is running (should not) or other errors occurred." + exit 1 +fi + +# configure RPZ file and turn on watchdog +kresctl config set -p /local-data/rpz/0/watchdog true +sleep 1 +if [ "$?" -ne "0" ]; then + echo "Could not turn on RPZ file watchdog." + exit 1 +fi + +# }} + +# test RPZ modification +# {{ + +# modify RPZ file, it will trigger reload +rel_count=$(count_reloads) +echo "32.1.2.0.192.rpz-client-ip CNAME rpz-passthru." >> $rpz_file + +# wait for files reload to finish +sleep 10 + +if [ $(count_errors) -ne $err_count ] || [ $(count_reloads) -eq $rel_count ]; then + echo "Could not reload modified RPZ file." + exit 1 +fi + +# }} + +# test replacement +# {{ + +rel_count=$(count_reloads) + +# copy RPZ file +cp $rpz_file $rpz_file.new + +# edit new files +echo "48.zz.101.db8.2001.rpz-client-ip CNAME rpz-passthru." >> $rpz_file.new + +# replace files +cp -f $rpz_file.new $rpz_file + +# wait for files reload to finish +sleep 10 + +if [ $(count_errors) -ne $err_count ] || [ $(count_reloads) -eq $rel_count ]; then + echo "Could not reload replaced RPZ file." + exit 1 +fi + +# }} + +# test recovery from deletion and creation +# {{ + +rel_count=$(count_reloads) + +# backup rpz file +cp $rpz_file $rpz_file.backup + +# delete RPZ file +rm $rpz_file + +# create cert files +cp -f $rpz_file.backup $rpz_file + +# wait for files reload to finish +sleep 10 + +if [ $(count_errors) -ne $err_count ] || [ $(count_reloads) -eq $rel_count ]; then + echo "Could not reload created RPZ file." + exit 1 +fi + +# }} diff --git a/tests/packaging/interactive/watchdog.sh b/tests/packaging/interactive/watchdog.sh index ffc76e921..104bbdd64 100755 --- a/tests/packaging/interactive/watchdog.sh +++ b/tests/packaging/interactive/watchdog.sh @@ -26,7 +26,7 @@ function count_errors(){ } function count_reloads(){ - echo "$(journalctl -u knot-resolver.service | grep -c "Reloading of TLS certificate files has finished")" + echo "$(journalctl -u knot-resolver.service | grep -c "to reload watched files has finished")" } # test that files watchdog is turned off -- 2.47.2