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