1 # -*- coding: utf-8 -*-
3 # Copyright 2019 Atos - CoE Telco NFV Team
6 # Contributors: Oscar Luis Peral, Atos
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
12 # http://www.apache.org/licenses/LICENSE-2.0
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
20 # For those usages not covered by the Apache License, Version 2.0 please
21 # contact with: <oscarluis.peral@atos.net>
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.
27 # This work has been performed in the context of Arista Telefonica OSM PoC.
30 from osm_ro_plugin
.sdnconn
import SdnConnectorBase
, SdnConnectorError
34 # Required by compare function
37 # Library that uses Levenshtein Distance to calculate the differences
39 # from fuzzywuzzy import fuzz
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
50 from osm_rosdn_arista_cloudvision
.aristaConfigLet
import AristaSDNConfigLet
51 from osm_rosdn_arista_cloudvision
.aristaTask
import AristaCVPTask
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.'
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"
74 class AristaSdnConnector(SdnConnectorBase
):
75 """Arista class for the SDN connectors
78 wim (dict): WIM record, as stored in the database
79 wim_account (dict): WIM account record, as stored in the database
81 The arguments of the constructor are converted to object attributes.
82 An extra property, ``service_endpoint_mapping`` is created from ``config``.
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.
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.
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"
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"
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"
137 _VLAN_MLAG
= "VLAN-MLAG"
138 _VXLAN_MLAG
= "VXLAN-MLAG"
140 def __init__(self
, wim
, wim_account
, config
=None, logger
=None):
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.
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
171 self
.raiseException
= True
172 self
.logger
= logger
or logging
.getLogger(self
.__WIM
_LOGGER
)
173 super().__init
__(wim
, wim_account
, config
, self
.logger
)
175 self
.__wim
_account
= wim_account
176 self
.__config
= config
178 if self
.is_valid_destination(self
.__wim
.get("wim_url")):
179 self
.__wim
_url
= self
.__wim
.get("wim_url")
181 raise SdnConnectorError(message
="Invalid wim_url value", http_code
=500)
183 self
.__user
= wim_account
.get("user")
184 self
.__passwd
= wim_account
.get("password")
186 self
.cvp_inventory
= None
189 "Arista SDN plugin {}, cvprac version {}, user:{} and config:{}".format(
193 self
.delete_keys_from_dict(config
, ("passwd",)),
196 self
.allDeviceFacts
= []
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
206 except ConnectionError
as ce
:
207 raise SdnConnectorError(
208 message
=SdnError
.UNREACHABLE
+ " " + str(ce
), http_code
=404
210 except SdnConnectorError
as 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
220 "Using topology {} in Arista Leaf switches: {}".format(
221 self
.topology
, self
.delete_keys_from_dict(self
.switches
, ("passwd",))
224 self
.clC
= AristaSDNConfigLet(self
.topology
)
226 def __load_topology(self
):
227 self
.topology
= self
._VXLAN
_MLAG
229 if self
.__config
and self
.__config
.get("topology"):
230 topology
= self
.__config
.get("topology")
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
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'
253 All the search methods will be used
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
,
266 "serialNumber": None,
267 "mlagPeerDevice": None,
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
,
281 "serialNumber": None,
282 "mlagPeerDevice": None,
286 self
.switches
[cs
].update(cs_content
)
288 # Load the rest of the data
289 if self
.client
is None:
290 self
.client
= self
.__connect
()
292 self
.__load
_inventory
()
294 if not self
.switches
:
295 self
.__get
_tags
(self
.__SWITCH
_TAG
_NAME
, self
.__SWITCH
_TAG
_VALUE
)
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"]):
302 "passwd": self
.__passwd
,
303 "ip": device
["ipAddress"],
307 "serialNumber": None,
308 "mlagPeerDevice": None,
310 self
.switches
[device
["hostname"]] = switch_data
312 if len(self
.switches
) == 0:
313 self
.logger
.error("Unable to load Leaf switches from CVP")
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"]
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
332 self
.switches
[s
]["lo0"] = inf
.split("/")[0]
334 if not self
.switches
[s
].get("AS"):
335 self
.switches
[s
]["AS"] = self
.__get
_device
_ASN
(
336 self
.switches
[s
]["serialNumber"]
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"]
354 """Reviews the connection points elements looking for semantic errors in the incoming data"""
355 if service_type
not in self
.__supported
_service
_types
:
357 "The service '{}' is not supported. Only '{}' are accepted".format(
358 service_type
, self
.__supported
_service
_types
363 if len(connection_points
) < 2:
364 raise Exception(SdnError
.CONNECTION_POINTS_SIZE
)
367 len(connection_points
) != self
.__ELINE
_num
_connection
_points
368 and service_type
== self
.__service
_types
_ELINE
370 raise Exception(SdnError
.CONNECTION_POINTS_SIZE
)
375 for cp
in connection_points
:
376 enc_type
= cp
.get(self
.__ENCAPSULATION
_TYPE
_PARAM
)
378 if enc_type
and enc_type
not in self
.__supported
_encapsulation
_types
:
379 raise Exception(SdnError
.ENCAPSULATION_TYPE
)
381 encap_info
= cp
.get(self
.__ENCAPSULATION
_INFO
_PARAM
)
382 cp_vlan_id
= str(encap_info
.get(self
.__VLAN
_PARAM
))
387 elif vlan_id
!= cp_vlan_id
:
388 raise Exception(SdnError
.VLAN_INCONSISTENT
)
391 raise Exception(SdnError
.VLAN_NOT_PROVIDED
)
393 if vlan_id
in self
.__get
_srvVLANs
():
395 "VLAN {} already assigned to a connectivity service".format(vlan_id
)
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)
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)
408 def check_credentials(self
):
409 """Retrieves the CloudVision version information, as the easiest way
410 for testing the access to CloudVision API
413 if self
.client
is None:
414 self
.client
= self
.__connect
()
416 result
= self
.client
.api
.get_cvp_info()
417 self
.logger
.debug(result
)
418 except CvpLoginError
as e
:
419 self
.logger
.info(str(e
))
422 raise SdnConnectorError(
423 message
=SdnError
.UNAUTHORIZED
+ " " + str(e
), http_code
=401
425 except Exception as ex
:
427 self
.logger
.error(str(ex
))
429 raise SdnConnectorError(
430 message
=SdnError
.INTERNAL_ERROR
+ " " + str(ex
), http_code
=500
433 def get_connectivity_service_status(self
, service_uuid
, conn_info
=None):
434 """Monitor the status of the connectivity service established
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
442 dict: JSON/YAML-serializable dict that contains a mandatory key
443 ``sdn_status`` associated with one of the following values::
445 {'sdn_status': 'ACTIVE'}
446 # The service is up and running.
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).
453 {'sdn_status': 'DOWN'}
454 # Connection was previously established,
455 # but an error/failure was detected.
457 {'sdn_status': 'ERROR'}
458 # An error occurred when trying to create the service/
459 # establish the connectivity.
461 {'sdn_status': 'BUILD'}
462 # Still trying to create the service, the caller
463 # needs to wait and check again.
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.
471 "invoked get_connectivity_service_status '{}'".format(service_uuid
)
475 raise SdnConnectorError(
476 message
="No connection service UUID", http_code
=500
479 self
.__get
_Connection
()
481 if conn_info
is None:
482 raise SdnConnectorError(
483 message
="No connection information for service UUID {}".format(
489 if "configLetPerSwitch" in conn_info
.keys():
494 cls_perSw
= self
.__get
_serviceData
(
495 service_uuid
, conn_info
["service_type"], conn_info
["vlan_id"], c_info
498 t_isCancelled
= False
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
510 not cls_perSw
[s
][0]["config"]
511 or not cl
.get("note")
512 or self
.__MANAGED
_BY
_OSM
not in cl
["note"]
517 t_id
= note
.split(self
.__SEPARATOR
)[1]
518 result
= self
.client
.api
.get_task_by_id(t_id
)
520 if result
["workOrderUserDefinedStatus"] == "Completed":
522 elif result
["workOrderUserDefinedStatus"] == "Cancelled":
524 elif result
["workOrderUserDefinedStatus"] == "Failed":
529 failed_switches
.append(s
)
532 error_msg
= "Some works were cancelled in switches: {}".format(
537 error_msg
= "Some works failed in switches: {}".format(
543 "Some works are still under execution in switches: {}".format(
550 sdn_status
= "ACTIVE"
555 "sdn_status": sdn_status
,
556 "error_msg": error_msg
,
557 "sdn_info": sdn_info
,
559 except CvpLoginError
as e
:
560 self
.logger
.info(str(e
))
563 raise SdnConnectorError(
564 message
=SdnError
.UNAUTHORIZED
+ " " + str(e
), http_code
=401
566 except Exception as ex
:
568 self
.logger
.error(str(ex
), exc_info
=True)
570 raise SdnConnectorError(
571 message
=str(ex
) + " " + str(ex
), http_code
=500
574 def create_connectivity_service(self
, service_type
, connection_points
, **kwargs
):
575 """Establish SDN/WAN connectivity between the endpoints
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)
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
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
625 "invoked create_connectivity_service '{}' ports: {}".format(
626 service_type
, connection_points
629 self
.__get
_Connection
()
630 self
.__check
_service
(
631 service_type
, connection_points
, check_vlan
=True, kwargs
=kwargs
633 service_uuid
= str(uuid
.uuid4())
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
641 self
.__addMetadata
(s_uid
, service_type
, s_connInf
["vlan_id"])
645 return (s_uid
, s_connInf
)
646 except CvpLoginError
as e
:
647 self
.logger
.info(str(e
))
650 raise SdnConnectorError(
651 message
=SdnError
.UNAUTHORIZED
+ " " + str(e
), http_code
=401
653 except SdnConnectorError
as sde
:
655 except ValueError as err
:
657 self
.logger
.error(str(err
), exc_info
=True)
659 raise SdnConnectorError(message
=str(err
), http_code
=500) from err
660 except Exception as ex
:
662 self
.logger
.error(str(ex
), exc_info
=True)
664 if self
.raiseException
:
667 raise SdnConnectorError(message
=str(ex
), http_code
=500) from ex
669 def __processConnection(
670 self
, service_uuid
, service_type
, connection_points
, kwargs
673 Invoked from creation and edit methods
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
688 for s
in self
.switches
:
692 vlan_processed
= False
695 processed_connection_points
= []
697 for cp
in connection_points
:
699 encap_info
= cp
.get(self
.__ENCAPSULATION
_INFO
_PARAM
)
701 if not vlan_processed
:
702 vlan_id
= str(encap_info
.get(self
.__VLAN
_PARAM
))
707 vni_id
= encap_info
.get(self
.__VNI
_PARAM
)
710 vni_id
= str(10000 + int(vlan_id
))
712 if service_type
== self
.__service
_types
_ELAN
:
713 cl_vlan
= self
.clC
.getElan_vlan(service_uuid
, vlan_id
, vni_id
)
715 cl_vlan
= self
.clC
.getEline_vlan(service_uuid
, vlan_id
, vni_id
)
717 vlan_processed
= True
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
}]
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
]
731 processed_connection_points
+= switches
733 for switch
in switches
:
735 raise SdnConnectorError(
736 message
="Connection point switch port empty for switch_dpid {}".format(
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
749 cl_encap
= self
.clC
.getEline_sriov(
750 service_uuid
, interface
, vlan_id
, i
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
759 cl_encap
= self
.clC
.getEline_passthrough(
760 service_uuid
, interface
, vlan_id
, i
763 if cls_cp
.get(switch
["name"]):
764 cls_cp
[switch
["name"]] = str(cls_cp
[switch
["name"]]) + cl_encap
766 cls_cp
[switch
["name"]] = cl_encap
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
774 for s
in self
.switches
:
775 # for cl in cp_configLets:
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(
794 self
.switches
[s
]["lo0"],
795 self
.switches
[s
]["AS"],
798 cl_bgp
[s
] = self
.clC
.getEline_bgp(
802 self
.switches
[s
]["lo0"],
803 self
.switches
[s
]["AS"],
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
:
815 if self
.topology
== self
._VXLAN
_MLAG
:
816 cl_config
= str(cl_vlan
) + str(cl_bgp
[s
])
818 cl_config
= str(cl_vlan
)
820 cl_config
= str(cl_vlan
) + str(cl_bgp
[s
]) + str(cls_cp
[s
])
822 cls_perSw
[s
] = [{"name": cl_name
, "config": cl_config
}]
824 allLeafConfigured
, allLeafModified
= self
.__updateConnection
(cls_perSw
)
827 "uuid": service_uuid
,
829 "service_type": service_type
,
831 "connection_points": connection_points
,
832 "configLetPerSwitch": cls_perSw
,
833 "allLeafConfigured": allLeafConfigured
,
834 "allLeafModified": allLeafModified
,
837 return service_uuid
, conn_info
838 except Exception as ex
:
840 "Exception processing connection {}: {}".format(service_uuid
, str(ex
))
844 def __updateConnection(self
, cls_perSw
):
845 """Invoked in the creation and modification
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
859 allLeafConfigured
= {}
862 for s
in self
.switches
:
863 allLeafConfigured
[s
] = False
864 allLeafModified
[s
] = False
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
875 # get config let name and key
879 cvp_cl
= self
.client
.api
.get_configlet_by_name(cl
[0]["name"])
881 cl_toDelete
.append(cvp_cl
)
883 toDelete_in_cvp
= True
884 except CvpApiError
as error
:
885 if "Entity does not exist" in error
.msg
:
889 # remove configLet from device
891 res
= self
.__configlet
_modify
(cls_perSw
[s
])
892 allLeafConfigured
[s
] = res
[0]
894 if not allLeafConfigured
[s
]:
899 res
= self
.__device
_modify
(
900 device_to_update
=s
, new_configlets
=cl
, delete
=toDelete_in_cvp
903 if "errorMessage" in str(res
):
904 raise Exception(str(res
))
906 self
.logger
.info("Device {} modify result {}".format(s
, res
))
908 for t_id
in res
[1]["tasks"]:
909 if not toDelete_in_cvp
:
910 note_msg
= "{}{}{}{}##".format(
911 self
.__MANAGED
_BY
_OSM
,
916 self
.client
.api
.add_note_to_configlet(
917 cls_perSw
[s
][0]["key"], note_msg
919 cls_perSw
[s
][0]["note"] = note_msg
921 tasks
= {t_id
: {"workOrderId": t_id
}}
922 self
.__exec
_task
(tasks
, self
.__EXC
_TASK
_EXEC
_WAIT
)
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
929 if len(cl_toDelete
) > 0:
930 self
.__configlet
_modify
(cl_toDelete
, delete
=True)
932 return allLeafConfigured
, allLeafModified
933 except Exception as ex
:
935 self
.__rollbackConnection
(cls_perSw
, allLeafConfigured
, allLeafModified
)
936 except Exception as e
:
938 "Exception rolling back in updating connection: {}".format(e
),
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
]:
949 res
= self
.__device
_modify
(
951 new_configlets
=cls_perSw
[s
],
955 if "errorMessage" in str(res
):
956 raise Exception(str(res
))
960 for t_id
in res
[1]["tasks"]:
961 tasks
[t_id
] = {"workOrderId": t_id
}
963 self
.__exec
_task
(tasks
)
964 self
.logger
.info("Device {} modify result {}".format(s
, res
))
965 except Exception as e
:
967 "Error removing configlets from device {}: {}".format(s
, e
)
971 for s
in self
.switches
:
972 if allLeafConfigured
[s
]:
973 self
.__configlet
_modify
(cls_perSw
[s
], delete
=True)
975 def __exec_task(self
, tasks
, tout
=10):
976 if self
.taskC
is None:
979 data
= self
.taskC
.update_all_tasks(tasks
).values()
980 self
.taskC
.task_action(data
, tout
, "executed")
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
986 self
.logger
.info("Enter in __device_modify delete: {}".format(delete
))
989 # Task Ids that have been identified during device actions
993 len(new_configlets
) == 0
994 or device_to_update
is None
995 or len(device_to_update
) == 0
997 data
= {"updated": updated
, "tasks": newTasks
}
999 return [changed
, data
]
1001 self
.__load
_inventory
()
1003 allDeviceFacts
= self
.allDeviceFacts
1004 # Work through Devices list adding device specific information
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
:
1012 dev_cvp_configlets
= self
.client
.api
.get_configlets_by_device_id(
1013 try_device
["systemMacAddress"]
1015 # self.logger.debug(dev_cvp_configlets)
1016 try_device
["deviceSpecificConfiglets"] = []
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"]}
1024 # self.logger.debug(device)
1028 # Check assigned configlets
1029 device_update
= False
1031 remove_configlets
= []
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
1041 for configlet
in new_configlets
:
1042 if configlet
not in device
["deviceSpecificConfiglets"]:
1043 add_configlets
.append(configlet
)
1044 device_update
= True
1047 update_devices
.append(
1049 "hostname": device
["hostname"],
1050 "configlets": [add_configlets
, remove_configlets
],
1055 self
.logger
.info("Device to modify: {}".format(update_devices
))
1057 up_device
= update_devices
[0]
1058 cl_toAdd
= up_device
["configlets"][0]
1059 cl_toDel
= up_device
["configlets"][1]
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
1069 "remove_configlets_from_device {} {}".format(dev_action
, cl_toDel
)
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
1077 "apply_configlets_to_device {} {}".format(dev_action
, cl_toAdd
)
1079 except Exception as error
:
1080 errorMessage
= str(error
)
1081 msg
= "errorMessage: Device {} Configlets could not be updated: {}".format(
1082 up_device
["hostname"], errorMessage
1084 raise SdnConnectorError(msg
) from error
1086 if "errorMessage" in str(dev_action
):
1087 m
= "Device {} Configlets update fail: {}".format(
1088 up_device
["name"], dev_action
["errorMessage"]
1090 raise SdnConnectorError(m
)
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"]
1102 for taskId
in dev_action
["data"]["taskIds"]:
1104 {up_device
["hostname"]: "Configlets-{}".format(taskId
)}
1106 newTasks
.append(taskId
)
1109 {up_device
["hostname"]: "Configlets-No_Specific_Tasks"}
1112 data
= {"updated": updated
, "tasks": newTasks
}
1114 return [changed
, data
]
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
1123 self
.logger
.info("Enter in __configlet_modify delete:{}".format(delete
))
1125 # Compare configlets against cvp_facts-configlets
1132 for cl
in configletsToApply
:
1133 found_in_cvp
= False
1140 cvp_cl
= self
.client
.api
.get_configlet_by_name(cl
["name"])
1141 cl
["key"] = cvp_cl
["key"]
1142 cl
["note"] = cvp_cl
["note"]
1144 except CvpApiError
as error
:
1145 if "Entity does not exist" in error
.msg
:
1153 configlet
= {"name": cvp_cl
["name"], "data": cvp_cl
}
1156 cl_compare
= self
.__compare
(cl
["config"], cvp_cl
["config"])
1158 # compare function returns a floating point number
1159 if cl_compare
[0] != 100.0:
1164 "config": cl
["config"],
1170 "key": cvp_cl
["key"],
1172 "config": cl
["config"],
1176 configlet
= {"name": cl
["name"], "config": cl
["config"]}
1179 operation
= "delete"
1180 resp
= self
.client
.api
.delete_configlet(
1181 configlet
["data"]["name"], configlet
["data"]["key"]
1184 operation
= "update"
1185 resp
= self
.client
.api
.update_configlet(
1186 configlet
["config"],
1187 configlet
["data"]["key"],
1188 configlet
["data"]["name"],
1192 operation
= "create"
1193 resp
= self
.client
.api
.add_configlet(
1194 configlet
["name"], configlet
["config"]
1197 operation
= "checked"
1199 except Exception as error
:
1200 errorMessage
= str(error
).split(":")[-1]
1201 message
= "Configlet {} cannot be {}: {}".format(
1202 cl
["name"], operation
, errorMessage
1206 deleted
.append({configlet
["name"]: message
})
1208 updated
.append({configlet
["name"]: message
})
1210 new
.append({configlet
["name"]: message
})
1212 checked
.append({configlet
["name"]: message
})
1214 if "error" in str(resp
).lower():
1215 message
= "Configlet {} cannot be deleted: {}".format(
1216 cl
["name"], resp
["errorMessage"]
1220 deleted
.append({configlet
["name"]: message
})
1222 updated
.append({configlet
["name"]: message
})
1224 new
.append({configlet
["name"]: message
})
1226 checked
.append({configlet
["name"]: message
})
1230 deleted
.append({configlet
["name"]: "success"})
1233 updated
.append({configlet
["name"]: "success"})
1236 # This key is used in API call deviceApplyConfigLet FGA
1238 new
.append({configlet
["name"]: "success"})
1241 checked
.append({configlet
["name"]: "success"})
1243 data
= {"new": new
, "updated": updated
, "deleted": deleted
, "checked": checked
}
1245 return [changed
, data
]
1247 def __get_configletsDevices(self
, configlets
):
1248 for s
in self
.switches
:
1249 configlet
= configlets
[s
]
1251 # Add applied Devices
1252 if len(configlet
) > 0:
1253 configlet
["devices"] = []
1254 applied_devices
= self
.client
.api
.get_applied_devices(configlet
["name"])
1256 for device
in applied_devices
["data"]:
1257 configlet
["devices"].append(device
["hostName"])
1259 def __get_serviceData(self
, service_uuid
, service_type
, vlan_id
, conn_info
=None):
1262 for s
in self
.switches
:
1266 srv_cls
= self
.__get
_serviceConfigLets
(service_uuid
, service_type
, vlan_id
)
1267 self
.__get
_configletsDevices
(srv_cls
)
1269 for s
in self
.switches
:
1272 for dev
in cl
["devices"]:
1273 cls_perSw
[dev
].append(cl
)
1275 cls_perSw
= conn_info
["configLetPerSwitch"]
1279 def delete_connectivity_service(self
, service_uuid
, conn_info
=None):
1281 Disconnect multi-site endpoints previously connected
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
1287 :raises: SdnConnectorException: In case of error. The parameter http_code must be filled
1291 "invoked delete_connectivity_service {}".format(service_uuid
)
1294 if not service_uuid
:
1295 raise SdnConnectorError(
1296 message
="No connection service UUID", http_code
=500
1299 self
.__get
_Connection
()
1301 if conn_info
is None:
1302 raise SdnConnectorError(
1303 message
="No connection information for service UUID {}".format(
1310 cls_perSw
= self
.__get
_serviceData
(
1311 service_uuid
, conn_info
["service_type"], conn_info
["vlan_id"], c_info
1313 allLeafConfigured
= {}
1314 allLeafModified
= {}
1316 for s
in self
.switches
:
1317 allLeafConfigured
[s
] = True
1318 allLeafModified
[s
] = True
1320 found_in_cvp
= False
1322 for s
in self
.switches
:
1327 self
.__rollbackConnection
(cls_perSw
, allLeafConfigured
, allLeafModified
)
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
1337 self
.__removeMetadata
(service_uuid
)
1338 except CvpLoginError
as e
:
1339 self
.logger
.info(str(e
))
1341 raise SdnConnectorError(
1342 message
=SdnError
.UNAUTHORIZED
+ " " + str(e
), http_code
=401
1344 except SdnConnectorError
as sde
:
1346 except Exception as ex
:
1348 self
.logger
.error(ex
)
1350 if self
.raiseException
:
1353 raise SdnConnectorError(
1354 message
=SdnError
.INTERNAL_ERROR
+ " " + str(ex
), http_code
=500
1357 def __addMetadata(self
, service_uuid
, service_type
, vlan_id
):
1358 """Adds the connectivity service from 'OSM_metadata' configLet"""
1359 found_in_cvp
= False
1362 cvp_cl
= self
.client
.api
.get_configlet_by_name(self
.__OSM
_METADATA
)
1364 except CvpApiError
as error
:
1365 if "Entity does not exist" in error
.msg
:
1371 new_serv
= "{} {} {} {}\n".format(
1372 self
.__METADATA
_PREFIX
, service_type
, vlan_id
, service_uuid
1376 cl_config
= cvp_cl
["config"] + new_serv
1378 cl_config
= new_serv
1380 cl_meta
= [{"name": self
.__OSM
_METADATA
, "config": cl_config
}]
1381 self
.__configlet
_modify
(cl_meta
)
1382 except Exception as e
:
1384 "Error in setting metadata in CloudVision from OSM for service {}: {}".format(
1385 service_uuid
, str(e
)
1390 def __removeMetadata(self
, service_uuid
):
1391 """Removes the connectivity service from 'OSM_metadata' configLet"""
1392 found_in_cvp
= False
1395 cvp_cl
= self
.client
.api
.get_configlet_by_name(self
.__OSM
_METADATA
)
1397 except CvpApiError
as error
:
1398 if "Entity does not exist" in error
.msg
:
1405 if service_uuid
in cvp_cl
["config"]:
1408 for line
in cvp_cl
["config"].split("\n"):
1409 if service_uuid
in line
:
1412 cl_config
= cl_config
+ line
1414 cl_meta
= [{"name": self
.__OSM
_METADATA
, "config": cl_config
}]
1415 self
.__configlet
_modify
(cl_meta
)
1416 except Exception as e
:
1418 "Error in removing metadata in CloudVision from OSM for service {}: {}".format(
1419 service_uuid
, str(e
)
1424 def edit_connectivity_service(
1425 self
, service_uuid
, conn_info
=None, connection_points
=None, **kwargs
1427 """Change an existing connectivity service.
1429 This method's arguments and return value follow the same convention as
1430 :meth:`~.create_connectivity_service`.
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
1441 When ``None`` is returned, no information should be changed.
1442 When an empty dict is returned, the database record will
1444 **MUST** be JSON/YAML-serializable (plain data structures).
1446 SdnConnectorError: In case of error.
1450 "invoked edit_connectivity_service for service {}. ports: {}".format(
1451 service_uuid
, connection_points
1455 if not service_uuid
:
1456 raise SdnConnectorError(
1457 message
="Unable to perform operation, missing or empty uuid",
1462 raise SdnConnectorError(
1463 message
="Unable to perform operation, missing or empty connection information",
1467 if connection_points
is None:
1470 self
.__get
_Connection
()
1472 cls_currentPerSw
= conn_info
["configLetPerSwitch"]
1473 service_type
= conn_info
["service_type"]
1475 self
.__check
_service
(
1483 s_uid
, s_connInf
= self
.__processConnection
(
1484 service_uuid
, service_type
, connection_points
, kwargs
1486 self
.logger
.info("Service with uuid {} configuration updated".format(s_uid
))
1489 except CvpLoginError
as e
:
1490 self
.logger
.info(str(e
))
1492 raise SdnConnectorError(
1493 message
=SdnError
.UNAUTHORIZED
+ " " + str(e
), http_code
=401
1495 except SdnConnectorError
as sde
:
1497 except Exception as ex
:
1500 # TODO check if there are pending task, and cancel them before restoring
1501 self
.__updateConnection
(cls_currentPerSw
)
1502 except Exception as e
:
1504 "Unable to restore configuration in service {} after an error in the configuration"
1505 " updated: {}".format(service_uuid
, str(e
))
1508 if self
.raiseException
:
1511 raise SdnConnectorError(message
=str(ex
), http_code
=500) from ex
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
1519 self
.logger
.debug("invoked AristaImpl clear_all_connectivity_services")
1520 self
.__get
_Connection
()
1521 s_list
= self
.__get
_srvUUIDs
()
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
))
1532 raise SdnConnectorError(
1533 message
=SdnError
.UNAUTHORIZED
+ " " + str(e
), http_code
=401
1535 except SdnConnectorError
as sde
:
1537 except Exception as ex
:
1539 self
.logger
.error(ex
)
1541 if self
.raiseException
:
1544 raise SdnConnectorError(
1545 message
=SdnError
.INTERNAL_ERROR
+ " " + str(ex
), http_code
=500
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
1555 "invoked AristaImpl {}".format("get_all_active_connectivity_services")
1557 self
.__get
_Connection
()
1558 s_list
= self
.__get
_srvUUIDs
()
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
)
1567 if status
["sdn_status"] == "ACTIVE":
1568 result
.append(serv
["uuid"])
1571 except CvpLoginError
as e
:
1572 self
.logger
.info(str(e
))
1574 raise SdnConnectorError(
1575 message
=SdnError
.UNAUTHORIZED
+ " " + str(e
), http_code
=401
1577 except SdnConnectorError
as sde
:
1579 except Exception as ex
:
1581 self
.logger
.error(ex
)
1583 if self
.raiseException
:
1586 raise SdnConnectorError(
1587 message
=SdnError
.INTERNAL_ERROR
, http_code
=500
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
1597 for s
in self
.switches
:
1599 found_in_cvp
= False
1611 cvp_cl
= self
.client
.api
.get_configlet_by_name(name
)
1613 except CvpApiError
as error
:
1614 if "Entity does not exist" in error
.msg
:
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
1629 found_in_cvp
= False
1632 cvp_cl
= self
.client
.api
.get_configlet_by_name(self
.__OSM
_METADATA
)
1634 except CvpApiError
as error
:
1635 if "Entity does not exist" in error
.msg
:
1642 lines
= cvp_cl
["config"].split("\n")
1645 if self
.__METADATA
_PREFIX
in line
:
1646 s_vlan
= line
.split(" ")[3]
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
)
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
1659 found_in_cvp
= False
1662 cvp_cl
= self
.client
.api
.get_configlet_by_name(self
.__OSM
_METADATA
)
1664 except CvpApiError
as error
:
1665 if "Entity does not exist" in error
.msg
:
1672 lines
= cvp_cl
["config"].split("\n")
1675 if self
.__METADATA
_PREFIX
in line
:
1676 line
= line
.split(" ")
1677 serv
= {"uuid": line
[4], "type": line
[2], "vlan": line
[3]}
1681 if serv
is not None and len(serv
) > 0 and serv
not in serv_list
:
1682 serv_list
.append(serv
)
1686 def __get_Connection(self
):
1687 """Open a connection with Arista CloudVision,
1688 invoking the version retrival as test
1691 if self
.client
is None:
1692 self
.client
= self
.__connect
()
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()
1700 def __connect(self
):
1701 """Connects to CVP device using user provided credentials from initialization.
1702 :return: CvpClient object with connection instantiated.
1704 client
= CvpClient()
1705 protocol
, _
, rest_url
= self
.__wim
_url
.rpartition("://")
1706 host
, _
, port
= rest_url
.partition(":")
1708 if port
and port
.endswith("/"):
1709 port
= int(port
[:-1])
1719 protocol
=protocol
or "https",
1723 client
.api
= CvpApi(client
, request_timeout
=self
.__API
_REQUEST
_TOUT
)
1724 self
.taskC
= AristaCVPTask(client
.api
)
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.
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
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)
1748 return [diffRatio
, diff
]
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()
1755 self
.allDeviceFacts
= []
1757 for device
in self
.cvp_inventory
:
1758 self
.allDeviceFacts
.append(device
)
1760 def __get_tags(self
, name
, value
):
1761 if not self
.cvp_tags
:
1763 url
= "/api/v1/rest/analytics/tags/labels/devices/{}/value/{}/elements".format(
1766 self
.logger
.debug("get_tags: URL {}".format(url
))
1767 data
= self
.client
.get(url
, timeout
=self
.__API
_REQUEST
_TOUT
)
1769 for dev
in data
["notifications"]:
1770 for elem
in dev
["updates"]:
1771 self
.cvp_tags
.append(elem
)
1774 "Available devices with tag_name {} - value {}: {}".format(
1775 name
, value
, self
.cvp_tags
1779 def __get_interface_ip(self
, device_id
, interface
):
1780 url
= "/api/v1/rest/{}/Sysdb/ip/config/ipIntfConfig/{}/".format(
1781 device_id
, interface
1783 self
.logger
.debug("get_interface_ip: URL {}".format(url
))
1787 data
= self
.client
.get(url
, timeout
=self
.__API
_REQUEST
_TOUT
)
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
))
1799 raise SdnConnectorError(
1800 "Unable to get ip for interface {} in device {}, data {}".format(
1801 interface
, device_id
, data
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
))
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"][
1819 except Exception as e
:
1820 raise SdnConnectorError(
1821 "Invalid response from url {}: data {} - {}".format(url
, data
, str(e
))
1824 raise SdnConnectorError(
1825 "Unable to get AS in device {}, data {}".format(device_id
, data
)
1828 def __get_peer_MLAG(self
, device_id
):
1830 url
= "/api/v1/rest/{}/Sysdb/mlag/status/".format(device_id
)
1831 self
.logger
.debug("get_MLAG_status: URL {}".format(url
))
1834 data
= self
.client
.get(url
, timeout
=self
.__API
_REQUEST
_TOUT
)
1836 if data
["notifications"]:
1839 for notification
in data
["notifications"]:
1840 for update
in notification
["updates"]:
1841 if update
== "systemId":
1842 mlagSystemId
= notification
["updates"][update
]["value"]
1849 # search the MLAG System Id
1851 for s
in self
.switches
:
1852 if self
.switches
[s
]["serialNumber"] == device_id
:
1855 url
= "/api/v1/rest/{}/Sysdb/mlag/status/".format(
1856 self
.switches
[s
]["serialNumber"]
1859 "Searching for MLAG system id {} in switch {}".format(
1863 data
= self
.client
.get(url
, timeout
=self
.__API
_REQUEST
_TOUT
)
1866 for notification
in data
["notifications"]:
1867 for update
in notification
["updates"]:
1868 if update
== "systemId":
1871 == notification
["updates"][update
]["value"]
1885 "No Peer device found for device {} with MLAG address {}".format(
1886 device_id
, mlagSystemId
1891 "Peer MLAG for device {} - value {}".format(device_id
, peer
)
1896 raise SdnConnectorError(
1897 "Invalid response from url {}: data {}".format(url
, data
)
1900 def is_valid_destination(self
, url
):
1901 """Check that the provided WIM URL is correct"""
1902 if re
.match(self
.__regex
, url
):
1904 elif self
.is_valid_ipv4_address(url
):
1907 return self
.is_valid_ipv6_address(url
)
1909 def is_valid_ipv4_address(self
, address
):
1910 """Checks that the given IP is IPv4 valid"""
1912 socket
.inet_pton(socket
.AF_INET
, address
)
1913 except AttributeError: # no inet_pton here, sorry
1915 socket
.inet_aton(address
)
1916 except socket
.error
:
1919 return address
.count(".") == 3
1920 except socket
.error
: # not a valid address
1925 def is_valid_ipv6_address(self
, address
):
1926 """Checks that the given IP is IPv6 valid"""
1928 socket
.inet_pton(socket
.AF_INET6
, address
)
1929 except socket
.error
: # not a valid address
1934 def delete_keys_from_dict(self
, dict_del
, lst_keys
):
1935 if dict_del
is None:
1938 dict_copy
= {k
: v
for k
, v
in dict_del
.items() if k
not in lst_keys
}
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
)