From a5c26d8be1a5896675f7eb07c6afeb1aa26c7172 Mon Sep 17 00:00:00 2001 From: lloretgalleg Date: Thu, 7 May 2020 17:02:25 +0200 Subject: [PATCH] Developing juniper contrail plugin, implenting CRUD operations and first version of create, edit and delete connectivity service Change-Id: I05435d4e5c4fab87184b057472ac12610c1071c9 Signed-off-by: lloretgalleg --- .../osm_rosdn_juniper_contrail/rest_lib.py | 269 +++--- .../osm_rosdn_juniper_contrail/sdn_api.py | 305 +++++++ .../sdn_assist_juniper_contrail.py | 830 ++++++++++-------- 3 files changed, 909 insertions(+), 495 deletions(-) create mode 100644 RO-SDN-juniper_contrail/osm_rosdn_juniper_contrail/sdn_api.py 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 index 18c3bd21..7ec2401a 100644 --- a/RO-SDN-juniper_contrail/osm_rosdn_juniper_contrail/rest_lib.py +++ b/RO-SDN-juniper_contrail/osm_rosdn_juniper_contrail/rest_lib.py @@ -14,133 +14,190 @@ # License for the specific language governing permissions and limitations # under the License. -from io import BytesIO -import pycurl +import requests import json import copy +import logging + +from time import time +from requests.exceptions import ConnectionError class HttpException(Exception): pass + class NotFound(HttpException): pass -class Http(object): - def __init__(self, logger): - self._logger = logger - self._response_headers = None +class AuthError(HttpException): + pass + +class DuplicateFound(HttpException): + pass - 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)) +class ServiceUnavailableException(HttpException): + pass - 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 +class ContrailHttp(object): + + def __init__(self, auth_info, logger): + self._logger = logger + # default don't verify client cert + self._ssl_verify = False + # auth info: must contain auth_url and auth_dict + self.auth_url = auth_info["auth_url"] + self.auth_dict = auth_info["auth_dict"] + + self.max_retries = 3 + + # Default token timeout + self.token_timeout = 3500 + self.token = None + # TODO - improve configuration timeouts 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 - + resp = self._request("GET", url, headers) + return resp.json() - def delete_cmd(self, url, headers): + def post_headers_cmd(self, url, headers, post_fields_dict=None): 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 + # obfuscate password before logging dict + if post_fields_dict.get('auth', {}).get('identity', {}).get('password', {}).get('user', {}).get('password'): + post_fields_dict_copy = copy.deepcopy(post_fields_dict) + post_fields_dict['auth']['identity']['password']['user']['password'] = '******' + json_data_log = post_fields_dict_copy 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) + json_data_log = post_fields_dict + self._logger.debug("Request POSTFIELDS: {}".format(json.dumps(json_data_log))) + resp = self._request("POST_HEADERS", url, headers, data=post_fields_dict) + return resp.text + + def post_cmd(self, url, headers, post_fields_dict=None): + self._logger.debug("") + # obfuscate password before logging dict + if post_fields_dict.get('auth', {}).get('identity', {}).get('password', {}).get('user', {}).get('password'): + post_fields_dict_copy = copy.deepcopy(post_fields_dict) + post_fields_dict['auth']['identity']['password']['user']['password'] = '******' + json_data_log = post_fields_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 + json_data_log = post_fields_dict + self._logger.debug("Request POSTFIELDS: {}".format(json.dumps(json_data_log))) + resp = self._request("POST", url, headers, data=post_fields_dict) + return resp.text + + def delete_cmd(self, url, headers): + self._logger.debug("") + resp = self._request("DELETE", url, headers) + return resp.text + + def _get_token(self, headers): + self._logger.debug('Current Token:'.format(self.token)) + auth_url = self.auth_url + 'auth/tokens' + if self.token is None or self._token_expired(): + if not self.auth_url: + self.token = "" + resp = self._request_noauth(url=auth_url, op="POST", headers=headers, + data=self.auth_dict) + self.token = resp.headers.get('x-subject-token') + self.last_token_time = time.time() + self._logger.debug('Obtained token: '.format(self.token)) + + return self.token + + def _token_expired(self): + current_time = time.time() + if self.last_token_time and (current_time - self.last_token_time < self.token_timeout): + return False else: - return http_code, None + return True + + def _request(self, op, url, http_headers, data=None, retry_auth_error=True): + headers = http_headers.copy() + + # Get authorization (include authentication headers) + # todo - añadir token de nuevo + #token = self._get_token(headers) + token = None + if token: + headers['X-Auth-Token'] = token + try: + return self._request_noauth(op, url, headers, data) + except AuthError: + # If there is an auth error retry just once + if retry_auth_error: + return self._request(self, op, url, headers, data, retry_auth_error=False) + + def _request_noauth(self, op, url, headers, data=None): + # Method to execute http requests with error control + # Authentication error, always make just one retry + # ConnectionError or ServiceUnavailable make configured retries with sleep between them + # Other errors to raise: + # - NotFound + # - Conflict + + retry = 0 + while retry < self.max_retries: + retry += 1 + + # Execute operation + try: + self._logger.info("Request METHOD: {} URL: {}".format(op, url)) + if (op == "GET"): + resp = self._http_get(url, headers, query_params=data) + elif (op == "POST"): + resp = self._http_post(url, headers, json_data=data) + elif (op == "POST_HEADERS"): + resp = self._http_post_headers(url, headers, json_data=data) + elif (op == "DELETE"): + resp = self._http_delete(url, headers, json_data=data) + else: + raise HttpException("Unsupported operation: {}".format(op)) + self._logger.info("Response HTTPCODE: {}".format(resp.status_code)) + + # Check http return code + if resp: + return resp + else: + status_code = resp.status_code + if status_code == 401: + # Auth Error - set token to None to reload it and raise AuthError + self.token = None + raise AuthError("Auth error executing operation") + elif status_code == 409: + raise DuplicateFound("Duplicate resource url: {}, response: {}".format(url, resp.text)) + elif status_code == 404: + raise NotFound("Not found resource url: {}, response: {}".format(url, resp.text)) + elif resp.status_code in [502, 503]: + if not self.max_retries or retry >= self.max_retries: + raise ServiceUnavailableException("Service unavailable error url: {}".format(url)) + + continue + else: + raise HttpException("Error status_code: {}, error_text: {}".format(resp.status_code, resp.text)) + + except ConnectionError as e: + self._logger.error("Connection error executing request: {}".format(repr(e))) + if not self.max_retries or retry >= self.max_retries: + raise ConnectionError + continue + except Exception as e: + self._logger.error("Error executing request: {}".format(repr(e))) + raise e + + def _http_get(self, url, headers, query_params=None): + return requests.get(url, headers=headers, params=query_params) + + def _http_post_headers(self, url, headers, json_data=None): + return requests.head(url, json=json_data, headers=headers, verify=False) + + def _http_post(self, url, headers, json_data=None): + return requests.post(url, json=json_data, headers=headers, verify=False) + + def _http_delete(self, url, headers, json_data=None): + return requests.delete(url, json=json_data, headers=headers) diff --git a/RO-SDN-juniper_contrail/osm_rosdn_juniper_contrail/sdn_api.py b/RO-SDN-juniper_contrail/osm_rosdn_juniper_contrail/sdn_api.py new file mode 100644 index 00000000..1c5b5285 --- /dev/null +++ b/RO-SDN-juniper_contrail/osm_rosdn_juniper_contrail/sdn_api.py @@ -0,0 +1,305 @@ +# 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. + +import logging +import json + +from osm_ro.wim.sdnconn import SdnConnectorError +from osm_rosdn_juniper_contrail.rest_lib import ContrailHttp +from osm_rosdn_juniper_contrail.rest_lib import NotFound +from osm_rosdn_juniper_contrail.rest_lib import DuplicateFound +from osm_rosdn_juniper_contrail.rest_lib import HttpException + +class UnderlayApi: + """ Class with CRUD operations for the underlay API """ + + def __init__(self, url, config=None, user=None, password=None, logger=None): + + self.logger = logger or logging.getLogger("openmano.sdnconn.junipercontrail.sdnapi") + self.controller_url = url + + 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 + + auth_url = None + self.project = None + self.domain = None + self.asn = None + self.fabric = None + if config: + self.auth_url = config.get("auth_url") + self.project = config.get("project") + self.domain = config.get("domain") + self.asn = config.get("asn") + self.fabric = config.get("fabric") + + # Init http headers for all requests + self.http_header = {'Content-Type': 'application/json'} + + if user: + self.user = user + + if password: + self.password = password + + 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 + + # Init http lib + auth_info = {"auth_url": self.auth_url, "auth_dict": auth_dict} + self.http = ContrailHttp(auth_info, self.logger) + + def check_auth(self): + response = self.http.get_cmd(url=self.auth_url, headers=self.http_header) + return response + + # Helper methods for CRUD operations + def get_all_by_type(self, controller_url, type): + endpoint = controller_url + type + response = self.http.get_cmd(url=endpoint, headers=self.http_header) + return response.get(type) + + def get_by_uuid(self, type, uuid): + try: + endpoint = self.controller_url + type + "/{}".format(uuid) + response = self.http.get_cmd(url=endpoint, headers=self.http_header) + return response.get(type) + except NotFound: + return None + + def delete_by_uuid(self, controller_url, type, uuid): + endpoint = controller_url + type + "/{}".format(uuid) + self.http.delete_cmd(url=endpoint, headers=self.http_header) + + def get_uuid_from_fqname(self, type, fq_name): + """ + Obtain uuid from fqname + Returns: If resource not found returns None + In case of error raises an Exception + """ + payload = { + "type": type, + "fq_name": fq_name + } + try: + endpoint = self.controller_url + "fqname-to-id" + resp = self.http.post_cmd(url=endpoint, + headers=self.http_header, + post_fields_dict=payload) + return json.loads(resp).get("uuid") + except NotFound: + return None + + def get_by_fq_name(self, type, fq_name): + # Obtain uuid by fqdn and then get data by uuid + uuid = self.get_uuid_from_fqname(type, fq_name) + if uuid: + return self.get_by_uuid(type, uuid) + else: + return None + + + # Aux methods to avoid code duplication of name conventions + def get_vpg_name(self, switch_id, switch_port): + return "{}_{}".format(switch_id, switch_port) + + def get_vmi_name(self, switch_id, switch_port, vlan): + return "{}_{}-{}".format(switch_id, switch_port, vlan) + + # Virtual network operations + + def create_virtual_network(self, name, vni): + self.logger.debug("create vname, name: {}, vni: {}".format(name, vni)) + routetarget = '{}:{}'.format(self.asn, vni) + vnet_dict = { + "virtual-network": { + "virtual_network_properties": { + "vxlan_network_identifier": vni, + }, + "parent_type": "project", + "fq_name": [ + self.domain, + self.project, + name + ], + "route_target_list": { + "route_target": [ + "target:" + routetarget + ] + } + } + } + endpoint = self.controller_url + 'virtual-networks' + resp = self.http.post_cmd(url=endpoint, + headers=self.http_header, + post_fields_dict=vnet_dict) + if not resp: + raise SdnConnectorError('Error creating virtual network: empty response') + vnet_info = json.loads(resp) + self.logger.debug("created vnet, vnet_info: {}".format(vnet_info)) + return vnet_info.get("virtual-network").get('uuid'), vnet_info.get("virtual-network") + + def get_virtual_networks(self): + return self.get_all_by_type('virtual-networks') + + def get_virtual_network(self, network_id): + return self.get_by_uuid('virtual-network', network_id) + + def delete_virtual_network(self, network_id): + self.logger.debug("delete vnet uuid: {}".format(network_id)) + self.delete_by_uuid(self.controller_url, 'virtual-network', network_id) + self.logger.debug("deleted vnet uuid: {}".format(network_id)) + + # Vpg operations + + def create_vpg(self, switch_id, switch_port): + self.logger.debug("create vpg, switch_id: {}, switch_port: {}".format(switch_id, switch_port)) + vpg_name = self.get_vpg_name(switch_id, switch_port) + vpg_dict = { + "virtual-port-group": { + "parent_type": "fabric", + "fq_name": [ + "default-global-system-config", + self.fabric, + vpg_name + ] + } + } + endpoint = self.controller_url + 'virtual-port-groups' + resp = self.http.post_cmd(url=endpoint, + headers=self.http_header, + post_fields_dict=vpg_dict) + if not resp: + raise SdnConnectorError('Error creating virtual port group: empty response') + vpg_info = json.loads(resp) + self.logger.debug("created vpg, vpg_info: {}".format(vpg_info)) + return vpg_info.get("virtual-port-group").get('uuid'), vpg_info.get("virtual-port-group") + + def get_vpgs(self): + return self.get_all_by_type(self.controller_url, 'virtual-port-groups') + + def get_vpg(self, vpg_id): + return self.get_by_uuid(self.controller_url, "virtual-port-group", vpg_id) + + def get_vpg_by_name(self, vpg_name): + fq_name = [ + "default-global-system-config", + self.fabric, + vpg_name + ] + return self.get_by_fq_name("virtual-port-group", fq_name) + + def delete_vpg(self, vpg_id): + self.logger.debug("delete vpg, uuid: {}".format(vpg_id)) + self.delete_by_uuid(self.controller_url, 'virtual-port-group', vpg_id) + self.logger.debug("deleted vpg, uuid: {}".format(vpg_id)) + + def create_vmi(self, switch_id, switch_port, network, vlan): + self.logger.debug("create vmi, switch_id: {}, switch_port: {}, network: {}, vlan: {}".format( + switch_id, switch_port, network, vlan)) + vmi_name = self.get_vmi_name(switch_id, switch_port, vlan) + vpg_name = self.get_vpg_name(switch_id, switch_port) + profile_dict = { + "local_link_information": [ + { + "port_id": switch_port, + "switch_id": switch_port, + "switch_info": switch_id, + "fabric": self.fabric + } + ] + + } + vmi_dict = { + "virtual-machine-interface": { + "parent_type": "project", + "fq_name": [ + self.domain, + self.project, + vmi_name + ], + "virtual_network_refs": [ + { + "to": [ + self.domain, + self.project, + network + ] + } + ], + "virtual_machine_interface_properties": { + "sub_interface_vlan_tag": vlan + }, + "virtual_machine_interface_bindings": { + "key_value_pair": [ + { + "key": "vnic_type", + "value": "baremetal" + }, + { + "key": "vif_type", + "value": "vrouter" + }, + { + "key": "vpg", + "value": vpg_name + }, + { + "key": "profile", + "value": json.dumps(profile_dict) + } + ] + } + } + } + endpoint = self.controller_url + 'virtual-machine-interfaces' + self.logger.debug("vmi_dict: {}".format(vmi_dict)) + resp = self.http.post_cmd(url=endpoint, + headers=self.http_header, + post_fields_dict=vmi_dict) + if not resp: + raise SdnConnectorError('Error creating vmi: empty response') + vmi_info = json.loads(resp) + self.logger.debug("created vmi, info: {}".format(vmi_info)) + return vmi_info.get("virtual-machine-interface").get('uuid'), vmi_info.get("virtual-machine-interface") + + def get_vmi(self, vmi_uuid): + return self.get_by_uuid(self.controller_url, 'virtual-machine-interface', vmi_uuid) + + def delete_vmi(self, uuid): + self.logger.debug("delete vmi uuid: {}".format(uuid)) + self.delete_by_uuid(self.controller_url, 'virtual-machine-interface', uuid) + self.logger.debug("deleted vmi: {}".format(uuid)) + 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 index 32b80ddb..2ba53bd5 100644 --- 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 @@ -18,18 +18,16 @@ # 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 osm_rosdn_juniper_contrail.rest_lib import ContrailHttp +from osm_rosdn_juniper_contrail.rest_lib import NotFound +from osm_rosdn_juniper_contrail.rest_lib import DuplicateFound +from osm_rosdn_juniper_contrail.rest_lib import HttpException -from io import BytesIO -import pycurl +from osm_rosdn_juniper_contrail.sdn_api import UnderlayApi class JuniperContrail(SdnConnectorBase): @@ -73,21 +71,21 @@ class JuniperContrail(SdnConnectorBase): self.user = wim_account.get("user") self.password = wim_account.get("password") - url = wim.get("wim_url") + url = wim.get("wim_url") # underlay url auth_url = None - overlay_url = None self.project = None self.domain = None self.asn = None self.fabric = None + overlay_url = 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.overlay_url = config.get("overlay_url") self.vni_range = config.get("vni_range") if not url: @@ -98,29 +96,11 @@ class JuniperContrail(SdnConnectorBase): 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']") self.used_vni = set() - 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 @@ -128,34 +108,36 @@ class JuniperContrail(SdnConnectorBase): 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 + 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 - self.logger.info("Juniper Contrail Connector Initialized.") + 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 it 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-domain' + self.logger.info("No domain was provided. Using 'default-domain'") + + underlay_api_config = { + "auth_url": self.auth_url, + "project": self.project, + "domain": self.domain, + "asn": self.asn, + "fabric": self.fabric + } + self.underlay_api = UnderlayApi(url, underlay_api_config, user=self.user, password=self.password, logger=logger) + self._max_duplicate_retry = 2 + self.logger.info("Juniper Contrail Connector Initialized.") def _generate_vni(self): """ @@ -178,154 +160,65 @@ class JuniperContrail(SdnConnectorBase): raise SdnConnectorError("Unable to create the virtual network."\ " All VNI in VNI range {} are in use.".format(self.vni_range)) - - 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 _create_port(self, switch_id, switch_port, network, vlan): + """ + 1 - Look for virtual port groups for provided switch_id, switch_port using name + 2 - It the virtual port group does not exist, create it + 3 - Create virtual machine interface for the indicated network and vlan + """ + self.logger.debug("create_port: switch_id: {}, switch_port: {}, network: {}, vlan: {}".format( + switch_id, switch_port, network, vlan)) + + # 1 - Check if the vpg exists + vpg_name = self.underlay_api.get_vpg_name(switch_id, switch_port) + vpg = self.underlay_api.get_vpg_by_name(vpg_name) + if not vpg: + # 2 - If it does not exist create it + vpg_id, _ = self.underlay_api.create_vpg(switch_id, switch_port) + else: + # Assign vpg_id from vpg + vpg_id = vpg.get("uuid") + + # 3 - Create vmi + vmi_id, _ = self.underlay_api.create_vmi(switch_id, switch_port, network, vlan) + self.logger.debug("port created") + + return vpg_id, vmi_id + + def _delete_port(self, vpg_id, vmi_id): + self.logger.debug("delete port, vpg_id: {}, vmi_id: {}".format(vpg_id, vmi_id)) + # 1 - Obtain vpg by id (if not vpg_id must have been error creating ig, nothing to be done) + if vpg_id: + vpg = self.underlay_api.get_by_uuid("virtual-port-group", vpg_id) + if not vpg: + self.logger.warning("vpg: {} to be deleted not found".format(vpg_id)) + else: + # 2 - Get vmi interfaces from vpg + vmi_list = vpg.get("virtual_machine_interface_refs") + if not vmi_list: + # must have been an error during port creation when vmi is created + # may happen if there has been an error during creation + self.logger.warning("vpg: {} has not vmi, will delete nothing".format(vpg)) + else: + num_vmis = len(vmi_list) + for vmi in vmi_list: + uuid = vmi.get("uuid") + if uuid == vmi_id: + self.underlay_api.delete_vmi(vmi.get("uuid")) + num_vmis = num_vmis - 1 + + # 3 - If there are no more vmi delete the vpg + if not vmi_list or num_vmis == 0: + self.underlay_api.delete_vpg(vpg.get("uuid")) def check_credentials(self): """Check if the connector itself can access the SDN/WIM with the provided url (wim.wim_url), @@ -336,15 +229,13 @@ class JuniperContrail(SdnConnectorBase): 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') + resp = self.underlay_api.check_auth() + if not resp: + raise SdnConnectorError('Empty response') except Exception as e: self.logger.error('Error checking credentials') - raise SdnConnectorError('Error checking credentials', http_code=http_code) - + raise SdnConnectorError('Error checking credentials: {}'.format(str(e))) def get_connectivity_service_status(self, service_uuid, conn_info=None): """Monitor the status of the connectivity service established @@ -384,18 +275,27 @@ class JuniperContrail(SdnConnectorBase): 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') + resp = self.http.get_cmd(endpoint='virtual-network/{}'.format(service_uuid)) + if not resp: + raise SdnConnectorError('Empty response') if resp: vnet_info = json.loads(resp) - return {'sdn_status': 'ACTIVE', 'sdn_info': vnet_info['virtual-network']} + + # Check if conn_info reports error + if conn_info.get("sdn_status") == "ERROR": + return {'sdn_status': 'ACTIVE', 'sdn_info': "conn_info indicates pending error"} + else: + return {'sdn_status': 'ACTIVE', 'sdn_info': vnet_info['virtual-network']} else: return {'sdn_status': 'ERROR', 'sdn_info': 'not found'} + except SdnConnectorError: + raise + except HttpException as e: + self.logger.error("Error getting connectivity service: {}".format(e)) + raise SdnConnectorError("Exception deleting connectivity service: {}".format(str(e))) except Exception as e: - self.logger.error('Exception getting connectivity service info: %s', e) + self.logger.error('Exception getting connectivity service info: %s', e, exc_info=True) return {'sdn_status': 'ERROR', 'error_msg': str(e)} @@ -435,11 +335,6 @@ class JuniperContrail(SdnConnectorBase): :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 @@ -457,17 +352,131 @@ class JuniperContrail(SdnConnectorBase): # name = 'osm-plugin-' + overlay_name # Else: # name = 'osm-plugin-' + VNI + self.logger.info("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.') + + try: + # Initialize data + conn_info = None + conn_info_cp = {} + + # 1 - Obtain VLAN-ID + vlan = self._get_vlan(connection_points) + self.logger.debug("Provided vlan: {}".format(vlan)) + + # 2 - Obtain free VNI + vni = self._generate_vni() + self.logger.debug("VNI: {}".format(vni)) + + # 3 - Create virtual network (name, VNI, RT), by the moment the name will use VNI + retry = 0 + while retry < self._max_duplicate_retry: + try: + vnet_name = 'osm-plugin-' + str(vni) + vnet_id, _ = self.underlay_api.create_virtual_network(vnet_name, vni) + self.used_vni.add(vni) + break + except DuplicateFound as e: + self.logger.debug("Duplicate error for vnet_name: {}".format(vnet_name)) + self.used_vni.add(vni) + retry += 1 + if retry >= self._max_duplicate_retry: + raise e + else: + # Try to obtain a new vni + vni = self._generate_vni() + continue + conn_info = { + "vnet": { + "uuid": vnet_id, + "name": vnet_name + }, + "connection_points": conn_info_cp # dict with port_name as key + } + + # 4 - Create a port for each endpoint + for cp in connection_points: + switch_id = cp.get("service_endpoint_encapsulation_info").get("switch_dpid") + switch_port = cp.get("service_endpoint_encapsulation_info").get("switch_port") + vpg_id, vmi_id = self._create_port(switch_id, switch_port, vnet_name, vlan) + cp_added = cp.copy() + cp_added["vpg_id"] = vpg_id + cp_added["vmi_id"] = vmi_id + conn_info_cp[self.underlay_api.get_vpg_name(switch_id, switch_port)] = cp_added + + return vnet_id, conn_info + self.logger.info("created connectivity service, uuid: {}, name: {}".format(vnet_id, vnet_name)) + except Exception as e: + # Log error + if isinstance(e, SdnConnectorError) or isinstance(e, HttpException): + self.logger.error("Error creating connectivity service: {}".format(e)) + else: + self.logger.error("Error creating connectivity service: {}".format(e), exc_info=True) + + + # If nothing is created raise error else return what has been created and mask as error + if not conn_info: + raise SdnConnectorError("Exception create connectivity service: {}".format(str(e))) + else: + conn_info["sdn_status"] = "ERROR" + # iterate over not added connection_points and add but marking them as error + for cp in connection_points[len(conn_info_cp):]: + cp_error = cp.copy() + cp_error["sdn_status"] = "ERROR" + switch_id = cp.get("service_endpoint_encapsulation_info").get("switch_dpid") + switch_port = cp.get("service_endpoint_encapsulation_info").get("switch_port") + conn_info_cp[self.underlay_api.get_vpg_name(switch_id, switch_port)] = cp_error + return vnet_id, conn_info + + def delete_connectivity_service(self, service_uuid, conn_info=None): + """ + Disconnect multi-site endpoints previously connected + + :param service_uuid: The one returned by create_connectivity_service + :param conn_info: The one returned by last call to 'create_connectivity_service' or 'edit_connectivity_service' + if they do not return None + :return: None + :raises: SdnConnectorException: In case of error. The parameter http_code must be filled + """ + self.logger.info("delete_connectivity_service vnet_name: {}, connection_points: {}". + format(service_uuid, conn_info)) + try: - name = 'test-test-1' - vni = 999999 - network_id, network_info = self._create_virtual_network(self.url, name, vni) + vnet_uuid = service_uuid + vnet_name = conn_info["vnet"]["name"] # always should exist as the network is the first thing created + connection_points = conn_info["connection_points"].values() + vlan = self._get_vlan(connection_points) + + # 1: For each connection point delete vlan from vpg and it is is the + # last one, delete vpg + for cp in connection_points: + self._delete_port(cp.get("vpg_id"), cp.get("vmi_id")) + + # 2: Delete vnet + self.underlay_api.delete_virtual_network(vnet_uuid) except SdnConnectorError: - raise SdnConnectorError('Failed to create connectivity service {}'.format(name)) + raise + except HttpException as e: + self.logger.error("Error deleting connectivity service: {}".format(e)) + raise SdnConnectorError("Exception deleting connectivity service: {}".format(str(e))) 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 + self.logger.error("Error deleting connectivity service: {}".format(e), exc_info=True) + raise SdnConnectorError("Exception deleting connectivity service: {}".format(str(e))) + # Helper methods + @staticmethod + def _get_vlan(connection_points): + vlan = None + for cp in connection_points: + cp_vlan = cp.get("service_endpoint_encapsulation_info").get("vlan") + if not vlan: + vlan = cp_vlan + else: + if vlan != cp_vlan: + raise SdnConnectorError("More that one cp provided") + return vlan def edit_connectivity_service(self, service_uuid, conn_info = None, connection_points = None, **kwargs): """ Change an existing connectivity service. @@ -487,122 +496,134 @@ class JuniperContrail(SdnConnectorBase): 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: {}, " + # 0 - Check if there are connection_points marked as error and delete them + # 1 - Compare conn_info (old connection points) and connection_points (new ones to be applied): + # Obtain list of connection points to be added and to be deleted + # Obtain vlan and check it has not changed + # 2 - Obtain network: Check vnet exists and obtain name + # 3 - Delete unnecesary ports + # 4 - Add new ports + self.logger.info("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) + # conn_info should always exist and have connection_points and vnet elements + old_cp = conn_info.get("connection_points", {}) + old_vlan = self._get_vlan(old_cp) + # Check if an element of old_cp is marked as error, in case it is delete it + # Not return a new conn_info in this case because it is only partial information + # Current conn_info already marks ports as error 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 + delete_conn_info = [] + for cp in old_cp: + if cp.get("sdn_status") == "ERROR": + switch_id = cp.get("service_endpoint_encapsulation_info").get("switch_dpid") + switch_port = cp.get("service_endpoint_encapsulation_info").get("switch_port") + self._delete_port(switch_id, switch_port, old_vlan) + delete_conn_info.append(self.underlay_api.get_vpg_name(switch_id, switch_port)) + + for i in delete_conn_info: + del old_cp[i] + + # Delete vnet status if exists (possibly marked as error) + if conn_info.get("vnet",{}).get("sdn_status"): + del conn_info["vnet"]["sdn_status"] + except HttpException as e: + self.logger.error("Error trying to delete old ports marked as error: {}".format(e)) + raise SdnConnectorError(e) + except SdnConnectorError as e: + self.logger.error("Error trying to delete old ports marked as error: {}".format(e)) + raise except Exception as e: - self.logger.error('Exception add connection_service: %s', e) - # try to rollback push original config + self.logger.error("Error trying to delete old ports marked as error: {}".format(e), exc_info=True) + raise SdnConnectorError("Error trying to delete old ports marked as error: {}".format(e)) + + if connection_points: + + # Check and obtain what should be added and deleted, if there is an error here raise an exception 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): + + vlan = self._get_vlan(connection_points) + + old_port_list = ["{}_{}".format(cp["service_endpoint_encapsulation_info"]["switch_dpid"], + cp["service_endpoint_encapsulation_info"]["switch_port"]) + for cp in old_cp.values()] + port_list = ["{}_{}".format(cp["service_endpoint_encapsulation_info"]["switch_dpid"], + cp["service_endpoint_encapsulation_info"]["switch_port"]) + for cp in connection_points] + to_delete_ports = list(set(old_port_list) - set(port_list)) + to_add_ports = list(set(port_list) - set(old_port_list)) + + # Obtain network + vnet = self.underlay_api.get_virtual_network(self.get_url(), service_uuid) + vnet_name = vnet["name"] + + except SdnConnectorError: raise - else: - raise SdnConnectorError("Exception create_connectivity_service: {}".format(e)) + except Exception as e: + self.logger.error("Error edit connectivity service: {}".format(e), exc_info=True) + raise SdnConnectorError("Exception edit connectivity service: {}".format(str(e))) - def delete_connectivity_service(self, service_uuid, conn_info=None): - """ - Disconnect multi-site endpoints previously connected + # Delete unneeded ports and add new ones: if there is an error return conn_info + try: + # Connection points returned in con_info should reflect what has (and should as ERROR) be done + # Start with old cp dictionary and modify it as we work + conn_info_cp = old_cp + + # Delete unneeded ports + for port_name in conn_info_cp.keys(): + if port_name in to_delete_ports: + cp = conn_info_cp[port_name] + switch_id = cp.get("service_endpoint_encapsulation_info").get("switch_dpid") + switch_port = cp.get("service_endpoint_encapsulation_info").get("switch_port") + self.logger.debug("delete port switch_id, switch_port: {}".format(switch_id, switch_port)) + self._delete_port(switch_id, switch_port, vlan) + del conn_info_cp[port_name] + + # Add needed ports + for cp in connection_points: + if port_name in to_add_ports: + switch_id = cp.get("service_endpoint_encapsulation_info").get("switch_dpid") + switch_port = cp.get("service_endpoint_encapsulation_info").get("switch_port") + self.logger.debug("add port switch_id, switch_port: {}".format(switch_id, switch_port)) + self._create_port(switch_id, switch_port, vnet_name, vlan) + conn_info_cp[port_name] + + conn_info["connection_points"] = conn_info_cp + return conn_info + + except Exception as e: + # Log error + if isinstance(e, SdnConnectorError) or isinstance(e, HttpException): + self.logger.error("Error edit connectivity service: {}".format(e), exc_info=True) + else: + self.logger.error("Error edit connectivity service: {}".format(e)) + + # There has been an error mount conn_info_cp marking as error cp that should + # have been deleted but have not or should have been added + for port_name, cp in conn_info_cp.items(): + if port_name in to_delete_ports: + cp["sdn_status"] = "ERROR" + + for cp in connection_points: + switch_id = cp.get("service_endpoint_encapsulation_info").get("switch_dpid") + switch_port = cp.get("service_endpoint_encapsulation_info").get("switch_port") + port_name = self.underlay_api.get_vpg_name(switch_id, switch_port) + if port_name in to_add_ports: + cp_error = cp.copy() + cp_error["sdn_status"] = "ERROR" + conn_info_cp[port_name] = cp_error + + conn_info["sdn_status"] = "ERROR" + conn_info["connection_points"] = conn_info_cp + return conn_info - :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))) + + else: + # Connection points have not changed, so do nothing + self.logger.info("no new connection_points provided, nothing to be done") + return if __name__ == '__main__': @@ -613,8 +634,8 @@ if __name__ == '__main__': 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.setLevel(level=logging.INFO) + logger.setLevel(level=logging.DEBUG) logger.addHandler(handler) # Read config @@ -645,6 +666,17 @@ if __name__ == '__main__': underlay_url = juniper_contrail.get_url() overlay_url = juniper_contrail.get_overlay_url() + # Generate VNI + for i in range(5): + vni = juniper_contrail._generate_vni() + juniper_contrail.used_vni.add(vni) + print(juniper_contrail.used_vni) + juniper_contrail.used_vni.remove(1000003) + print(juniper_contrail.used_vni) + for i in range(2): + vni = juniper_contrail._generate_vni() + juniper_contrail.used_vni.add(vni) + print(juniper_contrail.used_vni) # 1. Read virtual networks from overlay controller print('1. Read virtual networks from overlay controller') try: @@ -653,8 +685,7 @@ if __name__ == '__main__': print('OK') except Exception as e: logger.error('Exception reading virtual networks from overlay controller: %s', e) - print('FAILED') - + 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) @@ -703,66 +734,87 @@ if __name__ == '__main__': logger.info('Exception reading virtual networks from overlay controller: %s', e) exit(0) + # Test CRUD: + net_name = "gerardo" + net_vni = "2000" + net_vlan = "501" + switch_1 = "LEAF-2" + port_1 = "xe-0/0/18" + switch_2 = "LEAF-1" + port_2 = "xe-0/0/18" + + # 1 - Create a new virtual network + vnet2_id, vnet2_created = juniper_contrail._create_virtual_network(underlay_url, net_name, net_vni) + print("Created virtual network:") + print(vnet2_id) + print(yaml.safe_dump(vnet2_created, indent=4, default_flow_style=False)) + print("Get virtual network:") + vnet2_info = juniper_contrail._get_virtual_network(underlay_url, vnet2_id) + print(json.dumps(vnet2_info, indent=4)) + print('OK') + + # 2 - Create a new virtual port group + vpg_id, vpg_info = juniper_contrail._create_vpg(underlay_url, switch_1, port_1, net_name, net_vlan) + print("Created virtual port group:") + print(vpg_id) + print(json.dumps(vpg_info, indent=4)) - #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) + print("Get virtual network:") + 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') + + # 3 - Create a new virtual machine interface + vmi_id, vmi_info = juniper_contrail._create_vmi(underlay_url, switch_1, port_1, net_name, net_vlan) + print("Created virtual machine interface:") + print(vmi_id) + print(yaml.safe_dump(vmi_info, indent=4, default_flow_style=False)) + + # 4 - Create a second virtual port group + # 5 - Create a second virtual machine interface + + ### Test rapido de modificación de requests: + # Ver que metodos siguen funcionando y cuales no e irlos corrigiendo + + """ + vnets = juniper_contrail._get_virtual_networks(underlay_url) + logger.debug("Virtual networks:") + logger.debug(json.dumps(vnets, indent=2)) + + vpgs = juniper_contrail._get_vpgs(underlay_url) + logger.debug("Virtual port groups:") + logger.debug(json.dumps(vpgs, indent=2)) + """ + # Get by uuid + + """ + # 3 - Get vmi + vmi_uuid = "dbfd2099-b895-459e-98af-882d77d968c1" + vmi = juniper_contrail._get_vmi(underlay_url, vmi_uuid) + logger.debug("Virtual machine interface:") + logger.debug(json.dumps(vmi, indent=2)) + + # Delete vmi + logger.debug("Delete vmi") + juniper_contrail._delete_vmi(underlay_url, vmi_uuid) + """ + + """ + # 2 - Get vpg + vpg_uuid = "85156474-d1a5-44c0-9d8b-8f690f39d27e" + vpg = juniper_contrail._get_vpg(underlay_url, vpg_uuid) + logger.debug("Virtual port group:") + logger.debug(json.dumps(vpg, indent=2)) + # Delete vpg + vpg = juniper_contrail._delete_vpg(underlay_url, vpg_uuid) + """ + + # 1 - Obtain virtual network + """ + vnet_uuid = "68457d61-6558-4d38-a03d-369a9de803ea" + vnet = juniper_contrail._get_virtual_network(underlay_url, vnet_uuid) + logger.debug("Virtual network:") + logger.debug(json.dumps(vnet, indent=2)) + # Delete virtual network + juniper_contrail._delete_virtual_network(underlay_url, vnet_uuid) + """ -- 2.17.1