diff --git a/dentos-poe-agent/opt/poeagent/bin/poecli b/dentos-poe-agent/opt/poeagent/bin/poecli index ce9291e..e83da4f 100755 --- a/dentos-poe-agent/opt/poeagent/bin/poecli +++ b/dentos-poe-agent/opt/poeagent/bin/poecli @@ -4,9 +4,11 @@ BIN_PATH=$POE_ROOT/bin/ INC_PATH=$POE_ROOT/inc/ LIB_PATH=$POE_ROOT/lib/ DRIVERS_PATH=$POE_ROOT/drivers/ +PD69200_PATH=$POE_ROOT/drivers/pd69200 PLATFORMS_PATH=$POE_ROOT/platforms/ -export PYTHONPATH=$BIN_PATH:$INC_PATH:$LIB_PATH:$DRIVERS_PATH:$PLATFORMS_PATH +export PYTHONPATH=$POE_ROOT:$BIN_PATH:$INC_PATH:$LIB_PATH:$DRIVERS_PATH:$PD69200_PATH:$PLATFORMS_PATH cd $BIN_PATH -/usr/bin/python3 poecli.py $@ + +/usr/bin/env python3 poecli.py $@ diff --git a/dentos-poe-agent/opt/poeagent/bin/poecli.py b/dentos-poe-agent/opt/poeagent/bin/poecli.py index e626297..6450e25 100755 --- a/dentos-poe-agent/opt/poeagent/bin/poecli.py +++ b/dentos-poe-agent/opt/poeagent/bin/poecli.py @@ -14,494 +14,774 @@ limitations under the License. ''' -from poe_common import * -from datetime import datetime, date -from time import sleep -from poe_version import * - -import binascii -import re -import imp -import sys -import subprocess -import os import argparse -import time -import collections +import errno +import getpass import json -import pathlib +import os +import re +import sys +from argparse import ArgumentParser +from enum import Enum +from typing import NoReturn, OrderedDict + +import grpc +import poed_ipc_pb2 +import poed_ipc_pb2_grpc +from agent_constants import AgentConstants +from poe_common import * +from poe_log import PoeLog -bootcmd_path = "/proc/cmdline" -pa_root_path = os.getcwd() + "/../" -plat_root_path = pa_root_path + "platforms" -PORTLIST_VALIDATION1 = "^([1-9]{0,1}[0-9]{1})-([1-9]{0,1}[0-9]{1})$" -PORTLIST_VALIDATION2 = "^([1-9]{0,1}[0-9]{1})$" class PoeCLI(object): - TIME_FMT = "%Y/%m/%d %H:%M:%S" - - def __init__(self): - self.log = PoeLog() - self.poe_plat = self.load_poe_platform() + """poecli implementation + The PoE CLI is used by the user to apply direct changes to the PoE chipset. + Additionally, the user can also trigger a manual save or load action + for the config through the poed daemon by running -c --save or -c --load. + All ports will be initially enabled to support LLDP negotiation. + For disabling this behavior, please refer to the 'set --lldp' + argument. + To configure the default power limit that can be assigned for each PoE + power class, refer to the 'set --default-limit' argument. + + Note: synchronized access to the PoE settings is necessary, because both + the CLI and the daemon have write-through access to the PoE system. + """ + + def __init__(self) -> None: + self._log: PoeLog = PoeLog() - # Get platform model name from boot cmd - def platform_model(self, file_path=bootcmd_path): try: - with open(file_path, 'r') as f: - d = dict(i.split('=') for i in f.read().split(' ')) - return d.get("onl_platform").rstrip() - except Exception as e: - print_stderr("Failed to get model name from %s. err: %s" % (bootcmd_path, str(e))) - return "Unknown" + self._channel = grpc.insecure_channel(AgentConstants.POED_GRPC_SERVER_ADDRESS) + self._stub = poed_ipc_pb2_grpc.PoeIpcStub(self._channel) + except Exception as ex: + self._log.exc(f"Failed to connect to gRPC server: {str(ex)}") - def platform_src_path(self): - try: - # dentOS platform format: --- - [arch, manufacturer, model_revision] = self.platform_model().split('-', 2) - return "/".join([plat_root_path, manufacturer, - model_revision, "poe_platform.py"]) - except Exception as e: - print_stderr("Failed to get platform path. err: %s" % str(e)) + self._bt_support = int(self.request_data_from_poed(json.dumps([AgentConstants.POECLI_GET_BT_SUPPORT]))) + self._port_count = int(self.request_data_from_poed(json.dumps([AgentConstants.POECLI_GET_PORT_COUNT]))) + + self._parser: ArgumentParser = ArgumentParser(description="Query or change the PoE settings", prog="poecli") + self.__build_parser() + + def request_data_from_poed(self, poecli_request: str): + """Sends poecli request to poed gRPC server and receives the response from poed - def load_poe_platform(self): - plat_src = imp.load_source("poe_plat", self.platform_src_path()) - poe_plat = plat_src.get_poe_platform() - return poe_plat + Args: + poecli_request (str): User string input + Returns: + str : The response string returned by poed as a response + """ + if not self.__is_poed_alive(): + raise RuntimeError("poed daemon not running. Not sending the IPC command") - def valid_ports(self, data): - portList = [] - total_poe_port = self.poe_plat.total_poe_port() try: - targets = data.split(',') - re1 = re.compile(PORTLIST_VALIDATION1) - re2 = re.compile(PORTLIST_VALIDATION2) - for ports in targets: - if re1.match(ports): - [start, end] = ports.split('-') - start = int(start, 0) - 1 - end = int(end, 0) - 1 - if end < start: + poecli_request = poed_ipc_pb2.PoecliRequest(request=poecli_request) + poed_reply = self._stub.HandlePoecli(poecli_request) + self._log.dbg(f"Sent poed IPC command: {poecli_request}") + return poed_reply.reply + except Exception as ex: + self._log.exc(f"Failed to connect to gRPC server: {str(ex)}") + raise + + def __parse_port_input(self, user_input: str) -> list[int] | NoReturn: + """Validate the user port input by matching either a port range + or a single port index + + Args: + user_input (str): User string input + + Raises: + argparse.ArgumentTypeError: Raised if the user input is invalid + + Returns: + list[int] | NoReturn: The list of ports, if successful + """ + ports = [] + port_range_regex = "^[1-9][0-9]?-[1-9][0-9]?$" + single_port_regex = "^[1-9][0-9]?$" + port_count = self._port_count + try: + targets = user_input.split(",") + for target in targets: + if re.match(port_range_regex, target): + start, end = target.split("-") + start = int(start) + end = int(end) + if end < start: # Got them reversed. start, end = end, start - if start < 0 or end >= total_poe_port: + if end > port_count: raise ValueError - portList += list(range(start, end + 1)) - elif re2.match(ports): - port = int(ports, 0) - 1 - if port < 0 or port >= total_poe_port: + # Zero-based values for the driver facing API. + ports += list(range(start - 1, (end - 1) + 1)) + elif re.match(single_port_regex, target): + port = int(target) + if port > port_count: raise ValueError - portList.append(port) + # Zero-based values for the driver facing API. + ports.append(port - 1) else: raise ValueError - portList = sorted(set(portList)) - return portList + ports = sorted(set(ports)) + return ports except ValueError: - error = "Invalid port inputs: '{0}'.".format(data) - raise argparse.ArgumentTypeError(error) + raise argparse.ArgumentTypeError(f"Invalid port input: '{user_input}'") + + def __parse_user_power_limit(self, user_input: str) -> int | NoReturn: + """Validate the user power limit input and convert it to an integer + + Args: + user_input (str): User string input - def valid_powerlimit(self, data): + Raises: + argparse.ArgumentTypeError: Raised if the user input is invalid + + Returns: + int | NoReturn: The converted value, if successful + """ try: - power = int(data, 0) - if 0 <= power <= 0xffff: + power = int(user_input, 0) + if 0 <= power <= 0xFFFF: return power else: raise ValueError except ValueError: - error = "Invalid power limit: '{0}'.".format(data) - raise argparse.ArgumentTypeError(error) - - def _build_parser(self): - root_parser = argparse.ArgumentParser() - root_sub_parser = root_parser.add_subparsers(dest="subcmd", - help="Descriptions", - metavar="Commands") - - # Show Sub Command - show_parser = root_sub_parser.add_parser("show", - help="Show PoE information", - formatter_class=argparse.RawTextHelpFormatter) - show_parser.add_argument("-d", "--debug", action="store_true", - help="Show more Information for debugging\n") - show_parser.add_argument("-j", "--json", action="store_true", - help="Display information in JSON format\n") - show_group = show_parser.add_mutually_exclusive_group() - show_group.add_argument("-p", "--ports", metavar="", type=self.valid_ports, - help="Show PoE Ports Information\n" - "Example: 1,3-5,45-48") - show_group.add_argument("-s", "--system", action="store_true", - help="Show PoE System Information") - show_group.add_argument("-m", "--mask", action="store_true", - help="Show Individual mask registers") - show_group.add_argument("-a", "--all", action="store_true", - help="Show port, system, and individual masks Information") - show_group.add_argument("-v", "--version", action="store_true", - help="Show PoE versions\n") - - # Set Sub Command - set_parser = root_sub_parser.add_parser("set", help="Set PoE ports", - formatter_class=argparse.RawTextHelpFormatter) - set_parser.add_argument("-p", "--ports", metavar="", required=True, type=self.valid_ports, - help="Logic ports\n" - "Example: 1,3-5,45-48") - set_parser.add_argument("-e", "--enable", type=lambda x: int(x, 0), choices=[0, 1], - metavar="", - help="Port Enable/Disable\n" - "disable = 0, enable = 1") - set_parser.add_argument("-l", "--level", type=lambda x: int(x, 0), choices=[1, 2, 3], - metavar="", - help="Port Priority Level\n" - "crit = 1, high = 2, low = 3") - set_parser.add_argument("-o", "--powerLimit", type=self.valid_powerlimit, - metavar="", - help="Port Power Limit\n" - "range: 0x0 (mW) - 0xffff (mW)\n" - "This field will be ignored if val sets to 0xffff") - # Save Sub Command - save_parser = root_sub_parser.add_parser("savechip", help= - "This command saves the current user values into the non-volatile memory and these user values" - "become the defaults after any reset. " - "To change the default back to the initial factory values, use the Restore Factory Defaults cmd." - "(poecli restore_poe_system). The persistent config won't change until 'poecli cfg --save' issued." + raise argparse.ArgumentTypeError(f"Invalid power limit input: '{user_input}'") + + def __build_parser(self) -> None: + """Add the subparser and arguments for the main arg parser""" + sub_parser = self._parser.add_subparsers(dest="subcmd", help="Description", metavar="Command") + + # show sub-command + show_parser = sub_parser.add_parser("show", help="show PoE system and port information") + show_parser.add_argument("-d", "--debug", action="store_true", help="show verbose information") + show_parser.add_argument("-j", "--json", action="store_true", help="dump output as JSON") + show_exclusive_group = show_parser.add_mutually_exclusive_group(required=True) + show_exclusive_group.add_argument( + "-p", + "--ports", + metavar="", + type=self.__parse_port_input, + help="show PoE port(s) information", + ) + show_exclusive_group.add_argument("-s", "--system", action="store_true", help="show PoE system information") + show_exclusive_group.add_argument( + "-m", "--mask", action="store_true", help="show system individual mask registers" + ) + show_exclusive_group.add_argument( + "--default-limits", action="store_true", help="show default class power limits" + ) + show_exclusive_group.add_argument( + "-a", "--all", action="store_true", help="show port, system, and individual mask registers" + ) + show_exclusive_group.add_argument( + "-v", "--version", action="store_true", help="show PoE firmware, agent and config versions" ) - # CFG Sub Command - cfg_parser = root_sub_parser.add_parser("cfg", help="CFG command, to manipulate the poe agent config files.", - formatter_class=argparse.RawTextHelpFormatter) - cfg_parser.add_argument("-s", "--save", action="store_true", - help="Save current runtime settings to persistent file.\n") - cfg_parser.add_argument("-l", "--load", action="store_true", - help="Load settings from persistent file.\n") - cfg_parser.add_argument("-c", "--config", - metavar="", - help="Assign file path for save/load operation,\n" - "instead of persistent config, Example:\n" - "poecli cfg -s -c [Config Path]") - - # Restore Sub Command - restore_parser = root_sub_parser.add_parser("restore_poe_system", help= - "This command restores modified values to POE chip factory default values \n" - "that are part of the firmware release version." - "Ports will shut down after sending this command." - "After restore factory default, it will initialize the port setting for the platform." - "The persistent config won't change until 'poecli cfg --save' issued.\n" - ) - # CFG Sub Command - guide_parser = root_sub_parser.add_parser("guide", help="Show user guide", - formatter_class=argparse.RawTextHelpFormatter) - - return root_parser - - def json_output(self, data): - print(json.dumps(data, indent = 4)) - - def get_versions(self): - data = collections.OrderedDict() - data[SW_VERSION] = self.poe_plat.get_poe_versions() - data[POE_AGT_VER] = POE_AGENT_VERSION - data[POE_CFG_VER] = POE_CONFIG_VERSION - return data - - def get_system_running_state(self): - return self.poe_plat.get_system_information() + # set sub-command + set_parser = sub_parser.add_parser("set", help="change PoE configuration") + port_group = set_parser.add_argument_group("port settings") + port_group.add_argument( + "-p", "--ports", metavar="", type=self.__parse_port_input, help="port index/indices" + ) + port_group.add_argument( + "-e", "--enable", type=int, choices=[0, 1], metavar="", help="port enable/disable" + ) + port_group.add_argument( + "-l", + "--level", + type=int, + choices=[1, 2, 3], + metavar="", + help="port priority (critical = 1, high = 2, low = 3", + ) + port_group.add_argument( + "-o", + "--power-limit", + type=self.__parse_user_power_limit, + metavar="", + help="port power limit (in mW)", + ) + port_group.add_argument( + "--lldp", type=int, choices=[0, 1], metavar="", help="lldp processing enable/disable" + ) + limits_group = set_parser.add_argument_group("power limit settings") + limits_group.add_argument( + "--default-limit", + nargs=2, + type=int, + metavar=("", ""), + help="set the default class power limit (power class, watts)", + ) - def get_ports_running_state(self, portList): - return self.poe_plat.get_ports_information(portList) + # flush sub-command + sub_parser.add_parser( + "flush", + help="flush the current PoE configuration to the chipset " + "non-volatile memory. Thus, these settings will become defaults " + "after subsequent resets. To change the settings back to factory " + "defaults, use the factory-reset poecli command", + ) - def get_individual_masks(self): - data = collections.OrderedDict() - masks = list(range(0x54)) - for mask in masks: - val = self.poe_plat.get_individual_mask(mask).get(ENDIS) - key = "0x{:02x}".format(mask) - data[key] = val - return data + # factory-reset sub-command + sub_parser.add_parser( + "factory-reset", + help="restore the PoE chipset to factory default state. " + "Ports will shut down after sending this command. ", + ) - def print_poe_version(self, versions): - print("PoE SW Versions: %s" % versions[SW_VERSION]) - print("PoE Agent Version: %s" % versions[POE_AGT_VER]) - print("PoE Config Version: %s" % versions[POE_CFG_VER]) + # config sub-command + cfg_parser = sub_parser.add_parser( + "config", help="either save the current config or load the config " "to/from a file" + ) + config_exclusive_group = cfg_parser.add_mutually_exclusive_group(required=True) + config_exclusive_group.add_argument( + "-s", "--save", action="store_true", help="save persisted runtime config to a file" + ) + config_exclusive_group.add_argument( + "-l", "--load", action="store_true", help="load and apply config from a file" + ) + cfg_parser.add_argument( + "-c", + "--config-file", + metavar="", + help="file used for the save/load command (by default the " "permanent config file is used)", + ) - def print_ports_information(self, ports_info, debug): - print("") - if debug: - print("Port Status En/Dis Priority Protocol Class PWR Consump PWR Limit Voltage Current Latch En4Pair") - print("---- ----------------- ------- -------- -------------- ----- ----------- ----------- --------- -------- ----- -------") + def __get_version_info(self) -> OrderedDict: + """Get the firmware, agent and config versions + + Returns: + OrderedDict: Version information + """ + poed_request = json.dumps([AgentConstants.POECLI_SHOW_CMD, AgentConstants.POECLI_GET_VERSIONS_INFO_CMD]) + poed_reply = self.request_data_from_poed(poed_request) + version_info = json.loads(poed_reply) + return version_info + + def __get_system_info(self) -> OrderedDict: + """Get verbose system info + + Returns: + OrderedDict: System info + """ + poed_request = json.dumps([AgentConstants.POECLI_SHOW_CMD, AgentConstants.POECLI_GET_SYSTEM_INFO_CMD]) + poed_reply = self.request_data_from_poed(poed_request) + system_info = json.loads(poed_reply) + return system_info + + def __get_ports_info(self, ports: list[int]) -> list[dict] | NoReturn: + """Query the PoE HAL to get verbose ports info + The LLDP endis status must be got through poed, + as there may be state changes that the CLI is not aware of + through the local configuration. + + Args: + ports (list[int]): Ports to get info for + + Returns: + list[OrderedDict]: Ports info + """ + poed_request = [AgentConstants.POECLI_SHOW_CMD, AgentConstants.POECLI_GET_PORTS_INFO_CMD] + poed_request.append(str(len(ports))) + # Ports were previously converted to zero-based, + # because driver required zero-based indices. + poed_request.extend(list(map(lambda p: str(p + 1), ports))) + poed_request = json.dumps(poed_request) + poed_reply = self.request_data_from_poed(poed_request) + return json.loads(poed_reply) + + def __get_default_limits(self) -> OrderedDict: + """Query the default power limits from POED + + Returns: + list[OrderedDict]: Default power limits info + """ + poed_request = json.dumps([AgentConstants.POECLI_SHOW_CMD, AgentConstants.POECLI_GET_DEFAULT_LIMITS_CMD]) + poed_reply = self.request_data_from_poed(poed_request) + default_power_limits = json.loads(poed_reply) + data = OrderedDict() + if not default_power_limits: + data["N/A"] = "N/A" else: - print("Port Status En/Dis Priority Protocol Class PWR Consump PWR Limit Voltage Current ") - print("---- ----------------- ------- -------- -------------- ----- ----------- ----------- --------- --------") - for info in ports_info: - if debug: - output = "{:<4d} {:17s} {:7s} {:^8s} {:14s} {:^5s} {:6d} (mW) {:6d} (mW) {:5.1f} (V) {:3d} (mA) {:5s} {:4d}".format( - info.get(PORT_ID), info.get(STATUS), info.get(ENDIS), - info.get(PRIORITY), info.get(PROTOCOL), info.get(CLASS), - info.get(POWER_CONSUMP), info.get(POWER_LIMIT), info.get(VOLTAGE), - info.get(CURRENT), "0x{:02x}".format(info.get(LATCH)), info.get(EN_4PAIR)) - else: - output = "{:<4d} {:17s} {:7s} {:^8s} {:14s} {:^5s} {:6d} (mW) {:6d} (mW) {:5.1f} (V) {:3d} (mA)".format( - info.get(PORT_ID), info.get(STATUS), info.get(ENDIS), - info.get(PRIORITY), info.get(PROTOCOL), info.get(CLASS), - info.get(POWER_CONSUMP), info.get(POWER_LIMIT), info.get(VOLTAGE), - info.get(CURRENT)) - print(output) - print("") + data = default_power_limits + return data - def print_system_information(self, system_info, debug): - print("") - print("==============================") - print(" PoE System Information") - print("==============================") - print(" Total PoE Ports : %d" % system_info.get(TOTAL_PORTS)) + def __get_system_individual_mask_regs(self) -> OrderedDict: + """Get all individual mask registers + Refer to the "MASK Registers List" chapter for further info. + + Returns: + OrderedDict: Mask values + """ + poed_request = json.dumps([AgentConstants.POECLI_SHOW_CMD, AgentConstants.POECLI_GET_MASK_REGS_CMD]) + poed_reply = self.request_data_from_poed(poed_request) + mask_regs = json.loads(poed_reply) + return mask_regs + + def __print_versions(self, versions: OrderedDict) -> None: + """Format and display the versions + + Args: + versions (OrderedDict): Version dictionary + """ + print("=" * 17) + print("PoE Versions Info") + print("=" * 17) + print(f" PoE firmware version : {versions[SW_VERSION]}") + print(f" PoE agent version : {versions[AgentConstants.POE_AGT_VER]}") + print(f" PoE config version : {versions[AgentConstants.POE_CFG_VER]}") + + def __print_ports_information(self, ports: list[OrderedDict], verbose: bool) -> None: + """Format and display port(s) information + + Args: + ports (list[OrderedDict]): Collected ports info + verbose (bool): Verbose flag + """ print("") - print(" Total Power : %.1f W" % system_info.get(TOTAL_POWER)) - print(" Power Consumption : %.1f W" % system_info.get(POWER_CONSUMP)) - print(" Power Avaliable : %.1f W" % system_info.get(POWER_AVAIL)) + print("=" * 21) + print("PoE Ports Information") + print("=" * 21) + + # Print the table header first. + # Some columns may be hidden, depending on the verbose arg. + print( + f"{'Port':<4} {'Status':<17} {'En/Dis':<7} {'Priority':<8} " + f"{'Protocol':<14} {'Class':<5} {'PWR Consump':<11} " + f"{'PWR Limit':<11} {'Voltage':<9} {'Current':<8} " + f"{'LLDP En/Dis':<12}" + f"{(' Latch ') if verbose else ''}" + f"{'En4Pair' if verbose else ''}" + ) + print( + f"{'-' * 4} {'-' * 17} {'-' * 7} {'-' * 8} " + f"{'-' * 14} {'-' * 5} {'-' * 11} {'-' * 11} " + f"{'-' * 9} {'-' * 8} {'-' * 12}" + f"{(' ' + '-' * 5) if verbose else ''}" + f"{(' ' + '-' * 7) if verbose else ''}" + ) + + # Print each port info, aligning it to each column header. + for port in ports: + port_id = port.get(PORT_ID) + power_consumption = port.get(POWER_CONSUMP) + if power_consumption is None: + raise AssertionError(f"Power consumption value for port {port_id} must " "not be None") + power_consumption = str(power_consumption) + " (mW)" + power_limit = port.get(POWER_LIMIT) + if power_limit is None: + raise AssertionError(f"Power limit value for port {port_id} must not be None") + power_limit = str(power_limit) + " (mW)" + voltage = port.get(VOLTAGE) + if voltage is None: + raise AssertionError(f"Voltage value for port {port_id} must not be None") + voltage = f"{voltage:.1f} (V)" + current = port.get(CURRENT) + if current is None: + raise AssertionError(f"Current value for port {port_id} must not be None") + current = str(current) + " (mA)" + lldp_endis = port.get(AgentConstants.LLDP_ENDIS) + if lldp_endis is None: + raise AssertionError(f"LLDP endis value for port {port_id} must not be None") + latch = port.get(LATCH) + if latch is None: + raise AssertionError(f"Latch value for port {port_id} must be not be None") + latch = f"0x{latch:02x}" + latch = f" {latch:<5s} " if verbose else "" + en_4pair = f"{port.get(EN_4PAIR):^7d} " if verbose else "" + print( + f"{port_id:<4d} " + f"{port.get(STATUS):<17s} " + f"{port.get(ENDIS):<7s} " + f"{port.get(PRIORITY):^8s} " + f"{port.get(PROTOCOL):<14s} " + f"{port.get(CLASS):^5s} " + f"{power_consumption:<11s} {power_limit:<11s} " + f"{voltage:<9s} {current:<8s} {lldp_endis:<12s}" + latch + en_4pair + ) + + def __print_system_information_header(self): print("") - print(" Power Bank # : %d" % system_info.get(POWER_BANK)) - print(" Power Sources : %s" % system_info.get(POWER_SRC)) + print("=" * 22) + print("PoE System Information") + print("=" * 22) + + def __print_system_information(self, sys_info: list[OrderedDict], verbose: bool) -> None: + """Format and display the system power information + + Args: + sys_info (List of OrderedDict): Collected system information + verbose (bool): Verbose flag + """ + total_ports = 0 + total_power = 0 + consumed_power = 0 + + for i in range(0, len(sys_info)): + total_ports = total_ports + sys_info[i].get(TOTAL_PORTS) + total_power = total_power + sys_info[i].get(TOTAL_POWER) + consumed_power = consumed_power + sys_info[i].get(POWER_CONSUMP) + + self.__print_system_information_header() + print(f" {'Total PoE ports':<18s}: " f"{total_ports}") + print(f" {'Total Power':<18s}: " f"{total_power:.1f} W") + print(f" {'Total Consumed power':<18s}: " f"{consumed_power:.1f} W") + + for i in range(0, len(sys_info)): + self.__print_chip_system_information(sys_info[i], i, verbose, False) + + def __print_chip_system_information(self, sys_info: OrderedDict, index: int, verbose: bool, isheader:bool = False) -> None: + """Format and display the system power information + + Args: + sys_info (OrderedDict): Collected system information + verbose (bool): Verbose flag + """ + if isheader: + self.__print_system_information_header() + else: + print("-" * 22) + print(f" {'Chip index':<18s}: " f"{index}") + print(f" {'PoE ports':<18s}: " f"{sys_info.get(TOTAL_PORTS)}") + print(f" {'Power':<18s}: " f"{sys_info.get(TOTAL_POWER):.1f} W") + print(f" {'Consumed power':<18s}: " f"{sys_info.get(POWER_CONSUMP):.1f} W") + if CALCULATED_POWER in sys_info: + print(f" {'Calculated power':<18s}: " f"{sys_info.get(CALCULATED_POWER):.1f} W") + print(f" {'Available power':<18s}: " f"{sys_info.get(POWER_AVAIL):.1f} W") print("") - if debug: - print(" Max Shutdown Volt : %.1f V" % system_info.get(MAX_SD_VOLT)) - print(" Min Shutdown Volt : %.1f V" % system_info.get(MIN_SD_VOLT)) - print("") - print(" PM1 : 0x%02x" % system_info.get(PM1)) - print(" PM2 : 0x%02x" % system_info.get(PM2)) - print(" PM3 : 0x%02x" % system_info.get(PM3)) + print(f" {'Power bank #':<18s}: " f"{sys_info.get(POWER_BANK)}") + print(f" {'Power sources':<18s}: " f"{sys_info.get(POWER_SRC)}") + if verbose: + print("=" * 26) + print("System Status") + print("=" * 26) + print(f" {'Max Shutdown (V)':<18s}: " f"{sys_info.get(MAX_SD_VOLT)}") + print(f" {'Min Shutdown (V)':<18s}: " f"{sys_info.get(MIN_SD_VOLT)}") print("") - print(" CPU Status1 : 0x%02x" % system_info.get(CPU_STATUS1)) - print(" CPU Status2 : 0x%02x" % system_info.get(CPU_STATUS2)) - print(" FAC Default : %d" % system_info.get(FAC_DEFAULT)) - print(" General Intl Err : 0x%02x" % system_info.get(GIE)) - print(" Private Label : 0x%02x" % system_info.get(PRIV_LABEL)) - print(" User Byte : 0x%02x" % system_info.get(USER_BYTE)) - print(" Device Fail : 0x%02x" % system_info.get(DEVICE_FAIL)) - print(" Temp Disconnect : 0x%02x" % system_info.get(TEMP_DISCO)) - print(" Temp Alarm : 0x%02x" % system_info.get(TEMP_ALARM)) - print(" Interrupt Reg : 0x%04x" % system_info.get(INTR_REG)) + print(f" {'PM1 (system power)':<18s}: " f"0x{sys_info.get(PM1):02x}") + print(f" {'PM2 (PPL)':<18s}: " f"0x{sys_info.get(PM2):02x}") + print(f" {'PM3 (startup cond)':<18s}: " f"0x{sys_info.get(PM3):02x}") print("") + print(f" {'CPU Status1':<18s}: " f"0x{sys_info.get(CPU_STATUS1):02x}") + print(f" {'CPU Status2':<18s}: " f"0x{sys_info.get(CPU_STATUS2):02x}") + print(f" {'Factory default':<18s}: " f"0x{sys_info.get(FAC_DEFAULT):02x}") + print(f" {'General error':<18s}: " f"0x{sys_info.get(GIE):02x}") + print(f" {'Private label':<18s}: " f"0x{sys_info.get(PRIV_LABEL):02x}") + print(f" {'User byte':<18s}: " f"0x{sys_info.get(USER_BYTE):02x}") + print(f" {'Device fail':<18s}: " f"0x{sys_info.get(DEVICE_FAIL):02x}") + print(f" {'Temp disconnect':<18s}: " f"0x{sys_info.get(TEMP_DISCO):02x}") + print(f" {'Temp alarm':<18s}: " f"0x{sys_info.get(TEMP_ALARM):02x}") + print(f" {'Interrupt reg':<18s}: " f"0x{sys_info.get(INTR_REG):02x}") + + def __print_default_limits(self, default_power_limits: OrderedDict) -> None: + """Formats and Print default power limits + + Args: + default_power_limits (OrderedDict): Collected default power limits + """ + print("") + print("=" * 20) + print("Default power limits") + print("=" * 20) + + for key, value in default_power_limits.items(): + print(f" Class {key}: {value}(W)") - def print_indv_masks(self, masks): + def __print_mask_registers(self, masks: OrderedDict) -> None: + """Print individual mask registers + + Args: + masks (OrderedDict): Collected mask registers + """ print("") - print("==================") - print(" Individual Masks") - print("==================") - for key in masks: - print(" {:s}:{:2d}".format(key, masks[key])) + print("=" * 21) + print("System mask registers") + print("=" * 21) print("") - @PoeAccessExclusiveLock - def show_versions(self, json): - try: - data = collections.OrderedDict() - data[VERSIONS] = self.get_versions() - if json: - self.json_output(data) - else: - self.print_poe_version(data[VERSIONS]) - except Exception as e: - print_stderr("Failed to show poe versions! (%s)" % str(e)) + print(f"{'-' *124}") + length = len(masks) + keys = [] + values = [] - @PoeAccessExclusiveLock - def show_system_information(self, debug, json): + for key in masks: + keys.append(key) + values.append(masks[key]) + + index = 0 + rows_num = length // 16 + 1 if length % 16 else length // 16 + + for _ in range(rows_num): + register_row = "| Register |" + mask_row = "| Mask |" + for _ in range(0, 16): + if index < length: + register_row += " " + keys[index] + " |" + mask_row += " " + str(values[index]) + for _ in range(6 - len(str(values[index])) - 2): + mask_row += " " + mask_row += "|" + else: + register_row += " |" + mask_row += " |" + index += 1 + print(register_row) + print(f"{'-' *124}") + print(mask_row) + print(f"{'-' *124}") + + def __show_versions(self, json_flag: bool) -> None: + """Print the software versions + + Args: + json_flag (bool): Dump as JSON flag + """ try: - data = collections.OrderedDict() - data[SYS_INFO] = self.get_system_running_state() - if json: - self.json_output(data) + data = OrderedDict() + data[AgentConstants.VERSIONS] = self.__get_version_info() + if json_flag: + print(json.dumps(data, indent=4)) else: - self.print_system_information(data[SYS_INFO], debug) + self.__print_versions(data[AgentConstants.VERSIONS]) except Exception as e: - print_stderr( - "Failed to show poe system information! (%s)" % str(e)) + self._log.exc(f"Failed to print the software versions: {str(e)}") - @PoeAccessExclusiveLock - def show_ports_information(self, portList, debug, json): - try: - data = collections.OrderedDict() - data[PORT_INFO] = self.get_ports_running_state(portList) - if json: - self.json_output(data) - else: - self.print_ports_information(data[PORT_INFO], debug) - except Exception as e: - print_stderr( - "Failed to show poe ports information! (%s)" % str(e)) + def __show_system_information(self, debug_flag: bool, json_flag: bool) -> None: + """Print the system information - @PoeAccessExclusiveLock - def show_individual_masks(self, json): + Args: + debug_flag (bool): Verbose output flag + json_flag (bool): Dump as JSON flag + """ try: - data = collections.OrderedDict() - data[INDV_MASKS] = self.get_individual_masks() - if json: - self.json_output(data) + data = OrderedDict() + data[AgentConstants.SYS_INFO] = self.__get_system_info() + if json_flag: + print(json.dumps(data, indent=4)) else: - self.print_indv_masks(data[INDV_MASKS]) + if type(data[AgentConstants.SYS_INFO]) is list: + self.__print_system_information(data[AgentConstants.SYS_INFO], debug_flag) + else: + self.__print_chip_system_information(data[AgentConstants.SYS_INFO], 0, debug_flag, True) except Exception as e: - print_stderr("Failed to show individual masks! (%s)" % str(e)) + self._log.exc(f"Failed to print the system information: {str(e)}") + + def __show_ports_information(self, ports: list[int], debug_flag: bool, json_flag: bool) -> None: + """Print info for the given ports - @PoeAccessExclusiveLock - def show_all_information(self, debug, json): + Args: + ports (list[int]): Ports to query for + debug_flag (bool): Verbose output flag + json_flag (bool): Dump as JSON flag + """ try: - portList = list(range(self.poe_plat.total_poe_port())) - data = collections.OrderedDict() - data[VERSIONS] = self.get_versions() - data[SYS_INFO] = self.get_system_running_state() - data[PORT_INFO] = self.get_ports_running_state(portList) - data[INDV_MASKS] = self.get_individual_masks() - if json: - self.json_output(data) + data = OrderedDict() + data[AgentConstants.PORT_INFO] = self.__get_ports_info(ports) + if json_flag: + print(json.dumps(data, indent=4)) else: - self.print_poe_version(data[VERSIONS]) - self.print_system_information(data[SYS_INFO], debug) - self.print_ports_information(data[PORT_INFO], debug) - self.print_indv_masks(data[INDV_MASKS]) + self.__print_ports_information(data[AgentConstants.PORT_INFO], debug_flag) except Exception as e: - print_stderr("Failed to show all information! (%s)" % str(e)) + self._log.exc(f"Failed to print the ports information: {str(e)}") - @PoeAccessExclusiveLock - def set_ports_enDis(self, portList, val): - try: - for port_id in portList: - poe_port = self.poe_plat.get_poe_port(port_id) - poe_port.set_enDis(val) - return True - except Exception as e: - print_stderr( - "Failed to set ports enable/disable! (%s)" % str(e)) - return False + def __show_individual_mask_regs(self, json_flag: bool) -> None: + """Print the individual mask registers - @PoeAccessExclusiveLock - def set_ports_powerLimit(self, portList, val): + Args: + json_flag (bool): Dump as JSON flag + """ try: - for port_id in portList: - poe_port = self.poe_plat.get_poe_port(port_id) - poe_port.set_powerLimit(val) - return True + data = OrderedDict() + data[AgentConstants.REG_MASKS] = self.__get_system_individual_mask_regs() + if json_flag: + print(json.dumps(data, indent=4)) + else: + self.__print_mask_registers(data[AgentConstants.REG_MASKS]) except Exception as e: - print_stderr("Failed to set ports power limit! (%s)" % str(e)) - return False + self._log.exc(f"Failed to print the individual registers: {str(e)}") - @PoeAccessExclusiveLock - def set_ports_priority(self, portList, val): - try: - for port_id in portList: - poe_port = self.poe_plat.get_poe_port(port_id) - poe_port.set_priority(val) - return True - except Exception as e: - print_stderr("Failed to set ports priority! (%s)" % str(e)) - return False + def __show_default_power_limits(self, json_flag: bool) -> None | NoReturn: + """Print the default class power limits - @PoeAccessExclusiveLock - def save_system_settings(self): + Args: + json_flag (bool): Dump as JSON flag + """ try: - self.poe_plat.save_system_settings() + data = OrderedDict() + data[AgentConstants.DEFAULT_LIMITS] = self.__get_default_limits() + if json_flag: + print(json.dumps(data, indent=4)) + else: + self.__print_default_limits(data[AgentConstants.DEFAULT_LIMITS]) except Exception as e: - print_stderr( - "Failed to save poe system settings! (%s)" % str(e)) + self._log.exc(f"Failed to print the default power limits: {str(e)}") - @PoeAccessExclusiveLock - def restore_factory_default(self): + def __show_all_information(self, debug_flag: bool, json_flag: bool) -> None: + """Print all information regarding versions, system, ports + and mask registers + + Args: + debug_flag (bool): Verbose output flag + json_flag (bool): Dump as JSON flag + """ try: - self.poe_plat.restore_factory_default() - self.poe_plat.init_poe() - print("Success to restore factory default and take platform poe settings!") + port_count = self._port_count + ports = list(range(port_count)) + if json_flag: + data = OrderedDict() + data[AgentConstants.VERSIONS] = self.__get_version_info() + data[AgentConstants.SYS_INFO] = self.__get_system_info() + data[AgentConstants.PORT_INFO] = self.__get_ports_info(ports) + data[AgentConstants.DEFAULT_LIMITS] = self.__get_default_limits() + data[AgentConstants.REG_MASKS] = self.__get_system_individual_mask_regs() + print(json.dumps(data, indent=4)) + else: + self.__show_versions(False) + self.__show_system_information(debug_flag, False) + self.__show_ports_information(ports, debug_flag, False) + self.__show_default_power_limits(False) + self.__show_individual_mask_regs(False) except Exception as e: - print_stderr( - "Failed to restore factory default! (%s)" % str(e)) + self._log.exc(f"Failed to print all PoE information: {str(e)}") - def get_current_time(self): - return datetime.now().strftime(self.TIME_FMT) + def __is_poed_alive(self) -> bool: + """Determine whether the PoE agent is still alive + through the PID file - def is_poed_alive(self): + Returns: + bool: True if still alive, False otherwise + """ try: - pid = int(open(POED_PID_PATH, 'r').read()) + pid = int(open(AgentConstants.POED_PID_PATH, "r").read()) os.kill(pid, 0) except OSError: return False - else: - return True - def send_ipc_event(self, action=POECLI_SET): - try: - with open(POE_IPC_EVT, "w") as f: - f.write(action) - except Exception as e: - pass + return True -def main(argv): + def __log_current_command(self) -> None: + """Log the current user command, TTY, working directory and user""" + if sys.stdin.isatty(): + tty = os.ttyname(sys.stdin.fileno()) + else: + tty = "unknown" + current_dir = os.getcwd() + user = getpass.getuser() + command = " ".join(sys.argv) + + self._log.dbg(f"Command executed: TTY={tty}; WD={current_dir}; " f"USER={user}; COMMAND={command}") + + def __set_handle_config_args(self, args, action_args): + action_args.append(AgentConstants.POECLI_CFG_CMD) + if args.save: + action_args.append(AgentConstants.POECLI_SAVE_CMD) + elif args.load: + action_args.append(AgentConstants.POECLI_LOAD_CMD) + + # Append the config file path, if given. + if args.config_file is not None: + action_args.append(args.config_file) + + def __set_default_limit_args(self, args, action_args): + """Change the default power limit by adding the class + and its limit to the action args to be processed by poed + + Args: + args (list): user Input list + action_args (list): IPC arguments list + """ + action_args.append(AgentConstants.POECLI_SET_DEFAULT_LIMIT_CMD) + action_args.append(args.default_limit[0]) + action_args.append(args.default_limit[1]) + + def execute(self) -> None | NoReturn: + """Run the main logic for executing a user command""" + + class CmdAction(Enum): + SHOW_PORT_CONFIG = 1 + SET_PORT_CONFIG = 2 + SET_LLDP_ENDIS = 3 + SET_DEFAULT_LIMIT = 4 + SET_CONFIG = 5 + + args = self._parser.parse_args() + self.__log_current_command() + action_args = [] + if args.subcmd == "show": + debug_flag: bool = args.debug + json_flag: bool = args.json + if args.ports: + self.__show_ports_information(args.ports, debug_flag, json_flag) + elif args.system: + self.__show_system_information(debug_flag, json_flag) + elif args.mask: + self.__show_individual_mask_regs(json_flag) + elif args.default_limits: + self.__show_default_power_limits(json_flag) + elif args.all: + self.__show_all_information(debug_flag, json_flag) + elif args.version: + self.__show_versions(json_flag) + elif args.subcmd == "set": + action_args.append(AgentConstants.POECLI_SET_CMD) + if args.ports is not None and args.default_limit is not None: + self._parser.error("Must not change port configuration and default power " "limits at the same time") + if args.ports: + if args.enable is None and args.level is None and args.power_limit is None and args.lldp is None: + self._parser.error(f"No action requested for {args.subcmd} command") + + action_details = OrderedDict() + # ports are zero-based indices here + ports_detail = [str(len(args.ports))] + ports_detail.extend(list(map(lambda p: str(p), args.ports))) + action_details["ports_detail"] = ports_detail + if args.enable is not None: + action_details[AgentConstants.POECLI_SET_PORT_ENDIS_CMD] = args.enable + if args.level is not None: + action_details[AgentConstants.POECLI_SET_PORT_PRIORITY_CMD] = args.level + if args.power_limit is not None: + action_details[AgentConstants.POECLI_SET_PORT_POWER_LIMIT_CMD] = args.power_limit + if args.lldp is not None: + action_details[AgentConstants.POECLI_SET_LLDP_ENDIS_CMD] = ( + AgentConstants.ENABLE if args.lldp else AgentConstants.DISABLE + ) + action_args.append(action_details) + elif args.default_limit: + # 60W is the maximum power limit for a Type 3 PSE. + # Supported PoE classes range from 1 to 4 (802.3af and at) + # and from 1 to 6 (802.3bt). + power_class, power_limit = (args.default_limit[0], args.default_limit[1]) + if self.bt_support and (args.default_limit[0] > 6 or args.default_limit[1] > 60): + self._parser.error("Invalid power class or value") + elif not self.bt_support and (args.default_limit[0] > 4 or args.default_limit[1] > 30): + self._parser.error("Invalid power class or value") + + self.__set_default_limit_args(args, action_args) + elif args.subcmd == "flush": + action_args.append(AgentConstants.POECLI_FLUSH_CMD) + elif args.subcmd == "factory-reset": + action_args.append(AgentConstants.POECLI_FACTORY_RESET_CMD) + elif args.subcmd == "config": + self.__set_handle_config_args(args, action_args) + + # Notify poed of the set operation, if the command went + # through. + if args.subcmd != "show": + poecli_request = json.dumps(action_args) + try: + reply = self.request_data_from_poed(poecli_request) + if reply == "success": + print("command excecuted successfully") + except Exception as e: + print(f"Command failed with exception: {e}") + + +def main() -> None: try: - poecli = PoeCLI() + cli = PoeCLI() + cli.execute() except Exception as e: - print_stderr("Failed to load poe platform! (%s)" % str(e)) - os._exit(-9) - - if wait_poed_busy() == False: - # POED is busy within 5s, BusyID=248 - os._exit(-8) - - parser = poecli._build_parser() - args = parser.parse_args() - cfg_action="" - set_flag = False - poed_alive = poecli.is_poed_alive() - if args.subcmd == "show": - if (args.ports is None and args.system is False and \ - args.all is False and args.mask is False and args.version is False): - parser.error("No action requested for %s command" % args.subcmd) - - debug_flag = args.debug - json_flag = args.json - if args.ports: - poecli.show_ports_information(args.ports, debug_flag, json_flag) - elif args.system: - poecli.show_system_information(debug_flag, json_flag) - elif args.mask: - poecli.show_individual_masks(json_flag) - elif args.all: - poecli.show_all_information(debug_flag, json_flag) - elif args.version: - poecli.show_versions(json_flag) - elif args.subcmd == "set": - if (args.enable is None and args.level is None and args.powerLimit is None): - parser.error("No action requested for %s command" % args.subcmd) - if args.enable is not None: - set_flag |= poecli.set_ports_enDis(args.ports, args.enable) - if args.level is not None: - set_flag |= poecli.set_ports_priority(args.ports, args.level) - if args.powerLimit is not None: - set_flag |= poecli.set_ports_powerLimit(args.ports, args.powerLimit) - - elif args.subcmd == "guide": - try: - with open(POE_USERGUIDE,'r') as f: - print_stderr(f.read()) - except Exception as e: - print_stderr("Unadle to open "+POE_USERGUIDE) - elif args.subcmd == "savechip": - poecli.save_system_settings() - set_flag = True - elif args.subcmd == "restore_poe_system": - poecli.restore_factory_default() - elif args.subcmd == "cfg": - if poed_alive: - cfg_action += POECLI_CFG+"," - if args.save: - cfg_action += POED_SAVE_ACTION+"," - if args.config is not None: - cfg_action += args.config+"," - elif args.load: - cfg_action += POED_LOAD_ACTION+"," - if args.config is not None: - cfg_action += args.config+"," - cfg_action = "".join(cfg_action.rsplit(",", 1)) - print("cfg_action: {0}".format(cfg_action)) - else: - print("Poe Agent not started, cfg operation will be ignore.") - - if set_flag == True and poed_alive == True: - poecli.send_ipc_event() - elif len(cfg_action)>0 and poed_alive == True: - poecli.send_ipc_event(cfg_action) - -if __name__ == '__main__': - main(sys.argv) + print(f"PoeCLI failed with exception: {e}") +if __name__ == "__main__": + main() diff --git a/dentos-poe-agent/opt/poeagent/bin/poed b/dentos-poe-agent/opt/poeagent/bin/poed index 0c3918b..b10dddb 100755 --- a/dentos-poe-agent/opt/poeagent/bin/poed +++ b/dentos-poe-agent/opt/poeagent/bin/poed @@ -4,9 +4,11 @@ BIN_PATH=$POE_ROOT/bin/ INC_PATH=$POE_ROOT/inc/ LIB_PATH=$POE_ROOT/lib/ DRIVERS_PATH=$POE_ROOT/drivers/ +PD69200_PATH=$POE_ROOT/drivers/pd69200 PLATFORMS_PATH=$POE_ROOT/platforms/ -export PYTHONPATH=$BIN_PATH:$INC_PATH:$LIB_PATH:$DRIVERS_PATH:$PLATFORMS_PATH +export PYTHONPATH=$POE_ROOT:$BIN_PATH:$INC_PATH:$LIB_PATH:$DRIVERS_PATH:$PD69200_PATH:$PLATFORMS_PATH cd $BIN_PATH -/usr/bin/python3 poed.py $@ + +/usr/bin/env python3 poed.py $@ diff --git a/dentos-poe-agent/opt/poeagent/bin/poed.py b/dentos-poe-agent/opt/poeagent/bin/poed.py index 97f76aa..8f2476d 100755 --- a/dentos-poe-agent/opt/poeagent/bin/poed.py +++ b/dentos-poe-agent/opt/poeagent/bin/poed.py @@ -13,563 +13,1706 @@ See the License for the specific language governing permissions and limitations under the License. ''' -from datetime import datetime, date -from collections import OrderedDict -from shutil import copyfile -from poe_common import * -from poe_version import * -from pathlib import Path +import errno +import json import os +import signal import sys -import errno import threading -import signal -import imp import time -import json -import fcntl -import binascii import traceback +from collections import OrderedDict +from concurrent import futures +from datetime import datetime, timedelta +from pathlib import Path +from shutil import copyfile +from typing import NoReturn + +import grpc +import poed_ipc_pb2 +import poed_ipc_pb2_grpc +from agent_constants import AgentConstants +from filelock import FileLock +from pd69200.poe_driver_def import ( + POE_PD69200_BT_MSG_DATA_LAYER2_REQ_EXECUTED, + POE_PD69200_BT_MSG_DATA_PORT_LAYER2_USAGE_L1, + POE_PD69200_BT_MSG_DATA_PORT_LAYER2_USAGE_LLDP, + POE_PD69200_BT_MSG_DATA_PORT_LAYER2_USAGE_OFF, + POE_PD69200_BT_MSG_DATA_PORT_PRIORITY_NO_CHANGE, + POE_PD69200_MSG_DATA_PORT_LAYER2_USAGE_L1, + TBL_BT_LAYER2_EXECUTION_TO_CFG, +) +from poe_common import * +from poe_log import PoeLog +from poe_platform import PoePlatform, PoePlatformFactory +from poe_telemetry import publish_metrics +from poe_version import POE_AGENT_VERSION, POE_CONFIG_VERSION +from singleton_thread_safe import SingletonThreadSafe +from tinyrpc.dispatch import RPCDispatcher +from tinyrpc.protocols.jsonrpc import FixedErrorMessageMixin, JSONRPCProtocol +from tinyrpc.server import RPCServer +from tinyrpc.transports.callback import CallbackServerTransport + +TIME_FMT = "%Y/%m/%d %H:%M:%S" +MAX_WORKER_THREADS = 10 +GRPC_STOP_NUM_SECS_TO_WAIT = 60 +NS_IN_S = 1000000000 + +# Global thread flag used for signaling program exit. +THREAD_FLAG: bool = True +# Lock for synchronising poecli, lldp-poed, and autosave threads +PoeClientLock = threading.Lock() + + +class JSONRpcInvalidPortIdError(FixedErrorMessageMixin, Exception): + jsonrpc_error_code = -42000 + message = "Invalid port ID" + + +class JSONRpcDriverError(FixedErrorMessageMixin, Exception): + jsonrpc_error_code = -42100 + message = "Unexpected driver error" + + +class JSONRpcInvalidPowerRequestError(FixedErrorMessageMixin, Exception): + jsonrpc_error_code = -42200 + message = "The port power request is invalid" + + +class JSONRpcInvalidOperationError(FixedErrorMessageMixin, Exception): + jsonrpc_error_code = -42300 + message = "Invalid operation for the given port" + + +class PoeConfigDao(object): + def __init__(self, cfg_path: str, plat_name: str, log=PoeLog()) -> None: + self._local_cfg_path: str = cfg_path + self._plat_name: str = plat_name + self._log: PoeLog = log + + @staticmethod + def is_time_sequence_increasing(t1: str, t2: str) -> bool: + """Check whether the input timestamps are strictly increasing + + Args: + t1 (str): First timestamp + t2 (str): Second timestamp + + Returns: + bool: True if the timestamps are strictly increasing, False + otherwise + """ -bootcmd_path = "/proc/cmdline" -pa_root_path = os.getcwd() + "/../" -plat_root_path = pa_root_path + "platforms" + return datetime.strptime(t2, TIME_FMT) > datetime.strptime(t1, TIME_FMT) -TIME_FMT = "%Y/%m/%d %H:%M:%S" + @property + def local_cfg_path(self) -> str: + return self._local_cfg_path -thread_flag = True + @local_cfg_path.setter + def local_cfg_path(self, local_cfg_path: str) -> None: + """Set the local configuration file path, by first checking if the + parent folder exists -class PoeAgentState(object): - CLEAN_START = 0 - UNCLEAN_START = 1 + Args: + local_cfg_path (str): Path to the local config + """ + Path(local_cfg_path.rsplit("/", 1)[0]).mkdir(True, True) + self._local_cfg_path = local_cfg_path -class PoeConfig(object): - def __init__(self, cfg_path, plat_name): - self._path = cfg_path - self.plat_name = plat_name - self.root_path = self.path().rsplit("/", 1)[0] - self.create_dir(self.root_path) + def __is_valid_cfg_platform(self, cfg_plat: str) -> bool: + """Compare the local platform name with the one read from the config + file - def path(self): - return self._path + Args: + cfg_plat (str): Platform string - def create_dir(self, path): - if self.is_exist(path) != True: - os.mkdir(path) + Returns: + bool: True if the platform names match, False otherwise + """ + return cfg_plat == self._plat_name - def is_exist(self, path=None): - if path is None: - path = self.path() - return os.path.exists(path) + def __is_valid_poe_agt_ver(self, agt_ver: str) -> bool: + """Compare the runtime PoE agent major version with the one read from + the local config - def is_valid_cfg_platform(self, cfg_plat): - return cfg_plat == self.plat_name + Args: + agt_ver (str): Agent version string - def is_valid_poe_agt_ver(self, agt_ver): - maj_ver_cfg = agt_ver.split('.')[0] - maj_ver_def = POE_AGENT_VERSION.split('.')[0] + Returns: + bool: True if both major versions match, False otherwise + """ + maj_ver_cfg = agt_ver.split(".")[0] + maj_ver_def = POE_AGENT_VERSION.split(".")[0] return maj_ver_cfg == maj_ver_def - def is_valid_poe_cfg_ver(self, cfg_ver): - maj_ver_cfg = cfg_ver.split('.')[0] - maj_ver_def = POE_CONFIG_VERSION.split('.')[0] + def __is_valid_poe_cfg_ver(self, cfg_ver: str) -> bool: + """Compare the runtime PoE config major version with the one read from + the config file + + Args: + cfg_ver (str): Config version string + + Returns: + bool: True if both major versions match, False otherwise + """ + maj_ver_cfg = cfg_ver.split(".")[0] + maj_ver_def = POE_CONFIG_VERSION.split(".")[0] return maj_ver_cfg == maj_ver_def - def is_valid_gen_info(self, gen_info): - return self.is_valid_cfg_platform(gen_info[PLATFORM]) and \ - self.is_valid_poe_agt_ver(gen_info[POE_AGT_VER]) and \ - self.is_valid_poe_cfg_ver(gen_info[POE_CFG_VER]) - - def is_increasing_time_sequence(self, t1, t2): - tDelta = datetime.strptime(t2, TIME_FMT) - \ - datetime.strptime(t1, TIME_FMT) - result1 =(tDelta.days > 0 or tDelta.seconds > 0) - result2 =(tDelta.days * tDelta.seconds) >= 0 - # print_stderr("is_increasing_time_sequence(self, t1, t2): result1={0},result2={1} ".format(str(result1), - # str(result2))) - return result1 and result2 - def is_valid_timestamp(self, timestamp): - last_save_time = timestamp[LAST_SAVE_TIME] - last_set_time = timestamp[LAST_SET_TIME] - result_is_increasing_time_sequence = self.is_increasing_time_sequence(str(last_set_time), str(last_save_time)) - # print_stderr( - # "is_valid_timestamp(self, timestamp): result_is_increasing_time_sequence={0}".format(str(result_is_increasing_time_sequence))) - return result_is_increasing_time_sequence - - def is_valid_data(self, data): - result_is_valid_gen_info =self.is_valid_gen_info(data[GEN_INFO]) - result_is_valid_timestamp =self.is_valid_timestamp(data[TIMESTAMP]) - # print_stderr("is_valid_data(self, data): result_is_valid_gen_info={0},result_is_valid_timestamp={1}".format(str(result_is_valid_gen_info),str(result_is_valid_timestamp))) - return result_is_valid_gen_info and result_is_valid_timestamp - - - def is_valid(self): - result_is_exist = self.is_exist() - result_is_valid_data = False - if result_is_exist: - result_is_valid_data = self.is_valid_data(self.load()) - # print_stderr("is_valid(self): result_is_exist={0},result_is_valid_data={1}".format(str(result_is_exist), - # str(result_is_valid_data))) - return result_is_exist and result_is_valid_data - - def save(self, data): - json_data = json.dumps(data, indent = 4) - with open(self.path(), 'w') as f: - f.write(json_data) - return True + def __is_valid_gen_info(self, gen_info: dict) -> bool: + """Check whether the loaded information is valid or not + + Args: + gen_info (dict): Information regarding platform, agent and config + versions + + Returns: + bool: True if the information is valid, False otherwise + """ + return ( + self.__is_valid_cfg_platform(gen_info[AgentConstants.PLATFORM]) + and self.__is_valid_poe_agt_ver(gen_info[AgentConstants.POE_AGT_VER]) + and self.__is_valid_poe_cfg_ver(gen_info[AgentConstants.POE_CFG_VER]) + ) + + def __is_valid_timestamp(self, timestamp: dict) -> bool: + """Check whether the config timestamp is valid or not + + Args: + timestamp (dict): Config file last saved/set timestamp + + Returns: + bool: True if the timestamps are increasing, False otherwise + """ + last_save_time = timestamp[AgentConstants.LAST_SAVE_TIME] + last_set_time = timestamp[AgentConstants.LAST_SET_TIME] + + return self.is_time_sequence_increasing(str(last_set_time), str(last_save_time)) + + def is_config_valid(self, config: OrderedDict) -> bool: + """Check whether the given config metadata is valid + + Args: + config (dict): Read config + + Returns: + bool: True if the metadata is valid, False otherwise + """ + return self.__is_valid_gen_info(config[AgentConstants.GEN_INFO]) and self.__is_valid_timestamp( + config[AgentConstants.TIMESTAMP] + ) + + def lazy_is_valid(self) -> bool: + """Check if the local configuration file is valid or not + by lazy loading + + Returns: + bool: True if the path exists and the configuration metadata is + valid + """ + try: + file_cfg = self.load() + if Path(self.local_cfg_path).exists() and file_cfg is not None: + return self.is_config_valid(file_cfg) + except Exception as e: + self._log.err("Unexpected error when reading the config from " f"{self.local_cfg_path}: {e}") + return False - def load(self): + def save(self, config: OrderedDict) -> bool: + """Persist the input config to the local config file as JSON + + Args: + data (OrderedDict): Config to save + + Returns: + bool: True if successful, False otherwise + """ + json_config = "" try: - with open(self.path(), 'r') as f: - read_buf = f.read() - if len(read_buf) > 1: - return json.loads(read_buf) - return None - except Exception as e: - raise RuntimeError("Load json failed: {0}".format(str(e))) - -class PoeAgent(object): - UNIX_START_TIME = "1970/01/01 0:0:0" - - def __init__(self): - self.log = PoeLog() - self.plat_name = self.platform_model() - self.poe_plat = self.load_poe_plat() - self.plat_supported = self.is_valid_plat(self.poe_plat) - self.poe_agent_state = PoeAgentState.CLEAN_START - - self.system_state = None - self.all_port_state = None - self.last_cfg_save_time = self.UNIX_START_TIME - self.prev_poe_set_time = self.UNIX_START_TIME - self.last_poe_set_time = self.UNIX_START_TIME - self.last_power_bank = 0 - self.cfg_serial_num = 0 - - self.runtime_cfg = PoeConfig(POED_RUNTIME_CFG_PATH, - self.plat_name) - self.permanent_cfg = PoeConfig(POED_PERM_CFG_PATH, - self.plat_name) - self.cfg_update_intvl_rt = 4 - self.cfg_update_intvl_perm = 30 - self.cfg_load_retry = 3 - self.rt_counter = 0 - self.fail_counter = 0 - self.autosave_intvl = 1 - self.autosave_thread = threading.Thread(target=self.autosave_main) - self.failsafe_flag=False - - # Get platform model from boot cmd - def platform_model(self, file_path=bootcmd_path): + json_config = json.dumps(config, indent=4) + with open(self.local_cfg_path, "w") as f: + f.write(json_config) + return True + except IOError: + self._log.err("Failed to persist the configuration to " f"{self.local_cfg_path}") + self._log.dbg(json_config) + + return False + + def load(self) -> OrderedDict | None: + """Load and parse the local configuration JSON file + + Returns: + OrderedDict | None: Parsed config as a dictionary, if successful + """ try: - with open(file_path, 'r') as f: - d = dict(i.split('=') for i in f.read().split(' ')) - return d.get("onl_platform").rstrip() + with open(self.local_cfg_path, "r") as f: + raw_json = f.read() + if raw_json: + return json.loads(raw_json, object_pairs_hook=OrderedDict) + except IOError: + self._log.err(f"Failed to load the local configuration at " f"{self.local_cfg_path}") + + return None + + +class PoedServicer(poed_ipc_pb2_grpc.PoeIpcServicer): + """ + PoedServicer is gRPC server for poed and serves all request coming from + PoeCLI and lldp-poed. As of now it serves the requests from PoeCLI + """ + + def __init__(self, grpc_callback_handler): + self._grpc_callback = grpc_callback_handler + + def HandlePoecli(self, request, context): + """ + Handles the requests coming from PoeCLI + """ + args = request.request + poecli_reply = poed_ipc_pb2.PoecliReply() + poecli_reply.reply = self._grpc_callback(args, "poecli") + return poecli_reply + + +def respose_required_sec(delay): + """ + Expect response in `delay` seconds. + + A call to `respose_received` is expected in less then `delay` seconds. + If respose_received not called in time, `alarm_handler` will be + called and the process terminated. + + :param delay: Delay to be expected in seconds. + :rtype: None + """ + signal.alarm(delay) + + +def respose_received(): + """ + Response received to confirm a `respose_required_sec` call. + + Ending with success any previous alarm. + + :rtype: None + """ + signal.alarm(0) + + +class PoeAgent(object, metaclass=SingletonThreadSafe): + """poed implementation + The PoE daemon takes care of keeping in sync and saving/loading the PoE + user configuration. The runtime configuration is persisted periodically to + a local file, which can be later loaded in case of a system or agent + restart. The user can also trigger a manual load from file or flushing the + current settings to the PoE chipset through the CLI. + We'll also listen for incoming requests from lldp-poed and try to honor + them as they come, using a named pipe, as the transport mechanism, and + JSON-RPC as the underlying protocol. + + Note: synchronized access to the PoE settings is necessary, because both + the CLI and the daemon have write-through access to the PoE system. + """ + + def __init__(self) -> None: + global THREAD_FLAG + self._log: PoeLog = PoeLog() + + # First get a valid HAL and platform name. + hal, plat_name = PoePlatformFactory.create_platform_from_bootcmd(AgentConstants.BOOTCMD_PATH) + if hal is None or plat_name is None: + self._log.err("Current platform is not supported or " "cannot be initialized") + self._log.err("Poed will now exit !!!") + poed_exit(EXIT_CODES.HAL_INIT_FAILED) + if plat_name is None: + raise AssertionError("Platform name cannot be empty") + if hal is None: + raise AssertionError("Platform HAL must not be None") + self._plat_name: str = plat_name + self._hal: PoePlatform = hal + + self._runtime_cfg: PoeConfigDao = PoeConfigDao(AgentConstants.POED_RUNTIME_CFG_PATH, plat_name, log=self._log) + self._permanent_cfg: PoeConfigDao = PoeConfigDao(AgentConstants.POED_PERM_CFG_PATH, plat_name, log=self._log) + + # Cache the current ports configuration, update only if there was a + # change. By default, LLDP processing is enabled for all the ports. + self._ports_config: list | None = None + self._default_power_limits: dict[int, int] = self._hal.default_power_limits + + # Config timestamp initial placeholder. + unix_start_time = "1970/01/01 0:0:0" + # Must be updated every time a new save is done. + self._last_save_time = unix_start_time + # Used for checking the updates sanity. + self._prev_set_time = unix_start_time + self._last_set_time = unix_start_time + self._last_bank_type = None + self._cfg_serial_num = 0 + + # Local intervals (in seconds). + self._autosave_wait_interval_s = 60 + + self._cfg_load_max_retry = 3 + + self._autosave_thread = threading.Thread(target=self.__handle_autosave) + self._lldp_poe_thread = threading.Thread(target=self.__handle_lldp_poed) + + # Used to avoid persisting failsafe config. + self._failsafe_flag = False + + self._rpc_dispatcher = RPCDispatcher() + + # Ensure the metrics FIFO is already created. + if not os.path.exists(AgentConstants.POE_METRICS_FIFO_FOLDER): + os.makedirs(AgentConstants.POE_METRICS_FIFO_FOLDER, 755) + self.__create_fifo(AgentConstants.POE_METRICS_FIFO_PATH) + + @PoeAccessExclusiveLock + def __get_current_bank_source(self) -> int | None: + """Query the PoE driver to get the current power source type + + Returns: + int | None: The source type + """ + try: + return self._hal.get_current_power_bank() except Exception as e: - self.log.alert("Failed to get model name. err: %s" % str(e)) - return "Unknown" + self._log.exc(f"Failed to get the system power bank: {str(e)}") - def platform_src_path(self): + @PoeAccessExclusiveLock + def __get_system_running_state(self) -> dict | None: + """Query the PoE driver to get the current system power state + + Returns: + dict | None: The system running state + """ try: - # dentOS platform format: --- - [arch, manufacturer, model_revision] = self.plat_name.split('-', 2) - return "/".join([plat_root_path, manufacturer, - model_revision, "poe_platform.py"]) + return self._hal.get_system_information(False) except Exception as e: - self.log.alert("Failed to get platform path. err: %s" % str(e)) - return "" + self._log.exc(f"Failed to get the system running state: {str(e)}") + + @PoeAccessExclusiveLock + def __get_ports_running_config(self) -> list[OrderedDict] | None: + """Query the PoE driver to get all ports status - def load_poe_plat(self): - poe_plat = None + Returns: + list[OrderedDict] | None: Ports power info + """ try: - plat_src = imp.load_source("poe_plat", self.platform_src_path()) - poe_plat = plat_src.get_poe_platform() + ports = list(range(self._hal.port_count())) + return self._hal.get_ports_status(ports, False, False) except Exception as e: - self.log.alert("Failed to load PoE platform. err: %s" % str(e)) - return poe_plat + self._log.exc(f"Failed to get the ports running state: {str(e)}") + + def __has_psu_changes(self) -> bool: + """Check if there is a new PSU event, by detecting a source type change + + Returns: + bool: True if a change is detected, False otherwise + """ + current_bank_source = self.__get_current_bank_source() + if self._last_bank_type != current_bank_source: + self._last_bank_type = current_bank_source + self._log.dbg( + "New power supply parameters: \n" + json.dumps(self._hal.get_power_supply_params(), ensure_ascii=True) + ) + return True + + return False - def is_valid_plat(self, poe_plat): - return poe_plat is not None + def __has_config_changes(self) -> bool: + """Check if the configuration changed, based on the set timestamps + Update the previous timestamp, if there is a new change. - def have_set_event(self): - if self.runtime_cfg.is_increasing_time_sequence(self.prev_poe_set_time, - self.last_poe_set_time): - self.prev_poe_set_time = self.last_poe_set_time + Returns: + bool: True if the last set time is greater than the previous one, + False otherwise + """ + if PoeConfigDao.is_time_sequence_increasing(self._prev_set_time, self._last_set_time): + self._prev_set_time = self._last_set_time return True + return False - def get_system_power_bank(self): + def __has_state_changes(self) -> bool: + """Check if there are config changes or PSU changes + + Returns: + bool: True if detected changes, False otherwise + """ + return self.__has_config_changes() or self.__has_psu_changes() + + def __get_current_time(self) -> str: + """Get the current time based on the predefined format + + Returns: + str: Current time as a string + """ + return datetime.now().strftime(TIME_FMT) + + def __update_last_set_time(self) -> None: + """Update the last config set timestamp with the current time + This one may be called whenever there is a new CLI event marking that + the PoE config was changed by the user. + """ + with PoeClientLock: + self._last_set_time = self.__get_current_time() + + @PoeAccessExclusiveLock + def __init_platform(self, skip_port_init: bool = False) -> bool: + """Initialize the PoE chipset through the HAL. This will also + update the last set timestamp + + Args: + skip_port_init (bool, optional): Whether to preserve the current + port matrix or not. Defaults to False + + Returns: + bool: True if init was successful, False otherwise + """ try: - return self.poe_plat.get_current_power_bank() - except Exception as e: - self.log.err("Failed to get system power bank: %s" % str(e)) - return None + result = self._hal.init_poe(skip_port_init) + if not has_any_op_failed(result): + self._log.info("PoE chipset initialized successfully") + else: + self._log.info("Failed to initialize the PoE chipset: " f"{json.dumps(result, ensure_ascii=True)}") + return False - def have_psu_event(self): - cur_power_bank = self.get_system_power_bank() - if self.last_power_bank != cur_power_bank: - self.last_power_bank = cur_power_bank return True + except Exception as e: + self._log.exc(f"Failed to initialize the PoE chipset: {str(e)}") + return False - def is_state_changes(self): - return self.have_set_event() or self.have_psu_event() + def __collect_general_info(self) -> OrderedDict: + """Build a dictionary containing the platform details and the + agent metadata (agent and config versions) + + Returns: + OrderedDict: General info + """ + info = OrderedDict() + info[AgentConstants.PLATFORM] = self._plat_name + info[AgentConstants.POE_AGT_VER] = POE_AGENT_VERSION + info[AgentConstants.POE_CFG_VER] = POE_CONFIG_VERSION + info[AgentConstants.CFG_SERIAL_NUM] = self._cfg_serial_num + return info + + def __collect_running_config(self, update_time: bool = False) -> OrderedDict | None: + """Build a dictionary containing the current running PoE config + + Returns: + OrderedDict | None: The config, if successful + """ + try: + if self.__has_state_changes(): + # Update the port configuration, preserving the LLDP admin + # endis state. + self._log.dbg("Detected port config changes. " "Will refresh the configuration") + with PoeClientLock: + if self._ports_config is None: + raise AssertionError("Ports config must not be None") + lldp_endis = { + port[PORT_ID]: port[AgentConstants.LLDP_ENDIS] for port in self._ports_config + } + self._ports_config = self.__get_ports_running_config() + if self._ports_config is None: + raise AssertionError("Ports config must not be None") + self._ports_config = [ + {**port, AgentConstants.LLDP_ENDIS: lldp_endis[port[PORT_ID]]} + for port in self._ports_config + ] + + with PoeClientLock: + config = OrderedDict() + config[AgentConstants.PORT_CONFIGS] = self._ports_config + config[AgentConstants.DEFAULT_LIMITS] = self._default_power_limits + config[AgentConstants.GEN_INFO] = self.__collect_general_info() + if update_time: + config[AgentConstants.GEN_INFO][AgentConstants.CFG_SERIAL_NUM] += 1 + config[AgentConstants.TIMESTAMP] = OrderedDict( + { + AgentConstants.LAST_SAVE_TIME: (self.__get_current_time() if update_time else self._last_save_time), + AgentConstants.LAST_SET_TIME: self._last_set_time, + } + ) + config[AgentConstants.SYS_INFO] = self.__get_system_running_state() + return config + except Exception as e: + self._log.exc(f"Failed to collect the running configuration: {str(e)}") - def get_system_running_state(self): + return None + + def __persist_running_config(self) -> bool: + """Collect the PoE running configuration and persist it to the runtime + config local file + The configuration will be saved only if it's valid first. + + Returns: + bool: True if successful, False otherwise + """ try: - return self.poe_plat.get_system_information(False) + running_config = self.__collect_running_config(update_time=True) + if running_config is None: + raise AssertionError("Running config must not be None") + if not self._runtime_cfg.is_config_valid(running_config): + self._log.err("The current active config is invalid. Cannot persist it") + self._log.dbg(f"{json.dumps(running_config, ensure_ascii=True)}") + return False + + if self._runtime_cfg.save(running_config): + # Update the runtime serial number and timestamp, if + # successful, for further checks. + self._last_save_time = running_config[AgentConstants.TIMESTAMP][AgentConstants.LAST_SAVE_TIME] + self._cfg_serial_num = running_config[AgentConstants.GEN_INFO][AgentConstants.CFG_SERIAL_NUM] + self._log.dbg("Successfully persisted: " f"{json.dumps(running_config, ensure_ascii=True)}") + return True except Exception as e: - self.log.err("Failed to get system running state: %s" % str(e)) - raise e + self._log.exc(f"Failed to persist the running config: {str(e)}") + + return False - def get_ports_running_state(self): + def __save_config_to_permanent_file(self, out_path: str) -> None: + """Copy the runtime configuration to the given config path + This will ensure that all the parents are created or already exist. + + Args: + out_path (str): Output file path + """ + if self._runtime_cfg.lazy_is_valid(): + with PoeClientLock: + Path(out_path).parent.mkdir(exist_ok=True, parents=True) + copyfile(self._runtime_cfg.local_cfg_path, out_path) + else: + self._log.err("Runtime configuration is invalid. " "Will not save it") + + def __create_fifo(self, fifo_path: str) -> None: + """Check if the FIFO already exists and if it's a FIFO. If it doesn't, + create it and set read and write permissions. + + Args: + fifo_path (str): Path to FIFO + """ try: - portList = list(range(self.poe_plat.total_poe_port())) - return self.poe_plat.get_ports_information(portList, False) + if Path(fifo_path).exists() and not Path(fifo_path).is_fifo(): + os.remove(Path(fifo_path).as_posix()) + if not Path(fifo_path).exists(): + Path(fifo_path).parent.mkdir(exist_ok=True, parents=True) + os.mkfifo(Path(fifo_path).as_posix()) + os.chmod(Path(fifo_path).as_posix(), 0o664) except Exception as e: - self.log.err("Failed to get ports running state: %s" % str(e)) - raise e + self._log.exc(f"Failed to create the FIFO: {str(e)}") + poed_exit(ret_code=EXIT_CODES.CREATE_FIFO_FAILED) + + def get_port_count(self): + return str(self._hal.port_count()) + + def get_bt_support(self): + return "1" if self._hal._bt_support else "0" + + def get_default_power_limits(self): + """Get the default power limits + + Returns: + str: default power limits + """ + with PoeClientLock: + ret_val = json.dumps(self._default_power_limits, separators=(",", ":")) + return ret_val + + def get_ports_lldp_endis(self, args: list): + port_count = int(args[2]) + start, end = 3, 3 + port_count + + # If the ports were not initialized yet, return enabled + # by default. + with PoeClientLock: + if self._ports_config is None: + return ",".join([AgentConstants.ENABLE] * port_count) + # The LLDP endis should be returned in the same order + # as the ports were given in. + status = args[start:end] + status_idx, ports_idx = 0, 0 + with PoeClientLock: + while status_idx < port_count: + # The port range may be sparse, and in the happy scenario + # it is contiguous as the _ports_config. + while int(status[status_idx]) > self._ports_config[ports_idx][PORT_ID]: + ports_idx += 1 + status[status_idx] = self._ports_config[ports_idx][AgentConstants.LLDP_ENDIS] + status_idx += 1 + return ",".join(status) + + @PoeAccessExclusiveLock + def get_ports_info(self, args: list): + # hal need ports zero-based indecis + ports = [int(val) - 1 for val in args[3:]] + lldp_endis = self.get_ports_lldp_endis(args).split(",") + + ports_status: list[dict] = [ + {**port, AgentConstants.LLDP_ENDIS: endis_value} + for port, endis_value in ( + zip(self._hal.get_ports_status(ports, more_info=True, log_port_status=False), lldp_endis) + ) + ] + + return json.dumps(ports_status, separators=(",", ":")) @PoeAccessExclusiveLock - def init_platform(self,cfg_data=None): - result = dict({}) - all_result=None + def get_versions_info(self): + versions = OrderedDict() + versions[SW_VERSION] = self._hal.get_poe_versions() + versions[AgentConstants.POE_AGT_VER] = POE_AGENT_VERSION + versions[AgentConstants.POE_CFG_VER] = POE_CONFIG_VERSION + + return json.dumps(versions, separators=(",", ":")) + + @PoeAccessExclusiveLock + def get_system_info(self): + """get verbose system info from hal + + Returns: + str: System info + """ + sys_info = self._hal.get_system_information(verbose=True) + return json.dumps(sys_info, separators=(",", ":")) + + @PoeAccessExclusiveLock + def get_individual_mask_registers(self): + """Get all individual mask registers + Refer to the "MASK Registers List" chapter for further info. + + Returns: + str: Mask values + """ + # This includes the 802.3bt mask keys. + masks = list(range(0x54)) + result = OrderedDict() + for mask in masks: + reg_value = self._hal.get_individual_mask_regs(mask).get(ENDIS) + result[f"0x{mask:<02x}"] = reg_value + + return json.dumps(result, separators=(",", ":")) + + @PoeAccessExclusiveLock + def __set_port_endis(self, enable: bool, ports_detail: list): + """Set enable/disable for the given ports + + Args: + ports (list[int]): Ports to change + enable (bool): True if enabling, False otherwise + + Returns: + bool: True if successful, False otherwise + """ + ports = [int(val) for val in ports_detail[1:]] + try: - result = self.poe_plat.init_poe(cfg_data) - all_result = check_init_plat_ret_result(result) - if all_result[1] == 0: - self.log.info( - "init_poe all_result: {0}".format(str(all_result[1]))) - else: - self.log.info( - "init_poe all_result(some command failed): {0}".format(str(all_result))) - return False + for port_id in ports: + port = self._hal.get_poe_port(port_id) + port.set_en_dis(enable) + return True + except Exception as e: + self._log.exc(f"Failed to set enable/disable: {str(e)}") + + return False - self.update_set_time() + @PoeAccessExclusiveLock + def __set_port_priority(self, priority: int, ports_detail: list): + """Set a new port priority for the given ports + + Args: + ports (list[str]): Ports to change + priority (int): New port priority + + Returns: + bool: True if successful, False otherwise + """ + ports = [int(val) for val in ports_detail[1:]] + try: + for port_id in ports: + port = self._hal.get_poe_port(port_id) + port.set_priority(priority) return True except Exception as e: - self.log.err( - "An exception when initializing poe chip: {0}".format(str(e))) - return False + self._log.exc(f"Failed to set port priority: {str(e)}") + return False + + @PoeAccessExclusiveLock + def __set_port_power_limit(self, power_limit: int, ports_detail: list): + """Set a new power limit for the given ports - def collect_general_info(self): - gen_info = OrderedDict() - gen_info[PLATFORM] = self.plat_name - gen_info[POE_AGT_VER] = POE_AGENT_VERSION - gen_info[POE_CFG_VER] = POE_CONFIG_VERSION - gen_info[CFG_SERIAL_NUM] = self.cfg_serial_num + 1 - return gen_info + Args: + ports (list[str]): Ports to change + limit (int): New power limit - def get_current_time(self): - return datetime.now().strftime(TIME_FMT) + Returns: + bool: True if successful, False otherwise + """ + ports = [int(val) for val in ports_detail[1:]] + try: + for port_id in ports: + port = self._hal.get_poe_port(port_id) + port.set_power_limit(power_limit) + return True + except Exception as e: + self._log.exc(f"Failed to set the power limit: {str(e)}") - def update_set_time(self): - self.last_poe_set_time = self.get_current_time() + return False + + def __set_lldp_endis(self, endis_value: str, port_details: list): + """Set a lldp enable/disable for the given ports + + Args: + ports (list[str]): Ports to change + + Returns: + bool: True if successful, False otherwise + """ + start, end = 1, 1 + int(port_details[0]) + # User-facing values are one-based + ports = [int(val) + 1 for val in port_details[start:end]] + ports_to_change = set(map(int, ports)) + with PoeClientLock: + for port in self._ports_config or []: + if port[PORT_ID] in ports_to_change: + port[AgentConstants.LLDP_ENDIS] = endis_value + ports_to_change.remove(port[PORT_ID]) + + if not ports_to_change: + break + return True + + def __set_default_power_limit(self, args: list): + """Set default power limit - def collect_timestamp(self): - time_stamp = OrderedDict() - time_stamp[LAST_SAVE_TIME] = self.get_current_time() - time_stamp[LAST_SET_TIME] = self.last_poe_set_time - return time_stamp + Args: + args (list) : class, default value + + Returns: + bool: True if successful, False otherwise + """ + power_class, power_limit = int(args[2]), int(args[3]) + with PoeClientLock: + if power_limit: + self._default_power_limits[power_class] = power_limit + elif power_class in self._default_power_limits and not power_limit: + del self._default_power_limits[power_class] + + return True @PoeAccessExclusiveLock - def collect_running_state(self): + def __reset_to_factory_defaults(self): + """Reset the chipset to the factory defaults by re-running the HAL init + + Returns: + bool: True if successful, False otherwise + """ try: - if self.is_state_changes() == True: - self.all_port_state = self.get_ports_running_state() - self.system_state = self.get_system_running_state() - - cur_state = OrderedDict() - cur_state[GEN_INFO] = self.collect_general_info() - cur_state[TIMESTAMP] = self.collect_timestamp() - cur_state[SYS_INFO] = self.system_state - cur_state[PORT_CONFIGS] = self.all_port_state - return cur_state + self._hal.restore_factory_defaults() + if self._hal.init_poe(skip_port_init=False): + self._log.notice("Successfully restored factory defaults") + return True except Exception as e: - self.log.err("Failed to collect running state!") - return None + self._log.exc(f"Failed to restore factory defaults: {str(e)}") + + return False - def save_poe_cfg(self, poe_cfg, cfg_data=None): + @PoeAccessExclusiveLock + def __handle_flush_settings_command(self): + """Flush the current settings to the PoE chipset non-volatile memory.""" try: - if poe_cfg.is_valid_data(cfg_data) == False: - self.log.warn("Get invalid cfg data to save!") - return False + self._hal.save_system_settings() + except Exception as e: + self._log.exc(f"Failed to flush the system settings: {str(e)}") - if poe_cfg.save(cfg_data) == True: - self.last_cfg_save_time = cfg_data[TIMESTAMP][LAST_SAVE_TIME] - self.cfg_serial_num = cfg_data[GEN_INFO][CFG_SERIAL_NUM] - return True + def __handle_config_command(self, args: list): + user_file = None + try: + _, action, user_file = args + except ValueError: + _, action = args + + self._log.dbg(f"Config action: {action}") + if len(args) >= 2: + self._log.dbg(f"Config user file: {user_file}") + + try: + if action == AgentConstants.POECLI_SAVE_CMD: + if user_file is None: + self._log.info("Saving persisted config to the " "permanent config file") + self.__save_config_to_permanent_file(self._permanent_cfg.local_cfg_path) + else: + self._log.info(f"Saving persisted config to: {user_file}") + self.__save_config_to_permanent_file(user_file) + elif action == AgentConstants.POECLI_LOAD_CMD: + if user_file is None: + self._log.info("Loading config from the permanent config file") + self.__apply_poe_config(self._permanent_cfg) + else: + self._log.info(f"Loading config from: {user_file}") + user_cfg = PoeConfigDao(user_file, self._plat_name, self._log) + self.__apply_poe_config(user_cfg) + else: + self._log.exc(f"Failed to handle config command: {str(e)}") + return False + return True except Exception as e: - self.log.err("An exception to save poe cfg: %s" % str(e)) + self._log.exc(f"Exception handleing config command: {str(e)}") + return False + def __handle_poecli(self, args: str): + reply = "success" + try: + args = json.loads(args) + + if args[0] == AgentConstants.POECLI_GET_PORT_COUNT: + self._log.info(f"Received a get command from poecli: {args}") + reply = self.get_port_count() + elif args[0] == AgentConstants.POECLI_GET_BT_SUPPORT: + self._log.info(f"Received a get command from poecli: {args}") + reply = self.get_bt_support() + elif args[0] == AgentConstants.POECLI_SHOW_CMD: + if args[1] == AgentConstants.POECLI_GET_PORTS_INFO_CMD: + self._log.info(f"Received a get command from poecli: {args}") + reply = self.get_ports_info(args) + elif args[1] == AgentConstants.POECLI_GET_SYSTEM_INFO_CMD: + self._log.info(f"Received a get command from poecli: {args}") + reply = self.get_system_info() + elif args[1] == AgentConstants.POECLI_GET_MASK_REGS_CMD: + self._log.info(f"Received a get command from poecli: {args}") + reply = self.get_individual_mask_registers() + elif args[1] == AgentConstants.POECLI_GET_DEFAULT_LIMITS_CMD: + self._log.info(f"Received a get command from poecli: {args}") + reply = self.get_default_power_limits() + elif args[1] == AgentConstants.POECLI_GET_VERSIONS_INFO_CMD: + self._log.info(f"Received a get command from poecli: {args}") + reply = self.get_versions_info() + elif args[0] == AgentConstants.POECLI_SET_CMD: + self._log.info(f"Received a set command from poecli: {args}") + # The second argument is the type of the subcommand + # and the third one is the number of ports. + next_arg = args[1] + if next_arg == AgentConstants.POECLI_SET_DEFAULT_LIMIT_CMD: + self.__set_default_power_limit(args) + elif "ports_detail" in next_arg: + port_details = next_arg["ports_detail"] + if AgentConstants.POECLI_SET_PORT_ENDIS_CMD in next_arg: + self.__set_port_endis(next_arg[AgentConstants.POECLI_SET_PORT_ENDIS_CMD], port_details) + if AgentConstants.POECLI_SET_LLDP_ENDIS_CMD in next_arg: + self.__set_lldp_endis(next_arg[AgentConstants.POECLI_SET_LLDP_ENDIS_CMD], port_details) + if AgentConstants.POECLI_SET_PORT_PRIORITY_CMD in next_arg: + self.__set_port_priority(next_arg[AgentConstants.POECLI_SET_PORT_PRIORITY_CMD], port_details) + if AgentConstants.POECLI_SET_PORT_POWER_LIMIT_CMD in next_arg: + self.__set_port_power_limit( + next_arg[AgentConstants.POECLI_SET_PORT_POWER_LIMIT_CMD], port_details + ) + else: + self._log.err(f"Unknown poecli IPC set subcommand: {args}") + self.__update_last_set_time() + elif args[0] == AgentConstants.POECLI_FACTORY_RESET_CMD: + self._log.info("Received a factory reset command " f"from poecli: {args}") + if self.__reset_to_factory_defaults(): + self.__update_last_set_time() + elif args[0] == AgentConstants.POECLI_FLUSH_CMD: + self._log.info("Received a flush settings command " f"from poecli: {args}") + self.__handle_flush_settings_command() + elif args[0] == AgentConstants.POECLI_CFG_CMD: + self._log.info("Received a config command " f"from poecli: {args}") + self.__handle_config_command(args) + else: + self._log.err(f"Unknown poecli IPC command: {args[0]}") + except Exception as e: + self._log.exc(f"An exception occurred while serving poecli request: {str(e)}") + reply = "failed" + return reply + + + def grpc_callback_handler(self, args: str, requester: str): + global THREAD_FLAG + if not THREAD_FLAG: + self._log.err(f"grpc_callback_handler ignoring request from {requester} during shutdown") + return None + + if requester == "poecli": + return self.__handle_poecli(args) + else: + self._log.err(f"grpc_callback_handler unknown handler: {requester}") + + return None + + + def __get_disabled_ports(self) -> dict: + """Return the total ports count, disabled ports indices and + LLDP disabled ports + This method is meant for dispatching a response through the + JSON-RPC server. + + Returns: + dict: response + """ + with PoeClientLock: + if self._ports_config is None: + raise AssertionError("Ports config must not be None") + return { + "ports_total_count": self._hal.port_count(), + # User-facing values are one-based + "disabled_ports": ( + [ + port[PORT_ID] + for port in self._ports_config + if port[ENDIS] == AgentConstants.DISABLE + ] + ), + "lldp_disabled_ports": ( + [ + port[PORT_ID] + for port in self._ports_config + if port[AgentConstants.LLDP_ENDIS] == AgentConstants.DISABLE + ] + or None + ), + } + + def __validate_port_id(self, port_id: int) -> None | NoReturn: + """Validate the port ID based on the current number of ports + + Args: + port_id (int): Port ID being queried + + Raises: + JSONRpcInvalidPortIdError: Raised if the port ID is invalid + + Returns: + None | NoReturn + """ + if port_id < 0 or port_id > (self._hal.port_count() - 1): + raise JSONRpcInvalidPortIdError() + + def __fill_port_details(self, port_id: int) -> dict: + """Query the port through the HAL, differentiating + between bt and non-bt API + + Args: + port_id (int): Port ID to query for + + Returns: + dict: the parsed port details + """ + if self._hal.bt_support: + port_status = self._hal.bt_get_port_status(port_id) + port_class_info = self._hal.bt_get_port_class(port_id) + pd_l2_info = self._hal.bt_get_port_l2_lldp_pd_request(port_id) + pse_l2_info = self._hal.bt_get_port_l2_lldp_pse_data(port_id) + + endis = port_status[ENDIS] == 1 + # Small catch here - if a PD is cut off (0x1F state) + # the L2 usage field will still show as "on". Therefore, + # must also check the TPPL. + operational_status = ( + "on" + if ( + pse_l2_info[LAYER2_USAGE] != POE_PD69200_BT_MSG_DATA_PORT_LAYER2_USAGE_OFF + and port_class_info[TPPL] + ) + else "off" + ) + power_mode, assigned_class, tppl, priority = None, None, None, None + requested_power, allocated_power = None, None + mode_a_class, mode_b_class = None, None + mode_a_requested, mode_b_requested = None, None + mode_a_allocated, mode_b_allocated = None, None + ieee_pse_power_status, ieee_pse_power_pairs = None, None + max_power = None + if endis: + if pse_l2_info[LAYER2_USAGE] == POE_PD69200_BT_MSG_DATA_PORT_LAYER2_USAGE_L1: + power_mode = "l1" + elif pse_l2_info[LAYER2_USAGE] == POE_PD69200_BT_MSG_DATA_PORT_LAYER2_USAGE_LLDP: + power_mode = "l2" + # Assigned class will always have the mode A assigned class, + # even if the PD is dual-signature. + assigned_class = pse_l2_info[ASSIGNED_CLASS_ALT_A] + tppl = port_class_info[TPPL] // 10 + # May need to determine the PSE type based on the platform + # name and eventually a dictionary mapping it to the PSE type. + # At the moment, it's enough to decide based on the 802.3bt + # support. + priority = pse_l2_info[PRIORITY] + # These values should be equal, in case the port is already + # reconciled or if it hasn't started L2 negotiation yet. + requested_power = pd_l2_info[PD_REQUESTED_POWER_SINGLE] + allocated_power = pse_l2_info[PSE_ALLOCATED_POWER_SINGLE_ALT_A] + # TODO: Dual-signature + mode_a_class, mode_b_class = None, None + mode_a_requested, mode_b_requested = None, None + mode_a_allocated, mode_b_allocated = None, None + ieee_pse_power_status = pse_l2_info[PSE_POWERING_STATUS] + ieee_pse_power_pairs = pse_l2_info[PSE_POWER_PAIRS_EXT] + max_power = pse_l2_info[PSE_MAX_POWER] + + return { + "endis": endis, + "status": operational_status, + "power_mode": power_mode, + "assigned_class": assigned_class, + "pse_type": "type_3", + "tppl": tppl, + "priority": priority, + "requested_power": requested_power, + "allocated_power": allocated_power, + "mode_a_class": mode_a_class, + "mode_b_class": mode_b_class, + "mode_a_requested": mode_a_requested, + "mode_b_requested": mode_b_requested, + "mode_a_allocated": mode_a_allocated, + "mode_b_allocated": mode_b_allocated, + "ieee_pse_power_status": ieee_pse_power_status, + "ieee_pse_power_pairs": ieee_pse_power_pairs, + "max_power": max_power, + } + else: + # At the moment of writing, all derivatives that are non-BT + # are Type 2 PSEs. May need to be changed in the future. + port_status = self._hal.get_port_status(port_id) + endis = port_status[ENDIS] == 1 + operational_status = "off" + power_mode, assigned_class, tppl, priority = None, None, None, None + requested_power, allocated_power = None, None + if endis: + pse_l2_info = self._hal.get_port_l2_pse_data(port_id) + port_power_limit = self._hal.get_port_power_limit(port_id) + # Small catch here - if a PD is cut off (0x1F state) + # the L2 usage field will still show as "on". Therefore, + # must also check the TPPL. + operational_status = ( + "on" + if ( + pse_l2_info[LAYER2_USAGE] != POE_PD69200_BT_MSG_DATA_PORT_LAYER2_USAGE_OFF + and port_power_limit[TPPL] + ) + else "off" + ) + if pse_l2_info[LAYER2_USAGE] == POE_PD69200_MSG_DATA_PORT_LAYER2_USAGE_L1: + power_mode = "l1" + elif pse_l2_info[LAYER2_USAGE] == POE_PD69200_BT_MSG_DATA_PORT_LAYER2_USAGE_LLDP: + power_mode = "l2" + if operational_status == "on": + assigned_class = port_status[CLASS] + tppl = port_power_limit[TPPL] // 1000 + priority = pse_l2_info[PRIORITY] + requested_power = pse_l2_info[PD_REQUESTED_POWER] + allocated_power = pse_l2_info[PSE_ALLOCATED_POWER] + + return { + "endis": endis, + "status": operational_status, + "power_mode": power_mode, + "assigned_class": assigned_class, + "pse_type": "type_2", + "tppl": tppl, + "priority": priority, + "requested_power": requested_power, + "allocated_power": allocated_power, + } - def save_curerent_runtime(self): - if self.runtime_cfg.is_valid(): - copyfile(self.runtime_cfg.path(), - self.permanent_cfg.path()) + @PoeAccessExclusiveLock + def __get_port_details(self, port_id: int) -> dict | NoReturn: + """Get the current port status and, optionally, the + 802.3at and 802.3bt PoE-related fields + This must, at a minimum, announce the current admin and operational + state. This method is meant for dispatching a response through the + JSON-RPC server. + + Args: + port_id (int): Port ID to query for + + Returns: + dict: response, if successful + """ + port_id -= 1 # Engineering port ID is zero-based. + self.__validate_port_id(port_id) + with PoeClientLock: + if self._ports_config is None: + raise AssertionError("Ports config must not be None") + is_lldp_enabled = [ + port[AgentConstants.LLDP_ENDIS] == AgentConstants.ENABLE + for port in self._ports_config + if port[PORT_ID] == port_id + 1 + ][0] - def autosave_main(self): - global thread_flag - self.log.info("Start autosave thread") - self.rt_counter = 0 - self.fail_counter = 0 - while thread_flag is True: + try: + port_details = self.__fill_port_details(port_id) + + dot3at = None + dot3bt = None + if port_details["pse_type"] == "type_3" and port_details["status"] == "on": + # This means we have to fill in both dot3at and dot3bt fields. + dot3at = { + "pse_type": "type_3", + "priority": port_details["priority"], + "requested_power": port_details["requested_power"], + "allocated_power": port_details["allocated_power"], + } + dot3bt = { + "mode_a_assigned_class": port_details["mode_a_class"], + "mode_b_assigned_class": port_details["mode_b_class"], + "mode_a_requested_power": port_details["mode_a_requested"], + "mode_b_requested_power": port_details["mode_b_requested"], + "mode_a_allocated_power": port_details["mode_a_allocated"], + "mode_b_allocated_power": port_details["mode_b_allocated"], + "pse_power_status": port_details["ieee_pse_power_status"], + "pse_power_pairs": port_details["ieee_pse_power_pairs"], + "max_power": port_details["max_power"], + } + elif port_details["pse_type"] == "type_2" and port_details["status"] == "on": + dot3at = { + "pse_type": "type_2", + "priority": port_details["priority"], + "requested_power": port_details["requested_power"], + "allocated_power": port_details["allocated_power"], + } + elif port_details["pse_type"] != "type_2" and port_details["pse_type"] != "type_3": + raise JSONRpcDriverError(data=f"Invalid PSE type: {port_details['pse_type']}") + + return { + "is_admin_enabled": port_details["endis"], + "status": port_details["status"], + "power_mode": port_details["power_mode"], + "assigned_class": port_details["assigned_class"], + "tppl": port_details["tppl"], + "is_lldp_enabled": is_lldp_enabled, + "dot3at": dot3at, + "dot3bt": dot3bt, + } + except Exception as e: + raise JSONRpcDriverError(data=str(e)) + + @PoeAccessExclusiveLock + def __set_power_limit( + self, port_id: int, default_power: bool, dot3at: dict | None, dot3bt: dict | None + ) -> int | NoReturn: + """This method can be used for either setting the default power limit, + for requesting a port TPPL update, as a result of an LLDP power request + or for disabling the port L2 mode. This method is meant for dispatching + a response through the JSON-RPC server. + + Args: + port_id (int): Port ID to query for + default (bool): whether to set the default power limit or not + dot3at (dict): 802.3at fields + dot3bt (dict): 802.3bt fields + + Returns: + int: The current port TPPL, as a result of the set operation, + if successful + """ + port_id -= 1 # Engineering port ID is zero-based. + self.__validate_port_id(port_id) + + if default_power and (dot3at or dot3bt): + raise JSONRpcInvalidOperationError( + data="Invalid parameter combination (cannot assign default " + "power limit and set LLDP PD request at the same time)" + ) + + power_limit = None + if default_power: + # This will set the default power limit. + requested_class = ( + self._hal.bt_get_port_class(port_id)[REQUESTED_CLASS_ALT_A] + if self._hal.bt_support + else self._hal.get_port_status(port_id)[CLASS] + ) + # This value is in W. + with PoeClientLock: + power_limit = self._default_power_limits.get(requested_class, 0) + + # Using zero cable resistance will make the controller + # use no power loss compensation. Hence, the power limit + # at the PD input will equal the one at PSE output. + # For a 802.3bt PSE, we still have to use the LLDP API + # in order to change the TPPL (changing the power reserve + # doesn't work for lowering the power limit). + if not power_limit: + self._log.notice("Skipping default power limit allocation for " f"port {port_id}") + tppl = ( + (self._hal.bt_get_port_class(port_id)[TPPL] // 10) + if self._hal.bt_support + else (self._hal.get_port_power_limit(port_id)[TPPL] // 1000) + ) + return tppl + if self._hal.bt_support: + self._hal.bt_set_port_l2_lldp_pd_request(port_id, power_limit * 10, 0, 0, 0) + else: + self._hal.set_port_power_limit(port_id, power_limit * 1000) + elif not dot3at and not dot3bt: + # This should disable the L2 port mode and set it back to L1. + # For some reason, there is no way to do that with the 802.3bt + # firmware (probably because there is no way to change the + # TPPL besides executing an L2 request). + # Hence, we'll just return the current TPPL. + result = ( + self._hal.bt_get_port_class(port_id)[TPPL] // 10 + if self._hal.bt_support + else self._hal.get_port_power_limit(port_id)[TPPL] // 1000 + ) + return result + elif dot3at: + if dot3at["requested_power"] < 0 or dot3at["requested_power"] >= 999: + raise JSONRpcInvalidPowerRequestError( + data="Invalid PD requested power value " "(must be between 0 and 999)" + ) + # This will set the PD request through the L2 controller API. + # TODO: AS4224 and TN48M L2 neg support. + self._hal.bt_set_port_l2_lldp_pd_request( + port_id, + dot3at["requested_power"], + 0, + 0, + 0, + dot3at["priority"] or POE_PD69200_BT_MSG_DATA_PORT_PRIORITY_NO_CHANGE, + ) + + # Check if the request went through. + result = 0 + if self._hal.bt_support: + retry_count, retry_timeout = 3, 0.5 + pse_l2_info = {} + for _ in range(retry_count): + pse_l2_info = self._hal.bt_get_port_l2_lldp_pse_data(port_id) + port_class_info = self._hal.bt_get_port_class(port_id) + tppl: int = port_class_info[TPPL] + if POE_PD69200_BT_MSG_DATA_LAYER2_REQ_EXECUTED == pse_l2_info[LAYER2_EXECUTION]: + result = tppl // 10 + break + time.sleep(retry_timeout) + if not result: + raise JSONRpcDriverError( + data=TBL_BT_LAYER2_EXECUTION_TO_CFG[pse_l2_info[LAYER2_EXECUTION]] + ) + else: + tppl = self._hal.get_port_power_limit(port_id) + if power_limit and power_limit == tppl // 1000: + result = power_limit + else: + raise JSONRpcDriverError(data="Failed to set the port TPPL for non-BT device") + + self.__update_last_set_time() + return result + + def __handle_lldp_poed(self) -> None | NoReturn: + """Handle the lldp_poed JSON-RPC requests + + Raises: + SystemExit: Raised if the lldp_poed FIFO doesn't exist and cannot + be created + """ + global THREAD_FLAG + read_fifo = AgentConstants.LLDP_POED_WRITE_FIFO + write_fifo = AgentConstants.LLDP_POED_READ_FIFO + + def read_from_lldp_poed_fifo() -> bytes: + nonlocal read_fifo try: - if self.rt_counter >= self.cfg_update_intvl_rt: - cfg_data = self.collect_running_state() - if self.failsafe_flag == False: - if self.save_poe_cfg(self.runtime_cfg, cfg_data) == True: - self.rt_counter = 0 - else: - self.log.warn( - "Failed to save cfg data in autosave routine!") + with open(read_fifo, "r") as fifo: + raw_data = fifo.read() + return raw_data.encode("ascii") + except Exception as e: + self._log.exc(f"Failed to read from lldp-poed FIFO: {str(e)}") + return bytes() + + def write_to_lldp_poed_fifo(payload: bytes) -> None: + nonlocal write_fifo + write_fd = None + retry_count, retry_timeout = 2, 1 + for i in range(retry_count): + try: + if i > 0: + self._log.info("Retrying to send the response. " f"{retry_count - i} retries remaining...") + time.sleep(retry_timeout) + write_fd = os.open(write_fifo, os.O_WRONLY | os.O_NONBLOCK) + break + except OSError as e: + if e.errno == errno.ENXIO: + self._log.err("lldp-poed hasn't opened the read pipe. Cannot send back the response") + else: + self._log.exc(f"Failed to write to lldp-poed FIFO: {str(e)}") + if write_fd: + try: + os.write(write_fd, payload) + except OSError as e: + if e.errno == errno.EPIPE: + self._log.err("lldp-poed unexpectedly closed the read pipe") else: - self.log.warn( - "POE Agent in failsafe mode, stop saving runtime cfg") - self.rt_counter = 0 + self._log.exc(f"Failed to write to lldp-poed FIFO: {str(e)}") + os.close(write_fd) - self.rt_counter += self.autosave_intvl - time.sleep(self.autosave_intvl) - except Exception as e: - self.fail_counter += 1 - self.log.err("An exception in autosave routine: %s, cnt: %d" % - (str(e), self.fail_counter)) - time.sleep(1) + self.__create_fifo(read_fifo) + self.__create_fifo(write_fifo) + + # Initialize the RPC server and assign the read/write + # local callbacks. + self._rpc_dispatcher.add_method(self.__get_disabled_ports, "get_disabled_ports") + self._rpc_dispatcher.add_method(self.__get_port_details, "get_port_details") + self._rpc_dispatcher.add_method(self.__set_power_limit, "set_power_limit") + transport = CallbackServerTransport(read_from_lldp_poed_fifo, write_to_lldp_poed_fifo) + rpc_server = RPCServer(transport, JSONRPCProtocol(), self._rpc_dispatcher) + + # This will take care of logging all incoming and outgoing messages. + def log_message(direction: str, context: str, message: str): + return self._log.dbg(f"{direction} {context} {message}") + + self._log.dbg("Starting lldp_poed thread...") + while THREAD_FLAG: + rpc_server.receive_one_message() + + self._log.dbg("Exited lldp_poed thread") + + + + def __on_heartbeat_callback(self) -> None: + """Telemetry heartbeat callback that handles the publishing of + various metrics together with the agent heartbeat + """ + publish_metrics("poed_heartbeat", 1) + + total_power = self._hal.get_total_power() + + self._log.dbg(f"total_power = {total_power}") + calculated_power = total_power.get(CALCULATED_POWER) + self._log.dbg(f"State: Calculated power: {calculated_power}W") + + power_avail = total_power.get(POWER_AVAIL) + self._log.dbg(f"State: Available power: {power_avail}W") + + power_consump = total_power.get(POWER_CONSUMP) + self._log.dbg(f"State: Instant power consumption: {power_consump}W") + + ports_delivering_mask: list = self._hal.get_all_ports_en_dis()[ENDIS] + total_active_ports = ports_delivering_mask.count(1) + self._log.dbg(f"State: Enabled port count: {total_active_ports}") + + # communicate with chipset to make this thread block if chipset is in a bad state + # and is blocking on all calls. Blocking this thread will be caught by the watchdog + version_reply = self.get_versions_info() + version_reply_json = json.loads(version_reply) + self._log.dbg(f"State: Version: {version_reply_json}") + + gie = self._hal.get_system_status2().get(GIE) + if 0 != gie: + self._log.err(f"State: GIE: 0x{gie:02x}") + + def __handle_autosave_slice(self) -> None: + """Handle the work payload to be done on autosave thread on each tick.""" + try: + respose_required_sec(60) + self.__on_heartbeat_callback() + if self.__persist_running_config(): + self._log.notice("Successfully autosaved the running configuration") + else: + self._log.err("Failed to autosave the running configuration") + except Exception as e: + self._log.exc(f"__handle_autosave_slice Exception: {str(e)}") + except: + self._log.exc(f"__handle_autosave_slice Exception.") + finally: + respose_received() + + + def __handle_autosave(self) -> None: + """Handle the periodic persistence of the runtime PoE config + This will also send periodic poed heartbeats, as this thread is + non-blocking. + """ + global THREAD_FLAG + + self._log.dbg("Starting autosave thread...") + while THREAD_FLAG: + if self._failsafe_flag: + continue + + self.__handle_autosave_slice() + + time.sleep(self._autosave_wait_interval_s) + + self._log.dbg("Exited autosave thread") @PoeAccessExclusiveLock - def flush_settings_to_chip(self, poe_cfg): + def __flush_port_config(self, config_dao: PoeConfigDao) -> bool: + """Flush the current port configuration to the PoE chipset + This will load the current config through the DAO and apply it + via the driver. Will continue to apply the configuration even if + any operation fails. + + Args: + config (PoeConfigDao): PoE config Data Access Object + + Returns: + bool: True if successful, False otherwise + """ try: - # Read all port enDis status - all_port_endis = self.poe_plat.get_all_ports_enDis() - ret_result = True - data = poe_cfg.load() - all_port_configs = data[PORT_CONFIGS] - last_save_time = data[TIMESTAMP][LAST_SAVE_TIME] + loaded_config = config_dao.load() + if loaded_config is None: + raise AssertionError("Failed to load the PoE " "configuration") + + all_port_configs = loaded_config[AgentConstants.PORT_CONFIGS] + current_en_dis = self._hal.get_all_ports_en_dis() + result = True for params in all_port_configs: + # We expose the port as being one-indexed, but the + # driver in fact works works with zero-based channels. port_id = params.get(PORT_ID) - 1 - poe_port = self.poe_plat.get_poe_port(port_id) - set_result = poe_port.set_all_params( - params, current_enDis=all_port_endis) - if (set_result[ENDIS] != 0 - or set_result[PRIORITY] != 0 - or (self.poe_plat._4wire_bt == 0 and set_result[POWER_LIMIT] != 0)): - self.log.warn("Port[{0}] setting failed: {1}".format( - str(params.get(PORT_ID)), json.dumps(set_result))) - ret_result=False - - self.all_port_state = all_port_configs - self.last_poe_set_time = self.get_current_time() - self.last_cfg_save_time = last_save_time - return ret_result + poe_port = self._hal.get_poe_port(port_id) + if poe_port is None: + raise AssertionError(f"Failed to get the PoE port: {port_id}") + set_result = poe_port.set_all_params(params, current_en_dis, readback=False) + + if ( + set_result[ENDIS] != 0 + or set_result[PRIORITY] != 0 + or (not self._hal._bt_support and set_result[POWER_LIMIT] != 0) + ): + self._log.err( + f"Flushing Port ID {PORT_ID}] " + f"failed: {json.dumps(set_result, ensure_ascii=True)}" + ) + result = False + + # Update the local LLDP en/dis status and the default power limits. + with PoeClientLock: + if self._ports_config is None: + raise AssertionError("Ports config must not be None") + for local_port, loaded_port in zip(self._ports_config, all_port_configs): + local_port[AgentConstants.LLDP_ENDIS] = loaded_port[AgentConstants.LLDP_ENDIS] + # The keys must be converted back to ints from JSON. + with PoeClientLock: + self._default_power_limits = { + int(power_class): limit + for power_class, limit in loaded_config[AgentConstants.DEFAULT_LIMITS].items() + } + + # Must notify of the config change, otherwise the port config will + # be stale. + self.__update_last_set_time() + return result except Exception as e: - error_class = e.__class__.__name__ - detail = e.args[0] - cl, exc, tb = sys.exc_info() - lastCallStack = traceback.extract_tb( - tb)[-1] - fileName = lastCallStack[0] - lineNum = lastCallStack[1] - funcName = lastCallStack[2] - errMsg = "File \"{}\", line {}, in {}: [{}] {}".format( - fileName, lineNum, funcName, error_class, detail) - self.log.warn( - "Flush settings to chip failed, exception: {0}".format(str(errMsg))) - return False - - - def failsafe_mode(self): - self.log.warn("Entering fail safe mode(All port disabled).") - self.failsafe_flag = True - for idx in range(self.poe_plat.total_poe_port()): - self.poe_plat.set_port_enDis(idx, 0) - - def load_poe_cfg(self, poe_cfg): - retry = 0 - while retry < self.cfg_load_retry: + self._log.exc(f"Failed to flush the configuration: {str(e)}") + + return False + + def __enter_failsafe_mode(self) -> None: + """Enter failsafe mode by disabling all PoE ports""" + self._log.warn("Entering failsafe mode (all PoE ports are disabled)") + self._failsafe_flag = True + for i in range(self._hal.port_count()): + self._hal.set_port_en_dis(i, 0) + + def __apply_poe_config(self, config: PoeConfigDao) -> bool: + """Load the configuration through the DAO (if it's valid) + and flush the port configuration to the PoE chipset + This will have a limited number of retries. + + Args: + config (PoeConfigDao): PoE config Data Access Object + + Returns: + bool: True if successful, False otherwise + """ + retry_count = 0 + while retry_count < self._cfg_load_max_retry: try: - if poe_cfg.is_valid() == False: - self.log.warn("Invalid cfg data to load!") + if not config.lazy_is_valid(): + self._log.err("Loaded PoE configuration is invalid: " f"{config.local_cfg_path}") return False - return self.flush_settings_to_chip(poe_cfg) + + if self.__flush_port_config(config): + return True except Exception as e: - self.log.err("An exception to load cfg (%s): %s, retry = %s" % - (poe_cfg.path(), str(e), str(retry))) - retry += 1 + self._log.exc(f"Failed to apply config file from {config.local_cfg_path}" f": {str(e)}") + retry_count += 1 time.sleep(1) + return False - def set_poe_agent_state(self, val): - if val != PoeAgentState.UNCLEAN_START and \ - val != PoeAgentState.CLEAN_START: - self.log.warn("Invalid poe agent state: %d, skipped!" % val) - else: - self.poe_agent_state = val + def init_config(self, warm_boot: bool) -> None | NoReturn: + """Initialize the agent configuration either from the runtime or + permanent configuration file (try to pick the runtime one if it's + a warm boot and if it's a valid config). If there is no valid + pre-existing configuration available, reconstruct the config file + from the default chipset configuration. - def get_poe_agent_stae(self): - return self.poe_agent_state + Args: + warm_boot (bool): If True, the system hasn't gone through a cold + boot yet. This means that the agent may have run previously. - def create_poe_set_ipc(self): + Returns: + None | NoReturn: + """ try: - os.mkfifo(POE_IPC_EVT) - except OSError as oe: - if oe.errno != errno.EEXIST: - self.log.err("Failed to open named pipe: %s" % str(e)) + respose_required_sec(60) + # Decide between the permanent config and the previous runtime one + # (if any). + active_config = None + if warm_boot and self._runtime_cfg.lazy_is_valid(): + active_config = self._runtime_cfg + elif self._permanent_cfg.lazy_is_valid(): + active_config = self._permanent_cfg + + # Initialize the PoE chipset. + skip_port_init = active_config is not None + if self.__init_platform(skip_port_init): + self._log.notice("Successfully initialized the PoE chipset") + + # It wouldn't be safe to do the read before the init. + self._ports_config = self.__get_ports_running_config() + if self._ports_config is None: + raise AssertionError("Ports config must not be None") + for port in self._ports_config: + # The LLDP en/dis will be updated once the config is + # loaded. + # By default, all ports are enabled for LLDP processing. + port[AgentConstants.LLDP_ENDIS] = AgentConstants.ENABLE + self._last_bank_type = self.__get_current_bank_source() + else: + self.__enter_failsafe_mode() + poed_exit(ret_code=EXIT_CODES.HAL_INIT_FAILED) -def get_prev_pid(): - return int(open(POED_PID_PATH, 'r').read()) + if active_config is not None: + self._log.notice("Trying to restore PoE configuration from: " f"{active_config.local_cfg_path}") -def is_still_alive(pid): + if self.__apply_poe_config(active_config): + self._log.notice("Successfully restored configuration") + else: + self._log.err("Failed to restore the PoE configuration") + self.__enter_failsafe_mode() + else: + # We have to reconstruct the config from the default running + # state. + self._log.notice("Reconstructing the local PoE configuration from the " "current chipset state...") + if self.__persist_running_config(): + self._log.notice("Successfully reconstructed the configuration") + else: + self._log.err("Failed to reconstruct the PoE " "configuration. Entering failsafe mode...") + self.__enter_failsafe_mode() + except Exception as e: + self._log.exc(f"Config initialization failed: {str(e)}") + poed_exit(ret_code=EXIT_CODES.CONFIG_INIT_FAILED) + finally: + respose_received() + + def start(self) -> None: + """Start the all agent threads: autosave, lldp-poed and poecli""" + try: + self._server = grpc.server(futures.ThreadPoolExecutor(max_workers=MAX_WORKER_THREADS)) + poed_ipc_pb2_grpc.add_PoeIpcServicer_to_server(PoedServicer(self.grpc_callback_handler), self._server) + self._server.add_insecure_port(AgentConstants.POED_GRPC_SERVER_ADDRESS) + self._server.start() + except Exception as ex: + self._log.exc(f"Poe Agent gRPC server start failed: {str(ex)}") + + self._autosave_thread.start() + self._lldp_poe_thread.start() + + def stop(self) -> None: + try: + self._log.err("gRPC stop sent. Waiting for RPCs to complete...") + self._server.stop(GRPC_STOP_NUM_SECS_TO_WAIT).wait() + self._log.err("gRPC stop done") + except Exception as ex: + self._log.exc(f"Poe Agent gRPC server stop failed: {str(ex)}") + + def wait_on_agent_threads(self): + """wait for the all agent threads: autosave, poecli and lldp_poed""" + for thread in [self._autosave_thread, self._lldp_poe_thread]: + if thread.is_alive(): + thread.join() + + +def is_process_alive(pid: int) -> bool: + """Check if the process is still alive, given its PID + + Args: + pid (int): Process PID + + Returns: + bool: True if alive, False otherwise + """ try: os.kill(pid, 0) except OSError: return False - else: - return True -def save_cur_pid(): - open(POED_PID_PATH, 'w').write(str(os.getpid())) + return True + + +def main() -> None: + global THREAD_FLAG -def main(argv): - global thread_flag if os.geteuid() != 0: - raise RuntimeError("Warning, poed service must be run as root!") + raise RuntimeError("poed must be run as root") + # A warm boot is equivalent to having the PID file present under run/ + # as this folder will get emptied on every cold boot. is_warm_boot = True try: - prevPid = get_prev_pid() - if is_still_alive(prevPid) == True: - PoeLog().warn("Previos poed service is still alive!") - os._exit(-1) - except: + prev_pid = int(open(AgentConstants.POED_PID_PATH, "r").read()) + if is_process_alive(prev_pid): + raise SystemExit("Previous poed service is still alive." "Will not launch another instance") + except Exception: + # The PID file doesn't exist or the process is not alive. is_warm_boot = False finally: - save_cur_pid() - - pa = PoeAgent() - if pa.plat_supported: - touch_file(POED_BUSY_FLAG) - try: - poe_cfg = pa.permanent_cfg - if is_warm_boot and pa.runtime_cfg.is_valid(): - poe_cfg = pa.runtime_cfg - pa.log.info("Configure PoE ports from \"%s\"" % poe_cfg.path()) - if poe_cfg.is_valid() == True: - if pa.init_platform(True) == True: - pa.log.info("Success to initialize platform PoE settings!") - if pa.load_poe_cfg(poe_cfg)== True: - pa.log.info( - "Success to restore port configurations from \"%s\"." % poe_cfg.path()) - else: - pa.log.info( - "Failed to restore port configurations from \"%s\"." % poe_cfg.path()) - pa.failsafe_mode() - else: - pa.log.info("Failed to initialize platform PoE settings!") - pa.set_poe_agent_state(PoeAgentState.UNCLEAN_START) - pa.failsafe_mode() - else: - if pa.init_platform(False) == True: - pa.log.info("Success to initialize platform PoE settings!") - if Path(pa.runtime_cfg.path()).exists() == False: - pa.log.info( - "Runtime config file loss, reconstruct \"%s\" config from poe chip runtime setting." % pa.runtime_cfg.path()) - cfg_data = pa.collect_running_state() - if pa.save_poe_cfg(pa.runtime_cfg, cfg_data) == True: - pa.log.info("Runtime config reconstruct completed.") - else: - pa.log.warn( - "Runtime config broken, please check: \"%s\"" % pa.runtime_cfg.path()) - pa.set_poe_agent_state(PoeAgentState.UNCLEAN_START) - pa.failsafe_mode() - else: - pa.log.info("Failed to initialize platform PoE settings!") - pa.set_poe_agent_state(PoeAgentState.UNCLEAN_START) - pa.failsafe_mode() - # Start Autosave thread - pa.autosave_thread.start() - except Exception as e: - pa.log.warn("Load config failed: {0}".format(str(e))) - poed_exit(ret_code=-2) - remove_file(POED_BUSY_FLAG) - pa.create_poe_set_ipc() - while thread_flag is True: - try: - with open(POE_IPC_EVT, 'r') as f: - data_list = str(f.read()).split(",") - for data in data_list: - if data == POECLI_SET: - pa.update_set_time() - pa.log.info("Receive a set event from poecli!") - if pa.rt_counter 1: - action = data_list[1] - pa.log.info("CFG Action: {0}".format(action)) - if len(data_list) > 2: - file = data_list[2] - pa.log.info("CFG File: {0}".format(file)) - if len(data_list) > 3: - apply = data_list[3] - pa.log.info("CFG Apply: {0}".format(apply)) - if action==POED_SAVE_ACTION: - if file == None: - pa.log.info( - "CFG Save: Save runtime setting to persistent file") - pa.save_curerent_runtime() - else: - copyfile(pa.runtime_cfg.path(), - file) - pa.log.info( - "CFG Save: Save runtime setting to {0}".format(file)) - elif action==POED_LOAD_ACTION: - if file == None: - pa.log.info( - "CFG Load: Load persistent file") - result = pa.load_poe_cfg(pa.permanent_cfg) - else: - pa.log.info( - "CFG Load: Load cfg file from {0}".format(file)) - temp_cfg = PoeConfig(file, pa.plat_name) - result = pa.load_poe_cfg(temp_cfg) - if result == True: - pa.update_set_time() - break - else: - pa.log.notice("Receive data: %s, skipped!" % data) + # Save our current PID. + open(AgentConstants.POED_PID_PATH, "w").write(str(os.getpid())) + + # The initialization sequence shouldn't be interrupted by + # any other command (e.g. CLI set command). + poed_lock = FileLock(AgentConstants.POED_INIT_FLAG_PATH) + with poed_lock: + PoeAgent().init_config(is_warm_boot) + + PoeAgent().start() + + while THREAD_FLAG: + time.sleep(1) + print_stderr("main() exit") + +def poed_exit(sig=0, frame=None, ret_code: int = 0) -> NoReturn: + print_stderr(f"poed_exit({sig}, {frame}, {ret_code})") + global THREAD_FLAG + THREAD_FLAG = False + publish_metrics("poed_exit", ret_code) + PoeAgent().wait_on_agent_threads() + PoeAgent().stop() + sys.exit(ret_code) - except Exception as e: - pa.log.err("An exception to listen poe set event: %s, skipped." - % str(e)) - poed_exit(ret_code=-3) - else: - while thread_flag is True: - time.sleep(1) +def alarm_handler(signum, frame): + """ + Handle an alarm call. -def poed_exit(sig=0, frame=None,ret_code=0): - global thread_flag - remove_file(POED_BUSY_FLAG) - thread_flag = False - print_stderr("exitcode={0}".format(ret_code)) - sys.exit(ret_code) + :param signum: Alarm signum. + :param frame: Alarm frame. + :rtype: None + """ + signame = signal.Signals(signum).name + print_stderr(f'Signal handler called with signal {signame} ({signum})') + print_stderr("Hang alarm raised. Restarting daemon.") + publish_metrics("poed_exit", EXIT_CODES.HUNG_DETECTED) + os.kill(os.getpid(), signal.SIGKILL) if __name__ == "__main__": try: signal.signal(signal.SIGTERM, poed_exit) - main(sys.argv) + signal.signal(signal.SIGALRM, alarm_handler) + main() except Exception as e: - print_stderr("Main Exception: {0}".format(str(e))) - - poed_exit() + print_stderr(f"Unexpected error when running the daemon: {str(e)}") + finally: + poed_exit(EXIT_CODES.SUCCESS) diff --git a/dentos-poe-agent/opt/poeagent/drivers/bus_driver.py b/dentos-poe-agent/opt/poeagent/drivers/bus_driver.py new file mode 100644 index 0000000..4b5632f --- /dev/null +++ b/dentos-poe-agent/opt/poeagent/drivers/bus_driver.py @@ -0,0 +1,42 @@ +''' +Copyright 2021 Delta Electronic Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +''' + +from abc import ABC, abstractmethod + +class BusDriver(ABC): + """Bus driver interface used for accessing the PoE chipset + through the system interconnect (e.g. I2c) + """ + + @abstractmethod + def write_message(self, msg: list, delay: int) -> None: + pass + + @abstractmethod + def read_message(self) -> list: + pass + + @abstractmethod + def read(self, size) -> list: + pass + + @abstractmethod + def bus_lock(self) -> None: + pass + + @abstractmethod + def bus_unlock(self) -> None: + pass diff --git a/dentos-poe-agent/opt/poeagent/drivers/i2c_driver.py b/dentos-poe-agent/opt/poeagent/drivers/i2c_driver.py new file mode 100644 index 0000000..7011ef9 --- /dev/null +++ b/dentos-poe-agent/opt/poeagent/drivers/i2c_driver.py @@ -0,0 +1,87 @@ +''' +Copyright 2021 Delta Electronic Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +''' + +import fcntl +import threading +import time +from typing import Iterable + +from bus_driver import BusDriver +from smbus2 import SMBus, i2c_msg + + +class I2cDriver(BusDriver): + """Common PoeDriver implementation for I2c-based designs.""" + + I2C_WRITE_DELAY = 0.3 + + def __init__(self, i2c_bus: int, i2c_addr: int): + if i2c_bus is None: + raise AssertionError("I2c bus must not be None") + self._i2c_bus = i2c_bus + if i2c_addr is None: + raise AssertionError("I2c address must not be None") + self._i2c_addr = i2c_addr + self._poe_bus = SMBus(self.i2c_bus) + self._poe_lock = threading.Lock() + + @property + def i2c_bus(self) -> int: + return self._i2c_bus + + @property + def i2c_addr(self) -> int: + return self._i2c_addr + + def __bus(self): + if self._poe_bus.fd is None: + self._poe_bus = SMBus(self._poe_bus) + return self._poe_bus + + def __lock(self): + if self._poe_lock is None: + self._poe_lock = threading.Lock() + return self._poe_lock + + def __i2c_write(self, bus, msg, delay=I2C_WRITE_DELAY): + write = i2c_msg.write(self.i2c_addr, msg) + bus.i2c_rdwr(write) + time.sleep(delay) + + def __i2c_read(self, bus, size=15): + result = i2c_msg.read(self.i2c_addr, size) + bus.i2c_rdwr(result) + + if not isinstance(result, Iterable): + raise AssertionError("The result for an I2c read must be iterable") + return list(result) + + def write_message(self, msg: list, delay: int) -> None: + self.__i2c_write(self.__bus(), msg, delay) + + def read_message(self) -> list: + return self.__i2c_read(self.__bus()) + + def read(self, size) -> list: + return self.__i2c_read(self.__bus(), size) + + def bus_lock(self) -> None: + self.__lock().acquire() + fcntl.flock(self.__bus().fd, fcntl.LOCK_EX) + + def bus_unlock(self) -> None: + fcntl.flock(self.__bus().fd, fcntl.LOCK_UN) + self.__lock().release() diff --git a/dentos-poe-agent/opt/poeagent/drivers/pd69200/poe_driver.py b/dentos-poe-agent/opt/poeagent/drivers/pd69200/poe_driver.py new file mode 100755 index 0000000..30e4596 --- /dev/null +++ b/dentos-poe-agent/opt/poeagent/drivers/pd69200/poe_driver.py @@ -0,0 +1,1402 @@ +''' +Copyright 2021 Delta Electronic Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +''' + + +import errno +import fcntl +import json +import os +import sys +import time +from abc import ABC, abstractmethod +from collections import OrderedDict +from io import TextIOWrapper +from time import perf_counter_ns +from typing import Any, Callable + +from agent_constants import AgentConstants +from bus_driver import BusDriver +from filelock import FileLock +from pd69200.poe_driver_def import * +from pd69200.poe_driver_msg_parser import PoeMsgParser +from poe_common import * +from poe_log import PoeLog + + +class RxTxDesync(RuntimeError): + pass + + +class PoeCommExclusiveLock: + """Synchronize the access to the PoE chipset, + using both an internal and an external lock + """ + + def __call__(self, comm): + def wrap_comm(*args, **kargs): + poe_driver: PoeDriver_microsemi_pd69200 = args[0] + if not isinstance(poe_driver, PoeDriver_microsemi_pd69200): + raise AssertionError("Invalid PoE driver supplied") + try: + poe_driver.bus_lock() + result = comm(*args, **kargs) + except Exception as e: + raise e + finally: + poe_driver.bus_unlock() + return result + + return wrap_comm + + +class StateContext: + def __init__(self, path: str = AgentConstants.POE_COMM_STATE_PATH): + self._path = path + self._fd: TextIOWrapper + self._data = {} + + def __enter__(self): + if os.path.exists(self._path): + self._fd = open(self._path, "rt+") + else: + self._fd = open(self._path, "w") + fcntl.flock(self._fd, fcntl.LOCK_EX) + + self._fd.seek(0, 2) + sz = self._fd.tell() + self._fd.seek(0) + + if sz: + try: + self._data = json.load(self._fd) or {} + except json.JSONDecodeError: + self._data = {} + else: + self._data = {} + + return self._data + + def __exit__(self, t, v, tb): + try: + # if an exception was raised, do not update the state file + if v is None: + self._fd.seek(0) + self._fd.truncate() + json.dump(self._data, self._fd) + finally: + fcntl.flock(self._fd, fcntl.LOCK_UN) + + return None + + +# TODO: Add type hints and extract mixin for 802.3bt. +class PoeDriver_microsemi_pd69200(ABC): + def __init__( + self, + bus_driver: BusDriver, + port_count: int, + max_shutdown_voltage: int, + min_shutdown_voltage: int, + guard_band: int, + power_bank_to_str: Callable[[int], str], + ): + # Passed from the child HAL. + self._bus_driver = bus_driver + self._port_count = port_count + self._max_shutdown_voltage = max_shutdown_voltage + self._min_shutdown_voltage = min_shutdown_voltage + self._guard_band = guard_band + self._power_bank_to_str = power_bank_to_str + + self._log = PoeLog(debug_mode=os.isatty(sys.stdin.fileno())) + + self._bt_support = False + # Minimum waiting time since last 15 bytes transmission and + # before reading back the telemetry or report from the PoE + # controller: 30ms + self._msg_delay = 0.03 + # Message read timeout (nano-seconds) + self._msg_read_timeout_ns = 1000000000 + # Minimum waiting time since last command report and before + # sending a new command to the PoE controller + self._msg_min_time_between_commands_sec = 0.03 + # Wait time after saving system setting: 50ms + self._save_sys_delay = 0.05 + # Wait time after restoring to factory defaults: 100ms + self._restore_factory_defaults_delay = 0.1 + # Wait time to clear up poe chip I2C buffer: 500ms + self._clear_bus_buffer_delay = 0.5 + # Wake-up time delay after resetting the chip: 300ms + self._reset_poe_chip_delay = 0.3 + + if os.path.exists(AgentConstants.POE_CPLD_RESET_RQ_PATH): + self._log.warn("PoE chipset reset via CPLD requested") + self._reset_cpld() + os.unlink(AgentConstants.POE_CPLD_RESET_RQ_PATH) + + @property + def bt_support(self) -> bool: + return self._bt_support + + def bus_lock(self) -> None: + self._bus_driver.bus_lock() + + def bus_unlock(self) -> None: + self._bus_driver.bus_unlock() + + def __calc_msg_echo(self): + with StateContext() as data: + echo = data.get("echo", 0x00) + echo += 1 + if echo == 0xFF: + echo = 0x00 + data["echo"] = echo + + return echo + + def __calc_msg_csum(self, msg): + if len(msg) > POE_PD69200_MSG_LEN - POE_PD69200_MSG_CSUM_LEN: + raise RuntimeError("Invalid POE message Length: %d" % len(msg)) + + csum16 = 0 + for data in msg: + csum16 += data + csum16 = csum16 & 0xFFFF + return [csum16 >> 8, csum16 & 0xFF] + + def __build_tx_msg(self, command): + if len(command) > POE_PD69200_MSG_LEN - POE_PD69200_MSG_CSUM_LEN: + raise RuntimeError("Invalid POE Tx command Length: %d" % len(command)) + + tx_msg = command[:] + lenN = POE_PD69200_MSG_LEN - len(tx_msg) - POE_PD69200_MSG_CSUM_LEN + for _ in range(lenN): + tx_msg.append(POE_PD69200_MSG_N) + tx_msg += self.__calc_msg_csum(tx_msg) + return tx_msg + + def __xmit(self, msg, delay): + if len(msg) != POE_PD69200_MSG_LEN: + raise RuntimeError("Invalid POE Tx message Length: %d" % len(msg)) + self._bus_driver.write_message(msg, delay) + + def __recv(self): + return self._bus_driver.read_message() + + def __read_message(self, echo_byte): + """Reading a message from PoE chipset in a safe way + by handling possible errors. + """ + ret_msg = [] + read_len = POE_PD69200_MSG_LEN + start_time_ns = perf_counter_ns() + byte_searched = 0 + while len(ret_msg) != POE_PD69200_MSG_LEN and perf_counter_ns() - start_time_ns < self._msg_read_timeout_ns: + rx_msg = self._bus_driver.read(read_len) + if len(ret_msg) == 0: + for i, byte in enumerate(rx_msg): + if byte_searched == 0 and ( + byte == POE_PD69200_MSG_KEY_TELEMETRY or byte == POE_PD69200_MSG_KEY_REPORT + ): + byte_searched = 1 + elif byte_searched == 1: + if byte == echo_byte: + ret_msg.extend(rx_msg[i - 1 :]) + read_len -= len(ret_msg) + if len(ret_msg) != POE_PD69200_MSG_LEN: + PoeLog().dbg( + "Read (raw buff) : {0} / len={1}".format(conv_byte_to_hex(rx_msg), len(rx_msg)) + ) + PoeLog().dbg( + "Read (out buff) : {0} / len={1}".format(conv_byte_to_hex(ret_msg), len(ret_msg)) + ) + break + else: + PoeLog().err("Faild to match second message byte: {0}".format(conv_byte_to_hex(rx_msg))) + byte_searched = 0 + elif len(ret_msg) < POE_PD69200_MSG_LEN: + ret_msg.extend(rx_msg[0 : POE_PD69200_MSG_LEN - len(ret_msg)]) + PoeLog().dbg("Read (next read) : {0} / len={1}".format(conv_byte_to_hex(ret_msg), len(ret_msg))) + if len(ret_msg) == POE_PD69200_MSG_LEN: + csum = self.__calc_msg_csum(rx_msg[0:POE_PD69200_MSG_OFFSET_CSUM_H]) + if ret_msg[POE_PD69200_MSG_OFFSET_CSUM_H] != csum[0] or ret_msg[POE_PD69200_MSG_OFFSET_CSUM_L] != csum[1]: + PoeLog().err("Read (out): {0}".format(conv_byte_to_hex(ret_msg))) + PoeLog().err(f"Read CRC failed {ret_msg[POE_PD69200_MSG_OFFSET_CSUM_H]} != {csum[0]}") + PoeLog().err(f"Read CRC failed {ret_msg[POE_PD69200_MSG_OFFSET_CSUM_L]} != {csum[1]}") + else: + PoeLog().err("Read (out) : {0} / len={1}".format(conv_byte_to_hex(ret_msg), len(ret_msg))) + return ret_msg + + def __check_rx_msg(self, rx_msg, tx_msg): + if rx_msg == None: + raise RuntimeError("Received POE message is None") + if len(rx_msg) != POE_PD69200_MSG_LEN: + PoeLog().err("__check_rx_msg Send: {0}".format(conv_byte_to_hex(tx_msg))) + PoeLog().err("__check_rx_msg Read: {0}".format(conv_byte_to_hex(rx_msg))) + raise RuntimeError("Received POE message Length is invalid: %d" % len(rx_msg)) + if rx_msg.count(0x00) == POE_PD69200_MSG_LEN: + raise RuntimeError("POE RX is not ready") + + tx_key, rx_key = (tx_msg[POE_PD69200_MSG_OFFSET_KEY], rx_msg[POE_PD69200_MSG_OFFSET_KEY]) + if ( + tx_key == POE_PD69200_MSG_KEY_COMMAND or tx_key == POE_PD69200_MSG_KEY_PROGRAM + ) and rx_key != POE_PD69200_MSG_KEY_REPORT: + PoeLog().err("Send: {0}".format(conv_byte_to_hex(tx_msg))) + PoeLog().err("Read: {0}".format(conv_byte_to_hex(rx_msg))) + raise RxTxDesync( + "Key field in Tx/Rx message is mismatch, " + "Tx key is %02x, Rx key should be %02x, but " + "received %02x" % (tx_key, POE_PD69200_MSG_KEY_REPORT, rx_key) + ) + if tx_key == POE_PD69200_MSG_KEY_REQUEST and rx_key != POE_PD69200_MSG_KEY_TELEMETRY: + PoeLog().err("Send: {0}".format(conv_byte_to_hex(tx_msg))) + PoeLog().err("Read: {0}".format(conv_byte_to_hex(rx_msg))) + raise RxTxDesync( + "Key field in Tx/Rx message is mismatch, " + "Tx key is %02x, Rx key should be %02x, but " + "received %02x" % (tx_key, POE_PD69200_MSG_KEY_TELEMETRY, rx_key) + ) + + tx_echo, rx_echo = (tx_msg[POE_PD69200_MSG_OFFSET_ECHO], rx_msg[POE_PD69200_MSG_OFFSET_ECHO]) + if rx_echo != tx_echo: + PoeLog().err("Send: {0}".format(conv_byte_to_hex(tx_msg))) + PoeLog().err("Read: {0}".format(conv_byte_to_hex(rx_msg))) + raise RuntimeError( + "Echo field in Tx/Rx message is mismatch, " "Tx Echo is %02x, Rx Echo is %02x" % (tx_echo, rx_echo) + ) + + csum = self.__calc_msg_csum(rx_msg[0:POE_PD69200_MSG_OFFSET_CSUM_H]) + if rx_msg[POE_PD69200_MSG_OFFSET_CSUM_H] != csum[0] or rx_msg[POE_PD69200_MSG_OFFSET_CSUM_L] != csum[1]: + PoeLog().err("Send: {0}".format(conv_byte_to_hex(tx_msg))) + PoeLog().err("Read: {0}".format(conv_byte_to_hex(rx_msg))) + raise RuntimeError("Invalid checksum in POE Rx message") + + @PoeCommExclusiveLock() + def __communicate(self, tx_msg, delay): + retry = 0 + rx_msg = [] + while True: + try: + self.__xmit(tx_msg, delay) + if retry > 0: + self._log.dbg("Send (retry: {0}): {1}".format(retry, conv_byte_to_hex(tx_msg))) + rx_msg = self.__read_message(tx_msg[POE_PD69200_MSG_OFFSET_ECHO]) + self.__check_rx_msg(rx_msg, tx_msg) + return rx_msg + except OSError as e: + # Handling case OSError: [Errno 6] No such device or address + # https://issues.amazon.com/issues/IHMNEET-205 + self._log.err(f"__communicate Exception (retry {retry}) (OSError): {str(e)}") + if retry != 0 and e.errno == errno.ENXIO: + self._log.err(f"__communicate exit current process to reopen all resources !!!!!!!!!!!!") + sys.exit(e.errno) + except RxTxDesync as rxe: + self._log.err(f"__communicate Exception (RxTxDesync): {str(rxe)}") + self.__run_syncronization_protocol() + except Exception as e: + self._log.exc(f"__communicate Exception: {str(e)}") + + # Wait 0.5s to clear up I2C buffer + time.sleep(self._clear_bus_buffer_delay) + retry += 1 + if retry < POE_PD69200_COMM_RETRY_TIMES: + # Increment echo byte + command = tx_msg[0:POE_PD69200_MSG_OFFSET_DATA12] + command[POE_PD69200_MSG_OFFSET_ECHO] = self.__calc_msg_echo() + tx_msg = self.__build_tx_msg(command) + else: + raise RuntimeError("Failed to run the PoE serial communication protocol") + + def __run_communication_protocol(self, command, delay, msg_type=None) -> Any: + tx_msg = self.__build_tx_msg(command) + # An external lock is required as there are multiple Python + # modules using the PoE driver. + with FileLock(AgentConstants.POE_BUSY_FLAG_PATH): + with StateContext() as data: + last_send_key = data.get("last_send_key", None) + + # A pre-defined delay is required between two consecutive + # commands. + if ( + last_send_key == tx_msg[POE_PD69200_MSG_OFFSET_KEY] + and tx_msg[POE_PD69200_MSG_OFFSET_KEY] == POE_PD69200_MSG_KEY_COMMAND + ): + time.sleep(self._msg_min_time_between_commands_sec) + + rx_msg = self.__communicate(tx_msg, delay) + + with StateContext() as data: + data["last_send_key"] = tx_msg[POE_PD69200_MSG_OFFSET_KEY] + # XXX teeny tiny race condition if multiple threads enter here + # the last_send_key may not be updated correctly + # Here we assume that the state update is quicker than the + # _communicate() so we can be fairly confident that we will win the + # race + + if rx_msg is not None and msg_type is not None: + result = PoeMsgParser().parse(rx_msg, msg_type) + return result + + def __run_syncronization_protocol(self) -> None: + """This function will syncronise PoE Rx/Tx buffer by sending a query + message (Get Interrupt Mask) and + 1) reading data until beginning of the message received (the pair + [MESSAGE TYPE, ECHO]) + 2) read the rest of the 15 bytes message. + """ + self._log.dbg("__run_syncronization_protocol-->") + time.sleep(self._msg_delay) + command = [ + POE_PD69200_MSG_KEY_REQUEST, + self.__calc_msg_echo(), + POE_PD69200_MSG_SUB_GLOBAL, + POE_PD69200_MSG_SUB1_GETINTERRUPTMASK, + ] + tx_msg = self.__build_tx_msg(command) + self._log.dbg("__run_syncronization_protocol send: {0}".format(conv_byte_to_hex(command))) + self.__xmit(tx_msg, self._msg_delay) + + max_read_left = POE_PD69200_MSG_LEN * 2 + message_byte = 0 + while max_read_left > 0: + rx_byte = self._bus_driver.read(1) + if rx_byte is None or len(rx_byte) != 1: + raise RuntimeError("Invalid response message from read(1)") + self._log.info("__run_syncronization_protocol: recv byte" + str(rx_byte)) + max_read_left -= 1 + if message_byte == 0: + if rx_byte[0] == POE_PD69200_MSG_KEY_REPORT: + self._log.info("__run_syncronization_protocol: found first byte") + message_byte = 1 + continue + if message_byte == 1: + if rx_byte[0] == command[1]: + self._log.info("__run_syncronization_protocol: found second byte") + rx_byte = self._bus_driver.read(13) + break + else: + message_byte = 0 + self._log.dbg("__run_syncronization_protocol<--") + + def reset_poe(self): + command = [ + POE_PD69200_MSG_KEY_COMMAND, + self.__calc_msg_echo(), + POE_PD69200_MSG_SUB_GLOBAL, + POE_PD69200_MSG_SUB1_RESET, + 0x00, + POE_PD69200_MSG_SUB1_RESET, + 0x00, + POE_PD69200_MSG_SUB1_RESET, + ] + return self.__run_communication_protocol( + command, self._reset_poe_chip_delay, PoeMsgParser.MessageType.MSG_CMD_STATUS + ) + + @abstractmethod + def _reset_cpld(self) -> None: + """Override this with the specific CPLD reset sequence for this chip, + if any.""" + pass + + def restore_factory_defaults(self): + command = [POE_PD69200_MSG_KEY_PROGRAM, self.__calc_msg_echo(), POE_PD69200_MSG_SUB_RESTORE_FACT] + return self.__run_communication_protocol( + command, self._restore_factory_defaults_delay, PoeMsgParser.MessageType.MSG_CMD_STATUS + ) + + def save_system_settings(self): + command = [ + POE_PD69200_MSG_KEY_PROGRAM, + self.__calc_msg_echo(), + POE_PD69200_MSG_SUB_E2, + POE_PD69200_MSG_SUB1_SAVE_CONFIG, + ] + return self.__run_communication_protocol(command, self._save_sys_delay, PoeMsgParser.MessageType.MSG_CMD_STATUS) + + def supports_bt_protocol(self, min_major_ver: int = 3) -> bool: + """Determine if the current driver supports 802.3bt + + Args: + min_major_ver (int, optional): Firmware major version. + Defaults to 3 + + Returns: + bool: True if supported, False otherwise + """ + poe_ver = self.get_poe_versions() + major_ver = int(poe_ver.split(".")[1]) + if major_ver >= min_major_ver: + self._bt_support = True + else: + self._bt_support = False + + return self._bt_support + + def __set_user_byte_to_save(self, user_val): + command = [POE_PD69200_MSG_KEY_PROGRAM, self.__calc_msg_echo(), POE_PD69200_MSG_SUB_USER_BYTE, user_val] + return self.__run_communication_protocol(command, self._save_sys_delay, PoeMsgParser.MessageType.MSG_CMD_STATUS) + + def set_system_status(self, priv_label): + command = [ + POE_PD69200_MSG_KEY_COMMAND, + self.__calc_msg_echo(), + POE_PD69200_MSG_SUB_GLOBAL, + POE_PD69200_MSG_SUB1_SYSTEM_STATUS, + priv_label, + ] + return self.__run_communication_protocol(command, self._msg_delay, PoeMsgParser.MessageType.MSG_CMD_STATUS) + + def get_system_status(self): + command = [ + POE_PD69200_MSG_KEY_REQUEST, + self.__calc_msg_echo(), + POE_PD69200_MSG_SUB_GLOBAL, + POE_PD69200_MSG_SUB1_SYSTEM_STATUS, + ] + return self.__run_communication_protocol(command, self._msg_delay, PoeMsgParser.MessageType.MSG_SYSTEM_STATUS) + + def set_individual_mask(self, mask_num, enDis): + command = [ + POE_PD69200_MSG_KEY_COMMAND, + self.__calc_msg_echo(), + POE_PD69200_MSG_SUB_GLOBAL, + POE_PD69200_MSG_SUB1_INDV_MSK, + mask_num, + enDis, + ] + return self.__run_communication_protocol(command, self._msg_delay, PoeMsgParser.MessageType.MSG_CMD_STATUS) + + def get_individual_mask_regs(self, mask_num): + command = [ + POE_PD69200_MSG_KEY_REQUEST, + self.__calc_msg_echo(), + POE_PD69200_MSG_SUB_GLOBAL, + POE_PD69200_MSG_SUB1_INDV_MSK, + mask_num, + ] + return self.__run_communication_protocol(command, self._msg_delay, PoeMsgParser.MessageType.MSG_INDV_MASK) + + def __get_software_version(self): + command = [ + POE_PD69200_MSG_KEY_REQUEST, + self.__calc_msg_echo(), + POE_PD69200_MSG_SUB_GLOBAL, + POE_PD69200_MSG_SUB1_VERSIONS, + POE_PD69200_MSG_SUB2_SW_VERSION, + ] + return self.__run_communication_protocol(command, self._msg_delay, PoeMsgParser.MessageType.MSG_SW_VERSION) + + def set_temp_matrix(self, port_index, phy_port_a, phy_port_b=0xFF): + command = [ + POE_PD69200_MSG_KEY_COMMAND, + self.__calc_msg_echo(), + POE_PD69200_MSG_SUB_CHANNEL, + POE_PD69200_MSG_SUB1_TEMP_MATRIX, + port_index, + phy_port_a, + phy_port_b, + ] + return self.__run_communication_protocol(command, self._msg_delay, PoeMsgParser.MessageType.MSG_CMD_STATUS) + + def get_temp_matrix(self, port_index): + command = [ + POE_PD69200_MSG_KEY_REQUEST, + self.__calc_msg_echo(), + POE_PD69200_MSG_SUB_CHANNEL, + POE_PD69200_MSG_SUB1_TEMP_MATRIX, + port_index, + ] + return self.__run_communication_protocol(command, self._msg_delay) + + def program_active_matrix(self): + command = [ + POE_PD69200_MSG_KEY_COMMAND, + self.__calc_msg_echo(), + POE_PD69200_MSG_SUB_GLOBAL, + POE_PD69200_MSG_SUB1_TEMP_MATRIX, + ] + return self.__run_communication_protocol(command, self._msg_delay, PoeMsgParser.MessageType.MSG_CMD_STATUS) + + def get_active_matrix(self, port_index: int) -> dict[str, int]: + command = [ + POE_PD69200_MSG_KEY_REQUEST, + self.__calc_msg_echo(), + POE_PD69200_MSG_SUB_CHANNEL, + POE_PD69200_MSG_SUB1_CH_MATRIX, + port_index, + ] + return self.__run_communication_protocol(command, self._msg_delay, PoeMsgParser.MessageType.MSG_ACTIVE_MATRIX) + + def set_port_type_and_sum_as_tppl(self, port_index: int, port_type: int, sum_as_tppl: int) -> int: + """Set the port type and the Sum_as_TPPL field for a given port. + + Args: + port_index (int): Port ID (0-based) + type (int): Port type engineering value + sum_as_tppl (int): Sum_as_TPPL engineering value + + Returns: + int: 0 if successful. != 0 otherwise + """ + command = [ + POE_PD69200_MSG_KEY_COMMAND, + self.__calc_msg_echo(), + POE_PD69200_MSG_SUB_CHANNEL, + POE_PD69200_MSG_SUB1_PORT_4PAIR, + port_index, + POE_PD69200_MSG_N, + 0xFF, + 0xFF, + 0xFF, + port_type, + sum_as_tppl, + POE_PD69200_MSG_N, + ] + return self.__run_communication_protocol(command, self._msg_delay, PoeMsgParser.MessageType.MSG_CMD_STATUS) + + def set_port_en_dis(self, port_index, en_dis): + command = [ + POE_PD69200_MSG_KEY_COMMAND, + self.__calc_msg_echo(), + POE_PD69200_MSG_SUB_CHANNEL, + POE_PD69200_MSG_SUB1_EN_DIS, + port_index, + POE_PD69200_MSG_DATA_CMD_ENDIS_ONLY | en_dis, + POE_PD69200_MSG_DATA_PORT_TYPE_AT, + ] + return self.__run_communication_protocol(command, self._msg_delay, PoeMsgParser.MessageType.MSG_CMD_STATUS) + + def get_all_ports_en_dis(self) -> dict[str, list]: + """Get all port enable/disable state, depending on the + 802.3bt support. + + Returns: + dict: Parsed data + """ + if self.bt_support: + return self.__bt_get_all_ports_en_dis() + + command = [ + POE_PD69200_MSG_KEY_REQUEST, + self.__calc_msg_echo(), + POE_PD69200_MSG_SUB_GLOBAL, + POE_PD69200_MSG_SUB1_EN_DIS, + ] + return self.__run_communication_protocol(command, self._msg_delay, PoeMsgParser.MessageType.MSG_ALL_PORTS_ENDIS) + + # port range: 0x00 to 0x2F, 'AllChannels' = 0x80 + def set_port_power_limit(self, port_index, power_limit): + command = [ + POE_PD69200_MSG_KEY_COMMAND, + self.__calc_msg_echo(), + POE_PD69200_MSG_SUB_CHANNEL, + POE_PD69200_MSG_SUB1_SUPPLY, + port_index, + power_limit >> 8, + power_limit & 0xFF, + ] + return self.__run_communication_protocol(command, self._msg_delay, PoeMsgParser.MessageType.MSG_CMD_STATUS) + + def get_port_power_limit(self, port_index): + command = [ + POE_PD69200_MSG_KEY_REQUEST, + self.__calc_msg_echo(), + POE_PD69200_MSG_SUB_CHANNEL, + POE_PD69200_MSG_SUB1_SUPPLY, + port_index, + ] + return self.__run_communication_protocol( + command, self._msg_delay, PoeMsgParser.MessageType.MSG_PORT_POWER_LIMIT + ) + + def set_port_priority(self, port_index, priority): + command = [ + POE_PD69200_MSG_KEY_COMMAND, + self.__calc_msg_echo(), + POE_PD69200_MSG_SUB_CHANNEL, + POE_PD69200_MSG_SUB1_PRIORITY, + port_index, + priority, + ] + return self.__run_communication_protocol(command, self._msg_delay, PoeMsgParser.MessageType.MSG_CMD_STATUS) + + def get_port_priority(self, port_index): + command = [ + POE_PD69200_MSG_KEY_REQUEST, + self.__calc_msg_echo(), + POE_PD69200_MSG_SUB_CHANNEL, + POE_PD69200_MSG_SUB1_PRIORITY, + port_index, + ] + return self.__run_communication_protocol(command, self._msg_delay, PoeMsgParser.MessageType.MSG_PORT_PRIORITY) + + def get_port_status(self, port_index): + command = [ + POE_PD69200_MSG_KEY_REQUEST, + self.__calc_msg_echo(), + POE_PD69200_MSG_SUB_CHANNEL, + POE_PD69200_MSG_SUB1_PORT_STATUS, + port_index, + ] + return self.__run_communication_protocol(command, self._msg_delay, PoeMsgParser.MessageType.MSG_PORT_STATUS) + + def set_pm_method(self, pm1, pm2, pm3): + command = [ + POE_PD69200_MSG_KEY_COMMAND, + self.__calc_msg_echo(), + POE_PD69200_MSG_SUB_GLOBAL, + POE_PD69200_MSG_SUB1_SUPPLY, + POE_PD69200_MSG_SUB2_PWR_MANAGE_MODE, + pm1, + pm2, + pm3, + ] + return self.__run_communication_protocol(command, self._msg_delay, PoeMsgParser.MessageType.MSG_CMD_STATUS) + + def get_pm_method(self): + command = [ + POE_PD69200_MSG_KEY_REQUEST, + self.__calc_msg_echo(), + POE_PD69200_MSG_SUB_GLOBAL, + POE_PD69200_MSG_SUB1_SUPPLY, + POE_PD69200_MSG_SUB2_PWR_MANAGE_MODE, + ] + return self.__run_communication_protocol(command, self._msg_delay, PoeMsgParser.MessageType.MSG_PM_METHOD) + + def get_total_power(self): + command = [ + POE_PD69200_MSG_KEY_REQUEST, + self.__calc_msg_echo(), + POE_PD69200_MSG_SUB_GLOBAL, + POE_PD69200_MSG_SUB1_SUPPLY, + POE_PD69200_MSG_SUB2_TOTAL_PWR, + ] + return self.__run_communication_protocol(command, self._msg_delay, PoeMsgParser.MessageType.MSG_TOTAL_POWER) + + def set_power_bank(self, bank, power_limit): + command = [ + POE_PD69200_MSG_KEY_COMMAND, + self.__calc_msg_echo(), + POE_PD69200_MSG_SUB_GLOBAL, + POE_PD69200_MSG_SUB1_SUPPLY, + POE_PD69200_MSG_SUB2_PWR_BUDGET, + bank, + ] + command += [x for x in int(power_limit).to_bytes(2, byteorder="big")] + command += [x for x in int(self._max_shutdown_voltage).to_bytes(2, byteorder="big")] + command += [x for x in int(self._min_shutdown_voltage).to_bytes(2, byteorder="big")] + command.append(self._guard_band) + return self.__run_communication_protocol(command, self._msg_delay, PoeMsgParser.MessageType.MSG_CMD_STATUS) + + def get_power_bank(self, bank): + command = [ + POE_PD69200_MSG_KEY_REQUEST, + self.__calc_msg_echo(), + POE_PD69200_MSG_SUB_GLOBAL, + POE_PD69200_MSG_SUB1_SUPPLY, + POE_PD69200_MSG_SUB2_PWR_BUDGET, + bank, + ] + return self.__run_communication_protocol(command, self._msg_delay, PoeMsgParser.MessageType.MSG_POWER_BANK) + + def get_power_supply_params(self): + command = [ + POE_PD69200_MSG_KEY_REQUEST, + self.__calc_msg_echo(), + POE_PD69200_MSG_SUB_GLOBAL, + POE_PD69200_MSG_SUB1_SUPPLY, + POE_PD69200_MSG_SUB2_MAIN, + ] + return self.__run_communication_protocol( + command, self._msg_delay, PoeMsgParser.MessageType.MSG_POWER_SUPPLY_PARAMS + ) + + def get_port_measurements(self, port_index): + command = [ + POE_PD69200_MSG_KEY_REQUEST, + self.__calc_msg_echo(), + POE_PD69200_MSG_SUB_CHANNEL, + POE_PD69200_MSG_SUB1_PARAMS, + port_index, + ] + return self.__run_communication_protocol( + command, self._msg_delay, PoeMsgParser.MessageType.MSG_PORT_MEASUREMENTS + ) + + def get_poe_device_parameters(self, csnum): + command = [ + POE_PD69200_MSG_KEY_REQUEST, + self.__calc_msg_echo(), + POE_PD69200_MSG_SUB_GLOBAL, + POE_PD69200_MSG_SUB1_DEV_PARAMS, + csnum, + ] + return self.__run_communication_protocol( + command, self._msg_delay, PoeMsgParser.MessageType.MSG_POE_DEVICE_STATUS + ) + + def get_poe_versions(self): + versions = self.__get_software_version() + prod = str(versions.get(PROD_NUM)) + sw_ver = int(versions.get(SW_VERSION)) + major_ver = str(int(sw_ver // 100)) + minor_ver = str(int(sw_ver // 10) % 10) + pa_ver = str(int(sw_ver % 10)) + return f"{prod}.{major_ver}.{minor_ver}.{pa_ver}" + + def get_current_power_bank(self): + params = self.get_power_supply_params() + return params.get(POWER_BANK) + + def get_poe_port(self, port_id): + return PoePort(self, port_id) + + def get_ports_status( + self, ports: list[int], more_info: bool = True, log_port_status: bool = False + ) -> list[OrderedDict]: + ports_info = [] + for port in ports: + info = PoePort(self, port, log_port_status).get_current_status(more_info) + ports_info.append(info) + return ports_info + + def get_system_information(self, verbose: bool = True) -> OrderedDict: + return PoeSystem(self, self._port_count, self._power_bank_to_str).get_current_status(verbose) + + def get_port_l2_pse_data(self, port_index) -> dict: + """Get the Layer 2 PSE data necessary for advertising the + power capabilities of the port via LLDP. This includes the PSE + allocated power at PD input, the PD requested power, the cable length + and the port priority + + Args: + port_index (_type_): Port ID (0-based) + + Returns: + dict: Parsed data + """ + command = [ + POE_PD69200_MSG_KEY_REQUEST, + self.__calc_msg_echo(), + POE_PD69200_MSG_SUB_CHANNEL, + POE_PD69200_BT_MSG_SUB1_LAYER2_LLDP_PSE, + port_index, + ] + + return self.__run_communication_protocol(command, self._msg_delay, PoeMsgParser.MessageType.MSG_LLDP_PSE_DATA) + + def bt_get_port_measurements(self, port_index): + if not self._bt_support: + raise AssertionError("The PoE chipset doesn't support 802.3bt") + + command = [ + POE_PD69200_MSG_KEY_REQUEST, + self.__calc_msg_echo(), + POE_PD69200_MSG_SUB_CHANNEL, + POE_PD69200_BT_MSG_SUB1_PORTS_MEASUREMENT, + port_index, + ] + return self.__run_communication_protocol( + command, self._msg_delay, PoeMsgParser.MessageType.MSG_BT_PORT_MEASUREMENTS + ) + + def bt_get_port_parameters(self, port_index): + if not self._bt_support: + raise AssertionError("The PoE chipset doesn't support 802.3bt") + + command = [ + POE_PD69200_MSG_KEY_REQUEST, + self.__calc_msg_echo(), + POE_PD69200_MSG_SUB_CHANNEL, + POE_PD69200_BT_MSG_SUB1_PORT_CONFIG, + port_index, + ] + return self.__run_communication_protocol( + command, self._msg_delay, PoeMsgParser.MessageType.MSG_BT_PORT_PARAMETERS + ) + + def bt_get_port_class(self, port_index: int) -> dict: + """Get the BT port class, including the measured class, + assigned class for both modes and the port TPPL + + Args: + port_index (int): Port ID (0-based) + + Returns: + dict: Parsed data + """ + if not self._bt_support: + raise AssertionError("The PoE chipset doesn't support 802.3bt") + + command = [ + POE_PD69200_MSG_KEY_REQUEST, + self.__calc_msg_echo(), + POE_PD69200_MSG_SUB_CHANNEL, + POE_PD69200_BT_MSG_SUB1_PORTS_CLASS, + port_index, + ] + return self.__run_communication_protocol(command, self._msg_delay, PoeMsgParser.MessageType.MSG_BT_PORT_CLASS) + + def bt_set_port_params(self, port_index: int, pm_mode: int, op_mode: int) -> int: + """Set the power management mode and operation mode fields + for a BT port + + Args: + port_index (int): Port ID (0-based) + pm_mode (int): Port PM mode engineering value + op_mode (int): Port operation mode engineering value + + Returns: + int: 0 if successful, != 0 otherwise + """ + if not self._bt_support: + raise AssertionError("The PoE chipset doesn't support 802.3bt") + + command = [ + POE_PD69200_MSG_KEY_COMMAND, + self.__calc_msg_echo(), + POE_PD69200_MSG_SUB_CHANNEL, + POE_PD69200_BT_MSG_SUB1_PORT_CONFIG, + port_index, + POE_PD69200_BT_MSG_DATA_CMD_ENDIS_NO_CHANGE, + pm_mode | POE_PD69200_BT_MSG_DATA_PORT_CLASS_ERROR_NO_CHANGE, + op_mode, + POE_PD69200_BT_MSG_DATA_PORT_NO_ADDITIONAL_POWER, + POE_PD69200_BT_MSG_DATA_PORT_PRIORITY_NO_CHANGE, + ] + return self.__run_communication_protocol(command, self._msg_delay, PoeMsgParser.MessageType.MSG_CMD_STATUS) + + def bt_get_system_status(self): + if not self._bt_support: + raise AssertionError("The PoE chipset doesn't support 802.3bt") + + command = [ + POE_PD69200_MSG_KEY_REQUEST, + self.__calc_msg_echo(), + POE_PD69200_MSG_SUB_GLOBAL, + POE_PD69200_BT_MSG_SUB1_SYSTEM_STATUS, + ] + return self.__run_communication_protocol( + command, self._msg_delay, PoeMsgParser.MessageType.MSG_BT_SYSTEM_STATUS + ) + + def get_system_status2(self): + command = [ + POE_PD69200_MSG_KEY_REQUEST, + self.__calc_msg_echo(), + POE_PD69200_MSG_SUB_GLOBAL, + POE_PD69200_BT_MSG_SUB1_SYSTEM_STATUS2, + ] + return self.__run_communication_protocol(command, self._msg_delay, PoeMsgParser.MessageType.MSG_SYSTEM_STATUS2) + + def bt_set_port_en_dis(self, port_index, en_dis): + if not self._bt_support: + raise AssertionError("The PoE chipset doesn't support 802.3bt") + + command = [ + POE_PD69200_MSG_KEY_COMMAND, + self.__calc_msg_echo(), + POE_PD69200_MSG_SUB_CHANNEL, + POE_PD69200_BT_MSG_SUB1_PORT_CONFIG, + port_index, + POE_PD69200_MSG_DATA_CMD_ENDIS_ONLY | en_dis, + POE_PD69200_BT_MSG_DATA_PORT_MODE_NO_CHANGE | POE_PD69200_BT_MSG_DATA_PORT_CLASS_ERROR_NO_CHANGE, + POE_PD69200_BT_MSG_DATA_PORT_OP_MODE_NO_CHANGE, + POE_PD69200_BT_MSG_DATA_PORT_NO_ADDITIONAL_POWER, + POE_PD69200_BT_MSG_DATA_PORT_PRIORITY_NO_CHANGE, + ] + return self.__run_communication_protocol(command, self._msg_delay, PoeMsgParser.MessageType.MSG_CMD_STATUS) + + def bt_get_port_status(self, port_index: int) -> dict: + """Get the BT port status, including the enable/disable state, + the assigned class for each mode, the measured port power, the + last shutdown error status and the port event + + Args: + port_index (int): Port ID (0-based) + + Returns: + dict: Parsed data + """ + if not self._bt_support: + raise AssertionError("The PoE chipset doesn't support 802.3bt") + + command = [ + POE_PD69200_MSG_KEY_REQUEST, + self.__calc_msg_echo(), + POE_PD69200_MSG_SUB_CHANNEL, + POE_PD69200_BT_MSG_SUB1_PORT_STATUS, + port_index, + ] + return self.__run_communication_protocol(command, self._msg_delay, PoeMsgParser.MessageType.MSG_BT_PORT_STATUS) + + def __bt_get_all_ports_en_dis(self) -> dict: + """Get all BT ports en/dis status + This can be done, for the BT firmware, only by querying each port + individually. + + Returns: + dict: Parsed state + """ + ports = list(range(self._port_count)) + statuses = self.get_ports_status(ports, False, False) + return {ENDIS: [1 if status[ENDIS] == "enable" else 0 for status in statuses]} + + def bt_get_port_l2_lldp_pd_request(self, port_index: int) -> dict: + """Get the BT Layer 2 PD power request, including the PD requested + power for both modes and the cable length requirement + + Args: + port_index (int): Port ID (0-based) + + Returns: + dict: Parsed data + """ + if not self._bt_support: + raise AssertionError("The PoE chipset doesn't support 802.3bt") + + command = [ + POE_PD69200_MSG_KEY_REQUEST, + self.__calc_msg_echo(), + POE_PD69200_MSG_SUB_CHANNEL, + POE_PD69200_BT_MSG_SUB1_BT_LAYER2_LLDP_PD, + port_index, + ] + return self.__run_communication_protocol(command, self._msg_delay, PoeMsgParser.MessageType.MSG_BT_LLDP_PD_DATA) + + def bt_get_port_l2_lldp_pse_data(self, port_index: int) -> dict: + """Get the BT Layer 2 PSE data necessary for advertising the + power capabilities of the port via LLDP. This includes the PSE + allocated power at PD input for both modes, the PSE max power, + assigned class and the BT PSE powering status and + the power pairs ext bits IEEE fields + + Args: + port_index (int): Port ID (0-based) + + Returns: + dict: Parsed data + """ + if not self._bt_support: + raise AssertionError("The PoE chipset doesn't support 802.3bt") + + command = [ + POE_PD69200_MSG_KEY_REQUEST, + self.__calc_msg_echo(), + POE_PD69200_MSG_SUB_CHANNEL, + POE_PD69200_BT_MSG_SUB1_BT_LAYER2_LLDP_PSE, + port_index, + ] + return self.__run_communication_protocol( + command, self._msg_delay, PoeMsgParser.MessageType.MSG_BT_LLDP_PSE_DATA + ) + + def bt_set_port_l2_lldp_pd_request( + self, + port_index: int, + power_limit_single: int, + power_limit_mode_a: int, + power_limit_mode_b: int, + cable_len: int, + priority: int = POE_PD69200_BT_MSG_DATA_PORT_PRIORITY_NO_CHANGE, + ) -> None: + """Set the BT TPPL for a port, as a result of an L2 power request + + Args: + port_index (int): Port ID (0-based) + power_limit_single (int): Requested power at PD input for + single-signature PDs (in 0.1W) + power_limit_mode_a (int): Requested power at PD input for + mode A (in 0.1W) + power_limit_mode_b (int): Requested power at PD input for + mode B (in 0.1W) + cable_len (int): Engineering value used to compute the cable + resistance (0...0xA, one step means 10 meters). Setting this to 0 + will tell the controller to not compensate for the cable loss + priority (int): Port priority + """ + if not self._bt_support: + raise AssertionError("The PoE chipset doesn't support 802.3bt") + + command = [ + POE_PD69200_MSG_KEY_COMMAND, + self.__calc_msg_echo(), + POE_PD69200_MSG_SUB_CHANNEL, + POE_PD69200_BT_MSG_SUB1_BT_LAYER2_LLDP_PD, + port_index, + power_limit_single >> 8, + power_limit_single & 0xFF, + power_limit_mode_a >> 8, + power_limit_mode_a & 0xFF, + power_limit_mode_b >> 8, + power_limit_mode_b & 0xFF, + cable_len & 0x0F, + priority & 0x0F, + ] + self.__run_communication_protocol(command, self._msg_delay) + + def bt_get_port_reserve_power_request(self, port_index): + if not self._bt_support: + raise AssertionError("The PoE chipset doesn't support 802.3bt") + + # as per the spec, + # SUB1 value to set for "Get BT Port Reserve Power Request" + # is 0x55 + # POE_PD69200_MSG_SUB1_RESET = 0x55 + command = [ + POE_PD69200_MSG_KEY_REQUEST, + self.__calc_msg_echo(), + POE_PD69200_MSG_SUB_CHANNEL, + POE_PD69200_MSG_SUB1_RESET, + port_index, + ] + return self.__run_communication_protocol( + command, self._msg_delay, PoeMsgParser.MessageType.MSG_PORT_POWER_LIMIT + ) + + def bt_set_port_priority(self, port_index, priority): + if not self._bt_support: + raise AssertionError("The PoE chipset doesn't support 802.3bt") + + command = [ + POE_PD69200_MSG_KEY_COMMAND, + self.__calc_msg_echo(), + POE_PD69200_MSG_SUB_CHANNEL, + POE_PD69200_BT_MSG_SUB1_PORT_CONFIG, + port_index, + POE_PD69200_BT_MSG_DATA_CMD_ENDIS_NO_CHANGE, + POE_PD69200_BT_MSG_DATA_PORT_MODE_NO_CHANGE | POE_PD69200_BT_MSG_DATA_PORT_CLASS_ERROR_NO_CHANGE, + POE_PD69200_BT_MSG_DATA_PORT_OP_MODE_NO_CHANGE, + POE_PD69200_BT_MSG_DATA_PORT_NO_ADDITIONAL_POWER, + priority, + ] + return self.__run_communication_protocol(command, self._msg_delay, PoeMsgParser.MessageType.MSG_CMD_STATUS) + + +class PoePort: + + def __init__(self, driver: PoeDriver_microsemi_pd69200, port_id: int, log_port_status: bool = False) -> None: + self._driver: PoeDriver_microsemi_pd69200 = driver + self._port_id: int = port_id + self._en_dis: str = "enable" + self._status: str = "" + self._priority: str = "" + self._protocol: str = "" + self._latch: int = 0x00 + self._class_type: str = "0" + self._FPairEn: int = 0 + self._power_consump: int = 0 + self._power_limit: int = 0 + self._voltage: int = 0 + self._current: int = 0 + # TODO: dual-signature + self._measured_class: int = 0 + self._bt_support: bool = self._driver.bt_support + self._log_port_status = log_port_status + + def __update_port_status(self) -> None: + if self._bt_support: + params = self._driver.bt_get_port_parameters(self._port_id) + params_class = self._driver.bt_get_port_class(self._port_id) + port_status = params.get(STATUS) + if self._log_port_status: + PoeLog().dbg(f"Port {self._port_id} is in status " f"0x{port_status:02X}") + # TODO: extract conversion table as a strategy. + self._status = TBL_BT_STATUS_TO_CFG[port_status] + self._en_dis = TBL_ENDIS_TO_CFG[params.get(ENDIS)] + self._measured_class = params_class[MEASURED_CLASS_ALT_A] + # Delivers power, port status: 0x80-0x91 + if 0x80 <= port_status <= 0x91: + if self._measured_class >= 0 and self._measured_class <= 4: + self._protocol = "IEEE802.3AF/AT" + elif self._measured_class >= 5 and self._measured_class <= 8: + self._protocol = "IEEE802.3BT" + else: + self._protocol = "N/A" + else: + self._protocol = "N/A" + + self._priority = TBL_PRIORITY_TO_CFG[params.get(PRIORITY)] + + power_limit = self._driver.bt_get_port_class(self._port_id) + port_class = power_limit[ASSIGNED_CLASS_ALT_A] + self._class_type = TBL_BT_CLASS_TO_CFG[port_class] + self._power_limit = power_limit[TPPL] + + meas = self._driver.bt_get_port_measurements(self._port_id) + self._current = meas.get(CURRENT) + self._power_consump = meas.get(POWER_CONSUMP) + self._voltage = meas.get(VOLTAGE) + else: + status = self._driver.get_port_status(self._port_id) + self._en_dis = TBL_ENDIS_TO_CFG[status.get(ENDIS)] + port_status = status.get(STATUS) + if self._log_port_status: + PoeLog().dbg(f"Port {self._port_id} is in status " f"0x{port_status:02X}") + self._status = TBL_STATUS_TO_CFG[port_status] + self._latch = status.get(LATCH) + self._class_type = TBL_CLASS_TO_CFG[status.get(CLASS)] + self._protocol = TBL_PROTOCOL_TO_CFG[status.get(PROTOCOL)] + self._FPairEn = status.get(EN_4PAIR) + + priority = self._driver.get_port_priority(self._port_id) + self._priority = TBL_PRIORITY_TO_CFG[priority.get(PRIORITY)] + + power_limit = self._driver.get_port_power_limit(self._port_id) + self._power_limit = power_limit.get(PPL) + + meas = self._driver.get_port_measurements(self._port_id) + self._current = meas.get(CURRENT) + self._power_consump = meas.get(POWER_CONSUMP) + self._voltage = meas.get(VOLTAGE) + + def get_current_status(self, more_info=True, log_status=False) -> OrderedDict: + self.__update_port_status() + port_status = OrderedDict() + if self._bt_support: + port_status[PORT_ID] = self._port_id + 1 + port_status[ENDIS] = self._en_dis + port_status[PRIORITY] = self._priority + port_status[POWER_LIMIT] = self._power_limit * 100 + if more_info: + port_status[STATUS] = self._status + port_status[PROTOCOL] = self._protocol + port_status[LATCH] = self._latch + port_status[EN_4PAIR] = self._FPairEn + port_status[CLASS] = self._class_type + port_status[POWER_CONSUMP] = self._power_consump * 100 + port_status[VOLTAGE] = self._voltage // 10 + port_status[CURRENT] = self._current + else: + port_status[PORT_ID] = self._port_id + 1 + port_status[ENDIS] = self._en_dis + port_status[PRIORITY] = self._priority + port_status[POWER_LIMIT] = self._power_limit + if more_info: + port_status[STATUS] = self._status + port_status[LATCH] = self._latch + port_status[PROTOCOL] = self._protocol + port_status[EN_4PAIR] = self._FPairEn + port_status[CLASS] = self._class_type + port_status[POWER_CONSUMP] = self._power_consump + port_status[VOLTAGE] = self._voltage // 10 + port_status[CURRENT] = self._current + + return port_status + + def set_en_dis(self, set_val, current_enDis=None, readback=False): + status = 0 + result_get = set_val + if ( + current_enDis is not None + and ENDIS in current_enDis + and self._port_id <= (len(current_enDis[ENDIS]) - 1) + and current_enDis[ENDIS][self._port_id] == set_val + ): + return status + else: + if self._bt_support: + result = self._driver.bt_set_port_en_dis(self._port_id, set_val) + if readback: + result_get = self._driver.bt_get_port_parameters(self._port_id).get(ENDIS) + if result == 0 or result_get == set_val: + status = result + else: + result = self._driver.set_port_en_dis(self._port_id, set_val) + if readback: + result_get = self._driver.get_port_status(self._port_id).get(ENDIS) + if result == 0 or result_get == set_val: + status = result + + return status + + def set_power_limit(self, set_val, readback=False): + ret_flag = 0 + result_get = set_val + if self._bt_support: + # Convert from 0.1W to mW (refer to chapter 3.5.2 in the user + # manual). + # Note: checking if the operation was successful can be done + # only through a readback. + set_val //= 100 + self._driver.bt_set_port_l2_lldp_pd_request(self._port_id, set_val, 0, 0, 0) + ret_flag = True + + if readback: + return self._driver.bt_get_port_reserve_power_request(self._port_id).get(TPPL) + else: + result = self._driver.set_port_power_limit(self._port_id, set_val) + if readback: + result_get = self._driver.get_port_power_limit(self._port_id).get(PPL) + if result == 0 or result_get == set_val: + ret_flag = result + return ret_flag + + def get_power_reserve(self): + if self._bt_support: + result = self._driver.get_port_power_limit(self._port_id) + return result + return None + + def set_priority(self, set_val, readback=False): + ret_flag = 0 + result_get = set_val + if self._bt_support: + result = self._driver.bt_set_port_priority(self._port_id, set_val) + if readback: + result_get = self._driver.bt_get_port_parameters(self._port_id).get(PRIORITY) + if result == 0 or result_get == set_val: + ret_flag = result + else: + result = self._driver.set_port_priority(self._port_id, set_val) + if readback: + result_get = self._driver.get_port_priority(self._port_id).get(PRIORITY) + if result == 0 or result_get == set_val: + ret_flag = result + return ret_flag + + def set_all_params(self, params: dict, current_en_dis: dict = {}, readback: bool = False) -> dict: + """Set the port enable/disable, priority and/or power limit + + Args: + params (dict): The port parameters + current_enDis (dict, optional): Current ports. Defaults to {}. + readback (bool, optional): Readback flag. Defaults to False. + + Returns: + dict: Each operation result as a dictionary + """ + status = {} + if ENDIS in params: + set_val = TBL_ENDIS_TO_DRV[params[ENDIS]] + status[ENDIS] = self.set_en_dis(set_val, current_en_dis, readback) + + if PRIORITY in params: + set_val = TBL_PRIORITY_TO_DRV[params[PRIORITY]] + status[PRIORITY] = self.set_priority(set_val, readback) + + if POWER_LIMIT in params: + set_val = params[POWER_LIMIT] + status[POWER_LIMIT] = self.set_power_limit(set_val, readback) + + return status + + +class PoeSystem: + # TODO: Extract driver interface to avoid circular import and tight + # coupling. + # Having this in a separate file without having a common driver interface + # will result in a circular import. + + def __init__(self, driver: PoeDriver_microsemi_pd69200, port_count: int, power_bank_to_str: Callable[[int], str]): + self._driver: PoeDriver_microsemi_pd69200 = driver + self._total_ports: int = port_count + self._power_bank_to_str: Callable[[int], str] = power_bank_to_str + self._total_power: int = 0 + self._calculated_power: int = 0 + self._power_avail: int = 0 + self._power_bank: int = 0 + self._max_sd_volt: int = 0 + self._min_sd_volt: int = 0 + self._power_src: str = "" + self._cpu_status1: int = 0 + self._cpu_status2: int = 0 + self._fac_default: int = 0 + self._gie: int = 0 + self._priv_label: int = 0 + self._user_byte: int = 0 + self._device_fail: int = 0 + self._temp_disco: int = 0 + self._temp_alarm: int = 0 + self._intr_reg: int = 0x00 + self._pm1: int = 0 + self._pm2: int = 0 + self._pm3: int = 0 + self._nvm_user_byte: int = 0 + self._found_device: int = 0 + self._event_exist: int = 0 + self._bt_support: bool = self._driver.bt_support + + def __update_system_status(self): + power_params = self._driver.get_total_power() + psu_params = self._driver.get_power_supply_params() + self._total_power = power_params.get(POWER_LIMIT) + self._consumed_power = power_params.get(POWER_CONSUMP) + self._calculated_power = power_params.get(CALCULATED_POWER) + self._power_avail = power_params.get(POWER_AVAIL) + self._max_sd_volt = psu_params.get(MAX_SD_VOLT) + self._min_sd_volt = psu_params.get(MIN_SD_VOLT) + self._power_bank = power_params.get(POWER_BANK) + self._power_src = self._power_bank_to_str(self._power_bank) + if self._bt_support: + system_status = self._driver.bt_get_system_status() + self._cpu_status2 = system_status.get(CPU_STATUS2) + self._fac_default = system_status.get(FAC_DEFAULT) + self._priv_label = system_status.get(PRIV_LABEL) + self._nvm_user_byte = system_status.get(NVM_USER_BYTE) + self._found_device = system_status.get(FOUND_DEVICE) + self._event_exist = system_status.get(EVENT_EXIST) + else: + system_status = self._driver.get_system_status() + self._cpu_status1 = system_status.get(CPU_STATUS1) + self._cpu_status2 = system_status.get(CPU_STATUS2) + self._fac_default = system_status.get(FAC_DEFAULT) + self._gie = system_status.get(GIE) + self._priv_label = system_status.get(PRIV_LABEL) + self._user_byte = system_status.get(USER_BYTE) + self._device_fail = system_status.get(DEVICE_FAIL) + self._temp_disco = system_status.get(TEMP_DISCO) + self._temp_alarm = system_status.get(TEMP_ALARM) + self._intr_reg = system_status.get(INTR_REG) + + pm_method = self._driver.get_pm_method() + self._pm1 = pm_method.get(PM1) + self._pm2 = pm_method.get(PM2) + self._pm3 = pm_method.get(PM3) + + def get_current_status(self, verbose=True): + self.__update_system_status() + system_status = OrderedDict() + system_status[TOTAL_PORTS] = self._total_ports + system_status[TOTAL_POWER] = self._total_power + system_status[POWER_CONSUMP] = self._consumed_power + system_status[CALCULATED_POWER] = self._calculated_power + system_status[POWER_AVAIL] = self._power_avail + system_status[POWER_BANK] = self._power_bank + system_status[POWER_SRC] = self._power_src + if verbose: + system_status[MAX_SD_VOLT] = self._max_sd_volt // 10 + system_status[MIN_SD_VOLT] = self._min_sd_volt // 10 + system_status[PM1] = self._pm1 + system_status[PM2] = self._pm2 + system_status[PM3] = self._pm3 + system_status[CPU_STATUS1] = self._cpu_status1 + # cpu status2 on AT and BT + system_status[CPU_STATUS2] = self._cpu_status2 + system_status[FAC_DEFAULT] = self._fac_default + system_status[GIE] = self._gie + system_status[PRIV_LABEL] = self._priv_label + system_status[USER_BYTE] = self._user_byte + system_status[DEVICE_FAIL] = self._device_fail + system_status[TEMP_DISCO] = self._temp_disco + system_status[TEMP_ALARM] = self._temp_alarm + system_status[INTR_REG] = self._intr_reg + # only on BT + system_status[NVM_USER_BYTE] = self._nvm_user_byte + system_status[FOUND_DEVICE] = self._found_device + system_status[EVENT_EXIST] = self._event_exist + return system_status diff --git a/dentos-poe-agent/opt/poeagent/drivers/pd69200/poe_driver_def.py b/dentos-poe-agent/opt/poeagent/drivers/pd69200/poe_driver_def.py new file mode 100755 index 0000000..e19b739 --- /dev/null +++ b/dentos-poe-agent/opt/poeagent/drivers/pd69200/poe_driver_def.py @@ -0,0 +1,406 @@ +''' +Copyright 2021 Delta Electronic Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +''' + +# PD69200 Global Definitions +POE_PD69200_MSG_LEN = 15 +POE_PD69200_MSG_CSUM_LEN = 2 +POE_PD69200_MSG_N = 0x4E +POE_PD69200_COMM_RETRY_TIMES = 6 + +# PD69200 Message Structure +POE_PD69200_MSG_OFFSET_KEY = 0 +POE_PD69200_MSG_OFFSET_ECHO = 1 +POE_PD69200_MSG_OFFSET_SUB = 2 +POE_PD69200_MSG_OFFSET_SUB1 = 3 +POE_PD69200_MSG_OFFSET_SUB2 = 4 +POE_PD69200_MSG_OFFSET_DATA5 = 5 +POE_PD69200_MSG_OFFSET_DATA6 = 6 +POE_PD69200_MSG_OFFSET_DATA7 = 7 +POE_PD69200_MSG_OFFSET_DATA8 = 8 +POE_PD69200_MSG_OFFSET_DATA9 = 9 +POE_PD69200_MSG_OFFSET_DATA10 = 10 +POE_PD69200_MSG_OFFSET_DATA11 = 11 +POE_PD69200_MSG_OFFSET_DATA12 = 12 +POE_PD69200_MSG_OFFSET_CSUM_H = 13 +POE_PD69200_MSG_OFFSET_CSUM_L = 14 + +# PD69200 Message - Byte 1: KEY +POE_PD69200_MSG_KEY_COMMAND = 0x00 +POE_PD69200_MSG_KEY_PROGRAM = 0x01 +POE_PD69200_MSG_KEY_REQUEST = 0x02 +POE_PD69200_MSG_KEY_TELEMETRY = 0x03 +POE_PD69200_MSG_KEY_TEST = 0x04 +POE_PD69200_MSG_KEY_REPORT = 0x52 + +# PD69200 Message - Byte 3: SUB +POE_PD69200_MSG_SUB_CHANNEL = 0x05 +POE_PD69200_MSG_SUB_E2 = 0x06 +POE_PD69200_MSG_SUB_GLOBAL = 0x07 +POE_PD69200_MSG_SUB_RESTORE_FACT = 0x2D +POE_PD69200_MSG_SUB_USER_BYTE = 0x41 +POE_PD69200_MSG_SUB_FLASH = 0xFF + +# PD69200 Message - Byte 4: SUB1 +POE_PD69200_MSG_SUB1_PRIORITY = 0x0A +POE_PD69200_MSG_SUB1_SUPPLY = 0x0B +POE_PD69200_MSG_SUB1_PORT_4PAIR = 0xAF +POE_PD69200_MSG_SUB1_EN_DIS = 0x0C +POE_PD69200_MSG_SUB1_POWER_4PAIR = 0xC0 +POE_PD69200_MSG_SUB1_PORT_STATUS = 0x0E +POE_PD69200_MSG_SUB1_SAVE_CONFIG = 0x0F +POE_PD69200_MSG_SUB1_VERSIONS = 0x1E +POE_PD69200_MSG_SUB1_PARAMS = 0x25 +POE_PD69200_MSG_SUB1_PORTS_STATUS1 = 0x31 +POE_PD69200_MSG_SUB1_PORTS_STATUS2 = 0x32 +POE_PD69200_MSG_SUB1_PORTS_STATUS3 = 0x33 +POE_PD69200_MSG_SUB1_SYSTEM_STATUS = 0x3D +POE_PD69200_MSG_SUB1_TEMP_MATRIX = 0x43 +POE_PD69200_MSG_SUB1_CH_MATRIX = 0x44 +POE_PD69200_MSG_SUB1_PORTS_STATUS4 = 0x47 +POE_PD69200_MSG_SUB1_PORTS_STATUS5 = 0x48 +POE_PD69200_MSG_SUB1_PORTS_POW1 = 0x4B +POE_PD69200_MSG_SUB1_PORTS_POW2 = 0x4C +POE_PD69200_MSG_SUB1_PORTS_POW3 = 0x4D +POE_PD69200_MSG_SUB1_PORTS_POW4 = 0x4F +POE_PD69200_MSG_SUB1_PORTS_POW5 = 0x50 +POE_PD69200_MSG_SUB1_RESET = 0x55 +POE_PD69200_MSG_SUB1_INDV_MSK = 0x56 +POE_PD69200_MSG_SUB1_DEV_PARAMS = 0x87 +POE_PD69200_MSG_SUB1_PORTS_DLV_PWR = 0xC0 +POE_PD69200_MSG_SUB1_GETINTERRUPTMASK = 0x63 + +# PD69200 BT Message - Byte 4: SUB1 +POE_PD69200_BT_MSG_SUB1_SYSTEM_STATUS = 0xD0 +POE_PD69200_BT_MSG_SUB1_PORT_CONFIG = 0xC0 +POE_PD69200_BT_MSG_SUB1_PORT_STATUS = 0xC1 +POE_PD69200_BT_MSG_SUB1_PORTS_CLASS = 0xC4 +POE_PD69200_BT_MSG_SUB1_PORTS_MEASUREMENT = 0xC5 +POE_PD69200_BT_MSG_SUB1_BT_LAYER2_LLDP_PD = 0x50 +POE_PD69200_BT_MSG_SUB1_BT_LAYER2_LLDP_PSE = 0x51 +POE_PD69200_BT_MSG_SUB1_LAYER2_LLDP_PSE = 0xA8 +POE_PD69200_BT_MSG_SUB1_SYSTEM_STATUS2 = 0x84 + +POE_PD69200_BT_MSG_SUB1_PORT_STATUS_CFG1_ENDIS = 0x01 + +# PD69200 Message - Byte 5: SUB2 +POE_PD69200_MSG_SUB2_MAIN = 0x17 +POE_PD69200_MSG_SUB2_SW_VERSION = 0x21 +POE_PD69200_MSG_SUB2_PWR_BUDGET = 0x57 +POE_PD69200_MSG_SUB2_PWR_MANAGE_MODE = 0x5F +POE_PD69200_MSG_SUB2_TOTAL_PWR = 0x60 +POE_PD69200_MSG_SUB2_ALL_CHANNEL = 0x80 + +# PD69200 Message - Byte 6 to Byte 13: DATA +POE_PD69200_MSG_DATA_CLASS_0 = 0 +POE_PD69200_MSG_DATA_CLASS_1 = 1 +POE_PD69200_MSG_DATA_CLASS_2 = 2 +POE_PD69200_MSG_DATA_CLASS_3 = 3 +POE_PD69200_MSG_DATA_CLASS_4 = 4 +POE_PD69200_MSG_DATA_CLASS_5 = 5 + +POE_PD69200_MSG_DATA_PROTOCOL_AF = 0 +POE_PD69200_MSG_DATA_PROTOCOL_ATAF = 1 +POE_PD69200_MSG_DATA_PROTOCOL_AOH = 2 + +POE_PD69200_MSG_DATA_PORT_TYPE_AF = 0 +POE_PD69200_MSG_DATA_PORT_TYPE_AT = 1 +POE_PD69200_MSG_DATA_PORT_TYPE_AOH = 2 + +POE_PD69200_MSG_DATA_CMD_ENDIS_ONLY = 0 +POE_PD69200_MSG_DATA_CMD_DISABLE = 0 +POE_PD69200_MSG_DATA_CMD_ENABLE = 1 +POE_PD69200_BT_MSG_DATA_CMD_ENDIS_NO_CHANGE = 0xF + +POE_PD69200_MSG_DATA_PORT_PRIORITY_CRIT = 1 +POE_PD69200_MSG_DATA_PORT_PRIORITY_HIGH = 2 +POE_PD69200_MSG_DATA_PORT_PRIORITY_LOW = 3 + +POE_PD69200_MSG_DATA_PM1_DYNAMIC = 0 +POE_PD69200_MSG_DATA_PM1_USER_DEFINED = 0x80 +POE_PD69200_MSG_DATA_PM2_PPL = 0 +POE_PD69200_MSG_DATA_PM3_NO_COND = 0 + +POE_PD69200_MSG_DATA_SUM_AS_TPPL_DYNAMIC = 0 +POE_PD69200_MSG_DATA_SUM_AS_TPPL_STATIC = 1 + +# PD69200 BT Message - Byte 6: DATA +# Port Mode CFG2 +# BITS[3:0] BT Port PM Mode +POE_PD69200_BT_MSG_DATA_PORT_MODE_DYNAMIC = 0x0 +POE_PD69200_BT_MSG_DATA_PORT_MODE_TPPL = 0x01 +POE_PD69200_BT_MSG_DATA_PORT_MODE_NO_CHANGE = 0x0F +# BIT[7:4] Class Error Operation Select +POE_PD69200_BT_MSG_DATA_PORT_CLASS_ERROR_DISABLE = 0x0 +POE_PD69200_BT_MSG_DATA_PORT_CLASS_SSPD_3_DSPD_3 = 0x10 +POE_PD69200_BT_MSG_DATA_PORT_CLASS_SSPD_4_DSPD_3 = 0x20 +POE_PD69200_BT_MSG_DATA_PORT_CLASS_SSPD_6_DSPD_4 = 0x30 +POE_PD69200_BT_MSG_DATA_PORT_CLASS_SSPD_8_DSPD_5 = 0x40 +POE_PD69200_BT_MSG_DATA_PORT_CLASS_ERROR_NO_CHANGE = 0xF0 + +# Byte 7: DATA +# Port Operation Mode +POE_PD69200_BT_MSG_DATA_PORT_OP_MODE_NO_CHANGE = 0xFF +POE_PD69200_BT_MSG_DATA_PORT_OP_MODE_4P_60W_2P_30W = 0x01 +POE_PD69200_BT_MSG_DATA_PORT_OP_MODE_4P_30W_2P_30W = 0x09 + +# Byte 8: DATA +# Add Power for Port Mode +POE_PD69200_BT_MSG_DATA_PORT_NO_ADDITIONAL_POWER = 0x0 + +# Byte 9: DATA +# Port Priority Mode +POE_PD69200_BT_MSG_DATA_PORT_PRIORITY_NO_CHANGE = 0xFF + +# Layer 2 execution field +POE_PD69200_BT_MSG_DATA_LAYER2_IDLE = 0x0 +POE_PD69200_BT_MSG_DATA_LAYER2_REQ_PENDING = 0x1 +POE_PD69200_BT_MSG_DATA_LAYER2_REQ_EXECUTED = 0x2 +POE_PD69200_BT_MSG_DATA_LAYER2_REQ_INSUFFICIENT_POWER = 0x3 +POE_PD69200_BT_MSG_DATA_LAYER2_REQ_ERROR = 0x4 +POE_PD69200_BT_MSG_DATA_LAYER2_REQ_MISMATCH = 0x5 + +# Layer 2 usage field +POE_PD69200_BT_MSG_DATA_PORT_LAYER2_USAGE_OFF = 0x0 +POE_PD69200_BT_MSG_DATA_PORT_LAYER2_USAGE_L1 = 0x1 +POE_PD69200_MSG_DATA_PORT_LAYER2_USAGE_L1 = 0x2 +POE_PD69200_BT_MSG_DATA_PORT_LAYER2_USAGE_LLDP = 0x3 + +TBL_ENDIS_TO_CFG = {POE_PD69200_MSG_DATA_CMD_ENABLE: "enable", POE_PD69200_MSG_DATA_CMD_DISABLE: "disable"} + +TBL_ENDIS_TO_DRV = {"enable": POE_PD69200_MSG_DATA_CMD_ENABLE, "disable": POE_PD69200_MSG_DATA_CMD_DISABLE} + +TBL_PRIORITY_TO_CFG = { + POE_PD69200_MSG_DATA_PORT_PRIORITY_CRIT: "crit", + POE_PD69200_MSG_DATA_PORT_PRIORITY_HIGH: "high", + POE_PD69200_MSG_DATA_PORT_PRIORITY_LOW: "low", +} + +TBL_PRIORITY_TO_DRV = { + "crit": POE_PD69200_MSG_DATA_PORT_PRIORITY_CRIT, + "high": POE_PD69200_MSG_DATA_PORT_PRIORITY_HIGH, + "low": POE_PD69200_MSG_DATA_PORT_PRIORITY_LOW, +} + +TBL_CLASS_TO_CFG = { + POE_PD69200_MSG_DATA_CLASS_0: "0", + POE_PD69200_MSG_DATA_CLASS_1: "1", + POE_PD69200_MSG_DATA_CLASS_2: "2", + POE_PD69200_MSG_DATA_CLASS_3: "3", + POE_PD69200_MSG_DATA_CLASS_4: "4", + POE_PD69200_MSG_DATA_CLASS_5: "Err", +} + +TBL_BT_CLASS_TO_CFG = { + 0x0: "0", + 0x1: "1", + 0x2: "2", + 0x3: "3", + 0x4: "4", + 0x5: "5", + 0x6: "6", + 0x7: "7", + 0x8: "8", + 0xC: "None", +} + +TBL_PROTOCOL_TO_CFG = { + POE_PD69200_MSG_DATA_PROTOCOL_AF: "IEEE802.3AF", + POE_PD69200_MSG_DATA_PORT_TYPE_AT: "IEEE802.3AF/AT", + POE_PD69200_MSG_DATA_PORT_TYPE_AOH: "POH", +} + +# Port Operation Mode as Protocol +# 802.3BT, 802.3AF/AT or Non-Compliant +# TODO: Create key aliases +TBL_BT_PROTOCOL_TO_CFG = { + 0x00: "IEEE802.3BT", + 0x01: "IEEE802.3BT", + 0x02: "IEEE802.3BT", + 0x03: "IEEE802.3BT", + 0x09: "IEEE802.3AF/AT", + 0x10: "Non-Compliant", + 0x11: "Non-Compliant", + 0x12: "Non-Compliant", + 0x13: "Non-Compliant", + 0x14: "Non-Compliant", + 0x15: "Non-Compliant", + 0x20: "Non-Compliant", + 0x21: "Non-Compliant", + 0x22: "Non-Compliant", + 0x23: "Non-Compliant", + 0x24: "Non-Compliant", + 0x25: "Non-Compliant", + 0x26: "Non-Compliant", + 0x27: "Non-Compliant", + 0x30: "Non-Compliant", + 0x50: "Non-Compliant", + 0xFF: "Non-Compliant", +} + +TBL_STATUS_TO_CFG = { + 0x00: "Port On (0x00)", + 0x01: "Port On (0x01)", + 0x02: "Port On (0x02)", + 0x03: "Port On (0x03)", + 0x04: "Port On (0x04)", + 0x06: "Port Off (0x06)", + 0x07: "Port Off (0x07)", + 0x08: "Port Off (0x08)", + 0x0C: "Port Off (0x0C)", + 0x11: "Port Undef (0x11)", + 0x12: "Port Off (0x12)", + 0x1A: "Port Off (0x1A)", + 0x1B: "Port Off (0x1B)", + 0x1C: "Port Off (0x1C)", + 0x1D: "Port Off (0x1D)", + 0x1E: "Port Off (0x1E)", + 0x1F: "Port Off (0x1F)", + 0x20: "Port Off (0x20)", + 0x21: "Port Off (0x21)", + 0x24: "Port Off (0x24)", + 0x25: "Port Off (0x25)", + 0x26: "Port Off (0x26)", + 0x2B: "Force On (0x2B)", + 0x2C: "Undef Err (0x2C)", + 0x2D: "Volt Err (0x2D)", + 0x2E: "Volt Err (0x2E)", + 0x2F: "Dis PDU (0x2F)", + 0x31: "Port Off (0x31)", + 0x32: "Port Off (0x32)", + 0x33: "Comm Err (0x33)", + 0x34: "Port Off (0x34)", + 0x35: "Port Off (0x35)", + 0x36: "Port Off (0x36)", + 0x37: "Unknown (0x37)", + 0x38: "S/C (0x38)", + 0x39: "Over Temp (0x39)", + 0x3A: "Over Temp (0x3A)", + 0x3C: "Overload (0x3C)", + 0x3D: "Overload (0x3D)", + 0x3E: "Overload (0x3E)", + 0x3F: "Overload (0x3F)", + 0x43: "Port Off (0x43)", + 0x44: "Port Off (0x44)", + 0x45: "Port Off (0x45)", + 0x46: "Port Off (0x46)", + 0x47: "Power Err (0x47)", +} + +TBL_BT_STATUS_TO_CFG = { + 0x06: "Port Off (0x06)", + 0x07: "Port Off (0x07)", + 0x08: "Port Off (0x08)", + 0x0C: "Port Off (0x0C)", + 0x11: "Port Undef (0x11)", + 0x12: "Port Off (0x12)", + 0x1A: "Port Off (0x1A)", + 0x1B: "Port Off (0x1B)", + 0x1C: "Port Off (0x1C)", + 0x1E: "Port Off (0x1E)", + 0x1F: "Port Off (0x1F)", + 0x20: "Port Off (0x20)", + 0x21: "Port Off (0x21)", + 0x22: "Port Off (0x22)", + 0x24: "Port Off (0x24)", + 0x25: "Port Off (0x25)", + 0x26: "Port Off (0x26)", + 0x34: "Port Off (0x34)", + 0x35: "Port Off (0x35)", + 0x36: "Port Off (0x36)", + 0x37: "Unknown (0x37)", + 0x3C: "PWR MS (0x3C)", + 0x3D: "PWR MS-OVL (0x3D)", + 0x41: "PWR Error (0x41)", + 0x43: "Port Off (0x43)", + 0x44: "Port Off (0x44)", + 0x45: "Port Off (0x45)", + 0x46: "Port Off (0x46)", + 0x47: "PWR Error (0x47)", + 0x48: "Port Off (0x48)", + 0x49: "Port Off (0x49)", + 0x4A: "Port Off (0x4A)", + 0x4B: "Port Off (0x4B)", + 0x4C: "Port Off (0x4C)", + 0x80: "2Pair-D NC (0x80)", + 0x81: "2Pair-D (0x81)", + 0x82: "2Pair-D NC (0x82)", + 0x83: "2Pair-D NC (0x83)", + 0x84: "4Pair-D NC (0x84)", + 0x85: "2Pair-D (0x85)", + 0x86: "4Pair-D (0x86)", + 0x87: "2Pair-D (0x87)", + 0x88: "2Pair-D (0x88)", + 0x89: "4Pair-D (0x89)", + 0x90: "Force 2Pair (0x90)", + 0x91: "Force 4Pair (0x91)", + 0xA0: "Force PWR-E (0xA0)", + 0xA7: "CONN Error (0xA7)", + 0xA8: "Open (0xA8)", +} + +TBL_BT_ASSIGNED_CLASS_ALT_A_TO_CFG = { + 0x0C: None, # Unassigned + 0x01: "1", + 0x02: "2", + 0x03: "3", + 0x04: "4", + 0x05: "5", + 0x06: "6", + 0x07: "7", + 0x08: "8", +} + +TBL_BT_ASSIGNED_CLASS_ALT_B_TO_CFG = {0x0C: None, 0x01: "1", 0x02: "2", 0x03: "3", 0x04: "4", 0x05: "5"} # Unassigned + +TBL_BT_LAYER2_EXECUTION_TO_CFG = { + POE_PD69200_BT_MSG_DATA_LAYER2_IDLE: "Layer 2 idle", + POE_PD69200_BT_MSG_DATA_LAYER2_REQ_PENDING: "Request pending", + POE_PD69200_BT_MSG_DATA_LAYER2_REQ_EXECUTED: "Request executed", + POE_PD69200_BT_MSG_DATA_LAYER2_REQ_INSUFFICIENT_POWER: "Request rejected (lack of power)", + POE_PD69200_BT_MSG_DATA_LAYER2_REQ_ERROR: "Request rejected (error)", + POE_PD69200_BT_MSG_DATA_LAYER2_REQ_MISMATCH: "Request rejected (mismatch)", +} + +TBL_BT_LAYER2_USAGE_TO_CFG = { + POE_PD69200_BT_MSG_DATA_PORT_LAYER2_USAGE_OFF: "Not delivering power", + POE_PD69200_BT_MSG_DATA_PORT_LAYER2_USAGE_L1: "L1 mode", + POE_PD69200_MSG_DATA_PORT_LAYER2_USAGE_L1: "L1 mode", + POE_PD69200_BT_MSG_DATA_PORT_LAYER2_USAGE_LLDP: "L2 mode", +} + +TBL_BT_PSE_POWERING_STATUS_TO_CFG = { + 0x03: "4-pair dual-signature PD", + 0x02: "4-pair single-signature PD", + 0x01: "2-pair single-signature PD", +} + +TBL_BT_PSE_POWER_PAIRS_EXT = {0x03: "both", 0x02: "mode_b", 0x01: "mode_a"} + +TBL_BT_ENDIS_TO_CFG = { + 0x0: "disable", + 0x1: "enable", + 0x2: "enable (ignore inrush check)", + 0x3: "enable (force power)", + 0x4: "enable (force power)", +} + +TBL_PORT_EVENT_TO_CFG = { + 0x1: "Port is on", + 0x2: "Port turned off by user", + 0x4: "Check counters", + 0x8: "Port is open", + 0x16: "Port fault", +} diff --git a/dentos-poe-agent/opt/poeagent/drivers/pd69200/poe_driver_msg_parser.py b/dentos-poe-agent/opt/poeagent/drivers/pd69200/poe_driver_msg_parser.py new file mode 100644 index 0000000..de12a48 --- /dev/null +++ b/dentos-poe-agent/opt/poeagent/drivers/pd69200/poe_driver_msg_parser.py @@ -0,0 +1,426 @@ +''' +Copyright 2021 Delta Electronic Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +''' + +from enum import Enum +from typing import Any + +from poe_common import * +from poe_driver_def import * + + +class PoeMsgParser(object): + """Map the driver engineering values to an + application-facing format. + """ + + class MessageType(Enum): + MSG_PORT_POWER_LIMIT = 1 + MSG_PORT_PRIORITY = 2 + MSG_PORT_STATUS = 3 + MSG_POWER_SUPPLY_PARAMS = 4 + MSG_PORT_MEASUREMENTS = 5 + MSG_SYSTEM_STATUS = 6 + MSG_ALL_PORTS_ENDIS = 7 + MSG_POE_DEVICE_STATUS = 8 + MSG_INDV_MASK = 9 + MSG_PM_METHOD = 10 + MSG_SW_VERSION = 11 + MSG_BT_PORT_MEASUREMENTS = 12 + MSG_BT_PORT_PARAMETERS = 13 + MSG_BT_SYSTEM_STATUS = 14 + MSG_BT_PORT_CLASS = 15 + MSG_ACTIVE_MATRIX = 16 + MSG_BT_ALL_PORTS_POWER = 17 + MSG_BT_PORT_STATUS = 18 + MSG_BT_LLDP_PSE_DATA = 19 + MSG_BT_LLDP_PD_DATA = 20 + MSG_TOTAL_POWER = 21 + MSG_LLDP_PSE_DATA = 22 + MSG_SYSTEM_STATUS2 = 23 + MSG_POWER_BANK = 24 + MSG_CMD_STATUS = 255 + + def __to_word(self, byteH: int, byteL: int) -> int: + return (byteH << 8 | byteL) & 0xFFFF + + def __parse_port_power_limit(self, msg: list) -> dict[str, int]: + parsed_data = { + PPL: self.__to_word(msg[POE_PD69200_MSG_OFFSET_SUB], msg[POE_PD69200_MSG_OFFSET_SUB1]), + TPPL: self.__to_word(msg[POE_PD69200_MSG_OFFSET_SUB2], msg[POE_PD69200_MSG_OFFSET_DATA5]), + } + + return parsed_data + + def __parse_port_priority(self, msg: list) -> dict[str, int]: + parsed_data = {PRIORITY: msg[POE_PD69200_MSG_OFFSET_SUB]} + + return parsed_data + + def __parse_port_status(self, msg: list) -> dict[str, int]: + parsed_data = { + ENDIS: msg[POE_PD69200_MSG_OFFSET_SUB], + STATUS: msg[POE_PD69200_MSG_OFFSET_SUB1], + LATCH: msg[POE_PD69200_MSG_OFFSET_DATA5], + CLASS: msg[POE_PD69200_MSG_OFFSET_DATA6], + PROTOCOL: msg[POE_PD69200_MSG_OFFSET_DATA10], + EN_4PAIR: msg[POE_PD69200_MSG_OFFSET_DATA11], + } + + return parsed_data + + def __parse_bt_port_status_parameters(self, msg: list) -> dict[str, int]: + parsed_data = { + STATUS: msg[POE_PD69200_MSG_OFFSET_SUB], + ENDIS: msg[POE_PD69200_MSG_OFFSET_SUB1], + OPERATION_MODE: msg[POE_PD69200_MSG_OFFSET_DATA5], + PRIORITY: msg[POE_PD69200_MSG_OFFSET_DATA7], + } + + return parsed_data + + def __parse_bt_port_status(self, msg: list) -> dict[str, int]: + parsed_data = { + STATUS: msg[POE_PD69200_MSG_OFFSET_SUB], + ENDIS: (msg[POE_PD69200_MSG_OFFSET_SUB1] & 0x0F), + ASSIGNED_CLASS_ALT_A: (msg[POE_PD69200_MSG_OFFSET_SUB2] >> 4), + ASSIGNED_CLASS_ALT_B: (msg[POE_PD69200_MSG_OFFSET_SUB2] & 0x0F), + POWER_CONSUMP: self.__to_word( + msg[POE_PD69200_MSG_OFFSET_DATA5], msg[POE_PD69200_MSG_OFFSET_DATA6] + ), + SHUTDOWN_STATUS: msg[POE_PD69200_MSG_OFFSET_DATA9], + PORT_EVENT: msg[POE_PD69200_MSG_OFFSET_DATA11], + } + + return parsed_data + + def __parse_all_ports_endis(self, msg: list) -> dict[str, list[int]]: + parsed_data = {ENDIS: []} + all_ports_endis = [ + msg[POE_PD69200_MSG_OFFSET_SUB], # port_7_0 + msg[POE_PD69200_MSG_OFFSET_SUB1], # port_15_8 + msg[POE_PD69200_MSG_OFFSET_SUB2], # port_23_16 + msg[POE_PD69200_MSG_OFFSET_DATA6], # port_31_24 + msg[POE_PD69200_MSG_OFFSET_DATA7], # port_39_32 + msg[POE_PD69200_MSG_OFFSET_DATA8], + ] # port_47_40 + + for endis_group in all_ports_endis: + for idx in range(8): + port_endis = (endis_group >> idx) & 1 + parsed_data[ENDIS].append(port_endis) + + return parsed_data + + def __parse_bt_all_ports_power(self, msg: list) -> dict[str, list[int]]: + """Get All Ports Delivering Power State message + + Not the same as enable/disable state on an AF/AT system. + """ + parsed_data = {ENDIS: []} + all_ports_endis = [ + msg[POE_PD69200_MSG_OFFSET_SUB], # port_7_0 + msg[POE_PD69200_MSG_OFFSET_SUB1], # port_15_8 + msg[POE_PD69200_MSG_OFFSET_SUB2], # port_23_16 + msg[POE_PD69200_MSG_OFFSET_DATA5], # port_31_24 + msg[POE_PD69200_MSG_OFFSET_DATA6], # port_39_32 + msg[POE_PD69200_MSG_OFFSET_DATA7], + ] # port_47_40 + + for endis_group in all_ports_endis: + for idx in range(8): + port_endis = (endis_group >> idx) & 1 + parsed_data[ENDIS].append(port_endis) + + return parsed_data + + def __parse_power_supply_params(self, msg: list) -> dict[str, int]: + parsed_data = { + POWER_CONSUMP: self.__to_word( + msg[POE_PD69200_MSG_OFFSET_SUB], msg[POE_PD69200_MSG_OFFSET_SUB1] + ), + MAX_SD_VOLT: self.__to_word( + msg[POE_PD69200_MSG_OFFSET_SUB2], msg[POE_PD69200_MSG_OFFSET_DATA5] + ), + MIN_SD_VOLT: self.__to_word( + msg[POE_PD69200_MSG_OFFSET_DATA6], msg[POE_PD69200_MSG_OFFSET_DATA7] + ), + POWER_BANK: msg[POE_PD69200_MSG_OFFSET_DATA9], + TOTAL_POWER: self.__to_word( + msg[POE_PD69200_MSG_OFFSET_DATA10], msg[POE_PD69200_MSG_OFFSET_DATA11] + ), + } + + return parsed_data + + def __parse_total_power_params(self, msg: list) -> dict[str, int]: + parsed_data = { + POWER_CONSUMP: self.__to_word( + msg[POE_PD69200_MSG_OFFSET_SUB], msg[POE_PD69200_MSG_OFFSET_SUB1] + ), + CALCULATED_POWER: self.__to_word( + msg[POE_PD69200_MSG_OFFSET_SUB2], msg[POE_PD69200_MSG_OFFSET_DATA5] + ), + POWER_AVAIL: self.__to_word( + msg[POE_PD69200_MSG_OFFSET_DATA6], msg[POE_PD69200_MSG_OFFSET_DATA7] + ), + POWER_LIMIT: self.__to_word( + msg[POE_PD69200_MSG_OFFSET_DATA8], msg[POE_PD69200_MSG_OFFSET_DATA9] + ), + POWER_BANK: msg[POE_PD69200_MSG_OFFSET_DATA10], + } + + return parsed_data + + def __parse_port_measurements(self, msg: list) -> dict[str, int]: + parsed_data = { + CURRENT: self.__to_word(msg[POE_PD69200_MSG_OFFSET_SUB2], msg[POE_PD69200_MSG_OFFSET_DATA5]), + POWER_CONSUMP: self.__to_word( + msg[POE_PD69200_MSG_OFFSET_DATA6], msg[POE_PD69200_MSG_OFFSET_DATA7] + ), + VOLTAGE: self.__to_word(msg[POE_PD69200_MSG_OFFSET_DATA9], msg[POE_PD69200_MSG_OFFSET_DATA10]), + } + + return parsed_data + + def __parse_bt_port_measurements(self, msg: list) -> dict[str, int]: + parsed_data = { + CURRENT: self.__to_word(msg[POE_PD69200_MSG_OFFSET_SUB2], msg[POE_PD69200_MSG_OFFSET_DATA5]), + POWER_CONSUMP: self.__to_word( + msg[POE_PD69200_MSG_OFFSET_DATA6], msg[POE_PD69200_MSG_OFFSET_DATA7] + ), + VOLTAGE: self.__to_word(msg[POE_PD69200_MSG_OFFSET_DATA9], msg[POE_PD69200_MSG_OFFSET_DATA10]), + } + + return parsed_data + + def __parse_system_status(self, msg: list) -> dict[str, int]: + parsed_data = { + CPU_STATUS1: msg[POE_PD69200_MSG_OFFSET_SUB], + CPU_STATUS2: msg[POE_PD69200_MSG_OFFSET_SUB1], + FAC_DEFAULT: msg[POE_PD69200_MSG_OFFSET_SUB2], + GIE: msg[POE_PD69200_MSG_OFFSET_DATA5], + PRIV_LABEL: msg[POE_PD69200_MSG_OFFSET_DATA6], + USER_BYTE: msg[POE_PD69200_MSG_OFFSET_DATA7], + DEVICE_FAIL: msg[POE_PD69200_MSG_OFFSET_DATA8], + TEMP_DISCO: msg[POE_PD69200_MSG_OFFSET_DATA9], + TEMP_ALARM: msg[POE_PD69200_MSG_OFFSET_DATA10], + INTR_REG: self.__to_word( + msg[POE_PD69200_MSG_OFFSET_DATA11], msg[POE_PD69200_MSG_OFFSET_DATA12] + ), + } + + return parsed_data + + def __parse_bt_system_status(self, msg: list) -> dict[str, int]: + parsed_data = { + CPU_STATUS2: msg[POE_PD69200_MSG_OFFSET_SUB1], + FAC_DEFAULT: msg[POE_PD69200_MSG_OFFSET_SUB2], + PRIV_LABEL: msg[POE_PD69200_MSG_OFFSET_DATA6], + NVM_USER_BYTE: msg[POE_PD69200_MSG_OFFSET_DATA7], + FOUND_DEVICE: msg[POE_PD69200_MSG_OFFSET_DATA8], + EVENT_EXIST: msg[POE_PD69200_MSG_OFFSET_DATA12], + } + + return parsed_data + + def __parse_poe_device_params(self, msg: list) -> dict[str, int]: + parsed_data = { + CSNUM: msg[POE_PD69200_MSG_OFFSET_SUB], + STATUS: msg[POE_PD69200_MSG_OFFSET_DATA5], + TEMP: msg[POE_PD69200_MSG_OFFSET_DATA9], + TEMP_ALARM: msg[POE_PD69200_MSG_OFFSET_DATA10], + } + return parsed_data + + def __parse_indv_mask(self, msg: list) -> dict[str, int]: + parsed_data = {ENDIS: msg[POE_PD69200_MSG_OFFSET_SUB]} + + return parsed_data + + def __parse_pm_method(self, msg: list) -> dict[str, int]: + parsed_data = { + PM1: msg[POE_PD69200_MSG_OFFSET_SUB], + PM2: msg[POE_PD69200_MSG_OFFSET_SUB1], + PM3: msg[POE_PD69200_MSG_OFFSET_SUB2], + } + + return parsed_data + + def __parse_sw_version(self, msg: list) -> dict[str, int]: + parsed_data = { + PROD_NUM: msg[POE_PD69200_MSG_OFFSET_SUB2], + SW_VERSION: self.__to_word( + msg[POE_PD69200_MSG_OFFSET_DATA5], msg[POE_PD69200_MSG_OFFSET_DATA6] + ), + } + return parsed_data + + def __parse_bt_port_class(self, msg: list) -> dict[str, int]: + parsed_data = { + MEASURED_CLASS_ALT_A: (msg[POE_PD69200_MSG_OFFSET_SUB2] >> 4), + MEASURED_CLASS_ALT_B: (msg[POE_PD69200_MSG_OFFSET_SUB2] & 0x0F), + REQUESTED_CLASS_ALT_A: (msg[POE_PD69200_MSG_OFFSET_DATA5] >> 4), + REQUESTED_CLASS_ALT_B: (msg[POE_PD69200_MSG_OFFSET_DATA5] & 0x0F), + ASSIGNED_CLASS_ALT_A: (msg[POE_PD69200_MSG_OFFSET_DATA8] >> 4), + ASSIGNED_CLASS_ALT_B: (msg[POE_PD69200_MSG_OFFSET_DATA8] & 0x0F), + TPPL: self.__to_word(msg[POE_PD69200_MSG_OFFSET_DATA9], msg[POE_PD69200_MSG_OFFSET_DATA10]), + } + + return parsed_data + + def __parse_bt_port_lldp_pse_data(self, msg: list) -> dict[str, int]: + parsed_data = { + PSE_ALLOCATED_POWER_SINGLE_ALT_A: self.__to_word( + msg[POE_PD69200_MSG_OFFSET_SUB], msg[POE_PD69200_MSG_OFFSET_SUB1] + ), + PSE_ALLOCATED_POWER_ALT_B: self.__to_word( + msg[POE_PD69200_MSG_OFFSET_SUB2], msg[POE_PD69200_MSG_OFFSET_DATA5] + ), + PSE_MAX_POWER: self.__to_word( + msg[POE_PD69200_MSG_OFFSET_DATA6], msg[POE_PD69200_MSG_OFFSET_DATA7] + ), + ASSIGNED_CLASS_ALT_A: (msg[POE_PD69200_MSG_OFFSET_DATA8] >> 4), + ASSIGNED_CLASS_ALT_B: (msg[POE_PD69200_MSG_OFFSET_DATA8] & 0x0F), + LAYER2_EXECUTION: (msg[POE_PD69200_MSG_OFFSET_DATA9] >> 4), + LAYER2_USAGE: (msg[POE_PD69200_MSG_OFFSET_DATA9] & 0x0F), + PSE_POWERING_STATUS: ((msg[POE_PD69200_MSG_OFFSET_DATA10] >> 2) & 0x03), + PSE_POWER_PAIRS_EXT: (msg[POE_PD69200_MSG_OFFSET_DATA10] & 0x03), + CABLE_LENGTH: (msg[POE_PD69200_MSG_OFFSET_DATA11] & 0x0F), + PRIORITY: (msg[POE_PD69200_MSG_OFFSET_DATA12] & 0x0F), + } + + return parsed_data + + def __parse_bt_port_lldp_pd_data(self, msg: list) -> dict[str, int]: + parsed_data = { + PD_REQUESTED_POWER_SINGLE: self.__to_word( + msg[POE_PD69200_MSG_OFFSET_SUB], msg[POE_PD69200_MSG_OFFSET_SUB1] + ), + PD_REQUESTED_POWER_MODE_A: self.__to_word( + msg[POE_PD69200_MSG_OFFSET_SUB2], msg[POE_PD69200_MSG_OFFSET_DATA5] + ), + PD_REQUESTED_POWER_MODE_B: self.__to_word( + msg[POE_PD69200_MSG_OFFSET_DATA6], msg[POE_PD69200_MSG_OFFSET_DATA7] + ), + REQUESTED_CABLE_LENGTH: msg[POE_PD69200_MSG_OFFSET_DATA8], + } + + return parsed_data + + def __parse_port_lldp_pse_data(self, msg: list) -> dict[str, int]: + power_consumption = self.__to_word(msg[POE_PD69200_MSG_OFFSET_DATA11], msg[POE_PD69200_MSG_OFFSET_DATA12]) + parsed_data = { + PSE_ALLOCATED_POWER: self.__to_word( + msg[POE_PD69200_MSG_OFFSET_SUB], msg[POE_PD69200_MSG_OFFSET_SUB1] + ), + PD_REQUESTED_POWER: self.__to_word( + msg[POE_PD69200_MSG_OFFSET_SUB2], + msg[POE_PD69200_MSG_OFFSET_DATA5], + ), + LAYER2_USAGE: (power_consumption >> 11) & 0x3, + LAYER2_EXECUTION: (power_consumption >> 13) & 0x1, + CABLE_LENGTH: msg[POE_PD69200_MSG_OFFSET_DATA10], + PRIORITY: msg[POE_PD69200_MSG_OFFSET_DATA6] & 0x3, + } + + return parsed_data + + def __parse_cmd_status(self, msg: list) -> int: + parsed_data = int.from_bytes( + bytes([msg[POE_PD69200_MSG_OFFSET_SUB], msg[POE_PD69200_MSG_OFFSET_SUB1]]), byteorder="big" + ) + + return parsed_data + + def __parse_system_status2(self, msg: list) -> dict[str, int]: + parsed_data = { + GIE: msg[POE_PD69200_MSG_OFFSET_SUB1], + } + + return parsed_data + + def __parse_active_matrix(self, msg: list) -> dict[str, int]: + parsed_data = { + ACTIVE_MATRIX_PHYA: msg[POE_PD69200_MSG_OFFSET_SUB], + ACTIVE_MATRIX_PHYB: msg[POE_PD69200_MSG_OFFSET_SUB1], + } + + return parsed_data + + def __parse_power_bank(self, msg: list) -> dict[str, int]: + parsed_data = { + POWER_LIMIT: self.__to_word(msg[POE_PD69200_MSG_OFFSET_SUB], msg[POE_PD69200_MSG_OFFSET_SUB1]), + MAX_SD_VOLT: self.__to_word( + msg[POE_PD69200_MSG_OFFSET_SUB2], msg[POE_PD69200_MSG_OFFSET_DATA5] + ), + MIN_SD_VOLT: self.__to_word( + msg[POE_PD69200_MSG_OFFSET_DATA6], msg[POE_PD69200_MSG_OFFSET_DATA7] + ), + } + + return parsed_data + + def parse(self, msg: list, msg_type: MessageType) -> Any: + if msg_type == self.MessageType.MSG_PORT_POWER_LIMIT: + return self.__parse_port_power_limit(msg) + elif msg_type == self.MessageType.MSG_PORT_PRIORITY: + return self.__parse_port_priority(msg) + elif msg_type == self.MessageType.MSG_PORT_STATUS: + return self.__parse_port_status(msg) + elif msg_type == self.MessageType.MSG_POWER_SUPPLY_PARAMS: + return self.__parse_power_supply_params(msg) + elif msg_type == self.MessageType.MSG_POWER_BANK: + return self.__parse_power_bank(msg) + elif msg_type == self.MessageType.MSG_TOTAL_POWER: + return self.__parse_total_power_params(msg) + elif msg_type == self.MessageType.MSG_PORT_MEASUREMENTS: + return self.__parse_port_measurements(msg) + elif msg_type == self.MessageType.MSG_SYSTEM_STATUS: + return self.__parse_system_status(msg) + elif msg_type == self.MessageType.MSG_ALL_PORTS_ENDIS: + return self.__parse_all_ports_endis(msg) + elif msg_type == self.MessageType.MSG_BT_ALL_PORTS_POWER: + return self.__parse_bt_all_ports_power(msg) + elif msg_type == self.MessageType.MSG_POE_DEVICE_STATUS: + return self.__parse_poe_device_params(msg) + elif msg_type == self.MessageType.MSG_INDV_MASK: + return self.__parse_indv_mask(msg) + elif msg_type == self.MessageType.MSG_PM_METHOD: + return self.__parse_pm_method(msg) + elif msg_type == self.MessageType.MSG_SW_VERSION: + return self.__parse_sw_version(msg) + elif msg_type == self.MessageType.MSG_BT_PORT_PARAMETERS: + return self.__parse_bt_port_status_parameters(msg) + elif msg_type == self.MessageType.MSG_BT_PORT_CLASS: + return self.__parse_bt_port_class(msg) + elif msg_type == self.MessageType.MSG_BT_PORT_STATUS: + return self.__parse_bt_port_status(msg) + elif msg_type == self.MessageType.MSG_BT_SYSTEM_STATUS: + return self.__parse_bt_system_status(msg) + elif msg_type == self.MessageType.MSG_BT_PORT_MEASUREMENTS: + return self.__parse_bt_port_measurements(msg) + elif msg_type == self.MessageType.MSG_LLDP_PSE_DATA: + return self.__parse_port_lldp_pse_data(msg) + elif msg_type == self.MessageType.MSG_BT_LLDP_PSE_DATA: + return self.__parse_bt_port_lldp_pse_data(msg) + elif msg_type == self.MessageType.MSG_BT_LLDP_PD_DATA: + return self.__parse_bt_port_lldp_pd_data(msg) + elif msg_type == self.MessageType.MSG_ACTIVE_MATRIX: + return self.__parse_active_matrix(msg) + elif msg_type == self.MessageType.MSG_CMD_STATUS: + return self.__parse_cmd_status(msg) + elif msg_type == self.MessageType.MSG_SYSTEM_STATUS2: + return self.__parse_system_status2(msg) diff --git a/dentos-poe-agent/opt/poeagent/drivers/pd69200/poe_driver_pd69200_def.py b/dentos-poe-agent/opt/poeagent/drivers/pd69200/poe_driver_pd69200_def.py new file mode 100755 index 0000000..d572946 --- /dev/null +++ b/dentos-poe-agent/opt/poeagent/drivers/pd69200/poe_driver_pd69200_def.py @@ -0,0 +1,306 @@ +''' +Copyright 2021 Delta Electronic Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +''' + +# PD69200 Global Definitions +POE_PD69200_MSG_LEN = 15 +POE_PD69200_MSG_CSUM_LEN = 2 +POE_PD69200_MSG_N = 0x4E +POE_PD69200_COMM_RETRY_TIMES = 6 + +# PD69200 Message Structure +POE_PD69200_MSG_OFFSET_KEY = 0 +POE_PD69200_MSG_OFFSET_ECHO = 1 +POE_PD69200_MSG_OFFSET_SUB = 2 +POE_PD69200_MSG_OFFSET_SUB1 = 3 +POE_PD69200_MSG_OFFSET_SUB2 = 4 +POE_PD69200_MSG_OFFSET_DATA5 = 5 +POE_PD69200_MSG_OFFSET_DATA6 = 6 +POE_PD69200_MSG_OFFSET_DATA7 = 7 +POE_PD69200_MSG_OFFSET_DATA8 = 8 +POE_PD69200_MSG_OFFSET_DATA9 = 9 +POE_PD69200_MSG_OFFSET_DATA10 = 10 +POE_PD69200_MSG_OFFSET_DATA11 = 11 +POE_PD69200_MSG_OFFSET_DATA12 = 12 +POE_PD69200_MSG_OFFSET_CSUM_H = 13 +POE_PD69200_MSG_OFFSET_CSUM_L = 14 + +# PD69200 Message - Byte 1: KEY +POE_PD69200_MSG_KEY_COMMAND = 0x00 +POE_PD69200_MSG_KEY_PROGRAM = 0x01 +POE_PD69200_MSG_KEY_REQUEST = 0x02 +POE_PD69200_MSG_KEY_TELEMETRY = 0x03 +POE_PD69200_MSG_KEY_TEST = 0x04 +POE_PD69200_MSG_KEY_REPORT = 0x52 + +# PD69200 Message - Byte 3: SUB +POE_PD69200_MSG_SUB_CHANNEL = 0x05 +POE_PD69200_MSG_SUB_E2 = 0x06 +POE_PD69200_MSG_SUB_GLOBAL = 0x07 +POE_PD69200_MSG_SUB_RESOTRE_FACT = 0x2D +POE_PD69200_MSG_SUB_USER_BYTE = 0x41 +POE_PD69200_MSG_SUB_FLASH = 0xFF + +# PD69200 Message - Byte 4: SUB1 +POE_PD69200_MSG_SUB1_PRIORITY = 0x0A +POE_PD69200_MSG_SUB1_SUPPLY = 0x0B +POE_PD69200_MSG_SUB1_EN_DIS = 0x0C +POE_PD69200_MSG_SUB1_PORT_STATUS = 0x0E +POE_PD69200_MSG_SUB1_SAVE_CONFIG = 0x0F +POE_PD69200_MSG_SUB1_VERSIONZ = 0x1E +POE_PD69200_MSG_SUB1_PARAMZ = 0x25 +POE_PD69200_MSG_SUB1_PORTS_STATUS1 = 0x31 +POE_PD69200_MSG_SUB1_PORTS_STATUS2 = 0x32 +POE_PD69200_MSG_SUB1_PORTS_STATUS3 = 0x33 +POE_PD69200_MSG_SUB1_SYSTEM_STATUS = 0x3D +POE_PD69200_MSG_SUB1_TEMP_MATRIX = 0x43 +POE_PD69200_MSG_SUB1_CH_MATRIX = 0x44 +POE_PD69200_MSG_SUB1_PORTS_STATUS4 = 0x47 +POE_PD69200_MSG_SUB1_PORTS_STATUS5 = 0x48 +POE_PD69200_MSG_SUB1_PORTS_POW1 = 0x4B +POE_PD69200_MSG_SUB1_PORTS_POW2 = 0x4C +POE_PD69200_MSG_SUB1_PORTS_POW3 = 0x4D +POE_PD69200_MSG_SUB1_PORTS_POW4 = 0x4F +POE_PD69200_MSG_SUB1_PORTS_POW5 = 0x50 +POE_PD69200_MSG_SUB1_RESET = 0x55 +POE_PD69200_MSG_SUB1_INDV_MSK = 0x56 +POE_PD69200_MSG_SUB1_DEV_PARAMS = 0x87 +POE_PD69200_MSG_SUB1_PORTS_DLV_PWR = 0xC0 +# PD69200 BT Message - Byte 4: SUB1 +POE_PD69200_BT_MSG_SUB1_SYSTEM_STATUS = 0xD0 +POE_PD69200_BT_MSG_SUB1_PORTS_PARAMETERS = 0xC0 +POE_PD69200_BT_MSG_SUB1_PORTS_CLASS = 0xC4 +POE_PD69200_BT_MSG_SUB1_PORTS_MEASUREMENT = 0xC5 + +# PD69200 Message - Byte 5: SUB2 +POE_PD69200_MSG_SUB2_MAIN = 0x17 +POE_PD69200_MSG_SUB2_SW_VERSION = 0x21 +POE_PD69200_MSG_SUB2_PWR_BUDGET = 0x57 +POE_PD69200_MSG_SUB2_PWR_MANAGE_MODE = 0x5F +POE_PD69200_MSG_SUB2_TOTAL_PWR = 0x60 + +# PD69200 Message - Byte 6 to Byte 13: DATA +POE_PD69200_MSG_DATA_CLASS_0 = 0 +POE_PD69200_MSG_DATA_CLASS_1 = 1 +POE_PD69200_MSG_DATA_CLASS_2 = 2 +POE_PD69200_MSG_DATA_CLASS_3 = 3 +POE_PD69200_MSG_DATA_CLASS_4 = 4 +POE_PD69200_MSG_DATA_CLASS_5 = 5 + +POE_PD69200_MSG_DATA_PROTOCOL_AF = 0 +POE_PD69200_MSG_DATA_PROTOCOL_ATAF = 1 +POE_PD69200_MSG_DATA_PROTOCOL_AOH = 2 + +POE_PD69200_MSG_DATA_PORT_TYPE_AF = 0 +POE_PD69200_MSG_DATA_PORT_TYPE_AT = 1 +POE_PD69200_MSG_DATA_PORT_TYPE_AOH = 2 + +POE_PD69200_MSG_DATA_CMD_ENDIS_ONLY = 0 +POE_PD69200_MSG_DATA_CMD_DISABLE = 0 +POE_PD69200_MSG_DATA_CMD_ENABLE = 1 +POE_PD69200_BT_MSG_DATA_CMD_ENDIS_NO_CHAGNE = 0xF + +POE_PD69200_MSG_DATA_PORT_PRIORITY_CRIT = 1 +POE_PD69200_MSG_DATA_PORT_PRIORITY_HIGH = 2 +POE_PD69200_MSG_DATA_PORT_PRIORITY_LOW = 3 + +POE_PD69200_MSG_DATA_PM1_DYNAMIC = 0 +POE_PD69200_MSG_DATA_PM2_PPL = 0 +POE_PD69200_MSG_DATA_PM3_NO_COND = 0 + +# PD69200 BT Message - Byte 6: DATA +#Port Mode CFG2 +# BITS[3:0] BT Port PM Mode +POE_PD69200_BT_MSG_DATA_PORT_MODE_DYNAMIC = 0x0 +POE_PD69200_BT_MSG_DATA_PORT_MODE_TPPL = 0x01 +POE_PD69200_BT_MSG_DATA_PORT_MODE_DYNAMIC_NON_LLDP_CDP_AUTO_AND_TPPL_BT_LLDP_CDP_AUTO = 0x02 +POE_PD69200_BT_MSG_DATA_PORT_MODE_NO_CHANGE = 0x0F +# BIT[7:4] Class Error Operation Select +POE_PD69200_BT_MSG_DATA_PORT_CLASS_ERROR_DISABLE = 0x0 +POE_PD69200_BT_MSG_DATA_PORT_CLASS_SSPD_3_DSPD_3 = 0x10 +POE_PD69200_BT_MSG_DATA_PORT_CLASS_SSPD_4_DSPD_3 = 0x20 +POE_PD69200_BT_MSG_DATA_PORT_CLASS_SSPD_6_DSPD_4 = 0x30 +POE_PD69200_BT_MSG_DATA_PORT_CLASS_SSPD_8_DSPD_5 = 0x40 +POE_PD69200_BT_MSG_DATA_PORT_CLASS_ERROR_NO_CHANGE = 0xF0 + +#Byte 7: DATA +#Port Operation Mode +POE_PD69200_BT_MSG_DATA_PORT_OP_MODE_NO_CHANGE = 0xFF + +#Byte 8: DATA +#Add Power for Port Mode +POE_PD69200_BT_MSG_DATA_PORT_MODE_POWER_SAME = 0x0 + +#Byte 9: DATA +#Port Priority Mode +POE_PD69200_BT_MSG_DATA_PORT_PRIORITY_NO_CHANGE = 0xFF + +TBL_ENDIS_TO_CFG = {POE_PD69200_MSG_DATA_CMD_ENABLE : "enable", + POE_PD69200_MSG_DATA_CMD_DISABLE: "disable"} + +TBL_ENDIS_TO_DRV = {"enable" : POE_PD69200_MSG_DATA_CMD_ENABLE, + "disable": POE_PD69200_MSG_DATA_CMD_DISABLE} + +TBL_PRIORITY_TO_CFG = {POE_PD69200_MSG_DATA_PORT_PRIORITY_CRIT: "crit", + POE_PD69200_MSG_DATA_PORT_PRIORITY_HIGH: "high", + POE_PD69200_MSG_DATA_PORT_PRIORITY_LOW : "low"} + +TBL_PRIORITY_TO_DRV = {"crit": POE_PD69200_MSG_DATA_PORT_PRIORITY_CRIT, + "high": POE_PD69200_MSG_DATA_PORT_PRIORITY_HIGH, + "low" : POE_PD69200_MSG_DATA_PORT_PRIORITY_LOW} + +TBL_CLASS_TO_CFG = {POE_PD69200_MSG_DATA_CLASS_0: "0", + POE_PD69200_MSG_DATA_CLASS_1: "1", + POE_PD69200_MSG_DATA_CLASS_2: "2", + POE_PD69200_MSG_DATA_CLASS_3: "3", + POE_PD69200_MSG_DATA_CLASS_4: "4", + POE_PD69200_MSG_DATA_CLASS_5: "Err"} + +TBL_BT_CLASS_TO_CFG = {0x0: "0", + 0x1: "1", + 0x2: "2", + 0x3: "3", + 0x4: "4", + 0x5: "5", + 0x6: "6", + 0x7: "7", + 0x8: "8", + 0xc: "Non"} + +TBL_PROTOCOL_TO_CFG = {POE_PD69200_MSG_DATA_PROTOCOL_AF : "IEEE802.3AF", + POE_PD69200_MSG_DATA_PORT_TYPE_AT : "IEEE802.3AF/AT", + POE_PD69200_MSG_DATA_PORT_TYPE_AOH: "POH"} + +# Port Operation Mode as Protocol +# 802.3BT, 802.3AF/AT or Non-Compliant +TBL_BT_PROTOCOL_TO_CFG = {0x00 : "IEEE802.3BT", + 0x01 : "IEEE802.3BT", + 0x02 : "IEEE802.3BT", + 0x03 : "IEEE802.3BT", + 0x09 : "IEEE802.3AF/AT", + 0x10 : "Non-Compliant", + 0x11 : "Non-Compliant", + 0x12 : "Non-Compliant", + 0x13 : "Non-Compliant", + 0x14 : "Non-Compliant", + 0x15 : "Non-Compliant", + 0x20 : "Non-Compliant", + 0x21 : "Non-Compliant", + 0x22 : "Non-Compliant", + 0x23 : "Non-Compliant", + 0x24 : "Non-Compliant", + 0x25 : "Non-Compliant", + 0x26 : "Non-Compliant", + 0x27 : "Non-Compliant", + 0x30 : "Non-Compliant", + 0x50 : "Non-Compliant", + 0xFF : "Non-Compliant"} + +TBL_STATUS_TO_CFG = {0x00: "Port On (0x00)", + 0x01: "Port On (0x01)", + 0x02: "Port On (0x02)", + 0x03: "Port On (0x03)", + 0x04: "Port On (0x04)", + 0x06: "Port Off (0x06)", + 0x07: "Port Off (0x07)", + 0x08: "Port Off (0x08)", + 0x0C: "Port Off (0x0C)", + 0x11: "Port Undef (0x11)", + 0x12: "Port Off (0x12)", + 0x1A: "Port Off (0x1A)", + 0x1B: "Port Off (0x1B)", + 0x1C: "Port Off (0x1C)", + 0x1D: "Port Off (0x1D)", + 0x1E: "Port Off (0x1E)", + 0x1F: "Port Off (0x1F)", + 0x20: "Port Off (0x20)", + 0x21: "Port Off (0x21)", + 0x24: "Port Off (0x24)", + 0x25: "Port Off (0x25)", + 0x26: "Port Off (0x26)", + 0x2B: "Force On (0x2B)", + 0x2C: "Undef Err (0x2C)", + 0x2D: "Volt Err (0x2D)", + 0x2E: "Volt Err (0x2E)", + 0x2F: "Dis PDU (0x2F)", + 0x31: "Port Off (0x31)", + 0x32: "Port Off (0x32)", + 0x33: "Comm Err (0x33)", + 0x34: "Port Off (0x34)", + 0x35: "Port Off (0x35)", + 0x36: "Port Off (0x36)", + 0x37: "Unknown (0x37)", + 0x38: "S/C (0x38)", + 0x39: "Over Temp (0x39)", + 0x3A: "Over Temp (0x3A)", + 0x3C: "Overload (0x3C)", + 0x3D: "Overload (0x3D)", + 0x3E: "Overload (0x3E)", + 0x3F: "Overload (0x3F)", + 0x43: "Port Off (0x43)", + 0x44: "Port Off (0x44)", + 0x45: "Port Off (0x45)", + 0x46: "Port Off (0x46)", + 0x47: "Power Err (0x47)"} + +TBL_BT_STATUS_TO_CFG = {0x06: "Port Off (0x06)", + 0x07: "Port Off (0x07)", + 0x08: "Port Off (0x08)", + 0x0C: "Port Off (0x0C)", + 0x11: "Port Undef (0x11)", + 0x12: "Port Off (0x12)", + 0x1A: "Port Off (0x1A)", + 0x1B: "Port Off (0x1B)", + 0x1C: "Port Off (0x1C)", + 0x1E: "Port Off (0x1E)", + 0x1F: "Port Off (0x1F)", + 0x20: "Port Off (0x20)", + 0x21: "Port Off (0x21)", + 0x22: "Port Off (0x22)", + 0x24: "Port Off (0x24)", + 0x25: "Port Off (0x25)", + 0x26: "Port Off (0x26)", + 0x34: "Port Off (0x34)", + 0x35: "Port Off (0x35)", + 0x36: "Port Off (0x36)", + 0x37: "Unknown (0x37)", + 0x3C: "PWR MS (0x3C)", + 0x3D: "PWR MS-OVL (0x3D)", + 0x41: "PWR Error (0x41)", + 0x43: "Port Off (0x43)", + 0x44: "Port Off (0x44)", + 0x45: "Port Off (0x45)", + 0x46: "Port Off (0x46)", + 0x47: "PWR Error (0x47)", + 0x48: "Port Off (0x48)", + 0x49: "Port Off (0x49)", + 0x4A: "Port Off (0x4A)", + 0x4B: "Port Off (0x4B)", + 0x4C: "Port Off (0x4C)", + 0x80: "2Pair-D NC (0x80)", + 0x81: "2Pair-D (0x81)", + 0x82: "2Pair-D NC (0x82)", + 0x83: "2Pair-D NC (0x83)", + 0x84: "4Pair-D NC (0x84)", + 0x85: "2Pair-D (0x85)", + 0x86: "4Pair-D (0x86)", + 0x87: "2Pair-D (0x87)", + 0x88: "2Pair-D (0x88)", + 0x89: "4Pair-D (0x89)", + 0x90: "Force 2Pair (0x90)", + 0x91: "Force 4Pair (0x91)", + 0xA0: "Force PWR-E (0xA0)", + 0xA7: "CONN Error (0xA7)", + 0xA8: "Open (0xA8)"} diff --git a/dentos-poe-agent/opt/poeagent/drivers/poe_driver_pd69200.py b/dentos-poe-agent/opt/poeagent/drivers/poe_driver_pd69200.py index 976a4cb..2c21c8e 100755 --- a/dentos-poe-agent/opt/poeagent/drivers/poe_driver_pd69200.py +++ b/dentos-poe-agent/opt/poeagent/drivers/poe_driver_pd69200.py @@ -20,7 +20,6 @@ import json from collections import OrderedDict from poe_common import * -from poe_common import print_stderr from poe_driver_pd69200_def import * class PoeCommExclusiveLock(object): diff --git a/dentos-poe-agent/opt/poeagent/inc/agent_constants.py b/dentos-poe-agent/opt/poeagent/inc/agent_constants.py new file mode 100644 index 0000000..e59f061 --- /dev/null +++ b/dentos-poe-agent/opt/poeagent/inc/agent_constants.py @@ -0,0 +1,90 @@ +''' +Copyright Amazon Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +''' + + +from pathlib import Path + +class AgentConstants: + """Agent config metadata string mapping""" + + BOOTCMD_PATH: str = "/proc/cmdline" + ONL_PLATFORM_PATH: str = "/etc/onl/platform" + PLAT_VENDOR_PATH: str = Path.cwd().parent.joinpath("platforms").as_posix() + GEN_INFO: str = "GENERAL_INFORMATION" + TIMESTAMP: str = "TIMESTAMP" + SYS_INFO: str = "SYSTEM_INFORMATION" + PORT_CONFIGS: str = "PORTS_CONFIG" + DEFAULT_LIMITS: str = "DEFAULT_POWER_LIMITS" + LLDP_ENDIS: str = "lldpEnDis" + ENABLE: str = "enable" + DISABLE: str = "disable" + PORT_INFO: str = "PORTS_INFORMATION" + REG_MASKS: str = "REG_MASKS" + VERSIONS: str = "VERSIONS" + PLATFORM: str = "platform" + POE_AGT_VER: str = "poe_agent_version" + POE_CFG_VER: str = "poe_config_version" + CFG_SERIAL_NUM: str = "file_serial_number" + LAST_SAVE_TIME: str = "file_save_time" + LAST_SET_TIME: str = "last_updated" + CMD_RESULT_RET: str = "ret" + POE_CPLD_RESET_RQ_PATH: str = "/run/.poed_cpld_reset" + + # Track global chipset state, like non-overlapping echo bytes. + POE_COMM_STATE_PATH: str = "/run/poe_comm_state.json" + + # poed persistence related. + POED_PERM_CFG_PATH: str = "/etc/poe_agent/poe_perm_cfg.json" + POED_RUNTIME_CFG_PATH: str = "/run/poe_runtime_cfg.json" + POED_PID_PATH: str = "/run/poed.pid" + POE_ACCESS_LOCK_PATH: str = "/run/poe_access.lock" + EXLOCK_RETRY: int = 5 + + # File flag, indicating the resource status. + POE_BUSY_FLAG_PATH: str = "/run/poe_busy.lock" + POED_INIT_FLAG_PATH: str = "/run/poed_init.lock" + POED_EXIT_FLAG_PATH: str = "/run/.poed_exit" + FILEFLAG_RETRY: int = 5 + + # poecli interop. + POECLI_GET_PORT_COUNT: str = "poecli_get_port_count" + POECLI_GET_BT_SUPPORT: str = "poecli_get_bt_support" + POECLI_SHOW_CMD: str = "poecli_show" + POECLI_GET_PORTS_INFO_CMD: str = "poecli_get_ports_info" + POECLI_GET_SYSTEM_INFO_CMD: str = "poecli_get_system_info" + POECLI_GET_VERSIONS_INFO_CMD: str = "poecli_get_versions_info" + POECLI_GET_MASK_REGS_CMD: str = "poecli_get_mask_regs" + POECLI_GET_LLDP_ENDIS_CMD: str = "poecli_get_lldp_endis" + POECLI_GET_DEFAULT_LIMITS_CMD: str = "poecli_get_default_limits" + POECLI_SET_CMD: str = "poecli_set" + POECLI_SET_PORT_ENDIS_CMD: str = "poecli_set_port_endis" + POECLI_SET_LLDP_ENDIS_CMD: str = "poecli_set_lldp_endis" + POECLI_SET_DEFAULT_LIMIT_CMD: str = "poecli_set_default_pwr" + POECLI_SET_PORT_PRIORITY_CMD: str = "poecli_set_port_priority" + POECLI_SET_PORT_POWER_LIMIT_CMD: str = "poecli_set_port_power_limit" + POECLI_FACTORY_RESET_CMD: str = "poecli_factory_reset" + POECLI_FLUSH_CMD: str = "poecli_flush" + POECLI_CFG_CMD: str = "poecli_cfg" + POECLI_SAVE_CMD: str = "poecli_save" + POECLI_LOAD_CMD: str = "poecli_load" + + POE_METRICS_FIFO_FOLDER: str = "/run/poe_helper/" + POE_METRICS_FIFO_PATH: str = "/run/poe_helper/poe_metrics_fifo" + + LLDP_POED_READ_FIFO: str = "/run/lldp_poed_read" + LLDP_POED_WRITE_FIFO: str = "/run/lldp_poed_write" + + POED_GRPC_SERVER_ADDRESS: str = "localhost:5005" diff --git a/dentos-poe-agent/opt/poeagent/inc/poe_common.py b/dentos-poe-agent/opt/poeagent/inc/poe_common.py index fbdd872..7b0deed 100755 --- a/dentos-poe-agent/opt/poeagent/inc/poe_common.py +++ b/dentos-poe-agent/opt/poeagent/inc/poe_common.py @@ -20,280 +20,283 @@ import syslog import fcntl import traceback -from pathlib import Path +from typing import Callable, ParamSpec, TypeVar -# POE Driver Attributes -TOTAL_PORTS = "total_ports" -TOTAL_POWER = "total_power" -POWER_LIMIT = "power_limit" -POWER_CONSUMP = "power_consump" -POWER_AVAIL = "power_avail" -POWER_BANK = "power_bank" -POWER_SRC = "power_src" -STATUS = "status" -PRIORITY = "priority" -PORT_ID = "port_id" -MAX_SD_VOLT = "max_sd_volt" -MIN_SD_VOLT = "min_sd_volt" -PPL = "ppl" -TPPL = "tppl" -ENDIS = "enDis" -CPU_STATUS1 = "cpu_status1" -CPU_STATUS2 = "cpu_status2" -FAC_DEFAULT = "fac_def" -GIE = "gen_intl_err" -PRIV_LABEL = "priv_label" -USER_BYTE = "user_byte" -DEVICE_FAIL = "device_fail" -TEMP_DISCO = "temp_disc" -TEMP_ALARM = "temp_alarm" -INTR_REG = "intr_reg" -PROTOCOL = "protocol" -CLASS = "class" -VOLTAGE = "voltage" -CURRENT = "current" -CSNUM = "poe_dev_addr_num" -TEMP = "temperature" -LATCH = "latch" -EN_4PAIR = "enable_4pair" -PM1 = "pm1" -PM2 = "pm2" -PM3 = "pm3" -SW_VERSION = "sw_version" -PROD_NUM = "prod_num" -CPU_STATUS2_ERROR = "cpu_status2_error" -NVM_USER_BYTE = "nvm_user_byte" -FOUND_DEVICE = "found_device" -EVENT_EXIST = "event_exist" -# POE Configuration Attributes -GEN_INFO = "GENERAL_INFORMATION" -TIMESTAMP = "TIMESTAMP" -SYS_INFO = "SYSTEM_INFORMATION" -PORT_CONFIGS = "PORTS_CONFIGURATIONS" -PORT_INFO = "PORT_INFORMATION" -INDV_MASKS = "INDV_MASKS" -VERSIONS = "VERSIONS" -PLATFORM = "platform" -POE_AGT_VER = "poe_agent_version" -POE_CFG_VER = "poe_config_version" -CFG_SERIAL_NUM = "file_serial_number" -LAST_SAVE_TIME = "file_save_time" -LAST_SET_TIME = "last_poe_set_time" -OPERATION_MODE = "operation_mode" -MEASURED_CLASS = "measured_class" -ACTIVE_MATRIX_PHYA = "ACTIVE_MATRIX_A" -ACTIVE_MATRIX_PHYB = "ACTIVE_MATRIX_B" -CMD_RESULT_RET = "ret" - -# IPC EVENT -POE_IPC_EVT = "/run/poe_ipc_event" -POECLI_SET = "poecli_set" -POECLI_CFG = "poecli_cfg" +from agent_constants import AgentConstants +from poe_log import PoeLog +from tinyrpc.protocols.jsonrpc import FixedErrorMessageMixin -# User guide -POE_USERGUIDE = "/opt/poeagent/docs/Userguide" +class EXIT_CODES: + SUCCESS = 0 + HAL_INIT_FAILED = -1 + CONFIG_INIT_FAILED = -2 + READ_FIFO_FAILED = -3 + WRITE_FIFO_FAILED = -4 + CREATE_FIFO_FAILED = -5 + LISTEN_POECLI_EVENTS_FAILED = -6 + HUNG_DETECTED = -7 -#POED CFG Predefine -POED_PERM_CFG_PATH = "/etc/poe_agent/poe_perm_cfg.json" -POED_RUNTIME_CFG_PATH = "/run/poe_runtime_cfg.json" -POED_SAVE_ACTION = "save" -POED_LOAD_ACTION = "load" - -# POE Access Exclusive Lock -POE_ACCESS_LOCK = "/run/poe_access.lock" -EXLOCK_RETRY = 5 +# POE Driver Attributes +TOTAL_PORTS: str = "total_ports" +TOTAL_POWER: str = "total_power" +POWER_LIMIT: str = "power_limit" +POWER_CONSUMP: str = "power_consump" +CALCULATED_POWER: str = "calculated_power" +POWER_AVAIL: str = "power_avail" +POWER_BANK: str = "power_bank" +POWER_SRC: str = "power_src" +STATUS: str = "status" +PRIORITY: str = "priority" +PORT_ID: str = "port_id" +MAX_SD_VOLT: str = "max_sd_volt" +MIN_SD_VOLT: str = "min_sd_volt" +PPL: str = "ppl" +TPPL: str = "tppl" +ENDIS: str = "enDis" +CPU_STATUS1: str = "cpu_status1" +CPU_STATUS2: str = "cpu_status2" +FAC_DEFAULT: str = "fac_def" +GIE: str = "gen_intl_err" +PRIV_LABEL: str = "priv_label" +USER_BYTE: str = "user_byte" +DEVICE_FAIL: str = "device_fail" +TEMP_DISCO: str = "temp_disc" +TEMP_ALARM: str = "temp_alarm" +INTR_REG: str = "intr_reg" +PROTOCOL: str = "protocol" +CLASS: str = "class" +VOLTAGE: str = "voltage" +CURRENT: str = "current" +CSNUM: str = "poe_dev_addr_num" +TEMP: str = "temperature" +LATCH: str = "latch" +EN_4PAIR: str = "enable_4pair" +PM1: str = "pm1" +PM2: str = "pm2" +PM3: str = "pm3" +SW_VERSION: str = "sw_version" +PROD_NUM: str = "prod_num" +CPU_STATUS2_ERROR: str = "cpu_status2_error" +NVM_USER_BYTE: str = "nvm_user_byte" +FOUND_DEVICE: str = "found_device" +EVENT_EXIST: str = "event_exist" +PORT_MODE_CFG1: str = "port_mode_cfg1" +SHUTDOWN_STATUS: str = "shutdown_status" +PORT_EVENT: str = "port_event" +ACTIVE_MATRIX_PHYA: str = "active_matrix_a" +ACTIVE_MATRIX_PHYB: str = "active_matrix_b" +OPERATION_MODE: str = "operation_mode" +PSE_ALLOCATED_POWER: str = "allocated_power" +PSE_ALLOCATED_POWER_SINGLE_ALT_A: str = "allocated_power_single_alt_a" +PSE_ALLOCATED_POWER_ALT_B: str = "allocated_power_single_alt_b" +PSE_MAX_POWER: str = "pse_max_power" +MEASURED_CLASS_ALT_A: str = "measured_class_alt_a" +MEASURED_CLASS_ALT_B: str = "measured_class_alt_b" +REQUESTED_CLASS_ALT_A: str = "requested_class_alt_a" +REQUESTED_CLASS_ALT_B: str = "requested_class_alt_b" +ASSIGNED_CLASS_ALT_A: str = "assigned_class_alt_a" +ASSIGNED_CLASS_ALT_B: str = "assigned_class_alt_b" +LAYER2_EXECUTION: str = "layer2_execution" +LAYER2_USAGE: str = "layer2_usage" +PSE_POWERING_STATUS: str = "pse_powering_status" +PSE_POWER_PAIRS_EXT: str = "pse_power_pairs_ext" +CABLE_LENGTH: str = "cable_length" +PD_REQUESTED_POWER: str = "requested_power" +PD_REQUESTED_POWER_SINGLE: str = "requested_power_single" +PD_REQUESTED_POWER_MODE_A: str = "requested_power_mode_a" +PD_REQUESTED_POWER_MODE_B: str = "requested_power_mode_b" +REQUESTED_CABLE_LENGTH: str = "requested_cable_length" +MEASURED_CLASS = "measured_class" -# POE PID file location -POED_PID_PATH = "/run/poed.pid" -# POE fileflag function -POED_BUSY_FLAG = "/run/.poed_busy" -POED_EXIT_FLAG = "/run/.poed_exit" -FILEFLAG_RETRY = 5 +def print_stderr(msg: str, end: str = "\n", flush: bool = True): + """Flush the message to stderr, when logging is not an option -def print_stderr(msg,end="\n",flush=True): - sys.stderr.write(msg+end) + Args: + msg (str): Message to print + end (str, optional): Termination token. Defaults to "\n". + flush (bool, optional): Flush flag. Defaults to True. + """ + sys.stderr.write(msg + end) if flush: sys.stderr.flush() -class PoeLog(object): - def __init__(self, debug_mode=False): - self.debug_mode = debug_mode - def emerg(self, msg): - self._record(syslog.LOG_EMERG, "EMERG: %s" % msg) +def conv_byte_to_hex(bytes_in: list[int]) -> str: + """Convert a list of byte integers into a hex-formatted string + + Args: + byte_in (list[int]): List to convert - def alert(self, msg): - self._record(syslog.LOG_ALERT, "ALERT: %s" % msg) + Returns: + str: Hex string + """ + hex_string = "".join("%02x," % b for b in bytes_in) + hex_string = hex_string + "[EOF]" + return hex_string - def crit(self, msg): - self._record(syslog.LOG_CRIT, "CRIT: %s" % msg) - def err(self, msg): - self._record(syslog.LOG_ERR, "ERR: %s" % msg) +_P = ParamSpec("P") # type: ignore +_T = TypeVar("T") # type: ignore - def warn(self, msg): - self._record(syslog.LOG_WARNING, "WARN: %s" % msg) - def notice(self, msg): - self._record(syslog.LOG_NOTICE, "NOTICE: %s" % msg) +def PoeAccessExclusiveLock(func: Callable[_P, _T]) -> Callable[_P, _T | None]: + """Generic function synchronization decorator - def info(self, msg): - self._record(syslog.LOG_INFO, "INFO: %s" % msg) + Args: + func (Callable[_P, _T]): Decorated function - def dbg(self, msg): - self._record(syslog.LOG_DEBUG, "DBG: %s" % msg) + Returns: + Callable[_P, _T]: Wrapper + """ - def _record(self, priority, msg): - syslog.syslog(priority, msg) - if self.debug_mode == True: - sys.stdout.write(msg+"\n") + def wrap_cmd(*args: _P.args, **kwargs: _P.kwargs) -> _T | None: + """Execute the wrapped function only if the locking is successful. + If the locking fails, there's a predefined number of retries + (i.e., EXLOCK_RETRY). + Locking is done based on a pre-defined file to allow both the PoE CLI + and the PoE agent to have write-through access to the PoE chipset. -def PoeAccessExclusiveLock(func): - def wrap_cmd(*args, **kwargs): + Returns: + _T | None: The decorated function return value after executing + it or None + """ try: - fd = open(POE_ACCESS_LOCK, 'r') + fd = open(AgentConstants.POE_ACCESS_LOCK_PATH, "r") except IOError: - fd = open(POE_ACCESS_LOCK, 'wb') - res = False - LOCKED = False - retry = EXLOCK_RETRY + fd = open(AgentConstants.POE_ACCESS_LOCK_PATH, "wb") + locked = False + retry = AgentConstants.EXLOCK_RETRY while retry > 0: try: fcntl.flock(fd, fcntl.LOCK_EX) - - if retry < EXLOCK_RETRY: - print_stderr("[{0}]Locked, retry: {1}".format( - func.__name__, str(retry))) - LOCKED = True + if retry < AgentConstants.EXLOCK_RETRY: + PoeLog().err(f"[{func.__name__}] Locked, remaining retries: " f"{str(retry)}") + locked = True break except Exception as e: - # pass - retry = retry-1 - print_stderr("[{0}]Retry locking, retry: {1}, Exception: {2}".format( - func.__name__, str(retry),str(e))) + retry -= 1 + PoeLog().err( + f"[{func.__name__}] Retry locking, remaining retries: " f"{str(retry)}, exception: {str(e)}" + ) time.sleep(0.1) - if retry == 0: - return res - if LOCKED: + if locked: try: - if retry < EXLOCK_RETRY: - print_stderr("[{0}]Locked execution code".format( - func.__name__)) - res = func(*args, **kwargs) + if retry < AgentConstants.EXLOCK_RETRY: + PoeLog().err(f"[{func.__name__}] Locked execution code") + return func(*args, **kwargs) except Exception as e: + if isinstance(e, FixedErrorMessageMixin): + # Tinyrpc exceptions must be propagated to allow + # a proper error response. + raise + # Print the closest entry in the stack trace. error_class = e.__class__.__name__ detail = e.args[0] - cl, exc, tb = sys.exc_info() - lastCallStack = traceback.extract_tb(tb)[-1] - fileName = lastCallStack[0] - lineNum = lastCallStack[1] - funcName = lastCallStack[2] - errMsg = "File \"{}\", line {}, in {}: [{}] {}".format( - fileName, lineNum, funcName, error_class, detail) - print_stderr("[{0}]Locked but execution failed: {1}".format( - func.__name__, str(errMsg))) + _, _, tb = sys.exc_info() + last_entry = traceback.extract_tb(tb)[-1] + file_name = last_entry[0] + line_number = last_entry[1] + func_name = last_entry[2] + err_message = f'File "{file_name}", line {line_number}, ' f"in {func_name}: [{error_class}] {detail}" + PoeLog().err(f"[{func_name}] Locked, but execution failed: " f"{str(err_message)}") finally: fcntl.flock(fd, fcntl.LOCK_UN) - return res - return wrap_cmd + return None -def touch_file(file_path): - try: - return Path(file_path).touch() - except Exception as e: - print_stderr("Fail to touch: "+file_path+",err: "+str(e)) - return False + return wrap_cmd -def remove_file(file_path): - try: - if check_file(file_path): - return Path(file_path).unlink() - else: - return True - except Exception as e: - print_stderr("Fail to remove: "+file_path+",err: "+str(e)) - return False - - -def check_file(file_path): - try: - return Path(file_path).exists() - except Exception as e: - print_stderr("Fail to check: "+file_path+",err: "+str(e)) - return False - - -def wait_poed_busy(timeout=FILEFLAG_RETRY): - ret = check_file(POED_BUSY_FLAG) - while ret == True: - ret = check_file(POED_BUSY_FLAG) - print_stderr("\rpoe agent busy...") - if timeout > 0: - timeout -= 1 - else: - print_stderr("\r\rpoe agent busy...timeout") - return False - time.sleep(1) +def is_active_port_matrix_different(new_matrix: list, platform_cb: Callable[[int], dict[str, int]]) -> bool: + """Compare the actual port matrix against the current active port matrix. + If the two matrices differ in terms of mapped physical ports, then return True + Args: + new_matrix (list): New port matrix + platform_cb (Callable[[int], dict[str, int]])): HAL callback for + querying the active port matrix, given a logical port index -def conv_byte_to_hex(byte_in): - hex_string = "".join("%02x," % b for b in byte_in) - hex_string = hex_string+"[EOF]" - return hex_string - -def fast_temp_matrix_compare(def_matrix,plat_obj): - get_phya = None - get_phyb = None - if len(def_matrix[0]) == 3: - print_stderr("Select 4-Pair mode") + Returns: + bool: True if the two matrices differ, False otherwise + """ + if len(new_matrix[0]) == 3: + PoeLog().info("Detected 4-Pair mode") four_pair = True else: - print_stderr("Select 2-Pair mode") + PoeLog().info("Detected 2-Pair mode") four_pair = False - for def_mat_pair in def_matrix: - idx = def_mat_pair[0] - get_phya = plat_obj.get_active_matrix(idx)[ACTIVE_MATRIX_PHYA] - if get_phya != def_mat_pair[1]: - print_stderr("Port map mismatch, run program global matrix") + + for port_tuple in new_matrix: + port_index = port_tuple[0] + get_phya = platform_cb(port_index)[ACTIVE_MATRIX_PHYA] + if get_phya != port_tuple[1]: + PoeLog().err( + f"Active port map logical port {port_index} PHY A is " + "different from the new port map. Must " + "reprogram the global matrix" + ) return False - if four_pair == True: - get_phyb = plat_obj.get_active_matrix(idx)[ACTIVE_MATRIX_PHYB] - if get_phyb != def_mat_pair[2]: - print_stderr("Port map mismatch, run program global matrix") + + if four_pair: + get_phyb = platform_cb(port_index)[ACTIVE_MATRIX_PHYB] + if get_phyb != port_tuple[2]: + PoeLog().err( + f"Active port map logical port {port_index} PHY B is " + "different from the new port map. Must " + "reprogram the global matrix" + ) return False - print_stderr("Port map match, skip program global matrix") + + PoeLog().info("Both port matrices match") return True -def check_init_plat_ret_result(init_poe_result, sum_mode=0): - all_ret = [] - sum_result = 0 - for name in init_poe_result: - if type(init_poe_result[name]) is dict: - if CMD_RESULT_RET in init_poe_result[name]: - all_ret.append((name, init_poe_result[name][CMD_RESULT_RET])) - else: - all_ret += (name, check_init_plat_ret_result(init_poe_result[name], sum_mode+1)) - - elif type(init_poe_result[name]) is list: - for itm in init_poe_result[name]: - all_ret += (name,check_init_plat_ret_result(itm, sum_mode+1)) - elif type(name) is int: - all_ret.append(name) - if sum_mode == 0: - sum_result += name - elif name == CMD_RESULT_RET: - all_ret.append(init_poe_result[name]) - - for itm_result in all_ret: - if type(itm_result) is tuple: - sum_result += itm_result[1] - elif type(itm_result) is int: - sum_result += itm_result - return (all_ret, sum_result) +def has_any_op_failed(result: dict | list) -> bool: + """Detect whether any command, that is part of the result dictionary, + has failed. + + Args: + result (dict | list): Result object, containing the operation results + + Returns: + bool: True if any operation failed, False otherwise + """ + if isinstance(result, dict): + if "ret" in result: + inner_result = result["ret"] + if not isinstance(inner_result, int) and not isinstance(inner_result, dict): + raise AssertionError("Invalid operation result object format") + + # Check for multiple operation results that can be + # lumped together for the same item. + final_result = inner_result + if not isinstance(inner_result, int): + final_result = 0 + for inner_name, inner_val in inner_result.items(): + assert isinstance(inner_val, int), ( + "Nested operation results must be passed directly " + "as a value to the operation key: " + f"{inner_name}" + ) + final_result += inner_val + + if 0 != final_result: + return True + else: + # Recurse into nested op result. + for _, value in result.items(): + if has_any_op_failed(value): + return True + elif isinstance(result, list): + for op in result: + if not isinstance(op, dict) and not isinstance(op, list): + raise AssertionError("The operation can only be a dictionary or a list") + if has_any_op_failed(op): + return True + + # We're good, no command failed yet. + return False diff --git a/dentos-poe-agent/opt/poeagent/inc/poe_log.py b/dentos-poe-agent/opt/poeagent/inc/poe_log.py new file mode 100644 index 0000000..d8a595e --- /dev/null +++ b/dentos-poe-agent/opt/poeagent/inc/poe_log.py @@ -0,0 +1,76 @@ +''' +Copyright Amazon Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +''' + + +import sys +import syslog +import traceback + +from singleton_thread_safe import SingletonThreadSafe + + +class PoeLog(object, metaclass=SingletonThreadSafe): + """Syslog-based wrapper""" + + def __init__(self, debug_mode: bool = False) -> None: + self.debug_mode = debug_mode + + def emerg(self, msg: str) -> None: + self.__record(syslog.LOG_EMERG, "EMERG: %s" % msg) + + def alert(self, msg: str) -> None: + self.__record(syslog.LOG_ALERT, "ALERT: %s" % msg) + + def crit(self, msg: str) -> None: + self.__record(syslog.LOG_CRIT, "CRIT: %s" % msg) + + def err(self, msg: str) -> None: + self.__record(syslog.LOG_ERR, "ERR: %s" % msg) + + def warn(self, msg: str) -> None: + self.__record(syslog.LOG_WARNING, "WARN: %s" % msg) + + def notice(self, msg: str) -> None: + self.__record(syslog.LOG_NOTICE, "NOTICE: %s" % msg) + + def info(self, msg: str) -> None: + self.__record(syslog.LOG_INFO, "INFO: %s" % msg) + + def dbg(self, msg: str) -> None: + self.__record(syslog.LOG_DEBUG, "DBG: %s" % msg) + + def exc(self, msg: str) -> None: + """Log an error message beside the current exception message as an + error + + Args: + msg (string): Error message to log + """ + if sys.exc_info()[0] is not None: + for line in traceback.format_exc().splitlines(): + self.err(line) + self.err(msg) + + def __record(self, priority: int, msg: str) -> None: + """Forward the priority and the message to syslog + + Args: + priority (integer): Log priority + msg (string): Log message + """ + syslog.syslog(priority, msg) + if self.debug_mode: + sys.stdout.write(msg + "\n") diff --git a/dentos-poe-agent/opt/poeagent/inc/poe_platform.py b/dentos-poe-agent/opt/poeagent/inc/poe_platform.py new file mode 100644 index 0000000..5e3004e --- /dev/null +++ b/dentos-poe-agent/opt/poeagent/inc/poe_platform.py @@ -0,0 +1,144 @@ +''' +Copyright 2021 Delta Electronic Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +''' + + +import importlib.util +from abc import abstractmethod + +from agent_constants import AgentConstants +from drivers.pd69200.poe_driver import PoeDriver_microsemi_pd69200 +from poe_log import PoeLog +from poe_common import print_stderr +import importlib.util +import os + +class PoePlatform(PoeDriver_microsemi_pd69200): + @abstractmethod + def port_count(self) -> int: + pass + + @property + @abstractmethod + def default_power_limits(self) -> dict[int, int]: + pass + + @abstractmethod + def init_poe(self, skip_port_init: bool) -> dict: + pass + + @abstractmethod + def power_bank_to_str(self, bank_index: int) -> str: + pass + + +class PoePlatformFactory: + """Platform HAL factory, facilitating the instantiation of a HAL object.""" + + def __init__(self) -> None: + self._log = PoeLog() + + @staticmethod + def create_platform_from_bootcmd(bootcmd_path: str) -> tuple[PoePlatform | None, str | None]: + """Create the platform HAL based on the bootcmd string + + Args: + bootcmd_path (str): Path to the bootcmd file + + Returns: + tuple[PoePlatform | None, str | None]: The platform HAL instance, + if successful + """ + factory = PoePlatformFactory() + platform_string = factory.__get_platform_string(bootcmd_path) + # Import the platform module based on the bootcmd string. + if platform_string is not None: + return (factory.__load_poe_plat(factory.__get_platform_module_path(platform_string)), platform_string) + + return (None, None) + + def __get_platform_string(self, bootcmd_path: str) -> str | None: + """Extract the platform string from /etc/onl/platform if available, or the bootcmd file otherwise. + Args: + bootcmd_path (str): Path to the bootcmd file + + Returns: + str | None: Platform string, if successful + """ + + try: + with open(AgentConstants.ONL_PLATFORM_PATH, "r") as fh: + return fh.read().rstrip() + except FileNotFoundError: + self.log.warn( + "Couldn't find platform file %s, falling back to kernel cmdline" % AgentConstants.ONL_PLATFORM_PATH + ) + + try: + with open(bootcmd_path, "r") as f: + d = dict() + for arg in f.read().split(" "): + # this test is necessary to avoid choking on args like "rw" + # we are choosing to not store such args in the dict + if "=" in arg: + key, value = arg.split("=") + d[key] = value + self._log.dbg(f"onl_platform: {d.get('onl_platform')}") + return d.get("onl_platform").rstrip() + except Exception as e: + self._log.crit(f"Failed to get the platform string: {e}") + + return None + + def __get_platform_module_path(self, platform: str) -> str: + """Build the platform HAL module path + + Args: + platform (str): The platform string + + Returns: + str: The module path + """ + # dentOS platform format: --- + [_, _, model_revision] = platform.replace("_", "-").split("-", 2) + model_revision = model_revision.replace("-", "_") + py_path = "/".join([AgentConstants.PLAT_VENDOR_PATH, f"{model_revision}.py"]) + self._log.dbg(f"Platform HAL module path: {py_path}") + return py_path + + def __load_poe_plat(self, platform_py_path: str) -> PoePlatform | None: + """Programmatically import the platform module and instantiate a HAL + + Args: + platform_py_path (str): Path to the platform module + + Returns: + PoePlatform | None: The platform HAL, if successful + """ + if os.path.exists(platform_py_path): + poe_plat = None + try: + spec = importlib.util.spec_from_file_location("poe_plat", platform_py_path) + poe_plat = importlib.util.module_from_spec(spec) + spec.loader.exec_module(poe_plat) + poe_plat = poe_plat.get_poe_platform() + return poe_plat + except Exception as e: + print_stderr(f"Failed to instantiate the PoE platform HAL: {e}") + raise + else: + print_stderr(f"No PoE platform found at {platform_py_path}, assuming no PoE support.") + + return None \ No newline at end of file diff --git a/dentos-poe-agent/opt/poeagent/inc/poe_telemetry.py b/dentos-poe-agent/opt/poeagent/inc/poe_telemetry.py new file mode 100644 index 0000000..e4c24ac --- /dev/null +++ b/dentos-poe-agent/opt/poeagent/inc/poe_telemetry.py @@ -0,0 +1,27 @@ +''' +Copyright Amazon Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +''' + + +import errno +import json +import os + +from agent_constants import AgentConstants +from poe_common import print_stderr + + +def publish_metrics(metric_name: str, metric_value: int) -> bool: + pass diff --git a/dentos-poe-agent/opt/poeagent/inc/poe_version.py b/dentos-poe-agent/opt/poeagent/inc/poe_version.py index b2df280..f3e2ead 100755 --- a/dentos-poe-agent/opt/poeagent/inc/poe_version.py +++ b/dentos-poe-agent/opt/poeagent/inc/poe_version.py @@ -1,5 +1,5 @@ ''' -Copyright 2021 Delta Electronic Inc. +Copyright Amazon Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -13,5 +13,6 @@ See the License for the specific language governing permissions and limitations under the License. ''' -POE_AGENT_VERSION = "0.4.0-alpha" -POE_CONFIG_VERSION = "1.1.0" + +POE_AGENT_VERSION = "0.7.0" +POE_CONFIG_VERSION = "1.2.0" diff --git a/dentos-poe-agent/opt/poeagent/inc/poed_ipc_pb2.py b/dentos-poe-agent/opt/poeagent/inc/poed_ipc_pb2.py new file mode 100644 index 0000000..a062119 --- /dev/null +++ b/dentos-poe-agent/opt/poeagent/inc/poed_ipc_pb2.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: poed_ipc.proto +# Protobuf Python Version: 4.25.0 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder + +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\n\x0epoed_ipc.proto\x12\x07PoedIpc" \n\rPoecliRequest\x12\x0f\n\x07request\x18\x01 \x01(\t"\x1c\n\x0bPoecliReply\x12\r\n\x05reply\x18\x01 \x01(\t2F\n\x06PoeIpc\x12<\n\x0cHandlePoecli\x12\x16.PoedIpc.PoecliRequest\x1a\x14.PoedIpc.PoecliReplyb\x06proto3' +) + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "poed_ipc_pb2", _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals["_POECLIREQUEST"]._serialized_start = 27 + _globals["_POECLIREQUEST"]._serialized_end = 59 + _globals["_POECLIREPLY"]._serialized_start = 61 + _globals["_POECLIREPLY"]._serialized_end = 89 + _globals["_POEIPC"]._serialized_start = 91 + _globals["_POEIPC"]._serialized_end = 161 +# @@protoc_insertion_point(module_scope) diff --git a/dentos-poe-agent/opt/poeagent/inc/poed_ipc_pb2_grpc.py b/dentos-poe-agent/opt/poeagent/inc/poed_ipc_pb2_grpc.py new file mode 100644 index 0000000..73e1339 --- /dev/null +++ b/dentos-poe-agent/opt/poeagent/inc/poed_ipc_pb2_grpc.py @@ -0,0 +1,76 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc +import poed_ipc_pb2 as poed__ipc__pb2 + + +class PoeIpcStub(object): + """Missing associated documentation comment in .proto file.""" + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.HandlePoecli = channel.unary_unary( + "/PoedIpc.PoeIpc/HandlePoecli", + request_serializer=poed__ipc__pb2.PoecliRequest.SerializeToString, + response_deserializer=poed__ipc__pb2.PoecliReply.FromString, + ) + + +class PoeIpcServicer(object): + """Missing associated documentation comment in .proto file.""" + + def HandlePoecli(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details("Method not implemented!") + raise NotImplementedError("Method not implemented!") + + +def add_PoeIpcServicer_to_server(servicer, server): + rpc_method_handlers = { + "HandlePoecli": grpc.unary_unary_rpc_method_handler( + servicer.HandlePoecli, + request_deserializer=poed__ipc__pb2.PoecliRequest.FromString, + response_serializer=poed__ipc__pb2.PoecliReply.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler("PoedIpc.PoeIpc", rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + + +# This class is part of an EXPERIMENTAL API. +class PoeIpc(object): + """Missing associated documentation comment in .proto file.""" + + @staticmethod + def HandlePoecli( + request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None, + ): + return grpc.experimental.unary_unary( + request, + target, + "/PoedIpc.PoeIpc/HandlePoecli", + poed__ipc__pb2.PoecliRequest.SerializeToString, + poed__ipc__pb2.PoecliReply.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + ) diff --git a/dentos-poe-agent/opt/poeagent/inc/singleton_thread_safe.py b/dentos-poe-agent/opt/poeagent/inc/singleton_thread_safe.py new file mode 100644 index 0000000..b3afa23 --- /dev/null +++ b/dentos-poe-agent/opt/poeagent/inc/singleton_thread_safe.py @@ -0,0 +1,37 @@ +''' +Copyright Amazon Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +''' + + +import threading + + +class SingletonThreadSafe(type): + _instances = {} + _instance_locks = {} + _singleton_lock = threading.Lock() + + def __call__(cls, *args, **kwargs): + # double-checked locking pattern + instance = (cls, frozenset(args), frozenset(kwargs.items())) + if instance not in cls._instances: + with cls._singleton_lock: + lock = cls._instance_locks.setdefault(instance, threading.Lock()) + with lock: + if instance not in cls._instances: + cls._instances[instance] = super(SingletonThreadSafe, cls).__call__(*args, **kwargs) + with cls._singleton_lock: + del cls._instance_locks[instance] + return cls._instances[instance] diff --git a/dentos-poe-agent/opt/poeagent/platforms/accton/as4224-52p-r0/poe_platform.py b/dentos-poe-agent/opt/poeagent/platforms/accton/as4224-52p-r0/poe_platform.py deleted file mode 100644 index 8e8d1d8..0000000 --- a/dentos-poe-agent/opt/poeagent/platforms/accton/as4224-52p-r0/poe_platform.py +++ /dev/null @@ -1,162 +0,0 @@ -from collections import OrderedDict -from poe_driver_pd69200_def import * -from poe_common import * -from poe_common import print_stderr -from smbus2 import SMBus, i2c_msg - -import os -import sys -import time -import fcntl -import poe_driver_pd69200 as PoeDrv - -def get_poe_platform(): - return PoePlatform_accton_as4224_52p() - -class PoePlatform_accton_as4224_52p(PoeDrv.PoeDriver_microsemi_pd69200): - def __init__(self): - PoeDrv.PoeDriver_microsemi_pd69200.__init__(self) - self.log = PoeLog() - self._total_poe_port = 48 - self._i2c_bus = 1 - self._i2c_addr = 0x3C - self._poe_bus = SMBus(self._i2c_bus) - - # Add read 15byte first to cleanup buffer - self.plat_poe_read() - - # item in matrix: (logic port, phy port) - self._default_matrix = [ - # locgic port - ( 0, 7), ( 1, 4), ( 2, 5), ( 3, 6), ( 4, 0), ( 5, 1), ( 6, 2), ( 7, 3), - ( 8, 12), ( 9, 13), (10, 14), (11, 15), (12, 9), (13, 10), (14, 11), (15, 8), - (16, 20), (17, 21), (18, 22), (19, 23), (20, 17), (21, 18), (22, 19), (23, 16), - (24, 28), (25, 29), (26, 30), (27, 31), (28, 27), (29, 26), (30, 25), (31, 24), - (32, 39), (33, 36), (34, 37), (35, 38), (36, 32), (37, 33), (38, 34), (39, 35), - (40, 47), (41, 44), (42, 45), (43, 46), (44, 40), (45, 41), (46, 42), (47, 43)] - - self._max_shutdown_vol = 0x0249 # 58.5 V - self._min_shutdown_vol = 0x0190 # 40.0 V - self._guard_band = 0x01 - self._port_power_limit = 0x7530 # 30000 mW - self._default_power_banks = [(1, 800)] - - def total_poe_port(self): - return self._total_poe_port - - def _bus(self): - if self._poe_bus.fd is None: - self._poe_bus = SMBus(self._poe_bus) - return self._poe_bus - - def _i2c_write(self, bus, msg, delay = 0.03): - write = i2c_msg.write(self._i2c_addr, msg) - bus.i2c_rdwr(write) - time.sleep(delay) - - def _i2c_read(self, bus, size = 15): - read = i2c_msg.read(self._i2c_addr, size) - bus.i2c_rdwr(read) - msg = list(read) - return msg - - def plat_poe_write(self, msg, delay): - return self._i2c_write(self._bus(), msg, delay) - - def plat_poe_read(self): - return self._i2c_read(self._bus()) - - def bus_lock(self): - fcntl.flock(self._bus().fd, fcntl.LOCK_EX) - - def bus_unlock(self): - fcntl.flock(self._bus().fd, fcntl.LOCK_UN) - - def init_poe(self, config_in=None): - ret_item = OrderedDict() - # Clean buffers to reduce retry time - self.plat_poe_read() - - # Fast compare active and temp matrix - if fast_temp_matrix_compare(self._default_matrix, self) == False: - prog_global_matrix = True - else: - prog_global_matrix = False - - # Port result list - set_port_item = dict() - # Default values - set_port_item["set_port_params"] = [] - set_port_item["set_temp_matrix"] = [] - ret_item["set_power_bank"] = [] - ret_item["set_op_mode"] = [] - result_prog_matrix = None - result_save_sys = None - - # Create default parameter (Disable, low priority) - default_param = dict({ - ENDIS: "disable", - PRIORITY: "low", - POWER_LIMIT: self._port_power_limit, - }) - # Set Temporary Matrix and - for temp_matrix_mapping in self._default_matrix: - logic_port = temp_matrix_mapping[0] - phy_porta = temp_matrix_mapping[1] - if config_in == None: - port = self.get_poe_port(logic_port) - result = port.set_all_params(default_param) - set_port_item["set_port_params"].append({ - "idx": logic_port, - CMD_RESULT_RET: result - }) - elif config_in == True: - # Preserve current state - pass - - if prog_global_matrix == True: - result = self.set_temp_matrix(logic_port, phy_porta) - set_port_item["set_temp_matrix"].append({ - "idx": logic_port, - CMD_RESULT_RET: result - }) - ret_item["set_port_item"] = set_port_item - - # Set Power Bank - for _power_bank in self._default_power_banks: - (bank, power_limit) = _power_bank - result = self.set_power_bank(bank, power_limit) - ret_item["set_power_bank"].append({ - "setting": _power_bank, - CMD_RESULT_RET: result - }) - - # Set POE Power Management Method - result = self.set_pm_method(POE_PD69200_MSG_DATA_PM1_DYNAMIC, - POE_PD69200_MSG_DATA_PM2_PPL, - POE_PD69200_MSG_DATA_PM3_NO_COND) - ret_item["set_pm_method"] = { - CMD_RESULT_RET: result - } - - if prog_global_matrix == True: - print_stderr( - "Program active matrix, all ports will shutdown a while") - result_prog_matrix = self.program_active_matrix() - print_stderr( - "Program active matrix completed, save platform settings to chip") - result_save_sys = self.save_system_settings() - ret_item["program_active_matrix"] = { - CMD_RESULT_RET: result_prog_matrix - } - ret_item["save_system_settings"] = { - CMD_RESULT_RET: result_save_sys - } - return ret_item - - - def bank_to_psu_str(self, bank): - powerSrc = "None" - if bank == 1: - powerSrc = "PSU1, PSU2" - return powerSrc diff --git a/dentos-poe-agent/opt/poeagent/platforms/accton/as4224_52p_r0.py b/dentos-poe-agent/opt/poeagent/platforms/accton/as4224_52p_r0.py new file mode 100644 index 0000000..ddeac13 --- /dev/null +++ b/dentos-poe-agent/opt/poeagent/platforms/accton/as4224_52p_r0.py @@ -0,0 +1,237 @@ +from collections import OrderedDict + +from agent_constants import AgentConstants +from i2c_driver import I2cDriver +from pd69200.poe_driver import PoeDriver_microsemi_pd69200 +from pd69200.poe_driver_def import ( + POE_PD69200_MSG_DATA_PM1_DYNAMIC, + POE_PD69200_MSG_DATA_PM2_PPL, + POE_PD69200_MSG_DATA_PM3_NO_COND, + POE_PD69200_MSG_DATA_PROTOCOL_ATAF, + POE_PD69200_MSG_DATA_SUM_AS_TPPL_STATIC, + POE_PD69200_MSG_SUB2_ALL_CHANNEL, +) +from poe_common import * +from poe_log import PoeLog +from poe_platform import PoePlatform + + +class As4224_52p(PoePlatform): + # Accton AS4224-52P + def __init__(self): + self._max_shutdown_vol = 0x0249 # 56.9 V + self._min_shutdown_vol = 0x0190 # 50.1 V + self._guard_band = 0x01 + """ + +-----------------------------------------------+ + | Power Banks | PSU1 PG | PSU2 PG | Power Limit | + |-----------------------------------------------| + | Bank 13 | NO | YES | 680 W | + |-----------------------------------------------| + | Bank 14 | YES | NO | 680 W | + |-----------------------------------------------| + | Bank 15 | YES | YES | 1500 W | + +-----------------------------------------------+ + """ + self._default_power_banks = [(1, 800)] + self._default_port_power_limit = 0x7530 # 30000 mW + self._bus_driver = I2cDriver(i2c_bus=0x01, i2c_addr=0x3C) + self._port_count = 48 + PoeDriver_microsemi_pd69200.__init__( + self, + self._bus_driver, + self.port_count, + self._max_shutdown_vol, + self._min_shutdown_vol, + self._guard_band, + self.power_bank_to_str, + ) + self._log = PoeLog() + + # Clear the I2C buffer. + self._bus_driver.read_message() + + # Mapping: (logical port, phy port) + self._port_matrix = [ + (0, 7), + (1, 4), + (2, 5), + (3, 6), + (4, 0), + (5, 1), + (6, 2), + (7, 3), + (8, 12), + (9, 13), + (10, 14), + (11, 15), + (12, 9), + (13, 10), + (14, 11), + (15, 8), + (16, 20), + (17, 21), + (18, 22), + (19, 23), + (20, 17), + (21, 18), + (22, 19), + (23, 16), + (24, 28), + (25, 29), + (26, 30), + (27, 31), + (28, 27), + (29, 26), + (30, 25), + (31, 24), + (32, 39), + (33, 36), + (34, 37), + (35, 38), + (36, 32), + (37, 33), + (38, 34), + (39, 35), + (40, 47), + (41, 44), + (42, 45), + (43, 46), + (44, 40), + (45, 41), + (46, 42), + (47, 43), + ] + + # Ignore default power limit allocation. + self._default_power_limits = {} + + + def port_count(self) -> int: + """Get the total PoE port count + + Returns: + int: Port count + """ + return self._port_count + + @property + def default_power_limits(self) -> dict[int, int]: + """Get the port ranges default power limit + + Returns: + dict[tuple[int, int], int]: Default limits as a dictionary + """ + return self._default_power_limits + + def init_poe(self, skip_port_init: bool) -> dict: + """Initialize the PoE ports, power bank config and + each port operation mode. If skip_port_init is true, + will not set the default port parameters + + The global port matrix will be reprogrammed, only if + the actual matrix is different than the active port matrix. + + Args: + skip_port_init (bool): Skip port init flag + + Returns: + dict: Result dictionary. Contains the result for each + individual operation + """ + # Clear the I2C buffer. + self._bus_driver.read_message() + + # Default port params to initialize with. + port_default_params = { + ENDIS: AgentConstants.ENABLE, + PRIORITY: "low", + POWER_LIMIT: self._default_port_power_limit, + } + + # Determine if we need to reprogram the port matrix. + program_port_matrix = False + if not is_active_port_matrix_different(self._port_matrix, self.get_active_matrix): + program_port_matrix = True + + # Configure the power bank power and voltage limits with the actual + # values. + result = OrderedDict() + result["power_bank"] = [] + for power_bank in self._default_power_banks: + (bank_index, power_limit) = power_bank + result["power_bank"].append( + { + "bank_details": power_bank, + AgentConstants.CMD_RESULT_RET: self.set_power_bank(bank_index, power_limit), + } + ) + # Confirm that the power bank was successfully configured. + power_bank_details = self.get_power_bank(bank_index) + result["power_bank"][-1][AgentConstants.CMD_RESULT_RET] = ( + 0 if power_bank_details[POWER_LIMIT] == power_limit else 1 + ) + # Prevent enabling or changing any port parameter if the power bank + # configuration failed. + if power_bank_details[POWER_LIMIT] != power_limit: + return result + + set_port_results = {} + set_port_results["set_port_params"] = [] + if program_port_matrix: + set_port_results["set_temp_matrix"] = [] + if skip_port_init: + self._log.dbg("Skipping port initialization") + for ports in self._port_matrix: + port_index, phy_port = ports + if not skip_port_init: + port = self.get_poe_port(port_index) + set_port_results["set_port_params"].append( + {"idx": port_index, AgentConstants.CMD_RESULT_RET: port.set_all_params(port_default_params)} + ) + if program_port_matrix: + # The temporary port matrix must be set before saving the + # global matrix. + self._log.info("Setting the temporary port matrix...") + set_port_results["set_temp_matrix"].append( + {"idx": port_index, AgentConstants.CMD_RESULT_RET: self.set_temp_matrix(port_index, phy_port)} + ) + result["port_init"] = set_port_results + + # Set power management mode across all ports. + result["set_power_management"] = { + AgentConstants.CMD_RESULT_RET: self.set_pm_method( + POE_PD69200_MSG_DATA_PM1_DYNAMIC, POE_PD69200_MSG_DATA_PM2_PPL, POE_PD69200_MSG_DATA_PM3_NO_COND + ) + } + + if program_port_matrix: + # Persist global port matrix and save system settings. + self._log.notice("Ports will be shutdown while reprogramming " "the active port matrix") + result["program_active_matrix"] = {AgentConstants.CMD_RESULT_RET: self.program_active_matrix()} + self._log.notice("Programming port matrix completed, " "flushing platform settings...") + result["save_system_settings"] = {AgentConstants.CMD_RESULT_RET: self.save_system_settings()} + + return result + + def power_bank_to_str(self, bank: int) -> str: + """Stringify the given power bank as a combination + of one or more PSUs + + Args: + bank (int): Power bank index + + Returns: + str: Power bank as a string + """ + psu = "None" + if bank == 1: + psu = "PSU1, PSU2" + return psu + + def _reset_cpld(self) -> None: + pass + + +def get_poe_platform(): + return As4224_52p() diff --git a/dentos-poe-agent/opt/poeagent/platforms/accton/as4564-26p-r0/poe_platform.py b/dentos-poe-agent/opt/poeagent/platforms/accton/as4564-26p-r0/poe_platform.py deleted file mode 100644 index c175931..0000000 --- a/dentos-poe-agent/opt/poeagent/platforms/accton/as4564-26p-r0/poe_platform.py +++ /dev/null @@ -1,171 +0,0 @@ - -from collections import OrderedDict -from poe_driver_pd69200_def import * -from poe_common import * -from poe_common import print_stderr -from smbus2 import SMBus, i2c_msg - -import os -import sys -import time -import fcntl -import poe_driver_pd69200 as PoeDrv - -def get_poe_platform(): - return PoePlatform_accton_as4564_26p() - -class PoePlatform_accton_as4564_26p(PoeDrv.PoeDriver_microsemi_pd69200): - def __init__(self): - PoeDrv.PoeDriver_microsemi_pd69200.__init__(self) - self.log = PoeLog() - self._total_poe_port = 24 - self._i2c_bus = 1 - self._i2c_addr = 0x3C - self._poe_bus = SMBus(self._i2c_bus) - # Add read 15byte first to cleanup buffer - self.plat_poe_read() - self._4wire_bt = self.support_4wire_bt(3) - # item in matrix: (logic port, phy port a, phy port b) - self._default_matrix = [ - (0, 4, 0xff), (1, 5, 0xff), (2, 6, 0xff), (3, 7, 0xff), - (4, 1, 0xff), (5, 2, 0xff), (6, 3, 0xff), (7, 0, 0xff), - (8, 12, 0xff), (9, 13, 0xff), (10, 14, 0xff), (11, 15, 0xff), - (12, 11, 0xff), (13, 10, 0xff), (14, 9, 0xff), (15, 8, 0xff), - (16, 22, 21), (17, 20, 23), (18, 19, 18), (19, 17, 16), - (20, 30, 29), (21, 28, 31), (22, 27, 26), (23, 25, 24), - (24, 0xff, 0xff), (25, 0xff, 0xff), (26, 0xff, 0xff), (27, 0xff, 0xff), - (28, 0xff, 0xff), (29, 0xff, 0xff), (30, 0xff, 0xff), (31, 0xff, 0xff), - (32, 0xff, 0xff), (33, 0xff, 0xff), (34, 0xff, 0xff), (35, 0xff, 0xff), - (36, 0xff, 0xff), (37, 0xff, 0xff), (38, 0xff, 0xff), (39, 0xff, 0xff), - (40, 0xff, 0xff), (41, 0xff, 0xff), (42, 0xff, 0xff), (43, 0xff, 0xff), - (44, 0xff, 0xff), (45, 0xff, 0xff), (46, 0xff, 0xff), (47, 0xff, 0xff)] - - self._max_shutdown_vol = 0x0249 # 58.5 V - self._min_shutdown_vol = 0x01E0 # 48.0 V - self._guard_band = 0x0A - self._default_power_banks = [(1, 520)] - - def total_poe_port(self): - return self._total_poe_port - - def _bus(self): - if self._poe_bus.fd is None: - self._poe_bus = SMBus(self._poe_bus) - return self._poe_bus - - def _i2c_write(self, bus, msg, delay = 0.03): - write = i2c_msg.write(self._i2c_addr, msg) - bus.i2c_rdwr(write) - time.sleep(delay) - - def _i2c_read(self, bus, size = 15): - read = i2c_msg.read(self._i2c_addr, size) - bus.i2c_rdwr(read) - msg = list(read) - return msg - - def plat_poe_write(self, msg, delay): - return self._i2c_write(self._bus(), msg, delay) - - def plat_poe_read(self): - return self._i2c_read(self._bus()) - - def bus_lock(self): - fcntl.flock(self._bus().fd, fcntl.LOCK_EX) - - def bus_unlock(self): - fcntl.flock(self._bus().fd, fcntl.LOCK_UN) - - def init_poe(self, config_in=None): - ret_item = OrderedDict() - # Clean buffers to reduce retry time - self.plat_poe_read() - - # Fast compare active and temp matrix - if fast_temp_matrix_compare(self._default_matrix, self) == False: - prog_global_matrix = True - else: - prog_global_matrix = False - - # Port result list - set_port_item = dict() - # Default values - set_port_item["set_port_params"] = [] - set_port_item["set_temp_matrix"] = [] - ret_item["set_power_bank"] = [] - ret_item["set_op_mode"] = [] - result_prog_matrix = None - result_save_sys = None - - - # Create default parameter (Disable, low priority) - default_param = dict({ - ENDIS: "disable", - PRIORITY: "low", - }) - - # Set Temporary Matrix and - for temp_matrix_mapping in self._default_matrix: - logic_port = temp_matrix_mapping[0] - phy_porta = temp_matrix_mapping[1] - phy_portb = temp_matrix_mapping[2] - if config_in == None: - port = self.get_poe_port(logic_port) - result = port.set_all_params(default_param) - set_port_item["set_port_params"].append({ - "idx": logic_port, - CMD_RESULT_RET: result - }) - elif config_in == True: - # Preserve current state - pass - - if prog_global_matrix == True: - result = self.set_temp_matrix(logic_port, phy_porta, phy_portb) - set_port_item["set_temp_matrix"].append({ - "idx": logic_port, - CMD_RESULT_RET: result - }) - ret_item["set_port_item"] = set_port_item - - # Set Power Bank - for _power_bank in self._default_power_banks: - (bank, power_limit) = _power_bank - result = self.set_power_bank(bank, power_limit) - ret_item["set_power_bank"].append({ - "setting": _power_bank, - CMD_RESULT_RET: result - }) - - # Set opration mode - for port_id in range(self.total_poe_port()): - if port_id <= 15: - result = self.set_bt_port_operation_mode(port_id, 0x9) - else: - result = self.set_bt_port_operation_mode(port_id, 0x1) - ret_item["set_op_mode"].append({ - "idx": port_id, - CMD_RESULT_RET: result - }) - - - if prog_global_matrix == True: - print_stderr( - "Program active matrix, all ports will shutdown a while") - result_prog_matrix = self.program_active_matrix() - print_stderr( - "Program active matrix completed, save platform settings to chip") - result_save_sys = self.save_system_settings() - ret_item["program_active_matrix"] = { - CMD_RESULT_RET: result_prog_matrix - } - ret_item["save_system_settings"] = { - CMD_RESULT_RET: result_save_sys - } - return ret_item - - def bank_to_psu_str(self, bank): - powerSrc = "None" - if bank == 1: - powerSrc = "PSU1, PSU2" - return powerSrc diff --git a/dentos-poe-agent/opt/poeagent/platforms/accton/as4564_26p_r0.py b/dentos-poe-agent/opt/poeagent/platforms/accton/as4564_26p_r0.py new file mode 100644 index 0000000..d6bf53b --- /dev/null +++ b/dentos-poe-agent/opt/poeagent/platforms/accton/as4564_26p_r0.py @@ -0,0 +1,253 @@ +import time +from collections import OrderedDict + +from agent_constants import AgentConstants +from i2c_driver import I2cDriver +from pd69200.poe_driver import PoeDriver_microsemi_pd69200 +from pd69200.poe_driver_def import ( + POE_PD69200_BT_MSG_DATA_PORT_MODE_TPPL, + POE_PD69200_BT_MSG_DATA_PORT_OP_MODE_4P_30W_2P_30W, + POE_PD69200_BT_MSG_DATA_PORT_OP_MODE_4P_60W_2P_30W, +) +from poe_common import * +from poe_log import PoeLog +from poe_platform import PoePlatform +from smbus2 import SMBus, i2c_msg + + +class As4564_26p(PoePlatform): + # Accton AS4564-26P + def __init__(self) -> None: + self._echo = 0x00 + self._max_shutdown_vol = 0x0249 # 58.5 V + self._min_shutdown_vol = 0x01E0 # 48.0 V + self._guard_band = 0x0A + self._default_power_banks = [(1, 520)] + self._bus_driver = I2cDriver(i2c_bus=0x01, i2c_addr=0x3C) + self._port_count = 24 + PoeDriver_microsemi_pd69200.__init__( + self, + self._bus_driver, + self.port_count(), + self._max_shutdown_vol, + self._min_shutdown_vol, + self._guard_band, + self.power_bank_to_str, + ) + self._log = PoeLog() + + # Clear the I2C buffer. + self._bus_driver.read_message() + + # Mapping: (logical port, phy port a, phy port b) + self._port_matrix = [ + (0, 4, 0xFF), + (1, 5, 0xFF), + (2, 6, 0xFF), + (3, 7, 0xFF), + (4, 1, 0xFF), + (5, 2, 0xFF), + (6, 3, 0xFF), + (7, 0, 0xFF), + (8, 12, 0xFF), + (9, 13, 0xFF), + (10, 14, 0xFF), + (11, 15, 0xFF), + (12, 11, 0xFF), + (13, 10, 0xFF), + (14, 9, 0xFF), + (15, 8, 0xFF), + (16, 22, 21), + (17, 20, 23), + (18, 19, 18), + (19, 17, 16), + (20, 30, 29), + (21, 28, 31), + (22, 27, 26), + (23, 25, 24), + (24, 0xFF, 0xFF), + (25, 0xFF, 0xFF), + (26, 0xFF, 0xFF), + (27, 0xFF, 0xFF), + (28, 0xFF, 0xFF), + (29, 0xFF, 0xFF), + (30, 0xFF, 0xFF), + (31, 0xFF, 0xFF), + (32, 0xFF, 0xFF), + (33, 0xFF, 0xFF), + (34, 0xFF, 0xFF), + (35, 0xFF, 0xFF), + (36, 0xFF, 0xFF), + (37, 0xFF, 0xFF), + (38, 0xFF, 0xFF), + (39, 0xFF, 0xFF), + (40, 0xFF, 0xFF), + (41, 0xFF, 0xFF), + (42, 0xFF, 0xFF), + (43, 0xFF, 0xFF), + (44, 0xFF, 0xFF), + (45, 0xFF, 0xFF), + (46, 0xFF, 0xFF), + (47, 0xFF, 0xFF), + ] + + # Minimum firmware major for BT support is 3.x + self.supports_bt_protocol(3) + + # Map the default port power limit (in W) for the ECAs (class 6) + # and for cameras (class 3 and 4). + self._default_power_limits = {3: 14, 4: 14, 6: 45} + + + def port_count(self) -> int: + """Get the total PoE port count + + Returns: + int: Port count + """ + return self._port_count + + + @property + def default_power_limits(self) -> dict[int, int]: + """Get the port ranges default power limits + + Returns: + dict[tuple[int, int], int]: Default limits as a dictionary + """ + return self._default_power_limits + + def init_poe(self, skip_port_init: bool) -> dict: + """Initialize the PoE ports, power bank config and + each port operation mode. If skip_port_init is true, + will not set the default port parameters + + The global port matrix will be reprogrammed, only if + the actual matrix is different than the active port matrix. + + Args: + skip_port_init (bool): Skip port init flag + + Returns: + dict: Result dictionary. Contains the result for each + individual operation + """ + # Clear the I2C buffer. + self._bus_driver.read_message() + + # Default port params to initialize with. + port_default_params = {ENDIS: AgentConstants.ENABLE, PRIORITY: "low"} + + # Determine if we need to reprogram the port matrix. + program_port_matrix = False + if not is_active_port_matrix_different(self._port_matrix, self.get_active_matrix): + program_port_matrix = True + + # Configure the power bank power and voltage limits with the actual + # values. + result = OrderedDict() + result["power_bank"] = [] + for power_bank in self._default_power_banks: + (bank_index, power_limit) = power_bank + result["power_bank"].append( + { + "bank_details": power_bank, + AgentConstants.CMD_RESULT_RET: self.set_power_bank(bank_index, power_limit), + } + ) + # Confirm that the power bank was successfully configured. + power_bank_details = self.get_power_bank(bank_index) + result["power_bank"][-1][AgentConstants.CMD_RESULT_RET] = ( + 0 if power_bank_details[POWER_LIMIT] == power_limit else 1 + ) + # Prevent enabling or changing any port parameter if the power bank + # configuration failed. + if power_bank_details[POWER_LIMIT] != power_limit: + return result + + set_port_results = {} + set_port_results["set_port_params"] = [] + if program_port_matrix: + set_port_results["set_temp_matrix"] = [] + if skip_port_init: + self._log.dbg("Skipping port initialization") + for ports in self._port_matrix: + port_index, phy_port_a, phy_port_b = ports + if not skip_port_init: + port = self.get_poe_port(port_index) + set_port_results["set_port_params"].append( + {"idx": port_index, AgentConstants.CMD_RESULT_RET: port.set_all_params(port_default_params)} + ) + if program_port_matrix: + # The temporary port matrix must be set before saving the + # global matrix. + self._log.info("Setting the temporary port matrix...") + set_port_results["set_temp_matrix"].append( + { + "idx": port_index, + AgentConstants.CMD_RESULT_RET: self.set_temp_matrix(port_index, phy_port_a, phy_port_b), + } + ) + result["port_init"] = set_port_results + + # Set port operation mode (i.e., limit first 16 ports to 30W/at, and + # the rest to 60W/bt). + # Configure the port power management to use the port TPPL for + # computing the available power. + result["port_operation_mode"] = [] + for port_id in range(self.port_count()): + result["port_operation_mode"].append( + { + "idx": port_id, + AgentConstants.CMD_RESULT_RET: self.bt_set_port_params( + port_id, + POE_PD69200_BT_MSG_DATA_PORT_MODE_TPPL, + ( + POE_PD69200_BT_MSG_DATA_PORT_OP_MODE_4P_60W_2P_30W + if port_id > 15 + else POE_PD69200_BT_MSG_DATA_PORT_OP_MODE_4P_30W_2P_30W + ), + ), + } + ) + + if program_port_matrix: + # Persist global port matrix and save system settings. + self._log.notice("Ports will be shutdown while reprogramming " "the active port matrix") + result["program_active_matrix"] = {AgentConstants.CMD_RESULT_RET: self.program_active_matrix()} + self._log.notice("Programming port matrix completed, " "flushing platform settings...") + result["save_system_settings"] = {AgentConstants.CMD_RESULT_RET: self.save_system_settings()} + + return result + + def power_bank_to_str(self, bank: int) -> str: + """Stringify the given power bank as a combination + of one or more PSUs + + Args: + bank (int): Power bank index + + Returns: + str: Power bank as a string + """ + psu = "None" + if bank == 1: + psu = "PSU1" + return psu + + def _reset_cpld(self) -> None: + self._log.info("Resetting the PoE chipset via CPLD") + + bus = SMBus(0x00) + for msg in ( + i2c_msg.write(0x40, [0xE0, 0x01]), + i2c_msg.write(0x40, [0x11, 0xFB]), + i2c_msg.write(0x40, [0x11, 0xFF]), + ): + bus.i2c_rdwr(msg) + + time.sleep(self._reset_poe_chip_delay) + + +def get_poe_platform(): + return As4564_26p() diff --git a/dentos-poe-agent/opt/poeagent/platforms/delta/tn48m-poe-r0/poe_platform.py b/dentos-poe-agent/opt/poeagent/platforms/delta/tn48m-poe-r0/poe_platform.py deleted file mode 100755 index 761c6ea..0000000 --- a/dentos-poe-agent/opt/poeagent/platforms/delta/tn48m-poe-r0/poe_platform.py +++ /dev/null @@ -1,199 +0,0 @@ -''' -Copyright 2021 Delta Electronic Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -''' -from collections import OrderedDict -from poe_driver_pd69200_def import * -from poe_common import * -from poe_common import print_stderr -from smbus2 import SMBus, i2c_msg - -import os -import sys -import time -import fcntl -import poe_driver_pd69200 as PoeDrv - -def get_poe_platform(): - return PoePlatform_delta_tn48m_poe() - -class PoePlatform_delta_tn48m_poe(PoeDrv.PoeDriver_microsemi_pd69200): - def __init__(self): - PoeDrv.PoeDriver_microsemi_pd69200.__init__(self) - self.log = PoeLog() - self._total_poe_port = 48 - self._i2c_bus = 1 - self._i2c_addr = 0x3C - self._poe_bus = SMBus(self._i2c_bus) - - # Add read 15byte first to cleanup buffer - self.plat_poe_read() - - # Time between commands (from hw spec): 30ms - self._msg_delay = 0.03 - # Wait time after saving system setting: 50ms - self._save_sys_delay = 0.05 - - # item in matrix: (logic port, phy port) - self._default_matrix = [ - ( 0, 2), ( 1, 3), ( 2, 0), ( 3, 1), ( 4, 5), ( 5, 4), ( 6, 7), ( 7, 6), - ( 8, 10), ( 9, 11), (10, 8), (11, 9), (12, 13), (13, 12), (14, 15), (15, 14), - (16, 21), (17, 20), (18, 23), (19, 22), (20, 18), (21, 19), (22, 16), (23, 17), - (24, 29), (25, 28), (26, 31), (27, 30), (28, 26), (29, 27), (30, 24), (31, 25), - (32, 37), (33, 36), (34, 39), (35, 38), (36, 34), (37, 35), (38, 32), (39, 33), - (40, 45), (41, 44), (42, 47), (43, 46), (44, 42), (45, 43), (46, 40), (47, 41)] - - ''' - +-----------------------------------------------+ - | Power Banks | PSU1 PG | PSU2 PG | Power Limit | - |-----------------------------------------------| - | Bank 13 | NO | YES | 680 W | - |-----------------------------------------------| - | Bank 14 | YES | NO | 680 W | - |-----------------------------------------------| - | Bank 15 | YES | YES | 1500 W | - +-----------------------------------------------+ - item in power bank: (bank, power limit) - ''' - self._default_power_banks = [(13, 680), (14, 680), (15, 1500)] - self._max_shutdown_vol = 0x0239 # 56.9 V - self._min_shutdown_vol = 0x01F5 # 50.1 V - self._guard_band = 0x01 - self._port_power_limit = 0x7530 # 30000 mW - - def total_poe_port(self): - return self._total_poe_port - - def _bus(self): - if self._poe_bus.fd is None: - self._poe_bus = SMBus(self._poe_bus) - return self._poe_bus - - def _i2c_write(self, bus, msg, delay = 0.03): - write = i2c_msg.write(self._i2c_addr, msg) - bus.i2c_rdwr(write) - time.sleep(delay) - - def _i2c_read(self, bus, size = 15): - read = i2c_msg.read(self._i2c_addr, size) - bus.i2c_rdwr(read) - msg = list(read) - return msg - - def plat_poe_write(self, msg, delay): - return self._i2c_write(self._bus(), msg, delay) - - def plat_poe_read(self): - return self._i2c_read(self._bus()) - - def bus_lock(self): - fcntl.flock(self._bus().fd, fcntl.LOCK_EX) - - def bus_unlock(self): - fcntl.flock(self._bus().fd, fcntl.LOCK_UN) - - def init_poe(self, config_in=None): - ret_item = OrderedDict() - # Clean buffers to reduce retry time - self.plat_poe_read() - - # Fast compare active and temp matrix - if fast_temp_matrix_compare(self._default_matrix, self) == False: - prog_global_matrix = True - else: - prog_global_matrix = False - - # Port result list - set_port_item = dict() - # Default values - set_port_item["set_port_params"] = [] - set_port_item["set_temp_matrix"] = [] - ret_item["set_power_bank"] = [] - ret_item["set_op_mode"] = [] - result_prog_matrix = None - result_save_sys = None - - # Create default parameter (Disable, low priority, Apply default power Limit) - default_param = dict({ - ENDIS: "disable", - PRIORITY: "low", - POWER_LIMIT: self._port_power_limit - }) - - # Set Temporary Matrix and port default - for temp_matrix_mapping in self._default_matrix: - logic_port = temp_matrix_mapping[0] - phy_porta = temp_matrix_mapping[1] - if config_in == None: - port = self.get_poe_port(logic_port) - result = port.set_all_params(default_param) - set_port_item["set_port_params"].append({ - "idx": logic_port, - CMD_RESULT_RET: result - }) - elif config_in == True: - # Preserve current state - pass - - if prog_global_matrix == True: - result = self.set_temp_matrix(logic_port, phy_porta) - set_port_item["set_temp_matrix"].append({ - "idx": logic_port, - CMD_RESULT_RET: result - }) - ret_item["set_port_item"] = set_port_item - - # Set Power Bank - for _power_bank in self._default_power_banks: - (bank, power_limit) = _power_bank - result = self.set_power_bank(bank, power_limit) - ret_item["set_power_bank"].append({ - "setting" : _power_bank, - CMD_RESULT_RET: result - }) - - # Set POE Power Management Method - result = self.set_pm_method(POE_PD69200_MSG_DATA_PM1_DYNAMIC, - POE_PD69200_MSG_DATA_PM2_PPL, - POE_PD69200_MSG_DATA_PM3_NO_COND) - ret_item["set_pm_method"] = { - CMD_RESULT_RET: result - } - - if prog_global_matrix == True: - print_stderr( - "Program active matrix, all ports will shutdown a while") - result_prog_matrix = self.program_active_matrix() - print_stderr( - "Program active matrix completed, save platform settings to chip") - result_save_sys = self.save_system_settings() - ret_item["program_active_matrix"] = { - CMD_RESULT_RET: result_prog_matrix - } - ret_item["save_system_settings"] = { - CMD_RESULT_RET: result_save_sys - } - # print_stderr("init_poe result: {0}".format(str(ret_item))) - - return ret_item - - def bank_to_psu_str(self, bank): - powerSrc = "None" - if bank == 13: - powerSrc = "PSU2" - elif bank == 14: - powerSrc = "PSU1" - elif bank == 15: - powerSrc = "PSU1, PSU2" - return powerSrc diff --git a/dentos-poe-agent/opt/poeagent/platforms/delta/tn48m_poe_r0.py b/dentos-poe-agent/opt/poeagent/platforms/delta/tn48m_poe_r0.py new file mode 100755 index 0000000..380b7c8 --- /dev/null +++ b/dentos-poe-agent/opt/poeagent/platforms/delta/tn48m_poe_r0.py @@ -0,0 +1,257 @@ +''' +Copyright 2021 Delta Electronic Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +''' + +from collections import OrderedDict + +from agent_constants import AgentConstants +from drivers.i2c_driver import I2cDriver +from drivers.pd69200.poe_driver import PoeDriver_microsemi_pd69200 +from drivers.pd69200.poe_driver_def import ( + POE_PD69200_MSG_DATA_PM1_USER_DEFINED, + POE_PD69200_MSG_DATA_PM2_PPL, + POE_PD69200_MSG_DATA_PM3_NO_COND, + POE_PD69200_MSG_DATA_PROTOCOL_ATAF, + POE_PD69200_MSG_DATA_SUM_AS_TPPL_STATIC, + POE_PD69200_MSG_SUB2_ALL_CHANNEL, +) +from poe_common import * +from poe_log import PoeLog +from poe_platform import PoePlatform + + +class Tn48mPoe(PoePlatform): + # Delta TN48M-P + def __init__(self): + self._max_shutdown_vol = 0x0239 # 56.9 V + self._min_shutdown_vol = 0x01F5 # 50.1 V + self._guard_band = 0x01 + """ + +-----------------------------------------------+ + | Power Banks | PSU1 PG | PSU2 PG | Power Limit | + |-----------------------------------------------| + | Bank 13 | NO | YES | 680 W | + |-----------------------------------------------| + | Bank 14 | YES | NO | 680 W | + |-----------------------------------------------| + | Bank 15 | YES | YES | 1500 W | + +-----------------------------------------------+ + """ + self._default_power_banks = [(13, 680), (14, 680), (15, 1500)] + self._default_port_power_limit = 0x7530 # 30000 mW + self._bus_driver = I2cDriver(i2c_bus=0x01, i2c_addr=0x3C) + self._port_count = 48 + PoeDriver_microsemi_pd69200.__init__( + self, + self._bus_driver, + self.port_count(), + self._max_shutdown_vol, + self._min_shutdown_vol, + self._guard_band, + self.power_bank_to_str, + ) + self._log = PoeLog() + + # Clear the I2C buffer. + self._bus_driver.read_message() + + # Mapping: (logical port, phy port) + self._port_matrix = [ + (0, 2), + (1, 3), + (2, 0), + (3, 1), + (4, 5), + (5, 4), + (6, 7), + (7, 6), + (8, 10), + (9, 11), + (10, 8), + (11, 9), + (12, 13), + (13, 12), + (14, 15), + (15, 14), + (16, 21), + (17, 20), + (18, 23), + (19, 22), + (20, 18), + (21, 19), + (22, 16), + (23, 17), + (24, 29), + (25, 28), + (26, 31), + (27, 30), + (28, 26), + (29, 27), + (30, 24), + (31, 25), + (32, 37), + (33, 36), + (34, 39), + (35, 38), + (36, 34), + (37, 35), + (38, 32), + (39, 33), + (40, 45), + (41, 44), + (42, 47), + (43, 46), + (44, 42), + (45, 43), + (46, 40), + (47, 41), + ] + + # Ignore default power limit allocation. + self._default_power_limits = {} + + + def port_count(self) -> int: + """Get the total PoE port count + + Returns: + int: Port count + """ + return self._port_count + + @property + def default_power_limits(self) -> dict[tuple[int, int], int]: + """Get the port ranges default power limits + + Returns: + dict[tuple[int, int], int]: Default limits as a dictionary + """ + return self._default_power_limits + + def init_poe(self, skip_port_init: bool) -> dict: + """Initialize the PoE ports, power bank config and + each port operation mode. If skip_port_init is true, + will not set the default port parameters + + The global port matrix will be reprogrammed, only if + the actual matrix is different than the active port matrix. + + Args: + skip_port_init (bool): Skip port init flag + + Returns: + dict: Result dictionary. Contains the result for each + individual operation + """ + # Clear the I2C buffer. + self._bus_driver.read_message() + + # Default port params to initialize with. + port_default_params = { + ENDIS: AgentConstants.ENABLE, + PRIORITY: "low", + POWER_LIMIT: self._default_port_power_limit, + } + + # Determine if we need to reprogram the port matrix. + program_port_matrix = False + if not is_active_port_matrix_different(self._port_matrix, self.get_active_matrix): + program_port_matrix = True + + # Configure the power bank power and voltage limits with the actual + # values. + result = OrderedDict() + result["power_bank"] = [] + for power_bank in self._default_power_banks: + (bank_index, power_limit) = power_bank + result["power_bank"].append( + { + "bank_details": power_bank, + AgentConstants.CMD_RESULT_RET: self.set_power_bank(bank_index, power_limit), + } + ) + # Confirm that the power bank was successfully configured. + power_bank_details = self.get_power_bank(bank_index) + result["power_bank"][-1][AgentConstants.CMD_RESULT_RET] = ( + 0 if power_bank_details[POWER_LIMIT] == power_limit else 1 + ) + # Prevent enabling or changing any port parameter if the power bank + # configuration failed. + if power_bank_details[POWER_LIMIT] != power_limit: + return result + + set_port_results = {} + set_port_results["set_port_params"] = [] + if program_port_matrix: + set_port_results["set_temp_matrix"] = [] + if skip_port_init: + self._log.dbg("Skipping port initialization") + for ports in self._port_matrix: + port_index, phy_port = ports + if not skip_port_init: + port = self.get_poe_port(port_index) + set_port_results["set_port_params"].append( + {"idx": port_index, AgentConstants.CMD_RESULT_RET: port.set_all_params(port_default_params)} + ) + if program_port_matrix: + # The temporary port matrix must be set before saving the + # global matrix. + self._log.info("Setting the temporary port matrix...") + set_port_results["set_temp_matrix"].append( + {"idx": port_index, AgentConstants.CMD_RESULT_RET: self.set_temp_matrix(port_index, phy_port)} + ) + result["port_init"] = set_port_results + + # Set power management mode across all ports. + result["set_power_management"] = { + AgentConstants.CMD_RESULT_RET: self.set_pm_method( + POE_PD69200_MSG_DATA_PM1_USER_DEFINED, POE_PD69200_MSG_DATA_PM2_PPL, POE_PD69200_MSG_DATA_PM3_NO_COND + ) + } + + if program_port_matrix: + # Persist global port matrix and save system settings. + self._log.notice("Ports will be shutdown while reprogramming " "the active port matrix") + result["program_active_matrix"] = {AgentConstants.CMD_RESULT_RET: self.program_active_matrix()} + self._log.notice("Programming port matrix completed, " "flushing platform settings...") + result["save_system_settings"] = {AgentConstants.CMD_RESULT_RET: self.save_system_settings()} + + return result + + def power_bank_to_str(self, bank: int) -> str: + """Stringify the given power bank as a combination + of one or more PSUs + + Args: + bank (int): Power bank index + + Returns: + str: Power bank as a string + """ + psu = "None" + if bank == 13: + psu = "PSU2" + elif bank == 14: + psu = "PSU1" + elif bank == 15: + psu = "PSU1, PSU2" + return psu + + def _reset_cpld(self) -> None: + pass + + +def get_poe_platform(): + return Tn48mPoe() diff --git a/lldp-poe/.clang-format b/lldp-poe/.clang-format new file mode 100644 index 0000000..49eb540 --- /dev/null +++ b/lldp-poe/.clang-format @@ -0,0 +1,12 @@ +# clang-format configuration file. +# +# For more information, see: +# https://clang.llvm.org/docs/ClangFormat.html +# https://clang.llvm.org/docs/ClangFormatStyleOptions.html +BasedOnStyle: LLVM +IndentWidth: 4 +BreakBeforeBraces: Linux +AllowShortIfStatementsOnASingleLine: false +IndentCaseLabels: false +SpaceAfterCStyleCast: true +SpaceBeforeSquareBrackets: false diff --git a/lldp-poe/LICENCE b/lldp-poe/LICENCE new file mode 100644 index 0000000..cd6697e --- /dev/null +++ b/lldp-poe/LICENCE @@ -0,0 +1,191 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2021-2022 Amazon, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/lldp-poe/Makefile b/lldp-poe/Makefile new file mode 100644 index 0000000..d85d03d --- /dev/null +++ b/lldp-poe/Makefile @@ -0,0 +1,40 @@ +TARGET_EXEC ?= lldp-poed +CC := aarch64-linux-gnu-gcc +BUILD_DIR ?= ./build +SRC_DIRS ?= ./src ./include ./lib + +SRCS := $(shell find $(SRC_DIRS) -name *.cpp -or -name *.c -or -name *.s) +OBJS := $(SRCS:%=$(BUILD_DIR)/%.o) +DEPS := $(OBJS:.o=.d) + +INC_DIRS := ./ ./lib /usr/local/include +INC_FLAGS := $(addprefix -I,$(INC_DIRS)) + +CPPFLAGS ?= -pthread $(INC_FLAGS) -MMD -MP -Wall -Werror + +# TODO: Use vendor submodules instead of relying on local setup. +LIBDIRS := /lib /usr/lib /usr/local/lib +LIBPATH := $(addprefix -L,$(LIBDIRS)) +LDFLAGS := -llldpctl + +$(BUILD_DIR)/$(TARGET_EXEC): $(OBJS) + $(CC) $(OBJS) -o $@ $(LIBPATH) $(LDFLAGS) -pthread -Wall -Werror + +# Assembly files +$(BUILD_DIR)/%.s.o: %.s + $(MKDIR_P) $(dir $@) + $(AS) $(ASFLAGS) -c $< -o $@ + +# C source files +$(BUILD_DIR)/%.c.o: %.c + $(MKDIR_P) $(dir $@) + $(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@ + +.PHONY: clean + +clean: + $(RM) -r $(BUILD_DIR) + +-include $(DEPS) + +MKDIR_P ?= mkdir -p diff --git a/lldp-poe/README b/lldp-poe/README new file mode 100644 index 0000000..b5ed76b --- /dev/null +++ b/lldp-poe/README @@ -0,0 +1,11 @@ +# Port state machine and LLDP neighbor updates processing daemon - `lldp-poed` + +This agent is capable of holding each individual `swp` state for further +dispatching commands to `poed` for setting the port power limit and +querying dot3 port details. To better understand the flow transitions +from `port_state_machine.c`, please refer to the following state machine diagram: + +![State machine diagram](./res/lldp_port_state_machine.png) + +The LLDP neighbor updates are send/received using `liblldpctl` in the `lldp_event_handler.c`. +Incoming updates are sent to the state machine to be processed further. diff --git a/lldp-poe/bin/lldp-poed-config b/lldp-poe/bin/lldp-poed-config new file mode 100755 index 0000000..8a9d1b4 --- /dev/null +++ b/lldp-poe/bin/lldp-poed-config @@ -0,0 +1,8 @@ +#!/bin/sh + +if ( test -f /etc/onl/platform && grep -q "arm64-accton-as4564-26p" /etc/onl/platform ); then + touch /var/run/lldp-poed-enable +else + rm -f /var/run/lldp-poed-enable + echo "Disabling lldp-poed file flag since platform is not arm64-accton-as4564-26p-r0" +fi diff --git a/lldp-poe/include/common.h b/lldp-poe/include/common.h new file mode 100644 index 0000000..86f2956 --- /dev/null +++ b/lldp-poe/include/common.h @@ -0,0 +1,43 @@ +/** + * Copyright Amazon Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +#ifndef _LLDP_POE_COMMON_H_ +#define _LLDP_POE_COMMON_H_ + +#include +#include +/** + * Get the size of any C array. + */ +#define COUNT_OF(x) (sizeof(x) / sizeof(0 [x])) + +/** + * Iterate in range and iterate for each array element. + */ +#define FOR_I_IN(from, to) for (size_t i = (from); i <= (to); i++) +#define FOR_EACH(item, arr, len) \ + for ((item) = &((arr)[0]); (item) < &((arr)[len]); (item)++) + +#define METRICS_FIFO_PATH "/run/poe_helper/poe_metrics_fifo" + +#define READ_FIFO_PATH "/run/lldp_poed_read" +#define WRITE_FIFO_PATH "/run/lldp_poed_write" + +extern volatile sig_atomic_t thread_exit; + +int publish_metrics(const char *metric_name, int metric_value, int port_id); + +#endif /* _LLDP_POE_COMMON_H_ */ diff --git a/lldp-poe/include/lldp_event_handler.h b/lldp-poe/include/lldp_event_handler.h new file mode 100644 index 0000000..c5c047a --- /dev/null +++ b/lldp-poe/include/lldp_event_handler.h @@ -0,0 +1,32 @@ +/** + * Copyright Amazon Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +#ifndef _LLDP_POE_LLDP_EVENT_HANDLER_H_ +#define _LLDP_POE_LLDP_EVENT_HANDLER_H_ + +#include + +#include "port_state_machine.h" + +int send_mdi_pse_advertisement(const char *, + const struct port_dot3_power_settings *, + time_t *); + +bool is_neighbor_already_reconciled(const char *); + +void *handle_lldp_events(); + +#endif /* _LLDP_POE_LLDP_EVENT_HANDLER_H_ */ diff --git a/lldp-poe/include/lldp_poed_err.h b/lldp-poe/include/lldp_poed_err.h new file mode 100644 index 0000000..9920731 --- /dev/null +++ b/lldp-poe/include/lldp_poed_err.h @@ -0,0 +1,77 @@ +/** + * Copyright Amazon Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + + +#ifndef _LLDP_POE_LLDP_POED_ERR_H_ +#define _LLDP_POE_LLDP_POED_ERR_H_ + +/** + * lldp_poed_err - Function status codes + * @LLDP_POED_ERR_OK: successful + * @LLDP_POED_ERR_INVALID_PARAM: invalid argument supplied to the function + * @LLDP_POED_ERR_GETPORTDETAILS_FAILED: poed query for get_port_details failed + * @LLDP_POED_ERR_SEND_REQUEST_FAILED: failed to send request to poed + * @LLDP_POED_ERR_UNEXPECTED_DELETED_NEIGHBOR: LLDP neighbor got deleted in the + * meantime (unexpectedly) + * @LLDP_POED_ERR_DELETED_NEIGHBOR: LLDP neighbor got deleted or aged out. Port + * will go back to L1 neg complete + * @LLDP_POED_INVALID_PAYLOAD: malformed payload + * @LLDP_POED_ERR_PORT_GOT_DISABLED: PoE port got disabled in the meantime + * @LLDP_POED_ERR_PORT_ERROR: PoE port is in error state + * @LLDP_POED_ERR_PORT_ERROR: PoE port got the default power limit + * @LLDP_POED_ERR_8023AT_FIELDS_MISSING: 802.3at fields were not supplied + * @LLDP_POED_ERR_INVALID_8023AT_FIELDS: 802.3at fields are invalid + * @LLDP_POED_8023BT_FIELDS_ERROR: 802.3bt fields are invalid + * @LLDP_POED_ERR_SERIALIZE_ERROR: failed to serialize the payload + * @LLDP_POED_ERR_PARSE_ERROR: failed to parse the payload + * @LLDP_POED_ERR_LLDP_PROCESSING_DISABLED: LLDP is disabled for the port + * @LLDP_POED_ERR_INACTIVE_DATALINK: there is no active data link for the port + * @LLDP_POED_ERR_UNEXPECTED_DEVICE_TYPE: invalid power device detected + * @LLDP_POED_ERR_FAILED_TO_SET_L2_TPPL: poed failed to apply the port TPPL + * @LLDP_POED_ERR_DUALSIG_PD_NOT_SUPPORTED: dual-signature PDs are not supported + * @LLDP_POED_ERR_DOT3_POWER_ATOM_FAILED: failed to set the lldpctl atom + * @LLDP_POED_ERR_MED_POWER_ATOM_FAILED: failed to set the lldpctl MED atom + * @LLDP_POED_ERR_PIPE_ISSUE: named pipe error + * @LLDP_POED_ERR_INTERNAL_ERROR: generic internal error + */ +enum lldp_poed_err { + LLDP_POED_ERR_OK = 0, + LLDP_POED_ERR_INVALID_PARAM, + LLDP_POED_ERR_GETPORTDETAILS_FAILED, + LLDP_POED_ERR_SEND_REQUEST_FAILED, + LLDP_POED_ERR_UNEXPECTED_DELETED_NEIGHBOR, + LLDP_POED_ERR_DELETED_NEIGHBOR, + LLDP_POED_ERR_INVALID_PAYLOAD, + LLDP_POED_ERR_PORT_GOT_DISABLED, + LLDP_POED_ERR_PORT_ERROR, + LLDP_POED_ERR_PORT_DEFAULT_POWER, + LLDP_POED_ERR_8023AT_FIELDS_MISSING, + LLDP_POED_ERR_INVALID_8023AT_FIELDS, + LLDP_POED_ERR_INVALID_8023BT_FIELDS, + LLDP_POED_ERR_SERIALIZE_ERROR, + LLDP_POED_ERR_PARSE_ERROR, + LLDP_POED_ERR_LLDP_PROCESSING_DISABLED, + LLDP_POED_ERR_INACTIVE_DATALINK, + LLDP_POED_ERR_UNEXPECTED_DEVICE_TYPE, + LLDP_POED_ERR_FAILED_TO_SET_L2_TPPL, + LLDP_POED_ERR_DUALSIG_PD_NOT_SUPPORTED, + LLDP_POED_ERR_DOT3_POWER_ATOM_FAILED, + LLDP_POED_ERR_MED_POWER_ATOM_FAILED, + LLDP_POED_ERR_PIPE_ISSUE, + LLDP_POED_ERR_INTERNAL_ERROR, +}; + +#endif /* _LLDP_POE_LLDP_POED_ERR_H_ */ diff --git a/lldp-poe/include/logger.h b/lldp-poe/include/logger.h new file mode 100644 index 0000000..c9e9a08 --- /dev/null +++ b/lldp-poe/include/logger.h @@ -0,0 +1,83 @@ +/** + * Copyright Amazon Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +#ifndef _LLDP_POE_LOGGER_H_ +#define _LLDP_POE_LOGGER_H_ + +#include +#include +#include + +#if defined __GNUC__ && __GNUC__ >= 2 +#define __func__ __FUNCTION__ +#else /* defined __GNUC__ && __GNUC__ >= 2 */ +#error Missing platform support for determining the function name. +#endif /* defined __GNUC__ && __GNUC__ >= 2 */ + +#ifndef LOG_LINE_CHARS +#define LOG_LINE_CHARS 256 +#endif /* LOG_LINE_CHARS */ + +#define POE_CRIT(format, ...) \ + { \ + char buffer[LOG_LINE_CHARS] = {}; \ + snprintf(buffer, LOG_LINE_CHARS, format, ##__VA_ARGS__); \ + syslog(LOG_CRIT, "(%s) %s", __func__, buffer); \ + } + +#define POE_ERR(format, ...) \ + { \ + char buffer[LOG_LINE_CHARS] = {}; \ + snprintf(buffer, LOG_LINE_CHARS, format, ##__VA_ARGS__); \ + syslog(LOG_ERR, "(%s) %s", __func__, buffer); \ + } + +#define POE_WARN(format, ...) \ + { \ + char buffer[LOG_LINE_CHARS] = {}; \ + snprintf(buffer, LOG_LINE_CHARS, format, ##__VA_ARGS__); \ + syslog(LOG_WARNING, "(%s) %s", __func__, buffer); \ + } + +#define POE_NOTICE(format, ...) \ + { \ + char buffer[LOG_LINE_CHARS] = {}; \ + snprintf(buffer, LOG_LINE_CHARS, format, ##__VA_ARGS__); \ + syslog(LOG_NOTICE, "(%s) %s", __func__, buffer); \ + } + +#define POE_INFO(format, ...) \ + { \ + char buffer[LOG_LINE_CHARS] = {}; \ + snprintf(buffer, LOG_LINE_CHARS, format, ##__VA_ARGS__); \ + syslog(LOG_INFO, "(%s) %s", __func__, buffer); \ + } + +#define POE_DEBUG(format, ...) \ + { \ + char buffer[LOG_LINE_CHARS] = {}; \ + snprintf(buffer, LOG_LINE_CHARS, format, ##__VA_ARGS__); \ + syslog(LOG_DEBUG, "(%s) %s", __func__, buffer); \ + } + +#define POE_LOG(severity, format, ...) \ + { \ + char buffer[LOG_LINE_CHARS] = {}; \ + snprintf(buffer, LOG_LINE_CHARS, format, ##__VA_ARGS__); \ + syslog(severity, "(%s) %s", __func__, buffer); \ + } + +#endif /* _LLDP_POE_LOGGER_H_ */ diff --git a/lldp-poe/include/netlink_event_handler.h b/lldp-poe/include/netlink_event_handler.h new file mode 100644 index 0000000..56ba88a --- /dev/null +++ b/lldp-poe/include/netlink_event_handler.h @@ -0,0 +1,46 @@ +/** + * Copyright Amazon Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +#ifndef _LLDP_POE_NETLINK_EVENT_HANDLER_H_ +#define _LLDP_POE_NETLINK_EVENT_HANDLER_H_ + +#include +#include + +#include +#include +#include + +/** + * Maximum number of characters for the prefix size (e.g. "eth" is 4 characters, + * including the null terminator). + */ +#define IFNAME_PREFIX_SIZE 4U + +/** + * struct port_range - Holds a port range together with the port prefix. + */ +struct port_range { + char ifname_prefix[IFNAME_PREFIX_SIZE]; + size_t start_index; + size_t end_index; +}; + +int netlink_scan_all_ports(struct port_range *); + +void *handle_netlink_events(); + +#endif /* _LLDP_POE_NETLINK_EVENT_HANDLER_H_ */ diff --git a/lldp-poe/include/payload.h b/lldp-poe/include/payload.h new file mode 100644 index 0000000..b1c6fc1 --- /dev/null +++ b/lldp-poe/include/payload.h @@ -0,0 +1,106 @@ +/** + * Copyright Amazon Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +#ifndef _LLDP_POE_PAYLOAD_H_ +#define _LLDP_POE_PAYLOAD_H_ + +/** + * Payload abstraction layer used for translating a port state + * machine message to the underlying messaging protocol. The payload represents + * a tree-like structure containing communication data exchanged between + * poed and lldp-poed. + * Ideally, we should have a common API for translating to and from + * the underlying protocol and the init API should set the desired protocol + * to be used by the translate API. + * This is usually achieved via dependency injection and polymorphism by + * having several different implementations that use the same + * interface/contract, but since we are in C and we have only one protocol + * at the moment, we'll couple to just one implementation treating JSON-RPC. + */ + +#include +#include + +/** + * enum payload_value_type - Possible payload value type + * @PAYLOAD_VALUE_BOOLEAN: bool data type (true/false) + * @PAYLOAD_VALUE_NUMBER: only ints + * @PAYLOAD_VALUE_STRING: string (char[]) + * @PAYLOAD_VALUE_ARRAY: array of payload objects + * @PAYLOAD_VALUE_OBJECT: nested object + * @PAYLOAD_VALUE_NULL: null + * @PAYLOAD_VALUE_MAX: total number of types + */ +enum payload_value_type { + PAYLOAD_VALUE_BOOLEAN, + PAYLOAD_VALUE_NUMBER, + PAYLOAD_VALUE_STRING, + PAYLOAD_VALUE_ARRAY, + PAYLOAD_VALUE_OBJECT, + PAYLOAD_VALUE_NULL, + PAYLOAD_VALUE_MAX, +}; + +/** + * Payload boundaries. + */ +#define PAYLOAD_NAME_MAX_SIZE 32U +#define PAYLOAD_VAL_STR_MAX_SIZE 32U + +/** + * Currently supported data types are: int, bool and string. + */ +union object_value { + int val_int; + bool val_bool; + char val_str[PAYLOAD_VAL_STR_MAX_SIZE]; +}; + +/** + * struct poed_payload - Payload model covering all possible use-cases + * for making requests and receiving replies from poed + * @name: key name. Array children names are ignored + * @type: the type of the object (see @payload_value_type) + * @value: object value (this field must not be used with ARRAY, OBJECT or + * NULL) + * @child_count: number of child objects (valid only for ARRAY and OBJECT) + * @children: child nodes (each node value can be accessed through @value) + * + * Note that this container is agnostic of the data format used for + * serializing/deserializing the final message. + */ +struct poed_payload { + char name[PAYLOAD_NAME_MAX_SIZE]; + enum payload_value_type type; + union object_value value; + size_t child_count; + struct poed_payload *children; +}; + +int find_payload_by_key(const struct poed_payload *, const char *, + const struct poed_payload **); + +int payload_to_json_rpc(const struct poed_payload *, const char *, ssize_t *, + char *, size_t); + +int json_rpc_to_payload(const char *, size_t, const ssize_t, + struct poed_payload *); + +void log_payload(const struct poed_payload *); + +void free_payload(struct poed_payload *); + +#endif /* _LLDP_POE_PAYLOAD_H_ */ diff --git a/lldp-poe/include/port_state_machine.h b/lldp-poe/include/port_state_machine.h new file mode 100644 index 0000000..8943d31 --- /dev/null +++ b/lldp-poe/include/port_state_machine.h @@ -0,0 +1,109 @@ +/** + * Copyright Amazon Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +#ifndef _LLDP_POE_PORT_STATE_MACHINE_H_ +#define _LLDP_POE_PORT_STATE_MACHINE_H_ + +#include + +/** + * We support only ints and map the ID to the port array index. + * However, in the future this will allow using a + * different identifier for finding the port (e.g. the MAC address). + */ +typedef int port_id_t; + +/** + * struct port_dot3_power_settings - Container for Dot3 power information + * received from the LLDP neighbor and for sending MDI advertisements + * + * For details regarding each field, please consult lldpctl.h and + * lldpd-structs.h. The fields are used as defined in the IEEE 802.3bt standard. + */ +struct port_dot3_power_settings { + /** + * 802.1ab and 802.3at fields. + */ + uint8_t poe_device_type; + uint8_t mdi_supported; + uint8_t mdi_enabled; + uint8_t mdi_paircontrol; + uint8_t pse_power_pair; + uint8_t pd_class; + uint8_t power_type; + uint8_t power_source; + uint8_t power_priority; + uint16_t pd_requested; + uint16_t pse_allocated; + + /** + * 802.3bt additions for Type 3 and Type 4 devices. + */ + uint8_t pd_4pid; + uint16_t pd_requested_a; + uint16_t pd_requested_b; + uint16_t pse_allocated_a; + uint16_t pse_allocated_b; + uint16_t pse_status; + uint8_t pd_status; + uint8_t pse_pairs_ext; + uint8_t power_class_mode_a; + uint8_t power_class_mode_b; + uint8_t pd_power_class_ext; + uint8_t power_type_ext; + uint8_t pd_load; + uint16_t pse_max_available_power; +}; + +/** + * struct port_med_power_settings - Container for MED power information + * received from the LLDP neighbor and for sending MDI advertisements + * + * For details regarding each field, please consult lldpctl.h and + * lldpd-structs.h. The fields are used as defined in the ANSI/TIA-1057 standard. + */ +struct port_med_power_settings { + uint8_t poe_device_type; + uint8_t power_source; + uint8_t power_priority; + uint16_t value; +}; + +int med_to_dot3(const struct port_med_power_settings *med_config, + struct port_dot3_power_settings *dot3_config); + +int dot3_to_med(const struct port_dot3_power_settings* dot3_config, + struct port_med_power_settings* med_config); + +int push_lldp_neighbor_update(const char *, + const struct port_dot3_power_settings *); + +/** + * enum port_if_link_event - Relevant link change events for the port state + * machine + * @PORT_IF_UP: interface is enabled an has an active data link + * @PORT_IF_DOWN: interface is operationally down + */ +enum port_if_link_event { + PORT_IF_UP, + PORT_IF_DOWN, +}; + +int push_if_link_update(const char *, enum port_if_link_event); + +void *handle_port_state_machine(); + +#endif /* _LLDP_POE_PORT_STATE_MACHINE_H_ */ diff --git a/lldp-poe/include/queue.h b/lldp-poe/include/queue.h new file mode 100644 index 0000000..bf59b78 --- /dev/null +++ b/lldp-poe/include/queue.h @@ -0,0 +1,77 @@ +/** + * Copyright Amazon Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +#ifndef _LLDP_POE_QUEUE_H_ +#define _LLDP_POE_QUEUE_H_ + +#include +#include + +/** + * Shortcut macros for performing mutex lock/unlock. + */ +#define Q_LOCK(q) \ + ({ \ + if (q->use_lock) \ + pthread_mutex_lock(&q->q_mutex); \ + }) +#define Q_UNLOCK(q) \ + ({ \ + if (q->use_lock) \ + pthread_mutex_unlock(&q->q_mutex); \ + }) + +/** + * struct linked_list - Singly linked list + * @value: generic value + * @next: next node in the list + * + * TODO: Provide dealloc hook for the caller. + * Note: @value must not have dynamically-allocated fields. + */ +struct linked_list { + void *value; + struct linked_list *next; +}; + +/** + * struct queue - Reentrant queue structure, using a singly linked list as the + * underlying data structure. + * @head: front of the queue + * @tail: back of the queue + * @q_mutex: mutex object to use for synchronizing access + * @use_lock: sync flag + */ +struct queue { + struct linked_list *head; + struct linked_list *tail; + pthread_mutex_t q_mutex; + bool use_lock; +}; + +struct linked_list *insert_after(struct linked_list *); + +void free_linked_list(struct linked_list *); + +void q_init(struct queue *, bool); + +void q_enqueue(struct queue *, struct linked_list *); + +struct linked_list *q_dequeue(struct queue *); + +void q_destroy(struct queue *); + +#endif /* _LLDP_POE_QUEUE_H_ */ \ No newline at end of file diff --git a/lldp-poe/lib/cJSON/cJSON.c b/lldp-poe/lib/cJSON/cJSON.c new file mode 100644 index 0000000..5edad54 --- /dev/null +++ b/lldp-poe/lib/cJSON/cJSON.c @@ -0,0 +1,3119 @@ +/* + Copyright (c) 2009-2017 Dave Gamble and cJSON contributors + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +/* cJSON */ +/* JSON parser in C. */ + +/* disable warnings about old C89 functions in MSVC */ +#if !defined(_CRT_SECURE_NO_DEPRECATE) && defined(_MSC_VER) +#define _CRT_SECURE_NO_DEPRECATE +#endif + +#ifdef __GNUC__ +#pragma GCC visibility push(default) +#endif +#if defined(_MSC_VER) +#pragma warning (push) +/* disable warning about single line comments in system headers */ +#pragma warning (disable : 4001) +#endif + +#include +#include +#include +#include +#include +#include +#include + +#ifdef ENABLE_LOCALES +#include +#endif + +#if defined(_MSC_VER) +#pragma warning (pop) +#endif +#ifdef __GNUC__ +#pragma GCC visibility pop +#endif + +#include "cJSON.h" + +/* define our own boolean type */ +#ifdef true +#undef true +#endif +#define true ((cJSON_bool)1) + +#ifdef false +#undef false +#endif +#define false ((cJSON_bool)0) + +/* define isnan and isinf for ANSI C, if in C99 or above, isnan and isinf has been defined in math.h */ +#ifndef isinf +#define isinf(d) (isnan((d - d)) && !isnan(d)) +#endif +#ifndef isnan +#define isnan(d) (d != d) +#endif + +#ifndef NAN +#ifdef _WIN32 +#define NAN sqrt(-1.0) +#else +#define NAN 0.0/0.0 +#endif +#endif + +typedef struct { + const unsigned char *json; + size_t position; +} error; +static error global_error = { NULL, 0 }; + +CJSON_PUBLIC(const char *) cJSON_GetErrorPtr(void) +{ + return (const char*) (global_error.json + global_error.position); +} + +CJSON_PUBLIC(char *) cJSON_GetStringValue(const cJSON * const item) +{ + if (!cJSON_IsString(item)) + { + return NULL; + } + + return item->valuestring; +} + +CJSON_PUBLIC(double) cJSON_GetNumberValue(const cJSON * const item) +{ + if (!cJSON_IsNumber(item)) + { + return (double) NAN; + } + + return item->valuedouble; +} + +/* This is a safeguard to prevent copy-pasters from using incompatible C and header files */ +#if (CJSON_VERSION_MAJOR != 1) || (CJSON_VERSION_MINOR != 7) || (CJSON_VERSION_PATCH != 15) + #error cJSON.h and cJSON.c have different versions. Make sure that both have the same. +#endif + +CJSON_PUBLIC(const char*) cJSON_Version(void) +{ + static char version[15]; + sprintf(version, "%i.%i.%i", CJSON_VERSION_MAJOR, CJSON_VERSION_MINOR, CJSON_VERSION_PATCH); + + return version; +} + +/* Case insensitive string comparison, doesn't consider two NULL pointers equal though */ +static int case_insensitive_strcmp(const unsigned char *string1, const unsigned char *string2) +{ + if ((string1 == NULL) || (string2 == NULL)) + { + return 1; + } + + if (string1 == string2) + { + return 0; + } + + for(; tolower(*string1) == tolower(*string2); (void)string1++, string2++) + { + if (*string1 == '\0') + { + return 0; + } + } + + return tolower(*string1) - tolower(*string2); +} + +typedef struct internal_hooks +{ + void *(CJSON_CDECL *allocate)(size_t size); + void (CJSON_CDECL *deallocate)(void *pointer); + void *(CJSON_CDECL *reallocate)(void *pointer, size_t size); +} internal_hooks; + +#if defined(_MSC_VER) +/* work around MSVC error C2322: '...' address of dllimport '...' is not static */ +static void * CJSON_CDECL internal_malloc(size_t size) +{ + return malloc(size); +} +static void CJSON_CDECL internal_free(void *pointer) +{ + free(pointer); +} +static void * CJSON_CDECL internal_realloc(void *pointer, size_t size) +{ + return realloc(pointer, size); +} +#else +#define internal_malloc malloc +#define internal_free free +#define internal_realloc realloc +#endif + +/* strlen of character literals resolved at compile time */ +#define static_strlen(string_literal) (sizeof(string_literal) - sizeof("")) + +static internal_hooks global_hooks = { internal_malloc, internal_free, internal_realloc }; + +static unsigned char* cJSON_strdup(const unsigned char* string, const internal_hooks * const hooks) +{ + size_t length = 0; + unsigned char *copy = NULL; + + if (string == NULL) + { + return NULL; + } + + length = strlen((const char*)string) + sizeof(""); + copy = (unsigned char*)hooks->allocate(length); + if (copy == NULL) + { + return NULL; + } + memcpy(copy, string, length); + + return copy; +} + +CJSON_PUBLIC(void) cJSON_InitHooks(cJSON_Hooks* hooks) +{ + if (hooks == NULL) + { + /* Reset hooks */ + global_hooks.allocate = malloc; + global_hooks.deallocate = free; + global_hooks.reallocate = realloc; + return; + } + + global_hooks.allocate = malloc; + if (hooks->malloc_fn != NULL) + { + global_hooks.allocate = hooks->malloc_fn; + } + + global_hooks.deallocate = free; + if (hooks->free_fn != NULL) + { + global_hooks.deallocate = hooks->free_fn; + } + + /* use realloc only if both free and malloc are used */ + global_hooks.reallocate = NULL; + if ((global_hooks.allocate == malloc) && (global_hooks.deallocate == free)) + { + global_hooks.reallocate = realloc; + } +} + +/* Internal constructor. */ +static cJSON *cJSON_New_Item(const internal_hooks * const hooks) +{ + cJSON* node = (cJSON*)hooks->allocate(sizeof(cJSON)); + if (node) + { + memset(node, '\0', sizeof(cJSON)); + } + + return node; +} + +/* Delete a cJSON structure. */ +CJSON_PUBLIC(void) cJSON_Delete(cJSON *item) +{ + cJSON *next = NULL; + while (item != NULL) + { + next = item->next; + if (!(item->type & cJSON_IsReference) && (item->child != NULL)) + { + cJSON_Delete(item->child); + } + if (!(item->type & cJSON_IsReference) && (item->valuestring != NULL)) + { + global_hooks.deallocate(item->valuestring); + } + if (!(item->type & cJSON_StringIsConst) && (item->string != NULL)) + { + global_hooks.deallocate(item->string); + } + global_hooks.deallocate(item); + item = next; + } +} + +/* get the decimal point character of the current locale */ +static unsigned char get_decimal_point(void) +{ +#ifdef ENABLE_LOCALES + struct lconv *lconv = localeconv(); + return (unsigned char) lconv->decimal_point[0]; +#else + return '.'; +#endif +} + +typedef struct +{ + const unsigned char *content; + size_t length; + size_t offset; + size_t depth; /* How deeply nested (in arrays/objects) is the input at the current offset. */ + internal_hooks hooks; +} parse_buffer; + +/* check if the given size is left to read in a given parse buffer (starting with 1) */ +#define can_read(buffer, size) ((buffer != NULL) && (((buffer)->offset + size) <= (buffer)->length)) +/* check if the buffer can be accessed at the given index (starting with 0) */ +#define can_access_at_index(buffer, index) ((buffer != NULL) && (((buffer)->offset + index) < (buffer)->length)) +#define cannot_access_at_index(buffer, index) (!can_access_at_index(buffer, index)) +/* get a pointer to the buffer at the position */ +#define buffer_at_offset(buffer) ((buffer)->content + (buffer)->offset) + +/* Parse the input text to generate a number, and populate the result into item. */ +static cJSON_bool parse_number(cJSON * const item, parse_buffer * const input_buffer) +{ + double number = 0; + unsigned char *after_end = NULL; + unsigned char number_c_string[64]; + unsigned char decimal_point = get_decimal_point(); + size_t i = 0; + + if ((input_buffer == NULL) || (input_buffer->content == NULL)) + { + return false; + } + + /* copy the number into a temporary buffer and replace '.' with the decimal point + * of the current locale (for strtod) + * This also takes care of '\0' not necessarily being available for marking the end of the input */ + for (i = 0; (i < (sizeof(number_c_string) - 1)) && can_access_at_index(input_buffer, i); i++) + { + switch (buffer_at_offset(input_buffer)[i]) + { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case '+': + case '-': + case 'e': + case 'E': + number_c_string[i] = buffer_at_offset(input_buffer)[i]; + break; + + case '.': + number_c_string[i] = decimal_point; + break; + + default: + goto loop_end; + } + } +loop_end: + number_c_string[i] = '\0'; + + number = strtod((const char*)number_c_string, (char**)&after_end); + if (number_c_string == after_end) + { + return false; /* parse_error */ + } + + item->valuedouble = number; + + /* use saturation in case of overflow */ + if (number >= INT_MAX) + { + item->valueint = INT_MAX; + } + else if (number <= (double)INT_MIN) + { + item->valueint = INT_MIN; + } + else + { + item->valueint = (int)number; + } + + item->type = cJSON_Number; + + input_buffer->offset += (size_t)(after_end - number_c_string); + return true; +} + +/* don't ask me, but the original cJSON_SetNumberValue returns an integer or double */ +CJSON_PUBLIC(double) cJSON_SetNumberHelper(cJSON *object, double number) +{ + if (number >= INT_MAX) + { + object->valueint = INT_MAX; + } + else if (number <= (double)INT_MIN) + { + object->valueint = INT_MIN; + } + else + { + object->valueint = (int)number; + } + + return object->valuedouble = number; +} + +CJSON_PUBLIC(char*) cJSON_SetValuestring(cJSON *object, const char *valuestring) +{ + char *copy = NULL; + /* if object's type is not cJSON_String or is cJSON_IsReference, it should not set valuestring */ + if (!(object->type & cJSON_String) || (object->type & cJSON_IsReference)) + { + return NULL; + } + if (strlen(valuestring) <= strlen(object->valuestring)) + { + strcpy(object->valuestring, valuestring); + return object->valuestring; + } + copy = (char*) cJSON_strdup((const unsigned char*)valuestring, &global_hooks); + if (copy == NULL) + { + return NULL; + } + if (object->valuestring != NULL) + { + cJSON_free(object->valuestring); + } + object->valuestring = copy; + + return copy; +} + +typedef struct +{ + unsigned char *buffer; + size_t length; + size_t offset; + size_t depth; /* current nesting depth (for formatted printing) */ + cJSON_bool noalloc; + cJSON_bool format; /* is this print a formatted print */ + internal_hooks hooks; +} printbuffer; + +/* realloc printbuffer if necessary to have at least "needed" bytes more */ +static unsigned char* ensure(printbuffer * const p, size_t needed) +{ + unsigned char *newbuffer = NULL; + size_t newsize = 0; + + if ((p == NULL) || (p->buffer == NULL)) + { + return NULL; + } + + if ((p->length > 0) && (p->offset >= p->length)) + { + /* make sure that offset is valid */ + return NULL; + } + + if (needed > INT_MAX) + { + /* sizes bigger than INT_MAX are currently not supported */ + return NULL; + } + + needed += p->offset + 1; + if (needed <= p->length) + { + return p->buffer + p->offset; + } + + if (p->noalloc) { + return NULL; + } + + /* calculate new buffer size */ + if (needed > (INT_MAX / 2)) + { + /* overflow of int, use INT_MAX if possible */ + if (needed <= INT_MAX) + { + newsize = INT_MAX; + } + else + { + return NULL; + } + } + else + { + newsize = needed * 2; + } + + if (p->hooks.reallocate != NULL) + { + /* reallocate with realloc if available */ + newbuffer = (unsigned char*)p->hooks.reallocate(p->buffer, newsize); + if (newbuffer == NULL) + { + p->hooks.deallocate(p->buffer); + p->length = 0; + p->buffer = NULL; + + return NULL; + } + } + else + { + /* otherwise reallocate manually */ + newbuffer = (unsigned char*)p->hooks.allocate(newsize); + if (!newbuffer) + { + p->hooks.deallocate(p->buffer); + p->length = 0; + p->buffer = NULL; + + return NULL; + } + + memcpy(newbuffer, p->buffer, p->offset + 1); + p->hooks.deallocate(p->buffer); + } + p->length = newsize; + p->buffer = newbuffer; + + return newbuffer + p->offset; +} + +/* calculate the new length of the string in a printbuffer and update the offset */ +static void update_offset(printbuffer * const buffer) +{ + const unsigned char *buffer_pointer = NULL; + if ((buffer == NULL) || (buffer->buffer == NULL)) + { + return; + } + buffer_pointer = buffer->buffer + buffer->offset; + buffer->buffer[buffer->length - 1] = '\0'; + buffer->offset += strlen((const char*)buffer_pointer); +} + +/* securely comparison of floating-point variables */ +static cJSON_bool compare_double(double a, double b) +{ + double maxVal = fabs(a) > fabs(b) ? fabs(a) : fabs(b); + return (fabs(a - b) <= maxVal * DBL_EPSILON); +} + +/* Render the number nicely from the given item into a string. */ +static cJSON_bool print_number(const cJSON * const item, printbuffer * const output_buffer) +{ + unsigned char *output_pointer = NULL; + double d = item->valuedouble; + int length = 0; + size_t i = 0; + unsigned char number_buffer[26] = {0}; /* temporary buffer to print the number into */ + unsigned char decimal_point = get_decimal_point(); + double test = 0.0; + + if (output_buffer == NULL) + { + return false; + } + + /* This checks for NaN and Infinity */ + if (isnan(d) || isinf(d)) + { + length = sprintf((char*)number_buffer, "null"); + } + else if(d == (double)item->valueint) + { + length = sprintf((char*)number_buffer, "%d", item->valueint); + } + else + { + /* Try 15 decimal places of precision to avoid nonsignificant nonzero digits */ + length = sprintf((char*)number_buffer, "%1.15g", d); + + /* Check whether the original double can be recovered */ + if ((sscanf((char*)number_buffer, "%lg", &test) != 1) || !compare_double((double)test, d)) + { + /* If not, print with 17 decimal places of precision */ + length = sprintf((char*)number_buffer, "%1.17g", d); + } + } + + /* sprintf failed or buffer overrun occurred */ + if ((length < 0) || (length > (int)(sizeof(number_buffer) - 1))) + { + return false; + } + + /* reserve appropriate space in the output */ + output_pointer = ensure(output_buffer, (size_t)length + sizeof("")); + if (output_pointer == NULL) + { + return false; + } + + /* copy the printed number to the output and replace locale + * dependent decimal point with '.' */ + for (i = 0; i < ((size_t)length); i++) + { + if (number_buffer[i] == decimal_point) + { + output_pointer[i] = '.'; + continue; + } + + output_pointer[i] = number_buffer[i]; + } + output_pointer[i] = '\0'; + + output_buffer->offset += (size_t)length; + + return true; +} + +/* parse 4 digit hexadecimal number */ +static unsigned parse_hex4(const unsigned char * const input) +{ + unsigned int h = 0; + size_t i = 0; + + for (i = 0; i < 4; i++) + { + /* parse digit */ + if ((input[i] >= '0') && (input[i] <= '9')) + { + h += (unsigned int) input[i] - '0'; + } + else if ((input[i] >= 'A') && (input[i] <= 'F')) + { + h += (unsigned int) 10 + input[i] - 'A'; + } + else if ((input[i] >= 'a') && (input[i] <= 'f')) + { + h += (unsigned int) 10 + input[i] - 'a'; + } + else /* invalid */ + { + return 0; + } + + if (i < 3) + { + /* shift left to make place for the next nibble */ + h = h << 4; + } + } + + return h; +} + +/* converts a UTF-16 literal to UTF-8 + * A literal can be one or two sequences of the form \uXXXX */ +static unsigned char utf16_literal_to_utf8(const unsigned char * const input_pointer, const unsigned char * const input_end, unsigned char **output_pointer) +{ + long unsigned int codepoint = 0; + unsigned int first_code = 0; + const unsigned char *first_sequence = input_pointer; + unsigned char utf8_length = 0; + unsigned char utf8_position = 0; + unsigned char sequence_length = 0; + unsigned char first_byte_mark = 0; + + if ((input_end - first_sequence) < 6) + { + /* input ends unexpectedly */ + goto fail; + } + + /* get the first utf16 sequence */ + first_code = parse_hex4(first_sequence + 2); + + /* check that the code is valid */ + if (((first_code >= 0xDC00) && (first_code <= 0xDFFF))) + { + goto fail; + } + + /* UTF16 surrogate pair */ + if ((first_code >= 0xD800) && (first_code <= 0xDBFF)) + { + const unsigned char *second_sequence = first_sequence + 6; + unsigned int second_code = 0; + sequence_length = 12; /* \uXXXX\uXXXX */ + + if ((input_end - second_sequence) < 6) + { + /* input ends unexpectedly */ + goto fail; + } + + if ((second_sequence[0] != '\\') || (second_sequence[1] != 'u')) + { + /* missing second half of the surrogate pair */ + goto fail; + } + + /* get the second utf16 sequence */ + second_code = parse_hex4(second_sequence + 2); + /* check that the code is valid */ + if ((second_code < 0xDC00) || (second_code > 0xDFFF)) + { + /* invalid second half of the surrogate pair */ + goto fail; + } + + + /* calculate the unicode codepoint from the surrogate pair */ + codepoint = 0x10000 + (((first_code & 0x3FF) << 10) | (second_code & 0x3FF)); + } + else + { + sequence_length = 6; /* \uXXXX */ + codepoint = first_code; + } + + /* encode as UTF-8 + * takes at maximum 4 bytes to encode: + * 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx */ + if (codepoint < 0x80) + { + /* normal ascii, encoding 0xxxxxxx */ + utf8_length = 1; + } + else if (codepoint < 0x800) + { + /* two bytes, encoding 110xxxxx 10xxxxxx */ + utf8_length = 2; + first_byte_mark = 0xC0; /* 11000000 */ + } + else if (codepoint < 0x10000) + { + /* three bytes, encoding 1110xxxx 10xxxxxx 10xxxxxx */ + utf8_length = 3; + first_byte_mark = 0xE0; /* 11100000 */ + } + else if (codepoint <= 0x10FFFF) + { + /* four bytes, encoding 1110xxxx 10xxxxxx 10xxxxxx 10xxxxxx */ + utf8_length = 4; + first_byte_mark = 0xF0; /* 11110000 */ + } + else + { + /* invalid unicode codepoint */ + goto fail; + } + + /* encode as utf8 */ + for (utf8_position = (unsigned char)(utf8_length - 1); utf8_position > 0; utf8_position--) + { + /* 10xxxxxx */ + (*output_pointer)[utf8_position] = (unsigned char)((codepoint | 0x80) & 0xBF); + codepoint >>= 6; + } + /* encode first byte */ + if (utf8_length > 1) + { + (*output_pointer)[0] = (unsigned char)((codepoint | first_byte_mark) & 0xFF); + } + else + { + (*output_pointer)[0] = (unsigned char)(codepoint & 0x7F); + } + + *output_pointer += utf8_length; + + return sequence_length; + +fail: + return 0; +} + +/* Parse the input text into an unescaped cinput, and populate item. */ +static cJSON_bool parse_string(cJSON * const item, parse_buffer * const input_buffer) +{ + const unsigned char *input_pointer = buffer_at_offset(input_buffer) + 1; + const unsigned char *input_end = buffer_at_offset(input_buffer) + 1; + unsigned char *output_pointer = NULL; + unsigned char *output = NULL; + + /* not a string */ + if (buffer_at_offset(input_buffer)[0] != '\"') + { + goto fail; + } + + { + /* calculate approximate size of the output (overestimate) */ + size_t allocation_length = 0; + size_t skipped_bytes = 0; + while (((size_t)(input_end - input_buffer->content) < input_buffer->length) && (*input_end != '\"')) + { + /* is escape sequence */ + if (input_end[0] == '\\') + { + if ((size_t)(input_end + 1 - input_buffer->content) >= input_buffer->length) + { + /* prevent buffer overflow when last input character is a backslash */ + goto fail; + } + skipped_bytes++; + input_end++; + } + input_end++; + } + if (((size_t)(input_end - input_buffer->content) >= input_buffer->length) || (*input_end != '\"')) + { + goto fail; /* string ended unexpectedly */ + } + + /* This is at most how much we need for the output */ + allocation_length = (size_t) (input_end - buffer_at_offset(input_buffer)) - skipped_bytes; + output = (unsigned char*)input_buffer->hooks.allocate(allocation_length + sizeof("")); + if (output == NULL) + { + goto fail; /* allocation failure */ + } + } + + output_pointer = output; + /* loop through the string literal */ + while (input_pointer < input_end) + { + if (*input_pointer != '\\') + { + *output_pointer++ = *input_pointer++; + } + /* escape sequence */ + else + { + unsigned char sequence_length = 2; + if ((input_end - input_pointer) < 1) + { + goto fail; + } + + switch (input_pointer[1]) + { + case 'b': + *output_pointer++ = '\b'; + break; + case 'f': + *output_pointer++ = '\f'; + break; + case 'n': + *output_pointer++ = '\n'; + break; + case 'r': + *output_pointer++ = '\r'; + break; + case 't': + *output_pointer++ = '\t'; + break; + case '\"': + case '\\': + case '/': + *output_pointer++ = input_pointer[1]; + break; + + /* UTF-16 literal */ + case 'u': + sequence_length = utf16_literal_to_utf8(input_pointer, input_end, &output_pointer); + if (sequence_length == 0) + { + /* failed to convert UTF16-literal to UTF-8 */ + goto fail; + } + break; + + default: + goto fail; + } + input_pointer += sequence_length; + } + } + + /* zero terminate the output */ + *output_pointer = '\0'; + + item->type = cJSON_String; + item->valuestring = (char*)output; + + input_buffer->offset = (size_t) (input_end - input_buffer->content); + input_buffer->offset++; + + return true; + +fail: + if (output != NULL) + { + input_buffer->hooks.deallocate(output); + } + + if (input_pointer != NULL) + { + input_buffer->offset = (size_t)(input_pointer - input_buffer->content); + } + + return false; +} + +/* Render the cstring provided to an escaped version that can be printed. */ +static cJSON_bool print_string_ptr(const unsigned char * const input, printbuffer * const output_buffer) +{ + const unsigned char *input_pointer = NULL; + unsigned char *output = NULL; + unsigned char *output_pointer = NULL; + size_t output_length = 0; + /* numbers of additional characters needed for escaping */ + size_t escape_characters = 0; + + if (output_buffer == NULL) + { + return false; + } + + /* empty string */ + if (input == NULL) + { + output = ensure(output_buffer, sizeof("\"\"")); + if (output == NULL) + { + return false; + } + strcpy((char*)output, "\"\""); + + return true; + } + + /* set "flag" to 1 if something needs to be escaped */ + for (input_pointer = input; *input_pointer; input_pointer++) + { + switch (*input_pointer) + { + case '\"': + case '\\': + case '\b': + case '\f': + case '\n': + case '\r': + case '\t': + /* one character escape sequence */ + escape_characters++; + break; + default: + if (*input_pointer < 32) + { + /* UTF-16 escape sequence uXXXX */ + escape_characters += 5; + } + break; + } + } + output_length = (size_t)(input_pointer - input) + escape_characters; + + output = ensure(output_buffer, output_length + sizeof("\"\"")); + if (output == NULL) + { + return false; + } + + /* no characters have to be escaped */ + if (escape_characters == 0) + { + output[0] = '\"'; + memcpy(output + 1, input, output_length); + output[output_length + 1] = '\"'; + output[output_length + 2] = '\0'; + + return true; + } + + output[0] = '\"'; + output_pointer = output + 1; + /* copy the string */ + for (input_pointer = input; *input_pointer != '\0'; (void)input_pointer++, output_pointer++) + { + if ((*input_pointer > 31) && (*input_pointer != '\"') && (*input_pointer != '\\')) + { + /* normal character, copy */ + *output_pointer = *input_pointer; + } + else + { + /* character needs to be escaped */ + *output_pointer++ = '\\'; + switch (*input_pointer) + { + case '\\': + *output_pointer = '\\'; + break; + case '\"': + *output_pointer = '\"'; + break; + case '\b': + *output_pointer = 'b'; + break; + case '\f': + *output_pointer = 'f'; + break; + case '\n': + *output_pointer = 'n'; + break; + case '\r': + *output_pointer = 'r'; + break; + case '\t': + *output_pointer = 't'; + break; + default: + /* escape and print as unicode codepoint */ + sprintf((char*)output_pointer, "u%04x", *input_pointer); + output_pointer += 4; + break; + } + } + } + output[output_length + 1] = '\"'; + output[output_length + 2] = '\0'; + + return true; +} + +/* Invoke print_string_ptr (which is useful) on an item. */ +static cJSON_bool print_string(const cJSON * const item, printbuffer * const p) +{ + return print_string_ptr((unsigned char*)item->valuestring, p); +} + +/* Predeclare these prototypes. */ +static cJSON_bool parse_value(cJSON * const item, parse_buffer * const input_buffer); +static cJSON_bool print_value(const cJSON * const item, printbuffer * const output_buffer); +static cJSON_bool parse_array(cJSON * const item, parse_buffer * const input_buffer); +static cJSON_bool print_array(const cJSON * const item, printbuffer * const output_buffer); +static cJSON_bool parse_object(cJSON * const item, parse_buffer * const input_buffer); +static cJSON_bool print_object(const cJSON * const item, printbuffer * const output_buffer); + +/* Utility to jump whitespace and cr/lf */ +static parse_buffer *buffer_skip_whitespace(parse_buffer * const buffer) +{ + if ((buffer == NULL) || (buffer->content == NULL)) + { + return NULL; + } + + if (cannot_access_at_index(buffer, 0)) + { + return buffer; + } + + while (can_access_at_index(buffer, 0) && (buffer_at_offset(buffer)[0] <= 32)) + { + buffer->offset++; + } + + if (buffer->offset == buffer->length) + { + buffer->offset--; + } + + return buffer; +} + +/* skip the UTF-8 BOM (byte order mark) if it is at the beginning of a buffer */ +static parse_buffer *skip_utf8_bom(parse_buffer * const buffer) +{ + if ((buffer == NULL) || (buffer->content == NULL) || (buffer->offset != 0)) + { + return NULL; + } + + if (can_access_at_index(buffer, 4) && (strncmp((const char*)buffer_at_offset(buffer), "\xEF\xBB\xBF", 3) == 0)) + { + buffer->offset += 3; + } + + return buffer; +} + +CJSON_PUBLIC(cJSON *) cJSON_ParseWithOpts(const char *value, const char **return_parse_end, cJSON_bool require_null_terminated) +{ + size_t buffer_length; + + if (NULL == value) + { + return NULL; + } + + /* Adding null character size due to require_null_terminated. */ + buffer_length = strlen(value) + sizeof(""); + + return cJSON_ParseWithLengthOpts(value, buffer_length, return_parse_end, require_null_terminated); +} + +/* Parse an object - create a new root, and populate. */ +CJSON_PUBLIC(cJSON *) cJSON_ParseWithLengthOpts(const char *value, size_t buffer_length, const char **return_parse_end, cJSON_bool require_null_terminated) +{ + parse_buffer buffer = { 0, 0, 0, 0, { 0, 0, 0 } }; + cJSON *item = NULL; + + /* reset error position */ + global_error.json = NULL; + global_error.position = 0; + + if (value == NULL || 0 == buffer_length) + { + goto fail; + } + + buffer.content = (const unsigned char*)value; + buffer.length = buffer_length; + buffer.offset = 0; + buffer.hooks = global_hooks; + + item = cJSON_New_Item(&global_hooks); + if (item == NULL) /* memory fail */ + { + goto fail; + } + + if (!parse_value(item, buffer_skip_whitespace(skip_utf8_bom(&buffer)))) + { + /* parse failure. ep is set. */ + goto fail; + } + + /* if we require null-terminated JSON without appended garbage, skip and then check for a null terminator */ + if (require_null_terminated) + { + buffer_skip_whitespace(&buffer); + if ((buffer.offset >= buffer.length) || buffer_at_offset(&buffer)[0] != '\0') + { + goto fail; + } + } + if (return_parse_end) + { + *return_parse_end = (const char*)buffer_at_offset(&buffer); + } + + return item; + +fail: + if (item != NULL) + { + cJSON_Delete(item); + } + + if (value != NULL) + { + error local_error; + local_error.json = (const unsigned char*)value; + local_error.position = 0; + + if (buffer.offset < buffer.length) + { + local_error.position = buffer.offset; + } + else if (buffer.length > 0) + { + local_error.position = buffer.length - 1; + } + + if (return_parse_end != NULL) + { + *return_parse_end = (const char*)local_error.json + local_error.position; + } + + global_error = local_error; + } + + return NULL; +} + +/* Default options for cJSON_Parse */ +CJSON_PUBLIC(cJSON *) cJSON_Parse(const char *value) +{ + return cJSON_ParseWithOpts(value, 0, 0); +} + +CJSON_PUBLIC(cJSON *) cJSON_ParseWithLength(const char *value, size_t buffer_length) +{ + return cJSON_ParseWithLengthOpts(value, buffer_length, 0, 0); +} + +#define cjson_min(a, b) (((a) < (b)) ? (a) : (b)) + +static unsigned char *print(const cJSON * const item, cJSON_bool format, const internal_hooks * const hooks) +{ + static const size_t default_buffer_size = 256; + printbuffer buffer[1]; + unsigned char *printed = NULL; + + memset(buffer, 0, sizeof(buffer)); + + /* create buffer */ + buffer->buffer = (unsigned char*) hooks->allocate(default_buffer_size); + buffer->length = default_buffer_size; + buffer->format = format; + buffer->hooks = *hooks; + if (buffer->buffer == NULL) + { + goto fail; + } + + /* print the value */ + if (!print_value(item, buffer)) + { + goto fail; + } + update_offset(buffer); + + /* check if reallocate is available */ + if (hooks->reallocate != NULL) + { + printed = (unsigned char*) hooks->reallocate(buffer->buffer, buffer->offset + 1); + if (printed == NULL) { + goto fail; + } + buffer->buffer = NULL; + } + else /* otherwise copy the JSON over to a new buffer */ + { + printed = (unsigned char*) hooks->allocate(buffer->offset + 1); + if (printed == NULL) + { + goto fail; + } + memcpy(printed, buffer->buffer, cjson_min(buffer->length, buffer->offset + 1)); + printed[buffer->offset] = '\0'; /* just to be sure */ + + /* free the buffer */ + hooks->deallocate(buffer->buffer); + } + + return printed; + +fail: + if (buffer->buffer != NULL) + { + hooks->deallocate(buffer->buffer); + } + + if (printed != NULL) + { + hooks->deallocate(printed); + } + + return NULL; +} + +/* Render a cJSON item/entity/structure to text. */ +CJSON_PUBLIC(char *) cJSON_Print(const cJSON *item) +{ + return (char*)print(item, true, &global_hooks); +} + +CJSON_PUBLIC(char *) cJSON_PrintUnformatted(const cJSON *item) +{ + return (char*)print(item, false, &global_hooks); +} + +CJSON_PUBLIC(char *) cJSON_PrintBuffered(const cJSON *item, int prebuffer, cJSON_bool fmt) +{ + printbuffer p = { 0, 0, 0, 0, 0, 0, { 0, 0, 0 } }; + + if (prebuffer < 0) + { + return NULL; + } + + p.buffer = (unsigned char*)global_hooks.allocate((size_t)prebuffer); + if (!p.buffer) + { + return NULL; + } + + p.length = (size_t)prebuffer; + p.offset = 0; + p.noalloc = false; + p.format = fmt; + p.hooks = global_hooks; + + if (!print_value(item, &p)) + { + global_hooks.deallocate(p.buffer); + return NULL; + } + + return (char*)p.buffer; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_PrintPreallocated(cJSON *item, char *buffer, const int length, const cJSON_bool format) +{ + printbuffer p = { 0, 0, 0, 0, 0, 0, { 0, 0, 0 } }; + + if ((length < 0) || (buffer == NULL)) + { + return false; + } + + p.buffer = (unsigned char*)buffer; + p.length = (size_t)length; + p.offset = 0; + p.noalloc = true; + p.format = format; + p.hooks = global_hooks; + + return print_value(item, &p); +} + +/* Parser core - when encountering text, process appropriately. */ +static cJSON_bool parse_value(cJSON * const item, parse_buffer * const input_buffer) +{ + if ((input_buffer == NULL) || (input_buffer->content == NULL)) + { + return false; /* no input */ + } + + /* parse the different types of values */ + /* null */ + if (can_read(input_buffer, 4) && (strncmp((const char*)buffer_at_offset(input_buffer), "null", 4) == 0)) + { + item->type = cJSON_NULL; + input_buffer->offset += 4; + return true; + } + /* false */ + if (can_read(input_buffer, 5) && (strncmp((const char*)buffer_at_offset(input_buffer), "false", 5) == 0)) + { + item->type = cJSON_False; + input_buffer->offset += 5; + return true; + } + /* true */ + if (can_read(input_buffer, 4) && (strncmp((const char*)buffer_at_offset(input_buffer), "true", 4) == 0)) + { + item->type = cJSON_True; + item->valueint = 1; + input_buffer->offset += 4; + return true; + } + /* string */ + if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == '\"')) + { + return parse_string(item, input_buffer); + } + /* number */ + if (can_access_at_index(input_buffer, 0) && ((buffer_at_offset(input_buffer)[0] == '-') || ((buffer_at_offset(input_buffer)[0] >= '0') && (buffer_at_offset(input_buffer)[0] <= '9')))) + { + return parse_number(item, input_buffer); + } + /* array */ + if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == '[')) + { + return parse_array(item, input_buffer); + } + /* object */ + if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == '{')) + { + return parse_object(item, input_buffer); + } + + return false; +} + +/* Render a value to text. */ +static cJSON_bool print_value(const cJSON * const item, printbuffer * const output_buffer) +{ + unsigned char *output = NULL; + + if ((item == NULL) || (output_buffer == NULL)) + { + return false; + } + + switch ((item->type) & 0xFF) + { + case cJSON_NULL: + output = ensure(output_buffer, 5); + if (output == NULL) + { + return false; + } + strcpy((char*)output, "null"); + return true; + + case cJSON_False: + output = ensure(output_buffer, 6); + if (output == NULL) + { + return false; + } + strcpy((char*)output, "false"); + return true; + + case cJSON_True: + output = ensure(output_buffer, 5); + if (output == NULL) + { + return false; + } + strcpy((char*)output, "true"); + return true; + + case cJSON_Number: + return print_number(item, output_buffer); + + case cJSON_Raw: + { + size_t raw_length = 0; + if (item->valuestring == NULL) + { + return false; + } + + raw_length = strlen(item->valuestring) + sizeof(""); + output = ensure(output_buffer, raw_length); + if (output == NULL) + { + return false; + } + memcpy(output, item->valuestring, raw_length); + return true; + } + + case cJSON_String: + return print_string(item, output_buffer); + + case cJSON_Array: + return print_array(item, output_buffer); + + case cJSON_Object: + return print_object(item, output_buffer); + + default: + return false; + } +} + +/* Build an array from input text. */ +static cJSON_bool parse_array(cJSON * const item, parse_buffer * const input_buffer) +{ + cJSON *head = NULL; /* head of the linked list */ + cJSON *current_item = NULL; + + if (input_buffer->depth >= CJSON_NESTING_LIMIT) + { + return false; /* to deeply nested */ + } + input_buffer->depth++; + + if (buffer_at_offset(input_buffer)[0] != '[') + { + /* not an array */ + goto fail; + } + + input_buffer->offset++; + buffer_skip_whitespace(input_buffer); + if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == ']')) + { + /* empty array */ + goto success; + } + + /* check if we skipped to the end of the buffer */ + if (cannot_access_at_index(input_buffer, 0)) + { + input_buffer->offset--; + goto fail; + } + + /* step back to character in front of the first element */ + input_buffer->offset--; + /* loop through the comma separated array elements */ + do + { + /* allocate next item */ + cJSON *new_item = cJSON_New_Item(&(input_buffer->hooks)); + if (new_item == NULL) + { + goto fail; /* allocation failure */ + } + + /* attach next item to list */ + if (head == NULL) + { + /* start the linked list */ + current_item = head = new_item; + } + else + { + /* add to the end and advance */ + current_item->next = new_item; + new_item->prev = current_item; + current_item = new_item; + } + + /* parse next value */ + input_buffer->offset++; + buffer_skip_whitespace(input_buffer); + if (!parse_value(current_item, input_buffer)) + { + goto fail; /* failed to parse value */ + } + buffer_skip_whitespace(input_buffer); + } + while (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == ',')); + + if (cannot_access_at_index(input_buffer, 0) || buffer_at_offset(input_buffer)[0] != ']') + { + goto fail; /* expected end of array */ + } + +success: + input_buffer->depth--; + + if (head != NULL) { + head->prev = current_item; + } + + item->type = cJSON_Array; + item->child = head; + + input_buffer->offset++; + + return true; + +fail: + if (head != NULL) + { + cJSON_Delete(head); + } + + return false; +} + +/* Render an array to text */ +static cJSON_bool print_array(const cJSON * const item, printbuffer * const output_buffer) +{ + unsigned char *output_pointer = NULL; + size_t length = 0; + cJSON *current_element = item->child; + + if (output_buffer == NULL) + { + return false; + } + + /* Compose the output array. */ + /* opening square bracket */ + output_pointer = ensure(output_buffer, 1); + if (output_pointer == NULL) + { + return false; + } + + *output_pointer = '['; + output_buffer->offset++; + output_buffer->depth++; + + while (current_element != NULL) + { + if (!print_value(current_element, output_buffer)) + { + return false; + } + update_offset(output_buffer); + if (current_element->next) + { + length = (size_t) (output_buffer->format ? 2 : 1); + output_pointer = ensure(output_buffer, length + 1); + if (output_pointer == NULL) + { + return false; + } + *output_pointer++ = ','; + if(output_buffer->format) + { + *output_pointer++ = ' '; + } + *output_pointer = '\0'; + output_buffer->offset += length; + } + current_element = current_element->next; + } + + output_pointer = ensure(output_buffer, 2); + if (output_pointer == NULL) + { + return false; + } + *output_pointer++ = ']'; + *output_pointer = '\0'; + output_buffer->depth--; + + return true; +} + +/* Build an object from the text. */ +static cJSON_bool parse_object(cJSON * const item, parse_buffer * const input_buffer) +{ + cJSON *head = NULL; /* linked list head */ + cJSON *current_item = NULL; + + if (input_buffer->depth >= CJSON_NESTING_LIMIT) + { + return false; /* to deeply nested */ + } + input_buffer->depth++; + + if (cannot_access_at_index(input_buffer, 0) || (buffer_at_offset(input_buffer)[0] != '{')) + { + goto fail; /* not an object */ + } + + input_buffer->offset++; + buffer_skip_whitespace(input_buffer); + if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == '}')) + { + goto success; /* empty object */ + } + + /* check if we skipped to the end of the buffer */ + if (cannot_access_at_index(input_buffer, 0)) + { + input_buffer->offset--; + goto fail; + } + + /* step back to character in front of the first element */ + input_buffer->offset--; + /* loop through the comma separated array elements */ + do + { + /* allocate next item */ + cJSON *new_item = cJSON_New_Item(&(input_buffer->hooks)); + if (new_item == NULL) + { + goto fail; /* allocation failure */ + } + + /* attach next item to list */ + if (head == NULL) + { + /* start the linked list */ + current_item = head = new_item; + } + else + { + /* add to the end and advance */ + current_item->next = new_item; + new_item->prev = current_item; + current_item = new_item; + } + + /* parse the name of the child */ + input_buffer->offset++; + buffer_skip_whitespace(input_buffer); + if (!parse_string(current_item, input_buffer)) + { + goto fail; /* failed to parse name */ + } + buffer_skip_whitespace(input_buffer); + + /* swap valuestring and string, because we parsed the name */ + current_item->string = current_item->valuestring; + current_item->valuestring = NULL; + + if (cannot_access_at_index(input_buffer, 0) || (buffer_at_offset(input_buffer)[0] != ':')) + { + goto fail; /* invalid object */ + } + + /* parse the value */ + input_buffer->offset++; + buffer_skip_whitespace(input_buffer); + if (!parse_value(current_item, input_buffer)) + { + goto fail; /* failed to parse value */ + } + buffer_skip_whitespace(input_buffer); + } + while (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == ',')); + + if (cannot_access_at_index(input_buffer, 0) || (buffer_at_offset(input_buffer)[0] != '}')) + { + goto fail; /* expected end of object */ + } + +success: + input_buffer->depth--; + + if (head != NULL) { + head->prev = current_item; + } + + item->type = cJSON_Object; + item->child = head; + + input_buffer->offset++; + return true; + +fail: + if (head != NULL) + { + cJSON_Delete(head); + } + + return false; +} + +/* Render an object to text. */ +static cJSON_bool print_object(const cJSON * const item, printbuffer * const output_buffer) +{ + unsigned char *output_pointer = NULL; + size_t length = 0; + cJSON *current_item = item->child; + + if (output_buffer == NULL) + { + return false; + } + + /* Compose the output: */ + length = (size_t) (output_buffer->format ? 2 : 1); /* fmt: {\n */ + output_pointer = ensure(output_buffer, length + 1); + if (output_pointer == NULL) + { + return false; + } + + *output_pointer++ = '{'; + output_buffer->depth++; + if (output_buffer->format) + { + *output_pointer++ = '\n'; + } + output_buffer->offset += length; + + while (current_item) + { + if (output_buffer->format) + { + size_t i; + output_pointer = ensure(output_buffer, output_buffer->depth); + if (output_pointer == NULL) + { + return false; + } + for (i = 0; i < output_buffer->depth; i++) + { + *output_pointer++ = '\t'; + } + output_buffer->offset += output_buffer->depth; + } + + /* print key */ + if (!print_string_ptr((unsigned char*)current_item->string, output_buffer)) + { + return false; + } + update_offset(output_buffer); + + length = (size_t) (output_buffer->format ? 2 : 1); + output_pointer = ensure(output_buffer, length); + if (output_pointer == NULL) + { + return false; + } + *output_pointer++ = ':'; + if (output_buffer->format) + { + *output_pointer++ = '\t'; + } + output_buffer->offset += length; + + /* print value */ + if (!print_value(current_item, output_buffer)) + { + return false; + } + update_offset(output_buffer); + + /* print comma if not last */ + length = ((size_t)(output_buffer->format ? 1 : 0) + (size_t)(current_item->next ? 1 : 0)); + output_pointer = ensure(output_buffer, length + 1); + if (output_pointer == NULL) + { + return false; + } + if (current_item->next) + { + *output_pointer++ = ','; + } + + if (output_buffer->format) + { + *output_pointer++ = '\n'; + } + *output_pointer = '\0'; + output_buffer->offset += length; + + current_item = current_item->next; + } + + output_pointer = ensure(output_buffer, output_buffer->format ? (output_buffer->depth + 1) : 2); + if (output_pointer == NULL) + { + return false; + } + if (output_buffer->format) + { + size_t i; + for (i = 0; i < (output_buffer->depth - 1); i++) + { + *output_pointer++ = '\t'; + } + } + *output_pointer++ = '}'; + *output_pointer = '\0'; + output_buffer->depth--; + + return true; +} + +/* Get Array size/item / object item. */ +CJSON_PUBLIC(int) cJSON_GetArraySize(const cJSON *array) +{ + cJSON *child = NULL; + size_t size = 0; + + if (array == NULL) + { + return 0; + } + + child = array->child; + + while(child != NULL) + { + size++; + child = child->next; + } + + /* FIXME: Can overflow here. Cannot be fixed without breaking the API */ + + return (int)size; +} + +static cJSON* get_array_item(const cJSON *array, size_t index) +{ + cJSON *current_child = NULL; + + if (array == NULL) + { + return NULL; + } + + current_child = array->child; + while ((current_child != NULL) && (index > 0)) + { + index--; + current_child = current_child->next; + } + + return current_child; +} + +CJSON_PUBLIC(cJSON *) cJSON_GetArrayItem(const cJSON *array, int index) +{ + if (index < 0) + { + return NULL; + } + + return get_array_item(array, (size_t)index); +} + +static cJSON *get_object_item(const cJSON * const object, const char * const name, const cJSON_bool case_sensitive) +{ + cJSON *current_element = NULL; + + if ((object == NULL) || (name == NULL)) + { + return NULL; + } + + current_element = object->child; + if (case_sensitive) + { + while ((current_element != NULL) && (current_element->string != NULL) && (strcmp(name, current_element->string) != 0)) + { + current_element = current_element->next; + } + } + else + { + while ((current_element != NULL) && (case_insensitive_strcmp((const unsigned char*)name, (const unsigned char*)(current_element->string)) != 0)) + { + current_element = current_element->next; + } + } + + if ((current_element == NULL) || (current_element->string == NULL)) { + return NULL; + } + + return current_element; +} + +CJSON_PUBLIC(cJSON *) cJSON_GetObjectItem(const cJSON * const object, const char * const string) +{ + return get_object_item(object, string, false); +} + +CJSON_PUBLIC(cJSON *) cJSON_GetObjectItemCaseSensitive(const cJSON * const object, const char * const string) +{ + return get_object_item(object, string, true); +} + +CJSON_PUBLIC(cJSON_bool) cJSON_HasObjectItem(const cJSON *object, const char *string) +{ + return cJSON_GetObjectItem(object, string) ? 1 : 0; +} + +/* Utility for array list handling. */ +static void suffix_object(cJSON *prev, cJSON *item) +{ + prev->next = item; + item->prev = prev; +} + +/* Utility for handling references. */ +static cJSON *create_reference(const cJSON *item, const internal_hooks * const hooks) +{ + cJSON *reference = NULL; + if (item == NULL) + { + return NULL; + } + + reference = cJSON_New_Item(hooks); + if (reference == NULL) + { + return NULL; + } + + memcpy(reference, item, sizeof(cJSON)); + reference->string = NULL; + reference->type |= cJSON_IsReference; + reference->next = reference->prev = NULL; + return reference; +} + +static cJSON_bool add_item_to_array(cJSON *array, cJSON *item) +{ + cJSON *child = NULL; + + if ((item == NULL) || (array == NULL) || (array == item)) + { + return false; + } + + child = array->child; + /* + * To find the last item in array quickly, we use prev in array + */ + if (child == NULL) + { + /* list is empty, start new one */ + array->child = item; + item->prev = item; + item->next = NULL; + } + else + { + /* append to the end */ + if (child->prev) + { + suffix_object(child->prev, item); + array->child->prev = item; + } + } + + return true; +} + +/* Add item to array/object. */ +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToArray(cJSON *array, cJSON *item) +{ + return add_item_to_array(array, item); +} + +#if defined(__clang__) || (defined(__GNUC__) && ((__GNUC__ > 4) || ((__GNUC__ == 4) && (__GNUC_MINOR__ > 5)))) + #pragma GCC diagnostic push +#endif +#ifdef __GNUC__ +#pragma GCC diagnostic ignored "-Wcast-qual" +#endif +/* helper function to cast away const */ +static void* cast_away_const(const void* string) +{ + return (void*)string; +} +#if defined(__clang__) || (defined(__GNUC__) && ((__GNUC__ > 4) || ((__GNUC__ == 4) && (__GNUC_MINOR__ > 5)))) + #pragma GCC diagnostic pop +#endif + + +static cJSON_bool add_item_to_object(cJSON * const object, const char * const string, cJSON * const item, const internal_hooks * const hooks, const cJSON_bool constant_key) +{ + char *new_key = NULL; + int new_type = cJSON_Invalid; + + if ((object == NULL) || (string == NULL) || (item == NULL) || (object == item)) + { + return false; + } + + if (constant_key) + { + new_key = (char*)cast_away_const(string); + new_type = item->type | cJSON_StringIsConst; + } + else + { + new_key = (char*)cJSON_strdup((const unsigned char*)string, hooks); + if (new_key == NULL) + { + return false; + } + + new_type = item->type & ~cJSON_StringIsConst; + } + + if (!(item->type & cJSON_StringIsConst) && (item->string != NULL)) + { + hooks->deallocate(item->string); + } + + item->string = new_key; + item->type = new_type; + + return add_item_to_array(object, item); +} + +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObject(cJSON *object, const char *string, cJSON *item) +{ + return add_item_to_object(object, string, item, &global_hooks, false); +} + +/* Add an item to an object with constant string as key */ +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObjectCS(cJSON *object, const char *string, cJSON *item) +{ + return add_item_to_object(object, string, item, &global_hooks, true); +} + +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToArray(cJSON *array, cJSON *item) +{ + if (array == NULL) + { + return false; + } + + return add_item_to_array(array, create_reference(item, &global_hooks)); +} + +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToObject(cJSON *object, const char *string, cJSON *item) +{ + if ((object == NULL) || (string == NULL)) + { + return false; + } + + return add_item_to_object(object, string, create_reference(item, &global_hooks), &global_hooks, false); +} + +CJSON_PUBLIC(cJSON*) cJSON_AddNullToObject(cJSON * const object, const char * const name) +{ + cJSON *null = cJSON_CreateNull(); + if (add_item_to_object(object, name, null, &global_hooks, false)) + { + return null; + } + + cJSON_Delete(null); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddTrueToObject(cJSON * const object, const char * const name) +{ + cJSON *true_item = cJSON_CreateTrue(); + if (add_item_to_object(object, name, true_item, &global_hooks, false)) + { + return true_item; + } + + cJSON_Delete(true_item); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddFalseToObject(cJSON * const object, const char * const name) +{ + cJSON *false_item = cJSON_CreateFalse(); + if (add_item_to_object(object, name, false_item, &global_hooks, false)) + { + return false_item; + } + + cJSON_Delete(false_item); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddBoolToObject(cJSON * const object, const char * const name, const cJSON_bool boolean) +{ + cJSON *bool_item = cJSON_CreateBool(boolean); + if (add_item_to_object(object, name, bool_item, &global_hooks, false)) + { + return bool_item; + } + + cJSON_Delete(bool_item); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddNumberToObject(cJSON * const object, const char * const name, const double number) +{ + cJSON *number_item = cJSON_CreateNumber(number); + if (add_item_to_object(object, name, number_item, &global_hooks, false)) + { + return number_item; + } + + cJSON_Delete(number_item); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddStringToObject(cJSON * const object, const char * const name, const char * const string) +{ + cJSON *string_item = cJSON_CreateString(string); + if (add_item_to_object(object, name, string_item, &global_hooks, false)) + { + return string_item; + } + + cJSON_Delete(string_item); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddRawToObject(cJSON * const object, const char * const name, const char * const raw) +{ + cJSON *raw_item = cJSON_CreateRaw(raw); + if (add_item_to_object(object, name, raw_item, &global_hooks, false)) + { + return raw_item; + } + + cJSON_Delete(raw_item); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddObjectToObject(cJSON * const object, const char * const name) +{ + cJSON *object_item = cJSON_CreateObject(); + if (add_item_to_object(object, name, object_item, &global_hooks, false)) + { + return object_item; + } + + cJSON_Delete(object_item); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddArrayToObject(cJSON * const object, const char * const name) +{ + cJSON *array = cJSON_CreateArray(); + if (add_item_to_object(object, name, array, &global_hooks, false)) + { + return array; + } + + cJSON_Delete(array); + return NULL; +} + +CJSON_PUBLIC(cJSON *) cJSON_DetachItemViaPointer(cJSON *parent, cJSON * const item) +{ + if ((parent == NULL) || (item == NULL)) + { + return NULL; + } + + if (item != parent->child) + { + /* not the first element */ + item->prev->next = item->next; + } + if (item->next != NULL) + { + /* not the last element */ + item->next->prev = item->prev; + } + + if (item == parent->child) + { + /* first element */ + parent->child = item->next; + } + else if (item->next == NULL) + { + /* last element */ + parent->child->prev = item->prev; + } + + /* make sure the detached item doesn't point anywhere anymore */ + item->prev = NULL; + item->next = NULL; + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromArray(cJSON *array, int which) +{ + if (which < 0) + { + return NULL; + } + + return cJSON_DetachItemViaPointer(array, get_array_item(array, (size_t)which)); +} + +CJSON_PUBLIC(void) cJSON_DeleteItemFromArray(cJSON *array, int which) +{ + cJSON_Delete(cJSON_DetachItemFromArray(array, which)); +} + +CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObject(cJSON *object, const char *string) +{ + cJSON *to_detach = cJSON_GetObjectItem(object, string); + + return cJSON_DetachItemViaPointer(object, to_detach); +} + +CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObjectCaseSensitive(cJSON *object, const char *string) +{ + cJSON *to_detach = cJSON_GetObjectItemCaseSensitive(object, string); + + return cJSON_DetachItemViaPointer(object, to_detach); +} + +CJSON_PUBLIC(void) cJSON_DeleteItemFromObject(cJSON *object, const char *string) +{ + cJSON_Delete(cJSON_DetachItemFromObject(object, string)); +} + +CJSON_PUBLIC(void) cJSON_DeleteItemFromObjectCaseSensitive(cJSON *object, const char *string) +{ + cJSON_Delete(cJSON_DetachItemFromObjectCaseSensitive(object, string)); +} + +/* Replace array/object items with new ones. */ +CJSON_PUBLIC(cJSON_bool) cJSON_InsertItemInArray(cJSON *array, int which, cJSON *newitem) +{ + cJSON *after_inserted = NULL; + + if (which < 0) + { + return false; + } + + after_inserted = get_array_item(array, (size_t)which); + if (after_inserted == NULL) + { + return add_item_to_array(array, newitem); + } + + newitem->next = after_inserted; + newitem->prev = after_inserted->prev; + after_inserted->prev = newitem; + if (after_inserted == array->child) + { + array->child = newitem; + } + else + { + newitem->prev->next = newitem; + } + return true; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemViaPointer(cJSON * const parent, cJSON * const item, cJSON * replacement) +{ + if ((parent == NULL) || (replacement == NULL) || (item == NULL)) + { + return false; + } + + if (replacement == item) + { + return true; + } + + replacement->next = item->next; + replacement->prev = item->prev; + + if (replacement->next != NULL) + { + replacement->next->prev = replacement; + } + if (parent->child == item) + { + if (parent->child->prev == parent->child) + { + replacement->prev = replacement; + } + parent->child = replacement; + } + else + { /* + * To find the last item in array quickly, we use prev in array. + * We can't modify the last item's next pointer where this item was the parent's child + */ + if (replacement->prev != NULL) + { + replacement->prev->next = replacement; + } + if (replacement->next == NULL) + { + parent->child->prev = replacement; + } + } + + item->next = NULL; + item->prev = NULL; + cJSON_Delete(item); + + return true; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInArray(cJSON *array, int which, cJSON *newitem) +{ + if (which < 0) + { + return false; + } + + return cJSON_ReplaceItemViaPointer(array, get_array_item(array, (size_t)which), newitem); +} + +static cJSON_bool replace_item_in_object(cJSON *object, const char *string, cJSON *replacement, cJSON_bool case_sensitive) +{ + if ((replacement == NULL) || (string == NULL)) + { + return false; + } + + /* replace the name in the replacement */ + if (!(replacement->type & cJSON_StringIsConst) && (replacement->string != NULL)) + { + cJSON_free(replacement->string); + } + replacement->string = (char*)cJSON_strdup((const unsigned char*)string, &global_hooks); + if (replacement->string == NULL) + { + return false; + } + + replacement->type &= ~cJSON_StringIsConst; + + return cJSON_ReplaceItemViaPointer(object, get_object_item(object, string, case_sensitive), replacement); +} + +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObject(cJSON *object, const char *string, cJSON *newitem) +{ + return replace_item_in_object(object, string, newitem, false); +} + +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObjectCaseSensitive(cJSON *object, const char *string, cJSON *newitem) +{ + return replace_item_in_object(object, string, newitem, true); +} + +/* Create basic types: */ +CJSON_PUBLIC(cJSON *) cJSON_CreateNull(void) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type = cJSON_NULL; + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateTrue(void) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type = cJSON_True; + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateFalse(void) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type = cJSON_False; + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateBool(cJSON_bool boolean) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type = boolean ? cJSON_True : cJSON_False; + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateNumber(double num) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type = cJSON_Number; + item->valuedouble = num; + + /* use saturation in case of overflow */ + if (num >= INT_MAX) + { + item->valueint = INT_MAX; + } + else if (num <= (double)INT_MIN) + { + item->valueint = INT_MIN; + } + else + { + item->valueint = (int)num; + } + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateString(const char *string) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type = cJSON_String; + item->valuestring = (char*)cJSON_strdup((const unsigned char*)string, &global_hooks); + if(!item->valuestring) + { + cJSON_Delete(item); + return NULL; + } + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateStringReference(const char *string) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if (item != NULL) + { + item->type = cJSON_String | cJSON_IsReference; + item->valuestring = (char*)cast_away_const(string); + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateObjectReference(const cJSON *child) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if (item != NULL) { + item->type = cJSON_Object | cJSON_IsReference; + item->child = (cJSON*)cast_away_const(child); + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateArrayReference(const cJSON *child) { + cJSON *item = cJSON_New_Item(&global_hooks); + if (item != NULL) { + item->type = cJSON_Array | cJSON_IsReference; + item->child = (cJSON*)cast_away_const(child); + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateRaw(const char *raw) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type = cJSON_Raw; + item->valuestring = (char*)cJSON_strdup((const unsigned char*)raw, &global_hooks); + if(!item->valuestring) + { + cJSON_Delete(item); + return NULL; + } + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateArray(void) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type=cJSON_Array; + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateObject(void) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if (item) + { + item->type = cJSON_Object; + } + + return item; +} + +/* Create Arrays: */ +CJSON_PUBLIC(cJSON *) cJSON_CreateIntArray(const int *numbers, int count) +{ + size_t i = 0; + cJSON *n = NULL; + cJSON *p = NULL; + cJSON *a = NULL; + + if ((count < 0) || (numbers == NULL)) + { + return NULL; + } + + a = cJSON_CreateArray(); + + for(i = 0; a && (i < (size_t)count); i++) + { + n = cJSON_CreateNumber(numbers[i]); + if (!n) + { + cJSON_Delete(a); + return NULL; + } + if(!i) + { + a->child = n; + } + else + { + suffix_object(p, n); + } + p = n; + } + + if (a && a->child) { + a->child->prev = n; + } + + return a; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateFloatArray(const float *numbers, int count) +{ + size_t i = 0; + cJSON *n = NULL; + cJSON *p = NULL; + cJSON *a = NULL; + + if ((count < 0) || (numbers == NULL)) + { + return NULL; + } + + a = cJSON_CreateArray(); + + for(i = 0; a && (i < (size_t)count); i++) + { + n = cJSON_CreateNumber((double)numbers[i]); + if(!n) + { + cJSON_Delete(a); + return NULL; + } + if(!i) + { + a->child = n; + } + else + { + suffix_object(p, n); + } + p = n; + } + + if (a && a->child) { + a->child->prev = n; + } + + return a; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateDoubleArray(const double *numbers, int count) +{ + size_t i = 0; + cJSON *n = NULL; + cJSON *p = NULL; + cJSON *a = NULL; + + if ((count < 0) || (numbers == NULL)) + { + return NULL; + } + + a = cJSON_CreateArray(); + + for(i = 0; a && (i < (size_t)count); i++) + { + n = cJSON_CreateNumber(numbers[i]); + if(!n) + { + cJSON_Delete(a); + return NULL; + } + if(!i) + { + a->child = n; + } + else + { + suffix_object(p, n); + } + p = n; + } + + if (a && a->child) { + a->child->prev = n; + } + + return a; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateStringArray(const char *const *strings, int count) +{ + size_t i = 0; + cJSON *n = NULL; + cJSON *p = NULL; + cJSON *a = NULL; + + if ((count < 0) || (strings == NULL)) + { + return NULL; + } + + a = cJSON_CreateArray(); + + for (i = 0; a && (i < (size_t)count); i++) + { + n = cJSON_CreateString(strings[i]); + if(!n) + { + cJSON_Delete(a); + return NULL; + } + if(!i) + { + a->child = n; + } + else + { + suffix_object(p,n); + } + p = n; + } + + if (a && a->child) { + a->child->prev = n; + } + + return a; +} + +/* Duplication */ +CJSON_PUBLIC(cJSON *) cJSON_Duplicate(const cJSON *item, cJSON_bool recurse) +{ + cJSON *newitem = NULL; + cJSON *child = NULL; + cJSON *next = NULL; + cJSON *newchild = NULL; + + /* Bail on bad ptr */ + if (!item) + { + goto fail; + } + /* Create new item */ + newitem = cJSON_New_Item(&global_hooks); + if (!newitem) + { + goto fail; + } + /* Copy over all vars */ + newitem->type = item->type & (~cJSON_IsReference); + newitem->valueint = item->valueint; + newitem->valuedouble = item->valuedouble; + if (item->valuestring) + { + newitem->valuestring = (char*)cJSON_strdup((unsigned char*)item->valuestring, &global_hooks); + if (!newitem->valuestring) + { + goto fail; + } + } + if (item->string) + { + newitem->string = (item->type&cJSON_StringIsConst) ? item->string : (char*)cJSON_strdup((unsigned char*)item->string, &global_hooks); + if (!newitem->string) + { + goto fail; + } + } + /* If non-recursive, then we're done! */ + if (!recurse) + { + return newitem; + } + /* Walk the ->next chain for the child. */ + child = item->child; + while (child != NULL) + { + newchild = cJSON_Duplicate(child, true); /* Duplicate (with recurse) each item in the ->next chain */ + if (!newchild) + { + goto fail; + } + if (next != NULL) + { + /* If newitem->child already set, then crosswire ->prev and ->next and move on */ + next->next = newchild; + newchild->prev = next; + next = newchild; + } + else + { + /* Set newitem->child and move to it */ + newitem->child = newchild; + next = newchild; + } + child = child->next; + } + if (newitem && newitem->child) + { + newitem->child->prev = newchild; + } + + return newitem; + +fail: + if (newitem != NULL) + { + cJSON_Delete(newitem); + } + + return NULL; +} + +static void skip_oneline_comment(char **input) +{ + *input += static_strlen("//"); + + for (; (*input)[0] != '\0'; ++(*input)) + { + if ((*input)[0] == '\n') { + *input += static_strlen("\n"); + return; + } + } +} + +static void skip_multiline_comment(char **input) +{ + *input += static_strlen("/*"); + + for (; (*input)[0] != '\0'; ++(*input)) + { + if (((*input)[0] == '*') && ((*input)[1] == '/')) + { + *input += static_strlen("*/"); + return; + } + } +} + +static void minify_string(char **input, char **output) { + (*output)[0] = (*input)[0]; + *input += static_strlen("\""); + *output += static_strlen("\""); + + + for (; (*input)[0] != '\0'; (void)++(*input), ++(*output)) { + (*output)[0] = (*input)[0]; + + if ((*input)[0] == '\"') { + (*output)[0] = '\"'; + *input += static_strlen("\""); + *output += static_strlen("\""); + return; + } else if (((*input)[0] == '\\') && ((*input)[1] == '\"')) { + (*output)[1] = (*input)[1]; + *input += static_strlen("\""); + *output += static_strlen("\""); + } + } +} + +CJSON_PUBLIC(void) cJSON_Minify(char *json) +{ + char *into = json; + + if (json == NULL) + { + return; + } + + while (json[0] != '\0') + { + switch (json[0]) + { + case ' ': + case '\t': + case '\r': + case '\n': + json++; + break; + + case '/': + if (json[1] == '/') + { + skip_oneline_comment(&json); + } + else if (json[1] == '*') + { + skip_multiline_comment(&json); + } else { + json++; + } + break; + + case '\"': + minify_string(&json, (char**)&into); + break; + + default: + into[0] = json[0]; + json++; + into++; + } + } + + /* and null-terminate. */ + *into = '\0'; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsInvalid(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_Invalid; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsFalse(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_False; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsTrue(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xff) == cJSON_True; +} + + +CJSON_PUBLIC(cJSON_bool) cJSON_IsBool(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & (cJSON_True | cJSON_False)) != 0; +} +CJSON_PUBLIC(cJSON_bool) cJSON_IsNull(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_NULL; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsNumber(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_Number; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsString(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_String; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsArray(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_Array; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsObject(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_Object; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsRaw(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_Raw; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_Compare(const cJSON * const a, const cJSON * const b, const cJSON_bool case_sensitive) +{ + if ((a == NULL) || (b == NULL) || ((a->type & 0xFF) != (b->type & 0xFF))) + { + return false; + } + + /* check if type is valid */ + switch (a->type & 0xFF) + { + case cJSON_False: + case cJSON_True: + case cJSON_NULL: + case cJSON_Number: + case cJSON_String: + case cJSON_Raw: + case cJSON_Array: + case cJSON_Object: + break; + + default: + return false; + } + + /* identical objects are equal */ + if (a == b) + { + return true; + } + + switch (a->type & 0xFF) + { + /* in these cases and equal type is enough */ + case cJSON_False: + case cJSON_True: + case cJSON_NULL: + return true; + + case cJSON_Number: + if (compare_double(a->valuedouble, b->valuedouble)) + { + return true; + } + return false; + + case cJSON_String: + case cJSON_Raw: + if ((a->valuestring == NULL) || (b->valuestring == NULL)) + { + return false; + } + if (strcmp(a->valuestring, b->valuestring) == 0) + { + return true; + } + + return false; + + case cJSON_Array: + { + cJSON *a_element = a->child; + cJSON *b_element = b->child; + + for (; (a_element != NULL) && (b_element != NULL);) + { + if (!cJSON_Compare(a_element, b_element, case_sensitive)) + { + return false; + } + + a_element = a_element->next; + b_element = b_element->next; + } + + /* one of the arrays is longer than the other */ + if (a_element != b_element) { + return false; + } + + return true; + } + + case cJSON_Object: + { + cJSON *a_element = NULL; + cJSON *b_element = NULL; + cJSON_ArrayForEach(a_element, a) + { + /* TODO This has O(n^2) runtime, which is horrible! */ + b_element = get_object_item(b, a_element->string, case_sensitive); + if (b_element == NULL) + { + return false; + } + + if (!cJSON_Compare(a_element, b_element, case_sensitive)) + { + return false; + } + } + + /* doing this twice, once on a and b to prevent true comparison if a subset of b + * TODO: Do this the proper way, this is just a fix for now */ + cJSON_ArrayForEach(b_element, b) + { + a_element = get_object_item(a, b_element->string, case_sensitive); + if (a_element == NULL) + { + return false; + } + + if (!cJSON_Compare(b_element, a_element, case_sensitive)) + { + return false; + } + } + + return true; + } + + default: + return false; + } +} + +CJSON_PUBLIC(void *) cJSON_malloc(size_t size) +{ + return global_hooks.allocate(size); +} + +CJSON_PUBLIC(void) cJSON_free(void *object) +{ + global_hooks.deallocate(object); +} diff --git a/lldp-poe/lib/cJSON/cJSON.h b/lldp-poe/lib/cJSON/cJSON.h new file mode 100644 index 0000000..95a9cf6 --- /dev/null +++ b/lldp-poe/lib/cJSON/cJSON.h @@ -0,0 +1,300 @@ +/* + Copyright (c) 2009-2017 Dave Gamble and cJSON contributors + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +#ifndef cJSON__h +#define cJSON__h + +#ifdef __cplusplus +extern "C" +{ +#endif + +#if !defined(__WINDOWS__) && (defined(WIN32) || defined(WIN64) || defined(_MSC_VER) || defined(_WIN32)) +#define __WINDOWS__ +#endif + +#ifdef __WINDOWS__ + +/* When compiling for windows, we specify a specific calling convention to avoid issues where we are being called from a project with a different default calling convention. For windows you have 3 define options: + +CJSON_HIDE_SYMBOLS - Define this in the case where you don't want to ever dllexport symbols +CJSON_EXPORT_SYMBOLS - Define this on library build when you want to dllexport symbols (default) +CJSON_IMPORT_SYMBOLS - Define this if you want to dllimport symbol + +For *nix builds that support visibility attribute, you can define similar behavior by + +setting default visibility to hidden by adding +-fvisibility=hidden (for gcc) +or +-xldscope=hidden (for sun cc) +to CFLAGS + +then using the CJSON_API_VISIBILITY flag to "export" the same symbols the way CJSON_EXPORT_SYMBOLS does + +*/ + +#define CJSON_CDECL __cdecl +#define CJSON_STDCALL __stdcall + +/* export symbols by default, this is necessary for copy pasting the C and header file */ +#if !defined(CJSON_HIDE_SYMBOLS) && !defined(CJSON_IMPORT_SYMBOLS) && !defined(CJSON_EXPORT_SYMBOLS) +#define CJSON_EXPORT_SYMBOLS +#endif + +#if defined(CJSON_HIDE_SYMBOLS) +#define CJSON_PUBLIC(type) type CJSON_STDCALL +#elif defined(CJSON_EXPORT_SYMBOLS) +#define CJSON_PUBLIC(type) __declspec(dllexport) type CJSON_STDCALL +#elif defined(CJSON_IMPORT_SYMBOLS) +#define CJSON_PUBLIC(type) __declspec(dllimport) type CJSON_STDCALL +#endif +#else /* !__WINDOWS__ */ +#define CJSON_CDECL +#define CJSON_STDCALL + +#if (defined(__GNUC__) || defined(__SUNPRO_CC) || defined (__SUNPRO_C)) && defined(CJSON_API_VISIBILITY) +#define CJSON_PUBLIC(type) __attribute__((visibility("default"))) type +#else +#define CJSON_PUBLIC(type) type +#endif +#endif + +/* project version */ +#define CJSON_VERSION_MAJOR 1 +#define CJSON_VERSION_MINOR 7 +#define CJSON_VERSION_PATCH 15 + +#include + +/* cJSON Types: */ +#define cJSON_Invalid (0) +#define cJSON_False (1 << 0) +#define cJSON_True (1 << 1) +#define cJSON_NULL (1 << 2) +#define cJSON_Number (1 << 3) +#define cJSON_String (1 << 4) +#define cJSON_Array (1 << 5) +#define cJSON_Object (1 << 6) +#define cJSON_Raw (1 << 7) /* raw json */ + +#define cJSON_IsReference 256 +#define cJSON_StringIsConst 512 + +/* The cJSON structure: */ +typedef struct cJSON +{ + /* next/prev allow you to walk array/object chains. Alternatively, use GetArraySize/GetArrayItem/GetObjectItem */ + struct cJSON *next; + struct cJSON *prev; + /* An array or object item will have a child pointer pointing to a chain of the items in the array/object. */ + struct cJSON *child; + + /* The type of the item, as above. */ + int type; + + /* The item's string, if type==cJSON_String and type == cJSON_Raw */ + char *valuestring; + /* writing to valueint is DEPRECATED, use cJSON_SetNumberValue instead */ + int valueint; + /* The item's number, if type==cJSON_Number */ + double valuedouble; + + /* The item's name string, if this item is the child of, or is in the list of subitems of an object. */ + char *string; +} cJSON; + +typedef struct cJSON_Hooks +{ + /* malloc/free are CDECL on Windows regardless of the default calling convention of the compiler, so ensure the hooks allow passing those functions directly. */ + void *(CJSON_CDECL *malloc_fn)(size_t sz); + void (CJSON_CDECL *free_fn)(void *ptr); +} cJSON_Hooks; + +typedef int cJSON_bool; + +/* Limits how deeply nested arrays/objects can be before cJSON rejects to parse them. + * This is to prevent stack overflows. */ +#ifndef CJSON_NESTING_LIMIT +#define CJSON_NESTING_LIMIT 1000 +#endif + +/* returns the version of cJSON as a string */ +CJSON_PUBLIC(const char*) cJSON_Version(void); + +/* Supply malloc, realloc and free functions to cJSON */ +CJSON_PUBLIC(void) cJSON_InitHooks(cJSON_Hooks* hooks); + +/* Memory Management: the caller is always responsible to free the results from all variants of cJSON_Parse (with cJSON_Delete) and cJSON_Print (with stdlib free, cJSON_Hooks.free_fn, or cJSON_free as appropriate). The exception is cJSON_PrintPreallocated, where the caller has full responsibility of the buffer. */ +/* Supply a block of JSON, and this returns a cJSON object you can interrogate. */ +CJSON_PUBLIC(cJSON *) cJSON_Parse(const char *value); +CJSON_PUBLIC(cJSON *) cJSON_ParseWithLength(const char *value, size_t buffer_length); +/* ParseWithOpts allows you to require (and check) that the JSON is null terminated, and to retrieve the pointer to the final byte parsed. */ +/* If you supply a ptr in return_parse_end and parsing fails, then return_parse_end will contain a pointer to the error so will match cJSON_GetErrorPtr(). */ +CJSON_PUBLIC(cJSON *) cJSON_ParseWithOpts(const char *value, const char **return_parse_end, cJSON_bool require_null_terminated); +CJSON_PUBLIC(cJSON *) cJSON_ParseWithLengthOpts(const char *value, size_t buffer_length, const char **return_parse_end, cJSON_bool require_null_terminated); + +/* Render a cJSON entity to text for transfer/storage. */ +CJSON_PUBLIC(char *) cJSON_Print(const cJSON *item); +/* Render a cJSON entity to text for transfer/storage without any formatting. */ +CJSON_PUBLIC(char *) cJSON_PrintUnformatted(const cJSON *item); +/* Render a cJSON entity to text using a buffered strategy. prebuffer is a guess at the final size. guessing well reduces reallocation. fmt=0 gives unformatted, =1 gives formatted */ +CJSON_PUBLIC(char *) cJSON_PrintBuffered(const cJSON *item, int prebuffer, cJSON_bool fmt); +/* Render a cJSON entity to text using a buffer already allocated in memory with given length. Returns 1 on success and 0 on failure. */ +/* NOTE: cJSON is not always 100% accurate in estimating how much memory it will use, so to be safe allocate 5 bytes more than you actually need */ +CJSON_PUBLIC(cJSON_bool) cJSON_PrintPreallocated(cJSON *item, char *buffer, const int length, const cJSON_bool format); +/* Delete a cJSON entity and all subentities. */ +CJSON_PUBLIC(void) cJSON_Delete(cJSON *item); + +/* Returns the number of items in an array (or object). */ +CJSON_PUBLIC(int) cJSON_GetArraySize(const cJSON *array); +/* Retrieve item number "index" from array "array". Returns NULL if unsuccessful. */ +CJSON_PUBLIC(cJSON *) cJSON_GetArrayItem(const cJSON *array, int index); +/* Get item "string" from object. Case insensitive. */ +CJSON_PUBLIC(cJSON *) cJSON_GetObjectItem(const cJSON * const object, const char * const string); +CJSON_PUBLIC(cJSON *) cJSON_GetObjectItemCaseSensitive(const cJSON * const object, const char * const string); +CJSON_PUBLIC(cJSON_bool) cJSON_HasObjectItem(const cJSON *object, const char *string); +/* For analysing failed parses. This returns a pointer to the parse error. You'll probably need to look a few chars back to make sense of it. Defined when cJSON_Parse() returns 0. 0 when cJSON_Parse() succeeds. */ +CJSON_PUBLIC(const char *) cJSON_GetErrorPtr(void); + +/* Check item type and return its value */ +CJSON_PUBLIC(char *) cJSON_GetStringValue(const cJSON * const item); +CJSON_PUBLIC(double) cJSON_GetNumberValue(const cJSON * const item); + +/* These functions check the type of an item */ +CJSON_PUBLIC(cJSON_bool) cJSON_IsInvalid(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsFalse(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsTrue(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsBool(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsNull(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsNumber(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsString(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsArray(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsObject(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsRaw(const cJSON * const item); + +/* These calls create a cJSON item of the appropriate type. */ +CJSON_PUBLIC(cJSON *) cJSON_CreateNull(void); +CJSON_PUBLIC(cJSON *) cJSON_CreateTrue(void); +CJSON_PUBLIC(cJSON *) cJSON_CreateFalse(void); +CJSON_PUBLIC(cJSON *) cJSON_CreateBool(cJSON_bool boolean); +CJSON_PUBLIC(cJSON *) cJSON_CreateNumber(double num); +CJSON_PUBLIC(cJSON *) cJSON_CreateString(const char *string); +/* raw json */ +CJSON_PUBLIC(cJSON *) cJSON_CreateRaw(const char *raw); +CJSON_PUBLIC(cJSON *) cJSON_CreateArray(void); +CJSON_PUBLIC(cJSON *) cJSON_CreateObject(void); + +/* Create a string where valuestring references a string so + * it will not be freed by cJSON_Delete */ +CJSON_PUBLIC(cJSON *) cJSON_CreateStringReference(const char *string); +/* Create an object/array that only references it's elements so + * they will not be freed by cJSON_Delete */ +CJSON_PUBLIC(cJSON *) cJSON_CreateObjectReference(const cJSON *child); +CJSON_PUBLIC(cJSON *) cJSON_CreateArrayReference(const cJSON *child); + +/* These utilities create an Array of count items. + * The parameter count cannot be greater than the number of elements in the number array, otherwise array access will be out of bounds.*/ +CJSON_PUBLIC(cJSON *) cJSON_CreateIntArray(const int *numbers, int count); +CJSON_PUBLIC(cJSON *) cJSON_CreateFloatArray(const float *numbers, int count); +CJSON_PUBLIC(cJSON *) cJSON_CreateDoubleArray(const double *numbers, int count); +CJSON_PUBLIC(cJSON *) cJSON_CreateStringArray(const char *const *strings, int count); + +/* Append item to the specified array/object. */ +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToArray(cJSON *array, cJSON *item); +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObject(cJSON *object, const char *string, cJSON *item); +/* Use this when string is definitely const (i.e. a literal, or as good as), and will definitely survive the cJSON object. + * WARNING: When this function was used, make sure to always check that (item->type & cJSON_StringIsConst) is zero before + * writing to `item->string` */ +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObjectCS(cJSON *object, const char *string, cJSON *item); +/* Append reference to item to the specified array/object. Use this when you want to add an existing cJSON to a new cJSON, but don't want to corrupt your existing cJSON. */ +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToArray(cJSON *array, cJSON *item); +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToObject(cJSON *object, const char *string, cJSON *item); + +/* Remove/Detach items from Arrays/Objects. */ +CJSON_PUBLIC(cJSON *) cJSON_DetachItemViaPointer(cJSON *parent, cJSON * const item); +CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromArray(cJSON *array, int which); +CJSON_PUBLIC(void) cJSON_DeleteItemFromArray(cJSON *array, int which); +CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObject(cJSON *object, const char *string); +CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObjectCaseSensitive(cJSON *object, const char *string); +CJSON_PUBLIC(void) cJSON_DeleteItemFromObject(cJSON *object, const char *string); +CJSON_PUBLIC(void) cJSON_DeleteItemFromObjectCaseSensitive(cJSON *object, const char *string); + +/* Update array items. */ +CJSON_PUBLIC(cJSON_bool) cJSON_InsertItemInArray(cJSON *array, int which, cJSON *newitem); /* Shifts pre-existing items to the right. */ +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemViaPointer(cJSON * const parent, cJSON * const item, cJSON * replacement); +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInArray(cJSON *array, int which, cJSON *newitem); +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObject(cJSON *object,const char *string,cJSON *newitem); +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObjectCaseSensitive(cJSON *object,const char *string,cJSON *newitem); + +/* Duplicate a cJSON item */ +CJSON_PUBLIC(cJSON *) cJSON_Duplicate(const cJSON *item, cJSON_bool recurse); +/* Duplicate will create a new, identical cJSON item to the one you pass, in new memory that will + * need to be released. With recurse!=0, it will duplicate any children connected to the item. + * The item->next and ->prev pointers are always zero on return from Duplicate. */ +/* Recursively compare two cJSON items for equality. If either a or b is NULL or invalid, they will be considered unequal. + * case_sensitive determines if object keys are treated case sensitive (1) or case insensitive (0) */ +CJSON_PUBLIC(cJSON_bool) cJSON_Compare(const cJSON * const a, const cJSON * const b, const cJSON_bool case_sensitive); + +/* Minify a strings, remove blank characters(such as ' ', '\t', '\r', '\n') from strings. + * The input pointer json cannot point to a read-only address area, such as a string constant, + * but should point to a readable and writable address area. */ +CJSON_PUBLIC(void) cJSON_Minify(char *json); + +/* Helper functions for creating and adding items to an object at the same time. + * They return the added item or NULL on failure. */ +CJSON_PUBLIC(cJSON*) cJSON_AddNullToObject(cJSON * const object, const char * const name); +CJSON_PUBLIC(cJSON*) cJSON_AddTrueToObject(cJSON * const object, const char * const name); +CJSON_PUBLIC(cJSON*) cJSON_AddFalseToObject(cJSON * const object, const char * const name); +CJSON_PUBLIC(cJSON*) cJSON_AddBoolToObject(cJSON * const object, const char * const name, const cJSON_bool boolean); +CJSON_PUBLIC(cJSON*) cJSON_AddNumberToObject(cJSON * const object, const char * const name, const double number); +CJSON_PUBLIC(cJSON*) cJSON_AddStringToObject(cJSON * const object, const char * const name, const char * const string); +CJSON_PUBLIC(cJSON*) cJSON_AddRawToObject(cJSON * const object, const char * const name, const char * const raw); +CJSON_PUBLIC(cJSON*) cJSON_AddObjectToObject(cJSON * const object, const char * const name); +CJSON_PUBLIC(cJSON*) cJSON_AddArrayToObject(cJSON * const object, const char * const name); + +/* When assigning an integer value, it needs to be propagated to valuedouble too. */ +#define cJSON_SetIntValue(object, number) ((object) ? (object)->valueint = (object)->valuedouble = (number) : (number)) +/* helper for the cJSON_SetNumberValue macro */ +CJSON_PUBLIC(double) cJSON_SetNumberHelper(cJSON *object, double number); +#define cJSON_SetNumberValue(object, number) ((object != NULL) ? cJSON_SetNumberHelper(object, (double)number) : (number)) +/* Change the valuestring of a cJSON_String object, only takes effect when type of object is cJSON_String */ +CJSON_PUBLIC(char*) cJSON_SetValuestring(cJSON *object, const char *valuestring); + +/* If the object is not a boolean type this does nothing and returns cJSON_Invalid else it returns the new type*/ +#define cJSON_SetBoolValue(object, boolValue) ( \ + (object != NULL && ((object)->type & (cJSON_False|cJSON_True))) ? \ + (object)->type=((object)->type &(~(cJSON_False|cJSON_True)))|((boolValue)?cJSON_True:cJSON_False) : \ + cJSON_Invalid\ +) + +/* Macro for iterating over an array or object */ +#define cJSON_ArrayForEach(element, array) for(element = (array != NULL) ? (array)->child : NULL; element != NULL; element = element->next) + +/* malloc/free objects using the malloc/free functions that have been set with cJSON_InitHooks */ +CJSON_PUBLIC(void *) cJSON_malloc(size_t size); +CJSON_PUBLIC(void) cJSON_free(void *object); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/lldp-poe/lib/systemd/system/lldp-poed-config.service b/lldp-poe/lib/systemd/system/lldp-poed-config.service new file mode 100644 index 0000000..f484cc7 --- /dev/null +++ b/lldp-poe/lib/systemd/system/lldp-poed-config.service @@ -0,0 +1,11 @@ +[Unit] +Description=DentOS LLDP POE config oneshot +After=local-fs.target + +[Service] +Type=oneshot +ExecStart=/usr/bin/lldp-poed-config +RemainAfterExit=true + +[Install] +WantedBy=multi-user.target diff --git a/lldp-poe/lib/systemd/system/lldp-poed.service b/lldp-poe/lib/systemd/system/lldp-poed.service new file mode 100644 index 0000000..3892783 --- /dev/null +++ b/lldp-poe/lib/systemd/system/lldp-poed.service @@ -0,0 +1,14 @@ +[Unit] +Description=DentOS LLDP POE Agent +After=syslog.service lldpd.service ntp.service lldp-poed-config.service +Requires=lldp-poed-config.service +ConditionPathExists=/var/run/lldp-poed-enable +BindsTo=lldpd.service + +[Service] +Type=simple +Restart=always +ExecStart=/usr/sbin/lldp-poed + +[Install] +WantedBy=multi-user.target diff --git a/lldp-poe/res/lldp_port_state_machine.png b/lldp-poe/res/lldp_port_state_machine.png new file mode 100644 index 0000000..0579b2f Binary files /dev/null and b/lldp-poe/res/lldp_port_state_machine.png differ diff --git a/lldp-poe/src/common.c b/lldp-poe/src/common.c new file mode 100644 index 0000000..e99418b --- /dev/null +++ b/lldp-poe/src/common.c @@ -0,0 +1,39 @@ +/** + * Copyright Amazon Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +#include +#include +#include +#include + +#include "cJSON/cJSON.h" +#include "include/common.h" +#include "include/lldp_poed_err.h" +#include "include/logger.h" + +/** + * publish_metrics - Send a metric formatted as JSON through + * the metrics FIFO + * @metric_name: the metric key used for identification + * @metric_value: the metric value + * @port_id: port id associated to the metric. Should be set to 0 if no port. + * + * Returns 0, LLDP_POED_ERR_OK, if successful, an lldp_poed_err otherwise. + */ +int publish_metrics(const char *metric_name, int metric_value, int port_id) +{ + return LLDP_POED_ERR_INTERNAL_ERROR; +} \ No newline at end of file diff --git a/lldp-poe/src/lldp_event_handler.c b/lldp-poe/src/lldp_event_handler.c new file mode 100644 index 0000000..3e53575 --- /dev/null +++ b/lldp-poe/src/lldp_event_handler.c @@ -0,0 +1,861 @@ +/** + * Copyright Amazon Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +#include +#include +#include +#include +#include +#include +#include + +#include "include/common.h" +#include "include/lldp_event_handler.h" +#include "include/lldp_poed_err.h" +#include "include/logger.h" + +static lldpctl_conn_t *lldpctl_conn = NULL; + +/** + * get_local_ttl - Compute the TTL used for outgoing LLDPDUs. + * @ttl: computed TTL value + * + * Returns 0 if successful, 1 otherwise. + */ +static int get_local_ttl(int *ttl) +{ + if (!ttl) + return 1; + + lldpctl_atom_t *config = lldpctl_get_configuration(lldpctl_conn); + if (!config) { + POE_ERR("Failed to get the global lldpd config: %s", + lldpctl_last_strerror(lldpctl_conn)); + return 1; + } + int tx_hold = lldpctl_atom_get_int(config, lldpctl_k_config_tx_hold); + /* lldpctl_k_config_tx_interval counterpart is derived from this one */ + int tx_interval = + lldpctl_atom_get_int(config, lldpctl_k_config_tx_interval_ms); + + /** + * Output seconds, rounding to the next second. + */ + *ttl = ((((long) tx_interval) * tx_hold) + 999) / 1000; + + lldpctl_atom_dec_ref(config); + + return 0; +} + +/** + * read_med_power - Read all MED atom keys for the given port + * @port: port atom to read from + * @config: output config values + * @ifname: network interface name associated to the remote port + * + * The ANSI/TIA-1057 (LLDP-MED) are required. If either one is absent, + * return an error. + * + * Returns 0, LLDP_POED_ERR_OK, if successful, an error_code otherwise. + */ +static int read_med_power(lldpctl_atom_t *port, + struct port_med_power_settings *config, + const char *ifname) +{ + if (!port || !config || !ifname) + return LLDP_POED_ERR_INVALID_PARAM; + + lldpctl_atom_t *med_power = + lldpctl_atom_get(port, lldpctl_k_port_med_power); + if (!med_power) { + POE_ERR("Unable to retrieve the MED power atom for " + "%s: %s", + ifname, lldpctl_last_strerror(lldpctl_conn)); + return LLDP_POED_ERR_MED_POWER_ATOM_FAILED; + } + + int status = LLDP_POED_ERR_OK; + config->poe_device_type = + lldpctl_atom_get_int(med_power, lldpctl_k_med_power_type); + if (LLDP_MED_POW_TYPE_PD != config->poe_device_type) { + POE_ERR("LLDP MED PoE device type is not " + "valid for %s", + ifname); + status = LLDP_POED_ERR_MED_POWER_ATOM_FAILED; + goto fail; + } + + /** + * Read ANSI/TIA-1057 (LLDP-MED) fields, if available (given the Power Type field). + */ + config->power_source = + lldpctl_atom_get_int(med_power, lldpctl_k_med_power_source); + config->power_priority = + lldpctl_atom_get_int(med_power, lldpctl_k_med_power_priority); + /** + * @warning: for some reason, liblldpctl will work in mW, + * instead of 0.1W (as recommended by the standard). + * Therefore, convert to 0.1W for interop with the PoE controller, + * which is 802.3at/bt compliant. + */ + config->value = + lldpctl_atom_get_int(med_power, lldpctl_k_med_power_val) / 100; + + POE_INFO("Successfully read LLDP MED power for %s", ifname); + lldpctl_atom_dec_ref(med_power); + + return status; + +fail: + lldpctl_atom_dec_ref(med_power); + return status; +} + +/** + * read_dot3_power - Read all Dot3 atom keys for the given port + * @port: port atom to read from + * @config: output config values + * @ifname: network interface name associated to the remote port + * + * The 802.1ab and 802.3at fields are required. If either one is absent, + * return an error. + * + * Returns 0, LLDP_POED_ERR_OK, if successful, an error_code otherwise. + */ +static int read_dot3_power(lldpctl_atom_t *port, + struct port_dot3_power_settings *config, + const char *ifname) +{ + if (!port || !config || !ifname) + return LLDP_POED_ERR_INVALID_PARAM; + + lldpctl_atom_t *dot3_power = + lldpctl_atom_get(port, lldpctl_k_port_dot3_power); + if (!dot3_power) { + POE_ERR("Unable to retrieve the Dot3 power atom for " + "%s: %s", + ifname, lldpctl_last_strerror(lldpctl_conn)); + return LLDP_POED_ERR_DOT3_POWER_ATOM_FAILED; + } + + int status = LLDP_POED_ERR_OK; + config->poe_device_type = + lldpctl_atom_get_int(dot3_power, lldpctl_k_dot3_power_devicetype); + if (LLDP_DOT3_POWER_PD != config->poe_device_type) { + POE_ERR("LLDP Dot3 PoE device type is not " + "valid for %s", + ifname); + status = LLDP_POED_ERR_DOT3_POWER_ATOM_FAILED; + goto fail; + } + + /** + * 802.1ab fields that are not transmitted by a PD and hence set to -1: MDI + * power support, MDI power state, PSE pairs control and PSE power pair and + * PD power class. + */ + config->mdi_supported = -1; + config->mdi_enabled = -1; + config->mdi_paircontrol = -1; + config->pse_power_pair = -1; + config->pd_class = -1; + + /** + * Read 802.3at PoE fields, if available (given the Power Type field). + */ + config->power_type = + lldpctl_atom_get_int(dot3_power, lldpctl_k_dot3_power_type); + if (LLDP_DOT3_POWER_8023AT_OFF == config->power_type) { + POE_ERR("LLDP Dot3 DLL classification fields are not " + "available for %s", + ifname); + status = LLDP_POED_ERR_DOT3_POWER_ATOM_FAILED; + goto fail; + } + config->power_source = + lldpctl_atom_get_int(dot3_power, lldpctl_k_dot3_power_source); + config->power_priority = + lldpctl_atom_get_int(dot3_power, lldpctl_k_dot3_power_priority); + /** + * @warning: for some reason, liblldpctl will work in mW, + * instead of 0.1W (as recommended by the standard). + * Therefore, convert to 0.1W for interop with the PoE controller, + * which is 802.3at/bt compliant. + */ + config->pd_requested = + lldpctl_atom_get_int(dot3_power, lldpctl_k_dot3_power_requested) / 100; + config->pse_allocated = + lldpctl_atom_get_int(dot3_power, lldpctl_k_dot3_power_allocated) / 100; + + /** + * Read 802.3bt PoE fields, if available (given the Power Type ext field). + */ + config->power_type_ext = + lldpctl_atom_get_int(dot3_power, lldpctl_k_dot3_power_type_ext); + if (LLDP_DOT3_POWER_8023BT_OFF == config->power_type_ext) { + POE_WARN("LLDP Dot3 802.3bt fields are not available " + "for %s", + ifname); + } else { + config->pd_4pid = + lldpctl_atom_get_int(dot3_power, lldpctl_k_dot3_power_pd_4pid); + config->pd_requested_a = + lldpctl_atom_get_int(dot3_power, lldpctl_k_dot3_power_requested_a) / + 100; + config->pd_requested_b = + lldpctl_atom_get_int(dot3_power, lldpctl_k_dot3_power_requested_b) / + 100; + config->pse_allocated_a = + lldpctl_atom_get_int(dot3_power, lldpctl_k_dot3_power_allocated_a) / + 100; + config->pse_allocated_b = + lldpctl_atom_get_int(dot3_power, lldpctl_k_dot3_power_allocated_b) / + 100; + config->pse_status = + lldpctl_atom_get_int(dot3_power, lldpctl_k_dot3_power_pse_status); + config->pd_status = + lldpctl_atom_get_int(dot3_power, lldpctl_k_dot3_power_pd_status); + config->pse_pairs_ext = lldpctl_atom_get_int( + dot3_power, lldpctl_k_dot3_power_pse_pairs_ext); + config->power_class_mode_a = + lldpctl_atom_get_int(dot3_power, lldpctl_k_dot3_power_class_a); + config->power_class_mode_b = + lldpctl_atom_get_int(dot3_power, lldpctl_k_dot3_power_class_b); + config->pd_power_class_ext = + lldpctl_atom_get_int(dot3_power, lldpctl_k_dot3_power_class_ext); + config->pd_load = + lldpctl_atom_get_int(dot3_power, lldpctl_k_dot3_power_pd_load); + config->pse_max_available_power = + lldpctl_atom_get_int(dot3_power, lldpctl_k_dot3_power_pse_max); + + /** + * These fields must be set to zero by the PD, according to the 802.3bt + * standard. + */ + if (config->pse_status) { + POE_WARN("PSE powering status field " + "was not set to zero by the PD for %s", + ifname); + } else if (config->pse_pairs_ext) { + POE_WARN("PSE power pairs ext field " + "was not set to zero by the PD for %s", + ifname); + } else if (config->pse_max_available_power) { + POE_WARN("PSE maximum available power " + "value field was not set to zero by the PD for %s", + ifname); + } + } + + POE_INFO("Successfully read LLDP Dot3 power for %s", ifname); + lldpctl_atom_dec_ref(dot3_power); + + return status; + +fail: + lldpctl_atom_dec_ref(dot3_power); + return status; +} + +/** + * write_med_power - Update the port MED atom config + * @port: port atom to write to + * @config: input config values + * @ifname: network interface name associated to the remote port + * + * If writing the MED power atom is successful, this function will set the new + * MED power to the port atom, thus transmitting the update to the LLDP + * neighbor. + * + * Returns 0, LLDP_POED_ERR_OK, if successful, an error_code otherwise. + */ +static int write_med_power(lldpctl_atom_t *port, + const struct port_med_power_settings *config, + const char *ifname) +{ + if (!port || !config || !ifname) + return LLDP_POED_ERR_INVALID_PARAM; + + if (LLDP_MED_POW_TYPE_PSE != config->poe_device_type) { + POE_ERR("Invalid MED input config for %s", ifname); + return LLDP_POED_ERR_MED_POWER_ATOM_FAILED; + } + + lldpctl_atom_t *med_power = + lldpctl_atom_get(port, lldpctl_k_port_med_power); + if (!med_power) { + POE_ERR("Unable to retrieve the MED power atom for " + "%s: %s", + ifname, lldpctl_last_strerror(lldpctl_conn)); + return LLDP_POED_ERR_MED_POWER_ATOM_FAILED; + } + + int status = LLDP_POED_ERR_OK; + const char *to_set = NULL; + if (NULL == (to_set = "MED PoE device type", + lldpctl_atom_set_int(med_power, lldpctl_k_med_power_type, + config->poe_device_type)) || + NULL == (to_set = "MED Power source", + lldpctl_atom_set_int(med_power, lldpctl_k_med_power_source, + config->power_source)) || + NULL == (to_set = "MED Power priority", + lldpctl_atom_set_int(med_power, lldpctl_k_med_power_priority, + config->power_priority)) || + NULL == (to_set = "MED Power value", + lldpctl_atom_set_int(med_power, lldpctl_k_med_power_val, + config->value * 100))) { + POE_ERR("Failed to set %s for %s", to_set, ifname); + status = LLDP_POED_ERR_MED_POWER_ATOM_FAILED; + goto fail; + } + + if (lldpctl_atom_set(port, lldpctl_k_port_med_power, med_power)) { + POE_INFO("Successfully transmitted the LLDP MED " + "power settings for %s", + ifname); + lldpctl_atom_dec_ref(med_power); + } else { + POE_ERR("Failed to transmit the LLDP MED " + "power settings for %s", + ifname); + status = LLDP_POED_ERR_MED_POWER_ATOM_FAILED; + goto fail; + } + + return status; + +fail: + lldpctl_atom_dec_ref(med_power); + return status; +} + +/** + * write_dot3_power - Update the port Dot3 atom config + * @port: port atom to write to + * @config: input config values + * @ifname: network interface name associated to the remote port + * + * If writing the Dot3 power atom is successful, this function will set the new + * Dot3 power to the port atom, thus transmitting the update to the LLDP + * neighbor. + * + * Returns 0, LLDP_POED_ERR_OK, if successful, an error_code otherwise. + */ +static int write_dot3_power(lldpctl_atom_t *port, + const struct port_dot3_power_settings *config, + const char *ifname) +{ + if (!port || !config || !ifname) + return LLDP_POED_ERR_INVALID_PARAM; + + lldpctl_atom_t *dot3_power = + lldpctl_atom_get(port, lldpctl_k_port_dot3_power); + if (!dot3_power) { + POE_ERR("Unable to retrieve the Dot3 power atom for " + "%s: %s", + ifname, lldpctl_last_strerror(lldpctl_conn)); + return LLDP_POED_ERR_DOT3_POWER_ATOM_FAILED; + } + + /** + * PSE must be able to fill in all fields, at least the 802.3at ones. + */ + int status = LLDP_POED_ERR_OK; + if (LLDP_DOT3_POWER_PSE != config->poe_device_type || + LLDP_DOT3_POWER_8023AT_OFF == config->power_type) { + POE_ERR("Invalid Dot3 input config for %s", ifname); + status = LLDP_POED_ERR_DOT3_POWER_ATOM_FAILED; + goto fail; + } + + /** + * Following keys are required to be set by the PSE. If any of them fails, + * then return with error. + */ + /** + * @warning: for some reason, liblldpctl will work in mW, + * instead of 0.1W (as recommended by the standard). + * Therefore, convert to mW from 0.1W. + */ + const char *to_set = NULL; + if (NULL == + (to_set = "PoE device type", + lldpctl_atom_set_int(dot3_power, lldpctl_k_dot3_power_devicetype, + config->poe_device_type)) || + NULL == + (to_set = "MDI power support", + lldpctl_atom_set_int(dot3_power, lldpctl_k_dot3_power_supported, + config->mdi_supported)) || + NULL == (to_set = "MDI power state (enabled/disabled)", + lldpctl_atom_set_int(dot3_power, lldpctl_k_dot3_power_enabled, + config->mdi_enabled)) || + NULL == + (to_set = "PSE pair control", + lldpctl_atom_set_int(dot3_power, lldpctl_k_dot3_power_paircontrol, + config->mdi_paircontrol)) || + NULL == (to_set = "PSE power pair", + lldpctl_atom_set_int(dot3_power, lldpctl_k_dot3_power_pairs, + config->pse_power_pair)) || + NULL == (to_set = "PD power class", + lldpctl_atom_set_int(dot3_power, lldpctl_k_dot3_power_class, + config->pd_class)) || + NULL == (to_set = "Power type", + lldpctl_atom_set_int(dot3_power, lldpctl_k_dot3_power_type, + config->power_type)) || + NULL == (to_set = "Power source", + lldpctl_atom_set_int(dot3_power, lldpctl_k_dot3_power_source, + config->power_source)) || + NULL == (to_set = "Power priority", + lldpctl_atom_set_int(dot3_power, lldpctl_k_dot3_power_priority, + config->power_priority)) || + NULL == + (to_set = "PD requested power value", + lldpctl_atom_set_int(dot3_power, lldpctl_k_dot3_power_requested, + config->pd_requested * 100)) || + NULL == + (to_set = "PSE allocated power value", + lldpctl_atom_set_int(dot3_power, lldpctl_k_dot3_power_allocated, + config->pse_allocated * 100))) { + POE_ERR("Failed to set %s for %s", to_set, ifname); + status = LLDP_POED_ERR_DOT3_POWER_ATOM_FAILED; + goto fail; + } + + if (LLDP_DOT3_POWER_8023BT_OFF != config->power_type_ext) { + if (NULL == (to_set = "PD 4PID (to zero)", + lldpctl_atom_set_int(dot3_power, + lldpctl_k_dot3_power_pd_4pid, 0)) || + (USHRT_MAX != + config->pd_requested_a && /* Not set for single-signature. */ + NULL == (to_set = "PD requested power value mode A", + lldpctl_atom_set_int(dot3_power, + lldpctl_k_dot3_power_requested_a, + config->pd_requested_a * 100))) || + (USHRT_MAX != + config->pd_requested_b && /* Not set for single-signature. */ + NULL == (to_set = "PD requested power value mode B", + lldpctl_atom_set_int(dot3_power, + lldpctl_k_dot3_power_requested_b, + config->pd_requested_b * 100))) || + (USHRT_MAX != + config->pse_allocated_a && /* Not set for single-signature. */ + NULL == (to_set = "PSE allocated power value mode A", + lldpctl_atom_set_int(dot3_power, + lldpctl_k_dot3_power_allocated_a, + config->pse_allocated_a * 100))) || + (USHRT_MAX != + config->pse_allocated_b && /* Not set for single-signature. */ + NULL == (to_set = "PSE allocated power value mode B", + lldpctl_atom_set_int(dot3_power, + lldpctl_k_dot3_power_allocated_b, + config->pse_allocated_b * 100))) || + NULL == (to_set = "PSE powering status", + lldpctl_atom_set_int(dot3_power, + lldpctl_k_dot3_power_pse_status, + config->pse_status)) || + NULL == (to_set = "PD powered status (to zero)", + lldpctl_atom_set_int(dot3_power, + lldpctl_k_dot3_power_pd_status, 0)) || + NULL == (to_set = "PSE power pairs ext", + lldpctl_atom_set_int(dot3_power, + lldpctl_k_dot3_power_pse_pairs_ext, + config->pse_pairs_ext)) || + (USHRT_MAX != + config->pse_allocated_b && /* Not set for single-signature. */ + NULL == + (to_set = "Power class ext mode A", + lldpctl_atom_set_int(dot3_power, lldpctl_k_dot3_power_class_a, + config->power_class_mode_a))) || + (USHRT_MAX != + config->pse_allocated_b && /* Not set for single-signature. */ + NULL == + (to_set = "Power class ext mode B", + lldpctl_atom_set_int(dot3_power, lldpctl_k_dot3_power_class_b, + config->power_class_mode_b))) || + NULL == (to_set = "Power class ext", + lldpctl_atom_set_int(dot3_power, + lldpctl_k_dot3_power_class_ext, + config->pd_power_class_ext)) || + NULL == + (to_set = "Power type ext", + lldpctl_atom_set_int(dot3_power, lldpctl_k_dot3_power_type_ext, + config->power_type_ext)) || + NULL == (to_set = "PD load (to zero)", + lldpctl_atom_set_int(dot3_power, + lldpctl_k_dot3_power_pd_load, 0)) || + NULL == + (to_set = "PSE maximum available power value", + lldpctl_atom_set_int(dot3_power, lldpctl_k_dot3_power_pse_max, + config->pse_max_available_power))) { + POE_ERR("Failed to set %s for %s", to_set, ifname); + status = LLDP_POED_ERR_DOT3_POWER_ATOM_FAILED; + goto fail; + } + } + + if (lldpctl_atom_set(port, lldpctl_k_port_dot3_power, dot3_power)) { + POE_INFO("Successfully transmitted the LLDP Dot3 " + "power settings for %s", + ifname); + lldpctl_atom_dec_ref(dot3_power); + } else { + POE_ERR("Failed to transmit the LLDP Dot3 " + "power settings for %s", + ifname); + status = LLDP_POED_ERR_DOT3_POWER_ATOM_FAILED; + goto fail; + } + + return status; + +fail: + lldpctl_atom_dec_ref(dot3_power); + return status; +} + +/** + * get_port_atom_by_ifname - Iterate through all interfaces and return the port + * associated with the given interface name + * @ifname: network interface name + * @port: found port lldpctl atom + * + * Returns 0 if successful, 1 otherwise. + */ +static int get_port_atom_by_ifname(const char *ifname, lldpctl_atom_t **port) +{ + if (!ifname || !port) + return LLDP_POED_ERR_INVALID_PARAM; + + lldpctl_atom_t *all_ifaces = lldpctl_get_interfaces(lldpctl_conn); + if (!all_ifaces) { + POE_ERR("Failed to retrieve the " + "interfaces list: %s", + lldpctl_last_strerror(lldpctl_conn)); + return LLDP_POED_ERR_DOT3_POWER_ATOM_FAILED; + } + + lldpctl_atom_t *iface_it = NULL; + int status = LLDP_POED_ERR_DOT3_POWER_ATOM_FAILED; + lldpctl_atom_foreach(all_ifaces, iface_it) + { + if (0 != + strcasecmp(lldpctl_atom_get_str(iface_it, lldpctl_k_interface_name), + ifname)) + continue; + + /** + * Matched the port for the given ifname. + */ + *port = lldpctl_get_port(iface_it); + lldpctl_atom_dec_ref(iface_it); + status = LLDP_POED_ERR_OK; + break; + } + + lldpctl_atom_dec_ref(all_ifaces); + + return status; +} + +/** + * send_mdi_pse_advertisement - Update the local Dot3 power settings for @ifname + * and advertise it to its LLDP neighbor + * @ifname: networking interface name + * @config: LLDP Dot3 port config + * @timeout: time at which the advertised LLDPDU expires (nullable) + * + * The first call will be treated as the initial PSE MDI advertisement and, + * hence, will populate the @timeout with the current time plus the local + * configured TTL (equivalent to the value used by the neighbor to discard the + * information after it expires). For this to happen, @timeout must be a + * valid non-null reference. + * Subsequent calls, where @timeout is set to NULL, will not generate the + * timeout value again. Every call will determine the Dot3 power configuration + * to be advertised immediately to the LLDP neighbor. + * + * Returns 0, LLDP_POED_ERR_OK, if successful, an error_code otherwise. + */ +int send_mdi_pse_advertisement(const char *ifname, + const struct port_dot3_power_settings *config, + time_t *timeout) +{ + if (!ifname || !config) + return LLDP_POED_ERR_INVALID_PARAM; + + int status = LLDP_POED_ERR_OK; + lldpctl_atom_t *port = NULL; + if (LLDP_POED_ERR_OK != (status = get_port_atom_by_ifname(ifname, &port))) { + POE_ERR("Failed to find port with " + "interface name %s", + ifname); + goto done; + } + + if (LLDP_POED_ERR_OK != (status = write_dot3_power(port, config, ifname))) { + POE_ERR("Failed to update the Dot3 LLDP " + "configuration for %s", + ifname); + goto done; + } + + struct port_med_power_settings med_config; + if (LLDP_POED_ERR_OK != (status = dot3_to_med(config, &med_config))) { + POE_ERR("Failed to convert Dot3 to the MED LLDP " + "configuration for %s", + ifname); + goto done; + } + + if (LLDP_POED_ERR_OK != (status = write_med_power(port, &med_config, ifname))) { + POE_ERR("Failed to update the MED LLDP " + "configuration for %s", + ifname); + goto done; + } + + if (timeout) { + /** + * Populate the timeout as the current time + the local TTL (in + * seconds). + */ + time(timeout); + int local_ttl; + if (0 != get_local_ttl(&local_ttl)) { + POE_ERR("Failed to compute " + "the local LLDP TTL"); + goto done; + } + *timeout += local_ttl; + } + +done: + lldpctl_atom_dec_ref(port); + + return status; +} + +/** + * is_neighbor_already_reconciled - Check if the only neighbor for the given + * port has already finished the L2 negotiation. + * @ifname: networking interface name + * + * This function can be used to check if the LLDP neighbor, if exists, + * already finished the L2 negotiation by having the same value for both + * "PD requested power value" and "PSE allocated power value". + * + * Returns true if already reconciled, false otherwise. + */ +bool is_neighbor_already_reconciled(const char *ifname) +{ + if (!ifname) + return false; + + lldpctl_atom_t *port = NULL; + if (0 != get_port_atom_by_ifname(ifname, &port)) { + POE_ERR("Failed to find port with " + "interface name %s", + ifname); + return false; + } + + lldpctl_atom_t *neighbors = + lldpctl_atom_get(port, lldpctl_k_port_neighbors); + lldpctl_atom_t *neighbor_it = NULL; + bool status = false; + lldpctl_atom_foreach(neighbors, neighbor_it) + { + lldpctl_atom_t *dot3_power = + lldpctl_atom_get(neighbor_it, lldpctl_k_port_dot3_power); + if (!dot3_power) { + POE_ERR("Unable to retrieve the " + "Dot3 power atom for %s: %s", + ifname, lldpctl_last_strerror(lldpctl_conn)); + goto done; + } + if (lldpctl_atom_get_int(dot3_power, lldpctl_k_dot3_power_devicetype) > + 0 && + lldpctl_atom_get_int(dot3_power, lldpctl_k_dot3_power_type) > + LLDP_DOT3_POWER_8023AT_OFF) { + status = lldpctl_atom_get_int(dot3_power, + lldpctl_k_dot3_power_requested) == + lldpctl_atom_get_int(dot3_power, + lldpctl_k_dot3_power_allocated); + lldpctl_atom_dec_ref(dot3_power); + goto done; + } + lldpctl_atom_dec_ref(dot3_power); + break; /* Neighbor count is limited to 1 during init. */ + } + +done: + lldpctl_atom_dec_ref(neighbor_it); + lldpctl_atom_dec_ref(neighbors); + lldpctl_atom_dec_ref(port); + return status; +} + +/** + * process_neighbor_change - lldpctl watch callback + * + * This callback tracks every added/updated/deleted neighbor. For all events + * we'll push a notification to the port state machine. There is no distinction + * being made for an added neighbor as compared to an updated one. Remote ports + * that use any other protocol than standard LLDP will be ignored. + */ +static void process_neighbor_change(lldpctl_conn_t *conn, lldpctl_change_t type, + lldpctl_atom_t *interface, + lldpctl_atom_t *neighbor, void *data) +{ + const char *ifname = + lldpctl_atom_get_str(interface, lldpctl_k_interface_name); + int protocol = lldpctl_atom_get_int(neighbor, lldpctl_k_port_protocol); + if (LLDPD_MODE_LLDP != protocol) { + for (lldpctl_map_t *protocol_map = + lldpctl_key_get_map(lldpctl_k_port_age); + protocol_map->string; protocol_map++) { + if (protocol_map->value == protocol) { + POE_WARN("Unsupported neighbor " + "protocol %s for %s", + protocol_map->string, ifname); + return; + } + } + } + + switch (type) { + case lldpctl_c_added: + case lldpctl_c_updated: { + /** + * TODO: Throttle burst updates. + */ + struct port_dot3_power_settings dot3_config; + struct port_med_power_settings med_config; + int dot3_status; + + if (0 != (dot3_status = read_dot3_power(neighbor, &dot3_config, ifname))) + POE_WARN("Failed to read Dot3 power " + "settings for %s", + ifname); + + /** + * If we receive both dot3 and MED, ignore MED. + */ + if(0 != dot3_status) { + if (0 != read_med_power(neighbor, &med_config, ifname)) { + POE_WARN("Failed to read MED power " + "settings for %s", + ifname); + } else { + med_to_dot3(&med_config, &dot3_config); + } + } + + push_lldp_neighbor_update(ifname, &dot3_config); + } break; + case lldpctl_c_deleted: + push_lldp_neighbor_update(ifname, NULL); + break; + default: + POE_WARN("Unknown LLDP change event: %d", type); + return; + } +} + +/** + * forward_to_syslog - Call the log with the given severity and message + */ +static void forward_to_syslog(int severity, const char *message) +{ + POE_LOG(severity, "%s", message); +} + +/** + * handle_lldp_events - Receive and send LLDP advertisements + * + * On this thread, we'll process neighbor updates in synchronous manner by + * calling lldpctl_watch() and notifying the port state machine for all neighbor + * updates. We allow updating the LLDP Dot3 power settings for the initial MDI + * power support advertisement and for finalizing the L2 negotiation. + */ +void *handle_lldp_events() +{ + /** + * Redirect all lldpctl logs to syslog. + */ + lldpctl_log_callback(forward_to_syslog); + + /** + * Allocate two separate connections with the default synchronous callbacks. + * One is used for querying the neighbors and the other one for actively + * watching for changes. + */ + const char *ctlname = lldpctl_get_default_transport(); + lldpctl_conn = lldpctl_new_name(ctlname, NULL, NULL, NULL); + lldpctl_conn_t *watch_conn = lldpctl_new_name(ctlname, NULL, NULL, NULL); + if (!lldpctl_conn || !watch_conn) { + POE_CRIT("Failed to create an lldpctl connection"); + goto fail; + } + /** + * Check if we have a valid connection with lldpd. + */ + lldpctl_atom_t *config = lldpctl_get_configuration(watch_conn); + if (!config) { + POE_CRIT("Invalid lldpctl connection"); + goto fail; + } + if (!lldpctl_atom_set_int(config, lldpctl_k_config_max_neighbors, 1)) { + POE_CRIT("Failed to limit the maximum number of neighbors: %s", + lldpctl_last_strerror(lldpctl_conn)); + lldpctl_atom_dec_ref(config); + goto fail; + } + + lldpctl_atom_dec_ref(config); + /** + * There is a subtle difference between lldpctl_watch_callback + * and lldpctl_watch_callback2, but for the sake of using lldpctl + * 1.0.5, we're going to use lldpctl_watch_callback. + */ + if (0 != + lldpctl_watch_callback(watch_conn, process_neighbor_change, NULL)) { + POE_CRIT("Failed to register the lldpctl watch callback: %s", + lldpctl_last_strerror(lldpctl_conn)); + goto fail; + } + + POE_WARN("Successfully opened a connection with lldpd. Watching for " + "changes..."); + while (!thread_exit) { + if (0 != lldpctl_watch(watch_conn)) { + POE_CRIT("Unexpected error when watching for neighbor changes: %s", + lldpctl_last_strerror(lldpctl_conn)); + goto fail; + } + } + + POE_INFO("Exiting handle_lldp_events gracefully"); + + return NULL; + +fail: + if (lldpctl_conn) + lldpctl_release(lldpctl_conn); + if (watch_conn) + lldpctl_release(watch_conn); + return NULL; +} diff --git a/lldp-poe/src/main.c b/lldp-poe/src/main.c new file mode 100644 index 0000000..775f7a7 --- /dev/null +++ b/lldp-poe/src/main.c @@ -0,0 +1,187 @@ +/** + * Copyright Amazon Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +#include +#include +#include +#include +#include +#include +#include +#include + +#define SYSLOG_NAMES +#include "include/logger.h" +#include "include/common.h" +#include "include/lldp_event_handler.h" +#include "include/lldp_poed_err.h" +#include "include/netlink_event_handler.h" +#include "include/port_state_machine.h" + +#define LOG_LEVEL_UNKNOWN "UNKNOWN" + +/** + * struct thread_details - Hold the thread details required for starting a + * thread and waiting for a thread to finish. + */ +struct thread_details { + pthread_t thread_id; + char *thread_name; + void *(*thread_handler_fn)(void *); +}; + +/** + * Convenience macro for handling a pthread call error. + */ +#define HANDLE_PTHREAD_ERR(op, thread_name, msg) \ + do { \ + POE_CRIT("Failed to " op " %s: %s (errno = %s)", thread_name, msg, \ + strerror(errno)); \ + exit(EXIT_FAILURE); \ + } while (0) + +/** + * Add a new thread to the initializer list. + */ +#define INITIALIZE_NEW_PTHREAD(handler_fn) \ + { \ + .thread_name = #handler_fn, .thread_handler_fn = handler_fn, \ + } + +/** + * Global thread exit flag. + */ +volatile sig_atomic_t thread_exit = 0; + +/** + * exit_threads - Sets the global thread flag to true. + */ +void exit_threads(int sig_num) +{ + POE_DEBUG("Exited via signal %d", sig_num); + thread_exit = sig_num; +} + +/** + * Assign an initial logging level. + */ +int log_level = LOG_WARNING; + +/** + * get_loglevel - finds log level name in prioritynames struct from syslog. + * @loglevel_number: the priority number of log level. + * + * returns the log level name of the log level prio number. + * returns UNKNOWN in case the prio number is not correct. + */ +char* loglevel_to_string(int loglevel_number) +{ + int i; + static const int length = sizeof(prioritynames) / sizeof(CODE); + for (i = 0; i < length; i++) { + if (prioritynames[i].c_val == loglevel_number) { + return prioritynames[i].c_name; + } + } + syslog(LOG_ERR, "Changing log level to unknown level name using prio number (%d).", loglevel_number); + return LOG_LEVEL_UNKNOWN; +} + +/** + * Signal handler to increase log level with SIGUSR1 and decrease it with SIGUSR2. + */ +void change_loglevel(int sig_num) +{ + int loglevel_before_change = log_level; + bool log_changed = false; + if (sig_num == SIGUSR1) { + if (log_level < LOG_DEBUG) { + log_level += 1; + log_changed = true; + } + } else if (sig_num == SIGUSR2) { + if (log_level > LOG_EMERG) { + log_level -= 1; + log_changed = true; + } + } + /** + * Set new log level when a change has happened. + */ + if (log_changed) { + setlogmask(LOG_UPTO(log_level)); + syslog(log_level, "Log level changed from (%d)(%s) to (%d)(%s).", loglevel_before_change, loglevel_to_string(loglevel_before_change), log_level, loglevel_to_string(log_level)); + } +} + +/** + * init_log - Set up logging through syslog. + */ +void init_logging() +{ + setlogmask(LOG_UPTO(log_level)); + openlog("lldp-poed", LOG_CONS | LOG_PID | LOG_NDELAY, LOG_DAEMON); + /** + * Handle log level changes. + */ + signal(SIGUSR1, change_loglevel); + signal(SIGUSR2, change_loglevel); +} + +int main() +{ + /** + * Handle both SIGTERM and SIGSEGV. + */ + signal(SIGTERM, exit_threads); + signal(SIGSEGV, exit_threads); + + init_logging(); + + /** + * The threads that are going to be created, in the given order. + */ + struct thread_details threads[] = { + INITIALIZE_NEW_PTHREAD(handle_port_state_machine), + INITIALIZE_NEW_PTHREAD(handle_netlink_events), + INITIALIZE_NEW_PTHREAD(handle_lldp_events), + }; + + struct thread_details *thread_it = NULL; + FOR_EACH(thread_it, threads, COUNT_OF(threads)) + { + int status = pthread_create(&(thread_it->thread_id), NULL, + thread_it->thread_handler_fn, NULL); + if (0 != status) + HANDLE_PTHREAD_ERR("create", thread_it->thread_name, + strerror(errno)); + POE_DEBUG("Created %s thread", thread_it->thread_name); + } + + /* Wait for all threads to finish. */ + FOR_EACH(thread_it, threads, COUNT_OF(threads)) + { + int status = pthread_join(thread_it->thread_id, NULL); + if (0 != status) + HANDLE_PTHREAD_ERR("join", thread_it->thread_name, strerror(errno)); + POE_DEBUG("Thread %s successfully joined", thread_it->thread_name); + } + + publish_metrics("lldp_poed_exit", thread_exit, 0); + closelog(); + + return 0; +} diff --git a/lldp-poe/src/netlink_event_handler.c b/lldp-poe/src/netlink_event_handler.c new file mode 100644 index 0000000..4e40309 --- /dev/null +++ b/lldp-poe/src/netlink_event_handler.c @@ -0,0 +1,388 @@ +/** + * Copyright Amazon Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +/** + * This thread establishes netlink socket (AF_NETLINK) connection and monitors + * network interface up/down events. + * + * After binding to socket, the thread subscribes to notifications about changes + * in network interface (RTMGRP_LINK). Every netlink message has header, + * followed by a payload with messages being specific to port-id. + * + * All messages communications happen over two well known structures nlmsghdr + * and iovec. Sometimes large messages are split into multiple messages. + * + * Upon receiving a network interface down->up event, we will validate if the + * interface is L1 UP and also RUNNING. we will NOT wait for ARP. We will notify + * the port state machine for each up and down operational status event. + * + * The nlmsghdr and iovec structure are defined as: + * struct nlmsghdr { + * __u32 nlmsg_len; // Length of message including header + * __u16 nlmsg_type; // Type of message content + * __u16 nlmsg_flags; // Additional flags + * __u32 nlmsg_seq; // Sequence number + * __u32 nlmsg_pid; // Sender port ID + * }; + * + * struct iovec { + * void *iov_base; // data buff + * __kernel_size_t iov_len; // size of the data + * }; + * + * Another Important structure is the ifinfomsg, that we need to do deep + * inspection on to get if_index and state of interface: + * struct ifinfomsg { + * unsigned char ifi_family; + * unsigned char __ifi_pad; + * unsigned short ifi_type; // ARPHRD_* + * int ifi_index; // Link index that + * will get us port ID and interface name. + * unsigned ifi_flags; // IFF_* flags that we need to + * check for interface being UP and running + * unsigned ifi_change; //IFF_* change mask + * }; + * + * When aligning netlink messages, after completion of reading them, we will + * use NLMSG_ALIGN, that is defined in netlink.h: + * #define NLMSG_ALIGN(len) (((len)+NLMSG_ALIGNTO-1) & ~(NLMSG_ALIGNTO-1)) + * + * More details about netlink data structures and flags in netlink manpage: + * https://man7.org/linux/man-pages/man7/netlink.7.html + */ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "cJSON/cJSON.h" +#include "include/common.h" +#include "include/lldp_poed_err.h" +#include "include/logger.h" +#include "include/netlink_event_handler.h" +#include "include/port_state_machine.h" + +/** + * Subscribe mask used for filtering out the netlink events. + */ +#ifndef NETLINK_SUBSCRIBE_GROUP +#define NETLINK_SUBSCRIBE_GROUP RTMGRP_LINK +#endif + +/** + * Must increase this for larger systems or longer thread waits. + * A lower value may cause truncation on systems + * with the page size larger than 4096. + */ +#ifndef MESSAGE_BUFFER_MAX_SIZE +#define MESSAGE_BUFFER_MAX_SIZE 8192U +#endif + +#ifndef HEARTBEAT_INTERVAL_SEC +#define HEARTBEAT_INTERVAL_SEC 60U +#endif + +static const useconds_t netlink_thread_sleep_time = 300000U; + +/** + * setup_netlink_socket_connection - Set up the netlink socket connection + * @sockfd: socket descriptor to initialize + * @client_id: client ID structure to initialize + * + * Returns whether creating the socket connection and binding to it was + * successful. + */ +static int setup_netlink_socket_connection(int *sockfd, + struct sockaddr_nl *client_id) +{ + if (!sockfd || !client_id) + return 1; + + /* Initialize client ID structure. */ + memset(client_id, 0, sizeof(*client_id)); + client_id->nl_family = AF_NETLINK; + client_id->nl_groups = NETLINK_SUBSCRIBE_GROUP; + client_id->nl_pid = pthread_self(); + + *sockfd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); + if (*sockfd < 0) { + POE_ERR("Failed to set up the netlink socket: %s", strerror(errno)); + return EXIT_FAILURE; + } + POE_DEBUG("Successfully set up the netlink socket"); + + if (bind(*sockfd, (struct sockaddr *) client_id, sizeof(*client_id)) < 0) { + POE_ERR("Failed to bind to netlink socket: %s", strerror(errno)); + close(*sockfd); + return 1; + } + POE_DEBUG("Binding to netlink socket completed."); + + return 0; +} + +/** + * get_ifname_from_interface_info - Get port number as per front panel + * numbering + * @interface_info: interface link-level information + * @current_message_length: netlink message length + * @ifname: the matched interface name + * + * We do not handle if the port is a poe port or not in here. That is + * up to poed. The interface is identified based on the input structure. + */ +static int get_ifname_from_interface_info(struct ifinfomsg *interface_info, + int current_message_length, + char *ifname) +{ + if (!ifname) + return 1; + + struct rtattr *attribute_struct = IFLA_RTA(interface_info); + while (RTA_OK(attribute_struct, current_message_length)) { + if (attribute_struct->rta_type == IFLA_IFNAME) { + strncpy(ifname, RTA_DATA(attribute_struct), IFNAMSIZ); + return 0; + } + attribute_struct = RTA_NEXT(attribute_struct, current_message_length); + } + + *ifname = '\0'; + return 1; +} + +/** + * process_netlink_messages - Detect which interfaces went up or down, based on + * the ifi_flags field, when receiving a new netlink message + * @sockfd: caller-initialized socket descriptor + * @client_id: caller-initialized client ID structure + * + * Returns 0 if successful, 1 when there was an error parsing the netlink + * message. + */ +static int process_netlink_messages(int sockfd, struct sockaddr_nl *client_id) +{ + if (sockfd < 0 || !client_id) { + POE_ERR("Invalid argument(s)"); + return 1; + } + + /** + * Initialize buffers and structures necessary to receive messages over + * the netlink socket. + */ + struct nlmsghdr + message_buffer[MESSAGE_BUFFER_MAX_SIZE / sizeof(struct nlmsghdr)]; + struct iovec iov = { + .iov_base = + message_buffer, /* Starting address of socket message buffer */ + .iov_len = sizeof(message_buffer)}; + + /* Initialize message header to be used in recvmsg processing. */ + struct msghdr message_header = {.msg_name = client_id, + .msg_namelen = sizeof(*client_id), + .msg_iov = &iov, + .msg_iovlen = 1, + .msg_control = NULL, + .msg_controllen = 0, + .msg_flags = 0}; + + /* Receive message in a non-blocking fashion. */ + ssize_t message_len = recvmsg(sockfd, &message_header, MSG_DONTWAIT); + if (message_len < 0) { + if (errno == EAGAIN || errno == EINTR) { + /** + * Netlink is busy, need to retry. + */ + return 0; + } + + POE_ERR("Netlink recv message failed: %s", strerror(errno)); + return 1; + } + + /** + * Go through the all messages and notify the port state machine of each + * link up/down. + * Map the starting address of buffer to the netlink message + * header and read until message_len runs out. + */ + for (struct nlmsghdr *netlink_header = message_buffer; + NLMSG_OK(netlink_header, message_len); + NLMSG_NEXT(netlink_header, message_len)) { + int current_message_length = netlink_header->nlmsg_len; + POE_DEBUG("message_len: %lu", message_len); + char ifname[IFNAMSIZ]; + struct ifinfomsg *interface_info = NLMSG_DATA(netlink_header); + const int err = get_ifname_from_interface_info( + interface_info, current_message_length, ifname); + if (0 != err) { + POE_ERR("Failed to get ifname from interface RTA structure: %s", + strerror(errno)); + return 1; + } + + /** + * TODO: Coalesce multiple events coming for the same port and send the + * link update in batches to the state machine. + */ + bool l1_up = interface_info->ifi_flags & IFF_LOWER_UP; + bool l2_up = interface_info->ifi_flags & IFF_RUNNING; + bool admin_up = interface_info->ifi_flags & IFF_UP; + if (l2_up) { + POE_INFO("Interface is operationally up (RUNNING). Name: %s, " + "ifi_index: %d", + ifname, interface_info->ifi_index); + push_if_link_update(ifname, PORT_IF_UP); + } else if (admin_up) { + POE_INFO("Interface %d is set to admin UP, but has no active L2 " + "link. Carrier L1 (LOWER_UP) status is %s", + interface_info->ifi_index, (l1_up) ? "UP" : "DOWN"); + push_if_link_update(ifname, PORT_IF_DOWN); + } else { + POE_INFO("Interface was set to admin DOWN. Name: %s, ifi_index: %d", + ifname, interface_info->ifi_index); + push_if_link_update(ifname, PORT_IF_DOWN); + } + + int current_message_type = netlink_header->nlmsg_type; + POE_DEBUG("Received netlink message of type: %d", current_message_type); + } + + return 0; +} + +/** + * handle_netlink_events - Process relevant netlink messages + * + * Detect all ports that come up or down and advertise their state to the port + * state machine. + */ +void *handle_netlink_events() +{ + int sockfd; + struct sockaddr_nl client_id; + int status = setup_netlink_socket_connection(&sockfd, &client_id); + if (status != 0) { + POE_ERR("Failed to setup netlink socket, " + "Exiting..."); + return NULL; + } + POE_NOTICE("Successfully completed netlink socket communication"); + + /** + * Process netlink events from RTMGRP_LINK group. + * We are going to recvmsg on socket and process them one at a + * time. + */ + time_t current_system_time = time(NULL); + struct tm *heartbeat_time = + gmtime(¤t_system_time); /* This will ensure portability. */ + while (!thread_exit) { + status = process_netlink_messages(sockfd, &client_id); + if (time(NULL) > mktime(heartbeat_time)) { + publish_metrics("lldp_poed_heartbeat", 1, 0); + heartbeat_time->tm_min += HEARTBEAT_INTERVAL_SEC / 60; + } + + usleep(netlink_thread_sleep_time); + } + + POE_NOTICE("Closing netlink socket and exiting " + "handle_netlink_events gracefully"); + close(sockfd); + + return NULL; +} + +/** + * is_link_state_up_and_running - Determine if the given interface is up and + * running + * @ifname: the interface name + * + * Returns true if IFF_UP and IFF_RUNNING flags are present for the given + * interface. + */ +static bool is_link_state_up_and_running(const char *ifname) +{ + int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); + if (sockfd < 0) { + POE_ERR("Socket failed. Errno = %s\n", strerror(errno)); + return false; + } + + struct ifreq if_req; + strncpy(if_req.ifr_name, ifname, sizeof(if_req.ifr_name)); + const int ret = ioctl(sockfd, SIOCGIFFLAGS, &if_req); + close(sockfd); + + if (-1 == ret) { + POE_ERR("Ioctl failed. Errno = %s\n", strerror(errno)); + return false; + } + POE_DEBUG("Port state flag: 0x%x. Name: %s", if_req.ifr_flags, ifname); + + if ((if_req.ifr_flags & IFF_UP) && (if_req.ifr_flags & IFF_RUNNING)) + return true; + return false; +} + +/** + * scan_all_ports - Determine which ports are already up and running + * @pr: port range used for scanning + * + * The detected ports will be reported as operationally up to the port state + * machine. + * + * Returns 0 if successful, 1 otherwise. + */ +int netlink_scan_all_ports(struct port_range *pr) +{ + if (!pr) + return 1; + + FOR_I_IN(pr->start_index, pr->end_index) + { + char port_name[IFNAMSIZ]; + snprintf(port_name, IFNAMSIZ, "%s%ld", pr->ifname_prefix, i); + POE_INFO("Scanning port: %s", port_name); + if (is_link_state_up_and_running(port_name)) { + POE_INFO("Port %s is up and running", port_name); + push_if_link_update(port_name, PORT_IF_UP); + } + } + + return 0; +} diff --git a/lldp-poe/src/payload.c b/lldp-poe/src/payload.c new file mode 100644 index 0000000..7dd0cc6 --- /dev/null +++ b/lldp-poe/src/payload.c @@ -0,0 +1,426 @@ +/** + * Copyright Amazon Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +#include +#include +#include + +#include "cJSON/cJSON.h" +#include "include/common.h" +#include "include/lldp_poed_err.h" +#include "include/logger.h" +#include "include/payload.h" + +#ifndef JSON_RPC_VER +#define JSON_RPC_VER "2.0" +#endif + +/** + * find_payload_by_key - Find the payload node by key and return its value + * @payload: payload to be searched + * @key_name: key name + * @val: value to be referenced, if found + * + * The search is done recursively across all children. @val is used + * as struct poed_payload to cover also compound objects (array and + * object). + * + * Returns 0 if the key was found, 1 otherwise. + */ +int find_payload_by_key(const struct poed_payload *payload, + const char *key_name, const struct poed_payload **val) +{ + if (!payload || !key_name || !val || payload->type >= PAYLOAD_VALUE_MAX) + return 1; + + int status = 1; + if (0 == strncmp(key_name, payload->name, PAYLOAD_NAME_MAX_SIZE)) { + *val = payload; + status = 0; + } else if (PAYLOAD_VALUE_OBJECT == payload->type || + PAYLOAD_VALUE_ARRAY == payload->type) { + const struct poed_payload *payload_it = NULL; + FOR_EACH(payload_it, payload->children, payload->child_count) + { + status = find_payload_by_key(payload_it, key_name, val); + if (0 == status) + break; + } + } + + return status; +} + +/** + * add_all_payload_children - Recurse into all @payload children and + * add all fields to the @root node. + * @root: cJSON root node + * @children: children to translate to cJSON + * @type: root payload type + * @count: children count + * + * @warning: caller has the responsibility to free the @root + * + * @children must be an array of valid types, otherwise it will return 1. + * + * Returns 0 if successful, 1 otherwise. + */ +static int add_all_payload_children(struct cJSON *root, + const struct poed_payload *children, + enum payload_value_type type, size_t count) +{ + if (!root || !children || !count || type >= PAYLOAD_VALUE_MAX || + (!cJSON_IsObject(root) && !cJSON_IsArray(root))) + return 1; + + const struct poed_payload *children_it = NULL; + FOR_EACH(children_it, children, count) + { + cJSON *child_node = NULL; + switch (children_it->type) { + case PAYLOAD_VALUE_BOOLEAN: + child_node = cJSON_CreateBool(children_it->value.val_bool); + break; + case PAYLOAD_VALUE_NULL: + child_node = cJSON_CreateNull(); + break; + case PAYLOAD_VALUE_NUMBER: + child_node = cJSON_CreateNumber(children_it->value.val_int); + break; + case PAYLOAD_VALUE_STRING: + child_node = cJSON_CreateString(children_it->value.val_str); + break; + case PAYLOAD_VALUE_OBJECT: + child_node = cJSON_CreateObject(); + break; + case PAYLOAD_VALUE_ARRAY: + child_node = cJSON_CreateArray(); + break; + default: + POE_ERR("Unknown payload type: %d", children_it->type); + return 1; + } + if (NULL == child_node) + return 1; + + /** + * Recurse for objects and arrays before appending the child to the + * root. In case it's a primitive type, just append it to the root. + */ + if (PAYLOAD_VALUE_OBJECT == children_it->type || + PAYLOAD_VALUE_ARRAY == children_it->type) { + + if (0 != add_all_payload_children(child_node, children_it->children, + children_it->type, + children_it->child_count)) + return 1; + + if (PAYLOAD_VALUE_ARRAY == children_it->type) + cJSON_AddItemToArray(root, child_node); + else if (PAYLOAD_VALUE_OBJECT == children_it->type) { + if ('\0' == children_it->name[0]) { + POE_ERR("JSON key name cannot be empty for an object"); + return 1; + } + } + } + + cJSON_AddItemToObject(root, children_it->name, child_node); + } + + return 0; +} + +/** + * payload_to_json_rpc - Serialize the poed payload to a JSON-RPC message + * @payload: payload to be serialized + * @method: value for the "method" JSON field + * @id: generated request ID + * @json: buffer for the final JSON string + * @max_size: pre-allocated @json buffer size + * + * Will populate the "params" field with the payload and also "id" and "method" + * fields. If @payload is empty, then this field will be omitted from the final + * request. + * + * Returns 0 if successful, 1 otherwise. + */ +int payload_to_json_rpc(const struct poed_payload *payload, const char *method, + ssize_t *id, char *json, size_t max_size) +{ + static ssize_t request_id_counter = 0; + + if (!payload || !method || !id || !json) + return 1; + + cJSON *message = cJSON_CreateObject(); + if (NULL == message) + goto fail; + if (NULL == cJSON_AddStringToObject(message, "jsonrpc", JSON_RPC_VER)) + goto fail; + if (NULL == cJSON_AddStringToObject(message, "method", method)) + goto fail; + + switch (payload->type) { + case PAYLOAD_VALUE_OBJECT: + if (0 == payload->child_count) { + POE_DEBUG("Skipping 'params' field for %s", method); + break; + } + + /* This means we have children to add as params to the request. */ + cJSON *params = NULL; + params = cJSON_AddObjectToObject(message, "params"); + if (0 != add_all_payload_children(params, payload->children, + payload->type, payload->child_count)) + goto fail; + + break; + default: + POE_ERR("Invalid payload type ('params' must be an object)"); + goto fail; + break; + } + + *id = ++request_id_counter; + cJSON *req_id = cJSON_CreateNumber(request_id_counter); + if (NULL == req_id) + goto fail; + if (false == cJSON_AddItemToObject(message, "id", req_id)) + goto fail; + + if (false == cJSON_PrintPreallocated(message, json, max_size, false)) + goto fail; + cJSON_Delete(message); + + return 0; + +fail: + POE_ERR("Failed to construct JSON-RPC message for %s", method); + cJSON_Delete(message); + *id = -1; + return 1; +} + +/** + * add_all_cjson_children - Recurse into all @cjson children and add + * all fields to the @root node. + * @root: payload root node + * @cjson: cJSON to traverse + * + * @warning: caller has the responsibility to free the @root + * + * Returns 0 if successful, 1 otherwise. + */ +static int add_all_cjson_children(struct poed_payload *root, + const struct cJSON *cjson) +{ + if (!root || !cjson) + return 1; + + root->child_count = 0; + root->children = NULL; + if (cJSON_IsNumber(cjson)) { + root->type = PAYLOAD_VALUE_NUMBER; + root->value.val_int = cjson->valueint; + } else if (cJSON_IsString(cjson)) { + root->type = PAYLOAD_VALUE_STRING; + strncpy(root->value.val_str, cjson->valuestring, + PAYLOAD_VAL_STR_MAX_SIZE); + } else if (cJSON_IsBool(cjson)) { + root->type = PAYLOAD_VALUE_BOOLEAN; + root->value.val_int = (cjson->valueint ? true : false); + } else if (cJSON_IsNull(cjson)) { + root->type = PAYLOAD_VALUE_NULL; + memset(&(root->value), 0, sizeof(root->value)); + } else if (cJSON_IsObject(cjson) || cJSON_IsArray(cjson)) { + root->type = + cJSON_IsObject(cjson) ? PAYLOAD_VALUE_OBJECT : PAYLOAD_VALUE_ARRAY; + /* The cJSON API is misleading here. Array can also mean Object... */ + root->child_count = cJSON_GetArraySize(cjson); + root->children = calloc(root->child_count, sizeof(struct poed_payload)); + int child_idx = 0; + const cJSON *child_it = NULL; + cJSON_ArrayForEach(child_it, cjson) + { + if (cJSON_IsObject(cjson)) + strncpy(root->children[child_idx].name, child_it->string, + PAYLOAD_NAME_MAX_SIZE); + if (0 != + add_all_cjson_children(&(root->children[child_idx]), child_it)) + return 1; + child_idx++; + } + } else { + POE_ERR("Unknown cJSON type: %d", cjson->type); + return 1; + } + + return 0; +} + +/** + * json_rpc_to_payload - Deserialize a JSON-RPC message to poed payload + * @json: input buffer + * @max_size: pre-allocated @json buffer size + * @id: id to match in the message + * @payload: payload to deserialize to + * + * @warning: caller has the responsibility to free the @payload + * + * Returns 0, LLDP_POED_ERR_OK, if successful, an error_code otherwise. + * + * Note: if the response is actually an error, this will return 1 and log + * the error. An error is generated also if the message ID doesn't match the + * input @id. + */ +int json_rpc_to_payload(const char *json, size_t max_size, const ssize_t id, + struct poed_payload *payload) +{ + if (!json || !payload) + return LLDP_POED_ERR_INVALID_PARAM; + + int status = LLDP_POED_ERR_OK; + cJSON *message = cJSON_ParseWithLength(json, max_size); + if (NULL == message) { + status = LLDP_POED_ERR_PARSE_ERROR; + goto fail; + } + cJSON *json_rpc_version = + cJSON_GetObjectItemCaseSensitive(message, "jsonrpc"); + if (!cJSON_IsString(json_rpc_version) || !json_rpc_version->valuestring || + 0 != strncmp(json_rpc_version->valuestring, "2.0", 4)) { + status = LLDP_POED_ERR_PARSE_ERROR; + goto fail; + } + cJSON *res_id = cJSON_GetObjectItemCaseSensitive(message, "id"); + if (!cJSON_IsNumber(res_id) || res_id->valueint != id) { + status = LLDP_POED_ERR_PARSE_ERROR; + goto fail; + } + + if (cJSON_HasObjectItem(message, "error")) { + cJSON *error = cJSON_GetObjectItemCaseSensitive(message, "error"); + cJSON *message = cJSON_GetObjectItemCaseSensitive(error, "message"); + if (message->valuestring) { + POE_ERR("JSON-RPC response error message: %s", + message->valuestring); + } else { + POE_ERR( + "Unknown JSON-RPC response error (missing 'message' field)"); + status = LLDP_POED_ERR_PARSE_ERROR; + goto fail; + } + } + + if (cJSON_HasObjectItem(message, "result")) { + cJSON *result = cJSON_GetObjectItemCaseSensitive(message, "result"); + if (cJSON_IsNumber(result) || cJSON_IsString(result) || + cJSON_IsObject(result)) { + strncpy(payload->name, "result", PAYLOAD_NAME_MAX_SIZE); + if (0 != add_all_cjson_children(payload, result)) { + status = LLDP_POED_ERR_INTERNAL_ERROR; + goto fail; + } + } else { + POE_ERR("Invalid cJSON 'result' type: %d", result->type); + status = LLDP_POED_ERR_PARSE_ERROR; + goto fail; + } + } else { + POE_ERR("Missing 'result' field in the JSON-RPC response"); + status = LLDP_POED_ERR_PARSE_ERROR; + goto fail; + } + + cJSON_Delete(message); + + return status; + +fail:; /* C89 compliance for labels (labels must always start with a statement) + */ + const char *err = cJSON_GetErrorPtr(); + POE_ERR("Failed to parse JSON-RPC message: %s", (err) ? err : ""); + cJSON_Delete(message); + return status; +} + +/** + * log_payload - Log all payload contents for debug purposes + * @payload: caller-initialized payload + */ +void log_payload(const struct poed_payload *payload) +{ + if (!payload) + return; + + if (PAYLOAD_VALUE_OBJECT == payload->type || + PAYLOAD_VALUE_ARRAY == payload->type) { + POE_DEBUG("Payload array/object name: %s", + (strlen(payload->name)) ? payload->name : "None"); + struct poed_payload *payload_it = NULL; + FOR_EACH(payload_it, payload->children, payload->child_count) + { + POE_DEBUG("------------------------------------------------" + "--------------"); + log_payload(payload_it); + } + } else if (PAYLOAD_VALUE_BOOLEAN == payload->type) { + POE_DEBUG("Payload type: boolean, name: %s, value: %s", + (strlen(payload->name)) ? payload->name : "NULL", + (payload->value.val_bool ? "true" : "false")); + } else if (PAYLOAD_VALUE_NUMBER == payload->type) { + POE_DEBUG("Payload type: number, name: %s, value: %d", + (strlen(payload->name)) ? payload->name : "NULL", + payload->value.val_int); + } else if (PAYLOAD_VALUE_STRING == payload->type) { + POE_DEBUG("Payload type: string, name: %s, value: %s", + (strlen(payload->name)) ? payload->name : "NULL", + payload->value.val_str); + } else if (PAYLOAD_VALUE_NULL == payload->type) { + POE_DEBUG("Payload type: null, name: %s", + (strlen(payload->name)) ? payload->name : "NULL"); + } else { + POE_DEBUG("Unknown payload type: %d, name: %s", payload->type, + (strlen(payload->name)) ? payload->name : "NULL"); + } +} + +/** + * free_payload - Free all children dynamically-allocated memory. + * @payload: caller-initialized payload + * + * Once the payload has been freed up, it can be safely reused. + */ +void free_payload(struct poed_payload *payload) +{ + if (!payload) + return; + + if (PAYLOAD_VALUE_OBJECT == payload->type || + PAYLOAD_VALUE_ARRAY == payload->type) { + if (!payload->children) + return; + + struct poed_payload *payload_it = NULL; + FOR_EACH(payload_it, payload->children, payload->child_count) + { + free_payload(payload_it); + } + free(payload->children); + payload->children = NULL; + } +} diff --git a/lldp-poe/src/port_state_machine.c b/lldp-poe/src/port_state_machine.c new file mode 100644 index 0000000..7b0deea --- /dev/null +++ b/lldp-poe/src/port_state_machine.c @@ -0,0 +1,2516 @@ +/** + * Copyright Amazon Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "include/common.h" +#include "include/lldp_event_handler.h" +#include "include/lldp_poed_err.h" +#include "include/logger.h" +#include "include/netlink_event_handler.h" +#include "include/payload.h" +#include "include/port_state_machine.h" +#include "include/queue.h" + +/** + * Prefix used for constructing the whole interface name (e.g. eth0). + * Note: this is platform-specific, + */ +#ifndef PORT_INTERFACE_NAME_PREFIX +#define PORT_INTERFACE_NAME_PREFIX "swp" +#endif /* PORT_INTERFACE_NAME_PREFIX */ + +#ifndef POED_MESSAGE_MAX_SIZE +#define POED_MESSAGE_MAX_SIZE 1024U +#endif /* POED_MESSAGE_MAX_SIZE */ + +/** + * enum port_state - States a port may go through during the L2 negotiation, + * starting with PORT_UNINIT + * @PORT_INVALID_STATE: invalid state as a consequence of an illegal + * state machine transition + * @PORT_UNINIT: default starting state + * @PORT_DISABLED: port disabled by the user for PoE + * @PORT_FAULT: denied operation due to an internal hardware error + * @PORT_WAIT_PD: port is enabled and waiting for a PD to connect + * @PORT_L1_NEG_COMPLETE: L1 negotiation completed successfully + * Preparing to send the initial power advertisement as a PSE + * @PORT_WAIT_LLDP_REQ: Dot3 PoE-MDI advertisement was sent successfully + * Waiting for a valid PD PoE power request + * @PORT_DEFAULT_PWR_LIMIT: the port was assigned a default power + * limit due to not receiving any PoE request from the neighbor or failing to + * apply the power configuration + * @PORT_L2_NEG_COMPLETE: received a valid PoE-MDI power request from the PD + * and was able to reconcile it and adjust the power budget, using + * the neighbor data + * @PORT_LOST_POWER_LINK: lost the PD physical link + * @PORT_STATE_MAX: total number of port states + * + * A port state should be advanced only by a result of calling the handler + * and checking the state and event against the lookup table. + */ +enum port_state { + PORT_INVALID_STATE = 0, + PORT_UNINIT, + PORT_DISABLED, + PORT_FAULT, + PORT_WAIT_PD, + PORT_L1_NEG_COMPLETE, + PORT_WAIT_LLDP_REQ, + PORT_DEFAULT_PWR_LIMIT, + PORT_L2_NEG_COMPLETE, + PORT_LOST_POWER_LINK, + PORT_STATE_MAX, +}; + +/** + * enum port_state_event - Events that may trigger a port state change + * @PORT_EVENT_PORT_ENABLED: the port was enabled for PoE operation + * @PORT_EVENT_PORT_DISABLED: detected that the port got disabled + * @PORT_EVENT_LOST_POWER: detected that both the data link and the physical + * to the PD got lost. This means that the L1 and L2 negotiation have to be + * reinitiated + * @PORT_EVENT_LLDP_RESTORE: restore the Dot3 PoE data from the neighbor + * information, if it already exists + * @PORT_EVENT_LLDP_TIMEOUT: there was no valid PoE-MDI advertisement received + * within the holdtime window (aka TTL) + * @PORT_EVENT_OK: operation was successful. This can mean, for example, that + * an incoming PoE request was reconciled successfully + * @PORT_EVENT_ERR: port operation failed. Either a driver request failed or + * an LLDP request wasn't fulfilled + * @PORT_EVENT_IDLE: no change is requested + * @PORT_EVENT_MAX: total number of port events + * + * Some states may require executing a single command, while other states + * may require listening for a certain event. Either way, all states + * must have a handler defined. + */ +enum port_state_event { + PORT_EVENT_PORT_ENABLED = 0, + PORT_EVENT_PORT_DISABLED, + PORT_EVENT_LOST_POWER, + PORT_EVENT_LLDP_RESTORE, + PORT_EVENT_LLDP_TIMEOUT, + PORT_EVENT_OK, + PORT_EVENT_ERR, + PORT_EVENT_IDLE, /* No transition. */ + PORT_EVENT_MAX, +}; + +/** + * port_state_string - Reverse lookup table for stringifying the pot state + */ +const char *port_state_string[PORT_STATE_MAX] = { + "PORT_INVALID_STATE", "PORT_UNINIT", + "PORT_DISABLED", "PORT_FAULT", + "PORT_WAIT_PD", "PORT_L1_NEG_COMPLETE", + "PORT_WAIT_LLDP_REQ", "PORT_DEFAULT_PWR_LIMIT", + "PORT_L2_NEG_COMPLETE", "PORT_LOST_POWER_LINK", +}; + +/** + * port_transition_table - Lookup table for all possible state transitions + * depending on the current port state and the given port event (PORT_EVENT_IDLE + * can be skipped and handled the same way for all states, hence the minus 1). + * By indexing through a state-event combination, the caller can determine the + * next state in which to move a port in or if it's an illegal transition. + */ +static enum port_state + port_transition_table[PORT_STATE_MAX][PORT_EVENT_MAX - 1] = { + /** + * PORT_INVALID_STATE - No road to take from here. + */ + { + PORT_INVALID_STATE, + PORT_INVALID_STATE, + PORT_INVALID_STATE, + PORT_INVALID_STATE, + PORT_INVALID_STATE, + PORT_INVALID_STATE, + PORT_INVALID_STATE, + }, + /** + * PORT_UNINIT + */ + { + PORT_INVALID_STATE, + PORT_INVALID_STATE, + PORT_INVALID_STATE, + PORT_INVALID_STATE, + PORT_INVALID_STATE, + PORT_INVALID_STATE, + PORT_INVALID_STATE, + }, + /** + * PORT_DISABLED + */ + { + PORT_INVALID_STATE, + PORT_INVALID_STATE, + PORT_INVALID_STATE, + PORT_INVALID_STATE, + PORT_INVALID_STATE, + PORT_INVALID_STATE, + PORT_INVALID_STATE, + }, + /** + * PORT_FAULT + */ + { + PORT_INVALID_STATE, + PORT_INVALID_STATE, + PORT_INVALID_STATE, + PORT_INVALID_STATE, + PORT_INVALID_STATE, + PORT_INVALID_STATE, + PORT_INVALID_STATE, + }, + /** + * PORT_WAIT_PD + */ + { + PORT_INVALID_STATE, + PORT_INVALID_STATE, + PORT_INVALID_STATE, + PORT_INVALID_STATE, + PORT_INVALID_STATE, + PORT_INVALID_STATE, + PORT_INVALID_STATE, + }, + /** + * PORT_L1_NEG_COMPLETE + */ + { + PORT_INVALID_STATE, + PORT_INVALID_STATE, + PORT_INVALID_STATE, + PORT_INVALID_STATE, + PORT_INVALID_STATE, + PORT_INVALID_STATE, + PORT_INVALID_STATE, + }, + /** + * PORT_WAIT_LLDP_REQ + */ + { + PORT_INVALID_STATE, + PORT_INVALID_STATE, + PORT_INVALID_STATE, + PORT_INVALID_STATE, + PORT_INVALID_STATE, + PORT_INVALID_STATE, + PORT_INVALID_STATE, + }, + /** + * PORT_DEFAULT_PWR_LIMIT + */ + { + PORT_INVALID_STATE, + PORT_INVALID_STATE, + PORT_INVALID_STATE, + PORT_INVALID_STATE, + PORT_INVALID_STATE, + PORT_INVALID_STATE, + PORT_INVALID_STATE, + }, + /** + * PORT_L2_NEG_COMPLETE + */ + { + PORT_INVALID_STATE, + PORT_INVALID_STATE, + PORT_INVALID_STATE, + PORT_INVALID_STATE, + PORT_INVALID_STATE, + PORT_INVALID_STATE, + PORT_INVALID_STATE, + }, + /** + * PORT_LOST_POWER_LINK + */ + { + PORT_INVALID_STATE, + PORT_INVALID_STATE, + PORT_INVALID_STATE, + PORT_INVALID_STATE, + PORT_INVALID_STATE, + PORT_INVALID_STATE, + PORT_INVALID_STATE, + }, +}; + +/** + * init_transition_table - Assign the valid state transitions, based on the port + * event + */ +static void init_transition_table(void) +{ + /** + * PORT_UNINIT + */ + port_transition_table[PORT_UNINIT][PORT_EVENT_PORT_ENABLED] = PORT_WAIT_PD; + port_transition_table[PORT_UNINIT][PORT_EVENT_PORT_DISABLED] = + PORT_DISABLED; + port_transition_table[PORT_UNINIT][PORT_EVENT_ERR] = PORT_FAULT; + + /** + * PORT_DISABLED + */ + port_transition_table[PORT_DISABLED][PORT_EVENT_PORT_ENABLED] = + PORT_WAIT_PD; + port_transition_table[PORT_DISABLED][PORT_EVENT_ERR] = PORT_FAULT; + + /** + * PORT_FAULT + */ + port_transition_table[PORT_FAULT][PORT_EVENT_PORT_ENABLED] = PORT_WAIT_PD; + port_transition_table[PORT_FAULT][PORT_EVENT_PORT_DISABLED] = PORT_DISABLED; + + /** + * PORT_WAIT_PD + */ + port_transition_table[PORT_WAIT_PD][PORT_EVENT_PORT_DISABLED] = + PORT_DISABLED; + port_transition_table[PORT_WAIT_PD][PORT_EVENT_LLDP_RESTORE] = + PORT_L2_NEG_COMPLETE; + port_transition_table[PORT_WAIT_PD][PORT_EVENT_OK] = PORT_L1_NEG_COMPLETE; + port_transition_table[PORT_WAIT_PD][PORT_EVENT_ERR] = PORT_FAULT; + + /** + * PORT_L1_NEG_COMPLETE + */ + port_transition_table[PORT_L1_NEG_COMPLETE][PORT_EVENT_LOST_POWER] = + PORT_LOST_POWER_LINK; + port_transition_table[PORT_L1_NEG_COMPLETE][PORT_EVENT_OK] = + PORT_WAIT_LLDP_REQ; + port_transition_table[PORT_L1_NEG_COMPLETE][PORT_EVENT_ERR] = + PORT_DEFAULT_PWR_LIMIT; /* Couldn't advertise the MDI support, + therefore this entails falling back to the + default power limit. */ + + /** + * PORT_WAIT_LLDP_REQ + */ + port_transition_table[PORT_WAIT_LLDP_REQ][PORT_EVENT_LOST_POWER] = + PORT_LOST_POWER_LINK; + port_transition_table[PORT_WAIT_LLDP_REQ][PORT_EVENT_LLDP_TIMEOUT] = + PORT_DEFAULT_PWR_LIMIT; + port_transition_table[PORT_WAIT_LLDP_REQ][PORT_EVENT_OK] = + PORT_L2_NEG_COMPLETE; + + /** + * PORT_DEFAULT_PWR_LIMIT + */ + port_transition_table[PORT_DEFAULT_PWR_LIMIT][PORT_EVENT_LOST_POWER] = + PORT_LOST_POWER_LINK; + port_transition_table[PORT_DEFAULT_PWR_LIMIT][PORT_EVENT_OK] = + PORT_L2_NEG_COMPLETE; /* Received a valid PD request after all. */ + + /** + * PORT_L2_NEG_COMPLETE + */ + port_transition_table[PORT_L2_NEG_COMPLETE][PORT_EVENT_LOST_POWER] = + PORT_LOST_POWER_LINK; + port_transition_table[PORT_L2_NEG_COMPLETE][PORT_EVENT_ERR] = + PORT_L1_NEG_COMPLETE; /* This will allow PDs to change the power + allocation by reinitiating the L2 negotiation, + after aging out. */ + + /** + * PORT_LOST_POWER_LINK + */ + port_transition_table[PORT_LOST_POWER_LINK][PORT_EVENT_OK] = PORT_UNINIT; +} + +/** + * State handler prototype, specific to each state. + * Returns a port event which may determine a state transition. + */ +struct port_state_machine; +typedef enum port_state_event (*state_handler_fn_t)(struct port_state_machine *, + const void *); + +/** + * struct port_state_machine - Port state machine binding + * @id: ID used to identify the port + * @ifname: network interface name + * @admin_lldp_enabled: lldp processing enable/disable flag + * @if_up: interface operational status + * @timeout_time: future timestamp when the MDI advertisement expires (nullable) + * @current_state: current port state + * @process_state: state handler to be called in order to generate a + * port_state_event + */ +struct port_state_machine { + port_id_t id; + char ifname[IFNAMSIZ]; + bool admin_lldp_enabled; + bool lldp_default_pwr_limit_update_pending; + bool if_up; + time_t timeout_time; + enum port_state current_state; + state_handler_fn_t process_state; +}; + +/** + * struct port_neighbor_update - Container used for queueing up LLDP + * neighbor updates to be processed in the state machine + * @id: ID used to identify the port + * @settings: LLDP Dot3 port config + * @was_deleted: neighbor deleted flag + * + * @warning: the LLDP neighbor count must be limited to 1 to avoid undefined PoE + * behavior. + */ +struct port_neighbor_update { + port_id_t id; + struct port_dot3_power_settings settings; + bool was_deleted; +}; + +/** + * struct port_array - Non-resizable port array + * + * The structure should be initialized upon querying the total number of ports + * available on the device. + */ +static struct port_array { + size_t size; + struct port_state_machine *p; +} ports = { + .size = 0, + .p = NULL, +}; + +/** + * get_port_context_by_id - Find a port, given its ID + * @id: port ID + * @port: valid pointer to be used for referencing the port + * + * Returns 0 if the port is found, 1 otherwise. + * + * @warning: the port index may be different than what is returned by + * if_nametoindex(). Therefore, events must be reported through the interface + * name, not the index. + */ +static int get_port_context_by_id(const port_id_t id, + struct port_state_machine **port) +{ + /** + * Account for the one-indexing in the port map. + */ + if (!port || id <= 0 || id > ports.size || !ports.size) + return 1; + if (id != ports.p[id - 1].id) + return 1; + + *port = &(ports.p[id - 1]); + return 0; +} + +/** + * get_port_context_by_ifname - Find a port, given its Linux interface name + * @name: port interface name + * @port: valid pointer to be used for referencing the port + * + * Returns 0 if the port is found, 1 otherwise. + */ +static int get_port_context_by_ifname(const char *ifname, + struct port_state_machine **port) +{ + if (!port || !ifname || !ports.size) + return 1; + + int status = 1; + struct port_state_machine *port_it = NULL; + FOR_EACH(port_it, ports.p, ports.size) + { + if (0 == strncmp(port_it->ifname, ifname, IFNAMSIZ)) { + *port = port_it; + status = 0; + break; + } + } + + return status; +} + +/* State handlers begin */ + +static int wait_for_poed_response(char *message, size_t message_len); +static int sync_send_poed_request(struct poed_payload *query, + const char *method); + +/** + * create_get_port_details_query - Populate the poed_payload fields for the + * get_port_details method + * @id: port ID + * + * @warning: caller has the responsibility to free the payload memory. + * + * Returns the newly created payload. + */ +static struct poed_payload *create_get_port_details_query(port_id_t id) +{ + struct poed_payload *params = malloc(sizeof(struct poed_payload)); + struct poed_payload *port_query = malloc(sizeof(struct poed_payload)); + if (!params || !port_query) + return NULL; + + strncpy(params->name, "port_id", PAYLOAD_NAME_MAX_SIZE); + params->type = PAYLOAD_VALUE_NUMBER; + params->value.val_int = id; + strncpy(port_query->name, "params", PAYLOAD_NAME_MAX_SIZE); + port_query->type = PAYLOAD_VALUE_OBJECT; + port_query->child_count = 1; + port_query->children = params; + + return port_query; +} + +/** + * Convenience macro for declaring a state handler. + */ +#define DECLARE_STATE_HANDLER(state, fn_name) \ + static enum port_state_event fn_name(struct port_state_machine *port, \ + const void *data) + +DECLARE_STATE_HANDLER(PORT_INVALID_STATE, process_invalid_state); +DECLARE_STATE_HANDLER(PORT_UNINIT, process_uninit_state); +DECLARE_STATE_HANDLER(PORT_DISABLED, process_disabled_state); +DECLARE_STATE_HANDLER(PORT_FAULT, process_fault_state); +DECLARE_STATE_HANDLER(PORT_WAIT_PD, process_wait_pd_state); +DECLARE_STATE_HANDLER(PORT_L1_NEG_COMPLETE, process_l1_neg_complete_state); +DECLARE_STATE_HANDLER(PORT_WAIT_LLDP_REQ, process_wait_lldp_req_state); +DECLARE_STATE_HANDLER(PORT_DEFAULT_PWR_LIMIT, process_default_pwr_limit_state); +DECLARE_STATE_HANDLER(PORT_L2_NEG_COMPLETE, process_l2_neg_complete_state); +DECLARE_STATE_HANDLER(PORT_LOST_POWER_LINK, process_lost_power_link_state); + +/** + * state_handlers - Handler to port state mapping + * + * Each state has a unique handler attached to it. + */ +static state_handler_fn_t state_handlers[PORT_STATE_MAX] = { + process_invalid_state, /* PORT_INVALID_STATE */ + process_uninit_state, /* PORT_UNINIT */ + process_disabled_state, /* PORT_DISABLED */ + process_fault_state, /* PORT_FAULT */ + process_wait_pd_state, /* PORT_WAIT_PD */ + process_l1_neg_complete_state, /* PORT_L1_NEG_COMPLETE */ + process_wait_lldp_req_state, /* PORT_WAIT_LLDP_REQ */ + process_default_pwr_limit_state, /* PORT_DEFAULT_PWR_LIMIT */ + process_l2_neg_complete_state, /* PORT_L2_NEG_COMPLETE */ + process_lost_power_link_state, /* PORT_LOST_POWER_LINK */ +}; + +/** + * process_invalid_state - Invalid state handler + * @port: port to be processed + * @data: (ignored) + */ +DECLARE_STATE_HANDLER(PORT_INVALID_STATE, process_invalid_state) +{ + if (!port) { + POE_DEBUG("Port arg is NULL"); + return PORT_EVENT_IDLE; + } + + POE_ERR("Port %s is in INVALID_STATE due to an illegal transition " + "(shouldn't have got here)", + port->ifname); + + return PORT_EVENT_IDLE; +} + +/** + * determine_l1_port_state - Decide whether the port is in either + * disabled, enabled or error state + * + * @port: port to be processed + */ +static enum port_state_event +determine_l1_port_state(struct port_state_machine *port) +{ + if (!port) { + POE_DEBUG("Port arg is NULL"); + return PORT_EVENT_IDLE; + } + + struct poed_payload *query = create_get_port_details_query(port->id); + if (LLDP_POED_ERR_OK != sync_send_poed_request(query, "get_port_details")) { + POE_ERR("Failed to send get_port_details request for %s", port->ifname); + free_payload(query); + free(query); + return PORT_EVENT_IDLE; + } + + /** + * Parse the payload and generate event. + * The port may be in error state, hence check the status first. + */ + const struct poed_payload *is_admin_enabled = NULL; + const struct poed_payload *status = NULL; + const struct poed_payload *is_lldp_enabled = NULL; + enum port_state_event result = PORT_EVENT_IDLE; + if (0 == + find_payload_by_key(query, "is_admin_enabled", &is_admin_enabled) && + 0 == find_payload_by_key(query, "status", &status) && + 0 == find_payload_by_key(query, "is_lldp_enabled", &is_lldp_enabled)) { + if (!(PAYLOAD_VALUE_BOOLEAN == is_admin_enabled->type && + PAYLOAD_VALUE_STRING == status->type && + PAYLOAD_VALUE_BOOLEAN == is_lldp_enabled->type)) { + POE_ERR("Invalid payload type"); + goto parsing_failed; + } + + if (is_admin_enabled->value.val_bool) + result = PORT_EVENT_PORT_ENABLED; + else if (!is_admin_enabled->value.val_bool) + result = PORT_EVENT_PORT_DISABLED; + else if (0 == strcasecmp(status->value.val_str, "err")) + result = PORT_EVENT_ERR; + + port->admin_lldp_enabled = is_lldp_enabled->value.val_bool; + } else + goto parsing_failed; + + free_payload(query); + free(query); + + return result; + +parsing_failed: + POE_ERR("Failed to parse the poed payload for %s", port->ifname); + free_payload(query); + free(query); + return PORT_EVENT_IDLE; +} + +/** + * process_uninit_state - Process an uninitialized port + * @port: port to be processed + * @data: (ignored) + * + * Usually, all ports are initialized by init_ports() and are either disabled or + * waiting for link. However, a connection reset event will render the port back + * to uninitialized, thus requiring us to query the poed agent of its state. + * This relies on sending a synchronous request for detecting the current status + * of the port and acting on the reported status change in init_ports(). + */ +DECLARE_STATE_HANDLER(PORT_UNINIT, process_uninit_state) +{ + enum port_state_event result = determine_l1_port_state(port); + + if (PORT_EVENT_ERR == result) + POE_CRIT("Port %s went into fault state from uninit state", + port->ifname); + + return result; +} + +/** + * process_disabled_state - Process a disabled port + * @port: port to be processed + * @data: (ignored) + * + * Query the poed agent to detect whether the port was enabled by the user. A + * port that has a down operational status, doesn't necessarily mean it's + * enabled or disabled. + * For instance, a port must have both a power link and a data link to + * go up to PORT_L2_NEG_COMPLETE. + */ +DECLARE_STATE_HANDLER(PORT_DISABLED, process_disabled_state) +{ + enum port_state_event result = determine_l1_port_state(port); + + if (PORT_EVENT_ERR == result) { + POE_CRIT("Port %s went into fault state from disabled state", + port->ifname); + } else if (PORT_EVENT_PORT_DISABLED == result) + return PORT_EVENT_IDLE; /* Port is already disabled. */ + + return result; +} + +/** + * process_fault_state - Process a port in error state + * @port: port to be processed + * @data: (ignored) + * + * Query the poed agent to detect whether the port has recovered as enabled or + * disabled. + */ +DECLARE_STATE_HANDLER(PORT_FAULT, process_fault_state) +{ + enum port_state_event result = determine_l1_port_state(port); + + if (PORT_EVENT_ERR == result) + return PORT_EVENT_IDLE; /* Port is already in error state. */ + + return result; +} + +/** + * process_wait_pd_state - Process a port which is waiting for the L1 + * negotiation to complete after a PD is connected + * @port: port to be processed + * @data: (ignored) + * + * A successful transition is considered if, at least, the port has an active + * operational status and, not necessarily, an active data link (the PD may not + * support DLL classification at all). + */ +DECLARE_STATE_HANDLER(PORT_WAIT_PD, process_wait_pd_state) +{ + if (!port) { + POE_DEBUG("Port arg is NULL"); + return PORT_EVENT_IDLE; + } + + struct poed_payload *query = create_get_port_details_query(port->id); + if (0 != sync_send_poed_request(query, "get_port_details")) { + POE_ERR("Failed to send get_port_details request for %s", port->ifname); + free_payload(query); + free(query); + return PORT_EVENT_IDLE; + } + + /** + * Parse the payload and generate event. + * The port may be in error state, hence check the status first. + * In case the port was working already as an L2 port, go to + * L2_NEG_COMPLETE. + */ + const struct poed_payload *is_admin_enabled = NULL; + const struct poed_payload *status = NULL; + const struct poed_payload *power_mode = NULL; + const struct poed_payload *assigned_class = NULL; + const struct poed_payload *tppl = NULL; + const struct poed_payload *is_lldp_enabled = NULL; + enum port_state_event result = PORT_EVENT_IDLE; + if (0 == + find_payload_by_key(query, "is_admin_enabled", &is_admin_enabled) && + 0 == find_payload_by_key(query, "status", &status) && + 0 == find_payload_by_key(query, "power_mode", &power_mode) && + 0 == find_payload_by_key(query, "assigned_class", &assigned_class) && + 0 == find_payload_by_key(query, "tppl", &tppl) && + 0 == find_payload_by_key(query, "is_lldp_enabled", &is_lldp_enabled)) { + if (!(PAYLOAD_VALUE_BOOLEAN == is_admin_enabled->type && + PAYLOAD_VALUE_STRING == status->type && + (PAYLOAD_VALUE_STRING == power_mode->type || + PAYLOAD_VALUE_NULL == power_mode->type) && + (PAYLOAD_VALUE_NUMBER == assigned_class->type || + PAYLOAD_VALUE_NULL == assigned_class->type) && + (PAYLOAD_VALUE_NUMBER == tppl->type || + PAYLOAD_VALUE_NULL == tppl->type) && + PAYLOAD_VALUE_BOOLEAN == is_lldp_enabled->type)) { + POE_ERR("Invalid payload type"); + goto parsing_failed; + } + + if (0 == strcasecmp(status->value.val_str, "err")) { + result = PORT_EVENT_ERR; + } else if (0 == strcasecmp(status->value.val_str, "on")) { + if (PAYLOAD_VALUE_NULL == power_mode->type || + PAYLOAD_VALUE_NULL == assigned_class->type || + PAYLOAD_VALUE_NULL == tppl->type) { + POE_ERR("Invalid power fields type"); + goto parsing_failed; + } + bool is_already_reconciled = + is_neighbor_already_reconciled(port->ifname); + if (0 == strcasecmp(power_mode->value.val_str, "l1") || + (0 == strcasecmp(power_mode->value.val_str, "l2") && + !is_already_reconciled)) { + /** + * There's the case when a port is running in l2 mode, + * because that's the only way for the user to change the TPPL + * (through the L2 API). + */ + POE_INFO( + "Port %s came online and has an " + "active power link. Assigned class: %d, current TPPL: %dW, " + "data link status: %s", + port->ifname, assigned_class->value.val_int, + tppl->value.val_int, port->if_up ? "up" : "down"); + result = PORT_EVENT_OK; + } else if (0 == strcasecmp(power_mode->value.val_str, "l2") && + is_already_reconciled) { + /** + * Port is already working in L2 mode, restore L2_NEG_COMPLETE. + */ + result = PORT_EVENT_LLDP_RESTORE; + } + } else if (false == is_admin_enabled->value.val_bool) + result = PORT_EVENT_PORT_DISABLED; + + port->admin_lldp_enabled = is_lldp_enabled->value.val_bool; + } else + goto parsing_failed; + + free_payload(query); + free(query); + + if (PORT_EVENT_ERR == result) { + POE_CRIT("Port %s went into fault state from wait_pd state", + port->ifname); + } else if (PORT_EVENT_PORT_DISABLED == result) { + POE_NOTICE("Port %s got disabled in wait_pd state", port->ifname); + } else if (PORT_EVENT_LLDP_RESTORE == result) { + POE_NOTICE("Port %s got restored to L2 complete from wait_pd state", + port->ifname); + } else if (PORT_EVENT_OK == result) { + POE_NOTICE("Port %s completed the L1 negotiation successfully", + port->ifname); + } + + return result; + +parsing_failed: + POE_ERR("Failed to parse the poed payload for %s", port->ifname); + free_payload(query); + free(query); + return PORT_EVENT_IDLE; +} + +/** + * create_set_power_limit_query - Populate the poed_payload fields for the + * set_power_limit method + * @id: port ID + * @set_default: default power limit flag + * @requested_power: PD requested power (single-signature, nullable) + * @priority: 802.3at power priority (nullable) + * @requested_power_a: PD requested power for mode A (dual-signature, nullable) + * @requested_power_b: PD requested power for mode B (dual-signature, nullable) + * + * @warning: caller has the responsibility to free the returned payload memory. + * + * Returns the newly created payload. + */ +static struct poed_payload *create_set_power_limit_query( + port_id_t id, bool set_default, unsigned requested_power, unsigned priority, + unsigned requested_power_a, unsigned requested_power_b) +{ + struct poed_payload *params = malloc(4 * sizeof(struct poed_payload)); + struct poed_payload *set_query = malloc(sizeof(struct poed_payload)); + if (!params || !set_query) + return NULL; + + strncpy(params[0].name, "port_id", PAYLOAD_NAME_MAX_SIZE); + params[0].type = PAYLOAD_VALUE_NUMBER; + params[0].value.val_int = id; + strncpy(params[1].name, "default_power", PAYLOAD_NAME_MAX_SIZE); + params[1].type = PAYLOAD_VALUE_BOOLEAN; + params[1].value.val_bool = set_default; + + strncpy(params[2].name, "dot3at", PAYLOAD_NAME_MAX_SIZE); + if (0 != requested_power) { + struct poed_payload *dot3at = malloc(2 * sizeof(struct poed_payload)); + strncpy(dot3at[0].name, "requested_power", PAYLOAD_NAME_MAX_SIZE); + dot3at[0].type = PAYLOAD_VALUE_NUMBER; + dot3at[0].value.val_int = requested_power; + strncpy(dot3at[1].name, "priority", PAYLOAD_NAME_MAX_SIZE); + dot3at[1].type = PAYLOAD_VALUE_NUMBER; + dot3at[1].value.val_int = priority; + + params[2].type = PAYLOAD_VALUE_OBJECT; + params[2].child_count = 2; + params[2].children = dot3at; + } else { + params[2].type = PAYLOAD_VALUE_NULL; + params[2].child_count = 0; + params[2].children = NULL; + } + strncpy(params[3].name, "dot3bt", PAYLOAD_NAME_MAX_SIZE); + if (requested_power_a || requested_power_b) { + struct poed_payload *dot3bt = malloc(2 * sizeof(struct poed_payload)); + strncpy(dot3bt[0].name, "mode_a_requested_power", + PAYLOAD_NAME_MAX_SIZE); + dot3bt[0].type = PAYLOAD_VALUE_NUMBER; + dot3bt[0].value.val_int = requested_power_a; + strncpy(dot3bt[0].name, "mode_b_requested_power", + PAYLOAD_NAME_MAX_SIZE); + dot3bt[1].type = PAYLOAD_VALUE_NUMBER; + dot3bt[1].value.val_int = requested_power_b; + strncpy(dot3bt[1].name, "mode_b_requested_power", + PAYLOAD_NAME_MAX_SIZE); + + params[3].type = PAYLOAD_VALUE_OBJECT; + params[3].child_count = 2; + params[3].children = dot3bt; + } else { + params[3].type = PAYLOAD_VALUE_NULL; + params[3].child_count = 0; + params[3].children = NULL; + } + + strncpy(set_query->name, "params", PAYLOAD_NAME_MAX_SIZE); + set_query->type = PAYLOAD_VALUE_OBJECT; + set_query->child_count = 4; + set_query->children = params; + + return set_query; +} + +/** + * send_set_default_power_limit_request - Send a request for setting the default + * power limit for the given port + * @id: port ID + * + * Returns 0, LLDP_POED_ERR_OK, if successful, an lldp_poed_err otherwise. + */ +static int send_set_default_power_limit_request(port_id_t id) +{ + struct poed_payload *query = + create_set_power_limit_query(id, true, 0, 0, 0, 0); + if (0 != sync_send_poed_request(query, "set_power_limit")) { + POE_ERR("Failed to send set_power_limit request for port ID %d", id); + free_payload(query); + free(query); + return LLDP_POED_ERR_SEND_REQUEST_FAILED; + } + + const struct poed_payload *result = NULL; + int status = LLDP_POED_ERR_OK; + if (0 == find_payload_by_key(query, "result", &result)) { + if (PAYLOAD_VALUE_NUMBER != result->type) + status = LLDP_POED_ERR_INVALID_PAYLOAD; + else + POE_DEBUG("Port ID %d, TPPL (W, at PSE output): %d", id, + result->value.val_int); + } else { + POE_ERR("Failed to parse the poed payload for port ID %d", id); + status = LLDP_POED_ERR_INVALID_PAYLOAD; + } + free_payload(query); + free(query); + + return status; +} + +/** + * fill_at_power_settings - Populate the dot3 power settings with the parsed + * payload + * @at_payload: payload to parse + * @config: output dot3 config + * + * Returns 0 if successful, 1 otherwise. + */ +static int fill_at_power_settings(const struct poed_payload *at_payload, + struct port_dot3_power_settings *config) +{ + if (!at_payload || !config) + return 1; + + const struct poed_payload *pse_type = NULL; + const struct poed_payload *priority = NULL; + const struct poed_payload *requested_power = NULL; + const struct poed_payload *allocated_power = NULL; + if (0 == find_payload_by_key(at_payload, "pse_type", &pse_type) && + 0 == find_payload_by_key(at_payload, "priority", &priority) && + 0 == find_payload_by_key(at_payload, "requested_power", + &requested_power) && + 0 == find_payload_by_key(at_payload, "allocated_power", + &allocated_power)) { + if (PAYLOAD_VALUE_NULL == pse_type->type || + PAYLOAD_VALUE_NULL == allocated_power->type || + PAYLOAD_VALUE_NULL == requested_power->type) { + POE_ERR("Invalid 802.3at payload type"); + goto parsing_failed; + } + + if (0 == strcasecmp(pse_type->value.val_str, "type_2")) { + POE_DEBUG("Type 2 PSE"); + config->power_type = LLDP_DOT3_POWER_8023AT_TYPE2; + } else if (0 == strcasecmp(pse_type->value.val_str, "type_3")) { + POE_DEBUG("Type 3 PSE"); + config->power_type = LLDP_DOT3_POWER_8023AT_TYPE2; + config->power_type_ext = LLDP_DOT3_POWER_8023BT_TYPE3; + } else { + POE_ERR("Unsupported PSE type"); + goto parsing_failed; + } + /** + * May need to factor for backup sources too in the future. + */ + config->power_source = LLDP_DOT3_POWER_SOURCE_PRIMARY; + config->power_priority = ((PAYLOAD_VALUE_NULL != priority->type) + ? priority->value.val_int + : LLDP_DOT3_POWER_PRIO_UNKNOWN); + config->pd_requested = requested_power->value.val_int; + config->pse_allocated = allocated_power->value.val_int; + } else + goto parsing_failed; + + return 0; + +parsing_failed: + POE_ERR("Failed to parse the 802.3at payload"); + return 1; +} + +/** + * fill_bt_power_settings - Populate the dot3 power settings with the parsed + * payload + * @at_payload: payload to parse + * @config: output dot3 config + * + * Returns 0, LLDP_POED_ERR_OK, if successful, an lldp_poed_err otherwise. + */ +static int fill_bt_power_settings(const struct poed_payload *bt_payload, + struct port_dot3_power_settings *config) +{ + if (!bt_payload || !config) + return LLDP_POED_ERR_INVALID_PARAM; + + int func_status = LLDP_POED_ERR_OK; + /** + * TODO: Dual-signature PD handling. + */ + const struct poed_payload *pse_power_status = NULL; + const struct poed_payload *pse_power_pairs = NULL; + const struct poed_payload *max_power = NULL; + if (0 == find_payload_by_key(bt_payload, "pse_power_status", + &pse_power_status) && + 0 == find_payload_by_key(bt_payload, "pse_power_pairs", + &pse_power_pairs) && + 0 == find_payload_by_key(bt_payload, "max_power", &max_power)) { + if (PAYLOAD_VALUE_NUMBER != pse_power_status->type || + PAYLOAD_VALUE_NUMBER != pse_power_pairs->type || + PAYLOAD_VALUE_NUMBER != max_power->type) { + POE_ERR("Invalid 802.3bt payload type"); + func_status = LLDP_POED_ERR_INVALID_8023BT_FIELDS; + goto parsing_failed; + } + + config->pse_power_pair = + ((0 == strcasecmp(pse_power_pairs->value.val_str, "mode_b")) + ? LLDP_DOT3_POWERPAIRS_SPARE + : LLDP_DOT3_POWERPAIRS_SIGNAL); + config->pd_4pid = 0; + config->pd_requested_a = USHRT_MAX; + config->pd_requested_b = USHRT_MAX; + config->pse_allocated_a = USHRT_MAX; + config->pse_allocated_b = USHRT_MAX; + config->pse_status = pse_power_status->value.val_int; + config->pd_status = 0; + config->pse_pairs_ext = + (0 == strcasecmp(pse_power_status->value.val_str, "mode_b")) + ? 0x2 + : ((0 == strcasecmp(pse_power_status->value.val_str, "mode_a")) + ? 0x1 + : 0x3); /* Both modes, otherwise set to signal or spare + */ + config->power_class_mode_a = -1; + config->power_class_mode_b = -1; + /** + * Power Class ext was already initialized with the assigned class. + * However, for a dual-signature PD, this field must be set to 0xF. + */ + if (0x3 == config->pse_status) + config->pd_power_class_ext = 0xF; + /* Power Type ext already set in fill_at_power_settings() for a 802.3bt + * PSE. */ + config->pd_load = 0; + config->pse_max_available_power = max_power->value.val_int; + } else { + func_status = LLDP_POED_ERR_PARSE_ERROR; + goto parsing_failed; + } + + return func_status; + +parsing_failed: + POE_ERR("Failed to parse the 802.3bt payload"); + return func_status; +} + +/** + * send_lldp_neg_confirmation - Advertise the current PSE configuration to the + * LLDP neighbor + * @port: port to be processed + * @event: in case a transition to an error state is necessary (nullable) + * @is_initial: initial MDI advertisement flag + * + * In order to send a power advertisement, the port must be on and + * have an active data link. Otherwise, this will fail. + * + * Returns 0, LLDP_POED_ERR_OK, if successful, an lldp-poed_err otherwise. + */ +static int advertise_pse_dot3_config(struct port_state_machine *port, + enum port_state_event *event, + bool is_initial) +{ + struct poed_payload *query = create_get_port_details_query(port->id); + if (0 != sync_send_poed_request(query, "get_port_details")) { + POE_ERR("Failed to send get_port_details request for %s", port->ifname); + free_payload(query); + free(query); + return LLDP_POED_ERR_GETPORTDETAILS_FAILED; + } + + /** + * Parse the payload and generate event. If there was any problem with the + * power link, then this will go to CONN_RESET. In the happy case, we rely + * on the LLDP MDI power advertisement to be sent successfully. + */ + const struct poed_payload *is_admin_enabled = NULL; + const struct poed_payload *status = NULL; + const struct poed_payload *power_mode = NULL; + const struct poed_payload *assigned_class = NULL; + const struct poed_payload *is_lldp_enabled = NULL; + const struct poed_payload *dot3at = NULL; + const struct poed_payload *dot3bt = NULL; + int func_status = LLDP_POED_ERR_OK; + if (0 == + find_payload_by_key(query, "is_admin_enabled", &is_admin_enabled) && + 0 == find_payload_by_key(query, "status", &status) && + 0 == find_payload_by_key(query, "power_mode", &power_mode) && + 0 == find_payload_by_key(query, "assigned_class", &assigned_class) && + 0 == find_payload_by_key(query, "is_lldp_enabled", &is_lldp_enabled) && + 0 == find_payload_by_key(query, "dot3at", &dot3at) && + 0 == find_payload_by_key(query, "dot3bt", &dot3bt)) { + if (!(PAYLOAD_VALUE_BOOLEAN == is_admin_enabled->type && + PAYLOAD_VALUE_STRING == status->type && + (PAYLOAD_VALUE_NULL == power_mode->type || + PAYLOAD_VALUE_STRING == power_mode->type) && + (PAYLOAD_VALUE_NULL == assigned_class->type || + PAYLOAD_VALUE_NUMBER == assigned_class->type) && + PAYLOAD_VALUE_BOOLEAN == is_lldp_enabled->type && + (PAYLOAD_VALUE_NULL == dot3at->type || + PAYLOAD_VALUE_OBJECT == dot3at->type) && + (PAYLOAD_VALUE_NULL == dot3bt->type || + PAYLOAD_VALUE_OBJECT == dot3bt->type))) { + POE_ERR("Invalid payload type"); + func_status = LLDP_POED_ERR_INVALID_PAYLOAD; + goto parsing_failed; + } + if (0 == strcasecmp(status->value.val_str, "on")) { + /** + * If either LLDP processing is disabled or + * the data link is not active, then we'll return an error. + * In this case, the PD can still come back online later and send + * an L2 power request to be reconciled. + */ + if (!is_lldp_enabled->value.val_bool) { + POE_WARN("LLDP processing is disabled for port %s. Will skip " + "advertising", + port->ifname); + func_status = LLDP_POED_ERR_LLDP_PROCESSING_DISABLED; + *event = PORT_EVENT_ERR; + } else if (!port->if_up) { + POE_WARN("Port %s does not have an " + "active data link. Will skip advertising", + port->ifname); + func_status = LLDP_POED_ERR_INACTIVE_DATALINK; + *event = PORT_EVENT_ERR; + } else if ((0 == strcasecmp(power_mode->value.val_str, "l1") && + is_initial) || + (0 == strcasecmp(power_mode->value.val_str, "l2") && + !is_neighbor_already_reconciled(port->ifname)) || + (0 == strcasecmp(power_mode->value.val_str, "l2") && + !is_initial)) { /* If the port is already in L2 mode, + then this can't be the initial + advertisement. */ + if (PAYLOAD_VALUE_NULL == dot3at->type) { + POE_ERR("802.3at fields are mandatory"); + func_status = LLDP_POED_ERR_8023AT_FIELDS_MISSING; + goto parsing_failed; + } + /** + * We're good to send the MDI advertisement. + * Fill in the basic 802.1ab fields first. + */ + struct port_dot3_power_settings pse_config = { + .poe_device_type = LLDP_DOT3_POWER_PSE, + .mdi_supported = 1, + .mdi_enabled = 1, + .mdi_paircontrol = 1, + .pse_power_pair = LLDP_DOT3_POWERPAIRS_SIGNAL, + /* TODO: dual-signature PDs handling for Power Class. */ + .pd_class = ((assigned_class->value.val_int >= 4) + ? 5 + : assigned_class->value.val_int + 1), + /* Fill this in for 802.3bt ease of processing. */ + .pd_power_class_ext = assigned_class->value.val_int, + /* This is going to be enabled if dot3bt payload is present. + */ + .power_type_ext = LLDP_DOT3_POWER_8023BT_OFF, + }; + + if (0 != fill_at_power_settings(dot3at, &pse_config)) { + POE_ERR("Failed to fill in the 802.3at fields for %s.", + port->ifname); + func_status = LLDP_POED_ERR_INVALID_8023AT_FIELDS; + } else if (PAYLOAD_VALUE_OBJECT == dot3bt->type && + 0 != (func_status = fill_bt_power_settings( + dot3bt, &pse_config))) { + POE_ERR("Failed to fill in the 802.3bt fields for %s.", + port->ifname); + } else if (0 != (func_status = send_mdi_pse_advertisement( + port->ifname, &pse_config, + &port->timeout_time))) { + POE_ERR("Failed to send the MDI power advertisement for %s", + port->ifname); + port->timeout_time = 0; + } else { + /* Success. */ + POE_INFO( + "Successfully sent the MDI power advertisement for %s ", + port->ifname); + } + } + } else if (false == is_admin_enabled->value.val_bool || + 0 == strcasecmp(status->value.val_str, "off") || + 0 == strcasecmp(status->value.val_str, "err")) { + /* Port got disabled in the meantime or lost the PD connection. */ + *event = PORT_EVENT_LOST_POWER; + } + + port->admin_lldp_enabled = is_lldp_enabled; + } else + goto parsing_failed; + + free_payload(query); + free(query); + + return func_status; + +parsing_failed: + POE_ERR("Failed to parse the poed payload for %s", port->ifname); + free_payload(query); + free(query); + return func_status; +} + +/** + * process_l1_neg_complete_state - Process a port which finished the L1 + * negotiation + * @port: port to be processed + * @data: (ignored) + * + * Send the initial MDI power advertisement for the port which completed the L1 + * negotiation and that was classified by the PoE chipset, only if there is an + * active data and power link state and LLDP processing is enabled. If the + * advertisement was sent successfully, then this will transition to the next + * state (WAIT_LLDP_REQ). If sending the MDI advertisement fails for any reason, + * we fall back to the default power limit. + */ +DECLARE_STATE_HANDLER(PORT_L1_NEG_COMPLETE, process_l1_neg_complete_state) +{ + if (!port) { + POE_DEBUG("Port arg is NULL"); + return PORT_EVENT_IDLE; + } + + enum port_state_event result = PORT_EVENT_IDLE; + if (0 == advertise_pse_dot3_config(port, &result, true)) { + result = PORT_EVENT_OK; + } else if (PORT_EVENT_LOST_POWER != result) { + POE_WARN( + "Failed to send the initial MDI " + "power advertisement. Trying to set the default power limit for %s", + port->ifname); + if (0 != send_set_default_power_limit_request(port->id)) { + POE_ERR( + "Failed to set the default power limit for port %s. Will retry", + port->ifname); + result = PORT_EVENT_IDLE; + } else { + /** + * The statement is a bit misleading, as the default power limit + * was assigned successfully, but the dot3 advertisement failed. + * This means we are going to PORT_DEFAULT_PWR_LIMIT. + */ + result = PORT_EVENT_ERR; + port->lldp_default_pwr_limit_update_pending = true; + } + } + + if (PORT_EVENT_LOST_POWER == result) { + POE_NOTICE("Port %s lost the PD power link in l1_neg_complete state", + port->ifname); + } + + return result; +} + +/** + * send_set_l2_power_limit_request - Convert the power settings to a poed + * message for setting the new power limit + * @id: port ID + * @settings: the dot3 power settings to be used for the command + * + * Returns 0, LLDP_POED_ERR_OK, if successful, an lldp_poed_err otherwise. + */ +static int +send_set_l2_power_limit_request(port_id_t id, + const struct port_dot3_power_settings *settings) +{ + if (!settings) + return LLDP_POED_ERR_INVALID_PARAM; + + /** + * TODO: dual-signature reconciliation. + */ + struct poed_payload *query = create_set_power_limit_query( + id, false, settings->pd_requested, settings->power_priority, 0, 0); + if (0 != sync_send_poed_request(query, "set_power_limit")) { + POE_ERR("Failed to send set_power_limit request for port ID %d", id); + free_payload(query); + free(query); + return LLDP_POED_ERR_SEND_REQUEST_FAILED; + } + + const struct poed_payload *result = NULL; + int status = LLDP_POED_ERR_OK; + if (0 == find_payload_by_key(query, "result", &result)) { + if (PAYLOAD_VALUE_NUMBER != result->type || !result->value.val_int) + status = LLDP_POED_ERR_INVALID_PAYLOAD; + } else { + POE_ERR("Failed to parse the poed payload for port ID %d", id); + status = LLDP_POED_ERR_PARSE_ERROR; + } + free_payload(query); + free(query); + + return status; +} + +/** + * reconcile_pd_power_request - Compare the dot3 configuration with the current + * PSE config and apply the LLDP PD power request, if possible + * @config: PD dot3 power config to process + * @event: in case a transition to an error state is necessary + * @port: port to process + * + * Returns 0, LLDP_POED_ERR_OK, if successful, an lldp_poed_err otherwise. + */ +static int +reconcile_pd_power_request(const struct port_dot3_power_settings *config, + enum port_state_event *event, + struct port_state_machine *port) +{ + if (!config || !event || !port) + return LLDP_POED_ERR_INVALID_PARAM; + + /** + * Cannot rely on the initial port details that were advertised to the PD. + * Hence, fetch again the PSE config. + */ + struct poed_payload *query = create_get_port_details_query(port->id); + if (0 != sync_send_poed_request(query, "get_port_details")) { + POE_ERR("Failed to send " + "get_port_details request for %s", + port->ifname); + free_payload(query); + free(query); + return LLDP_POED_ERR_GETPORTDETAILS_FAILED; + } + + const struct poed_payload *is_admin_enabled = NULL; + const struct poed_payload *status = NULL; + const struct poed_payload *power_mode = NULL; + const struct poed_payload *assigned_class = NULL; + const struct poed_payload *is_lldp_enabled = NULL; + const struct poed_payload *dot3at = NULL; + const struct poed_payload *dot3bt = NULL; + int func_status = LLDP_POED_ERR_OK; + if (0 == find_payload_by_key(query, "is_admin_enabled", &is_admin_enabled) && + 0 == find_payload_by_key(query, "status", &status) && + 0 == find_payload_by_key(query, "power_mode", &power_mode) && + 0 == find_payload_by_key(query, "assigned_class", &assigned_class) && + 0 == find_payload_by_key(query, "is_lldp_enabled", &is_lldp_enabled) && + 0 == find_payload_by_key(query, "dot3at", &dot3at) && + 0 == find_payload_by_key(query, "dot3bt", &dot3bt)) { + if (!(PAYLOAD_VALUE_BOOLEAN == is_admin_enabled->type && + PAYLOAD_VALUE_STRING == status->type && + (PAYLOAD_VALUE_NULL == power_mode->type || + PAYLOAD_VALUE_STRING == power_mode->type) && + (PAYLOAD_VALUE_NULL == assigned_class->type || + PAYLOAD_VALUE_NUMBER == assigned_class->type) && + PAYLOAD_VALUE_BOOLEAN == is_lldp_enabled->type && + (PAYLOAD_VALUE_NULL == dot3at->type || + PAYLOAD_VALUE_OBJECT == dot3at->type) && + (PAYLOAD_VALUE_NULL == dot3bt->type || + PAYLOAD_VALUE_OBJECT == dot3bt->type))) { + POE_ERR("Invalid payload type"); + func_status = LLDP_POED_ERR_INVALID_PAYLOAD; + goto parsing_failed; + } + if (false == is_admin_enabled->value.val_bool || + 0 == strcasecmp(status->value.val_str, "off") || + 0 == strcasecmp(status->value.val_str, "err")) { + /* Port got disabled in the meantime or lost the PD connection. */ + *event = PORT_EVENT_LOST_POWER; + free_payload(query); + free(query); + return LLDP_POED_ERR_PORT_GOT_DISABLED; + } + + if (PAYLOAD_VALUE_NULL == dot3at->type) { + POE_ERR("802.3at fields are mandatory"); + func_status = LLDP_POED_ERR_8023AT_FIELDS_MISSING; + goto parsing_failed; + } + if (0 == strcasecmp(status->value.val_str, "on")) { + if (!is_lldp_enabled->value.val_bool) { + POE_WARN("LLDP processing is " + "disabled for port %s", + port->ifname); + func_status = LLDP_POED_ERR_LLDP_PROCESSING_DISABLED; + } else if (!port->if_up) { + POE_WARN("Port %s does not have an " + "active data link", + port->ifname); + func_status = LLDP_POED_ERR_INACTIVE_DATALINK; + } else { + /** + * Parse the local PSE config from the query. + * Note that the 802.1ab fields are left out intentionally. + */ + struct port_dot3_power_settings pse_config = { + .power_type_ext = LLDP_DOT3_POWER_8023BT_OFF, + }; + if (0 != fill_at_power_settings(dot3at, &pse_config)) { + POE_ERR("Failed to parse the 802.3at fields for %s", + port->ifname); + func_status = LLDP_POED_ERR_INVALID_8023AT_FIELDS; + } else if (PAYLOAD_VALUE_OBJECT == dot3bt->type && + 0 != (func_status = fill_bt_power_settings( + dot3bt, &pse_config))) { + POE_ERR("Failed to parse the the 802.3bt fields for %s", + port->ifname); + func_status = LLDP_POED_ERR_INVALID_8023BT_FIELDS; + } + + if (LLDP_DOT3_POWER_PSE == config->poe_device_type) { + /* Somebody plugged in a PSE instead of a PD... */ + POE_WARN("Unexpected PD PoE device type for port %s", + port->ifname); + func_status = LLDP_POED_ERR_UNEXPECTED_DEVICE_TYPE; + } else if (0x0 == config->pd_load || + LLDP_DOT3_POWER_8023BT_OFF == + config->power_type_ext) { + /** + * If we don't support, as a PSE, the 802.3bt standard, just + * hope for the best... Alternatively, rely on the PSE + * maximum available power field. + */ + if (LLDP_DOT3_POWER_8023BT_OFF == + pse_config.power_type_ext || + config->pd_requested <= + pse_config.pse_max_available_power) { + func_status = + send_set_l2_power_limit_request(port->id, config); + } else { + POE_ERR("Failed to set the L2 TPPL for %s", + port->ifname); + func_status = LLDP_POED_ERR_FAILED_TO_SET_L2_TPPL; + } + } else if (0x1 == config->pd_load) { + /** + * TODO: Reconcile dual-signature + */ + POE_ERR("Dual-signature PDs are not supported"); + func_status = LLDP_POED_ERR_DUALSIG_PD_NOT_SUPPORTED; + } + } + } + port->admin_lldp_enabled = is_lldp_enabled; + } else + goto parsing_failed; + + free_payload(query); + free(query); + + return func_status; + +parsing_failed: + POE_ERR("Failed to parse the poed " + "payload for %s", + port->ifname); + free_payload(query); + free(query); + return func_status; +} + +/** + * is_port_on - Check if the port is operationally active + * @id: port ID + * + * If the request or parsing fails, this will return false. + * + * Returns true, if the port is on, false otherwise. + */ +static bool is_port_on(port_id_t id) +{ + struct poed_payload *query = create_get_port_details_query(id); + if (LLDP_POED_ERR_OK != sync_send_poed_request(query, "get_port_details")) { + POE_ERR("Failed to send get_port_details request for port ID %d", id); + free_payload(query); + free(query); + return false; + } + + bool result = false; + const struct poed_payload *status = NULL; + if (0 == find_payload_by_key(query, "status", &status)) { + if (PAYLOAD_VALUE_STRING != status->type) { + POE_ERR("Invalid payload type"); + goto parsing_failed; + } + + if (0 == strcasecmp("on", status->value.val_str)) + result = true; + } else + goto parsing_failed; + + free_payload(query); + free(query); + + return result; + +parsing_failed: + POE_ERR("Failed to parse the poed payload for port ID %d", id); + free_payload(query); + free(query); + return result; +} + +/** + * process_wait_lldp_req_state - Process a port waiting for an LLDP power + * request + * @port: port to be processed + * @data: LLDP neighbor update, containing the power request (nullable) + * + * If the handler receives a non-NULL update, then this update is parsed and the + * request is reconciled against the current port status. If the update is NULL, + * then this will compare the current time with the timeout value and fall back + * to the default power limit, if the PD timed out. + */ +DECLARE_STATE_HANDLER(PORT_WAIT_LLDP_REQ, process_wait_lldp_req_state) +{ + if (!port) { + POE_ERR("Port arg is NULL"); + return PORT_EVENT_IDLE; + } + + /** + * Need to check first on the port status, otherwise we run + * the risk of stalling in the current state. + */ + enum port_state_event result = PORT_EVENT_IDLE; + bool port_on = false; + if (!(port_on = is_port_on(port->id))) + result = PORT_EVENT_LOST_POWER; + + int func_status = LLDP_POED_ERR_OK; + const struct port_neighbor_update *update = data; + if (port_on && update) { + if (update->was_deleted) { + /** + * We lost the LLDP neighbor here. We will most likely + * transitions to default power state when the timeout expires. + */ + POE_WARN("Unexpected deleted neighbor"); + func_status = LLDP_POED_ERR_UNEXPECTED_DELETED_NEIGHBOR; + } else { + if (0 == (func_status = reconcile_pd_power_request( + &update->settings, &result, port))) { + POE_INFO("PD power request reconciled successfully for %s", + port->ifname); + if (0 == (func_status = advertise_pse_dot3_config(port, &result, + false))) { + POE_INFO("Advertised the new " + "power configuration successfully for %s", + port->ifname); + result = PORT_EVENT_OK; + } else if (PORT_EVENT_LOST_POWER != result) { + POE_ERR("Failed to advertise " + "the new power configuration for %s", + port->ifname); + result = PORT_EVENT_IDLE; + } + } else { + POE_WARN("Failed to reconcile the power request for %s", + port->ifname); + } + } + } else if (port_on) { + /** + * If there is no update, check if the timeout hasn't expired yet. + */ + time_t current_time = time(NULL); + if (current_time > port->timeout_time) { + if (LLDP_POED_ERR_OK != + (func_status = + send_set_default_power_limit_request(port->id))) { + POE_ERR("Failed to set the default power limit for port %s", + port->ifname); + } else { + result = PORT_EVENT_LLDP_TIMEOUT; + port->timeout_time = 0; + port->lldp_default_pwr_limit_update_pending = true; + } + } + } + + if (PORT_EVENT_LOST_POWER == result) + POE_NOTICE("Port %s lost the PD power link in wait_lldp_req state", + port->ifname); + + return result; +} + +/** + * process_default_pwr_limit_state - Process a port that failed the L2 + * negotiation or timed out + * @port: port to be processed + * @data: LLDP neighbor update, containing the power request (nullable) + * + * If the LLDP neighbor will send a valid PD power request, we'll still try to + * reconcile it. + */ +DECLARE_STATE_HANDLER(PORT_DEFAULT_PWR_LIMIT, process_default_pwr_limit_state) +{ + if (!port) { + POE_DEBUG("Port arg is NULL"); + return PORT_EVENT_IDLE; + } + + /** + * Need to check first on the port status, otherwise we run + * the risk of stalling in the current state. + */ + enum port_state_event result = PORT_EVENT_IDLE; + bool port_on = false; + if (!(port_on = is_port_on(port->id))) { + port->lldp_default_pwr_limit_update_pending = false; + return PORT_EVENT_LOST_POWER; + } + + /** + * A port may be flapped without the state machine detecting the transition + * and lose the TPPL. Hence, ensuring that the default power limit + * is still present. + */ + if (0 != send_set_default_power_limit_request(port->id)) { + POE_ERR("Failed to set the default power limit for port %s. Will retry", + port->ifname); + } else if (port->lldp_default_pwr_limit_update_pending) { + enum port_state_event lldp_result = PORT_EVENT_IDLE; + if (LLDP_POED_ERR_OK == + advertise_pse_dot3_config(port, &lldp_result, false)) { + POE_INFO("Advertised the default power configuration " + "successfully for %s", + port->ifname); + port->lldp_default_pwr_limit_update_pending = false; + } else if (PORT_EVENT_LOST_POWER != lldp_result) { + POE_WARN("Failed to advertise the new power configuration " + "for %s", + port->ifname); + } else { + port->lldp_default_pwr_limit_update_pending = false; + return PORT_EVENT_LOST_POWER; + } + } + + int func_status = LLDP_POED_ERR_OK; + const struct port_neighbor_update *update = data; + if (update) { + if (update->was_deleted) { + /** + * We lost the LLDP neighbor here. If it doesn't + * come back, the port will remain in the L1 default power + * state forever. + */ + POE_DEBUG("Unexpected deleted neighbor"); + } else { + if (LLDP_POED_ERR_OK == (func_status = reconcile_pd_power_request( + &update->settings, &result, port))) { + POE_INFO("PD power request reconciled successfully for %s", + port->ifname); + if (LLDP_POED_ERR_OK == + advertise_pse_dot3_config(port, &result, false)) { + POE_INFO("Advertised the new power configuration " + "successfully for %s", + port->ifname); + result = PORT_EVENT_OK; + port->lldp_default_pwr_limit_update_pending = false; + } else if (PORT_EVENT_LOST_POWER != result) { + POE_WARN("Failed to advertise the new power configuration " + "for %s", + port->ifname); + result = PORT_EVENT_IDLE; + } + } else { + POE_WARN("Failed to reconcile the power request for %s", + port->ifname); + } + } + } + + if (PORT_EVENT_LOST_POWER == result) { + POE_NOTICE("Port %s lost the PD power link in default_pwr_limit state", + port->ifname); + port->lldp_default_pwr_limit_update_pending = false; + } + + return result; +} + +/** + * process_l2_neg_complete_state - Process a port that has finished the L2 + * negotiation successfully + * @port: port to be processed + * @data: LLDP neighbor update for deleted neighbors + */ +DECLARE_STATE_HANDLER(PORT_L2_NEG_COMPLETE, process_l2_neg_complete_state) +{ + if (!port) { + POE_DEBUG("port arg is NULL"); + return PORT_EVENT_IDLE; + } + + struct poed_payload *query = create_get_port_details_query(port->id); + if (0 != sync_send_poed_request(query, "get_port_details")) { + POE_ERR("Failed to send get_port_details request for %s", port->ifname); + free_payload(query); + free(query); + return PORT_EVENT_IDLE; + } + + /** + * Just check if the port is still providing power. + * If the port is still active, must check if the LLDP neighbor has not aged + * out. + */ + const struct poed_payload *status = NULL; + const struct port_neighbor_update *update = data; + enum port_state_event result = PORT_EVENT_IDLE; + if (0 == find_payload_by_key(query, "status", &status)) { + if (PAYLOAD_VALUE_STRING != status->type) { + POE_ERR("Invalid payload type"); + goto parsing_failed; + } + + if (0 == strcasecmp(status->value.val_str, "off")) + result = PORT_EVENT_LOST_POWER; + else if (update && update->was_deleted) { + /** + * TODO: Disabling L2 mode is impossible for some firmware versions. + * At the moment, the port will remain in L2 mode, even though the + * LLDP neighbor aged out. + */ + POE_WARN("Port %s neighbor aged out and got deleted", port->ifname); + result = PORT_EVENT_ERR; + } + } else + goto parsing_failed; + + free_payload(query); + free(query); + + if (PORT_EVENT_LOST_POWER == result) { + POE_NOTICE("Port %s lost the PD power link in l2_neg_complete state", + port->ifname); + } + + return result; + +parsing_failed: + POE_ERR("Failed to parse the poed payload for %s", port->ifname); + free_payload(query); + free(query); + return PORT_EVENT_IDLE; +} + +/** + * process_lost_power_link_state - Process a port which lost the power link + * @port: port to be processed + * @data: caller custom data + */ +DECLARE_STATE_HANDLER(PORT_LOST_POWER_LINK, process_lost_power_link_state) +{ + if (!port) { + POE_DEBUG("Port arg is NULL"); + return PORT_EVENT_IDLE; + } + + POE_WARN("Physical connection lost for port %s. Will reinitialize the port", + port->ifname); + + struct poed_payload *query = create_get_port_details_query(port->id); + if (LLDP_POED_ERR_OK == sync_send_poed_request(query, "get_port_details")) { + const struct poed_payload *status = NULL; + if (0 == find_payload_by_key(query, "status", &status) && + PAYLOAD_VALUE_STRING == status->type) { + POE_NOTICE("Port %d state is %s", port->id, status->value.val_str); + } else + POE_ERR("Failed to parse the poed payload for %s", port->ifname); + } else + POE_ERR("Failed to send get_port_details request for %s", port->ifname); + free_payload(query); + free(query); + + return PORT_EVENT_OK; +} + +/* State handlers end */ + +static pthread_mutex_t port_mutex; + +/** + * push_if_link_update - Update the operational state of port, based on the link + * change event + * @ifname: network interface name + * @event: link change event + * + * Returns 0 if the update was processed successfully, 1 otherwise. + */ +int push_if_link_update(const char *ifname, enum port_if_link_event event) +{ + struct port_state_machine *port = NULL; + if (0 != get_port_context_by_ifname(ifname, &port)) { + POE_ERR("Failed to find port %s by ifname", ifname); + return 1; + } + + POE_DEBUG("Received an %s event for %s interface", + (PORT_IF_UP == event) + ? "IF_UP" + : ((PORT_IF_DOWN == event) ? "IF_DOWN" : "Unknown"), + ifname); + + int status = 0; + pthread_mutex_lock(&port_mutex); + switch (event) { + case PORT_IF_UP: + port->if_up = true; + break; + case PORT_IF_DOWN: + port->if_up = false; + break; + default: + status = 1; + break; + } + pthread_mutex_unlock(&port_mutex); + + return status; +} + +/** + * Flag set to true when there's a new pending LLDP update. + */ +static volatile bool has_lldp_update = false; + +/** + * Local request queue to be processed every time a new LLDP update is received. + */ +static struct queue lldp_request_queue; + +/** + * log_lldp_update - Log the neighbor update field for debug purposes + * @update: caller-initialized update + */ +static void log_lldp_update(const struct port_neighbor_update *update) +{ + if (!update) + return; + + POE_DEBUG("Neighbor port ID: %d", update->id); + POE_DEBUG("PoE device type: %X", update->settings.poe_device_type); + POE_DEBUG("MDI supported: %X", update->settings.mdi_supported); + POE_DEBUG("MDI enabled: %X", update->settings.mdi_enabled); + POE_DEBUG("MDI paircontrol: %X", update->settings.mdi_paircontrol); + POE_DEBUG("PSE power pair: %X", update->settings.pse_power_pair); + POE_DEBUG("PD class: %X", update->settings.pd_class); + if (update->settings.power_type > LLDP_DOT3_POWER_8023AT_OFF) { + POE_DEBUG("Power type: %X", update->settings.power_type); + POE_DEBUG("Power source: %X", update->settings.power_source); + POE_DEBUG("Power priority: %X", update->settings.power_priority); + POE_DEBUG("PD requested power: %X", update->settings.pd_requested); + POE_DEBUG("PSE allocated power: %X", update->settings.pse_allocated); + } + if (update->settings.power_type_ext > LLDP_DOT3_POWER_8023BT_OFF) { + POE_DEBUG("PD 4PID: %X", update->settings.pd_4pid); + POE_DEBUG("PD requested A: %X", update->settings.pd_requested_a); + POE_DEBUG("PD requested B: %X", update->settings.pd_requested_b); + POE_DEBUG("PSE allocated A: %X", update->settings.pse_allocated_a); + POE_DEBUG("PSE allocated B: %X", update->settings.pse_allocated_b); + POE_DEBUG("PSE status: %X", update->settings.pse_status); + POE_DEBUG("PD status: %X", update->settings.pd_status); + POE_DEBUG("PSE pairs ext: %X", update->settings.pse_pairs_ext); + POE_DEBUG("Power class mode A: %X", + update->settings.power_class_mode_a); + POE_DEBUG("Power class mode B: %X", + update->settings.power_class_mode_b); + POE_DEBUG("PD power class ext: %X", + update->settings.pd_power_class_ext); + POE_DEBUG("Power type ext: %X", update->settings.power_type_ext); + POE_DEBUG("PD load: %X", update->settings.pd_load); + POE_DEBUG("PSE max available power: %X", + update->settings.pse_max_available_power); + } +} + +/** + * push_lldp_neighbor_update - Enqueue neighbor update to be processed + * @ifname: network interface name + * @config: neighbor Dot3 power settings (nullable) + * + * If the @config comes in as NULL, then this is treated as the neighbor was + * deleted. Memory allocation for @config must be managed by the caller. + * + * Returns 0, LLDP_POED_ERR_OK, if successful, an lldp_poed_err otherwise. + * + * Note: the processing here is asynchronous, so the caller doesn't have to wait + * for the whole propagation down to the driver to happen. + */ +int push_lldp_neighbor_update(const char *ifname, + const struct port_dot3_power_settings *config) +{ + if (!ifname) + return LLDP_POED_ERR_INVALID_PARAM; + + struct port_state_machine *port = NULL; + if (0 != get_port_context_by_ifname(ifname, &port)) { + POE_ERR("Failed to find port %s by ifname", ifname); + return LLDP_POED_ERR_GETPORTDETAILS_FAILED; + } + + has_lldp_update = true; + struct port_neighbor_update *update = + malloc(sizeof(struct port_neighbor_update)); + if (!update) + return LLDP_POED_ERR_INTERNAL_ERROR; + + update->id = port->id; + if (config) { + memcpy(&(update->settings), config, + sizeof(struct port_dot3_power_settings)); + update->was_deleted = false; + } else { + update->was_deleted = true; + POE_DEBUG("LLDP neighbor for port %s got deleted", ifname); + } + log_lldp_update(update); + + /** + * Enqueue the update to be processed by the state machine thread. + */ + struct linked_list *node = malloc(sizeof(struct linked_list)); + if (!node) { + free(update); + return LLDP_POED_ERR_INTERNAL_ERROR; + } + node->value = update; + node->next = NULL; + q_enqueue(&lldp_request_queue, node); + + return LLDP_POED_ERR_OK; +} + +int med_to_dot3(const struct port_med_power_settings *med_config, + struct port_dot3_power_settings *dot3_config) +{ + #define RET(val) ({return val; val;}) + + memset(dot3_config, 0, sizeof(struct port_dot3_power_settings)); + + dot3_config->poe_device_type = + med_config->poe_device_type == LLDP_MED_POW_TYPE_PSE ? + LLDP_DOT3_POWER_PSE : + med_config->poe_device_type == LLDP_MED_POW_TYPE_PD ? + LLDP_DOT3_POWER_PD : + RET(LLDP_POED_ERR_INVALID_PARAM); + + if (med_config->poe_device_type == LLDP_MED_POW_TYPE_PSE) { + dot3_config->power_source = + med_config->power_source == LLDP_MED_POW_SOURCE_UNKNOWN ? + LLDP_DOT3_POWER_SOURCE_UNKNOWN : + med_config->power_source == LLDP_MED_POW_SOURCE_PRIMARY ? + LLDP_DOT3_POWER_SOURCE_PRIMARY : + med_config->power_source == LLDP_MED_POW_SOURCE_BACKUP ? + LLDP_DOT3_POWER_SOURCE_BACKUP : + RET(LLDP_POED_ERR_INVALID_PARAM); + } else { + dot3_config->power_source = + med_config->power_source == LLDP_MED_POW_SOURCE_UNKNOWN ? + LLDP_DOT3_POWER_SOURCE_UNKNOWN : + med_config->power_source == LLDP_MED_POW_SOURCE_PSE ? + LLDP_DOT3_POWER_SOURCE_PSE : + med_config->power_source == LLDP_MED_POW_SOURCE_LOCAL ? + LLDP_DOT3_POWER_SOURCE_LOCAL : + med_config->power_source == LLDP_MED_POW_SOURCE_BOTH ? + LLDP_DOT3_POWER_SOURCE_BOTH : + RET(LLDP_POED_ERR_INVALID_PARAM); + } + + dot3_config->power_priority = + med_config->power_priority == LLDP_MED_POW_PRIO_UNKNOWN ? + LLDP_DOT3_POWER_PRIO_UNKNOWN : + med_config->power_priority == LLDP_MED_POW_PRIO_CRITICAL ? + LLDP_DOT3_POWER_PRIO_CRITICAL : + med_config->power_priority == LLDP_MED_POW_PRIO_HIGH ? + LLDP_DOT3_POWER_PRIO_HIGH : + med_config->power_priority == LLDP_MED_POW_PRIO_LOW ? + LLDP_DOT3_POWER_PRIO_LOW : + RET(LLDP_POED_ERR_INVALID_PARAM); + + #undef RET + + /** + * Map MED to dot3ab + */ + dot3_config->power_type_ext = LLDP_DOT3_POWER_8023BT_OFF; + dot3_config->pd_requested = med_config->value; + dot3_config->power_type = LLDP_DOT3_POWER_8023AT_TYPE2; + /** + * 802.1ab fields that are not transmitted by a PD and hence set to -1: MDI + * power support, MDI power state, PSE pairs control and PSE power pair and + * PD power class. + */ + dot3_config->mdi_supported = -1; + dot3_config->mdi_enabled = -1; + dot3_config->mdi_paircontrol = -1; + dot3_config->pse_power_pair = -1; + dot3_config->pd_class = -1; + + return LLDP_POED_ERR_OK; +} + +int dot3_to_med(const struct port_dot3_power_settings *dot3_config, + struct port_med_power_settings *med_config) +{ + #define RET(val) ({return val; val;}) + + med_config->poe_device_type = + dot3_config->poe_device_type == LLDP_DOT3_POWER_PSE ? + LLDP_MED_POW_TYPE_PSE : + dot3_config->poe_device_type == LLDP_DOT3_POWER_PD ? + LLDP_MED_POW_TYPE_PD : + RET(LLDP_POED_ERR_INVALID_PARAM); + + if (med_config->poe_device_type == LLDP_MED_POW_TYPE_PSE) { + med_config->power_source = + dot3_config->power_source == LLDP_DOT3_POWER_SOURCE_UNKNOWN ? + LLDP_MED_POW_SOURCE_UNKNOWN : + dot3_config->power_source == LLDP_DOT3_POWER_SOURCE_PRIMARY ? + LLDP_MED_POW_SOURCE_PRIMARY : + dot3_config->power_source == LLDP_DOT3_POWER_SOURCE_BACKUP ? + LLDP_MED_POW_SOURCE_BACKUP : + RET(LLDP_POED_ERR_INVALID_PARAM); + } else { + med_config->power_source = + dot3_config->power_source == LLDP_DOT3_POWER_SOURCE_UNKNOWN ? + LLDP_MED_POW_SOURCE_UNKNOWN : + dot3_config->power_source == LLDP_DOT3_POWER_SOURCE_PSE ? + LLDP_MED_POW_SOURCE_PSE : + dot3_config->power_source == LLDP_DOT3_POWER_SOURCE_LOCAL ? + LLDP_MED_POW_SOURCE_LOCAL : + dot3_config->power_source == LLDP_DOT3_POWER_SOURCE_BOTH ? + LLDP_MED_POW_SOURCE_BOTH : + RET(LLDP_POED_ERR_INVALID_PARAM); + } + + med_config->power_priority = + dot3_config->power_priority == LLDP_DOT3_POWER_PRIO_UNKNOWN ? + LLDP_MED_POW_PRIO_UNKNOWN : + dot3_config->power_priority == LLDP_DOT3_POWER_PRIO_CRITICAL ? + LLDP_MED_POW_PRIO_CRITICAL : + dot3_config->power_priority == LLDP_DOT3_POWER_PRIO_HIGH ? + LLDP_MED_POW_PRIO_HIGH : + dot3_config->power_priority == LLDP_DOT3_POWER_PRIO_LOW ? + LLDP_MED_POW_PRIO_LOW : + RET(LLDP_POED_ERR_INVALID_PARAM); + + #undef RET + + med_config->value = dot3_config->pse_allocated; + + return LLDP_POED_ERR_OK; +} + +/** + * fill_port_range - Fill the port range based on the current port count + * @pr: caller-allocated port range + * + * Returns 0, LLDP_POED_ERR_OK, if successful, an lldp_poed_err otherwise. + */ +static int fill_port_range(struct port_range *pr) +{ + if (!pr || !ports.p) + return LLDP_POED_ERR_INVALID_PARAM; + + memset(pr, 0, sizeof(struct port_range)); + strncpy(pr->ifname_prefix, PORT_INTERFACE_NAME_PREFIX, IFNAME_PREFIX_SIZE); + /** + * Assumptions is that all ports are spread contiguously. + * If the ports are interleaved with non-PoE ports, the behavior is + * undefined. + */ + pr->start_index = 1; + pr->end_index = ports.size; + + return LLDP_POED_ERR_OK; +} + +/** + * wait_for_poed_response - Write the @message into the named pipe and then poll + * for the response, retrying if there was an error along the way. The response + * buffer is copied back in @message + * @message: used for both for sending the request and copying back the response + * + * Returns 0 if successful. This function will always retry to send and receive + * a response from poed. + */ +static int wait_for_poed_response(char *message, size_t message_len) +{ + /** + * Number of milliseconds to wait for the poed response through polling. + */ + static const size_t poed_reply_timeout_ms = 5000U; + + /** + * Write the request in blocking mode first and then poll for the reply + * from the poed, having a pre-defined timeout. + */ + int write_fd = open(WRITE_FIFO_PATH, O_WRONLY); + if (write_fd < 0) { + POE_ERR("Failed to open the write FIFO: %s", strerror(errno)); + return 1; + } + message[message_len - 1] = '\0'; + if (write(write_fd, message, strlen(message)) < 0) { + POE_ERR("Failed to write to FIFO: %s", strerror(errno)); + close(write_fd); + return 1; + } + close(write_fd); + + int read_fd = open(READ_FIFO_PATH, O_RDONLY | O_NONBLOCK); + if (read_fd < 0) { + POE_ERR("Failed to open the read FIFO: %s", strerror(errno)); + return 1; + } + struct pollfd waiter = { + .fd = read_fd, + .events = POLLIN, + }; + while (1) { + int status = poll(&waiter, 1, poed_reply_timeout_ms); + switch (status) { + case 0: + POE_DEBUG("Poed reply timed out. " + "Retrying..."); + break; + case 1: + if (waiter.revents & POLLIN) { + ssize_t ret = read(waiter.fd, message, POED_MESSAGE_MAX_SIZE); + if (ret > 0) { + /** + * Got the response, can exit now. + */ + goto success; + } + while (-1 == ret && EINTR == errno) { + /** + * Syscall got interrupted. Must retry. + */ + ret = read(waiter.fd, message, POED_MESSAGE_MAX_SIZE); + } + if (-1 == ret && EAGAIN == errno) { + /** + * Retry polling the FD again. + */ + continue; + } else if (0 == ret) /* Connection was closed. */ + { + POE_ERR("Received EOF"); + goto fail; + } + goto success; + } else /* POLLERR or POLLHUP */ + { + POE_ERR("Read pipe is in invalid state " + "(not open or closed prematurely)"); + goto fail; + } + break; + default: + POE_ERR("Unexpected poed polling error: %s", strerror(errno)); + goto fail; + } + } + +success: + close(read_fd); + return 0; + +fail: + close(read_fd); + return 1; +} + +/** + * Number of microseconds to wait for a retry, in case the poed request failed. + */ +static const useconds_t poed_retry_interval_us = 1000000U; + +/** + * sync_send_poed_request - Send a synchronous request to poed, waiting for the + * response + * @query: query params to be copied to the JSON-RPC message. This is going + * to be used for filling back the response from poed + * @method: JSON-RPC method + * + * @warning: caller has the responsibilty to free the @query + * + * Returns 0, LLDP_POED_ERR_OK, if successful, an lldp_poed_err otherwise. + * + * Note: the request ID is incremented automatically and must match the ID from + * the poed reply. + */ +static int sync_send_poed_request(struct poed_payload *query, + const char *method) +{ + if (!query || !method) + return 1; + + POE_DEBUG("Sending request for %s method", method); + log_payload(query); + + char json_rpc_message[POED_MESSAGE_MAX_SIZE]; + memset(json_rpc_message, '\0', POED_MESSAGE_MAX_SIZE); + ssize_t request_id; + if (0 != payload_to_json_rpc(query, method, &request_id, json_rpc_message, + POED_MESSAGE_MAX_SIZE)) { + POE_ERR("Unable to create the JSON-RPC request " + "for %s", + method); + log_payload(query); + return LLDP_POED_ERR_SERIALIZE_ERROR; + } + + while (0 != wait_for_poed_response(json_rpc_message, POED_MESSAGE_MAX_SIZE)) { + POE_WARN("Retrying..."); + usleep(poed_retry_interval_us); + } + + free_payload(query); /* Reuse the same query. */ + if (0 != json_rpc_to_payload(json_rpc_message, POED_MESSAGE_MAX_SIZE, + request_id, query)) { + POE_ERR("Unable to parse the JSON-RPC response " + "for %s", + method); + return LLDP_POED_ERR_SERIALIZE_ERROR; + } + + POE_DEBUG("Received valid response from poed for %s method", method); + log_payload(query); + + return LLDP_POED_ERR_OK; +} + +/** + * init_ports - Query poed to determine the number of ports and determine the + * already disabled/enabled PoE ports. + * + * Returns 0, LLDP_POED_ERR_OK, if successful, an lldp_poed_err otherwise. + */ +static int init_ports(void) +{ + struct poed_payload port_query = { + .type = PAYLOAD_VALUE_OBJECT, + .child_count = 0, /* No request params. */ + .children = NULL, + }; + if (0 != sync_send_poed_request(&port_query, "get_disabled_ports")) { + POE_ERR("Failed to send the request for detecting the PoE ports"); + free_payload(&port_query); + return LLDP_POED_ERR_SEND_REQUEST_FAILED; + } + + /** + * Parse the payload and initialize all ports. + */ + const struct poed_payload *port_count = NULL; + const struct poed_payload *disabled_ports = NULL; + const struct poed_payload *lldp_disabled_ports = NULL; + if (0 == find_payload_by_key(&port_query, "ports_total_count", + &port_count) && + 0 == find_payload_by_key(&port_query, "disabled_ports", + &disabled_ports) && + 0 == find_payload_by_key(&port_query, "lldp_disabled_ports", + &lldp_disabled_ports)) { + if (!(PAYLOAD_VALUE_NUMBER == port_count->type && + (PAYLOAD_VALUE_ARRAY == disabled_ports->type || + PAYLOAD_VALUE_NULL == disabled_ports->type) && + (PAYLOAD_VALUE_ARRAY == lldp_disabled_ports->type || + PAYLOAD_VALUE_NULL == lldp_disabled_ports->type))) { + POE_ERR("Invalid payload type"); + goto parsing_failed; + } + + /** + * Initialize, by default, all ports to WAIT_LINK, having no + * active data link. + */ + ports.size = port_count->value.val_int; + ports.p = malloc(ports.size * sizeof(struct port_state_machine)); + FOR_I_IN(0, ports.size - 1) + { + int port_id = i + 1; + ports.p[i].id = port_id; + snprintf(ports.p[i].ifname, IFNAMSIZ, "%s%d", + PORT_INTERFACE_NAME_PREFIX, port_id); + ports.p[i].admin_lldp_enabled = true; + ports.p[i].lldp_default_pwr_limit_update_pending = false; + ports.p[i].if_up = false; + ports.p[i].timeout_time = 0; + ports.p[i].current_state = PORT_WAIT_PD; + ports.p[i].process_state = state_handlers[PORT_WAIT_PD]; + } + if (PAYLOAD_VALUE_ARRAY == disabled_ports->type) { + /** + * Override disabled ports and transition to PORT_DISABLED. + */ + const struct poed_payload *port_it = NULL; + FOR_EACH(port_it, disabled_ports->children, + disabled_ports->child_count) + { + if (PAYLOAD_VALUE_NUMBER != port_it->type) { + POE_ERR("Invalid payload type"); + goto parsing_failed; + } + + struct port_state_machine *port = NULL; + if (0 != + get_port_context_by_id(port_it->value.val_int, &port)) { + POE_ERR("Failed to find port by ID: %d", + port_it->value.val_int); + goto parsing_failed; + } + port->current_state = PORT_DISABLED; + port->process_state = state_handlers[PORT_DISABLED]; + } + } + if (PAYLOAD_VALUE_ARRAY == lldp_disabled_ports->type) { + /** + * Reflect the LLDP admin state set by the user. + */ + const struct poed_payload *port_it = NULL; + FOR_EACH(port_it, disabled_ports->children, + disabled_ports->child_count) + { + if (PAYLOAD_VALUE_NUMBER != port_it->type) { + POE_ERR("Invalid payload type"); + goto parsing_failed; + } + + struct port_state_machine *port = NULL; + if (0 != + get_port_context_by_id(port_it->value.val_int, &port)) { + POE_ERR("Failed to find port by ID: %d", + port_it->value.val_int); + goto parsing_failed; + } + port->admin_lldp_enabled = false; + } + } + } else + goto parsing_failed; + + POE_NOTICE("State machine was initialized successfully for all ports"); + free_payload(&port_query); + + return LLDP_POED_ERR_OK; + +parsing_failed: + POE_ERR("Failed to parse the poed payload for detecting the " + "PoE ports"); + free_payload(&port_query); + return LLDP_POED_ERR_PARSE_ERROR; +} + +/** + * create_poed_fifo - Creates the named pipe FIFO to communicate with the poed + * agent. + * + * Returns 0 if successful, otherwise 1. + */ +static int create_poed_fifo(void) +{ + /* Create the FIFOs with the poed agent, if inexistent. */ + int status = mkfifo(READ_FIFO_PATH, 0600); + if (status != 0) { + if (errno != EEXIST) { + POE_ERR("Failed to create " READ_FIFO_PATH " FIFO: %s", + strerror(errno)); + return 1; + } + POE_WARN("LLDP-POED <-> POED read FIFO exists, not creating one"); + } + status = mkfifo(WRITE_FIFO_PATH, 0600); + if (status != 0) { + if (errno != EEXIST) { + POE_ERR("Failed to create " WRITE_FIFO_PATH " FIFO: %s", + strerror(errno)); + return 1; + } + POE_WARN("LLDP-POED <-> POED write FIFO exists, not creating one"); + } + + return 0; +} + +/** + * process_port - Process the port state machine by calling the state handler + * and setting the new state based on the handler generated event + * @port: port state machine to be processed + * @data: optional data arg + */ +static void process_port(struct port_state_machine *port, const void *data) +{ + if (!port) + return; + + enum port_state_event ev = port->process_state(port, data); + if (PORT_EVENT_IDLE == ev) { + POE_DEBUG("Port %s remained in %s", port->ifname, + port_state_string[port->current_state]); + return; /* Skip updating the state if IDLE. */ + } + + enum port_state prev_state = port->current_state; + enum port_state next_state = port_transition_table[port->current_state][ev]; + port->current_state = next_state; + port->process_state = state_handlers[next_state]; + POE_INFO("Port %s went to state %s from %s", port->ifname, + port_state_string[next_state], port_state_string[prev_state]); +} + +static const useconds_t port_state_machine_sleep_time = 1000000U; + +/** + * handle_port_state_machine - Process each port state machine and incoming + * neighbor updates from lldpctl + * + * Process each state machine one by one, by calling each port's assigned state + * handler (each possible state corresponds to one unique state handler). + * Other threads may interact with this one in order to push updates (either + * link changes or LLDP neighbor updates). This may, in turn, trigger state + * changes. Acts similarly to a work queue for all ports, which calls each state + * handler and maps the returned event to a certain state, if the transition is + * valid. Illegal transitions will render the port in PORT_INVALID_STATE + * indefinitely. + */ +void *handle_port_state_machine() +{ + q_init(&lldp_request_queue, true); + pthread_mutex_init(&port_mutex, NULL); + init_transition_table(); + + if (0 != create_poed_fifo()) { + POE_CRIT("Unavailable poed FIFO. Exiting.."); + return NULL; + } + + if (0 != init_ports()) { + POE_CRIT( + "Failed to initialize the port state machine array. Exiting.."); + return NULL; + } + + struct port_range pr; + if (0 != fill_port_range(&pr)) { + POE_CRIT("Failed to initialize the port range structure"); + return NULL; + } + /** + * Trigger an IF_UP event for all operationally up ports. + */ + netlink_scan_all_ports(&pr); + + while (!thread_exit) { + if (has_lldp_update) { + int count_processed = 0; + struct linked_list *node = NULL; + /** + * Process all enqueued updates, passing the neighbor data if the + * port is in PORT_WAIT_LLDP_REQ. + */ + while (NULL != (node = q_dequeue(&lldp_request_queue))) { + struct port_neighbor_update *update = node->value; + struct port_state_machine *port = NULL; + if (0 != get_port_context_by_id(update->id, &port)) { + POE_ERR("Failed to find port by ID: %d", update->id); + POE_WARN("Ignoring neighbor update for port %d", + update->id); + } else { + if (PORT_WAIT_LLDP_REQ != port->current_state && + PORT_DEFAULT_PWR_LIMIT != port->current_state && + PORT_L2_NEG_COMPLETE != port->current_state) { + POE_WARN("Ignoring neighbor " + "update. %s is not waiting for LLDP updates", + port->ifname); + } else { + process_port(port, update); + count_processed++; + } + } + free(update); + free(node); + } + has_lldp_update = false; + POE_DEBUG("Processed %d LLDP neighbor updates", count_processed); + } + + /** + * Process each separate port state machine, after previously treating + * updates that were pending. + */ + struct port_state_machine *port_it = NULL; + FOR_EACH(port_it, ports.p, ports.size) { process_port(port_it, NULL); } + + usleep(port_state_machine_sleep_time); + } + + POE_NOTICE("Exiting handle_port_state_machine gracefully"); + + free(ports.p); + q_destroy(&lldp_request_queue); + + return NULL; +} diff --git a/lldp-poe/src/queue.c b/lldp-poe/src/queue.c new file mode 100644 index 0000000..7eec991 --- /dev/null +++ b/lldp-poe/src/queue.c @@ -0,0 +1,155 @@ +/** + * Copyright Amazon Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +#include +#include +#include +#include +#include +#include + +#include "include/common.h" +#include "include/lldp_poed_err.h" +#include "include/logger.h" +#include "include/queue.h" + +/** + * free_linked_list - Free all linked list nodes + * @head: the head node of the list + */ +void free_linked_list(struct linked_list *head) +{ + while (head) { + struct linked_list *temp = head; + head = head->next; + free(temp); + } +} + +/** + * insert_after - Create a new node and insert it after the input node + * @current: existing node after which to insert + * + * Returns the newly added node. + */ +struct linked_list *insert_after(struct linked_list *current) +{ + if (!current) + return NULL; + + struct linked_list *new_node = + (struct linked_list *) malloc(sizeof(struct linked_list)); + new_node->next = NULL; + current->next = new_node; + + return new_node; +} + +/** + * q_init - Initialize the queue data structure + * @q: input queue reference + * @use_lock: reentrant flag + */ +void q_init(struct queue *q, bool use_lock) +{ + if (!q) + return; + + q->head = NULL; + q->tail = NULL; + q->use_lock = use_lock; + if (q->use_lock) { + pthread_mutex_init(&q->q_mutex, NULL); + } +} + +/** + * q_enqueue - Enqueue a new node into the given queue + * @q: caller-initialized queue + * @node: new node to insert + * + * This function will copy the input node to the queue, not reference it. + */ +void q_enqueue(struct queue *q, struct linked_list *node) +{ + if (!q || !node) + return; + + struct linked_list *new_node = + (struct linked_list *) malloc(sizeof(struct linked_list)); + memcpy(new_node, node, sizeof(struct linked_list)); + new_node->next = NULL; + Q_LOCK(q); + if (q->head == NULL) { + Q_UNLOCK(q); + q->head = q->tail = new_node; + return; + } + /** + * Otherwise, update the tail only. + */ + q->tail->next = new_node; + q->tail = new_node; + Q_UNLOCK(q); +} + +/** + * q_dequeue - Dequeue a list from the queue + * @q: caller-initialized queue + * + * @warning: caller has the responsibility to free the returned list node. + */ +struct linked_list *q_dequeue(struct queue *q) +{ + if (!q) + return NULL; + + struct linked_list *front; + Q_LOCK(q); + if (q->head == NULL) { + Q_UNLOCK(q); + return NULL; + } + front = q->head; + q->head = q->head->next; + if (!q->head) { + POE_DEBUG("Port queue is now empty"); + } + Q_UNLOCK(q); + + return front; +} + +/** + * @q_destroy - Free all queue nodes. + * @q: caller-initialized queue + * + * @warning: caller has to free up the node member's dynamic memory (if any). + */ +void q_destroy(struct queue *q) +{ + if (!q) + return; + + struct linked_list *node_it; + Q_LOCK(q); + while (q->head) { + node_it = q_dequeue(q); + free(node_it->value); + free(node_it); + } + Q_UNLOCK(q); +}