diff options
| author | Adam Schultz <adam.schultz@live.com> | 2026-01-24 09:55:14 -0500 |
|---|---|---|
| committer | Adam Schultz <adam.schultz@live.com> | 2026-02-09 22:59:37 -0500 |
| commit | 696df602557dd7b70b353c3b9cfb110c0908a2fe (patch) | |
| tree | d7ec98633981069f21b0af09575bf1e53cf79f56 | |
| parent | 9707b90b9ddacd27b74ef235a6f5a8f585c2bc50 (diff) | |
| download | vyos-1x-696df602557dd7b70b353c3b9cfb110c0908a2fe.tar.gz vyos-1x-696df602557dd7b70b353c3b9cfb110c0908a2fe.zip | |
T7756: Fix rsyslog configuration generation
1.) Fix both syslog and logrotate writing /etc/logrotate.d/vyos-rsyslog
- system_syslog.py generates /etc/logrotate.d/vyos-rsyslog-user for user file
logrotate
- system_logs.py generates /etc/logrotate.d/vyos-rsyslog using configured
rotation parameters
2.) Fix /run/rsyslog/rsyslog.conf ignoring configured size for /var/log/messages
- system_syslog.py retrieves "system logs" config to set rotation limits
- default rotation size is now based on configuration defaults
3.) prifilt strings for rsyslog are modified to use override syntax
(e.g. ";facility.none") when all is combined with other facilities to honor
per facility user intent
Smoke test changes:
Commonize configuration of syslog facilities
Check that prifilt is sane based on configured facilities (i.e. overides only
present when required)
Test wildcard (all) only, specific facility only, and wildcard + specific facility
| -rw-r--r-- | data/config-mode-dependencies/vyos-1x.json | 3 | ||||
| -rw-r--r-- | data/templates/logs/logrotate/vyos-rsyslog.j2 | 2 | ||||
| -rw-r--r-- | data/templates/rsyslog/logrotate.j2 | 8 | ||||
| -rw-r--r-- | data/templates/rsyslog/rsyslog.conf.j2 | 54 | ||||
| -rwxr-xr-x | smoketest/scripts/cli/test_system_logs.py | 12 | ||||
| -rwxr-xr-x | smoketest/scripts/cli/test_system_syslog.py | 148 | ||||
| -rwxr-xr-x | src/conf_mode/system_logs.py | 8 | ||||
| -rwxr-xr-x | src/conf_mode/system_syslog.py | 20 |
8 files changed, 176 insertions, 79 deletions
diff --git a/data/config-mode-dependencies/vyos-1x.json b/data/config-mode-dependencies/vyos-1x.json index 47a83ca0a..2337cb74b 100644 --- a/data/config-mode-dependencies/vyos-1x.json +++ b/data/config-mode-dependencies/vyos-1x.json @@ -110,6 +110,9 @@ "ip_ipv6": ["system_ip", "system_ipv6"], "sysctl": ["system_sysctl"] }, + "system_logs": { + "syslog": ["system_syslog"] + }, "system_sflow": { "vpp_sflow": ["vpp_sflow"] } diff --git a/data/templates/logs/logrotate/vyos-rsyslog.j2 b/data/templates/logs/logrotate/vyos-rsyslog.j2 index f2e4d2ab2..23e5810cd 100644 --- a/data/templates/logs/logrotate/vyos-rsyslog.j2 +++ b/data/templates/logs/logrotate/vyos-rsyslog.j2 @@ -1,3 +1,4 @@ +### Autogenerated by system_logs.py ### /var/log/messages { create missingok @@ -10,4 +11,3 @@ /usr/lib/rsyslog/rsyslog-rotate endscript } - diff --git a/data/templates/rsyslog/logrotate.j2 b/data/templates/rsyslog/logrotate.j2 index b9689a1cf..4434a3a32 100644 --- a/data/templates/rsyslog/logrotate.j2 +++ b/data/templates/rsyslog/logrotate.j2 @@ -1,12 +1,4 @@ ### Autogenerated by system_syslog.py ### -/var/log/messages { - missingok - notifempty - create - rotate 5 - size=256k -} - {% if file is vyos_defined %} {% for file_name, file_options in file.items() %} /var/log/user/{{ file_name }} { diff --git a/data/templates/rsyslog/rsyslog.conf.j2 b/data/templates/rsyslog/rsyslog.conf.j2 index 714788df0..3eb6f258a 100644 --- a/data/templates/rsyslog/rsyslog.conf.j2 +++ b/data/templates/rsyslog/rsyslog.conf.j2 @@ -52,49 +52,61 @@ global(localHostname="{{ preserve_fqdn.host_name }}.{{ preserve_fqdn.domain_name {% endif %} {% endif %} +{# Build a prifilt selector list with "all" excluding any explicitly-set facilities #} +{% macro prifilt_selectors(facility_map) %} +{# pass 1: collect explicit facilities #} +{% set selectors = [] %} +{% set keys = facility_map.keys() | list | sort %} +{% set specific = [] %} +{% for facility in keys %} +{% if facility != 'all' %} +{% set _ = specific.append(facility) %} +{% endif %} +{% endfor %} +{# pass 2: build selectors; add ;fac.none exclusions to wildcard #} +{% for facility in keys %} +{% set opts = facility_map[facility] %} +{% set level = opts.level.replace('all', 'debug') %} +{% if facility == 'all' %} +{% set ns = namespace(sel="*." ~ level) %} +{% for sf in specific %} +{% set ns.sel = ns.sel ~ ";" ~ sf ~ ".none" %} +{% endfor %} +{% set sel = ns.sel %} +{% else %} +{% set sel = facility ~ "." ~ level %} +{% endif %} +{% set _ = selectors.append(sel) %} +{% endfor %} +{{ selectors | join(',') }} +{% endmacro %} + #### GLOBAL LOGGING #### {% if local.facility is vyos_defined %} -{% set tmp = [] %} -{% if local.facility is vyos_defined %} -{% for facility, facility_options in local.facility.items() %} -{% set _ = tmp.append(facility.replace('all', '*') ~ "." ~ facility_options.level.replace('all', 'debug')) %} -{% endfor %} -if prifilt("{{ tmp | join(',') }}") then { +if prifilt("{{ prifilt_selectors(local.facility) | trim }}") then { action( type="omfile" file="/var/log/messages" - rotation.sizeLimit="524288" # 512Kib - maximum filesize before rotation + rotation.sizeLimit="{{ logrotate_size_limit }}" # maximum filesize before rotation rotation.sizeLimitCommand="/usr/sbin/logrotate {{ logrotate }}" ) } -{% endif %} {% endif %} #### CONSOLE LOGGING #### {% if console.facility is vyos_defined %} -{% set tmp = [] %} -{% if console.facility is vyos_defined %} -{% for facility, facility_options in console.facility.items() %} -{% set _ = tmp.append(facility.replace('all', '*') ~ "." ~ facility_options.level.replace('all', 'debug')) %} -{% endfor %} -if prifilt("{{ tmp | join(',') }}") then { +if prifilt("{{ prifilt_selectors(console.facility) | trim }}") then { action(type="omfile" file="/dev/console") } -{% endif %} {% endif %} #### REMOTE LOGGING #### {% if remote is vyos_defined %} {% for remote_name, remote_options in remote.items() %} -{% set tmp = [] %} {% if remote_options.facility is vyos_defined %} -{% for facility, facility_options in remote_options.facility.items() %} -{% set _ = tmp.append(facility.replace('all', '*') ~ "." ~ facility_options.level.replace('all', 'debug')) %} -{% endfor %} -{% set _ = tmp.sort() %} {% set tls = remote_options.tls %} # Remote syslog to {{ remote_name }} -if prifilt("{{ tmp | join(',') }}") then { +if prifilt("{{ prifilt_selectors(remote_options.facility) | trim }}") then { action( type="omfwd" # Remote syslog server where we send our logs to diff --git a/smoketest/scripts/cli/test_system_logs.py b/smoketest/scripts/cli/test_system_logs.py index 81fbf8048..ccddcdec6 100755 --- a/smoketest/scripts/cli/test_system_logs.py +++ b/smoketest/scripts/cli/test_system_logs.py @@ -18,18 +18,20 @@ import re import unittest from base_vyostest_shim import VyOSUnitTestSHIM from vyos.utils.file import read_file +from vyos.xml_ref import default_value # path to logrotate configs logrotate_atop_file = '/etc/logrotate.d/vyos-atop' logrotate_rsyslog_file = '/etc/logrotate.d/vyos-rsyslog' -# default values -default_atop_maxsize = '10M' -default_atop_rotate = '10' -default_rsyslog_size = '1M' -default_rsyslog_rotate = '10' base_path = ['system', 'logs'] +# default values +default_atop_maxsize = f"{default_value(base_path + ['logrotate', 'atop', 'max-size'])}M" +default_atop_rotate = default_value(base_path + ['logrotate', 'atop', 'rotate']) +default_rsyslog_size = f"{default_value(base_path + ['logrotate', 'messages', 'max-size'])}M" +default_rsyslog_rotate = default_value(base_path + ['logrotate', 'messages', 'rotate']) + def logrotate_config_parse(file_path): # read the file diff --git a/smoketest/scripts/cli/test_system_syslog.py b/smoketest/scripts/cli/test_system_syslog.py index 79fbe27ec..2d1e0aef4 100755 --- a/smoketest/scripts/cli/test_system_syslog.py +++ b/smoketest/scripts/cli/test_system_syslog.py @@ -30,6 +30,7 @@ RSYSLOG_CONF = '/run/rsyslog/rsyslog.conf' CERT_DIR = '/etc/rsyslog.d/certs' base_path = ['system', 'syslog'] +base_logs_path = ['system', 'logs'] pki_base = ['pki'] dummy_interface = 'dum372874' @@ -94,6 +95,7 @@ class TestRSYSLOGService(VyOSUnitTestSHIM.TestCase): # delete testing SYSLOG config self.cli_delete(base_path) + self.cli_delete(base_logs_path) self.cli_commit() # The default syslog implementation should make syslog.service a @@ -130,23 +132,115 @@ class TestRSYSLOGService(VyOSUnitTestSHIM.TestCase): ] ) + def _set_facilities(self, base, facility_map): + for facility, facility_options in facility_map.items(): + level = facility_options['level'] + self.cli_set(base + ['facility', facility, 'level'], value=level) + + # Build prifilt selector strings the same way as rsyslog.conf.j2, e.g. + # "auth.info,*.notice;auth.none" when specific facilities coexist with "all". + def _prifilt_selectors(self, facility_map): + keys = sorted(facility_map) + specific = [] + for facility in keys: + if facility != 'all': + specific.append(facility) + + selectors = [] + for facility in keys: + opts = facility_map[facility] + level = opts['level'].replace('all', 'debug') + if facility == 'all': + sel = f'*.{level}' + for sf in specific: + sel += f';{sf}.none' + else: + sel = f'{facility}.{level}' + selectors.append(sel) + + prifilt = ','.join(selectors) + self._assert_prifilt_sane(prifilt, facility_map) + return prifilt + + def _assert_prifilt_sane(self, prifilt, facility_map): + has_all = 'all' in facility_map + specific_facilities = sorted(f for f in facility_map if f != 'all') + self.assertTrue(prifilt) + self.assertNotIn(' ', prifilt) + parts = prifilt.split(',') + self.assertTrue(all(parts)) + wildcard_parts = [p for p in parts if p.startswith('*.')] + if has_all: + self.assertEqual(len(wildcard_parts), 1) + wildcard = wildcard_parts[0] + if specific_facilities: + base, *exclusions = wildcard.split(';') + self.assertTrue(base.startswith('*.')) + expected = {f'{fac}.none' for fac in specific_facilities} + self.assertEqual(set(exclusions), expected) + else: + self.assertNotIn(';', wildcard) + else: + self.assertEqual(len(wildcard_parts), 0) + self.assertNotIn(';', prifilt) + + for part in parts: + if ';' in part: + base, *exclusions = part.split(';') + self.assertTrue(base.startswith('*.')) + self.assertNotIn('*.none', base) + for ex in exclusions: + self.assertTrue(ex.endswith('.none')) + self.assertFalse(ex.startswith('*.')) + self.assertNotIn(',', ex) + else: + if part.startswith('*.'): + self.assertTrue(has_all) + continue + self.assertEqual(part.count('.'), 1) + self.assertFalse(part.startswith('*.')) + def test_console(self): - level = 'warning' - self.cli_set(base_path + ['console', 'facility', 'all', 'level'], value=level) + facility = { + 'all': {'level': 'warning'}, + } + self._set_facilities(base_path + ['console'], facility) self.cli_commit() rsyslog_conf = get_config() - config = [ - f'if prifilt("*.{level}") then {{', # {{ required to escape { in f-string - 'action(type="omfile" file="/dev/console")', - ] - for tmp in config: - self.assertIn(tmp, rsyslog_conf) + expected_prifilt = self._prifilt_selectors(facility) + self.assertIn(f'if prifilt("{expected_prifilt}") then {{', rsyslog_conf) + self.assertIn('action(type="omfile" file="/dev/console")', rsyslog_conf) + + self.cli_delete(base_path + ['console']) + facility = { + 'auth': {'level': 'info'}, + 'kern': {'level': 'debug'}, + } + self._set_facilities(base_path + ['console'], facility) + self.cli_commit() + + rsyslog_conf = get_config() + expected_prifilt = self._prifilt_selectors(facility) + self.assertIn(f'if prifilt("{expected_prifilt}") then {{', rsyslog_conf) + self.assertIn('action(type="omfile" file="/dev/console")', rsyslog_conf) + + facility['all'] = {'level': 'notice'} + self._set_facilities(base_path + ['console'], facility) + self.cli_commit() + + rsyslog_conf = get_config() + expected_prifilt = self._prifilt_selectors(facility) + self.assertIn(f'if prifilt("{expected_prifilt}") then {{', rsyslog_conf) + self.assertIn('action(type="omfile" file="/dev/console")', rsyslog_conf) def test_basic(self): hostname = 'vyos123' domain_name = 'example.local' default_marker_interval = default_value(base_path + ['marker', 'interval']) + default_rsyslog_max_size = default_value( + base_logs_path + ['logrotate', 'messages', 'max-size'] + ) facility = { 'auth': {'level': 'info'}, @@ -158,9 +252,7 @@ class TestRSYSLOGService(VyOSUnitTestSHIM.TestCase): self.cli_set(['system', 'domain-name'], value=domain_name) self.cli_set(base_path + ['preserve-fqdn']) - for tmp, tmp_options in facility.items(): - level = tmp_options['level'] - self.cli_set(base_path + ['local', 'facility', tmp, 'level'], value=level) + self._set_facilities(base_path + ['local'], facility) self.cli_commit() @@ -174,21 +266,13 @@ class TestRSYSLOGService(VyOSUnitTestSHIM.TestCase): self.assertIn(e, config) config = get_config('#### GLOBAL LOGGING ####') - prifilt = [] - for tmp, tmp_options in facility.items(): - if tmp == 'all': - tmp = '*' - level = tmp_options['level'] - prifilt.append(f'{tmp}.{level}') - - prifilt.sort() - prifilt = ','.join(prifilt) - - self.assertIn(f'if prifilt("{prifilt}") then {{', config) + expected_prifilt = self._prifilt_selectors(facility) + self.assertIn(f'if prifilt("{expected_prifilt}") then {{', config) self.assertIn( ' action(', config) self.assertIn( ' type="omfile"', config) self.assertIn( ' file="/var/log/messages"', config) - self.assertIn( ' rotation.sizeLimit="524288"', config) + size_limit = int(default_rsyslog_max_size) * 1024 * 1024 + self.assertIn(f' rotation.sizeLimit="{size_limit}"', config) self.assertIn( ' rotation.sizeLimitCommand="/usr/sbin/logrotate /etc/logrotate.d/vyos-rsyslog"', config) self.cli_set(base_path + ['marker', 'disable']) @@ -234,10 +318,7 @@ class TestRSYSLOGService(VyOSUnitTestSHIM.TestCase): self.cli_set(remote_base + ['port'], value=remote_options['port']) if 'facility' in remote_options: - for facility, facility_options in remote_options['facility'].items(): - level = facility_options['level'] - self.cli_set(remote_base + ['facility', facility, 'level'], - value=level) + self._set_facilities(remote_base, remote_options['facility']) if 'format' in remote_options: for format in remote_options['format']: @@ -261,16 +342,9 @@ class TestRSYSLOGService(VyOSUnitTestSHIM.TestCase): config = read_file(RSYSLOG_CONF) for remote, remote_options in rhosts.items(): config = get_config(f'# Remote syslog to {remote}') - prifilt = [] + prifilt = '' if 'facility' in remote_options: - for facility, facility_options in remote_options['facility'].items(): - level = facility_options['level'] - if facility == 'all': - facility = '*' - prifilt.append(f'{facility}.{level}') - - prifilt.sort() - prifilt = ','.join(prifilt) + prifilt = self._prifilt_selectors(remote_options['facility']) if not prifilt: # Skip test - as we do not render anything if no facility is set continue @@ -429,7 +503,7 @@ class TestRSYSLOGService(VyOSUnitTestSHIM.TestCase): self.assertIn(f'StreamDriverPermittedPeers="{value}"', config) if not tls: - self.assertIn(f'StreamDriverAuthMode="anon"', config) + self.assertIn('StreamDriverAuthMode="anon"', config) def test_remote_tls_protocol_udp(self): remote_base = base_path + ['remote', '172.11.0.1'] diff --git a/src/conf_mode/system_logs.py b/src/conf_mode/system_logs.py index f0e742d1b..f31986034 100755 --- a/src/conf_mode/system_logs.py +++ b/src/conf_mode/system_logs.py @@ -19,6 +19,8 @@ from sys import exit from vyos import ConfigError from vyos import airbag from vyos.config import Config +from vyos.configdep import set_dependents +from vyos.configdep import call_dependents from vyos.logger import syslog from vyos.template import render from vyos.utils.dict import dict_search @@ -35,6 +37,8 @@ def get_config(config=None): else: conf = Config() + set_dependents('syslog', conf) + base = ['system', 'logs'] logs_config = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, @@ -64,8 +68,8 @@ def generate(logs_config): def apply(logs_config): - # No further actions needed - pass + # Ensure dependent config scripts (e.g., syslog) are re-run + call_dependents() if __name__ == '__main__': diff --git a/src/conf_mode/system_syslog.py b/src/conf_mode/system_syslog.py index 90bb45c51..7e5b4f90c 100755 --- a/src/conf_mode/system_syslog.py +++ b/src/conf_mode/system_syslog.py @@ -40,7 +40,8 @@ airbag.enable() cert_dir = '/etc/rsyslog.d/certs' rsyslog_conf = '/run/rsyslog/rsyslog.conf' -logrotate_conf = '/etc/logrotate.d/vyos-rsyslog' +logrotate_user_conf = '/etc/logrotate.d/vyos-rsyslog-user' +logrotate_messages_conf = '/etc/logrotate.d/vyos-rsyslog' systemd_socket = 'syslog.socket' systemd_service = systemd_services['syslog'] @@ -124,7 +125,16 @@ def get_config(config=None): with_pki=True, ) - syslog.update({ 'logrotate' : logrotate_conf }) + syslog.update({ 'logrotate' : logrotate_messages_conf }) + + logs_config = conf.get_config_dict( + ['system', 'logs'], + key_mangling=('-', '_'), + get_first_key=True, + with_recursive_defaults=True, + ) + max_size_mb = dict_search('logrotate.messages.max_size', logs_config) + syslog['logrotate_size_limit'] = int(max_size_mb) * 1024 * 1024 syslog = conf.merge_defaults(syslog, recursive=True) if syslog.from_defaults(['local']): @@ -192,8 +202,8 @@ def generate(syslog): if not syslog: if os.path.exists(rsyslog_conf): os.unlink(rsyslog_conf) - if os.path.exists(logrotate_conf): - os.unlink(logrotate_conf) + if os.path.exists(logrotate_user_conf): + os.unlink(logrotate_user_conf) return None @@ -203,7 +213,7 @@ def generate(syslog): _save_tls_certificates_for_remote(syslog, remote_options) render(rsyslog_conf, 'rsyslog/rsyslog.conf.j2', syslog) - render(logrotate_conf, 'rsyslog/logrotate.j2', syslog) + render(logrotate_user_conf, 'rsyslog/logrotate.j2', syslog) return None def apply(syslog): |
