Juniper Contrail SDN plugin 00/8800/3
authorgarciadeblas <gerardo.garciadeblas@telefonica.com>
Fri, 17 Apr 2020 05:55:46 +0000 (05:55 +0000)
committergarciadeblas <gerardo.garciadeblas@telefonica.com>
Mon, 27 Apr 2020 08:03:03 +0000 (08:03 +0000)
Change-Id: I14987a321218b5cda98e6c4be68ae4a86596a2de
Signed-off-by: garciadeblas <gerardo.garciadeblas@telefonica.com>
Dockerfile-local
RO-SDN-juniper_contrail/Makefile [new file with mode: 0644]
RO-SDN-juniper_contrail/osm_rosdn_juniper_contrail/rest_lib.py [new file with mode: 0644]
RO-SDN-juniper_contrail/osm_rosdn_juniper_contrail/sdn_assist_juniper_contrail.py [new file with mode: 0644]
RO-SDN-juniper_contrail/requirements.txt [new file with mode: 0644]
RO-SDN-juniper_contrail/setup.py [new file with mode: 0644]
RO-SDN-juniper_contrail/stdeb.cfg [new file with mode: 0644]
RO-SDN-juniper_contrail/tox.ini [new file with mode: 0644]

index c007209..1e4e29b 100644 (file)
@@ -58,6 +58,7 @@ RUN /root/RO/RO/osm_ro/scripts/install-osm-im.sh --develop && \
     python3 -m pip install -e /root/RO/RO-SDN-odl_openflow && \
     python3 -m pip install -e /root/RO/RO-SDN-floodlight_openflow && \
     python3 -m pip install -e /root/RO/RO-SDN-arista && \
+    python3 -m pip install -e /root/RO/RO-SDN-juniper_contrail && \
     rm -rf /root/.cache && \
     apt-get clean && \
     rm -rf /var/lib/apt/lists/*
diff --git a/RO-SDN-juniper_contrail/Makefile b/RO-SDN-juniper_contrail/Makefile
new file mode 100644 (file)
index 0000000..ad691c6
--- /dev/null
@@ -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_juniper_contrail-*.tar.gz osm_rosdn_juniper_contrail.egg-info .eggs
+
+package:
+       python3 setup.py --command-packages=stdeb.command sdist_dsc
+       cd deb_dist/osm-rosdn-juniper-contrail*/ && dpkg-buildpackage -rfakeroot -uc -us
+
diff --git a/RO-SDN-juniper_contrail/osm_rosdn_juniper_contrail/rest_lib.py b/RO-SDN-juniper_contrail/osm_rosdn_juniper_contrail/rest_lib.py
new file mode 100644 (file)
index 0000000..18c3bd2
--- /dev/null
@@ -0,0 +1,146 @@
+# Copyright 2020 ETSI
+#
+# 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.
+
+from io import BytesIO
+import pycurl
+import json
+import copy
+
+class HttpException(Exception):
+    pass
+
+class NotFound(HttpException):
+    pass
+
+class Http(object):
+
+    def __init__(self, logger):
+        self._logger = logger
+        self._response_headers = None
+
+
+    def _check_http_response(self, http_code, data):
+        if http_code >= 300:
+            resp = ""
+            if data.getvalue():
+                data_text = data.getvalue().decode()
+                self._logger.info("Response {} DATA: {}".format(http_code, data_text))
+                resp = ": " + data_text
+            else:
+                self._logger.info("Response {}".format(http_code))
+            if http_code == 404:
+                raise NotFound("Error {}{}".format(http_code, resp))
+            raise HttpException("Error {}{}".format(http_code, resp))
+
+
+    def _get_curl_cmd(self, url, headers):
+        self._logger.debug("")
+        curl_cmd = pycurl.Curl()
+        curl_cmd.setopt(pycurl.URL, url)
+        curl_cmd.setopt(pycurl.SSL_VERIFYPEER, 0)
+        curl_cmd.setopt(pycurl.SSL_VERIFYHOST, 0)
+        if headers:
+            curl_cmd.setopt(pycurl.HTTPHEADER, headers)
+        return curl_cmd
+
+
+    def get_cmd(self, url, headers):
+        self._logger.debug("")
+        data = BytesIO()
+        curl_cmd = self._get_curl_cmd(url, headers)
+        curl_cmd.setopt(pycurl.HTTPGET, 1)
+        curl_cmd.setopt(pycurl.WRITEFUNCTION, data.write)
+        self._logger.info("Request METHOD: {} URL: {}".format("GET", url))
+        curl_cmd.perform()
+        http_code = curl_cmd.getinfo(pycurl.HTTP_CODE)
+        self._logger.info("Response HTTPCODE: {}".format(http_code))
+        curl_cmd.close()
+        if data.getvalue():
+            data_text = data.getvalue().decode()
+            self._logger.debug("Response DATA: {}".format(data_text))
+            return http_code, data_text
+        return http_code, None
+
+
+    def delete_cmd(self, url, headers):
+        self._logger.debug("")
+        data = BytesIO()
+        curl_cmd = self._get_curl_cmd(url, headers)
+        curl_cmd.setopt(pycurl.CUSTOMREQUEST, "DELETE")
+        curl_cmd.setopt(pycurl.WRITEFUNCTION, data.write)
+        self._logger.info("Request METHOD: {} URL: {}".format("DELETE", url))
+        curl_cmd.perform()
+        http_code = curl_cmd.getinfo(pycurl.HTTP_CODE)
+        self._logger.info("Response HTTPCODE: {}".format(http_code))
+        curl_cmd.close()
+        self._check_http_response(http_code, data)
+        # TODO 202 accepted should be returned somehow
+        if data.getvalue():
+            data_text = data.getvalue().decode()
+            self._logger.debug("Response DATA: {}".format(data_text))
+            return http_code, data_text
+        else:
+            self._logger.debug("Response DATA: NONE")
+            return http_code, None
+
+
+    def header_function(self, header_line):
+        header_line = header_line.decode('iso-8859-1')
+        if ':' not in header_line:
+            return
+        name, value = header_line.split(':', 1)
+        name = name.strip()
+        value = value.strip()
+        name = name.lower()
+        self._response_headers[name] = value
+
+
+    def post_cmd(self, url, headers, postfields_dict=None, return_header=None):
+        self._logger.debug('url: {}, headers: {}, postfields_dict: {}, return_header: {}'.format(url, headers, postfields_dict, return_header))
+        data = BytesIO()
+        curl_cmd = self._get_curl_cmd(url, headers)
+        curl_cmd.setopt(pycurl.POST, 1)
+        curl_cmd.setopt(pycurl.WRITEFUNCTION, data.write)
+        if return_header:
+            self._response_headers = {}
+            curl_cmd.setopt(pycurl.HEADERFUNCTION, self.header_function)
+
+        jsondata = json.dumps(postfields_dict)
+        if postfields_dict.get('auth',{}).get('identity',{}).get('password',{}).get('user',{}).get('password'):
+            postfields_dict_copy = copy.deepcopy(postfields_dict)
+            postfields_dict_copy['auth']['identity']['password']['user']['password'] = '******'
+            jsondata_log = json.dumps(postfields_dict_copy)
+        else:
+            jsondata_log = jsondata
+        self._logger.debug("Request POSTFIELDS: {}".format(jsondata_log))
+        curl_cmd.setopt(pycurl.POSTFIELDS, jsondata)
+
+        self._logger.info("Request METHOD: {} URL: {}".format("POST", url))
+        curl_cmd.perform()
+        http_code = curl_cmd.getinfo(pycurl.HTTP_CODE)
+        self._logger.info("Response HTTPCODE: {}".format(http_code))
+        curl_cmd.close()
+        if return_header:
+            data_text = self._response_headers.get(return_header)
+            self._logger.debug("Response HEADER: {}".format(data_text))
+            return http_code, data_text
+        if data.getvalue():
+            data_text = data.getvalue().decode()
+            self._logger.debug("Response DATA: {}".format(data_text))
+            return http_code, data_text
+        else:
+            return http_code, None
+
diff --git a/RO-SDN-juniper_contrail/osm_rosdn_juniper_contrail/sdn_assist_juniper_contrail.py b/RO-SDN-juniper_contrail/osm_rosdn_juniper_contrail/sdn_assist_juniper_contrail.py
new file mode 100644 (file)
index 0000000..e29ac79
--- /dev/null
@@ -0,0 +1,734 @@
+# -*- coding: utf-8 -*-
+
+# Copyright 2020 ETSI OSM
+#
+# 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
+import uuid
+import copy
+import json
+import yaml
+
+#import requests
+#from requests.auth import HTTPBasicAuth
+from osm_ro.wim.sdnconn import SdnConnectorBase, SdnConnectorError
+from osm_rosdn_juniper_contrail.rest_lib import Http
+
+from io import BytesIO
+import pycurl
+
+
+class JuniperContrail(SdnConnectorBase):
+    """
+    Juniper Contrail SDN plugin. The plugin interacts with Juniper Contrail Controller,
+    whose API details can be found in these links:
+
+    - https://github.com/tonyliu0592/contrail/wiki/API-Configuration-REST
+    - https://www.juniper.net/documentation/en_US/contrail19/information-products/pathway-pages/api-guide-1910/tutorial_with_rest.html
+    - https://github.com/tonyliu0592/contrail-toolbox/blob/master/sriov/sriov
+    """
+    _WIM_LOGGER = "openmano.sdnconn.junipercontrail"
+
+    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(self._WIM_LOGGER)
+        self.logger.debug('wim: {}, wim_account: {}, config: {}'.format(wim, wim_account, config))
+        super().__init__(wim, wim_account, config, logger)
+
+        self.user = wim_account.get("user")
+        self.password = wim_account.get("password")
+
+        url = wim.get("wim_url")
+        auth_url = None
+        overlay_url = None
+        self.project = None
+        self.domain = None
+        self.asn = None
+        self.fabric = None
+        self.vni_range = None
+        if config:
+            auth_url = config.get("auth_url")
+            overlay_url = config.get("overlay_url")
+            self.project = config.get("project")
+            self.domain = config.get("domain")
+            self.asn = config.get("asn")
+            self.fabric = config.get("fabric")
+            self.vni_range = config.get("vni_range")
+
+        if not url:
+            raise SdnConnectorError("'url' must be provided")
+        if not url.startswith("http"):
+            url = "http://" + url
+        if not url.endswith("/"):
+            url = url + "/"
+        self.url = url
+
+        if not self.project:
+            raise SdnConnectorError("'project' must be provided")
+        if not self.asn:
+            # TODO: Get ASN from controller config; otherwise raise ERROR for the moment
+            raise SdnConnectorError("'asn' was not provided and was not possible to obtain it")
+        if not self.fabric:
+            # TODO: Get FABRIC from controller config; otherwise raise ERROR for the moment
+            raise SdnConnectorError("'fabric' was not provided and was not possible to obtain it")
+        if not self.domain:
+            self.domain = 'default'
+            self.logger.info("No domain was provided. Using 'default'")
+        if not self.vni_range:
+            self.vni_range = '1000001-2000000'
+            self.logger.info("No vni_range was provided. Using '1000001-2000000'")
+
+        if overlay_url:
+            if not overlay_url.startswith("http"):
+                overlay_url = "http://" + overlay_url
+            if not overlay_url.endswith("/"):
+                overlay_url = overlay_url + "/"
+        self.overlay_url = overlay_url
+
+        if auth_url:
+            if not auth_url.startswith("http"):
+                auth_url = "http://" + auth_url
+            if not auth_url.endswith("/"):
+                auth_url = auth_url + "/"
+        self.auth_url = auth_url
+
+        # Init http lib
+        self.http = Http(self.logger)
+
+        # Init http headers for all requests
+        self.headers = {'Content-Type': 'application/json'}
+        self.http_header = ['{}: {}'.format(key, val)
+                           for (key, val) in list(self.headers.items())]
+
+        auth_dict = {}
+        auth_dict['auth'] = {}
+        auth_dict['auth']['scope'] = {}
+        auth_dict['auth']['scope']['project'] = {}
+        auth_dict['auth']['scope']['project']['domain'] = {}
+        auth_dict['auth']['scope']['project']['domain']["id"] = self.domain
+        auth_dict['auth']['scope']['project']['name'] = self.project
+        auth_dict['auth']['identity'] = {}
+        auth_dict['auth']['identity']['methods'] = ['password']
+        auth_dict['auth']['identity']['password'] = {}
+        auth_dict['auth']['identity']['password']['user'] = {}
+        auth_dict['auth']['identity']['password']['user']['name'] = self.user
+        auth_dict['auth']['identity']['password']['user']['password'] = self.password
+        auth_dict['auth']['identity']['password']['user']['domain'] = {}
+        auth_dict['auth']['identity']['password']['user']['domain']['id'] = self.domain
+        self.auth_dict = auth_dict
+        self.token = None
+
+        self.logger.info("Juniper Contrail Connector Initialized.")
+
+
+    def _get_token(self):
+        self.logger.debug('Current Token:'.format(str(self.token)))
+        auth_url = self.auth_url + 'auth/tokens'
+        if self.token is None:
+            if not self.auth_url:
+                self.token = ""
+            http_code, resp = self.http.post_cmd(url=auth_url, headers=self.http_header,
+                                                 postfields_dict=self.auth_dict,
+                                                 return_header = 'x-subject-token')
+            self.token = resp
+            self.logger.debug('Token: '.format(self.token))
+
+            if self.token:
+                self.headers['X-Auth-Token'] = self.token
+            else:
+                self.headers.pop('X-Auth-Token', None)
+            http_header = ['{}: {}'.format(key, val)
+                           for (key, val) in list(self.headers.items())]
+
+    
+    # Aux functions for testing
+    def get_url(self):
+        return self.url
+
+
+    def get_overlay_url(self):
+        return self.overlay_url
+
+    # Virtual network operations
+
+    def _create_virtual_network(self, controller_url, name, vni):
+        routetarget = '{}:{}'.format(self.asn,vni)
+        vnet_dict = {
+                        "virtual-network": {
+                            "virtual_network_properties": {
+                                "vxlan_network_identifier": vni,
+                            },
+                            "parent_type": "project",
+                            "fq_name": [
+                                "default-domain",
+                                "admin",
+                                name
+                            ],
+                            "route_target_list": {
+                                "route_target": [
+                                    "target:" + routetarget
+                                ]
+                            }
+                        }
+                    }
+        self._get_token()
+        endpoint = controller_url + 'virtual-networks'
+        http_code, resp = self.http.post_cmd(url = endpoint,
+                                             headers = self.http_header,
+                                             postfields_dict = vnet_dict)
+        if http_code not in (200, 201, 202, 204) or not resp:
+           raise SdnConnectorError('Unexpected http status code, or empty response')
+        vnet_info = json.loads(resp)
+        self.logger.debug("vnet_info: {}".format(vnet_info))
+        return vnet_info.get("virtual-network").get('uuid'), vnet_info.get("virtual-network")
+
+
+    def _get_virtual_networks(self, controller_url):
+        self._get_token()
+        endpoint = controller_url + 'virtual-networks'
+        print(endpoint)
+        http_code, resp = self.http.get_cmd(url=endpoint, headers=self.http_header)
+        if http_code not in (200, 201, 202, 204) or not resp:
+            raise SdnConnectorError('Unexpected http status code, or empty response')
+        vnets_info = json.loads(resp)
+        self.logger.debug("vnets_info: {}".format(vnets_info))
+        return vnets_info.get('virtual-networks')
+
+
+    def _get_virtual_network(self, controller_url, network_id):
+        self._get_token()
+        endpoint = controller_url + 'virtual-network/{}'.format(network_id)
+        http_code, resp = self.http.get_cmd(url=endpoint, headers=self.http_header)
+        if http_code not in (200, 201, 202, 204) or not resp:
+            if http_code == 404:
+                return None
+            raise SdnConnectorError('Unexpected http status code, or empty response')
+        vnet_info = json.loads(resp)
+        self.logger.debug("vnet_info: {}".format(vnet_info))
+        return vnet_info.get("virtual-network")
+
+
+    def _delete_virtual_network(self, controller_url, network_id):
+        self._get_token()
+        endpoint = controller_url + 'virtual-network/{}'.format(network_id)
+        http_code, _ = self.http.delete_cmd(url=endpoint, headers=self.http_header)
+        if http_code not in (200, 201, 202, 204):
+            raise SdnConnectorError('Unexpected http status code')
+        return
+
+
+    # Virtual port group operations
+
+    def _create_vpg(self, controller_url, switch_id, switch_port, network, vlan):
+        vpg_dict = {
+                       "virtual-port-group": {
+                       }
+                   }
+        self._get_token()
+        endpoint = controller_url + 'virtual-port-groups'
+        http_code, resp = self.http.post_cmd(url = endpoint,
+                                               headers = self.http_header,
+                                               postfields_dict = vpg_dict)
+        if http_code not in (200, 201, 202, 204) or not resp:
+           raise SdnConnectorError('Unexpected http status code, or empty response')
+        vpg_info = json.loads(resp)
+        self.logger.debug("vpg_info: {}".format(vpg_info))
+        return vpg_info.get("virtual-port-group").get('uuid'), vpg_info.get("virtual-port-group")
+
+
+    def _get_vpgs(self, controller_url):
+        self._get_token()
+        endpoint = controller_url + 'virtual-port-groups'
+        http_code, resp = self.http.get_cmd(url=endpoint, headers=self.http_header)
+        if http_code not in (200, 201, 202, 204) or not resp:
+            raise SdnConnectorError('Unexpected http status code, or empty response')
+        vpgs_info = json.loads(resp)
+        self.logger.debug("vpgs_info: {}".format(vpgs_info))
+        return vpgs_info.get('virtual-port-groups')
+
+
+    def _get_vpg(self, controller_url, vpg_id):
+        self._get_token()
+        endpoint = controller_url + 'virtual-port-group/{}'.format(vpg_id)
+        http_code, resp = self.http.get_cmd(url=endpoint, headers=self.http_header)
+        if http_code not in (200, 201, 202, 204) or not resp:
+            if http_code == 404:
+                return None
+            raise SdnConnectorError('Unexpected http status code, or empty response')
+        vpg_info = json.loads(resp)
+        self.logger.debug("vpg_info: {}".format(vpg_info))
+        return vpg_info.get("virtual-port-group")
+
+
+    def _delete_vpg(self, controller_url, vpg_id):
+        self._get_token()
+        endpoint = controller_url + 'virtual-port-group/{}'.format(vpg_id)
+        http_code, resp = self.http.delete_cmd(url=endpoint, headers=self.http_header)
+        if http_code not in (200, 201, 202, 204):
+            raise SdnConnectorError('Unexpected http status code')
+        return
+
+
+    def check_credentials(self):
+        """Check if the connector itself can access the SDN/WIM with the provided url (wim.wim_url),
+            user (wim_account.user), and password (wim_account.password)
+
+        Raises:
+            SdnConnectorError: Issues regarding authorization, access to
+                external URLs, etc are detected.
+        """
+        self.logger.debug("")
+        self._get_token()
+        try:
+            http_code, resp = self.http.get_cmd(url=self.auth_url, headers=self.http_header)
+            if http_code not in (200, 201, 202, 204) or not resp:
+                raise SdnConnectorError('Unexpected http status code, or empty response')
+        except Exception as e:
+            self.logger.error('Error checking credentials')
+            raise SdnConnectorError('Error checking credentials', http_code=http_code)
+
+
+    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.
+        """
+        self.logger.debug("")
+        self._get_token()
+        try:
+            http_code, resp = self.http.get_cmd(endpoint='virtual-network/{}'.format(service_uuid))
+            if http_code not in (200, 201, 202, 204) or not resp:
+                raise SdnConnectorError('Unexpected http status code, or empty response')
+            if resp:
+                vnet_info = json.loads(resp)
+                return {'sdn_status': 'ACTIVE', 'sdn_info': vnet_info['virtual-network']}
+            else:
+                return {'sdn_status': 'ERROR', 'sdn_info': 'not found'}
+        except Exception as e:
+            self.logger.error('Exception getting connectivity service info: %s', e)
+            return {'sdn_status': 'ERROR', 'error_msg': str(e)}
+
+
+    def create_connectivity_service(self, service_type, connection_points, **kwargs):
+        """
+        Establish SDN/WAN connectivity between the endpoints
+        :param service_type: (str): ``ELINE`` (L2), ``ELAN`` (L2), ``ETREE`` (L2), ``L3``.
+        :param connection_points:  (list): each point corresponds to
+            an entry point to be connected. For WIM: from the DC to the transport network.
+            For SDN: Compute/PCI to the transport network. One
+            connection point serves to identify the specific access and
+            some other service parameters, such as encapsulation type.
+            Each item of the list is a dict with:
+                "service_endpoint_id": (str)(uuid)  Same meaning that for 'service_endpoint_mapping' (see __init__)
+                    In case the config attribute mapping_not_needed is True, this value is not relevant. In this case
+                    it will contain the string "device_id:device_interface_id"
+                "service_endpoint_encapsulation_type": None, "dot1q", ...
+                "service_endpoint_encapsulation_info": (dict) with:
+                    "vlan": ..., (int, present if encapsulation is dot1q)
+                    "vni": ... (int, present if encapsulation is vxlan),
+                    "peers": [(ipv4_1), (ipv4_2)] (present if encapsulation is vxlan)
+                    "mac": ...
+                    "device_id": ..., same meaning that for 'service_endpoint_mapping' (see __init__)
+                    "device_interface_id": same meaning that for 'service_endpoint_mapping' (see __init__)
+                    "switch_dpid": ..., present if mapping has been found for this device_id,device_interface_id
+                    "switch_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
+        """
+        self.logger.debug("create_connectivity_service, service_type: {}, connection_points: {}".
+                          format(service_type, connection_points))
+        if service_type.lower() != 'elan':
+            raise SdnConnectorError('Only ELAN network type is supported by Juniper Contrail.')
+
+        # Step 1. Check in the overlay controller the virtual network created by the VIM
+        #   Best option: get network id of the VIM as param (if the VIM already created the network),
+        #    and do a request to the controller of the virtual networks whose VIM network id is the provided
+        #   Next best option: obtain the network by doing a request to the controller
+        #    of the virtual networks using the VLAN ID of any service endpoint.
+        #   1.1 Read VLAN ID from a service endpoint
+        #   1.2 Look for virtual networks with "Provider Network" including a VLAN ID.
+        #   1.3 If more than one, ERROR
+        # Step 2. Modify the existing virtual network in the overlay controller
+        #   2.1 Add VNI (VxLAN Network Identifier - one free from the provided range)
+        #   2.2 Add RouteTarget (RT) ('ASN:VNI', ASN = Autonomous System Number, provided as param or read from controller config)
+        # Step 3. Create a virtual network in the underlay controller
+        #   3.1 Create virtual network (name, VNI, RT)
+        #      If the network already existed in the overlay controller, we should use the same name
+        #         name = 'osm-plugin-' + overlay_name
+        #      Else:
+        #         name = 'osm-plugin-' + VNI
+        try:
+            name = 'test-test-1'
+            vni = 999999
+            network_id, network_info = self._create_virtual_network(self.url, name, vni)
+        except SdnConnectorError:
+            raise SdnConnectorError('Failed to create connectivity service {}'.format(name))
+        except Exception as e:
+            self.logger.error('Exception creating connection_service: %s', e, exc_info=True)
+            raise SdnConnectorError("Exception creating connectivity service: {}".format(str(e)))
+        return service_id
+
+
+    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.
+        """
+        #TODO: to be done. This comes from ONOS VPLS plugin
+        self.logger.debug("edit connectivity service, service_uuid: {}, conn_info: {}, "
+                          "connection points: {} ".format(service_uuid, conn_info, connection_points))
+
+        conn_info = conn_info or []
+        # Obtain current configuration
+        config_orig = self._get_onos_netconfig()
+        config = copy.deepcopy(config_orig)
+
+        # get current service data and check if it does not exists
+        #TODO: update
+        for vpls in config.get('apps', {}).get('org.onosproject.vpls', {}).get('vpls', {}).get('vplsList', {}):
+            if vpls['name'] == service_uuid:
+                self.logger.debug("service exists")
+                curr_interfaces = vpls.get("interfaces", [])
+                curr_encapsulation = vpls.get("encapsulation")
+                break
+        else:
+            raise SdnConnectorError("service uuid: {} does not exist".format(service_uuid))
+
+        self.logger.debug("current interfaces: {}".format(curr_interfaces))
+        self.logger.debug("current encapsulation: {}".format(curr_encapsulation))
+
+        # new interfaces names
+        new_interfaces = [port['service_endpoint_id'] for port in new_connection_points]
+
+        # obtain interfaces to delete, list will contain port
+        ifs_delete = list(set(curr_interfaces) - set(new_interfaces))
+        ifs_add = list(set(new_interfaces) - set(curr_interfaces))
+        self.logger.debug("interfaces to delete: {}".format(ifs_delete))
+        self.logger.debug("interfaces to add: {}".format(ifs_add))
+
+        # check if some data of the interfaces that already existed has changed
+        # in that case delete it and add it again
+        ifs_remain = list(set(new_interfaces) & set(curr_interfaces))
+        for port in connection_points:
+            if port['service_endpoint_id'] in ifs_remain:
+                # check if there are some changes
+                curr_port_name, curr_vlan = self._get_current_port_data(config, port['service_endpoint_id'])
+                new_port_name = 'of:{}/{}'.format(port['service_endpoint_encapsulation_info']['switch_dpid'],
+                                        port['service_endpoint_encapsulation_info']['switch_port'])
+                new_vlan = port['service_endpoint_encapsulation_info']['vlan']
+                if (curr_port_name != new_port_name or curr_vlan != new_vlan):
+                    self.logger.debug("TODO: must update data interface: {}".format(port['service_endpoint_id']))
+                    ifs_delete.append(port['service_endpoint_id'])
+                    ifs_add.append(port['service_endpoint_id'])
+
+        new_encapsulation = self._get_encapsulation(connection_points)
+
+        try:
+            # Delete interfaces, only will delete interfaces that are in provided conn_info
+            # because these are the ones that have been created for this service
+            if ifs_delete:
+                for port in config['ports'].values():
+                    for port_interface in port['interfaces']:
+                        interface_name = port_interface['name']
+                        self.logger.debug("interface name: {}".format(port_interface['name']))
+                        if interface_name in ifs_delete and interface_name in conn_info:
+                            self.logger.debug("delete interface name: {}".format(interface_name))
+                            port['interfaces'].remove(port_interface)
+                            conn_info.remove(interface_name)
+
+            # Add new interfaces
+            for port in connection_points:
+                if port['service_endpoint_id'] in ifs_add:
+                    created_ifz = self._append_port_to_config(port, config)
+                    if created_ifz:
+                        conn_info.append(created_ifz[1])
+            self._pop_last_update_time(config)
+            self._post_netconfig(config)
+
+            self.logger.debug("contrail config after updating interfaces: {}".format(config))
+            self.logger.debug("conn_info after updating interfaces: {}".format(conn_info))
+
+            # Update interfaces list in vpls service
+            for vpls in config.get('apps', {}).get('org.onosproject.vpls', {}).get('vpls', {}).get('vplsList', {}):
+                if vpls['name'] == service_uuid:
+                    vpls['interfaces'] = new_interfaces
+                    vpls['encapsulation'] = new_encapsulation
+
+            self._pop_last_update_time(config)
+            self._post_netconfig(config)
+            return conn_info
+        except Exception as e:
+            self.logger.error('Exception add connection_service: %s', e)
+            # try to rollback push original config
+            try:
+                self._post_netconfig(config_orig)
+            except Exception as e2:
+                self.logger.error('Exception rolling back to original config: %s', e2)
+            # raise exception
+            if isinstance(e, SdnConnectorError):
+                raise
+            else:
+                raise SdnConnectorError("Exception create_connectivity_service: {}".format(e))
+
+
+    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
+        """
+        self.logger.debug("delete_connectivity_service uuid: {}".format(service_uuid))
+        try:
+            #TO DO: check if virtual port groups have to be deleted
+            self._delete_virtual_network(self.url, service_uuid)
+        except SdnConnectorError:
+            raise SdnConnectorError('Failed to delete service uuid {}'.format(service_uuid))
+        except Exception as e:
+            self.logger.error('Exception deleting connection_service: %s', e, exc_info=True)
+            raise SdnConnectorError("Exception deleting connectivity service: {}".format(str(e)))
+
+
+if __name__ == '__main__':
+    # Init logger
+    log_format = "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(funcName)s(): %(message)s"
+    log_formatter = logging.Formatter(log_format, datefmt='%Y-%m-%dT%H:%M:%S')
+    handler = logging.StreamHandler()
+    handler.setFormatter(log_formatter)
+    logger = logging.getLogger('openmano.sdnconn.junipercontrail')
+    #logger.setLevel(level=logging.ERROR)
+    logger.setLevel(level=logging.INFO)
+    #logger.setLevel(level=logging.DEBUG)
+    logger.addHandler(handler)
+
+    # Read config
+    with open('test.yaml') as f:
+        config = yaml.safe_load(f.read())
+    wim = {'wim_url': config.pop('wim_url')}
+    wim_account = {'user': config.pop('user'), 'password': config.pop('password')}
+    logger.info('wim: {}, wim_account: {}, config: {}'.format(wim, wim_account, config))
+
+    # Init controller
+    juniper_contrail = JuniperContrail(wim=wim, wim_account=wim_account, config=config, logger=logger)
+
+    # Tests
+    # 0. Check credentials
+    print('0. Check credentials')
+    juniper_contrail.check_credentials()
+
+    underlay_url = juniper_contrail.get_url()
+    overlay_url = juniper_contrail.get_overlay_url()
+    # 1. Read virtual networks from overlay controller
+    print('1. Read virtual networks from overlay controller')
+    try:
+        vnets = juniper_contrail._get_virtual_networks(overlay_url)
+        logger.debug(yaml.safe_dump(vnets, indent=4, default_flow_style=False))
+        print('OK')
+    except Exception as e:
+        logger.error('Exception reading virtual networks from overlay controller: %s', e)
+        print('FAILED')
+
+    # 2. Read virtual networks from underlay controller
+    print('2. Read virtual networks from underlay controller')
+    vnets = juniper_contrail._get_virtual_networks(underlay_url)
+    logger.debug(yaml.safe_dump(vnets, indent=4, default_flow_style=False))
+    print('OK')
+    # 3. Delete virtual networks gerardoX from underlay controller
+    print('3. Delete virtual networks gerardoX from underlay controller')
+    for vn in vnets:
+        name = vn['fq_name'][2]
+        logger.debug('Virtual network: {}'.format(name))
+    for vn in vnets:
+        name = vn['fq_name'][2]
+        if 'gerardo' in name:
+            logger.info('Virtual Network *gerardo*: {}, {}'.format(name,vn['uuid']))
+            if name != "gerardo":
+                print('Deleting Virtual Network: {}, {}'.format(name,vn['uuid']))
+                logger.info('Deleting Virtual Network: {}, {}'.format(name,vn['uuid']))
+                juniper_contrail._delete_virtual_network(underlay_url, vn['uuid'])
+                print('OK')
+    # 4. Get virtual network (gerardo) from underlay controller
+    print('4. Get virtual network (gerardo) from underlay controller')
+    vnet1_info = juniper_contrail._get_virtual_network(underlay_url, 'c5d332f7-420a-4e2b-a7b1-b56a59f20c97')
+    print(yaml.safe_dump(vnet1_info, indent=4, default_flow_style=False))
+    print('OK')
+    # 5. Create virtual network in underlay controller
+    print('5. Create virtual network in underlay controller')
+    myname = 'gerardo4'
+    myvni = 20004
+    vnet2_id, _ = juniper_contrail._create_virtual_network(underlay_url, myname, myvni)
+    vnet2_info = juniper_contrail._get_virtual_network(underlay_url, vnet2_id)
+    print(yaml.safe_dump(vnet2_info, indent=4, default_flow_style=False))
+    print('OK')
+    # 6. Delete virtual network in underlay controller
+    print('6. Delete virtual network in underlay controller')
+    juniper_contrail._delete_virtual_network(underlay_url, vnet2_id)
+    print('OK')
+    # 7. Read previously deleted virtual network in underlay controller
+    print('7. Read previously deleted virtual network in underlay controller')
+    try:
+        vnet2_info = juniper_contrail._get_virtual_network(underlay_url, vnet2_id)
+        if vnet2_info:
+            print('FAILED. Network {} exists'.format(vnet2_id))
+        else:
+            print('OK. Network {} does not exist because it has been deleted'.format(vnet2_id))
+    except Exception as e:
+        logger.info('Exception reading virtual networks from overlay controller: %s', e)
+    exit(0)
+
+
+    #TODO: to be deleted (it comes from ONOS VPLS plugin)
+    service_type = 'ELAN'
+    conn_point_0 = {
+        "service_endpoint_id": "switch1:ifz1",
+        "service_endpoint_encapsulation_type": "dot1q",
+        "service_endpoint_encapsulation_info": {
+            "switch_dpid": "0000000000000011",
+            "switch_port": "1",
+            "vlan": "600"
+        }
+    }
+    conn_point_1 = {
+        "service_endpoint_id": "switch3:ifz1",
+        "service_endpoint_encapsulation_type": "dot1q",
+        "service_endpoint_encapsulation_info": {
+            "switch_dpid": "0000000000000031",
+            "switch_port": "3",
+            "vlan": "600"
+        }
+    }
+    connection_points = [conn_point_0, conn_point_1]
+    # service_uuid, created_items = juniper_contrail.create_connectivity_service(service_type, connection_points)
+    #print(service_uuid)
+    #print(created_items)
+    #sleep(10)
+    #juniper_contrail.delete_connectivity_service("5496dfea-27dc-457d-970d-b82bac266e5c"))
+
+
+    conn_info = None
+    conn_info = ['switch1:ifz1', 'switch3_ifz3']
+    juniper_contrail.delete_connectivity_service("f7afc4de-556d-4b5a-8a12-12b5ef97d269", conn_info)
+
+    conn_point_0 = {
+        "service_endpoint_id": "switch1:ifz1",
+        "service_endpoint_encapsulation_type": "dot1q",
+        "service_endpoint_encapsulation_info": {
+            "switch_dpid": "0000000000000011",
+            "switch_port": "1",
+            "vlan": "500"
+        }
+    }
+    conn_point_2 = {
+        "service_endpoint_id": "switch1:ifz3",
+        "service_endpoint_encapsulation_type": "dot1q",
+        "service_endpoint_encapsulation_info": {
+            "switch_dpid": "0000000000000011",
+            "switch_port": "3",
+            "vlan": "500"
+        }
+    }
+    conn_point_3 = {
+        "service_endpoint_id": "switch3_ifz3",
+        "service_endpoint_encapsulation_type": "dot1q",
+        "service_endpoint_encapsulation_info": {
+            "switch_dpid": "0000000000000033",
+            "switch_port": "3",
+            "vlan": "500"
+        }
+    }
+    new_connection_points = [conn_point_0, conn_point_3]
+    #conn_info = juniper_contrail.edit_connectivity_service("f7afc4de-556d-4b5a-8a12-12b5ef97d269", conn_info, new_connection_points)
+    #print(conn_info)
diff --git a/RO-SDN-juniper_contrail/requirements.txt b/RO-SDN-juniper_contrail/requirements.txt
new file mode 100644 (file)
index 0000000..08b8919
--- /dev/null
@@ -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.
+##
+
+#requests
+pycurl
+git+https://osm.etsi.org/gerrit/osm/RO.git#egg=osm-ro&subdirectory=RO
+
diff --git a/RO-SDN-juniper_contrail/setup.py b/RO-SDN-juniper_contrail/setup.py
new file mode 100644 (file)
index 0000000..fdce2ac
--- /dev/null
@@ -0,0 +1,57 @@
+#!/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_juniper_contrail"
+
+README = """
+===========
+osm-rosdn_juniper_contrail
+===========
+
+osm-ro plugin for Juniper Contrail SDN
+"""
+
+setup(
+    name=_name,
+    description='OSM RO SDN plugin for Juniper Contrail',
+    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='OSM_TECH@list.etsi.org',
+    maintainer='ETSI OSM',
+    maintainer_email='OSM_TECH@list.etsi.org',
+    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=[
+        "pycurl",
+        #"requests",
+        "osm-ro @ git+https://osm.etsi.org/gerrit/osm/RO.git#egg=osm-ro&subdirectory=RO"
+    ],
+    setup_requires=['setuptools-version-command'],
+    entry_points={
+        'osm_rosdn.plugins': ['rosdn_juniper_contrail = osm_rosdn_juniper_contrail.sdn_assist_juniper_contrail:JuniperContrail'],
+    },
+)
diff --git a/RO-SDN-juniper_contrail/stdeb.cfg b/RO-SDN-juniper_contrail/stdeb.cfg
new file mode 100644 (file)
index 0000000..87177f8
--- /dev/null
@@ -0,0 +1,20 @@
+#
+# 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-pycurl, python3-osm-ro
+#Depends3: python3-requests, python3-osm-ro
+
diff --git a/RO-SDN-juniper_contrail/tox.ini b/RO-SDN-juniper_contrail/tox.ini
new file mode 100644 (file)
index 0000000..dfa3fde
--- /dev/null
@@ -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_juniper_contrail --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_juniper_contrail.tests
+
+[testenv:build]
+basepython = python3
+deps = stdeb
+       setuptools-version-command
+commands = python3 setup.py --command-packages=stdeb.command bdist_deb
+