Skip to content

Commit 7016be5

Browse files
committed
manager: files: watchdog: added RPZ files
Separate timer for each command.
1 parent 03f29dc commit 7016be5

File tree

5 files changed

+214
-25
lines changed

5 files changed

+214
-25
lines changed

NEWS

+4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ Bugfixes
55
--------
66
- /management/unix-socket: revert to absolute path (#926, !1664)
77

8+
Improvements
9+
------------
10+
- /local-data/rpz/*/watchdog: new configuration to enable watchdog for RPZ files (!1665)
11+
812

913
Knot Resolver 6.0.11 (2025-02-26)
1014
=================================

python/knot_resolver/manager/files/watchdog.py

+70-24
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,29 @@
22
from pathlib import Path
33
from threading import Timer
44
from typing import Any, Dict, List, Optional
5+
from urllib.parse import quote
56

67
from knot_resolver.constants import WATCHDOG_LIB
78
from knot_resolver.controller.registered_workers import command_registered_workers
89
from knot_resolver.datamodel import KresConfig
910
from knot_resolver.manager.config_store import ConfigStore, only_on_real_changes_update
1011
from knot_resolver.utils import compat
12+
from knot_resolver.utils.requests import SocketDesc, request
1113

1214
logger = logging.getLogger(__name__)
1315

16+
FilesToWatch = Dict[Path, Optional[str]]
1417

15-
def tls_cert_files_config(config: KresConfig) -> List[Any]:
18+
19+
def watched_files_config(config: KresConfig) -> List[Any]:
1620
return [
1721
config.network.tls.files_watchdog,
1822
config.network.tls.cert_file,
1923
config.network.tls.key_file,
24+
config.local_data.rpz,
2025
]
2126

2227

23-
FilesToWatch = Dict[Path, str]
24-
25-
2628
if WATCHDOG_LIB:
2729
from watchdog.events import (
2830
FileSystemEvent,
@@ -31,58 +33,96 @@ def tls_cert_files_config(config: KresConfig) -> List[Any]:
3133
from watchdog.observers import Observer
3234

3335
class FilesWatchdogEventHandler(FileSystemEventHandler):
34-
def __init__(self, files: FilesToWatch) -> None:
36+
def __init__(self, files: FilesToWatch, config: KresConfig) -> None:
3537
self._files = files
36-
self._timer: Optional[Timer] = None
38+
self._config = config
39+
self._policy_timer: Optional[Timer] = None
40+
self._timers: Dict[str, Timer] = {}
41+
42+
def _trigger(self, cmd: Optional[str]) -> None:
43+
def policy_reload() -> None:
44+
management = self._config.management
45+
socket = SocketDesc(
46+
f'http+unix://{quote(str(management.unix_socket), safe="")}/',
47+
'Key "/management/unix-socket" in validated configuration',
48+
)
49+
if management.interface:
50+
socket = SocketDesc(
51+
f"http://{management.interface.addr}:{management.interface.port}",
52+
'Key "/management/interface" in validated configuration',
53+
)
54+
55+
response = request(socket, "POST", "renew")
56+
if response.status != 200:
57+
logger.error(f"Failed to reload policy rules: {response.body}")
58+
logger.info("Reloading policy rules has finished")
59+
60+
if not cmd:
61+
# skipping if reload was already triggered
62+
if self._policy_timer and self._policy_timer.is_alive():
63+
logger.info("Skipping reloading policy rules, it was already triggered")
64+
return
65+
# start a 5sec timer
66+
logger.info("Delayed policy rules reload has started")
67+
self._policy_timer = Timer(5, policy_reload)
68+
self._policy_timer.start()
69+
return
3770

38-
def _reload(self, cmd: str) -> None:
3971
def command() -> None:
4072
if compat.asyncio.is_event_loop_running():
4173
compat.asyncio.create_task(command_registered_workers(cmd))
4274
else:
4375
compat.asyncio.run(command_registered_workers(cmd))
44-
logger.info("Reloading of TLS certificate files has finished")
76+
logger.info(f"Sending '{cmd}' command to reload watched files has finished")
4577

46-
# skipping if reload was already triggered
47-
if self._timer and self._timer.is_alive():
48-
logger.info("Skipping TLS certificate files reloading, reload command was already triggered")
78+
# skipping if command was already triggered
79+
if cmd in self._timers and self._timers[cmd].is_alive():
80+
logger.info(f"Skipping sending '{cmd}' command, it was already triggered")
4981
return
5082
# start a 5sec timer
51-
logger.info("Delayed reload of TLS certificate files has started")
52-
self._timer = Timer(5, command)
53-
self._timer.start()
83+
logger.info(f"Delayed send of '{cmd}' command has started")
84+
self._timers[cmd] = Timer(5, command)
85+
self._timers[cmd].start()
5486

5587
def on_created(self, event: FileSystemEvent) -> None:
5688
src_path = Path(str(event.src_path))
5789
if src_path in self._files.keys():
5890
logger.info(f"Watched file '{src_path}' has been created")
59-
self._reload(self._files[src_path])
91+
self._trigger(self._files[src_path])
6092

6193
def on_deleted(self, event: FileSystemEvent) -> None:
6294
src_path = Path(str(event.src_path))
6395
if src_path in self._files.keys():
6496
logger.warning(f"Watched file '{src_path}' has been deleted")
65-
if self._timer:
66-
self._timer.cancel()
97+
cmd = self._files[src_path]
98+
if cmd in self._timers:
99+
self._timers[cmd].cancel()
67100
for file in self._files.keys():
68101
if file.parent == src_path:
69102
logger.warning(f"Watched directory '{src_path}' has been deleted")
70-
if self._timer:
71-
self._timer.cancel()
103+
cmd = self._files[file]
104+
if cmd in self._timers:
105+
self._timers[cmd].cancel()
106+
107+
def on_moved(self, event: FileSystemEvent) -> None:
108+
src_path = Path(str(event.src_path))
109+
if src_path in self._files.keys():
110+
logger.info(f"Watched file '{src_path}' has been moved")
111+
self._trigger(self._files[src_path])
72112

73113
def on_modified(self, event: FileSystemEvent) -> None:
74114
src_path = Path(str(event.src_path))
75115
if src_path in self._files.keys():
76116
logger.info(f"Watched file '{src_path}' has been modified")
77-
self._reload(self._files[src_path])
117+
self._trigger(self._files[src_path])
78118

79119
_files_watchdog: Optional["FilesWatchdog"] = None
80120

81121
class FilesWatchdog:
82-
def __init__(self, files_to_watch: FilesToWatch) -> None:
122+
def __init__(self, files_to_watch: FilesToWatch, config: KresConfig) -> None:
83123
self._observer = Observer()
84124

85-
event_handler = FilesWatchdogEventHandler(files_to_watch)
125+
event_handler = FilesWatchdogEventHandler(files_to_watch, config)
86126
dirs_to_watch: List[Path] = []
87127
for file in files_to_watch.keys():
88128
if file.parent not in dirs_to_watch:
@@ -104,7 +144,7 @@ def stop(self) -> None:
104144
self._observer.join()
105145

106146

107-
@only_on_real_changes_update(tls_cert_files_config)
147+
@only_on_real_changes_update(watched_files_config)
108148
async def _init_files_watchdog(config: KresConfig) -> None:
109149
if WATCHDOG_LIB:
110150
global _files_watchdog
@@ -119,9 +159,15 @@ async def _init_files_watchdog(config: KresConfig) -> None:
119159
files_to_watch[config.network.tls.cert_file.to_path()] = net_tls
120160
files_to_watch[config.network.tls.key_file.to_path()] = net_tls
121161

162+
# local-data.rpz
163+
if config.local_data.rpz:
164+
for rpz in config.local_data.rpz:
165+
if rpz.watchdog:
166+
files_to_watch[rpz.file.to_path()] = None
167+
122168
if files_to_watch:
123169
logger.info("Initializing files watchdog")
124-
_files_watchdog = FilesWatchdog(files_to_watch)
170+
_files_watchdog = FilesWatchdog(files_to_watch, config)
125171
_files_watchdog.start()
126172

127173

python/knot_resolver/manager/server.py

+18
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,14 @@ async def _reload_config(self) -> None:
138138
logger.error(f"Reloading of the configuration file failed: {e}")
139139
logger.error("Configuration have NOT been changed.")
140140

141+
async def _renew_config(self) -> None:
142+
try:
143+
await self.config_store.renew()
144+
logger.info("Configuration successfully renewed")
145+
except KresManagerException as e:
146+
logger.error(f"Renewing the configuration failed: {e}")
147+
logger.error("Configuration have NOT been renewed.")
148+
141149
async def sigint_handler(self) -> None:
142150
logger.info("Received SIGINT, triggering graceful shutdown")
143151
self.trigger_shutdown(0)
@@ -325,6 +333,15 @@ async def _handler_reload(self, _request: web.Request) -> web.Response:
325333
await self._reload_config()
326334
return web.Response(text="Reloading...")
327335

336+
async def _handler_renew(self, _request: web.Request) -> web.Response:
337+
"""
338+
Route handler for renewing the configuration
339+
"""
340+
341+
logger.info("Renewing configuration event triggered...")
342+
await self._renew_config()
343+
return web.Response(text="Renewing configuration...")
344+
328345
async def _handler_processes(self, request: web.Request) -> web.Response:
329346
"""
330347
Route handler for listing PIDs of subprocesses
@@ -359,6 +376,7 @@ def _setup_routes(self) -> None:
359376
web.patch(r"/v1/config{path:.*}", self._handler_config_query),
360377
web.post("/stop", self._handler_stop),
361378
web.post("/reload", self._handler_reload),
379+
web.post("/renew", self._handler_renew),
362380
web.get("/schema", self._handler_schema),
363381
web.get("/schema/ui", self._handle_view_schema),
364382
web.get("/metrics", self._handler_metrics),
+121
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
#!/usr/bin/env bash
2+
3+
set -e
4+
5+
gitroot=$(git rev-parse --show-toplevel)
6+
rpz_file=$gitroot/example.rpz
7+
8+
rpz_example=$(cat <<EOF
9+
\$ORIGIN RPZ.EXAMPLE.ORG.
10+
ok.example.com CNAME rpz-passthru.
11+
EOF
12+
)
13+
# create example RPZ
14+
echo "$rpz_example" >> $rpz_file
15+
16+
rpz_conf=$(cat <<EOF
17+
local-data:
18+
rpz:
19+
- file: $rpz_file
20+
watchdog: false
21+
EOF
22+
)
23+
# add RPZ to config
24+
echo "$rpz_conf" >> /etc/knot-resolver/config.yaml
25+
26+
function count_errors(){
27+
echo "$(journalctl -u knot-resolver.service | grep -c error)"
28+
}
29+
30+
function count_reloads(){
31+
echo "$(journalctl -u knot-resolver.service | grep -c "Reloading policy rules has finished")"
32+
}
33+
34+
# test that RPZ watchdog
35+
# {{
36+
37+
err_count=$(count_errors)
38+
rel_count=$(count_reloads)
39+
40+
# reload config with RPZ configured without watchdog turned on
41+
kresctl reload
42+
sleep 1
43+
if [ $(count_errors) -ne $err_count ] || [ $(count_reloads) -ne $rel_count ]; then
44+
echo "RPZ file watchdog is running (should not) or other errors occurred."
45+
exit 1
46+
fi
47+
48+
# configure RPZ file and turn on watchdog
49+
kresctl config set -p /local-data/rpz/0/watchdog true
50+
sleep 1
51+
if [ "$?" -ne "0" ]; then
52+
echo "Could not turn on RPZ file watchdog."
53+
exit 1
54+
fi
55+
56+
# }}
57+
58+
# test RPZ modification
59+
# {{
60+
61+
# modify RPZ file, it will trigger reload
62+
rel_count=$(count_reloads)
63+
echo "32.1.2.0.192.rpz-client-ip CNAME rpz-passthru." >> $rpz_file
64+
65+
# wait for files reload to finish
66+
sleep 10
67+
68+
if [ $(count_errors) -ne $err_count ] || [ $(count_reloads) -eq $rel_count ]; then
69+
echo "Could not reload modified RPZ file."
70+
exit 1
71+
fi
72+
73+
# }}
74+
75+
# test replacement
76+
# {{
77+
78+
rel_count=$(count_reloads)
79+
80+
# copy RPZ file
81+
cp $rpz_file $rpz_file.new
82+
83+
# edit new files
84+
echo "48.zz.101.db8.2001.rpz-client-ip CNAME rpz-passthru." >> $rpz_file.new
85+
86+
# replace files
87+
cp -f $rpz_file.new $rpz_file
88+
89+
# wait for files reload to finish
90+
sleep 10
91+
92+
if [ $(count_errors) -ne $err_count ] || [ $(count_reloads) -eq $rel_count ]; then
93+
echo "Could not reload replaced RPZ file."
94+
exit 1
95+
fi
96+
97+
# }}
98+
99+
# test recovery from deletion and creation
100+
# {{
101+
102+
rel_count=$(count_reloads)
103+
104+
# backup rpz file
105+
cp $rpz_file $rpz_file.backup
106+
107+
# delete RPZ file
108+
rm $rpz_file
109+
110+
# create cert files
111+
cp -f $rpz_file.backup $rpz_file
112+
113+
# wait for files reload to finish
114+
sleep 10
115+
116+
if [ $(count_errors) -ne $err_count ] || [ $(count_reloads) -eq $rel_count ]; then
117+
echo "Could not reload created RPZ file."
118+
exit 1
119+
fi
120+
121+
# }}

tests/packaging/interactive/watchdog.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ function count_errors(){
2626
}
2727

2828
function count_reloads(){
29-
echo "$(journalctl -u knot-resolver.service | grep -c "Reloading of TLS certificate files has finished")"
29+
echo "$(journalctl -u knot-resolver.service | grep -c "to reload watched files has finished")"
3030
}
3131

3232
# test that files watchdog is turned off

0 commit comments

Comments
 (0)