diff --git a/.gitignore b/.gitignore index 2b1a01ab0..810be15f3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ -build/ +*.pyc */__pycache__/ +build/ sonic_platform_common.egg-info/ +.cache \ No newline at end of file diff --git a/setup.py b/setup.py index 55a26e5a2..3f004983b 100644 --- a/setup.py +++ b/setup.py @@ -13,12 +13,13 @@ packages=[ 'sonic_eeprom', 'sonic_led', + 'sonic_fan', 'sonic_platform_base', 'sonic_platform_base.sonic_eeprom', 'sonic_platform_base.sonic_sfp', 'sonic_platform_base.sonic_ssd', + 'sonic_platform_base.sonic_thermal_control', 'sonic_psu', - 'sonic_fan', 'sonic_sfp', ], classifiers=[ @@ -34,5 +35,5 @@ 'Programming Language :: Python :: 3.6', 'Topic :: Utilities', ], - keywords='sonic SONiC platform hardware interface api API', + keywords='sonic SONiC platform hardware interface api API' ) diff --git a/sonic_platform_base/chassis_base.py b/sonic_platform_base/chassis_base.py index eb1e089b5..051dc2dcd 100644 --- a/sonic_platform_base/chassis_base.py +++ b/sonic_platform_base/chassis_base.py @@ -333,6 +333,14 @@ def get_thermal(self, index): return thermal + def get_thermal_manager(self): + """ + Retrieves thermal manager class on this chassis + :return: A class derived from ThermalManagerBase representing the + specified thermal manager. ThermalManagerBase is returned as default + """ + raise NotImplementedError + ############################################## # SFP methods ############################################## diff --git a/sonic_platform_base/sonic_thermal_control/__init__.py b/sonic_platform_base/sonic_thermal_control/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/sonic_platform_base/sonic_thermal_control/thermal_action_base.py b/sonic_platform_base/sonic_thermal_control/thermal_action_base.py new file mode 100644 index 000000000..ee5373794 --- /dev/null +++ b/sonic_platform_base/sonic_thermal_control/thermal_action_base.py @@ -0,0 +1,17 @@ +from .thermal_json_object import ThermalJsonObject + + +class ThermalPolicyActionBase(ThermalJsonObject): + """ + Base class for thermal action. Once all thermal conditions in a thermal policy are matched, + all predefined thermal action will be executed. + """ + def execute(self, thermal_info_dict): + """ + Take action when thermal condition matches. For example, adjust speed of fan or shut + down the switch. + :param thermal_info_dict: A dictionary stores all thermal information. + :return: + """ + raise NotImplementedError + diff --git a/sonic_platform_base/sonic_thermal_control/thermal_condition_base.py b/sonic_platform_base/sonic_thermal_control/thermal_condition_base.py new file mode 100644 index 000000000..2196efad1 --- /dev/null +++ b/sonic_platform_base/sonic_thermal_control/thermal_condition_base.py @@ -0,0 +1,14 @@ +from .thermal_json_object import ThermalJsonObject + + +class ThermalPolicyConditionBase(ThermalJsonObject): + """ + Base class for thermal condition + """ + def is_match(self, thermal_info_dict): + """ + Indicate if this condition is matched. + :param thermal_info_dict: A dictionary stores all thermal information. + :return: True if condition matched else False. + """ + raise NotImplementedError diff --git a/sonic_platform_base/sonic_thermal_control/thermal_info_base.py b/sonic_platform_base/sonic_thermal_control/thermal_info_base.py new file mode 100644 index 000000000..32aa06ee0 --- /dev/null +++ b/sonic_platform_base/sonic_thermal_control/thermal_info_base.py @@ -0,0 +1,15 @@ +from .thermal_json_object import ThermalJsonObject + + +class ThermalPolicyInfoBase(object): + """ + Base class for thermal information + """ + def collect(self, chassis): + """ + Collect thermal information for thermal policy. + :param chassis: The chassis object. + :return: + """ + raise NotImplementedError + diff --git a/sonic_platform_base/sonic_thermal_control/thermal_json_object.py b/sonic_platform_base/sonic_thermal_control/thermal_json_object.py new file mode 100644 index 000000000..3060d8665 --- /dev/null +++ b/sonic_platform_base/sonic_thermal_control/thermal_json_object.py @@ -0,0 +1,63 @@ +class ThermalJsonObject(object): + """ + Base class for thermal json object. + """ + # JSON field definition + JSON_FIELD_TYPE = 'type' + + # Dictionary of ThermalJsonObject-derived class representing all thermal json types + _object_type_dict = {} + + def load_from_json(self, json_obj): + """ + Initialize this object by a json object. The json object is read from policy json file. + Derived class can define any field in policy json file and interpret them in this function. + :param json_obj: A json object representing an object. + :return: + """ + pass + + @classmethod + def register_concrete_type(cls, type_name, object_type): + """ + Register a concrete class by type name. The concrete class must derive from + ThermalJsonObject. + :param type_name: Name of the class. + :param object_type: A concrete class. + :return: + """ + if type_name not in cls._object_type_dict: + cls._object_type_dict[type_name] = object_type + else: + raise Exception('ThermalJsonObject type {} already exists'.format(type_name)) + + @classmethod + def get_type(cls, json_obj): + """ + Get a concrete class by json object. The json object represents an object and must + have a 'type' field. This function returns a pre-registered concrete class if the specific + 'type' is found. + :param json_obj: A json object representing an action. + :return: A concrete class if requested type exists; Otherwise None. + """ + if ThermalJsonObject.JSON_FIELD_TYPE in json_obj: + type_str = json_obj[ThermalJsonObject.JSON_FIELD_TYPE] + if type_str in cls._object_type_dict: + return cls._object_type_dict[type_str] + else: + raise Exception('ThermalJsonObject type {} not found'.format(type_str) ) + + raise Exception('Invalid policy file, {} field must be presented'.format(ThermalJsonObject.JSON_FIELD_TYPE)) + + +def thermal_json_object(type_name): + """ + Decorator to auto register a ThermalJsonObject-derived class + :param type_name: Type name of the concrete class which corresponding to the 'type' field of + a condition, action or info. + :return: Wrapper function + """ + def wrapper(object_type): + ThermalJsonObject.register_concrete_type(type_name, object_type) + return object_type + return wrapper diff --git a/sonic_platform_base/sonic_thermal_control/thermal_manager_base.py b/sonic_platform_base/sonic_thermal_control/thermal_manager_base.py new file mode 100644 index 000000000..8ffe5d6ed --- /dev/null +++ b/sonic_platform_base/sonic_thermal_control/thermal_manager_base.py @@ -0,0 +1,202 @@ +import json +from .thermal_policy import ThermalPolicy +from .thermal_json_object import ThermalJsonObject + + +class ThermalManagerBase(object): + """ + Base class of ThermalManager representing a manager to control all thermal policies. + """ + # JSON field definition. + JSON_FIELD_POLICIES = 'policies' + JSON_FIELD_INFO_TYPES = 'info_types' + JSON_FIELD_POLICY_NAME = 'name' + JSON_FIELD_THERMAL_ALGORITHM = "thermal_control_algorithm" + JSON_FIELD_FAN_SPEED_WHEN_SUSPEND = "fan_speed_when_suspend" + JSON_FIELD_RUN_AT_BOOT_UP = "run_at_boot_up" + + # Dictionary of ThermalPolicy objects. + _policy_dict = {} + + # Dictionary of thermal information objects. A thermal information object is used by Thermal Policy + _thermal_info_dict = {} + + _fan_speed_when_suspend = None + + _run_thermal_algorithm_at_boot_up = None + + @classmethod + def initialize(cls): + """ + Initialize thermal manager, including register thermal condition types and thermal action types + and any other vendor specific initialization. + :return: + """ + pass + + @classmethod + def deinitialize(cls): + """ + Destroy thermal manager, including any vendor specific cleanup. The default behavior of this function + is a no-op. + :return: + """ + pass + + @classmethod + def start_thermal_control_algorithm(cls): + """ + Start vendor specific thermal control algorithm. The default behavior of this function is a no-op. + :return: + """ + pass + + @classmethod + def stop_thermal_control_algorithm(cls): + """ + Stop vendor specific thermal control algorithm. The default behavior of this function is a no-op. + :return: + """ + pass + + @classmethod + def load(cls, policy_file_name): + """ + Load all thermal policies from JSON policy file. An example looks like: + { + "thermal_control_algorithm": { + "run_at_boot_up": "false", + "fan_speed_when_suspend": "60" + }, + "info_types": [ + { + "type": "fan_info" # collect fan information for each iteration + }, + { + "type": "psu_info" # collect psu information for each iteration + } + ], + "policies": [ + { + "name": "any fan absence", # if any fan absence, set all fan speed to 100% and disable thermal control algorithm + "conditions": [ + { + "type": "fan.any.absence" # see sonic-platform-daemons.sonic-thermalctld.thermal_policy.thermal_conditions + } + ], + "actions": [ + { + "type": "fan.all.set_speed", # see sonic-platform-daemons.sonic-thermalctld.thermal_policy.thermal_actions + "speed": "100" + }, + { + "type": "thermal_control.control", + "status": "false" + } + ] + }, + { + "name": "all fan absence", # if all fan absence, shutdown the switch + "conditions": [ + { + "type": "fan.all.absence" + } + ], + "actions": [ + { + "type": "switch.shutdown" + } + ] + } + ] + } + :param policy_file_name: Path of JSON policy file. + :return: + """ + with open(policy_file_name, 'r') as policy_file: + json_obj = json.load(policy_file) + if cls.JSON_FIELD_POLICIES in json_obj: + json_policies = json_obj[cls.JSON_FIELD_POLICIES] + for json_policy in json_policies: + cls._load_policy(json_policy) + + if cls.JSON_FIELD_INFO_TYPES in json_obj: + for json_info in json_obj[cls.JSON_FIELD_INFO_TYPES]: + info_type = ThermalJsonObject.get_type(json_info) + info_obj = info_type() + cls._thermal_info_dict[json_info[ThermalJsonObject.JSON_FIELD_TYPE]] = info_obj + + if cls.JSON_FIELD_THERMAL_ALGORITHM in json_obj: + json_thermal_algorithm_config = json_obj[cls.JSON_FIELD_THERMAL_ALGORITHM] + if cls.JSON_FIELD_RUN_AT_BOOT_UP in json_thermal_algorithm_config: + cls._run_thermal_algorithm_at_boot_up = \ + True if json_thermal_algorithm_config[cls.JSON_FIELD_RUN_AT_BOOT_UP].lower() == 'true' else False + + if cls.JSON_FIELD_FAN_SPEED_WHEN_SUSPEND in json_thermal_algorithm_config: + # if the string is not a valid int, let it raise + cls._fan_speed_when_suspend = \ + int(json_thermal_algorithm_config[cls.JSON_FIELD_FAN_SPEED_WHEN_SUSPEND]) + + @classmethod + def _load_policy(cls, json_policy): + """ + Load a policy object from a JSON object. + :param json_policy: A JSON object representing a thermal policy. + :return: + """ + if cls.JSON_FIELD_POLICY_NAME in json_policy: + name = json_policy[cls.JSON_FIELD_POLICY_NAME] + if name in cls._policy_dict: + raise Exception('Policy {} already exists'.format(name)) + + policy = ThermalPolicy() + policy.load_from_json(json_policy) + cls._policy_dict[name] = policy + else: + raise Exception('{} not found in policy'.format(cls.JSON_FIELD_POLICY_NAME)) + + @classmethod + def run_policy(cls, chassis): + """ + Collect thermal information, run each policy, if one policy matches, execute the policy's action. + :param chassis: The chassis object. + :return: + """ + if not cls._policy_dict: + return + + cls._collect_thermal_information(chassis) + + for policy in cls._policy_dict.values(): + if policy.is_match(cls._thermal_info_dict): + policy.do_action(cls._thermal_info_dict) + + @classmethod + def _collect_thermal_information(cls, chassis): + """ + Collect thermal information. This function will be called before run_policy. + :param chassis: The chassis object. + :return: + """ + for thermal_info in cls._thermal_info_dict.values(): + thermal_info.collect(chassis) + + @classmethod + def init_thermal_algorithm(cls, chassis): + """ + Initialize thermal algorithm according to policy file. + :param chassis: The chassis object. + :return: + """ + if cls._run_thermal_algorithm_at_boot_up is not None: + if cls._run_thermal_algorithm_at_boot_up: + cls.start_thermal_control_algorithm() + else: + cls.stop_thermal_control_algorithm() + if cls._fan_speed_when_suspend is not None: + for fan in chassis.get_all_fans(): + fan.set_speed(cls._fan_speed_when_suspend) + + for psu in chassis.get_all_psus(): + for fan in psu.get_all_fans(): + fan.set_speed(cls._fan_speed_when_suspend) diff --git a/sonic_platform_base/sonic_thermal_control/thermal_policy.py b/sonic_platform_base/sonic_thermal_control/thermal_policy.py new file mode 100644 index 000000000..56e619bff --- /dev/null +++ b/sonic_platform_base/sonic_thermal_control/thermal_policy.py @@ -0,0 +1,67 @@ +from .thermal_json_object import ThermalJsonObject + + +class ThermalPolicy(object): + """ + Class representing a thermal policy. A thermal policy object is initialized by JSON policy file. + """ + # JSON field definition. + JSON_FIELD_NAME = 'name' + JSON_FIELD_CONDITIONS = 'conditions' + JSON_FIELD_ACTIONS = 'actions' + + def __init__(self): + # Name of the policy + self.name = None + + # Conditions load from JSON policy file + self.conditions = [] + + # Actions load from JSON policy file + self.actions = [] + + def load_from_json(self, json_obj): + """ + Load thermal policy from JSON policy file. + :param json_obj: A json object representing a thermal policy. + :return: + """ + if self.JSON_FIELD_NAME in json_obj: + self.name = json_obj[self.JSON_FIELD_NAME] + + if self.JSON_FIELD_CONDITIONS in json_obj: + for json_condition in json_obj[self.JSON_FIELD_CONDITIONS]: + cond_type = ThermalJsonObject.get_type(json_condition) + cond_obj = cond_type() + cond_obj.load_from_json(json_condition) + self.conditions.append(cond_obj) + + if self.JSON_FIELD_ACTIONS in json_obj: + for json_action in json_obj[self.JSON_FIELD_ACTIONS]: + action_type = ThermalJsonObject.get_type(json_action) + action_obj = action_type() + action_obj.load_from_json(json_action) + self.actions.append(action_obj) + else: + raise Exception('name field not found in policy') + + def is_match(self, thermal_info_dict): + """ + Indicate if this policy is match. + :param thermal_info_dict: A dictionary stores all thermal information. + :return: True if all conditions matches else False. + """ + for condition in self.conditions: + if not condition.is_match(thermal_info_dict): + return False + + return True + + def do_action(self, thermal_info_dict): + """ + Execute all actions if is_match returns True. + :param thermal_info_dict: A dictionary stores all thermal information. + :return: + """ + for action in self.actions: + action.execute(thermal_info_dict) diff --git a/sonic_platform_base/thermal_base.py b/sonic_platform_base/thermal_base.py index 6dc8dc1f3..0388d4f60 100644 --- a/sonic_platform_base/thermal_base.py +++ b/sonic_platform_base/thermal_base.py @@ -72,3 +72,23 @@ def set_low_threshold(self, temperature): A boolean, True if threshold is set successfully, False if not """ raise NotImplementedError + + def get_high_critical_threshold(self): + """ + Retrieves the high critical threshold temperature of thermal + + Returns: + A float number, the high critical threshold temperature of thermal in Celsius + up to nearest thousandth of one degree Celsius, e.g. 30.125 + """ + raise NotImplementedError + + def get_low_critical_threshold(self): + """ + Retrieves the low critical threshold temperature of thermal + + Returns: + A float number, the low critical threshold temperature of thermal in Celsius + up to nearest thousandth of one degree Celsius, e.g. 30.125 + """ + raise NotImplementedError