X-Git-Url: https://osm.etsi.org/gitweb/?p=osm%2FRO.git;a=blobdiff_plain;f=RO-SDN-juniper_contrail%2Fosm_rosdn_juniper_contrail%2Fsdn_assist_juniper_contrail.py;h=08947130a771b1e8efb41cf467d9141f4a391e39;hp=32b80ddbe0298ffdc6ed48472dd612f595a2aa4d;hb=HEAD;hpb=3d0ead412f462595c4399d81af579dee9064a9ec 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..08947130 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,13 @@ # 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 +import random -from io import BytesIO -import pycurl +from osm_ro_plugin.sdnconn import SdnConnectorBase, SdnConnectorError +from osm_rosdn_juniper_contrail.rest_lib import DuplicateFound +from osm_rosdn_juniper_contrail.rest_lib import HttpException +from osm_rosdn_juniper_contrail.sdn_api import UnderlayApi +import yaml class JuniperContrail(SdnConnectorBase): @@ -38,10 +33,12 @@ class JuniperContrail(SdnConnectorBase): 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://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" + + _WIM_LOGGER = "ro.sdn.junipercontrail" def __init__(self, wim, wim_account, config=None, logger=None): """ @@ -64,268 +61,230 @@ class JuniperContrail(SdnConnectorBase): 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. + :param logger (logging.Logger): optional logger object. If none is passed 'ro.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)) + 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") + 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 + self.verify = True + 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 config.get("insecure") and config.get("ca_cert"): + raise SdnConnectorError( + "options insecure and ca_cert are mutually exclusive" + ) + + if config.get("ca_cert"): + self.verify = config.get("ca_cert") + + elif config.get("insecure"): + self.verify = False + + else: + raise SdnConnectorError( + "certificate should provided or ssl verification should be " + "disabled by setting insecure as True in sdn/wim config." + ) + 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.vni_range = ["1000001-2000000"] self.logger.info("No vni_range was provided. Using ['1000001-2000000']") + self.used_vni = set() + 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 + 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 + if not self.project: + raise SdnConnectorError("'project' must be provided") - # 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 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" + ) - self.logger.info("Juniper Contrail Connector Initialized.") + 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, + "verify": self.verify, + } + 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): """ - Method to get unused VxLAN Network Identifier (VNI) + Method to get unused VxLAN Network Identifier (VNI) Args: None Returns: VNI """ - #find unused VLAN ID + # find unused VLAN ID for vlanID_range in self.vni_range: try: - start_vni , end_vni = map(int, vlanID_range.replace(" ", "").split("-")) - for vni in range(start_vni, end_vni + 1): + start_vni, end_vni = map(int, vlanID_range.replace(" ", "").split("-")) + + for i in range(start_vni, end_vni + 1): + vni = random.randrange(start_vni, end_vni, 1) + if vni not in self.used_vni: return vni except Exception as exp: - raise SdnConnectorError("Exception {} occurred while searching a free VNI.".format(exp)) + raise SdnConnectorError( + "Exception {} occurred while searching a free VNI.".format(exp) + ) else: - 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())] + raise SdnConnectorError( + "Unable to create the virtual network." + " All VNI in VNI range {} are in use.".format(self.vni_range) + ) - # Aux functions for testing def get_url(self): return self.url + 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") - 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 + # 3 - Check if the vmi alreaady exists + 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, switch_id, switch_port, vlan): + self.logger.debug( + "delete port, switch_id: {}, switch_port: {}, vlan: {}".format( + switch_id, switch_port, vlan + ) + ) + + vpg_name = self.underlay_api.get_vpg_name(switch_id, switch_port) + vmi_name = self.underlay_api.get_vmi_name(switch_id, switch_port, vlan) + # 1 - Obtain vpg by id (if not vpg_id must have been error creating ig, nothing to be done) + vpg_fqdn = ["default-global-system-config", self.fabric, vpg_name] + vpg = self.underlay_api.get_by_fq_name("virtual-port-group", vpg_fqdn) + + if not vpg: + self.logger.warning("vpg: {} to be deleted not found".format(vpg_name)) + 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: + fqdn = vmi.get("to") + # check by name + + if fqdn[2] == vmi_name: + self.underlay_api.unref_vmi_vpg( + vpg.get("uuid"), vmi.get("uuid"), fqdn + ) + 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 +295,15 @@ 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) + self.logger.error("Error checking credentials") + 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,20 +343,36 @@ 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.underlay_api.get_virtual_network(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']} + vnet_info = resp + + # Check if conn_info reports error + if conn_info.get("sdn_status") == "ERROR": + return {"sdn_status": "ERROR", "sdn_info": conn_info} + else: + return {"sdn_status": "ACTIVE", "sdn_info": vnet_info} else: - return {'sdn_status': 'ERROR', 'sdn_info': 'not found'} + 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) - return {'sdn_status': 'ERROR', 'error_msg': str(e)} + self.logger.error( + "Exception getting connectivity service info: %s", e, exc_info=True + ) + return {"sdn_status": "ERROR", "error_msg": str(e)} def create_connectivity_service(self, service_type, connection_points, **kwargs): """ @@ -435,11 +410,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 @@ -450,27 +420,205 @@ class JuniperContrail(SdnConnectorBase): # 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) + # 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 + 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 + + # 1 - Filter connection_points (transform cp to a dictionary with no duplicates) + # This data will be returned even if no cp can be created if something is created + work_cps = {} + 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" + ) + service_endpoint_id = cp.get("service_endpoint_id") + cp_name = self.underlay_api.get_vpg_name(switch_id, switch_port) + add_cp = work_cps.get(cp_name) + + if not add_cp: + # check cp has vlan + vlan = cp.get("service_endpoint_encapsulation_info").get("vlan") + + if vlan: + # add cp to dict + service_endpoint_ids = [] + service_endpoint_ids.append(service_endpoint_id) + add_cp = { + "service_endpoint_ids": service_endpoint_ids, + "switch_dpid": switch_id, + "switch_port": switch_port, + "vlan": vlan, + } + work_cps[cp_name] = add_cp + else: + self.logger.warning( + "cp service_endpoint_id : {} has no vlan, ignore".format( + service_endpoint_id + ) + ) + else: + # add service_endpoint_id to list + service_endpoint_ids = add_cp["service_endpoint_ids"] + service_endpoint_ids.append(service_endpoint_id) + + # 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": work_cps, # dict with port_name as key + } + + # 4 - Create a port for each endpoint + for cp in work_cps.values(): + switch_id = cp.get("switch_dpid") + switch_port = cp.get("switch_port") + vlan = cp.get("vlan") + vpg_id, vmi_id = self._create_port( + switch_id, switch_port, vnet_name, vlan + ) + cp["vpg_id"] = vpg_id + cp["vmi_id"] = vmi_id + + self.logger.info( + "created connectivity service, uuid: {}, name: {}".format( + vnet_id, vnet_name + ) + ) + + return vnet_id, conn_info + 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" + conn_info["sdn_info"] = repr(e) + # iterate over not added connection_points and add but marking them as error + for cp in work_cps.values(): + if not cp.get("vmi_id") or not cp.get("vpg_id"): + cp["sdn_status"] = "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 + work_cps = conn_info["connection_points"] + + # 1: For each connection point delete vlan from vpg and it is is the + # last one, delete vpg + for cp in work_cps.values(): + self._delete_port( + cp.get("switch_dpid"), cp.get("switch_port"), cp.get("vlan") + ) + + # 2: Delete vnet + self.underlay_api.delete_virtual_network(vnet_uuid) + self.logger.info( + "deleted connectivity_service vnet_uuid: {}, connection_points: {}".format( + service_uuid, conn_info + ) + ) 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)) + ) - def edit_connectivity_service(self, service_uuid, conn_info = None, connection_points = None, **kwargs): - """ Change an existing connectivity service. + 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`. @@ -487,282 +635,330 @@ 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: {}, " - "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) - + # 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 should always exist and have connection_points and vnet elements + old_cp = conn_info.get("connection_points", {}) + + # 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 + deleted_ports = [] + for cp in old_cp.values(): + if cp.get("sdn_status") == "ERROR": + switch_id = cp.get("switch_dpid") + switch_port = cp.get("switch_port") + old_vlan = cp.get("vlan") + self._delete_port(switch_id, switch_port, old_vlan) + deleted_ports.append( + self.underlay_api.get_vpg_name(switch_id, switch_port) + ) + + for port in deleted_ports: + del old_cp[port] + + # Delete sdn_status and sdn_info 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): + work_cps = {} + 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" + ) + service_endpoint_id = cp.get("service_endpoint_id") + cp_name = self.underlay_api.get_vpg_name(switch_id, switch_port) + add_cp = work_cps.get(cp_name) + + if not add_cp: + # add cp to dict + # check cp has vlan + vlan = cp.get("service_endpoint_encapsulation_info").get("vlan") + + if vlan: + service_endpoint_ids = [] + service_endpoint_ids.append(service_endpoint_id) + add_cp = { + "service_endpoint_ids": service_endpoint_ids, + "switch_dpid": switch_id, + "switch_port": switch_port, + "vlan": vlan, + } + work_cps[cp_name] = add_cp + else: + self.logger.warning( + "cp service_endpoint_id : {} has no vlan, ignore".format( + service_endpoint_id + ) + ) + else: + # add service_endpoint_id to list + service_endpoint_ids = add_cp["service_endpoint_ids"] + service_endpoint_ids.append(service_endpoint_id) + + old_port_list = list(old_cp.keys()) + port_list = list(work_cps.keys()) + to_delete_ports = list(set(old_port_list) - set(port_list)) + to_add_ports = list(set(port_list) - set(old_port_list)) + self.logger.debug("ports to delete: {}".format(to_delete_ports)) + self.logger.debug("ports to add: {}".format(to_add_ports)) + + # Obtain network (check it is correctly created) + vnet = self.underlay_api.get_virtual_network(service_uuid) + if vnet: + vnet_name = vnet["name"] + else: + raise SdnConnectorError( + "vnet uuid: {} not found".format(service_uuid) + ) + 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 + deleted_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("switch_dpid") + switch_port = cp.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) + deleted_ports.append(port_name) + + # Delete ports + for port_name in deleted_ports: + del conn_info_cp[port_name] + + # Add needed ports + for port_name, cp in work_cps.items(): + if port_name in to_add_ports: + switch_id = cp.get("switch_dpid") + switch_port = cp.get("switch_port") + vlan = cp.get("vlan") + self.logger.debug( + "add port switch_id={}, switch_port={}".format( + switch_id, 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[port_name] = cp_added + + # replace endpoints in case they have changed + conn_info_cp[port_name]["service_endpoint_ids"] = cp[ + "service_endpoint_ids" + ] + + 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 port_name, cp in work_cps.items(): + curr_cp = conn_info_cp.get(port_name) + + if not curr_cp: + cp_error = work_cps.get(port_name).copy() + cp_error["sdn_status"] = "ERROR" + conn_info_cp[port_name] = cp_error + + conn_info_cp[port_name]["service_endpoint_ids"] = cp[ + "service_endpoint_ids" + ] + + conn_info["sdn_status"] = "ERROR" + conn_info["sdn_info"] = repr(e) + conn_info["connection_points"] = conn_info_cp + + return conn_info + else: + # Connection points have not changed, so do nothing + self.logger.info("no new connection_points provided, nothing to be done") - :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))) + return -if __name__ == '__main__': +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') + 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 = logging.getLogger("ro.sdn.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: + 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)) + + 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) + juniper_contrail = JuniperContrail( + wim=wim, wim_account=wim_account, config=config, logger=logger + ) # Tests # 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) + # 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) - # 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) + # 0. Check credentials + print("0. Check credentials") + # juniper_contrail.check_credentials() - #TODO: to be deleted (it comes from ONOS VPLS plugin) - service_type = 'ELAN' + # 1 - Create and delete connectivity service conn_point_0 = { - "service_endpoint_id": "switch1:ifz1", + "service_endpoint_id": "0000:83:11.4", "service_endpoint_encapsulation_type": "dot1q", "service_endpoint_encapsulation_info": { - "switch_dpid": "0000000000000011", - "switch_port": "1", - "vlan": "600" - } + "switch_dpid": "LEAF-1", + "switch_port": "xe-0/0/17", + "vlan": "501", + }, } conn_point_1 = { - "service_endpoint_id": "switch3:ifz1", + "service_endpoint_id": "0000:81:10.3", "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" - } + "switch_dpid": "LEAF-2", + "switch_port": "xe-0/0/16", + "vlan": "501", + }, } conn_point_2 = { - "service_endpoint_id": "switch1:ifz3", + "service_endpoint_id": "0000:08:11.7", "service_endpoint_encapsulation_type": "dot1q", "service_endpoint_encapsulation_info": { - "switch_dpid": "0000000000000011", - "switch_port": "3", - "vlan": "500" - } + "switch_dpid": "LEAF-2", + "switch_port": "xe-0/0/16", + "vlan": "502", + }, } conn_point_3 = { - "service_endpoint_id": "switch3_ifz3", + "service_endpoint_id": "0000:83:10.4", "service_endpoint_encapsulation_type": "dot1q", "service_endpoint_encapsulation_info": { - "switch_dpid": "0000000000000033", - "switch_port": "3", - "vlan": "500" - } + "switch_dpid": "LEAF-1", + "switch_port": "xe-0/0/17", + "vlan": "502", + }, } - 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) + + # 1 - Define connection points + logger.debug("create first connection service") + print("Create connectivity service") + connection_points = [conn_point_0, conn_point_1] + service_id, conn_info = juniper_contrail.create_connectivity_service( + "ELAN", connection_points + ) + logger.info("Created connectivity service 1") + logger.info(service_id) + logger.info(yaml.safe_dump(conn_info, indent=4, default_flow_style=False)) + + logger.debug("create second connection service") + print("Create connectivity service") + connection_points = [conn_point_2, conn_point_3] + service_id2, conn_info2 = juniper_contrail.create_connectivity_service( + "ELAN", connection_points + ) + logger.info("Created connectivity service 2") + logger.info(service_id2) + logger.info(yaml.safe_dump(conn_info2, indent=4, default_flow_style=False)) + + logger.debug("Delete connectivity service 1") + juniper_contrail.delete_connectivity_service(service_id, conn_info) + logger.debug("Delete Ok") + + logger.debug("Delete connectivity service 2") + juniper_contrail.delete_connectivity_service(service_id2, conn_info2) + logger.debug("Delete Ok")