From b03b81da1e6cd408a0926d64ad556224d194a336 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Wed, 3 Jan 2024 13:36:13 +0000 Subject: [PATCH 01/18] IETF ACTN Driver: - Proto: Added to proto files - Common/type_checkers: added assertions - Context: Added to database model - Device: Added driver skeleton - Policy: Added Serializer - Service: Added to service handler filter fields - WebUI: Added to forms and pages - ZTP: Added Serializer --- proto/context.proto | 1 + src/common/type_checkers/Assertions.py | 2 + .../database/models/enums/DeviceDriver.py | 1 + .../drivers/ietf_actn/IetfActnDriver.py | 131 ++++++++++++ src/device/service/drivers/ietf_actn/Tools.py | 198 ++++++++++++++++++ .../service/drivers/ietf_actn/__init__.py | 20 ++ .../java/org/etsi/tfs/policy/Serializer.java | 12 ++ .../org/etsi/tfs/policy/SerializerTest.java | 9 + .../service_handler_api/FilterFields.py | 1 + src/webui/service/device/forms.py | 2 + src/webui/service/device/routes.py | 4 + src/webui/service/templates/device/add.html | 3 + .../java/org/etsi/tfs/ztp/Serializer.java | 12 ++ .../java/org/etsi/tfs/ztp/SerializerTest.java | 9 + 14 files changed, 405 insertions(+) create mode 100644 src/device/service/drivers/ietf_actn/IetfActnDriver.py create mode 100644 src/device/service/drivers/ietf_actn/Tools.py create mode 100644 src/device/service/drivers/ietf_actn/__init__.py diff --git a/proto/context.proto b/proto/context.proto index 7570a4596..d5022ac29 100644 --- a/proto/context.proto +++ b/proto/context.proto @@ -202,6 +202,7 @@ enum DeviceDriverEnum { DEVICEDRIVER_IETF_L2VPN = 7; DEVICEDRIVER_GNMI_OPENCONFIG = 8; DEVICEDRIVER_FLEXSCALE = 9; + DEVICEDRIVER_IETF_ACTN = 10; } enum DeviceOperationalStatusEnum { diff --git a/src/common/type_checkers/Assertions.py b/src/common/type_checkers/Assertions.py index 286ae179d..8b7d5ff03 100644 --- a/src/common/type_checkers/Assertions.py +++ b/src/common/type_checkers/Assertions.py @@ -35,6 +35,8 @@ def validate_device_driver_enum(message): 'DEVICEDRIVER_XR', 'DEVICEDRIVER_IETF_L2VPN', 'DEVICEDRIVER_GNMI_OPENCONFIG', + 'DEVICEDRIVER_FLEXSCALE', + 'DEVICEDRIVER_IETF_ACTN', ] def validate_device_operational_status_enum(message): diff --git a/src/context/service/database/models/enums/DeviceDriver.py b/src/context/service/database/models/enums/DeviceDriver.py index f84833601..8e15bf058 100644 --- a/src/context/service/database/models/enums/DeviceDriver.py +++ b/src/context/service/database/models/enums/DeviceDriver.py @@ -32,6 +32,7 @@ class ORM_DeviceDriverEnum(enum.Enum): IETF_L2VPN = DeviceDriverEnum.DEVICEDRIVER_IETF_L2VPN GNMI_OPENCONFIG = DeviceDriverEnum.DEVICEDRIVER_GNMI_OPENCONFIG FLEXSCALE = DeviceDriverEnum.DEVICEDRIVER_FLEXSCALE + IETF_ACTN = DeviceDriverEnum.DEVICEDRIVER_IETF_ACTN grpc_to_enum__device_driver = functools.partial( grpc_to_enum, DeviceDriverEnum, ORM_DeviceDriverEnum) diff --git a/src/device/service/drivers/ietf_actn/IetfActnDriver.py b/src/device/service/drivers/ietf_actn/IetfActnDriver.py new file mode 100644 index 000000000..34362c0b0 --- /dev/null +++ b/src/device/service/drivers/ietf_actn/IetfActnDriver.py @@ -0,0 +1,131 @@ +# 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 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 +from . import ALL_RESOURCE_KEYS +#from .Tools import create_connectivity_service, find_key, config_getter, delete_connectivity_service + +LOGGER = logging.getLogger(__name__) + +DRIVER_NAME = 'ietf_actn' +METRICS_POOL = MetricsPool('Device', 'Driver', labels={'driver': DRIVER_NAME}) + +class IetfActnDriver(_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)) + + def Connect(self) -> bool: + #url = self.__base_url + '/restconf/data/tapi-common:context' + #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.__base_url))) + # return False + # except Exception: # pylint: disable=broad-except + # LOGGER.exception('Exception connecting {:s}'.format(str(self.__base_url))) + # 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) + # chk_string(str_resource_name, resource_key, allow_empty=False) + # results.extend(config_getter( + # self.__base_url, resource_key, timeout=self.__timeout, auth=self.__auth)) + 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))) + # + # uuid = find_key(resource, 'uuid') + # input_sip = find_key(resource, 'input_sip_uuid') + # output_sip = find_key(resource, 'output_sip_uuid') + # capacity_value = find_key(resource, 'capacity_value') + # capacity_unit = find_key(resource, 'capacity_unit') + # layer_protocol_name = find_key(resource, 'layer_protocol_name') + # layer_protocol_qualifier = find_key(resource, 'layer_protocol_qualifier') + # direction = find_key(resource, 'direction') + # + # data = create_connectivity_service( + # self.__base_url, uuid, input_sip, output_sip, direction, capacity_value, capacity_unit, + # layer_protocol_name, layer_protocol_qualifier, timeout=self.__timeout, auth=self.__auth) + # results.extend(data) + 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))) + # uuid = find_key(resource, 'uuid') + # results.extend(delete_connectivity_service( + # self.__base_url, uuid, timeout=self.__timeout, auth=self.__auth)) + return results + + @metered_subclass_method(METRICS_POOL) + def SubscribeState(self, subscriptions : List[Tuple[str, float, float]]) -> List[Union[bool, Exception]]: + # TODO: IETF ACTN 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 ACTN 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 ACTN does not support monitoring by now + return [] diff --git a/src/device/service/drivers/ietf_actn/Tools.py b/src/device/service/drivers/ietf_actn/Tools.py new file mode 100644 index 000000000..bbd4247f0 --- /dev/null +++ b/src/device/service/drivers/ietf_actn/Tools.py @@ -0,0 +1,198 @@ +# 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 json, logging, operator, requests +from requests.auth import HTTPBasicAuth +from typing import Optional +from device.service.driver_api._Driver import RESOURCE_ENDPOINTS, RESOURCE_SERVICES + +LOGGER = logging.getLogger(__name__) + +HTTP_OK_CODES = { + 200, # OK + 201, # Created + 202, # Accepted + 204, # No Content +} + +def find_key(resource, key): + return json.loads(resource[1])[key] + + +def config_getter( + root_url : str, resource_key : str, auth : Optional[HTTPBasicAuth] = None, timeout : Optional[int] = None +): + url = '{:s}/restconf/data/tapi-common:context'.format(root_url) + result = [] + try: + response = requests.get(url, timeout=timeout, verify=False, auth=auth) + except requests.exceptions.Timeout: + LOGGER.exception('Timeout connecting {:s}'.format(url)) + return result + except Exception as e: # pylint: disable=broad-except + LOGGER.exception('Exception retrieving {:s}'.format(resource_key)) + result.append((resource_key, e)) + return result + + try: + context = json.loads(response.content) + except Exception as e: # pylint: disable=broad-except + LOGGER.warning('Unable to decode reply: {:s}'.format(str(response.content))) + 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 'tapi-common:context' in context: + context = context['tapi-common:context'] + elif 'context' in context: + context = context['context'] + + if 'tapi-connectivity:connectivity-context' in context: + context = context['tapi-connectivity:connectivity-context'] + elif 'connectivity-context' in context: + context = context['connectivity-context'] + + for conn_svc in context['connectivity-service']: + service_uuid = conn_svc['uuid'] + constraints = conn_svc.get('connectivity-constraint', {}) + total_req_cap = constraints.get('requested-capacity', {}).get('total-size', {}) + + service_url = '/services/service[{:s}]'.format(service_uuid) + service_data = { + 'uuid': service_uuid, + 'direction': constraints.get('connectivity-direction', 'UNIDIRECTIONAL'), + 'capacity_unit': total_req_cap.get('unit', ''), + 'capacity_value': total_req_cap.get('value', ''), + } + + for i,endpoint in enumerate(conn_svc.get('end-point', [])): + layer_protocol_name = endpoint.get('layer-protocol-name') + if layer_protocol_name is not None: + service_data['layer_protocol_name'] = layer_protocol_name + + layer_protocol_qualifier = endpoint.get('layer-protocol-qualifier') + if layer_protocol_qualifier is not None: + service_data['layer_protocol_qualifier'] = layer_protocol_qualifier + + sip = endpoint['service-interface-point']['service-interface-point-uuid'] + service_data['input_sip' if i == 0 else 'output_sip'] = sip + + result.append((service_url, service_data)) + + return result + +def create_connectivity_service( + root_url, uuid, input_sip, output_sip, direction, capacity_value, capacity_unit, layer_protocol_name, + layer_protocol_qualifier, + auth : Optional[HTTPBasicAuth] = None, timeout : Optional[int] = None +): + + url = '{:s}/restconf/data/tapi-common:context/tapi-connectivity:connectivity-context'.format(root_url) + headers = {'content-type': 'application/json'} + data = { + 'tapi-connectivity:connectivity-service': [ + { + 'uuid': uuid, + 'connectivity-constraint': { + 'requested-capacity': { + 'total-size': { + 'value': capacity_value, + 'unit': capacity_unit + } + }, + 'connectivity-direction': direction + }, + 'end-point': [ + { + 'service-interface-point': { + 'service-interface-point-uuid': input_sip + }, + 'layer-protocol-name': layer_protocol_name, + 'layer-protocol-qualifier': layer_protocol_qualifier, + 'local-id': input_sip + }, + { + 'service-interface-point': { + 'service-interface-point-uuid': output_sip + }, + 'layer-protocol-name': layer_protocol_name, + 'layer-protocol-qualifier': layer_protocol_qualifier, + 'local-id': output_sip + } + ] + } + ] + } + results = [] + try: + LOGGER.info('Connectivity service {:s}: {:s}'.format(str(uuid), str(data))) + response = requests.post( + url=url, data=json.dumps(data), timeout=timeout, headers=headers, verify=False, auth=auth) + LOGGER.info('TAPI response: {:s}'.format(str(response))) + except Exception as e: # pylint: disable=broad-except + LOGGER.exception('Exception creating ConnectivityService(uuid={:s}, data={:s})'.format(str(uuid), str(data))) + results.append(e) + else: + if response.status_code not in HTTP_OK_CODES: + msg = 'Could not create ConnectivityService(uuid={:s}, data={:s}). status_code={:s} reply={:s}' + LOGGER.error(msg.format(str(uuid), str(data), str(response.status_code), str(response))) + results.append(response.status_code in HTTP_OK_CODES) + return results + +def delete_connectivity_service(root_url, uuid, auth : Optional[HTTPBasicAuth] = None, timeout : Optional[int] = None): + url = '{:s}/restconf/data/tapi-common:context/tapi-connectivity:connectivity-context/connectivity-service={:s}' + url = url.format(root_url, uuid) + results = [] + try: + response = requests.delete(url=url, timeout=timeout, verify=False, auth=auth) + except Exception as e: # pylint: disable=broad-except + LOGGER.exception('Exception deleting ConnectivityService(uuid={:s})'.format(str(uuid))) + results.append(e) + else: + if response.status_code not in HTTP_OK_CODES: + msg = 'Could not delete ConnectivityService(uuid={:s}). status_code={:s} reply={:s}' + LOGGER.error(msg.format(str(uuid), 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/__init__.py b/src/device/service/drivers/ietf_actn/__init__.py new file mode 100644 index 000000000..d5073c330 --- /dev/null +++ b/src/device/service/drivers/ietf_actn/__init__.py @@ -0,0 +1,20 @@ +# 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. + +from device.service.driver_api._Driver import RESOURCE_ENDPOINTS, RESOURCE_SERVICES + +ALL_RESOURCE_KEYS = [ + RESOURCE_ENDPOINTS, + RESOURCE_SERVICES, +] diff --git a/src/policy/src/main/java/org/etsi/tfs/policy/Serializer.java b/src/policy/src/main/java/org/etsi/tfs/policy/Serializer.java index b7de8ff99..f777c7720 100644 --- a/src/policy/src/main/java/org/etsi/tfs/policy/Serializer.java +++ b/src/policy/src/main/java/org/etsi/tfs/policy/Serializer.java @@ -2284,6 +2284,12 @@ public class Serializer { return ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_XR; case IETF_L2VPN: return ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_IETF_L2VPN; + case GNMI_OPENCONFIG: + return ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_GNMI_OPENCONFIG; + case FLEXSCALE: + return ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_FLEXSCALE; + case IETF_ACTN: + return ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_IETF_ACTN; case UNDEFINED: default: return ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_UNDEFINED; @@ -2307,6 +2313,12 @@ public class Serializer { return DeviceDriverEnum.XR; case DEVICEDRIVER_IETF_L2VPN: return DeviceDriverEnum.IETF_L2VPN; + case DEVICEDRIVER_GNMI_OPENCONFIG: + return DeviceDriverEnum.GNMI_OPENCONFIG; + case DEVICEDRIVER_FLEXSCALE: + return DeviceDriverEnum.FLEXSCALE; + case DEVICEDRIVER_IETF_ACTN: + return DeviceDriverEnum.IETF_ACTN; case DEVICEDRIVER_UNDEFINED: case UNRECOGNIZED: default: diff --git a/src/policy/src/test/java/org/etsi/tfs/policy/SerializerTest.java b/src/policy/src/test/java/org/etsi/tfs/policy/SerializerTest.java index f29ae3697..df2ae46cf 100644 --- a/src/policy/src/test/java/org/etsi/tfs/policy/SerializerTest.java +++ b/src/policy/src/test/java/org/etsi/tfs/policy/SerializerTest.java @@ -3614,6 +3614,15 @@ class SerializerTest { Arguments.of( DeviceDriverEnum.IETF_L2VPN, ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_IETF_L2VPN), + Arguments.of( + DeviceDriverEnum.GNMI_OPENCONFIG, + ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_GNMI_OPENCONFIG), + Arguments.of( + DeviceDriverEnum.FLEXSCALE, + ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_FLEXSCALE), + Arguments.of( + DeviceDriverEnum.IETF_ACTN, + ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_IETF_ACTN), Arguments.of( DeviceDriverEnum.UNDEFINED, ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_UNDEFINED)); } diff --git a/src/service/service/service_handler_api/FilterFields.py b/src/service/service/service_handler_api/FilterFields.py index 35c45c996..e771e24f1 100644 --- a/src/service/service/service_handler_api/FilterFields.py +++ b/src/service/service/service_handler_api/FilterFields.py @@ -39,6 +39,7 @@ DEVICE_DRIVER_VALUES = { DeviceDriverEnum.DEVICEDRIVER_IETF_L2VPN, DeviceDriverEnum.DEVICEDRIVER_GNMI_OPENCONFIG, DeviceDriverEnum.DEVICEDRIVER_FLEXSCALE, + DeviceDriverEnum.DEVICEDRIVER_IETF_ACTN, } # Map allowed filter fields to allowed values per Filter field. If no restriction (free text) None is specified diff --git a/src/webui/service/device/forms.py b/src/webui/service/device/forms.py index bcd5804d3..4c04bbfe1 100644 --- a/src/webui/service/device/forms.py +++ b/src/webui/service/device/forms.py @@ -31,6 +31,8 @@ class AddDeviceForm(FlaskForm): device_drivers_xr = BooleanField('XR') device_drivers_ietf_l2vpn = BooleanField('IETF L2VPN') device_drivers_gnmi_openconfig = BooleanField('GNMI OPENCONFIG') + device_drivers_flexscale = BooleanField('FLEXSCALE') + device_drivers_ietf_actn = BooleanField('IETF ACTN') device_config_address = StringField('connect/address',default='127.0.0.1',validators=[DataRequired(), Length(min=5)]) device_config_port = StringField('connect/port',default='0',validators=[DataRequired(), Length(min=1)]) diff --git a/src/webui/service/device/routes.py b/src/webui/service/device/routes.py index 4459deeeb..b75e9fb9f 100644 --- a/src/webui/service/device/routes.py +++ b/src/webui/service/device/routes.py @@ -125,6 +125,10 @@ def add(): device_drivers.append(DeviceDriverEnum.DEVICEDRIVER_IETF_L2VPN) if form.device_drivers_gnmi_openconfig.data: device_drivers.append(DeviceDriverEnum.DEVICEDRIVER_GNMI_OPENCONFIG) + if form.device_drivers_flexscale.data: + device_drivers.append(DeviceDriverEnum.DEVICEDRIVER_FLEXSCALE) + if form.device_drivers_ietf_actn.data: + device_drivers.append(DeviceDriverEnum.DEVICEDRIVER_IETF_ACTN) device_obj.device_drivers.extend(device_drivers) # pylint: disable=no-member try: diff --git a/src/webui/service/templates/device/add.html b/src/webui/service/templates/device/add.html index c115657aa..c4d7f1685 100644 --- a/src/webui/service/templates/device/add.html +++ b/src/webui/service/templates/device/add.html @@ -92,6 +92,9 @@ {{ form.device_drivers_xr }} {{ form.device_drivers_xr.label(class="col-sm-3 col-form-label") }} {{ form.device_drivers_ietf_l2vpn }} {{ form.device_drivers_ietf_l2vpn.label(class="col-sm-3 col-form-label") }} {{ form.device_drivers_gnmi_openconfig }} {{ form.device_drivers_gnmi_openconfig.label(class="col-sm-3 col-form-label") }} +
+ {{ form.device_drivers_flexscale }} {{ form.device_drivers_flexscale.label(class="col-sm-3 col-form-label") }} + {{ form.device_drivers_ietf_actn }} {{ form.device_drivers_ietf_actn.label(class="col-sm-3 col-form-label") }} {% endif %} diff --git a/src/ztp/src/main/java/org/etsi/tfs/ztp/Serializer.java b/src/ztp/src/main/java/org/etsi/tfs/ztp/Serializer.java index cf49280a8..feb65b77c 100644 --- a/src/ztp/src/main/java/org/etsi/tfs/ztp/Serializer.java +++ b/src/ztp/src/main/java/org/etsi/tfs/ztp/Serializer.java @@ -863,6 +863,12 @@ public class Serializer { return ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_XR; case IETF_L2VPN: return ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_IETF_L2VPN; + case GNMI_OPENCONFIG: + return ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_GNMI_OPENCONFIG; + case FLEXSCALE: + return ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_FLEXSCALE; + case IETF_ACTN: + return ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_IETF_ACTN; case UNDEFINED: default: return ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_UNDEFINED; @@ -886,6 +892,12 @@ public class Serializer { return DeviceDriverEnum.XR; case DEVICEDRIVER_IETF_L2VPN: return DeviceDriverEnum.IETF_L2VPN; + case DEVICEDRIVER_GNMI_OPENCONFIG: + return DeviceDriverEnum.GNMI_OPENCONFIG; + case DEVICEDRIVER_FLEXSCALE: + return DeviceDriverEnum.FLEXSCALE; + case DEVICEDRIVER_IETF_ACTN: + return DeviceDriverEnum.IETF_ACTN; case DEVICEDRIVER_UNDEFINED: case UNRECOGNIZED: default: diff --git a/src/ztp/src/test/java/org/etsi/tfs/ztp/SerializerTest.java b/src/ztp/src/test/java/org/etsi/tfs/ztp/SerializerTest.java index 5a7887a04..37c6df36d 100644 --- a/src/ztp/src/test/java/org/etsi/tfs/ztp/SerializerTest.java +++ b/src/ztp/src/test/java/org/etsi/tfs/ztp/SerializerTest.java @@ -1223,6 +1223,15 @@ class SerializerTest { Arguments.of( DeviceDriverEnum.IETF_L2VPN, ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_IETF_L2VPN), + Arguments.of( + DeviceDriverEnum.GNMI_OPENCONFIG, + ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_GNMI_OPENCONFIG), + Arguments.of( + DeviceDriverEnum.FLEXSCALE, + ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_FLEXSCALE), + Arguments.of( + DeviceDriverEnum.IETF_ACTN, + ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_IETF_ACTN), Arguments.of( DeviceDriverEnum.UNDEFINED, ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_UNDEFINED)); } -- GitLab From efabee0e5b328bcccbc806cfed5f4ba5196bbdec Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Wed, 3 Jan 2024 14:35:14 +0000 Subject: [PATCH 02/18] Device - IETF ACTN Driver: - Intermediate backup --- .../drivers/ietf_actn/ComposerEthService.py | 14 +++ .../drivers/ietf_actn/ComposerOsuTunnel.py | 80 ++++++++++++++++ .../drivers/ietf_actn/IetfActnDriver.py | 94 ++++++++++--------- src/device/service/drivers/ietf_actn/Tools.py | 36 ++++--- 4 files changed, 167 insertions(+), 57 deletions(-) create mode 100644 src/device/service/drivers/ietf_actn/ComposerEthService.py create mode 100644 src/device/service/drivers/ietf_actn/ComposerOsuTunnel.py diff --git a/src/device/service/drivers/ietf_actn/ComposerEthService.py b/src/device/service/drivers/ietf_actn/ComposerEthService.py new file mode 100644 index 000000000..1549d9811 --- /dev/null +++ b/src/device/service/drivers/ietf_actn/ComposerEthService.py @@ -0,0 +1,14 @@ +# 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. + diff --git a/src/device/service/drivers/ietf_actn/ComposerOsuTunnel.py b/src/device/service/drivers/ietf_actn/ComposerOsuTunnel.py new file mode 100644 index 000000000..ad369482e --- /dev/null +++ b/src/device/service/drivers/ietf_actn/ComposerOsuTunnel.py @@ -0,0 +1,80 @@ +# 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 enum +from typing import Dict + +OSU_TUNNEL_URL = '/restconf/data/ietf-te:tunnel' + +class EndpointProtectionRoleEnum(enum.Enum): + WORK = 'work' + +class LspProtectionTypeEnum(enum.Enum): + UNPROTECTED = 'ietf-te-types:lsp-protection-unprotected' + +class LspRestorationTypeEnum(enum.Enum): + NOT_APPLICABLE = 'ietf-te-types:lsp-restoration-not-applicable' + +class TunnelAdminStateEnum(enum.Enum): + UP = 'ietf-te-types:tunnel-admin-state-up' + +class OduTypeEnum(enum.Enum): + OSUFLEX = 'osuflex' + +def compose_osu_tunnel_endpoint( + node_id : str, tp_id : str, ttp_channel_name : str, + protection_role : EndpointProtectionRoleEnum = EndpointProtectionRoleEnum.WORK +) -> Dict: + return { + 'node-id': node_id, 'tp-id': tp_id, 'ttp-channel-name': ttp_channel_name, + 'protection-role': protection_role.value + } + +def compose_osu_tunnel_te_bandwidth_odu(odu_type : OduTypeEnum, number : int) -> Dict: + return {'layer': 'odu', 'odu-type': odu_type.value, 'number': number} + +def compose_osu_tunnel_protection( + type_ : LspProtectionTypeEnum = LspProtectionTypeEnum.UNPROTECTED, reversion_disable : bool = True +) -> Dict: + return {'protection-type': type_.value, 'protection-reversion-disable': reversion_disable} + +def compose_osu_tunnel_restoration( + type_ : LspRestorationTypeEnum = LspRestorationTypeEnum.NOT_APPLICABLE, restoration_lock : bool = False +) -> Dict: + return {'restoration-type': type_.value, 'restoration-lock': restoration_lock} + +def compose_osu_tunnel( + name : str, + src_node_id : str, src_tp_id : str, src_ttp_channel_name : str, + dst_node_id : str, dst_tp_id : str, dst_ttp_channel_name : str, + odu_type : OduTypeEnum, osuflex_number : int, + delay : int, bidirectional : bool = True, + admin_state : TunnelAdminStateEnum = TunnelAdminStateEnum.UP +) -> Dict: + return {'ietf-te:tunnel': [{ + 'name': name.lower(), + 'title': name.upper(), + 'admin-state': admin_state.value, + 'delay': delay, + 'te-bandwidth': compose_osu_tunnel_te_bandwidth_odu(odu_type, osuflex_number), + 'bidirectional': bidirectional, + 'source-endpoints': {'source-endpoint': [ + compose_osu_tunnel_endpoint(src_node_id, src_tp_id, src_ttp_channel_name), + ]}, + 'destination-endpoints': {'destination-endpoint': [ + compose_osu_tunnel_endpoint(dst_node_id, dst_tp_id, dst_ttp_channel_name), + ]}, + 'restoration': compose_osu_tunnel_restoration(), + 'protection': compose_osu_tunnel_protection(), + }]} diff --git a/src/device/service/drivers/ietf_actn/IetfActnDriver.py b/src/device/service/drivers/ietf_actn/IetfActnDriver.py index 34362c0b0..c31bd85b9 100644 --- a/src/device/service/drivers/ietf_actn/IetfActnDriver.py +++ b/src/device/service/drivers/ietf_actn/IetfActnDriver.py @@ -12,12 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -import logging, requests, threading +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 from device.service.driver_api._Driver import _Driver +from device.service.drivers.ietf_actn.Tools import create_resource, delete_resource, get_resource from . import ALL_RESOURCE_KEYS #from .Tools import create_connectivity_service, find_key, config_getter, delete_connectivity_service @@ -26,33 +27,40 @@ 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: 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)) + base_url = self.settings.get('base_url', DEFAULT_BASE_URL) + self.__base_url = '{:s}://{:s}:{:d}{:s}'.format(scheme, address, int(port), base_url) + + self.__timeout = int(self.settings.get('timeout', DEFAULT_TIMEOUT)) def Connect(self) -> bool: - #url = self.__base_url + '/restconf/data/tapi-common:context' - #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.__base_url))) - # return False - # except Exception: # pylint: disable=broad-except - # LOGGER.exception('Exception connecting {:s}'.format(str(self.__base_url))) - # return False - # else: - # self.__started.set() + url = self.__base_url + '/tapi-common:context' + 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.__base_url))) + return False + except Exception: # pylint: disable=broad-except + LOGGER.exception('Exception connecting {:s}'.format(str(self.__base_url))) + return False + else: + self.__started.set() return True def Disconnect(self) -> bool: @@ -69,13 +77,14 @@ class IetfActnDriver(_Driver): 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) - # chk_string(str_resource_name, resource_key, allow_empty=False) - # results.extend(config_getter( - # self.__base_url, resource_key, timeout=self.__timeout, auth=self.__auth)) + with self.__lock: + if len(resource_keys) == 0: resource_keys = ALL_RESOURCE_KEYS + for i, resource_key in enumerate(resource_keys): + chk_string('resource_key[#{:d}]'.format(i), resource_key, allow_empty=False) + results.extend(get_resource( + self.__base_url, resource_key, + timeout=self.__timeout, auth=self.__auth + )) return results @metered_subclass_method(METRICS_POOL) @@ -83,35 +92,28 @@ class IetfActnDriver(_Driver): results = [] if len(resources) == 0: return results - #with self.__lock: - # for resource in resources: - # LOGGER.info('resource = {:s}'.format(str(resource))) - # - # uuid = find_key(resource, 'uuid') - # input_sip = find_key(resource, 'input_sip_uuid') - # output_sip = find_key(resource, 'output_sip_uuid') - # capacity_value = find_key(resource, 'capacity_value') - # capacity_unit = find_key(resource, 'capacity_unit') - # layer_protocol_name = find_key(resource, 'layer_protocol_name') - # layer_protocol_qualifier = find_key(resource, 'layer_protocol_qualifier') - # direction = find_key(resource, 'direction') - # - # data = create_connectivity_service( - # self.__base_url, uuid, input_sip, output_sip, direction, capacity_value, capacity_unit, - # layer_protocol_name, layer_protocol_qualifier, timeout=self.__timeout, auth=self.__auth) - # results.extend(data) + with self.__lock: + for resource_key, resource_value in resources: + LOGGER.info('resource: key({:s}) => value({:s})'.format(str(resource_key), str(resource_value))) + if isinstance(value, str): value = json.loads(value) + results.extend(create_resource( + self.__base_url, resource_key, resource_value, + timeout=self.__timeout, auth=self.__auth + )) 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))) - # uuid = find_key(resource, 'uuid') - # results.extend(delete_connectivity_service( - # self.__base_url, uuid, timeout=self.__timeout, auth=self.__auth)) + with self.__lock: + for resource_key, resource_value in resources: + LOGGER.info('resource: key({:s}) => value({:s})'.format(str(resource_key), str(resource_value))) + if isinstance(value, str): value = json.loads(value) + results.extend(delete_resource( + self.__base_url, resource_key, resource_value, + timeout=self.__timeout, auth=self.__auth + )) return results @metered_subclass_method(METRICS_POOL) diff --git a/src/device/service/drivers/ietf_actn/Tools.py b/src/device/service/drivers/ietf_actn/Tools.py index bbd4247f0..1b89315b1 100644 --- a/src/device/service/drivers/ietf_actn/Tools.py +++ b/src/device/service/drivers/ietf_actn/Tools.py @@ -14,7 +14,7 @@ import json, logging, operator, requests from requests.auth import HTTPBasicAuth -from typing import Optional +from typing import Dict, Optional from device.service.driver_api._Driver import RESOURCE_ENDPOINTS, RESOURCE_SERVICES LOGGER = logging.getLogger(__name__) @@ -29,11 +29,11 @@ HTTP_OK_CODES = { def find_key(resource, key): return json.loads(resource[1])[key] - -def config_getter( - root_url : str, resource_key : str, auth : Optional[HTTPBasicAuth] = None, timeout : Optional[int] = None +def get_resource( + base_url : str, resource_key : str, + auth : Optional[HTTPBasicAuth] = None, timeout : Optional[int] = None ): - url = '{:s}/restconf/data/tapi-common:context'.format(root_url) + url = '{:s}/restconf/data/tapi-common:context'.format(base_url) result = [] try: response = requests.get(url, timeout=timeout, verify=False, auth=auth) @@ -123,13 +123,22 @@ def config_getter( return result -def create_connectivity_service( - root_url, uuid, input_sip, output_sip, direction, capacity_value, capacity_unit, layer_protocol_name, - layer_protocol_qualifier, +def create_resource( + base_url : str, resource_key : str, resource_value : Dict, auth : Optional[HTTPBasicAuth] = None, timeout : Optional[int] = None ): - url = '{:s}/restconf/data/tapi-common:context/tapi-connectivity:connectivity-context'.format(root_url) + uuid = find_key(resource, 'uuid') + input_sip = find_key(resource, 'input_sip_uuid') + output_sip = find_key(resource, 'output_sip_uuid') + capacity_value = find_key(resource, 'capacity_value') + capacity_unit = find_key(resource, 'capacity_unit') + layer_protocol_name = find_key(resource, 'layer_protocol_name') + layer_protocol_qualifier = find_key(resource, 'layer_protocol_qualifier') + direction = find_key(resource, 'direction') + + + url = '{:s}/restconf/data/tapi-common:context/tapi-connectivity:connectivity-context'.format(base_url) headers = {'content-type': 'application/json'} data = { 'tapi-connectivity:connectivity-service': [ @@ -181,9 +190,14 @@ def create_connectivity_service( results.append(response.status_code in HTTP_OK_CODES) return results -def delete_connectivity_service(root_url, uuid, auth : Optional[HTTPBasicAuth] = None, timeout : Optional[int] = None): +def delete_resource( + base_url : str, resource_key : str, resource_value : Dict, + auth : Optional[HTTPBasicAuth] = None, timeout : Optional[int] = None +): + uuid = find_key(resource, 'uuid') + url = '{:s}/restconf/data/tapi-common:context/tapi-connectivity:connectivity-context/connectivity-service={:s}' - url = url.format(root_url, uuid) + url = url.format(base_url, uuid) results = [] try: response = requests.delete(url=url, timeout=timeout, verify=False, auth=auth) -- GitLab From de330c26c61cee3cad8ce82199dab198ef4ffdfe Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Wed, 3 Jan 2024 15:18:23 +0000 Subject: [PATCH 03/18] Tests - Mock IETF ACTN SDN Ctrl: - Added skeleton for a test IETF SCTN SDN controller --- .../MockIetfActnSdnCtrl.py | 150 ++++++++++++++++++ .../tools/mock_ietf_actn_sdn_ctrl/README.md | 53 +++++++ .../tools/mock_ietf_actn_sdn_ctrl/run.sh | 16 ++ .../scenario/ietf_actn_deploy.sh | 36 +++++ .../scenario/network_descriptors.json | 117 ++++++++++++++ .../scenario/service_descriptor.json | 25 +++ .../ssl_not_working/Dockerfile | 35 ++++ .../ssl_not_working/build.sh | 5 + .../ssl_not_working/deploy.sh | 4 + .../ssl_not_working/mock-mw-sdn-ctrl.yaml | 46 ++++++ .../ssl_not_working/requirements.in | 21 +++ .../mock_ietf_actn_sdn_ctrl/test_ietf_actn.py | 84 ++++++++++ 12 files changed, 592 insertions(+) create mode 100644 src/tests/tools/mock_ietf_actn_sdn_ctrl/MockIetfActnSdnCtrl.py create mode 100644 src/tests/tools/mock_ietf_actn_sdn_ctrl/README.md create mode 100755 src/tests/tools/mock_ietf_actn_sdn_ctrl/run.sh create mode 100755 src/tests/tools/mock_ietf_actn_sdn_ctrl/scenario/ietf_actn_deploy.sh create mode 100644 src/tests/tools/mock_ietf_actn_sdn_ctrl/scenario/network_descriptors.json create mode 100644 src/tests/tools/mock_ietf_actn_sdn_ctrl/scenario/service_descriptor.json create mode 100644 src/tests/tools/mock_ietf_actn_sdn_ctrl/ssl_not_working/Dockerfile create mode 100755 src/tests/tools/mock_ietf_actn_sdn_ctrl/ssl_not_working/build.sh create mode 100755 src/tests/tools/mock_ietf_actn_sdn_ctrl/ssl_not_working/deploy.sh create mode 100644 src/tests/tools/mock_ietf_actn_sdn_ctrl/ssl_not_working/mock-mw-sdn-ctrl.yaml create mode 100644 src/tests/tools/mock_ietf_actn_sdn_ctrl/ssl_not_working/requirements.in create mode 100644 src/tests/tools/mock_ietf_actn_sdn_ctrl/test_ietf_actn.py diff --git a/src/tests/tools/mock_ietf_actn_sdn_ctrl/MockIetfActnSdnCtrl.py b/src/tests/tools/mock_ietf_actn_sdn_ctrl/MockIetfActnSdnCtrl.py new file mode 100644 index 000000000..c20dde1b9 --- /dev/null +++ b/src/tests/tools/mock_ietf_actn_sdn_ctrl/MockIetfActnSdnCtrl.py @@ -0,0 +1,150 @@ +# 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. + +# Mock MicroWave SDN controller +# ----------------------------- +# REST server implementing minimal support for: +# - IETF YANG data model for Network Topology +# Ref: https://www.rfc-editor.org/rfc/rfc8345.html +# - IETF YANG data model for Transport Network Client Signals +# Ref: https://www.ietf.org/archive/id/draft-ietf-ccamp-client-signal-yang-07.html + + +# Ref: https://blog.miguelgrinberg.com/post/running-your-flask-application-over-https +# Ref: https://blog.miguelgrinberg.com/post/designing-a-restful-api-using-flask-restful + +import functools, logging, sys, time +from flask import Flask, abort, jsonify, make_response, request +from flask_restful import Api, Resource + +BIND_ADDRESS = '0.0.0.0' +BIND_PORT = 8443 +BASE_URL = '/nmswebs/restconf/data' +STR_ENDPOINT = 'https://{:s}:{:s}{:s}'.format(str(BIND_ADDRESS), str(BIND_PORT), str(BASE_URL)) +LOG_LEVEL = logging.DEBUG + +NETWORK_NODES = [ + {'node-id': '192.168.27.139', 'ietf-network-topology:termination-point': [ + {'tp-id': '1', 'ietf-te-topology:te': {'name': 'ethernet'}}, + {'tp-id': '2', 'ietf-te-topology:te': {'name': 'ethernet'}}, + {'tp-id': '3', 'ietf-te-topology:te': {'name': 'ethernet'}}, + {'tp-id': '4', 'ietf-te-topology:te': {'name': 'ethernet'}}, + {'tp-id': '5', 'ietf-te-topology:te': {'name': 'ethernet'}}, + {'tp-id': '6', 'ietf-te-topology:te': {'name': 'ethernet'}}, + {'tp-id': '10', 'ietf-te-topology:te': {'name': 'antena' }}, + ]}, + {'node-id': '192.168.27.140', 'ietf-network-topology:termination-point': [ + {'tp-id': '1', 'ietf-te-topology:te': {'name': 'ethernet'}}, + {'tp-id': '2', 'ietf-te-topology:te': {'name': 'ethernet'}}, + {'tp-id': '3', 'ietf-te-topology:te': {'name': 'ethernet'}}, + {'tp-id': '4', 'ietf-te-topology:te': {'name': 'ethernet'}}, + {'tp-id': '5', 'ietf-te-topology:te': {'name': 'ethernet'}}, + {'tp-id': '6', 'ietf-te-topology:te': {'name': 'ethernet'}}, + {'tp-id': '10', 'ietf-te-topology:te': {'name': 'antena' }}, + ]}, + {'node-id': '192.168.27.141', 'ietf-network-topology:termination-point': [ + {'tp-id': '1', 'ietf-te-topology:te': {'name': 'ethernet'}}, + {'tp-id': '2', 'ietf-te-topology:te': {'name': 'ethernet'}}, + {'tp-id': '3', 'ietf-te-topology:te': {'name': 'ethernet'}}, + {'tp-id': '4', 'ietf-te-topology:te': {'name': 'ethernet'}}, + {'tp-id': '5', 'ietf-te-topology:te': {'name': 'ethernet'}}, + {'tp-id': '6', 'ietf-te-topology:te': {'name': 'ethernet'}}, + {'tp-id': '10', 'ietf-te-topology:te': {'name': 'antena' }}, + ]}, + {'node-id': '192.168.27.142', 'ietf-network-topology:termination-point': [ + {'tp-id': '1', 'ietf-te-topology:te': {'name': 'ethernet'}}, + {'tp-id': '2', 'ietf-te-topology:te': {'name': 'ethernet'}}, + {'tp-id': '3', 'ietf-te-topology:te': {'name': 'ethernet'}}, + {'tp-id': '4', 'ietf-te-topology:te': {'name': 'ethernet'}}, + {'tp-id': '5', 'ietf-te-topology:te': {'name': 'ethernet'}}, + {'tp-id': '6', 'ietf-te-topology:te': {'name': 'ethernet'}}, + {'tp-id': '10', 'ietf-te-topology:te': {'name': 'antena' }}, + ]} +] +NETWORK_LINKS = [ + { 'link-id' : '192.168.27.139:10--192.168.27.140:10', + 'source' : {'source-node': '192.168.27.139', 'source-tp': '10'}, + 'destination': {'dest-node' : '192.168.27.140', 'dest-tp' : '10'}, + }, + { 'link-id' : '192.168.27.141:10--192.168.27.142:10', + 'source' : {'source-node': '192.168.27.141', 'source-tp': '10'}, + 'destination': {'dest-node' : '192.168.27.142', 'dest-tp' : '10'}, + } +] +NETWORK_SERVICES = {} + + +logging.basicConfig(level=LOG_LEVEL, format="[%(asctime)s] %(levelname)s:%(name)s:%(message)s") +LOGGER = logging.getLogger(__name__) + +logging.getLogger('werkzeug').setLevel(logging.WARNING) + +def log_request(logger : logging.Logger, response): + timestamp = time.strftime('[%Y-%b-%d %H:%M]') + logger.info('%s %s %s %s %s', timestamp, request.remote_addr, request.method, request.full_path, response.status) + return response + +class Health(Resource): + def get(self): + return make_response(jsonify({}), 200) + +class Network(Resource): + def get(self, network_uuid : str): + if network_uuid != 'SIAE-ETH-TOPOLOGY': abort(400) + network = {'node': NETWORK_NODES, 'ietf-network-topology:link': NETWORK_LINKS} + return make_response(jsonify({'ietf-network:network': network}), 200) + +class Services(Resource): + def get(self): + services = [service for service in NETWORK_SERVICES.values()] + return make_response(jsonify({'ietf-eth-tran-service:etht-svc': {'etht-svc-instances': services}}), 200) + + def post(self): + json_request = request.get_json() + if not json_request: abort(400) + if not isinstance(json_request, dict): abort(400) + if 'etht-svc-instances' not in json_request: abort(400) + json_services = json_request['etht-svc-instances'] + if not isinstance(json_services, list): abort(400) + if len(json_services) != 1: abort(400) + svc_data = json_services[0] + etht_svc_name = svc_data['etht-svc-name'] + NETWORK_SERVICES[etht_svc_name] = svc_data + return make_response(jsonify({}), 201) + +class DelServices(Resource): + def delete(self, service_uuid : str): + NETWORK_SERVICES.pop(service_uuid, None) + return make_response(jsonify({}), 204) + +def main(): + LOGGER.info('Starting...') + + app = Flask(__name__) + app.after_request(functools.partial(log_request, LOGGER)) + + api = Api(app, prefix=BASE_URL) + api.add_resource(Health, '/ietf-network:networks') + api.add_resource(Network, '/ietf-network:networks/network=') + api.add_resource(Services, '/ietf-eth-tran-service:etht-svc') + api.add_resource(DelServices, '/ietf-eth-tran-service:etht-svc/etht-svc-instances=') + + LOGGER.info('Listening on {:s}...'.format(str(STR_ENDPOINT))) + app.run(debug=True, host=BIND_ADDRESS, port=BIND_PORT, ssl_context='adhoc') + + LOGGER.info('Bye') + return 0 + +if __name__ == '__main__': + sys.exit(main()) diff --git a/src/tests/tools/mock_ietf_actn_sdn_ctrl/README.md b/src/tests/tools/mock_ietf_actn_sdn_ctrl/README.md new file mode 100644 index 000000000..a12ae907e --- /dev/null +++ b/src/tests/tools/mock_ietf_actn_sdn_ctrl/README.md @@ -0,0 +1,53 @@ +# Mock IETF ACTN SDN Controller + +This REST server implements very basic support for the following YANG data models: +- IETF YANG Data Model for Transport Network Client Signals (draft-ietf-ccamp-client-signal-yang-10) + - Ref: https://datatracker.ietf.org/doc/draft-ietf-ccamp-client-signal-yang/ +- IETF YANG Data Model for Traffic Engineering Tunnels, Label Switched Paths and Interfaces (draft-ietf-teas-yang-te-34) + - Ref: https://datatracker.ietf.org/doc/draft-ietf-teas-yang-te/ + +The aim of this server is to enable testing the IetfActnDeviceDriver and the IetfActnServiceHandler. +Follow the steps below to perform the test: + +## 1. Deploy TeraFlowSDN controller and the scenario +Deploy the test scenario "ietf_actn_deploy.sh": +```bash +source src/tests/tools/mock_ietf_actn_sdn_ctrl/scenario/ietf_actn_deploy.sh +./deploy/all.sh +``` + +## 2. Install requirements and run the Mock IETF ACTN SDN controller +__NOTE__: if you run the Mock IETF ACTN SDN controller from the PyEnv used for developping on the TeraFlowSDN framework, +all the requirements are already in place. Install them only if you execute it in a separate/standalone environment. + +Install the required dependencies as follows: +```bash +pip install Flask==2.1.3 Flask-RESTful==0.3.9 +``` + +Run the Mock IETF ACTN SDN Controller as follows: +```bash +python src/tests/tools/mock_ietf_actn_sdn_ctrl/MockIetfActnSdnCtrl.py +``` + +## 3. Deploy the test descriptors +Edit the descriptors to meet your environment specifications. +Edit "network_descriptors.json" and change IP address and port of the IETF ACTN SDN controller of the "ACTN" device. +- Set value of config rule "_connect/address" to the address of the host where the Mock IETF ACTN SDN controller is + running (default="192.168.1.1"). +- Set value of config rule "_connect/port" to the port where your Mock IETF ACTN SDN controller is listening on + (default="8443"). + +Upload the "network_descriptors.json" through the TeraFlowSDN WebUI. +- If not already selected, select Context(admin)/Topology(admin). +- Check that a network topology with 4 routers + 1 IETF ACTN radio system are loaded. They should form 2 rings. + +Upload the "service_descriptor.json" through the TeraFlowSDN WebUI. +- Check that 2 services have been created. +- The "actn-svc" should have a connection and be supported by a sub-service. +- The sub-service should also have a connection. +- The R1, R3, and MW devices should have configuration rules established. + +# 4. Delete the IETF ACTN service +Find the "mw-svc" on the WebUI, navigate to its details, and delete the service pressing the "Delete Service" button. +The service, sub-service, and device configuration rules should be removed. diff --git a/src/tests/tools/mock_ietf_actn_sdn_ctrl/run.sh b/src/tests/tools/mock_ietf_actn_sdn_ctrl/run.sh new file mode 100755 index 000000000..2697e538e --- /dev/null +++ b/src/tests/tools/mock_ietf_actn_sdn_ctrl/run.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# 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. + +python MockIetfActnSdnCtrl.py diff --git a/src/tests/tools/mock_ietf_actn_sdn_ctrl/scenario/ietf_actn_deploy.sh b/src/tests/tools/mock_ietf_actn_sdn_ctrl/scenario/ietf_actn_deploy.sh new file mode 100755 index 000000000..665d337c9 --- /dev/null +++ b/src/tests/tools/mock_ietf_actn_sdn_ctrl/scenario/ietf_actn_deploy.sh @@ -0,0 +1,36 @@ +# 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. + +# Set the URL of your local Docker registry where the images will be uploaded to. +export TFS_REGISTRY_IMAGE="http://localhost:32000/tfs/" + +# Set the list of components, separated by spaces, you want to build images for, and deploy. +# Supported components are: +# context device ztp policy service nbi monitoring webui +# interdomain slice pathcomp dlt +# dbscanserving opticalattackmitigator opticalattackdetector +# l3_attackmitigator l3_centralizedattackdetector l3_distributedattackdetector +export TFS_COMPONENTS="context device pathcomp service slice webui" + +# Set the tag you want to use for your images. +export TFS_IMAGE_TAG="dev" + +# Set the name of the Kubernetes namespace to deploy to. +export TFS_K8S_NAMESPACE="tfs" + +# Set additional manifest files to be applied after the deployment +export TFS_EXTRA_MANIFESTS="manifests/nginx_ingress_http.yaml" + +# Set the neew Grafana admin password +export TFS_GRAFANA_PASSWORD="admin123+" diff --git a/src/tests/tools/mock_ietf_actn_sdn_ctrl/scenario/network_descriptors.json b/src/tests/tools/mock_ietf_actn_sdn_ctrl/scenario/network_descriptors.json new file mode 100644 index 000000000..25fa940a4 --- /dev/null +++ b/src/tests/tools/mock_ietf_actn_sdn_ctrl/scenario/network_descriptors.json @@ -0,0 +1,117 @@ +{ + "contexts": [ + { + "context_id": {"context_uuid": {"uuid": "admin"}}, + "topology_ids": [], + "service_ids": [] + } + ], + "topologies": [ + { + "topology_id": {"topology_uuid": {"uuid": "admin"}, "context_id": {"context_uuid": {"uuid": "admin"}}}, + "device_ids": [], + "link_ids": [] + } + ], + "devices": [ + { + "device_id": {"device_uuid": {"uuid": "R1"}}, "device_type": "emu-packet-router", "device_drivers": [0], + "device_operational_status": 2, "device_endpoints": [], + "device_config": {"config_rules": [ + {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "127.0.0.1"}}, + {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "0"}}, + {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": {"endpoints": [ + {"type": "copper", "uuid": "MW", "sample_types": []}, + {"type": "copper", "uuid": "R2", "sample_types": []}, + {"type": "copper", "uuid": "EXT", "sample_types": []} + ]}}} + ]} + }, + { + "device_id": {"device_uuid": {"uuid": "R2"}}, "device_type": "emu-packet-router", "device_drivers": [0], + "device_operational_status": 2, "device_endpoints": [], + "device_config": {"config_rules": [ + {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "127.0.0.1"}}, + {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "0"}}, + {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": {"endpoints": [ + {"type": "copper", "uuid": "MW", "sample_types": []}, + {"type": "copper", "uuid": "R1", "sample_types": []}, + {"type": "copper", "uuid": "EXT", "sample_types": []} + ]}}} + ]} + }, + { + "device_id": {"device_uuid": {"uuid": "R3"}}, "device_type": "emu-packet-router", "device_drivers": [0], + "device_operational_status": 2, "device_endpoints": [], + "device_config": {"config_rules": [ + {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "127.0.0.1"}}, + {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "0"}}, + {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": {"endpoints": [ + {"type": "copper", "uuid": "MW", "sample_types": []}, + {"type": "copper", "uuid": "R4", "sample_types": []}, + {"type": "copper", "uuid": "EXT", "sample_types": []} + ]}}} + ]} + }, + { + "device_id": {"device_uuid": {"uuid": "R4"}}, "device_type": "emu-packet-router", "device_drivers": [0], + "device_operational_status": 2, "device_endpoints": [], + "device_config": {"config_rules": [ + {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "127.0.0.1"}}, + {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "0"}}, + {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": {"endpoints": [ + {"type": "copper", "uuid": "MW", "sample_types": []}, + {"type": "copper", "uuid": "R3", "sample_types": []}, + {"type": "copper", "uuid": "EXT", "sample_types": []} + ]}}} + ]} + }, + { + "device_id": {"device_uuid": {"uuid": "MW"}}, "device_type": "microwave-radio-system", "device_drivers": [4], + "device_operational_status": 2, "device_endpoints": [], + "device_config": {"config_rules": [ + {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "192.168.1.1"}}, + {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "8443"}}, + {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": {"timeout": 120}}} + ]} + } + ], + "links": [ + { + "link_id": {"link_uuid": {"uuid": "R1/R2==R2/R1"}}, "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "R1"}}, "endpoint_uuid": {"uuid": "R2"}}, + {"device_id": {"device_uuid": {"uuid": "R2"}}, "endpoint_uuid": {"uuid": "R1"}} + ] + }, + { + "link_id": {"link_uuid": {"uuid": "R3/R4==R4/R3"}}, "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "R3"}}, "endpoint_uuid": {"uuid": "R4"}}, + {"device_id": {"device_uuid": {"uuid": "R4"}}, "endpoint_uuid": {"uuid": "R3"}} + ] + }, + { + "link_id": {"link_uuid": {"uuid": "R1/MW==MW/172.18.0.1:1"}}, "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "R1"}}, "endpoint_uuid": {"uuid": "MW"}}, + {"device_id": {"device_uuid": {"uuid": "MW"}}, "endpoint_uuid": {"uuid": "172.18.0.1:1"}} + ] + }, + { + "link_id": {"link_uuid": {"uuid": "R2/MW==MW/172.18.0.2:1"}}, "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "R2"}}, "endpoint_uuid": {"uuid": "MW"}}, + {"device_id": {"device_uuid": {"uuid": "MW"}}, "endpoint_uuid": {"uuid": "172.18.0.2:1"}} + ] + }, + { + "link_id": {"link_uuid": {"uuid": "R3/MW==MW/172.18.0.3:1"}}, "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "R3"}}, "endpoint_uuid": {"uuid": "MW"}}, + {"device_id": {"device_uuid": {"uuid": "MW"}}, "endpoint_uuid": {"uuid": "172.18.0.3:1"}} + ] + }, + { + "link_id": {"link_uuid": {"uuid": "R4/MW==MW/172.18.0.4:1"}}, "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "R4"}}, "endpoint_uuid": {"uuid": "MW"}}, + {"device_id": {"device_uuid": {"uuid": "MW"}}, "endpoint_uuid": {"uuid": "172.18.0.4:1"}} + ] + } + ] +} \ No newline at end of file diff --git a/src/tests/tools/mock_ietf_actn_sdn_ctrl/scenario/service_descriptor.json b/src/tests/tools/mock_ietf_actn_sdn_ctrl/scenario/service_descriptor.json new file mode 100644 index 000000000..2d4ed3eaf --- /dev/null +++ b/src/tests/tools/mock_ietf_actn_sdn_ctrl/scenario/service_descriptor.json @@ -0,0 +1,25 @@ +{ + "services": [ + { + "service_id": { + "context_id": {"context_uuid": {"uuid": "admin"}}, + "service_uuid": {"uuid": "mw-svc"} + }, + "service_type": 2, + "service_status": {"service_status": 1}, + "service_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "R1"}}, "endpoint_uuid": {"uuid": "EXT"}}, + {"device_id": {"device_uuid": {"uuid": "R3"}}, "endpoint_uuid": {"uuid": "EXT"}} + ], + "service_constraints": [ + {"sla_capacity": {"capacity_gbps": 10.0}}, + {"sla_latency": {"e2e_latency_ms": 15.2}} + ], + "service_config": {"config_rules": [ + {"action": 1, "custom": {"resource_key": "/settings", "resource_value": { + "vlan_id": 121 + }}} + ]} + } + ] +} diff --git a/src/tests/tools/mock_ietf_actn_sdn_ctrl/ssl_not_working/Dockerfile b/src/tests/tools/mock_ietf_actn_sdn_ctrl/ssl_not_working/Dockerfile new file mode 100644 index 000000000..70fc81e54 --- /dev/null +++ b/src/tests/tools/mock_ietf_actn_sdn_ctrl/ssl_not_working/Dockerfile @@ -0,0 +1,35 @@ +# 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. + +FROM python:3.9-slim + +# Set Python to show logs as they occur +ENV PYTHONUNBUFFERED=0 + +# Get generic Python packages +RUN python3 -m pip install --upgrade pip +RUN python3 -m pip install --upgrade setuptools wheel +RUN python3 -m pip install --upgrade pip-tools + +# Create component sub-folders, and copy content +RUN mkdir -p /var/teraflow/mock_mw_sdn_ctrl +WORKDIR /var/teraflow/mock_mw_sdn_ctrl +COPY . . + +# Get specific Python packages +RUN pip-compile --quiet --output-file=requirements.txt requirements.in +RUN python3 -m pip install -r requirements.txt + +# Start the service +ENTRYPOINT ["python", "MockMWSdnCtrl.py"] diff --git a/src/tests/tools/mock_ietf_actn_sdn_ctrl/ssl_not_working/build.sh b/src/tests/tools/mock_ietf_actn_sdn_ctrl/ssl_not_working/build.sh new file mode 100755 index 000000000..4df315cec --- /dev/null +++ b/src/tests/tools/mock_ietf_actn_sdn_ctrl/ssl_not_working/build.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +docker build -t mock-mw-sdn-ctrl:test -f Dockerfile . +docker tag mock-mw-sdn-ctrl:test localhost:32000/tfs/mock-mw-sdn-ctrl:test +docker push localhost:32000/tfs/mock-mw-sdn-ctrl:test diff --git a/src/tests/tools/mock_ietf_actn_sdn_ctrl/ssl_not_working/deploy.sh b/src/tests/tools/mock_ietf_actn_sdn_ctrl/ssl_not_working/deploy.sh new file mode 100755 index 000000000..ded232e5c --- /dev/null +++ b/src/tests/tools/mock_ietf_actn_sdn_ctrl/ssl_not_working/deploy.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +kubectl delete namespace mocks +kubectl --namespace mocks apply -f mock-mw-sdn-ctrl.yaml diff --git a/src/tests/tools/mock_ietf_actn_sdn_ctrl/ssl_not_working/mock-mw-sdn-ctrl.yaml b/src/tests/tools/mock_ietf_actn_sdn_ctrl/ssl_not_working/mock-mw-sdn-ctrl.yaml new file mode 100644 index 000000000..05b89f949 --- /dev/null +++ b/src/tests/tools/mock_ietf_actn_sdn_ctrl/ssl_not_working/mock-mw-sdn-ctrl.yaml @@ -0,0 +1,46 @@ +kind: Namespace +apiVersion: v1 +metadata: + name: mocks +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mock-mw-sdn-ctrl +spec: + selector: + matchLabels: + app: mock-mw-sdn-ctrl + replicas: 1 + template: + metadata: + labels: + app: mock-mw-sdn-ctrl + spec: + terminationGracePeriodSeconds: 5 + containers: + - name: server + image: localhost:32000/tfs/mock-mw-sdn-ctrl:test + imagePullPolicy: IfNotPresent + ports: + - containerPort: 8443 + resources: + requests: + cpu: 250m + memory: 512Mi + limits: + cpu: 700m + memory: 1024Mi +--- +apiVersion: v1 +kind: Service +metadata: + name: mock-mw-sdn-ctrl +spec: + type: ClusterIP + selector: + app: mock-mw-sdn-ctrl + ports: + - name: https + port: 8443 + targetPort: 8443 diff --git a/src/tests/tools/mock_ietf_actn_sdn_ctrl/ssl_not_working/requirements.in b/src/tests/tools/mock_ietf_actn_sdn_ctrl/ssl_not_working/requirements.in new file mode 100644 index 000000000..f4bc19106 --- /dev/null +++ b/src/tests/tools/mock_ietf_actn_sdn_ctrl/ssl_not_working/requirements.in @@ -0,0 +1,21 @@ +# 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. + +cryptography==39.0.1 +pyopenssl==23.0.0 +Flask==2.1.3 +Flask-HTTPAuth==4.5.0 +Flask-RESTful==0.3.9 +jsonschema==4.4.0 +requests==2.27.1 diff --git a/src/tests/tools/mock_ietf_actn_sdn_ctrl/test_ietf_actn.py b/src/tests/tools/mock_ietf_actn_sdn_ctrl/test_ietf_actn.py new file mode 100644 index 000000000..0329d30ad --- /dev/null +++ b/src/tests/tools/mock_ietf_actn_sdn_ctrl/test_ietf_actn.py @@ -0,0 +1,84 @@ +import json, logging, requests +from requests.auth import HTTPBasicAuth +from typing import Optional + +LOGGER = logging.getLogger(__name__) + +HTTP_OK_CODES = { + 200, # OK + 201, # Created + 202, # Accepted + 204, # No Content +} + +def create_connectivity_service( + root_url, uuid, node_id_src, tp_id_src, node_id_dst, tp_id_dst, vlan_id, + auth : Optional[HTTPBasicAuth] = None, timeout : Optional[int] = None +): + + url = '{:s}/nmswebs/restconf/data/ietf-eth-tran-service:etht-svc'.format(root_url) + headers = {'content-type': 'application/json'} + data = { + 'etht-svc-instances': [ + { + 'etht-svc-name': uuid, + 'etht-svc-type': 'ietf-eth-tran-types:p2p-svc', + 'etht-svc-end-points': [ + { + 'etht-svc-access-points': [ + {'access-node-id': node_id_src, 'access-ltp-id': tp_id_src, 'access-point-id': '1'} + ], + 'outer-tag': {'vlan-value': vlan_id, 'tag-type': 'ietf-eth-tran-types:classify-c-vlan'}, + 'etht-svc-end-point-name': '{:s}:{:s}'.format(str(node_id_src), str(tp_id_src)), + 'service-classification-type': 'ietf-eth-tran-types:vlan-classification' + }, + { + 'etht-svc-access-points': [ + {'access-node-id': node_id_dst, 'access-ltp-id': tp_id_dst, 'access-point-id': '2'} + ], + 'outer-tag': {'vlan-value': vlan_id, 'tag-type': 'ietf-eth-tran-types:classify-c-vlan'}, + 'etht-svc-end-point-name': '{:s}:{:s}'.format(str(node_id_dst), str(tp_id_dst)), + 'service-classification-type': 'ietf-eth-tran-types:vlan-classification' + } + ] + } + ] + } + results = [] + try: + LOGGER.info('Connectivity service {:s}: {:s}'.format(str(uuid), str(data))) + response = requests.post( + url=url, data=json.dumps(data), timeout=timeout, headers=headers, verify=False, auth=auth) + LOGGER.info('Microwave Driver response: {:s}'.format(str(response))) + except Exception as e: # pylint: disable=broad-except + LOGGER.exception('Exception creating ConnectivityService(uuid={:s}, data={:s})'.format(str(uuid), str(data))) + results.append(e) + else: + if response.status_code not in HTTP_OK_CODES: + msg = 'Could not create ConnectivityService(uuid={:s}, data={:s}). status_code={:s} reply={:s}' + LOGGER.error(msg.format(str(uuid), str(data), str(response.status_code), str(response))) + results.append(response.status_code in HTTP_OK_CODES) + return results + +def delete_connectivity_service(root_url, uuid, auth : Optional[HTTPBasicAuth] = None, timeout : Optional[int] = None): + url = '{:s}/nmswebs/restconf/data/ietf-eth-tran-service:etht-svc/etht-svc-instances={:s}' + url = url.format(root_url, uuid) + results = [] + try: + response = requests.delete(url=url, timeout=timeout, verify=False, auth=auth) + except Exception as e: # pylint: disable=broad-except + LOGGER.exception('Exception deleting ConnectivityService(uuid={:s})'.format(str(uuid))) + results.append(e) + else: + if response.status_code not in HTTP_OK_CODES: + msg = 'Could not delete ConnectivityService(uuid={:s}). status_code={:s} reply={:s}' + LOGGER.error(msg.format(str(uuid), str(response.status_code), str(response))) + results.append(response.status_code in HTTP_OK_CODES) + return results + +if __name__ == '__main__': + ROOT_URL = 'https://127.0.0.1:8443' + SERVICE_UUID = 'my-service' + + create_connectivity_service(ROOT_URL, SERVICE_UUID, '172.18.0.1', '1', '172.18.0.2', '2', 300) + delete_connectivity_service(ROOT_URL, SERVICE_UUID) -- GitLab From d0bbc4e725da5d29199ea7b6ea0e6b73a3e2d237 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Mon, 15 Jan 2024 17:15:36 +0000 Subject: [PATCH 04/18] Tests - Mock IETF ACTN SDN Ctrl: - Updated Mock IETF ACTN SDN Controller --- .../{ssl_not_working => }/Dockerfile | 8 +- .../MockIetfActnSdnCtrl.py | 118 ++++-------------- .../ResourceEthServices.py | 53 ++++++++ .../ResourceOsuTunnels.py | 51 ++++++++ .../tools/mock_ietf_actn_sdn_ctrl/__init__.py | 14 +++ .../tools/mock_ietf_actn_sdn_ctrl/build.sh | 5 + .../tools/mock_ietf_actn_sdn_ctrl/deploy.sh | 4 + .../mock-ietf-actn-sdn-ctrl.yaml | 64 ++++++++++ .../{ssl_not_working => }/requirements.in | 1 + .../ssl_not_working/build.sh | 5 - .../ssl_not_working/deploy.sh | 4 - .../ssl_not_working/mock-mw-sdn-ctrl.yaml | 46 ------- 12 files changed, 221 insertions(+), 152 deletions(-) rename src/tests/tools/mock_ietf_actn_sdn_ctrl/{ssl_not_working => }/Dockerfile (86%) create mode 100644 src/tests/tools/mock_ietf_actn_sdn_ctrl/ResourceEthServices.py create mode 100644 src/tests/tools/mock_ietf_actn_sdn_ctrl/ResourceOsuTunnels.py create mode 100644 src/tests/tools/mock_ietf_actn_sdn_ctrl/__init__.py create mode 100755 src/tests/tools/mock_ietf_actn_sdn_ctrl/build.sh create mode 100755 src/tests/tools/mock_ietf_actn_sdn_ctrl/deploy.sh create mode 100644 src/tests/tools/mock_ietf_actn_sdn_ctrl/mock-ietf-actn-sdn-ctrl.yaml rename src/tests/tools/mock_ietf_actn_sdn_ctrl/{ssl_not_working => }/requirements.in (97%) delete mode 100755 src/tests/tools/mock_ietf_actn_sdn_ctrl/ssl_not_working/build.sh delete mode 100755 src/tests/tools/mock_ietf_actn_sdn_ctrl/ssl_not_working/deploy.sh delete mode 100644 src/tests/tools/mock_ietf_actn_sdn_ctrl/ssl_not_working/mock-mw-sdn-ctrl.yaml diff --git a/src/tests/tools/mock_ietf_actn_sdn_ctrl/ssl_not_working/Dockerfile b/src/tests/tools/mock_ietf_actn_sdn_ctrl/Dockerfile similarity index 86% rename from src/tests/tools/mock_ietf_actn_sdn_ctrl/ssl_not_working/Dockerfile rename to src/tests/tools/mock_ietf_actn_sdn_ctrl/Dockerfile index 70fc81e54..05c785fb1 100644 --- a/src/tests/tools/mock_ietf_actn_sdn_ctrl/ssl_not_working/Dockerfile +++ b/src/tests/tools/mock_ietf_actn_sdn_ctrl/Dockerfile @@ -23,13 +23,15 @@ RUN python3 -m pip install --upgrade setuptools wheel RUN python3 -m pip install --upgrade pip-tools # Create component sub-folders, and copy content -RUN mkdir -p /var/teraflow/mock_mw_sdn_ctrl -WORKDIR /var/teraflow/mock_mw_sdn_ctrl +RUN mkdir -p /var/teraflow/mock_ietf_actn_sdn_ctrl +WORKDIR /var/teraflow/mock_ietf_actn_sdn_ctrl COPY . . # Get specific Python packages RUN pip-compile --quiet --output-file=requirements.txt requirements.in RUN python3 -m pip install -r requirements.txt +RUN python3 -m pip list + # Start the service -ENTRYPOINT ["python", "MockMWSdnCtrl.py"] +ENTRYPOINT ["python", "MockIetfActnSdnCtrl.py"] diff --git a/src/tests/tools/mock_ietf_actn_sdn_ctrl/MockIetfActnSdnCtrl.py b/src/tests/tools/mock_ietf_actn_sdn_ctrl/MockIetfActnSdnCtrl.py index c20dde1b9..ad49b4a42 100644 --- a/src/tests/tools/mock_ietf_actn_sdn_ctrl/MockIetfActnSdnCtrl.py +++ b/src/tests/tools/mock_ietf_actn_sdn_ctrl/MockIetfActnSdnCtrl.py @@ -12,79 +12,27 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Mock MicroWave SDN controller +# Mock IETF ACTN SDN controller # ----------------------------- # REST server implementing minimal support for: -# - IETF YANG data model for Network Topology -# Ref: https://www.rfc-editor.org/rfc/rfc8345.html -# - IETF YANG data model for Transport Network Client Signals -# Ref: https://www.ietf.org/archive/id/draft-ietf-ccamp-client-signal-yang-07.html +# - IETF YANG Data Model for Transport Network Client Signals +# Ref: https://www.ietf.org/archive/id/draft-ietf-ccamp-client-signal-yang-10.html +# - IETF YANG Data Model for Traffic Engineering Tunnels, Label Switched Paths and Interfaces +# Ref: https://www.ietf.org/archive/id/draft-ietf-teas-yang-te-34.html -# Ref: https://blog.miguelgrinberg.com/post/running-your-flask-application-over-https -# Ref: https://blog.miguelgrinberg.com/post/designing-a-restful-api-using-flask-restful - import functools, logging, sys, time -from flask import Flask, abort, jsonify, make_response, request +from flask import Flask, jsonify, make_response, request from flask_restful import Api, Resource +from ResourceEthServices import EthService, EthServices +from ResourceOsuTunnels import OsuTunnel, OsuTunnels BIND_ADDRESS = '0.0.0.0' BIND_PORT = 8443 -BASE_URL = '/nmswebs/restconf/data' +BASE_URL = '/restconf/data' STR_ENDPOINT = 'https://{:s}:{:s}{:s}'.format(str(BIND_ADDRESS), str(BIND_PORT), str(BASE_URL)) LOG_LEVEL = logging.DEBUG -NETWORK_NODES = [ - {'node-id': '192.168.27.139', 'ietf-network-topology:termination-point': [ - {'tp-id': '1', 'ietf-te-topology:te': {'name': 'ethernet'}}, - {'tp-id': '2', 'ietf-te-topology:te': {'name': 'ethernet'}}, - {'tp-id': '3', 'ietf-te-topology:te': {'name': 'ethernet'}}, - {'tp-id': '4', 'ietf-te-topology:te': {'name': 'ethernet'}}, - {'tp-id': '5', 'ietf-te-topology:te': {'name': 'ethernet'}}, - {'tp-id': '6', 'ietf-te-topology:te': {'name': 'ethernet'}}, - {'tp-id': '10', 'ietf-te-topology:te': {'name': 'antena' }}, - ]}, - {'node-id': '192.168.27.140', 'ietf-network-topology:termination-point': [ - {'tp-id': '1', 'ietf-te-topology:te': {'name': 'ethernet'}}, - {'tp-id': '2', 'ietf-te-topology:te': {'name': 'ethernet'}}, - {'tp-id': '3', 'ietf-te-topology:te': {'name': 'ethernet'}}, - {'tp-id': '4', 'ietf-te-topology:te': {'name': 'ethernet'}}, - {'tp-id': '5', 'ietf-te-topology:te': {'name': 'ethernet'}}, - {'tp-id': '6', 'ietf-te-topology:te': {'name': 'ethernet'}}, - {'tp-id': '10', 'ietf-te-topology:te': {'name': 'antena' }}, - ]}, - {'node-id': '192.168.27.141', 'ietf-network-topology:termination-point': [ - {'tp-id': '1', 'ietf-te-topology:te': {'name': 'ethernet'}}, - {'tp-id': '2', 'ietf-te-topology:te': {'name': 'ethernet'}}, - {'tp-id': '3', 'ietf-te-topology:te': {'name': 'ethernet'}}, - {'tp-id': '4', 'ietf-te-topology:te': {'name': 'ethernet'}}, - {'tp-id': '5', 'ietf-te-topology:te': {'name': 'ethernet'}}, - {'tp-id': '6', 'ietf-te-topology:te': {'name': 'ethernet'}}, - {'tp-id': '10', 'ietf-te-topology:te': {'name': 'antena' }}, - ]}, - {'node-id': '192.168.27.142', 'ietf-network-topology:termination-point': [ - {'tp-id': '1', 'ietf-te-topology:te': {'name': 'ethernet'}}, - {'tp-id': '2', 'ietf-te-topology:te': {'name': 'ethernet'}}, - {'tp-id': '3', 'ietf-te-topology:te': {'name': 'ethernet'}}, - {'tp-id': '4', 'ietf-te-topology:te': {'name': 'ethernet'}}, - {'tp-id': '5', 'ietf-te-topology:te': {'name': 'ethernet'}}, - {'tp-id': '6', 'ietf-te-topology:te': {'name': 'ethernet'}}, - {'tp-id': '10', 'ietf-te-topology:te': {'name': 'antena' }}, - ]} -] -NETWORK_LINKS = [ - { 'link-id' : '192.168.27.139:10--192.168.27.140:10', - 'source' : {'source-node': '192.168.27.139', 'source-tp': '10'}, - 'destination': {'dest-node' : '192.168.27.140', 'dest-tp' : '10'}, - }, - { 'link-id' : '192.168.27.141:10--192.168.27.142:10', - 'source' : {'source-node': '192.168.27.141', 'source-tp': '10'}, - 'destination': {'dest-node' : '192.168.27.142', 'dest-tp' : '10'}, - } -] -NETWORK_SERVICES = {} - - logging.basicConfig(level=LOG_LEVEL, format="[%(asctime)s] %(levelname)s:%(name)s:%(message)s") LOGGER = logging.getLogger(__name__) @@ -99,35 +47,6 @@ class Health(Resource): def get(self): return make_response(jsonify({}), 200) -class Network(Resource): - def get(self, network_uuid : str): - if network_uuid != 'SIAE-ETH-TOPOLOGY': abort(400) - network = {'node': NETWORK_NODES, 'ietf-network-topology:link': NETWORK_LINKS} - return make_response(jsonify({'ietf-network:network': network}), 200) - -class Services(Resource): - def get(self): - services = [service for service in NETWORK_SERVICES.values()] - return make_response(jsonify({'ietf-eth-tran-service:etht-svc': {'etht-svc-instances': services}}), 200) - - def post(self): - json_request = request.get_json() - if not json_request: abort(400) - if not isinstance(json_request, dict): abort(400) - if 'etht-svc-instances' not in json_request: abort(400) - json_services = json_request['etht-svc-instances'] - if not isinstance(json_services, list): abort(400) - if len(json_services) != 1: abort(400) - svc_data = json_services[0] - etht_svc_name = svc_data['etht-svc-name'] - NETWORK_SERVICES[etht_svc_name] = svc_data - return make_response(jsonify({}), 201) - -class DelServices(Resource): - def delete(self, service_uuid : str): - NETWORK_SERVICES.pop(service_uuid, None) - return make_response(jsonify({}), 204) - def main(): LOGGER.info('Starting...') @@ -135,10 +54,21 @@ def main(): app.after_request(functools.partial(log_request, LOGGER)) api = Api(app, prefix=BASE_URL) - api.add_resource(Health, '/ietf-network:networks') - api.add_resource(Network, '/ietf-network:networks/network=') - api.add_resource(Services, '/ietf-eth-tran-service:etht-svc') - api.add_resource(DelServices, '/ietf-eth-tran-service:etht-svc/etht-svc-instances=') + api.add_resource( + Health, '/' + ) + api.add_resource( + OsuTunnels, '/ietf-te:tunnel' + ) + api.add_resource( + OsuTunnel, '/ietf-te:tunnel[name=]' + ) + api.add_resource( + EthServices, '/ietf-eth-tran-service:etht-svc' + ) + api.add_resource( + EthService, '/ietf-eth-tran-service:etht-svc/etht-svc-instances[etht-svc-name=]' + ) LOGGER.info('Listening on {:s}...'.format(str(STR_ENDPOINT))) app.run(debug=True, host=BIND_ADDRESS, port=BIND_PORT, ssl_context='adhoc') diff --git a/src/tests/tools/mock_ietf_actn_sdn_ctrl/ResourceEthServices.py b/src/tests/tools/mock_ietf_actn_sdn_ctrl/ResourceEthServices.py new file mode 100644 index 000000000..4fcc0ca71 --- /dev/null +++ b/src/tests/tools/mock_ietf_actn_sdn_ctrl/ResourceEthServices.py @@ -0,0 +1,53 @@ +# 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. + +# REST-API resource implementing minimal support for "IETF YANG Data Model for Transport Network Client Signals". +# Ref: https://www.ietf.org/archive/id/draft-ietf-ccamp-client-signal-yang-10.html + +from flask import abort, jsonify, make_response, request +from flask_restful import Resource + +ETH_SERVICES = {} + +class EthServices(Resource): + def get(self): + eth_services = [eth_service for eth_service in ETH_SERVICES.values()] + data = {'ietf-eth-tran-service:etht-svc': {'etht-svc-instances': eth_services}} + return make_response(jsonify(data), 200) + + def post(self): + json_request = request.get_json() + if not json_request: abort(400) + if not isinstance(json_request, dict): abort(400) + if 'ietf-eth-tran-service:etht-svc' not in json_request: abort(400) + json_request = json_request['ietf-eth-tran-service:etht-svc'] + if 'etht-svc-instances' not in json_request: abort(400) + eth_services = json_request['etht-svc-instances'] + if not isinstance(eth_services, list): abort(400) + if len(eth_services) != 1: abort(400) + eth_service = eth_services[0] + etht_svc_name = eth_service['etht-svc-name'] + ETH_SERVICES[etht_svc_name] = eth_service + return make_response(jsonify({}), 201) + +class EthService(Resource): + def get(self, service_uuid : str): + eth_service = ETH_SERVICES.get(service_uuid, None) + data,status = ({}, 404) if eth_service is None else (eth_service, 200) + return make_response(jsonify(data), status) + + def delete(self, service_uuid : str): + eth_service = ETH_SERVICES.pop(service_uuid, None) + data,status = ({}, 404) if eth_service is None else (eth_service, 204) + return make_response(jsonify(data), status) diff --git a/src/tests/tools/mock_ietf_actn_sdn_ctrl/ResourceOsuTunnels.py b/src/tests/tools/mock_ietf_actn_sdn_ctrl/ResourceOsuTunnels.py new file mode 100644 index 000000000..2fe2f319b --- /dev/null +++ b/src/tests/tools/mock_ietf_actn_sdn_ctrl/ResourceOsuTunnels.py @@ -0,0 +1,51 @@ +# 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. + +# REST-API resource implementing minimal support for "IETF YANG Data Model for Traffic Engineering Tunnels, +# Label Switched Paths and Interfaces". +# Ref: https://www.ietf.org/archive/id/draft-ietf-teas-yang-te-34.html + + +from flask import abort, jsonify, make_response, request +from flask_restful import Resource + +OSU_TUNNELS = {} + +class OsuTunnels(Resource): + def get(self): + osu_tunnels = [osu_tunnel for osu_tunnel in OSU_TUNNELS.values()] + data = {'ietf-te:tunnel': osu_tunnels} + return make_response(jsonify(data), 200) + + def post(self): + json_request = request.get_json() + if not json_request: abort(400) + if not isinstance(json_request, list): abort(400) + osu_tunnels = json_request['ietf-te:tunnel'] + if len(osu_tunnels) != 1: abort(400) + osu_tunnel = osu_tunnels[0] + name = osu_tunnel['name'] + OSU_TUNNELS[name] = osu_tunnel + return make_response(jsonify({}), 201) + +class OsuTunnel(Resource): + def get(self, osu_tunnel_name : str): + osu_tunnel = OSU_TUNNELS.get(osu_tunnel_name, None) + data,status = ({}, 404) if osu_tunnel is None else (osu_tunnel, 200) + return make_response(jsonify(data), status) + + def delete(self, osu_tunnel_name : str): + osu_tunnel = OSU_TUNNELS.pop(osu_tunnel_name, None) + data,status = ({}, 404) if osu_tunnel is None else (osu_tunnel, 204) + return make_response(jsonify(data), status) diff --git a/src/tests/tools/mock_ietf_actn_sdn_ctrl/__init__.py b/src/tests/tools/mock_ietf_actn_sdn_ctrl/__init__.py new file mode 100644 index 000000000..1549d9811 --- /dev/null +++ b/src/tests/tools/mock_ietf_actn_sdn_ctrl/__init__.py @@ -0,0 +1,14 @@ +# 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. + diff --git a/src/tests/tools/mock_ietf_actn_sdn_ctrl/build.sh b/src/tests/tools/mock_ietf_actn_sdn_ctrl/build.sh new file mode 100755 index 000000000..8d125727b --- /dev/null +++ b/src/tests/tools/mock_ietf_actn_sdn_ctrl/build.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +docker build -t mock-ietf-actn-sdn-ctrl:test -f Dockerfile . +docker tag mock-ietf-actn-sdn-ctrl:test localhost:32000/tfs/mock-ietf-actn-sdn-ctrl:test +docker push localhost:32000/tfs/mock-ietf-actn-sdn-ctrl:test diff --git a/src/tests/tools/mock_ietf_actn_sdn_ctrl/deploy.sh b/src/tests/tools/mock_ietf_actn_sdn_ctrl/deploy.sh new file mode 100755 index 000000000..822ecc06e --- /dev/null +++ b/src/tests/tools/mock_ietf_actn_sdn_ctrl/deploy.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +kubectl delete namespace mocks +kubectl --namespace mocks apply -f mock-ietf-actn-sdn-ctrl.yaml diff --git a/src/tests/tools/mock_ietf_actn_sdn_ctrl/mock-ietf-actn-sdn-ctrl.yaml b/src/tests/tools/mock_ietf_actn_sdn_ctrl/mock-ietf-actn-sdn-ctrl.yaml new file mode 100644 index 000000000..32cfd9228 --- /dev/null +++ b/src/tests/tools/mock_ietf_actn_sdn_ctrl/mock-ietf-actn-sdn-ctrl.yaml @@ -0,0 +1,64 @@ +# 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. + +kind: Namespace +apiVersion: v1 +metadata: + name: mocks +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mock-ietf-actn-sdn-ctrl +spec: + selector: + matchLabels: + app: mock-ietf-actn-sdn-ctrl + replicas: 1 + template: + metadata: + annotations: + config.linkerd.io/skip-inbound-ports: "8443" + labels: + app: mock-ietf-actn-sdn-ctrl + spec: + terminationGracePeriodSeconds: 5 + containers: + - name: server + image: localhost:32000/tfs/mock-ietf-actn-sdn-ctrl:test + imagePullPolicy: IfNotPresent + ports: + - containerPort: 8443 + resources: + requests: + cpu: 250m + memory: 512Mi + limits: + cpu: 700m + memory: 1024Mi +--- +apiVersion: v1 +kind: Service +metadata: + name: mock-ietf-actn-sdn-ctrl + labels: + app: mock-ietf-actn-sdn-ctrl +spec: + type: ClusterIP + selector: + app: mock-ietf-actn-sdn-ctrl + ports: + - name: https + port: 8443 + targetPort: 8443 diff --git a/src/tests/tools/mock_ietf_actn_sdn_ctrl/ssl_not_working/requirements.in b/src/tests/tools/mock_ietf_actn_sdn_ctrl/requirements.in similarity index 97% rename from src/tests/tools/mock_ietf_actn_sdn_ctrl/ssl_not_working/requirements.in rename to src/tests/tools/mock_ietf_actn_sdn_ctrl/requirements.in index f4bc19106..d91775403 100644 --- a/src/tests/tools/mock_ietf_actn_sdn_ctrl/ssl_not_working/requirements.in +++ b/src/tests/tools/mock_ietf_actn_sdn_ctrl/requirements.in @@ -19,3 +19,4 @@ Flask-HTTPAuth==4.5.0 Flask-RESTful==0.3.9 jsonschema==4.4.0 requests==2.27.1 +werkzeug==2.3.7 diff --git a/src/tests/tools/mock_ietf_actn_sdn_ctrl/ssl_not_working/build.sh b/src/tests/tools/mock_ietf_actn_sdn_ctrl/ssl_not_working/build.sh deleted file mode 100755 index 4df315cec..000000000 --- a/src/tests/tools/mock_ietf_actn_sdn_ctrl/ssl_not_working/build.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -docker build -t mock-mw-sdn-ctrl:test -f Dockerfile . -docker tag mock-mw-sdn-ctrl:test localhost:32000/tfs/mock-mw-sdn-ctrl:test -docker push localhost:32000/tfs/mock-mw-sdn-ctrl:test diff --git a/src/tests/tools/mock_ietf_actn_sdn_ctrl/ssl_not_working/deploy.sh b/src/tests/tools/mock_ietf_actn_sdn_ctrl/ssl_not_working/deploy.sh deleted file mode 100755 index ded232e5c..000000000 --- a/src/tests/tools/mock_ietf_actn_sdn_ctrl/ssl_not_working/deploy.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash - -kubectl delete namespace mocks -kubectl --namespace mocks apply -f mock-mw-sdn-ctrl.yaml diff --git a/src/tests/tools/mock_ietf_actn_sdn_ctrl/ssl_not_working/mock-mw-sdn-ctrl.yaml b/src/tests/tools/mock_ietf_actn_sdn_ctrl/ssl_not_working/mock-mw-sdn-ctrl.yaml deleted file mode 100644 index 05b89f949..000000000 --- a/src/tests/tools/mock_ietf_actn_sdn_ctrl/ssl_not_working/mock-mw-sdn-ctrl.yaml +++ /dev/null @@ -1,46 +0,0 @@ -kind: Namespace -apiVersion: v1 -metadata: - name: mocks ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: mock-mw-sdn-ctrl -spec: - selector: - matchLabels: - app: mock-mw-sdn-ctrl - replicas: 1 - template: - metadata: - labels: - app: mock-mw-sdn-ctrl - spec: - terminationGracePeriodSeconds: 5 - containers: - - name: server - image: localhost:32000/tfs/mock-mw-sdn-ctrl:test - imagePullPolicy: IfNotPresent - ports: - - containerPort: 8443 - resources: - requests: - cpu: 250m - memory: 512Mi - limits: - cpu: 700m - memory: 1024Mi ---- -apiVersion: v1 -kind: Service -metadata: - name: mock-mw-sdn-ctrl -spec: - type: ClusterIP - selector: - app: mock-mw-sdn-ctrl - ports: - - name: https - port: 8443 - targetPort: 8443 -- GitLab From 9c83759f29d524d76229bcef10ee7edffb359205 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Mon, 15 Jan 2024 18:15:01 +0000 Subject: [PATCH 05/18] Device - IETF ACTN Driver: - Intermediate backup - Added example messages - Implemented request message composers - Implemented handlers (work in progress) --- .../drivers/ietf_actn/ComposerOsuTunnel.py | 80 ------ src/device/service/drivers/ietf_actn/Tools.py | 39 +-- .../drivers/ietf_actn/examples/eth_svc_1.json | 91 +++++++ .../drivers/ietf_actn/examples/eth_svc_2.json | 91 +++++++ .../ietf_actn/examples/osu_tunnel_1.json | 44 ++++ .../ietf_actn/examples/osu_tunnel_2.json | 44 ++++ .../drivers/ietf_actn/handlers/EthService.py | 106 ++++++++ .../drivers/ietf_actn/handlers/OsuTunnel.py | 236 ++++++++++++++++++ .../drivers/ietf_actn/handlers/Tools.py | 20 ++ .../__init__.py} | 1 - 10 files changed, 635 insertions(+), 117 deletions(-) delete mode 100644 src/device/service/drivers/ietf_actn/ComposerOsuTunnel.py create mode 100644 src/device/service/drivers/ietf_actn/examples/eth_svc_1.json create mode 100644 src/device/service/drivers/ietf_actn/examples/eth_svc_2.json create mode 100644 src/device/service/drivers/ietf_actn/examples/osu_tunnel_1.json create mode 100644 src/device/service/drivers/ietf_actn/examples/osu_tunnel_2.json create mode 100644 src/device/service/drivers/ietf_actn/handlers/EthService.py create mode 100644 src/device/service/drivers/ietf_actn/handlers/OsuTunnel.py create mode 100644 src/device/service/drivers/ietf_actn/handlers/Tools.py rename src/device/service/drivers/ietf_actn/{ComposerEthService.py => handlers/__init__.py} (99%) diff --git a/src/device/service/drivers/ietf_actn/ComposerOsuTunnel.py b/src/device/service/drivers/ietf_actn/ComposerOsuTunnel.py deleted file mode 100644 index ad369482e..000000000 --- a/src/device/service/drivers/ietf_actn/ComposerOsuTunnel.py +++ /dev/null @@ -1,80 +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. - -import enum -from typing import Dict - -OSU_TUNNEL_URL = '/restconf/data/ietf-te:tunnel' - -class EndpointProtectionRoleEnum(enum.Enum): - WORK = 'work' - -class LspProtectionTypeEnum(enum.Enum): - UNPROTECTED = 'ietf-te-types:lsp-protection-unprotected' - -class LspRestorationTypeEnum(enum.Enum): - NOT_APPLICABLE = 'ietf-te-types:lsp-restoration-not-applicable' - -class TunnelAdminStateEnum(enum.Enum): - UP = 'ietf-te-types:tunnel-admin-state-up' - -class OduTypeEnum(enum.Enum): - OSUFLEX = 'osuflex' - -def compose_osu_tunnel_endpoint( - node_id : str, tp_id : str, ttp_channel_name : str, - protection_role : EndpointProtectionRoleEnum = EndpointProtectionRoleEnum.WORK -) -> Dict: - return { - 'node-id': node_id, 'tp-id': tp_id, 'ttp-channel-name': ttp_channel_name, - 'protection-role': protection_role.value - } - -def compose_osu_tunnel_te_bandwidth_odu(odu_type : OduTypeEnum, number : int) -> Dict: - return {'layer': 'odu', 'odu-type': odu_type.value, 'number': number} - -def compose_osu_tunnel_protection( - type_ : LspProtectionTypeEnum = LspProtectionTypeEnum.UNPROTECTED, reversion_disable : bool = True -) -> Dict: - return {'protection-type': type_.value, 'protection-reversion-disable': reversion_disable} - -def compose_osu_tunnel_restoration( - type_ : LspRestorationTypeEnum = LspRestorationTypeEnum.NOT_APPLICABLE, restoration_lock : bool = False -) -> Dict: - return {'restoration-type': type_.value, 'restoration-lock': restoration_lock} - -def compose_osu_tunnel( - name : str, - src_node_id : str, src_tp_id : str, src_ttp_channel_name : str, - dst_node_id : str, dst_tp_id : str, dst_ttp_channel_name : str, - odu_type : OduTypeEnum, osuflex_number : int, - delay : int, bidirectional : bool = True, - admin_state : TunnelAdminStateEnum = TunnelAdminStateEnum.UP -) -> Dict: - return {'ietf-te:tunnel': [{ - 'name': name.lower(), - 'title': name.upper(), - 'admin-state': admin_state.value, - 'delay': delay, - 'te-bandwidth': compose_osu_tunnel_te_bandwidth_odu(odu_type, osuflex_number), - 'bidirectional': bidirectional, - 'source-endpoints': {'source-endpoint': [ - compose_osu_tunnel_endpoint(src_node_id, src_tp_id, src_ttp_channel_name), - ]}, - 'destination-endpoints': {'destination-endpoint': [ - compose_osu_tunnel_endpoint(dst_node_id, dst_tp_id, dst_ttp_channel_name), - ]}, - 'restoration': compose_osu_tunnel_restoration(), - 'protection': compose_osu_tunnel_protection(), - }]} diff --git a/src/device/service/drivers/ietf_actn/Tools.py b/src/device/service/drivers/ietf_actn/Tools.py index 1b89315b1..ef60446e7 100644 --- a/src/device/service/drivers/ietf_actn/Tools.py +++ b/src/device/service/drivers/ietf_actn/Tools.py @@ -140,40 +140,7 @@ def create_resource( url = '{:s}/restconf/data/tapi-common:context/tapi-connectivity:connectivity-context'.format(base_url) headers = {'content-type': 'application/json'} - data = { - 'tapi-connectivity:connectivity-service': [ - { - 'uuid': uuid, - 'connectivity-constraint': { - 'requested-capacity': { - 'total-size': { - 'value': capacity_value, - 'unit': capacity_unit - } - }, - 'connectivity-direction': direction - }, - 'end-point': [ - { - 'service-interface-point': { - 'service-interface-point-uuid': input_sip - }, - 'layer-protocol-name': layer_protocol_name, - 'layer-protocol-qualifier': layer_protocol_qualifier, - 'local-id': input_sip - }, - { - 'service-interface-point': { - 'service-interface-point-uuid': output_sip - }, - 'layer-protocol-name': layer_protocol_name, - 'layer-protocol-qualifier': layer_protocol_qualifier, - 'local-id': output_sip - } - ] - } - ] - } + data = compose_... results = [] try: LOGGER.info('Connectivity service {:s}: {:s}'.format(str(uuid), str(data))) @@ -194,9 +161,9 @@ def delete_resource( base_url : str, resource_key : str, resource_value : Dict, auth : Optional[HTTPBasicAuth] = None, timeout : Optional[int] = None ): - uuid = find_key(resource, 'uuid') + uuid = find_key(resource_value, 'uuid') - url = '{:s}/restconf/data/tapi-common:context/tapi-connectivity:connectivity-context/connectivity-service={:s}' + url = '{:s}/tapi-common:context/tapi-connectivity:connectivity-context/connectivity-service={:s}' url = url.format(base_url, uuid) results = [] try: diff --git a/src/device/service/drivers/ietf_actn/examples/eth_svc_1.json b/src/device/service/drivers/ietf_actn/examples/eth_svc_1.json new file mode 100644 index 000000000..840092429 --- /dev/null +++ b/src/device/service/drivers/ietf_actn/examples/eth_svc_1.json @@ -0,0 +1,91 @@ +{ + "ietf-eth-tran-service:etht-svc": { + "etht-svc-instances": [ + { + "etht-svc-name": "etht_service_1", + "etht-svc-title": "ETHT_SVC_1", + "etht-svc-type": "op-mp2mp-svc?", + "source-endpoints": { + "source-endpoint": [ + { + "node-id": "10.0.10.1", + "tp-id": "200", + "protection-role": "work?", + "layer-specific": { + "access-type": "port" + }, + "is-extendable": false, + "is-terminal": true, + "static-route-list": [ + { + "destination": "128.32.10.5", + "destination-mask": 24, + "next-hop": "128.32.33.5" + }, + { + "destination": "128.32.20.5", + "destination-mask": 24, + "next-hop": "128.32.33.5" + } + ], + "outer-tag": { + "tag-type": "ietf-eth-tran-types:classify-c-vlan", + "vlan-value": 21 + }, + "service-classification-type": "ietf-eth-tran-type:vlan-classification", + "ingress-egress-bandwidth-profile" : { + "bandwidth-profile-type": "ietf-eth-tran-types:mef-10-bwp", + "CIR": 10000000, + "EIR": 10000000 + } + } + ] + }, + "destination-endpoints": { + "destination-endpoint": [ + { + "node-id": "10.0.30.1", + "tp-id": "200", + "protection-role": "work?", + "layer-specific": { + "access-type": "port" + }, + "is-extendable": false, + "is-terminal": true, + "static-route-list": [ + { + "destination": "172.1.101.22", + "destination-mask": 24, + "next-hop": "172.10.33.5" + } + ], + "outer-tag": { + "tag-type": "ietf-eth-tran-types:classify-c-vlan", + "vlan-value": 101 + }, + "service-classification-type": "ietf-eth-tran-type:vlan-classification", + "ingress-egress-bandwidth-profile" : { + "bandwidth-profile-type": "ietf-eth-tran-types:mef-10-bwp", + "CIR": 10000000, + "EIR": 10000000 + } + } + ] + }, + "svc-tunnel": [ + { + "tunnel-name": "osu_tunnel_1" + } + ], + "optimizations": { + "optimization-metric": [ + { + "metric-role": "work?", + "metric-type": "ietf-te-types:path-metric-te" + } + ] + } + } + ] + } +} \ No newline at end of file diff --git a/src/device/service/drivers/ietf_actn/examples/eth_svc_2.json b/src/device/service/drivers/ietf_actn/examples/eth_svc_2.json new file mode 100644 index 000000000..74ca61d03 --- /dev/null +++ b/src/device/service/drivers/ietf_actn/examples/eth_svc_2.json @@ -0,0 +1,91 @@ +{ + "ietf-eth-tran-service:etht-svc": { + "etht-svc-instances": [ + { + "etht-svc-name": "etht_service_2", + "etht-svc-title": "ETHT_SVC_2", + "etht-svc-type": "op-p2mp-svc?", + "source-endpoints": { + "source-endpoint": [ + { + "node-id": "10.0.10.1", + "tp-id": "200", + "protection-role": "work?", + "layer-specific": { + "access-type": "port" + }, + "is-extendable": false, + "is-terminal": true, + "static-route-list": [ + { + "destination": "128.32.10.5", + "destination-mask": 24, + "next-hop": "128.32.33.5" + }, + { + "destination": "128.32.20.5", + "destination-mask": 24, + "next-hop": "128.32.33.5" + } + ], + "outer-tag": { + "tag-type": "ietf-eth-tran-types:classify-c-vlan", + "vlan-value": 31 + }, + "service-classification-type": "ietf-eth-tran-type:vlan-classification", + "ingress-egress-bandwidth-profile" : { + "bandwidth-profile-type": "ietf-eth-tran-types:mef-10-bwp", + "CIR": 10000000, + "EIR": 10000000 + } + } + ] + }, + "destination-endpoints": { + "destination-endpoint": [ + { + "node-id": "10.0.30.1", + "tp-id": "200", + "protection-role": "work?", + "layer-specific": { + "access-type": "port" + }, + "is-extendable": false, + "is-terminal": true, + "static-route-list": [ + { + "destination": "172.1.101.22", + "destination-mask": 24, + "next-hop": "172.10.33.5" + } + ], + "outer-tag": { + "tag-type": "ietf-eth-tran-types:classify-c-vlan", + "vlan-value": 201 + }, + "service-classification-type": "ietf-eth-tran-type:vlan-classification", + "ingress-egress-bandwidth-profile" : { + "bandwidth-profile-type": "ietf-eth-tran-types:mef-10-bwp", + "CIR": 10000000, + "EIR": 10000000 + } + } + ] + }, + "svc-tunnel": [ + { + "tunnel-name": "osu_tunnel_2" + } + ], + "optimizations": { + "optimization-metric": [ + { + "metric-role": "work?", + "metric-type": "ietf-te-types:path-metric-te" + } + ] + } + } + ] + } +} \ No newline at end of file diff --git a/src/device/service/drivers/ietf_actn/examples/osu_tunnel_1.json b/src/device/service/drivers/ietf_actn/examples/osu_tunnel_1.json new file mode 100644 index 000000000..728450b92 --- /dev/null +++ b/src/device/service/drivers/ietf_actn/examples/osu_tunnel_1.json @@ -0,0 +1,44 @@ +{ + "ietf-te:tunnel": [ + { + "name": "osu_tunnel_1", + "title": "OSU_TUNNEL_1", + "admin-state": "ietf-te-types:tunnel-admin-state-up", + "delay": 20, + "te-bandwidth": { + "layer": "odu", + "odu-type": "osuflex", + "number": 1 + }, + "bidirectional": true, + "destination-endpoints": { + "destination-endpoint": [ + { + "node-id": "10.0.30.1", + "tp-id": "200", + "ttp-channel-name": "och:1-odu2:1-oduflex:3-osuflex:1?", + "protection-role": "work" + } + ] + }, + "source-endpoints": { + "source-endpoint": [ + { + "node-id": "10.0.10.1", + "tp-id": "200", + "ttp-channel-name": "och:1-odu2:1-oduflex:1-osuflex:2?", + "protection-role": "work" + } + ] + }, + "restoration": { + "restoration-type": "ietf-te-types:lsp-restoration-not-applicable", + "restoration-lock": false + }, + "protection": { + "protection-type": "ietf-te-types:lsp-protection-unprotected", + "protection-reversion-disable": true + } + } + ] +} \ No newline at end of file diff --git a/src/device/service/drivers/ietf_actn/examples/osu_tunnel_2.json b/src/device/service/drivers/ietf_actn/examples/osu_tunnel_2.json new file mode 100644 index 000000000..4e6966b8f --- /dev/null +++ b/src/device/service/drivers/ietf_actn/examples/osu_tunnel_2.json @@ -0,0 +1,44 @@ +{ + "ietf-te:tunnel": [ + { + "name": "osu_tunnel_2", + "title": "OSU_TUNNEL_2", + "admin-state": "ietf-te-types:tunnel-admin-state-up", + "delay": 20, + "te-bandwidth": { + "layer": "odu", + "odu-type": "osuflex", + "number": 1 + }, + "bidirectional": true, + "destination-endpoints": { + "destination-endpoint": [ + { + "node-id": "10.0.30.1", + "tp-id": "200", + "ttp-channel-name": "och:1-odu2:1-oduflex:3-osuflex:1?", + "protection-role": "work" + } + ] + }, + "source-endpoints": { + "source-endpoint": [ + { + "node-id": "10.0.10.1", + "tp-id": "200", + "ttp-channel-name": "och:1-odu2:1-oduflex:1-osuflex:2?", + "protection-role": "work" + } + ] + }, + "restoration": { + "restoration-type": "ietf-te-types:lsp-restoration-not-applicable", + "restoration-lock": false + }, + "protection": { + "protection-type": "ietf-te-types:lsp-protection-unprotected", + "protection-reversion-disable": true + } + } + ] +} \ No newline at end of file diff --git a/src/device/service/drivers/ietf_actn/handlers/EthService.py b/src/device/service/drivers/ietf_actn/handlers/EthService.py new file mode 100644 index 000000000..0d923b16c --- /dev/null +++ b/src/device/service/drivers/ietf_actn/handlers/EthService.py @@ -0,0 +1,106 @@ +# 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 enum +from typing import Dict, List, Tuple + +OSU_TUNNEL_URL = '/restconf/data/ietf-te:tunnel' + +class BandwidthProfileTypeEnum(enum.Enum): + MEF_10_BWP = 'ietf-eth-tran-types:mef-10-bwp' + +class EndpointLayerSpecificAccessTypeEnum(enum.Enum): + PORT = 'port' + +class EndpointProtectionRoleEnum(enum.Enum): + WORK = 'work' + +class OptimizationMetricRole(enum.Enum): + WORK = 'work' + +class OptimizationMetricType(enum.Enum): + PATH_METRIC_TE = 'ietf-te-types:path-metric-te' + +class OuterTagTypeEnum(enum.Enum): + CLASSIFY_C_VLAN = 'ietf-eth-tran-types:classify-c-vlan' + +class ServiceClassificationTypeEnum(enum.Enum): + VLAN_CLASSIFICATION = 'ietf-eth-tran-type:vlan-classification' + +class ServiceTypeEnum(enum.Enum): + MP2MP = 'op-mp2mp-svc' + P2MP = 'op-p2mp-svc' + +def compose_outer_tag(tag_type : OuterTagTypeEnum, vlan_value : int) -> Dict: + return {'tag-type': tag_type.value, 'vlan-value': vlan_value} + +def compose_ingress_egress_bandwidth_profile() -> Dict: + return { + 'bandwidth-profile-type': BandwidthProfileTypeEnum.MEF_10_BWP.value, + 'CIR': 10_000_000, + 'EIR': 10_000_000, + } + +def compose_layer_specific_access_type() -> Dict: + return {'access-type': EndpointLayerSpecificAccessTypeEnum.PORT.value} + +def compose_static_route(prefix : str, mask : int, next_hop : str) -> Dict: + return {'destination': prefix, 'destination-mask': mask, 'next-hop': next_hop} + +def compose_static_route_list(static_routes : List[Tuple[str, int, str]]) -> List[Dict]: + return [ + compose_static_route(prefix, mask, next_hop) + for prefix, mask, next_hop in static_routes + ] + +def compose_etht_service_endpoint( + node_id : str, tp_id : str, vlan_value : int, static_routes : List[Tuple[str, int, str]] = list() +) -> Dict: + return { + 'node-id' : node_id, + 'tp-id' : tp_id, + 'protection-role' : EndpointProtectionRoleEnum.WORK.value, + 'layer-specific' : compose_layer_specific_access_type, + 'is-extendable' : False, + 'is-terminal' : True, + 'static-route-list' : compose_static_route_list(static_routes), + 'outer-tag' : compose_outer_tag(OuterTagTypeEnum.CLASSIFY_C_VLAN, vlan_value), + 'service-classification-type' : ServiceClassificationTypeEnum.VLAN_CLASSIFICATION.value, + 'ingress-egress-bandwidth-profile': compose_ingress_egress_bandwidth_profile(), + } + +def compose_optimizations() -> Dict: + return {'optimization-metric': [{ + 'metric-role': OptimizationMetricRole.WORK.value, + 'metric-type': OptimizationMetricType.PATH_METRIC_TE.value, + }]} + +def compose_etht_service( + name : str, service_type : ServiceTypeEnum, osu_tunnel_name : str, + src_node_id : str, src_tp_id : str, src_vlan_tag : int, dst_node_id : str, dst_tp_id : str, dst_vlan_tag : int, + src_static_routes : List[Tuple[str, int, str]] = list(), dst_static_routes : List[Tuple[str, int, str]] = list() +) -> Dict: + return {'ietf-eth-tran-service:etht-svc': {'etht-svc-instances': [{ + 'etht-svc-name' : name.lower(), + 'etht-svc-title': name.upper(), + 'etht-svc-type' : service_type.value, + 'source-endpoints': {'source-endpoint': [ + compose_etht_service_endpoint(src_node_id, src_tp_id, src_vlan_tag, src_static_routes), + ]}, + 'destination-endpoints': {'destination-endpoint': [ + compose_etht_service_endpoint(dst_node_id, dst_tp_id, dst_vlan_tag, dst_static_routes), + ]}, + 'svc-tunnel': [{'tunnel-name': osu_tunnel_name}], + 'optimizations': compose_optimizations(), + }]}} diff --git a/src/device/service/drivers/ietf_actn/handlers/OsuTunnel.py b/src/device/service/drivers/ietf_actn/handlers/OsuTunnel.py new file mode 100644 index 000000000..a15f73eab --- /dev/null +++ b/src/device/service/drivers/ietf_actn/handlers/OsuTunnel.py @@ -0,0 +1,236 @@ +# 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 enum, json, logging, operator, requests +from requests.auth import HTTPBasicAuth +from typing import Dict, List, Optional +from device.service.driver_api._Driver import RESOURCE_ENDPOINTS, RESOURCE_SERVICES +from .Tools import HTTP_OK_CODES + +LOGGER = logging.getLogger(__name__) + +BASE_URL_OSU_TUNNEL = '{:s}/ietf-te:tunnel' + +class EndpointProtectionRoleEnum(enum.Enum): + WORK = 'work' + +class LspProtectionTypeEnum(enum.Enum): + UNPROTECTED = 'ietf-te-types:lsp-protection-unprotected' + +class LspRestorationTypeEnum(enum.Enum): + NOT_APPLICABLE = 'ietf-te-types:lsp-restoration-not-applicable' + +class TunnelAdminStateEnum(enum.Enum): + UP = 'ietf-te-types:tunnel-admin-state-up' + +class OduTypeEnum(enum.Enum): + OSUFLEX = 'osuflex' + +def compose_osu_tunnel_endpoint( + node_id : str, tp_id : str, ttp_channel_name : str, + protection_role : EndpointProtectionRoleEnum = EndpointProtectionRoleEnum.WORK +) -> Dict: + return { + 'node-id': node_id, 'tp-id': tp_id, 'ttp-channel-name': ttp_channel_name, + 'protection-role': protection_role.value + } + +def compose_osu_tunnel_te_bandwidth_odu(odu_type : OduTypeEnum, number : int) -> Dict: + return {'layer': 'odu', 'odu-type': odu_type.value, 'number': number} + +def compose_osu_tunnel_protection( + type_ : LspProtectionTypeEnum = LspProtectionTypeEnum.UNPROTECTED, reversion_disable : bool = True +) -> Dict: + return {'protection-type': type_.value, 'protection-reversion-disable': reversion_disable} + +def compose_osu_tunnel_restoration( + type_ : LspRestorationTypeEnum = LspRestorationTypeEnum.NOT_APPLICABLE, restoration_lock : bool = False +) -> Dict: + return {'restoration-type': type_.value, 'restoration-lock': restoration_lock} + +def compose_osu_tunnel( + name : str, + src_node_id : str, src_tp_id : str, src_ttp_channel_name : str, + dst_node_id : str, dst_tp_id : str, dst_ttp_channel_name : str, + odu_type : OduTypeEnum, osuflex_number : int, + delay : int, bidirectional : bool = True, + admin_state : TunnelAdminStateEnum = TunnelAdminStateEnum.UP +) -> Dict: + return {'ietf-te:tunnel': [{ + 'name': name.lower(), + 'title': name.upper(), + 'admin-state': admin_state.value, + 'delay': delay, + 'te-bandwidth': compose_osu_tunnel_te_bandwidth_odu(odu_type, osuflex_number), + 'bidirectional': bidirectional, + 'source-endpoints': {'source-endpoint': [ + compose_osu_tunnel_endpoint(src_node_id, src_tp_id, src_ttp_channel_name), + ]}, + 'destination-endpoints': {'destination-endpoint': [ + compose_osu_tunnel_endpoint(dst_node_id, dst_tp_id, dst_ttp_channel_name), + ]}, + 'restoration': compose_osu_tunnel_restoration(), + '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 + + def get(self, resource_key : str) -> None: + url = '{:s}/restconf/data/tapi-common:context'.format(base_url) + result = [] + try: + response = requests.get(url, timeout=timeout, verify=False, auth=auth) + except requests.exceptions.Timeout: + LOGGER.exception('Timeout connecting {:s}'.format(url)) + return result + except Exception as e: # pylint: disable=broad-except + LOGGER.exception('Exception retrieving {:s}'.format(resource_key)) + result.append((resource_key, e)) + return result + + try: + context = json.loads(response.content) + except Exception as e: # pylint: disable=broad-except + LOGGER.warning('Unable to decode reply: {:s}'.format(str(response.content))) + 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 'tapi-common:context' in context: + context = context['tapi-common:context'] + elif 'context' in context: + context = context['context'] + + if 'tapi-connectivity:connectivity-context' in context: + context = context['tapi-connectivity:connectivity-context'] + elif 'connectivity-context' in context: + context = context['connectivity-context'] + + for conn_svc in context['connectivity-service']: + service_uuid = conn_svc['uuid'] + constraints = conn_svc.get('connectivity-constraint', {}) + total_req_cap = constraints.get('requested-capacity', {}).get('total-size', {}) + + service_url = '/services/service[{:s}]'.format(service_uuid) + service_data = { + 'uuid': service_uuid, + 'direction': constraints.get('connectivity-direction', 'UNIDIRECTIONAL'), + 'capacity_unit': total_req_cap.get('unit', ''), + 'capacity_value': total_req_cap.get('value', ''), + } + + for i,endpoint in enumerate(conn_svc.get('end-point', [])): + layer_protocol_name = endpoint.get('layer-protocol-name') + if layer_protocol_name is not None: + service_data['layer_protocol_name'] = layer_protocol_name + + layer_protocol_qualifier = endpoint.get('layer-protocol-qualifier') + if layer_protocol_qualifier is not None: + service_data['layer_protocol_qualifier'] = layer_protocol_qualifier + + sip = endpoint['service-interface-point']['service-interface-point-uuid'] + service_data['input_sip' if i == 0 else 'output_sip'] = sip + + result.append((service_url, service_data)) + + 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 ) + + 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 diff --git a/src/device/service/drivers/ietf_actn/handlers/Tools.py b/src/device/service/drivers/ietf_actn/handlers/Tools.py new file mode 100644 index 000000000..c14c65afa --- /dev/null +++ b/src/device/service/drivers/ietf_actn/handlers/Tools.py @@ -0,0 +1,20 @@ +# 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 +} diff --git a/src/device/service/drivers/ietf_actn/ComposerEthService.py b/src/device/service/drivers/ietf_actn/handlers/__init__.py similarity index 99% rename from src/device/service/drivers/ietf_actn/ComposerEthService.py rename to src/device/service/drivers/ietf_actn/handlers/__init__.py index 1549d9811..38d04994f 100644 --- a/src/device/service/drivers/ietf_actn/ComposerEthService.py +++ b/src/device/service/drivers/ietf_actn/handlers/__init__.py @@ -11,4 +11,3 @@ # 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. - -- GitLab From fcd3d2f766ba9868fd4d3479a451787e166a7fec Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Tue, 16 Jan 2024 16:41:06 +0000 Subject: [PATCH 06/18] Device - IETF ACTN Driver: - Implemented EthtServiceHandler (partial, missing get) - Implemented OsuTunnelHandler (partial, missing get) - Implemented common RestApiClient --- .../drivers/ietf_actn/IetfActnDriver.py | 3 - .../drivers/ietf_actn/handlers/EthService.py | 46 +++++- .../drivers/ietf_actn/handlers/OsuTunnel.py | 118 ++++----------- .../ietf_actn/handlers/RestApiClient.py | 143 ++++++++++++++++++ .../drivers/ietf_actn/handlers/Tools.py | 20 --- 5 files changed, 217 insertions(+), 113 deletions(-) create mode 100644 src/device/service/drivers/ietf_actn/handlers/RestApiClient.py delete mode 100644 src/device/service/drivers/ietf_actn/handlers/Tools.py diff --git a/src/device/service/drivers/ietf_actn/IetfActnDriver.py b/src/device/service/drivers/ietf_actn/IetfActnDriver.py index c31bd85b9..6d0aada4e 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 0d923b16c..e8fe9817b 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 a15f73eab..d6332a8d7 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 000000000..8660f35ce --- /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 c14c65afa..000000000 --- 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 -} -- GitLab From 7b3ff4a85242c710a6239fe3d9dbe79e2cd020fd Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Thu, 18 Jan 2024 09:39:28 +0000 Subject: [PATCH 07/18] Device - IETF ACTN Driver: - Completed implementation of EthtServiceHandler and OsuTunnelHandler - Updated implementation of RestApiClient --- .../{EthService.py => EthtServiceHandler.py} | 75 +++++++++-- .../{OsuTunnel.py => OsuTunnelHandler.py} | 114 +++++++---------- .../ietf_actn/handlers/RestApiClient.py | 117 +++++++----------- 3 files changed, 152 insertions(+), 154 deletions(-) rename src/device/service/drivers/ietf_actn/handlers/{EthService.py => EthtServiceHandler.py} (63%) rename src/device/service/drivers/ietf_actn/handlers/{OsuTunnel.py => OsuTunnelHandler.py} (59%) diff --git a/src/device/service/drivers/ietf_actn/handlers/EthService.py b/src/device/service/drivers/ietf_actn/handlers/EthtServiceHandler.py similarity index 63% rename from src/device/service/drivers/ietf_actn/handlers/EthService.py rename to src/device/service/drivers/ietf_actn/handlers/EthtServiceHandler.py index e8fe9817b..66734199a 100644 --- a/src/device/service/drivers/ietf_actn/handlers/EthService.py +++ b/src/device/service/drivers/ietf_actn/handlers/EthtServiceHandler.py @@ -12,9 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -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 +import enum, logging +from typing import Dict, List, Optional, Tuple, Union from .RestApiClient import RestApiClient LOGGER = logging.getLogger(__name__) @@ -73,7 +72,7 @@ def compose_etht_service_endpoint( 'node-id' : node_id, 'tp-id' : tp_id, 'protection-role' : EndpointProtectionRoleEnum.WORK.value, - 'layer-specific' : compose_layer_specific_access_type, + 'layer-specific' : compose_layer_specific_access_type(), 'is-extendable' : False, 'is-terminal' : True, 'static-route-list' : compose_static_route_list(static_routes), @@ -94,7 +93,7 @@ def compose_etht_service( src_static_routes : List[Tuple[str, int, str]] = list(), dst_static_routes : List[Tuple[str, int, str]] = list() ) -> Dict: return {'ietf-eth-tran-service:etht-svc': {'etht-svc-instances': [{ - 'etht-svc-name' : name.lower(), + 'etht-svc-name' : name, 'etht-svc-title': name.upper(), 'etht-svc-type' : service_type.value, 'source-endpoints': {'source-endpoint': [ @@ -113,10 +112,66 @@ class EthtServiceHandler: 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]]: + def get(self, etht_service_name : Optional[str] = None) -> Union[Dict, List]: + filters = [] if etht_service_name is None else [('etht-svc-name', etht_service_name)] + data = self._rest_api_client.get(self._object_name, self._subpath_url, filters) + + if not isinstance(data, dict): return ValueError('data should be a dict') + if 'ietf-eth-tran-service:etht-svc' not in data: + return ValueError('data does not contain key "ietf-eth-tran-service:etht-svc"') + data = data['ietf-eth-tran-service:etht-svc'] + if 'etht-svc-instances' not in data: + return ValueError('data["ietf-eth-tran-service:etht-svc"] does not contain key "etht-svc-instances"') + data = data['etht-svc-instances'] + if not isinstance(data, list): + return ValueError('data["ietf-eth-tran-service:etht-svc"]["etht-svc-instances"] should be a list') + + etht_services : List[Dict] = list() + for item in data: + src_endpoints = item['source-endpoints']['source-endpoint'] + if len(src_endpoints) != 1: + MSG = 'EthtService({:s}) has zero/multiple source endpoints' + raise Exception(MSG.format(str(item))) + src_endpoint = src_endpoints[0] + + dst_endpoints = item['destination-endpoints']['destination-endpoint'] + if len(dst_endpoints) != 1: + MSG = 'EthtService({:s}) has zero/multiple destination endpoints' + raise Exception(MSG.format(str(item))) + dst_endpoint = dst_endpoints[0] + + svc_tunnels = item['svc-tunnel'] + if len(svc_tunnels) != 1: + MSG = 'EthtService({:s}) has zero/multiple service tunnels' + raise Exception(MSG.format(str(item))) + svc_tunnel = svc_tunnels[0] + + etht_service = { + 'name' : item['etht-svc-name'], + 'service_type' : item['etht-svc-type'], + 'osu_tunnel_name' : svc_tunnel['tunnel-name'], + + 'src_node_id' : src_endpoint['node-id'], + 'src_tp_id' : src_endpoint['tp-id'], + 'src_vlan_tag' : src_endpoint['outer-tag']['vlan-value'], + 'src_static_routes': [ + (static_route['destination'], static_route['destination-mask'], static_route['next-hop']) + for static_route in src_endpoint.get('static-route-list', list()) + ], + + 'dst_node_id' : dst_endpoint['node-id'], + 'dst_tp_id' : dst_endpoint['tp-id'], + 'dst_vlan_tag' : src_endpoint['outer-tag']['vlan-value'], + 'dst_static_routes': [ + (static_route['destination'], static_route['destination-mask'], static_route['next-hop']) + for static_route in src_endpoint.get('static-route-list', list()) + ], + } + etht_services.append(etht_service) + + return etht_services + + def update(self, parameters : Dict) -> bool: name = parameters['name' ] service_type = parameters['service_type' ] osu_tunnel_name = parameters['osu_tunnel_name'] @@ -141,6 +196,6 @@ class EthtServiceHandler: return self._rest_api_client.update(self._object_name, self._subpath_url, data) - def delete(self, etht_service_name : str) -> List[Union[bool, Exception]]: + def delete(self, etht_service_name : str) -> bool: 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/OsuTunnelHandler.py similarity index 59% rename from src/device/service/drivers/ietf_actn/handlers/OsuTunnel.py rename to src/device/service/drivers/ietf_actn/handlers/OsuTunnelHandler.py index d6332a8d7..57395de4f 100644 --- a/src/device/service/drivers/ietf_actn/handlers/OsuTunnel.py +++ b/src/device/service/drivers/ietf_actn/handlers/OsuTunnelHandler.py @@ -12,9 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -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 +import enum, logging +from typing import Dict, List, Optional, Union from .RestApiClient import RestApiClient LOGGER = logging.getLogger(__name__) @@ -65,7 +64,7 @@ def compose_osu_tunnel( admin_state : TunnelAdminStateEnum = TunnelAdminStateEnum.UP ) -> Dict: return {'ietf-te:tunnel': [{ - 'name': name.lower(), + 'name': name, 'title': name.upper(), 'admin-state': admin_state.value, 'delay': delay, @@ -87,70 +86,47 @@ class OsuTunnelHandler: 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) - result = [] - try: - response = requests.get(url, timeout=timeout, verify=False, auth=auth) - except requests.exceptions.Timeout: - LOGGER.exception('Timeout connecting {:s}'.format(url)) - return result - except Exception as e: # pylint: disable=broad-except - LOGGER.exception('Exception retrieving {:s}'.format(resource_key)) - result.append((resource_key, e)) - return result - - try: - context = json.loads(response.content) - except Exception as e: # pylint: disable=broad-except - LOGGER.warning('Unable to decode reply: {:s}'.format(str(response.content))) - result.append((resource_key, e)) - return result - - if resource_key == RESOURCE_SERVICES: - if 'tapi-common:context' in context: - context = context['tapi-common:context'] - elif 'context' in context: - context = context['context'] - - if 'tapi-connectivity:connectivity-context' in context: - context = context['tapi-connectivity:connectivity-context'] - elif 'connectivity-context' in context: - context = context['connectivity-context'] - - for conn_svc in context['connectivity-service']: - service_uuid = conn_svc['uuid'] - constraints = conn_svc.get('connectivity-constraint', {}) - total_req_cap = constraints.get('requested-capacity', {}).get('total-size', {}) - - service_url = '/services/service[{:s}]'.format(service_uuid) - service_data = { - 'uuid': service_uuid, - 'direction': constraints.get('connectivity-direction', 'UNIDIRECTIONAL'), - 'capacity_unit': total_req_cap.get('unit', ''), - 'capacity_value': total_req_cap.get('value', ''), - } - - for i,endpoint in enumerate(conn_svc.get('end-point', [])): - layer_protocol_name = endpoint.get('layer-protocol-name') - if layer_protocol_name is not None: - service_data['layer_protocol_name'] = layer_protocol_name - - layer_protocol_qualifier = endpoint.get('layer-protocol-qualifier') - if layer_protocol_qualifier is not None: - service_data['layer_protocol_qualifier'] = layer_protocol_qualifier - - sip = endpoint['service-interface-point']['service-interface-point-uuid'] - service_data['input_sip' if i == 0 else 'output_sip'] = sip - - result.append((service_url, service_data)) - - return result - - def update(self, parameters : Dict) -> List[Union[bool, Exception]]: + def get(self, osu_tunnel_name : Optional[str] = None) -> Union[Dict, List]: + filters = [] if osu_tunnel_name is None else [('name', osu_tunnel_name)] + data = self._rest_api_client.get(self._object_name, self._subpath_url, filters) + + if not isinstance(data, dict): return ValueError('data should be a dict') + if 'ietf-te:tunnel' not in data: return ValueError('data does not contain key "ietf-te:tunnel"') + data = data['ietf-te:tunnel'] + if not isinstance(data, list): return ValueError('data[ietf-te:tunnel] should be a list') + + osu_tunnels : List[Dict] = list() + for item in data: + src_endpoints = item['source-endpoints']['source-endpoint'] + if len(src_endpoints) != 1: + MSG = 'OsuTunnel({:s}) has zero/multiple source endpoints' + raise Exception(MSG.format(str(item))) + src_endpoint = src_endpoints[0] + + dst_endpoints = item['destination-endpoints']['destination-endpoint'] + if len(dst_endpoints) != 1: + MSG = 'OsuTunnel({:s}) has zero/multiple destination endpoints' + raise Exception(MSG.format(str(item))) + dst_endpoint = dst_endpoints[0] + + osu_tunnel = { + 'name' : item['name'], + 'src_node_id' : src_endpoint['node-id'], + 'src_tp_id' : src_endpoint['node-id'], + 'src_ttp_channel_name': src_endpoint['ttp-channel-name'], + 'dst_node_id' : dst_endpoint['node-id'], + 'dst_tp_id' : dst_endpoint['node-id'], + 'dst_ttp_channel_name': src_endpoint['ttp-channel-name'], + 'odu_type' : item['te-bandwidth']['odu-type'], + 'osuflex_number' : item['te-bandwidth']['number'], + 'delay' : item['delay'], + 'bidirectional' : item['bidirectional'], + } + osu_tunnels.append(osu_tunnel) + + return osu_tunnels + + def update(self, parameters : Dict) -> bool: name = parameters['name' ] src_node_id = parameters['src_node_id' ] @@ -175,6 +151,6 @@ class OsuTunnelHandler: return self._rest_api_client.update(self._object_name, self._subpath_url, data) - def delete(self, osu_tunnel_name : str) -> List[Union[bool, Exception]]: + def delete(self, osu_tunnel_name : str) -> bool: 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 index 8660f35ce..15321f7a0 100644 --- a/src/device/service/drivers/ietf_actn/handlers/RestApiClient.py +++ b/src/device/service/drivers/ietf_actn/handlers/RestApiClient.py @@ -43,101 +43,68 @@ class RestApiClient: 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]]: + ) -> Union[Dict, List]: 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 + 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))) + + if response.status_code in HTTP_OK_CODES: return json.loads(response.content) + + 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) + return Exception(msg) def update( self, object_name : str, url : str, data : Dict, headers : Dict[str, Any] = dict() - ) -> List[Union[bool, Exception]]: + ) -> bool: 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 + 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))) + if response.status_code in HTTP_OK_CODES: return True + + 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))) + return False def delete( self, object_name : str, url : str, filters : List[Tuple[str, str]] - ) -> List[Union[bool, Exception]]: + ) -> bool: 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 + 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))) + + if response.status_code in HTTP_OK_CODES: return True + + 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))) + return False -- GitLab From b43796928c8198b8542e69b2dd06b0368c5d8799 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Thu, 18 Jan 2024 16:00:25 +0000 Subject: [PATCH 08/18] Device - IETF ACTN Driver: - Minor code improvements in EthtServiceHandler, OsuTunnelHandler, and RestApiClient --- .../ietf_actn/handlers/EthtServiceHandler.py | 25 +++++++++++--- .../ietf_actn/handlers/OsuTunnelHandler.py | 27 +++++++++++---- .../ietf_actn/handlers/RestApiClient.py | 34 ++++++++++++------- 3 files changed, 62 insertions(+), 24 deletions(-) diff --git a/src/device/service/drivers/ietf_actn/handlers/EthtServiceHandler.py b/src/device/service/drivers/ietf_actn/handlers/EthtServiceHandler.py index 66734199a..ff0dadbbc 100644 --- a/src/device/service/drivers/ietf_actn/handlers/EthtServiceHandler.py +++ b/src/device/service/drivers/ietf_actn/handlers/EthtServiceHandler.py @@ -14,7 +14,7 @@ import enum, logging from typing import Dict, List, Optional, Tuple, Union -from .RestApiClient import RestApiClient +from .RestApiClient import HTTP_STATUS_CREATED, HTTP_STATUS_NO_CONTENT, HTTP_STATUS_OK, RestApiClient LOGGER = logging.getLogger(__name__) @@ -110,11 +110,26 @@ 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' + self._subpath_url = '/ietf-eth-tran-service:etht-svc' + + def _rest_api_get(self, filters : List[Tuple[str, str]]) -> Union[Dict, List]: + return self._rest_api_client.get( + self._object_name, self._subpath_url, filters, expected_http_status={HTTP_STATUS_OK} + ) + + def _rest_api_update(self, data : Dict) -> bool: + return self._rest_api_client.update( + self._object_name, self._subpath_url, data, expected_http_status={HTTP_STATUS_CREATED} + ) + + def _rest_api_delete(self, filters : List[Tuple[str, str]]) -> bool: + return self._rest_api_client.delete( + self._object_name, self._subpath_url, filters, expected_http_status={HTTP_STATUS_NO_CONTENT} + ) def get(self, etht_service_name : Optional[str] = None) -> Union[Dict, List]: filters = [] if etht_service_name is None else [('etht-svc-name', etht_service_name)] - data = self._rest_api_client.get(self._object_name, self._subpath_url, filters) + data = self._rest_api_get(filters) if not isinstance(data, dict): return ValueError('data should be a dict') if 'ietf-eth-tran-service:etht-svc' not in data: @@ -194,8 +209,8 @@ class EthtServiceHandler: 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) + return self._rest_api_update(data) def delete(self, etht_service_name : str) -> bool: filters = [('etht-svc-name', etht_service_name)] - return self._rest_api_client.delete(self._object_name, self._subpath_url, filters) + return self._rest_api_delete(filters) diff --git a/src/device/service/drivers/ietf_actn/handlers/OsuTunnelHandler.py b/src/device/service/drivers/ietf_actn/handlers/OsuTunnelHandler.py index 57395de4f..2cb6f46f0 100644 --- a/src/device/service/drivers/ietf_actn/handlers/OsuTunnelHandler.py +++ b/src/device/service/drivers/ietf_actn/handlers/OsuTunnelHandler.py @@ -13,8 +13,8 @@ # limitations under the License. import enum, logging -from typing import Dict, List, Optional, Union -from .RestApiClient import RestApiClient +from typing import Dict, List, Optional, Tuple, Union +from .RestApiClient import HTTP_STATUS_CREATED, HTTP_STATUS_NO_CONTENT, HTTP_STATUS_OK, RestApiClient LOGGER = logging.getLogger(__name__) @@ -84,11 +84,26 @@ 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' + self._subpath_url = '/ietf-te:te/tunnels' + + def _rest_api_get(self, filters : List[Tuple[str, str]]) -> Union[Dict, List]: + return self._rest_api_client.get( + self._object_name, self._subpath_url, filters, expected_http_status={HTTP_STATUS_OK} + ) + + def _rest_api_update(self, data : Dict) -> bool: + return self._rest_api_client.update( + self._object_name, self._subpath_url, data, expected_http_status={HTTP_STATUS_CREATED} + ) + + def _rest_api_delete(self, filters : List[Tuple[str, str]]) -> bool: + return self._rest_api_client.delete( + self._object_name, self._subpath_url, filters, expected_http_status={HTTP_STATUS_NO_CONTENT} + ) def get(self, osu_tunnel_name : Optional[str] = None) -> Union[Dict, List]: filters = [] if osu_tunnel_name is None else [('name', osu_tunnel_name)] - data = self._rest_api_client.get(self._object_name, self._subpath_url, filters) + data = self._rest_api_get(filters) if not isinstance(data, dict): return ValueError('data should be a dict') if 'ietf-te:tunnel' not in data: return ValueError('data does not contain key "ietf-te:tunnel"') @@ -149,8 +164,8 @@ class OsuTunnelHandler: odu_type, osuflex_number, delay, bidirectional=bidirectional ) - return self._rest_api_client.update(self._object_name, self._subpath_url, data) + return self._rest_api_update(data) def delete(self, osu_tunnel_name : str) -> bool: filters = [('name', osu_tunnel_name)] - return self._rest_api_client.delete(self._object_name, self._subpath_url, filters) + return self._rest_api_delete(filters) diff --git a/src/device/service/drivers/ietf_actn/handlers/RestApiClient.py b/src/device/service/drivers/ietf_actn/handlers/RestApiClient.py index 15321f7a0..7154dfc48 100644 --- a/src/device/service/drivers/ietf_actn/handlers/RestApiClient.py +++ b/src/device/service/drivers/ietf_actn/handlers/RestApiClient.py @@ -14,20 +14,25 @@ import copy, json, logging, requests from requests.auth import HTTPBasicAuth -from typing import Any, Dict, List, Tuple, Union +from typing import Any, Dict, List, Set, Tuple, Union LOGGER = logging.getLogger(__name__) -DEFAULT_BASE_URL = '/restconf/data' -DEFAULT_SCHEMA = 'http' +DEFAULT_BASE_URL = '/restconf/v2/data' +DEFAULT_SCHEMA = 'https' DEFAULT_TIMEOUT = 120 DEFAULT_VERIFY = False +HTTP_STATUS_OK = 200 +HTTP_STATUS_CREATED = 201 +HTTP_STATUS_ACCEPTED = 202 +HTTP_STATUS_NO_CONTENT = 204 + HTTP_OK_CODES = { - 200, # OK - 201, # Created - 202, # Accepted - 204, # No Content + HTTP_STATUS_OK, + HTTP_STATUS_CREATED, + HTTP_STATUS_ACCEPTED, + HTTP_STATUS_NO_CONTENT, } class RestApiClient: @@ -44,7 +49,8 @@ class RestApiClient: self._verify = int(settings.get('verify', DEFAULT_VERIFY )) def get( - self, object_name : str, url : str, filters : List[Tuple[str, str]] + self, object_name : str, url : str, filters : List[Tuple[str, str]], + expected_http_status : Set[int] = {HTTP_STATUS_OK} ) -> Union[Dict, List]: str_filters = ''.join([ '[{:s}={:s}]'.format(filter_field, filter_value) @@ -59,7 +65,7 @@ class RestApiClient: ) LOGGER.info(' Response: {:s}'.format(str(response))) - if response.status_code in HTTP_OK_CODES: return json.loads(response.content) + if response.status_code in expected_http_status: return json.loads(response.content) 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)) @@ -67,7 +73,8 @@ class RestApiClient: return Exception(msg) def update( - self, object_name : str, url : str, data : Dict, headers : Dict[str, Any] = dict() + self, object_name : str, url : str, data : Dict, headers : Dict[str, Any] = dict(), + expected_http_status : Set[int] = HTTP_OK_CODES ) -> bool: headers = copy.deepcopy(headers) if 'content-type' not in {header_name.lower() for header_name in headers.keys()}: @@ -81,14 +88,15 @@ class RestApiClient: ) LOGGER.info(' Response: {:s}'.format(str(response))) - if response.status_code in HTTP_OK_CODES: return True + if response.status_code in expected_http_status: return True 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))) return False def delete( - self, object_name : str, url : str, filters : List[Tuple[str, str]] + self, object_name : str, url : str, filters : List[Tuple[str, str]], + expected_http_status : Set[int] = HTTP_OK_CODES ) -> bool: str_filters = ''.join([ '[{:s}={:s}]'.format(filter_field, filter_value) @@ -103,7 +111,7 @@ class RestApiClient: ) LOGGER.info(' Response: {:s}'.format(str(response))) - if response.status_code in HTTP_OK_CODES: return True + if response.status_code in expected_http_status: return True 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))) -- GitLab From bc3954ad6d0f29a51ab82e11ac7cf2e2a63bebab Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Fri, 19 Jan 2024 19:16:48 +0000 Subject: [PATCH 09/18] Device - IETF ACTN Driver: - Progress (WIP) on IetfActnDriver class --- .../drivers/ietf_actn/IetfActnDriver.py | 64 +++++++++---------- .../service/drivers/ietf_actn/__init__.py | 7 -- src/device/service/drivers/ietf_actn/data.txt | 14 ++++ 3 files changed, 43 insertions(+), 42 deletions(-) create mode 100644 src/device/service/drivers/ietf_actn/data.txt diff --git a/src/device/service/drivers/ietf_actn/IetfActnDriver.py b/src/device/service/drivers/ietf_actn/IetfActnDriver.py index 6d0aada4e..e432a41ce 100644 --- a/src/device/service/drivers/ietf_actn/IetfActnDriver.py +++ b/src/device/service/drivers/ietf_actn/IetfActnDriver.py @@ -16,45 +16,40 @@ import json, logging, requests, threading 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 -from device.service.drivers.ietf_actn.Tools import create_resource, delete_resource, get_resource -from . import ALL_RESOURCE_KEYS -#from .Tools import create_connectivity_service, find_key, config_getter, delete_connectivity_service +from device.service.driver_api._Driver import _Driver, RESOURCE_SERVICES +from .handlers.EthtServiceHandler import EthtServiceHandler +from .handlers.OsuTunnelHandler import OsuTunnelHandler +from .handlers.RestApiClient import RestApiClient LOGGER = logging.getLogger(__name__) +ALL_RESOURCE_KEYS = [ + RESOURCE_SERVICES, +] + DRIVER_NAME = 'ietf_actn' METRICS_POOL = MetricsPool('Device', 'Driver', labels={'driver': DRIVER_NAME}) - class IetfActnDriver(_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') - base_url = self.settings.get('base_url', DEFAULT_BASE_URL) - self.__base_url = '{:s}://{:s}:{:d}{:s}'.format(scheme, address, int(port), base_url) - - self.__timeout = int(self.settings.get('timeout', DEFAULT_TIMEOUT)) + self._rest_api_client = RestApiClient(address, port, settings=settings) + self._handler_osu_tunnel = OsuTunnelHandler(self._rest_api_client) + self._handler_etht_service = EthtServiceHandler(self._rest_api_client) def Connect(self) -> bool: - url = self.__base_url + '/tapi-common:context' with self.__lock: if self.__started.is_set(): return True try: - requests.get(url, timeout=self.__timeout, verify=False, auth=self.__auth) + self._rest_api_client.get('Check Credentials', '', []) except requests.exceptions.Timeout: - LOGGER.exception('Timeout connecting {:s}'.format(str(self.__base_url))) + LOGGER.exception('Timeout exception checking connectivity') return False except Exception: # pylint: disable=broad-except - LOGGER.exception('Exception connecting {:s}'.format(str(self.__base_url))) + LOGGER.exception('Unhandled exception checking connectivity') return False else: self.__started.set() @@ -78,25 +73,24 @@ class IetfActnDriver(_Driver): if len(resource_keys) == 0: resource_keys = ALL_RESOURCE_KEYS for i, resource_key in enumerate(resource_keys): chk_string('resource_key[#{:d}]'.format(i), resource_key, allow_empty=False) - results.extend(get_resource( - self.__base_url, resource_key, - timeout=self.__timeout, auth=self.__auth - )) + etht_service = self._handler_etht_service.get(etht_service_name) + osu_tunnel = self._handler_osu_tunnel.get(osu_tunnel_name) + service_data = {} + results.extend(('/service/service[...]', service_data)) 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 + if len(resources) == 0: return results with self.__lock: for resource_key, resource_value in resources: LOGGER.info('resource: key({:s}) => value({:s})'.format(str(resource_key), str(resource_value))) - if isinstance(value, str): value = json.loads(value) - results.extend(create_resource( - self.__base_url, resource_key, resource_value, - timeout=self.__timeout, auth=self.__auth - )) + if isinstance(resource_value, str): resource_value = json.loads(resource_value) + succeeded = self._handler_osu_tunnel.update(resource_value) + if succeeded: + succeeded = self._handler_etht_service.update(resource_value) + results.extend(succeeded) return results @metered_subclass_method(METRICS_POOL) @@ -106,11 +100,11 @@ class IetfActnDriver(_Driver): with self.__lock: for resource_key, resource_value in resources: LOGGER.info('resource: key({:s}) => value({:s})'.format(str(resource_key), str(resource_value))) - if isinstance(value, str): value = json.loads(value) - results.extend(delete_resource( - self.__base_url, resource_key, resource_value, - timeout=self.__timeout, auth=self.__auth - )) + if isinstance(resource_value, str): resource_value = json.loads(resource_value) + succeeded = self._handler_etht_service.delete(etht_service_name) + if succeeded: + succeeded = self._handler_osu_tunnel.delete(osu_tunnel_name) + results.extend(succeeded) return results @metered_subclass_method(METRICS_POOL) diff --git a/src/device/service/drivers/ietf_actn/__init__.py b/src/device/service/drivers/ietf_actn/__init__.py index d5073c330..38d04994f 100644 --- a/src/device/service/drivers/ietf_actn/__init__.py +++ b/src/device/service/drivers/ietf_actn/__init__.py @@ -11,10 +11,3 @@ # 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/ietf_actn/data.txt b/src/device/service/drivers/ietf_actn/data.txt new file mode 100644 index 000000000..7248933e6 --- /dev/null +++ b/src/device/service/drivers/ietf_actn/data.txt @@ -0,0 +1,14 @@ + +osu_tunnel_1: + delay = 20 + te_odu_number = 40 + src_ttp_channel_name = 'och:1-odu2:1-oduflex:1-osuflex:2' + dst_ttp_channel_name = 'och:1-odu2:1-oduflex:3-osuflex:1' + +etht_service_1: + etht_svc_type = 'op-mp2mp-svc' + src_endpoint_static_route_list: + dst='128.32.10.5', mask=24 => next_hop='128.32.33.5' + dst='128.32.20.5', mask=24 => next_hop='128.32.33.5' + dst_endpoint_static_route_list: + dst='172.1.101.22', mask=24 => next_hop='172.10.33.5' -- GitLab From f1d61a8c2890ae6c3e904bde4374db3d15e444a3 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Thu, 25 Jan 2024 12:08:10 +0000 Subject: [PATCH 10/18] Device - IETF ACTN Driver: - Completed implementation of GetConfig, SetConfig and DeleteConfig methods, and related helper methods --- .../drivers/ietf_actn/IetfActnDriver.py | 41 +++- src/device/service/drivers/ietf_actn/Tools.py | 197 ++++-------------- 2 files changed, 66 insertions(+), 172 deletions(-) diff --git a/src/device/service/drivers/ietf_actn/IetfActnDriver.py b/src/device/service/drivers/ietf_actn/IetfActnDriver.py index e432a41ce..a419a002b 100644 --- a/src/device/service/drivers/ietf_actn/IetfActnDriver.py +++ b/src/device/service/drivers/ietf_actn/IetfActnDriver.py @@ -20,6 +20,7 @@ from device.service.driver_api._Driver import _Driver, RESOURCE_SERVICES from .handlers.EthtServiceHandler import EthtServiceHandler from .handlers.OsuTunnelHandler import OsuTunnelHandler from .handlers.RestApiClient import RestApiClient +from .Tools import get_etht_services, get_osu_tunnels, parse_resource_key LOGGER = logging.getLogger(__name__) @@ -73,10 +74,18 @@ class IetfActnDriver(_Driver): if len(resource_keys) == 0: resource_keys = ALL_RESOURCE_KEYS for i, resource_key in enumerate(resource_keys): chk_string('resource_key[#{:d}]'.format(i), resource_key, allow_empty=False) - etht_service = self._handler_etht_service.get(etht_service_name) - osu_tunnel = self._handler_osu_tunnel.get(osu_tunnel_name) - service_data = {} - results.extend(('/service/service[...]', service_data)) + + if resource_key == RESOURCE_SERVICES: + get_osu_tunnels(self._handler_osu_tunnel, results) + get_etht_services(self._handler_etht_service, results) + else: + # check if resource key is for a specific OSU tunnel or ETHT service, and get them accordingly + osu_tunnel_name, etht_service_name = parse_resource_key(resource_key) + if osu_tunnel_name is not None: + get_osu_tunnels(self._handler_osu_tunnel, results, osu_tunnel_name=osu_tunnel_name) + if etht_service_name is not None: + get_etht_services(self._handler_etht_service, results, etht_service_name=etht_service_name) + return results @metered_subclass_method(METRICS_POOL) @@ -87,10 +96,16 @@ class IetfActnDriver(_Driver): for resource_key, resource_value in resources: LOGGER.info('resource: key({:s}) => value({:s})'.format(str(resource_key), str(resource_value))) if isinstance(resource_value, str): resource_value = json.loads(resource_value) - succeeded = self._handler_osu_tunnel.update(resource_value) - if succeeded: + osu_tunnel_name, etht_service_name = parse_resource_key(resource_key) + + if osu_tunnel_name is not None: + succeeded = self._handler_osu_tunnel.update(resource_value) + results.extend(succeeded) + + if etht_service_name is not None: succeeded = self._handler_etht_service.update(resource_value) - results.extend(succeeded) + results.extend(succeeded) + return results @metered_subclass_method(METRICS_POOL) @@ -101,10 +116,16 @@ class IetfActnDriver(_Driver): for resource_key, resource_value in resources: LOGGER.info('resource: key({:s}) => value({:s})'.format(str(resource_key), str(resource_value))) if isinstance(resource_value, str): resource_value = json.loads(resource_value) - succeeded = self._handler_etht_service.delete(etht_service_name) - if succeeded: + osu_tunnel_name, etht_service_name = parse_resource_key(resource_key) + + if osu_tunnel_name is not None: succeeded = self._handler_osu_tunnel.delete(osu_tunnel_name) - results.extend(succeeded) + results.extend(succeeded) + + if etht_service_name is not None: + succeeded = self._handler_etht_service.delete(etht_service_name) + results.extend(succeeded) + return results @metered_subclass_method(METRICS_POOL) diff --git a/src/device/service/drivers/ietf_actn/Tools.py b/src/device/service/drivers/ietf_actn/Tools.py index ef60446e7..736911fdd 100644 --- a/src/device/service/drivers/ietf_actn/Tools.py +++ b/src/device/service/drivers/ietf_actn/Tools.py @@ -12,168 +12,41 @@ # 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 Dict, Optional -from device.service.driver_api._Driver import RESOURCE_ENDPOINTS, RESOURCE_SERVICES +import logging, re +from typing import Any, List, Optional, Tuple, Union +from .handlers.EthtServiceHandler import EthtServiceHandler +from .handlers.OsuTunnelHandler import OsuTunnelHandler LOGGER = logging.getLogger(__name__) -HTTP_OK_CODES = { - 200, # OK - 201, # Created - 202, # Accepted - 204, # No Content -} - -def find_key(resource, key): - return json.loads(resource[1])[key] - -def get_resource( - base_url : str, resource_key : str, - auth : Optional[HTTPBasicAuth] = None, timeout : Optional[int] = None -): - url = '{:s}/restconf/data/tapi-common:context'.format(base_url) - result = [] - try: - response = requests.get(url, timeout=timeout, verify=False, auth=auth) - except requests.exceptions.Timeout: - LOGGER.exception('Timeout connecting {:s}'.format(url)) - return result - except Exception as e: # pylint: disable=broad-except - LOGGER.exception('Exception retrieving {:s}'.format(resource_key)) - result.append((resource_key, e)) - return result - - try: - context = json.loads(response.content) - except Exception as e: # pylint: disable=broad-except - LOGGER.warning('Unable to decode reply: {:s}'.format(str(response.content))) - 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 'tapi-common:context' in context: - context = context['tapi-common:context'] - elif 'context' in context: - context = context['context'] - - if 'tapi-connectivity:connectivity-context' in context: - context = context['tapi-connectivity:connectivity-context'] - elif 'connectivity-context' in context: - context = context['connectivity-context'] - - for conn_svc in context['connectivity-service']: - service_uuid = conn_svc['uuid'] - constraints = conn_svc.get('connectivity-constraint', {}) - total_req_cap = constraints.get('requested-capacity', {}).get('total-size', {}) - - service_url = '/services/service[{:s}]'.format(service_uuid) - service_data = { - 'uuid': service_uuid, - 'direction': constraints.get('connectivity-direction', 'UNIDIRECTIONAL'), - 'capacity_unit': total_req_cap.get('unit', ''), - 'capacity_value': total_req_cap.get('value', ''), - } - - for i,endpoint in enumerate(conn_svc.get('end-point', [])): - layer_protocol_name = endpoint.get('layer-protocol-name') - if layer_protocol_name is not None: - service_data['layer_protocol_name'] = layer_protocol_name - - layer_protocol_qualifier = endpoint.get('layer-protocol-qualifier') - if layer_protocol_qualifier is not None: - service_data['layer_protocol_qualifier'] = layer_protocol_qualifier - - sip = endpoint['service-interface-point']['service-interface-point-uuid'] - service_data['input_sip' if i == 0 else 'output_sip'] = sip - - result.append((service_url, service_data)) - - return result - -def create_resource( - base_url : str, resource_key : str, resource_value : Dict, - auth : Optional[HTTPBasicAuth] = None, timeout : Optional[int] = None -): - - uuid = find_key(resource, 'uuid') - input_sip = find_key(resource, 'input_sip_uuid') - output_sip = find_key(resource, 'output_sip_uuid') - capacity_value = find_key(resource, 'capacity_value') - capacity_unit = find_key(resource, 'capacity_unit') - layer_protocol_name = find_key(resource, 'layer_protocol_name') - layer_protocol_qualifier = find_key(resource, 'layer_protocol_qualifier') - direction = find_key(resource, 'direction') - - - url = '{:s}/restconf/data/tapi-common:context/tapi-connectivity:connectivity-context'.format(base_url) - headers = {'content-type': 'application/json'} - data = compose_... - results = [] - try: - LOGGER.info('Connectivity service {:s}: {:s}'.format(str(uuid), str(data))) - response = requests.post( - url=url, data=json.dumps(data), timeout=timeout, headers=headers, verify=False, auth=auth) - LOGGER.info('TAPI response: {:s}'.format(str(response))) - except Exception as e: # pylint: disable=broad-except - LOGGER.exception('Exception creating ConnectivityService(uuid={:s}, data={:s})'.format(str(uuid), str(data))) - results.append(e) - else: - if response.status_code not in HTTP_OK_CODES: - msg = 'Could not create ConnectivityService(uuid={:s}, data={:s}). status_code={:s} reply={:s}' - LOGGER.error(msg.format(str(uuid), str(data), str(response.status_code), str(response))) - results.append(response.status_code in HTTP_OK_CODES) - return results - -def delete_resource( - base_url : str, resource_key : str, resource_value : Dict, - auth : Optional[HTTPBasicAuth] = None, timeout : Optional[int] = None -): - uuid = find_key(resource_value, 'uuid') - - url = '{:s}/tapi-common:context/tapi-connectivity:connectivity-context/connectivity-service={:s}' - url = url.format(base_url, uuid) - results = [] - try: - response = requests.delete(url=url, timeout=timeout, verify=False, auth=auth) - except Exception as e: # pylint: disable=broad-except - LOGGER.exception('Exception deleting ConnectivityService(uuid={:s})'.format(str(uuid))) - results.append(e) - else: - if response.status_code not in HTTP_OK_CODES: - msg = 'Could not delete ConnectivityService(uuid={:s}). status_code={:s} reply={:s}' - LOGGER.error(msg.format(str(uuid), str(response.status_code), str(response))) - results.append(response.status_code in HTTP_OK_CODES) - return results +RE_OSU_TUNNEL = re.compile(r'^\/osu\_tunnels\/osu\_tunnel\[([^\]]+)\]$') +RE_ETHT_SERVICE = re.compile(r'^\/etht\_services\/etht\_service\[([^\]]+)\]$') + +def parse_resource_key(resource_key : str) -> Tuple[Optional[str], Optional[str]]: + re_match_osu_tunnel = RE_OSU_TUNNEL.match(resource_key) + osu_tunnel_name = None if re_match_osu_tunnel is None else re_match_osu_tunnel.group(1) + + re_match_etht_service = RE_ETHT_SERVICE.match(resource_key) + etht_service_name = None if re_match_etht_service is None else re_match_etht_service.group(1) + + return osu_tunnel_name, etht_service_name + +def get_osu_tunnels( + handler_osu_tunnel : OsuTunnelHandler, results : List[Tuple[str, Union[Any, None, Exception]]], + osu_tunnel_name : Optional[str] = None +) -> None: + osu_tunnels = handler_osu_tunnel.get(osu_tunnel_name=osu_tunnel_name) + for osu_tunnel in osu_tunnels: + osu_tunnel_name = osu_tunnel['name'] + resource_key = '/osu_tunnels/osu_tunnel[{:s}]'.format(osu_tunnel_name) + results.extend((resource_key, osu_tunnel)) + +def get_etht_services( + handler_etht_service : EthtServiceHandler, results : List[Tuple[str, Union[Any, None, Exception]]], + etht_service_name : Optional[str] = None +) -> None: + etht_services = handler_etht_service.get(etht_service_name=etht_service_name) + for etht_service in etht_services: + etht_service_name = etht_service['name'] + resource_key = '/etht_services/etht_service[{:s}]'.format(etht_service_name) + results.extend((resource_key, etht_service)) -- GitLab From 69b96fbcde90621c227595f29522d1453a12937c Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Thu, 25 Jan 2024 18:40:57 +0000 Subject: [PATCH 11/18] Device - IETF ACTN Driver: - Added test script - Improved unitary test - Minor code fixings - Removed unneeded files - Added related objects factory methods --- scripts/run_tests_locally-device-ietf-actn.sh | 24 ++ src/common/tools/object_factory/Device.py | 12 + src/device/service/drivers/__init__.py | 9 + src/device/service/drivers/ietf_actn/data.txt | 14 -- .../ietf_actn/handlers/EthtServiceHandler.py | 4 +- .../ietf_actn/handlers/OsuTunnelHandler.py | 6 +- .../ietf_actn/handlers/RestApiClient.py | 6 +- src/device/tests/test_unitary_ietf_actn.py | 223 ++++++++++++++++++ 8 files changed, 276 insertions(+), 22 deletions(-) create mode 100755 scripts/run_tests_locally-device-ietf-actn.sh delete mode 100644 src/device/service/drivers/ietf_actn/data.txt create mode 100644 src/device/tests/test_unitary_ietf_actn.py diff --git a/scripts/run_tests_locally-device-ietf-actn.sh b/scripts/run_tests_locally-device-ietf-actn.sh new file mode 100755 index 000000000..c694b6424 --- /dev/null +++ b/scripts/run_tests_locally-device-ietf-actn.sh @@ -0,0 +1,24 @@ +#!/bin/bash +# 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. + + +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=INFO --verbose \ + device/tests/test_unitary_ietf_actn.py diff --git a/src/common/tools/object_factory/Device.py b/src/common/tools/object_factory/Device.py index bc5c28740..76959232a 100644 --- a/src/common/tools/object_factory/Device.py +++ b/src/common/tools/object_factory/Device.py @@ -46,6 +46,10 @@ DEVICE_P4_DRIVERS = [DeviceDriverEnum.DEVICEDRIVER_P4] DEVICE_TFS_TYPE = DeviceTypeEnum.TERAFLOWSDN_CONTROLLER.value DEVICE_TFS_DRIVERS = [DeviceDriverEnum.DEVICEDRIVER_IETF_L2VPN] +DEVICE_IETF_ACTN_TYPE = DeviceTypeEnum.OPEN_LINE_SYSTEM.value +DEVICE_IETF_ACTN_DRIVERS = [DeviceDriverEnum.DEVICEDRIVER_IETF_ACTN] + + def json_device_id(device_uuid : str): return {'device_uuid': {'uuid': device_uuid}} @@ -136,6 +140,14 @@ def json_device_tfs_disabled( device_uuid, DEVICE_TFS_TYPE, DEVICE_DISABLED, name=name, endpoints=endpoints, config_rules=config_rules, drivers=drivers) +def json_device_ietf_actn_disabled( + device_uuid : str, name : Optional[str] = None, endpoints : List[Dict] = [], config_rules : List[Dict] = [], + drivers : List[Dict] = DEVICE_IETF_ACTN_DRIVERS + ): + return json_device( + device_uuid, DEVICE_IETF_ACTN_TYPE, DEVICE_DISABLED, name=name, endpoints=endpoints, config_rules=config_rules, + drivers=drivers) + def json_device_connect_rules(address : str, port : int, settings : Dict = {}) -> List[Dict]: return [ json_config_rule_set('_connect/address', address), diff --git a/src/device/service/drivers/__init__.py b/src/device/service/drivers/__init__.py index 442acf839..27c61f89f 100644 --- a/src/device/service/drivers/__init__.py +++ b/src/device/service/drivers/__init__.py @@ -84,6 +84,15 @@ DRIVERS.append( } ])) +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( diff --git a/src/device/service/drivers/ietf_actn/data.txt b/src/device/service/drivers/ietf_actn/data.txt deleted file mode 100644 index 7248933e6..000000000 --- a/src/device/service/drivers/ietf_actn/data.txt +++ /dev/null @@ -1,14 +0,0 @@ - -osu_tunnel_1: - delay = 20 - te_odu_number = 40 - src_ttp_channel_name = 'och:1-odu2:1-oduflex:1-osuflex:2' - dst_ttp_channel_name = 'och:1-odu2:1-oduflex:3-osuflex:1' - -etht_service_1: - etht_svc_type = 'op-mp2mp-svc' - src_endpoint_static_route_list: - dst='128.32.10.5', mask=24 => next_hop='128.32.33.5' - dst='128.32.20.5', mask=24 => next_hop='128.32.33.5' - dst_endpoint_static_route_list: - dst='172.1.101.22', mask=24 => next_hop='172.10.33.5' diff --git a/src/device/service/drivers/ietf_actn/handlers/EthtServiceHandler.py b/src/device/service/drivers/ietf_actn/handlers/EthtServiceHandler.py index ff0dadbbc..ac9a96633 100644 --- a/src/device/service/drivers/ietf_actn/handlers/EthtServiceHandler.py +++ b/src/device/service/drivers/ietf_actn/handlers/EthtServiceHandler.py @@ -176,10 +176,10 @@ class EthtServiceHandler: 'dst_node_id' : dst_endpoint['node-id'], 'dst_tp_id' : dst_endpoint['tp-id'], - 'dst_vlan_tag' : src_endpoint['outer-tag']['vlan-value'], + 'dst_vlan_tag' : dst_endpoint['outer-tag']['vlan-value'], 'dst_static_routes': [ (static_route['destination'], static_route['destination-mask'], static_route['next-hop']) - for static_route in src_endpoint.get('static-route-list', list()) + for static_route in dst_endpoint.get('static-route-list', list()) ], } etht_services.append(etht_service) diff --git a/src/device/service/drivers/ietf_actn/handlers/OsuTunnelHandler.py b/src/device/service/drivers/ietf_actn/handlers/OsuTunnelHandler.py index 2cb6f46f0..960ad70d7 100644 --- a/src/device/service/drivers/ietf_actn/handlers/OsuTunnelHandler.py +++ b/src/device/service/drivers/ietf_actn/handlers/OsuTunnelHandler.py @@ -127,11 +127,11 @@ class OsuTunnelHandler: osu_tunnel = { 'name' : item['name'], 'src_node_id' : src_endpoint['node-id'], - 'src_tp_id' : src_endpoint['node-id'], + 'src_tp_id' : src_endpoint['tp-id'], 'src_ttp_channel_name': src_endpoint['ttp-channel-name'], 'dst_node_id' : dst_endpoint['node-id'], - 'dst_tp_id' : dst_endpoint['node-id'], - 'dst_ttp_channel_name': src_endpoint['ttp-channel-name'], + 'dst_tp_id' : dst_endpoint['tp-id'], + 'dst_ttp_channel_name': dst_endpoint['ttp-channel-name'], 'odu_type' : item['te-bandwidth']['odu-type'], 'osuflex_number' : item['te-bandwidth']['number'], 'delay' : item['delay'], diff --git a/src/device/service/drivers/ietf_actn/handlers/RestApiClient.py b/src/device/service/drivers/ietf_actn/handlers/RestApiClient.py index 7154dfc48..25bc9fc71 100644 --- a/src/device/service/drivers/ietf_actn/handlers/RestApiClient.py +++ b/src/device/service/drivers/ietf_actn/handlers/RestApiClient.py @@ -19,7 +19,7 @@ from typing import Any, Dict, List, Set, Tuple, Union LOGGER = logging.getLogger(__name__) DEFAULT_BASE_URL = '/restconf/v2/data' -DEFAULT_SCHEMA = 'https' +DEFAULT_SCHEME = 'https' DEFAULT_TIMEOUT = 120 DEFAULT_VERIFY = False @@ -41,12 +41,12 @@ class RestApiClient: 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 ) + scheme = settings.get('scheme', DEFAULT_SCHEME ) 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 )) + self._verify = bool(settings.get('verify', DEFAULT_VERIFY)) def get( self, object_name : str, url : str, filters : List[Tuple[str, str]], diff --git a/src/device/tests/test_unitary_ietf_actn.py b/src/device/tests/test_unitary_ietf_actn.py new file mode 100644 index 000000000..e6afd4fff --- /dev/null +++ b/src/device/tests/test_unitary_ietf_actn.py @@ -0,0 +1,223 @@ +# 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, logging, os, pytest, time +from flask import Flask +from common.proto.context_pb2 import ConfigActionEnum, Device, DeviceId +from common.tools.grpc.Tools import grpc_message_to_json_string +from common.tools.object_factory.ConfigRule import json_config_rule_delete, json_config_rule_set +from common.tools.object_factory.Device import ( + json_device_connect_rules, json_device_id, json_device_ietf_actn_disabled +) +from common.tools.service.GenericRestServer import GenericRestServer +from context.client.ContextClient import ContextClient +from device.client.DeviceClient import DeviceClient +from device.service.DeviceService import DeviceService +from device.service.driver_api._Driver import _Driver +from tests.tools.mock_ietf_actn_sdn_ctrl.ResourceEthServices import EthService, EthServices +from tests.tools.mock_ietf_actn_sdn_ctrl.ResourceOsuTunnels import OsuTunnel, OsuTunnels + +os.environ['DEVICE_EMULATED_ONLY'] = 'TRUE' +from .PrepareTestScenario import ( # pylint: disable=unused-import + # be careful, order of symbols is important here! + mock_service, device_service, context_client, device_client, test_prepare_environment +) + +DEVICE_UUID = 'DEVICE-IETF-ACTN' +DEVICE_ADDRESS = '127.0.0.1' +DEVICE_PORT = 8080 +DEVICE_USERNAME = 'admin' +DEVICE_PASSWORD = 'admin' +DEVICE_SCHEME = 'http' +DEVICE_BASE_URL = '/restconf/v2/data' +DEVICE_TIMEOUT = 120 +DEVICE_VERIFY = False + +DEVICE_ID = json_device_id(DEVICE_UUID) +DEVICE = json_device_ietf_actn_disabled(DEVICE_UUID) + +DEVICE_CONNECT_RULES = json_device_connect_rules(DEVICE_ADDRESS, DEVICE_PORT, { + 'scheme' : DEVICE_SCHEME, + 'username': DEVICE_USERNAME, + 'password': DEVICE_PASSWORD, + 'base_url': DEVICE_BASE_URL, + 'timeout' : DEVICE_TIMEOUT, + 'verify' : DEVICE_VERIFY, +}) + +DEVICE_CONFIG_RULES = [ + json_config_rule_set('/osu_tunnels/osu_tunnel[osu_tunnel_1]', { + 'name' : 'osu_tunnel_1', + 'src_node_id' : '10.0.10.1', + 'src_tp_id' : '200', + 'src_ttp_channel_name': 'och:1-odu2:1-oduflex:1-osuflex:2', + 'dst_node_id' : '10.0.30.1', + 'dst_tp_id' : '200', + 'dst_ttp_channel_name': 'och:1-odu2:1-oduflex:3-osuflex:1', + 'odu_type' : 'osuflex', + 'osuflex_number' : 40, + 'delay' : 20, + 'bidirectional' : True, + }), + json_config_rule_set('/etht_services/etht_service[etht_service_1]', { + 'name' : 'etht_service_1', + 'service_type' : 'op-mp2mp-svc', + 'osu_tunnel_name' : 'osu_tunnel_1', + 'src_node_id' : '10.0.10.1', + 'src_tp_id' : '200', + 'src_vlan_tag' : 21, + 'src_static_routes': [('128.32.10.5', 24, '128.32.33.5'), ('128.32.20.5', 24, '128.32.33.5')], + 'dst_node_id' : '10.0.30.1', + 'dst_tp_id' : '200', + 'dst_vlan_tag' : 101, + 'dst_static_routes': [('172.1.101.22', 24, '172.10.33.5')], + }), +] + +DEVICE_DECONFIG_RULES = [ + json_config_rule_delete('/osu_tunnels/osu_tunnel[osu_tunnel_1]', {'name': 'osu_tunnel_1'}), + json_config_rule_delete('/etht_services/etht_service[etht_service_1]', {'name': 'etht_service_1'}), +] + +LOGGER = logging.getLogger(__name__) +LOGGER.setLevel(logging.DEBUG) + +@pytest.fixture(scope='session') +def ietf_actn_sdn_ctrl( + device_service: DeviceService, # pylint: disable=redefined-outer-name +) -> Flask: + _rest_server = GenericRestServer(DEVICE_PORT, DEVICE_BASE_URL, bind_address=DEVICE_ADDRESS) + _rest_server.app.debug = True + _rest_server.app.env = 'development' + _rest_server.app.testing = True + + _rest_server.add_resource(OsuTunnels, '/ietf-te:tunnel') + _rest_server.add_resource(OsuTunnel, '/ietf-te:tunnel[name=]') + _rest_server.add_resource(EthServices, '/ietf-eth-tran-service:etht-svc') + _rest_server.add_resource(EthService, '/ietf-eth-tran-service:etht-svc[etht-svc-name=]') + + _rest_server.start() + time.sleep(1) # bring time for the server to start + yield _rest_server + _rest_server.shutdown() + _rest_server.join() + + +def test_device_ietf_actn_add_correct( + device_client: DeviceClient, # pylint: disable=redefined-outer-name + device_service: DeviceService, # pylint: disable=redefined-outer-name +) -> None: + DEVICE_WITH_CONNECT_RULES = copy.deepcopy(DEVICE) + DEVICE_WITH_CONNECT_RULES['device_config']['config_rules'].extend(DEVICE_CONNECT_RULES) + device_client.AddDevice(Device(**DEVICE_WITH_CONNECT_RULES)) + driver_instance_cache = device_service.device_servicer.driver_instance_cache + driver: _Driver = driver_instance_cache.get(DEVICE_UUID) + assert driver is not None + + +def test_device_ietf_actn_get( + context_client: ContextClient, # pylint: disable=redefined-outer-name + device_client: DeviceClient, # pylint: disable=redefined-outer-name +) -> None: + + initial_config = device_client.GetInitialConfig(DeviceId(**DEVICE_ID)) + LOGGER.info('initial_config = {:s}'.format(grpc_message_to_json_string(initial_config))) + + device_data = context_client.GetDevice(DeviceId(**DEVICE_ID)) + LOGGER.info('device_data = {:s}'.format(grpc_message_to_json_string(device_data))) + + +def test_device_ietf_actn_configure( + context_client: ContextClient, # pylint: disable=redefined-outer-name + device_client: DeviceClient, # pylint: disable=redefined-outer-name + device_service: DeviceService, # pylint: disable=redefined-outer-name +) -> None: + + driver_instance_cache = device_service.device_servicer.driver_instance_cache + driver : _Driver = driver_instance_cache.get(DEVICE_UUID) + assert driver is not None + + # Requires to retrieve data from device; might be slow. Uncomment only when needed and test does not pass directly. + #driver_config = sorted(driver.GetConfig(), key=operator.itemgetter(0)) + #LOGGER.info('driver_config = {:s}'.format(str(driver_config))) + + DEVICE_WITH_CONFIG_RULES = copy.deepcopy(DEVICE) + DEVICE_WITH_CONFIG_RULES['device_config']['config_rules'].extend(DEVICE_CONFIG_RULES) + device_client.ConfigureDevice(Device(**DEVICE_WITH_CONFIG_RULES)) + + # Requires to retrieve data from device; might be slow. Uncomment only when needed and test does not pass directly. + #driver_config = sorted(driver.GetConfig(), key=operator.itemgetter(0)) + #LOGGER.info('driver_config = {:s}'.format(str(driver_config))) + + device_data = context_client.GetDevice(DeviceId(**DEVICE_ID)) + config_rules = [ + (ConfigActionEnum.Name(config_rule.action), config_rule.custom.resource_key, config_rule.custom.resource_value) + for config_rule in device_data.device_config.config_rules + if config_rule.WhichOneof('config_rule') == 'custom' + ] + LOGGER.info('device_data.device_config.config_rules = \n{:s}'.format( + '\n'.join(['{:s} {:s} = {:s}'.format(*config_rule) for config_rule in config_rules]))) + for config_rule in DEVICE_CONFIG_RULES: + assert 'custom' in config_rule + config_rule = ( + ConfigActionEnum.Name(config_rule['action']), config_rule['custom']['resource_key'], + config_rule['custom']['resource_value']) + assert config_rule in config_rules + + +def test_device_ietf_actn_deconfigure( + context_client: ContextClient, # pylint: disable=redefined-outer-name + device_client: DeviceClient, # pylint: disable=redefined-outer-name + device_service: DeviceService, # pylint: disable=redefined-outer-name +) -> None: + + driver_instance_cache = device_service.device_servicer.driver_instance_cache + driver: _Driver = driver_instance_cache.get(DEVICE_UUID) + assert driver is not None + + # Requires to retrieve data from device; might be slow. Uncomment only when needed and test does not pass directly. + #driver_config = sorted(driver.GetConfig(), key=operator.itemgetter(0)) + #LOGGER.info('driver_config = {:s}'.format(str(driver_config))) + + DEVICE_WITH_DECONFIG_RULES = copy.deepcopy(DEVICE) + DEVICE_WITH_DECONFIG_RULES['device_config']['config_rules'].extend(DEVICE_DECONFIG_RULES) + device_client.ConfigureDevice(Device(**DEVICE_WITH_DECONFIG_RULES)) + + # Requires to retrieve data from device; might be slow. Uncomment only when needed and test does not pass directly. + #driver_config = sorted(driver.GetConfig(), key=operator.itemgetter(0)) + #LOGGER.info('driver_config = {:s}'.format(str(driver_config))) + + device_data = context_client.GetDevice(DeviceId(**DEVICE_ID)) + config_rules = [ + (ConfigActionEnum.Name(config_rule.action), config_rule.custom.resource_key, config_rule.custom.resource_value) + for config_rule in device_data.device_config.config_rules + if config_rule.WhichOneof('config_rule') == 'custom' + ] + LOGGER.info('device_data.device_config.config_rules = \n{:s}'.format( + '\n'.join(['{:s} {:s} = {:s}'.format(*config_rule) for config_rule in config_rules]))) + for config_rule in DEVICE_DECONFIG_RULES: + assert 'custom' in config_rule + action_set = ConfigActionEnum.Name(ConfigActionEnum.CONFIGACTION_SET) + config_rule = (action_set, config_rule['custom']['resource_key'], config_rule['custom']['resource_value']) + assert config_rule not in config_rules + + +def test_device_ietf_actn_delete( + device_client : DeviceClient, # pylint: disable=redefined-outer-name + device_service : DeviceService, # pylint: disable=redefined-outer-name +) -> None: + device_client.DeleteDevice(DeviceId(**DEVICE_ID)) + driver_instance_cache = device_service.device_servicer.driver_instance_cache + driver : _Driver = driver_instance_cache.get(DEVICE_UUID, {}) + assert driver is None -- GitLab From e0bf1a2efe53e773731bbf33b829fad46099bb9e Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Fri, 26 Jan 2024 13:11:40 +0000 Subject: [PATCH 12/18] Device - IETF ACTN Driver: - Updated test script - Completed unitary tests - Improved driver's error handling - Fixed handlers and RestApiClient - Minor code fixings - Rearranged data files - Added required Python dependencies - Corrected MockIetfActnSdnCtrl tool --- scripts/run_tests_locally-device-ietf-actn.sh | 1 + src/device/requirements.in | 2 + .../drivers/ietf_actn/IetfActnDriver.py | 75 +++-- src/device/service/drivers/ietf_actn/Tools.py | 4 +- .../drivers/ietf_actn/examples/eth_svc_1.json | 91 ------ .../drivers/ietf_actn/examples/eth_svc_2.json | 91 ------ .../ietf_actn/examples/osu_tunnel_1.json | 44 --- .../ietf_actn/examples/osu_tunnel_2.json | 44 --- .../ietf_actn/handlers/EthtServiceHandler.py | 41 +-- .../ietf_actn/handlers/OsuTunnelHandler.py | 37 +-- .../ietf_actn/handlers/RestApiClient.py | 54 ++-- .../tests/data/ietf_actn/config_rules.json | 32 +++ .../tests/data/ietf_actn/deconfig_rules.json | 14 + .../ietf_actn/expected_etht_services.json | 176 ++++++++++++ .../data/ietf_actn/expected_osu_tunnels.json | 84 ++++++ src/device/tests/test_unitary_ietf_actn.py | 263 +++++++++++------- .../MockIetfActnSdnCtrl.py | 6 +- .../ResourceEthServices.py | 30 +- .../ResourceOsuTunnels.py | 8 +- 19 files changed, 607 insertions(+), 490 deletions(-) delete mode 100644 src/device/service/drivers/ietf_actn/examples/eth_svc_1.json delete mode 100644 src/device/service/drivers/ietf_actn/examples/eth_svc_2.json delete mode 100644 src/device/service/drivers/ietf_actn/examples/osu_tunnel_1.json delete mode 100644 src/device/service/drivers/ietf_actn/examples/osu_tunnel_2.json create mode 100644 src/device/tests/data/ietf_actn/config_rules.json create mode 100644 src/device/tests/data/ietf_actn/deconfig_rules.json create mode 100644 src/device/tests/data/ietf_actn/expected_etht_services.json create mode 100644 src/device/tests/data/ietf_actn/expected_osu_tunnels.json diff --git a/scripts/run_tests_locally-device-ietf-actn.sh b/scripts/run_tests_locally-device-ietf-actn.sh index c694b6424..8e602b31d 100755 --- a/scripts/run_tests_locally-device-ietf-actn.sh +++ b/scripts/run_tests_locally-device-ietf-actn.sh @@ -20,5 +20,6 @@ cd $PROJECTDIR/src RCFILE=$PROJECTDIR/coverage/.coveragerc # Run unitary tests and analyze coverage of code at same time +# helpful pytest flags: --log-level=INFO -o log_cli=true --verbose --maxfail=1 --durations=0 coverage run --rcfile=$RCFILE --append -m pytest --log-level=INFO --verbose \ device/tests/test_unitary_ietf_actn.py diff --git a/src/device/requirements.in b/src/device/requirements.in index ece761571..46f4a7518 100644 --- a/src/device/requirements.in +++ b/src/device/requirements.in @@ -16,6 +16,8 @@ anytree==2.8.0 APScheduler==3.10.1 cryptography==36.0.2 +deepdiff==6.7.* +deepmerge==1.1.* #fastcache==1.1.0 Jinja2==3.0.3 ncclient==0.6.13 diff --git a/src/device/service/drivers/ietf_actn/IetfActnDriver.py b/src/device/service/drivers/ietf_actn/IetfActnDriver.py index a419a002b..a33c403f3 100644 --- a/src/device/service/drivers/ietf_actn/IetfActnDriver.py +++ b/src/device/service/drivers/ietf_actn/IetfActnDriver.py @@ -45,7 +45,7 @@ class IetfActnDriver(_Driver): with self.__lock: if self.__started.is_set(): return True try: - self._rest_api_client.get('Check Credentials', '', []) + self._rest_api_client.get('Check Credentials', '') except requests.exceptions.Timeout: LOGGER.exception('Timeout exception checking connectivity') return False @@ -75,16 +75,23 @@ class IetfActnDriver(_Driver): for i, resource_key in enumerate(resource_keys): chk_string('resource_key[#{:d}]'.format(i), resource_key, allow_empty=False) - if resource_key == RESOURCE_SERVICES: - get_osu_tunnels(self._handler_osu_tunnel, results) - get_etht_services(self._handler_etht_service, results) - else: - # check if resource key is for a specific OSU tunnel or ETHT service, and get them accordingly - osu_tunnel_name, etht_service_name = parse_resource_key(resource_key) - if osu_tunnel_name is not None: - get_osu_tunnels(self._handler_osu_tunnel, results, osu_tunnel_name=osu_tunnel_name) - if etht_service_name is not None: - get_etht_services(self._handler_etht_service, results, etht_service_name=etht_service_name) + try: + _results = list() + + if resource_key == RESOURCE_SERVICES: + get_osu_tunnels(self._handler_osu_tunnel, _results) + get_etht_services(self._handler_etht_service, _results) + else: + # check if resource key is for a specific OSU tunnel or ETHT service, and get them accordingly + osu_tunnel_name, etht_service_name = parse_resource_key(resource_key) + if osu_tunnel_name is not None: + get_osu_tunnels(self._handler_osu_tunnel, _results, osu_tunnel_name=osu_tunnel_name) + if etht_service_name is not None: + get_etht_services(self._handler_etht_service, _results, etht_service_name=etht_service_name) + + results.extend(_results) + except Exception as e: + results.append((resource_key, e)) return results @@ -95,16 +102,23 @@ class IetfActnDriver(_Driver): with self.__lock: for resource_key, resource_value in resources: LOGGER.info('resource: key({:s}) => value({:s})'.format(str(resource_key), str(resource_value))) - if isinstance(resource_value, str): resource_value = json.loads(resource_value) - osu_tunnel_name, etht_service_name = parse_resource_key(resource_key) + try: + _results = list() - if osu_tunnel_name is not None: - succeeded = self._handler_osu_tunnel.update(resource_value) - results.extend(succeeded) + if isinstance(resource_value, str): resource_value = json.loads(resource_value) + osu_tunnel_name, etht_service_name = parse_resource_key(resource_key) - if etht_service_name is not None: - succeeded = self._handler_etht_service.update(resource_value) - results.extend(succeeded) + if osu_tunnel_name is not None: + succeeded = self._handler_osu_tunnel.update(resource_value) + _results.append(succeeded) + + if etht_service_name is not None: + succeeded = self._handler_etht_service.update(resource_value) + _results.append(succeeded) + + results.extend(_results) + except Exception as e: + results.append(e) return results @@ -115,16 +129,23 @@ class IetfActnDriver(_Driver): with self.__lock: for resource_key, resource_value in resources: LOGGER.info('resource: key({:s}) => value({:s})'.format(str(resource_key), str(resource_value))) - if isinstance(resource_value, str): resource_value = json.loads(resource_value) - osu_tunnel_name, etht_service_name = parse_resource_key(resource_key) + try: + _results = list() + + if isinstance(resource_value, str): resource_value = json.loads(resource_value) + osu_tunnel_name, etht_service_name = parse_resource_key(resource_key) - if osu_tunnel_name is not None: - succeeded = self._handler_osu_tunnel.delete(osu_tunnel_name) - results.extend(succeeded) + if osu_tunnel_name is not None: + succeeded = self._handler_osu_tunnel.delete(osu_tunnel_name) + _results.append(succeeded) + + if etht_service_name is not None: + succeeded = self._handler_etht_service.delete(etht_service_name) + _results.append(succeeded) - if etht_service_name is not None: - succeeded = self._handler_etht_service.delete(etht_service_name) - results.extend(succeeded) + results.extend(_results) + except Exception as e: + results.append(e) return results diff --git a/src/device/service/drivers/ietf_actn/Tools.py b/src/device/service/drivers/ietf_actn/Tools.py index 736911fdd..52f5b15c4 100644 --- a/src/device/service/drivers/ietf_actn/Tools.py +++ b/src/device/service/drivers/ietf_actn/Tools.py @@ -39,7 +39,7 @@ def get_osu_tunnels( for osu_tunnel in osu_tunnels: osu_tunnel_name = osu_tunnel['name'] resource_key = '/osu_tunnels/osu_tunnel[{:s}]'.format(osu_tunnel_name) - results.extend((resource_key, osu_tunnel)) + results.append((resource_key, osu_tunnel)) def get_etht_services( handler_etht_service : EthtServiceHandler, results : List[Tuple[str, Union[Any, None, Exception]]], @@ -49,4 +49,4 @@ def get_etht_services( for etht_service in etht_services: etht_service_name = etht_service['name'] resource_key = '/etht_services/etht_service[{:s}]'.format(etht_service_name) - results.extend((resource_key, etht_service)) + results.append((resource_key, etht_service)) diff --git a/src/device/service/drivers/ietf_actn/examples/eth_svc_1.json b/src/device/service/drivers/ietf_actn/examples/eth_svc_1.json deleted file mode 100644 index 840092429..000000000 --- a/src/device/service/drivers/ietf_actn/examples/eth_svc_1.json +++ /dev/null @@ -1,91 +0,0 @@ -{ - "ietf-eth-tran-service:etht-svc": { - "etht-svc-instances": [ - { - "etht-svc-name": "etht_service_1", - "etht-svc-title": "ETHT_SVC_1", - "etht-svc-type": "op-mp2mp-svc?", - "source-endpoints": { - "source-endpoint": [ - { - "node-id": "10.0.10.1", - "tp-id": "200", - "protection-role": "work?", - "layer-specific": { - "access-type": "port" - }, - "is-extendable": false, - "is-terminal": true, - "static-route-list": [ - { - "destination": "128.32.10.5", - "destination-mask": 24, - "next-hop": "128.32.33.5" - }, - { - "destination": "128.32.20.5", - "destination-mask": 24, - "next-hop": "128.32.33.5" - } - ], - "outer-tag": { - "tag-type": "ietf-eth-tran-types:classify-c-vlan", - "vlan-value": 21 - }, - "service-classification-type": "ietf-eth-tran-type:vlan-classification", - "ingress-egress-bandwidth-profile" : { - "bandwidth-profile-type": "ietf-eth-tran-types:mef-10-bwp", - "CIR": 10000000, - "EIR": 10000000 - } - } - ] - }, - "destination-endpoints": { - "destination-endpoint": [ - { - "node-id": "10.0.30.1", - "tp-id": "200", - "protection-role": "work?", - "layer-specific": { - "access-type": "port" - }, - "is-extendable": false, - "is-terminal": true, - "static-route-list": [ - { - "destination": "172.1.101.22", - "destination-mask": 24, - "next-hop": "172.10.33.5" - } - ], - "outer-tag": { - "tag-type": "ietf-eth-tran-types:classify-c-vlan", - "vlan-value": 101 - }, - "service-classification-type": "ietf-eth-tran-type:vlan-classification", - "ingress-egress-bandwidth-profile" : { - "bandwidth-profile-type": "ietf-eth-tran-types:mef-10-bwp", - "CIR": 10000000, - "EIR": 10000000 - } - } - ] - }, - "svc-tunnel": [ - { - "tunnel-name": "osu_tunnel_1" - } - ], - "optimizations": { - "optimization-metric": [ - { - "metric-role": "work?", - "metric-type": "ietf-te-types:path-metric-te" - } - ] - } - } - ] - } -} \ No newline at end of file diff --git a/src/device/service/drivers/ietf_actn/examples/eth_svc_2.json b/src/device/service/drivers/ietf_actn/examples/eth_svc_2.json deleted file mode 100644 index 74ca61d03..000000000 --- a/src/device/service/drivers/ietf_actn/examples/eth_svc_2.json +++ /dev/null @@ -1,91 +0,0 @@ -{ - "ietf-eth-tran-service:etht-svc": { - "etht-svc-instances": [ - { - "etht-svc-name": "etht_service_2", - "etht-svc-title": "ETHT_SVC_2", - "etht-svc-type": "op-p2mp-svc?", - "source-endpoints": { - "source-endpoint": [ - { - "node-id": "10.0.10.1", - "tp-id": "200", - "protection-role": "work?", - "layer-specific": { - "access-type": "port" - }, - "is-extendable": false, - "is-terminal": true, - "static-route-list": [ - { - "destination": "128.32.10.5", - "destination-mask": 24, - "next-hop": "128.32.33.5" - }, - { - "destination": "128.32.20.5", - "destination-mask": 24, - "next-hop": "128.32.33.5" - } - ], - "outer-tag": { - "tag-type": "ietf-eth-tran-types:classify-c-vlan", - "vlan-value": 31 - }, - "service-classification-type": "ietf-eth-tran-type:vlan-classification", - "ingress-egress-bandwidth-profile" : { - "bandwidth-profile-type": "ietf-eth-tran-types:mef-10-bwp", - "CIR": 10000000, - "EIR": 10000000 - } - } - ] - }, - "destination-endpoints": { - "destination-endpoint": [ - { - "node-id": "10.0.30.1", - "tp-id": "200", - "protection-role": "work?", - "layer-specific": { - "access-type": "port" - }, - "is-extendable": false, - "is-terminal": true, - "static-route-list": [ - { - "destination": "172.1.101.22", - "destination-mask": 24, - "next-hop": "172.10.33.5" - } - ], - "outer-tag": { - "tag-type": "ietf-eth-tran-types:classify-c-vlan", - "vlan-value": 201 - }, - "service-classification-type": "ietf-eth-tran-type:vlan-classification", - "ingress-egress-bandwidth-profile" : { - "bandwidth-profile-type": "ietf-eth-tran-types:mef-10-bwp", - "CIR": 10000000, - "EIR": 10000000 - } - } - ] - }, - "svc-tunnel": [ - { - "tunnel-name": "osu_tunnel_2" - } - ], - "optimizations": { - "optimization-metric": [ - { - "metric-role": "work?", - "metric-type": "ietf-te-types:path-metric-te" - } - ] - } - } - ] - } -} \ No newline at end of file diff --git a/src/device/service/drivers/ietf_actn/examples/osu_tunnel_1.json b/src/device/service/drivers/ietf_actn/examples/osu_tunnel_1.json deleted file mode 100644 index 728450b92..000000000 --- a/src/device/service/drivers/ietf_actn/examples/osu_tunnel_1.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "ietf-te:tunnel": [ - { - "name": "osu_tunnel_1", - "title": "OSU_TUNNEL_1", - "admin-state": "ietf-te-types:tunnel-admin-state-up", - "delay": 20, - "te-bandwidth": { - "layer": "odu", - "odu-type": "osuflex", - "number": 1 - }, - "bidirectional": true, - "destination-endpoints": { - "destination-endpoint": [ - { - "node-id": "10.0.30.1", - "tp-id": "200", - "ttp-channel-name": "och:1-odu2:1-oduflex:3-osuflex:1?", - "protection-role": "work" - } - ] - }, - "source-endpoints": { - "source-endpoint": [ - { - "node-id": "10.0.10.1", - "tp-id": "200", - "ttp-channel-name": "och:1-odu2:1-oduflex:1-osuflex:2?", - "protection-role": "work" - } - ] - }, - "restoration": { - "restoration-type": "ietf-te-types:lsp-restoration-not-applicable", - "restoration-lock": false - }, - "protection": { - "protection-type": "ietf-te-types:lsp-protection-unprotected", - "protection-reversion-disable": true - } - } - ] -} \ No newline at end of file diff --git a/src/device/service/drivers/ietf_actn/examples/osu_tunnel_2.json b/src/device/service/drivers/ietf_actn/examples/osu_tunnel_2.json deleted file mode 100644 index 4e6966b8f..000000000 --- a/src/device/service/drivers/ietf_actn/examples/osu_tunnel_2.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "ietf-te:tunnel": [ - { - "name": "osu_tunnel_2", - "title": "OSU_TUNNEL_2", - "admin-state": "ietf-te-types:tunnel-admin-state-up", - "delay": 20, - "te-bandwidth": { - "layer": "odu", - "odu-type": "osuflex", - "number": 1 - }, - "bidirectional": true, - "destination-endpoints": { - "destination-endpoint": [ - { - "node-id": "10.0.30.1", - "tp-id": "200", - "ttp-channel-name": "och:1-odu2:1-oduflex:3-osuflex:1?", - "protection-role": "work" - } - ] - }, - "source-endpoints": { - "source-endpoint": [ - { - "node-id": "10.0.10.1", - "tp-id": "200", - "ttp-channel-name": "och:1-odu2:1-oduflex:1-osuflex:2?", - "protection-role": "work" - } - ] - }, - "restoration": { - "restoration-type": "ietf-te-types:lsp-restoration-not-applicable", - "restoration-lock": false - }, - "protection": { - "protection-type": "ietf-te-types:lsp-protection-unprotected", - "protection-reversion-disable": true - } - } - ] -} \ No newline at end of file diff --git a/src/device/service/drivers/ietf_actn/handlers/EthtServiceHandler.py b/src/device/service/drivers/ietf_actn/handlers/EthtServiceHandler.py index ac9a96633..230d13797 100644 --- a/src/device/service/drivers/ietf_actn/handlers/EthtServiceHandler.py +++ b/src/device/service/drivers/ietf_actn/handlers/EthtServiceHandler.py @@ -109,37 +109,43 @@ def compose_etht_service( 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' - - def _rest_api_get(self, filters : List[Tuple[str, str]]) -> Union[Dict, List]: + self._object_name = 'EthtService' + self._subpath_root = '/ietf-eth-tran-service:etht-svc' + self._subpath_item = self._subpath_root + '/etht-svc-instances="{etht_service_name:s}"' + + def _rest_api_get(self, etht_service_name : Optional[str] = None) -> Union[Dict, List]: + if etht_service_name is None: + subpath_url = self._subpath_root + else: + subpath_url = self._subpath_item.format(etht_service_name=etht_service_name) return self._rest_api_client.get( - self._object_name, self._subpath_url, filters, expected_http_status={HTTP_STATUS_OK} + self._object_name, subpath_url, expected_http_status={HTTP_STATUS_OK} ) def _rest_api_update(self, data : Dict) -> bool: return self._rest_api_client.update( - self._object_name, self._subpath_url, data, expected_http_status={HTTP_STATUS_CREATED} + self._object_name, self._subpath_root, data, expected_http_status={HTTP_STATUS_CREATED} ) - def _rest_api_delete(self, filters : List[Tuple[str, str]]) -> bool: + def _rest_api_delete(self, etht_service_name : str) -> bool: + if etht_service_name is None: raise Exception('etht_service_name is None') + subpath_url = self._subpath_item.format(etht_service_name=etht_service_name) return self._rest_api_client.delete( - self._object_name, self._subpath_url, filters, expected_http_status={HTTP_STATUS_NO_CONTENT} + self._object_name, subpath_url, expected_http_status={HTTP_STATUS_NO_CONTENT} ) def get(self, etht_service_name : Optional[str] = None) -> Union[Dict, List]: - filters = [] if etht_service_name is None else [('etht-svc-name', etht_service_name)] - data = self._rest_api_get(filters) + data = self._rest_api_get(etht_service_name=etht_service_name) - if not isinstance(data, dict): return ValueError('data should be a dict') + if not isinstance(data, dict): raise ValueError('data should be a dict') if 'ietf-eth-tran-service:etht-svc' not in data: - return ValueError('data does not contain key "ietf-eth-tran-service:etht-svc"') + raise ValueError('data does not contain key "ietf-eth-tran-service:etht-svc"') data = data['ietf-eth-tran-service:etht-svc'] if 'etht-svc-instances' not in data: - return ValueError('data["ietf-eth-tran-service:etht-svc"] does not contain key "etht-svc-instances"') + raise ValueError('data["ietf-eth-tran-service:etht-svc"] does not contain key "etht-svc-instances"') data = data['etht-svc-instances'] if not isinstance(data, list): - return ValueError('data["ietf-eth-tran-service:etht-svc"]["etht-svc-instances"] should be a list') + raise ValueError('data["ietf-eth-tran-service:etht-svc"]["etht-svc-instances"] should be a list') etht_services : List[Dict] = list() for item in data: @@ -170,7 +176,7 @@ class EthtServiceHandler: 'src_tp_id' : src_endpoint['tp-id'], 'src_vlan_tag' : src_endpoint['outer-tag']['vlan-value'], 'src_static_routes': [ - (static_route['destination'], static_route['destination-mask'], static_route['next-hop']) + [static_route['destination'], static_route['destination-mask'], static_route['next-hop']] for static_route in src_endpoint.get('static-route-list', list()) ], @@ -178,7 +184,7 @@ class EthtServiceHandler: 'dst_tp_id' : dst_endpoint['tp-id'], 'dst_vlan_tag' : dst_endpoint['outer-tag']['vlan-value'], 'dst_static_routes': [ - (static_route['destination'], static_route['destination-mask'], static_route['next-hop']) + [static_route['destination'], static_route['destination-mask'], static_route['next-hop']] for static_route in dst_endpoint.get('static-route-list', list()) ], } @@ -212,5 +218,4 @@ class EthtServiceHandler: return self._rest_api_update(data) def delete(self, etht_service_name : str) -> bool: - filters = [('etht-svc-name', etht_service_name)] - return self._rest_api_delete(filters) + return self._rest_api_delete(etht_service_name) diff --git a/src/device/service/drivers/ietf_actn/handlers/OsuTunnelHandler.py b/src/device/service/drivers/ietf_actn/handlers/OsuTunnelHandler.py index 960ad70d7..bcecdf89e 100644 --- a/src/device/service/drivers/ietf_actn/handlers/OsuTunnelHandler.py +++ b/src/device/service/drivers/ietf_actn/handlers/OsuTunnelHandler.py @@ -13,7 +13,7 @@ # limitations under the License. import enum, logging -from typing import Dict, List, Optional, Tuple, Union +from typing import Dict, List, Optional, Union from .RestApiClient import HTTP_STATUS_CREATED, HTTP_STATUS_NO_CONTENT, HTTP_STATUS_OK, RestApiClient LOGGER = logging.getLogger(__name__) @@ -83,32 +83,38 @@ def compose_osu_tunnel( 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:te/tunnels' - - def _rest_api_get(self, filters : List[Tuple[str, str]]) -> Union[Dict, List]: + self._object_name = 'OsuTunnel' + self._subpath_root = '/ietf-te:te/tunnels' + self._subpath_item = self._subpath_root + '/tunnel="{osu_tunnel_name:s}"' + + def _rest_api_get(self, osu_tunnel_name : Optional[str] = None) -> Union[Dict, List]: + if osu_tunnel_name is None: + subpath_url = self._subpath_root + else: + subpath_url = self._subpath_item.format(osu_tunnel_name=osu_tunnel_name) return self._rest_api_client.get( - self._object_name, self._subpath_url, filters, expected_http_status={HTTP_STATUS_OK} + self._object_name, subpath_url, expected_http_status={HTTP_STATUS_OK} ) def _rest_api_update(self, data : Dict) -> bool: return self._rest_api_client.update( - self._object_name, self._subpath_url, data, expected_http_status={HTTP_STATUS_CREATED} + self._object_name, self._subpath_root, data, expected_http_status={HTTP_STATUS_CREATED} ) - def _rest_api_delete(self, filters : List[Tuple[str, str]]) -> bool: + def _rest_api_delete(self, osu_tunnel_name : str) -> bool: + if osu_tunnel_name is None: raise Exception('osu_tunnel_name is None') + subpath_url = self._subpath_item.format(osu_tunnel_name=osu_tunnel_name) return self._rest_api_client.delete( - self._object_name, self._subpath_url, filters, expected_http_status={HTTP_STATUS_NO_CONTENT} + self._object_name, subpath_url, expected_http_status={HTTP_STATUS_NO_CONTENT} ) def get(self, osu_tunnel_name : Optional[str] = None) -> Union[Dict, List]: - filters = [] if osu_tunnel_name is None else [('name', osu_tunnel_name)] - data = self._rest_api_get(filters) + data = self._rest_api_get(osu_tunnel_name=osu_tunnel_name) - if not isinstance(data, dict): return ValueError('data should be a dict') - if 'ietf-te:tunnel' not in data: return ValueError('data does not contain key "ietf-te:tunnel"') + if not isinstance(data, dict): raise ValueError('data should be a dict') + if 'ietf-te:tunnel' not in data: raise ValueError('data does not contain key "ietf-te:tunnel"') data = data['ietf-te:tunnel'] - if not isinstance(data, list): return ValueError('data[ietf-te:tunnel] should be a list') + if not isinstance(data, list): raise ValueError('data[ietf-te:tunnel] should be a list') osu_tunnels : List[Dict] = list() for item in data: @@ -167,5 +173,4 @@ class OsuTunnelHandler: return self._rest_api_update(data) def delete(self, osu_tunnel_name : str) -> bool: - filters = [('name', osu_tunnel_name)] - return self._rest_api_delete(filters) + return self._rest_api_delete(osu_tunnel_name) diff --git a/src/device/service/drivers/ietf_actn/handlers/RestApiClient.py b/src/device/service/drivers/ietf_actn/handlers/RestApiClient.py index 25bc9fc71..1eed066b9 100644 --- a/src/device/service/drivers/ietf_actn/handlers/RestApiClient.py +++ b/src/device/service/drivers/ietf_actn/handlers/RestApiClient.py @@ -14,7 +14,7 @@ import copy, json, logging, requests from requests.auth import HTTPBasicAuth -from typing import Any, Dict, List, Set, Tuple, Union +from typing import Any, Dict, List, Set, Union LOGGER = logging.getLogger(__name__) @@ -49,70 +49,56 @@ class RestApiClient: self._verify = bool(settings.get('verify', DEFAULT_VERIFY)) def get( - self, object_name : str, url : str, filters : List[Tuple[str, str]], + self, object_name : str, url : str, expected_http_status : Set[int] = {HTTP_STATUS_OK} ) -> Union[Dict, List]: - str_filters = ''.join([ - '[{:s}={:s}]'.format(filter_field, filter_value) - for filter_field, filter_value in filters - ]) - MSG = 'Get {:s}({:s})' - LOGGER.info(MSG.format(str(object_name), str(str_filters))) + LOGGER.info(MSG.format(str(object_name), str(url))) response = requests.get( - self._base_url + url + str_filters, + self._base_url + url, timeout=self._timeout, verify=self._verify, auth=self._auth ) - LOGGER.info(' Response: {:s}'.format(str(response))) + LOGGER.info(' Response[{:s}]: {:s}'.format(str(response.status_code), str(response.content))) if response.status_code in expected_http_status: return json.loads(response.content) 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) - return Exception(msg) + raise Exception(MSG.format(str(object_name), str(url), str(response.status_code), str(response))) def update( self, object_name : str, url : str, data : Dict, headers : Dict[str, Any] = dict(), expected_http_status : Set[int] = HTTP_OK_CODES - ) -> bool: + ) -> None: headers = copy.deepcopy(headers) if 'content-type' not in {header_name.lower() for header_name in headers.keys()}: headers.update({'content-type': 'application/json'}) - MSG = 'Create/Update {:s}({:s})' - LOGGER.info(MSG.format(str(object_name), str(data))) + MSG = 'Create/Update {:s}({:s}, {:s})' + LOGGER.info(MSG.format(str(object_name), str(url), 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))) + LOGGER.info(' Response[{:s}]: {:s}'.format(str(response.status_code), str(response.content))) - if response.status_code in expected_http_status: return True + if response.status_code in expected_http_status: return - 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))) - return False + MSG = 'Could not create/update {:s}({:s}, {:s}): status_code={:s} reply={:s}' + raise Exception(MSG.format(str(object_name), str(url), str(data), str(response.status_code), str(response))) def delete( - self, object_name : str, url : str, filters : List[Tuple[str, str]], + self, object_name : str, url : str, expected_http_status : Set[int] = HTTP_OK_CODES - ) -> bool: - str_filters = ''.join([ - '[{:s}={:s}]'.format(filter_field, filter_value) - for filter_field, filter_value in filters - ]) - + ) -> None: MSG = 'Delete {:s}({:s})' - LOGGER.info(MSG.format(str(object_name), str(str_filters))) + LOGGER.info(MSG.format(str(object_name), str(url))) response = requests.delete( - self._base_url + url + str_filters, + self._base_url + url, timeout=self._timeout, verify=self._verify, auth=self._auth ) - LOGGER.info(' Response: {:s}'.format(str(response))) + LOGGER.info(' Response[{:s}]: {:s}'.format(str(response.status_code), str(response.content))) - if response.status_code in expected_http_status: return True + if response.status_code in expected_http_status: return 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))) - return False + raise Exception(MSG.format(str(object_name), str(url), str(response.status_code), str(response))) diff --git a/src/device/tests/data/ietf_actn/config_rules.json b/src/device/tests/data/ietf_actn/config_rules.json new file mode 100644 index 000000000..d73a68674 --- /dev/null +++ b/src/device/tests/data/ietf_actn/config_rules.json @@ -0,0 +1,32 @@ +[ + {"action": 1, "custom": {"resource_key": "/osu_tunnels/osu_tunnel[osu_tunnel_1]", "resource_value": { + "name": "osu_tunnel_1", "odu_type": "osuflex", "osuflex_number": 40, "bidirectional": true, "delay": 20, + "src_node_id": "10.0.10.1", "src_tp_id": "200", "src_ttp_channel_name": "och:1-odu2:1-oduflex:1-osuflex:2", + "dst_node_id": "10.0.30.1", "dst_tp_id": "200", "dst_ttp_channel_name": "och:1-odu2:1-oduflex:3-osuflex:1" + }}}, + {"action": 1, "custom": {"resource_key": "/osu_tunnels/osu_tunnel[osu_tunnel_2]", "resource_value": { + "name": "osu_tunnel_2", "odu_type": "osuflex", "osuflex_number": 40, "bidirectional": true, "delay": 20, + "src_node_id": "10.0.10.1", "src_tp_id": "200", "src_ttp_channel_name": "och:1-odu2:1-oduflex:1-osuflex:2", + "dst_node_id": "10.0.30.1", "dst_tp_id": "200", "dst_ttp_channel_name": "och:1-odu2:1-oduflex:3-osuflex:1" + }}}, + {"action": 1, "custom": {"resource_key": "/etht_services/etht_service[etht_service_1]", "resource_value": { + "name": "etht_service_1", "osu_tunnel_name": "osu_tunnel_1", "service_type": "op-mp2mp-svc", + "src_node_id": "10.0.10.1", "src_tp_id": "200", "src_vlan_tag": 21, "src_static_routes": [ + ["128.32.10.5", 24, "128.32.33.5"], + ["128.32.20.5", 24, "128.32.33.5"] + ], + "dst_node_id": "10.0.30.1", "dst_tp_id": "200", "dst_vlan_tag": 101, "dst_static_routes": [ + ["172.1.101.22", 24, "172.10.33.5"] + ] + }}}, + {"action": 1, "custom": {"resource_key": "/etht_services/etht_service[etht_service_2]", "resource_value": { + "name": "etht_service_2", "osu_tunnel_name": "osu_tunnel_2", "service_type": "op-mp2mp-svc", + "src_node_id": "10.0.10.1", "src_tp_id": "200", "src_vlan_tag": 31, "src_static_routes": [ + ["128.32.10.5", 24, "128.32.33.5"], + ["128.32.20.5", 24, "128.32.33.5"] + ], + "dst_node_id": "10.0.30.1", "dst_tp_id": "200", "dst_vlan_tag": 201, "dst_static_routes": [ + ["172.1.101.22", 24, "172.10.33.5"] + ] + }}} +] diff --git a/src/device/tests/data/ietf_actn/deconfig_rules.json b/src/device/tests/data/ietf_actn/deconfig_rules.json new file mode 100644 index 000000000..f18e5fb2d --- /dev/null +++ b/src/device/tests/data/ietf_actn/deconfig_rules.json @@ -0,0 +1,14 @@ +[ + {"action": 2, "custom": {"resource_key": "/osu_tunnels/osu_tunnel[osu_tunnel_1]", "resource_value": { + "name": "osu_tunnel_1" + }}}, + {"action": 2, "custom": {"resource_key": "/osu_tunnels/osu_tunnel[osu_tunnel_2]", "resource_value": { + "name": "osu_tunnel_2" + }}}, + {"action": 2, "custom": {"resource_key": "/etht_services/etht_service[etht_service_1]", "resource_value": { + "name": "etht_service_1" + }}}, + {"action": 2, "custom": {"resource_key": "/etht_services/etht_service[etht_service_2]", "resource_value": { + "name": "etht_service_2" + }}} +] diff --git a/src/device/tests/data/ietf_actn/expected_etht_services.json b/src/device/tests/data/ietf_actn/expected_etht_services.json new file mode 100644 index 000000000..d9f410526 --- /dev/null +++ b/src/device/tests/data/ietf_actn/expected_etht_services.json @@ -0,0 +1,176 @@ +{ + "ietf-eth-tran-service:etht-svc": { + "etht-svc-instances": [ + { + "etht-svc-name": "etht_service_1", + "etht-svc-title": "ETHT_SERVICE_1", + "etht-svc-type": "op-mp2mp-svc", + "source-endpoints": { + "source-endpoint": [ + { + "node-id": "10.0.10.1", + "tp-id": "200", + "protection-role": "work", + "layer-specific": { + "access-type": "port" + }, + "is-extendable": false, + "is-terminal": true, + "static-route-list": [ + { + "destination": "128.32.10.5", + "destination-mask": 24, + "next-hop": "128.32.33.5" + }, + { + "destination": "128.32.20.5", + "destination-mask": 24, + "next-hop": "128.32.33.5" + } + ], + "outer-tag": { + "tag-type": "ietf-eth-tran-types:classify-c-vlan", + "vlan-value": 21 + }, + "service-classification-type": "ietf-eth-tran-type:vlan-classification", + "ingress-egress-bandwidth-profile" : { + "bandwidth-profile-type": "ietf-eth-tran-types:mef-10-bwp", + "CIR": 10000000, + "EIR": 10000000 + } + } + ] + }, + "destination-endpoints": { + "destination-endpoint": [ + { + "node-id": "10.0.30.1", + "tp-id": "200", + "protection-role": "work", + "layer-specific": { + "access-type": "port" + }, + "is-extendable": false, + "is-terminal": true, + "static-route-list": [ + { + "destination": "172.1.101.22", + "destination-mask": 24, + "next-hop": "172.10.33.5" + } + ], + "outer-tag": { + "tag-type": "ietf-eth-tran-types:classify-c-vlan", + "vlan-value": 101 + }, + "service-classification-type": "ietf-eth-tran-type:vlan-classification", + "ingress-egress-bandwidth-profile" : { + "bandwidth-profile-type": "ietf-eth-tran-types:mef-10-bwp", + "CIR": 10000000, + "EIR": 10000000 + } + } + ] + }, + "svc-tunnel": [ + { + "tunnel-name": "osu_tunnel_1" + } + ], + "optimizations": { + "optimization-metric": [ + { + "metric-role": "work", + "metric-type": "ietf-te-types:path-metric-te" + } + ] + } + }, + { + "etht-svc-name": "etht_service_2", + "etht-svc-title": "ETHT_SERVICE_2", + "etht-svc-type": "op-mp2mp-svc", + "source-endpoints": { + "source-endpoint": [ + { + "node-id": "10.0.10.1", + "tp-id": "200", + "protection-role": "work", + "layer-specific": { + "access-type": "port" + }, + "is-extendable": false, + "is-terminal": true, + "static-route-list": [ + { + "destination": "128.32.10.5", + "destination-mask": 24, + "next-hop": "128.32.33.5" + }, + { + "destination": "128.32.20.5", + "destination-mask": 24, + "next-hop": "128.32.33.5" + } + ], + "outer-tag": { + "tag-type": "ietf-eth-tran-types:classify-c-vlan", + "vlan-value": 31 + }, + "service-classification-type": "ietf-eth-tran-type:vlan-classification", + "ingress-egress-bandwidth-profile" : { + "bandwidth-profile-type": "ietf-eth-tran-types:mef-10-bwp", + "CIR": 10000000, + "EIR": 10000000 + } + } + ] + }, + "destination-endpoints": { + "destination-endpoint": [ + { + "node-id": "10.0.30.1", + "tp-id": "200", + "protection-role": "work", + "layer-specific": { + "access-type": "port" + }, + "is-extendable": false, + "is-terminal": true, + "static-route-list": [ + { + "destination": "172.1.101.22", + "destination-mask": 24, + "next-hop": "172.10.33.5" + } + ], + "outer-tag": { + "tag-type": "ietf-eth-tran-types:classify-c-vlan", + "vlan-value": 201 + }, + "service-classification-type": "ietf-eth-tran-type:vlan-classification", + "ingress-egress-bandwidth-profile" : { + "bandwidth-profile-type": "ietf-eth-tran-types:mef-10-bwp", + "CIR": 10000000, + "EIR": 10000000 + } + } + ] + }, + "svc-tunnel": [ + { + "tunnel-name": "osu_tunnel_2" + } + ], + "optimizations": { + "optimization-metric": [ + { + "metric-role": "work", + "metric-type": "ietf-te-types:path-metric-te" + } + ] + } + } + ] + } +} \ No newline at end of file diff --git a/src/device/tests/data/ietf_actn/expected_osu_tunnels.json b/src/device/tests/data/ietf_actn/expected_osu_tunnels.json new file mode 100644 index 000000000..1debf555b --- /dev/null +++ b/src/device/tests/data/ietf_actn/expected_osu_tunnels.json @@ -0,0 +1,84 @@ +{ + "ietf-te:tunnel": [ + { + "name": "osu_tunnel_1", + "title": "OSU_TUNNEL_1", + "admin-state": "ietf-te-types:tunnel-admin-state-up", + "delay": 20, + "te-bandwidth": { + "layer": "odu", + "odu-type": "osuflex", + "number": 40 + }, + "bidirectional": true, + "destination-endpoints": { + "destination-endpoint": [ + { + "node-id": "10.0.30.1", + "tp-id": "200", + "ttp-channel-name": "och:1-odu2:1-oduflex:3-osuflex:1", + "protection-role": "work" + } + ] + }, + "source-endpoints": { + "source-endpoint": [ + { + "node-id": "10.0.10.1", + "tp-id": "200", + "ttp-channel-name": "och:1-odu2:1-oduflex:1-osuflex:2", + "protection-role": "work" + } + ] + }, + "restoration": { + "restoration-type": "ietf-te-types:lsp-restoration-not-applicable", + "restoration-lock": false + }, + "protection": { + "protection-type": "ietf-te-types:lsp-protection-unprotected", + "protection-reversion-disable": true + } + }, + { + "name": "osu_tunnel_2", + "title": "OSU_TUNNEL_2", + "admin-state": "ietf-te-types:tunnel-admin-state-up", + "delay": 20, + "te-bandwidth": { + "layer": "odu", + "odu-type": "osuflex", + "number": 40 + }, + "bidirectional": true, + "destination-endpoints": { + "destination-endpoint": [ + { + "node-id": "10.0.30.1", + "tp-id": "200", + "ttp-channel-name": "och:1-odu2:1-oduflex:3-osuflex:1", + "protection-role": "work" + } + ] + }, + "source-endpoints": { + "source-endpoint": [ + { + "node-id": "10.0.10.1", + "tp-id": "200", + "ttp-channel-name": "och:1-odu2:1-oduflex:1-osuflex:2", + "protection-role": "work" + } + ] + }, + "restoration": { + "restoration-type": "ietf-te-types:lsp-restoration-not-applicable", + "restoration-lock": false + }, + "protection": { + "protection-type": "ietf-te-types:lsp-protection-unprotected", + "protection-reversion-disable": true + } + } + ] +} diff --git a/src/device/tests/test_unitary_ietf_actn.py b/src/device/tests/test_unitary_ietf_actn.py index e6afd4fff..5f01a412d 100644 --- a/src/device/tests/test_unitary_ietf_actn.py +++ b/src/device/tests/test_unitary_ietf_actn.py @@ -12,11 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -import copy, logging, os, pytest, time -from flask import Flask +import copy, deepdiff, json, logging, operator, os, pytest, time +from flask import Flask, jsonify, make_response +from flask_restful import Resource from common.proto.context_pb2 import ConfigActionEnum, Device, DeviceId +from common.tools.descriptor.Tools import format_custom_config_rules from common.tools.grpc.Tools import grpc_message_to_json_string -from common.tools.object_factory.ConfigRule import json_config_rule_delete, json_config_rule_set from common.tools.object_factory.Device import ( json_device_connect_rules, json_device_id, json_device_ietf_actn_disabled ) @@ -31,7 +32,7 @@ from tests.tools.mock_ietf_actn_sdn_ctrl.ResourceOsuTunnels import OsuTunnel, Os os.environ['DEVICE_EMULATED_ONLY'] = 'TRUE' from .PrepareTestScenario import ( # pylint: disable=unused-import # be careful, order of symbols is important here! - mock_service, device_service, context_client, device_client, test_prepare_environment + mock_service, device_service, context_client, device_client, monitoring_client, test_prepare_environment ) DEVICE_UUID = 'DEVICE-IETF-ACTN' @@ -56,56 +57,34 @@ DEVICE_CONNECT_RULES = json_device_connect_rules(DEVICE_ADDRESS, DEVICE_PORT, { 'verify' : DEVICE_VERIFY, }) -DEVICE_CONFIG_RULES = [ - json_config_rule_set('/osu_tunnels/osu_tunnel[osu_tunnel_1]', { - 'name' : 'osu_tunnel_1', - 'src_node_id' : '10.0.10.1', - 'src_tp_id' : '200', - 'src_ttp_channel_name': 'och:1-odu2:1-oduflex:1-osuflex:2', - 'dst_node_id' : '10.0.30.1', - 'dst_tp_id' : '200', - 'dst_ttp_channel_name': 'och:1-odu2:1-oduflex:3-osuflex:1', - 'odu_type' : 'osuflex', - 'osuflex_number' : 40, - 'delay' : 20, - 'bidirectional' : True, - }), - json_config_rule_set('/etht_services/etht_service[etht_service_1]', { - 'name' : 'etht_service_1', - 'service_type' : 'op-mp2mp-svc', - 'osu_tunnel_name' : 'osu_tunnel_1', - 'src_node_id' : '10.0.10.1', - 'src_tp_id' : '200', - 'src_vlan_tag' : 21, - 'src_static_routes': [('128.32.10.5', 24, '128.32.33.5'), ('128.32.20.5', 24, '128.32.33.5')], - 'dst_node_id' : '10.0.30.1', - 'dst_tp_id' : '200', - 'dst_vlan_tag' : 101, - 'dst_static_routes': [('172.1.101.22', 24, '172.10.33.5')], - }), -] - -DEVICE_DECONFIG_RULES = [ - json_config_rule_delete('/osu_tunnels/osu_tunnel[osu_tunnel_1]', {'name': 'osu_tunnel_1'}), - json_config_rule_delete('/etht_services/etht_service[etht_service_1]', {'name': 'etht_service_1'}), -] +DATA_FILE_CONFIG_RULES = 'device/tests/data/ietf_actn/config_rules.json' +DATA_FILE_DECONFIG_RULES = 'device/tests/data/ietf_actn/deconfig_rules.json' +DATA_FILE_EXPECTED_OSU_TUNNELS = 'device/tests/data/ietf_actn/expected_osu_tunnels.json' +DATA_FILE_EXPECTED_ETHT_SERVICES = 'device/tests/data/ietf_actn/expected_etht_services.json' LOGGER = logging.getLogger(__name__) LOGGER.setLevel(logging.DEBUG) @pytest.fixture(scope='session') def ietf_actn_sdn_ctrl( - device_service: DeviceService, # pylint: disable=redefined-outer-name + device_service : DeviceService, # pylint: disable=redefined-outer-name ) -> Flask: _rest_server = GenericRestServer(DEVICE_PORT, DEVICE_BASE_URL, bind_address=DEVICE_ADDRESS) - _rest_server.app.debug = True - _rest_server.app.env = 'development' - _rest_server.app.testing = True - - _rest_server.add_resource(OsuTunnels, '/ietf-te:tunnel') - _rest_server.add_resource(OsuTunnel, '/ietf-te:tunnel[name=]') - _rest_server.add_resource(EthServices, '/ietf-eth-tran-service:etht-svc') - _rest_server.add_resource(EthService, '/ietf-eth-tran-service:etht-svc[etht-svc-name=]') + _rest_server.app.config['DEBUG' ] = True + _rest_server.app.config['ENV' ] = 'development' + _rest_server.app.config['SERVER_NAME'] = '{:s}:{:d}'.format(DEVICE_ADDRESS, DEVICE_PORT) + _rest_server.app.config['TESTING' ] = True + + class Root(Resource): + def get(self): + return make_response(jsonify({}), 200) + + add_rsrc = _rest_server.add_resource + add_rsrc(Root, '/') + add_rsrc(OsuTunnels, '/ietf-te:te/tunnels') + add_rsrc(OsuTunnel, '/ietf-te:te/tunnels/tunnel=""') + add_rsrc(EthServices, '/ietf-eth-tran-service:etht-svc') + add_rsrc(EthService, '/ietf-eth-tran-service:etht-svc/etht-svc-instances=""') _rest_server.start() time.sleep(1) # bring time for the server to start @@ -114,9 +93,10 @@ def ietf_actn_sdn_ctrl( _rest_server.join() -def test_device_ietf_actn_add_correct( - device_client: DeviceClient, # pylint: disable=redefined-outer-name - device_service: DeviceService, # pylint: disable=redefined-outer-name +def test_device_ietf_actn_add( + device_client : DeviceClient, # pylint: disable=redefined-outer-name + device_service : DeviceService, # pylint: disable=redefined-outer-name + ietf_actn_sdn_ctrl : GenericRestServer, # pylint: disable=redefined-outer-name ) -> None: DEVICE_WITH_CONNECT_RULES = copy.deepcopy(DEVICE) DEVICE_WITH_CONNECT_RULES['device_config']['config_rules'].extend(DEVICE_CONNECT_RULES) @@ -127,8 +107,9 @@ def test_device_ietf_actn_add_correct( def test_device_ietf_actn_get( - context_client: ContextClient, # pylint: disable=redefined-outer-name - device_client: DeviceClient, # pylint: disable=redefined-outer-name + context_client : ContextClient, # pylint: disable=redefined-outer-name + device_client : DeviceClient, # pylint: disable=redefined-outer-name + ietf_actn_sdn_ctrl : GenericRestServer, # pylint: disable=redefined-outer-name ) -> None: initial_config = device_client.GetInitialConfig(DeviceId(**DEVICE_ID)) @@ -139,83 +120,161 @@ def test_device_ietf_actn_get( def test_device_ietf_actn_configure( - context_client: ContextClient, # pylint: disable=redefined-outer-name - device_client: DeviceClient, # pylint: disable=redefined-outer-name - device_service: DeviceService, # pylint: disable=redefined-outer-name + device_client : DeviceClient, # pylint: disable=redefined-outer-name + device_service : DeviceService, # pylint: disable=redefined-outer-name + ietf_actn_sdn_ctrl : GenericRestServer, # pylint: disable=redefined-outer-name ) -> None: + ietf_actn_client = ietf_actn_sdn_ctrl.app.test_client() driver_instance_cache = device_service.device_servicer.driver_instance_cache driver : _Driver = driver_instance_cache.get(DEVICE_UUID) assert driver is not None - # Requires to retrieve data from device; might be slow. Uncomment only when needed and test does not pass directly. - #driver_config = sorted(driver.GetConfig(), key=operator.itemgetter(0)) - #LOGGER.info('driver_config = {:s}'.format(str(driver_config))) + retrieved_osu_tunnels = ietf_actn_client.get('/restconf/v2/data/ietf-te:te/tunnels') + retrieved_osu_tunnels = retrieved_osu_tunnels.json + LOGGER.info('osu_tunnels = {:s}'.format(str(retrieved_osu_tunnels))) + expected_osu_tunnels = {'ietf-te:tunnel': []} + osu_tunnels_diff = deepdiff.DeepDiff(expected_osu_tunnels, retrieved_osu_tunnels) + if len(osu_tunnels_diff) > 0: + LOGGER.error('PRE OSU TUNNELS - Differences:\n{:s}'.format(str(osu_tunnels_diff.pretty()))) + assert len(osu_tunnels_diff) == 0 + + retrieved_etht_services = ietf_actn_client.get('/restconf/v2/data/ietf-eth-tran-service:etht-svc') + retrieved_etht_services = retrieved_etht_services.json + LOGGER.info('etht_services = {:s}'.format(str(retrieved_etht_services))) + expected_etht_services = {'ietf-eth-tran-service:etht-svc': {'etht-svc-instances': []}} + etht_services_diff = deepdiff.DeepDiff(expected_etht_services, retrieved_etht_services) + if len(etht_services_diff) > 0: + LOGGER.error('PRE ETHT SERVICES - Differences:\n{:s}'.format(str(etht_services_diff.pretty()))) + assert len(etht_services_diff) == 0 + + retrieved_driver_config_rules = sorted(driver.GetConfig(), key=operator.itemgetter(0)) + LOGGER.info('driver_config = {:s}'.format(str(retrieved_driver_config_rules))) + assert isinstance(retrieved_driver_config_rules, list) + if len(retrieved_driver_config_rules) > 0: + LOGGER.error('PRE DRIVER CONFIG RULES - Differences:\n{:s}'.format(str(retrieved_driver_config_rules))) + assert len(retrieved_driver_config_rules) == 0 DEVICE_WITH_CONFIG_RULES = copy.deepcopy(DEVICE) - DEVICE_WITH_CONFIG_RULES['device_config']['config_rules'].extend(DEVICE_CONFIG_RULES) + with open(DATA_FILE_CONFIG_RULES, 'r', encoding='UTF-8') as f: + config_rules = format_custom_config_rules(json.load(f)) + DEVICE_WITH_CONFIG_RULES['device_config']['config_rules'].extend(config_rules) device_client.ConfigureDevice(Device(**DEVICE_WITH_CONFIG_RULES)) - # Requires to retrieve data from device; might be slow. Uncomment only when needed and test does not pass directly. - #driver_config = sorted(driver.GetConfig(), key=operator.itemgetter(0)) - #LOGGER.info('driver_config = {:s}'.format(str(driver_config))) - - device_data = context_client.GetDevice(DeviceId(**DEVICE_ID)) - config_rules = [ - (ConfigActionEnum.Name(config_rule.action), config_rule.custom.resource_key, config_rule.custom.resource_value) - for config_rule in device_data.device_config.config_rules - if config_rule.WhichOneof('config_rule') == 'custom' + retrieved_osu_tunnels = ietf_actn_client.get('/restconf/v2/data/ietf-te:te/tunnels') + retrieved_osu_tunnels = retrieved_osu_tunnels.json + LOGGER.info('osu_tunnels = {:s}'.format(str(retrieved_osu_tunnels))) + with open(DATA_FILE_EXPECTED_OSU_TUNNELS, 'r', encoding='UTF-8') as f: + expected_osu_tunnels = json.load(f) + osu_tunnels_diff = deepdiff.DeepDiff(expected_osu_tunnels, retrieved_osu_tunnels) + if len(osu_tunnels_diff) > 0: + LOGGER.error('POST OSU TUNNELS - Differences:\n{:s}'.format(str(osu_tunnels_diff.pretty()))) + assert len(osu_tunnels_diff) == 0 + + retrieved_etht_services = ietf_actn_client.get('/restconf/v2/data/ietf-eth-tran-service:etht-svc') + retrieved_etht_services = retrieved_etht_services.json + LOGGER.info('etht_services = {:s}'.format(str(retrieved_etht_services))) + with open(DATA_FILE_EXPECTED_ETHT_SERVICES, 'r', encoding='UTF-8') as f: + expected_etht_services = json.load(f) + etht_services_diff = deepdiff.DeepDiff(expected_etht_services, retrieved_etht_services) + if len(etht_services_diff) > 0: + LOGGER.error('POST ETHT SERVICES - Differences:\n{:s}'.format(str(etht_services_diff.pretty()))) + assert len(etht_services_diff) == 0 + + retrieved_driver_config_rules = sorted(driver.GetConfig(), key=operator.itemgetter(0)) + LOGGER.info('driver_config = {:s}'.format(str(retrieved_driver_config_rules))) + retrieved_driver_config_rules = [ + {'action': 1, 'custom': {'resource_key': resource_key, 'resource_value': resource_value}} + for resource_key, resource_value in retrieved_driver_config_rules ] - LOGGER.info('device_data.device_config.config_rules = \n{:s}'.format( - '\n'.join(['{:s} {:s} = {:s}'.format(*config_rule) for config_rule in config_rules]))) - for config_rule in DEVICE_CONFIG_RULES: - assert 'custom' in config_rule - config_rule = ( - ConfigActionEnum.Name(config_rule['action']), config_rule['custom']['resource_key'], - config_rule['custom']['resource_value']) - assert config_rule in config_rules + with open(DATA_FILE_CONFIG_RULES, 'r', encoding='UTF-8') as f: + expected_driver_config_rules = sorted(json.load(f), key=lambda cr: cr['custom']['resource_key']) + driver_config_rules_diff = deepdiff.DeepDiff(expected_driver_config_rules, retrieved_driver_config_rules) + if len(driver_config_rules_diff) > 0: + LOGGER.error('POST DRIVER CONFIG RULES - Differences:\n{:s}'.format(str(driver_config_rules_diff.pretty()))) + assert len(driver_config_rules_diff) == 0 def test_device_ietf_actn_deconfigure( - context_client: ContextClient, # pylint: disable=redefined-outer-name - device_client: DeviceClient, # pylint: disable=redefined-outer-name - device_service: DeviceService, # pylint: disable=redefined-outer-name + device_client : DeviceClient, # pylint: disable=redefined-outer-name + device_service : DeviceService, # pylint: disable=redefined-outer-name + ietf_actn_sdn_ctrl : GenericRestServer, # pylint: disable=redefined-outer-name ) -> None: + ietf_actn_client = ietf_actn_sdn_ctrl.app.test_client() driver_instance_cache = device_service.device_servicer.driver_instance_cache - driver: _Driver = driver_instance_cache.get(DEVICE_UUID) + driver : _Driver = driver_instance_cache.get(DEVICE_UUID) assert driver is not None - # Requires to retrieve data from device; might be slow. Uncomment only when needed and test does not pass directly. - #driver_config = sorted(driver.GetConfig(), key=operator.itemgetter(0)) - #LOGGER.info('driver_config = {:s}'.format(str(driver_config))) + retrieved_osu_tunnels = ietf_actn_client.get('/restconf/v2/data/ietf-te:te/tunnels') + retrieved_osu_tunnels = retrieved_osu_tunnels.json + LOGGER.info('osu_tunnels = {:s}'.format(str(retrieved_osu_tunnels))) + with open(DATA_FILE_EXPECTED_OSU_TUNNELS, 'r', encoding='UTF-8') as f: + expected_osu_tunnels = json.load(f) + osu_tunnels_diff = deepdiff.DeepDiff(expected_osu_tunnels, retrieved_osu_tunnels) + if len(osu_tunnels_diff) > 0: + LOGGER.error('PRE OSU TUNNELS - Differences:\n{:s}'.format(str(osu_tunnels_diff.pretty()))) + assert len(osu_tunnels_diff) == 0 + + retrieved_etht_services = ietf_actn_client.get('/restconf/v2/data/ietf-eth-tran-service:etht-svc') + retrieved_etht_services = retrieved_etht_services.json + LOGGER.info('etht_services = {:s}'.format(str(retrieved_etht_services))) + with open(DATA_FILE_EXPECTED_ETHT_SERVICES, 'r', encoding='UTF-8') as f: + expected_etht_services = json.load(f) + etht_services_diff = deepdiff.DeepDiff(expected_etht_services, retrieved_etht_services) + if len(etht_services_diff) > 0: + LOGGER.error('PRE ETHT SERVICES - Differences:\n{:s}'.format(str(etht_services_diff.pretty()))) + assert len(etht_services_diff) == 0 + + retrieved_driver_config_rules = sorted(driver.GetConfig(), key=operator.itemgetter(0)) + LOGGER.info('driver_config = {:s}'.format(str(retrieved_driver_config_rules))) + retrieved_driver_config_rules = [ + {'action': 1, 'custom': {'resource_key': resource_key, 'resource_value': resource_value}} + for resource_key, resource_value in retrieved_driver_config_rules + ] + with open(DATA_FILE_CONFIG_RULES, 'r', encoding='UTF-8') as f: + expected_driver_config_rules = sorted(json.load(f), key=lambda cr: cr['custom']['resource_key']) + driver_config_rules_diff = deepdiff.DeepDiff(expected_driver_config_rules, retrieved_driver_config_rules) + if len(driver_config_rules_diff) > 0: + LOGGER.error('PRE DRIVER CONFIG RULES - Differences:\n{:s}'.format(str(driver_config_rules_diff.pretty()))) + assert len(driver_config_rules_diff) == 0 DEVICE_WITH_DECONFIG_RULES = copy.deepcopy(DEVICE) - DEVICE_WITH_DECONFIG_RULES['device_config']['config_rules'].extend(DEVICE_DECONFIG_RULES) + with open(DATA_FILE_DECONFIG_RULES, 'r', encoding='UTF-8') as f: + deconfig_rules = format_custom_config_rules(json.load(f)) + DEVICE_WITH_DECONFIG_RULES['device_config']['config_rules'].extend(deconfig_rules) device_client.ConfigureDevice(Device(**DEVICE_WITH_DECONFIG_RULES)) - # Requires to retrieve data from device; might be slow. Uncomment only when needed and test does not pass directly. - #driver_config = sorted(driver.GetConfig(), key=operator.itemgetter(0)) - #LOGGER.info('driver_config = {:s}'.format(str(driver_config))) - - device_data = context_client.GetDevice(DeviceId(**DEVICE_ID)) - config_rules = [ - (ConfigActionEnum.Name(config_rule.action), config_rule.custom.resource_key, config_rule.custom.resource_value) - for config_rule in device_data.device_config.config_rules - if config_rule.WhichOneof('config_rule') == 'custom' - ] - LOGGER.info('device_data.device_config.config_rules = \n{:s}'.format( - '\n'.join(['{:s} {:s} = {:s}'.format(*config_rule) for config_rule in config_rules]))) - for config_rule in DEVICE_DECONFIG_RULES: - assert 'custom' in config_rule - action_set = ConfigActionEnum.Name(ConfigActionEnum.CONFIGACTION_SET) - config_rule = (action_set, config_rule['custom']['resource_key'], config_rule['custom']['resource_value']) - assert config_rule not in config_rules + retrieved_osu_tunnels = ietf_actn_client.get('/restconf/v2/data/ietf-te:te/tunnels') + retrieved_osu_tunnels = retrieved_osu_tunnels.json + LOGGER.info('osu_tunnels = {:s}'.format(str(retrieved_osu_tunnels))) + expected_osu_tunnels = {'ietf-te:tunnel': []} + osu_tunnels_diff = deepdiff.DeepDiff(expected_osu_tunnels, retrieved_osu_tunnels) + if len(osu_tunnels_diff) > 0: + LOGGER.error('POST OSU TUNNELS - Differences:\n{:s}'.format(str(osu_tunnels_diff.pretty()))) + assert len(osu_tunnels_diff) == 0 + + retrieved_etht_services = ietf_actn_client.get('/restconf/v2/data/ietf-eth-tran-service:etht-svc') + retrieved_etht_services = retrieved_etht_services.json + LOGGER.info('etht_services = {:s}'.format(str(retrieved_etht_services))) + expected_etht_services = {'ietf-eth-tran-service:etht-svc': {'etht-svc-instances': []}} + etht_services_diff = deepdiff.DeepDiff(expected_etht_services, retrieved_etht_services) + if len(etht_services_diff) > 0: + LOGGER.error('POST ETHT SERVICES - Differences:\n{:s}'.format(str(etht_services_diff.pretty()))) + assert len(etht_services_diff) == 0 + + retrieved_driver_config_rules = sorted(driver.GetConfig(), key=operator.itemgetter(0)) + LOGGER.info('retrieved_driver_config_rules = {:s}'.format(str(retrieved_driver_config_rules))) + assert isinstance(retrieved_driver_config_rules, list) + if len(retrieved_driver_config_rules) > 0: + LOGGER.error('POST DRIVER CONFIG RULES - Differences:\n{:s}'.format(str(retrieved_driver_config_rules))) + assert len(retrieved_driver_config_rules) == 0 def test_device_ietf_actn_delete( - device_client : DeviceClient, # pylint: disable=redefined-outer-name - device_service : DeviceService, # pylint: disable=redefined-outer-name + device_client : DeviceClient, # pylint: disable=redefined-outer-name + device_service : DeviceService, # pylint: disable=redefined-outer-name + ietf_actn_sdn_ctrl : GenericRestServer, # pylint: disable=redefined-outer-name ) -> None: device_client.DeleteDevice(DeviceId(**DEVICE_ID)) driver_instance_cache = device_service.device_servicer.driver_instance_cache diff --git a/src/tests/tools/mock_ietf_actn_sdn_ctrl/MockIetfActnSdnCtrl.py b/src/tests/tools/mock_ietf_actn_sdn_ctrl/MockIetfActnSdnCtrl.py index ad49b4a42..c459c294c 100644 --- a/src/tests/tools/mock_ietf_actn_sdn_ctrl/MockIetfActnSdnCtrl.py +++ b/src/tests/tools/mock_ietf_actn_sdn_ctrl/MockIetfActnSdnCtrl.py @@ -58,16 +58,16 @@ def main(): Health, '/' ) api.add_resource( - OsuTunnels, '/ietf-te:tunnel' + OsuTunnels, '/ietf-te:te/tunnels' ) api.add_resource( - OsuTunnel, '/ietf-te:tunnel[name=]' + OsuTunnel, '/ietf-te:te/tunnels/tunnel=""' ) api.add_resource( EthServices, '/ietf-eth-tran-service:etht-svc' ) api.add_resource( - EthService, '/ietf-eth-tran-service:etht-svc/etht-svc-instances[etht-svc-name=]' + EthService, '/ietf-eth-tran-service:etht-svc/etht-svc-instances=""' ) LOGGER.info('Listening on {:s}...'.format(str(STR_ENDPOINT))) diff --git a/src/tests/tools/mock_ietf_actn_sdn_ctrl/ResourceEthServices.py b/src/tests/tools/mock_ietf_actn_sdn_ctrl/ResourceEthServices.py index 4fcc0ca71..0e08bbdaa 100644 --- a/src/tests/tools/mock_ietf_actn_sdn_ctrl/ResourceEthServices.py +++ b/src/tests/tools/mock_ietf_actn_sdn_ctrl/ResourceEthServices.py @@ -18,12 +18,12 @@ from flask import abort, jsonify, make_response, request from flask_restful import Resource -ETH_SERVICES = {} +ETHT_SERVICES = {} class EthServices(Resource): def get(self): - eth_services = [eth_service for eth_service in ETH_SERVICES.values()] - data = {'ietf-eth-tran-service:etht-svc': {'etht-svc-instances': eth_services}} + etht_services = [etht_service for etht_service in ETHT_SERVICES.values()] + data = {'ietf-eth-tran-service:etht-svc': {'etht-svc-instances': etht_services}} return make_response(jsonify(data), 200) def post(self): @@ -33,21 +33,21 @@ class EthServices(Resource): if 'ietf-eth-tran-service:etht-svc' not in json_request: abort(400) json_request = json_request['ietf-eth-tran-service:etht-svc'] if 'etht-svc-instances' not in json_request: abort(400) - eth_services = json_request['etht-svc-instances'] - if not isinstance(eth_services, list): abort(400) - if len(eth_services) != 1: abort(400) - eth_service = eth_services[0] - etht_svc_name = eth_service['etht-svc-name'] - ETH_SERVICES[etht_svc_name] = eth_service + etht_services = json_request['etht-svc-instances'] + if not isinstance(etht_services, list): abort(400) + if len(etht_services) != 1: abort(400) + etht_service = etht_services[0] + etht_service_name = etht_service['etht-svc-name'] + ETHT_SERVICES[etht_service_name] = etht_service return make_response(jsonify({}), 201) class EthService(Resource): - def get(self, service_uuid : str): - eth_service = ETH_SERVICES.get(service_uuid, None) - data,status = ({}, 404) if eth_service is None else (eth_service, 200) + def get(self, etht_service_name : str): + etht_service = ETHT_SERVICES.get(etht_service_name, None) + data,status = ({}, 404) if etht_service is None else (etht_service, 200) return make_response(jsonify(data), status) - def delete(self, service_uuid : str): - eth_service = ETH_SERVICES.pop(service_uuid, None) - data,status = ({}, 404) if eth_service is None else (eth_service, 204) + def delete(self, etht_service_name : str): + etht_service = ETHT_SERVICES.pop(etht_service_name, None) + data,status = ({}, 404) if etht_service is None else (etht_service, 204) return make_response(jsonify(data), status) diff --git a/src/tests/tools/mock_ietf_actn_sdn_ctrl/ResourceOsuTunnels.py b/src/tests/tools/mock_ietf_actn_sdn_ctrl/ResourceOsuTunnels.py index 2fe2f319b..914f7096d 100644 --- a/src/tests/tools/mock_ietf_actn_sdn_ctrl/ResourceOsuTunnels.py +++ b/src/tests/tools/mock_ietf_actn_sdn_ctrl/ResourceOsuTunnels.py @@ -31,12 +31,14 @@ class OsuTunnels(Resource): def post(self): json_request = request.get_json() if not json_request: abort(400) - if not isinstance(json_request, list): abort(400) + if not isinstance(json_request, dict): abort(400) + if 'ietf-te:tunnel' not in json_request: abort(400) osu_tunnels = json_request['ietf-te:tunnel'] + if not isinstance(osu_tunnels, list): abort(400) if len(osu_tunnels) != 1: abort(400) osu_tunnel = osu_tunnels[0] - name = osu_tunnel['name'] - OSU_TUNNELS[name] = osu_tunnel + osu_tunnel_name = osu_tunnel['name'] + OSU_TUNNELS[osu_tunnel_name] = osu_tunnel return make_response(jsonify({}), 201) class OsuTunnel(Resource): -- GitLab From 2acda8b819eb9070837fe7d4f181bb09066cb925 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Fri, 26 Jan 2024 13:15:02 +0000 Subject: [PATCH 13/18] Device - IETF ACTN Driver: - Added unitary test to GitLab CI/CD pipeline --- src/device/.gitlab-ci.yml | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/device/.gitlab-ci.yml b/src/device/.gitlab-ci.yml index 5fed57be6..bcc2e05e5 100644 --- a/src/device/.gitlab-ci.yml +++ b/src/device/.gitlab-ci.yml @@ -48,15 +48,26 @@ unit_test device: - build device before_script: - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY - - if docker network list | grep teraflowbridge; then echo "teraflowbridge is already created"; else docker network create -d bridge teraflowbridge; fi - - if docker container ls | grep $IMAGE_NAME; then docker rm -f $IMAGE_NAME; else echo "$IMAGE_NAME image is not in the system"; fi + - > + if docker network list | grep teraflowbridge; then + echo "teraflowbridge is already created"; + else + docker network create -d bridge teraflowbridge; + fi + - > + if docker container ls | grep $IMAGE_NAME; then + docker rm -f $IMAGE_NAME; + else + echo "$IMAGE_NAME image is not in the system"; + fi script: - docker pull "$CI_REGISTRY_IMAGE/$IMAGE_NAME:$IMAGE_TAG" - docker run --name $IMAGE_NAME -d -p 2020:2020 -v "$PWD/src/$IMAGE_NAME/tests:/opt/results" --network=teraflowbridge $CI_REGISTRY_IMAGE/$IMAGE_NAME:$IMAGE_TAG - sleep 5 - docker ps -a - docker logs $IMAGE_NAME - - docker exec -i $IMAGE_NAME bash -c "coverage run -m pytest --log-level=INFO --verbose $IMAGE_NAME/tests/test_unitary_emulated.py --junitxml=/opt/results/${IMAGE_NAME}_report.xml" + - docker exec -i $IMAGE_NAME bash -c "coverage run --append -m pytest --log-level=INFO --verbose $IMAGE_NAME/tests/test_unitary_emulated.py --junitxml=/opt/results/${IMAGE_NAME}_report_emulated.xml" + - docker exec -i $IMAGE_NAME bash -c "coverage run --append -m pytest --log-level=INFO --verbose $IMAGE_NAME/tests/test_unitary_ietf_actn.py --junitxml=/opt/results/${IMAGE_NAME}_report_ietf_actn.xml" - docker exec -i $IMAGE_NAME bash -c "coverage report --include='${IMAGE_NAME}/*' --show-missing" coverage: '/TOTAL\s+\d+\s+\d+\s+(\d+%)/' after_script: @@ -77,7 +88,7 @@ unit_test device: artifacts: when: always reports: - junit: src/$IMAGE_NAME/tests/${IMAGE_NAME}_report.xml + junit: src/$IMAGE_NAME/tests/${IMAGE_NAME}_report_*.xml ## Deployment of the service in Kubernetes Cluster #deploy device: -- GitLab From d95bc9e22e5a38e21e5f18f59d00e99c93296cb2 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Fri, 26 Jan 2024 15:58:37 +0000 Subject: [PATCH 14/18] Device - IETF ACTN Driver: - Added required Python dependencies --- src/device/requirements.in | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/device/requirements.in b/src/device/requirements.in index 46f4a7518..1a09542a3 100644 --- a/src/device/requirements.in +++ b/src/device/requirements.in @@ -19,6 +19,9 @@ cryptography==36.0.2 deepdiff==6.7.* deepmerge==1.1.* #fastcache==1.1.0 +Flask==2.1.3 +Flask-HTTPAuth==4.5.0 +Flask-RESTful==0.3.9 Jinja2==3.0.3 ncclient==0.6.13 p4runtime==1.3.0 @@ -34,9 +37,10 @@ tabulate ipaddress macaddress yattag -pyang +pyang==2.6.0 git+https://github.com/robshakir/pyangbind.git websockets==10.4 +werkzeug==2.3.7 # pip's dependency resolver does not take into account installed packages. # p4runtime does not specify the version of grpcio/protobuf it needs, so it tries to install latest one -- GitLab From 5f8866be9cb91871607627819258b0b375410467 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Fri, 26 Jan 2024 16:12:01 +0000 Subject: [PATCH 15/18] Device - IETF ACTN Driver: - Fixed dependencies of unitary test for GitLab CI/CD pipeline --- src/device/Dockerfile | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/device/Dockerfile b/src/device/Dockerfile index 656662552..909ae3bd3 100644 --- a/src/device/Dockerfile +++ b/src/device/Dockerfile @@ -66,5 +66,11 @@ COPY src/context/. context/ COPY src/device/. device/ COPY src/monitoring/. monitoring/ +RUN mkdir -p tests/tools/mock_ietf_actn_sdn_ctrl +RUN touch tests/__init__.py +RUN touch tests/tools/__init__.py +RUN touch tests/tools/mock_ietf_actn_sdn_ctrl/__init__.py +COPY src/tests/tools/mock_ietf_actn_sdn_ctrl/. tests/tools/mock_ietf_actn_sdn_ctrl/ + # Start the service ENTRYPOINT ["python", "-m", "device.service"] -- GitLab From 0397b7c58263b07184c83e7f55e360ce14bba7e7 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Fri, 26 Jan 2024 16:42:52 +0000 Subject: [PATCH 16/18] Policy and ZTP components: - Fixed new device drivers added IETF ACTN, gNMI OpenConfig, FlexScale - Updated generated proto code --- .../context/model/DeviceDriverEnum.java | 5 +- .../org/etsi/tfs/policy/SerializerTest.java | 6 +- .../grpc/context/ContextOuterClass.java | 233 ++++++++++-------- src/policy/target/kubernetes/kubernetes.yml | 32 +-- .../ztp/context/model/DeviceDriverEnum.java | 5 +- .../java/org/etsi/tfs/ztp/SerializerTest.java | 6 +- .../grpc/context/ContextOuterClass.java | 233 ++++++++++-------- src/ztp/target/kubernetes/kubernetes.yml | 24 +- 8 files changed, 302 insertions(+), 242 deletions(-) diff --git a/src/policy/src/main/java/org/etsi/tfs/policy/context/model/DeviceDriverEnum.java b/src/policy/src/main/java/org/etsi/tfs/policy/context/model/DeviceDriverEnum.java index 63e96a4c6..72a1d7136 100644 --- a/src/policy/src/main/java/org/etsi/tfs/policy/context/model/DeviceDriverEnum.java +++ b/src/policy/src/main/java/org/etsi/tfs/policy/context/model/DeviceDriverEnum.java @@ -24,5 +24,8 @@ public enum DeviceDriverEnum { IETF_NETWORK_TOPOLOGY, ONF_TR_532, XR, - IETF_L2VPN + IETF_L2VPN, + GNMI_OPENCONFIG, + FLEXSCALE, + IETF_ACTN } diff --git a/src/policy/src/test/java/org/etsi/tfs/policy/SerializerTest.java b/src/policy/src/test/java/org/etsi/tfs/policy/SerializerTest.java index df2ae46cf..63fb1ad7a 100644 --- a/src/policy/src/test/java/org/etsi/tfs/policy/SerializerTest.java +++ b/src/policy/src/test/java/org/etsi/tfs/policy/SerializerTest.java @@ -3618,11 +3618,9 @@ class SerializerTest { DeviceDriverEnum.GNMI_OPENCONFIG, ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_GNMI_OPENCONFIG), Arguments.of( - DeviceDriverEnum.FLEXSCALE, - ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_FLEXSCALE), + DeviceDriverEnum.FLEXSCALE, ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_FLEXSCALE), Arguments.of( - DeviceDriverEnum.IETF_ACTN, - ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_IETF_ACTN), + DeviceDriverEnum.IETF_ACTN, ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_IETF_ACTN), Arguments.of( DeviceDriverEnum.UNDEFINED, ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_UNDEFINED)); } diff --git a/src/policy/target/generated-sources/grpc/context/ContextOuterClass.java b/src/policy/target/generated-sources/grpc/context/ContextOuterClass.java index a25798b88..d4873899b 100644 --- a/src/policy/target/generated-sources/grpc/context/ContextOuterClass.java +++ b/src/policy/target/generated-sources/grpc/context/ContextOuterClass.java @@ -185,6 +185,14 @@ public final class ContextOuterClass { * DEVICEDRIVER_GNMI_OPENCONFIG = 8; */ DEVICEDRIVER_GNMI_OPENCONFIG(8), + /** + * DEVICEDRIVER_FLEXSCALE = 9; + */ + DEVICEDRIVER_FLEXSCALE(9), + /** + * DEVICEDRIVER_IETF_ACTN = 10; + */ + DEVICEDRIVER_IETF_ACTN(10), UNRECOGNIZED(-1), ; @@ -228,6 +236,14 @@ public final class ContextOuterClass { * DEVICEDRIVER_GNMI_OPENCONFIG = 8; */ public static final int DEVICEDRIVER_GNMI_OPENCONFIG_VALUE = 8; + /** + * DEVICEDRIVER_FLEXSCALE = 9; + */ + public static final int DEVICEDRIVER_FLEXSCALE_VALUE = 9; + /** + * DEVICEDRIVER_IETF_ACTN = 10; + */ + public static final int DEVICEDRIVER_IETF_ACTN_VALUE = 10; public final int getNumber() { @@ -263,6 +279,8 @@ public final class ContextOuterClass { case 6: return DEVICEDRIVER_XR; case 7: return DEVICEDRIVER_IETF_L2VPN; case 8: return DEVICEDRIVER_GNMI_OPENCONFIG; + case 9: return DEVICEDRIVER_FLEXSCALE; + case 10: return DEVICEDRIVER_IETF_ACTN; default: return null; } } @@ -461,6 +479,10 @@ public final class ContextOuterClass { * SERVICETYPE_TE = 4; */ SERVICETYPE_TE(4), + /** + * SERVICETYPE_E2E = 5; + */ + SERVICETYPE_E2E(5), UNRECOGNIZED(-1), ; @@ -484,6 +506,10 @@ public final class ContextOuterClass { * SERVICETYPE_TE = 4; */ public static final int SERVICETYPE_TE_VALUE = 4; + /** + * SERVICETYPE_E2E = 5; + */ + public static final int SERVICETYPE_E2E_VALUE = 5; public final int getNumber() { @@ -515,6 +541,7 @@ public final class ContextOuterClass { case 2: return SERVICETYPE_L2NM; case 3: return SERVICETYPE_TAPI_CONNECTIVITY_SERVICE; case 4: return SERVICETYPE_TE; + case 5: return SERVICETYPE_E2E; default: return null; } } @@ -75826,114 +75853,116 @@ public final class ContextOuterClass { "\0132\022.context.ContextId\022\025\n\rauthenticated\030\002" + " \001(\010*j\n\rEventTypeEnum\022\027\n\023EVENTTYPE_UNDEF" + "INED\020\000\022\024\n\020EVENTTYPE_CREATE\020\001\022\024\n\020EVENTTYP" + - "E_UPDATE\020\002\022\024\n\020EVENTTYPE_REMOVE\020\003*\231\002\n\020Dev" + + "E_UPDATE\020\002\022\024\n\020EVENTTYPE_REMOVE\020\003*\321\002\n\020Dev" + "iceDriverEnum\022\032\n\026DEVICEDRIVER_UNDEFINED\020" + "\000\022\033\n\027DEVICEDRIVER_OPENCONFIG\020\001\022\036\n\032DEVICE" + "DRIVER_TRANSPORT_API\020\002\022\023\n\017DEVICEDRIVER_P" + "4\020\003\022&\n\"DEVICEDRIVER_IETF_NETWORK_TOPOLOG" + "Y\020\004\022\033\n\027DEVICEDRIVER_ONF_TR_532\020\005\022\023\n\017DEVI" + "CEDRIVER_XR\020\006\022\033\n\027DEVICEDRIVER_IETF_L2VPN" + - "\020\007\022 \n\034DEVICEDRIVER_GNMI_OPENCONFIG\020\010*\217\001\n" + - "\033DeviceOperationalStatusEnum\022%\n!DEVICEOP" + - "ERATIONALSTATUS_UNDEFINED\020\000\022$\n DEVICEOPE" + - "RATIONALSTATUS_DISABLED\020\001\022#\n\037DEVICEOPERA" + - "TIONALSTATUS_ENABLED\020\002*\225\001\n\017ServiceTypeEn" + - "um\022\027\n\023SERVICETYPE_UNKNOWN\020\000\022\024\n\020SERVICETY" + - "PE_L3NM\020\001\022\024\n\020SERVICETYPE_L2NM\020\002\022)\n%SERVI" + - "CETYPE_TAPI_CONNECTIVITY_SERVICE\020\003\022\022\n\016SE" + - "RVICETYPE_TE\020\004*\304\001\n\021ServiceStatusEnum\022\033\n\027" + - "SERVICESTATUS_UNDEFINED\020\000\022\031\n\025SERVICESTAT" + - "US_PLANNED\020\001\022\030\n\024SERVICESTATUS_ACTIVE\020\002\022\032" + - "\n\026SERVICESTATUS_UPDATING\020\003\022!\n\035SERVICESTA" + - "TUS_PENDING_REMOVAL\020\004\022\036\n\032SERVICESTATUS_S" + - "LA_VIOLATED\020\005*\251\001\n\017SliceStatusEnum\022\031\n\025SLI" + - "CESTATUS_UNDEFINED\020\000\022\027\n\023SLICESTATUS_PLAN" + - "NED\020\001\022\024\n\020SLICESTATUS_INIT\020\002\022\026\n\022SLICESTAT" + - "US_ACTIVE\020\003\022\026\n\022SLICESTATUS_DEINIT\020\004\022\034\n\030S" + - "LICESTATUS_SLA_VIOLATED\020\005*]\n\020ConfigActio" + - "nEnum\022\032\n\026CONFIGACTION_UNDEFINED\020\000\022\024\n\020CON" + - "FIGACTION_SET\020\001\022\027\n\023CONFIGACTION_DELETE\020\002" + - "*m\n\024ConstraintActionEnum\022\036\n\032CONSTRAINTAC" + - "TION_UNDEFINED\020\000\022\030\n\024CONSTRAINTACTION_SET" + - "\020\001\022\033\n\027CONSTRAINTACTION_DELETE\020\002*\203\002\n\022Isol" + - "ationLevelEnum\022\020\n\014NO_ISOLATION\020\000\022\026\n\022PHYS" + - "ICAL_ISOLATION\020\001\022\025\n\021LOGICAL_ISOLATION\020\002\022" + - "\025\n\021PROCESS_ISOLATION\020\003\022\035\n\031PHYSICAL_MEMOR" + - "Y_ISOLATION\020\004\022\036\n\032PHYSICAL_NETWORK_ISOLAT" + - "ION\020\005\022\036\n\032VIRTUAL_RESOURCE_ISOLATION\020\006\022\037\n" + - "\033NETWORK_FUNCTIONS_ISOLATION\020\007\022\025\n\021SERVIC" + - "E_ISOLATION\020\0102\245\026\n\016ContextService\022:\n\016List" + - "ContextIds\022\016.context.Empty\032\026.context.Con" + - "textIdList\"\000\0226\n\014ListContexts\022\016.context.E" + - "mpty\032\024.context.ContextList\"\000\0224\n\nGetConte" + - "xt\022\022.context.ContextId\032\020.context.Context" + - "\"\000\0224\n\nSetContext\022\020.context.Context\032\022.con" + - "text.ContextId\"\000\0225\n\rRemoveContext\022\022.cont" + - "ext.ContextId\032\016.context.Empty\"\000\022=\n\020GetCo" + - "ntextEvents\022\016.context.Empty\032\025.context.Co" + - "ntextEvent\"\0000\001\022@\n\017ListTopologyIds\022\022.cont" + - "ext.ContextId\032\027.context.TopologyIdList\"\000" + - "\022=\n\016ListTopologies\022\022.context.ContextId\032\025" + - ".context.TopologyList\"\000\0227\n\013GetTopology\022\023" + - ".context.TopologyId\032\021.context.Topology\"\000" + - "\022E\n\022GetTopologyDetails\022\023.context.Topolog" + - "yId\032\030.context.TopologyDetails\"\000\0227\n\013SetTo" + - "pology\022\021.context.Topology\032\023.context.Topo" + - "logyId\"\000\0227\n\016RemoveTopology\022\023.context.Top" + - "ologyId\032\016.context.Empty\"\000\022?\n\021GetTopology" + - "Events\022\016.context.Empty\032\026.context.Topolog" + - "yEvent\"\0000\001\0228\n\rListDeviceIds\022\016.context.Em" + - "pty\032\025.context.DeviceIdList\"\000\0224\n\013ListDevi" + - "ces\022\016.context.Empty\032\023.context.DeviceList" + - "\"\000\0221\n\tGetDevice\022\021.context.DeviceId\032\017.con" + - "text.Device\"\000\0221\n\tSetDevice\022\017.context.Dev" + - "ice\032\021.context.DeviceId\"\000\0223\n\014RemoveDevice" + - "\022\021.context.DeviceId\032\016.context.Empty\"\000\022;\n" + - "\017GetDeviceEvents\022\016.context.Empty\032\024.conte" + - "xt.DeviceEvent\"\0000\001\022<\n\014SelectDevice\022\025.con" + - "text.DeviceFilter\032\023.context.DeviceList\"\000" + - "\022I\n\021ListEndPointNames\022\027.context.EndPoint" + - "IdList\032\031.context.EndPointNameList\"\000\0224\n\013L" + - "istLinkIds\022\016.context.Empty\032\023.context.Lin" + - "kIdList\"\000\0220\n\tListLinks\022\016.context.Empty\032\021" + - ".context.LinkList\"\000\022+\n\007GetLink\022\017.context" + - ".LinkId\032\r.context.Link\"\000\022+\n\007SetLink\022\r.co" + - "ntext.Link\032\017.context.LinkId\"\000\022/\n\nRemoveL" + - "ink\022\017.context.LinkId\032\016.context.Empty\"\000\0227" + - "\n\rGetLinkEvents\022\016.context.Empty\032\022.contex" + - "t.LinkEvent\"\0000\001\022>\n\016ListServiceIds\022\022.cont" + - "ext.ContextId\032\026.context.ServiceIdList\"\000\022" + - ":\n\014ListServices\022\022.context.ContextId\032\024.co" + - "ntext.ServiceList\"\000\0224\n\nGetService\022\022.cont" + - "ext.ServiceId\032\020.context.Service\"\000\0224\n\nSet" + - "Service\022\020.context.Service\032\022.context.Serv" + - "iceId\"\000\0226\n\014UnsetService\022\020.context.Servic" + - "e\032\022.context.ServiceId\"\000\0225\n\rRemoveService" + - "\022\022.context.ServiceId\032\016.context.Empty\"\000\022=" + - "\n\020GetServiceEvents\022\016.context.Empty\032\025.con" + - "text.ServiceEvent\"\0000\001\022?\n\rSelectService\022\026" + - ".context.ServiceFilter\032\024.context.Service" + - "List\"\000\022:\n\014ListSliceIds\022\022.context.Context" + - "Id\032\024.context.SliceIdList\"\000\0226\n\nListSlices" + - "\022\022.context.ContextId\032\022.context.SliceList" + - "\"\000\022.\n\010GetSlice\022\020.context.SliceId\032\016.conte" + - "xt.Slice\"\000\022.\n\010SetSlice\022\016.context.Slice\032\020" + - ".context.SliceId\"\000\0220\n\nUnsetSlice\022\016.conte" + - "xt.Slice\032\020.context.SliceId\"\000\0221\n\013RemoveSl" + - "ice\022\020.context.SliceId\032\016.context.Empty\"\000\022" + - "9\n\016GetSliceEvents\022\016.context.Empty\032\023.cont" + - "ext.SliceEvent\"\0000\001\0229\n\013SelectSlice\022\024.cont" + - "ext.SliceFilter\032\022.context.SliceList\"\000\022D\n" + - "\021ListConnectionIds\022\022.context.ServiceId\032\031" + - ".context.ConnectionIdList\"\000\022@\n\017ListConne" + - "ctions\022\022.context.ServiceId\032\027.context.Con" + - "nectionList\"\000\022=\n\rGetConnection\022\025.context" + - ".ConnectionId\032\023.context.Connection\"\000\022=\n\r" + - "SetConnection\022\023.context.Connection\032\025.con" + - "text.ConnectionId\"\000\022;\n\020RemoveConnection\022" + - "\025.context.ConnectionId\032\016.context.Empty\"\000" + - "\022C\n\023GetConnectionEvents\022\016.context.Empty\032" + - "\030.context.ConnectionEvent\"\0000\001b\006proto3" + "\020\007\022 \n\034DEVICEDRIVER_GNMI_OPENCONFIG\020\010\022\032\n\026" + + "DEVICEDRIVER_FLEXSCALE\020\t\022\032\n\026DEVICEDRIVER" + + "_IETF_ACTN\020\n*\217\001\n\033DeviceOperationalStatus" + + "Enum\022%\n!DEVICEOPERATIONALSTATUS_UNDEFINE" + + "D\020\000\022$\n DEVICEOPERATIONALSTATUS_DISABLED\020" + + "\001\022#\n\037DEVICEOPERATIONALSTATUS_ENABLED\020\002*\252" + + "\001\n\017ServiceTypeEnum\022\027\n\023SERVICETYPE_UNKNOW" + + "N\020\000\022\024\n\020SERVICETYPE_L3NM\020\001\022\024\n\020SERVICETYPE" + + "_L2NM\020\002\022)\n%SERVICETYPE_TAPI_CONNECTIVITY" + + "_SERVICE\020\003\022\022\n\016SERVICETYPE_TE\020\004\022\023\n\017SERVIC" + + "ETYPE_E2E\020\005*\304\001\n\021ServiceStatusEnum\022\033\n\027SER" + + "VICESTATUS_UNDEFINED\020\000\022\031\n\025SERVICESTATUS_" + + "PLANNED\020\001\022\030\n\024SERVICESTATUS_ACTIVE\020\002\022\032\n\026S" + + "ERVICESTATUS_UPDATING\020\003\022!\n\035SERVICESTATUS" + + "_PENDING_REMOVAL\020\004\022\036\n\032SERVICESTATUS_SLA_" + + "VIOLATED\020\005*\251\001\n\017SliceStatusEnum\022\031\n\025SLICES" + + "TATUS_UNDEFINED\020\000\022\027\n\023SLICESTATUS_PLANNED" + + "\020\001\022\024\n\020SLICESTATUS_INIT\020\002\022\026\n\022SLICESTATUS_" + + "ACTIVE\020\003\022\026\n\022SLICESTATUS_DEINIT\020\004\022\034\n\030SLIC" + + "ESTATUS_SLA_VIOLATED\020\005*]\n\020ConfigActionEn" + + "um\022\032\n\026CONFIGACTION_UNDEFINED\020\000\022\024\n\020CONFIG" + + "ACTION_SET\020\001\022\027\n\023CONFIGACTION_DELETE\020\002*m\n" + + "\024ConstraintActionEnum\022\036\n\032CONSTRAINTACTIO" + + "N_UNDEFINED\020\000\022\030\n\024CONSTRAINTACTION_SET\020\001\022" + + "\033\n\027CONSTRAINTACTION_DELETE\020\002*\203\002\n\022Isolati" + + "onLevelEnum\022\020\n\014NO_ISOLATION\020\000\022\026\n\022PHYSICA" + + "L_ISOLATION\020\001\022\025\n\021LOGICAL_ISOLATION\020\002\022\025\n\021" + + "PROCESS_ISOLATION\020\003\022\035\n\031PHYSICAL_MEMORY_I" + + "SOLATION\020\004\022\036\n\032PHYSICAL_NETWORK_ISOLATION" + + "\020\005\022\036\n\032VIRTUAL_RESOURCE_ISOLATION\020\006\022\037\n\033NE" + + "TWORK_FUNCTIONS_ISOLATION\020\007\022\025\n\021SERVICE_I" + + "SOLATION\020\0102\245\026\n\016ContextService\022:\n\016ListCon" + + "textIds\022\016.context.Empty\032\026.context.Contex" + + "tIdList\"\000\0226\n\014ListContexts\022\016.context.Empt" + + "y\032\024.context.ContextList\"\000\0224\n\nGetContext\022" + + "\022.context.ContextId\032\020.context.Context\"\000\022" + + "4\n\nSetContext\022\020.context.Context\032\022.contex" + + "t.ContextId\"\000\0225\n\rRemoveContext\022\022.context" + + ".ContextId\032\016.context.Empty\"\000\022=\n\020GetConte" + + "xtEvents\022\016.context.Empty\032\025.context.Conte" + + "xtEvent\"\0000\001\022@\n\017ListTopologyIds\022\022.context" + + ".ContextId\032\027.context.TopologyIdList\"\000\022=\n" + + "\016ListTopologies\022\022.context.ContextId\032\025.co" + + "ntext.TopologyList\"\000\0227\n\013GetTopology\022\023.co" + + "ntext.TopologyId\032\021.context.Topology\"\000\022E\n" + + "\022GetTopologyDetails\022\023.context.TopologyId" + + "\032\030.context.TopologyDetails\"\000\0227\n\013SetTopol" + + "ogy\022\021.context.Topology\032\023.context.Topolog" + + "yId\"\000\0227\n\016RemoveTopology\022\023.context.Topolo" + + "gyId\032\016.context.Empty\"\000\022?\n\021GetTopologyEve" + + "nts\022\016.context.Empty\032\026.context.TopologyEv" + + "ent\"\0000\001\0228\n\rListDeviceIds\022\016.context.Empty" + + "\032\025.context.DeviceIdList\"\000\0224\n\013ListDevices" + + "\022\016.context.Empty\032\023.context.DeviceList\"\000\022" + + "1\n\tGetDevice\022\021.context.DeviceId\032\017.contex" + + "t.Device\"\000\0221\n\tSetDevice\022\017.context.Device" + + "\032\021.context.DeviceId\"\000\0223\n\014RemoveDevice\022\021." + + "context.DeviceId\032\016.context.Empty\"\000\022;\n\017Ge" + + "tDeviceEvents\022\016.context.Empty\032\024.context." + + "DeviceEvent\"\0000\001\022<\n\014SelectDevice\022\025.contex" + + "t.DeviceFilter\032\023.context.DeviceList\"\000\022I\n" + + "\021ListEndPointNames\022\027.context.EndPointIdL" + + "ist\032\031.context.EndPointNameList\"\000\0224\n\013List" + + "LinkIds\022\016.context.Empty\032\023.context.LinkId" + + "List\"\000\0220\n\tListLinks\022\016.context.Empty\032\021.co" + + "ntext.LinkList\"\000\022+\n\007GetLink\022\017.context.Li" + + "nkId\032\r.context.Link\"\000\022+\n\007SetLink\022\r.conte" + + "xt.Link\032\017.context.LinkId\"\000\022/\n\nRemoveLink" + + "\022\017.context.LinkId\032\016.context.Empty\"\000\0227\n\rG" + + "etLinkEvents\022\016.context.Empty\032\022.context.L" + + "inkEvent\"\0000\001\022>\n\016ListServiceIds\022\022.context" + + ".ContextId\032\026.context.ServiceIdList\"\000\022:\n\014" + + "ListServices\022\022.context.ContextId\032\024.conte" + + "xt.ServiceList\"\000\0224\n\nGetService\022\022.context" + + ".ServiceId\032\020.context.Service\"\000\0224\n\nSetSer" + + "vice\022\020.context.Service\032\022.context.Service" + + "Id\"\000\0226\n\014UnsetService\022\020.context.Service\032\022" + + ".context.ServiceId\"\000\0225\n\rRemoveService\022\022." + + "context.ServiceId\032\016.context.Empty\"\000\022=\n\020G" + + "etServiceEvents\022\016.context.Empty\032\025.contex" + + "t.ServiceEvent\"\0000\001\022?\n\rSelectService\022\026.co" + + "ntext.ServiceFilter\032\024.context.ServiceLis" + + "t\"\000\022:\n\014ListSliceIds\022\022.context.ContextId\032" + + "\024.context.SliceIdList\"\000\0226\n\nListSlices\022\022." + + "context.ContextId\032\022.context.SliceList\"\000\022" + + ".\n\010GetSlice\022\020.context.SliceId\032\016.context." + + "Slice\"\000\022.\n\010SetSlice\022\016.context.Slice\032\020.co" + + "ntext.SliceId\"\000\0220\n\nUnsetSlice\022\016.context." + + "Slice\032\020.context.SliceId\"\000\0221\n\013RemoveSlice" + + "\022\020.context.SliceId\032\016.context.Empty\"\000\0229\n\016" + + "GetSliceEvents\022\016.context.Empty\032\023.context" + + ".SliceEvent\"\0000\001\0229\n\013SelectSlice\022\024.context" + + ".SliceFilter\032\022.context.SliceList\"\000\022D\n\021Li" + + "stConnectionIds\022\022.context.ServiceId\032\031.co" + + "ntext.ConnectionIdList\"\000\022@\n\017ListConnecti" + + "ons\022\022.context.ServiceId\032\027.context.Connec" + + "tionList\"\000\022=\n\rGetConnection\022\025.context.Co" + + "nnectionId\032\023.context.Connection\"\000\022=\n\rSet" + + "Connection\022\023.context.Connection\032\025.contex" + + "t.ConnectionId\"\000\022;\n\020RemoveConnection\022\025.c" + + "ontext.ConnectionId\032\016.context.Empty\"\000\022C\n" + + "\023GetConnectionEvents\022\016.context.Empty\032\030.c" + + "ontext.ConnectionEvent\"\0000\001b\006proto3" }; descriptor = com.google.protobuf.Descriptors.FileDescriptor .internalBuildGeneratedFileFrom(descriptorData, diff --git a/src/policy/target/kubernetes/kubernetes.yml b/src/policy/target/kubernetes/kubernetes.yml index 5cd1f1c4c..55847f89e 100644 --- a/src/policy/target/kubernetes/kubernetes.yml +++ b/src/policy/target/kubernetes/kubernetes.yml @@ -3,8 +3,8 @@ apiVersion: v1 kind: Service metadata: annotations: - app.quarkus.io/commit-id: 46486023929121fc955e9550fc8fd625ded433d2 - app.quarkus.io/build-timestamp: 2023-12-15 - 11:56:20 +0000 + app.quarkus.io/commit-id: 5f8866be9cb91871607627819258b0b375410467 + app.quarkus.io/build-timestamp: 2024-01-26 - 16:40:15 +0000 prometheus.io/scrape: "true" prometheus.io/path: /q/metrics prometheus.io/port: "8080" @@ -15,12 +15,12 @@ metadata: name: policyservice spec: ports: - - name: grpc-server - port: 6060 - targetPort: 6060 - name: http port: 9192 targetPort: 8080 + - name: grpc-server + port: 6060 + targetPort: 6060 selector: app.kubernetes.io/name: policyservice type: ClusterIP @@ -29,8 +29,8 @@ apiVersion: apps/v1 kind: Deployment metadata: annotations: - app.quarkus.io/commit-id: 46486023929121fc955e9550fc8fd625ded433d2 - app.quarkus.io/build-timestamp: 2023-12-15 - 11:56:20 +0000 + app.quarkus.io/commit-id: 5f8866be9cb91871607627819258b0b375410467 + app.quarkus.io/build-timestamp: 2024-01-26 - 16:40:15 +0000 prometheus.io/scrape: "true" prometheus.io/path: /q/metrics prometheus.io/port: "8080" @@ -47,8 +47,8 @@ spec: template: metadata: annotations: - app.quarkus.io/commit-id: 46486023929121fc955e9550fc8fd625ded433d2 - app.quarkus.io/build-timestamp: 2023-12-15 - 11:56:20 +0000 + app.quarkus.io/commit-id: 5f8866be9cb91871607627819258b0b375410467 + app.quarkus.io/build-timestamp: 2024-01-26 - 16:40:15 +0000 prometheus.io/scrape: "true" prometheus.io/path: /q/metrics prometheus.io/port: "8080" @@ -63,12 +63,12 @@ spec: valueFrom: fieldRef: fieldPath: metadata.namespace - - name: CONTEXT_SERVICE_HOST - value: contextservice - - name: MONITORING_SERVICE_HOST - value: monitoringservice - name: SERVICE_SERVICE_HOST value: serviceservice + - name: MONITORING_SERVICE_HOST + value: monitoringservice + - name: CONTEXT_SERVICE_HOST + value: contextservice image: labs.etsi.org:5050/tfs/controller/policy:0.1.0 imagePullPolicy: Always livenessProbe: @@ -83,12 +83,12 @@ spec: timeoutSeconds: 10 name: policyservice ports: - - containerPort: 6060 - name: grpc-server - protocol: TCP - containerPort: 8080 name: http protocol: TCP + - containerPort: 6060 + name: grpc-server + protocol: TCP readinessProbe: failureThreshold: 3 httpGet: diff --git a/src/ztp/src/main/java/org/etsi/tfs/ztp/context/model/DeviceDriverEnum.java b/src/ztp/src/main/java/org/etsi/tfs/ztp/context/model/DeviceDriverEnum.java index 7c87b0638..8e89be8a6 100644 --- a/src/ztp/src/main/java/org/etsi/tfs/ztp/context/model/DeviceDriverEnum.java +++ b/src/ztp/src/main/java/org/etsi/tfs/ztp/context/model/DeviceDriverEnum.java @@ -24,5 +24,8 @@ public enum DeviceDriverEnum { IETF_NETWORK_TOPOLOGY, ONF_TR_532, XR, - IETF_L2VPN + IETF_L2VPN, + GNMI_OPENCONFIG, + FLEXSCALE, + IETF_ACTN } diff --git a/src/ztp/src/test/java/org/etsi/tfs/ztp/SerializerTest.java b/src/ztp/src/test/java/org/etsi/tfs/ztp/SerializerTest.java index 37c6df36d..67048119d 100644 --- a/src/ztp/src/test/java/org/etsi/tfs/ztp/SerializerTest.java +++ b/src/ztp/src/test/java/org/etsi/tfs/ztp/SerializerTest.java @@ -1227,11 +1227,9 @@ class SerializerTest { DeviceDriverEnum.GNMI_OPENCONFIG, ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_GNMI_OPENCONFIG), Arguments.of( - DeviceDriverEnum.FLEXSCALE, - ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_FLEXSCALE), + DeviceDriverEnum.FLEXSCALE, ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_FLEXSCALE), Arguments.of( - DeviceDriverEnum.IETF_ACTN, - ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_IETF_ACTN), + DeviceDriverEnum.IETF_ACTN, ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_IETF_ACTN), Arguments.of( DeviceDriverEnum.UNDEFINED, ContextOuterClass.DeviceDriverEnum.DEVICEDRIVER_UNDEFINED)); } diff --git a/src/ztp/target/generated-sources/grpc/context/ContextOuterClass.java b/src/ztp/target/generated-sources/grpc/context/ContextOuterClass.java index a25798b88..d4873899b 100644 --- a/src/ztp/target/generated-sources/grpc/context/ContextOuterClass.java +++ b/src/ztp/target/generated-sources/grpc/context/ContextOuterClass.java @@ -185,6 +185,14 @@ public final class ContextOuterClass { * DEVICEDRIVER_GNMI_OPENCONFIG = 8; */ DEVICEDRIVER_GNMI_OPENCONFIG(8), + /** + * DEVICEDRIVER_FLEXSCALE = 9; + */ + DEVICEDRIVER_FLEXSCALE(9), + /** + * DEVICEDRIVER_IETF_ACTN = 10; + */ + DEVICEDRIVER_IETF_ACTN(10), UNRECOGNIZED(-1), ; @@ -228,6 +236,14 @@ public final class ContextOuterClass { * DEVICEDRIVER_GNMI_OPENCONFIG = 8; */ public static final int DEVICEDRIVER_GNMI_OPENCONFIG_VALUE = 8; + /** + * DEVICEDRIVER_FLEXSCALE = 9; + */ + public static final int DEVICEDRIVER_FLEXSCALE_VALUE = 9; + /** + * DEVICEDRIVER_IETF_ACTN = 10; + */ + public static final int DEVICEDRIVER_IETF_ACTN_VALUE = 10; public final int getNumber() { @@ -263,6 +279,8 @@ public final class ContextOuterClass { case 6: return DEVICEDRIVER_XR; case 7: return DEVICEDRIVER_IETF_L2VPN; case 8: return DEVICEDRIVER_GNMI_OPENCONFIG; + case 9: return DEVICEDRIVER_FLEXSCALE; + case 10: return DEVICEDRIVER_IETF_ACTN; default: return null; } } @@ -461,6 +479,10 @@ public final class ContextOuterClass { * SERVICETYPE_TE = 4; */ SERVICETYPE_TE(4), + /** + * SERVICETYPE_E2E = 5; + */ + SERVICETYPE_E2E(5), UNRECOGNIZED(-1), ; @@ -484,6 +506,10 @@ public final class ContextOuterClass { * SERVICETYPE_TE = 4; */ public static final int SERVICETYPE_TE_VALUE = 4; + /** + * SERVICETYPE_E2E = 5; + */ + public static final int SERVICETYPE_E2E_VALUE = 5; public final int getNumber() { @@ -515,6 +541,7 @@ public final class ContextOuterClass { case 2: return SERVICETYPE_L2NM; case 3: return SERVICETYPE_TAPI_CONNECTIVITY_SERVICE; case 4: return SERVICETYPE_TE; + case 5: return SERVICETYPE_E2E; default: return null; } } @@ -75826,114 +75853,116 @@ public final class ContextOuterClass { "\0132\022.context.ContextId\022\025\n\rauthenticated\030\002" + " \001(\010*j\n\rEventTypeEnum\022\027\n\023EVENTTYPE_UNDEF" + "INED\020\000\022\024\n\020EVENTTYPE_CREATE\020\001\022\024\n\020EVENTTYP" + - "E_UPDATE\020\002\022\024\n\020EVENTTYPE_REMOVE\020\003*\231\002\n\020Dev" + + "E_UPDATE\020\002\022\024\n\020EVENTTYPE_REMOVE\020\003*\321\002\n\020Dev" + "iceDriverEnum\022\032\n\026DEVICEDRIVER_UNDEFINED\020" + "\000\022\033\n\027DEVICEDRIVER_OPENCONFIG\020\001\022\036\n\032DEVICE" + "DRIVER_TRANSPORT_API\020\002\022\023\n\017DEVICEDRIVER_P" + "4\020\003\022&\n\"DEVICEDRIVER_IETF_NETWORK_TOPOLOG" + "Y\020\004\022\033\n\027DEVICEDRIVER_ONF_TR_532\020\005\022\023\n\017DEVI" + "CEDRIVER_XR\020\006\022\033\n\027DEVICEDRIVER_IETF_L2VPN" + - "\020\007\022 \n\034DEVICEDRIVER_GNMI_OPENCONFIG\020\010*\217\001\n" + - "\033DeviceOperationalStatusEnum\022%\n!DEVICEOP" + - "ERATIONALSTATUS_UNDEFINED\020\000\022$\n DEVICEOPE" + - "RATIONALSTATUS_DISABLED\020\001\022#\n\037DEVICEOPERA" + - "TIONALSTATUS_ENABLED\020\002*\225\001\n\017ServiceTypeEn" + - "um\022\027\n\023SERVICETYPE_UNKNOWN\020\000\022\024\n\020SERVICETY" + - "PE_L3NM\020\001\022\024\n\020SERVICETYPE_L2NM\020\002\022)\n%SERVI" + - "CETYPE_TAPI_CONNECTIVITY_SERVICE\020\003\022\022\n\016SE" + - "RVICETYPE_TE\020\004*\304\001\n\021ServiceStatusEnum\022\033\n\027" + - "SERVICESTATUS_UNDEFINED\020\000\022\031\n\025SERVICESTAT" + - "US_PLANNED\020\001\022\030\n\024SERVICESTATUS_ACTIVE\020\002\022\032" + - "\n\026SERVICESTATUS_UPDATING\020\003\022!\n\035SERVICESTA" + - "TUS_PENDING_REMOVAL\020\004\022\036\n\032SERVICESTATUS_S" + - "LA_VIOLATED\020\005*\251\001\n\017SliceStatusEnum\022\031\n\025SLI" + - "CESTATUS_UNDEFINED\020\000\022\027\n\023SLICESTATUS_PLAN" + - "NED\020\001\022\024\n\020SLICESTATUS_INIT\020\002\022\026\n\022SLICESTAT" + - "US_ACTIVE\020\003\022\026\n\022SLICESTATUS_DEINIT\020\004\022\034\n\030S" + - "LICESTATUS_SLA_VIOLATED\020\005*]\n\020ConfigActio" + - "nEnum\022\032\n\026CONFIGACTION_UNDEFINED\020\000\022\024\n\020CON" + - "FIGACTION_SET\020\001\022\027\n\023CONFIGACTION_DELETE\020\002" + - "*m\n\024ConstraintActionEnum\022\036\n\032CONSTRAINTAC" + - "TION_UNDEFINED\020\000\022\030\n\024CONSTRAINTACTION_SET" + - "\020\001\022\033\n\027CONSTRAINTACTION_DELETE\020\002*\203\002\n\022Isol" + - "ationLevelEnum\022\020\n\014NO_ISOLATION\020\000\022\026\n\022PHYS" + - "ICAL_ISOLATION\020\001\022\025\n\021LOGICAL_ISOLATION\020\002\022" + - "\025\n\021PROCESS_ISOLATION\020\003\022\035\n\031PHYSICAL_MEMOR" + - "Y_ISOLATION\020\004\022\036\n\032PHYSICAL_NETWORK_ISOLAT" + - "ION\020\005\022\036\n\032VIRTUAL_RESOURCE_ISOLATION\020\006\022\037\n" + - "\033NETWORK_FUNCTIONS_ISOLATION\020\007\022\025\n\021SERVIC" + - "E_ISOLATION\020\0102\245\026\n\016ContextService\022:\n\016List" + - "ContextIds\022\016.context.Empty\032\026.context.Con" + - "textIdList\"\000\0226\n\014ListContexts\022\016.context.E" + - "mpty\032\024.context.ContextList\"\000\0224\n\nGetConte" + - "xt\022\022.context.ContextId\032\020.context.Context" + - "\"\000\0224\n\nSetContext\022\020.context.Context\032\022.con" + - "text.ContextId\"\000\0225\n\rRemoveContext\022\022.cont" + - "ext.ContextId\032\016.context.Empty\"\000\022=\n\020GetCo" + - "ntextEvents\022\016.context.Empty\032\025.context.Co" + - "ntextEvent\"\0000\001\022@\n\017ListTopologyIds\022\022.cont" + - "ext.ContextId\032\027.context.TopologyIdList\"\000" + - "\022=\n\016ListTopologies\022\022.context.ContextId\032\025" + - ".context.TopologyList\"\000\0227\n\013GetTopology\022\023" + - ".context.TopologyId\032\021.context.Topology\"\000" + - "\022E\n\022GetTopologyDetails\022\023.context.Topolog" + - "yId\032\030.context.TopologyDetails\"\000\0227\n\013SetTo" + - "pology\022\021.context.Topology\032\023.context.Topo" + - "logyId\"\000\0227\n\016RemoveTopology\022\023.context.Top" + - "ologyId\032\016.context.Empty\"\000\022?\n\021GetTopology" + - "Events\022\016.context.Empty\032\026.context.Topolog" + - "yEvent\"\0000\001\0228\n\rListDeviceIds\022\016.context.Em" + - "pty\032\025.context.DeviceIdList\"\000\0224\n\013ListDevi" + - "ces\022\016.context.Empty\032\023.context.DeviceList" + - "\"\000\0221\n\tGetDevice\022\021.context.DeviceId\032\017.con" + - "text.Device\"\000\0221\n\tSetDevice\022\017.context.Dev" + - "ice\032\021.context.DeviceId\"\000\0223\n\014RemoveDevice" + - "\022\021.context.DeviceId\032\016.context.Empty\"\000\022;\n" + - "\017GetDeviceEvents\022\016.context.Empty\032\024.conte" + - "xt.DeviceEvent\"\0000\001\022<\n\014SelectDevice\022\025.con" + - "text.DeviceFilter\032\023.context.DeviceList\"\000" + - "\022I\n\021ListEndPointNames\022\027.context.EndPoint" + - "IdList\032\031.context.EndPointNameList\"\000\0224\n\013L" + - "istLinkIds\022\016.context.Empty\032\023.context.Lin" + - "kIdList\"\000\0220\n\tListLinks\022\016.context.Empty\032\021" + - ".context.LinkList\"\000\022+\n\007GetLink\022\017.context" + - ".LinkId\032\r.context.Link\"\000\022+\n\007SetLink\022\r.co" + - "ntext.Link\032\017.context.LinkId\"\000\022/\n\nRemoveL" + - "ink\022\017.context.LinkId\032\016.context.Empty\"\000\0227" + - "\n\rGetLinkEvents\022\016.context.Empty\032\022.contex" + - "t.LinkEvent\"\0000\001\022>\n\016ListServiceIds\022\022.cont" + - "ext.ContextId\032\026.context.ServiceIdList\"\000\022" + - ":\n\014ListServices\022\022.context.ContextId\032\024.co" + - "ntext.ServiceList\"\000\0224\n\nGetService\022\022.cont" + - "ext.ServiceId\032\020.context.Service\"\000\0224\n\nSet" + - "Service\022\020.context.Service\032\022.context.Serv" + - "iceId\"\000\0226\n\014UnsetService\022\020.context.Servic" + - "e\032\022.context.ServiceId\"\000\0225\n\rRemoveService" + - "\022\022.context.ServiceId\032\016.context.Empty\"\000\022=" + - "\n\020GetServiceEvents\022\016.context.Empty\032\025.con" + - "text.ServiceEvent\"\0000\001\022?\n\rSelectService\022\026" + - ".context.ServiceFilter\032\024.context.Service" + - "List\"\000\022:\n\014ListSliceIds\022\022.context.Context" + - "Id\032\024.context.SliceIdList\"\000\0226\n\nListSlices" + - "\022\022.context.ContextId\032\022.context.SliceList" + - "\"\000\022.\n\010GetSlice\022\020.context.SliceId\032\016.conte" + - "xt.Slice\"\000\022.\n\010SetSlice\022\016.context.Slice\032\020" + - ".context.SliceId\"\000\0220\n\nUnsetSlice\022\016.conte" + - "xt.Slice\032\020.context.SliceId\"\000\0221\n\013RemoveSl" + - "ice\022\020.context.SliceId\032\016.context.Empty\"\000\022" + - "9\n\016GetSliceEvents\022\016.context.Empty\032\023.cont" + - "ext.SliceEvent\"\0000\001\0229\n\013SelectSlice\022\024.cont" + - "ext.SliceFilter\032\022.context.SliceList\"\000\022D\n" + - "\021ListConnectionIds\022\022.context.ServiceId\032\031" + - ".context.ConnectionIdList\"\000\022@\n\017ListConne" + - "ctions\022\022.context.ServiceId\032\027.context.Con" + - "nectionList\"\000\022=\n\rGetConnection\022\025.context" + - ".ConnectionId\032\023.context.Connection\"\000\022=\n\r" + - "SetConnection\022\023.context.Connection\032\025.con" + - "text.ConnectionId\"\000\022;\n\020RemoveConnection\022" + - "\025.context.ConnectionId\032\016.context.Empty\"\000" + - "\022C\n\023GetConnectionEvents\022\016.context.Empty\032" + - "\030.context.ConnectionEvent\"\0000\001b\006proto3" + "\020\007\022 \n\034DEVICEDRIVER_GNMI_OPENCONFIG\020\010\022\032\n\026" + + "DEVICEDRIVER_FLEXSCALE\020\t\022\032\n\026DEVICEDRIVER" + + "_IETF_ACTN\020\n*\217\001\n\033DeviceOperationalStatus" + + "Enum\022%\n!DEVICEOPERATIONALSTATUS_UNDEFINE" + + "D\020\000\022$\n DEVICEOPERATIONALSTATUS_DISABLED\020" + + "\001\022#\n\037DEVICEOPERATIONALSTATUS_ENABLED\020\002*\252" + + "\001\n\017ServiceTypeEnum\022\027\n\023SERVICETYPE_UNKNOW" + + "N\020\000\022\024\n\020SERVICETYPE_L3NM\020\001\022\024\n\020SERVICETYPE" + + "_L2NM\020\002\022)\n%SERVICETYPE_TAPI_CONNECTIVITY" + + "_SERVICE\020\003\022\022\n\016SERVICETYPE_TE\020\004\022\023\n\017SERVIC" + + "ETYPE_E2E\020\005*\304\001\n\021ServiceStatusEnum\022\033\n\027SER" + + "VICESTATUS_UNDEFINED\020\000\022\031\n\025SERVICESTATUS_" + + "PLANNED\020\001\022\030\n\024SERVICESTATUS_ACTIVE\020\002\022\032\n\026S" + + "ERVICESTATUS_UPDATING\020\003\022!\n\035SERVICESTATUS" + + "_PENDING_REMOVAL\020\004\022\036\n\032SERVICESTATUS_SLA_" + + "VIOLATED\020\005*\251\001\n\017SliceStatusEnum\022\031\n\025SLICES" + + "TATUS_UNDEFINED\020\000\022\027\n\023SLICESTATUS_PLANNED" + + "\020\001\022\024\n\020SLICESTATUS_INIT\020\002\022\026\n\022SLICESTATUS_" + + "ACTIVE\020\003\022\026\n\022SLICESTATUS_DEINIT\020\004\022\034\n\030SLIC" + + "ESTATUS_SLA_VIOLATED\020\005*]\n\020ConfigActionEn" + + "um\022\032\n\026CONFIGACTION_UNDEFINED\020\000\022\024\n\020CONFIG" + + "ACTION_SET\020\001\022\027\n\023CONFIGACTION_DELETE\020\002*m\n" + + "\024ConstraintActionEnum\022\036\n\032CONSTRAINTACTIO" + + "N_UNDEFINED\020\000\022\030\n\024CONSTRAINTACTION_SET\020\001\022" + + "\033\n\027CONSTRAINTACTION_DELETE\020\002*\203\002\n\022Isolati" + + "onLevelEnum\022\020\n\014NO_ISOLATION\020\000\022\026\n\022PHYSICA" + + "L_ISOLATION\020\001\022\025\n\021LOGICAL_ISOLATION\020\002\022\025\n\021" + + "PROCESS_ISOLATION\020\003\022\035\n\031PHYSICAL_MEMORY_I" + + "SOLATION\020\004\022\036\n\032PHYSICAL_NETWORK_ISOLATION" + + "\020\005\022\036\n\032VIRTUAL_RESOURCE_ISOLATION\020\006\022\037\n\033NE" + + "TWORK_FUNCTIONS_ISOLATION\020\007\022\025\n\021SERVICE_I" + + "SOLATION\020\0102\245\026\n\016ContextService\022:\n\016ListCon" + + "textIds\022\016.context.Empty\032\026.context.Contex" + + "tIdList\"\000\0226\n\014ListContexts\022\016.context.Empt" + + "y\032\024.context.ContextList\"\000\0224\n\nGetContext\022" + + "\022.context.ContextId\032\020.context.Context\"\000\022" + + "4\n\nSetContext\022\020.context.Context\032\022.contex" + + "t.ContextId\"\000\0225\n\rRemoveContext\022\022.context" + + ".ContextId\032\016.context.Empty\"\000\022=\n\020GetConte" + + "xtEvents\022\016.context.Empty\032\025.context.Conte" + + "xtEvent\"\0000\001\022@\n\017ListTopologyIds\022\022.context" + + ".ContextId\032\027.context.TopologyIdList\"\000\022=\n" + + "\016ListTopologies\022\022.context.ContextId\032\025.co" + + "ntext.TopologyList\"\000\0227\n\013GetTopology\022\023.co" + + "ntext.TopologyId\032\021.context.Topology\"\000\022E\n" + + "\022GetTopologyDetails\022\023.context.TopologyId" + + "\032\030.context.TopologyDetails\"\000\0227\n\013SetTopol" + + "ogy\022\021.context.Topology\032\023.context.Topolog" + + "yId\"\000\0227\n\016RemoveTopology\022\023.context.Topolo" + + "gyId\032\016.context.Empty\"\000\022?\n\021GetTopologyEve" + + "nts\022\016.context.Empty\032\026.context.TopologyEv" + + "ent\"\0000\001\0228\n\rListDeviceIds\022\016.context.Empty" + + "\032\025.context.DeviceIdList\"\000\0224\n\013ListDevices" + + "\022\016.context.Empty\032\023.context.DeviceList\"\000\022" + + "1\n\tGetDevice\022\021.context.DeviceId\032\017.contex" + + "t.Device\"\000\0221\n\tSetDevice\022\017.context.Device" + + "\032\021.context.DeviceId\"\000\0223\n\014RemoveDevice\022\021." + + "context.DeviceId\032\016.context.Empty\"\000\022;\n\017Ge" + + "tDeviceEvents\022\016.context.Empty\032\024.context." + + "DeviceEvent\"\0000\001\022<\n\014SelectDevice\022\025.contex" + + "t.DeviceFilter\032\023.context.DeviceList\"\000\022I\n" + + "\021ListEndPointNames\022\027.context.EndPointIdL" + + "ist\032\031.context.EndPointNameList\"\000\0224\n\013List" + + "LinkIds\022\016.context.Empty\032\023.context.LinkId" + + "List\"\000\0220\n\tListLinks\022\016.context.Empty\032\021.co" + + "ntext.LinkList\"\000\022+\n\007GetLink\022\017.context.Li" + + "nkId\032\r.context.Link\"\000\022+\n\007SetLink\022\r.conte" + + "xt.Link\032\017.context.LinkId\"\000\022/\n\nRemoveLink" + + "\022\017.context.LinkId\032\016.context.Empty\"\000\0227\n\rG" + + "etLinkEvents\022\016.context.Empty\032\022.context.L" + + "inkEvent\"\0000\001\022>\n\016ListServiceIds\022\022.context" + + ".ContextId\032\026.context.ServiceIdList\"\000\022:\n\014" + + "ListServices\022\022.context.ContextId\032\024.conte" + + "xt.ServiceList\"\000\0224\n\nGetService\022\022.context" + + ".ServiceId\032\020.context.Service\"\000\0224\n\nSetSer" + + "vice\022\020.context.Service\032\022.context.Service" + + "Id\"\000\0226\n\014UnsetService\022\020.context.Service\032\022" + + ".context.ServiceId\"\000\0225\n\rRemoveService\022\022." + + "context.ServiceId\032\016.context.Empty\"\000\022=\n\020G" + + "etServiceEvents\022\016.context.Empty\032\025.contex" + + "t.ServiceEvent\"\0000\001\022?\n\rSelectService\022\026.co" + + "ntext.ServiceFilter\032\024.context.ServiceLis" + + "t\"\000\022:\n\014ListSliceIds\022\022.context.ContextId\032" + + "\024.context.SliceIdList\"\000\0226\n\nListSlices\022\022." + + "context.ContextId\032\022.context.SliceList\"\000\022" + + ".\n\010GetSlice\022\020.context.SliceId\032\016.context." + + "Slice\"\000\022.\n\010SetSlice\022\016.context.Slice\032\020.co" + + "ntext.SliceId\"\000\0220\n\nUnsetSlice\022\016.context." + + "Slice\032\020.context.SliceId\"\000\0221\n\013RemoveSlice" + + "\022\020.context.SliceId\032\016.context.Empty\"\000\0229\n\016" + + "GetSliceEvents\022\016.context.Empty\032\023.context" + + ".SliceEvent\"\0000\001\0229\n\013SelectSlice\022\024.context" + + ".SliceFilter\032\022.context.SliceList\"\000\022D\n\021Li" + + "stConnectionIds\022\022.context.ServiceId\032\031.co" + + "ntext.ConnectionIdList\"\000\022@\n\017ListConnecti" + + "ons\022\022.context.ServiceId\032\027.context.Connec" + + "tionList\"\000\022=\n\rGetConnection\022\025.context.Co" + + "nnectionId\032\023.context.Connection\"\000\022=\n\rSet" + + "Connection\022\023.context.Connection\032\025.contex" + + "t.ConnectionId\"\000\022;\n\020RemoveConnection\022\025.c" + + "ontext.ConnectionId\032\016.context.Empty\"\000\022C\n" + + "\023GetConnectionEvents\022\016.context.Empty\032\030.c" + + "ontext.ConnectionEvent\"\0000\001b\006proto3" }; descriptor = com.google.protobuf.Descriptors.FileDescriptor .internalBuildGeneratedFileFrom(descriptorData, diff --git a/src/ztp/target/kubernetes/kubernetes.yml b/src/ztp/target/kubernetes/kubernetes.yml index f3e4a6d6d..d2a59eb05 100644 --- a/src/ztp/target/kubernetes/kubernetes.yml +++ b/src/ztp/target/kubernetes/kubernetes.yml @@ -3,8 +3,8 @@ apiVersion: v1 kind: Service metadata: annotations: - app.quarkus.io/commit-id: 46486023929121fc955e9550fc8fd625ded433d2 - app.quarkus.io/build-timestamp: 2023-12-15 - 12:04:12 +0000 + app.quarkus.io/commit-id: 5f8866be9cb91871607627819258b0b375410467 + app.quarkus.io/build-timestamp: 2024-01-26 - 16:39:32 +0000 prometheus.io/scrape: "true" prometheus.io/path: /q/metrics prometheus.io/port: "8080" @@ -15,12 +15,12 @@ metadata: name: ztpservice spec: ports: - - name: grpc-server - port: 5050 - targetPort: 5050 - name: http port: 9192 targetPort: 8080 + - name: grpc-server + port: 5050 + targetPort: 5050 selector: app.kubernetes.io/name: ztpservice type: ClusterIP @@ -29,8 +29,8 @@ apiVersion: apps/v1 kind: Deployment metadata: annotations: - app.quarkus.io/commit-id: 46486023929121fc955e9550fc8fd625ded433d2 - app.quarkus.io/build-timestamp: 2023-12-15 - 12:04:12 +0000 + app.quarkus.io/commit-id: 5f8866be9cb91871607627819258b0b375410467 + app.quarkus.io/build-timestamp: 2024-01-26 - 16:39:32 +0000 prometheus.io/scrape: "true" prometheus.io/path: /q/metrics prometheus.io/port: "8080" @@ -47,8 +47,8 @@ spec: template: metadata: annotations: - app.quarkus.io/commit-id: 46486023929121fc955e9550fc8fd625ded433d2 - app.quarkus.io/build-timestamp: 2023-12-15 - 12:04:12 +0000 + app.quarkus.io/commit-id: 5f8866be9cb91871607627819258b0b375410467 + app.quarkus.io/build-timestamp: 2024-01-26 - 16:39:32 +0000 prometheus.io/scrape: "true" prometheus.io/path: /q/metrics prometheus.io/port: "8080" @@ -81,12 +81,12 @@ spec: timeoutSeconds: 10 name: ztpservice ports: - - containerPort: 5050 - name: grpc-server - protocol: TCP - containerPort: 8080 name: http protocol: TCP + - containerPort: 5050 + name: grpc-server + protocol: TCP readinessProbe: failureThreshold: 3 httpGet: -- GitLab From 647c096181adeaa9cb6a44d18a4c1ce4eadc330c Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Fri, 26 Jan 2024 17:29:49 +0000 Subject: [PATCH 17/18] Tests - Tools - Mock IETF ACTN SDN Ctrl: - Pre-merge cleanup --- .../scenario/ietf_actn_deploy.sh | 36 ------ .../scenario/network_descriptors.json | 117 ------------------ .../scenario/service_descriptor.json | 25 ---- .../mock_ietf_actn_sdn_ctrl/test_ietf_actn.py | 84 ------------- 4 files changed, 262 deletions(-) delete mode 100755 src/tests/tools/mock_ietf_actn_sdn_ctrl/scenario/ietf_actn_deploy.sh delete mode 100644 src/tests/tools/mock_ietf_actn_sdn_ctrl/scenario/network_descriptors.json delete mode 100644 src/tests/tools/mock_ietf_actn_sdn_ctrl/scenario/service_descriptor.json delete mode 100644 src/tests/tools/mock_ietf_actn_sdn_ctrl/test_ietf_actn.py diff --git a/src/tests/tools/mock_ietf_actn_sdn_ctrl/scenario/ietf_actn_deploy.sh b/src/tests/tools/mock_ietf_actn_sdn_ctrl/scenario/ietf_actn_deploy.sh deleted file mode 100755 index 665d337c9..000000000 --- a/src/tests/tools/mock_ietf_actn_sdn_ctrl/scenario/ietf_actn_deploy.sh +++ /dev/null @@ -1,36 +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. - -# Set the URL of your local Docker registry where the images will be uploaded to. -export TFS_REGISTRY_IMAGE="http://localhost:32000/tfs/" - -# Set the list of components, separated by spaces, you want to build images for, and deploy. -# Supported components are: -# context device ztp policy service nbi monitoring webui -# interdomain slice pathcomp dlt -# dbscanserving opticalattackmitigator opticalattackdetector -# l3_attackmitigator l3_centralizedattackdetector l3_distributedattackdetector -export TFS_COMPONENTS="context device pathcomp service slice webui" - -# Set the tag you want to use for your images. -export TFS_IMAGE_TAG="dev" - -# Set the name of the Kubernetes namespace to deploy to. -export TFS_K8S_NAMESPACE="tfs" - -# Set additional manifest files to be applied after the deployment -export TFS_EXTRA_MANIFESTS="manifests/nginx_ingress_http.yaml" - -# Set the neew Grafana admin password -export TFS_GRAFANA_PASSWORD="admin123+" diff --git a/src/tests/tools/mock_ietf_actn_sdn_ctrl/scenario/network_descriptors.json b/src/tests/tools/mock_ietf_actn_sdn_ctrl/scenario/network_descriptors.json deleted file mode 100644 index 25fa940a4..000000000 --- a/src/tests/tools/mock_ietf_actn_sdn_ctrl/scenario/network_descriptors.json +++ /dev/null @@ -1,117 +0,0 @@ -{ - "contexts": [ - { - "context_id": {"context_uuid": {"uuid": "admin"}}, - "topology_ids": [], - "service_ids": [] - } - ], - "topologies": [ - { - "topology_id": {"topology_uuid": {"uuid": "admin"}, "context_id": {"context_uuid": {"uuid": "admin"}}}, - "device_ids": [], - "link_ids": [] - } - ], - "devices": [ - { - "device_id": {"device_uuid": {"uuid": "R1"}}, "device_type": "emu-packet-router", "device_drivers": [0], - "device_operational_status": 2, "device_endpoints": [], - "device_config": {"config_rules": [ - {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "127.0.0.1"}}, - {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "0"}}, - {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": {"endpoints": [ - {"type": "copper", "uuid": "MW", "sample_types": []}, - {"type": "copper", "uuid": "R2", "sample_types": []}, - {"type": "copper", "uuid": "EXT", "sample_types": []} - ]}}} - ]} - }, - { - "device_id": {"device_uuid": {"uuid": "R2"}}, "device_type": "emu-packet-router", "device_drivers": [0], - "device_operational_status": 2, "device_endpoints": [], - "device_config": {"config_rules": [ - {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "127.0.0.1"}}, - {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "0"}}, - {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": {"endpoints": [ - {"type": "copper", "uuid": "MW", "sample_types": []}, - {"type": "copper", "uuid": "R1", "sample_types": []}, - {"type": "copper", "uuid": "EXT", "sample_types": []} - ]}}} - ]} - }, - { - "device_id": {"device_uuid": {"uuid": "R3"}}, "device_type": "emu-packet-router", "device_drivers": [0], - "device_operational_status": 2, "device_endpoints": [], - "device_config": {"config_rules": [ - {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "127.0.0.1"}}, - {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "0"}}, - {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": {"endpoints": [ - {"type": "copper", "uuid": "MW", "sample_types": []}, - {"type": "copper", "uuid": "R4", "sample_types": []}, - {"type": "copper", "uuid": "EXT", "sample_types": []} - ]}}} - ]} - }, - { - "device_id": {"device_uuid": {"uuid": "R4"}}, "device_type": "emu-packet-router", "device_drivers": [0], - "device_operational_status": 2, "device_endpoints": [], - "device_config": {"config_rules": [ - {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "127.0.0.1"}}, - {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "0"}}, - {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": {"endpoints": [ - {"type": "copper", "uuid": "MW", "sample_types": []}, - {"type": "copper", "uuid": "R3", "sample_types": []}, - {"type": "copper", "uuid": "EXT", "sample_types": []} - ]}}} - ]} - }, - { - "device_id": {"device_uuid": {"uuid": "MW"}}, "device_type": "microwave-radio-system", "device_drivers": [4], - "device_operational_status": 2, "device_endpoints": [], - "device_config": {"config_rules": [ - {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "192.168.1.1"}}, - {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "8443"}}, - {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": {"timeout": 120}}} - ]} - } - ], - "links": [ - { - "link_id": {"link_uuid": {"uuid": "R1/R2==R2/R1"}}, "link_endpoint_ids": [ - {"device_id": {"device_uuid": {"uuid": "R1"}}, "endpoint_uuid": {"uuid": "R2"}}, - {"device_id": {"device_uuid": {"uuid": "R2"}}, "endpoint_uuid": {"uuid": "R1"}} - ] - }, - { - "link_id": {"link_uuid": {"uuid": "R3/R4==R4/R3"}}, "link_endpoint_ids": [ - {"device_id": {"device_uuid": {"uuid": "R3"}}, "endpoint_uuid": {"uuid": "R4"}}, - {"device_id": {"device_uuid": {"uuid": "R4"}}, "endpoint_uuid": {"uuid": "R3"}} - ] - }, - { - "link_id": {"link_uuid": {"uuid": "R1/MW==MW/172.18.0.1:1"}}, "link_endpoint_ids": [ - {"device_id": {"device_uuid": {"uuid": "R1"}}, "endpoint_uuid": {"uuid": "MW"}}, - {"device_id": {"device_uuid": {"uuid": "MW"}}, "endpoint_uuid": {"uuid": "172.18.0.1:1"}} - ] - }, - { - "link_id": {"link_uuid": {"uuid": "R2/MW==MW/172.18.0.2:1"}}, "link_endpoint_ids": [ - {"device_id": {"device_uuid": {"uuid": "R2"}}, "endpoint_uuid": {"uuid": "MW"}}, - {"device_id": {"device_uuid": {"uuid": "MW"}}, "endpoint_uuid": {"uuid": "172.18.0.2:1"}} - ] - }, - { - "link_id": {"link_uuid": {"uuid": "R3/MW==MW/172.18.0.3:1"}}, "link_endpoint_ids": [ - {"device_id": {"device_uuid": {"uuid": "R3"}}, "endpoint_uuid": {"uuid": "MW"}}, - {"device_id": {"device_uuid": {"uuid": "MW"}}, "endpoint_uuid": {"uuid": "172.18.0.3:1"}} - ] - }, - { - "link_id": {"link_uuid": {"uuid": "R4/MW==MW/172.18.0.4:1"}}, "link_endpoint_ids": [ - {"device_id": {"device_uuid": {"uuid": "R4"}}, "endpoint_uuid": {"uuid": "MW"}}, - {"device_id": {"device_uuid": {"uuid": "MW"}}, "endpoint_uuid": {"uuid": "172.18.0.4:1"}} - ] - } - ] -} \ No newline at end of file diff --git a/src/tests/tools/mock_ietf_actn_sdn_ctrl/scenario/service_descriptor.json b/src/tests/tools/mock_ietf_actn_sdn_ctrl/scenario/service_descriptor.json deleted file mode 100644 index 2d4ed3eaf..000000000 --- a/src/tests/tools/mock_ietf_actn_sdn_ctrl/scenario/service_descriptor.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "services": [ - { - "service_id": { - "context_id": {"context_uuid": {"uuid": "admin"}}, - "service_uuid": {"uuid": "mw-svc"} - }, - "service_type": 2, - "service_status": {"service_status": 1}, - "service_endpoint_ids": [ - {"device_id": {"device_uuid": {"uuid": "R1"}}, "endpoint_uuid": {"uuid": "EXT"}}, - {"device_id": {"device_uuid": {"uuid": "R3"}}, "endpoint_uuid": {"uuid": "EXT"}} - ], - "service_constraints": [ - {"sla_capacity": {"capacity_gbps": 10.0}}, - {"sla_latency": {"e2e_latency_ms": 15.2}} - ], - "service_config": {"config_rules": [ - {"action": 1, "custom": {"resource_key": "/settings", "resource_value": { - "vlan_id": 121 - }}} - ]} - } - ] -} diff --git a/src/tests/tools/mock_ietf_actn_sdn_ctrl/test_ietf_actn.py b/src/tests/tools/mock_ietf_actn_sdn_ctrl/test_ietf_actn.py deleted file mode 100644 index 0329d30ad..000000000 --- a/src/tests/tools/mock_ietf_actn_sdn_ctrl/test_ietf_actn.py +++ /dev/null @@ -1,84 +0,0 @@ -import json, logging, requests -from requests.auth import HTTPBasicAuth -from typing import Optional - -LOGGER = logging.getLogger(__name__) - -HTTP_OK_CODES = { - 200, # OK - 201, # Created - 202, # Accepted - 204, # No Content -} - -def create_connectivity_service( - root_url, uuid, node_id_src, tp_id_src, node_id_dst, tp_id_dst, vlan_id, - auth : Optional[HTTPBasicAuth] = None, timeout : Optional[int] = None -): - - url = '{:s}/nmswebs/restconf/data/ietf-eth-tran-service:etht-svc'.format(root_url) - headers = {'content-type': 'application/json'} - data = { - 'etht-svc-instances': [ - { - 'etht-svc-name': uuid, - 'etht-svc-type': 'ietf-eth-tran-types:p2p-svc', - 'etht-svc-end-points': [ - { - 'etht-svc-access-points': [ - {'access-node-id': node_id_src, 'access-ltp-id': tp_id_src, 'access-point-id': '1'} - ], - 'outer-tag': {'vlan-value': vlan_id, 'tag-type': 'ietf-eth-tran-types:classify-c-vlan'}, - 'etht-svc-end-point-name': '{:s}:{:s}'.format(str(node_id_src), str(tp_id_src)), - 'service-classification-type': 'ietf-eth-tran-types:vlan-classification' - }, - { - 'etht-svc-access-points': [ - {'access-node-id': node_id_dst, 'access-ltp-id': tp_id_dst, 'access-point-id': '2'} - ], - 'outer-tag': {'vlan-value': vlan_id, 'tag-type': 'ietf-eth-tran-types:classify-c-vlan'}, - 'etht-svc-end-point-name': '{:s}:{:s}'.format(str(node_id_dst), str(tp_id_dst)), - 'service-classification-type': 'ietf-eth-tran-types:vlan-classification' - } - ] - } - ] - } - results = [] - try: - LOGGER.info('Connectivity service {:s}: {:s}'.format(str(uuid), str(data))) - response = requests.post( - url=url, data=json.dumps(data), timeout=timeout, headers=headers, verify=False, auth=auth) - LOGGER.info('Microwave Driver response: {:s}'.format(str(response))) - except Exception as e: # pylint: disable=broad-except - LOGGER.exception('Exception creating ConnectivityService(uuid={:s}, data={:s})'.format(str(uuid), str(data))) - results.append(e) - else: - if response.status_code not in HTTP_OK_CODES: - msg = 'Could not create ConnectivityService(uuid={:s}, data={:s}). status_code={:s} reply={:s}' - LOGGER.error(msg.format(str(uuid), str(data), str(response.status_code), str(response))) - results.append(response.status_code in HTTP_OK_CODES) - return results - -def delete_connectivity_service(root_url, uuid, auth : Optional[HTTPBasicAuth] = None, timeout : Optional[int] = None): - url = '{:s}/nmswebs/restconf/data/ietf-eth-tran-service:etht-svc/etht-svc-instances={:s}' - url = url.format(root_url, uuid) - results = [] - try: - response = requests.delete(url=url, timeout=timeout, verify=False, auth=auth) - except Exception as e: # pylint: disable=broad-except - LOGGER.exception('Exception deleting ConnectivityService(uuid={:s})'.format(str(uuid))) - results.append(e) - else: - if response.status_code not in HTTP_OK_CODES: - msg = 'Could not delete ConnectivityService(uuid={:s}). status_code={:s} reply={:s}' - LOGGER.error(msg.format(str(uuid), str(response.status_code), str(response))) - results.append(response.status_code in HTTP_OK_CODES) - return results - -if __name__ == '__main__': - ROOT_URL = 'https://127.0.0.1:8443' - SERVICE_UUID = 'my-service' - - create_connectivity_service(ROOT_URL, SERVICE_UUID, '172.18.0.1', '1', '172.18.0.2', '2', 300) - delete_connectivity_service(ROOT_URL, SERVICE_UUID) -- GitLab From 36758538ce2893924d8e081c3ac5286ff540c2e4 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Fri, 26 Jan 2024 17:32:00 +0000 Subject: [PATCH 18/18] Tests - Tools - Mock IETF ACTN SDN Ctrl: - Pre-merge cleanup --- src/tests/tools/mock_ietf_actn_sdn_ctrl/build.sh | 13 +++++++++++++ src/tests/tools/mock_ietf_actn_sdn_ctrl/deploy.sh | 13 +++++++++++++ .../tools/mock_mw_sdn_ctrl/ssl_not_working/build.sh | 13 +++++++++++++ .../mock_mw_sdn_ctrl/ssl_not_working/deploy.sh | 13 +++++++++++++ 4 files changed, 52 insertions(+) diff --git a/src/tests/tools/mock_ietf_actn_sdn_ctrl/build.sh b/src/tests/tools/mock_ietf_actn_sdn_ctrl/build.sh index 8d125727b..d9db334cb 100755 --- a/src/tests/tools/mock_ietf_actn_sdn_ctrl/build.sh +++ b/src/tests/tools/mock_ietf_actn_sdn_ctrl/build.sh @@ -1,4 +1,17 @@ #!/bin/bash +# 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. docker build -t mock-ietf-actn-sdn-ctrl:test -f Dockerfile . docker tag mock-ietf-actn-sdn-ctrl:test localhost:32000/tfs/mock-ietf-actn-sdn-ctrl:test diff --git a/src/tests/tools/mock_ietf_actn_sdn_ctrl/deploy.sh b/src/tests/tools/mock_ietf_actn_sdn_ctrl/deploy.sh index 822ecc06e..47270dc4b 100755 --- a/src/tests/tools/mock_ietf_actn_sdn_ctrl/deploy.sh +++ b/src/tests/tools/mock_ietf_actn_sdn_ctrl/deploy.sh @@ -1,4 +1,17 @@ #!/bin/bash +# 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. kubectl delete namespace mocks kubectl --namespace mocks apply -f mock-ietf-actn-sdn-ctrl.yaml diff --git a/src/tests/tools/mock_mw_sdn_ctrl/ssl_not_working/build.sh b/src/tests/tools/mock_mw_sdn_ctrl/ssl_not_working/build.sh index 4df315cec..5501614c4 100755 --- a/src/tests/tools/mock_mw_sdn_ctrl/ssl_not_working/build.sh +++ b/src/tests/tools/mock_mw_sdn_ctrl/ssl_not_working/build.sh @@ -1,4 +1,17 @@ #!/bin/bash +# 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. docker build -t mock-mw-sdn-ctrl:test -f Dockerfile . docker tag mock-mw-sdn-ctrl:test localhost:32000/tfs/mock-mw-sdn-ctrl:test diff --git a/src/tests/tools/mock_mw_sdn_ctrl/ssl_not_working/deploy.sh b/src/tests/tools/mock_mw_sdn_ctrl/ssl_not_working/deploy.sh index ded232e5c..ed77bcfbc 100755 --- a/src/tests/tools/mock_mw_sdn_ctrl/ssl_not_working/deploy.sh +++ b/src/tests/tools/mock_mw_sdn_ctrl/ssl_not_working/deploy.sh @@ -1,4 +1,17 @@ #!/bin/bash +# 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. kubectl delete namespace mocks kubectl --namespace mocks apply -f mock-mw-sdn-ctrl.yaml -- GitLab