314c6733e18d3f26940687f4a2cef0d41fdec1a7
[osm/RO.git] / RO-SDN-arista_cloudvision / osm_rosdn_arista_cloudvision / wimconn_arista.py
1 # -*- coding: utf-8 -*-
2 ##
3 # Copyright 2019 Atos - CoE Telco NFV Team
4 # All Rights Reserved.
5 #
6 # Contributors: Oscar Luis Peral, Atos
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License"); you may
9 # not use this file except in compliance with the License. You may obtain
10 # a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
17 # License for the specific language governing permissions and limitations
18 # under the License.
19 #
20 # For those usages not covered by the Apache License, Version 2.0 please
21 # contact with: <oscarluis.peral@atos.net>
22 #
23 # Neither the name of Atos nor the names of its
24 # contributors may be used to endorse or promote products derived from
25 # this software without specific prior written permission.
26 #
27 # This work has been performed in the context of Arista Telefonica OSM PoC.
28 ##
29
30 from osm_ro_plugin.sdnconn import SdnConnectorBase, SdnConnectorError
31 import re
32 import socket
33
34 # Required by compare function
35 import difflib
36
37 # Library that uses Levenshtein Distance to calculate the differences
38 # between strings.
39 # from fuzzywuzzy import fuzz
40
41 import logging
42 import uuid
43 from enum import Enum
44 from requests import RequestException, ConnectionError, ConnectTimeout, Timeout
45 from cvprac.cvp_client import CvpClient
46 from cvprac.cvp_api import CvpApi
47 from cvprac.cvp_client_errors import CvpLoginError, CvpSessionLogOutError, CvpApiError
48 from cvprac import __version__ as cvprac_version
49
50 from osm_rosdn_arista_cloudvision.aristaConfigLet import AristaSDNConfigLet
51 from osm_rosdn_arista_cloudvision.aristaTask import AristaCVPTask
52
53
54 class SdnError(Enum):
55 UNREACHABLE = "Unable to reach the WIM url, connect error."
56 TIMEOUT = "Unable to reach the WIM url, timeout."
57 VLAN_INCONSISTENT = "VLAN value inconsistent between the connection points"
58 VLAN_NOT_PROVIDED = "VLAN value not provided"
59 CONNECTION_POINTS_SIZE = "Unexpected number of connection points: 2 expected."
60 ENCAPSULATION_TYPE = (
61 'Unexpected service_endpoint_encapsulation_type. Only "dotq1" is accepted.'
62 )
63 BANDWIDTH = "Unable to get the bandwidth."
64 STATUS = "Unable to get the status for the service."
65 DELETE = "Unable to delete service."
66 CLEAR_ALL = "Unable to clear all the services"
67 UNKNOWN_ACTION = "Unknown action invoked."
68 BACKUP = "Unable to get the backup parameter."
69 UNSUPPORTED_FEATURE = "Unsupported feature"
70 UNAUTHORIZED = "Failed while authenticating"
71 INTERNAL_ERROR = "Internal error"
72
73
74 class AristaSdnConnector(SdnConnectorBase):
75 """Arista class for the SDN connectors
76
77 Arguments:
78 wim (dict): WIM record, as stored in the database
79 wim_account (dict): WIM account record, as stored in the database
80 config
81 The arguments of the constructor are converted to object attributes.
82 An extra property, ``service_endpoint_mapping`` is created from ``config``.
83
84 The access to Arista CloudVision is made through the API defined in
85 https://github.com/aristanetworks/cvprac
86 The a connectivity service consist in creating a VLAN and associate the interfaces
87 of the connection points MAC addresses to this VLAN in all the switches of the topology,
88 the BDP is also configured for this VLAN.
89
90 The Arista Cloud Vision API workflow is the following
91 -- The switch configuration is defined as a set of switch configuration commands,
92 what is called 'ConfigLet'
93 -- The ConfigLet is associated to the device (leaf switch)
94 -- Automatically a task is associated to this activity for change control, the task
95 in this stage is in 'Pending' state
96 -- The task will be executed so that the configuration is applied to the switch.
97 -- The service information is saved in the response of the creation call
98 -- All created services identification is stored in a generic ConfigLet 'OSM_metadata'
99 to keep track of the managed resources by OSM in the Arista deployment.
100 """
101
102 __supported_service_types = ["ELINE (L2)", "ELINE", "ELAN"]
103 __service_types_ELAN = "ELAN"
104 __service_types_ELINE = "ELINE"
105 __ELINE_num_connection_points = 2
106 __supported_service_types = ["ELINE", "ELAN"]
107 __supported_encapsulation_types = ["dot1q"]
108 __WIM_LOGGER = "ro.sdn.arista"
109 __SERVICE_ENDPOINT_MAPPING = "service_endpoint_mapping"
110 __ENCAPSULATION_TYPE_PARAM = "service_endpoint_encapsulation_type"
111 __ENCAPSULATION_INFO_PARAM = "service_endpoint_encapsulation_info"
112 __BACKUP_PARAM = "backup"
113 __BANDWIDTH_PARAM = "bandwidth"
114 __SERVICE_ENDPOINT_PARAM = "service_endpoint_id"
115 __MAC_PARAM = "mac"
116 __WAN_SERVICE_ENDPOINT_PARAM = "service_endpoint_id"
117 __WAN_MAPPING_INFO_PARAM = "service_mapping_info"
118 __DEVICE_ID_PARAM = "device_id"
119 __DEVICE_INTERFACE_ID_PARAM = "device_interface_id"
120 __SW_ID_PARAM = "switch_dpid"
121 __SW_PORT_PARAM = "switch_port"
122 __VLAN_PARAM = "vlan"
123 __VNI_PARAM = "vni"
124 __SEPARATOR = "_"
125 __MANAGED_BY_OSM = "## Managed by OSM "
126 __OSM_PREFIX = "osm_"
127 __OSM_METADATA = "OSM_metadata"
128 __METADATA_PREFIX = "!## Service"
129 __EXC_TASK_EXEC_WAIT = 10
130 __ROLLB_TASK_EXEC_WAIT = 10
131 __API_REQUEST_TOUT = 60
132 __SWITCH_TAG_NAME = "topology_type"
133 __SWITCH_TAG_VALUE = "leaf"
134 __LOOPBACK_INTF = "Loopback0"
135 _VLAN = "VLAN"
136 _VXLAN = "VXLAN"
137 _VLAN_MLAG = "VLAN-MLAG"
138 _VXLAN_MLAG = "VXLAN-MLAG"
139
140 def __init__(self, wim, wim_account, config=None, logger=None):
141 """
142
143 :param wim: (dict). Contains among others 'wim_url'
144 :param wim_account: (dict). Contains among others 'uuid' (internal id), 'name',
145 'sdn' (True if is intended for SDN-assist or False if intended for WIM), 'user', 'password'.
146 :param config: (dict or None): Particular information of plugin. These keys if present have a common meaning:
147 'mapping_not_needed': (bool) False by default or if missing, indicates that mapping is not needed.
148 'service_endpoint_mapping': (list) provides the internal endpoint mapping. The meaning is:
149 KEY meaning for WIM meaning for SDN assist
150 -------- -------- --------
151 device_id pop_switch_dpid compute_id
152 device_interface_id pop_switch_port compute_pci_address
153 service_endpoint_id wan_service_endpoint_id SDN_service_endpoint_id
154 service_mapping_info wan_service_mapping_info SDN_service_mapping_info
155 contains extra information if needed. Text in Yaml format
156 switch_dpid wan_switch_dpid SDN_switch_dpid
157 switch_port wan_switch_port SDN_switch_port
158 datacenter_id vim_account vim_account
159 id: (internal, do not use)
160 wim_id: (internal, do not use)
161 :param logger (logging.Logger): optional logger object. If none is passed 'ro.sdn.sdnconn' is used.
162 """
163 self.__regex = re.compile(
164 r"^(?:http|ftp)s?://" # http:// or https://
165 r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|" # domain...
166 r"localhost|" # localhost...
167 r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})" # ...or ip
168 r"(?::\d+)?",
169 re.IGNORECASE,
170 ) # optional port
171 self.raiseException = True
172 self.logger = logger or logging.getLogger(self.__WIM_LOGGER)
173 super().__init__(wim, wim_account, config, self.logger)
174 self.__wim = wim
175 self.__wim_account = wim_account
176 self.__config = config
177
178 if self.is_valid_destination(self.__wim.get("wim_url")):
179 self.__wim_url = self.__wim.get("wim_url")
180 else:
181 raise SdnConnectorError(message="Invalid wim_url value", http_code=500)
182
183 self.__user = wim_account.get("user")
184 self.__passwd = wim_account.get("password")
185 self.client = None
186 self.cvp_inventory = None
187 self.cvp_tags = None
188 self.logger.debug(
189 "Arista SDN plugin {}, cvprac version {}, user:{} and config:{}".format(
190 wim,
191 cvprac_version,
192 self.__user,
193 self.delete_keys_from_dict(config, ("passwd",)),
194 )
195 )
196 self.allDeviceFacts = []
197 self.taskC = None
198
199 try:
200 self.__load_topology()
201 self.__load_switches()
202 except (ConnectTimeout, Timeout) as ct:
203 raise SdnConnectorError(
204 message=SdnError.TIMEOUT + " " + str(ct), http_code=408
205 )
206 except ConnectionError as ce:
207 raise SdnConnectorError(
208 message=SdnError.UNREACHABLE + " " + str(ce), http_code=404
209 )
210 except SdnConnectorError as sc:
211 raise sc
212 except CvpLoginError as le:
213 raise SdnConnectorError(message=le.msg, http_code=500) from le
214 except Exception as e:
215 raise SdnConnectorError(
216 message="Unable to load switches from CVP " + str(e), http_code=500
217 ) from e
218
219 self.logger.debug(
220 "Using topology {} in Arista Leaf switches: {}".format(
221 self.topology, self.delete_keys_from_dict(self.switches, ("passwd",))
222 )
223 )
224 self.clC = AristaSDNConfigLet(self.topology)
225
226 def __load_topology(self):
227 self.topology = self._VXLAN_MLAG
228
229 if self.__config and self.__config.get("topology"):
230 topology = self.__config.get("topology")
231
232 if topology == "VLAN":
233 self.topology = self._VLAN
234 elif topology == "VXLAN":
235 self.topology = self._VXLAN
236 elif topology == "VLAN-MLAG":
237 self.topology = self._VLAN_MLAG
238 elif topology == "VXLAN-MLAG":
239 self.topology = self._VXLAN_MLAG
240
241 def __load_switches(self):
242 """Retrieves the switches to configure in the following order
243 1. from incoming configuration:
244 1.1 using port mapping
245 using user and password from WIM
246 retrieving Lo0 and AS from switch
247 1.2 from 'switches' parameter,
248 if any parameter is not present
249 Lo0 and AS - it will be requested to the switch
250 2. Looking in the CloudVision inventory if not in configuration parameters
251 2.1 using the switches with the topology_type tag set to 'leaf'
252
253 All the search methods will be used
254 """
255 self.switches = {}
256 if self.__config and self.__config.get(self.__SERVICE_ENDPOINT_MAPPING):
257 for port in self.__config.get(self.__SERVICE_ENDPOINT_MAPPING):
258 switch_dpid = port.get(self.__SW_ID_PARAM)
259 if switch_dpid and switch_dpid not in self.switches:
260 self.switches[switch_dpid] = {
261 "passwd": self.__passwd,
262 "ip": None,
263 "usr": self.__user,
264 "lo0": None,
265 "AS": None,
266 "serialNumber": None,
267 "mlagPeerDevice": None,
268 }
269
270 if self.__config and self.__config.get("switches"):
271 # Not directly from json, complete one by one
272 config_switches = self.__config.get("switches")
273 for cs, cs_content in config_switches.items():
274 if cs not in self.switches:
275 self.switches[cs] = {
276 "passwd": self.__passwd,
277 "ip": None,
278 "usr": self.__user,
279 "lo0": None,
280 "AS": None,
281 "serialNumber": None,
282 "mlagPeerDevice": None,
283 }
284
285 if cs_content:
286 self.switches[cs].update(cs_content)
287
288 # Load the rest of the data
289 if self.client is None:
290 self.client = self.__connect()
291
292 self.__load_inventory()
293
294 if not self.switches:
295 self.__get_tags(self.__SWITCH_TAG_NAME, self.__SWITCH_TAG_VALUE)
296
297 for device in self.allDeviceFacts:
298 # get the switches whose topology_tag is 'leaf'
299 if device["serialNumber"] in self.cvp_tags:
300 if not self.switches.get(device["hostname"]):
301 switch_data = {
302 "passwd": self.__passwd,
303 "ip": device["ipAddress"],
304 "usr": self.__user,
305 "lo0": None,
306 "AS": None,
307 "serialNumber": None,
308 "mlagPeerDevice": None,
309 }
310 self.switches[device["hostname"]] = switch_data
311
312 if len(self.switches) == 0:
313 self.logger.error("Unable to load Leaf switches from CVP")
314 return
315
316 # self.switches are switch objects, one for each switch in self.switches,
317 # used to make eAPI calls by using switch.py module
318 for s in self.switches:
319 for device in self.allDeviceFacts:
320 if device["hostname"] == s:
321 if not self.switches[s].get("ip"):
322 self.switches[s]["ip"] = device["ipAddress"]
323 self.switches[s]["serialNumber"] = device["serialNumber"]
324 break
325
326 # Each switch has a different loopback address,
327 # so it's a different configLet
328 if not self.switches[s].get("lo0"):
329 inf = self.__get_interface_ip(
330 self.switches[s]["serialNumber"], self.__LOOPBACK_INTF
331 )
332 self.switches[s]["lo0"] = inf.split("/")[0]
333
334 if not self.switches[s].get("AS"):
335 self.switches[s]["AS"] = self.__get_device_ASN(
336 self.switches[s]["serialNumber"]
337 )
338
339 if self.topology in (self._VXLAN_MLAG, self._VLAN_MLAG):
340 for s in self.switches:
341 if not self.switches[s].get("mlagPeerDevice"):
342 self.switches[s]["mlagPeerDevice"] = self.__get_peer_MLAG(
343 self.switches[s]["serialNumber"]
344 )
345
346 def __check_service(
347 self,
348 service_type,
349 connection_points,
350 check_vlan=True,
351 check_num_cp=True,
352 kwargs=None,
353 ):
354 """Reviews the connection points elements looking for semantic errors in the incoming data"""
355 if service_type not in self.__supported_service_types:
356 raise Exception(
357 "The service '{}' is not supported. Only '{}' are accepted".format(
358 service_type, self.__supported_service_types
359 )
360 )
361
362 if check_num_cp:
363 if len(connection_points) < 2:
364 raise Exception(SdnError.CONNECTION_POINTS_SIZE)
365
366 if (
367 len(connection_points) != self.__ELINE_num_connection_points
368 and service_type == self.__service_types_ELINE
369 ):
370 raise Exception(SdnError.CONNECTION_POINTS_SIZE)
371
372 if check_vlan:
373 vlan_id = ""
374
375 for cp in connection_points:
376 enc_type = cp.get(self.__ENCAPSULATION_TYPE_PARAM)
377
378 if enc_type and enc_type not in self.__supported_encapsulation_types:
379 raise Exception(SdnError.ENCAPSULATION_TYPE)
380
381 encap_info = cp.get(self.__ENCAPSULATION_INFO_PARAM)
382 cp_vlan_id = str(encap_info.get(self.__VLAN_PARAM))
383
384 if cp_vlan_id:
385 if not vlan_id:
386 vlan_id = cp_vlan_id
387 elif vlan_id != cp_vlan_id:
388 raise Exception(SdnError.VLAN_INCONSISTENT)
389
390 if not vlan_id:
391 raise Exception(SdnError.VLAN_NOT_PROVIDED)
392
393 if vlan_id in self.__get_srvVLANs():
394 raise Exception(
395 "VLAN {} already assigned to a connectivity service".format(vlan_id)
396 )
397
398 # Commented out for as long as parameter isn't implemented
399 # bandwidth = kwargs.get(self.__BANDWIDTH_PARAM)
400 # if not isinstance(bandwidth, int):
401 # self.__exception(SdnError.BANDWIDTH, http_code=400)
402
403 # Commented out for as long as parameter isn't implemented
404 # backup = kwargs.get(self.__BACKUP_PARAM)
405 # if not isinstance(backup, bool):
406 # self.__exception(SdnError.BACKUP, http_code=400)
407
408 def check_credentials(self):
409 """Retrieves the CloudVision version information, as the easiest way
410 for testing the access to CloudVision API
411 """
412 try:
413 if self.client is None:
414 self.client = self.__connect()
415
416 result = self.client.api.get_cvp_info()
417 self.logger.debug(result)
418 except CvpLoginError as e:
419 self.logger.info(str(e))
420 self.client = None
421
422 raise SdnConnectorError(
423 message=SdnError.UNAUTHORIZED + " " + str(e), http_code=401
424 ) from e
425 except Exception as ex:
426 self.client = None
427 self.logger.error(str(ex))
428
429 raise SdnConnectorError(
430 message=SdnError.INTERNAL_ERROR + " " + str(ex), http_code=500
431 ) from ex
432
433 def get_connectivity_service_status(self, service_uuid, conn_info=None):
434 """Monitor the status of the connectivity service established
435 Arguments:
436 service_uuid (str): UUID of the connectivity service
437 conn_info (dict or None): Information returned by the connector
438 during the service creation/edition and subsequently stored in
439 the database.
440
441 Returns:
442 dict: JSON/YAML-serializable dict that contains a mandatory key
443 ``sdn_status`` associated with one of the following values::
444
445 {'sdn_status': 'ACTIVE'}
446 # The service is up and running.
447
448 {'sdn_status': 'INACTIVE'}
449 # The service was created, but the connector
450 # cannot determine yet if connectivity exists
451 # (ideally, the caller needs to wait and check again).
452
453 {'sdn_status': 'DOWN'}
454 # Connection was previously established,
455 # but an error/failure was detected.
456
457 {'sdn_status': 'ERROR'}
458 # An error occurred when trying to create the service/
459 # establish the connectivity.
460
461 {'sdn_status': 'BUILD'}
462 # Still trying to create the service, the caller
463 # needs to wait and check again.
464
465 Additionally ``error_msg``(**str**) and ``sdn_info``(**dict**)
466 keys can be used to provide additional status explanation or
467 new information available for the connectivity service.
468 """
469 try:
470 self.logger.debug(
471 "invoked get_connectivity_service_status '{}'".format(service_uuid)
472 )
473
474 if not service_uuid:
475 raise SdnConnectorError(
476 message="No connection service UUID", http_code=500
477 )
478
479 self.__get_Connection()
480
481 if conn_info is None:
482 raise SdnConnectorError(
483 message="No connection information for service UUID {}".format(
484 service_uuid
485 ),
486 http_code=500,
487 )
488
489 if "configLetPerSwitch" in conn_info.keys():
490 c_info = conn_info
491 else:
492 c_info = None
493
494 cls_perSw = self.__get_serviceData(
495 service_uuid, conn_info["service_type"], conn_info["vlan_id"], c_info
496 )
497
498 t_isCancelled = False
499 t_isFailed = False
500 t_isPending = False
501 failed_switches = []
502
503 for s in self.switches:
504 if len(cls_perSw[s]) > 0:
505 for cl in cls_perSw[s]:
506 # Fix 1030 SDN-ARISTA Key error note when deploy a NS
507 # Added protection to check that 'note' exists and additionally
508 # verify that it is managed by OSM
509 if (
510 not cls_perSw[s][0]["config"]
511 or not cl.get("note")
512 or self.__MANAGED_BY_OSM not in cl["note"]
513 ):
514 continue
515
516 note = cl["note"]
517 t_id = note.split(self.__SEPARATOR)[1]
518 result = self.client.api.get_task_by_id(t_id)
519
520 if result["workOrderUserDefinedStatus"] == "Completed":
521 continue
522 elif result["workOrderUserDefinedStatus"] == "Cancelled":
523 t_isCancelled = True
524 elif result["workOrderUserDefinedStatus"] == "Failed":
525 t_isFailed = True
526 else:
527 t_isPending = True
528
529 failed_switches.append(s)
530
531 if t_isCancelled:
532 error_msg = "Some works were cancelled in switches: {}".format(
533 str(failed_switches)
534 )
535 sdn_status = "DOWN"
536 elif t_isFailed:
537 error_msg = "Some works failed in switches: {}".format(
538 str(failed_switches)
539 )
540 sdn_status = "ERROR"
541 elif t_isPending:
542 error_msg = (
543 "Some works are still under execution in switches: {}".format(
544 str(failed_switches)
545 )
546 )
547 sdn_status = "BUILD"
548 else:
549 error_msg = ""
550 sdn_status = "ACTIVE"
551
552 sdn_info = ""
553
554 return {
555 "sdn_status": sdn_status,
556 "error_msg": error_msg,
557 "sdn_info": sdn_info,
558 }
559 except CvpLoginError as e:
560 self.logger.info(str(e))
561 self.client = None
562
563 raise SdnConnectorError(
564 message=SdnError.UNAUTHORIZED + " " + str(e), http_code=401
565 ) from e
566 except Exception as ex:
567 self.client = None
568 self.logger.error(str(ex), exc_info=True)
569
570 raise SdnConnectorError(
571 message=str(ex) + " " + str(ex), http_code=500
572 ) from ex
573
574 def create_connectivity_service(self, service_type, connection_points, **kwargs):
575 """Establish SDN/WAN connectivity between the endpoints
576 :param service_type:
577 (str): ``ELINE`` (L2), ``ELAN`` (L2), ``ETREE`` (L2), ``L3``.
578 :param connection_points: (list): each point corresponds to
579 an entry point to be connected. For WIM: from the DC
580 to the transport network.
581 For SDN: Compute/PCI to the transport network. One
582 connection point serves to identify the specific access and
583 some other service parameters, such as encapsulation type.
584 Each item of the list is a dict with:
585 "service_endpoint_id": (str)(uuid) Same meaning that for
586 'service_endpoint_mapping' (see __init__)
587 In case the config attribute mapping_not_needed is True,
588 this value is not relevant. In this case
589 it will contain the string "device_id:device_interface_id"
590 "service_endpoint_encapsulation_type": None, "dot1q", ...
591 "service_endpoint_encapsulation_info": (dict) with:
592 "vlan": ..., (int, present if encapsulation is dot1q)
593 "vni": ... (int, present if encapsulation is vxlan),
594 "peers": [(ipv4_1), (ipv4_2)] (present if
595 encapsulation is vxlan)
596 "mac": ...
597 "device_id": ..., same meaning that for
598 'service_endpoint_mapping' (see __init__)
599 "device_interface_id": same meaning that for
600 'service_endpoint_mapping' (see __init__)
601 "switch_dpid": ..., present if mapping has been found
602 for this device_id,device_interface_id
603 "switch_port": ... present if mapping has been found
604 for this device_id,device_interface_id
605 "service_mapping_info": present if mapping has
606 been found for this device_id,device_interface_id
607 :param kwargs: For future versions:
608 bandwidth (int): value in kilobytes
609 latency (int): value in milliseconds
610 Other QoS might be passed as keyword arguments.
611 :return: tuple: ``(service_id, conn_info)`` containing:
612 - *service_uuid* (str): UUID of the established
613 connectivity service
614 - *conn_info* (dict or None): Information to be
615 stored at the database (or ``None``).
616 This information will be provided to the
617 :meth:`~.edit_connectivity_service` and :obj:`~.delete`.
618 **MUST** be JSON/YAML-serializable (plain data structures).
619 :raises: SdnConnectorError: In case of error. Nothing should be
620 created in this case.
621 Provide the parameter http_code
622 """
623 try:
624 self.logger.debug(
625 "invoked create_connectivity_service '{}' ports: {}".format(
626 service_type, connection_points
627 )
628 )
629 self.__get_Connection()
630 self.__check_service(
631 service_type, connection_points, check_vlan=True, kwargs=kwargs
632 )
633 service_uuid = str(uuid.uuid4())
634
635 self.logger.info("Service with uuid {} created.".format(service_uuid))
636 s_uid, s_connInf = self.__processConnection(
637 service_uuid, service_type, connection_points, kwargs
638 )
639
640 try:
641 self.__addMetadata(s_uid, service_type, s_connInf["vlan_id"])
642 except Exception:
643 pass
644
645 return (s_uid, s_connInf)
646 except CvpLoginError as e:
647 self.logger.info(str(e))
648 self.client = None
649
650 raise SdnConnectorError(
651 message=SdnError.UNAUTHORIZED + " " + str(e), http_code=401
652 ) from e
653 except SdnConnectorError as sde:
654 raise sde
655 except ValueError as err:
656 self.client = None
657 self.logger.error(str(err), exc_info=True)
658
659 raise SdnConnectorError(message=str(err), http_code=500) from err
660 except Exception as ex:
661 self.client = None
662 self.logger.error(str(ex), exc_info=True)
663
664 if self.raiseException:
665 raise ex
666
667 raise SdnConnectorError(message=str(ex), http_code=500) from ex
668
669 def __processConnection(
670 self, service_uuid, service_type, connection_points, kwargs
671 ):
672 """
673 Invoked from creation and edit methods
674
675 Process the connection points array,
676 creating a set of configuration per switch where it has to be applied
677 for creating the configuration, the switches have to be queried for obtaining:
678 - the loopback address
679 - the BGP ASN (autonomous system number)
680 - the interface name of the MAC address to add in the connectivity service
681 Once the new configuration is ready, the __updateConnection method is invoked for appling the changes
682 """
683 try:
684 cls_perSw = {}
685 cls_cp = {}
686 cl_bgp = {}
687
688 for s in self.switches:
689 cls_perSw[s] = []
690 cls_cp[s] = []
691
692 vlan_processed = False
693 vlan_id = ""
694 i = 0
695 processed_connection_points = []
696
697 for cp in connection_points:
698 i += 1
699 encap_info = cp.get(self.__ENCAPSULATION_INFO_PARAM)
700
701 if not vlan_processed:
702 vlan_id = str(encap_info.get(self.__VLAN_PARAM))
703
704 if not vlan_id:
705 continue
706
707 vni_id = encap_info.get(self.__VNI_PARAM)
708
709 if not vni_id:
710 vni_id = str(10000 + int(vlan_id))
711
712 if service_type == self.__service_types_ELAN:
713 cl_vlan = self.clC.getElan_vlan(service_uuid, vlan_id, vni_id)
714 else:
715 cl_vlan = self.clC.getEline_vlan(service_uuid, vlan_id, vni_id)
716
717 vlan_processed = True
718
719 encap_type = cp.get(self.__ENCAPSULATION_TYPE_PARAM)
720 switch_id = encap_info.get(self.__SW_ID_PARAM)
721 interface = encap_info.get(self.__SW_PORT_PARAM)
722 switches = [{"name": switch_id, "interface": interface}]
723
724 # remove those connections that are equal. This happens when several sriovs are located in the same
725 # compute node interface, that is, in the same switch and interface
726 switches = [x for x in switches if x not in processed_connection_points]
727
728 if not switches:
729 continue
730
731 processed_connection_points += switches
732
733 for switch in switches:
734 if not interface:
735 raise SdnConnectorError(
736 message="Connection point switch port empty for switch_dpid {}".format(
737 switch_id
738 ),
739 http_code=406,
740 )
741 # it should be only one switch where the mac is attached
742 if encap_type == "dot1q":
743 # SRIOV configLet for Leaf switch mac's attached to
744 if service_type == self.__service_types_ELAN:
745 cl_encap = self.clC.getElan_sriov(
746 service_uuid, interface, vlan_id, i
747 )
748 else:
749 cl_encap = self.clC.getEline_sriov(
750 service_uuid, interface, vlan_id, i
751 )
752 elif not encap_type:
753 # PT configLet for Leaf switch attached to the mac
754 if service_type == self.__service_types_ELAN:
755 cl_encap = self.clC.getElan_passthrough(
756 service_uuid, interface, vlan_id, i
757 )
758 else:
759 cl_encap = self.clC.getEline_passthrough(
760 service_uuid, interface, vlan_id, i
761 )
762
763 if cls_cp.get(switch["name"]):
764 cls_cp[switch["name"]] = str(cls_cp[switch["name"]]) + cl_encap
765 else:
766 cls_cp[switch["name"]] = cl_encap
767
768 # at least 1 connection point has to be received
769 if not vlan_processed:
770 raise SdnConnectorError(
771 message=SdnError.UNSUPPORTED_FEATURE, http_code=406
772 )
773
774 for s in self.switches:
775 # for cl in cp_configLets:
776 cl_name = (
777 self.__OSM_PREFIX
778 + s
779 + self.__SEPARATOR
780 + service_type
781 + str(vlan_id)
782 + self.__SEPARATOR
783 + service_uuid
784 )
785 cl_config = ""
786
787 # Apply BGP configuration only for VXLAN topologies
788 if self.topology in (self._VXLAN_MLAG, self._VXLAN):
789 if service_type == self.__service_types_ELAN:
790 cl_bgp[s] = self.clC.getElan_bgp(
791 service_uuid,
792 vlan_id,
793 vni_id,
794 self.switches[s]["lo0"],
795 self.switches[s]["AS"],
796 )
797 else:
798 cl_bgp[s] = self.clC.getEline_bgp(
799 service_uuid,
800 vlan_id,
801 vni_id,
802 self.switches[s]["lo0"],
803 self.switches[s]["AS"],
804 )
805 else:
806 cl_bgp[s] = ""
807
808 if not cls_cp.get(s):
809 # Apply VLAN configuration to peer MLAG switch,
810 # only necessary when there are no connection points in the switch
811 if self.topology in (self._VXLAN_MLAG, self._VLAN_MLAG):
812 for p in self.switches:
813 if self.switches[p]["mlagPeerDevice"] == s:
814 if cls_cp.get(p):
815 if self.topology == self._VXLAN_MLAG:
816 cl_config = str(cl_vlan) + str(cl_bgp[s])
817 else:
818 cl_config = str(cl_vlan)
819 else:
820 cl_config = str(cl_vlan) + str(cl_bgp[s]) + str(cls_cp[s])
821
822 cls_perSw[s] = [{"name": cl_name, "config": cl_config}]
823
824 allLeafConfigured, allLeafModified = self.__updateConnection(cls_perSw)
825
826 conn_info = {
827 "uuid": service_uuid,
828 "status": "BUILD",
829 "service_type": service_type,
830 "vlan_id": vlan_id,
831 "connection_points": connection_points,
832 "configLetPerSwitch": cls_perSw,
833 "allLeafConfigured": allLeafConfigured,
834 "allLeafModified": allLeafModified,
835 }
836
837 return service_uuid, conn_info
838 except Exception as ex:
839 self.logger.debug(
840 "Exception processing connection {}: {}".format(service_uuid, str(ex))
841 )
842 raise ex
843
844 def __updateConnection(self, cls_perSw):
845 """Invoked in the creation and modification
846
847 checks if the new connection points config is:
848 - already in the Cloud Vision, the configLet is modified, and applied to the switch,
849 executing the corresponding task
850 - if it has to be removed:
851 then configuration has to be removed from the switch executing the corresponding task,
852 before trying to remove the configuration
853 - created, the configuration set is created, associated to the switch, and the associated
854 task to the configLet modification executed
855 In case of any error, rollback is executed, removing the created elements, and restoring to the
856 previous state.
857 """
858 try:
859 allLeafConfigured = {}
860 allLeafModified = {}
861
862 for s in self.switches:
863 allLeafConfigured[s] = False
864 allLeafModified[s] = False
865
866 cl_toDelete = []
867
868 for s in self.switches:
869 toDelete_in_cvp = False
870 if not (cls_perSw.get(s) and cls_perSw[s][0].get("config")):
871 # when there is no configuration, means that there is no interface
872 # in the switch to be connected, so the configLet has to be removed from CloudVision
873 # after removing the ConfigLet from the switch if it was already there
874
875 # get config let name and key
876 cl = cls_perSw[s]
877
878 try:
879 cvp_cl = self.client.api.get_configlet_by_name(cl[0]["name"])
880 # remove configLet
881 cl_toDelete.append(cvp_cl)
882 cl[0] = cvp_cl
883 toDelete_in_cvp = True
884 except CvpApiError as error:
885 if "Entity does not exist" in error.msg:
886 continue
887 else:
888 raise error
889 # remove configLet from device
890 else:
891 res = self.__configlet_modify(cls_perSw[s])
892 allLeafConfigured[s] = res[0]
893
894 if not allLeafConfigured[s]:
895 continue
896
897 cl = cls_perSw[s]
898
899 res = self.__device_modify(
900 device_to_update=s, new_configlets=cl, delete=toDelete_in_cvp
901 )
902
903 if "errorMessage" in str(res):
904 raise Exception(str(res))
905
906 self.logger.info("Device {} modify result {}".format(s, res))
907
908 for t_id in res[1]["tasks"]:
909 if not toDelete_in_cvp:
910 note_msg = "{}{}{}{}##".format(
911 self.__MANAGED_BY_OSM,
912 self.__SEPARATOR,
913 t_id,
914 self.__SEPARATOR,
915 )
916 self.client.api.add_note_to_configlet(
917 cls_perSw[s][0]["key"], note_msg
918 )
919 cls_perSw[s][0]["note"] = note_msg
920
921 tasks = {t_id: {"workOrderId": t_id}}
922 self.__exec_task(tasks, self.__EXC_TASK_EXEC_WAIT)
923
924 # with just one configLet assigned to a device,
925 # delete all if there are errors in next loops
926 if not toDelete_in_cvp:
927 allLeafModified[s] = True
928
929 if len(cl_toDelete) > 0:
930 self.__configlet_modify(cl_toDelete, delete=True)
931
932 return allLeafConfigured, allLeafModified
933 except Exception as ex:
934 try:
935 self.__rollbackConnection(cls_perSw, allLeafConfigured, allLeafModified)
936 except Exception as e:
937 self.logger.error(
938 "Exception rolling back in updating connection: {}".format(e),
939 exc_info=True,
940 )
941
942 raise ex
943
944 def __rollbackConnection(self, cls_perSw, allLeafConfigured, allLeafModified):
945 """Removes the given configLet from the devices and then remove the configLets"""
946 for s in self.switches:
947 if allLeafModified[s]:
948 try:
949 res = self.__device_modify(
950 device_to_update=s,
951 new_configlets=cls_perSw[s],
952 delete=True,
953 )
954
955 if "errorMessage" in str(res):
956 raise Exception(str(res))
957
958 tasks = dict()
959
960 for t_id in res[1]["tasks"]:
961 tasks[t_id] = {"workOrderId": t_id}
962
963 self.__exec_task(tasks)
964 self.logger.info("Device {} modify result {}".format(s, res))
965 except Exception as e:
966 self.logger.error(
967 "Error removing configlets from device {}: {}".format(s, e)
968 )
969 pass
970
971 for s in self.switches:
972 if allLeafConfigured[s]:
973 self.__configlet_modify(cls_perSw[s], delete=True)
974
975 def __exec_task(self, tasks, tout=10):
976 if self.taskC is None:
977 self.__connect()
978
979 data = self.taskC.update_all_tasks(tasks).values()
980 self.taskC.task_action(data, tout, "executed")
981
982 def __device_modify(self, device_to_update, new_configlets, delete):
983 """Updates the devices (switches) adding or removing the configLet,
984 the tasks Id's associated to the change are returned
985 """
986 self.logger.info("Enter in __device_modify delete: {}".format(delete))
987 updated = []
988 changed = False
989 # Task Ids that have been identified during device actions
990 newTasks = []
991
992 if (
993 len(new_configlets) == 0
994 or device_to_update is None
995 or len(device_to_update) == 0
996 ):
997 data = {"updated": updated, "tasks": newTasks}
998
999 return [changed, data]
1000
1001 self.__load_inventory()
1002
1003 allDeviceFacts = self.allDeviceFacts
1004 # Work through Devices list adding device specific information
1005 device = None
1006 for try_device in allDeviceFacts:
1007 # Add Device Specific Configlets
1008 # self.logger.debug(device)
1009 if try_device["hostname"] not in device_to_update:
1010 continue
1011
1012 dev_cvp_configlets = self.client.api.get_configlets_by_device_id(
1013 try_device["systemMacAddress"]
1014 )
1015 # self.logger.debug(dev_cvp_configlets)
1016 try_device["deviceSpecificConfiglets"] = []
1017
1018 for cvp_configlet in dev_cvp_configlets:
1019 if int(cvp_configlet["containerCount"]) == 0:
1020 try_device["deviceSpecificConfiglets"].append(
1021 {"name": cvp_configlet["name"], "key": cvp_configlet["key"]}
1022 )
1023
1024 # self.logger.debug(device)
1025 device = try_device
1026 break
1027
1028 # Check assigned configlets
1029 device_update = False
1030 add_configlets = []
1031 remove_configlets = []
1032 update_devices = []
1033
1034 if delete:
1035 for cvp_configlet in device["deviceSpecificConfiglets"]:
1036 for cl in new_configlets:
1037 if cvp_configlet["name"] == cl["name"]:
1038 remove_configlets.append(cvp_configlet)
1039 device_update = True
1040 else:
1041 for configlet in new_configlets:
1042 if configlet not in device["deviceSpecificConfiglets"]:
1043 add_configlets.append(configlet)
1044 device_update = True
1045
1046 if device_update:
1047 update_devices.append(
1048 {
1049 "hostname": device["hostname"],
1050 "configlets": [add_configlets, remove_configlets],
1051 "device": device,
1052 }
1053 )
1054
1055 self.logger.info("Device to modify: {}".format(update_devices))
1056
1057 up_device = update_devices[0]
1058 cl_toAdd = up_device["configlets"][0]
1059 cl_toDel = up_device["configlets"][1]
1060
1061 # Update Configlets
1062 try:
1063 if delete and len(cl_toDel) > 0:
1064 r = self.client.api.remove_configlets_from_device(
1065 "OSM", up_device["device"], cl_toDel, create_task=True
1066 )
1067 dev_action = r
1068 self.logger.debug(
1069 "remove_configlets_from_device {} {}".format(dev_action, cl_toDel)
1070 )
1071 elif len(cl_toAdd) > 0:
1072 r = self.client.api.apply_configlets_to_device(
1073 "OSM", up_device["device"], cl_toAdd, create_task=True
1074 )
1075 dev_action = r
1076 self.logger.debug(
1077 "apply_configlets_to_device {} {}".format(dev_action, cl_toAdd)
1078 )
1079 except Exception as error:
1080 errorMessage = str(error)
1081 msg = "errorMessage: Device {} Configlets could not be updated: {}".format(
1082 up_device["hostname"], errorMessage
1083 )
1084 raise SdnConnectorError(msg) from error
1085 else:
1086 if "errorMessage" in str(dev_action):
1087 m = "Device {} Configlets update fail: {}".format(
1088 up_device["name"], dev_action["errorMessage"]
1089 )
1090 raise SdnConnectorError(m)
1091 else:
1092 changed = True
1093 if "taskIds" in str(dev_action):
1094 # Fix 1030 SDN-ARISTA Key error note when deploy a NS
1095 if not dev_action["data"]["taskIds"]:
1096 raise SdnConnectorError(
1097 "No taskIds found: Device {} Configlets could not be updated".format(
1098 up_device["hostname"]
1099 )
1100 )
1101
1102 for taskId in dev_action["data"]["taskIds"]:
1103 updated.append(
1104 {up_device["hostname"]: "Configlets-{}".format(taskId)}
1105 )
1106 newTasks.append(taskId)
1107 else:
1108 updated.append(
1109 {up_device["hostname"]: "Configlets-No_Specific_Tasks"}
1110 )
1111
1112 data = {"updated": updated, "tasks": newTasks}
1113
1114 return [changed, data]
1115
1116 def __configlet_modify(self, configletsToApply, delete=False):
1117 """Adds/update or delete the provided configLets
1118 :param configletsToApply: list of configLets to apply
1119 :param delete: flag to indicate if the configLets have to be deleted
1120 from Cloud Vision Portal
1121 :return: data: dict of module actions and taskIDs
1122 """
1123 self.logger.info("Enter in __configlet_modify delete:{}".format(delete))
1124
1125 # Compare configlets against cvp_facts-configlets
1126 changed = False
1127 checked = []
1128 deleted = []
1129 updated = []
1130 new = []
1131
1132 for cl in configletsToApply:
1133 found_in_cvp = False
1134 to_delete = False
1135 to_update = False
1136 to_create = False
1137 to_check = False
1138
1139 try:
1140 cvp_cl = self.client.api.get_configlet_by_name(cl["name"])
1141 cl["key"] = cvp_cl["key"]
1142 cl["note"] = cvp_cl["note"]
1143 found_in_cvp = True
1144 except CvpApiError as error:
1145 if "Entity does not exist" in error.msg:
1146 pass
1147 else:
1148 raise error
1149
1150 if delete:
1151 if found_in_cvp:
1152 to_delete = True
1153 configlet = {"name": cvp_cl["name"], "data": cvp_cl}
1154 else:
1155 if found_in_cvp:
1156 cl_compare = self.__compare(cl["config"], cvp_cl["config"])
1157
1158 # compare function returns a floating point number
1159 if cl_compare[0] != 100.0:
1160 to_update = True
1161 configlet = {
1162 "name": cl["name"],
1163 "data": cvp_cl,
1164 "config": cl["config"],
1165 }
1166 else:
1167 to_check = True
1168 configlet = {
1169 "name": cl["name"],
1170 "key": cvp_cl["key"],
1171 "data": cvp_cl,
1172 "config": cl["config"],
1173 }
1174 else:
1175 to_create = True
1176 configlet = {"name": cl["name"], "config": cl["config"]}
1177 try:
1178 if to_delete:
1179 operation = "delete"
1180 resp = self.client.api.delete_configlet(
1181 configlet["data"]["name"], configlet["data"]["key"]
1182 )
1183 elif to_update:
1184 operation = "update"
1185 resp = self.client.api.update_configlet(
1186 configlet["config"],
1187 configlet["data"]["key"],
1188 configlet["data"]["name"],
1189 wait_task_ids=True,
1190 )
1191 elif to_create:
1192 operation = "create"
1193 resp = self.client.api.add_configlet(
1194 configlet["name"], configlet["config"]
1195 )
1196 else:
1197 operation = "checked"
1198 resp = "checked"
1199 except Exception as error:
1200 errorMessage = str(error).split(":")[-1]
1201 message = "Configlet {} cannot be {}: {}".format(
1202 cl["name"], operation, errorMessage
1203 )
1204
1205 if to_delete:
1206 deleted.append({configlet["name"]: message})
1207 elif to_update:
1208 updated.append({configlet["name"]: message})
1209 elif to_create:
1210 new.append({configlet["name"]: message})
1211 elif to_check:
1212 checked.append({configlet["name"]: message})
1213 else:
1214 if "error" in str(resp).lower():
1215 message = "Configlet {} cannot be deleted: {}".format(
1216 cl["name"], resp["errorMessage"]
1217 )
1218
1219 if to_delete:
1220 deleted.append({configlet["name"]: message})
1221 elif to_update:
1222 updated.append({configlet["name"]: message})
1223 elif to_create:
1224 new.append({configlet["name"]: message})
1225 elif to_check:
1226 checked.append({configlet["name"]: message})
1227 else:
1228 if to_delete:
1229 changed = True
1230 deleted.append({configlet["name"]: "success"})
1231 elif to_update:
1232 changed = True
1233 updated.append({configlet["name"]: "success"})
1234 elif to_create:
1235 changed = True
1236 # This key is used in API call deviceApplyConfigLet FGA
1237 cl["key"] = resp
1238 new.append({configlet["name"]: "success"})
1239 elif to_check:
1240 changed = False
1241 checked.append({configlet["name"]: "success"})
1242
1243 data = {"new": new, "updated": updated, "deleted": deleted, "checked": checked}
1244
1245 return [changed, data]
1246
1247 def __get_configletsDevices(self, configlets):
1248 for s in self.switches:
1249 configlet = configlets[s]
1250
1251 # Add applied Devices
1252 if len(configlet) > 0:
1253 configlet["devices"] = []
1254 applied_devices = self.client.api.get_applied_devices(configlet["name"])
1255
1256 for device in applied_devices["data"]:
1257 configlet["devices"].append(device["hostName"])
1258
1259 def __get_serviceData(self, service_uuid, service_type, vlan_id, conn_info=None):
1260 cls_perSw = {}
1261
1262 for s in self.switches:
1263 cls_perSw[s] = []
1264
1265 if not conn_info:
1266 srv_cls = self.__get_serviceConfigLets(service_uuid, service_type, vlan_id)
1267 self.__get_configletsDevices(srv_cls)
1268
1269 for s in self.switches:
1270 cl = srv_cls[s]
1271 if len(cl) > 0:
1272 for dev in cl["devices"]:
1273 cls_perSw[dev].append(cl)
1274 else:
1275 cls_perSw = conn_info["configLetPerSwitch"]
1276
1277 return cls_perSw
1278
1279 def delete_connectivity_service(self, service_uuid, conn_info=None):
1280 """
1281 Disconnect multi-site endpoints previously connected
1282
1283 :param service_uuid: The one returned by create_connectivity_service
1284 :param conn_info: The one returned by last call to 'create_connectivity_service' or 'edit_connectivity_service'
1285 if they do not return None
1286 :return: None
1287 :raises: SdnConnectorException: In case of error. The parameter http_code must be filled
1288 """
1289 try:
1290 self.logger.debug(
1291 "invoked delete_connectivity_service {}".format(service_uuid)
1292 )
1293
1294 if not service_uuid:
1295 raise SdnConnectorError(
1296 message="No connection service UUID", http_code=500
1297 )
1298
1299 self.__get_Connection()
1300
1301 if conn_info is None:
1302 raise SdnConnectorError(
1303 message="No connection information for service UUID {}".format(
1304 service_uuid
1305 ),
1306 http_code=500,
1307 )
1308
1309 c_info = None
1310 cls_perSw = self.__get_serviceData(
1311 service_uuid, conn_info["service_type"], conn_info["vlan_id"], c_info
1312 )
1313 allLeafConfigured = {}
1314 allLeafModified = {}
1315
1316 for s in self.switches:
1317 allLeafConfigured[s] = True
1318 allLeafModified[s] = True
1319
1320 found_in_cvp = False
1321
1322 for s in self.switches:
1323 if cls_perSw[s]:
1324 found_in_cvp = True
1325
1326 if found_in_cvp:
1327 self.__rollbackConnection(cls_perSw, allLeafConfigured, allLeafModified)
1328 else:
1329 # if the service is not defined in Cloud Vision, return a 404 - NotFound error
1330 raise SdnConnectorError(
1331 message="Service {} was not found in Arista Cloud Vision {}".format(
1332 service_uuid, self.__wim_url
1333 ),
1334 http_code=404,
1335 )
1336
1337 self.__removeMetadata(service_uuid)
1338 except CvpLoginError as e:
1339 self.logger.info(str(e))
1340 self.client = None
1341 raise SdnConnectorError(
1342 message=SdnError.UNAUTHORIZED + " " + str(e), http_code=401
1343 ) from e
1344 except SdnConnectorError as sde:
1345 raise sde
1346 except Exception as ex:
1347 self.client = None
1348 self.logger.error(ex)
1349
1350 if self.raiseException:
1351 raise ex
1352
1353 raise SdnConnectorError(
1354 message=SdnError.INTERNAL_ERROR + " " + str(ex), http_code=500
1355 ) from ex
1356
1357 def __addMetadata(self, service_uuid, service_type, vlan_id):
1358 """Adds the connectivity service from 'OSM_metadata' configLet"""
1359 found_in_cvp = False
1360
1361 try:
1362 cvp_cl = self.client.api.get_configlet_by_name(self.__OSM_METADATA)
1363 found_in_cvp = True
1364 except CvpApiError as error:
1365 if "Entity does not exist" in error.msg:
1366 pass
1367 else:
1368 raise error
1369
1370 try:
1371 new_serv = "{} {} {} {}\n".format(
1372 self.__METADATA_PREFIX, service_type, vlan_id, service_uuid
1373 )
1374
1375 if found_in_cvp:
1376 cl_config = cvp_cl["config"] + new_serv
1377 else:
1378 cl_config = new_serv
1379
1380 cl_meta = [{"name": self.__OSM_METADATA, "config": cl_config}]
1381 self.__configlet_modify(cl_meta)
1382 except Exception as e:
1383 self.logger.error(
1384 "Error in setting metadata in CloudVision from OSM for service {}: {}".format(
1385 service_uuid, str(e)
1386 )
1387 )
1388 pass
1389
1390 def __removeMetadata(self, service_uuid):
1391 """Removes the connectivity service from 'OSM_metadata' configLet"""
1392 found_in_cvp = False
1393
1394 try:
1395 cvp_cl = self.client.api.get_configlet_by_name(self.__OSM_METADATA)
1396 found_in_cvp = True
1397 except CvpApiError as error:
1398 if "Entity does not exist" in error.msg:
1399 pass
1400 else:
1401 raise error
1402
1403 try:
1404 if found_in_cvp:
1405 if service_uuid in cvp_cl["config"]:
1406 cl_config = ""
1407
1408 for line in cvp_cl["config"].split("\n"):
1409 if service_uuid in line:
1410 continue
1411 else:
1412 cl_config = cl_config + line
1413
1414 cl_meta = [{"name": self.__OSM_METADATA, "config": cl_config}]
1415 self.__configlet_modify(cl_meta)
1416 except Exception as e:
1417 self.logger.error(
1418 "Error in removing metadata in CloudVision from OSM for service {}: {}".format(
1419 service_uuid, str(e)
1420 )
1421 )
1422 pass
1423
1424 def edit_connectivity_service(
1425 self, service_uuid, conn_info=None, connection_points=None, **kwargs
1426 ):
1427 """Change an existing connectivity service.
1428
1429 This method's arguments and return value follow the same convention as
1430 :meth:`~.create_connectivity_service`.
1431
1432 :param service_uuid: UUID of the connectivity service.
1433 :param conn_info: (dict or None): Information previously returned
1434 by last call to create_connectivity_service
1435 or edit_connectivity_service
1436 :param connection_points: (list): If provided, the old list of
1437 connection points will be replaced.
1438 :param kwargs: Same meaning that create_connectivity_service
1439 :return: dict or None: Information to be updated and stored at
1440 the database.
1441 When ``None`` is returned, no information should be changed.
1442 When an empty dict is returned, the database record will
1443 be deleted.
1444 **MUST** be JSON/YAML-serializable (plain data structures).
1445 Raises:
1446 SdnConnectorError: In case of error.
1447 """
1448 try:
1449 self.logger.debug(
1450 "invoked edit_connectivity_service for service {}. ports: {}".format(
1451 service_uuid, connection_points
1452 )
1453 )
1454
1455 if not service_uuid:
1456 raise SdnConnectorError(
1457 message="Unable to perform operation, missing or empty uuid",
1458 http_code=500,
1459 )
1460
1461 if not conn_info:
1462 raise SdnConnectorError(
1463 message="Unable to perform operation, missing or empty connection information",
1464 http_code=500,
1465 )
1466
1467 if connection_points is None:
1468 return None
1469
1470 self.__get_Connection()
1471
1472 cls_currentPerSw = conn_info["configLetPerSwitch"]
1473 service_type = conn_info["service_type"]
1474
1475 self.__check_service(
1476 service_type,
1477 connection_points,
1478 check_vlan=False,
1479 check_num_cp=False,
1480 kwargs=kwargs,
1481 )
1482
1483 s_uid, s_connInf = self.__processConnection(
1484 service_uuid, service_type, connection_points, kwargs
1485 )
1486 self.logger.info("Service with uuid {} configuration updated".format(s_uid))
1487
1488 return s_connInf
1489 except CvpLoginError as e:
1490 self.logger.info(str(e))
1491 self.client = None
1492 raise SdnConnectorError(
1493 message=SdnError.UNAUTHORIZED + " " + str(e), http_code=401
1494 ) from e
1495 except SdnConnectorError as sde:
1496 raise sde
1497 except Exception as ex:
1498 try:
1499 # Add previous
1500 # TODO check if there are pending task, and cancel them before restoring
1501 self.__updateConnection(cls_currentPerSw)
1502 except Exception as e:
1503 self.logger.error(
1504 "Unable to restore configuration in service {} after an error in the configuration"
1505 " updated: {}".format(service_uuid, str(e))
1506 )
1507
1508 if self.raiseException:
1509 raise ex
1510
1511 raise SdnConnectorError(message=str(ex), http_code=500) from ex
1512
1513 def clear_all_connectivity_services(self):
1514 """Removes all connectivity services from Arista CloudVision with two steps:
1515 - retrieves all the services from Arista CloudVision
1516 - removes each service
1517 """
1518 try:
1519 self.logger.debug("invoked AristaImpl clear_all_connectivity_services")
1520 self.__get_Connection()
1521 s_list = self.__get_srvUUIDs()
1522
1523 for serv in s_list:
1524 conn_info = {}
1525 conn_info["service_type"] = serv["type"]
1526 conn_info["vlan_id"] = serv["vlan"]
1527 self.delete_connectivity_service(serv["uuid"], conn_info)
1528 except CvpLoginError as e:
1529 self.logger.info(str(e))
1530 self.client = None
1531
1532 raise SdnConnectorError(
1533 message=SdnError.UNAUTHORIZED + " " + str(e), http_code=401
1534 ) from e
1535 except SdnConnectorError as sde:
1536 raise sde
1537 except Exception as ex:
1538 self.client = None
1539 self.logger.error(ex)
1540
1541 if self.raiseException:
1542 raise ex
1543
1544 raise SdnConnectorError(
1545 message=SdnError.INTERNAL_ERROR + " " + str(ex), http_code=500
1546 ) from ex
1547
1548 def get_all_active_connectivity_services(self):
1549 """Return the uuid of all the active connectivity services with two steps:
1550 - retrives all the services from Arista CloudVision
1551 - retrives the status of each server
1552 """
1553 try:
1554 self.logger.debug(
1555 "invoked AristaImpl {}".format("get_all_active_connectivity_services")
1556 )
1557 self.__get_Connection()
1558 s_list = self.__get_srvUUIDs()
1559 result = []
1560
1561 for serv in s_list:
1562 conn_info = {}
1563 conn_info["service_type"] = serv["type"]
1564 conn_info["vlan_id"] = serv["vlan"]
1565 status = self.get_connectivity_service_status(serv["uuid"], conn_info)
1566
1567 if status["sdn_status"] == "ACTIVE":
1568 result.append(serv["uuid"])
1569
1570 return result
1571 except CvpLoginError as e:
1572 self.logger.info(str(e))
1573 self.client = None
1574 raise SdnConnectorError(
1575 message=SdnError.UNAUTHORIZED + " " + str(e), http_code=401
1576 ) from e
1577 except SdnConnectorError as sde:
1578 raise sde
1579 except Exception as ex:
1580 self.client = None
1581 self.logger.error(ex)
1582
1583 if self.raiseException:
1584 raise ex
1585
1586 raise SdnConnectorError(
1587 message=SdnError.INTERNAL_ERROR, http_code=500
1588 ) from ex
1589
1590 def __get_serviceConfigLets(self, service_uuid, service_type, vlan_id):
1591 """Return the configLet's associated with a connectivity service,
1592 There should be one, as maximum, per device (switch) for a given
1593 connectivity service
1594 """
1595 srv_cls = {}
1596
1597 for s in self.switches:
1598 srv_cls[s] = []
1599 found_in_cvp = False
1600 name = (
1601 self.__OSM_PREFIX
1602 + s
1603 + self.__SEPARATOR
1604 + service_type
1605 + str(vlan_id)
1606 + self.__SEPARATOR
1607 + service_uuid
1608 )
1609
1610 try:
1611 cvp_cl = self.client.api.get_configlet_by_name(name)
1612 found_in_cvp = True
1613 except CvpApiError as error:
1614 if "Entity does not exist" in error.msg:
1615 pass
1616 else:
1617 raise error
1618
1619 if found_in_cvp:
1620 srv_cls[s] = cvp_cl
1621
1622 return srv_cls
1623
1624 def __get_srvVLANs(self):
1625 """Returns a list with all the VLAN id's used in the connectivity services managed
1626 in tha Arista CloudVision by checking the 'OSM_metadata' configLet where this
1627 information is stored
1628 """
1629 found_in_cvp = False
1630
1631 try:
1632 cvp_cl = self.client.api.get_configlet_by_name(self.__OSM_METADATA)
1633 found_in_cvp = True
1634 except CvpApiError as error:
1635 if "Entity does not exist" in error.msg:
1636 pass
1637 else:
1638 raise error
1639
1640 s_vlan_list = []
1641 if found_in_cvp:
1642 lines = cvp_cl["config"].split("\n")
1643
1644 for line in lines:
1645 if self.__METADATA_PREFIX in line:
1646 s_vlan = line.split(" ")[3]
1647 else:
1648 continue
1649
1650 if s_vlan is not None and len(s_vlan) > 0 and s_vlan not in s_vlan_list:
1651 s_vlan_list.append(s_vlan)
1652
1653 return s_vlan_list
1654
1655 def __get_srvUUIDs(self):
1656 """Retrieves all the connectivity services, managed in tha Arista CloudVision
1657 by checking the 'OSM_metadata' configLet where this information is stored
1658 """
1659 found_in_cvp = False
1660
1661 try:
1662 cvp_cl = self.client.api.get_configlet_by_name(self.__OSM_METADATA)
1663 found_in_cvp = True
1664 except CvpApiError as error:
1665 if "Entity does not exist" in error.msg:
1666 pass
1667 else:
1668 raise error
1669
1670 serv_list = []
1671 if found_in_cvp:
1672 lines = cvp_cl["config"].split("\n")
1673
1674 for line in lines:
1675 if self.__METADATA_PREFIX in line:
1676 line = line.split(" ")
1677 serv = {"uuid": line[4], "type": line[2], "vlan": line[3]}
1678 else:
1679 continue
1680
1681 if serv is not None and len(serv) > 0 and serv not in serv_list:
1682 serv_list.append(serv)
1683
1684 return serv_list
1685
1686 def __get_Connection(self):
1687 """Open a connection with Arista CloudVision,
1688 invoking the version retrival as test
1689 """
1690 try:
1691 if self.client is None:
1692 self.client = self.__connect()
1693
1694 self.client.api.get_cvp_info()
1695 except (CvpSessionLogOutError, RequestException) as e:
1696 self.logger.debug("Connection error '{}'. Reconnecting".format(e))
1697 self.client = self.__connect()
1698 self.client.api.get_cvp_info()
1699
1700 def __connect(self):
1701 """Connects to CVP device using user provided credentials from initialization.
1702 :return: CvpClient object with connection instantiated.
1703 """
1704 client = CvpClient()
1705 protocol, _, rest_url = self.__wim_url.rpartition("://")
1706 host, _, port = rest_url.partition(":")
1707
1708 if port and port.endswith("/"):
1709 port = int(port[:-1])
1710 elif port:
1711 port = int(port)
1712 else:
1713 port = 443
1714
1715 client.connect(
1716 [host],
1717 self.__user,
1718 self.__passwd,
1719 protocol=protocol or "https",
1720 port=port,
1721 connect_timeout=2,
1722 )
1723 client.api = CvpApi(client, request_timeout=self.__API_REQUEST_TOUT)
1724 self.taskC = AristaCVPTask(client.api)
1725
1726 return client
1727
1728 def __compare(self, fromText, toText, lines=10):
1729 """Compare text string in 'fromText' with 'toText' and produce
1730 diffRatio - a score as a float in the range [0, 1] 2.0*M / T
1731 T is the total number of elements in both sequences,
1732 M is the number of matches.
1733 Score - 1.0 if the sequences are identical, and
1734 0.0 if they have nothing in common.
1735 unified diff list
1736 Code Meaning
1737 '- ' line unique to sequence 1
1738 '+ ' line unique to sequence 2
1739 ' ' line common to both sequences
1740 '? ' line not present in either input sequence
1741 """
1742 fromlines = fromText.splitlines(1)
1743 tolines = toText.splitlines(1)
1744 diff = list(difflib.unified_diff(fromlines, tolines, n=lines))
1745 textComp = difflib.SequenceMatcher(None, fromText, toText)
1746 diffRatio = round(textComp.quick_ratio() * 100, 2)
1747
1748 return [diffRatio, diff]
1749
1750 def __load_inventory(self):
1751 """Get Inventory Data for All Devices (aka switches) from the Arista CloudVision"""
1752 if not self.cvp_inventory:
1753 self.cvp_inventory = self.client.api.get_inventory()
1754
1755 self.allDeviceFacts = []
1756
1757 for device in self.cvp_inventory:
1758 self.allDeviceFacts.append(device)
1759
1760 def __get_tags(self, name, value):
1761 if not self.cvp_tags:
1762 self.cvp_tags = []
1763 url = "/api/v1/rest/analytics/tags/labels/devices/{}/value/{}/elements".format(
1764 name, value
1765 )
1766 self.logger.debug("get_tags: URL {}".format(url))
1767 data = self.client.get(url, timeout=self.__API_REQUEST_TOUT)
1768
1769 for dev in data["notifications"]:
1770 for elem in dev["updates"]:
1771 self.cvp_tags.append(elem)
1772
1773 self.logger.debug(
1774 "Available devices with tag_name {} - value {}: {}".format(
1775 name, value, self.cvp_tags
1776 )
1777 )
1778
1779 def __get_interface_ip(self, device_id, interface):
1780 url = "/api/v1/rest/{}/Sysdb/ip/config/ipIntfConfig/{}/".format(
1781 device_id, interface
1782 )
1783 self.logger.debug("get_interface_ip: URL {}".format(url))
1784 data = None
1785
1786 try:
1787 data = self.client.get(url, timeout=self.__API_REQUEST_TOUT)
1788
1789 if data["notifications"]:
1790 for notification in data["notifications"]:
1791 for update in notification["updates"]:
1792 if update == "addrWithMask":
1793 return notification["updates"][update]["value"]
1794 except Exception as e:
1795 raise SdnConnectorError(
1796 "Invalid response from url {}: data {} - {}".format(url, data, str(e))
1797 )
1798
1799 raise SdnConnectorError(
1800 "Unable to get ip for interface {} in device {}, data {}".format(
1801 interface, device_id, data
1802 )
1803 )
1804
1805 def __get_device_ASN(self, device_id):
1806 url = "/api/v1/rest/{}/Sysdb/routing/bgp/config/".format(device_id)
1807 self.logger.debug("get_device_ASN: URL {}".format(url))
1808 data = None
1809
1810 try:
1811 data = self.client.get(url, timeout=self.__API_REQUEST_TOUT)
1812 if data["notifications"]:
1813 for notification in data["notifications"]:
1814 for update in notification["updates"]:
1815 if update == "asNumber":
1816 return notification["updates"][update]["value"]["value"][
1817 "int"
1818 ]
1819 except Exception as e:
1820 raise SdnConnectorError(
1821 "Invalid response from url {}: data {} - {}".format(url, data, str(e))
1822 )
1823
1824 raise SdnConnectorError(
1825 "Unable to get AS in device {}, data {}".format(device_id, data)
1826 )
1827
1828 def __get_peer_MLAG(self, device_id):
1829 peer = None
1830 url = "/api/v1/rest/{}/Sysdb/mlag/status/".format(device_id)
1831 self.logger.debug("get_MLAG_status: URL {}".format(url))
1832
1833 try:
1834 data = self.client.get(url, timeout=self.__API_REQUEST_TOUT)
1835
1836 if data["notifications"]:
1837 found = False
1838
1839 for notification in data["notifications"]:
1840 for update in notification["updates"]:
1841 if update == "systemId":
1842 mlagSystemId = notification["updates"][update]["value"]
1843 found = True
1844 break
1845
1846 if found:
1847 break
1848
1849 # search the MLAG System Id
1850 if found:
1851 for s in self.switches:
1852 if self.switches[s]["serialNumber"] == device_id:
1853 continue
1854
1855 url = "/api/v1/rest/{}/Sysdb/mlag/status/".format(
1856 self.switches[s]["serialNumber"]
1857 )
1858 self.logger.debug(
1859 "Searching for MLAG system id {} in switch {}".format(
1860 mlagSystemId, s
1861 )
1862 )
1863 data = self.client.get(url, timeout=self.__API_REQUEST_TOUT)
1864 found = False
1865
1866 for notification in data["notifications"]:
1867 for update in notification["updates"]:
1868 if update == "systemId":
1869 if (
1870 mlagSystemId
1871 == notification["updates"][update]["value"]
1872 ):
1873 peer = s
1874 found = True
1875 break
1876
1877 if found:
1878 break
1879
1880 if found:
1881 break
1882
1883 if peer is None:
1884 self.logger.error(
1885 "No Peer device found for device {} with MLAG address {}".format(
1886 device_id, mlagSystemId
1887 )
1888 )
1889 else:
1890 self.logger.debug(
1891 "Peer MLAG for device {} - value {}".format(device_id, peer)
1892 )
1893
1894 return peer
1895 except Exception:
1896 raise SdnConnectorError(
1897 "Invalid response from url {}: data {}".format(url, data)
1898 )
1899
1900 def is_valid_destination(self, url):
1901 """Check that the provided WIM URL is correct"""
1902 if re.match(self.__regex, url):
1903 return True
1904 elif self.is_valid_ipv4_address(url):
1905 return True
1906 else:
1907 return self.is_valid_ipv6_address(url)
1908
1909 def is_valid_ipv4_address(self, address):
1910 """Checks that the given IP is IPv4 valid"""
1911 try:
1912 socket.inet_pton(socket.AF_INET, address)
1913 except AttributeError: # no inet_pton here, sorry
1914 try:
1915 socket.inet_aton(address)
1916 except socket.error:
1917 return False
1918
1919 return address.count(".") == 3
1920 except socket.error: # not a valid address
1921 return False
1922
1923 return True
1924
1925 def is_valid_ipv6_address(self, address):
1926 """Checks that the given IP is IPv6 valid"""
1927 try:
1928 socket.inet_pton(socket.AF_INET6, address)
1929 except socket.error: # not a valid address
1930 return False
1931
1932 return True
1933
1934 def delete_keys_from_dict(self, dict_del, lst_keys):
1935 if dict_del is None:
1936 return dict_del
1937
1938 dict_copy = {k: v for k, v in dict_del.items() if k not in lst_keys}
1939
1940 for k, v in dict_copy.items():
1941 if isinstance(v, dict):
1942 dict_copy[k] = self.delete_keys_from_dict(v, lst_keys)
1943
1944 return dict_copy