Update from master part 2
Squashed commit of the following:
commit 364627c364a86a85696781766326dd690a362bc4
Author: vegall <lvega@whitestack.com>
Date: Fri Mar 17 15:09:50 2023 +0000
Feature 10972: Support of volume multi-attach
Change-Id: I6e88ee52e5e882dbb4ec7d66cf648fbe07d40509
Signed-off-by: vegall <lvega@whitestack.com>
commit 0e51779fd37dc5c12f3bd19d78f7341ed0a67b7a
Author: gifrerenom <lluis.gifre@cttc.es>
Date: Tue Apr 18 16:38:42 2023 +0000
Feature 10937: Transport API (TAPI) WIM connector for RO
Change-Id: If0dac9f8ba2d00945eb86a89fb0b2f174c672794
Signed-off-by: gifrerenom <lluis.gifre@cttc.es>
commit 370e36bafdcb90f212e289b87290f39be141b3d4
Author: elumalai <deepika.e@tataelxsi.co.in>
Date: Tue Apr 25 16:22:56 2023 +0530
Feature 10979: Static IPv6 Dual Stack Assignment
Added support for static dual stack IP assignment
Change-Id: Ief10ae955fb870a3417f68e1c5f7bda570cb6470
Signed-off-by: elumalai <deepika.e@tataelxsi.co.in>
commit b1bc66933aa392b9d7518f7cebc711700335389c
Author: Gabriel Cuba <gcuba@whitestack.com>
Date: Fri Aug 19 18:23:00 2022 -0500
Fix Bug 2098: Get VDUs from VNFR when Heal op has no additionalPrameters
When Heal is requested without vdu or count-index parameters, RO will recreate all VDUs from VNFR
Change-Id: Idf2cf158bcb33e7b0c307298c14504cc7aa77e2a
Signed-off-by: Gabriel Cuba <gcuba@whitestack.com>
(cherry picked from commit 2fbb3a264e4117f4a6569fede6558836d67ac4a4)
commit aba1518f487b4b65861eb30f553c4edb72ad972e
Author: Gulsum Atici <gulsum.atici@canonical.com>
Date: Mon May 15 11:55:13 2023 +0300
Fix VimAdminThread run method
The run_coroutine_threadsafe() function is used to schedule a coroutine object from a different thread and returns a concurrent.futures.Future.
run_coroutine_threadsafe is unnecessary to run the main task and replaced with asyncio.run().
Change-Id: I8ea31828a9798140d596165443bdf26659b4eef8
Signed-off-by: Gulsum Atici <gulsum.atici@canonical.com>
commit f17e5bb6b6da4432628dd65ce9ad633e6441f67c
Author: Gulsum Atici <gulsum.atici@canonical.com>
Date: Wed May 10 22:52:57 2023 +0300
Minor updates in Dockerfile
Change-Id: I79b43654d181f6976a4e544d58fb92aa1b67e760
Signed-off-by: Gulsum Atici <gulsum.atici@canonical.com>
commit a264b7a460b28d7454fc95fe659da46f55b0c155
Author: Gulsum Atici <gulsum.atici@canonical.com>
Date: Tue May 9 14:57:22 2023 +0300
Ubuntu 22.04 and Python 3.10 preparation
Change-Id: I87164827a8849c16b5e3a804d9673a578e5a5593
Signed-off-by: Gulsum Atici <gulsum.atici@canonical.com>
commit 1c89c08a0dd1c79b5adff3ac1cc123239762e06a
Author: garciadeblas <gerardo.garciadeblas@telefonica.com>
Date: Tue Apr 18 15:06:30 2023 +0200
Clean stage-archive.sh and use allowlist_extenals in tox.ini
Change-Id: I18f0bc3e263063b5b1d2cf211f028f6bb0e4bceb
Signed-off-by: garciadeblas <gerardo.garciadeblas@telefonica.com>
commit 51e72a0f7479b3064b4b11891eb524d42f4738b0
Author: elumalai <deepika.e@tataelxsi.co.in>
Date: Fri Apr 28 19:41:49 2023 +0530
Coverity CWE 330: Use of Insufficiently Random Values
Added support to fix CWE 330: Use of Insufficiently Random Values
Coverity issue
Change-Id: Ib12ebeeb9b0cc10af9980fe8661eb6230c2f6d6d
Signed-off-by: elumalai <deepika.e@tataelxsi.co.in>
commit e17cd946aed699b5feca83d37591d04f129a8f52
Author: elumalai <deepika.e@tataelxsi.co.in>
Date: Fri Apr 28 18:04:24 2023 +0530
Coverity CWE 688: Function Call With Incorrect Variable or Reference as Argument
Added fix for CWE 688 Typo in Identifier
Change-Id: I53b5142451b809be638d73626265531057722169
Signed-off-by: elumalai <deepika.e@tataelxsi.co.in>
commit 730cfaff466fb3c9b1446ecef5213916195ff861
Author: Gabriel Cuba <gcuba@whitestack.com>
Date: Mon Mar 13 22:26:38 2023 -0500
Feature 10975: get flavor-id from additionalParams if specified
Change-Id: I1c9b1ec43c80f3793b475187681f4c2005d77375
Signed-off-by: Gabriel Cuba <gcuba@whitestack.com>
commit 2d3f63b055e6a38e95bcff56a8ddef32767b11ef
Author: garciadeblas <gerardo.garciadeblas@telefonica.com>
Date: Tue Apr 11 10:08:26 2023 +0200
Update stage-build to run tox sequentially
Change-Id: I967f19a8c35700290e93c9d8bd863b63b7c2d239
Signed-off-by: garciadeblas <gerardo.garciadeblas@telefonica.com>
(cherry picked from commit ea063c7a6ae6a5d7e11e8c22f9707d5c8f674ac7)
commit b3dbfcad6f4b2bebc9ebc20fd7129a18879cb20c
Author: Gabriel Cuba <gcuba@whitestack.com>
Date: Tue Mar 14 10:58:39 2023 -0500
Feature 10978: Add support of ipv6_address_mode and ipv6_ra_mode to openstack connector
Change-Id: I8ca741a215bd2c52999dee1ea301d4e02aafcb24
Signed-off-by: Gabriel Cuba <gcuba@whitestack.com>
commit 01619d5b596e01ac8cd6d27bf01a1174e6b3f97b
Author: Gulsum Atici <gulsum.atici@canonical.com>
Date: Wed Mar 22 22:57:26 2023 +0300
Keep vim_details while reporting VM deletion
Change-Id: I27577b2fc93a585affc947abcec8352562f23f49
Signed-off-by: Gulsum Atici <gulsum.atici@canonical.com>
commit 98740c03567ff8c5a22f06fd3f049248a9e5f98d
Author: Pedro Escaleira <escaleira@av.it.pt>
Date: Wed Feb 22 10:48:52 2023 +0000
Bug 2217 fixed: modified the cloud-init merge configs and defined the default SSH keys within the system_info instead of users
Change-Id: I12e26a88fb9b50c4a78b9fa8ee2cb5d4b4bf6d00
Signed-off-by: Pedro Escaleira <escaleira@av.it.pt>
commit d586d89bde00acaf22debd7f657d605c9d095571
Author: Gulsum Atici <gulsum.atici@canonical.com>
Date: Mon Feb 13 18:40:03 2023 +0300
Feature 10960 Performance optimizations for the polling of VM status in RO
Change-Id: If785bbeaab66e0839541bf94184ce37114e67bd4
Signed-off-by: Gulsum Atici <gulsum.atici@canonical.com>
commit 4c1dd54ae02e82f11a60058a1b7c7b0137ac572e
Author: Gabriel Cuba <gcuba@whitestack.com>
Date: Tue Feb 14 12:43:32 2023 -0500
Refactor ns.py so that RO uses the IP profile as it comes from LCM
Change-Id: I36983c86d7c76ad8a9b93eb6eae254f844717e0e
Signed-off-by: Gabriel Cuba <gcuba@whitestack.com>
commit 3822010a26b2e21290b6acdf288db277c7f36605
Author: garciadeblas <gerardo.garciadeblas@telefonica.com>
Date: Mon Feb 13 17:48:32 2023 +0100
Fix bug 2216 to remove hardcoded numa affinity in VIO
Change-Id: I0912c2841e7c5c1febe056ba092afedaea77f6a1
Signed-off-by: garciadeblas <gerardo.garciadeblas@telefonica.com>
commit 778f3cc8c052bd17d0da32f07b880616d25f935a
Author: Lovejeet Singh <lovejeet.singh@hsc.com>
Date: Mon Feb 13 16:15:40 2023 +0530
Bug 2202: Adding support for cinder V3 API with V2 API for persistent volumes.
Change-Id: I7034564b91b94e6be242cb2ce0f4a5b147b87d64
Signed-off-by: Lovejeet Singh <lovejeet.singh@hsc.com>
Change-Id: I7ac1bd1d9896788812f456c678b1f5222a1f1ad6
Signed-off-by: Dario Faccin <dario.faccin@canonical.com>
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 0000000..ab3006a
--- /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 0000000..70c6e59
--- /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 <lluis.gifre@cttc.es>
+# - Ricard Vilalta <ricard.vilalta@cttc.es>
+#######################################################################################
+
+"""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 0000000..6634422
--- /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 <lluis.gifre@cttc.es>
+# - Ricard Vilalta <ricard.vilalta@cttc.es>
+#######################################################################################
+
+"""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 0000000..f8fc02a
--- /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 <lluis.gifre@cttc.es>
+# - Ricard Vilalta <ricard.vilalta@cttc.es>
+#######################################################################################
+
+"""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 0000000..f5bcdae
--- /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 <lluis.gifre@cttc.es>
+# - Ricard Vilalta <ricard.vilalta@cttc.es>
+#######################################################################################
+
+"""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 0000000..ec2f0a1
--- /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 <lluis.gifre@cttc.es>
+# - Ricard Vilalta <ricard.vilalta@cttc.es>
+#######################################################################################
+
+"""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 0000000..720e423
--- /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 <lluis.gifre@cttc.es>
+# - Ricard Vilalta <ricard.vilalta@cttc.es>
+#######################################################################################
+
+"""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 0000000..ad5f9f3
--- /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 <lluis.gifre@cttc.es>
+# - Ricard Vilalta <ricard.vilalta@cttc.es>
+#######################################################################################
+
+"""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 0000000..ab3006a
--- /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 0000000..a9808ba
--- /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 <lluis.gifre@cttc.es>
+# - Ricard Vilalta <ricard.vilalta@cttc.es>
+#######################################################################################
+
+"""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 0000000..f52a020
--- /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 <lluis.gifre@cttc.es>
+# - Ricard Vilalta <ricard.vilalta@cttc.es>
+#######################################################################################
+
+"""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 0000000..71c2148
--- /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 <lluis.gifre@cttc.es>
+# - Ricard Vilalta <ricard.vilalta@cttc.es>
+#######################################################################################
+
+"""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 0000000..5f38210
--- /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 <lluis.gifre@cttc.es>
+# - Ricard Vilalta <ricard.vilalta@cttc.es>
+#######################################################################################
+
+"""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 0000000..8c18b86
--- /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 <lluis.gifre@cttc.es>
+# - Ricard Vilalta <ricard.vilalta@cttc.es>
+#######################################################################################
+
+"""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 0000000..b75543f
--- /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 <lluis.gifre@cttc.es>
+# - Ricard Vilalta <ricard.vilalta@cttc.es>
+#######################################################################################
+
+"""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 0000000..a89f4c8
--- /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 <lluis.gifre@cttc.es>
+# - Ricard Vilalta <ricard.vilalta@cttc.es>
+#######################################################################################
+
+"""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 0000000..25e572e
--- /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 0000000..107778a
--- /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 0000000..262a47c
--- /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