diff --git a/proto/context.proto b/proto/context.proto index 85972d956a93dfe09ec9a955cf304c8b3e298bb3..b125646c094fcc1d20fe7e4c895f034f1602f031 100644 --- a/proto/context.proto +++ b/proto/context.proto @@ -215,6 +215,7 @@ enum DeviceDriverEnum { DEVICEDRIVER_IETF_ACTN = 10; DEVICEDRIVER_OC = 11; DEVICEDRIVER_QKD = 12; + DEVICEDRIVER_IETF_L3VPN = 13; } enum DeviceOperationalStatusEnum { diff --git a/src/device/service/drivers/__init__.py b/src/device/service/drivers/__init__.py index a5e7f377113342b98203a23a426540f6188f784e..ff5aa44f9042751dc1ec6260a832fcffec9045a5 100644 --- a/src/device/service/drivers/__init__.py +++ b/src/device/service/drivers/__init__.py @@ -81,6 +81,16 @@ DRIVERS.append( } ])) + +from .ietf_l3vpn.driver import IetfL3VpnDriver # pylint: disable=wrong-import-position +DRIVERS.append( + (IetfL3VpnDriver, [ + { + FilterFieldEnum.DEVICE_TYPE: DeviceTypeEnum.TERAFLOWSDN_CONTROLLER, + FilterFieldEnum.DRIVER: DeviceDriverEnum.DEVICEDRIVER_IETF_L3VPN, + } + ])) + from .ietf_actn.IetfActnDriver import IetfActnDriver # pylint: disable=wrong-import-position DRIVERS.append( (IetfActnDriver, [ diff --git a/src/device/service/drivers/ietf_l3vpn/TfsApiClient.py b/src/device/service/drivers/ietf_l3vpn/TfsApiClient.py new file mode 100644 index 0000000000000000000000000000000000000000..86ee2b1bed13c09f2a024f5bb8790590f09e1d8a --- /dev/null +++ b/src/device/service/drivers/ietf_l3vpn/TfsApiClient.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 logging +from typing import Dict, List, Optional + +import requests +from requests.auth import HTTPBasicAuth + +from device.service.driver_api.ImportTopologyEnum import ImportTopologyEnum + +GET_DEVICES_URL = "{:s}://{:s}:{:d}/tfs-api/devices" +GET_LINKS_URL = "{:s}://{:s}:{:d}/tfs-api/links" +L3VPN_URL = "{:s}://{:s}:{:d}/restconf/data/ietf-l3vpn-svc:l3vpn-svc/vpn-services" +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, +} + +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) + 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, import_topology: ImportTopologyEnum = ImportTopologyEnum.DEVICES + ) -> List[Dict]: + LOGGER.debug("[get_devices_endpoints] begin") + LOGGER.debug( + "[get_devices_endpoints] import_topology={:s}".format(str(import_topology)) + ) + + reply = requests.get( + self._devices_url, timeout=TIMEOUT, verify=False, auth=self._auth + ) + if reply.status_code not in HTTP_OK_CODES: + msg = MSG_ERROR.format( + str(self._devices_url), str(reply.status_code), str(reply) + ) + LOGGER.error(msg) + raise Exception(msg) + + if import_topology == ImportTopologyEnum.DISABLED: + raise Exception( + "Unsupported import_topology mode: {:s}".format(str(import_topology)) + ) + + result = list() + for json_device in reply.json()["devices"]: + device_uuid: str = json_device["device_id"]["device_uuid"]["uuid"] + device_type: str = json_device["device_type"] + device_status = json_device["device_operational_status"] + device_url = "/devices/device[{:s}]".format(device_uuid) + device_data = { + "uuid": json_device["device_id"]["device_uuid"]["uuid"], + "name": json_device["name"], + "type": device_type, + "status": MAPPING_STATUS[device_status], + "drivers": [ + MAPPING_DRIVER[driver] for driver in json_device["device_drivers"] + ], + } + result.append((device_url, device_data)) + + for json_endpoint in json_device["device_endpoints"]: + endpoint_uuid = json_endpoint["endpoint_id"]["endpoint_uuid"]["uuid"] + endpoint_url = "/endpoints/endpoint[{:s}]".format(endpoint_uuid) + endpoint_data = { + "device_uuid": device_uuid, + "uuid": endpoint_uuid, + "name": json_endpoint["name"], + "type": json_endpoint["endpoint_type"], + } + result.append((endpoint_url, endpoint_data)) + + if import_topology == ImportTopologyEnum.DEVICES: + LOGGER.debug("[get_devices_endpoints] devices only; returning") + return result + + 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()["links"]: + link_uuid: str = json_link["link_id"]["link_uuid"]["uuid"] + link_url = "/links/link[{:s}]".format(link_uuid) + link_endpoint_ids = [ + ( + json_endpoint_id["device_id"]["device_uuid"]["uuid"], + json_endpoint_id["endpoint_uuid"]["uuid"], + ) + for json_endpoint_id in json_link["link_endpoint_ids"] + ] + link_data = { + "uuid": json_link["link_id"]["link_uuid"]["uuid"], + "name": json_link["name"], + "endpoints": link_endpoint_ids, + } + result.append((link_url, link_data)) + + LOGGER.debug("[get_devices_endpoints] topology; returning") + return result + + def create_connectivity_service(self, l3vpn_data: dict) -> None: + try: + requests.post(L3VPN_URL, json=l3vpn_data, auth=self._auth) + except requests.exceptions.ConnectionError: + raise Exception("faild to send post request to TFS L3VPN NBI") + + def delete_connectivity_service(self, service_uuid: str) -> None: + url = L3VPN_URL + f"/vpn-service={service_uuid}" + try: + requests.delete(url, auth=self._auth) + except requests.exceptions.ConnectionError: + raise Exception("faild to send delete request to TFS L3VPN NBI") diff --git a/src/device/service/drivers/ietf_l3vpn/Tools.py b/src/device/service/drivers/ietf_l3vpn/Tools.py new file mode 100644 index 0000000000000000000000000000000000000000..8930cde00ad08f6d05126c39759bc04b62dd25a8 --- /dev/null +++ b/src/device/service/drivers/ietf_l3vpn/Tools.py @@ -0,0 +1,284 @@ +# 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 logging +from typing import TypedDict + +import requests + + +class LANPrefixesDict(TypedDict): + lan: str + lan_tag: str + + +LOGGER = logging.getLogger(__name__) + +SITE_NETWORK_ACCESS_TYPE = "ietf-l3vpn-svc:multipoint" + + +def service_exists(wim_url: str, auth, service_uuid: str) -> bool: + try: + get_connectivity_service(wim_url, auth, service_uuid) + return True + except: # pylint: disable=bare-except + return False + + +def get_all_active_connectivity_services(wim_url: str, auth): + try: + LOGGER.info("Sending get all connectivity services") + servicepoint = f"{wim_url}/restconf/data/ietf-l3vpn-svc:l3vpn-svc/vpn-services" + response = requests.get(servicepoint, auth=auth) + + if response.status_code != requests.codes.ok: + raise Exception( + "Unable to get all connectivity services", + http_code=response.status_code, + ) + + return response + except requests.exceptions.ConnectionError: + raise Exception("Request Timeout", http_code=408) + + +def get_connectivity_service(wim_url, auth, service_uuid): + try: + LOGGER.info("Sending get connectivity service") + servicepoint = f"{wim_url}/restconf/data/ietf-l3vpn-svc:l3vpn-svc/vpn-service={service_uuid}" + + response = requests.get(servicepoint, auth=auth) + + if response.status_code != requests.codes.ok: + raise Exception( + "Unable to get connectivity service{:s}".format(str(service_uuid)), + http_code=response.status_code, + ) + + return response + except requests.exceptions.ConnectionError: + raise Exception("Request Timeout", http_code=408) + + +def create_l3vpn_datamodel(service_uuid, resource_value: dict) -> dict: + src_device_uuid: str = resource_value["src_device_name"] + src_endpoint_uuid: str = resource_value["src_endpoint_name"] + src_site_location: str = resource_value["src_site_location"] + src_ipv4_lan_prefixes: list[LANPrefixesDict] = resource_value.get( + "src_ipv4_lan_prefixes" + ) + src_site_id: str = resource_value.get("src_site_id", f"site_{src_site_location}") + src_management_type: str = resource_value.get( + "src_management_type", "ietf-l3vpn-svc:provider-managed" + ) + if src_management_type != "ietf-l3vpn-svc:provider-managed": + raise Exception("management type %s not supported", src_management_type) + src_role: str = "ietf-l3vpn-svc:hub-role" + src_ce_address: str = resource_value["src_ce_address"] + src_pe_address: str = resource_value["src_pe_address"] + src_ce_pe_network_prefix: int = resource_value["src_ce_pe_network_prefix"] + src_mtu: int = resource_value["src_mtu"] + src_input_bw: int = resource_value["src_input_bw"] + src_output_bw: int = resource_value["src_output_bw"] + src_qos_profile_id: str = resource_value["src_qos_profile_id"] + src_qos_profile_direction: str = resource_value["src_qos_profile_direction"] + src_qos_profile_latency: int = resource_value["src_qos_profile_latency"] + src_qos_profile_bw_guarantee: int = resource_value["src_qos_profile_bw_guarantee"] + + dst_device_uuid = resource_value["dst_device_name"] + dst_endpoint_uuid = resource_value["dst_endpoint_name"] + dst_site_location: str = resource_value["dst_site_location"] + dst_ipv4_lan_prefixes: list[LANPrefixesDict] = resource_value.get( + "dst_ipv4_lan_prefixes" + ) + dst_site_id: str = resource_value.get("dst_site_id", f"site_{dst_site_location}") + dst_management_type: str = resource_value.get( + "dst_management_type", "ietf-l3vpn-svc:provider-managed" + ) + if dst_management_type != "ietf-l3vpn-svc:provider-managed": + raise Exception("management type %s not supported", dst_management_type) + dst_role: str = "ietf-l3vpn-svc:spoke-role" + dst_ce_address: str = resource_value["dst_ce_address"] + dst_pe_address: str = resource_value["dst_pe_address"] + dst_ce_pe_network_prefix: int = resource_value["dst_ce_pe_network_prefix"] + dst_mtu: int = resource_value["dst_mtu"] + dst_input_bw: int = resource_value["dst_input_bw"] + dst_output_bw: int = resource_value["dst_output_bw"] + dst_qos_profile_id: str = resource_value["dst_qos_profile_id"] + dst_qos_profile_direction: str = resource_value["dst_qos_profile_direction"] + dst_qos_profile_latency: int = resource_value["dst_qos_profile_latency"] + dst_qos_profile_bw_guarantee: int = resource_value["dst_qos_profile_bw_guarantee"] + + # Create source site information + src_management = {"type": "ietf-l3vpn-svc:provider-managed"} + src_locations = {"location": [{"location-id": src_site_location}]} + src_devices = { + "device": [{"device-id": src_device_uuid, "location": src_site_location}] + } + src_site_lan_prefixes = [ + {"lan": lp["lan"], "lan-tag": lp["lan_tag"], "next-hop": src_ce_address} + for lp in src_ipv4_lan_prefixes + ] + src_site_routing_protocols = { + "routing-protocol": [ + { + "type": "ietf-l3vpn-svc:static", + "static": { + "cascaded-lan-prefixes": { + "ipv4-lan-prefixes": src_site_lan_prefixes + } + }, + } + ] + } + src_site_network_accesses = { + "site-network-access": [ + { + "site-network-access-id": src_endpoint_uuid, + "site-network-access-type": SITE_NETWORK_ACCESS_TYPE, + "device-reference": src_device_uuid, + "vpn-attachment": {"vpn-id": service_uuid, "site-role": src_role}, + "ip-connection": { + "ipv4": { + "address-allocation-type": "ietf-l3vpn-svc:static-address", + "addresses": { + "provider-address": src_pe_address, + "customer-address": src_ce_address, + "prefix-length": src_ce_pe_network_prefix, + }, + } + }, + "service": { + "svc-mtu": src_mtu, + "svc-input-bandwidth": src_input_bw, + "svc-output-bandwidth": src_output_bw, + "qos": { + "qos-profile": { + "classes": { + "class": [ + { + "class-id": src_qos_profile_id, + "direction": src_qos_profile_direction, + "latency": { + "latency-boundary": src_qos_profile_latency + }, + "bandwidth": { + "guaranteed-bw-percent": src_qos_profile_bw_guarantee + }, + } + ] + } + } + }, + }, + } + ] + } + + # Create destination site information + dst_management = {"type": "ietf-l3vpn-svc:provider-managed"} + dst_locations = {"location": [{"location-id": dst_site_location}]} + dst_devices = { + "device": [{"device-id": dst_device_uuid, "location": dst_site_location}] + } + dst_site_lan_prefixes = [ + {"lan": lp["lan"], "lan-tag": lp["lan_tag"], "next-hop": dst_ce_address} + for lp in dst_ipv4_lan_prefixes + ] + dst_site_routing_protocols = { + "routing-protocol": [ + { + "type": "ietf-l3vpn-svc:static", + "static": { + "cascaded-lan-prefixes": { + "ipv4-lan-prefixes": dst_site_lan_prefixes + } + }, + } + ] + } + dst_site_network_accesses = { + "site-network-access": [ + { + "site-network-access-id": dst_endpoint_uuid, + "site-network-access-type": SITE_NETWORK_ACCESS_TYPE, + "device-reference": dst_device_uuid, + "vpn-attachment": {"vpn-id": service_uuid, "site-role": dst_role}, + "ip-connection": { + "ipv4": { + "address-allocation-type": "ietf-l3vpn-svc:static-address", + "addresses": { + "provider-address": dst_pe_address, + "customer-address": dst_ce_address, + "prefix-length": dst_ce_pe_network_prefix, + }, + } + }, + "service": { + "svc-mtu": dst_mtu, + "svc-input-bandwidth": dst_input_bw, + "svc-output-bandwidth": dst_output_bw, + "qos": { + "qos-profile": { + "classes": { + "class": [ + { + "class-id": dst_qos_profile_id, + "direction": dst_qos_profile_direction, + "latency": { + "latency-boundary": dst_qos_profile_latency + }, + "bandwidth": { + "guaranteed-bw-percent": dst_qos_profile_bw_guarantee + }, + } + ] + } + } + }, + }, + } + ] + } + + sites = { + "site": [ + { + "site-id": src_site_id, + "management": src_management, + "locations": src_locations, + "devices": src_devices, + "routing-protocols": src_site_routing_protocols, + "site-network-accesses": src_site_network_accesses, + }, + { + { + "site-id": dst_site_id, + "management": dst_management, + "locations": dst_locations, + "devices": dst_devices, + "routing-protocols": dst_site_routing_protocols, + "site-network-accesses": dst_site_network_accesses, + }, + }, + ] + } + + l3_vpn_data_model = { + "ietf-l3vpn-svc:l3vpn-svc": { + "vpn-services": {"vpn-service": [{"vpn-id": service_uuid}]}, + "sites": sites, + } + } + + return l3_vpn_data_model diff --git a/src/device/service/drivers/ietf_l3vpn/__init__.py b/src/device/service/drivers/ietf_l3vpn/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..bbfc943b68af13a11e562abbc8680ade71db8f02 --- /dev/null +++ b/src/device/service/drivers/ietf_l3vpn/__init__.py @@ -0,0 +1,13 @@ +# 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. diff --git a/src/device/service/drivers/ietf_l3vpn/driver.py b/src/device/service/drivers/ietf_l3vpn/driver.py new file mode 100644 index 0000000000000000000000000000000000000000..3d3fa2299cb7517912206fae6476aa2ef448afb2 --- /dev/null +++ b/src/device/service/drivers/ietf_l3vpn/driver.py @@ -0,0 +1,240 @@ +# 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 +import threading +from typing import Any, Iterator, List, Optional, Tuple, Union + +import requests +from requests.auth import HTTPBasicAuth + +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 ( + RESOURCE_ENDPOINTS, + RESOURCE_SERVICES, + _Driver, +) +from device.service.driver_api.ImportTopologyEnum import ( + ImportTopologyEnum, + get_import_topology, +) + +from .TfsApiClient import TfsApiClient +from .Tools import ( + create_l3vpn_datamodel, + get_all_active_connectivity_services, + get_connectivity_service, + service_exists, +) + +LOGGER = logging.getLogger(__name__) + + +ALL_RESOURCE_KEYS = [ + RESOURCE_ENDPOINTS, + RESOURCE_SERVICES, +] + +DRIVER_NAME = "ietf_l3vpn" +METRICS_POOL = MetricsPool("Device", "Driver", labels={"driver": DRIVER_NAME}) + + +class IetfL3VpnDriver(_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() + scheme = self.settings.get("scheme", "http") + username = self.settings.get("username") + password = self.settings.get("password") + self.tac = TfsApiClient( + self.address, + int(self.port), + scheme=scheme, + username=username, + password=password, + ) + self.__auth = ( + HTTPBasicAuth(username, password) + if username is not None and password is not None + else None + ) + self.__tfs_nbi_root = "{:s}://{:s}:{:d}".format( + scheme, self.address, int(self.port) + ) + self.__timeout = int(self.settings.get("timeout", 120)) + self.__import_topology = get_import_topology( + self.settings, default=ImportTopologyEnum.DEVICES + ) + + def Connect(self) -> bool: + url = ( + self.__tfs_nbi_root + "/restconf/data/ietf-l3vpn-svc:l3vpn-svc/vpn-services" + ) + with self.__lock: + if self.__started.is_set(): + return True + try: + requests.get( + url, timeout=self.__timeout, verify=False, auth=self.__auth + ) + except requests.exceptions.Timeout: + LOGGER.exception( + "Timeout connecting {:s}".format(str(self.__tapi_root)) + ) + return False + except Exception: # pylint: disable=broad-except + LOGGER.exception( + "Exception connecting {:s}".format(str(self.__tapi_root)) + ) + 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 + 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: + # return endpoints through TFS NBI API and list-devices method + results.extend( + self.tac.get_devices_endpoints(self.__import_topology) + ) + elif resource_key == RESOURCE_SERVICES: + # return all services through + reply = get_all_active_connectivity_services( + wim_url=self.__tfs_nbi_root, auth=self.__auth + ) + results.extend(reply.json()) + else: + # assume single-service retrieval + reply = get_connectivity_service( + self.__tfs_nbi_root, resource_key + ) + results.append(reply.json()) + except Exception as e: # pylint: disable=broad-except + 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 SetConfig( + self, resources: List[Tuple[str, Any]] + ) -> List[Union[bool, Exception]]: + results = [] + if len(resources) == 0: + return results + with self.__lock: + for resource in resources: + LOGGER.info("resource = {:s}".format(str(resource))) + resource_key, resource_value = resource + try: + resource_value = json.loads(resource_value) + + service_uuid = resource_value["uuid"] #! fix based on resources + + if service_exists(self.__tfs_nbi_root, self.__auth, service_uuid): + exc = NotImplementedError( + "IETF L3VPN Service Update is still not supported" + ) + results.append((resource[0], exc)) + continue + + l3vpn_datamodel = create_l3vpn_datamodel( + service_uuid, resource_value + ) + self.tac.create_connectivity_service(l3vpn_datamodel) + results.append((resource_key, True)) + except Exception as e: # pylint: disable=broad-except + 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 DeleteConfig( + self, resources: List[Tuple[str, Any]] + ) -> List[Union[bool, Exception]]: + results = [] + if len(resources) == 0: + return results + with self.__lock: + for resource in resources: + LOGGER.info("resource = {:s}".format(str(resource))) + resource_key, resource_value = resource + try: + resource_value = json.loads(resource_value) + service_uuid = resource_value["uuid"] + + if service_exists(self.__tfs_nbi_root, self.__auth, service_uuid): + self.tac.delete_connectivity_service(service_uuid) + results.append((resource_key, True)) + except Exception as e: # pylint: disable=broad-except + 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 SubscribeState( + self, subscriptions: List[Tuple[str, float, float]] + ) -> List[Union[bool, Exception]]: + # TODO: IETF L3VPN 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: IETF L3VPN 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: IETF L3VPN does not support monitoring by now + return []