From fc455a7d01f8df1ed6a55960056facdf1b3b0b3c Mon Sep 17 00:00:00 2001 From: Junchao-Mellanox <57339448+Junchao-Mellanox@users.noreply.github.com> Date: Sat, 15 Feb 2020 02:38:43 +0800 Subject: [PATCH] Add thermal control daemon to monitor FAN and thermal status and run thermal policy (#49) --- .gitignore | 3 + sonic-thermalctld/pytest.ini | 3 + sonic-thermalctld/scripts/thermalctld | 570 ++++++++++++++++++++ sonic-thermalctld/setup.cfg | 2 + sonic-thermalctld/setup.py | 40 ++ sonic-thermalctld/tests/__init__.py | 0 sonic-thermalctld/tests/mock_platform.py | 161 ++++++ sonic-thermalctld/tests/mock_swsscommon.py | 17 + sonic-thermalctld/tests/test_thermalctld.py | 200 +++++++ 9 files changed, 996 insertions(+) create mode 100644 sonic-thermalctld/pytest.ini create mode 100644 sonic-thermalctld/scripts/thermalctld create mode 100644 sonic-thermalctld/setup.cfg create mode 100644 sonic-thermalctld/setup.py create mode 100644 sonic-thermalctld/tests/__init__.py create mode 100644 sonic-thermalctld/tests/mock_platform.py create mode 100644 sonic-thermalctld/tests/mock_swsscommon.py create mode 100644 sonic-thermalctld/tests/test_thermalctld.py diff --git a/.gitignore b/.gitignore index d759bc20dada..b24e94ab4097 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ */dist/ */*.tar.gz */*.egg-info +*/.cache/ +*.pyc +*/__pycache__/ diff --git a/sonic-thermalctld/pytest.ini b/sonic-thermalctld/pytest.ini new file mode 100644 index 000000000000..c24fe5bb9e65 --- /dev/null +++ b/sonic-thermalctld/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +filterwarnings = + ignore::DeprecationWarning diff --git a/sonic-thermalctld/scripts/thermalctld b/sonic-thermalctld/scripts/thermalctld new file mode 100644 index 000000000000..1e26b49f1a4e --- /dev/null +++ b/sonic-thermalctld/scripts/thermalctld @@ -0,0 +1,570 @@ +#!/usr/bin/env python2 + +""" + thermalctld + Thermal control daemon for SONiC +""" + +try: + import os + import sys + import time + import signal + import threading + from datetime import datetime + from sonic_daemon_base import daemon_base + from sonic_daemon_base.daemon_base import Logger + from sonic_daemon_base.daemon_base import DaemonBase + from sonic_daemon_base.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 + +SYSLOG_IDENTIFIER = 'thermalctld' +NOT_AVAILABLE = 'N/A' +logger = Logger(SYSLOG_IDENTIFIER) + + +# utility functions + +# try get information from platform API and return a default value if caught NotImplementedError +def try_get(callback, default=NOT_AVAILABLE): + """ + 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 + """ + try: + ret = callback() + except NotImplementedError: + ret = default + + return ret + + +def log_on_status_changed(normal_status, normal_log, abnormal_log): + """ + Log when any status changed + :param normal_status: Expected status. + :param normal_log: Log string for expected status. + :param abnormal_log: Log string for unexpected status + :return: + """ + if normal_status: + logger.log_notice(normal_log) + else: + logger.log_warning(abnormal_log) + + +class FanStatus(object): + def __init__(self): + """ + Constructor of FanStatus + """ + self.presence = True + self.under_speed = False + self.over_speed = False + self.invalid_direction = False + + def set_presence(self, presence): + """ + Set and cache Fan presence status + :param presence: Fan presence status + :return: True if status changed else False + """ + if presence == self.presence: + return False + + self.presence = presence + return True + + def _check_speed_value_available(self, speed, target_speed, tolerance, current_status): + if speed == NOT_AVAILABLE or target_speed == NOT_AVAILABLE or tolerance == NOT_AVAILABLE: + if tolerance > 100 or tolerance < 0: + logger.log_warning('Invalid tolerance value: {}'.format(tolerance)) + return False + + if current_status is True: + logger.log_warning('Fan speed or target_speed or tolerance become unavailable, ' + 'speed={}, target_speed={}, tolerance={}'.format(speed, target_speed, tolerance)) + return False + return True + + def set_under_speed(self, speed, target_speed, tolerance): + """ + Set and cache Fan under speed status + :param speed: Fan speed + :param target_speed: Fan target speed + :param tolerance: Threshold between Fan speed and target speed + :return: True if status changed else False + """ + if not self._check_speed_value_available(speed, target_speed, tolerance, self.under_speed): + old_status = self.under_speed + self.under_speed = False + return old_status != self.under_speed + + status = speed < target_speed * (1 - float(tolerance) / 100) + if status == self.under_speed: + return False + + self.under_speed = status + return True + + def set_over_speed(self, speed, target_speed, tolerance): + """ + Set and cache Fan over speed status + :param speed: Fan speed + :param target_speed: Fan target speed + :param tolerance: Threshold between Fan speed and target speed + :return: True if status changed else False + """ + if not self._check_speed_value_available(speed, target_speed, tolerance, self.over_speed): + old_status = self.over_speed + self.over_speed = False + return old_status != self.over_speed + + status = speed > target_speed * (1 + float(tolerance) / 100) + if status == self.over_speed: + return False + + self.over_speed = status + return True + + def is_ok(self): + """ + Indicate the Fan works as expect + :return: True if Fan works normal else False + """ + return self.presence and not self.under_speed and not self.over_speed and not self.invalid_direction + + +# +# FanUpdater =================================================================== +# +class FanUpdater(object): + # Fan information table name in database + FAN_INFO_TABLE_NAME = 'FAN_INFO' + + def __init__(self, chassis): + """ + Constructor for FanUpdater + :param chassis: Object representing a platform chassis + """ + self.chassis = chassis + self.fan_status_dict = {} + state_db = daemon_base.db_connect(swsscommon.STATE_DB) + self.table = swsscommon.Table(state_db, FanUpdater.FAN_INFO_TABLE_NAME) + + def deinit(self): + """ + Destructor of FanUpdater + :return: + """ + for name in self.fan_status_dict.keys(): + self.table._del(name) + + def update(self): + """ + Update all Fan information to database + :return: + """ + logger.log_debug("Start fan updating") + try: + for index, fan in enumerate(self.chassis.get_all_fans()): + self._refresh_fan_status(fan, index) + + for psu_index, psu in enumerate(self.chassis.get_all_psus()): + psu_name = try_get(psu.get_name, 'PSU {}'.format(psu_index)) + for fan_index, fan in enumerate(psu.get_all_fans()): + self._refresh_fan_status(fan, fan_index, '{} FAN'.format(psu_name)) + except Exception as e: + logger.log_warning('Failed to update FAN status - {}'.format(e)) + + logger.log_debug("End fan updating") + + def _refresh_fan_status(self, fan, index, name_prefix='FAN'): + """ + Get Fan status by platform API and write to database for a given Fan + :param fan: Object representing a platform Fan + :param index: Index of the Fan object in the platform + :param name_prefix: name prefix of Fan object if Fan.get_name not presented + :return: + """ + fan_name = try_get(fan.get_name, '{} {}'.format(name_prefix, index + 1)) + if fan_name not in self.fan_status_dict: + self.fan_status_dict[fan_name] = FanStatus() + + fan_status = self.fan_status_dict[fan_name] + + presence = try_get(fan.get_presence, False) + speed = try_get(fan.get_speed) + speed_tolerance = try_get(fan.get_speed_tolerance) + speed_target = try_get(fan.get_target_speed) + + set_led = False + if fan_status.set_presence(presence): + set_led = True + log_on_status_changed(fan_status.presence, + 'Fan removed warning cleared: {} was inserted.'.format(fan_name), + 'Fan removed warning: {} was removed from ' + 'the system, potential overheat hazard'.format(fan_name) + ) + + if fan_status.set_under_speed(speed, speed_target, speed_tolerance): + set_led = True + log_on_status_changed(not fan_status.under_speed, + 'Fan under speed warning cleared: {} speed back to normal.'.format(fan_name), + 'Fan under speed warning: {} current speed={}, target speed={}, tolerance={}.'. + format(fan_name, speed, speed_target, speed_tolerance) + ) + + if fan_status.set_over_speed(speed, speed_target, speed_tolerance): + set_led = True + log_on_status_changed(not fan_status.over_speed, + 'Fan over speed warning cleared: {} speed back to normal.'.format(fan_name), + 'Fan over speed warning: {} target speed={}, current speed={}, tolerance={}.'. + format(fan_name, speed_target, speed, speed_tolerance) + ) + + # TODO: handle invalid fan direction + + if set_led: + self._set_fan_led(fan, fan_name, fan_status) + + fvs = swsscommon.FieldValuePairs( + [('presence', str(presence)), + ('model', str(try_get(fan.get_model))), + ('serial', str(try_get(fan.get_serial))), + ('status', str(try_get(fan.get_status, False))), + ('direction', str(try_get(fan.get_direction))), + ('speed', str(speed)), + ('speed_tolerance', str(speed_tolerance)), + ('speed_target', str(speed_target)), + ('led_status', str(try_get(fan.get_status_led))), + ('timestamp', datetime.now().strftime('%Y%m%d %H:%M:%S')) + ]) + + self.table.set(fan_name, fvs) + + def _set_fan_led(self, fan, fan_name, fan_status): + """ + Set fan led according to current status + :param fan: Object representing a platform Fan + :param fan_name: Name of the Fan object in case any vendor not implement Fan.get_name + :param fan_status: Object representing the FanStatus + :return: + """ + try: + if fan_status.is_ok(): + fan.set_status_led(fan.STATUS_LED_COLOR_GREEN) + else: + # TODO: wait for Kebo to define the mapping of fan status to led color, + # just set it to red so far + fan.set_status_led(fan.STATUS_LED_COLOR_RED) + except NotImplementedError as e: + logger.log_warning('Failed to set led to fan, set_status_led not implemented') + + +class TemperatureStatus(object): + TEMPERATURE_DIFF_THRESHOLD = 10 + + def __init__(self): + self.temperature = None + self.over_temperature = False + self.under_temperature = False + + def set_temperature(self, name, temperature): + """ + Record temperature changes, if it changed too fast, raise a warning. + :param name: Name of the thermal. + :param temperature: New temperature value. + :return: + """ + if temperature == NOT_AVAILABLE: + if self.temperature is not None: + logger.log_warning('Temperature of {} become unavailable'.format(name)) + self.temperature = None + return + + if self.temperature is None: + self.temperature = temperature + else: + diff = abs(temperature - self.temperature) + if diff > TemperatureStatus.TEMPERATURE_DIFF_THRESHOLD: + logger.log_warning( + 'Temperature of {} changed too fast, from {} to {}, please check your hardware'.format( + name, self.temperature, temperature)) + self.temperature = temperature + + def _check_temperature_value_available(self, temperature, threshold, current_status): + if temperature == NOT_AVAILABLE or threshold == NOT_AVAILABLE: + if current_status is True: + logger.log_warning('Thermal temperature or threshold become unavailable, ' + 'temperature={}, threshold={}'.format(temperature, threshold)) + return False + return True + + def set_over_temperature(self, temperature, threshold): + """ + Set over temperature status + :param temperature: Temperature + :param threshold: High threshold + :return: True if over temperature status changed else False + """ + if not self._check_temperature_value_available(temperature, threshold, self.over_temperature): + old_status = self.over_temperature + self.over_temperature = False + return old_status != self.over_temperature + + status = temperature > threshold + if status == self.over_temperature: + return False + + self.over_temperature = status + return True + + def set_under_temperature(self, temperature, threshold): + """ + Set over temperature status + :param temperature: Temperature + :param threshold: Low threshold + :return: True if under temperature status changed else False + """ + if not self._check_temperature_value_available(temperature, threshold, self.under_temperature): + old_status = self.under_temperature + self.under_temperature = False + return old_status != self.under_temperature + + status = temperature < threshold + if status == self.under_temperature: + return False + + self.under_temperature = status + return True + + +# +# TemperatureUpdater ====================================================================== +# +class TemperatureUpdater(object): + # Temperature information table name in database + TEMPER_INFO_TABLE_NAME = 'TEMPERATURE_INFO' + + def __init__(self, chassis): + """ + Constructor of TemperatureUpdater + :param chassis: Object representing a platform chassis + """ + self.chassis = chassis + self.temperature_status_dict = {} + state_db = daemon_base.db_connect(swsscommon.STATE_DB) + self.table = swsscommon.Table(state_db, TemperatureUpdater.TEMPER_INFO_TABLE_NAME) + + def deinit(self): + """ + Destructor of TemperatureUpdater + :return: + """ + for name in self.temperature_status_dict.keys(): + self.table._del(name) + + def update(self): + """ + Update all temperature information to database + :return: + """ + logger.log_debug("Start temperature updating") + try: + for index, thermal in enumerate(self.chassis.get_all_thermals()): + self._refresh_temperature_status(thermal, index) + except Exception as e: + logger.log_warning('Failed to update thermal status - {}'.format(e)) + + logger.log_debug("End temperature updating") + + def _refresh_temperature_status(self, thermal, index): + """ + Get temperature status by platform API and write to database + :param thermal: Object representing a platform thermal zone + :param index: Index of the thermal object in platform chassis + :return: + """ + name = try_get(thermal.get_name, 'Thermal {}'.format(index + 1)) + if name not in self.temperature_status_dict: + self.temperature_status_dict[name] = TemperatureStatus() + + temperature_status = self.temperature_status_dict[name] + + temperature = try_get(thermal.get_temperature) + temperature_status.set_temperature(name, temperature) + high_threshold = try_get(thermal.get_high_threshold) + low_threshold = try_get(thermal.get_low_threshold) + + warning = False + if temperature_status.set_over_temperature(temperature, high_threshold): + log_on_status_changed(not temperature_status.over_temperature, + 'High temperature warning: {} current temperature {}C, high threshold {}C'. + format(name, temperature, high_threshold), + 'High temperature warning cleared: {} temperature restore to {}C, high threshold {}C.'. + format(name, temperature, high_threshold) + ) + warning = warning | temperature_status.over_temperature + + if temperature_status.set_under_temperature(temperature, low_threshold): + log_on_status_changed(not temperature_status.under_temperature, + 'Low temperature warning: {} current temperature {}C, low threshold {}C'. + format(name, temperature, low_threshold), + 'Low temperature warning cleared: {} temperature restore to {}C, low threshold {}C.'. + format(name, temperature, low_threshold) + ) + warning = warning | temperature_status.under_temperature + + fvs = swsscommon.FieldValuePairs( + [('temperature', str(temperature)), + ('high_threshold', str(high_threshold)), + ('low_threshold', str(low_threshold)), + ('warning_status', str(warning)), + ('critical_high_threshold', str(try_get(thermal.get_high_critical_threshold))), + ('critical_low_threshold', str(try_get(thermal.get_low_critical_threshold))), + ('timestamp', datetime.now().strftime('%Y%m%d %H:%M:%S')) + ]) + + self.table.set(name, fvs) + + +class ThermalMonitor(ProcessTaskBase): + # Initial update interval + INITIAL_INTERVAL = 5 + # Update interval value + UPDATE_INTERVAL = 60 + # Update elapse threshold. If update used time is larger than the value, generate a warning log. + UPDATE_ELAPSE_THRESHOLD = 30 + + def __init__(self, chassis): + """ + Constructor for ThermalMonitor + :param chassis: Object representing a platform chassis + """ + ProcessTaskBase.__init__(self) + self.fan_updater = FanUpdater(chassis) + self.temperature_updater = TemperatureUpdater(chassis) + + def task_worker(self): + """ + Thread function to handle Fan status update and temperature status update + :return: + """ + logger.log_info("Start thermal monitoring loop") + + # Start loop to update fan, temperature info in DB periodically + wait_time = ThermalMonitor.INITIAL_INTERVAL + while not self.task_stopping_event.wait(wait_time): + begin = time.time() + self.fan_updater.update() + self.temperature_updater.update() + elapse = time.time() - begin + if elapse < ThermalMonitor.UPDATE_INTERVAL: + wait_time = ThermalMonitor.UPDATE_INTERVAL - elapse + else: + wait_time = ThermalMonitor.INITIAL_INTERVAL + + if elapse > ThermalMonitor.UPDATE_ELAPSE_THRESHOLD: + logger.log_warning('Update fan and temperature status takes {} seconds, ' + 'there might be performance risk'.format(elapse)) + + self.fan_updater.deinit() + self.temperature_updater.deinit() + + logger.log_info("Stop thermal monitoring loop") + + +# +# Daemon ======================================================================= +# +class ThermalControlDaemon(DaemonBase): + # Interval to run thermal control logic + INTERVAL = 60 + POLICY_FILE = '/usr/share/sonic/platform/thermal_policy.json' + + def __init__(self): + """ + Constructor of ThermalControlDaemon + """ + DaemonBase.__init__(self) + self.stop_event = threading.Event() + + # Signal handler + def signal_handler(self, sig, frame): + """ + Signal handler + :param sig: Signal number + :param frame: not used + :return: + """ + if sig == signal.SIGHUP: + logger.log_info("Caught SIGHUP - ignoring...") + elif sig == signal.SIGINT: + logger.log_info("Caught SIGINT - exiting...") + self.stop_event.set() + elif sig == signal.SIGTERM: + logger.log_info("Caught SIGTERM - exiting...") + self.stop_event.set() + else: + logger.log_warning("Caught unhandled signal '" + sig + "'") + + def run(self): + """ + Run main logical of this daemon + :return: + """ + logger.log_info("Starting up...") + + import sonic_platform.platform + chassis = sonic_platform.platform.Platform().get_chassis() + + thermal_monitor = ThermalMonitor(chassis) + thermal_monitor.task_run() + + thermal_manager = None + try: + thermal_manager = chassis.get_thermal_manager() + if thermal_manager: + thermal_manager.initialize() + thermal_manager.load(ThermalControlDaemon.POLICY_FILE) + thermal_manager.init_thermal_algorithm(chassis) + except Exception as e: + logger.log_error('Caught exception while initializing thermal manager - {}'.format(e)) + + while not self.stop_event.wait(ThermalControlDaemon.INTERVAL): + try: + if thermal_manager: + thermal_manager.run_policy(chassis) + except Exception as e: + logger.log_error('Caught exception while running thermal policy - {}'.format(e)) + + try: + if thermal_manager: + thermal_manager.deinitialize() + except Exception as e: + logger.log_error('Caught exception while destroy thermal manager - {}'.format(e)) + + thermal_monitor.task_stop() + + logger.log_info("Shutdown...") + + +# +# Main ========================================================================= +# +def main(): + thermal_control = ThermalControlDaemon() + thermal_control.run() + + +if __name__ == '__main__': + main() diff --git a/sonic-thermalctld/setup.cfg b/sonic-thermalctld/setup.cfg new file mode 100644 index 000000000000..b7e478982ccf --- /dev/null +++ b/sonic-thermalctld/setup.cfg @@ -0,0 +1,2 @@ +[aliases] +test=pytest diff --git a/sonic-thermalctld/setup.py b/sonic-thermalctld/setup.py new file mode 100644 index 000000000000..b2f9a4333085 --- /dev/null +++ b/sonic-thermalctld/setup.py @@ -0,0 +1,40 @@ +from setuptools import setup + +setup( + name='sonic-thermalctld', + version='1.0', + description='Thermal control daemon for SONiC', + license='Apache 2.0', + author='SONiC Team', + author_email='linuxnetdev@microsoft.com', + url='https://github.com/Azure/sonic-platform-daemons', + maintainer='Junchao Chen', + maintainer_email='junchao@mellanox.com', + packages=[ + 'tests' + ], + scripts=[ + 'scripts/thermalctld', + ], + setup_requires= [ + 'pytest-runner' + ], + tests_require = [ + 'pytest', + 'mock>=2.0.0' + ], + 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 THERMALCONTROL thermalcontrol THERMALCTL thermalctl thermalctld', + test_suite='setup.get_test_suite' +) diff --git a/sonic-thermalctld/tests/__init__.py b/sonic-thermalctld/tests/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/sonic-thermalctld/tests/mock_platform.py b/sonic-thermalctld/tests/mock_platform.py new file mode 100644 index 000000000000..ddbaf404af54 --- /dev/null +++ b/sonic-thermalctld/tests/mock_platform.py @@ -0,0 +1,161 @@ +class MockDevice: + def __init__(self): + self.name = None + self.presence = True + self.model = 'FAN Model' + self.serial = 'Fan 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 MockFan(MockDevice): + STATUS_LED_COLOR_RED = 'red' + STATUS_LED_COLOR_GREEN = 'green' + + def __init__(self): + MockDevice.__init__(self) + self.speed = 20 + self.speed_tolerance = 20 + self.target_speed = 20 + self.status = True + self.direction = 'intake' + self.led_status = 'red' + + def get_speed(self): + return self.speed + + def get_speed_tolerance(self): + return self.speed_tolerance + + def get_target_speed(self): + return self.target_speed + + def get_status(self): + return self.status + + def get_direction(self): + return self.direction + + def get_status_led(self): + return self.led_status + + def set_status_led(self, value): + self.led_status = value + + def make_under_speed(self): + self.speed = 1 + self.target_speed = 2 + self.speed_tolerance = 0 + + def make_over_speed(self): + self.speed = 2 + self.target_speed = 1 + self.speed_tolerance = 0 + + def make_normal_speed(self): + self.speed = 1 + self.target_speed = 1 + self.speed_tolerance = 0 + + +class MockPsu(MockDevice): + def __init__(self): + self.fan_list = [] + + def get_all_fans(self): + return self.fan_list + + +class MockThermal: + def __init__(self): + self.name = None + self.temperature = 2 + self.high_threshold = 3 + self.low_threshold = 1 + self.high_critical_threshold = 4 + self.low_critical_threshold = 0 + + def get_name(self): + return self.name + + def get_temperature(self): + return self.temperature + + def get_high_threshold(self): + return self.high_threshold + + def get_low_threshold(self): + return self.low_threshold + + def get_high_critical_threshold(self): + return self.high_critical_threshold + + def get_low_critical_threshold(self): + return self.low_critical_threshold + + def make_over_temper(self): + self.high_threshold = 2 + self.temperature = 3 + self.low_threshold = 1 + + def make_under_temper(self): + self.high_threshold = 3 + self.temperature = 1 + self.low_threshold = 2 + + def make_normal_temper(self): + self.high_threshold = 3 + self.temperature = 2 + self.low_threshold = 1 + + +class MockChassis: + def __init__(self): + self.fan_list = [] + self.psu_list = [] + self.thermal_list = [] + + def get_all_fans(self): + return self.fan_list + + def get_all_psus(self): + return self.psu_list + + def get_all_thermals(self): + return self.thermal_list + + def make_absence_fan(self): + fan = MockFan() + fan.presence = False + self.fan_list.append(fan) + + def make_under_speed_fan(self): + fan = MockFan() + fan.make_under_speed() + self.fan_list.append(fan) + + def make_over_speed_fan(self): + fan = MockFan() + fan.make_over_speed() + self.fan_list.append(fan) + + def make_over_temper_thermal(self): + thermal = MockThermal() + thermal.make_over_temper() + self.thermal_list.append(thermal) + + def make_under_temper_thermal(self): + thermal = MockThermal() + thermal.make_under_temper() + self.thermal_list.append(thermal) + diff --git a/sonic-thermalctld/tests/mock_swsscommon.py b/sonic-thermalctld/tests/mock_swsscommon.py new file mode 100644 index 000000000000..174e4935b1cf --- /dev/null +++ b/sonic-thermalctld/tests/mock_swsscommon.py @@ -0,0 +1,17 @@ +STATE_DB = '' + + +class Table: + def __init__(self, db, table_name): + self.table_name = table_name + + def _del(self, key): + pass + + def set(self, key, fvs): + pass + + +class FieldValuePairs: + def __init__(self, fvs): + pass diff --git a/sonic-thermalctld/tests/test_thermalctld.py b/sonic-thermalctld/tests/test_thermalctld.py new file mode 100644 index 000000000000..ac6bd51069e5 --- /dev/null +++ b/sonic-thermalctld/tests/test_thermalctld.py @@ -0,0 +1,200 @@ +import os +import sys +from mock import Mock, MagicMock, patch +from sonic_daemon_base import daemon_base +from .mock_platform import MockChassis, MockFan + +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('thermalctld', scripts_path + '/thermalctld') +from thermalctld import * + + +def setup_function(): + logger.log_notice = MagicMock() + logger.log_warning = MagicMock() + + +def teardown_function(): + logger.log_notice.reset() + logger.log_warning.reset() + + +def test_fanstatus_set_presence(): + fan_status = FanStatus() + ret = fan_status.set_presence(True) + assert fan_status.presence + assert not ret + + ret = fan_status.set_presence(False) + assert not fan_status.presence + assert ret + + +def test_fanstatus_set_under_speed(): + fan_status = FanStatus() + ret = fan_status.set_under_speed(NOT_AVAILABLE, NOT_AVAILABLE, NOT_AVAILABLE) + assert not ret + + ret = fan_status.set_under_speed(NOT_AVAILABLE, NOT_AVAILABLE, 0) + assert not ret + + ret = fan_status.set_under_speed(NOT_AVAILABLE, 0, 0) + assert not ret + + ret = fan_status.set_under_speed(0, 0, 0) + assert not ret + + ret = fan_status.set_under_speed(80, 100, 19) + assert ret + assert fan_status.under_speed + assert not fan_status.is_ok() + + ret = fan_status.set_under_speed(81, 100, 19) + assert ret + assert not fan_status.under_speed + assert fan_status.is_ok() + + +def test_fanstatus_set_over_speed(): + fan_status = FanStatus() + ret = fan_status.set_over_speed(NOT_AVAILABLE, NOT_AVAILABLE, NOT_AVAILABLE) + assert not ret + + ret = fan_status.set_over_speed(NOT_AVAILABLE, NOT_AVAILABLE, 0) + assert not ret + + ret = fan_status.set_over_speed(NOT_AVAILABLE, 0, 0) + assert not ret + + ret = fan_status.set_over_speed(0, 0, 0) + assert not ret + + ret = fan_status.set_over_speed(120, 100, 19) + assert ret + assert fan_status.over_speed + assert not fan_status.is_ok() + + ret = fan_status.set_over_speed(120, 100, 21) + assert ret + assert not fan_status.over_speed + assert fan_status.is_ok() + + +def test_fanupdater_fan_absence(): + chassis = MockChassis() + chassis.make_absence_fan() + fan_updater = FanUpdater(chassis) + fan_updater.update() + fan_list = chassis.get_all_fans() + assert fan_list[0].get_status_led() == MockFan.STATUS_LED_COLOR_RED + logger.log_warning.assert_called_once() + + fan_list[0].presence = True + fan_updater.update() + assert fan_list[0].get_status_led() == MockFan.STATUS_LED_COLOR_GREEN + logger.log_notice.assert_called_once() + + +def test_fanupdater_fan_under_speed(): + chassis = MockChassis() + chassis.make_under_speed_fan() + fan_updater = FanUpdater(chassis) + fan_updater.update() + fan_list = chassis.get_all_fans() + assert fan_list[0].get_status_led() == MockFan.STATUS_LED_COLOR_RED + logger.log_warning.assert_called_once() + + fan_list[0].make_normal_speed() + fan_updater.update() + assert fan_list[0].get_status_led() == MockFan.STATUS_LED_COLOR_GREEN + logger.log_notice.assert_called_once() + + +def test_fanupdater_fan_over_speed(): + chassis = MockChassis() + chassis.make_over_speed_fan() + fan_updater = FanUpdater(chassis) + fan_updater.update() + fan_list = chassis.get_all_fans() + assert fan_list[0].get_status_led() == MockFan.STATUS_LED_COLOR_RED + logger.log_warning.assert_called_once() + + fan_list[0].make_normal_speed() + fan_updater.update() + assert fan_list[0].get_status_led() == MockFan.STATUS_LED_COLOR_GREEN + logger.log_notice.assert_called_once() + + +def test_temperature_status_set_over_temper(): + temperatue_status = TemperatureStatus() + ret = temperatue_status.set_over_temperature(NOT_AVAILABLE, NOT_AVAILABLE) + assert not ret + + ret = temperatue_status.set_over_temperature(NOT_AVAILABLE, 0) + assert not ret + + ret = temperatue_status.set_over_temperature(0, NOT_AVAILABLE) + assert not ret + + ret = temperatue_status.set_over_temperature(2, 1) + assert ret + assert temperatue_status.over_temperature + + ret = temperatue_status.set_over_temperature(1, 2) + assert ret + assert not temperatue_status.over_temperature + + +def test_temperstatus_set_under_temper(): + temperature_status = TemperatureStatus() + ret = temperature_status.set_under_temperature(NOT_AVAILABLE, NOT_AVAILABLE) + assert not ret + + ret = temperature_status.set_under_temperature(NOT_AVAILABLE, 0) + assert not ret + + ret = temperature_status.set_under_temperature(0, NOT_AVAILABLE) + assert not ret + + ret = temperature_status.set_under_temperature(1, 2) + assert ret + assert temperature_status.under_temperature + + ret = temperature_status.set_under_temperature(2, 1) + assert ret + assert not temperature_status.under_temperature + + +def test_temperupdater_over_temper(): + chassis = MockChassis() + chassis.make_over_temper_thermal() + temperature_updater = TemperatureUpdater(chassis) + temperature_updater.update() + thermal_list = chassis.get_all_thermals() + logger.log_warning.assert_called_once() + + thermal_list[0].make_normal_temper() + temperature_updater.update() + logger.log_notice.assert_called_once() + + +def test_temperupdater_under_temper(): + chassis = MockChassis() + chassis.make_under_temper_thermal() + temperature_updater = TemperatureUpdater(chassis) + temperature_updater.update() + thermal_list = chassis.get_all_thermals() + logger.log_warning.assert_called_once() + + thermal_list[0].make_normal_temper() + temperature_updater.update() + logger.log_notice.assert_called_once() +