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 && \
+++ /dev/null
-##
-# 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
-
+++ /dev/null
-#!/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
+++ /dev/null
-# -*- coding: utf-8 -*-\r
-##\r
-# Copyright 2019 Atos - CoE Telco NFV Team\r
-# All Rights Reserved.\r
-#\r
-# Contributors: Oscar Luis Peral, Atos\r
-#\r
-# Licensed under the Apache License, Version 2.0 (the "License"); you may\r
-# not use this file except in compliance with the License. You may obtain\r
-# a copy of the License at\r
-#\r
-# http://www.apache.org/licenses/LICENSE-2.0\r
-#\r
-# Unless required by applicable law or agreed to in writing, software\r
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT\r
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\r
-# License for the specific language governing permissions and limitations\r
-# under the License.\r
-#\r
-# For those usages not covered by the Apache License, Version 2.0 please\r
-# contact with: <oscarluis.peral@atos.net>\r
-#\r
-# Neither the name of Atos nor the names of its\r
-# contributors may be used to endorse or promote products derived from\r
-# this software without specific prior written permission.\r
-#\r
-# This work has been performed in the context of Arista Telefonica OSM PoC.\r
-##\r
-\r
-\r
-class AristaSDNConfigLet:\r
- _VLAN = "VLAN"\r
- _VXLAN = "VXLAN"\r
- _VLAN_MLAG = "VLAN-MLAG"\r
- _VXLAN_MLAG = "VXLAN-MLAG"\r
- topology = _VXLAN_MLAG\r
-\r
- def __init__(self, topology=_VXLAN_MLAG):\r
- self.topology = topology\r
-\r
- _basic_int ="""\r
-interface {interface}\r
- !! service: {uuid}\r
- switchport\r
- switchport mode {type}\r
- switchport {switchport_def}\r
-!\r
-"""\r
- _int_SRIOV = "trunk group {service}{vlan_id}"\r
- _int_PASSTROUGH = "access vlan {vlan_id}"\r
-\r
- def _get_interface(self, uuid, interface, vlan_id, s_type, index, i_type):\r
- if i_type == "trunk":\r
- switchport_def = self._int_SRIOV.format(service=s_type, vlan_id=vlan_id)\r
- else:\r
- switchport_def = self._int_PASSTROUGH.format(vlan_id=vlan_id)\r
- return self._basic_int.format(uuid=uuid,\r
- interface=interface,\r
- type=i_type,\r
- switchport_def=switchport_def)\r
-\r
- def getElan_sriov(self, uuid, interface, vlan_id, index):\r
- return self._get_interface(uuid, interface, vlan_id, "ELAN", index, "trunk")\r
-\r
- def getEline_sriov(self, uuid, interface, vlan_id, index):\r
- return self._get_interface(uuid, interface, vlan_id, "ELINE", index, "trunk")\r
-\r
- def getElan_passthrough(self, uuid, interface, vlan_id, index):\r
- return self._get_interface(uuid, interface, vlan_id, "ELAN", index, "dot1q-tunnel")\r
-\r
- def getEline_passthrough(self, uuid, interface, vlan_id, index):\r
- return self._get_interface(uuid, interface, vlan_id, "ELINE", index, "dot1q-tunnel")\r
-\r
- _basic_vlan ="""\r
-vlan {vlan}\r
- !! service: {service} {vlan} {uuid}\r
- name {service}{vlan}\r
- trunk group {service}{vlan}\r
-"""\r
- _basic_mlag =""" trunk group MLAGPEER\r
-"""\r
- _basic_vxlan ="""interface VXLAN1\r
- VXLAN vlan {vlan} vni {vni}\r
-"""\r
- _basic_end ="!"\r
-\r
- _configLet_VLAN = _basic_vlan + _basic_end\r
- _configLet_VXLAN = _basic_vlan + _basic_vxlan + _basic_end\r
- _configLet_VLAN_MLAG = _basic_vlan + _basic_mlag + _basic_end\r
- _configLet_VXLAN_MLAG = _basic_vlan + _basic_mlag + _basic_vxlan + _basic_end\r
-\r
- def _get_vlan(self, uuid, vlan_id, vni_id, s_type):\r
- if self.topology == self._VLAN:\r
- return self._configLet_VLAN.format(service=s_type, vlan=vlan_id, uuid=uuid)\r
- if self.topology == self._VLAN_MLAG:\r
- return self._configLet_VLAN_MLAG.format(service=s_type, vlan=vlan_id, uuid=uuid)\r
- if self.topology == self._VXLAN:\r
- return self._configLet_VXLAN.format(service=s_type, vlan=vlan_id, uuid=uuid, vni=vni_id)\r
- if self.topology == self._VXLAN_MLAG:\r
- return self._configLet_VXLAN_MLAG.format(service=s_type, vlan=vlan_id, uuid=uuid, vni=vni_id)\r
-\r
- def getElan_vlan(self, uuid, vlan_id, vni_id):\r
- return self._get_vlan(uuid, vlan_id, vni_id, "ELAN")\r
-\r
- def getEline_vlan(self, uuid, vlan_id, vni_id):\r
- return self._get_vlan(uuid, vlan_id, vni_id, "ELINE")\r
-\r
- _configLet_BGP = """\r
-router bgp {bgp}\r
- vlan {vlan}\r
- !! service: {uuid}\r
- rd {loopback}:{vni}\r
- route-target both {vni}:{vni}\r
- redistribute learned\r
-!\r
-"""\r
-\r
- def _get_bgp(self, uuid, vlan_id, vni_id, loopback0, bgp, s_type):\r
- if self.topology == self._VXLAN or self.topology == self._VXLAN_MLAG:\r
- return self._configLet_BGP.format(uuid=uuid,\r
- bgp=bgp,\r
- vlan=vlan_id,\r
- loopback=loopback0,\r
- vni=vni_id)\r
-\r
-\r
- def getElan_bgp(self, uuid, vlan_id, vni_id, loopback0, bgp):\r
- return self._get_bgp(uuid, vlan_id, vni_id, loopback0, bgp, "ELAN")\r
-\r
- def getEline_bgp(self, uuid, vlan_id, vni_id, loopback0, bgp):\r
- return self._get_bgp(uuid, vlan_id, vni_id, loopback0, bgp, "ELINE")\r
+++ /dev/null
-# -*- coding: utf-8 -*-\r
-##\r
-# Copyright 2019 Atos - CoE Telco NFV Team\r
-# All Rights Reserved.\r
-#\r
-# Contributors: Oscar Luis Peral, Atos\r
-#\r
-# Licensed under the Apache License, Version 2.0 (the "License"); you may\r
-# not use this file except in compliance with the License. You may obtain\r
-# a copy of the License at\r
-#\r
-# http://www.apache.org/licenses/LICENSE-2.0\r
-#\r
-# Unless required by applicable law or agreed to in writing, software\r
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT\r
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\r
-# License for the specific language governing permissions and limitations\r
-# under the License.\r
-#\r
-# For those usages not covered by the Apache License, Version 2.0 please\r
-# contact with: <oscarluis.peral@atos.net>\r
-#\r
-# Neither the name of Atos nor the names of its\r
-# contributors may be used to endorse or promote products derived from\r
-# this software without specific prior written permission.\r
-#\r
-# This work has been performed in the context of Arista Telefonica OSM PoC.\r
-##\r
-import time\r
-\r
-\r
-class AristaCVPTask:\r
- def __init__(self, cvpClientApi):\r
- self.cvpClientApi = cvpClientApi\r
-\r
- def __get_id(self, task):\r
- return task.get("workOrderId")\r
-\r
- def __get_state(self, task):\r
- return task.get("workOrderUserDefinedStatus")\r
-\r
- def __execute_task(self, task_id):\r
- return self.cvpClientApi.execute_task(task_id)\r
-\r
- def __cancel_task(self, task_id):\r
- return self.cvpClientApi.cancel_task(task_id)\r
-\r
- def __apply_state(self, task, state):\r
- t_id = self.__get_id(task)\r
- self.cvpClientApi.add_note_to_task(t_id, "Executed by OSM")\r
- if state == "executed":\r
- return self.__execute_task(t_id)\r
- elif state == "cancelled":\r
- return self.__cancel_task(t_id)\r
-\r
- def __actionable(self, state):\r
- return state in ["Pending"]\r
-\r
- def __terminal(self, state):\r
- return state in ["Completed", "Cancelled"]\r
-\r
- def __state_is_different(self, task, target):\r
- return self.__get_state(task) != target\r
-\r
- def update_all_tasks(self, data):\r
- new_data = dict()\r
- for task_id in data.keys():\r
- res = self.cvpClientApi.get_task_by_id(task_id)\r
- new_data[task_id] = res\r
- return new_data\r
-\r
- def get_pending_tasks(self):\r
- return self.cvpClientApi.get_tasks_by_status('Pending')\r
-\r
- def get_pending_tasks_old(self):\r
- taskList = []\r
- tasksField = {'workOrderId': 'workOrderId',\r
- 'workOrderState': 'workOrderState',\r
- 'currentTaskName': 'currentTaskName',\r
- 'description': 'description',\r
- 'workOrderUserDefinedStatus':\r
- 'workOrderUserDefinedStatus',\r
- 'note': 'note',\r
- 'taskStatus': 'taskStatus',\r
- 'workOrderDetails': 'workOrderDetails'}\r
- tasks = self.cvpClientApi.get_tasks_by_status('Pending')\r
- # Reduce task data to required fields\r
- for task in tasks:\r
- taskFacts = {}\r
- for field in task.keys():\r
- if field in tasksField:\r
- taskFacts[tasksField[field]] = task[field]\r
- taskList.append(taskFacts)\r
- return taskList\r
-\r
- def task_action(self, tasks, wait, state):\r
- changed = False\r
- data = dict()\r
- warnings = list()\r
-\r
- at = [t for t in tasks if self.__actionable(self.__get_state(t))]\r
- actionable_tasks = at\r
-\r
- if len(actionable_tasks) == 0:\r
- warnings.append("No actionable tasks found on CVP")\r
- return changed, data, warnings\r
-\r
- for task in actionable_tasks:\r
- if self.__state_is_different(task, state):\r
- self.__apply_state(task, state)\r
- changed = True\r
- data[self.__get_id(task)] = task\r
-\r
- if wait == 0:\r
- return changed, data, warnings\r
-\r
- start = time.time()\r
- now = time.time()\r
- while (now - start) < wait:\r
- data = self.update_all_tasks(data)\r
- if all([self.__terminal(self.__get_state(t)) for t in data.values()]):\r
- break\r
- time.sleep(1)\r
- now = time.time()\r
-\r
- if wait:\r
- for i, task in data.items():\r
- if not self.__terminal(self.__get_state(task)):\r
- warnings.append("Task {} has not completed in {} seconds".\r
- format(i, wait))\r
-\r
- return changed, data, warnings\r
+++ /dev/null
-# -*- 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: <oscarluis.peral@atos.net>
-#
-# 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
+++ /dev/null
-##
-# 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
-
+++ /dev/null
-#!/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']
- },
-)
+++ /dev/null
-#
-# 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
-
+++ /dev/null
-##
-# 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
-
--- /dev/null
+##
+# 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
+
--- /dev/null
+#!/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
--- /dev/null
+# -*- coding: utf-8 -*-\r
+##\r
+# Copyright 2019 Atos - CoE Telco NFV Team\r
+# All Rights Reserved.\r
+#\r
+# Contributors: Oscar Luis Peral, Atos\r
+#\r
+# Licensed under the Apache License, Version 2.0 (the "License"); you may\r
+# not use this file except in compliance with the License. You may obtain\r
+# a copy of the License at\r
+#\r
+# http://www.apache.org/licenses/LICENSE-2.0\r
+#\r
+# Unless required by applicable law or agreed to in writing, software\r
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT\r
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\r
+# License for the specific language governing permissions and limitations\r
+# under the License.\r
+#\r
+# For those usages not covered by the Apache License, Version 2.0 please\r
+# contact with: <oscarluis.peral@atos.net>\r
+#\r
+# Neither the name of Atos nor the names of its\r
+# contributors may be used to endorse or promote products derived from\r
+# this software without specific prior written permission.\r
+#\r
+# This work has been performed in the context of Arista Telefonica OSM PoC.\r
+##\r
+\r
+\r
+class AristaSDNConfigLet:\r
+ _VLAN = "VLAN"\r
+ _VXLAN = "VXLAN"\r
+ _VLAN_MLAG = "VLAN-MLAG"\r
+ _VXLAN_MLAG = "VXLAN-MLAG"\r
+ topology = _VXLAN_MLAG\r
+\r
+ def __init__(self, topology=_VXLAN_MLAG):\r
+ self.topology = topology\r
+\r
+ _basic_int ="""\r
+interface {interface}\r
+ !! service: {uuid}\r
+ switchport\r
+ switchport mode {type}\r
+ switchport {switchport_def}\r
+!\r
+"""\r
+ _int_SRIOV = "trunk group {service}{vlan_id}"\r
+ _int_PASSTROUGH = "access vlan {vlan_id}"\r
+\r
+ def _get_interface(self, uuid, interface, vlan_id, s_type, index, i_type):\r
+ if i_type == "trunk":\r
+ switchport_def = self._int_SRIOV.format(service=s_type, vlan_id=vlan_id)\r
+ else:\r
+ switchport_def = self._int_PASSTROUGH.format(vlan_id=vlan_id)\r
+ return self._basic_int.format(uuid=uuid,\r
+ interface=interface,\r
+ type=i_type,\r
+ switchport_def=switchport_def)\r
+\r
+ def getElan_sriov(self, uuid, interface, vlan_id, index):\r
+ return self._get_interface(uuid, interface, vlan_id, "ELAN", index, "trunk")\r
+\r
+ def getEline_sriov(self, uuid, interface, vlan_id, index):\r
+ return self._get_interface(uuid, interface, vlan_id, "ELINE", index, "trunk")\r
+\r
+ def getElan_passthrough(self, uuid, interface, vlan_id, index):\r
+ return self._get_interface(uuid, interface, vlan_id, "ELAN", index, "dot1q-tunnel")\r
+\r
+ def getEline_passthrough(self, uuid, interface, vlan_id, index):\r
+ return self._get_interface(uuid, interface, vlan_id, "ELINE", index, "dot1q-tunnel")\r
+\r
+ _basic_vlan ="""\r
+vlan {vlan}\r
+ !! service: {service} {vlan} {uuid}\r
+ name {service}{vlan}\r
+ trunk group {service}{vlan}\r
+"""\r
+ _basic_mlag =""" trunk group MLAGPEER\r
+"""\r
+ _basic_vxlan ="""interface VXLAN1\r
+ VXLAN vlan {vlan} vni {vni}\r
+"""\r
+ _basic_end ="!"\r
+\r
+ _configLet_VLAN = _basic_vlan + _basic_end\r
+ _configLet_VXLAN = _basic_vlan + _basic_vxlan + _basic_end\r
+ _configLet_VLAN_MLAG = _basic_vlan + _basic_mlag + _basic_end\r
+ _configLet_VXLAN_MLAG = _basic_vlan + _basic_mlag + _basic_vxlan + _basic_end\r
+\r
+ def _get_vlan(self, uuid, vlan_id, vni_id, s_type):\r
+ if self.topology == self._VLAN:\r
+ return self._configLet_VLAN.format(service=s_type, vlan=vlan_id, uuid=uuid)\r
+ if self.topology == self._VLAN_MLAG:\r
+ return self._configLet_VLAN_MLAG.format(service=s_type, vlan=vlan_id, uuid=uuid)\r
+ if self.topology == self._VXLAN:\r
+ return self._configLet_VXLAN.format(service=s_type, vlan=vlan_id, uuid=uuid, vni=vni_id)\r
+ if self.topology == self._VXLAN_MLAG:\r
+ return self._configLet_VXLAN_MLAG.format(service=s_type, vlan=vlan_id, uuid=uuid, vni=vni_id)\r
+\r
+ def getElan_vlan(self, uuid, vlan_id, vni_id):\r
+ return self._get_vlan(uuid, vlan_id, vni_id, "ELAN")\r
+\r
+ def getEline_vlan(self, uuid, vlan_id, vni_id):\r
+ return self._get_vlan(uuid, vlan_id, vni_id, "ELINE")\r
+\r
+ _configLet_BGP = """\r
+router bgp {bgp}\r
+ vlan {vlan}\r
+ !! service: {uuid}\r
+ rd {loopback}:{vni}\r
+ route-target both {vni}:{vni}\r
+ redistribute learned\r
+!\r
+"""\r
+\r
+ def _get_bgp(self, uuid, vlan_id, vni_id, loopback0, bgp, s_type):\r
+ if self.topology == self._VXLAN or self.topology == self._VXLAN_MLAG:\r
+ return self._configLet_BGP.format(uuid=uuid,\r
+ bgp=bgp,\r
+ vlan=vlan_id,\r
+ loopback=loopback0,\r
+ vni=vni_id)\r
+\r
+\r
+ def getElan_bgp(self, uuid, vlan_id, vni_id, loopback0, bgp):\r
+ return self._get_bgp(uuid, vlan_id, vni_id, loopback0, bgp, "ELAN")\r
+\r
+ def getEline_bgp(self, uuid, vlan_id, vni_id, loopback0, bgp):\r
+ return self._get_bgp(uuid, vlan_id, vni_id, loopback0, bgp, "ELINE")\r
--- /dev/null
+# -*- coding: utf-8 -*-\r
+##\r
+# Copyright 2019 Atos - CoE Telco NFV Team\r
+# All Rights Reserved.\r
+#\r
+# Contributors: Oscar Luis Peral, Atos\r
+#\r
+# Licensed under the Apache License, Version 2.0 (the "License"); you may\r
+# not use this file except in compliance with the License. You may obtain\r
+# a copy of the License at\r
+#\r
+# http://www.apache.org/licenses/LICENSE-2.0\r
+#\r
+# Unless required by applicable law or agreed to in writing, software\r
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT\r
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\r
+# License for the specific language governing permissions and limitations\r
+# under the License.\r
+#\r
+# For those usages not covered by the Apache License, Version 2.0 please\r
+# contact with: <oscarluis.peral@atos.net>\r
+#\r
+# Neither the name of Atos nor the names of its\r
+# contributors may be used to endorse or promote products derived from\r
+# this software without specific prior written permission.\r
+#\r
+# This work has been performed in the context of Arista Telefonica OSM PoC.\r
+##\r
+import time\r
+\r
+\r
+class AristaCVPTask:\r
+ def __init__(self, cvpClientApi):\r
+ self.cvpClientApi = cvpClientApi\r
+\r
+ def __get_id(self, task):\r
+ return task.get("workOrderId")\r
+\r
+ def __get_state(self, task):\r
+ return task.get("workOrderUserDefinedStatus")\r
+\r
+ def __execute_task(self, task_id):\r
+ return self.cvpClientApi.execute_task(task_id)\r
+\r
+ def __cancel_task(self, task_id):\r
+ return self.cvpClientApi.cancel_task(task_id)\r
+\r
+ def __apply_state(self, task, state):\r
+ t_id = self.__get_id(task)\r
+ self.cvpClientApi.add_note_to_task(t_id, "Executed by OSM")\r
+ if state == "executed":\r
+ return self.__execute_task(t_id)\r
+ elif state == "cancelled":\r
+ return self.__cancel_task(t_id)\r
+\r
+ def __actionable(self, state):\r
+ return state in ["Pending"]\r
+\r
+ def __terminal(self, state):\r
+ return state in ["Completed", "Cancelled"]\r
+\r
+ def __state_is_different(self, task, target):\r
+ return self.__get_state(task) != target\r
+\r
+ def update_all_tasks(self, data):\r
+ new_data = dict()\r
+ for task_id in data.keys():\r
+ res = self.cvpClientApi.get_task_by_id(task_id)\r
+ new_data[task_id] = res\r
+ return new_data\r
+\r
+ def get_pending_tasks(self):\r
+ return self.cvpClientApi.get_tasks_by_status('Pending')\r
+\r
+ def get_pending_tasks_old(self):\r
+ taskList = []\r
+ tasksField = {'workOrderId': 'workOrderId',\r
+ 'workOrderState': 'workOrderState',\r
+ 'currentTaskName': 'currentTaskName',\r
+ 'description': 'description',\r
+ 'workOrderUserDefinedStatus':\r
+ 'workOrderUserDefinedStatus',\r
+ 'note': 'note',\r
+ 'taskStatus': 'taskStatus',\r
+ 'workOrderDetails': 'workOrderDetails'}\r
+ tasks = self.cvpClientApi.get_tasks_by_status('Pending')\r
+ # Reduce task data to required fields\r
+ for task in tasks:\r
+ taskFacts = {}\r
+ for field in task.keys():\r
+ if field in tasksField:\r
+ taskFacts[tasksField[field]] = task[field]\r
+ taskList.append(taskFacts)\r
+ return taskList\r
+\r
+ def task_action(self, tasks, wait, state):\r
+ changed = False\r
+ data = dict()\r
+ warnings = list()\r
+\r
+ at = [t for t in tasks if self.__actionable(self.__get_state(t))]\r
+ actionable_tasks = at\r
+\r
+ if len(actionable_tasks) == 0:\r
+ warnings.append("No actionable tasks found on CVP")\r
+ return changed, data, warnings\r
+\r
+ for task in actionable_tasks:\r
+ if self.__state_is_different(task, state):\r
+ self.__apply_state(task, state)\r
+ changed = True\r
+ data[self.__get_id(task)] = task\r
+\r
+ if wait == 0:\r
+ return changed, data, warnings\r
+\r
+ start = time.time()\r
+ now = time.time()\r
+ while (now - start) < wait:\r
+ data = self.update_all_tasks(data)\r
+ if all([self.__terminal(self.__get_state(t)) for t in data.values()]):\r
+ break\r
+ time.sleep(1)\r
+ now = time.time()\r
+\r
+ if wait:\r
+ for i, task in data.items():\r
+ if not self.__terminal(self.__get_state(task)):\r
+ warnings.append("Task {} has not completed in {} seconds".\r
+ format(i, wait))\r
+\r
+ return changed, data, warnings\r
--- /dev/null
+# -*- 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: <oscarluis.peral@atos.net>
+#
+# 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
--- /dev/null
+##
+# 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
+
--- /dev/null
+#!/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']
+ },
+)
--- /dev/null
+#
+# 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
+
--- /dev/null
+##
+# 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
+
--- /dev/null
+##
+# 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
+
--- /dev/null
+# -*- 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)
--- /dev/null
+##
+# 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
+
--- /dev/null
+#!/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'],
+ },
+)
--- /dev/null
+#
+# 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
+
--- /dev/null
+##
+# 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
+
+++ /dev/null
-##
-# 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
-
+++ /dev/null
-# -*- 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)
+++ /dev/null
-##
-# 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
-
+++ /dev/null
-#!/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'],
- },
-)
+++ /dev/null
-#
-# 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
-
+++ /dev/null
-##
-# 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
-
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",
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
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
)
# 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",
"description": description_schema,
"type": {
"type": "string",
- # "enum": ["tapi", "onos", "odl", "dynpac", "dummy"]
+ # "enum": ["ietfl2vpn", "onos", "odl", "dynpac", "dummy", ...]
},
"wim_url": description_schema,
"config": {
return {'name': 'wim%d' % identifier,
'uuid': uuid('wim%d' % identifier),
'wim_url': 'localhost',
- 'type': 'tapi'}
+ 'type': 'ietfl2vpn'}
def tenant(identifier=0):
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