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"])
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
)
964 for s
in self
.switches
:
965 if allLeafConfigured
[s
]:
966 self
.__configlet
_modify
(cls_perSw
[s
], delete
=True)
968 def __exec_task(self
, tasks
, tout
=10):
969 if self
.taskC
is None:
972 data
= self
.taskC
.update_all_tasks(tasks
).values()
973 self
.taskC
.task_action(data
, tout
, "executed")
975 def __device_modify(self
, device_to_update
, new_configlets
, delete
):
976 """Updates the devices (switches) adding or removing the configLet,
977 the tasks Id's associated to the change are returned
979 self
.logger
.info("Enter in __device_modify delete: {}".format(delete
))
982 # Task Ids that have been identified during device actions
986 len(new_configlets
) == 0
987 or device_to_update
is None
988 or len(device_to_update
) == 0
990 data
= {"updated": updated
, "tasks": newTasks
}
992 return [changed
, data
]
994 self
.__load
_inventory
()
996 allDeviceFacts
= self
.allDeviceFacts
997 # Work through Devices list adding device specific information
999 for try_device
in allDeviceFacts
:
1000 # Add Device Specific Configlets
1001 # self.logger.debug(device)
1002 if try_device
["hostname"] not in device_to_update
:
1005 dev_cvp_configlets
= self
.client
.api
.get_configlets_by_device_id(
1006 try_device
["systemMacAddress"]
1008 # self.logger.debug(dev_cvp_configlets)
1009 try_device
["deviceSpecificConfiglets"] = []
1011 for cvp_configlet
in dev_cvp_configlets
:
1012 if int(cvp_configlet
["containerCount"]) == 0:
1013 try_device
["deviceSpecificConfiglets"].append(
1014 {"name": cvp_configlet
["name"], "key": cvp_configlet
["key"]}
1017 # self.logger.debug(device)
1021 # Check assigned configlets
1022 device_update
= False
1024 remove_configlets
= []
1028 for cvp_configlet
in device
["deviceSpecificConfiglets"]:
1029 for cl
in new_configlets
:
1030 if cvp_configlet
["name"] == cl
["name"]:
1031 remove_configlets
.append(cvp_configlet
)
1032 device_update
= True
1034 for configlet
in new_configlets
:
1035 if configlet
not in device
["deviceSpecificConfiglets"]:
1036 add_configlets
.append(configlet
)
1037 device_update
= True
1040 update_devices
.append(
1042 "hostname": device
["hostname"],
1043 "configlets": [add_configlets
, remove_configlets
],
1048 self
.logger
.info("Device to modify: {}".format(update_devices
))
1050 up_device
= update_devices
[0]
1051 cl_toAdd
= up_device
["configlets"][0]
1052 cl_toDel
= up_device
["configlets"][1]
1056 if delete
and len(cl_toDel
) > 0:
1057 r
= self
.client
.api
.remove_configlets_from_device(
1058 "OSM", up_device
["device"], cl_toDel
, create_task
=True
1062 "remove_configlets_from_device {} {}".format(dev_action
, cl_toDel
)
1064 elif len(cl_toAdd
) > 0:
1065 r
= self
.client
.api
.apply_configlets_to_device(
1066 "OSM", up_device
["device"], cl_toAdd
, create_task
=True
1070 "apply_configlets_to_device {} {}".format(dev_action
, cl_toAdd
)
1072 except Exception as error
:
1073 errorMessage
= str(error
)
1074 msg
= "errorMessage: Device {} Configlets could not be updated: {}".format(
1075 up_device
["hostname"], errorMessage
1077 raise SdnConnectorError(msg
) from error
1079 if "errorMessage" in str(dev_action
):
1080 m
= "Device {} Configlets update fail: {}".format(
1081 up_device
["name"], dev_action
["errorMessage"]
1083 raise SdnConnectorError(m
)
1086 if "taskIds" in str(dev_action
):
1087 # Fix 1030 SDN-ARISTA Key error note when deploy a NS
1088 if not dev_action
["data"]["taskIds"]:
1089 raise SdnConnectorError(
1090 "No taskIds found: Device {} Configlets could not be updated".format(
1091 up_device
["hostname"]
1095 for taskId
in dev_action
["data"]["taskIds"]:
1097 {up_device
["hostname"]: "Configlets-{}".format(taskId
)}
1099 newTasks
.append(taskId
)
1102 {up_device
["hostname"]: "Configlets-No_Specific_Tasks"}
1105 data
= {"updated": updated
, "tasks": newTasks
}
1107 return [changed
, data
]
1109 def __configlet_modify(self
, configletsToApply
, delete
=False):
1110 """Adds/update or delete the provided configLets
1111 :param configletsToApply: list of configLets to apply
1112 :param delete: flag to indicate if the configLets have to be deleted
1113 from Cloud Vision Portal
1114 :return: data: dict of module actions and taskIDs
1116 self
.logger
.info("Enter in __configlet_modify delete:{}".format(delete
))
1118 # Compare configlets against cvp_facts-configlets
1125 for cl
in configletsToApply
:
1126 found_in_cvp
= False
1133 cvp_cl
= self
.client
.api
.get_configlet_by_name(cl
["name"])
1134 cl
["key"] = cvp_cl
["key"]
1135 cl
["note"] = cvp_cl
["note"]
1137 except CvpApiError
as error
:
1138 if "Entity does not exist" in error
.msg
:
1146 configlet
= {"name": cvp_cl
["name"], "data": cvp_cl
}
1149 cl_compare
= self
.__compare
(cl
["config"], cvp_cl
["config"])
1151 # compare function returns a floating point number
1152 if cl_compare
[0] != 100.0:
1157 "config": cl
["config"],
1163 "key": cvp_cl
["key"],
1165 "config": cl
["config"],
1169 configlet
= {"name": cl
["name"], "config": cl
["config"]}
1172 operation
= "delete"
1173 resp
= self
.client
.api
.delete_configlet(
1174 configlet
["data"]["name"], configlet
["data"]["key"]
1177 operation
= "update"
1178 resp
= self
.client
.api
.update_configlet(
1179 configlet
["config"],
1180 configlet
["data"]["key"],
1181 configlet
["data"]["name"],
1185 operation
= "create"
1186 resp
= self
.client
.api
.add_configlet(
1187 configlet
["name"], configlet
["config"]
1190 operation
= "checked"
1192 except Exception as error
:
1193 errorMessage
= str(error
).split(":")[-1]
1194 message
= "Configlet {} cannot be {}: {}".format(
1195 cl
["name"], operation
, errorMessage
1199 deleted
.append({configlet
["name"]: message
})
1201 updated
.append({configlet
["name"]: message
})
1203 new
.append({configlet
["name"]: message
})
1205 checked
.append({configlet
["name"]: message
})
1207 if "error" in str(resp
).lower():
1208 message
= "Configlet {} cannot be deleted: {}".format(
1209 cl
["name"], resp
["errorMessage"]
1213 deleted
.append({configlet
["name"]: message
})
1215 updated
.append({configlet
["name"]: message
})
1217 new
.append({configlet
["name"]: message
})
1219 checked
.append({configlet
["name"]: message
})
1223 deleted
.append({configlet
["name"]: "success"})
1226 updated
.append({configlet
["name"]: "success"})
1229 # This key is used in API call deviceApplyConfigLet FGA
1231 new
.append({configlet
["name"]: "success"})
1234 checked
.append({configlet
["name"]: "success"})
1236 data
= {"new": new
, "updated": updated
, "deleted": deleted
, "checked": checked
}
1238 return [changed
, data
]
1240 def __get_configletsDevices(self
, configlets
):
1241 for s
in self
.switches
:
1242 configlet
= configlets
[s
]
1244 # Add applied Devices
1245 if len(configlet
) > 0:
1246 configlet
["devices"] = []
1247 applied_devices
= self
.client
.api
.get_applied_devices(configlet
["name"])
1249 for device
in applied_devices
["data"]:
1250 configlet
["devices"].append(device
["hostName"])
1252 def __get_serviceData(self
, service_uuid
, service_type
, vlan_id
, conn_info
=None):
1255 for s
in self
.switches
:
1259 srv_cls
= self
.__get
_serviceConfigLets
(service_uuid
, service_type
, vlan_id
)
1260 self
.__get
_configletsDevices
(srv_cls
)
1262 for s
in self
.switches
:
1265 for dev
in cl
["devices"]:
1266 cls_perSw
[dev
].append(cl
)
1268 cls_perSw
= conn_info
["configLetPerSwitch"]
1272 def delete_connectivity_service(self
, service_uuid
, conn_info
=None):
1274 Disconnect multi-site endpoints previously connected
1276 :param service_uuid: The one returned by create_connectivity_service
1277 :param conn_info: The one returned by last call to 'create_connectivity_service' or 'edit_connectivity_service'
1278 if they do not return None
1280 :raises: SdnConnectorException: In case of error. The parameter http_code must be filled
1284 "invoked delete_connectivity_service {}".format(service_uuid
)
1287 if not service_uuid
:
1288 raise SdnConnectorError(
1289 message
="No connection service UUID", http_code
=500
1292 self
.__get
_Connection
()
1294 if conn_info
is None:
1295 raise SdnConnectorError(
1296 message
="No connection information for service UUID {}".format(
1303 cls_perSw
= self
.__get
_serviceData
(
1304 service_uuid
, conn_info
["service_type"], conn_info
["vlan_id"], c_info
1306 allLeafConfigured
= {}
1307 allLeafModified
= {}
1309 for s
in self
.switches
:
1310 allLeafConfigured
[s
] = True
1311 allLeafModified
[s
] = True
1313 found_in_cvp
= False
1315 for s
in self
.switches
:
1320 self
.__rollbackConnection
(cls_perSw
, allLeafConfigured
, allLeafModified
)
1322 # if the service is not defined in Cloud Vision, return a 404 - NotFound error
1323 raise SdnConnectorError(
1324 message
="Service {} was not found in Arista Cloud Vision {}".format(
1325 service_uuid
, self
.__wim
_url
1330 self
.__removeMetadata
(service_uuid
)
1331 except CvpLoginError
as e
:
1332 self
.logger
.info(str(e
))
1334 raise SdnConnectorError(
1335 message
=SdnError
.UNAUTHORIZED
+ " " + str(e
), http_code
=401
1337 except SdnConnectorError
as sde
:
1339 except Exception as ex
:
1341 self
.logger
.error(ex
)
1343 if self
.raiseException
:
1346 raise SdnConnectorError(
1347 message
=SdnError
.INTERNAL_ERROR
+ " " + str(ex
), http_code
=500
1350 def __addMetadata(self
, service_uuid
, service_type
, vlan_id
):
1351 """Adds the connectivity service from 'OSM_metadata' configLet"""
1352 found_in_cvp
= False
1355 cvp_cl
= self
.client
.api
.get_configlet_by_name(self
.__OSM
_METADATA
)
1357 except CvpApiError
as error
:
1358 if "Entity does not exist" in error
.msg
:
1364 new_serv
= "{} {} {} {}\n".format(
1365 self
.__METADATA
_PREFIX
, service_type
, vlan_id
, service_uuid
1369 cl_config
= cvp_cl
["config"] + new_serv
1371 cl_config
= new_serv
1373 cl_meta
= [{"name": self
.__OSM
_METADATA
, "config": cl_config
}]
1374 self
.__configlet
_modify
(cl_meta
)
1375 except Exception as e
:
1377 "Error in setting metadata in CloudVision from OSM for service {}: {}".format(
1378 service_uuid
, str(e
)
1383 def __removeMetadata(self
, service_uuid
):
1384 """Removes the connectivity service from 'OSM_metadata' configLet"""
1385 found_in_cvp
= False
1388 cvp_cl
= self
.client
.api
.get_configlet_by_name(self
.__OSM
_METADATA
)
1390 except CvpApiError
as error
:
1391 if "Entity does not exist" in error
.msg
:
1398 if service_uuid
in cvp_cl
["config"]:
1401 for line
in cvp_cl
["config"].split("\n"):
1402 if service_uuid
in line
:
1405 cl_config
= cl_config
+ line
1407 cl_meta
= [{"name": self
.__OSM
_METADATA
, "config": cl_config
}]
1408 self
.__configlet
_modify
(cl_meta
)
1409 except Exception as e
:
1411 "Error in removing metadata in CloudVision from OSM for service {}: {}".format(
1412 service_uuid
, str(e
)
1417 def edit_connectivity_service(
1418 self
, service_uuid
, conn_info
=None, connection_points
=None, **kwargs
1420 """Change an existing connectivity service.
1422 This method's arguments and return value follow the same convention as
1423 :meth:`~.create_connectivity_service`.
1425 :param service_uuid: UUID of the connectivity service.
1426 :param conn_info: (dict or None): Information previously returned
1427 by last call to create_connectivity_service
1428 or edit_connectivity_service
1429 :param connection_points: (list): If provided, the old list of
1430 connection points will be replaced.
1431 :param kwargs: Same meaning that create_connectivity_service
1432 :return: dict or None: Information to be updated and stored at
1434 When ``None`` is returned, no information should be changed.
1435 When an empty dict is returned, the database record will
1437 **MUST** be JSON/YAML-serializable (plain data structures).
1439 SdnConnectorError: In case of error.
1443 "invoked edit_connectivity_service for service {}. ports: {}".format(
1444 service_uuid
, connection_points
1448 if not service_uuid
:
1449 raise SdnConnectorError(
1450 message
="Unable to perform operation, missing or empty uuid",
1455 raise SdnConnectorError(
1456 message
="Unable to perform operation, missing or empty connection information",
1460 if connection_points
is None:
1463 self
.__get
_Connection
()
1465 cls_currentPerSw
= conn_info
["configLetPerSwitch"]
1466 service_type
= conn_info
["service_type"]
1468 self
.__check
_service
(
1476 s_uid
, s_connInf
= self
.__processConnection
(
1477 service_uuid
, service_type
, connection_points
, kwargs
1479 self
.logger
.info("Service with uuid {} configuration updated".format(s_uid
))
1482 except CvpLoginError
as e
:
1483 self
.logger
.info(str(e
))
1485 raise SdnConnectorError(
1486 message
=SdnError
.UNAUTHORIZED
+ " " + str(e
), http_code
=401
1488 except SdnConnectorError
as sde
:
1490 except Exception as ex
:
1493 # TODO check if there are pending task, and cancel them before restoring
1494 self
.__updateConnection
(cls_currentPerSw
)
1495 except Exception as e
:
1497 "Unable to restore configuration in service {} after an error in the configuration"
1498 " updated: {}".format(service_uuid
, str(e
))
1501 if self
.raiseException
:
1504 raise SdnConnectorError(message
=str(ex
), http_code
=500) from ex
1506 def clear_all_connectivity_services(self
):
1507 """Removes all connectivity services from Arista CloudVision with two steps:
1508 - retrieves all the services from Arista CloudVision
1509 - removes each service
1512 self
.logger
.debug("invoked AristaImpl clear_all_connectivity_services")
1513 self
.__get
_Connection
()
1514 s_list
= self
.__get
_srvUUIDs
()
1518 conn_info
["service_type"] = serv
["type"]
1519 conn_info
["vlan_id"] = serv
["vlan"]
1520 self
.delete_connectivity_service(serv
["uuid"], conn_info
)
1521 except CvpLoginError
as e
:
1522 self
.logger
.info(str(e
))
1525 raise SdnConnectorError(
1526 message
=SdnError
.UNAUTHORIZED
+ " " + str(e
), http_code
=401
1528 except SdnConnectorError
as sde
:
1530 except Exception as ex
:
1532 self
.logger
.error(ex
)
1534 if self
.raiseException
:
1537 raise SdnConnectorError(
1538 message
=SdnError
.INTERNAL_ERROR
+ " " + str(ex
), http_code
=500
1541 def get_all_active_connectivity_services(self
):
1542 """Return the uuid of all the active connectivity services with two steps:
1543 - retrives all the services from Arista CloudVision
1544 - retrives the status of each server
1548 "invoked AristaImpl {}".format("get_all_active_connectivity_services")
1550 self
.__get
_Connection
()
1551 s_list
= self
.__get
_srvUUIDs
()
1556 conn_info
["service_type"] = serv
["type"]
1557 conn_info
["vlan_id"] = serv
["vlan"]
1558 status
= self
.get_connectivity_service_status(serv
["uuid"], conn_info
)
1560 if status
["sdn_status"] == "ACTIVE":
1561 result
.append(serv
["uuid"])
1564 except CvpLoginError
as e
:
1565 self
.logger
.info(str(e
))
1567 raise SdnConnectorError(
1568 message
=SdnError
.UNAUTHORIZED
+ " " + str(e
), http_code
=401
1570 except SdnConnectorError
as sde
:
1572 except Exception as ex
:
1574 self
.logger
.error(ex
)
1576 if self
.raiseException
:
1579 raise SdnConnectorError(
1580 message
=SdnError
.INTERNAL_ERROR
, http_code
=500
1583 def __get_serviceConfigLets(self
, service_uuid
, service_type
, vlan_id
):
1584 """Return the configLet's associated with a connectivity service,
1585 There should be one, as maximum, per device (switch) for a given
1586 connectivity service
1590 for s
in self
.switches
:
1592 found_in_cvp
= False
1604 cvp_cl
= self
.client
.api
.get_configlet_by_name(name
)
1606 except CvpApiError
as error
:
1607 if "Entity does not exist" in error
.msg
:
1617 def __get_srvVLANs(self
):
1618 """Returns a list with all the VLAN id's used in the connectivity services managed
1619 in tha Arista CloudVision by checking the 'OSM_metadata' configLet where this
1620 information is stored
1622 found_in_cvp
= False
1625 cvp_cl
= self
.client
.api
.get_configlet_by_name(self
.__OSM
_METADATA
)
1627 except CvpApiError
as error
:
1628 if "Entity does not exist" in error
.msg
:
1635 lines
= cvp_cl
["config"].split("\n")
1638 if self
.__METADATA
_PREFIX
in line
:
1639 s_vlan
= line
.split(" ")[3]
1643 if s_vlan
is not None and len(s_vlan
) > 0 and s_vlan
not in s_vlan_list
:
1644 s_vlan_list
.append(s_vlan
)
1648 def __get_srvUUIDs(self
):
1649 """Retrieves all the connectivity services, managed in tha Arista CloudVision
1650 by checking the 'OSM_metadata' configLet where this information is stored
1652 found_in_cvp
= False
1655 cvp_cl
= self
.client
.api
.get_configlet_by_name(self
.__OSM
_METADATA
)
1657 except CvpApiError
as error
:
1658 if "Entity does not exist" in error
.msg
:
1665 lines
= cvp_cl
["config"].split("\n")
1668 if self
.__METADATA
_PREFIX
in line
:
1669 line
= line
.split(" ")
1670 serv
= {"uuid": line
[4], "type": line
[2], "vlan": line
[3]}
1674 if serv
is not None and len(serv
) > 0 and serv
not in serv_list
:
1675 serv_list
.append(serv
)
1679 def __get_Connection(self
):
1680 """Open a connection with Arista CloudVision,
1681 invoking the version retrival as test
1684 if self
.client
is None:
1685 self
.client
= self
.__connect
()
1687 self
.client
.api
.get_cvp_info()
1688 except (CvpSessionLogOutError
, RequestException
) as e
:
1689 self
.logger
.debug("Connection error '{}'. Reconnecting".format(e
))
1690 self
.client
= self
.__connect
()
1691 self
.client
.api
.get_cvp_info()
1693 def __connect(self
):
1694 """Connects to CVP device using user provided credentials from initialization.
1695 :return: CvpClient object with connection instantiated.
1697 client
= CvpClient()
1698 protocol
, _
, rest_url
= self
.__wim
_url
.rpartition("://")
1699 host
, _
, port
= rest_url
.partition(":")
1701 if port
and port
.endswith("/"):
1702 port
= int(port
[:-1])
1712 protocol
=protocol
or "https",
1716 client
.api
= CvpApi(client
, request_timeout
=self
.__API
_REQUEST
_TOUT
)
1717 self
.taskC
= AristaCVPTask(client
.api
)
1721 def __compare(self
, fromText
, toText
, lines
=10):
1722 """Compare text string in 'fromText' with 'toText' and produce
1723 diffRatio - a score as a float in the range [0, 1] 2.0*M / T
1724 T is the total number of elements in both sequences,
1725 M is the number of matches.
1726 Score - 1.0 if the sequences are identical, and
1727 0.0 if they have nothing in common.
1730 '- ' line unique to sequence 1
1731 '+ ' line unique to sequence 2
1732 ' ' line common to both sequences
1733 '? ' line not present in either input sequence
1735 fromlines
= fromText
.splitlines(1)
1736 tolines
= toText
.splitlines(1)
1737 diff
= list(difflib
.unified_diff(fromlines
, tolines
, n
=lines
))
1738 textComp
= difflib
.SequenceMatcher(None, fromText
, toText
)
1739 diffRatio
= round(textComp
.quick_ratio() * 100, 2)
1741 return [diffRatio
, diff
]
1743 def __load_inventory(self
):
1744 """Get Inventory Data for All Devices (aka switches) from the Arista CloudVision"""
1745 if not self
.cvp_inventory
:
1746 self
.cvp_inventory
= self
.client
.api
.get_inventory()
1748 self
.allDeviceFacts
= []
1750 for device
in self
.cvp_inventory
:
1751 self
.allDeviceFacts
.append(device
)
1753 def __get_tags(self
, name
, value
):
1754 if not self
.cvp_tags
:
1756 url
= "/api/v1/rest/analytics/tags/labels/devices/{}/value/{}/elements".format(
1759 self
.logger
.debug("get_tags: URL {}".format(url
))
1760 data
= self
.client
.get(url
, timeout
=self
.__API
_REQUEST
_TOUT
)
1762 for dev
in data
["notifications"]:
1763 for elem
in dev
["updates"]:
1764 self
.cvp_tags
.append(elem
)
1767 "Available devices with tag_name {} - value {}: {}".format(
1768 name
, value
, self
.cvp_tags
1772 def __get_interface_ip(self
, device_id
, interface
):
1773 url
= "/api/v1/rest/{}/Sysdb/ip/config/ipIntfConfig/{}/".format(
1774 device_id
, interface
1776 self
.logger
.debug("get_interface_ip: URL {}".format(url
))
1780 data
= self
.client
.get(url
, timeout
=self
.__API
_REQUEST
_TOUT
)
1782 if data
["notifications"]:
1783 for notification
in data
["notifications"]:
1784 for update
in notification
["updates"]:
1785 if update
== "addrWithMask":
1786 return notification
["updates"][update
]["value"]
1787 except Exception as e
:
1788 raise SdnConnectorError(
1789 "Invalid response from url {}: data {} - {}".format(url
, data
, str(e
))
1792 raise SdnConnectorError(
1793 "Unable to get ip for interface {} in device {}, data {}".format(
1794 interface
, device_id
, data
1798 def __get_device_ASN(self
, device_id
):
1799 url
= "/api/v1/rest/{}/Sysdb/routing/bgp/config/".format(device_id
)
1800 self
.logger
.debug("get_device_ASN: URL {}".format(url
))
1804 data
= self
.client
.get(url
, timeout
=self
.__API
_REQUEST
_TOUT
)
1805 if data
["notifications"]:
1806 for notification
in data
["notifications"]:
1807 for update
in notification
["updates"]:
1808 if update
== "asNumber":
1809 return notification
["updates"][update
]["value"]["value"][
1812 except Exception as e
:
1813 raise SdnConnectorError(
1814 "Invalid response from url {}: data {} - {}".format(url
, data
, str(e
))
1817 raise SdnConnectorError(
1818 "Unable to get AS in device {}, data {}".format(device_id
, data
)
1821 def __get_peer_MLAG(self
, device_id
):
1823 url
= "/api/v1/rest/{}/Sysdb/mlag/status/".format(device_id
)
1824 self
.logger
.debug("get_MLAG_status: URL {}".format(url
))
1827 data
= self
.client
.get(url
, timeout
=self
.__API
_REQUEST
_TOUT
)
1829 if data
["notifications"]:
1832 for notification
in data
["notifications"]:
1833 for update
in notification
["updates"]:
1834 if update
== "systemId":
1835 mlagSystemId
= notification
["updates"][update
]["value"]
1842 # search the MLAG System Id
1844 for s
in self
.switches
:
1845 if self
.switches
[s
]["serialNumber"] == device_id
:
1848 url
= "/api/v1/rest/{}/Sysdb/mlag/status/".format(
1849 self
.switches
[s
]["serialNumber"]
1852 "Searching for MLAG system id {} in switch {}".format(
1856 data
= self
.client
.get(url
, timeout
=self
.__API
_REQUEST
_TOUT
)
1859 for notification
in data
["notifications"]:
1860 for update
in notification
["updates"]:
1861 if update
== "systemId":
1864 == notification
["updates"][update
]["value"]
1878 "No Peer device found for device {} with MLAG address {}".format(
1879 device_id
, mlagSystemId
1884 "Peer MLAG for device {} - value {}".format(device_id
, peer
)
1889 raise SdnConnectorError(
1890 "Invalid response from url {}: data {}".format(url
, data
)
1893 def is_valid_destination(self
, url
):
1894 """Check that the provided WIM URL is correct"""
1895 if re
.match(self
.__regex
, url
):
1897 elif self
.is_valid_ipv4_address(url
):
1900 return self
.is_valid_ipv6_address(url
)
1902 def is_valid_ipv4_address(self
, address
):
1903 """Checks that the given IP is IPv4 valid"""
1905 socket
.inet_pton(socket
.AF_INET
, address
)
1906 except AttributeError: # no inet_pton here, sorry
1908 socket
.inet_aton(address
)
1909 except socket
.error
:
1912 return address
.count(".") == 3
1913 except socket
.error
: # not a valid address
1918 def is_valid_ipv6_address(self
, address
):
1919 """Checks that the given IP is IPv6 valid"""
1921 socket
.inet_pton(socket
.AF_INET6
, address
)
1922 except socket
.error
: # not a valid address
1927 def delete_keys_from_dict(self
, dict_del
, lst_keys
):
1928 if dict_del
is None:
1931 dict_copy
= {k
: v
for k
, v
in dict_del
.items() if k
not in lst_keys
}
1933 for k
, v
in dict_copy
.items():
1934 if isinstance(v
, dict):
1935 dict_copy
[k
] = self
.delete_keys_from_dict(v
, lst_keys
)