diff --git a/scripts/hostcfgd b/scripts/hostcfgd index 224b2b9d..67ea73d5 100644 --- a/scripts/hostcfgd +++ b/scripts/hostcfgd @@ -9,7 +9,9 @@ import syslog import signal import re import jinja2 +import time import json +import psutil from shutil import copy2 from datetime import datetime from sonic_py_common import device_info @@ -1716,7 +1718,179 @@ class FipsCfg(object): syslog.syslog(syslog.LOG_INFO, f'FipsCfg: update the FIPS enforce option {self.enforce}.') loader.set_fips(image, self.enforce) +class MemoryStatisticsCfg: + """ + The MemoryStatisticsCfg class manages the configuration updates for the MemoryStatisticsDaemon, a daemon + responsible for collecting memory usage statistics. It monitors configuration changes in ConfigDB and, based + on those updates, performs actions such as restarting, shutting down, or reloading the daemon. + + Attributes: + VALID_KEYS (list): List of valid configuration keys ("enabled", "sampling_interval", "retention_period"). + PID_FILE_PATH (str): Path where the daemon’s process ID (PID) is stored. + DAEMON_EXEC_PATH (str): Path to the executable file of the memory statistics daemon. + DAEMON_PROCESS_NAME (str): Name of the daemon process used for validation. + """ + VALID_KEYS = ["enabled", "sampling_interval", "retention_period"] + PID_FILE_PATH = '/var/run/memory_statistics_daemon.pid' + DAEMON_EXEC_PATH = '/usr/bin/memory_statistics_service.py' + DAEMON_PROCESS_NAME = 'memory_statistics_service.py' + + def __init__(self, config_db): + """ + Initialize MemoryStatisticsCfg with a configuration database. + + Parameters: + config_db (object): Instance of the configuration database (ConfigDB) used to retrieve and + apply configuration changes. + """ + self.cache = { + "enabled": "false", + "sampling_interval": "5", + "retention_period": "15" + } + self.config_db = config_db + + def load(self, memory_statistics_config: dict): + """ + Load the initial memory statistics configuration from a provided dictionary. + + Parameters: + memory_statistics_config (dict): Dictionary containing the initial configuration values. + """ + syslog.syslog(syslog.LOG_INFO, 'MemoryStatisticsCfg: Loading initial configuration') + + if not memory_statistics_config: + memory_statistics_config = {} + + for key, value in memory_statistics_config.items(): + if key not in self.VALID_KEYS: + syslog.syslog(syslog.LOG_ERR, f"MemoryStatisticsCfg: Invalid key '{key}' in initial configuration.") + continue + self.memory_statistics_update(key, value) + + def memory_statistics_update(self, key, data): + """ + Handles updates for each configuration setting, validates the data, and updates the cache if the value changes. + + Parameters: + key (str): Configuration key, e.g., "enabled", "sampling_interval", or "retention_period". + data (str): The new value for the configuration key. + """ + if key not in self.VALID_KEYS: + syslog.syslog(syslog.LOG_ERR, f"MemoryStatisticsCfg: Invalid key '{key}' received.") + return + + data = str(data) + + if key in ["retention_period", "sampling_interval"] and (not data.isdigit() or int(data) <= 0): + syslog.syslog(syslog.LOG_ERR, f"MemoryStatisticsCfg: Invalid value '{data}' for key '{key}'. Must be a positive integer.") + return + + if data != self.cache.get(key): + syslog.syslog(syslog.LOG_INFO, f"MemoryStatisticsCfg: Detected change in '{key}' to '{data}'") + + try: + self.apply_setting(key, data) + self.cache[key] = data + except Exception as e: + syslog.syslog(syslog.LOG_ERR, f'MemoryStatisticsCfg: Failed to manage MemoryStatisticsDaemon: {e}') + + def apply_setting(self, key, data): + """ + Apply the setting based on the key. If "enabled" is set to true or false, start or stop the daemon. + For other keys, reload the daemon configuration. + + Parameters: + key (str): The specific configuration setting being updated. + data (str): The value for the setting. + """ + try: + if key == "enabled": + if data.lower() == "true": + self.restart_memory_statistics() + else: + self.shutdown_memory_statistics() + else: + self.reload_memory_statistics() + except Exception as e: + syslog.syslog(syslog.LOG_ERR, f"MemoryStatisticsCfg: {type(e).__name__} in apply_setting() for key '{key}': {e}") + + def restart_memory_statistics(self): + """Restarts the memory statistics daemon by first shutting it down (if running) and then starting it again.""" + try: + self.shutdown_memory_statistics() + time.sleep(1) + syslog.syslog(syslog.LOG_INFO, "MemoryStatisticsCfg: Starting MemoryStatisticsDaemon") + subprocess.Popen([self.DAEMON_EXEC_PATH]) + except Exception as e: + syslog.syslog(syslog.LOG_ERR, f"MemoryStatisticsCfg: Failed to start MemoryStatisticsDaemon: {e}") + + def reload_memory_statistics(self): + """Sends a SIGHUP signal to the daemon to reload its configuration without restarting.""" + pid = self.get_memory_statistics_pid() + if pid: + try: + os.kill(pid, signal.SIGHUP) + syslog.syslog(syslog.LOG_INFO, "MemoryStatisticsCfg: Sent SIGHUP to reload daemon configuration") + except Exception as e: + syslog.syslog(syslog.LOG_ERR, f"MemoryStatisticsCfg: Failed to reload MemoryStatisticsDaemon: {e}") + + def shutdown_memory_statistics(self): + """Sends a SIGTERM signal to gracefully shut down the daemon.""" + pid = self.get_memory_statistics_pid() + if pid: + try: + os.kill(pid, signal.SIGTERM) + syslog.syslog(syslog.LOG_INFO, "MemoryStatisticsCfg: Sent SIGTERM to stop MemoryStatisticsDaemon") + self.wait_for_shutdown(pid) + except Exception as e: + syslog.syslog(syslog.LOG_ERR, f"MemoryStatisticsCfg: Failed to shutdown MemoryStatisticsDaemon: {e}") + + def wait_for_shutdown(self, pid, timeout=10): + """ + Waits for the daemon process to terminate gracefully within a given timeout. + + Parameters: + pid (int): Process ID of the daemon to shut down. + timeout (int): Maximum wait time in seconds for the process to terminate (default is 10 seconds). + """ + try: + process = psutil.Process(pid) + process.wait(timeout=timeout) + syslog.syslog(syslog.LOG_INFO, "MemoryStatisticsCfg: MemoryStatisticsDaemon stopped gracefully") + except psutil.TimeoutExpired: + syslog.syslog(syslog.LOG_WARNING, f"MemoryStatisticsCfg: Timed out while waiting for daemon (PID {pid}) to shut down.") + except psutil.NoSuchProcess: + syslog.syslog(syslog.LOG_WARNING, "MemoryStatisticsCfg: MemoryStatisticsDaemon process not found.") + except Exception as e: + syslog.syslog(syslog.LOG_ERR, f"MemoryStatisticsCfg: Exception in wait_for_shutdown(): {e}") + def get_memory_statistics_pid(self): + """ + Retrieves the PID of the currently running daemon from the PID file, verifying it matches the expected daemon. + + Returns: + int or None: Returns the PID if the process is running and matches the expected daemon; otherwise, returns None. + """ + try: + with open(self.PID_FILE_PATH, 'r') as pid_file: + pid = int(pid_file.read().strip()) + if psutil.pid_exists(pid): + process = psutil.Process(pid) + if process.name() == self.DAEMON_PROCESS_NAME: + return pid + else: + syslog.syslog(syslog.LOG_WARNING, f"MemoryStatisticsCfg: PID {pid} does not correspond to {self.DAEMON_PROCESS_NAME}.") + else: + syslog.syslog(syslog.LOG_WARNING, "MemoryStatisticsCfg: PID does not exist.") + except FileNotFoundError: + syslog.syslog(syslog.LOG_WARNING, "MemoryStatisticsCfg: PID file not found. Daemon might not be running.") + except ValueError: + syslog.syslog(syslog.LOG_ERR, "MemoryStatisticsCfg: PID file contents invalid.") + except Exception as e: + syslog.syslog(syslog.LOG_ERR, f"MemoryStatisticsCfg: {type(e).__name__} failed to retrieve MemoryStatisticsDaemon PID: {e}") + return None + class SerialConsoleCfg: def __init__(self): @@ -1748,7 +1922,6 @@ class SerialConsoleCfg: return - class BannerCfg(object): """ Banner Config Daemon @@ -1826,7 +1999,6 @@ class BannerCfg(object): for k,v in data.items(): self.cache[k] = v - class HostConfigDaemon: def __init__(self): self.state_db_conn = DBConnector(STATE_DB, 0) @@ -1842,6 +2014,9 @@ class HostConfigDaemon: # Initialize KDump Config and set the config to default if nothing is provided self.kdumpCfg = KdumpCfg(self.config_db) + # Initialize MemoryStatisticsCfg + self.memorystatisticscfg = MemoryStatisticsCfg(self.config_db) + # Initialize IpTables self.iptables = Iptables() @@ -1896,6 +2071,7 @@ class HostConfigDaemon: kdump = init_data['KDUMP'] passwh = init_data['PASSW_HARDENING'] ssh_server = init_data['SSH_SERVER'] + memory_statistics = init_data["MEMORY_STATISTICS"] dev_meta = init_data.get(swsscommon.CFG_DEVICE_METADATA_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, {}) @@ -1912,6 +2088,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.memorystatisticscfg.load(memory_statistics) self.passwcfg.load(passwh) self.sshscfg.load(ssh_server) self.devmetacfg.load(dev_meta) @@ -2043,6 +2220,14 @@ 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...') + try: + self.memorystatisticscfg.memory_statistics_update(key, data) + except Exception as e: + syslog.syslog(syslog.LOG_ERR, f"MemoryStatisticsCfg: Error while handling memory statistics update: {e}") + + def device_metadata_handler(self, key, op, data): syslog.syslog(syslog.LOG_INFO, 'DeviceMeta handler...') self.devmetacfg.hostname_update(data) @@ -2099,6 +2284,7 @@ class HostConfigDaemon: return callback self.config_db.subscribe('KDUMP', make_callback(self.kdump_handler)) + self.config_db.subscribe('MEMORY_STATISTICS',make_callback(self.memory_statistics_handler)) # Handle AAA, TACACS and RADIUS related tables self.config_db.subscribe('AAA', make_callback(self.aaa_handler)) self.config_db.subscribe('TACPLUS', make_callback(self.tacacs_global_handler)) @@ -2123,6 +2309,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, @@ -2169,5 +2358,4 @@ def main(): daemon.start() if __name__ == "__main__": - main() - + main() \ No newline at end of file diff --git a/tests/hostcfgd/hostcfgd_test.py b/tests/hostcfgd/hostcfgd_test.py index 9ec3f658..e541fdb9 100644 --- a/tests/hostcfgd/hostcfgd_test.py +++ b/tests/hostcfgd/hostcfgd_test.py @@ -4,17 +4,17 @@ import swsscommon as swsscommon_package from sonic_py_common import device_info from swsscommon import swsscommon - from parameterized import parameterized from sonic_py_common.general import load_module_from_source from unittest import TestCase, mock - from .test_vectors import HOSTCFG_DAEMON_INIT_CFG_DB, HOSTCFG_DAEMON_CFG_DB from tests.common.mock_configdb import MockConfigDb, MockDBConnector - from pyfakefs.fake_filesystem_unittest import patchfs from deepdiff import DeepDiff from unittest.mock import call +import syslog +import psutil +import signal test_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) modules_path = os.path.dirname(test_path) @@ -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. @@ -353,7 +354,6 @@ def test_load(self): data = {} dns_cfg.load(data) - dns_cfg.dns_update.assert_called() class TestBannerCfg: @@ -371,3 +371,770 @@ def test_banner_message(self, mock_run_cmd): banner_cfg.banner_message(None, {'test': 'test'}) mock_run_cmd.assert_has_calls([call(['systemctl', 'restart', 'banner-config'], True, True)]) + + +# class TestMemoryStatisticsCfgd(TestCase): +# """Test MemoryStatisticsCfg functionalities.""" + +# def setUp(self): +# # Initial configuration for Memory Statistics +# MockConfigDb.CONFIG_DB['MEMORY_STATISTICS'] = { +# 'memory_statistics': { +# 'enabled': 'false', +# 'sampling_interval': '5', +# 'retention_period': '15' +# } +# } +# self.mem_stat_cfg = hostcfgd.MemoryStatisticsCfg(MockConfigDb.CONFIG_DB) + +# def tearDown(self): +# MockConfigDb.CONFIG_DB = {} + +# # Test initial loading +# def test_load_with_invalid_key(self): +# """Test loading configuration with invalid key""" +# config = {'invalid_key': 'value', 'enabled': 'true'} +# with mock.patch('hostcfgd.syslog.syslog') as mock_syslog: +# self.mem_stat_cfg.load(config) +# mock_syslog.assert_any_call(mock.ANY, "MemoryStatisticsCfg: Invalid key 'invalid_key' in initial configuration.") + +# def test_load_with_empty_config(self): +# """Test loading empty configuration""" +# with mock.patch('hostcfgd.syslog.syslog') as mock_syslog: +# self.mem_stat_cfg.load(None) +# mock_syslog.assert_any_call(mock.ANY, "MemoryStatisticsCfg: Loading initial configuration") + +# # Test validation +# def test_memory_statistics_update_invalid_key(self): +# """Test update with invalid key""" +# with mock.patch('hostcfgd.syslog.syslog') as mock_syslog: +# self.mem_stat_cfg.memory_statistics_update('invalid_key', 'value') +# mock_syslog.assert_any_call(mock.ANY, "MemoryStatisticsCfg: Invalid key 'invalid_key' received.") + +# def test_memory_statistics_update_invalid_numeric_value(self): +# """Test update with invalid numeric value""" +# with mock.patch('hostcfgd.syslog.syslog') as mock_syslog: +# self.mem_stat_cfg.memory_statistics_update('sampling_interval', '-1') +# mock_syslog.assert_any_call(mock.ANY, "MemoryStatisticsCfg: Invalid value '-1' for key 'sampling_interval'. Must be a positive integer.") + +# # Test daemon management +# @mock.patch('hostcfgd.subprocess.Popen') +# @mock.patch('hostcfgd.os.kill') +# def test_restart_memory_statistics_success(self, mock_kill, mock_popen): +# """Test successful daemon restart""" +# with mock.patch('hostcfgd.syslog.syslog') as mock_syslog: +# with mock.patch.object(self.mem_stat_cfg, 'get_memory_statistics_pid', return_value=123): +# self.mem_stat_cfg.restart_memory_statistics() +# mock_kill.assert_called_with(123, signal.SIGTERM) +# mock_popen.assert_called_once() + +# @mock.patch('hostcfgd.subprocess.Popen') +# def test_restart_memory_statistics_failure(self, mock_popen): +# """Test failed daemon restart""" +# mock_popen.side_effect = Exception("Failed to start") +# with mock.patch('hostcfgd.syslog.syslog') as mock_syslog: +# self.mem_stat_cfg.restart_memory_statistics() +# mock_syslog.assert_any_call(mock.ANY, "MemoryStatisticsCfg: Failed to start MemoryStatisticsDaemon: Failed to start") + +# # Test PID management +# def test_get_memory_statistics_pid_file_not_found(self): +# """Test PID retrieval when file doesn't exist""" +# with mock.patch('builtins.open', side_effect=FileNotFoundError): +# with mock.patch('hostcfgd.syslog.syslog') as mock_syslog: +# pid = self.mem_stat_cfg.get_memory_statistics_pid() +# self.assertIsNone(pid) +# mock_syslog.assert_any_call(mock.ANY, "MemoryStatisticsCfg: PID file not found. Daemon might not be running.") + + # def test_get_memory_statistics_pid_invalid_content(self): + # """Test PID retrieval with invalid file content""" + # mock_open = mock.mock_open(read_data="invalid") + # with mock.patch('builtins.open', mock_open): + # with mock.patch('hostcfgd.syslog.syslog') as mock_syslog: + # pid = self.mem_stat_cfg.get_memory_statistics_pid() + # self.assertIsNone(pid) + # mock_syslog.assert_any_call(mock.ANY, "MemoryStatisticsCfg: PID file contents invalid.") + + # @mock.patch('hostcfgd.psutil.pid_exists', return_value=True) + # @mock.patch('hostcfgd.psutil.Process') + # def test_get_memory_statistics_pid_wrong_process(self, mock_process, mock_pid_exists): + # """Test PID retrieval when process name doesn't match""" + # mock_process_instance = mock.Mock() + # mock_process_instance.name.return_value = "wrong_process" + # mock_process.return_value = mock_process_instance + + # mock_open = mock.mock_open(read_data="123") + # with mock.patch('builtins.open', mock_open): + # with mock.patch('hostcfgd.syslog.syslog') as mock_syslog: + # pid = self.mem_stat_cfg.get_memory_statistics_pid() + # self.assertIsNone(pid) + # mock_syslog.assert_any_call(mock.ANY, "MemoryStatisticsCfg: PID 123 does not correspond to memory_statistics_service.py.") + + +# def test_get_memory_statistics_pid_success(self): +# """Test successful PID retrieval when process exists and name matches""" +# # Create a mock process with the correct name +# mock_process = mock.Mock() +# mock_process.name.return_value = "memory_statistics_service.py" # Match DAEMON_PROCESS_NAME + +# # Set up all the mocks +# mock_open = mock.mock_open(read_data="123") + +# with mock.patch('builtins.open', mock_open), \ +# mock.patch('hostcfgd.psutil.pid_exists', return_value=True), \ +# mock.patch('hostcfgd.psutil.Process', return_value=mock_process), \ +# mock.patch('hostcfgd.syslog.syslog') as mock_syslog: + +# # Call the method +# pid = self.mem_stat_cfg.get_memory_statistics_pid() + +# # Verify the result +# self.assertEqual(pid, 123) + +# # Verify the mocks were called correctly +# mock_open.assert_called_once_with(self.mem_stat_cfg.PID_FILE_PATH, 'r') +# mock_process.name.assert_called_once() +# mock_syslog.assert_not_called() # No warnings should be logged for successful case + + + +# # Test daemon shutdown +# @mock.patch('hostcfgd.psutil.Process') +# def test_wait_for_shutdown_timeout(self, mock_process): +# """Test shutdown waiting timeout""" +# mock_process_instance = mock.Mock() +# mock_process_instance.wait.side_effect = psutil.TimeoutExpired(123, 10) +# mock_process.return_value = mock_process_instance + +# with mock.patch('hostcfgd.syslog.syslog') as mock_syslog: +# self.mem_stat_cfg.wait_for_shutdown(123) +# mock_syslog.assert_any_call(mock.ANY, "MemoryStatisticsCfg: Timed out while waiting for daemon (PID 123) to shut down.") + + # @mock.patch('hostcfgd.psutil.Process') + # def test_wait_for_shutdown_no_process(self, mock_process): + # """Test shutdown waiting when process doesn't exist""" + # mock_process.side_effect = psutil.NoSuchProcess(123) + + # with mock.patch('hostcfgd.syslog.syslog') as mock_syslog: + # self.mem_stat_cfg.wait_for_shutdown(123) + # mock_syslog.assert_any_call(mock.ANY, "MemoryStatisticsCfg: MemoryStatisticsDaemon process not found.") + +# # Test enable/disable functionality +# def test_memory_statistics_enable(self): +# """Test enabling memory statistics""" +# with mock.patch.object(self.mem_stat_cfg, 'restart_memory_statistics') as mock_restart: +# self.mem_stat_cfg.memory_statistics_update('enabled', 'true') +# mock_restart.assert_called_once() +# self.assertEqual(self.mem_stat_cfg.cache['enabled'], 'true') + +# def test_memory_statistics_update_same_value(self): +# """Test update with same value (should not trigger apply_setting)""" +# with mock.patch.object(self.mem_stat_cfg, 'apply_setting') as mock_apply: +# self.mem_stat_cfg.memory_statistics_update('sampling_interval', '5') +# mock_apply.assert_not_called() + +# def test_memory_statistics_update_exception_handling(self): +# """Test exception handling in memory_statistics_update""" +# with mock.patch.object(self.mem_stat_cfg, 'apply_setting', side_effect=Exception("Test error")): +# with mock.patch('hostcfgd.syslog.syslog') as mock_syslog: +# self.mem_stat_cfg.memory_statistics_update('enabled', 'true') +# mock_syslog.assert_any_call(mock.ANY, "MemoryStatisticsCfg: Failed to manage MemoryStatisticsDaemon: Test error") + + # def test_apply_setting_with_non_enabled_key(self): + # """Test apply_setting with sampling_interval or retention_period""" + # with mock.patch.object(self.mem_stat_cfg, 'reload_memory_statistics') as mock_reload: + # self.mem_stat_cfg.apply_setting('sampling_interval', '10') + # mock_reload.assert_called_once() + + # def test_apply_setting_with_enabled_false(self): + # """Test apply_setting with enabled=false""" + # with mock.patch.object(self.mem_stat_cfg, 'shutdown_memory_statistics') as mock_shutdown: + # self.mem_stat_cfg.apply_setting('enabled', 'false') + # mock_shutdown.assert_called_once() + + # def test_apply_setting_exception(self): + # """Test exception handling in apply_setting""" + # with mock.patch.object(self.mem_stat_cfg, 'restart_memory_statistics', + # side_effect=Exception("Test error")): + # with mock.patch('hostcfgd.syslog.syslog') as mock_syslog: + # self.mem_stat_cfg.apply_setting('enabled', 'true') + # mock_syslog.assert_any_call(mock.ANY, + # "MemoryStatisticsCfg: Exception in apply_setting() for key 'enabled': Test error") + + # @mock.patch('hostcfgd.psutil.pid_exists', return_value=False) + # def test_get_memory_statistics_pid_nonexistent(self, mock_pid_exists): + # """Test get_memory_statistics_pid when PID doesn't exist""" + # mock_open = mock.mock_open(read_data="123") + # with mock.patch('builtins.open', mock_open): + # with mock.patch('hostcfgd.syslog.syslog') as mock_syslog: + # pid = self.mem_stat_cfg.get_memory_statistics_pid() + # self.assertIsNone(pid) + # mock_syslog.assert_any_call(mock.ANY, "MemoryStatisticsCfg: PID does not exist.") + + # @mock.patch('hostcfgd.psutil.Process') + # def test_get_memory_statistics_pid_exception(self, mock_process): + # """Test general exception handling in get_memory_statistics_pid""" + # mock_process.side_effect = Exception("Unexpected error") + # mock_open = mock.mock_open(read_data="123") + + # with mock.patch('hostcfgd.psutil.pid_exists', return_value=True): + # with mock.patch('builtins.open', mock_open): + # with mock.patch('hostcfgd.syslog.syslog') as mock_syslog: + # pid = self.mem_stat_cfg.get_memory_statistics_pid() + # self.assertIsNone(pid) + # mock_syslog.assert_any_call(mock.ANY, + # "MemoryStatisticsCfg: Exception failed to retrieve MemoryStatisticsDaemon PID: Unexpected error") + +# def test_memory_statistics_handler(self): +# """Test memory_statistics_handler in HostConfigDaemon""" +# daemon = hostcfgd.HostConfigDaemon() +# with mock.patch.object(daemon.memorystatisticscfg, 'memory_statistics_update') as mock_update: +# daemon.memory_statistics_handler('enabled', None, 'true') +# mock_update.assert_called_once_with('enabled', 'true') + + # def test_memory_statistics_handler_exception(self): + # """Test exception handling in memory_statistics_handler""" + # daemon = hostcfgd.HostConfigDaemon() + # with mock.patch.object(daemon.memorystatisticscfg, 'memory_statistics_update', + # side_effect=Exception("Handler error")): + # with mock.patch('hostcfgd.syslog.syslog') as mock_syslog: + # daemon.memory_statistics_handler('enabled', None, 'true') + # mock_syslog.assert_any_call(mock.ANY, + # "MemoryStatisticsCfg: Error while handling memory statistics update: Handler error") + + # @mock.patch('hostcfgd.psutil.Process') + # def test_wait_for_shutdown_general_exception(self, mock_process): + # """Test general exception handling in wait_for_shutdown""" + # mock_process.side_effect = Exception("Unexpected shutdown error") + # with mock.patch('hostcfgd.syslog.syslog') as mock_syslog: + # self.mem_stat_cfg.wait_for_shutdown(123) + # mock_syslog.assert_any_call(mock.ANY, + # "MemoryStatisticsCfg: Exception in wait_for_shutdown(): Unexpected shutdown error") + +# def test_memory_statistics_disable(self): +# """Test disabling memory statistics""" +# # First set the initial state to enabled +# self.mem_stat_cfg.cache['enabled'] = 'true' + +# # Mock the apply_setting method to avoid actual system calls +# with mock.patch.object(self.mem_stat_cfg, 'apply_setting') as mock_apply: +# self.mem_stat_cfg.memory_statistics_update('enabled', 'false') + +# # Verify apply_setting was called with correct parameters +# mock_apply.assert_called_once_with('enabled', 'false') + +# # Verify the cache was updated +# self.assertEqual(self.mem_stat_cfg.cache['enabled'], 'false') + + # def test_memory_statistics_disable_with_shutdown(self): + # """Test disabling memory statistics with full shutdown chain""" + # # First set the initial state to enabled + # self.mem_stat_cfg.cache['enabled'] = 'true' + + # # Mock both get_memory_statistics_pid and os.kill to simulate full shutdown + # with mock.patch.object(self.mem_stat_cfg, 'get_memory_statistics_pid', return_value=123) as mock_get_pid, \ + # mock.patch('hostcfgd.os.kill') as mock_kill, \ + # mock.patch.object(self.mem_stat_cfg, 'wait_for_shutdown') as mock_wait: + + # self.mem_stat_cfg.memory_statistics_update('enabled', 'false') + + # # Verify the shutdown sequence + # mock_get_pid.assert_called_once() + # mock_kill.assert_called_once_with(123, signal.SIGTERM) + # mock_wait.assert_called_once_with(123) + + # # Verify the cache was updated + # self.assertEqual(self.mem_stat_cfg.cache['enabled'], 'false') + + # def test_memory_statistics_disable_no_running_daemon(self): + # """Test disabling memory statistics when daemon is not running""" + # # First set the initial state to enabled + # self.mem_stat_cfg.cache['enabled'] = 'true' + + # # Mock get_memory_statistics_pid to return None (no running daemon) + # with mock.patch.object(self.mem_stat_cfg, 'get_memory_statistics_pid', return_value=None) as mock_get_pid: + # self.mem_stat_cfg.memory_statistics_update('enabled', 'false') + + # # Verify get_pid was called + # mock_get_pid.assert_called_once() + + # # Verify the cache was updated despite no running daemon + # self.assertEqual(self.mem_stat_cfg.cache['enabled'], 'false') + + + + + +# def test_reload_memory_statistics_success(self): +# """Test successful reload of memory statistics daemon""" +# with mock.patch.object(self.mem_stat_cfg, 'get_memory_statistics_pid', return_value=123) as mock_get_pid, \ +# mock.patch('hostcfgd.os.kill') as mock_kill, \ +# mock.patch('hostcfgd.syslog.syslog') as mock_syslog: + +# self.mem_stat_cfg.reload_memory_statistics() + +# # Verify the method calls +# mock_get_pid.assert_called_once() +# mock_kill.assert_called_once_with(123, signal.SIGHUP) +# mock_syslog.assert_any_call(mock.ANY, "MemoryStatisticsCfg: Sent SIGHUP to reload daemon configuration") + +# def test_reload_memory_statistics_no_pid(self): +# """Test reload when no daemon is running""" +# with mock.patch.object(self.mem_stat_cfg, 'get_memory_statistics_pid', return_value=None) as mock_get_pid, \ +# mock.patch('hostcfgd.os.kill') as mock_kill: + +# self.mem_stat_cfg.reload_memory_statistics() + +# # Verify the method calls +# mock_get_pid.assert_called_once() +# mock_kill.assert_not_called() + + # def test_reload_memory_statistics_failure(self): + # """Test reload failure with exception""" + # with mock.patch.object(self.mem_stat_cfg, 'get_memory_statistics_pid', return_value=123) as mock_get_pid, \ + # mock.patch('hostcfgd.os.kill', side_effect=Exception("Test error")), \ + # mock.patch('hostcfgd.syslog.syslog') as mock_syslog: + + # self.mem_stat_cfg.reload_memory_statistics() + + # mock_get_pid.assert_called_once() + # mock_syslog.assert_any_call(mock.ANY, "MemoryStatisticsCfg: Failed to reload MemoryStatisticsDaemon: Test error") + +# def test_shutdown_memory_statistics_success(self): +# """Test successful shutdown of memory statistics daemon""" +# with mock.patch.object(self.mem_stat_cfg, 'get_memory_statistics_pid', return_value=123) as mock_get_pid, \ +# mock.patch('hostcfgd.os.kill') as mock_kill, \ +# mock.patch.object(self.mem_stat_cfg, 'wait_for_shutdown') as mock_wait, \ +# mock.patch('hostcfgd.syslog.syslog') as mock_syslog: + +# self.mem_stat_cfg.shutdown_memory_statistics() + +# mock_get_pid.assert_called_once() +# mock_kill.assert_called_once_with(123, signal.SIGTERM) +# mock_wait.assert_called_once_with(123) +# mock_syslog.assert_any_call(mock.ANY, "MemoryStatisticsCfg: Sent SIGTERM to stop MemoryStatisticsDaemon") + + # def test_shutdown_memory_statistics_failure(self): + # """Test shutdown failure with exception""" + # with mock.patch.object(self.mem_stat_cfg, 'get_memory_statistics_pid', return_value=123) as mock_get_pid, \ + # mock.patch('hostcfgd.os.kill', side_effect=Exception("Test error")), \ + # mock.patch('hostcfgd.syslog.syslog') as mock_syslog: + + # self.mem_stat_cfg.shutdown_memory_statistics() + + # mock_get_pid.assert_called_once() + # mock_syslog.assert_any_call(mock.ANY, "MemoryStatisticsCfg: Failed to shutdown MemoryStatisticsDaemon: Test error") + + # def test_wait_for_shutdown_success(self): + # """Test successful wait for shutdown""" + # mock_process = mock.Mock() + # with mock.patch('hostcfgd.psutil.Process', return_value=mock_process) as mock_process_class, \ + # mock.patch('hostcfgd.syslog.syslog') as mock_syslog: + + # self.mem_stat_cfg.wait_for_shutdown(123) + + # mock_process_class.assert_called_once_with(123) + # mock_process.wait.assert_called_once_with(timeout=10) + # mock_syslog.assert_any_call(mock.ANY, "MemoryStatisticsCfg: MemoryStatisticsDaemon stopped gracefully") + +# def test_process_name_mismatch(self): +# """Test when process name doesn't match expected daemon name""" +# mock_process = mock.Mock() +# mock_process.name.return_value = "wrong_process_name" + +# with mock.patch('builtins.open', mock.mock_open(read_data="123")), \ +# mock.patch('hostcfgd.psutil.pid_exists', return_value=True), \ +# mock.patch('hostcfgd.psutil.Process', return_value=mock_process), \ +# mock.patch('hostcfgd.syslog.syslog') as mock_syslog: + +# pid = self.mem_stat_cfg.get_memory_statistics_pid() + +# self.assertIsNone(pid) +# mock_syslog.assert_any_call(mock.ANY, "MemoryStatisticsCfg: PID 123 does not correspond to memory_statistics_service.py.") + + + + +class TestMemoryStatisticsCfgd(TestCase): + """Test suite for MemoryStatisticsCfg class which handles memory statistics configuration and daemon management.""" + + def setUp(self): + """Set up test environment before each test case.""" + MockConfigDb.CONFIG_DB['MEMORY_STATISTICS'] = { + 'memory_statistics': { + 'enabled': 'false', + 'sampling_interval': '5', + 'retention_period': '15' + } + } + self.mem_stat_cfg = hostcfgd.MemoryStatisticsCfg(MockConfigDb.CONFIG_DB) + + def tearDown(self): + """Clean up after each test case.""" + MockConfigDb.CONFIG_DB = {} + + # Group 1: Configuration Loading Tests + def test_load_with_invalid_key(self): + """ + Test loading configuration with an invalid key. + Ensures the system properly logs when encountering unknown configuration parameters. + """ + config = {'invalid_key': 'value', 'enabled': 'true'} + with mock.patch('hostcfgd.syslog.syslog') as mock_syslog: + self.mem_stat_cfg.load(config) + mock_syslog.assert_any_call(mock.ANY, "MemoryStatisticsCfg: Invalid key 'invalid_key' in initial configuration.") + + def test_load_with_empty_config(self): + """ + Test loading an empty configuration. + Verifies system behavior when no configuration is provided. + """ + with mock.patch('hostcfgd.syslog.syslog') as mock_syslog: + self.mem_stat_cfg.load(None) + mock_syslog.assert_any_call(mock.ANY, "MemoryStatisticsCfg: Loading initial configuration") + + # Group 2: Configuration Update Tests + def test_memory_statistics_update_invalid_key(self): + """ + Test updating configuration with an invalid key. + Ensures system properly handles and logs attempts to update non-existent configuration parameters. + """ + with mock.patch('hostcfgd.syslog.syslog') as mock_syslog: + self.mem_stat_cfg.memory_statistics_update('invalid_key', 'value') + mock_syslog.assert_any_call(mock.ANY, "MemoryStatisticsCfg: Invalid key 'invalid_key' received.") + + def test_memory_statistics_update_invalid_numeric_value(self): + """ + Test updating numeric configuration with invalid value. + Verifies system properly validates numeric input parameters. + """ + with mock.patch('hostcfgd.syslog.syslog') as mock_syslog: + self.mem_stat_cfg.memory_statistics_update('sampling_interval', '-1') + mock_syslog.assert_any_call(mock.ANY, "MemoryStatisticsCfg: Invalid value '-1' for key 'sampling_interval'. Must be a positive integer.") + + def test_memory_statistics_update_same_value(self): + """ + Test updating configuration with the same value. + Ensures system doesn't perform unnecessary updates when value hasn't changed. + """ + with mock.patch.object(self.mem_stat_cfg, 'apply_setting') as mock_apply: + self.mem_stat_cfg.memory_statistics_update('sampling_interval', '5') + mock_apply.assert_not_called() + + # Group 3: Daemon Management Tests + @mock.patch('hostcfgd.subprocess.Popen') + @mock.patch('hostcfgd.os.kill') + def test_restart_memory_statistics_success(self, mock_kill, mock_popen): + """ + Test successful restart of the memory statistics daemon. + Verifies proper shutdown of existing process and startup of new process. + """ + with mock.patch('hostcfgd.syslog.syslog'): + with mock.patch.object(self.mem_stat_cfg, 'get_memory_statistics_pid', return_value=123): + self.mem_stat_cfg.restart_memory_statistics() + mock_kill.assert_called_with(123, signal.SIGTERM) + mock_popen.assert_called_once() + + @mock.patch('hostcfgd.subprocess.Popen') + def test_restart_memory_statistics_failure(self, mock_popen): + """ + Test failed restart of memory statistics daemon. + Ensures proper error handling when daemon fails to start. + """ + mock_popen.side_effect = Exception("Failed to start") + with mock.patch('hostcfgd.syslog.syslog') as mock_syslog: + self.mem_stat_cfg.restart_memory_statistics() + mock_syslog.assert_any_call(mock.ANY, "MemoryStatisticsCfg: Failed to start MemoryStatisticsDaemon: Failed to start") + + # Group 4: PID Management Tests + def test_get_memory_statistics_pid_success(self): + """ + Test successful retrieval of daemon PID. + Verifies proper PID retrieval when daemon is running correctly. + """ + mock_process = mock.Mock() + mock_process.name.return_value = "memory_statistics_service.py" + + with mock.patch('builtins.open', mock.mock_open(read_data="123")), \ + mock.patch('hostcfgd.psutil.pid_exists', return_value=True), \ + mock.patch('hostcfgd.psutil.Process', return_value=mock_process): + pid = self.mem_stat_cfg.get_memory_statistics_pid() + self.assertEqual(pid, 123) + + def test_get_memory_statistics_pid_file_not_found(self): + """ + Test PID retrieval when PID file doesn't exist. + Ensures proper handling of missing PID file. + """ + with mock.patch('builtins.open', side_effect=FileNotFoundError): + with mock.patch('hostcfgd.syslog.syslog') as mock_syslog: + pid = self.mem_stat_cfg.get_memory_statistics_pid() + self.assertIsNone(pid) + mock_syslog.assert_any_call(mock.ANY, "MemoryStatisticsCfg: PID file not found. Daemon might not be running.") + + def test_get_memory_statistics_pid_invalid_content(self): + """ + Test PID retrieval when PID file contains invalid content. + Ensures proper handling and error logging when PID file is corrupted or contains non-numeric data. + """ + mock_open = mock.mock_open(read_data="invalid") + with mock.patch('builtins.open', mock_open): + with mock.patch('hostcfgd.syslog.syslog') as mock_syslog: + pid = self.mem_stat_cfg.get_memory_statistics_pid() + self.assertIsNone(pid) + mock_syslog.assert_any_call(mock.ANY, "MemoryStatisticsCfg: PID file contents invalid.") + + @mock.patch('hostcfgd.psutil.pid_exists', return_value=True) + @mock.patch('hostcfgd.psutil.Process') + def test_get_memory_statistics_pid_wrong_process(self, mock_process, mock_pid_exists): + """ + Test PID retrieval when process exists but name doesn't match expected daemon name. + Verifies proper handling when PID belongs to a different process than the memory statistics daemon. + """ + mock_process_instance = mock.Mock() + mock_process_instance.name.return_value = "wrong_process" + mock_process.return_value = mock_process_instance + + mock_open = mock.mock_open(read_data="123") + with mock.patch('builtins.open', mock_open): + with mock.patch('hostcfgd.syslog.syslog') as mock_syslog: + pid = self.mem_stat_cfg.get_memory_statistics_pid() + self.assertIsNone(pid) + mock_syslog.assert_any_call(mock.ANY, "MemoryStatisticsCfg: PID 123 does not correspond to memory_statistics_service.py.") + + @mock.patch('hostcfgd.psutil.pid_exists', return_value=False) + def test_get_memory_statistics_pid_nonexistent(self, mock_pid_exists): + """Test get_memory_statistics_pid when PID doesn't exist""" + mock_open = mock.mock_open(read_data="123") + with mock.patch('builtins.open', mock_open): + with mock.patch('hostcfgd.syslog.syslog') as mock_syslog: + pid = self.mem_stat_cfg.get_memory_statistics_pid() + self.assertIsNone(pid) + mock_syslog.assert_any_call(mock.ANY, "MemoryStatisticsCfg: PID does not exist.") + + # Group 5: Enable/Disable Tests + def test_memory_statistics_enable(self): + """ + Test enabling memory statistics functionality. + Verifies proper activation of memory statistics monitoring. + """ + with mock.patch.object(self.mem_stat_cfg, 'restart_memory_statistics') as mock_restart: + self.mem_stat_cfg.memory_statistics_update('enabled', 'true') + mock_restart.assert_called_once() + self.assertEqual(self.mem_stat_cfg.cache['enabled'], 'true') + + def test_apply_setting_with_non_enabled_key(self): + """Test apply_setting with sampling_interval or retention_period""" + with mock.patch.object(self.mem_stat_cfg, 'reload_memory_statistics') as mock_reload: + self.mem_stat_cfg.apply_setting('sampling_interval', '10') + mock_reload.assert_called_once() + + def test_apply_setting_with_enabled_false(self): + """Test apply_setting with enabled=false""" + with mock.patch.object(self.mem_stat_cfg, 'shutdown_memory_statistics') as mock_shutdown: + self.mem_stat_cfg.apply_setting('enabled', 'false') + mock_shutdown.assert_called_once() + + def test_memory_statistics_disable(self): + """ + Test disabling memory statistics functionality. + Ensures proper deactivation of memory statistics monitoring. + """ + self.mem_stat_cfg.cache['enabled'] = 'true' + with mock.patch.object(self.mem_stat_cfg, 'apply_setting') as mock_apply: + self.mem_stat_cfg.memory_statistics_update('enabled', 'false') + mock_apply.assert_called_once_with('enabled', 'false') + self.assertEqual(self.mem_stat_cfg.cache['enabled'], 'false') + + def test_memory_statistics_disable_with_shutdown(self): + """Test disabling memory statistics with full shutdown chain""" + # First set the initial state to enabled + self.mem_stat_cfg.cache['enabled'] = 'true' + + # Mock both get_memory_statistics_pid and os.kill to simulate full shutdown + with mock.patch.object(self.mem_stat_cfg, 'get_memory_statistics_pid', return_value=123) as mock_get_pid, \ + mock.patch('hostcfgd.os.kill') as mock_kill, \ + mock.patch.object(self.mem_stat_cfg, 'wait_for_shutdown') as mock_wait: + + self.mem_stat_cfg.memory_statistics_update('enabled', 'false') + + # Verify the shutdown sequence + mock_get_pid.assert_called_once() + mock_kill.assert_called_once_with(123, signal.SIGTERM) + mock_wait.assert_called_once_with(123) + + # Verify the cache was updated + self.assertEqual(self.mem_stat_cfg.cache['enabled'], 'false') + + def test_memory_statistics_disable_no_running_daemon(self): + """Test disabling memory statistics when daemon is not running""" + # First set the initial state to enabled + self.mem_stat_cfg.cache['enabled'] = 'true' + + # Mock get_memory_statistics_pid to return None (no running daemon) + with mock.patch.object(self.mem_stat_cfg, 'get_memory_statistics_pid', return_value=None) as mock_get_pid: + self.mem_stat_cfg.memory_statistics_update('enabled', 'false') + + # Verify get_pid was called + mock_get_pid.assert_called_once() + + # Verify the cache was updated despite no running daemon + self.assertEqual(self.mem_stat_cfg.cache['enabled'], 'false') + + # Group 6: Reload Tests + def test_reload_memory_statistics_success(self): + """ + Test successful reload of memory statistics configuration. + Verifies proper handling of configuration updates without restart. + """ + with mock.patch.object(self.mem_stat_cfg, 'get_memory_statistics_pid', return_value=123), \ + mock.patch('hostcfgd.os.kill') as mock_kill, \ + mock.patch('hostcfgd.syslog.syslog'): + self.mem_stat_cfg.reload_memory_statistics() + mock_kill.assert_called_once_with(123, signal.SIGHUP) + + def test_reload_memory_statistics_no_pid(self): + """ + Test reload when daemon is not running. + Ensures proper handling of reload request when daemon is inactive. + """ + with mock.patch.object(self.mem_stat_cfg, 'get_memory_statistics_pid', return_value=None), \ + mock.patch('hostcfgd.os.kill') as mock_kill: + self.mem_stat_cfg.reload_memory_statistics() + mock_kill.assert_not_called() + + def test_reload_memory_statistics_failure(self): + """Test reload failure with exception""" + with mock.patch.object(self.mem_stat_cfg, 'get_memory_statistics_pid', return_value=123) as mock_get_pid, \ + mock.patch('hostcfgd.os.kill', side_effect=Exception("Test error")), \ + mock.patch('hostcfgd.syslog.syslog') as mock_syslog: + + self.mem_stat_cfg.reload_memory_statistics() + + mock_get_pid.assert_called_once() + mock_syslog.assert_any_call(mock.ANY, "MemoryStatisticsCfg: Failed to reload MemoryStatisticsDaemon: Test error") + + # Group 7: Shutdown Tests + def test_shutdown_memory_statistics_success(self): + """ + Test successful shutdown of memory statistics daemon. + Verifies proper termination of the daemon process. + """ + with mock.patch.object(self.mem_stat_cfg, 'get_memory_statistics_pid', return_value=123), \ + mock.patch('hostcfgd.os.kill') as mock_kill, \ + mock.patch.object(self.mem_stat_cfg, 'wait_for_shutdown'), \ + mock.patch('hostcfgd.syslog.syslog'): + self.mem_stat_cfg.shutdown_memory_statistics() + mock_kill.assert_called_once_with(123, signal.SIGTERM) + + def test_wait_for_shutdown_timeout(self): + """ + Test shutdown behavior when daemon doesn't respond to termination signal. + Ensures proper handling of timeout during shutdown. + """ + mock_process = mock.Mock() + mock_process.wait.side_effect = psutil.TimeoutExpired(123, 10) + with mock.patch('hostcfgd.psutil.Process', return_value=mock_process), \ + mock.patch('hostcfgd.syslog.syslog') as mock_syslog: + self.mem_stat_cfg.wait_for_shutdown(123) + mock_syslog.assert_any_call(mock.ANY, "MemoryStatisticsCfg: Timed out while waiting for daemon (PID 123) to shut down.") + + @mock.patch('hostcfgd.psutil.Process') + def test_wait_for_shutdown_no_process(self, mock_process): + """Test shutdown waiting when process doesn't exist""" + mock_process.side_effect = psutil.NoSuchProcess(123) + + with mock.patch('hostcfgd.syslog.syslog') as mock_syslog: + self.mem_stat_cfg.wait_for_shutdown(123) + mock_syslog.assert_any_call(mock.ANY, "MemoryStatisticsCfg: MemoryStatisticsDaemon process not found.") + + def test_shutdown_memory_statistics_failure(self): + """Test shutdown failure with exception""" + with mock.patch.object(self.mem_stat_cfg, 'get_memory_statistics_pid', return_value=123) as mock_get_pid, \ + mock.patch('hostcfgd.os.kill', side_effect=Exception("Test error")), \ + mock.patch('hostcfgd.syslog.syslog') as mock_syslog: + + self.mem_stat_cfg.shutdown_memory_statistics() + + mock_get_pid.assert_called_once() + mock_syslog.assert_any_call(mock.ANY, "MemoryStatisticsCfg: Failed to shutdown MemoryStatisticsDaemon: Test error") + + def test_wait_for_shutdown_success(self): + """Test successful wait for shutdown""" + mock_process = mock.Mock() + with mock.patch('hostcfgd.psutil.Process', return_value=mock_process) as mock_process_class, \ + mock.patch('hostcfgd.syslog.syslog') as mock_syslog: + + self.mem_stat_cfg.wait_for_shutdown(123) + + mock_process_class.assert_called_once_with(123) + mock_process.wait.assert_called_once_with(timeout=10) + mock_syslog.assert_any_call(mock.ANY, "MemoryStatisticsCfg: MemoryStatisticsDaemon stopped gracefully") + + # Group 8: Error Handling Tests + def test_memory_statistics_update_exception_handling(self): + """ + Test exception handling during configuration updates. + Verifies proper error handling and logging of exceptions. + """ + with mock.patch.object(self.mem_stat_cfg, 'apply_setting', side_effect=Exception("Test error")), \ + mock.patch('hostcfgd.syslog.syslog') as mock_syslog: + self.mem_stat_cfg.memory_statistics_update('enabled', 'true') + mock_syslog.assert_any_call(mock.ANY, "MemoryStatisticsCfg: Failed to manage MemoryStatisticsDaemon: Test error") + + def test_apply_setting_exception(self): + """Test exception handling in apply_setting""" + with mock.patch.object(self.mem_stat_cfg, 'restart_memory_statistics', + side_effect=Exception("Test error")): + with mock.patch('hostcfgd.syslog.syslog') as mock_syslog: + self.mem_stat_cfg.apply_setting('enabled', 'true') + mock_syslog.assert_any_call(mock.ANY, + "MemoryStatisticsCfg: Exception in apply_setting() for key 'enabled': Test error") + + @mock.patch('hostcfgd.psutil.Process') + def test_get_memory_statistics_pid_exception(self, mock_process): + """Test general exception handling in get_memory_statistics_pid""" + mock_process.side_effect = Exception("Unexpected error") + mock_open = mock.mock_open(read_data="123") + + with mock.patch('hostcfgd.psutil.pid_exists', return_value=True): + with mock.patch('builtins.open', mock_open): + with mock.patch('hostcfgd.syslog.syslog') as mock_syslog: + pid = self.mem_stat_cfg.get_memory_statistics_pid() + self.assertIsNone(pid) + mock_syslog.assert_any_call(mock.ANY, + "MemoryStatisticsCfg: Exception failed to retrieve MemoryStatisticsDaemon PID: Unexpected error") + + def test_memory_statistics_handler_exception(self): + """Test exception handling in memory_statistics_handler""" + daemon = hostcfgd.HostConfigDaemon() + with mock.patch.object(daemon.memorystatisticscfg, 'memory_statistics_update', + side_effect=Exception("Handler error")): + with mock.patch('hostcfgd.syslog.syslog') as mock_syslog: + daemon.memory_statistics_handler('enabled', None, 'true') + mock_syslog.assert_any_call(mock.ANY, + "MemoryStatisticsCfg: Error while handling memory statistics update: Handler error") + + @mock.patch('hostcfgd.psutil.Process') + def test_wait_for_shutdown_general_exception(self, mock_process): + """Test general exception handling in wait_for_shutdown""" + mock_process.side_effect = Exception("Unexpected shutdown error") + with mock.patch('hostcfgd.syslog.syslog') as mock_syslog: + self.mem_stat_cfg.wait_for_shutdown(123) + mock_syslog.assert_any_call(mock.ANY, + "MemoryStatisticsCfg: Exception in wait_for_shutdown(): Unexpected shutdown error") + + def test_process_name_mismatch(self): + """ + Test handling of process name mismatches. + Ensures proper validation of daemon process identity. + """ + mock_process = mock.Mock() + mock_process.name.return_value = "wrong_process_name" + + with mock.patch('builtins.open', mock.mock_open(read_data="123")), \ + mock.patch('hostcfgd.psutil.pid_exists', return_value=True), \ + mock.patch('hostcfgd.psutil.Process', return_value=mock_process), \ + mock.patch('hostcfgd.syslog.syslog') as mock_syslog: + pid = self.mem_stat_cfg.get_memory_statistics_pid() + self.assertIsNone(pid) + mock_syslog.assert_any_call(mock.ANY, "MemoryStatisticsCfg: PID 123 does not correspond to memory_statistics_service.py.") \ No newline at end of file diff --git a/tests/hostcfgd/test_vectors.py b/tests/hostcfgd/test_vectors.py index afa50564..44217477 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": "false", + "sampling_interval": "5", + "retention_period": "15" + } + }, "MGMT_INTERFACE": { "eth0|1.2.3.4/24": {} },