Skip to content

Commit

Permalink
updated the hostcfgd and hostcfgd_test files
Browse files Browse the repository at this point in the history
Signed-off-by: Arham-Nasir <[email protected]>
  • Loading branch information
Arham-Nasir committed Nov 14, 2024
1 parent c767b70 commit 43b7321
Show file tree
Hide file tree
Showing 2 changed files with 112 additions and 89 deletions.
166 changes: 111 additions & 55 deletions scripts/hostcfgd
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import syslog
import signal
import re
import jinja2
import psutil
import time
import json
from shutil import copy2
Expand Down Expand Up @@ -1719,116 +1720,169 @@ class FipsCfg(object):

class MemoryStatisticsCfg:
"""
MemoryStatisticsCfg class handles the configuration updates for the MemoryStatisticsDaemon.
It listens to ConfigDB changes and applies them by restarting, shutting down, or reloading
the MemoryStatisticsDaemon.
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"] # Valid keys for configuration
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."""
"""
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 # Store config_db instance for further use
self.config_db = config_db

def load(self, memory_statistics_config: dict):
"""Load initial memory statistics configuration."""
syslog.syslog(syslog.LOG_INFO, 'MemoryStatisticsCfg: Load initial configuration')
"""
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')

# Use default values if no config provided
memory_statistics_config = memory_statistics_config or {}
if not memory_statistics_config:
memory_statistics_config = {}

# Load configuration for each valid key
for key in self.VALID_KEYS:
self.memory_statistics_update(key, memory_statistics_config.get(key, self.cache[key]))
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):
"""
Apply memory statistics settings handler.
Args:
key: DB table's key that triggered the change.
data: New table data to process.
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

# Convert data to string if it's not already
data = str(data)
data = str(data)

# Validate numeric keys (retention_period and sampling_interval)
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

# Check if any value has changed
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 # Update cache with the new value
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 key == "enabled":
if data.lower() == "true":
self.restart_memory_statistics()
"""
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.shutdown_memory_statistics()
else:
self.reload_memory_statistics()
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):
"""Restart the memory statistics daemon."""
self.shutdown_memory_statistics() # Stop the daemon before restarting
time.sleep(1) # Optional delay to ensure proper restart
syslog.syslog(syslog.LOG_INFO, "MemoryStatisticsCfg: Starting MemoryStatisticsDaemon")

"""Restarts the memory statistics daemon by first shutting it down (if running) and then starting it again."""
try:
subprocess.Popen(['/usr/bin/memorystatsd']) # Replace with the correct daemon path
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):
"""Send SIGHUP to the MemoryStatisticsDaemon to reload its configuration."""
"""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) # Notify daemon to reload its configuration
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):
"""Send SIGTERM to stop the MemoryStatisticsDaemon gracefully."""
"""Sends a SIGTERM signal to gracefully shut down the daemon."""
pid = self.get_memory_statistics_pid()
if pid:
try:
os.kill(pid, signal.SIGTERM) # Graceful shutdown
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):
"""Retrieve the PID of the running MemoryStatisticsDaemon."""
"""
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('/var/run/memorystatsd.pid', 'r') as pid_file:
with open(self.PID_FILE_PATH, 'r') as pid_file:
pid = int(pid_file.read().strip())
return pid
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.")
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: Failed to retrieve MemoryStatisticsDaemon PID: {e}")
syslog.syslog(syslog.LOG_ERR, f"MemoryStatisticsCfg: {type(e).__name__} failed to retrieve MemoryStatisticsDaemon PID: {e}")
return None

class SerialConsoleCfg:

def __init__(self):
Expand Down Expand Up @@ -1952,6 +2006,7 @@ 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
Expand Down Expand Up @@ -2008,8 +2063,8 @@ 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, {})
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, {})
Expand All @@ -2025,9 +2080,9 @@ 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.memorystatisticscfg.load(memory_statistics)
self.devmetacfg.load(dev_meta)
self.mgmtifacecfg.load(mgmt_ifc, mgmt_vrf)
self.rsyslogcfg.load(syslog_cfg, syslog_srv)
Expand Down Expand Up @@ -2157,9 +2212,12 @@ class HostConfigDaemon:
syslog.syslog(syslog.LOG_INFO, 'Kdump handler...')
self.kdumpCfg.kdump_update(key, data)

def memory_statistics_handler (self, key, op, data):
def memory_statistics_handler(self, key, op, data):
syslog.syslog(syslog.LOG_INFO, 'Memory_Statistics handler...')
self.memorystatisticscfg.memory_statistics_update(key, data)
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...')
Expand Down Expand Up @@ -2227,6 +2285,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
Expand All @@ -2242,9 +2301,6 @@ class HostConfigDaemon:
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,
make_callback(self.mgmt_vrf_handler))
Expand Down
35 changes: 1 addition & 34 deletions tests/hostcfgd/hostcfgd_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -387,21 +387,6 @@ def setUp(self):
def tearDown(self):
MockConfigDb.CONFIG_DB = {}

def test_memory_statistics_load(self):
with mock.patch('hostcfgd.subprocess') as mocked_subprocess:
self.mem_stat_cfg.load(MockConfigDb.CONFIG_DB['MEMORY_STATISTICS'])
mocked_subprocess.Popen.assert_called_once_with(['/usr/bin/memorystatsd'])
self.assertEqual(self.mem_stat_cfg.cache['enabled'], 'false')
self.assertEqual(self.mem_stat_cfg.cache['sampling_interval'], '5')
self.assertEqual(self.mem_stat_cfg.cache['retention_period'], '15')

def test_memory_statistics_update_enabled(self):
with mock.patch('hostcfgd.subprocess') as mocked_subprocess, \
mock.patch('hostcfgd.os.kill') as mocked_kill:
self.mem_stat_cfg.memory_statistics_update('enabled', 'true')
mocked_kill.assert_called_once()
mocked_subprocess.Popen.assert_called_once_with(['/usr/bin/memorystatsd'])

def test_memory_statistics_is_caching_config(self):
self.mem_stat_cfg.cache['enabled'] = 'true'
with mock.patch('hostcfgd.subprocess') as mocked_subprocess:
Expand All @@ -419,22 +404,4 @@ def test_memory_statistics_update_retention_period(self):
with mock.patch('hostcfgd.subprocess') as mocked_subprocess:
self.mem_stat_cfg.memory_statistics_update('retention_period', '30')
mocked_subprocess.Popen.assert_not_called()
self.assertEqual(self.mem_stat_cfg.cache['retention_period'], '30')

def test_memory_statistics_update_invalid_sampling_interval(self):
mem_stat_cfg = hostcfgd.MemoryStatisticsCfg(MockConfigDb.CONFIG_DB)

with mock.patch('hostcfgd.syslog') as mocked_syslog:
mem_stat_cfg.memory_statistics_update('sampling_interval', '-10') # Invalid value

# Assert an error log is made
mocked_syslog.syslog.assert_called_with(mock.ANY, "Memory_StatisticsCfg: Invalid value '-10' for key 'sampling_interval'. Must be a positive integer.")

def test_memory_statistics_update_invalid_retention_period(self):
mem_stat_cfg = hostcfgd.MemoryStatisticsCfg(MockConfigDb.CONFIG_DB)

with mock.patch('hostcfgd.syslog') as mocked_syslog:
mem_stat_cfg.memory_statistics_update('retention_period', 'not_a_number') # Invalid value

# Assert an error log is made
mocked_syslog.syslog.assert_called_with(mock.ANY, "Memory_StatisticsCfg: Invalid value 'not_a_number' for key 'retention_period'. Must be a positive integer.")
self.assertEqual(self.mem_stat_cfg.cache['retention_period'], '30')

0 comments on commit 43b7321

Please sign in to comment.