From 8fe885282fa6c8a11260fe7eca5012984b7be61f Mon Sep 17 00:00:00 2001 From: Vivek Reddy Date: Tue, 27 Jun 2023 20:54:22 +0000 Subject: [PATCH 1/4] Feature related code removed from hostcfgd Signed-off-by: Vivek Reddy --- scripts/hostcfgd | 443 +----------- tests/hostcfgd/hostcfgd_test.py | 376 +--------- tests/hostcfgd/test_vectors.py | 1193 +------------------------------ 3 files changed, 4 insertions(+), 2008 deletions(-) diff --git a/scripts/hostcfgd b/scripts/hostcfgd index c99978d4..c08ac3cd 100644 --- a/scripts/hostcfgd +++ b/scripts/hostcfgd @@ -1,6 +1,5 @@ #!/usr/bin/env python3 -import ast import copy import ipaddress import os @@ -10,10 +9,9 @@ import syslog import signal import re import jinja2 -import threading from sonic_py_common import device_info from sonic_py_common.general import check_output_pipe -from swsscommon.swsscommon import ConfigDBConnector, DBConnector, Table, SonicDBConfig +from swsscommon.swsscommon import ConfigDBConnector, DBConnector, Table from swsscommon import swsscommon # FILE @@ -60,18 +58,6 @@ RADIUS_PAM_AUTH_CONF_DIR = "/etc/pam_radius_auth.d/" # MISC Constants CFG_DB = "CONFIG_DB" STATE_DB = "STATE_DB" -HOSTCFGD_MAX_PRI = 10 # Used to enforce ordering b/w daemons under Hostcfgd -DEFAULT_SELECT_TIMEOUT = 1000 -PORT_INIT_TIMEOUT_SEC = 180 - - -def safe_eval(val, default_value=False): - """ Safely evaluate the expression, without raising an exception """ - try: - ret = ast.literal_eval(val) - except ValueError: - ret = default_value - return ret def signal_handler(sig, frame): @@ -147,415 +133,6 @@ def get_pid(procname): return "" -class Feature(object): - """ Represents a feature configuration from CONFIG_DB data. """ - - def __init__(self, feature_name, feature_cfg, device_config=None): - """ Initialize Feature object based on CONFIG_DB data. - - Args: - feature_name (str): Feature name string - feature_cfg (dict): Feature CONFIG_DB configuration - deviec_config (dict): DEVICE_METADATA section of CONFIG_DB - """ - if 'has_timer' in feature_cfg: - err_str = "Invalid obsolete field 'has_timer' in FEATURE table. Please update configuration schema version" - syslog.syslog(syslog.LOG_ERR, err_str) - raise ValueError(err_str) - - self.name = feature_name - self.state = self._get_feature_table_key_render_value(feature_cfg.get('state'), device_config or {}, ['enabled', 'disabled', 'always_enabled', 'always_disabled']) - self.auto_restart = feature_cfg.get('auto_restart', 'disabled') - self.delayed = safe_eval(feature_cfg.get('delayed', 'False')) - self.has_global_scope = safe_eval(feature_cfg.get('has_global_scope', 'True')) - self.has_per_asic_scope = safe_eval(self._get_feature_table_key_render_value(feature_cfg.get('has_per_asic_scope', 'False'), device_config or {}, ['True', 'False'])) - - def _get_feature_table_key_render_value(self, configuration, device_config, expected_values): - """ Returns the target value for the feature by rendering the configuration as J2 template. - - Args: - configuration (str): Feature Table value from CONFIG_DB for given key - device_config (dict): DEVICE_METADATA section of CONFIG_DB and populated Device Running Metadata - expected_values (list): Expected set of Feature Table value for given key - Returns: - (str): Target feature table value for given key - """ - - if configuration is None: - return None - - template = jinja2.Template(configuration) - target_value = template.render(device_config) - if target_value not in expected_values: - raise ValueError('Invalid value rendered for feature {}: {}'.format(self.name, target_value)) - return target_value - - def compare_state(self, feature_name, feature_cfg): - if self.name != feature_name or not isinstance(feature_cfg, dict): - return False - - if self.state != feature_cfg.get('state', ''): - return False - return True - - -class FeatureHandler(object): - """ Handles FEATURE table updates. """ - - SYSTEMD_SYSTEM_DIR = '/etc/systemd/system/' - SYSTEMD_SERVICE_CONF_DIR = os.path.join(SYSTEMD_SYSTEM_DIR, '{}.service.d/') - - # Feature state constants - FEATURE_STATE_ENABLED = "enabled" - FEATURE_STATE_DISABLED = "disabled" - FEATURE_STATE_FAILED = "failed" - - def __init__(self, config_db, feature_state_table, device_config, is_advanced_boot): - self._config_db = config_db - self._feature_state_table = feature_state_table - self._device_config = device_config - self._cached_config = {} - self.is_multi_npu = device_info.is_multi_npu() - self.enable_delayed_service = False - self.is_advanced_boot = is_advanced_boot - self.lock = threading.Lock() - self.port_listener_thread = threading.Thread(target=self._portListener, name='port_listener_thread') - self.port_listener_thread.daemon = True - self.port_listener_thread.start() - self._device_running_config = device_info.get_device_runtime_metadata() - self.ns_cfg_db = {} - self.ns_feature_state_tbl = {} - - # Initlaize Global config that loads all database*.json - if self.is_multi_npu: - SonicDBConfig.initializeGlobalConfig() - namespaces = device_info.get_namespaces() - for ns in namespaces: - #Connect to ConfigDB in each namespace - self.ns_cfg_db[ns] = ConfigDBConnector(namespace=ns) - self.ns_cfg_db[ns].connect(wait_for_init=True, retry_on=True) - - #Connect to stateDB in each namespace - db_conn = DBConnector(STATE_DB, 0, False, ns); - self.ns_feature_state_tbl[ns] = Table(db_conn, 'FEATURE') - - def _enableDelayedServices(self): - with self.lock: - self.enable_delayed_service = True - for feature_name in self._cached_config: - if self._cached_config[feature_name].delayed: - self.update_feature_state(self._cached_config[feature_name]) - - def _portListener(self): - syslog.syslog(syslog.LOG_INFO, "Starting port listener") - appl_db_connector = DBConnector("APPL_DB", 0) - subscribe_port = swsscommon.SubscriberStateTable(appl_db_connector, "PORT_TABLE") - sel = swsscommon.Select() - sel.addSelectable(subscribe_port) - - if self.is_advanced_boot: - syslog.syslog(syslog.LOG_INFO, "Updating delayed features after warm/fast boot") - self._enableDelayedServices() - - while True: - (state, selectableObj) = sel.select(PORT_INIT_TIMEOUT_SEC*1000) - # Continue if select is timeout or selectable object is not return - if state == swsscommon.Select.ERROR: - if not self.enable_delayed_service: - syslog.syslog(syslog.LOG_ERR, "Received unexpected error in waiting for port init. Restarting services") - self._enableDelayedServices() - continue - if state != swsscommon.Select.OBJECT: - if not self.enable_delayed_service: - syslog.syslog(syslog.LOG_INFO, "Updating delayed features after timeout") - self._enableDelayedServices() - continue - - key, op, fvs = subscribe_port.pop() - if not key: - break - if op == 'SET' and key == 'PortInitDone': - syslog.syslog(syslog.LOG_INFO, "Updating delayed features after port initialization") - self._enableDelayedServices() - - def handler(self, feature_name, op, feature_cfg): - with self.lock: - if not feature_cfg: - syslog.syslog(syslog.LOG_INFO, "Deregistering feature {}".format(feature_name)) - self._cached_config.pop(feature_name, None) - self._feature_state_table._del(feature_name) - return - - device_config = {} - device_config.update(self._device_config) - device_config.update(self._device_running_config) - - feature = Feature(feature_name, feature_cfg, device_config) - self._cached_config.setdefault(feature_name, Feature(feature_name, {})) - - # Change auto-restart configuration first. - # If service reached failed state before this configuration applies (e.g. on boot) - # the next called self.update_feature_state will start it again. If it will fail - # again the auto restart will kick-in. Another order may leave it in failed state - # and not auto restart. - if self._cached_config[feature_name].auto_restart != feature.auto_restart: - syslog.syslog(syslog.LOG_INFO, "Auto-restart status of feature '{}' is changed from '{}' to '{}' ..." - .format(feature_name, self._cached_config[feature_name].auto_restart, feature.auto_restart)) - self.update_systemd_config(feature) - self._cached_config[feature_name].auto_restart = feature.auto_restart - - # Enable/disable the container service if the feature state was changed from its previous state. - if self._cached_config[feature_name].state != feature.state: - if self.update_feature_state(feature): - self.sync_feature_asic_scope(feature) - self._cached_config[feature_name] = feature - else: - self.resync_feature_state(self._cached_config[feature_name]) - - def sync_state_field(self, feature_table): - """ - Summary: - Updates the state field in the FEATURE|* tables as the state field - might have to be rendered based on DEVICE_METADATA table and generated Device Running Metadata - """ - with self.lock: - for feature_name in feature_table.keys(): - if not feature_name: - syslog.syslog(syslog.LOG_WARNING, "Feature is None") - continue - - device_config = {} - device_config.update(self._device_config) - device_config.update(self._device_running_config) - feature = Feature(feature_name, feature_table[feature_name], device_config) - - self._cached_config.setdefault(feature_name, feature) - self.update_systemd_config(feature) - self.update_feature_state(feature) - self.sync_feature_asic_scope(feature) - self.resync_feature_state(feature) - - def update_feature_state(self, feature): - cached_feature = self._cached_config[feature.name] - enable = False - disable = False - - # Allowed transitions: - # None -> always_enabled - # -> always_disabled - # -> enabled - # -> disabled - # always_enabled -> always_disabled - # enabled -> disabled - # disabled -> enabled - if cached_feature.state is None: - enable = feature.state in ("always_enabled", "enabled") - disable = feature.state in ("always_disabled", "disabled") - elif cached_feature.state in ("always_enabled", "always_disabled"): - disable = feature.state == "always_disabled" - enable = feature.state == "always_enabled" - elif cached_feature.state in ("enabled", "disabled"): - enable = feature.state == "enabled" - disable = feature.state == "disabled" - else: - syslog.syslog(syslog.LOG_INFO, "Feature {} service is {}".format(feature.name, cached_feature.state)) - return False - - if not enable and not disable: - syslog.syslog(syslog.LOG_ERR, "Unexpected state value '{}' for feature {}" - .format(feature.state, feature.name)) - return False - - if feature.delayed and not self.enable_delayed_service: - syslog.syslog(syslog.LOG_INFO, "Feature is {} delayed for port init".format(feature.name)) - return True - - if enable: - self.enable_feature(feature) - syslog.syslog(syslog.LOG_INFO, "Feature {} is enabled and started".format(feature.name)) - - if disable: - self.disable_feature(feature) - syslog.syslog(syslog.LOG_INFO, "Feature {} is stopped and disabled".format(feature.name)) - - return True - - def sync_feature_asic_scope(self, feature_config): - """Updates the has_per_asic_scope field in the FEATURE|* tables as the field - might have to be rendered based on DEVICE_METADATA table or Device Running configuration. - Disable the ASIC instance service unit file it the render value is False and update config - - Args: - feature: An object represents a feature's configuration in `FEATURE` - table of `CONFIG_DB`. - - Returns: - None. - """ - - - feature_names, feature_suffixes = self.get_multiasic_feature_instances(feature_config, True) - for feature_name in feature_names: - if '@' not in feature_name: - continue - unit_file_state = self.get_systemd_unit_state("{}.{}".format(feature_name, feature_suffixes[-1])) - if not unit_file_state: - continue - if unit_file_state != "disabled" and not feature_config.has_per_asic_scope: - cmds = [] - for suffix in reversed(feature_suffixes): - cmds.append(["sudo", "systemctl", "stop", "{}.{}".format(feature_name, suffix)]) - cmds.append(["sudo", "systemctl", "disable", "{}.{}".format(feature_name, feature_suffixes[-1])]) - cmds.append(["sudo", "systemctl", "mask", "{}.{}".format(feature_name, feature_suffixes[-1])]) - for cmd in cmds: - syslog.syslog(syslog.LOG_INFO, "Running cmd: '{}'".format(cmd)) - try: - run_cmd(cmd, raise_exception=True) - except Exception as err: - syslog.syslog(syslog.LOG_ERR, "Feature '{}.{}' failed to be stopped and disabled" - .format(feature.name, feature_suffixes[-1])) - self.set_feature_state(feature, self.FEATURE_STATE_FAILED) - return - self._config_db.mod_entry('FEATURE', feature_config.name, {'has_per_asic_scope': str(feature_config.has_per_asic_scope)}) - - # sync has_per_asic_scope to CONFIG_DB in namespaces in multi-asic platform - for ns, db in self.ns_cfg_db.items(): - db.mod_entry('FEATURE', feature_config.name, {'has_per_asic_scope': str(feature_config.has_per_asic_scope)}) - - def update_systemd_config(self, feature_config): - """Updates `Restart=` field in feature's systemd configuration file - according to the value of `auto_restart` field in `FEATURE` table of `CONFIG_DB`. - - Args: - feature: An object represents a feature's configuration in `FEATURE` - table of `CONFIG_DB`. - - Returns: - None. - """ - restart_field_str = "always" if "enabled" in feature_config.auto_restart else "no" - feature_systemd_config = "[Service]\nRestart={}\n".format(restart_field_str) - feature_names, feature_suffixes = self.get_multiasic_feature_instances(feature_config) - - # On multi-ASIC device, creates systemd configuration file for each feature instance - # residing in difference namespace. - for feature_name in feature_names: - syslog.syslog(syslog.LOG_INFO, "Updating feature '{}' systemd config file related to auto-restart ..." - .format(feature_name)) - feature_systemd_config_dir_path = self.SYSTEMD_SERVICE_CONF_DIR.format(feature_name) - feature_systemd_config_file_path = os.path.join(feature_systemd_config_dir_path, 'auto_restart.conf') - - if not os.path.exists(feature_systemd_config_dir_path): - os.mkdir(feature_systemd_config_dir_path) - with open(feature_systemd_config_file_path, 'w') as feature_systemd_config_file_handler: - feature_systemd_config_file_handler.write(feature_systemd_config) - - syslog.syslog(syslog.LOG_INFO, "Feature '{}' systemd config file related to auto-restart is updated!" - .format(feature_name)) - - try: - syslog.syslog(syslog.LOG_INFO, "Reloading systemd configuration files ...") - run_cmd(["sudo", "systemctl", "daemon-reload"], raise_exception=True) - syslog.syslog(syslog.LOG_INFO, "Systemd configuration files are reloaded!") - except Exception as err: - syslog.syslog(syslog.LOG_ERR, "Failed to reload systemd configuration files!") - - def get_multiasic_feature_instances(self, feature, all_instance=False): - # Create feature name suffix depending feature is running in host or namespace or in both - feature_names = ( - ([feature.name] if feature.has_global_scope or not self.is_multi_npu else []) + - ([(feature.name + '@' + str(asic_inst)) for asic_inst in range(device_info.get_num_npus()) - if self.is_multi_npu and (all_instance or feature.has_per_asic_scope)]) - ) - - if not feature_names: - syslog.syslog(syslog.LOG_ERR, "Feature '{}' service not available" - .format(feature.name)) - - feature_suffixes = ["service"] - - return feature_names, feature_suffixes - - def get_systemd_unit_state(self, unit): - """ Returns service configuration """ - - cmd = ["sudo", "systemctl", "show", unit, "--property", "UnitFileState"] - proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - stdout, stderr = proc.communicate() - if proc.returncode != 0: - syslog.syslog(syslog.LOG_ERR, "Failed to get status of {}: rc={} stderr={}".format(unit, proc.returncode, stderr)) - return 'invalid' # same as systemd's "invalid indicates that it could not be determined whether the unit file is enabled". - - props = dict([line.split("=") for line in stdout.decode().strip().splitlines()]) - return props["UnitFileState"] - - def enable_feature(self, feature): - feature_names, feature_suffixes = self.get_multiasic_feature_instances(feature) - for feature_name in feature_names: - # Check if it is already enabled, if yes skip the system call - unit_file_state = self.get_systemd_unit_state("{}.{}".format(feature_name, feature_suffixes[-1])) - if unit_file_state == "enabled" or not unit_file_state: - continue - cmds = [] - for suffix in feature_suffixes: - cmds.append(["sudo", "systemctl", "unmask", "{}.{}".format(feature_name, suffix)]) - - # If feature has timer associated with it, start/enable corresponding systemd .timer unit - # otherwise, start/enable corresponding systemd .service unit - - cmds.append(["sudo", "systemctl", "enable", "{}.{}".format(feature_name, feature_suffixes[-1])]) - cmds.append(["sudo", "systemctl", "start", "{}.{}".format(feature_name, feature_suffixes[-1])]) - - for cmd in cmds: - syslog.syslog(syslog.LOG_INFO, "Running cmd: '{}'".format(cmd)) - try: - run_cmd(cmd, raise_exception=True) - except Exception as err: - syslog.syslog(syslog.LOG_ERR, "Feature '{}.{}' failed to be enabled and started" - .format(feature.name, feature_suffixes[-1])) - self.set_feature_state(feature, self.FEATURE_STATE_FAILED) - return - - self.set_feature_state(feature, self.FEATURE_STATE_ENABLED) - - def disable_feature(self, feature): - feature_names, feature_suffixes = self.get_multiasic_feature_instances(feature) - for feature_name in feature_names: - # Check if it is already disabled, if yes skip the system call - unit_file_state = self.get_systemd_unit_state("{}.{}".format(feature_name, feature_suffixes[-1])) - if unit_file_state in ("disabled", "masked") or not unit_file_state: - continue - cmds = [] - for suffix in reversed(feature_suffixes): - cmds.append(["sudo", "systemctl", "stop", "{}.{}".format(feature_name, suffix)]) - cmds.append(["sudo", "systemctl", "disable", "{}.{}".format(feature_name, feature_suffixes[-1])]) - cmds.append(["sudo", "systemctl", "mask", "{}.{}".format(feature_name, feature_suffixes[-1])]) - for cmd in cmds: - syslog.syslog(syslog.LOG_INFO, "Running cmd: '{}'".format(cmd)) - try: - run_cmd(cmd, raise_exception=True) - except Exception as err: - syslog.syslog(syslog.LOG_ERR, "Feature '{}.{}' failed to be stopped and disabled" - .format(feature.name, feature_suffixes[-1])) - self.set_feature_state(feature, self.FEATURE_STATE_FAILED) - return - - self.set_feature_state(feature, self.FEATURE_STATE_DISABLED) - - def resync_feature_state(self, feature): - self._config_db.mod_entry('FEATURE', feature.name, {'state': feature.state}) - - # resync the feature state to CONFIG_DB in namespaces in multi-asic platform - for ns, db in self.ns_cfg_db.items(): - db.mod_entry('FEATURE', feature.name, {'state': feature.state}) - - def set_feature_state(self, feature, state): - self._feature_state_table.set(feature.name, [('state', state)]) - - # Update the feature state to STATE_DB in namespaces in multi-asic platform - for ns, tbl in self.ns_feature_state_tbl.items(): - tbl.set(feature.name, [('state', state)]) - class Iptables(object): def __init__(self): ''' @@ -1627,32 +1204,18 @@ class SyslogCfg: class HostConfigDaemon: def __init__(self): self.state_db_conn = DBConnector(STATE_DB, 0) - self.advanced_boot = False - if swsscommon.RestartWaiter.isAdvancedBootInProgress(self.state_db_conn): - self.advanced_boot = True - swsscommon.RestartWaiter.waitAdvancedBootDone() # Just a sanity check to verify if the CONFIG_DB has been initialized # before moving forward self.config_db = ConfigDBConnector() self.config_db.connect(wait_for_init=True, retry_on=True) syslog.syslog(syslog.LOG_INFO, 'ConfigDB connect success') - # Load DEVICE metadata configurations - self.device_config = {} - self.device_config['DEVICE_METADATA'] = self.config_db.get_table('DEVICE_METADATA') - - # Load feature state table - feature_state_table = Table(self.state_db_conn, 'FEATURE') - # Initialize KDump Config and set the config to default if nothing is provided self.kdumpCfg = KdumpCfg(self.config_db) # Initialize IpTables self.iptables = Iptables() - # Intialize Feature Handler - self.feature_handler = FeatureHandler(self.config_db, feature_state_table, self.device_config, self.advanced_boot) - # Initialize Ntp Config Handler self.ntpcfg = NtpCfg() @@ -1678,7 +1241,6 @@ class HostConfigDaemon: self.syslogcfg = SyslogCfg() def load(self, init_data): - features = init_data['FEATURE'] aaa = init_data['AAA'] tacacs_global = init_data['TACPLUS'] tacacs_server = init_data['TACPLUS_SERVER'] @@ -1694,7 +1256,6 @@ class HostConfigDaemon: mgmt_vrf = init_data.get(swsscommon.CFG_MGMT_VRF_CONFIG_TABLE_NAME, {}) syslog = init_data.get('SYSLOG_CONFIG', {}) - self.feature_handler.sync_state_field(features) self.aaacfg.load(aaa, tacacs_global, tacacs_server, radius_global, radius_server) self.iptables.load(lpbk_table) self.ntpcfg.load(ntp_global, ntp_server) @@ -1823,8 +1384,6 @@ class HostConfigDaemon: return callback self.config_db.subscribe('KDUMP', make_callback(self.kdump_handler)) - # Handle FEATURE updates before other tables - self.config_db.subscribe('FEATURE', make_callback(self.feature_handler.handler)) # Handle AAA, TACACS and RADIUS related tables self.config_db.subscribe('AAA', make_callback(self.aaa_handler)) self.config_db.subscribe('TACPLUS', make_callback(self.tacacs_global_handler)) diff --git a/tests/hostcfgd/hostcfgd_test.py b/tests/hostcfgd/hostcfgd_test.py index fbeecb1c..f997b84b 100644 --- a/tests/hostcfgd/hostcfgd_test.py +++ b/tests/hostcfgd/hostcfgd_test.py @@ -9,10 +9,8 @@ from sonic_py_common.general import load_module_from_source from unittest import TestCase, mock -from .test_vectors import HOSTCFG_DAEMON_INIT_CFG_DB -from .test_vectors import HOSTCFGD_TEST_VECTOR, HOSTCFG_DAEMON_CFG_DB -from tests.common.mock_configdb import MockConfigDb, MockDBConnector, MockSubscriberStateTable, MockSelect -from tests.common.mock_restart_waiter import MockRestartWaiter +from .test_vectors import HOSTCFG_DAEMON_INIT_CFG_DB, HOSTCFG_DAEMON_CFG_DB +from tests.common.mock_configdb import MockConfigDb, MockDBConnector from pyfakefs.fake_filesystem_unittest import patchfs from deepdiff import DeepDiff @@ -29,227 +27,6 @@ hostcfgd.ConfigDBConnector = MockConfigDb hostcfgd.DBConnector = MockDBConnector hostcfgd.Table = mock.Mock() -swsscommon.Select = MockSelect -swsscommon.SubscriberStateTable = MockSubscriberStateTable -swsscommon.RestartWaiter = MockRestartWaiter - -class TestFeatureHandler(TestCase): - """Test methods of `FeatureHandler` class. - """ - def checks_config_table(self, feature_table, expected_table): - """Compares `FEATURE` table in `CONFIG_DB` with expected output table. - - Args: - feature_table: A dictionary indicates current `FEATURE` table in `CONFIG_DB`. - expected_table A dictionary indicates the expected `FEATURE` table in `CONFIG_DB`. - - Returns: - Returns True if `FEATURE` table in `CONFIG_DB` was not modified unexpectedly; - otherwise, returns False. - """ - ddiff = DeepDiff(feature_table, expected_table, ignore_order=True) - - return True if not ddiff else False - - def checks_systemd_config_file(self, feature_table, feature_systemd_name_map=None): - """Checks whether the systemd configuration file of each feature was created or not - and whether the `Restart=` field in the file is set correctly or not. - - Args: - feature_table: A dictionary indicates `Feature` table in `CONFIG_DB`. - - Returns: Boolean value indicates whether test passed or not. - """ - - truth_table = {'enabled': 'always', - 'disabled': 'no'} - - systemd_config_file_path = os.path.join(hostcfgd.FeatureHandler.SYSTEMD_SERVICE_CONF_DIR, - 'auto_restart.conf') - - for feature_name in feature_table: - auto_restart_status = feature_table[feature_name].get('auto_restart', 'disabled') - if "enabled" in auto_restart_status: - auto_restart_status = "enabled" - elif "disabled" in auto_restart_status: - auto_restart_status = "disabled" - - feature_systemd_list = feature_systemd_name_map[feature_name] if feature_systemd_name_map else [feature_name] - - for feature_systemd in feature_systemd_list: - feature_systemd_config_file_path = systemd_config_file_path.format(feature_systemd) - is_config_file_existing = os.path.exists(feature_systemd_config_file_path) - assert is_config_file_existing, "Systemd configuration file of feature '{}' does not exist!".format(feature_systemd) - - with open(feature_systemd_config_file_path) as systemd_config_file: - status = systemd_config_file.read().strip() - assert status == '[Service]\nRestart={}'.format(truth_table[auto_restart_status]) - - def get_state_db_set_calls(self, feature_table): - """Returns a Mock call objects which recorded the `set` calls to `FEATURE` table in `STATE_DB`. - - Args: - feature_table: A dictionary indicates `FEATURE` table in `CONFIG_DB`. - - Returns: - set_call_list: A list indicates Mock call objects. - """ - set_call_list = [] - - for feature_name in feature_table.keys(): - feature_state = "" - if "enabled" in feature_table[feature_name]["state"]: - feature_state = "enabled" - elif "disabled" in feature_table[feature_name]["state"]: - feature_state = "disabled" - else: - feature_state = feature_table[feature_name]["state"] - - set_call_list.append(mock.call(feature_name, [("state", feature_state)])) - - return set_call_list - - @parameterized.expand(HOSTCFGD_TEST_VECTOR) - @patchfs - def test_sync_state_field(self, test_scenario_name, config_data, fs): - """Tests the method `sync_state_field(...)` of `FeatureHandler` class. - - Args: - test_secnario_name: A string indicates different testing scenario. - config_data: A dictionary contains initial `CONFIG_DB` tables and expected results. - - Returns: - Boolean value indicates whether test will pass or not. - """ - # add real path of sesscommon for database_config.json - fs.add_real_paths(swsscommon_package.__path__) - fs.create_dir(hostcfgd.FeatureHandler.SYSTEMD_SYSTEM_DIR) - - MockConfigDb.set_config_db(config_data['config_db']) - feature_state_table_mock = mock.Mock() - with mock.patch('hostcfgd.subprocess') as mocked_subprocess: - with mock.patch("sonic_py_common.device_info.get_device_runtime_metadata", return_value=config_data['device_runtime_metadata']): - with mock.patch("sonic_py_common.device_info.is_multi_npu", return_value=True if 'num_npu' in config_data else False): - with mock.patch("sonic_py_common.device_info.get_num_npus", return_value=config_data['num_npu'] if 'num_npu' in config_data else 1): - with mock.patch("sonic_py_common.device_info.get_namespaces", return_value=["asic{}".format(a) for a in range(config_data['num_npu'])] if 'num_npu' in config_data else []): - popen_mock = mock.Mock() - attrs = config_data['popen_attributes'] - popen_mock.configure_mock(**attrs) - mocked_subprocess.Popen.return_value = popen_mock - - device_config = {} - device_config['DEVICE_METADATA'] = MockConfigDb.CONFIG_DB['DEVICE_METADATA'] - device_config.update(config_data['device_runtime_metadata']) - - feature_handler = hostcfgd.FeatureHandler(MockConfigDb(), feature_state_table_mock, - device_config, False) - feature_handler.enable_delayed_service = True - feature_table = MockConfigDb.CONFIG_DB['FEATURE'] - feature_handler.sync_state_field(feature_table) - - feature_systemd_name_map = {} - for feature_name in feature_table.keys(): - feature = hostcfgd.Feature(feature_name, feature_table[feature_name], device_config) - feature_names, _ = feature_handler.get_multiasic_feature_instances(feature) - feature_systemd_name_map[feature_name] = feature_names - - is_any_difference = self.checks_config_table(MockConfigDb.get_config_db()['FEATURE'], - config_data['expected_config_db']['FEATURE']) - assert is_any_difference, "'FEATURE' table in 'CONFIG_DB' is modified unexpectedly!" - - if 'num_npu' in config_data: - for ns in range(config_data['num_npu']): - namespace = "asic{}".format(ns) - is_any_difference = self.checks_config_table(feature_handler.ns_cfg_db[namespace].get_config_db()['FEATURE'], - config_data['expected_config_db']['FEATURE']) - assert is_any_difference, "'FEATURE' table in 'CONFIG_DB' in namespace {} is modified unexpectedly!".format(namespace) - - feature_table_state_db_calls = self.get_state_db_set_calls(feature_table) - - self.checks_systemd_config_file(config_data['config_db']['FEATURE'], feature_systemd_name_map) - mocked_subprocess.check_call.assert_has_calls(config_data['enable_feature_subprocess_calls'], - any_order=True) - mocked_subprocess.check_call.assert_has_calls(config_data['daemon_reload_subprocess_call'], - any_order=True) - feature_state_table_mock.set.assert_has_calls(feature_table_state_db_calls) - self.checks_systemd_config_file(config_data['config_db']['FEATURE'], feature_systemd_name_map) - - @parameterized.expand(HOSTCFGD_TEST_VECTOR) - @patchfs - def test_handler(self, test_scenario_name, config_data, fs): - """Tests the method `handle(...)` of `FeatureHandler` class. - - Args: - test_secnario_name: A string indicates different testing scenario. - config_data: A dictionary contains initial `CONFIG_DB` tables and expected results. - - Returns: - Boolean value indicates whether test will pass or not. - """ - # add real path of sesscommon for database_config.json - fs.add_real_paths(swsscommon_package.__path__) - fs.create_dir(hostcfgd.FeatureHandler.SYSTEMD_SYSTEM_DIR) - - MockConfigDb.set_config_db(config_data['config_db']) - feature_state_table_mock = mock.Mock() - with mock.patch('hostcfgd.subprocess') as mocked_subprocess: - with mock.patch("sonic_py_common.device_info.get_device_runtime_metadata", return_value=config_data['device_runtime_metadata']): - with mock.patch("sonic_py_common.device_info.is_multi_npu", return_value=True if 'num_npu' in config_data else False): - with mock.patch("sonic_py_common.device_info.get_num_npus", return_value=config_data['num_npu'] if 'num_npu' in config_data else 1): - popen_mock = mock.Mock() - attrs = config_data['popen_attributes'] - popen_mock.configure_mock(**attrs) - mocked_subprocess.Popen.return_value = popen_mock - - device_config = {} - device_config['DEVICE_METADATA'] = MockConfigDb.CONFIG_DB['DEVICE_METADATA'] - device_config.update(config_data['device_runtime_metadata']) - feature_handler = hostcfgd.FeatureHandler(MockConfigDb(), feature_state_table_mock, - device_config, False) - feature_handler.enable_delayed_service = True - - feature_table = MockConfigDb.CONFIG_DB['FEATURE'] - - feature_systemd_name_map = {} - for feature_name, feature_config in feature_table.items(): - feature_handler.handler(feature_name, 'SET', feature_config) - feature = hostcfgd.Feature(feature_name, feature_table[feature_name], device_config) - feature_names, _ = feature_handler.get_multiasic_feature_instances(feature) - feature_systemd_name_map[feature_name] = feature_names - - self.checks_systemd_config_file(config_data['config_db']['FEATURE'], feature_systemd_name_map) - mocked_subprocess.check_call.assert_has_calls(config_data['enable_feature_subprocess_calls'], - any_order=True) - mocked_subprocess.check_call.assert_has_calls(config_data['daemon_reload_subprocess_call'], - any_order=True) - - def test_feature_config_parsing(self): - swss_feature = hostcfgd.Feature('swss', { - 'state': 'enabled', - 'auto_restart': 'enabled', - 'delayed': 'True', - 'has_global_scope': 'False', - 'has_per_asic_scope': 'True', - }) - - assert swss_feature.name == 'swss' - assert swss_feature.state == 'enabled' - assert swss_feature.auto_restart == 'enabled' - assert swss_feature.delayed - assert not swss_feature.has_global_scope - assert swss_feature.has_per_asic_scope - - def test_feature_config_parsing_defaults(self): - swss_feature = hostcfgd.Feature('swss', { - 'state': 'enabled', - }) - - assert swss_feature.name == 'swss' - assert swss_feature.state == 'enabled' - assert swss_feature.auto_restart == 'disabled' - assert not swss_feature.delayed - assert swss_feature.has_global_scope - assert not swss_feature.has_per_asic_scope class TesNtpCfgd(TestCase): @@ -310,155 +87,6 @@ def setUp(self): def tearDown(self): MockConfigDb.CONFIG_DB = {} - @patchfs - def test_feature_events(self, fs): - fs.create_dir(hostcfgd.FeatureHandler.SYSTEMD_SYSTEM_DIR) - MockConfigDb.event_queue = [('FEATURE', 'dhcp_relay'), - ('FEATURE', 'mux'), - ('FEATURE', 'telemetry')] - daemon = hostcfgd.HostConfigDaemon() - daemon.feature_handler.enable_delayed_service = True - daemon.register_callbacks() - 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 - try: - daemon.start() - except TimeoutError: - pass - expected = [call(['sudo', 'systemctl', 'daemon-reload']), - call(['sudo', 'systemctl', 'unmask', 'dhcp_relay.service']), - call(['sudo', 'systemctl', 'enable', 'dhcp_relay.service']), - call(['sudo', 'systemctl', 'start', 'dhcp_relay.service']), - call(['sudo', 'systemctl', 'daemon-reload']), - call(['sudo', 'systemctl', 'unmask', 'mux.service']), - call(['sudo', 'systemctl', 'enable', 'mux.service']), - call(['sudo', 'systemctl', 'start', 'mux.service']), - call(['sudo', 'systemctl', 'daemon-reload']), - call(['sudo', 'systemctl', 'unmask', 'telemetry.service']), - call(['sudo', 'systemctl', 'enable', 'telemetry.service']), - call(['sudo', 'systemctl', 'start', 'telemetry.service'])] - mocked_subprocess.check_call.assert_has_calls(expected) - - # Change the state to disabled - MockConfigDb.CONFIG_DB['FEATURE']['telemetry']['state'] = 'disabled' - MockConfigDb.event_queue = [('FEATURE', 'telemetry')] - try: - daemon.start() - except TimeoutError: - pass - expected = [call(['sudo', 'systemctl', 'stop', 'telemetry.service']), - call(['sudo', 'systemctl', 'disable', 'telemetry.service']), - call(['sudo', 'systemctl', 'mask', 'telemetry.service'])] - mocked_subprocess.check_call.assert_has_calls(expected) - MockConfigDb.CONFIG_DB['FEATURE']['telemetry']['state'] = 'enabled' - - @patchfs - def test_delayed_service(self, fs): - fs.create_dir(hostcfgd.FeatureHandler.SYSTEMD_SYSTEM_DIR) - MockConfigDb.event_queue = [('FEATURE', 'dhcp_relay'), - ('FEATURE', 'mux'), - ('FEATURE', 'telemetry')] - MockConfigDb.CONFIG_DB['PORT_TABLE'] = {'PortInitDone': {'lanes': '0'}, 'PortConfigDone': {'val': 'true'}} - MockSelect.event_queue = [('PORT_TABLE', 'PortInitDone')] - daemon = hostcfgd.HostConfigDaemon() - daemon.register_callbacks() - 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 - try: - daemon.start() - except TimeoutError: - pass - expected = [call(['sudo', 'systemctl', 'daemon-reload']), - call(['sudo', 'systemctl', 'unmask', 'dhcp_relay.service']), - call(['sudo', 'systemctl', 'enable', 'dhcp_relay.service']), - call(['sudo', 'systemctl', 'start', 'dhcp_relay.service']), - call(['sudo', 'systemctl', 'daemon-reload']), - call(['sudo', 'systemctl', 'unmask', 'mux.service']), - call(['sudo', 'systemctl', 'enable', 'mux.service']), - call(['sudo', 'systemctl', 'start', 'mux.service']), - call(['sudo', 'systemctl', 'daemon-reload']), - call(['sudo', 'systemctl', 'unmask', 'telemetry.service']), - call(['sudo', 'systemctl', 'enable', 'telemetry.service']), - call(['sudo', 'systemctl', 'start', 'telemetry.service'])] - - mocked_subprocess.check_call.assert_has_calls(expected) - - @patchfs - def test_advanced_reboot(self, fs): - fs.create_dir(hostcfgd.FeatureHandler.SYSTEMD_SYSTEM_DIR) - MockConfigDb.event_queue = [('FEATURE', 'dhcp_relay'), - ('FEATURE', 'mux'), - ('FEATURE', 'telemetry')] - MockRestartWaiter.advancedReboot = True - daemon = hostcfgd.HostConfigDaemon() - daemon.register_callbacks() - 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 - try: - daemon.start() - except TimeoutError: - pass - expected = [call(['sudo', 'systemctl', 'daemon-reload']), - call(['sudo', 'systemctl', 'unmask', 'dhcp_relay.service']), - call(['sudo', 'systemctl', 'enable', 'dhcp_relay.service']), - call(['sudo', 'systemctl', 'start', 'dhcp_relay.service']), - call(['sudo', 'systemctl', 'daemon-reload']), - call(['sudo', 'systemctl', 'unmask', 'mux.service']), - call(['sudo', 'systemctl', 'enable', 'mux.service']), - call(['sudo', 'systemctl', 'start', 'mux.service']), - call(['sudo', 'systemctl', 'daemon-reload']), - call(['sudo', 'systemctl', 'unmask', 'telemetry.service']), - call(['sudo', 'systemctl', 'enable', 'telemetry.service']), - call(['sudo', 'systemctl', 'start', 'telemetry.service'])] - - mocked_subprocess.check_call.assert_has_calls(expected) - MockRestartWaiter.advancedReboot = False - - @patchfs - def test_portinit_timeout(self, fs): - fs.create_dir(hostcfgd.FeatureHandler.SYSTEMD_SYSTEM_DIR) - old_timeout = hostcfgd.PORT_INIT_TIMEOUT_SEC - MockConfigDb.event_queue = [('FEATURE', 'dhcp_relay'), - ('FEATURE', 'mux'), - ('FEATURE', 'telemetry')] - hostcfgd.PORT_INIT_TIMEOUT_SEC = 0.1 - daemon = hostcfgd.HostConfigDaemon() - daemon.register_callbacks() - 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 - try: - daemon.start() - except TimeoutError: - pass - time.sleep(hostcfgd.PORT_INIT_TIMEOUT_SEC * 2) - expected = [call(['sudo', 'systemctl', 'daemon-reload']), - call(['sudo', 'systemctl', 'unmask', 'dhcp_relay.service']), - call(['sudo', 'systemctl', 'enable', 'dhcp_relay.service']), - call(['sudo', 'systemctl', 'start', 'dhcp_relay.service']), - call(['sudo', 'systemctl', 'daemon-reload']), - call(['sudo', 'systemctl', 'unmask', 'mux.service']), - call(['sudo', 'systemctl', 'enable', 'mux.service']), - call(['sudo', 'systemctl', 'start', 'mux.service']), - call(['sudo', 'systemctl', 'daemon-reload']), - call(['sudo', 'systemctl', 'unmask', 'telemetry.service']), - call(['sudo', 'systemctl', 'enable', 'telemetry.service']), - call(['sudo', 'systemctl', 'start', 'telemetry.service'])] - - mocked_subprocess.check_call.assert_has_calls(expected) - hostcfgd.PORT_INIT_TIMEOUT_SEC = old_timeout - def test_loopback_events(self): MockConfigDb.set_config_db(HOSTCFG_DAEMON_CFG_DB) MockConfigDb.event_queue = [('NTP', 'global'), diff --git a/tests/hostcfgd/test_vectors.py b/tests/hostcfgd/test_vectors.py index 72ab8ca6..1ba2698a 100644 --- a/tests/hostcfgd/test_vectors.py +++ b/tests/hostcfgd/test_vectors.py @@ -1,1169 +1,8 @@ from unittest.mock import call """ - hostcfgd test vector + hostcfgd test vectors """ -HOSTCFGD_TEST_VECTOR = [ - [ - "DualTorCase", - { - "device_runtime_metadata": { - "DEVICE_RUNTIME_METADATA": { - "ETHERNET_PORTS_PRESENT":True - } - }, - "config_db": { - "DEVICE_METADATA": { - "localhost": { - "subtype": "DualToR", - "type": "ToRRouter", - } - }, - "KDUMP": { - "config": { - "enabled": "false", - "num_dumps": "3", - "memory": "0M-2G:256M,2G-4G:320M,4G-8G:384M,8G-:448M" - } - }, - "FEATURE": { - "dhcp_relay": { - "auto_restart": "enabled", - "has_global_scope": "True", - "has_per_asic_scope": "False", - "delayed": "False", - "high_mem_alert": "disabled", - "set_owner": "kube", - "state": "{% if not (DEVICE_METADATA is defined and DEVICE_METADATA['localhost'] is defined and DEVICE_METADATA['localhost']['type'] is defined and DEVICE_METADATA['localhost']['type'] != 'ToRRouter') %}enabled{% else %}disabled{% endif %}" - }, - "mux": { - "auto_restart": "enabled", - "has_global_scope": "True", - "has_per_asic_scope": "False", - "delayed": "False", - "high_mem_alert": "disabled", - "set_owner": "local", - "state": "{% if 'subtype' in DEVICE_METADATA['localhost'] and DEVICE_METADATA['localhost']['subtype'] == 'DualToR' %}enabled{% else %}always_disabled{% endif %}" - }, - "telemetry": { - "auto_restart": "enabled", - "has_global_scope": "True", - "has_per_asic_scope": "False", - "delayed": "True", - "high_mem_alert": "disabled", - "set_owner": "kube", - "state": "enabled", - "status": "enabled" - }, - }, - }, - "expected_config_db": { - "FEATURE": { - "dhcp_relay": { - "auto_restart": "enabled", - "has_global_scope": "True", - "has_per_asic_scope": "False", - "delayed": "False", - "high_mem_alert": "disabled", - "set_owner": "kube", - "state": "enabled" - }, - "mux": { - "auto_restart": "enabled", - "has_global_scope": "True", - "has_per_asic_scope": "False", - "delayed": "False", - "high_mem_alert": "disabled", - "set_owner": "local", - "state": "enabled" - }, - "telemetry": { - "auto_restart": "enabled", - "has_global_scope": "True", - "has_per_asic_scope": "False", - "delayed": "True", - "high_mem_alert": "disabled", - "set_owner": "kube", - "state": "enabled", - "status": "enabled" - }, - }, - }, - "enable_feature_subprocess_calls": [ - call(["sudo", "systemctl", "unmask", "dhcp_relay.service"]), - call(["sudo", "systemctl", "enable", "dhcp_relay.service"]), - call(["sudo", "systemctl", "start", "dhcp_relay.service"]), - call(["sudo", "systemctl", "unmask", "mux.service"]), - call(["sudo", "systemctl", "enable", "mux.service"]), - call(["sudo", "systemctl", "start", "mux.service"]), - call(["sudo", "systemctl", "unmask", "telemetry.service"]), - call(["sudo", "systemctl", "enable", "telemetry.service"]), - call(["sudo", "systemctl", "start", "telemetry.service"]), - ], - "daemon_reload_subprocess_call": [ - call(["sudo", "systemctl", "daemon-reload"]), - ], - "popen_attributes": { - 'communicate.return_value': ('output', 'error') - }, - }, - ], - [ - "SingleToRCase", - { - "device_runtime_metadata": { - "DEVICE_RUNTIME_METADATA": { - "ETHERNET_PORTS_PRESENT":True - } - }, - "config_db": { - "DEVICE_METADATA": { - "localhost": { - "type": "ToR", - } - }, - "KDUMP": { - "config": { - "enabled": "false", - "num_dumps": "3", - "memory": "0M-2G:256M,2G-4G:320M,4G-8G:384M,8G-:448M" - } - }, - "FEATURE": { - "dhcp_relay": { - "auto_restart": "enabled", - "has_global_scope": "True", - "has_per_asic_scope": "False", - "delayed": "False", - "high_mem_alert": "disabled", - "set_owner": "kube", - "state": "{% if not (DEVICE_METADATA is defined and DEVICE_METADATA['localhost'] is defined and DEVICE_METADATA['localhost']['type'] is defined and DEVICE_METADATA['localhost']['type'] != 'ToRRouter') %}enabled{% else %}disabled{% endif %}" - }, - "mux": { - "auto_restart": "enabled", - "has_global_scope": "True", - "has_per_asic_scope": "False", - "delayed": "False", - "high_mem_alert": "disabled", - "set_owner": "local", - "state": "{% if 'subtype' in DEVICE_METADATA['localhost'] and DEVICE_METADATA['localhost']['subtype'] == 'DualToR' %}enabled{% else %}always_disabled{% endif %}" - }, - "telemetry": { - "auto_restart": "enabled", - "has_global_scope": "True", - "has_per_asic_scope": "False", - "delayed": "True", - "high_mem_alert": "disabled", - "set_owner": "kube", - "state": "enabled", - "status": "enabled" - }, - "sflow": { - "auto_restart": "enabled", - "has_global_scope": "True", - "has_per_asic_scope": "False", - "delayed": "False", - "high_mem_alert": "disabled", - "set_owner": "local", - "state": "always_enabled" - }, - }, - }, - "expected_config_db": { - "FEATURE": { - "dhcp_relay": { - "auto_restart": "enabled", - "has_global_scope": "True", - "has_per_asic_scope": "False", - "delayed": "False", - "high_mem_alert": "disabled", - "set_owner": "kube", - "state": "disabled" - }, - "mux": { - "auto_restart": "enabled", - "has_global_scope": "True", - "has_per_asic_scope": "False", - "delayed": "False", - "high_mem_alert": "disabled", - "set_owner": "local", - "state": "always_disabled" - }, - "telemetry": { - "auto_restart": "enabled", - "has_global_scope": "True", - "has_per_asic_scope": "False", - "delayed": "True", - "high_mem_alert": "disabled", - "set_owner": "kube", - "state": "enabled", - "status": "enabled" - }, - "sflow": { - "auto_restart": "enabled", - "has_global_scope": "True", - "has_per_asic_scope": "False", - "delayed": "False", - "high_mem_alert": "disabled", - "set_owner": "local", - "state": "always_enabled" - }, - }, - }, - "enable_feature_subprocess_calls": [ - call(["sudo", "systemctl", "stop", "mux.service"]), - call(["sudo", "systemctl", "disable", "mux.service"]), - call(["sudo", "systemctl", "mask", "mux.service"]), - call(["sudo", "systemctl", "unmask", "telemetry.service"]), - call(["sudo", "systemctl", "enable", "telemetry.service"]), - call(["sudo", "systemctl", "start", "telemetry.service"]), - call(["sudo", "systemctl", "unmask", "sflow.service"]), - call(["sudo", "systemctl", "enable", "sflow.service"]), - call(["sudo", "systemctl", "start", "sflow.service"]), - ], - "daemon_reload_subprocess_call": [ - call(["sudo", "systemctl", "daemon-reload"]), - ], - "popen_attributes": { - 'communicate.return_value': ('output', 'error') - }, - }, - ], - [ - "T1Case", - { - "device_runtime_metadata": { - "DEVICE_RUNTIME_METADATA": { - "ETHERNET_PORTS_PRESENT":True - } - }, - "config_db": { - "DEVICE_METADATA": { - "localhost": { - "type": "T1", - } - }, - "KDUMP": { - "config": { - "enabled": "false", - "num_dumps": "3", - "memory": "0M-2G:256M,2G-4G:320M,4G-8G:384M,8G-:448M" - } - }, - "FEATURE": { - "dhcp_relay": { - "auto_restart": "enabled", - "has_global_scope": "True", - "has_per_asic_scope": "False", - "delayed": "False", - "high_mem_alert": "disabled", - "set_owner": "kube", - "state": "{% if not (DEVICE_METADATA is defined and DEVICE_METADATA['localhost'] is defined and DEVICE_METADATA['localhost']['type'] is defined and DEVICE_METADATA['localhost']['type'] != 'ToRRouter') %}enabled{% else %}disabled{% endif %}" - }, - "mux": { - "auto_restart": "enabled", - "has_global_scope": "True", - "has_per_asic_scope": "False", - "delayed": "False", - "high_mem_alert": "disabled", - "set_owner": "local", - "state": "{% if 'subtype' in DEVICE_METADATA['localhost'] and DEVICE_METADATA['localhost']['subtype'] == 'DualToR' %}enabled{% else %}always_disabled{% endif %}" - }, - "telemetry": { - "auto_restart": "enabled", - "has_global_scope": "True", - "has_per_asic_scope": "False", - "delayed": "True", - "high_mem_alert": "disabled", - "set_owner": "kube", - "state": "enabled", - "status": "enabled" - }, - }, - }, - "expected_config_db": { - "FEATURE": { - "dhcp_relay": { - "auto_restart": "enabled", - "has_global_scope": "True", - "has_per_asic_scope": "False", - "delayed": "False", - "high_mem_alert": "disabled", - "set_owner": "kube", - "state": "disabled" - }, - "mux": { - "auto_restart": "enabled", - "has_global_scope": "True", - "has_per_asic_scope": "False", - "delayed": "False", - "high_mem_alert": "disabled", - "set_owner": "local", - "state": "always_disabled" - }, - "telemetry": { - "auto_restart": "enabled", - "has_global_scope": "True", - "has_per_asic_scope": "False", - "delayed": "True", - "high_mem_alert": "disabled", - "set_owner": "kube", - "state": "enabled", - "status": "enabled" - }, - }, - }, - "enable_feature_subprocess_calls": [ - call(["sudo", "systemctl", "stop", "mux.service"]), - call(["sudo", "systemctl", "disable", "mux.service"]), - call(["sudo", "systemctl", "mask", "mux.service"]), - call(["sudo", "systemctl", "unmask", "telemetry.service"]), - call(["sudo", "systemctl", "enable", "telemetry.service"]), - call(["sudo", "systemctl", "start", "telemetry.service"]), - ], - "daemon_reload_subprocess_call": [ - call(["sudo", "systemctl", "daemon-reload"]), - ], - "popen_attributes": { - 'communicate.return_value': ('output', 'error') - }, - }, - ], - [ - "SingleToRCase_DHCP_Relay_Enabled", - { - "device_runtime_metadata": { - "DEVICE_RUNTIME_METADATA": { - "ETHERNET_PORTS_PRESENT":True - } - }, - "config_db": { - "DEVICE_METADATA": { - "localhost": { - "type": "ToR", - } - }, - "KDUMP": { - "config": { - "enabled": "false", - "num_dumps": "3", - "memory": "0M-2G:256M,2G-4G:320M,4G-8G:384M,8G-:448M" - } - }, - "FEATURE": { - "dhcp_relay": { - "auto_restart": "enabled", - "has_global_scope": "True", - "has_per_asic_scope": "False", - "delayed": "False", - "high_mem_alert": "disabled", - "set_owner": "kube", - "state": "enabled" - }, - "mux": { - "auto_restart": "enabled", - "has_global_scope": "True", - "has_per_asic_scope": "False", - "delayed": "False", - "high_mem_alert": "disabled", - "set_owner": "local", - "state": "{% if 'subtype' in DEVICE_METADATA['localhost'] and DEVICE_METADATA['localhost']['subtype'] == 'DualToR' %}enabled{% else %}always_disabled{% endif %}" - }, - "telemetry": { - "auto_restart": "enabled", - "has_global_scope": "True", - "has_per_asic_scope": "False", - "delayed": "True", - "high_mem_alert": "disabled", - "set_owner": "kube", - "state": "enabled", - "status": "enabled" - }, - }, - }, - "expected_config_db": { - "FEATURE": { - "dhcp_relay": { - "auto_restart": "enabled", - "has_global_scope": "True", - "has_per_asic_scope": "False", - "delayed": "False", - "high_mem_alert": "disabled", - "set_owner": "kube", - "state": "enabled" - }, - "mux": { - "auto_restart": "enabled", - "has_global_scope": "True", - "has_per_asic_scope": "False", - "delayed": "False", - "high_mem_alert": "disabled", - "set_owner": "local", - "state": "always_disabled" - }, - "telemetry": { - "auto_restart": "enabled", - "has_global_scope": "True", - "has_per_asic_scope": "False", - "delayed": "True", - "high_mem_alert": "disabled", - "set_owner": "kube", - "state": "enabled", - "status": "enabled" - }, - }, - }, - "enable_feature_subprocess_calls": [ - call(["sudo", "systemctl", "unmask", "dhcp_relay.service"]), - call(["sudo", "systemctl", "enable", "dhcp_relay.service"]), - call(["sudo", "systemctl", "start", "dhcp_relay.service"]), - call(["sudo", "systemctl", "stop", "mux.service"]), - call(["sudo", "systemctl", "disable", "mux.service"]), - call(["sudo", "systemctl", "mask", "mux.service"]), - call(["sudo", "systemctl", "unmask", "telemetry.service"]), - call(["sudo", "systemctl", "enable", "telemetry.service"]), - call(["sudo", "systemctl", "start", "telemetry.service"]), - ], - "daemon_reload_subprocess_call": [ - call(["sudo", "systemctl", "daemon-reload"]), - ], - "popen_attributes": { - 'communicate.return_value': ('output', 'error') - }, - }, - ], - [ - "DualTorCaseWithNoSystemCalls", - { - "device_runtime_metadata": { - "DEVICE_RUNTIME_METADATA": { - "ETHERNET_PORTS_PRESENT":True - } - }, - "config_db": { - "DEVICE_METADATA": { - "localhost": { - "subtype": "DualToR", - "type": "ToRRouter", - } - }, - "KDUMP": { - "config": { - "enabled": "false", - "num_dumps": "3", - "memory": "0M-2G:256M,2G-4G:320M,4G-8G:384M,8G-:448M" - } - }, - "FEATURE": { - "dhcp_relay": { - "auto_restart": "enabled", - "has_global_scope": "True", - "has_per_asic_scope": "False", - "delayed": "False", - "high_mem_alert": "disabled", - "set_owner": "kube", - "state": "{% if not (DEVICE_METADATA is defined and DEVICE_METADATA['localhost'] is defined and DEVICE_METADATA['localhost']['type'] is defined and DEVICE_METADATA['localhost']['type'] != 'ToRRouter') %}enabled{% else %}disabled{% endif %}" - }, - "mux": { - "auto_restart": "enabled", - "has_global_scope": "True", - "has_per_asic_scope": "False", - "delayed": "False", - "high_mem_alert": "disabled", - "set_owner": "local", - "state": "{% if 'subtype' in DEVICE_METADATA['localhost'] and DEVICE_METADATA['localhost']['subtype'] == 'DualToR' %}enabled{% else %}always_disabled{% endif %}" - }, - "telemetry": { - "auto_restart": "enabled", - "has_global_scope": "True", - "has_per_asic_scope": "False", - "delayed": "True", - "high_mem_alert": "disabled", - "set_owner": "kube", - "state": "enabled", - "status": "enabled" - }, - }, - }, - "expected_config_db": { - "FEATURE": { - "dhcp_relay": { - "auto_restart": "enabled", - "has_global_scope": "True", - "has_per_asic_scope": "False", - "delayed": "False", - "high_mem_alert": "disabled", - "set_owner": "kube", - "state": "enabled" - }, - "mux": { - "auto_restart": "enabled", - "has_global_scope": "True", - "has_per_asic_scope": "False", - "delayed": "False", - "high_mem_alert": "disabled", - "set_owner": "local", - "state": "enabled" - }, - "telemetry": { - "auto_restart": "enabled", - "has_global_scope": "True", - "has_per_asic_scope": "False", - "delayed": "True", - "high_mem_alert": "disabled", - "set_owner": "kube", - "state": "enabled", - "status": "enabled" - }, - }, - }, - "enable_feature_subprocess_calls": [], - "daemon_reload_subprocess_call": [ - call(["sudo", "systemctl", "daemon-reload"]), - ], - "popen_attributes": { - 'communicate.return_value': ('enabled', 'error') - }, - } - ], - [ - "Chassis_Supervisor_PACKET", - { - "device_runtime_metadata": { - "DEVICE_RUNTIME_METADATA": { - "CHASSIS_METADATA": { - "module_type": "supervisor", - "chassis_type": "packet" - }, - "ETHERNET_PORTS_PRESENT":True - } - }, - "config_db": { - "DEVICE_METADATA": { - "localhost": { - "type": "SpineRouter", - } - }, - "KDUMP": { - "config": { - "enabled": "false", - "num_dumps": "3", - "memory": "0M-2G:256M,2G-4G:320M,4G-8G:384M,8G-:448M" - } - }, - "FEATURE": { - "bgp": { - "state": "{% if not DEVICE_RUNTIME_METADATA['ETHERNET_PORTS_PRESENT'] or ('CHASSIS_METADATA' in DEVICE_RUNTIME_METADATA and DEVICE_RUNTIME_METADATA['CHASSIS_METADATA']['module_type'] in ['supervisor']) %}disabled{% else %}enabled{% endif %}", - "delayed": "False", - "has_global_scope": "True", - "has_per_asic_scope": "True", - "auto_restart": "enabled", - "high_mem_alert": "disabled" - }, - "teamd": { - "state": "{% if not DEVICE_RUNTIME_METADATA['ETHERNET_PORTS_PRESENT'] %}disabled{% else %}enabled{% endif %}", - "delayed": "False", - "has_global_scope": "True", - "has_per_asic_scope": "True", - "auto_restart": "enabled", - "high_mem_alert": "disabled" - }, - "lldp": { - "state": "enabled", - "delayed": "False", - "has_global_scope": "True", - "has_per_asic_scope": "{% if not DEVICE_RUNTIME_METADATA['ETHERNET_PORTS_PRESENT'] or ('CHASSIS_METADATA' in DEVICE_RUNTIME_METADATA and DEVICE_RUNTIME_METADATA['CHASSIS_METADATA']['module_type'] in ['supervisor']) %}False{% else %}True{% endif %}", - "auto_restart": "enabled", - "high_mem_alert": "disabled" - }, - - - }, - }, - "expected_config_db": { - "FEATURE": { - "bgp": { - "auto_restart": "enabled", - "has_global_scope": "True", - "has_per_asic_scope": "True", - "delayed": "False", - "high_mem_alert": "disabled", - "state": "disabled" - }, - "teamd": { - "auto_restart": "enabled", - "has_global_scope": "True", - "has_per_asic_scope": "True", - "delayed": "False", - "high_mem_alert": "disabled", - "state": "enabled" - }, - "lldp": { - "auto_restart": "enabled", - "has_global_scope": "True", - "has_per_asic_scope": "False", - "delayed": "False", - "high_mem_alert": "disabled", - "state": "enabled" - }, - }, - }, - "enable_feature_subprocess_calls": [ - call(["sudo", "systemctl", "stop", "bgp.service"]), - call(["sudo", "systemctl", "disable", "bgp.service"]), - call(["sudo", "systemctl", "mask", "bgp.service"]), - ], - "daemon_reload_subprocess_call": [ - call(["sudo", "systemctl", "daemon-reload"]), - ], - "popen_attributes": { - 'communicate.return_value': ('output', 'error') - }, - }, - ], - [ - "Chassis_Supervisor_VOQ", - { - "device_runtime_metadata": { - "DEVICE_RUNTIME_METADATA": { - "CHASSIS_METADATA": { - "module_type": "supervisor", - "chassis_type": "voq" - }, - "ETHERNET_PORTS_PRESENT":False - } - }, - "config_db": { - "DEVICE_METADATA": { - "localhost": { - "type": "SpineRouter", - } - }, - "KDUMP": { - "config": { - "enabled": "false", - "num_dumps": "3", - "memory": "0M-2G:256M,2G-4G:320M,4G-8G:384M,8G-:448M" - } - }, - "FEATURE": { - "bgp": { - "state": "{% if not DEVICE_RUNTIME_METADATA['ETHERNET_PORTS_PRESENT'] or ('CHASSIS_METADATA' in DEVICE_RUNTIME_METADATA and DEVICE_RUNTIME_METADATA['CHASSIS_METADATA']['module_type'] in ['supervisor']) %}disabled{% else %}enabled{% endif %}", - "delayed": "False", - "has_global_scope": "True", - "has_per_asic_scope": "True", - "auto_restart": "enabled", - "high_mem_alert": "disabled" - }, - "teamd": { - "state": "{% if not DEVICE_RUNTIME_METADATA['ETHERNET_PORTS_PRESENT'] %}disabled{% else %}enabled{% endif %}", - "delayed": "False", - "has_global_scope": "True", - "has_per_asic_scope": "True", - "auto_restart": "enabled", - "high_mem_alert": "disabled" - }, - "lldp": { - "state": "enabled", - "delayed": "False", - "has_global_scope": "True", - "has_per_asic_scope": "{% if not DEVICE_RUNTIME_METADATA['ETHERNET_PORTS_PRESENT'] or ('CHASSIS_METADATA' in DEVICE_RUNTIME_METADATA and DEVICE_RUNTIME_METADATA['CHASSIS_METADATA']['module_type'] in ['supervisor']) %}False{% else %}True{% endif %}", - "auto_restart": "enabled", - "high_mem_alert": "disabled" - }, - - - }, - }, - "expected_config_db": { - "FEATURE": { - "bgp": { - "auto_restart": "enabled", - "has_global_scope": "True", - "has_per_asic_scope": "True", - "delayed": "False", - "high_mem_alert": "disabled", - "state": "disabled" - }, - "teamd": { - "auto_restart": "enabled", - "has_global_scope": "True", - "has_per_asic_scope": "True", - "delayed": "False", - "high_mem_alert": "disabled", - "state": "disabled" - }, - "lldp": { - "auto_restart": "enabled", - "has_global_scope": "True", - "has_per_asic_scope": "False", - "delayed": "False", - "high_mem_alert": "disabled", - "state": "enabled" - }, - }, - }, - "enable_feature_subprocess_calls": [ - call(["sudo", "systemctl", "stop", "bgp.service"]), - call(["sudo", "systemctl", "disable", "bgp.service"]), - call(["sudo", "systemctl", "mask", "bgp.service"]), - ], - "daemon_reload_subprocess_call": [ - call(["sudo", "systemctl", "daemon-reload"]), - ], - "popen_attributes": { - 'communicate.return_value': ('output', 'error') - }, - }, - ], - [ - "Chassis_LineCard_VOQ", - { - "device_runtime_metadata": { - "DEVICE_RUNTIME_METADATA": { - "CHASSIS_METADATA": { - "module_type": "linecard", - "chassis_type": "voq" - }, - "ETHERNET_PORTS_PRESENT":True - } - }, - "config_db": { - "DEVICE_METADATA": { - "localhost": { - "type": "SpineRouter", - } - }, - "KDUMP": { - "config": { - "enabled": "false", - "num_dumps": "3", - "memory": "0M-2G:256M,2G-4G:320M,4G-8G:384M,8G-:448M" - } - }, - "FEATURE": { - "bgp": { - "state": "{% if not DEVICE_RUNTIME_METADATA['ETHERNET_PORTS_PRESENT'] or ('CHASSIS_METADATA' in DEVICE_RUNTIME_METADATA and DEVICE_RUNTIME_METADATA['CHASSIS_METADATA']['module_type'] in ['supervisor']) %}disabled{% else %}enabled{% endif %}", - "delayed": "False", - "has_global_scope": "True", - "has_per_asic_scope": "True", - "auto_restart": "enabled", - "high_mem_alert": "disabled" - }, - "teamd": { - "state": "{% if not DEVICE_RUNTIME_METADATA['ETHERNET_PORTS_PRESENT'] %}disabled{% else %}enabled{% endif %}", - "delayed": "False", - "has_global_scope": "True", - "has_per_asic_scope": "True", - "auto_restart": "enabled", - "high_mem_alert": "disabled" - }, - "lldp": { - "state": "enabled", - "delayed": "False", - "has_global_scope": "True", - "has_per_asic_scope": "{% if not DEVICE_RUNTIME_METADATA['ETHERNET_PORTS_PRESENT'] or ('CHASSIS_METADATA' in DEVICE_RUNTIME_METADATA and DEVICE_RUNTIME_METADATA['CHASSIS_METADATA']['module_type'] in ['supervisor']) %}False{% else %}True{% endif %}", - "auto_restart": "enabled", - "high_mem_alert": "disabled" - }, - - - }, - }, - "expected_config_db": { - "FEATURE": { - "bgp": { - "auto_restart": "enabled", - "has_global_scope": "True", - "has_per_asic_scope": "True", - "delayed": "False", - "high_mem_alert": "disabled", - "state": "enabled" - }, - "teamd": { - "auto_restart": "enabled", - "has_global_scope": "True", - "has_per_asic_scope": "True", - "delayed": "False", - "high_mem_alert": "disabled", - "state": "enabled" - }, - "lldp": { - "auto_restart": "enabled", - "has_global_scope": "True", - "has_per_asic_scope": "True", - "delayed": "False", - "high_mem_alert": "disabled", - "state": "enabled" - }, - }, - }, - "enable_feature_subprocess_calls": [ - call(["sudo", "systemctl", "start", "bgp.service"]), - call(["sudo", "systemctl", "enable", "bgp.service"]), - call(["sudo", "systemctl", "unmask", "bgp.service"]), - call(["sudo", "systemctl", "start", "teamd.service"]), - call(["sudo", "systemctl", "enable", "teamd.service"]), - call(["sudo", "systemctl", "unmask", "teamd.service"]), - - ], - "daemon_reload_subprocess_call": [ - call(["sudo", "systemctl", "daemon-reload"]), - ], - "popen_attributes": { - 'communicate.return_value': ('output', 'error') - }, - }, - ], - [ - "Chassis_LineCard_Packet", - { - "device_runtime_metadata": { - "DEVICE_RUNTIME_METADATA": { - "CHASSIS_METADATA": { - "module_type": "linecard", - "chassis_type": "packet" - }, - "ETHERNET_PORTS_PRESENT":True - } - }, - "config_db": { - "DEVICE_METADATA": { - "localhost": { - "type": "SpineRouter", - } - }, - "KDUMP": { - "config": { - "enabled": "false", - "num_dumps": "3", - "memory": "0M-2G:256M,2G-4G:320M,4G-8G:384M,8G-:448M" - } - }, - "FEATURE": { - "bgp": { - "state": "{% if not DEVICE_RUNTIME_METADATA['ETHERNET_PORTS_PRESENT'] or ('CHASSIS_METADATA' in DEVICE_RUNTIME_METADATA and DEVICE_RUNTIME_METADATA['CHASSIS_METADATA']['module_type'] in ['supervisor']) %}disabled{% else %}enabled{% endif %}", - "delayed": "False", - "has_global_scope": "True", - "has_per_asic_scope": "True", - "auto_restart": "enabled", - "high_mem_alert": "disabled" - }, - "teamd": { - "state": "{% if not DEVICE_RUNTIME_METADATA['ETHERNET_PORTS_PRESENT'] %}disabled{% else %}enabled{% endif %}", - "delayed": "False", - "has_global_scope": "True", - "has_per_asic_scope": "True", - "auto_restart": "enabled", - "high_mem_alert": "disabled" - }, - "lldp": { - "state": "enabled", - "delayed": "False", - "has_global_scope": "True", - "has_per_asic_scope": "{% if not DEVICE_RUNTIME_METADATA['ETHERNET_PORTS_PRESENT'] or ('CHASSIS_METADATA' in DEVICE_RUNTIME_METADATA and DEVICE_RUNTIME_METADATA['CHASSIS_METADATA']['module_type'] in ['supervisor']) %}False{% else %}True{% endif %}", - "auto_restart": "enabled", - "high_mem_alert": "disabled" - }, - - - }, - }, - "expected_config_db": { - "FEATURE": { - "bgp": { - "auto_restart": "enabled", - "has_global_scope": "True", - "has_per_asic_scope": "True", - "delayed": "False", - "high_mem_alert": "disabled", - "state": "enabled" - }, - "teamd": { - "auto_restart": "enabled", - "has_global_scope": "True", - "has_per_asic_scope": "True", - "delayed": "False", - "high_mem_alert": "disabled", - "state": "enabled" - }, - "lldp": { - "auto_restart": "enabled", - "has_global_scope": "True", - "has_per_asic_scope": "True", - "delayed": "False", - "high_mem_alert": "disabled", - "state": "enabled" - }, - }, - }, - "enable_feature_subprocess_calls": [ - call(["sudo", "systemctl", "start", "bgp.service"]), - call(["sudo", "systemctl", "enable", "bgp.service"]), - call(["sudo", "systemctl", "unmask", "bgp.service"]), - call(["sudo", "systemctl", "start", "teamd.service"]), - call(["sudo", "systemctl", "enable", "teamd.service"]), - call(["sudo", "systemctl", "unmask", "teamd.service"]), - - ], - "daemon_reload_subprocess_call": [ - call(["sudo", "systemctl", "daemon-reload"]), - ], - "popen_attributes": { - 'communicate.return_value': ('output', 'error') - }, - }, - ], - [ - "Chassis_Supervisor_PACKET_multinpu", - { - "num_npu": 2, - "device_runtime_metadata": { - "DEVICE_RUNTIME_METADATA": { - "CHASSIS_METADATA": { - "module_type": "supervisor", - "chassis_type": "packet" - }, - "ETHERNET_PORTS_PRESENT":True - } - }, - "config_db": { - "DEVICE_METADATA": { - "localhost": { - "type": "SpineRouter", - } - }, - "KDUMP": { - "config": { - "enabled": "false", - "num_dumps": "3", - "memory": "0M-2G:256M,2G-4G:320M,4G-8G:384M,8G-:448M" - } - }, - "FEATURE": { - "bgp": { - "state": "{% if not DEVICE_RUNTIME_METADATA['ETHERNET_PORTS_PRESENT'] or ('CHASSIS_METADATA' in DEVICE_RUNTIME_METADATA and DEVICE_RUNTIME_METADATA['CHASSIS_METADATA']['module_type'] in ['supervisor']) %}disabled{% else %}enabled{% endif %}", - "delayed": "False", - "has_global_scope": "False", - "has_per_asic_scope": "True", - "auto_restart": "enabled", - "high_mem_alert": "disabled" - }, - "teamd": { - "state": "{% if not DEVICE_RUNTIME_METADATA['ETHERNET_PORTS_PRESENT'] %}disabled{% else %}enabled{% endif %}", - "delayed": "False", - "has_global_scope": "False", - "has_per_asic_scope": "True", - "auto_restart": "enabled", - "high_mem_alert": "disabled" - }, - "lldp": { - "state": "enabled", - "delayed": "False", - "has_global_scope": "True", - "has_per_asic_scope": "{% if not DEVICE_RUNTIME_METADATA['ETHERNET_PORTS_PRESENT'] or ('CHASSIS_METADATA' in DEVICE_RUNTIME_METADATA and DEVICE_RUNTIME_METADATA['CHASSIS_METADATA']['module_type'] in ['supervisor']) %}False{% else %}True{% endif %}", - "auto_restart": "enabled", - "high_mem_alert": "disabled" - }, - - - }, - }, - "expected_config_db": { - "FEATURE": { - "bgp": { - "auto_restart": "enabled", - "has_global_scope": "False", - "has_per_asic_scope": "True", - "delayed": "False", - "high_mem_alert": "disabled", - "state": "disabled" - }, - "teamd": { - "auto_restart": "enabled", - "has_global_scope": "False", - "has_per_asic_scope": "True", - "delayed": "False", - "high_mem_alert": "disabled", - "state": "enabled" - }, - "lldp": { - "auto_restart": "enabled", - "has_global_scope": "True", - "has_per_asic_scope": "False", - "delayed": "False", - "high_mem_alert": "disabled", - "state": "enabled" - }, - }, - }, - "enable_feature_subprocess_calls": [ - call(["sudo", "systemctl", "stop", "bgp@0.service"]), - call(["sudo", "systemctl", "disable", "bgp@0.service"]), - call(["sudo", "systemctl", "mask", "bgp@0.service"]), - call(["sudo", "systemctl", "stop", "bgp@1.service"]), - call(["sudo", "systemctl", "disable", "bgp@1.service"]), - call(["sudo", "systemctl", "mask", "bgp@1.service"]), - call(["sudo", "systemctl", "start", "teamd@0.service"]), - call(["sudo", "systemctl", "enable", "teamd@0.service"]), - call(["sudo", "systemctl", "unmask", "teamd@0.service"]), - call(["sudo", "systemctl", "start", "teamd@1.service"]), - call(["sudo", "systemctl", "enable", "teamd@1.service"]), - call(["sudo", "systemctl", "unmask", "teamd@1.service"]), - call(["sudo", "systemctl", "stop", "lldp@0.service"]), - call(["sudo", "systemctl", "disable", "lldp@0.service"]), - call(["sudo", "systemctl", "mask", "lldp@0.service"]), - call(["sudo", "systemctl", "stop", "lldp@1.service"]), - call(["sudo", "systemctl", "disable", "lldp@1.service"]), - call(["sudo", "systemctl", "mask", "lldp@1.service"]), - - ], - "daemon_reload_subprocess_call": [ - call(["sudo", "systemctl", "daemon-reload"]), - ], - "popen_attributes": { - 'communicate.return_value': ('output', 'error') - }, - }, - ], - [ - "Chassis_LineCard_VOQ_multinpu", - { - "num_npu": 2, - "device_runtime_metadata": { - "DEVICE_RUNTIME_METADATA": { - "CHASSIS_METADATA": { - "module_type": "linecard", - "chassis_type": "voq" - }, - "ETHERNET_PORTS_PRESENT":True, - "MACSEC_SUPPORTED":True - } - }, - "config_db": { - "DEVICE_METADATA": { - "localhost": { - "type": "SpineRouter", - } - }, - "KDUMP": { - "config": { - "enabled": "false", - "num_dumps": "3", - "memory": "0M-2G:256M,2G-4G:320M,4G-8G:384M,8G-:448M" - } - }, - "FEATURE": { - "bgp": { - "state": "{% if not DEVICE_RUNTIME_METADATA['ETHERNET_PORTS_PRESENT'] or ('CHASSIS_METADATA' in DEVICE_RUNTIME_METADATA and DEVICE_RUNTIME_METADATA['CHASSIS_METADATA']['module_type'] in ['supervisor']) %}disabled{% else %}enabled{% endif %}", - "delayed": "False", - "has_global_scope": "False", - "has_per_asic_scope": "True", - "auto_restart": "enabled", - "high_mem_alert": "disabled" - }, - "teamd": { - "state": "{% if not DEVICE_RUNTIME_METADATA['ETHERNET_PORTS_PRESENT'] %}disabled{% else %}enabled{% endif %}", - "delayed": "False", - "has_global_scope": "False", - "has_per_asic_scope": "True", - "auto_restart": "enabled", - "high_mem_alert": "disabled" - }, - "lldp": { - "state": "enabled", - "delayed": "False", - "has_global_scope": "True", - "has_per_asic_scope": "{% if not DEVICE_RUNTIME_METADATA['ETHERNET_PORTS_PRESENT'] or ('CHASSIS_METADATA' in DEVICE_RUNTIME_METADATA and DEVICE_RUNTIME_METADATA['CHASSIS_METADATA']['module_type'] in ['supervisor']) %}False{% else %}True{% endif %}", - "auto_restart": "enabled", - "high_mem_alert": "disabled" - }, - "macsec": { - "state": "{% if 'type' in DEVICE_METADATA['localhost'] and DEVICE_METADATA['localhost']['type'] == 'SpineRouter' and DEVICE_RUNTIME_METADATA['MACSEC_SUPPORTED'] %}enabled{% else %}disabled{% endif %}", - "delayed": "False", - "has_global_scope": "False", - "has_per_asic_scope": "True", - "auto_restart": "enabled", - "high_mem_alert": "disabled" - } - }, - }, - "expected_config_db": { - "FEATURE": { - "bgp": { - "auto_restart": "enabled", - "has_global_scope": "False", - "has_per_asic_scope": "True", - "delayed": "False", - "high_mem_alert": "disabled", - "state": "enabled" - }, - "teamd": { - "auto_restart": "enabled", - "has_global_scope": "False", - "has_per_asic_scope": "True", - "delayed": "False", - "high_mem_alert": "disabled", - "state": "enabled" - }, - "lldp": { - "auto_restart": "enabled", - "has_global_scope": "True", - "has_per_asic_scope": "True", - "delayed": "False", - "high_mem_alert": "disabled", - "state": "enabled" - }, - "macsec": { - "auto_restart": "enabled", - "has_global_scope": "False", - "has_per_asic_scope": "True", - "delayed": "False", - "high_mem_alert": "disabled", - "state": "enabled" - } - }, - }, - "enable_feature_subprocess_calls": [ - call(['sudo', 'systemctl', 'unmask', 'bgp@0.service']), - call(['sudo', 'systemctl', 'enable', 'bgp@0.service']), - call(['sudo', 'systemctl', 'start', 'bgp@0.service']), - call(['sudo', 'systemctl', 'unmask', 'bgp@1.service']), - call(['sudo', 'systemctl', 'enable', 'bgp@1.service']), - call(['sudo', 'systemctl', 'start', 'bgp@1.service']), - call(['sudo', 'systemctl', 'unmask', 'teamd@0.service']), - call(['sudo', 'systemctl', 'enable', 'teamd@0.service']), - call(['sudo', 'systemctl', 'start', 'teamd@0.service']), - call(['sudo', 'systemctl', 'unmask', 'teamd@1.service']), - call(['sudo', 'systemctl', 'enable', 'teamd@1.service']), - call(['sudo', 'systemctl', 'start', 'teamd@1.service']), - call(['sudo', 'systemctl', 'unmask', 'lldp.service']), - call(['sudo', 'systemctl', 'enable', 'lldp.service']), - call(['sudo', 'systemctl', 'start', 'lldp.service']), - call(['sudo', 'systemctl', 'unmask', 'lldp@0.service']), - call(['sudo', 'systemctl', 'enable', 'lldp@0.service']), - call(['sudo', 'systemctl', 'start', 'lldp@0.service']), - call(['sudo', 'systemctl', 'unmask', 'lldp@1.service']), - call(['sudo', 'systemctl', 'enable', 'lldp@1.service']), - call(['sudo', 'systemctl', 'start', 'lldp@1.service']), - call(['sudo', 'systemctl', 'unmask', 'macsec@0.service']), - call(['sudo', 'systemctl', 'enable', 'macsec@0.service']), - call(['sudo', 'systemctl', 'start', 'macsec@0.service']), - call(['sudo', 'systemctl', 'unmask', 'macsec@1.service']), - call(['sudo', 'systemctl', 'enable', 'macsec@1.service']), - call(['sudo', 'systemctl', 'start', 'macsec@1.service']) - ], - "daemon_reload_subprocess_call": [ - call(["sudo", "systemctl", "daemon-reload"]), - ], - "popen_attributes": { - 'communicate.return_value': ('output', 'error') - }, - }, - ] - -] - HOSTCFG_DAEMON_INIT_CFG_DB = { "FEATURE": {}, "AAA": {}, @@ -1187,36 +26,6 @@ HOSTCFG_DAEMON_CFG_DB = { - "FEATURE": { - "dhcp_relay": { - "auto_restart": "enabled", - "has_global_scope": "True", - "has_per_asic_scope": "False", - "delayed": "False", - "high_mem_alert": "disabled", - "set_owner": "kube", - "state": "{% if not (DEVICE_METADATA is defined and DEVICE_METADATA['localhost'] is defined and DEVICE_METADATA['localhost']['type'] is defined and DEVICE_METADATA['localhost']['type'] != 'ToRRouter') %}enabled{% else %}disabled{% endif %}" - }, - "mux": { - "auto_restart": "enabled", - "has_global_scope": "True", - "has_per_asic_scope": "False", - "delayed": "False", - "high_mem_alert": "disabled", - "set_owner": "local", - "state": "{% if 'subtype' in DEVICE_METADATA['localhost'] and DEVICE_METADATA['localhost']['subtype'] == 'DualToR' %}enabled{% else %}always_disabled{% endif %}" - }, - "telemetry": { - "auto_restart": "enabled", - "has_global_scope": "True", - "has_per_asic_scope": "False", - "delayed": "True", - "high_mem_alert": "disabled", - "set_owner": "kube", - "state": "enabled", - "status": "enabled" - }, - }, "AAA": {}, "TACPLUS": {}, "TACPLUS_SERVER": {}, From d6d7ad0bcd44b1dc67e5e6b74a9e1591d3da77c3 Mon Sep 17 00:00:00 2001 From: Vivek Reddy Date: Wed, 28 Jun 2023 00:32:36 +0000 Subject: [PATCH 2/4] Added new featured daemon Signed-off-by: Vivek Reddy --- scripts/featured | 539 +++++++++++++++ setup.py | 1 + tests/common/mock_configdb.py | 14 +- tests/featured/__init__.py | 0 tests/featured/featured_test.py | 409 +++++++++++ tests/featured/test_vectors.py | 1129 +++++++++++++++++++++++++++++++ 6 files changed, 2087 insertions(+), 5 deletions(-) create mode 100644 scripts/featured create mode 100644 tests/featured/__init__.py create mode 100644 tests/featured/featured_test.py create mode 100644 tests/featured/test_vectors.py diff --git a/scripts/featured b/scripts/featured new file mode 100644 index 00000000..576ee5be --- /dev/null +++ b/scripts/featured @@ -0,0 +1,539 @@ +#!/usr/bin/env python3 + +import ast +import os +import sys +import subprocess +import syslog +import signal +import jinja2 +import time +from sonic_py_common import device_info +from swsscommon.swsscommon import ConfigDBConnector, DBConnector, Table, SonicDBConfig +from swsscommon import swsscommon + + +# MISC Constants +APPL_DB = "APPL_DB" +CFG_DB = "CONFIG_DB" +STATE_DB = "STATE_DB" +FEATURE_TBL = swsscommon.CFG_FEATURE_TABLE_NAME +PORT_TBL = swsscommon.APP_PORT_TABLE_NAME +HOSTCFGD_MAX_PRI = 10 # Used to enforce ordering b/w daemons under Hostcfgd +DEFAULT_SELECT_TIMEOUT = 1000 # 1sec +PORT_INIT_TIMEOUT_SEC = 180 + + +def run_cmd(cmd, log_err=True, raise_exception=False): + try: + subprocess.check_call(cmd) + except Exception as err: + if log_err: + syslog.syslog(syslog.LOG_ERR, "{} - failed: return code - {}, output:\n{}" + .format(err.cmd, err.returncode, err.output)) + if raise_exception: + raise + + +def signal_handler(sig, frame): + if sig == signal.SIGHUP: + syslog.syslog(syslog.LOG_INFO, "FeatureDaemon: signal 'SIGHUP' is caught and ignoring..") + elif sig == signal.SIGINT: + syslog.syslog(syslog.LOG_INFO, "FeatureDaemon: signal 'SIGINT' is caught and exiting...") + sys.exit(128 + sig) + elif sig == signal.SIGTERM: + syslog.syslog(syslog.LOG_INFO, "FeatureDaemon: signal 'SIGTERM' is caught and exiting...") + sys.exit(128 + sig) + else: + syslog.syslog(syslog.LOG_INFO, "FeatureDaemon: invalid signal - ignoring..") + + +def safe_eval(val, default_value=False): + """ Safely evaluate the expression, without raising an exception """ + try: + ret = ast.literal_eval(val) + except ValueError: + ret = default_value + return ret + + +class Feature(object): + """ Represents a feature configuration from CONFIG_DB data. """ + + def __init__(self, feature_name, feature_cfg, device_config=None): + """ Initialize Feature object based on CONFIG_DB data. + + Args: + feature_name (str): Feature name string + feature_cfg (dict): Feature CONFIG_DB configuration + deviec_config (dict): DEVICE_METADATA section of CONFIG_DB + """ + if 'has_timer' in feature_cfg: + err_str = "Invalid obsolete field 'has_timer' in FEATURE table. Please update configuration schema version" + syslog.syslog(syslog.LOG_ERR, err_str) + raise ValueError(err_str) + + self.name = feature_name + self.state = self._get_feature_table_key_render_value(feature_cfg.get('state'), device_config or {}, ['enabled', 'disabled', 'always_enabled', 'always_disabled']) + self.auto_restart = feature_cfg.get('auto_restart', 'disabled') + self.delayed = safe_eval(feature_cfg.get('delayed', 'False')) + self.has_global_scope = safe_eval(feature_cfg.get('has_global_scope', 'True')) + self.has_per_asic_scope = safe_eval(self._get_feature_table_key_render_value(feature_cfg.get('has_per_asic_scope', 'False'), device_config or {}, ['True', 'False'])) + + def _get_feature_table_key_render_value(self, configuration, device_config, expected_values): + """ Returns the target value for the feature by rendering the configuration as J2 template. + + Args: + configuration (str): Feature Table value from CONFIG_DB for given key + device_config (dict): DEVICE_METADATA section of CONFIG_DB and populated Device Running Metadata + expected_values (list): Expected set of Feature Table value for given key + Returns: + (str): Target feature table value for given key + """ + + if configuration is None: + return None + + template = jinja2.Template(configuration) + target_value = template.render(device_config) + if target_value not in expected_values: + raise ValueError('Invalid value rendered for feature {}: {}'.format(self.name, target_value)) + return target_value + + def compare_state(self, feature_name, feature_cfg): + if self.name != feature_name or not isinstance(feature_cfg, dict): + return False + + if self.state != feature_cfg.get('state', ''): + return False + return True + + +class FeatureHandler(object): + """ Handles FEATURE table updates. """ + + SYSTEMD_SYSTEM_DIR = '/etc/systemd/system/' + SYSTEMD_SERVICE_CONF_DIR = os.path.join(SYSTEMD_SYSTEM_DIR, '{}.service.d/') + + # Feature state constants + FEATURE_STATE_ENABLED = "enabled" + FEATURE_STATE_DISABLED = "disabled" + FEATURE_STATE_FAILED = "failed" + + def __init__(self, config_db, feature_state_table, device_config, is_advanced_boot): + self._config_db = config_db + self._feature_state_table = feature_state_table + self._device_config = device_config + self._cached_config = {} + self.is_multi_npu = device_info.is_multi_npu() + self.is_delayed_enabled = False + self.is_advanced_boot = is_advanced_boot + self._device_running_config = device_info.get_device_runtime_metadata() + self.ns_cfg_db = {} + self.ns_feature_state_tbl = {} + + # Initlaize Global config that loads all database*.json + if self.is_multi_npu: + SonicDBConfig.initializeGlobalConfig() + namespaces = device_info.get_namespaces() + for ns in namespaces: + #Connect to ConfigDB in each namespace + self.ns_cfg_db[ns] = ConfigDBConnector(namespace=ns) + self.ns_cfg_db[ns].connect(wait_for_init=True, retry_on=True) + + #Connect to stateDB in each namespace + db_conn = DBConnector(STATE_DB, 0, False, ns) + self.ns_feature_state_tbl[ns] = Table(db_conn, FEATURE_TBL) + + def enable_delayed_services(self): + self.is_delayed_enabled = True + for feature_name in self._cached_config: + if self._cached_config[feature_name].delayed: + self.update_feature_state(self._cached_config[feature_name]) + + def handle_adv_boot(self): + if self.is_advanced_boot: + syslog.syslog(syslog.LOG_INFO, "Updating delayed features after warm/fast boot") + self.enable_delayed_services() + + def handle_port_table_timeout(self): + if not self.is_delayed_enabled: + syslog.syslog(syslog.LOG_INFO, "Updating delayed features after timeout") + self.enable_delayed_services() + + def port_listener(self, key, op, data): + if not key: + return + if op == 'SET' and key == 'PortInitDone' and not self.is_delayed_enabled: + syslog.syslog(syslog.LOG_INFO, "Updating delayed features after port initialization") + self.enable_delayed_services() + + def handler(self, feature_name, op, feature_cfg): + if not feature_cfg: + syslog.syslog(syslog.LOG_INFO, "Deregistering feature {}".format(feature_name)) + self._cached_config.pop(feature_name, None) + self._feature_state_table._del(feature_name) + return + + device_config = {} + device_config.update(self._device_config) + device_config.update(self._device_running_config) + + feature = Feature(feature_name, feature_cfg, device_config) + self._cached_config.setdefault(feature_name, Feature(feature_name, {})) + + # Change auto-restart configuration first. + # If service reached failed state before this configuration applies (e.g. on boot) + # the next called self.update_feature_state will start it again. If it will fail + # again the auto restart will kick-in. Another order may leave it in failed state + # and not auto restart. + if self._cached_config[feature_name].auto_restart != feature.auto_restart: + syslog.syslog(syslog.LOG_INFO, "Auto-restart status of feature '{}' is changed from '{}' to '{}' ..." + .format(feature_name, self._cached_config[feature_name].auto_restart, feature.auto_restart)) + self.update_systemd_config(feature) + self._cached_config[feature_name].auto_restart = feature.auto_restart + + # Enable/disable the container service if the feature state was changed from its previous state. + if self._cached_config[feature_name].state != feature.state: + if self.update_feature_state(feature): + self.sync_feature_asic_scope(feature) + self._cached_config[feature_name] = feature + else: + self.resync_feature_state(self._cached_config[feature_name]) + + def sync_state_field(self, feature_table): + """ + Summary: + Updates the state field in the FEATURE|* tables as the state field + might have to be rendered based on DEVICE_METADATA table and generated Device Running Metadata + """ + for feature_name in feature_table.keys(): + if not feature_name: + syslog.syslog(syslog.LOG_WARNING, "Feature is None") + continue + + device_config = {} + device_config.update(self._device_config) + device_config.update(self._device_running_config) + feature = Feature(feature_name, feature_table[feature_name], device_config) + + self._cached_config.setdefault(feature_name, feature) + self.update_systemd_config(feature) + self.update_feature_state(feature) + self.sync_feature_asic_scope(feature) + self.resync_feature_state(feature) + + def update_feature_state(self, feature): + cached_feature = self._cached_config[feature.name] + enable = False + disable = False + + # Allowed transitions: + # None -> always_enabled + # -> always_disabled + # -> enabled + # -> disabled + # always_enabled -> always_disabled + # enabled -> disabled + # disabled -> enabled + if cached_feature.state is None: + enable = feature.state in ("always_enabled", "enabled") + disable = feature.state in ("always_disabled", "disabled") + elif cached_feature.state in ("always_enabled", "always_disabled"): + disable = feature.state == "always_disabled" + enable = feature.state == "always_enabled" + elif cached_feature.state in ("enabled", "disabled"): + enable = feature.state == "enabled" + disable = feature.state == "disabled" + else: + syslog.syslog(syslog.LOG_INFO, "Feature {} service is {}".format(feature.name, cached_feature.state)) + return False + + if not enable and not disable: + syslog.syslog(syslog.LOG_ERR, "Unexpected state value '{}' for feature {}" + .format(feature.state, feature.name)) + return False + + if feature.delayed and not self.is_delayed_enabled: + syslog.syslog(syslog.LOG_INFO, "Feature is {} delayed for port init".format(feature.name)) + return True + + if enable: + self.enable_feature(feature) + syslog.syslog(syslog.LOG_INFO, "Feature {} is enabled and started".format(feature.name)) + + if disable: + self.disable_feature(feature) + syslog.syslog(syslog.LOG_INFO, "Feature {} is stopped and disabled".format(feature.name)) + + return True + + def sync_feature_asic_scope(self, feature_config): + """Updates the has_per_asic_scope field in the FEATURE|* tables as the field + might have to be rendered based on DEVICE_METADATA table or Device Running configuration. + Disable the ASIC instance service unit file it the render value is False and update config + + Args: + feature: An object represents a feature's configuration in `FEATURE` + table of `CONFIG_DB`. + + Returns: + None. + """ + feature_names, feature_suffixes = self.get_multiasic_feature_instances(feature_config, True) + for feature_name in feature_names: + if '@' not in feature_name: + continue + unit_file_state = self.get_systemd_unit_state("{}.{}".format(feature_name, feature_suffixes[-1])) + if not unit_file_state: + continue + if unit_file_state != "disabled" and not feature_config.has_per_asic_scope: + cmds = [] + for suffix in reversed(feature_suffixes): + cmds.append(["sudo", "systemctl", "stop", "{}.{}".format(feature_name, suffix)]) + cmds.append(["sudo", "systemctl", "disable", "{}.{}".format(feature_name, feature_suffixes[-1])]) + cmds.append(["sudo", "systemctl", "mask", "{}.{}".format(feature_name, feature_suffixes[-1])]) + for cmd in cmds: + syslog.syslog(syslog.LOG_INFO, "Running cmd: '{}'".format(cmd)) + try: + run_cmd(cmd, raise_exception=True) + except Exception as err: + syslog.syslog(syslog.LOG_ERR, "Feature '{}.{}' failed to be stopped and disabled" + .format(feature_name, feature_suffixes[-1])) + self.set_feature_state(feature_config, self.FEATURE_STATE_FAILED) + return + self._config_db.mod_entry(FEATURE_TBL, feature_config.name, {'has_per_asic_scope': str(feature_config.has_per_asic_scope)}) + + # sync has_per_asic_scope to CONFIG_DB in namespaces in multi-asic platform + for ns, db in self.ns_cfg_db.items(): + db.mod_entry(FEATURE_TBL, feature_config.name, {'has_per_asic_scope': str(feature_config.has_per_asic_scope)}) + + def update_systemd_config(self, feature_config): + """Updates `Restart=` field in feature's systemd configuration file + according to the value of `auto_restart` field in `FEATURE` table of `CONFIG_DB`. + + Args: + feature: An object represents a feature's configuration in `FEATURE` + table of `CONFIG_DB`. + + Returns: + None. + """ + restart_field_str = "always" if "enabled" in feature_config.auto_restart else "no" + feature_systemd_config = "[Service]\nRestart={}\n".format(restart_field_str) + feature_names, feature_suffixes = self.get_multiasic_feature_instances(feature_config) + + # On multi-ASIC device, creates systemd configuration file for each feature instance + # residing in difference namespace. + for feature_name in feature_names: + syslog.syslog(syslog.LOG_INFO, "Updating feature '{}' systemd config file related to auto-restart ..." + .format(feature_name)) + feature_systemd_config_dir_path = self.SYSTEMD_SERVICE_CONF_DIR.format(feature_name) + feature_systemd_config_file_path = os.path.join(feature_systemd_config_dir_path, 'auto_restart.conf') + + if not os.path.exists(feature_systemd_config_dir_path): + os.mkdir(feature_systemd_config_dir_path) + with open(feature_systemd_config_file_path, 'w') as feature_systemd_config_file_handler: + feature_systemd_config_file_handler.write(feature_systemd_config) + + syslog.syslog(syslog.LOG_INFO, "Feature '{}' systemd config file related to auto-restart is updated!" + .format(feature_name)) + + try: + syslog.syslog(syslog.LOG_INFO, "Reloading systemd configuration files ...") + run_cmd(["sudo", "systemctl", "daemon-reload"], raise_exception=True) + syslog.syslog(syslog.LOG_INFO, "Systemd configuration files are reloaded!") + except Exception as err: + syslog.syslog(syslog.LOG_ERR, "Failed to reload systemd configuration files!") + + def get_multiasic_feature_instances(self, feature, all_instance=False): + # Create feature name suffix depending feature is running in host or namespace or in both + feature_names = ( + ([feature.name] if feature.has_global_scope or not self.is_multi_npu else []) + + ([(feature.name + '@' + str(asic_inst)) for asic_inst in range(device_info.get_num_npus()) + if self.is_multi_npu and (all_instance or feature.has_per_asic_scope)]) + ) + + if not feature_names: + syslog.syslog(syslog.LOG_ERR, "Feature '{}' service not available" + .format(feature.name)) + + feature_suffixes = ["service"] + + return feature_names, feature_suffixes + + def get_systemd_unit_state(self, unit): + """ Returns service configuration """ + + cmd = ["sudo", "systemctl", "show", unit, "--property", "UnitFileState"] + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = proc.communicate() + if proc.returncode != 0: + syslog.syslog(syslog.LOG_ERR, "Failed to get status of {}: rc={} stderr={}".format(unit, proc.returncode, stderr)) + return 'invalid' # same as systemd's "invalid indicates that it could not be determined whether the unit file is enabled". + + props = dict([line.split("=") for line in stdout.decode().strip().splitlines()]) + return props["UnitFileState"] + + def enable_feature(self, feature): + feature_names, feature_suffixes = self.get_multiasic_feature_instances(feature) + for feature_name in feature_names: + # Check if it is already enabled, if yes skip the system call + unit_file_state = self.get_systemd_unit_state("{}.{}".format(feature_name, feature_suffixes[-1])) + if unit_file_state == "enabled" or not unit_file_state: + continue + cmds = [] + for suffix in feature_suffixes: + cmds.append(["sudo", "systemctl", "unmask", "{}.{}".format(feature_name, suffix)]) + + # If feature has timer associated with it, start/enable corresponding systemd .timer unit + # otherwise, start/enable corresponding systemd .service unit + + cmds.append(["sudo", "systemctl", "enable", "{}.{}".format(feature_name, feature_suffixes[-1])]) + cmds.append(["sudo", "systemctl", "start", "{}.{}".format(feature_name, feature_suffixes[-1])]) + + for cmd in cmds: + syslog.syslog(syslog.LOG_INFO, "Running cmd: '{}'".format(cmd)) + try: + run_cmd(cmd, raise_exception=True) + except Exception as err: + syslog.syslog(syslog.LOG_ERR, "Feature '{}.{}' failed to be enabled and started" + .format(feature.name, feature_suffixes[-1])) + self.set_feature_state(feature, self.FEATURE_STATE_FAILED) + return + + self.set_feature_state(feature, self.FEATURE_STATE_ENABLED) + + def disable_feature(self, feature): + feature_names, feature_suffixes = self.get_multiasic_feature_instances(feature) + for feature_name in feature_names: + # Check if it is already disabled, if yes skip the system call + unit_file_state = self.get_systemd_unit_state("{}.{}".format(feature_name, feature_suffixes[-1])) + if unit_file_state in ("disabled", "masked") or not unit_file_state: + continue + cmds = [] + for suffix in reversed(feature_suffixes): + cmds.append(["sudo", "systemctl", "stop", "{}.{}".format(feature_name, suffix)]) + cmds.append(["sudo", "systemctl", "disable", "{}.{}".format(feature_name, feature_suffixes[-1])]) + cmds.append(["sudo", "systemctl", "mask", "{}.{}".format(feature_name, feature_suffixes[-1])]) + for cmd in cmds: + syslog.syslog(syslog.LOG_INFO, "Running cmd: '{}'".format(cmd)) + try: + run_cmd(cmd, raise_exception=True) + except Exception as err: + syslog.syslog(syslog.LOG_ERR, "Feature '{}.{}' failed to be stopped and disabled" + .format(feature.name, feature_suffixes[-1])) + self.set_feature_state(feature, self.FEATURE_STATE_FAILED) + return + + self.set_feature_state(feature, self.FEATURE_STATE_DISABLED) + + def resync_feature_state(self, feature): + self._config_db.mod_entry(FEATURE_TBL, feature.name, {'state': feature.state}) + + # resync the feature state to CONFIG_DB in namespaces in multi-asic platform + for ns, db in self.ns_cfg_db.items(): + db.mod_entry(FEATURE_TBL, feature.name, {'state': feature.state}) + + def set_feature_state(self, feature, state): + self._feature_state_table.set(feature.name, [('state', state)]) + + # Update the feature state to STATE_DB in namespaces in multi-asic platform + for ns, tbl in self.ns_feature_state_tbl.items(): + tbl.set(feature.name, [('state', state)]) + + +class FeatureDaemon: + def __init__(self): + self.cfg_db_conn = DBConnector(CFG_DB, 0) + self.state_db_conn = DBConnector(STATE_DB, 0) + self.appl_db_conn = DBConnector(APPL_DB, 0) + self.callbacks = dict() # selectable <-> callback map + self.subscriber_map = dict() # subscriber <-> fd map + self.advanced_boot = False + if swsscommon.RestartWaiter.isAdvancedBootInProgress(self.state_db_conn): + self.advanced_boot = True + swsscommon.RestartWaiter.waitAdvancedBootDone() + self.config_db = ConfigDBConnector() + self.config_db.connect(wait_for_init=True, retry_on=True) + self.selector = swsscommon.Select() + syslog.syslog(syslog.LOG_INFO, 'ConfigDB connect success') + + # Load DEVICE metadata configurations + self.device_config = {} + self.device_config['DEVICE_METADATA'] = self.config_db.get_table('DEVICE_METADATA') + + # Load feature state table + feature_state_table = Table(self.state_db_conn, FEATURE_TBL) + + # Intialize Feature Handler + self.feature_handler = FeatureHandler(self.config_db, feature_state_table, self.device_config, self.advanced_boot) + self.feature_handler.handle_adv_boot() + + def subscribe(self, dbconn, table, callback, pri): + try: + if table not in self.callbacks: + self.callbacks[table] = [] + subscriber = swsscommon.SubscriberStateTable(dbconn, table, swsscommon.TableConsumable.DEFAULT_POP_BATCH_SIZE, pri) + self.selector.addSelectable(subscriber) # Add to the Selector + self.subscriber_map[subscriber.getFd()] = (subscriber, table) # Maintain a mapping b/w subscriber & fd + + self.callbacks[table].append(callback) + except Exception as err: + syslog.syslog(syslog.LOG_ERR, "Subscribe to table {} failed with error {}".format(table, err)) + + def register_callbacks(self): + def make_callback(func): + def callback(table, key, op, data): + return func(key, op, data) + return callback + + self.subscribe(self.cfg_db_conn, FEATURE_TBL, + make_callback(self.feature_handler.handler), HOSTCFGD_MAX_PRI) + + self.subscribe(self.appl_db_conn, PORT_TBL, + make_callback(self.feature_handler.port_listener), HOSTCFGD_MAX_PRI-1) + + def render_all_feature_states(self): + features = self.config_db.get_table(FEATURE_TBL) + self.feature_handler.sync_state_field(features) + + def start(self, init_time): + while True: + state, selectable_ = self.selector.select(DEFAULT_SELECT_TIMEOUT) + + if state == self.selector.TIMEOUT: + if int(time.time() - init_time) > PORT_INIT_TIMEOUT_SEC: + # if the delayed services are not enabled until PORT_INIT_TIMEOUT_SEC, enable them + self.feature_handler.handle_port_table_timeout() + continue + elif state == self.selector.ERROR: + syslog.syslog(syslog.LOG_ERR, "error returned by select") + continue + + fd = selectable_.getFd() + # Get the Corresponding subscriber & table + subscriber, table = self.subscriber_map.get(fd, (None, "")) + if not subscriber: + syslog.syslog(syslog.LOG_ERR, + "No Subscriber object found for fd: {}, subscriber map: {}".format(fd, self.subscriber_map)) + continue + key, op, fvs = subscriber.pop() + # Get the registered callback + cbs = self.callbacks.get(table, None) + for callback in cbs: + callback(table, key, op, dict(fvs)) + + +def main(): + init_time = time.time() + signal.signal(signal.SIGTERM, signal_handler) + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGHUP, signal_handler) + daemon = FeatureDaemon() + daemon.render_all_feature_states() + daemon.register_callbacks() + daemon.start(init_time) + +if __name__ == "__main__": + main() diff --git a/setup.py b/setup.py index 5c9898f9..dac72d0c 100644 --- a/setup.py +++ b/setup.py @@ -35,6 +35,7 @@ scripts = [ 'scripts/caclmgrd', 'scripts/hostcfgd', + 'scripts/featured', 'scripts/aaastatsd', 'scripts/procdockerstatsd', 'scripts/determine-reboot-cause', diff --git a/tests/common/mock_configdb.py b/tests/common/mock_configdb.py index 7cf58744..5fbb873d 100644 --- a/tests/common/mock_configdb.py +++ b/tests/common/mock_configdb.py @@ -1,4 +1,3 @@ -import time class MockConfigDb(object): """ Mock Config DB which responds to data tables requests and store updates to the data table @@ -56,11 +55,14 @@ def listen(self, init_data_handler=None): for e in MockConfigDb.event_queue: self.handlers[e[0]](e[0], e[1], self.get_entry(e[0], e[1])) + class MockSelect(): event_queue = [] OBJECT = "OBJECT" + TIMEOUT = "TIMEOUT" ERROR = "" + NUM_TIMEOUT_TRIES = 0 @staticmethod def set_event_queue(Q): @@ -83,12 +85,14 @@ def addSelectable(self, subscriber): self.sub_map[subscriber.table] = subscriber def select(self, TIMEOUT): - if not MockSelect.get_event_queue(): - time.sleep(TIMEOUT/1000) - return "TIMEOUT", {} + if not MockSelect.get_event_queue() and MockSelect.NUM_TIMEOUT_TRIES == 0: + raise TimeoutError + elif MockSelect.NUM_TIMEOUT_TRIES != 0: + MockSelect.NUM_TIMEOUT_TRIES = MockSelect.NUM_TIMEOUT_TRIES - 1 + return MockSelect.TIMEOUT, 0 + table, key = MockSelect.get_event_queue().pop(0) self.sub_map[table].nextKey(key) - self.reset_event_queue() return "OBJECT", self.sub_map[table] diff --git a/tests/featured/__init__.py b/tests/featured/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/featured/featured_test.py b/tests/featured/featured_test.py new file mode 100644 index 00000000..6a1d64f1 --- /dev/null +++ b/tests/featured/featured_test.py @@ -0,0 +1,409 @@ +import os +import sys +import time +import copy +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 FEATURED_TEST_VECTOR, FEATURE_DAEMON_CFG_DB +from tests.common.mock_configdb import MockConfigDb, MockDBConnector, MockSubscriberStateTable, MockSelect +from tests.common.mock_restart_waiter import MockRestartWaiter + +from pyfakefs.fake_filesystem_unittest import patchfs, Patcher +from deepdiff import DeepDiff +from unittest.mock import call + +test_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +modules_path = os.path.dirname(test_path) +scripts_path = os.path.join(modules_path, 'scripts') +sys.path.insert(0, modules_path) + +# Load the file under test +featured_path = os.path.join(scripts_path, 'featured') +featured = load_module_from_source('featured', featured_path) +featured.ConfigDBConnector = MockConfigDb +featured.DBConnector = MockDBConnector +featured.Table = mock.Mock() +swsscommon.Select = MockSelect +swsscommon.SubscriberStateTable = MockSubscriberStateTable +swsscommon.RestartWaiter = MockRestartWaiter + +def syslog_side_effect(pri, msg): + print(f"{pri}: {msg}") + +class TestFeatureHandler(TestCase): + """Test methods of `FeatureHandler` class. + """ + def checks_config_table(self, feature_table, expected_table): + """Compares `FEATURE` table in `CONFIG_DB` with expected output table. + + Args: + feature_table: A dictionary indicates current `FEATURE` table in `CONFIG_DB`. + expected_table A dictionary indicates the expected `FEATURE` table in `CONFIG_DB`. + + Returns: + Returns True if `FEATURE` table in `CONFIG_DB` was not modified unexpectedly; + otherwise, returns False. + """ + ddiff = DeepDiff(feature_table, expected_table, ignore_order=True) + + return True if not ddiff else False + + def checks_systemd_config_file(self, feature_table, feature_systemd_name_map=None): + """Checks whether the systemd configuration file of each feature was created or not + and whether the `Restart=` field in the file is set correctly or not. + + Args: + feature_table: A dictionary indicates `Feature` table in `CONFIG_DB`. + + Returns: Boolean value indicates whether test passed or not. + """ + + truth_table = {'enabled': 'always', + 'disabled': 'no'} + + systemd_config_file_path = os.path.join(featured.FeatureHandler.SYSTEMD_SERVICE_CONF_DIR, + 'auto_restart.conf') + + for feature_name in feature_table: + auto_restart_status = feature_table[feature_name].get('auto_restart', 'disabled') + if "enabled" in auto_restart_status: + auto_restart_status = "enabled" + elif "disabled" in auto_restart_status: + auto_restart_status = "disabled" + + feature_systemd_list = feature_systemd_name_map[feature_name] if feature_systemd_name_map else [feature_name] + + for feature_systemd in feature_systemd_list: + feature_systemd_config_file_path = systemd_config_file_path.format(feature_systemd) + is_config_file_existing = os.path.exists(feature_systemd_config_file_path) + assert is_config_file_existing, "Systemd configuration file of feature '{}' does not exist!".format(feature_systemd) + + with open(feature_systemd_config_file_path) as systemd_config_file: + status = systemd_config_file.read().strip() + assert status == '[Service]\nRestart={}'.format(truth_table[auto_restart_status]) + + def get_state_db_set_calls(self, feature_table): + """Returns a Mock call objects which recorded the `set` calls to `FEATURE` table in `STATE_DB`. + + Args: + feature_table: A dictionary indicates `FEATURE` table in `CONFIG_DB`. + + Returns: + set_call_list: A list indicates Mock call objects. + """ + set_call_list = [] + + for feature_name in feature_table.keys(): + feature_state = "" + if "enabled" in feature_table[feature_name]["state"]: + feature_state = "enabled" + elif "disabled" in feature_table[feature_name]["state"]: + feature_state = "disabled" + else: + feature_state = feature_table[feature_name]["state"] + + set_call_list.append(mock.call(feature_name, [("state", feature_state)])) + + return set_call_list + + @parameterized.expand(FEATURED_TEST_VECTOR) + @patchfs + def test_sync_state_field(self, test_scenario_name, config_data, fs): + """Tests the method `sync_state_field(...)` of `FeatureHandler` class. + + Args: + test_secnario_name: A string indicates different testing scenario. + config_data: A dictionary contains initial `CONFIG_DB` tables and expected results. + + Returns: + Boolean value indicates whether test will pass or not. + """ + # add real path of sesscommon for database_config.json + fs.add_real_paths(swsscommon_package.__path__) + fs.create_dir(featured.FeatureHandler.SYSTEMD_SYSTEM_DIR) + + MockConfigDb.set_config_db(config_data['config_db']) + feature_state_table_mock = mock.Mock() + with mock.patch('featured.subprocess') as mocked_subprocess: + with mock.patch("sonic_py_common.device_info.get_device_runtime_metadata", return_value=config_data['device_runtime_metadata']): + with mock.patch("sonic_py_common.device_info.is_multi_npu", return_value=True if 'num_npu' in config_data else False): + with mock.patch("sonic_py_common.device_info.get_num_npus", return_value=config_data['num_npu'] if 'num_npu' in config_data else 1): + with mock.patch("sonic_py_common.device_info.get_namespaces", return_value=["asic{}".format(a) for a in range(config_data['num_npu'])] if 'num_npu' in config_data else []): + popen_mock = mock.Mock() + attrs = config_data['popen_attributes'] + popen_mock.configure_mock(**attrs) + mocked_subprocess.Popen.return_value = popen_mock + + device_config = {} + device_config['DEVICE_METADATA'] = MockConfigDb.CONFIG_DB['DEVICE_METADATA'] + device_config.update(config_data['device_runtime_metadata']) + + feature_handler = featured.FeatureHandler(MockConfigDb(), feature_state_table_mock, + device_config, False) + feature_handler.is_delayed_enabled = True + feature_table = MockConfigDb.CONFIG_DB['FEATURE'] + feature_handler.sync_state_field(feature_table) + + feature_systemd_name_map = {} + for feature_name in feature_table.keys(): + feature = featured.Feature(feature_name, feature_table[feature_name], device_config) + feature_names, _ = feature_handler.get_multiasic_feature_instances(feature) + feature_systemd_name_map[feature_name] = feature_names + + is_any_difference = self.checks_config_table(MockConfigDb.get_config_db()['FEATURE'], + config_data['expected_config_db']['FEATURE']) + assert is_any_difference, "'FEATURE' table in 'CONFIG_DB' is modified unexpectedly!" + + if 'num_npu' in config_data: + for ns in range(config_data['num_npu']): + namespace = "asic{}".format(ns) + is_any_difference = self.checks_config_table(feature_handler.ns_cfg_db[namespace].get_config_db()['FEATURE'], + config_data['expected_config_db']['FEATURE']) + assert is_any_difference, "'FEATURE' table in 'CONFIG_DB' in namespace {} is modified unexpectedly!".format(namespace) + + feature_table_state_db_calls = self.get_state_db_set_calls(feature_table) + + self.checks_systemd_config_file(config_data['config_db']['FEATURE'], feature_systemd_name_map) + mocked_subprocess.check_call.assert_has_calls(config_data['enable_feature_subprocess_calls'], + any_order=True) + mocked_subprocess.check_call.assert_has_calls(config_data['daemon_reload_subprocess_call'], + any_order=True) + feature_state_table_mock.set.assert_has_calls(feature_table_state_db_calls) + self.checks_systemd_config_file(config_data['config_db']['FEATURE'], feature_systemd_name_map) + + @parameterized.expand(FEATURED_TEST_VECTOR) + @patchfs + def test_handler(self, test_scenario_name, config_data, fs): + """Tests the method `handle(...)` of `FeatureHandler` class. + + Args: + test_secnario_name: A string indicates different testing scenario. + config_data: A dictionary contains initial `CONFIG_DB` tables and expected results. + + Returns: + Boolean value indicates whether test will pass or not. + """ + # add real path of sesscommon for database_config.json + fs.add_real_paths(swsscommon_package.__path__) + fs.create_dir(featured.FeatureHandler.SYSTEMD_SYSTEM_DIR) + + MockConfigDb.set_config_db(config_data['config_db']) + feature_state_table_mock = mock.Mock() + with mock.patch('featured.subprocess') as mocked_subprocess: + with mock.patch("sonic_py_common.device_info.get_device_runtime_metadata", return_value=config_data['device_runtime_metadata']): + with mock.patch("sonic_py_common.device_info.is_multi_npu", return_value=True if 'num_npu' in config_data else False): + with mock.patch("sonic_py_common.device_info.get_num_npus", return_value=config_data['num_npu'] if 'num_npu' in config_data else 1): + popen_mock = mock.Mock() + attrs = config_data['popen_attributes'] + popen_mock.configure_mock(**attrs) + mocked_subprocess.Popen.return_value = popen_mock + + device_config = {} + device_config['DEVICE_METADATA'] = MockConfigDb.CONFIG_DB['DEVICE_METADATA'] + device_config.update(config_data['device_runtime_metadata']) + feature_handler = featured.FeatureHandler(MockConfigDb(), feature_state_table_mock, + device_config, False) + feature_handler.is_delayed_enabled = True + + feature_table = MockConfigDb.CONFIG_DB['FEATURE'] + + feature_systemd_name_map = {} + for feature_name, feature_config in feature_table.items(): + feature_handler.handler(feature_name, 'SET', feature_config) + feature = featured.Feature(feature_name, feature_table[feature_name], device_config) + feature_names, _ = feature_handler.get_multiasic_feature_instances(feature) + feature_systemd_name_map[feature_name] = feature_names + + self.checks_systemd_config_file(config_data['config_db']['FEATURE'], feature_systemd_name_map) + mocked_subprocess.check_call.assert_has_calls(config_data['enable_feature_subprocess_calls'], + any_order=True) + mocked_subprocess.check_call.assert_has_calls(config_data['daemon_reload_subprocess_call'], + any_order=True) + + def test_feature_config_parsing(self): + swss_feature = featured.Feature('swss', { + 'state': 'enabled', + 'auto_restart': 'enabled', + 'delayed': 'True', + 'has_global_scope': 'False', + 'has_per_asic_scope': 'True', + }) + + assert swss_feature.name == 'swss' + assert swss_feature.state == 'enabled' + assert swss_feature.auto_restart == 'enabled' + assert swss_feature.delayed + assert not swss_feature.has_global_scope + assert swss_feature.has_per_asic_scope + + def test_feature_config_parsing_defaults(self): + swss_feature = featured.Feature('swss', { + 'state': 'enabled', + }) + + assert swss_feature.name == 'swss' + assert swss_feature.state == 'enabled' + assert swss_feature.auto_restart == 'disabled' + assert not swss_feature.delayed + assert swss_feature.has_global_scope + assert not swss_feature.has_per_asic_scope + +@mock.patch("syslog.syslog", side_effect=syslog_side_effect) +@mock.patch('sonic_py_common.device_info.get_device_runtime_metadata') +class TestFeatureDaemon(TestCase): + + def setUp(self): + print("Running Setup") + self.patcher = Patcher() + self.patcher.setUp() + self.patcher.fs.create_dir(featured.FeatureHandler.SYSTEMD_SYSTEM_DIR) + MockConfigDb.CONFIG_DB = copy.deepcopy(FEATURE_DAEMON_CFG_DB) + MockRestartWaiter.advancedReboot = False + MockSelect.NUM_TIMEOUT_TRIES = 0 + + def tearDown(self): + print("Running TearDown") + self.patcher.tearDown() + MockConfigDb.CONFIG_DB.clear() + MockSelect.reset_event_queue() + + def test_feature_events(self, mock_syslog, get_runtime): + MockSelect.set_event_queue([('FEATURE', 'dhcp_relay'), + ('FEATURE', 'mux'), + ('FEATURE', 'telemetry')]) + with mock.patch('featured.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 + daemon = featured.FeatureDaemon() + daemon.render_all_feature_states() + daemon.register_callbacks() + try: + daemon.start(time.time()) + except TimeoutError as e: + pass + expected = [call(['sudo', 'systemctl', 'daemon-reload']), + call(['sudo', 'systemctl', 'unmask', 'dhcp_relay.service']), + call(['sudo', 'systemctl', 'enable', 'dhcp_relay.service']), + call(['sudo', 'systemctl', 'start', 'dhcp_relay.service']), + call(['sudo', 'systemctl', 'daemon-reload']), + call(['sudo', 'systemctl', 'unmask', 'mux.service']), + call(['sudo', 'systemctl', 'enable', 'mux.service']), + call(['sudo', 'systemctl', 'start', 'mux.service'])] + mocked_subprocess.check_call.assert_has_calls(expected) + + # Change the state to disabled + MockSelect.reset_event_queue() + MockConfigDb.CONFIG_DB['FEATURE']['dhcp_relay']['state'] = 'disabled' + MockSelect.set_event_queue([('FEATURE', 'dhcp_relay')]) + try: + daemon.start(time.time()) + except TimeoutError: + pass + expected = [call(['sudo', 'systemctl', 'stop', 'dhcp_relay.service']), + call(['sudo', 'systemctl', 'disable', 'dhcp_relay.service']), + call(['sudo', 'systemctl', 'mask', 'dhcp_relay.service'])] + mocked_subprocess.check_call.assert_has_calls(expected) + + def test_delayed_service(self, mock_syslog, get_runtime): + MockSelect.set_event_queue([('FEATURE', 'dhcp_relay'), + ('FEATURE', 'mux'), + ('FEATURE', 'telemetry'), + ('PORT_TABLE', 'PortInitDone')]) + # Note: To simplify testing, subscriberstatetable only read from CONFIG_DB + MockConfigDb.CONFIG_DB['PORT_TABLE'] = {'PortInitDone': {'lanes': '0'}, 'PortConfigDone': {'val': 'true'}} + with mock.patch('featured.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 + daemon = featured.FeatureDaemon() + daemon.register_callbacks() + daemon.render_all_feature_states() + try: + daemon.start(time.time()) + except TimeoutError: + pass + expected = [call(['sudo', 'systemctl', 'daemon-reload']), + call(['sudo', 'systemctl', 'unmask', 'dhcp_relay.service']), + call(['sudo', 'systemctl', 'enable', 'dhcp_relay.service']), + call(['sudo', 'systemctl', 'start', 'dhcp_relay.service']), + call(['sudo', 'systemctl', 'daemon-reload']), + call(['sudo', 'systemctl', 'unmask', 'mux.service']), + call(['sudo', 'systemctl', 'enable', 'mux.service']), + call(['sudo', 'systemctl', 'start', 'mux.service']), + call(['sudo', 'systemctl', 'daemon-reload']), + call(['sudo', 'systemctl', 'unmask', 'telemetry.service']), + call(['sudo', 'systemctl', 'enable', 'telemetry.service']), + call(['sudo', 'systemctl', 'start', 'telemetry.service'])] + + mocked_subprocess.check_call.assert_has_calls(expected) + + def test_advanced_reboot(self, mock_syslog, get_runtime): + MockRestartWaiter.advancedReboot = True + with mock.patch('featured.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 + daemon = featured.FeatureDaemon() + daemon.render_all_feature_states() + daemon.register_callbacks() + try: + daemon.start(time.time()) + except TimeoutError: + pass + expected = [call(['sudo', 'systemctl', 'daemon-reload']), + call(['sudo', 'systemctl', 'unmask', 'dhcp_relay.service']), + call(['sudo', 'systemctl', 'enable', 'dhcp_relay.service']), + call(['sudo', 'systemctl', 'start', 'dhcp_relay.service']), + call(['sudo', 'systemctl', 'daemon-reload']), + call(['sudo', 'systemctl', 'unmask', 'mux.service']), + call(['sudo', 'systemctl', 'enable', 'mux.service']), + call(['sudo', 'systemctl', 'start', 'mux.service']), + call(['sudo', 'systemctl', 'daemon-reload']), + call(['sudo', 'systemctl', 'unmask', 'telemetry.service']), + call(['sudo', 'systemctl', 'enable', 'telemetry.service']), + call(['sudo', 'systemctl', 'start', 'telemetry.service'])] + + mocked_subprocess.check_call.assert_has_calls(expected) + + def test_portinit_timeout(self, mock_syslog, get_runtime): + print(MockConfigDb.CONFIG_DB) + MockSelect.NUM_TIMEOUT_TRIES = 1 + MockSelect.set_event_queue([('FEATURE', 'dhcp_relay'), + ('FEATURE', 'mux'), + ('FEATURE', 'telemetry')]) + with mock.patch('featured.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 + daemon = featured.FeatureDaemon() + daemon.render_all_feature_states() + daemon.register_callbacks() + try: + daemon.start(0.0) + except TimeoutError: + pass + expected = [call(['sudo', 'systemctl', 'daemon-reload']), + call(['sudo', 'systemctl', 'unmask', 'dhcp_relay.service']), + call(['sudo', 'systemctl', 'enable', 'dhcp_relay.service']), + call(['sudo', 'systemctl', 'start', 'dhcp_relay.service']), + call(['sudo', 'systemctl', 'daemon-reload']), + call(['sudo', 'systemctl', 'unmask', 'mux.service']), + call(['sudo', 'systemctl', 'enable', 'mux.service']), + call(['sudo', 'systemctl', 'start', 'mux.service']), + call(['sudo', 'systemctl', 'daemon-reload']), + call(['sudo', 'systemctl', 'unmask', 'telemetry.service']), + call(['sudo', 'systemctl', 'enable', 'telemetry.service']), + call(['sudo', 'systemctl', 'start', 'telemetry.service'])] + mocked_subprocess.check_call.assert_has_calls(expected) + diff --git a/tests/featured/test_vectors.py b/tests/featured/test_vectors.py new file mode 100644 index 00000000..fd916eb0 --- /dev/null +++ b/tests/featured/test_vectors.py @@ -0,0 +1,1129 @@ +from unittest.mock import call + +""" + hostcfgd test vector +""" +FEATURED_TEST_VECTOR = [ + [ + "DualTorCase", + { + "device_runtime_metadata": { + "DEVICE_RUNTIME_METADATA": { + "ETHERNET_PORTS_PRESENT":True + } + }, + "config_db": { + "DEVICE_METADATA": { + "localhost": { + "subtype": "DualToR", + "type": "ToRRouter", + } + }, + "FEATURE": { + "dhcp_relay": { + "auto_restart": "enabled", + "has_global_scope": "True", + "has_per_asic_scope": "False", + "delayed": "False", + "high_mem_alert": "disabled", + "set_owner": "kube", + "state": "{% if not (DEVICE_METADATA is defined and DEVICE_METADATA['localhost'] is defined and DEVICE_METADATA['localhost']['type'] is defined and DEVICE_METADATA['localhost']['type'] != 'ToRRouter') %}enabled{% else %}disabled{% endif %}" + }, + "mux": { + "auto_restart": "enabled", + "has_global_scope": "True", + "has_per_asic_scope": "False", + "delayed": "False", + "high_mem_alert": "disabled", + "set_owner": "local", + "state": "{% if 'subtype' in DEVICE_METADATA['localhost'] and DEVICE_METADATA['localhost']['subtype'] == 'DualToR' %}enabled{% else %}always_disabled{% endif %}" + }, + "telemetry": { + "auto_restart": "enabled", + "has_global_scope": "True", + "has_per_asic_scope": "False", + "delayed": "True", + "high_mem_alert": "disabled", + "set_owner": "kube", + "state": "enabled" + }, + }, + }, + "expected_config_db": { + "FEATURE": { + "dhcp_relay": { + "auto_restart": "enabled", + "has_global_scope": "True", + "has_per_asic_scope": "False", + "delayed": "False", + "high_mem_alert": "disabled", + "set_owner": "kube", + "state": "enabled" + }, + "mux": { + "auto_restart": "enabled", + "has_global_scope": "True", + "has_per_asic_scope": "False", + "delayed": "False", + "high_mem_alert": "disabled", + "set_owner": "local", + "state": "enabled" + }, + "telemetry": { + "auto_restart": "enabled", + "has_global_scope": "True", + "has_per_asic_scope": "False", + "delayed": "True", + "high_mem_alert": "disabled", + "set_owner": "kube", + "state": "enabled" + }, + }, + }, + "enable_feature_subprocess_calls": [ + call(["sudo", "systemctl", "unmask", "dhcp_relay.service"]), + call(["sudo", "systemctl", "enable", "dhcp_relay.service"]), + call(["sudo", "systemctl", "start", "dhcp_relay.service"]), + call(["sudo", "systemctl", "unmask", "mux.service"]), + call(["sudo", "systemctl", "enable", "mux.service"]), + call(["sudo", "systemctl", "start", "mux.service"]), + call(["sudo", "systemctl", "unmask", "telemetry.service"]), + call(["sudo", "systemctl", "enable", "telemetry.service"]), + call(["sudo", "systemctl", "start", "telemetry.service"]), + ], + "daemon_reload_subprocess_call": [ + call(["sudo", "systemctl", "daemon-reload"]), + ], + "popen_attributes": { + 'communicate.return_value': ('output', 'error') + }, + }, + ], + [ + "SingleToRCase", + { + "device_runtime_metadata": { + "DEVICE_RUNTIME_METADATA": { + "ETHERNET_PORTS_PRESENT":True + } + }, + "config_db": { + "DEVICE_METADATA": { + "localhost": { + "type": "ToR", + } + }, + "FEATURE": { + "dhcp_relay": { + "auto_restart": "enabled", + "has_global_scope": "True", + "has_per_asic_scope": "False", + "delayed": "False", + "high_mem_alert": "disabled", + "set_owner": "kube", + "state": "{% if not (DEVICE_METADATA is defined and DEVICE_METADATA['localhost'] is defined and DEVICE_METADATA['localhost']['type'] is defined and DEVICE_METADATA['localhost']['type'] != 'ToRRouter') %}enabled{% else %}disabled{% endif %}" + }, + "mux": { + "auto_restart": "enabled", + "has_global_scope": "True", + "has_per_asic_scope": "False", + "delayed": "False", + "high_mem_alert": "disabled", + "set_owner": "local", + "state": "{% if 'subtype' in DEVICE_METADATA['localhost'] and DEVICE_METADATA['localhost']['subtype'] == 'DualToR' %}enabled{% else %}always_disabled{% endif %}" + }, + "telemetry": { + "auto_restart": "enabled", + "has_global_scope": "True", + "has_per_asic_scope": "False", + "delayed": "True", + "high_mem_alert": "disabled", + "set_owner": "kube", + "state": "enabled", + "status": "enabled" + }, + "sflow": { + "auto_restart": "enabled", + "has_global_scope": "True", + "has_per_asic_scope": "False", + "delayed": "False", + "high_mem_alert": "disabled", + "set_owner": "local", + "state": "always_enabled" + }, + }, + }, + "expected_config_db": { + "FEATURE": { + "dhcp_relay": { + "auto_restart": "enabled", + "has_global_scope": "True", + "has_per_asic_scope": "False", + "delayed": "False", + "high_mem_alert": "disabled", + "set_owner": "kube", + "state": "disabled" + }, + "mux": { + "auto_restart": "enabled", + "has_global_scope": "True", + "has_per_asic_scope": "False", + "delayed": "False", + "high_mem_alert": "disabled", + "set_owner": "local", + "state": "always_disabled" + }, + "telemetry": { + "auto_restart": "enabled", + "has_global_scope": "True", + "has_per_asic_scope": "False", + "delayed": "True", + "high_mem_alert": "disabled", + "set_owner": "kube", + "state": "enabled", + "status": "enabled" + }, + "sflow": { + "auto_restart": "enabled", + "has_global_scope": "True", + "has_per_asic_scope": "False", + "delayed": "False", + "high_mem_alert": "disabled", + "set_owner": "local", + "state": "always_enabled" + }, + }, + }, + "enable_feature_subprocess_calls": [ + call(["sudo", "systemctl", "stop", "mux.service"]), + call(["sudo", "systemctl", "disable", "mux.service"]), + call(["sudo", "systemctl", "mask", "mux.service"]), + call(["sudo", "systemctl", "unmask", "telemetry.service"]), + call(["sudo", "systemctl", "enable", "telemetry.service"]), + call(["sudo", "systemctl", "start", "telemetry.service"]), + call(["sudo", "systemctl", "unmask", "sflow.service"]), + call(["sudo", "systemctl", "enable", "sflow.service"]), + call(["sudo", "systemctl", "start", "sflow.service"]), + ], + "daemon_reload_subprocess_call": [ + call(["sudo", "systemctl", "daemon-reload"]), + ], + "popen_attributes": { + 'communicate.return_value': ('output', 'error') + }, + }, + ], + [ + "T1Case", + { + "device_runtime_metadata": { + "DEVICE_RUNTIME_METADATA": { + "ETHERNET_PORTS_PRESENT":True + } + }, + "config_db": { + "DEVICE_METADATA": { + "localhost": { + "type": "T1", + } + }, + "FEATURE": { + "dhcp_relay": { + "auto_restart": "enabled", + "has_global_scope": "True", + "has_per_asic_scope": "False", + "delayed": "False", + "high_mem_alert": "disabled", + "set_owner": "kube", + "state": "{% if not (DEVICE_METADATA is defined and DEVICE_METADATA['localhost'] is defined and DEVICE_METADATA['localhost']['type'] is defined and DEVICE_METADATA['localhost']['type'] != 'ToRRouter') %}enabled{% else %}disabled{% endif %}" + }, + "mux": { + "auto_restart": "enabled", + "has_global_scope": "True", + "has_per_asic_scope": "False", + "delayed": "False", + "high_mem_alert": "disabled", + "set_owner": "local", + "state": "{% if 'subtype' in DEVICE_METADATA['localhost'] and DEVICE_METADATA['localhost']['subtype'] == 'DualToR' %}enabled{% else %}always_disabled{% endif %}" + }, + "telemetry": { + "auto_restart": "enabled", + "has_global_scope": "True", + "has_per_asic_scope": "False", + "delayed": "True", + "high_mem_alert": "disabled", + "set_owner": "kube", + "state": "enabled", + "status": "enabled" + }, + }, + }, + "expected_config_db": { + "FEATURE": { + "dhcp_relay": { + "auto_restart": "enabled", + "has_global_scope": "True", + "has_per_asic_scope": "False", + "delayed": "False", + "high_mem_alert": "disabled", + "set_owner": "kube", + "state": "disabled" + }, + "mux": { + "auto_restart": "enabled", + "has_global_scope": "True", + "has_per_asic_scope": "False", + "delayed": "False", + "high_mem_alert": "disabled", + "set_owner": "local", + "state": "always_disabled" + }, + "telemetry": { + "auto_restart": "enabled", + "has_global_scope": "True", + "has_per_asic_scope": "False", + "delayed": "True", + "high_mem_alert": "disabled", + "set_owner": "kube", + "state": "enabled", + "status": "enabled" + }, + }, + }, + "enable_feature_subprocess_calls": [ + call(["sudo", "systemctl", "stop", "mux.service"]), + call(["sudo", "systemctl", "disable", "mux.service"]), + call(["sudo", "systemctl", "mask", "mux.service"]), + call(["sudo", "systemctl", "unmask", "telemetry.service"]), + call(["sudo", "systemctl", "enable", "telemetry.service"]), + call(["sudo", "systemctl", "start", "telemetry.service"]), + ], + "daemon_reload_subprocess_call": [ + call(["sudo", "systemctl", "daemon-reload"]), + ], + "popen_attributes": { + 'communicate.return_value': ('output', 'error') + }, + }, + ], + [ + "SingleToRCase_DHCP_Relay_Enabled", + { + "device_runtime_metadata": { + "DEVICE_RUNTIME_METADATA": { + "ETHERNET_PORTS_PRESENT":True + } + }, + "config_db": { + "DEVICE_METADATA": { + "localhost": { + "type": "ToR", + } + }, + "FEATURE": { + "dhcp_relay": { + "auto_restart": "enabled", + "has_global_scope": "True", + "has_per_asic_scope": "False", + "delayed": "False", + "high_mem_alert": "disabled", + "set_owner": "kube", + "state": "enabled" + }, + "mux": { + "auto_restart": "enabled", + "has_global_scope": "True", + "has_per_asic_scope": "False", + "delayed": "False", + "high_mem_alert": "disabled", + "set_owner": "local", + "state": "{% if 'subtype' in DEVICE_METADATA['localhost'] and DEVICE_METADATA['localhost']['subtype'] == 'DualToR' %}enabled{% else %}always_disabled{% endif %}" + }, + "telemetry": { + "auto_restart": "enabled", + "has_global_scope": "True", + "has_per_asic_scope": "False", + "delayed": "True", + "high_mem_alert": "disabled", + "set_owner": "kube", + "state": "enabled", + "status": "enabled" + }, + }, + }, + "expected_config_db": { + "FEATURE": { + "dhcp_relay": { + "auto_restart": "enabled", + "has_global_scope": "True", + "has_per_asic_scope": "False", + "delayed": "False", + "high_mem_alert": "disabled", + "set_owner": "kube", + "state": "enabled" + }, + "mux": { + "auto_restart": "enabled", + "has_global_scope": "True", + "has_per_asic_scope": "False", + "delayed": "False", + "high_mem_alert": "disabled", + "set_owner": "local", + "state": "always_disabled" + }, + "telemetry": { + "auto_restart": "enabled", + "has_global_scope": "True", + "has_per_asic_scope": "False", + "delayed": "True", + "high_mem_alert": "disabled", + "set_owner": "kube", + "state": "enabled", + "status": "enabled" + }, + }, + }, + "enable_feature_subprocess_calls": [ + call(["sudo", "systemctl", "unmask", "dhcp_relay.service"]), + call(["sudo", "systemctl", "enable", "dhcp_relay.service"]), + call(["sudo", "systemctl", "start", "dhcp_relay.service"]), + call(["sudo", "systemctl", "stop", "mux.service"]), + call(["sudo", "systemctl", "disable", "mux.service"]), + call(["sudo", "systemctl", "mask", "mux.service"]), + call(["sudo", "systemctl", "unmask", "telemetry.service"]), + call(["sudo", "systemctl", "enable", "telemetry.service"]), + call(["sudo", "systemctl", "start", "telemetry.service"]), + ], + "daemon_reload_subprocess_call": [ + call(["sudo", "systemctl", "daemon-reload"]), + ], + "popen_attributes": { + 'communicate.return_value': ('output', 'error') + }, + }, + ], + [ + "DualTorCaseWithNoSystemCalls", + { + "device_runtime_metadata": { + "DEVICE_RUNTIME_METADATA": { + "ETHERNET_PORTS_PRESENT":True + } + }, + "config_db": { + "DEVICE_METADATA": { + "localhost": { + "subtype": "DualToR", + "type": "ToRRouter", + } + }, + "FEATURE": { + "dhcp_relay": { + "auto_restart": "enabled", + "has_global_scope": "True", + "has_per_asic_scope": "False", + "delayed": "False", + "high_mem_alert": "disabled", + "set_owner": "kube", + "state": "{% if not (DEVICE_METADATA is defined and DEVICE_METADATA['localhost'] is defined and DEVICE_METADATA['localhost']['type'] is defined and DEVICE_METADATA['localhost']['type'] != 'ToRRouter') %}enabled{% else %}disabled{% endif %}" + }, + "mux": { + "auto_restart": "enabled", + "has_global_scope": "True", + "has_per_asic_scope": "False", + "delayed": "False", + "high_mem_alert": "disabled", + "set_owner": "local", + "state": "{% if 'subtype' in DEVICE_METADATA['localhost'] and DEVICE_METADATA['localhost']['subtype'] == 'DualToR' %}enabled{% else %}always_disabled{% endif %}" + }, + "telemetry": { + "auto_restart": "enabled", + "has_global_scope": "True", + "has_per_asic_scope": "False", + "delayed": "True", + "high_mem_alert": "disabled", + "set_owner": "kube", + "state": "enabled", + "status": "enabled" + }, + }, + }, + "expected_config_db": { + "FEATURE": { + "dhcp_relay": { + "auto_restart": "enabled", + "has_global_scope": "True", + "has_per_asic_scope": "False", + "delayed": "False", + "high_mem_alert": "disabled", + "set_owner": "kube", + "state": "enabled" + }, + "mux": { + "auto_restart": "enabled", + "has_global_scope": "True", + "has_per_asic_scope": "False", + "delayed": "False", + "high_mem_alert": "disabled", + "set_owner": "local", + "state": "enabled" + }, + "telemetry": { + "auto_restart": "enabled", + "has_global_scope": "True", + "has_per_asic_scope": "False", + "delayed": "True", + "high_mem_alert": "disabled", + "set_owner": "kube", + "state": "enabled", + "status": "enabled" + }, + }, + }, + "enable_feature_subprocess_calls": [], + "daemon_reload_subprocess_call": [ + call(["sudo", "systemctl", "daemon-reload"]), + ], + "popen_attributes": { + 'communicate.return_value': ('enabled', 'error') + }, + } + ], + [ + "Chassis_Supervisor_PACKET", + { + "device_runtime_metadata": { + "DEVICE_RUNTIME_METADATA": { + "CHASSIS_METADATA": { + "module_type": "supervisor", + "chassis_type": "packet" + }, + "ETHERNET_PORTS_PRESENT":True + } + }, + "config_db": { + "DEVICE_METADATA": { + "localhost": { + "type": "SpineRouter", + } + }, + "FEATURE": { + "bgp": { + "state": "{% if not DEVICE_RUNTIME_METADATA['ETHERNET_PORTS_PRESENT'] or ('CHASSIS_METADATA' in DEVICE_RUNTIME_METADATA and DEVICE_RUNTIME_METADATA['CHASSIS_METADATA']['module_type'] in ['supervisor']) %}disabled{% else %}enabled{% endif %}", + "delayed": "False", + "has_global_scope": "True", + "has_per_asic_scope": "True", + "auto_restart": "enabled", + "high_mem_alert": "disabled" + }, + "teamd": { + "state": "{% if not DEVICE_RUNTIME_METADATA['ETHERNET_PORTS_PRESENT'] %}disabled{% else %}enabled{% endif %}", + "delayed": "False", + "has_global_scope": "True", + "has_per_asic_scope": "True", + "auto_restart": "enabled", + "high_mem_alert": "disabled" + }, + "lldp": { + "state": "enabled", + "delayed": "False", + "has_global_scope": "True", + "has_per_asic_scope": "{% if not DEVICE_RUNTIME_METADATA['ETHERNET_PORTS_PRESENT'] or ('CHASSIS_METADATA' in DEVICE_RUNTIME_METADATA and DEVICE_RUNTIME_METADATA['CHASSIS_METADATA']['module_type'] in ['supervisor']) %}False{% else %}True{% endif %}", + "auto_restart": "enabled", + "high_mem_alert": "disabled" + }, + + + }, + }, + "expected_config_db": { + "FEATURE": { + "bgp": { + "auto_restart": "enabled", + "has_global_scope": "True", + "has_per_asic_scope": "True", + "delayed": "False", + "high_mem_alert": "disabled", + "state": "disabled" + }, + "teamd": { + "auto_restart": "enabled", + "has_global_scope": "True", + "has_per_asic_scope": "True", + "delayed": "False", + "high_mem_alert": "disabled", + "state": "enabled" + }, + "lldp": { + "auto_restart": "enabled", + "has_global_scope": "True", + "has_per_asic_scope": "False", + "delayed": "False", + "high_mem_alert": "disabled", + "state": "enabled" + }, + }, + }, + "enable_feature_subprocess_calls": [ + call(["sudo", "systemctl", "stop", "bgp.service"]), + call(["sudo", "systemctl", "disable", "bgp.service"]), + call(["sudo", "systemctl", "mask", "bgp.service"]), + ], + "daemon_reload_subprocess_call": [ + call(["sudo", "systemctl", "daemon-reload"]), + ], + "popen_attributes": { + 'communicate.return_value': ('output', 'error') + }, + }, + ], + [ + "Chassis_Supervisor_VOQ", + { + "device_runtime_metadata": { + "DEVICE_RUNTIME_METADATA": { + "CHASSIS_METADATA": { + "module_type": "supervisor", + "chassis_type": "voq" + }, + "ETHERNET_PORTS_PRESENT":False + } + }, + "config_db": { + "DEVICE_METADATA": { + "localhost": { + "type": "SpineRouter", + } + }, + "FEATURE": { + "bgp": { + "state": "{% if not DEVICE_RUNTIME_METADATA['ETHERNET_PORTS_PRESENT'] or ('CHASSIS_METADATA' in DEVICE_RUNTIME_METADATA and DEVICE_RUNTIME_METADATA['CHASSIS_METADATA']['module_type'] in ['supervisor']) %}disabled{% else %}enabled{% endif %}", + "delayed": "False", + "has_global_scope": "True", + "has_per_asic_scope": "True", + "auto_restart": "enabled", + "high_mem_alert": "disabled" + }, + "teamd": { + "state": "{% if not DEVICE_RUNTIME_METADATA['ETHERNET_PORTS_PRESENT'] %}disabled{% else %}enabled{% endif %}", + "delayed": "False", + "has_global_scope": "True", + "has_per_asic_scope": "True", + "auto_restart": "enabled", + "high_mem_alert": "disabled" + }, + "lldp": { + "state": "enabled", + "delayed": "False", + "has_global_scope": "True", + "has_per_asic_scope": "{% if not DEVICE_RUNTIME_METADATA['ETHERNET_PORTS_PRESENT'] or ('CHASSIS_METADATA' in DEVICE_RUNTIME_METADATA and DEVICE_RUNTIME_METADATA['CHASSIS_METADATA']['module_type'] in ['supervisor']) %}False{% else %}True{% endif %}", + "auto_restart": "enabled", + "high_mem_alert": "disabled" + }, + + + }, + }, + "expected_config_db": { + "FEATURE": { + "bgp": { + "auto_restart": "enabled", + "has_global_scope": "True", + "has_per_asic_scope": "True", + "delayed": "False", + "high_mem_alert": "disabled", + "state": "disabled" + }, + "teamd": { + "auto_restart": "enabled", + "has_global_scope": "True", + "has_per_asic_scope": "True", + "delayed": "False", + "high_mem_alert": "disabled", + "state": "disabled" + }, + "lldp": { + "auto_restart": "enabled", + "has_global_scope": "True", + "has_per_asic_scope": "False", + "delayed": "False", + "high_mem_alert": "disabled", + "state": "enabled" + }, + }, + }, + "enable_feature_subprocess_calls": [ + call(["sudo", "systemctl", "stop", "bgp.service"]), + call(["sudo", "systemctl", "disable", "bgp.service"]), + call(["sudo", "systemctl", "mask", "bgp.service"]), + ], + "daemon_reload_subprocess_call": [ + call(["sudo", "systemctl", "daemon-reload"]), + ], + "popen_attributes": { + 'communicate.return_value': ('output', 'error') + }, + }, + ], + [ + "Chassis_LineCard_VOQ", + { + "device_runtime_metadata": { + "DEVICE_RUNTIME_METADATA": { + "CHASSIS_METADATA": { + "module_type": "linecard", + "chassis_type": "voq" + }, + "ETHERNET_PORTS_PRESENT":True + } + }, + "config_db": { + "DEVICE_METADATA": { + "localhost": { + "type": "SpineRouter", + } + }, + "FEATURE": { + "bgp": { + "state": "{% if not DEVICE_RUNTIME_METADATA['ETHERNET_PORTS_PRESENT'] or ('CHASSIS_METADATA' in DEVICE_RUNTIME_METADATA and DEVICE_RUNTIME_METADATA['CHASSIS_METADATA']['module_type'] in ['supervisor']) %}disabled{% else %}enabled{% endif %}", + "delayed": "False", + "has_global_scope": "True", + "has_per_asic_scope": "True", + "auto_restart": "enabled", + "high_mem_alert": "disabled" + }, + "teamd": { + "state": "{% if not DEVICE_RUNTIME_METADATA['ETHERNET_PORTS_PRESENT'] %}disabled{% else %}enabled{% endif %}", + "delayed": "False", + "has_global_scope": "True", + "has_per_asic_scope": "True", + "auto_restart": "enabled", + "high_mem_alert": "disabled" + }, + "lldp": { + "state": "enabled", + "delayed": "False", + "has_global_scope": "True", + "has_per_asic_scope": "{% if not DEVICE_RUNTIME_METADATA['ETHERNET_PORTS_PRESENT'] or ('CHASSIS_METADATA' in DEVICE_RUNTIME_METADATA and DEVICE_RUNTIME_METADATA['CHASSIS_METADATA']['module_type'] in ['supervisor']) %}False{% else %}True{% endif %}", + "auto_restart": "enabled", + "high_mem_alert": "disabled" + }, + + + }, + }, + "expected_config_db": { + "FEATURE": { + "bgp": { + "auto_restart": "enabled", + "has_global_scope": "True", + "has_per_asic_scope": "True", + "delayed": "False", + "high_mem_alert": "disabled", + "state": "enabled" + }, + "teamd": { + "auto_restart": "enabled", + "has_global_scope": "True", + "has_per_asic_scope": "True", + "delayed": "False", + "high_mem_alert": "disabled", + "state": "enabled" + }, + "lldp": { + "auto_restart": "enabled", + "has_global_scope": "True", + "has_per_asic_scope": "True", + "delayed": "False", + "high_mem_alert": "disabled", + "state": "enabled" + }, + }, + }, + "enable_feature_subprocess_calls": [ + call(["sudo", "systemctl", "start", "bgp.service"]), + call(["sudo", "systemctl", "enable", "bgp.service"]), + call(["sudo", "systemctl", "unmask", "bgp.service"]), + call(["sudo", "systemctl", "start", "teamd.service"]), + call(["sudo", "systemctl", "enable", "teamd.service"]), + call(["sudo", "systemctl", "unmask", "teamd.service"]), + + ], + "daemon_reload_subprocess_call": [ + call(["sudo", "systemctl", "daemon-reload"]), + ], + "popen_attributes": { + 'communicate.return_value': ('output', 'error') + }, + }, + ], + [ + "Chassis_LineCard_Packet", + { + "device_runtime_metadata": { + "DEVICE_RUNTIME_METADATA": { + "CHASSIS_METADATA": { + "module_type": "linecard", + "chassis_type": "packet" + }, + "ETHERNET_PORTS_PRESENT":True + } + }, + "config_db": { + "DEVICE_METADATA": { + "localhost": { + "type": "SpineRouter", + } + }, + "FEATURE": { + "bgp": { + "state": "{% if not DEVICE_RUNTIME_METADATA['ETHERNET_PORTS_PRESENT'] or ('CHASSIS_METADATA' in DEVICE_RUNTIME_METADATA and DEVICE_RUNTIME_METADATA['CHASSIS_METADATA']['module_type'] in ['supervisor']) %}disabled{% else %}enabled{% endif %}", + "delayed": "False", + "has_global_scope": "True", + "has_per_asic_scope": "True", + "auto_restart": "enabled", + "high_mem_alert": "disabled" + }, + "teamd": { + "state": "{% if not DEVICE_RUNTIME_METADATA['ETHERNET_PORTS_PRESENT'] %}disabled{% else %}enabled{% endif %}", + "delayed": "False", + "has_global_scope": "True", + "has_per_asic_scope": "True", + "auto_restart": "enabled", + "high_mem_alert": "disabled" + }, + "lldp": { + "state": "enabled", + "delayed": "False", + "has_global_scope": "True", + "has_per_asic_scope": "{% if not DEVICE_RUNTIME_METADATA['ETHERNET_PORTS_PRESENT'] or ('CHASSIS_METADATA' in DEVICE_RUNTIME_METADATA and DEVICE_RUNTIME_METADATA['CHASSIS_METADATA']['module_type'] in ['supervisor']) %}False{% else %}True{% endif %}", + "auto_restart": "enabled", + "high_mem_alert": "disabled" + }, + + + }, + }, + "expected_config_db": { + "FEATURE": { + "bgp": { + "auto_restart": "enabled", + "has_global_scope": "True", + "has_per_asic_scope": "True", + "delayed": "False", + "high_mem_alert": "disabled", + "state": "enabled" + }, + "teamd": { + "auto_restart": "enabled", + "has_global_scope": "True", + "has_per_asic_scope": "True", + "delayed": "False", + "high_mem_alert": "disabled", + "state": "enabled" + }, + "lldp": { + "auto_restart": "enabled", + "has_global_scope": "True", + "has_per_asic_scope": "True", + "delayed": "False", + "high_mem_alert": "disabled", + "state": "enabled" + }, + }, + }, + "enable_feature_subprocess_calls": [ + call(["sudo", "systemctl", "start", "bgp.service"]), + call(["sudo", "systemctl", "enable", "bgp.service"]), + call(["sudo", "systemctl", "unmask", "bgp.service"]), + call(["sudo", "systemctl", "start", "teamd.service"]), + call(["sudo", "systemctl", "enable", "teamd.service"]), + call(["sudo", "systemctl", "unmask", "teamd.service"]), + + ], + "daemon_reload_subprocess_call": [ + call(["sudo", "systemctl", "daemon-reload"]), + ], + "popen_attributes": { + 'communicate.return_value': ('output', 'error') + }, + }, + ], + [ + "Chassis_Supervisor_PACKET_multinpu", + { + "num_npu": 2, + "device_runtime_metadata": { + "DEVICE_RUNTIME_METADATA": { + "CHASSIS_METADATA": { + "module_type": "supervisor", + "chassis_type": "packet" + }, + "ETHERNET_PORTS_PRESENT":True + } + }, + "config_db": { + "DEVICE_METADATA": { + "localhost": { + "type": "SpineRouter", + } + }, + "FEATURE": { + "bgp": { + "state": "{% if not DEVICE_RUNTIME_METADATA['ETHERNET_PORTS_PRESENT'] or ('CHASSIS_METADATA' in DEVICE_RUNTIME_METADATA and DEVICE_RUNTIME_METADATA['CHASSIS_METADATA']['module_type'] in ['supervisor']) %}disabled{% else %}enabled{% endif %}", + "delayed": "False", + "has_global_scope": "False", + "has_per_asic_scope": "True", + "auto_restart": "enabled", + "high_mem_alert": "disabled" + }, + "teamd": { + "state": "{% if not DEVICE_RUNTIME_METADATA['ETHERNET_PORTS_PRESENT'] %}disabled{% else %}enabled{% endif %}", + "delayed": "False", + "has_global_scope": "False", + "has_per_asic_scope": "True", + "auto_restart": "enabled", + "high_mem_alert": "disabled" + }, + "lldp": { + "state": "enabled", + "delayed": "False", + "has_global_scope": "True", + "has_per_asic_scope": "{% if not DEVICE_RUNTIME_METADATA['ETHERNET_PORTS_PRESENT'] or ('CHASSIS_METADATA' in DEVICE_RUNTIME_METADATA and DEVICE_RUNTIME_METADATA['CHASSIS_METADATA']['module_type'] in ['supervisor']) %}False{% else %}True{% endif %}", + "auto_restart": "enabled", + "high_mem_alert": "disabled" + }, + + + }, + }, + "expected_config_db": { + "FEATURE": { + "bgp": { + "auto_restart": "enabled", + "has_global_scope": "False", + "has_per_asic_scope": "True", + "delayed": "False", + "high_mem_alert": "disabled", + "state": "disabled" + }, + "teamd": { + "auto_restart": "enabled", + "has_global_scope": "False", + "has_per_asic_scope": "True", + "delayed": "False", + "high_mem_alert": "disabled", + "state": "enabled" + }, + "lldp": { + "auto_restart": "enabled", + "has_global_scope": "True", + "has_per_asic_scope": "False", + "delayed": "False", + "high_mem_alert": "disabled", + "state": "enabled" + }, + }, + }, + "enable_feature_subprocess_calls": [ + call(["sudo", "systemctl", "stop", "bgp@0.service"]), + call(["sudo", "systemctl", "disable", "bgp@0.service"]), + call(["sudo", "systemctl", "mask", "bgp@0.service"]), + call(["sudo", "systemctl", "stop", "bgp@1.service"]), + call(["sudo", "systemctl", "disable", "bgp@1.service"]), + call(["sudo", "systemctl", "mask", "bgp@1.service"]), + call(["sudo", "systemctl", "start", "teamd@0.service"]), + call(["sudo", "systemctl", "enable", "teamd@0.service"]), + call(["sudo", "systemctl", "unmask", "teamd@0.service"]), + call(["sudo", "systemctl", "start", "teamd@1.service"]), + call(["sudo", "systemctl", "enable", "teamd@1.service"]), + call(["sudo", "systemctl", "unmask", "teamd@1.service"]), + call(["sudo", "systemctl", "stop", "lldp@0.service"]), + call(["sudo", "systemctl", "disable", "lldp@0.service"]), + call(["sudo", "systemctl", "mask", "lldp@0.service"]), + call(["sudo", "systemctl", "stop", "lldp@1.service"]), + call(["sudo", "systemctl", "disable", "lldp@1.service"]), + call(["sudo", "systemctl", "mask", "lldp@1.service"]), + + ], + "daemon_reload_subprocess_call": [ + call(["sudo", "systemctl", "daemon-reload"]), + ], + "popen_attributes": { + 'communicate.return_value': ('output', 'error') + }, + }, + ], + [ + "Chassis_LineCard_VOQ_multinpu", + { + "num_npu": 2, + "device_runtime_metadata": { + "DEVICE_RUNTIME_METADATA": { + "CHASSIS_METADATA": { + "module_type": "linecard", + "chassis_type": "voq" + }, + "ETHERNET_PORTS_PRESENT":True, + "MACSEC_SUPPORTED":True + } + }, + "config_db": { + "DEVICE_METADATA": { + "localhost": { + "type": "SpineRouter", + } + }, + "FEATURE": { + "bgp": { + "state": "{% if not DEVICE_RUNTIME_METADATA['ETHERNET_PORTS_PRESENT'] or ('CHASSIS_METADATA' in DEVICE_RUNTIME_METADATA and DEVICE_RUNTIME_METADATA['CHASSIS_METADATA']['module_type'] in ['supervisor']) %}disabled{% else %}enabled{% endif %}", + "delayed": "False", + "has_global_scope": "False", + "has_per_asic_scope": "True", + "auto_restart": "enabled", + "high_mem_alert": "disabled" + }, + "teamd": { + "state": "{% if not DEVICE_RUNTIME_METADATA['ETHERNET_PORTS_PRESENT'] %}disabled{% else %}enabled{% endif %}", + "delayed": "False", + "has_global_scope": "False", + "has_per_asic_scope": "True", + "auto_restart": "enabled", + "high_mem_alert": "disabled" + }, + "lldp": { + "state": "enabled", + "delayed": "False", + "has_global_scope": "True", + "has_per_asic_scope": "{% if not DEVICE_RUNTIME_METADATA['ETHERNET_PORTS_PRESENT'] or ('CHASSIS_METADATA' in DEVICE_RUNTIME_METADATA and DEVICE_RUNTIME_METADATA['CHASSIS_METADATA']['module_type'] in ['supervisor']) %}False{% else %}True{% endif %}", + "auto_restart": "enabled", + "high_mem_alert": "disabled" + }, + "macsec": { + "state": "{% if 'type' in DEVICE_METADATA['localhost'] and DEVICE_METADATA['localhost']['type'] == 'SpineRouter' and DEVICE_RUNTIME_METADATA['MACSEC_SUPPORTED'] %}enabled{% else %}disabled{% endif %}", + "delayed": "False", + "has_global_scope": "False", + "has_per_asic_scope": "True", + "auto_restart": "enabled", + "high_mem_alert": "disabled" + } + }, + }, + "expected_config_db": { + "FEATURE": { + "bgp": { + "auto_restart": "enabled", + "has_global_scope": "False", + "has_per_asic_scope": "True", + "delayed": "False", + "high_mem_alert": "disabled", + "state": "enabled" + }, + "teamd": { + "auto_restart": "enabled", + "has_global_scope": "False", + "has_per_asic_scope": "True", + "delayed": "False", + "high_mem_alert": "disabled", + "state": "enabled" + }, + "lldp": { + "auto_restart": "enabled", + "has_global_scope": "True", + "has_per_asic_scope": "True", + "delayed": "False", + "high_mem_alert": "disabled", + "state": "enabled" + }, + "macsec": { + "auto_restart": "enabled", + "has_global_scope": "False", + "has_per_asic_scope": "True", + "delayed": "False", + "high_mem_alert": "disabled", + "state": "enabled" + } + }, + }, + "enable_feature_subprocess_calls": [ + call(['sudo', 'systemctl', 'unmask', 'bgp@0.service']), + call(['sudo', 'systemctl', 'enable', 'bgp@0.service']), + call(['sudo', 'systemctl', 'start', 'bgp@0.service']), + call(['sudo', 'systemctl', 'unmask', 'bgp@1.service']), + call(['sudo', 'systemctl', 'enable', 'bgp@1.service']), + call(['sudo', 'systemctl', 'start', 'bgp@1.service']), + call(['sudo', 'systemctl', 'unmask', 'teamd@0.service']), + call(['sudo', 'systemctl', 'enable', 'teamd@0.service']), + call(['sudo', 'systemctl', 'start', 'teamd@0.service']), + call(['sudo', 'systemctl', 'unmask', 'teamd@1.service']), + call(['sudo', 'systemctl', 'enable', 'teamd@1.service']), + call(['sudo', 'systemctl', 'start', 'teamd@1.service']), + call(['sudo', 'systemctl', 'unmask', 'lldp.service']), + call(['sudo', 'systemctl', 'enable', 'lldp.service']), + call(['sudo', 'systemctl', 'start', 'lldp.service']), + call(['sudo', 'systemctl', 'unmask', 'lldp@0.service']), + call(['sudo', 'systemctl', 'enable', 'lldp@0.service']), + call(['sudo', 'systemctl', 'start', 'lldp@0.service']), + call(['sudo', 'systemctl', 'unmask', 'lldp@1.service']), + call(['sudo', 'systemctl', 'enable', 'lldp@1.service']), + call(['sudo', 'systemctl', 'start', 'lldp@1.service']), + call(['sudo', 'systemctl', 'unmask', 'macsec@0.service']), + call(['sudo', 'systemctl', 'enable', 'macsec@0.service']), + call(['sudo', 'systemctl', 'start', 'macsec@0.service']), + call(['sudo', 'systemctl', 'unmask', 'macsec@1.service']), + call(['sudo', 'systemctl', 'enable', 'macsec@1.service']), + call(['sudo', 'systemctl', 'start', 'macsec@1.service']) + ], + "daemon_reload_subprocess_call": [ + call(["sudo", "systemctl", "daemon-reload"]), + ], + "popen_attributes": { + 'communicate.return_value': ('output', 'error') + }, + }, + ] +] + +FEATURE_DAEMON_CFG_DB = { + "FEATURE": { + "dhcp_relay": { + "auto_restart": "enabled", + "has_global_scope": "True", + "has_per_asic_scope": "False", + "delayed": "False", + "high_mem_alert": "disabled", + "set_owner": "kube", + "state": "{% if not (DEVICE_METADATA is defined and DEVICE_METADATA['localhost'] is defined and DEVICE_METADATA['localhost']['type'] is defined and DEVICE_METADATA['localhost']['type'] != 'ToRRouter') %}enabled{% else %}disabled{% endif %}" + }, + "mux": { + "auto_restart": "enabled", + "has_global_scope": "True", + "has_per_asic_scope": "False", + "delayed": "False", + "high_mem_alert": "disabled", + "set_owner": "local", + "state": "{% if 'subtype' in DEVICE_METADATA['localhost'] and DEVICE_METADATA['localhost']['subtype'] == 'DualToR' %}enabled{% else %}always_disabled{% endif %}" + }, + "telemetry": { + "auto_restart": "enabled", + "has_global_scope": "True", + "has_per_asic_scope": "False", + "delayed": "True", + "high_mem_alert": "disabled", + "set_owner": "kube", + "state": "enabled", + "status": "enabled" + }, + }, + "DEVICE_METADATA": { + "localhost": { + "subtype": "DualToR", + "type": "ToRRouter", + "hostname": "SomeNewHostname", + "timezone": "Europe/Kyiv" + } + }, + "DEVICE_RUNTIME_METADATA": { + "ETHERNET_PORTS_PRESENT":True + } +} From 1c83039f1dff47bf5a18d6bb6b5e5698b5483315 Mon Sep 17 00:00:00 2001 From: Vivek Reddy Date: Wed, 28 Jun 2023 00:38:57 +0000 Subject: [PATCH 3/4] retain warmboot check Signed-off-by: Vivek Reddy --- scripts/featured | 2 +- scripts/hostcfgd | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/featured b/scripts/featured index 576ee5be..a7defb07 100644 --- a/scripts/featured +++ b/scripts/featured @@ -526,11 +526,11 @@ class FeatureDaemon: def main(): - init_time = time.time() signal.signal(signal.SIGTERM, signal_handler) signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGHUP, signal_handler) daemon = FeatureDaemon() + init_time = time.time() daemon.render_all_feature_states() daemon.register_callbacks() daemon.start(init_time) diff --git a/scripts/hostcfgd b/scripts/hostcfgd index c08ac3cd..0e6720a5 100644 --- a/scripts/hostcfgd +++ b/scripts/hostcfgd @@ -1204,6 +1204,9 @@ class SyslogCfg: class HostConfigDaemon: def __init__(self): self.state_db_conn = DBConnector(STATE_DB, 0) + # Wait if the Warm/Fast boot is in progress + if swsscommon.RestartWaiter.isAdvancedBootInProgress(self.state_db_conn): + swsscommon.RestartWaiter.waitAdvancedBootDone() # Just a sanity check to verify if the CONFIG_DB has been initialized # before moving forward self.config_db = ConfigDBConnector() From 72d58489e80bb43ca7102332ec4e21c614c16be4 Mon Sep 17 00:00:00 2001 From: Vivek Reddy Date: Sat, 22 Jul 2023 06:29:37 +0000 Subject: [PATCH 4/4] Add missing import Signed-off-by: Vivek Reddy --- scripts/hostcfgd | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/hostcfgd b/scripts/hostcfgd index 029a4dfc..c4239199 100644 --- a/scripts/hostcfgd +++ b/scripts/hostcfgd @@ -9,6 +9,7 @@ import syslog import signal import re import jinja2 +import json from shutil import copy2 from datetime import datetime from sonic_py_common import device_info