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