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.
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
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.'
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"
67 class AristaSdnConnector(SdnConnectorBase
):
68 """Arista class for the SDN connectors
71 wim (dict): WIM record, as stored in the database
72 wim_account (dict): WIM account record, as stored in the database
74 The arguments of the constructor are converted to object attributes.
75 An extra property, ``service_endpoint_mapping`` is created from ``config``.
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.
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.
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"
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"
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"
130 _VLAN_MLAG
= "VLAN-MLAG"
131 _VXLAN_MLAG
= "VXLAN-MLAG"
133 def __init__(self
, wim
, wim_account
, config
=None, logger
=None):
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.
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
164 self
.raiseException
= True
165 self
.logger
= logger
or logging
.getLogger(self
.__WIM
_LOGGER
)
166 super().__init
__(wim
, wim_account
, config
, self
.logger
)
168 self
.__wim
_account
= wim_account
169 self
.__config
= config
171 if self
.is_valid_destination(self
.__wim
.get("wim_url")):
172 self
.__wim
_url
= self
.__wim
.get("wim_url")
174 raise SdnConnectorError(message
="Invalid wim_url value", http_code
=500)
176 self
.__user
= wim_account
.get("user")
177 self
.__passwd
= wim_account
.get("password")
179 self
.cvp_inventory
= None
182 "Arista SDN plugin {}, cvprac version {}, user:{} and config:{}".format(
186 self
.delete_keys_from_dict(config
, ("passwd",)),
189 self
.allDeviceFacts
= []
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
199 except ConnectionError
as ce
:
200 raise SdnConnectorError(
201 message
=SdnError
.UNREACHABLE
+ " " + str(ce
), http_code
=404
203 except SdnConnectorError
as 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
213 "Using topology {} in Arista Leaf switches: {}".format(
214 self
.topology
, self
.delete_keys_from_dict(self
.switches
, ("passwd",))
217 self
.clC
= AristaSDNConfigLet(self
.topology
)
219 def __load_topology(self
):
220 self
.topology
= self
._VXLAN
_MLAG
222 if self
.__config
and self
.__config
.get("topology"):
223 topology
= self
.__config
.get("topology")
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
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'
246 All the search methods will be used
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
,
259 "serialNumber": None,
260 "mlagPeerDevice": None,
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
,
274 "serialNumber": None,
275 "mlagPeerDevice": None,
279 self
.switches
[cs
].update(cs_content
)
281 # Load the rest of the data
282 if self
.client
is None:
283 self
.client
= self
.__connect
()
285 self
.__load
_inventory
()
287 if not self
.switches
:
288 self
.__get
_tags
(self
.__SWITCH
_TAG
_NAME
, self
.__SWITCH
_TAG
_VALUE
)
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"]):
295 "passwd": self
.__passwd
,
296 "ip": device
["ipAddress"],
300 "serialNumber": None,
301 "mlagPeerDevice": None,
303 self
.switches
[device
["hostname"]] = switch_data
305 if len(self
.switches
) == 0:
306 self
.logger
.error("Unable to load Leaf switches from CVP")
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"]
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
325 self
.switches
[s
]["lo0"] = inf
.split("/")[0]
327 if not self
.switches
[s
].get("AS"):
328 self
.switches
[s
]["AS"] = self
.__get
_device
_ASN
(
329 self
.switches
[s
]["serialNumber"]
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"]
347 """Reviews the connection points elements looking for semantic errors in the incoming data"""
348 if service_type
not in self
.__supported
_service
_types
:
350 "The service '{}' is not supported. Only '{}' are accepted".format(
351 service_type
, self
.__supported
_service
_types
356 if len(connection_points
) < 2:
357 raise Exception(SdnError
.CONNECTION_POINTS_SIZE
)
360 len(connection_points
) != self
.__ELINE
_num
_connection
_points
361 and service_type
== self
.__service
_types
_ELINE
363 raise Exception(SdnError
.CONNECTION_POINTS_SIZE
)
368 for cp
in connection_points
:
369 enc_type
= cp
.get(self
.__ENCAPSULATION
_TYPE
_PARAM
)
371 if enc_type
and enc_type
not in self
.__supported
_encapsulation
_types
:
372 raise Exception(SdnError
.ENCAPSULATION_TYPE
)
374 encap_info
= cp
.get(self
.__ENCAPSULATION
_INFO
_PARAM
)
375 cp_vlan_id
= str(encap_info
.get(self
.__VLAN
_PARAM
))
380 elif vlan_id
!= cp_vlan_id
:
381 raise Exception(SdnError
.VLAN_INCONSISTENT
)
384 raise Exception(SdnError
.VLAN_NOT_PROVIDED
)
386 if vlan_id
in self
.__get
_srvVLANs
():
388 "VLAN {} already assigned to a connectivity service".format(vlan_id
)
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)
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)
401 def check_credentials(self
):
402 """Retrieves the CloudVision version information, as the easiest way
403 for testing the access to CloudVision API
406 if self
.client
is None:
407 self
.client
= self
.__connect
()
409 result
= self
.client
.api
.get_cvp_info()
410 self
.logger
.debug(result
)
411 except CvpLoginError
as e
:
412 self
.logger
.info(str(e
))
415 raise SdnConnectorError(
416 message
=SdnError
.UNAUTHORIZED
+ " " + str(e
), http_code
=401
418 except Exception as ex
:
420 self
.logger
.error(str(ex
))
422 raise SdnConnectorError(
423 message
=SdnError
.INTERNAL_ERROR
+ " " + str(ex
), http_code
=500
426 def get_connectivity_service_status(self
, service_uuid
, conn_info
=None):
427 """Monitor the status of the connectivity service established
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
435 dict: JSON/YAML-serializable dict that contains a mandatory key
436 ``sdn_status`` associated with one of the following values::
438 {'sdn_status': 'ACTIVE'}
439 # The service is up and running.
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).
446 {'sdn_status': 'DOWN'}
447 # Connection was previously established,
448 # but an error/failure was detected.
450 {'sdn_status': 'ERROR'}
451 # An error occurred when trying to create the service/
452 # establish the connectivity.
454 {'sdn_status': 'BUILD'}
455 # Still trying to create the service, the caller
456 # needs to wait and check again.
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.
464 "invoked get_connectivity_service_status '{}'".format(service_uuid
)
468 raise SdnConnectorError(
469 message
="No connection service UUID", http_code
=500
472 self
.__get
_Connection
()
474 if conn_info
is None:
475 raise SdnConnectorError(
476 message
="No connection information for service UUID {}".format(
482 if "configLetPerSwitch" in conn_info
.keys():
487 cls_perSw
= self
.__get
_serviceData
(
488 service_uuid
, conn_info
["service_type"], conn_info
["vlan_id"], c_info
491 t_isCancelled
= False
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
503 not cls_perSw
[s
][0]["config"]
504 or not cl
.get("note")
505 or self
.__MANAGED
_BY
_OSM
not in cl
["note"]
510 t_id
= note
.split(self
.__SEPARATOR
)[1]
511 result
= self
.client
.api
.get_task_by_id(t_id
)
513 if result
["workOrderUserDefinedStatus"] == "Completed":
515 elif result
["workOrderUserDefinedStatus"] == "Cancelled":
517 elif result
["workOrderUserDefinedStatus"] == "Failed":
522 failed_switches
.append(s
)
525 error_msg
= "Some works were cancelled in switches: {}".format(
530 error_msg
= "Some works failed in switches: {}".format(
536 "Some works are still under execution in switches: {}".format(
543 sdn_status
= "ACTIVE"
548 "sdn_status": sdn_status
,
549 "error_msg": error_msg
,
550 "sdn_info": sdn_info
,
552 except CvpLoginError
as e
:
553 self
.logger
.info(str(e
))
556 raise SdnConnectorError(
557 message
=SdnError
.UNAUTHORIZED
+ " " + str(e
), http_code
=401
559 except Exception as ex
:
561 self
.logger
.error(str(ex
), exc_info
=True)
563 raise SdnConnectorError(
564 message
=str(ex
) + " " + str(ex
), http_code
=500
567 def create_connectivity_service(self
, service_type
, connection_points
, **kwargs
):
568 """Establish SDN/WAN connectivity between the endpoints
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)
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
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
618 "invoked create_connectivity_service '{}' ports: {}".format(
619 service_type
, connection_points
622 self
.__get
_Connection
()
623 self
.__check
_service
(
624 service_type
, connection_points
, check_vlan
=True, kwargs
=kwargs
626 service_uuid
= str(uuid
.uuid4())
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
634 self
.__addMetadata
(s_uid
, service_type
, s_connInf
["vlan_id"])
635 except Exception as e
:
636 self
.logger
.exception(f
"{e} occured.")
638 return (s_uid
, s_connInf
)
639 except CvpLoginError
as e
:
640 self
.logger
.info(str(e
))
643 raise SdnConnectorError(
644 message
=SdnError
.UNAUTHORIZED
+ " " + str(e
), http_code
=401
646 except SdnConnectorError
as sde
:
648 except ValueError as err
:
650 self
.logger
.error(str(err
), exc_info
=True)
652 raise SdnConnectorError(message
=str(err
), http_code
=500) from err
653 except Exception as ex
:
655 self
.logger
.error(str(ex
), exc_info
=True)
657 if self
.raiseException
:
660 raise SdnConnectorError(message
=str(ex
), http_code
=500) from ex
662 def __processConnection(
663 self
, service_uuid
, service_type
, connection_points
, kwargs
666 Invoked from creation and edit methods
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
681 for s
in self
.switches
:
685 vlan_processed
= False
688 processed_connection_points
= []
690 for cp
in connection_points
:
692 encap_info
= cp
.get(self
.__ENCAPSULATION
_INFO
_PARAM
)
694 if not vlan_processed
:
695 vlan_id
= str(encap_info
.get(self
.__VLAN
_PARAM
))
700 vni_id
= encap_info
.get(self
.__VNI
_PARAM
)
703 vni_id
= str(10000 + int(vlan_id
))
705 if service_type
== self
.__service
_types
_ELAN
:
706 cl_vlan
= self
.clC
.getElan_vlan(service_uuid
, vlan_id
, vni_id
)
708 cl_vlan
= self
.clC
.getEline_vlan(service_uuid
, vlan_id
, vni_id
)
710 vlan_processed
= True
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
}]
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
]
724 processed_connection_points
+= switches
726 for switch
in switches
:
728 raise SdnConnectorError(
729 message
="Connection point switch port empty for switch_dpid {}".format(
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
742 cl_encap
= self
.clC
.getEline_sriov(
743 service_uuid
, interface
, vlan_id
, i
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
752 cl_encap
= self
.clC
.getEline_passthrough(
753 service_uuid
, interface
, vlan_id
, i
756 if cls_cp
.get(switch
["name"]):
757 cls_cp
[switch
["name"]] = str(cls_cp
[switch
["name"]]) + cl_encap
759 cls_cp
[switch
["name"]] = cl_encap
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
767 for s
in self
.switches
:
768 # for cl in cp_configLets:
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(
787 self
.switches
[s
]["lo0"],
788 self
.switches
[s
]["AS"],
791 cl_bgp
[s
] = self
.clC
.getEline_bgp(
795 self
.switches
[s
]["lo0"],
796 self
.switches
[s
]["AS"],
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
:
808 if self
.topology
== self
._VXLAN
_MLAG
:
809 cl_config
= str(cl_vlan
) + str(cl_bgp
[s
])
811 cl_config
= str(cl_vlan
)
813 cl_config
= str(cl_vlan
) + str(cl_bgp
[s
]) + str(cls_cp
[s
])
815 cls_perSw
[s
] = [{"name": cl_name
, "config": cl_config
}]
817 allLeafConfigured
, allLeafModified
= self
.__updateConnection
(cls_perSw
)
820 "uuid": service_uuid
,
822 "service_type": service_type
,
824 "connection_points": connection_points
,
825 "configLetPerSwitch": cls_perSw
,
826 "allLeafConfigured": allLeafConfigured
,
827 "allLeafModified": allLeafModified
,
830 return service_uuid
, conn_info
831 except Exception as ex
:
833 "Exception processing connection {}: {}".format(service_uuid
, str(ex
))
837 def __updateConnection(self
, cls_perSw
):
838 """Invoked in the creation and modification
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
852 allLeafConfigured
= {}
855 for s
in self
.switches
:
856 allLeafConfigured
[s
] = False
857 allLeafModified
[s
] = False
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
868 # get config let name and key
872 cvp_cl
= self
.client
.api
.get_configlet_by_name(cl
[0]["name"])
874 cl_toDelete
.append(cvp_cl
)
876 toDelete_in_cvp
= True
877 except CvpApiError
as error
:
878 if "Entity does not exist" in error
.msg
:
882 # remove configLet from device
884 res
= self
.__configlet
_modify
(cls_perSw
[s
])
885 allLeafConfigured
[s
] = res
[0]
887 if not allLeafConfigured
[s
]:
892 res
= self
.__device
_modify
(
893 device_to_update
=s
, new_configlets
=cl
, delete
=toDelete_in_cvp
896 if "errorMessage" in str(res
):
897 raise Exception(str(res
))
899 self
.logger
.info("Device {} modify result {}".format(s
, res
))
901 for t_id
in res
[1]["tasks"]:
902 if not toDelete_in_cvp
:
903 note_msg
= "{}{}{}{}##".format(
904 self
.__MANAGED
_BY
_OSM
,
909 self
.client
.api
.add_note_to_configlet(
910 cls_perSw
[s
][0]["key"], note_msg
912 cls_perSw
[s
][0]["note"] = note_msg
914 tasks
= {t_id
: {"workOrderId": t_id
}}
915 self
.__exec
_task
(tasks
, self
.__EXC
_TASK
_EXEC
_WAIT
)
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
922 if len(cl_toDelete
) > 0:
923 self
.__configlet
_modify
(cl_toDelete
, delete
=True)
925 return allLeafConfigured
, allLeafModified
926 except Exception as ex
:
928 self
.__rollbackConnection
(cls_perSw
, allLeafConfigured
, allLeafModified
)
929 except Exception as e
:
931 "Exception rolling back in updating connection: {}".format(e
),
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
]:
942 res
= self
.__device
_modify
(
944 new_configlets
=cls_perSw
[s
],
948 if "errorMessage" in str(res
):
949 raise Exception(str(res
))
953 for t_id
in res
[1]["tasks"]:
954 tasks
[t_id
] = {"workOrderId": t_id
}
956 self
.__exec
_task
(tasks
)
957 self
.logger
.info("Device {} modify result {}".format(s
, res
))
958 except Exception as e
:
960 "Error removing configlets from device {}: {}".format(s
, e
)
963 for s
in self
.switches
:
964 if allLeafConfigured
[s
]:
965 self
.__configlet
_modify
(cls_perSw
[s
], delete
=True)
967 def __exec_task(self
, tasks
, tout
=10):
968 if self
.taskC
is None:
971 data
= self
.taskC
.update_all_tasks(tasks
).values()
972 self
.taskC
.task_action(data
, tout
, "executed")
974 def __device_modify(self
, device_to_update
, new_configlets
, delete
):
975 """Updates the devices (switches) adding or removing the configLet,
976 the tasks Id's associated to the change are returned
978 self
.logger
.info("Enter in __device_modify delete: {}".format(delete
))
981 # Task Ids that have been identified during device actions
985 len(new_configlets
) == 0
986 or device_to_update
is None
987 or len(device_to_update
) == 0
989 data
= {"updated": updated
, "tasks": newTasks
}
991 return [changed
, data
]
993 self
.__load
_inventory
()
995 allDeviceFacts
= self
.allDeviceFacts
996 # Work through Devices list adding device specific information
998 for try_device
in allDeviceFacts
:
999 # Add Device Specific Configlets
1000 # self.logger.debug(device)
1001 if try_device
["hostname"] not in device_to_update
:
1004 dev_cvp_configlets
= self
.client
.api
.get_configlets_by_device_id(
1005 try_device
["systemMacAddress"]
1007 # self.logger.debug(dev_cvp_configlets)
1008 try_device
["deviceSpecificConfiglets"] = []
1010 for cvp_configlet
in dev_cvp_configlets
:
1011 if int(cvp_configlet
["containerCount"]) == 0:
1012 try_device
["deviceSpecificConfiglets"].append(
1013 {"name": cvp_configlet
["name"], "key": cvp_configlet
["key"]}
1016 # self.logger.debug(device)
1020 # Check assigned configlets
1021 device_update
= False
1023 remove_configlets
= []
1027 for cvp_configlet
in device
["deviceSpecificConfiglets"]:
1028 for cl
in new_configlets
:
1029 if cvp_configlet
["name"] == cl
["name"]:
1030 remove_configlets
.append(cvp_configlet
)
1031 device_update
= True
1033 for configlet
in new_configlets
:
1034 if configlet
not in device
["deviceSpecificConfiglets"]:
1035 add_configlets
.append(configlet
)
1036 device_update
= True
1039 update_devices
.append(
1041 "hostname": device
["hostname"],
1042 "configlets": [add_configlets
, remove_configlets
],
1047 self
.logger
.info("Device to modify: {}".format(update_devices
))
1049 up_device
= update_devices
[0]
1050 cl_toAdd
= up_device
["configlets"][0]
1051 cl_toDel
= up_device
["configlets"][1]
1055 if delete
and len(cl_toDel
) > 0:
1056 r
= self
.client
.api
.remove_configlets_from_device(
1057 "OSM", up_device
["device"], cl_toDel
, create_task
=True
1061 "remove_configlets_from_device {} {}".format(dev_action
, cl_toDel
)
1063 elif len(cl_toAdd
) > 0:
1064 r
= self
.client
.api
.apply_configlets_to_device(
1065 "OSM", up_device
["device"], cl_toAdd
, create_task
=True
1069 "apply_configlets_to_device {} {}".format(dev_action
, cl_toAdd
)
1071 except Exception as error
:
1072 errorMessage
= str(error
)
1073 msg
= "errorMessage: Device {} Configlets could not be updated: {}".format(
1074 up_device
["hostname"], errorMessage
1076 raise SdnConnectorError(msg
) from error
1078 if "errorMessage" in str(dev_action
):
1079 m
= "Device {} Configlets update fail: {}".format(
1080 up_device
["name"], dev_action
["errorMessage"]
1082 raise SdnConnectorError(m
)
1085 if "taskIds" in str(dev_action
):
1086 # Fix 1030 SDN-ARISTA Key error note when deploy a NS
1087 if not dev_action
["data"]["taskIds"]:
1088 raise SdnConnectorError(
1089 "No taskIds found: Device {} Configlets could not be updated".format(
1090 up_device
["hostname"]
1094 for taskId
in dev_action
["data"]["taskIds"]:
1096 {up_device
["hostname"]: "Configlets-{}".format(taskId
)}
1098 newTasks
.append(taskId
)
1101 {up_device
["hostname"]: "Configlets-No_Specific_Tasks"}
1104 data
= {"updated": updated
, "tasks": newTasks
}
1106 return [changed
, data
]
1108 def __configlet_modify(self
, configletsToApply
, delete
=False):
1109 """Adds/update or delete the provided configLets
1110 :param configletsToApply: list of configLets to apply
1111 :param delete: flag to indicate if the configLets have to be deleted
1112 from Cloud Vision Portal
1113 :return: data: dict of module actions and taskIDs
1115 self
.logger
.info("Enter in __configlet_modify delete:{}".format(delete
))
1117 # Compare configlets against cvp_facts-configlets
1124 for cl
in configletsToApply
:
1125 found_in_cvp
= False
1132 cvp_cl
= self
.client
.api
.get_configlet_by_name(cl
["name"])
1133 cl
["key"] = cvp_cl
["key"]
1134 cl
["note"] = cvp_cl
["note"]
1136 except CvpApiError
as error
:
1137 if "Entity does not exist" in error
.msg
:
1138 self
.logger
.exception(f
"{error} occured.")
1145 configlet
= {"name": cvp_cl
["name"], "data": cvp_cl
}
1148 cl_compare
= self
.__compare
(cl
["config"], cvp_cl
["config"])
1150 # compare function returns a floating point number
1151 if cl_compare
[0] != 100.0:
1156 "config": cl
["config"],
1162 "key": cvp_cl
["key"],
1164 "config": cl
["config"],
1168 configlet
= {"name": cl
["name"], "config": cl
["config"]}
1171 operation
= "delete"
1172 resp
= self
.client
.api
.delete_configlet(
1173 configlet
["data"]["name"], configlet
["data"]["key"]
1176 operation
= "update"
1177 resp
= self
.client
.api
.update_configlet(
1178 configlet
["config"],
1179 configlet
["data"]["key"],
1180 configlet
["data"]["name"],
1184 operation
= "create"
1185 resp
= self
.client
.api
.add_configlet(
1186 configlet
["name"], configlet
["config"]
1189 operation
= "checked"
1191 except Exception as error
:
1192 errorMessage
= str(error
).split(":")[-1]
1193 message
= "Configlet {} cannot be {}: {}".format(
1194 cl
["name"], operation
, errorMessage
1198 deleted
.append({configlet
["name"]: message
})
1200 updated
.append({configlet
["name"]: message
})
1202 new
.append({configlet
["name"]: message
})
1204 checked
.append({configlet
["name"]: message
})
1206 if "error" in str(resp
).lower():
1207 message
= "Configlet {} cannot be deleted: {}".format(
1208 cl
["name"], resp
["errorMessage"]
1212 deleted
.append({configlet
["name"]: message
})
1214 updated
.append({configlet
["name"]: message
})
1216 new
.append({configlet
["name"]: message
})
1218 checked
.append({configlet
["name"]: message
})
1222 deleted
.append({configlet
["name"]: "success"})
1225 updated
.append({configlet
["name"]: "success"})
1228 # This key is used in API call deviceApplyConfigLet FGA
1230 new
.append({configlet
["name"]: "success"})
1233 checked
.append({configlet
["name"]: "success"})
1235 data
= {"new": new
, "updated": updated
, "deleted": deleted
, "checked": checked
}
1237 return [changed
, data
]
1239 def __get_configletsDevices(self
, configlets
):
1240 for s
in self
.switches
:
1241 configlet
= configlets
[s
]
1243 # Add applied Devices
1244 if len(configlet
) > 0:
1245 configlet
["devices"] = []
1246 applied_devices
= self
.client
.api
.get_applied_devices(configlet
["name"])
1248 for device
in applied_devices
["data"]:
1249 configlet
["devices"].append(device
["hostName"])
1251 def __get_serviceData(self
, service_uuid
, service_type
, vlan_id
, conn_info
=None):
1254 for s
in self
.switches
:
1258 srv_cls
= self
.__get
_serviceConfigLets
(service_uuid
, service_type
, vlan_id
)
1259 self
.__get
_configletsDevices
(srv_cls
)
1261 for s
in self
.switches
:
1264 for dev
in cl
["devices"]:
1265 cls_perSw
[dev
].append(cl
)
1267 cls_perSw
= conn_info
["configLetPerSwitch"]
1271 def delete_connectivity_service(self
, service_uuid
, conn_info
=None):
1273 Disconnect multi-site endpoints previously connected
1275 :param service_uuid: The one returned by create_connectivity_service
1276 :param conn_info: The one returned by last call to 'create_connectivity_service' or 'edit_connectivity_service'
1277 if they do not return None
1279 :raises: SdnConnectorException: In case of error. The parameter http_code must be filled
1283 "invoked delete_connectivity_service {}".format(service_uuid
)
1286 if not service_uuid
:
1287 raise SdnConnectorError(
1288 message
="No connection service UUID", http_code
=500
1291 self
.__get
_Connection
()
1293 if conn_info
is None:
1294 raise SdnConnectorError(
1295 message
="No connection information for service UUID {}".format(
1302 cls_perSw
= self
.__get
_serviceData
(
1303 service_uuid
, conn_info
["service_type"], conn_info
["vlan_id"], c_info
1305 allLeafConfigured
= {}
1306 allLeafModified
= {}
1308 for s
in self
.switches
:
1309 allLeafConfigured
[s
] = True
1310 allLeafModified
[s
] = True
1312 found_in_cvp
= False
1314 for s
in self
.switches
:
1319 self
.__rollbackConnection
(cls_perSw
, allLeafConfigured
, allLeafModified
)
1321 # if the service is not defined in Cloud Vision, return a 404 - NotFound error
1322 raise SdnConnectorError(
1323 message
="Service {} was not found in Arista Cloud Vision {}".format(
1324 service_uuid
, self
.__wim
_url
1329 self
.__removeMetadata
(service_uuid
)
1330 except CvpLoginError
as e
:
1331 self
.logger
.info(str(e
))
1333 raise SdnConnectorError(
1334 message
=SdnError
.UNAUTHORIZED
+ " " + str(e
), http_code
=401
1336 except SdnConnectorError
as sde
:
1338 except Exception as ex
:
1340 self
.logger
.error(ex
)
1342 if self
.raiseException
:
1345 raise SdnConnectorError(
1346 message
=SdnError
.INTERNAL_ERROR
+ " " + str(ex
), http_code
=500
1349 def __addMetadata(self
, service_uuid
, service_type
, vlan_id
):
1350 """Adds the connectivity service from 'OSM_metadata' configLet"""
1351 found_in_cvp
= False
1354 cvp_cl
= self
.client
.api
.get_configlet_by_name(self
.__OSM
_METADATA
)
1356 except CvpApiError
as error
:
1357 if "Entity does not exist" in error
.msg
:
1358 self
.logger
.exception(f
"{error} occured.")
1363 new_serv
= "{} {} {} {}\n".format(
1364 self
.__METADATA
_PREFIX
, service_type
, vlan_id
, service_uuid
1368 cl_config
= cvp_cl
["config"] + new_serv
1370 cl_config
= new_serv
1372 cl_meta
= [{"name": self
.__OSM
_METADATA
, "config": cl_config
}]
1373 self
.__configlet
_modify
(cl_meta
)
1374 except Exception as e
:
1376 "Error in setting metadata in CloudVision from OSM for service {}: {}".format(
1377 service_uuid
, str(e
)
1381 def __removeMetadata(self
, service_uuid
):
1382 """Removes the connectivity service from 'OSM_metadata' configLet"""
1383 found_in_cvp
= False
1386 cvp_cl
= self
.client
.api
.get_configlet_by_name(self
.__OSM
_METADATA
)
1388 except CvpApiError
as error
:
1389 if "Entity does not exist" in error
.msg
:
1390 self
.logger
.exception(f
"{error} occured.")
1396 if service_uuid
in cvp_cl
["config"]:
1399 for line
in cvp_cl
["config"].split("\n"):
1400 if service_uuid
in line
:
1403 cl_config
= cl_config
+ line
1405 cl_meta
= [{"name": self
.__OSM
_METADATA
, "config": cl_config
}]
1406 self
.__configlet
_modify
(cl_meta
)
1407 except Exception as e
:
1409 "Error in removing metadata in CloudVision from OSM for service {}: {}".format(
1410 service_uuid
, str(e
)
1414 def edit_connectivity_service(
1415 self
, service_uuid
, conn_info
=None, connection_points
=None, **kwargs
1417 """Change an existing connectivity service.
1419 This method's arguments and return value follow the same convention as
1420 :meth:`~.create_connectivity_service`.
1422 :param service_uuid: UUID of the connectivity service.
1423 :param conn_info: (dict or None): Information previously returned
1424 by last call to create_connectivity_service
1425 or edit_connectivity_service
1426 :param connection_points: (list): If provided, the old list of
1427 connection points will be replaced.
1428 :param kwargs: Same meaning that create_connectivity_service
1429 :return: dict or None: Information to be updated and stored at
1431 When ``None`` is returned, no information should be changed.
1432 When an empty dict is returned, the database record will
1434 **MUST** be JSON/YAML-serializable (plain data structures).
1436 SdnConnectorError: In case of error.
1440 "invoked edit_connectivity_service for service {}. ports: {}".format(
1441 service_uuid
, connection_points
1445 if not service_uuid
:
1446 raise SdnConnectorError(
1447 message
="Unable to perform operation, missing or empty uuid",
1452 raise SdnConnectorError(
1453 message
="Unable to perform operation, missing or empty connection information",
1457 if connection_points
is None:
1460 self
.__get
_Connection
()
1462 cls_currentPerSw
= conn_info
["configLetPerSwitch"]
1463 service_type
= conn_info
["service_type"]
1465 self
.__check
_service
(
1473 s_uid
, s_connInf
= self
.__processConnection
(
1474 service_uuid
, service_type
, connection_points
, kwargs
1476 self
.logger
.info("Service with uuid {} configuration updated".format(s_uid
))
1479 except CvpLoginError
as e
:
1480 self
.logger
.info(str(e
))
1482 raise SdnConnectorError(
1483 message
=SdnError
.UNAUTHORIZED
+ " " + str(e
), http_code
=401
1485 except SdnConnectorError
as sde
:
1487 except Exception as ex
:
1490 # TODO check if there are pending task, and cancel them before restoring
1491 self
.__updateConnection
(cls_currentPerSw
)
1492 except Exception as e
:
1494 "Unable to restore configuration in service {} after an error in the configuration"
1495 " updated: {}".format(service_uuid
, str(e
))
1498 if self
.raiseException
:
1501 raise SdnConnectorError(message
=str(ex
), http_code
=500) from ex
1503 def clear_all_connectivity_services(self
):
1504 """Removes all connectivity services from Arista CloudVision with two steps:
1505 - retrieves all the services from Arista CloudVision
1506 - removes each service
1509 self
.logger
.debug("invoked AristaImpl clear_all_connectivity_services")
1510 self
.__get
_Connection
()
1511 s_list
= self
.__get
_srvUUIDs
()
1515 conn_info
["service_type"] = serv
["type"]
1516 conn_info
["vlan_id"] = serv
["vlan"]
1517 self
.delete_connectivity_service(serv
["uuid"], conn_info
)
1518 except CvpLoginError
as e
:
1519 self
.logger
.info(str(e
))
1522 raise SdnConnectorError(
1523 message
=SdnError
.UNAUTHORIZED
+ " " + str(e
), http_code
=401
1525 except SdnConnectorError
as sde
:
1527 except Exception as ex
:
1529 self
.logger
.error(ex
)
1531 if self
.raiseException
:
1534 raise SdnConnectorError(
1535 message
=SdnError
.INTERNAL_ERROR
+ " " + str(ex
), http_code
=500
1538 def get_all_active_connectivity_services(self
):
1539 """Return the uuid of all the active connectivity services with two steps:
1540 - retrives all the services from Arista CloudVision
1541 - retrives the status of each server
1545 "invoked AristaImpl {}".format("get_all_active_connectivity_services")
1547 self
.__get
_Connection
()
1548 s_list
= self
.__get
_srvUUIDs
()
1553 conn_info
["service_type"] = serv
["type"]
1554 conn_info
["vlan_id"] = serv
["vlan"]
1555 status
= self
.get_connectivity_service_status(serv
["uuid"], conn_info
)
1557 if status
["sdn_status"] == "ACTIVE":
1558 result
.append(serv
["uuid"])
1561 except CvpLoginError
as e
:
1562 self
.logger
.info(str(e
))
1564 raise SdnConnectorError(
1565 message
=SdnError
.UNAUTHORIZED
+ " " + str(e
), http_code
=401
1567 except SdnConnectorError
as sde
:
1569 except Exception as ex
:
1571 self
.logger
.error(ex
)
1573 if self
.raiseException
:
1576 raise SdnConnectorError(
1577 message
=SdnError
.INTERNAL_ERROR
, http_code
=500
1580 def __get_serviceConfigLets(self
, service_uuid
, service_type
, vlan_id
):
1581 """Return the configLet's associated with a connectivity service,
1582 There should be one, as maximum, per device (switch) for a given
1583 connectivity service
1587 for s
in self
.switches
:
1589 found_in_cvp
= False
1601 cvp_cl
= self
.client
.api
.get_configlet_by_name(name
)
1603 except CvpApiError
as error
:
1604 if "Entity does not exist" in error
.msg
:
1605 self
.logger
.exception(f
"{error} occured.")
1614 def __get_srvVLANs(self
):
1615 """Returns a list with all the VLAN id's used in the connectivity services managed
1616 in tha Arista CloudVision by checking the 'OSM_metadata' configLet where this
1617 information is stored
1619 found_in_cvp
= False
1622 cvp_cl
= self
.client
.api
.get_configlet_by_name(self
.__OSM
_METADATA
)
1624 except CvpApiError
as error
:
1625 if "Entity does not exist" in error
.msg
:
1626 self
.logger
.exception(f
"{error} occured.")
1632 lines
= cvp_cl
["config"].split("\n")
1635 if self
.__METADATA
_PREFIX
in line
:
1636 s_vlan
= line
.split(" ")[3]
1640 if s_vlan
is not None and len(s_vlan
) > 0 and s_vlan
not in s_vlan_list
:
1641 s_vlan_list
.append(s_vlan
)
1645 def __get_srvUUIDs(self
):
1646 """Retrieves all the connectivity services, managed in tha Arista CloudVision
1647 by checking the 'OSM_metadata' configLet where this information is stored
1649 found_in_cvp
= False
1652 cvp_cl
= self
.client
.api
.get_configlet_by_name(self
.__OSM
_METADATA
)
1654 except CvpApiError
as error
:
1655 if "Entity does not exist" in error
.msg
:
1656 self
.logger
.exception(f
"{error} occured.")
1662 lines
= cvp_cl
["config"].split("\n")
1665 if self
.__METADATA
_PREFIX
in line
:
1666 line
= line
.split(" ")
1667 serv
= {"uuid": line
[4], "type": line
[2], "vlan": line
[3]}
1671 if serv
is not None and len(serv
) > 0 and serv
not in serv_list
:
1672 serv_list
.append(serv
)
1676 def __get_Connection(self
):
1677 """Open a connection with Arista CloudVision,
1678 invoking the version retrival as test
1681 if self
.client
is None:
1682 self
.client
= self
.__connect
()
1684 self
.client
.api
.get_cvp_info()
1685 except (CvpSessionLogOutError
, RequestException
) as e
:
1686 self
.logger
.debug("Connection error '{}'. Reconnecting".format(e
))
1687 self
.client
= self
.__connect
()
1688 self
.client
.api
.get_cvp_info()
1690 def __connect(self
):
1691 """Connects to CVP device using user provided credentials from initialization.
1692 :return: CvpClient object with connection instantiated.
1694 client
= CvpClient()
1695 protocol
, _
, rest_url
= self
.__wim
_url
.rpartition("://")
1696 host
, _
, port
= rest_url
.partition(":")
1698 if port
and port
.endswith("/"):
1699 port
= int(port
[:-1])
1709 protocol
=protocol
or "https",
1713 client
.api
= CvpApi(client
, request_timeout
=self
.__API
_REQUEST
_TOUT
)
1714 self
.taskC
= AristaCVPTask(client
.api
)
1718 def __compare(self
, fromText
, toText
, lines
=10):
1719 """Compare text string in 'fromText' with 'toText' and produce
1720 diffRatio - a score as a float in the range [0, 1] 2.0*M / T
1721 T is the total number of elements in both sequences,
1722 M is the number of matches.
1723 Score - 1.0 if the sequences are identical, and
1724 0.0 if they have nothing in common.
1727 '- ' line unique to sequence 1
1728 '+ ' line unique to sequence 2
1729 ' ' line common to both sequences
1730 '? ' line not present in either input sequence
1732 fromlines
= fromText
.splitlines(1)
1733 tolines
= toText
.splitlines(1)
1734 diff
= list(difflib
.unified_diff(fromlines
, tolines
, n
=lines
))
1735 textComp
= difflib
.SequenceMatcher(None, fromText
, toText
)
1736 diffRatio
= round(textComp
.quick_ratio() * 100, 2)
1738 return [diffRatio
, diff
]
1740 def __load_inventory(self
):
1741 """Get Inventory Data for All Devices (aka switches) from the Arista CloudVision"""
1742 if not self
.cvp_inventory
:
1743 self
.cvp_inventory
= self
.client
.api
.get_inventory()
1745 self
.allDeviceFacts
= []
1747 for device
in self
.cvp_inventory
:
1748 self
.allDeviceFacts
.append(device
)
1750 def __get_tags(self
, name
, value
):
1751 if not self
.cvp_tags
:
1753 url
= "/api/v1/rest/analytics/tags/labels/devices/{}/value/{}/elements".format(
1756 self
.logger
.debug("get_tags: URL {}".format(url
))
1757 data
= self
.client
.get(url
, timeout
=self
.__API
_REQUEST
_TOUT
)
1759 for dev
in data
["notifications"]:
1760 for elem
in dev
["updates"]:
1761 self
.cvp_tags
.append(elem
)
1764 "Available devices with tag_name {} - value {}: {}".format(
1765 name
, value
, self
.cvp_tags
1769 def __get_interface_ip(self
, device_id
, interface
):
1770 url
= "/api/v1/rest/{}/Sysdb/ip/config/ipIntfConfig/{}/".format(
1771 device_id
, interface
1773 self
.logger
.debug("get_interface_ip: URL {}".format(url
))
1777 data
= self
.client
.get(url
, timeout
=self
.__API
_REQUEST
_TOUT
)
1779 if data
["notifications"]:
1780 for notification
in data
["notifications"]:
1781 for update
in notification
["updates"]:
1782 if update
== "addrWithMask":
1783 return notification
["updates"][update
]["value"]
1784 except Exception as e
:
1785 raise SdnConnectorError(
1786 "Invalid response from url {}: data {} - {}".format(url
, data
, str(e
))
1789 raise SdnConnectorError(
1790 "Unable to get ip for interface {} in device {}, data {}".format(
1791 interface
, device_id
, data
1795 def __get_device_ASN(self
, device_id
):
1796 url
= "/api/v1/rest/{}/Sysdb/routing/bgp/config/".format(device_id
)
1797 self
.logger
.debug("get_device_ASN: URL {}".format(url
))
1801 data
= self
.client
.get(url
, timeout
=self
.__API
_REQUEST
_TOUT
)
1802 if data
["notifications"]:
1803 for notification
in data
["notifications"]:
1804 for update
in notification
["updates"]:
1805 if update
== "asNumber":
1806 return notification
["updates"][update
]["value"]["value"][
1809 except Exception as e
:
1810 raise SdnConnectorError(
1811 "Invalid response from url {}: data {} - {}".format(url
, data
, str(e
))
1814 raise SdnConnectorError(
1815 "Unable to get AS in device {}, data {}".format(device_id
, data
)
1818 def __get_peer_MLAG(self
, device_id
):
1820 url
= "/api/v1/rest/{}/Sysdb/mlag/status/".format(device_id
)
1821 self
.logger
.debug("get_MLAG_status: URL {}".format(url
))
1824 data
= self
.client
.get(url
, timeout
=self
.__API
_REQUEST
_TOUT
)
1826 if data
["notifications"]:
1829 for notification
in data
["notifications"]:
1830 for update
in notification
["updates"]:
1831 if update
== "systemId":
1832 mlagSystemId
= notification
["updates"][update
]["value"]
1839 # search the MLAG System Id
1841 for s
in self
.switches
:
1842 if self
.switches
[s
]["serialNumber"] == device_id
:
1845 url
= "/api/v1/rest/{}/Sysdb/mlag/status/".format(
1846 self
.switches
[s
]["serialNumber"]
1849 "Searching for MLAG system id {} in switch {}".format(
1853 data
= self
.client
.get(url
, timeout
=self
.__API
_REQUEST
_TOUT
)
1856 for notification
in data
["notifications"]:
1857 for update
in notification
["updates"]:
1858 if update
== "systemId":
1861 == notification
["updates"][update
]["value"]
1875 "No Peer device found for device {} with MLAG address {}".format(
1876 device_id
, mlagSystemId
1881 "Peer MLAG for device {} - value {}".format(device_id
, peer
)
1886 raise SdnConnectorError(
1887 "Invalid response from url {}: data {}".format(url
, data
)
1890 def is_valid_destination(self
, url
):
1891 """Check that the provided WIM URL is correct"""
1892 if re
.match(self
.__regex
, url
):
1894 elif self
.is_valid_ipv4_address(url
):
1897 return self
.is_valid_ipv6_address(url
)
1899 def is_valid_ipv4_address(self
, address
):
1900 """Checks that the given IP is IPv4 valid"""
1902 socket
.inet_pton(socket
.AF_INET
, address
)
1903 except AttributeError: # no inet_pton here, sorry
1905 socket
.inet_aton(address
)
1906 except socket
.error
:
1909 return address
.count(".") == 3
1910 except socket
.error
: # not a valid address
1915 def is_valid_ipv6_address(self
, address
):
1916 """Checks that the given IP is IPv6 valid"""
1918 socket
.inet_pton(socket
.AF_INET6
, address
)
1919 except socket
.error
: # not a valid address
1924 def delete_keys_from_dict(self
, dict_del
, lst_keys
):
1925 if dict_del
is None:
1928 dict_copy
= {k
: v
for k
, v
in dict_del
.items() if k
not in lst_keys
}
1930 for k
, v
in dict_copy
.items():
1931 if isinstance(v
, dict):
1932 dict_copy
[k
] = self
.delete_keys_from_dict(v
, lst_keys
)