From 64706a7b4a1950c7c64a3e2788d92a862051d329 Mon Sep 17 00:00:00 2001 From: "kanza.latif" Date: Thu, 26 Sep 2024 14:37:10 +0500 Subject: [PATCH 1/2] Added memory statistics host services --- scripts/hostcfgd | 222 ++++++++++++++++++++++++++++---- tests/hostcfgd/hostcfgd_test.py | 82 +++++++++++- tests/hostcfgd/test_vectors.py | 10 +- 3 files changed, 287 insertions(+), 27 deletions(-) diff --git a/scripts/hostcfgd b/scripts/hostcfgd index 09172945..188cc4d7 100644 --- a/scripts/hostcfgd +++ b/scripts/hostcfgd @@ -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 +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" @@ -54,13 +58,18 @@ LINUX_DEFAULT_PASS_MAX_DAYS = 99999 LINUX_DEFAULT_PASS_WARN_AGE = 7 # Ssh min-max values -SSH_MIN_VALUES={"authentication_retries": 3, "login_timeout": 1, "ports": 1} -SSH_MAX_VALUES={"authentication_retries": 100, "login_timeout": 600, "ports": 65535} -SSH_CONFIG_NAMES={"authentication_retries": "MaxAuthTries" , "login_timeout": "LoginGraceTime"} +SSH_MIN_VALUES={"authentication_retries": 3, "login_timeout": 1, "ports": 1, + "inactivity_timeout": 0, "max_sessions": 0} +SSH_MAX_VALUES={"authentication_retries": 100, "login_timeout": 600, + "ports": 65535, "inactivity_timeout": 35000, + "max_sessions": 100} +SSH_CONFIG_NAMES={"authentication_retries": "MaxAuthTries", + "login_timeout": "LoginGraceTime", "ports": "Port", + "inactivity_timeout": "ClientAliveInterval"} ACCOUNT_NAME = 0 # index of account name -AGE_DICT = { 'MAX_DAYS': {'REGEX_DAYS': r'^PASS_MAX_DAYS[ \t]*(?P\d*)', 'DAYS': 'max_days', 'CHAGE_FLAG': '-M '}, - 'WARN_DAYS': {'REGEX_DAYS': r'^PASS_WARN_AGE[ \t]*(?P\d*)', 'DAYS': 'warn_days', 'CHAGE_FLAG': '-W '} +AGE_DICT = { 'MAX_DAYS': {'REGEX_DAYS': r'^PASS_MAX_DAYS[ \t]*(?P-?\d*)', 'DAYS': 'max_days', 'CHAGE_FLAG': '-M '}, + 'WARN_DAYS': {'REGEX_DAYS': r'^PASS_WARN_AGE[ \t]*(?P-?\d*)', 'DAYS': 'warn_days', 'CHAGE_FLAG': '-W '} } PAM_LIMITS_CONF_TEMPLATE = "/usr/share/sonic/templates/pam_limits.j2" LIMITS_CONF_TEMPLATE = "/usr/share/sonic/templates/limits.conf.j2" @@ -612,8 +621,8 @@ class AaaCfg(object): e_operations = [item for sublist in zip(e_list, operations) for item in sublist] with open(filename+'.new', 'w') as f: subprocess.call(["sed"] + e_operations + [filename], stdout=f) - subprocess.call(["mv", '-f', filename, filename+'.old']) - subprocess.call(['mv', '-f', filename+'.new', filename]) + subprocess.call(["cp", '-f', filename, filename+'.old']) + subprocess.call(['cp', '-f', filename+'.new', filename]) self.check_file_not_empty(filename) @@ -916,15 +925,11 @@ class PasswHardening(object): if passw_policies: if 'state' in passw_policies: if passw_policies['state'] == 'enabled': - if 'expiration' in passw_policies: - if int(self.passw_policies['expiration']) != 0: # value '0' meaning age policy is disabled - # the logic is to modify the expiration time according the last updated modificatiion - # - curr_expiration = int(passw_policies['expiration']) - - if 'expiration_warning' in passw_policies: - if int(self.passw_policies['expiration_warning']) != 0: # value '0' meaning age policy is disabled - curr_expiration_warning = int(passw_policies['expiration_warning']) + # Special values of expiration/expiration warning + # 0: meaning password will be expired/warning immediately. + # -1: meaning password expired/warning never. + curr_expiration = int(passw_policies.get('expiration', -1)) + curr_expiration_warning = int(passw_policies.get('expiration_warning', -1)) if self.is_passwd_aging_expire_update(curr_expiration, 'MAX_DAYS'): # Set aging policy for existing users @@ -1105,9 +1110,15 @@ class SshServer(object): syslog.syslog(syslog.LOG_ERR, "Ssh {} {} out of range".format(key, value)) elif key in SSH_CONFIG_NAMES: # search replace configuration - if not in config file - append + if key == "inactivity_timeout": + # translate min to sec. + value = int(value) * 60 kv_str = "{} {}".format(SSH_CONFIG_NAMES[key], str(value)) # name +' '+ value format modify_single_file_inplace(SSH_CONFG_TMP,['-E', "/^#?" + SSH_CONFIG_NAMES[key]+"/{h;s/.*/"+ kv_str + "/};${x;/^$/{s//" + kv_str + "/;H};x}"]) + elif key in ['max_sessions']: + # Ignore, these parameters handled in other modules + continue else: syslog.syslog(syslog.LOG_ERR, "Failed to update sshd config file - wrong key {}".format(key)) @@ -1322,16 +1333,31 @@ class PamLimitsCfg(object): self.config_db = config_db self.hwsku = "" self.type = "" + self.max_sessions = None # Load config from ConfigDb and render config file/ def update_config_file(self): device_metadata = self.config_db.get_table('DEVICE_METADATA') - if "localhost" not in device_metadata: + ssh_server_policies = {} + try: + ssh_server_policies = self.config_db.get_table('SSH_SERVER') + except KeyError: + """Dont throw except in case we don`t have SSH_SERVER config.""" + pass + + if "localhost" not in device_metadata and "POLICIES" not in ssh_server_policies: return self.read_localhost_config(device_metadata["localhost"]) + self.read_max_sessions_config(ssh_server_policies.get("POLICIES", None)) self.render_conf_file() + # Read max_sessions config + def read_max_sessions_config(self, ssh_server_policies): + if ssh_server_policies is not None: + max_sess_cfg = ssh_server_policies.get('max_sessions', 0) + self.max_sessions = max_sess_cfg if max_sess_cfg != 0 else None + # Read localhost config def read_localhost_config(self, localhost): if "hwsku" in localhost: @@ -1348,7 +1374,6 @@ class PamLimitsCfg(object): def render_conf_file(self): env = jinja2.Environment(loader=jinja2.FileSystemLoader('/'), trim_blocks=True) env.filters['sub'] = sub - try: template_file = os.path.abspath(PAM_LIMITS_CONF_TEMPLATE) template = env.get_template(template_file) @@ -1362,7 +1387,8 @@ class PamLimitsCfg(object): template = env.get_template(template_file) limits_conf = template.render( hwsku=self.hwsku, - type=self.type) + type=self.type, + max_sessions=self.max_sessions) with open(LIMITS_CONF, 'w') as f: f.write(limits_conf) except Exception as e: @@ -1694,6 +1720,133 @@ class FipsCfg(object): syslog.syslog(syslog.LOG_INFO, f'FipsCfg: update the FIPS enforce option {self.enforce}.') loader.set_fips(image, self.enforce) +print(swsscommon.__file__) + +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: + + def __init__(self): + self.cache = {} + + def load(self, cli_sessions_conf): + self.cache = cli_sessions_conf or {} + syslog.syslog(syslog.LOG_INFO, + f'SerialConsoleCfg: Initial config: {self.cache}') + + def update_serial_console_cfg(self, key, data): + ''' + Apply config flow: + inactivity_timeout | set here AND in ssh_config flow | serial-config.service restarted. + max_sessions | set by PamLimitsCfg | serial-config.service DOESNT restarted. + sysrq_capabilities | set here | serial-config.service restarted. + ''' + + if self.cache.get(key, {}) != data: + ''' Config changed, need to restart the serial-config.service ''' + syslog.syslog(syslog.LOG_INFO, f'Set serial-config parameter {key} value: {data}') + try: + run_cmd(['sudo', 'service', 'serial-config', 'restart'], + True, True) + except Exception: + syslog.syslog(syslog.LOG_ERR, f'Failed to update {key} serial-config.service config') + return + self.cache.update({key: data}) + + return + + + class HostConfigDaemon: def __init__(self): self.state_db_conn = DBConnector(STATE_DB, 0) @@ -1709,6 +1862,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() @@ -1745,6 +1900,9 @@ class HostConfigDaemon: # Initialize FipsCfg self.fipscfg = FipsCfg(self.state_db_conn) + # Initialize SerialConsoleCfg + self.serialconscfg = SerialConsoleCfg() + def load(self, init_data): aaa = init_data['AAA'] tacacs_global = init_data['TACPLUS'] @@ -1758,6 +1916,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, {}) @@ -1767,19 +1926,22 @@ class HostConfigDaemon: ntp_global = init_data.get(swsscommon.CFG_NTP_GLOBAL_TABLE_NAME) ntp_servers = init_data.get(swsscommon.CFG_NTP_SERVER_TABLE_NAME) ntp_keys = init_data.get(swsscommon.CFG_NTP_KEY_TABLE_NAME) + serial_console = init_data.get('SERIAL_CONSOLE', {}) 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) self.mgmtifacecfg.load(mgmt_ifc, mgmt_vrf) - self.rsyslogcfg.load(syslog_cfg, syslog_srv) self.dnscfg.load(dns) self.fipscfg.load(fips_cfg) self.ntpcfg.load(ntp_global, ntp_servers, ntp_keys) + self.serialconscfg.load(serial_console) + self.pamLimitsCfg.update_config_file() # Update AAA with the hostname self.aaacfg.hostname_update(self.devmetacfg.hostname) @@ -1801,6 +1963,8 @@ class HostConfigDaemon: def ssh_handler(self, key, op, data): self.sshscfg.policies_update(key, data) + self.pamLimitsCfg.update_config_file() + syslog.syslog(syslog.LOG_INFO, 'SSH Update: key: {}, op: {}, data: {}'.format(key, op, data)) def tacacs_server_handler(self, key, op, data): @@ -1897,6 +2061,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) @@ -1926,6 +2094,10 @@ class HostConfigDaemon: data = self.config_db.get_table("FIPS") self.fipscfg.fips_handler(data) + def serial_console_config_handler(self, key, op, data): + syslog.syslog(syslog.LOG_INFO, 'SERIAL_CONSOLE table handler...') + self.serialconscfg.update_serial_console_cfg(key, data) + def wait_till_system_init_done(self): # No need to print the output in the log file so using the "--quiet" # flag @@ -1955,6 +2127,8 @@ 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)) + # Handle SERIAL_CONSOLE + self.config_db.subscribe('SERIAL_CONSOLE', make_callback(self.serial_console_config_handler)) # Handle IPTables configuration self.config_db.subscribe('LOOPBACK_INTERFACE', make_callback(self.lpbk_handler)) # Handle updates to src intf changes in radius @@ -1967,6 +2141,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, @@ -2009,5 +2186,4 @@ def main(): daemon.start() if __name__ == "__main__": - main() - + main() \ No newline at end of file diff --git a/tests/hostcfgd/hostcfgd_test.py b/tests/hostcfgd/hostcfgd_test.py index 2eaef658..cb3a1450 100644 --- a/tests/hostcfgd/hostcfgd_test.py +++ b/tests/hostcfgd/hostcfgd_test.py @@ -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 @@ -121,6 +120,48 @@ def test_loopback_update(self): ]) +class TestSerialConsoleCfgd(TestCase): + """ + Test hostcfd daemon - SerialConsoleCfg + """ + def setUp(self): + MockConfigDb.CONFIG_DB['SERIAL_CONSOLE'] = { + 'POLICIES': {'inactivity-timeout': '15', 'sysrq-capabilities': 'disabled'} + } + + def tearDown(self): + MockConfigDb.CONFIG_DB = {} + + def test_serial_console_update_cfg(self): + with mock.patch('hostcfgd.subprocess') as mocked_subprocess: + popen_mock = mock.Mock() + attrs = {'communicate.return_value': ('output', 'error')} + popen_mock.configure_mock(**attrs) + mocked_subprocess.Popen.return_value = popen_mock + + serialcfg = hostcfgd.SerialConsoleCfg() + serialcfg.update_serial_console_cfg( + 'POLICIES', MockConfigDb.CONFIG_DB['SERIAL_CONSOLE']['POLICIES']) + mocked_subprocess.check_call.assert_has_calls([ + call(['sudo', 'service', 'serial-config', 'restart']), + ]) + + def test_serial_console_is_caching_config(self): + with mock.patch('hostcfgd.subprocess') as mocked_subprocess: + popen_mock = mock.Mock() + attrs = {'communicate.return_value': ('output', 'error')} + popen_mock.configure_mock(**attrs) + mocked_subprocess.Popen.return_value = popen_mock + + serialcfg = hostcfgd.SerialConsoleCfg() + serialcfg.cache['POLICIES'] = MockConfigDb.CONFIG_DB['SERIAL_CONSOLE']['POLICIES'] + + serialcfg.update_serial_console_cfg( + 'POLICIES', MockConfigDb.CONFIG_DB['SERIAL_CONSOLE']['POLICIES']) + + mocked_subprocess.check_call.assert_not_called() + + class TestHostcfgdDaemon(TestCase): def setUp(self): @@ -174,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. @@ -282,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')] @@ -311,4 +387,4 @@ def test_load(self): data = {} dns_cfg.load(data) - dns_cfg.dns_update.assert_called() + dns_cfg.dns_update.assert_called() \ No newline at end of file diff --git a/tests/hostcfgd/test_vectors.py b/tests/hostcfgd/test_vectors.py index afa50564..3862f43d 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": "true", + "retention_time": "15", + "sampling_interval": "5" + } + }, "MGMT_INTERFACE": { "eth0|1.2.3.4/24": {} }, @@ -90,4 +98,4 @@ "DNS_NAMESERVER": { "1.1.1.1": {} }, -} +} \ No newline at end of file From 42e71468bc8d69901b4907fbe70c4d375073be57 Mon Sep 17 00:00:00 2001 From: "kanza.latif" Date: Thu, 26 Sep 2024 16:02:21 +0500 Subject: [PATCH 2/2] updated the hostcfgd file --- scripts/hostcfgd | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/hostcfgd b/scripts/hostcfgd index 5b17668f..176e827f 100644 --- a/scripts/hostcfgd +++ b/scripts/hostcfgd @@ -17,8 +17,8 @@ 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 -import swsscommon -# from swsscommon import RestartWaiter +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)