diff --git a/.gitignore b/.gitignore index 1c2875d68..2d9c861eb 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,9 @@ # Compiled code which doesn't end in '.pyc' sonic-thermalctld/scripts/thermalctldc +sonic-chassisd/scripts/chassisdc + +# Unit test / coverage reports +coverage.xml +.coverage +htmlcov/ diff --git a/sonic-chassisd/pytest.ini b/sonic-chassisd/pytest.ini new file mode 100644 index 000000000..83b74d373 --- /dev/null +++ b/sonic-chassisd/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +addopts = --cov=scripts --cov-report html --cov-report term --cov-report xml diff --git a/sonic-chassisd/scripts/chassisd b/sonic-chassisd/scripts/chassisd new file mode 100644 index 000000000..d064f7e07 --- /dev/null +++ b/sonic-chassisd/scripts/chassisd @@ -0,0 +1,334 @@ +#!/usr/bin/env python3 + +""" + chassisd + Module information update daemon for SONiC + This daemon will loop to collect all modules related information and then write the information to state DB. + The loop interval is CHASSIS_INFO_UPDATE_PERIOD_SECS in seconds. +""" + +try: + import signal + import sys + import threading + + from sonic_py_common import daemon_base, logger + from sonic_py_common.task_base import ProcessTaskBase +except ImportError as e: + raise ImportError (str(e) + " - required module not found") + +try: + from swsscommon import swsscommon +except ImportError as e: + from tests import mock_swsscommon as swsscommon + +try: + from sonic_platform_base.module_base import ModuleBase +except ImportError as e: + from tests.mock_module_base import ModuleBase + +# +# Constants ==================================================================== +# + +SYSLOG_IDENTIFIER = "chassisd" + +CHASSIS_CFG_TABLE = 'CHASSIS_MODULE' + +CHASSIS_INFO_TABLE = 'CHASSIS_TABLE' +CHASSIS_INFO_KEY_TEMPLATE = 'CHASSIS {}' +CHASSIS_INFO_CARD_NUM_FIELD = 'module_num' + +CHASSIS_MODULE_INFO_TABLE = 'CHASSIS_MODULE_TABLE' +CHASSIS_MODULE_INFO_KEY_TEMPLATE = 'CHASSIS_MODULE {}' +CHASSIS_MODULE_INFO_NAME_FIELD = 'name' +CHASSIS_MODULE_INFO_DESC_FIELD = 'desc' +CHASSIS_MODULE_INFO_SLOT_FIELD = 'slot' +CHASSIS_MODULE_INFO_OPERSTATUS_FIELD = 'oper_status' + +CHASSIS_INFO_UPDATE_PERIOD_SECS = 10 + +CHASSIS_LOAD_ERROR = 1 +CHASSIS_NOT_SUPPORTED = 2 + +platform_chassis = None + +SELECT_TIMEOUT = 1000 + +NOT_AVAILABLE = 'N/A' +INVALID_SLOT = ModuleBase.MODULE_INVALID_SLOT +INVALID_MODULE_INDEX = -1 + +MODULE_ADMIN_DOWN = 0 +MODULE_ADMIN_UP = 1 + +# +# Helper functions ============================================================= +# + +# try get information from platform API and return a default value if caught NotImplementedError +def try_get(callback, *args, **kwargs): + """ + Handy function to invoke the callback and catch NotImplementedError + :param callback: Callback to be invoked + :param default: Default return value if exception occur + :return: Default return value if exception occur else return value of the callback + """ + default = kwargs.get('default', NOT_AVAILABLE) + try: + ret = callback(*args) + if ret is None: + ret = default + except NotImplementedError: + ret = default + + return ret + +# +# Module Config Updater ======================================================== +# +class ModuleConfigUpdater(logger.Logger): + + def __init__(self, log_identifier, chassis): + """ + Constructor for ModuleConfigUpdater + :param chassis: Object representing a platform chassis + """ + super(ModuleConfigUpdater, self).__init__(log_identifier) + + self.chassis = chassis + + def deinit(self): + """ + Destructor of ModuleConfigUpdater + :return: + """ + + def module_config_update(self, key, admin_state): + if not key.startswith(ModuleBase.MODULE_TYPE_SUPERVISOR) and \ + not key.startswith(ModuleBase.MODULE_TYPE_LINE) and \ + not key.startswith(ModuleBase.MODULE_TYPE_FABRIC): + self.log_error("Incorrect module-name {}. Should start with {} or {} or {}".format(key, + ModuleBase.MODULE_TYPE_SUPERVISOR, ModuleBase.MODULE_TYPE_LINE, + ModuleBase.MODULE_TYPE_FABRIC)) + 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): + # Setting the module to administratively up/down state + 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) + +# +# Module Updater ============================================================== +# +class ModuleUpdater(logger.Logger): + + 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 = 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.info_dict_keys = [CHASSIS_MODULE_INFO_NAME_FIELD, + CHASSIS_MODULE_INFO_DESC_FIELD, + CHASSIS_MODULE_INFO_SLOT_FIELD, + CHASSIS_MODULE_INFO_OPERSTATUS_FIELD] + + 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.chassis_table is not None: + self.chassis_table._del(CHASSIS_INFO_KEY_TEMPLATE.format(1)) + + def modules_num_update(self): + # Check if module list is populated + num_modules = self.chassis.get_num_modules() + if num_modules == 0: + self.log_error("Chassisd has no modules available") + return + + # Post number-of-modules info to STATE_DB + fvs = swsscommon.FieldValuePairs([(CHASSIS_INFO_CARD_NUM_FIELD, str(num_modules))]) + self.chassis_table.set(CHASSIS_INFO_KEY_TEMPLATE.format(1), fvs) + + 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_SUPERVISOR) and \ + not key.startswith(ModuleBase.MODULE_TYPE_LINE) and \ + not key.startswith(ModuleBase.MODULE_TYPE_FABRIC): + self.log_error("Incorrect module-name {}. Should start with {} or {} or {}".format(key, + ModuleBase.MODULE_TYPE_SUPERVISOR, ModuleBase.MODULE_TYPE_LINE, + ModuleBase.MODULE_TYPE_FABRIC)) + 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])]) + self.module_table.set(key, fvs) + + 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) + slot = try_get(self.chassis.get_module(module_index).get_slot, default=INVALID_SLOT) + status = try_get(self.chassis.get_module(module_index).get_oper_status, default=ModuleBase.MODULE_STATUS_OFFLINE) + + 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] = str(slot) + module_info_dict[CHASSIS_MODULE_INFO_OPERSTATUS_FIELD] = str(status) + + return module_info_dict + +# +# Config Manager task ======================================================== +# +class ConfigManagerTask(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 = ModuleConfigUpdater(SYSLOG_IDENTIFIER, platform_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': + admin_state = MODULE_ADMIN_DOWN + elif op == 'DEL': + admin_state = MODULE_ADMIN_UP + else: + continue + + self.config_updater.module_config_update(key, admin_state) + +# +# Daemon ======================================================================= +# + +class ChassisdDaemon(daemon_base.DaemonBase): + def __init__(self, log_identifier): + super(ChassisdDaemon, self).__init__(log_identifier) + + self.stop = threading.Event() + + # Signal handler + def signal_handler(self, sig, frame): + if sig == signal.SIGHUP: + self.log_info("Caught SIGHUP - ignoring...") + elif sig == signal.SIGINT: + self.log_info("Caught SIGINT - exiting...") + self.stop.set() + elif sig == signal.SIGTERM: + self.log_info("Caught SIGTERM - exiting...") + self.stop.set() + else: + self.log_warning("Caught unhandled signal '" + sig + "'") + + # Run daemon + def run(self): + global platform_chassis + + self.log_info("Starting up...") + + # Load new platform api class + try: + import sonic_platform.platform + platform_chassis = sonic_platform.platform.Platform().get_chassis() + except Exception as e: + self.log_error("Failed to load chassis due to {}".format(repr(e))) + sys.exit(CHASSIS_LOAD_ERROR) + + # Check if module list is populated + self.module_updater = ModuleUpdater(SYSLOG_IDENTIFIER, platform_chassis) + self.module_updater.modules_num_update() + + # Check for valid slot numbers + my_slot = try_get(platform_chassis.get_my_slot, default=INVALID_SLOT) + supervisor_slot = try_get(platform_chassis.get_supervisor_slot, default=INVALID_SLOT) + if (my_slot == INVALID_SLOT) or (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 supervisor_slot == my_slot: + config_manager = ConfigManagerTask() + config_manager.task_run() + + # Start main loop + self.log_info("Start daemon main loop") + + while not self.stop.wait(CHASSIS_INFO_UPDATE_PERIOD_SECS): + self.module_updater.module_db_update() + + self.log_info("Stop daemon main loop") + + if config_manager is not None: + config_manager.task_stop() + + # Delete all the information from DB and then exit + self.module_updater.deinit() + + self.log_info("Shutting down...") + +# +# Main ========================================================================= +# + +def main(): + chassisd = ChassisdDaemon(SYSLOG_IDENTIFIER) + chassisd.run() + +if __name__ == '__main__': + main() diff --git a/sonic-chassisd/setup.cfg b/sonic-chassisd/setup.cfg new file mode 100644 index 000000000..b7e478982 --- /dev/null +++ b/sonic-chassisd/setup.cfg @@ -0,0 +1,2 @@ +[aliases] +test=pytest diff --git a/sonic-chassisd/setup.py b/sonic-chassisd/setup.py new file mode 100644 index 000000000..fa415e597 --- /dev/null +++ b/sonic-chassisd/setup.py @@ -0,0 +1,42 @@ +from setuptools import setup + +setup( + name='sonic-chassisd', + version='1.0', + description='Chassis daemon for SONiC', + license='Apache 2.0', + author='SONiC Team', + author_email='linuxnetdev@microsoft.com', + url='https://github.com/Azure/sonic-platform-daemons', + maintainer='Manju Prabhu', + maintainer_email='manjunath.prabhu@nokia.com', + packages=[ + 'tests' + ], + scripts=[ + 'scripts/chassisd', + ], + setup_requires= [ + 'pytest-runner', + 'wheel' + ], + tests_require = [ + 'pytest', + 'mock>=2.0.0', + 'pytest-cov' + ], + classifiers=[ + 'Development Status :: 4 - Beta', + 'Environment :: No Input/Output (Daemon)', + 'Intended Audience :: Developers', + 'Intended Audience :: Information Technology', + 'Intended Audience :: System Administrators', + 'License :: OSI Approved :: Apache Software License', + 'Natural Language :: English', + 'Operating System :: POSIX :: Linux', + 'Programming Language :: Python :: 2.7', + 'Topic :: System :: Hardware', + ], + keywords='sonic SONiC chassis Chassis daemon chassisd', + test_suite='setup.get_test_suite' +) diff --git a/sonic-chassisd/tests/__init__.py b/sonic-chassisd/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/sonic-chassisd/tests/mock_module_base.py b/sonic-chassisd/tests/mock_module_base.py new file mode 100644 index 000000000..f49222980 --- /dev/null +++ b/sonic-chassisd/tests/mock_module_base.py @@ -0,0 +1,21 @@ +class ModuleBase(): + # Invalid slot for modular chassis + MODULE_INVALID_SLOT = -1 + + # Possible card types for modular chassis + MODULE_TYPE_SUPERVISOR = "SUPERVISOR" + MODULE_TYPE_LINE = "LINE-CARD" + MODULE_TYPE_FABRIC = "FABRIC-CARD" + + # Possible card status for modular chassis + # Module state is Empty if no module is inserted in the slot + MODULE_STATUS_EMPTY = "Empty" + # Module state if Offline if powered down. This is also the admin-down state. + MODULE_STATUS_OFFLINE = "Offline" + # Module state is Present when it is powered up, but not fully functional. + MODULE_STATUS_PRESENT = "Present" + # Module state is Present when it is powered up, but entered a fault state. + # Module is not able to go Online. + MODULE_STATUS_FAULT = "Fault" + # Module state is Online when fully operational + MODULE_STATUS_ONLINE = "Online" diff --git a/sonic-chassisd/tests/mock_platform.py b/sonic-chassisd/tests/mock_platform.py new file mode 100644 index 000000000..a8350a2cd --- /dev/null +++ b/sonic-chassisd/tests/mock_platform.py @@ -0,0 +1,73 @@ +class MockDevice: + def __init__(self): + self.name = None + self.presence = True + self.model = 'Module Model' + self.serial = 'Module Serial' + + def get_name(self): + return self.name + + def get_presence(self): + return self.presence + + def get_model(self): + return self.model + + def get_serial(self): + return self.serial + + +class MockModule(MockDevice): + def __init__(self, module_index, module_name, module_desc, module_type, module_slot): + self.module_index = module_index + self.module_name = module_name + self.module_desc = module_desc + self.module_type = module_type + self.hw_slot = module_slot + self.module_status = '' + self.admin_state = 1 + + def get_name(self): + return self.module_name + + def get_description(self): + return self.module_desc + + def get_type(self): + return self.module_type + + def get_slot(self): + return self.hw_slot + + def get_oper_status(self): + return self.module_status + + def set_oper_status(self, status): + self.module_status = status + + def set_admin_state(self, up): + self.admin_state = up + + def get_admin_state(self): + return self.admin_state + +class MockChassis: + def __init__(self): + self.module_list = [] + + 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 diff --git a/sonic-chassisd/tests/mock_swsscommon.py b/sonic-chassisd/tests/mock_swsscommon.py new file mode 100644 index 000000000..df8c052d5 --- /dev/null +++ b/sonic-chassisd/tests/mock_swsscommon.py @@ -0,0 +1,25 @@ +STATE_DB = '' + + +class Table: + def __init__(self, db, table_name): + self.table_name = table_name + self.mock_dict = {} + + def _del(self, key): + del self.mock_dict[key] + pass + + def set(self, key, fvs): + self.mock_dict[key] = fvs.fv_dict + pass + + def get(self, key): + if key in self.mock_dict: + return self.mock_dict[key] + return None + +class FieldValuePairs: + def __init__(self, fvs): + self.fv_dict = dict(fvs) + pass diff --git a/sonic-chassisd/tests/test_chassisd.py b/sonic-chassisd/tests/test_chassisd.py new file mode 100644 index 000000000..6684ea932 --- /dev/null +++ b/sonic-chassisd/tests/test_chassisd.py @@ -0,0 +1,231 @@ +import os +import sys + +from mock import Mock, MagicMock, patch +from sonic_py_common import daemon_base + +from .mock_platform import MockChassis, MockModule +from .mock_module_base import ModuleBase + +SYSLOG_IDENTIFIER = 'chassisd_test' +NOT_AVAILABLE = 'N/A' + +daemon_base.db_connect = MagicMock() + +test_path = 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) + +from imp import load_source + +load_source('chassisd', scripts_path + '/chassisd') +from chassisd import * + +CHASSIS_MODULE_INFO_NAME_FIELD = 'name' +CHASSIS_MODULE_INFO_DESC_FIELD = 'desc' +CHASSIS_MODULE_INFO_SLOT_FIELD = 'slot' +CHASSIS_MODULE_INFO_OPERSTATUS_FIELD = 'oper_status' + +CHASSIS_INFO_KEY_TEMPLATE = 'CHASSIS {}' +CHASSIS_INFO_CARD_NUM_FIELD = 'module_num' + +def setup_function(): + ModuleUpdater.log_notice = MagicMock() + ModuleUpdater.log_warning = MagicMock() + + +def teardown_function(): + ModuleUpdater.log_notice.reset() + ModuleUpdater.log_warning.reset() + + +def test_moduleupdater_check_valid_fields(): + chassis = MockChassis() + index = 0 + name = "FABRIC-CARD0" + desc = "Switch Fabric Module" + slot = 10 + module_type = ModuleBase.MODULE_TYPE_FABRIC + module = MockModule(index, name, desc, module_type, slot) + + # Set initial state + status = ModuleBase.MODULE_STATUS_ONLINE + module.set_oper_status(status) + + chassis.module_list.append(module) + + module_updater = ModuleUpdater(SYSLOG_IDENTIFIER, chassis) + module_updater.module_db_update() + fvs = module_updater.module_table.get(name) + 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] + +def test_moduleupdater_check_invalid_name(): + chassis = MockChassis() + index = 0 + name = "TEST-CARD0" + desc = "36 port 400G card" + slot = 2 + module_type = ModuleBase.MODULE_TYPE_LINE + module = MockModule(index, name, desc, module_type, slot) + + # Set initial state + status = ModuleBase.MODULE_STATUS_PRESENT + module.set_oper_status(status) + + chassis.module_list.append(module) + + module_updater = ModuleUpdater(SYSLOG_IDENTIFIER, chassis) + module_updater.module_db_update() + fvs = module_updater.module_table.get(name) + assert fvs == None + +def test_moduleupdater_check_status_update(): + chassis = MockChassis() + index = 0 + name = "LINE-CARD0" + desc = "36 port 400G card" + slot = 1 + module_type = ModuleBase.MODULE_TYPE_LINE + module = MockModule(index, name, desc, module_type, slot) + + # Set initial state + status = ModuleBase.MODULE_STATUS_ONLINE + module.set_oper_status(status) + chassis.module_list.append(module) + + module_updater = ModuleUpdater(SYSLOG_IDENTIFIER, chassis) + module_updater.module_db_update() + fvs = module_updater.module_table.get(name) + print('Initial DB-entry {}'.format(fvs)) + assert status == fvs[CHASSIS_MODULE_INFO_OPERSTATUS_FIELD] + + # Update status + status = ModuleBase.MODULE_STATUS_OFFLINE + module.set_oper_status(status) + fvs = module_updater.module_table.get(name) + print('Not updated DB-entry {}'.format(fvs)) + assert status != fvs[CHASSIS_MODULE_INFO_OPERSTATUS_FIELD] + + # Update status and db + module_updater.module_db_update() + fvs = module_updater.module_table.get(name) + print('Updated DB-entry {}'.format(fvs)) + assert status == fvs[CHASSIS_MODULE_INFO_OPERSTATUS_FIELD] + +def test_moduleupdater_check_deinit(): + chassis = MockChassis() + index = 0 + name = "LINE-CARD0" + desc = "36 port 400G card" + slot = 1 + module_type = ModuleBase.MODULE_TYPE_LINE + module = MockModule(index, name, desc, module_type, slot) + + # Set initial state + status = ModuleBase.MODULE_STATUS_ONLINE + module.set_oper_status(status) + chassis.module_list.append(module) + + module_updater = ModuleUpdater(SYSLOG_IDENTIFIER, chassis) + module_updater.modules_num_update() + module_updater.module_db_update() + fvs = module_updater.module_table.get(name) + 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() + index = 0 + name = "TEST-CARD0" + desc = "36 port 400G card" + slot = 1 + module_type = ModuleBase.MODULE_TYPE_LINE + module = MockModule(index, name, desc, module_type, slot) + + # Set initial state + status = ModuleBase.MODULE_STATUS_ONLINE + module.set_oper_status(status) + chassis.module_list.append(module) + + config_updater = ModuleConfigUpdater(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_configupdater_check_valid_index(): + chassis = MockChassis() + index = -1 + name = "LINE-CARD0" + desc = "36 port 400G card" + slot = 1 + module_type = ModuleBase.MODULE_TYPE_LINE + module = MockModule(index, name, desc, module_type, slot) + + # Set initial state + status = ModuleBase.MODULE_STATUS_ONLINE + module.set_oper_status(status) + chassis.module_list.append(module) + + config_updater = ModuleConfigUpdater(SYSLOG_IDENTIFIER, chassis) + admin_state = 0 + config_updater.module_config_update(name, admin_state) + + # No change since invalid index + assert module.get_admin_state() != admin_state + +def test_configupdater_check_admin_state(): + chassis = MockChassis() + index = 0 + name = "LINE-CARD0" + desc = "36 port 400G card" + slot = 1 + module_type = ModuleBase.MODULE_TYPE_LINE + module = MockModule(index, name, desc, module_type, slot) + + # Set initial state + status = ModuleBase.MODULE_STATUS_ONLINE + module.set_oper_status(status) + chassis.module_list.append(module) + + config_updater = ModuleConfigUpdater(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 + +def test_configupdater_check_num_modules(): + chassis = MockChassis() + index = 0 + name = "LINE-CARD0" + desc = "36 port 400G card" + slot = 1 + module_type = ModuleBase.MODULE_TYPE_LINE + module = MockModule(index, name, desc, module_type, slot) + + # No modules + module_updater = ModuleUpdater(SYSLOG_IDENTIFIER, chassis) + module_updater.modules_num_update() + fvs = module_updater.chassis_table.get(CHASSIS_INFO_KEY_TEMPLATE.format(1)) + assert fvs == None + + # Add a module + chassis.module_list.append(module) + module_updater.modules_num_update() + fvs = module_updater.chassis_table.get(CHASSIS_INFO_KEY_TEMPLATE.format(1)) + assert chassis.get_num_modules() == int(fvs[CHASSIS_INFO_CARD_NUM_FIELD]) + + module_updater.deinit() + fvs = module_updater.chassis_table.get(CHASSIS_INFO_KEY_TEMPLATE.format(1)) + assert fvs == None