From: tierno Date: Mon, 1 Jun 2020 06:23:06 +0000 (+0000) Subject: Merge branch 'contrail' into master X-Git-Tag: release-v8.0-start~19 X-Git-Url: https://osm.etsi.org/gitweb/?p=osm%2FRO.git;a=commitdiff_plain;h=d0f7e798d3462027dc2c8d612a89600f4a6bad3e;hp=4f4ce1725a3fef7c8e2074a1f8dc4e1979217f97 Merge branch 'contrail' into master Change-Id: I49a77de6c1d9ade3a932a315f00e59d628367cbe Signed-off-by: tierno --- diff --git a/RO-SDN-arista/osm_rosdn_arista/aristaSwitch.py b/RO-SDN-arista/osm_rosdn_arista/aristaSwitch.py deleted file mode 100644 index 840f3a94..00000000 --- a/RO-SDN-arista/osm_rosdn_arista/aristaSwitch.py +++ /dev/null @@ -1,108 +0,0 @@ -# -*- coding: utf-8 -*- -## -# Copyright 2019 Atos - CoE Telco NFV Team -# All Rights Reserved. -# -# Contributors: Oscar Luis Peral, Atos -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# -# For those usages not covered by the Apache License, Version 2.0 please -# contact with: -# -# Neither the name of Atos nor the names of its -# contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# This work has been performed in the context of Arista Telefonica OSM PoC. -## - -from jsonrpclib import Server -import socket -import ssl - - -class AristaSwitch(): - """ - Used to run switch commands through eAPI and check command output - """ - - def __init__(self, name=None, host=None, user=None, passwd=None, - verify_ssl=False, unix_socket=None, - logger=None): - - self.host = host - self.user = user - self.passwd = passwd - - self.unix_socket = unix_socket - self.local_ep = Server(unix_socket) \ - if unix_socket is not None else None - - s = "https://{user}:{passwd}@{host}/command-api" - self.url = s.format(user=user, passwd=passwd, host=host) - self.ep = Server(self.url) - self.verify_ssl = verify_ssl - if not self.verify_ssl: - try: - ssl._create_default_https_context = ssl.\ - _create_unverified_context - except AttributeError: - # Old python versions do not verify certs by default - pass - - self.log = logger - - def _multilinestr_to_list(self, multilinestr=None): - """ - Returns a list, each item been one line of a (multi)line string - Handy for running multiple lines commands through one API call - """ - mylist = \ - [x.strip() for x in multilinestr.split('\n') if x.strip() != ''] - return mylist - - def run(self, cmds=None, timeout=10, local_run=False): - """ - Runs commands through eAPI - - If local_run is True eAPI call will be done using local unix socket - If local run is False eAPI call will be done using TCPIP - """ - socket.setdefaulttimeout(timeout) - - r = None - - if type(cmds) is str: - run_list = self._multilinestr_to_list(cmds) - - if type(cmds) is list: - run_list = cmds - - if local_run: - ep = self.local_ep - ep_log = "local unix socket {}".format(str(self.unix_socket)) - else: - ep = self.ep - ep_log = "tcpip socket {}".format(str(self.host)) - - self.log.debug("Calling eAPI at {} with commands {}". - format(ep_log, str(run_list))) - - try: - r = ep.runCmds(1, run_list) - except Exception as e: - self.log.error(str(e)) - raise(e) - - return r diff --git a/RO-SDN-arista/osm_rosdn_arista/wimconn_arista.py b/RO-SDN-arista/osm_rosdn_arista/wimconn_arista.py index 5c28867e..45032756 100644 --- a/RO-SDN-arista/osm_rosdn_arista/wimconn_arista.py +++ b/RO-SDN-arista/osm_rosdn_arista/wimconn_arista.py @@ -45,7 +45,6 @@ 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 @@ -130,6 +129,7 @@ class AristaSdnConnector(SdnConnectorBase): __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): @@ -183,7 +183,13 @@ class AristaSdnConnector(SdnConnectorBase): self.allDeviceFacts = [] self.clC = AristaSDNConfigLet() self.taskC = None - self.__load_switches() + 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 @@ -211,14 +217,20 @@ class AristaSdnConnector(SdnConnectorBase): 'ip': None, 'usr': self.__user, 'lo0': None, - 'AS': 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} + 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) @@ -240,94 +252,32 @@ class AristaSdnConnector(SdnConnectorBase): 'ip': device['ipAddress'], 'usr': self.__user, 'lo0': None, - 'AS': 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.s_api are switch objects, one for each switch in self.switches, + # 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: - if not self.switches[s].get('ip'): - for device in self.allDeviceFacts: - if device['hostname'] == s: + for device in self.allDeviceFacts: + if device['hostname'] == s: + if not self.switches[s].get('ip'): 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) + 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_switch_interface_ip(s, 'Loopback0') - self.switches[s]["lo0"] = inf.split('/')[0] + 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_switch_asn(s) + 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 __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) - 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 @@ -453,7 +403,7 @@ class AristaSdnConnector(SdnConnectorBase): 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 @@ -610,7 +560,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 @@ -640,18 +590,8 @@ 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) - 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 @@ -660,13 +600,6 @@ class AristaSdnConnector(SdnConnectorBase): 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) @@ -697,7 +630,7 @@ 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 + @@ -760,12 +693,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 @@ -801,7 +733,6 @@ class AristaSdnConnector(SdnConnectorBase): self.logger.info("Device {} modify result {}".format(s, res)) for t_id in res[1]['tasks']: if not toDelete_in_cvp: - tasks[t_id] = {'workOrderId': t_id} note_msg = "{}{}{}{}##".format(self.__MANAGED_BY_OSM, self.__SEPARATOR, t_id, @@ -810,15 +741,12 @@ class AristaSdnConnector(SdnConnectorBase): cls_perSw[s][0]['key'], note_msg) cls_perSw[s][0]['note'] = note_msg - else: - delete_tasks = { t_id : {'workOrderId': t_id} } - self.__exec_task(delete_tasks) + 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(tasks) > 0: - self.__exec_task(tasks, self.__EXC_TASK_EXEC_WAIT) if len(cl_toDelete) > 0: self.__configlet_modify(cl_toDelete, delete=True) @@ -839,7 +767,7 @@ class AristaSdnConnector(SdnConnectorBase): allLeafModified): """ Removes the given configLet from the devices and then remove the configLets """ - for s in self.s_api: + for s in self.switches: if allLeafModified[s]: try: res = self.__device_modify( @@ -856,7 +784,7 @@ class AristaSdnConnector(SdnConnectorBase): except Exception as e: self.logger.error('Error removing configlets from device {}: {}'.format(s, e)) pass - for s in self.s_api: + for s in self.switches: if allLeafConfigured[s]: self.__configlet_modify(cls_perSw[s], delete=True) @@ -1104,7 +1032,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: @@ -1116,14 +1044,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']: @@ -1160,11 +1088,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: @@ -1397,7 +1325,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 + @@ -1552,6 +1480,25 @@ class AristaSdnConnector(SdnConnectorBase): 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 """ @@ -1587,6 +1534,8 @@ class AristaSdnConnector(SdnConnectorBase): 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): diff --git a/RO-SDN-arista/requirements.txt b/RO-SDN-arista/requirements.txt index e0aac53f..cd1edfe0 100644 --- a/RO-SDN-arista/requirements.txt +++ b/RO-SDN-arista/requirements.txt @@ -14,7 +14,6 @@ ## requests -jsonrpclib-pelix uuid cvprac git+https://osm.etsi.org/gerrit/osm/RO.git#egg=osm-ro diff --git a/RO-SDN-arista/setup.py b/RO-SDN-arista/setup.py index 45320781..d974aad9 100644 --- a/RO-SDN-arista/setup.py +++ b/RO-SDN-arista/setup.py @@ -46,7 +46,6 @@ setup( include_package_data=True, install_requires=["requests", "uuid", - "jsonrpclib-pelix", "cvprac", "osm-ro @ git+https://osm.etsi.org/gerrit/osm/RO.git#egg=osm-ro&subdirectory=RO"], setup_requires=['setuptools-version-command'], diff --git a/RO-SDN-arista/stdeb.cfg b/RO-SDN-arista/stdeb.cfg index d5d358aa..0c718e4f 100644 --- a/RO-SDN-arista/stdeb.cfg +++ b/RO-SDN-arista/stdeb.cfg @@ -15,5 +15,5 @@ [DEFAULT] X-Python3-Version : >= 3.5 -Depends3: python3-requests, python3-osm-ro, python3-jsonrpclib-pelix +Depends3: python3-requests, python3-osm-ro diff --git a/RO/osm_ro/nfvo.py b/RO/osm_ro/nfvo.py index f06bce41..2efd6104 100644 --- a/RO/osm_ro/nfvo.py +++ b/RO/osm_ro/nfvo.py @@ -3311,31 +3311,6 @@ def create_instance(mydb, tenant_id, instance_dict): involved_datacenters.append(default_datacenter_id) target_wim_account = sce_net.get("wim_account", default_wim_account) - # --> WIM - # TODO: use this information during network creation - wim_account_id = wim_account_name = None - if len(involved_datacenters) > 1 and 'uuid' in sce_net: - urls = [myvims[v].url for v in involved_datacenters] - if len(set(urls)) < 2: - wim_usage[sce_net['uuid']] = False - elif target_wim_account is None or target_wim_account is True: # automatic selection of WIM - # OBS: sce_net without uuid are used internally to VNFs - # and the assumption is that VNFs will not be split among - # different datacenters - wim_account = wim_engine.find_suitable_wim_account( - involved_datacenters, tenant_id) - wim_account_id = wim_account['uuid'] - wim_account_name = wim_account['name'] - wim_usage[sce_net['uuid']] = wim_account_id - elif isinstance(target_wim_account, str): # manual selection of WIM - wim_account.persist.get_wim_account_by(target_wim_account, tenant_id) - wim_account_id = wim_account['uuid'] - wim_account_name = wim_account['name'] - wim_usage[sce_net['uuid']] = wim_account_id - else: # not WIM usage - wim_usage[sce_net['uuid']] = False - # <-- WIM - descriptor_net = {} if instance_dict.get("networks"): if sce_net.get("uuid") in instance_dict["networks"]: @@ -3368,14 +3343,39 @@ def create_instance(mydb, tenant_id, instance_dict): ) if not target_instance_nets: raise NfvoException( - "Cannot find the target network at instance:networks[{}]:use-network".format(descriptor_net_name), - httperrors.Bad_Request) + "Cannot find the target network at instance:networks[{}]:use-network".format( + descriptor_net_name), httperrors.Bad_Request) else: use_network = target_instance_nets[0]["related"] if sce_net["external"]: number_mgmt_networks += 1 + # --> WIM + # TODO: use this information during network creation + wim_account_id = wim_account_name = None + if len(involved_datacenters) > 1 and 'uuid' in sce_net: + urls = [myvims[v].url for v in involved_datacenters] + if len(set(urls)) < 2: + wim_usage[sce_net['uuid']] = False + elif target_wim_account is None or target_wim_account is True: # automatic selection of WIM + # OBS: sce_net without uuid are used internally to VNFs + # and the assumption is that VNFs will not be split among + # different datacenters + wim_account = wim_engine.find_suitable_wim_account( + involved_datacenters, tenant_id) + wim_account_id = wim_account['uuid'] + wim_account_name = wim_account['name'] + wim_usage[sce_net['uuid']] = wim_account_id + elif isinstance(target_wim_account, str): # manual selection of WIM + wim_account.persist.get_wim_account_by(target_wim_account, tenant_id) + wim_account_id = wim_account['uuid'] + wim_account_name = wim_account['name'] + wim_usage[sce_net['uuid']] = wim_account_id + else: # not WIM usage + wim_usage[sce_net['uuid']] = False + # <-- WIM + for datacenter_id in involved_datacenters: netmap_use = None netmap_create = None @@ -5265,12 +5265,13 @@ def create_vim_account(mydb, nfvo_tenant, datacenter_id, name=None, vim_id=None, else: #if vim_tenant==None: #create tenant at VIM if not provided try: - _, myvim = get_datacenter_by_name_uuid(mydb, None, datacenter, vim_user=vim_username, - vim_passwd=vim_password) + _, myvim = get_datacenter_by_name_uuid(mydb, None, datacenter_id, vim_user=vim_username, + vim_passwd=vim_password) datacenter_name = myvim["name"] vim_tenant = myvim.new_tenant(vim_tenant_name, "created by openmano for datacenter "+datacenter_name) except vimconn.vimconnException as e: - raise NfvoException("Not possible to create vim_tenant {} at VIM: {}".format(vim_tenant_id, str(e)), httperrors.Internal_Server_Error) + raise NfvoException("Not possible to create vim_tenant {} at VIM: {}".format(vim_tenant_name, e), + httperrors.Internal_Server_Error) datacenter_tenants_dict = {} datacenter_tenants_dict["created"]="true" diff --git a/RO/osm_ro/vim_thread.py b/RO/osm_ro/vim_thread.py index 622cccb5..380bd6a6 100644 --- a/RO/osm_ro/vim_thread.py +++ b/RO/osm_ro/vim_thread.py @@ -126,7 +126,7 @@ class vim_thread(threading.Thread): self.error_status = None self.wim_account_id = wim_account_id self.datacenter_tenant_id = datacenter_tenant_id - self.port_mapping = None + self.port_mappings = None if self.wim_account_id: self.target_k = "wim_account_id" self.target_v = self.wim_account_id diff --git a/RO/osm_ro/wim/persistence.py b/RO/osm_ro/wim/persistence.py index f4945bb9..27a87c45 100644 --- a/RO/osm_ro/wim/persistence.py +++ b/RO/osm_ro/wim/persistence.py @@ -870,10 +870,7 @@ def _preprocess_wim_account(wim_account): """ wim_account = preprocess_record(wim_account) - created = wim_account.get('created') - wim_account['created'] = ( - 'true' if created is True or created == 'true' else 'false') - + wim_account['sdn'] = False return wim_account