diff --git a/scripts/hostcfgd b/scripts/hostcfgd index 7f296f70..e5f702d2 100644 --- a/scripts/hostcfgd +++ b/scripts/hostcfgd @@ -9,18 +9,21 @@ import syslog import signal import re import jinja2 +import time import json +import importlib from shutil import copy2 from datetime import datetime from sonic_py_common import device_info from sonic_py_common.general import check_output_pipe from swsscommon.swsscommon import ConfigDBConnector, DBConnector, Table -from swsscommon import swsscommon +import swsscommon from sonic_installer import bootloader hostcfg_file_path = os.path.abspath(__file__) hostcfg_dir_path = os.path.dirname(hostcfg_file_path) sys.path.append(hostcfg_dir_path) import ldap +importlib.reload(swsscommon) # FILE PAM_AUTH_CONF = "/etc/pam.d/common-auth-sonic" @@ -1716,6 +1719,99 @@ class FipsCfg(object): syslog.syslog(syslog.LOG_INFO, f'FipsCfg: update the FIPS enforce option {self.enforce}.') loader.set_fips(image, self.enforce) +print(swsscommon.__file__) + +class Memory_StatisticsCfg: + """ + Memory_StatisticsCfg class handles the configuration updates for the MemoryStatisticsDaemon. + It listens to ConfigDB changes and applies them by restarting, shutting down, or reloading + the MemoryStatisticsDaemon. + """ + + def __init__(self, config_db): + self.cache = { + "enabled": "false", + "retention": "15", + "sampling": "5" + } + self.config_db = config_db # Store config_db instance for further use + + def load(self, memory_statistics_config: dict): + """Load initial memory statistics configuration.""" + syslog.syslog(syslog.LOG_INFO, 'Memory_StatisticsCfg: Load initial configuration') + + if not memory_statistics_config: + memory_statistics_config = {} + + # Call memory_statistics_message to handle the initial config load for each key + self.memory_statistics_message("enabled", memory_statistics_config.get("enabled", "false")) + self.memory_statistics_message("retention", memory_statistics_config.get("retention", "15")) + self.memory_statistics_message("sampling", memory_statistics_config.get("sampling", "5")) + + def memory_statistics_update(self, key, data): + """ + Apply memory statistics settings handler. + Args: + key: DB table's key that triggered the change. + data: New table data to process. + """ + # Ensure data is a string or convertible to the required value + if not isinstance(data, str): + data = str(data) + + # Check if any value has changed + if data != self.cache.get(key): + syslog.syslog(syslog.LOG_INFO, f"Memory_StatisticsCfg: Detected change in '{key}'") + + try: + if key == "enabled": + enabled = data.lower() == "true" + if enabled: + self.restart_memory_statistics() # Start or restart the daemon + else: + self.shutdown_memory_statistics() # Stop the daemon if disabled + else: + # If other keys (like sampling/retention) are changed, just reload the daemon config + self.reload_memory_statistics() + + # Update cache with the new value + self.cache[key] = data + except Exception as e: + syslog.syslog(syslog.LOG_ERR, f'Memory_StatisticsCfg: Failed to manage MemoryStatisticsDaemon: {e}') + + def restart_memory_statistics(self): + """Restart the memory statistics daemon.""" + self.shutdown_memory_statistics() # Ensure the daemon is stopped before restarting + time.sleep(1) # Brief delay to allow shutdown + syslog.syslog(syslog.LOG_INFO, "Memory_StatisticsCfg: Starting MemoryStatisticsDaemon") + try: + subprocess.Popen(['/usr/bin/memorystatsd']) + except Exception as e: + syslog.syslog(syslog.LOG_ERR, f"Memory_StatisticsCfg: Failed to start MemoryStatisticsDaemon: {e}") + + def reload_memory_statistics(self): + """Send SIGHUP to the MemoryStatisticsDaemon to reload its configuration.""" + pid = self.get_memory_statistics_pid() + if pid: + os.kill(pid, signal.SIGHUP) # Notify daemon to reload its configuration + syslog.syslog(syslog.LOG_INFO, "Memory_StatisticsCfg: Sent SIGHUP to reload daemon configuration") + + def shutdown_memory_statistics(self): + """Send SIGTERM to stop the MemoryStatisticsDaemon gracefully.""" + pid = self.get_memory_statistics_pid() + if pid: + os.kill(pid, signal.SIGTERM) # Graceful shutdown + syslog.syslog(syslog.LOG_INFO, "Memory_StatisticsCfg: Sent SIGTERM to stop MemoryStatisticsDaemon") + + def get_memory_statistics_pid(self): + """Retrieve the PID of the running MemoryStatisticsDaemon.""" + try: + with open('/var/run/memorystatsd.pid', 'r') as pid_file: + pid = int(pid_file.read().strip()) + return pid + except Exception as e: + syslog.syslog(syslog.LOG_ERR, f"Memory_StatisticsCfg: Failed to retrieve MemoryStatisticsDaemon PID: {e}") + return None class SerialConsoleCfg: @@ -1749,6 +1845,7 @@ class SerialConsoleCfg: return + class HostConfigDaemon: def __init__(self): self.state_db_conn = DBConnector(STATE_DB, 0) @@ -1764,6 +1861,8 @@ class HostConfigDaemon: # Initialize KDump Config and set the config to default if nothing is provided self.kdumpCfg = KdumpCfg(self.config_db) + self.memory_statisticsCfg = Memory_StatisticsCfg(self.config_db) + # Initialize IpTables self.iptables = Iptables() @@ -1816,6 +1915,7 @@ class HostConfigDaemon: passwh = init_data['PASSW_HARDENING'] ssh_server = init_data['SSH_SERVER'] dev_meta = init_data.get(swsscommon.CFG_DEVICE_METADATA_TABLE_NAME, {}) + memory_statistics = init_data.get[swsscommon.CFG_MEMORY_STATISTICS_TABLE_NAME, {}] mgmt_ifc = init_data.get(swsscommon.CFG_MGMT_INTERFACE_TABLE_NAME, {}) mgmt_vrf = init_data.get(swsscommon.CFG_MGMT_VRF_CONFIG_TABLE_NAME, {}) syslog_cfg = init_data.get(swsscommon.CFG_SYSLOG_CONFIG_TABLE_NAME, {}) @@ -1830,6 +1930,7 @@ class HostConfigDaemon: self.aaacfg.load(aaa, tacacs_global, tacacs_server, radius_global, radius_server, ldap_global, ldap_server) self.iptables.load(lpbk_table) self.kdumpCfg.load(kdump) + self.memory_statisticsCfg.load(memory_statistics) self.passwcfg.load(passwh) self.sshscfg.load(ssh_server) self.devmetacfg.load(dev_meta) @@ -1959,6 +2060,10 @@ class HostConfigDaemon: syslog.syslog(syslog.LOG_INFO, 'Kdump handler...') self.kdumpCfg.kdump_update(key, data) + def memory_statistics_handler (self, key, op, data): + syslog.syslog(syslog.LOG_INFO, 'Memory_Statistics handler...') + self.memory_statisticsCfg.memory_statistics_update(key, data) + def device_metadata_handler(self, key, op, data): syslog.syslog(syslog.LOG_INFO, 'DeviceMeta handler...') self.devmetacfg.hostname_update(data) @@ -2035,6 +2140,9 @@ class HostConfigDaemon: # Handle DEVICE_MEATADATA changes self.config_db.subscribe(swsscommon.CFG_DEVICE_METADATA_TABLE_NAME, make_callback(self.device_metadata_handler)) + + self.config_db.subscribe(swsscommon.CFG_MEMORY_STATISTICS_TABLE_NAME, + make_callback(self.memory_statistics_handler)) # Handle MGMT_VRF_CONFIG changes self.config_db.subscribe(swsscommon.CFG_MGMT_VRF_CONFIG_TABLE_NAME, diff --git a/tests/hostcfgd/hostcfgd_test.py b/tests/hostcfgd/hostcfgd_test.py index 85f80625..d4ea439c 100644 --- a/tests/hostcfgd/hostcfgd_test.py +++ b/tests/hostcfgd/hostcfgd_test.py @@ -216,6 +216,7 @@ def test_kdump_event(self): call(['sonic-kdump-config', '--memory', '0M-2G:256M,2G-4G:320M,4G-8G:384M,8G-:448M'])] mocked_subprocess.check_call.assert_has_calls(expected, any_order=True) + def test_devicemeta_event(self): """ Test handling DEVICE_METADATA events. @@ -324,6 +325,35 @@ def test_mgmtiface_event(self): ] mocked_check_output.assert_has_calls(expected) + def test_memory_statistics_event(self, mock_config_db_connector): + # Mock the ConfigDBConnector instance methods + mock_instance = mock_config_db_connector.return_value + # Make sure get_table returns the correct nested structure + mock_instance.get_table.return_value = HOSTCFG_DAEMON_CFG_DB['MEMORY_STATISTICS']['memory_statistics'] + + # Mock the subprocess module to avoid real process callswith mock.patch('hostcfgd.subprocess') as mocked_subprocess: + daemon = hostcfgd.HostConfigDaemon() # Create the daemon instance# Load config using the correct nested dictionary + daemon.memory_statisticsCfg.load(HOSTCFG_DAEMON_CFG_DB['MEMORY_STATISTICS']['memory_statistics']) + + # Mock the subprocess.Popen and check_call + popen_mock = mock.Mock() + attrs = {'communicate.return_value': ('output', 'error')} + popen_mock.configure_mock(**attrs) + mocked_subprocess.Popen.return_value = popen_mock + mocked_subprocess.check_call = mock.Mock() + + # Trigger event handler + MockConfigDb.event_queue = [('MEMORY_STATISTICS', 'config')] + daemon.memory_statistics_handler('enabled', 'SET', 'true') + + # Define expected subprocess calls + expected_calls = [ + mock.call(['/usr/bin/memorystatsd']), + ] + + # Check if subprocess check_call was made with correct arguments + mocked_subprocess.Popen.assert_has_calls(expected_calls, any_order=True) + def test_dns_events(self): MockConfigDb.set_config_db(HOSTCFG_DAEMON_CFG_DB) MockConfigDb.event_queue = [('DNS_NAMESERVER', '1.1.1.1')] @@ -353,4 +383,4 @@ def test_load(self): data = {} dns_cfg.load(data) - dns_cfg.dns_update.assert_called() + dns_cfg.dns_update.assert_called() \ No newline at end of file diff --git a/tests/hostcfgd/test_vectors.py b/tests/hostcfgd/test_vectors.py index afa50564..8f3bc353 100644 --- a/tests/hostcfgd/test_vectors.py +++ b/tests/hostcfgd/test_vectors.py @@ -15,6 +15,7 @@ "PASSW_HARDENING": {}, "SSH_SERVER": {}, "KDUMP": {}, + "MEMORY_STATISTICS": {}, "NTP": {}, "NTP_SERVER": {}, "LOOPBACK_INTERFACE": {}, @@ -79,6 +80,13 @@ "timezone": "Europe/Kyiv" } }, + "MEMORY_STATISTICS": { + "memory_statistics": { + "enabled": "true", + "retention_time": "15", + "sampling_interval": "5" + } + }, "MGMT_INTERFACE": { "eth0|1.2.3.4/24": {} },