diff --git a/sonic-syseepromd/pytest.ini b/sonic-syseepromd/pytest.ini new file mode 100644 index 000000000000..d90ee9ed9e12 --- /dev/null +++ b/sonic-syseepromd/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +addopts = --cov=scripts --cov-report html --cov-report term --cov-report xml --junitxml=test-results.xml -vv diff --git a/sonic-syseepromd/scripts/syseepromd b/sonic-syseepromd/scripts/syseepromd index 8300f576f3c2..3ccf9a07727b 100644 --- a/sonic-syseepromd/scripts/syseepromd +++ b/sonic-syseepromd/scripts/syseepromd @@ -8,83 +8,100 @@ With this daemon, show syseeprom CLI will be able to get data from state DB instead of access hw or cache. ''' -try: - import signal - import sys - import threading +import signal +import sys +import threading - from sonic_py_common import daemon_base - from swsscommon import swsscommon -except ImportError as e: - raise ImportError(str(e) + " - required module not found") +from sonic_py_common import daemon_base +from swsscommon import swsscommon + + +# TODO: Once we no longer support Python 2, we can eliminate this and get the +# name using the 'name' field (e.g., `signal.SIGINT.name`) starting with Python 3.5 +SIGNALS_TO_NAMES_DICT = dict((getattr(signal, n), n) + for n in dir(signal) if n.startswith('SIG') and '_' not in n) PLATFORM_SPECIFIC_MODULE_NAME = 'eeprom' PLATFORM_SPECIFIC_CLASS_NAME = 'board' EEPROM_INFO_UPDATE_PERIOD_SECS = 60 -POST_EEPROM_SUCCESS = 0 +ERR_NONE = 0 ERR_PLATFORM_NOT_SUPPORT = 1 ERR_FAILED_EEPROM = 2 ERR_FAILED_UPDATE_DB = 3 ERR_INVALID_PARAMETER = 4 -ERR_EEPROMUTIL_LOAD = 5 +ERR_EEPROM_LOAD = 5 EEPROM_TABLE_NAME = 'EEPROM_INFO' + SYSLOG_IDENTIFIER = 'syseepromd' +exit_code = 0 + class DaemonSyseeprom(daemon_base.DaemonBase): - def __init__(self, log_identifier): - super(DaemonSyseeprom, self).__init__(log_identifier) + def __init__(self): + super(DaemonSyseeprom, self).__init__(SYSLOG_IDENTIFIER) + + # Set minimum logging level to INFO + self.set_min_log_priority_info() self.stop_event = threading.Event() self.eeprom = None + self.eeprom_tbl = None - state_db = daemon_base.db_connect("STATE_DB") - self.eeprom_tbl = swsscommon.Table(state_db, EEPROM_TABLE_NAME) - self.eepromtbl_keys = [] + # First, try to load the new platform API + try: + import sonic_platform + self.eeprom = sonic_platform.platform.Platform().get_chassis().get_eeprom() + except Exception as e: + self.log_warning( + "Failed to load platform-specific eeprom from sonic_platform package due to {}. Trying deprecated plugin method ...".format(repr(e))) - def _wrapper_read_eeprom(self): - if self.eeprom is not None: + # If we didn't successfully load the class from the sonic_platform package, try loading the old plugin try: - return self.eeprom.read_eeprom() - except (NotImplementedError, IOError): - pass + self.eeprom = self.load_platform_util(PLATFORM_SPECIFIC_MODULE_NAME, PLATFORM_SPECIFIC_CLASS_NAME) + except Exception as e: + self.log_error("Failed to load platform-specific eeprom from deprecated plugin: {}".format(repr(e))) - try: - return self.eeprom.read_eeprom() - except IOError: - pass + if not self.eeprom: + sys.exit(ERR_EEPROM_LOAD) - def _wrapper_update_eeprom_db(self, eeprom): - if self.eeprom is not None: - try: - return self.eeprom.update_eeprom_db(eeprom) - except NotImplementedError: - pass + # Connect to STATE_DB + state_db = daemon_base.db_connect("STATE_DB") + self.eeprom_tbl = swsscommon.Table(state_db, EEPROM_TABLE_NAME) + self.eepromtbl_keys = [] - return self.eeprom.update_eeprom_db(eeprom) + # Post system EEPROM info to state DB once at start-up + rc = self.post_eeprom_to_db() + if rc != ERR_NONE: + self.log_error("Failed to post system EEPROM info to database") + + def __del__(self): + # Delete all the information from DB + self.clear_db() def post_eeprom_to_db(self): - eeprom = self._wrapper_read_eeprom() - if eeprom is None: - self.log_error("Failed to read eeprom") + eeprom_data = self.eeprom.read_eeprom() + if eeprom_data is None: + self.log_error("Failed to read EEPROM") return ERR_FAILED_EEPROM - err = self._wrapper_update_eeprom_db(eeprom) + err = self.eeprom.update_eeprom_db(eeprom_data) if err: - self.log_error("Failed to update eeprom info to database") + self.log_error("Failed to update EEPROM info in database") return ERR_FAILED_UPDATE_DB self.eepromtbl_keys = self.eeprom_tbl.getKeys() - return POST_EEPROM_SUCCESS + return ERR_NONE def clear_db(self): - keys = self.eeprom_tbl.getKeys() - for key in keys: - self.eeprom_tbl._del(key) + if self.eeprom_tbl: + keys = self.eeprom_tbl.getKeys() + for key in keys: + self.eeprom_tbl._del(key) def detect_eeprom_table_integrity(self): keys = self.eeprom_tbl.getKeys() @@ -98,65 +115,37 @@ class DaemonSyseeprom(daemon_base.DaemonBase): return True - # Signal handler + # Override signal handler from DaemonBase 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_event.set() - elif sig == signal.SIGTERM: - self.log_info("Caught SIGTERM - exiting...") + FATAL_SIGNALS = [signal.SIGINT, signal.SIGTERM] + NONFATAL_SIGNALS = [signal.SIGHUP] + + global exit_code + + if sig in FATAL_SIGNALS: + self.log_info("Caught signal '{}' - exiting...".format(SIGNALS_TO_NAMES_DICT[sig])) + exit_code = 128 + sig # Make sure we exit with a non-zero code so that supervisor will try to restart us self.stop_event.set() + elif sig in NONFATAL_SIGNALS: + self.log_info("Caught signal '{}' - ignoring...".format(SIGNALS_TO_NAMES_DICT[sig])) else: - self.log_warning("Caught unhandled signal '" + sig + "'") + self.log_warning("Caught unhandled signal '{}' - ignoring...".format(SIGNALS_TO_NAMES_DICT[sig])) - # Run daemon + # Main daemon logic def run(self): - self.log_info("Starting up...") - - # First, try to load the new platform API - try: - import sonic_platform - self.chassis = sonic_platform.platform.Platform().get_chassis() - self.eeprom = self.chassis.get_eeprom() - except Exception as e: - self.log_warning("Failed to load data from eeprom using sonic_platform package due to {}, retrying using deprecated plugin method".format(repr(e))) - - # If we didn't successfully load the class from the sonic_platform package, try loading the old plugin - if not self.eeprom: - try: - self.eeprom = self.load_platform_util(PLATFORM_SPECIFIC_MODULE_NAME, PLATFORM_SPECIFIC_CLASS_NAME) - except Exception as e: - self.log_error("Failed to load platform-specific eeprom implementation: {}".format(repr(e))) - - if not self.eeprom: - sys.exit(ERR_EEPROMUTIL_LOAD) - - # Connect to STATE_DB and post syseeprom info to state DB - rc = self.post_eeprom_to_db() - if rc != POST_EEPROM_SUCCESS: - self.log_error("Failed to post eeprom to database") - - # Start main loop - self.log_info("Start daemon main loop") - - while not self.stop_event.wait(EEPROM_INFO_UPDATE_PERIOD_SECS): - rc = self.detect_eeprom_table_integrity() - if not rc: - self.log_info("sys eeprom table was changed, need update") - self.clear_db() - rcs = self.post_eeprom_to_db() - if rcs != POST_EEPROM_SUCCESS: - self.log_error("Failed to post eeprom to database") - continue - - self.log_info("Stop daemon main loop") + if self.stop_event.wait(EEPROM_INFO_UPDATE_PERIOD_SECS): + # We received a fatal signal + return False - # Delete all the information from DB and then exit - self.clear_db() + rc = self.detect_eeprom_table_integrity() + if not rc: + self.log_info("System EEPROM table was changed, needs update") + self.clear_db() + rcs = self.post_eeprom_to_db() + if rcs != ERR_NONE: + self.log_error("Failed to post EEPROM to database") - self.log_info("Shutting down...") + return True # # Main ========================================================================= @@ -164,9 +153,17 @@ class DaemonSyseeprom(daemon_base.DaemonBase): def main(): - syseepromd = DaemonSyseeprom(SYSLOG_IDENTIFIER) - syseepromd.run() + syseepromd = DaemonSyseeprom() + + syseepromd.log_info("Starting up...") + + while syseepromd.run(): + pass + + syseepromd.log_info("Shutting down...") + + return exit_code if __name__ == '__main__': - main() + sys.exit(main()) diff --git a/sonic-syseepromd/setup.cfg b/sonic-syseepromd/setup.cfg new file mode 100644 index 000000000000..b7e478982ccf --- /dev/null +++ b/sonic-syseepromd/setup.cfg @@ -0,0 +1,2 @@ +[aliases] +test=pytest diff --git a/sonic-syseepromd/setup.py b/sonic-syseepromd/setup.py index 1f485b2fbca4..4232e3d88d6d 100644 --- a/sonic-syseepromd/setup.py +++ b/sonic-syseepromd/setup.py @@ -16,6 +16,12 @@ setup_requires=[ 'wheel' ], + tests_require=[ + 'mock>=2.0.0; python_version < "3.3"', + 'pytest', + 'pytest-cov', + 'sonic_platform_common' + ], classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: No Input/Output (Daemon)', diff --git a/sonic-syseepromd/tests/__init__.py b/sonic-syseepromd/tests/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/sonic-syseepromd/tests/mocked_libs/sonic_platform/__init__.py b/sonic-syseepromd/tests/mocked_libs/sonic_platform/__init__.py new file mode 100644 index 000000000000..e491d5b52166 --- /dev/null +++ b/sonic-syseepromd/tests/mocked_libs/sonic_platform/__init__.py @@ -0,0 +1,6 @@ +""" + Mock implementation of sonic_platform package for unit testing +""" + +from . import chassis +from . import platform diff --git a/sonic-syseepromd/tests/mocked_libs/sonic_platform/chassis.py b/sonic-syseepromd/tests/mocked_libs/sonic_platform/chassis.py new file mode 100644 index 000000000000..918e01243b4f --- /dev/null +++ b/sonic-syseepromd/tests/mocked_libs/sonic_platform/chassis.py @@ -0,0 +1,21 @@ +""" + Mock implementation of sonic_platform package for unit testing +""" + +# TODO: Clean this up once we no longer need to support Python 2 +import sys +if sys.version_info.major == 3: + from unittest import mock +else: + import mock + +from sonic_platform_base.chassis_base import ChassisBase + + +class Chassis(ChassisBase): + def __init__(self): + ChassisBase.__init__(self) + self.eeprom = mock.MagicMock() + + def get_eeprom(self): + return self.eeprom diff --git a/sonic-syseepromd/tests/mocked_libs/sonic_platform/platform.py b/sonic-syseepromd/tests/mocked_libs/sonic_platform/platform.py new file mode 100644 index 000000000000..a1b61e13e5a6 --- /dev/null +++ b/sonic-syseepromd/tests/mocked_libs/sonic_platform/platform.py @@ -0,0 +1,12 @@ +""" + Mock implementation of sonic_platform package for unit testing +""" + +from sonic_platform_base.platform_base import PlatformBase +from sonic_platform.chassis import Chassis + + +class Platform(PlatformBase): + def __init__(self): + PlatformBase.__init__(self) + self._chassis = Chassis() diff --git a/sonic-syseepromd/tests/mocked_libs/swsscommon/__init__.py b/sonic-syseepromd/tests/mocked_libs/swsscommon/__init__.py new file mode 100644 index 000000000000..012af621e5f0 --- /dev/null +++ b/sonic-syseepromd/tests/mocked_libs/swsscommon/__init__.py @@ -0,0 +1,5 @@ +''' + Mock implementation of swsscommon package for unit testing +''' + +from . import swsscommon diff --git a/sonic-syseepromd/tests/mocked_libs/swsscommon/swsscommon.py b/sonic-syseepromd/tests/mocked_libs/swsscommon/swsscommon.py new file mode 100644 index 000000000000..8a0a87692eb4 --- /dev/null +++ b/sonic-syseepromd/tests/mocked_libs/swsscommon/swsscommon.py @@ -0,0 +1,51 @@ +''' + Mock implementation of swsscommon package for unit testing +''' + +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: + fv_dict = {} + + def __init__(self, tuple_list): + if isinstance(tuple_list, list) and isinstance(tuple_list[0], tuple): + self.fv_dict = dict(tuple_list) + + def __setitem__(self, key, kv_tuple): + self.fv_dict[kv_tuple[0]] = kv_tuple[1] + + def __getitem__(self, key): + return self.fv_dict[key] + + def __eq__(self, other): + if not isinstance(other, FieldValuePairs): + # don't attempt to compare against unrelated types + return NotImplemented + + return self.fv_dict == other.fv_dict + + def __repr__(self): + return repr(self.fv_dict) + + def __str__(self): + return repr(self.fv_dict) diff --git a/sonic-syseepromd/tests/test_syseepromd.py b/sonic-syseepromd/tests/test_syseepromd.py new file mode 100644 index 000000000000..e25b94ce3f6c --- /dev/null +++ b/sonic-syseepromd/tests/test_syseepromd.py @@ -0,0 +1,221 @@ +import os +import sys +from imp import load_source # Replace with importlib once we no longer need to support Python 2 + +import pytest + +# TODO: Clean this up once we no longer need to support Python 2 +if sys.version_info.major == 3: + from unittest import mock +else: + import mock +from sonic_py_common import daemon_base + +SYSLOG_IDENTIFIER = 'syseepromd_test' +NOT_AVAILABLE = 'N/A' + +daemon_base.db_connect = mock.MagicMock() + +tests_path = os.path.dirname(os.path.abspath(__file__)) + +# Add mocked_libs path so that the file under test can load mocked modules from there +mocked_libs_path = os.path.join(tests_path, 'mocked_libs') +sys.path.insert(0, mocked_libs_path) + +# Add path to the file under test so that we can load it +modules_path = os.path.dirname(tests_path) +scripts_path = os.path.join(modules_path, 'scripts') +sys.path.insert(0, modules_path) + +load_source('syseepromd', os.path.join(scripts_path, 'syseepromd')) +import syseepromd + + +def test_post_eeprom_to_db_eeprom_read_fail(): + daemon_syseepromd = syseepromd.DaemonSyseeprom() + daemon_syseepromd.eeprom.read_eeprom = mock.MagicMock(return_value=None) + daemon_syseepromd.eeprom_tbl = mock.MagicMock() + daemon_syseepromd.log_error = mock.MagicMock() + + ret = daemon_syseepromd.post_eeprom_to_db() + assert ret == syseepromd.ERR_FAILED_EEPROM + assert daemon_syseepromd.log_error.call_count == 1 + daemon_syseepromd.log_error.assert_called_with('Failed to read EEPROM') + assert daemon_syseepromd.eeprom_tbl.getKeys.call_count == 0 + + +def test_post_eeprom_to_db_update_fail(): + daemon_syseepromd = syseepromd.DaemonSyseeprom() + daemon_syseepromd.eeprom.update_eeprom_db = mock.MagicMock(return_value=1) + daemon_syseepromd.eeprom_tbl = mock.MagicMock() + daemon_syseepromd.log_error = mock.MagicMock() + + ret = daemon_syseepromd.post_eeprom_to_db() + assert ret == syseepromd.ERR_FAILED_UPDATE_DB + assert daemon_syseepromd.log_error.call_count == 1 + daemon_syseepromd.log_error.assert_called_with('Failed to update EEPROM info in database') + assert daemon_syseepromd.eeprom_tbl.getKeys.call_count == 0 + + +def test_post_eeprom_to_db_ok(): + daemon_syseepromd = syseepromd.DaemonSyseeprom() + daemon_syseepromd.eeprom.update_eeprom_db = mock.MagicMock(return_value=0) + daemon_syseepromd.eeprom_tbl = mock.MagicMock() + daemon_syseepromd.log_error = mock.MagicMock() + + ret = daemon_syseepromd.post_eeprom_to_db() + assert ret == syseepromd.ERR_NONE + assert daemon_syseepromd.log_error.call_count == 0 + assert daemon_syseepromd.eeprom_tbl.getKeys.call_count == 1 + + +def test_clear_db(): + daemon_syseepromd = syseepromd.DaemonSyseeprom() + daemon_syseepromd.eeprom_tbl.getKeys = mock.MagicMock(return_value=['key1', 'key2']) + daemon_syseepromd.eeprom_tbl._del = mock.MagicMock() + + daemon_syseepromd.clear_db() + assert daemon_syseepromd.eeprom_tbl.getKeys.call_count == 1 + assert daemon_syseepromd.eeprom_tbl._del.call_count == 2 + + +def test_detect_eeprom_table_integrity(): + daemon_syseepromd = syseepromd.DaemonSyseeprom() + + # Test entries as expected + daemon_syseepromd.eeprom_tbl.getKeys = mock.MagicMock(return_value=['key1', 'key2']) + daemon_syseepromd.eepromtbl_keys = ['key1', 'key2'] + ret = daemon_syseepromd.detect_eeprom_table_integrity() + assert ret == True + + # Test differing amounts of entries + daemon_syseepromd.eeprom_tbl.getKeys = mock.MagicMock(return_value=['key1', 'key2']) + daemon_syseepromd.eepromtbl_keys = ['key1'] + ret = daemon_syseepromd.detect_eeprom_table_integrity() + assert ret == False + + # Test same amount of entries, but with different keys + daemon_syseepromd.eeprom_tbl.getKeys = mock.MagicMock(return_value=['key1', 'key2']) + daemon_syseepromd.eepromtbl_keys = ['key1', 'key3'] + ret = daemon_syseepromd.detect_eeprom_table_integrity() + assert ret == False + + +def test_signal_handler(): + daemon_syseepromd = syseepromd.DaemonSyseeprom() + daemon_syseepromd.stop_event.set = mock.MagicMock() + daemon_syseepromd.log_info = mock.MagicMock() + daemon_syseepromd.log_warning = mock.MagicMock() + + # Test SIGHUP + daemon_syseepromd.signal_handler(syseepromd.signal.SIGHUP, None) + assert daemon_syseepromd.log_info.call_count == 1 + daemon_syseepromd.log_info.assert_called_with("Caught signal 'SIGHUP' - ignoring...") + assert daemon_syseepromd.log_warning.call_count == 0 + assert daemon_syseepromd.stop_event.set.call_count == 0 + assert syseepromd.exit_code == 0 + + # Reset + daemon_syseepromd.log_info.reset_mock() + daemon_syseepromd.log_warning.reset_mock() + daemon_syseepromd.stop_event.set.reset_mock() + + # Test SIGINT + test_signal = syseepromd.signal.SIGINT + daemon_syseepromd.signal_handler(test_signal, None) + assert daemon_syseepromd.log_info.call_count == 1 + daemon_syseepromd.log_info.assert_called_with("Caught signal 'SIGINT' - exiting...") + assert daemon_syseepromd.log_warning.call_count == 0 + assert daemon_syseepromd.stop_event.set.call_count == 1 + assert syseepromd.exit_code == (128 + test_signal) + + # Reset + daemon_syseepromd.log_info.reset_mock() + daemon_syseepromd.log_warning.reset_mock() + daemon_syseepromd.stop_event.set.reset_mock() + + # Test SIGTERM + test_signal = syseepromd.signal.SIGTERM + daemon_syseepromd.signal_handler(test_signal, None) + assert daemon_syseepromd.log_info.call_count == 1 + daemon_syseepromd.log_info.assert_called_with("Caught signal 'SIGTERM' - exiting...") + assert daemon_syseepromd.log_warning.call_count == 0 + assert daemon_syseepromd.stop_event.set.call_count == 1 + assert syseepromd.exit_code == (128 + test_signal) + + # Reset + daemon_syseepromd.log_info.reset_mock() + daemon_syseepromd.log_warning.reset_mock() + daemon_syseepromd.stop_event.set.reset_mock() + syseepromd.exit_code = 0 + + # Test an unhandled signal + daemon_syseepromd.signal_handler(syseepromd.signal.SIGUSR1, None) + assert daemon_syseepromd.log_warning.call_count == 1 + daemon_syseepromd.log_warning.assert_called_with("Caught unhandled signal 'SIGUSR1' - ignoring...") + assert daemon_syseepromd.log_info.call_count == 0 + assert daemon_syseepromd.stop_event.set.call_count == 0 + assert syseepromd.exit_code == 0 + + +@mock.patch('syseepromd.EEPROM_INFO_UPDATE_PERIOD_SECS', 1) +def test_run(): + daemon_syseepromd = syseepromd.DaemonSyseeprom() + daemon_syseepromd.clear_db = mock.MagicMock() + daemon_syseepromd.log_info = mock.MagicMock() + daemon_syseepromd.log_error = mock.MagicMock() + daemon_syseepromd.post_eeprom_to_db = mock.MagicMock(return_value=syseepromd.ERR_NONE) + + # Test no change to EEPROM data + daemon_syseepromd.detect_eeprom_table_integrity = mock.MagicMock(return_value=True) + + ret = daemon_syseepromd.run() + assert ret == True + assert daemon_syseepromd.detect_eeprom_table_integrity.call_count == 1 + assert daemon_syseepromd.log_info.call_count == 0 + assert daemon_syseepromd.log_error.call_count == 0 + assert daemon_syseepromd.clear_db.call_count == 0 + assert daemon_syseepromd.post_eeprom_to_db.call_count == 0 + + # Reset mocks + daemon_syseepromd.detect_eeprom_table_integrity.reset_mock() + + # Test EEPROM data has changed, update succeeds + daemon_syseepromd.detect_eeprom_table_integrity = mock.MagicMock(return_value=False) + + ret = daemon_syseepromd.run() + assert ret == True + assert daemon_syseepromd.detect_eeprom_table_integrity.call_count == 1 + assert daemon_syseepromd.log_info.call_count == 1 + daemon_syseepromd.log_info.assert_called_with('System EEPROM table was changed, needs update') + assert daemon_syseepromd.clear_db.call_count == 1 + assert daemon_syseepromd.post_eeprom_to_db.call_count == 1 + assert daemon_syseepromd.log_error.call_count == 0 + + # Reset mocks + daemon_syseepromd.detect_eeprom_table_integrity.reset_mock() + daemon_syseepromd.log_info.reset_mock() + daemon_syseepromd.clear_db.reset_mock() + daemon_syseepromd.post_eeprom_to_db.reset_mock() + + # Test EEPROM data has changed, update fails + daemon_syseepromd.detect_eeprom_table_integrity = mock.MagicMock(return_value=False) + daemon_syseepromd.post_eeprom_to_db = mock.MagicMock(return_value=syseepromd.ERR_FAILED_UPDATE_DB) + + ret = daemon_syseepromd.run() + assert ret == True + assert daemon_syseepromd.detect_eeprom_table_integrity.call_count == 1 + assert daemon_syseepromd.log_info.call_count == 1 + daemon_syseepromd.log_info.assert_called_with('System EEPROM table was changed, needs update') + assert daemon_syseepromd.clear_db.call_count == 1 + assert daemon_syseepromd.post_eeprom_to_db.call_count == 1 + assert daemon_syseepromd.log_error.call_count == 1 + daemon_syseepromd.log_error.assert_called_with('Failed to post EEPROM to database') + + +@mock.patch('syseepromd.DaemonSyseeprom.run') +def test_main(mock_run): + mock_run.return_value = False + + syseepromd.main() + assert mock_run.call_count == 1