X-Git-Url: https://osm.etsi.org/gitweb/?p=osm%2FRO.git;a=blobdiff_plain;f=RO-SDN-arista_cloudvision%2Fosm_rosdn_arista_cloudvision%2Fwimconn_arista.py;h=314c6733e18d3f26940687f4a2cef0d41fdec1a7;hp=e72a082204c50d6d89d38a779e1689c3fdbb44ed;hb=80135b928ab442c38898750b4751480205b4affc;hpb=e493e9b91720e5116e00b4c06cf66c767bccce2f diff --git a/RO-SDN-arista_cloudvision/osm_rosdn_arista_cloudvision/wimconn_arista.py b/RO-SDN-arista_cloudvision/osm_rosdn_arista_cloudvision/wimconn_arista.py index e72a0822..314c6733 100644 --- a/RO-SDN-arista_cloudvision/osm_rosdn_arista_cloudvision/wimconn_arista.py +++ b/RO-SDN-arista_cloudvision/osm_rosdn_arista_cloudvision/wimconn_arista.py @@ -26,11 +26,14 @@ # # This work has been performed in the context of Arista Telefonica OSM PoC. ## + from osm_ro_plugin.sdnconn import SdnConnectorBase, SdnConnectorError import re import socket + # Required by compare function import difflib + # Library that uses Levenshtein Distance to calculate the differences # between strings. # from fuzzywuzzy import fuzz @@ -49,24 +52,22 @@ from osm_rosdn_arista_cloudvision.aristaTask import AristaCVPTask class SdnError(Enum): - UNREACHABLE = 'Unable to reach the WIM url, connect error.', - TIMEOUT = 'Unable to reach the WIM url, timeout.', - VLAN_INCONSISTENT = \ - 'VLAN value inconsistent between the connection points', - VLAN_NOT_PROVIDED = 'VLAN value not provided', - CONNECTION_POINTS_SIZE = \ - 'Unexpected number of connection points: 2 expected.', - ENCAPSULATION_TYPE = \ - 'Unexpected service_endpoint_encapsulation_type. ' \ - 'Only "dotq1" is accepted.', - BANDWIDTH = 'Unable to get the bandwidth.', - STATUS = 'Unable to get the status for the service.', - DELETE = 'Unable to delete service.', - CLEAR_ALL = 'Unable to clear all the services', - UNKNOWN_ACTION = 'Unknown action invoked.', - BACKUP = 'Unable to get the backup parameter.', - UNSUPPORTED_FEATURE = "Unsupported feature", - UNAUTHORIZED = "Failed while authenticating", + UNREACHABLE = "Unable to reach the WIM url, connect error." + TIMEOUT = "Unable to reach the WIM url, timeout." + VLAN_INCONSISTENT = "VLAN value inconsistent between the connection points" + VLAN_NOT_PROVIDED = "VLAN value not provided" + CONNECTION_POINTS_SIZE = "Unexpected number of connection points: 2 expected." + ENCAPSULATION_TYPE = ( + 'Unexpected service_endpoint_encapsulation_type. Only "dotq1" is accepted.' + ) + BANDWIDTH = "Unable to get the bandwidth." + STATUS = "Unable to get the status for the service." + DELETE = "Unable to delete service." + CLEAR_ALL = "Unable to clear all the services" + UNKNOWN_ACTION = "Unknown action invoked." + BACKUP = "Unable to get the backup parameter." + UNSUPPORTED_FEATURE = "Unsupported feature" + UNAUTHORIZED = "Failed while authenticating" INTERNAL_ERROR = "Internal error" @@ -97,14 +98,15 @@ class AristaSdnConnector(SdnConnectorBase): -- All created services identification is stored in a generic ConfigLet 'OSM_metadata' to keep track of the managed resources by OSM in the Arista deployment. """ + __supported_service_types = ["ELINE (L2)", "ELINE", "ELAN"] __service_types_ELAN = "ELAN" __service_types_ELINE = "ELINE" __ELINE_num_connection_points = 2 __supported_service_types = ["ELINE", "ELAN"] __supported_encapsulation_types = ["dot1q"] - __WIM_LOGGER = 'ro.sdn.arista' - __SERVICE_ENDPOINT_MAPPING = 'service_endpoint_mapping' + __WIM_LOGGER = "ro.sdn.arista" + __SERVICE_ENDPOINT_MAPPING = "service_endpoint_mapping" __ENCAPSULATION_TYPE_PARAM = "service_endpoint_encapsulation_type" __ENCAPSULATION_INFO_PARAM = "service_endpoint_encapsulation_info" __BACKUP_PARAM = "backup" @@ -119,16 +121,16 @@ class AristaSdnConnector(SdnConnectorBase): __SW_PORT_PARAM = "switch_port" __VLAN_PARAM = "vlan" __VNI_PARAM = "vni" - __SEPARATOR = '_' - __MANAGED_BY_OSM = '## Managed by OSM ' + __SEPARATOR = "_" + __MANAGED_BY_OSM = "## Managed by OSM " __OSM_PREFIX = "osm_" __OSM_METADATA = "OSM_metadata" - __METADATA_PREFIX = '!## Service' + __METADATA_PREFIX = "!## Service" __EXC_TASK_EXEC_WAIT = 10 __ROLLB_TASK_EXEC_WAIT = 10 __API_REQUEST_TOUT = 60 - __SWITCH_TAG_NAME = 'topology_type' - __SWITCH_TAG_VALUE = 'leaf' + __SWITCH_TAG_NAME = "topology_type" + __SWITCH_TAG_VALUE = "leaf" __LOOPBACK_INTF = "Loopback0" _VLAN = "VLAN" _VXLAN = "VXLAN" @@ -159,55 +161,74 @@ class AristaSdnConnector(SdnConnectorBase): :param logger (logging.Logger): optional logger object. If none is passed 'ro.sdn.sdnconn' is used. """ self.__regex = re.compile( - r'^(?:http|ftp)s?://' # http:// or https:// - r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # domain... - r'localhost|' # localhost... - r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip - r'(?::\d+)?', re.IGNORECASE) # optional port + r"^(?:http|ftp)s?://" # http:// or https:// + r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|" # domain... + r"localhost|" # localhost... + r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})" # ...or ip + r"(?::\d+)?", + re.IGNORECASE, + ) # optional port self.raiseException = True self.logger = logger or logging.getLogger(self.__WIM_LOGGER) super().__init__(wim, wim_account, config, self.logger) self.__wim = wim self.__wim_account = wim_account self.__config = config + if self.is_valid_destination(self.__wim.get("wim_url")): self.__wim_url = self.__wim.get("wim_url") else: - raise SdnConnectorError(message='Invalid wim_url value', - http_code=500) + raise SdnConnectorError(message="Invalid wim_url value", http_code=500) + self.__user = wim_account.get("user") self.__passwd = wim_account.get("password") self.client = None self.cvp_inventory = None self.cvp_tags = None - self.logger.debug("Arista SDN plugin {}, cvprac version {}, user:{} and config:{}". - format(wim, cvprac_version, self.__user, - self.delete_keys_from_dict(config, ('passwd',)))) + self.logger.debug( + "Arista SDN plugin {}, cvprac version {}, user:{} and config:{}".format( + wim, + cvprac_version, + self.__user, + self.delete_keys_from_dict(config, ("passwd",)), + ) + ) self.allDeviceFacts = [] self.taskC = None + try: self.__load_topology() self.__load_switches() except (ConnectTimeout, Timeout) as ct: - raise SdnConnectorError(message=SdnError.TIMEOUT + " " + str(ct), http_code=408) + raise SdnConnectorError( + message=SdnError.TIMEOUT + " " + str(ct), http_code=408 + ) except ConnectionError as ce: - raise SdnConnectorError(message=SdnError.UNREACHABLE + " " + str(ce), http_code=404) + raise SdnConnectorError( + message=SdnError.UNREACHABLE + " " + str(ce), http_code=404 + ) except SdnConnectorError as sc: raise sc except CvpLoginError as le: raise SdnConnectorError(message=le.msg, http_code=500) from le except Exception as e: - raise SdnConnectorError(message="Unable to load switches from CVP" + " " + str(e), - http_code=500) from e - self.logger.debug("Using topology {} in Arista Leaf switches: {}".format( - self.topology, - self.delete_keys_from_dict(self.switches, ('passwd',)))) + raise SdnConnectorError( + message="Unable to load switches from CVP " + str(e), http_code=500 + ) from e + + self.logger.debug( + "Using topology {} in Arista Leaf switches: {}".format( + self.topology, self.delete_keys_from_dict(self.switches, ("passwd",)) + ) + ) self.clC = AristaSDNConfigLet(self.topology) def __load_topology(self): self.topology = self._VXLAN_MLAG - if self.__config and self.__config.get('topology'): - topology = self.__config.get('topology') + + if self.__config and self.__config.get("topology"): + topology = self.__config.get("topology") + if topology == "VLAN": self.topology = self._VLAN elif topology == "VXLAN": @@ -218,7 +239,7 @@ class AristaSdnConnector(SdnConnectorBase): self.topology = self._VXLAN_MLAG def __load_switches(self): - """ Retrieves the switches to configure in the following order + """Retrieves the switches to configure in the following order 1. from incoming configuration: 1.1 using port mapping using user and password from WIM @@ -236,47 +257,58 @@ class AristaSdnConnector(SdnConnectorBase): for port in self.__config.get(self.__SERVICE_ENDPOINT_MAPPING): switch_dpid = port.get(self.__SW_ID_PARAM) if switch_dpid and switch_dpid not in self.switches: - self.switches[switch_dpid] = {'passwd': self.__passwd, - 'ip': None, - 'usr': self.__user, - 'lo0': None, - 'AS': None, - 'serialNumber': None, - 'mlagPeerDevice': None} - - if self.__config and self.__config.get('switches'): + self.switches[switch_dpid] = { + "passwd": self.__passwd, + "ip": None, + "usr": self.__user, + "lo0": None, + "AS": None, + "serialNumber": None, + "mlagPeerDevice": None, + } + + if self.__config and self.__config.get("switches"): # Not directly from json, complete one by one - config_switches = self.__config.get('switches') + config_switches = self.__config.get("switches") for cs, cs_content in config_switches.items(): if cs not in self.switches: - self.switches[cs] = {'passwd': self.__passwd, - 'ip': None, - 'usr': self.__user, - 'lo0': None, - 'AS': None, - 'serialNumber': None, - 'mlagPeerDevice': None} + self.switches[cs] = { + "passwd": self.__passwd, + "ip": None, + "usr": self.__user, + "lo0": None, + "AS": None, + "serialNumber": None, + "mlagPeerDevice": None, + } + if cs_content: self.switches[cs].update(cs_content) # Load the rest of the data if self.client is None: self.client = self.__connect() + self.__load_inventory() + if not self.switches: self.__get_tags(self.__SWITCH_TAG_NAME, self.__SWITCH_TAG_VALUE) + for device in self.allDeviceFacts: # get the switches whose topology_tag is 'leaf' - if device['serialNumber'] in self.cvp_tags: - if not self.switches.get(device['hostname']): - switch_data = {'passwd': self.__passwd, - 'ip': device['ipAddress'], - 'usr': self.__user, - 'lo0': None, - 'AS': None, - 'serialNumber': None, - 'mlagPeerDevice': None} - self.switches[device['hostname']] = switch_data + if device["serialNumber"] in self.cvp_tags: + if not self.switches.get(device["hostname"]): + switch_data = { + "passwd": self.__passwd, + "ip": device["ipAddress"], + "usr": self.__user, + "lo0": None, + "AS": None, + "serialNumber": None, + "mlagPeerDevice": None, + } + self.switches[device["hostname"]] = switch_data + if len(self.switches) == 0: self.logger.error("Unable to load Leaf switches from CVP") return @@ -285,68 +317,93 @@ class AristaSdnConnector(SdnConnectorBase): # used to make eAPI calls by using switch.py module for s in self.switches: for device in self.allDeviceFacts: - if device['hostname'] == s: - if not self.switches[s].get('ip'): - self.switches[s]['ip'] = device['ipAddress'] - self.switches[s]['serialNumber'] = device['serialNumber'] + if device["hostname"] == s: + if not self.switches[s].get("ip"): + self.switches[s]["ip"] = device["ipAddress"] + self.switches[s]["serialNumber"] = device["serialNumber"] break # Each switch has a different loopback address, # so it's a different configLet - if not self.switches[s].get('lo0'): - inf = self.__get_interface_ip(self.switches[s]['serialNumber'], self.__LOOPBACK_INTF) - self.switches[s]["lo0"] = inf.split('/')[0] - if not self.switches[s].get('AS'): - self.switches[s]["AS"] = self.__get_device_ASN(self.switches[s]['serialNumber']) + if not self.switches[s].get("lo0"): + inf = self.__get_interface_ip( + self.switches[s]["serialNumber"], self.__LOOPBACK_INTF + ) + self.switches[s]["lo0"] = inf.split("/")[0] + + if not self.switches[s].get("AS"): + self.switches[s]["AS"] = self.__get_device_ASN( + self.switches[s]["serialNumber"] + ) + if self.topology in (self._VXLAN_MLAG, self._VLAN_MLAG): for s in self.switches: - if not self.switches[s].get('mlagPeerDevice'): - self.switches[s]['mlagPeerDevice'] = self.__get_peer_MLAG(self.switches[s]['serialNumber']) - - def __check_service(self, service_type, connection_points, - check_vlan=True, check_num_cp=True, kwargs=None): - """ Reviews the connection points elements looking for semantic errors in the incoming data - """ + if not self.switches[s].get("mlagPeerDevice"): + self.switches[s]["mlagPeerDevice"] = self.__get_peer_MLAG( + self.switches[s]["serialNumber"] + ) + + def __check_service( + self, + service_type, + connection_points, + check_vlan=True, + check_num_cp=True, + kwargs=None, + ): + """Reviews the connection points elements looking for semantic errors in the incoming data""" if service_type not in self.__supported_service_types: - raise Exception("The service '{}' is not supported. Only '{}' are accepted".format( - service_type, - self.__supported_service_types)) + raise Exception( + "The service '{}' is not supported. Only '{}' are accepted".format( + service_type, self.__supported_service_types + ) + ) if check_num_cp: if len(connection_points) < 2: raise Exception(SdnError.CONNECTION_POINTS_SIZE) - if (len(connection_points) != self.__ELINE_num_connection_points and - service_type == self.__service_types_ELINE): + + if ( + len(connection_points) != self.__ELINE_num_connection_points + and service_type == self.__service_types_ELINE + ): raise Exception(SdnError.CONNECTION_POINTS_SIZE) if check_vlan: - vlan_id = '' + vlan_id = "" + for cp in connection_points: enc_type = cp.get(self.__ENCAPSULATION_TYPE_PARAM) - if (enc_type and - enc_type not in self.__supported_encapsulation_types): + + if enc_type and enc_type not in self.__supported_encapsulation_types: raise Exception(SdnError.ENCAPSULATION_TYPE) + encap_info = cp.get(self.__ENCAPSULATION_INFO_PARAM) cp_vlan_id = str(encap_info.get(self.__VLAN_PARAM)) + if cp_vlan_id: if not vlan_id: vlan_id = cp_vlan_id elif vlan_id != cp_vlan_id: raise Exception(SdnError.VLAN_INCONSISTENT) + if not vlan_id: raise Exception(SdnError.VLAN_NOT_PROVIDED) + if vlan_id in self.__get_srvVLANs(): - raise Exception('VLAN {} already assigned to a connectivity service'.format(vlan_id)) + raise Exception( + "VLAN {} already assigned to a connectivity service".format(vlan_id) + ) # Commented out for as long as parameter isn't implemented # bandwidth = kwargs.get(self.__BANDWIDTH_PARAM) # if not isinstance(bandwidth, int): - # self.__exception(SdnError.BANDWIDTH, http_code=400) + # self.__exception(SdnError.BANDWIDTH, http_code=400) # Commented out for as long as parameter isn't implemented # backup = kwargs.get(self.__BACKUP_PARAM) # if not isinstance(backup, bool): - # self.__exception(SdnError.BACKUP, http_code=400) + # self.__exception(SdnError.BACKUP, http_code=400) def check_credentials(self): """Retrieves the CloudVision version information, as the easiest way @@ -355,18 +412,23 @@ class AristaSdnConnector(SdnConnectorBase): try: if self.client is None: self.client = self.__connect() + result = self.client.api.get_cvp_info() self.logger.debug(result) except CvpLoginError as e: self.logger.info(str(e)) self.client = None - raise SdnConnectorError(message=SdnError.UNAUTHORIZED + " " + str(e), - http_code=401) from e + + raise SdnConnectorError( + message=SdnError.UNAUTHORIZED + " " + str(e), http_code=401 + ) from e except Exception as ex: self.client = None self.logger.error(str(ex)) - raise SdnConnectorError(message=SdnError.INTERNAL_ERROR + " " + str(ex), - http_code=500) from ex + + raise SdnConnectorError( + message=SdnError.INTERNAL_ERROR + " " + str(ex), http_code=500 + ) from ex def get_connectivity_service_status(self, service_uuid, conn_info=None): """Monitor the status of the connectivity service established @@ -405,81 +467,112 @@ class AristaSdnConnector(SdnConnectorBase): new information available for the connectivity service. """ try: - self.logger.debug("invoked get_connectivity_service_status '{}'".format(service_uuid)) + self.logger.debug( + "invoked get_connectivity_service_status '{}'".format(service_uuid) + ) + if not service_uuid: - raise SdnConnectorError(message='No connection service UUID', - http_code=500) + raise SdnConnectorError( + message="No connection service UUID", http_code=500 + ) self.__get_Connection() - if conn_info is None: - raise SdnConnectorError(message='No connection information for service UUID {}'.format(service_uuid), - http_code=500) - if 'configLetPerSwitch' in conn_info.keys(): + if conn_info is None: + raise SdnConnectorError( + message="No connection information for service UUID {}".format( + service_uuid + ), + http_code=500, + ) + + if "configLetPerSwitch" in conn_info.keys(): c_info = conn_info else: c_info = None - cls_perSw = self.__get_serviceData(service_uuid, - conn_info['service_type'], - conn_info['vlan_id'], - c_info) + + cls_perSw = self.__get_serviceData( + service_uuid, conn_info["service_type"], conn_info["vlan_id"], c_info + ) t_isCancelled = False t_isFailed = False t_isPending = False failed_switches = [] + for s in self.switches: if len(cls_perSw[s]) > 0: for cl in cls_perSw[s]: # Fix 1030 SDN-ARISTA Key error note when deploy a NS # Added protection to check that 'note' exists and additionally # verify that it is managed by OSM - if (not cls_perSw[s][0]['config'] or - not cl.get('note') or - self.__MANAGED_BY_OSM not in cl['note']): + if ( + not cls_perSw[s][0]["config"] + or not cl.get("note") + or self.__MANAGED_BY_OSM not in cl["note"] + ): continue - note = cl['note'] + + note = cl["note"] t_id = note.split(self.__SEPARATOR)[1] result = self.client.api.get_task_by_id(t_id) - if result['workOrderUserDefinedStatus'] == 'Completed': + + if result["workOrderUserDefinedStatus"] == "Completed": continue - elif result['workOrderUserDefinedStatus'] == 'Cancelled': + elif result["workOrderUserDefinedStatus"] == "Cancelled": t_isCancelled = True - elif result['workOrderUserDefinedStatus'] == 'Failed': + elif result["workOrderUserDefinedStatus"] == "Failed": t_isFailed = True else: t_isPending = True + failed_switches.append(s) + if t_isCancelled: - error_msg = 'Some works were cancelled in switches: {}'.format(str(failed_switches)) - sdn_status = 'DOWN' + error_msg = "Some works were cancelled in switches: {}".format( + str(failed_switches) + ) + sdn_status = "DOWN" elif t_isFailed: - error_msg = 'Some works failed in switches: {}'.format(str(failed_switches)) - sdn_status = 'ERROR' + error_msg = "Some works failed in switches: {}".format( + str(failed_switches) + ) + sdn_status = "ERROR" elif t_isPending: - error_msg = 'Some works are still under execution in switches: {}'.format(str(failed_switches)) - sdn_status = 'BUILD' + error_msg = ( + "Some works are still under execution in switches: {}".format( + str(failed_switches) + ) + ) + sdn_status = "BUILD" else: - error_msg = '' - sdn_status = 'ACTIVE' - sdn_info = '' - return {'sdn_status': sdn_status, - 'error_msg': error_msg, - 'sdn_info': sdn_info} + error_msg = "" + sdn_status = "ACTIVE" + + sdn_info = "" + + return { + "sdn_status": sdn_status, + "error_msg": error_msg, + "sdn_info": sdn_info, + } except CvpLoginError as e: self.logger.info(str(e)) self.client = None - raise SdnConnectorError(message=SdnError.UNAUTHORIZED + " " + str(e), - http_code=401) from e + + raise SdnConnectorError( + message=SdnError.UNAUTHORIZED + " " + str(e), http_code=401 + ) from e except Exception as ex: self.client = None self.logger.error(str(ex), exc_info=True) - raise SdnConnectorError(message=str(ex) + " " + str(ex), - http_code=500) from ex - def create_connectivity_service(self, service_type, connection_points, - **kwargs): - """Stablish SDN/WAN connectivity between the endpoints + raise SdnConnectorError( + message=str(ex) + " " + str(ex), http_code=500 + ) from ex + + def create_connectivity_service(self, service_type, connection_points, **kwargs): + """Establish SDN/WAN connectivity between the endpoints :param service_type: (str): ``ELINE`` (L2), ``ELAN`` (L2), ``ETREE`` (L2), ``L3``. :param connection_points: (list): each point corresponds to @@ -528,24 +621,24 @@ class AristaSdnConnector(SdnConnectorBase): Provide the parameter http_code """ try: - self.logger.debug("invoked create_connectivity_service '{}' ports: {}". - format(service_type, connection_points)) + self.logger.debug( + "invoked create_connectivity_service '{}' ports: {}".format( + service_type, connection_points + ) + ) self.__get_Connection() - self.__check_service(service_type, - connection_points, - check_vlan=True, - kwargs=kwargs) + self.__check_service( + service_type, connection_points, check_vlan=True, kwargs=kwargs + ) service_uuid = str(uuid.uuid4()) - self.logger.info("Service with uuid {} created.". - format(service_uuid)) + self.logger.info("Service with uuid {} created.".format(service_uuid)) s_uid, s_connInf = self.__processConnection( - service_uuid, - service_type, - connection_points, - kwargs) + service_uuid, service_type, connection_points, kwargs + ) + try: - self.__addMetadata(s_uid, service_type, s_connInf['vlan_id']) + self.__addMetadata(s_uid, service_type, s_connInf["vlan_id"]) except Exception: pass @@ -553,28 +646,29 @@ class AristaSdnConnector(SdnConnectorBase): except CvpLoginError as e: self.logger.info(str(e)) self.client = None - raise SdnConnectorError(message=SdnError.UNAUTHORIZED + " " + str(e), - http_code=401) from e + + raise SdnConnectorError( + message=SdnError.UNAUTHORIZED + " " + str(e), http_code=401 + ) from e except SdnConnectorError as sde: raise sde except ValueError as err: self.client = None self.logger.error(str(err), exc_info=True) - raise SdnConnectorError(message=str(err), - http_code=500) from err + + raise SdnConnectorError(message=str(err), http_code=500) from err except Exception as ex: self.client = None self.logger.error(str(ex), exc_info=True) + if self.raiseException: raise ex - raise SdnConnectorError(message=str(ex), - http_code=500) from ex - def __processConnection(self, - service_uuid, - service_type, - connection_points, - kwargs): + raise SdnConnectorError(message=str(ex), http_code=500) from ex + + def __processConnection( + self, service_uuid, service_type, connection_points, kwargs + ): """ Invoked from creation and edit methods @@ -590,107 +684,133 @@ class AristaSdnConnector(SdnConnectorBase): cls_perSw = {} cls_cp = {} cl_bgp = {} + for s in self.switches: cls_perSw[s] = [] cls_cp[s] = [] + vlan_processed = False - vlan_id = '' + vlan_id = "" i = 0 processed_connection_points = [] + for cp in connection_points: i += 1 encap_info = cp.get(self.__ENCAPSULATION_INFO_PARAM) + if not vlan_processed: vlan_id = str(encap_info.get(self.__VLAN_PARAM)) + if not vlan_id: continue + vni_id = encap_info.get(self.__VNI_PARAM) + if not vni_id: vni_id = str(10000 + int(vlan_id)) if service_type == self.__service_types_ELAN: - cl_vlan = self.clC.getElan_vlan(service_uuid, - vlan_id, - vni_id) + cl_vlan = self.clC.getElan_vlan(service_uuid, vlan_id, vni_id) else: - cl_vlan = self.clC.getEline_vlan(service_uuid, - vlan_id, - vni_id) + cl_vlan = self.clC.getEline_vlan(service_uuid, vlan_id, vni_id) + vlan_processed = True encap_type = cp.get(self.__ENCAPSULATION_TYPE_PARAM) switch_id = encap_info.get(self.__SW_ID_PARAM) interface = encap_info.get(self.__SW_PORT_PARAM) - switches = [{'name': switch_id, 'interface': interface}] + switches = [{"name": switch_id, "interface": interface}] # remove those connections that are equal. This happens when several sriovs are located in the same # compute node interface, that is, in the same switch and interface switches = [x for x in switches if x not in processed_connection_points] + if not switches: continue + processed_connection_points += switches + for switch in switches: if not interface: raise SdnConnectorError( - message="Connection point switch port empty for switch_dpid {}".format(switch_id), - http_code=406) + message="Connection point switch port empty for switch_dpid {}".format( + switch_id + ), + http_code=406, + ) # it should be only one switch where the mac is attached - if encap_type == 'dot1q': + if encap_type == "dot1q": # SRIOV configLet for Leaf switch mac's attached to if service_type == self.__service_types_ELAN: - cl_encap = self.clC.getElan_sriov(service_uuid, interface, vlan_id, i) + cl_encap = self.clC.getElan_sriov( + service_uuid, interface, vlan_id, i + ) else: - cl_encap = self.clC.getEline_sriov(service_uuid, interface, vlan_id, i) + cl_encap = self.clC.getEline_sriov( + service_uuid, interface, vlan_id, i + ) elif not encap_type: # PT configLet for Leaf switch attached to the mac if service_type == self.__service_types_ELAN: - cl_encap = self.clC.getElan_passthrough(service_uuid, - interface, - vlan_id, i) + cl_encap = self.clC.getElan_passthrough( + service_uuid, interface, vlan_id, i + ) else: - cl_encap = self.clC.getEline_passthrough(service_uuid, - interface, - vlan_id, i) - if cls_cp.get(switch['name']): - cls_cp[switch['name']] = str(cls_cp[switch['name']]) + cl_encap + cl_encap = self.clC.getEline_passthrough( + service_uuid, interface, vlan_id, i + ) + + if cls_cp.get(switch["name"]): + cls_cp[switch["name"]] = str(cls_cp[switch["name"]]) + cl_encap else: - cls_cp[switch['name']] = cl_encap + cls_cp[switch["name"]] = cl_encap # at least 1 connection point has to be received if not vlan_processed: - raise SdnConnectorError(message=SdnError.UNSUPPORTED_FEATURE, - http_code=406) + raise SdnConnectorError( + message=SdnError.UNSUPPORTED_FEATURE, http_code=406 + ) for s in self.switches: # for cl in cp_configLets: - cl_name = (self.__OSM_PREFIX + - s + - self.__SEPARATOR + service_type + str(vlan_id) + - self.__SEPARATOR + service_uuid) - cl_config = '' + cl_name = ( + self.__OSM_PREFIX + + s + + self.__SEPARATOR + + service_type + + str(vlan_id) + + self.__SEPARATOR + + service_uuid + ) + cl_config = "" + # Apply BGP configuration only for VXLAN topologies if self.topology in (self._VXLAN_MLAG, self._VXLAN): if service_type == self.__service_types_ELAN: - cl_bgp[s] = self.clC.getElan_bgp(service_uuid, - vlan_id, - vni_id, - self.switches[s]['lo0'], - self.switches[s]['AS']) + cl_bgp[s] = self.clC.getElan_bgp( + service_uuid, + vlan_id, + vni_id, + self.switches[s]["lo0"], + self.switches[s]["AS"], + ) else: - cl_bgp[s] = self.clC.getEline_bgp(service_uuid, - vlan_id, - vni_id, - self.switches[s]['lo0'], - self.switches[s]['AS']) + cl_bgp[s] = self.clC.getEline_bgp( + service_uuid, + vlan_id, + vni_id, + self.switches[s]["lo0"], + self.switches[s]["AS"], + ) else: - cl_bgp[s] = '' + cl_bgp[s] = "" if not cls_cp.get(s): # Apply VLAN configuration to peer MLAG switch, # only necessary when there are no connection points in the switch if self.topology in (self._VXLAN_MLAG, self._VLAN_MLAG): for p in self.switches: - if self.switches[p]['mlagPeerDevice'] == s: + if self.switches[p]["mlagPeerDevice"] == s: if cls_cp.get(p): if self.topology == self._VXLAN_MLAG: cl_config = str(cl_vlan) + str(cl_bgp[s]) @@ -699,7 +819,7 @@ class AristaSdnConnector(SdnConnectorBase): else: cl_config = str(cl_vlan) + str(cl_bgp[s]) + str(cls_cp[s]) - cls_perSw[s] = [{'name': cl_name, 'config': cl_config}] + cls_perSw[s] = [{"name": cl_name, "config": cl_config}] allLeafConfigured, allLeafModified = self.__updateConnection(cls_perSw) @@ -710,17 +830,19 @@ class AristaSdnConnector(SdnConnectorBase): "vlan_id": vlan_id, "connection_points": connection_points, "configLetPerSwitch": cls_perSw, - 'allLeafConfigured': allLeafConfigured, - 'allLeafModified': allLeafModified} + "allLeafConfigured": allLeafConfigured, + "allLeafModified": allLeafModified, + } return service_uuid, conn_info except Exception as ex: - self.logger.debug("Exception processing connection {}: {}". - format(service_uuid, str(ex))) + self.logger.debug( + "Exception processing connection {}: {}".format(service_uuid, str(ex)) + ) raise ex def __updateConnection(self, cls_perSw): - """ Invoked in the creation and modification + """Invoked in the creation and modification checks if the new connection points config is: - already in the Cloud Vision, the configLet is modified, and applied to the switch, @@ -740,18 +862,21 @@ class AristaSdnConnector(SdnConnectorBase): for s in self.switches: allLeafConfigured[s] = False allLeafModified[s] = False + cl_toDelete = [] + for s in self.switches: toDelete_in_cvp = False - if not (cls_perSw.get(s) and cls_perSw[s][0].get('config')): + if not (cls_perSw.get(s) and cls_perSw[s][0].get("config")): # when there is no configuration, means that there is no interface # in the switch to be connected, so the configLet has to be removed from CloudVision # after removing the ConfigLet from the switch if it was already there # get config let name and key cl = cls_perSw[s] + try: - cvp_cl = self.client.api.get_configlet_by_name(cl[0]['name']) + cvp_cl = self.client.api.get_configlet_by_name(cl[0]["name"]) # remove configLet cl_toDelete.append(cvp_cl) cl[0] = cvp_cl @@ -765,69 +890,84 @@ class AristaSdnConnector(SdnConnectorBase): else: res = self.__configlet_modify(cls_perSw[s]) allLeafConfigured[s] = res[0] + if not allLeafConfigured[s]: continue + cl = cls_perSw[s] + res = self.__device_modify( - device_to_update=s, - new_configlets=cl, - delete=toDelete_in_cvp) + device_to_update=s, new_configlets=cl, delete=toDelete_in_cvp + ) + if "errorMessage" in str(res): raise Exception(str(res)) + self.logger.info("Device {} modify result {}".format(s, res)) - for t_id in res[1]['tasks']: + + for t_id in res[1]["tasks"]: if not toDelete_in_cvp: - note_msg = "{}{}{}{}##".format(self.__MANAGED_BY_OSM, - self.__SEPARATOR, - t_id, - self.__SEPARATOR) + note_msg = "{}{}{}{}##".format( + self.__MANAGED_BY_OSM, + self.__SEPARATOR, + t_id, + self.__SEPARATOR, + ) self.client.api.add_note_to_configlet( - cls_perSw[s][0]['key'], - note_msg) - cls_perSw[s][0]['note'] = note_msg - tasks = {t_id: {'workOrderId': t_id}} + cls_perSw[s][0]["key"], note_msg + ) + cls_perSw[s][0]["note"] = note_msg + + tasks = {t_id: {"workOrderId": t_id}} self.__exec_task(tasks, self.__EXC_TASK_EXEC_WAIT) + # with just one configLet assigned to a device, # delete all if there are errors in next loops if not toDelete_in_cvp: allLeafModified[s] = True + if len(cl_toDelete) > 0: self.__configlet_modify(cl_toDelete, delete=True) return allLeafConfigured, allLeafModified except Exception as ex: try: - self.__rollbackConnection(cls_perSw, - allLeafConfigured, - allLeafModified) + self.__rollbackConnection(cls_perSw, allLeafConfigured, allLeafModified) except Exception as e: - self.logger.error("Exception rolling back in updating connection: {}". - format(e), exc_info=True) + self.logger.error( + "Exception rolling back in updating connection: {}".format(e), + exc_info=True, + ) + raise ex - def __rollbackConnection(self, - cls_perSw, - allLeafConfigured, - allLeafModified): - """ Removes the given configLet from the devices and then remove the configLets - """ + def __rollbackConnection(self, cls_perSw, allLeafConfigured, allLeafModified): + """Removes the given configLet from the devices and then remove the configLets""" for s in self.switches: if allLeafModified[s]: try: res = self.__device_modify( device_to_update=s, new_configlets=cls_perSw[s], - delete=True) + delete=True, + ) + if "errorMessage" in str(res): raise Exception(str(res)) + tasks = dict() - for t_id in res[1]['tasks']: - tasks[t_id] = {'workOrderId': t_id} + + for t_id in res[1]["tasks"]: + tasks[t_id] = {"workOrderId": t_id} + self.__exec_task(tasks) self.logger.info("Device {} modify result {}".format(s, res)) except Exception as e: - self.logger.error('Error removing configlets from device {}: {}'.format(s, e)) + self.logger.error( + "Error removing configlets from device {}: {}".format(s, e) + ) pass + for s in self.switches: if allLeafConfigured[s]: self.__configlet_modify(cls_perSw[s], delete=True) @@ -835,23 +975,27 @@ class AristaSdnConnector(SdnConnectorBase): def __exec_task(self, tasks, tout=10): if self.taskC is None: self.__connect() + data = self.taskC.update_all_tasks(tasks).values() - self.taskC.task_action(data, tout, 'executed') + self.taskC.task_action(data, tout, "executed") def __device_modify(self, device_to_update, new_configlets, delete): - """ Updates the devices (switches) adding or removing the configLet, + """Updates the devices (switches) adding or removing the configLet, the tasks Id's associated to the change are returned """ - self.logger.info('Enter in __device_modify delete: {}'.format(delete)) + self.logger.info("Enter in __device_modify delete: {}".format(delete)) updated = [] changed = False # Task Ids that have been identified during device actions newTasks = [] - if (len(new_configlets) == 0 or - device_to_update is None or - len(device_to_update) == 0): - data = {'updated': updated, 'tasks': newTasks} + if ( + len(new_configlets) == 0 + or device_to_update is None + or len(device_to_update) == 0 + ): + data = {"updated": updated, "tasks": newTasks} + return [changed, data] self.__load_inventory() @@ -862,17 +1006,21 @@ class AristaSdnConnector(SdnConnectorBase): for try_device in allDeviceFacts: # Add Device Specific Configlets # self.logger.debug(device) - if try_device['hostname'] not in device_to_update: + if try_device["hostname"] not in device_to_update: continue + dev_cvp_configlets = self.client.api.get_configlets_by_device_id( - try_device['systemMacAddress']) + try_device["systemMacAddress"] + ) # self.logger.debug(dev_cvp_configlets) - try_device['deviceSpecificConfiglets'] = [] + try_device["deviceSpecificConfiglets"] = [] + for cvp_configlet in dev_cvp_configlets: - if int(cvp_configlet['containerCount']) == 0: - try_device['deviceSpecificConfiglets'].append( - {'name': cvp_configlet['name'], - 'key': cvp_configlet['key']}) + if int(cvp_configlet["containerCount"]) == 0: + try_device["deviceSpecificConfiglets"].append( + {"name": cvp_configlet["name"], "key": cvp_configlet["key"]} + ) + # self.logger.debug(device) device = try_device break @@ -884,81 +1032,95 @@ class AristaSdnConnector(SdnConnectorBase): update_devices = [] if delete: - for cvp_configlet in device['deviceSpecificConfiglets']: + for cvp_configlet in device["deviceSpecificConfiglets"]: for cl in new_configlets: - if cvp_configlet['name'] == cl['name']: + if cvp_configlet["name"] == cl["name"]: remove_configlets.append(cvp_configlet) device_update = True else: for configlet in new_configlets: - if configlet not in device['deviceSpecificConfiglets']: + if configlet not in device["deviceSpecificConfiglets"]: add_configlets.append(configlet) device_update = True + if device_update: - update_devices.append({'hostname': device['hostname'], - 'configlets': [add_configlets, - remove_configlets], - 'device': device}) + update_devices.append( + { + "hostname": device["hostname"], + "configlets": [add_configlets, remove_configlets], + "device": device, + } + ) + self.logger.info("Device to modify: {}".format(update_devices)) up_device = update_devices[0] - cl_toAdd = up_device['configlets'][0] - cl_toDel = up_device['configlets'][1] + cl_toAdd = up_device["configlets"][0] + cl_toDel = up_device["configlets"][1] + # Update Configlets try: if delete and len(cl_toDel) > 0: r = self.client.api.remove_configlets_from_device( - 'OSM', - up_device['device'], - cl_toDel, - create_task=True) + "OSM", up_device["device"], cl_toDel, create_task=True + ) dev_action = r - self.logger.debug("remove_configlets_from_device {} {}".format(dev_action, cl_toDel)) + self.logger.debug( + "remove_configlets_from_device {} {}".format(dev_action, cl_toDel) + ) elif len(cl_toAdd) > 0: r = self.client.api.apply_configlets_to_device( - 'OSM', - up_device['device'], - cl_toAdd, - create_task=True) + "OSM", up_device["device"], cl_toAdd, create_task=True + ) dev_action = r - self.logger.debug("apply_configlets_to_device {} {}".format(dev_action, cl_toAdd)) - + self.logger.debug( + "apply_configlets_to_device {} {}".format(dev_action, cl_toAdd) + ) except Exception as error: errorMessage = str(error) - msg = "errorMessage: Device {} Configlets couldnot be updated: {}".format( - up_device['hostname'], errorMessage) + msg = "errorMessage: Device {} Configlets could not be updated: {}".format( + up_device["hostname"], errorMessage + ) raise SdnConnectorError(msg) from error else: if "errorMessage" in str(dev_action): m = "Device {} Configlets update fail: {}".format( - up_device['name'], dev_action['errorMessage']) + up_device["name"], dev_action["errorMessage"] + ) raise SdnConnectorError(m) else: changed = True - if 'taskIds' in str(dev_action): + if "taskIds" in str(dev_action): # Fix 1030 SDN-ARISTA Key error note when deploy a NS - if not dev_action['data']['taskIds']: - raise SdnConnectorError("No taskIds found: Device {} Configlets could not be updated".format( - up_device['hostname'])) - for taskId in dev_action['data']['taskIds']: - updated.append({ - up_device['hostname']: "Configlets-{}".format(taskId)}) + if not dev_action["data"]["taskIds"]: + raise SdnConnectorError( + "No taskIds found: Device {} Configlets could not be updated".format( + up_device["hostname"] + ) + ) + + for taskId in dev_action["data"]["taskIds"]: + updated.append( + {up_device["hostname"]: "Configlets-{}".format(taskId)} + ) newTasks.append(taskId) else: - updated.append({up_device['hostname']: - "Configlets-No_Specific_Tasks"}) - data = {'updated': updated, 'tasks': newTasks} + updated.append( + {up_device["hostname"]: "Configlets-No_Specific_Tasks"} + ) + + data = {"updated": updated, "tasks": newTasks} + return [changed, data] def __configlet_modify(self, configletsToApply, delete=False): - ''' adds/update or delete the provided configLets + """Adds/update or delete the provided configLets :param configletsToApply: list of configLets to apply :param delete: flag to indicate if the configLets have to be deleted from Cloud Vision Portal :return: data: dict of module actions and taskIDs - ''' - self.logger.info('Enter in __configlet_modify delete:{}'.format( - delete)) + """ + self.logger.info("Enter in __configlet_modify delete:{}".format(delete)) # Compare configlets against cvp_facts-configlets changed = False @@ -973,10 +1135,11 @@ class AristaSdnConnector(SdnConnectorBase): to_update = False to_create = False to_check = False + try: - cvp_cl = self.client.api.get_configlet_by_name(cl['name']) - cl['key'] = cvp_cl['key'] - cl['note'] = cvp_cl['note'] + cvp_cl = self.client.api.get_configlet_by_name(cl["name"]) + cl["key"] = cvp_cl["key"] + cl["note"] = cvp_cl["note"] found_in_cvp = True except CvpApiError as error: if "Entity does not exist" in error.msg: @@ -987,119 +1150,130 @@ class AristaSdnConnector(SdnConnectorBase): if delete: if found_in_cvp: to_delete = True - configlet = {'name': cvp_cl['name'], - 'data': cvp_cl} + configlet = {"name": cvp_cl["name"], "data": cvp_cl} else: if found_in_cvp: - cl_compare = self.__compare(cl['config'], - cvp_cl['config']) + cl_compare = self.__compare(cl["config"], cvp_cl["config"]) + # compare function returns a floating point number if cl_compare[0] != 100.0: to_update = True - configlet = {'name': cl['name'], - 'data': cvp_cl, - 'config': cl['config']} + configlet = { + "name": cl["name"], + "data": cvp_cl, + "config": cl["config"], + } else: to_check = True - configlet = {'name': cl['name'], - 'key': cvp_cl['key'], - 'data': cvp_cl, - 'config': cl['config']} + configlet = { + "name": cl["name"], + "key": cvp_cl["key"], + "data": cvp_cl, + "config": cl["config"], + } else: to_create = True - configlet = {'name': cl['name'], - 'config': cl['config']} + configlet = {"name": cl["name"], "config": cl["config"]} try: if to_delete: - operation = 'delete' + operation = "delete" resp = self.client.api.delete_configlet( - configlet['data']['name'], - configlet['data']['key']) + configlet["data"]["name"], configlet["data"]["key"] + ) elif to_update: - operation = 'update' + operation = "update" resp = self.client.api.update_configlet( - configlet['config'], - configlet['data']['key'], - configlet['data']['name'], - wait_task_ids=True) + configlet["config"], + configlet["data"]["key"], + configlet["data"]["name"], + wait_task_ids=True, + ) elif to_create: - operation = 'create' + operation = "create" resp = self.client.api.add_configlet( - configlet['name'], - configlet['config']) + configlet["name"], configlet["config"] + ) else: - operation = 'checked' - resp = 'checked' + operation = "checked" + resp = "checked" except Exception as error: - errorMessage = str(error).split(':')[-1] + errorMessage = str(error).split(":")[-1] message = "Configlet {} cannot be {}: {}".format( - cl['name'], operation, errorMessage) + cl["name"], operation, errorMessage + ) + if to_delete: - deleted.append({configlet['name']: message}) + deleted.append({configlet["name"]: message}) elif to_update: - updated.append({configlet['name']: message}) + updated.append({configlet["name"]: message}) elif to_create: - new.append({configlet['name']: message}) + new.append({configlet["name"]: message}) elif to_check: - checked.append({configlet['name']: message}) - + checked.append({configlet["name"]: message}) else: if "error" in str(resp).lower(): message = "Configlet {} cannot be deleted: {}".format( - cl['name'], resp['errorMessage']) + cl["name"], resp["errorMessage"] + ) + if to_delete: - deleted.append({configlet['name']: message}) + deleted.append({configlet["name"]: message}) elif to_update: - updated.append({configlet['name']: message}) + updated.append({configlet["name"]: message}) elif to_create: - new.append({configlet['name']: message}) + new.append({configlet["name"]: message}) elif to_check: - checked.append({configlet['name']: message}) + checked.append({configlet["name"]: message}) else: if to_delete: changed = True - deleted.append({configlet['name']: "success"}) + deleted.append({configlet["name"]: "success"}) elif to_update: changed = True - updated.append({configlet['name']: "success"}) + updated.append({configlet["name"]: "success"}) elif to_create: changed = True - cl['key'] = resp # This key is used in API call deviceApplyConfigLet FGA - new.append({configlet['name']: "success"}) + # This key is used in API call deviceApplyConfigLet FGA + cl["key"] = resp + new.append({configlet["name"]: "success"}) elif to_check: changed = False - checked.append({configlet['name']: "success"}) + checked.append({configlet["name"]: "success"}) + + data = {"new": new, "updated": updated, "deleted": deleted, "checked": checked} - data = {'new': new, 'updated': updated, 'deleted': deleted, 'checked': checked} return [changed, data] def __get_configletsDevices(self, configlets): for s in self.switches: configlet = configlets[s] + # Add applied Devices if len(configlet) > 0: - configlet['devices'] = [] - applied_devices = self.client.api.get_applied_devices( - configlet['name']) - for device in applied_devices['data']: - configlet['devices'].append(device['hostName']) + configlet["devices"] = [] + applied_devices = self.client.api.get_applied_devices(configlet["name"]) + + for device in applied_devices["data"]: + configlet["devices"].append(device["hostName"]) def __get_serviceData(self, service_uuid, service_type, vlan_id, conn_info=None): cls_perSw = {} + for s in self.switches: cls_perSw[s] = [] + if not conn_info: - srv_cls = self.__get_serviceConfigLets(service_uuid, - service_type, - vlan_id) + srv_cls = self.__get_serviceConfigLets(service_uuid, service_type, vlan_id) self.__get_configletsDevices(srv_cls) + for s in self.switches: cl = srv_cls[s] if len(cl) > 0: - for dev in cl['devices']: + for dev in cl["devices"]: cls_perSw[dev].append(cl) else: - cls_perSw = conn_info['configLetPerSwitch'] + cls_perSw = conn_info["configLetPerSwitch"] + return cls_perSw def delete_connectivity_service(self, service_uuid, conn_info=None): @@ -1113,59 +1287,77 @@ class AristaSdnConnector(SdnConnectorBase): :raises: SdnConnectorException: In case of error. The parameter http_code must be filled """ try: - self.logger.debug('invoked delete_connectivity_service {}'. - format(service_uuid)) + self.logger.debug( + "invoked delete_connectivity_service {}".format(service_uuid) + ) + if not service_uuid: - raise SdnConnectorError(message='No connection service UUID', - http_code=500) + raise SdnConnectorError( + message="No connection service UUID", http_code=500 + ) self.__get_Connection() + if conn_info is None: - raise SdnConnectorError(message='No connection information for service UUID {}'.format(service_uuid), - http_code=500) + raise SdnConnectorError( + message="No connection information for service UUID {}".format( + service_uuid + ), + http_code=500, + ) + c_info = None - cls_perSw = self.__get_serviceData(service_uuid, - conn_info['service_type'], - conn_info['vlan_id'], - c_info) + cls_perSw = self.__get_serviceData( + service_uuid, conn_info["service_type"], conn_info["vlan_id"], c_info + ) allLeafConfigured = {} allLeafModified = {} + for s in self.switches: allLeafConfigured[s] = True allLeafModified[s] = True + found_in_cvp = False + for s in self.switches: if cls_perSw[s]: found_in_cvp = True + if found_in_cvp: - self.__rollbackConnection(cls_perSw, - allLeafConfigured, - allLeafModified) + self.__rollbackConnection(cls_perSw, allLeafConfigured, allLeafModified) else: # if the service is not defined in Cloud Vision, return a 404 - NotFound error - raise SdnConnectorError(message='Service {} was not found in Arista Cloud Vision {}'. - format(service_uuid, self.__wim_url), - http_code=404) + raise SdnConnectorError( + message="Service {} was not found in Arista Cloud Vision {}".format( + service_uuid, self.__wim_url + ), + http_code=404, + ) + self.__removeMetadata(service_uuid) except CvpLoginError as e: self.logger.info(str(e)) self.client = None - raise SdnConnectorError(message=SdnError.UNAUTHORIZED + " " + str(e), - http_code=401) from e + raise SdnConnectorError( + message=SdnError.UNAUTHORIZED + " " + str(e), http_code=401 + ) from e except SdnConnectorError as sde: raise sde except Exception as ex: self.client = None self.logger.error(ex) + if self.raiseException: raise ex - raise SdnConnectorError(message=SdnError.INTERNAL_ERROR + " " + str(ex), - http_code=500) from ex + + raise SdnConnectorError( + message=SdnError.INTERNAL_ERROR + " " + str(ex), http_code=500 + ) from ex def __addMetadata(self, service_uuid, service_type, vlan_id): - """ Adds the connectivity service from 'OSM_metadata' configLet - """ + """Adds the connectivity service from 'OSM_metadata' configLet""" found_in_cvp = False + try: cvp_cl = self.client.api.get_configlet_by_name(self.__OSM_METADATA) found_in_cvp = True @@ -1174,24 +1366,31 @@ class AristaSdnConnector(SdnConnectorBase): pass else: raise error + try: - new_serv = '{} {} {} {}\n'.format(self.__METADATA_PREFIX, service_type, vlan_id, service_uuid) + new_serv = "{} {} {} {}\n".format( + self.__METADATA_PREFIX, service_type, vlan_id, service_uuid + ) if found_in_cvp: - cl_config = cvp_cl['config'] + new_serv + cl_config = cvp_cl["config"] + new_serv else: cl_config = new_serv - cl_meta = [{'name': self.__OSM_METADATA, 'config': cl_config}] + + cl_meta = [{"name": self.__OSM_METADATA, "config": cl_config}] self.__configlet_modify(cl_meta) except Exception as e: - self.logger.error('Error in setting metadata in CloudVision from OSM for service {}: {}'. - format(service_uuid, str(e))) + self.logger.error( + "Error in setting metadata in CloudVision from OSM for service {}: {}".format( + service_uuid, str(e) + ) + ) pass def __removeMetadata(self, service_uuid): - """ Removes the connectivity service from 'OSM_metadata' configLet - """ + """Removes the connectivity service from 'OSM_metadata' configLet""" found_in_cvp = False + try: cvp_cl = self.client.api.get_configlet_by_name(self.__OSM_METADATA) found_in_cvp = True @@ -1200,28 +1399,32 @@ class AristaSdnConnector(SdnConnectorBase): pass else: raise error + try: if found_in_cvp: - if service_uuid in cvp_cl['config']: - cl_config = '' - for line in cvp_cl['config'].split('\n'): + if service_uuid in cvp_cl["config"]: + cl_config = "" + + for line in cvp_cl["config"].split("\n"): if service_uuid in line: continue else: cl_config = cl_config + line - cl_meta = [{'name': self.__OSM_METADATA, 'config': cl_config}] + + cl_meta = [{"name": self.__OSM_METADATA, "config": cl_config}] self.__configlet_modify(cl_meta) except Exception as e: - self.logger.error('Error in removing metadata in CloudVision from OSM for service {}: {}'. - format(service_uuid, str(e))) + self.logger.error( + "Error in removing metadata in CloudVision from OSM for service {}: {}".format( + service_uuid, str(e) + ) + ) pass - 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`. @@ -1243,43 +1446,52 @@ class AristaSdnConnector(SdnConnectorBase): SdnConnectorError: In case of error. """ try: - self.logger.debug('invoked edit_connectivity_service for service {}. ports: {}'.format(service_uuid, - connection_points)) + self.logger.debug( + "invoked edit_connectivity_service for service {}. ports: {}".format( + service_uuid, connection_points + ) + ) if not service_uuid: - raise SdnConnectorError(message='Unable to perform operation, missing or empty uuid', - http_code=500) + raise SdnConnectorError( + message="Unable to perform operation, missing or empty uuid", + http_code=500, + ) + if not conn_info: - raise SdnConnectorError(message='Unable to perform operation, missing or empty connection information', - http_code=500) + raise SdnConnectorError( + message="Unable to perform operation, missing or empty connection information", + http_code=500, + ) if connection_points is None: return None self.__get_Connection() - cls_currentPerSw = conn_info['configLetPerSwitch'] - service_type = conn_info['service_type'] - - self.__check_service(service_type, - connection_points, - check_vlan=False, - check_num_cp=False, - kwargs=kwargs) + cls_currentPerSw = conn_info["configLetPerSwitch"] + service_type = conn_info["service_type"] - s_uid, s_connInf = self.__processConnection( - service_uuid, + self.__check_service( service_type, connection_points, - kwargs) - self.logger.info("Service with uuid {} configuration updated". - format(s_uid)) + check_vlan=False, + check_num_cp=False, + kwargs=kwargs, + ) + + s_uid, s_connInf = self.__processConnection( + service_uuid, service_type, connection_points, kwargs + ) + self.logger.info("Service with uuid {} configuration updated".format(s_uid)) + return s_connInf except CvpLoginError as e: self.logger.info(str(e)) self.client = None - raise SdnConnectorError(message=SdnError.UNAUTHORIZED + " " + str(e), - http_code=401) from e + raise SdnConnectorError( + message=SdnError.UNAUTHORIZED + " " + str(e), http_code=401 + ) from e except SdnConnectorError as sde: raise sde except Exception as ex: @@ -1288,92 +1500,113 @@ class AristaSdnConnector(SdnConnectorBase): # TODO check if there are pending task, and cancel them before restoring self.__updateConnection(cls_currentPerSw) except Exception as e: - self.logger.error("Unable to restore configuration in service {} after an error in the configuration" - " updated: {}".format(service_uuid, str(e))) + self.logger.error( + "Unable to restore configuration in service {} after an error in the configuration" + " updated: {}".format(service_uuid, str(e)) + ) + if self.raiseException: raise ex - raise SdnConnectorError(message=str(ex), - http_code=500) from ex + + raise SdnConnectorError(message=str(ex), http_code=500) from ex def clear_all_connectivity_services(self): - """ Removes all connectivity services from Arista CloudVision with two steps: - - retrives all the services from Arista CloudVision + """Removes all connectivity services from Arista CloudVision with two steps: + - retrieves all the services from Arista CloudVision - removes each service """ try: - self.logger.debug('invoked AristaImpl ' + - 'clear_all_connectivity_services') + self.logger.debug("invoked AristaImpl clear_all_connectivity_services") self.__get_Connection() s_list = self.__get_srvUUIDs() + for serv in s_list: conn_info = {} - conn_info['service_type'] = serv['type'] - conn_info['vlan_id'] = serv['vlan'] - - self.delete_connectivity_service(serv['uuid'], conn_info) + conn_info["service_type"] = serv["type"] + conn_info["vlan_id"] = serv["vlan"] + self.delete_connectivity_service(serv["uuid"], conn_info) except CvpLoginError as e: self.logger.info(str(e)) self.client = None - raise SdnConnectorError(message=SdnError.UNAUTHORIZED + " " + str(e), - http_code=401) from e + + raise SdnConnectorError( + message=SdnError.UNAUTHORIZED + " " + str(e), http_code=401 + ) from e except SdnConnectorError as sde: raise sde except Exception as ex: self.client = None self.logger.error(ex) + if self.raiseException: raise ex - raise SdnConnectorError(message=SdnError.INTERNAL_ERROR + " " + str(ex), - http_code=500) from ex + + raise SdnConnectorError( + message=SdnError.INTERNAL_ERROR + " " + str(ex), http_code=500 + ) from ex def get_all_active_connectivity_services(self): - """ Return the uuid of all the active connectivity services with two steps: + """Return the uuid of all the active connectivity services with two steps: - retrives all the services from Arista CloudVision - retrives the status of each server """ try: - self.logger.debug('invoked AristaImpl {}'.format( - 'get_all_active_connectivity_services')) + self.logger.debug( + "invoked AristaImpl {}".format("get_all_active_connectivity_services") + ) self.__get_Connection() s_list = self.__get_srvUUIDs() result = [] + for serv in s_list: conn_info = {} - conn_info['service_type'] = serv['type'] - conn_info['vlan_id'] = serv['vlan'] + conn_info["service_type"] = serv["type"] + conn_info["vlan_id"] = serv["vlan"] + status = self.get_connectivity_service_status(serv["uuid"], conn_info) + + if status["sdn_status"] == "ACTIVE": + result.append(serv["uuid"]) - status = self.get_connectivity_service_status(serv['uuid'], conn_info) - if status['sdn_status'] == 'ACTIVE': - result.append(serv['uuid']) return result except CvpLoginError as e: self.logger.info(str(e)) self.client = None - raise SdnConnectorError(message=SdnError.UNAUTHORIZED + " " + str(e), - http_code=401) from e + raise SdnConnectorError( + message=SdnError.UNAUTHORIZED + " " + str(e), http_code=401 + ) from e except SdnConnectorError as sde: raise sde except Exception as ex: self.client = None self.logger.error(ex) + if self.raiseException: raise ex - raise SdnConnectorError(message=SdnError.INTERNAL_ERROR, - http_code=500) from ex + + raise SdnConnectorError( + message=SdnError.INTERNAL_ERROR, http_code=500 + ) from ex def __get_serviceConfigLets(self, service_uuid, service_type, vlan_id): - """ Return the configLet's associated with a connectivity service, + """Return the configLet's associated with a connectivity service, There should be one, as maximum, per device (switch) for a given connectivity service """ srv_cls = {} + for s in self.switches: srv_cls[s] = [] found_in_cvp = False - name = (self.__OSM_PREFIX + - s + - self.__SEPARATOR + service_type + str(vlan_id) + - self.__SEPARATOR + service_uuid) + name = ( + self.__OSM_PREFIX + + s + + self.__SEPARATOR + + service_type + + str(vlan_id) + + self.__SEPARATOR + + service_uuid + ) + try: cvp_cl = self.client.api.get_configlet_by_name(name) found_in_cvp = True @@ -1382,16 +1615,19 @@ class AristaSdnConnector(SdnConnectorBase): pass else: raise error + if found_in_cvp: srv_cls[s] = cvp_cl + return srv_cls def __get_srvVLANs(self): - """ Returns a list with all the VLAN id's used in the connectivity services managed + """Returns a list with all the VLAN id's used in the connectivity services managed in tha Arista CloudVision by checking the 'OSM_metadata' configLet where this information is stored """ found_in_cvp = False + try: cvp_cl = self.client.api.get_configlet_by_name(self.__OSM_METADATA) found_in_cvp = True @@ -1400,26 +1636,28 @@ class AristaSdnConnector(SdnConnectorBase): pass else: raise error + s_vlan_list = [] if found_in_cvp: - lines = cvp_cl['config'].split('\n') + lines = cvp_cl["config"].split("\n") + for line in lines: if self.__METADATA_PREFIX in line: - s_vlan = line.split(' ')[3] + s_vlan = line.split(" ")[3] else: continue - if (s_vlan is not None and - len(s_vlan) > 0 and - s_vlan not in s_vlan_list): + + if s_vlan is not None and len(s_vlan) > 0 and s_vlan not in s_vlan_list: s_vlan_list.append(s_vlan) return s_vlan_list def __get_srvUUIDs(self): - """ Retrieves all the connectivity services, managed in tha Arista CloudVision + """Retrieves all the connectivity services, managed in tha Arista CloudVision by checking the 'OSM_metadata' configLet where this information is stored """ found_in_cvp = False + try: cvp_cl = self.client.api.get_configlet_by_name(self.__OSM_METADATA) found_in_cvp = True @@ -1428,29 +1666,31 @@ class AristaSdnConnector(SdnConnectorBase): pass else: raise error + serv_list = [] if found_in_cvp: - lines = cvp_cl['config'].split('\n') + lines = cvp_cl["config"].split("\n") + for line in lines: if self.__METADATA_PREFIX in line: - line = line.split(' ') - serv = {'uuid': line[4], 'type': line[2], 'vlan': line[3]} + line = line.split(" ") + serv = {"uuid": line[4], "type": line[2], "vlan": line[3]} else: continue - if (serv is not None and - len(serv) > 0 and - serv not in serv_list): + + if serv is not None and len(serv) > 0 and serv not in serv_list: serv_list.append(serv) return serv_list def __get_Connection(self): - """ Open a connection with Arista CloudVision, - invoking the version retrival as test + """Open a connection with Arista CloudVision, + invoking the version retrival as test """ try: if self.client is None: self.client = self.__connect() + self.client.api.get_cvp_info() except (CvpSessionLogOutError, RequestException) as e: self.logger.debug("Connection error '{}'. Reconnecting".format(e)) @@ -1458,12 +1698,13 @@ class AristaSdnConnector(SdnConnectorBase): self.client.api.get_cvp_info() def __connect(self): - ''' Connects to CVP device using user provided credentials from initialization. + """Connects to CVP device using user provided credentials from initialization. :return: CvpClient object with connection instantiated. - ''' + """ client = CvpClient() protocol, _, rest_url = self.__wim_url.rpartition("://") host, _, port = rest_url.partition(":") + if port and port.endswith("/"): port = int(port[:-1]) elif port: @@ -1471,18 +1712,21 @@ class AristaSdnConnector(SdnConnectorBase): else: port = 443 - client.connect([host], - self.__user, - self.__passwd, - protocol=protocol or "https", - port=port, - connect_timeout=2) + client.connect( + [host], + self.__user, + self.__passwd, + protocol=protocol or "https", + port=port, + connect_timeout=2, + ) client.api = CvpApi(client, request_timeout=self.__API_REQUEST_TOUT) self.taskC = AristaCVPTask(client.api) + return client def __compare(self, fromText, toText, lines=10): - """ Compare text string in 'fromText' with 'toText' and produce + """Compare text string in 'fromText' with 'toText' and produce diffRatio - a score as a float in the range [0, 1] 2.0*M / T T is the total number of elements in both sequences, M is the number of matches. @@ -1499,108 +1743,162 @@ class AristaSdnConnector(SdnConnectorBase): tolines = toText.splitlines(1) diff = list(difflib.unified_diff(fromlines, tolines, n=lines)) textComp = difflib.SequenceMatcher(None, fromText, toText) - diffRatio = round(textComp.quick_ratio()*100, 2) + diffRatio = round(textComp.quick_ratio() * 100, 2) + return [diffRatio, diff] def __load_inventory(self): - """ Get Inventory Data for All Devices (aka switches) from the Arista CloudVision - """ + """Get Inventory Data for All Devices (aka switches) from the Arista CloudVision""" if not self.cvp_inventory: self.cvp_inventory = self.client.api.get_inventory() + self.allDeviceFacts = [] + for device in self.cvp_inventory: self.allDeviceFacts.append(device) def __get_tags(self, name, value): if not self.cvp_tags: self.cvp_tags = [] - url = '/api/v1/rest/analytics/tags/labels/devices/{}/value/{}/elements'.format(name, value) - self.logger.debug('get_tags: URL {}'.format(url)) + url = "/api/v1/rest/analytics/tags/labels/devices/{}/value/{}/elements".format( + name, value + ) + self.logger.debug("get_tags: URL {}".format(url)) data = self.client.get(url, timeout=self.__API_REQUEST_TOUT) - for dev in data['notifications']: - for elem in dev['updates']: + + for dev in data["notifications"]: + for elem in dev["updates"]: self.cvp_tags.append(elem) - self.logger.debug('Available devices with tag_name {} - value {}: {} '.format(name, value, self.cvp_tags)) + + self.logger.debug( + "Available devices with tag_name {} - value {}: {}".format( + name, value, self.cvp_tags + ) + ) def __get_interface_ip(self, device_id, interface): - url = '/api/v1/rest/{}/Sysdb/ip/config/ipIntfConfig/{}/'.format(device_id, interface) - self.logger.debug('get_interface_ip: URL {}'.format(url)) + url = "/api/v1/rest/{}/Sysdb/ip/config/ipIntfConfig/{}/".format( + device_id, interface + ) + self.logger.debug("get_interface_ip: URL {}".format(url)) data = None + try: data = self.client.get(url, timeout=self.__API_REQUEST_TOUT) - if data['notifications']: - for notification in data['notifications']: - for update in notification['updates']: - if update == 'addrWithMask': - return notification['updates'][update]['value'] + + if data["notifications"]: + for notification in data["notifications"]: + for update in notification["updates"]: + if update == "addrWithMask": + return notification["updates"][update]["value"] except Exception as e: - raise SdnConnectorError("Invalid response from url {}: data {} - {}".format(url, data, str(e))) - raise SdnConnectorError("Unable to get ip for interface {} in device {}, data {}". - format(interface, device_id, data)) + raise SdnConnectorError( + "Invalid response from url {}: data {} - {}".format(url, data, str(e)) + ) + + raise SdnConnectorError( + "Unable to get ip for interface {} in device {}, data {}".format( + interface, device_id, data + ) + ) def __get_device_ASN(self, device_id): - url = '/api/v1/rest/{}/Sysdb/routing/bgp/config/'.format(device_id) - self.logger.debug('get_device_ASN: URL {}'.format(url)) + url = "/api/v1/rest/{}/Sysdb/routing/bgp/config/".format(device_id) + self.logger.debug("get_device_ASN: URL {}".format(url)) data = None + try: data = self.client.get(url, timeout=self.__API_REQUEST_TOUT) - if data['notifications']: - for notification in data['notifications']: - for update in notification['updates']: - if update == 'asNumber': - return notification['updates'][update]['value']['value']['int'] + if data["notifications"]: + for notification in data["notifications"]: + for update in notification["updates"]: + if update == "asNumber": + return notification["updates"][update]["value"]["value"][ + "int" + ] except Exception as e: - raise SdnConnectorError("Invalid response from url {}: data {} - {}".format(url, data, str(e))) - raise SdnConnectorError("Unable to get AS in device {}, data {}".format(device_id, data)) + raise SdnConnectorError( + "Invalid response from url {}: data {} - {}".format(url, data, str(e)) + ) + + raise SdnConnectorError( + "Unable to get AS in device {}, data {}".format(device_id, data) + ) def __get_peer_MLAG(self, device_id): peer = None - url = '/api/v1/rest/{}/Sysdb/mlag/status/'.format(device_id) - self.logger.debug('get_MLAG_status: URL {}'.format(url)) + url = "/api/v1/rest/{}/Sysdb/mlag/status/".format(device_id) + self.logger.debug("get_MLAG_status: URL {}".format(url)) + try: data = self.client.get(url, timeout=self.__API_REQUEST_TOUT) - if data['notifications']: + + if data["notifications"]: found = False - for notification in data['notifications']: - for update in notification['updates']: - if update == 'systemId': - mlagSystemId = notification['updates'][update]['value'] + + for notification in data["notifications"]: + for update in notification["updates"]: + if update == "systemId": + mlagSystemId = notification["updates"][update]["value"] found = True break + if found: break + # search the MLAG System Id if found: for s in self.switches: - if self.switches[s]['serialNumber'] == device_id: + if self.switches[s]["serialNumber"] == device_id: continue - url = '/api/v1/rest/{}/Sysdb/mlag/status/'.format(self.switches[s]['serialNumber']) - self.logger.debug('Searching for MLAG system id {} in switch {}'.format(mlagSystemId, s)) + + url = "/api/v1/rest/{}/Sysdb/mlag/status/".format( + self.switches[s]["serialNumber"] + ) + self.logger.debug( + "Searching for MLAG system id {} in switch {}".format( + mlagSystemId, s + ) + ) data = self.client.get(url, timeout=self.__API_REQUEST_TOUT) found = False - for notification in data['notifications']: - for update in notification['updates']: - if update == 'systemId': - if mlagSystemId == notification['updates'][update]['value']: + + for notification in data["notifications"]: + for update in notification["updates"]: + if update == "systemId": + if ( + mlagSystemId + == notification["updates"][update]["value"] + ): peer = s found = True break + if found: break + if found: break + if peer is None: - self.logger.error('No Peer device found for device {} with MLAG address {}'.format(device_id, - mlagSystemId)) + self.logger.error( + "No Peer device found for device {} with MLAG address {}".format( + device_id, mlagSystemId + ) + ) else: - self.logger.debug('Peer MLAG for device {} - value {}'.format(device_id, peer)) + self.logger.debug( + "Peer MLAG for device {} - value {}".format(device_id, peer) + ) + return peer except Exception: - raise SdnConnectorError("Invalid response from url {}: data {}".format(url, data)) + raise SdnConnectorError( + "Invalid response from url {}: data {}".format(url, data) + ) def is_valid_destination(self, url): - """ Check that the provided WIM URL is correct - """ + """Check that the provided WIM URL is correct""" if re.match(self.__regex, url): return True elif self.is_valid_ipv4_address(url): @@ -1609,8 +1907,7 @@ class AristaSdnConnector(SdnConnectorBase): return self.is_valid_ipv6_address(url) def is_valid_ipv4_address(self, address): - """ Checks that the given IP is IPv4 valid - """ + """Checks that the given IP is IPv4 valid""" try: socket.inet_pton(socket.AF_INET, address) except AttributeError: # no inet_pton here, sorry @@ -1618,25 +1915,30 @@ class AristaSdnConnector(SdnConnectorBase): socket.inet_aton(address) except socket.error: return False - return address.count('.') == 3 + + return address.count(".") == 3 except socket.error: # not a valid address return False + return True def is_valid_ipv6_address(self, address): - """ Checks that the given IP is IPv6 valid - """ + """Checks that the given IP is IPv6 valid""" try: socket.inet_pton(socket.AF_INET6, address) except socket.error: # not a valid address return False + return True def delete_keys_from_dict(self, dict_del, lst_keys): if dict_del is None: return dict_del + dict_copy = {k: v for k, v in dict_del.items() if k not in lst_keys} + for k, v in dict_copy.items(): if isinstance(v, dict): dict_copy[k] = self.delete_keys_from_dict(v, lst_keys) + return dict_copy