From e5bb6499e47bf401b31b506c8c0c01a2a4bbc175 Mon Sep 17 00:00:00 2001 From: Vlad GEORGESCU Date: Mon, 12 Aug 2024 08:59:52 +0000 Subject: [PATCH 1/3] Change platform files location Signed-off-by: Vlad GEORGESCU --- .../accton/{as4224-52p-r0/poe_platform.py => as4224_52p_r0.py} | 0 .../accton/{as4564-26p-r0/poe_platform.py => as4564_26p_r0.py} | 0 .../delta/{tn48m-poe-r0/poe_platform.py => tn48m_poe_r0.py} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename dentos-poe-agent/opt/poeagent/platforms/accton/{as4224-52p-r0/poe_platform.py => as4224_52p_r0.py} (100%) rename dentos-poe-agent/opt/poeagent/platforms/accton/{as4564-26p-r0/poe_platform.py => as4564_26p_r0.py} (100%) rename dentos-poe-agent/opt/poeagent/platforms/delta/{tn48m-poe-r0/poe_platform.py => tn48m_poe_r0.py} (100%) 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.py similarity index 100% rename from dentos-poe-agent/opt/poeagent/platforms/accton/as4224-52p-r0/poe_platform.py rename to dentos-poe-agent/opt/poeagent/platforms/accton/as4224_52p_r0.py 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.py similarity index 100% rename from dentos-poe-agent/opt/poeagent/platforms/accton/as4564-26p-r0/poe_platform.py rename to dentos-poe-agent/opt/poeagent/platforms/accton/as4564_26p_r0.py 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.py similarity index 100% rename from dentos-poe-agent/opt/poeagent/platforms/delta/tn48m-poe-r0/poe_platform.py rename to dentos-poe-agent/opt/poeagent/platforms/delta/tn48m_poe_r0.py From ebd20c2d55a8456f2e69e6cb444d0987a1f596ad Mon Sep 17 00:00:00 2001 From: Vlad GEORGESCU Date: Mon, 12 Aug 2024 09:15:20 +0000 Subject: [PATCH 2/3] * Refactoring existing code * Sanitise usage of resources * Add watchdog to detect chipset hung, restart poed and reset chipset in order to recover from this state * Add gRPC communication between poecli and poed * Read messages safe from PoE chipset * Add overrider for hardware status for use during testing * wait for the poed threads to complete before system exit for poed process to be killed gracefully during update * Compact System mask registers print * reduce un-necessary logging in poed and lldp-poe * Use /etc/onl/platform as source of truth if possible Signed-off-by: Vlad GEORGESCU --- dentos-poe-agent/opt/poeagent/bin/poecli | 6 +- dentos-poe-agent/opt/poeagent/bin/poecli.py | 1112 +++++---- dentos-poe-agent/opt/poeagent/bin/poed | 6 +- dentos-poe-agent/opt/poeagent/bin/poed.py | 2043 +++++++++++++---- .../opt/poeagent/drivers/bus_driver.py | 42 + .../opt/poeagent/drivers/i2c_driver.py | 87 + .../poeagent/drivers/pd69200/poe_driver.py | 1402 +++++++++++ .../drivers/pd69200/poe_driver_def.py | 406 ++++ .../drivers/pd69200/poe_driver_msg_parser.py | 426 ++++ .../drivers/pd69200/poe_driver_pd69200_def.py | 306 +++ .../poeagent/drivers/poe_driver_pd69200.py | 1 - .../opt/poeagent/inc/agent_constants.py | 90 + .../opt/poeagent/inc/poe_common.py | 457 ++-- dentos-poe-agent/opt/poeagent/inc/poe_log.py | 76 + .../opt/poeagent/inc/poe_platform.py | 144 ++ .../opt/poeagent/inc/poe_telemetry.py | 27 + .../opt/poeagent/inc/poe_version.py | 7 +- .../opt/poeagent/inc/poed_ipc_pb2.py | 31 + .../opt/poeagent/inc/poed_ipc_pb2_grpc.py | 76 + .../opt/poeagent/inc/singleton_thread_safe.py | 37 + .../platforms/accton/as4224_52p_r0.py | 377 +-- .../platforms/accton/as4564_26p_r0.py | 408 ++-- .../poeagent/platforms/delta/tn48m_poe_r0.py | 384 ++-- 23 files changed, 6373 insertions(+), 1578 deletions(-) create mode 100644 dentos-poe-agent/opt/poeagent/drivers/bus_driver.py create mode 100644 dentos-poe-agent/opt/poeagent/drivers/i2c_driver.py create mode 100755 dentos-poe-agent/opt/poeagent/drivers/pd69200/poe_driver.py create mode 100755 dentos-poe-agent/opt/poeagent/drivers/pd69200/poe_driver_def.py create mode 100644 dentos-poe-agent/opt/poeagent/drivers/pd69200/poe_driver_msg_parser.py create mode 100755 dentos-poe-agent/opt/poeagent/drivers/pd69200/poe_driver_pd69200_def.py create mode 100644 dentos-poe-agent/opt/poeagent/inc/agent_constants.py create mode 100644 dentos-poe-agent/opt/poeagent/inc/poe_log.py create mode 100644 dentos-poe-agent/opt/poeagent/inc/poe_platform.py create mode 100644 dentos-poe-agent/opt/poeagent/inc/poe_telemetry.py create mode 100644 dentos-poe-agent/opt/poeagent/inc/poed_ipc_pb2.py create mode 100644 dentos-poe-agent/opt/poeagent/inc/poed_ipc_pb2_grpc.py create mode 100644 dentos-poe-agent/opt/poeagent/inc/singleton_thread_safe.py 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.py b/dentos-poe-agent/opt/poeagent/platforms/accton/as4224_52p_r0.py index 8e8d1d8..ddeac13 100644 --- a/dentos-poe-agent/opt/poeagent/platforms/accton/as4224_52p_r0.py +++ b/dentos-poe-agent/opt/poeagent/platforms/accton/as4224_52p_r0.py @@ -1,162 +1,237 @@ 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 +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 -def get_poe_platform(): - return PoePlatform_accton_as4224_52p() -class PoePlatform_accton_as4224_52p(PoeDrv.PoeDriver_microsemi_pd69200): +class As4224_52p(PoePlatform): + # Accton AS4224-52P 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._max_shutdown_vol = 0x0249 # 56.9 V + self._min_shutdown_vol = 0x0190 # 50.1 V self._guard_band = 0x01 - self._port_power_limit = 0x7530 # 30000 mW + """ + +-----------------------------------------------+ + | 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)] - - 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", + 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._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 + POWER_LIMIT: self._default_port_power_limit, } - 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" + # 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: - powerSrc = "PSU1, PSU2" - return powerSrc + 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.py b/dentos-poe-agent/opt/poeagent/platforms/accton/as4564_26p_r0.py index c175931..d6bf53b 100644 --- a/dentos-poe-agent/opt/poeagent/platforms/accton/as4564_26p_r0.py +++ b/dentos-poe-agent/opt/poeagent/platforms/accton/as4564_26p_r0.py @@ -1,171 +1,253 @@ - +import time from collections import OrderedDict -from poe_driver_pd69200_def import * + +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_common import print_stderr +from poe_log import PoeLog +from poe_platform import PoePlatform 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 +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)] - - 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" + 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: - powerSrc = "PSU1, PSU2" - return powerSrc + 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.py b/dentos-poe-agent/opt/poeagent/platforms/delta/tn48m_poe_r0.py index 761c6ea..380b7c8 100755 --- a/dentos-poe-agent/opt/poeagent/platforms/delta/tn48m_poe_r0.py +++ b/dentos-poe-agent/opt/poeagent/platforms/delta/tn48m_poe_r0.py @@ -13,48 +13,32 @@ 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 +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 -def get_poe_platform(): - return PoePlatform_delta_tn48m_poe() -class PoePlatform_delta_tn48m_poe(PoeDrv.PoeDriver_microsemi_pd69200): +class Tn48mPoe(PoePlatform): + # Delta TN48M-P 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)] - - ''' + 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 | |-----------------------------------------------| @@ -64,136 +48,210 @@ def __init__(self): |-----------------------------------------------| | 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", + 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._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 + 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 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 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: - powerSrc = "PSU2" + psu = "PSU2" elif bank == 14: - powerSrc = "PSU1" + psu = "PSU1" elif bank == 15: - powerSrc = "PSU1, PSU2" - return powerSrc + psu = "PSU1, PSU2" + return psu + + def _reset_cpld(self) -> None: + pass + + +def get_poe_platform(): + return Tn48mPoe() From 5cc382eb301dd952de59c5757badeb87cb9b0f09 Mon Sep 17 00:00:00 2001 From: Vlad GEORGESCU Date: Mon, 12 Aug 2024 09:16:43 +0000 Subject: [PATCH 3/3] lldp-poe initial commit Signed-off-by: Vlad GEORGESCU --- lldp-poe/.clang-format | 12 + lldp-poe/LICENCE | 191 + lldp-poe/Makefile | 40 + lldp-poe/README | 11 + lldp-poe/bin/lldp-poed-config | 8 + lldp-poe/include/common.h | 43 + lldp-poe/include/lldp_event_handler.h | 32 + lldp-poe/include/lldp_poed_err.h | 77 + lldp-poe/include/logger.h | 83 + lldp-poe/include/netlink_event_handler.h | 46 + lldp-poe/include/payload.h | 106 + lldp-poe/include/port_state_machine.h | 109 + lldp-poe/include/queue.h | 77 + lldp-poe/lib/cJSON/cJSON.c | 3119 +++++++++++++++++ lldp-poe/lib/cJSON/cJSON.h | 300 ++ .../systemd/system/lldp-poed-config.service | 11 + lldp-poe/lib/systemd/system/lldp-poed.service | 14 + lldp-poe/res/lldp_port_state_machine.png | Bin 0 -> 134234 bytes lldp-poe/src/common.c | 39 + lldp-poe/src/lldp_event_handler.c | 861 +++++ lldp-poe/src/main.c | 187 + lldp-poe/src/netlink_event_handler.c | 388 ++ lldp-poe/src/payload.c | 426 +++ lldp-poe/src/port_state_machine.c | 2516 +++++++++++++ lldp-poe/src/queue.c | 155 + 25 files changed, 8851 insertions(+) create mode 100644 lldp-poe/.clang-format create mode 100644 lldp-poe/LICENCE create mode 100644 lldp-poe/Makefile create mode 100644 lldp-poe/README create mode 100755 lldp-poe/bin/lldp-poed-config create mode 100644 lldp-poe/include/common.h create mode 100644 lldp-poe/include/lldp_event_handler.h create mode 100644 lldp-poe/include/lldp_poed_err.h create mode 100644 lldp-poe/include/logger.h create mode 100644 lldp-poe/include/netlink_event_handler.h create mode 100644 lldp-poe/include/payload.h create mode 100644 lldp-poe/include/port_state_machine.h create mode 100644 lldp-poe/include/queue.h create mode 100644 lldp-poe/lib/cJSON/cJSON.c create mode 100644 lldp-poe/lib/cJSON/cJSON.h create mode 100644 lldp-poe/lib/systemd/system/lldp-poed-config.service create mode 100644 lldp-poe/lib/systemd/system/lldp-poed.service create mode 100644 lldp-poe/res/lldp_port_state_machine.png create mode 100644 lldp-poe/src/common.c create mode 100644 lldp-poe/src/lldp_event_handler.c create mode 100644 lldp-poe/src/main.c create mode 100644 lldp-poe/src/netlink_event_handler.c create mode 100644 lldp-poe/src/payload.c create mode 100644 lldp-poe/src/port_state_machine.c create mode 100644 lldp-poe/src/queue.c 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 0000000000000000000000000000000000000000..0579b2ffb1a27dd0d710644c6275efe428245bfd GIT binary patch literal 134234 zcmeFZXIN8N_cjiUCw2%>2Sds^`|Q2;+H2iwt$XXBt&UM{mEuRPc>^s^C2MqbntNpmp*Zmb}o>nQbb=P-UVh z4nK^ov)bDA>$}xR1TS1FTW#&O8i~Y&u~{-nxEQ%OZ;)`YBQr>E7H2CZ#ma)UYU+^_} z^&^58D)?uG2)056+nIs4E)fx7Yzmv{!g_Hcp71diC%RT+i$V|du$FkU`j)6o<%$y2KV?!NwB z(G+xqjo8bTi^qqP!EaQ5Il-Fjt5VU}UfvR46gkvR7NMkZ?ZqU4pNlZe*Pd#_a~0u~ za;_}G9cvRUR@zg&B`R#RM8sraXs8J710tj3G?trtB%esJ7Hg+Uvt~vMl|q~?31cnt zwuw^6u%5C=YY7cVAI+3UgsON*mcO^V7tTJMLzL0Iut*z^2(G&|E80)Ur+MIjDx&SV zY;QkoG|r!b!H0RffNwZT2+hLC+ypKJUsNOme1l_AAsZi#jV}`y>KY;D`4YKeB$7c4 z55F0LdtN$lbUqz)0@uHF$2Sbw_+Jekksq7`gB%FV{hg`lFiN79*6fh01_ z-qS_kk41*^F)D(qN=1-zFcdn+6^}=IxQ4QLq|h*bqQcuB!txj*nU|}=Mi%8F^HR~n zWkRMOH9|mim4*7ai^(<$98yLCQb*I7p^;1vE|#P~QkY_rucwsjK(ca^AQV)p1K4kO zirhmSO=59u?ZF2uIK5~Ecw-l7?-QwD(4%a4*k}(PD>~ZE%U>Xu$}ln>)ssM{NIg6- zp+vreJuiYHjbMAD{i$xQtZ=?H%NikdutWPtA(S3;B^u#jhv5rdJp4Uu>9(G}p}t@b z++D=hKH&-%7k4*=7{|sSg=7VTNC7|CBJe&C&B~jL5dr;hcL2|H!HH~vK;4*J@CC|` z%#ez_BFzg(8(i648?_wZdQ(QY)IjgBQ;OPmID5qr#LbfsY)6ab-v0T*)p%JAoTM z5+zmeXkuA3F#^p1M~mdZwpHLHQ6y5dJUT+o3FnBsaGpdTYbq7PC(xo~6s$kwA@xA` zxQZ}*8zzUsBq$>#LIjUTMT#7vB0OCok*$@VcAa)Uem1r)Of-_kcIBYC9GM#x&sU&1 zR2L#DY#l-)F+4A&fWrzy`@8u^ zDR~s0tqAFfRUsXikV9A~xWmU=#*cKj#zcqVWY%srcqzd<8gDHL4OKAM_IPiEJ0U_Q z4o7)&TJkwJ?6JsSMw0s5^IV8g3MC2hvQb9)qgix; z2;=91A^F%_(+~_VF&a2CcbN;xKAKMw;e1$8K%daGNScF7l)t|xhmZSW-MnnWXzp%; zC~pUE5^yPQ3KoikwhFh!ifj=|7lyl-Pozha&_ZjvjRT$L0EQ$W`4~5kXje4fhs7du zygjg70m@qrP77MZLlL3~;0>CK<`NvRE|EU2QPMDsGSZtDM)xH^;C8w~DzK+p(LE6e zgq;-cW=D?jRghI!VkkrAr~i5Gf~0Rcsa5$Vg?Fy@cY8;mM?OBq>V45JliQ3YId|fy87< zd?TZXVxkZ)ri)cxNLEB-m>b8RuXPZSu0Skkq?OV$Ql(_rN>H|9A3jGVmf{d}mJ|w; zQEl8wKGr-5jwKPIqW#c(o;{IE#`@d4E3hcKG>XQ>E24aO@^HRCnWW{Jh0fPhfuC={{_!#KtB% zGLlIrNNrIhDJdL>BuPST!~9VILZDSV51D(o2&jnMUm%PSxXJu+6i-j48;hXywkL!t zH+>#!p#Hn5;JXm@IG>( zotsd}k$5O^mK>Z?7azY-Yhi9lNMpbO|a!=;S3KHdDn>(pYGw0~rU|`A2%XD>2&f1);uVZwWOjER;%%_LFhEC}fm~?!dC8 zi^vcGxNmp1ESxBEwL*BgS&2l9NPCGdRzzhwQ0=|l@os+J5g5F$9L4qy_aeJN3LiU? zl!cBUk*&gft#MXBgD_T6HZD;J35sGZa-*{&!H5p7RGE|@isUjWd=EUu8t>`A;$t1C zEE_gWYwp;7cmPh(p`Lauv9FlQq~Ywu6tXpoWEU1G;SnLaHIB-_%iWYd3TCvYR%Ngh zhP55X+D^{VZi$_hJ=(^B1kkTL6A{YOdSW5h4&WKI#D?f6jkfdm4VAlu${9F1OJz%m z6j<}IVMsbpz;f}XXNG~66 zECsT0b(Odws04Qp|1g#}Muk_ZJY8&UF+?|OKPo^KDiYp@O7gc-5Q+X_3UP#|mxqAL zj&N`%V)>D1To{=qc8yZ-Rd^MS?QUaF0@&1p6HP)|6IEzW8w5?lwiic)eE~@dvH%$- z_G8O=DoUiaJ<|p2;HRK*A{cUnR*m@-71;`*-Lgm-BEpA=_ruaFd4%y7Z*LKQ4e z8#zKNKo%ErrAE@MTtpE9z7N_P;Tz82iB)tvE+d?Qjdb;gloSFd+)HUIvSNoKq;!d` zGCWEs!+-$59p|cwVn(32RHPru3x$+(unt6`pNAXO4~O&^e%Mv@|cp_GWY z)M%{A+b0aj#$S8X&^Rm(SP3*4CGbZ3`2noMRrt~fBDp7qtZ)yNInYp2iU@&GQ6(rl zhL1GD&RW4FGx$>QXg@2O2Q9)IsZyX_c-k`_5sj0Ri4L?-jJFu6a!2y*FceRupU_L| zsc1wb0>KyXWn@2&yN6uHpt6uw{vrljleF6#?$0?6pPRUnxU2dM zau1r|`D-ZecZU`5TSmd8KNfsS8XN!AIJva7e5luV;^bR;(w=EUT`@0R3?CEo zz_Nnk@W)P8l{4EY-L#{@aphGXdQXhrTzmSb57r~`%8VQi|gF~W%kYlm{Gx+49nP6|3v_%-PXGLISY}Y z1>k9IzNwq|Z+q(tGjS){hq@|X=3o9K(H%(mVcL38d(mh|_+?tDxT|xl`tzq4Ki$AW zPW@m>hq$mMH(>3=9pcK6*!MkqM2)GN$wj<-p~u6S`Z*uoJS)WPn_nK8A5_(T3kYHT zlceDlKd!+Re0bZ}5i?w6@;GDL@-IS7nD?V;2+k^c+1FYB$6h(VFii-aE%1=+h~|vBic|zmxWi7xy!yj~`!B6&m&&u?XNK!=b$kU%h^EBbRvI*H+lF zUK3PRAs^~&i<=yw2=ARqWZ(MzW+wJHlM-J;2#+dmq95RP)2^vI_^6Z+X;ZEqUrmd8w(MH+TEQ%hJ)ejaw?6 zsz1K*i5a|bBar>pm=f4ieZKgJ$IJF&(Zd2p!&yPInyevPv~q!{JKw(xdNG-@Np%r? zxK;1a6oq!!^5&0KhTRlb zjgU>$W~RjM^J%`1DFs+ImM9T%_KT@`Ak25*u^x^4m zcDzb`MA(w^V%)y~?wWXxo^LB^-?b!3@594} zn6V+busos%eZo<$j@g4guy(Ig9`=i+t2qxg$!Y6?-HHEk9KRVBgLNTL2rU!4u(b;A zRg5W6ula#nFqKl!pYrC}?T!b2TVCqbIulLld4Ucl9JoQ%+lGx6(%w6kVZ`{>b6!7I zK0WNyS#oup7Ap`k_0P9%Ri4tEBt4Jny|ZvERzDr~+Gd%^Cns|+KCwuh2SGXNpPEU< zNeW1A0uAnuitH<{*El>{wV_arLXW{YL>XwZ| zWmluehF&(*&RhTwNzPDnjrW_+NG^nP#{H|n`>?hZ#4+SLOy4ZS&1Z`y1%}`dg*yp`ohnV#& z=jGlX0;bGf>iF*D>vLYetub?&|L&KsheUt6Q9+QAiOGL8-A0e}E|eO~O2uWxL&>LM zJ@fAK4STvX5m+c0);VvRnQZ2i9N6=Dh^ad;kPSS3@=4N`&eqGH#)T$EQ}^tKc^B7* z0afc{kv6NBtNTfRm~8XqJ-d}Gdewa;FUi?-;;!rLa#Iu#?v{JrX+e#! zDLHEQ1Hw)13Wwhd;c-9cYD)4CZeFH8VDl9#5a{vos0>Pc-+z_pq5XP6dgR3Ijys_% z=SrfWXBj)ZL<<-5)GKkPovJpjBBp1<*A!>uHqf+wdF%7@&V(!J@ZcK<7WeHS=xf?n zQu}h@*S3A09P2_ko`-Z#!JYc<9Df2|wa;XOc+2Fw-^_}d8r6H7tVt`>`TT)^#2lDP z?yG^bj4h{5%W2nBHD&pW2Wa)K9q-jhf(tjxsX08W+pq8T?r8EhT@CtvSlh*NBYJwa z+7p%fP*+n+5Z%)(uOM?ODBN6iwj*>!y+a5Uz(bZ=bN2Iv4E@xUq!f6R{82yHdvS2= zNOH$T8gw4UV@=C}A?0=I9fU-U+WF&f^jTv~Yo>r_*4`_t zJ3F=}FCJoChbX~oc-|>CN|EnoKcPRd7JPJ_+yd-^c ze57msB^YLBM{oU!tq;$uNg|BK)Gwe7?j~xxm|?iNI5xNBJ+#F!n3#}?wjRlFCii8+ z@y+vVU}LhWnv&FJMQO#SjH5vpQ#Z#{6cSP5#KQ541_KC_J>I%NrpQKP^($cfVw@-~ zTT%r4lUNg=AKeb4_0_3Q<743qs{7TNKzvSSOmD-ffri5~N7mQ7a!;vmK|Q(fEyhRS zhroG-Z!-2Z$3MEr-q)c=+oIWPTb3OSF-lU;$IUw?2nd0G6?|0dkXK)T$DvYBh%0U; z)~k({S}}LYpWnSAKaYnlw|uyea<%~pvBLo**=nyYq&J7=ng%E&Ktb_PBQfzpi*uc$ zA-Sil1lv;BJdZBK?fikf^}Khhv%coh4^V9#Dz)rn^JR5das36oQT!ACZcX`f+F$yM z2Ku|YNwwyGgR)w9JI6FFME<1ifaSI(Rc1owT^nHX#U0&O_sp*Jriy|!QrG5r1?su} zJvloZq1$S+cf_Bwy&IddiKmO}bF1^yASkP1?cI4Z;CD!Ovp@YQfT4i3exY}>d%n7{ zZ2Z-DPWJ>jYqI8(Hcp9^>TA&Z1(n%MD7kFW*&U2T^~L+GqV2vD`E>2tOfTsd?W@b%Iwhy4YVZDK--~Ufue0vJ zaqBsD7Hf8Wb=yA~o^*)%6feOm>O)Xza4188pMQpYB7 z#fwb0>3NNIv-Db~#HE!l1v~lk8ep?|bW!i#?{OUi1i6cBeXcvt(W=1oDS9nC^bOm! zo}=_pv_}52y}2%FdH4@gXB2GzJlUFJ7cOqxJa#g}_f&iJS^Q}%BfSnk%!5)kOz z0U&sTNpw)`NV8>A^s6h=askRbvSs_H!c1*+Q`t|1PJuA3C3>Wv4Wik7T2+J&tJ*4|=J0`G;w<9)idv9AFjYThc~(|L@O0nD{7Wc=PQOcKPSyN3_mE`ABu6D_;d7 zuNxz|ec9fM&a%ig9XEel+cDA_9KL+pD;F#~qZY4UdBL38*LXS{xGkwmel4@jR?zeZ zz~h``bBbc%rva0os4E}}WdYw_0@hJp{mhzo*HjzB2WYBd#^}J{1*Wb4{Oc-@cCf=Q z>av}jK7W*sjZFV>4OM$zC+Uh~iQz%bq>UOF*#OocJ&y9;67WOXLG&3gd+3R>!fNh1Q(1*ZM6Z02t5sU^!)j zB2ae#s9xCT4~^RgCx$PVZc@G3HG^uLClsr(K^H@RDclU=$C&f)Ec?6e{JLt-+oQ?w zO!OOkabM++%aAh5>>g9=vu(if|CBoJQ{Tjc` zBP6V|$;K!{;E4u_X%l0Qw^w%C11pm$;v0YB=fU2SrfXh_-aWsUw^9Lo;p?e6kGH)& zVCZ!D{hx*=zHsNQC1J-6pS^R{l>qdMg-&Y24nq`3X7rcnljYfJw%jkG20k2EE-BBy zNeUPcJZc}O^yHV=kr!m^KnZ86^L2=g4;~sz;P%i%di6GyHaHJ#&WC$G%hn;TP=dfN z5RroiPPP-o-HWeg8xhh=TU=tzH<8)Tw<^2XFCLs%lIezREjhFYTQwHHpv3iFu`kB8 zAs?5QQ^D=2O%%r^tWj23C&-hzS;`foj;F)-h3LPICTHay9YNK{j=y^$UIOb!DXKn$ z!zLR}Ysrk89Jl1PzJC|DF5bfbjN8cplYVKs>G7`#ta#w~o?-r-E8Ditxu*XrhT#cZ zj&qd3x&?&mJ@?Lfh(3Rsv@9d(sQ0?-`w5oEy-(j7_u-l4bKQmzi#8gN?6W_BAI@4{B4ubwHjqPd1p%8q z8Uu?3$pN!RR!DRAwX7__6!flmyK~xXdP!JYXQP_eTnw8FjF{4Hmi|c?qqsUDGk&B? z`B)w>Q}d$S5k!T@tcIMR?dly+EB_=6Zrlw=P>2z^d;OqXL0t6E_{e~(IA95taw1ZI*sLF=E@~RRtj*#8qe{uF zF~3W)%I^1$wJ6BQt3I#epL`g^Zc^RU6f_ZXoEo?MxhvywYw-4$dd<#<)UU{mvv(E3 zRzow#!J#UHd3K%6-PUYr8#43JO8cmr#syQu=PdcSc>_4Nz0D7yE1Lc~Eyl*%8x_Fa z$VB}-xVQ01wS=&uupPMH7ja8BhCuqcdbGq07#G5*a}$gYtM3ySrFE1n{EE_yms)_o z93*UV52z0(E#ESv_BAG(UuqiX1q6?1GA;J_9JO33RpeG@c9WXR)FsdZI*4qpCmw`} zp>j2xbKrt=g5HpWdRNPEvuv3t_5|6{iM$H`zC3{sd6J%-;aEuONlne&`$u;?tY(_EWLdoFB z+yXeFg?!U7hSF0|yA{$MI2l;JZbqv^Axh9xFl(t}nY9vXR+pQ}DA#j$un)LOAp##ytGsLa{3xhbc4_>Cj*UBuNHdXA?eX&@1)^gZA3=nio69p2=|`^Pmf zYB4)!2e!Cd>LL#w%s|j%_lQRZP_&t zbW;6rMxdt9*Eqg^Pc7qs#}V}+=;uY1J5Uj$IXk?dndbXj%Fn@O0pq``dHyABt~Yxx zSxR1>u$KxQZ8&ayL$p)#a;LHQ7FqW^eD~1}!9y*P040i}>B_{4D*;o?cP-fzRnVb+ ze>dG#oZ3(9YlU|+Bh%31KSW>LUi1hX@?Sgu@2tdaU0kyAq zD*aW3`WC9G(&*AGI`_cpvYVt8`6&%^T3YI&^O<%I`i+|v`WiT`*j-)ILcWz!*)X$Z z5pSTstYt$}A39TG$G_K8dw7n~vHP~=!V^>Kj>>>ymIZ$GDTH|b-ZdPxu zNYY%C9F`YY-muZ8o@jjZ$_m!t;(2NCn%{3E+Ui%*Hzt+}ZX8@Hb?E`XEINH_d5qjP zUrpF~Vn-At6-f|7&`!q-x)|hwc@NypesA z4WLeLD?1eU*1^_9q85K4`6;Z*)# zcQ`j0fXCMpto{G>__g5i5p;{Xf5B}7QzIN0`(9D|oqv^;YTbcL2s^SjVddYu^QCm< zc3ekS_`8Mu1I%~fYr-od(~y}-a^~-UP2n4&zXvvXQLM0G{}(s&ZH=Zgrht$&{C&*VWojQr@Y zM|>Tds?dj(sxQiy~W z{P=Gv=;)CO-pu-p^f4aDfiO$pbSS)6*+!x>GY~q@y>^* zKi|+BH-2-yu;r(iDNFw;EBJUe>gWxH{BNY9gn2#{#| zdA>FoOXK%0Y}Q-0<+;D;+S9)=+vVvXb@7-FER-QcHUo$X1-n`+-B>FE8ky8Csb zIh^{V;mJ57P>4%f{_gh$iyGbjcE&%u#>Xd9e)t<)n6^)OA9IHn5ZSxW`8_P^i0?6xZKJaXZc5dan4+w_)wzO*vr847KB zQh!t2-K(m-`?Z^eWWAQ30|?&=4qAV2lmupd(|m`HUe8MHoB!|Jm}+TkSh)U;csl_Tr@rOHm4-U4nXDHjc+lg zOY}GOT9OKOKlu~MnWCrN>G*%Z(R)t-tvvQ!!T%$P!x#_-(2l7L^bCI55r3mIF&;H1 z=Y<}iNxgdS;&yW>}U4j&#NR|0N_nyIld)vt;KddyW)>|eu0m08!ru#u1?)ONs z2Oz&i2Q_ua{?@iBb60}8o6+VWj84K+03mU{qSQoZ*z~B6Y3J`gk6QoF;?@B${K)X1 z3GhII@j!_&x7uJ=zILv#Zx8$)@H*Rrz1jUlZN>TLzFi<(TH}@!GsoG~0j%LS;tB4G zLujpV%xoN&?fV-k8hHaf9@G0v!JL5}8&J=Rt{>l^*JIvorA*)PRgM1zFO3drL$@-) z{)PYCw*q`M{oa!xu${hOJD)c7(hNY28f+)*z`+~;Sb$w~LB_P|AE^AZEx7vcb8}~F zR>;i9t2s~qjgof()GM^lTg3WTU<6M1RPD`FcJG3bFDHC>%HIp?2B9H&OYT&b%U@^q z47wZSzK>Y3+%2^)RLN@~;OR|`NY`m*UokVAf0BI?NT zJ>_U3dwe};(>G?Jg>tAfQgI>VieBa5#oIZdHxHY9Ja%yDM*63ZAG8IXmeTOxnAg`$ zL=Q4?ib+sCi3pDDG7`*+86AvdImM0_W!&FBwuU(|kbilwYV1)VIhWp+Ln|BvIj|x> zrw>2PIUT{+IyrXvb1|R?v}-w?6|YlchAse(R6>wL7m@CV}tjUnkUn`F8>%3 z`j>5Ro(I&=z2}TI`IO-QwtaZhb_TOAC+hW|kxrmW#Zh#KN@*awzah%AqcmKZoUG+)8bZc{U4Ut3(%JYy zY=GLMt&)Mdl!$%%2(F2ntz{k2vk4i4fd4fLs(C53onXv0#-Y8cezKyV>6+=~rzz&p~(-`^_$xV)W+~07~C&#pBqq>-(Q%o)BMLdwSdb_R{ch zP(95xECPvs6V0~*p5*BtJ@7ziGAr#x_W9I{AnV(vU+9svthg^VrU8l_9cXQIKe*&Y z?FRjm;}^o(kSFXT&n$J2T!YPD&ja;|3qe)U<<6Ak{o}>8kGeDC``fsui#J^UcpqA? zR!3g@d5-P3TG`?!%eQ~*Ni7Gd6pB~Yu;|gHHhK|{aU99MXhzI@c);$_^E$N^?;s4T z&HC9-qCB7*p8yp#FM6b6AWy1$F*Q3=u(jT+Cq?aI>?xd~j)pRXfNm?QPeHYmMY0Ck zi#0JVKg`|~vfBIi2;(I6yJWwRS>QxPPA;>LsL?M^X$7^iZ4I6IWqvcr##V`E=mVhGSeZtDB52J! zCH+E11l6ikb1JB|FhFKWf(^<06g@h~Puldx^Ukq()kBXjKh7=07P&_^Hlgc1pxW^n z&zBoyZ8=a3PE;nGew_D`l&Dre23fd`N!?06sg|MI6_YmR(_GyDvDd_hRqZAJeJGTnWop8 z_MeXWbGkV`E#5HNSZroQ%*u53(M_+*a~=eGkTgBG`KTUTKO9yzZ`+GKDZS|t)F=MQ zg25U@+LDJWsbl==_W9(VpcET^=jz3FVp)1Fn!EWtC|xODWE^cb&%gAi-i4Kzy=AX? zaWG@!OUag7+o7QTWl)RYL`TgMy$;=J;d64*HjZ{qj14b;yYl%*g6P>tpv~nfczjbd z{M~~E?`DjoU)Tz2nU*%B=U*tHNwv6Ke~|VJG}3%nXJkpqJ2^oc-h+PTT+>}W#Sf_;AbyHUQ?5E1=?!3VVZHE-)W~VX^mPMVIBHMH+^ zM^k(vp0Afy?R(QQ_Aai!Y64ghbi-PsZRS|$*oL!Kth$H5#1H8y5*q=%IZ~J{+Q(k} zaVT4lIy?*rnO(=;eej1gV5*MD<4MG z#|zXyk(}EA_4=3#AVfBarh{TgfbufhHSy$?j-q9bvOf|lVSmi~-Rv6a`C_oWjJ#Up z<^bbN*bFFL1M^5^r_*08%z;&r*yc&jhJgRL5csBn5(;^BLG244%kpau{alB2^P0#d zfbkfTt3$A8nt$bru4czWyma=m{1*Pr!>h%Xu&46~n!~ohkBQAgPH8l!$A-EF#D-yW za*CVU;e#>H)Aw|eZ^LGTIDoI^MuHmj`xjbQ z!~xRwDrJ6)OV6damPKZFMxv*Jq;?u<_&31T+aWlx^F#|S;X-Xk{Dt1E{$BiBe|wD* z&k?BA&Er!(MJCi`jW$z)%M0kk+h0}a1Bz))uKaGFo9<<}VkAgi;aKtku%4c5^B5|7 zGRvf2y@)nk=p?)l``&t{?qfp$6+njZ^5Gdq32zMmkYjzNUuywK_bP2E-(srkH|&uR zG5_Z(Oo84Cec$#v3bX^%)FT<>G_&;&$QNMUoW|6b>czzwIcOJX##Atik<)4?W1hMS z@Lt6O0s7})Sh+%-Qqm8-pSSH%mfB4oo@VyGc?5QI9{t4!7-OfaWg=71ei&->w_mWN zpnsr!2PGjN-RS(Fq#E`FjRLjDT9?G50(DT6Juqv}&=;IvsSjCc>B2lB>X|UVsu#{^ z{u8kMPdhD@3?;noUjm6dG`ArAM@YhT%}3N!QL_d{5#7wGp={X$3KU|$Et5rt+aZ57 zg4BUDCpB#};;Glqa@u2?*EdbC#Tk($CuZce;EYcvM4Ihy+V~t5(j2qKbtGLB%?dx7 z6DjBlP5Urq|CSF=e>6F?CnZVkS9~x*33b?O2#^?t{vlwk`6lR(<&+xr-Vg{Q^kF)@ zSU&rjalyewD0qX?E%{;l85)bg%S!_U$djLcKXCs-jCL>1zMBkbp96xRoy%4fvKRez zteiu&m4iA*BnkOi3fp3qb~x!#Z8DH*a$%{%>wU}D-trD^MMB!8;0nghj{^d z^ozR85PV}}3QE&)k#gmPUJ2;;Xa`1pk=!Y#Ix}uw>I)&P-Nr+ITXbFnvAHexO1)2w z+CpxeBQT_iHs|F2isF`oB5CU)8(w#VDa6jMJEyuni;CLQed}R2j4X;>A{a`Dgxfcf ztARl%m6BIS99V?v{7@>n*SKhBt?R-UGXnJU)!h(AkhTN5Ord5A^|*0@AiD5C0HYF4 zgEr^DTi>T@MXaVB@Ck4eT7;ex^x<_iEv^ZiG_y`h^Myw%ps1-Sb@LyV(1Q1whd^12 z1x^J84~OhLOp4I~2*MkauIJG#j7#=v{`u6u2P(NWW zYu$L}lL3|ZG0A=EA-rxz)jI2LptTCd!CH-+eZpVooj-p>QZ+#j|F$J~U1LlBbW*OLW zJati_ngjZK22uenQ#W6atltd!H6%%})SV6Ckl`#Ud;XUVvmqH}_X!UaZ7p0x!VCWSpkRvqKF5I8rXTLd10q4ND#7^6}xpn z(F{>&t$AC!*wG$PqPp9rX`79E{Z>}f#xu8|Q?E6qL9x$cn@<)yMQ<<8kiDwlTR{36 zQ!=-{UlkqvPL&1V*~B#6^g96K$nqa~i5zmAiWQ`SHMf>4Wrt6G4TPTt&I2(m(-4@# zwWF*Tv;PW_oK1DLp<&Gv@?RU>T|0$jwcw$eWUNUjVc(vw3_4%(Xr_&RoB)vOVW1}4 zHA(MFP0cssL62cwXq4!xCWO=@?r+NOTHPOf72FPX|RM~Ny<93*duFx%&efkWfK(u zo!2K`NhvIO3E|hRJ%!Q4Z!~(;@1%de9d3{K{JwWRYh+<7t+--KUf+%8(($H>btnCX z=EpdVd+03k0WDak4#X{Ukc46}g?f_Qj25drI+Kke?MN5W*5{?XLDNk` zpzCH9+w|WhO)EN3q$bFBi@uO(bJcrK04kEnS7=e?87y|KG111t)eFpbI@fH_yK(S% z{V8Ed=&x=45GPzaW95U|0B7ir;J6PN9cWsBr3RJ)CO+Nh zOK7>K=2*ZS&&V%QS-$oM=bG7i11-lVxn*s;ly`0%&*X5;~nzH&=Ov5+nM1zDJYb%^&SYR*SRrn8JHeA2b9n2mQNqZl%Ye~ zkcAh{)@L>#H1xW`gL63<`KZbv@50-X6KwCEypZA zJ<9P~RX9Jl-au^GkX-p!$oFgtNIFf)F)MZ3v@l`U7p4E`@-E(af#H%(yp|{YLM1mv zE(jfc+Qs=GNqoI)LG(p~L5GmhmbIMxdPGb?I_>ic&bakaEsgA_-5|TcTz7w(o9)f=Gp^JC?J*IBAn+aEiGyz8gDeEO~ecf;0w{Cp;7MH}wSw@5$ST)Bv{rm*JYhqqfto`4W!5D+^HwHcCk#kV(H?i{L~5QB!h*3S7w z>ViPfDF9{RuS}h}=qY@yvA}6!uy7P`+uK=5n>kB2DvAKfaV2M}`Ywt6=dEP(uGV)m zx*mXD(ip(qd#TNTX;VtyxUOAEercw`XRiK+gJh`iRr*w;*xXKf|s4;g@Eg9%?X1sHI#huNPlx-qM=Dexwg4; z>Wl^5y&acSQqYIk*I82B?-#q&Y1>1HCK#-^?7C;xPz-;Oz0%;?+O+M- zw2Q~u?j8vJ?Z!a7{I+PJF0({;)HDN_Rpnm0m+a2=8(}_4V_!N$pxXI1q28w#WH^kM60wR_e}DgCM|>)wW=~!vgkg|D&iU zGtG&a`xlR$Qa<80ZhDKo^YGKhw|=*i&5sUH5>Cc#&7oSESc1^$QB_RG&e=}S$M0_| zHqKu^Z{?>~S7#5=>%DX{_3n#0O3F{mwcPE&x-%Ex5vM>`@bZ!Nh`+c1hNgh=B?Nmb zy9|PYvBvh5fMgmZtw&jQ0_8rlxDd_jGU0)=P=UuX)wOwWb7za7OJ#=F9!C#YaL?r) z1{y!0w-q&#jtF23g5@?k4v>0!?H+GqU3^>>Q)>8&ydH!x{k4LTj6SoUn)DpO4eC?%fI>}@nKH&j-62ldH_FO$!eu*W>+nSIU9evO~75rZ!zEg>EnvW zdq7=~p1MgDxMiF+DZs)8U61!q8Yr-UySm-i)acY#K>2*z=40}6+R)nLmhAX6z1Og! zaQ>0rLv62GUI*RMH~7gj?Fp}t*j693{I@i58K_14(?fG;m)?d^#6-`=!m*guJ+99e zCAiJwy*f;LEdefTHS@eI(fQBmS!W2zNAoYe*{kuyf-c04IC4T-Z39sH@&cdc^XeI! zLAF?rn2+`PJjMpk8?A%p&I+bvz~ElwJquSvZDvL_uhH5y|uX z-g@WYiXa!sS@#1u+{RS#5&>+DF)t2O0xWZ4!E0A5YQw3w*#gSe`~hH9p7ov^e$kd- zO$ChoL(n?==yyEj=cFy4p6dnZCta}^oqt7rzlmxW{%8<1LhgkxAUB^+U9m-#4JQki z@G3l9Dtfarjfhg>mi|>n$IP86n2Rkm;hD#p!?fgdSSgAFAV+sW?ggUQmYcsWP}uXsGIK7e(&G^MUw+~r7NFFk4&FlFtd)2rUitNXf(h#=PP zgYlY^?d1>F2iPKvoI|=C3kA@f5*+Gw%JkV&4wJiI`4^&bcbq=g0@QXccD}aw`xLb2 zx<5)mu9`w1bWaS|3p)8Fj7z}76D2=wTC6*)b@9eAonbvI0SLPE_(rn9Pro^XUf8c4 z?WbXLLDS5WT0dwmJdD_#kr@n{V3Utl_`0OmV)g0jKblG^V8wbCiW>FZNYJJ<K9HSE?~(d{~W^V2#Lu=DYL&>7gyW)P75iSEYh zYBSCjptxbVi)hpVdW?^8HI8q=78HR0{i1G{0>){c*PH#hh6*5XKWL&ZPz%ODZ@i+_ z%&nPR3S(+>-KEL-@Mvg7ii5OQsz(+*(^yRjPd@?5RNTd#e}aZtm%>^h6tCV()O`V( zoaRb3aEYfV^vG`#_(zm(_c#{@wOZ<)Dm{=S}M2 z2Of;3%==>?_LnZ3snCLHy;(X{{0Ess7XfoR;FF*py0@r5_O7DwI(RS}gh9b?tq{rj|v|l>7?CsX((H z@V=Q3IFsvWOfv7qY7`mUI{j>E)|oRy=E)26pm_k-dX`XT$+niF9oweN>W*#|T=-Md zFH6!@wub(;3>Qf96QZR@_htFYX$le`913~0sZM4t`Rn~|rE$r2*%B_9_Wi7O~ck|!KSI?S(m0)*N9+!L<#Q2JG z1{ckN)dt>lm%Ay|2xxbqiL#ZOk;kmtjxLbVV}uuuYSreuND-TESYc#fzaV~iU+*iN zA0OE(p+8~sc8U!K6zbh$I?%g?y{h)M>@30E*vin?1vUoIBsQ|K=S78#c;NIMLETE( z{;^RA(qi;e7&~GVp(#f~^%-r`r+9V2j(Peng-yhv`P{OG=HRnto%4!8Tp1P7l8v8h zd_f7+6S3{cKaLAJ)vh^v>QNjCazJK4&+dm|8c~fFjwo}8*$}QgZtF`bn06=ZQUnxAmyJq+`ZTZ}&e z@H*1~i*BOH*rycX^QaXU(D6IWHKO7-1U)bYgNh^gVZk+1XgWb_D7=_nntNW5sR7MT zeg~J1l=EYizK4`J$2jR92J4}Th5Q<7C54v3f3dBIKNZ2DAz-T?rTiATx2>*57OJMU5IV9!$8EV))yv;i6c_M~Zq;yv-murj&@ zLzBOkmYXLj%85(YG$qolEND|~+GF`sixNZ z>gx0*@#RhT3e-NS)V#%hc2Bu24JiJV3whq#)XTPgg`4nWgnW2J>#jf9$zMHr&H6^m zk<3@6pk#Q0xSh=r?-{hEa7lySdQYIyi8HCXXAIUor6<@-@>f5ur5tmNXxyfxZEY^3 zUgcX+Cz&Hwvn*(Gze)>W&sy-x$A*pl(6g#DmJjdS_w_@sHXauycti1ST%EX}jdS#p zPTwADX7BbkEx6ix+z;I4OT5P$p7e)ndg$L@{ad-gih4~5LQ$2_CrUH|?w{Wxb-#^T z8dAz#X(fFfLN`i#UAky$;6#R^ffLE9W1br-!Hem8 z_U(6mkYz@5EbgOa0!+ql5(8@>m4mNn==pbre5DCMRH#c>q*l>Lk8!xzfYtOxm z0b-5B?avR6$G^618*$YvczM;+8*ooG&2PnzTLx_5O^`o*3vQbK&O(q*Z{VR?I9s!KIZE=T#`5VY86|HIZahT!4_ zd7@-h%iAAt@G(8^rZqYFe$!3+ze}-lP$~NCFr0k{cG4lSYu8{- zou`a{t_w7vWjTs+IxD?Q-1)cM%{pe#BKLAWtL8mdT!GBw6&TNK8eFEvU)zZKzFdv#sH4? z!-n--TM%6JEd3I*`RT~L@d@MG)?8KNElJb)%4cK!DrpA;j$}Yy^*#t(D`XsmQqsU5 zfld5WUu1G?z3D0&`B`ijM-Q z2GJX{*Ujq8(U^B_Z*Nqm7@}rY7DDh+UQ2yYshJPRcn!Gcnv?Ah(`y-}<&+K_=6;s~ zQNJNIE^8CT@-ggNtwB=Nj*F+T!cjMYw=-S9@qC|M=V?Y9tGbZ6ywBO5`sS3|7v+@d zZo{Hix)_CpCS!**K3l|>7n|zw%*r;GLjht)0tU<9JF}PDQ?{k9&=j}L_?pEZR#VaR zyLG+#&{;sF&Ii{60SYe$2x{R(YUGquoz5HEBp4$BS z#nbsyy?6JyC#^E@s_t^`&Pe)2LeBG9~)|96|w zS-h2{3q~C+P0uePG(p+K@_z!W3>zl4QXKp%xm$c^LCN%*v=zv2CP8n(zR5=w2`!o{ z*MD0#9_%_*Qth-{2*f{v^~FISaJ?qNi0VdY0vh9S9=aKBjpq)ir3jzeZq7C&5wp8j zHC-JrDrShK;pk|WT=n#Rv9@MW<#L{0fFUO5a#eTj1!vTLf`qUK!0vBsJFz21%9yK| z(1}gjJmY`kl`!kGdkAzTcZxLE1W1u$*GH7&MH5-Dl9|YpPRa`V?Wqhy69fqrvfAdj zu-K@n<38@2_3yvOi$By~CJPk52Ab-uJdUUWlSvbMJ zWz!@e=!lwm|Iav5+K9>WNvSC(20J$NPaL%StK)#2^$E z(h&c_0A6GC459r}i*B`9XtjWY(ex|{l^BTFd2y(+W1ILX$^<2&D3V8MK`Aoc>%ADSJXHIxn5D~;P^Ae#Q#tBc^$?-QWZ*{muoiY$KRHkmvU z(-|-<*)R=8+D?4JJ9kp+*s7Vod6Ob|yEATZc;a1c*ZmMnCu=8)nmIxO4kVF}mE<2_ zN;d^wIxJLL<^itb#xRs9@ELSKg#anr>&*KIT@oCL15?nHt^Jp}`C;*roCWP0>MOQM z(Pd%~(gU|!gG{)QMKHGEYW1CmcBhv!E)KubMQPjjs_dS(g9Z|^hz;5b#AynocK7vs z?{0FpY?a%VSesG`syGj60$x?r?;^r&oE6OH;ftg zAJoDEmB7utkQxX*wHz{<4z7=+fPTc%xfNWKn=v3}elWF?u;z5wm#{0gIkAi7v-I)B zj{X{s&T`7cIof!W&+Us{A=8!yQ11k1CwHleH!WAI!$9oAw<>GC{)aSIXQ|r8`uDVp z#(x8GnhnT+_&jaodGH4V9YJvY0sXU7>jyvxh#3%tyq8D>g?Y$tIM0kH_=L+$OK9ld zjliDVN{Ggy!{`6E&s1q1_FV#?*N?WP;}^Y&EFuJ|rq?cY71X zp&h&@-;xr4@4g z*?jGu<+gj1D_jH}tH2fq`m#i5v3GbwuKV^|plv9K2F6}hPPMNwoJLp4&;n-{rB=Ph zv$1$~ZP6P363OvNWtRy9x1FLX@LkSU$4ckT!99=nmeQ;WsArBZ45Hs7_>wtDc=mFNLc<0~#qyE)&lhRzBkHB3%}sc!aHXFDZR?&~9N zJBU$`u))D>0`fg#IQ5>5exem$N1I9FOPCxISFJ+m@};?fP^h~wp%-AN01&7e>>$Q1 z`o`;uLx{}*C;r2pAXPkoi&PI#(BFmLbsr8WE+WvT$BO3NUC4zT3nOW+)%(d2Ym{XWm5E@Zc?C&zT&qG!ITdBw~*B! zN%fM>Ly0wAm!59s6Ub)FoX_$Uxs)McxZ|*dR`^G{pX$u=U%If93Vw&+PD%QhuI>fC6w?$ckkVNB3`eb3OuF~ds1B&eVoZ%7Vx|M6Y z+fn@jI}Ydt#9D3ytwg0eL>yOZER*4 zDgipKD4v?ZqAf|z^`Aq`^049)MG*RZ#Qt-zWu`DZUsODES;gHq+-Yv{*v*LNvl0Lz zYWwa@Vj(sy4mG;cqVEw!Y0q6cU1bZ{R4dGTJjX4t)u4-oVvH_t77PNQxXiyEeELb%nX(>`FF%`Ma;>0v&D-_ z4I57CoO;EYtNku%`aZA4x)>}saQ`{|rNswwX)@ogFoQBk162kYdgew-Ku~8A!fbdB zq}yYpeXVr(@~D4gT*+|(Wrg&=SQC=`ix{nk3mnGut>vX4OvXG3!Uu?+=fvs6x!~%1 zUGIoX8AOBt(woc_Yby&dcaNI>Z?4&Vz|WatQvDf3HA!UB^`d(8m`4 zJN)qnA`4|awuzsrdCX*Tygl;tDY`E^uz^hHyf(pn9~l^R54Qab9N<$Rt0h*<5#J^J z>3_DUU1J;msb+5kJjYR>_e;^RQ;&uMNZxf!fB5;VxXPv-xYymwK7Iko8!rHr&>);C zEj;x1FCKTpXnT5q>7;D(MD9yXwUIqDY7NkJXj~uoV8yoHMSfR)dYPA``%?y*u7Imv206cY9f=1+`lr zA%KC?NBKYHRIkkG+6T(ry1Wl@>>lq#Z?70YwB)k=$<|7>`}QObjow~U%S}C~O(@MY z^G;sv=~3R@#3;oOq{DYdEliC$E?H1M@jn^@*=@@2?F^DAz7?|C%ZMMU*vfKub0o)h zHYr_jPKQ-td_(f~*gqB*0?T``#xE7(ignxfE_G!@58+9xu*G(oiuv@%T)I`G}ugv^{PJezcm;O?!dBdySsUPdJ2TxT&CQO zdpeR0%EOoM?v10iY41Y#c|gK~k9O%CNL&)N?urVq1O&74(GK z=j5NX<2=<2l|2DfQtTGMSOIBg%2&NOIpB=ewck! zHJE1XA?`3!JBuogp$MHBW|ghi69z7I7!ME(vW*VnH?^~K{+CH)`qv&HIk9UyPFpX1 z+3$aqX!gb+tBe^{ewwoB97jgbBJ%rW`*Qk)yPb)s_cOOyjCuWfS4I7*4HkCj#tX&I z%vF(O`=cEW14^wFVgWZ#HPE?WbS{<^*Oyp2+~ZM~;JBavd)#|!7)7vx+DSp@CTg<{ zTLKzY9Xd|51`OW$F@?c07dy*)xA zsByevO^0;cMx`&Iw5B&scoJY)8$cSBD<$oe1M&RmfWq{yk&E)<>6%>zZF`D|8C0dV zT5|bcJp-!@zrU$Gp!C8vC1&H+d-lw-nDSZe54~!HEr{jH!a)!x_i9>|h=EcS#HE)T z2Ke12UAn(RE$-jU?;!RQ9FZ}gIIv|>B&kk)E_EfIlJJP|ZV@R{$xD!V2*NCUuE|8v zh8h0_tGNmAI@xp-xH5RG-TK0!mk2D0Q$G`k0jnV4)SCoZY<9qE0YsCi+s@g|B`y3<08v3eYm{K{;bQB1=@$UQ~e$x>@jQSb;mP z67U0^B8n~P!299@YlCAH18s7Nw>ywZ#0xqYoo+65gzE!W(KZONKACYD`psE0V~WyH zfd5Cmf4%FvDJp{pC=gP|v=+ZAlWmTK~EKRW0QZFmui5<+q^19g8bUehGhlH4Z*r!*PMy zH#p8DXczg$UxUrE1(NAp=^OQc6mj^Ve z-oa=w5Ep&#QBX=Tp@c8(8Ys*M4DZwh+`c?bP`j8|wE0hC2pP*%dVU*kKWl8%pK)8f z&i$=3a&z_OfN8X4E`uj4YSyVsQ^3*0R{IIk>u`66yB*x<$jd(5Ynfj_p0Ir5YNgv&I}%phISF9}ga#)6$ie8}E0Qq^p{_9Fp-eJ|X+XPeJVBQFzf$BGi%5A|DaNH<4T z#CPn9B!Isvr9sZh5CuFtBa)VHG8F^9jW&95o|2kegfqo&w;|Yz(;{ z8n}xp;epIU4k7j7ve@tP5+rUq%mkFu2tDn(7PO!P#ksdr|3le&qlPNhHxW%f;Ww0( zewT|UU9=T;V}+bnX>EmUUCI1?dZ@L2%iR%26&ijp(}r30Z3L`c{ zC?#!*K0sXTYH)+L%TShNu>0HaMZvkVfUPvt1c=Mq3|`Ch9z=9BedhZJdOq@S=A<=x zmr&durA#k6>29snx(Iqd~gECcCDHaMK+0k{4y+4Q?ndcQuv*XMsHw9gzZ(pKFB zqZ+)v)c65cQc;P#q1y zg*Z=F7u;r8Jim76`gH6j==1LWiuvE44oy3Ar8;FrU)Z&!FHRjw+`e&U^GUhh)kvhz zbREsF=CvBsQ;4QZe`U0FR;rm-%3&a6P^b-*Baz7k>tl2}o8z1eAWC%f$Y##H)2w+m zysUyYTuUKVE5`pi8CQD+$v4SlTtMK`SIGs+JSBcs; z!Ik!pznwQm&spKb$L?D~Mnj&#nC-HST!s`P689+&@yqJ0h9+XUjoq)Cb&(K@{YI}% z{#PRg$e_Ir(u|J~9eVa6?cE$_Vzc(uj@LhOdrDhW;$xXIaO+L@4_Wn&?61@A7^r`u z5oLJp{7l0s3@^dc7i8O$`#zsm(g|s=Apc)H^&n&7o?L~DGNC+BnOergP!iPh9omY1HazSccH}drlEeVPzaI5~rMJO_P6V58# z0Ag)_*3^T=`Kl)xm#TkggQDPN`Jk9NmHA=#DTk=Z1I=B{=uj$7ec716(~$9g(Trh z)z`@NRS3cLQUq_G?ocl;Hi;SdQS8fI9?>9yx32lqGnZj3*bKNgVR3`3I_T1j9GPrS zVT#1qFh%7D!cu6}bP{bg;nt=s3aumPS40T;gz#sjc$9Ouvp5*!3iwcvx~3Ba=hv^k z4{lfG**|MHJKy|Oc=p7t7Wd)A&%~{?sM4ssQ4=)E+eei9o(mKh)TQ*kX{24I_B-B~e}?n$5AR@={lr^Dwo;Oo z=2Hf0Fm~>g@rUT~uy$Kmmc&vA`|f9x*}B88L{ARl4H~Mo1Y<9tiI7Qd-^L_#yO)9R zF_K^HDzSI4LPMkGg5hzhI9QWO5^!`#jYF3gdWQc&&%GybdeInD63H^4wu^N> z4Icq+aN?sLd_-rhn)nX?g%OCojl>N$3GNnvyd@|I|En$SFPWannI)kCo-{pxJ&WDG z0dy#9fow1d7P{|&36pn{&NEfQ&wPUuQ@)Dk6j3VWEyXI8o{Yi1N{|D_$79 zr4mlOL~Z?v=V9pLgT&!ZDnHX)2{y1rP^BpIc zU-FKXWD>?^2;SoytpfOq4X~@14YJqLZHwGUpNvSlZ(kTVuh<}NLrnNJ8r;@-)jbl; zCj5cZ{23b&TBXmaESnAh4dJi!vf7D=7sQxCOgg^*%2sgU=cK~cAZ0KGf#lzlDagX@ zTFP$wjq;? zo@oC#hGDuJ=n+OD9iO{cj_?`O*iX~SAT!@m;RR*U?$LzeGRDkVzNrw}apIxp8r(&$3M8qN0#o!#6i=apeqi;yYWZkwkzKUjFqIk~cz1XgL8 zx1`)gehav;#x+Oej%Ed7{PajdCCmMOZF=l%=(j+p{q=OlF5Q@5cK9f*j%OIX|KG_JfKBhHxcC+uhIp}^)) zZ2H(LTap)21wgWpv2*GIhi3MVV<-|*%&mdOfK9UQ3v!Bqgc`6C$99@mFO3Nl>ijO< ziG!kJg`!5miT$gV%_jT8KwdigfxsX?hM>c;1SPn-q=(=*kmm$VIJyyFdIyat7A*TV z*e*C*C7Lcswj7d7GF&F~h}bH7sU;D|QO3rs8x%qwwnR@xo-#CXUqsbEB^B}@nP7M^ z#!*jzmHPCxWbr*T(wO#z@GmX$(1^Be*ako>=$pO)q6-k7~2h**|u zVvutJZ3`o{bzw8|ofm}@TfZkL#;W`9S1I*#n6Z}o&P;)NmM2daZ8lR#`}^*WLB&QT zfN8)}r2R1z4&XvC2-uLwf;pvgptEiOgbe$V*ZNB+-FxE+F#^9UG@1YQd!W-x6^eq$ z;nTKv4{Uo)u9n~z5usXp_qpYlK#()0294APq?|;F=iw3g2t@$st~5EfpBIM;U2hbX zez6AL5OhCDboNy&XNuvL1ze|8VUM>HiP3QK^=NvdeZSPEutTGm4RTY`nBp@&Fa(ac zxRykcd>DDQ(rNN23A`QLZ<<0!JD!)|BkNcYbv_TOs4Q z4CNV~srSR(7FxOaF#z5B!&UC$>q}$NXvid0Ti9jRbW_x>8G+SsrmEkI{ z0YuK#?IIFGR38!1?Rb_H|~QxxqtIS7sd0q@`C=lCzc# zYC2W#(;I2h?Tgc{H6=I0qwnN8z{Or|H-Gan_f|Q*F_}Z>sglR$P->k<7WSS)wxDcw zYps)uc^mwj24eED^Vyy?m;x5;?H{*-{a?qj58SI&S43s+7)`v;{4rt1otD{Y!HhE5 zZ>B1rxGkruM|`OTQ47CL^`&)Xwdoe(IeupoB=ZoDdMvezd%rELQ(hL6R*(!~e)UB{ zBTt3K-mUhbTef1t$g*2^xhYi*fo2O`@~{ycU7e4Qe0TNCknRNy{rLHe z=Q$FzpN-N4}2 zphV@;91u0v)_X2@;$8b>N`HK|jnBwzM7XR0HX&nI6z#;Dfe&pzr9SovMfl)*Ve@P& z2K@;}1WWLfM(hH2S1jmsvHG=h+lHEVaI>zWS>jLE&S|!Bn@6|emkO7&wjsZ@EN&&g#l>TaBXVcr?=$#CogMYHUs?&Dz+)tVx8f2` zyp{ri5Lx?xnQ!cF8%MupuULiTuyB;-AUaOUN)4yD{227UU-|s^uAj-&dj)_NN zXDXM0aOwkj<(X=Gdr@1PqrbZ4uInV{>v@+rcV$0vv9!${dD z+9jdsSeA7F_0&Ci{XxF<0DR{EybI|Pq3A_5`SPcHa#_n5=RnYAsEilE+AYsfwn*@C z`B8VG9f0GquRoc}oaBq&UTF#%EPJ?_C+mTs0+#jPbGL!>PD9YwY?)sm-Hh)P_;k2| zdvFA=f70%g8?l{@TvN9YtjY$OB*ySDFkID?$uzs*E+^+%r59G zU;XXg%Uw=lIt((0)JY-HO70p*{PgS-W}B`f$>;qYjR$63VuS0>+<$31KXz-YVjR;S+!;ob|6tbxwn2~e z>pXc-!GUccD!N65H3^r9#@Ga0P0?=+qkyJDIjYJ058XqO!8B0>6M*~svzAdK4>~FQ z8_x(8?L4KI&hGItV~z)K-_I^ogN?Yg-Wfn*^n5_;;Rl&y$^;P{YSH@bpkG0T$fxpcP;+bi7c3oiYKtc)2Zj!&%44y50Sx(MjM6V1!XR-$Ti zP*@qliOK8f-qG{>t>bhCdE*!3kYg8o#9I{N!af)`M8XAmzr2(%wUd2G4IH~gmUHmX z5NQk#q{ImlTw#SrA!j1KXUH%Gs)qNtZ}F?}V;?-C8Re0R05d=|v58o}o>G9ahXO+G z?EdrX%L`l(wbn8AfMJ?!m86{L_T-3T!5`DUTU*{xvUCS5D?~Y|sY3s6vvUzgqaq@~iU}dGaChGYTFE zq7fl{1?2y`0b?40ZpWx&Muo$TWC7sYXOPlI8EuhK;y)V*@(!${t+~V!tDVfk;sK;5 zQ?_Asqn( z0~h;Ur$ckWwZVU9>fMSIkxTg*py;z1W&5Nmsl{MU$N2Tq?@aJL{U<|7a(C+%T;IlQ!13*gTCoI2rTT&Sjm@p|M1p8M)vjm%D zx1<)N2nNU(evGH(@4W~S-OU(II3KaG?1?}vswiax@g^Sr1_l;t>_qpnEMufQEi)jJE#pkb1;L zmj3>LwI%5JBG#QFF=13Z=;|Mc%A?;!+l11|jOJhZC02mXXl-LmB(HHbYlzIGA;Oer zATDh%>6n-Q1)Vi-^v5H6lIf7|s9ANrzgh98 zqnG4qaTm9Z=yaTLACEj<^e!@WM(h3+0Z%KOV5>z zvIRQ*{ytc}t6m{*hP!W5z^AkS(D#G`!}h1R+Hcezp9j~_wQM)GeCq}HQ?a<**6Tei zHRSya9{tetI~eR4_=I#plZ2q`C?Uv!+;(fZlZXDI-28k8dcJP^d?y_ z?3fXhJfB>s-Rp-%jHt$fFst8VEq8fi^2`bc+}u}wY*_y1b6LssG9Cfw>FqzH68gIS zhTzHXe02uViN|nM${|5@E z3@yGomioE^5@ITt+x#^$5uH6zl1@^7XR=J{68&YEYws#rTbTbMF0@-aTYj!3pylAc zl(e@g4IR4^4Hvo)ItK)*&J9uI(T>i>pO6=fAZj)FO!pEk(p=^}D)> zt3e_ayY#%ZB4|k8m)c$u0qNi!c`1c7#U;ame4&5qbi!FH@dxsX%+w2Tplm1fVf%I_ z2GQlOxrq?3Gtk@v(dAp^z+n)BFIk{S7$Sswy6B0h8d%YLGO@?)ao|)K^z4ffI&y(C zv^FRzr-Bv)!KZ7xm@sBp+3!l}Ew{+Z*2pm}x1aWZV$_&3Q(q;y)D@N2)FByA1j{ZFnhy8civg-Nbhc=$^;l^_{(V8lyB*_s_@M;u|AM(sCZ^J*(u(1H)?nJ%V z*@AFf6&}Txa2G#V%dp7YNY1okPwcq`pcTq?s?Uv5yBfR>X~>`b#nTqWA+h_SZA1?7 zIrZlRcB62&>qj#t005Usl#^pvzoB2D`#~p3-vtDYCBwNwniYUrpdkMEtRy-R^!O)^ zD)8+x0&ePX$(~BQoog%@qjx44&aiC5*1b<2N>mN_rm;xl~7hgqcUGsWeLY`J!}=W3m#7#!(YhmBKP!n{H9TCqJn6Yt2HBtlZ_VkF@& zI{P4VM^e7*%XVgIwn2oOxIU5b1D_+j8Tuvyb=(-c>~a18YPTgNeb5_yMOKgwG6+i6 z{w#T8&H|YH_dGAUM_wG42FyE-gdrtoB)u~wkUridbU{vU*{~t;^#1#G;e0TH;IQ)t zsv>-zxjzN7?}lu3kU8!q@|YcQ7#l?KS@XR0#j|RGV@$O+d5(YA1J!I;2I{?Phl5q- zFxl52M6!cbZWyGxq?Si}h>uv!hlAc%Pr1c(wtChF*hJ8>uco|)yiyij(-wI0v?P*o1~=Q2 za-$=KK8#K*Z8wAaIlopa%-CTC40BH&Bq1mXwUKE6^w~kdI#XEN!j0qSF}PflSvMJi zx>>*jb?(wn5G#SiLYF`SIfZJvS8iOYcnhkAED?1lMS%PdLm8;je`x_GD%*hBB2JD* zfrz~gTYai{TJB)hgZ*gEyoV;{doZns3=)8M&3cav4xdw+IPc@$R}JoNf0C>Y6n>Xe zTg=uQ9vuDh;%C}N@m3Q?l!XQ1^xh!AwIJpT$Q^~BkPlZ8vx#g^1%`U4b+>{cRz>rI z@0UUEb)~pQ>}T_z?ilrzZL5r?$JNuJ{Gt7Acla z0%-iXt%Zm(+-<Hl=EsoBRSuA zoT}J*i5a)f9-h}u9ok5Nrr*m^*3P#E+F7#PFTFuw)>q`qslKEDGFjonC%-v#Y{xdT ztg-<-R*EP)+WM_ksO2S_Bzcj!19YegIVYn9U^wAAtt&&Glf&8|Ubc4W6ShF{i~R8F zNEg@0Lr-?t7MuOlsJp_V&@HAqvXh+J!8iKOpqfm3iH|FCOxLi*-*Cu>e}qiGsika( z7Q+@1vwt7poTmg|P4FIj3Mis|L!tUtQ+9oY=nx&GHwYmCH@`XvjYuEt72bUo4^l`w zZ1Q}Seyww6-Z>RLHP8pA1PTVae!e=HzI%Rkm|ib6e{HKOmc)Q;m@?aNt!-9qqu@HQ zOF-XJ1awxjOIP~mKqw|S_wL&-6g0Bs3(-bdg9s4-yT?m^WV3$Hn#rQxZ-snFag?K! z(JX1%@2TD50i@{zBVZV&RV=GIn%6k~&HomMLZ??N)s?ax8;T794Z?UO0y82MGEz7P|1?&C7-i$|^LmkqVatsN$a{XLh=f2+W8{oR zyN%@yU5;#B;UETQ{h*8X!^&pq<=Ko}6sZRCqASYdb09V9L%xomlmnallG_)>5izy| zM1<5F{1jatwKHV^JQ6Kur;FxwH~YzDWwQue1oG()5CE>`aBJnc248hErjW* zfFc;yyBk!3u8Dj*iPXQel}5_4=&&d!7g?>B7>n-*i2+|sSN-tgwV?|rD3iW-r6~L_ z(h}5~>=}0n9Z^J_RX?8`xo8k32_iicTLf#ICH)QayZcS_;Kkar2KPjimMZ}Uck@{U z_BgQWzbeqU^|<+*S4zH(Zl~>|MEz*u^oh<7ne^@0KtE7@O?m80x)36Q!!|bu3+>;9PGA)Gg|a!UtFWOrlk&sXK{PMI0;O0pJct6fz-U_ zr>*+XKuV2Pzn%=zwAskJAdgDusqz|&{S=Y63On{-g5PHGM)Fu^sbt#*so0#-|585$ z0JY>($13knG%09eE|dmd)hC-v16ijJyKA%`{ugqh$7tt*|K6!hIn3pI5iRCW`B8>(c}TDz)WO1i9wgYgPsUU9J-4o z6bO*cJ?12-h!_cU9GI#)1`ig@*uA%_{TaBEIU+h>$ov1lot{bt&3X06Yc%n864otu3Kx_3himyoET`>Ay&<2#mDAPtiS||> zz`tdlv64$Xt05z`0^gNh{!(RTd3leNkva-ydnujDUHW0eTK4uW36U7! z$m=UT7FIw5IHA`n5Iq&8Q6WF`&_nm@s>Yu#&NLakGGYc&VCe6m|Op=SFh|`8O zZ+(ecFFPPnj~4cb*TEVO+_8x&__cT;hGARVU13Ow4Gl`QT=aHm1^=a~|p*4SEQR=wxjh zdk+GCc>MRB#us$9FY}h;@jjAWY&E6WCsl8!Tme9ieNU`G4c^QLrjOMZikrYM zQnRErTgM)oU*5ev4cnr=L_uaK*ew4(xm!|n5^&;so#c6;gP^iO_;u1|rUO%n@&94( z&7+}y|A*mBqXuE@duGO3q_Qv3GzOzWh$vf#EEQQxWUOOtMp|qw(jv-M${J-$D9Tom zO7@bnMe2E7KELnp_q)&Yy`OXL`}yZN_j%6ge9p(rys!7`x~|v0iqj80^iIG(JbKl` zqr*Y&oI$4X%XV(sbr)`&u`LC25j=U`d7NR!avnjAVIreJPxl6KpMV+m$vFBpX90()m3XSQS=Ll?idvJKRpWNHF z1j~^!z-E?xsnTqWxlY7`%9qJgCdAL6180}!<7b=!*jYQ9C5aqVPS8VW8K$Yx*jyr9 zd2=-On}DLDmHU_yiT+mq*!KIuDVuSN_k)w#lY%|JnzIaE8}v-FPBJ8*^`v|GBVp zp7U#lg!p=QON`;I1j#BH)h}vE!OQ^?Jf#dzy)ft2Gscv|#d&AtyPAZfTqHo~R-C?U}AVBbFCPYAJecaTUU(uerRW=mRf1W5w7v5%yB%)$Ge z0azjv;H0yeAE!Y#;ZMqTCj6j1-S?<%o<=Cf-X9P&wYDC9J6G59;w{`;p%EPE!JqYm z@ZwN#G_>B1{;#`iO02uNhZM`nNIt&BswlAV_?Jf6AvL;mPhw!s=F&-CzG}Y`c#cW- zg`;S?C|abz#{Zp4@YH14$QYf}-)$i4asm@?o{&CGJh^zern!C)+)q@CNXW~>f%fhQ zTY7usR-Xm`_h>)`>F=RF4g8D0JXyxvRNEBcO_>U=@z3e833VsdQ(HrXGJe(2E^IzL zfz6VG>jHh>_MHWBJsES6urEKGc21mRbESkD(#OHiP@`Xc#7K5fEIkvHT`-gaXRk?! z3>gX-rFP|&?DFh4;wP|~KqNnCb~fOE;h1P^% zm<2(DLgPEhc}7ZUfFuO^W0Y8>r#A=;{rg$$=|DcyaI-uM0V^U~n1HCR<1mGv#d4d{ z=b66(al+Gp_Gc)F_$YA7Dr$`pX_3-!Ju8vThh(nQ-L^KW3=#QN9Z#z%<=g4Nv#kZo z6kcp+k)%`e9Jp1zZ7ndVkjXj0@EReQ62`D2FGYSGjcIxLENW0tfi`vu3jW zG7&b-SyHs2Rt;9Ib3&W*R@E=ox7dS+Wa^0L$@}1n*0C!o$+0!_fyYz!)JfL)GNZ1W zf$zWT4B?31UmCI6wkA2$DubOFvmx;rkl%M8@C&@_{_Xu1xZxZsWxH-ywIuhGawHhl zjyAF~FPHt=em4hQ^Xy1EQ%ZH8@?|w~Q{z$v>t6_QA*#nBe-rct;CbjSUS(U5xI6SHasPeA zAI&19jMv70LUo?D5d3~*)96~%i}&!GoOY--HCQ56_WwZZN`N6rYebfQZChm_M>)O+ zXg#QggS)|(OR9^FFxUVr zMHYV$^ZPybmJ{X17;~cEw`^LY2sJZZXuQ8=(~S}Buf{G7%B<-gMxmWd+Qm)xzG{X~ zZRJ=*>~=L-nMR0#i!j7xnX#bR2%tyOj(2kO7FhmIOQuC?9Ei9j>|%w_ijFIgR8(OK6|MNCIyY-96H;z23<7B4uG%hs7hE;Bb~UJSmp! z@bEph#84Jbf;SK`?H=%s-yjkvv?#V@Ob^Hn?|*O3y|>= zFIJ(mg&Q?l2#U?#qBZOGd{qb&?#DL06cPKLL2C{sMaLP>+v(^6DkDny!9o1DQXJU6 zAKwsVr!=_WfZ1(fi})6IQR|Qln}R@JrH%XdDhLWO9^55%RhQB#)6M0dmc&QZ?G!Pp zby871LZ{U#7%e?ffYKYqgB}t{*^&pDz$HSpx>)LqU>(Lwu|;!fGtvR)NlQJ z`aJplzUU(xZQpW{n{i72pSK{=r1?u;Bxy^Ja z1adm-IQ(L(QJS2LR0wAAuh<`?zc3@WV!xl7nSMgrlyP@Z#Gx8+`M+)G4np=s^>;*i zi#$U1E6%B@@P^X}*=uwTpMx`@)d|oCYmAIj_{V$9-21kvBGC)XJsz0*&e_ndz{M01 zM@`dwNBVERSUzI_nNRb&BoDyw1^7wt59-FGWWZ zcJ)GFX!QP<+YBj?4Sw2e0ke1MQI>@~XBD;2z_(9en@cH7Avo3;2Jl}7xlD{72T3J8 zeqEmLWbrx(J#_+}v)~7P(Lirsw_8ks@QV!K7@F$tyfJN3Bpz#7*b%-_`y@b9hXmmU zlODh&JhEBpJ==t7gein{xl4p;r0WR5tM;)x^knfUz2k7q%Uh;|D71W({CPvfK-kgpu9J_LS=+9fQTr{7ZO~39-w-FB*TlWdn~dYLPT~w z2{}4L1`v^vWyQk)Kt3TvoZ)a?SS1$fnkOM2Dh%se<5#)V|0YKPD6YYKe-T#2u?xI- zU7U;$93;*F_A{jB!6gd~IR2GKK#D>*>?*EpwkGjDcWVDRN$#~=4ey5`(2%Z<76XCZ`@){ zGh&O1u*zyVAFT2p(X^c$cF2*$E>CwP&~a!L++`{cTE!NvQZVl739Cq=RX*~riASp- z*2ZSIYOMa3qF@IWe1VV$J)FR{EHFfQvNFn)L8vK$aL1>%?-1EZcEIXJ9?uHs;NfLWq4`!0f0XAL}u}g18%Se>UPEB3LZA$IGFJf4izN^!$C~<_VMPUjy6J&O zCz6Jq{3q6c4BK;wnx=3rObX#nB$fh7orYj~|2mEsk(tzaJLXqx+nr= z*M*?8$VLQ;FQre4zjzY@CGb<?zmok|F2D+yrU)$k(G+AMf!h$kT8$eZt{dr6bIKa~_XyBA z;5|bRNd0ly=Ntg@I+BbB2YpkpPtjJD`}!3*sFFxNyiY$#h93wkfP6@@z*m_d{w0r@ z`JD9rF@W^{bD{s^xe$#>9)7FA$FLPfScAMGb$Z_~D3MIhH`VYU-w2=&!g9`5-)QVp zWR|rF)Qy-DC`dk2^+TN2q?Hp<2^GMg zx!7;vutSc=3m(zoM7Cu_8R4U;`iRpiAbpu!PZYLdPznWsGGo|Pikx$^B zR^(ci;j}4W=xXfG>*wK!VSyA9{aEHzg%ABb)BFVniku93bn;7U*KG@FD0;|#X=n!n zfZSWtO*w@&5C7D+t@RtMKJ^2NwO&AjY5dKsB;hS-&?lh<@>B94lhbx1C3|@urpB5= zYdT0Cc;Gb2c8@`i^z$>8(Q7H2}`guNfsr-;DPgh|14X?Z4e_tU#Lqv|13W9 z#MBzV-K&;vQ9jS zgb)$|e8Zr$^Bm{_H5~96U#Qr3`M{&&jw^atr9Y?~0$W`}yZ;^f45vfG(1VaZD42E_ zq9xO5N(WR-VQ5A6yBjVkYP#rjh5+-Gu5tm6RKcPTU+gWpDm%0uq{_~r8Qa_VNEc`o zRJ|W^$>M)Q1G4d_P0)+w3(WSp2t6QiF>d2sH*W@vcKrC1w6zuT42R+9N@1Ax%94q6 zxGW4aT17!B#dXJEL=)7<7jX=4zXB6{7Rk^mYW@NX* z)S_9bm&$g~6=AVX&lSoW9S5|b+M)K*uGxnYN{6*3r`LsgHr1eVcNm21^!Htvr$so->^_!d8(;g;vFz;MKcBu12!}T!@kvht8n?gebZ1BWiyXTiJFD1r3u0%0a8ge zjQ3|?4%x%49`%W32OuH8;0Irl#!LN$?1DknBdue0@)lQ=8(<&KLtbdBtC+Cm16!D3Bz$DR;UC$|m*X1_K5@@XZ`Qnhw1Mahio64XWLC z*+OBA6O=;T>cVH?W>n%rOdkA%IX1h2qXepvG<$d9J2Ex)d^qLruwK*D^?dH_1+|~c zvP(EVAHkntVZgy3s{G#Q0?9akKMx2{=Ag2pcYa@diM_z|g*f+yX`Yt2X#cR&j~Z{i z2psPkuhMKvU9kFXKwoh8rR{(gT`t!a-gP&msD8#4tP*N0W zube&qn9%=QxhoBUgiEE}EpP zIB3=6dVL+24kW;qDya%ngxI;LcKhjX8Yuk1LpNOFb%gJt*}WylXwNY0oGs8!&t`)w zk-h3oNhtgC#1a=GPVZyMzNm7yiM2~Hv6pi6LcFE-Nt5&jUb-~8UMOifvQffnBA453 z1C({XIU03A>A+nVG>*!>J=5mP_B=UZX7RSzVE-aZyvF1)$;Uk|$CXv-LQP5RTNySo zLE9gbb)Uc#`byCw4xSOBgc6pxcoLm~BPV(?f}q%(T1TR+;lnX%Bkwecru>R)nd@<8 z99&i37!XSB(Z2B+#mTPAMK0JkI42@~ zqwSvY1PSI<88wiwO2O4`%{z7nLa}x${$s*vs9C&g^+vIe?Ofv>s3q`eQ05fgx39S6 zQ~oh!ZQ^PE&If(%=VlMvsn@H7;*6EaV(goGHbb#_epl|~+WXBNKcWAk>50atf`2wb z@2rVN%$uqHvWHI;Y5iROUN~Df6Ad{Y9yjHy9o-_Iu(~NIXLQx&sA%fHWq3fetUYu7rxA73$Ok$e? zsD7O?lDGC|2hsjjKsQ`Z^s4Q{dL0T z3sXm-AKaU*XMa3b5dSivV#n8=;6pU1nPV>%of~o#obG}`q@J16y3yTm!S?okIWu~k zOHm==zG)59_(gp-_N)yv8&gwKuS8gon}nglD)!^#v`C9>^slIv*uJJ)mM!+i#c}ha zso)-K07;S3m;a@XM)(Alf$^$k`W>0c212rU89J0<#7GIp6^lkUt_3O=(HB;PlZxWq z)JfRU-dMda+KAYqH#O1xEh2C3sm@hee4OImDQy$MEyB8lC4#p8LvDJakzC%3*lQZX zcZ%0aaoLY?3CA#9xx<^H5=!ib@9{E58t$LT&Wbf4e{yAe0WB|%T2k+B)D5Kl<>E*w zAteg`owq5D%6a5Zz6iZFm~GnUg}AImWjg9GhRV9T0s-@nuD7hBv2E~A)M>1+AJY=6L&L&Oqa8obgiUT0Zns-ChENvB=j2o^a3jWCjD)n^R>6F?8J;B= zQaGuXaXWg1YSI=3(c`AOkp227glWLI4JsY=U=-LRA-!L3>#+UQvPX(19OEBa`P`tFO_Z)_`> zw-%&k-tXT(U!I=jSp2vm*?c{8NuSM&$z3m=^#R5z#u(B4C#`q)ZeFyzQ-j(Ic>VP6sQ0GkJq) zii;(cxisq7FtFQ5cJ=Ly_ZpW^NyKNYlC1$TXd6k2X~p9e&eyocE3wFJ=0!M*^B8ey zjb(vdLzKE!2iG&q8yK=VE2*R`N-P!0g|-m>-?ogb`|?`=B0#?*Hv}QSI%IG4d7|SM&6gj~?5i#!y5R;{GZ9H< ze)x-_kDlpmBzdqj*iAxRUK2Yu8qL6khtozv-c^N6?*{GI9Y%~+o_eW$PH4J5FEqV^KsU1pU?H9~oSmj&n(ApB912{x=J!+PGrWN_&8BD{XLB z8oXh@iqz%^sbf;KSb)@Ex%!A+D>H@s6%b7O1U$p|XlpvpLBldd_1hnUq&EjA7&c^t zWQ9FF{APVgQlO`CeH8z<#-}NkXO6vl&(m?7;@l~6qVbS4^xeE}S)y&RNrjzj2Xy7@ zOmC!xPg%?yksMhnxfAWU%#bvq7H?(GA0y=PUH6_9nR8JgT()G!@$hjl8)-)II))P8 zeoDRjv(&W{g_?(402z6%oZ-pAlFpF>rx{JRDA^{73#v5Zfch%ALCcv4v3DDYe`>FO z%)y_0tsK*-EUDPY3La; zg1OlD{pcUZwI9wY@meg#vZOeqMwcb|+OxURS?Y+PBZ(CD$nr5o!8$Vn%+{xL% zJexQqPJmgDTMMt)M)rgdqc?2#x_rkxx}ksm)t!Zp{;0Wg!~4q#ksFB6JL?i?xqZ1$ zM$Ac8vtUp62tNG}-!vP_x$)_x@+ukRgtwByPR@8dnr z8+UEQ6u_GS4iF{}N2^PEWrwJ+P4n!a)P8@!ODU}{lG`}2u@LZxj=FGRHJ*3?TCA_xlk8!&kZ!&50KlhgM9{+k9zW*@%{nHx|bX(PCf?(D(DLM zDu!Q&LqZ{-S2{|i1TWOT^Gv{}BH-=&c^F{>7%)Sl$B&*W(}0hQurRnx9*XhKL#Kc} zVBIMo9ckx>h~;~OuOIun!4xm%BFXN_(u5LBcutkv8Q}nIIHnbHGc166yI> zWT_fFG-gkdi=y|y9P1;{s(7>UWOu>splb%y(NEKi?^0?W$*tD!-@%YU=s^@$z#HdM z6TV#DmZNt7sve5wA}2vY$%Ze@whHhITiy?qCnQT~c*}wZpd05#RfbyqL(4B9p!`2L z2aDH$4#u??Qw8A;RA2A=wD2m*edb!}xa^sV@;7x+oGz!r#exRe!~`r{pHBviXKuTq ze<=9t+nX4FfEHYj{JVDB;b=5#FkvkC3c=g3pD*FTV|>};jb3N;yW<7R7B}V^b*>wh zzHcH;kl@|8LlL>$z%c^s&C8)|7VgIjE0^a2UIIDC%0SgqKipUJVva*E1DmMKJ>V01 zXgmHK&px!rfJX<)bCH)BD*q*aT3vb0wQpZ8YJre9w)H|537DJ#t_5L3e!e^(vZ?Ut`VXfi^%;!mL9fOgf#>*+eDXvh{F-y^w z$=NksNc{x^W;z++xTEjRqH_+qisu=?dL|HIut!Sn~kyt=$8>Y zr!qbuh6?$}wgqk2W$69}*{tH!$Ay_#Nh(|fPo^&q3TpGCi%9TtQ2-+RCJ^~1Wvi3lHkb`0MWsJuUSM9mG7;?;)1cPHFOxY7e%7wB zy)+Z58Rh8d-D`dGaB<%!2dW+wq^3w{d3%3EK2VK^qc5)>U@e2Ea_nrU6Hvex@G&o) zpk6Up8oLoiGp7!a(n7$4qOx&u37oe3T`5i!Thy^aSj9o~o(uc>6HuNe%g1);?*2_e zHBjNS-xwN&b}PD4ALkNI3h|3ZiBQxmbE$8ifccq^Z(N~ugR173k{~ z_t!3(gk|~2=s*@+xazQWZUXTD)W{z%b$_0lOlX2~@J%lS>wjY*9>&lB)6w~f3Zym` zp7?@9o-E!91Ou_Ly@^KH0g~a_vz&Wr1?_L{ z{$Biij<+qAXa~;HyBK__06R&rGsmO85}b-=H^RUiHod&e76Eh@qs@-**YhRO?0<`>@_%@C>iS63gK|b&!>{Z3ThM#? zO^YSY?9A5`t-)6jq?=o@${hp^(rMNw5zCUr=$5x2Hws)D=a|{}8&I_9?7!}=eQ~Z+ z-h$DDYXE)r%)&s3jjK%ewxtK)Y}MZ*Y*{U1oct<+oc+nyo?7&2?|wnX(d?AO0575& z%Ujg4X?bC~cmEUSeWA_!{ceWy+;V9Lo7(tc6BxcHBSf|N;S7dAHgl^uuO@7315VN1 zo@dhPO5y0X=xI!ea`YvFwsB0u5mh;ov3L-8*uA89$RRhZQ?}z<;n)>F*0DJAc@I=X z?`Oivomr>U@tgYQmZe{+Vy+Uj*1w724$`r5Ll--z&Px9dMJv7=&iE`QQ~QySRiA@LM|OUi@K?f@ZKVA~JEAEP zE29jO+;)<8mbw?e+!0l(8uT{Mi@n2s70C52AyzOB2j1tRQc8NheetLFwx1miCcji< zUSYnVeFy_*?XJJy!&|&$+P5yPwcOy95=Cm1rPA1vQY|J|xrD!-fTJ-?owoWgu$ppZ zZoi+tyL?PZSiOB2Bhu`maz=9)F`4Bm+9j5s29Vv#59#E6kaEZV1pBdEr+{>YZ;d)d zsGrDq($AwQQA>Q?Nq;U|i!47~mn)Pj5)_v^RP97_J@KB>xxO4FYp4NsXl88-zJ!&8 zIBIwO`Ip!AP##>sNT!S&-38*8XSa=)LKZL^e<%}F-(UMzLvd~#;I7*g4A#}+=|cB^ z9Ldq`J%4*U&%0pVLkXTIt0NJ2S zU9%~OW$N9+rELQoC(mSMvUVz09dTI8FAQ=DS zAAKJRYOx07pQ9Q)*{$Egh^B}61rg`k()d`L_M&M0PPv2yXI@7xr^6%x5Hc$q) zu*kN>+1D0FKqJ{cI697@jU0Pd{v4K;`tmMp;oF6e&*EP|y9Ct6x2yQD;n%B{waef1 zyxu;@Z`S0M(v?MB<3U?`F}DKov&laAj{+=k0UoKUy#zs~@i+PBANclLHKj^H6Y`>q z(D}a6YX?+N)9UKc-S7A%smsxq40pepnnZsLH<7}nF^CLYb9DFe$k_YucTZ5nLN^(i z4AmoPz`}NODpO7=GLAO}f%arwPZ)?TrzOP|e(J=!i?qVZ|$T*9;YSICao`b2*y z&DUz${N(qKJy7$W(3Y*E1})Y3O3oQ9-Pr-G{KrVviB+vyZg)BpLN8TI!9PdLDnR)C zsRHlt4{#V&xoc2;p+UcU1A}k#^S{f}JCQ6`cCK}Z)xr8egTYxJkdJB5y5)I6QSD}E zubn2Q<^c-OBKA85@a-J-JI|TVakQt#xJpGy(z6V{H`c0yFY6hu`%N zGw0T@0rA9KMu~bGvfF_g8mMbJ?|@ZXyZ7?8T{peJs+oHU<4$f`-P~LzX%vU+`eW)JA@+KC*ldlXKeHy4E&p{hM0)cf)>?D|L*E=skWYBh3 zUscG78eYmY0Gt}IGEX29S@ZxJXF#pq4|3%v z=!c7fj1Q(!8^2{Pq0O@Jj_$vtuAdp6S6F{mmiKh#8hsfE`C#LP^#8AGV_+QKTm!V` zJhy1jl%(oXx8_(GgmRarQMbS&BM^TKbgP~9y($8&`|}fnVYhKgx%^$_p);S-R2+59 zjwC&iiQUpQI6u}Y(;7-tN7ENR#P54wdL)U{6i00_dT=;zW&j$}S3^8|^KqgfUkL~6 z%y0|eg_OtZbNR#dz<4N+-rp$t0Y*p^{EbU)xJZo`jQz}hUPtnYsm zTG*xkfyS>x>x9R6N4WAM`8pFzLN`Ny@g~lS4VGN}5?h^iNO6(uSf@o_vQ3z{D>cP1 z?DEcdBo2w5%?pM44k!YE{FzhMvb!)pR+GAX!2kbn{&#VREFKSxmaV>JH_Bvv{A&6? zGFd5mA;p!ru44p_+m*DYo`a=HQxj=v2J?q2*O^r+%*thK zHo_|8Qo%g-tbk08wNXLPLR!2&;pYy^k(XDC=ExT*k|>nEG$*d%ee}WYWwAozM3-4! zzFcOmB2Yy-45B?`{XUZ`*N>>zSp?VCIyc!nx3R^v#e(|KbptIcY5O&EYB|YOh`qCb z**vE8t$qP{Au0Sn1U{1o7ON~FU}N}AeDn?F8l@>JLxD%TsN11Br#{g~)3@)Ms^DJy zuSo+FV|_Te`!B)PZa63H{6(F*P5YX8n{Nu;^qn{YG4c3u)}hxIQqXeDqhA=FV?-YYG)ai3u=C%lnaPdgGY zBM);9Sgj(K6^!2+Tuuud3vy7&z0ptoHcs;_%mHYUR&WC2W(cIC(p7F?ng1p^1U~)u zE*QEfM;o|M8b85&rn#woI2d;k=d}bKFq^>UFc9?$WZA-CqGy>+y@#+qQCJT3Iptyb zKdHEV)Id@u%${YvjR%CFLA+Qax5pZ*)Ws$9gMYMfHH7l6#LXmSSiQrz@fxd369h=+ zQ_=BcKGVct=)8w>ryZvC!XzDw8~ge46Vfa2^~}2qMs?y%OKYfXbn6U>6HX?;yDO}e_C1sMku3gIENfmz^f;F{iS3}Zu1|l zPgH-Z=F#S-!Tu}t&8vTc5?fG!i2$-B7*ff-FJ-vyG_B(W+KQ3F7_;wTs0QJtN{4)GU_&nv*=3^vnMpce7~TMb;g$Lm<}sQa z*GFAo(fVZq`JsFd6Ut0jX^+4KxOt2az={xlI*U?rzy~z`>o$n}-WIDC^N^2>07K$V z|5_z0v2z>s4;G>R&R2vuD|}Q?6V(7m;z7&5;3~SEe+ooO(1l7-nV8KTSMS;0%e(@8 z+U>Hp0DHF=DrdA9xo?3sX7V@4#gKEgA73-+1l_4Op*aT(m*i;}6BAT>Sm)L)u0!BMt&`QvfIoRi7{_GndZfJ+g}vWDF$Y8!Pe{G^6D~ z5~B5S#Ow@)t3dZic`6k~Rq zCdi6Wnylg|n~7$*w40PnMI%{aLRVfIr&ufV=>E8teZOgxSraNXaW{wM*pqK7E29CY z!1Bg zheF2T=;<=s&i*;0&@FL!6PN!%aA_+!*PeN`J#EV}_$I}~3R$Jr)&{2B@KZJ-(;UZe zF}vOU$}T1nkBg7Q*$zlk2)^$G8tQNoR7#{3vXzr8}$grlKIiP{|Pd@ zYwaq)$y74d$5MT`vjF%*I#dZQbTE(weI@FPG>(E<77wq1R(Y@w9)zOm{GX74F}!9@&EZlmsg*e##$Q z-4YAW97u^_Mu?zQ@^~>})5qptTX^8_c_Y}^i2M9tQ^JyZIhz2ulYINu`J9~T>5d`q zMtkA!3$i3KgV0R`?YYar0S7vQYx#ET7se}e^>Jym0ligFKSi$z8#?x`E%<}J!Aiae zx;1hxT13&ky9iH{L1)+LdQ_1@h~{J7*cmNiVF)l0WcZi^?JY-MUyF^zw^+iDM#i4{ zyoJRZq+pZ&@%yDX;jIMtLLO%qq=fC_fRD!?Kw1gD#Q{4!_VWG1*DdJTy;%1UwSWP2 zZ+P=b0r*ix#kb6da6mIAa5TTS+nj?%W!J$M3H3HLob(42`1s(K_;oT^Rbe)rCAHvj zbw7AkRgkTJV1uJJ_aVTwY;<+zDpm66>M&?GYxZ^Dghe@a!50)4o7)sZ4f@z=@_HwZ zzSoGH`e(1BP6~P!{s`8xK4AV?7`7?G2R}+GxUa(nyTM`m*8ZKl)PKB6E;nQj4n7jk zKqvV*{^n_Zj2F7U2ae+0dvWf9n3J=fZ%%p@1MKc>n9?fyVn${Zyk938ew1zRZMGlY zgp~oDs$WwO2H<~+gD)QM%1@0UFCkP5?=_YYz+}WkO0Hx)I4E8K&-O<~P94`Q%pD=X zrtTBqM+a;V{uBf#I}x?0Kb>%fMMH`3g%jwAOKSaH&u47|Lw#7DQdw1I8+B|5Sjc46|ljM`*H44XnzA~-W1qho(}BK ztClQ&1z@DqtrF`$6u*dIG|;|_kc$5t?61MCPzf;t`1s47Yiu}rAsG(7(9YP50+4T@ zz`;ig?=cjDEsdgs|M@6#4XjB(+#p4OIbjUHia}3*#81MCK3YeahEK+5t`Cmr{#@&OlEO;}VnM-ic89~;m~B@zCwMG!vsXx;6V zO}U1LHMd|xb#37I&cI5&?SMhR7ggZmo;MykAb|AVD%;AF2*XV~a0O4t?_CegVW>+kvx~{bcX@4j|$&2C$8Z zU^qiKtMvfMOuc0dLfcUITz;dQ<1SpDBz(Nd83U5TogLw2qpCMQ{M;Q6+Y@_WCw>>v z9YVkCC+t|XrauLkKz8JQE%=ozdYU8NP6Ut1M+|48mn^eNYLYAc#fh2G@N*!-spInZ zE;b?BxsTAVcY00x|43_q;3@D1s!ZXZSh6_U8Mp8*Lp;Vl5(xNT#4i>L3mI>@v`z^Y zN<|AbpIBpt7J7_`_V~WLGH^uv$*>T1ZQ-|AvMyRk%669<0C5g+f+umAbOaGv$m8B6 zW3(}t3_VF*h!+@j=8O#*@@2}?MvjreMKy-=0F^yO2if7>&0*K{d^D6l; zI;jAZPQ==yOXwt}fC1+j-sf6HAkO49JmuX5?eqP&>m0|ZumoIZ@eWrTkiP8jmny>4 zjN;wmH}Hx`MsjgnGlvb-0iSwu9E*MqQVkG6Ww0_6c1*dp4M!b!QEHP5ypR4QC?Kp8 zcu{D|2~330y7EB*8!gR%jvm`s~s=bw! zctuIH+xV*;NWAc}g%6LJuVS~X2&}eB8R&V==(>K-EVC=Vw)Y(lpYE)HWdQ51=@+jO z-T#Cp3DRgekAJ_M!SdyMjy~lJgR$dA|Im0$3mRAgK!#s=27&u+fe{Gk? zK-J%@dfk(G{q0&=13{PUi=1>eFmI`#gJh@yo%yfZKd_1@X^b@u6Bau&gp@8eBu!W$ zxarE|YyNI!qgS_)}9%>P%~w7R(Nuv+hhI4%ZaS<%+<%g1NL`I zk+3J%z`J#dTQYO;G(0AJ=`y$U@$#U_CnAam-xPXUz?^f_S4Q3UR*^6sDM23_d*2|o zeiS?Z_J!|f<1+pEmNBoR-&a>nC-^Omd%(c&pEoV^+NXB@HGR$uw1nNpkg34IjALl) zar8FCct(n@dl0Zzv1|JGA8EY|Q>LHD)Op4H&L}-#{cv+7Li+w>B>#)^&ZO?Mw@MGVJ+m}H+KL0>kRm4>&2V{G*wQAtqlgg3K00z|7EF$x z>mBVr@-GJ6fb;=Thi;@~PzWqsvBjUST4f|7{!>D8s~U8R6BbiuXQ8hVYU?vkx|V?y zN-xAM9tgb-jU}@IEgoF+Di9*dkY^655x?>+IBL1#Cd1xQ7maW|iaAvO|oXM?wmz$KZoJh6CMBcK7 zB|yNH=ErfXO?K~jcp%7URfA2V!?~#L>(PMkL2Wd2Sr}+9Kd$k4`OvCJ1!9VeywrI2 zZ2|i;3r__4zdspt@?YO(Kz@dIkw^DaZ&kR!ye&w#sZtleym~Z>829^sKmk;#!V%CN z0)E0rB|Uczm-}5WJ;0TCy#r((;S}Pw8+$6#@H84cFk~~hdYc2bg z^d|~=?EK?CzbtL9`%yB}wnV?3N72o;qO{*sA*);W_0;d5?RWBTqKgKW^;KCiLO0A4 z>s}Ug0N6V5`{-4Vtu^lRJrW+$5l830aF#vXnXyvQz4!NFzxR)_iaCNzqS`kawYOf} zuJ^n6?{wKqO}*(IYP*?tIOx8{mr^~*4`0};v!|yj!b4DKbRJK=@o}cfxfAA)=^)DY&zxy6j5$kRt z5L>t;gegJNFSso*4&R~;2=DPe&8yBUt}fTeW9@ORL*;J@tqsdGM>8*M%J7JOxB{)jT!k*1j~SRp6w5_>y%qh2R75wq@RIyOYr3v8ua!#YC3%KgtL%Z=1?A z_Bb|~@zt4>vhL1xcR9?YW`^0BZr|TOJ#6k?o|Cv19&D=;m}<6rUcFg(huJNquuOTb z>xYLCs}DH-T;rnyvPV?zSB^+-#U6h$Fjf1}W3BFG#}Lcv(>6OZ66@}Gd8};QKH2%Y z@a*5cIk8P!kiP8e`DuHP5J`rRB_$>!MZ}Va=4(A}eAAkI8SyB88y_Zrm&=%AKkah2nAuh()W#x2P`K8RS6$8G3B4s~r#$LqJwqOc+vmBQr`=zTcn`Qgj9$awu z7NH(EC(hRY`p)$(x+w*KG9^!{{--`hRzW(Bh^>7xEBXaR zzLaaz*fJ3V^(>v{DW+kmmq%t2UQrZNn#2Cg%u{YXTmRJXP+@^P59Y&}^N&6pvwP+{ zB$T0cJVjj#Kel|iKYQWle7}&=NK>}Pi#8*8cR;U-h_OmoCRahB&Mkgmi}_UNUSSNt zh4o2`>^V}3VJj_~xN;9y(}_rm`J`Qv^?;lL$xYDb{`*RJfUvLv;9hh%!#Y__V&y=$fWF^-@iciAP~Yb z#U}GCJ>=m+{;_%SNSShr+OvLC8^bMeImga6qd+G@qCq{pG}qvukLKu?%jU1^DTD%q zoOi#pO*{wWocrN@(vtM5xVAq*D7`{l&Oui>7RNj_*BRmw;AHxd5a>n3uVd@C9@v-> z1=w(_Fuf_`->~c5<|M!6uO3%bwq>0TGdFiWuY$$2FK5}@JJ$2$EK#>JpF%L?gV(-_ zeev<|3y;z7pL`~Qm$$$J>RE3sTtIG-ul-t6c2}c)iMK6z#yh(J}wzy39Uy*%xAR%z^nL!(}A*el*0+Y|^rvaw`3-tIw5;c=<5 zzXl$`ul}%4P!zsw!+a=G)4P?^U)|e&_rS};1ogRWLvp#==*1mM^2%8`$bJ=E!{!^j zQt7crT0_aXa*foOAd!i|+7@Z)mDltu{e1A!ob7j|I}VsdkdHeFQ+hk&vA1|YC1M!^ z2WX%7h+*E9XPD)9&%agjNZPq=+52XHlk*?BE!LXFl9k_8sxDtpJ|5h6-9zDkn%?bN z`=28oMv23CT&8yy{%F^$!fzivdL7dr?3tk0#~~8_Sqpfo@IwC9IF7yzP-J4hWj`=& zwV*HR-<}d)4P(kGZQAbpSY37kC6;D@x#4lSeYwqdBf%(%U<{<(+7-ODxxEroAtJ(v;R&v!*`c+Z{H zZhPiL`FFLlOCnh{GxUCq-pd28-a#~OIN9*P^9Zc4^0undl>Q&hWoPea9(Lu^{=r$s zalXyF*L*7IO%*a;-Su7{7E+IU3N-F0IvaTW7BRBD_U^s@(Q`-MwrDu@Do5J>D;I#s zjIXo&+P3k^o$&H+@o!`RwU;zr;W*;~1wMX%-A}0li_3O^7aRh0D+^fzZ{8JJyOsH5 zWk_wtZ2kJ}+y$RDV+$<_trx!EYdi)jZw}x!u5F-RbCf9ioRl@^%c4j3GGBAjKU%k4 z{Ovm5m-j&YB6xiGqks(|_l+r)tf`maGHJERwQRD>1V7NExDh`-;NM{k!l-KlJ!ET9vVK+Rb%y3HoE% z?xgP@MLY(YGi?ldraBXzSC`(ua17@=;B9-xxy;78tu?)^yx+^?K!5Z zbhe~_q}?NIitnqvt~ss&41m|j|7V5B$lI^irVP2m`YY~d+9c!DD_#}Os(74xkN=#Y zyW`2ebeEE-_s;ZoRK~?FEmr`&wBVmf>X&4oPq; zMgTrG@$Q*RKL{9Uo%H1X6-jJyKko+3uwPe(9V`y;VG=0Uc1f41Xn9@DQ9RRq!X2na z#w|}qbuEC^W~Y#%x`O;1e|+-z?jTHc4UtK(D0iK!#y zAGUftdeHI7)Z5$CXy}_lTZGDSO?Nx1tp!OO^gI&bUB>>$K_yrcAuEUM*B$hh;P-jX zV|nFx@ScJPVrlD0Sk9Qb0gKF)>PP)dK6YJ41r9m|(Cgq)T=%X_#6pp1b>D$lXat7_ z{V9Rm-o5EuX1lZ#(A9BWf}e*)nebPe^BPh;q}NURic24+&JR>}@%<7;o)F4K)vzB$gZrA>w|?c!yv@fkH5yEP72m*f1a zw`th>IC_?UQYt(7EB_Z&Ul|l-xc)7$lt?2TO9_gAAhC3Z zpn#xstWr`^(j^T_he(T{fPgF@-Q8UaOLwO%4e#wab7tQE2WR-81FX;e+}HKX_bEjY zA>>?`Cbw|yb;Yv8g0~V{odo^>Zu6pLq90e?0&kH2;9^t$U~=BkRqFqK2XAI*)b`T4 z>`gj>L#f&`RfuP#AnHfml!iH5?&?L(_BZrg?@lg_MLYWWB&oGwa!Av@-`7MdYc%T1 zbGm9Ug@cW@;;Coh2QyDFkfhITXAigI`q!fcPzQBW#tO@$nVw)Gk%jkamuW`!Rb0jX zBHgH|jsj2e?|F==r+!nTJ9?`9Y~}wxC+?dX&rc-CivGe^&&t#p`NUpLMLpCHJ~MG$ zr7c?){c4ANr=6pGb*U3Q72X8i!ghYFV(UNkeh~Q^4G5tcBwQvZ6N3SM2j)uvdP*hZ zby;wb+Zl09;USo&?1anwUe!RShPFNNW#N;e7PnXN-v+?Kh6j#S{`+WKIWPvGv|k-R z!v>F#mVeFfWGBc2bs?`KjIwpy9B)oq=`_-JV}m0AW*TZoOONC0=erK&TVk>XVw+f|A}%|Vr`(p?y?u?+3f)p zA@NpHcvk)F>iBhq=ZR>6)_P75QR3z5l;u3ad9djH|=0rPVFS`4~%G_vZ-sP2%>-4+x;U*y*PK(7Y3#9qaj} zT|{x5Vn2J4v|FMiVNiF*EVh}$`c~2}l-#WBANHbV50NFo1opTj=jZBa?w{#y2c_&Q zG=I=e4P~Vl0ol`@{q6Z+u4@O)tnKj{RV&#yUoYxDV;oHRkI#OnB z9U+jnP~EA^(!s;&hq_{9ghMFL|IZW(;M8w*+f{P}DPWZ;UlivB= z#H@M=Ng)hv>~}4>4*gsn4v)Og$B|P>@^q%&MM1v>}iQ0vL)O+hY zJ5z!5hOnAkbrbl0-LbUp*%kW9*ZOoIl;uVRL3RM`XC_Q`wAdd;r{cgL4o_ z5omkgSf75Mt0FX>no;V;W4$S$X=iBBdwaTEPZIx==~4(?$F$#V?TGcHOHw`7eM>CvP%oAdF_>!a`@|2F z4E;qcM~U@aQ@(F#eqUBOchUB~dX@QdBb(OJz z`F3&Rd^bZN0rhyX$*f|;hD9Kbgw<2ahF!d&+P#{QD5(ago38^BrxZ!SwPxLSZARA& z>BK5>S<|rawGkM{gOZmM&#HjAW^A#`8<%+C^9qMnq0i-UkydWUo5|>%(!E)wT1O!1 z;Y07$-oD1MsX)>87a#n4J`tHpj8oH+m$q?b{z!P3Cf>LmYE2}7w541zGJ7{lY`Xe+ zVYM@rh(Q=3Gx_$?=Rv3CK=~%dw>p7Ss*Us)!t;fK{Knlq&6KZ-eG|%pV|Sov>Pz{d z9@gj?GbJV?J|?=Hkjs-)xbr%Z^FAhRCurA*s6Xm?LMP$*>(c)BGfIslo)>AyjBlsx z%MkH8hK|v!2_=(U+JzOrjg*kRyp8rsjC#$!XK(6feNM+^p6UW`rBTdG>6LAeUj1VFPP{~rHbt5 zLwE9X*F>JF+pZ<*2z_u~leioZQ%$$?28W^0dc4(8sI-x<1HFqo<#X!deEa~pUR1d! zJdxvd`Vu{D!&GzJ9dk5+20vRRkC7shtN5`Kbpo%+G>~O1@#ismzNOCN` zox2k9IvHQou5&LzTo0sHB4?fJ=!nD-f68s#GHNYyE<25s0i3oQ;0XW(Y^ZMif~*SN zPK5zO<$m2xX${ZERexLyn87gUDL6I!^62%;z+lE1`cNmG`L|u$p+0j2Wl80=f`Xymo#HdaESLCZ40wBFV@#m)k&S_}43uHqyeIV=C^888wkr z+RLqiy5|dFFVF%k@}fZQ@DF&B5Bnf3#DduVZQhsw^yQftah%FANsm7Z8e83m zeGYREji_3V>O5NaT?`fmSB>h7d$i~l$u`n4&*x;^TItDTflfc^1iR{9JTH6CSEypl zX|Th`p;cN~*R>h;KxHejz4_@Js=J6ds!WN|qq*VQ^^>zZ^S+R`yN;41xJ^6eyzKS5 zPE5rUxhkD(aL*%I=No;4J=Z8Gv^dW`IHj2w|bev&qGDzctto2{hod=x~IP-eM~9b9N4)jNgkh_rV^Bb1_51Ey8|?CjD;VzMCb zq!Zp6I0Pt_27dFNb?HwZW!UVxd!MKaH$$>F(Z+&U;)7|!2Nu&`hShZhl*|r4m1AcO zrEy*JtwtMoRFiCt8v8F60#~)`J(ZN6j7CXRq$fVqY5}4qJFlIWZL#sldN^6LafMca z_DP}3_Qk*e5e@%T!`ad^v8#i?$75n44OflDyDPuv>hbg1-0wqWE&?d>)bAU)VS6DHDO8hPQVw<<**J~8YDwkm!(+TQc zQe@&MG1ZeaKR;!q{Ce&sWa*EMo4O>9+uV7Tm%Uo5P?J%o7cQ7f>b#^vw<>L?u0hBZ zj_Y=j9sO`yt@T_TvF*?|8;lW)Q1lfX&;t~PxJ=bPN$WIc8KR4?; zVpK=jhtpa0s*gAnl6Y*aLs9F12+Dcp%wY8_Jlw%+4nF9)KbN%o=gFLJ)I4}X7tqc- zE+r2aGxmK>xhW?kCDBOL0prr#5JMj@*BNQdqId17Pb%f6aM0b{26JzKf}1|BU(%$>xfOI;P(*`OF0%ptNfA0Nkew|P3Jnq>vf)lv7?aPgBG5PQhD%nsQ*Q95s#WNH+A^jhtI*VF7|G@xW)f0>36L_6U`&CD zORd>92wJet#-XRpYd^!m;YH%6JkETSZ<$JZko5pkdf%Vg&aw_)+pnJUI}*r_SJPgZ zDUhB-lV^mm(tf9~#3SNj8LEGd+1U1YeW!N+9Q}8+a#bv+dn@Qwd-(n5r9-R~P+cpS z_nhyg*5%D*X4+Gx(6d54w^}}k3gnXEf`rhF^<`2xPBp=1b9Kxg|0BDbbDTNjF;{ly zp7^W3zL{dDJ~l%Oib}SgIgmDt9F8g(#KbR`x>;{$F1Nd-S<1zHIJC*;FbDA+%+3$H z<*Y_HIOX)$&T<_Q)h{xxGT z&elTpT#ZZWte+n-*UKf$%ah5ZonHleE+=QuEvS*-<*s>Bzx;!75G%5YS3X`aDHit^ zjBd5}UNuTYC@$UiSOT`(IZ7V-^`CT+4Bm(BUb}V2)e3F+6=Yq@`vA z-h-xlmGg|`nv^4S z%e;+sa1u6w-vrH{`2$ad!veW}zgs|<4&2oE``&9IeNuNRdxHA;0?Vy2D`KervR!QK zG;-~R6a)ZsWf5g7KotWL7i0oXkSJ0Ir3xW0-3(SUhnKhCvgkhw@W^mMAexsK(Ne~a zPKX#k`?|x}%SZ|CW;e+1VARvBv=-mq+d|ybw+p^|HQS0{z-_FJICmHnHoa@fbRG#@ zHCcQ&qAs4W0Kv3=8J7pN;c!fCB9WClwd)yC=w-S*QS5%U#@7 z*f9A-v7O1XpJ)6Di+pZezY=YPhMs?2Z@F(C>hvbj*((a%Qu5%yc$qaU<+L2iO!WEV zewlvb^HYss;X8_%tn2RgbNrgO?P$;&KfR+tMG&rF=I5xUz@i)8*Z6njMvaj?lxBr( z#N6zmJW#|Z5JPEWIgvBo>*lbZ zwYtVEqkHQMGcPj@dCmdUDR1yF_`f+Ij}2!XJG0ezT(8db)W}Zzo~k2~pUyk!y@Jiq z3Dr%-7MLBAy1hea!mIpQuo!aj9Bn~3~SREV9+7`P$#(0W+&=J+g zeH?n0CfL`OMuR@FKn-(u{#+p{{95v9DWO#zyCkjF8J)$17wv%ce6#CJMCp|1^C zBRdL66n139j-^*qq)Qgi~fr#IpSIPq={6A1ee)-DC()taX&5COpXV)c~BCs z2r4rPPQ3>+uGMr=O~}25%STBbs>kGoi?&1+%-R8og_jO!!D;O@46JOGyR3zIbADq zje|M%gZy2hTv|ewZ1+Qgv{w3DLxk?zV$W-+|6QSmqZw4o+qz> ztzb=UAC>-qOICV~`!!gd5iD3c`F>}i-Yxh#pz|}g>iH{L!L{wleRB{KkQTqzp?Fvy z8671}fX-1KE6qgjI8;e!JapagM>@w_y%+HyE!5t3W)+u_# zN4Uc33L1q9HZ=Fc*$yFW3@R@!fiKR-hiI~Ygq!cSOf5J0gSY32HNvw8)1R!FPl~oQ zmrw2|c~H|oW#ssa|31DE_p@vFQdZKe>(Z_xqlc1A7?)Vkt%CN>4>SiT+d+x#+7!>RT!QhdK{7gJV zOgD;@6fV1Xc-s@Zl2i+VzE^|;ihDIS2Vs{EL9EG1tV(L->zR$HU$4b_fG*0ESV910 zj_-aVU0jc5(bULmSITPjO)-T>XwjsrZ_o1}=A zo5E-D4d;ToB1xXXW6`Y`A$3bPmq`u~u5yf6m$vHYezv1DJ{iwaoxf-wl3zQdjsmRs zq1nkhS-N-=9>opO00k%Aibqx%zNOK-m%4!wsx(gc;91Db^971#b z%d`h?{hy)nuh<-0s0(~;hXSKZ{iK(w+|p_AE)H@db?HnUw-`Q|$E^1B*4N_SX=yAc zqVJh1)7W1(CELU@kQGY4MRF(zi7A8tZvHMNDY!A5zVMouXF!1S&bBg+ovy~xsX1Vn z2R2d0?Mtm*~UJGh>Fv!HDMurJXI~aP~`zfS||R0mkCVOW=6@4rT~(KWN}qjPf2Z zr!r-V_0M_p)I&YaF(TrLD=8*U{&E0WAxWj#z#B1KqPiZVP&ddYVzZI}2B&7T#W*Wf z#ez_b9DK0+HaqvY-k%buu8I4B>erPJ2l*)K7*ge78oQ^wHxgEPl}Adkn^ z{$Ybf2B;02KM~)dJDhQL6HT2i-^Md2-{LU%^RBKJ#vtkasbE+oa#d`92d;dx6b)NV zcyvCpwTP}OiTSiMj2v8S4XR>;{ZNYjY&0a7w@xQ==zw-yYD=0G3pgxGGPm!g*O0}x zbk1YyuUi|uMPjcXGRk5@5$E{Yz66#g+3hS2>MBjja7si{b5Dj)ngkJju@Yj5__uC} z;mx2o*8b6YnuxqrJaa{QcYqV$Jet9bIA|0u2Bez@Bq+JS-f zpr!`oBVW=Hl`5B;!fpU!T?ukw1{CbD6=QLt*bk7>dY^SKj>rpvSJ!#!=Rb(FaFp1i z?!Q{{rEqByFX`F%?CAHYCeGcTsn<(U75&bGKR8zJ=l>4B)w}zEJQ*C`KwN{M=j>{i z?tTB^+7h|-$A__FdkogGMErC1mcH!L|vx& zIPF@3<|eXQv1MfN6=AYNaffmTs+p3+qDoPd(B(Rb*s^U?1;jD#7#ZkuIL#Bu=oZ_zb;!el0J zE$#ZzJntm%Z}rs?22TOffJxc9IHN%6&h=?yG-_B$Ct}xMCAR+t1l~sY0rx~HnY=Ce z2vAKd0LQQEz@d@btl(=o#YdnxLOW) zqX2?XSQZGeb^ixUvvG~iLuFr599Sbdta0W1dn# zpy`}6d^$#2Vho^UA)b$Rc?8?NC>e@3tNvpN~Qx~D4# zdo>h#PE+kCYTU4?<1~z`?#22^@H;Wb z#z=s@>V7v}as6o_@dW82uI5;cc8=FmS5&CU?mP|L1| z901j}+kKH}YAP@Zw*-m&^vH3G~r#6yqcQKOA z&Oe!(!zqR^C7JoHX6nYLIQHFzIQF8pg=PzG{T$ZEv|9f#b*(4bu%GxbLuER!tWPFx;LZZ0ooYeQ%0*a?!lmemqbX`j>ss230|L4|f!cxbI0$pyS-Un~5-$ zNE8Lh0QpVk4r|yvoarg}UDi|Yu{{Y8Pky@2FgXcO7$ZX2ozEQYYaF_cX&Q0|mGd_U zCB|y2Txqh4CDVv#i8L?yqB%c5H?iBRl)Vak%T7cQY**M8xx*&hOuOC?ibZ1qt8H2A z6AVjXe|oo+!OA>mHjFxok|Ul$B~{4~>lyI=%&FdY9YZFU+xS&nqfwK)Np&6b^$X?T zdyOg7$$hzqZoYZ*6ef%ybtvm(RB(gM%?s0H!@QBp*h2h|Z?BwfhI~aR^VjLeV0v~|@ki7IdX7zj_XilkdNr-4*} zGy)_D8v{Z0RwYf1qb0z0Uk(hCQ-_^Q_Q1zqiA5|v^uFPI4f7P3)ZuP7APAr+i}y(l zFjES-?&unTaC}x0fTwO;*#L;A0mP%T0^l?uk;iEII^AaofFE##yy{ zv>Am6QO3a@`ZReE{nOaXC{5A%0SJwBY6g|eqB7rC#6NiL21UBj1!YU zQKPg)UfS&USiZ@zv%GS#wN_L!OE#DveLUE>H#E61JUVmYP}rYw%~X57hI_p22&Rn2 zIy$z4i<8DGY-phW<9kAiBp?Z}!mZq+#?5|@Un<7n;$pkobt7vv;)1ZuveDFSW_SQ# zwsEsAlk!~pf5Px4U~@fNkBIPs5V$dswuo*P7I%!?Cm4h?r(@`4nh*z8r2j%Fi-N0j zGod{6!kp@QknCk@7iV03TPP-t8mKjJYIx38hJRY;MY_`UMD{m}{WA@0O)>x7P^Jk3 z0AZ#F=4j7WPxfas}6CXqV)mkcRdYvBp_@ZTvIakE@>M=;kGe1AE zX_?|vH!f26Ec(7E1_yYzOBsC6nt3*I+z;RWUW3EZBxqh*FTpLt+d^}|!F@R35&cSo zEe$xZ7N=fOZAMB5oN9)rh?lHt z24ve~J>3#S6lSHnOy~hRHsPDxC#vS@-$r6$Cu3TG#Zv>^>b!0&i$bCI`4M01Liz$} zAvk=FkS8D~#V!g4f*OvAAYe_SGsM{gK?F9!vmfHG4_b*qC=$#1t5v3(TK)2&9~5`R zyJRyqItycrb{#w`$O{kK9-%HiX@~HSt~2~5vKv{W#FcF#pg!#I?&I@bcBzbFI9~bc zT=e4WljanmAKDJaAD_!igQy8x;C~mgpA!e#j_e%};#Ll_UL-8&Xts5iPIw}%2OJ;h zX4hEYFv^EKn!knol!KC=(dYls#8G=H7DA8fmBJwXj`E)ctSt~e`(Z#tX#e`O=)AbG zLp(13a>vv~U8G&5?hD6B&I3qXfOJ55z%W^1xi!N+TgW?Q*$7&}{_%JCiiit>!T9>u z6}AtR;viyQ?uqg1Sot@~QTqT=hKJn#Wb5P$)vnasFCD3#XYp3tRs&^l@QDnMQl^X!XUP#mhTr?!7l+(}u_*v5Jis zrprf-Dp;FI#umTrVV7%uqR3L~ZXBfb$NNm>_ePwk7+<<{6HX##xpv}u~HFR)M3`gr)Ky2--IlgZ-D)ioSP>SsqV`sNG3 z^`mC0<&5SbhOhER9!Px*B!0}V>UVoh%)w9%Z|2m=e56ZI$gIG&C4*~Z+nXfy2JlM= z)&{vLK0IxZ24N7g zx+E0}Mo0=s`X}b$Tx%nbc8FpsN0NT4&77{d92NMnOFY6QJY^ zz+WHC@V&4STlk1Ix-j65 zzt&%D6}a|tS0aHZ#bO^wbbN6@&W!;>R_f{A?M#1ogU$G_`w8%CVEqq_`uunvR8&km zb~3F0c5EdSuDYimX3g*1ypW)!=186PVdNQ60SM)r0@*C9zd==*N$tk>&olGC>(TnH ziv!e63p37xgr8~7+zByhyp7!#u?uudzR$WZN4j>iC&8ZUDy^Z%b-GgANa@5|u(z(q{byc2#7! zSw-o3hp_D0C58K*K9EQH-8oK707?WEK&I0XLaf9g0)`Y~Ac_X+c_;b1*7?mvLhk<;Hl&+7` zac!~)7NcRrVt`AQ6^m{RwmSb`RISiwo&h zk>m2ywC4`1Jw->`DTlRq6EnU$Bd%!X9Sr_x0LuPR_{{i0Zy{BhAp+yH$&?vCS#k6-JLZ6eAE zVJ5<|;m7`M@CQ<$^y#<$NntNK2;R;dC{71)gsp2qn|r9cL|Ng1J4grD$3qoP?*$+Q zOzRbP`>rk+-A~ZMW|`;G5jY&cD1X^z13#37Hhbl}6KY-K`R4(vS@T_*PywFg0+9Gt zA?b60e63@&+NF}#Zx+1N){(6Kh1iT?>N|kr>gjmQ9tS;;XM}L-N&ib=Dz`&TfOM%h zfCVcf;H?`X^}dSi8wU9@u(u>{$-);v1q(7A7}OpsQH~IsnSs>1SwScfllOs)Qcv8; zRxwS_S>xFffkA_J_2RwIG4_IOq-nHE&<_(9q&i}RH|h|~R;b?~!RH%f8|Z#WKNv3j z9n(_R1M!|lYy0g~COd2Tb?%NFTH>C6-ziZ_T^+PmI4sK6f;3KsVFLVn%$VUl;!Qw% zWby|b)cM#fWrkwI>JbL2yHBH`7FU2XD&9T?Ihp);m|-SiuN_t|;O8KqX$qUlaN%Zv zk6ZtbgAL(g|2&ONg$hLJM0^v<9P#fazWo}E1)^o<PdzJ5D2D9kqJ}*gR5YzwhBK59B zRk3OULXOE>Lr{+(GDkG*u0KQ!yS}fI$@lBp7ST|io5ITr^WF!vC%?#^AqG-~XSxYT zuON?|>ibh>t?7isGfgV*ma@?X+{a&D&+r^PFaN2I|jmdN?LL(%g%6CMjz;o6;F8@H|yIFTglrlS3nq}lPlq9LRUWlb}ZUdWY zW+kIpI^bkES_c`| zT+8q$q1LNW#MY4Nj!RCLggh=3pD& z_+7_d-fzi>OwTg*`Algk77$kB+|@=^!DGFkFvUgMM49hvh4J&3)>{Q=lu)X^1t7eu zSh2e!g4#evb+&WHviAdmgvVObh&Qci@pMjm-vWtB!Dl$&@F~7pMToQQL2^F0BPzrg zIRuHkmG6Y9M`fL_zOSb(Tu*b3V&!7KfHHxbW1Ig)_xn{tW#qqifTjh%I$2}eZl7*OT|78|AA#kgYsPc-mVA-lBa4vRW zSiP{|8gQqbDu^L6?T9D=*UGqg!^l^$948QPCiix&(Wq$!jC+4>Z%!Md#xew~;^GxT zI6s~>fH8A+k)+$~JWj7yL@spgp}TaGgB@w%+SZr2W_fLqlby2atlzSTr8%$tmLgsId$u4)(6JdS@-DUr9C@PPQX#+B$dS+vH2mDKAaX%mH`;GhAE(S(%s2 zx=dhjX(KXS^q~#I4RS!h*@3U*IPB-8avsOcgh8h6@mjc_u+PQjr2Z`=g{hBTmrii$D86Pq1DT~pI6RZSvQlLV24OfKbAIvoX9^?A$FF_U$~#eg9Cqayjb9(Ryuqfr0UP)5gpzbHO4rQ#-G0~}T|e^f*5l{P zeeN+kd;AYowEJ?3+LujA?8Zr-?jgxC6T=}Z9AldD+Q*5C_))>h)DDORuUy(f-)9a| zhoNk&gOAIf5d$GvB=un-w{e}gbvS&BBif@cpG3yk%GOdWj;CIuNMQr5-d>2K%j`2^Eo9K^1JDb zdeYAG<)M}n$3qLL0dL#!-#$ezfRvJ_{x7{{V_uopS~3UHzR@oHQk=tpX;g z@`3RqQr75-_i0ivyX}d_wAl<1=Wkg7bvJ3#e7BZ=LxTfba4Fx6|~)IIh4!=McAsyTh> z`Y!5p@oL+C`ftE}-VB|dP@K&dhY2EN_i2kgWYTxqulX}QFxZp! zKiWNy=iO*_go1j%SjW(fY#HP4kEBWPkCw791@WBxzq0kl*{bi5x~9NscH{(P_-?FB z#kzbsKyie!2}!*GrUYXCD%BPR+JCyI4%k({RGrEJ@x`l;1OG}1ZN-l{@WL);>zHJ_ zkKT^DbC8~4RY#0qlJ!7pp7_n#!7;wXdm25cbw~EGIByW`?Dn%K1mgjoNf<>Ag{WbS5liFo5pogC5|I#QqB0k6HAGo z<;5*f$RAOi8>XHA5uY0Jbv^3#C~tsbSWiCK5-rBzn#wYJ!AyGZ4aiPa0(*8w?-#r; zRtgC_Sj2_^^nz{W1*&K>K!}rFjZKvO%HKJLzDU3LS^J=|KkdAt{&Y^a;N|i=o9REr zm%>bLtV)vWTWoPzN|8!J(3=qX%~s<1cNE27TS9P~H1u3Q=bi{Bw=enm!lrtDK&-g3 z^<$$}F1F^o=|33Z^#)7e9kL@`R^TR?R@He$=8T=r%%tU8T@ip@9-tHID>UsK@~3*U z<4@K4|(mfdmp)kf&g z(GDflR5M-KRjRzSPivu4V_s<4XTW+ux)6cmMV)A>9OfW>BPg9|&cv^*L;J3wf(j zXpQT;G^UjzyLcynn(V8OcymUE%EaY?R;UmZMHDW|MV4Nw9%=&SfQqgPvgYJ~o`XC=- z1q9jGz-xA(QlP`rT8RP4-%B@Yv$kFD0$v1=7S=TeU}qgv!hO#0wO3{X7cqRZ0uq;; zh<_$aME0iL9eRe~7sK9oz^xu04Cj7K#G*TTL^<>=NZOl}le1Z6cf29~!IY09y&CaT zI^@q6vNk0w@&1LxOeVGwF=U<#{}+GRg!a(tk}DUg7~)sKd(=3 zg`MgVfq@^Oy{rc*-t-`{LR9ICCbjF&`~P8&GUu&* zH0Dt3%~C1K2~<~qZBWmqNrzayU$nB zbP7Rz?Me4Xj&DIt#Ps8{HGoOktP6sf%fUj@U+fAnYwI^A;Xln7iRMaSIIAZ87TA(9@%50Pd-`Did}H=fvvw_|W3O>a>n&Y1Wd+!jw)Q-;cBD>P zK!GMj60@oUeozatL?YdreV+_7kkK6;(rdxO3FKondik2|MP8@FJk&T5Frrm*O!U^6 zNH^%l@Uj`V{upm#sm6&#a_<;8^hD3qOIu@nEqQiP#7;gefyA-T;i~$Tuw903*=c}Z z8$F1hcRi>Nr+*TZrL!MpKWshzt2zS8yB%`Np*m6)+odCm8Poi1f>f^vR&wpK*K}V* z)jCJ^DE4*ua^}8%Y5dthux+q%dxh*TSvbq8a^{Y6x5kmMRC7-)2Cg{d(KQ%O=R9_1 z9lpft-PMy`*2Tl)kEz4KC$%-4bJ@V9hYf%cDYThVHgVs+%NqT!?$~MN;W5brtrx6sW6;K|SeBxUs16s}0O+5Uus}H?kv7kg%nvj%_vvXcH=DhoS$bN^SY0Mv z8#xsYLSo$zE{recnd)$q7xr{hmYi7aw`O3!8M?WgPvkjs@B7N=I^!5B zvVkzAGIaPwARqF?ZgNlDX11n*z`9NCc%CyG(o21UryY=lACo2c-urW}HoG!)K;kJz z#qd8T!x|=LeVpXYiClkB)0-WalP#s{O!r&?tC2};jH7<9_WP$*aV*}){C+uWC6kb0 z&{H$V({NV!trD`2JEwqL4LLqn3V!mVLf{b& z#Dzq>)i~r^47~Ya7Ul=2(>ae4#>a0UFby$knB;N3E4aLJEh9}5S6A2RUg;oN4lhaa z7KZZDKMG6)Gnou$-F(2wB57g7Dro5?`h?rezZj5+7qvWtSt(GOThfP-$O%xA`Ko$e z|MqAp-_Ngqr&(gLP8pDKyLSLgi6E)p4`mmWm0nu*Mx528bT=5w8J?|Mxr(xkBI+i0p`8SfKGtP97wXV zP^j#W3sdd^)A*Xq$l&9qkcHGBj+8d1va{&OKnd=RA1)arU9^z9Dv0-@e>Dm&`-Ram z^s#vs7RO|qE0ls^B1Rn`3o5Xx|!NVUW9!K{ew4trD<2f z^SrQvbNYsSyx8p;SY4H2k-)euqi;YxS*Kg^ZQXNT&|H{6mH(U=i;-BtQdohuI6 z1oorptN}-XGm92Je?i@+Z~DX{`J!C&5XYU`i&yP@OurVPEh4hlQuMSiM1JibfSeU* zvbz@>8^V_f+<-Z)6!&6 zJK{_(Geeeb>(?0s(>%2cAGEa6Gm2@7IngO(=Vgb=b)KS_xE4AFbz zODy;sn4rg9wwcJilp9dYpOPDM9a8s(O}jMAJ|$<~?)#e(h0t`yl&AJvVq+XRA97y7j~G23s2z?ik0}&LJ3)^AP-Q}G*8A7-mbQ}A0Dn(x z7vN}|_LG~|NDK4oqz`TmS_Q!|FSp_MAN+=*s~- zW2%zOK&n+W(t@qycuvm^R7{K*lh`&x>V zjgikK^;Z7Ed4p@uJ9rs*&u4E+1H79$T8uNl(O9RGeUlv`DmN*~Hw z7=0-QJ%A`++p_IHFiT`sWwya$515(+Xc9&-+(#pOMvAh7;$E~yvr0^V9$N&%WihR0 z->C)d)TnMpR2&F-@!Q*B@#jdG0+nE&xXk4b4A{JS%Q+o8GGabb;q`(~5h*cG@|7d~ zub+zDOJs{%%v`OjVAVqp|$pY>f+Cj>Gv2 zqzKH1bV$snGRy|iu%)A`dZ(&$FAM?XYuiV^d^zv9Lu~R6?WRh{^(*R3ap@7efKsm< zRTBfRCHuXxQp9XVkf^mKpwpLa&q_MAOMzx~BknyQlbwXOb(v${;87&NXDd1xwLQ1B zQS_bXah+ui?_P2pt!Fy!oXr0Y3uu4V6@t+cr1%BGq;B?@WU+ldoFo;nY@bV)S|gGq zkCf$ES##UgI(m=3DPqRfWbyNn@yF_l&e9Lw|Fqg-p00$q%?iHneAQ)9SD5yWikhQun~pSh^Tj!#-vqq0Cp4wv?oEI#dgQh4AkP@ zQ(xede|01y7bH%4BELmU4;uoqk?KJ1rR-ouGtTmFP-!S**SA$UT%`ekyRKm>gnhRg z{vJ#~o#oe954u+KMF%@Pu1AE0qN4?O7)YzV%@QB$jhLUA?sr{vlCCKf@=N`WOlK3r zLOuJ*qx(j^uwZco+so6w9wf2iRm!QZN zqHKAm>yt?_+%JBTW81^bpa{%LFp82ep(WMOZ69r6~~I0&|PD?4HAn6v{PrTCnkKxl9y ziiG`ydNV_)#pHR^`1RSW%HaGPW(6$K_*R}v;i0t=+O$(RU73qlr}RdCSBIbCd)dVN zZ4T+qq&=nIAJ8S&#BA2hyNmjA6{10z2ht3K`2PDB$|c;H{K$ldV+ zVx1h{@tbIA&eYle$JSehMcIXIqkw~eNF#`JDoO}QOGvkb(xoCuH^|UPNDYlN($Wpm zT@oW84KvaVHApw#;@R)}WAFX_xwf9qGo(php&CaEY*#C&Wzmp8F7A| zXVsVr5X)k0d?fYmb6-p#0ls!$)Ybag+ou+TiQd+?LwAdGS0kB#qr7hvvuEok0_6xR5EFa7 z{%F{4R}F58EVX+QdH72zHZ}ZIr@@_6SdJ7>XV#S%h4chAwYu9{XAJ8!lA}jWcu;&@A}@ai{F$N@83Mgwv1Q>^9+ZX%NpR$x2--nhm`-p5U=hH zfw7X)bp#}+TmGdlH+FmBj0aT#aqiPPMu%?kspxsvzTeu@ru? zyLUw!`C=E4y8gaVQ@3RYM1aEQ)n-f{e<5xFnMs+yQ#=^o_425)tToVe?KCNc6r8;}} z5$Uv0p@%Wu?I_$c{}tkH>=B-@d9s9{9D;Q!dV7LLFC0dE6^Lm(iHU7qZi1PN09%i8 z_iHPD%!(pqB20yZum|sod4ttQmmXkXrkOcfeXF;sCF?i`BT?bbcp@@f%~@cUwvqdS z@l9c|YtI86p^Fa3OR~@RNiv8tWSW>3lv&v8LQRR_6dECeaREo0C8nM)shF6Q3%^ON zh$rI-oicuAe8wn#d3-M$(nl^Zg5+La>LTD>ir^b@IA9o0;Ca=!M~%rwOh8O06R*jP ze}R&XlzP6G`glT4jNIxSV*wZvh{(4?sY1ZgKZeOX z@=>yCDw}8YlL+Sqq|j`ZRL4*6rnxO*VHkvE<|QOT^6C4wXnN_9nDzAa42v`s?S87bufiA`i6W}2HhcsDo=|7U;W^ zG~O2=v(>H48&oOP%*%GZ-vZDFk8xDecL9;B;eOy&IsGyVe@unU$iuV6)qe7f5u{GyX5RecuNRg7mOUFVXsx24E7>MzekEnW1omz#Pm3T= zu+=B;H0f8bja22)b)0wL5QzwTii|n!A84RQ0wv!&O?1r6a8QWwP;) z2kPqy=jH5$)lJsvv{%6~8DH9weDi9{%6nubm1+{HQS!%0EXm;B`aHcz4g$v1vuMCg zJ3C#DIrSa+INJB#URVH(nN!;{YHa83+L_EZpXmue58p^P1P(#BA8D>-*)Zu-9=?7r9m#HX9f*CHq@YN5TSO`ApOC!>qKA;W~-{uoMDQ09(pd$Ua@ zGT@S1Oq;tlo?+UP5v*<#c8nMY+{$&5?=-&-;C*Tp=I`$o*H0@aayY`C=dw8@#`It; zZhnIY(tJJ`7BmXR?8fTmnE6OdK}-S+rz$@N$~eTc|I#~Qh%OE5avHzMaR7*S?k}}I z-qq|{LrRI{S+TC+yQ%6l)x-fGE0!b++#V#S3Az0km11LuUD+)(3OBvdCe3A=QO|<; z2j*#$>*)$@qM}sTafz!wIPQ85GwX&TSi&BN0t+L)Z=7L=RHONq)a;B{GGEzx$~=z4 zd;%09&fk&A&o`#d0G6?ym;beT`h!Az^!$D1yVDy<`BILdZ%nDe%Xn}(2C*vB_&WXL@CJyGaVzhHW;DVSFJ<^GZWJ3MW(K6Uym#~m#-;hwKy7F>`| zzx(xj7S;0EG3lml9$~Uij}(_R{+f}qA&7k9-v54O;X~~Wn#uV@SK4x;TUKX{oKZ+aUkvl-I_RNmVT_@=$5Nv`E)ZLm~WKI_q$BV&3xwf z=Pk=RsvO6U=}zt@$_a?>RE}YcQ~Ik5h^q4H0v!}y^%#~DE9lZnKHHPP1=eqAEo1;9 zOCD&+0V!^~TkN|CC`1IVgzM6p>dxe$hP_*b_fC=23ep1BZq}_WD=+((KH&n4*i9AY z9lH8f@44v=Pl{Rv$QA&NEFx^9$)C{u2Ive;_r2fJ1|o{Tfke;G#`5TXMSI15Q`Pgd z_dgks$Uol4o2q>dMTC(Q{-8B7N+=v{dW0pQ&WZv;0;Rx@@?j~E*!*D@47^&tNjb~0iGDNgiEHBQ4n2*4Iv zRGhE-4zSjXiYoqP8g7e%y=axBSsU`0QpBo$%tkzpRCi(5uR#!gOdHEE9d`75v~!TR z94Pscc$)o-t5%+pw)^U@(<#$Z-BG1zqa;7MyLWk0&9b2c!oq20W-%Tscd+MUhQDwy z`uoJah>umtha`fyeNzG>Y`2olZ~{>NR@H!3HN<6%eG3a6fY zzgX-8*-{c;@5gE9(Pcub1JHY}_jy~*;+d-P9Tj)jzJ3Li_S_tugJhzoxI?#t+NE)3 zV@9>FdnVQ9k<2f|Jh87kfOpau0K*Aes+PZMW~jt#kj$r?3VOkI5%1_^^vGHI-q$A@ zQKqc;5zdy;B7cg5y1^Ng0cUWVV_&8pKaLe=0AGVzFx1rQ$6wt}iqky*6WJv3tDRRB z7C7v*X>SVB-6EWS_T>b5UNavNRX%s$WwIW{vY6(gur=v_%Va&-e3mRalO9ne+<_66 zLmL+8n~MZn5O0cg`$b-ntmX`kRr!F4(G(J0n`;6}^ZWu?FH`0~J=y7n!w17Eg6 zE#ijBRH!9A?_=ONIF`R5{b{DNsOzymU{DGi^3<#z#**>^Sm0K7D7Pqp#FE7N^WQ2- zeg3y!!LM7$9Ujl@Rwo_O0ZZEf-B)In&{p5w2GK~7LZc>F#oJ<^D%tQ}QEG9oQ*xc0 z1Zxel=q<^oTH~&Zoy~qFw^B$}Q>~N2Qv($}JOZwpUZMv>WZU(~Iy3XT*1;Ap^73d$ zzsH71f{w__qY@R7BpC;Wj!kTL$sZY>yX+%nF$s-;18cDk5JbS5k%@o4#?ltJA;-1s zbv6Z)dCmXKC?j&0Kt}|4tJ$EN`FbbZW!GZ2VvHK?7D~Q~W8m6;jR3RQRg*`W0qz)Mr`K|9Tx0#j zjs3`*!*MyF7e)ocRf7WQG?#wo%>YaD*Ev0O0S`2tZ#A)80A{7;PVC0?wC)b)lv(*p zfTZ9`B23lFR~bkTULpy*tZ-bWd8YB+{&qP0->`VE)0C2&&mOT9Ef90X+#1QIY6To! zb~cZT4;LpIk4=a*YaEviw!4Vv-TDaWF5_c5(AYOYsW(YHtK&c z7JKFIS~C!yDr@+>df;E8`^e3AYtk0>DL8sn)a-Px%+7bd;Q&`>N}FFdrzOrcjTq>t zyD{nd+Rsg%tEHHo^f-=)4cVBc3fvM7gpSYcb*=gZOf>*4;p9g0%DMVY!s6zq|B+5%62 z;$A|njb2~0-ZPu!oD%cl4>r>kT<@CBfhv>H4iFf?X=FN7@RrOa72B>wwDbwfGt$S| zd>X6kiJA5@?Ja z(xco*1W16Y_cD+%kDqG3ul@C{*;w>`-UxjiKJWgBD7pstbT>7InJtVHQJsLE*=;&6 zKsXmN#I2d7a6c~Z2!MldSal20RX|+p;j%yZM<2sMKnG48!j?+hl6>R|;9Vn&WwMjp zy1bMtV#{(ByR|xW4YS2M#=~!;bcZoKi8raFk7HGG^{RJ6H-tjhgd^9^h^C^Dg)%EG z$izTVd(>EhHt6o%YJ(UWwY4Ym2t1x<0gZ=jy)K8oZlwxMoebaR5|xYvaMRnEuo%c| zw@g(iLdrDeW~|LtJ;$PASONg6c3K92voruVz>w`3CKu@S6XT4A?Yv^63W(z&?ho)@ zLiYbW5OviCb2Hvs)4cc6SQ)Ljf#8Q0Zw9?;<2w5%sSzzhPXhSyZ61%1r_I(ZN0o1< z(frBVoUKh}Jen?me;LU_*8-o%(T|^@f!QWZp8>m98%QSY{s!jD3rg`n+evD3RY*cfmFOTPZ>dKgXnC?z@y)X`+?(9K*lhk0T=-8BOc9gb zc_?f5rsd3Y=4W#hk#KI%pq$_L<3}usd;hZII)xXrbG1IM(%rVW#MmQt8cNeztBJkB|LDqBctaNV*&C@&vwa+Lf5CRGu= zRMSC0C@^Cdw})1%M=9WBB3GYqa6i}g)3q+G?Q9dq8YU?hr)G=G`-HWX;VJSM_9rJ~ zrGZ?wL_kn27+V_+NZ$eQ10|{M!$H3wd~}9w;c4Dd-Z#X=`|x49)LhLE(TB}k6mQeHrq@v(&cSfbL*Wn#;g)`T%s->0t7ZSz z2)R!U;txHF)Eq5&81`kED%lE4jl{IkebHi;IN!(_z$=jsR#2mzHPnraNO|Y0zAJc)_HAQC z|2@f8t4SI|Am8)1-6YOFd7#_!{z;QA&MnObtX|mhW-!?JnWn_87X-5a$$kMt$Fq(C2!KJ3W zOO86W9uB{-q$icl5Sb{?k?>9|*{b=prOLcpe`N|3{W^OS4n{<|PwN44{JD*SqnS^e z&((@cS8K1&qz}B)vJk-UqNO`UbFRK=<%v@JQ?AhtTX#%QYh|BEnpkVY6iQd(I4iiC z_d2s|DZ@+Yl+Cpuf?v@!NgJQeuql$7Pv@IIhcrw`niLd-JwHDhos@np?%|)PXTBIskY^}Ij-Q1v)e2SRzp9bo zk+6{D>6%Zxf3`i(tCF?s!zI&Nf97d_ushv0XMb@%w#1?1Lw7n8w>1_ES9@g|*)=9U z9*(3i8~mslrlpfuR%?HFa+&5ll9Fe~FK0rmOWg(2qm@;=vW#b%-g3Q$9XA>uj8XV* z4MTnUQhm-RtCzqqOG$hb9SamhRfWcK=JU7KTpK0$;PW0uC&!b$Xg5Hn3?~jX*lqdA zpJ@d9g3N2_@Y-!?5@^Y3YFk4`e)B0~tt>Mb&%AeClmEGp1}hJePoc{ythxtNYfHo^ zj$|>a?`KLmx;i2IQF`<=4KcOuEK5Sx+BGs69y0oFLrr+yx;g0>&~QSq1-NOxQJ@2^ zfMsXE@8|)|7yd_K&zd6(C!~X_Q<$d1y~bZ7i`f(84o!`nhwpTbRI`3Fe6GS*pte64 z3nw_(t;mwTd7z19YJ1crkfUQ?eHdXg9)sBN|bUqv$Dk53K?frKhxsf7@orQtbF_H>K$qFcx7<3l-w*c)cA@9*VAaA8`z5< zwj%|_aYd~J5vNfi|GHb>@yR>;Pb~d=g_BM>g6+bhXXm9R{G9zXE7wO5HtBA;tNgTu z^6m0g?52V8(R9f4jf_R>Y`qBUp0oh+v>>8w_O=>pNyBKeR^VdYFkG|?o}_Vk!f)HTUxqiMC92w z+ZK-%9oohuG@H|Q^#MVvcS}9IzT90qVJ+DpWomv8MDr(nTorZp6b|l;cq|;aw;T;d zTEuOtd1aH;n zYBRd9t43s{!7p;%Upc!UepN`=`rNf%n8}?t9qmJ!U*cVi3!HqRW>m+(`iQN);8iU? zZOzSjzEi&=w|8AqZAF4+TYzr4P03=jLc-W8w5=~AxbEpsZ%^93j->O}8Q*es#0^aK_m!lkLijI+BvFS2yr@_WC~?+svV1Dji~>b889 zQB0DQyLy$UF~SF5YectpY);x1Iw-mjB2JJJ#M!ClX^t(#mDh_Qf`h103KOpjhr4k% z!zh+1!vpul5|#o?->p)jR4>`vE#qy4YVLgUHb&OCz+?+~qbY0@0~h9apu2P9m66$_ zV^0^V#C5w(2sT1UuU99UMYxBA-cPVcaOOk*4UH-rf68W&WUy%E^Cv0L4`Oy~NnWdl z%Y~@A9j}V(6l?xVE^YBFrx2J|FJg%1$!zJNFbEuC94T9Qashy=qPc?g+|DA-nZ8%E z*5yG2%p$v(>WNb|~QggnS5S*8AsohY(lZNXUsl%(T`|(~jot(03b?*mtiO z=KgLNncr^QH(@iR~>gNM$kqyl3X_1a67A=-&<;`27#A zN`F5l&ai*2fmzvtW9MSaQc==!eD9+cME<$&jb|onT^sc1{3znL%9SB+okAjnT0Zpr z@>9WXE{h67&h_{CCvE4yP`QE=0Pp-eev)mUM4YT=-3zc9`Jt-`CojBC8yFv5<;Imw z404;$6;WBo+^@y@F%mw2p&8f1%o!-mlm&N?ai(}cfUOQ4gmkZ6DCz; z@}FQ|AB@cLpeUZdBIo76U4gOEj@K!bpW+v)_N0~QgZxB3_)9)BzkTGz#(jF_HRdPHGv>pfs_HX6 z#ihe9810R9qJG;<@)5kUDKL6^=v8@@e^qzkzCZ97N5YQ&)4z!ukGN35(P^EFAK^!n z2HZAs|0)^tdc61V(zF@OS$IbHaJ@j6qwA4wO)y+Kaj(yxEC5$3OF|scMeTNZa=bV~ z{Gn~v0;cD&(AA2B<3 zE92$9*Y_=Zwm2)!BT$%kuJeKPr9G)`lM9MwM9O11vbamwXIOd7TTS@GV#pqEq#D?> z)uXB%r=+(*($?Sg_xD$?AcstKveKF8?H*`&x^epn4?mIEVYC+{i1~Gk&@QePB|ZiP z9cK!(vY|!>%bJ|_@Yl2QPhfsQ)cJ!Fog)HNxC@DeH{V21aN`ism1qG2I~gfGnj@yR z9!#XgyUFl**r5}WhIX_6O~EJkbd93pF(0qPQTccN3sWA9KI`f3gddJiUuqW=14il9 z(ea#ph=}(N;GvNOu2=(BAIGF9fjLMgQZ0WOZS}Z^(~s&EtDT))U!ZX+C$Wlyf|QoO zYQi5sA0I2kdc{DyTcafJ9>JFU+jLa{Y)e3t0+tpys@{c&w-q(Eb zVD*~`Ve53>*&3PrnZ#MH;8bSTR#_95%+B1@Y=LLo5J=sLd9j3=+WTXezx+k{HUXcB zFXdIpgR{|9fud=9&zvy9a@{mTpR-kI3V$l|Bt|nK2lA4>_au`61)0wb>>BbwkyrK? zDL8HVrJ})NIa1K>QFbQfaTMLRzQ$2GYV0Ee^bE(J9oh5QXv-Kzmno^eq27 zXn$Yi-F!20J-gk^)9*6_PgF4FtoP`H--)DUY4> zcGC)xrUXl_;nh5;Xgq)uN-P_)TMnJ0f6muaGbw3NOY$xEM0X)W9spvxEs0B=1Gy{J@s;~YWcW%)7k zhGtk~OJz}IjX*%xD1_E6%h>Cmk=W&)UW(hS<;sEDl$LeH(mwLQOdzS7f*& zUNL@jJlSm{vKVOe&0{?fFQ6g6REM@%>X#Au-#;4x%r-S2O8pO!NUO!5_0g{+<$)Y@ z;^mDsrzM{DB3U(R?a73fYBSaAN~I<=%V9Ch5($F%wl$-VQY`bS-p}hTEAy>YxA^UR zWs+C(AeoCPhH-F$vJrQ8=}iNSsRhKF)NT!KB~Slrz@Ep$vak$gs-p2&S^D%^o<{iI zWM_VlJ|#C41RtMwqe!XJoUnNu{0ecL-z0C1{oKupv~C(p$DHvv3CjUdk1@#M;68^M zChAQaEWk;OLdmz~GJnt^;{I*gef2?7bh&+d&^N|FLr9ii$3(B-s_wkO;H?Yj!zn#;F3hv&+{5C zG?jk`If@AF1t=hBVf)iggS(Y;g{uU~M_rD0(<>N$k5QHECibXbNLxr%PFu#*)=XQ> zNzBqi$HQ~}alY4{t$By}2Nj%-ScoZ17j-9WoeWs6?4zURCI2}sXprmLZ4IhBCJT@U z&&xfy7#$<;7^NyXuHKFFt_|-|kzbmXn4=e24i87W9%I@&goa%eRny8)J6v+?U!EKk z82ZvSSZT~i6@-`9{@pIYDVw6UOcU;abK>`M0eSnZ=Ec6ND)V`r^?=7hF)VRYzW$`vIiJnb6C9yf zTAEr`wc8^!Cd64Ge)v0CE3I_NrLDNMF6w$O1i5K))2M(*;_0JF4MgrvnIOrswQtU+ zif?7MQ5l|U1UKTJeos8mg6N!I>1=9wVo#!5n@j-i=i{EQ_{rJ4^W%a?Ly>THoVyb7 zr>_(4@yRhrw~kXQ(RBDu85IO$=Z*nb8b9|F?#k|Cpjw`Kn2L;k+E_aSWK8Mi@L zwzYiS#buQDfG*C+s|c}vcmvQx_{D12+p>H02u6Nm^!T#_uP?B)qnZqaRv0%!g@E9hJv#BxgF1Br=RTI z`qkUscVMd!>7a(*HGD^uwMB%G6%tH*J2|r31vpsrFUx1H`@rPgF^_f>N7mnhAS8=Q^v+b?GG;K0c!VX+e2a8 zyw_^8ypR9C!;=XP5BQ!RZwjW5*vKz9FCy0R{#b~1j*%prcpts(QKb&&s8aZvUmuNH z!qvN&$q@4lR^=K+UcirgRw3H*+1j%|G}TO9%}U211{E%q_Vq;d?VCIzwH4va23JE4 zYym%ndKz;quDl%ou~AV<`2IQqO*G4Nb90{QLbNirb^+|A}=Ucfj!uP2Sp1dO4XAA8|IGi0}t;JZBDv-=-jj&_i7fi?qseZM~1E zl<$`DO86&)X`on*_h=VlP{+rbz$?)hlnw!abi1+FQ4(b%ncn%R$7+9G(~53}oUKnC zT9=oO%-Jv>p|rw%G%cL5a7$L#Wr8WNl|?uD!%zk2yb!uays|Do;N}a~<$S9fwRws9 z3VcJqH2}v;U*W=IQcrL&qyb#_R9QuEaq7~ub)+V}uU9yQuBn=B-3$-o{?SH`yUme) z0WKZS(j~*1{x&F#+cpz)J)WnYj@D17-)Q>@+UnZPuQscMr<1KzZ41ofk+zO&7+$@mW@Z`q2ghD34`+k~s zF;#0l<$yo}-5KhUsp{$JJmqi1Mk_t`1raVOD%_t%tNJwzVLz&f&w)NohzMtqh}C3v zF?{Gi>e6k_mQoX6)HMfA%E75dVEJ!iM>PAF1u>Lkw;%`)X+bh|<>V++GQoyg%Fc80}ov)?9YW71PTyu+Hk| zZHp4|T^9*J^q~wLI(8b**4}BEMO&c|a>Tk~tt#h>LcOZG#9txW_{kcTPd;Iez5fBn zHF_V)+9`2{PPK0Y(Lmp>W zi{_j%3GV|{E8=j8KwMmBBGm>4()hjRubPll<%VHVAB5~n8oX_b+#IO4K)rVp--FX+ z^|)9@-`QEwJuve5{#t^zRNVWT!MkhWJnHp0u67?M1HpYXcNN$n)r>o5Z-N~UzH9;l>~y@74>5(C%CR6>i>kwZrtTS~cEh|F<+Zt|fPiM-TO<`8A-v^nG0IBBWUl9)tfl zy~Z={hvlM2%aQg3t@}$7ftBr=acJY|Qdj3e*4a{QbqbBoX%9GeM72E>3$-`bsg@IO zzu}>S5N6Af5^Ct(cyw`jtBowzXcE!OJ;O*UKDC?EzRH=T3E`s|<3qQVxN&8XAhgT4 zs^CPYuX8(vE-jB$)@Z;+Cy)v6VKojs322hVmWTGTt3{%|GKaTL#1lzessjxs-;H|a zY?=gvLe=nW3iI^dsdY?!hP~;*gv2t}^LN~x1}s0`EWvV8D)XVL!4Q8T8j#OT8GA~Y zIYAFctN8(Me-lKP1i_^Rm+^M>9Y0LpaVh-a2bX(lqUki6XR|l6IwDf8#iiHeWk4A_ zdvAX4ZhP&d$aBLFzH7Xo1Crv{`-^0d6R*UFZQ31rr-rZk^PN!dtz5;ZphkoJhEm6W zEr{o~$lXXz9m7$RbW!+spv?0cEb{yQ_tjIz)Pj+tB-SBx(09a9Vi^hA`b>xGhvO%f zR!^OMe)8ky-DFfSfELwolPM!SRaPln@VgBAWKJ9R0{JorhT6y7Gj+@H;HwI)UEt>N zfzh^1i`anW&^+*TB@Ipky@_lRy z!*r)<%g;N^>6;7G4qk~{55Qx_)A>Y+9pf(9TZa7|Xe-Bjb(#(YHoJsNVOEhDkFQV0 zuJs{YrPUsgVrIRa!Sw5eRGV#hlRKJj#J{%R)_;Nn=yjp0Qr(q2KBpm0mp61yI+l-m zb5j!TpN(4~{FQ|b-4~=g1m#k*4c>Q)?v?8qKDz!dd#py%J&^v6iOW#2Ht`i)b)4T~ zWILSFQ=czBF0GI<*=gK57uozl%gcYz{CnM{TbxK>+#+e!C5A(*mrK(EoW`fg9ixRg za*>le^_k=X5`U_^E_P1N2Tjeab(*DXtL#Q;iO)J876b;#0P#v^8%`BgdY3zC+9qc_kh{(3vkLl4ur= z7!L@kEAV+?0(|?ze%7iO_`p&1_D+5IaPJUzG#o|&{xn@41B90ax0?|$-zG0V z@$z`O;e0&Pr~GJ2g2^kj^#*9G17z3g7I)LOgKgD7ipb67T+QWvcyP4_x?#PLp%n)D z<$I%}d`r>N*V&kNxCpRTWhrSNHLj_sAviP(Ygj7u_ZIA$$PTC4c8kQlCx4gL6HZwc zTy38G&+*{Gp{@C-;M{h$W^H|S6cvx`YQgN&Xs{D~|L*3dpP4(bm7{v7^U;fqr>@&V zEdbflouALU9%ArC{TwY!21&7>byz9LIckaN(WpWsz0VqrNkd33DQMgnp&BL;rb}Zl zp><<&5AYKZ+e?u;8kHMcPkg6FFKK41_C!-AkMzu7N_p3H+L>Au7lt%k3HppPk}P5Cy6=Pu;Z?!# z=kMyZth5Fj_n(=WMn&unjmO>)pS@+OEwt|Em%5B`r|oHa7rap_RzYA_W`g(0>5 zDF&9h7GZ_BX7lv%)ze8lAAm;P>!2Q1c>(_S0|{_;3ye#3W;NyBH%bh5S`R=NoiKE^ zEu#1NY#Mfwmy11V;zKp*vX(+w&GFr?`5*nN^W~-53bs9^`~zC2e(YC%R|lCy6Tp3~ z{8>2-F_25r^SXm$TnN5jIY(|mphwSMoHW}V%^XnC@LMX5k~rObA$vX9VwW1+Kek{t zv=YDFM|0o~Wbr&fi$$vwWT-{5!*Gc-o7_Pg*b3i25K6LgD^%&fT{0 zioMi6ZZmoH$MepBs9mGB5yD6IGtHrM!e6$qJw;ez{!!qVFT4SG`9=x`yX>m@Z;@|^ zpVZ`Qd~=+hM9nIgx{4qT6CzH>h0yJ5)UxFQ60j1|3kB0v+MgKccHxtPhJAXMx;>C+ z9Ef0pY{P)-PI!Pvx@N zM$!8BN`Q2(*4J62+nhwmC-#EEH2YTe@meHwE0mFXfW2UwBwO8R3AaQIx#qM}+o!_A zB_B!W`sbr6R^iR4hLYNAFi`PLS28`SBoogBsbLQG*%PCW2R+x}+^u}MFOI2Z^011T z>C)t;M+@^-PnX?C4V_)V@7snBzC0MBC~_bleldRd$H3l*I@?!^`*iz{sw9q;0t0gZ zaFo^FM;B!JNbePhqUS|zt{0pJB~X_K_7gGkb_K7#lo7`0sYT5d!2KAWh}`iP%uR!C zN*tnnO=vWerj$%;(EzV6R?|nf`v0ttz%csgz!X&3M5&p|;W_T<_h!}&znxDxVOg(T zI#?4c-LzmrFf+wAoeR82YTVSBZYTKr+8`BvDGI5Q0%!GE;@3GM$mD*vSH^F66m-kS zFxxQG6)TQSar|0uKb#vIYl67nPeEN(uz}42qFVw_?%1%i!J^tr7Z-%%&o1{#N1 zmX{9~5!*Xkr+=ByZuM%>IiztJHqdEr>2bCi z0|wp;Kx~c3Dws+n*w-pl*#$ji5`{l%f%JCweQOcWmyzjc-)_;z+I!@|6ixDqfv`&H z`U+KmyuWTKD!VH%cPT96;L4Z_aOqcM$m$kipt)0){%m3Q*c}fi!`3Bvr(~|3naXw4Z5sK{!d@c zbU2OPjz9|4#*EvY)l;iT+W%Fe^q6+KPsFC>$V6K10X?RMt)(hBAi7pb?xvTPwG#Mc zUnHCG7%J+h-zhy{s7S<-@YF#qSmz0wD-Jw=VZeu#)2F1o%My8(Z?3BD_=N;4#&1&@ zDmC4Ktwmx^UCHl`cxK)1`lj9+S$wtNniH(Fv`zTeb?WPW zuHcS3!&~0EBfZ14jXf;A_d1>!xy^{Z^S?>4ro&yYrE{MjNwIGg?mR01ZU<^_0Z@9r z*>zG_1r&O^#027(d(Oh=rXUdqxE|XAAg0EVi%BPb`SG@dZd}h8|5E^`pJk~yzntEw z+j=mfdZbrG5eTL%xGVIKBJigzdk4XBzG7!7FtX?)A#Wb?jQx`E82h_7yaV)gz#F@$FM)! z!x!_h?N*h?CDqqdng=4){^Kp8oEDaoeo7!r3JimZV@})Iu)wTsJ%QAplJHDCvd7nl zH%kW$!a+aA`=UVJjzjBW+)tsN!X)%R87ho{ID}$~O_4$Rx~A6Mv@LwMYOqjbJ?%~n zD&Lx4BguJ0Px@|R;yrYG9Nd(%TlD$$3q_(=M#P_DFtDtRALC+KdkmIVWxI@uk68__ zO;jB=k@{O8;*XnTpu4OOUws$Bq9Va7+Tzez4aNvRYT$W5Ys-9F>t~A2md8eR>IcO;Xjmo;wfz{MWMpqB(@r$Jqi4he`S3pF0p7G}wzy?U<=s z+toz4qv{lj-jnr|-we}WW9x%*J6yGYq#lVJzTm9v#(E{=;1VAWKX#&j>KJ<0Wh0C7 z)1Mzajf=qp>y7!wdozWpeuRIbC2y;k9FBFQcbj*!GWlEjbk2u(Wo?(d>@qf>b+hM? z4jNs4fL$HlRh@N6wfO0^{jUaqOzNryT_8UZ_uV&@sfZ`b{iT^cP?l4ri%}Bhktu!a zE3VTQnQTmSTL%${ zQQQ2MICT#nLfrqLk@x2R9|NK#Y}z zQr>km+Ia~?oU$$RQNPLYK30z7pLT_0K9i?=`xI7k#MBVJ1uV^ZQa-@+3Kaae+uUW! zC^rpAvGljRYE0XPc=4@!sYHWr^q<1ij)c--4Z^8Fg!4Nd8JpU_lhvO;uR#sq3=l#B z(8ZuZDUo3naxXWwA<4&9GPhYx8cAJWTNCZYGx3bz1+3uaF7X#9}9Dzn=5?q%c@rjAXD^0lX3js?^xEUF8;O-pDZnK}bgE z%@zC?*2c7BV>GnBv?oS(&W29`^z7c~e)ibNT9PTUyF_rb;j%c2m%2iUfg1f^jcqoV#RB5FSmV_LLSloN{^s1?HjuKbm&XUr)Tr} z;^Enj3HM5Fo4*mk{~KC9T-TI}S0(y{xc}WJa2rUTlcn3>fVq}h zCf=q?o$vYh3g4@0_125sJ_ox^i_A#vsWLw6VSZoA5uahre=~*v^pfwfmJG(WT@ejtEO9cQS<5_=;E5mpvu{&nZxEBC`HRjut-y4& zX^~^nxTTT3+DC|R+7hZn(fEY;hoC=b8gra0Ap8ek{~9_szO-yHaquTXn zoI>c?RL|c$s`{V*YQw$q&pRK~(7<|-27~$o|77#{)@JF=m(p*)Ig>#2S@*g=Tb2#y zqJ3?SG`ajF?ePAJI$Mi~AR;itQ`t2jSbQT4kqrhZl^;mt1|L!;JB z%xv`7{3M2}TsI78ut08G^OFK=ht02j)-I6!tzWx7)#VS3v88au*J+PGu3i?z=POFT zxL9f$)5lR{MgqZP?LYL69E}x+{iw0`Ghd-rW`BaH<~%bMTVU~sn~LYs=HIbd z{LmcVN2y)=o>E z_6<`pfoF^6*-#?+uR-t&w}jJRHvm63wLG5g@e)tDR&yb8fhevHp(^s%Z=NU!^G|ZS zh*%!8Ks>Zudp+2)>>b|Ay$F}yma(og<({bufp?|@WbN*3@c2jLjDB#n5Yf`fZ{QQT z%mT5{a($$mB>|Qq+2_A{s0h#85PIcC^UW_H4DA(6I4%`!leK4>BjpY&vi^IRG~AL> z1H1b4JVFy?9U8=WtznERmNz_}ZJ5o-Lsj5HzPw8H?^mBOn-6(8?n7=8C?g--G^g~# zBaSgD>HTpF=so|RkiB%rP)BcVyua1eR#fK+EdI=d3Fa3XBG4ul>pU*pKbh_9Q_3dp zi6cTy*S6zK5ETsHUoStAgO!r2WLfUV!-PzxJ+vyaW2(@$Xiyc5UEA>M*QYJDG5YA} zddPwr@vNHQhQ`rjxg*$(KBpZ1wLC-di46<*AKg{BAc_`PcrkaC>rVWM2-C~LbHmX- z5f?m&8Ns>cGpyqoN`gc>p;~QDb#BO}^!F2uF`p|Bco~tcKD#`4ist|OQ`ig3SHZlD zMBGhA%t|x#p*rV76JNL!DNJDf6Ne?gA9B7z9A`xqpJsLQ!L<=xo3nIp-?wU3P>DBKnl_1{sTDoVq7jOFx1u`CuiT6nb^ zH)m&SUkwxY-vh5oo8y(ltP)1%g0|EAH+ih4U53)@`|r9 z32<|j!INf1|TURqvIY2JanCzUjf#N*)m#$3@W*aHq%a)I(UhWA#B_~=^!92#614PfMq@R>iRIwWvg^7 zhe(n8C3nk-|KYIiKsJ4zIE2I8JzZAchNN)r*I4FMLfyP(iqH9Z&Fg19Ocj>&g;S(s zuMEho=OYA!vDO#Z#P;b?BUu9zimJr@BH&d{a?0F1C26AR_#_CBIx2 z>}bw_1Zr#0cj&EpoHOtMe5GM>ac(uPwMVUC)s;CN%V6B>qg2UE#8}vY!ecaIdfCin zfJA7`36%%W%OYaYdCdQc!5%~!k7`}Vi*Pts*$&lNIpn zG5N9IAyo^np*n^jzAVj(+cWoNt_{CGtnc_Q;C=u9d}VtQk7l`q;@q{yW}EkisD#nl z%=W)qdDNM)bW~i!;+_{7(h48xqdshzr&7e z7T5kaHIF*@wGTYwOZ?=_3(^+S>v+Xz9PkJ)bwkAkg#LTS5C7lqnCx^<+HzccEchA8 z!0hRy-32Z!wktY(EL}P;h|Zn5 zc-in&bo&2c@2$h4eAj((7*YWV1w>lJ00yK>N<}&Z=~P5w7`i(|kwyd*B$V!GF{?c$T0I^(}$Fso`fe)@ik%F@NBuNiPqueC=m-0foR94TR{ zPm6eRYisVY)qv$na#ZHhRNR|J4l*y*v2&h2G-fLW6(YoQ@^LF$HWjymUh2Hu)h3q? zA_z8CpA8{cO!K^OF~q4CXP1Tc)z?_joA({QI#2oZU5!%WX<&(9xaYH-IUj7S*l
zG9!Pg}pkQwaLxRAjg3mp9Lo% z7YN`dYVbTkeLn3b&zx~G3j-dWS`3U-K#z{E6?9jw(6IQHo3Fn z1UQ2el=VGXn-vF_xI7qV3I+etCPCqk1D^8}9=P83t5`{X=h57qzwmxTc$Y;lWq>f? zCgt4-Tzw7J=(f@EH zWYXmEy$@FPEL0+mBQ8HJkWIu5r-o3-$ATSA+_R#`ZqNgWw_E#M>)=1EN#>9G@2Q(! z`p0K{N-W~(5-FNtl^rMM7T*^B>2!(NH~Rcg%4*ND>jm_eq3LIEIWh2H{WGfh*vpMQ z^xs5Rs?RZDU#BMUrujI*iV4}kqv!4^{LJKL4tp;_wIu zVnx}snI)Uzirv(fJ3vglUrpcwA;bxY69vDz=8wHV#BUN`V`zd+<0qn@I~hX6*Wz$U z&SWbd%z-`ZyXilr682!@A-Hdd8hDPQ-_>|vH*g@FPS~a3{4s)`Xixu1Y&mdgPQt7` z+Q@K;Gh}fZlW07~fM~E#q+;dalLHZu2V8vZMeK{h`6KsyJV*cFG_a^YgK+K94(^15 zjh7F@@fC*dl#_sEvq<7LAm{k!u3%pYalvowal#7duJudft=<39urg^m_`1tHc?)#H z_$<*hxTm)I^H#K2A@arwkwqKhKL`=-A0hhjuR?SQ(PQ$ggWne16$&{*mH+7H#)|O~ zIK7eG;o{&4z~}ggz^@<_*pm_SFDK*c|6nJjP0g=>Cm~?L3L4vPV5&|B`?%erOzDGM zGKegD(?h?f(C%t*ILnEr-TH5;A+gJzy*iB)m*hu-Z^7q&F@S$BgAEldezHg)^_L+0 zB?x~B!hfp~0+=K++L@H{UH<4LoB$E*seTj&!|KUrAnj!C3XUb&6YQxXTnPB^&&Lwj z^8ZFLd|(5YF#i9z#9t@-FBSinipM4h{<8Id+4?`3qW>QY!ZPqOf#Cr99_yd|{aX*# zrV}M&E?Ftyz7=2-^r(k>TC~=w)NVKx&i1c zCi53rNw&2@hr#sVvu!N?CRSi7>}BAHGT}}J&u-6JV{esv)%1+rbuW-@J0`UIw*a5 zY*D!J(`*A61_~R}vls-^bB(lo+`zbkqMF4UC;%nbGU}EEsnh`QC{~xJW~Rne#Wyly z)g<+j#a#08FdN&@PWqg-JI@Al#SCAYI$y17JS)5c#zb#`ajO95TU8F4)e4;%fCe1p+imn~b-P{pCyd>j z(g6HC)SVz+?x5+xjxmT8U5X|mk$Y+LLVm+?{jNq)i6Tlew)*>r<)6~!Y*S+lJ-0YOg zN!P)+*j#{)eG1-**T^%yoBi(pfdzP~Vijd)W$Y&?`6W?uaPG#bqQu>PLi`(1(i#xJ zb42G!oz9Stfu7Y_{x5NR>0hdft=tmr^Ih)wG9$4KdL#7 zdNKw2meu67bln2D{0zYN`>Ji^GN?-!iLV`%FOU?{ZkoD&0WSpL4wNu)0r*pkg36lf z*l5f465{;5L4U6Ucwc59{-@(7Mg@RyTM5vs>hb$)Q;9T?KMYKbUVSom2COc@!7_jg zUIQ|6>E<#(bmawI5tC_Aavb=Eoicip)E4jnOZOXqOaZ+8+4pY%E>oBdz>2p* zq{eN^R45)X{`CC8`u4SaADDM72Y}N#fLlWi=KsQE+kwT{+8=n1s;qF(_qQfg!!5J{w~dFp9);Kq7v_n}np@4>5%gUt3%xr9?EXPW=R^ z@~Ht%45WJ>%dkE22R}=l8rO$xUbIEf)B3tadIBIM^4q4s4)h!@+uEqdvt{7FZ#P6a zPj7<=z%~=2cn500*}-=+8hkKsrZO%#_XPp7xZ?bvs`2O7e4jA@MiC^a(eRs{s;B*( zglXuLFI--9Y;=)(Dq7np7sXUN8_N061dN;*>)AXwigTR%{3_9Ei<9nztcRL-AW51% zV1L}MaWH_0>H19r0NyCvQk`|fA`z7LyLJIxx1%?aze^%N-;`a{?|T`TZe2dXGtCWtV& zmdR)D>9J9d@A=m-?)UBXv+wS_;UK)iaD8llu@Us-*8rTn4A)Wf1obG)24+2K3cwEU z(^x#(nmYbu*0|{j4nVDXLRoor=McVj-E8plTvB z_-9K}7cpgb%9rkjdUmspVY77*gNoI69J(>o|Qo&;=l4)W9BuEkbK(8x@AGv z^$~>qykiTc9KORh5<42QvkS(J)xB>tvRrkZ+ZTZNA4i}sSLDG8A3;2S{$(ZJ!1fX( zNVCU-vh(5G4OjuMtaA|U7MG$sFIT9gJLvH94KhTf#6?NdI}u5s8J0vN08Oi&1mIlZ zkkp0MjnOg-3TtSeX8?LJ$u8xlETVm`!Hc127_M8s|5g7Gje3Msbz3T_F+pe3-UD=P zh8EK*E1BG9rbgb|twBv@xI^o8)k|Uw(6#cW$VnIc5CU5KAy<{>wp|psJk>x8%shWS z5tu_;oy<2*GQQcBBYvltE9HPX zcXFCKoVqyIp%GA0s+p=ycZc5eiv0l&4CCpth7vRrG!`i9^lkO6m=LKNc|mfj_{JB{ znENu+JEuIjY{ZfQCaaKU@0tB#1xOqA zBRPu&cP1(N0j%igtjkPl+3Tw#ke=W}#LV}%9lrg(BVP-J2S!W`PWO4|Y0uh%V5+nEGLx(^JRfYgqeErsTh$ z@J0}d2PAJ5R5|o5rUi&_i3buv%C7LzaGo!d{KicKRnV6;KAnldYJmVMm(vB5w@jS~ z7=0Dkv`SEVR~A8_E=Mk+-_iQ?Rit(qF&M$!PRQFwNnX@_^DS-uuT0`fdyk8=!?%`C z!}uk;_rP0sqo8IWTa)r~=+0ebz?}&x1G~>kT-qCGyz7}<*=2@6%wANOf+p$tYODw` z=;w8ke>xW@faT6tqQ*@4MepiGh0!nCADr(L<)E*Us4A zxrAjsSma{#MJ(;SA;X#J%%y+|Bh8OV{EwSvb7?D}p0!+vCJ2_j|LAeGtlEWd{W5ws z*g-B0sQvBdykjIs2mEmw_+C!0{a`eg3{MSQuH~hdct_EqjTqRa0J!R{>=t-(+TH|1 z$DfFq=JPza&B>W5#W31gHow`{1ILE$MpPMS5;U<$;9}Qw_t2o~x{Z{@Ow+eg%n}*H zd12LH@krGN`s`MGtZ3l_(Q^4CTH>Ko&0O41B>P{aojKOHrNnFCOMy8#%~8lBi7xsU zhEL4N&H6W*S7CJlT}g1R&re#vX0#eLF4Y_*uf*vXUpgOwtN=Xdwe!h`#RM9MgK%Ba z$`EVj%|qTsj7LyXu!-E*iGJhU=#tp%;AZF~%W{O^`4lj>H(v^Gi|4Qz8od=>@=)%Q zq>?k;*DLos-%WkQ=$mz9GV_@2iOSv>k4Er>)w7Lkyk*M`R4vX`L6c2rLUF%(~ zWZ93AjN*hP{rg@|e^ZhgS36yJx5dk9^1N3O-zb-nukfFn%^@PGFuooRXt8A{zF}RF zdt2Yqf{hQ`A8>KVuIw4Um8GGT>wXdzMw51SL8&_~7+fRz2p>pnWNj6gmKwYlI*FK> zb&fbUl7+zb=!>Fcsq*ovmIcxw6!1%oX8py4aV~Y@JhG9bPEEQpSfId9sB4)rN@A3Rc?%m{@ z!@b5d_qblVH+O_#BDoOqD+ARuoTsU5xOF)T#403LCsCWScP}hT`YzC(>8%1hCwlXU zy9WAo{uU*zB&Ia{KJjtgMlJ&W7maMJ-YH*bSL*KSB)%Wsf0j4=FsFNLN5n9Sc3lzN zwXN)EmA{^ALM*f^FjuqZn2RSn*vUC~9PEAex%282R+3p?49CEq`f=gZ@X$Q3?^$~0 z7MX}(S^rTBTxj9P(e5$*FL)s^WY&FQix6ogNWM?7=Da5*ik_WeWkddOkz|5Kmu*Tz zOu%k$tNwtI=2g8s`h-DcLEyu=k7rp>`e%mTd8Y)n)akY4%8`c|Y(pDYPrb@1Zp1B~ zXT0BZ!uNUhAQJdIBuoIIABL-Xb&{s>1#iuSNb=3=jqkrO;~E7>Ctn(w(zV$eO?z6B zS{AbWL(MLJ*DFcbPtRuW6=HGofv?-{@d0O1^$Yi@d(cjRE=k+1z5 zUTk6GQK3_)i91tsFrV_E^v(L3EWjp*ffcoG2K6q|O)Zf^e09Q8$TD-ieI~1>=*}cx zrNW*LeR}dae$wha^t6&aumXJIP_MDS#&7Zg7;FANw-m7EfhA+vRx96hVxo&=(D0Dt z9Poc1o$#mD)PiV@K&JO;o^f4i+6;jR2}46$41Of3eT#p(E~CeSB_YY3hW4*T-4^>E zTIZM?UDJx{l@Posvy!&lCqBOU>GF*5#Sp+P}Knmz|Y zj5-HnRnkwH`wBEknX6=(YZbjf%9=+tKV_mZs@GjQ7x@YmE8|^_3O3CaaJf|!mM+9a zEW1=A=90`N@G1!|1e}phQ`;`8fOvfEiz3GQl@HOXu`q}Ju(@-uY|A~nZhAgNpD@3- zPq!fr{dnUh$8N6zn!@&x>!o2=B=&k(xOKP6r>&W1rgZ18*2!=38>1?PRYgt)k|enx zk27nea_2FLyWGGK6yKxPMOoNoZ27_WIhnjN9cFb(_c+sdC~|mkf!{!u+1i^p*+j*N zns%eMAVYMAPj)6Y?mo82@MKf^u!wTeWYzjjSBbCg&FqZ(^)TG7i2^ zs+&ozS{l&WEjx+5uzT-N(j?Ze%x?u8z?p8}JI(`nntS^94=`Ejih#?%`I1H)MN5)$ zzMB9zRYDh{`~hbTskpvx0a`4xJ9V)e&(8WDwQapZ+nK`?n&AyMgtb;OId;D`E^YpyOEshWqgo zF3^ThWkZnJJ=4=CdA!VWO}4_Z6_8wifi?wBOzE>B+J_xJj;q2^#m;q1k5=|GUBo%q z6xnn*=G_@o>3+`|7-exsybSU8OE(@ku=|m8k0Wlep>-H^60#eyz?=ckXvUB6-NLo0 zUb|C}wn(|jZtj#iz3g!Ds{V&D{N55Kamrw&$~ZUT;D{Pix;W=7eiUa7{pCV&_e#JI z;p4Nyy_~m-n-303e7kpFC1ivV3{kQ@c}=W_LByxvX2QiQN@SJo%ELm1C*0>!g^?=! zodTLJE~iJ^&)L*gZc9cTgP52k$;R;WXwxhQ!Jh=r3o4L#xqY7-Bm`L018~ki-!ZIyvoOLY;omZ0z`;$jXdxEd)mrJRe2kNJnLETqhB$nTlSU?P0cGds* z_nToIqiCA0-miad*m+6hWEqf=o(&fnJ(}le2o!~Yo3SwCGnjAw&|)4`S8-T;4~QTS zBTbN2*% ziW6{hwP|xGRTPRX6I*hEBJH?hBnJBc$_fCV6ApZk|NB~X8Uw&c?EZU<#9zf`!hpYu z%>XR(mmvHl2!Bvhe+j~0g7B9h{2ih9mudKC(=ZU$6wTUK(yo}De4Ei5aqSod@Oy-$ z(W+t!tHZ^{^oNTUHdAMk{};3xalzm5a;vhY z6Czdse7KSo3&4j@qT*kr8l7y*t@vFeoi1@3tq#(UfeHw4nf?)S@^>KFw?9C#-2V+o zCT$A+8vZ9e}4 zZyWgE;B71jn!f?;a_->Dl>m^b!1Hh5nya^bj@f(j_sjAATrxxnyoTzQY9{Q}J+QBV z@PG6gRMG@O2Yd71GKn1^+z$cFb{QXsxyF)9gw}!-k9rxJ1-J{cv3=DpD4Q9!;0M=a{!0zY{3>=qe(%& zgO4E>xaL1dfmi(d8X+HDkJ^C5B4%;^6SWT({D~Fh-RLx1)``h8{(%!B zlhy_HX^r&rB38C;tA0Qu)jVe%R$6H@V%Rcy8b0`5pEAWW7ja?h@gTH9*JQNPCTOqT zE*vx(GsXM4G^Bxju>(n9{rk67y}*k~2k=s%Jaal){L7%oCRm)c^zYD1%c3bQ(_&!`x-RVtjo!K%Z=us1VE6u@U|A#s0`M}Oy56g< zb2668o?>e7APN*Bn?ftqoEeX<#1fJzetoivl_m90;6?ua11|juqUGD)WAwpIe`8N& z_`MgduZc92P02h(chh9ZIuO{LAaQ{Up{f(b57q!)vy9}**jGmiVs?I30isSK#I+OZ z|3tfxFYBIE|8904to#k}7Al$54Ajr0S%yKRRU^cl41&olAf}1Kdd>d|=LN;mKZxL* z$fmcuA|Lx8nyUFDM)4z2jW8>ozw~XOB3^wXb%sDb-J1R}XzcgC@+^J8 z52NfFqbegERGeVzt?2nuB&I-_2&~ux=$Jczrw9@WFMzbs^rORXK*+EiXNueYEmp2v zHBHUVs?{UJ5T1NZCqtzvrhvzXo6Lu0Bc(?YNRA~|j0D2?00J8ex9$HM+*X|pykbF! z!LGFj%OHaT_YG5~`ESb3RFF>qP@m{=wO58X;~|!1QbE~Qh^J|l@`RNlsVP1vV_`U+ zkSx@PQy8ntbL9OuZEs^i#{n+^P$G6l9tvK@E9^tJUE>x1hIj8ag0TLxN6#N+lfw@L zM+`4plz861$*EhR-c!-ekR5v8;)<9uEi21PY;ndEnnQw5S>K#G8hd$ptorzApFx9F zOh=knRP+DQPJ}9%e#2iuBQs~dXV9i5OnRA|ScmWRUxapKJe89?a|XN;r7$Q*VR`9Z zc1m9xJzzQru7-e<47hgvk{ZSA89|kn<7eQEloNk;OWOP4mm&T(YY-0$VZ0Fm))4XP zX&WA^qlN5$U;+N1ul*ALhQ79b4CLsEDa;s3o{`AbC5!hv$nfJk)mI;zIE&?JQ+SM= z3J091bB8bYadgNlakF#|YL#A>v*Lo#bT*zo@jSe`Qd5;7XB_iJ;?720Z23h%-(^mL zA$K}5fS&(PqMvOCY634{?-ExeZ4l$12Wg9Ah(}jTWu`ntYmcIc%{#cd-5s-ra6H9`VFo1~p!@=aEv3c@71*C$J7_Hevc&Q6y&Nn); zUZZwxsxQ+oVaT9(->Vt|iFjf<``9X;yH8G7Epk3vnk5SVbFS%^!pxGnj7Y{^5y%NC zuzl~q^JAHL!1nKeVqT)fTCW>8oLGxhr%Liq{=|*crl|{VM%NH$EkCO;bcB@glMx|E zl?3;On6B6wgH@Eom^P7{3`;RSx=yopOJJqBe@6^*LKg57e~6xCLrQwgSu+GHn3l}g zL`-`W5{qhm4AcOiPX0r=`XA^Qf*filQcZ$1ASlJx=jLk9rhYhc6V?!LK^gV2pZ%0qKo0&b{XSeSY?33%4 zPcYX0!%+uCc)S3Yz6^yMg4W>xDDtF}PsvvZd3w6bb3@Aa>#M|Ek={DcAVlHfbjeKL zu3k%E+=eWghB2UGlIisUOV3BF3@Ib|IKUR41Cjk3helcjupNi4*^zNB1w4CHEt;3; z5zS`SJ6L-a%f1%N-=vSh5a8Owe> zyzPcrB;3{YxdGVLwC&7|WdK8j`u%*$Dm!+t@!~+=_gMDP6qE0fMz zZ+L1*Y!BPF&hJ9WIkBl<`*RJXY8FxLf zRR7Lb@R)eQG0eGDw6Ffo9Sz1fX=!>Uwtb1mW7eFe*j_`o?jnjnbJ>$kKdIq3or7V} zyRQ)2QWx715OfuZw5>5$U%c+R-1F%Bo8)vR32joJGU-z^l6md0(WK5gfKMsNbGy}T zuy8yj@&3$n(9F(A%P2pyskW7--Kv#3p=Y~&Q$x#ys)jf~kTU!kB;XrXd2VUq;DFUX z1gpOkkZ}X(Z%(lKvKQ~F&!z*eCFq(NcR#QMo0s>Jn4!9{{Ku`_DWRVOa!`uvjbRWa5mGV!!n6*5gk%GINxTKJfBg`xJJuRyE-xwj{!a@mTliTJoqzuYpFIUWNO0 z!;;-K7Ji3zg-aE-s9V&~M4r&$cUQAPLqsd*qphbds{*QJ7R5b+!x{YmNK(Kgk>dP( z(ZCMc8%SVsS2K$=Q3zf_vUqX;|9Y7k?F^6O7}OukpIS49v0CP93l1XP%raHH8oqhA zhPr?@NtedTt=_H<+{+e%TjxH07l^I_(?ivxn682eR2MGPabT*zU_2Bl;J)R&(3#Y) zl`mwqlSZOXFX*WU+V&dX(=hxJLGPzEUYTBQGpY{?aSg)RP86NKw`yC<``z3jLm3=?XJ zkWfom=g_!Sd|PFeD%(ok_Xn|V&^LKD;#*$0=V;XfJ6s$JX%>0Ngjx{30zWYe1qTGb z;0xc?cZ@`h(NvuA0^LhK(E>|x`IjT5KY3-~l4W3Q$yL3|1DkeSG`d&%@_0(5SynFN z-!rX1IPS~`kJWkT7iglg6_Hm*EtxB>f-9-$>L}R`ZKBf(Xy{(a8G!=^*wBmo=t^?kVXVn-xsx1-@RTu)n$U1 zR+2ceB%rkhn90IKF_)Nv)dAz()QeL&sKFJt;lugZ!+h((C8I4HNueSzUQ2zh-EH9f z%&*0>P&3QBAWMY^_VfL7?o+6~z$O7EPv1U?#{^H%`q?1Cgt!}ecrNNNth&f?VXVf! zLM3SoEmqJ;$u{ZK>|xj?bpE3qjUsy1=*zs&?N?7y4Ck3NuwSsa8eLq? zRy-~GC6^J`?rGd|A8j9bV;84e<7hqzt(qVD#P5fsXyyp#K0xI0=GT=@vzfLJ=Z!S$DjgF1OIY zZEY^fyA)+eO-@Gz~WjMsT&U#79i z2_jmm$9cb?bcD5f2pOhe6Kba>qEWcM=78D{IBVwF~#n#Mi$c zyl1!TM?5dQI_~{pcjLQLep+d0$z=W1z(h^{3o?AFzNdT9tUIuqZ>hE4#eGaSX}oZX zv!xL*tepEjrBR#Lx5A$QCw30^U&3zRT)rt8ewIOHU1pM9!=%yBxE;^ad~G%`-dT+Le?~-2J6RAjcxlD)W%Y3>WtHN)*s3rNDr@A?p{{@m`IaahJ4@lx!j; z(Aovp8P}9Hz8^WS6gkU$&t!4SyF2+knZs-pej^~M^EwirPp;`OI zc|itetAcl&b*0oB=4(f|kjs(la%!z+SPv@Z?Oq)4N7X&HH|*k#n1X2M+f>v$ZXnJK z=lIBeMNu5C|6htZMMgBsm!J~=!FW%bkuBUqDol^((blJ;G3&WDlflZ3yQ5UGG{y&O z!_QpS;+>X8-+w&bUF!StNUq}L&l`ienyNgFrywD+Mgs`z&>YCHa>4~r& zD^IkkF(n~;)ph#&%x=89TXL()+%KKE(!w}Nh;5=TiQw}5&}2Ok+rb0xrQ}Tg8prza z;qFL>xk)!AxiL}g=dY+MXkLfk5L9~fwqcDUw?MzjWi7Gv*L{P7VaQOqRk5>7p^@53 zU$CHyHMx!hb$uWhR(Y7~gXy(ZP^Wj=T^s~`JP&Ic;K%C7C-kvUJ)5|ugXUB7Leq+X z?dLuZ{cefy7!m|JZ$5BCe?}vV>kVdy8Z2LUh3m^(n=t8>y>e10}WkTZ)pKL(zXdW~b%TGiB*T*n2~qP{`&u(<1b zEL3Z};f$^Heu?+7xSEfub;eTidMVRq+o=fyRH%;Rtc+L$zre_ zoiXotQcX>6qq-g@8WSZ=3{w&uuP~r4+3mA4!fnIYT~ZU zI#y1p^+pBYO}&S**)>-kk8OrmD+lecwMdBH4HpQME~TB`M8CYC!2u-nz*|jc0`k!U zK{${vH>I0yaPY^BIBb#JD|w-?>pe-N;AC8rH%*Lx@0R?s=~tqH%8Tg)6I**Pe1P-4 z%3)mRFXrMo;G>Sp?*0%$UJQ*cx8^X&8h-d>Jk}095y$OJil1D$X&HJ==P_NV*4YOx zk9^Mqo4W>~PDfjZd(A|6FO@dClB)FMIH-pKdxB{}u+lqggYKi?n&9f>nL?&QtfERH zsm5>-*#uQHn_QP8wXAmY{_&Djp7Tnjt<^7Jx#!N|%%@dWn&Sl2xf+@FUvdd5dmp{A zYZzVsHuC!ue~wPE0*66m&ZPU?@m<_}pB|HH`>SyrTJpd#b-{2-p1VuIrz9TQJ+QEtcQ#Ae#LPrg)8A3 z1Y1luSj8pN5b8%;yxm8dHnY&G>yIB0L^f>$53Po$VWjjMRG-wz#J+?lVxT}zzcox4 z>t>y^%TZs&LUVvo+*jKhHmN0j~qJOObf{l2kW8|+mHQpL>WHfVI)j3 zgJTK$HlUAqdDKwxRNmf7f2|MVa6^2Z1HKWGzqxGJ`~yCkU{_!*#N8j}Ln7zFhznD{ zZiOH4lBWq|0ned0pfLfyf;Y@9NY4V`zehbU!0`JXCZxs#T>gn6Krm7pJwy9taJGqdZ@aSKW6c$k*c4tBBWH z8hd--s*C8&c}gv@tL&1gpgF2luNPdz$XY^_PxhL8?dr~4ljscBbaQ=wpc+y~SFqM+ z`{v2>xWWU&w(yl84hxfc#QmNIWvY5OeSd?`+Yh3=TQcN@JgHEHg}$iB{%fM zX5L|cmde$EZNeL6al|%SLF=RZPihXw<|;oRHac#cg{*IF&wo z1-02;hwr#eNf|nm$X%3{^<6BLz2a{RnO2Zv=!Kc)<5?;ZeesC$UwO+)GO9_Hy4reP zz=@$Wdj~bG@HrB5zMg*Rs-*pre(*+`OsBb$>R4$TO`E<;f>!V-UqfZxqEq)^6Y&c(sa&#n`QlrE*jsx5QJSN_4rblb+4+awI4BXwmd1k?(v4V?26?*z3C*Vlq}a48U&%SnHR0^{@@dr zwS!s%?!G)*U_Leu_VU$dRNeF|tc|ovUX)zN(>8GJiywI4@S+4z9}`0S2M_!!+Q}Ic z{bXc6KxIFRxh&l6(4#_lV&)#M5kDqA^4@&RgsTRgL}DS+0snmj*5^vfU3^F>4Tvk~ zY$m+^2N2>TsC3h6l{?~Oj#dM4>pPNhl4wT4FMQGfH}z5A2t?b!nh)3|BduI$I@0^N zmgIm7s+pDJ#cf}pzaK;b^*cX?R~_a-bLo6Wr+(nBedKjAG=+Ipk4^e(^NKlJ!Ul^? zSVK!TniKD7x>UKPo-G0YcqKQRuy)~H`et*j{fti0ugX`Qe72-2NmpDkH^*w|_$3R3 zr`8wqoOKFxHv=`si|?y9TWb}4e^b%hEw$O^_b8&BlKhFqRFrLucChqM{ulHj-Z;*K5XKb>BZv9#A7Z?0nshi2sZ2xZ(V9;bu zTVvXs!2P;hz4wYiLzPKm7TV8%05)1GJ6N&Sh?`FU;P?7JtbkV;#yzrDtX-H=i$NO< zrnkp}sjGCWeG6J{MNmqc=B1W*-R;#nDO&CN!TF4phxx$?H0VkzO@s6&oqcs4sj!v4 z8U0%4LhE5PHSZ#amf~{7aEcS_QZ(yk4s3YXkiH7#nyw1G?x8|hp3~AUFcZr+slSHy1-Ut8jHejH`p_YzTQGfKyMkgMsHqg}`c2#Q+BYS;C|%ha_` zMIB$>Hr+qSeltWVZ{K7cx9D12TCe%&3~`nXC%{EU*`IPoa~TrrdUUY)OW(&oLW*Ax zEe`;iTYf^#gO@*S@mV8_8ikVuaCRw0y0)OuWrPHhQ@QTFH#o|vJ7iVTSV7E(!lECTx!Dc zTkoM$#dJT1A@~iA8NGIm*KA9!Zfex&O{D7huAo6BEdAN4zJHn5A@1-St==~+&2RTv zdC5q>Jqo~|6!$vx)dX={U#&~*EMvIO&v_t-d3IB`w^P$${ca0YjDlh;y;1kpqSS3#&mgW3AXp`g=Z zwtmSAXYXv!9q$~%f@*g@)KmJ1wUm0d;(@Qh0(L3ncwfTouZnmb6s$|#139*;FZ{+l z;I)sFXpMog)cjrGgo$>E4Tnp=r@_AXm4ANmG?I{|E(OrMD^s3zf0f+@&6TOjxbbN4 zLVih-Bo+1|V}=#DZcMu^cX^Ci0&v&FH@V`VyyCvza%X$}JhoCxcT#c+9Of2l8<5T( z%253QdoSLEd%Fb(n^FZ-?3Zho;(3@C^5#JDtI)io!3iTLY-el~^}sRJtHG?jY){>{e z-{t(Y8zflQ#t<{KRWC8s4vfz-X_51wUhvt3w96kGCa!Pd|7b ze9l?vPvB6BhYggPlL^8OqQ$dSlZ`oa(5}O87>9DEp%w}$#cF_Rj!$Nm@+~sZYmN_L7A-AITJrbXUp=%wSf{=i zA4X<*9@>6Ri(@pz<3*K0bqfu2PG-Y)cP*D0WN5F8I%Y^>*7-Y=z0OnYOWJ2!!(31< z!?~8dU!NvgqG~H{D}SWe9)VY(vJN)J^F4MKLGCmb%drhv>dRIuULDMb?Jf8I`Yh@c z_=eA-i=iG6Z^69XC@N=P@;yJ}3ZEZk5K!#qilwZv{-p z$f;A%8pQB*8;NQqFIxA?eduE5`UsAYm_9NqTp;`Rfwib32kFRnmnaZ#)?V+B+%vS` zuAW=;p)gd_C*cWr{@sSZf0s4i$8s?J?LcmSqYMo4C~&LR@`=|dus=MQYxi-Y2&mjQ z8AKi9Nl4(`uMEzsUjEe1(<>_{(W-)v&mDM5I$JgXS0&RHW$YxA7hK|sb^2`^>pv;R zabz59tXc0aCfla2Zj4(jx6v0nd5Mni)I>?@HwV9~oZ3q;InP*1NlF6~MU=JjCkyHL z<5$M*s+Py?Ox)m+bo_F`BvbPbTfhu#=8sGdM7jxF90=agT%qNj7zy?n%tNL3rdz;{ zcU;CQHyEn~PW;v_j5Uz1E-0pf{nfZe)0JJ2(XH76<9`>qA6tz&R*%EL)QU~FW*cCf zLFjuQ<)~y!%`u>B)?TTDsy#HC*2q$(tgw~1IaK+E%xjqLqr%3RIcjnU>2|@C9Yzof zA`bP*I#FyEb@Uv?>?yiC zxS>WTg>B(h#qX|4Jp*yIptB|C(pdiRt)&MRs(wqhW{9SV2k@E`Lb8)Jl$+ODGm+Bm zbt2Xs39oLZl^D5mGVQ|Pv5HJlC#oSD3wA{oU0m2)TZBuu#9@v($Yh;V;qa;?3V4DD z#(D8K9aI+~jim)2pi%iCyZD`;;O!MgN9r69$3_Ro9!w+(!lwCz}2F-U= z>{RBo#JU@`H+6c(1YiD$ZZ=Z#-l()5{6$dE{a{!5>&fE0F3?^{RfG_b_+bEI)$EW# z4U{V#Nc=>93VnR#HAwv6D}Bw*R^x$>-Zb*rKPFNBA)J2IQT6_~qP*WF(e_Q=W0c85 zviHjd$6vWSI8^z}-vdILOBjXb@<$l3aC zL#+fY4F`NZ5Lx_46QFg3Bv2qyBrsNw?PDao;NaBkmacwZR+`~{(H6!3A6Ni`O6@of z7st4QmHuoOd>RcQ>u0j?u>n_HSntsZIm~j^jmw9*cK#s8!h}qsJ;jHRv&Q9(bq`=(It#eF%w* znOK3RpiwOcw?O^jK=tjlKN<-JQ|V4~gW)hRd#rp|Q^ zfGh;}T`~r3c9>T6)!WXFV?mgO`Gav40yg25;-9w|s_c2@F$@7&Gxb^EjD?pufeyqe{ ze68VRl@mTu-D}hVDiHHb^!%^FOD($5V;-PEhM@tNq4sf&zrla-Tfa2YWo<|+=BBKG zSsSU&wwTSx{lPpfr5L;+-?NjI#*4G!+Yt*N^1O4E3Ax{T_8l|hsTV&s&f#(#roBX94Q@| z0)5$E;dHF@h$k1)ry91T+6(4`W|a)DrbEsH_A<-6Vd+4R@5IZmViU`DHJM+KJQ4@G ztt1d_5|$)E`mcSN-9+yLzy!8F;HWy#BI5694iE$cu$}nyANY61tx;}F9H$i3+)f6K z8N@A$`XBdul13yAq((yqJr%T>i*5hYQv&N$$SIb(Y%PxOzD7$ z(#&M&yf?b$1S`#NleRkz41FT#4;%8WE59$IzBw6)jefJqKZW${R zccCmWvVADoX=_?$_%&42rsMj!{ zkaQ&q>fsmkN~H;h9zg<>{I1@fqaoIf-JZV_t8~a>T4CQASg)1eCiWQfA_RKLMu|9E z`FQg@5O=19umCn>;n2q`NipbqB+>9(L-DW3mSOD|ctZoZnzcAiqotITUV~$f zt<;<}$Q*ycA`g;h=WRm?Mw_6#U2cogj<~*^0vs@}jZuDs)=-Ib5N28ah~DjJ^xgSc zReN#XHCFEx&y{gSvFS$m<)N25bIiXx!r|{s^O-gV^AkvDHxR2@BzR5>zcSymE9Jq* zE72vuX{qVqEn*L(h3aR3-WeSgW?toERLvec^Y2E21VP3qSu^k1Jfksik z8nfNbe6hLY_<5+vsVf=nK;j8ysVLEBrXe&4v7RZ=dK-D2_hAqbxodN= z*4S7L(G>=JnH;UUvB_MyNQRgVBG^T}--RzIn(F|got|%BA@qin?k4iKfrbb#ZvKX> zLCFi*;kHiUsR9s30j~Cl^_i}@?aa5QjXhMqYqFsD6w)BS`{&F1e6a7;T@SEd80J7% zKhsO$jIh*aspML0MEQE9pEp?pSY;4zVn4dBKibLFq|MhYNiz2uGVk;(ysf;PL&S9V z%glER>+f$&P<@F3e@a};%8jRvyLV9l*}@$MVu}1vj;CMKyt?8i%InbuJFx9p!BQvv zioD#(dV0BMDuucwT^pkoO4P%kx&Z8MsJqYnm9Edn2AN7#gX4CGeVxK4n<=Z~ZlOi> z2i7{c=4RO7y7Tk#C(KkOPufPV1%?z9J0&aZq{cRomQnu^p}@Y!zh3X1Q>YUDvSA?N z;i!Z1gr&4l7&kaw(&b5&kif8kc+EoD3-QGGridfKTAr3vwU0bD-H|Ep5Nt4c00xda zX@%htJ~(Ui8Wu~I^l96GCXVR_tc}f*Y|xt6I6nn>hbC>K8~t^orF$j& zU?}tgYO^jL3agfIxTE|rAC0=R+7#e%R~ewar!9$xSFYdNoTp6B^IN35V?QQ!m>a`=Hg?q~`f672ax`vLz# zKW}piFejjE93)XFVjr=a*~AO2=i6JIrjK{1v$+4q$t6g$ygU4Bp1wsm_LfeE?bb&~ zm)N#aPC|uEFij*~uKtLAkcxlQ4Yoy8{*XxFC=jJMTqgv^N^>L3i?^z$M5}pq7`C@ zOhlm5b8`erC|k?17J8OnW7Y1%UKc}hf!cp|2JR1jpfuQWep_Udzzs3gtNkbb-5 zRXt#iReyY!a3v~|T>1)#a{Yrv*0yg3?}ch>r%1hmemv$w!Sd>5L#TGy{_?L}HZ9(H z{_B+ZHDJ{r*gsVHyPXDK6Vcal%IEQAvAbEF({wX-Lzb)3xj=dCa_}1IFf?tP?Wdpc zE=3yj%&Fhhv%;m@86Kt0wLGJ>=i32VN*E^Q5I@nnW}=y}FQ(i@WxwItG7?Q*7A)s8 zb$V{LrW?VIyEG5vjNIc|JCQB%--(tuR*X=mF-HF|cmqPl*G#K6s+0=@f86-`*=6bL zyUB2kKAm%RZIw`xDITH72}uCkcC&d@$+)Zhss*;1><~6zy{1n@6Fb+-0*iPvUj=XG z0FSDPHL8K*Zb2Bat!Q1}xb)DBfCo*aUQX|-kO|g5StuN-TIO7U{JC1uTMT`rbi6%T z5ofF&Fu|L)^9adDdvo#SChp_SY1~SKS9YJWqK@v^NYu-fO48r#H@9n`+ zUV~P%m}0_N39Z%m9c3h)vfep4K9neJNg=R56#i~72@u;ODK-82->fLr&bsrmjn zFzcOX`(V{dKnM9P>!dj+2I_74ucoCvz-9%Y`DIhDFI=aEQRnt(V8(;=U|b(YtJs>cM&QDisZR6rRGPk zw3V}sUi*5K-AJ3eB4JOUUY-_JfpT~Gn4kB9j@`I7d}lBH2?!Y2q)Ov3?-S($b8O%H zeh{9}J!2C? zrvW3Zs@7_xo*Qkn{4?^2r5At>M9(uHA22R14%&}cCZ5aTMNd0;pves$6cbmqo`tI8&DBgIQgT#Il6)kS)-NVr9r#-3;@`-Y<_FEXTw5qlPd&4{y`vhI*9P zv`h%7KHTw5nEd#siBR%2LVjj)qY^GWR1w;Kjd$Vk1yUiUZP*12usLLdb6 zgygQyK#g5%;y7C0>7E%PD(?UhtwBRIOV1fFT(5oP0Ue680DEIVXfBCQlZADom(ajc z-iyk`DHonM%pbTpHzeN^ks5u(z)MrB z(ev0b3+#Q)wafnXx3&FdazYcOed6x3v>wSgZ+w;z|1K)v4Qt_if5ng<$MqKu7aV50 zu_nYXRSUvbOKg>{ReE-%T9K zG~~OBb4Nl@!flK9+uYa`#XK9pA`^DAoXUp6Ba51D{5q(v53f`Qbl*NIxCIBZIYqk6 zA4>Zv37c%6oeowA&eqT0zXeM>BIOZvgo^g(0fv!SF2CAJWX}R6+yO;GdUHaJ5^kwL zuhi&m7F{+@CuOdcweyT~|C)169XP)iW!QUAUbt&xzv(2tcJ*lEt6BL+i{5AAxxO8! z3*8BY(6XTA;o#gYN%tGZ20}A8Sd(2 zPc01Jt?F{-z5&m02 z%?1Ijhq=L=3kipNH8gG84B;`#1m<7Wgb&eo<&Yi|31!^3>(2hvX_6lHGGU*wna|+s z(0o5J*MJ}6J1NB0p`f)&Ab~Ps5{1?wRkL5nWHnBD@?(*TMnZlw9n~y!W+kqEom^2& zlKH{wHFa7%ZIhbL9a$H40YLE{y7hp?ItMWqxoc}Hobf3><8e6ygE`^!WgUVA?=L3^ z7hE(^8mJrxt>X85hh@0Iu01&(jK4XnQ-^j zItx2fYr|zXgo$iEgCIzmaboE@v3Nu0=IiG&S`Oly_-Aa9; z!^>$_^C+DTlix`h{oc}Dm`q7v!jR^qbI;l9p0sXUawA!C=cgnI8pI>o5Hbm}{*8wc zXmD;Ra_eQqHdXqYMdx>|gzuki2;6x%IDp#Hbs=iufR*2%z~biWs0fA0!utK&u{E}} z1Fc>?Ho8rZ%Z|4q$51I07OUc4A;+(+s;B@E9qsXvv^xs5epeh{Gf>e!exz_Hk{0-Y z1g{>Vw3-q@k!lf8778Ne>+DGoQ@X- z8oYVgL+8)>j1N%mfbfVN{{F#--9$7 zSIFNVu_s`7Ww(fqCfZ!eJJJCsLYHgT9a`?`NWN<+%EQMeM*5N3m>~|B^33Xs^qK zDGE1Qd@jymD5x}EXiXd9S%6In@>Mx1+OcJx(?(tk8(zxx z9J~42(&GRu)^mZK_y3+L%ADO!qC~2#h8C~4*kv^{IsLxQVe;GV) z)BFzZrHqL-cE&Rv6e7spBl|iuMZ{pk_}Bw6y!qKz2SAOpz_b1m*6cu>|1kBT1RL-} z%Bhd^kdRTOyI;P)l@0@eUtWELi+jr)#9!|tO3Z`tSHRKHZB5KD&30%xQ062ppiy|F z$9PiS+vQPLzNslBp-th^(IcWwuc5DTV_nP*k|Y+ofOFhQniIX@Z$?}xE-Q{KjUmH^}d{YIlFmnV`C#L{VtvTMb3Z&e;l zL^GsN*n#?a4i{<5)hC?VWTl%f@v(+&pQl8ar|ecW#dk`ziI-2dLC3W*0meq|P(Aif z2kJ631~Pw{ggpSMdmB8z23ZX7ps%OM-0BTn4K9k*Qvf%Y0;;Pz^%^3n4eRa}eLx!+@?me~;R{rmncvf`jVGU}_fQ6`gMOU-jnq#?B$V>J|zj{7D z>tsrt28=N6^;iV!aeMMoMsKB8rGLQYMnf|SkDF*cGKJO?vY@aHD!q`ysMK@zp%-IN zue@fDtI+1!wFjX$pS>J2L)K;D#j6{ryCV>h*$%|2ft;53ytoKdOG1n;j5J`)l1xGD z_a4#zAl7rugUW4<6f*^Y-F^<;)9NK&rQ(cDkZb{RH!O&pQ{8U-I^66Yd;lIYx;wBr6W@Ef(~ zI(a>{dUK;DQ8cSY?l+@|UGlUJ1oP!51}qx}y+Y?dzg4mhzy~hYSiQYjW@2mg@Xt{h z$Y^r-z9>QP*ZXlQ%jN34HxBV=xWW~+kaEVYC;LM~M0}oc&wXDhr8^F6XVRh*#r#h+ zYdYRDm*$}_sPO3VV9q0vr>JKA7Xl~Z58@QWFW0f1x4@6b-RoOgz71{Q6pG=p|Fp7Of^v&v4Z(~PjAu|Qe&iM;Cf-AQxl|i(*;-`(zikesN z%m@(kfpIL`I9On^j5?yOhHLdcC1hj)#&Xa@Amm=cV6EN$$0%s$pY4{ z-Ekn$M^kpWizdHVjBGhM=%y-~B3Wc;VD?<9xzn`r^$f=>XieU^oK8)eM9qmx4sict zw&8D0s&IRmOeVsI-9tCtkPvffOSS`sSUn&I&WtA{sj5*T&$575b9p`>t}*d?Oo!I5 zt8^PJaOj${3H0^^CD0R&jzKf!%H7-Rf)}9(^9T*Kj|l99rhq3`YG4 z{Q#=K`=g{zs@$v3ZxQAGxPf4&VB)TW6KLRvIrD|TRI-EC*q;*RgJ0Zdt)YaHNyoK+ zcH|wkSXuC#M&AV3R^a+K@f2wvVXGz*S})aS#U1hZNmre$w7M2QD-ch&9Ln2|!LFTX zy=c{_SetlAs7-}Jj$q8>4i#D`cO9`{7S!jm>i$jCte=ZE$nMaBG-r-Ric}oJux8!+ z!F(mAqAmIQOnmk!)5<&qy)#}cMh*&ENxsVn>o?P|pMLsf zMIc@ zN9LEyvj%s2^PjQbLG76q7=-j8qW5Hbz1cE=2~gC6hI{D8U7i0g3S zI#wQnm^HhFL6>Iz3x(F8nL(RWl&>jjwXMV68^!?n0?he{VxA)^L?=u2a1_ z9Drn7!Ec6J?c*KpI$%u;AiEQ#f&w04u3<-)rfWb~WuukYrJ|Lj?-8Ryfm&-FG?k5( ziS-dm{gD$N65{%S1(IjFR0?f4# z%U)JJ$6cf4`v9CAqWe^AI$QnmfF#_TBJ1lb^3GzRQ<1XOE<=_*-b?;@bnk(V@^Grm zwCaPi7tI<$`>}Mn9e>5i@v!a2@-h<`%ed!iUnCg}4+=K^VBm`Fr>#GI%&^3^1kQLi zud3YrXL+^N8NVH(@^^sow4-FR;07zt5SjB``mj5nu#}nP{wIl>Lbh<|K9vWNN7eWp zr!#^g>^3o_vtR0tP`Jcps_>f)H^hNU6zWGn;&Keb5TXGB#kzS)^FjTTRen5I3b@!Y5 zZgSKN$0R~e@7V=3qf^E_t7A=k8c&YLC7m(;Ea5fN%%pj~*Sd%*3aZMMJ`*4neHL+(8BI`9((Yc@A!@#N6)D%U$}-b0V>`G zNOD2yAk3Y`pX32Lw5#KPK2jfV{=q4;7WQ7YkJVP{!nV`!_|>l@iqv086sj89`g6w$ zxjdU5zZ5V!XMX!yij+vX4OirT7hZ&pF_!J6%&lVcLi*Xqe8?M`7lD}5DCF}A96eJH zEvdanSEJ6_1{lHt*8&MfC+Hi28?p|KAftJxyMY7c0E9v3O*w?ClgouZgGGv`Q%sM{ z&`2?vS_4z>IpFoY-z*PTfEE%vO0h@Q{=3B;B|EU}@${VCk>jOjrj!}dt2D@g`AZ5+CcQYpcap(39YHq&2gkjCGZ2AG=*ReD7 zWdRHELM|1%Kh#?4y<&0h<+!Dv!q#W$pFBUb8Hth6N)UFSOQb8%A7Sg0^MCg+r3;`- z&xXC9m<^pJaY5S2k`#%mfIlhfN?SBCxn`qMM`F;>9NDAYmEtpalv8C&Hh9A2q^aKY z(({fZM|c&v45{M;)!Gf7p!?Z#S?uXUTX+knkkfH7={jN`F>|| zDGu3RD#W1WL#08eATl#}`4@l?qN%}bOXcRspnqmRn*&}zJ=WcX5ZVJtfjjncNWz{p?@QLSr=H09%b zWAqN{pj}TZRM|QdDi^$K{Pq~hzt!53{K1^q`7^Z_UWncdYpd+ft) z$B{}cJ?gSqn!qFA_|&s8y@yJk7J9O}ybViD3 zMsCk>yIuLR$#g-RO3mxpo@3chrMx7~cUc2xeCx4bxB{=qnDWdw60idiCVrUqv#ig= zG!tcX$u||wMh<@qKYlqctE)5|`k7onKEA`qpdQ+|HzVzEx^~Q+vBg7wP=DRMC-r2W zJdf6MmW7<;Z4M^94S-&Zg=SGkCtlwP5&Oe7^bCw}0+B4mXX+naI>&xeHS;pZNoxds zC_Xa%b&O{4&yyMI2lw&XMlqPYu5FIfExAL8->g~{_vNEbxcWkY*Pd`L$MI9k`Q7A; z+uua39(2beQWgJ%)qmXl8ufH7w`=yKynjYP`iVutHTy|s0`lOK&qOyZ29pz)zjNp4 z2IZKtdjfVG`oSYTV>3?9kkM?^wsh+{z$;nu0q~~Z$!uS^<<}e5n4vFglgHUl1{%rx zXBMO%b6qO;933TO38ts1JR!un2{F78F0_rlmJ?QLdH<;(LYLuoWbHf+!6`Z;BUQs^ zfSJSE_UXTeItfF?cOJj@w^u$y?PRQZa-Z6(-qiuobw1`$(r9{cv6}Te{y4$UQjBWP z6cz>vF@hPN@CUu+?6_Sk=w?W9yA$HbBz!;b%iyic4at;a!W0bgAjlqnV~8%HuT(SA zBGN6})x4U23`nm>RMjRk>rUPykWS!h8C9PF~}B><=X$G<6F23eT$aV*r!XFbc?7HtZA1HKcq&Msf|l( zluW4YmtlNpQ2UIj+%r@uGF@XuJ!U}A%gJ?m?gKTo8N(iwaO|v*U^3t}{LLL?aOm5mEvwNGkban;{cJ zb?rlT-PXu-2Or%gwg6M3*Vm{pChJqu)8TZ4y%Hu4$k$LDW7}JW(n1#M_}!DVIy@G8 z>(njPfEf6Vnx)u#hD&R@*}aZi{(6F22=0{YXdPkk(QQnE8{3o=XfYpSM@9*8cIh9S z(oL=_I)?1w|Fr9i92fQof6663E&s@7;*FJ^!(rQOcxVd%y?(dWMXG(MyC0N|#({hF zn$EFem_@GZJsm(LT+BD}= zgYG_(BO)TF;*|AAf3?J=m*=c-OTIMI<8^>sB-otrysu!$kb9kXc|pkOr@F(B*sevY zle1XMGoCvSH&WMZa%&l%!dEI(Vq=#S&8MN6&1%>oscohXkO~9^?Ek{_7ue4LU1w%9 z+^o}cI@5RisK?#c@L_)H+~y^BN-gf}(~B*JVcf10E=_Xjo43kJZJ(5S;+Lk`rgQG- zCfQ}Asf2$(IB~_w(CX(uJ&qS{eReWV@OYfaDP|F_fn?)K;aA`?jE&Ul?KB@=Ri7?v zAhb(oxRj}-uAj&=6qF@pcjZ=G!#OUlrK@b;15~sJfQO#QkPDY-h*)}NcpX@?XB2DJ znkpi+d5t`8Y~SZSUCZ56FZnQ?&*J5+_)EHjN$JuLG*|Ru(FO@nHpQr?i3rE1cDj>L zA3;Df6MT>vdBz#*HJU7s-MC!OP_wkzwFr#2@DE(SByd&RZ}GxOubBt-(LGxr*I(<$ z=n2dA;t9EniwdLD;khNuREVY9_|6GkTDT-MJpN@~7~mS+L4`3V)Xjr)@iDD5@}Em5 zL>`XPhGZvV``YZF=76p5M>~6~gf=e)Sr!{yoYn7|fXj&%!lsi@mrL zb6aqS;TRZRzHMnc+{YCXq6&j^)cFmbqQXqR@!YaSL-^qvR3QPBPuu4p2p)^f$NnV< zDpCk(_;2}kl^2GO5yfx+d3g6nFt}i zDo*Ndiw6XbEzht4?@k`QB(i1PU%$}z7!1yoJJ|{nCv7OG_RlE3AV8hjB^dp$1Nh&P z{I?|kosz##{C}t9Kf|@f%tFfIzq{nWyX3#?{eNfT|3i=Kn8FJOBHF0*=A(T_C0{_z z3)R;=AXFd&mCbAH&quVS2bQ0zKRKSGpJ#NXSMw$>=yp1xb7B^hewvXdS$geJh5w1; zRow@&7_HFi4d_|vYhGqiN4=L*&IEKjF)RJIA0~YNU%2NA{@!b*ZJ{4Tf(-wQk>q~I zND77sTI%kmC;X5;IzDeyy*4imnVU<l~Fuhqhgg+?!qnseUckQDg^;;9Y7@J;?36@J?zkI+I2pR4EO zFR=|oxwo+)KvouVplC1$f@`2_E*To>3`; z$kAFLAL_iz#M^J^c!~5HOHqO3nN$!`I*V;ILCa(Sv{DaZ(Gdyfs3|DhCEpFrRY%P1 zMxiE1iX3^07YGHaP_ytg=s&X*YRS;&QDx~Xa0ZdBiDFFFuK~jo11@xr_EM3l{~Xp* zwr9_;;Ja3diduian?2jVB5|q(z{T(qq|C^r=?o!bpp=p_1C1z|9V^CcqXHD#pdcSg zgd)dY~ z7CF@l!bY`jL$yy?Hhrz7q3F#cREFqn9UtX zp8m0(BuUw=skG=p{{mFNH}%$3d2=WL^El!~j2t-|Aui+^B?t-Dj=FGq44eQ+kQL8Y zOmbwXJ>s$YGziaP+{a=h_w7$!rWVE;X!|&?A>sIOm|W^WsO89IDYOM0F5Z8|d(vGp?kLDY3YA>}?&mdwf;^iye|#IyZ*{5uaS=*=bW+HP)@SDG{C=*w#fg}k@oC0) zJv(D<#opH@zqw?uKLsUIAd~WiRwM2)i_q>WOHrt}|1bO+&O&XoG)2&>z*R?$J6b@O z3zP2AvSy?PrSu$#AW8;ta0O^|P7~U&yY>5Mh$JIyN2lWZOsynHT$?`>wO|LWGQt^T zcS>S6J8Jc+cRmEhy6%42rOQu2`f!6Lu7oACSGN|#clSgs=7wr&pfjygS$d%!4w99E z-62FWZkEE89XL>rD?>V3m&}g->nZ>Fr`K)ZHjq|4IcSNgJ}n`F-tB>JQpLXG8N{0fYQx+ji7k{0bZc7sxiajwpgDIn`XA z7bXU(Pf54>6g%cjy4`b$#5iG3xMuul12WXp%hoNv7lUR)6ZBcog8-aj1iU4cPHxR?LR|b4it8ie*Al+>PYAugE^mk}Q+YJQe5t2T3T|3qH^RBY7 zMKBsRnkzf^Aq<7ou*2Q^jAwe{H)R9KrE4qvsR5r!A<;(*2VEQX&h%H5_z%|WV}!9i z*OwiklNB$ySk?f+EHFm00Fg>%pDMAfEYK_%nx~hwJA#}e?G%%ve}NIvacq=4ThP;% zB5i%t7(|Zs&OwL&pYNye6PD)d5jTpCH$;Jl4s7ozkOD!0{?wq?y2^V_7qpsQZ1uVrkEdsCnw1|4kg9X7mO-P{{Fv`S?tY9$Yb>`?T5nBP z5g%Gq`(HTpKii_*5yno4Mm^ky?fu)BQ8sx$Xo_;?_~D|CjN$0q4R=wVO0amz|gX*cThH1OBM1Xe;L{T7>-{`peVU literal 0 HcmV?d00001 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); +}