Bug 1149 1150: improve exceptions error message
[osm/RO.git] / RO-SDN-arista_cloudvision / osm_rosdn_arista_cloudvision / wimconn_arista.py
1 # -*- coding: utf-8 -*-
2 ##
3 # Copyright 2019 Atos - CoE Telco NFV Team
4 # All Rights Reserved.
5 #
6 # Contributors: Oscar Luis Peral, Atos
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License"); you may
9 # not use this file except in compliance with the License. You may obtain
10 # a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
17 # License for the specific language governing permissions and limitations
18 # under the License.
19 #
20 # For those usages not covered by the Apache License, Version 2.0 please
21 # contact with: <oscarluis.peral@atos.net>
22 #
23 # Neither the name of Atos nor the names of its
24 # contributors may be used to endorse or promote products derived from
25 # this software without specific prior written permission.
26 #
27 # This work has been performed in the context of Arista Telefonica OSM PoC.
28 ##
29 from osm_ro_plugin.sdnconn import SdnConnectorBase, SdnConnectorError
30 import re
31 import socket
32 # Required by compare function
33 import difflib
34 # Library that uses Levenshtein Distance to calculate the differences
35 # between strings.
36 # from fuzzywuzzy import fuzz
37
38 import logging
39 import uuid
40 from enum import Enum
41 from requests import RequestException, ConnectionError, ConnectTimeout, Timeout
42 from cvprac.cvp_client import CvpClient
43 from cvprac.cvp_api import CvpApi
44 from cvprac.cvp_client_errors import CvpLoginError, CvpSessionLogOutError, CvpApiError
45 from cvprac import __version__ as cvprac_version
46
47 from osm_rosdn_arista_cloudvision.aristaConfigLet import AristaSDNConfigLet
48 from osm_rosdn_arista_cloudvision.aristaTask import AristaCVPTask
49
50
51 class SdnError(Enum):
52 UNREACHABLE = 'Unable to reach the WIM url, connect error.',
53 TIMEOUT = 'Unable to reach the WIM url, timeout.',
54 VLAN_INCONSISTENT = \
55 'VLAN value inconsistent between the connection points',
56 VLAN_NOT_PROVIDED = 'VLAN value not provided',
57 CONNECTION_POINTS_SIZE = \
58 'Unexpected number of connection points: 2 expected.',
59 ENCAPSULATION_TYPE = \
60 'Unexpected service_endpoint_encapsulation_type. ' \
61 'Only "dotq1" is accepted.',
62 BANDWIDTH = 'Unable to get the bandwidth.',
63 STATUS = 'Unable to get the status for the service.',
64 DELETE = 'Unable to delete service.',
65 CLEAR_ALL = 'Unable to clear all the services',
66 UNKNOWN_ACTION = 'Unknown action invoked.',
67 BACKUP = 'Unable to get the backup parameter.',
68 UNSUPPORTED_FEATURE = "Unsupported feature",
69 UNAUTHORIZED = "Failed while authenticating",
70 INTERNAL_ERROR = "Internal error"
71
72
73 class AristaSdnConnector(SdnConnectorBase):
74 """Arista class for the SDN connectors
75
76 Arguments:
77 wim (dict): WIM record, as stored in the database
78 wim_account (dict): WIM account record, as stored in the database
79 config
80 The arguments of the constructor are converted to object attributes.
81 An extra property, ``service_endpoint_mapping`` is created from ``config``.
82
83 The access to Arista CloudVision is made through the API defined in
84 https://github.com/aristanetworks/cvprac
85 The a connectivity service consist in creating a VLAN and associate the interfaces
86 of the connection points MAC addresses to this VLAN in all the switches of the topology,
87 the BDP is also configured for this VLAN.
88
89 The Arista Cloud Vision API workflow is the following
90 -- The switch configuration is defined as a set of switch configuration commands,
91 what is called 'ConfigLet'
92 -- The ConfigLet is associated to the device (leaf switch)
93 -- Automatically a task is associated to this activity for change control, the task
94 in this stage is in 'Pending' state
95 -- The task will be executed so that the configuration is applied to the switch.
96 -- The service information is saved in the response of the creation call
97 -- All created services identification is stored in a generic ConfigLet 'OSM_metadata'
98 to keep track of the managed resources by OSM in the Arista deployment.
99 """
100 __supported_service_types = ["ELINE (L2)", "ELINE", "ELAN"]
101 __service_types_ELAN = "ELAN"
102 __service_types_ELINE = "ELINE"
103 __ELINE_num_connection_points = 2
104 __supported_service_types = ["ELINE", "ELAN"]
105 __supported_encapsulation_types = ["dot1q"]
106 __WIM_LOGGER = 'openmano.sdnconn.arista'
107 __SERVICE_ENDPOINT_MAPPING = 'service_endpoint_mapping'
108 __ENCAPSULATION_TYPE_PARAM = "service_endpoint_encapsulation_type"
109 __ENCAPSULATION_INFO_PARAM = "service_endpoint_encapsulation_info"
110 __BACKUP_PARAM = "backup"
111 __BANDWIDTH_PARAM = "bandwidth"
112 __SERVICE_ENDPOINT_PARAM = "service_endpoint_id"
113 __MAC_PARAM = "mac"
114 __WAN_SERVICE_ENDPOINT_PARAM = "service_endpoint_id"
115 __WAN_MAPPING_INFO_PARAM = "service_mapping_info"
116 __DEVICE_ID_PARAM = "device_id"
117 __DEVICE_INTERFACE_ID_PARAM = "device_interface_id"
118 __SW_ID_PARAM = "switch_dpid"
119 __SW_PORT_PARAM = "switch_port"
120 __VLAN_PARAM = "vlan"
121 __VNI_PARAM = "vni"
122 __SEPARATOR = '_'
123 __MANAGED_BY_OSM = '## Managed by OSM '
124 __OSM_PREFIX = "osm_"
125 __OSM_METADATA = "OSM_metadata"
126 __METADATA_PREFIX = '!## Service'
127 __EXC_TASK_EXEC_WAIT = 10
128 __ROLLB_TASK_EXEC_WAIT = 10
129 __API_REQUEST_TOUT = 60
130 __SWITCH_TAG_NAME = 'topology_type'
131 __SWITCH_TAG_VALUE = 'leaf'
132 __LOOPBACK_INTF = "Loopback0"
133 _VLAN = "VLAN"
134 _VXLAN = "VXLAN"
135 _VLAN_MLAG = "VLAN-MLAG"
136 _VXLAN_MLAG = "VXLAN-MLAG"
137
138 def __init__(self, wim, wim_account, config=None, logger=None):
139 """
140
141 :param wim: (dict). Contains among others 'wim_url'
142 :param wim_account: (dict). Contains among others 'uuid' (internal id), 'name',
143 'sdn' (True if is intended for SDN-assist or False if intended for WIM), 'user', 'password'.
144 :param config: (dict or None): Particular information of plugin. These keys if present have a common meaning:
145 'mapping_not_needed': (bool) False by default or if missing, indicates that mapping is not needed.
146 'service_endpoint_mapping': (list) provides the internal endpoint mapping. The meaning is:
147 KEY meaning for WIM meaning for SDN assist
148 -------- -------- --------
149 device_id pop_switch_dpid compute_id
150 device_interface_id pop_switch_port compute_pci_address
151 service_endpoint_id wan_service_endpoint_id SDN_service_endpoint_id
152 service_mapping_info wan_service_mapping_info SDN_service_mapping_info
153 contains extra information if needed. Text in Yaml format
154 switch_dpid wan_switch_dpid SDN_switch_dpid
155 switch_port wan_switch_port SDN_switch_port
156 datacenter_id vim_account vim_account
157 id: (internal, do not use)
158 wim_id: (internal, do not use)
159 :param logger (logging.Logger): optional logger object. If none is passed 'openmano.sdn.sdnconn' is used.
160 """
161 self.__regex = re.compile(
162 r'^(?:http|ftp)s?://' # http:// or https://
163 r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # domain...
164 r'localhost|' # localhost...
165 r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip
166 r'(?::\d+)?', re.IGNORECASE) # optional port
167 self.raiseException = True
168 self.logger = logger or logging.getLogger(self.__WIM_LOGGER)
169 super().__init__(wim, wim_account, config, self.logger)
170 self.__wim = wim
171 self.__wim_account = wim_account
172 self.__config = config
173 if self.is_valid_destination(self.__wim.get("wim_url")):
174 self.__wim_url = self.__wim.get("wim_url")
175 else:
176 raise SdnConnectorError(message='Invalid wim_url value',
177 http_code=500)
178 self.__user = wim_account.get("user")
179 self.__passwd = wim_account.get("password")
180 self.client = None
181 self.cvp_inventory = None
182 self.cvp_tags = None
183 self.logger.debug("Arista SDN plugin {}, cvprac version {}, user:{} and config:{}".
184 format(wim, cvprac_version, self.__user,
185 self.delete_keys_from_dict(config, ('passwd',))))
186 self.allDeviceFacts = []
187 self.taskC = None
188 try:
189 self.__load_topology()
190 self.__load_switches()
191 except (ConnectTimeout, Timeout) as ct:
192 raise SdnConnectorError(message=SdnError.TIMEOUT + " " + str(ct), http_code=408)
193 except ConnectionError as ce:
194 raise SdnConnectorError(message=SdnError.UNREACHABLE + " " + str(ce), http_code=404)
195 except SdnConnectorError as sc:
196 raise sc
197 except CvpLoginError as le:
198 raise SdnConnectorError(message=le.msg, http_code=500) from le
199 except Exception as e:
200 raise SdnConnectorError(message="Unable to load switches from CVP" + " " + str(e),
201 http_code=500) from e
202 self.logger.debug("Using topology {} in Arista Leaf switches: {}".format(
203 self.topology,
204 self.delete_keys_from_dict(self.switches, ('passwd',))))
205 self.clC = AristaSDNConfigLet(self.topology)
206
207 def __load_topology(self):
208 self.topology = self._VXLAN_MLAG
209 if self.__config and self.__config.get('topology'):
210 topology = self.__config.get('topology')
211 if topology == "VLAN":
212 self.topology = self._VLAN
213 elif topology == "VXLAN":
214 self.topology = self._VXLAN
215 elif topology == "VLAN-MLAG":
216 self.topology = self._VLAN_MLAG
217 elif topology == "VXLAN-MLAG":
218 self.topology = self._VXLAN_MLAG
219
220 def __load_switches(self):
221 """ Retrieves the switches to configure in the following order
222 1. from incoming configuration:
223 1.1 using port mapping
224 using user and password from WIM
225 retrieving Lo0 and AS from switch
226 1.2 from 'switches' parameter,
227 if any parameter is not present
228 Lo0 and AS - it will be requested to the switch
229 2. Looking in the CloudVision inventory if not in configuration parameters
230 2.1 using the switches with the topology_type tag set to 'leaf'
231
232 All the search methods will be used
233 """
234 self.switches = {}
235 if self.__config and self.__config.get(self.__SERVICE_ENDPOINT_MAPPING):
236 for port in self.__config.get(self.__SERVICE_ENDPOINT_MAPPING):
237 switch_dpid = port.get(self.__SW_ID_PARAM)
238 if switch_dpid and switch_dpid not in self.switches:
239 self.switches[switch_dpid] = {'passwd': self.__passwd,
240 'ip': None,
241 'usr': self.__user,
242 'lo0': None,
243 'AS': None,
244 'serialNumber': None,
245 'mlagPeerDevice': None}
246
247 if self.__config and self.__config.get('switches'):
248 # Not directly from json, complete one by one
249 config_switches = self.__config.get('switches')
250 for cs, cs_content in config_switches.items():
251 if cs not in self.switches:
252 self.switches[cs] = {'passwd': self.__passwd,
253 'ip': None,
254 'usr': self.__user,
255 'lo0': None,
256 'AS': None,
257 'serialNumber': None,
258 'mlagPeerDevice': None}
259 if cs_content:
260 self.switches[cs].update(cs_content)
261
262 # Load the rest of the data
263 if self.client is None:
264 self.client = self.__connect()
265 self.__load_inventory()
266 if not self.switches:
267 self.__get_tags(self.__SWITCH_TAG_NAME, self.__SWITCH_TAG_VALUE)
268 for device in self.allDeviceFacts:
269 # get the switches whose topology_tag is 'leaf'
270 if device['serialNumber'] in self.cvp_tags:
271 if not self.switches.get(device['hostname']):
272 switch_data = {'passwd': self.__passwd,
273 'ip': device['ipAddress'],
274 'usr': self.__user,
275 'lo0': None,
276 'AS': None,
277 'serialNumber': None,
278 'mlagPeerDevice': None}
279 self.switches[device['hostname']] = switch_data
280 if len(self.switches) == 0:
281 self.logger.error("Unable to load Leaf switches from CVP")
282 return
283
284 # self.switches are switch objects, one for each switch in self.switches,
285 # used to make eAPI calls by using switch.py module
286 for s in self.switches:
287 for device in self.allDeviceFacts:
288 if device['hostname'] == s:
289 if not self.switches[s].get('ip'):
290 self.switches[s]['ip'] = device['ipAddress']
291 self.switches[s]['serialNumber'] = device['serialNumber']
292 break
293
294 # Each switch has a different loopback address,
295 # so it's a different configLet
296 if not self.switches[s].get('lo0'):
297 inf = self.__get_interface_ip(self.switches[s]['serialNumber'], self.__LOOPBACK_INTF)
298 self.switches[s]["lo0"] = inf.split('/')[0]
299 if not self.switches[s].get('AS'):
300 self.switches[s]["AS"] = self.__get_device_ASN(self.switches[s]['serialNumber'])
301 if self.topology in (self._VXLAN_MLAG, self._VLAN_MLAG):
302 for s in self.switches:
303 if not self.switches[s].get('mlagPeerDevice'):
304 self.switches[s]['mlagPeerDevice'] = self.__get_peer_MLAG(self.switches[s]['serialNumber'])
305
306 def __check_service(self, service_type, connection_points,
307 check_vlan=True, check_num_cp=True, kwargs=None):
308 """ Reviews the connection points elements looking for semantic errors in the incoming data
309 """
310 if service_type not in self.__supported_service_types:
311 raise Exception("The service '{}' is not supported. Only '{}' are accepted".format(
312 service_type,
313 self.__supported_service_types))
314
315 if check_num_cp:
316 if len(connection_points) < 2:
317 raise Exception(SdnError.CONNECTION_POINTS_SIZE)
318 if (len(connection_points) != self.__ELINE_num_connection_points and
319 service_type == self.__service_types_ELINE):
320 raise Exception(SdnError.CONNECTION_POINTS_SIZE)
321
322 if check_vlan:
323 vlan_id = ''
324 for cp in connection_points:
325 enc_type = cp.get(self.__ENCAPSULATION_TYPE_PARAM)
326 if (enc_type and
327 enc_type not in self.__supported_encapsulation_types):
328 raise Exception(SdnError.ENCAPSULATION_TYPE)
329 encap_info = cp.get(self.__ENCAPSULATION_INFO_PARAM)
330 cp_vlan_id = str(encap_info.get(self.__VLAN_PARAM))
331 if cp_vlan_id:
332 if not vlan_id:
333 vlan_id = cp_vlan_id
334 elif vlan_id != cp_vlan_id:
335 raise Exception(SdnError.VLAN_INCONSISTENT)
336 if not vlan_id:
337 raise Exception(SdnError.VLAN_NOT_PROVIDED)
338 if vlan_id in self.__get_srvVLANs():
339 raise Exception('VLAN {} already assigned to a connectivity service'.format(vlan_id))
340
341 # Commented out for as long as parameter isn't implemented
342 # bandwidth = kwargs.get(self.__BANDWIDTH_PARAM)
343 # if not isinstance(bandwidth, int):
344 # self.__exception(SdnError.BANDWIDTH, http_code=400)
345
346 # Commented out for as long as parameter isn't implemented
347 # backup = kwargs.get(self.__BACKUP_PARAM)
348 # if not isinstance(backup, bool):
349 # self.__exception(SdnError.BACKUP, http_code=400)
350
351 def check_credentials(self):
352 """Retrieves the CloudVision version information, as the easiest way
353 for testing the access to CloudVision API
354 """
355 try:
356 if self.client is None:
357 self.client = self.__connect()
358 result = self.client.api.get_cvp_info()
359 self.logger.debug(result)
360 except CvpLoginError as e:
361 self.logger.info(str(e))
362 self.client = None
363 raise SdnConnectorError(message=SdnError.UNAUTHORIZED + " " + str(e),
364 http_code=401) from e
365 except Exception as ex:
366 self.client = None
367 self.logger.error(str(ex))
368 raise SdnConnectorError(message=SdnError.INTERNAL_ERROR + " " + str(ex),
369 http_code=500) from ex
370
371 def get_connectivity_service_status(self, service_uuid, conn_info=None):
372 """Monitor the status of the connectivity service established
373 Arguments:
374 service_uuid (str): UUID of the connectivity service
375 conn_info (dict or None): Information returned by the connector
376 during the service creation/edition and subsequently stored in
377 the database.
378
379 Returns:
380 dict: JSON/YAML-serializable dict that contains a mandatory key
381 ``sdn_status`` associated with one of the following values::
382
383 {'sdn_status': 'ACTIVE'}
384 # The service is up and running.
385
386 {'sdn_status': 'INACTIVE'}
387 # The service was created, but the connector
388 # cannot determine yet if connectivity exists
389 # (ideally, the caller needs to wait and check again).
390
391 {'sdn_status': 'DOWN'}
392 # Connection was previously established,
393 # but an error/failure was detected.
394
395 {'sdn_status': 'ERROR'}
396 # An error occurred when trying to create the service/
397 # establish the connectivity.
398
399 {'sdn_status': 'BUILD'}
400 # Still trying to create the service, the caller
401 # needs to wait and check again.
402
403 Additionally ``error_msg``(**str**) and ``sdn_info``(**dict**)
404 keys can be used to provide additional status explanation or
405 new information available for the connectivity service.
406 """
407 try:
408 self.logger.debug("invoked get_connectivity_service_status '{}'".format(service_uuid))
409 if not service_uuid:
410 raise SdnConnectorError(message='No connection service UUID',
411 http_code=500)
412
413 self.__get_Connection()
414 if conn_info is None:
415 raise SdnConnectorError(message='No connection information for service UUID {}'.format(service_uuid),
416 http_code=500)
417
418 if 'configLetPerSwitch' in conn_info.keys():
419 c_info = conn_info
420 else:
421 c_info = None
422 cls_perSw = self.__get_serviceData(service_uuid,
423 conn_info['service_type'],
424 conn_info['vlan_id'],
425 c_info)
426
427 t_isCancelled = False
428 t_isFailed = False
429 t_isPending = False
430 failed_switches = []
431 for s in self.switches:
432 if len(cls_perSw[s]) > 0:
433 for cl in cls_perSw[s]:
434 # Fix 1030 SDN-ARISTA Key error note when deploy a NS
435 # Added protection to check that 'note' exists and additionally
436 # verify that it is managed by OSM
437 if (not cls_perSw[s][0]['config'] or
438 not cl.get('note') or
439 self.__MANAGED_BY_OSM not in cl['note']):
440 continue
441 note = cl['note']
442 t_id = note.split(self.__SEPARATOR)[1]
443 result = self.client.api.get_task_by_id(t_id)
444 if result['workOrderUserDefinedStatus'] == 'Completed':
445 continue
446 elif result['workOrderUserDefinedStatus'] == 'Cancelled':
447 t_isCancelled = True
448 elif result['workOrderUserDefinedStatus'] == 'Failed':
449 t_isFailed = True
450 else:
451 t_isPending = True
452 failed_switches.append(s)
453 if t_isCancelled:
454 error_msg = 'Some works were cancelled in switches: {}'.format(str(failed_switches))
455 sdn_status = 'DOWN'
456 elif t_isFailed:
457 error_msg = 'Some works failed in switches: {}'.format(str(failed_switches))
458 sdn_status = 'ERROR'
459 elif t_isPending:
460 error_msg = 'Some works are still under execution in switches: {}'.format(str(failed_switches))
461 sdn_status = 'BUILD'
462 else:
463 error_msg = ''
464 sdn_status = 'ACTIVE'
465 sdn_info = ''
466 return {'sdn_status': sdn_status,
467 'error_msg': error_msg,
468 'sdn_info': sdn_info}
469 except CvpLoginError as e:
470 self.logger.info(str(e))
471 self.client = None
472 raise SdnConnectorError(message=SdnError.UNAUTHORIZED + " " + str(e),
473 http_code=401) from e
474 except Exception as ex:
475 self.client = None
476 self.logger.error(str(ex), exc_info=True)
477 raise SdnConnectorError(message=str(ex) + " " + str(ex),
478 http_code=500) from ex
479
480 def create_connectivity_service(self, service_type, connection_points,
481 **kwargs):
482 """Stablish SDN/WAN connectivity between the endpoints
483 :param service_type:
484 (str): ``ELINE`` (L2), ``ELAN`` (L2), ``ETREE`` (L2), ``L3``.
485 :param connection_points: (list): each point corresponds to
486 an entry point to be connected. For WIM: from the DC
487 to the transport network.
488 For SDN: Compute/PCI to the transport network. One
489 connection point serves to identify the specific access and
490 some other service parameters, such as encapsulation type.
491 Each item of the list is a dict with:
492 "service_endpoint_id": (str)(uuid) Same meaning that for
493 'service_endpoint_mapping' (see __init__)
494 In case the config attribute mapping_not_needed is True,
495 this value is not relevant. In this case
496 it will contain the string "device_id:device_interface_id"
497 "service_endpoint_encapsulation_type": None, "dot1q", ...
498 "service_endpoint_encapsulation_info": (dict) with:
499 "vlan": ..., (int, present if encapsulation is dot1q)
500 "vni": ... (int, present if encapsulation is vxlan),
501 "peers": [(ipv4_1), (ipv4_2)] (present if
502 encapsulation is vxlan)
503 "mac": ...
504 "device_id": ..., same meaning that for
505 'service_endpoint_mapping' (see __init__)
506 "device_interface_id": same meaning that for
507 'service_endpoint_mapping' (see __init__)
508 "switch_dpid": ..., present if mapping has been found
509 for this device_id,device_interface_id
510 "switch_port": ... present if mapping has been found
511 for this device_id,device_interface_id
512 "service_mapping_info": present if mapping has
513 been found for this device_id,device_interface_id
514 :param kwargs: For future versions:
515 bandwidth (int): value in kilobytes
516 latency (int): value in milliseconds
517 Other QoS might be passed as keyword arguments.
518 :return: tuple: ``(service_id, conn_info)`` containing:
519 - *service_uuid* (str): UUID of the established
520 connectivity service
521 - *conn_info* (dict or None): Information to be
522 stored at the database (or ``None``).
523 This information will be provided to the
524 :meth:`~.edit_connectivity_service` and :obj:`~.delete`.
525 **MUST** be JSON/YAML-serializable (plain data structures).
526 :raises: SdnConnectorError: In case of error. Nothing should be
527 created in this case.
528 Provide the parameter http_code
529 """
530 try:
531 self.logger.debug("invoked create_connectivity_service '{}' ports: {}".
532 format(service_type, connection_points))
533 self.__get_Connection()
534 self.__check_service(service_type,
535 connection_points,
536 check_vlan=True,
537 kwargs=kwargs)
538 service_uuid = str(uuid.uuid4())
539
540 self.logger.info("Service with uuid {} created.".
541 format(service_uuid))
542 s_uid, s_connInf = self.__processConnection(
543 service_uuid,
544 service_type,
545 connection_points,
546 kwargs)
547 try:
548 self.__addMetadata(s_uid, service_type, s_connInf['vlan_id'])
549 except Exception:
550 pass
551
552 return (s_uid, s_connInf)
553 except CvpLoginError as e:
554 self.logger.info(str(e))
555 self.client = None
556 raise SdnConnectorError(message=SdnError.UNAUTHORIZED + " " + str(e),
557 http_code=401) from e
558 except SdnConnectorError as sde:
559 raise sde
560 except ValueError as err:
561 self.client = None
562 self.logger.error(str(err), exc_info=True)
563 raise SdnConnectorError(message=str(err),
564 http_code=500) from err
565 except Exception as ex:
566 self.client = None
567 self.logger.error(str(ex), exc_info=True)
568 if self.raiseException:
569 raise ex
570 raise SdnConnectorError(message=str(ex),
571 http_code=500) from ex
572
573 def __processConnection(self,
574 service_uuid,
575 service_type,
576 connection_points,
577 kwargs):
578 """
579 Invoked from creation and edit methods
580
581 Process the connection points array,
582 creating a set of configuration per switch where it has to be applied
583 for creating the configuration, the switches have to be queried for obtaining:
584 - the loopback address
585 - the BGP ASN (autonomous system number)
586 - the interface name of the MAC address to add in the connectivity service
587 Once the new configuration is ready, the __updateConnection method is invoked for appling the changes
588 """
589 try:
590 cls_perSw = {}
591 cls_cp = {}
592 cl_bgp = {}
593 for s in self.switches:
594 cls_perSw[s] = []
595 cls_cp[s] = []
596 vlan_processed = False
597 vlan_id = ''
598 i = 0
599 processed_connection_points = []
600 for cp in connection_points:
601 i += 1
602 encap_info = cp.get(self.__ENCAPSULATION_INFO_PARAM)
603 if not vlan_processed:
604 vlan_id = str(encap_info.get(self.__VLAN_PARAM))
605 if not vlan_id:
606 continue
607 vni_id = encap_info.get(self.__VNI_PARAM)
608 if not vni_id:
609 vni_id = str(10000 + int(vlan_id))
610
611 if service_type == self.__service_types_ELAN:
612 cl_vlan = self.clC.getElan_vlan(service_uuid,
613 vlan_id,
614 vni_id)
615 else:
616 cl_vlan = self.clC.getEline_vlan(service_uuid,
617 vlan_id,
618 vni_id)
619 vlan_processed = True
620
621 encap_type = cp.get(self.__ENCAPSULATION_TYPE_PARAM)
622 switch_id = encap_info.get(self.__SW_ID_PARAM)
623 interface = encap_info.get(self.__SW_PORT_PARAM)
624 switches = [{'name': switch_id, 'interface': interface}]
625
626 # remove those connections that are equal. This happens when several sriovs are located in the same
627 # compute node interface, that is, in the same switch and interface
628 switches = [x for x in switches if x not in processed_connection_points]
629 if not switches:
630 continue
631 processed_connection_points += switches
632 for switch in switches:
633 if not interface:
634 raise SdnConnectorError(
635 message="Connection point switch port empty for switch_dpid {}".format(switch_id),
636 http_code=406)
637 # it should be only one switch where the mac is attached
638 if encap_type == 'dot1q':
639 # SRIOV configLet for Leaf switch mac's attached to
640 if service_type == self.__service_types_ELAN:
641 cl_encap = self.clC.getElan_sriov(service_uuid, interface, vlan_id, i)
642 else:
643 cl_encap = self.clC.getEline_sriov(service_uuid, interface, vlan_id, i)
644 elif not encap_type:
645 # PT configLet for Leaf switch attached to the mac
646 if service_type == self.__service_types_ELAN:
647 cl_encap = self.clC.getElan_passthrough(service_uuid,
648 interface,
649 vlan_id, i)
650 else:
651 cl_encap = self.clC.getEline_passthrough(service_uuid,
652 interface,
653 vlan_id, i)
654 if cls_cp.get(switch['name']):
655 cls_cp[switch['name']] = str(cls_cp[switch['name']]) + cl_encap
656 else:
657 cls_cp[switch['name']] = cl_encap
658
659 # at least 1 connection point has to be received
660 if not vlan_processed:
661 raise SdnConnectorError(message=SdnError.UNSUPPORTED_FEATURE,
662 http_code=406)
663
664 for s in self.switches:
665 # for cl in cp_configLets:
666 cl_name = (self.__OSM_PREFIX +
667 s +
668 self.__SEPARATOR + service_type + str(vlan_id) +
669 self.__SEPARATOR + service_uuid)
670 cl_config = ''
671 # Apply BGP configuration only for VXLAN topologies
672 if self.topology in (self._VXLAN_MLAG, self._VXLAN):
673 if service_type == self.__service_types_ELAN:
674 cl_bgp[s] = self.clC.getElan_bgp(service_uuid,
675 vlan_id,
676 vni_id,
677 self.switches[s]['lo0'],
678 self.switches[s]['AS'])
679 else:
680 cl_bgp[s] = self.clC.getEline_bgp(service_uuid,
681 vlan_id,
682 vni_id,
683 self.switches[s]['lo0'],
684 self.switches[s]['AS'])
685 else:
686 cl_bgp[s] = ''
687
688 if not cls_cp.get(s):
689 # Apply VLAN configuration to peer MLAG switch,
690 # only necessary when there are no connection points in the switch
691 if self.topology in (self._VXLAN_MLAG, self._VLAN_MLAG):
692 for p in self.switches:
693 if self.switches[p]['mlagPeerDevice'] == s:
694 if cls_cp.get(p):
695 cl_config = str(cl_vlan)
696 else:
697 cl_config = str(cl_vlan) + str(cl_bgp[s]) + str(cls_cp[s])
698
699 cls_perSw[s] = [{'name': cl_name, 'config': cl_config}]
700
701 allLeafConfigured, allLeafModified = self.__updateConnection(cls_perSw)
702
703 conn_info = {
704 "uuid": service_uuid,
705 "status": "BUILD",
706 "service_type": service_type,
707 "vlan_id": vlan_id,
708 "connection_points": connection_points,
709 "configLetPerSwitch": cls_perSw,
710 'allLeafConfigured': allLeafConfigured,
711 'allLeafModified': allLeafModified}
712
713 return service_uuid, conn_info
714 except Exception as ex:
715 self.logger.debug("Exception processing connection {}: {}".
716 format(service_uuid, str(ex)))
717 raise ex
718
719 def __updateConnection(self, cls_perSw):
720 """ Invoked in the creation and modification
721
722 checks if the new connection points config is:
723 - already in the Cloud Vision, the configLet is modified, and applied to the switch,
724 executing the corresponding task
725 - if it has to be removed:
726 then configuration has to be removed from the switch executing the corresponding task,
727 before trying to remove the configuration
728 - created, the configuration set is created, associated to the switch, and the associated
729 task to the configLet modification executed
730 In case of any error, rollback is executed, removing the created elements, and restoring to the
731 previous state.
732 """
733 try:
734 allLeafConfigured = {}
735 allLeafModified = {}
736
737 for s in self.switches:
738 allLeafConfigured[s] = False
739 allLeafModified[s] = False
740 cl_toDelete = []
741 for s in self.switches:
742 toDelete_in_cvp = False
743 if not (cls_perSw.get(s) and cls_perSw[s][0].get('config')):
744 # when there is no configuration, means that there is no interface
745 # in the switch to be connected, so the configLet has to be removed from CloudVision
746 # after removing the ConfigLet from the switch if it was already there
747
748 # get config let name and key
749 cl = cls_perSw[s]
750 try:
751 cvp_cl = self.client.api.get_configlet_by_name(cl[0]['name'])
752 # remove configLet
753 cl_toDelete.append(cvp_cl)
754 cl[0] = cvp_cl
755 toDelete_in_cvp = True
756 except CvpApiError as error:
757 if "Entity does not exist" in error.msg:
758 continue
759 else:
760 raise error
761 # remove configLet from device
762 else:
763 res = self.__configlet_modify(cls_perSw[s])
764 allLeafConfigured[s] = res[0]
765 if not allLeafConfigured[s]:
766 continue
767 cl = cls_perSw[s]
768 res = self.__device_modify(
769 device_to_update=s,
770 new_configlets=cl,
771 delete=toDelete_in_cvp)
772 if "errorMessage" in str(res):
773 raise Exception(str(res))
774 self.logger.info("Device {} modify result {}".format(s, res))
775 for t_id in res[1]['tasks']:
776 if not toDelete_in_cvp:
777 note_msg = "{}{}{}{}##".format(self.__MANAGED_BY_OSM,
778 self.__SEPARATOR,
779 t_id,
780 self.__SEPARATOR)
781 self.client.api.add_note_to_configlet(
782 cls_perSw[s][0]['key'],
783 note_msg)
784 cls_perSw[s][0]['note'] = note_msg
785 tasks = {t_id: {'workOrderId': t_id}}
786 self.__exec_task(tasks, self.__EXC_TASK_EXEC_WAIT)
787 # with just one configLet assigned to a device,
788 # delete all if there are errors in next loops
789 if not toDelete_in_cvp:
790 allLeafModified[s] = True
791 if len(cl_toDelete) > 0:
792 self.__configlet_modify(cl_toDelete, delete=True)
793
794 return allLeafConfigured, allLeafModified
795 except Exception as ex:
796 try:
797 self.__rollbackConnection(cls_perSw,
798 allLeafConfigured,
799 allLeafModified)
800 except Exception as e:
801 self.logger.error("Exception rolling back in updating connection: {}".
802 format(e), exc_info=True)
803 raise ex
804
805 def __rollbackConnection(self,
806 cls_perSw,
807 allLeafConfigured,
808 allLeafModified):
809 """ Removes the given configLet from the devices and then remove the configLets
810 """
811 for s in self.switches:
812 if allLeafModified[s]:
813 try:
814 res = self.__device_modify(
815 device_to_update=s,
816 new_configlets=cls_perSw[s],
817 delete=True)
818 if "errorMessage" in str(res):
819 raise Exception(str(res))
820 tasks = dict()
821 for t_id in res[1]['tasks']:
822 tasks[t_id] = {'workOrderId': t_id}
823 self.__exec_task(tasks)
824 self.logger.info("Device {} modify result {}".format(s, res))
825 except Exception as e:
826 self.logger.error('Error removing configlets from device {}: {}'.format(s, e))
827 pass
828 for s in self.switches:
829 if allLeafConfigured[s]:
830 self.__configlet_modify(cls_perSw[s], delete=True)
831
832 def __exec_task(self, tasks, tout=10):
833 if self.taskC is None:
834 self.__connect()
835 data = self.taskC.update_all_tasks(tasks).values()
836 self.taskC.task_action(data, tout, 'executed')
837
838 def __device_modify(self, device_to_update, new_configlets, delete):
839 """ Updates the devices (switches) adding or removing the configLet,
840 the tasks Id's associated to the change are returned
841 """
842 self.logger.info('Enter in __device_modify delete: {}'.format(delete))
843 updated = []
844 changed = False
845 # Task Ids that have been identified during device actions
846 newTasks = []
847
848 if (len(new_configlets) == 0 or
849 device_to_update is None or
850 len(device_to_update) == 0):
851 data = {'updated': updated, 'tasks': newTasks}
852 return [changed, data]
853
854 self.__load_inventory()
855
856 allDeviceFacts = self.allDeviceFacts
857 # Work through Devices list adding device specific information
858 device = None
859 for try_device in allDeviceFacts:
860 # Add Device Specific Configlets
861 # self.logger.debug(device)
862 if try_device['hostname'] not in device_to_update:
863 continue
864 dev_cvp_configlets = self.client.api.get_configlets_by_device_id(
865 try_device['systemMacAddress'])
866 # self.logger.debug(dev_cvp_configlets)
867 try_device['deviceSpecificConfiglets'] = []
868 for cvp_configlet in dev_cvp_configlets:
869 if int(cvp_configlet['containerCount']) == 0:
870 try_device['deviceSpecificConfiglets'].append(
871 {'name': cvp_configlet['name'],
872 'key': cvp_configlet['key']})
873 # self.logger.debug(device)
874 device = try_device
875 break
876
877 # Check assigned configlets
878 device_update = False
879 add_configlets = []
880 remove_configlets = []
881 update_devices = []
882
883 if delete:
884 for cvp_configlet in device['deviceSpecificConfiglets']:
885 for cl in new_configlets:
886 if cvp_configlet['name'] == cl['name']:
887 remove_configlets.append(cvp_configlet)
888 device_update = True
889 else:
890 for configlet in new_configlets:
891 if configlet not in device['deviceSpecificConfiglets']:
892 add_configlets.append(configlet)
893 device_update = True
894 if device_update:
895 update_devices.append({'hostname': device['hostname'],
896 'configlets': [add_configlets,
897 remove_configlets],
898 'device': device})
899 self.logger.info("Device to modify: {}".format(update_devices))
900
901 up_device = update_devices[0]
902 cl_toAdd = up_device['configlets'][0]
903 cl_toDel = up_device['configlets'][1]
904 # Update Configlets
905 try:
906 if delete and len(cl_toDel) > 0:
907 r = self.client.api.remove_configlets_from_device(
908 'OSM',
909 up_device['device'],
910 cl_toDel,
911 create_task=True)
912 dev_action = r
913 self.logger.debug("remove_configlets_from_device {} {}".format(dev_action, cl_toDel))
914 elif len(cl_toAdd) > 0:
915 r = self.client.api.apply_configlets_to_device(
916 'OSM',
917 up_device['device'],
918 cl_toAdd,
919 create_task=True)
920 dev_action = r
921 self.logger.debug("apply_configlets_to_device {} {}".format(dev_action, cl_toAdd))
922
923 except Exception as error:
924 errorMessage = str(error)
925 msg = "errorMessage: Device {} Configlets couldnot be updated: {}".format(
926 up_device['hostname'], errorMessage)
927 raise SdnConnectorError(msg) from error
928 else:
929 if "errorMessage" in str(dev_action):
930 m = "Device {} Configlets update fail: {}".format(
931 up_device['name'], dev_action['errorMessage'])
932 raise SdnConnectorError(m)
933 else:
934 changed = True
935 if 'taskIds' in str(dev_action):
936 # Fix 1030 SDN-ARISTA Key error note when deploy a NS
937 if not dev_action['data']['taskIds']:
938 raise SdnConnectorError("No taskIds found: Device {} Configlets could not be updated".format(
939 up_device['hostname']))
940 for taskId in dev_action['data']['taskIds']:
941 updated.append({
942 up_device['hostname']: "Configlets-{}".format(taskId)})
943 newTasks.append(taskId)
944 else:
945 updated.append({up_device['hostname']:
946 "Configlets-No_Specific_Tasks"})
947 data = {'updated': updated, 'tasks': newTasks}
948 return [changed, data]
949
950 def __configlet_modify(self, configletsToApply, delete=False):
951 ''' adds/update or delete the provided configLets
952 :param configletsToApply: list of configLets to apply
953 :param delete: flag to indicate if the configLets have to be deleted
954 from Cloud Vision Portal
955 :return: data: dict of module actions and taskIDs
956 '''
957 self.logger.info('Enter in __configlet_modify delete:{}'.format(
958 delete))
959
960 # Compare configlets against cvp_facts-configlets
961 changed = False
962 checked = []
963 deleted = []
964 updated = []
965 new = []
966
967 for cl in configletsToApply:
968 found_in_cvp = False
969 to_delete = False
970 to_update = False
971 to_create = False
972 to_check = False
973 try:
974 cvp_cl = self.client.api.get_configlet_by_name(cl['name'])
975 cl['key'] = cvp_cl['key']
976 cl['note'] = cvp_cl['note']
977 found_in_cvp = True
978 except CvpApiError as error:
979 if "Entity does not exist" in error.msg:
980 pass
981 else:
982 raise error
983
984 if delete:
985 if found_in_cvp:
986 to_delete = True
987 configlet = {'name': cvp_cl['name'],
988 'data': cvp_cl}
989 else:
990 if found_in_cvp:
991 cl_compare = self.__compare(cl['config'],
992 cvp_cl['config'])
993 # compare function returns a floating point number
994 if cl_compare[0] != 100.0:
995 to_update = True
996 configlet = {'name': cl['name'],
997 'data': cvp_cl,
998 'config': cl['config']}
999 else:
1000 to_check = True
1001 configlet = {'name': cl['name'],
1002 'key': cvp_cl['key'],
1003 'data': cvp_cl,
1004 'config': cl['config']}
1005 else:
1006 to_create = True
1007 configlet = {'name': cl['name'],
1008 'config': cl['config']}
1009 try:
1010 if to_delete:
1011 operation = 'delete'
1012 resp = self.client.api.delete_configlet(
1013 configlet['data']['name'],
1014 configlet['data']['key'])
1015 elif to_update:
1016 operation = 'update'
1017 resp = self.client.api.update_configlet(
1018 configlet['config'],
1019 configlet['data']['key'],
1020 configlet['data']['name'],
1021 wait_task_ids=True)
1022 elif to_create:
1023 operation = 'create'
1024 resp = self.client.api.add_configlet(
1025 configlet['name'],
1026 configlet['config'])
1027 else:
1028 operation = 'checked'
1029 resp = 'checked'
1030 except Exception as error:
1031 errorMessage = str(error).split(':')[-1]
1032 message = "Configlet {} cannot be {}: {}".format(
1033 cl['name'], operation, errorMessage)
1034 if to_delete:
1035 deleted.append({configlet['name']: message})
1036 elif to_update:
1037 updated.append({configlet['name']: message})
1038 elif to_create:
1039 new.append({configlet['name']: message})
1040 elif to_check:
1041 checked.append({configlet['name']: message})
1042
1043 else:
1044 if "error" in str(resp).lower():
1045 message = "Configlet {} cannot be deleted: {}".format(
1046 cl['name'], resp['errorMessage'])
1047 if to_delete:
1048 deleted.append({configlet['name']: message})
1049 elif to_update:
1050 updated.append({configlet['name']: message})
1051 elif to_create:
1052 new.append({configlet['name']: message})
1053 elif to_check:
1054 checked.append({configlet['name']: message})
1055 else:
1056 if to_delete:
1057 changed = True
1058 deleted.append({configlet['name']: "success"})
1059 elif to_update:
1060 changed = True
1061 updated.append({configlet['name']: "success"})
1062 elif to_create:
1063 changed = True
1064 cl['key'] = resp # This key is used in API call deviceApplyConfigLet FGA
1065 new.append({configlet['name']: "success"})
1066 elif to_check:
1067 changed = False
1068 checked.append({configlet['name']: "success"})
1069
1070 data = {'new': new, 'updated': updated, 'deleted': deleted, 'checked': checked}
1071 return [changed, data]
1072
1073 def __get_configletsDevices(self, configlets):
1074 for s in self.switches:
1075 configlet = configlets[s]
1076 # Add applied Devices
1077 if len(configlet) > 0:
1078 configlet['devices'] = []
1079 applied_devices = self.client.api.get_applied_devices(
1080 configlet['name'])
1081 for device in applied_devices['data']:
1082 configlet['devices'].append(device['hostName'])
1083
1084 def __get_serviceData(self, service_uuid, service_type, vlan_id, conn_info=None):
1085 cls_perSw = {}
1086 for s in self.switches:
1087 cls_perSw[s] = []
1088 if not conn_info:
1089 srv_cls = self.__get_serviceConfigLets(service_uuid,
1090 service_type,
1091 vlan_id)
1092 self.__get_configletsDevices(srv_cls)
1093 for s in self.switches:
1094 cl = srv_cls[s]
1095 if len(cl) > 0:
1096 for dev in cl['devices']:
1097 cls_perSw[dev].append(cl)
1098 else:
1099 cls_perSw = conn_info['configLetPerSwitch']
1100 return cls_perSw
1101
1102 def delete_connectivity_service(self, service_uuid, conn_info=None):
1103 """
1104 Disconnect multi-site endpoints previously connected
1105
1106 :param service_uuid: The one returned by create_connectivity_service
1107 :param conn_info: The one returned by last call to 'create_connectivity_service' or 'edit_connectivity_service'
1108 if they do not return None
1109 :return: None
1110 :raises: SdnConnectorException: In case of error. The parameter http_code must be filled
1111 """
1112 try:
1113 self.logger.debug('invoked delete_connectivity_service {}'.
1114 format(service_uuid))
1115 if not service_uuid:
1116 raise SdnConnectorError(message='No connection service UUID',
1117 http_code=500)
1118
1119 self.__get_Connection()
1120 if conn_info is None:
1121 raise SdnConnectorError(message='No connection information for service UUID {}'.format(service_uuid),
1122 http_code=500)
1123 c_info = None
1124 cls_perSw = self.__get_serviceData(service_uuid,
1125 conn_info['service_type'],
1126 conn_info['vlan_id'],
1127 c_info)
1128 allLeafConfigured = {}
1129 allLeafModified = {}
1130 for s in self.switches:
1131 allLeafConfigured[s] = True
1132 allLeafModified[s] = True
1133 found_in_cvp = False
1134 for s in self.switches:
1135 if cls_perSw[s]:
1136 found_in_cvp = True
1137 if found_in_cvp:
1138 self.__rollbackConnection(cls_perSw,
1139 allLeafConfigured,
1140 allLeafModified)
1141 else:
1142 # if the service is not defined in Cloud Vision, return a 404 - NotFound error
1143 raise SdnConnectorError(message='Service {} was not found in Arista Cloud Vision {}'.
1144 format(service_uuid, self.__wim_url),
1145 http_code=404)
1146 self.__removeMetadata(service_uuid)
1147 except CvpLoginError as e:
1148 self.logger.info(str(e))
1149 self.client = None
1150 raise SdnConnectorError(message=SdnError.UNAUTHORIZED + " " + str(e),
1151 http_code=401) from e
1152 except SdnConnectorError as sde:
1153 raise sde
1154 except Exception as ex:
1155 self.client = None
1156 self.logger.error(ex)
1157 if self.raiseException:
1158 raise ex
1159 raise SdnConnectorError(message=SdnError.INTERNAL_ERROR + " " + str(ex),
1160 http_code=500) from ex
1161
1162 def __addMetadata(self, service_uuid, service_type, vlan_id):
1163 """ Adds the connectivity service from 'OSM_metadata' configLet
1164 """
1165 found_in_cvp = False
1166 try:
1167 cvp_cl = self.client.api.get_configlet_by_name(self.__OSM_METADATA)
1168 found_in_cvp = True
1169 except CvpApiError as error:
1170 if "Entity does not exist" in error.msg:
1171 pass
1172 else:
1173 raise error
1174 try:
1175 new_serv = '{} {} {} {}\n'.format(self.__METADATA_PREFIX, service_type, vlan_id, service_uuid)
1176
1177 if found_in_cvp:
1178 cl_config = cvp_cl['config'] + new_serv
1179 else:
1180 cl_config = new_serv
1181 cl_meta = [{'name': self.__OSM_METADATA, 'config': cl_config}]
1182 self.__configlet_modify(cl_meta)
1183 except Exception as e:
1184 self.logger.error('Error in setting metadata in CloudVision from OSM for service {}: {}'.
1185 format(service_uuid, str(e)))
1186 pass
1187
1188 def __removeMetadata(self, service_uuid):
1189 """ Removes the connectivity service from 'OSM_metadata' configLet
1190 """
1191 found_in_cvp = False
1192 try:
1193 cvp_cl = self.client.api.get_configlet_by_name(self.__OSM_METADATA)
1194 found_in_cvp = True
1195 except CvpApiError as error:
1196 if "Entity does not exist" in error.msg:
1197 pass
1198 else:
1199 raise error
1200 try:
1201 if found_in_cvp:
1202 if service_uuid in cvp_cl['config']:
1203 cl_config = ''
1204 for line in cvp_cl['config'].split('\n'):
1205 if service_uuid in line:
1206 continue
1207 else:
1208 cl_config = cl_config + line
1209 cl_meta = [{'name': self.__OSM_METADATA, 'config': cl_config}]
1210 self.__configlet_modify(cl_meta)
1211 except Exception as e:
1212 self.logger.error('Error in removing metadata in CloudVision from OSM for service {}: {}'.
1213 format(service_uuid, str(e)))
1214 pass
1215
1216 def edit_connectivity_service(self,
1217 service_uuid,
1218 conn_info=None,
1219 connection_points=None,
1220 **kwargs):
1221 """ Change an existing connectivity service.
1222
1223 This method's arguments and return value follow the same convention as
1224 :meth:`~.create_connectivity_service`.
1225
1226 :param service_uuid: UUID of the connectivity service.
1227 :param conn_info: (dict or None): Information previously returned
1228 by last call to create_connectivity_service
1229 or edit_connectivity_service
1230 :param connection_points: (list): If provided, the old list of
1231 connection points will be replaced.
1232 :param kwargs: Same meaning that create_connectivity_service
1233 :return: dict or None: Information to be updated and stored at
1234 the database.
1235 When ``None`` is returned, no information should be changed.
1236 When an empty dict is returned, the database record will
1237 be deleted.
1238 **MUST** be JSON/YAML-serializable (plain data structures).
1239 Raises:
1240 SdnConnectorError: In case of error.
1241 """
1242 try:
1243 self.logger.debug('invoked edit_connectivity_service for service {}. ports: {}'.format(service_uuid,
1244 connection_points))
1245
1246 if not service_uuid:
1247 raise SdnConnectorError(message='Unable to perform operation, missing or empty uuid',
1248 http_code=500)
1249 if not conn_info:
1250 raise SdnConnectorError(message='Unable to perform operation, missing or empty connection information',
1251 http_code=500)
1252
1253 if connection_points is None:
1254 return None
1255
1256 self.__get_Connection()
1257
1258 cls_currentPerSw = conn_info['configLetPerSwitch']
1259 service_type = conn_info['service_type']
1260
1261 self.__check_service(service_type,
1262 connection_points,
1263 check_vlan=False,
1264 check_num_cp=False,
1265 kwargs=kwargs)
1266
1267 s_uid, s_connInf = self.__processConnection(
1268 service_uuid,
1269 service_type,
1270 connection_points,
1271 kwargs)
1272 self.logger.info("Service with uuid {} configuration updated".
1273 format(s_uid))
1274 return s_connInf
1275 except CvpLoginError as e:
1276 self.logger.info(str(e))
1277 self.client = None
1278 raise SdnConnectorError(message=SdnError.UNAUTHORIZED + " " + str(e),
1279 http_code=401) from e
1280 except SdnConnectorError as sde:
1281 raise sde
1282 except Exception as ex:
1283 try:
1284 # Add previous
1285 # TODO check if there are pending task, and cancel them before restoring
1286 self.__updateConnection(cls_currentPerSw)
1287 except Exception as e:
1288 self.logger.error("Unable to restore configuration in service {} after an error in the configuration"
1289 " updated: {}".format(service_uuid, str(e)))
1290 if self.raiseException:
1291 raise ex
1292 raise SdnConnectorError(message=str(ex),
1293 http_code=500) from ex
1294
1295 def clear_all_connectivity_services(self):
1296 """ Removes all connectivity services from Arista CloudVision with two steps:
1297 - retrives all the services from Arista CloudVision
1298 - removes each service
1299 """
1300 try:
1301 self.logger.debug('invoked AristaImpl ' +
1302 'clear_all_connectivity_services')
1303 self.__get_Connection()
1304 s_list = self.__get_srvUUIDs()
1305 for serv in s_list:
1306 conn_info = {}
1307 conn_info['service_type'] = serv['type']
1308 conn_info['vlan_id'] = serv['vlan']
1309
1310 self.delete_connectivity_service(serv['uuid'], conn_info)
1311 except CvpLoginError as e:
1312 self.logger.info(str(e))
1313 self.client = None
1314 raise SdnConnectorError(message=SdnError.UNAUTHORIZED + " " + str(e),
1315 http_code=401) from e
1316 except SdnConnectorError as sde:
1317 raise sde
1318 except Exception as ex:
1319 self.client = None
1320 self.logger.error(ex)
1321 if self.raiseException:
1322 raise ex
1323 raise SdnConnectorError(message=SdnError.INTERNAL_ERROR + " " + str(ex),
1324 http_code=500) from ex
1325
1326 def get_all_active_connectivity_services(self):
1327 """ Return the uuid of all the active connectivity services with two steps:
1328 - retrives all the services from Arista CloudVision
1329 - retrives the status of each server
1330 """
1331 try:
1332 self.logger.debug('invoked AristaImpl {}'.format(
1333 'get_all_active_connectivity_services'))
1334 self.__get_Connection()
1335 s_list = self.__get_srvUUIDs()
1336 result = []
1337 for serv in s_list:
1338 conn_info = {}
1339 conn_info['service_type'] = serv['type']
1340 conn_info['vlan_id'] = serv['vlan']
1341
1342 status = self.get_connectivity_service_status(serv['uuid'], conn_info)
1343 if status['sdn_status'] == 'ACTIVE':
1344 result.append(serv['uuid'])
1345 return result
1346 except CvpLoginError as e:
1347 self.logger.info(str(e))
1348 self.client = None
1349 raise SdnConnectorError(message=SdnError.UNAUTHORIZED + " " + str(e),
1350 http_code=401) from e
1351 except SdnConnectorError as sde:
1352 raise sde
1353 except Exception as ex:
1354 self.client = None
1355 self.logger.error(ex)
1356 if self.raiseException:
1357 raise ex
1358 raise SdnConnectorError(message=SdnError.INTERNAL_ERROR,
1359 http_code=500) from ex
1360
1361 def __get_serviceConfigLets(self, service_uuid, service_type, vlan_id):
1362 """ Return the configLet's associated with a connectivity service,
1363 There should be one, as maximum, per device (switch) for a given
1364 connectivity service
1365 """
1366 srv_cls = {}
1367 for s in self.switches:
1368 srv_cls[s] = []
1369 found_in_cvp = False
1370 name = (self.__OSM_PREFIX +
1371 s +
1372 self.__SEPARATOR + service_type + str(vlan_id) +
1373 self.__SEPARATOR + service_uuid)
1374 try:
1375 cvp_cl = self.client.api.get_configlet_by_name(name)
1376 found_in_cvp = True
1377 except CvpApiError as error:
1378 if "Entity does not exist" in error.msg:
1379 pass
1380 else:
1381 raise error
1382 if found_in_cvp:
1383 srv_cls[s] = cvp_cl
1384 return srv_cls
1385
1386 def __get_srvVLANs(self):
1387 """ Returns a list with all the VLAN id's used in the connectivity services managed
1388 in tha Arista CloudVision by checking the 'OSM_metadata' configLet where this
1389 information is stored
1390 """
1391 found_in_cvp = False
1392 try:
1393 cvp_cl = self.client.api.get_configlet_by_name(self.__OSM_METADATA)
1394 found_in_cvp = True
1395 except CvpApiError as error:
1396 if "Entity does not exist" in error.msg:
1397 pass
1398 else:
1399 raise error
1400 s_vlan_list = []
1401 if found_in_cvp:
1402 lines = cvp_cl['config'].split('\n')
1403 for line in lines:
1404 if self.__METADATA_PREFIX in line:
1405 s_vlan = line.split(' ')[3]
1406 else:
1407 continue
1408 if (s_vlan is not None and
1409 len(s_vlan) > 0 and
1410 s_vlan not in s_vlan_list):
1411 s_vlan_list.append(s_vlan)
1412
1413 return s_vlan_list
1414
1415 def __get_srvUUIDs(self):
1416 """ Retrieves all the connectivity services, managed in tha Arista CloudVision
1417 by checking the 'OSM_metadata' configLet where this information is stored
1418 """
1419 found_in_cvp = False
1420 try:
1421 cvp_cl = self.client.api.get_configlet_by_name(self.__OSM_METADATA)
1422 found_in_cvp = True
1423 except CvpApiError as error:
1424 if "Entity does not exist" in error.msg:
1425 pass
1426 else:
1427 raise error
1428 serv_list = []
1429 if found_in_cvp:
1430 lines = cvp_cl['config'].split('\n')
1431 for line in lines:
1432 if self.__METADATA_PREFIX in line:
1433 line = line.split(' ')
1434 serv = {'uuid': line[4], 'type': line[2], 'vlan': line[3]}
1435 else:
1436 continue
1437 if (serv is not None and
1438 len(serv) > 0 and
1439 serv not in serv_list):
1440 serv_list.append(serv)
1441
1442 return serv_list
1443
1444 def __get_Connection(self):
1445 """ Open a connection with Arista CloudVision,
1446 invoking the version retrival as test
1447 """
1448 try:
1449 if self.client is None:
1450 self.client = self.__connect()
1451 self.client.api.get_cvp_info()
1452 except (CvpSessionLogOutError, RequestException) as e:
1453 self.logger.debug("Connection error '{}'. Reconnecting".format(e))
1454 self.client = self.__connect()
1455 self.client.api.get_cvp_info()
1456
1457 def __connect(self):
1458 ''' Connects to CVP device using user provided credentials from initialization.
1459 :return: CvpClient object with connection instantiated.
1460 '''
1461 client = CvpClient()
1462 protocol, _, rest_url = self.__wim_url.rpartition("://")
1463 host, _, port = rest_url.partition(":")
1464 if port and port.endswith("/"):
1465 port = int(port[:-1])
1466 elif port:
1467 port = int(port)
1468 else:
1469 port = 443
1470
1471 client.connect([host],
1472 self.__user,
1473 self.__passwd,
1474 protocol=protocol or "https",
1475 port=port,
1476 connect_timeout=2)
1477 client.api = CvpApi(client, request_timeout=self.__API_REQUEST_TOUT)
1478 self.taskC = AristaCVPTask(client.api)
1479 return client
1480
1481 def __compare(self, fromText, toText, lines=10):
1482 """ Compare text string in 'fromText' with 'toText' and produce
1483 diffRatio - a score as a float in the range [0, 1] 2.0*M / T
1484 T is the total number of elements in both sequences,
1485 M is the number of matches.
1486 Score - 1.0 if the sequences are identical, and
1487 0.0 if they have nothing in common.
1488 unified diff list
1489 Code Meaning
1490 '- ' line unique to sequence 1
1491 '+ ' line unique to sequence 2
1492 ' ' line common to both sequences
1493 '? ' line not present in either input sequence
1494 """
1495 fromlines = fromText.splitlines(1)
1496 tolines = toText.splitlines(1)
1497 diff = list(difflib.unified_diff(fromlines, tolines, n=lines))
1498 textComp = difflib.SequenceMatcher(None, fromText, toText)
1499 diffRatio = round(textComp.quick_ratio()*100, 2)
1500 return [diffRatio, diff]
1501
1502 def __load_inventory(self):
1503 """ Get Inventory Data for All Devices (aka switches) from the Arista CloudVision
1504 """
1505 if not self.cvp_inventory:
1506 self.cvp_inventory = self.client.api.get_inventory()
1507 self.allDeviceFacts = []
1508 for device in self.cvp_inventory:
1509 self.allDeviceFacts.append(device)
1510
1511 def __get_tags(self, name, value):
1512 if not self.cvp_tags:
1513 self.cvp_tags = []
1514 url = '/api/v1/rest/analytics/tags/labels/devices/{}/value/{}/elements'.format(name, value)
1515 self.logger.debug('get_tags: URL {}'.format(url))
1516 data = self.client.get(url, timeout=self.__API_REQUEST_TOUT)
1517 for dev in data['notifications']:
1518 for elem in dev['updates']:
1519 self.cvp_tags.append(elem)
1520 self.logger.debug('Available devices with tag_name {} - value {}: {} '.format(name, value, self.cvp_tags))
1521
1522 def __get_interface_ip(self, device_id, interface):
1523 url = '/api/v1/rest/{}/Sysdb/ip/config/ipIntfConfig/{}/'.format(device_id, interface)
1524 self.logger.debug('get_interface_ip: URL {}'.format(url))
1525 try:
1526 data = self.client.get(url, timeout=self.__API_REQUEST_TOUT)
1527 return data['notifications'][0]['updates']['addrWithMask']['value'].split('/')[0]
1528 except Exception:
1529 raise SdnConnectorError("Invalid response from url {}: data {}".format(url, data))
1530
1531 def __get_device_ASN(self, device_id):
1532 url = '/api/v1/rest/{}/Sysdb/routing/bgp/config/'.format(device_id)
1533 self.logger.debug('get_device_ASN: URL {}'.format(url))
1534 try:
1535 data = self.client.get(url, timeout=self.__API_REQUEST_TOUT)
1536 return data['notifications'][0]['updates']['asNumber']['value']['value']['int']
1537 except Exception:
1538 raise SdnConnectorError("Invalid response from url {}: data {}".format(url, data))
1539
1540 def __get_peer_MLAG(self, device_id):
1541 peer = None
1542 url = '/api/v1/rest/{}/Sysdb/mlag/status/'.format(device_id)
1543 self.logger.debug('get_MLAG_status: URL {}'.format(url))
1544 try:
1545 data = self.client.get(url, timeout=self.__API_REQUEST_TOUT)
1546 if data['notifications']:
1547 found = False
1548 for notification in data['notifications']:
1549 for update in notification['updates']:
1550 if update == 'systemId':
1551 mlagSystemId = notification['updates'][update]['value']
1552 found = True
1553 break
1554 if found:
1555 break
1556 # search the MLAG System Id
1557 if found:
1558 for s in self.switches:
1559 if self.switches[s]['serialNumber'] == device_id:
1560 continue
1561 url = '/api/v1/rest/{}/Sysdb/mlag/status/'.format(self.switches[s]['serialNumber'])
1562 self.logger.debug('Searching for MLAG system id {} in switch {}'.format(mlagSystemId, s))
1563 data = self.client.get(url, timeout=self.__API_REQUEST_TOUT)
1564 found = False
1565 for notification in data['notifications']:
1566 for update in notification['updates']:
1567 if update == 'systemId':
1568 if mlagSystemId == notification['updates'][update]['value']:
1569 peer = s
1570 found = True
1571 break
1572 if found:
1573 break
1574 if found:
1575 break
1576 if peer is None:
1577 self.logger.error('No Peer device found for device {} with MLAG address {}'.format(device_id,
1578 mlagSystemId))
1579 else:
1580 self.logger.debug('Peer MLAG for device {} - value {}'.format(device_id, peer))
1581 return peer
1582 except Exception:
1583 raise SdnConnectorError("Invalid response from url {}: data {}".format(url, data))
1584
1585 def is_valid_destination(self, url):
1586 """ Check that the provided WIM URL is correct
1587 """
1588 if re.match(self.__regex, url):
1589 return True
1590 elif self.is_valid_ipv4_address(url):
1591 return True
1592 else:
1593 return self.is_valid_ipv6_address(url)
1594
1595 def is_valid_ipv4_address(self, address):
1596 """ Checks that the given IP is IPv4 valid
1597 """
1598 try:
1599 socket.inet_pton(socket.AF_INET, address)
1600 except AttributeError: # no inet_pton here, sorry
1601 try:
1602 socket.inet_aton(address)
1603 except socket.error:
1604 return False
1605 return address.count('.') == 3
1606 except socket.error: # not a valid address
1607 return False
1608 return True
1609
1610 def is_valid_ipv6_address(self, address):
1611 """ Checks that the given IP is IPv6 valid
1612 """
1613 try:
1614 socket.inet_pton(socket.AF_INET6, address)
1615 except socket.error: # not a valid address
1616 return False
1617 return True
1618
1619 def delete_keys_from_dict(self, dict_del, lst_keys):
1620 if dict_del is None:
1621 return dict_del
1622 dict_copy = {k: v for k, v in dict_del.items() if k not in lst_keys}
1623 for k, v in dict_copy.items():
1624 if isinstance(v, dict):
1625 dict_copy[k] = self.delete_keys_from_dict(v, lst_keys)
1626 return dict_copy