From ed3e4d4fc2819e425bb8b165e43fcbac259d3f1b Mon Sep 17 00:00:00 2001 From: tierno Date: Mon, 21 Oct 2019 15:31:27 +0000 Subject: [PATCH] feature8030 move WIM connector to plugins Change-Id: I2e08ce7aa04f3e61adcf866925bf235b53e15baf Signed-off-by: tierno --- Dockerfile | 2 +- Dockerfile-local | 3 +- RO-SDN-dynpac/Makefile | 24 + .../osm_rosdn_dynpac}/wimconn_dynpac.py | 54 +- RO-SDN-dynpac/requirements.txt | 18 + RO-SDN-dynpac/setup.py | 53 ++ RO-SDN-dynpac/stdeb.cfg | 19 + RO-SDN-dynpac/tox.ini | 41 ++ RO-SDN-onos_openflow/Makefile | 24 + .../osm_rosdn_onosof/onos_of.py | 469 +++++++++++++++ .../osm_rosdn_onosof/sdnconn_onosof.py | 41 ++ RO-SDN-onos_openflow/requirements.txt | 18 + RO-SDN-onos_openflow/setup.py | 53 ++ RO-SDN-onos_openflow/stdeb.cfg | 19 + RO-SDN-onos_openflow/tox.ini | 41 ++ RO-SDN-tapi/Makefile | 24 + .../osm_rosdn_tapi}/wimconn_ietfl2vpn.py | 92 +-- RO-SDN-tapi/requirements.txt | 18 + RO-SDN-tapi/setup.py | 53 ++ RO-SDN-tapi/stdeb.cfg | 19 + RO-SDN-tapi/tox.ini | 41 ++ RO-client/README.rst | 18 +- RO/README.rst | 14 + RO/osm_ro/__init__.py | 13 + RO/osm_ro/database_utils/install-db-server.sh | 17 + RO/osm_ro/database_utils/migrate_mano_db.sh | 53 +- .../migrations/down/34_remove_wim_tables.sql | 13 + .../down/35_remove_sfc_ingress_and_egress.sql | 13 + .../migrations/up/34_add_wim_tables.sql | 13 + .../up/35_add_sfc_ingress_and_egress.sql | 13 + RO/osm_ro/http_tools/__init__.py | 13 + RO/osm_ro/http_tools/errors.py | 14 + RO/osm_ro/http_tools/request_processing.py | 13 + RO/osm_ro/http_tools/tests/__init__.py | 13 + RO/osm_ro/http_tools/tests/test_errors.py | 14 + RO/osm_ro/http_tools/tests/test_handler.py | 14 + RO/osm_ro/http_tools/tox.ini | 14 + RO/osm_ro/nfvo.py | 308 +++++++--- RO/osm_ro/nfvo_db.py | 11 +- RO/osm_ro/openmanod.py | 6 +- RO/osm_ro/osm-ro.service | 15 + RO/osm_ro/scripts/RO-start.sh | 14 + RO/osm_ro/sdn.py | 329 +++++++++++ RO/osm_ro/tests/__init__.py | 13 + RO/osm_ro/tests/test_db.py | 14 + RO/osm_ro/tests/test_utils.py | 14 + RO/osm_ro/vim_thread.py | 558 ++++++++++++------ RO/osm_ro/wim/__init__.py | 13 + RO/osm_ro/wim/engine.py | 37 +- RO/osm_ro/wim/errors.py | 10 +- RO/osm_ro/wim/failing_connector.py | 22 +- RO/osm_ro/wim/openflow_conn.py | 464 +++++++++++++++ RO/osm_ro/wim/persistence.py | 7 +- RO/osm_ro/wim/schemas.py | 31 +- RO/osm_ro/wim/sdnconn.py | 238 ++++++++ RO/osm_ro/wim/tests/__init__.py | 13 + RO/osm_ro/wim/tests/fixtures.py | 20 +- RO/osm_ro/wim/tests/test_actions.py | 12 +- RO/osm_ro/wim/tests/test_http_handler.py | 20 +- RO/osm_ro/wim/tests/test_wim_thread.py | 4 +- RO/osm_ro/wim/tox.ini | 14 + RO/osm_ro/wim/wan_link_actions.py | 44 +- RO/osm_ro/wim/wim_thread.py | 28 +- RO/osm_ro/wim/wimconn.py | 236 -------- RO/osm_ro/wim/wimconn_fake.py | 37 +- RO/osm_ro/wim/wimconn_odl.py | 4 +- RO/requirements.txt | 14 + .../scenario_2vdu_set_ip_mac.yaml | 13 + .../vnfd_2vdu_set_ip_mac.yaml | 13 + .../vnfd_2vdu_set_ip_mac2.yaml | 13 + devops-stages/stage-build.sh | 13 + 71 files changed, 3272 insertions(+), 718 deletions(-) create mode 100644 RO-SDN-dynpac/Makefile rename {RO/osm_ro/wim => RO-SDN-dynpac/osm_rosdn_dynpac}/wimconn_dynpac.py (84%) create mode 100644 RO-SDN-dynpac/requirements.txt create mode 100644 RO-SDN-dynpac/setup.py create mode 100644 RO-SDN-dynpac/stdeb.cfg create mode 100644 RO-SDN-dynpac/tox.ini create mode 100644 RO-SDN-onos_openflow/Makefile create mode 100644 RO-SDN-onos_openflow/osm_rosdn_onosof/onos_of.py create mode 100644 RO-SDN-onos_openflow/osm_rosdn_onosof/sdnconn_onosof.py create mode 100644 RO-SDN-onos_openflow/requirements.txt create mode 100644 RO-SDN-onos_openflow/setup.py create mode 100644 RO-SDN-onos_openflow/stdeb.cfg create mode 100644 RO-SDN-onos_openflow/tox.ini create mode 100644 RO-SDN-tapi/Makefile rename {RO/osm_ro/wim => RO-SDN-tapi/osm_rosdn_tapi}/wimconn_ietfl2vpn.py (86%) create mode 100644 RO-SDN-tapi/requirements.txt create mode 100644 RO-SDN-tapi/setup.py create mode 100644 RO-SDN-tapi/stdeb.cfg create mode 100644 RO-SDN-tapi/tox.ini create mode 100755 RO/osm_ro/sdn.py create mode 100644 RO/osm_ro/wim/openflow_conn.py create mode 100644 RO/osm_ro/wim/sdnconn.py delete mode 100644 RO/osm_ro/wim/wimconn.py diff --git a/Dockerfile b/Dockerfile index c758db93..8eec0077 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,7 +20,7 @@ FROM ubuntu:16.04 RUN apt-get update && \ DEBIAN_FRONTEND=noninteractive apt-get --yes install git tox make python-all python3 python3-pip debhelper wget && \ - DEBIAN_FRONTEND=noninteractive apt-get --yes install python3-all libssl-dev && \ + DEBIAN_FRONTEND=noninteractive apt-get --yes install python3-all libssl-dev flake8 && \ DEBIAN_FRONTEND=noninteractive pip3 install -U setuptools setuptools-version-command stdeb # FROM ubuntu:16.04 diff --git a/Dockerfile-local b/Dockerfile-local index 48447acc..8430fa90 100644 --- a/Dockerfile-local +++ b/Dockerfile-local @@ -26,7 +26,6 @@ RUN apt-get update && apt-get install -y git python3 python3-pip \ # This is not needed, because package dependency will install anyway. # But done here in order to harry up image generation using cache - RUN DEBIAN_FRONTEND=noninteractive apt-get -y install python3-neutronclient python3-openstackclient \ python3-requests python3-netaddr python3-argcomplete @@ -54,6 +53,8 @@ RUN /root/RO/RO/osm_ro/scripts/install-osm-im.sh --develop && \ python3 -m pip install -e /root/RO/RO-VIM-openvim && \ python3 -m pip install -e /root/RO/RO-VIM-aws && \ python3 -m pip install -e /root/RO/RO-VIM-fos && \ + python3 -m pip install -e /root/RO/RO-SDN-dynpac && \ + python3 -m pip install -e /root/RO/RO-SDN-tapi && \ rm -rf /root/.cache && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* diff --git a/RO-SDN-dynpac/Makefile b/RO-SDN-dynpac/Makefile new file mode 100644 index 00000000..9fb4408b --- /dev/null +++ b/RO-SDN-dynpac/Makefile @@ -0,0 +1,24 @@ +## +# 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. +## + +all: clean package + +clean: + rm -rf dist deb_dist osm_rosdn_dynpac-*.tar.gz osm_rosdn_dynpac.egg-info .eggs + +package: + python3 setup.py --command-packages=stdeb.command sdist_dsc + cd deb_dist/osm-rosdn-dynpac*/ && dpkg-buildpackage -rfakeroot -uc -us + diff --git a/RO/osm_ro/wim/wimconn_dynpac.py b/RO-SDN-dynpac/osm_rosdn_dynpac/wimconn_dynpac.py similarity index 84% rename from RO/osm_ro/wim/wimconn_dynpac.py rename to RO-SDN-dynpac/osm_rosdn_dynpac/wimconn_dynpac.py index 661f6f6e..b32856b4 100644 --- a/RO/osm_ro/wim/wimconn_dynpac.py +++ b/RO-SDN-dynpac/osm_rosdn_dynpac/wimconn_dynpac.py @@ -26,10 +26,10 @@ import json import logging from enum import Enum -from .wimconn import WimConnector, WimConnectorError +from osm_ro.wim.sdnconn import SdnConnectorBase, SdnConnectorError -class WimError(Enum): +class SdnError(Enum): UNREACHABLE = 'Unable to reach the WIM.', SERVICE_TYPE_ERROR = 'Unexpected service_type. Only "L2" is accepted.', CONNECTION_POINTS_SIZE = \ @@ -47,7 +47,7 @@ class WimError(Enum): UNAUTHORIZED = "Failed while authenticating" -class WimAPIActions(Enum): +class SdnAPIActions(Enum): CHECK_CONNECTIVITY = "CHECK_CONNECTIVITY", CREATE_SERVICE = "CREATE_SERVICE", DELETE_SERVICE = "DELETE_SERVICE", @@ -55,36 +55,34 @@ class WimAPIActions(Enum): SERVICE_STATUS = "SERVICE_STATUS", -class DynpacConnector(WimConnector): +class DynpacConnector(SdnConnectorBase): __supported_service_types = ["ELINE (L2)", "ELINE"] __supported_encapsulation_types = ["dot1q"] - __WIM_LOGGER = 'openmano.wimconn.dynpac' + __WIM_LOGGER = 'openmano.sdnconn.dynpac' __ENCAPSULATION_TYPE_PARAM = "service_endpoint_encapsulation_type" __ENCAPSULATION_INFO_PARAM = "service_endpoint_encapsulation_info" __BACKUP_PARAM = "backup" __BANDWIDTH_PARAM = "bandwidth" __SERVICE_ENDPOINT_PARAM = "service_endpoint_id" - __WAN_SERVICE_ENDPOINT_PARAM = "wan_service_endpoint_id" - __WAN_MAPPING_INFO_PARAM = "wan_service_mapping_info" - __SW_ID_PARAM = "wan_switch_dpid" - __SW_PORT_PARAM = "wan_switch_port" + __WAN_SERVICE_ENDPOINT_PARAM = "service_endpoint_id" + __WAN_MAPPING_INFO_PARAM = "service_mapping_info" + __SW_ID_PARAM = "switch_dpid" + __SW_PORT_PARAM = "switch_port" __VLAN_PARAM = "vlan" # Public functions exposed to the Resource Orchestrator - def __init__(self, wim, wim_account, config): - self.logger = logging.getLogger(self.__WIM_LOGGER) + def __init__(self, wim, wim_account, config=None, logger=None): + self.logger = logger or logging.getLogger(self.__WIM_LOGGER) + super().__init__(wim, wim_account, config, self.logger) self.__wim = wim self.__wim_account = wim_account self.__config = config self.__wim_url = self.__wim.get("wim_url") self.__user = wim_account.get("user") - self.__passwd = wim_account.get("passwd") + self.__passwd = wim_account.get("password") self.logger.info("Initialized.") - def create_connectivity_service(self, - service_type, - connection_points, - **kwargs): + def create_connectivity_service(self, service_type, connection_points, **kwargs): self.__check_service(service_type, connection_points, kwargs) body = self.__get_body(service_type, connection_points, kwargs) @@ -110,7 +108,7 @@ class DynpacConnector(WimConnector): def edit_connectivity_service(self, service_uuid, conn_info, connection_points, **kwargs): - self.__exception(WimError.UNSUPPORTED_FEATURE, http_code=501) + self.__exception(SdnError.UNSUPPORTED_FEATURE, http_code=501) def get_connectivity_service_status(self, service_uuid): endpoint = "{}/service/status/{}".format(self.__wim_url, service_uuid) @@ -120,7 +118,7 @@ class DynpacConnector(WimConnector): self.__exception(e.message, http_code=503) if response.status_code != 200: - self.__exception(WimError.STATUS, http_code=response.status_code) + self.__exception(SdnError.STATUS, http_code=response.status_code) self.logger.info("Status for service with uuid {}: {}" .format(service_uuid, response.content)) return response.content @@ -132,7 +130,7 @@ class DynpacConnector(WimConnector): except requests.exceptions.RequestException as e: self.__exception(e.message, http_code=503) if response.status_code != 200: - self.__exception(WimError.DELETE, http_code=response.status_code) + self.__exception(SdnError.DELETE, http_code=response.status_code) self.logger.info("Service with uuid: {} deleted".format(service_uuid)) @@ -144,7 +142,7 @@ class DynpacConnector(WimConnector): except requests.exceptions.RequestException as e: self.__exception(e.message, http_code=503) if http_code != 200: - self.__exception(WimError.CLEAR_ALL, http_code=http_code) + self.__exception(SdnError.CLEAR_ALL, http_code=http_code) self.logger.info("{} services deleted".format(response.content)) return "{} services deleted".format(response.content) @@ -159,7 +157,7 @@ class DynpacConnector(WimConnector): self.__exception(e.message, http_code=503) if http_code != 200: - self.__exception(WimError.UNREACHABLE, http_code=http_code) + self.__exception(SdnError.UNREACHABLE, http_code=http_code) self.logger.info("Connectivity checked") def check_credentials(self): @@ -173,7 +171,7 @@ class DynpacConnector(WimConnector): self.__exception(e.message, http_code=503) if http_code != 200: - self.__exception(WimError.UNAUTHORIZED, http_code=http_code) + self.__exception(SdnError.UNAUTHORIZED, http_code=http_code) self.logger.info("Credentials checked") # Private functions @@ -184,29 +182,29 @@ class DynpacConnector(WimConnector): else: error = x self.logger.error(error) - raise WimConnectorError(error, http_code=http_code) + raise SdnConnectorError(error, http_code=http_code) def __check_service(self, service_type, connection_points, kwargs): if service_type not in self.__supported_service_types: - self.__exception(WimError.SERVICE_TYPE_ERROR, http_code=400) + self.__exception(SdnError.SERVICE_TYPE_ERROR, http_code=400) if len(connection_points) != 2: - self.__exception(WimError.CONNECTION_POINTS_SIZE, http_code=400) + self.__exception(SdnError.CONNECTION_POINTS_SIZE, http_code=400) for connection_point in connection_points: enc_type = connection_point.get(self.__ENCAPSULATION_TYPE_PARAM) if enc_type not in self.__supported_encapsulation_types: - self.__exception(WimError.ENCAPSULATION_TYPE, http_code=400) + self.__exception(SdnError.ENCAPSULATION_TYPE, http_code=400) # Commented out for as long as parameter isn't implemented # bandwidth = kwargs.get(self.__BANDWIDTH_PARAM) # if not isinstance(bandwidth, int): - # self.__exception(WimError.BANDWIDTH, http_code=400) + # self.__exception(SdnError.BANDWIDTH, http_code=400) # Commented out for as long as parameter isn't implemented # backup = kwargs.get(self.__BACKUP_PARAM) # if not isinstance(backup, bool): - # self.__exception(WimError.BACKUP, http_code=400) + # self.__exception(SdnError.BACKUP, http_code=400) def __get_body(self, service_type, connection_points, kwargs): port_mapping = self.__config.get("service_endpoint_mapping") diff --git a/RO-SDN-dynpac/requirements.txt b/RO-SDN-dynpac/requirements.txt new file mode 100644 index 00000000..44c797f2 --- /dev/null +++ b/RO-SDN-dynpac/requirements.txt @@ -0,0 +1,18 @@ +## +# 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 +git+https://osm.etsi.org/gerrit/osm/RO.git#egg=osm-ro + diff --git a/RO-SDN-dynpac/setup.py b/RO-SDN-dynpac/setup.py new file mode 100644 index 00000000..46d25e16 --- /dev/null +++ b/RO-SDN-dynpac/setup.py @@ -0,0 +1,53 @@ +#!/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_dynpac" + +README = """ +=========== +osm-rosdn_dynpac +=========== + +osm-ro pluging for dynpac SDN +""" + +setup( + name=_name, + description='OSM ro sdn plugin for dynpac', + long_description=README, + version_command=('git describe --match v* --tags --long --dirty', 'pep440-git-full'), + # version=VERSION, + # python_requires='>3.5.0', + author='ETSI OSM', + # TODO py3 author_email='', + maintainer='OSM_TECH@LIST.ETSI.ORG', # TODO py3 + # TODO py3 maintainer_email='', + url='https://osm.etsi.org/gitweb/?p=osm/RO.git;a=summary', + license='Apache 2.0', + + packages=[_name], + include_package_data=True, + dependency_links=["git+https://osm.etsi.org/gerrit/osm/RO.git#egg=osm-ro"], + install_requires=["requests", "osm-ro"], + setup_requires=['setuptools-version-command'], + entry_points={ + 'osm_rosdn.plugins': ['rosdn_dynpac = osm_rosdn_dynpac.wimconn_dynpac'], + }, +) diff --git a/RO-SDN-dynpac/stdeb.cfg b/RO-SDN-dynpac/stdeb.cfg new file mode 100644 index 00000000..0c718e4f --- /dev/null +++ b/RO-SDN-dynpac/stdeb.cfg @@ -0,0 +1,19 @@ +# +# 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 +Depends3: python3-requests, python3-osm-ro + diff --git a/RO-SDN-dynpac/tox.ini b/RO-SDN-dynpac/tox.ini new file mode 100644 index 00000000..a1e866ab --- /dev/null +++ b/RO-SDN-dynpac/tox.ini @@ -0,0 +1,41 @@ +## +# 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. +## + +[tox] +envlist = py3 +toxworkdir={homedir}/.tox + +[testenv] +basepython = python3 +install_command = python3 -m pip install -r requirements.txt -U {opts} {packages} +# deps = -r{toxinidir}/test-requirements.txt +commands=python3 -m unittest discover -v + +[testenv:flake8] +basepython = python3 +deps = flake8 +commands = flake8 osm_rosdn_dynpac --max-line-length 120 \ + --exclude .svn,CVS,.gz,.git,__pycache__,.tox,local,temp --ignore W291,W293,E226,W504 + +[testenv:unittest] +basepython = python3 +commands = python3 -m unittest osm_rosdn_dynpac.tests + +[testenv:build] +basepython = python3 +deps = stdeb + setuptools-version-command +commands = python3 setup.py --command-packages=stdeb.command bdist_deb + diff --git a/RO-SDN-onos_openflow/Makefile b/RO-SDN-onos_openflow/Makefile new file mode 100644 index 00000000..5e96ce0b --- /dev/null +++ b/RO-SDN-onos_openflow/Makefile @@ -0,0 +1,24 @@ +## +# 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. +## + +all: clean package + +clean: + rm -rf dist deb_dist osm_rosdn_onosof-*.tar.gz osm_rosdn_onosof.egg-info .eggs + +package: + python3 setup.py --command-packages=stdeb.command sdist_dsc + cd deb_dist/osm-rosdn-onosof*/ && dpkg-buildpackage -rfakeroot -uc -us + diff --git a/RO-SDN-onos_openflow/osm_rosdn_onosof/onos_of.py b/RO-SDN-onos_openflow/osm_rosdn_onosof/onos_of.py new file mode 100644 index 00000000..060d1d37 --- /dev/null +++ b/RO-SDN-onos_openflow/osm_rosdn_onosof/onos_of.py @@ -0,0 +1,469 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +## +# Copyright 2016, I2T Research Group (UPV/EHU) +# This file is part of openvim +# All Rights Reserved. +# +# 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. +# +# For those usages not covered by the Apache License, Version 2.0 please +# contact with: alaitz.mendiola@ehu.eus or alaitz.mendiola@gmail.com +## + +''' +ImplementS the pluging for the Open Network Operating System (ONOS) openflow +controller. It creates the class OF_conn to create dataplane connections +with static rules based on packet destination MAC address +''' + +__author__="Alaitz Mendiola" +__date__ ="$22-nov-2016$" + + +import json +import requests +import base64 +import logging +from osm_ro.wim.openflow_conn import OpenflowConn, OpenflowConnException, OpenflowConnConnectionException, \ + OpenflowConnUnexpectedResponse, OpenflowConnAuthException, OpenflowConnNotFoundException, \ + OpenflowConnConflictException, OpenflowConnNotSupportedException, OpenflowConnNotImplemented + + +class OfConnOnos(OpenflowConn): + """ + ONOS connector. No MAC learning is used + """ + def __init__(self, params): + """ Constructor. + Params: dictionary with the following keys: + of_dpid: DPID to use for this controller ?? Does a controller have a dpid? + url: must be [http://HOST:PORT/ + of_user: user credentials, can be missing or None + of_password: password credentials + of_debug: debug level for logging. Default to ERROR + other keys are ignored + Raise an exception if same parameter is missing or wrong + """ + + OpenflowConn.__init__(self, params) + + # check params + url = params.get("of_url") + if not url: + raise ValueError("'url' must be provided") + if not url.startswith("http"): + url = "http://" + url + if not url.endswith("/"): + url = url + "/" + self.url = url + "onos/v1/" + + #internal variables + self.name = "onosof" + self.headers = {'content-type':'application/json','accept':'application/json',} + + self.auth="None" + self.pp2ofi={} # From Physical Port to OpenFlow Index + self.ofi2pp={} # From OpenFlow Index to Physical Port + + self.dpid = str(params["of_dpid"]) + self.id = 'of:'+str(self.dpid.replace(':', '')) + + # TODO This may not be straightforward + if params.get("of_user"): + of_password=params.get("of_password", "") + self.auth = base64.b64encode(bytes(params["of_user"] + ":" + of_password, "utf-8")) + self.auth = self.auth.decode() + self.headers['authorization'] = 'Basic ' + self.auth + + self.logger = logging.getLogger('vim.OF.onos') + self.logger.setLevel( getattr(logging, params.get("of_debug", "ERROR")) ) + self.ip_address = None + + def get_of_switches(self): + """ + Obtain a a list of switches or DPID detected by this controller + :return: list where each element a tuple pair (DPID, IP address) + Raise a openflowconnUnexpectedResponse expection in case of failure + """ + try: + self.headers['content-type'] = 'text/plain' + of_response = requests.get(self.url + "devices", headers=self.headers) + error_text = "Openflow response %d: %s" % (of_response.status_code, of_response.text) + if of_response.status_code != 200: + self.logger.warning("get_of_switches " + error_text) + raise OpenflowConnUnexpectedResponse(error_text) + + self.logger.debug("get_of_switches " + error_text) + info = of_response.json() + + if type(info) != dict: + self.logger.error("get_of_switches. Unexpected response, not a dict: %s", str(info)) + raise OpenflowConnUnexpectedResponse("Unexpected response, not a dict. Wrong version?") + + node_list = info.get('devices') + + if type(node_list) is not list: + self.logger.error( + "get_of_switches. Unexpected response, at 'devices', not found or not a list: %s", + str(type(node_list))) + raise OpenflowConnUnexpectedResponse("Unexpected response, at 'devices', not found " + "or not a list. Wrong version?") + + switch_list = [] + for node in node_list: + node_id = node.get('id') + if node_id is None: + self.logger.error("get_of_switches. Unexpected response at 'device':'id', not found: %s", + str(node)) + raise OpenflowConnUnexpectedResponse("Unexpected response at 'device':'id', " + "not found . Wrong version?") + + node_ip_address = node.get('annotations').get('managementAddress') + if node_ip_address is None: + self.logger.error( + "get_of_switches. Unexpected response at 'device':'managementAddress', not found: %s", + str(node)) + raise OpenflowConnUnexpectedResponse( + "Unexpected response at 'device':'managementAddress', not found. Wrong version?") + + node_id_hex = hex(int(node_id.split(':')[1])).split('x')[1].zfill(16) + + switch_list.append( + (':'.join(a + b for a, b in zip(node_id_hex[::2], node_id_hex[1::2])), node_ip_address)) + return switch_list + + except requests.exceptions.RequestException as e: + error_text = type(e).__name__ + ": " + str(e) + self.logger.error("get_of_switches " + error_text) + raise OpenflowConnConnectionException(error_text) + except ValueError as e: + # ValueError in the case that JSON can not be decoded + error_text = type(e).__name__ + ": " + str(e) + self.logger.error("get_of_switches " + error_text) + raise OpenflowConnUnexpectedResponse(error_text) + + def obtain_port_correspondence(self): + """ + Obtain the correspondence between physical and openflow port names + :return: dictionary with physical name as key, openflow name as value + Raise a openflowconnUnexpectedResponse expection in case of failure + """ + try: + self.headers['content-type'] = 'text/plain' + of_response = requests.get(self.url + "devices/" + self.id + "/ports", headers=self.headers) + error_text = "Openflow response %d: %s" % (of_response.status_code, of_response.text) + if of_response.status_code != 200: + self.logger.warning("obtain_port_correspondence " + error_text) + raise OpenflowConnUnexpectedResponse(error_text) + + self.logger.debug("obtain_port_correspondence " + error_text) + info = of_response.json() + + node_connector_list = info.get('ports') + if type(node_connector_list) is not list: + self.logger.error( + "obtain_port_correspondence. Unexpected response at 'ports', not found or not a list: %s", + str(node_connector_list)) + raise OpenflowConnUnexpectedResponse("Unexpected response at 'ports', not found or not " + "a list. Wrong version?") + + for node_connector in node_connector_list: + if node_connector['port'] != "local": + self.pp2ofi[str(node_connector['annotations']['portName'])] = str(node_connector['port']) + self.ofi2pp[str(node_connector['port'])] = str(node_connector['annotations']['portName']) + + node_ip_address = info['annotations']['managementAddress'] + if node_ip_address is None: + self.logger.error( + "obtain_port_correspondence. Unexpected response at 'managementAddress', not found: %s", + str(self.id)) + raise OpenflowConnUnexpectedResponse("Unexpected response at 'managementAddress', " + "not found. Wrong version?") + self.ip_address = node_ip_address + + # print self.name, ": obtain_port_correspondence ports:", self.pp2ofi + return self.pp2ofi + except requests.exceptions.RequestException as e: + error_text = type(e).__name__ + ": " + str(e) + self.logger.error("obtain_port_correspondence " + error_text) + raise OpenflowConnConnectionException(error_text) + except ValueError as e: + # ValueError in the case that JSON can not be decoded + error_text = type(e).__name__ + ": " + str(e) + self.logger.error("obtain_port_correspondence " + error_text) + raise OpenflowConnUnexpectedResponse(error_text) + + def get_of_rules(self, translate_of_ports=True): + """ + Obtain the rules inserted at openflow controller + :param translate_of_ports: if True it translates ports from openflow index to physical switch name + :return: list where each item is a dictionary with the following content: + priority: rule priority + name: rule name (present also as the master dict key) + ingress_port: match input port of the rule + dst_mac: match destination mac address of the rule, can be missing or None if not apply + vlan_id: match vlan tag of the rule, can be missing or None if not apply + actions: list of actions, composed by a pair tuples: + (vlan, None/int): for stripping/setting a vlan tag + (out, port): send to this port + switch: DPID, all + Raise a openflowconnUnexpectedResponse expection in case of failure + """ + + try: + + if len(self.ofi2pp) == 0: + self.obtain_port_correspondence() + + # get rules + self.headers['content-type'] = 'text/plain' + of_response = requests.get(self.url + "flows/" + self.id, headers=self.headers) + error_text = "Openflow response %d: %s" % (of_response.status_code, of_response.text) + + # The configured page does not exist if there are no rules installed. In that case we return an empty dict + if of_response.status_code == 404: + return {} + + elif of_response.status_code != 200: + self.logger.warning("get_of_rules " + error_text) + raise OpenflowConnUnexpectedResponse(error_text) + self.logger.debug("get_of_rules " + error_text) + + info = of_response.json() + + if type(info) != dict: + self.logger.error("get_of_rules. Unexpected response, not a dict: %s", str(info)) + raise OpenflowConnUnexpectedResponse("Unexpected openflow response, not a dict. " + "Wrong version?") + + flow_list = info.get('flows') + + if flow_list is None: + return {} + + if type(flow_list) is not list: + self.logger.error( + "get_of_rules. Unexpected response at 'flows', not a list: %s", + str(type(flow_list))) + raise OpenflowConnUnexpectedResponse("Unexpected response at 'flows', not a list. " + "Wrong version?") + + rules = [] # Response list + for flow in flow_list: + if not ('id' in flow and 'selector' in flow and 'treatment' in flow and \ + 'instructions' in flow['treatment'] and 'criteria' in \ + flow['selector']): + raise OpenflowConnUnexpectedResponse("unexpected openflow response, one or more " + "elements are missing. Wrong version?") + + rule = dict() + rule['switch'] = self.dpid + rule['priority'] = flow.get('priority') + rule['name'] = flow['id'] + + for criteria in flow['selector']['criteria']: + if criteria['type'] == 'IN_PORT': + in_port = str(criteria['port']) + if in_port != "CONTROLLER": + if not in_port in self.ofi2pp: + raise OpenflowConnUnexpectedResponse("Error: Ingress port {} is not " + "in switch port list".format(in_port)) + if translate_of_ports: + in_port = self.ofi2pp[in_port] + rule['ingress_port'] = in_port + + elif criteria['type'] == 'VLAN_VID': + rule['vlan_id'] = criteria['vlanId'] + + elif criteria['type'] == 'ETH_DST': + rule['dst_mac'] = str(criteria['mac']).lower() + + actions = [] + for instruction in flow['treatment']['instructions']: + if instruction['type'] == "OUTPUT": + out_port = str(instruction['port']) + if out_port != "CONTROLLER": + if not out_port in self.ofi2pp: + raise OpenflowConnUnexpectedResponse("Error: Output port {} is not in " + "switch port list".format(out_port)) + + if translate_of_ports: + out_port = self.ofi2pp[out_port] + + actions.append( ('out', out_port) ) + + if instruction['type'] == "L2MODIFICATION" and instruction['subtype'] == "VLAN_POP": + actions.append( ('vlan', 'None') ) + if instruction['type'] == "L2MODIFICATION" and instruction['subtype'] == "VLAN_ID": + actions.append( ('vlan', instruction['vlanId']) ) + + rule['actions'] = actions + rules.append(rule) + return rules + + except requests.exceptions.RequestException as e: + # ValueError in the case that JSON can not be decoded + error_text = type(e).__name__ + ": " + str(e) + self.logger.error("get_of_rules " + error_text) + raise OpenflowConnConnectionException(error_text) + except ValueError as e: + # ValueError in the case that JSON can not be decoded + error_text = type(e).__name__ + ": " + str(e) + self.logger.error("get_of_rules " + error_text) + raise OpenflowConnUnexpectedResponse(error_text) + + def del_flow(self, flow_name): + """ + Delete an existing rule + :param flow_name: + :return: Raise a openflowconnUnexpectedResponse expection in case of failure + """ + + try: + self.headers['content-type'] = None + of_response = requests.delete(self.url + "flows/" + self.id + "/" + flow_name, headers=self.headers) + error_text = "Openflow response %d: %s" % (of_response.status_code, of_response.text) + + if of_response.status_code != 204: + self.logger.warning("del_flow " + error_text) + raise OpenflowConnUnexpectedResponse(error_text) + + self.logger.debug("del_flow OK " + error_text) + return None + + except requests.exceptions.RequestException as e: + error_text = type(e).__name__ + ": " + str(e) + self.logger.error("del_flow " + error_text) + raise OpenflowConnConnectionException(error_text) + + def new_flow(self, data): + """ + Insert a new static rule + :param data: dictionary with the following content: + priority: rule priority + name: rule name + ingress_port: match input port of the rule + dst_mac: match destination mac address of the rule, missing or None if not apply + vlan_id: match vlan tag of the rule, missing or None if not apply + actions: list of actions, composed by a pair tuples with these posibilities: + ('vlan', None/int): for stripping/setting a vlan tag + ('out', port): send to this port + :return: Raise a openflowconnUnexpectedResponse expection in case of failure + """ + try: + + if len(self.pp2ofi) == 0: + self.obtain_port_correspondence() + + # Build the dictionary with the flow rule information for ONOS + flow = dict() + #flow['id'] = data['name'] + flow['tableId'] = 0 + flow['priority'] = data.get('priority') + flow['timeout'] = 0 + flow['isPermanent'] = "true" + flow['appId'] = 10 # FIXME We should create an appId for OSM + flow['selector'] = dict() + flow['selector']['criteria'] = list() + + # Flow rule matching criteria + if not data['ingress_port'] in self.pp2ofi: + error_text = 'Error. Port ' + data['ingress_port'] + ' is not present in the switch' + self.logger.warning("new_flow " + error_text) + raise OpenflowConnUnexpectedResponse(error_text) + + ingress_port_criteria = dict() + ingress_port_criteria['type'] = "IN_PORT" + ingress_port_criteria['port'] = self.pp2ofi[data['ingress_port']] + flow['selector']['criteria'].append(ingress_port_criteria) + + if 'dst_mac' in data: + dst_mac_criteria = dict() + dst_mac_criteria["type"] = "ETH_DST" + dst_mac_criteria["mac"] = data['dst_mac'] + flow['selector']['criteria'].append(dst_mac_criteria) + + if data.get('vlan_id'): + vlan_criteria = dict() + vlan_criteria["type"] = "VLAN_VID" + vlan_criteria["vlanId"] = int(data['vlan_id']) + flow['selector']['criteria'].append(vlan_criteria) + + # Flow rule treatment + flow['treatment'] = dict() + flow['treatment']['instructions'] = list() + flow['treatment']['deferred'] = list() + + for action in data['actions']: + new_action = dict() + if action[0] == "vlan": + new_action['type'] = "L2MODIFICATION" + if action[1] == None: + new_action['subtype'] = "VLAN_POP" + else: + new_action['subtype'] = "VLAN_ID" + new_action['vlanId'] = int(action[1]) + elif action[0] == 'out': + new_action['type'] = "OUTPUT" + if not action[1] in self.pp2ofi: + error_msj = 'Port '+ action[1] + ' is not present in the switch' + raise OpenflowConnUnexpectedResponse(error_msj) + new_action['port'] = self.pp2ofi[action[1]] + else: + error_msj = "Unknown item '%s' in action list" % action[0] + self.logger.error("new_flow " + error_msj) + raise OpenflowConnUnexpectedResponse(error_msj) + + flow['treatment']['instructions'].append(new_action) + + self.headers['content-type'] = 'application/json' + path = self.url + "flows/" + self.id + of_response = requests.post(path, headers=self.headers, data=json.dumps(flow) ) + + error_text = "Openflow response %d: %s" % (of_response.status_code, of_response.text) + if of_response.status_code != 201: + self.logger.warning("new_flow " + error_text) + raise OpenflowConnUnexpectedResponse(error_text) + + flowId = of_response.headers['location'][path.__len__() + 1:] + + data['name'] = flowId + + self.logger.debug("new_flow OK " + error_text) + return None + + except requests.exceptions.RequestException as e: + error_text = type(e).__name__ + ": " + str(e) + self.logger.error("new_flow " + error_text) + raise OpenflowConnConnectionException(error_text) + + def clear_all_flows(self): + """ + Delete all existing rules + :return: Raise a openflowconnUnexpectedResponse expection in case of failure + """ + try: + rules = self.get_of_rules(True) + + for rule in rules: + self.del_flow(rule) + + self.logger.debug("clear_all_flows OK ") + return None + + except requests.exceptions.RequestException as e: + error_text = type(e).__name__ + ": " + str(e) + self.logger.error("clear_all_flows " + error_text) + raise OpenflowConnConnectionException(error_text) diff --git a/RO-SDN-onos_openflow/osm_rosdn_onosof/sdnconn_onosof.py b/RO-SDN-onos_openflow/osm_rosdn_onosof/sdnconn_onosof.py new file mode 100644 index 00000000..79c14412 --- /dev/null +++ b/RO-SDN-onos_openflow/osm_rosdn_onosof/sdnconn_onosof.py @@ -0,0 +1,41 @@ +## +# Copyright 2019 Telefonica Investigacion y Desarrollo, S.A.U. +# All Rights Reserved. +# +# 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. +# +## +"""The SdnConnectorOnosOf connector is responsible for creating services using pro active operflow rules. +""" + +import logging +from osm_ro.wim.openflow_conn import SdnConnectorOpenFlow +from .onos_of import OfConnOnos + + +class SdnConnectorOnosOf(SdnConnectorOpenFlow): + + def __init__(self, wim, wim_account, config=None, logger=None): + """Creates a connectivity based on pro-active openflow rules + """ + self.logger = logging.getLogger('openmano.sdnconn.onosof') + super().__init__(wim, wim_account, config, logger) + of_params = { + "of_url": wim["wim_url"], + "of_dpid": config.get("dpid"), + "of_user": wim_account["user"], + "of_password": wim_account["password"], + } + self.openflow_conn = OfConnOnos(of_params) + super().__init__(wim, wim_account, config, logger, self.openflow_conn) diff --git a/RO-SDN-onos_openflow/requirements.txt b/RO-SDN-onos_openflow/requirements.txt new file mode 100644 index 00000000..44c797f2 --- /dev/null +++ b/RO-SDN-onos_openflow/requirements.txt @@ -0,0 +1,18 @@ +## +# 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 +git+https://osm.etsi.org/gerrit/osm/RO.git#egg=osm-ro + diff --git a/RO-SDN-onos_openflow/setup.py b/RO-SDN-onos_openflow/setup.py new file mode 100644 index 00000000..380adc7d --- /dev/null +++ b/RO-SDN-onos_openflow/setup.py @@ -0,0 +1,53 @@ +#!/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_onosof" + +README = """ +=========== +osm-rosdn_onosof +=========== + +osm-ro pluging for onosof (ietfl2vpn) SDN +""" + +setup( + name=_name, + description='OSM ro sdn plugin for onosof (ietfl2vpn)', + long_description=README, + version_command=('git describe --match v* --tags --long --dirty', 'pep440-git-full'), + # version=VERSION, + # python_requires='>3.5.0', + author='ETSI OSM', + author_email='alfonso.tiernosepulveda@telefonica.com', + maintainer='Alfonso Tierno', + maintainer_email='alfonso.tiernosepulveda@telefonica.com', + url='https://osm.etsi.org/gitweb/?p=osm/RO.git;a=summary', + license='Apache 2.0', + + packages=[_name], + include_package_data=True, + dependency_links=["git+https://osm.etsi.org/gerrit/osm/RO.git#egg=osm-ro"], + install_requires=["requests", "osm-ro"], + setup_requires=['setuptools-version-command'], + entry_points={ + 'osm_rosdn.plugins': ['rosdn_onosof = osm_rosdn_onosof.sdnconn_onosof:SdnConnectorOnosOf'], + }, +) diff --git a/RO-SDN-onos_openflow/stdeb.cfg b/RO-SDN-onos_openflow/stdeb.cfg new file mode 100644 index 00000000..0c718e4f --- /dev/null +++ b/RO-SDN-onos_openflow/stdeb.cfg @@ -0,0 +1,19 @@ +# +# 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 +Depends3: python3-requests, python3-osm-ro + diff --git a/RO-SDN-onos_openflow/tox.ini b/RO-SDN-onos_openflow/tox.ini new file mode 100644 index 00000000..00b45857 --- /dev/null +++ b/RO-SDN-onos_openflow/tox.ini @@ -0,0 +1,41 @@ +## +# 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. +## + +[tox] +envlist = py3 +toxworkdir={homedir}/.tox + +[testenv] +basepython = python3 +install_command = python3 -m pip install -r requirements.txt -U {opts} {packages} +# deps = -r{toxinidir}/test-requirements.txt +commands=python3 -m unittest discover -v + +[testenv:flake8] +basepython = python3 +deps = flake8 +commands = flake8 osm_rosdn_onosof --max-line-length 120 \ + --exclude .svn,CVS,.gz,.git,__pycache__,.tox,local,temp --ignore W291,W293,E226,W504 + +[testenv:unittest] +basepython = python3 +commands = python3 -m unittest osm_rosdn_onosof.tests + +[testenv:build] +basepython = python3 +deps = stdeb + setuptools-version-command +commands = python3 setup.py --command-packages=stdeb.command bdist_deb + diff --git a/RO-SDN-tapi/Makefile b/RO-SDN-tapi/Makefile new file mode 100644 index 00000000..2e052802 --- /dev/null +++ b/RO-SDN-tapi/Makefile @@ -0,0 +1,24 @@ +## +# 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. +## + +all: clean package + +clean: + rm -rf dist deb_dist osm_rosdn_tapi-*.tar.gz osm_rosdn_tapi.egg-info .eggs + +package: + python3 setup.py --command-packages=stdeb.command sdist_dsc + cd deb_dist/osm-rosdn-tapi*/ && dpkg-buildpackage -rfakeroot -uc -us + diff --git a/RO/osm_ro/wim/wimconn_ietfl2vpn.py b/RO-SDN-tapi/osm_rosdn_tapi/wimconn_ietfl2vpn.py similarity index 86% rename from RO/osm_ro/wim/wimconn_ietfl2vpn.py rename to RO-SDN-tapi/osm_rosdn_tapi/wimconn_ietfl2vpn.py index dc7cc975..26680b5a 100644 --- a/RO/osm_ro/wim/wimconn_ietfl2vpn.py +++ b/RO-SDN-tapi/osm_rosdn_tapi/wimconn_ietfl2vpn.py @@ -21,10 +21,10 @@ # funded by the European Commission under Grant number 761727 through the # Horizon 2020 program. ## -"""The WIM connector is responsible for establishing wide area network +"""The SDN/WIM connector is responsible for establishing wide area network connectivity. -This WIM connector implements the standard IETF RFC 8466 "A YANG Data +This SDN/WIM connector implements the standard IETF RFC 8466 "A YANG Data Model for Layer 2 Virtual Private Network (L2VPN) Service Delivery" It receives the endpoints and the necessary details to request @@ -33,11 +33,11 @@ the Layer 2 service. import requests import uuid import logging -from .wimconn import WimConnector, WimConnectorError +from osm_ro.wim.sdnconn import SdnConnectorBase, SdnConnectorError """CHeck layer where we move it""" -class WimconnectorIETFL2VPN(WimConnector): +class WimconnectorIETFL2VPN(SdnConnectorBase): def __init__(self, wim, wim_account, config=None, logger=None): """IETF L2VPM WIM connector @@ -46,13 +46,13 @@ class WimconnectorIETFL2VPN(WimConnector): wim (dict): WIM record, as stored in the database wim_account (dict): WIM account record, as stored in the database """ - self.logger = logging.getLogger('openmano.wimconn.ietfl2vpn') - super(WimconnectorIETFL2VPN, self).__init__(wim, wim_account, config, logger) + self.logger = logging.getLogger('openmano.sdnconn.ietfl2vpn') + super().__init__(wim, wim_account, config, logger) self.headers = {'Content-Type': 'application/json'} - self.mappings = {m['wan_service_endpoint_id']: m + self.mappings = {m['service_endpoint_id']: m for m in self.service_endpoint_mapping} self.user = wim_account.get("user") - self.passwd = wim_account.get("passwd") + self.passwd = wim_account.get("passwordd") if self.user and self.passwd is not None: self.auth = (self.user, self.passwd) else: @@ -65,10 +65,10 @@ class WimconnectorIETFL2VPN(WimConnector): response = requests.get(endpoint, auth=self.auth) http_code = response.status_code except requests.exceptions.RequestException as e: - raise WimConnectorError(e.message, http_code=503) + raise SdnConnectorError(e.message, http_code=503) if http_code != 200: - raise WimConnectorError("Failed while authenticating", http_code=http_code) + raise SdnConnectorError("Failed while authenticating", http_code=http_code) self.logger.info("Credentials checked") def get_connectivity_service_status(self, service_uuid, conn_info=None): @@ -79,10 +79,10 @@ class WimconnectorIETFL2VPN(WimConnector): Returns: Examples:: - {'wim_status': 'ACTIVE'} - {'wim_status': 'INACTIVE'} - {'wim_status': 'DOWN'} - {'wim_status': 'ERROR'} + {'sdn_status': 'ACTIVE'} + {'sdn_status': 'INACTIVE'} + {'sdn_status': 'DOWN'} + {'sdn_status': 'ERROR'} """ try: self.logger.info("Sending get connectivity service stuatus") @@ -90,16 +90,16 @@ class WimconnectorIETFL2VPN(WimConnector): self.wim["wim_url"], service_uuid) response = requests.get(servicepoint, auth=self.auth) if response.status_code != requests.codes.ok: - raise WimConnectorError("Unable to obtain connectivity servcice status", http_code=response.status_code) - service_status = {'wim_status': 'ACTIVE'} + raise SdnConnectorError("Unable to obtain connectivity servcice status", http_code=response.status_code) + service_status = {'sdn_status': 'ACTIVE'} return service_status except requests.exceptions.ConnectionError: - raise WimConnectorError("Request Timeout", http_code=408) + raise SdnConnectorError("Request Timeout", http_code=408) def search_mapp(self, connection_point): id = connection_point['service_endpoint_id'] if id not in self.mappings: - raise WimConnectorError("Endpoint {} not located".format(str(id))) + raise SdnConnectorError("Endpoint {} not located".format(str(id))) else: return self.mappings[id] @@ -147,13 +147,13 @@ class WimconnectorIETFL2VPN(WimConnector): **MUST** be JSON/YAML-serializable (plain data structures). Raises: - WimConnectorException: In case of error. + SdnConnectorException: In case of error. """ if service_type == "ELINE": if len(connection_points) > 2: - raise WimConnectorError('Connections between more than 2 endpoints are not supported') + raise SdnConnectorError('Connections between more than 2 endpoints are not supported') if len(connection_points) < 2: - raise WimConnectorError('Connections must be of at least 2 endpoints') + raise SdnConnectorError('Connections must be of at least 2 endpoints') """ First step, create the vpn service """ uuid_l2vpn = str(uuid.uuid4()) vpn_service = {} @@ -173,11 +173,11 @@ class WimconnectorIETFL2VPN(WimConnector): response_service_creation = requests.post(endpoint_service_creation, headers=self.headers, json=vpn_service_l, auth=self.auth) except requests.exceptions.ConnectionError: - raise WimConnectorError("Request to create service Timeout", http_code=408) + raise SdnConnectorError("Request to create service Timeout", http_code=408) if response_service_creation.status_code == 409: - raise WimConnectorError("Service already exists", http_code=response_service_creation.status_code) + raise SdnConnectorError("Service already exists", http_code=response_service_creation.status_code) elif response_service_creation.status_code != requests.codes.created: - raise WimConnectorError("Request to create service not accepted", + raise SdnConnectorError("Request to create service not accepted", http_code=response_service_creation.status_code) """ Second step, create the connections and vpn attachments """ for connection_point in connection_points: @@ -192,7 +192,7 @@ class WimconnectorIETFL2VPN(WimConnector): tagged_interf = {} service_endpoint_encapsulation_info = connection_point["service_endpoint_encapsulation_info"] if service_endpoint_encapsulation_info["vlan"] is None: - raise WimConnectorError("VLAN must be provided") + raise SdnConnectorError("VLAN must be provided") tagged_interf["cvlan-id"] = service_endpoint_encapsulation_info["vlan"] tagged["dot1q-vlan-tagged"] = tagged_interf connection["tagged-interface"] = tagged @@ -207,20 +207,20 @@ class WimconnectorIETFL2VPN(WimConnector): self.logger.info("Sending vpn-attachement :{}".format(vpn_attach)) uuid_sna = str(uuid.uuid4()) site_network_access["network-access-id"] = uuid_sna - site_network_access["bearer"] = connection_point_wan_info["wan_service_mapping_info"]["bearer"] + site_network_access["bearer"] = connection_point_wan_info["service_mapping_info"]["bearer"] site_network_accesses = {} site_network_access_list = [] site_network_access_list.append(site_network_access) site_network_accesses["ietf-l2vpn-svc:site-network-access"] = site_network_access_list conn_info_d = {} - conn_info_d["site"] = connection_point_wan_info["wan_service_mapping_info"]["site-id"] + conn_info_d["site"] = connection_point_wan_info["service_mapping_info"]["site-id"] conn_info_d["site-network-access-id"] = site_network_access["network-access-id"] conn_info_d["mapping"] = None conn_info.append(conn_info_d) try: endpoint_site_network_access_creation = \ "{}/restconf/data/ietf-l2vpn-svc:l2vpn-svc/sites/site={}/site-network-accesses/".format( - self.wim["wim_url"], connection_point_wan_info["wan_service_mapping_info"]["site-id"]) + self.wim["wim_url"], connection_point_wan_info["service_mapping_info"]["site-id"]) response_endpoint_site_network_access_creation = requests.post( endpoint_site_network_access_creation, headers=self.headers, @@ -229,25 +229,25 @@ class WimconnectorIETFL2VPN(WimConnector): if response_endpoint_site_network_access_creation.status_code == 409: self.delete_connectivity_service(vpn_service["vpn-id"]) - raise WimConnectorError("Site_Network_Access with ID '{}' already exists".format( + raise SdnConnectorError("Site_Network_Access with ID '{}' already exists".format( site_network_access["network-access-id"]), http_code=response_endpoint_site_network_access_creation.status_code) elif response_endpoint_site_network_access_creation.status_code == 400: self.delete_connectivity_service(vpn_service["vpn-id"]) - raise WimConnectorError("Site {} does not exist".format( - connection_point_wan_info["wan_service_mapping_info"]["site-id"]), + raise SdnConnectorError("Site {} does not exist".format( + connection_point_wan_info["service_mapping_info"]["site-id"]), http_code=response_endpoint_site_network_access_creation.status_code) elif response_endpoint_site_network_access_creation.status_code != requests.codes.created and \ response_endpoint_site_network_access_creation.status_code != requests.codes.no_content: self.delete_connectivity_service(vpn_service["vpn-id"]) - raise WimConnectorError("Request no accepted", + raise SdnConnectorError("Request no accepted", http_code=response_endpoint_site_network_access_creation.status_code) except requests.exceptions.ConnectionError: self.delete_connectivity_service(vpn_service["vpn-id"]) - raise WimConnectorError("Request Timeout", http_code=408) + raise SdnConnectorError("Request Timeout", http_code=408) return uuid_l2vpn, conn_info else: @@ -265,9 +265,9 @@ class WimconnectorIETFL2VPN(WimConnector): self.wim["wim_url"], service_uuid) response = requests.delete(servicepoint, auth=self.auth) if response.status_code != requests.codes.no_content: - raise WimConnectorError("Error in the request", http_code=response.status_code) + raise SdnConnectorError("Error in the request", http_code=response.status_code) except requests.exceptions.ConnectionError: - raise WimConnectorError("Request Timeout", http_code=408) + raise SdnConnectorError("Request Timeout", http_code=408) def edit_connectivity_service(self, service_uuid, conn_info=None, connection_points=None, **kwargs): @@ -283,7 +283,7 @@ class WimconnectorIETFL2VPN(WimConnector): site_network_access = {} connection_point_wan_info = self.search_mapp(connection_point) params_site = {} - params_site["site-id"] = connection_point_wan_info["wan_service_mapping_info"]["site-id"] + params_site["site-id"] = connection_point_wan_info["service_mapping_info"]["site-id"] params_site["site-vpn-flavor"] = "site-vpn-flavor-single" device_site = {} device_site["device-id"] = connection_point_wan_info["device-id"] @@ -298,7 +298,7 @@ class WimconnectorIETFL2VPN(WimConnector): tagged_interf = {} service_endpoint_encapsulation_info = connection_point["service_endpoint_encapsulation_info"] if service_endpoint_encapsulation_info["vlan"] is None: - raise WimConnectorError("VLAN must be provided") + raise SdnConnectorError("VLAN must be provided") tagged_interf["cvlan-id"] = service_endpoint_encapsulation_info["vlan"] tagged["dot1q-vlan-tagged"] = tagged_interf connection["tagged-interface"] = tagged @@ -311,7 +311,7 @@ class WimconnectorIETFL2VPN(WimConnector): site_network_access["vpn-attachment"] = vpn_attach uuid_sna = conn_info[counter]["site-network-access-id"] site_network_access["network-access-id"] = uuid_sna - site_network_access["bearer"] = connection_point_wan_info["wan_service_mapping_info"]["bearer"] + site_network_access["bearer"] = connection_point_wan_info["service_mapping_info"]["bearer"] site_network_accesses = {} site_network_access_list = [] site_network_access_list.append(site_network_access) @@ -319,20 +319,20 @@ class WimconnectorIETFL2VPN(WimConnector): try: endpoint_site_network_access_edit = \ "{}/restconf/data/ietf-l2vpn-svc:l2vpn-svc/sites/site={}/site-network-accesses/".format( - self.wim["wim_url"], connection_point_wan_info["wan_service_mapping_info"]["site-id"]) + self.wim["wim_url"], connection_point_wan_info["service_mapping_info"]["site-id"]) response_endpoint_site_network_access_creation = requests.put(endpoint_site_network_access_edit, headers=self.headers, json=site_network_accesses, auth=self.auth) if response_endpoint_site_network_access_creation.status_code == 400: - raise WimConnectorError("Service does not exist", + raise SdnConnectorError("Service does not exist", http_code=response_endpoint_site_network_access_creation.status_code) elif response_endpoint_site_network_access_creation.status_code != 201 and \ response_endpoint_site_network_access_creation.status_code != 204: - raise WimConnectorError("Request no accepted", + raise SdnConnectorError("Request no accepted", http_code=response_endpoint_site_network_access_creation.status_code) except requests.exceptions.ConnectionError: - raise WimConnectorError("Request Timeout", http_code=408) + raise SdnConnectorError("Request Timeout", http_code=408) counter += 1 return None @@ -343,9 +343,9 @@ class WimconnectorIETFL2VPN(WimConnector): servicepoint = "{}/restconf/data/ietf-l2vpn-svc:l2vpn-svc/vpn-services".format(self.wim["wim_url"]) response = requests.delete(servicepoint, auth=self.auth) if response.status_code != requests.codes.no_content: - raise WimConnectorError("Unable to clear all connectivity services", http_code=response.status_code) + raise SdnConnectorError("Unable to clear all connectivity services", http_code=response.status_code) except requests.exceptions.ConnectionError: - raise WimConnectorError("Request Timeout", http_code=408) + raise SdnConnectorError("Request Timeout", http_code=408) def get_all_active_connectivity_services(self): """Provide information about all active connections provisioned by a @@ -356,7 +356,7 @@ class WimconnectorIETFL2VPN(WimConnector): servicepoint = "{}/restconf/data/ietf-l2vpn-svc:l2vpn-svc/vpn-services".format(self.wim["wim_url"]) response = requests.get(servicepoint, auth=self.auth) if response.status_code != requests.codes.ok: - raise WimConnectorError("Unable to get all connectivity services", http_code=response.status_code) + raise SdnConnectorError("Unable to get all connectivity services", http_code=response.status_code) return response except requests.exceptions.ConnectionError: - raise WimConnectorError("Request Timeout", http_code=408) + raise SdnConnectorError("Request Timeout", http_code=408) diff --git a/RO-SDN-tapi/requirements.txt b/RO-SDN-tapi/requirements.txt new file mode 100644 index 00000000..44c797f2 --- /dev/null +++ b/RO-SDN-tapi/requirements.txt @@ -0,0 +1,18 @@ +## +# 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 +git+https://osm.etsi.org/gerrit/osm/RO.git#egg=osm-ro + diff --git a/RO-SDN-tapi/setup.py b/RO-SDN-tapi/setup.py new file mode 100644 index 00000000..931dd66c --- /dev/null +++ b/RO-SDN-tapi/setup.py @@ -0,0 +1,53 @@ +#!/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" + +README = """ +=========== +osm-rosdn_tapi +=========== + +osm-ro pluging for tapi (ietfl2vpn) SDN +""" + +setup( + name=_name, + description='OSM ro sdn plugin for tapi (ietfl2vpn)', + long_description=README, + version_command=('git describe --match v* --tags --long --dirty', 'pep440-git-full'), + # version=VERSION, + # python_requires='>3.5.0', + author='ETSI OSM', + # TODO py3 author_email='', + maintainer='OSM_TECH@LIST.ETSI.ORG', # TODO py3 + # TODO py3 maintainer_email='', + url='https://osm.etsi.org/gitweb/?p=osm/RO.git;a=summary', + license='Apache 2.0', + + packages=[_name], + include_package_data=True, + dependency_links=["git+https://osm.etsi.org/gerrit/osm/RO.git#egg=osm-ro"], + install_requires=["requests", "osm-ro"], + setup_requires=['setuptools-version-command'], + entry_points={ + 'osm_rosdn.plugins': ['rosdn_tapi = osm_rosdn_tapi.wimconn_ietfl2vpn:WimconnectorIETFL2VPN'], + }, +) diff --git a/RO-SDN-tapi/stdeb.cfg b/RO-SDN-tapi/stdeb.cfg new file mode 100644 index 00000000..0c718e4f --- /dev/null +++ b/RO-SDN-tapi/stdeb.cfg @@ -0,0 +1,19 @@ +# +# 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 +Depends3: python3-requests, python3-osm-ro + diff --git a/RO-SDN-tapi/tox.ini b/RO-SDN-tapi/tox.ini new file mode 100644 index 00000000..7d643cd2 --- /dev/null +++ b/RO-SDN-tapi/tox.ini @@ -0,0 +1,41 @@ +## +# 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. +## + +[tox] +envlist = py3 +toxworkdir={homedir}/.tox + +[testenv] +basepython = python3 +install_command = python3 -m pip install -r requirements.txt -U {opts} {packages} +# deps = -r{toxinidir}/test-requirements.txt +commands=python3 -m unittest discover -v + +[testenv:flake8] +basepython = python3 +deps = flake8 +commands = flake8 osm_rosdn_tapi --max-line-length 120 \ + --exclude .svn,CVS,.gz,.git,__pycache__,.tox,local,temp --ignore W291,W293,E226,W504 + +[testenv:unittest] +basepython = python3 +commands = python3 -m unittest osm_rosdn_tapi.tests + +[testenv:build] +basepython = python3 +deps = stdeb + setuptools-version-command +commands = python3 setup.py --command-packages=stdeb.command bdist_deb + diff --git a/RO-client/README.rst b/RO-client/README.rst index 0e9c887f..9b60216f 100644 --- a/RO-client/README.rst +++ b/RO-client/README.rst @@ -1,6 +1,20 @@ -=========== + Copyright 2018 Telefonica S.A. + 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. + +============ osm-roclient -=========== +============ osm-roclient is a client for interact with osm-ro server diff --git a/RO/README.rst b/RO/README.rst index 3a2be888..44a4fc49 100644 --- a/RO/README.rst +++ b/RO/README.rst @@ -1,3 +1,17 @@ + Copyright 2018 Telefonica S.A. + 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. + =========== osm-ro =========== diff --git a/RO/osm_ro/__init__.py b/RO/osm_ro/__init__.py index e69de29b..7284a2be 100644 --- a/RO/osm_ro/__init__.py +++ b/RO/osm_ro/__init__.py @@ -0,0 +1,13 @@ +## +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +## diff --git a/RO/osm_ro/database_utils/install-db-server.sh b/RO/osm_ro/database_utils/install-db-server.sh index 36b8003c..8ef780c9 100755 --- a/RO/osm_ro/database_utils/install-db-server.sh +++ b/RO/osm_ro/database_utils/install-db-server.sh @@ -1,5 +1,22 @@ #!/usr/bin/env bash +## +# Copyright Telefonica Investigacion y Desarrollo, S.A.U. +# All Rights Reserved. +# +# 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. +## + DB_NAME='mano_db' DB_ADMIN_USER="root" DB_USER="mano" diff --git a/RO/osm_ro/database_utils/migrate_mano_db.sh b/RO/osm_ro/database_utils/migrate_mano_db.sh index 096a21ad..5d03b960 100755 --- a/RO/osm_ro/database_utils/migrate_mano_db.sh +++ b/RO/osm_ro/database_utils/migrate_mano_db.sh @@ -36,7 +36,7 @@ QUIET_MODE="" BACKUP_DIR="" BACKUP_FILE="" #TODO update it with the last database version -LAST_DB_VERSION=39 +LAST_DB_VERSION=40 # Detect paths MYSQL=$(which mysql) @@ -197,6 +197,7 @@ fi #[ $OPENMANO_VER_NUM -ge 6009 ] && DB_VERSION=37 #0.6.09 => 37 #[ $OPENMANO_VER_NUM -ge 6011 ] && DB_VERSION=38 #0.6.11 => 38 #[ $OPENMANO_VER_NUM -ge 6020 ] && DB_VERSION=39 #0.6.20 => 39 +#[ $OPENMANO_VER_NUM -ge 6000004 ] && DB_VERSION=40 #6.0.4 => 40 #TODO ... put next versions here function upgrade_to_1(){ @@ -1461,6 +1462,56 @@ function downgrade_from_39(){ sql "DELETE FROM schema_version WHERE version_int='39';" } +function upgrade_to_40(){ + echo " Adding instance_wim_net_id, created_at, modified_at at 'instance_interfaces'" + sql "ALTER TABLE instance_interfaces ADD COLUMN instance_wim_net_id VARCHAR(36) NULL AFTER instance_net_id, "\ + "ADD COLUMN model VARCHAR(12) NULL DEFAULT NULL AFTER type, "\" + "ADD COLUMN created_at DOUBLE NULL DEFAULT NULL AFTER vlan, " \ + "ADD COLUMN modified_at DOUBLE NULL DEFAULT NULL AFTER created_at;" + echo " Adding sdn to 'instance_wim_nets'" + sql "ALTER TABLE instance_wim_nets ADD COLUMN sdn ENUM('true','false') NOT NULL DEFAULT 'false' AFTER created;" + echo " Change from created to sdn at 'wim_accounts'" + sql "ALTER TABLE wim_accounts CHANGE COLUMN created sdn ENUM('true','false') NOT NULL DEFAULT 'false' AFTER wim_id;" + echo " Remove unique_datacenter_port_mapping at 'wim_port_mappings'" + sql "ALTER TABLE wim_port_mappings DROP INDEX unique_datacenter_port_mapping;" + echo " change 'wim_port_mappings' pop_x to device_x, adding switch_dpid, switch_port" + sql "ALTER TABLE wim_port_mappings ALTER pop_switch_dpid DROP DEFAULT, ALTER pop_switch_port DROP DEFAULT;" + sql "ALTER TABLE wim_port_mappings CHANGE COLUMN pop_switch_dpid device_id VARCHAR(64) NULL AFTER datacenter_id," \ + " CHANGE COLUMN pop_switch_port device_interface_id VARCHAR(64) NULL AFTER device_id, " \ + " CHANGE COLUMN wan_service_endpoint_id service_endpoint_id VARCHAR(256) NOT NULL AFTER device_interface_id, " \ + " CHANGE COLUMN wan_service_mapping_info service_mapping_info TEXT NULL AFTER service_endpoint_id, " \ + " ADD COLUMN switch_dpid VARCHAR(64) NULL AFTER wan_service_endpoint_id," \ + " ADD COLUMN switch_port VARCHAR(64) NULL AFTER switch_dpid;" + echo " remove unique name to 'datacenters'" + sql "ALTER TABLE datacenters DROP INDEX name;" + + sql "INSERT INTO schema_version (version_int, version, openmano_ver, comments, date) " \ + "VALUES (40, '0.40', '6.0.4', 'Chagnes to SDN ', '2019-10-23');" +} +function downgrade_from_40(){ + echo " Removing instance_wim_net_id, created_at, modified_at from 'instance_interfaces'" + sql "ALTER TABLE instance_interfaces DROP COLUMN instance_wim_net_id, DROP COLUMN created_at, " \ + "DROP COLUMN modified_at, DROP COLUMN model;" + echo " Removing sdn from 'instance_wim_nets'" + sql "ALTER TABLE instance_wim_nets DROP COLUMN sdn;" + echo " Change back from sdn to created at 'wim_accounts'" + sql "ALTER TABLE wim_accounts CHANGE COLUMN sdn created ENUM('true','false') NOT NULL DEFAULT 'false' AFTER wim_id;" + echo " Restore back unique_datacenter_port_mapping at 'wim_port_mappings'" + echo " change 'wim_port_mappings' device_x to pop_x, remove switch_dpid, switch_port" + sql "ALTER TABLE wim_port_mappings ALTER device_id DROP DEFAULT, ALTER device_interface_id DROP DEFAULT;" + sql "ALTER TABLE wim_port_mappings CHANGE COLUMN device_id pop_switch_dpid VARCHAR(64) NOT NULL AFTER " \ + "datacenter_id, CHANGE COLUMN device_interface_id pop_switch_port VARCHAR(64) NOT NULL AFTER pop_switch_dpid," \ + " CHANGE COLUMN service_endpoint_id wan_service_endpoint_id VARCHAR(256) NOT NULL AFTER pop_switch_port, " \ + " CHANGE COLUMN service_mapping_info wan_service_mapping_info TEXT NULL AFTER wan_service_endpoint_id, " \ + " DROP COLUMN switch_dpid, DROP COLUMN switch_port;" + sql "ALTER TABLE wim_port_mappings ADD UNIQUE INDEX unique_datacenter_port_mapping(datacenter_id, pop_switch_dpid, + pop_switch_port);" + echo " add unique name to 'datacenters'" + sql "ALTER TABLE datacenters ADD UNIQUE INDEX name (name);" + sql "DELETE FROM schema_version WHERE version_int='40';" +} + + #TODO ... put functions here diff --git a/RO/osm_ro/database_utils/migrations/down/34_remove_wim_tables.sql b/RO/osm_ro/database_utils/migrations/down/34_remove_wim_tables.sql index 4400e39f..7ab4bf77 100644 --- a/RO/osm_ro/database_utils/migrations/down/34_remove_wim_tables.sql +++ b/RO/osm_ro/database_utils/migrations/down/34_remove_wim_tables.sql @@ -1,3 +1,16 @@ +/** +* 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. +**/ -- -- Tear down database structure required for integrating OSM with -- Wide Are Network Infrastructure Managers diff --git a/RO/osm_ro/database_utils/migrations/down/35_remove_sfc_ingress_and_egress.sql b/RO/osm_ro/database_utils/migrations/down/35_remove_sfc_ingress_and_egress.sql index 01f38f4d..668eeb39 100644 --- a/RO/osm_ro/database_utils/migrations/down/35_remove_sfc_ingress_and_egress.sql +++ b/RO/osm_ro/database_utils/migrations/down/35_remove_sfc_ingress_and_egress.sql @@ -1,3 +1,16 @@ +/** +* 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. +**/ -- -- Removing ingress and egress ports for SFC purposes. -- Inserting only one port for ingress and egress. diff --git a/RO/osm_ro/database_utils/migrations/up/34_add_wim_tables.sql b/RO/osm_ro/database_utils/migrations/up/34_add_wim_tables.sql index 343f370a..eb99b8b0 100644 --- a/RO/osm_ro/database_utils/migrations/up/34_add_wim_tables.sql +++ b/RO/osm_ro/database_utils/migrations/up/34_add_wim_tables.sql @@ -1,3 +1,16 @@ +/** +* 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. +**/ -- -- Setup database structure required for integrating OSM with -- Wide Are Network Infrastructure Managers diff --git a/RO/osm_ro/database_utils/migrations/up/35_add_sfc_ingress_and_egress.sql b/RO/osm_ro/database_utils/migrations/up/35_add_sfc_ingress_and_egress.sql index b528c6da..fea4cef6 100644 --- a/RO/osm_ro/database_utils/migrations/up/35_add_sfc_ingress_and_egress.sql +++ b/RO/osm_ro/database_utils/migrations/up/35_add_sfc_ingress_and_egress.sql @@ -1,3 +1,16 @@ +/** +* 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. +**/ -- -- Adding different ingress and egress ports for SFC. -- diff --git a/RO/osm_ro/http_tools/__init__.py b/RO/osm_ro/http_tools/__init__.py index e69de29b..7284a2be 100644 --- a/RO/osm_ro/http_tools/__init__.py +++ b/RO/osm_ro/http_tools/__init__.py @@ -0,0 +1,13 @@ +## +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +## diff --git a/RO/osm_ro/http_tools/errors.py b/RO/osm_ro/http_tools/errors.py index 552e85b1..2a3f0274 100644 --- a/RO/osm_ro/http_tools/errors.py +++ b/RO/osm_ro/http_tools/errors.py @@ -1,4 +1,18 @@ # -*- 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. +## + import logging from functools import wraps diff --git a/RO/osm_ro/http_tools/request_processing.py b/RO/osm_ro/http_tools/request_processing.py index 13e19ed5..72851429 100644 --- a/RO/osm_ro/http_tools/request_processing.py +++ b/RO/osm_ro/http_tools/request_processing.py @@ -1,4 +1,17 @@ # -*- 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. +## # # Util functions previously in `httpserver` diff --git a/RO/osm_ro/http_tools/tests/__init__.py b/RO/osm_ro/http_tools/tests/__init__.py index e69de29b..7284a2be 100644 --- a/RO/osm_ro/http_tools/tests/__init__.py +++ b/RO/osm_ro/http_tools/tests/__init__.py @@ -0,0 +1,13 @@ +## +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +## diff --git a/RO/osm_ro/http_tools/tests/test_errors.py b/RO/osm_ro/http_tools/tests/test_errors.py index a968e768..e2b1d434 100644 --- a/RO/osm_ro/http_tools/tests/test_errors.py +++ b/RO/osm_ro/http_tools/tests/test_errors.py @@ -1,4 +1,18 @@ # -*- 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. +## + import unittest import bottle diff --git a/RO/osm_ro/http_tools/tests/test_handler.py b/RO/osm_ro/http_tools/tests/test_handler.py index af325450..e0157584 100644 --- a/RO/osm_ro/http_tools/tests/test_handler.py +++ b/RO/osm_ro/http_tools/tests/test_handler.py @@ -1,4 +1,18 @@ # -*- 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. +## + import unittest from mock import MagicMock, patch diff --git a/RO/osm_ro/http_tools/tox.ini b/RO/osm_ro/http_tools/tox.ini index 43055c26..93e2f15f 100644 --- a/RO/osm_ro/http_tools/tox.ini +++ b/RO/osm_ro/http_tools/tox.ini @@ -1,3 +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. +## + # This tox file allows the devs to run unit tests only for this subpackage. # In order to do so, cd into the directory and run `tox` diff --git a/RO/osm_ro/nfvo.py b/RO/osm_ro/nfvo.py index a28f57f6..6a06a4ca 100644 --- a/RO/osm_ro/nfvo.py +++ b/RO/osm_ro/nfvo.py @@ -45,13 +45,11 @@ from osm_ro import nfvo_db from threading import Lock import time as t # TODO py3 BEGIN -# from lib_osm_openvim import ovim as ovim_module +from osm_ro.sdn import Sdn, SdnException as ovimException # from lib_osm_openvim.ovim import ovimException -from unittest.mock import MagicMock -ovim_module = MagicMock() -class ovimException(Exception): - pass -ovim_module.ovimException = ovimException +# from unittest.mock import MagicMock +# class ovimException(Exception): +# pass # TODO py3 END from Crypto.PublicKey import RSA @@ -64,11 +62,12 @@ from pkg_resources import iter_entry_points # WIM -import osm_ro.wim.wimconn as wimconn -import osm_ro.wim.wim_thread as wim_thread -from osm_ro.http_tools import errors as httperrors -from osm_ro.wim.engine import WimEngine -from osm_ro.wim.persistence import WimPersistence +from .wim import sdnconn +from .wim.wimconn_fake import FakeConnector +from .wim.failing_connector import FailingConnector +from .http_tools import errors as httperrors +from .wim.engine import WimEngine +from .wim.persistence import WimPersistence from copy import deepcopy from pprint import pformat # @@ -77,7 +76,7 @@ global global_config # WIM global wim_engine wim_engine = None -global wimconn_imported +global sdnconn_imported # global logger global default_volume_size @@ -90,7 +89,7 @@ plugins = {} # dictionary with VIM type as key, loaded module as value vim_threads = {"running":{}, "deleting": {}, "names": []} # threads running for attached-VIMs vim_persistent_info = {} # WIM -wimconn_imported = {} # dictionary with WIM type as key, loaded module as value +sdnconn_imported = {} # dictionary with WIM type as key, loaded module as value wim_threads = {"running":{}, "deleting": {}, "names": []} # threads running for attached-WIMs wim_persistent_info = {} # @@ -105,13 +104,23 @@ db_lock = Lock() class NfvoException(httperrors.HttpMappedError): """Common Class for NFVO errors""" -def _load_vim_plugin(name): +def _load_plugin(name, type="vim"): + # type can be vim or sdn global plugins - for v in iter_entry_points('osm_rovim.plugins', name): - plugins[name] = v.load() + try: + for v in iter_entry_points('osm_ro{}.plugins'.format(type), name): + plugins[name] = v.load() + except Exception as e: + logger.critical("Cannot load osm_{}: {}".format(name, e)) + if name: + plugins[name] = FailingConnector("Cannot load osm_{}: {}".format(name, e)) if name and name not in plugins: - raise NfvoException("Unknown vim type '{}'. This plugin has not been registered".format(name), - httperrors.Bad_Request) + error_text = "Cannot load a module for {t} type '{n}'. The plugin 'osm_{n}' has not been" \ + " registered".format(t=type, n=name) + logger.critical(error_text) + plugins[name] = FailingConnector(error_text) + # raise NfvoException("Cannot load a module for {t} type '{n}'. The plugin 'osm_{n}' has not been registered". + # format(t=type, n=name), httperrors.Bad_Request) def get_task_id(): global last_task_id @@ -166,37 +175,21 @@ def get_non_used_wim_name(wim_name, wim_id, tenant_name, tenant_id): def start_service(mydb, persistence=None, wim=None): - global db, global_config, plugins + global db, global_config, plugins, ovim db = nfvo_db.nfvo_db(lock=db_lock) mydb.lock = db_lock db.connect(global_config['db_host'], global_config['db_user'], global_config['db_passwd'], global_config['db_name']) - global ovim persistence = persistence or WimPersistence(db) - # Initialize openvim for SDN control - # TODO: Avoid static configuration by adding new parameters to openmanod.cfg - # TODO: review ovim.py to delete not needed configuration - ovim_configuration = { - 'logger_name': 'openmano.ovim', - 'network_vlan_range_start': 1000, - 'network_vlan_range_end': 4096, - 'db_name': global_config["db_ovim_name"], - 'db_host': global_config["db_ovim_host"], - 'db_user': global_config["db_ovim_user"], - 'db_passwd': global_config["db_ovim_passwd"], - 'bridge_ifaces': {}, - 'mode': 'normal', - 'network_type': 'bridge', - #TODO: log_level_of should not be needed. To be modified in ovim - 'log_level_of': 'DEBUG' - } try: + if "rosdn_fake" not in plugins: + plugins["rosdn_fake"] = FakeConnector # starts ovim library - ovim = ovim_module.ovim(ovim_configuration) + ovim = Sdn(db, plugins) global wim_engine - wim_engine = wim or WimEngine(persistence) + wim_engine = wim or WimEngine(persistence, plugins) wim_engine.ovim = ovim ovim.start_service() @@ -221,7 +214,7 @@ def start_service(mydb, persistence=None, wim=None): extra.update(yaml.load(vim["dt_config"], Loader=yaml.Loader)) plugin_name = "rovim_" + vim["type"] if plugin_name not in plugins: - _load_vim_plugin(plugin_name) + _load_plugin(plugin_name, type="vim") thread_id = vim['datacenter_tenant_id'] vim_persistent_info[thread_id] = {} @@ -240,19 +233,33 @@ def start_service(mydb, persistence=None, wim=None): logger.error("Cannot launch thread for VIM {} '{}': {}".format(vim['datacenter_name'], vim['datacenter_id'], e)) except Exception as e: - raise NfvoException("Error at VIM {}; {}: {}".format(vim["type"], type(e).__name__, e), - httperrors.Internal_Server_Error) + logger.critical("Cannot launch thread for VIM {} '{}': {}".format(vim['datacenter_name'], + vim['datacenter_id'], e)) + # raise NfvoException("Error at VIM {}; {}: {}".format(vim["type"], type(e).__name__, e), + # httperrors.Internal_Server_Error) thread_name = get_non_used_vim_name(vim['datacenter_name'], vim['datacenter_id'], vim['vim_tenant_name'], vim['vim_tenant_id']) - new_thread = vim_thread(task_lock, plugins, thread_name, vim['datacenter_name'], - vim['datacenter_tenant_id'], db=db, db_lock=db_lock, ovim=ovim) + new_thread = vim_thread(task_lock, plugins, thread_name, None, + vim['datacenter_tenant_id'], db=db) new_thread.start() vim_threads["running"][thread_id] = new_thread + wims = mydb.get_rows(FROM="wim_accounts join wims on wim_accounts.wim_id=wims.uuid", + WHERE={"sdn": "true"}, + SELECT=("wim_accounts.uuid as uuid", "type", "wim_accounts.name as name")) + for wim in wims: + plugin_name = "rosdn_" + wim["type"] + if plugin_name not in plugins: + _load_plugin(plugin_name, type="sdn") + thread_id = wim['uuid'] + thread_name = get_non_used_vim_name(wim['name'], wim['uuid'], wim['uuid'], None) + new_thread = vim_thread(task_lock, plugins, thread_name, wim['uuid'], None, db=db) + new_thread.start() + vim_threads["running"][thread_id] = new_thread wim_engine.start_threads() except db_base_Exception as e: raise NfvoException(str(e) + " at nfvo.get_vim", e.http_code) - except ovim_module.ovimException as e: + except ovimException as e: message = str(e) if message[:22] == "DATABASE wrong version": message = "DATABASE wrong version of lib_osm_openvim {msg} -d{dbname} -u{dbuser} -p{dbpass} {ver}' "\ @@ -397,7 +404,7 @@ def get_vim(mydb, nfvo_tenant=None, datacenter_id=None, datacenter_name=None, da plugin_name = "rovim_" + vim["type"] if plugin_name not in plugins: try: - _load_vim_plugin(plugin_name) + _load_plugin(plugin_name, type="vim") except NfvoException as e: if ignore_errors: logger.error("{}".format(e)) @@ -3030,6 +3037,14 @@ def update(d, u): return d +def _get_wim(db, wim_account_id): + # get wim from wim_account + wim_accounts = db.get_rows(FROM='wim_accounts', WHERE={"uuid": wim_account_id}) + if not wim_accounts: + raise NfvoException("Not found sdn id={}".format(wim_account_id), http_code=httperrors.Not_Found) + return wim_accounts[0]["wim_id"] + + def create_instance(mydb, tenant_id, instance_dict): # print "Checking that nfvo_tenant_id exists and getting the VIM URI and the VIM tenant_id" # logger.debug("Creating instance...") @@ -3094,6 +3109,7 @@ def create_instance(mydb, tenant_id, instance_dict): } # Auxiliary dictionaries from x to y + sce_net2wim_instance = {} sce_net2instance = {} net2task_id = {'scenario': {}} # Mapping between local networks and WIMs @@ -3229,6 +3245,7 @@ def create_instance(mydb, tenant_id, instance_dict): # 1. Creating new nets (sce_nets) in the VIM" number_mgmt_networks = 0 db_instance_nets = [] + db_instance_wim_nets = [] for sce_net in scenarioDict['nets']: sce_net_uuid = sce_net.get('uuid', sce_net["name"]) # get involved datacenters where this network need to be created @@ -3286,6 +3303,7 @@ def create_instance(mydb, tenant_id, instance_dict): if site.get("datacenter") and site["datacenter"] not in involved_datacenters: involved_datacenters.append(site["datacenter"]) sce_net2instance[sce_net_uuid] = {} + sce_net2wim_instance[sce_net_uuid] = {} net2task_id['scenario'][sce_net_uuid] = {} use_network = None @@ -3393,6 +3411,41 @@ def create_instance(mydb, tenant_id, instance_dict): sce_net2instance[sce_net_uuid][datacenter_id] = net_uuid if not related_network: # all db_instance_nets will have same related related_network = use_network or net_uuid + sdn_net_id = None + sdn_controller = vim.config.get('sdn-controller') + sce_net2wim_instance[sce_net_uuid][datacenter_id] = None + if sdn_controller and net_type in ("data", "ptp"): + wim_id = _get_wim(mydb, sdn_controller) + sdn_net_id = str(uuid4()) + sce_net2wim_instance[sce_net_uuid][datacenter_id] = sdn_net_id + task_extra["sdn_net_id"] = sdn_net_id + db_instance_wim_nets.append({ + "uuid": sdn_net_id, + "instance_scenario_id": instance_uuid, + "sce_net_id": sce_net.get("uuid"), + "wim_id": wim_id, + "wim_account_id": sdn_controller, + 'status': 'BUILD', # if create_network else "ACTIVE" + "related": related_network, + 'multipoint': True if net_type=="data" else False, + "created": create_network, # TODO py3 + "sdn": True, + }) + task_wim_extra = {"params": [net_type, wim_account_name]} + db_vim_action = { + "instance_action_id": instance_action_id, + "status": "SCHEDULED", + "task_index": task_index, + # "datacenter_vim_id": myvim_thread_id, + "wim_account_id": sdn_controller, + "action": task_action, + "item": "instance_wim_nets", + "item_id": sdn_net_id, + "related": related_network, + "extra": yaml.safe_dump(task_wim_extra, default_flow_style=True, width=256) + } + task_index += 1 + db_vim_actions.append(db_vim_action) db_net = { "uuid": net_uuid, "osm_id": sce_net.get("osm_id") or sce_net["name"], @@ -3404,7 +3457,8 @@ def create_instance(mydb, tenant_id, instance_dict): "created": create_network, 'datacenter_id': datacenter_id, 'datacenter_tenant_id': myvim_thread_id, - 'status': 'BUILD' # if create_network else "ACTIVE" + 'status': 'BUILD', # if create_network else "ACTIVE" + 'sdn_net_id': sdn_net_id, } db_instance_nets.append(db_net) db_vim_action = { @@ -3451,6 +3505,7 @@ def create_instance(mydb, tenant_id, instance_dict): "task_index": task_index, "uuid_list": uuid_list, "db_instance_nets": db_instance_nets, + "db_instance_wim_nets": db_instance_wim_nets, "db_vim_actions": db_vim_actions, "db_ip_profiles": db_ip_profiles, "db_instance_vnfs": db_instance_vnfs, @@ -3458,6 +3513,7 @@ def create_instance(mydb, tenant_id, instance_dict): "db_instance_interfaces": db_instance_interfaces, "net2task_id": net2task_id, "sce_net2instance": sce_net2instance, + "sce_net2wim_instance": sce_net2wim_instance, } # sce_vnf_list = sorted(scenarioDict['vnfs'], key=lambda k: k['name']) for sce_vnf in scenarioDict.get('vnfs', ()): # sce_vnf_list: @@ -3655,7 +3711,7 @@ def create_instance(mydb, tenant_id, instance_dict): {"instance_sfs": db_instance_sfs}, {"instance_classifications": db_instance_classifications}, {"instance_sfps": db_instance_sfps}, - {"instance_wim_nets": wan_links}, + {"instance_wim_nets": db_instance_wim_nets + wan_links}, {"vim_wim_actions": db_vim_actions + wim_actions} ] @@ -3670,13 +3726,13 @@ def create_instance(mydb, tenant_id, instance_dict): returned_instance = mydb.get_instance_scenario(instance_uuid) returned_instance["action_id"] = instance_action_id return returned_instance - except (NfvoException, vimconn.vimconnException, wimconn.WimConnectorError, db_base_Exception) as e: + except (NfvoException, vimconn.vimconnException, sdnconn.SdnConnectorError, db_base_Exception) as e: message = rollback(mydb, myvims, rollbackList) if isinstance(e, db_base_Exception): error_text = "database Exception" elif isinstance(e, vimconn.vimconnException): error_text = "VIM Exception" - elif isinstance(e, wimconn.WimConnectorError): + elif isinstance(e, sdnconn.SdnConnectorError): error_text = "WIM Exception" else: error_text = "Exception" @@ -3699,6 +3755,7 @@ def instantiate_vnf(mydb, sce_vnf, params, params_out, rollbackList): task_index = params_out["task_index"] uuid_list = params_out["uuid_list"] db_instance_nets = params_out["db_instance_nets"] + db_instance_wim_nets = params_out["db_instance_wim_nets"] db_vim_actions = params_out["db_vim_actions"] db_ip_profiles = params_out["db_ip_profiles"] db_instance_vnfs = params_out["db_instance_vnfs"] @@ -3706,15 +3763,18 @@ def instantiate_vnf(mydb, sce_vnf, params, params_out, rollbackList): db_instance_interfaces = params_out["db_instance_interfaces"] net2task_id = params_out["net2task_id"] sce_net2instance = params_out["sce_net2instance"] + sce_net2wim_instance = params_out["sce_net2wim_instance"] vnf_net2instance = {} # 2. Creating new nets (vnf internal nets) in the VIM" # For each vnf net, we create it and we add it to instanceNetlist. if sce_vnf.get("datacenter"): + vim = myvims[sce_vnf["datacenter"]] datacenter_id = sce_vnf["datacenter"] myvim_thread_id = myvim_threads_id[sce_vnf["datacenter"]] else: + vim = myvims[default_datacenter_id] datacenter_id = default_datacenter_id myvim_thread_id = myvim_threads_id[default_datacenter_id] for net in sce_vnf['nets']: @@ -3731,12 +3791,29 @@ def instantiate_vnf(mydb, sce_vnf, params, params_out, rollbackList): vnf_net2instance[sce_vnf['uuid']] = {} if sce_vnf['uuid'] not in net2task_id: net2task_id[sce_vnf['uuid']] = {} - net2task_id[sce_vnf['uuid']][net['uuid']] = task_index # fill database content net_uuid = str(uuid4()) uuid_list.append(net_uuid) vnf_net2instance[sce_vnf['uuid']][net['uuid']] = net_uuid + + sdn_controller = vim.config.get('sdn-controller') + sdn_net_id = None + if sdn_controller and net_type in ("data", "ptp"): + wim_id = _get_wim(mydb, sdn_controller) + sdn_net_id = str(uuid4()) + db_instance_wim_nets.append({ + "uuid": sdn_net_id, + "instance_scenario_id": instance_uuid, + "wim_id": wim_id, + "wim_account_id": sdn_controller, + 'status': 'BUILD', # if create_network else "ACTIVE" + "related": net_uuid, + 'multipoint': True if net_type == "data" else False, + "created": True, # TODO py3 + "sdn": True, + }) + db_net = { "uuid": net_uuid, "related": net_uuid, @@ -3747,6 +3824,7 @@ def instantiate_vnf(mydb, sce_vnf, params, params_out, rollbackList): "created": True, 'datacenter_id': datacenter_id, 'datacenter_tenant_id': myvim_thread_id, + 'sdn_net_id': sdn_net_id, } db_instance_nets.append(db_net) @@ -3761,7 +3839,25 @@ def instantiate_vnf(mydb, sce_vnf, params, params_out, rollbackList): else: task_action = "CREATE" task_extra = {"params": (net_name, net_type, net.get('ip_profile', None))} + if sdn_net_id: + task_extra["sdn_net_id"] = sdn_net_id + if sdn_net_id: + task_wim_extra = {"params": [net_type, None]} + db_vim_action = { + "instance_action_id": instance_action_id, + "status": "SCHEDULED", + "task_index": task_index, + # "datacenter_vim_id": myvim_thread_id, + "wim_account_id": sdn_controller, + "action": task_action, + "item": "instance_wim_nets", + "item_id": sdn_net_id, + "related": net_uuid, + "extra": yaml.safe_dump(task_wim_extra, default_flow_style=True, width=256) + } + task_index += 1 + db_vim_actions.append(db_vim_action) db_vim_action = { "instance_action_id": instance_action_id, "task_index": task_index, @@ -3773,6 +3869,7 @@ def instantiate_vnf(mydb, sce_vnf, params, params_out, rollbackList): "related": net_uuid, "extra": yaml.safe_dump(task_extra, default_flow_style=True, width=256) } + net2task_id[sce_vnf['uuid']][net['uuid']] = task_index task_index += 1 db_vim_actions.append(db_vim_action) @@ -3946,6 +4043,7 @@ def instantiate_vnf(mydb, sce_vnf, params, params_out, rollbackList): netDict['net_id'] = "TASK-{}".format( net2task_id['scenario'][vnf_iface['sce_net_id']][datacenter_id]) instance_net_id = sce_net2instance[vnf_iface['sce_net_id']][datacenter_id] + instance_wim_net_id = sce_net2wim_instance[vnf_iface['sce_net_id']][datacenter_id] task_depends_on.append(net2task_id['scenario'][vnf_iface['sce_net_id']][datacenter_id]) break else: @@ -3960,9 +4058,11 @@ def instantiate_vnf(mydb, sce_vnf, params, params_out, rollbackList): # "uuid" # 'instance_vm_id': instance_vm_uuid, "instance_net_id": instance_net_id, + "instance_wim_net_id": instance_wim_net_id, 'interface_id': iface['uuid'], # 'vim_interface_id': , 'type': 'external' if iface['external_name'] is not None else 'internal', + 'model': iface['model'], 'ip_address': iface.get('ip_address'), 'mac_address': iface.get('mac'), 'floating_ip': int(iface.get('floating-ip', False)), @@ -4349,6 +4449,23 @@ def delete_instance(mydb, tenant_id, instance_id): } task_index += 1 db_vim_actions.append(db_vim_action) + for sdn_net in instanceDict['sdn_nets']: + if not sdn_net["sdn"]: + continue + extra = {} + db_vim_action = { + "instance_action_id": instance_action_id, + "task_index": task_index, + "wim_account_id": sdn_net["wim_account_id"], + "action": "DELETE", + "status": "SCHEDULED", + "item": "instance_wim_nets", + "item_id": sdn_net["uuid"], + "related": sdn_net["related"], + "extra": yaml.safe_dump(extra, default_flow_style=True, width=256) + } + task_index += 1 + db_vim_actions.append(db_vim_action) db_instance_action["number_tasks"] = task_index @@ -4383,18 +4500,19 @@ def get_instance_id(mydb, tenant_id, instance_id): #obtain data instance_dict = mydb.get_instance_scenario(instance_id, tenant_id, verbose=True) - for net in instance_dict["nets"]: - if net.get("sdn_net_id"): - net_sdn = ovim.show_network(net["sdn_net_id"]) - net["sdn_info"] = { - "admin_state_up": net_sdn.get("admin_state_up"), - "flows": net_sdn.get("flows"), - "last_error": net_sdn.get("last_error"), - "ports": net_sdn.get("ports"), - "type": net_sdn.get("type"), - "status": net_sdn.get("status"), - "vlan": net_sdn.get("vlan"), - } + # TODO py3 + # for net in instance_dict["nets"]: + # if net.get("sdn_net_id"): + # net_sdn = ovim.show_network(net["sdn_net_id"]) + # net["sdn_info"] = { + # "admin_state_up": net_sdn.get("admin_state_up"), + # "flows": net_sdn.get("flows"), + # "last_error": net_sdn.get("last_error"), + # "ports": net_sdn.get("ports"), + # "type": net_sdn.get("type"), + # "status": net_sdn.get("status"), + # "vlan": net_sdn.get("vlan"), + # } return instance_dict @deprecated("Instance is automatically refreshed by vim_threads") @@ -4943,7 +5061,7 @@ def new_datacenter(mydb, datacenter_descriptor): # load plugin plugin_name = "rovim_" + datacenter_type if plugin_name not in plugins: - _load_vim_plugin(plugin_name) + _load_plugin(plugin_name, type="vim") datacenter_id = mydb.new_row("datacenters", datacenter_descriptor, add_uuid=True, confidential_data=True) if sdn_port_mapping: @@ -5095,8 +5213,7 @@ def create_vim_account(mydb, nfvo_tenant, datacenter_id, name=None, vim_id=None, # create thread thread_name = get_non_used_vim_name(datacenter_name, datacenter_id, tenant_dict['name'], tenant_dict['uuid']) - new_thread = vim_thread(task_lock, plugins, thread_name, datacenter_name, datacenter_tenant_id, - db=db, db_lock=db_lock, ovim=ovim) + new_thread = vim_thread(task_lock, plugins, thread_name, None, datacenter_tenant_id, db=db) new_thread.start() thread_id = datacenter_tenants_dict["uuid"] vim_threads["running"][thread_id] = new_thread @@ -5609,13 +5726,20 @@ def vim_action_create(mydb, tenant_id, datacenter, item, descriptor): return vim_action_get(mydb, tenant_id, datacenter, item, content) def sdn_controller_create(mydb, tenant_id, sdn_controller): - data = ovim.new_of_controller(sdn_controller) - logger.debug('New SDN controller created with uuid {}'.format(data)) - return data + wim_id = ovim.new_of_controller(sdn_controller) + + thread_name = get_non_used_vim_name(sdn_controller['name'], wim_id, wim_id, None) + new_thread = vim_thread(task_lock, plugins, thread_name, wim_id, None, db=db) + new_thread.start() + thread_id = wim_id + vim_threads["running"][thread_id] = new_thread + logger.debug('New SDN controller created with uuid {}'.format(wim_id)) + return wim_id def sdn_controller_update(mydb, tenant_id, controller_id, sdn_controller): data = ovim.edit_of_controller(controller_id, sdn_controller) msg = 'SDN controller {} updated'.format(data) + vim_threads["running"][controller_id].insert_task("reload") logger.debug(msg) return msg @@ -5661,20 +5785,23 @@ def datacenter_sdn_port_mapping_set(mydb, tenant_id, datacenter_id, sdn_port_map #element = {"ofc_id": sdn_controller_id, "region": datacenter_id, "switch_dpid": switch_dpid} element = dict() element["compute_node"] = compute_node["compute_node"] - for port in compute_node["ports"]: - pci = port.get("pci") - element["switch_port"] = port.get("switch_port") - element["switch_mac"] = port.get("switch_mac") - if not element["switch_port"] and not element["switch_mac"]: - raise NfvoException ("The mapping must contain 'switch_port' or 'switch_mac'", httperrors.Bad_Request) - for pci_expanded in utils.expand_brackets(pci): - element["pci"] = pci_expanded - maps.append(dict(element)) - - return ovim.set_of_port_mapping(maps, ofc_id=sdn_controller_id, switch_dpid=switch_dpid, region=datacenter_id) + if compute_node["ports"]: + for port in compute_node["ports"]: + pci = port.get("pci") + element["switch_port"] = port.get("switch_port") + element["switch_mac"] = port.get("switch_mac") + if not element["switch_port"] and not element["switch_mac"]: + raise NfvoException ("The mapping must contain 'switch_port' or 'switch_mac'", httperrors.Bad_Request) + for pci_expanded in utils.expand_brackets(pci): + element["pci"] = pci_expanded + maps.append(dict(element)) + + out = ovim.set_of_port_mapping(maps, sdn_id=sdn_controller_id, switch_dpid=switch_dpid, vim_id=datacenter_id) + vim_threads["running"][sdn_controller_id].insert_task("reload") + return out def datacenter_sdn_port_mapping_list(mydb, tenant_id, datacenter_id): - maps = ovim.get_of_port_mappings(db_filter={"region": datacenter_id}) + maps = ovim.get_of_port_mappings(db_filter={"datacenter_id": datacenter_id}) result = { "sdn-controller": None, @@ -5703,24 +5830,25 @@ def datacenter_sdn_port_mapping_list(mydb, tenant_id, datacenter_id): ports_correspondence_dict = dict() for link in maps: - if result["sdn-controller"] != link["ofc_id"]: + if result["sdn-controller"] != link["wim_id"]: raise NfvoException("The sdn-controller specified for different port mappings differ", httperrors.Internal_Server_Error) if result["dpid"] != link["switch_dpid"]: raise NfvoException("The dpid specified for different port mappings differ", httperrors.Internal_Server_Error) + link_config = link["service_mapping_info"] element = dict() - element["pci"] = link["pci"] + element["pci"] = link.get("device_interface_id") if link["switch_port"]: element["switch_port"] = link["switch_port"] - if link["switch_mac"]: - element["switch_mac"] = link["switch_mac"] + if link_config["switch_mac"]: + element["switch_mac"] = link_config.get("switch_mac") - if not link["compute_node"] in ports_correspondence_dict: + if not link.get("interface_id") in ports_correspondence_dict: content = dict() - content["compute_node"] = link["compute_node"] + content["compute_node"] = link.get("interface_id") content["ports"] = list() - ports_correspondence_dict[link["compute_node"]] = content + ports_correspondence_dict[link.get("interface_id")] = content - ports_correspondence_dict[link["compute_node"]]["ports"].append(element) + ports_correspondence_dict[link["interface_id"]]["ports"].append(element) for key in sorted(ports_correspondence_dict): result["ports_mapping"].append(ports_correspondence_dict[key]) @@ -5728,7 +5856,7 @@ def datacenter_sdn_port_mapping_list(mydb, tenant_id, datacenter_id): return result def datacenter_sdn_port_mapping_delete(mydb, tenant_id, datacenter_id): - return ovim.clear_of_port_mapping(db_filter={"region":datacenter_id}) + return ovim.clear_of_port_mapping(db_filter={"datacenter_id":datacenter_id}) def create_RO_keypair(tenant_id): """ diff --git a/RO/osm_ro/nfvo_db.py b/RO/osm_ro/nfvo_db.py index afd9d152..df4f1613 100644 --- a/RO/osm_ro/nfvo_db.py +++ b/RO/osm_ro/nfvo_db.py @@ -48,7 +48,7 @@ tables_with_createdat_field=["datacenters","instance_nets","instance_scenarios", "instance_actions", "sce_vnffgs", "sce_rsps", "sce_rsp_hops", "sce_classifiers", "sce_classifier_matches", "instance_sfis", "instance_sfs", "instance_classifications", "instance_sfps", "wims", "wim_accounts", "wim_nfvo_tenants", - "wim_port_mappings", "vim_wim_actions", + "wim_port_mappings", "vim_wim_actions", "instance_interfaces", "instance_wim_nets"] @@ -976,7 +976,7 @@ class nfvo_db(db_base.db_base): cmd = "SELECT vim_interface_id, instance_net_id, internal_name,external_name, mac_address,"\ " ii.ip_address as ip_address, vim_info, i.type as type, sdn_port_id, i.uuid"\ " FROM instance_interfaces as ii join interfaces as i on ii.interface_id=i.uuid"\ - " WHERE instance_vm_id='{}' ORDER BY created_at".format(vm['uuid']) + " WHERE instance_vm_id='{}' ORDER BY i.created_at".format(vm['uuid']) self.logger.debug(cmd) self.cur.execute(cmd ) vm['interfaces'] = self.cur.fetchall() @@ -1010,6 +1010,13 @@ class nfvo_db(db_base.db_base): self.cur.execute(cmd) instance_dict['nets'] = self.cur.fetchall() + # instance sdn_nets: + cmd = "SELECT * FROM instance_wim_nets WHERE instance_scenario_id='{}' ORDER BY created_at;".format( + instance_dict['uuid']) + self.logger.debug(cmd) + self.cur.execute(cmd) + instance_dict['sdn_nets'] = self.cur.fetchall() + #instance_sfps cmd = "SELECT uuid,vim_sfp_id,sce_rsp_id,datacenter_id,"\ "datacenter_tenant_id,status,error_msg,vim_info, related"\ diff --git a/RO/osm_ro/openmanod.py b/RO/osm_ro/openmanod.py index cdf451a1..637b1da2 100755 --- a/RO/osm_ro/openmanod.py +++ b/RO/osm_ro/openmanod.py @@ -53,9 +53,9 @@ import osm_ro __author__ = "Alfonso Tierno, Gerardo Garcia, Pablo Montes" __date__ = "$26-aug-2014 11:09:29$" -__version__ = "6.0.3.post5" +__version__ = "6.0.4.post6" version_date = "Oct 2019" -database_version = 39 # expected database schema version +database_version = 40 # expected database schema version global global_config global logger @@ -327,7 +327,7 @@ if __name__ == "__main__": # WIM module wim_persistence = WimPersistence(mydb) - wim_engine = WimEngine(wim_persistence) + wim_engine = WimEngine(wim_persistence, nfvo.plugins) # --- nfvo.start_service(mydb, wim_persistence, wim_engine) diff --git a/RO/osm_ro/osm-ro.service b/RO/osm_ro/osm-ro.service index 22468855..4f4e4cae 100644 --- a/RO/osm_ro/osm-ro.service +++ b/RO/osm_ro/osm-ro.service @@ -1,3 +1,18 @@ +## +# Copyright 2015 Telefonica Investigacion y Desarrollo, S.A.U. +# 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. +## + [Unit] Description=openmano server (OSM RO) After=mysql.service diff --git a/RO/osm_ro/scripts/RO-start.sh b/RO/osm_ro/scripts/RO-start.sh index 1a8750dc..94183e92 100755 --- a/RO/osm_ro/scripts/RO-start.sh +++ b/RO/osm_ro/scripts/RO-start.sh @@ -1,4 +1,18 @@ #!/bin/bash +## +# Copyright 2015 Telefonica Investigacion y Desarrollo, S.A.U. +# 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 script is intended for launching RO from a docker container. # It waits for mysql server ready, normally running on a separate container, ... diff --git a/RO/osm_ro/sdn.py b/RO/osm_ro/sdn.py new file mode 100755 index 00000000..91cc9b2c --- /dev/null +++ b/RO/osm_ro/sdn.py @@ -0,0 +1,329 @@ +# -*- coding: utf-8 -*- + +## +# Copyright 2019 Telefonica Investigacion y Desarrollo, S.A.U. +# All Rights Reserved. +# +# 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 is the thread for the http server North API. +Two thread will be launched, with normal and administrative permissions. +""" +import yaml +from uuid import uuid4 +from http import HTTPStatus + +__author__ = "Alfonso Tierno" +__date__ = "2019-10-22" +__version__ = "0.1" +version_date = "Oct 2019" + + +class SdnException(Exception): + def __init__(self, message, http_code=HTTPStatus.BAD_REQUEST.value): + self.http_code = http_code + Exception.__init__(self, message) + + +class Sdn(): + running_info = {} # TODO OVIM move the info of running threads from config_dic to this static variable + of_module = {} + + def __init__(self, db, plugins): + self.db = db + self.plugins = plugins + + def start_service(self): + pass # TODO py3 needed to load wims and plugins + + def stop_service(self): + pass # nothing needed + + def show_network(self, uuid): + pass + + def delete_network(self, uuid): + pass + + def new_network(self, network): + pass + + def get_openflow_rules(self, network_id=None): + """ + Get openflow id from DB + :param network_id: Network id, if none all networks will be retrieved + :return: Return a list with Openflow rules per net + """ + # ignore input data + if not network_id: + + where_ = {} + else: + where_ = {"net_id": network_id} + result, content = self.db.get_table( + SELECT=("name", "net_id", "ofc_id", "priority", "vlan_id", "ingress_port", "src_mac", "dst_mac", "actions"), + WHERE=where_, FROM='of_flows') + + if result < 0: + raise SdnException(str(content), -result) + return content + + def edit_openflow_rules(self, network_id=None): + + """ + To make actions over the net. The action is to reinstall the openflow rules + network_id can be 'all' + :param network_id: Network id, if none all networks will be retrieved + :return : Number of nets updated + """ + + # ignore input data + if not network_id: + where_ = {} + else: + where_ = {"uuid": network_id} + result, content = self.db.get_table(SELECT=("uuid", "type"), WHERE=where_, FROM='nets') + + if result < 0: + raise SdnException(str(content), -result) + + for net in content: + if net["type"] != "ptp" and net["type"] != "data": + result -= 1 + continue + + try: + self.net_update_ofc_thread(net['uuid']) + except SdnException as e: + raise SdnException("Error updating network'{}' {}".format(net['uuid'], e), + HTTPStatus.INTERNAL_SERVER_ERROR.value) + except Exception as e: + raise SdnException("Error updating network '{}' {}".format(net['uuid'], e), + HTTPStatus.INTERNAL_SERVER_ERROR.value) + + return result + + def delete_openflow_rules(self, ofc_id=None): + """ + To make actions over the net. The action is to delete ALL openflow rules + :return: return operation result + """ + + if not ofc_id: + if 'Default' in self.config['ofcs_thread']: + r, c = self.config['ofcs_thread']['Default'].insert_task("clear-all") + else: + raise SdnException("Default Openflow controller not not running", HTTPStatus.NOT_FOUND.value) + + elif ofc_id in self.config['ofcs_thread']: + r, c = self.config['ofcs_thread'][ofc_id].insert_task("clear-all") + + # ignore input data + if r < 0: + raise SdnException(str(c), -r) + else: + raise SdnException("Openflow controller not found with ofc_id={}".format(ofc_id), + HTTPStatus.NOT_FOUND.value) + return r + + def get_openflow_ports(self, ofc_id=None): + """ + Obtain switch ports names of openflow controller + :return: Return flow ports in DB + """ + if not ofc_id: + if 'Default' in self.config['ofcs_thread']: + conn = self.config['ofcs_thread']['Default'].OF_connector + else: + raise SdnException("Default Openflow controller not not running", HTTPStatus.NOT_FOUND.value) + + elif ofc_id in self.config['ofcs_thread']: + conn = self.config['ofcs_thread'][ofc_id].OF_connector + else: + raise SdnException("Openflow controller not found with ofc_id={}".format(ofc_id), + HTTPStatus.NOT_FOUND.value) + return conn.pp2ofi + + def new_of_controller(self, ofc_data): + """ + Create a new openflow controller into DB + :param ofc_data: Dict openflow controller data + :return: openflow controller dpid + """ + db_wim = { + "uuid": str(uuid4()), + "name": ofc_data["name"], + "description": "", + "type": ofc_data["type"], + "wim_url": "{}:{}".format(ofc_data["ip"], ofc_data["port"]), + } + db_wim_account = { + "uuid": str(uuid4()), + "name": ofc_data["name"], + "wim_id": db_wim["uuid"], + "sdn": "true", + "user": ofc_data.get("user"), + "password": ofc_data.get("password"), + "config": yaml.safe_dump({"dpid": ofc_data["dpid"], "version": ofc_data.get("version")}, + default_flow_style=True, width=256) + } + db_tables = [ + {"wims": db_wim}, + {"wim_accounts": db_wim_account}, + ] + uuid_list = [db_wim["uuid"], db_wim_account["uuid"]] + self.db.new_rows(db_tables, uuid_list) + return db_wim_account["uuid"] + + def edit_of_controller(self, of_id, ofc_data): + """ + Edit an openflow controller entry from DB + :return: + """ + if not ofc_data: + raise SdnException("No data received during uptade OF contorller", + http_code=HTTPStatus.INTERNAL_SERVER_ERROR.value) + + old_of_controller = self.show_of_controller(of_id) + + if old_of_controller: + result, content = self.db.update_rows('ofcs', ofc_data, WHERE={'uuid': of_id}, log=False) + if result >= 0: + return ofc_data + else: + raise SdnException("Error uptating OF contorller with uuid {}".format(of_id), + http_code=-result) + else: + raise SdnException("Error uptating OF contorller with uuid {}".format(of_id), + http_code=HTTPStatus.INTERNAL_SERVER_ERROR.value) + + def delete_of_controller(self, of_id): + """ + Delete an openflow controller from DB. + :param of_id: openflow controller dpid + :return: + """ + wim_accounts = self.db.get_rows(FROM='wim_accounts', WHERE={"uuid": of_id, "sdn": "true"}) + if not wim_accounts: + raise SdnException("Cannot find sdn controller with id='{}'".format(of_id), + http_code=HTTPStatus.NOT_FOUND.value) + elif len(wim_accounts) > 1: + raise SdnException("Found more than one sdn controller with id='{}'".format(of_id), + http_code=HTTPStatus.CONFLICT.value) + self.db.delete_row(FROM='wim_accounts', WHERE={"uuid": of_id}) + self.db.delete_row(FROM='wims', WHERE={"uuid": wim_accounts[0]["wim_id"]}) + return of_id + + def _format_of_controller(self, wim_account, wim=None): + of_data = {x: wim_account[x] for x in ("uuid", "name", "user")} + if isinstance(wim_account["config"], str): + config = yaml.load(wim_account["config"], Loader=yaml.Loader) + of_data["dpid"] = config.get("dpid") + of_data["version"] = config.get("version") + if wim: + ip, port = wim["wim_url"].split(":") + of_data["ip"] = ip + of_data["port"] = port + of_data["type"] = wim["type"] + return of_data + + def show_of_controller(self, of_id): + """ + Show an openflow controller by dpid from DB. + :param db_filter: List with where query parameters + :return: + """ + wim_accounts = self.db.get_rows(FROM='wim_accounts', WHERE={"uuid": of_id, "sdn": "true"}) + if not wim_accounts: + raise SdnException("Cannot find sdn controller with id='{}'".format(of_id), + http_code=HTTPStatus.NOT_FOUND.value) + elif len(wim_accounts) > 1: + raise SdnException("Found more than one sdn controller with id='{}'".format(of_id), + http_code=HTTPStatus.CONFLICT.value) + wims = self.db.get_rows(FROM='wims', WHERE={"uuid": wim_accounts[0]["wim_id"]}) + return self._format_of_controller(wim_accounts[0], wims[0]) + + def get_of_controllers(self, filter=None): + """ + Show an openflow controllers from DB. + :return: + """ + filter = filter or {} + filter["sdn"] = "true" + wim_accounts = self.db.get_rows(FROM='wim_accounts', WHERE=filter) + return [self._format_of_controller(w) for w in wim_accounts] + + def set_of_port_mapping(self, maps, sdn_id, switch_dpid, vim_id): + """ + Create new port mapping entry + :param of_maps: List with port mapping information + # maps =[{"ofc_id": ,"region": datacenter region,"compute_node": compute uuid,"pci": pci adress, + "switch_dpid": swith dpid,"switch_port": port name,"switch_mac": mac}] + :param sdn_id: ofc id + :param switch_dpid: switch dpid + :param vim_id: datacenter + :return: + """ + # get wim from wim_account + wim_accounts = self.db.get_rows(FROM='wim_accounts', WHERE={"uuid": sdn_id}) + if not wim_accounts: + raise SdnException("Not found sdn id={}".format(sdn_id), http_code=HTTPStatus.NOT_FOUND.value) + wim_id = wim_accounts[0]["wim_id"] + db_wim_port_mappings = [] + for map in maps: + new_map = { + 'wim_id': wim_id, + 'switch_dpid': switch_dpid, + "switch_port": map.get("switch_port"), + 'datacenter_id': vim_id, + "device_id": map.get("compute_node"), + "service_endpoint_id": switch_dpid + "-" + str(uuid4()) + } + if map.get("pci"): + new_map["device_interface_id"] = map["pci"].lower() + config = {} + if map.get("switch_mac"): + config["switch_mac"] = map["switch_mac"] + if config: + new_map["service_mapping_info"] = yaml.safe_dump(config, default_flow_style=True, width=256) + db_wim_port_mappings.append(new_map) + + db_tables = [ + {"wim_port_mappings": db_wim_port_mappings}, + ] + self.db.new_rows(db_tables, []) + return db_wim_port_mappings + + def clear_of_port_mapping(self, db_filter=None): + """ + Clear port mapping filtering using db_filter dict + :param db_filter: Parameter to filter during remove process + :return: + """ + return self.db.delete_row(FROM='wim_port_mappings', WHERE=db_filter) + + def get_of_port_mappings(self, db_filter=None): + """ + Retrive port mapping from DB + :param db_filter: + :return: + """ + maps = self.db.get_rows(WHERE=db_filter, FROM='wim_port_mappings') + for map in maps: + if map.get("service_mapping_info"): + map["service_mapping_info"] = yaml.load(map["service_mapping_info"], Loader=yaml.Loader) + else: + map["service_mapping_info"] = {} + return maps diff --git a/RO/osm_ro/tests/__init__.py b/RO/osm_ro/tests/__init__.py index e69de29b..7284a2be 100644 --- a/RO/osm_ro/tests/__init__.py +++ b/RO/osm_ro/tests/__init__.py @@ -0,0 +1,13 @@ +## +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +## diff --git a/RO/osm_ro/tests/test_db.py b/RO/osm_ro/tests/test_db.py index 5e90bd97..e381116b 100644 --- a/RO/osm_ro/tests/test_db.py +++ b/RO/osm_ro/tests/test_db.py @@ -1,4 +1,18 @@ # -*- 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. +## + # pylint: disable=E1101 import unittest diff --git a/RO/osm_ro/tests/test_utils.py b/RO/osm_ro/tests/test_utils.py index 9fd71cf0..c62c954c 100644 --- a/RO/osm_ro/tests/test_utils.py +++ b/RO/osm_ro/tests/test_utils.py @@ -1,4 +1,18 @@ # -*- 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. +## + # pylint: disable=E1101 import unittest diff --git a/RO/osm_ro/vim_thread.py b/RO/osm_ro/vim_thread.py index 1e1e6d20..728b659b 100644 --- a/RO/osm_ro/vim_thread.py +++ b/RO/osm_ro/vim_thread.py @@ -81,12 +81,10 @@ import time import queue import logging from osm_ro import vimconn +from osm_ro.wim.sdnconn import SdnConnectorError import yaml from osm_ro.db_base import db_base_Exception -# TODO py3 BEGIN -class ovimException(Exception): - pass -# TODO py3 END +from http import HTTPStatus from copy import deepcopy __author__ = "Alfonso Tierno, Pablo Montes" @@ -111,8 +109,7 @@ class vim_thread(threading.Thread): REFRESH_ERROR = 600 REFRESH_DELETE = 3600 * 10 - def __init__(self, task_lock, plugins, name=None, datacenter_name=None, datacenter_tenant_id=None, - db=None, db_lock=None, ovim=None): + def __init__(self, task_lock, plugins, name=None, wim_account_id=None, datacenter_tenant_id=None, db=None): """Init a thread. Arguments: 'id' number of thead @@ -122,60 +119,114 @@ class vim_thread(threading.Thread): """ threading.Thread.__init__(self) self.plugins = plugins + self.plugin_name = "unknown" self.vim = None + self.sdnconnector = None + self.sdnconn_config = None self.error_status = None - self.datacenter_name = datacenter_name + self.wim_account_id = wim_account_id self.datacenter_tenant_id = datacenter_tenant_id - self.ovim = ovim + self.port_mapping = None + if self.wim_account_id: + self.target_k = "wim_account_id" + self.target_v = self.wim_account_id + else: + self.target_k = "datacenter_vim_id" + self.target_v = self.datacenter_tenant_id if not name: - self.name = vimconn["id"] + "." + vimconn["config"]["datacenter_tenant_id"] + self.name = wim_account_id or str(datacenter_tenant_id) else: self.name = name self.vim_persistent_info = {} self.my_id = self.name[:64] - self.logger = logging.getLogger('openmano.vim.' + self.name) + self.logger = logging.getLogger('openmano.{}.{}'.format("vim" if self.datacenter_tenant_id else "sdn", + self.name)) self.db = db - self.db_lock = db_lock self.task_lock = task_lock self.task_queue = queue.Queue(2000) - def get_vimconnector(self): - try: - from_ = "datacenter_tenants as dt join datacenters as d on dt.datacenter_id=d.uuid" - select_ = ('type', 'd.config as config', 'd.uuid as datacenter_id', 'vim_url', 'vim_url_admin', - 'd.name as datacenter_name', 'dt.uuid as datacenter_tenant_id', - 'dt.vim_tenant_name as vim_tenant_name', 'dt.vim_tenant_id as vim_tenant_id', - 'user', 'passwd', 'dt.config as dt_config') - where_ = {"dt.uuid": self.datacenter_tenant_id} - vims = self.db.get_rows(FROM=from_, SELECT=select_, WHERE=where_) - vim = vims[0] - vim_config = {} - if vim["config"]: - vim_config.update(yaml.load(vim["config"], Loader=yaml.Loader)) - if vim["dt_config"]: - vim_config.update(yaml.load(vim["dt_config"], Loader=yaml.Loader)) - vim_config['datacenter_tenant_id'] = vim.get('datacenter_tenant_id') - vim_config['datacenter_id'] = vim.get('datacenter_id') - - # get port_mapping - with self.db_lock: - vim_config["wim_external_ports"] = self.ovim.get_of_port_mappings( - db_filter={"region": vim_config['datacenter_id'], "pci": None}) - - self.vim = self.plugins["rovim_" + vim["type"]].vimconnector( - uuid=vim['datacenter_id'], name=vim['datacenter_name'], - tenant_id=vim['vim_tenant_id'], tenant_name=vim['vim_tenant_name'], - url=vim['vim_url'], url_admin=vim['vim_url_admin'], - user=vim['user'], passwd=vim['passwd'], - config=vim_config, persistent_info=self.vim_persistent_info - ) - self.error_status = None - except Exception as e: - self.logger.error("Cannot load vimconnector for vim_account {}: {}".format(self.datacenter_tenant_id, e)) - self.vim = None - self.error_status = "Error loading vimconnector: {}".format(e) + def _proccess_sdn_exception(self, exc): + if isinstance(exc, SdnConnectorError): + raise + else: + self.logger.error("plugin={} throws a non SdnConnectorError exception {}".format(self.plugin_name, exc), + exc_info=True) + raise SdnConnectorError(str(exc), http_code=HTTPStatus.INTERNAL_SERVER_ERROR.value) from exc + + def _proccess_vim_exception(self, exc): + if isinstance(exc, vimconn.vimconnException): + raise + else: + self.logger.error("plugin={} throws a non vimconnException exception {}".format(self.plugin_name, exc), + exc_info=True) + raise vimconn.vimconnException(str(exc), http_code=HTTPStatus.INTERNAL_SERVER_ERROR.value) from exc + + def get_vim_sdn_connector(self): + if self.datacenter_tenant_id: + try: + from_ = "datacenter_tenants as dt join datacenters as d on dt.datacenter_id=d.uuid" + select_ = ('type', 'd.config as config', 'd.uuid as datacenter_id', 'vim_url', 'vim_url_admin', + 'd.name as datacenter_name', 'dt.uuid as datacenter_tenant_id', + 'dt.vim_tenant_name as vim_tenant_name', 'dt.vim_tenant_id as vim_tenant_id', + 'user', 'passwd', 'dt.config as dt_config') + where_ = {"dt.uuid": self.datacenter_tenant_id} + vims = self.db.get_rows(FROM=from_, SELECT=select_, WHERE=where_) + vim = vims[0] + vim_config = {} + if vim["config"]: + vim_config.update(yaml.load(vim["config"], Loader=yaml.Loader)) + if vim["dt_config"]: + vim_config.update(yaml.load(vim["dt_config"], Loader=yaml.Loader)) + vim_config['datacenter_tenant_id'] = vim.get('datacenter_tenant_id') + vim_config['datacenter_id'] = vim.get('datacenter_id') + + # get port_mapping + # vim_port_mappings = self.ovim.get_of_port_mappings( + # db_filter={"datacenter_id": vim_config['datacenter_id']}) + # vim_config["wim_external_ports"] = [x for x in vim_port_mappings + # if x["service_mapping_info"].get("wim")] + self.plugin_name = "rovim_" + vim["type"] + self.vim = self.plugins[self.plugin_name].vimconnector( + uuid=vim['datacenter_id'], name=vim['datacenter_name'], + tenant_id=vim['vim_tenant_id'], tenant_name=vim['vim_tenant_name'], + url=vim['vim_url'], url_admin=vim['vim_url_admin'], + user=vim['user'], passwd=vim['passwd'], + config=vim_config, persistent_info=self.vim_persistent_info + ) + self.error_status = None + self.logger.info("Vim Connector loaded for vim_account={}, plugin={}".format( + self.datacenter_tenant_id, self.plugin_name)) + except Exception as e: + self.logger.error("Cannot load vimconnector for vim_account={} plugin={}: {}".format( + self.datacenter_tenant_id, self.plugin_name, e)) + self.vim = None + self.error_status = "Error loading vimconnector: {}".format(e) + else: + try: + wim_account = self.db.get_rows(FROM="wim_accounts", WHERE={"uuid": self.wim_account_id})[0] + wim = self.db.get_rows(FROM="wims", WHERE={"uuid": wim_account["wim_id"]})[0] + if wim["config"]: + self.sdnconn_config = yaml.load(wim["config"], Loader=yaml.Loader) + else: + self.sdnconn_config = {} + if wim_account["config"]: + self.sdnconn_config.update(yaml.load(wim_account["config"], Loader=yaml.Loader)) + self.port_mappings = self.db.get_rows(FROM="wim_port_mappings", WHERE={"wim_id": wim_account["wim_id"]}) + if self.port_mappings: + self.sdnconn_config["service_endpoint_mapping"] = self.port_mappings + self.plugin_name = "rosdn_" + wim["type"] + self.sdnconnector = self.plugins[self.plugin_name]( + wim, wim_account, config=self.sdnconn_config) + self.error_status = None + self.logger.info("Sdn Connector loaded for wim_account={}, plugin={}".format( + self.wim_account_id, self.plugin_name)) + except Exception as e: + self.logger.error("Cannot load sdn connector for wim_account={}, plugin={}: {}".format( + self.wim_account_id, self.plugin_name, e)) + self.sdnconnector = None + self.error_status = "Error loading sdn connector: {}".format(e) def _get_db_task(self): """ @@ -189,7 +240,7 @@ class vim_thread(threading.Thread): while True: # get 20 (database_limit) entries each time vim_actions = self.db.get_rows(FROM="vim_wim_actions", - WHERE={"datacenter_vim_id": self.datacenter_tenant_id, + WHERE={self.target_k: self.target_v, "status": ['SCHEDULED', 'BUILD', 'DONE'], "worker": [None, self.my_id], "modified_at<=": now }, @@ -206,7 +257,7 @@ class vim_thread(threading.Thread): task_related = task["related"] # lock ... self.db.update_rows("vim_wim_actions", UPDATE={"worker": self.my_id}, modified_time=0, - WHERE={"datacenter_vim_id": self.datacenter_tenant_id, + WHERE={self.target_k: self.target_v, "status": ['SCHEDULED', 'BUILD', 'DONE', 'FAILED'], "worker": [None, self.my_id], "related": task_related, @@ -214,7 +265,7 @@ class vim_thread(threading.Thread): }) # ... and read all related and check if locked related_tasks = self.db.get_rows(FROM="vim_wim_actions", - WHERE={"datacenter_vim_id": self.datacenter_tenant_id, + WHERE={self.target_k: self.target_v, "status": ['SCHEDULED', 'BUILD', 'DONE', 'FAILED'], "related": task_related, "item": task["item"], @@ -235,7 +286,7 @@ class vim_thread(threading.Thread): if some_tasks_not_locked: if some_tasks_locked: # unlock self.db.update_rows("vim_wim_actions", UPDATE={"worker": None}, modified_time=0, - WHERE={"datacenter_vim_id": self.datacenter_tenant_id, + WHERE={self.target_k: self.target_v, "worker": self.my_id, "related": task_related, "item": task["item"], @@ -285,7 +336,7 @@ class vim_thread(threading.Thread): try: # get all related tasks related_tasks = self.db.get_rows(FROM="vim_wim_actions", - WHERE={"datacenter_vim_id": self.datacenter_tenant_id, + WHERE={self.target_k: self.target_v, "status": ['SCHEDULED', 'BUILD', 'DONE', 'FAILED'], "action": ["FIND", "CREATE"], "related": task["related"], @@ -308,7 +359,7 @@ class vim_thread(threading.Thread): # mark task_create as FINISHED self.db.update_rows("vim_wim_actions", UPDATE={"status": "FINISHED"}, - WHERE={"datacenter_vim_id": self.datacenter_tenant_id, + WHERE={self.target_k: self.target_v, "instance_action_id": task_create["instance_action_id"], "task_index": task_create["task_index"] }) @@ -324,7 +375,7 @@ class vim_thread(threading.Thread): UPDATE={"extra": yaml.safe_dump(extra_new_created, default_flow_style=True, width=256), "vim_id": task_create.get("vim_id")}, - WHERE={"datacenter_vim_id": self.datacenter_tenant_id, + WHERE={self.target_k: self.target_v, "instance_action_id": dependency_task["instance_action_id"], "task_index": dependency_task["task_index"] }) @@ -366,40 +417,38 @@ class vim_thread(threading.Thread): task_vim_interface = task_interface.get("vim_info") if task_vim_interface != interface: # delete old port - if task_interface.get("sdn_port_id"): - try: - with self.db_lock: - self.ovim.delete_port(task_interface["sdn_port_id"], idempotent=True) - task_interface["sdn_port_id"] = None - except ovimException as e: - error_text = "ovimException deleting external_port={}: {}".format( - task_interface["sdn_port_id"], e) - self.logger.error("task={} get-VM: {}".format(task_id, error_text), exc_info=True) - task_warning_msg += error_text - # TODO Set error_msg at instance_nets instead of instance VMs + # if task_interface.get("sdn_port_id"): + # try: + # self.ovim.delete_port(task_interface["sdn_port_id"], idempotent=True) + # task_interface["sdn_port_id"] = None + # except ovimException as e: + # error_text = "ovimException deleting external_port={}: {}".format( + # task_interface["sdn_port_id"], e) + # self.logger.error("task={} get-VM: {}".format(task_id, error_text), exc_info=True) + # task_warning_msg += error_text + # # TODO Set error_msg at instance_nets instead of instance VMs # Create SDN port - sdn_net_id = task_interface.get("sdn_net_id") - if sdn_net_id and interface.get("compute_node") and interface.get("pci"): - sdn_port_name = sdn_net_id + "." + task["vim_id"] - sdn_port_name = sdn_port_name[:63] - try: - with self.db_lock: - sdn_port_id = self.ovim.new_external_port( - {"compute_node": interface["compute_node"], - "pci": interface["pci"], - "vlan": interface.get("vlan"), - "net_id": sdn_net_id, - "region": self.vim["config"]["datacenter_id"], - "name": sdn_port_name, - "mac": interface.get("mac_address")}) - task_interface["sdn_port_id"] = sdn_port_id - except (ovimException, Exception) as e: - error_text = "ovimException creating new_external_port compute_node={} pci={} vlan={} {}".\ - format(interface["compute_node"], interface["pci"], interface.get("vlan"), e) - self.logger.error("task={} get-VM: {}".format(task_id, error_text), exc_info=True) - task_warning_msg += error_text - # TODO Set error_msg at instance_nets instead of instance VMs + # sdn_net_id = task_interface.get("sdn_net_id") + # if sdn_net_id and interface.get("compute_node") and interface.get("pci"): + # sdn_port_name = sdn_net_id + "." + task["vim_id"] + # sdn_port_name = sdn_port_name[:63] + # try: + # sdn_port_id = self.ovim.new_external_port( + # {"compute_node": interface["compute_node"], + # "pci": interface["pci"], + # "vlan": interface.get("vlan"), + # "net_id": sdn_net_id, + # "region": self.vim["config"]["datacenter_id"], + # "name": sdn_port_name, + # "mac": interface.get("mac_address")}) + # task_interface["sdn_port_id"] = sdn_port_id + # except (ovimException, Exception) as e: + # error_text = "ovimException creating new_external_port compute_node={} pci={} vlan={} {}".\ + # format(interface["compute_node"], interface["pci"], interface.get("vlan"), e) + # self.logger.error("task={} get-VM: {}".format(task_id, error_text), exc_info=True) + # task_warning_msg += error_text + # # TODO Set error_msg at instance_nets instead of instance VMs self.db.update_rows('instance_interfaces', UPDATE={"mac_address": interface.get("mac_address"), @@ -412,6 +461,8 @@ class vim_thread(threading.Thread): "vlan": interface.get("vlan")}, WHERE={'uuid': task_interface["iface_id"]}) task_interface["vim_info"] = interface + # if sdn_net_id and interface.get("compute_node") and interface.get("pci"): + # # TODO Send message to task SDN to update # check and update task and instance_vms database vim_info_error_msg = None @@ -455,30 +506,29 @@ class vim_thread(threading.Thread): task_vim_info = task["extra"].get("vim_info") task_vim_status = task["extra"].get("vim_status") task_error_msg = task.get("error_msg") - task_sdn_net_id = task["extra"].get("sdn_net_id") + # task_sdn_net_id = task["extra"].get("sdn_net_id") vim_info_status = vim_info["status"] vim_info_error_msg = vim_info.get("error_msg") # get ovim status - if task_sdn_net_id: - try: - with self.db_lock: - sdn_net = self.ovim.show_network(task_sdn_net_id) - except (ovimException, Exception) as e: - text_error = "ovimException getting network snd_net_id={}: {}".format(task_sdn_net_id, e) - self.logger.error("task={} get-net: {}".format(task_id, text_error), exc_info=True) - sdn_net = {"status": "ERROR", "last_error": text_error} - if sdn_net["status"] == "ERROR": - if not vim_info_error_msg: - vim_info_error_msg = str(sdn_net.get("last_error")) - else: - vim_info_error_msg = "VIM_ERROR: {} && SDN_ERROR: {}".format( - self._format_vim_error_msg(vim_info_error_msg, 1024 // 2 - 14), - self._format_vim_error_msg(sdn_net["last_error"], 1024 // 2 - 14)) - vim_info_status = "ERROR" - elif sdn_net["status"] == "BUILD": - if vim_info_status == "ACTIVE": - vim_info_status = "BUILD" + # if task_sdn_net_id: + # try: + # sdn_net = self.ovim.show_network(task_sdn_net_id) + # except (ovimException, Exception) as e: + # text_error = "ovimException getting network snd_net_id={}: {}".format(task_sdn_net_id, e) + # self.logger.error("task={} get-net: {}".format(task_id, text_error), exc_info=True) + # sdn_net = {"status": "ERROR", "last_error": text_error} + # if sdn_net["status"] == "ERROR": + # if not vim_info_error_msg: + # vim_info_error_msg = str(sdn_net.get("last_error")) + # else: + # vim_info_error_msg = "VIM_ERROR: {} && SDN_ERROR: {}".format( + # self._format_vim_error_msg(vim_info_error_msg, 1024 // 2 - 14), + # self._format_vim_error_msg(sdn_net["last_error"], 1024 // 2 - 14)) + # vim_info_status = "ERROR" + # elif sdn_net["status"] == "BUILD": + # if vim_info_status == "ACTIVE": + # vim_info_status = "BUILD" # update database if vim_info_error_msg: @@ -530,7 +580,7 @@ class vim_thread(threading.Thread): # Move this task to the time dependency is going to be modified plus 10 seconds. self.db.update_rows("vim_wim_actions", modified_time=dependency_modified_at + 10, UPDATE={"worker": None}, - WHERE={"datacenter_vim_id": self.datacenter_tenant_id, "worker": self.my_id, + WHERE={self.target_k: self.target_v, "worker": self.my_id, "related": task["related"], }) # task["extra"]["tries"] = task["extra"].get("tries", 0) + 1 @@ -553,7 +603,7 @@ class vim_thread(threading.Thread): if task["status"] == "SUPERSEDED": # not needed to do anything but update database with the new status database_update = None - elif not self.vim: + elif not self.vim and not self.sdnconnector: task["status"] = "FAILED" task["error_msg"] = self.error_status database_update = {"status": "VIM_ERROR", "error_msg": task["error_msg"]} @@ -595,6 +645,19 @@ class vim_thread(threading.Thread): database_update = self.get_net(task) else: raise vimconn.vimconnException(self.name + "unknown task action {}".format(task["action"])) + elif task["item"] == 'instance_wim_nets': + if task["status"] in ('BUILD', 'DONE') and task["action"] in ("FIND", "CREATE"): + database_update = self.new_or_update_sdn_net(task) + create_or_find = True + elif task["action"] == "CREATE": + create_or_find = True + database_update = self.new_or_update_sdn_net(task) + elif task["action"] == "DELETE": + self.del_sdn_net(task) + elif task["action"] == "FIND": + database_update = self.get_sdn_net(task) + else: + raise vimconn.vimconnException(self.name + "unknown task action {}".format(task["action"])) elif task["item"] == 'instance_sfis': if task["action"] == "CREATE": create_or_find = True @@ -666,7 +729,7 @@ class vim_thread(threading.Thread): "error_msg": task["error_msg"], }, - WHERE={"datacenter_vim_id": self.datacenter_tenant_id, + WHERE={self.target_k: self.target_v, "worker": self.my_id, "action": ["FIND", "CREATE"], "related": task["related"], @@ -683,7 +746,7 @@ class vim_thread(threading.Thread): self.db.update_rows( table="vim_wim_actions", modified_time=0, UPDATE={"worker": None}, - WHERE={"datacenter_vim_id": self.datacenter_tenant_id, + WHERE={self.target_k: self.target_v, "worker": self.my_id, "related": task["related"], }) @@ -723,7 +786,7 @@ class vim_thread(threading.Thread): def run(self): self.logger.debug("Starting") while True: - self.get_vimconnector() + self.get_vim_sdn_connector() self.logger.debug("Vimconnector loaded") reload_thread = False @@ -848,19 +911,18 @@ class vim_thread(threading.Thread): return instance_element_update def del_vm(self, task): - task_id = task["instance_action_id"] + "." + str(task["task_index"]) + # task_id = task["instance_action_id"] + "." + str(task["task_index"]) vm_vim_id = task["vim_id"] - interfaces = task["extra"].get("interfaces", ()) + # interfaces = task["extra"].get("interfaces", ()) try: - for iface in interfaces.values(): - if iface.get("sdn_port_id"): - try: - with self.db_lock: - self.ovim.delete_port(iface["sdn_port_id"], idempotent=True) - except ovimException as e: - self.logger.error("task={} del-VM: ovimException when deleting external_port={}: {} ".format( - task_id, iface["sdn_port_id"], e), exc_info=True) - # TODO Set error_msg at instance_nets + # for iface in interfaces.values(): + # if iface.get("sdn_port_id"): + # try: + # self.ovim.delete_port(iface["sdn_port_id"], idempotent=True) + # except ovimException as e: + # self.logger.error("task={} del-VM: ovimException when deleting external_port={}: {} ".format( + # task_id, iface["sdn_port_id"], e), exc_info=True) + # # TODO Set error_msg at instance_nets self.vim.delete_vminstance(vm_vim_id, task["extra"].get("created_items")) task["status"] = "FINISHED" # with FINISHED instead of DONE it will not be refreshing @@ -929,7 +991,6 @@ class vim_thread(threading.Thread): def new_net(self, task): vim_net_id = None - sdn_net_id = None task_id = task["instance_action_id"] + "." + str(task["task_index"]) action_text = "" try: @@ -947,89 +1008,70 @@ class vim_thread(threading.Thread): action_text = "creating VIM" vim_net_id, created_items = self.vim.new_network(*params[0:3]) - net_name = params[0] - net_type = params[1] - wim_account_name = None - if len(params) >= 4: - wim_account_name = params[3] - - sdn_controller = self.vim.config.get('sdn-controller') - if sdn_controller and (net_type == "data" or net_type == "ptp"): - network = {"name": net_name, "type": net_type, "region": self.vim["config"]["datacenter_id"]} - - vim_net = self.vim.get_network(vim_net_id) - if vim_net.get('encapsulation') != 'vlan': - raise vimconn.vimconnException( - "net '{}' defined as type '{}' has not vlan encapsulation '{}'".format( - net_name, net_type, vim_net['encapsulation'])) - network["vlan"] = vim_net.get('segmentation_id') - action_text = "creating SDN" - with self.db_lock: - sdn_net_id = self.ovim.new_network(network) - - if wim_account_name and self.vim.config["wim_external_ports"]: - # add external port to connect WIM. Try with compute node __WIM:wim_name and __WIM - action_text = "attaching external port to ovim network" - sdn_port_name = "external_port" - sdn_port_data = { - "compute_node": "__WIM:" + wim_account_name[0:58], - "pci": None, - "vlan": network["vlan"], - "net_id": sdn_net_id, - "region": self.vim["config"]["datacenter_id"], - "name": sdn_port_name, - } - try: - with self.db_lock: - sdn_external_port_id = self.ovim.new_external_port(sdn_port_data) - except ovimException: - sdn_port_data["compute_node"] = "__WIM" - with self.db_lock: - sdn_external_port_id = self.ovim.new_external_port(sdn_port_data) - self.logger.debug("Added sdn_external_port {} to sdn_network {}".format(sdn_external_port_id, - sdn_net_id)) + # net_name = params[0] + # net_type = params[1] + # wim_account_name = None + # if len(params) >= 4: + # wim_account_name = params[3] + + # TODO fix at nfvo adding external port + # if wim_account_name and self.vim.config["wim_external_ports"]: + # # add external port to connect WIM. Try with compute node __WIM:wim_name and __WIM + # action_text = "attaching external port to ovim network" + # sdn_port_name = "external_port" + # sdn_port_data = { + # "compute_node": "__WIM:" + wim_account_name[0:58], + # "pci": None, + # "vlan": network["vlan"], + # "net_id": sdn_net_id, + # "region": self.vim["config"]["datacenter_id"], + # "name": sdn_port_name, + # } + # try: + # sdn_external_port_id = self.ovim.new_external_port(sdn_port_data) + # except ovimException: + # sdn_port_data["compute_node"] = "__WIM" + # sdn_external_port_id = self.ovim.new_external_port(sdn_port_data) + # self.logger.debug("Added sdn_external_port {} to sdn_network {}".format(sdn_external_port_id, + # sdn_net_id)) task["status"] = "DONE" task["extra"]["vim_info"] = {} - task["extra"]["sdn_net_id"] = sdn_net_id + # task["extra"]["sdn_net_id"] = sdn_net_id task["extra"]["vim_status"] = "BUILD" task["extra"]["created"] = True task["extra"]["created_items"] = created_items task["error_msg"] = None task["vim_id"] = vim_net_id - instance_element_update = {"vim_net_id": vim_net_id, "sdn_net_id": sdn_net_id, "status": "BUILD", + instance_element_update = {"vim_net_id": vim_net_id, "status": "BUILD", "created": True, "error_msg": None} return instance_element_update - except (vimconn.vimconnException, ovimException) as e: + except vimconn.vimconnException as e: self.logger.error("task={} new-net: Error {}: {}".format(task_id, action_text, e)) task["status"] = "FAILED" task["vim_id"] = vim_net_id task["error_msg"] = self._format_vim_error_msg(str(e)) - task["extra"]["sdn_net_id"] = sdn_net_id - instance_element_update = {"vim_net_id": vim_net_id, "sdn_net_id": sdn_net_id, "status": "VIM_ERROR", + # task["extra"]["sdn_net_id"] = sdn_net_id + instance_element_update = {"vim_net_id": vim_net_id, "status": "VIM_ERROR", "error_msg": task["error_msg"]} return instance_element_update def del_net(self, task): net_vim_id = task["vim_id"] - sdn_net_id = task["extra"].get("sdn_net_id") + # sdn_net_id = task["extra"].get("sdn_net_id") try: if net_vim_id: self.vim.delete_network(net_vim_id, task["extra"].get("created_items")) - if sdn_net_id: - # Delete any attached port to this sdn network. There can be ports associated to this network in case - # it was manually done using 'openmano vim-net-sdn-attach' - with self.db_lock: - port_list = self.ovim.get_ports(columns={'uuid'}, - filter={'name': 'external_port', 'net_id': sdn_net_id}) - for port in port_list: - self.ovim.delete_port(port['uuid'], idempotent=True) - self.ovim.delete_network(sdn_net_id, idempotent=True) + # if sdn_net_id: + # # Delete any attached port to this sdn network. There can be ports associated to this network in case + # # it was manually done using 'openmano vim-net-sdn-attach' + # port_list = self.ovim.get_ports(columns={'uuid'}, + # filter={'name': 'external_port', 'net_id': sdn_net_id}) + # for port in port_list: + # self.ovim.delete_port(port['uuid'], idempotent=True) + # self.ovim.delete_network(sdn_net_id, idempotent=True) task["status"] = "FINISHED" # with FINISHED instead of DONE it will not be refreshing task["error_msg"] = None return None - except ovimException as e: - task["error_msg"] = self._format_vim_error_msg("ovimException obtaining and deleting external " - "ports for net {}: {}".format(sdn_net_id, str(e))) except vimconn.vimconnException as e: task["error_msg"] = self._format_vim_error_msg(str(e)) if isinstance(e, vimconn.vimconnNotFoundException): @@ -1039,6 +1081,160 @@ class vim_thread(threading.Thread): task["status"] = "FAILED" return None + def new_or_update_sdn_net(self, task): + wimconn_net_id = task["vim_id"] + created_items = task["extra"].get("created_items") + connected_ports = task["extra"].get("connected_ports", []) + new_connected_ports = [] + last_update = task["extra"].get("last_update", 0) + sdn_status = "BUILD" + sdn_info = None + + task_id = task["instance_action_id"] + "." + str(task["task_index"]) + error_list = [] + try: + # FIND + if task["extra"].get("find"): + wimconn_id = task["extra"]["find"][0] + try: + instance_element_update = self.sdnconnector.get_connectivity_service_status(wimconn_id) + wimconn_net_id = wimconn_id + instance_element_update = {"wim_internal_id": wimconn_net_id, "created": False, "status": "BUILD", + "error_msg": None, } + return instance_element_update + except Exception as e: + if isinstance(e, SdnConnectorError) and e.http_error == HTTPStatus.NOT_FOUND.value: + pass + else: + self._proccess_sdn_exception(e) + + params = task["params"] + # CREATE + # look for ports + sdn_ports = [] + pending_ports = 0 + + ports = self.db.get_rows(FROM='instance_interfaces', WHERE={'instance_wim_net_id': task["item_id"]}) + sdn_need_update = False + for port in ports: + # TODO. Do not connect if already done + if port.get("compute_node") and port.get("pci"): + for map in self.port_mappings: + if map.get("device_id") == port["compute_node"] and \ + map.get("device_interface_id") == port["pci"]: + break + else: + if self.sdnconn_config.get("mapping_not_needed"): + map = { + "service_endpoint_id": "{}:{}".format(port["compute_node"], port["pci"]), + "service_endpoint_encapsulation_info": { + "vlan": port["vlan"], + "mac": port["mac_address"], + "device_id": port["compute_node"], + "device_interface_id": port["pci"] + } + } + else: + map = None + error_list.append("Port mapping not found for compute_node={} pci={}".format( + port["compute_node"], port["pci"])) + + if map: + if port["uuid"] not in connected_ports or port["modified_at"] > last_update: + sdn_need_update = True + new_connected_ports.append(port["uuid"]) + sdn_ports.append({ + "service_endpoint_id": map["service_endpoint_id"], + "service_endpoint_encapsulation_type": "dot1q" if port["model"] == "SR-IOV" else None, + "service_endpoint_encapsulation_info": { + "vlan": port["vlan"], + "mac": port["mac_address"], + "device_id": map.get("device_id"), + "device_interface_id": map.get("device_interface_id"), + "switch_dpid": map.get("switch_dpid"), + "switch_port": map.get("switch_port"), + "service_mapping_info": map.get("service_mapping_info"), + } + }) + + else: + pending_ports += 1 + if pending_ports: + error_list.append("Waiting for getting interfaces location from VIM. Obtained '{}' of {}" + .format(len(ports)-pending_ports, len(ports))) + # if there are more ports to connect or they have been modified, call create/update + if sdn_need_update and len(sdn_ports) >= 2: + if not wimconn_net_id: + if params[0] == "data": + net_type = "ELAN" + elif params[0] == "ptp": + net_type = "ELINE" + else: + net_type = "L3" + + wimconn_net_id, created_items = self.sdnconnector.create_connectivity_service(net_type, sdn_ports) + else: + created_items = self.sdnconnector.edit_connectivity_service(wimconn_net_id, conn_info=created_items, + connection_points=sdn_ports) + last_update = time.time() + connected_ports = new_connected_ports + elif wimconn_net_id: + try: + wim_status_dict = self.sdnconnector.get_connectivity_service_status(wimconn_net_id, + conn_info=created_items) + sdn_status = wim_status_dict["sdn_status"] + if wim_status_dict.get("error_msg"): + error_list.append(wim_status_dict.get("error_msg")) + if wim_status_dict.get("sdn_info"): + sdn_info = str(wim_status_dict.get("sdn_info")) + except Exception as e: + self._proccess_sdn_exception(e) + + task["status"] = "DONE" + task["extra"]["vim_info"] = {} + # task["extra"]["sdn_net_id"] = sdn_net_id + task["extra"]["vim_status"] = "BUILD" + task["extra"]["created"] = True + task["extra"]["created_items"] = created_items + task["extra"]["connected_ports"] = connected_ports + task["extra"]["last_update"] = last_update + task["error_msg"] = self._format_vim_error_msg(" ; ".join(error_list)) + task["vim_id"] = wimconn_net_id + instance_element_update = {"wim_internal_id": wimconn_net_id, "status": sdn_status, + "created": True, "error_msg": task["error_msg"] or None} + except (vimconn.vimconnException, SdnConnectorError) as e: + self.logger.error("task={} new-sdn-net: Error: {}".format(task_id, e)) + task["status"] = "FAILED" + task["vim_id"] = wimconn_net_id + task["error_msg"] = self._format_vim_error_msg(str(e)) + # task["extra"]["sdn_net_id"] = sdn_net_id + instance_element_update = {"wim_internal_id": wimconn_net_id, "status": "WIM_ERROR", + "error_msg": task["error_msg"]} + if sdn_info: + instance_element_update["wim_info"] = sdn_info + return instance_element_update + + def del_sdn_net(self, task): + wimconn_net_id = task["vim_id"] + try: + try: + if wimconn_net_id: + self.sdnconnector.delete_connectivity_service(wimconn_net_id, task["extra"].get("created_items")) + task["status"] = "FINISHED" # with FINISHED instead of DONE it will not be refreshing + task["error_msg"] = None + return None + except Exception as e: + self._proccess_sdn_exception(e) + except SdnConnectorError as e: + task["error_msg"] = self._format_vim_error_msg(str(e)) + if e.http_code == HTTPStatus.NOT_FOUND.value: + # If not found mark as Done and fill error_msg + task["status"] = "FINISHED" # with FINISHED instead of DONE it will not be refreshing + task["error_msg"] = None + return None + task["status"] = "FAILED" + return None + # Service Function Instances def new_sfi(self, task): vim_sfi_id = None diff --git a/RO/osm_ro/wim/__init__.py b/RO/osm_ro/wim/__init__.py index e69de29b..7284a2be 100644 --- a/RO/osm_ro/wim/__init__.py +++ b/RO/osm_ro/wim/__init__.py @@ -0,0 +1,13 @@ +## +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +## diff --git a/RO/osm_ro/wim/engine.py b/RO/osm_ro/wim/engine.py index cf5b85a0..6a232a45 100644 --- a/RO/osm_ro/wim/engine.py +++ b/RO/osm_ro/wim/engine.py @@ -48,7 +48,7 @@ import logging from contextlib import contextmanager from itertools import groupby from operator import itemgetter -from sys import exc_info +# from sys import exc_info from uuid import uuid4 from ..utils import remove_none_items @@ -57,22 +57,33 @@ from .errors import ( DbBaseException, NoWimConnectedToDatacenters, UnexpectedDatabaseError, - WimAccountNotActive + WimAccountNotActive, + UndefinedWimConnector ) from .wim_thread import WimThread +# from ..http_tools.errors import Bad_Request +from pkg_resources import iter_entry_points class WimEngine(object): """Logic supporting the establishment of WAN links when NS spans across different datacenters. """ - def __init__(self, persistence, logger=None, ovim=None): + def __init__(self, persistence, plugins, logger=None, ovim=None): self.persist = persistence + self.plugins = plugins if plugins is not None else {} self.logger = logger or logging.getLogger('openmano.wim.engine') self.threads = {} self.connectors = {} self.ovim = ovim + def _load_plugin(self, name, type="sdn"): + # type can be vim or sdn + for v in iter_entry_points('osm_ro{}.plugins'.format(type), name): + self.plugins[name] = v.load() + if name and name not in self.plugins: + raise UndefinedWimConnector(type, name) + def create_wim(self, properties): """Create a new wim record according to the properties @@ -88,6 +99,10 @@ class WimEngine(object): """ port_mapping = ((properties.get('config', {}) or {}) .pop('wim_port_mapping', {})) + plugin_name = "rosdn_" + properties["type"] + if plugin_name not in self.plugins: + self._load_plugin(plugin_name, type="sdn") + uuid = self.persist.create_wim(properties) if port_mapping: @@ -415,7 +430,7 @@ class WimEngine(object): past""" if instance_scenario_id: wan_links = self.persist.get_wan_links( - instance_scenario_id=instance_scenario_id) + instance_scenario_id=instance_scenario_id, sdn='false') return [self.delete_action(l) for l in wan_links] def incorporate_actions(self, wim_actions, instance_action): @@ -466,7 +481,7 @@ class WimEngine(object): """ thread = None try: - thread = WimThread(self.persist, wim_account, ovim=self.ovim) + thread = WimThread(self.persist, self.plugins, wim_account, ovim=self.ovim) self.threads[wim_account['uuid']] = thread thread.start() except: # noqa @@ -478,8 +493,16 @@ class WimEngine(object): def start_threads(self): """Start the threads responsible for processing WIM Actions""" accounts = self.persist.get_wim_accounts(error_if_none=False) - self.threads = remove_none_items( - {a['uuid']: self._spawn_thread(a) for a in accounts}) + thread_dict = {} + for account in accounts: + try: + plugin_name = "rosdn_" + account["wim"]["type"] + if plugin_name not in self.plugins: + self._load_plugin(plugin_name, type="sdn") + thread_dict[account["uuid"]] = self._spawn_thread(account) + except UndefinedWimConnector as e: + self.logger.error(e) + self.threads = remove_none_items(thread_dict) def stop_threads(self): """Stop the threads responsible for processing WIM Actions""" diff --git a/RO/osm_ro/wim/errors.py b/RO/osm_ro/wim/errors.py index e8d4b63e..8fd0a88c 100644 --- a/RO/osm_ro/wim/errors.py +++ b/RO/osm_ro/wim/errors.py @@ -103,12 +103,10 @@ class UndefinedAction(HttpMappedError): class UndefinedWimConnector(DbBaseException): """The connector class for the specified wim type is not implemented""" - def __init__(self, wim_type, module_name, location_reference): - super(UndefinedWimConnector, self).__init__( - ('{}: `{}`. Could not find module `{}` ' - '(check if it is necessary to install a plugin)' - .format(self.__class__.__doc__, wim_type, module_name)), - http_code=Bad_Request) + def __init__(self, wim_type, module_name): + super(UndefinedWimConnector, self).__init__("Cannot load a module for {t} type '{n}'. The plugin 'osm_{n}' has" + " not been registered".format(t=wim_type, n=module_name), + http_code=Bad_Request) class WimAccountOverwrite(DbBaseException): diff --git a/RO/osm_ro/wim/failing_connector.py b/RO/osm_ro/wim/failing_connector.py index b66551cc..6bbab357 100644 --- a/RO/osm_ro/wim/failing_connector.py +++ b/RO/osm_ro/wim/failing_connector.py @@ -37,7 +37,7 @@ we need a replacement for it, that will throw an error every time we try to execute any action """ import json -from .wimconn import WimConnectorError +from .sdnconn import SdnConnectorError class FailingConnector(object): @@ -51,32 +51,38 @@ class FailingConnector(object): def __init__(self, error_msg): self.error_msg = error_msg + def __call__(self, wim, wim_account, config=None, logger=None): + return self + + def vimconnector(self, *args, **kwargs): + raise Exception(self.error_msg) + def check_credentials(self): - raise WimConnectorError('Impossible to use WIM:\n' + self.error_msg) + raise SdnConnectorError('Impossible to use WIM:\n' + self.error_msg) def get_connectivity_service_status(self, service_uuid, _conn_info=None): - raise WimConnectorError('Impossible to retrieve status for {}\n\n{}' + raise SdnConnectorError('Impossible to retrieve status for {}\n\n{}' .format(service_uuid, self.error_msg)) def create_connectivity_service(self, service_uuid, *args, **kwargs): - raise WimConnectorError('Impossible to connect {}.\n{}\n{}\n{}' + raise SdnConnectorError('Impossible to connect {}.\n{}\n{}\n{}' .format(service_uuid, self.error_msg, json.dumps(args, indent=4), json.dumps(kwargs, indent=4))) def delete_connectivity_service(self, service_uuid, _conn_info=None): - raise WimConnectorError('Impossible to disconnect {}\n\n{}' + raise SdnConnectorError('Impossible to disconnect {}\n\n{}' .format(service_uuid, self.error_msg)) def edit_connectivity_service(self, service_uuid, *args, **kwargs): - raise WimConnectorError('Impossible to change connection {}.\n{}\n' + raise SdnConnectorError('Impossible to change connection {}.\n{}\n' '{}\n{}' .format(service_uuid, self.error_msg, json.dumps(args, indent=4), json.dumps(kwargs, indent=4))) def clear_all_connectivity_services(self): - raise WimConnectorError('Impossible to use WIM:\n' + self.error_msg) + raise SdnConnectorError('Impossible to use WIM:\n' + self.error_msg) def get_all_active_connectivity_services(self): - raise WimConnectorError('Impossible to use WIM:\n' + self.error_msg) + raise SdnConnectorError('Impossible to use WIM:\n' + self.error_msg) diff --git a/RO/osm_ro/wim/openflow_conn.py b/RO/osm_ro/wim/openflow_conn.py new file mode 100644 index 00000000..7d029f7f --- /dev/null +++ b/RO/osm_ro/wim/openflow_conn.py @@ -0,0 +1,464 @@ +## +# Copyright 2019 Telefonica Investigacion y Desarrollo, S.A.U. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +## +import logging +from http import HTTPStatus +from osm_ro.wim.sdnconn import SdnConnectorBase, SdnConnectorError +from uuid import uuid4 + +""" +Implement an Abstract class 'OpenflowConn' and an engine 'SdnConnectorOpenFlow' used for base class for SDN plugings +that implements a pro-active opeflow rules. +""" + +__author__ = "Alfonso Tierno" +__date__ = "2019-11-11" + + +class OpenflowConnException(Exception): + """Common and base class Exception for all vimconnector exceptions""" + def __init__(self, message, http_code=HTTPStatus.BAD_REQUEST.value): + Exception.__init__(self, message) + self.http_code = http_code + + +class OpenflowConnConnectionException(OpenflowConnException): + """Connectivity error with the VIM""" + def __init__(self, message, http_code=HTTPStatus.SERVICE_UNAVAILABLE.value): + OpenflowConnException.__init__(self, message, http_code) + + +class OpenflowConnUnexpectedResponse(OpenflowConnException): + """Get an wrong response from VIM""" + def __init__(self, message, http_code=HTTPStatus.INTERNAL_SERVER_ERROR.value): + OpenflowConnException.__init__(self, message, http_code) + + +class OpenflowConnAuthException(OpenflowConnException): + """Invalid credentials or authorization to perform this action over the VIM""" + def __init__(self, message, http_code=HTTPStatus.UNAUTHORIZED.value): + OpenflowConnException.__init__(self, message, http_code) + + +class OpenflowConnNotFoundException(OpenflowConnException): + """The item is not found at VIM""" + def __init__(self, message, http_code=HTTPStatus.NOT_FOUND.value): + OpenflowConnException.__init__(self, message, http_code) + + +class OpenflowConnConflictException(OpenflowConnException): + """There is a conflict, e.g. more item found than one""" + def __init__(self, message, http_code=HTTPStatus.CONFLICT.value): + OpenflowConnException.__init__(self, message, http_code) + + +class OpenflowConnNotSupportedException(OpenflowConnException): + """The request is not supported by connector""" + def __init__(self, message, http_code=HTTPStatus.SERVICE_UNAVAILABLE.value): + OpenflowConnException.__init__(self, message, http_code) + + +class OpenflowConnNotImplemented(OpenflowConnException): + """The method is not implemented by the connected""" + def __init__(self, message, http_code=HTTPStatus.NOT_IMPLEMENTED.value): + OpenflowConnException.__init__(self, message, http_code) + + +class OpenflowConn: + """ + Openflow controller connector abstract implementeation. + """ + def __init__(self, params): + self.name = "openflow_conector" + self.pp2ofi = {} # From Physical Port to OpenFlow Index + self.ofi2pp = {} # From OpenFlow Index to Physical Port + self.logger = logging.getLogger('openflow_conn') + self.logger.setLevel(getattr(logging, params.get("of_debug", "ERROR"))) + + def get_of_switches(self): + """" + Obtain a a list of switches or DPID detected by this controller + :return: list length, and a list where each element a tuple pair (DPID, IP address), text_error: if fails + """ + raise OpenflowConnNotImplemented("Should have implemented this") + + def obtain_port_correspondence(self): + """ + Obtain the correspondence between physical and openflow port names + :return: dictionary: with physical name as key, openflow name as value, error_text: if fails + """ + raise OpenflowConnNotImplemented("Should have implemented this") + + def get_of_rules(self, translate_of_ports=True): + """ + Obtain the rules inserted at openflow controller + :param translate_of_ports: if True it translates ports from openflow index to physical switch name + :return: list where each item is a dictionary with the following content: + priority: rule priority + priority: rule priority + name: rule name (present also as the master dict key) + ingress_port: match input port of the rule + dst_mac: match destination mac address of the rule, can be missing or None if not apply + vlan_id: match vlan tag of the rule, can be missing or None if not apply + actions: list of actions, composed by a pair tuples: + (vlan, None/int): for stripping/setting a vlan tag + (out, port): send to this port + switch: DPID, all + text_error if fails + """ + raise OpenflowConnNotImplemented("Should have implemented this") + + def del_flow(self, flow_name): + """ + Delete all existing rules + :param flow_name: flow_name, this is the rule name + :return: None if ok, text_error if fails + """ + raise OpenflowConnNotImplemented("Should have implemented this") + + def new_flow(self, data): + """ + Insert a new static rule + :param data: dictionary with the following content: + priority: rule priority + name: rule name + ingress_port: match input port of the rule + dst_mac: match destination mac address of the rule, missing or None if not apply + vlan_id: match vlan tag of the rule, missing or None if not apply + actions: list of actions, composed by a pair tuples with these posibilities: + ('vlan', None/int): for stripping/setting a vlan tag + ('out', port): send to this port + :return: None if ok, text_error if fails + """ + raise OpenflowConnNotImplemented("Should have implemented this") + + def clear_all_flows(self): + """" + Delete all existing rules + :return: None if ok, text_error if fails + """ + raise OpenflowConnNotImplemented("Should have implemented this") + + +class SdnConnectorOpenFlow(SdnConnectorBase): + """ + This class is the base engine of SDN plugins base on openflow rules + """ + flow_fields = ('priority', 'vlan', 'ingress_port', 'actions', 'dst_mac', 'src_mac', 'net_id') + + def __init__(self, wim, wim_account, config=None, logger=None, of_connector=None): + self.logger = logger or logging.getLogger('openmano.sdnconn.openflow') + self.of_connector = of_connector + self.of_controller_nets_with_same_vlan = config.get("of_controller_nets_with_same_vlan", False) + + def check_credentials(self): + try: + self.openflow_conn.obtain_port_correspondence() + except OpenflowConnException as e: + raise SdnConnectorError(e, http_code=e.http_code) + + def get_connectivity_service_status(self, service_uuid, conn_info=None): + conn_info = conn_info or {} + return { + "sdn_status": conn_info.get("status", "ERROR"), + "error_msg": conn_info.get("error_msg", "Variable conn_info not provided"), + } + # TODO check rules connectirng to of_connector + + def create_connectivity_service(self, service_type, connection_points, **kwargs): + net_id = str(uuid4()) + ports = [] + for cp in connection_points: + port = { + "uuid": cp["service_endpoint_id"], + "vlan": cp.get("service_endpoint_encapsulation_info", {}).get("vlan"), + "mac": cp.get("service_endpoint_encapsulation_info", {}).get("mac"), + "switch_port": cp.get("service_endpoint_encapsulation_info", {}).get("switch_port"), + } + ports.append(port) + try: + created_items = self._set_openflow_rules(service_type, net_id, ports, created_items=None) + return net_id, created_items + except (SdnConnectorError, OpenflowConnException) as e: + raise SdnConnectorError(e, http_code=e.http_code) + + def delete_connectivity_service(self, service_uuid, conn_info=None): + try: + service_type = "ELAN" + ports = [] + self._set_openflow_rules(service_type, service_uuid, ports, created_items=conn_info) + return None + except (SdnConnectorError, OpenflowConnException) as e: + raise SdnConnectorError(e, http_code=e.http_code) + + def edit_connectivity_service(self, service_uuid, conn_info=None, connection_points=None, **kwargs): + ports = [] + for cp in connection_points: + port = { + "uuid": cp["service_endpoint_id"], + "vlan": cp.get("service_endpoint_encapsulation_info", {}).get("vlan"), + "mac": cp.get("service_endpoint_encapsulation_info", {}).get("mac"), + "switch_port": cp.get("service_endpoint_encapsulation_info", {}).get("switch_port"), + } + ports.append(port) + service_type = "ELAN" # TODO. Store at conn_info for later use + try: + created_items = self._set_openflow_rules(service_type, service_uuid, ports, created_items=conn_info) + return created_items + except (SdnConnectorError, OpenflowConnException) as e: + raise SdnConnectorError(e, http_code=e.http_code) + + def clear_all_connectivity_services(self): + """Delete all WAN Links corresponding to a WIM""" + pass + + def get_all_active_connectivity_services(self): + """Provide information about all active connections provisioned by a + WIM + """ + pass + + def _set_openflow_rules(self, net_type, net_id, ports, created_items=None): + ifaces_nb = len(ports) + if not created_items: + created_items = {"status": None, "error_msg": None, "installed_rules_ids": []} + rules_to_delete = created_items.get("installed_rules_ids") or [] + new_installed_rules_ids = [] + error_list = [] + + try: + step = "Checking ports and network type compatibility" + if ifaces_nb < 2: + pass + elif net_type == 'ELINE': + if ifaces_nb > 2: + raise SdnConnectorError("'ELINE' type network cannot connect {} interfaces, only 2".format( + ifaces_nb)) + elif net_type == 'ELAN': + if ifaces_nb > 2 and self.of_controller_nets_with_same_vlan: + # check all ports are VLAN (tagged) or none + vlan_tags = [] + for port in ports: + if port["vlan"] not in vlan_tags: + vlan_tags.append(port["vlan"]) + if len(vlan_tags) > 1: + raise SdnConnectorError("This pluging cannot connect ports with diferent VLAN tags when flag " + "'of_controller_nets_with_same_vlan' is active") + else: + raise SdnConnectorError('Only ELINE or ELAN network types are supported for openflow') + + # Get the existing flows at openflow controller + step = "Getting installed openflow rules" + existing_flows = self.of_connector.get_of_rules() + existing_flows_ids = [flow["name"] for flow in existing_flows] + + # calculate new flows to be inserted + step = "Compute needed openflow rules" + new_flows = self._compute_net_flows(net_id, ports) + + name_index = 0 + for flow in new_flows: + # 1 check if an equal flow is already present + index = self._check_flow_already_present(flow, existing_flows) + if index >= 0: + flow_id = existing_flows[index]["name"] + self.logger.debug("Skipping already present flow %s", str(flow)) + else: + # 2 look for a non used name + flow_name = flow["net_id"] + "." + str(name_index) + while flow_name in existing_flows_ids: + name_index += 1 + flow_name = flow["net_id"] + "." + str(name_index) + flow['name'] = flow_name + # 3 insert at openflow + try: + self.of_connector.new_flow(flow) + flow_id = flow["name"] + existing_flows_ids.append(flow_id) + except OpenflowConnException as e: + flow_id = None + error_list.append("Cannot create rule for ingress_port={}, dst_mac={}: {}" + .format(flow["ingress_port"], flow["dst_mac"], e)) + + # 4 insert at database + if flow_id: + new_installed_rules_ids.append(flow_id) + if flow_id in rules_to_delete: + rules_to_delete.remove(flow_id) + + # delete not needed old flows from openflow + for flow_id in rules_to_delete: + # Delete flow + try: + self.of_connector.del_flow(flow_id) + except OpenflowConnNotFoundException: + pass + except OpenflowConnException as e: + error_text = "Cannot remove rule '{}': {}".format(flow['name'], e) + error_list.append(error_text) + self.logger.error(error_text) + created_items["installed_rules_ids"] = new_installed_rules_ids + if error_list: + created_items["error_msg"] = ";".join(error_list)[:1000] + created_items["error_msg"] = "ERROR" + else: + created_items["error_msg"] = None + created_items["status"] = "ACTIVE" + return created_items + except (SdnConnectorError, OpenflowConnException) as e: + raise SdnConnectorError("Error while {}: {}".format(step, e)) from e + except Exception as e: + self.logger.critical(error_text, exc_info=True) + raise SdnConnectorError("Error while {}: {}".format(step, e)) + + def _compute_net_flows(self, net_id, ports): + new_flows = [] + new_broadcast_flows = {} + nb_ports = len(ports) + + # Check switch_port information is right + for port in ports: + nb_ports += 1 + if str(port['switch_port']) not in self.of_connector.pp2ofi: + raise SdnConnectorError("switch port name '{}' is not valid for the openflow controller". + format(port['switch_port'])) + priority = 1000 # 1100 + + for src_port in ports: + # if src_port.get("groups") + vlan_in = src_port['vlan'] + + # BROADCAST: + broadcast_key = src_port['uuid'] + "." + str(vlan_in) + if broadcast_key in new_broadcast_flows: + flow_broadcast = new_broadcast_flows[broadcast_key] + else: + flow_broadcast = {'priority': priority, + 'net_id': net_id, + 'dst_mac': 'ff:ff:ff:ff:ff:ff', + "ingress_port": str(src_port['switch_port']), + 'vlan_id': vlan_in, + 'actions': [] + } + new_broadcast_flows[broadcast_key] = flow_broadcast + if vlan_in is not None: + flow_broadcast['vlan_id'] = str(vlan_in) + + for dst_port in ports: + vlan_out = dst_port['vlan'] + if src_port['switch_port'] == dst_port['switch_port'] and vlan_in == vlan_out: + continue + flow = { + "priority": priority, + 'net_id': net_id, + "ingress_port": str(src_port['switch_port']), + 'vlan_id': vlan_in, + 'actions': [] + } + # allow that one port have no mac + if dst_port['mac'] is None or nb_ports == 2: # point to point or nets with 2 elements + flow['priority'] = priority - 5 # less priority + else: + flow['dst_mac'] = str(dst_port['mac']) + + if vlan_out is None: + if vlan_in: + flow['actions'].append(('vlan', None)) + else: + flow['actions'].append(('vlan', vlan_out)) + flow['actions'].append(('out', str(dst_port['switch_port']))) + + if self._check_flow_already_present(flow, new_flows) >= 0: + self.logger.debug("Skipping repeated flow '%s'", str(flow)) + continue + + new_flows.append(flow) + + # BROADCAST: + if nb_ports <= 2: # point to multipoint or nets with more than 2 elements + continue + out = (vlan_out, str(dst_port['switch_port'])) + if out not in flow_broadcast['actions']: + flow_broadcast['actions'].append(out) + + # BROADCAST + for flow_broadcast in new_broadcast_flows.values(): + if len(flow_broadcast['actions']) == 0: + continue # nothing to do, skip + flow_broadcast['actions'].sort() + if 'vlan_id' in flow_broadcast: + previous_vlan = 0 # indicates that a packet contains a vlan, and the vlan + else: + previous_vlan = None + final_actions = [] + action_number = 0 + for action in flow_broadcast['actions']: + if action[0] != previous_vlan: + final_actions.append(('vlan', action[0])) + previous_vlan = action[0] + if self.of_controller_nets_with_same_vlan and action_number: + raise SdnConnectorError("Cannot interconnect different vlan tags in a network when flag " + "'of_controller_nets_with_same_vlan' is True.") + action_number += 1 + final_actions.append(('out', action[1])) + flow_broadcast['actions'] = final_actions + + if self._check_flow_already_present(flow_broadcast, new_flows) >= 0: + self.logger.debug("Skipping repeated flow '%s'", str(flow_broadcast)) + continue + + new_flows.append(flow_broadcast) + + # UNIFY openflow rules with the same input port and vlan and the same output actions + # These flows differ at the dst_mac; and they are unified by not filtering by dst_mac + # this can happen if there is only two ports. It is converted to a point to point connection + flow_dict = {} # use as key vlan_id+ingress_port and as value the list of flows matching these values + for flow in new_flows: + key = str(flow.get("vlan_id")) + ":" + flow["ingress_port"] + if key in flow_dict: + flow_dict[key].append(flow) + else: + flow_dict[key] = [flow] + new_flows2 = [] + for flow_list in flow_dict.values(): + convert2ptp = False + if len(flow_list) >= 2: + convert2ptp = True + for f in flow_list: + if f['actions'] != flow_list[0]['actions']: + convert2ptp = False + break + if convert2ptp: # add only one unified rule without dst_mac + self.logger.debug("Convert flow rules to NON mac dst_address " + str(flow_list)) + flow_list[0].pop('dst_mac') + flow_list[0]["priority"] -= 5 + new_flows2.append(flow_list[0]) + else: # add all the rules + new_flows2 += flow_list + return new_flows2 + + def _check_flow_already_present(self, new_flow, flow_list): + '''check if the same flow is already present in the flow list + The flow is repeated if all the fields, apart from name, are equal + Return the index of matching flow, -1 if not match''' + for index, flow in enumerate(flow_list): + for f in self.flow_fields: + if flow.get(f) != new_flow.get(f): + break + else: + return index + return -1 diff --git a/RO/osm_ro/wim/persistence.py b/RO/osm_ro/wim/persistence.py index f0f1ac33..32a46b35 100644 --- a/RO/osm_ro/wim/persistence.py +++ b/RO/osm_ro/wim/persistence.py @@ -44,7 +44,7 @@ from contextlib import contextmanager from hashlib import sha1 from itertools import groupby from operator import itemgetter -from sys import exc_info +# from sys import exc_info # from time import time from uuid import uuid1 as generate_uuid @@ -362,6 +362,7 @@ class WimPersistence(object): def get_wim_accounts(self, **kwargs): """Retrieve all the accounts from the database""" kwargs.setdefault('postprocess', _postprocess_wim_account) + kwargs.setdefault('WHERE', {"sdn": "false"}) return self.query(FROM=_WIM_ACCOUNT_JOIN, **kwargs) def get_wim_account(self, uuid_or_name, **kwargs): @@ -523,7 +524,7 @@ class WimPersistence(object): self.logger.exception(old_exception) ex = InvalidParameters( "The mapping must contain the " - "'pop_switch_dpid', 'pop_switch_port', and " + "'device_id', 'device_interface_id', and " "wan_service_mapping_info: " "('wan_switch_dpid' and 'wan_switch_port') or " "'wan_service_endpoint_id}'") @@ -727,7 +728,7 @@ class WimPersistence(object): kwargs.setdefault('error_if_none', False) criteria_fields = ('uuid', 'instance_scenario_id', 'sce_net_id', - 'wim_id', 'wim_account_id') + 'wim_id', 'wim_account_id', 'sdn') criteria = remove_none_items(filter_dict_keys(kwargs, criteria_fields)) kwargs = filter_out_dict_keys(kwargs, criteria_fields) diff --git a/RO/osm_ro/wim/schemas.py b/RO/osm_ro/wim/schemas.py index 101bcb1d..8f9653b3 100644 --- a/RO/osm_ro/wim/schemas.py +++ b/RO/osm_ro/wim/schemas.py @@ -69,36 +69,21 @@ wim_port_mapping_desc = { "items": { "type": "object", "properties": { - "pop_switch_dpid": dpid_type, - "pop_switch_port": port_type, - "wan_service_endpoint_id": name_schema, - "wan_service_mapping_info": { + "device_id": nameshort_schema, + "device_interface_id": nameshort_schema, + "service_endpoint_id": name_schema, + "switch_dpid": dpid_type, + "switch_port": port_type, + "service_mapping_info": { "type": "object", "properties": { "mapping_type": name_schema, - "wan_switch_dpid": dpid_type, - "wan_switch_port": port_type }, "additionalProperties": True, "required": ["mapping_type"] } }, - "anyOf": [ - { - "required": [ - "pop_switch_dpid", - "pop_switch_port", - "wan_service_endpoint_id" - ] - }, - { - "required": [ - "pop_switch_dpid", - "pop_switch_port", - "wan_service_mapping_info" - ] - } - ] + "required": ["service_endpoint_id"] } } }, @@ -111,7 +96,7 @@ wim_schema_properties = { "description": description_schema, "type": { "type": "string", - "enum": ["tapi", "onos", "odl", "dynpac", "fake"] + # "enum": ["tapi", "onos", "odl", "dynpac", "fake"] }, "wim_url": description_schema, "config": { diff --git a/RO/osm_ro/wim/sdnconn.py b/RO/osm_ro/wim/sdnconn.py new file mode 100644 index 00000000..46649ce4 --- /dev/null +++ b/RO/osm_ro/wim/sdnconn.py @@ -0,0 +1,238 @@ +# -*- coding: utf-8 -*- +## +# Copyright 2018 University of Bristol - High Performance Networks Research +# Group +# All Rights Reserved. +# +# Contributors: Anderson Bravalheri, Dimitrios Gkounis, Abubakar Siddique +# Muqaddas, Navdeep Uniyal, Reza Nejabati and Dimitra Simeonidou +# +# 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. +# +# For those usages not covered by the Apache License, Version 2.0 please +# contact with: +# +# Neither the name of the University of Bristol nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# This work has been performed in the context of DCMS UK 5G Testbeds +# & Trials Programme and in the framework of the Metro-Haul project - +# funded by the European Commission under Grant number 761727 through the +# Horizon 2020 and 5G-PPP programmes. +## +"""The SDN connector is responsible for establishing both wide area network connectivity (WIM) +and intranet SDN connectivity. + +It receives information from ports to be connected . +""" +import logging + +from ..http_tools.errors import HttpMappedError + + +class SdnConnectorError(HttpMappedError): + """Base Exception for all connector related errors + provide the parameter 'http_code' (int) with the error code: + Bad_Request = 400 + Unauthorized = 401 (e.g. credentials are not valid) + Not_Found = 404 (e.g. try to edit or delete a non existing connectivity service) + Forbidden = 403 + Method_Not_Allowed = 405 + Not_Acceptable = 406 + Request_Timeout = 408 (e.g timeout reaching server, or cannot reach the server) + Conflict = 409 + Service_Unavailable = 503 + Internal_Server_Error = 500 + """ + + +class SdnConnectorBase(object): + """Abstract base class for all the SDN connectors + + Arguments: + wim (dict): WIM record, as stored in the database + wim_account (dict): WIM account record, as stored in the database + config + The arguments of the constructor are converted to object attributes. + An extra property, ``service_endpoint_mapping`` is created from ``config``. + """ + def __init__(self, wim, wim_account, config=None, logger=None): + """ + + :param wim: (dict). Contains among others 'wim_url' + :param wim_account: (dict). Contains among others 'uuid' (internal id), 'name', + 'sdn' (True if is intended for SDN-assist or False if intended for WIM), 'user', 'password'. + :param config: (dict or None): Particular information of plugin. These keys if present have a common meaning: + 'mapping_not_needed': (bool) False by default or if missing, indicates that mapping is not needed. + 'service_endpoint_mapping': (list) provides the internal endpoint mapping. The meaning is: + KEY meaning for WIM meaning for SDN assist + -------- -------- -------- + device_id pop_switch_dpid compute_id + device_interface_id pop_switch_port compute_pci_address + service_endpoint_id wan_service_endpoint_id SDN_service_endpoint_id + service_mapping_info wan_service_mapping_info SDN_service_mapping_info + contains extra information if needed. Text in Yaml format + switch_dpid wan_switch_dpid SDN_switch_dpid + switch_port wan_switch_port SDN_switch_port + datacenter_id vim_account vim_account + id: (internal, do not use) + wim_id: (internal, do not use) + :param logger (logging.Logger): optional logger object. If none is passed 'openmano.sdn.sdnconn' is used. + """ + self.logger = logger or logging.getLogger('openmano.sdn.sdnconn') + + self.wim = wim + self.wim_account = wim_account + self.config = config or {} + self.service_endpoint_mapping = ( + self.config.get('service_endpoint_mapping', [])) + + 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. + """ + raise NotImplementedError + + 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. + """ + raise NotImplementedError + + def create_connectivity_service(self, service_type, connection_points, **kwargs): + """ + Stablish 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 + """ + raise NotImplementedError + + 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 + """ + raise NotImplementedError + + 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. + """ + + 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/osm_ro/wim/tests/__init__.py b/RO/osm_ro/wim/tests/__init__.py index e69de29b..7284a2be 100644 --- a/RO/osm_ro/wim/tests/__init__.py +++ b/RO/osm_ro/wim/tests/__init__.py @@ -0,0 +1,13 @@ +## +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +## diff --git a/RO/osm_ro/wim/tests/fixtures.py b/RO/osm_ro/wim/tests/fixtures.py index 8984020d..b37cc135 100644 --- a/RO/osm_ro/wim/tests/fixtures.py +++ b/RO/osm_ro/wim/tests/fixtures.py @@ -99,7 +99,7 @@ def datacenter(identifier, external_ports_config=False): 'encapsulation_type': 'vlan'}, 'vim_external_port': dict(zip(('switch', 'port'), - _datacenter_to_switch_port(identifier)))} + _datacenter_to_switch_port(identifier)))} ]}) return {'uuid': uuid('dc%d' % identifier), @@ -144,13 +144,13 @@ def wim_port_mapping(wim, datacenter, return {'wim_id': uuid('wim%d' % wim), 'datacenter_id': uuid('dc%d' % datacenter), - 'pop_switch_dpid': pop_dpid, - 'pop_switch_port': (str(pop_port) if pop_port else - str(int(datacenter) + int(wim) + 1)), + 'device_id': pop_dpid, + 'device_interface_id': (str(pop_port) if pop_port else + str(int(datacenter) + int(wim) + 1)), # ^ Datacenter router have one port managed by each WIM - 'wan_service_endpoint_id': id_, + 'service_endpoint_id': id_, # ^ WIM managed router have one port connected to each DC - 'wan_service_mapping_info': json.dumps(mapping_info)} + 'service_mapping_info': json.dumps(mapping_info)} def processed_port_mapping(wim, datacenter, @@ -164,11 +164,11 @@ def processed_port_mapping(wim, datacenter, 'wim_id': uuid('wim%d' % wim), 'datacenter_id': uuid('dc%d' % datacenter), 'pop_wan_mappings': [ - {'pop_switch_dpid': pop_dpid, - 'pop_switch_port': wim + 1 + i, - 'wan_service_endpoint_id': + {'device_id': pop_dpid, + 'device_interface_id': wim + 1 + i, + 'service_endpoint_id': sha1('dpid-port|%s|%d' % (wan_dpid, datacenter + 1 + i)), - 'wan_service_mapping_info': { + 'service_mapping_info': { 'mapping_type': 'dpid-port', 'wan_switch_dpid': wan_dpid, 'wan_switch_port': datacenter + 1 + i}} diff --git a/RO/osm_ro/wim/tests/test_actions.py b/RO/osm_ro/wim/tests/test_actions.py index 37568692..ba66fbc0 100644 --- a/RO/osm_ro/wim/tests/test_actions.py +++ b/RO/osm_ro/wim/tests/test_actions.py @@ -33,7 +33,7 @@ ## # pylint: disable=E1101 -#from __future__ import unicode_literals, print_function +# from __future__ import unicode_literals, print_function import json import unittest @@ -49,7 +49,7 @@ from ...tests.db_helpers import ( ) from ..persistence import WimPersistence, preprocess_record from ..wan_link_actions import WanLinkCreate, WanLinkDelete -from ..wimconn import WimConnectorError +from ..sdnconn import SdnConnectorError class TestActionsWithDb(TestCaseWithDatabasePerTest): @@ -192,7 +192,7 @@ class TestCreate(TestActionsWithDb): # If the connector raises an error with patch.object(self.connector, 'create_connectivity_service', - MagicMock(side_effect=WimConnectorError('foobar'))): + MagicMock(side_effect=SdnConnectorError('foobar'))): # When we try to process a CREATE action that refers to the same # instance_scenario_id and sce_net_id action.process(self.connector, self.persist, self.ovim) @@ -219,8 +219,8 @@ class TestCreate(TestActionsWithDb): port_mappings = next(r['wim_port_mappings'] for r in db_state if 'wim_port_mappings' in r) for mapping in port_mappings: - mapping['pop_switch_dpid'] = switch - mapping['pop_switch_port'] = port + mapping['device_id'] = switch + mapping['device_interface_id'] = port instance_action = eg.instance_action(action_id='ACTION-000') instance_nets = eg.instance_nets(num_datacenters=2, num_links=1, @@ -294,7 +294,7 @@ class TestCreate(TestActionsWithDb): connector_patch = patch.object( self.connector, 'create_connectivity_service', - MagicMock(side_effect=WimConnectorError('foobar'))) + MagicMock(side_effect=SdnConnectorError('foobar'))) # If the connector throws an error with connector_patch, ovim_patch: diff --git a/RO/osm_ro/wim/tests/test_http_handler.py b/RO/osm_ro/wim/tests/test_http_handler.py index e42e53c6..ab3e2d0b 100644 --- a/RO/osm_ro/wim/tests/test_http_handler.py +++ b/RO/osm_ro/wim/tests/test_http_handler.py @@ -147,9 +147,9 @@ class TestHttpHandler(TestCaseWithDatabasePerTest): config={'wim_port_mapping': [{ 'datacenter_name': 'dc0', 'pop_wan_mappings': [{ - 'pop_switch_dpid': '00:AA:11:BB:22:CC:33:DD', - 'pop_switch_port': 1, - 'wan_service_mapping_info': { + 'device_id': '00:AA:11:BB:22:CC:33:DD', + 'device_interface_id': 1, + 'service_mapping_info': { 'mapping_type': 'dpid-port', 'wan_switch_dpid': 'BB:BB:BB:BB:BB:BB:BB:0A', 'wan_switch_port': 1 @@ -170,7 +170,7 @@ class TestHttpHandler(TestCaseWithDatabasePerTest): mappings = response.json['wim']['config']['wim_port_mapping'] self.assertEqual(len(mappings), 1) self.assertEqual( - mappings[0]['pop_wan_mappings'][0]['pop_switch_dpid'], + mappings[0]['pop_wan_mappings'][0]['device_id'], '00:AA:11:BB:22:CC:33:DD') def test_delete_wim(self): @@ -224,9 +224,9 @@ class TestHttpHandler(TestCaseWithDatabasePerTest): config={'wim_port_mapping': [{ 'datacenter_name': 'dc0', 'pop_wan_mappings': [{ - 'pop_switch_dpid': 'AA:AA:AA:AA:AA:AA:AA:01', - 'pop_switch_port': 1, - 'wan_service_mapping_info': { + 'device_id': 'AA:AA:AA:AA:AA:AA:AA:01', + 'device_interface_id': 1, + 'service_mapping_info': { 'mapping_type': 'dpid-port', 'wan_switch_dpid': 'BB:BB:BB:BB:BB:BB:BB:01', 'wan_switch_port': 1 @@ -523,9 +523,9 @@ class TestHttpHandler(TestCaseWithDatabasePerTest): {'wim_port_mapping': [{ 'datacenter_name': 'dc888', 'pop_wan_mappings': [ - {'pop_switch_dpid': 'AA:AA:AA:AA:AA:AA:AA:AA', - 'pop_switch_port': 1, - 'wan_service_mapping_info': { + {'device_id': 'AA:AA:AA:AA:AA:AA:AA:AA', + 'device_interface_id': 1, + 'service_mapping_info': { 'mapping_type': 'dpid-port', 'wan_switch_dpid': 'BB:BB:BB:BB:BB:BB:BB:BB', 'wan_switch_port': 1 diff --git a/RO/osm_ro/wim/tests/test_wim_thread.py b/RO/osm_ro/wim/tests/test_wim_thread.py index b8c8231b..7ad66c2d 100644 --- a/RO/osm_ro/wim/tests/test_wim_thread.py +++ b/RO/osm_ro/wim/tests/test_wim_thread.py @@ -67,7 +67,7 @@ class TestWimThreadWithDb(TestCaseWithDatabasePerTest): wim = eg.wim(0) account = eg.wim_account(0, 0) account['wim'] = wim - self.thread = WimThread(self.persist, account) + self.thread = WimThread(self.persist, {}, account) self.thread.connector = MagicMock() def assertTasksEqual(self, left, right): @@ -282,7 +282,7 @@ class TestWimThread(unittest.TestCase): account = eg.wim_account(0, 0) account['wim'] = wim self.persist = MagicMock() - self.thread = WimThread(self.persist, account) + self.thread = WimThread(self.persist, {}, account) self.thread.connector = MagicMock() super(TestWimThread, self).setUp() diff --git a/RO/osm_ro/wim/tox.ini b/RO/osm_ro/wim/tox.ini index 29f1a8f7..a426d4d0 100644 --- a/RO/osm_ro/wim/tox.ini +++ b/RO/osm_ro/wim/tox.ini @@ -1,3 +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. +## + # This tox file allows the devs to run unit tests only for this subpackage. # In order to do so, cd into the directory and run `tox` diff --git a/RO/osm_ro/wim/wan_link_actions.py b/RO/osm_ro/wim/wan_link_actions.py index 0d878b2c..458f3d52 100644 --- a/RO/osm_ro/wim/wan_link_actions.py +++ b/RO/osm_ro/wim/wan_link_actions.py @@ -34,7 +34,7 @@ # pylint: disable=E1101,E0203,W0201 import json from pprint import pformat -from sys import exc_info +# from sys import exc_info from time import time from ..utils import filter_dict_keys as filter_keys @@ -45,7 +45,7 @@ from .errors import ( NoRecordFound, NoExternalPortFound ) -from .wimconn import WimConnectorError +from .sdnconn import SdnConnectorError INSTANCE_NET_STATUS_ERROR = ('DOWN', 'ERROR', 'VIM_ERROR', 'DELETED', 'SCHEDULED_DELETION') @@ -64,26 +64,26 @@ class RefreshMixin(object): Infrastructure Manager system persistence: abstraction layer for the database """ - fields = ('wim_status', 'wim_info', 'error_msg') + fields = ('sdn_status', 'sdn_info', 'error_msg') result = dict.fromkeys(fields) try: result.update( connector .get_connectivity_service_status(self.wim_internal_id)) - except WimConnectorError as ex: + except SdnConnectorError as ex: self.logger.exception(ex) - result.update(wim_status='WIM_ERROR', error_msg=truncate(ex)) + result.update(sdn_status='WIM_ERROR', error_msg=truncate(ex)) result = filter_keys(result, fields) action_changes = remove_none_items({ 'extra': merge_dicts(self.extra, result), - 'status': 'BUILD' if result['wim_status'] == 'BUILD' else None, + 'status': 'BUILD' if result['sdn_status'] == 'BUILD' else None, 'error_msg': result['error_msg'], 'modified_at': time()}) - link_changes = merge_dicts(result, status=result.pop('wim_status')) - # ^ Rename field: wim_status => status + link_changes = merge_dicts(result, status=result.pop('sdn_status')) + # ^ Rename field: sdn_status => status persistence.update_wan_link(self.item_id, remove_none_items(link_changes)) @@ -159,10 +159,10 @@ class WanLinkCreate(RefreshMixin, CreateAction): Returns: dict: Record representing the wan_port_mapping associated to the given instance_net. The expected fields are: - **wim_id**, **datacenter_id**, **pop_switch_dpid** (the local - network is expected to be connected at this switch), - **pop_switch_port**, **wan_service_endpoint_id**, - **wan_service_mapping_info**. + **wim_id**, **datacenter_id**, **device_id** (the local + network is expected to be connected at this switch dpid), + **device_interface_id**, **service_endpoint_id**, + **service_mapping_info**. """ # First, we need to find a route from the datacenter to the outside # world. For that, we can use the rules given in the datacenter @@ -185,8 +185,8 @@ class WanLinkCreate(RefreshMixin, CreateAction): criteria = { 'wim_id': wim_account['wim_id'], - 'pop_switch_dpid': external_port[0], - 'pop_switch_port': external_port[1], + 'device_id': external_port[0], + 'device_interface_id': external_port[1], 'datacenter_id': datacenter_id} wan_port_mapping = persistence.query_one( @@ -200,11 +200,11 @@ class WanLinkCreate(RefreshMixin, CreateAction): # It is important to return encapsulation information if present mapping = merge_dicts( - wan_port_mapping.get('wan_service_mapping_info'), + wan_port_mapping.get('service_mapping_info'), filter_keys(vim_info, ('encapsulation_type', 'encapsulation_id')) ) - return merge_dicts(wan_port_mapping, wan_service_mapping_info=mapping) + return merge_dicts(wan_port_mapping, service_mapping_info=mapping) def _get_port_sdn(self, ovim, instance_net): criteria = {'net_id': instance_net['sdn_net_id']} @@ -310,9 +310,9 @@ class WanLinkCreate(RefreshMixin, CreateAction): @staticmethod def _derive_connection_point(wan_info): - point = {'service_endpoint_id': wan_info['wan_service_endpoint_id']} + point = {'service_endpoint_id': wan_info['service_endpoint_id']} # TODO: Cover other scenarios, e.g. VXLAN. - details = wan_info.get('wan_service_mapping_info', {}) + details = wan_info.get('service_mapping_info', {}) if details.get('encapsulation_type') == 'vlan': point['service_endpoint_encapsulation_type'] = 'dot1q' point['service_endpoint_encapsulation_info'] = { @@ -335,7 +335,7 @@ class WanLinkCreate(RefreshMixin, CreateAction): """Store plugin/connector specific information in the database""" persistence.update_wan_link(self.item_id, { 'wim_internal_id': service_uuid, - 'wim_info': {'conn_info': conn_info}, + 'sdn_info': {'conn_info': conn_info}, 'status': 'BUILD'}) def execute(self, connector, persistence, ovim, instance_nets): @@ -353,7 +353,7 @@ class WanLinkCreate(RefreshMixin, CreateAction): connection_points # TODO: other properties, e.g. bandwidth ) - except (WimConnectorError, InconsistentState, + except (SdnConnectorError, InconsistentState, NoExternalPortFound) as ex: self.logger.exception(ex) return self.fail( @@ -412,12 +412,12 @@ class WanLinkDelete(DeleteAction): try: id = self.wim_internal_id - conn_info = safe_get(wan_link, 'wim_info.conn_info') + conn_info = safe_get(wan_link, 'sdn_info.conn_info') self.logger.debug('Connection Service %s (wan_link: %s):\n%s\n', id, wan_link['uuid'], json.dumps(conn_info, indent=4)) result = connector.delete_connectivity_service(id, conn_info) - except (WimConnectorError, InconsistentState) as ex: + except (SdnConnectorError, InconsistentState) as ex: self.logger.exception(ex) return self.fail( persistence, diff --git a/RO/osm_ro/wim/wim_thread.py b/RO/osm_ro/wim/wim_thread.py index 13502b99..34661935 100644 --- a/RO/osm_ro/wim/wim_thread.py +++ b/RO/osm_ro/wim/wim_thread.py @@ -45,7 +45,7 @@ from contextlib import contextmanager from functools import partial from itertools import islice, chain, takewhile from operator import itemgetter, attrgetter -from sys import exc_info +# from sys import exc_info from time import time, sleep import queue @@ -60,10 +60,8 @@ from .errors import ( UndefinedAction, ) from .failing_connector import FailingConnector -from .wimconn import WimConnectorError -from .wimconn_dynpac import DynpacConnector +from .sdnconn import SdnConnectorError from .wimconn_fake import FakeConnector -from .wimconn_ietfl2vpn import WimconnectorIETFL2VPN ACTIONS = { 'instance_wim_nets': wan_link_actions.ACTIONS @@ -71,10 +69,8 @@ ACTIONS = { CONNECTORS = { # "odl": wimconn_odl.OdlConnector, - "dynpac": DynpacConnector, "fake": FakeConnector, - "tapi": WimconnectorIETFL2VPN, - # Add extra connectors here + # Add extra connectors here not managed via plugins } @@ -101,17 +97,21 @@ class WimThread(threading.Thread): MAX_RECOVERY_TIME = 180 WAITING_TIME = 1 # Wait 1s for taks to arrive, when there are none - def __init__(self, persistence, wim_account, logger=None, ovim=None): + def __init__(self, persistence, plugins, wim_account, logger=None, ovim=None): """Init a thread. Arguments: persistence: Database abstraction layer + plugins: dictionary with the vim/sdn plugins wim_account: Record containing wim_account, tenant and wim information. """ name = '{}.{}.{}'.format(wim_account['wim']['name'], wim_account['name'], wim_account['uuid']) super(WimThread, self).__init__(name=name) + self.plugins = plugins + if "rosdn_fake" not in self.plugins: + self.plugins["rosdn_fake"] = FakeConnector self.name = name self.connector = None @@ -160,9 +160,11 @@ class WimThread(threading.Thread): mapping = self.persist.query('wim_port_mappings', WHERE={'wim_id': wim['uuid']}, error_if_none=False) - return CONNECTORS[wim['type']](wim, account, { - 'service_endpoint_mapping': mapping or [] - }) + if wim["type"] in CONNECTORS: + return CONNECTORS[wim['type']](wim, account, {'service_endpoint_mapping': mapping or []}) + else: # load a plugin + return self.plugins["rosdn_" + wim["type"]]( + wim, account, {'service_endpoint_mapping': mapping or []}) except DbBaseException as ex: error_msg = ('Error when retrieving WIM account ({})\n' .format(account_id)) + str(ex) @@ -170,8 +172,8 @@ class WimThread(threading.Thread): except KeyError as ex: error_msg = ('Unable to find the WIM connector for WIM ({})\n' .format(wim['type'])) + str(ex) - self.logger.error(error_msg, exc_info=True) - except (WimConnectorError, Exception) as ex: + self.logger.error(error_msg) + except (SdnConnectorError, Exception) as ex: # TODO: Remove the Exception class here when the connector class is # ready error_msg = ('Error when loading WIM connector for WIM ({})\n' diff --git a/RO/osm_ro/wim/wimconn.py b/RO/osm_ro/wim/wimconn.py deleted file mode 100644 index 92b6db08..00000000 --- a/RO/osm_ro/wim/wimconn.py +++ /dev/null @@ -1,236 +0,0 @@ -# -*- coding: utf-8 -*- -## -# Copyright 2018 University of Bristol - High Performance Networks Research -# Group -# All Rights Reserved. -# -# Contributors: Anderson Bravalheri, Dimitrios Gkounis, Abubakar Siddique -# Muqaddas, Navdeep Uniyal, Reza Nejabati and Dimitra Simeonidou -# -# 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. -# -# For those usages not covered by the Apache License, Version 2.0 please -# contact with: -# -# Neither the name of the University of Bristol nor the names of its -# contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# This work has been performed in the context of DCMS UK 5G Testbeds -# & Trials Programme and in the framework of the Metro-Haul project - -# funded by the European Commission under Grant number 761727 through the -# Horizon 2020 and 5G-PPP programmes. -## -"""The WIM connector is responsible for establishing wide area network -connectivity. - -It receives information from the WimThread/WAN Actions about the endpoints of -a link that spans across multiple datacenters and stablish a path between them. -""" -import logging - -from ..http_tools.errors import HttpMappedError - - -class WimConnectorError(HttpMappedError): - """Base Exception for all connector related errors""" - - -class WimConnector(object): - """Abstract base class for all the WIM connectors - - Arguments: - wim (dict): WIM record, as stored in the database - wim_account (dict): WIM account record, as stored in the database - config (dict): optional persistent information related to an specific - connector. Inside this dict, a special key, - ``service_endpoint_mapping`` provides the internal endpoint - mapping. - logger (logging.Logger): optional logger object. If none is passed - ``openmano.wim.wimconn`` is used. - - The arguments of the constructor are converted to object attributes. - An extra property, ``service_endpoint_mapping`` is created from ``config``. - """ - def __init__(self, wim, wim_account, config=None, logger=None): - self.logger = logger or logging.getLogger('openmano.wim.wimconn') - - self.wim = wim - self.wim_account = wim_account - self.config = config or {} - self.service_endpoint_mapping = ( - config.get('service_endpoint_mapping', [])) - - def check_credentials(self): - """Check if the connector itself can access the WIM. - - Raises: - WimConnectorError: Issues regarding authorization, access to - external URLs, etc are detected. - """ - raise NotImplementedError - - 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 - ``wim_status`` associated with one of the following values:: - - {'wim_status': 'ACTIVE'} - # The service is up and running. - - {'wim_status': 'INACTIVE'} - # The service was created, but the connector - # cannot determine yet if connectivity exists - # (ideally, the caller needs to wait and check again). - - {'wim_status': 'DOWN'} - # Connection was previously established, - # but an error/failure was detected. - - {'wim_status': 'ERROR'} - # An error occurred when trying to create the service/ - # establish the connectivity. - - {'wim_status': 'BUILD'} - # Still trying to create the service, the caller - # needs to wait and check again. - - Additionally ``error_msg``(**str**) and ``wim_info``(**dict**) - keys can be used to provide additional status explanation or - new information available for the connectivity service. - """ - raise NotImplementedError - - def create_connectivity_service(self, service_type, connection_points, - **kwargs): - """Stablish WAN connectivity between the endpoints - - Arguments: - service_type (str): ``ELINE`` (L2), ``ELAN`` (L2), ``ETREE`` (L2), - ``L3``. - connection_points (list): each point corresponds to - an entry point from the DC to the transport network. One - connection point serves to identify the specific access and - some other service parameters, such as encapsulation type. - Represented by a dict as follows:: - - { - "service_endpoint_id": ..., (str[uuid]) - "service_endpoint_encapsulation_type": ..., - (enum: none, dot1q, ...) - "service_endpoint_encapsulation_info": { - ... (dict) - "vlan": ..., (int, present if encapsulation is dot1q) - "vni": ... (int, present if encapsulation is vxlan), - "peers": [(ipv4_1), (ipv4_2)] - (present if encapsulation is vxlan) - } - } - - The service endpoint ID should be previously informed to the WIM - engine in the RO when the WIM port mapping is registered. - - Keyword Arguments: - bandwidth (int): value in kilobytes - latency (int): value in milliseconds - - Other QoS might be passed as keyword arguments. - - Returns: - 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: - WimConnectorException: In case of error. - """ - raise NotImplementedError - - def delete_connectivity_service(self, service_uuid, conn_info=None): - """Disconnect multi-site endpoints previously connected - - This method should receive as arguments both the UUID and the - connection info dict (respectively), as returned by - :meth:`~.create_connectivity_service` and - :meth:`~.edit_connectivity_service`. - - Arguments: - service_uuid (str): UUID of the connectivity service - conn_info (dict or None): Information returned by the connector - during the service creation and subsequently stored in the - database. - - Raises: - WimConnectorException: In case of error. - """ - raise NotImplementedError - - 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`. - - Arguments: - service_uuid (str): UUID of the connectivity service. - conn_info (dict or None): Information previously stored in the - database. - connection_points (list): If provided, the old list of connection - points will be replaced. - - Returns: - 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: - WimConnectorException: 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, not only the WIM connections that - a specific RO is aware of. - - Raises: - WimConnectorException: In case of error. - """ - raise NotImplementedError - - def get_all_active_connectivity_services(self): - """Provide information about all active connections provisioned by a - WIM. - - Raises: - WimConnectorException: In case of error. - """ - raise NotImplementedError diff --git a/RO/osm_ro/wim/wimconn_fake.py b/RO/osm_ro/wim/wimconn_fake.py index 36929f4c..168996d7 100644 --- a/RO/osm_ro/wim/wimconn_fake.py +++ b/RO/osm_ro/wim/wimconn_fake.py @@ -22,12 +22,12 @@ This WIM does nothing and allows using it for testing and when no WIM is needed import logging from uuid import uuid4 -from .wimconn import WimConnector - +from .sdnconn import SdnConnectorBase, SdnConnectorError +from http import HTTPStatus __author__ = "Alfonso Tierno " -class FakeConnector(WimConnector): +class FakeConnector(SdnConnectorBase): """Abstract base class for all the WIM connectors Arguments: @@ -44,8 +44,8 @@ class FakeConnector(WimConnector): An extra property, ``service_endpoint_mapping`` is created from ``config``. """ def __init__(self, wim, wim_account, config=None, logger=None): - self.logger = logging.getLogger('openmano.wimconn.fake') - super(FakeConnector, self).__init__(wim, wim_account, config, logger) + self.logger = logger or logging.getLogger('openmano.sdnconn.fake') + super(FakeConnector, self).__init__(wim, wim_account, config, self.logger) self.logger.debug("__init: wim='{}' wim_account='{}'".format(wim, wim_account)) self.connections = {} self.counter = 0 @@ -54,7 +54,7 @@ class FakeConnector(WimConnector): """Check if the connector itself can access the WIM. Raises: - WimConnectorError: Issues regarding authorization, access to + SdnConnectorError: Issues regarding authorization, access to external URLs, etc are detected. """ self.logger.debug("check_credentials") @@ -71,15 +71,15 @@ class FakeConnector(WimConnector): Returns: dict: JSON/YAML-serializable dict that contains a mandatory key - ``wim_status`` associated with one of the following values:: + ``sdn_status`` associated with one of the following values:: - Additionally ``error_msg``(**str**) and ``wim_info``(**dict**) + 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. """ self.logger.debug("get_connectivity_service_status: service_uuid='{}' conn_info='{}'".format(service_uuid, conn_info)) - return {'wim_status': 'ACTIVE', 'wim_info': self.connectivity.get(service_uuid)} + return {'sdn_status': 'ACTIVE', 'sdn_info': self.connectivity.get(service_uuid)} def create_connectivity_service(self, service_type, connection_points, **kwargs): @@ -90,9 +90,9 @@ class FakeConnector(WimConnector): self.logger.debug("create_connectivity_service: service_type='{}' connection_points='{}', kwargs='{}'". format(service_type, connection_points, kwargs)) _id = str(uuid4()) - self.connectivity[_id] = {"nb": self.counter} + self.connections[_id] = connection_points.copy() self.counter += 1 - return _id, self.connectivity[_id] + return _id, None def delete_connectivity_service(self, service_uuid, conn_info=None): """Disconnect multi-site endpoints previously connected @@ -100,7 +100,10 @@ class FakeConnector(WimConnector): """ self.logger.debug("delete_connectivity_service: service_uuid='{}' conn_info='{}'".format(service_uuid, conn_info)) - self.connectivity.pop(service_uuid, None) + if service_uuid not in self.connections: + raise SdnConnectorError("connectivity {} not found".format(service_uuid), + http_code=HTTPStatus.NOT_FOUND.value) + self.connections.pop(service_uuid, None) return None def edit_connectivity_service(self, service_uuid, conn_info=None, @@ -112,6 +115,10 @@ class FakeConnector(WimConnector): """ self.logger.debug("edit_connectivity_service: service_uuid='{}' conn_info='{}', connection_points='{}'" "kwargs='{}'".format(service_uuid, conn_info, connection_points, kwargs)) + if service_uuid not in self.connections: + raise SdnConnectorError("connectivity {} not found".format(service_uuid), + http_code=HTTPStatus.NOT_FOUND.value) + self.connections[service_uuid] = connection_points.copy() return None def clear_all_connectivity_services(self): @@ -123,7 +130,7 @@ class FakeConnector(WimConnector): """ self.logger.debug("clear_all_connectivity_services") - self.connectivity.clear() + self.connections.clear() return None def get_all_active_connectivity_services(self): @@ -131,7 +138,7 @@ class FakeConnector(WimConnector): WIM. Raises: - WimConnectorException: In case of error. + SdnConnectorException: In case of error. """ self.logger.debug("get_all_active_connectivity_services") - return self.connectivity + return self.connections diff --git a/RO/osm_ro/wim/wimconn_odl.py b/RO/osm_ro/wim/wimconn_odl.py index 23710467..e4af3c6a 100644 --- a/RO/osm_ro/wim/wimconn_odl.py +++ b/RO/osm_ro/wim/wimconn_odl.py @@ -31,12 +31,12 @@ # funded by the European Commission under Grant number 761727 through the # Horizon 2020 and 5G-PPP programmes. ## -from .wimconn import WimConnector +from .sdnconn import SdnConnectorBase # TODO: Basically create this file -class OdlConnector(WimConnector): +class OdlConnector(SdnConnectorBase): def get_connectivity_service_status(self, link_uuid): raise NotImplementedError diff --git a/RO/requirements.txt b/RO/requirements.txt index 731b5050..973b82d3 100644 --- a/RO/requirements.txt +++ b/RO/requirements.txt @@ -1,3 +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. +## + PyYAML bottle MySQL-python diff --git a/RO/test/RO_tests/v3_2vdu_set_ip_mac/scenario_2vdu_set_ip_mac.yaml b/RO/test/RO_tests/v3_2vdu_set_ip_mac/scenario_2vdu_set_ip_mac.yaml index fb760799..1b998b69 100644 --- a/RO/test/RO_tests/v3_2vdu_set_ip_mac/scenario_2vdu_set_ip_mac.yaml +++ b/RO/test/RO_tests/v3_2vdu_set_ip_mac/scenario_2vdu_set_ip_mac.yaml @@ -1,3 +1,16 @@ +## +# 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. +## nsd:nsd-catalog: nsd: - id: test_2vdu_nsd diff --git a/RO/test/RO_tests/v3_2vdu_set_ip_mac/vnfd_2vdu_set_ip_mac.yaml b/RO/test/RO_tests/v3_2vdu_set_ip_mac/vnfd_2vdu_set_ip_mac.yaml index e790a9cb..c95162e2 100644 --- a/RO/test/RO_tests/v3_2vdu_set_ip_mac/vnfd_2vdu_set_ip_mac.yaml +++ b/RO/test/RO_tests/v3_2vdu_set_ip_mac/vnfd_2vdu_set_ip_mac.yaml @@ -1,3 +1,16 @@ +## +# 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. +## vnfd-catalog: vnfd: - connection-point: diff --git a/RO/test/RO_tests/v3_2vdu_set_ip_mac/vnfd_2vdu_set_ip_mac2.yaml b/RO/test/RO_tests/v3_2vdu_set_ip_mac/vnfd_2vdu_set_ip_mac2.yaml index 6c4b6cfb..ad574a16 100644 --- a/RO/test/RO_tests/v3_2vdu_set_ip_mac/vnfd_2vdu_set_ip_mac2.yaml +++ b/RO/test/RO_tests/v3_2vdu_set_ip_mac/vnfd_2vdu_set_ip_mac2.yaml @@ -1,3 +1,16 @@ +## +# 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. +## vnfd-catalog: vnfd: - connection-point: diff --git a/devops-stages/stage-build.sh b/devops-stages/stage-build.sh index b1640eb4..b48eaa34 100755 --- a/devops-stages/stage-build.sh +++ b/devops-stages/stage-build.sh @@ -51,3 +51,16 @@ cp RO-VIM-azure/deb_dist/python3-osm-rovim-azure_*.deb deb_dist/ # VIM Opennebula plugin make -C RO-VIM-opennebula clean package cp RO-VIM-opennebula/deb_dist/python3-osm-rovim-opennebula_*.deb deb_dist/ + +# SDN Dynpack plugin +make -C RO-SDN-dynpac clean package +cp RO-SDN-dynpac/deb_dist/python3-osm-rosdn-dynpac_*.deb deb_dist/ + +# SDN Tapi plugin +make -C RO-SDN-tapi clean package +cp RO-SDN-tapi/deb_dist/python3-osm-rosdn-tapi_*.deb deb_dist/ + +# SDN Onos openflow +make -C RO-SDN-onos_openflow clean package +cp RO-SDN-onos_openflow/deb_dist/python3-osm-rosdn-onosof_*.deb deb_dist/ + -- 2.17.1