76d14b4cefcf47a6125877bc02baee4d05f8d73a
[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 for cp in connection_points:
565 i += 1
566 encap_info = cp.get(self.__ENCAPSULATION_INFO_PARAM)
567 if not vlan_processed:
568 vlan_id = str(encap_info.get(self.__VLAN_PARAM))
569 if not vlan_id:
570 continue
571 vni_id = encap_info.get(self.__VNI_PARAM)
572 if not vni_id:
573 vni_id = str(10000 + int(vlan_id))
574
575 if service_type == self.__service_types_ELAN:
576 cl_vlan = self.clC.getElan_vlan(service_uuid,
577 vlan_id,
578 vni_id)
579 else:
580 cl_vlan = self.clC.getEline_vlan(service_uuid,
581 vlan_id,
582 vni_id)
583 vlan_processed = True
584
585 encap_type = cp.get(self.__ENCAPSULATION_TYPE_PARAM)
586 switch_id = encap_info.get(self.__SW_ID_PARAM)
587 if not switch_id:
588 point_mac = encap_info.get(self.__MAC_PARAM)
589 switches = self.__lldp_find_neighbor("chassisId", point_mac)
590
591 if len(switches) == 0:
592 raise SdnConnectorError(message="Connection point MAC address {} not found in the switches".format(point_mac),
593 http_code=406)
594 self.logger.debug("Found connection point for MAC {}: {}".
595 format(point_mac, switches))
596 port_channel = self.__get_switch_po(switch['name'],
597 switch['interface'])
598 if len(port_channel) > 0:
599 interface = port_channel[0]
600 else:
601 interface = switch['interface']
602 else:
603 interface = encap_info.get(self.__SW_PORT_PARAM)
604 switches = [{'name': switch_id, 'interface': interface}]
605
606 if not interface:
607 raise SdnConnectorError(message="Connection point switch port empty for switch_dpid {}".format(switch_id),
608 http_code=406)
609 for switch in switches:
610 # it should be only one switch where the mac is attached
611 if encap_type == 'dot1q':
612 # SRIOV configLet for Leaf switch mac's attached to
613 if service_type == self.__service_types_ELAN:
614 cl_encap = self.clC.getElan_sriov(service_uuid, interface, vlan_id, i)
615 else:
616 cl_encap = self.clC.getEline_sriov(service_uuid, interface, vlan_id, i)
617 elif not encap_type:
618 # PT configLet for Leaf switch attached to the mac
619 if service_type == self.__service_types_ELAN:
620 cl_encap = self.clC.getElan_passthrough(service_uuid,
621 interface,
622 vlan_id, i)
623 else:
624 cl_encap = self.clC.getEline_passthrough(service_uuid,
625 interface,
626 vlan_id, i)
627 if cls_cp.get(switch['name']):
628 cls_cp[switch['name']] = str(cls_cp[switch['name']]) + cl_encap
629 else:
630 cls_cp[switch['name']] = cl_encap
631
632 # at least 1 connection point has to be received
633 if not vlan_processed:
634 raise SdnConnectorError(message=SdnError.UNSUPPORTED_FEATURE,
635 http_code=406)
636
637 for s in self.s_api:
638 # for cl in cp_configLets:
639 cl_name = (self.__OSM_PREFIX +
640 s +
641 self.__SEPARATOR + service_type + str(vlan_id) +
642 self.__SEPARATOR + service_uuid)
643 # apply VLAN and BGP configLet to all Leaf switches
644 if service_type == self.__service_types_ELAN:
645 cl_bgp[s] = self.clC.getElan_bgp(service_uuid,
646 vlan_id,
647 vni_id,
648 self.sw_loopback0[s],
649 self.bgp[s])
650 else:
651 cl_bgp[s] = self.clC.getEline_bgp(service_uuid,
652 vlan_id,
653 vni_id,
654 self.sw_loopback0[s],
655 self.bgp[s])
656
657 if not cls_cp.get(s):
658 cl_config = ''
659 else:
660 cl_config = str(cl_vlan) + str(cl_bgp[s]) + str(cls_cp[s])
661
662 cls_perSw[s] = [{'name': cl_name, 'config': cl_config}]
663
664 allLeafConfigured, allLeafModified = self.__updateConnection(cls_perSw)
665
666 conn_info = {
667 "uuid": service_uuid,
668 "status": "BUILD",
669 "service_type": service_type,
670 "vlan_id": vlan_id,
671 "connection_points": connection_points,
672 "configLetPerSwitch": cls_perSw,
673 'allLeafConfigured': allLeafConfigured,
674 'allLeafModified': allLeafModified}
675
676 return service_uuid, conn_info
677 except Exception as ex:
678 self.logger.debug("Exception processing connection {}: {}".
679 format(service_uuid, str(ex)))
680 raise ex
681
682 def __updateConnection(self, cls_perSw):
683 """ Invoked in the creation and modification
684
685 checks if the new connection points config is:
686 - already in the Cloud Vision, the configLet is modified, and applied to the switch,
687 executing the corresponding task
688 - if it has to be removed:
689 then configuration has to be removed from the switch executing the corresponding task,
690 before trying to remove the configuration
691 - created, the configuration set is created, associated to the switch, and the associated
692 task to the configLet modification executed
693 In case of any error, rollback is executed, removing the created elements, and restoring to the
694 previous state.
695 """
696 try:
697 allLeafConfigured = {}
698 allLeafModified = {}
699
700 for s in self.s_api:
701 allLeafConfigured[s] = False
702 allLeafModified[s] = False
703 tasks = dict()
704 cl_toDelete = []
705 for s in self.s_api:
706 toDelete_in_cvp = False
707 if not (cls_perSw.get(s) and cls_perSw[s][0].get('config')):
708 # when there is no configuration, means that there is no interface
709 # in the switch to be connected, so the configLet has to be removed from CloudVision
710 # after removing the ConfigLet fron the switch if it was already there
711
712 # get config let name and key
713 cl = cls_perSw[s]
714 try:
715 cvp_cl = self.client.api.get_configlet_by_name(cl[0]['name'])
716 # remove configLet
717 cl_toDelete.append(cvp_cl)
718 cl[0] = cvp_cl
719 toDelete_in_cvp = True
720 except CvpApiError as error:
721 if "Entity does not exist" in error.msg:
722 continue
723 else:
724 raise error
725 # remove configLet from device
726 else:
727 res = self.__configlet_modify(cls_perSw[s])
728 allLeafConfigured[s] = res[0]
729 if not allLeafConfigured[s]:
730 continue
731 cl = cls_perSw[s]
732 res = self.__device_modify(
733 device_to_update=s,
734 new_configlets=cl,
735 delete=toDelete_in_cvp)
736 if "errorMessage" in str(res):
737 raise Exception(str(res))
738 self.logger.info("Device {} modify result {}".format(s, res))
739 for t_id in res[1]['tasks']:
740 tasks[t_id] = {'workOrderId': t_id}
741 if not toDelete_in_cvp:
742 note_msg = "## Managed by OSM {}{}{}##".format(self.__SEPARATOR,
743 t_id,
744 self.__SEPARATOR)
745 self.client.api.add_note_to_configlet(
746 cls_perSw[s][0]['key'],
747 note_msg)
748 cls_perSw[s][0].update([('note', note_msg)])
749 # with just one configLet assigned to a device,
750 # delete all if there are errors in next loops
751 if not toDelete_in_cvp:
752 allLeafModified[s] = True
753 if self.taskC is None:
754 self.__connect()
755 data = self.taskC.update_all_tasks(tasks).values()
756 self.taskC.task_action(data,
757 self.__EXC_TASK_EXEC_WAIT,
758 'executed')
759 if len(cl_toDelete) > 0:
760 self.__configlet_modify(cl_toDelete, delete=True)
761 return allLeafConfigured, allLeafModified
762 except Exception as ex:
763 try:
764 self.__rollbackConnection(cls_perSw,
765 allLeafConfigured=True,
766 allLeafModified=True)
767 except Exception as e:
768 self.logger.info("Exception rolling back in updating connection: {}".
769 format(e))
770 raise ex
771
772 def __rollbackConnection(self,
773 cls_perSw,
774 allLeafConfigured,
775 allLeafModified):
776 """ Removes the given configLet from the devices and then remove the configLets
777 """
778 tasks = dict()
779 for s in self.s_api:
780 if allLeafModified[s]:
781 try:
782 res = self.__device_modify(
783 device_to_update=s,
784 new_configlets=cls_perSw[s],
785 delete=True)
786 if "errorMessage" in str(res):
787 raise Exception(str(res))
788 for t_id in res[1]['tasks']:
789 tasks[t_id] = {'workOrderId': t_id}
790 self.logger.info("Device {} modify result {}".format(s, res))
791 except Exception as e:
792 self.logger.info('Error removing configlets from device {}: {}'.format(s, e))
793 pass
794 if self.taskC is None:
795 self.__connect()
796 data = self.taskC.update_all_tasks(tasks).values()
797 self.taskC.task_action(data,
798 self.__ROLLB_TASK_EXEC_WAIT,
799 'executed')
800 for s in self.s_api:
801 if allLeafConfigured[s]:
802 self.__configlet_modify(cls_perSw[s], delete=True)
803
804 def __device_modify(self, device_to_update, new_configlets, delete):
805 """ Updates the devices (switches) adding or removing the configLet,
806 the tasks Id's associated to the change are returned
807 """
808 self.logger.info('Enter in __device_modify delete: {}'.format(
809 delete))
810 updated = []
811 changed = False
812 # Task Ids that have been identified during device actions
813 newTasks = []
814
815 if (len(new_configlets) == 0 or
816 device_to_update is None or
817 len(device_to_update) == 0):
818 data = {'updated': updated, 'tasks': newTasks}
819 return [changed, data]
820
821 self.__load_inventory()
822
823 allDeviceFacts = self.allDeviceFacts
824 # Work through Devices list adding device specific information
825 device = None
826 for try_device in allDeviceFacts:
827 # Add Device Specific Configlets
828 # self.logger.debug(device)
829 if try_device['hostname'] not in device_to_update:
830 continue
831 dev_cvp_configlets = self.client.api.get_configlets_by_device_id(
832 try_device['systemMacAddress'])
833 # self.logger.debug(dev_cvp_configlets)
834 try_device['deviceSpecificConfiglets'] = []
835 for cvp_configlet in dev_cvp_configlets:
836 if int(cvp_configlet['containerCount']) == 0:
837 try_device['deviceSpecificConfiglets'].append(
838 {'name': cvp_configlet['name'],
839 'key': cvp_configlet['key']})
840 # self.logger.debug(device)
841 device = try_device
842 break
843
844 # Check assigned configlets
845 device_update = False
846 add_configlets = []
847 remove_configlets = []
848 update_devices = []
849
850 if delete:
851 for cvp_configlet in device['deviceSpecificConfiglets']:
852 for cl in new_configlets:
853 if cvp_configlet['name'] == cl['name']:
854 remove_configlets.append(cvp_configlet)
855 device_update = True
856 else:
857 for configlet in new_configlets:
858 if configlet not in device['deviceSpecificConfiglets']:
859 add_configlets.append(configlet)
860 device_update = True
861 if device_update:
862 update_devices.append({'hostname': device['hostname'],
863 'configlets': [add_configlets,
864 remove_configlets],
865 'device': device})
866 self.logger.info("Device to modify: {}".format(update_devices))
867
868 up_device = update_devices[0]
869 cl_toAdd = up_device['configlets'][0]
870 cl_toDel = up_device['configlets'][1]
871 # Update Configlets
872 try:
873 if delete and len(cl_toDel) > 0:
874 r = self.client.api.remove_configlets_from_device(
875 'OSM',
876 up_device['device'],
877 cl_toDel,
878 create_task=True)
879 dev_action = r
880 self.logger.debug("remove_configlets_from_device {} {}".format(dev_action, cl_toDel))
881 elif len(cl_toAdd) > 0:
882 r = self.client.api.apply_configlets_to_device(
883 'OSM',
884 up_device['device'],
885 cl_toAdd,
886 create_task=True)
887 dev_action = r
888 self.logger.debug("apply_configlets_to_device {} {}".format(dev_action, cl_toAdd))
889
890 except Exception as error:
891 errorMessage = str(error)
892 msg = "errorMessage: Device {} Configlets couldnot be updated: {}".format(
893 up_device['hostname'], errorMessage)
894 raise SdnConnectorError(msg) from error
895 else:
896 if "errorMessage" in str(dev_action):
897 m = "Device {} Configlets update fail: {}".format(
898 up_device['name'], dev_action['errorMessage'])
899 raise SdnConnectorError(m)
900 else:
901 changed = True
902 if 'taskIds' in str(dev_action):
903 for taskId in dev_action['data']['taskIds']:
904 updated.append({up_device['hostname']:
905 "Configlets-{}".format(
906 taskId)})
907 newTasks.append(taskId)
908 else:
909 updated.append({up_device['hostname']:
910 "Configlets-No_Specific_Tasks"})
911 data = {'updated': updated, 'tasks': newTasks}
912 return [changed, data]
913
914 def __configlet_modify(self, configletsToApply, delete=False):
915 ''' adds/update or delete the provided configLets
916 :param configletsToApply: list of configLets to apply
917 :param delete: flag to indicate if the configLets have to be deleted
918 from Cloud Vision Portal
919 :return: data: dict of module actions and taskIDs
920 '''
921 self.logger.info('Enter in __configlet_modify delete:{}'.format(
922 delete))
923
924 # Compare configlets against cvp_facts-configlets
925 changed = False
926 checked = []
927 deleted = []
928 updated = []
929 new = []
930
931 for cl in configletsToApply:
932 found_in_cvp = False
933 to_delete = False
934 to_update = False
935 to_create = False
936 to_check = False
937 try:
938 cvp_cl = self.client.api.get_configlet_by_name(cl['name'])
939 cl['key'] = cvp_cl['key']
940 cl['note'] = cvp_cl['note']
941 found_in_cvp = True
942 except CvpApiError as error:
943 if "Entity does not exist" in error.msg:
944 pass
945 else:
946 raise error
947
948 if delete:
949 if found_in_cvp:
950 to_delete = True
951 configlet = {'name': cvp_cl['name'],
952 'data': cvp_cl}
953 else:
954 if found_in_cvp:
955 cl_compare = self.__compare(cl['config'],
956 cvp_cl['config'])
957 # compare function returns a floating point number
958 if cl_compare[0] != 100.0:
959 to_update = True
960 configlet = {'name': cl['name'],
961 'data': cvp_cl,
962 'config': cl['config']}
963 else:
964 to_check = True
965 configlet = {'name': cl['name'],
966 'key': cvp_cl['key'],
967 'data': cvp_cl,
968 'config': cl['config']}
969 else:
970 to_create = True
971 configlet = {'name': cl['name'],
972 'config': cl['config']}
973 try:
974 if to_delete:
975 operation = 'delete'
976 resp = self.client.api.delete_configlet(
977 configlet['data']['name'],
978 configlet['data']['key'])
979 elif to_update:
980 operation = 'update'
981 resp = self.client.api.update_configlet(
982 configlet['config'],
983 configlet['data']['key'],
984 configlet['data']['name'])
985 elif to_create:
986 operation = 'create'
987 resp = self.client.api.add_configlet(
988 configlet['name'],
989 configlet['config'])
990 else:
991 operation = 'checked'
992 resp = 'checked'
993 except Exception as error:
994 errorMessage = str(error).split(':')[-1]
995 message = "Configlet {} cannot be {}: {}".format(
996 cl['name'], operation, errorMessage)
997 if to_delete:
998 deleted.append({configlet['name']: message})
999 elif to_update:
1000 updated.append({configlet['name']: message})
1001 elif to_create:
1002 new.append({configlet['name']: message})
1003 elif to_check:
1004 checked.append({configlet['name']: message})
1005
1006 else:
1007 if "error" in str(resp).lower():
1008 message = "Configlet {} cannot be deleted: {}".format(
1009 cl['name'], resp['errorMessage'])
1010 if to_delete:
1011 deleted.append({configlet['name']: message})
1012 elif to_update:
1013 updated.append({configlet['name']: message})
1014 elif to_create:
1015 new.append({configlet['name']: message})
1016 elif to_check:
1017 checked.append({configlet['name']: message})
1018 else:
1019 if to_delete:
1020 changed = True
1021 deleted.append({configlet['name']: "success"})
1022 elif to_update:
1023 changed = True
1024 updated.append({configlet['name']: "success"})
1025 elif to_create:
1026 changed = True
1027 cl['key'] = resp # This key is used in API call deviceApplyConfigLet FGA
1028 new.append({configlet['name']: "success"})
1029 elif to_check:
1030 changed = False
1031 checked.append({configlet['name']: "success"})
1032
1033 data = {'new': new, 'updated': updated, 'deleted': deleted, 'checked': checked}
1034 return [changed, data]
1035
1036 def __get_configletsDevices(self, configlets):
1037 for s in self.s_api:
1038 configlet = configlets[s]
1039 # Add applied Devices
1040 if len(configlet) > 0:
1041 configlet['devices'] = []
1042 applied_devices = self.client.api.get_applied_devices(
1043 configlet['name'])
1044 for device in applied_devices['data']:
1045 configlet['devices'].append(device['hostName'])
1046
1047 def __get_serviceData(self, service_uuid, service_type, vlan_id, conn_info=None):
1048 cls_perSw = {}
1049 for s in self.s_api:
1050 cls_perSw[s] = []
1051 if not conn_info:
1052 srv_cls = self.__get_serviceConfigLets(service_uuid,
1053 service_type,
1054 vlan_id)
1055 self.__get_configletsDevices(srv_cls)
1056 for s in self.s_api:
1057 cl = srv_cls[s]
1058 if len(cl) > 0:
1059 for dev in cl['devices']:
1060 cls_perSw[dev].append(cl)
1061 else:
1062 cls_perSw = conn_info['configLetPerSwitch']
1063 return cls_perSw
1064
1065 def delete_connectivity_service(self, service_uuid, conn_info=None):
1066 """
1067 Disconnect multi-site endpoints previously connected
1068
1069 :param service_uuid: The one returned by create_connectivity_service
1070 :param conn_info: The one returned by last call to 'create_connectivity_service' or 'edit_connectivity_service'
1071 if they do not return None
1072 :return: None
1073 :raises: SdnConnectorException: In case of error. The parameter http_code must be filled
1074 """
1075 try:
1076 self.logger.debug('invoked delete_connectivity_service {}'.
1077 format(service_uuid))
1078 if not service_uuid:
1079 raise SdnConnectorError(message='No connection service UUID',
1080 http_code=500)
1081
1082 self.__get_Connection()
1083 if conn_info is None:
1084 raise SdnConnectorError(message='No connection information for service UUID {}'.format(service_uuid),
1085 http_code=500)
1086 c_info = None
1087 cls_perSw = self.__get_serviceData(service_uuid,
1088 conn_info['service_type'],
1089 conn_info['vlan_id'],
1090 c_info)
1091 allLeafConfigured = {}
1092 allLeafModified = {}
1093 for s in self.s_api:
1094 allLeafConfigured[s] = True
1095 allLeafModified[s] = True
1096 found_in_cvp = False
1097 for s in self.s_api:
1098 if cls_perSw[s]:
1099 found_in_cvp = True
1100 if found_in_cvp:
1101 self.__rollbackConnection(cls_perSw,
1102 allLeafConfigured,
1103 allLeafModified)
1104 else:
1105 # if the service is not defined in Cloud Vision, return a 404 - NotFound error
1106 raise SdnConnectorError(message='Service {} was not found in Arista Cloud Vision {}'.
1107 format(service_uuid, self.__wim_url),
1108 http_code=404)
1109 self.__removeMetadata(service_uuid)
1110 except CvpLoginError as e:
1111 self.logger.info(str(e))
1112 self.client = None
1113 raise SdnConnectorError(message=SdnError.UNAUTHORIZED,
1114 http_code=401) from e
1115 except SdnConnectorError as sde:
1116 raise sde
1117 except Exception as ex:
1118 self.client = None
1119 self.logger.error(ex)
1120 if self.raiseException:
1121 raise ex
1122 raise SdnConnectorError(message=SdnError.INTERNAL_ERROR,
1123 http_code=500) from ex
1124
1125 def __addMetadata(self, service_uuid, service_type, vlan_id):
1126 """ Adds the connectivity service from 'OSM_metadata' configLet
1127 """
1128 found_in_cvp = False
1129 try:
1130 cvp_cl = self.client.api.get_configlet_by_name(self.__OSM_METADATA)
1131 found_in_cvp = True
1132 except CvpApiError as error:
1133 if "Entity does not exist" in error.msg:
1134 pass
1135 else:
1136 raise error
1137 try:
1138 new_serv = '{} {} {} {}\n'.format(self.__METADATA_PREFIX, service_type, vlan_id, service_uuid)
1139
1140 if found_in_cvp:
1141 cl_config = cvp_cl['config'] + new_serv
1142 else:
1143 cl_config = new_serv
1144 cl_meta = [{'name': self.__OSM_METADATA, 'config': cl_config}]
1145 self.__configlet_modify(cl_meta)
1146 except Exception as e:
1147 self.logger.error('Error in setting metadata in CloudVision from OSM for service {}: {}'.
1148 format(service_uuid, str(e)))
1149 pass
1150
1151 def __removeMetadata(self, service_uuid):
1152 """ Removes the connectivity service from 'OSM_metadata' configLet
1153 """
1154 found_in_cvp = False
1155 try:
1156 cvp_cl = self.client.api.get_configlet_by_name(self.__OSM_METADATA)
1157 found_in_cvp = True
1158 except CvpApiError as error:
1159 if "Entity does not exist" in error.msg:
1160 pass
1161 else:
1162 raise error
1163 try:
1164 if found_in_cvp:
1165 if service_uuid in cvp_cl['config']:
1166 cl_config = ''
1167 for line in cvp_cl['config'].split('\n'):
1168 if service_uuid in line:
1169 continue
1170 else:
1171 cl_config = cl_config + line
1172 cl_meta = [{'name': self.__OSM_METADATA, 'config': cl_config}]
1173 self.__configlet_modify(cl_meta)
1174 except Exception as e:
1175 self.logger.error('Error in removing metadata in CloudVision from OSM for service {}: {}'.
1176 format(service_uuid, str(e)))
1177 pass
1178
1179 def edit_connectivity_service(self,
1180 service_uuid,
1181 conn_info=None,
1182 connection_points=None,
1183 **kwargs):
1184 """ Change an existing connectivity service.
1185
1186 This method's arguments and return value follow the same convention as
1187 :meth:`~.create_connectivity_service`.
1188
1189 :param service_uuid: UUID of the connectivity service.
1190 :param conn_info: (dict or None): Information previously returned
1191 by last call to create_connectivity_service
1192 or edit_connectivity_service
1193 :param connection_points: (list): If provided, the old list of
1194 connection points will be replaced.
1195 :param kwargs: Same meaning that create_connectivity_service
1196 :return: dict or None: Information to be updated and stored at
1197 the database.
1198 When ``None`` is returned, no information should be changed.
1199 When an empty dict is returned, the database record will
1200 be deleted.
1201 **MUST** be JSON/YAML-serializable (plain data structures).
1202 Raises:
1203 SdnConnectorError: In case of error.
1204 """
1205 try:
1206 self.logger.debug('invoked edit_connectivity_service for service {}. ports: {}'.format(service_uuid,
1207 connection_points))
1208
1209 if not service_uuid:
1210 raise SdnConnectorError(message='Unable to perform operation, missing or empty uuid',
1211 http_code=500)
1212 if not conn_info:
1213 raise SdnConnectorError(message='Unable to perform operation, missing or empty connection information',
1214 http_code=500)
1215
1216 if connection_points is None:
1217 return None
1218
1219 self.__get_Connection()
1220
1221 cls_currentPerSw = conn_info['configLetPerSwitch']
1222 service_type = conn_info['service_type']
1223
1224 self.__check_service(service_type,
1225 connection_points,
1226 check_vlan=False,
1227 check_num_cp=False,
1228 kwargs=kwargs)
1229
1230 s_uid, s_connInf = self.__processConnection(
1231 service_uuid,
1232 service_type,
1233 connection_points,
1234 kwargs)
1235 self.logger.info("Service with uuid {} configuration updated".
1236 format(s_uid))
1237 return s_connInf
1238 except CvpLoginError as e:
1239 self.logger.info(str(e))
1240 self.client = None
1241 raise SdnConnectorError(message=SdnError.UNAUTHORIZED,
1242 http_code=401) from e
1243 except SdnConnectorError as sde:
1244 raise sde
1245 except Exception as ex:
1246 try:
1247 # Add previous
1248 # TODO check if there are pending task, and cancel them before restoring
1249 self.__updateConnection(cls_currentPerSw)
1250 except Exception as e:
1251 self.logger.error("Unable to restore configuration in service {} after an error in the configuration updated: {}".
1252 format(service_uuid, str(e)))
1253 if self.raiseException:
1254 raise ex
1255 raise SdnConnectorError(message=str(ex),
1256 http_code=500) from ex
1257
1258 def clear_all_connectivity_services(self):
1259 """ Removes all connectivity services from Arista CloudVision with two steps:
1260 - retrives all the services from Arista CloudVision
1261 - removes each service
1262 """
1263 try:
1264 self.logger.debug('invoked AristaImpl ' +
1265 'clear_all_connectivity_services')
1266 self.__get_Connection()
1267 s_list = self.__get_srvUUIDs()
1268 for serv in s_list:
1269 conn_info = {}
1270 conn_info['service_type'] = serv['type']
1271 conn_info['vlan_id'] = serv['vlan']
1272
1273 self.delete_connectivity_service(serv['uuid'], conn_info)
1274 except CvpLoginError as e:
1275 self.logger.info(str(e))
1276 self.client = None
1277 raise SdnConnectorError(message=SdnError.UNAUTHORIZED,
1278 http_code=401) from e
1279 except SdnConnectorError as sde:
1280 raise sde
1281 except Exception as ex:
1282 self.client = None
1283 self.logger.error(ex)
1284 if self.raiseException:
1285 raise ex
1286 raise SdnConnectorError(message=SdnError.INTERNAL_ERROR,
1287 http_code=500) from ex
1288
1289 def get_all_active_connectivity_services(self):
1290 """ Return the uuid of all the active connectivity services with two steps:
1291 - retrives all the services from Arista CloudVision
1292 - retrives the status of each server
1293 """
1294 try:
1295 self.logger.debug('invoked AristaImpl {}'.format(
1296 'get_all_active_connectivity_services'))
1297 self.__get_Connection()
1298 s_list = self.__get_srvUUIDs()
1299 result = []
1300 for serv in s_list:
1301 conn_info = {}
1302 conn_info['service_type'] = serv['type']
1303 conn_info['vlan_id'] = serv['vlan']
1304
1305 status = self.get_connectivity_service_status(serv['uuid'], conn_info)
1306 if status['sdn_status'] == 'ACTIVE':
1307 result.append(serv['uuid'])
1308 return result
1309 except CvpLoginError as e:
1310 self.logger.info(str(e))
1311 self.client = None
1312 raise SdnConnectorError(message=SdnError.UNAUTHORIZED,
1313 http_code=401) from e
1314 except SdnConnectorError as sde:
1315 raise sde
1316 except Exception as ex:
1317 self.client = None
1318 self.logger.error(ex)
1319 if self.raiseException:
1320 raise ex
1321 raise SdnConnectorError(message=SdnError.INTERNAL_ERROR,
1322 http_code=500) from ex
1323
1324 def __get_serviceConfigLets(self, service_uuid, service_type, vlan_id):
1325 """ Return the configLet's associated with a connectivity service,
1326 There should be one, as maximum, per device (switch) for a given
1327 connectivity service
1328 """
1329 srv_cls = {}
1330 for s in self.s_api:
1331 srv_cls[s] = []
1332 found_in_cvp = False
1333 name = (self.__OSM_PREFIX +
1334 s +
1335 self.__SEPARATOR + service_type + str(vlan_id) +
1336 self.__SEPARATOR + service_uuid)
1337 try:
1338 cvp_cl = self.client.api.get_configlet_by_name(name)
1339 found_in_cvp = True
1340 except CvpApiError as error:
1341 if "Entity does not exist" in error.msg:
1342 pass
1343 else:
1344 raise error
1345 if found_in_cvp:
1346 srv_cls[s] = cvp_cl
1347 return srv_cls
1348
1349 def __get_srvVLANs(self):
1350 """ Returns a list with all the VLAN id's used in the connectivity services managed
1351 in tha Arista CloudVision by checking the 'OSM_metadata' configLet where this
1352 information is stored
1353 """
1354 found_in_cvp = False
1355 try:
1356 cvp_cl = self.client.api.get_configlet_by_name(self.__OSM_METADATA)
1357 found_in_cvp = True
1358 except CvpApiError as error:
1359 if "Entity does not exist" in error.msg:
1360 pass
1361 else:
1362 raise error
1363 s_vlan_list = []
1364 if found_in_cvp:
1365 lines = cvp_cl['config'].split('\n')
1366 for line in lines:
1367 if self.__METADATA_PREFIX in line:
1368 s_vlan = line.split(' ')[3]
1369 else:
1370 continue
1371 if (s_vlan is not None and
1372 len(s_vlan) > 0 and
1373 s_vlan not in s_vlan_list):
1374 s_vlan_list.append(s_vlan)
1375
1376 return s_vlan_list
1377
1378 def __get_srvUUIDs(self):
1379 """ Retrieves all the connectivity services, managed in tha Arista CloudVision
1380 by checking the 'OSM_metadata' configLet where this information is stored
1381 """
1382 found_in_cvp = False
1383 try:
1384 cvp_cl = self.client.api.get_configlet_by_name(self.__OSM_METADATA)
1385 found_in_cvp = True
1386 except CvpApiError as error:
1387 if "Entity does not exist" in error.msg:
1388 pass
1389 else:
1390 raise error
1391 serv_list = []
1392 if found_in_cvp:
1393 lines = cvp_cl['config'].split('\n')
1394 for line in lines:
1395 if self.__METADATA_PREFIX in line:
1396 line = line.split(' ')
1397 serv = {'uuid': line[4], 'type': line[2], 'vlan': line[3]}
1398 else:
1399 continue
1400 if (serv is not None and
1401 len(serv) > 0 and
1402 serv not in serv_list):
1403 serv_list.append(serv)
1404
1405 return serv_list
1406
1407 def __get_Connection(self):
1408 """ Open a connection with Arista CloudVision,
1409 invoking the version retrival as test
1410 """
1411 try:
1412 if self.client is None:
1413 self.client = self.__connect()
1414 self.client.api.get_cvp_info()
1415 except (CvpSessionLogOutError, RequestException) as e:
1416 self.logger.debug("Connection error '{}'. Reconnencting".format(e))
1417 self.client = self.__connect()
1418 self.client.api.get_cvp_info()
1419
1420 def __connect(self):
1421 ''' Connects to CVP device using user provided credentials from initialization.
1422 :return: CvpClient object with connection instantiated.
1423 '''
1424 client = CvpClient()
1425 protocol, _, rest_url = self.__wim_url.rpartition("://")
1426 host, _, port = rest_url.partition(":")
1427 if port and port.endswith("/"):
1428 port = int(port[:-1])
1429 elif port:
1430 port = int(port)
1431 else:
1432 port = 443
1433
1434 client.connect([host],
1435 self.__user,
1436 self.__passwd,
1437 protocol=protocol or "https",
1438 port=port,
1439 connect_timeout=2)
1440 self.taskC = AristaCVPTask(client.api)
1441 return client
1442
1443 def __compare(self, fromText, toText, lines=10):
1444 """ Compare text string in 'fromText' with 'toText' and produce
1445 diffRatio - a score as a float in the range [0, 1] 2.0*M / T
1446 T is the total number of elements in both sequences,
1447 M is the number of matches.
1448 Score - 1.0 if the sequences are identical, and
1449 0.0 if they have nothing in common.
1450 unified diff list
1451 Code Meaning
1452 '- ' line unique to sequence 1
1453 '+ ' line unique to sequence 2
1454 ' ' line common to both sequences
1455 '? ' line not present in either input sequence
1456 """
1457 fromlines = fromText.splitlines(1)
1458 tolines = toText.splitlines(1)
1459 diff = list(difflib.unified_diff(fromlines, tolines, n=lines))
1460 textComp = difflib.SequenceMatcher(None, fromText, toText)
1461 diffRatio = round(textComp.quick_ratio()*100, 2)
1462 return [diffRatio, diff]
1463
1464 def __load_inventory(self):
1465 """ Get Inventory Data for All Devices (aka switches) from the Arista CloudVision
1466 """
1467 if not self.cvp_inventory:
1468 self.cvp_inventory = self.client.api.get_inventory()
1469 self.allDeviceFacts = []
1470 for device in self.cvp_inventory:
1471 self.allDeviceFacts.append(device)
1472
1473 def is_valid_destination(self, url):
1474 """ Check that the provided WIM URL is correct
1475 """
1476 if re.match(self.__regex, url):
1477 return True
1478 elif self.is_valid_ipv4_address(url):
1479 return True
1480 else:
1481 return self.is_valid_ipv6_address(url)
1482
1483 def is_valid_ipv4_address(self, address):
1484 """ Checks that the given IP is IPv4 valid
1485 """
1486 try:
1487 socket.inet_pton(socket.AF_INET, address)
1488 except AttributeError: # no inet_pton here, sorry
1489 try:
1490 socket.inet_aton(address)
1491 except socket.error:
1492 return False
1493 return address.count('.') == 3
1494 except socket.error: # not a valid address
1495 return False
1496 return True
1497
1498 def is_valid_ipv6_address(self, address):
1499 """ Checks that the given IP is IPv6 valid
1500 """
1501 try:
1502 socket.inet_pton(socket.AF_INET6, address)
1503 except socket.error: # not a valid address
1504 return False
1505 return True