X-Git-Url: https://osm.etsi.org/gitweb/?a=blobdiff_plain;f=RO-SDN-arista%2Fosm_rosdn_arista%2Fwimconn_arista.py;fp=RO-SDN-arista%2Fosm_rosdn_arista%2Fwimconn_arista.py;h=0000000000000000000000000000000000000000;hb=667d158c0d3ee7b4c176ad0b27ac428c81b0ddbc;hp=571add6853d12a80c861e838004b7cf8b8dc1ef8;hpb=07df0d2f27b26e7723b923c9583970be9530fe69;p=osm%2FRO.git diff --git a/RO-SDN-arista/osm_rosdn_arista/wimconn_arista.py b/RO-SDN-arista/osm_rosdn_arista/wimconn_arista.py deleted file mode 100644 index 571add68..00000000 --- a/RO-SDN-arista/osm_rosdn_arista/wimconn_arista.py +++ /dev/null @@ -1,1621 +0,0 @@ -# -*- coding: utf-8 -*- -## -# Copyright 2019 Atos - CoE Telco NFV Team -# All Rights Reserved. -# -# Contributors: Oscar Luis Peral, Atos -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# -# For those usages not covered by the Apache License, Version 2.0 please -# contact with: -# -# Neither the name of Atos nor the names of its -# contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# This work has been performed in the context of Arista Telefonica OSM PoC. -## -from osm_ro.wim.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 - -import logging -import uuid -from enum import Enum -from requests import RequestException - -from cvprac.cvp_client import CvpClient -from cvprac.cvp_api import CvpApi -from cvprac.cvp_client_errors import CvpLoginError, CvpSessionLogOutError, CvpApiError -from cvprac import __version__ as cvprac_version - -from osm_rosdn_arista.aristaConfigLet import AristaSDNConfigLet -from osm_rosdn_arista.aristaTask import AristaCVPTask - - -class SdnError(Enum): - UNREACHABLE = 'Unable to reach the WIM.', - 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" - - -class AristaSdnConnector(SdnConnectorBase): - """Arista class for the SDN connectors - - Arguments: - wim (dict): WIM record, as stored in the database - wim_account (dict): WIM account record, as stored in the database - config - The arguments of the constructor are converted to object attributes. - An extra property, ``service_endpoint_mapping`` is created from ``config``. - - The access to Arista CloudVision is made through the API defined in - https://github.com/aristanetworks/cvprac - The a connectivity service consist in creating a VLAN and associate the interfaces - of the connection points MAC addresses to this VLAN in all the switches of the topology, - the BDP is also configured for this VLAN. - - The Arista Cloud Vision API workflow is the following - -- The switch configuration is defined as a set of switch configuration commands, - what is called 'ConfigLet' - -- The ConfigLet is associated to the device (leaf switch) - -- Automatically a task is associated to this activity for change control, the task - in this stage is in 'Pending' state - -- The task will be executed so that the configuration is applied to the switch. - -- The service information is saved in the response of the creation call - -- 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 = 'openmano.sdnconn.arista' - __SERVICE_ENDPOINT_MAPPING = 'service_endpoint_mapping' - __ENCAPSULATION_TYPE_PARAM = "service_endpoint_encapsulation_type" - __ENCAPSULATION_INFO_PARAM = "service_endpoint_encapsulation_info" - __BACKUP_PARAM = "backup" - __BANDWIDTH_PARAM = "bandwidth" - __SERVICE_ENDPOINT_PARAM = "service_endpoint_id" - __MAC_PARAM = "mac" - __WAN_SERVICE_ENDPOINT_PARAM = "service_endpoint_id" - __WAN_MAPPING_INFO_PARAM = "service_mapping_info" - __DEVICE_ID_PARAM = "device_id" - __DEVICE_INTERFACE_ID_PARAM = "device_interface_id" - __SW_ID_PARAM = "switch_dpid" - __SW_PORT_PARAM = "switch_port" - __VLAN_PARAM = "vlan" - __VNI_PARAM = "vni" - __SEPARATOR = '_' - __MANAGED_BY_OSM = '## Managed by OSM ' - __OSM_PREFIX = "osm_" - __OSM_METADATA = "OSM_metadata" - __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' - __LOOPBACK_INTF = "Loopback0" - _VLAN = "VLAN" - _VXLAN = "VXLAN" - _VLAN_MLAG = "VLAN-MLAG" - _VXLAN_MLAG = "VXLAN-MLAG" - - - def __init__(self, wim, wim_account, config=None, logger=None): - """ - - :param wim: (dict). Contains among others 'wim_url' - :param wim_account: (dict). Contains among others 'uuid' (internal id), 'name', - 'sdn' (True if is intended for SDN-assist or False if intended for WIM), 'user', 'password'. - :param config: (dict or None): Particular information of plugin. These keys if present have a common meaning: - 'mapping_not_needed': (bool) False by default or if missing, indicates that mapping is not needed. - 'service_endpoint_mapping': (list) provides the internal endpoint mapping. The meaning is: - KEY meaning for WIM meaning for SDN assist - -------- -------- -------- - device_id pop_switch_dpid compute_id - device_interface_id pop_switch_port compute_pci_address - service_endpoint_id wan_service_endpoint_id SDN_service_endpoint_id - service_mapping_info wan_service_mapping_info SDN_service_mapping_info - contains extra information if needed. Text in Yaml format - switch_dpid wan_switch_dpid SDN_switch_dpid - switch_port wan_switch_port SDN_switch_port - datacenter_id vim_account vim_account - id: (internal, do not use) - wim_id: (internal, do not use) - :param logger (logging.Logger): optional logger object. If none is passed 'openmano.sdn.sdnconn' is used. - """ - self.__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 - 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) - 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.allDeviceFacts = [] - self.taskC = None - try: - self.__load_topology() - self.__load_switches() - except SdnConnectorError as sc: - raise sc - except Exception as e: - raise SdnConnectorError(message="Unable to load switches from CVP", - 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 topology == "VLAN": - self.topology = self._VLAN - elif topology == "VXLAN": - self.topology = self._VXLAN - elif topology == "VLAN-MLAG": - self.topology = self._VLAN_MLAG - elif topology == "VXLAN-MLAG": - self.topology = self._VXLAN_MLAG - - def __load_switches(self): - """ Retrieves the switches to configure in the following order - 1. from incoming configuration: - 1.1 using port mapping - using user and password from WIM - retrieving Lo0 and AS from switch - 1.2 from 'switches' parameter, - if any parameter is not present - Lo0 and AS - it will be requested to the switch - 2. Looking in the CloudVision inventory if not in configuration parameters - 2.1 using the switches with the topology_type tag set to 'leaf' - - All the search methods will be used - """ - self.switches = {} - if self.__config and self.__config.get(self.__SERVICE_ENDPOINT_MAPPING): - 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'): - # Not directly from json, complete one by one - 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} - if cs_content: - self.switches[cs].update(cs_content) - - # Load the rest of the data - if self.client == 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 len(self.switches) == 0: - self.logger.error("Unable to load Leaf switches from CVP") - return - - # self.switches are switch objects, one for each switch in self.switches, - # 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'] - 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 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 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)) - - 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): - raise Exception(SdnError.CONNECTION_POINTS_SIZE) - - if check_vlan: - 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): - 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)) - - # 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) - - # 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) - - def check_credentials(self): - """Retrieves the CloudVision version information, as the easiest way - for testing the access to CloudVision API - """ - try: - if self.client == 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, - http_code=401) from e - except Exception as ex: - self.client = None - self.logger.error(str(ex)) - raise SdnConnectorError(message=SdnError.INTERNAL_ERROR, - http_code=500) from ex - - def get_connectivity_service_status(self, service_uuid, conn_info=None): - """Monitor the status of the connectivity service established - Arguments: - service_uuid (str): UUID of the connectivity service - conn_info (dict or None): Information returned by the connector - during the service creation/edition and subsequently stored in - the database. - - Returns: - dict: JSON/YAML-serializable dict that contains a mandatory key - ``sdn_status`` associated with one of the following values:: - - {'sdn_status': 'ACTIVE'} - # The service is up and running. - - {'sdn_status': 'INACTIVE'} - # The service was created, but the connector - # cannot determine yet if connectivity exists - # (ideally, the caller needs to wait and check again). - - {'sdn_status': 'DOWN'} - # Connection was previously established, - # but an error/failure was detected. - - {'sdn_status': 'ERROR'} - # An error occurred when trying to create the service/ - # establish the connectivity. - - {'sdn_status': 'BUILD'} - # Still trying to create the service, the caller - # needs to wait and check again. - - Additionally ``error_msg``(**str**) and ``sdn_info``(**dict**) - keys can be used to provide additional status explanation or - new information available for the connectivity service. - """ - try: - 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) - - self.__get_Connection() - if conn_info == 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) - - 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']): - continue - note = cl['note'] - t_id = note.split(self.__SEPARATOR)[1] - result = self.client.api.get_task_by_id(t_id) - if result['workOrderUserDefinedStatus'] == 'Completed': - continue - elif result['workOrderUserDefinedStatus'] == 'Cancelled': - t_isCancelled = True - 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' - elif t_isFailed: - 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' - else: - 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, - 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), - http_code=500) from ex - - def create_connectivity_service(self, service_type, connection_points, - **kwargs): - """Stablish SDN/WAN connectivity between the endpoints - :param service_type: - (str): ``ELINE`` (L2), ``ELAN`` (L2), ``ETREE`` (L2), ``L3``. - :param connection_points: (list): each point corresponds to - an entry point to be connected. For WIM: from the DC - to the transport network. - For SDN: Compute/PCI to the transport network. One - connection point serves to identify the specific access and - some other service parameters, such as encapsulation type. - Each item of the list is a dict with: - "service_endpoint_id": (str)(uuid) Same meaning that for - 'service_endpoint_mapping' (see __init__) - In case the config attribute mapping_not_needed is True, - this value is not relevant. In this case - it will contain the string "device_id:device_interface_id" - "service_endpoint_encapsulation_type": None, "dot1q", ... - "service_endpoint_encapsulation_info": (dict) with: - "vlan": ..., (int, present if encapsulation is dot1q) - "vni": ... (int, present if encapsulation is vxlan), - "peers": [(ipv4_1), (ipv4_2)] (present if - encapsulation is vxlan) - "mac": ... - "device_id": ..., same meaning that for - 'service_endpoint_mapping' (see __init__) - "device_interface_id": same meaning that for - 'service_endpoint_mapping' (see __init__) - "switch_dpid": ..., present if mapping has been found - for this device_id,device_interface_id - "switch_port": ... present if mapping has been found - for this device_id,device_interface_id - "service_mapping_info": present if mapping has - been found for this device_id,device_interface_id - :param kwargs: For future versions: - bandwidth (int): value in kilobytes - latency (int): value in milliseconds - Other QoS might be passed as keyword arguments. - :return: tuple: ``(service_id, conn_info)`` containing: - - *service_uuid* (str): UUID of the established - connectivity service - - *conn_info* (dict or None): Information to be - stored at the database (or ``None``). - This information will be provided to the - :meth:`~.edit_connectivity_service` and :obj:`~.delete`. - **MUST** be JSON/YAML-serializable (plain data structures). - :raises: SdnConnectorError: In case of error. Nothing should be - created in this case. - Provide the parameter http_code - """ - try: - 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) - service_uuid = str(uuid.uuid4()) - - self.logger.info("Service with uuid {} created.". - format(service_uuid)) - s_uid, s_connInf = self.__processConnection( - service_uuid, - service_type, - connection_points, - kwargs) - try: - self.__addMetadata(s_uid, service_type, s_connInf['vlan_id']) - except Exception as e: - pass - - return (s_uid, s_connInf) - except CvpLoginError as e: - self.logger.info(str(e)) - self.client = None - raise SdnConnectorError(message=SdnError.UNAUTHORIZED, - 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 - 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): - """ - Invoked from creation and edit methods - - Process the connection points array, - creating a set of configuration per switch where it has to be applied - for creating the configuration, the switches have to be queried for obtaining: - - the loopback address - - the BGP ASN (autonomous system number) - - the interface name of the MAC address to add in the connectivity service - Once the new configuration is ready, the __updateConnection method is invoked for appling the changes - """ - try: - cls_perSw = {} - cls_cp = {} - cl_bgp = {} - for s in self.switches: - cls_perSw[s] = [] - cls_cp[s] = [] - vlan_processed = False - 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) - else: - 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}] - - # 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) - # it should be only one switch where the mac is attached - 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) - else: - 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) - 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 - else: - 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) - - 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 = '' - # 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']) - else: - 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] = '' - - 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 cls_cp.get(p): - cl_config = str(cl_vlan) - else: - cl_config = str(cl_vlan) + str(cl_bgp[s]) + str(cls_cp[s]) - - cls_perSw[s] = [{'name': cl_name, 'config': cl_config}] - - allLeafConfigured, allLeafModified = self.__updateConnection(cls_perSw) - - conn_info = { - "uuid": service_uuid, - "status": "BUILD", - "service_type": service_type, - "vlan_id": vlan_id, - "connection_points": connection_points, - "configLetPerSwitch": cls_perSw, - 'allLeafConfigured': allLeafConfigured, - 'allLeafModified': allLeafModified} - - return service_uuid, conn_info - except Exception as 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 - - checks if the new connection points config is: - - already in the Cloud Vision, the configLet is modified, and applied to the switch, - executing the corresponding task - - if it has to be removed: - then configuration has to be removed from the switch executing the corresponding task, - before trying to remove the configuration - - created, the configuration set is created, associated to the switch, and the associated - task to the configLet modification executed - In case of any error, rollback is executed, removing the created elements, and restoring to the - previous state. - """ - try: - allLeafConfigured = {} - allLeafModified = {} - - 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')): - # 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 fron 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']) - # remove configLet - cl_toDelete.append(cvp_cl) - cl[0] = cvp_cl - toDelete_in_cvp = True - except CvpApiError as error: - if "Entity does not exist" in error.msg: - continue - else: - raise error - # remove configLet from device - 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) - 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']: - if not toDelete_in_cvp: - 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} } - 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) - except Exception as e: - 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 - """ - for s in self.switches: - if allLeafModified[s]: - try: - res = self.__device_modify( - device_to_update=s, - new_configlets=cls_perSw[s], - 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} - 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)) - pass - for s in self.switches: - if allLeafConfigured[s]: - self.__configlet_modify(cls_perSw[s], delete=True) - - def __exec_task(self, tasks, tout=10): - if self.taskC == None: - self.__connect() - data = self.taskC.update_all_tasks(tasks).values() - 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, - the tasks Id's associated to the change are returned - """ - 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 == None or - len(device_to_update) == 0): - data = {'updated': updated, 'tasks': newTasks} - return [changed, data] - - self.__load_inventory() - - allDeviceFacts = self.allDeviceFacts - # Work through Devices list adding device specific information - device = None - for try_device in allDeviceFacts: - # Add Device Specific Configlets - # self.logger.debug(device) - if try_device['hostname'] not in device_to_update: - continue - dev_cvp_configlets = self.client.api.get_configlets_by_device_id( - try_device['systemMacAddress']) - # self.logger.debug(dev_cvp_configlets) - 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']}) - # self.logger.debug(device) - device = try_device - break - - # Check assigned configlets - device_update = False - add_configlets = [] - remove_configlets = [] - update_devices = [] - - if delete: - for cvp_configlet in device['deviceSpecificConfiglets']: - for cl in new_configlets: - 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']: - add_configlets.append(configlet) - device_update = True - if device_update: - 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] - # 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) - dev_action = r - 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) - dev_action = r - 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) - raise SdnConnectorError(msg) from error - else: - if "errorMessage" in str(dev_action): - m = "Device {} Configlets update fail: {}".format( - up_device['name'], dev_action['errorMessage']) - raise SdnConnectorError(m) - else: - changed = True - 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 couldnot 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} - return [changed, data] - - def __configlet_modify(self, configletsToApply, delete=False): - ''' 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)) - - # Compare configlets against cvp_facts-configlets - changed = False - checked = [] - deleted = [] - updated = [] - new = [] - - for cl in configletsToApply: - found_in_cvp = False - to_delete = False - 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'] - found_in_cvp = True - except CvpApiError as error: - if "Entity does not exist" in error.msg: - pass - else: - raise error - - if delete: - if found_in_cvp: - to_delete = True - configlet = {'name': cvp_cl['name'], - 'data': cvp_cl} - else: - if found_in_cvp: - 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']} - else: - to_check = True - 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']} - try: - if to_delete: - operation = 'delete' - resp = self.client.api.delete_configlet( - configlet['data']['name'], - configlet['data']['key']) - elif to_update: - operation = 'update' - resp = self.client.api.update_configlet( - configlet['config'], - configlet['data']['key'], - configlet['data']['name'], - wait_task_ids=True) - elif to_create: - operation = 'create' - resp = self.client.api.add_configlet( - configlet['name'], - configlet['config']) - else: - operation = 'checked' - resp = 'checked' - except Exception as error: - errorMessage = str(error).split(':')[-1] - message = "Configlet {} cannot be {}: {}".format( - cl['name'], operation, errorMessage) - if to_delete: - deleted.append({configlet['name']: message}) - elif to_update: - updated.append({configlet['name']: message}) - elif to_create: - new.append({configlet['name']: message}) - elif to_check: - checked.append({configlet['name']: message}) - - else: - if "error" in str(resp).lower(): - message = "Configlet {} cannot be deleted: {}".format( - cl['name'], resp['errorMessage']) - if to_delete: - deleted.append({configlet['name']: message}) - elif to_update: - updated.append({configlet['name']: message}) - elif to_create: - new.append({configlet['name']: message}) - elif to_check: - checked.append({configlet['name']: message}) - else: - if to_delete: - changed = True - deleted.append({configlet['name']: "success"}) - elif to_update: - changed = True - 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"}) - elif to_check: - changed = False - checked.append({configlet['name']: "success"}) - - 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']) - - 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) - self.__get_configletsDevices(srv_cls) - for s in self.switches: - cl = srv_cls[s] - if len(cl) > 0: - for dev in cl['devices']: - cls_perSw[dev].append(cl) - else: - cls_perSw = conn_info['configLetPerSwitch'] - return cls_perSw - - 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 - """ - try: - self.logger.debug('invoked delete_connectivity_service {}'. - format(service_uuid)) - if not service_uuid: - raise SdnConnectorError(message='No connection service UUID', - http_code=500) - - self.__get_Connection() - if conn_info == None: - 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) - 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) - 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) - self.__removeMetadata(service_uuid) - except CvpLoginError as e: - self.logger.info(str(e)) - self.client = None - raise SdnConnectorError(message=SdnError.UNAUTHORIZED, - 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 - - def __addMetadata(self, service_uuid, service_type, vlan_id): - """ 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 - except CvpApiError as error: - if "Entity does not exist" in error.msg: - pass - else: - raise error - try: - new_serv = '{} {} {} {}\n'.format(self.__METADATA_PREFIX, service_type, vlan_id, service_uuid) - - if found_in_cvp: - cl_config = cvp_cl['config'] + new_serv - else: - cl_config = new_serv - 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))) - pass - - def __removeMetadata(self, service_uuid): - """ 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 - except CvpApiError as error: - if "Entity does not exist" in error.msg: - 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 line: - continue - else: - cl_config = cl_config + line - 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))) - pass - - def edit_connectivity_service(self, - service_uuid, - conn_info=None, - connection_points=None, - **kwargs): - """ Change an existing connectivity service. - - This method's arguments and return value follow the same convention as - :meth:`~.create_connectivity_service`. - - :param service_uuid: UUID of the connectivity service. - :param conn_info: (dict or None): Information previously returned - by last call to create_connectivity_service - or edit_connectivity_service - :param connection_points: (list): If provided, the old list of - connection points will be replaced. - :param kwargs: Same meaning that create_connectivity_service - :return: dict or None: Information to be updated and stored at - the database. - When ``None`` is returned, no information should be changed. - When an empty dict is returned, the database record will - be deleted. - **MUST** be JSON/YAML-serializable (plain data structures). - Raises: - SdnConnectorError: In case of error. - """ - try: - 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) - if not conn_info: - raise SdnConnectorError(message='Unable to perform operation, missing or empty connection information', - http_code=500) - - if connection_points == 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) - - 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, - http_code=401) from e - except SdnConnectorError as sde: - raise sde - except Exception as ex: - try: - # Add previous - # 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))) - if self.raiseException: - raise 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 each service - """ - try: - 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) - except CvpLoginError as e: - self.logger.info(str(e)) - self.client = None - raise SdnConnectorError(message=SdnError.UNAUTHORIZED, - 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 - - def get_all_active_connectivity_services(self): - """ 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.__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'] - - 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, - 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 - - def __get_serviceConfigLets(self, service_uuid, service_type, vlan_id): - """ 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) - try: - cvp_cl = self.client.api.get_configlet_by_name(name) - found_in_cvp = True - except CvpApiError as error: - if "Entity does not exist" in error.msg: - 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 - 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 - except CvpApiError as error: - if "Entity does not exist" in error.msg: - pass - else: - raise error - s_vlan_list = [] - if found_in_cvp: - lines = cvp_cl['config'].split('\n') - for line in lines: - if self.__METADATA_PREFIX in line: - 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): - s_vlan_list.append(s_vlan) - - return s_vlan_list - - def __get_srvUUIDs(self): - """ 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 - except CvpApiError as error: - if "Entity does not exist" in error.msg: - pass - else: - raise error - serv_list = [] - if found_in_cvp: - 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]} - else: - continue - 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 - """ - try: - if self.client == None: - self.client = self.__connect() - self.client.api.get_cvp_info() - except (CvpSessionLogOutError, RequestException) as e: - self.logger.debug("Connection error '{}'. Reconnecting".format(e)) - self.client = self.__connect() - self.client.api.get_cvp_info() - - def __connect(self): - ''' 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: - port = int(port) - else: - port = 443 - - 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 - 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. - Score - 1.0 if the sequences are identical, and - 0.0 if they have nothing in common. - unified diff list - Code Meaning - '- ' line unique to sequence 1 - '+ ' line unique to sequence 2 - ' ' line common to both sequences - '? ' line not present in either input sequence - """ - fromlines = fromText.splitlines(1) - 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) - return [diffRatio, diff] - - def __load_inventory(self): - """ 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)) - data = self.client.get(url, timeout=self.__API_REQUEST_TOUT) - 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)) - - 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)) - try: - data = self.client.get(url, timeout=self.__API_REQUEST_TOUT) - return data['notifications'][0]['updates']['addrWithMask']['value'].split('/')[0] - except Exception: - raise SdnConnectorError("Invalid response from url {}: data {}".format(url, 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)) - try: - data = self.client.get(url, timeout=self.__API_REQUEST_TOUT) - return data['notifications'][0]['updates']['asNumber']['value']['value']['int'] - except Exception: - raise SdnConnectorError("Invalid response from url {}: data {}".format(url, 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)) - try: - data = self.client.get(url, timeout=self.__API_REQUEST_TOUT) - if data['notifications']: - found = False - 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: - 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)) - 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']: - peer = s - found = True - break - if found: - break - if found: - break - if peer == None: - 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)) - return peer - except Exception: - raise SdnConnectorError("Invalid response from url {}: data {}".format(url, data)) - - def is_valid_destination(self, url): - """ Check that the provided WIM URL is correct - """ - if re.match(self.__regex, url): - return True - elif self.is_valid_ipv4_address(url): - return True - else: - return self.is_valid_ipv6_address(url) - - def is_valid_ipv4_address(self, address): - """ Checks that the given IP is IPv4 valid - """ - try: - socket.inet_pton(socket.AF_INET, address) - except AttributeError: # no inet_pton here, sorry - try: - socket.inet_aton(address) - except socket.error: - return False - 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 - """ - 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 == 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