From f4f95e12de51b57f839a0340519412465bc4d7e3 Mon Sep 17 00:00:00 2001 From: garciadeblas Date: Fri, 17 Apr 2020 05:55:46 +0000 Subject: [PATCH] Juniper Contrail SDN plugin Change-Id: I14987a321218b5cda98e6c4be68ae4a86596a2de Signed-off-by: garciadeblas --- Dockerfile-local | 1 + RO-SDN-juniper_contrail/Makefile | 24 + .../osm_rosdn_juniper_contrail/rest_lib.py | 146 ++++ .../sdn_assist_juniper_contrail.py | 734 ++++++++++++++++++ RO-SDN-juniper_contrail/requirements.txt | 19 + RO-SDN-juniper_contrail/setup.py | 57 ++ RO-SDN-juniper_contrail/stdeb.cfg | 20 + RO-SDN-juniper_contrail/tox.ini | 41 + 8 files changed, 1042 insertions(+) create mode 100644 RO-SDN-juniper_contrail/Makefile create mode 100644 RO-SDN-juniper_contrail/osm_rosdn_juniper_contrail/rest_lib.py create mode 100644 RO-SDN-juniper_contrail/osm_rosdn_juniper_contrail/sdn_assist_juniper_contrail.py create mode 100644 RO-SDN-juniper_contrail/requirements.txt create mode 100644 RO-SDN-juniper_contrail/setup.py create mode 100644 RO-SDN-juniper_contrail/stdeb.cfg create mode 100644 RO-SDN-juniper_contrail/tox.ini diff --git a/Dockerfile-local b/Dockerfile-local index c0072094..1e4e29b5 100644 --- a/Dockerfile-local +++ b/Dockerfile-local @@ -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 index 00000000..ad691c6a --- /dev/null +++ b/RO-SDN-juniper_contrail/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_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 index 00000000..18c3bd21 --- /dev/null +++ b/RO-SDN-juniper_contrail/osm_rosdn_juniper_contrail/rest_lib.py @@ -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 index 00000000..e29ac79a --- /dev/null +++ b/RO-SDN-juniper_contrail/osm_rosdn_juniper_contrail/sdn_assist_juniper_contrail.py @@ -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 index 00000000..08b8919e --- /dev/null +++ b/RO-SDN-juniper_contrail/requirements.txt @@ -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 index 00000000..fdce2acb --- /dev/null +++ b/RO-SDN-juniper_contrail/setup.py @@ -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 index 00000000..87177f8c --- /dev/null +++ b/RO-SDN-juniper_contrail/stdeb.cfg @@ -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 index 00000000..dfa3fde4 --- /dev/null +++ b/RO-SDN-juniper_contrail/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_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 + -- 2.25.1