From: gifrerenom Date: Tue, 18 Apr 2023 16:38:42 +0000 (+0000) Subject: Feature 10937: Transport API (TAPI) WIM connector for RO X-Git-Tag: release-v14.0-start~7 X-Git-Url: https://osm.etsi.org/gitweb/?p=osm%2FRO.git;a=commitdiff_plain;h=0e51779fd37dc5c12f3bd19d78f7341ed0a67b7a Feature 10937: Transport API (TAPI) WIM connector for RO Change-Id: If0dac9f8ba2d00945eb86a89fb0b2f174c672794 Signed-off-by: gifrerenom --- diff --git a/Dockerfile.local b/Dockerfile.local index 36a53eef..843e0537 100644 --- a/Dockerfile.local +++ b/Dockerfile.local @@ -82,6 +82,9 @@ RUN python3 -m build /build/RO-SDN-juniper_contrail && \ RUN python3 -m build /build/RO-VIM-gcp && \ python3 -m pip install /build/RO-VIM-gcp/dist/*.whl +RUN python3 -m build /build/RO-SDN-tapi && \ + python3 -m pip install /build/RO-SDN-tapi/dist/*.whl + FROM ubuntu:20.04 RUN DEBIAN_FRONTEND=noninteractive apt-get --yes update && \ diff --git a/RO-SDN-tapi/osm_rosdn_tapi/__init__.py b/RO-SDN-tapi/osm_rosdn_tapi/__init__.py new file mode 100644 index 00000000..ab3006ab --- /dev/null +++ b/RO-SDN-tapi/osm_rosdn_tapi/__init__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- + +####################################################################################### +# This file is part of OSM RO module +# +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +####################################################################################### +# This work has been performed in the context of the TeraFlow Project - +# funded by the European Commission under Grant number 101015857 through the +# Horizon 2020 program. +####################################################################################### diff --git a/RO-SDN-tapi/osm_rosdn_tapi/conn_info.py b/RO-SDN-tapi/osm_rosdn_tapi/conn_info.py new file mode 100644 index 00000000..70c6e590 --- /dev/null +++ b/RO-SDN-tapi/osm_rosdn_tapi/conn_info.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- + +####################################################################################### +# This file is part of OSM RO module +# +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +####################################################################################### +# This work has been performed in the context of the TeraFlow Project - +# funded by the European Commission under Grant number 101015857 through the +# Horizon 2020 program. +# Contributors: +# - Lluis Gifre +# - Ricard Vilalta +####################################################################################### + +"""This file contains the methods to compose the conn_info data structures for the +Transport API (TAPI) WIM connector.""" + + +def conn_info_compose_unidirectional( + service_az_uuid, + service_az_endpoints, + service_za_uuid, + service_za_endpoints, + requested_capacity=None, + vlan_constraint=None, +): + conn_info_az = { + "uuid": service_az_uuid, + "endpoints": service_az_endpoints, + } + conn_info_za = { + "uuid": service_za_uuid, + "endpoints": service_za_endpoints, + } + if requested_capacity is not None: + conn_info_az["requested_capacity"] = requested_capacity + conn_info_za["requested_capacity"] = requested_capacity + if vlan_constraint is not None: + conn_info_az["vlan_constraint"] = vlan_constraint + conn_info_za["vlan_constraint"] = vlan_constraint + conn_info = { + "az": conn_info_az, + "za": conn_info_za, + "bidirectional": False, + } + return conn_info + + +def conn_info_compose_bidirectional( + service_uuid, + service_endpoints, + requested_capacity=None, + vlan_constraint=None, +): + conn_info = { + "uuid": service_uuid, + "endpoints": service_endpoints, + "bidirectional": True, + } + if requested_capacity is not None: + conn_info["requested_capacity"] = requested_capacity + if vlan_constraint is not None: + conn_info["vlan_constraint"] = vlan_constraint + return conn_info diff --git a/RO-SDN-tapi/osm_rosdn_tapi/exceptions.py b/RO-SDN-tapi/osm_rosdn_tapi/exceptions.py new file mode 100644 index 00000000..66344227 --- /dev/null +++ b/RO-SDN-tapi/osm_rosdn_tapi/exceptions.py @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- + +####################################################################################### +# This file is part of OSM RO module +# +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +####################################################################################### +# This work has been performed in the context of the TeraFlow Project - +# funded by the European Commission under Grant number 101015857 through the +# Horizon 2020 program. +# Contributors: +# - Lluis Gifre +# - Ricard Vilalta +####################################################################################### + +"""This file contains the exception classes the Transport API (TAPI) WIM connector +can raise in case of error.""" + + +from http import HTTPStatus + +from osm_ro_plugin.sdnconn import SdnConnectorError + +from .log_messages import ( + _PREFIX, +) + + +class WimTapiError(SdnConnectorError): + """Base Exception for all WIM TAPI related errors.""" + + def __init__(self, message, http_code=HTTPStatus.INTERNAL_SERVER_ERROR.value): + super().__init__(_PREFIX + message) + self.http_code = http_code + + +class WimTapiConnectionPointsBadFormat(SdnConnectorError): + def __init__(self, connection_points): + MESSAGE = "ConnectionPoints({:s}) must be a list or tuple of length 2" + message = MESSAGE.format(str(connection_points)) + super().__init__(message, http_code=HTTPStatus.BAD_REQUEST) + + +class WimTapiIncongruentDirectionality(WimTapiError): + def __init__(self, services, service_endpoint_id): + MESSAGE = "Incongruent directionality: services={:s} service_endpoint_id={:s}" + message = MESSAGE.format(str(services), str(service_endpoint_id)) + super().__init__(message, http_code=HTTPStatus.INTERNAL_SERVER_ERROR) + + +class WimTapiIncongruentEndPoints(WimTapiError): + def __init__(self, services, service_endpoint_id): + MESSAGE = "Incongruent endpoints: services={:s} service_endpoint_id={:s}" + message = MESSAGE.format(str(services), str(service_endpoint_id)) + super().__init__(message, http_code=HTTPStatus.INTERNAL_SERVER_ERROR) + + +class WimTapiMissingConnPointField(WimTapiError): + def __init__(self, connection_point, field_name): + MESSAGE = "ConnectionPoint({:s}) has no field '{:s}'" + message = MESSAGE.format(str(connection_point), str(field_name)) + super().__init__(message, http_code=HTTPStatus.INTERNAL_SERVER_ERROR) + + +class WimTapiMissingMappingField(WimTapiError): + def __init__(self, mapping, field_name): + MESSAGE = "Mapping({:s}) has no field '{:s}'" + message = MESSAGE.format(str(mapping), str(field_name)) + super().__init__(message, http_code=HTTPStatus.INTERNAL_SERVER_ERROR) + + +class WimTapiServerNotAvailable(WimTapiError): + def __init__(self, message): + message = "Server not available: " + message + super().__init__(message, http_code=HTTPStatus.SERVICE_UNAVAILABLE) + + +class WimTapiServerRequestFailed(WimTapiError): + def __init__(self, message, http_code): + message = "Server request failed: " + message + super().__init__(message, http_code=http_code) + + +class WimTapiSipNotFound(WimTapiError): + def __init__(self, sip_id, sips): + MESSAGE = "SIP({:s}) not found in context SIPs({:s})" + message = MESSAGE.format(str(sip_id), str(sips)) + super().__init__(message, http_code=HTTPStatus.INTERNAL_SERVER_ERROR) + + +class WimTapiConnectivityServiceCreateFailed(WimTapiError): + def __init__(self, name, service_id, status_code, reply): + MESSAGE = "Create ConnectivityService({:s}, {:s}) Failed: reply={:s}" + message = MESSAGE.format(str(name), str(service_id), str(reply)) + super().__init__(message, http_code=status_code) + + +class WimTapiConnectivityServiceGetStatusFailed(WimTapiError): + def __init__(self, name, service_id, status_code, reply): + MESSAGE = "Get Status of ConnectivityService({:s}, {:s}) Failed: reply={:s}" + message = MESSAGE.format(str(name), str(service_id), str(reply)) + super().__init__(message, http_code=status_code) + + +class WimTapiConnectivityServiceDeleteFailed(WimTapiError): + def __init__(self, name, service_id, status_code, reply): + MESSAGE = "Delete ConnectivityService({:s}, {:s}) Failed: reply={:s}" + message = MESSAGE.format(str(name), str(service_id), str(reply)) + super().__init__(message, http_code=status_code) + + +class WimTapiUnsupportedServiceType(SdnConnectorError): + def __init__(self, service_type, supported_service_types): + MESSAGE = "Unsupported ServiceType({:s}). Supported ServiceTypes({:s})" + message = MESSAGE.format(str(service_type), str(supported_service_types)) + super().__init__(message, http_code=HTTPStatus.BAD_REQUEST) diff --git a/RO-SDN-tapi/osm_rosdn_tapi/log_messages.py b/RO-SDN-tapi/osm_rosdn_tapi/log_messages.py new file mode 100644 index 00000000..f8fc02ad --- /dev/null +++ b/RO-SDN-tapi/osm_rosdn_tapi/log_messages.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- + +####################################################################################### +# This file is part of OSM RO module +# +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +####################################################################################### +# This work has been performed in the context of the TeraFlow Project - +# funded by the European Commission under Grant number 101015857 through the +# Horizon 2020 program. +# Contributors: +# - Lluis Gifre +# - Ricard Vilalta +####################################################################################### + +"""This file contains the templete strings used to generate log messages for the +Transport API (TAPI) WIM connector.""" + +_PREFIX = "WIM TAPI Connector: " + + +LOG_MSG_CREATE_REQUEST = ( + _PREFIX + "Create Connectivity Service: Request {:s} {:s}: {:s}" +) + +LOG_MSG_CREATE_REPLY = ( + _PREFIX + + "Create Connectivity Service: Reply {:s} {:s}: status_code={:d} reply={:s}" +) + +LOG_MSG_GET_STATUS_REQUEST = ( + _PREFIX + "Get Connectivity Service Status: Request {:s} {:s}" +) + +LOG_MSG_GET_STATUS_REPLY = ( + _PREFIX + + "Get Connectivity Service Status: Reply {:s} {:s}: status_code={:d} reply={:s}" +) + +LOG_MSG_DELETE_REQUEST = ( + _PREFIX + "Delete Connectivity Service: Request {:s} {:s}: {:s}" +) + +LOG_MSG_DELETE_REPLY = ( + _PREFIX + + "Delete Connectivity Service: Reply {:s} {:s}: status_code={:d} reply={:s}" +) diff --git a/RO-SDN-tapi/osm_rosdn_tapi/message_composers.py b/RO-SDN-tapi/osm_rosdn_tapi/message_composers.py new file mode 100644 index 00000000..f5bcdae2 --- /dev/null +++ b/RO-SDN-tapi/osm_rosdn_tapi/message_composers.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- + +####################################################################################### +# This file is part of OSM RO module +# +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +####################################################################################### +# This work has been performed in the context of the TeraFlow Project - +# funded by the European Commission under Grant number 101015857 through the +# Horizon 2020 program. +# Contributors: +# - Lluis Gifre +# - Ricard Vilalta +####################################################################################### + +"""This file contains the helper methods used to compose the Transport API (TAPI) +messages sent by the TAPI WIM connector to the WIM.""" + + +import copy + +from .message_templates import ( + CREATE_TEMPLATE, + DELETE_TEMPLATE, + ENDPOINT_TEMPLATE, + REQUESTED_CAPACITY_TEMPLATE, + VLAN_CONSTRAINT_TEMPLATE, +) + + +def compose_requested_capacity(capacity, unit="GBPS"): + requested_capacity = copy.deepcopy(REQUESTED_CAPACITY_TEMPLATE) + total_size = requested_capacity["total-size"] + total_size["value"] = capacity + total_size["unit"] = "GBPS" + return requested_capacity + + +def compose_vlan_constraint(vlan_id): + vlan_constraint = copy.deepcopy(VLAN_CONSTRAINT_TEMPLATE) + vlan_constraint["vlan-id"] = vlan_id + return vlan_constraint + + +def compose_endpoint(sip): + sip_uuid = sip["uuid"] + endpoint = copy.deepcopy(ENDPOINT_TEMPLATE) + endpoint["service-interface-point"]["service-interface-point-uuid"] = sip_uuid + endpoint["layer-protocol-name"] = sip["layer-protocol-name"] + # TODO: implement smart selection of layer-protocol-qualifier instead of selecting first one available + supported_layer_protocol_qualifier = sip["supported-layer-protocol-qualifier"][0] + endpoint["layer-protocol-qualifier"] = supported_layer_protocol_qualifier + endpoint["local-id"] = sip_uuid + return endpoint + + +def compose_create_request( + service_uuid, + endpoints, + bidirectional=False, + requested_capacity=None, + vlan_constraint=None, +): + request = copy.deepcopy(CREATE_TEMPLATE) + con_svc = request["tapi-connectivity:connectivity-service"][0] + con_svc["uuid"] = service_uuid + con_svc["connectivity-direction"] = ( + "BIDIRECTIONAL" if bidirectional else "UNIDIRECTIONAL" + ) + con_svc["end-point"] = endpoints + if requested_capacity is not None: + con_svc["requested-capacity"] = requested_capacity + if vlan_constraint is not None: + con_svc["vlan-constraint"] = vlan_constraint + return request + + +def compose_delete_request(service_uuid): + request = copy.deepcopy(DELETE_TEMPLATE) + request["tapi-connectivity:input"]["uuid"] = service_uuid + return request diff --git a/RO-SDN-tapi/osm_rosdn_tapi/message_templates.py b/RO-SDN-tapi/osm_rosdn_tapi/message_templates.py new file mode 100644 index 00000000..ec2f0a16 --- /dev/null +++ b/RO-SDN-tapi/osm_rosdn_tapi/message_templates.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- + +####################################################################################### +# This file is part of OSM RO module +# +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +####################################################################################### +# This work has been performed in the context of the TeraFlow Project - +# funded by the European Commission under Grant number 101015857 through the +# Horizon 2020 program. +# Contributors: +# - Lluis Gifre +# - Ricard Vilalta +####################################################################################### + +"""This file contains the template JSON-encoded messages used to compose the Transport +API (TAPI) messages sent by the TAPI WIM connector to the WIM.""" + +REQUESTED_CAPACITY_TEMPLATE = {"total-size": {"value": None, "unit": "GBPS"}} + +VLAN_CONSTRAINT_TEMPLATE = {"vlan-id": None} + +ENDPOINT_TEMPLATE = { + "service-interface-point": {"service-interface-point-uuid": None}, + "layer-protocol-name": None, + "layer-protocol-qualifier": None, + "local-id": None, +} + +CREATE_TEMPLATE = { + "tapi-connectivity:connectivity-service": [ + { + "uuid": None, + # "requested-capacity": REQUESTED_CAPACITY_TEMPLATE, + "connectivity-direction": "UNIDIRECTIONAL", + "end-point": [], + # "vlan-constraint": VLAN_CONSTRAINT_TEMPLATE, + } + ] +} + +DELETE_TEMPLATE = {"tapi-connectivity:input": {"uuid": None}} diff --git a/RO-SDN-tapi/osm_rosdn_tapi/services_composer.py b/RO-SDN-tapi/osm_rosdn_tapi/services_composer.py new file mode 100644 index 00000000..720e4230 --- /dev/null +++ b/RO-SDN-tapi/osm_rosdn_tapi/services_composer.py @@ -0,0 +1,151 @@ +# -*- coding: utf-8 -*- + +####################################################################################### +# This file is part of OSM RO module +# +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +####################################################################################### +# This work has been performed in the context of the TeraFlow Project - +# funded by the European Commission under Grant number 101015857 through the +# Horizon 2020 program. +# Contributors: +# - Lluis Gifre +# - Ricard Vilalta +####################################################################################### + +"""This file contains the ServiceComposer class used by the Transport API (TAPI) WIM +connector to compose the services based on the service_endpoint_ids and their +directionality.""" + +from .exceptions import ( + WimTapiIncongruentDirectionality, + WimTapiIncongruentEndPoints, + WimTapiMissingMappingField, + WimTapiSipNotFound, +) +from .message_composers import ( + compose_endpoint, + compose_requested_capacity, + # compose_vlan_constraint, +) + + +class ServicesComposer: + def __init__(self, service_interface_points) -> None: + self.sips = service_interface_points + + # if unidirectional + # - a single service_endpoint item is created + # - the service_endpoint item contains with the 2 bidirectional SIPs + # if bidirectional + # - two service_endpoint items are created + # - each service_endpoint item containing a list of 2 unidirectional SIPs (in, out) + self.services = list() + + # TODO: populate dynamically capacity of the connection + self.requested_capacity = compose_requested_capacity(1, unit="GBPS") + + self.vlan_constraint = None + # TODO: VLAN needs to be processed by connection point; by now deactivated + # if connection_point.get("service_endpoint_encapsulation_type") == "dot1q": + # encap_info = connection_point.get("service_endpoint_encapsulation_info", {}) + # vlan_id = encap_info.get("vlan") + # if vlan_id is not None: + # vlan_constraint = compose_vlan_constraint(vlan_id) + + def add_bidirectional(self, service_endpoint_id): + if len(self.services) == 0: + # assume bidirectional, SIP is service_endpoint_id + service_interface_point = self.sips[service_endpoint_id] + self.services.append([compose_endpoint(service_interface_point)]) + elif len(self.services) == 1: + # is bidirectional, SIP is service_endpoint_id + if len(self.services[0]) > 1: + # too much endpoints per service + raise WimTapiIncongruentEndPoints(self.services, service_endpoint_id) + self.services[0].append(compose_endpoint(self.sips[service_endpoint_id])) + else: + raise WimTapiIncongruentDirectionality(self.services, service_endpoint_id) + + def add_unidirectional(self, service_endpoint_id, sip_input, sip_output): + if len(self.services) == 0: + # assume unidirectional + self.services.append([compose_endpoint(self.sips[sip_output])]) # AZ + self.services.append([compose_endpoint(self.sips[sip_input])]) # ZA + elif len(self.services) == 2: + # is unidirectional + + if len(self.services[0]) > 1: + # too much endpoints per service + raise WimTapiIncongruentEndPoints(self.services[0], service_endpoint_id) + self.services[0].append(compose_endpoint(self.sips[sip_input])) # AZ + + if len(self.services[1]) > 1: + # too much endpoints per service + raise WimTapiIncongruentEndPoints(self.services[1], service_endpoint_id) + self.services[1].insert(0, compose_endpoint(self.sips[sip_output])) # ZA + else: + raise WimTapiIncongruentDirectionality(self.services, service_endpoint_id) + + def add_service_endpoint(self, service_endpoint_id, mapping): + service_mapping_info = mapping.get("service_mapping_info", {}) + + if ( + len(service_mapping_info) == 0 + or "sip_input" not in service_mapping_info + or "sip_output" not in service_mapping_info + ): + # bidirectional (no mapping or no sip_input or no sip_output) + if service_endpoint_id not in self.sips: + raise WimTapiSipNotFound(service_endpoint_id, self.sips) + self.add_bidirectional(service_endpoint_id) + + else: + # unidirectional, sip_input and sip_output provided in mapping + + sip_input = service_mapping_info.get("sip_input") + if sip_input is None: + raise WimTapiMissingMappingField( + mapping, "service_mapping_info.sip_input" + ) + + if sip_input not in self.sips: + raise WimTapiSipNotFound(sip_input, self.sips) + + sip_output = service_mapping_info.get("sip_output") + if sip_output is None: + raise WimTapiMissingMappingField( + mapping, "service_mapping_info.sip_output" + ) + + if sip_output not in self.sips: + raise WimTapiSipNotFound(sip_output, self.sips) + + self.add_unidirectional(service_endpoint_id, sip_input, sip_output) + + def is_bidirectional(self): + return len(self.services) == 1 + + def dump(self, logger): + str_data = "\n".join( + [ + "services_composer {", + " services={:s}".format(str(self.services)), + " requested_capacity={:s}".format(str(self.requested_capacity)), + " vlan_constraint={:s}".format(str(self.vlan_constraint)), + "}", + ] + ) + logger.debug(str_data) diff --git a/RO-SDN-tapi/osm_rosdn_tapi/tapi_client.py b/RO-SDN-tapi/osm_rosdn_tapi/tapi_client.py new file mode 100644 index 00000000..ad5f9f3a --- /dev/null +++ b/RO-SDN-tapi/osm_rosdn_tapi/tapi_client.py @@ -0,0 +1,240 @@ +# -*- coding: utf-8 -*- + +####################################################################################### +# This file is part of OSM RO module +# +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +####################################################################################### +# This work has been performed in the context of the TeraFlow Project - +# funded by the European Commission under Grant number 101015857 through the +# Horizon 2020 program. +# Contributors: +# - Lluis Gifre +# - Ricard Vilalta +####################################################################################### + +"""This file contains the TransportApiClient class used by the Transport API +(TAPI) WIM connector to interact with the underlying WIM.""" + +import requests + +from .exceptions import ( + WimTapiConnectivityServiceCreateFailed, + WimTapiConnectivityServiceDeleteFailed, + WimTapiConnectivityServiceGetStatusFailed, + WimTapiServerNotAvailable, + WimTapiServerRequestFailed, +) +from .log_messages import ( + LOG_MSG_CREATE_REPLY, + LOG_MSG_CREATE_REQUEST, + LOG_MSG_DELETE_REPLY, + LOG_MSG_DELETE_REQUEST, + LOG_MSG_GET_STATUS_REPLY, + LOG_MSG_GET_STATUS_REQUEST, +) +from .message_composers import ( + compose_create_request, + compose_delete_request, +) + +DEFAULT_TIMEOUT = 30 + +SUCCESS_HTTP_CODES = { + requests.codes.ok, # pylint: disable=no-member + requests.codes.created, # pylint: disable=no-member + requests.codes.accepted, # pylint: disable=no-member + requests.codes.no_content, # pylint: disable=no-member +} + +RESTCONF_DATA_URL = "{:s}/restconf/data" +RESTCONF_OPER_URL = "{:s}/restconf/operations" + +CONTEXT_URL = RESTCONF_DATA_URL + "/tapi-common:context" +CTX_SIPS_URL = CONTEXT_URL + "/service-interface-point" +CONN_CTX_URL = CONTEXT_URL + "/tapi-connectivity:connectivity-context" +CONN_SVC_URL = CONN_CTX_URL + "/connectivity-service" +DELETE_URL = RESTCONF_OPER_URL + "/tapi-connectivity:delete-connectivity-service" + + +class TransportApiClient: + def __init__(self, logger, wim, wim_account, config) -> None: + self.logger = logger + self.wim_url = wim["wim_url"] + + user = wim_account.get("user") + password = wim_account.get("password") + self.auth = ( + None + if user is None or user == "" or password is None or password == "" + else (user, password) + ) + + self.headers = {"Content-Type": "application/json"} + self.timeout = int(config.get("timeout", DEFAULT_TIMEOUT)) + + def get_root_context(self): + context_url = CONTEXT_URL.format(self.wim_url) + + try: + response = requests.get( + context_url, auth=self.auth, headers=self.headers, timeout=self.timeout + ) + http_code = response.status_code + except requests.exceptions.RequestException as e: + raise WimTapiServerNotAvailable(str(e)) + + if http_code != 200: + raise WimTapiServerRequestFailed( + "Unexpected status code", http_code=http_code + ) + + return response.json() + + def get_service_interface_points(self): + get_sips_url = CTX_SIPS_URL.format(self.wim_url) + + try: + response = requests.get( + get_sips_url, auth=self.auth, headers=self.headers, timeout=self.timeout + ) + http_code = response.status_code + except requests.exceptions.RequestException as e: + raise WimTapiServerNotAvailable(str(e)) + + if http_code != 200: + raise WimTapiServerRequestFailed( + "Unexpected status code", http_code=http_code + ) + + response = response.json() + response = response.get("tapi-common:service-interface-point", []) + return {sip["uuid"]: sip for sip in response} + + def get_service_status(self, name, service_uuid): + self.logger.debug(LOG_MSG_GET_STATUS_REQUEST.format(name, service_uuid)) + + try: + services_url = CONN_SVC_URL.format(self.wim_url) + response = requests.get( + services_url, auth=self.auth, headers=self.headers, timeout=self.timeout + ) + self.logger.debug( + LOG_MSG_GET_STATUS_REPLY.format( + name, service_uuid, response.status_code, response.text + ) + ) + except requests.exceptions.ConnectionError as e: + status_code = e.response.status_code if e.response is not None else 500 + content = e.response.text if e.response is not None else "" + raise WimTapiConnectivityServiceGetStatusFailed( + name, service_uuid, status_code, content + ) + + if response.status_code not in SUCCESS_HTTP_CODES: + raise WimTapiConnectivityServiceGetStatusFailed( + name, service_uuid, response.status_code, response.text + ) + + json_response = response.json() + connectivity_services = json_response.get( + "tapi-connectivity:connectivity-service", [] + ) + connectivity_service = next( + iter( + [ + connectivity_service + for connectivity_service in connectivity_services + if connectivity_service.get("uuid") == service_uuid + ] + ), + None, + ) + + if connectivity_service is None: + service_status = {"sdn_status": "ERROR"} + else: + service_status = {"sdn_status": "ACTIVE"} + return service_status + + def create_service( + self, + name, + service_uuid, + service_endpoints, + bidirectional=False, + requested_capacity=None, + vlan_constraint=None, + ): + request_create = compose_create_request( + service_uuid, + service_endpoints, + bidirectional=bidirectional, + requested_capacity=requested_capacity, + vlan_constraint=vlan_constraint, + ) + self.logger.debug( + LOG_MSG_CREATE_REQUEST.format(name, service_uuid, str(request_create)) + ) + + try: + create_url = CONN_CTX_URL.format(self.wim_url) + response = requests.post( + create_url, headers=self.headers, json=request_create, auth=self.auth + ) + self.logger.debug( + LOG_MSG_CREATE_REPLY.format( + name, service_uuid, response.status_code, response.text + ) + ) + except requests.exceptions.ConnectionError as e: + status_code = e.response.status_code if e.response is not None else 500 + content = e.response.text if e.response is not None else "" + raise WimTapiConnectivityServiceCreateFailed( + name, service_uuid, status_code, content + ) + + if response.status_code not in SUCCESS_HTTP_CODES: + raise WimTapiConnectivityServiceCreateFailed( + name, service_uuid, response.status_code, response.text + ) + + def delete_service(self, name, service_uuid): + request_delete = compose_delete_request(service_uuid) + self.logger.debug( + LOG_MSG_DELETE_REQUEST.format(name, service_uuid, str(request_delete)) + ) + + try: + delete_url = DELETE_URL.format(self.wim_url) + response = requests.post( + delete_url, headers=self.headers, json=request_delete, auth=self.auth + ) + self.logger.debug( + LOG_MSG_DELETE_REPLY.format( + name, service_uuid, response.status_code, response.text + ) + ) + except requests.exceptions.ConnectionError as e: + status_code = e.response.status_code if e.response is not None else 500 + content = e.response.text if e.response is not None else "" + raise WimTapiConnectivityServiceDeleteFailed( + name, service_uuid, status_code, content + ) + + if response.status_code not in SUCCESS_HTTP_CODES: + raise WimTapiConnectivityServiceDeleteFailed( + name, service_uuid, response.status_code, response.text + ) diff --git a/RO-SDN-tapi/osm_rosdn_tapi/tests/__init__.py b/RO-SDN-tapi/osm_rosdn_tapi/tests/__init__.py new file mode 100644 index 00000000..ab3006ab --- /dev/null +++ b/RO-SDN-tapi/osm_rosdn_tapi/tests/__init__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- + +####################################################################################### +# This file is part of OSM RO module +# +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +####################################################################################### +# This work has been performed in the context of the TeraFlow Project - +# funded by the European Commission under Grant number 101015857 through the +# Horizon 2020 program. +####################################################################################### diff --git a/RO-SDN-tapi/osm_rosdn_tapi/tests/constants.py b/RO-SDN-tapi/osm_rosdn_tapi/tests/constants.py new file mode 100644 index 00000000..a9808ba6 --- /dev/null +++ b/RO-SDN-tapi/osm_rosdn_tapi/tests/constants.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- + +####################################################################################### +# This file is part of OSM RO module +# +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +####################################################################################### +# This work has been performed in the context of the TeraFlow Project - +# funded by the European Commission under Grant number 101015857 through the +# Horizon 2020 program. +# Contributors: +# - Lluis Gifre +# - Ricard Vilalta +####################################################################################### + +"""This file contains the WIM settings for the unit test used to validate the +Transport API (TAPI) WIM connector.""" + + +from osm_rosdn_tapi.tests.tools import wim_port_mapping + + +WIM_HOST_PORT = ("127.0.0.127", 49000) + +# WIM_URL should be populated with the WIM url provided for the WIM connector during its instantiation +WIM_URL = "http://{:s}:{:d}".format(*WIM_HOST_PORT) + +# WIM_ACCOUNT should be populated with the WIM credentials provided for the WIM connector during its instantiation +WIM_ACCOUNT = {"user": "admin", "password": "admin"} + +# WIM_PORT_MAPPING should be populated with the port mapping provided for the WIM connector during its instantiation +# In this example, SIPs are taken from mock_tapi_handler.py file. +WIM_PORT_MAPPING = [ + wim_port_mapping( + "dc1", + "dc1r1", + "eth0", + "R1-eth0", + service_mapping_info={}, + ), + wim_port_mapping( + "dc2", + "dc2r2", + "eth0", + "R2-eth0", + service_mapping_info={}, + ), + wim_port_mapping( + "dc3", + "dc3r3", + "eth0", + "R3-opt1", + service_mapping_info={ + "sip_input": "R3-opt1-rx", + "sip_output": "R3-opt1-tx", + }, + ), + wim_port_mapping( + "dc4", + "dc4r4", + "eth0", + "R4-opt1", + service_mapping_info={ + "sip_input": "R4-opt1-rx", + "sip_output": "R4-opt1-tx", + }, + ), +] diff --git a/RO-SDN-tapi/osm_rosdn_tapi/tests/exceptions.py b/RO-SDN-tapi/osm_rosdn_tapi/tests/exceptions.py new file mode 100644 index 00000000..f52a020d --- /dev/null +++ b/RO-SDN-tapi/osm_rosdn_tapi/tests/exceptions.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- + +####################################################################################### +# This file is part of OSM RO module +# +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +####################################################################################### +# This work has been performed in the context of the TeraFlow Project - +# funded by the European Commission under Grant number 101015857 through the +# Horizon 2020 program. +# Contributors: +# - Lluis Gifre +# - Ricard Vilalta +####################################################################################### + +"""This file contains the exception classes the Mock OSM RO module can raise.""" + + +_PREFIX = "Mock OSM RO: " + + +class MockOsmRoError(Exception): + """Base Exception for all Mock OSM RO related errors.""" + + def __init__(self, message): + super().__init__(_PREFIX + message) + + +class MockOsmRoServiceNotFound(MockOsmRoError): + def __init__(self, service_id): + MESSAGE = "ServiceId({:s}) not found" + message = MESSAGE.format(str(service_id)) + super().__init__(message) diff --git a/RO-SDN-tapi/osm_rosdn_tapi/tests/mock_osm_ro.py b/RO-SDN-tapi/osm_rosdn_tapi/tests/mock_osm_ro.py new file mode 100644 index 00000000..71c21486 --- /dev/null +++ b/RO-SDN-tapi/osm_rosdn_tapi/tests/mock_osm_ro.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- + +####################################################################################### +# This file is part of OSM RO module +# +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +####################################################################################### +# This work has been performed in the context of the TeraFlow Project - +# funded by the European Commission under Grant number 101015857 through the +# Horizon 2020 program. +# Contributors: +# - Lluis Gifre +# - Ricard Vilalta +####################################################################################### + +"""This file contains a Mock OSM RO component that can be used for rapid unit testing. + +This code is based on code taken with permission from ETSI TeraFlowSDN project at: + https://labs.etsi.org/rep/tfs/controller +""" + + +from typing import Dict, List + +from osm_ro_plugin.sdnconn import SdnConnectorBase + +from .exceptions import MockOsmRoServiceNotFound + + +class MockOsmRo: + def __init__( + self, + klass: SdnConnectorBase, + url: str, + wim_account: Dict, + wim_port_mapping: Dict, + ) -> None: + wim = {"wim_url": url} + config = { + "mapping_not_needed": False, + "service_endpoint_mapping": wim_port_mapping, + } + + # Instantiate WIM connector + self.wim_connector = klass(wim, wim_account, config=config) + + # Internal DB emulating OSM RO storage provided to WIM Connectors + self.conn_info = {} + + def create_connectivity_service( + self, service_type: str, connection_points: List[Dict] + ) -> str: + self.wim_connector.check_credentials() + service_uuid, conn_info = self.wim_connector.create_connectivity_service( + service_type, connection_points + ) + self.conn_info[service_uuid] = conn_info + return service_uuid + + def get_connectivity_service_status(self, service_uuid: str) -> Dict: + conn_info = self.conn_info.get(service_uuid) + if conn_info is None: + raise MockOsmRoServiceNotFound(service_uuid) + self.wim_connector.check_credentials() + return self.wim_connector.get_connectivity_service_status( + service_uuid, conn_info=conn_info + ) + + def edit_connectivity_service( + self, service_uuid: str, connection_points: List[Dict] + ) -> None: + conn_info = self.conn_info.get(service_uuid) + if conn_info is None: + raise MockOsmRoServiceNotFound(service_uuid) + self.wim_connector.check_credentials() + self.wim_connector.edit_connectivity_service( + service_uuid, conn_info=conn_info, connection_points=connection_points + ) + + def delete_connectivity_service(self, service_uuid: str) -> None: + conn_info = self.conn_info.get(service_uuid) + if conn_info is None: + raise MockOsmRoServiceNotFound(service_uuid) + self.wim_connector.check_credentials() + self.wim_connector.delete_connectivity_service( + service_uuid, conn_info=conn_info + ) diff --git a/RO-SDN-tapi/osm_rosdn_tapi/tests/mock_tapi_handler.py b/RO-SDN-tapi/osm_rosdn_tapi/tests/mock_tapi_handler.py new file mode 100644 index 00000000..5f38210a --- /dev/null +++ b/RO-SDN-tapi/osm_rosdn_tapi/tests/mock_tapi_handler.py @@ -0,0 +1,174 @@ +# -*- coding: utf-8 -*- + +####################################################################################### +# This file is part of OSM RO module +# +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +####################################################################################### +# This work has been performed in the context of the TeraFlow Project - +# funded by the European Commission under Grant number 101015857 through the +# Horizon 2020 program. +# Contributors: +# - Lluis Gifre +# - Ricard Vilalta +####################################################################################### + +"""This file contains a minimalistic Mock Transport API (TAPI) WIM server.""" + +import http.server +import json +import uuid + + +PHOTONIC_PROTOCOL_QUALIFIER = "tapi-photonic-media:PHOTONIC_LAYER_QUALIFIER_NMC" +DSR_PROTOCOL_QUALIFIER = "tapi-dsr:DIGITAL_SIGNAL_TYPE" + + +def compose_sip( + uuid, layer_protocol_name, supported_layer_protocol_qualifier, direction +): + return { + "uuid": uuid, + "layer-protocol-name": layer_protocol_name, + "supported-layer-protocol-qualifier": [supported_layer_protocol_qualifier], + "administrative-state": "UNLOCKED", + "operational-state": "ENABLED", + "direction": direction, + } + + +def compose_sip_dsr(uuid): + return compose_sip(uuid, "DSR", DSR_PROTOCOL_QUALIFIER, "BIDIRECTIONAL") + + +def compose_sip_photonic_input(uuid): + return compose_sip(uuid, "PHOTONIC_MEDIA", PHOTONIC_PROTOCOL_QUALIFIER, "INPUT") + + +def compose_sip_photonic_output(uuid): + return compose_sip(uuid, "PHOTONIC_MEDIA", PHOTONIC_PROTOCOL_QUALIFIER, "OUTPUT") + + +CONTEXT = { + "uuid": str(uuid.uuid4()), + "service-interface-point": [ + compose_sip_dsr("R1-eth0"), + compose_sip_dsr("R2-eth0"), + compose_sip_photonic_input("R3-opt1-rx"), + compose_sip_photonic_output("R3-opt1-tx"), + compose_sip_photonic_input("R4-opt1-rx"), + compose_sip_photonic_output("R4-opt1-tx"), + ], + # topology details not used by the WIM connector + "topology-context": {}, + "connectivity-context": {"connectivity-service": [], "connection": []}, +} + + +class MockTapiRequestHandler(http.server.BaseHTTPRequestHandler): + """Mock TAPI Request Handler for the unit tests""" + + def do_GET(self): # pylint: disable=invalid-name + """Handle GET requests""" + path = self.path.replace("tapi-common:", "").replace("tapi-connectivity:", "") + + if path == "/restconf/data/context": + status = 200 # ok + headers = {"Content-Type": "application/json"} + data = CONTEXT + elif path == "/restconf/data/context/service-interface-point": + status = 200 # ok + headers = {"Content-Type": "application/json"} + data = CONTEXT["service-interface-point"] + data = {"tapi-common:service-interface-point": data} + elif path == "/restconf/data/context/connectivity-context/connectivity-service": + status = 200 # ok + headers = {"Content-Type": "application/json"} + data = CONTEXT["connectivity-context"]["connectivity-service"] + data = {"tapi-connectivity:connectivity-service": data} + else: + status = 404 # not found + headers = {} + data = {"error": "Not found"} + + self.send_response(status) + for header_name, header_value in headers.items(): + self.send_header(header_name, header_value) + self.end_headers() + data = json.dumps(data) + self.wfile.write(data.encode("UTF-8")) + + def do_POST(self): # pylint: disable=invalid-name + """Handle POST requests""" + path = self.path.replace("tapi-common:", "").replace("tapi-connectivity:", "") + length = int(self.headers["content-length"]) + data = json.loads(self.rfile.read(length)) + + if path == "/restconf/data/context/connectivity-context": + if "tapi-connectivity:connectivity-service" in data: + data["connectivity-service"] = data.pop( + "tapi-connectivity:connectivity-service" + ) + + if ( + isinstance(data["connectivity-service"], list) + and len(data["connectivity-service"]) > 0 + ): + data["connectivity-service"] = data["connectivity-service"][0] + + conn_svc = data["connectivity-service"] + if "connectivity-constraint" in conn_svc: + conn_constr = conn_svc.pop("connectivity-constraint") + if "requested-capacity" in conn_constr: + req_cap = conn_constr.pop("requested-capacity") + conn_svc["requested-capacity"] = req_cap + if "connectivity-direction" in conn_constr: + conn_dir = conn_constr.pop("connectivity-direction") + conn_svc["connectivity-direction"] = conn_dir + + connection = {"uuid": conn_svc["uuid"], "connection-end-point": []} + conn_svc["connection"] = [{"connection_uuid": conn_svc["uuid"]}] + + CONTEXT["connectivity-context"]["connection"].append(connection) + CONTEXT["connectivity-context"]["connectivity-service"].append(conn_svc) + + status = 201 # created + headers = {} + elif path == "/restconf/operations/delete-connectivity-service": + if "tapi-connectivity:input" in data: + data["input"] = data.pop("tapi-connectivity:input") + conn_svc_uuid = data["input"]["uuid"] + conn_ctx = CONTEXT["connectivity-context"] + + # keep connectivity services and connections with different uuid + conn_ctx["connection"] = [ + conn for conn in conn_ctx["connection"] if conn["uuid"] != conn_svc_uuid + ] + conn_ctx["connectivity-service"] = [ + conn_svc + for conn_svc in conn_ctx["connectivity-service"] + if conn_svc["uuid"] != conn_svc_uuid + ] + + status = 204 # ok, no content + headers = {} + else: + status = 404 # not found + headers = {} + + self.send_response(status) + for header_name, header_value in headers.items(): + self.send_header(header_name, header_value) + self.end_headers() diff --git a/RO-SDN-tapi/osm_rosdn_tapi/tests/test_wim_tapi.py b/RO-SDN-tapi/osm_rosdn_tapi/tests/test_wim_tapi.py new file mode 100644 index 00000000..8c18b86b --- /dev/null +++ b/RO-SDN-tapi/osm_rosdn_tapi/tests/test_wim_tapi.py @@ -0,0 +1,179 @@ +# -*- coding: utf-8 -*- + +####################################################################################### +# This file is part of OSM RO module +# +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +####################################################################################### +# This work has been performed in the context of the TeraFlow Project - +# funded by the European Commission under Grant number 101015857 through the +# Horizon 2020 program. +# Contributors: +# - Lluis Gifre +# - Ricard Vilalta +####################################################################################### + +"""This file contains the unit tests for the Transport API (TAPI) WIM connector.""" + +import http.server +import threading +import unittest + +from osm_rosdn_tapi.exceptions import ( + WimTapiConnectionPointsBadFormat, + WimTapiMissingConnPointField, + WimTapiUnsupportedServiceType, +) +from osm_rosdn_tapi.tests.constants import ( + WIM_ACCOUNT, + WIM_HOST_PORT, + WIM_PORT_MAPPING, + WIM_URL, +) +from osm_rosdn_tapi.tests.mock_osm_ro import MockOsmRo +from osm_rosdn_tapi.tests.mock_tapi_handler import MockTapiRequestHandler +from osm_rosdn_tapi.wimconn_tapi import WimconnectorTAPI + + +SERVICE_TYPE = "ELINE" +SERVICE_CONNECTION_POINTS_BIDIRECTIONAL = [ + # SIPs taken from mock_tapi_handler.py + {"service_endpoint_id": "R1-eth0"}, + {"service_endpoint_id": "R2-eth0"}, +] +SERVICE_CONNECTION_POINTS_UNIDIRECTIONAL = [ + # SIPs taken from mock_tapi_handler.py + {"service_endpoint_id": "R3-opt1"}, + {"service_endpoint_id": "R4-opt1"}, +] + + +class UnitTests(unittest.TestCase): + """Unit tests for Transport API WIM connector""" + + def setUp(self) -> None: + self.wim_server = http.server.ThreadingHTTPServer( + WIM_HOST_PORT, MockTapiRequestHandler + ) + + def test_wrong_cases(self): + with self.wim_server: + wim_server_thread = threading.Thread(target=self.wim_server.serve_forever) + wim_server_thread.daemon = True + wim_server_thread.start() + + mock_osm_ro_tapi = MockOsmRo( + WimconnectorTAPI, WIM_URL, WIM_ACCOUNT, WIM_PORT_MAPPING + ) + + # Unsupported service type + with self.assertRaises(WimTapiUnsupportedServiceType) as test_context: + mock_osm_ro_tapi.create_connectivity_service( + "ELAN", SERVICE_CONNECTION_POINTS_BIDIRECTIONAL + ) + self.assertEqual( + str(test_context.exception.args[0]), + "Unsupported ServiceType(ELAN). Supported ServiceTypes({'ELINE'})", + ) + + # Wrong number of connection_points + with self.assertRaises(WimTapiConnectionPointsBadFormat) as test_context: + mock_osm_ro_tapi.create_connectivity_service(SERVICE_TYPE, []) + self.assertEqual( + str(test_context.exception.args[0]), + "ConnectionPoints([]) must be a list or tuple of length 2", + ) + + # Wrong type of connection_points + with self.assertRaises(WimTapiConnectionPointsBadFormat) as test_context: + mock_osm_ro_tapi.create_connectivity_service( + SERVICE_TYPE, {"a": "b", "c": "d"} + ) + self.assertEqual( + str(test_context.exception.args[0]), + "ConnectionPoints({'a': 'b', 'c': 'd'}) must be a list or tuple of length 2", + ) + + with self.assertRaises(WimTapiMissingConnPointField) as test_context: + mock_osm_ro_tapi.create_connectivity_service( + SERVICE_TYPE, + [ + {"wrong_service_endpoint_id": "value"}, + {"service_endpoint_id": "value"}, + ], + ) + self.assertEqual( + str(test_context.exception.args[0]), + "WIM TAPI Connector: ConnectionPoint({'wrong_service_endpoint_id': 'value'}) has no field 'service_endpoint_id'", + ) + + self.wim_server.shutdown() + wim_server_thread.join() + + def test_correct_bidirectional(self): + with self.wim_server: + wim_server_thread = threading.Thread(target=self.wim_server.serve_forever) + wim_server_thread.daemon = True + wim_server_thread.start() + + mock_osm_ro_tapi = MockOsmRo( + WimconnectorTAPI, WIM_URL, WIM_ACCOUNT, WIM_PORT_MAPPING + ) + + # Create bidirectional TAPI service + service_uuid = mock_osm_ro_tapi.create_connectivity_service( + SERVICE_TYPE, SERVICE_CONNECTION_POINTS_BIDIRECTIONAL + ) + self.assertIsInstance(service_uuid, str) + + # Check status of bidirectional TAPI service + status = mock_osm_ro_tapi.get_connectivity_service_status(service_uuid) + self.assertIsInstance(status, dict) + self.assertIn("sdn_status", status) + self.assertEqual(status["sdn_status"], "ACTIVE") + + # Delete bidirectional TAPI service + mock_osm_ro_tapi.delete_connectivity_service(service_uuid) + + self.wim_server.shutdown() + wim_server_thread.join() + + def test_correct_unidirectional(self): + with self.wim_server: + wim_server_thread = threading.Thread(target=self.wim_server.serve_forever) + wim_server_thread.daemon = True + wim_server_thread.start() + + mock_osm_ro_tapi = MockOsmRo( + WimconnectorTAPI, WIM_URL, WIM_ACCOUNT, WIM_PORT_MAPPING + ) + + # Create unidirectional TAPI service + service_uuid = mock_osm_ro_tapi.create_connectivity_service( + SERVICE_TYPE, SERVICE_CONNECTION_POINTS_UNIDIRECTIONAL + ) + self.assertIsInstance(service_uuid, str) + + # Check status of unidirectional TAPI service + status = mock_osm_ro_tapi.get_connectivity_service_status(service_uuid) + self.assertIsInstance(status, dict) + self.assertIn("sdn_status", status) + self.assertEqual(status["sdn_status"], "ACTIVE") + + # Delete unidirectional TAPI service + mock_osm_ro_tapi.delete_connectivity_service(service_uuid) + + self.wim_server.shutdown() + wim_server_thread.join() diff --git a/RO-SDN-tapi/osm_rosdn_tapi/tests/tools.py b/RO-SDN-tapi/osm_rosdn_tapi/tests/tools.py new file mode 100644 index 00000000..b75543f4 --- /dev/null +++ b/RO-SDN-tapi/osm_rosdn_tapi/tests/tools.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- + +####################################################################################### +# This file is part of OSM RO module +# +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +####################################################################################### +# This work has been performed in the context of the TeraFlow Project - +# funded by the European Commission under Grant number 101015857 through the +# Horizon 2020 program. +# Contributors: +# - Lluis Gifre +# - Ricard Vilalta +####################################################################################### + +"""This file contains a helper methods for the Mock OSM RO component that can be used +for rapid unit testing. + +This code is based on code taken with permission from ETSI TeraFlowSDN project at: + https://labs.etsi.org/rep/tfs/controller +""" + +from typing import Dict, Optional + + +# Ref: https://osm.etsi.org/wikipub/index.php/WIM +# Fields defined according to from osm_ro_plugin.sdnconn import SdnConnectorBase +def wim_port_mapping( + datacenter_id: str, + device_id: str, + device_interface_id: str, + service_endpoint_id: str, + switch_dpid: Optional[str] = None, + switch_port: Optional[str] = None, + service_mapping_info: Dict = {}, +): + mapping = { + "datacenter_id": datacenter_id, + "device_id": device_id, + "device_interface_id": device_interface_id, + "service_endpoint_id": service_endpoint_id, + "service_mapping_info": service_mapping_info, + } + if switch_dpid is not None: + mapping["switch_dpid"] = switch_dpid + if switch_port is not None: + mapping["switch_port"] = switch_port + return mapping diff --git a/RO-SDN-tapi/osm_rosdn_tapi/wimconn_tapi.py b/RO-SDN-tapi/osm_rosdn_tapi/wimconn_tapi.py new file mode 100644 index 00000000..a89f4c86 --- /dev/null +++ b/RO-SDN-tapi/osm_rosdn_tapi/wimconn_tapi.py @@ -0,0 +1,346 @@ +# -*- coding: utf-8 -*- + +####################################################################################### +# This file is part of OSM RO module +# +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +####################################################################################### +# This work has been performed in the context of the TeraFlow Project - +# funded by the European Commission under Grant number 101015857 through the +# Horizon 2020 program. +# Contributors: +# - Lluis Gifre +# - Ricard Vilalta +####################################################################################### + +"""The SDN/WIM connector is responsible for establishing wide area network +connectivity. + +This SDN/WIM connector implements the standard ONF Transport API (TAPI). + +It receives the endpoints and the necessary details to request the Layer 2 +service through the use of the ONF Transport API. +""" + +import logging +import uuid + +from osm_ro_plugin.sdnconn import SdnConnectorBase + +from .conn_info import ( + conn_info_compose_bidirectional, + conn_info_compose_unidirectional, +) +from .exceptions import ( + WimTapiConnectionPointsBadFormat, + WimTapiMissingConnPointField, + WimTapiUnsupportedServiceType, +) +from .services_composer import ServicesComposer +from .tapi_client import TransportApiClient + + +class WimconnectorTAPI(SdnConnectorBase): + """ONF TAPI WIM connector""" + + def __init__(self, wim, wim_account, config=None, logger=None): + """ONF TAPI WIM connector + + Arguments: + wim (dict): WIM record, as stored in the database + wim_account (dict): WIM account record, as stored in the database + config (optional dict): optional configuration from the configuration database + logger (optional Logger): logger to use with this WIM connector + The arguments of the constructor are converted to object attributes. + An extra property, ``service_endpoint_mapping`` is created from ``config``. + """ + logger = logger or logging.getLogger("ro.sdn.tapi") + + super().__init__(wim, wim_account, config, logger) + + self.logger.debug("self.config={:s}".format(str(self.config))) + + if len(self.service_endpoint_mapping) == 0 and self.config.get( + "wim_port_mapping" + ): + self.service_endpoint_mapping = self.config.get("wim_port_mapping", []) + + self.mappings = { + m["service_endpoint_id"]: m for m in self.service_endpoint_mapping + } + + self.logger.debug("self.mappings={:s}".format(str(self.mappings))) + + self.tapi_client = TransportApiClient(self.logger, wim, wim_account, config) + + self.logger.info("TAPI WIM Connector Initialized.") + + def check_credentials(self): + """Check if the connector itself can access the SDN/WIM with the provided url (wim.wim_url), + user (wim_account.user), and password (wim_account.password) + + Raises: + SdnConnectorError: Issues regarding authorization, access to + external URLs, etc are detected. + """ + _ = self.tapi_client.get_root_context() + self.logger.info("Credentials checked") + + def get_connectivity_service_status(self, service_uuid, conn_info=None): + """Monitor the status of the connectivity service established + + Arguments: + service_uuid (str): UUID of the connectivity service + conn_info (dict or None): Information returned by the connector + during the service creation/edition and subsequently stored in + the database. + + Returns: + dict: JSON/YAML-serializable dict that contains a mandatory key + ``sdn_status`` associated with one of the following values:: + + {'sdn_status': 'ACTIVE'} + # The service is up and running. + + {'sdn_status': 'INACTIVE'} + # The service was created, but the connector + # cannot determine yet if connectivity exists + # (ideally, the caller needs to wait and check again). + + {'sdn_status': 'DOWN'} + # Connection was previously established, + # but an error/failure was detected. + + {'sdn_status': 'ERROR'} + # An error occurred when trying to create the service/ + # establish the connectivity. + + {'sdn_status': 'BUILD'} + # Still trying to create the service, the caller + # needs to wait and check again. + + Additionally ``error_msg``(**str**) and ``sdn_info``(**dict**) + keys can be used to provide additional status explanation or + new information available for the connectivity service. + """ + sdn_status = set() + bidirectional = conn_info["bidirectional"] + + tapi_client = self.tapi_client + if bidirectional: + service_uuid = conn_info["uuid"] + service_status = tapi_client.get_service_status("<>", service_uuid) + sdn_status.add(service_status["sdn_status"]) + else: + service_az_uuid = conn_info["az"]["uuid"] + service_za_uuid = conn_info["za"]["uuid"] + service_az_status = tapi_client.get_service_status(">>", service_az_uuid) + service_za_status = tapi_client.get_service_status("<<", service_za_uuid) + sdn_status.add(service_az_status["sdn_status"]) + sdn_status.add(service_za_status["sdn_status"]) + + if len(sdn_status) == 1 and "ACTIVE" in sdn_status: + service_status = {"sdn_status": "ACTIVE"} + else: + service_status = {"sdn_status": "ERROR"} + + return service_status + + def create_connectivity_service(self, service_type, connection_points, **kwargs): + """ + Establish SDN/WAN connectivity between the endpoints + :param service_type: (str): ``ELINE`` (L2), ``ELAN`` (L2), ``ETREE`` (L2), ``L3``. + :param connection_points: (list): each point corresponds to + an entry point to be connected. For WIM: from the DC to the transport network. + For SDN: Compute/PCI to the transport network. One + connection point serves to identify the specific access and + some other service parameters, such as encapsulation type. + Each item of the list is a dict with: + "service_endpoint_id": (str)(uuid) Same meaning that for 'service_endpoint_mapping' (see __init__) + In case the config attribute mapping_not_needed is True, this value is not relevant. In this case + it will contain the string "device_id:device_interface_id" + "service_endpoint_encapsulation_type": None, "dot1q", ... + "service_endpoint_encapsulation_info": (dict) with: + "vlan": ..., (int, present if encapsulation is dot1q) + "vni": ... (int, present if encapsulation is vxlan), + "peers": [(ipv4_1), (ipv4_2)] (present if encapsulation is vxlan) + "mac": ... + "device_id": ..., same meaning that for 'service_endpoint_mapping' (see __init__) + "device_interface_id": same meaning that for 'service_endpoint_mapping' (see __init__) + "switch_dpid": ..., present if mapping has been found for this device_id,device_interface_id + "swith_port": ... present if mapping has been found for this device_id,device_interface_id + "service_mapping_info": present if mapping has been found for this device_id,device_interface_id + :param kwargs: For future versions: + bandwidth (int): value in kilobytes + latency (int): value in milliseconds + Other QoS might be passed as keyword arguments. + :return: tuple: ``(service_id, conn_info)`` containing: + - *service_uuid* (str): UUID of the established connectivity service + - *conn_info* (dict or None): Information to be stored at the database (or ``None``). + This information will be provided to the :meth:`~.edit_connectivity_service` and :obj:`~.delete`. + **MUST** be JSON/YAML-serializable (plain data structures). + :raises: SdnConnectorException: In case of error. Nothing should be created in this case. + Provide the parameter http_code + """ + supported_service_types = {"ELINE"} + if service_type not in supported_service_types: + raise WimTapiUnsupportedServiceType(service_type, supported_service_types) + + self.logger.debug("connection_points={:s}".format(str(connection_points))) + + if not isinstance(connection_points, (list, tuple)): + raise WimTapiConnectionPointsBadFormat(connection_points) + + if len(connection_points) != 2: + raise WimTapiConnectionPointsBadFormat(connection_points) + + sips = self.tapi_client.get_service_interface_points() + services_composer = ServicesComposer(sips) + + for connection_point in connection_points: + service_endpoint_id = connection_point.get("service_endpoint_id") + if service_endpoint_id is None: + raise WimTapiMissingConnPointField( + connection_point, "service_endpoint_id" + ) + + mapping = self.mappings.get(service_endpoint_id, {}) + services_composer.add_service_endpoint(service_endpoint_id, mapping) + + services_composer.dump(self.logger) + + service_uuid, conn_info = self._create_services_and_conn_info(services_composer) + return service_uuid, conn_info + + def _create_services_and_conn_info(self, services_composer: ServicesComposer): + services = services_composer.services + requested_capacity = services_composer.requested_capacity + vlan_constraint = services_composer.vlan_constraint + + service_uuid = str(uuid.uuid4()) + + if services_composer.is_bidirectional(): + service_endpoints = services[0] + self.tapi_client.create_service( + "<>", + service_uuid, + service_endpoints, + bidirectional=True, + requested_capacity=requested_capacity, + vlan_constraint=vlan_constraint, + ) + conn_info = conn_info_compose_bidirectional( + service_uuid, + service_endpoints, + requested_capacity=requested_capacity, + vlan_constraint=vlan_constraint, + ) + + else: + service_uuid = service_uuid[0 : len(service_uuid) - 4] + "00**" + service_az_uuid = service_uuid.replace("**", "af") + service_az_endpoints = services[0] + service_za_uuid = service_uuid.replace("**", "fa") + service_za_endpoints = services[1] + + self.tapi_client.create_service( + ">>", + service_az_uuid, + service_az_endpoints, + bidirectional=False, + requested_capacity=requested_capacity, + vlan_constraint=vlan_constraint, + ) + self.tapi_client.create_service( + "<<", + service_za_uuid, + service_za_endpoints, + bidirectional=False, + requested_capacity=requested_capacity, + vlan_constraint=vlan_constraint, + ) + conn_info = conn_info_compose_unidirectional( + service_az_uuid, + service_az_endpoints, + service_za_uuid, + service_za_endpoints, + requested_capacity=requested_capacity, + vlan_constraint=vlan_constraint, + ) + + return service_uuid, conn_info + + def delete_connectivity_service(self, service_uuid, conn_info=None): + """ + Disconnect multi-site endpoints previously connected + + :param service_uuid: The one returned by create_connectivity_service + :param conn_info: The one returned by last call to 'create_connectivity_service' or 'edit_connectivity_service' + if they do not return None + :return: None + :raises: SdnConnectorException: In case of error. The parameter http_code must be filled + """ + bidirectional = conn_info["bidirectional"] + if bidirectional: + service_uuid = conn_info["uuid"] + self.tapi_client.delete_service("<>", service_uuid) + else: + service_az_uuid = conn_info["az"]["uuid"] + service_za_uuid = conn_info["za"]["uuid"] + self.tapi_client.delete_service(">>", service_az_uuid) + self.tapi_client.delete_service("<<", service_za_uuid) + + def edit_connectivity_service( + self, service_uuid, conn_info=None, connection_points=None, **kwargs + ): + """Change an existing connectivity service. + + This method's arguments and return value follow the same convention as + :meth:`~.create_connectivity_service`. + + :param service_uuid: UUID of the connectivity service. + :param conn_info: (dict or None): Information previously returned by last call to create_connectivity_service + or edit_connectivity_service + :param connection_points: (list): If provided, the old list of connection points will be replaced. + :param kwargs: Same meaning that create_connectivity_service + :return: dict or None: Information to be updated and stored at the database. + When ``None`` is returned, no information should be changed. + When an empty dict is returned, the database record will be deleted. + **MUST** be JSON/YAML-serializable (plain data structures). + Raises: + SdnConnectorException: In case of error. + """ + raise NotImplementedError + + def clear_all_connectivity_services(self): + """Delete all WAN Links in a WIM. + + This method is intended for debugging only, and should delete all the + connections controlled by the WIM/SDN, not only the connections that + a specific RO is aware of. + + Raises: + SdnConnectorException: In case of error. + """ + raise NotImplementedError + + def get_all_active_connectivity_services(self): + """Provide information about all active connections provisioned by a + WIM. + + Raises: + SdnConnectorException: In case of error. + """ + raise NotImplementedError diff --git a/RO-SDN-tapi/requirements.in b/RO-SDN-tapi/requirements.in new file mode 100644 index 00000000..25e572e9 --- /dev/null +++ b/RO-SDN-tapi/requirements.in @@ -0,0 +1,17 @@ +# Copyright ETSI Contributors and Others. +# +# 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. +## + +requests diff --git a/RO-SDN-tapi/setup.py b/RO-SDN-tapi/setup.py new file mode 100644 index 00000000..107778a6 --- /dev/null +++ b/RO-SDN-tapi/setup.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +## +# 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 setuptools import setup + +_name = "osm_rosdn_tapi" +_version_command = ("git describe --match v* --tags --long --dirty", "pep440-git-full") +_description = "OSM ro sdn plugin for tapi" +_author = "OSM Support" +_author_email = "osmsupport@etsi.org" +_maintainer = "OSM Support" +_maintainer_email = "osmsupport@etsi.org" +_license = "Apache 2.0" +_url = "https://osm.etsi.org/gitweb/?p=osm/RO.git;a=summary" + +_readme = """ +=========== +osm-rosdn_tapi +=========== + +osm-ro pluging for tapi SDN +""" + +setup( + name=_name, + description=_description, + long_description=_readme, + version_command=_version_command, + author=_author, + author_email=_author_email, + maintainer=_maintainer, + maintainer_email=_maintainer_email, + url=_url, + license=_license, + packages=[_name], + include_package_data=True, + setup_requires=["setuptools-version-command"], + entry_points={ + "osm_rosdn.plugins": [ + "rosdn_tapi = osm_rosdn_tapi.wimconn_tapi:WimconnectorTAPI" + ], + }, +) diff --git a/RO-SDN-tapi/stdeb.cfg b/RO-SDN-tapi/stdeb.cfg new file mode 100644 index 00000000..262a47c8 --- /dev/null +++ b/RO-SDN-tapi/stdeb.cfg @@ -0,0 +1,17 @@ +# +# 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. +# + +[DEFAULT] +X-Python3-Version : >= 3.5 diff --git a/devops-stages/stage-build.sh b/devops-stages/stage-build.sh index 3ac7ab60..3d570c78 100755 --- a/devops-stages/stage-build.sh +++ b/devops-stages/stage-build.sh @@ -29,6 +29,7 @@ dist_ro_sdn_juniper_contrail dist_ro_sdn_odl_of dist_ro_sdn_onos_of dist_ro_sdn_onos_vpls +dist_ro_sdn_tapi dist_ro_vim_aws dist_ro_vim_azure dist_ro_vim_openstack @@ -53,7 +54,7 @@ do cp ${vim_plugin}/deb_dist/python3-osm-rovim*.deb deb_dist/ done -# SDN plugins: DynPac, Ietfl2vpn, Onosof Floodlightof +# SDN plugins: DynPac, Ietfl2vpn, Onosof Floodlightof, Transport API (TAPI) for sdn_plugin in RO-SDN-* do cp ${sdn_plugin}/deb_dist/python3-osm-rosdn*.deb deb_dist/ diff --git a/releasenotes/notes/feature_10937_Transport_API_WIM_connector_for_RO-9dc1e3aba7e1f3ad.yaml b/releasenotes/notes/feature_10937_Transport_API_WIM_connector_for_RO-9dc1e3aba7e1f3ad.yaml new file mode 100644 index 00000000..70c79ea4 --- /dev/null +++ b/releasenotes/notes/feature_10937_Transport_API_WIM_connector_for_RO-9dc1e3aba7e1f3ad.yaml @@ -0,0 +1,20 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# 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. +####################################################################################### +--- +features: + - | + Feature 10937: Transport API (TAPI) WIM connector for RO diff --git a/requirements.in b/requirements.in index 36e393a2..0c2252a6 100644 --- a/requirements.in +++ b/requirements.in @@ -25,6 +25,7 @@ -r RO-SDN-odl_openflow/requirements.in -r RO-SDN-onos_openflow/requirements.in -r RO-SDN-onos_vpls/requirements.in +-r RO-SDN-tapi/requirements.in -r RO-VIM-aws/requirements.in -r RO-VIM-azure/requirements.in -r RO-VIM-openstack/requirements.in diff --git a/tox.ini b/tox.ini index c971bc17..44220840 100644 --- a/tox.ini +++ b/tox.ini @@ -57,6 +57,7 @@ commands = black --check --diff RO-SDN-odl_openflow black --check --diff RO-SDN-onos_openflow black --check --diff RO-SDN-onos_vpls + black --check --diff RO-SDN-tapi black --check --diff RO-VIM-aws black --check --diff RO-VIM-azure black --check --diff RO-VIM-openstack @@ -109,6 +110,9 @@ commands = # RO-SDN-onos_vpls # nose2 -C --coverage RO-SDN-onos_vpls/osm_rosdn_onos_vpls -s RO-SDN-onos_vpls/osm_rosdn_onos_vpls # sh -c 'mv .coverage .coverage_rosdn_onos_vpls' + # RO-SDN-tapi + nose2 -C --coverage RO-SDN-tapi/osm_rosdn_tapi -s RO-SDN-tapi/osm_rosdn_tapi + sh -c 'mv .coverage .coverage_rosdn_tapi' # RO-VIM-aws # nose2 -C --coverage RO-VIM-aws/osm_rovim_aws -s RO-VIM-aws/osm_rovim_aws # sh -c 'mv .coverage .coverage_rovim_aws' @@ -128,7 +132,7 @@ commands = # nose2 -C --coverage RO-VIM-gcp/osm_rovim_gcp -s RO-VIM-gcp/osm_rovim_gcp # sh -c 'mv .coverage .coverage_rovim_gcp' # Combine results and generate reports - # coverage combine .coverage_ng_ro .coverage_ro_plugin .coverage_rosdn_arista_cloudvision .coverage_rosdn_dpb .coverage_rosdn_dynpac .coverage_rosdn_floodlightof .coverage_rosdn_ietfl2vpn .coverage_rosdn_juniper_contrail .coverage_rosdn_odlof .coverage_rosdn_onos_vpls .coverage_rosdn_onosof .coverage_rovim_aws .coverage_rovim_azure .coverage_rovim_openvim .coverage_rovim_gcp # .coverage_rovim_openstack .coverage_rovim_vmware + # coverage combine .coverage_ng_ro .coverage_ro_plugin .coverage_rosdn_arista_cloudvision .coverage_rosdn_dpb .coverage_rosdn_dynpac .coverage_rosdn_floodlightof .coverage_rosdn_ietfl2vpn .coverage_rosdn_juniper_contrail .coverage_rosdn_odlof .coverage_rosdn_onos_vpls .coverage_rosdn_onosof .coverage_rosdn_tapi .coverage_rovim_aws .coverage_rovim_azure .coverage_rovim_openvim .coverage_rovim_gcp # .coverage_rovim_openstack .coverage_rovim_vmware coverage combine .coverage_ng_ro .coverage_rovim_openstack .coverage_rosdn_juniper_contrail coverage report --omit='*tests*' coverage html -d ./cover --omit='*tests*' @@ -153,6 +157,7 @@ commands = flake8 RO-SDN-odl_openflow/osm_rosdn_odlof/ RO-SDN-odl_openflow/setup.py flake8 RO-SDN-onos_openflow/osm_rosdn_onosof/ RO-SDN-onos_openflow/setup.py flake8 RO-SDN-onos_vpls/osm_rosdn_onos_vpls/ RO-SDN-onos_vpls/setup.py + flake8 RO-SDN-tapi/osm_rosdn_tapi/ RO-SDN-tapi/setup.py flake8 RO-VIM-aws/osm_rovim_aws/ RO-VIM-aws/setup.py flake8 RO-VIM-azure/osm_rovim_azure/ RO-VIM-azure/setup.py flake8 RO-VIM-openstack/osm_rovim_openstack/ RO-VIM-openstack/setup.py @@ -181,6 +186,7 @@ commands = pylint -E RO-SDN-odl_openflow/osm_rosdn_odlof pylint -E RO-SDN-onos_openflow/osm_rosdn_onosof pylint -E RO-SDN-onos_vpls/osm_rosdn_onos_vpls --disable=E1101 + pylint -E RO-SDN-tapi/osm_rosdn_tapi pylint -E RO-VIM-aws/osm_rovim_aws pylint -E RO-VIM-azure/osm_rovim_azure --disable=all pylint -E RO-VIM-openstack/osm_rovim_openstack --disable=E1101 @@ -362,6 +368,19 @@ commands = sh -c 'cd deb_dist/osm-rosdn-onos-vpls*/ && dpkg-buildpackage -rfakeroot -uc -us' +####################################################################################### +[testenv:dist_ro_sdn_tapi] +deps = {[testenv]deps} + -r{toxinidir}/requirements-dist.txt +skip_install = true +whitelist_externals = sh +changedir = {toxinidir}/RO-SDN-tapi +commands = + sh -c 'rm -rf deb_dist dist osm_rosdn_tapi.egg-info osm_rosdn_tapi*.tar.gz' + python3 setup.py --command-packages=stdeb.command sdist_dsc + sh -c 'cd deb_dist/osm-rosdn-tapi*/ && dpkg-buildpackage -rfakeroot -uc -us' + + ####################################################################################### [testenv:dist_ro_vim_aws] deps = {[testenv]deps}