Fixing bug 1437
[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 = 'ro.sdn.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 'ro.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 if self.topology == self._VXLAN_MLAG:
696 cl_config = str(cl_vlan) + str(cl_bgp[s])
697 else:
698 cl_config = str(cl_vlan)
699 else:
700 cl_config = str(cl_vlan) + str(cl_bgp[s]) + str(cls_cp[s])
701
702 cls_perSw[s] = [{'name': cl_name, 'config': cl_config}]
703
704 allLeafConfigured, allLeafModified = self.__updateConnection(cls_perSw)
705
706 conn_info = {
707 "uuid": service_uuid,
708 "status": "BUILD",
709 "service_type": service_type,
710 "vlan_id": vlan_id,
711 "connection_points": connection_points,
712 "configLetPerSwitch": cls_perSw,
713 'allLeafConfigured': allLeafConfigured,
714 'allLeafModified': allLeafModified}
715
716 return service_uuid, conn_info
717 except Exception as ex:
718 self.logger.debug("Exception processing connection {}: {}".
719 format(service_uuid, str(ex)))
720 raise ex
721
722 def __updateConnection(self, cls_perSw):
723 """ Invoked in the creation and modification
724
725 checks if the new connection points config is:
726 - already in the Cloud Vision, the configLet is modified, and applied to the switch,
727 executing the corresponding task
728 - if it has to be removed:
729 then configuration has to be removed from the switch executing the corresponding task,
730 before trying to remove the configuration
731 - created, the configuration set is created, associated to the switch, and the associated
732 task to the configLet modification executed
733 In case of any error, rollback is executed, removing the created elements, and restoring to the
734 previous state.
735 """
736 try:
737 allLeafConfigured = {}
738 allLeafModified = {}
739
740 for s in self.switches:
741 allLeafConfigured[s] = False
742 allLeafModified[s] = False
743 cl_toDelete = []
744 for s in self.switches:
745 toDelete_in_cvp = False
746 if not (cls_perSw.get(s) and cls_perSw[s][0].get('config')):
747 # when there is no configuration, means that there is no interface
748 # in the switch to be connected, so the configLet has to be removed from CloudVision
749 # after removing the ConfigLet from the switch if it was already there
750
751 # get config let name and key
752 cl = cls_perSw[s]
753 try:
754 cvp_cl = self.client.api.get_configlet_by_name(cl[0]['name'])
755 # remove configLet
756 cl_toDelete.append(cvp_cl)
757 cl[0] = cvp_cl
758 toDelete_in_cvp = True
759 except CvpApiError as error:
760 if "Entity does not exist" in error.msg:
761 continue
762 else:
763 raise error
764 # remove configLet from device
765 else:
766 res = self.__configlet_modify(cls_perSw[s])
767 allLeafConfigured[s] = res[0]
768 if not allLeafConfigured[s]:
769 continue
770 cl = cls_perSw[s]
771 res = self.__device_modify(
772 device_to_update=s,
773 new_configlets=cl,
774 delete=toDelete_in_cvp)
775 if "errorMessage" in str(res):
776 raise Exception(str(res))
777 self.logger.info("Device {} modify result {}".format(s, res))
778 for t_id in res[1]['tasks']:
779 if not toDelete_in_cvp:
780 note_msg = "{}{}{}{}##".format(self.__MANAGED_BY_OSM,
781 self.__SEPARATOR,
782 t_id,
783 self.__SEPARATOR)
784 self.client.api.add_note_to_configlet(
785 cls_perSw[s][0]['key'],
786 note_msg)
787 cls_perSw[s][0]['note'] = note_msg
788 tasks = {t_id: {'workOrderId': t_id}}
789 self.__exec_task(tasks, self.__EXC_TASK_EXEC_WAIT)
790 # with just one configLet assigned to a device,
791 # delete all if there are errors in next loops
792 if not toDelete_in_cvp:
793 allLeafModified[s] = True
794 if len(cl_toDelete) > 0:
795 self.__configlet_modify(cl_toDelete, delete=True)
796
797 return allLeafConfigured, allLeafModified
798 except Exception as ex:
799 try:
800 self.__rollbackConnection(cls_perSw,
801 allLeafConfigured,
802 allLeafModified)
803 except Exception as e:
804 self.logger.error("Exception rolling back in updating connection: {}".
805 format(e), exc_info=True)
806 raise ex
807
808 def __rollbackConnection(self,
809 cls_perSw,
810 allLeafConfigured,
811 allLeafModified):
812 """ Removes the given configLet from the devices and then remove the configLets
813 """
814 for s in self.switches:
815 if allLeafModified[s]:
816 try:
817 res = self.__device_modify(
818 device_to_update=s,
819 new_configlets=cls_perSw[s],
820 delete=True)
821 if "errorMessage" in str(res):
822 raise Exception(str(res))
823 tasks = dict()
824 for t_id in res[1]['tasks']:
825 tasks[t_id] = {'workOrderId': t_id}
826 self.__exec_task(tasks)
827 self.logger.info("Device {} modify result {}".format(s, res))
828 except Exception as e:
829 self.logger.error('Error removing configlets from device {}: {}'.format(s, e))
830 pass
831 for s in self.switches:
832 if allLeafConfigured[s]:
833 self.__configlet_modify(cls_perSw[s], delete=True)
834
835 def __exec_task(self, tasks, tout=10):
836 if self.taskC is None:
837 self.__connect()
838 data = self.taskC.update_all_tasks(tasks).values()
839 self.taskC.task_action(data, tout, 'executed')
840
841 def __device_modify(self, device_to_update, new_configlets, delete):
842 """ Updates the devices (switches) adding or removing the configLet,
843 the tasks Id's associated to the change are returned
844 """
845 self.logger.info('Enter in __device_modify delete: {}'.format(delete))
846 updated = []
847 changed = False
848 # Task Ids that have been identified during device actions
849 newTasks = []
850
851 if (len(new_configlets) == 0 or
852 device_to_update is None or
853 len(device_to_update) == 0):
854 data = {'updated': updated, 'tasks': newTasks}
855 return [changed, data]
856
857 self.__load_inventory()
858
859 allDeviceFacts = self.allDeviceFacts
860 # Work through Devices list adding device specific information
861 device = None
862 for try_device in allDeviceFacts:
863 # Add Device Specific Configlets
864 # self.logger.debug(device)
865 if try_device['hostname'] not in device_to_update:
866 continue
867 dev_cvp_configlets = self.client.api.get_configlets_by_device_id(
868 try_device['systemMacAddress'])
869 # self.logger.debug(dev_cvp_configlets)
870 try_device['deviceSpecificConfiglets'] = []
871 for cvp_configlet in dev_cvp_configlets:
872 if int(cvp_configlet['containerCount']) == 0:
873 try_device['deviceSpecificConfiglets'].append(
874 {'name': cvp_configlet['name'],
875 'key': cvp_configlet['key']})
876 # self.logger.debug(device)
877 device = try_device
878 break
879
880 # Check assigned configlets
881 device_update = False
882 add_configlets = []
883 remove_configlets = []
884 update_devices = []
885
886 if delete:
887 for cvp_configlet in device['deviceSpecificConfiglets']:
888 for cl in new_configlets:
889 if cvp_configlet['name'] == cl['name']:
890 remove_configlets.append(cvp_configlet)
891 device_update = True
892 else:
893 for configlet in new_configlets:
894 if configlet not in device['deviceSpecificConfiglets']:
895 add_configlets.append(configlet)
896 device_update = True
897 if device_update:
898 update_devices.append({'hostname': device['hostname'],
899 'configlets': [add_configlets,
900 remove_configlets],
901 'device': device})
902 self.logger.info("Device to modify: {}".format(update_devices))
903
904 up_device = update_devices[0]
905 cl_toAdd = up_device['configlets'][0]
906 cl_toDel = up_device['configlets'][1]
907 # Update Configlets
908 try:
909 if delete and len(cl_toDel) > 0:
910 r = self.client.api.remove_configlets_from_device(
911 'OSM',
912 up_device['device'],
913 cl_toDel,
914 create_task=True)
915 dev_action = r
916 self.logger.debug("remove_configlets_from_device {} {}".format(dev_action, cl_toDel))
917 elif len(cl_toAdd) > 0:
918 r = self.client.api.apply_configlets_to_device(
919 'OSM',
920 up_device['device'],
921 cl_toAdd,
922 create_task=True)
923 dev_action = r
924 self.logger.debug("apply_configlets_to_device {} {}".format(dev_action, cl_toAdd))
925
926 except Exception as error:
927 errorMessage = str(error)
928 msg = "errorMessage: Device {} Configlets couldnot be updated: {}".format(
929 up_device['hostname'], errorMessage)
930 raise SdnConnectorError(msg) from error
931 else:
932 if "errorMessage" in str(dev_action):
933 m = "Device {} Configlets update fail: {}".format(
934 up_device['name'], dev_action['errorMessage'])
935 raise SdnConnectorError(m)
936 else:
937 changed = True
938 if 'taskIds' in str(dev_action):
939 # Fix 1030 SDN-ARISTA Key error note when deploy a NS
940 if not dev_action['data']['taskIds']:
941 raise SdnConnectorError("No taskIds found: Device {} Configlets could not be updated".format(
942 up_device['hostname']))
943 for taskId in dev_action['data']['taskIds']:
944 updated.append({
945 up_device['hostname']: "Configlets-{}".format(taskId)})
946 newTasks.append(taskId)
947 else:
948 updated.append({up_device['hostname']:
949 "Configlets-No_Specific_Tasks"})
950 data = {'updated': updated, 'tasks': newTasks}
951 return [changed, data]
952
953 def __configlet_modify(self, configletsToApply, delete=False):
954 ''' adds/update or delete the provided configLets
955 :param configletsToApply: list of configLets to apply
956 :param delete: flag to indicate if the configLets have to be deleted
957 from Cloud Vision Portal
958 :return: data: dict of module actions and taskIDs
959 '''
960 self.logger.info('Enter in __configlet_modify delete:{}'.format(
961 delete))
962
963 # Compare configlets against cvp_facts-configlets
964 changed = False
965 checked = []
966 deleted = []
967 updated = []
968 new = []
969
970 for cl in configletsToApply:
971 found_in_cvp = False
972 to_delete = False
973 to_update = False
974 to_create = False
975 to_check = False
976 try:
977 cvp_cl = self.client.api.get_configlet_by_name(cl['name'])
978 cl['key'] = cvp_cl['key']
979 cl['note'] = cvp_cl['note']
980 found_in_cvp = True
981 except CvpApiError as error:
982 if "Entity does not exist" in error.msg:
983 pass
984 else:
985 raise error
986
987 if delete:
988 if found_in_cvp:
989 to_delete = True
990 configlet = {'name': cvp_cl['name'],
991 'data': cvp_cl}
992 else:
993 if found_in_cvp:
994 cl_compare = self.__compare(cl['config'],
995 cvp_cl['config'])
996 # compare function returns a floating point number
997 if cl_compare[0] != 100.0:
998 to_update = True
999 configlet = {'name': cl['name'],
1000 'data': cvp_cl,
1001 'config': cl['config']}
1002 else:
1003 to_check = True
1004 configlet = {'name': cl['name'],
1005 'key': cvp_cl['key'],
1006 'data': cvp_cl,
1007 'config': cl['config']}
1008 else:
1009 to_create = True
1010 configlet = {'name': cl['name'],
1011 'config': cl['config']}
1012 try:
1013 if to_delete:
1014 operation = 'delete'
1015 resp = self.client.api.delete_configlet(
1016 configlet['data']['name'],
1017 configlet['data']['key'])
1018 elif to_update:
1019 operation = 'update'
1020 resp = self.client.api.update_configlet(
1021 configlet['config'],
1022 configlet['data']['key'],
1023 configlet['data']['name'],
1024 wait_task_ids=True)
1025 elif to_create:
1026 operation = 'create'
1027 resp = self.client.api.add_configlet(
1028 configlet['name'],
1029 configlet['config'])
1030 else:
1031 operation = 'checked'
1032 resp = 'checked'
1033 except Exception as error:
1034 errorMessage = str(error).split(':')[-1]
1035 message = "Configlet {} cannot be {}: {}".format(
1036 cl['name'], operation, errorMessage)
1037 if to_delete:
1038 deleted.append({configlet['name']: message})
1039 elif to_update:
1040 updated.append({configlet['name']: message})
1041 elif to_create:
1042 new.append({configlet['name']: message})
1043 elif to_check:
1044 checked.append({configlet['name']: message})
1045
1046 else:
1047 if "error" in str(resp).lower():
1048 message = "Configlet {} cannot be deleted: {}".format(
1049 cl['name'], resp['errorMessage'])
1050 if to_delete:
1051 deleted.append({configlet['name']: message})
1052 elif to_update:
1053 updated.append({configlet['name']: message})
1054 elif to_create:
1055 new.append({configlet['name']: message})
1056 elif to_check:
1057 checked.append({configlet['name']: message})
1058 else:
1059 if to_delete:
1060 changed = True
1061 deleted.append({configlet['name']: "success"})
1062 elif to_update:
1063 changed = True
1064 updated.append({configlet['name']: "success"})
1065 elif to_create:
1066 changed = True
1067 cl['key'] = resp # This key is used in API call deviceApplyConfigLet FGA
1068 new.append({configlet['name']: "success"})
1069 elif to_check:
1070 changed = False
1071 checked.append({configlet['name']: "success"})
1072
1073 data = {'new': new, 'updated': updated, 'deleted': deleted, 'checked': checked}
1074 return [changed, data]
1075
1076 def __get_configletsDevices(self, configlets):
1077 for s in self.switches:
1078 configlet = configlets[s]
1079 # Add applied Devices
1080 if len(configlet) > 0:
1081 configlet['devices'] = []
1082 applied_devices = self.client.api.get_applied_devices(
1083 configlet['name'])
1084 for device in applied_devices['data']:
1085 configlet['devices'].append(device['hostName'])
1086
1087 def __get_serviceData(self, service_uuid, service_type, vlan_id, conn_info=None):
1088 cls_perSw = {}
1089 for s in self.switches:
1090 cls_perSw[s] = []
1091 if not conn_info:
1092 srv_cls = self.__get_serviceConfigLets(service_uuid,
1093 service_type,
1094 vlan_id)
1095 self.__get_configletsDevices(srv_cls)
1096 for s in self.switches:
1097 cl = srv_cls[s]
1098 if len(cl) > 0:
1099 for dev in cl['devices']:
1100 cls_perSw[dev].append(cl)
1101 else:
1102 cls_perSw = conn_info['configLetPerSwitch']
1103 return cls_perSw
1104
1105 def delete_connectivity_service(self, service_uuid, conn_info=None):
1106 """
1107 Disconnect multi-site endpoints previously connected
1108
1109 :param service_uuid: The one returned by create_connectivity_service
1110 :param conn_info: The one returned by last call to 'create_connectivity_service' or 'edit_connectivity_service'
1111 if they do not return None
1112 :return: None
1113 :raises: SdnConnectorException: In case of error. The parameter http_code must be filled
1114 """
1115 try:
1116 self.logger.debug('invoked delete_connectivity_service {}'.
1117 format(service_uuid))
1118 if not service_uuid:
1119 raise SdnConnectorError(message='No connection service UUID',
1120 http_code=500)
1121
1122 self.__get_Connection()
1123 if conn_info is None:
1124 raise SdnConnectorError(message='No connection information for service UUID {}'.format(service_uuid),
1125 http_code=500)
1126 c_info = None
1127 cls_perSw = self.__get_serviceData(service_uuid,
1128 conn_info['service_type'],
1129 conn_info['vlan_id'],
1130 c_info)
1131 allLeafConfigured = {}
1132 allLeafModified = {}
1133 for s in self.switches:
1134 allLeafConfigured[s] = True
1135 allLeafModified[s] = True
1136 found_in_cvp = False
1137 for s in self.switches:
1138 if cls_perSw[s]:
1139 found_in_cvp = True
1140 if found_in_cvp:
1141 self.__rollbackConnection(cls_perSw,
1142 allLeafConfigured,
1143 allLeafModified)
1144 else:
1145 # if the service is not defined in Cloud Vision, return a 404 - NotFound error
1146 raise SdnConnectorError(message='Service {} was not found in Arista Cloud Vision {}'.
1147 format(service_uuid, self.__wim_url),
1148 http_code=404)
1149 self.__removeMetadata(service_uuid)
1150 except CvpLoginError as e:
1151 self.logger.info(str(e))
1152 self.client = None
1153 raise SdnConnectorError(message=SdnError.UNAUTHORIZED + " " + str(e),
1154 http_code=401) from e
1155 except SdnConnectorError as sde:
1156 raise sde
1157 except Exception as ex:
1158 self.client = None
1159 self.logger.error(ex)
1160 if self.raiseException:
1161 raise ex
1162 raise SdnConnectorError(message=SdnError.INTERNAL_ERROR + " " + str(ex),
1163 http_code=500) from ex
1164
1165 def __addMetadata(self, service_uuid, service_type, vlan_id):
1166 """ Adds the connectivity service from 'OSM_metadata' configLet
1167 """
1168 found_in_cvp = False
1169 try:
1170 cvp_cl = self.client.api.get_configlet_by_name(self.__OSM_METADATA)
1171 found_in_cvp = True
1172 except CvpApiError as error:
1173 if "Entity does not exist" in error.msg:
1174 pass
1175 else:
1176 raise error
1177 try:
1178 new_serv = '{} {} {} {}\n'.format(self.__METADATA_PREFIX, service_type, vlan_id, service_uuid)
1179
1180 if found_in_cvp:
1181 cl_config = cvp_cl['config'] + new_serv
1182 else:
1183 cl_config = new_serv
1184 cl_meta = [{'name': self.__OSM_METADATA, 'config': cl_config}]
1185 self.__configlet_modify(cl_meta)
1186 except Exception as e:
1187 self.logger.error('Error in setting metadata in CloudVision from OSM for service {}: {}'.
1188 format(service_uuid, str(e)))
1189 pass
1190
1191 def __removeMetadata(self, service_uuid):
1192 """ Removes the connectivity service from 'OSM_metadata' configLet
1193 """
1194 found_in_cvp = False
1195 try:
1196 cvp_cl = self.client.api.get_configlet_by_name(self.__OSM_METADATA)
1197 found_in_cvp = True
1198 except CvpApiError as error:
1199 if "Entity does not exist" in error.msg:
1200 pass
1201 else:
1202 raise error
1203 try:
1204 if found_in_cvp:
1205 if service_uuid in cvp_cl['config']:
1206 cl_config = ''
1207 for line in cvp_cl['config'].split('\n'):
1208 if service_uuid in line:
1209 continue
1210 else:
1211 cl_config = cl_config + line
1212 cl_meta = [{'name': self.__OSM_METADATA, 'config': cl_config}]
1213 self.__configlet_modify(cl_meta)
1214 except Exception as e:
1215 self.logger.error('Error in removing metadata in CloudVision from OSM for service {}: {}'.
1216 format(service_uuid, str(e)))
1217 pass
1218
1219 def edit_connectivity_service(self,
1220 service_uuid,
1221 conn_info=None,
1222 connection_points=None,
1223 **kwargs):
1224 """ Change an existing connectivity service.
1225
1226 This method's arguments and return value follow the same convention as
1227 :meth:`~.create_connectivity_service`.
1228
1229 :param service_uuid: UUID of the connectivity service.
1230 :param conn_info: (dict or None): Information previously returned
1231 by last call to create_connectivity_service
1232 or edit_connectivity_service
1233 :param connection_points: (list): If provided, the old list of
1234 connection points will be replaced.
1235 :param kwargs: Same meaning that create_connectivity_service
1236 :return: dict or None: Information to be updated and stored at
1237 the database.
1238 When ``None`` is returned, no information should be changed.
1239 When an empty dict is returned, the database record will
1240 be deleted.
1241 **MUST** be JSON/YAML-serializable (plain data structures).
1242 Raises:
1243 SdnConnectorError: In case of error.
1244 """
1245 try:
1246 self.logger.debug('invoked edit_connectivity_service for service {}. ports: {}'.format(service_uuid,
1247 connection_points))
1248
1249 if not service_uuid:
1250 raise SdnConnectorError(message='Unable to perform operation, missing or empty uuid',
1251 http_code=500)
1252 if not conn_info:
1253 raise SdnConnectorError(message='Unable to perform operation, missing or empty connection information',
1254 http_code=500)
1255
1256 if connection_points is None:
1257 return None
1258
1259 self.__get_Connection()
1260
1261 cls_currentPerSw = conn_info['configLetPerSwitch']
1262 service_type = conn_info['service_type']
1263
1264 self.__check_service(service_type,
1265 connection_points,
1266 check_vlan=False,
1267 check_num_cp=False,
1268 kwargs=kwargs)
1269
1270 s_uid, s_connInf = self.__processConnection(
1271 service_uuid,
1272 service_type,
1273 connection_points,
1274 kwargs)
1275 self.logger.info("Service with uuid {} configuration updated".
1276 format(s_uid))
1277 return s_connInf
1278 except CvpLoginError as e:
1279 self.logger.info(str(e))
1280 self.client = None
1281 raise SdnConnectorError(message=SdnError.UNAUTHORIZED + " " + str(e),
1282 http_code=401) from e
1283 except SdnConnectorError as sde:
1284 raise sde
1285 except Exception as ex:
1286 try:
1287 # Add previous
1288 # TODO check if there are pending task, and cancel them before restoring
1289 self.__updateConnection(cls_currentPerSw)
1290 except Exception as e:
1291 self.logger.error("Unable to restore configuration in service {} after an error in the configuration"
1292 " updated: {}".format(service_uuid, str(e)))
1293 if self.raiseException:
1294 raise ex
1295 raise SdnConnectorError(message=str(ex),
1296 http_code=500) from ex
1297
1298 def clear_all_connectivity_services(self):
1299 """ Removes all connectivity services from Arista CloudVision with two steps:
1300 - retrives all the services from Arista CloudVision
1301 - removes each service
1302 """
1303 try:
1304 self.logger.debug('invoked AristaImpl ' +
1305 'clear_all_connectivity_services')
1306 self.__get_Connection()
1307 s_list = self.__get_srvUUIDs()
1308 for serv in s_list:
1309 conn_info = {}
1310 conn_info['service_type'] = serv['type']
1311 conn_info['vlan_id'] = serv['vlan']
1312
1313 self.delete_connectivity_service(serv['uuid'], conn_info)
1314 except CvpLoginError as e:
1315 self.logger.info(str(e))
1316 self.client = None
1317 raise SdnConnectorError(message=SdnError.UNAUTHORIZED + " " + str(e),
1318 http_code=401) from e
1319 except SdnConnectorError as sde:
1320 raise sde
1321 except Exception as ex:
1322 self.client = None
1323 self.logger.error(ex)
1324 if self.raiseException:
1325 raise ex
1326 raise SdnConnectorError(message=SdnError.INTERNAL_ERROR + " " + str(ex),
1327 http_code=500) from ex
1328
1329 def get_all_active_connectivity_services(self):
1330 """ Return the uuid of all the active connectivity services with two steps:
1331 - retrives all the services from Arista CloudVision
1332 - retrives the status of each server
1333 """
1334 try:
1335 self.logger.debug('invoked AristaImpl {}'.format(
1336 'get_all_active_connectivity_services'))
1337 self.__get_Connection()
1338 s_list = self.__get_srvUUIDs()
1339 result = []
1340 for serv in s_list:
1341 conn_info = {}
1342 conn_info['service_type'] = serv['type']
1343 conn_info['vlan_id'] = serv['vlan']
1344
1345 status = self.get_connectivity_service_status(serv['uuid'], conn_info)
1346 if status['sdn_status'] == 'ACTIVE':
1347 result.append(serv['uuid'])
1348 return result
1349 except CvpLoginError as e:
1350 self.logger.info(str(e))
1351 self.client = None
1352 raise SdnConnectorError(message=SdnError.UNAUTHORIZED + " " + str(e),
1353 http_code=401) from e
1354 except SdnConnectorError as sde:
1355 raise sde
1356 except Exception as ex:
1357 self.client = None
1358 self.logger.error(ex)
1359 if self.raiseException:
1360 raise ex
1361 raise SdnConnectorError(message=SdnError.INTERNAL_ERROR,
1362 http_code=500) from ex
1363
1364 def __get_serviceConfigLets(self, service_uuid, service_type, vlan_id):
1365 """ Return the configLet's associated with a connectivity service,
1366 There should be one, as maximum, per device (switch) for a given
1367 connectivity service
1368 """
1369 srv_cls = {}
1370 for s in self.switches:
1371 srv_cls[s] = []
1372 found_in_cvp = False
1373 name = (self.__OSM_PREFIX +
1374 s +
1375 self.__SEPARATOR + service_type + str(vlan_id) +
1376 self.__SEPARATOR + service_uuid)
1377 try:
1378 cvp_cl = self.client.api.get_configlet_by_name(name)
1379 found_in_cvp = True
1380 except CvpApiError as error:
1381 if "Entity does not exist" in error.msg:
1382 pass
1383 else:
1384 raise error
1385 if found_in_cvp:
1386 srv_cls[s] = cvp_cl
1387 return srv_cls
1388
1389 def __get_srvVLANs(self):
1390 """ Returns a list with all the VLAN id's used in the connectivity services managed
1391 in tha Arista CloudVision by checking the 'OSM_metadata' configLet where this
1392 information is stored
1393 """
1394 found_in_cvp = False
1395 try:
1396 cvp_cl = self.client.api.get_configlet_by_name(self.__OSM_METADATA)
1397 found_in_cvp = True
1398 except CvpApiError as error:
1399 if "Entity does not exist" in error.msg:
1400 pass
1401 else:
1402 raise error
1403 s_vlan_list = []
1404 if found_in_cvp:
1405 lines = cvp_cl['config'].split('\n')
1406 for line in lines:
1407 if self.__METADATA_PREFIX in line:
1408 s_vlan = line.split(' ')[3]
1409 else:
1410 continue
1411 if (s_vlan is not None and
1412 len(s_vlan) > 0 and
1413 s_vlan not in s_vlan_list):
1414 s_vlan_list.append(s_vlan)
1415
1416 return s_vlan_list
1417
1418 def __get_srvUUIDs(self):
1419 """ Retrieves all the connectivity services, managed in tha Arista CloudVision
1420 by checking the 'OSM_metadata' configLet where this information is stored
1421 """
1422 found_in_cvp = False
1423 try:
1424 cvp_cl = self.client.api.get_configlet_by_name(self.__OSM_METADATA)
1425 found_in_cvp = True
1426 except CvpApiError as error:
1427 if "Entity does not exist" in error.msg:
1428 pass
1429 else:
1430 raise error
1431 serv_list = []
1432 if found_in_cvp:
1433 lines = cvp_cl['config'].split('\n')
1434 for line in lines:
1435 if self.__METADATA_PREFIX in line:
1436 line = line.split(' ')
1437 serv = {'uuid': line[4], 'type': line[2], 'vlan': line[3]}
1438 else:
1439 continue
1440 if (serv is not None and
1441 len(serv) > 0 and
1442 serv not in serv_list):
1443 serv_list.append(serv)
1444
1445 return serv_list
1446
1447 def __get_Connection(self):
1448 """ Open a connection with Arista CloudVision,
1449 invoking the version retrival as test
1450 """
1451 try:
1452 if self.client is None:
1453 self.client = self.__connect()
1454 self.client.api.get_cvp_info()
1455 except (CvpSessionLogOutError, RequestException) as e:
1456 self.logger.debug("Connection error '{}'. Reconnecting".format(e))
1457 self.client = self.__connect()
1458 self.client.api.get_cvp_info()
1459
1460 def __connect(self):
1461 ''' Connects to CVP device using user provided credentials from initialization.
1462 :return: CvpClient object with connection instantiated.
1463 '''
1464 client = CvpClient()
1465 protocol, _, rest_url = self.__wim_url.rpartition("://")
1466 host, _, port = rest_url.partition(":")
1467 if port and port.endswith("/"):
1468 port = int(port[:-1])
1469 elif port:
1470 port = int(port)
1471 else:
1472 port = 443
1473
1474 client.connect([host],
1475 self.__user,
1476 self.__passwd,
1477 protocol=protocol or "https",
1478 port=port,
1479 connect_timeout=2)
1480 client.api = CvpApi(client, request_timeout=self.__API_REQUEST_TOUT)
1481 self.taskC = AristaCVPTask(client.api)
1482 return client
1483
1484 def __compare(self, fromText, toText, lines=10):
1485 """ Compare text string in 'fromText' with 'toText' and produce
1486 diffRatio - a score as a float in the range [0, 1] 2.0*M / T
1487 T is the total number of elements in both sequences,
1488 M is the number of matches.
1489 Score - 1.0 if the sequences are identical, and
1490 0.0 if they have nothing in common.
1491 unified diff list
1492 Code Meaning
1493 '- ' line unique to sequence 1
1494 '+ ' line unique to sequence 2
1495 ' ' line common to both sequences
1496 '? ' line not present in either input sequence
1497 """
1498 fromlines = fromText.splitlines(1)
1499 tolines = toText.splitlines(1)
1500 diff = list(difflib.unified_diff(fromlines, tolines, n=lines))
1501 textComp = difflib.SequenceMatcher(None, fromText, toText)
1502 diffRatio = round(textComp.quick_ratio()*100, 2)
1503 return [diffRatio, diff]
1504
1505 def __load_inventory(self):
1506 """ Get Inventory Data for All Devices (aka switches) from the Arista CloudVision
1507 """
1508 if not self.cvp_inventory:
1509 self.cvp_inventory = self.client.api.get_inventory()
1510 self.allDeviceFacts = []
1511 for device in self.cvp_inventory:
1512 self.allDeviceFacts.append(device)
1513
1514 def __get_tags(self, name, value):
1515 if not self.cvp_tags:
1516 self.cvp_tags = []
1517 url = '/api/v1/rest/analytics/tags/labels/devices/{}/value/{}/elements'.format(name, value)
1518 self.logger.debug('get_tags: URL {}'.format(url))
1519 data = self.client.get(url, timeout=self.__API_REQUEST_TOUT)
1520 for dev in data['notifications']:
1521 for elem in dev['updates']:
1522 self.cvp_tags.append(elem)
1523 self.logger.debug('Available devices with tag_name {} - value {}: {} '.format(name, value, self.cvp_tags))
1524
1525 def __get_interface_ip(self, device_id, interface):
1526 url = '/api/v1/rest/{}/Sysdb/ip/config/ipIntfConfig/{}/'.format(device_id, interface)
1527 self.logger.debug('get_interface_ip: URL {}'.format(url))
1528 data = None
1529 try:
1530 data = self.client.get(url, timeout=self.__API_REQUEST_TOUT)
1531 if data['notifications']:
1532 for notification in data['notifications']:
1533 for update in notification['updates']:
1534 if update == 'addrWithMask':
1535 return notification['updates'][update]['value']
1536 except Exception as e:
1537 raise SdnConnectorError("Invalid response from url {}: data {} - {}".format(url, data, str(e)))
1538 raise SdnConnectorError("Unable to get ip for interface {} in device {}, data {}".
1539 format(interface, device_id, data))
1540
1541 def __get_device_ASN(self, device_id):
1542 url = '/api/v1/rest/{}/Sysdb/routing/bgp/config/'.format(device_id)
1543 self.logger.debug('get_device_ASN: URL {}'.format(url))
1544 data = None
1545 try:
1546 data = self.client.get(url, timeout=self.__API_REQUEST_TOUT)
1547 if data['notifications']:
1548 for notification in data['notifications']:
1549 for update in notification['updates']:
1550 if update == 'asNumber':
1551 return notification['updates'][update]['value']['value']['int']
1552 except Exception as e:
1553 raise SdnConnectorError("Invalid response from url {}: data {} - {}".format(url, data, str(e)))
1554 raise SdnConnectorError("Unable to get AS in device {}, data {}".format(device_id, data))
1555
1556 def __get_peer_MLAG(self, device_id):
1557 peer = None
1558 url = '/api/v1/rest/{}/Sysdb/mlag/status/'.format(device_id)
1559 self.logger.debug('get_MLAG_status: URL {}'.format(url))
1560 try:
1561 data = self.client.get(url, timeout=self.__API_REQUEST_TOUT)
1562 if data['notifications']:
1563 found = False
1564 for notification in data['notifications']:
1565 for update in notification['updates']:
1566 if update == 'systemId':
1567 mlagSystemId = notification['updates'][update]['value']
1568 found = True
1569 break
1570 if found:
1571 break
1572 # search the MLAG System Id
1573 if found:
1574 for s in self.switches:
1575 if self.switches[s]['serialNumber'] == device_id:
1576 continue
1577 url = '/api/v1/rest/{}/Sysdb/mlag/status/'.format(self.switches[s]['serialNumber'])
1578 self.logger.debug('Searching for MLAG system id {} in switch {}'.format(mlagSystemId, s))
1579 data = self.client.get(url, timeout=self.__API_REQUEST_TOUT)
1580 found = False
1581 for notification in data['notifications']:
1582 for update in notification['updates']:
1583 if update == 'systemId':
1584 if mlagSystemId == notification['updates'][update]['value']:
1585 peer = s
1586 found = True
1587 break
1588 if found:
1589 break
1590 if found:
1591 break
1592 if peer is None:
1593 self.logger.error('No Peer device found for device {} with MLAG address {}'.format(device_id,
1594 mlagSystemId))
1595 else:
1596 self.logger.debug('Peer MLAG for device {} - value {}'.format(device_id, peer))
1597 return peer
1598 except Exception:
1599 raise SdnConnectorError("Invalid response from url {}: data {}".format(url, data))
1600
1601 def is_valid_destination(self, url):
1602 """ Check that the provided WIM URL is correct
1603 """
1604 if re.match(self.__regex, url):
1605 return True
1606 elif self.is_valid_ipv4_address(url):
1607 return True
1608 else:
1609 return self.is_valid_ipv6_address(url)
1610
1611 def is_valid_ipv4_address(self, address):
1612 """ Checks that the given IP is IPv4 valid
1613 """
1614 try:
1615 socket.inet_pton(socket.AF_INET, address)
1616 except AttributeError: # no inet_pton here, sorry
1617 try:
1618 socket.inet_aton(address)
1619 except socket.error:
1620 return False
1621 return address.count('.') == 3
1622 except socket.error: # not a valid address
1623 return False
1624 return True
1625
1626 def is_valid_ipv6_address(self, address):
1627 """ Checks that the given IP is IPv6 valid
1628 """
1629 try:
1630 socket.inet_pton(socket.AF_INET6, address)
1631 except socket.error: # not a valid address
1632 return False
1633 return True
1634
1635 def delete_keys_from_dict(self, dict_del, lst_keys):
1636 if dict_del is None:
1637 return dict_del
1638 dict_copy = {k: v for k, v in dict_del.items() if k not in lst_keys}
1639 for k, v in dict_copy.items():
1640 if isinstance(v, dict):
1641 dict_copy[k] = self.delete_keys_from_dict(v, lst_keys)
1642 return dict_copy