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
__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"
__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'
+
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)
+ self.__load_switches()
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}
+
+ 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}
+ 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']
+ # 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}
+ self.switches[device['hostname']] = switch_data
+ if len(self.switches) == 0:
+ self.logger.error("Unable to load Leaf switches from CVP")
+ return
+
# self.s_api 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"],
+ if not self.switches[s].get('ip'):
+ for device in self.allDeviceFacts:
+ if device['hostname'] == s:
+ self.switches[s]['ip'] = device['ipAddress']
+ if self.is_valid_destination(self.switches[s].get('ip')):
+ self.s_api[s] = AristaSwitch(host=self.switches[s]['ip'],
+ user=self.switches[s]['usr'],
+ passwd=self.switches[s]['passwd'],
logger=self.logger)
+ # Each switch has a different loopback address,
+ # so it's a different configLet
+ if not self.switches[s].get('lo0'):
+ inf = self.__get_switch_interface_ip(s, 'Loopback0')
+ self.switches[s]["lo0"] = inf.split('/')[0]
+ if not self.switches[s].get('AS'):
+ self.switches[s]["AS"] = self.__get_switch_asn(s)
+ self.logger.debug("Using Arista Leaf switches: {}".format(
+ self.delete_keys_from_dict(self.switches, ('passwd',))))
def __lldp_find_neighbor(self, tlv_name=None, tlv_value=None):
"""Returns a list of dicts where a mathing LLDP neighbor has been found
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)
for s in self.s_api:
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]
'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
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 switch_id:
point_mac = encap_info.get(self.__MAC_PARAM)
switches = self.__lldp_find_neighbor("chassisId", point_mac)
-
- 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 len(switches) == 0:
+ raise SdnConnectorError(message="Connection point MAC address {} not found in the switches".format(point_mac),
+ 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]
+ if not switches:
+ 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)
- for switch in switches:
# it should be only one switch where the mac is attached
if encap_type == 'dot1q':
# SRIOV configLet for Leaf switch mac's attached to
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 = ''
for s in self.s_api:
allLeafConfigured[s] = False
allLeafModified[s] = False
- tasks = dict()
cl_toDelete = []
for s in self.s_api:
toDelete_in_cvp = False
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,
allLeafModified):
""" Removes the given configLet from the devices and then remove the configLets
"""
- tasks = dict()
for s in self.s_api:
if allLeafModified[s]:
try:
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:
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
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']:
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(
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()
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
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 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):
+ 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