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