From df552ea6b001fe25e0d20887d95f020577586699 Mon Sep 17 00:00:00 2001 From: rahhal Date: Fri, 22 Nov 2024 10:01:53 +0000 Subject: [PATCH] Device component - Ryu Driver: - Added Topology discovery functionality --- manifests/deviceservice.yaml | 2 +- my_deploy.sh | 4 +- proto/context.proto | 1 + scripts/run_openflow.sh | 8 + src/common/DeviceTypes.py | 4 +- .../database/models/enums/DeviceDriver.py | 1 + .../drivers/OpenFlow/OpenFlowDriver.py | 196 +++++ .../service/drivers/OpenFlow/TfsApiClient.py | 144 ++++ src/device/service/drivers/OpenFlow/Tools.py | 174 +++++ .../service/drivers/OpenFlow/__init__.py | 20 + src/device/service/drivers/__init__.py | 12 +- src/device/tests/test_OpenFlow.py | 85 +++ .../openflow-ryu-controller.png | Bin 0 -> 51312 bytes tmp-code/DeviceTypes.py | 55 ++ tmp-code/OpenFlow/OpenFlowDriver.py | 173 +++++ tmp-code/OpenFlow/Tools.py | 174 +++++ tmp-code/OpenFlow/__init__.py | 20 + tmp-code/__init__.py | 202 +++++ tmp-code/context.proto | 698 ++++++++++++++++++ tmp-code/run_openflow.sh | 8 + tmp-code/test_OpenFlow.py | 77 ++ 21 files changed, 2052 insertions(+), 6 deletions(-) create mode 100755 scripts/run_openflow.sh create mode 100644 src/device/service/drivers/OpenFlow/OpenFlowDriver.py create mode 100644 src/device/service/drivers/OpenFlow/TfsApiClient.py create mode 100644 src/device/service/drivers/OpenFlow/Tools.py create mode 100644 src/device/service/drivers/OpenFlow/__init__.py create mode 100644 src/device/tests/test_OpenFlow.py create mode 100644 src/webui/service/static/topology_icons/openflow-ryu-controller.png create mode 100644 tmp-code/DeviceTypes.py create mode 100644 tmp-code/OpenFlow/OpenFlowDriver.py create mode 100644 tmp-code/OpenFlow/Tools.py create mode 100644 tmp-code/OpenFlow/__init__.py create mode 100644 tmp-code/__init__.py create mode 100644 tmp-code/context.proto create mode 100755 tmp-code/run_openflow.sh create mode 100644 tmp-code/test_OpenFlow.py diff --git a/manifests/deviceservice.yaml b/manifests/deviceservice.yaml index 950b98442..ef5195eae 100644 --- a/manifests/deviceservice.yaml +++ b/manifests/deviceservice.yaml @@ -39,7 +39,7 @@ spec: - containerPort: 9192 env: - name: LOG_LEVEL - value: "INFO" + value: "DEBUG" startupProbe: exec: command: ["/bin/grpc_health_probe", "-addr=:2020"] diff --git a/my_deploy.sh b/my_deploy.sh index a048edb30..59c7c0a9a 100755 --- a/my_deploy.sh +++ b/my_deploy.sh @@ -20,7 +20,7 @@ export TFS_REGISTRY_IMAGES="http://localhost:32000/tfs/" # Set the list of components, separated by spaces, you want to build images for, and deploy. -export TFS_COMPONENTS="context device pathcomp service slice nbi webui" +export TFS_COMPONENTS="context device pathcomp service webui" # Uncomment to activate Monitoring (old) #export TFS_COMPONENTS="${TFS_COMPONENTS} monitoring" @@ -134,7 +134,7 @@ export CRDB_PASSWORD="tfs123" export CRDB_DEPLOY_MODE="single" # Disable flag for dropping database, if it exists. -export CRDB_DROP_DATABASE_IF_EXISTS="" +export CRDB_DROP_DATABASE_IF_EXISTS="YES" # Disable flag for re-deploying CockroachDB from scratch. export CRDB_REDEPLOY="" diff --git a/proto/context.proto b/proto/context.proto index 9f06d32ee..80281f833 100644 --- a/proto/context.proto +++ b/proto/context.proto @@ -223,6 +223,7 @@ enum DeviceDriverEnum { DEVICEDRIVER_IETF_ACTN = 10; DEVICEDRIVER_OC = 11; DEVICEDRIVER_QKD = 12; + DEVICEDRIVER_RYU = 13; } enum DeviceOperationalStatusEnum { diff --git a/scripts/run_openflow.sh b/scripts/run_openflow.sh new file mode 100755 index 000000000..2c525ca70 --- /dev/null +++ b/scripts/run_openflow.sh @@ -0,0 +1,8 @@ +PROJECTDIR=`pwd` + +cd $PROJECTDIR/src +RCFILE=$PROJECTDIR/coverage/.coveragerc + +# Run unitary tests and analyze coverage of code at same time +coverage run --rcfile=$RCFILE --append -m pytest --log-level=DEBUG --verbose \ + device/tests/test_OpenFlow.py \ No newline at end of file diff --git a/src/common/DeviceTypes.py b/src/common/DeviceTypes.py index eb315352b..ccc83c9a6 100644 --- a/src/common/DeviceTypes.py +++ b/src/common/DeviceTypes.py @@ -1,4 +1,4 @@ -# Copyright 2022-2024 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -48,7 +48,7 @@ class DeviceTypeEnum(Enum): PACKET_SWITCH = 'packet-switch' XR_CONSTELLATION = 'xr-constellation' QKD_NODE = 'qkd-node' - OPEN_ROADM = 'openroadm' + OPENFLOW_RYU_CONTROLLER = 'openflow-ryu-controller' # ETSI TeraFlowSDN controller TERAFLOWSDN_CONTROLLER = 'teraflowsdn' diff --git a/src/context/service/database/models/enums/DeviceDriver.py b/src/context/service/database/models/enums/DeviceDriver.py index 5342f788a..691a7c05d 100644 --- a/src/context/service/database/models/enums/DeviceDriver.py +++ b/src/context/service/database/models/enums/DeviceDriver.py @@ -35,6 +35,7 @@ class ORM_DeviceDriverEnum(enum.Enum): IETF_ACTN = DeviceDriverEnum.DEVICEDRIVER_IETF_ACTN OC = DeviceDriverEnum.DEVICEDRIVER_OC QKD = DeviceDriverEnum.DEVICEDRIVER_QKD + RYU = DeviceDriverEnum.DEVICEDRIVER_RYU grpc_to_enum__device_driver = functools.partial( grpc_to_enum, DeviceDriverEnum, ORM_DeviceDriverEnum) diff --git a/src/device/service/drivers/OpenFlow/OpenFlowDriver.py b/src/device/service/drivers/OpenFlow/OpenFlowDriver.py new file mode 100644 index 000000000..7e70d11fb --- /dev/null +++ b/src/device/service/drivers/OpenFlow/OpenFlowDriver.py @@ -0,0 +1,196 @@ +# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# +# 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 json +import logging, requests, threading +from requests.auth import HTTPBasicAuth +from typing import Any, Iterator, List, Optional, Tuple, Union +from common.method_wrappers.Decorator import MetricsPool, metered_subclass_method +from common.type_checkers.Checkers import chk_string, chk_type +from device.service.driver_api._Driver import _Driver,RESOURCE_ENDPOINTS +from device.service.drivers.OpenFlow.TfsApiClient import TfsApiClient +from device.service.drivers.OpenFlow.Tools import find_key, get_switches, get_flows , add_flow , delete_flow , get_desc,get_port_desc, get_links_information,get_switches_information,del_flow_entry +LOGGER = logging.getLogger(__name__) + +DRIVER_NAME = 'ryu' +METRICS_POOL = MetricsPool('Device', 'Driver', labels={'driver': DRIVER_NAME}) + +ALL_RESOURCE_KEYS = [ + RESOURCE_ENDPOINTS, +] + +class OpenFlowDriver(_Driver): + def __init__(self, address: str, port: int, **settings) -> None: + super().__init__(DRIVER_NAME, address, port, **settings) + self.__lock = threading.Lock() + self.__started = threading.Event() + self.__terminate = threading.Event() + username = self.settings.get('username') + password = self.settings.get('password') + self.__auth = HTTPBasicAuth(username, password) if username is not None and password is not None else None + scheme = self.settings.get('scheme', 'http') + self.__base_url = '{:s}://{:s}:{:d}'.format(scheme, self.address, int(self.port)) + self.__timeout = int(self.settings.get('timeout', 120)) + config = {'mapping_not_needed': False, 'service_endpoint_mapping': []} + self.tac = TfsApiClient(self.address, int(self.port), scheme=scheme, username=username, password=password) + + def Connect(self) -> bool: + url = f"{self.__base_url}" + with self.__lock: + try: + response = requests.get(url, timeout=self.__timeout, verify=False, auth=self.__auth) + response.raise_for_status() + except requests.exceptions.Timeout: + LOGGER.exception(f"Timeout connecting to {self.__base_url}") + return False + except requests.exceptions.RequestException as e: + LOGGER.exception(f"Exception connecting to {self.__base_url}: {e}") + return False + else: + self.__started.set() + return True + + def Disconnect(self) -> bool: + with self.__lock: + self.__terminate.set() + return True + + @metered_subclass_method(METRICS_POOL) + def GetInitialConfig(self) -> List[Tuple[str, Any]]: + with self.__lock: + return [] + + @metered_subclass_method(METRICS_POOL) + def GetConfig(self, resource_keys: List[str] = []) -> List[Tuple[str, Union[Any, None, Exception]]]: + chk_type('resources', resource_keys, list) + results = [] + with self.__lock: + if len(resource_keys) == 0:resource_keys = ALL_RESOURCE_KEYS + LOGGER.info(f'resource_key:{ALL_RESOURCE_KEYS}') + for i, resource_key in enumerate(resource_keys): + str_resource_name = 'resource_key[#{:d}]'.format(i) + try: + chk_string(str_resource_name, resource_key, allow_empty=False) + if resource_key == RESOURCE_ENDPOINTS: + LOGGER.info(f'resource_key:{RESOURCE_ENDPOINTS}') + results.extend(self.tac.get_devices_endpoints()) + except Exception as e: + LOGGER.exception('Unhandled error processing resource_key({:s})'.format(str(resource_key))) + results.append((resource_key, e)) + return results + + +# @metered_subclass_method(METRICS_POOL) +# def GetConfig(self, resource_keys: List[str] = []) -> List[Tuple[str, Union[Any, None, Exception]]]: +# chk_type('resources', resource_keys, list) +# results = [] +# with self.__lock: +# for key in resource_keys: +# try: +# if key.startswith('flows:'): +# dpid = key.split(':', 1)[1] +# flows = get_flows(self.__base_url, dpid, auth=self.__auth, timeout=self.__timeout) +# results.append((key, flows)) +# elif key.startswith('description:'): +# dpid = key.split(':', 1)[1] +# desc = get_desc(self.__base_url, dpid, auth=self.__auth, timeout=self.__timeout) +# results.append((key, desc)) +# elif key.startswith('switches'): +# switches = get_switches(self.__base_url, auth=self.__auth, timeout=self.__timeout) +# results.append((key, switches)) +# elif key.startswith('port_description:'): +# dpid = key.split(':', 1)[1] +# desc = get_port_desc(self.__base_url,dpid, auth=self.__auth, timeout=self.__timeout) +# results.append((key, desc)) +# elif key.startswith('switch_info'): +# sin = get_switches_information(self.__base_url, auth=self.__auth, timeout=self.__timeout) +# results.append((key, sin)) +# elif key.startswith('links_info'): +# lin = get_links_information(self.__base_url, auth=self.__auth, timeout=self.__timeout) +# results.append((key, lin)) +# else: +# results.append((key, None)) # If key not handled, append None +# except Exception as e: +# results.append((key, e)) +# return results +# +# @metered_subclass_method(METRICS_POOL) +# def DeleteConfig(self, resource_keys: List[str] = []) -> List[Tuple[str, Union[Any, None, Exception]]]: +# chk_type('resources', resource_keys, list) +# results = [] +# with self.__lock: +# for item in resource_keys: +# try: +# if isinstance(item, tuple): +# key, data = item +# else: +# key, data = item, None +# if key.startswith('flowentry_delete:'): +# dpid = key.split(':', 1)[1] +# flows = del_flow_entry(self.__base_url, dpid, auth=self.__auth, timeout=self.__timeout) +# results.append((key, flows)) +# elif key=='flow_data' and data: +# flow_del = delete_flow (self.__base_url,data,auth=self.__auth, timeout=self.__timeout) +# results.append((key, flow_del)) +# else: +# results.append((key, None)) +# except Exception as e: +# results.append((key, e)) +# return results +# +# @metered_subclass_method(METRICS_POOL) +# def SetConfig(self, resources: List[Tuple[str, Any]]) -> List[Union[bool, Exception]]: +# results = [] +# if not resources: +# return results +# with self.__lock: +# for item in resources: +# LOGGER.info('resources contains: %s', item) +# try: +# if isinstance(item, tuple) and len(item) == 2: +# key, flow_data = item +# else: +# LOGGER.warning("Resource format invalid. Each item should be a tuple with (key, data).") +# results.append(False) +# continue +# if key == "flow_data" and isinstance(flow_data, dict): +# LOGGER.info(f"Found valid flow_data entry: {flow_data}") +# success = add_flow(self.__base_url, flow_data, auth=self.__auth, timeout=self.__timeout) +# results.append(success) +# else: +# LOGGER.warning(f"Skipping item with key: {key} due to invalid format or missing data.") +# results.append(False) +# +# except Exception as e: +# LOGGER.error(f"Exception while setting configuration for item {item}: {str(e)}") +# results.append(e) +# +# return results +# +# +# +# @metered_subclass_method(METRICS_POOL) +# def SubscribeState(self, subscriptions : List[Tuple[str, float, float]]) -> List[Union[bool, Exception]]: +# # TODO: TAPI does not support monitoring by now +# return [False for _ in subscriptions] +# +# @metered_subclass_method(METRICS_POOL) +# def UnsubscribeState(self, subscriptions : List[Tuple[str, float, float]]) -> List[Union[bool, Exception]]: +# # TODO: TAPI does not support monitoring by now +# return [False for _ in subscriptions] +# +# def GetState( +# self, blocking=False, terminate : Optional[threading.Event] = None +# ) -> Iterator[Tuple[float, str, Any]]: +# # TODO: TAPI does not support monitoring by now +# return [] diff --git a/src/device/service/drivers/OpenFlow/TfsApiClient.py b/src/device/service/drivers/OpenFlow/TfsApiClient.py new file mode 100644 index 000000000..5db9c202c --- /dev/null +++ b/src/device/service/drivers/OpenFlow/TfsApiClient.py @@ -0,0 +1,144 @@ +# Copyright 2022-2024 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# +# 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 json, logging, requests +from os import name +from requests.auth import HTTPBasicAuth +from typing import Dict, List, Optional + + +GET_DEVICES_URL = '{:s}://{:s}:{:d}/v1.0/topology/switches' +GET_LINKS_URL = '{:s}://{:s}:{:d}/v1.0/topology/links' +TIMEOUT = 30 + +HTTP_OK_CODES = { + 200, # OK + 201, # Created + 202, # Accepted + 204, # No Content +} + +MAPPING_STATUS = { + 'DEVICEOPERATIONALSTATUS_UNDEFINED': 0, + 'DEVICEOPERATIONALSTATUS_DISABLED' : 1, + 'DEVICEOPERATIONALSTATUS_ENABLED' : 2, +} + +MAPPING_DRIVER = { + 'DEVICEDRIVER_UNDEFINED' : 0, + 'DEVICEDRIVER_OPENCONFIG' : 1, + 'DEVICEDRIVER_TRANSPORT_API' : 2, + 'DEVICEDRIVER_P4' : 3, + 'DEVICEDRIVER_IETF_NETWORK_TOPOLOGY': 4, + 'DEVICEDRIVER_ONF_TR_532' : 5, + 'DEVICEDRIVER_XR' : 6, + 'DEVICEDRIVER_IETF_L2VPN' : 7, + 'DEVICEDRIVER_GNMI_OPENCONFIG' : 8, + 'DEVICEDRIVER_OPTICAL_TFS' : 9, + 'DEVICEDRIVER_IETF_ACTN' : 10, + 'DEVICEDRIVER_OC' : 11, + 'DEVICEDRIVER_QKD' : 12, + 'DEVICEDRIVER_RYU' : 13, +} + +MSG_ERROR = 'Could not retrieve devices in remote TeraFlowSDN instance({:s}). status_code={:s} reply={:s}' + +LOGGER = logging.getLogger(__name__) + +class TfsApiClient: + def __init__( + self, address : str, port : int, scheme : str = 'http', + username : Optional[str] = None, password : Optional[str] = None + ) -> None: + self._devices_url = GET_DEVICES_URL.format(scheme, address, port) + LOGGER.info(f'self_devices_url{self._devices_url}') + self._links_url = GET_LINKS_URL.format(scheme, address, port) + self._auth = HTTPBasicAuth(username, password) if username is not None and password is not None else None + + def get_devices_endpoints(self) -> List[Dict]: + LOGGER.debug('[get_devices_endpoints] begin') + + reply_switches = requests.get(self._devices_url, timeout=TIMEOUT, verify=False, auth=self._auth) + if reply_switches.status_code not in HTTP_OK_CODES: + msg = MSG_ERROR.format(str(self._devices_url), str(reply_switches.status_code), str(reply_switches)) + LOGGER.error(msg) + raise Exception(msg) + + json_reply_switches = reply_switches.json() + LOGGER.info('[get_devices_endpoints] json_reply_switches={:s}'.format(json.dumps(json_reply_switches))) + + result = list() + for json_switch in json_reply_switches: + device_uuid: str = json_switch['dpid'] + device_ports = json_switch.get('ports', []) + + for port in device_ports: + port_name = port.get('name', '') + device_name = port_name.split('-')[0] + port_no = port.get('port_no', '') + hw_address = port.get('hw_addr', '') + + device_url = '/devices/device[{:s}]'.format(device_uuid) + device_data = { + 'uuid': device_uuid, + 'name': device_name, + 'type': 'packet-switch', + 'status': 2, # Uncomment if device_status is included + 'drivers': 'DEVICEDRIVER_RYU', + } + result.append((device_url, device_data)) + for json_switch in json_reply_switches: + device_uuid: str = json_switch['dpid'] + device_ports = json_switch.get('ports', []) + for port in device_ports: + port_name = port.get('name', '') + port_no = port.get('port_no','') + + endpoint_uuid = port_name + endpoint_url = '/endpoints/endpoint[{:s}]'.format(endpoint_uuid) + endpoint_data = { + 'device_uuid': device_uuid, + 'uuid': port_no, + 'name': port_name, + 'type': 'copper', + } + result.append((endpoint_url, endpoint_data)) +# + reply = requests.get(self._links_url, timeout=TIMEOUT, verify=False, auth=self._auth) + if reply.status_code not in HTTP_OK_CODES: + msg = MSG_ERROR.format(str(self._links_url), str(reply.status_code), str(reply)) + LOGGER.error(msg) + raise Exception(msg) + for json_link in reply.json(): + dpid_src = json_link.get('src', {}).get('dpid', '') + dpid_dst = json_link.get('dst', {}).get('dpid', '') + port_src_name = json_link.get('src', {}).get('name', '') + port_dst_name = json_link.get('dst', {}).get('name', '') + link_name = f"{port_src_name}=={port_dst_name}" + link_uuid = f"{dpid_src}-{port_src_name}==={dpid_dst}-{port_dst_name}" + link_endpoint_ids = [ + (dpid_src, port_src_name), + (dpid_dst, port_dst_name), + ] + LOGGER.info('link_endpoint_ids [{:s}]'.format(link_endpoint_ids)) + link_url = '/links/link[{:s}]'.format(link_uuid) + link_data = { + 'uuid': link_uuid, + 'name': link_name, + 'endpoints': link_endpoint_ids, + } + result.append((link_url, link_data)) +# + LOGGER.debug('[get_devices_endpoints] topology; returning') + return result diff --git a/src/device/service/drivers/OpenFlow/Tools.py b/src/device/service/drivers/OpenFlow/Tools.py new file mode 100644 index 000000000..d68347087 --- /dev/null +++ b/src/device/service/drivers/OpenFlow/Tools.py @@ -0,0 +1,174 @@ +# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# +# 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 json, logging, operator, requests +from requests.auth import HTTPBasicAuth +from typing import Optional +from device.service.driver_api._Driver import RESOURCE_ENDPOINTS, RESOURCE_SERVICES +from typing import List, Dict, Optional, Tuple, Union + +LOGGER = logging.getLogger(__name__) + +RESOURCE_ENDPOINTS = { + #get configurations + "switches": "/stats/switches", + "description": "/stats/desc", + "flows": "/stats/flow", + "port_description":"/stats/portdesc", + "switch_info":"/v1.0/topology/switches", + "links_info":"/v1.0/topology/links", + #add flow + "flow_add": "/stats/flowentry/add", + #Delete all matching flow entries of the switch. + "flow_delete": "/stats/flowentry/delete", + "flowentry_delete":"/stats/flowentry/clear", #according to dpid + +} + +HTTP_OK_CODES = { + 200, # OK + 201, # Created + 202, # Accepted + 204, # No Content +} + +# Utility function to find and extract a specific key from a resource. +def find_key(resource: Tuple[str, str], key: str) -> Union[dict, str, None]: + try: + return json.loads(resource[1])[key] + except KeyError: + LOGGER.warning(f"Key '{key}' not found in resource.") + return None + +def get_switches(root_url: str, auth: Optional[HTTPBasicAuth] = None, timeout: Optional[int] = None) -> List[Dict]: + url = f"{root_url}{RESOURCE_ENDPOINTS['switches']}" + result = [] + try: + response = requests.get(url, timeout=timeout, verify=False, auth=auth) + response.raise_for_status() + switches = response.json() + LOGGER.info(f"Successfully retrieved switches: {switches}") + result = switches + except requests.exceptions.Timeout: + LOGGER.exception(f"Timeout connecting to {url}") + except requests.exceptions.RequestException as e: + LOGGER.exception(f"Error retrieving switches: {str(e)}") + return result + +def get_switches_information(root_url: str, auth: Optional[HTTPBasicAuth] = None, timeout: Optional[int] = None) -> List[Dict]: + url = f"{root_url}{RESOURCE_ENDPOINTS['switch_info']}" + result = [] + try: + response = requests.get(url, timeout=timeout, verify=False, auth=auth) + response.raise_for_status() + switches_info = response.json() + LOGGER.info(f"Successfully retrieved switches: {switches_info}") + result = switches_info + except requests.exceptions.Timeout: + LOGGER.exception(f"Timeout connecting to {url}") + except requests.exceptions.RequestException as e: + LOGGER.exception(f"Error retrieving switches: {str(e)}") + return result + +def get_links_information(root_url: str, auth: Optional[HTTPBasicAuth] = None, timeout: Optional[int] = None) -> List[Dict]: + url = f"{root_url}{RESOURCE_ENDPOINTS['links_info']}" + result = [] + try: + response = requests.get(url, timeout=timeout, verify=False, auth=auth) + response.raise_for_status() + links_info = response.json() + LOGGER.info(f"Successfully retrieved switches: {links_info}") + result = links_info + except requests.exceptions.Timeout: + LOGGER.exception(f"Timeout connecting to {url}") + except requests.exceptions.RequestException as e: + LOGGER.exception(f"Error retrieving switches: {str(e)}") + return result + +def get_flows(root_url: str, dpid: str, auth: Optional[HTTPBasicAuth] = None, timeout: Optional[int] = None) -> List[Dict]: + url = f"{root_url}{RESOURCE_ENDPOINTS['flows']}/{dpid}" + try: + response = requests.get(url, timeout=timeout, verify=False, auth=auth) + response.raise_for_status() + flows = response.json() + LOGGER.info(f"Successfully retrieved flow rules for DPID {dpid}") + return flows + except requests.exceptions.RequestException as e: + LOGGER.error(f"Failed to retrieve flow rules for DPID {dpid}: {str(e)}") + return [] + +#get description +def get_desc(root_url: str, dpid: str, auth: Optional[HTTPBasicAuth] = None, timeout: Optional[int] = None) -> Dict: + url = f"{root_url}{RESOURCE_ENDPOINTS['description']}/{dpid}" + try: + response = requests.get(url, timeout=timeout, verify=False, auth=auth) + response.raise_for_status() + desc = response.json() + LOGGER.info(f"Successfully retrieved description for DPID {dpid}: {desc}") + return desc + except requests.exceptions.RequestException as e: + LOGGER.error(f"Failed to retrieve description for DPID {dpid}: {str(e)}") + return {} + +def get_port_desc(root_url: str, dpid: str, auth: Optional[HTTPBasicAuth] = None, timeout: Optional[int] = None) -> Dict: + url = f"{root_url}{RESOURCE_ENDPOINTS['port_description']}/{dpid}" + try: + response = requests.get(url, timeout=timeout, verify=False, auth=auth) + response.raise_for_status() + port_desc = response.json() + LOGGER.info(f"Successfully retrieved description for DPID {dpid}: {port_desc}") + return port_desc + except requests.exceptions.RequestException as e: + LOGGER.error(f"Failed to retrieve description for DPID {dpid}: {str(e)}") + return {} + +##according to dpid +def del_flow_entry(root_url: str, dpid: str, auth: Optional[HTTPBasicAuth] = None, timeout: Optional[int] = None) -> Dict: + url = f"{root_url}{RESOURCE_ENDPOINTS['flowentry_delete']}/{dpid}" + try: + response = requests.delete(url, timeout=timeout, verify=False, auth=auth) + response.raise_for_status() + flow_desc = response.json() + LOGGER.info(f"Successfully retrieved description for DPID {dpid}: {flow_desc}") + return flow_desc + except requests.exceptions.RequestException as e: + LOGGER.error(f"Failed to retrieve description for DPID {dpid}: {str(e)}") + return {} + +# to delete a flow based on match criteria. +def delete_flow(root_url: str, flow_data: dict, auth: Optional[HTTPBasicAuth] = None, timeout: Optional[int] = None) -> bool: + url = f"{root_url}{RESOURCE_ENDPOINTS['flow_delete']}" + try: + response = requests.post(url, json=flow_data, timeout=timeout, verify=False, auth=auth) + response.raise_for_status() + LOGGER.info(f"Flow configuration deleted successfully for DPID {flow_data.get('dpid')}.") + return True + except requests.exceptions.RequestException as e: + LOGGER.error(f"Failed to delete flow configuration for DPID {flow_data.get('dpid')}: {str(e)}") + return False + +def add_flow(root_url: str, flow_data: dict, auth: Optional[HTTPBasicAuth] = None, timeout: Optional[int] = None) -> bool: + url = f"{root_url}{RESOURCE_ENDPOINTS['flow_add']}" + LOGGER.info(f"Posting flow data: {flow_data} (type: {type(flow_data)}) to URL: {url}") + try: + response = requests.post(url, json=flow_data, timeout=timeout, verify=False, auth=auth) + response.raise_for_status() + LOGGER.info("Flow configuration added successfully.") + return True + except requests.exceptions.RequestException as e: + LOGGER.error(f"Failed to add flow configuration: {str(e)}") + return False + + + diff --git a/src/device/service/drivers/OpenFlow/__init__.py b/src/device/service/drivers/OpenFlow/__init__.py new file mode 100644 index 000000000..4f3d1a042 --- /dev/null +++ b/src/device/service/drivers/OpenFlow/__init__.py @@ -0,0 +1,20 @@ +# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# +# 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 device.service.driver_api._Driver import RESOURCE_ENDPOINTS, RESOURCE_SERVICES + +ALL_RESOURCE_KEYS = [ + RESOURCE_ENDPOINTS, + RESOURCE_SERVICES, +] diff --git a/src/device/service/drivers/__init__.py b/src/device/service/drivers/__init__.py index b99ee50ca..837d83d53 100644 --- a/src/device/service/drivers/__init__.py +++ b/src/device/service/drivers/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2022-2024 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -143,6 +143,16 @@ if LOAD_ALL_DEVICE_DRIVERS: FilterFieldEnum.DRIVER : DeviceDriverEnum.DEVICEDRIVER_IETF_NETWORK_TOPOLOGY, } ])) +if LOAD_ALL_DEVICE_DRIVERS: + from.OpenFlow.OpenFlowDriver import OpenFlowDriver + DRIVERS.append( + (OpenFlowDriver, [ + { + FilterFieldEnum.DEVICE_TYPE: DeviceTypeEnum.OPENFLOW_RYU_CONTROLLER , + FilterFieldEnum.DRIVER : DeviceDriverEnum.DEVICEDRIVER_RYU , + } + ]) + ) if LOAD_ALL_DEVICE_DRIVERS: from .xr.XrDriver import XrDriver # pylint: disable=wrong-import-position diff --git a/src/device/tests/test_OpenFlow.py b/src/device/tests/test_OpenFlow.py new file mode 100644 index 000000000..c1d3919ea --- /dev/null +++ b/src/device/tests/test_OpenFlow.py @@ -0,0 +1,85 @@ +import json +from re import A +import resource +import logging, os, sys, time + +from joblib import Logger +#from typing import Dict, Self, Tuple +os.environ['DEVICE_EMULATED_ONLY'] = 'YES' +from device.service.drivers.OpenFlow.OpenFlowDriver import OpenFlowDriver +logging.basicConfig(level=logging.DEBUG) +LOGGER = logging.getLogger(__name__) +LOGGER.setLevel(logging.DEBUG) + + +def test_main(): + driver_settings = { + 'protocol': 'http', + 'username': None, + 'password': None, + 'use_tls': False, + } + driver = OpenFlowDriver('127.0.0.1', 8080 , **driver_settings) + driver.Connect() + + import requests + + response = requests.get("http://127.0.0.1:8080/v1.0/topology/switches", timeout=10) + LOGGER.info(f'the response is{response}') + + + + # Test: GetConfig + #resource_keys = [ 'flows:1','description:1','switches','port_description:1','switch_info','links_info'] + # config = driver.GetConfig(resource_keys ) + # LOGGER.info('Specific configuration: %s', config) + + #resource_delete=["flowentry_delete:1"] + #config = driver.DeleteConfig(resource_delete) + #LOGGER.info('Specific configuration: %s', config) + #a=driver.GetConfig(["flows:1"]) + #LOGGER.info('flow 1 = {:s}'.format(str(a))) +# delete_data = { +# "dpid": 2, +# "cookie": 1, +# "cookie_mask": 1, +# "table_id": 0, +# "idle_timeout": 30, +# "hard_timeout": 30, +# "priority": 11111, +# "flags": 1, +# "match":{ +# "in_port":2 +# }, +# "actions":[ +# { +# "type":"ddf", +# "port": 1 +# } +# ] +# } +# delete_result = driver.DeleteConfig([("flow_data", delete_data)]) +# LOGGER.info('resources_to_delete = {:s}'.format(str(delete_result))) +# a=driver.GetConfig(["flows:1"]) +# LOGGER.info('flow 2 = {:s}'.format(str(a))) +# flow_data = { +# "dpid": 2, +# "priority": 22224, +# "match": { +# "in_port": 1 +# }, +# "actions": [ +# { +# "type": "GOTO_TABLE", +# "table_id": 1 +# } +# ] +# } +# set_result = driver.SetConfig([('flow_data',flow_data)]) +# LOGGER.info(set_result) +# driver.Disconnect() +# + raise Exception () + +if __name__ == '__main__': + sys.exit(test_main()) diff --git a/src/webui/service/static/topology_icons/openflow-ryu-controller.png b/src/webui/service/static/topology_icons/openflow-ryu-controller.png new file mode 100644 index 0000000000000000000000000000000000000000..2982c57308983b367f1fa13c559fb702edcbadfe GIT binary patch literal 51312 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�!K`ff0Ma9w~W(#SH$!z zHhEq8fb%dH507?JCyvXzV`N$2zLHT8J#y;ZxGoHh8N7@ILfs_(a~`Zp#a)kEWddK- zy|;-<^^I4bW*P!%M7I}R;$Dz0Sj9#8Y-H*_4BmZY3iI~bu4$xDhI_p))G9ncHpv3f2AZ zYSCLh%!RljtIB2tC&p5eip@bQ5b_S52k_q=w%c(V)-z<>x(2=%YBzD;Ygar#LMHDM zinCs1f~G!6Hf zfh<}O(U)}Ri z0i@7Am>Nu&bcf#FoAMt>j9dFDKFuMZTNTDA(B$UbbJ|rAMxOr+F7Pu-FT<({8^_5m zM!Z=)^NtOQw<^H%|VLJLqYwhC#?(!CqbGWzAG!JkhtetE{gxNX{>m1n(S zX0x2)w*~)qUoj;YXYJOX->2~QCAS*8j;?}eZ~Evbxb-_zIl|v$A;{?~%L?%hhfC>; zs=-3b6^YsFIhvPLat~5JY0e>vDOQQ44A;Pb=O0;OAJCf`#C$08=FZgl|2cLRxyD2) zWk)_ERh$D83nrDudfg9#>qiau;Q0oUSTg(!XX-%}w@%r!QpW{3;S&g8GxF1--jHVD_^pXByC5 zLa$=|@n+IVyRIJw`dia931wzDl3LrI!{r_k-4e&A*-{U6&3}1LELoA!&x1po`Uv+( z^JwMSSX0#Nmf(z!xr9u9W%Q17Kf<+mk{8;(@m8~IuHtz;17Y<~+Kji6bC@l>ImYB^ z@o1zR!Yqy;Tr?h85mAgY%|Ux)kaLI-4=!sMJmU{8mYy{>>|xwO$1RbT51d15wK276-1Iz6ZNKs9=;^rDttyR8nkdSwq3W6F>_q;3@2L_T zb%i#_#MsPyB2*Natw$qH#yrh==FpehYuoaBYDY(r>hSif&GoP05(G6wy4F)p-kVdo z;NlkCLYBX0BZ8K?qUe%?Y@l0jG#RpDhjQJJZT&EUN_<4c%P1F`W^&@B zeiKsy*YM?O$q{!-CyWgLj~)B$MLJQr+yuqW?~Ed_2yna!(5CL3HAr_&V9|YAhClb=}~boNB9()n1LS)kFJL&|jbFzlk=VrUc59S@pu0!pcPE&Sik z-+d*b%54>-?BiJ?Y{PF=1Sb371()0FQX!Bu-qH(fT|Ht0kZtNQXm|INMcjM9|! zi?->>sCc~}8!4nN=Eo6BLgX;yCFhnu{EBZk9li3C5}?dqUU)|*-IG2&Y}@m1b^NqR zJt~QyMYf%LLu13&_R|5sxA^Ub(;JD)(?mW~U!?ZN1F7CfWXJcx{C{hMZ{xpdT|g)G z*A^*Ps?9#^r2nj)OnpufRP}F6Qrb#{9)yNO1h4@rzBWpv)@Qxk;385Tzu^B30ma|c z2BjtZzO!~VPE5*?Pf)C_o8i|!#X`)#IiN@Bz6VKCv5?xNE0{DEvaynj&O^TsL9`cn zJBMlv_eXI%U}EWNGdGb>e+bl*bNBIG9yRZ*eo`U$qWg_!Sf}NhPQzZuW*F<~9IuTB z^JBSWVcW1@m*so)4Hm`aQ#g2r}PakZ`f6)p!krn#uP zx;A@q0%zEvTSzDE<1L0`$dq+Z(F$8)^TvW{^2NVZ{|%n_KJIg3b2Ueeqg>;A)h5$x zQ_bMT>?8I8RIO}9cqx&2V|%89{|Sltt57vf>&ka!r%j?%w@SnvB zqA#vjR=drBwVAyI+`^v%>pvBm{hJ+Mf=vtG{BRiF^Pn(IRC*M&O-_Ea@^x1DBtiv(Jdckq67SsV_-JW@~cK#8@s>+*js9JR6_XzM|CSzDx;Gj1^?O!VRKipMu$t+>%WSPgKMFA5Y)7zqf9IXpW$|momJcmjt7Lh zK>ZNph54qb_`wVn45Na(Gf<`(Dpjl*fKc#vq`hyZ2*Wxa zNd+E<>e}AP_RqpMw*n(Y|J_VH7552*f$z><2*c{QYKog#sEfQVu=?uECk7-3_jm^H z)5YsuRX=NMj=%BH{_D^t2s>$5_QN$|_H`^ZM>_P{uU`0HY&J2pfS6WD`7o+KwN=h1#{vxnd3?T(@I=3)cq;%LOm34|^Xivn)f{qD=bJG@zBk;T);lXh+T zLKT)Tb6@)SJ%3*WD$(Z@vbe}cegvX@EbT|GA+Ncn%bnc9|4#!m3uzMzzb6yZQUj~I?FA1=S=c^`~1$i_;r`En~n zVv>D@%_m~udE@NatH}UhLzI08&D4?t%M)8xD-YHY5}j`)0SqdMfj6`fUtT#@=V+Mg z1OOhxY*0W;et$|gMWnBumgYN-iZle@K0*(o@U=}oEsR_Aiw9g-h~;|jg+5{(__&>3 z^_gdbVK7AJ&0g*M)JN2Meyw`?+;8%fmd`J_fls1wmqLFomEiVXgJ{v^y0LnHXQxM7 z2HY0iEVKYMfdfgziN(+`^v4^`H}bE!x%Wrm4p-AgOSzTSFR%rk%h#ffxx&U9Pd3!H zKkWmVzh^))wWu_BH9C2UFeA}c@wuVCm`eGZWDc2q8xXKUw^Fljygf}r7ewQL{Ty|1 z!T&W-XoO}Zav8Zx>z8BkxyV8mlVaS5ubT~lIoy0=bkWn6X}LF6GAOD^I~{Youei^I zT|+Rg?JZS7HPEU6h(PRIIDL<7_Ud3&z`^0R9Lv_7xVMuhToK5n+ zIz>!c++4d}`>ik@yY_RfMX%W6upQ=KH=fDwOh3O^zeCq-XKAW8`5A*O@3WG@siBPCN0Z`Dh z=}N~}m)|asqojrko3$xm#>n9%eg%l8t-(WnlntPh{A4^UBvMvqznVV zAg>W!(+xGEK2Zs3B7yLHX#s>5HRm_l6eC&5u>8sWHZS+EpK2q@ZW34ff*LqNu%1FOKb3q?mAUe;R-zfBcur zDrhXx3|X^vHI=kIJKxB+y%24r+#=Z(yXA#c|1Jtmrk90@3-~~kMQ#+;fdD3d;j&@b z^$HJ-xbeD3cyZ{`JS~6zg-~DLEN)6hfkJik|F&+d@4~WeUa5TB{5?!gB9@@f@n)vTD?aa2a-q3Q|U?CHCg}qfsIRsoTE$K7qgaOXM3(ClYkYg zZv|HF7g9FXc0wX%0%v{CWhlDNxxpIrKy7?OjsdMP45GG}sN3Cw(Ay z@>N!z`tWi{HNO1bHRE=0#xi)yxXHPw+BX_ejH+Xbf&Y^D*900iubYAQeCNmVw{|ae zbeCi7=!Kg$vgG@n1|#`wlP?Ydj1xQzv!31KCm{`9>2uC%0HS3ucLBHJrn->=ri9m`}lMdrqA_AM)|2_*GpR1KyGSPUUUFS|nO&}R{=XKW|m2(W0lvm(RMRmf* zXbn|{;IP^)oJ+2pmQ!ED+86yFMw>q@7Tnub^!`D&WZ?&J((-q-^~48vuKKV>lri%F z##kk7iGjisxA%KCvHY7vcvWvcTavU1g9=a}00L^a?UcB|S1Et+ylvG2x$WY%QZ64z zblKZ}o(Nqkel?cj^Rf2JixhhJ(+)8C?AtoZKN*{hjeDpcQX*_D_ARYP(HaK$R@`{bWMBQ&b8Rye zuCv&48S}4_r|}Jz(T~XX^Ji0wh|6>*Uq47IB>#YoEKq;iIJu6HCsL#fe4?>VNkCj^ zs}X_`Uo4!vFVcM^{uSp##?&_HWj`*|nGvcrE_sXf?{Ld1Vq%pm9fnez2jd)T!FsvZyD7w#OKidb^Njlr1^Z9*Oy1vx$0#< zUd#st7UHCmvLx=;*IX-GuS-ofc~=;m(>OKwxE5$I8?0W>PiG`^KTR{e+=gpJr7K_L zcw4uWBW;a)<*!On?uxO4{OnZ)qZNGe~V*Z>fC@9hyn@No^Pbp9p{p$(m z`RT5X+vmMud*r4;+67bd5s`+(AlFy1`PLcuZM_yA27cpv{Yo%GD0#SdSCA2cBn!+! zl`znSWJuq8LNjMoThMedzF#eHuK*)*fB*d6A5tly7Eh21W1kEA z4xzt!Pm{ay5^UddCnhz+AT<&9N_V`G@Px6XBA@q*Y}?Nh^rrA4cKK-PJpeTZ5^0}I z4983k9#A%%2MJ+LpUmq8cKv$wTA$rngm>4~RLP|2FBtTa-$>_dl7;}xCdkN%@PJ2xc=_=34fZ|O4C(}od8PNTJ+=OtFiQ(tB{^#^}-s~3VH z=z`1zVpdyNG$qHR5+#P$`AI3Yh!f@P#r$dXsX3WxUHrU_nvMgQ&LDr%8Xg(JC4x($rjYJciG{+VL9vrMHnDo$ zMn!Lo0&`54oVoql6{~6}BZ9lbKSwC*cRo*YBq*zoBH@lq6lc=X;G#T})*N?s-WK1? zHzY4nK}Zqa~cG94E!)suGibP@>f%DIX449cd=`wLf1xwx)Zi}L#sHirU>H6X5uLuW(3F`n zoyLl}n1fSe@?NFEtQy#|Qetik7cYw#vR~To-|E9b+A#i}NClYCHJHu@-;m78J6VPo zV)v)JIK^0918}y=p@BUUb8cLCTA@AjBWoe}b8`+Q@=RJPPRSj1u1d^FSE%C*Cnn&t zdiJdXJ$$0kfL6q8G#Q@PAb9<3cry~YSixVFWa>+}>#sRh1glfq7c1!6I^qd?;vv7sgxMpJ0#VXcL^)zVz`W&uzPL;c+t2*7#QXy6-<;^W1L#=xa(&9w;~Hx<{CtL+L8&kaV`|GoWjCKK}<< zlL^5{0ZD*mx^I?#j57SdMLbs)=JTq{8XR8LkbRu|ywLndc#Z*ZwF|y$UDkL7W|Ip% zduwIsGA|R6lvSx5-f{zZ59 zF!EXc-6!;t_^9^9-Lbd(Bf{~AW>zN9Yi&{9^*}nFhT*2`GMv=VhN$!M+9d(}_y{Ad zl)%7iky?TB)pKTpy??9u)!(fq#xlaOE%xwvr6Jy>d<(EX5^*h)b!>O(XO=rZP#_F5 zNIyyBa5c~42A$s zq0Q24XD33>Jb&B>ypDzsiBNfPbJjW<@ z^T!7zvoO(h(vi8oJ{@hwi?EEhJKn{SNOPvU9eSz&O>IE?m}%sE$EQCV-AcG|<1?&N zaa|c8<6F7v4z8*TuNa@E2!E-fV^cYZEXv=~QPvwDaQGB6ZG~yY>DY7Ega5$yoJG>v z@8vi9YXg>WGhM!r3sOZ7qVU4={#j}~2W+emx)Ztu$Iwlbk@a?O>wo*K(KKA$Qc9R< zN+S00^cBmyV|fS}c|#jJFe^mZMDvG4=TwNUUO*S!yP!X!y{SA6VWjw<-r-397)Gk& zXRnOQ-q(yhC9^BvUb|3~TcWdN59+JfXNHo@pNUk6VHyfdDo2LS?v zbb9ssets8cO?Io(Ek}F^vgruF3L#&4Oj)J04jVHiAqDVdZ{b2(EHN!x!H~+LGZ~a=0{O6S>b@R*kZLzzf*y>hW zCo=;Ge!9}`kbO5ii;@wR?XgsY^$hCkz(#|5y!=fDv6z!SrI%E)$F_h>86kLfX%YsK#xz z!mzat7MV}5(M(9!lf`po805ckD~95yG$MdB@G9XlxO6h7-D65@IBqGkJ4Lc2uXD*p zG79We2&`=6mMlhiCcYI&f1~YYpE;=yUY+wjmVx$td?)Q{Cy+>7Qv#N102l*6z}Mqk zEcHkw^=tS9R<38ykCsO|8 zR(VZ@YL!4HP(;K9fjbgPw9<@+$NUP0xv3eG20xS%WW!2qz>2G=Rejx*)xfO9RzlG+ zn^-~Wc@y(JFtG9+D2Vj^S+<1PPx~8X8KWA{k;7gE+U>2u{GaSBxZH>22M*=UO##&OyO!=C3K5|tVy^7@b^|G|6^nUUA*Gvi57z_#5DShouf6#+G1ItJvZA8?OL;>gx_c|9@kGjaIzlOsBi&l1Q@&E zg5*e_^ruEe|1_RJ)?fmeLc+@{fjg?=XvKMeDulQ#A$^N9IV6EF3?KJJh1=ScxX#w8 zu>7)Z`N)8@S&)ZJTM1~sWEHD+MvS+fBtvmO_`r7gy8SjU;Q~S;L{&8@-v?VEQyqK2 z=tzGoiasZe4gr>sS4`p%I`1WF$v(N)rIX!pcu2~j^$U)HLaM})xHTA7{+7}LQM7m8 zT$Ol9?gExts;s9U@Y2X;ZrnrsglJ-`k1P;kaQyeI&pSu47tvEY2n~2&pT--LSe|v3L_3 z?Tmem1@ZqQVEKj_3(%T1~?%#HI?iH~Gvc69Xn;C%~@`Xc>!Rx5PU zMO}~Ecu)x^dS>?ajc2(rVJM@-pKz6}u>eigB1mv%k33&0JQJbf@gY|VY!OGx(wmOO z5Tz3sG91s*kn?CdWTr+dj+cmn2cG;K8$2ZSw;709dj!CLRKE);Jnr~qi@JYfV2X<7 zmft_vytTmfm93x@9oGNJ{7Y)GN2zGKRBY%qGq!J4G;8sI;L@MtHTPFj%nYhuaZLQ; zJ5GmPtw38{;MAQ;jV{@}J+Is-CPV+)FpG|Xeg6}bS;qHL1V}khy@7656(c<@BL7o9 zFZ(>5B32_7xz;iQ9SOq@3EYwSnG7CImM70pq|a!^r-rwJ zCpQc--8Ai20(XE&`lYbw8eLjdzt<=?tEEsXNN+_Dnh7VJ3_1=& zd%y?K`tAz`;6uK z%YL{6JNNnF`%{Ubtz+Z@%R>K?H~iF?Brsv&)9BpT8e>H28X^EtH>_vCH9pXua8=^3 z%R#bK9JJ)Gng%{ssHy>!<&K5AiXwDL!m zBFXIN*TGXZL%sBtV^v-3t(*&u;p1O|zcwKTE?_H={TEGNo!1eWI2{H~J=qRvRAIe^ zfBIDU;*aq)q$_4%nqevD49aZ%>E|Z@%g@M_Z$nu0C#f~+zwi+u=bMu$Aua{wANDC& zHc)bW)Otg1hFdseeR*G>sP#F%`Re!CEtKy1Znk`k8YCo8P)@l6y_H-zTDL5qA~5$+ zcD#^M9&{6&*rkwr`WzjGgz#attR|dr$K>1AlhI3KLELq;`<&MGho;d(B0Qz+!o{G5 z3xWhXZoBe6vsaKvD~9UKU>fq9D?jn|whkHnX%b~%+e-#hZ3g#}pcwxN2cMNm^&Yvv z)GKYh&D=cbD~2sQMdG7mwV_{#+plaAE5nVaHHi7h5S=l7BU?6=sWc*@EjJIZ z9U1AEn8Vcg0lD_!wR)tTx5xb}L^NNb;b5+7Wn6BSII|xf2I|@ig=(A#v7RTE#b{g| zujHm^qWPIvD0bRzI?vZOiS)!h37x^?-EQAuWu6)fn4pnSQTUp*!VJTETH_yD!NK=& zs4jn|4qJ*dNbY(PvaF2;svWYW*-k579xW7a@*;zt*Dmw^6nHdEYqvYR!K4B%P9<-6 z2YGU-aZ1sV;xxgRsOV1B`OVa#=AU0_#aj|J5^RIhw)h%Kz>AsT1wb$0WarzJx%MoL zZv&)86hukBM0ciSCD@7X8lH}gbt3JIp6Oi`E#dh%=>$|yiFv+RNvyHkawszX)B%qzSHZ1 zKYUPOK$Rp%ZVc-*pGr!xPy9cWkCxG zGcrs^PG~)fw%q-=h&yR0SMOz+wK$wj;HO@h;CC{;M($I&^1nfu^Onab%jGmIOES%z;4%qK&3)vQ564SXz4+a#~ zbymzCrkptu*#om7zRv8ij{L+)MOAFHSOb!z1o5ei)`m0FNXJ$vzYeNYNOR&QcuV8$ z2~e&hy<^ZC%T@H*cos|pl0SNU#O4N?)+|Mpa#dyXo%lfoPX# zuVjU{y?9sf+O0Az%rScYB+60Hf?ozpfCY>>U_2&tzP6;5|tbFy4;L1X#paP59)lIQFuHa^kDI@BPw4IK< z-*cV6bbNE*AP&O5P~EJNWLwqp*$pV9ZI28CU0UWC5BnMZP@MN0BI%J~$em@#NfaKz z|4|afGd4Gq?+npKt95gUOy&0RsCsH-?D!NM!gC~LvnNf_XpnuQBK!EwQ*P<1<1?Ui$#ZlkdbM@UErMDuOgj_a^x_G3l9(}WALDhEaV^j zufw9UVtJ(2r(8K+X)BYQKvH&8WYK`x5dMJS2yBWZ!;ATdqoU)4Y9*But4`Wo(TuwZ zw&qn<9`ZSdD#ijqRv3NQ(zjq7PUg05oF!jsNRT+$``-*v_V)~RRdI4Dyaq;YMbQPJ7uUJ-|EDAUf2 zl*Td~#m~e#pO8PevZrPLgx1L@fr5>5M|@179X$fr$qZR|G9YxHbA-C;n3s_;=C02S ziVM(|J9GoFfuZLYO6?kDB^Il#u}#rRPUzq^9HQOWktnEHy4|sgwhfZ*QX*|dx+YiS zF2L#hsFHe(q^%4IZ^WSJ_W*4q9Ug=Eb$-u}QQh5~!{})i>YL=4v8Z|FwtFq_+t6`= z_v9wOjCvQ!@*V{X=q0AetTuz2vUy<2;5clvTCUGg4?jZtA+w6~1Foc$lfgJ`2(C<| zn{1keiN%;)bSLh@C>R-uBRCyIy@!_o04W7s8~*v7CMN>lu^jMD~3$ z1c6p4JwMmhCv*wnx!dLDkH{MiE`v*cP#ElclIP#OcD@-1t57s17!ohXpqcf%`PL&Q z$)35`*?O(WiI=8ph^3ML*-6Sfi<*I%i+!OfAfkro$`z^BdT^hUvwSPL^ZU}1*s2G6 z#~SF*Rt^43z8o-;;g6*`@}r17&uff149Wy%+>K+rqW`ollRuX0!A4HO7>GT9c@-7G zA;j||75&G}P&8hcF+tSB z(4=JCk_{>eigJa&ToQjN+i#n~IDKjpK8xqvI(6?ee=UPuC5|L&g1)SNsmWRRQX@VE zs4uby^aF0O!uzu9IBe*Sme?gv2NjZ>B*DB*d454aVJ{flsg5|PdS`Vla+~sVz)P=d zKZ%Sgabb@~bhrHZ1kYkQFbw{adGFe&ES+Ce=umH#5>d0cu6WN|7=i+Fr7z z5QRRF@QFb3FrU3RC%72JC3%=K=2NCZ{i#azYbAB%P6{dNG%}MxKbq6W^)!3Bj)Fmv zeP(|$P2;qtxNzq|c59V1@T0^u&CCAvwXb1Vm`=txS7Ym@B^c%*lu`0SzjL6cfv~W! z_w@*FYoW3-`Z+=P*XRdaXWroyKK!XN5=8X`N_%KGHmg3MkISB94h!gwPr=7xq8mI4 znAjnBrF~HrZCPd3sn%3*b%c)h@SgrM9@O~Ythw|h?J0d{mNYcBWQg~|<2 zqL~N0l;l9ao>VbR$wyfhSXfx}6#07csdj<)Zbzhz<7sKro_8yabB9UbL}ekabv*z~-yCS(r2}pnV-tKXQNgQ7hXnL4oJT#}3dTT9}cxlVq-k6YWL5 z%Nr!g`?4n+*L7|cB6&sYYJ&H%Qq_)mXVK@n6ZgPU1QiBo!un^BtC(Z zL%uz!A}x{aNpeGEl|T)D+P!M?_5tA3C^kHVP$t_B9qaN;7Dac7RO#5Cy3viIDWot_ zEQ=4_n<1-OEuQTVzUb<#t)A}hSBgBHc1~H`pWe+e1hd=C%3LBpEV89fml#guAG$%v zBQD#UYt)aeBzEPf%$+ui8zt1l+<_JRj;uA3Px_>{UO4ZSL z1@l|mx0G+CUt!f6dxG2Cq;_`ihBPN_@7b5{&&!@$A5bX!wrm0rdRzMiI_jJ_?9TbY zhi?BZ+<$hD)tMxl;CKf_J_VtP{4)cczV+R{B~FP7&1{N%M|$BYebW~gnx{TZCnni2 zc|Bugrb$P8n%ve8*CVC>tJ=TH9X7pfO(Vq0b@=jE zd|lDNuycB$I2ck?NzRh-yf?|O+{Mxj!6C9H4i@1-qVSS|SwfnnxW?LtgSkv6tGq48 zx!P@Yb5t_iA0GTr13jNQV#36ZIstPXQLL8xRqHTPhK`jJe4dAMY zEMx9iu4eH4X@+NA#JGH5NuUqq0{_|N^wHfCR}0GOm1pr}@zBKQzTBs z;VUMztZ7DHhWnq^rP$PdV3l7mwF7)jH$byVk2=K}(Mc4r0C*v8rfLPdwS>XuxMzs%z2#;igsXr!M57uGCrAXzbKh+DuKmEG^x5K8bR zcS7ne%&HvK5x@|gpg04{0mmmD8UI#IY5jaDUVu7UG}NSacVK8gQyX9C#<`@P8Qh-F z?#Dy34`Vn~CTFOt;;Qs?qxXQw)Ys~MY5I~Q@EE1zoNIg z>Q%xLC`zTmk%v^sJPBvN0Hq)|M@MqdM;sZ*Zz$yTCS!-x4LA1f#S}mb(gAvqKaRdj z+MU_8zUEqEmOTD0!HqCzDSJRsv{PK)xO!Ci;wCm+#N^wj%cwJ?jrYX=egRuGb1zSm zg1Tt^c_6~eh{ZkxDb8(%j$>ye3gZa4xifiPE#A+uU82qcDXaWHpiK|fCzD)>s+RzD zL-m^-l+gwQop{eO4@EP6g;RivYRw}2UnLKc_fwMz`;N4AX=-izI1(zbQenS^OGt=Xi zx45z2 zGOU9AYH&nm0btq(L9XJh4y>bvtq2CyCI*HFgj>k%N>DR8GLmW_j~|S)SjCpK^zNh4 zP^oEOyq1cxW}HmmhH@@IoS?GKXyVqWFK*NYE?gzgXpp(~nG5bS39O!ryae|}%*uGX zh9o#91bb`kB`%^KPswR3)H`Ju@O^|%Zg&ALP!Sgq8tP5afPTprU@}nPPLQjo^{jBx z6?>cST#88DNXS*Q=%^tpOE;YQ&6m$rNzUx*xHx$O%^glTHICpQ8byNd*yZMVf<3Y2 zFgfNH(!f149t!DNk1K|8f!%z`s1F2T?b;v%5U3WATkvOlpdPPWHOq)$puN$cA+z;7E2tS=UiGull; zJYOn*94n%io8cJch|?nYE+AJFi!<|XwPh+r>vN_i1&)+2jqIRGsS%l~x9iVfDi7M+ z_M;7oR-PpLq5dOB?y=xL5P;^hD*V$#W^=!;#h~`yZrEG;B5^5L7cUam@X}VuNDI zD)-B~&la}x-t9uWxVgq@J-JgQW=!Zktun!b$K=NeMn)CccNfT4xK|-n%6k3}WhooX zf+~=(GVyUva%$IOf9O{F4U_yPc*c6iYSa1y!MkiiL319Y^~rdDDd&P@%7pl}dd1nX zT=Tq+g7<~&Uqtx)8=7w&TNt^A?>5T4S6?3B2qoB+2E|*mk5taDx)YGSsTCu9>~rK( zVS&h*Ej1@8vAbT8O1kfCsbLuG213|pCRcKtQwKK%E~QD&%J^m#+>d+J8~<$WCYmTw zr7-6EBI^00#>JsOxx^R=35k5|rN^dK>&3WwR$?^nGN?+~OhxJ=yRBFdGcu z`ZA+Rib=o_YG-ubrSU+!CZH^a^sFGH%PQ%RtAD#?l< zgbY)fLx7UGs_x|;r;SWv<@ONi-7#-swsMp1zHh~~IBN~YcncsZ<41rXg?W&ah39BQ zrH(xd)X2arjR#VLJljWVBu($vnBL**wYCbhT^AL951aX97EF-_@v4`6$cg>o1(3!s zvG5$?_5hD8PK)^}*VAGslQP9sZvAxkdky&RquTbxA^UO~Tc$^`Lha%ELl_Qu z*5(4Z$jG;3_<&9}x#pTbgemC_DVbDCqsPj7D-^y$npG@kc6Swvv!!ihZX&gJntg{L zCK;CH`VQ0h6rXVFh_4VYLFk$x`v540eIzvUspF*&4r>#cZzWe*lE21tig6y75_xRV zFt$(jC48HKIdH_Y`;AU?I-2Zl77-s|uhybd^=X_c z^7vh`acL$NgGGa)nI)QSlvW|=_pNY3BZ(4zIu1Q2)(u>P!w`^`XdrL#_J%i)HK+&z zlcCFp#K{=cqSO|w{14!iJ)8{nx2l)4SVzyrC~+p6(j_wYi`7-WuBX#&aDGNQ_-Dr#D*Uh_ zu8nyU;h`B#=3I_rwL5YY>f9=DFduo>z^`8|GE9x2M*)Fw- zKVNE@%1wV=oVu@BvRhb>qIhf8H9;Ro`>=Oa-!y}H{S)tq-Om15{n=W|+Rk^>@zvOM=@>>U;}VePmdwg0gZ%W3fn;AQm`uE48aIMgFHEU!aP zsqnYnE75sX*~QzZ>hWv4F6$s_-ng$bN(&sRUqcLu0Phy*Dok&JuB1t^#DL`W2%S(R<1V#^quW2DvyD$wkfce8Aty0HP^hA-)PgmLGl)XX;rpVbrd3G%YEa?wMNlelCA% zDWQW>^`Y^vkcfA}GYU?RbqB}YfLmW-lGze62M_M%xT;_wCP!!gr`yxd z4}5DHWU^zDS;dHaYG?hrdJ2UR?904dm1saX4(~ffd8A?S?F`Z6p%~;^?kWlp*cCas z+*&fjkJ{I7j?_}LL8suG*$rKXR#Jg`mSF1Se5jwS^{41hOMR%*f^VleHg62#$6h(l z_lZfc4k~*BGzc88)_D6f+nuFk81k3N2>%O{GKrBMtVO-U&e%4-RJ=+rpXDPkue;uT zF`Pdj!neaiX_A{W4R=e$N^SC25hiY}?i_K?Ip2y+Z3CkmQ@Veaf5skwg00(9B@Yop z_U?NN>-N!LJP3{;sxs0(UTh{k0PewgQ3qzhDvQ!yIQW^qk1;(4f+mq2G%F zd<&~?d2Y202U9(-SDj@ll+rU}ak1lXN5rpR>MUQM$ISFM!i&;D^gCTG3a8Os?ZKb2 zB__pI$TNng6T{zZ<9QfW-L}>z%X6j)kMO;Q5qJ7^QerS}O*>m8qeHA%)W|qm599xu z5NjsB_efZuX1Ko}@0eG8y`PSDUS3fn9qW8gY&v_h19ZTdh|?`KA^Y0D+iS7n2mlt3 zo%;M~YRSr|+HWr(P$^W@Qy~x)8PuCPd2*U}w;f>64HVrlAKWHJ!VVC<&vWD3SLkSQ65K|q7>%d(fqyG@%ux@-IL`py$Z ze2Jzvx$~Zt*R|qRF45SzXJzLm^I$*M`vIVqi8ZO%nh*cI6mXfltAe~x$ulXVJDKs2 zMqN(ar&Q`)clUK0Wv1G#?~1MuQ!9eOIxKI3jj6RZ%D zI@bq-kRGRr=XDbLx=jBs#d3ayc}vxe2j4fskL0pOzYm`N$pG{!Ai;jBxBniXVq0kf z!x(EU`fy1|?8CQh&&O!vH#(TT@J1}?^$WkWsk*msl9jSx#yc@>yD6Y-c_tyMxVLI! zEu|VwE#a39<=L|ZV*>pl^i5#u9z$0taUYO4Jho9>Zac0{f4#LHm;)>0VDbljxe3-6 zA@!$v2ecq&6jOtZ6)ZjIldZtMnBgmLg|*?uF5KP^#kzJJMEDvN-H!ysK% zi|Iwazu|HlZJcQqRr2exn=b4s@3SzJs=0Alz7<4AhsHTq21-Wuq%Bci#^Po5byDEF zoy)`s{Q!ppFOb~>ol;h2XYGqjsP&u~tNvzY_9@)vux)C(;`=teuja-v!oz9ynLAx# z4w$61tL{^t1Q0l+7J|~Gb5Ta%4m+2-O7`2g_1wzqUTfCc*}iRmvV@2n@FB+H0c!BekiN}Dk>V7X5? ztIm;&n^+ULGYo8heL{YW;#%%g>B5)cP!+rpM`^UbyX5-w(Hn#LoMB%;jpWBEp|wv} zDYgta9b_eJNTYFR=0ZImawDy#L#J>!$N^E2rG5QR%uCN0y&20n}aUnjuUk#0%>5M`BA;z${MLwc+36f7F5P$*Az~ z@DHeKZiZwtL$5=T%o7f6==H^a`c&dU=zfKE(m%jBs-lfj{MfC+hBeEqxf{8QFP>aV zGOXrAUNCO9nt4{}?+KPjp@(`1_;vv^`7&-Y@tozbhh*b zd~~yKzdspX`U20k_BJ~bj;Qg26!T20ytqBecNN$Ey_DOvWrp8|1EXjN~ z0H~O{8|dg&{%Mr20jbC^wQ!cFV{U%E{dSOEx!jD;MQ-X>^XH8Bk-ZXJ&nH(K9cwt% zyvYyJ1T)$xOHp4^0kZOEeCS-F_Bu14v4W+JOgS0s`9`ZLSl|_Jo%tQteR5E@oB`G{ zxK}wxLwTfD1x%`*^D-$v9zQ1)R))j&xy+Q^{0&!Jie{%ky?>k>cFO#(maDf1B?C;@ zUe7APgz0QMy2Pbvc1KKgD)w}VT)=-RFJxJrz!JS_b;ao&r?(XUj^ZUF0p6T5jsYU> zIiVa@chHLGJvpH2@yBYPcUM}35*OJK^R3btIuBl5-B@h7qrX{_FG3tjI@ZD$i$V`v zsT|sC-sX;U?sLU2_CI=O&Si+dl}KGvg9g9~oj0$s@gK}6-`Nsvn@0+dl>zwl<3T$B zPXtrDgd5Q&h_7yM*<(e00XxNLyT+A6RN$R)kc)_trk=z&Q#~f5&_e6Pr;4)x zunebKGxD~xCQku^YsdG68^ij@hf4PIlvIT~54DD@{z-i@PT|-|>?JU(rsbX6v6&~$Z(=z1z;JB5l`AGn z5v2h%(m9F)yICX4TNZu33|`Y>0dMFmX}ig0 z%sCeqkShg;5E1K!hve@*Q{2S0F()cWYVKQ(kNGhzTr#W95+lnj0q#&D<^9!eR+5al zK6G0|3kI6?)|)S#*iwHiun0E#*# zz|gy^g^Q}IomJL<@qVj2;9b!q#KX=aHH?sS$SaAxVNCFQ+1xFPxDvnQX zJ4u`*&FjHjMt&ai{gGVg`uVssnBj2bwpQJ=1|&;LE7rI3{DsrkxmB#0#``GyioQq8 z4b(%Lof;#d(;mq$zVwh8*IpUcOtd{O217C8s8!l7OTfwA!<9zaG1R=|+E@PQwD zx8n+Jzq@}C_2sVb`m_TZEUd|j3H85U4QCZj`X*Kcqtt*+RTuGFOo`oK#F`1%9^f9U`V}rM=523Uw)1{x z|3LZfu*X7j#6(s9+9uk+EBfA!a|p7jAi2bW^fd$9lboD1s!f6smANO?EC?mX+-b_Y z+Akezpz2@M&M8zIrnvY_o7IVEufoXjX%K+kLPvb1N5bN0jb7NDpABcXVs<~oX-jvN zKYy^kU#%JP@{3T^`^*=?K8eR~ZmDSF;i+1PR;w+xGR36g5M_`L z3V;|)g8b9bl_^$w)_8%9Jr0Ro&7|I>OSAJw^#+&YGK;$0_CvnJ>`kf~K1xX{^&WR5 z=@FkvdWG=pZ)D5o59Qr(97D!~wj?#9dMs4ViUICP4Ljb=WKboAF_u`K)>K%cRsuDVv zgzV^}mS@N`jw=9PDF*lF50e97@^{z+`)d$km@RK=xGHPua3=N{n}v2<6t6ZtST165 zOfS`;Ul}i{{&YipB}|0s-dY1tF^JZABPol$6wIwTwRE_dB2tHOc19z=x-DS>^DYh# z;zQ$#$@~hu|K7;B^yI-n$3y1dU@ z|K-xj>ZZz=?Xrdmy&L<$S8RAHi+@lPAB^eJ9;8=7_YfT(7uDE^1qTdQaHM>cS!slv zHA+W4v;NLu0VU+b^8?tfy&+IQezoK*K(i_i@_#G1?voQd-+3Ovw#$hb9A?zonQn9v z2QcU6;ujEwe|-xzAk_BwsxlMD*hLyYyOc7>u7UsT1Qs1m06313I`Ijn7h{IaYd$oI`qX*`LSRc`%IOP)-o9=1Umpz(b|BDDT zl!q=*#WX_FjeZ&m$t&;NuN!;zL|r{0YIkWTT-@*Lf7P>TZ`o}QeWbZ6w=%)anb-1}8+GU@N% z5#?w`vu*9O+rAiHV|6zdP>&|mPIZ?BTE44F0EbE{p8jxHB!{^ODuiI;Ogx0yq-c;EbH1(6Sg(Evhep@f-aWD`Q?NZ<#1rhPhQv*Jns7xAP0vsI-C(|A>D+fk%kn&C zYMm5g``nB4llV~4olcq?7seBTkn%i<_8gSDjK^j7T>zZGR7o+_A!yi}b-d!=Q|8fq zrQz=w@wF?~=X&4;p*?NMC&cJ6Akue3=bB47ce$87XPO0DElcwZ{XLRNd5+{-O%#6s zTM(poQ~~AHSkB33Y06xt;&<-?powZQ4k#1YR9MQ!UuExlP{{AivOai{)NG-QV(Li7Gw+RS^3NXq0#)6x^bcsApGJe!)5A!~)4xx6@X%PqD1@Rw<^ zmEhpMUS}Tg$J7N*e@_-&lBwA8!lu3toQp-Aj8sItK;m_{wO!$q7>MQMnBUPLjfeOz!k|$e={C7DcV!aDrW;3PB(te% zPy}8YiR$a1(#<9RS%Xm0U*2P&PyZ4uO-6yH``|&j0~`HE*|(`w4IGD=D%kzypCCI0 zPf_IMU%Q1~E)ID_A_*Vu*MOUl`i?jSl)hvN>4~JLP0MbssLU=* z4fgcttAP9w1wq~)fp&dB5#-p*B$aMOW}^g?;yAu-zi1(%YU0hm=ZT5!+6kUVBr@VQ zH)O4xyAYZA)>zuJx-z+I_UIBO@n4eqWY!rgDE&8tO3)lAP@^{kJ`3DH8>1L&EK5Jr zSUjFb&rQ?7X!k}wDRA<9s{3cVwEsIrzuqi`u4}tSi_Vg}k(r9!-2MmXIEm|t>1`1t zjc<>L@2!6HNq^_b2Ht!jzaKo3WHn*gTlS>(5j2ri^jlKwzd!!wS4k-6k5J4W?);94 zBN|4aanC}WL(2%OEDq#ZxS^IRDVrk`WHp-qp^~kCm$v8qZHq-@2DYd-omiiM=Svku z`DmdNj#@-nq~aK_cFaO0_A;V4zQ0N?e7=#U6TrwAjGgJk_|iG z6m$XU9BNT4ZO6S3?O&?brz$v*PWLyU`17LjL>Zw)BR5uibbRjs5fy|g_b33jPnV9|!VdWrfbUJq%T0zxP4~mfQB7opIlDZ*2wQUL zvTRnX(n^hxAF~Vo2;x%#;T(SZC8j`uB=wZ?da$Gc1BC&C<-;86=kwT^WIoU!+%I<36iMPl)hb0^sYZbpi=_8-C6;^&Lf8DmmbDnd#S(`h@1d=(Q#}LNCkz{|S0nyFjTHA}d6=SDQ`}P3u}3Rk3JIFfiX1kyx-g*L@-|Tar_P z+v$1r@B7y67x62F@2}mN%!jOxmg7{duaAO0drPlpzIH`S&daNJu62ta54qTQoVTF? z#Jgj{7X=Bl3}NCgT{0mbtfj4}&;Y_-=|Hcqz6{@d=jpu8g*GcSE>))hK9NvD6MfwR z*Ss;pR7U-nYWcM<_XF8i7QCwJ-UR4Ps?Rf=97<$^3Rvpd=r{a4i?LYN@iZKoD&B&Q zYsvs9N^|ejl@Ffq-6^jCIR_T_5XrBGGCx_hI&*e+L*T-x80TXOfSC@VH^w%7jQ-a0?WV1>9%y;S&LDgdbW)M9cqUQ zmfG$1uLkG0^KaYE_T>gx&$t|##)GaD9K19WwD0()QaJcg2pP9-p0V?{A4b-UA)d2^Y05H)Ih1;D$oB z8JS{sW$QE5UvPla?dnwfGUkg3Y)onBZ@>>YFMXQM#qVdE38|%sRrH{PWWGU}YKAlV znHP+J8wCv2w5LiiQk}AE00FM06jp0HOB6$YT3I5=7420 zw}O7a@M#Igcf5GlEs|`8qE($ICD0iG^EOh&sF#FiP@sMUb(VbpJ4=$WVXmeb?q;cC z#=qjk25CTch8*ZhiU9F=ns)iP^MWIIYlCYGu1gpStH&= z-&x}(ZfsHvrgGvd6(4!zIcLh-58iPuxq1R;jz=Phgdgx;*Bh8urebqD!V~EqX2#lp zp^{EGkF*5F(`LLEMtf_8vC$jvy>9sN*!+&P;JXd<1S9>Y9wbP8ZEm39tD?eADAmNj zR`HO>l}5^d!Vq4pcrX{Fq^o8Yztn$zNvGjusOalj&vsbJ*F6PmB*=#Y)Max-HH~u& zr3`>-W`ZF`Xl9(v_ZG4f3o6@n`9JOGD`|H+S$jfC1dqS^2PRJxzy4r~F1_ePC9p6+ zZy6SF<8<{NH;TeHYyx^h8O%g;HQD*#!e2oJnZ&8(O?LVn@JSb+4k9ek+-O&-Xu$%% z?dQbj0Ug+Jn8d*s@|kbL>d+8oL_K@CQ&x$j#YZHdGu@j=;#`9|sXyE0^u7uk{Uual zFvx8KA%SlfYa}~22pW+VGh_6|U8j4glRvEs)U70nqU-DpSbG7h({?^Yp<|_Q>PKxL zdZa5t+ARm;f~OJx9uC3Q!`W|7yoE?k**^E*01Ggt0VEC?&z)sF`PIm3%ygG(QCgs? z6PquPoh@PcH1n+M5tgtH;+YyCC#Q5>jWn<`*p3K{RjF^&#vgVH1 zm?Xx~0Xn@PO>OvcO0&>Iht{*Na(@coY1HJx*SWY3e|B@oT!Xu9s?UYvUNKj-;59MxCE@j1 zjWy9D=>02U1si8)HkkTb$Az#su|;1mA1Bjyoes08M!*#P8aEXwKBn|UXOqt`9rwD3 z`|7EvoH8o7{IdHUh#-+gJvy|8E#>vQICB0!_ zIw8ymLx9o~+$$wlTW9K;V!rMeCWZa*;Q#XyV) z(Lv5H?R@&&MvZW4Z*>F&UVyvIzt2WXd6f95Vjl!gw^kV91QOrri0_lAsGWMncOo&cj8rP=8~m?C(m^B(?@x? z4!s+;4&ks?5+^{_c#^OIlekjZR6k;V(q_L&Vkz~S{B17OcTZVrR(jNOGj>Yq0L8z< zHBQB*zz)MM-}jm_CDi&rbunR38N`Fl>9mw>T}ocgbm~sY!f1ARSkj(v+c2J6BXk@| z1U`BKqkx-+^#xxKyDT4i%Gcp6feOhu7_juq&4+c$HPDzM@nqaVB#@rH17v;`v90`n zhtf#J{cE=CsIe7)s9glpNye4}_)tq(=9bKoJEI?i+Lor&<%P@|dFZ?eCw_32A_ltt zjnxWQ_r_R8)!H*DW5E$S?p$ep0``1r5dIVJe{ynvI`qV+zLg@kFPn|`36R?o;Cml? zT$dTZY)9e$!BnP}!T5~6Lk6c6K5@?Pes-X5r?x8(ST+GW>YEUP)mib$zn3dGIlaae z491;{`#&oA&X2TM=2KBz2d<|j;G?EJUUn}*Yin$?&^K7$Q78b6!XR%I`4RuHi5aDl zkIcJ+tJ0_Hpk0y({}yX#A7dX;J_Vh}lK`)TL0~sY5e8{!sKZ{^Kip-VmtMxqUgwU& ze5gzs4$EgxyQoF8IUfb+HO3fdC(GUFhM!gy6c$6-J(we^RpB;i_^v;h0S*)-*{8{z zHSw1@tAbrYywhr}Giu5TB`kkIN=eW|ow7w1dr9j-pajwSN+` zVlBpTEyMZ-UyfK{K_?gECp5JTim1+LO*Em9xd)J70H%c(<(|r$zZq$u&d88wIKtdv zlUL&px}W~?=>R4t%bJ@QdB9=zS=I{^{RXZr<3oP+(#6r{lqp$wds(o4bS1NPho#UM zdUTKmp?o+^bTyn%B48LKC)b%u%$8eP`Sv5+Hg}&W-ISB`2MU6^jBgp2PmQ$Q1KBHJ1K&8!bgfA;o!6p z5=c%XuGILYaM6IM+AWt10sr1ELb#k$Em!XZ=Vj@3De%;9=SA!TudPC7MKpnu6AFc& zQpZ#Z-udw-&HlKt@a{%mC}I5zMe>7`tQb~aWNns$OTUe&`kY-Imk4p3=y=sMb>Eyh zymY5M`8nl=W)U8996x>jtdB#x*Q5yn&09+v-``s_wa0#yZMwG>TQg&UC6~q8Z*H7L zR-wKudjw7K@wTF;+KKvkJ)j<4XNuJALd?ZlVq3YT_&asQ&ye)&}X;F8*%t*40TvuV7-NGX4 zTFRX7dkQ0Ft{SIHGJArrgo9SJrLc7g2R@|B&+0Xpe__NvRZdLE&yo;|)LnbJGc3Ds zj@P>TDpxs#eXKEs}Nm z^5mrs3?h&Y=f7YVLDcqr{n%a0PregOp+P%XsV?Sqs$YQ!^^`6mLm#+{xeGU>snL3E zPlA4gJa&5g6A76u*&Y4p22sKFjC)4`HyL`%vpm|MQN3DuWiWG_0) zJ5#%NjrlGN+PZe)bmpTk8S<7z)hLniE!v$>8GUx<<1E&r z>+|*LhyD-BKOV6q8cspG*X^OAAG_Bl*k7;~O!nD=qx8ReK7$X75-d_u6!qCtY~ez1 znsQ$bd8Y7@vU$k4&_(UknDw*>+vXD$C8XHhRobqU{uB%!hwBIWyr@(eV@obYAyCJHhl@0dHRWOvH`+-3mcngjR`o@xY@7uwwk8im{3*S*GWM9PIL(+`h-Zys)$sDyIs`*8w% zmo%ko^O9*C)l(O`oyw+^mBZL9lZnT2fp5L>RqH39MNUaJg54=KA&hPTALYP4RiC|Uol-TCzaf6 z=5n>Y4&-$T53^Ar^}RHWz0;&v^goQ?l7GOGZ$Q;W{;TCtqTNetVJ3`XDn@VMr*GgL zYjmZPx|2dyw^ZF&pWAGRSm83$K4=N=ecpVcUaD?1#NR*I|JjGOK-Y(@bpS3-;$#9cQ1R_tt zM_%t|rLfdh++0$({?ie4t!6Z$P>rptQivJ1(H4F z?Y(TixsjiA=BCYBIIw<{9~JGe^!GS%FEwq<;qnlh(zqAElC3l`enx&*3(;GUIA-ki zsY1l4PW7UtHdNE$laul1cSKJ$+|K4&)IR0f*&xSZ4dp5w=ww`o9+XO$aG5rPTV)RL zR`B`C+Ote_!pkvTACC!-1@1Z&?;lc%Wz=^=}B zn#1TNXO-i6gxO)VZt{#nwQ`D(6U&4X0@&%7vF@eG&Mc&H*SA;9t{VJpO{q@Fyivae z*9dI6$1B-+I`of*zv$&Aj9=B(ZOYXdh@vJMc1tgBia!(xj8!y+p`~?s=M~404@4Kuc&juE$BdPfKKl8r zN8)K{Ysp;4h)FnC66j;ndM;~drSTAn|J{WKzG4zHySz*gY4~lD?iq7)dr1~qtApU1 zN|@6q{R=$$Z=L%D59khsA|Mrp_nOiiYg4G&DP?WP#llB zZx4gSy>LP|S@BRewVdI*;#Q@@a(->@keqbPOlJN#BhEeu|KW#;P!RtOlT^8wiSR<= zy3E-JCVGq5yZ|0a*SG4bC*x4;&F`T=)?&AGD|fwbT^Y+Mr*0XPdGI*q=$@%$Zv37? zwkCf(b#4FZn4s$yZ*ma+Saa_%h^>Nm<1l!v8ubZ|%!jYRAsf$kv3^KG8xrC1SB0^z05u+o`{zzs{^afhEqM3uXokzFd90s<#kHb>=$It z!nI4MdkVe>+s*CSo?7bJ z1(fTb#kTBIPz4QI#Dq9Oj=us8Ab@R?#iT`Htj3s|Z%Z0Et7(?Rn3iIa{GHnHx~&)$ ztiXOOJEis6nsWAIn`NVq%G1meC(B$Vo^;GWUQTB*6-EqtpG%4_)3EYCw4Z08Ny=nC zRdH+bR`|p|=RM(f)!HV~XYArL(xo|{Ta6#P_8DeL=l!evL-}Fyq{w3)A7|jD(f{_Y z4+2$IfYg@Z!YQKSbd z`t7yr?v7J}?Dwb6ejdid%Dp`YezHB15Fs~bf2IhR-%mz~N-f7=;tH`DWb`A5d3>>X z)u()oGL6QVP?=fah6QcUf6r;+I%Y|Ei9b`u43Jug_=%4Be3%?+Yr3UMs7Oh*3X-%^ zZ~;W%i_aRU8Tm8`JCYtKxupQQNNQ2=c^1H_ragu(SOb>qA;o@ za%tOiga3xp+*k6&BU0~Nqp)^b@sANhnHpORJ6*V|N($J>p4Uo0G9&a_Gxi$=U`Ah? z*tFdvNj)>5lM0xhpLmlgyOQW|osY)&&EnwU&yq*rZYJP`xuC7!EbFC8M!HO$88aHR zt{)n%wttIP=RYJ%gxoc19s1wOgpN;glqd2RlCHY!jGX zBvMY(VW}`htHflZA7(0fbNRQx$nd5)_H7D%Fxf#1vqAh-ny!N^J*dq#0W}jlfe6t} zXY!GZHC_I2%~!%iip3KHNZdli6GTQPtFFqeOFJU+%F8+!{iNdf{IKw}BiJ7kv}!z^ zY#d!?bddXbu)P)}GbJrd%S=&5h{Ut6ADk^v<5z+uC#e_A*2;Kj#V7wg8W&YEvruEc zzW7KQ+T{wvh{r)FK{Buio%Sz}R1!GiM54`P*tO&$L}$bBv?|Tn_LM~%XGMqOcuX@c z<@Af^<_j4fZLjIFnmLWxMdDca-8N(4X^f>1eVoYgt?wRS^_bquGvTD|hu<29zV-wP z8kGwX!x5N*v`=3L7xW!q;skt+3kF^-8K*(mM~TICu#qy1TpPDyb~_5Mom|V`Mb9>2 zuBa*v^x}M$0$?yIv+4PH_g3#S>zf}5Rzt*j zGeul=8;q60O!;^(vwQWYH~ekwIleuS1sHz?qU!^$BxHupo#dZ84ZEfZ{ryaM z`kNCh(__&HWG{Wcu!g**21HE;Ucs6HJ*pNAys`mCK${5h7XvHX(a$=-j;wh9l9iCJ z-|FCCL-%d7E3TgM%83K}Xj`*J0xY3Q1u`b`m1IJrv>7thbQHtfb2c9~a*fWG6zy_} z$WKsmAU!dH?N^BYm{PbZS%QPG^B(TvJAy^X`#{fJZK{N0OB=71A zEI9<^J}?{xvnaI-5&sw#fG2q@Oi!-DG??!U)6o9Y+s;mt*yLlL+s(Uj42yHQEL>7A zj7)eg=(dM%{%l#ipvvh<4eV2huiAxPZRe9CU9rj8vtC2sCM%%TFbSb7Q~TNnE*it& zdSZ{TKayk0r^(UKVRby*{)a(hte}CN-9#Db5FL0~G_|RWe&E{B-WQO6_;h5DH#op_ ztpp@r$RARg()p!enpyQigFvl0!@{XHW8R97j1XC@3mo67m$C#Z8f`b_1HQU}X<|b1 z_bEoL8mw=WDSuv-mE>=ne1+V)DJ?h5#j)? zP$|vd?ta`JL;wDe#iqy%M0f1x3%SW)`tX_H8LfT}M?zgxHzU_c|Dh_%5a676wzEm z)CI%n<5zv%yZ&f&f;Xg}?|lpeJd=i1vQUiqVK28`B-gS&`q(Qv<1A3IX<8{4qV<8P z_K*==B8nYq1HbOMCU>au8|3#F`LvE*KMRADP)n7j&(TXV#{6tv#wky?MOMoNOxo^|#{8^&mF!@YIwr+ZU8-~@ z#Os+z`g#Z5S|gFNp``e(E9U?bR0OE3V+OQ6gkaC;Wa5!Oh~*pzKE2e>8O;Oo_u;Md z@W2N8{xw)*KKyy|pIyW3a=KwxeZJJG*8%n&kTo#T`@kw2tysqT^-6cF31@#}`^{dL z7XttP@GQ&Wui)#9zKh90tt@~oe)jvHRV5H#%1<#EU7qVC_Q`L0O&Fam&ECsI$S1sg@0sh1ekq0`{Fa1WCAE9gkqnVziEp6yDFhJoj07WwhDJxr zbU1tt^V_(snTqqZG?n{Zc>6p>@=l}C4?`kQSR%UYX9(MtcjLGEI8S%U_;^%M=p&1Q zc|Ju@&ki8C<&1|~dgsj@W--$KC_sIGN1XNrVxA8Xh?)!W9^60|J2m5*4QuR!qp#$6 zzl8|0&R~>?yk1JyK&mkWNsgwQ4SmKEKa2iEtJrkR;K{;t6j@I4L(ICUO9|IRN=oyi zPkDN`VhXR?vlj$_Mf~d7cY-8sm)(%36lN3$|1n;P6!pte!oi8hr<%6v*?`Ns}|LcF6n^6TvHS2uEnMk<<_+^ylKE4c%4eKT%{wVri9|=KHCpA@>V7j;0 zlr)@^B;fNPl43K9q<^dVp>CBykB{DQy9VQsLIg=$N+#}FeJG{AD#tKKoO|MAkUKjZ zMg!Y~bZMxpW%Y=&jP<~^$!+&~-=SAP{#P30F%ods21O{DL`1%7VnChyt~|e4SpXzLqiE#i`r*#mg zXtLN`*&4#)Bk;x{yrIHxcKo`0(;#XSlEL{f+_@gLGZ zYu#W+QSmyRt0V;uea_bWIE2MvRKspq>Z)f>?pl@DUp8*;;&=L;@>~XO6aqQ|KRSzz zR>8;*3i>`NHSU;!`&}JVveUHOPhGjjR$h@t;s5((%6Uphj(Ic7>%DoK*YWTWA8Y7h znpxH6h_cl<-pk@O=e>0H3h^mgYXQ39&;e*3zP(v;_d_IJuXC;1o4=t40BmVm<*e=Q z<|E-Pt#<0VSjZ&P`LhAxzt?RVt_{}b2TkbdyM=XB06o9)m+8goU8w9yUz zI>5ZwPyD=F#VK5Sl#FaX4Ry6yGf5`;8|`SY9~^U_hR#2D8*hi@k8;)q$~$J-OBdJ& zyC1tNTF6#dI;Yt;~W_XN8nmtF9^& zLT}b1YnrWMb-B&@d0M*1`#VLB#=1=Ut2P9?k51n^*bHN@PmD~BS)W@2qHUD(3-+^0 z#&#SlBEKyZy5z<^4apPa1j8}6{M(;wfqXxe0!o;YPB)!_%$@ihM@Ojbtj^PL+WpqS zIMG=oc_?+&a;2)bIK)$V0h{US^srsG{mf=~CyFKF`4q$AaVV(ZCK%77iLE`}XgJnK zuveb|-wLVVghg=HdP0Z{|5-Mq5ml)R;t%#7$)r1!ArHlO#Nf~_12fX~vNxeq;$(9_3D81{ek0oK%03S*%Y=Oi9?9Z^oeIQ{R? zsZy{{0iKLd*~Lb9;!4SexBdS0U-LWuIJCFec-o?2LI3cVkq=g_fhFT7&VR*aEDzK; zJs)6nvD$cj{T&EhJ-r92 zvswH>TDc=8TLBn9>9cVznYLDWzFtKq@%w^`>N-Iz{dNiAyrM7`z#|A*hu=apXcl!6 z_6C}u5|iuPt*5%T zYRyrw^%XyzMtMItA5C7tm8eqtj&e`{L(yNDQuNK`y@T# zf_@n{d+X5MB!o~`)iK2^a>I2XB!DBi_5l^xfysSXe*r+ zzta$Y)R6!~R@OKzt4$a-?5JLi1Q}X_Hkgp0->^+NSZ01{xi`Kl{S$#3Okz_7XpFQ$ zxG?WSGyI4C1BZ?3Q(D?&JIYT4CfZBW3;~i#5D-8;Lb4bYoFCPtc9n3+Hw0im7XI2@ zrWfmLJ|NxV*;|~;qV6dIxqp^mTr3>^&3sB25j{?XD(VjK9yU2@Rc%|GBXH}+`>zyz z1c9~YiWmD;D>#||j-5T7n#7pdq`d@}#{dmqanK2+=f-Z*t&xMp`g3d`-=+qMl`@%c zO899x-j0Q&3s4=pLfF!ia5R`p<)%emgE8%Fn5TZh3iZciuO5#~(v>qIg6g}jV~}{u zNBYGxDTbM|1eOBXLqkZonwQ|1P@jI6%ZBp zl-a58Xj0cawh+C@NX%U7a$J=2%9l#wz~#ea`K<7_g{5ZnN^sL^H|e=&{m}0f@Nl&J zFQe1;#*-PiKo;4u%GeTM>R(0b(nVgEa#oqpb%GNi%?IXkCMn8KG4qJ{K;|wJ78dKv zzjmDMnC^!d{mH&DTz^8p_gvi({oezAW?Z#$z8dItWX{uF%_pJuC;BCRk$zinTdqz& zOI!2U{6suXp|Y7DB443a&m?Xk$eVxkUK>FO=|PGl;P$5( z{e`*`%=TtgjSMbI%&yz8ega`h{}U0>K(CJrD?}F#F=yfP2!(M^q?bk38&orEs{unzR*4XlytWo|JI$n zsX+YR{XpclY2|2eg4W-jW?R&CjY`**MHQc@J*LG8!bt)`fssmju?25zDzmHN5RERo zTRKptB&Q{0o!>M%SfCPpZ1!=t#{J|nqlcqD%YD~Ig{Rf1b;J@~xL^(3_yzoY7-&Il zrOW!BH^!Gk|D@|PqFdb`*}eIL-Min6aOr_rj8#)3wi(CI9nJ9%4U-NAox|dv+#~W^ zh#o9m*4BmR63vARHU4l5evo^j@mfApoewR2vuwK8SDz+^$VJApgo5~y(D%qifX+sn2fOf~8UJEe;cY&~ zSQiUJR#{#!*k(hIJ*74)-=e)C>xkq?Vtj16A;l-1g{*~rzo-5)=`0%?8{F&8k>Qxi zmgd>PiP_ubk>(=-!Rle~-qM%!w)<#Nh;!;SRG z$+hWa5wC^f?Ihs;8WRXh^4T)$17}74Bz_?5im&;x;F`XjG5fJ>`?(RH@>na9`&~;4 zTGK*vppRo7+)8QdmE|dV$DI4j@Z8d;!}C?UT#_AU5R@=O%tGK$L;S!hRi~SM*qpSb zjFgoweLGJwM0v|`Ta_PhFGY-qhnv?5;v;$0mG%?~Rzh*&C=;ZNyeYkElGvf}@0zNx zD1h%S39*P)o^0{{TJ@e|A$}-NV~VS!BZO2kGk3ja#O1sUtoHc#^e}ve5%2X@%haV< z$aGk`l6ehjP;MqJwL-~hC;GsNcO%;pwE5mv0UHQ78nOFO zG#apsrgijz5n&&v`y|!io7OIFS%Vii6ASGMOCQ%>_d`YfECm91Bj=`ipW@a~Hu**$ z3j1oRWK!uezek(rv5W(w8Tyz1udKHUsH+KFGt=GGRW*vVo}^;EU6@63AR|Vntq&H4X0%s3TRD0K zdlAkBnA;(t%|0K!Ff>!nQoMyqCLloKYh-V@97c`rKv|_ZeO&j zn<#v(N@bZ?D6$0^38EOoT10cUd3|9&Q!O4S$};Rfk#7?TSsL0L-}SY@jCPOt{(k-L zW8AW}?tkR-UIUKIiYfv8>Qv)n`8*un9^T$=<})Gnv{k?8`(gTC3tqm$6T1Te829vK z@(?kX7~a!eiM94Yg&jLV0g3OI)i&t>ma9Id8s_Ga=}hryTZvOD@7)vnjUc>F3#$RR zo>tYZ@Ltl|4{oCJ6O=^vk7~;aK%!CHltGXWeCiBL>=3!>&tgB-6@CykJQ;{>^O}cN z*-#g_F3Yc%bk$EeXmNFkZOZaZjzZ4oW2f6?CJFn~{!5;f=Ix-0!y#E(EaG{*<=Oehwi*g~iE`ztWvFk}>M4McQ z2nnC%Rm?tv0YSMLUUZrwJoB3^nxISc@)CrYm0+eE#yH=gBa5s@vpzBG63-4XlrNouo5&SVbx{4EgCz+ z1d=r}=cLP&>p|xaK@x=pN3~jdOUzF%@WuT;b6V2fzyWM@%W&FMgJd9;Ddz;rHySep3T(2{cL2LnX~t29 z^Va6sG&h>myr*)G-6xmiPmL|7+NbAT`F08AN7g&hiX6JNPOTGQ2Anca083b*pB1=! zK5|`=w`#x7JGlEE4WMrE97Y+;P;(XJgQQro9)vu z20si&d6awzH-hCtJ8@H~a{4_PY_%xeA!G5FJP_KX6d*WDQ+J-tN!~K!+fC1B*@n(@ zw7GfzwlOGU#*=(ft7Z#}eV&n$>r2Fr<#R>D&$v< z+0o(}cfC}1tC)utib?-^8}KJrjQTzlYRC3`5`YV|3-}pV6jeI$KGCHebE+^(zzZm> zrmoZzFdAEbPZWd+A=L6EDMcu=Q7X*b^m)1pX99Y9^&4`_Y=9Pi-5uZw_2I1m8Qy#z zc1#tE@Z*GMW~yYF4R+=}7N%RxMsuYW{ZGEUP!s6c*oy+YSVZJ|eRk9=KJyqBxbdyc z@wmHTVgP&Oo35RV#^t2ZH{*P&EFMB*jx{`l)2d)5`-23~0r(T2GH;UCSVx%i)|&si z+J0{Qc<0Fx>uFZa zeGn&dM%&8^c7@bjVcPq2cZ*G%S7LE+aGTDMnWo}1dYTMOD|sbdq_cbe_q&E#nAcvL zuokQP;(J~ffh7NOq|8D2HEjT&x}~OF>L5**L&>(s_VZ3SM&^3Hn@IybF-UEH~;HUd#SK zOZrG`S&Kb`ISF?j;W30>$E1Qk*oV-gFm6PFm4|`!ErzUuY>%EUB=)?T%`B-vipW4$ z9mMgtmZ(ocOHXg>q-1Yz!M6FN8;Mj`-u>=B@JuC-(g&1+6w(>AWd6O~?{=%O9-0r%~Yp`oGm?7i|b za_u^gzXh#TC)?9s$Bno2a8bglDFrqgc{~>KetjAPpyFVS-aLutt&Dq*W}?Rmt=QL5 z+CPrq#Bdl0VX1k6;sMUy%bZ+=6X^>zUFY=cK#hi*7kn)au2lGRTuD~)*z;+05Fq6t zJxhQk9uA`{sV;w1)CDH2L7P*SowdUdZG5#TiEfx~6(y5h*k+;C!1=uSFzneiyXbRi z(wu*gHy_r9+WYz&q1_xP#y2vUvS)Rht;JcryZ(QcU(f<6Xp3m(Lf^7rZMj4bdAW;b+ z3?VGp{i{7>R_4^0thFf9NbVXZbujpUsVSO{@f`4R_O-FGsa84H3s>y8r_cVaUc9nk zoJ@RBK*Fm2`G;+0@a`zYS%FRP4CTq@lTU^H5GGpSzgmDL@zI0C^lxfsW};S|x9Lwz zgxLy=Gn`Z*LFU66gyk@^#|1Fv)^Y7vAiH#j9V*&<)ct<{*3;7ytrP3c_Xci1eT(pZ zRtUx_tMPe&m{+;Dd?jTh@BC@9V<|nR<%q_(hWvWd1P(bBd1sDICc%{7Hr-(}Va{!i zWbm>1>Qp;C{6Mv z+9kwNut>NC+EbSGtI%)oWvphO5Sw0uzjq}om*45$j~QC#&@RBWxz@UzKvMqrWsjGa zS%KUB& z6BWz`_P;t$@>9k#oQ144rg?~kR&mwVFMC!MI$I#<^kQKYCLBq_0BWmX7f_ zK5(<6O-bnO(jm1IqBO;2A^hm!TZ>?#ZwO?VBmKB=UuRu5(0^u^&2MW~O)p zq~-RX?b7K=P2cQ3*B*~VayD>OM0J$O@8dYoSL|#{qvo8qbJw4}!EWnZS|uGN7zDHm z&#g1sQb{e8wRbQ)|sC%tt6Ftsb=xv(@&4n;h2oUB(f7^ zq1)7=NveVd+l9CjwgoiUj3AdJ$VKG&56R-^aqCVb--zjH8~IOQz}6{~qIUu3j#ia}1|hf5=yN%fH$O zSc5al>1F(^_{j7+!WkT#7wfSqwbI(XGQ@EgE7WrCiaSD=jV5<6NjyHy#Q8^&ci1#)US+b4WMSTG z;V2$Xa?YPBIt8Tvao%hPE}w6wh(X5VUqdYJw-PcY`SQSWOS4oYS9s9IH0purNWhI5kLyBwY#Luu{4SCuv+c#wJ`SqX!5g|9*Ff` zN~W%NEmF0mXph#S%p+P^oP}18K@o;gft+C^kQ)*S_tV{NXXErFq{R|le11Gc9fWaJ z9mJMa`NXaFYKT|TE@$3KX2cfE9rA%&*-~_t=E+m?R1&7Gwia01q}1V7i5jCw69t{xYg>VUJ^DFqrJR~AOSjZ%X)mrF-`{{^TAU#98#ps9s-s9}XuoTX|N125cQDAm_!$Y2 zL(d-HU$3!?jy{PoI-mHRJ7(cKLyXyxnAWcSfd@z%K~<^qF|w-Fv)V)>u3imKvA!+q zi+lPxjrN@vq^y7Mq?fw_ky%l+eLX*Qn$%Z+w!-L0%<9xeB?XyfE1}x{Ag|td7|~fr z?&Gvx6*@euwZcOvNy_86yO+z=AtI)kPl9EhRV|z^N(rxN6v9smF&ui3YU5ocW zS+Tzk{^?P|H4fV;vCyWa!r5`8)cICEHPmup zCiBE<&1MGiBld8rh|Vh9_}8X6e1_(xM?s_uqN+yG5Oos7(eAUw&X1)jt6{ONNArdf z(={q3c`NP+n-zdgZ<|_S;NfgaA`R!!e)CWkGdh5Klc5Rpqp^{q!|lya*5p(xFv=Yn z+2HyYF!*#257qZi)U7d0%*zj1Xh8nVYLA}UrHpd+nGv(yj%q<#@N6w`o#a*!$Z;gy zuzO<$QY!*6Gcxkd?5y(~l8qvXxj7}D%?)t=;i7vMV`*yu35as&5Ilb!kPI^vGjU*@ zO@FC%{$<{(w0vjx*6TC!OKRij)1ck8@av$V;us@SlrNF;ir8nv3b86*dpfSebJ^lt z+5ZT^P(2PKSLHxzsTX3zz4K#nCXXqqEOo%K-pH|+6r>tV33EufbfiHJ2mi;eF3-^g z%Ebz3x2D@*d^%&L6`jA@OST}YD>@--q+um83-`VlFr+2y!#e}Oqmr7$k&a?jAhkyO z@kT@!Vz#~5hXo0$6#x*h(h$M^V#tgk;Xyh)3hwX~Kv|N0vldg7Hv{{9OlI2(i&@W! zlbPEyIdyR`rbcdSO34ri0H5el3&)XAtm~=f&6r)svsFy?h~4^9@h1(fyE(=wdJG~B za+1I3{(XMsK?G8$VT;^AeTJ{+2?6rEbqk-3*1oiU8~$!DQIw}DWTUJ%f;F`s-!US2 zq;JWzE9*Zg9{7YSYa*!PCWs1fQkDQRLFz~4Y%3o^qu@kJ4y#W=^MT4EA=ZjS;c;g|f1hh>q z%^dBf_v4?KkoW6JVIk%h*<_>gKV`P*jHAg{I;GRMKnv=#;}@5BFG3gR)U^V(25OI1 zi*cIzCuFkGiIG7_myH>kEza${ZZnLl#gI9ales{e>f+@cE`Q-_=}HZpkC1>;M17k4 zPrvI%x}5*(7=L={YH24FX@AAd^Ryv1lr9oSd#)3b@?bnb?9F0&X`YV7^&Z&M!gSEp zVP)8JPxaK7b<4Z*pl4`o(C6tsAtd24yDbOEH<4%m+FfGJY^jMQ;zzmq#46c!By>yG! zzmMAlNl(OvU(aBnk4PZI1=EVDcEKGv(@I#Q9x*#?iGGtdfM1i<2Fm92C;EKed|~j%)$? z<>{%>&r|=h!zU5P2be5J1lH@$?>c0y71}yJK3Y7@ydICnW}Ah%mp)J*g3JmPQEzlU zx`X0UkUP5VQJ*%EnAVtq^qM`<&p2cHQ0`gV$GLd{Ir;R11?|C%T3kHYsqa| zV!QfZSu)99N4~&NUW#2FD0&B|tPnpIb$&VaRQAB(iyJZUq**IHFLEWLYhpP|)plBT<7WS_Gf{qU_sdp~$@dAur5f#wOVK=NM-z>kB+x=+R|<5GW~-EBQ5g$Z)2JGaEDaQP$>BR&8e(QZJi(+Iq;99&!n zek>JF9Yn>))Y`97d?OZOMJ+%+Il9+?J@fQ4f?P9Hzn)yjjQCrWnU0PQ3|s?%Slaz* z6pg)aA=X;3RF!xIbnWi0^a(|=ca{#FjMk*&pZAu8$s7qOnwFn^Hwltj`4t;_qhy3? z03i>#Dtt+ldwq`mJRQ*R|bBm}eZ9a;*1&F%eBxNA#!6|5J2ez>>~C+q8T8IY*%t70*Uz|lU) z*2@lf%%MjRzGxL4@eQ`GG_CCc8FA9goTE460n}6(8a&^p^|BiDPtP-e(1)2sskORn z9)_Xq>()=rvpT~LBZHZC@>gzFH1@t5~q+;n7m*Mj5{6v)-s)zm6iV z6^J7=9DOT(*C6~3u9E#G?X?t?4*H!ez;rV`{TCgI-wDx-op)<9u=y-hZFiPQELKlA zsSx9}xeZ66B!xApvL{K+s)*%<(OLe2P#~YF;zz>4SvrKJ@3^mSVt;L(mvk zd|3zws`Vqj1<*t}6=g2T=8u@gM{6^z{x8=qh0sN2+Rn)`j=WO@r*_t*&C?XwM6s(_ z3^I7enkJDdiMVB`(C(?C2hmHBWPM3%easi;z24|01G83y2R)1$w$o@JExgb7T97?R z4B=lktLCiu7*FF4FcYX3?iV}Ro*chF$kL*WrjL!*r4(e?efob4fK-`+niJ>IF#;O* z)_Sx~sc6E8_=b=%7NHvob3LKO{TR}1{ftTdj{uf5QtBy;b9gkKad`!b&$RhI4Fy{I zfDC90Fes+BA01L8XPZPGP8+rll9w>Kq*SEphEdFo>$0iyu_}SiQrpSQQG?)rT!7~& zDg@P1w!rgK(Jj(KBTZGBqu2L;1!>;7sQQ}DL<2vtQEcoz&(`05`rCFl^Xe)Yw#%QO zL$&+Rfp1y-6Bt3=C@=DkBM#EPs7EIs4bMID>;!&Wk=I{eufUh3A+E9P4OTf#_cJ4qm7pEx$FnT$h%7{pJO z;8K^--$`E!*^r;m(TK-^0|MOtsz96ckp_s9o7CzzckcJ8AMw$tPz$2G#Kyh~Ijbt_ zY~55AXDRf*9I6Us^m`fJr#U>`&&y@Z`tb3Q_d$j(-}tLAv#aM0Z7X3hDe|QQN4pgS z+_TW~@HUW0EUJrO(a|2v1AxqaqyLTbLCiRQEGOovem%4=dORM8M3N@V2v&+j+pCG_ zvGzSIFt8mr%RsT))k^Eka*EM^1vp7k#xL;d-b$qaYBbuJR^FY`AZ3XV*KEBnv|dO} zlx|4Jt?7iiOV$6t<=g^L5mr2aQIG?G3K7%P*5G#enquD@YSumeP`?fW{WY7Jg8B`j zknR_L^WQKNi`|9;~LXimQJ8h^~iPrSU6x)A`0z&qoH5c1$%dCPedVxo%K>i@Vc z>8iD}u~y||(yR@9xg%{If}Fn9ItY}r%}8CmJZ!eJN;(E;a>I&G_xF|Si(0rgXehs@ z=b;+OL3iKaLI`n!H>;TAbyBku0qme`hvjR%+)_FluBT*KS~`Wk6P;!)Jr9vPty>%* z;7eqKmG#yK(71@X)2V4q{Ceb$+IBVBb;yvJ%E3`((o+r86TXODN&QJ80jkA(mB=>?={N-R0<#Y~^vHaqPb<)u zOeg7|hc1H%_z|l$KeBwo+c>!YMhU8Y>&FAG?yT}J=kawNoB#86G+GSP9}EOVv?ES| z?0`?UT#7VvS8{tR#uMD5^?hNg#&t(LX1CP&MC?r9P_^iCFudaOH+;PnNCM>Gn|=YL z(mo_pIw4>wq*GGdsQ~*u#<)emBfudRt9xkz(uZVvtWy}t$$S~AnIY02Pg^sO*XRjc zs7F9Jm11kSEm>pqZ$frGk$CF;aX?6BQu6MsoSH&OPK_#N(h}gxJ!TOdzhk-=+gJr^#KmuWP4dH`GVyB_Y z%uK)DQZTUomMK}AASyr?$fEhdYPFf8Mw6DiD_QQkejbLa|70eyBwl^yaz9(J{aUGb zC;Rf}guJLAm%*6rdVN;Xj8@GW5dU}l@D4Ea4XBeSlv=CYVlcL_x@O0}tWZTnMo|6nL)bHrCg!+%DFQ(QyjQ0&`qm)N zVsnCU>4^tpQ!%|jD28T32%(zKE8sTVF0@exqts2=V&x5)?{el)yWY@xq}EgxKoo<( zWpCVZvn!IKckn|k( z6dDw9uYV3z{2mQGl9b-DwiY%yI?}`B+CvNVT>gOAvh=ib)6TbXq2u=E4+6c`j8a(T zWd@I^n8E^imPv`os9rD4oC)X>{)B=gNTYqanpN)t~LftMGptwHTf|-xbf-*e0C$jF%_tKUmF>*y-HyqY}q6fxx5#%NT4lUr3sq&?ZsHF-x*y{Wnrt){Fb3pUqDpZ~3eJ&iGkaA%C-c&$|mPg2w|g z4I^g=^uGu@GA$R%BsV(B6_SK!XtR7$5s}Ykes&;6d~CkWCMIS>=7!CA*4u@~=?`Q) zxgZ=eJNn0^Ee-k$pC3XhBd)q|IVE+vl*Ks{Kd;{c1R)O%in^E*PTwaRO1`dPmVBZkC zu#FktD~Xp6eUH z=zzS{rORUxQSBZIFkKF%0(_|Odhm+$85}66HGLZvrXBKK(j-P6D&Em$w!FSNxhBMx zX?{~z6|1`F4;%9kn0TfGD73%3FiI$~PdoFjR6WF(D$z!TxYGB({Q1Dp9sew*Qc1^U z5xP9^jy&D$91^)os>m)et9IPtT8DHxg4NYR1Wd@kdOq!9R#2wlzCp|y6g?fIh&@D{ zmoC>){9#X-%8o|0;w39}<&yowF=m1J)9uPYX#p}4P@bwJU3*mt9~h>aH7PMVcut%_ zkZ-+(787i`WL=YWhHu-T1;QXGPWr01yEu3jl_TRUoL_RyzoT|Zic90P0(Du(4f8nM zuWrTqz!6NIP|0LU13VSLBkNW!x3VFOrR)8mns37qyfalDSl(eojZ9jA z>W$-|a13Ey{I8+zyAcFOBx9moc*_cYe&XR{H2TbRHps-^jukK3U1DC8V~POj?83*4 zT~2Nb#$CE}(g%i6dJ!Ue<({L7yMP43mVCMIt*26LOJY)KBr|FOsxsj6uC7k9bPwr0 z&!G591u`+F#4Ici`P<~*e>r3uvMs{F9it)??65^dJXd@>UI=mj@GEgNKjAlcD3_(^ z^IRpbPK0VWF}T^iqMRP{!+rXu>m-!*$CYD>{Jq{h$u8Qu(}|UP`kLE(pXxOZFl|{T zgP=SO{uIWbZ%aq;uxdrNy|?oh8fWdUoJ#F94m%`3t8J-txWVsequL2 zo=Ar1giRA@EK!HugAkG>^rV9PtQ&SPEH(1*=Q9DNkxE_H?D>Z{Z1*cEN^`GVSzQ){ z9wuVpY|x_e$2;dDlm@L)u^~YDh@VomVlArU(vzQ-vC6nZ&)Ng2fqCNb_KCpnHo-uPJlc=SXikz<6PdacQG*U4j8YCfZOydz7_Lx`D-0qb^SfqF)luNc^g zGf*!q__`wBZ-vOksy+~z-xpEZO?q_BW|tLyg#g(=@jE~CA)^ZkQ<6J_^c|YizQ93Q z#29g5sWa976VkYbE=l!|X5jbP3!_F50a=FIOe>~zS6jiY*30ok2z7Qt0XqcB#Rsx1 zvn&%TOA4%Ammhg-Ye^9MDx(Zq7|}&)`(Tm3vR)g{;I zyK4NXn}(X?o`l%Q3nD>+pkht-qMKsNJ@nS`;&T?`J0XsKuMe@NgajD2K=}R+w0c_) z7@C!K1Gz@C)e^pRv_*N~c?z!GNu&3Fvci-T4X)A&knUGj6|GgpLCmG$hQHDb`JOj!bO+V_6Sp?<-rk&^BJyc1h{)5k-|=ECQQRp2DupYB_7BOPC}3)@rGb&#IAa0`-b8_QTgPv4J0NA zqOxJf`#D1oV6^7h8=YSu$jZF+yk!cD71Hkmk-dc{CZNd! z{%g%?#Uu#2_gI=Ev_z(iU^R)jYaH#-L?rr|v;D+MV9E{JPfGeDoG7 z1BCx0{b)$U@gzW%`3CifdW?7j|K$fPQ5=8s*u)=vg^ML&H2y>bWsP3&4hT?)yBJfXoAB0(Fw0$`Uu-SG8uvu#Mka&MD*wEpc61wMDn@ud0ESC zm4W6oTDMmu&FI}dw41rkXi`q=_Dkcy@WY>`HDAOaJeE7_sdw-{REBK!UpRDd82t!s^_*o9!x(KH8)&(2EXh)()tTShivn2(!WR0Tg_8|5FDJ;VH4&3x2Q6+nc*V|NI$#D!L-T-k#)F)Xd?w z8UdA2pEIB|0hBBb)Lt>N?92pv0=l0-izJI!>_Etyclu}4`=@-gAr?Jg@MZF4gd(f` zs4wXQ{GdMfSAVPM6Dy1-f6zDxXoflt{w&9Yy@;Hi zxXht+xD^bjN>nOYG_IdlQl}HK)mja;P4a~&Ta-IV1igsQ_(zkrMLr+z!GKcWyOb8n zDeq-IwXQr_k7BHnIfe=uU0Cz&Ka5I{41~$6Pa-Z12q7t5ME)Txa`vScu?{In2))QM zjM0c4A-M`|?}EU~vGK}UfXY(dY?j@1M@T7FDHhO1(q(=Dk(h7CwX@>gxPpSZ^em&d zO0!-OAFUu*@aQttO(U(^Srn&mNSAw?Eaqc^FZ^%os=Ou(2o8+_w~+z=z<+bNcK|01 zFc7X5(-mt8H9KHaNG}kV(}P^Unps~=Xg31MP(zv!f<|j4m*`sa`i_e_72}NpZ8w9f zVhvx==-mgJi`4eiK3^^g3gBFO1D#KXZa0_zbicUIho(`Dp}Q^9P>Z6T^Z0IW@lssP z9ta?kV2&tv5CinB@aSgDe$ojR@5SRAb3lG=;tXMH^kNzlxQCk6u02uh52E;xj^i4% z2LqxE$@?lwr;fy!l#=xilPj_9!?rh|0I3#_WjO+v zlAv99j{s3|E)-BzXu{@gJsSoo!iaY$oXXvY2pJ-ofOrss(j5e-Kx)a~3LeBjP1d2Y zA$s1BK^ajhv0QFuHso($oazr*jqsZmdN34`6(}B-q-w8eHn2FLLFs)+%{a@`CxL#( zi$+BUKd2OBAKvzI7BXm-Ci(E!>Og~`uDxC!%S~1UpfY%m2=x_^R+`I?=bzrG{u#OV z1Oit>V|~N{IQNe^Y_910=FBwo#%kCTN%-dFx%Wc3s-R3o@Dei*A^ZgeQOo3USWqAS z@BY{$0$tFMlhzr<|1zRoZf0iI$2{O{r*;rzM8`PX`JYk%<&^5TqoZTSb$%@Ns*8co z>ur+Pq}uK59`AtjrqaBU<$9}$a?g1$9%Rs0s(Vn75;Q-P0SMGIJb`n)WB6iHTh1Mj z_;RO*ox?*aczz97t)n|SS;8Q4*5XG(Cx(IpdfSW`q>G1>^;7%LX`<$vGw)k_m^%1ocMdc z03C~7_zECeoh~TcOJbZl@D6sT!jh>5qUghlAoNVAF@#THfVASJa->Slp8N1O)-yJo z{&lu!n_5BG6v0;jLi)XQfcrzYv>3yQPd&HwMNT2?Z9Yt6ijWw<4Xj9(9?31bL0ZVV z@Ul!H9>e~do3DqP5okK5S7lUVcnyaebQvL4to>LFVhYwMwKEA8#sWcIAQ97^cu@q< z$yDzdrN_XK8IuSh4$iROi{aalNXG#!w=bLR7}yPvX3bD1?0CRBBN?D@6K8&e{8nnD z*`|dE`i9?(1)Oe_VEAaRC;U{OOUA{2647!2o>^eV8SUCx9+-M4!tU|y zJ1qzg!6P8Jl;)X<0}fPn9fr>2*6a9)4N zXn4DRtXl#ttVHSE)H)||HUx+eiog4}59ta7OPW5?wcku8*{Z|+I`Ny;KNFVb{j!kH z^W1Bx|6b`k@7rDAuhXJD>gOIvSJdDomfR2TwXEQS;sL#LED1t{zd|_awd(VsWj@9*?3pQi zQ6_A{3+uIG;7wnPWE#z%d>Y|)K;qO47%dowKg$;pBB-c^@^J)Hnd)!?1;HipNlS8E%R?tWemmMHS~R@??X0qr9_z&|aI z?k>*`JrcSOw$Q||&R~2frOE1(Iv}vO0FqV03Rr}&V9Kp@G<}J7%bC=$BO+R9-}w~X zSoIi>ZC=rbd9@NO={YX3yrSb>_093Wnvq&y_Z%%K<^bf{TC4Inw3wo~5sBvmV5wrB z6};xy4*<)(67vjyDN_>W=(LJ?!TROnKT{asaceQP+>TFUXBA=JILyeMHd;Ryz4B6C zjE{|-2AW*&S9vX)2F$t!6Q0Ns^e-Bbe*ZZCLqyi&B+syMBS_Vy_w7R+c%QDF8;FLQ z!h(GtJL}cRh-n=o^xIgdQ`>ui9tMToY=OT53do;2=eRr|Oz7T#96ucs4~Ao(i!|KGDW0CR z-t8q!`RqElgxA$mUNi2c9m<~rWV1VDAI2j3)0A3B9;Y$N?$Kl9>F;nf|DXP@(#lcr zO69`${p|-_(w1>`*fUaqLl2edEGrkIU^O=z!HB}NMd|mM`Hz5luP2`l8!e_J1x0k( z))o@Eo>Kx_As1uQJDdMxKbX)kkdB`D-skIH_sRrP1o}_*Pfg8rz^XQ90c(V&CyTT{ zXv5h!I~=)8w6x0H+tAO=_Wh@1NYM*#1&mhZ2g>YsO$ZMdfcZ-6%vu;btk@W4FG48? z8+8}*^3rvX??dN15r?}dNL@;HY}Ar)2CyM<1l$Kpx0FJi7ViWuod81{={(f{uwsGd zSBsI`hsBni@Y)DZ{Zr7(F)$e7Rgh60-@2`oQbi*qe8rXcPl$09A8^4or}14YiPe{r z=;8cWiuC-v>wwrO~ZU|(6yYCE*ULycc z_{Mse{6D)wocDXM;;>;VtE4E8tP1SOXT`EsE^W9m5Z+EKEwI(94A_}C@XooyUx)r6 z3iz$AtWmT`i1h;?>md8{tOAiysR;p5KPc&baf9Tbol}BevRHkYs4PAG#{a$Yoxgp~ zz54Aq9v?N7q z(Kf_lQH?Mtz16)DXF3&AYuag;fL)z4^ZOckJ z2apqGOYRRCmI96ow~ew>+m<>d1_htD^O%V+)i&@{zcvB{W2e%42yE~9Xw#pM;?#I| zr7_BmTVm@8s4$?9cV95EIY4A>=ZGlgo{3h}>CD;!51xkW>AXU*V}(wO6;4VplB)@) z-a9YL_wkDCe=SqI?p-g5oW6yV6i$;`^pc_?8KrJDZS9+l$By`f0F&H6JtFT7Gjm_N-u=&wC;cwtned#X~2uZWO#|TnS>9(gi7$; zm{LXj@D=bz>swC@n-_UpCn0BBkMh)2b*N(l)`&=D)IW9x9smuLy5s#E`Ou#8!B^|@ zsxV!+W&zCf%J2Ed{mu&f_P=8m_v(*PiBEg{A#?Sp#qM1X2)HIMvo zx8OGEf`6U(=v}!LSE+c_;@^(je}ORzMP5-mO6Wxgz`H}*hlM8e^qq73HU1^N>FPRN zvrJeMHx~ECT&Vs8-fIY4wJceS`}HHD$jeU z{wC{}CKljg;+{`4{S;&q2MaEqpJQQSZqB6Scwo$;drGpf%8&%W)axRra(Onh6keS3 z)K_ir9{ykJdZ)tib7B_YzGXM^vX!IB?24JHzv-8%^xC($xo{Fc=wZz8Z;J*5Oq4uQ zESWX;LP~-@rY*8FN%o~0Vw-eHD!iDM%`U&?u%G^ya>YcRlw7!|F&_$SpT3<)^mTDS zy0)&y|DUXSC)!Pn`ec;al>sXa8s@)il4B*(4)WT~_5OgD&k=aej6hH=)^=aL`jPW? z!=6I(0})fNNV4*C7{6%JR>GzbhMnM*vCns13z|N#7f#Pixa+mUcRl4)U4lR+aZh@o zm-Rq{`8u&9BrXk1j4o%OxNnCH4*|61B63UlSX*LeQtUiM!m3nR@Pb0!jb@)O_HXa^ zzc3mK6;k>;eQEJTMy7icV7&^sT)NB$^g~CLWK*$#xo_GATf5my@3hvL>(hGQf8I`V zLni9G>OXr-T){WFmi+MPM&`oGF_Cak+ka-rO%Fv;=pJ*FPMYYkw-cQ@6{|sfOK|}8 z-fH;V8BACKbn$p?GXd7w>j`}nnksU2zB&=M`#0OYN2R=%c1(-o`S;wB0O=CeJ-=TW zCR4|2{y(gR9O=-*FDOed8J`Ab!`<>uj(e6TMV{g6+VSK2DcExo>;ZE6zuMht)|sl$nUL{7N> z@b7EuDtthK(BSZ^vmJ!h^+>v6FWulYT_eHH)8?qkMO{CVdh)7YT1vS!xz@^eT{dvd zHgQn;qkgnMbVe%T|1VQP^-+AtDeF$c3NEuWk53O6&(X`}0*onTz;O7o_Mzhl1EZ?M z6W}x)cXK^kq{@6=1_4Yu(iPH`Gx1*4o!`!{v(y4}IP=2gsq$TIW=Cim8g)jh_qzgp zfj?yT z_B7HGg4xSpi7}eFxHDc6^Sj@520&fL;yAQc3{Zq9;m7T(;^Tp?MA0whbcts?KgpA_Yj$%(rV!43jv(@ zzCJ!kc22R8R6j33lWi~*?gJW!0KYKcyCBz*VpT4=i$8@IA)1wUjH>WIF9AvzEYk1`+vWg66t7f^}1(~*dUz&E)GTl7#KGYiu?Hmd#{NK0C?@SwjJp5 zfm?l~aRkqj1 zx!d>rk)2hT93Vt None: + super().__init__(DRIVER_NAME, address, port, **settings) + self.__lock = threading.Lock() + self.__started = threading.Event() + self.__terminate = threading.Event() + username = self.settings.get('username') + password = self.settings.get('password') + self.__auth = HTTPBasicAuth(username, password) if username is not None and password is not None else None + scheme = self.settings.get('scheme', 'http') + self.__base_url = '{:s}://{:s}:{:d}'.format(scheme, self.address, int(self.port)) + self.__timeout = int(self.settings.get('timeout', 120)) + + def Connect(self) -> bool: + url = f"{self.__base_url}/stats/desc/1" + with self.__lock: + if self.__started.is_set(): + return True + try: + response = requests.get(url, timeout=self.__timeout, verify=False, auth=self.__auth) + response.raise_for_status() + except requests.exceptions.Timeout: + LOGGER.exception(f"Timeout connecting to {self.__base_url}") + return False + except requests.exceptions.RequestException as e: + LOGGER.exception(f"Exception connecting to {self.__base_url}: {e}") + return False + else: + self.__started.set() + return True + + def Disconnect(self) -> bool: + with self.__lock: + self.__terminate.set() + return True + + #@metered_subclass_method(METRICS_POOL) + #def GetInitialConfig(self) -> List[Tuple[str, Any]]: + # with self.__lock: + # switches = get_switches(self.__base_url, auth=self.__auth, timeout=self.__timeout) + # return [("switches", switches)] + + @metered_subclass_method(METRICS_POOL) + def GetConfig(self, resource_keys: List[str] = []) -> List[Tuple[str, Union[Any, None, Exception]]]: + chk_type('resources', resource_keys, list) + results = [] + with self.__lock: + for key in resource_keys: + try: + if key.startswith('flows:'): + dpid = key.split(':', 1)[1] + flows = get_flows(self.__base_url, dpid, auth=self.__auth, timeout=self.__timeout) + results.append((key, flows)) + elif key.startswith('description:'): + dpid = key.split(':', 1)[1] + desc = get_desc(self.__base_url, dpid, auth=self.__auth, timeout=self.__timeout) + results.append((key, desc)) + elif key.startswith('switches'): + switches = get_switches(self.__base_url, auth=self.__auth, timeout=self.__timeout) + results.append((key, switches)) + elif key.startswith('port_description:'): + dpid = key.split(':', 1)[1] + desc = get_port_desc(self.__base_url,dpid, auth=self.__auth, timeout=self.__timeout) + results.append((key, desc)) + elif key.startswith('switch_info'): + sin = get_switches_information(self.__base_url, auth=self.__auth, timeout=self.__timeout) + results.append((key, sin)) + elif key.startswith('links_info'): + lin = get_links_information(self.__base_url, auth=self.__auth, timeout=self.__timeout) + results.append((key, lin)) + else: + results.append((key, None)) # If key not handled, append None + except Exception as e: + results.append((key, e)) + return results + + @metered_subclass_method(METRICS_POOL) + def DeleteConfig(self, resource_keys: List[str] = []) -> List[Tuple[str, Union[Any, None, Exception]]]: + chk_type('resources', resource_keys, list) + results = [] + with self.__lock: + for item in resource_keys: + try: + if isinstance(item, tuple): + key, data = item + else: + key, data = item, None + if key.startswith('flowentry_delete:'): + dpid = key.split(':', 1)[1] + flows = del_flow_entry(self.__base_url, dpid, auth=self.__auth, timeout=self.__timeout) + results.append((key, flows)) + elif key=='flow_data' and data: + flow_del = delete_flow (self.__base_url,data,auth=self.__auth, timeout=self.__timeout) + results.append((key, flow_del)) + else: + results.append((key, None)) + except Exception as e: + results.append((key, e)) + return results + + @metered_subclass_method(METRICS_POOL) + def SetConfig(self, resources: List[Tuple[str, Any]]) -> List[Union[bool, Exception]]: + results = [] + if not resources: + return results + with self.__lock: + for item in resources: + LOGGER.info('resources contains: %s', item) + try: + if isinstance(item, tuple) and len(item) == 2: + key, flow_data = item + else: + LOGGER.warning("Resource format invalid. Each item should be a tuple with (key, data).") + results.append(False) + continue + if key == "flow_data" and isinstance(flow_data, dict): + LOGGER.info(f"Found valid flow_data entry: {flow_data}") + success = add_flow(self.__base_url, flow_data, auth=self.__auth, timeout=self.__timeout) + results.append(success) + else: + LOGGER.warning(f"Skipping item with key: {key} due to invalid format or missing data.") + results.append(False) + + except Exception as e: + LOGGER.error(f"Exception while setting configuration for item {item}: {str(e)}") + results.append(e) + + return results + + + + @metered_subclass_method(METRICS_POOL) + def SubscribeState(self, subscriptions : List[Tuple[str, float, float]]) -> List[Union[bool, Exception]]: + # TODO: TAPI does not support monitoring by now + return [False for _ in subscriptions] + + @metered_subclass_method(METRICS_POOL) + def UnsubscribeState(self, subscriptions : List[Tuple[str, float, float]]) -> List[Union[bool, Exception]]: + # TODO: TAPI does not support monitoring by now + return [False for _ in subscriptions] + + def GetState( + self, blocking=False, terminate : Optional[threading.Event] = None + ) -> Iterator[Tuple[float, str, Any]]: + # TODO: TAPI does not support monitoring by now + return [] diff --git a/tmp-code/OpenFlow/Tools.py b/tmp-code/OpenFlow/Tools.py new file mode 100644 index 000000000..d68347087 --- /dev/null +++ b/tmp-code/OpenFlow/Tools.py @@ -0,0 +1,174 @@ +# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# +# 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 json, logging, operator, requests +from requests.auth import HTTPBasicAuth +from typing import Optional +from device.service.driver_api._Driver import RESOURCE_ENDPOINTS, RESOURCE_SERVICES +from typing import List, Dict, Optional, Tuple, Union + +LOGGER = logging.getLogger(__name__) + +RESOURCE_ENDPOINTS = { + #get configurations + "switches": "/stats/switches", + "description": "/stats/desc", + "flows": "/stats/flow", + "port_description":"/stats/portdesc", + "switch_info":"/v1.0/topology/switches", + "links_info":"/v1.0/topology/links", + #add flow + "flow_add": "/stats/flowentry/add", + #Delete all matching flow entries of the switch. + "flow_delete": "/stats/flowentry/delete", + "flowentry_delete":"/stats/flowentry/clear", #according to dpid + +} + +HTTP_OK_CODES = { + 200, # OK + 201, # Created + 202, # Accepted + 204, # No Content +} + +# Utility function to find and extract a specific key from a resource. +def find_key(resource: Tuple[str, str], key: str) -> Union[dict, str, None]: + try: + return json.loads(resource[1])[key] + except KeyError: + LOGGER.warning(f"Key '{key}' not found in resource.") + return None + +def get_switches(root_url: str, auth: Optional[HTTPBasicAuth] = None, timeout: Optional[int] = None) -> List[Dict]: + url = f"{root_url}{RESOURCE_ENDPOINTS['switches']}" + result = [] + try: + response = requests.get(url, timeout=timeout, verify=False, auth=auth) + response.raise_for_status() + switches = response.json() + LOGGER.info(f"Successfully retrieved switches: {switches}") + result = switches + except requests.exceptions.Timeout: + LOGGER.exception(f"Timeout connecting to {url}") + except requests.exceptions.RequestException as e: + LOGGER.exception(f"Error retrieving switches: {str(e)}") + return result + +def get_switches_information(root_url: str, auth: Optional[HTTPBasicAuth] = None, timeout: Optional[int] = None) -> List[Dict]: + url = f"{root_url}{RESOURCE_ENDPOINTS['switch_info']}" + result = [] + try: + response = requests.get(url, timeout=timeout, verify=False, auth=auth) + response.raise_for_status() + switches_info = response.json() + LOGGER.info(f"Successfully retrieved switches: {switches_info}") + result = switches_info + except requests.exceptions.Timeout: + LOGGER.exception(f"Timeout connecting to {url}") + except requests.exceptions.RequestException as e: + LOGGER.exception(f"Error retrieving switches: {str(e)}") + return result + +def get_links_information(root_url: str, auth: Optional[HTTPBasicAuth] = None, timeout: Optional[int] = None) -> List[Dict]: + url = f"{root_url}{RESOURCE_ENDPOINTS['links_info']}" + result = [] + try: + response = requests.get(url, timeout=timeout, verify=False, auth=auth) + response.raise_for_status() + links_info = response.json() + LOGGER.info(f"Successfully retrieved switches: {links_info}") + result = links_info + except requests.exceptions.Timeout: + LOGGER.exception(f"Timeout connecting to {url}") + except requests.exceptions.RequestException as e: + LOGGER.exception(f"Error retrieving switches: {str(e)}") + return result + +def get_flows(root_url: str, dpid: str, auth: Optional[HTTPBasicAuth] = None, timeout: Optional[int] = None) -> List[Dict]: + url = f"{root_url}{RESOURCE_ENDPOINTS['flows']}/{dpid}" + try: + response = requests.get(url, timeout=timeout, verify=False, auth=auth) + response.raise_for_status() + flows = response.json() + LOGGER.info(f"Successfully retrieved flow rules for DPID {dpid}") + return flows + except requests.exceptions.RequestException as e: + LOGGER.error(f"Failed to retrieve flow rules for DPID {dpid}: {str(e)}") + return [] + +#get description +def get_desc(root_url: str, dpid: str, auth: Optional[HTTPBasicAuth] = None, timeout: Optional[int] = None) -> Dict: + url = f"{root_url}{RESOURCE_ENDPOINTS['description']}/{dpid}" + try: + response = requests.get(url, timeout=timeout, verify=False, auth=auth) + response.raise_for_status() + desc = response.json() + LOGGER.info(f"Successfully retrieved description for DPID {dpid}: {desc}") + return desc + except requests.exceptions.RequestException as e: + LOGGER.error(f"Failed to retrieve description for DPID {dpid}: {str(e)}") + return {} + +def get_port_desc(root_url: str, dpid: str, auth: Optional[HTTPBasicAuth] = None, timeout: Optional[int] = None) -> Dict: + url = f"{root_url}{RESOURCE_ENDPOINTS['port_description']}/{dpid}" + try: + response = requests.get(url, timeout=timeout, verify=False, auth=auth) + response.raise_for_status() + port_desc = response.json() + LOGGER.info(f"Successfully retrieved description for DPID {dpid}: {port_desc}") + return port_desc + except requests.exceptions.RequestException as e: + LOGGER.error(f"Failed to retrieve description for DPID {dpid}: {str(e)}") + return {} + +##according to dpid +def del_flow_entry(root_url: str, dpid: str, auth: Optional[HTTPBasicAuth] = None, timeout: Optional[int] = None) -> Dict: + url = f"{root_url}{RESOURCE_ENDPOINTS['flowentry_delete']}/{dpid}" + try: + response = requests.delete(url, timeout=timeout, verify=False, auth=auth) + response.raise_for_status() + flow_desc = response.json() + LOGGER.info(f"Successfully retrieved description for DPID {dpid}: {flow_desc}") + return flow_desc + except requests.exceptions.RequestException as e: + LOGGER.error(f"Failed to retrieve description for DPID {dpid}: {str(e)}") + return {} + +# to delete a flow based on match criteria. +def delete_flow(root_url: str, flow_data: dict, auth: Optional[HTTPBasicAuth] = None, timeout: Optional[int] = None) -> bool: + url = f"{root_url}{RESOURCE_ENDPOINTS['flow_delete']}" + try: + response = requests.post(url, json=flow_data, timeout=timeout, verify=False, auth=auth) + response.raise_for_status() + LOGGER.info(f"Flow configuration deleted successfully for DPID {flow_data.get('dpid')}.") + return True + except requests.exceptions.RequestException as e: + LOGGER.error(f"Failed to delete flow configuration for DPID {flow_data.get('dpid')}: {str(e)}") + return False + +def add_flow(root_url: str, flow_data: dict, auth: Optional[HTTPBasicAuth] = None, timeout: Optional[int] = None) -> bool: + url = f"{root_url}{RESOURCE_ENDPOINTS['flow_add']}" + LOGGER.info(f"Posting flow data: {flow_data} (type: {type(flow_data)}) to URL: {url}") + try: + response = requests.post(url, json=flow_data, timeout=timeout, verify=False, auth=auth) + response.raise_for_status() + LOGGER.info("Flow configuration added successfully.") + return True + except requests.exceptions.RequestException as e: + LOGGER.error(f"Failed to add flow configuration: {str(e)}") + return False + + + diff --git a/tmp-code/OpenFlow/__init__.py b/tmp-code/OpenFlow/__init__.py new file mode 100644 index 000000000..4f3d1a042 --- /dev/null +++ b/tmp-code/OpenFlow/__init__.py @@ -0,0 +1,20 @@ +# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# +# 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 device.service.driver_api._Driver import RESOURCE_ENDPOINTS, RESOURCE_SERVICES + +ALL_RESOURCE_KEYS = [ + RESOURCE_ENDPOINTS, + RESOURCE_SERVICES, +] diff --git a/tmp-code/__init__.py b/tmp-code/__init__.py new file mode 100644 index 000000000..487cf7d40 --- /dev/null +++ b/tmp-code/__init__.py @@ -0,0 +1,202 @@ +# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# +# 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 os +from common.DeviceTypes import DeviceTypeEnum +from common.proto.context_pb2 import DeviceDriverEnum +from device.Config import LOAD_ALL_DEVICE_DRIVERS +from ..driver_api.FilterFields import FilterFieldEnum + +DRIVERS = [] + +from .emulated.EmulatedDriver import EmulatedDriver # pylint: disable=wrong-import-position +DRIVERS.append( + (EmulatedDriver, [ + # TODO: multi-filter is not working + { + FilterFieldEnum.DEVICE_TYPE: [ + DeviceTypeEnum.EMULATED_DATACENTER, + DeviceTypeEnum.EMULATED_MICROWAVE_RADIO_SYSTEM, + DeviceTypeEnum.EMULATED_OPEN_LINE_SYSTEM, + DeviceTypeEnum.EMULATED_OPTICAL_ROADM, + DeviceTypeEnum.EMULATED_OPTICAL_TRANSPONDER, + DeviceTypeEnum.EMULATED_P4_SWITCH, + DeviceTypeEnum.EMULATED_PACKET_ROUTER, + DeviceTypeEnum.EMULATED_PACKET_SWITCH, + + #DeviceTypeEnum.DATACENTER, + #DeviceTypeEnum.MICROWAVE_RADIO_SYSTEM, + #DeviceTypeEnum.OPEN_LINE_SYSTEM, + #DeviceTypeEnum.OPTICAL_ROADM, + #DeviceTypeEnum.OPTICAL_TRANSPONDER, + #DeviceTypeEnum.P4_SWITCH, + #DeviceTypeEnum.PACKET_ROUTER, + #DeviceTypeEnum.PACKET_SWITCH, + ], + FilterFieldEnum.DRIVER: [ + DeviceDriverEnum.DEVICEDRIVER_UNDEFINED, + ], + }, + #{ + # # Emulated devices, all drivers => use Emulated + # FilterFieldEnum.DEVICE_TYPE: [ + # DeviceTypeEnum.EMULATED_DATACENTER, + # DeviceTypeEnum.EMULATED_MICROWAVE_RADIO_SYSTEM, + # DeviceTypeEnum.EMULATED_OPEN_LINE_SYSTEM, + # DeviceTypeEnum.EMULATED_OPTICAL_ROADM, + # DeviceTypeEnum.EMULATED_OPTICAL_TRANSPONDER, + # DeviceTypeEnum.EMULATED_P4_SWITCH, + # DeviceTypeEnum.EMULATED_PACKET_ROUTER, + # DeviceTypeEnum.EMULATED_PACKET_SWITCH, + # ], + # FilterFieldEnum.DRIVER: [ + # DeviceDriverEnum.DEVICEDRIVER_UNDEFINED, + # DeviceDriverEnum.DEVICEDRIVER_OPENCONFIG, + # DeviceDriverEnum.DEVICEDRIVER_TRANSPORT_API, + # DeviceDriverEnum.DEVICEDRIVER_P4, + # DeviceDriverEnum.DEVICEDRIVER_IETF_NETWORK_TOPOLOGY, + # DeviceDriverEnum.DEVICEDRIVER_ONF_TR_532, + # DeviceDriverEnum.DEVICEDRIVER_GNMI_OPENCONFIG, + # ], + #} + ])) + +from .ietf_l2vpn.IetfL2VpnDriver import IetfL2VpnDriver # pylint: disable=wrong-import-position +DRIVERS.append( + (IetfL2VpnDriver, [ + { + FilterFieldEnum.DEVICE_TYPE: DeviceTypeEnum.TERAFLOWSDN_CONTROLLER, + FilterFieldEnum.DRIVER: DeviceDriverEnum.DEVICEDRIVER_IETF_L2VPN, + } + ])) + +from .ietf_actn.IetfActnDriver import IetfActnDriver # pylint: disable=wrong-import-position +DRIVERS.append( + (IetfActnDriver, [ + { + FilterFieldEnum.DEVICE_TYPE: DeviceTypeEnum.OPEN_LINE_SYSTEM, + FilterFieldEnum.DRIVER: DeviceDriverEnum.DEVICEDRIVER_IETF_ACTN, + } + ])) + +if LOAD_ALL_DEVICE_DRIVERS: + from .openconfig.OpenConfigDriver import OpenConfigDriver # pylint: disable=wrong-import-position + DRIVERS.append( + (OpenConfigDriver, [ + { + # Real Packet Router, specifying OpenConfig Driver => use OpenConfigDriver + FilterFieldEnum.DEVICE_TYPE: DeviceTypeEnum.PACKET_ROUTER, + FilterFieldEnum.DRIVER : DeviceDriverEnum.DEVICEDRIVER_OPENCONFIG, + } + ])) + +if LOAD_ALL_DEVICE_DRIVERS: + from .gnmi_openconfig.GnmiOpenConfigDriver import GnmiOpenConfigDriver # pylint: disable=wrong-import-position + DRIVERS.append( + (GnmiOpenConfigDriver, [ + { + # Real Packet Router, specifying gNMI OpenConfig Driver => use GnmiOpenConfigDriver + FilterFieldEnum.DEVICE_TYPE: DeviceTypeEnum.PACKET_ROUTER, + FilterFieldEnum.DRIVER : DeviceDriverEnum.DEVICEDRIVER_GNMI_OPENCONFIG, + } + ])) + +if LOAD_ALL_DEVICE_DRIVERS: + from .transport_api.TransportApiDriver import TransportApiDriver # pylint: disable=wrong-import-position + DRIVERS.append( + (TransportApiDriver, [ + { + # Real OLS, specifying TAPI Driver => use TransportApiDriver + FilterFieldEnum.DEVICE_TYPE: DeviceTypeEnum.OPEN_LINE_SYSTEM, + FilterFieldEnum.DRIVER : DeviceDriverEnum.DEVICEDRIVER_TRANSPORT_API, + } + ])) + +if LOAD_ALL_DEVICE_DRIVERS: + from .p4.p4_driver import P4Driver # pylint: disable=wrong-import-position + DRIVERS.append( + (P4Driver, [ + { + # Real P4 Switch, specifying P4 Driver => use P4Driver + FilterFieldEnum.DEVICE_TYPE: DeviceTypeEnum.P4_SWITCH, + FilterFieldEnum.DRIVER : DeviceDriverEnum.DEVICEDRIVER_P4, + } + ])) + +if LOAD_ALL_DEVICE_DRIVERS: + from .microwave.IETFApiDriver import IETFApiDriver # pylint: disable=wrong-import-position + DRIVERS.append( + (IETFApiDriver, [ + { + FilterFieldEnum.DEVICE_TYPE: DeviceTypeEnum.MICROWAVE_RADIO_SYSTEM, + FilterFieldEnum.DRIVER : DeviceDriverEnum.DEVICEDRIVER_IETF_NETWORK_TOPOLOGY, + } + ])) +if LOAD_ALL_DEVICE_DRIVERS: + from.OpenFlow.OpenFlowDriver import OpenFlowDriver + DRIVERS.append( + (OpenFlowDriver, [ + { + # Specify the device type and driver that should use OpenFlowDriver + FilterFieldEnum.DEVICE_TYPE: DeviceTypeEnum.OPENFLOW_RYU_CONTROLLER , + FilterFieldEnum.DRIVER: DeviceDriverEnum.DEVICEDRIVER_OPENFLOW, + } + ]) + ) + +if LOAD_ALL_DEVICE_DRIVERS: + from .xr.XrDriver import XrDriver # pylint: disable=wrong-import-position + DRIVERS.append( + (XrDriver, [ + { + # Close enough, it does optical switching + FilterFieldEnum.DEVICE_TYPE: DeviceTypeEnum.XR_CONSTELLATION, + FilterFieldEnum.DRIVER : DeviceDriverEnum.DEVICEDRIVER_XR, + } + ])) + +if LOAD_ALL_DEVICE_DRIVERS: + from .optical_tfs.OpticalTfsDriver import OpticalTfsDriver # pylint: disable=wrong-import-position + DRIVERS.append( + (OpticalTfsDriver, [ + { + FilterFieldEnum.DEVICE_TYPE: DeviceTypeEnum.OPEN_LINE_SYSTEM, + FilterFieldEnum.DRIVER: DeviceDriverEnum.DEVICEDRIVER_OPTICAL_TFS, + } + ])) + +if LOAD_ALL_DEVICE_DRIVERS: + from .oc_driver.OCDriver import OCDriver # pylint: disable=wrong-import-position + DRIVERS.append( + (OCDriver, [ + { + # Real Packet Router, specifying OpenConfig Driver => use OpenConfigDriver + FilterFieldEnum.DEVICE_TYPE: [ + DeviceTypeEnum.OPTICAL_ROADM, + DeviceTypeEnum.OPTICAL_TRANSPONDER + ], + FilterFieldEnum.DRIVER : DeviceDriverEnum.DEVICEDRIVER_OC, + } + ])) + +if LOAD_ALL_DEVICE_DRIVERS: + from .qkd.QKDDriver2 import QKDDriver # pylint: disable=wrong-import-position + DRIVERS.append( + (QKDDriver, [ + { + # Close enough, it does optical switching + FilterFieldEnum.DEVICE_TYPE: DeviceTypeEnum.QKD_NODE, + FilterFieldEnum.DRIVER : DeviceDriverEnum.DEVICEDRIVER_QKD, + } + ])) diff --git a/tmp-code/context.proto b/tmp-code/context.proto new file mode 100644 index 000000000..2ab6f0aea --- /dev/null +++ b/tmp-code/context.proto @@ -0,0 +1,698 @@ +// Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +// +// 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. + +syntax = "proto3"; +package context; + +import "acl.proto"; +import "kpi_sample_types.proto"; + +service ContextService { + rpc ListContextIds (Empty ) returns ( ContextIdList ) {} + rpc ListContexts (Empty ) returns ( ContextList ) {} + rpc GetContext (ContextId ) returns ( Context ) {} + rpc SetContext (Context ) returns ( ContextId ) {} + rpc RemoveContext (ContextId ) returns ( Empty ) {} + rpc GetContextEvents (Empty ) returns (stream ContextEvent ) {} + + rpc ListTopologyIds (ContextId ) returns ( TopologyIdList ) {} + rpc ListTopologies (ContextId ) returns ( TopologyList ) {} + rpc GetTopology (TopologyId ) returns ( Topology ) {} + rpc GetTopologyDetails (TopologyId ) returns ( TopologyDetails ) {} + rpc SetTopology (Topology ) returns ( TopologyId ) {} + rpc RemoveTopology (TopologyId ) returns ( Empty ) {} + rpc GetTopologyEvents (Empty ) returns (stream TopologyEvent ) {} + + rpc ListDeviceIds (Empty ) returns ( DeviceIdList ) {} + rpc ListDevices (Empty ) returns ( DeviceList ) {} + rpc GetDevice (DeviceId ) returns ( Device ) {} + rpc SetDevice (Device ) returns ( DeviceId ) {} + rpc RemoveDevice (DeviceId ) returns ( Empty ) {} + rpc GetDeviceEvents (Empty ) returns (stream DeviceEvent ) {} + rpc SelectDevice (DeviceFilter ) returns ( DeviceList ) {} + rpc ListEndPointNames (EndPointIdList) returns ( EndPointNameList) {} + + rpc ListLinkIds (Empty ) returns ( LinkIdList ) {} + rpc ListLinks (Empty ) returns ( LinkList ) {} + rpc GetLink (LinkId ) returns ( Link ) {} + rpc SetLink (Link ) returns ( LinkId ) {} + rpc RemoveLink (LinkId ) returns ( Empty ) {} + rpc GetLinkEvents (Empty ) returns (stream LinkEvent ) {} + + rpc ListServiceIds (ContextId ) returns ( ServiceIdList ) {} + rpc ListServices (ContextId ) returns ( ServiceList ) {} + rpc GetService (ServiceId ) returns ( Service ) {} + rpc SetService (Service ) returns ( ServiceId ) {} + rpc UnsetService (Service ) returns ( ServiceId ) {} + rpc RemoveService (ServiceId ) returns ( Empty ) {} + rpc GetServiceEvents (Empty ) returns (stream ServiceEvent ) {} + rpc SelectService (ServiceFilter ) returns ( ServiceList ) {} + + rpc ListSliceIds (ContextId ) returns ( SliceIdList ) {} + rpc ListSlices (ContextId ) returns ( SliceList ) {} + rpc GetSlice (SliceId ) returns ( Slice ) {} + rpc SetSlice (Slice ) returns ( SliceId ) {} + rpc UnsetSlice (Slice ) returns ( SliceId ) {} + rpc RemoveSlice (SliceId ) returns ( Empty ) {} + rpc GetSliceEvents (Empty ) returns (stream SliceEvent ) {} + rpc SelectSlice (SliceFilter ) returns ( SliceList ) {} + + rpc ListConnectionIds (ServiceId ) returns ( ConnectionIdList) {} + rpc ListConnections (ServiceId ) returns ( ConnectionList ) {} + rpc GetConnection (ConnectionId ) returns ( Connection ) {} + rpc SetConnection (Connection ) returns ( ConnectionId ) {} + rpc RemoveConnection (ConnectionId ) returns ( Empty ) {} + rpc GetConnectionEvents(Empty ) returns (stream ConnectionEvent ) {} + + + // ------------------------------ Experimental ----------------------------- + rpc GetOpticalConfig (Empty ) returns (OpticalConfigList ) {} + rpc SetOpticalConfig (OpticalConfig ) returns (OpticalConfigId ) {} + rpc SelectOpticalConfig(OpticalConfigId) returns (OpticalConfig ) {} + + rpc SetOpticalLink (OpticalLink ) returns (Empty ) {} + rpc GetOpticalLink (OpticalLinkId ) returns (OpticalLink ) {} + rpc GetFiber (FiberId ) returns (Fiber ) {} +} + +// ----- Generic ------------------------------------------------------------------------------------------------------- +message Empty {} + +message Uuid { + string uuid = 1; +} + +enum EventTypeEnum { + EVENTTYPE_UNDEFINED = 0; + EVENTTYPE_CREATE = 1; + EVENTTYPE_UPDATE = 2; + EVENTTYPE_REMOVE = 3; +} + +message Timestamp { + double timestamp = 1; +} + +message Event { + Timestamp timestamp = 1; + EventTypeEnum event_type = 2; +} + +// ----- Context ------------------------------------------------------------------------------------------------------- +message ContextId { + Uuid context_uuid = 1; +} + +message Context { + ContextId context_id = 1; + string name = 2; + repeated TopologyId topology_ids = 3; + repeated ServiceId service_ids = 4; + repeated SliceId slice_ids = 5; + TeraFlowController controller = 6; +} + +message ContextIdList { + repeated ContextId context_ids = 1; +} + +message ContextList { + repeated Context contexts = 1; +} + +message ContextEvent { + Event event = 1; + ContextId context_id = 2; +} + + +// ----- Topology ------------------------------------------------------------------------------------------------------ +message TopologyId { + ContextId context_id = 1; + Uuid topology_uuid = 2; +} + +message Topology { + TopologyId topology_id = 1; + string name = 2; + repeated DeviceId device_ids = 3; + repeated LinkId link_ids = 4; +} + +message TopologyDetails { + TopologyId topology_id = 1; + string name = 2; + repeated Device devices = 3; + repeated Link links = 4; +} + +message TopologyIdList { + repeated TopologyId topology_ids = 1; +} + +message TopologyList { + repeated Topology topologies = 1; +} + +message TopologyEvent { + Event event = 1; + TopologyId topology_id = 2; +} + + +// ----- Device -------------------------------------------------------------------------------------------------------- +message DeviceId { + Uuid device_uuid = 1; +} + +message Device { + DeviceId device_id = 1; + string name = 2; + string device_type = 3; + DeviceConfig device_config = 4; + DeviceOperationalStatusEnum device_operational_status = 5; + repeated DeviceDriverEnum device_drivers = 6; + repeated EndPoint device_endpoints = 7; + repeated Component components = 8; // Used for inventory + DeviceId controller_id = 9; // Identifier of node controlling the actual device +} + +message Component { //Defined previously to this section - Tested OK + Uuid component_uuid = 1; + string name = 2; + string type = 3; + + map attributes = 4; // dict[attr.name => json.dumps(attr.value)] + string parent = 5; +} + +message DeviceConfig { + repeated ConfigRule config_rules = 1; +} + +enum DeviceDriverEnum { + DEVICEDRIVER_UNDEFINED = 0; // also used for emulated + DEVICEDRIVER_OPENCONFIG = 1; + DEVICEDRIVER_TRANSPORT_API = 2; + DEVICEDRIVER_P4 = 3; + DEVICEDRIVER_IETF_NETWORK_TOPOLOGY = 4; + DEVICEDRIVER_ONF_TR_532 = 5; + DEVICEDRIVER_XR = 6; + DEVICEDRIVER_IETF_L2VPN = 7; + DEVICEDRIVER_GNMI_OPENCONFIG = 8; + DEVICEDRIVER_OPTICAL_TFS = 9; + DEVICEDRIVER_IETF_ACTN = 10; + DEVICEDRIVER_OC = 11; + DEVICEDRIVER_QKD = 12; + DEVICEDRIVER_RYU = 13; +} + +enum DeviceOperationalStatusEnum { + DEVICEOPERATIONALSTATUS_UNDEFINED = 0; + DEVICEOPERATIONALSTATUS_DISABLED = 1; + DEVICEOPERATIONALSTATUS_ENABLED = 2; +} + +message DeviceIdList { + repeated DeviceId device_ids = 1; +} + +message DeviceList { + repeated Device devices = 1; +} + +message DeviceFilter { + DeviceIdList device_ids = 1; + bool include_endpoints = 2; + bool include_config_rules = 3; + bool include_components = 4; +} + +message DeviceEvent { + Event event = 1; + DeviceId device_id = 2; + DeviceConfig device_config = 3; +} + + +// ----- Link ---------------------------------------------------------------------------------------------------------- +message LinkId { + Uuid link_uuid = 1; +} + +message LinkAttributes { + float total_capacity_gbps = 1; + float used_capacity_gbps = 2; +} + +message Link { + LinkId link_id = 1; + string name = 2; + repeated EndPointId link_endpoint_ids = 3; + LinkAttributes attributes = 4; + LinkTypeEnum link_type = 5; +} + +message LinkIdList { + repeated LinkId link_ids = 1; +} + +message LinkList { + repeated Link links = 1; +} + +message LinkEvent { + Event event = 1; + LinkId link_id = 2; +} + +enum LinkTypeEnum { + LINKTYPE_UNKNOWN = 0; + LINKTYPE_COPPER = 1; + LINKTYPE_VIRTUAL_COPPER = 2; + LINKTYPE_OPTICAL = 3; + LINKTYPE_VIRTUAL_OPTICAL = 4; +} + +// ----- Service ------------------------------------------------------------------------------------------------------- +message ServiceId { + ContextId context_id = 1; + Uuid service_uuid = 2; +} + +message Service { + ServiceId service_id = 1; + string name = 2; + ServiceTypeEnum service_type = 3; + repeated EndPointId service_endpoint_ids = 4; + repeated Constraint service_constraints = 5; + ServiceStatus service_status = 6; + ServiceConfig service_config = 7; + Timestamp timestamp = 8; +} + +enum ServiceTypeEnum { + SERVICETYPE_UNKNOWN = 0; + SERVICETYPE_L3NM = 1; + SERVICETYPE_L2NM = 2; + SERVICETYPE_TAPI_CONNECTIVITY_SERVICE = 3; + SERVICETYPE_TE = 4; + SERVICETYPE_E2E = 5; + SERVICETYPE_OPTICAL_CONNECTIVITY = 6; + SERVICETYPE_QKD = 7; +} + +enum ServiceStatusEnum { + SERVICESTATUS_UNDEFINED = 0; + SERVICESTATUS_PLANNED = 1; + SERVICESTATUS_ACTIVE = 2; + SERVICESTATUS_UPDATING = 3; + SERVICESTATUS_PENDING_REMOVAL = 4; + SERVICESTATUS_SLA_VIOLATED = 5; +} + +message ServiceStatus { + ServiceStatusEnum service_status = 1; +} + +message ServiceConfig { + repeated ConfigRule config_rules = 1; +} + +message ServiceIdList { + repeated ServiceId service_ids = 1; +} + +message ServiceList { + repeated Service services = 1; +} + +message ServiceFilter { + ServiceIdList service_ids = 1; + bool include_endpoint_ids = 2; + bool include_constraints = 3; + bool include_config_rules = 4; +} + +message ServiceEvent { + Event event = 1; + ServiceId service_id = 2; +} + +// ----- Slice --------------------------------------------------------------------------------------------------------- +message SliceId { + ContextId context_id = 1; + Uuid slice_uuid = 2; +} + +message Slice { + SliceId slice_id = 1; + string name = 2; + repeated EndPointId slice_endpoint_ids = 3; + repeated Constraint slice_constraints = 4; + repeated ServiceId slice_service_ids = 5; + repeated SliceId slice_subslice_ids = 6; + SliceStatus slice_status = 7; + SliceConfig slice_config = 8; + SliceOwner slice_owner = 9; + Timestamp timestamp = 10; +} + +message SliceOwner { + Uuid owner_uuid = 1; + string owner_string = 2; +} + +enum SliceStatusEnum { + SLICESTATUS_UNDEFINED = 0; + SLICESTATUS_PLANNED = 1; + SLICESTATUS_INIT = 2; + SLICESTATUS_ACTIVE = 3; + SLICESTATUS_DEINIT = 4; + SLICESTATUS_SLA_VIOLATED = 5; +} + +message SliceStatus { + SliceStatusEnum slice_status = 1; +} + +message SliceConfig { + repeated ConfigRule config_rules = 1; +} + +message SliceIdList { + repeated SliceId slice_ids = 1; +} + +message SliceList { + repeated Slice slices = 1; +} + +message SliceFilter { + SliceIdList slice_ids = 1; + bool include_endpoint_ids = 2; + bool include_constraints = 3; + bool include_service_ids = 4; + bool include_subslice_ids = 5; + bool include_config_rules = 6; +} + +message SliceEvent { + Event event = 1; + SliceId slice_id = 2; +} + +// ----- Connection ---------------------------------------------------------------------------------------------------- +message ConnectionId { + Uuid connection_uuid = 1; +} + +message ConnectionSettings_L0 { + string lsp_symbolic_name = 1; +} + +message ConnectionSettings_L2 { + string src_mac_address = 1; + string dst_mac_address = 2; + uint32 ether_type = 3; + uint32 vlan_id = 4; + uint32 mpls_label = 5; + uint32 mpls_traffic_class = 6; +} + +message ConnectionSettings_L3 { + string src_ip_address = 1; + string dst_ip_address = 2; + uint32 dscp = 3; + uint32 protocol = 4; + uint32 ttl = 5; +} + +message ConnectionSettings_L4 { + uint32 src_port = 1; + uint32 dst_port = 2; + uint32 tcp_flags = 3; + uint32 ttl = 4; +} + +message ConnectionSettings { + ConnectionSettings_L0 l0 = 1; + ConnectionSettings_L2 l2 = 2; + ConnectionSettings_L3 l3 = 3; + ConnectionSettings_L4 l4 = 4; +} + +message Connection { + ConnectionId connection_id = 1; + ServiceId service_id = 2; + repeated EndPointId path_hops_endpoint_ids = 3; + repeated ServiceId sub_service_ids = 4; + ConnectionSettings settings = 5; +} + +message ConnectionIdList { + repeated ConnectionId connection_ids = 1; +} + +message ConnectionList { + repeated Connection connections = 1; +} + +message ConnectionEvent { + Event event = 1; + ConnectionId connection_id = 2; +} + + +// ----- Endpoint ------------------------------------------------------------------------------------------------------ +message EndPointId { + TopologyId topology_id = 1; + DeviceId device_id = 2; + Uuid endpoint_uuid = 3; +} + +message EndPoint { + EndPointId endpoint_id = 1; + string name = 2; + string endpoint_type = 3; + repeated kpi_sample_types.KpiSampleType kpi_sample_types = 4; + Location endpoint_location = 5; +} + +message EndPointName { + EndPointId endpoint_id = 1; + string device_name = 2; + string endpoint_name = 3; + string endpoint_type = 4; +} + +message EndPointIdList { + repeated EndPointId endpoint_ids = 1; +} + +message EndPointNameList { + repeated EndPointName endpoint_names = 1; +} + + +// ----- Configuration ------------------------------------------------------------------------------------------------- +enum ConfigActionEnum { + CONFIGACTION_UNDEFINED = 0; + CONFIGACTION_SET = 1; + CONFIGACTION_DELETE = 2; +} + +message ConfigRule_Custom { + string resource_key = 1; + string resource_value = 2; +} + +message ConfigRule_ACL { + EndPointId endpoint_id = 1; + acl.AclRuleSet rule_set = 2; +} + +message ConfigRule { + ConfigActionEnum action = 1; + oneof config_rule { + ConfigRule_Custom custom = 2; + ConfigRule_ACL acl = 3; + } +} + + +// ----- Constraint ---------------------------------------------------------------------------------------------------- +enum ConstraintActionEnum { + CONSTRAINTACTION_UNDEFINED = 0; + CONSTRAINTACTION_SET = 1; + CONSTRAINTACTION_DELETE = 2; +} + +message Constraint_Custom { + string constraint_type = 1; + string constraint_value = 2; +} + +message Constraint_Schedule { + double start_timestamp = 1; + float duration_days = 2; +} + +message GPS_Position { + float latitude = 1; + float longitude = 2; +} + +message Location { + oneof location { + string region = 1; + GPS_Position gps_position = 2; + } +} + +message Constraint_EndPointLocation { + EndPointId endpoint_id = 1; + Location location = 2; +} + +message Constraint_EndPointPriority { + EndPointId endpoint_id = 1; + uint32 priority = 2; +} + +message Constraint_SLA_Latency { + float e2e_latency_ms = 1; +} + +message Constraint_SLA_Capacity { + float capacity_gbps = 1; +} + +message Constraint_SLA_Availability { + uint32 num_disjoint_paths = 1; + bool all_active = 2; + float availability = 3; // 0.0 .. 100.0 percentage of availability +} + +enum IsolationLevelEnum { + NO_ISOLATION = 0; + PHYSICAL_ISOLATION = 1; + LOGICAL_ISOLATION = 2; + PROCESS_ISOLATION = 3; + PHYSICAL_MEMORY_ISOLATION = 4; + PHYSICAL_NETWORK_ISOLATION = 5; + VIRTUAL_RESOURCE_ISOLATION = 6; + NETWORK_FUNCTIONS_ISOLATION = 7; + SERVICE_ISOLATION = 8; +} + +message Constraint_SLA_Isolation_level { + repeated IsolationLevelEnum isolation_level = 1; +} + +message Constraint_Exclusions { + bool is_permanent = 1; + repeated DeviceId device_ids = 2; + repeated EndPointId endpoint_ids = 3; + repeated LinkId link_ids = 4; +} + + +message QoSProfileId { + context.Uuid qos_profile_id = 1; +} + +message Constraint_QoSProfile { + QoSProfileId qos_profile_id = 1; + string qos_profile_name = 2; +} + +message Constraint { + ConstraintActionEnum action = 1; + oneof constraint { + Constraint_Custom custom = 2; + Constraint_Schedule schedule = 3; + Constraint_EndPointLocation endpoint_location = 4; + Constraint_EndPointPriority endpoint_priority = 5; + Constraint_SLA_Capacity sla_capacity = 6; + Constraint_SLA_Latency sla_latency = 7; + Constraint_SLA_Availability sla_availability = 8; + Constraint_SLA_Isolation_level sla_isolation = 9; + Constraint_Exclusions exclusions = 10; + Constraint_QoSProfile qos_profile = 11; + } +} + + +// ----- Miscellaneous ------------------------------------------------------------------------------------------------- +message TeraFlowController { + ContextId context_id = 1; + string ip_address = 2; + uint32 port = 3; +} + +message AuthenticationResult { + ContextId context_id = 1; + bool authenticated = 2; +} + +// ---------------- Experimental ------------------------ +message OpticalConfigId { + string opticalconfig_uuid = 1; +} +message OpticalConfig { + OpticalConfigId opticalconfig_id = 1; + string config = 2; +} + +message OpticalConfigList { + repeated OpticalConfig opticalconfigs = 1; +} + +// ---- Optical Link ---- + +message OpticalLinkId { + Uuid optical_link_uuid = 1; +} + +message FiberId { + Uuid fiber_uuid = 1; +} + +message Fiber { + string ID = 10; + string src_port = 1; + string dst_port = 2; + string local_peer_port = 3; + string remote_peer_port = 4; + repeated int32 c_slots = 5; + repeated int32 l_slots = 6; + repeated int32 s_slots = 7; + float length = 8; + bool used = 9; + FiberId fiber_uuid = 11; + +} +message OpticalLinkDetails { + float length = 1; + string source = 2; + string target = 3; + repeated Fiber fibers = 4; +} + +message OpticalLink { + string name = 1; + OpticalLinkDetails details = 2; + OpticalLinkId optical_link_uuid = 3; +} diff --git a/tmp-code/run_openflow.sh b/tmp-code/run_openflow.sh new file mode 100755 index 000000000..2c525ca70 --- /dev/null +++ b/tmp-code/run_openflow.sh @@ -0,0 +1,8 @@ +PROJECTDIR=`pwd` + +cd $PROJECTDIR/src +RCFILE=$PROJECTDIR/coverage/.coveragerc + +# Run unitary tests and analyze coverage of code at same time +coverage run --rcfile=$RCFILE --append -m pytest --log-level=DEBUG --verbose \ + device/tests/test_OpenFlow.py \ No newline at end of file diff --git a/tmp-code/test_OpenFlow.py b/tmp-code/test_OpenFlow.py new file mode 100644 index 000000000..60ee4542c --- /dev/null +++ b/tmp-code/test_OpenFlow.py @@ -0,0 +1,77 @@ +import json +from re import A +import resource +import logging, os, sys, time +#from typing import Dict, Self, Tuple +os.environ['DEVICE_EMULATED_ONLY'] = 'YES' +from device.service.drivers.OpenFlow.OpenFlowDriver import OpenFlowDriver +logging.basicConfig(level=logging.DEBUG) +LOGGER = logging.getLogger(__name__) +LOGGER.setLevel(logging.DEBUG) + + +def test_main(): + driver_settings = { + 'protocol': 'http', + 'username': None, + 'password': None, + 'use_tls': False, + } + driver = OpenFlowDriver('127.0.0.1', 8080 , **driver_settings) + driver.Connect() + + + # Test: GetConfig + #resource_keys = [ 'flows:1','description:1','switches','port_description:1','switch_info','links_info'] + # config = driver.GetConfig(resource_keys ) + # LOGGER.info('Specific configuration: %s', config) + + #resource_delete=["flowentry_delete:1"] + #config = driver.DeleteConfig(resource_delete) + #LOGGER.info('Specific configuration: %s', config) + #a=driver.GetConfig(["flows:1"]) + #LOGGER.info('flow 1 = {:s}'.format(str(a))) +# delete_data = { +# "dpid": 2, +# "cookie": 1, +# "cookie_mask": 1, +# "table_id": 0, +# "idle_timeout": 30, +# "hard_timeout": 30, +# "priority": 11111, +# "flags": 1, +# "match":{ +# "in_port":2 +# }, +# "actions":[ +# { +# "type":"ddf", +# "port": 1 +# } +# ] +# } +# delete_result = driver.DeleteConfig([("flow_data", delete_data)]) +# LOGGER.info('resources_to_delete = {:s}'.format(str(delete_result))) +# a=driver.GetConfig(["flows:1"]) +# LOGGER.info('flow 2 = {:s}'.format(str(a))) + flow_data = { + "dpid": 2, + "priority": 22224, + "match": { + "in_port": 1 + }, + "actions": [ + { + "type": "GOTO_TABLE", + "table_id": 1 + } + ] + } + set_result = driver.SetConfig([('flow_data',flow_data)]) + LOGGER.info(set_result) + driver.Disconnect() + + raise Exception () + +if __name__ == '__main__': + sys.exit(test_main()) -- GitLab