diff --git a/sonic-chassisd/scripts/chassisd b/sonic-chassisd/scripts/chassisd index c0ff37445..f5328dad0 100755 --- a/sonic-chassisd/scripts/chassisd +++ b/sonic-chassisd/scripts/chassisd @@ -15,6 +15,8 @@ try: import sys import threading import time + import json + import glob from datetime import datetime from sonic_py_common import daemon_base, logger, device_info @@ -74,7 +76,9 @@ CHASSIS_MODULE_REBOOT_INFO_TABLE = 'CHASSIS_MODULE_REBOOT_INFO_TABLE' CHASSIS_MODULE_REBOOT_TIMESTAMP_FIELD = 'timestamp' CHASSIS_MODULE_REBOOT_REBOOT_FIELD = 'reboot' DEFAULT_LINECARD_REBOOT_TIMEOUT = 180 +DEFAULT_DPU_REBOOT_TIMEOUT = 360 PLATFORM_ENV_CONF_FILE = "/usr/share/sonic/platform/platform_env.conf" +PLATFORM_JSON_FILE = "/usr/share/sonic/platform/platform.json" CHASSIS_INFO_UPDATE_PERIOD_SECS = 10 CHASSIS_DB_CLEANUP_MODULE_DOWN_PERIOD = 30 # Minutes @@ -92,6 +96,8 @@ INVALID_IP = '0.0.0.0' CHASSIS_MODULE_ADMIN_STATUS = 'admin_status' MODULE_ADMIN_DOWN = 0 MODULE_ADMIN_UP = 1 +MODULE_REBOOT_CAUSE_DIR = "/host/reboot-cause/module/" +MAX_HISTORY_FILES = 10 # This daemon should return non-zero exit code so that supervisord will # restart it automatically. @@ -174,6 +180,52 @@ class ModuleConfigUpdater(logger.Logger): self.log_info("Changing module {} to admin {} state".format(key, 'DOWN' if admin_state == MODULE_ADMIN_DOWN else 'UP')) try_get(self.chassis.get_module(module_index).set_admin_state, admin_state, default=False) +# +# SmartSwitch Module Config Updater ======================================================== +# + + +class SmartSwitchModuleConfigUpdater(logger.Logger): + + def __init__(self, log_identifier, chassis): + """ + Constructor for SmartSwitchModuleConfigUpdater + :param chassis: Object representing a platform chassis + """ + super(SmartSwitchModuleConfigUpdater, self).__init__(log_identifier) + + self.chassis = chassis + + def deinit(self): + """ + Destructor of SmartSwitchModuleConfigUpdater + :return: + """ + + def module_config_update(self, key, admin_state): + if not key.startswith(ModuleBase.MODULE_TYPE_DPU): + self.log_error("Incorrect module-name {}. Should start with {}".format(key, + ModuleBase.MODULE_TYPE_DPU)) + return + + module_index = try_get(self.chassis.get_module_index, key, default=INVALID_MODULE_INDEX) + + # Continue if the index is invalid + if module_index < 0: + self.log_error("Unable to get module-index for key {} to set admin-state {}". format(key, admin_state)) + return + + if (admin_state == MODULE_ADMIN_DOWN) or (admin_state == MODULE_ADMIN_UP): + self.log_info("Changing module {} to admin {} state".format(key, 'DOWN' if admin_state == MODULE_ADMIN_DOWN else 'UP')) + t = threading.Thread(target=self.submit_callback, args=(module_index, admin_state)) + t.start() + else: + self.log_warning("Invalid admin_state value: {}".format(admin_state)) + + def submit_callback(self, module_index, admin_state): + try_get(self.chassis.get_module(module_index).set_admin_state, admin_state, default=False) + pass + # # Module Updater ============================================================== # @@ -573,6 +625,410 @@ class ModuleUpdater(logger.Logger): self._cleanup_chassis_app_db(module) self.down_modules[module]['cleaned'] = True +# +# Module Updater ============================================================== +# + + +class SmartSwitchModuleUpdater(ModuleUpdater): + + def __init__(self, log_identifier, chassis): + """ + Constructor for ModuleUpdater + :param chassis: Object representing a platform chassis + """ + super(ModuleUpdater, self).__init__(log_identifier) + + self.chassis = chassis + self.num_modules = self.chassis.get_num_modules() + # Connect to STATE_DB and create chassis info tables + state_db = daemon_base.db_connect("STATE_DB") + self.chassis_table = swsscommon.Table(state_db, CHASSIS_INFO_TABLE) + self.module_table = swsscommon.Table(state_db, CHASSIS_MODULE_INFO_TABLE) + self.midplane_table = swsscommon.Table(state_db, CHASSIS_MIDPLANE_INFO_TABLE) + self.info_dict_keys = [CHASSIS_MODULE_INFO_NAME_FIELD, + CHASSIS_MODULE_INFO_DESC_FIELD, + CHASSIS_MODULE_INFO_SLOT_FIELD, + CHASSIS_MODULE_INFO_SERIAL_FIELD, + CHASSIS_MODULE_INFO_OPERSTATUS_FIELD] + + self.chassis_state_db = daemon_base.db_connect("CHASSIS_STATE_DB") + + self.hostname_table = swsscommon.Table(self.chassis_state_db, CHASSIS_MODULE_HOSTNAME_TABLE) + self.module_reboot_table = swsscommon.Table(self.chassis_state_db, CHASSIS_MODULE_REBOOT_INFO_TABLE) + self.down_modules = {} + self.chassis_app_db_clean_sha = None + + self.midplane_initialized = try_get(chassis.init_midplane_switch, default=False) + if not self.midplane_initialized: + self.log_error("Chassisd midplane intialization failed") + + self.dpu_reboot_timeout = DEFAULT_DPU_REBOOT_TIMEOUT + if os.path.isfile(PLATFORM_JSON_FILE): + try: + with open(PLATFORM_JSON_FILE, 'r') as f: + platform_cfg = json.load(f) + # Extract the "dpu_reboot_timeout" if it exists + self.dpu_reboot_timeout = int(platform_cfg.get("dpu_reboot_timeout", DEFAULT_DPU_REBOOT_TIMEOUT)) + except (json.JSONDecodeError, ValueError) as e: + self.log_error("Error parsing {}: {}".format(PLATFORM_JSON_FILE, e)) + except Exception as e: + self.log_error("Unexpected error: {}".format(e)) + + def deinit(self): + """ + Destructor of ModuleUpdater + :return: + """ + # Delete all the information from DB and then exit + for module_index in range(0, self.num_modules): + name = try_get(self.chassis.get_module(module_index).get_name) + self.module_table._del(name) + if self.midplane_table.get(name) is not None: + self.midplane_table._del(name) + + if self.chassis_table is not None: + self.chassis_table._del(CHASSIS_INFO_KEY_TEMPLATE.format(1)) + + def get_module_admin_status(self, chassis_module_name): + config_db = daemon_base.db_connect("CONFIG_DB") + vtable = swsscommon.Table(config_db, CHASSIS_CFG_TABLE) + fvs = vtable.get(chassis_module_name) + if isinstance(fvs, list) and fvs[0] is True: + fvs = dict(fvs[-1]) + return fvs[CHASSIS_MODULE_ADMIN_STATUS] + else: + return 'down' + + def module_db_update(self): + for module_index in range(0, self.num_modules): + module_info_dict = self._get_module_info(module_index) + if module_info_dict is not None: + key = module_info_dict[CHASSIS_MODULE_INFO_NAME_FIELD] + + if not key.startswith(ModuleBase.MODULE_TYPE_DPU): + self.log_error("Incorrect module-name {}. Should start with {} ".format(key, + ModuleBase.MODULE_TYPE_DPU)) + continue + + fvs = swsscommon.FieldValuePairs([(CHASSIS_MODULE_INFO_DESC_FIELD, module_info_dict[CHASSIS_MODULE_INFO_DESC_FIELD]), + (CHASSIS_MODULE_INFO_SLOT_FIELD, module_info_dict[CHASSIS_MODULE_INFO_SLOT_FIELD]), + (CHASSIS_MODULE_INFO_OPERSTATUS_FIELD, module_info_dict[CHASSIS_MODULE_INFO_OPERSTATUS_FIELD]), + (CHASSIS_MODULE_INFO_SERIAL_FIELD, module_info_dict[CHASSIS_MODULE_INFO_SERIAL_FIELD])]) + + # Get a copy of the previous operational status of the module + prev_status = self.get_module_current_status(key) + self.module_table.set(key, fvs) + + # Get a copy of the current operational status of the module + current_status = module_info_dict[CHASSIS_MODULE_INFO_OPERSTATUS_FIELD] + + # Operational status transitioning to offline + if prev_status != str(ModuleBase.MODULE_STATUS_OFFLINE) and current_status == str(ModuleBase.MODULE_STATUS_OFFLINE): + self.log_notice("{} operational status transitioning to offline".format(key)) + + # Persist dpu down time + self.persist_dpu_reboot_time(key) + + elif prev_status == str(ModuleBase.MODULE_STATUS_OFFLINE) and current_status != str(ModuleBase.MODULE_STATUS_OFFLINE): + self.log_notice("{} operational status transitioning to online".format(key)) + reboot_cause = try_get(self.chassis.get_module(module_index).get_reboot_cause) + + if not self.retrieve_dpu_reboot_time(key) is None or self._is_first_boot(key): + # persist reboot cause + self.persist_dpu_reboot_cause(reboot_cause, key) + # publish reboot cause to db + self.update_dpu_reboot_cause_to_db(key) + + def _get_module_info(self, module_index): + """ + Retrieves module info of this module + """ + module_info_dict = {} + module_info_dict = dict.fromkeys(self.info_dict_keys, 'N/A') + name = try_get(self.chassis.get_module(module_index).get_name) + desc = try_get(self.chassis.get_module(module_index).get_description) + status = try_get(self.chassis.get_module(module_index).get_oper_status, + default=ModuleBase.MODULE_STATUS_OFFLINE) + asics = try_get(self.chassis.get_module(module_index).get_all_asics, + default=[]) + serial = try_get(self.chassis.get_module(module_index).get_serial) + + module_info_dict[CHASSIS_MODULE_INFO_NAME_FIELD] = name + module_info_dict[CHASSIS_MODULE_INFO_DESC_FIELD] = str(desc) + module_info_dict[CHASSIS_MODULE_INFO_SLOT_FIELD] = 'N/A' + module_info_dict[CHASSIS_MODULE_INFO_OPERSTATUS_FIELD] = str(status) + module_info_dict[CHASSIS_MODULE_INFO_SERIAL_FIELD] = str(serial) + + return module_info_dict + + + def update_dpu_state(self, key, state): + """ + Update DPU state in chassisStateDB using the given key. + """ + try: + # Connect to the CHASSIS_STATE_DB using daemon_base + if not self.chassis_state_db: + self.chassis_state_db = daemon_base.db_connect("CHASSIS_STATE_DB") + + # Fetch the current data for the given key and convert it to a dict + current_data = self._convert_to_dict(self.chassis_state_db.hgetall(key)) + + if current_data: + self.chassis_state_db.delete(key) + + # Prepare the updated data + updates = { + "dpu_midplane_link_state": state, + "dpu_midplane_link_reason": "", + "dpu_midplane_link_time": datetime.now().strftime("%a %b %d %I:%M:%S %p UTC %Y"), + } + current_data.update(updates) + + for field, value in current_data.items(): + self.chassis_state_db.hset(key, field, value) + + except Exception as e: + self.log_error(f"Unexpected error: {e}") + + def get_dpu_midplane_state(self, key): + """ + Get DPU midplane-state from chassisStateDB using the given key. + """ + try: + # Connect to the CHASSIS_STATE_DB using daemon_base + if not self.chassis_state_db: + self.chassis_state_db = daemon_base.db_connect("CHASSIS_STATE_DB") + + # Fetch the dpu_midplane_link_state + return self.chassis_state_db.hget(key, "dpu_midplane_link_state") + + except Exception as e: + self.log_error(f"Unexpected error: {e}") + + def _convert_to_dict(self, data): + """ + Converts SWIG proxy object or native dict to a Python dictionary. + """ + if isinstance(data, dict): + return data # Already a dict, return as-is + else: + return dict(data) # Convert SWIG proxy object to dict + + def _get_current_time_str(self): + """Returns the current time as a string in 'YYYY_MM_DD_HH_MM_SS' format.""" + return datetime.now().strftime("%Y_%m_%d_%H_%M_%S") + + def _get_history_path(self, module, file_name): + """Generates the full path for history files.""" + return os.path.join(MODULE_REBOOT_CAUSE_DIR, module.lower(), "history", file_name) + + def _is_first_boot(self, module): + """Checks if the reboot-cause file indicates a first boot.""" + file_path = os.path.join(MODULE_REBOOT_CAUSE_DIR, module.lower(), "reboot-cause.txt") + + try: + with open(file_path, 'r') as f: + content = f.read().strip() + return content == "First boot" + except FileNotFoundError: + return False + + def persist_dpu_reboot_time(self, module): + """Persist the current reboot time to a file.""" + time_str = self._get_current_time_str() + path = self._get_history_path(module, "prev_reboot_time.txt") + + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path, 'w') as f: + f.write(time_str) + + def retrieve_dpu_reboot_time(self, module): + """Retrieve the persisted reboot time from a file.""" + path = self._get_history_path(module, "prev_reboot_time.txt") + + try: + with open(path, 'r') as f: + return f.read().strip() + except FileNotFoundError: + return None + + def persist_dpu_reboot_cause(self, reboot_cause, module): + """Persist the reboot cause information and handle file rotation.""" + # Extract cause and comment from the reboot_cause + if reboot_cause: + try: + cause, comment = ( + reboot_cause.split(",", 1) if isinstance(reboot_cause, str) else reboot_cause + ) + except ValueError: + cause = reboot_cause if isinstance(reboot_cause, str) else "Unknown" + comment = "N/A" + else: + cause, comment = "Unknown", "N/A" + + prev_reboot_time = self.retrieve_dpu_reboot_time(module) + if prev_reboot_time is None: + prev_reboot_time = self._get_current_time_str() + + file_name = f"{prev_reboot_time}_reboot_cause.txt" + prev_reboot_path = self._get_history_path(module, "prev_reboot_time.txt") + + if os.path.exists(prev_reboot_path): + os.remove(prev_reboot_path) + + file_path = self._get_history_path(module, file_name) + try: + dt_obj = datetime.strptime(prev_reboot_time, "%Y_%m_%d_%H_%M_%S") + except ValueError: + dt_obj = datetime.now() + + formatted_time = dt_obj.strftime("%a %b %d %I:%M:%S %p UTC %Y") + + reboot_cause_dict = { + "cause": cause, + "comment": comment, + "device": module, + "time": formatted_time, + "name": prev_reboot_time, + } + + with open(file_path, 'w') as f: + json.dump(reboot_cause_dict, f) + + # Write the reboot_cause content to the reboot-cause.txt file, overwriting it + reboot_cause_path = os.path.join(MODULE_REBOOT_CAUSE_DIR, module.lower(), "reboot-cause.txt") + os.makedirs(os.path.dirname(reboot_cause_path), exist_ok=True) + with open(reboot_cause_path, 'w') as cause_file: + cause_file.write(json.dumps(reboot_cause) + '\n') + + # Update symlink to the latest reboot cause file + symlink_path = os.path.join(MODULE_REBOOT_CAUSE_DIR, module.lower(), "previous-reboot-cause.json") + if os.path.exists(symlink_path): + os.remove(symlink_path) + if os.path.exists(file_path): + os.symlink(file_path, symlink_path) + + # Perform file rotation if necessary + self._rotate_files(module) + + def _rotate_files(self, module): + """Rotate history files if they exceed the maximum limit.""" + history_dir = os.path.join(MODULE_REBOOT_CAUSE_DIR, module.lower(), "history") + os.makedirs(history_dir, exist_ok=True) + try: + files = sorted(os.listdir(history_dir)) + except FileNotFoundError: + return + + if not files: + return + + if len(files) > MAX_HISTORY_FILES: + for old_file in files[:-MAX_HISTORY_FILES]: + os.remove(os.path.join(history_dir, old_file)) + + def update_dpu_reboot_cause_to_db(self, module): + """Update the reboot cause in CHASSIS_STATE_DB for a given module.""" + + # Ensure the DB connection is active + if not self.chassis_state_db: + self.chassis_state_db = daemon_base.db_connect("CHASSIS_STATE_DB") + + # Delete existing keys for the module in CHASSIS_STATE_DB + pattern = f"REBOOT_CAUSE|{module.upper()}|*" + keys = self.chassis_state_db.keys(pattern) + if keys: + for key in keys: + self.chassis_state_db.delete(key) + + # Fetch the list of reboot cause history files + history_path = f"/host/reboot-cause/module/{module.lower()}/history/*_reboot_cause.txt" + reboot_cause_files = glob.glob(history_path) + + if not reboot_cause_files: + self.log_warning(f"No reboot cause history files found for module: {module}") + return + + # Iterate over each reboot cause file and store data in CHASSIS_STATE_DB + for file_path in reboot_cause_files: + try: + with open(file_path, "r") as file: + reboot_cause_dict = json.load(file) + + if not reboot_cause_dict: + self.log_warning(f"{module} reboot_cause_dict is empty") + continue + + # Generate the key based on module and reboot time + reboot_time = reboot_cause_dict.get("name", self._get_current_time_str()) + key = f"REBOOT_CAUSE|{module.upper()}|{reboot_time}" + + # Publish the reboot cause information to CHASSIS_STATE_DB + for field, value in reboot_cause_dict.items(): + if field and value is not None: + self.chassis_state_db.hset(key, field, value) + + except json.JSONDecodeError: + self.log_warning(f"Failed to decode JSON from file: {file_path}") + except Exception as e: + self.log_warning(f"Error processing file {file_path}: {e}") + + def check_midplane_reachability(self): + if not self.midplane_initialized: + return + + index = -1 + for module in self.chassis.get_all_modules(): + index += 1 + + module_key = try_get(module.get_name, default='MODULE {}'.format(index)) + midplane_ip = try_get(module.get_midplane_ip, default=INVALID_IP) + midplane_access = try_get(module.is_midplane_reachable, default=False) + # Generate syslog for the loss of midplane connectivity when midplane connectivity + # loss is detected for the first time + current_midplane_state = 'False' + fvs = self.midplane_table.get(module_key) + if isinstance(fvs, list) and fvs[0] is True: + fvs = dict(fvs[-1]) + current_midplane_state = fvs[CHASSIS_MIDPLANE_INFO_ACCESS_FIELD] + + if midplane_access is False and current_midplane_state == 'True': + self.log_warning("Unexpected: Module {} lost midplane connectivity".format(module_key)) + + elif midplane_access is True and current_midplane_state == 'False': + self.log_notice("Module {} midplane connectivity is up".format(module_key)) + + # Update midplane state in the chassisStateDB DPU_STATE table + key = "DPU_STATE|" + module_key + dpu_mp_state = self.get_dpu_midplane_state(key) + if midplane_access and dpu_mp_state != 'up': + self.update_dpu_state(key, 'up') + elif not midplane_access and dpu_mp_state != 'down': + self.update_dpu_state(key, "down") + + # Update db with midplane information + fvs = swsscommon.FieldValuePairs([(CHASSIS_MIDPLANE_INFO_IP_FIELD, midplane_ip), + (CHASSIS_MIDPLANE_INFO_ACCESS_FIELD, str(midplane_access))]) + self.midplane_table.set(module_key, fvs) + + def module_down_chassis_db_cleanup(self): + # cleanup CHASSIS_STATE_DB + if not self.chassis_state_db: + self.chassis_state_db = daemon_base.db_connect("CHASSIS_STATE_DB") + + for module_index in range(0, self.num_modules): + name = try_get(self.chassis.get_module(module_index).get_name) + pattern = "*" + name + "*" + if self.get_module_admin_status(name) != 'up': + keys = self.chassis_state_db.keys(pattern) + if keys: + for key in keys: + if not "DPU_STATE" in key and not "REBOOT_CAUSE" in key: + self.chassis_state_db.delete(key) + return + # # Config Manager task ======================================================== @@ -620,6 +1076,56 @@ class ConfigManagerTask(ProcessTaskBase): self.config_updater.module_config_update(key, admin_state) +# +# SmartSwitch Config Manager task ======================================================== +# + + +class SmartSwitchConfigManagerTask(ProcessTaskBase): + def __init__(self): + ProcessTaskBase.__init__(self) + + # TODO: Refactor to eliminate the need for this Logger instance + self.logger = logger.Logger(SYSLOG_IDENTIFIER) + + def task_worker(self): + self.config_updater = SmartSwitchModuleConfigUpdater(SYSLOG_IDENTIFIER, get_chassis()) + config_db = daemon_base.db_connect("CONFIG_DB") + + # Subscribe to CHASSIS_MODULE table notifications in the Config DB + sel = swsscommon.Select() + sst = swsscommon.SubscriberStateTable(config_db, CHASSIS_CFG_TABLE) + sel.addSelectable(sst) + + # Listen indefinitely for changes to the CFG_CHASSIS_MODULE_TABLE table in the Config DB + while True: + # Use timeout to prevent ignoring the signals we want to handle + # in signal_handler() (e.g. SIGTERM for graceful shutdown) + (state, c) = sel.select(SELECT_TIMEOUT) + + if state == swsscommon.Select.TIMEOUT: + # Do not flood log when select times out + continue + if state != swsscommon.Select.OBJECT: + self.logger.log_warning("sel.select() did not return swsscommon.Select.OBJECT") + continue + + (key, op, fvp) = sst.pop() + + if op == 'SET': + fvs = dict(fvp) + admin_status = fvs.get('admin_status') + if admin_status == 'up': + admin_state = MODULE_ADMIN_UP + else: + admin_state = MODULE_ADMIN_DOWN + elif op == 'DEL': + admin_state = MODULE_ADMIN_DOWN + else: + continue + + self.config_updater.module_config_update(key, admin_state) + # # State Manager task ======================================================== # @@ -753,26 +1259,33 @@ class ChassisdDaemon(daemon_base.DaemonBase): else: self.log_warning("Caught unhandled signal '{}' - ignoring...".format(SIGNALS_TO_NAMES_DICT[sig])) + # Run daemon def run(self): self.log_info("Starting up...") - # Check for valid slot numbers - my_slot = try_get(self.platform_chassis.get_my_slot, - default=INVALID_SLOT) - supervisor_slot = try_get(self.platform_chassis.get_supervisor_slot, - default=INVALID_SLOT) - # Check if module list is populated - self.module_updater = ModuleUpdater(SYSLOG_IDENTIFIER, self.platform_chassis, my_slot, supervisor_slot) + self.smartswitch = self.platform_chassis.is_smartswitch() + self.log_info("smartswitch: {}".format(self.smartswitch)) + + if self.smartswitch: + self.module_updater = SmartSwitchModuleUpdater(SYSLOG_IDENTIFIER, self.platform_chassis) + else: + my_slot = try_get(self.platform_chassis.get_my_slot, default=INVALID_SLOT) + supervisor_slot = try_get(self.platform_chassis.get_supervisor_slot, default=INVALID_SLOT) + self.module_updater = ModuleUpdater(SYSLOG_IDENTIFIER, self.platform_chassis, my_slot, supervisor_slot) self.module_updater.modules_num_update() - if ((self.module_updater.my_slot == INVALID_SLOT) or - (self.module_updater.supervisor_slot == INVALID_SLOT)): - self.log_error("Chassisd not supported for this platform") - sys.exit(CHASSIS_NOT_SUPPORTED) + if not self.smartswitch: + if ((self.module_updater.my_slot == INVALID_SLOT) or + (self.module_updater.supervisor_slot == INVALID_SLOT)): + self.log_error("Chassisd not supported for this platform") + sys.exit(CHASSIS_NOT_SUPPORTED) - # Start configuration manager task on supervisor module - if self.module_updater.supervisor_slot == self.module_updater.my_slot: + # Start configuration manager task + if self.smartswitch: + config_manager = SmartSwitchConfigManagerTask() + config_manager.task_run() + elif self.module_updater.supervisor_slot == self.module_updater.my_slot: config_manager = ConfigManagerTask() config_manager.task_run() else: diff --git a/sonic-chassisd/tests/mock_module_base.py b/sonic-chassisd/tests/mock_module_base.py index fcbe0ef58..2099e200a 100644 --- a/sonic-chassisd/tests/mock_module_base.py +++ b/sonic-chassisd/tests/mock_module_base.py @@ -6,6 +6,7 @@ class ModuleBase(): MODULE_TYPE_SUPERVISOR = "SUPERVISOR" MODULE_TYPE_LINE = "LINE-CARD" MODULE_TYPE_FABRIC = "FABRIC-CARD" + MODULE_TYPE_DPU = "DPU" # Possible card status for modular chassis # Module state is Empty if no module is inserted in the slot diff --git a/sonic-chassisd/tests/mock_platform.py b/sonic-chassisd/tests/mock_platform.py index a2b7a97c2..f2a58ee76 100644 --- a/sonic-chassisd/tests/mock_platform.py +++ b/sonic-chassisd/tests/mock_platform.py @@ -58,6 +58,8 @@ def get_admin_state(self): return self.admin_state def get_midplane_ip(self): + if "DPU" in self.get_name(): + self.midplane_ip = '169.254.200.0' return self.midplane_ip def set_midplane_ip(self): @@ -75,6 +77,9 @@ def set_midplane_reachable(self, up): def get_all_asics(self): return self.asic_list + def get_reboot_cause(self): + return 'reboot', 'N/A' + def get_serial(self): return self.module_serial @@ -82,6 +87,50 @@ class MockChassis: def __init__(self): self.module_list = [] self.midplane_supervisor_access = False + self._is_smartswitch = False + + def get_num_modules(self): + return len(self.module_list) + + def get_module(self, index): + module = self.module_list[index] + return module + + def get_all_modules(self): + return self.module_list + + def get_module_index(self, module_name): + for module in self.module_list: + if module.module_name == module_name: + return module.module_index + return -1 + + def init_midplane_switch(self): + return True + + def get_serial(self): + return "Serial No" + + def get_model(self): + return "Model A" + + def get_revision(self): + return "Rev C" + + def is_smartswitch(self): + return self._is_smartswitch + + def get_my_slot(self): + return 1 + + def get_supervisor_slot(self): + return 0 + +class MockSmartSwitchChassis: + def __init__(self): + self.module_list = [] + self.midplane_supervisor_access = False + self._is_smartswitch = True def get_num_modules(self): return len(self.module_list) @@ -111,6 +160,9 @@ def get_model(self): def get_revision(self): return "Rev C" + def is_smartswitch(self): + return self._is_smartswitch + def get_dataplane_state(self): raise NotImplementedError diff --git a/sonic-chassisd/tests/test_chassisd.py b/sonic-chassisd/tests/test_chassisd.py index bd1706af6..7dca0738e 100644 --- a/sonic-chassisd/tests/test_chassisd.py +++ b/sonic-chassisd/tests/test_chassisd.py @@ -1,14 +1,19 @@ import os import sys import mock +import tempfile +import json from imp import load_source -from mock import Mock, MagicMock, patch +from mock import Mock, MagicMock, patch, mock_open from sonic_py_common import daemon_base -from .mock_platform import MockChassis, MockModule +from .mock_platform import MockChassis, MockSmartSwitchChassis, MockModule from .mock_module_base import ModuleBase +# Assuming OBJECT should be a specific value, define it manually +SELECT_OBJECT = 1 # Replace with the actual value for OBJECT if know + SYSLOG_IDENTIFIER = 'chassisd_test' NOT_AVAILABLE = 'N/A' @@ -44,6 +49,8 @@ CHASSIS_MODULE_REBOOT_TIMESTAMP_FIELD = 'timestamp' CHASSIS_MODULE_REBOOT_REBOOT_FIELD = 'reboot' PLATFORM_ENV_CONF_FILE = "/usr/share/sonic/platform/platform_env.conf" +PLATFORM_JSON_FILE = "/usr/share/sonic/platform/platform.json" +DEFAULT_DPU_REBOOT_TIMEOUT = 360 def setup_function(): ModuleUpdater.log_notice = MagicMock() @@ -78,10 +85,162 @@ def test_moduleupdater_check_valid_fields(): if isinstance(fvs, list): fvs = dict(fvs[-1]) assert desc == fvs[CHASSIS_MODULE_INFO_DESC_FIELD] - assert slot == int(fvs[CHASSIS_MODULE_INFO_SLOT_FIELD]) assert status == fvs[CHASSIS_MODULE_INFO_OPERSTATUS_FIELD] assert serial == fvs[CHASSIS_MODULE_INFO_SERIAL_FIELD] +def test_smartswitch_moduleupdater_check_valid_fields(): + chassis = MockSmartSwitchChassis() + index = 0 + name = "DPU0" + desc = "DPU Module 0" + slot = 0 + serial = "DPU0-0000" + module_type = ModuleBase.MODULE_TYPE_DPU + module = MockModule(index, name, desc, module_type, slot, serial) + + # Set initial state + status = ModuleBase.MODULE_STATUS_ONLINE + module.set_oper_status(status) + + chassis.module_list.append(module) + + module_updater = SmartSwitchModuleUpdater(SYSLOG_IDENTIFIER, chassis) + module_updater.module_db_update() + fvs = module_updater.module_table.get(name) + if isinstance(fvs, list): + fvs = dict(fvs[-1]) + assert desc == fvs[CHASSIS_MODULE_INFO_DESC_FIELD] + assert NOT_AVAILABLE == fvs[CHASSIS_MODULE_INFO_SLOT_FIELD] + assert status == fvs[CHASSIS_MODULE_INFO_OPERSTATUS_FIELD] + assert serial == fvs[CHASSIS_MODULE_INFO_SERIAL_FIELD] + +def test_smartswitch_moduleupdater_status_transitions(): + # Mock the chassis and module + chassis = MockSmartSwitchChassis() + index = 0 + name = "DPU0" + desc = "DPU Module 0" + slot = 0 + serial = "DPU0-0000" + module_type = ModuleBase.MODULE_TYPE_DPU + module = MockModule(index, name, desc, module_type, slot, serial) + + # Add module to chassis and initialize with ONLINE status + initial_status_online = ModuleBase.MODULE_STATUS_ONLINE + module.set_oper_status(initial_status_online) + chassis.module_list.append(module) + + # Create the updater + module_updater = SmartSwitchModuleUpdater(SYSLOG_IDENTIFIER, chassis) + + # Mock dependent methods + with patch.object(module_updater, 'retrieve_dpu_reboot_time', return_value="2024-11-19T00:00:00") \ + as mock_retrieve_reboot_time, \ + patch.object(module_updater, '_is_first_boot', return_value=False) as mock_is_first_boot, \ + patch.object(module_updater, 'persist_dpu_reboot_cause') as mock_persist_reboot_cause, \ + patch.object(module_updater, 'update_dpu_reboot_cause_to_db') as mock_update_reboot_db, \ + patch("os.makedirs") as mock_makedirs, \ + patch("builtins.open", mock_open()) as mock_file, \ + patch.object(module_updater, '_get_history_path', + return_value="/tmp/prev_reboot_time.txt") as mock_get_history_path: + + # Transition from ONLINE to OFFLINE + offline_status = ModuleBase.MODULE_STATUS_OFFLINE + module.set_oper_status(offline_status) + module_updater.module_db_update() + assert module.get_oper_status() == offline_status + + # Reset mocks for next transition + mock_file.reset_mock() + mock_makedirs.reset_mock() + mock_persist_reboot_cause.reset_mock() + mock_update_reboot_db.reset_mock() + + # Ensure ONLINE transition is handled correctly + online_status = ModuleBase.MODULE_STATUS_ONLINE + module.set_oper_status(online_status) + module_updater.module_db_update() + assert module.get_oper_status() == online_status + + # Validate mock calls for ONLINE transition + mock_persist_reboot_cause.assert_called_once() + mock_update_reboot_db.assert_called_once() + +def test_smartswitch_moduleupdater_check_invalid_name(): + chassis = MockSmartSwitchChassis() + index = 0 + name = "TEST-CARD0" + desc = "36 port 400G card" + slot = 2 + serial = "TS1000101" + module_type = ModuleBase.MODULE_TYPE_DPU + module = MockModule(index, name, desc, module_type, slot, serial) + + # Set initial state + status = ModuleBase.MODULE_STATUS_PRESENT + module.set_oper_status(status) + + chassis.module_list.append(module) + + module_updater = SmartSwitchModuleUpdater(SYSLOG_IDENTIFIER, chassis) + module_updater.module_db_update() + fvs = module_updater.module_table.get(name) + assert fvs == None + + config_updater = SmartSwitchModuleConfigUpdater(SYSLOG_IDENTIFIER, chassis) + admin_state = 0 + config_updater.module_config_update(name, admin_state) + + # No change since invalid key + assert module.get_admin_state() != admin_state + +def test_smartswitch_moduleupdater_check_invalid_admin_state(): + chassis = MockSmartSwitchChassis() + index = 0 + name = "DPU0" + desc = "DPU Module 0" + slot = 0 + serial = "DPU0-0000" + module_type = ModuleBase.MODULE_TYPE_DPU + module = MockModule(index, name, desc, module_type, slot, serial) + + # Set initial state + status = ModuleBase.MODULE_STATUS_PRESENT + module.set_oper_status(status) + + chassis.module_list.append(module) + + module_updater = SmartSwitchModuleUpdater(SYSLOG_IDENTIFIER, chassis) + module_updater.module_db_update() + fvs = module_updater.module_table.get(name) + + config_updater = SmartSwitchModuleConfigUpdater(SYSLOG_IDENTIFIER, chassis) + admin_state = 2 + config_updater.module_config_update(name, admin_state) + + # No change since invalid key + assert module.get_admin_state() != admin_state + +def test_smartswitch_moduleupdater_check_invalid_slot(): + chassis = MockSmartSwitchChassis() + index = 0 + name = "DPU0" + desc = "DPU Module 0" + slot = -1 + serial = "TS1000101" + module_type = ModuleBase.MODULE_TYPE_DPU + module = MockModule(index, name, desc, module_type, slot, serial) + + # Set initial state + status = ModuleBase.MODULE_STATUS_PRESENT + module.set_oper_status(status) + + chassis.module_list.append(module) + + module_updater = SmartSwitchModuleUpdater(SYSLOG_IDENTIFIER, chassis) + module_updater.module_db_update() + fvs = module_updater.module_table.get(name) + assert fvs != None def test_moduleupdater_check_invalid_name(): chassis = MockChassis() @@ -105,6 +264,29 @@ def test_moduleupdater_check_invalid_name(): fvs = module_updater.module_table.get(name) assert fvs == None +def test_smartswitch_moduleupdater_check_invalid_index(): + chassis = MockSmartSwitchChassis() + index = -1 + name = "DPU0" + desc = "DPU Module 0" + slot = 0 + serial = "TS1000101" + module_type = ModuleBase.MODULE_TYPE_DPU + module = MockModule(index, name, desc, module_type, slot, serial) + + # Set initial state + status = ModuleBase.MODULE_STATUS_PRESENT + module.set_oper_status(status) + + chassis.module_list.append(module) + + module_updater = SmartSwitchModuleUpdater(SYSLOG_IDENTIFIER, chassis) + module_updater.module_db_update() + fvs = module_updater.module_table.get(name) + assert fvs != None + + # Run chassis db clean up + module_updater.module_down_chassis_db_cleanup() def test_moduleupdater_check_status_update(): chassis = MockChassis() @@ -179,6 +361,33 @@ def test_moduleupdater_check_deinit(): fvs = module_table.get(name) assert fvs == None +def test_smartswitch_moduleupdater_check_deinit(): + chassis = MockSmartSwitchChassis() + index = 0 + name = "DPU0" + desc = "DPU Module 0" + slot = 0 + serial = "DPU0-0000" + module_type = ModuleBase.MODULE_TYPE_DPU + module = MockModule(index, name, desc, module_type, slot, serial) + + # Set initial state + status = ModuleBase.MODULE_STATUS_ONLINE + module.set_oper_status(status) + chassis.module_list.append(module) + + module_updater = SmartSwitchModuleUpdater(SYSLOG_IDENTIFIER, chassis) + module_updater.modules_num_update() + module_updater.module_db_update() + fvs = module_updater.module_table.get(name) + # if isinstance(fvs, list): + # fvs = dict(fvs[-1]) + # assert status == fvs[CHASSIS_MODULE_INFO_OPERSTATUS_FIELD] + + module_table = module_updater.module_table + module_updater.deinit() + fvs = module_table.get(name) + assert fvs == None def test_configupdater_check_valid_names(): chassis = MockChassis() @@ -251,6 +460,148 @@ def test_configupdater_check_admin_state(): assert module.get_admin_state() == admin_state +def test_smartswitch_configupdater_check_admin_state(): + chassis = MockSmartSwitchChassis() + index = 0 + name = "DPU0" + desc = "DPU Module 0" + slot = 1 + serial = "DPU0-0000" + module_type = ModuleBase.MODULE_TYPE_DPU + module = MockModule(index, name, desc, module_type, slot, serial) + + # Set initial state + status = ModuleBase.MODULE_STATUS_ONLINE + module.set_oper_status(status) + chassis.module_list.append(module) + + config_updater = SmartSwitchModuleConfigUpdater(SYSLOG_IDENTIFIER, chassis) + admin_state = 0 + config_updater.module_config_update(name, admin_state) + assert module.get_admin_state() == admin_state + + admin_state = 1 + config_updater.module_config_update(name, admin_state) + assert module.get_admin_state() == admin_state + + + @patch("your_module.glob.glob") + @patch("your_module.open", new_callable=mock_open) + def test_update_dpu_reboot_cause_to_db(self, mock_open, mock_glob): + # Set up the SmartSwitchModuleUpdater and test inputs + module_updater = SmartSwitchModuleUpdater(SYSLOG_IDENTIFIER, chassis=MagicMock()) + module = "dpu0" + module_updater.chassis_state_db = MagicMock() + + # Case 1: No history files found + mock_glob.return_value = [] + with patch.object(module_updater, "log_warning") as mock_log_warning: + module_updater.update_dpu_reboot_cause_to_db(module) + mock_log_warning.assert_called_once_with(f"No reboot cause history files found for module: {module}") + + # Case 2: Valid JSON file with reboot cause + mock_glob.return_value = ["/host/reboot-cause/module/dpu0/history/file1.txt"] + mock_open().read.return_value = json.dumps({"name": "reboot_2024", "reason": "Power loss"}) + with patch.object(module_updater, "log_warning") as mock_log_warning: + module_updater.update_dpu_reboot_cause_to_db(module) + mock_log_warning.assert_not_called() # No warnings expected + module_updater.chassis_state_db.hset.assert_any_call("REBOOT_CAUSE|DPU0|reboot_2024", "name", "reboot_2024") + module_updater.chassis_state_db.hset.assert_any_call("REBOOT_CAUSE|DPU0|reboot_2024", "reason", "Power loss") + + # Case 3: Empty JSON object in file + mock_open().read.return_value = json.dumps({}) + with patch.object(module_updater, "log_warning") as mock_log_warning: + module_updater.update_dpu_reboot_cause_to_db(module) + mock_log_warning.assert_any_call(f"{module} reboot_cause_dict is empty") + + # Case 4: Invalid JSON in file + mock_open().read.side_effect = json.JSONDecodeError("Expecting value", "", 0) + with patch.object(module_updater, "log_warning") as mock_log_warning: + module_updater.update_dpu_reboot_cause_to_db(module) + mock_log_warning.assert_any_call("Failed to decode JSON from file: /host/reboot-cause/module/dpu0/history/file1.txt") + + # Case 5: General exception handling + mock_open.side_effect = IOError("Unable to read file") + with patch.object(module_updater, "log_warning") as mock_log_warning: + module_updater.update_dpu_reboot_cause_to_db(module) + mock_log_warning.assert_any_call("Error processing file /host/reboot-cause/module/dpu0/history/file1.txt: Unable to read file") + + +def test_smartswitch_module_db_update(): + chassis = MockSmartSwitchChassis() + reboot_cause = "Power loss" + key = "DPU0" + index = 0 + name = "DPU0" + desc = "DPU Module 0" + slot = 0 + serial = "DPU0-0000" + module_type = ModuleBase.MODULE_TYPE_DPU + module = MockModule(index, name, desc, module_type, slot, serial) + + # Set initial state + status = ModuleBase.MODULE_STATUS_ONLINE + module.set_oper_status(status) + chassis.module_list.append(module) + + module_updater = SmartSwitchModuleUpdater(SYSLOG_IDENTIFIER, chassis) + expected_path = "/host/reboot-cause/module/reboot_cause/dpu0/history/2024_11_13_15_06_40_reboot_cause.txt" + symlink_path = "/host/reboot-cause/module/dpu0/previous-reboot-cause.json" + + with patch("os.path.exists", return_value=True), \ + patch("os.makedirs") as mock_makedirs, \ + patch("builtins.open", mock_open(read_data="Power loss")) as mock_file, \ + patch("os.remove") as mock_remove, \ + patch("os.symlink") as mock_symlink: + + # Call the function to test + module_updater.persist_dpu_reboot_cause(reboot_cause, key) + module_updater._is_first_boot(name) + module_updater.persist_dpu_reboot_time(name) + module_updater.update_dpu_reboot_cause_to_db(name) + + +def test_platform_json_file_exists_and_valid(): + """Test case where the platform JSON file exists with valid data.""" + chassis = MockSmartSwitchChassis() + + # Define the custom mock_open function to handle specific file paths + def custom_mock_open(*args, **kwargs): + if args and args[0] == PLATFORM_JSON_FILE: + return mock_open(read_data='{"dpu_reboot_timeout": 360}')(*args, **kwargs) + return open(*args, **kwargs) # Call the real open for other files + + with patch("os.path.isfile", return_value=True), \ + patch("builtins.open", custom_mock_open): + + # Initialize the updater; it should read the mocked JSON data + updater = SmartSwitchModuleUpdater("SYSLOG", chassis) + + # Check that the extracted dpu_reboot_timeout value is as expected + assert updater.dpu_reboot_timeout == 360 + + +def test_platform_json_file_exists_fail_init(): + """Test case where the platform JSON file exists with valid data.""" + chassis = MockSmartSwitchChassis() + + # Define the custom mock_open function to handle specific file paths + def custom_mock_open(*args, **kwargs): + if args and args[0] == PLATFORM_JSON_FILE: + return mock_open(read_data='{"dpu_reboot_timeout": 360}')(*args, **kwargs) + return open(*args, **kwargs) # Call the real open for other files + + with patch("os.path.isfile", return_value=True), \ + patch("builtins.open", custom_mock_open): + + # Initialize the updater; it should read the mocked JSON data + updater = SmartSwitchModuleUpdater("SYSLOG", chassis) + updater.midplane_initialized = False + + # Check that the extracted dpu_reboot_timeout value is as expected + assert updater.dpu_reboot_timeout == 360 + + def test_configupdater_check_num_modules(): chassis = MockChassis() index = 0 @@ -362,14 +713,126 @@ def test_midplane_presence_modules(): fvs = midplane_table.get(name) assert fvs == None + +@patch('os.makedirs') +@patch('builtins.open', new_callable=mock_open) +def test_midplane_presence_dpu_modules(mock_open, mock_makedirs): + with tempfile.TemporaryDirectory() as temp_dir: + # Assume your method uses a path variable that you can set for testing + path = os.path.join(temp_dir, 'subdir') + + # Set up your mock or variable to use temp_dir + mock_makedirs.side_effect = lambda x, **kwargs: None # Prevent actual call + + chassis = MockSmartSwitchChassis() + + #DPU0 + index = 0 + name = "DPU0" + desc = "DPU Module 0" + slot = 0 + sup_slot = 0 + serial = "DPU0-0000" + module_type = ModuleBase.MODULE_TYPE_DPU + module = MockModule(index, name, desc, module_type, slot, serial) + module.set_midplane_ip() + module.prev_reboot_time = "2024_10_30_02_44_50" + chassis.module_list.append(module) + + #Run on supervisor + module_updater = SmartSwitchModuleUpdater(SYSLOG_IDENTIFIER, chassis) + module_updater.midplane_initialized = True + module_updater.modules_num_update() + module_updater.module_db_update() + module_updater.check_midplane_reachability() + + midplane_table = module_updater.midplane_table + #Check only one entry in database + assert 1 == midplane_table.size() + + #Check fields in database + fvs = midplane_table.get(name) + assert fvs != None + if isinstance(fvs, list): + fvs = dict(fvs[-1]) + assert module.get_midplane_ip() == fvs[CHASSIS_MIDPLANE_INFO_IP_FIELD] + assert str(module.is_midplane_reachable()) == fvs[CHASSIS_MIDPLANE_INFO_ACCESS_FIELD] + + #Set access of DPU0 to Up (midplane connectivity is down initially) + module.set_midplane_reachable(True) + module_updater.check_midplane_reachability() + fvs = midplane_table.get(name) + assert fvs != None + if isinstance(fvs, list): + fvs = dict(fvs[-1]) + assert module.get_midplane_ip() == fvs[CHASSIS_MIDPLANE_INFO_IP_FIELD] + assert str(module.is_midplane_reachable()) == fvs[CHASSIS_MIDPLANE_INFO_ACCESS_FIELD] + + #Set access of DPU0 to Down (to mock midplane connectivity state change) + module.set_midplane_reachable(False) + module_updater.check_midplane_reachability() + fvs = midplane_table.get(name) + assert fvs != None + if isinstance(fvs, list): + fvs = dict(fvs[-1]) + assert module.get_midplane_ip() == fvs[CHASSIS_MIDPLANE_INFO_IP_FIELD] + assert str(module.is_midplane_reachable()) == fvs[CHASSIS_MIDPLANE_INFO_ACCESS_FIELD] + + # Run chassis db clean up + module_updater.module_down_chassis_db_cleanup() + module_updater.chassis_state_db = None + module_updater.module_down_chassis_db_cleanup() + + #Deinit + module_updater.deinit() + fvs = midplane_table.get(name) + assert fvs == None + + +@patch('os.makedirs') +@patch('builtins.open', new_callable=mock_open) +def test_midplane_presence_uninitialized_dpu_modules(mock_open, mock_makedirs): + with tempfile.TemporaryDirectory() as temp_dir: + # Assume your method uses a path variable that you can set for testing + path = os.path.join(temp_dir, 'subdir') + + # Set up your mock or variable to use temp_dir + mock_makedirs.side_effect = lambda x, **kwargs: None # Prevent actual call + + chassis = MockSmartSwitchChassis() + + #DPU0 + index = 0 + name = "DPU0" + desc = "DPU Module 0" + slot = 0 + sup_slot = 0 + serial = "DPU0-0000" + module_type = ModuleBase.MODULE_TYPE_DPU + module = MockModule(index, name, desc, module_type, slot, serial) + module.set_midplane_ip() + module.prev_reboot_time = "2024_10_30_02_44_50" + chassis.module_list.append(module) + + #Run on supervisor + module_updater = SmartSwitchModuleUpdater(SYSLOG_IDENTIFIER, chassis) + module_updater.midplane_initialized = False + module_updater.modules_num_update() + module_updater.module_db_update() + module_updater.check_midplane_reachability() + + midplane_table = module_updater.midplane_table + #Check only one entry in database + assert 1 != midplane_table.size() + builtin_open = open # save the unpatched version -def mock_open(*args, **kwargs): - if args[0] == PLATFORM_ENV_CONF_FILE: +def lc_mock_open(*args, **kwargs): + if args and args[0] == PLATFORM_ENV_CONF_FILE: return mock.mock_open(read_data="dummy=1\nlinecard_reboot_timeout=240\n")(*args, **kwargs) # unpatched version for every other path return builtin_open(*args, **kwargs) -@patch("builtins.open", mock_open) +@patch("builtins.open", lc_mock_open) @patch('os.path.isfile', MagicMock(return_value=True)) def test_midplane_presence_modules_linecard_reboot(): chassis = MockChassis() @@ -749,6 +1212,58 @@ def test_signal_handler(): assert daemon_chassisd.stop.set.call_count == 0 assert exit_code == 0 +def test_daemon_run_smartswitch(): + # Test the chassisd run + chassis = MockSmartSwitchChassis() + + # DPU0 + index = 0 + name = "DPU0" + desc = "DPU Module 0" + slot = 0 + sup_slot = 0 + serial = "DPU0-0000" + module_type = ModuleBase.MODULE_TYPE_DPU + module = MockModule(index, name, desc, module_type, slot, serial) + module.set_midplane_ip() + # Set initial state + status = ModuleBase.MODULE_STATUS_PRESENT + module.set_oper_status(status) + chassis.module_list.append(module) + + # Supervisor ModuleUpdater + module_updater = SmartSwitchModuleUpdater(SYSLOG_IDENTIFIER, chassis) + module_updater.module_db_update() + module_updater.modules_num_update() + + daemon_chassisd = ChassisdDaemon(SYSLOG_IDENTIFIER, chassis) + daemon_chassisd.stop = MagicMock() + daemon_chassisd.stop.wait.return_value = True + daemon_chassisd.smartswitch = True + + import sonic_platform.platform + with patch.object(sonic_platform.platform.Chassis, 'is_smartswitch') as mock_is_smartswitch: + mock_is_smartswitch.return_value = True + + with patch.object(module_updater, 'num_modules', 1): + daemon_chassisd.run() + + +def test_daemon_run_supervisor_invalid_slot(): + chassis = MockChassis() + #Supervisor + index = 0 + sup_slot = -1 + # Supervisor ModuleUpdater + module_updater = ModuleUpdater(SYSLOG_IDENTIFIER, chassis, sup_slot, sup_slot) + + daemon_chassisd = ChassisdDaemon(SYSLOG_IDENTIFIER, chassis) + daemon_chassisd.stop = MagicMock() + daemon_chassisd.stop.wait.return_value = True + module_updater.my_slot = ModuleBase.MODULE_INVALID_SLOT + module_updater.supervisor_slot = ModuleBase.MODULE_INVALID_SLOT + daemon_chassisd.run() + def test_daemon_run_supervisor(): # Test the chassisd run chassis = MockChassis() @@ -763,6 +1278,27 @@ def test_daemon_run_supervisor(): daemon_chassisd.stop.wait.return_value = True daemon_chassisd.run() +def import_mock_swsscommon(): + return importlib.import_module('tests.mock_swsscommon') + +def test_task_worker_loop(): + # Create a mock for the Select object + mock_select = MagicMock() + + # Set up the mock to raise a KeyboardInterrupt after the first call + mock_select.select.side_effect = [(mock_select.TIMEOUT, None), KeyboardInterrupt] + + # Patch the swsscommon.Select to use this mock + with patch('tests.mock_swsscommon.Select', return_value=mock_select): + config_manager = SmartSwitchConfigManagerTask() + + config_manager.config_updater = MagicMock() + + try: + config_manager.task_worker() + except KeyboardInterrupt: + pass # Handle the KeyboardInterrupt as expected + def test_daemon_run_linecard(): # Test the chassisd run chassis = MockChassis() @@ -930,4 +1466,4 @@ def test_chassis_db_bootup_with_empty_slot(): fvs = dict(fvs[-1]) assert status == fvs[CHASSIS_MODULE_INFO_OPERSTATUS_FIELD] assert down_module_lc1_key in sup_module_updater.down_modules.keys() - +