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