Skip to content

Commit

Permalink
Support for Memory Statistics Host-Services (#167)
Browse files Browse the repository at this point in the history
* Added memory statistics host services

* updated the hostcfgd file

* set the indentation and parmater names of schema

Signed-off-by: Arham-Nasir <[email protected]>

* added key validation and log error for handling updates

Signed-off-by: Arham-Nasir <[email protected]>

* added memory_statistics message function

Signed-off-by: Arham-Nasir <[email protected]>

* update Memory_StatisticsCfg class

Signed-off-by: Arham-Nasir <[email protected]>

* Modified scripts/hostcfgd

* update the MemoryStatisticsCfg class

Signed-off-by: Arham-Nasir <[email protected]>

* update the tests cases

Signed-off-by: Arham-Nasir <[email protected]>

* updated the hostcfgd and hostcfgd_test files

Signed-off-by: Arham-Nasir <[email protected]>

* Add comprehensive test cases for MemoryStatisticsCfg functionalities

Signed-off-by: Arham-Nasir <[email protected]>

* Fix test_get_memory_statistics_pid_exception

Signed-off-by: Arham-Nasir <[email protected]>

* Improve test coverage for daemon management and error handling

Signed-off-by: Arham-Nasir <[email protected]>

* Update test file

Signed-off-by: Arham-Nasir <[email protected]>

* update test file

Signed-off-by: Arham-Nasir <[email protected]>

* update testfile

Signed-off-by: Arham-Nasir <[email protected]>

---------

Signed-off-by: Arham-Nasir <[email protected]>
Co-authored-by: Arham-Nasir <[email protected]>
Co-authored-by: Rida Hanif <[email protected]>
Co-authored-by: Arham-Nasir <[email protected]>
  • Loading branch information
4 people authored Dec 10, 2024
1 parent d455924 commit b0b3ca5
Show file tree
Hide file tree
Showing 3 changed files with 569 additions and 8 deletions.
185 changes: 180 additions & 5 deletions scripts/hostcfgd
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:

Expand Down Expand Up @@ -1748,7 +1914,6 @@ class SerialConsoleCfg:

return


class BannerCfg(object):
"""
Banner Config Daemon
Expand Down Expand Up @@ -1826,7 +1991,6 @@ class BannerCfg(object):
for k,v in data.items():
self.cache[k] = v


class LoggingCfg(object):
"""Logging Config Daemon
Expand Down Expand Up @@ -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)
Expand All @@ -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()

Expand Down Expand Up @@ -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, {})
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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))
Expand Down Expand Up @@ -2221,4 +2397,3 @@ def main():

if __name__ == "__main__":
main()

Loading

0 comments on commit b0b3ca5

Please sign in to comment.