# -*- 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: <oscarluis.peral@atos.net>
#
# 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_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

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_cloudvision.aristaConfigLet import AristaSDNConfigLet
from osm_rosdn_arista_cloudvision.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 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 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 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,
                                    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 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)

            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:
                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 is 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 is 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 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}
        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 is 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 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)

            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 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))
            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 is 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 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
