added support for different topologies
[osm/RO.git] / RO-SDN-arista / osm_rosdn_arista / wimconn_arista.py
index 0b49cb0..571add6 100644 (file)
@@ -41,9 +41,10 @@ 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.aristaSwitch import AristaSwitch
 from osm_rosdn_arista.aristaConfigLet import AristaSDNConfigLet
 from osm_rosdn_arista.aristaTask import AristaCVPTask
 
@@ -103,6 +104,7 @@ class AristaSdnConnector(SdnConnectorBase):
     __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"
@@ -118,11 +120,21 @@ class AristaSdnConnector(SdnConnectorBase):
     __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 = 1
-    __ROLLB_TASK_EXEC_WAIT = 5
+    __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):
         """
@@ -168,116 +180,123 @@ class AristaSdnConnector(SdnConnectorBase):
         self.__passwd = wim_account.get("password")
         self.client = None
         self.cvp_inventory = None
-        self.logger.debug("Arista SDN assist {}, user:{} and conf:{}".
-                          format(wim, self.__user, config))
+        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.__load_switches()
-        self.clC = AristaSDNConfigLet()
         self.taskC = None
-        self.sw_loopback0 = {}
-        self.bgp = {}
-
-        for s in self.s_api:
-            # Each switch has a different loopback address,
-            # so it's a different configLet
-            inf = self.__get_switch_interface_ip(s, 'Loopback0')
-            self.sw_loopback0[s] = inf.split('/')[0]
-            self.bgp[s] = self.__get_switch_asn(s)
+        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 incomming configuration
-        2.- Looking in the CloudVision inventory for those switches whose hostname starts with 'leaf'
+        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
         """
-        if not self.__config or not self.__config.get('switches'):
-            if self.client is None:
-                self.client = self.__connect()
-            self.__load_inventory()
-            self.switches = {}
+        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:
-                if device['hostname'].startswith('Leaf'):
-                    switch_data = {"passwd": self.__passwd,
-                                   "ip": device['ipAddress'],
-                                   "usr": self.__user}
-                    self.switches[device['hostname']] = switch_data
-            if len(self.switches) == 0:
-                self.logger.error("Unable to load Leaf switches from CVP")
-                return
-        else:
-            # directly json
-            self.switches = self.__config.get['switches']
-        # self.s_api are switch objects, one for each switch in self.switches,
+                # 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
-        self.s_api = {}
         for s in self.switches:
-            self.logger.debug("Using Arista Leaf switch: {} {} {}".format(
-                                s,
-                                self.switches[s]["ip"],
-                                self.switches[s]["usr"]))
-            if self.is_valid_destination(self.switches[s]["ip"]):
-                self.s_api[s] = AristaSwitch(host=self.switches[s]["ip"],
-                                             user=self.switches[s]["usr"],
-                                             passwd=self.switches[s]["passwd"],
-                                             logger=self.logger)
-
-    def __lldp_find_neighbor(self, tlv_name=None, tlv_value=None):
-        """Returns a list of dicts where a mathing LLDP neighbor has been found
-           Each dict has:
-             switch -> switch name
-             interface -> switch interface
-        """
-        r = []
-        lldp_info = {}
-
-        # Get LLDP info from each switch
-        for s in self.s_api:
-            result = self.s_api[s].run("show lldp neighbors detail")
-            lldp_info[s] = result[0]["lldpNeighbors"]
-            # Look LLDP match on each interface
-            # Note that eAPI returns [] for an interface with no LLDP neighbors
-            # in the corresponding interface lldpNeighborInfo field
-            for interface in lldp_info[s]:
-                if lldp_info[s][interface]["lldpNeighborInfo"]:
-                    lldp_nInf = lldp_info[s][interface]["lldpNeighborInfo"][0]
-                    if tlv_name in lldp_nInf:
-                        if lldp_nInf[tlv_name] == tlv_value:
-                            r.append({"name": s, "interface": interface})
-
-        return r
-
-    def __get_switch_asn(self, switch):
-        """Returns switch ASN in default VRF
-        """
-        bgp_info = self.s_api[switch].run("show ip bgp summary")[0]
-        return(bgp_info["vrfs"]["default"]["asn"])
-
-    def __get_switch_po(self, switch, interface=None):
-        """Returns Port-Channels for a given interface
-           If interface is None returns a list with all PO interfaces
-           Note that if specified, interface should be exact name
-           for instance: Ethernet3 and not e3 eth3 and so on
-        """
-        po_inf = self.s_api[switch].run("show port-channel")[0]["portChannels"]
-
-        if interface:
-            r = [x for x in po_inf if interface in po_inf[x]["activePorts"]]
-        else:
-            r = po_inf
-
-        return r
-
-    def __get_switch_interface_ip(self, switch, interface=None):
-        """Returns interface primary ip
-           interface should be exact name
-           for instance: Ethernet3 and not ethernet 3, e3 eth3 and so on
-        """
-        cmd = "show ip interface {}".format(interface)
-        ip_info = self.s_api[switch].run(cmd)[0]["interfaces"][interface]
-
-        ip = ip_info["interfaceAddress"]["primaryIp"]["address"]
-        mask = ip_info["interfaceAddress"]["primaryIp"]["maskLen"]
+            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
 
-        return "{}/{}".format(ip, mask)
+            # 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):
@@ -289,10 +308,10 @@ class AristaSdnConnector(SdnConnectorBase):
                             self.__supported_service_types))
 
         if check_num_cp:
-            if (len(connection_points) < 2):
+            if len(connection_points) < 2:
                 raise Exception(SdnError.CONNECTION_POINTS_SIZE)
-            if ((len(connection_points) != self.__ELINE_num_connection_points) and
-               (service_type == self.__service_types_ELINE)):
+            if (len(connection_points) != self.__ELINE_num_connection_points and
+               service_type == self.__service_types_ELINE):
                 raise Exception(SdnError.CONNECTION_POINTS_SIZE)
 
         if check_vlan:
@@ -311,8 +330,8 @@ class AristaSdnConnector(SdnConnectorBase):
                         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))
+            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)
@@ -329,7 +348,7 @@ class AristaSdnConnector(SdnConnectorBase):
         for testing the access to CloudVision API
         """
         try:
-            if self.client is None:
+            if self.client == None:
                 self.client = self.__connect()
             result = self.client.api.get_cvp_info()
             self.logger.debug(result)
@@ -381,12 +400,13 @@ class AristaSdnConnector(SdnConnectorBase):
                 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:
+            if conn_info == None:
                 raise SdnConnectorError(message='No connection information for service UUID {}'.format(service_uuid),
                                         http_code=500)
 
@@ -403,10 +423,15 @@ class AristaSdnConnector(SdnConnectorBase):
             t_isFailed = False
             t_isPending = False
             failed_switches = []
-            for s in self.s_api:
-                if (len(cls_perSw[s]) > 0):
+            for s in self.switches:
+                if len(cls_perSw[s]) > 0:
                     for cl in cls_perSw[s]:
-                        if len(cls_perSw[s][0]['config']) == 0:
+                        # 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]
@@ -477,7 +502,7 @@ class AristaSdnConnector(SdnConnectorBase):
                         'service_endpoint_mapping' (see __init__)
                     "switch_dpid": ..., present if mapping has been found
                         for this device_id,device_interface_id
-                    "swith_port": ... present if mapping has been found
+                    "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
@@ -527,6 +552,11 @@ class AristaSdnConnector(SdnConnectorBase):
                                     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)
@@ -555,7 +585,7 @@ class AristaSdnConnector(SdnConnectorBase):
             cls_perSw = {}
             cls_cp = {}
             cl_bgp = {}
-            for s in self.s_api:
+            for s in self.switches:
                 cls_perSw[s] = []
                 cls_cp[s] = []
             vlan_processed = False
@@ -585,28 +615,9 @@ class AristaSdnConnector(SdnConnectorBase):
 
                 encap_type = cp.get(self.__ENCAPSULATION_TYPE_PARAM)
                 switch_id = encap_info.get(self.__SW_ID_PARAM)
-                if not switch_id:
-                    point_mac = encap_info.get(self.__MAC_PARAM)
-                    switches = self.__lldp_find_neighbor("chassisId", point_mac)
+                interface = encap_info.get(self.__SW_PORT_PARAM)
+                switches = [{'name': switch_id, 'interface': interface}]
 
-                    if len(switches) == 0:
-                        raise SdnConnectorError(message="Connection point MAC address {} not found in the switches".format(point_mac),
-                                                http_code=406)
-                    self.logger.debug("Found connection point for MAC {}: {}".
-                                      format(point_mac, switches))
-                    port_channel = self.__get_switch_po(switch['name'],
-                                                        switch['interface'])
-                    if len(port_channel) > 0:
-                        interface = port_channel[0]
-                    else:
-                        interface = switch['interface']
-                else:
-                    interface = encap_info.get(self.__SW_PORT_PARAM)
-                    switches = [{'name': switch_id, 'interface': interface}]
-
-                    if not interface:
-                        raise SdnConnectorError(message="Connection point switch port empty for switch_dpid {}".format(switch_id),
-                                                http_code=406)
                 # 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]
@@ -614,6 +625,9 @@ class AristaSdnConnector(SdnConnectorBase):
                     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
@@ -641,28 +655,38 @@ class AristaSdnConnector(SdnConnectorBase):
                 raise SdnConnectorError(message=SdnError.UNSUPPORTED_FEATURE,
                                         http_code=406)
 
-            for s in self.s_api:
+            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)
-                # apply VLAN and BGP configLet to all Leaf switches
-                if service_type == self.__service_types_ELAN:
-                    cl_bgp[s] = self.clC.getElan_bgp(service_uuid,
-                                                     vlan_id,
-                                                     vni_id,
-                                                     self.sw_loopback0[s],
-                                                     self.bgp[s])
+                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] = self.clC.getEline_bgp(service_uuid,
-                                                      vlan_id,
-                                                      vni_id,
-                                                      self.sw_loopback0[s],
-                                                      self.bgp[s])
+                    cl_bgp[s] = ''
 
                 if not cls_cp.get(s):
-                    cl_config = ''
+                    # 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])
 
@@ -704,12 +728,11 @@ class AristaSdnConnector(SdnConnectorBase):
             allLeafConfigured = {}
             allLeafModified = {}
 
-            for s in self.s_api:
+            for s in self.switches:
                 allLeafConfigured[s] = False
                 allLeafModified[s] = False
-            tasks = dict()
             cl_toDelete = []
-            for s in self.s_api:
+            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
@@ -744,36 +767,33 @@ class AristaSdnConnector(SdnConnectorBase):
                     raise Exception(str(res))
                 self.logger.info("Device {} modify result {}".format(s, res))
                 for t_id in res[1]['tasks']:
-                    tasks[t_id] = {'workOrderId': t_id}
                     if not toDelete_in_cvp:
-                        note_msg = "## Managed by OSM {}{}{}##".format(self.__SEPARATOR,
-                                                                       t_id,
-                                                                       self.__SEPARATOR)
+                        note_msg = "{}{}{}{}##".format(self.__MANAGED_BY_OSM,
+                                                       self.__SEPARATOR,
+                                                       t_id,
+                                                       self.__SEPARATOR)
                         self.client.api.add_note_to_configlet(
                                 cls_perSw[s][0]['key'],
                                 note_msg)
-                        cls_perSw[s][0].update([('note', 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 self.taskC is None:
-                self.__connect()
-            data = self.taskC.update_all_tasks(tasks).values()
-            self.taskC.task_action(data,
-                                   self.__EXC_TASK_EXEC_WAIT,
-                                   'executed')
             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=True,
-                                          allLeafModified=True)
+                                          allLeafConfigured,
+                                          allLeafModified)
             except Exception as e:
-                self.logger.info("Exception rolling back in updating  connection: {}".
-                                 format(e))
+                self.logger.error("Exception rolling back in updating  connection: {}".
+                                 format(e), exc_info=True)
             raise ex
 
     def __rollbackConnection(self,
@@ -782,8 +802,7 @@ class AristaSdnConnector(SdnConnectorBase):
                              allLeafModified):
         """ Removes the given configLet from the devices and then remove the configLets
         """
-        tasks = dict()
-        for s in self.s_api:
+        for s in self.switches:
             if allLeafModified[s]:
                 try:
                     res = self.__device_modify(
@@ -792,22 +811,24 @@ class AristaSdnConnector(SdnConnectorBase):
                         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.info('Error removing configlets from device {}: {}'.format(s, e))
+                    self.logger.error('Error removing configlets from device {}: {}'.format(s, e))
                     pass
-        if self.taskC is None:
-            self.__connect()
-        data = self.taskC.update_all_tasks(tasks).values()
-        self.taskC.task_action(data,
-                               self.__ROLLB_TASK_EXEC_WAIT,
-                               'executed')
-        for s in self.s_api:
+        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
@@ -820,7 +841,7 @@ class AristaSdnConnector(SdnConnectorBase):
         newTasks = []
 
         if (len(new_configlets) == 0 or
-                device_to_update is None or
+                device_to_update == None or
                 len(device_to_update) == 0):
             data = {'updated': updated, 'tasks': newTasks}
             return [changed, data]
@@ -907,10 +928,14 @@ class AristaSdnConnector(SdnConnectorBase):
             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)})
+                            "Configlets-{}".format(
+                                taskId)})
                         newTasks.append(taskId)
                 else:
                     updated.append({up_device['hostname']:
@@ -988,7 +1013,8 @@ class AristaSdnConnector(SdnConnectorBase):
                     resp = self.client.api.update_configlet(
                                     configlet['config'],
                                     configlet['data']['key'],
-                                    configlet['data']['name'])
+                                    configlet['data']['name'],
+                                    wait_task_ids=True)
                 elif to_create:
                     operation = 'create'
                     resp = self.client.api.add_configlet(
@@ -1041,7 +1067,7 @@ class AristaSdnConnector(SdnConnectorBase):
         return [changed, data]
 
     def __get_configletsDevices(self, configlets):
-        for s in self.s_api:
+        for s in self.switches:
             configlet = configlets[s]
             # Add applied Devices
             if len(configlet) > 0:
@@ -1053,14 +1079,14 @@ class AristaSdnConnector(SdnConnectorBase):
 
     def __get_serviceData(self, service_uuid, service_type, vlan_id, conn_info=None):
         cls_perSw = {}
-        for s in self.s_api:
+        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.s_api:
+            for s in self.switches:
                 cl = srv_cls[s]
                 if len(cl) > 0:
                     for dev in cl['devices']:
@@ -1087,7 +1113,7 @@ class AristaSdnConnector(SdnConnectorBase):
                                         http_code=500)
 
             self.__get_Connection()
-            if conn_info is None:
+            if conn_info == None:
                 raise SdnConnectorError(message='No connection information for service UUID {}'.format(service_uuid),
                                         http_code=500)
             c_info = None
@@ -1097,11 +1123,11 @@ class AristaSdnConnector(SdnConnectorBase):
                                                c_info)
             allLeafConfigured = {}
             allLeafModified = {}
-            for s in self.s_api:
+            for s in self.switches:
                 allLeafConfigured[s] = True
                 allLeafModified[s] = True
             found_in_cvp = False
-            for s in self.s_api:
+            for s in self.switches:
                 if cls_perSw[s]:
                     found_in_cvp = True
             if found_in_cvp:
@@ -1220,7 +1246,7 @@ class AristaSdnConnector(SdnConnectorBase):
                 raise SdnConnectorError(message='Unable to perform operation, missing or empty connection information',
                                         http_code=500)
 
-            if connection_points is None:
+            if connection_points == None:
                 return None
 
             self.__get_Connection()
@@ -1334,7 +1360,7 @@ class AristaSdnConnector(SdnConnectorBase):
         connectivity service
         """
         srv_cls = {}
-        for s in self.s_api:
+        for s in self.switches:
             srv_cls[s] = []
             found_in_cvp = False
             name = (self.__OSM_PREFIX +
@@ -1416,11 +1442,11 @@ class AristaSdnConnector(SdnConnectorBase):
             invoking the version retrival as test
         """
         try:
-            if self.client is None:
+            if self.client == None:
                 self.client = self.__connect()
             self.client.api.get_cvp_info()
         except (CvpSessionLogOutError, RequestException) as e:
-            self.logger.debug("Connection error '{}'. Reconnencting".format(e))
+            self.logger.debug("Connection error '{}'. Reconnecting".format(e))
             self.client = self.__connect()
             self.client.api.get_cvp_info()
 
@@ -1438,12 +1464,13 @@ class AristaSdnConnector(SdnConnectorBase):
         else:
             port = 443
 
-        client.connect([host],        
+        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
 
@@ -1477,6 +1504,79 @@ class AristaSdnConnector(SdnConnectorBase):
         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
         """
@@ -1510,3 +1610,12 @@ class AristaSdnConnector(SdnConnectorBase):
         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