Skip to content

Commit 2981a66

Browse files
committed
tests: Add a logging config topotest
Test logging configuration variations including syslog. Signed-off-by: Christian Hopps <[email protected]>
1 parent ea8af88 commit 2981a66

8 files changed

Lines changed: 459 additions & 1 deletion

File tree

Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
# -*- coding: utf-8 eval: (blacken-mode 1) -*-
2+
#
3+
# June 16 2025, Christian Hopps <[email protected]>
4+
#
5+
# Copyright (c) 2025, LabN Consulting, L.L.C.
6+
#
7+
"""Common tests and utils for testing log output."""
8+
import logging
9+
import os
10+
import re
11+
import time
12+
13+
import pytest
14+
from lib.common_config import step
15+
from munet.testing.util import readline, retry
16+
from munet.watchlog import WatchLog
17+
18+
our_pid = os.getpid()
19+
daemons = ["mgmtd", "ospfd", "staticd"]
20+
stdout_logs = {}
21+
stderr_logs = {}
22+
daemon_logs = {}
23+
file_logs = {}
24+
25+
26+
class WatchSyslog(WatchLog):
27+
"""An object for watching the syslog (journalctl)."""
28+
29+
def __init__(self, host, extra_args="", encoding="utf-8"):
30+
"""Watch the syslog (journalctl)."""
31+
self.content = ""
32+
super().__init__("None", encoding)
33+
self.host = host
34+
self.p = host.popen(f"journalctl --follow --quiet --since=now {extra_args}")
35+
self.snapshot()
36+
37+
def update_content(self):
38+
"""Update our content with the output from journalctl."""
39+
self.host.cmd_raises("journalctl --sync --flush")
40+
newcontent = ""
41+
while True:
42+
s = readline(self.p.stdout, 1)
43+
if s is None:
44+
break
45+
newcontent += s
46+
self.content += newcontent
47+
return newcontent
48+
49+
50+
def setup_test(munet):
51+
"""Initialize watchlogs for the test."""
52+
r1 = munet.hosts["r1"]
53+
54+
logging.debug("%s rundir is %s", r1.name, r1.rundir)
55+
for daemon in daemons:
56+
stdout_logs[daemon] = WatchLog(r1.rundir / f"{daemon}.out")
57+
stderr_logs[daemon] = WatchLog(r1.rundir / f"{daemon}.err")
58+
daemon_logs[daemon] = WatchLog(r1.rundir / f"{daemon}.log")
59+
60+
61+
def _update_content(logs):
62+
for daemon in logs:
63+
logs[daemon].update_content()
64+
return {k: v.from_mark(v.last_snap_mark) for k, v in logs.items()}
65+
66+
67+
def _update_snaps(logs):
68+
for log in logs.values():
69+
log.last_snap_mark = len(log.content)
70+
71+
72+
@retry(retry_timeout=8, retry_sleep=0.1)
73+
def scan_log(logs, regex):
74+
"""Scan the WatchLog `log` for a regex match."""
75+
# Get latest content since last snapshot for all logs
76+
contents = _update_content(logs)
77+
78+
# Check for regex match in each log
79+
if hasattr(contents, "items"):
80+
for daemon, content in contents.items():
81+
if re.search(regex, content):
82+
break
83+
else:
84+
return f"{regex} not found in daemon logs: {contents}"
85+
else:
86+
if not re.search(regex, contents):
87+
return f"{regex} not found in '{contents}'"
88+
89+
90+
def scan_log_notfound(logs, regex):
91+
"""Scan the WatchLogs `logs` for no regex match."""
92+
# Get latest content since last snapshot for all logs
93+
contents = _update_content(logs)
94+
95+
# Check for regex match in each log
96+
for daemon in contents:
97+
if re.search(regex, contents[daemon]):
98+
logging.debug(
99+
"%s found in %s log content: '%s'", regex, daemon, contents[daemon]
100+
)
101+
return f"{regex} found in {daemon} log '{contents[daemon]}'"
102+
103+
104+
def _do_test_log_notfound(r1, s, logs, level="", delay=2):
105+
cmd = f"send log level {level}" if level else "send log"
106+
regex = f"(?<!{cmd} )" + re.escape(s)
107+
108+
# Check log first before sending command
109+
error = scan_log_notfound(logs, regex)
110+
assert not error
111+
112+
# Log `s` and verify it still doesn't appear
113+
r1.cmd_raises(f"vtysh -c '{cmd} {s}'")
114+
115+
# Wait delay seconds to give a chance for the looked for item to show up. Normally
116+
# non-retry delays are bad, but in the negative check case we need to wait.
117+
time.sleep(delay)
118+
119+
error = scan_log_notfound(logs, regex)
120+
if not error:
121+
_update_snaps(logs)
122+
assert not error, "Found the match"
123+
124+
125+
def _do_test_log(r1, s, logs, level=""):
126+
cmd = f"send log level {level}" if level else "send log"
127+
regex = f"(?<!{cmd} )" + re.escape(s)
128+
129+
# Verify `s` not in the logs to-date.
130+
contents = _update_content(logs)
131+
for daemon in contents:
132+
assert not re.search(regex, contents[daemon])
133+
134+
# Log `s` and verify it appears in each of the logs
135+
# Log `s` and verify it still doesn't appear
136+
r1.cmd_raises(f"vtysh -c '{cmd} {s}'")
137+
138+
error = scan_log(logs, regex)
139+
if not error:
140+
_update_snaps(logs)
141+
assert not error, f"Error: {error}"
142+
143+
144+
def do_test_log(unet, topotest_started=False):
145+
"""Test logging functionality."""
146+
r1 = unet.hosts["r1"]
147+
148+
if topotest_started:
149+
# Need this until topotest stops clearing cmdline targets and resetting per
150+
# daemon logfiles
151+
r1.cmd_raises('vtysh -c "conf\nlog file frr.log"')
152+
153+
r1.cmd_raises('vtysh -c "conf\nno log stdout\nno log commands"')
154+
155+
# Test logfiles
156+
step("Testing logging", reset=True)
157+
158+
step("Testing common frr.log from startup config")
159+
common_log = {"common": WatchLog(r1.rundir / "frr.log")}
160+
s = f"LOGFILE-0-{our_pid}-MARK"
161+
_do_test_log(r1, s, common_log)
162+
163+
if not topotest_started:
164+
# topotest launch will clear the log files, until fixed only munet launch works
165+
step("Testing daemon logfiles from cmdline")
166+
s = f"LOGFILE-1-{our_pid}-MARK"
167+
_do_test_log(r1, s, daemon_logs)
168+
169+
# Clear cmdline targets which will have the --log=file:daemon.log setup
170+
# which we just tested
171+
step("Clearing cmdline log targets")
172+
r1.cmd_raises('vtysh -c "clear log cmdline-targets"')
173+
174+
step("Testing daemon logfiles from cmdline no longer used")
175+
s = f"LOGFILE-2-{our_pid}-MARK"
176+
_do_test_log_notfound(r1, s, daemon_logs)
177+
178+
# Test stdout
179+
step("Testing stdout")
180+
r1.cmd_raises('vtysh -c "conf" -c "log stdout"')
181+
182+
s = f"STDOUT-0-{our_pid}-MARK"
183+
_do_test_log(r1, s, stdout_logs)
184+
185+
# Test disable stdout
186+
step("Testing NO stdout")
187+
r1.cmd_raises('vtysh -c "conf" -c "no log stdout"')
188+
189+
s = f"STDOUT-1-{our_pid}-MARK"
190+
_do_test_log_notfound(r1, s, stdout_logs)
191+
192+
# Test new common logfile
193+
step("Testing new common logfile frr2.log")
194+
195+
r1.cmd_raises('vtysh -c "conf\nlog file frr2.log"')
196+
common_log = {"common": WatchLog(r1.rundir / "frr2.log")}
197+
198+
s = f"LOGFILE-4-{our_pid}-MARK"
199+
_do_test_log(r1, s, common_log)
200+
201+
step("Testing new daemon logfile staticd2.log")
202+
203+
r1.cmd_raises('vtysh -c "conf\nlog daemon staticd file staticd2.log"')
204+
staticd_log = {"staticd": WatchLog(r1.rundir / "staticd2.log")}
205+
s = f"LOGFILE-5-{our_pid}-MARK"
206+
_do_test_log(r1, s, staticd_log)
207+
208+
209+
def do_test_filter_file(unet):
210+
"""Test filtered logging functionality."""
211+
r1 = unet.hosts["r1"]
212+
213+
# Test new filtered logfile
214+
step("Testing filtered logfile frr-filtered.log")
215+
r1.cmd_raises('vtysh -c "conf\nlog filtered-file frr-filtered.log"')
216+
r1.cmd_raises('vtysh -c "conf\nlog filter-text MARK-FOO"')
217+
filtered_log = {"filtered": WatchLog(r1.rundir / "frr-filtered.log")}
218+
219+
s = f"LOGFILTER-0-{our_pid}-MARK-FOO"
220+
_do_test_log(r1, s, filtered_log)
221+
s = f"LOGFILTER-1-{our_pid}-MARK-BAR"
222+
_do_test_log_notfound(r1, s, filtered_log)
223+
224+
225+
def do_test_syslog(unet):
226+
"""Test syslo functionality."""
227+
r1 = unet.hosts["r1"]
228+
229+
if not r1.get_exec_path_host("journalctl"):
230+
pytest.skip("Skipping syslog test as journalctl not found")
231+
return
232+
233+
step("Testing logging with syslog", reset=True)
234+
235+
#
236+
# Watch all sys
237+
#
238+
logs = {"syslog": WatchSyslog(r1)}
239+
240+
# Test syslog
241+
step("Testing syslog")
242+
r1.cmd_raises('vtysh -c "conf" -c "log syslog"')
243+
244+
s = f"SYSLOG-0-{our_pid}-MARK"
245+
_do_test_log(r1, s, logs)
246+
247+
# Test disable syslog
248+
step("Testing NO syslog")
249+
r1.cmd_raises('vtysh -c "conf" -c "no log syslog"')
250+
251+
s = f"SYSLOG-1-{our_pid}-MARK"
252+
_do_test_log_notfound(r1, s, logs)
253+
254+
#
255+
# switch to watching only severity >= info
256+
#
257+
258+
step("Testing syslog send and filter to 'info'")
259+
s = f"SYSLOG-2-{our_pid}-MARK"
260+
r1.cmd_raises('vtysh -c "conf" -c "log syslog info"')
261+
_do_test_log(r1, s, logs, "info")
262+
263+
step("Testing syslog send 'debug' and filter to 'info'")
264+
s = f"SYSLOG-3-{our_pid}-MARK"
265+
_do_test_log_notfound(r1, s, logs, "debug")
266+
267+
step("Testing set syslog filter level to 'debug'")
268+
s = f"SYSLOG-4-{our_pid}-MARK"
269+
r1.cmd_raises('vtysh -c "conf" -c "log syslog debug"')
270+
_do_test_log(r1, s, logs, "debug")
271+
272+
step("Testing set syslog to default filter level")
273+
s = f"SYSLOG-5-{our_pid}-MARK"
274+
r1.cmd_raises('vtysh -c "conf" -c "log syslog"')
275+
_do_test_log(r1, s, logs)
276+
277+
#
278+
# switch to watching only syslog facility user
279+
#
280+
logs = {"syslog": WatchSyslog(r1, extra_args="--facility=user")}
281+
282+
step("Testing syslog facility 'user' (capturing facility 'user')")
283+
s = f"SYSLOG-6-{our_pid}-MARK"
284+
# r1.cmd_raises('vtysh -c "conf" -c "log facility user" -c "log syslog"')
285+
r1.cmd_raises('vtysh -c "conf" -c "log syslog" -c "log facility user"')
286+
_do_test_log(r1, s, logs)
287+
288+
step("Testing syslog facility 'daemon'")
289+
s = f"SYSLOG-7-{our_pid}-MARK"
290+
r1.cmd_raises('vtysh -c "conf" -c "no log facility"')
291+
_do_test_log_notfound(r1, s, logs)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
version: 1
2+
topology:
3+
ipv6-enable: false
4+
networks-autonumber: true
5+
networks:
6+
- name: net1
7+
nodes:
8+
- name: r1
9+
connections: ["net1"]
10+
ready-cmd: |
11+
# Wait for log files to show up
12+
test -e %RUNDIR%/mgmtd.log
13+
test -e %RUNDIR%/ospfd.log
14+
test -e %RUNDIR%/staticd.log
15+
test -e %RUNDIR%/zebra.log
16+
cmd: |
17+
chown frr:frr -R %RUNDIR%
18+
chown frr:frr -R /var/run/frr
19+
chown frr:frr -R /var/log/frr
20+
/usr/lib/frr/frrinit.sh start
21+
cleanup-cmd: |
22+
/usr/lib/frr/frrinit.sh stop
23+
volumes:
24+
- "./%NAME%:/etc/frr"
25+
- "%RUNDIR%/test-mount:/var/tmp/frr"
26+
- "%RUNDIR%/var.log.frr:/var/log/frr"
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
ospfd=1
2+
staticd=1
3+
vtysh_enable=1
4+
watchfrr_enable=1
5+
zebra=1
6+
7+
mgmtd_options="--log=file:mgmtd.log > mgmtd.out 2> mgmtd.err"
8+
ospfd_options="--log=file:ospfd.log > ospfd.out 2> ospfd.err"
9+
staticd_options="--log=file:staticd.log > staticd.out 2> staticd.err"
10+
zebra_options="--log=file:zebra.log > zebra.out 2> zebra.err"
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
log timestamp precision 6
2+
log file frr.log
3+
4+
5+
debug northbound notifications
6+
!! debug northbound libyang
7+
debug northbound events
8+
debug northbound callbacks
9+
10+
debug mgmt backend datastore frontend transaction
11+
debug mgmt client frontend
12+
debug mgmt client backend
13+
14+
no debug memstats-at-exit
15+
16+
interface eth0
17+
ip ospf hello-interval 2
18+
ip ospf dead-interval 7
19+
ip ospf area 0.0.0.0
20+
exit
21+
22+
ip route 11.11.11.11/32 lo
23+
24+
router ospf
25+
ospf router-id 1.1.1.1
26+
!
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
service integrated-vtysh-config

0 commit comments

Comments
 (0)