From: tierno Date: Fri, 20 Dec 2019 12:16:46 +0000 (+0000) Subject: change SDN types from tapi to ietfl2vpn, from arista to arista_cloudvision X-Git-Tag: release-v8.0-start~5 X-Git-Url: https://osm.etsi.org/gitweb/?p=osm%2FRO.git;a=commitdiff_plain;h=667d158c0d3ee7b4c176ad0b27ac428c81b0ddbc;ds=sidebyside change SDN types from tapi to ietfl2vpn, from arista to arista_cloudvision Change-Id: I01d61cf64521707c92f58c668f0372b5daa010c5 Signed-off-by: tierno --- diff --git a/Dockerfile-local b/Dockerfile-local index 1f9a117e..d8c8432a 100644 --- a/Dockerfile-local +++ b/Dockerfile-local @@ -57,12 +57,12 @@ RUN /root/RO/RO/osm_ro/scripts/install-osm-im.sh --develop && \ python3 -m pip install -e /root/RO/RO-VIM-azure && \ python3 -m pip install -e /root/RO/RO-VIM-fos && \ python3 -m pip install -e /root/RO/RO-SDN-dynpac && \ - python3 -m pip install -e /root/RO/RO-SDN-tapi && \ + python3 -m pip install -e /root/RO/RO-SDN-ietfl2vpn && \ python3 -m pip install -e /root/RO/RO-SDN-onos_vpls && \ python3 -m pip install -e /root/RO/RO-SDN-onos_openflow && \ python3 -m pip install -e /root/RO/RO-SDN-odl_openflow && \ python3 -m pip install -e /root/RO/RO-SDN-floodlight_openflow && \ - python3 -m pip install -e /root/RO/RO-SDN-arista && \ + python3 -m pip install -e /root/RO/RO-SDN-arista_cloudvision && \ python3 -m pip install -e /root/RO/RO-SDN-juniper_contrail && \ rm -rf /root/.cache && \ apt-get clean && \ diff --git a/RO-SDN-arista/Makefile b/RO-SDN-arista/Makefile deleted file mode 100644 index 1606bb39..00000000 --- a/RO-SDN-arista/Makefile +++ /dev/null @@ -1,25 +0,0 @@ -## -# 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. -## - -all: clean package - -clean: - rm -rf dist deb_dist osm_rosdn_arista-*.tar.gz osm_rosdn_arista.egg-info .eggs - -package: - python3 setup.py --command-packages=stdeb.command sdist_dsc - cp debian/python3-osm-rosdn-arista.postinst deb_dist/osm-rosdn-arista*/debian/ - cd deb_dist/osm-rosdn-arista*/ && dpkg-buildpackage -rfakeroot -uc -us - diff --git a/RO-SDN-arista/debian/python3-osm-rosdn-arista.postinst b/RO-SDN-arista/debian/python3-osm-rosdn-arista.postinst deleted file mode 100755 index d87fa261..00000000 --- a/RO-SDN-arista/debian/python3-osm-rosdn-arista.postinst +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash - -## -# 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: OSM_TECH@list.etsi.org -## - -echo "POST INSTALL OSM-ROSDN-ARISTA" - -#Pip packages required for openstack connector -python3 -m pip install cvprac diff --git a/RO-SDN-arista/osm_rosdn_arista/aristaConfigLet.py b/RO-SDN-arista/osm_rosdn_arista/aristaConfigLet.py deleted file mode 100644 index 8e34091e..00000000 --- a/RO-SDN-arista/osm_rosdn_arista/aristaConfigLet.py +++ /dev/null @@ -1,131 +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. -## - - -class AristaSDNConfigLet: - _VLAN = "VLAN" - _VXLAN = "VXLAN" - _VLAN_MLAG = "VLAN-MLAG" - _VXLAN_MLAG = "VXLAN-MLAG" - topology = _VXLAN_MLAG - - def __init__(self, topology=_VXLAN_MLAG): - self.topology = topology - - _basic_int =""" -interface {interface} - !! service: {uuid} - switchport - switchport mode {type} - switchport {switchport_def} -! -""" - _int_SRIOV = "trunk group {service}{vlan_id}" - _int_PASSTROUGH = "access vlan {vlan_id}" - - def _get_interface(self, uuid, interface, vlan_id, s_type, index, i_type): - if i_type == "trunk": - switchport_def = self._int_SRIOV.format(service=s_type, vlan_id=vlan_id) - else: - switchport_def = self._int_PASSTROUGH.format(vlan_id=vlan_id) - return self._basic_int.format(uuid=uuid, - interface=interface, - type=i_type, - switchport_def=switchport_def) - - def getElan_sriov(self, uuid, interface, vlan_id, index): - return self._get_interface(uuid, interface, vlan_id, "ELAN", index, "trunk") - - def getEline_sriov(self, uuid, interface, vlan_id, index): - return self._get_interface(uuid, interface, vlan_id, "ELINE", index, "trunk") - - def getElan_passthrough(self, uuid, interface, vlan_id, index): - return self._get_interface(uuid, interface, vlan_id, "ELAN", index, "dot1q-tunnel") - - def getEline_passthrough(self, uuid, interface, vlan_id, index): - return self._get_interface(uuid, interface, vlan_id, "ELINE", index, "dot1q-tunnel") - - _basic_vlan =""" -vlan {vlan} - !! service: {service} {vlan} {uuid} - name {service}{vlan} - trunk group {service}{vlan} -""" - _basic_mlag =""" trunk group MLAGPEER -""" - _basic_vxlan ="""interface VXLAN1 - VXLAN vlan {vlan} vni {vni} -""" - _basic_end ="!" - - _configLet_VLAN = _basic_vlan + _basic_end - _configLet_VXLAN = _basic_vlan + _basic_vxlan + _basic_end - _configLet_VLAN_MLAG = _basic_vlan + _basic_mlag + _basic_end - _configLet_VXLAN_MLAG = _basic_vlan + _basic_mlag + _basic_vxlan + _basic_end - - def _get_vlan(self, uuid, vlan_id, vni_id, s_type): - if self.topology == self._VLAN: - return self._configLet_VLAN.format(service=s_type, vlan=vlan_id, uuid=uuid) - if self.topology == self._VLAN_MLAG: - return self._configLet_VLAN_MLAG.format(service=s_type, vlan=vlan_id, uuid=uuid) - if self.topology == self._VXLAN: - return self._configLet_VXLAN.format(service=s_type, vlan=vlan_id, uuid=uuid, vni=vni_id) - if self.topology == self._VXLAN_MLAG: - return self._configLet_VXLAN_MLAG.format(service=s_type, vlan=vlan_id, uuid=uuid, vni=vni_id) - - def getElan_vlan(self, uuid, vlan_id, vni_id): - return self._get_vlan(uuid, vlan_id, vni_id, "ELAN") - - def getEline_vlan(self, uuid, vlan_id, vni_id): - return self._get_vlan(uuid, vlan_id, vni_id, "ELINE") - - _configLet_BGP = """ -router bgp {bgp} - vlan {vlan} - !! service: {uuid} - rd {loopback}:{vni} - route-target both {vni}:{vni} - redistribute learned -! -""" - - def _get_bgp(self, uuid, vlan_id, vni_id, loopback0, bgp, s_type): - if self.topology == self._VXLAN or self.topology == self._VXLAN_MLAG: - return self._configLet_BGP.format(uuid=uuid, - bgp=bgp, - vlan=vlan_id, - loopback=loopback0, - vni=vni_id) - - - def getElan_bgp(self, uuid, vlan_id, vni_id, loopback0, bgp): - return self._get_bgp(uuid, vlan_id, vni_id, loopback0, bgp, "ELAN") - - def getEline_bgp(self, uuid, vlan_id, vni_id, loopback0, bgp): - return self._get_bgp(uuid, vlan_id, vni_id, loopback0, bgp, "ELINE") diff --git a/RO-SDN-arista/osm_rosdn_arista/aristaTask.py b/RO-SDN-arista/osm_rosdn_arista/aristaTask.py deleted file mode 100644 index a338afd4..00000000 --- a/RO-SDN-arista/osm_rosdn_arista/aristaTask.py +++ /dev/null @@ -1,132 +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. -## -import time - - -class AristaCVPTask: - def __init__(self, cvpClientApi): - self.cvpClientApi = cvpClientApi - - def __get_id(self, task): - return task.get("workOrderId") - - def __get_state(self, task): - return task.get("workOrderUserDefinedStatus") - - def __execute_task(self, task_id): - return self.cvpClientApi.execute_task(task_id) - - def __cancel_task(self, task_id): - return self.cvpClientApi.cancel_task(task_id) - - def __apply_state(self, task, state): - t_id = self.__get_id(task) - self.cvpClientApi.add_note_to_task(t_id, "Executed by OSM") - if state == "executed": - return self.__execute_task(t_id) - elif state == "cancelled": - return self.__cancel_task(t_id) - - def __actionable(self, state): - return state in ["Pending"] - - def __terminal(self, state): - return state in ["Completed", "Cancelled"] - - def __state_is_different(self, task, target): - return self.__get_state(task) != target - - def update_all_tasks(self, data): - new_data = dict() - for task_id in data.keys(): - res = self.cvpClientApi.get_task_by_id(task_id) - new_data[task_id] = res - return new_data - - def get_pending_tasks(self): - return self.cvpClientApi.get_tasks_by_status('Pending') - - def get_pending_tasks_old(self): - taskList = [] - tasksField = {'workOrderId': 'workOrderId', - 'workOrderState': 'workOrderState', - 'currentTaskName': 'currentTaskName', - 'description': 'description', - 'workOrderUserDefinedStatus': - 'workOrderUserDefinedStatus', - 'note': 'note', - 'taskStatus': 'taskStatus', - 'workOrderDetails': 'workOrderDetails'} - tasks = self.cvpClientApi.get_tasks_by_status('Pending') - # Reduce task data to required fields - for task in tasks: - taskFacts = {} - for field in task.keys(): - if field in tasksField: - taskFacts[tasksField[field]] = task[field] - taskList.append(taskFacts) - return taskList - - def task_action(self, tasks, wait, state): - changed = False - data = dict() - warnings = list() - - at = [t for t in tasks if self.__actionable(self.__get_state(t))] - actionable_tasks = at - - if len(actionable_tasks) == 0: - warnings.append("No actionable tasks found on CVP") - return changed, data, warnings - - for task in actionable_tasks: - if self.__state_is_different(task, state): - self.__apply_state(task, state) - changed = True - data[self.__get_id(task)] = task - - if wait == 0: - return changed, data, warnings - - start = time.time() - now = time.time() - while (now - start) < wait: - data = self.update_all_tasks(data) - if all([self.__terminal(self.__get_state(t)) for t in data.values()]): - break - time.sleep(1) - now = time.time() - - if wait: - for i, task in data.items(): - if not self.__terminal(self.__get_state(task)): - warnings.append("Task {} has not completed in {} seconds". - format(i, wait)) - - return changed, data, warnings diff --git a/RO-SDN-arista/osm_rosdn_arista/wimconn_arista.py b/RO-SDN-arista/osm_rosdn_arista/wimconn_arista.py deleted file mode 100644 index 571add68..00000000 --- a/RO-SDN-arista/osm_rosdn_arista/wimconn_arista.py +++ /dev/null @@ -1,1621 +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 osm_ro.wim.sdnconn import SdnConnectorBase, SdnConnectorError -import re -import socket -# Required by compare function -import difflib -# Library that uses Levenshtein Distance to calculate the differences -# between strings. -# from fuzzywuzzy import fuzz - -import logging -import uuid -from enum import Enum -from requests import RequestException - -from cvprac.cvp_client import CvpClient -from cvprac.cvp_api import CvpApi -from cvprac.cvp_client_errors import CvpLoginError, CvpSessionLogOutError, CvpApiError -from cvprac import __version__ as cvprac_version - -from osm_rosdn_arista.aristaConfigLet import AristaSDNConfigLet -from osm_rosdn_arista.aristaTask import AristaCVPTask - - -class SdnError(Enum): - UNREACHABLE = 'Unable to reach the WIM.', - VLAN_INCONSISTENT = \ - 'VLAN value inconsistent between the connection points', - VLAN_NOT_PROVIDED = 'VLAN value not provided', - CONNECTION_POINTS_SIZE = \ - 'Unexpected number of connection points: 2 expected.', - ENCAPSULATION_TYPE = \ - 'Unexpected service_endpoint_encapsulation_type. \ - Only "dotq1" is accepted.', - BANDWIDTH = 'Unable to get the bandwidth.', - STATUS = 'Unable to get the status for the service.', - DELETE = 'Unable to delete service.', - CLEAR_ALL = 'Unable to clear all the services', - UNKNOWN_ACTION = 'Unknown action invoked.', - BACKUP = 'Unable to get the backup parameter.', - UNSUPPORTED_FEATURE = "Unsupported feature", - UNAUTHORIZED = "Failed while authenticating", - INTERNAL_ERROR = "Internal error" - - -class AristaSdnConnector(SdnConnectorBase): - """Arista class for the SDN connectors - - Arguments: - wim (dict): WIM record, as stored in the database - wim_account (dict): WIM account record, as stored in the database - config - The arguments of the constructor are converted to object attributes. - An extra property, ``service_endpoint_mapping`` is created from ``config``. - - The access to Arista CloudVision is made through the API defined in - https://github.com/aristanetworks/cvprac - The a connectivity service consist in creating a VLAN and associate the interfaces - of the connection points MAC addresses to this VLAN in all the switches of the topology, - the BDP is also configured for this VLAN. - - The Arista Cloud Vision API workflow is the following - -- The switch configuration is defined as a set of switch configuration commands, - what is called 'ConfigLet' - -- The ConfigLet is associated to the device (leaf switch) - -- Automatically a task is associated to this activity for change control, the task - in this stage is in 'Pending' state - -- The task will be executed so that the configuration is applied to the switch. - -- The service information is saved in the response of the creation call - -- All created services identification is stored in a generic ConfigLet 'OSM_metadata' - to keep track of the managed resources by OSM in the Arista deployment. - """ - __supported_service_types = ["ELINE (L2)", "ELINE", "ELAN"] - __service_types_ELAN = "ELAN" - __service_types_ELINE = "ELINE" - __ELINE_num_connection_points = 2 - __supported_service_types = ["ELINE", "ELAN"] - __supported_encapsulation_types = ["dot1q"] - __WIM_LOGGER = 'openmano.sdnconn.arista' - __SERVICE_ENDPOINT_MAPPING = 'service_endpoint_mapping' - __ENCAPSULATION_TYPE_PARAM = "service_endpoint_encapsulation_type" - __ENCAPSULATION_INFO_PARAM = "service_endpoint_encapsulation_info" - __BACKUP_PARAM = "backup" - __BANDWIDTH_PARAM = "bandwidth" - __SERVICE_ENDPOINT_PARAM = "service_endpoint_id" - __MAC_PARAM = "mac" - __WAN_SERVICE_ENDPOINT_PARAM = "service_endpoint_id" - __WAN_MAPPING_INFO_PARAM = "service_mapping_info" - __DEVICE_ID_PARAM = "device_id" - __DEVICE_INTERFACE_ID_PARAM = "device_interface_id" - __SW_ID_PARAM = "switch_dpid" - __SW_PORT_PARAM = "switch_port" - __VLAN_PARAM = "vlan" - __VNI_PARAM = "vni" - __SEPARATOR = '_' - __MANAGED_BY_OSM = '## Managed by OSM ' - __OSM_PREFIX = "osm_" - __OSM_METADATA = "OSM_metadata" - __METADATA_PREFIX = '!## Service' - __EXC_TASK_EXEC_WAIT = 10 - __ROLLB_TASK_EXEC_WAIT = 10 - __API_REQUEST_TOUT = 60 - __SWITCH_TAG_NAME = 'topology_type' - __SWITCH_TAG_VALUE = 'leaf' - __LOOPBACK_INTF = "Loopback0" - _VLAN = "VLAN" - _VXLAN = "VXLAN" - _VLAN_MLAG = "VLAN-MLAG" - _VXLAN_MLAG = "VXLAN-MLAG" - - - def __init__(self, wim, wim_account, config=None, logger=None): - """ - - :param wim: (dict). Contains among others 'wim_url' - :param wim_account: (dict). Contains among others 'uuid' (internal id), 'name', - 'sdn' (True if is intended for SDN-assist or False if intended for WIM), 'user', 'password'. - :param config: (dict or None): Particular information of plugin. These keys if present have a common meaning: - 'mapping_not_needed': (bool) False by default or if missing, indicates that mapping is not needed. - 'service_endpoint_mapping': (list) provides the internal endpoint mapping. The meaning is: - KEY meaning for WIM meaning for SDN assist - -------- -------- -------- - device_id pop_switch_dpid compute_id - device_interface_id pop_switch_port compute_pci_address - service_endpoint_id wan_service_endpoint_id SDN_service_endpoint_id - service_mapping_info wan_service_mapping_info SDN_service_mapping_info - contains extra information if needed. Text in Yaml format - switch_dpid wan_switch_dpid SDN_switch_dpid - switch_port wan_switch_port SDN_switch_port - datacenter_id vim_account vim_account - id: (internal, do not use) - wim_id: (internal, do not use) - :param logger (logging.Logger): optional logger object. If none is passed 'openmano.sdn.sdnconn' is used. - """ - self.__regex = re.compile( - r'^(?:http|ftp)s?://' # http:// or https:// - r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # domain... - r'localhost|' # localhost... - r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip - r'(?::\d+)?', re.IGNORECASE) # optional port - self.raiseException = True - self.logger = logger or logging.getLogger(self.__WIM_LOGGER) - super().__init__(wim, wim_account, config, self.logger) - self.__wim = wim - self.__wim_account = wim_account - self.__config = config - if self.is_valid_destination(self.__wim.get("wim_url")): - self.__wim_url = self.__wim.get("wim_url") - else: - raise SdnConnectorError(message='Invalid wim_url value', - http_code=500) - self.__user = wim_account.get("user") - self.__passwd = wim_account.get("password") - self.client = None - self.cvp_inventory = None - self.cvp_tags = None - self.logger.debug("Arista SDN plugin {}, cvprac version {}, user:{} and config:{}". - format(wim, cvprac_version, self.__user, - self.delete_keys_from_dict(config, ('passwd',)))) - self.allDeviceFacts = [] - self.taskC = None - try: - self.__load_topology() - self.__load_switches() - except SdnConnectorError as sc: - raise sc - except Exception as e: - raise SdnConnectorError(message="Unable to load switches from CVP", - http_code=500) from e - self.logger.debug("Using topology {} in Arista Leaf switches: {}".format( - self.topology, - self.delete_keys_from_dict(self.switches, ('passwd',)))) - self.clC = AristaSDNConfigLet(self.topology) - - def __load_topology(self): - self.topology = self._VXLAN_MLAG - if self.__config and self.__config.get('topology'): - topology = self.__config.get('topology') - if topology == "VLAN": - self.topology = self._VLAN - elif topology == "VXLAN": - self.topology = self._VXLAN - elif topology == "VLAN-MLAG": - self.topology = self._VLAN_MLAG - elif topology == "VXLAN-MLAG": - self.topology = self._VXLAN_MLAG - - def __load_switches(self): - """ Retrieves the switches to configure in the following order - 1. from incoming configuration: - 1.1 using port mapping - using user and password from WIM - retrieving Lo0 and AS from switch - 1.2 from 'switches' parameter, - if any parameter is not present - Lo0 and AS - it will be requested to the switch - 2. Looking in the CloudVision inventory if not in configuration parameters - 2.1 using the switches with the topology_type tag set to 'leaf' - - All the search methods will be used - """ - self.switches = {} - if self.__config and self.__config.get(self.__SERVICE_ENDPOINT_MAPPING): - for port in self.__config.get(self.__SERVICE_ENDPOINT_MAPPING): - switch_dpid = port.get(self.__SW_ID_PARAM) - if switch_dpid and switch_dpid not in self.switches: - self.switches[switch_dpid] = {'passwd': self.__passwd, - 'ip': None, - 'usr': self.__user, - 'lo0': None, - 'AS': None, - 'serialNumber': None, - 'mlagPeerDevice': None} - - if self.__config and self.__config.get('switches'): - # Not directly from json, complete one by one - config_switches = self.__config.get('switches') - for cs, cs_content in config_switches.items(): - if cs not in self.switches: - self.switches[cs] = {'passwd': self.__passwd, - 'ip': None, - 'usr': self.__user, - 'lo0': None, - 'AS': None, - 'serialNumber': None, - 'mlagPeerDevice': None} - if cs_content: - self.switches[cs].update(cs_content) - - # Load the rest of the data - if self.client == None: - self.client = self.__connect() - self.__load_inventory() - if not self.switches: - self.__get_tags(self.__SWITCH_TAG_NAME, self.__SWITCH_TAG_VALUE) - for device in self.allDeviceFacts: - # get the switches whose topology_tag is 'leaf' - if device['serialNumber'] in self.cvp_tags: - if not self.switches.get(device['hostname']): - switch_data = {'passwd': self.__passwd, - 'ip': device['ipAddress'], - 'usr': self.__user, - 'lo0': None, - 'AS': None, - 'serialNumber': None, - 'mlagPeerDevice': None} - self.switches[device['hostname']] = switch_data - if len(self.switches) == 0: - self.logger.error("Unable to load Leaf switches from CVP") - return - - # self.switches are switch objects, one for each switch in self.switches, - # used to make eAPI calls by using switch.py module - for s in self.switches: - for device in self.allDeviceFacts: - if device['hostname'] == s: - if not self.switches[s].get('ip'): - self.switches[s]['ip'] = device['ipAddress'] - self.switches[s]['serialNumber'] = device['serialNumber'] - break - - # Each switch has a different loopback address, - # so it's a different configLet - if not self.switches[s].get('lo0'): - inf = self.__get_interface_ip(self.switches[s]['serialNumber'], self.__LOOPBACK_INTF) - self.switches[s]["lo0"] = inf.split('/')[0] - if not self.switches[s].get('AS'): - self.switches[s]["AS"] = self.__get_device_ASN(self.switches[s]['serialNumber']) - if self.topology in (self._VXLAN_MLAG, self._VLAN_MLAG): - for s in self.switches: - if not self.switches[s].get('mlagPeerDevice'): - self.switches[s]['mlagPeerDevice'] = self.__get_peer_MLAG(self.switches[s]['serialNumber']) - - def __check_service(self, service_type, connection_points, - check_vlan=True, check_num_cp=True, kwargs=None): - """ Reviews the connection points elements looking for semantic errors in the incoming data - """ - if service_type not in self.__supported_service_types: - raise Exception("The service '{}' is not supported. Only '{}' are accepted".format( - service_type, - self.__supported_service_types)) - - if check_num_cp: - if len(connection_points) < 2: - raise Exception(SdnError.CONNECTION_POINTS_SIZE) - if (len(connection_points) != self.__ELINE_num_connection_points and - service_type == self.__service_types_ELINE): - raise Exception(SdnError.CONNECTION_POINTS_SIZE) - - if check_vlan: - vlan_id = '' - for cp in connection_points: - enc_type = cp.get(self.__ENCAPSULATION_TYPE_PARAM) - if (enc_type and - enc_type not in self.__supported_encapsulation_types): - raise Exception(SdnError.ENCAPSULATION_TYPE) - encap_info = cp.get(self.__ENCAPSULATION_INFO_PARAM) - cp_vlan_id = str(encap_info.get(self.__VLAN_PARAM)) - if cp_vlan_id: - if not vlan_id: - vlan_id = cp_vlan_id - elif vlan_id != cp_vlan_id: - raise Exception(SdnError.VLAN_INCONSISTENT) - if not vlan_id: - raise Exception(SdnError.VLAN_NOT_PROVIDED) - if vlan_id in self.__get_srvVLANs(): - raise Exception('VLAN {} already assigned to a connectivity service'.format(vlan_id)) - - # Commented out for as long as parameter isn't implemented - # bandwidth = kwargs.get(self.__BANDWIDTH_PARAM) - # if not isinstance(bandwidth, int): - # self.__exception(SdnError.BANDWIDTH, http_code=400) - - # Commented out for as long as parameter isn't implemented - # backup = kwargs.get(self.__BACKUP_PARAM) - # if not isinstance(backup, bool): - # self.__exception(SdnError.BACKUP, http_code=400) - - def check_credentials(self): - """Retrieves the CloudVision version information, as the easiest way - for testing the access to CloudVision API - """ - try: - if self.client == None: - self.client = self.__connect() - result = self.client.api.get_cvp_info() - self.logger.debug(result) - except CvpLoginError as e: - self.logger.info(str(e)) - self.client = None - raise SdnConnectorError(message=SdnError.UNAUTHORIZED, - http_code=401) from e - except Exception as ex: - self.client = None - self.logger.error(str(ex)) - raise SdnConnectorError(message=SdnError.INTERNAL_ERROR, - http_code=500) from ex - - def get_connectivity_service_status(self, service_uuid, conn_info=None): - """Monitor the status of the connectivity service established - Arguments: - service_uuid (str): UUID of the connectivity service - conn_info (dict or None): Information returned by the connector - during the service creation/edition and subsequently stored in - the database. - - Returns: - dict: JSON/YAML-serializable dict that contains a mandatory key - ``sdn_status`` associated with one of the following values:: - - {'sdn_status': 'ACTIVE'} - # The service is up and running. - - {'sdn_status': 'INACTIVE'} - # The service was created, but the connector - # cannot determine yet if connectivity exists - # (ideally, the caller needs to wait and check again). - - {'sdn_status': 'DOWN'} - # Connection was previously established, - # but an error/failure was detected. - - {'sdn_status': 'ERROR'} - # An error occurred when trying to create the service/ - # establish the connectivity. - - {'sdn_status': 'BUILD'} - # Still trying to create the service, the caller - # needs to wait and check again. - - Additionally ``error_msg``(**str**) and ``sdn_info``(**dict**) - keys can be used to provide additional status explanation or - new information available for the connectivity service. - """ - try: - self.logger.debug("invoked get_connectivity_service_status '{}'".format(service_uuid)) - if not service_uuid: - raise SdnConnectorError(message='No connection service UUID', - http_code=500) - - self.__get_Connection() - if conn_info == None: - raise SdnConnectorError(message='No connection information for service UUID {}'.format(service_uuid), - http_code=500) - - if 'configLetPerSwitch' in conn_info.keys(): - c_info = conn_info - else: - c_info = None - cls_perSw = self.__get_serviceData(service_uuid, - conn_info['service_type'], - conn_info['vlan_id'], - c_info) - - t_isCancelled = False - t_isFailed = False - t_isPending = False - failed_switches = [] - for s in self.switches: - if len(cls_perSw[s]) > 0: - for cl in cls_perSw[s]: - # Fix 1030 SDN-ARISTA Key error note when deploy a NS - # Added protection to check that 'note' exists and additionally - # verify that it is managed by OSM - if (not cls_perSw[s][0]['config'] or - not cl.get('note') or - self.__MANAGED_BY_OSM not in cl['note']): - continue - note = cl['note'] - t_id = note.split(self.__SEPARATOR)[1] - result = self.client.api.get_task_by_id(t_id) - if result['workOrderUserDefinedStatus'] == 'Completed': - continue - elif result['workOrderUserDefinedStatus'] == 'Cancelled': - t_isCancelled = True - elif result['workOrderUserDefinedStatus'] == 'Failed': - t_isFailed = True - else: - t_isPending = True - failed_switches.append(s) - if t_isCancelled: - error_msg = 'Some works were cancelled in switches: {}'.format(str(failed_switches)) - sdn_status = 'DOWN' - elif t_isFailed: - error_msg = 'Some works failed in switches: {}'.format(str(failed_switches)) - sdn_status = 'ERROR' - elif t_isPending: - error_msg = 'Some works are still under execution in switches: {}'.format(str(failed_switches)) - sdn_status = 'BUILD' - else: - error_msg = '' - sdn_status = 'ACTIVE' - sdn_info = '' - return {'sdn_status': sdn_status, - 'error_msg': error_msg, - 'sdn_info': sdn_info} - except CvpLoginError as e: - self.logger.info(str(e)) - self.client = None - raise SdnConnectorError(message=SdnError.UNAUTHORIZED, - http_code=401) from e - except Exception as ex: - self.client = None - self.logger.error(str(ex), exc_info=True) - raise SdnConnectorError(message=str(ex), - http_code=500) from ex - - def create_connectivity_service(self, service_type, connection_points, - **kwargs): - """Stablish SDN/WAN connectivity between the endpoints - :param service_type: - (str): ``ELINE`` (L2), ``ELAN`` (L2), ``ETREE`` (L2), ``L3``. - :param connection_points: (list): each point corresponds to - an entry point to be connected. For WIM: from the DC - to the transport network. - For SDN: Compute/PCI to the transport network. One - connection point serves to identify the specific access and - some other service parameters, such as encapsulation type. - Each item of the list is a dict with: - "service_endpoint_id": (str)(uuid) Same meaning that for - 'service_endpoint_mapping' (see __init__) - In case the config attribute mapping_not_needed is True, - this value is not relevant. In this case - it will contain the string "device_id:device_interface_id" - "service_endpoint_encapsulation_type": None, "dot1q", ... - "service_endpoint_encapsulation_info": (dict) with: - "vlan": ..., (int, present if encapsulation is dot1q) - "vni": ... (int, present if encapsulation is vxlan), - "peers": [(ipv4_1), (ipv4_2)] (present if - encapsulation is vxlan) - "mac": ... - "device_id": ..., same meaning that for - 'service_endpoint_mapping' (see __init__) - "device_interface_id": same meaning that for - 'service_endpoint_mapping' (see __init__) - "switch_dpid": ..., present if mapping has been found - for this device_id,device_interface_id - "switch_port": ... present if mapping has been found - for this device_id,device_interface_id - "service_mapping_info": present if mapping has - been found for this device_id,device_interface_id - :param kwargs: For future versions: - bandwidth (int): value in kilobytes - latency (int): value in milliseconds - Other QoS might be passed as keyword arguments. - :return: tuple: ``(service_id, conn_info)`` containing: - - *service_uuid* (str): UUID of the established - connectivity service - - *conn_info* (dict or None): Information to be - stored at the database (or ``None``). - This information will be provided to the - :meth:`~.edit_connectivity_service` and :obj:`~.delete`. - **MUST** be JSON/YAML-serializable (plain data structures). - :raises: SdnConnectorError: In case of error. Nothing should be - created in this case. - Provide the parameter http_code - """ - try: - self.logger.debug("invoked create_connectivity_service '{}' ports: {}". - format(service_type, connection_points)) - self.__get_Connection() - self.__check_service(service_type, - connection_points, - check_vlan=True, - kwargs=kwargs) - service_uuid = str(uuid.uuid4()) - - self.logger.info("Service with uuid {} created.". - format(service_uuid)) - s_uid, s_connInf = self.__processConnection( - service_uuid, - service_type, - connection_points, - kwargs) - try: - self.__addMetadata(s_uid, service_type, s_connInf['vlan_id']) - except Exception as e: - pass - - return (s_uid, s_connInf) - except CvpLoginError as e: - self.logger.info(str(e)) - self.client = None - raise SdnConnectorError(message=SdnError.UNAUTHORIZED, - http_code=401) from e - except SdnConnectorError as sde: - raise sde - except ValueError as err: - self.client = None - self.logger.error(str(err), exc_info=True) - raise SdnConnectorError(message=str(err), - http_code=500) from err - except Exception as ex: - self.client = None - self.logger.error(str(ex), exc_info=True) - if self.raiseException: - raise ex - raise SdnConnectorError(message=str(ex), - http_code=500) from ex - - def __processConnection(self, - service_uuid, - service_type, - connection_points, - kwargs): - """ - Invoked from creation and edit methods - - Process the connection points array, - creating a set of configuration per switch where it has to be applied - for creating the configuration, the switches have to be queried for obtaining: - - the loopback address - - the BGP ASN (autonomous system number) - - the interface name of the MAC address to add in the connectivity service - Once the new configuration is ready, the __updateConnection method is invoked for appling the changes - """ - try: - cls_perSw = {} - cls_cp = {} - cl_bgp = {} - for s in self.switches: - cls_perSw[s] = [] - cls_cp[s] = [] - vlan_processed = False - vlan_id = '' - i = 0 - processed_connection_points = [] - for cp in connection_points: - i += 1 - encap_info = cp.get(self.__ENCAPSULATION_INFO_PARAM) - if not vlan_processed: - vlan_id = str(encap_info.get(self.__VLAN_PARAM)) - if not vlan_id: - continue - vni_id = encap_info.get(self.__VNI_PARAM) - if not vni_id: - vni_id = str(10000 + int(vlan_id)) - - if service_type == self.__service_types_ELAN: - cl_vlan = self.clC.getElan_vlan(service_uuid, - vlan_id, - vni_id) - else: - cl_vlan = self.clC.getEline_vlan(service_uuid, - vlan_id, - vni_id) - vlan_processed = True - - encap_type = cp.get(self.__ENCAPSULATION_TYPE_PARAM) - switch_id = encap_info.get(self.__SW_ID_PARAM) - interface = encap_info.get(self.__SW_PORT_PARAM) - switches = [{'name': switch_id, 'interface': interface}] - - # remove those connections that are equal. This happens when several sriovs are located in the same - # compute node interface, that is, in the same switch and interface - switches = [x for x in switches if x not in processed_connection_points] - if not switches: - continue - processed_connection_points += switches - for switch in switches: - if not interface: - raise SdnConnectorError(message="Connection point switch port empty for switch_dpid {}".format(switch_id), - http_code=406) - # it should be only one switch where the mac is attached - if encap_type == 'dot1q': - # SRIOV configLet for Leaf switch mac's attached to - if service_type == self.__service_types_ELAN: - cl_encap = self.clC.getElan_sriov(service_uuid, interface, vlan_id, i) - else: - cl_encap = self.clC.getEline_sriov(service_uuid, interface, vlan_id, i) - elif not encap_type: - # PT configLet for Leaf switch attached to the mac - if service_type == self.__service_types_ELAN: - cl_encap = self.clC.getElan_passthrough(service_uuid, - interface, - vlan_id, i) - else: - cl_encap = self.clC.getEline_passthrough(service_uuid, - interface, - vlan_id, i) - if cls_cp.get(switch['name']): - cls_cp[switch['name']] = str(cls_cp[switch['name']]) + cl_encap - else: - cls_cp[switch['name']] = cl_encap - - # at least 1 connection point has to be received - if not vlan_processed: - raise SdnConnectorError(message=SdnError.UNSUPPORTED_FEATURE, - http_code=406) - - for s in self.switches: - # for cl in cp_configLets: - cl_name = (self.__OSM_PREFIX + - s + - self.__SEPARATOR + service_type + str(vlan_id) + - self.__SEPARATOR + service_uuid) - cl_config = '' - # Apply BGP configuration only for VXLAN topologies - if self.topology in (self._VXLAN_MLAG, self._VXLAN): - if service_type == self.__service_types_ELAN: - cl_bgp[s] = self.clC.getElan_bgp(service_uuid, - vlan_id, - vni_id, - self.switches[s]['lo0'], - self.switches[s]['AS']) - else: - cl_bgp[s] = self.clC.getEline_bgp(service_uuid, - vlan_id, - vni_id, - self.switches[s]['lo0'], - self.switches[s]['AS']) - else: - cl_bgp[s] = '' - - if not cls_cp.get(s): - # Apply VLAN configuration to peer MLAG switch, - # only necessary when there are no connection points in the switch - if self.topology in (self._VXLAN_MLAG, self._VLAN_MLAG): - for p in self.switches: - if self.switches[p]['mlagPeerDevice'] == s: - if cls_cp.get(p): - cl_config = str(cl_vlan) - else: - cl_config = str(cl_vlan) + str(cl_bgp[s]) + str(cls_cp[s]) - - cls_perSw[s] = [{'name': cl_name, 'config': cl_config}] - - allLeafConfigured, allLeafModified = self.__updateConnection(cls_perSw) - - conn_info = { - "uuid": service_uuid, - "status": "BUILD", - "service_type": service_type, - "vlan_id": vlan_id, - "connection_points": connection_points, - "configLetPerSwitch": cls_perSw, - 'allLeafConfigured': allLeafConfigured, - 'allLeafModified': allLeafModified} - - return service_uuid, conn_info - except Exception as ex: - self.logger.debug("Exception processing connection {}: {}". - format(service_uuid, str(ex))) - raise ex - - def __updateConnection(self, cls_perSw): - """ Invoked in the creation and modification - - checks if the new connection points config is: - - already in the Cloud Vision, the configLet is modified, and applied to the switch, - executing the corresponding task - - if it has to be removed: - then configuration has to be removed from the switch executing the corresponding task, - before trying to remove the configuration - - created, the configuration set is created, associated to the switch, and the associated - task to the configLet modification executed - In case of any error, rollback is executed, removing the created elements, and restoring to the - previous state. - """ - try: - allLeafConfigured = {} - allLeafModified = {} - - for s in self.switches: - allLeafConfigured[s] = False - allLeafModified[s] = False - cl_toDelete = [] - for s in self.switches: - toDelete_in_cvp = False - if not (cls_perSw.get(s) and cls_perSw[s][0].get('config')): - # when there is no configuration, means that there is no interface - # in the switch to be connected, so the configLet has to be removed from CloudVision - # after removing the ConfigLet fron the switch if it was already there - - # get config let name and key - cl = cls_perSw[s] - try: - cvp_cl = self.client.api.get_configlet_by_name(cl[0]['name']) - # remove configLet - cl_toDelete.append(cvp_cl) - cl[0] = cvp_cl - toDelete_in_cvp = True - except CvpApiError as error: - if "Entity does not exist" in error.msg: - continue - else: - raise error - # remove configLet from device - else: - res = self.__configlet_modify(cls_perSw[s]) - allLeafConfigured[s] = res[0] - if not allLeafConfigured[s]: - continue - cl = cls_perSw[s] - res = self.__device_modify( - device_to_update=s, - new_configlets=cl, - delete=toDelete_in_cvp) - if "errorMessage" in str(res): - raise Exception(str(res)) - self.logger.info("Device {} modify result {}".format(s, res)) - for t_id in res[1]['tasks']: - if not toDelete_in_cvp: - note_msg = "{}{}{}{}##".format(self.__MANAGED_BY_OSM, - self.__SEPARATOR, - t_id, - self.__SEPARATOR) - self.client.api.add_note_to_configlet( - cls_perSw[s][0]['key'], - note_msg) - cls_perSw[s][0]['note'] = note_msg - tasks = { t_id : {'workOrderId': t_id} } - self.__exec_task(tasks, self.__EXC_TASK_EXEC_WAIT) - # with just one configLet assigned to a device, - # delete all if there are errors in next loops - if not toDelete_in_cvp: - allLeafModified[s] = True - if len(cl_toDelete) > 0: - self.__configlet_modify(cl_toDelete, delete=True) - - return allLeafConfigured, allLeafModified - except Exception as ex: - try: - self.__rollbackConnection(cls_perSw, - allLeafConfigured, - allLeafModified) - except Exception as e: - self.logger.error("Exception rolling back in updating connection: {}". - format(e), exc_info=True) - raise ex - - def __rollbackConnection(self, - cls_perSw, - allLeafConfigured, - allLeafModified): - """ Removes the given configLet from the devices and then remove the configLets - """ - for s in self.switches: - if allLeafModified[s]: - try: - res = self.__device_modify( - device_to_update=s, - new_configlets=cls_perSw[s], - delete=True) - if "errorMessage" in str(res): - raise Exception(str(res)) - tasks = dict() - for t_id in res[1]['tasks']: - tasks[t_id] = {'workOrderId': t_id} - self.__exec_task(tasks) - self.logger.info("Device {} modify result {}".format(s, res)) - except Exception as e: - self.logger.error('Error removing configlets from device {}: {}'.format(s, e)) - pass - for s in self.switches: - if allLeafConfigured[s]: - self.__configlet_modify(cls_perSw[s], delete=True) - - def __exec_task(self, tasks, tout=10): - if self.taskC == None: - self.__connect() - data = self.taskC.update_all_tasks(tasks).values() - self.taskC.task_action(data, tout, 'executed') - - def __device_modify(self, device_to_update, new_configlets, delete): - """ Updates the devices (switches) adding or removing the configLet, - the tasks Id's associated to the change are returned - """ - self.logger.info('Enter in __device_modify delete: {}'.format( - delete)) - updated = [] - changed = False - # Task Ids that have been identified during device actions - newTasks = [] - - if (len(new_configlets) == 0 or - device_to_update == None or - len(device_to_update) == 0): - data = {'updated': updated, 'tasks': newTasks} - return [changed, data] - - self.__load_inventory() - - allDeviceFacts = self.allDeviceFacts - # Work through Devices list adding device specific information - device = None - for try_device in allDeviceFacts: - # Add Device Specific Configlets - # self.logger.debug(device) - if try_device['hostname'] not in device_to_update: - continue - dev_cvp_configlets = self.client.api.get_configlets_by_device_id( - try_device['systemMacAddress']) - # self.logger.debug(dev_cvp_configlets) - try_device['deviceSpecificConfiglets'] = [] - for cvp_configlet in dev_cvp_configlets: - if int(cvp_configlet['containerCount']) == 0: - try_device['deviceSpecificConfiglets'].append( - {'name': cvp_configlet['name'], - 'key': cvp_configlet['key']}) - # self.logger.debug(device) - device = try_device - break - - # Check assigned configlets - device_update = False - add_configlets = [] - remove_configlets = [] - update_devices = [] - - if delete: - for cvp_configlet in device['deviceSpecificConfiglets']: - for cl in new_configlets: - if cvp_configlet['name'] == cl['name']: - remove_configlets.append(cvp_configlet) - device_update = True - else: - for configlet in new_configlets: - if configlet not in device['deviceSpecificConfiglets']: - add_configlets.append(configlet) - device_update = True - if device_update: - update_devices.append({'hostname': device['hostname'], - 'configlets': [add_configlets, - remove_configlets], - 'device': device}) - self.logger.info("Device to modify: {}".format(update_devices)) - - up_device = update_devices[0] - cl_toAdd = up_device['configlets'][0] - cl_toDel = up_device['configlets'][1] - # Update Configlets - try: - if delete and len(cl_toDel) > 0: - r = self.client.api.remove_configlets_from_device( - 'OSM', - up_device['device'], - cl_toDel, - create_task=True) - dev_action = r - self.logger.debug("remove_configlets_from_device {} {}".format(dev_action, cl_toDel)) - elif len(cl_toAdd) > 0: - r = self.client.api.apply_configlets_to_device( - 'OSM', - up_device['device'], - cl_toAdd, - create_task=True) - dev_action = r - self.logger.debug("apply_configlets_to_device {} {}".format(dev_action, cl_toAdd)) - - except Exception as error: - errorMessage = str(error) - msg = "errorMessage: Device {} Configlets couldnot be updated: {}".format( - up_device['hostname'], errorMessage) - raise SdnConnectorError(msg) from error - else: - if "errorMessage" in str(dev_action): - m = "Device {} Configlets update fail: {}".format( - up_device['name'], dev_action['errorMessage']) - raise SdnConnectorError(m) - else: - changed = True - if 'taskIds' in str(dev_action): - # Fix 1030 SDN-ARISTA Key error note when deploy a NS - if not dev_action['data']['taskIds']: - raise SdnConnectorError("No taskIds found: Device {} Configlets couldnot be updated".format( - up_device['hostname'])) - for taskId in dev_action['data']['taskIds']: - updated.append({up_device['hostname']: - "Configlets-{}".format( - taskId)}) - newTasks.append(taskId) - else: - updated.append({up_device['hostname']: - "Configlets-No_Specific_Tasks"}) - data = {'updated': updated, 'tasks': newTasks} - return [changed, data] - - def __configlet_modify(self, configletsToApply, delete=False): - ''' adds/update or delete the provided configLets - :param configletsToApply: list of configLets to apply - :param delete: flag to indicate if the configLets have to be deleted - from Cloud Vision Portal - :return: data: dict of module actions and taskIDs - ''' - self.logger.info('Enter in __configlet_modify delete:{}'.format( - delete)) - - # Compare configlets against cvp_facts-configlets - changed = False - checked = [] - deleted = [] - updated = [] - new = [] - - for cl in configletsToApply: - found_in_cvp = False - to_delete = False - to_update = False - to_create = False - to_check = False - try: - cvp_cl = self.client.api.get_configlet_by_name(cl['name']) - cl['key'] = cvp_cl['key'] - cl['note'] = cvp_cl['note'] - found_in_cvp = True - except CvpApiError as error: - if "Entity does not exist" in error.msg: - pass - else: - raise error - - if delete: - if found_in_cvp: - to_delete = True - configlet = {'name': cvp_cl['name'], - 'data': cvp_cl} - else: - if found_in_cvp: - cl_compare = self.__compare(cl['config'], - cvp_cl['config']) - # compare function returns a floating point number - if cl_compare[0] != 100.0: - to_update = True - configlet = {'name': cl['name'], - 'data': cvp_cl, - 'config': cl['config']} - else: - to_check = True - configlet = {'name': cl['name'], - 'key': cvp_cl['key'], - 'data': cvp_cl, - 'config': cl['config']} - else: - to_create = True - configlet = {'name': cl['name'], - 'config': cl['config']} - try: - if to_delete: - operation = 'delete' - resp = self.client.api.delete_configlet( - configlet['data']['name'], - configlet['data']['key']) - elif to_update: - operation = 'update' - resp = self.client.api.update_configlet( - configlet['config'], - configlet['data']['key'], - configlet['data']['name'], - wait_task_ids=True) - elif to_create: - operation = 'create' - resp = self.client.api.add_configlet( - configlet['name'], - configlet['config']) - else: - operation = 'checked' - resp = 'checked' - except Exception as error: - errorMessage = str(error).split(':')[-1] - message = "Configlet {} cannot be {}: {}".format( - cl['name'], operation, errorMessage) - if to_delete: - deleted.append({configlet['name']: message}) - elif to_update: - updated.append({configlet['name']: message}) - elif to_create: - new.append({configlet['name']: message}) - elif to_check: - checked.append({configlet['name']: message}) - - else: - if "error" in str(resp).lower(): - message = "Configlet {} cannot be deleted: {}".format( - cl['name'], resp['errorMessage']) - if to_delete: - deleted.append({configlet['name']: message}) - elif to_update: - updated.append({configlet['name']: message}) - elif to_create: - new.append({configlet['name']: message}) - elif to_check: - checked.append({configlet['name']: message}) - else: - if to_delete: - changed = True - deleted.append({configlet['name']: "success"}) - elif to_update: - changed = True - updated.append({configlet['name']: "success"}) - elif to_create: - changed = True - cl['key'] = resp # This key is used in API call deviceApplyConfigLet FGA - new.append({configlet['name']: "success"}) - elif to_check: - changed = False - checked.append({configlet['name']: "success"}) - - data = {'new': new, 'updated': updated, 'deleted': deleted, 'checked': checked} - return [changed, data] - - def __get_configletsDevices(self, configlets): - for s in self.switches: - configlet = configlets[s] - # Add applied Devices - if len(configlet) > 0: - configlet['devices'] = [] - applied_devices = self.client.api.get_applied_devices( - configlet['name']) - for device in applied_devices['data']: - configlet['devices'].append(device['hostName']) - - def __get_serviceData(self, service_uuid, service_type, vlan_id, conn_info=None): - cls_perSw = {} - for s in self.switches: - cls_perSw[s] = [] - if not conn_info: - srv_cls = self.__get_serviceConfigLets(service_uuid, - service_type, - vlan_id) - self.__get_configletsDevices(srv_cls) - for s in self.switches: - cl = srv_cls[s] - if len(cl) > 0: - for dev in cl['devices']: - cls_perSw[dev].append(cl) - else: - cls_perSw = conn_info['configLetPerSwitch'] - return cls_perSw - - def delete_connectivity_service(self, service_uuid, conn_info=None): - """ - Disconnect multi-site endpoints previously connected - - :param service_uuid: The one returned by create_connectivity_service - :param conn_info: The one returned by last call to 'create_connectivity_service' or 'edit_connectivity_service' - if they do not return None - :return: None - :raises: SdnConnectorException: In case of error. The parameter http_code must be filled - """ - try: - self.logger.debug('invoked delete_connectivity_service {}'. - format(service_uuid)) - if not service_uuid: - raise SdnConnectorError(message='No connection service UUID', - http_code=500) - - self.__get_Connection() - if conn_info == None: - raise SdnConnectorError(message='No connection information for service UUID {}'.format(service_uuid), - http_code=500) - c_info = None - cls_perSw = self.__get_serviceData(service_uuid, - conn_info['service_type'], - conn_info['vlan_id'], - c_info) - allLeafConfigured = {} - allLeafModified = {} - for s in self.switches: - allLeafConfigured[s] = True - allLeafModified[s] = True - found_in_cvp = False - for s in self.switches: - if cls_perSw[s]: - found_in_cvp = True - if found_in_cvp: - self.__rollbackConnection(cls_perSw, - allLeafConfigured, - allLeafModified) - else: - # if the service is not defined in Cloud Vision, return a 404 - NotFound error - raise SdnConnectorError(message='Service {} was not found in Arista Cloud Vision {}'. - format(service_uuid, self.__wim_url), - http_code=404) - self.__removeMetadata(service_uuid) - except CvpLoginError as e: - self.logger.info(str(e)) - self.client = None - raise SdnConnectorError(message=SdnError.UNAUTHORIZED, - http_code=401) from e - except SdnConnectorError as sde: - raise sde - except Exception as ex: - self.client = None - self.logger.error(ex) - if self.raiseException: - raise ex - raise SdnConnectorError(message=SdnError.INTERNAL_ERROR, - http_code=500) from ex - - def __addMetadata(self, service_uuid, service_type, vlan_id): - """ Adds the connectivity service from 'OSM_metadata' configLet - """ - found_in_cvp = False - try: - cvp_cl = self.client.api.get_configlet_by_name(self.__OSM_METADATA) - found_in_cvp = True - except CvpApiError as error: - if "Entity does not exist" in error.msg: - pass - else: - raise error - try: - new_serv = '{} {} {} {}\n'.format(self.__METADATA_PREFIX, service_type, vlan_id, service_uuid) - - if found_in_cvp: - cl_config = cvp_cl['config'] + new_serv - else: - cl_config = new_serv - cl_meta = [{'name': self.__OSM_METADATA, 'config': cl_config}] - self.__configlet_modify(cl_meta) - except Exception as e: - self.logger.error('Error in setting metadata in CloudVision from OSM for service {}: {}'. - format(service_uuid, str(e))) - pass - - def __removeMetadata(self, service_uuid): - """ Removes the connectivity service from 'OSM_metadata' configLet - """ - found_in_cvp = False - try: - cvp_cl = self.client.api.get_configlet_by_name(self.__OSM_METADATA) - found_in_cvp = True - except CvpApiError as error: - if "Entity does not exist" in error.msg: - pass - else: - raise error - try: - if found_in_cvp: - if service_uuid in cvp_cl['config']: - cl_config = '' - for line in cvp_cl['config'].split('\n'): - if service_uuid in line: - continue - else: - cl_config = cl_config + line - cl_meta = [{'name': self.__OSM_METADATA, 'config': cl_config}] - self.__configlet_modify(cl_meta) - except Exception as e: - self.logger.error('Error in removing metadata in CloudVision from OSM for service {}: {}'. - format(service_uuid, str(e))) - pass - - def edit_connectivity_service(self, - service_uuid, - conn_info=None, - connection_points=None, - **kwargs): - """ Change an existing connectivity service. - - This method's arguments and return value follow the same convention as - :meth:`~.create_connectivity_service`. - - :param service_uuid: UUID of the connectivity service. - :param conn_info: (dict or None): Information previously returned - by last call to create_connectivity_service - or edit_connectivity_service - :param connection_points: (list): If provided, the old list of - connection points will be replaced. - :param kwargs: Same meaning that create_connectivity_service - :return: dict or None: Information to be updated and stored at - the database. - When ``None`` is returned, no information should be changed. - When an empty dict is returned, the database record will - be deleted. - **MUST** be JSON/YAML-serializable (plain data structures). - Raises: - SdnConnectorError: In case of error. - """ - try: - self.logger.debug('invoked edit_connectivity_service for service {}. ports: {}'.format(service_uuid, - connection_points)) - - if not service_uuid: - raise SdnConnectorError(message='Unable to perform operation, missing or empty uuid', - http_code=500) - if not conn_info: - raise SdnConnectorError(message='Unable to perform operation, missing or empty connection information', - http_code=500) - - if connection_points == None: - return None - - self.__get_Connection() - - cls_currentPerSw = conn_info['configLetPerSwitch'] - service_type = conn_info['service_type'] - - self.__check_service(service_type, - connection_points, - check_vlan=False, - check_num_cp=False, - kwargs=kwargs) - - s_uid, s_connInf = self.__processConnection( - service_uuid, - service_type, - connection_points, - kwargs) - self.logger.info("Service with uuid {} configuration updated". - format(s_uid)) - return s_connInf - except CvpLoginError as e: - self.logger.info(str(e)) - self.client = None - raise SdnConnectorError(message=SdnError.UNAUTHORIZED, - http_code=401) from e - except SdnConnectorError as sde: - raise sde - except Exception as ex: - try: - # Add previous - # TODO check if there are pending task, and cancel them before restoring - self.__updateConnection(cls_currentPerSw) - except Exception as e: - self.logger.error("Unable to restore configuration in service {} after an error in the configuration updated: {}". - format(service_uuid, str(e))) - if self.raiseException: - raise ex - raise SdnConnectorError(message=str(ex), - http_code=500) from ex - - def clear_all_connectivity_services(self): - """ Removes all connectivity services from Arista CloudVision with two steps: - - retrives all the services from Arista CloudVision - - removes each service - """ - try: - self.logger.debug('invoked AristaImpl ' + - 'clear_all_connectivity_services') - self.__get_Connection() - s_list = self.__get_srvUUIDs() - for serv in s_list: - conn_info = {} - conn_info['service_type'] = serv['type'] - conn_info['vlan_id'] = serv['vlan'] - - self.delete_connectivity_service(serv['uuid'], conn_info) - except CvpLoginError as e: - self.logger.info(str(e)) - self.client = None - raise SdnConnectorError(message=SdnError.UNAUTHORIZED, - http_code=401) from e - except SdnConnectorError as sde: - raise sde - except Exception as ex: - self.client = None - self.logger.error(ex) - if self.raiseException: - raise ex - raise SdnConnectorError(message=SdnError.INTERNAL_ERROR, - http_code=500) from ex - - def get_all_active_connectivity_services(self): - """ Return the uuid of all the active connectivity services with two steps: - - retrives all the services from Arista CloudVision - - retrives the status of each server - """ - try: - self.logger.debug('invoked AristaImpl {}'.format( - 'get_all_active_connectivity_services')) - self.__get_Connection() - s_list = self.__get_srvUUIDs() - result = [] - for serv in s_list: - conn_info = {} - conn_info['service_type'] = serv['type'] - conn_info['vlan_id'] = serv['vlan'] - - status = self.get_connectivity_service_status(serv['uuid'], conn_info) - if status['sdn_status'] == 'ACTIVE': - result.append(serv['uuid']) - return result - except CvpLoginError as e: - self.logger.info(str(e)) - self.client = None - raise SdnConnectorError(message=SdnError.UNAUTHORIZED, - http_code=401) from e - except SdnConnectorError as sde: - raise sde - except Exception as ex: - self.client = None - self.logger.error(ex) - if self.raiseException: - raise ex - raise SdnConnectorError(message=SdnError.INTERNAL_ERROR, - http_code=500) from ex - - def __get_serviceConfigLets(self, service_uuid, service_type, vlan_id): - """ Return the configLet's associated with a connectivity service, - There should be one, as maximum, per device (switch) for a given - connectivity service - """ - srv_cls = {} - for s in self.switches: - srv_cls[s] = [] - found_in_cvp = False - name = (self.__OSM_PREFIX + - s + - self.__SEPARATOR + service_type + str(vlan_id) + - self.__SEPARATOR + service_uuid) - try: - cvp_cl = self.client.api.get_configlet_by_name(name) - found_in_cvp = True - except CvpApiError as error: - if "Entity does not exist" in error.msg: - pass - else: - raise error - if found_in_cvp: - srv_cls[s] = cvp_cl - return srv_cls - - def __get_srvVLANs(self): - """ Returns a list with all the VLAN id's used in the connectivity services managed - in tha Arista CloudVision by checking the 'OSM_metadata' configLet where this - information is stored - """ - found_in_cvp = False - try: - cvp_cl = self.client.api.get_configlet_by_name(self.__OSM_METADATA) - found_in_cvp = True - except CvpApiError as error: - if "Entity does not exist" in error.msg: - pass - else: - raise error - s_vlan_list = [] - if found_in_cvp: - lines = cvp_cl['config'].split('\n') - for line in lines: - if self.__METADATA_PREFIX in line: - s_vlan = line.split(' ')[3] - else: - continue - if (s_vlan is not None and - len(s_vlan) > 0 and - s_vlan not in s_vlan_list): - s_vlan_list.append(s_vlan) - - return s_vlan_list - - def __get_srvUUIDs(self): - """ Retrieves all the connectivity services, managed in tha Arista CloudVision - by checking the 'OSM_metadata' configLet where this information is stored - """ - found_in_cvp = False - try: - cvp_cl = self.client.api.get_configlet_by_name(self.__OSM_METADATA) - found_in_cvp = True - except CvpApiError as error: - if "Entity does not exist" in error.msg: - pass - else: - raise error - serv_list = [] - if found_in_cvp: - lines = cvp_cl['config'].split('\n') - for line in lines: - if self.__METADATA_PREFIX in line: - line = line.split(' ') - serv = {'uuid': line[4], 'type': line[2], 'vlan': line[3]} - else: - continue - if (serv is not None and - len(serv) > 0 and - serv not in serv_list): - serv_list.append(serv) - - return serv_list - - def __get_Connection(self): - """ Open a connection with Arista CloudVision, - invoking the version retrival as test - """ - try: - if self.client == None: - self.client = self.__connect() - self.client.api.get_cvp_info() - except (CvpSessionLogOutError, RequestException) as e: - self.logger.debug("Connection error '{}'. Reconnecting".format(e)) - self.client = self.__connect() - self.client.api.get_cvp_info() - - def __connect(self): - ''' Connects to CVP device using user provided credentials from initialization. - :return: CvpClient object with connection instantiated. - ''' - client = CvpClient() - protocol, _, rest_url = self.__wim_url.rpartition("://") - host, _, port = rest_url.partition(":") - if port and port.endswith("/"): - port = int(port[:-1]) - elif port: - port = int(port) - else: - port = 443 - - client.connect([host], - self.__user, - self.__passwd, - protocol=protocol or "https", - port=port, - connect_timeout=2) - client.api = CvpApi(client, request_timeout=self.__API_REQUEST_TOUT) - self.taskC = AristaCVPTask(client.api) - return client - - def __compare(self, fromText, toText, lines=10): - """ Compare text string in 'fromText' with 'toText' and produce - diffRatio - a score as a float in the range [0, 1] 2.0*M / T - T is the total number of elements in both sequences, - M is the number of matches. - Score - 1.0 if the sequences are identical, and - 0.0 if they have nothing in common. - unified diff list - Code Meaning - '- ' line unique to sequence 1 - '+ ' line unique to sequence 2 - ' ' line common to both sequences - '? ' line not present in either input sequence - """ - fromlines = fromText.splitlines(1) - tolines = toText.splitlines(1) - diff = list(difflib.unified_diff(fromlines, tolines, n=lines)) - textComp = difflib.SequenceMatcher(None, fromText, toText) - diffRatio = round(textComp.quick_ratio()*100, 2) - return [diffRatio, diff] - - def __load_inventory(self): - """ Get Inventory Data for All Devices (aka switches) from the Arista CloudVision - """ - if not self.cvp_inventory: - self.cvp_inventory = self.client.api.get_inventory() - self.allDeviceFacts = [] - for device in self.cvp_inventory: - self.allDeviceFacts.append(device) - - def __get_tags(self, name, value): - if not self.cvp_tags: - self.cvp_tags = [] - url = '/api/v1/rest/analytics/tags/labels/devices/{}/value/{}/elements'.format(name, value) - self.logger.debug('get_tags: URL {}'.format(url)) - data = self.client.get(url, timeout=self.__API_REQUEST_TOUT) - for dev in data['notifications']: - for elem in dev['updates']: - self.cvp_tags.append(elem) - self.logger.debug('Available devices with tag_name {} - value {}: {} '.format(name, value, self.cvp_tags)) - - def __get_interface_ip(self, device_id, interface): - url = '/api/v1/rest/{}/Sysdb/ip/config/ipIntfConfig/{}/'.format(device_id, interface) - self.logger.debug('get_interface_ip: URL {}'.format(url)) - try: - data = self.client.get(url, timeout=self.__API_REQUEST_TOUT) - return data['notifications'][0]['updates']['addrWithMask']['value'].split('/')[0] - except Exception: - raise SdnConnectorError("Invalid response from url {}: data {}".format(url, data)) - - def __get_device_ASN(self, device_id): - url = '/api/v1/rest/{}/Sysdb/routing/bgp/config/'.format(device_id) - self.logger.debug('get_device_ASN: URL {}'.format(url)) - try: - data = self.client.get(url, timeout=self.__API_REQUEST_TOUT) - return data['notifications'][0]['updates']['asNumber']['value']['value']['int'] - except Exception: - raise SdnConnectorError("Invalid response from url {}: data {}".format(url, data)) - - def __get_peer_MLAG(self, device_id): - peer = None - url = '/api/v1/rest/{}/Sysdb/mlag/status/'.format(device_id) - self.logger.debug('get_MLAG_status: URL {}'.format(url)) - try: - data = self.client.get(url, timeout=self.__API_REQUEST_TOUT) - if data['notifications']: - found = False - for notification in data['notifications']: - for update in notification['updates']: - if update == 'systemId': - mlagSystemId = notification['updates'][update]['value'] - found = True - break - if found: - break - # search the MLAG System Id - if found: - for s in self.switches: - if self.switches[s]['serialNumber'] == device_id: - continue - url = '/api/v1/rest/{}/Sysdb/mlag/status/'.format(self.switches[s]['serialNumber']) - self.logger.debug('Searching for MLAG system id {} in switch {}'.format(mlagSystemId, s)) - data = self.client.get(url, timeout=self.__API_REQUEST_TOUT) - found = False - for notification in data['notifications']: - for update in notification['updates']: - if update == 'systemId': - if mlagSystemId == notification['updates'][update]['value']: - peer = s - found = True - break - if found: - break - if found: - break - if peer == None: - self.logger.error('No Peer device found for device {} with MLAG address {}'.format(device_id, mlagSystemId)) - else: - self.logger.debug('Peer MLAG for device {} - value {}'.format(device_id, peer)) - return peer - except Exception: - raise SdnConnectorError("Invalid response from url {}: data {}".format(url, data)) - - def is_valid_destination(self, url): - """ Check that the provided WIM URL is correct - """ - if re.match(self.__regex, url): - return True - elif self.is_valid_ipv4_address(url): - return True - else: - return self.is_valid_ipv6_address(url) - - def is_valid_ipv4_address(self, address): - """ Checks that the given IP is IPv4 valid - """ - try: - socket.inet_pton(socket.AF_INET, address) - except AttributeError: # no inet_pton here, sorry - try: - socket.inet_aton(address) - except socket.error: - return False - return address.count('.') == 3 - except socket.error: # not a valid address - return False - return True - - def is_valid_ipv6_address(self, address): - """ Checks that the given IP is IPv6 valid - """ - try: - socket.inet_pton(socket.AF_INET6, address) - except socket.error: # not a valid address - return False - return True - - def delete_keys_from_dict(self, dict_del, lst_keys): - if dict_del == 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 diff --git a/RO-SDN-arista/requirements.txt b/RO-SDN-arista/requirements.txt deleted file mode 100644 index cd1edfe0..00000000 --- a/RO-SDN-arista/requirements.txt +++ /dev/null @@ -1,20 +0,0 @@ -## -# 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. -## - -requests -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 deleted file mode 100644 index d974aad9..00000000 --- a/RO-SDN-arista/setup.py +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -## -# 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. -## - -from setuptools import setup - -_name = "osm_rosdn_arista" - -README = """ -=========== -osm-rosdn_arista -=========== - -osm-ro pluging for arista SDN -""" - -setup( - name=_name, - description='OSM ro sdn plugin for arista', - long_description=README, - version_command=('git describe --match v* --tags --long --dirty', 'pep440-git-full'), - # version=VERSION, - # python_requires='>3.5.0', - author='ETSI OSM', - # TODO py3 author_email='', - maintainer='oscarluis.peral@atos.net', # TODO py3 - # TODO py3 maintainer_email='', - url='https://osm.etsi.org/gitweb/?p=osm/RO.git;a=summary', - license='Apache 2.0', - - packages=[_name], - include_package_data=True, - install_requires=["requests", - "uuid", - "cvprac", - "osm-ro @ git+https://osm.etsi.org/gerrit/osm/RO.git#egg=osm-ro&subdirectory=RO"], - setup_requires=['setuptools-version-command'], - entry_points={ - 'osm_rosdn.plugins': ['rosdn_arista = osm_rosdn_arista.wimconn_arista:AristaSdnConnector'] - }, -) diff --git a/RO-SDN-arista/stdeb.cfg b/RO-SDN-arista/stdeb.cfg deleted file mode 100644 index 0c718e4f..00000000 --- a/RO-SDN-arista/stdeb.cfg +++ /dev/null @@ -1,19 +0,0 @@ -# -# 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. -# - -[DEFAULT] -X-Python3-Version : >= 3.5 -Depends3: python3-requests, python3-osm-ro - diff --git a/RO-SDN-arista/tox.ini b/RO-SDN-arista/tox.ini deleted file mode 100644 index d737d6ec..00000000 --- a/RO-SDN-arista/tox.ini +++ /dev/null @@ -1,41 +0,0 @@ -## -# 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. -## - -[tox] -envlist = py3 -toxworkdir={homedir}/.tox - -[testenv] -basepython = python3 -install_command = python3 -m pip install -r requirements.txt -U {opts} {packages} -# deps = -r{toxinidir}/test-requirements.txt -commands=python3 -m unittest discover -v - -[testenv:flake8] -basepython = python3 -deps = flake8 -commands = flake8 osm_rosdn_arista --max-line-length 120 \ - --exclude .svn,CVS,.gz,.git,__pycache__,.tox,local,temp --ignore W291,W293,E226,W504 - -[testenv:unittest] -basepython = python3 -commands = python3 -m unittest osm_rosdn_arista.tests - -[testenv:build] -basepython = python3 -deps = stdeb - setuptools-version-command -commands = python3 setup.py --command-packages=stdeb.command bdist_deb - diff --git a/RO-SDN-arista_cloudvision/Makefile b/RO-SDN-arista_cloudvision/Makefile new file mode 100644 index 00000000..4890e8e3 --- /dev/null +++ b/RO-SDN-arista_cloudvision/Makefile @@ -0,0 +1,25 @@ +## +# 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. +## + +all: clean package + +clean: + rm -rf dist deb_dist osm_rosdn_arista_cloudvision-*.tar.gz osm_rosdn_arista_cloudvision.egg-info .eggs + +package: + python3 setup.py --command-packages=stdeb.command sdist_dsc + cp debian/python3-osm-rosdn-arista-cloudvision.postinst deb_dist/osm-rosdn-arista-cloudvision*/debian/ + cd deb_dist/osm-rosdn-arista-cloudvision*/ && dpkg-buildpackage -rfakeroot -uc -us + diff --git a/RO-SDN-arista_cloudvision/debian/python3-osm-rosdn-arista-cloudvision.postinst b/RO-SDN-arista_cloudvision/debian/python3-osm-rosdn-arista-cloudvision.postinst new file mode 100755 index 00000000..d87fa261 --- /dev/null +++ b/RO-SDN-arista_cloudvision/debian/python3-osm-rosdn-arista-cloudvision.postinst @@ -0,0 +1,23 @@ +#!/bin/bash + +## +# 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: OSM_TECH@list.etsi.org +## + +echo "POST INSTALL OSM-ROSDN-ARISTA" + +#Pip packages required for openstack connector +python3 -m pip install cvprac diff --git a/RO-SDN-arista_cloudvision/osm_rosdn_arista_cloudvision/aristaConfigLet.py b/RO-SDN-arista_cloudvision/osm_rosdn_arista_cloudvision/aristaConfigLet.py new file mode 100644 index 00000000..8e34091e --- /dev/null +++ b/RO-SDN-arista_cloudvision/osm_rosdn_arista_cloudvision/aristaConfigLet.py @@ -0,0 +1,131 @@ +# -*- 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. +## + + +class AristaSDNConfigLet: + _VLAN = "VLAN" + _VXLAN = "VXLAN" + _VLAN_MLAG = "VLAN-MLAG" + _VXLAN_MLAG = "VXLAN-MLAG" + topology = _VXLAN_MLAG + + def __init__(self, topology=_VXLAN_MLAG): + self.topology = topology + + _basic_int =""" +interface {interface} + !! service: {uuid} + switchport + switchport mode {type} + switchport {switchport_def} +! +""" + _int_SRIOV = "trunk group {service}{vlan_id}" + _int_PASSTROUGH = "access vlan {vlan_id}" + + def _get_interface(self, uuid, interface, vlan_id, s_type, index, i_type): + if i_type == "trunk": + switchport_def = self._int_SRIOV.format(service=s_type, vlan_id=vlan_id) + else: + switchport_def = self._int_PASSTROUGH.format(vlan_id=vlan_id) + return self._basic_int.format(uuid=uuid, + interface=interface, + type=i_type, + switchport_def=switchport_def) + + def getElan_sriov(self, uuid, interface, vlan_id, index): + return self._get_interface(uuid, interface, vlan_id, "ELAN", index, "trunk") + + def getEline_sriov(self, uuid, interface, vlan_id, index): + return self._get_interface(uuid, interface, vlan_id, "ELINE", index, "trunk") + + def getElan_passthrough(self, uuid, interface, vlan_id, index): + return self._get_interface(uuid, interface, vlan_id, "ELAN", index, "dot1q-tunnel") + + def getEline_passthrough(self, uuid, interface, vlan_id, index): + return self._get_interface(uuid, interface, vlan_id, "ELINE", index, "dot1q-tunnel") + + _basic_vlan =""" +vlan {vlan} + !! service: {service} {vlan} {uuid} + name {service}{vlan} + trunk group {service}{vlan} +""" + _basic_mlag =""" trunk group MLAGPEER +""" + _basic_vxlan ="""interface VXLAN1 + VXLAN vlan {vlan} vni {vni} +""" + _basic_end ="!" + + _configLet_VLAN = _basic_vlan + _basic_end + _configLet_VXLAN = _basic_vlan + _basic_vxlan + _basic_end + _configLet_VLAN_MLAG = _basic_vlan + _basic_mlag + _basic_end + _configLet_VXLAN_MLAG = _basic_vlan + _basic_mlag + _basic_vxlan + _basic_end + + def _get_vlan(self, uuid, vlan_id, vni_id, s_type): + if self.topology == self._VLAN: + return self._configLet_VLAN.format(service=s_type, vlan=vlan_id, uuid=uuid) + if self.topology == self._VLAN_MLAG: + return self._configLet_VLAN_MLAG.format(service=s_type, vlan=vlan_id, uuid=uuid) + if self.topology == self._VXLAN: + return self._configLet_VXLAN.format(service=s_type, vlan=vlan_id, uuid=uuid, vni=vni_id) + if self.topology == self._VXLAN_MLAG: + return self._configLet_VXLAN_MLAG.format(service=s_type, vlan=vlan_id, uuid=uuid, vni=vni_id) + + def getElan_vlan(self, uuid, vlan_id, vni_id): + return self._get_vlan(uuid, vlan_id, vni_id, "ELAN") + + def getEline_vlan(self, uuid, vlan_id, vni_id): + return self._get_vlan(uuid, vlan_id, vni_id, "ELINE") + + _configLet_BGP = """ +router bgp {bgp} + vlan {vlan} + !! service: {uuid} + rd {loopback}:{vni} + route-target both {vni}:{vni} + redistribute learned +! +""" + + def _get_bgp(self, uuid, vlan_id, vni_id, loopback0, bgp, s_type): + if self.topology == self._VXLAN or self.topology == self._VXLAN_MLAG: + return self._configLet_BGP.format(uuid=uuid, + bgp=bgp, + vlan=vlan_id, + loopback=loopback0, + vni=vni_id) + + + def getElan_bgp(self, uuid, vlan_id, vni_id, loopback0, bgp): + return self._get_bgp(uuid, vlan_id, vni_id, loopback0, bgp, "ELAN") + + def getEline_bgp(self, uuid, vlan_id, vni_id, loopback0, bgp): + return self._get_bgp(uuid, vlan_id, vni_id, loopback0, bgp, "ELINE") diff --git a/RO-SDN-arista_cloudvision/osm_rosdn_arista_cloudvision/aristaTask.py b/RO-SDN-arista_cloudvision/osm_rosdn_arista_cloudvision/aristaTask.py new file mode 100644 index 00000000..a338afd4 --- /dev/null +++ b/RO-SDN-arista_cloudvision/osm_rosdn_arista_cloudvision/aristaTask.py @@ -0,0 +1,132 @@ +# -*- 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. +## +import time + + +class AristaCVPTask: + def __init__(self, cvpClientApi): + self.cvpClientApi = cvpClientApi + + def __get_id(self, task): + return task.get("workOrderId") + + def __get_state(self, task): + return task.get("workOrderUserDefinedStatus") + + def __execute_task(self, task_id): + return self.cvpClientApi.execute_task(task_id) + + def __cancel_task(self, task_id): + return self.cvpClientApi.cancel_task(task_id) + + def __apply_state(self, task, state): + t_id = self.__get_id(task) + self.cvpClientApi.add_note_to_task(t_id, "Executed by OSM") + if state == "executed": + return self.__execute_task(t_id) + elif state == "cancelled": + return self.__cancel_task(t_id) + + def __actionable(self, state): + return state in ["Pending"] + + def __terminal(self, state): + return state in ["Completed", "Cancelled"] + + def __state_is_different(self, task, target): + return self.__get_state(task) != target + + def update_all_tasks(self, data): + new_data = dict() + for task_id in data.keys(): + res = self.cvpClientApi.get_task_by_id(task_id) + new_data[task_id] = res + return new_data + + def get_pending_tasks(self): + return self.cvpClientApi.get_tasks_by_status('Pending') + + def get_pending_tasks_old(self): + taskList = [] + tasksField = {'workOrderId': 'workOrderId', + 'workOrderState': 'workOrderState', + 'currentTaskName': 'currentTaskName', + 'description': 'description', + 'workOrderUserDefinedStatus': + 'workOrderUserDefinedStatus', + 'note': 'note', + 'taskStatus': 'taskStatus', + 'workOrderDetails': 'workOrderDetails'} + tasks = self.cvpClientApi.get_tasks_by_status('Pending') + # Reduce task data to required fields + for task in tasks: + taskFacts = {} + for field in task.keys(): + if field in tasksField: + taskFacts[tasksField[field]] = task[field] + taskList.append(taskFacts) + return taskList + + def task_action(self, tasks, wait, state): + changed = False + data = dict() + warnings = list() + + at = [t for t in tasks if self.__actionable(self.__get_state(t))] + actionable_tasks = at + + if len(actionable_tasks) == 0: + warnings.append("No actionable tasks found on CVP") + return changed, data, warnings + + for task in actionable_tasks: + if self.__state_is_different(task, state): + self.__apply_state(task, state) + changed = True + data[self.__get_id(task)] = task + + if wait == 0: + return changed, data, warnings + + start = time.time() + now = time.time() + while (now - start) < wait: + data = self.update_all_tasks(data) + if all([self.__terminal(self.__get_state(t)) for t in data.values()]): + break + time.sleep(1) + now = time.time() + + if wait: + for i, task in data.items(): + if not self.__terminal(self.__get_state(task)): + warnings.append("Task {} has not completed in {} seconds". + format(i, wait)) + + return changed, data, warnings diff --git a/RO-SDN-arista_cloudvision/osm_rosdn_arista_cloudvision/wimconn_arista.py b/RO-SDN-arista_cloudvision/osm_rosdn_arista_cloudvision/wimconn_arista.py new file mode 100644 index 00000000..37f4c582 --- /dev/null +++ b/RO-SDN-arista_cloudvision/osm_rosdn_arista_cloudvision/wimconn_arista.py @@ -0,0 +1,1621 @@ +# -*- 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 osm_ro.wim.sdnconn import SdnConnectorBase, SdnConnectorError +import re +import socket +# Required by compare function +import difflib +# Library that uses Levenshtein Distance to calculate the differences +# between strings. +# from fuzzywuzzy import fuzz + +import logging +import uuid +from enum import Enum +from requests import RequestException + +from cvprac.cvp_client import CvpClient +from cvprac.cvp_api import CvpApi +from cvprac.cvp_client_errors import CvpLoginError, CvpSessionLogOutError, CvpApiError +from cvprac import __version__ as cvprac_version + +from osm_rosdn_arista_cloudvision.aristaConfigLet import AristaSDNConfigLet +from osm_rosdn_arista_cloudvision.aristaTask import AristaCVPTask + + +class SdnError(Enum): + UNREACHABLE = 'Unable to reach the WIM.', + VLAN_INCONSISTENT = \ + 'VLAN value inconsistent between the connection points', + VLAN_NOT_PROVIDED = 'VLAN value not provided', + CONNECTION_POINTS_SIZE = \ + 'Unexpected number of connection points: 2 expected.', + ENCAPSULATION_TYPE = \ + 'Unexpected service_endpoint_encapsulation_type. \ + Only "dotq1" is accepted.', + BANDWIDTH = 'Unable to get the bandwidth.', + STATUS = 'Unable to get the status for the service.', + DELETE = 'Unable to delete service.', + CLEAR_ALL = 'Unable to clear all the services', + UNKNOWN_ACTION = 'Unknown action invoked.', + BACKUP = 'Unable to get the backup parameter.', + UNSUPPORTED_FEATURE = "Unsupported feature", + UNAUTHORIZED = "Failed while authenticating", + INTERNAL_ERROR = "Internal error" + + +class AristaSdnConnector(SdnConnectorBase): + """Arista class for the SDN connectors + + Arguments: + wim (dict): WIM record, as stored in the database + wim_account (dict): WIM account record, as stored in the database + config + The arguments of the constructor are converted to object attributes. + An extra property, ``service_endpoint_mapping`` is created from ``config``. + + The access to Arista CloudVision is made through the API defined in + https://github.com/aristanetworks/cvprac + The a connectivity service consist in creating a VLAN and associate the interfaces + of the connection points MAC addresses to this VLAN in all the switches of the topology, + the BDP is also configured for this VLAN. + + The Arista Cloud Vision API workflow is the following + -- The switch configuration is defined as a set of switch configuration commands, + what is called 'ConfigLet' + -- The ConfigLet is associated to the device (leaf switch) + -- Automatically a task is associated to this activity for change control, the task + in this stage is in 'Pending' state + -- The task will be executed so that the configuration is applied to the switch. + -- The service information is saved in the response of the creation call + -- All created services identification is stored in a generic ConfigLet 'OSM_metadata' + to keep track of the managed resources by OSM in the Arista deployment. + """ + __supported_service_types = ["ELINE (L2)", "ELINE", "ELAN"] + __service_types_ELAN = "ELAN" + __service_types_ELINE = "ELINE" + __ELINE_num_connection_points = 2 + __supported_service_types = ["ELINE", "ELAN"] + __supported_encapsulation_types = ["dot1q"] + __WIM_LOGGER = 'openmano.sdnconn.arista' + __SERVICE_ENDPOINT_MAPPING = 'service_endpoint_mapping' + __ENCAPSULATION_TYPE_PARAM = "service_endpoint_encapsulation_type" + __ENCAPSULATION_INFO_PARAM = "service_endpoint_encapsulation_info" + __BACKUP_PARAM = "backup" + __BANDWIDTH_PARAM = "bandwidth" + __SERVICE_ENDPOINT_PARAM = "service_endpoint_id" + __MAC_PARAM = "mac" + __WAN_SERVICE_ENDPOINT_PARAM = "service_endpoint_id" + __WAN_MAPPING_INFO_PARAM = "service_mapping_info" + __DEVICE_ID_PARAM = "device_id" + __DEVICE_INTERFACE_ID_PARAM = "device_interface_id" + __SW_ID_PARAM = "switch_dpid" + __SW_PORT_PARAM = "switch_port" + __VLAN_PARAM = "vlan" + __VNI_PARAM = "vni" + __SEPARATOR = '_' + __MANAGED_BY_OSM = '## Managed by OSM ' + __OSM_PREFIX = "osm_" + __OSM_METADATA = "OSM_metadata" + __METADATA_PREFIX = '!## Service' + __EXC_TASK_EXEC_WAIT = 10 + __ROLLB_TASK_EXEC_WAIT = 10 + __API_REQUEST_TOUT = 60 + __SWITCH_TAG_NAME = 'topology_type' + __SWITCH_TAG_VALUE = 'leaf' + __LOOPBACK_INTF = "Loopback0" + _VLAN = "VLAN" + _VXLAN = "VXLAN" + _VLAN_MLAG = "VLAN-MLAG" + _VXLAN_MLAG = "VXLAN-MLAG" + + + def __init__(self, wim, wim_account, config=None, logger=None): + """ + + :param wim: (dict). Contains among others 'wim_url' + :param wim_account: (dict). Contains among others 'uuid' (internal id), 'name', + 'sdn' (True if is intended for SDN-assist or False if intended for WIM), 'user', 'password'. + :param config: (dict or None): Particular information of plugin. These keys if present have a common meaning: + 'mapping_not_needed': (bool) False by default or if missing, indicates that mapping is not needed. + 'service_endpoint_mapping': (list) provides the internal endpoint mapping. The meaning is: + KEY meaning for WIM meaning for SDN assist + -------- -------- -------- + device_id pop_switch_dpid compute_id + device_interface_id pop_switch_port compute_pci_address + service_endpoint_id wan_service_endpoint_id SDN_service_endpoint_id + service_mapping_info wan_service_mapping_info SDN_service_mapping_info + contains extra information if needed. Text in Yaml format + switch_dpid wan_switch_dpid SDN_switch_dpid + switch_port wan_switch_port SDN_switch_port + datacenter_id vim_account vim_account + id: (internal, do not use) + wim_id: (internal, do not use) + :param logger (logging.Logger): optional logger object. If none is passed 'openmano.sdn.sdnconn' is used. + """ + self.__regex = re.compile( + r'^(?:http|ftp)s?://' # http:// or https:// + r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # domain... + r'localhost|' # localhost... + r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip + r'(?::\d+)?', re.IGNORECASE) # optional port + self.raiseException = True + self.logger = logger or logging.getLogger(self.__WIM_LOGGER) + super().__init__(wim, wim_account, config, self.logger) + self.__wim = wim + self.__wim_account = wim_account + self.__config = config + if self.is_valid_destination(self.__wim.get("wim_url")): + self.__wim_url = self.__wim.get("wim_url") + else: + raise SdnConnectorError(message='Invalid wim_url value', + http_code=500) + self.__user = wim_account.get("user") + self.__passwd = wim_account.get("password") + self.client = None + self.cvp_inventory = None + self.cvp_tags = None + self.logger.debug("Arista SDN plugin {}, cvprac version {}, user:{} and config:{}". + format(wim, cvprac_version, self.__user, + self.delete_keys_from_dict(config, ('passwd',)))) + self.allDeviceFacts = [] + self.taskC = None + try: + self.__load_topology() + self.__load_switches() + except SdnConnectorError as sc: + raise sc + except Exception as e: + raise SdnConnectorError(message="Unable to load switches from CVP", + http_code=500) from e + self.logger.debug("Using topology {} in Arista Leaf switches: {}".format( + self.topology, + self.delete_keys_from_dict(self.switches, ('passwd',)))) + self.clC = AristaSDNConfigLet(self.topology) + + def __load_topology(self): + self.topology = self._VXLAN_MLAG + if self.__config and self.__config.get('topology'): + topology = self.__config.get('topology') + if topology == "VLAN": + self.topology = self._VLAN + elif topology == "VXLAN": + self.topology = self._VXLAN + elif topology == "VLAN-MLAG": + self.topology = self._VLAN_MLAG + elif topology == "VXLAN-MLAG": + self.topology = self._VXLAN_MLAG + + def __load_switches(self): + """ Retrieves the switches to configure in the following order + 1. from incoming configuration: + 1.1 using port mapping + using user and password from WIM + retrieving Lo0 and AS from switch + 1.2 from 'switches' parameter, + if any parameter is not present + Lo0 and AS - it will be requested to the switch + 2. Looking in the CloudVision inventory if not in configuration parameters + 2.1 using the switches with the topology_type tag set to 'leaf' + + All the search methods will be used + """ + self.switches = {} + if self.__config and self.__config.get(self.__SERVICE_ENDPOINT_MAPPING): + for port in self.__config.get(self.__SERVICE_ENDPOINT_MAPPING): + switch_dpid = port.get(self.__SW_ID_PARAM) + if switch_dpid and switch_dpid not in self.switches: + self.switches[switch_dpid] = {'passwd': self.__passwd, + 'ip': None, + 'usr': self.__user, + 'lo0': None, + 'AS': None, + 'serialNumber': None, + 'mlagPeerDevice': None} + + if self.__config and self.__config.get('switches'): + # Not directly from json, complete one by one + config_switches = self.__config.get('switches') + for cs, cs_content in config_switches.items(): + if cs not in self.switches: + self.switches[cs] = {'passwd': self.__passwd, + 'ip': None, + 'usr': self.__user, + 'lo0': None, + 'AS': None, + 'serialNumber': None, + 'mlagPeerDevice': None} + if cs_content: + self.switches[cs].update(cs_content) + + # Load the rest of the data + if self.client == None: + self.client = self.__connect() + self.__load_inventory() + if not self.switches: + self.__get_tags(self.__SWITCH_TAG_NAME, self.__SWITCH_TAG_VALUE) + for device in self.allDeviceFacts: + # get the switches whose topology_tag is 'leaf' + if device['serialNumber'] in self.cvp_tags: + if not self.switches.get(device['hostname']): + switch_data = {'passwd': self.__passwd, + 'ip': device['ipAddress'], + 'usr': self.__user, + 'lo0': None, + 'AS': None, + 'serialNumber': None, + 'mlagPeerDevice': None} + self.switches[device['hostname']] = switch_data + if len(self.switches) == 0: + self.logger.error("Unable to load Leaf switches from CVP") + return + + # self.switches are switch objects, one for each switch in self.switches, + # used to make eAPI calls by using switch.py module + for s in self.switches: + for device in self.allDeviceFacts: + if device['hostname'] == s: + if not self.switches[s].get('ip'): + self.switches[s]['ip'] = device['ipAddress'] + self.switches[s]['serialNumber'] = device['serialNumber'] + break + + # Each switch has a different loopback address, + # so it's a different configLet + if not self.switches[s].get('lo0'): + inf = self.__get_interface_ip(self.switches[s]['serialNumber'], self.__LOOPBACK_INTF) + self.switches[s]["lo0"] = inf.split('/')[0] + if not self.switches[s].get('AS'): + self.switches[s]["AS"] = self.__get_device_ASN(self.switches[s]['serialNumber']) + if self.topology in (self._VXLAN_MLAG, self._VLAN_MLAG): + for s in self.switches: + if not self.switches[s].get('mlagPeerDevice'): + self.switches[s]['mlagPeerDevice'] = self.__get_peer_MLAG(self.switches[s]['serialNumber']) + + def __check_service(self, service_type, connection_points, + check_vlan=True, check_num_cp=True, kwargs=None): + """ Reviews the connection points elements looking for semantic errors in the incoming data + """ + if service_type not in self.__supported_service_types: + raise Exception("The service '{}' is not supported. Only '{}' are accepted".format( + service_type, + self.__supported_service_types)) + + if check_num_cp: + if len(connection_points) < 2: + raise Exception(SdnError.CONNECTION_POINTS_SIZE) + if (len(connection_points) != self.__ELINE_num_connection_points and + service_type == self.__service_types_ELINE): + raise Exception(SdnError.CONNECTION_POINTS_SIZE) + + if check_vlan: + vlan_id = '' + for cp in connection_points: + enc_type = cp.get(self.__ENCAPSULATION_TYPE_PARAM) + if (enc_type and + enc_type not in self.__supported_encapsulation_types): + raise Exception(SdnError.ENCAPSULATION_TYPE) + encap_info = cp.get(self.__ENCAPSULATION_INFO_PARAM) + cp_vlan_id = str(encap_info.get(self.__VLAN_PARAM)) + if cp_vlan_id: + if not vlan_id: + vlan_id = cp_vlan_id + elif vlan_id != cp_vlan_id: + raise Exception(SdnError.VLAN_INCONSISTENT) + if not vlan_id: + raise Exception(SdnError.VLAN_NOT_PROVIDED) + if vlan_id in self.__get_srvVLANs(): + raise Exception('VLAN {} already assigned to a connectivity service'.format(vlan_id)) + + # Commented out for as long as parameter isn't implemented + # bandwidth = kwargs.get(self.__BANDWIDTH_PARAM) + # if not isinstance(bandwidth, int): + # self.__exception(SdnError.BANDWIDTH, http_code=400) + + # Commented out for as long as parameter isn't implemented + # backup = kwargs.get(self.__BACKUP_PARAM) + # if not isinstance(backup, bool): + # self.__exception(SdnError.BACKUP, http_code=400) + + def check_credentials(self): + """Retrieves the CloudVision version information, as the easiest way + for testing the access to CloudVision API + """ + try: + if self.client == None: + self.client = self.__connect() + result = self.client.api.get_cvp_info() + self.logger.debug(result) + except CvpLoginError as e: + self.logger.info(str(e)) + self.client = None + raise SdnConnectorError(message=SdnError.UNAUTHORIZED, + http_code=401) from e + except Exception as ex: + self.client = None + self.logger.error(str(ex)) + raise SdnConnectorError(message=SdnError.INTERNAL_ERROR, + http_code=500) from ex + + def get_connectivity_service_status(self, service_uuid, conn_info=None): + """Monitor the status of the connectivity service established + Arguments: + service_uuid (str): UUID of the connectivity service + conn_info (dict or None): Information returned by the connector + during the service creation/edition and subsequently stored in + the database. + + Returns: + dict: JSON/YAML-serializable dict that contains a mandatory key + ``sdn_status`` associated with one of the following values:: + + {'sdn_status': 'ACTIVE'} + # The service is up and running. + + {'sdn_status': 'INACTIVE'} + # The service was created, but the connector + # cannot determine yet if connectivity exists + # (ideally, the caller needs to wait and check again). + + {'sdn_status': 'DOWN'} + # Connection was previously established, + # but an error/failure was detected. + + {'sdn_status': 'ERROR'} + # An error occurred when trying to create the service/ + # establish the connectivity. + + {'sdn_status': 'BUILD'} + # Still trying to create the service, the caller + # needs to wait and check again. + + Additionally ``error_msg``(**str**) and ``sdn_info``(**dict**) + keys can be used to provide additional status explanation or + new information available for the connectivity service. + """ + try: + self.logger.debug("invoked get_connectivity_service_status '{}'".format(service_uuid)) + if not service_uuid: + raise SdnConnectorError(message='No connection service UUID', + http_code=500) + + self.__get_Connection() + if conn_info == None: + raise SdnConnectorError(message='No connection information for service UUID {}'.format(service_uuid), + http_code=500) + + if 'configLetPerSwitch' in conn_info.keys(): + c_info = conn_info + else: + c_info = None + cls_perSw = self.__get_serviceData(service_uuid, + conn_info['service_type'], + conn_info['vlan_id'], + c_info) + + t_isCancelled = False + t_isFailed = False + t_isPending = False + failed_switches = [] + for s in self.switches: + if len(cls_perSw[s]) > 0: + for cl in cls_perSw[s]: + # Fix 1030 SDN-ARISTA Key error note when deploy a NS + # Added protection to check that 'note' exists and additionally + # verify that it is managed by OSM + if (not cls_perSw[s][0]['config'] or + not cl.get('note') or + self.__MANAGED_BY_OSM not in cl['note']): + continue + note = cl['note'] + t_id = note.split(self.__SEPARATOR)[1] + result = self.client.api.get_task_by_id(t_id) + if result['workOrderUserDefinedStatus'] == 'Completed': + continue + elif result['workOrderUserDefinedStatus'] == 'Cancelled': + t_isCancelled = True + elif result['workOrderUserDefinedStatus'] == 'Failed': + t_isFailed = True + else: + t_isPending = True + failed_switches.append(s) + if t_isCancelled: + error_msg = 'Some works were cancelled in switches: {}'.format(str(failed_switches)) + sdn_status = 'DOWN' + elif t_isFailed: + error_msg = 'Some works failed in switches: {}'.format(str(failed_switches)) + sdn_status = 'ERROR' + elif t_isPending: + error_msg = 'Some works are still under execution in switches: {}'.format(str(failed_switches)) + sdn_status = 'BUILD' + else: + error_msg = '' + sdn_status = 'ACTIVE' + sdn_info = '' + return {'sdn_status': sdn_status, + 'error_msg': error_msg, + 'sdn_info': sdn_info} + except CvpLoginError as e: + self.logger.info(str(e)) + self.client = None + raise SdnConnectorError(message=SdnError.UNAUTHORIZED, + http_code=401) from e + except Exception as ex: + self.client = None + self.logger.error(str(ex), exc_info=True) + raise SdnConnectorError(message=str(ex), + http_code=500) from ex + + def create_connectivity_service(self, service_type, connection_points, + **kwargs): + """Stablish SDN/WAN connectivity between the endpoints + :param service_type: + (str): ``ELINE`` (L2), ``ELAN`` (L2), ``ETREE`` (L2), ``L3``. + :param connection_points: (list): each point corresponds to + an entry point to be connected. For WIM: from the DC + to the transport network. + For SDN: Compute/PCI to the transport network. One + connection point serves to identify the specific access and + some other service parameters, such as encapsulation type. + Each item of the list is a dict with: + "service_endpoint_id": (str)(uuid) Same meaning that for + 'service_endpoint_mapping' (see __init__) + In case the config attribute mapping_not_needed is True, + this value is not relevant. In this case + it will contain the string "device_id:device_interface_id" + "service_endpoint_encapsulation_type": None, "dot1q", ... + "service_endpoint_encapsulation_info": (dict) with: + "vlan": ..., (int, present if encapsulation is dot1q) + "vni": ... (int, present if encapsulation is vxlan), + "peers": [(ipv4_1), (ipv4_2)] (present if + encapsulation is vxlan) + "mac": ... + "device_id": ..., same meaning that for + 'service_endpoint_mapping' (see __init__) + "device_interface_id": same meaning that for + 'service_endpoint_mapping' (see __init__) + "switch_dpid": ..., present if mapping has been found + for this device_id,device_interface_id + "switch_port": ... present if mapping has been found + for this device_id,device_interface_id + "service_mapping_info": present if mapping has + been found for this device_id,device_interface_id + :param kwargs: For future versions: + bandwidth (int): value in kilobytes + latency (int): value in milliseconds + Other QoS might be passed as keyword arguments. + :return: tuple: ``(service_id, conn_info)`` containing: + - *service_uuid* (str): UUID of the established + connectivity service + - *conn_info* (dict or None): Information to be + stored at the database (or ``None``). + This information will be provided to the + :meth:`~.edit_connectivity_service` and :obj:`~.delete`. + **MUST** be JSON/YAML-serializable (plain data structures). + :raises: SdnConnectorError: In case of error. Nothing should be + created in this case. + Provide the parameter http_code + """ + try: + self.logger.debug("invoked create_connectivity_service '{}' ports: {}". + format(service_type, connection_points)) + self.__get_Connection() + self.__check_service(service_type, + connection_points, + check_vlan=True, + kwargs=kwargs) + service_uuid = str(uuid.uuid4()) + + self.logger.info("Service with uuid {} created.". + format(service_uuid)) + s_uid, s_connInf = self.__processConnection( + service_uuid, + service_type, + connection_points, + kwargs) + try: + self.__addMetadata(s_uid, service_type, s_connInf['vlan_id']) + except Exception as e: + pass + + return (s_uid, s_connInf) + except CvpLoginError as e: + self.logger.info(str(e)) + self.client = None + raise SdnConnectorError(message=SdnError.UNAUTHORIZED, + http_code=401) from e + except SdnConnectorError as sde: + raise sde + except ValueError as err: + self.client = None + self.logger.error(str(err), exc_info=True) + raise SdnConnectorError(message=str(err), + http_code=500) from err + except Exception as ex: + self.client = None + self.logger.error(str(ex), exc_info=True) + if self.raiseException: + raise ex + raise SdnConnectorError(message=str(ex), + http_code=500) from ex + + def __processConnection(self, + service_uuid, + service_type, + connection_points, + kwargs): + """ + Invoked from creation and edit methods + + Process the connection points array, + creating a set of configuration per switch where it has to be applied + for creating the configuration, the switches have to be queried for obtaining: + - the loopback address + - the BGP ASN (autonomous system number) + - the interface name of the MAC address to add in the connectivity service + Once the new configuration is ready, the __updateConnection method is invoked for appling the changes + """ + try: + cls_perSw = {} + cls_cp = {} + cl_bgp = {} + for s in self.switches: + cls_perSw[s] = [] + cls_cp[s] = [] + vlan_processed = False + vlan_id = '' + i = 0 + processed_connection_points = [] + for cp in connection_points: + i += 1 + encap_info = cp.get(self.__ENCAPSULATION_INFO_PARAM) + if not vlan_processed: + vlan_id = str(encap_info.get(self.__VLAN_PARAM)) + if not vlan_id: + continue + vni_id = encap_info.get(self.__VNI_PARAM) + if not vni_id: + vni_id = str(10000 + int(vlan_id)) + + if service_type == self.__service_types_ELAN: + cl_vlan = self.clC.getElan_vlan(service_uuid, + vlan_id, + vni_id) + else: + cl_vlan = self.clC.getEline_vlan(service_uuid, + vlan_id, + vni_id) + vlan_processed = True + + encap_type = cp.get(self.__ENCAPSULATION_TYPE_PARAM) + switch_id = encap_info.get(self.__SW_ID_PARAM) + interface = encap_info.get(self.__SW_PORT_PARAM) + switches = [{'name': switch_id, 'interface': interface}] + + # remove those connections that are equal. This happens when several sriovs are located in the same + # compute node interface, that is, in the same switch and interface + switches = [x for x in switches if x not in processed_connection_points] + if not switches: + continue + processed_connection_points += switches + for switch in switches: + if not interface: + raise SdnConnectorError(message="Connection point switch port empty for switch_dpid {}".format(switch_id), + http_code=406) + # it should be only one switch where the mac is attached + if encap_type == 'dot1q': + # SRIOV configLet for Leaf switch mac's attached to + if service_type == self.__service_types_ELAN: + cl_encap = self.clC.getElan_sriov(service_uuid, interface, vlan_id, i) + else: + cl_encap = self.clC.getEline_sriov(service_uuid, interface, vlan_id, i) + elif not encap_type: + # PT configLet for Leaf switch attached to the mac + if service_type == self.__service_types_ELAN: + cl_encap = self.clC.getElan_passthrough(service_uuid, + interface, + vlan_id, i) + else: + cl_encap = self.clC.getEline_passthrough(service_uuid, + interface, + vlan_id, i) + if cls_cp.get(switch['name']): + cls_cp[switch['name']] = str(cls_cp[switch['name']]) + cl_encap + else: + cls_cp[switch['name']] = cl_encap + + # at least 1 connection point has to be received + if not vlan_processed: + raise SdnConnectorError(message=SdnError.UNSUPPORTED_FEATURE, + http_code=406) + + for s in self.switches: + # for cl in cp_configLets: + cl_name = (self.__OSM_PREFIX + + s + + self.__SEPARATOR + service_type + str(vlan_id) + + self.__SEPARATOR + service_uuid) + cl_config = '' + # Apply BGP configuration only for VXLAN topologies + if self.topology in (self._VXLAN_MLAG, self._VXLAN): + if service_type == self.__service_types_ELAN: + cl_bgp[s] = self.clC.getElan_bgp(service_uuid, + vlan_id, + vni_id, + self.switches[s]['lo0'], + self.switches[s]['AS']) + else: + cl_bgp[s] = self.clC.getEline_bgp(service_uuid, + vlan_id, + vni_id, + self.switches[s]['lo0'], + self.switches[s]['AS']) + else: + cl_bgp[s] = '' + + if not cls_cp.get(s): + # Apply VLAN configuration to peer MLAG switch, + # only necessary when there are no connection points in the switch + if self.topology in (self._VXLAN_MLAG, self._VLAN_MLAG): + for p in self.switches: + if self.switches[p]['mlagPeerDevice'] == s: + if cls_cp.get(p): + cl_config = str(cl_vlan) + else: + cl_config = str(cl_vlan) + str(cl_bgp[s]) + str(cls_cp[s]) + + cls_perSw[s] = [{'name': cl_name, 'config': cl_config}] + + allLeafConfigured, allLeafModified = self.__updateConnection(cls_perSw) + + conn_info = { + "uuid": service_uuid, + "status": "BUILD", + "service_type": service_type, + "vlan_id": vlan_id, + "connection_points": connection_points, + "configLetPerSwitch": cls_perSw, + 'allLeafConfigured': allLeafConfigured, + 'allLeafModified': allLeafModified} + + return service_uuid, conn_info + except Exception as ex: + self.logger.debug("Exception processing connection {}: {}". + format(service_uuid, str(ex))) + raise ex + + def __updateConnection(self, cls_perSw): + """ Invoked in the creation and modification + + checks if the new connection points config is: + - already in the Cloud Vision, the configLet is modified, and applied to the switch, + executing the corresponding task + - if it has to be removed: + then configuration has to be removed from the switch executing the corresponding task, + before trying to remove the configuration + - created, the configuration set is created, associated to the switch, and the associated + task to the configLet modification executed + In case of any error, rollback is executed, removing the created elements, and restoring to the + previous state. + """ + try: + allLeafConfigured = {} + allLeafModified = {} + + for s in self.switches: + allLeafConfigured[s] = False + allLeafModified[s] = False + cl_toDelete = [] + for s in self.switches: + toDelete_in_cvp = False + if not (cls_perSw.get(s) and cls_perSw[s][0].get('config')): + # when there is no configuration, means that there is no interface + # in the switch to be connected, so the configLet has to be removed from CloudVision + # after removing the ConfigLet fron the switch if it was already there + + # get config let name and key + cl = cls_perSw[s] + try: + cvp_cl = self.client.api.get_configlet_by_name(cl[0]['name']) + # remove configLet + cl_toDelete.append(cvp_cl) + cl[0] = cvp_cl + toDelete_in_cvp = True + except CvpApiError as error: + if "Entity does not exist" in error.msg: + continue + else: + raise error + # remove configLet from device + else: + res = self.__configlet_modify(cls_perSw[s]) + allLeafConfigured[s] = res[0] + if not allLeafConfigured[s]: + continue + cl = cls_perSw[s] + res = self.__device_modify( + device_to_update=s, + new_configlets=cl, + delete=toDelete_in_cvp) + if "errorMessage" in str(res): + raise Exception(str(res)) + self.logger.info("Device {} modify result {}".format(s, res)) + for t_id in res[1]['tasks']: + if not toDelete_in_cvp: + note_msg = "{}{}{}{}##".format(self.__MANAGED_BY_OSM, + self.__SEPARATOR, + t_id, + self.__SEPARATOR) + self.client.api.add_note_to_configlet( + cls_perSw[s][0]['key'], + note_msg) + cls_perSw[s][0]['note'] = note_msg + tasks = { t_id : {'workOrderId': t_id} } + self.__exec_task(tasks, self.__EXC_TASK_EXEC_WAIT) + # with just one configLet assigned to a device, + # delete all if there are errors in next loops + if not toDelete_in_cvp: + allLeafModified[s] = True + if len(cl_toDelete) > 0: + self.__configlet_modify(cl_toDelete, delete=True) + + return allLeafConfigured, allLeafModified + except Exception as ex: + try: + self.__rollbackConnection(cls_perSw, + allLeafConfigured, + allLeafModified) + except Exception as e: + self.logger.error("Exception rolling back in updating connection: {}". + format(e), exc_info=True) + raise ex + + def __rollbackConnection(self, + cls_perSw, + allLeafConfigured, + allLeafModified): + """ Removes the given configLet from the devices and then remove the configLets + """ + for s in self.switches: + if allLeafModified[s]: + try: + res = self.__device_modify( + device_to_update=s, + new_configlets=cls_perSw[s], + delete=True) + if "errorMessage" in str(res): + raise Exception(str(res)) + tasks = dict() + for t_id in res[1]['tasks']: + tasks[t_id] = {'workOrderId': t_id} + self.__exec_task(tasks) + self.logger.info("Device {} modify result {}".format(s, res)) + except Exception as e: + self.logger.error('Error removing configlets from device {}: {}'.format(s, e)) + pass + for s in self.switches: + if allLeafConfigured[s]: + self.__configlet_modify(cls_perSw[s], delete=True) + + def __exec_task(self, tasks, tout=10): + if self.taskC == None: + self.__connect() + data = self.taskC.update_all_tasks(tasks).values() + self.taskC.task_action(data, tout, 'executed') + + def __device_modify(self, device_to_update, new_configlets, delete): + """ Updates the devices (switches) adding or removing the configLet, + the tasks Id's associated to the change are returned + """ + self.logger.info('Enter in __device_modify delete: {}'.format( + delete)) + updated = [] + changed = False + # Task Ids that have been identified during device actions + newTasks = [] + + if (len(new_configlets) == 0 or + device_to_update == None or + len(device_to_update) == 0): + data = {'updated': updated, 'tasks': newTasks} + return [changed, data] + + self.__load_inventory() + + allDeviceFacts = self.allDeviceFacts + # Work through Devices list adding device specific information + device = None + for try_device in allDeviceFacts: + # Add Device Specific Configlets + # self.logger.debug(device) + if try_device['hostname'] not in device_to_update: + continue + dev_cvp_configlets = self.client.api.get_configlets_by_device_id( + try_device['systemMacAddress']) + # self.logger.debug(dev_cvp_configlets) + try_device['deviceSpecificConfiglets'] = [] + for cvp_configlet in dev_cvp_configlets: + if int(cvp_configlet['containerCount']) == 0: + try_device['deviceSpecificConfiglets'].append( + {'name': cvp_configlet['name'], + 'key': cvp_configlet['key']}) + # self.logger.debug(device) + device = try_device + break + + # Check assigned configlets + device_update = False + add_configlets = [] + remove_configlets = [] + update_devices = [] + + if delete: + for cvp_configlet in device['deviceSpecificConfiglets']: + for cl in new_configlets: + if cvp_configlet['name'] == cl['name']: + remove_configlets.append(cvp_configlet) + device_update = True + else: + for configlet in new_configlets: + if configlet not in device['deviceSpecificConfiglets']: + add_configlets.append(configlet) + device_update = True + if device_update: + update_devices.append({'hostname': device['hostname'], + 'configlets': [add_configlets, + remove_configlets], + 'device': device}) + self.logger.info("Device to modify: {}".format(update_devices)) + + up_device = update_devices[0] + cl_toAdd = up_device['configlets'][0] + cl_toDel = up_device['configlets'][1] + # Update Configlets + try: + if delete and len(cl_toDel) > 0: + r = self.client.api.remove_configlets_from_device( + 'OSM', + up_device['device'], + cl_toDel, + create_task=True) + dev_action = r + self.logger.debug("remove_configlets_from_device {} {}".format(dev_action, cl_toDel)) + elif len(cl_toAdd) > 0: + r = self.client.api.apply_configlets_to_device( + 'OSM', + up_device['device'], + cl_toAdd, + create_task=True) + dev_action = r + self.logger.debug("apply_configlets_to_device {} {}".format(dev_action, cl_toAdd)) + + except Exception as error: + errorMessage = str(error) + msg = "errorMessage: Device {} Configlets couldnot be updated: {}".format( + up_device['hostname'], errorMessage) + raise SdnConnectorError(msg) from error + else: + if "errorMessage" in str(dev_action): + m = "Device {} Configlets update fail: {}".format( + up_device['name'], dev_action['errorMessage']) + raise SdnConnectorError(m) + else: + changed = True + if 'taskIds' in str(dev_action): + # Fix 1030 SDN-ARISTA Key error note when deploy a NS + if not dev_action['data']['taskIds']: + raise SdnConnectorError("No taskIds found: Device {} Configlets couldnot be updated".format( + up_device['hostname'])) + for taskId in dev_action['data']['taskIds']: + updated.append({up_device['hostname']: + "Configlets-{}".format( + taskId)}) + newTasks.append(taskId) + else: + updated.append({up_device['hostname']: + "Configlets-No_Specific_Tasks"}) + data = {'updated': updated, 'tasks': newTasks} + return [changed, data] + + def __configlet_modify(self, configletsToApply, delete=False): + ''' adds/update or delete the provided configLets + :param configletsToApply: list of configLets to apply + :param delete: flag to indicate if the configLets have to be deleted + from Cloud Vision Portal + :return: data: dict of module actions and taskIDs + ''' + self.logger.info('Enter in __configlet_modify delete:{}'.format( + delete)) + + # Compare configlets against cvp_facts-configlets + changed = False + checked = [] + deleted = [] + updated = [] + new = [] + + for cl in configletsToApply: + found_in_cvp = False + to_delete = False + to_update = False + to_create = False + to_check = False + try: + cvp_cl = self.client.api.get_configlet_by_name(cl['name']) + cl['key'] = cvp_cl['key'] + cl['note'] = cvp_cl['note'] + found_in_cvp = True + except CvpApiError as error: + if "Entity does not exist" in error.msg: + pass + else: + raise error + + if delete: + if found_in_cvp: + to_delete = True + configlet = {'name': cvp_cl['name'], + 'data': cvp_cl} + else: + if found_in_cvp: + cl_compare = self.__compare(cl['config'], + cvp_cl['config']) + # compare function returns a floating point number + if cl_compare[0] != 100.0: + to_update = True + configlet = {'name': cl['name'], + 'data': cvp_cl, + 'config': cl['config']} + else: + to_check = True + configlet = {'name': cl['name'], + 'key': cvp_cl['key'], + 'data': cvp_cl, + 'config': cl['config']} + else: + to_create = True + configlet = {'name': cl['name'], + 'config': cl['config']} + try: + if to_delete: + operation = 'delete' + resp = self.client.api.delete_configlet( + configlet['data']['name'], + configlet['data']['key']) + elif to_update: + operation = 'update' + resp = self.client.api.update_configlet( + configlet['config'], + configlet['data']['key'], + configlet['data']['name'], + wait_task_ids=True) + elif to_create: + operation = 'create' + resp = self.client.api.add_configlet( + configlet['name'], + configlet['config']) + else: + operation = 'checked' + resp = 'checked' + except Exception as error: + errorMessage = str(error).split(':')[-1] + message = "Configlet {} cannot be {}: {}".format( + cl['name'], operation, errorMessage) + if to_delete: + deleted.append({configlet['name']: message}) + elif to_update: + updated.append({configlet['name']: message}) + elif to_create: + new.append({configlet['name']: message}) + elif to_check: + checked.append({configlet['name']: message}) + + else: + if "error" in str(resp).lower(): + message = "Configlet {} cannot be deleted: {}".format( + cl['name'], resp['errorMessage']) + if to_delete: + deleted.append({configlet['name']: message}) + elif to_update: + updated.append({configlet['name']: message}) + elif to_create: + new.append({configlet['name']: message}) + elif to_check: + checked.append({configlet['name']: message}) + else: + if to_delete: + changed = True + deleted.append({configlet['name']: "success"}) + elif to_update: + changed = True + updated.append({configlet['name']: "success"}) + elif to_create: + changed = True + cl['key'] = resp # This key is used in API call deviceApplyConfigLet FGA + new.append({configlet['name']: "success"}) + elif to_check: + changed = False + checked.append({configlet['name']: "success"}) + + data = {'new': new, 'updated': updated, 'deleted': deleted, 'checked': checked} + return [changed, data] + + def __get_configletsDevices(self, configlets): + for s in self.switches: + configlet = configlets[s] + # Add applied Devices + if len(configlet) > 0: + configlet['devices'] = [] + applied_devices = self.client.api.get_applied_devices( + configlet['name']) + for device in applied_devices['data']: + configlet['devices'].append(device['hostName']) + + def __get_serviceData(self, service_uuid, service_type, vlan_id, conn_info=None): + cls_perSw = {} + for s in self.switches: + cls_perSw[s] = [] + if not conn_info: + srv_cls = self.__get_serviceConfigLets(service_uuid, + service_type, + vlan_id) + self.__get_configletsDevices(srv_cls) + for s in self.switches: + cl = srv_cls[s] + if len(cl) > 0: + for dev in cl['devices']: + cls_perSw[dev].append(cl) + else: + cls_perSw = conn_info['configLetPerSwitch'] + return cls_perSw + + def delete_connectivity_service(self, service_uuid, conn_info=None): + """ + Disconnect multi-site endpoints previously connected + + :param service_uuid: The one returned by create_connectivity_service + :param conn_info: The one returned by last call to 'create_connectivity_service' or 'edit_connectivity_service' + if they do not return None + :return: None + :raises: SdnConnectorException: In case of error. The parameter http_code must be filled + """ + try: + self.logger.debug('invoked delete_connectivity_service {}'. + format(service_uuid)) + if not service_uuid: + raise SdnConnectorError(message='No connection service UUID', + http_code=500) + + self.__get_Connection() + if conn_info == None: + raise SdnConnectorError(message='No connection information for service UUID {}'.format(service_uuid), + http_code=500) + c_info = None + cls_perSw = self.__get_serviceData(service_uuid, + conn_info['service_type'], + conn_info['vlan_id'], + c_info) + allLeafConfigured = {} + allLeafModified = {} + for s in self.switches: + allLeafConfigured[s] = True + allLeafModified[s] = True + found_in_cvp = False + for s in self.switches: + if cls_perSw[s]: + found_in_cvp = True + if found_in_cvp: + self.__rollbackConnection(cls_perSw, + allLeafConfigured, + allLeafModified) + else: + # if the service is not defined in Cloud Vision, return a 404 - NotFound error + raise SdnConnectorError(message='Service {} was not found in Arista Cloud Vision {}'. + format(service_uuid, self.__wim_url), + http_code=404) + self.__removeMetadata(service_uuid) + except CvpLoginError as e: + self.logger.info(str(e)) + self.client = None + raise SdnConnectorError(message=SdnError.UNAUTHORIZED, + http_code=401) from e + except SdnConnectorError as sde: + raise sde + except Exception as ex: + self.client = None + self.logger.error(ex) + if self.raiseException: + raise ex + raise SdnConnectorError(message=SdnError.INTERNAL_ERROR, + http_code=500) from ex + + def __addMetadata(self, service_uuid, service_type, vlan_id): + """ Adds the connectivity service from 'OSM_metadata' configLet + """ + found_in_cvp = False + try: + cvp_cl = self.client.api.get_configlet_by_name(self.__OSM_METADATA) + found_in_cvp = True + except CvpApiError as error: + if "Entity does not exist" in error.msg: + pass + else: + raise error + try: + new_serv = '{} {} {} {}\n'.format(self.__METADATA_PREFIX, service_type, vlan_id, service_uuid) + + if found_in_cvp: + cl_config = cvp_cl['config'] + new_serv + else: + cl_config = new_serv + cl_meta = [{'name': self.__OSM_METADATA, 'config': cl_config}] + self.__configlet_modify(cl_meta) + except Exception as e: + self.logger.error('Error in setting metadata in CloudVision from OSM for service {}: {}'. + format(service_uuid, str(e))) + pass + + def __removeMetadata(self, service_uuid): + """ Removes the connectivity service from 'OSM_metadata' configLet + """ + found_in_cvp = False + try: + cvp_cl = self.client.api.get_configlet_by_name(self.__OSM_METADATA) + found_in_cvp = True + except CvpApiError as error: + if "Entity does not exist" in error.msg: + pass + else: + raise error + try: + if found_in_cvp: + if service_uuid in cvp_cl['config']: + cl_config = '' + for line in cvp_cl['config'].split('\n'): + if service_uuid in line: + continue + else: + cl_config = cl_config + line + cl_meta = [{'name': self.__OSM_METADATA, 'config': cl_config}] + self.__configlet_modify(cl_meta) + except Exception as e: + self.logger.error('Error in removing metadata in CloudVision from OSM for service {}: {}'. + format(service_uuid, str(e))) + pass + + def edit_connectivity_service(self, + service_uuid, + conn_info=None, + connection_points=None, + **kwargs): + """ Change an existing connectivity service. + + This method's arguments and return value follow the same convention as + :meth:`~.create_connectivity_service`. + + :param service_uuid: UUID of the connectivity service. + :param conn_info: (dict or None): Information previously returned + by last call to create_connectivity_service + or edit_connectivity_service + :param connection_points: (list): If provided, the old list of + connection points will be replaced. + :param kwargs: Same meaning that create_connectivity_service + :return: dict or None: Information to be updated and stored at + the database. + When ``None`` is returned, no information should be changed. + When an empty dict is returned, the database record will + be deleted. + **MUST** be JSON/YAML-serializable (plain data structures). + Raises: + SdnConnectorError: In case of error. + """ + try: + self.logger.debug('invoked edit_connectivity_service for service {}. ports: {}'.format(service_uuid, + connection_points)) + + if not service_uuid: + raise SdnConnectorError(message='Unable to perform operation, missing or empty uuid', + http_code=500) + if not conn_info: + raise SdnConnectorError(message='Unable to perform operation, missing or empty connection information', + http_code=500) + + if connection_points == None: + return None + + self.__get_Connection() + + cls_currentPerSw = conn_info['configLetPerSwitch'] + service_type = conn_info['service_type'] + + self.__check_service(service_type, + connection_points, + check_vlan=False, + check_num_cp=False, + kwargs=kwargs) + + s_uid, s_connInf = self.__processConnection( + service_uuid, + service_type, + connection_points, + kwargs) + self.logger.info("Service with uuid {} configuration updated". + format(s_uid)) + return s_connInf + except CvpLoginError as e: + self.logger.info(str(e)) + self.client = None + raise SdnConnectorError(message=SdnError.UNAUTHORIZED, + http_code=401) from e + except SdnConnectorError as sde: + raise sde + except Exception as ex: + try: + # Add previous + # TODO check if there are pending task, and cancel them before restoring + self.__updateConnection(cls_currentPerSw) + except Exception as e: + self.logger.error("Unable to restore configuration in service {} after an error in the configuration updated: {}". + format(service_uuid, str(e))) + if self.raiseException: + raise ex + raise SdnConnectorError(message=str(ex), + http_code=500) from ex + + def clear_all_connectivity_services(self): + """ Removes all connectivity services from Arista CloudVision with two steps: + - retrives all the services from Arista CloudVision + - removes each service + """ + try: + self.logger.debug('invoked AristaImpl ' + + 'clear_all_connectivity_services') + self.__get_Connection() + s_list = self.__get_srvUUIDs() + for serv in s_list: + conn_info = {} + conn_info['service_type'] = serv['type'] + conn_info['vlan_id'] = serv['vlan'] + + self.delete_connectivity_service(serv['uuid'], conn_info) + except CvpLoginError as e: + self.logger.info(str(e)) + self.client = None + raise SdnConnectorError(message=SdnError.UNAUTHORIZED, + http_code=401) from e + except SdnConnectorError as sde: + raise sde + except Exception as ex: + self.client = None + self.logger.error(ex) + if self.raiseException: + raise ex + raise SdnConnectorError(message=SdnError.INTERNAL_ERROR, + http_code=500) from ex + + def get_all_active_connectivity_services(self): + """ Return the uuid of all the active connectivity services with two steps: + - retrives all the services from Arista CloudVision + - retrives the status of each server + """ + try: + self.logger.debug('invoked AristaImpl {}'.format( + 'get_all_active_connectivity_services')) + self.__get_Connection() + s_list = self.__get_srvUUIDs() + result = [] + for serv in s_list: + conn_info = {} + conn_info['service_type'] = serv['type'] + conn_info['vlan_id'] = serv['vlan'] + + status = self.get_connectivity_service_status(serv['uuid'], conn_info) + if status['sdn_status'] == 'ACTIVE': + result.append(serv['uuid']) + return result + except CvpLoginError as e: + self.logger.info(str(e)) + self.client = None + raise SdnConnectorError(message=SdnError.UNAUTHORIZED, + http_code=401) from e + except SdnConnectorError as sde: + raise sde + except Exception as ex: + self.client = None + self.logger.error(ex) + if self.raiseException: + raise ex + raise SdnConnectorError(message=SdnError.INTERNAL_ERROR, + http_code=500) from ex + + def __get_serviceConfigLets(self, service_uuid, service_type, vlan_id): + """ Return the configLet's associated with a connectivity service, + There should be one, as maximum, per device (switch) for a given + connectivity service + """ + srv_cls = {} + for s in self.switches: + srv_cls[s] = [] + found_in_cvp = False + name = (self.__OSM_PREFIX + + s + + self.__SEPARATOR + service_type + str(vlan_id) + + self.__SEPARATOR + service_uuid) + try: + cvp_cl = self.client.api.get_configlet_by_name(name) + found_in_cvp = True + except CvpApiError as error: + if "Entity does not exist" in error.msg: + pass + else: + raise error + if found_in_cvp: + srv_cls[s] = cvp_cl + return srv_cls + + def __get_srvVLANs(self): + """ Returns a list with all the VLAN id's used in the connectivity services managed + in tha Arista CloudVision by checking the 'OSM_metadata' configLet where this + information is stored + """ + found_in_cvp = False + try: + cvp_cl = self.client.api.get_configlet_by_name(self.__OSM_METADATA) + found_in_cvp = True + except CvpApiError as error: + if "Entity does not exist" in error.msg: + pass + else: + raise error + s_vlan_list = [] + if found_in_cvp: + lines = cvp_cl['config'].split('\n') + for line in lines: + if self.__METADATA_PREFIX in line: + s_vlan = line.split(' ')[3] + else: + continue + if (s_vlan is not None and + len(s_vlan) > 0 and + s_vlan not in s_vlan_list): + s_vlan_list.append(s_vlan) + + return s_vlan_list + + def __get_srvUUIDs(self): + """ Retrieves all the connectivity services, managed in tha Arista CloudVision + by checking the 'OSM_metadata' configLet where this information is stored + """ + found_in_cvp = False + try: + cvp_cl = self.client.api.get_configlet_by_name(self.__OSM_METADATA) + found_in_cvp = True + except CvpApiError as error: + if "Entity does not exist" in error.msg: + pass + else: + raise error + serv_list = [] + if found_in_cvp: + lines = cvp_cl['config'].split('\n') + for line in lines: + if self.__METADATA_PREFIX in line: + line = line.split(' ') + serv = {'uuid': line[4], 'type': line[2], 'vlan': line[3]} + else: + continue + if (serv is not None and + len(serv) > 0 and + serv not in serv_list): + serv_list.append(serv) + + return serv_list + + def __get_Connection(self): + """ Open a connection with Arista CloudVision, + invoking the version retrival as test + """ + try: + if self.client == None: + self.client = self.__connect() + self.client.api.get_cvp_info() + except (CvpSessionLogOutError, RequestException) as e: + self.logger.debug("Connection error '{}'. Reconnecting".format(e)) + self.client = self.__connect() + self.client.api.get_cvp_info() + + def __connect(self): + ''' Connects to CVP device using user provided credentials from initialization. + :return: CvpClient object with connection instantiated. + ''' + client = CvpClient() + protocol, _, rest_url = self.__wim_url.rpartition("://") + host, _, port = rest_url.partition(":") + if port and port.endswith("/"): + port = int(port[:-1]) + elif port: + port = int(port) + else: + port = 443 + + client.connect([host], + self.__user, + self.__passwd, + protocol=protocol or "https", + port=port, + connect_timeout=2) + client.api = CvpApi(client, request_timeout=self.__API_REQUEST_TOUT) + self.taskC = AristaCVPTask(client.api) + return client + + def __compare(self, fromText, toText, lines=10): + """ Compare text string in 'fromText' with 'toText' and produce + diffRatio - a score as a float in the range [0, 1] 2.0*M / T + T is the total number of elements in both sequences, + M is the number of matches. + Score - 1.0 if the sequences are identical, and + 0.0 if they have nothing in common. + unified diff list + Code Meaning + '- ' line unique to sequence 1 + '+ ' line unique to sequence 2 + ' ' line common to both sequences + '? ' line not present in either input sequence + """ + fromlines = fromText.splitlines(1) + tolines = toText.splitlines(1) + diff = list(difflib.unified_diff(fromlines, tolines, n=lines)) + textComp = difflib.SequenceMatcher(None, fromText, toText) + diffRatio = round(textComp.quick_ratio()*100, 2) + return [diffRatio, diff] + + def __load_inventory(self): + """ Get Inventory Data for All Devices (aka switches) from the Arista CloudVision + """ + if not self.cvp_inventory: + self.cvp_inventory = self.client.api.get_inventory() + self.allDeviceFacts = [] + for device in self.cvp_inventory: + self.allDeviceFacts.append(device) + + def __get_tags(self, name, value): + if not self.cvp_tags: + self.cvp_tags = [] + url = '/api/v1/rest/analytics/tags/labels/devices/{}/value/{}/elements'.format(name, value) + self.logger.debug('get_tags: URL {}'.format(url)) + data = self.client.get(url, timeout=self.__API_REQUEST_TOUT) + for dev in data['notifications']: + for elem in dev['updates']: + self.cvp_tags.append(elem) + self.logger.debug('Available devices with tag_name {} - value {}: {} '.format(name, value, self.cvp_tags)) + + def __get_interface_ip(self, device_id, interface): + url = '/api/v1/rest/{}/Sysdb/ip/config/ipIntfConfig/{}/'.format(device_id, interface) + self.logger.debug('get_interface_ip: URL {}'.format(url)) + try: + data = self.client.get(url, timeout=self.__API_REQUEST_TOUT) + return data['notifications'][0]['updates']['addrWithMask']['value'].split('/')[0] + except Exception: + raise SdnConnectorError("Invalid response from url {}: data {}".format(url, data)) + + def __get_device_ASN(self, device_id): + url = '/api/v1/rest/{}/Sysdb/routing/bgp/config/'.format(device_id) + self.logger.debug('get_device_ASN: URL {}'.format(url)) + try: + data = self.client.get(url, timeout=self.__API_REQUEST_TOUT) + return data['notifications'][0]['updates']['asNumber']['value']['value']['int'] + except Exception: + raise SdnConnectorError("Invalid response from url {}: data {}".format(url, data)) + + def __get_peer_MLAG(self, device_id): + peer = None + url = '/api/v1/rest/{}/Sysdb/mlag/status/'.format(device_id) + self.logger.debug('get_MLAG_status: URL {}'.format(url)) + try: + data = self.client.get(url, timeout=self.__API_REQUEST_TOUT) + if data['notifications']: + found = False + for notification in data['notifications']: + for update in notification['updates']: + if update == 'systemId': + mlagSystemId = notification['updates'][update]['value'] + found = True + break + if found: + break + # search the MLAG System Id + if found: + for s in self.switches: + if self.switches[s]['serialNumber'] == device_id: + continue + url = '/api/v1/rest/{}/Sysdb/mlag/status/'.format(self.switches[s]['serialNumber']) + self.logger.debug('Searching for MLAG system id {} in switch {}'.format(mlagSystemId, s)) + data = self.client.get(url, timeout=self.__API_REQUEST_TOUT) + found = False + for notification in data['notifications']: + for update in notification['updates']: + if update == 'systemId': + if mlagSystemId == notification['updates'][update]['value']: + peer = s + found = True + break + if found: + break + if found: + break + if peer == None: + self.logger.error('No Peer device found for device {} with MLAG address {}'.format(device_id, mlagSystemId)) + else: + self.logger.debug('Peer MLAG for device {} - value {}'.format(device_id, peer)) + return peer + except Exception: + raise SdnConnectorError("Invalid response from url {}: data {}".format(url, data)) + + def is_valid_destination(self, url): + """ Check that the provided WIM URL is correct + """ + if re.match(self.__regex, url): + return True + elif self.is_valid_ipv4_address(url): + return True + else: + return self.is_valid_ipv6_address(url) + + def is_valid_ipv4_address(self, address): + """ Checks that the given IP is IPv4 valid + """ + try: + socket.inet_pton(socket.AF_INET, address) + except AttributeError: # no inet_pton here, sorry + try: + socket.inet_aton(address) + except socket.error: + return False + return address.count('.') == 3 + except socket.error: # not a valid address + return False + return True + + def is_valid_ipv6_address(self, address): + """ Checks that the given IP is IPv6 valid + """ + try: + socket.inet_pton(socket.AF_INET6, address) + except socket.error: # not a valid address + return False + return True + + def delete_keys_from_dict(self, dict_del, lst_keys): + if dict_del == 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 diff --git a/RO-SDN-arista_cloudvision/requirements.txt b/RO-SDN-arista_cloudvision/requirements.txt new file mode 100644 index 00000000..cd1edfe0 --- /dev/null +++ b/RO-SDN-arista_cloudvision/requirements.txt @@ -0,0 +1,20 @@ +## +# 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. +## + +requests +uuid +cvprac +git+https://osm.etsi.org/gerrit/osm/RO.git#egg=osm-ro + diff --git a/RO-SDN-arista_cloudvision/setup.py b/RO-SDN-arista_cloudvision/setup.py new file mode 100644 index 00000000..ab8385bc --- /dev/null +++ b/RO-SDN-arista_cloudvision/setup.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +## +# 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. +## + +from setuptools import setup + +_name = "osm_rosdn_arista_cloudvision" + +README = """ +=========== +osm-rosdn_arista_cloudvision +=========== + +osm-ro pluging for arista_cloudvision SDN +""" + +setup( + name=_name, + description='OSM ro sdn plugin for arista with CloudVision', + long_description=README, + version_command=('git describe --match v* --tags --long --dirty', 'pep440-git-full'), + # version=VERSION, + # python_requires='>3.5.0', + author='ETSI OSM', + # TODO py3 author_email='', + maintainer='oscarluis.peral@atos.net', # TODO py3 + # TODO py3 maintainer_email='', + url='https://osm.etsi.org/gitweb/?p=osm/RO.git;a=summary', + license='Apache 2.0', + + packages=[_name], + include_package_data=True, + install_requires=["requests", + "uuid", + "cvprac", + "osm-ro @ git+https://osm.etsi.org/gerrit/osm/RO.git#egg=osm-ro&subdirectory=RO"], + setup_requires=['setuptools-version-command'], + entry_points={ + 'osm_rosdn.plugins': ['rosdn_arista_cloudvision = osm_rosdn_arista_cloudvision.' + 'wimconn_arista:AristaSdnConnector'] + }, +) diff --git a/RO-SDN-arista_cloudvision/stdeb.cfg b/RO-SDN-arista_cloudvision/stdeb.cfg new file mode 100644 index 00000000..0c718e4f --- /dev/null +++ b/RO-SDN-arista_cloudvision/stdeb.cfg @@ -0,0 +1,19 @@ +# +# 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. +# + +[DEFAULT] +X-Python3-Version : >= 3.5 +Depends3: python3-requests, python3-osm-ro + diff --git a/RO-SDN-arista_cloudvision/tox.ini b/RO-SDN-arista_cloudvision/tox.ini new file mode 100644 index 00000000..d534123c --- /dev/null +++ b/RO-SDN-arista_cloudvision/tox.ini @@ -0,0 +1,41 @@ +## +# 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. +## + +[tox] +envlist = flake8 +toxworkdir={toxinidir}/.tox + +[testenv] +basepython = python3 +install_command = python3 -m pip install -r requirements.txt -U {opts} {packages} +# deps = -r{toxinidir}/test-requirements.txt +# commands=python3 -m unittest discover -v + +[testenv:flake8] +basepython = python3 +deps = flake8 +commands = flake8 osm_rosdn_arista_cloudvision --max-line-length 120 \ + --exclude .svn,CVS,.gz,.git,__pycache__,.tox,local,temp --ignore W291,W293,E226,W504 + +[testenv:unittest] +basepython = python3 +commands = python3 -m unittest osm_rosdn_arista_cloudvision.tests + +[testenv:build] +basepython = python3 +deps = stdeb + setuptools-version-command +commands = python3 setup.py --command-packages=stdeb.command bdist_deb + diff --git a/RO-SDN-ietfl2vpn/Makefile b/RO-SDN-ietfl2vpn/Makefile new file mode 100644 index 00000000..c88b0740 --- /dev/null +++ b/RO-SDN-ietfl2vpn/Makefile @@ -0,0 +1,26 @@ +## +# 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. +## + +all: clean package + +clean: + rm -rf dist deb_dist osm_rosdn_ietfl2vpn-*.tar.gz osm_rosdn_ietfl2vpn.egg-info .eggs + rm -rf osm_rosdn_tapi-*.tar.gz osm_rosdn_tapi.egg-info + +package: + python3 setup.py --command-packages=stdeb.command sdist_dsc + cd deb_dist/osm-rosdn-ietfl2vpn*/ && dpkg-buildpackage -rfakeroot -uc -us + for pkg in deb_dist/python3-osm-rosdn-ietfl2vpn*.deb; do cp $$pkg deb_dist/python3-osm-rosdn-tapi$${pkg#*-osm-rosdn-ietfl2vpn} ; done + diff --git a/RO-SDN-ietfl2vpn/osm_rosdn_ietfl2vpn/wimconn_ietfl2vpn.py b/RO-SDN-ietfl2vpn/osm_rosdn_ietfl2vpn/wimconn_ietfl2vpn.py new file mode 100644 index 00000000..26680b5a --- /dev/null +++ b/RO-SDN-ietfl2vpn/osm_rosdn_ietfl2vpn/wimconn_ietfl2vpn.py @@ -0,0 +1,362 @@ +# -*- coding: utf-8 -*- +## +# Copyright 2018 Telefonica +# All Rights Reserved. +# +# Contributors: Oscar Gonzalez de Dios, Manuel Lopez Bravo, Guillermo Pajares Martin +# 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. +# +# This work has been performed in the context of the Metro-Haul project - +# funded by the European Commission under Grant number 761727 through the +# Horizon 2020 program. +## +"""The SDN/WIM connector is responsible for establishing wide area network +connectivity. + +This SDN/WIM connector implements the standard IETF RFC 8466 "A YANG Data + Model for Layer 2 Virtual Private Network (L2VPN) Service Delivery" + +It receives the endpoints and the necessary details to request +the Layer 2 service. +""" +import requests +import uuid +import logging +from osm_ro.wim.sdnconn import SdnConnectorBase, SdnConnectorError +"""CHeck layer where we move it""" + + +class WimconnectorIETFL2VPN(SdnConnectorBase): + + def __init__(self, wim, wim_account, config=None, logger=None): + """IETF L2VPM WIM connector + + Arguments: (To be completed) + wim (dict): WIM record, as stored in the database + wim_account (dict): WIM account record, as stored in the database + """ + self.logger = logging.getLogger('openmano.sdnconn.ietfl2vpn') + super().__init__(wim, wim_account, config, logger) + self.headers = {'Content-Type': 'application/json'} + self.mappings = {m['service_endpoint_id']: m + for m in self.service_endpoint_mapping} + self.user = wim_account.get("user") + self.passwd = wim_account.get("passwordd") + if self.user and self.passwd is not None: + self.auth = (self.user, self.passwd) + else: + self.auth = None + self.logger.info("IETFL2VPN Connector Initialized.") + + def check_credentials(self): + endpoint = "{}/restconf/data/ietf-l2vpn-svc:l2vpn-svc/vpn-services".format(self.wim["wim_url"]) + try: + response = requests.get(endpoint, auth=self.auth) + http_code = response.status_code + except requests.exceptions.RequestException as e: + raise SdnConnectorError(e.message, http_code=503) + + if http_code != 200: + raise SdnConnectorError("Failed while authenticating", http_code=http_code) + self.logger.info("Credentials checked") + + def get_connectivity_service_status(self, service_uuid, conn_info=None): + """Monitor the status of the connectivity service stablished + + Arguments: + service_uuid: Connectivity service unique identifier + + Returns: + Examples:: + {'sdn_status': 'ACTIVE'} + {'sdn_status': 'INACTIVE'} + {'sdn_status': 'DOWN'} + {'sdn_status': 'ERROR'} + """ + try: + self.logger.info("Sending get connectivity service stuatus") + servicepoint = "{}/restconf/data/ietf-l2vpn-svc:l2vpn-svc/vpn-services/vpn-service={}/".format( + self.wim["wim_url"], service_uuid) + response = requests.get(servicepoint, auth=self.auth) + if response.status_code != requests.codes.ok: + raise SdnConnectorError("Unable to obtain connectivity servcice status", http_code=response.status_code) + service_status = {'sdn_status': 'ACTIVE'} + return service_status + except requests.exceptions.ConnectionError: + raise SdnConnectorError("Request Timeout", http_code=408) + + def search_mapp(self, connection_point): + id = connection_point['service_endpoint_id'] + if id not in self.mappings: + raise SdnConnectorError("Endpoint {} not located".format(str(id))) + else: + return self.mappings[id] + + def create_connectivity_service(self, service_type, connection_points, **kwargs): + """Stablish WAN connectivity between the endpoints + + Arguments: + service_type (str): ``ELINE`` (L2), ``ELAN`` (L2), ``ETREE`` (L2), + ``L3``. + connection_points (list): each point corresponds to + an entry point from the DC to the transport network. One + connection point serves to identify the specific access and + some other service parameters, such as encapsulation type. + Represented by a dict as follows:: + + { + "service_endpoint_id": ..., (str[uuid]) + "service_endpoint_encapsulation_type": ..., + (enum: none, dot1q, ...) + "service_endpoint_encapsulation_info": { + ... (dict) + "vlan": ..., (int, present if encapsulation is dot1q) + "vni": ... (int, present if encapsulation is vxlan), + "peers": [(ipv4_1), (ipv4_2)] + (present if encapsulation is vxlan) + } + } + + The service endpoint ID should be previously informed to the WIM + engine in the RO when the WIM port mapping is registered. + + Keyword Arguments: + bandwidth (int): value in kilobytes + latency (int): value in milliseconds + + Other QoS might be passed as keyword arguments. + + Returns: + tuple: ``(service_id, conn_info)`` containing: + - *service_uuid* (str): UUID of the established connectivity + service + - *conn_info* (dict or None): Information to be stored at the + database (or ``None``). This information will be provided to + the :meth:`~.edit_connectivity_service` and :obj:`~.delete`. + **MUST** be JSON/YAML-serializable (plain data structures). + + Raises: + SdnConnectorException: In case of error. + """ + if service_type == "ELINE": + if len(connection_points) > 2: + raise SdnConnectorError('Connections between more than 2 endpoints are not supported') + if len(connection_points) < 2: + raise SdnConnectorError('Connections must be of at least 2 endpoints') + """ First step, create the vpn service """ + uuid_l2vpn = str(uuid.uuid4()) + vpn_service = {} + vpn_service["vpn-id"] = uuid_l2vpn + vpn_service["vpn-scv-type"] = "vpws" + vpn_service["svc-topo"] = "any-to-any" + vpn_service["customer-name"] = "osm" + vpn_service_list = [] + vpn_service_list.append(vpn_service) + vpn_service_l = {"ietf-l2vpn-svc:vpn-service": vpn_service_list} + response_service_creation = None + conn_info = [] + self.logger.info("Sending vpn-service :{}".format(vpn_service_l)) + try: + endpoint_service_creation = "{}/restconf/data/ietf-l2vpn-svc:l2vpn-svc/vpn-services".format( + self.wim["wim_url"]) + response_service_creation = requests.post(endpoint_service_creation, headers=self.headers, + json=vpn_service_l, auth=self.auth) + except requests.exceptions.ConnectionError: + raise SdnConnectorError("Request to create service Timeout", http_code=408) + if response_service_creation.status_code == 409: + raise SdnConnectorError("Service already exists", http_code=response_service_creation.status_code) + elif response_service_creation.status_code != requests.codes.created: + raise SdnConnectorError("Request to create service not accepted", + http_code=response_service_creation.status_code) + """ Second step, create the connections and vpn attachments """ + for connection_point in connection_points: + connection_point_wan_info = self.search_mapp(connection_point) + site_network_access = {} + connection = {} + if connection_point["service_endpoint_encapsulation_type"] != "none": + if connection_point["service_endpoint_encapsulation_type"] == "dot1q": + """ The connection is a VLAN """ + connection["encapsulation-type"] = "dot1q-vlan-tagged" + tagged = {} + tagged_interf = {} + service_endpoint_encapsulation_info = connection_point["service_endpoint_encapsulation_info"] + if service_endpoint_encapsulation_info["vlan"] is None: + raise SdnConnectorError("VLAN must be provided") + tagged_interf["cvlan-id"] = service_endpoint_encapsulation_info["vlan"] + tagged["dot1q-vlan-tagged"] = tagged_interf + connection["tagged-interface"] = tagged + else: + raise NotImplementedError("Encapsulation type not implemented") + site_network_access["connection"] = connection + self.logger.info("Sending connection:{}".format(connection)) + vpn_attach = {} + vpn_attach["vpn-id"] = uuid_l2vpn + vpn_attach["site-role"] = vpn_service["svc-topo"]+"-role" + site_network_access["vpn-attachment"] = vpn_attach + self.logger.info("Sending vpn-attachement :{}".format(vpn_attach)) + uuid_sna = str(uuid.uuid4()) + site_network_access["network-access-id"] = uuid_sna + site_network_access["bearer"] = connection_point_wan_info["service_mapping_info"]["bearer"] + site_network_accesses = {} + site_network_access_list = [] + site_network_access_list.append(site_network_access) + site_network_accesses["ietf-l2vpn-svc:site-network-access"] = site_network_access_list + conn_info_d = {} + conn_info_d["site"] = connection_point_wan_info["service_mapping_info"]["site-id"] + conn_info_d["site-network-access-id"] = site_network_access["network-access-id"] + conn_info_d["mapping"] = None + conn_info.append(conn_info_d) + try: + endpoint_site_network_access_creation = \ + "{}/restconf/data/ietf-l2vpn-svc:l2vpn-svc/sites/site={}/site-network-accesses/".format( + self.wim["wim_url"], connection_point_wan_info["service_mapping_info"]["site-id"]) + response_endpoint_site_network_access_creation = requests.post( + endpoint_site_network_access_creation, + headers=self.headers, + json=site_network_accesses, + auth=self.auth) + + if response_endpoint_site_network_access_creation.status_code == 409: + self.delete_connectivity_service(vpn_service["vpn-id"]) + raise SdnConnectorError("Site_Network_Access with ID '{}' already exists".format( + site_network_access["network-access-id"]), + http_code=response_endpoint_site_network_access_creation.status_code) + + elif response_endpoint_site_network_access_creation.status_code == 400: + self.delete_connectivity_service(vpn_service["vpn-id"]) + raise SdnConnectorError("Site {} does not exist".format( + connection_point_wan_info["service_mapping_info"]["site-id"]), + http_code=response_endpoint_site_network_access_creation.status_code) + + elif response_endpoint_site_network_access_creation.status_code != requests.codes.created and \ + response_endpoint_site_network_access_creation.status_code != requests.codes.no_content: + self.delete_connectivity_service(vpn_service["vpn-id"]) + raise SdnConnectorError("Request no accepted", + http_code=response_endpoint_site_network_access_creation.status_code) + + except requests.exceptions.ConnectionError: + self.delete_connectivity_service(vpn_service["vpn-id"]) + raise SdnConnectorError("Request Timeout", http_code=408) + return uuid_l2vpn, conn_info + + else: + raise NotImplementedError + + def delete_connectivity_service(self, service_uuid, conn_info=None): + """Disconnect multi-site endpoints previously connected + + This method should receive as the first argument the UUID generated by + the ``create_connectivity_service`` + """ + try: + self.logger.info("Sending delete") + servicepoint = "{}/restconf/data/ietf-l2vpn-svc:l2vpn-svc/vpn-services/vpn-service={}/".format( + self.wim["wim_url"], service_uuid) + response = requests.delete(servicepoint, auth=self.auth) + if response.status_code != requests.codes.no_content: + raise SdnConnectorError("Error in the request", http_code=response.status_code) + except requests.exceptions.ConnectionError: + raise SdnConnectorError("Request Timeout", http_code=408) + + def edit_connectivity_service(self, service_uuid, conn_info=None, + connection_points=None, **kwargs): + """Change an existing connectivity service, see + ``create_connectivity_service``""" + + # sites = {"sites": {}} + # site_list = [] + vpn_service = {} + vpn_service["svc-topo"] = "any-to-any" + counter = 0 + for connection_point in connection_points: + site_network_access = {} + connection_point_wan_info = self.search_mapp(connection_point) + params_site = {} + params_site["site-id"] = connection_point_wan_info["service_mapping_info"]["site-id"] + params_site["site-vpn-flavor"] = "site-vpn-flavor-single" + device_site = {} + device_site["device-id"] = connection_point_wan_info["device-id"] + params_site["devices"] = device_site + # network_access = {} + connection = {} + if connection_point["service_endpoint_encapsulation_type"] != "none": + if connection_point["service_endpoint_encapsulation_type"] == "dot1q": + """ The connection is a VLAN """ + connection["encapsulation-type"] = "dot1q-vlan-tagged" + tagged = {} + tagged_interf = {} + service_endpoint_encapsulation_info = connection_point["service_endpoint_encapsulation_info"] + if service_endpoint_encapsulation_info["vlan"] is None: + raise SdnConnectorError("VLAN must be provided") + tagged_interf["cvlan-id"] = service_endpoint_encapsulation_info["vlan"] + tagged["dot1q-vlan-tagged"] = tagged_interf + connection["tagged-interface"] = tagged + else: + raise NotImplementedError("Encapsulation type not implemented") + site_network_access["connection"] = connection + vpn_attach = {} + vpn_attach["vpn-id"] = service_uuid + vpn_attach["site-role"] = vpn_service["svc-topo"]+"-role" + site_network_access["vpn-attachment"] = vpn_attach + uuid_sna = conn_info[counter]["site-network-access-id"] + site_network_access["network-access-id"] = uuid_sna + site_network_access["bearer"] = connection_point_wan_info["service_mapping_info"]["bearer"] + site_network_accesses = {} + site_network_access_list = [] + site_network_access_list.append(site_network_access) + site_network_accesses["ietf-l2vpn-svc:site-network-access"] = site_network_access_list + try: + endpoint_site_network_access_edit = \ + "{}/restconf/data/ietf-l2vpn-svc:l2vpn-svc/sites/site={}/site-network-accesses/".format( + self.wim["wim_url"], connection_point_wan_info["service_mapping_info"]["site-id"]) + response_endpoint_site_network_access_creation = requests.put(endpoint_site_network_access_edit, + headers=self.headers, + json=site_network_accesses, + auth=self.auth) + if response_endpoint_site_network_access_creation.status_code == 400: + raise SdnConnectorError("Service does not exist", + http_code=response_endpoint_site_network_access_creation.status_code) + elif response_endpoint_site_network_access_creation.status_code != 201 and \ + response_endpoint_site_network_access_creation.status_code != 204: + raise SdnConnectorError("Request no accepted", + http_code=response_endpoint_site_network_access_creation.status_code) + except requests.exceptions.ConnectionError: + raise SdnConnectorError("Request Timeout", http_code=408) + counter += 1 + return None + + def clear_all_connectivity_services(self): + """Delete all WAN Links corresponding to a WIM""" + try: + self.logger.info("Sending clear all connectivity services") + servicepoint = "{}/restconf/data/ietf-l2vpn-svc:l2vpn-svc/vpn-services".format(self.wim["wim_url"]) + response = requests.delete(servicepoint, auth=self.auth) + if response.status_code != requests.codes.no_content: + raise SdnConnectorError("Unable to clear all connectivity services", http_code=response.status_code) + except requests.exceptions.ConnectionError: + raise SdnConnectorError("Request Timeout", http_code=408) + + def get_all_active_connectivity_services(self): + """Provide information about all active connections provisioned by a + WIM + """ + try: + self.logger.info("Sending get all connectivity services") + servicepoint = "{}/restconf/data/ietf-l2vpn-svc:l2vpn-svc/vpn-services".format(self.wim["wim_url"]) + response = requests.get(servicepoint, auth=self.auth) + if response.status_code != requests.codes.ok: + raise SdnConnectorError("Unable to get all connectivity services", http_code=response.status_code) + return response + except requests.exceptions.ConnectionError: + raise SdnConnectorError("Request Timeout", http_code=408) diff --git a/RO-SDN-ietfl2vpn/requirements.txt b/RO-SDN-ietfl2vpn/requirements.txt new file mode 100644 index 00000000..a6f6d655 --- /dev/null +++ b/RO-SDN-ietfl2vpn/requirements.txt @@ -0,0 +1,18 @@ +## +# 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. +## + +requests +git+https://osm.etsi.org/gerrit/osm/RO.git#egg=osm-ro&subdirectory=RO + diff --git a/RO-SDN-ietfl2vpn/setup.py b/RO-SDN-ietfl2vpn/setup.py new file mode 100644 index 00000000..9db91ef9 --- /dev/null +++ b/RO-SDN-ietfl2vpn/setup.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +## +# 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. +## + +from setuptools import setup + +_name = "osm_rosdn_ietfl2vpn" + +README = """ +=========== +osm-rosdn_ietfl2vpn +=========== + +osm-ro pluging for ietfl2vpn SDN +""" + +setup( + name=_name, + description='OSM ro sdn plugin for ietfl2vpn', + long_description=README, + version_command=('git describe --match v* --tags --long --dirty', 'pep440-git-full'), + # version=VERSION, + # python_requires='>3.5.0', + author='ETSI OSM', + # TODO py3 author_email='', + maintainer='OSM_TECH@LIST.ETSI.ORG', # TODO py3 + # TODO py3 maintainer_email='', + url='https://osm.etsi.org/gitweb/?p=osm/RO.git;a=summary', + license='Apache 2.0', + + packages=[_name], + include_package_data=True, + dependency_links=["git+https://osm.etsi.org/gerrit/osm/RO.git#egg=osm-ro"], + install_requires=[ + "requests", + "osm-ro @ git+https://osm.etsi.org/gerrit/osm/RO.git#egg=osm-ro&subdirectory=RO" + ], + setup_requires=['setuptools-version-command'], + entry_points={ + 'osm_rosdn.plugins': ['rosdn_ietfl2vpn = osm_rosdn_ietfl2vpn.wimconn_ietfl2vpn:WimconnectorIETFL2VPN'], + }, +) diff --git a/RO-SDN-ietfl2vpn/stdeb.cfg b/RO-SDN-ietfl2vpn/stdeb.cfg new file mode 100644 index 00000000..0c718e4f --- /dev/null +++ b/RO-SDN-ietfl2vpn/stdeb.cfg @@ -0,0 +1,19 @@ +# +# 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. +# + +[DEFAULT] +X-Python3-Version : >= 3.5 +Depends3: python3-requests, python3-osm-ro + diff --git a/RO-SDN-ietfl2vpn/tox.ini b/RO-SDN-ietfl2vpn/tox.ini new file mode 100644 index 00000000..040210ce --- /dev/null +++ b/RO-SDN-ietfl2vpn/tox.ini @@ -0,0 +1,41 @@ +## +# 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. +## + +[tox] +envlist = py3 +toxworkdir={homedir}/.tox + +[testenv] +basepython = python3 +install_command = python3 -m pip install -r requirements.txt -U {opts} {packages} +# deps = -r{toxinidir}/test-requirements.txt +commands=python3 -m unittest discover -v + +[testenv:flake8] +basepython = python3 +deps = flake8 +commands = flake8 osm_rosdn_ietfl2vpn --max-line-length 120 \ + --exclude .svn,CVS,.gz,.git,__pycache__,.tox,local,temp --ignore W291,W293,E226,W504 + +[testenv:unittest] +basepython = python3 +commands = python3 -m unittest osm_rosdn_ietfl2vpn.tests + +[testenv:build] +basepython = python3 +deps = stdeb + setuptools-version-command +commands = python3 setup.py --command-packages=stdeb.command bdist_deb + diff --git a/RO-SDN-tapi/Makefile b/RO-SDN-tapi/Makefile deleted file mode 100644 index 2e052802..00000000 --- a/RO-SDN-tapi/Makefile +++ /dev/null @@ -1,24 +0,0 @@ -## -# 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. -## - -all: clean package - -clean: - rm -rf dist deb_dist osm_rosdn_tapi-*.tar.gz osm_rosdn_tapi.egg-info .eggs - -package: - python3 setup.py --command-packages=stdeb.command sdist_dsc - cd deb_dist/osm-rosdn-tapi*/ && dpkg-buildpackage -rfakeroot -uc -us - diff --git a/RO-SDN-tapi/osm_rosdn_tapi/wimconn_ietfl2vpn.py b/RO-SDN-tapi/osm_rosdn_tapi/wimconn_ietfl2vpn.py deleted file mode 100644 index 26680b5a..00000000 --- a/RO-SDN-tapi/osm_rosdn_tapi/wimconn_ietfl2vpn.py +++ /dev/null @@ -1,362 +0,0 @@ -# -*- coding: utf-8 -*- -## -# Copyright 2018 Telefonica -# All Rights Reserved. -# -# Contributors: Oscar Gonzalez de Dios, Manuel Lopez Bravo, Guillermo Pajares Martin -# 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. -# -# This work has been performed in the context of the Metro-Haul project - -# funded by the European Commission under Grant number 761727 through the -# Horizon 2020 program. -## -"""The SDN/WIM connector is responsible for establishing wide area network -connectivity. - -This SDN/WIM connector implements the standard IETF RFC 8466 "A YANG Data - Model for Layer 2 Virtual Private Network (L2VPN) Service Delivery" - -It receives the endpoints and the necessary details to request -the Layer 2 service. -""" -import requests -import uuid -import logging -from osm_ro.wim.sdnconn import SdnConnectorBase, SdnConnectorError -"""CHeck layer where we move it""" - - -class WimconnectorIETFL2VPN(SdnConnectorBase): - - def __init__(self, wim, wim_account, config=None, logger=None): - """IETF L2VPM WIM connector - - Arguments: (To be completed) - wim (dict): WIM record, as stored in the database - wim_account (dict): WIM account record, as stored in the database - """ - self.logger = logging.getLogger('openmano.sdnconn.ietfl2vpn') - super().__init__(wim, wim_account, config, logger) - self.headers = {'Content-Type': 'application/json'} - self.mappings = {m['service_endpoint_id']: m - for m in self.service_endpoint_mapping} - self.user = wim_account.get("user") - self.passwd = wim_account.get("passwordd") - if self.user and self.passwd is not None: - self.auth = (self.user, self.passwd) - else: - self.auth = None - self.logger.info("IETFL2VPN Connector Initialized.") - - def check_credentials(self): - endpoint = "{}/restconf/data/ietf-l2vpn-svc:l2vpn-svc/vpn-services".format(self.wim["wim_url"]) - try: - response = requests.get(endpoint, auth=self.auth) - http_code = response.status_code - except requests.exceptions.RequestException as e: - raise SdnConnectorError(e.message, http_code=503) - - if http_code != 200: - raise SdnConnectorError("Failed while authenticating", http_code=http_code) - self.logger.info("Credentials checked") - - def get_connectivity_service_status(self, service_uuid, conn_info=None): - """Monitor the status of the connectivity service stablished - - Arguments: - service_uuid: Connectivity service unique identifier - - Returns: - Examples:: - {'sdn_status': 'ACTIVE'} - {'sdn_status': 'INACTIVE'} - {'sdn_status': 'DOWN'} - {'sdn_status': 'ERROR'} - """ - try: - self.logger.info("Sending get connectivity service stuatus") - servicepoint = "{}/restconf/data/ietf-l2vpn-svc:l2vpn-svc/vpn-services/vpn-service={}/".format( - self.wim["wim_url"], service_uuid) - response = requests.get(servicepoint, auth=self.auth) - if response.status_code != requests.codes.ok: - raise SdnConnectorError("Unable to obtain connectivity servcice status", http_code=response.status_code) - service_status = {'sdn_status': 'ACTIVE'} - return service_status - except requests.exceptions.ConnectionError: - raise SdnConnectorError("Request Timeout", http_code=408) - - def search_mapp(self, connection_point): - id = connection_point['service_endpoint_id'] - if id not in self.mappings: - raise SdnConnectorError("Endpoint {} not located".format(str(id))) - else: - return self.mappings[id] - - def create_connectivity_service(self, service_type, connection_points, **kwargs): - """Stablish WAN connectivity between the endpoints - - Arguments: - service_type (str): ``ELINE`` (L2), ``ELAN`` (L2), ``ETREE`` (L2), - ``L3``. - connection_points (list): each point corresponds to - an entry point from the DC to the transport network. One - connection point serves to identify the specific access and - some other service parameters, such as encapsulation type. - Represented by a dict as follows:: - - { - "service_endpoint_id": ..., (str[uuid]) - "service_endpoint_encapsulation_type": ..., - (enum: none, dot1q, ...) - "service_endpoint_encapsulation_info": { - ... (dict) - "vlan": ..., (int, present if encapsulation is dot1q) - "vni": ... (int, present if encapsulation is vxlan), - "peers": [(ipv4_1), (ipv4_2)] - (present if encapsulation is vxlan) - } - } - - The service endpoint ID should be previously informed to the WIM - engine in the RO when the WIM port mapping is registered. - - Keyword Arguments: - bandwidth (int): value in kilobytes - latency (int): value in milliseconds - - Other QoS might be passed as keyword arguments. - - Returns: - tuple: ``(service_id, conn_info)`` containing: - - *service_uuid* (str): UUID of the established connectivity - service - - *conn_info* (dict or None): Information to be stored at the - database (or ``None``). This information will be provided to - the :meth:`~.edit_connectivity_service` and :obj:`~.delete`. - **MUST** be JSON/YAML-serializable (plain data structures). - - Raises: - SdnConnectorException: In case of error. - """ - if service_type == "ELINE": - if len(connection_points) > 2: - raise SdnConnectorError('Connections between more than 2 endpoints are not supported') - if len(connection_points) < 2: - raise SdnConnectorError('Connections must be of at least 2 endpoints') - """ First step, create the vpn service """ - uuid_l2vpn = str(uuid.uuid4()) - vpn_service = {} - vpn_service["vpn-id"] = uuid_l2vpn - vpn_service["vpn-scv-type"] = "vpws" - vpn_service["svc-topo"] = "any-to-any" - vpn_service["customer-name"] = "osm" - vpn_service_list = [] - vpn_service_list.append(vpn_service) - vpn_service_l = {"ietf-l2vpn-svc:vpn-service": vpn_service_list} - response_service_creation = None - conn_info = [] - self.logger.info("Sending vpn-service :{}".format(vpn_service_l)) - try: - endpoint_service_creation = "{}/restconf/data/ietf-l2vpn-svc:l2vpn-svc/vpn-services".format( - self.wim["wim_url"]) - response_service_creation = requests.post(endpoint_service_creation, headers=self.headers, - json=vpn_service_l, auth=self.auth) - except requests.exceptions.ConnectionError: - raise SdnConnectorError("Request to create service Timeout", http_code=408) - if response_service_creation.status_code == 409: - raise SdnConnectorError("Service already exists", http_code=response_service_creation.status_code) - elif response_service_creation.status_code != requests.codes.created: - raise SdnConnectorError("Request to create service not accepted", - http_code=response_service_creation.status_code) - """ Second step, create the connections and vpn attachments """ - for connection_point in connection_points: - connection_point_wan_info = self.search_mapp(connection_point) - site_network_access = {} - connection = {} - if connection_point["service_endpoint_encapsulation_type"] != "none": - if connection_point["service_endpoint_encapsulation_type"] == "dot1q": - """ The connection is a VLAN """ - connection["encapsulation-type"] = "dot1q-vlan-tagged" - tagged = {} - tagged_interf = {} - service_endpoint_encapsulation_info = connection_point["service_endpoint_encapsulation_info"] - if service_endpoint_encapsulation_info["vlan"] is None: - raise SdnConnectorError("VLAN must be provided") - tagged_interf["cvlan-id"] = service_endpoint_encapsulation_info["vlan"] - tagged["dot1q-vlan-tagged"] = tagged_interf - connection["tagged-interface"] = tagged - else: - raise NotImplementedError("Encapsulation type not implemented") - site_network_access["connection"] = connection - self.logger.info("Sending connection:{}".format(connection)) - vpn_attach = {} - vpn_attach["vpn-id"] = uuid_l2vpn - vpn_attach["site-role"] = vpn_service["svc-topo"]+"-role" - site_network_access["vpn-attachment"] = vpn_attach - self.logger.info("Sending vpn-attachement :{}".format(vpn_attach)) - uuid_sna = str(uuid.uuid4()) - site_network_access["network-access-id"] = uuid_sna - site_network_access["bearer"] = connection_point_wan_info["service_mapping_info"]["bearer"] - site_network_accesses = {} - site_network_access_list = [] - site_network_access_list.append(site_network_access) - site_network_accesses["ietf-l2vpn-svc:site-network-access"] = site_network_access_list - conn_info_d = {} - conn_info_d["site"] = connection_point_wan_info["service_mapping_info"]["site-id"] - conn_info_d["site-network-access-id"] = site_network_access["network-access-id"] - conn_info_d["mapping"] = None - conn_info.append(conn_info_d) - try: - endpoint_site_network_access_creation = \ - "{}/restconf/data/ietf-l2vpn-svc:l2vpn-svc/sites/site={}/site-network-accesses/".format( - self.wim["wim_url"], connection_point_wan_info["service_mapping_info"]["site-id"]) - response_endpoint_site_network_access_creation = requests.post( - endpoint_site_network_access_creation, - headers=self.headers, - json=site_network_accesses, - auth=self.auth) - - if response_endpoint_site_network_access_creation.status_code == 409: - self.delete_connectivity_service(vpn_service["vpn-id"]) - raise SdnConnectorError("Site_Network_Access with ID '{}' already exists".format( - site_network_access["network-access-id"]), - http_code=response_endpoint_site_network_access_creation.status_code) - - elif response_endpoint_site_network_access_creation.status_code == 400: - self.delete_connectivity_service(vpn_service["vpn-id"]) - raise SdnConnectorError("Site {} does not exist".format( - connection_point_wan_info["service_mapping_info"]["site-id"]), - http_code=response_endpoint_site_network_access_creation.status_code) - - elif response_endpoint_site_network_access_creation.status_code != requests.codes.created and \ - response_endpoint_site_network_access_creation.status_code != requests.codes.no_content: - self.delete_connectivity_service(vpn_service["vpn-id"]) - raise SdnConnectorError("Request no accepted", - http_code=response_endpoint_site_network_access_creation.status_code) - - except requests.exceptions.ConnectionError: - self.delete_connectivity_service(vpn_service["vpn-id"]) - raise SdnConnectorError("Request Timeout", http_code=408) - return uuid_l2vpn, conn_info - - else: - raise NotImplementedError - - def delete_connectivity_service(self, service_uuid, conn_info=None): - """Disconnect multi-site endpoints previously connected - - This method should receive as the first argument the UUID generated by - the ``create_connectivity_service`` - """ - try: - self.logger.info("Sending delete") - servicepoint = "{}/restconf/data/ietf-l2vpn-svc:l2vpn-svc/vpn-services/vpn-service={}/".format( - self.wim["wim_url"], service_uuid) - response = requests.delete(servicepoint, auth=self.auth) - if response.status_code != requests.codes.no_content: - raise SdnConnectorError("Error in the request", http_code=response.status_code) - except requests.exceptions.ConnectionError: - raise SdnConnectorError("Request Timeout", http_code=408) - - def edit_connectivity_service(self, service_uuid, conn_info=None, - connection_points=None, **kwargs): - """Change an existing connectivity service, see - ``create_connectivity_service``""" - - # sites = {"sites": {}} - # site_list = [] - vpn_service = {} - vpn_service["svc-topo"] = "any-to-any" - counter = 0 - for connection_point in connection_points: - site_network_access = {} - connection_point_wan_info = self.search_mapp(connection_point) - params_site = {} - params_site["site-id"] = connection_point_wan_info["service_mapping_info"]["site-id"] - params_site["site-vpn-flavor"] = "site-vpn-flavor-single" - device_site = {} - device_site["device-id"] = connection_point_wan_info["device-id"] - params_site["devices"] = device_site - # network_access = {} - connection = {} - if connection_point["service_endpoint_encapsulation_type"] != "none": - if connection_point["service_endpoint_encapsulation_type"] == "dot1q": - """ The connection is a VLAN """ - connection["encapsulation-type"] = "dot1q-vlan-tagged" - tagged = {} - tagged_interf = {} - service_endpoint_encapsulation_info = connection_point["service_endpoint_encapsulation_info"] - if service_endpoint_encapsulation_info["vlan"] is None: - raise SdnConnectorError("VLAN must be provided") - tagged_interf["cvlan-id"] = service_endpoint_encapsulation_info["vlan"] - tagged["dot1q-vlan-tagged"] = tagged_interf - connection["tagged-interface"] = tagged - else: - raise NotImplementedError("Encapsulation type not implemented") - site_network_access["connection"] = connection - vpn_attach = {} - vpn_attach["vpn-id"] = service_uuid - vpn_attach["site-role"] = vpn_service["svc-topo"]+"-role" - site_network_access["vpn-attachment"] = vpn_attach - uuid_sna = conn_info[counter]["site-network-access-id"] - site_network_access["network-access-id"] = uuid_sna - site_network_access["bearer"] = connection_point_wan_info["service_mapping_info"]["bearer"] - site_network_accesses = {} - site_network_access_list = [] - site_network_access_list.append(site_network_access) - site_network_accesses["ietf-l2vpn-svc:site-network-access"] = site_network_access_list - try: - endpoint_site_network_access_edit = \ - "{}/restconf/data/ietf-l2vpn-svc:l2vpn-svc/sites/site={}/site-network-accesses/".format( - self.wim["wim_url"], connection_point_wan_info["service_mapping_info"]["site-id"]) - response_endpoint_site_network_access_creation = requests.put(endpoint_site_network_access_edit, - headers=self.headers, - json=site_network_accesses, - auth=self.auth) - if response_endpoint_site_network_access_creation.status_code == 400: - raise SdnConnectorError("Service does not exist", - http_code=response_endpoint_site_network_access_creation.status_code) - elif response_endpoint_site_network_access_creation.status_code != 201 and \ - response_endpoint_site_network_access_creation.status_code != 204: - raise SdnConnectorError("Request no accepted", - http_code=response_endpoint_site_network_access_creation.status_code) - except requests.exceptions.ConnectionError: - raise SdnConnectorError("Request Timeout", http_code=408) - counter += 1 - return None - - def clear_all_connectivity_services(self): - """Delete all WAN Links corresponding to a WIM""" - try: - self.logger.info("Sending clear all connectivity services") - servicepoint = "{}/restconf/data/ietf-l2vpn-svc:l2vpn-svc/vpn-services".format(self.wim["wim_url"]) - response = requests.delete(servicepoint, auth=self.auth) - if response.status_code != requests.codes.no_content: - raise SdnConnectorError("Unable to clear all connectivity services", http_code=response.status_code) - except requests.exceptions.ConnectionError: - raise SdnConnectorError("Request Timeout", http_code=408) - - def get_all_active_connectivity_services(self): - """Provide information about all active connections provisioned by a - WIM - """ - try: - self.logger.info("Sending get all connectivity services") - servicepoint = "{}/restconf/data/ietf-l2vpn-svc:l2vpn-svc/vpn-services".format(self.wim["wim_url"]) - response = requests.get(servicepoint, auth=self.auth) - if response.status_code != requests.codes.ok: - raise SdnConnectorError("Unable to get all connectivity services", http_code=response.status_code) - return response - except requests.exceptions.ConnectionError: - raise SdnConnectorError("Request Timeout", http_code=408) diff --git a/RO-SDN-tapi/requirements.txt b/RO-SDN-tapi/requirements.txt deleted file mode 100644 index a6f6d655..00000000 --- a/RO-SDN-tapi/requirements.txt +++ /dev/null @@ -1,18 +0,0 @@ -## -# 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. -## - -requests -git+https://osm.etsi.org/gerrit/osm/RO.git#egg=osm-ro&subdirectory=RO - diff --git a/RO-SDN-tapi/setup.py b/RO-SDN-tapi/setup.py deleted file mode 100644 index 9217492a..00000000 --- a/RO-SDN-tapi/setup.py +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -## -# 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. -## - -from setuptools import setup - -_name = "osm_rosdn_tapi" - -README = """ -=========== -osm-rosdn_tapi -=========== - -osm-ro pluging for tapi (ietfl2vpn) SDN -""" - -setup( - name=_name, - description='OSM ro sdn plugin for tapi (ietfl2vpn)', - long_description=README, - version_command=('git describe --match v* --tags --long --dirty', 'pep440-git-full'), - # version=VERSION, - # python_requires='>3.5.0', - author='ETSI OSM', - # TODO py3 author_email='', - maintainer='OSM_TECH@LIST.ETSI.ORG', # TODO py3 - # TODO py3 maintainer_email='', - url='https://osm.etsi.org/gitweb/?p=osm/RO.git;a=summary', - license='Apache 2.0', - - packages=[_name], - include_package_data=True, - dependency_links=["git+https://osm.etsi.org/gerrit/osm/RO.git#egg=osm-ro"], - install_requires=[ - "requests", - "osm-ro @ git+https://osm.etsi.org/gerrit/osm/RO.git#egg=osm-ro&subdirectory=RO" - ], - setup_requires=['setuptools-version-command'], - entry_points={ - 'osm_rosdn.plugins': ['rosdn_tapi = osm_rosdn_tapi.wimconn_ietfl2vpn:WimconnectorIETFL2VPN'], - }, -) diff --git a/RO-SDN-tapi/stdeb.cfg b/RO-SDN-tapi/stdeb.cfg deleted file mode 100644 index 0c718e4f..00000000 --- a/RO-SDN-tapi/stdeb.cfg +++ /dev/null @@ -1,19 +0,0 @@ -# -# 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. -# - -[DEFAULT] -X-Python3-Version : >= 3.5 -Depends3: python3-requests, python3-osm-ro - diff --git a/RO-SDN-tapi/tox.ini b/RO-SDN-tapi/tox.ini deleted file mode 100644 index 7d643cd2..00000000 --- a/RO-SDN-tapi/tox.ini +++ /dev/null @@ -1,41 +0,0 @@ -## -# 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. -## - -[tox] -envlist = py3 -toxworkdir={homedir}/.tox - -[testenv] -basepython = python3 -install_command = python3 -m pip install -r requirements.txt -U {opts} {packages} -# deps = -r{toxinidir}/test-requirements.txt -commands=python3 -m unittest discover -v - -[testenv:flake8] -basepython = python3 -deps = flake8 -commands = flake8 osm_rosdn_tapi --max-line-length 120 \ - --exclude .svn,CVS,.gz,.git,__pycache__,.tox,local,temp --ignore W291,W293,E226,W504 - -[testenv:unittest] -basepython = python3 -commands = python3 -m unittest osm_rosdn_tapi.tests - -[testenv:build] -basepython = python3 -deps = stdeb - setuptools-version-command -commands = python3 setup.py --command-packages=stdeb.command bdist_deb - diff --git a/RO-client/osm_roclient/roclient.py b/RO-client/osm_roclient/roclient.py index 0e6d32ce..b2eb3ffa 100755 --- a/RO-client/osm_roclient/roclient.py +++ b/RO-client/osm_roclient/roclient.py @@ -2263,7 +2263,7 @@ def main(): wim_create_parser.add_argument("url", action="store", help="url for the wim") wim_create_parser.add_argument("--type", action="store", - help="wim type: tapi, onos, dynpac or odl (default)") + help="wim type: ietfl2vpn, dynpac, ...") wim_create_parser.add_argument("--config", action="store", help="additional configuration in json/yaml format") wim_create_parser.add_argument("--description", action="store", diff --git a/RO/osm_ro/openmanoclient.py b/RO/osm_ro/openmanoclient.py index fc8bde11..a72c73e0 100644 --- a/RO/osm_ro/openmanoclient.py +++ b/RO/osm_ro/openmanoclient.py @@ -602,7 +602,7 @@ class openmanoclient(): must be a dictionary or a json/yaml text. name: the wim name. Overwrite descriptor name if any wim_url: the wim URL. Overwrite descriptor vim_url if any - wim_type: the WIM type, can be tapi, odl, onos. Overwrite descriptor type if any + wim_type: the WIM type, can be ietfl2vpn, odl, onos. Overwrite descriptor type if any public: boolean, by default not public description: wim description. Overwrite descriptor description if any config: dictionary with extra configuration for the concrete wim @@ -639,7 +639,7 @@ class openmanoclient(): parameters to change can be supplied by the descriptor or as parameters: new_name: the wim name wim_url: the wim URL - wim_type: the wim type, can be tapi, onos, odl + wim_type: the wim type, can be ietfl2vpn, onos, odl public: boolean, available to other tenants description: wim description Return: Raises an exception on error, not found or found several diff --git a/RO/osm_ro/wim/schemas.py b/RO/osm_ro/wim/schemas.py index f20d418f..6c0a8489 100644 --- a/RO/osm_ro/wim/schemas.py +++ b/RO/osm_ro/wim/schemas.py @@ -39,7 +39,7 @@ from ..openmano_schemas import ( ) # WIM ------------------------------------------------------------------------- -wim_types = ["tapi", "onos", "onos_vpls", "odl", "dynpac", "dummy"] +wim_types = ["ietfl2vpn", "dynpac", "arista_cloudvision", "floodlightof", "onosof", "onos_vpls", "odlof", "dummy"] dpid_type = { "type": "string", @@ -96,7 +96,7 @@ wim_schema_properties = { "description": description_schema, "type": { "type": "string", - # "enum": ["tapi", "onos", "odl", "dynpac", "dummy"] + # "enum": ["ietfl2vpn", "onos", "odl", "dynpac", "dummy", ...] }, "wim_url": description_schema, "config": { diff --git a/RO/osm_ro/wim/tests/fixtures.py b/RO/osm_ro/wim/tests/fixtures.py index 4280a460..6e36b55d 100644 --- a/RO/osm_ro/wim/tests/fixtures.py +++ b/RO/osm_ro/wim/tests/fixtures.py @@ -53,7 +53,7 @@ def wim(identifier=0): return {'name': 'wim%d' % identifier, 'uuid': uuid('wim%d' % identifier), 'wim_url': 'localhost', - 'type': 'tapi'} + 'type': 'ietfl2vpn'} def tenant(identifier=0): diff --git a/devops-stages/stage-build.sh b/devops-stages/stage-build.sh index 28ad87c4..aea4749d 100755 --- a/devops-stages/stage-build.sh +++ b/devops-stages/stage-build.sh @@ -24,18 +24,18 @@ cp RO/deb_dist/python3-osm-ro_*.deb deb_dist/ make -C RO-client clean package cp RO-client/deb_dist/python3-osm-roclient_*.deb deb_dist/ -# VIM plugings: vmware openstack AWS fos azure Opennebula +# VIM plugings: vmware, openstack, AWS, fos, azure, Opennebula, for vim_plugin in RO-VIM-* do make -C $vim_plugin clean package cp ${vim_plugin}/deb_dist/python3-osm-rovim*.deb deb_dist/ done -# SDN plugins - -# SDN plugins: Dynpack Tapi Onosof Floodlightof +# SDN plugins: DynPac, Ietfl2vpn, Onosof Floodlightof for sdn_plugin in RO-SDN-* do + [[ "$sdn_plugin" == RO-SDN-tapi ]] && continue # tapi folder appears at Jenkins due to container reuse + [[ "$sdn_plugin" == RO-SDN-arista ]] && continue # arista folder appears at Jenkins due to container reuse make -C $sdn_plugin clean package cp ${sdn_plugin}/deb_dist/python3-osm-rosdn*.deb deb_dist/ done