diff --git a/src/device/service/drivers/ietf_actn/IetfActnDriver.py b/src/device/service/drivers/ietf_actn/IetfActnDriver.py index c31bd85b92c3421b8cdc6001a25f9a2e5e2a3b47..6d0aada4e2ae512192ac79799cdebe1cb4b14fac 100644 --- a/src/device/service/drivers/ietf_actn/IetfActnDriver.py +++ b/src/device/service/drivers/ietf_actn/IetfActnDriver.py @@ -13,7 +13,6 @@ # limitations under the License. import json, 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 @@ -27,8 +26,6 @@ LOGGER = logging.getLogger(__name__) DRIVER_NAME = 'ietf_actn' METRICS_POOL = MetricsPool('Device', 'Driver', labels={'driver': DRIVER_NAME}) -DEFAULT_BASE_URL = '/restconf/data' -DEFAULT_TIMEOUT = 120 class IetfActnDriver(_Driver): def __init__(self, address: str, port: int, **settings) -> None: diff --git a/src/device/service/drivers/ietf_actn/handlers/EthService.py b/src/device/service/drivers/ietf_actn/handlers/EthService.py index 0d923b16cd7bcc9c0cb1c01e38c66336a1c78cd1..e8fe9817bcf2d0f3eedf87c5f79d5e1de2030861 100644 --- a/src/device/service/drivers/ietf_actn/handlers/EthService.py +++ b/src/device/service/drivers/ietf_actn/handlers/EthService.py @@ -12,10 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -import enum -from typing import Dict, List, Tuple +import enum, json, logging, operator, requests +from typing import Any, Dict, List, Tuple, Union +from device.service.driver_api._Driver import RESOURCE_ENDPOINTS, RESOURCE_SERVICES +from .RestApiClient import RestApiClient -OSU_TUNNEL_URL = '/restconf/data/ietf-te:tunnel' +LOGGER = logging.getLogger(__name__) class BandwidthProfileTypeEnum(enum.Enum): MEF_10_BWP = 'ietf-eth-tran-types:mef-10-bwp' @@ -104,3 +106,41 @@ def compose_etht_service( 'svc-tunnel': [{'tunnel-name': osu_tunnel_name}], 'optimizations': compose_optimizations(), }]}} + +class EthtServiceHandler: + def __init__(self, rest_api_client : RestApiClient) -> None: + self._rest_api_client = rest_api_client + self._object_name = 'EthtService' + self._subpath_url = '/ietf-eth-tran-service:etht-svc/etht-svc-instances' + + def get(self) -> List[Tuple[str, Any]]: + pass + + def update(self, parameters : Dict) -> List[Union[bool, Exception]]: + name = parameters['name' ] + service_type = parameters['service_type' ] + osu_tunnel_name = parameters['osu_tunnel_name'] + + src_node_id = parameters['src_node_id' ] + src_tp_id = parameters['src_tp_id' ] + src_vlan_tag = parameters['src_vlan_tag' ] + src_static_routes = parameters.get('src_static_routes', []) + + dst_node_id = parameters['dst_node_id' ] + dst_tp_id = parameters['dst_tp_id' ] + dst_vlan_tag = parameters['dst_vlan_tag' ] + dst_static_routes = parameters.get('dst_static_routes', []) + + service_type = ServiceTypeEnum._value2member_map_[service_type] + + data = compose_etht_service( + name, service_type, osu_tunnel_name, + src_node_id, src_tp_id, src_vlan_tag, dst_node_id, dst_tp_id, dst_vlan_tag, + src_static_routes=src_static_routes, dst_static_routes=dst_static_routes + ) + + return self._rest_api_client.update(self._object_name, self._subpath_url, data) + + def delete(self, etht_service_name : str) -> List[Union[bool, Exception]]: + filters = [('etht-svc-name', etht_service_name)] + return self._rest_api_client.delete(self._object_name, self._subpath_url, filters) diff --git a/src/device/service/drivers/ietf_actn/handlers/OsuTunnel.py b/src/device/service/drivers/ietf_actn/handlers/OsuTunnel.py index a15f73eab7e550d3ba43447ba260cee19daf7fff..d6332a8d7cc6aa660b0959f328e3a8d5ad69ebfa 100644 --- a/src/device/service/drivers/ietf_actn/handlers/OsuTunnel.py +++ b/src/device/service/drivers/ietf_actn/handlers/OsuTunnel.py @@ -13,15 +13,12 @@ # limitations under the License. import enum, json, logging, operator, requests -from requests.auth import HTTPBasicAuth -from typing import Dict, List, Optional +from typing import Any, Dict, List, Tuple, Union from device.service.driver_api._Driver import RESOURCE_ENDPOINTS, RESOURCE_SERVICES -from .Tools import HTTP_OK_CODES +from .RestApiClient import RestApiClient LOGGER = logging.getLogger(__name__) -BASE_URL_OSU_TUNNEL = '{:s}/ietf-te:tunnel' - class EndpointProtectionRoleEnum(enum.Enum): WORK = 'work' @@ -84,11 +81,14 @@ def compose_osu_tunnel( 'protection': compose_osu_tunnel_protection(), }]} -class OsuTunnel: - def __init__(self, base_url : str, auth : Optional[HTTPBasicAuth] = None, timeout : Optional[int] = None) -> None: - self._base_url = base_url - self._auth = auth - self._timeout = timeout +class OsuTunnelHandler: + def __init__(self, rest_api_client : RestApiClient) -> None: + self._rest_api_client = rest_api_client + self._object_name = 'OsuTunnel' + self._subpath_url = '/ietf-te:tunnel' + + def get(self) -> List[Tuple[str, Any]]: + pass def get(self, resource_key : str) -> None: url = '{:s}/restconf/data/tapi-common:context'.format(base_url) @@ -110,38 +110,7 @@ class OsuTunnel: result.append((resource_key, e)) return result - if resource_key == RESOURCE_ENDPOINTS: - if 'tapi-common:context' in context: - context = context['tapi-common:context'] - elif 'context' in context: - context = context['context'] - - for sip in context['service-interface-point']: - layer_protocol_name = sip.get('layer-protocol-name', '?') - supportable_spectrum = sip.get('tapi-photonic-media:media-channel-service-interface-point-spec', {}) - supportable_spectrum = supportable_spectrum.get('mc-pool', {}) - supportable_spectrum = supportable_spectrum.get('supportable-spectrum', []) - supportable_spectrum = supportable_spectrum[0] if len(supportable_spectrum) == 1 else {} - grid_type = supportable_spectrum.get('frequency-constraint', {}).get('grid-type') - granularity = supportable_spectrum.get('frequency-constraint', {}).get('adjustment-granularity') - direction = sip.get('direction', '?') - - endpoint_type = [layer_protocol_name, grid_type, granularity, direction] - str_endpoint_type = ':'.join(filter(lambda i: operator.is_not(i, None), endpoint_type)) - sip_uuid = sip['uuid'] - - sip_names = sip.get('name', []) - sip_name = next(iter([ - sip_name['value'] - for sip_name in sip_names - if sip_name['value-name'] == 'local-name' - ]), sip_uuid) - - endpoint_url = '/endpoints/endpoint[{:s}]'.format(sip_uuid) - endpoint_data = {'uuid': sip_uuid, 'name': sip_name, 'type': str_endpoint_type} - result.append((endpoint_url, endpoint_data)) - - elif resource_key == RESOURCE_SERVICES: + if resource_key == RESOURCE_SERVICES: if 'tapi-common:context' in context: context = context['tapi-common:context'] elif 'context' in context: @@ -181,56 +150,31 @@ class OsuTunnel: return result - def update(self, resource_value : Dict) -> None: - name = resource_value['name' ] - src_node_id = resource_value['src_node_id' ] - src_tp_id = resource_value['src_tp_id' ] - src_ttp_channel_name = resource_value['src_ttp_channel_name'] - dst_node_id = resource_value['dst_node_id' ] - dst_tp_id = resource_value['dst_tp_id' ] - dst_ttp_channel_name = resource_value['dst_ttp_channel_name'] - odu_type = resource_value.get('odu_type', OduTypeEnum.OSUFLEX.value) - osuflex_number = resource_value.get('osuflex_number', 1 ) - delay = resource_value.get('delay', 20 ) - bidirectional = resource_value.get('bidirectional', True ) + def update(self, parameters : Dict) -> List[Union[bool, Exception]]: + name = parameters['name' ] + + src_node_id = parameters['src_node_id' ] + src_tp_id = parameters['src_tp_id' ] + src_ttp_channel_name = parameters['src_ttp_channel_name'] + + dst_node_id = parameters['dst_node_id' ] + dst_tp_id = parameters['dst_tp_id' ] + dst_ttp_channel_name = parameters['dst_ttp_channel_name'] + + odu_type = parameters.get('odu_type', OduTypeEnum.OSUFLEX.value) + osuflex_number = parameters.get('osuflex_number', 1 ) + delay = parameters.get('delay', 20 ) + bidirectional = parameters.get('bidirectional', True ) odu_type = OduTypeEnum._value2member_map_[odu_type] - headers = {'content-type': 'application/json'} data = compose_osu_tunnel( name, src_node_id, src_tp_id, src_ttp_channel_name, dst_node_id, dst_tp_id, dst_ttp_channel_name, odu_type, osuflex_number, delay, bidirectional=bidirectional ) - results = [] - try: - LOGGER.info('OSU Tunnel {:s}: {:s}'.format(str(name), str(data))) - response = requests.post( - self._base_url, data=json.dumps(data), timeout=self._timeout, - headers=headers, verify=False, auth=self._auth - ) - LOGGER.info('Response: {:s}'.format(str(response))) - except Exception as e: # pylint: disable=broad-except - LOGGER.exception('Exception creating OsuTunnel(name={:s}, data={:s})'.format(str(name), str(data))) - results.append(e) - else: - if response.status_code not in HTTP_OK_CODES: - msg = 'Could not create OsuTunnel(name={:s}, data={:s}). status_code={:s} reply={:s}' - LOGGER.error(msg.format(str(name), str(data), str(response.status_code), str(response))) - results.append(response.status_code in HTTP_OK_CODES) - return results - - def delete(self, osu_tunnel_name : str) -> List[]: - url = '{:s}[name={:s}]'.format(self._base_url, osu_tunnel_name) - results = [] - try: - response = requests.delete(url=url, timeout=self._timeout, verify=False, auth=self._auth) - except Exception as e: # pylint: disable=broad-except - LOGGER.exception('Exception deleting OsuTunnel(name={:s})'.format(str(osu_tunnel_name))) - results.append(e) - else: - if response.status_code not in HTTP_OK_CODES: - msg = 'Could not delete OsuTunnel(name={:s}). status_code={:s} reply={:s}' - LOGGER.error(msg.format(str(osu_tunnel_name), str(response.status_code), str(response))) - results.append(response.status_code in HTTP_OK_CODES) - return results + return self._rest_api_client.update(self._object_name, self._subpath_url, data) + + def delete(self, osu_tunnel_name : str) -> List[Union[bool, Exception]]: + filters = [('name', osu_tunnel_name)] + return self._rest_api_client.delete(self._object_name, self._subpath_url, filters) diff --git a/src/device/service/drivers/ietf_actn/handlers/RestApiClient.py b/src/device/service/drivers/ietf_actn/handlers/RestApiClient.py new file mode 100644 index 0000000000000000000000000000000000000000..8660f35ce1a6a8bd42cddcc2d0a50f81568fe27c --- /dev/null +++ b/src/device/service/drivers/ietf_actn/handlers/RestApiClient.py @@ -0,0 +1,143 @@ +# Copyright 2022-2023 ETSI TeraFlowSDN - TFS OSG (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 copy, json, logging, requests +from requests.auth import HTTPBasicAuth +from typing import Any, Dict, List, Tuple, Union + +LOGGER = logging.getLogger(__name__) + +DEFAULT_BASE_URL = '/restconf/data' +DEFAULT_SCHEMA = 'http' +DEFAULT_TIMEOUT = 120 +DEFAULT_VERIFY = False + +HTTP_OK_CODES = { + 200, # OK + 201, # Created + 202, # Accepted + 204, # No Content +} + +class RestApiClient: + def __init__(self, address : str, port : int, settings : Dict[str, Any] = dict()) -> None: + username = settings.get('username') + password = settings.get('password') + self._auth = HTTPBasicAuth(username, password) if username is not None and password is not None else None + + scheme = settings.get('scheme', DEFAULT_SCHEMA ) + base_url = settings.get('base_url', DEFAULT_BASE_URL) + self._base_url = '{:s}://{:s}:{:d}{:s}'.format(scheme, address, int(port), base_url) + + self._timeout = int(settings.get('timeout', DEFAULT_TIMEOUT)) + self._verify = int(settings.get('verify', DEFAULT_VERIFY )) + + + def get( + self, object_name : str, url : str, filters : List[Tuple[str, str]] + ) -> List[Union[Any, Exception]]: + str_filters = ''.join([ + '[{:s}={:s}]'.format(filter_field, filter_value) + for filter_field, filter_value in filters + ]) + + results = [] + try: + MSG = 'Get {:s}({:s})' + LOGGER.info(MSG.format(str(object_name), str(str_filters))) + response = requests.get( + self._base_url + url + str_filters, + timeout=self._timeout, verify=self._verify, auth=self._auth + ) + LOGGER.info(' Response: {:s}'.format(str(response))) + except Exception as e: # pylint: disable=broad-except + MSG = 'Exception Getting {:s}({:s})' + LOGGER.exception(MSG.format(str(object_name), str(str_filters))) + results.append(e) + return results + else: + if response.status_code not in HTTP_OK_CODES: + MSG = 'Could not get {:s}({:s}): status_code={:s} reply={:s}' + msg = MSG.format(str(object_name), str(str_filters), str(response.status_code), str(response)) + LOGGER.error(msg) + results.append(Exception(msg)) + return results + + try: + results.append(json.loads(response.content)) + except Exception: # pylint: disable=broad-except + MSG = 'Could not decode reply {:s}({:s}): {:s} {:s}' + msg = MSG.format(str(object_name), str(str_filters), str(response.status_code), str(response)) + LOGGER.exception(msg) + results.append(Exception(msg)) + + return results + + def update( + self, object_name : str, url : str, data : Dict, headers : Dict[str, Any] = dict() + ) -> List[Union[bool, Exception]]: + headers = copy.deepcopy(headers) + if 'content-type' not in {header_name.lower() for header_name in headers.keys()}: + headers.update({'content-type': 'application/json'}) + + results = [] + try: + MSG = 'Create/Update {:s}({:s})' + LOGGER.info(MSG.format(str(object_name), str(data))) + response = requests.post( + self._base_url + url, data=json.dumps(data), headers=headers, + timeout=self._timeout, verify=self._verify, auth=self._auth + ) + LOGGER.info(' Response: {:s}'.format(str(response))) + except Exception as e: # pylint: disable=broad-except + MSG = 'Exception Creating/Updating {:s}({:s})' + LOGGER.exception(MSG.format(str(object_name), str(data))) + results.append(e) + else: + if response.status_code not in HTTP_OK_CODES: + MSG = 'Could not create/update {:s}({:s}): status_code={:s} reply={:s}' + LOGGER.error(MSG.format(str(object_name), str(data), str(response.status_code), str(response))) + results.append(response.status_code in HTTP_OK_CODES) + + return results + + + def delete( + self, object_name : str, url : str, filters : List[Tuple[str, str]] + ) -> List[Union[bool, Exception]]: + str_filters = ''.join([ + '[{:s}={:s}]'.format(filter_field, filter_value) + for filter_field, filter_value in filters + ]) + + results = [] + try: + MSG = 'Delete {:s}({:s})' + LOGGER.info(MSG.format(str(object_name), str(str_filters))) + response = requests.delete( + self._base_url + url + str_filters, + timeout=self._timeout, verify=self._verify, auth=self._auth + ) + LOGGER.info(' Response: {:s}'.format(str(response))) + except Exception as e: # pylint: disable=broad-except + MSG = 'Exception Deleting {:s}({:s})' + LOGGER.exception(MSG.format(str(object_name), str(str_filters))) + results.append(e) + else: + if response.status_code not in HTTP_OK_CODES: + MSG = 'Could not delete {:s}({:s}): status_code={:s} reply={:s}' + LOGGER.error(MSG.format(str(object_name), str(str_filters), str(response.status_code), str(response))) + results.append(response.status_code in HTTP_OK_CODES) + + return results diff --git a/src/device/service/drivers/ietf_actn/handlers/Tools.py b/src/device/service/drivers/ietf_actn/handlers/Tools.py deleted file mode 100644 index c14c65afab4001fa5c3935d1a7ef4893d43342bc..0000000000000000000000000000000000000000 --- a/src/device/service/drivers/ietf_actn/handlers/Tools.py +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright 2022-2023 ETSI TeraFlowSDN - TFS OSG (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. - -HTTP_OK_CODES = { - 200, # OK - 201, # Created - 202, # Accepted - 204, # No Content -}