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 3 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
110 changes: 107 additions & 3 deletions scripts/hostcfgd
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,22 @@ import syslog
import signal
import re
import jinja2
import time
import json
import importlib
from shutil import copy2
from datetime import datetime
from sonic_py_common import device_info
from sonic_py_common.general import check_output_pipe
from swsscommon.swsscommon import ConfigDBConnector, DBConnector, Table
from swsscommon import swsscommon
from swsscommon import RestartWaiter
from sonic_installer import bootloader
hostcfg_file_path = os.path.abspath(__file__)
hostcfg_dir_path = os.path.dirname(hostcfg_file_path)
sys.path.append(hostcfg_dir_path)
import ldap
importlib.reload(swsscommon)

# FILE
PAM_AUTH_CONF = "/etc/pam.d/common-auth-sonic"
Expand Down Expand Up @@ -1716,6 +1720,97 @@ class FipsCfg(object):
syslog.syslog(syslog.LOG_INFO, f'FipsCfg: update the FIPS enforce option {self.enforce}.')
loader.set_fips(image, self.enforce)

class Memory_StatisticsCfg:
"""
Memory_StatisticsCfg class handles the configuration updates for the MemoryStatisticsDaemon.
It listens to ConfigDB changes and applies them by restarting, shutting down, or reloading
the MemoryStatisticsDaemon.
"""

def __init__(self, config_db):
self.cache = {
"enabled": "false",
"retention": "15",
"sampling": "5"
}
self.config_db = config_db # Store config_db instance for further use

def load(self, memory_statistics_config: dict):
"""Load initial memory statistics configuration."""
syslog.syslog(syslog.LOG_INFO, 'Memory_StatisticsCfg: Load initial configuration')

if not memory_statistics_config:
memory_statistics_config = {}

# Call memory_statistics_message to handle the initial config load for each key
self.memory_statistics_message("enabled", memory_statistics_config.get("enabled", "false"))
self.memory_statistics_message("retention", memory_statistics_config.get("retention", "15"))
self.memory_statistics_message("sampling", memory_statistics_config.get("sampling", "5"))

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.
"""
# Ensure data is a string or convertible to the required value
if not isinstance(data, str):
data = str(data)

# Check if any value has changed
if data != self.cache.get(key):
syslog.syslog(syslog.LOG_INFO, f"Memory_StatisticsCfg: Detected change in '{key}'")

try:
if key == "enabled":
enabled = data.lower() == "true"
if enabled:
self.restart_memory_statistics() # Start or restart the daemon
else:
self.shutdown_memory_statistics() # Stop the daemon if disabled
else:
# If other keys (like sampling/retention) are changed, just reload the daemon config
self.reload_memory_statistics()

# Update cache with the new value
self.cache[key] = data
except Exception as e:
syslog.syslog(syslog.LOG_ERR, f'Memory_StatisticsCfg: Failed to manage MemoryStatisticsDaemon: {e}')

def restart_memory_statistics(self):
"""Restart the memory statistics daemon."""
self.shutdown_memory_statistics() # Ensure the daemon is stopped before restarting
time.sleep(1) # Brief delay to allow shutdown
syslog.syslog(syslog.LOG_INFO, "Memory_StatisticsCfg: Starting MemoryStatisticsDaemon")
try:
subprocess.Popen(['/usr/bin/memorystatsd'])
except Exception as e:
syslog.syslog(syslog.LOG_ERR, f"Memory_StatisticsCfg: Failed to start MemoryStatisticsDaemon: {e}")

def reload_memory_statistics(self):
"""Send SIGHUP to the MemoryStatisticsDaemon to reload its configuration."""
pid = self.get_memory_statistics_pid()
if pid:
os.kill(pid, signal.SIGHUP) # Notify daemon to reload its configuration
syslog.syslog(syslog.LOG_INFO, "Memory_StatisticsCfg: Sent SIGHUP to reload daemon configuration")

def shutdown_memory_statistics(self):
"""Send SIGTERM to stop the MemoryStatisticsDaemon gracefully."""
pid = self.get_memory_statistics_pid()
if pid:
os.kill(pid, signal.SIGTERM) # Graceful shutdown
syslog.syslog(syslog.LOG_INFO, "Memory_StatisticsCfg: Sent SIGTERM to stop MemoryStatisticsDaemon")

def get_memory_statistics_pid(self):
"""Retrieve the PID of the running MemoryStatisticsDaemon."""
try:
with open('/var/run/memorystatsd.pid', 'r') as pid_file:
pid = int(pid_file.read().strip())
return pid
except Exception as e:
syslog.syslog(syslog.LOG_ERR, f"Memory_StatisticsCfg: Failed to retrieve MemoryStatisticsDaemon PID: {e}")
return None

class SerialConsoleCfg:

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

return


class HostConfigDaemon:
def __init__(self):
self.state_db_conn = DBConnector(STATE_DB, 0)
Expand All @@ -1764,6 +1858,8 @@ class HostConfigDaemon:
# Initialize KDump Config and set the config to default if nothing is provided
self.kdumpCfg = KdumpCfg(self.config_db)

self.memory_statisticsCfg = Memory_StatisticsCfg(self.config_db)

# Initialize IpTables
self.iptables = Iptables()

Expand Down Expand Up @@ -1816,6 +1912,7 @@ class HostConfigDaemon:
passwh = init_data['PASSW_HARDENING']
ssh_server = init_data['SSH_SERVER']
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 @@ -1830,6 +1927,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.memory_statisticsCfg.load(memory_statistics)
self.passwcfg.load(passwh)
self.sshscfg.load(ssh_server)
self.devmetacfg.load(dev_meta)
Expand Down Expand Up @@ -1959,6 +2057,10 @@ 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...')
self.memory_statisticsCfg.memory_statistics_update(key, data)

def device_metadata_handler(self, key, op, data):
syslog.syslog(syslog.LOG_INFO, 'DeviceMeta handler...')
self.devmetacfg.hostname_update(data)
Expand Down Expand Up @@ -2035,6 +2137,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,
Expand Down Expand Up @@ -2077,5 +2182,4 @@ def main():
daemon.start()

if __name__ == "__main__":
main()

main()
40 changes: 37 additions & 3 deletions tests/hostcfgd/hostcfgd_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@
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 unittest.mock import patch
from pyfakefs.fake_filesystem_unittest import patchfs
from deepdiff import DeepDiff
from unittest.mock import call
Expand Down Expand Up @@ -216,6 +215,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.
Expand Down Expand Up @@ -324,6 +324,40 @@ def test_mgmtiface_event(self):
]
mocked_check_output.assert_has_calls(expected)

@patch('sonic_py_common.ConfigDBConnector', autospec=True)
def test_memory_statistics_event(self, mock_config_db_connector):
# Mock the ConfigDBConnector instance methods
mock_instance = mock_config_db_connector.return_value
# Ensure get_table returns the correct nested structur
mock_instance.get_table.return_value = HOSTCFG_DAEMON_CFG_DB['MEMORY_STATISTICS']['memory_statistics']

# Patch subprocess.Popen and check_call
with mock.patch('hostcfgd.subprocess.Popen') as mocked_popen, \
mock.patch('hostcfgd.subprocess.check_call') as mocked_check_call:

# Create the daemon instance
daemon = hostcfgd.HostConfigDaemon()
# Load config using the correct nested dictionary
daemon.memory_statisticsCfg.load(HOSTCFG_DAEMON_CFG_DB['MEMORY_STATISTICS']['memory_statistics'])

# Mock subprocess.Popen behavior
popen_mock = mock.Mock()
attrs = {'communicate.return_value': ('output', 'error')}
popen_mock.configure_mock(**attrs)
mocked_popen.return_value = popen_mock

# Trigger the event handler via event queue
daemon.event_queue.append(('MEMORY_STATISTICS', 'memory_statistics'))
daemon.memory_statistics_handler('enabled', 'SET', 'true')

# Define expected subprocess calls
expected_calls = [
mock.call(['/usr/bin/memorystatsd']),
]

# Check if subprocess Popen was called with correct arguments
mocked_popen.assert_has_calls(expected_calls, any_order=True)

def test_dns_events(self):
MockConfigDb.set_config_db(HOSTCFG_DAEMON_CFG_DB)
MockConfigDb.event_queue = [('DNS_NAMESERVER', '1.1.1.1')]
Expand Down Expand Up @@ -353,4 +387,4 @@ def test_load(self):

data = {}
dns_cfg.load(data)
dns_cfg.dns_update.assert_called()
dns_cfg.dns_update.assert_called()
10 changes: 9 additions & 1 deletion tests/hostcfgd/test_vectors.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"PASSW_HARDENING": {},
"SSH_SERVER": {},
"KDUMP": {},
"MEMORY_STATISTICS": {},
"NTP": {},
"NTP_SERVER": {},
"LOOPBACK_INTERFACE": {},
Expand Down Expand Up @@ -79,6 +80,13 @@
"timezone": "Europe/Kyiv"
}
},
"MEMORY_STATISTICS": {
"memory_statistics": {
"enabled": "true",
"retention_time": "15",
"sampling_interval": "5"
}
},
"MGMT_INTERFACE": {
"eth0|1.2.3.4/24": {}
},
Expand All @@ -90,4 +98,4 @@
"DNS_NAMESERVER": {
"1.1.1.1": {}
},
}
}
Loading