From b0b3ca54614ac6fd350f2609f1ebd74757b72f14 Mon Sep 17 00:00:00 2001 From: kanza-latif Date: Tue, 10 Dec 2024 17:53:11 +0500 Subject: [PATCH] Support for Memory Statistics Host-Services (#167) * Added memory statistics host services * updated the hostcfgd file * set the indentation and parmater names of schema Signed-off-by: Arham-Nasir * added key validation and log error for handling updates Signed-off-by: Arham-Nasir * added memory_statistics message function Signed-off-by: Arham-Nasir * update Memory_StatisticsCfg class Signed-off-by: Arham-Nasir * Modified scripts/hostcfgd * update the MemoryStatisticsCfg class Signed-off-by: Arham-Nasir * update the tests cases Signed-off-by: Arham-Nasir * updated the hostcfgd and hostcfgd_test files Signed-off-by: Arham-Nasir * Add comprehensive test cases for MemoryStatisticsCfg functionalities Signed-off-by: Arham-Nasir * Fix test_get_memory_statistics_pid_exception Signed-off-by: Arham-Nasir * Improve test coverage for daemon management and error handling Signed-off-by: Arham-Nasir * Update test file Signed-off-by: Arham-Nasir * update test file Signed-off-by: Arham-Nasir * update testfile Signed-off-by: Arham-Nasir --------- Signed-off-by: Arham-Nasir Co-authored-by: Arham-Nasir Co-authored-by: Rida Hanif Co-authored-by: Arham-Nasir <100487254+Arham-Nasir@users.noreply.github.com> --- scripts/hostcfgd | 185 ++++++++++++++- tests/hostcfgd/hostcfgd_test.py | 384 +++++++++++++++++++++++++++++++- tests/hostcfgd/test_vectors.py | 8 + 3 files changed, 569 insertions(+), 8 deletions(-) diff --git a/scripts/hostcfgd b/scripts/hostcfgd index 77d75a0d..3c247c22 100644 --- a/scripts/hostcfgd +++ b/scripts/hostcfgd @@ -9,6 +9,8 @@ import syslog import signal import re import jinja2 +import psutil +import time import json from shutil import copy2 from datetime import datetime @@ -1715,7 +1717,171 @@ class FipsCfg(object): return 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: @@ -1748,7 +1914,6 @@ class SerialConsoleCfg: return - class BannerCfg(object): """ Banner Config Daemon @@ -1826,7 +1991,6 @@ class BannerCfg(object): for k,v in data.items(): self.cache[k] = v - class LoggingCfg(object): """Logging Config Daemon @@ -1864,7 +2028,6 @@ class LoggingCfg(object): # Update cache self.cache[key] = data - class HostConfigDaemon: def __init__(self): self.state_db_conn = DBConnector(STATE_DB, 0) @@ -1880,6 +2043,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() @@ -1937,6 +2103,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, {}) @@ -1956,6 +2123,7 @@ class HostConfigDaemon: self.kdumpCfg.load(kdump) self.passwcfg.load(passwh) self.sshscfg.load(ssh_server) + self.memorystatisticscfg.load(memory_statistics) self.devmetacfg.load(dev_meta) self.mgmtifacecfg.load(mgmt_ifc, mgmt_vrf) self.rsyslogcfg.load(syslog_cfg, syslog_srv) @@ -2086,6 +2254,13 @@ 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) @@ -2156,6 +2331,7 @@ class HostConfigDaemon: self.config_db.subscribe('LDAP_SERVER', make_callback(self.ldap_server_handler)) self.config_db.subscribe('PASSW_HARDENING', make_callback(self.passwh_handler)) self.config_db.subscribe('SSH_SERVER', make_callback(self.ssh_handler)) + self.config_db.subscribe('MEMORY_STATISTICS',make_callback(self.memory_statistics_handler)) # Handle SERIAL_CONSOLE self.config_db.subscribe('SERIAL_CONSOLE', make_callback(self.serial_console_config_handler)) # Handle IPTables configuration @@ -2170,7 +2346,7 @@ class HostConfigDaemon: # Handle DEVICE_MEATADATA changes self.config_db.subscribe(swsscommon.CFG_DEVICE_METADATA_TABLE_NAME, make_callback(self.device_metadata_handler)) - + # Handle MGMT_VRF_CONFIG changes self.config_db.subscribe(swsscommon.CFG_MGMT_VRF_CONFIG_TABLE_NAME, make_callback(self.mgmt_vrf_handler)) @@ -2221,4 +2397,3 @@ def main(): if __name__ == "__main__": main() - diff --git a/tests/hostcfgd/hostcfgd_test.py b/tests/hostcfgd/hostcfgd_test.py index 9ec3f658..fbb432f7 100644 --- a/tests/hostcfgd/hostcfgd_test.py +++ b/tests/hostcfgd/hostcfgd_test.py @@ -1,17 +1,17 @@ import os import sys import time +import signal +import psutil 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 @@ -271,7 +271,7 @@ def test_devicemeta_event(self): with mock.patch('hostcfgd.subprocess') as mocked_subprocess: mocked_syslog.LOG_INFO = original_syslog.LOG_INFO try: - daemon.start() + daemon.start() except TimeoutError: pass @@ -371,3 +371,381 @@ 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 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""" + self.mem_stat_cfg.cache['enabled'] = 'true' + + 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') + + mock_get_pid.assert_called_once() + mock_kill.assert_called_once_with(123, signal.SIGTERM) + mock_wait.assert_called_once_with(123) + + 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""" + self.mem_stat_cfg.cache['enabled'] = 'true' + + 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') + + mock_get_pid.assert_called_once() + + 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": {} },