diff --git a/scripts/hostcfgd b/scripts/hostcfgd index 7f296f70..cd6626b9 100644 --- a/scripts/hostcfgd +++ b/scripts/hostcfgd @@ -1749,6 +1749,44 @@ class SerialConsoleCfg: return +class LoggingCfg(object): + """Logging Config Daemon + + Handles changes in LOGGING table. + 1) Handle change of debug/syslog log files config + """ + def __init__(self): + self.cache = {} + + def load(self, logging_cfg={}): + # Get initial logging file configuration + self.cache = logging_cfg + syslog.syslog(syslog.LOG_DEBUG, f'Initial logging config: {self.cache}') + + def update_logging_cfg(self, key, data): + """Apply logging configuration + + The daemon restarts logrotate-config which will regenerate logrotate + config files. + Args: + key: DB table's key that was triggered change (basically it is a + config file) + data: File's config data + """ + syslog.syslog(syslog.LOG_DEBUG, 'LoggingCfg: logging files cfg update') + if self.cache.get(key) != data: + syslog.syslog(syslog.LOG_INFO, + f'Set logging file {key} config: {data}') + try: + run_cmd('sudo systemctl restart logrotate-config', True, True) + except Exception: + syslog.syslog(syslog.LOG_ERR, f'Failed to update {key} message') + return + + # Update cache + self.cache[key] = data + + class HostConfigDaemon: def __init__(self): self.state_db_conn = DBConnector(STATE_DB, 0) @@ -1803,6 +1841,9 @@ class HostConfigDaemon: # Initialize SerialConsoleCfg self.serialconscfg = SerialConsoleCfg() + # Initialize LoggingCfg + self.loggingcfg = LoggingCfg() + def load(self, init_data): aaa = init_data['AAA'] tacacs_global = init_data['TACPLUS'] @@ -1826,6 +1867,7 @@ class HostConfigDaemon: ntp_servers = init_data.get(swsscommon.CFG_NTP_SERVER_TABLE_NAME) ntp_keys = init_data.get(swsscommon.CFG_NTP_KEY_TABLE_NAME) serial_console = init_data.get('SERIAL_CONSOLE', {}) + logging = init_data.get('LOGGING', {}) self.aaacfg.load(aaa, tacacs_global, tacacs_server, radius_global, radius_server, ldap_global, ldap_server) self.iptables.load(lpbk_table) @@ -1840,6 +1882,7 @@ class HostConfigDaemon: self.ntpcfg.load(ntp_global, ntp_servers, ntp_keys) self.serialconscfg.load(serial_console) self.pamLimitsCfg.update_config_file() + self.loggingcfg.load(logging) # Update AAA with the hostname self.aaacfg.hostname_update(self.devmetacfg.hostname) @@ -1992,6 +2035,10 @@ class HostConfigDaemon: syslog.syslog(syslog.LOG_INFO, 'SERIAL_CONSOLE table handler...') self.serialconscfg.update_serial_console_cfg(key, data) + def logging_handler(self, key, op, data): + syslog.syslog(syslog.LOG_INFO, 'LOGGING table handler...') + self.loggingcfg.update_logging_cfg(key, data) + def wait_till_system_init_done(self): # No need to print the output in the log file so using the "--quiet" # flag @@ -2059,6 +2106,10 @@ class HostConfigDaemon: self.config_db.subscribe(swsscommon.CFG_NTP_KEY_TABLE_NAME, make_callback(self.ntp_srv_key_handler)) + # Handle LOGGING changes + self.config_db.subscribe(swsscommon.CFG_LOGGING_TABLE_NAME, + make_callback(self.logging_handler)) + syslog.syslog(syslog.LOG_INFO, "Waiting for systemctl to finish initialization") self.wait_till_system_init_done() diff --git a/tests/hostcfgd/hostcfgd_logging_test.py b/tests/hostcfgd/hostcfgd_logging_test.py new file mode 100644 index 00000000..1742246c --- /dev/null +++ b/tests/hostcfgd/hostcfgd_logging_test.py @@ -0,0 +1,91 @@ +import importlib.machinery +import importlib.util +import os +import sys + +from copy import copy +from swsscommon import swsscommon +from syslog import syslog, LOG_ERR +from tests.hostcfgd.test_logging_vectors \ + import HOSTCFGD_TEST_LOGGING_VECTOR as logging_test_data +from tests.common.mock_configdb import MockConfigDb, MockDBConnector +from unittest import TestCase, mock + +test_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +modules_path = os.path.dirname(test_path) +scripts_path = os.path.join(modules_path, "scripts") +src_path = os.path.dirname(modules_path) +templates_path = os.path.join(src_path, "sonic-host-services-data/templates") +output_path = os.path.join(test_path, "hostcfgd/output") +sample_output_path = os.path.join(test_path, "hostcfgd/sample_output") +sys.path.insert(0, modules_path) + +# Load the file under test +hostcfgd_path = os.path.join(scripts_path, 'hostcfgd') +loader = importlib.machinery.SourceFileLoader('hostcfgd', hostcfgd_path) +spec = importlib.util.spec_from_loader(loader.name, loader) +hostcfgd = importlib.util.module_from_spec(spec) +loader.exec_module(hostcfgd) +sys.modules['hostcfgd'] = hostcfgd + +# Mock swsscommon classes +hostcfgd.ConfigDBConnector = MockConfigDb +hostcfgd.DBConnector = MockDBConnector +hostcfgd.Table = mock.Mock() +hostcfgd.run_cmd = mock.Mock() + + +class TestHostcfgLogging(TestCase): + """ + Test hostcfgd daemon - LogRotate + """ + + def __init__(self, *args, **kwargs): + super(TestHostcfgLogging, self).__init__(*args, **kwargs) + self.host_config_daemon = None + + def setUp(self): + MockConfigDb.set_config_db(logging_test_data['initial']) + self.host_config_daemon = hostcfgd.HostConfigDaemon() + + logging_config = self.host_config_daemon.config_db.get_table( + swsscommon.CFG_LOGGING_TABLE_NAME) + + assert self.host_config_daemon.loggingcfg.cache == {} + self.host_config_daemon.loggingcfg.load(logging_config) + assert self.host_config_daemon.loggingcfg.cache != {} + + # Reset run_cmd mock + hostcfgd.run_cmd.reset_mock() + + def tearDown(self): + self.host_config_daemon = None + MockConfigDb.set_config_db({}) + + def update_config(self, config_name): + MockConfigDb.mod_config_db(logging_test_data[config_name]) + + syslog_data = logging_test_data[config_name]['LOGGING']['syslog'] + debug_data = logging_test_data[config_name]['LOGGING']['debug'] + + self.host_config_daemon.logging_handler(key='syslog', op=None, + data=syslog_data) + self.host_config_daemon.logging_handler(key='debug', op=None, + data=debug_data) + + def assert_applied(self, config_name): + """Assert that updated config triggered appropriate services + + Args: + config_name: str: Test vectors config name + + Assert: + Assert when config wasn't used + """ + orig_cache = copy(self.host_config_daemon.loggingcfg.cache) + self.update_config(config_name) + assert self.host_config_daemon.loggingcfg.cache != orig_cache + hostcfgd.run_cmd.assert_called() + + def test_rsyslog_handle_modified(self): + self.assert_applied('modified') diff --git a/tests/hostcfgd/test_logging_vectors.py b/tests/hostcfgd/test_logging_vectors.py new file mode 100644 index 00000000..b882cd7d --- /dev/null +++ b/tests/hostcfgd/test_logging_vectors.py @@ -0,0 +1,48 @@ +''' + hostcfgd test logging configuration vector +''' + +HOSTCFGD_TEST_LOGGING_VECTOR = { + 'initial': { + 'DEVICE_METADATA': { + 'localhost': { + 'hostname': 'logrotate', + }, + }, + 'LOGGING': { + 'syslog': { + 'disk_percentage': '', + 'frequency': 'daily', + 'max_number': '20', + 'size': '10.0' + }, + 'debug': { + 'disk_percentage': '', + 'frequency': 'daily', + 'max_number': '10', + 'size': '20.0' + } + }, + "SSH_SERVER": { + "POLICIES" :{ + "max_sessions": "100" + } + } + }, + 'modified': { + 'LOGGING': { + 'syslog': { + 'disk_percentage': '', + 'frequency': 'weekly', + 'max_number': '100', + 'size': '20.0' + }, + 'debug': { + 'disk_percentage': '', + 'frequency': 'weekly', + 'max_number': '20', + 'size': '100.0' + } + } + } +}