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
__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"
__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"
+
def __init__(self, wim, wim_account, config=None, logger=None):
"""
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_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
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
+ usr and pass - from WIM configuration
+ 2. Looking in the CloudVision inventory if not in configuration parameters
+ 2.1 using the switches with the topology_type tag set to 'leaf'
+ 2.2 using the switches whose parent container is 'leaf'
+ 2.3 using the switches whose hostname contains with '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}
+
+ 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}
+ 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:
- 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 container parent is 'leaf',
+ # or the topology_tag is 'leaf'
+ # or the hostname contains 'leaf'
+ if ((device['serialNumber'] in self.cvp_tags) or
+ (self.__SWITCH_TAG_VALUE in device['containerName'].lower()) or
+ (self.__SWITCH_TAG_VALUE in device['hostname'].lower())):
+ if not self.switches.get(device['hostname']):
+ switch_data = {'passwd': self.__passwd,
+ 'ip': device['ipAddress'],
+ 'usr': self.__user,
+ 'lo0': None,
+ 'AS': None,
+ 'serialNumber': 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"]
-
- return "{}/{}".format(ip, mask)
+ 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'):
+ self.switches[s]["lo0"] = self.__get_interface_ip(self.switches[s]['serialNumber'], self.__LOOPBACK_INTF)
+ if not self.switches[s].get('AS'):
+ self.switches[s]["AS"] = self.__get_device_ASN(self.switches[s]['serialNumber'])
+ self.logger.debug("Using Arista Leaf switches: {}".format(
+ self.delete_keys_from_dict(self.switches, ('passwd',))))
def __check_service(self, service_type, connection_points,
check_vlan=True, check_num_cp=True, kwargs=None):
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)
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)
t_isFailed = False
t_isPending = False
failed_switches = []
- for s in self.s_api:
+ 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
'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
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
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)
- self.logger.debug("Found connection point for MAC {}: {}".
- format(point_mac, switches))
- else:
- 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)
+ 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
continue
processed_connection_points += switches
for switch in switches:
- if not switch_id:
- port_channel = self.__get_switch_po(switch['name'],
- switch['interface'])
- if len(port_channel) > 0:
- interface = port_channel[0]
- else:
- interface = switch['interface']
if not interface:
raise SdnConnectorError(message="Connection point switch port empty for switch_dpid {}".format(switch_id),
http_code=406)
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 +
cl_bgp[s] = self.clC.getElan_bgp(service_uuid,
vlan_id,
vni_id,
- self.sw_loopback0[s],
- self.bgp[s])
+ 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])
+ self.switches[s]['lo0'],
+ self.switches[s]['AS'])
if not cls_cp.get(s):
cl_config = ''
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
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 = "{}{}{}{}##".format(self.__MANAGED_BY_OSM,
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:
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,
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(
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
+ 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,
- self.__ROLLB_TASK_EXEC_WAIT,
- 'executed')
- for s in self.s_api:
- if allLeafConfigured[s]:
- self.__configlet_modify(cls_perSw[s], delete=True)
+ 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,
if 'taskIds' in str(dev_action):
# Fix 1030 SDN-ARISTA Key error note when deploy a NS
if not dev_action['data']['taskIds']:
- raise Exception("No taskIds found: Device {} Configlets couldnot be updated".format(
+ 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']:
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:
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']:
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:
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 +
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
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):
+ ip = None
+ 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 is_valid_destination(self, url):
""" Check that the provided WIM URL is correct
"""
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