Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for Memory Statistics Host-Services #167

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 180 additions & 4 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 HostConfigDaemon:
def __init__(self):
self.state_db_conn = DBConnector(STATE_DB, 0)
Expand All @@ -1842,6 +2006,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 @@ -1896,6 +2063,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 @@ -1914,6 +2082,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 @@ -2043,6 +2212,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 @@ -2109,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 @@ -2123,7 +2300,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 @@ -2170,4 +2347,3 @@ def main():

if __name__ == "__main__":
main()

Loading
Loading