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.
29 from osm_ro_plugin
.sdnconn
import SdnConnectorBase
, SdnConnectorError
32 # Required by compare function
34 # Library that uses Levenshtein Distance to calculate the differences
36 # from fuzzywuzzy import fuzz
41 from requests
import RequestException
, ConnectionError
, ConnectTimeout
, Timeout
42 from cvprac
.cvp_client
import CvpClient
43 from cvprac
.cvp_api
import CvpApi
44 from cvprac
.cvp_client_errors
import CvpLoginError
, CvpSessionLogOutError
, CvpApiError
45 from cvprac
import __version__
as cvprac_version
47 from osm_rosdn_arista_cloudvision
.aristaConfigLet
import AristaSDNConfigLet
48 from osm_rosdn_arista_cloudvision
.aristaTask
import AristaCVPTask
52 UNREACHABLE
= 'Unable to reach the WIM url, connect error.',
53 TIMEOUT
= 'Unable to reach the WIM url, timeout.',
55 'VLAN value inconsistent between the connection points',
56 VLAN_NOT_PROVIDED
= 'VLAN value not provided',
57 CONNECTION_POINTS_SIZE
= \
58 'Unexpected number of connection points: 2 expected.',
59 ENCAPSULATION_TYPE
= \
60 'Unexpected service_endpoint_encapsulation_type. ' \
61 'Only "dotq1" is accepted.',
62 BANDWIDTH
= 'Unable to get the bandwidth.',
63 STATUS
= 'Unable to get the status for the service.',
64 DELETE
= 'Unable to delete service.',
65 CLEAR_ALL
= 'Unable to clear all the services',
66 UNKNOWN_ACTION
= 'Unknown action invoked.',
67 BACKUP
= 'Unable to get the backup parameter.',
68 UNSUPPORTED_FEATURE
= "Unsupported feature",
69 UNAUTHORIZED
= "Failed while authenticating",
70 INTERNAL_ERROR
= "Internal error"
73 class AristaSdnConnector(SdnConnectorBase
):
74 """Arista class for the SDN connectors
77 wim (dict): WIM record, as stored in the database
78 wim_account (dict): WIM account record, as stored in the database
80 The arguments of the constructor are converted to object attributes.
81 An extra property, ``service_endpoint_mapping`` is created from ``config``.
83 The access to Arista CloudVision is made through the API defined in
84 https://github.com/aristanetworks/cvprac
85 The a connectivity service consist in creating a VLAN and associate the interfaces
86 of the connection points MAC addresses to this VLAN in all the switches of the topology,
87 the BDP is also configured for this VLAN.
89 The Arista Cloud Vision API workflow is the following
90 -- The switch configuration is defined as a set of switch configuration commands,
91 what is called 'ConfigLet'
92 -- The ConfigLet is associated to the device (leaf switch)
93 -- Automatically a task is associated to this activity for change control, the task
94 in this stage is in 'Pending' state
95 -- The task will be executed so that the configuration is applied to the switch.
96 -- The service information is saved in the response of the creation call
97 -- All created services identification is stored in a generic ConfigLet 'OSM_metadata'
98 to keep track of the managed resources by OSM in the Arista deployment.
100 __supported_service_types
= ["ELINE (L2)", "ELINE", "ELAN"]
101 __service_types_ELAN
= "ELAN"
102 __service_types_ELINE
= "ELINE"
103 __ELINE_num_connection_points
= 2
104 __supported_service_types
= ["ELINE", "ELAN"]
105 __supported_encapsulation_types
= ["dot1q"]
106 __WIM_LOGGER
= 'openmano.sdnconn.arista'
107 __SERVICE_ENDPOINT_MAPPING
= 'service_endpoint_mapping'
108 __ENCAPSULATION_TYPE_PARAM
= "service_endpoint_encapsulation_type"
109 __ENCAPSULATION_INFO_PARAM
= "service_endpoint_encapsulation_info"
110 __BACKUP_PARAM
= "backup"
111 __BANDWIDTH_PARAM
= "bandwidth"
112 __SERVICE_ENDPOINT_PARAM
= "service_endpoint_id"
114 __WAN_SERVICE_ENDPOINT_PARAM
= "service_endpoint_id"
115 __WAN_MAPPING_INFO_PARAM
= "service_mapping_info"
116 __DEVICE_ID_PARAM
= "device_id"
117 __DEVICE_INTERFACE_ID_PARAM
= "device_interface_id"
118 __SW_ID_PARAM
= "switch_dpid"
119 __SW_PORT_PARAM
= "switch_port"
120 __VLAN_PARAM
= "vlan"
123 __MANAGED_BY_OSM
= '## Managed by OSM '
124 __OSM_PREFIX
= "osm_"
125 __OSM_METADATA
= "OSM_metadata"
126 __METADATA_PREFIX
= '!## Service'
127 __EXC_TASK_EXEC_WAIT
= 10
128 __ROLLB_TASK_EXEC_WAIT
= 10
129 __API_REQUEST_TOUT
= 60
130 __SWITCH_TAG_NAME
= 'topology_type'
131 __SWITCH_TAG_VALUE
= 'leaf'
132 __LOOPBACK_INTF
= "Loopback0"
135 _VLAN_MLAG
= "VLAN-MLAG"
136 _VXLAN_MLAG
= "VXLAN-MLAG"
138 def __init__(self
, wim
, wim_account
, config
=None, logger
=None):
141 :param wim: (dict). Contains among others 'wim_url'
142 :param wim_account: (dict). Contains among others 'uuid' (internal id), 'name',
143 'sdn' (True if is intended for SDN-assist or False if intended for WIM), 'user', 'password'.
144 :param config: (dict or None): Particular information of plugin. These keys if present have a common meaning:
145 'mapping_not_needed': (bool) False by default or if missing, indicates that mapping is not needed.
146 'service_endpoint_mapping': (list) provides the internal endpoint mapping. The meaning is:
147 KEY meaning for WIM meaning for SDN assist
148 -------- -------- --------
149 device_id pop_switch_dpid compute_id
150 device_interface_id pop_switch_port compute_pci_address
151 service_endpoint_id wan_service_endpoint_id SDN_service_endpoint_id
152 service_mapping_info wan_service_mapping_info SDN_service_mapping_info
153 contains extra information if needed. Text in Yaml format
154 switch_dpid wan_switch_dpid SDN_switch_dpid
155 switch_port wan_switch_port SDN_switch_port
156 datacenter_id vim_account vim_account
157 id: (internal, do not use)
158 wim_id: (internal, do not use)
159 :param logger (logging.Logger): optional logger object. If none is passed 'openmano.sdn.sdnconn' is used.
161 self
.__regex
= re
.compile(
162 r
'^(?:http|ftp)s?://' # http:// or https://
163 r
'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # domain...
164 r
'localhost|' # localhost...
165 r
'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip
166 r
'(?::\d+)?', re
.IGNORECASE
) # optional port
167 self
.raiseException
= True
168 self
.logger
= logger
or logging
.getLogger(self
.__WIM
_LOGGER
)
169 super().__init
__(wim
, wim_account
, config
, self
.logger
)
171 self
.__wim
_account
= wim_account
172 self
.__config
= config
173 if self
.is_valid_destination(self
.__wim
.get("wim_url")):
174 self
.__wim
_url
= self
.__wim
.get("wim_url")
176 raise SdnConnectorError(message
='Invalid wim_url value',
178 self
.__user
= wim_account
.get("user")
179 self
.__passwd
= wim_account
.get("password")
181 self
.cvp_inventory
= None
183 self
.logger
.debug("Arista SDN plugin {}, cvprac version {}, user:{} and config:{}".
184 format(wim
, cvprac_version
, self
.__user
,
185 self
.delete_keys_from_dict(config
, ('passwd',))))
186 self
.allDeviceFacts
= []
189 self
.__load
_topology
()
190 self
.__load
_switches
()
191 except (ConnectTimeout
, Timeout
) as ct
:
192 raise SdnConnectorError(message
=SdnError
.TIMEOUT
+ " " + str(ct
), http_code
=408)
193 except ConnectionError
as ce
:
194 raise SdnConnectorError(message
=SdnError
.UNREACHABLE
+ " " + str(ce
), http_code
=404)
195 except SdnConnectorError
as sc
:
197 except CvpLoginError
as le
:
198 raise SdnConnectorError(message
=le
.msg
, http_code
=500) from le
199 except Exception as e
:
200 raise SdnConnectorError(message
="Unable to load switches from CVP" + " " + str(e
),
201 http_code
=500) from e
202 self
.logger
.debug("Using topology {} in Arista Leaf switches: {}".format(
204 self
.delete_keys_from_dict(self
.switches
, ('passwd',))))
205 self
.clC
= AristaSDNConfigLet(self
.topology
)
207 def __load_topology(self
):
208 self
.topology
= self
._VXLAN
_MLAG
209 if self
.__config
and self
.__config
.get('topology'):
210 topology
= self
.__config
.get('topology')
211 if topology
== "VLAN":
212 self
.topology
= self
._VLAN
213 elif topology
== "VXLAN":
214 self
.topology
= self
._VXLAN
215 elif topology
== "VLAN-MLAG":
216 self
.topology
= self
._VLAN
_MLAG
217 elif topology
== "VXLAN-MLAG":
218 self
.topology
= self
._VXLAN
_MLAG
220 def __load_switches(self
):
221 """ Retrieves the switches to configure in the following order
222 1. from incoming configuration:
223 1.1 using port mapping
224 using user and password from WIM
225 retrieving Lo0 and AS from switch
226 1.2 from 'switches' parameter,
227 if any parameter is not present
228 Lo0 and AS - it will be requested to the switch
229 2. Looking in the CloudVision inventory if not in configuration parameters
230 2.1 using the switches with the topology_type tag set to 'leaf'
232 All the search methods will be used
235 if self
.__config
and self
.__config
.get(self
.__SERVICE
_ENDPOINT
_MAPPING
):
236 for port
in self
.__config
.get(self
.__SERVICE
_ENDPOINT
_MAPPING
):
237 switch_dpid
= port
.get(self
.__SW
_ID
_PARAM
)
238 if switch_dpid
and switch_dpid
not in self
.switches
:
239 self
.switches
[switch_dpid
] = {'passwd': self
.__passwd
,
244 'serialNumber': None,
245 'mlagPeerDevice': None}
247 if self
.__config
and self
.__config
.get('switches'):
248 # Not directly from json, complete one by one
249 config_switches
= self
.__config
.get('switches')
250 for cs
, cs_content
in config_switches
.items():
251 if cs
not in self
.switches
:
252 self
.switches
[cs
] = {'passwd': self
.__passwd
,
257 'serialNumber': None,
258 'mlagPeerDevice': None}
260 self
.switches
[cs
].update(cs_content
)
262 # Load the rest of the data
263 if self
.client
is None:
264 self
.client
= self
.__connect
()
265 self
.__load
_inventory
()
266 if not self
.switches
:
267 self
.__get
_tags
(self
.__SWITCH
_TAG
_NAME
, self
.__SWITCH
_TAG
_VALUE
)
268 for device
in self
.allDeviceFacts
:
269 # get the switches whose topology_tag is 'leaf'
270 if device
['serialNumber'] in self
.cvp_tags
:
271 if not self
.switches
.get(device
['hostname']):
272 switch_data
= {'passwd': self
.__passwd
,
273 'ip': device
['ipAddress'],
277 'serialNumber': None,
278 'mlagPeerDevice': None}
279 self
.switches
[device
['hostname']] = switch_data
280 if len(self
.switches
) == 0:
281 self
.logger
.error("Unable to load Leaf switches from CVP")
284 # self.switches are switch objects, one for each switch in self.switches,
285 # used to make eAPI calls by using switch.py module
286 for s
in self
.switches
:
287 for device
in self
.allDeviceFacts
:
288 if device
['hostname'] == s
:
289 if not self
.switches
[s
].get('ip'):
290 self
.switches
[s
]['ip'] = device
['ipAddress']
291 self
.switches
[s
]['serialNumber'] = device
['serialNumber']
294 # Each switch has a different loopback address,
295 # so it's a different configLet
296 if not self
.switches
[s
].get('lo0'):
297 inf
= self
.__get
_interface
_ip
(self
.switches
[s
]['serialNumber'], self
.__LOOPBACK
_INTF
)
298 self
.switches
[s
]["lo0"] = inf
.split('/')[0]
299 if not self
.switches
[s
].get('AS'):
300 self
.switches
[s
]["AS"] = self
.__get
_device
_ASN
(self
.switches
[s
]['serialNumber'])
301 if self
.topology
in (self
._VXLAN
_MLAG
, self
._VLAN
_MLAG
):
302 for s
in self
.switches
:
303 if not self
.switches
[s
].get('mlagPeerDevice'):
304 self
.switches
[s
]['mlagPeerDevice'] = self
.__get
_peer
_MLAG
(self
.switches
[s
]['serialNumber'])
306 def __check_service(self
, service_type
, connection_points
,
307 check_vlan
=True, check_num_cp
=True, kwargs
=None):
308 """ Reviews the connection points elements looking for semantic errors in the incoming data
310 if service_type
not in self
.__supported
_service
_types
:
311 raise Exception("The service '{}' is not supported. Only '{}' are accepted".format(
313 self
.__supported
_service
_types
))
316 if len(connection_points
) < 2:
317 raise Exception(SdnError
.CONNECTION_POINTS_SIZE
)
318 if (len(connection_points
) != self
.__ELINE
_num
_connection
_points
and
319 service_type
== self
.__service
_types
_ELINE
):
320 raise Exception(SdnError
.CONNECTION_POINTS_SIZE
)
324 for cp
in connection_points
:
325 enc_type
= cp
.get(self
.__ENCAPSULATION
_TYPE
_PARAM
)
327 enc_type
not in self
.__supported
_encapsulation
_types
):
328 raise Exception(SdnError
.ENCAPSULATION_TYPE
)
329 encap_info
= cp
.get(self
.__ENCAPSULATION
_INFO
_PARAM
)
330 cp_vlan_id
= str(encap_info
.get(self
.__VLAN
_PARAM
))
334 elif vlan_id
!= cp_vlan_id
:
335 raise Exception(SdnError
.VLAN_INCONSISTENT
)
337 raise Exception(SdnError
.VLAN_NOT_PROVIDED
)
338 if vlan_id
in self
.__get
_srvVLANs
():
339 raise Exception('VLAN {} already assigned to a connectivity service'.format(vlan_id
))
341 # Commented out for as long as parameter isn't implemented
342 # bandwidth = kwargs.get(self.__BANDWIDTH_PARAM)
343 # if not isinstance(bandwidth, int):
344 # self.__exception(SdnError.BANDWIDTH, http_code=400)
346 # Commented out for as long as parameter isn't implemented
347 # backup = kwargs.get(self.__BACKUP_PARAM)
348 # if not isinstance(backup, bool):
349 # self.__exception(SdnError.BACKUP, http_code=400)
351 def check_credentials(self
):
352 """Retrieves the CloudVision version information, as the easiest way
353 for testing the access to CloudVision API
356 if self
.client
is None:
357 self
.client
= self
.__connect
()
358 result
= self
.client
.api
.get_cvp_info()
359 self
.logger
.debug(result
)
360 except CvpLoginError
as e
:
361 self
.logger
.info(str(e
))
363 raise SdnConnectorError(message
=SdnError
.UNAUTHORIZED
+ " " + str(e
),
364 http_code
=401) from e
365 except Exception as ex
:
367 self
.logger
.error(str(ex
))
368 raise SdnConnectorError(message
=SdnError
.INTERNAL_ERROR
+ " " + str(ex
),
369 http_code
=500) from ex
371 def get_connectivity_service_status(self
, service_uuid
, conn_info
=None):
372 """Monitor the status of the connectivity service established
374 service_uuid (str): UUID of the connectivity service
375 conn_info (dict or None): Information returned by the connector
376 during the service creation/edition and subsequently stored in
380 dict: JSON/YAML-serializable dict that contains a mandatory key
381 ``sdn_status`` associated with one of the following values::
383 {'sdn_status': 'ACTIVE'}
384 # The service is up and running.
386 {'sdn_status': 'INACTIVE'}
387 # The service was created, but the connector
388 # cannot determine yet if connectivity exists
389 # (ideally, the caller needs to wait and check again).
391 {'sdn_status': 'DOWN'}
392 # Connection was previously established,
393 # but an error/failure was detected.
395 {'sdn_status': 'ERROR'}
396 # An error occurred when trying to create the service/
397 # establish the connectivity.
399 {'sdn_status': 'BUILD'}
400 # Still trying to create the service, the caller
401 # needs to wait and check again.
403 Additionally ``error_msg``(**str**) and ``sdn_info``(**dict**)
404 keys can be used to provide additional status explanation or
405 new information available for the connectivity service.
408 self
.logger
.debug("invoked get_connectivity_service_status '{}'".format(service_uuid
))
410 raise SdnConnectorError(message
='No connection service UUID',
413 self
.__get
_Connection
()
414 if conn_info
is None:
415 raise SdnConnectorError(message
='No connection information for service UUID {}'.format(service_uuid
),
418 if 'configLetPerSwitch' in conn_info
.keys():
422 cls_perSw
= self
.__get
_serviceData
(service_uuid
,
423 conn_info
['service_type'],
424 conn_info
['vlan_id'],
427 t_isCancelled
= False
431 for s
in self
.switches
:
432 if len(cls_perSw
[s
]) > 0:
433 for cl
in cls_perSw
[s
]:
434 # Fix 1030 SDN-ARISTA Key error note when deploy a NS
435 # Added protection to check that 'note' exists and additionally
436 # verify that it is managed by OSM
437 if (not cls_perSw
[s
][0]['config'] or
438 not cl
.get('note') or
439 self
.__MANAGED
_BY
_OSM
not in cl
['note']):
442 t_id
= note
.split(self
.__SEPARATOR
)[1]
443 result
= self
.client
.api
.get_task_by_id(t_id
)
444 if result
['workOrderUserDefinedStatus'] == 'Completed':
446 elif result
['workOrderUserDefinedStatus'] == 'Cancelled':
448 elif result
['workOrderUserDefinedStatus'] == 'Failed':
452 failed_switches
.append(s
)
454 error_msg
= 'Some works were cancelled in switches: {}'.format(str(failed_switches
))
457 error_msg
= 'Some works failed in switches: {}'.format(str(failed_switches
))
460 error_msg
= 'Some works are still under execution in switches: {}'.format(str(failed_switches
))
464 sdn_status
= 'ACTIVE'
466 return {'sdn_status': sdn_status
,
467 'error_msg': error_msg
,
468 'sdn_info': sdn_info
}
469 except CvpLoginError
as e
:
470 self
.logger
.info(str(e
))
472 raise SdnConnectorError(message
=SdnError
.UNAUTHORIZED
+ " " + str(e
),
473 http_code
=401) from e
474 except Exception as ex
:
476 self
.logger
.error(str(ex
), exc_info
=True)
477 raise SdnConnectorError(message
=str(ex
) + " " + str(ex
),
478 http_code
=500) from ex
480 def create_connectivity_service(self
, service_type
, connection_points
,
482 """Stablish SDN/WAN connectivity between the endpoints
484 (str): ``ELINE`` (L2), ``ELAN`` (L2), ``ETREE`` (L2), ``L3``.
485 :param connection_points: (list): each point corresponds to
486 an entry point to be connected. For WIM: from the DC
487 to the transport network.
488 For SDN: Compute/PCI to the transport network. One
489 connection point serves to identify the specific access and
490 some other service parameters, such as encapsulation type.
491 Each item of the list is a dict with:
492 "service_endpoint_id": (str)(uuid) Same meaning that for
493 'service_endpoint_mapping' (see __init__)
494 In case the config attribute mapping_not_needed is True,
495 this value is not relevant. In this case
496 it will contain the string "device_id:device_interface_id"
497 "service_endpoint_encapsulation_type": None, "dot1q", ...
498 "service_endpoint_encapsulation_info": (dict) with:
499 "vlan": ..., (int, present if encapsulation is dot1q)
500 "vni": ... (int, present if encapsulation is vxlan),
501 "peers": [(ipv4_1), (ipv4_2)] (present if
502 encapsulation is vxlan)
504 "device_id": ..., same meaning that for
505 'service_endpoint_mapping' (see __init__)
506 "device_interface_id": same meaning that for
507 'service_endpoint_mapping' (see __init__)
508 "switch_dpid": ..., present if mapping has been found
509 for this device_id,device_interface_id
510 "switch_port": ... present if mapping has been found
511 for this device_id,device_interface_id
512 "service_mapping_info": present if mapping has
513 been found for this device_id,device_interface_id
514 :param kwargs: For future versions:
515 bandwidth (int): value in kilobytes
516 latency (int): value in milliseconds
517 Other QoS might be passed as keyword arguments.
518 :return: tuple: ``(service_id, conn_info)`` containing:
519 - *service_uuid* (str): UUID of the established
521 - *conn_info* (dict or None): Information to be
522 stored at the database (or ``None``).
523 This information will be provided to the
524 :meth:`~.edit_connectivity_service` and :obj:`~.delete`.
525 **MUST** be JSON/YAML-serializable (plain data structures).
526 :raises: SdnConnectorError: In case of error. Nothing should be
527 created in this case.
528 Provide the parameter http_code
531 self
.logger
.debug("invoked create_connectivity_service '{}' ports: {}".
532 format(service_type
, connection_points
))
533 self
.__get
_Connection
()
534 self
.__check
_service
(service_type
,
538 service_uuid
= str(uuid
.uuid4())
540 self
.logger
.info("Service with uuid {} created.".
541 format(service_uuid
))
542 s_uid
, s_connInf
= self
.__processConnection
(
548 self
.__addMetadata
(s_uid
, service_type
, s_connInf
['vlan_id'])
552 return (s_uid
, s_connInf
)
553 except CvpLoginError
as e
:
554 self
.logger
.info(str(e
))
556 raise SdnConnectorError(message
=SdnError
.UNAUTHORIZED
+ " " + str(e
),
557 http_code
=401) from e
558 except SdnConnectorError
as sde
:
560 except ValueError as err
:
562 self
.logger
.error(str(err
), exc_info
=True)
563 raise SdnConnectorError(message
=str(err
),
564 http_code
=500) from err
565 except Exception as ex
:
567 self
.logger
.error(str(ex
), exc_info
=True)
568 if self
.raiseException
:
570 raise SdnConnectorError(message
=str(ex
),
571 http_code
=500) from ex
573 def __processConnection(self
,
579 Invoked from creation and edit methods
581 Process the connection points array,
582 creating a set of configuration per switch where it has to be applied
583 for creating the configuration, the switches have to be queried for obtaining:
584 - the loopback address
585 - the BGP ASN (autonomous system number)
586 - the interface name of the MAC address to add in the connectivity service
587 Once the new configuration is ready, the __updateConnection method is invoked for appling the changes
593 for s
in self
.switches
:
596 vlan_processed
= False
599 processed_connection_points
= []
600 for cp
in connection_points
:
602 encap_info
= cp
.get(self
.__ENCAPSULATION
_INFO
_PARAM
)
603 if not vlan_processed
:
604 vlan_id
= str(encap_info
.get(self
.__VLAN
_PARAM
))
607 vni_id
= encap_info
.get(self
.__VNI
_PARAM
)
609 vni_id
= str(10000 + int(vlan_id
))
611 if service_type
== self
.__service
_types
_ELAN
:
612 cl_vlan
= self
.clC
.getElan_vlan(service_uuid
,
616 cl_vlan
= self
.clC
.getEline_vlan(service_uuid
,
619 vlan_processed
= True
621 encap_type
= cp
.get(self
.__ENCAPSULATION
_TYPE
_PARAM
)
622 switch_id
= encap_info
.get(self
.__SW
_ID
_PARAM
)
623 interface
= encap_info
.get(self
.__SW
_PORT
_PARAM
)
624 switches
= [{'name': switch_id
, 'interface': interface
}]
626 # remove those connections that are equal. This happens when several sriovs are located in the same
627 # compute node interface, that is, in the same switch and interface
628 switches
= [x
for x
in switches
if x
not in processed_connection_points
]
631 processed_connection_points
+= switches
632 for switch
in switches
:
634 raise SdnConnectorError(
635 message
="Connection point switch port empty for switch_dpid {}".format(switch_id
),
637 # it should be only one switch where the mac is attached
638 if encap_type
== 'dot1q':
639 # SRIOV configLet for Leaf switch mac's attached to
640 if service_type
== self
.__service
_types
_ELAN
:
641 cl_encap
= self
.clC
.getElan_sriov(service_uuid
, interface
, vlan_id
, i
)
643 cl_encap
= self
.clC
.getEline_sriov(service_uuid
, interface
, vlan_id
, i
)
645 # PT configLet for Leaf switch attached to the mac
646 if service_type
== self
.__service
_types
_ELAN
:
647 cl_encap
= self
.clC
.getElan_passthrough(service_uuid
,
651 cl_encap
= self
.clC
.getEline_passthrough(service_uuid
,
654 if cls_cp
.get(switch
['name']):
655 cls_cp
[switch
['name']] = str(cls_cp
[switch
['name']]) + cl_encap
657 cls_cp
[switch
['name']] = cl_encap
659 # at least 1 connection point has to be received
660 if not vlan_processed
:
661 raise SdnConnectorError(message
=SdnError
.UNSUPPORTED_FEATURE
,
664 for s
in self
.switches
:
665 # for cl in cp_configLets:
666 cl_name
= (self
.__OSM
_PREFIX
+
668 self
.__SEPARATOR
+ service_type
+ str(vlan_id
) +
669 self
.__SEPARATOR
+ service_uuid
)
671 # Apply BGP configuration only for VXLAN topologies
672 if self
.topology
in (self
._VXLAN
_MLAG
, self
._VXLAN
):
673 if service_type
== self
.__service
_types
_ELAN
:
674 cl_bgp
[s
] = self
.clC
.getElan_bgp(service_uuid
,
677 self
.switches
[s
]['lo0'],
678 self
.switches
[s
]['AS'])
680 cl_bgp
[s
] = self
.clC
.getEline_bgp(service_uuid
,
683 self
.switches
[s
]['lo0'],
684 self
.switches
[s
]['AS'])
688 if not cls_cp
.get(s
):
689 # Apply VLAN configuration to peer MLAG switch,
690 # only necessary when there are no connection points in the switch
691 if self
.topology
in (self
._VXLAN
_MLAG
, self
._VLAN
_MLAG
):
692 for p
in self
.switches
:
693 if self
.switches
[p
]['mlagPeerDevice'] == s
:
695 cl_config
= str(cl_vlan
)
697 cl_config
= str(cl_vlan
) + str(cl_bgp
[s
]) + str(cls_cp
[s
])
699 cls_perSw
[s
] = [{'name': cl_name
, 'config': cl_config
}]
701 allLeafConfigured
, allLeafModified
= self
.__updateConnection
(cls_perSw
)
704 "uuid": service_uuid
,
706 "service_type": service_type
,
708 "connection_points": connection_points
,
709 "configLetPerSwitch": cls_perSw
,
710 'allLeafConfigured': allLeafConfigured
,
711 'allLeafModified': allLeafModified
}
713 return service_uuid
, conn_info
714 except Exception as ex
:
715 self
.logger
.debug("Exception processing connection {}: {}".
716 format(service_uuid
, str(ex
)))
719 def __updateConnection(self
, cls_perSw
):
720 """ Invoked in the creation and modification
722 checks if the new connection points config is:
723 - already in the Cloud Vision, the configLet is modified, and applied to the switch,
724 executing the corresponding task
725 - if it has to be removed:
726 then configuration has to be removed from the switch executing the corresponding task,
727 before trying to remove the configuration
728 - created, the configuration set is created, associated to the switch, and the associated
729 task to the configLet modification executed
730 In case of any error, rollback is executed, removing the created elements, and restoring to the
734 allLeafConfigured
= {}
737 for s
in self
.switches
:
738 allLeafConfigured
[s
] = False
739 allLeafModified
[s
] = False
741 for s
in self
.switches
:
742 toDelete_in_cvp
= False
743 if not (cls_perSw
.get(s
) and cls_perSw
[s
][0].get('config')):
744 # when there is no configuration, means that there is no interface
745 # in the switch to be connected, so the configLet has to be removed from CloudVision
746 # after removing the ConfigLet from the switch if it was already there
748 # get config let name and key
751 cvp_cl
= self
.client
.api
.get_configlet_by_name(cl
[0]['name'])
753 cl_toDelete
.append(cvp_cl
)
755 toDelete_in_cvp
= True
756 except CvpApiError
as error
:
757 if "Entity does not exist" in error
.msg
:
761 # remove configLet from device
763 res
= self
.__configlet
_modify
(cls_perSw
[s
])
764 allLeafConfigured
[s
] = res
[0]
765 if not allLeafConfigured
[s
]:
768 res
= self
.__device
_modify
(
771 delete
=toDelete_in_cvp
)
772 if "errorMessage" in str(res
):
773 raise Exception(str(res
))
774 self
.logger
.info("Device {} modify result {}".format(s
, res
))
775 for t_id
in res
[1]['tasks']:
776 if not toDelete_in_cvp
:
777 note_msg
= "{}{}{}{}##".format(self
.__MANAGED
_BY
_OSM
,
781 self
.client
.api
.add_note_to_configlet(
782 cls_perSw
[s
][0]['key'],
784 cls_perSw
[s
][0]['note'] = note_msg
785 tasks
= {t_id
: {'workOrderId': t_id
}}
786 self
.__exec
_task
(tasks
, self
.__EXC
_TASK
_EXEC
_WAIT
)
787 # with just one configLet assigned to a device,
788 # delete all if there are errors in next loops
789 if not toDelete_in_cvp
:
790 allLeafModified
[s
] = True
791 if len(cl_toDelete
) > 0:
792 self
.__configlet
_modify
(cl_toDelete
, delete
=True)
794 return allLeafConfigured
, allLeafModified
795 except Exception as ex
:
797 self
.__rollbackConnection
(cls_perSw
,
800 except Exception as e
:
801 self
.logger
.error("Exception rolling back in updating connection: {}".
802 format(e
), exc_info
=True)
805 def __rollbackConnection(self
,
809 """ Removes the given configLet from the devices and then remove the configLets
811 for s
in self
.switches
:
812 if allLeafModified
[s
]:
814 res
= self
.__device
_modify
(
816 new_configlets
=cls_perSw
[s
],
818 if "errorMessage" in str(res
):
819 raise Exception(str(res
))
821 for t_id
in res
[1]['tasks']:
822 tasks
[t_id
] = {'workOrderId': t_id
}
823 self
.__exec
_task
(tasks
)
824 self
.logger
.info("Device {} modify result {}".format(s
, res
))
825 except Exception as e
:
826 self
.logger
.error('Error removing configlets from device {}: {}'.format(s
, e
))
828 for s
in self
.switches
:
829 if allLeafConfigured
[s
]:
830 self
.__configlet
_modify
(cls_perSw
[s
], delete
=True)
832 def __exec_task(self
, tasks
, tout
=10):
833 if self
.taskC
is None:
835 data
= self
.taskC
.update_all_tasks(tasks
).values()
836 self
.taskC
.task_action(data
, tout
, 'executed')
838 def __device_modify(self
, device_to_update
, new_configlets
, delete
):
839 """ Updates the devices (switches) adding or removing the configLet,
840 the tasks Id's associated to the change are returned
842 self
.logger
.info('Enter in __device_modify delete: {}'.format(delete
))
845 # Task Ids that have been identified during device actions
848 if (len(new_configlets
) == 0 or
849 device_to_update
is None or
850 len(device_to_update
) == 0):
851 data
= {'updated': updated
, 'tasks': newTasks
}
852 return [changed
, data
]
854 self
.__load
_inventory
()
856 allDeviceFacts
= self
.allDeviceFacts
857 # Work through Devices list adding device specific information
859 for try_device
in allDeviceFacts
:
860 # Add Device Specific Configlets
861 # self.logger.debug(device)
862 if try_device
['hostname'] not in device_to_update
:
864 dev_cvp_configlets
= self
.client
.api
.get_configlets_by_device_id(
865 try_device
['systemMacAddress'])
866 # self.logger.debug(dev_cvp_configlets)
867 try_device
['deviceSpecificConfiglets'] = []
868 for cvp_configlet
in dev_cvp_configlets
:
869 if int(cvp_configlet
['containerCount']) == 0:
870 try_device
['deviceSpecificConfiglets'].append(
871 {'name': cvp_configlet
['name'],
872 'key': cvp_configlet
['key']})
873 # self.logger.debug(device)
877 # Check assigned configlets
878 device_update
= False
880 remove_configlets
= []
884 for cvp_configlet
in device
['deviceSpecificConfiglets']:
885 for cl
in new_configlets
:
886 if cvp_configlet
['name'] == cl
['name']:
887 remove_configlets
.append(cvp_configlet
)
890 for configlet
in new_configlets
:
891 if configlet
not in device
['deviceSpecificConfiglets']:
892 add_configlets
.append(configlet
)
895 update_devices
.append({'hostname': device
['hostname'],
896 'configlets': [add_configlets
,
899 self
.logger
.info("Device to modify: {}".format(update_devices
))
901 up_device
= update_devices
[0]
902 cl_toAdd
= up_device
['configlets'][0]
903 cl_toDel
= up_device
['configlets'][1]
906 if delete
and len(cl_toDel
) > 0:
907 r
= self
.client
.api
.remove_configlets_from_device(
913 self
.logger
.debug("remove_configlets_from_device {} {}".format(dev_action
, cl_toDel
))
914 elif len(cl_toAdd
) > 0:
915 r
= self
.client
.api
.apply_configlets_to_device(
921 self
.logger
.debug("apply_configlets_to_device {} {}".format(dev_action
, cl_toAdd
))
923 except Exception as error
:
924 errorMessage
= str(error
)
925 msg
= "errorMessage: Device {} Configlets couldnot be updated: {}".format(
926 up_device
['hostname'], errorMessage
)
927 raise SdnConnectorError(msg
) from error
929 if "errorMessage" in str(dev_action
):
930 m
= "Device {} Configlets update fail: {}".format(
931 up_device
['name'], dev_action
['errorMessage'])
932 raise SdnConnectorError(m
)
935 if 'taskIds' in str(dev_action
):
936 # Fix 1030 SDN-ARISTA Key error note when deploy a NS
937 if not dev_action
['data']['taskIds']:
938 raise SdnConnectorError("No taskIds found: Device {} Configlets could not be updated".format(
939 up_device
['hostname']))
940 for taskId
in dev_action
['data']['taskIds']:
942 up_device
['hostname']: "Configlets-{}".format(taskId
)})
943 newTasks
.append(taskId
)
945 updated
.append({up_device
['hostname']:
946 "Configlets-No_Specific_Tasks"})
947 data
= {'updated': updated
, 'tasks': newTasks
}
948 return [changed
, data
]
950 def __configlet_modify(self
, configletsToApply
, delete
=False):
951 ''' adds/update or delete the provided configLets
952 :param configletsToApply: list of configLets to apply
953 :param delete: flag to indicate if the configLets have to be deleted
954 from Cloud Vision Portal
955 :return: data: dict of module actions and taskIDs
957 self
.logger
.info('Enter in __configlet_modify delete:{}'.format(
960 # Compare configlets against cvp_facts-configlets
967 for cl
in configletsToApply
:
974 cvp_cl
= self
.client
.api
.get_configlet_by_name(cl
['name'])
975 cl
['key'] = cvp_cl
['key']
976 cl
['note'] = cvp_cl
['note']
978 except CvpApiError
as error
:
979 if "Entity does not exist" in error
.msg
:
987 configlet
= {'name': cvp_cl
['name'],
991 cl_compare
= self
.__compare
(cl
['config'],
993 # compare function returns a floating point number
994 if cl_compare
[0] != 100.0:
996 configlet
= {'name': cl
['name'],
998 'config': cl
['config']}
1001 configlet
= {'name': cl
['name'],
1002 'key': cvp_cl
['key'],
1004 'config': cl
['config']}
1007 configlet
= {'name': cl
['name'],
1008 'config': cl
['config']}
1011 operation
= 'delete'
1012 resp
= self
.client
.api
.delete_configlet(
1013 configlet
['data']['name'],
1014 configlet
['data']['key'])
1016 operation
= 'update'
1017 resp
= self
.client
.api
.update_configlet(
1018 configlet
['config'],
1019 configlet
['data']['key'],
1020 configlet
['data']['name'],
1023 operation
= 'create'
1024 resp
= self
.client
.api
.add_configlet(
1026 configlet
['config'])
1028 operation
= 'checked'
1030 except Exception as error
:
1031 errorMessage
= str(error
).split(':')[-1]
1032 message
= "Configlet {} cannot be {}: {}".format(
1033 cl
['name'], operation
, errorMessage
)
1035 deleted
.append({configlet
['name']: message
})
1037 updated
.append({configlet
['name']: message
})
1039 new
.append({configlet
['name']: message
})
1041 checked
.append({configlet
['name']: message
})
1044 if "error" in str(resp
).lower():
1045 message
= "Configlet {} cannot be deleted: {}".format(
1046 cl
['name'], resp
['errorMessage'])
1048 deleted
.append({configlet
['name']: message
})
1050 updated
.append({configlet
['name']: message
})
1052 new
.append({configlet
['name']: message
})
1054 checked
.append({configlet
['name']: message
})
1058 deleted
.append({configlet
['name']: "success"})
1061 updated
.append({configlet
['name']: "success"})
1064 cl
['key'] = resp
# This key is used in API call deviceApplyConfigLet FGA
1065 new
.append({configlet
['name']: "success"})
1068 checked
.append({configlet
['name']: "success"})
1070 data
= {'new': new
, 'updated': updated
, 'deleted': deleted
, 'checked': checked
}
1071 return [changed
, data
]
1073 def __get_configletsDevices(self
, configlets
):
1074 for s
in self
.switches
:
1075 configlet
= configlets
[s
]
1076 # Add applied Devices
1077 if len(configlet
) > 0:
1078 configlet
['devices'] = []
1079 applied_devices
= self
.client
.api
.get_applied_devices(
1081 for device
in applied_devices
['data']:
1082 configlet
['devices'].append(device
['hostName'])
1084 def __get_serviceData(self
, service_uuid
, service_type
, vlan_id
, conn_info
=None):
1086 for s
in self
.switches
:
1089 srv_cls
= self
.__get
_serviceConfigLets
(service_uuid
,
1092 self
.__get
_configletsDevices
(srv_cls
)
1093 for s
in self
.switches
:
1096 for dev
in cl
['devices']:
1097 cls_perSw
[dev
].append(cl
)
1099 cls_perSw
= conn_info
['configLetPerSwitch']
1102 def delete_connectivity_service(self
, service_uuid
, conn_info
=None):
1104 Disconnect multi-site endpoints previously connected
1106 :param service_uuid: The one returned by create_connectivity_service
1107 :param conn_info: The one returned by last call to 'create_connectivity_service' or 'edit_connectivity_service'
1108 if they do not return None
1110 :raises: SdnConnectorException: In case of error. The parameter http_code must be filled
1113 self
.logger
.debug('invoked delete_connectivity_service {}'.
1114 format(service_uuid
))
1115 if not service_uuid
:
1116 raise SdnConnectorError(message
='No connection service UUID',
1119 self
.__get
_Connection
()
1120 if conn_info
is None:
1121 raise SdnConnectorError(message
='No connection information for service UUID {}'.format(service_uuid
),
1124 cls_perSw
= self
.__get
_serviceData
(service_uuid
,
1125 conn_info
['service_type'],
1126 conn_info
['vlan_id'],
1128 allLeafConfigured
= {}
1129 allLeafModified
= {}
1130 for s
in self
.switches
:
1131 allLeafConfigured
[s
] = True
1132 allLeafModified
[s
] = True
1133 found_in_cvp
= False
1134 for s
in self
.switches
:
1138 self
.__rollbackConnection
(cls_perSw
,
1142 # if the service is not defined in Cloud Vision, return a 404 - NotFound error
1143 raise SdnConnectorError(message
='Service {} was not found in Arista Cloud Vision {}'.
1144 format(service_uuid
, self
.__wim
_url
),
1146 self
.__removeMetadata
(service_uuid
)
1147 except CvpLoginError
as e
:
1148 self
.logger
.info(str(e
))
1150 raise SdnConnectorError(message
=SdnError
.UNAUTHORIZED
+ " " + str(e
),
1151 http_code
=401) from e
1152 except SdnConnectorError
as sde
:
1154 except Exception as ex
:
1156 self
.logger
.error(ex
)
1157 if self
.raiseException
:
1159 raise SdnConnectorError(message
=SdnError
.INTERNAL_ERROR
+ " " + str(ex
),
1160 http_code
=500) from ex
1162 def __addMetadata(self
, service_uuid
, service_type
, vlan_id
):
1163 """ Adds the connectivity service from 'OSM_metadata' configLet
1165 found_in_cvp
= False
1167 cvp_cl
= self
.client
.api
.get_configlet_by_name(self
.__OSM
_METADATA
)
1169 except CvpApiError
as error
:
1170 if "Entity does not exist" in error
.msg
:
1175 new_serv
= '{} {} {} {}\n'.format(self
.__METADATA
_PREFIX
, service_type
, vlan_id
, service_uuid
)
1178 cl_config
= cvp_cl
['config'] + new_serv
1180 cl_config
= new_serv
1181 cl_meta
= [{'name': self
.__OSM
_METADATA
, 'config': cl_config
}]
1182 self
.__configlet
_modify
(cl_meta
)
1183 except Exception as e
:
1184 self
.logger
.error('Error in setting metadata in CloudVision from OSM for service {}: {}'.
1185 format(service_uuid
, str(e
)))
1188 def __removeMetadata(self
, service_uuid
):
1189 """ Removes the connectivity service from 'OSM_metadata' configLet
1191 found_in_cvp
= False
1193 cvp_cl
= self
.client
.api
.get_configlet_by_name(self
.__OSM
_METADATA
)
1195 except CvpApiError
as error
:
1196 if "Entity does not exist" in error
.msg
:
1202 if service_uuid
in cvp_cl
['config']:
1204 for line
in cvp_cl
['config'].split('\n'):
1205 if service_uuid
in line
:
1208 cl_config
= cl_config
+ line
1209 cl_meta
= [{'name': self
.__OSM
_METADATA
, 'config': cl_config
}]
1210 self
.__configlet
_modify
(cl_meta
)
1211 except Exception as e
:
1212 self
.logger
.error('Error in removing metadata in CloudVision from OSM for service {}: {}'.
1213 format(service_uuid
, str(e
)))
1216 def edit_connectivity_service(self
,
1219 connection_points
=None,
1221 """ Change an existing connectivity service.
1223 This method's arguments and return value follow the same convention as
1224 :meth:`~.create_connectivity_service`.
1226 :param service_uuid: UUID of the connectivity service.
1227 :param conn_info: (dict or None): Information previously returned
1228 by last call to create_connectivity_service
1229 or edit_connectivity_service
1230 :param connection_points: (list): If provided, the old list of
1231 connection points will be replaced.
1232 :param kwargs: Same meaning that create_connectivity_service
1233 :return: dict or None: Information to be updated and stored at
1235 When ``None`` is returned, no information should be changed.
1236 When an empty dict is returned, the database record will
1238 **MUST** be JSON/YAML-serializable (plain data structures).
1240 SdnConnectorError: In case of error.
1243 self
.logger
.debug('invoked edit_connectivity_service for service {}. ports: {}'.format(service_uuid
,
1246 if not service_uuid
:
1247 raise SdnConnectorError(message
='Unable to perform operation, missing or empty uuid',
1250 raise SdnConnectorError(message
='Unable to perform operation, missing or empty connection information',
1253 if connection_points
is None:
1256 self
.__get
_Connection
()
1258 cls_currentPerSw
= conn_info
['configLetPerSwitch']
1259 service_type
= conn_info
['service_type']
1261 self
.__check
_service
(service_type
,
1267 s_uid
, s_connInf
= self
.__processConnection
(
1272 self
.logger
.info("Service with uuid {} configuration updated".
1275 except CvpLoginError
as e
:
1276 self
.logger
.info(str(e
))
1278 raise SdnConnectorError(message
=SdnError
.UNAUTHORIZED
+ " " + str(e
),
1279 http_code
=401) from e
1280 except SdnConnectorError
as sde
:
1282 except Exception as ex
:
1285 # TODO check if there are pending task, and cancel them before restoring
1286 self
.__updateConnection
(cls_currentPerSw
)
1287 except Exception as e
:
1288 self
.logger
.error("Unable to restore configuration in service {} after an error in the configuration"
1289 " updated: {}".format(service_uuid
, str(e
)))
1290 if self
.raiseException
:
1292 raise SdnConnectorError(message
=str(ex
),
1293 http_code
=500) from ex
1295 def clear_all_connectivity_services(self
):
1296 """ Removes all connectivity services from Arista CloudVision with two steps:
1297 - retrives all the services from Arista CloudVision
1298 - removes each service
1301 self
.logger
.debug('invoked AristaImpl ' +
1302 'clear_all_connectivity_services')
1303 self
.__get
_Connection
()
1304 s_list
= self
.__get
_srvUUIDs
()
1307 conn_info
['service_type'] = serv
['type']
1308 conn_info
['vlan_id'] = serv
['vlan']
1310 self
.delete_connectivity_service(serv
['uuid'], conn_info
)
1311 except CvpLoginError
as e
:
1312 self
.logger
.info(str(e
))
1314 raise SdnConnectorError(message
=SdnError
.UNAUTHORIZED
+ " " + str(e
),
1315 http_code
=401) from e
1316 except SdnConnectorError
as sde
:
1318 except Exception as ex
:
1320 self
.logger
.error(ex
)
1321 if self
.raiseException
:
1323 raise SdnConnectorError(message
=SdnError
.INTERNAL_ERROR
+ " " + str(ex
),
1324 http_code
=500) from ex
1326 def get_all_active_connectivity_services(self
):
1327 """ Return the uuid of all the active connectivity services with two steps:
1328 - retrives all the services from Arista CloudVision
1329 - retrives the status of each server
1332 self
.logger
.debug('invoked AristaImpl {}'.format(
1333 'get_all_active_connectivity_services'))
1334 self
.__get
_Connection
()
1335 s_list
= self
.__get
_srvUUIDs
()
1339 conn_info
['service_type'] = serv
['type']
1340 conn_info
['vlan_id'] = serv
['vlan']
1342 status
= self
.get_connectivity_service_status(serv
['uuid'], conn_info
)
1343 if status
['sdn_status'] == 'ACTIVE':
1344 result
.append(serv
['uuid'])
1346 except CvpLoginError
as e
:
1347 self
.logger
.info(str(e
))
1349 raise SdnConnectorError(message
=SdnError
.UNAUTHORIZED
+ " " + str(e
),
1350 http_code
=401) from e
1351 except SdnConnectorError
as sde
:
1353 except Exception as ex
:
1355 self
.logger
.error(ex
)
1356 if self
.raiseException
:
1358 raise SdnConnectorError(message
=SdnError
.INTERNAL_ERROR
,
1359 http_code
=500) from ex
1361 def __get_serviceConfigLets(self
, service_uuid
, service_type
, vlan_id
):
1362 """ Return the configLet's associated with a connectivity service,
1363 There should be one, as maximum, per device (switch) for a given
1364 connectivity service
1367 for s
in self
.switches
:
1369 found_in_cvp
= False
1370 name
= (self
.__OSM
_PREFIX
+
1372 self
.__SEPARATOR
+ service_type
+ str(vlan_id
) +
1373 self
.__SEPARATOR
+ service_uuid
)
1375 cvp_cl
= self
.client
.api
.get_configlet_by_name(name
)
1377 except CvpApiError
as error
:
1378 if "Entity does not exist" in error
.msg
:
1386 def __get_srvVLANs(self
):
1387 """ Returns a list with all the VLAN id's used in the connectivity services managed
1388 in tha Arista CloudVision by checking the 'OSM_metadata' configLet where this
1389 information is stored
1391 found_in_cvp
= False
1393 cvp_cl
= self
.client
.api
.get_configlet_by_name(self
.__OSM
_METADATA
)
1395 except CvpApiError
as error
:
1396 if "Entity does not exist" in error
.msg
:
1402 lines
= cvp_cl
['config'].split('\n')
1404 if self
.__METADATA
_PREFIX
in line
:
1405 s_vlan
= line
.split(' ')[3]
1408 if (s_vlan
is not None and
1410 s_vlan
not in s_vlan_list
):
1411 s_vlan_list
.append(s_vlan
)
1415 def __get_srvUUIDs(self
):
1416 """ Retrieves all the connectivity services, managed in tha Arista CloudVision
1417 by checking the 'OSM_metadata' configLet where this information is stored
1419 found_in_cvp
= False
1421 cvp_cl
= self
.client
.api
.get_configlet_by_name(self
.__OSM
_METADATA
)
1423 except CvpApiError
as error
:
1424 if "Entity does not exist" in error
.msg
:
1430 lines
= cvp_cl
['config'].split('\n')
1432 if self
.__METADATA
_PREFIX
in line
:
1433 line
= line
.split(' ')
1434 serv
= {'uuid': line
[4], 'type': line
[2], 'vlan': line
[3]}
1437 if (serv
is not None and
1439 serv
not in serv_list
):
1440 serv_list
.append(serv
)
1444 def __get_Connection(self
):
1445 """ Open a connection with Arista CloudVision,
1446 invoking the version retrival as test
1449 if self
.client
is None:
1450 self
.client
= self
.__connect
()
1451 self
.client
.api
.get_cvp_info()
1452 except (CvpSessionLogOutError
, RequestException
) as e
:
1453 self
.logger
.debug("Connection error '{}'. Reconnecting".format(e
))
1454 self
.client
= self
.__connect
()
1455 self
.client
.api
.get_cvp_info()
1457 def __connect(self
):
1458 ''' Connects to CVP device using user provided credentials from initialization.
1459 :return: CvpClient object with connection instantiated.
1461 client
= CvpClient()
1462 protocol
, _
, rest_url
= self
.__wim
_url
.rpartition("://")
1463 host
, _
, port
= rest_url
.partition(":")
1464 if port
and port
.endswith("/"):
1465 port
= int(port
[:-1])
1471 client
.connect([host
],
1474 protocol
=protocol
or "https",
1477 client
.api
= CvpApi(client
, request_timeout
=self
.__API
_REQUEST
_TOUT
)
1478 self
.taskC
= AristaCVPTask(client
.api
)
1481 def __compare(self
, fromText
, toText
, lines
=10):
1482 """ Compare text string in 'fromText' with 'toText' and produce
1483 diffRatio - a score as a float in the range [0, 1] 2.0*M / T
1484 T is the total number of elements in both sequences,
1485 M is the number of matches.
1486 Score - 1.0 if the sequences are identical, and
1487 0.0 if they have nothing in common.
1490 '- ' line unique to sequence 1
1491 '+ ' line unique to sequence 2
1492 ' ' line common to both sequences
1493 '? ' line not present in either input sequence
1495 fromlines
= fromText
.splitlines(1)
1496 tolines
= toText
.splitlines(1)
1497 diff
= list(difflib
.unified_diff(fromlines
, tolines
, n
=lines
))
1498 textComp
= difflib
.SequenceMatcher(None, fromText
, toText
)
1499 diffRatio
= round(textComp
.quick_ratio()*100, 2)
1500 return [diffRatio
, diff
]
1502 def __load_inventory(self
):
1503 """ Get Inventory Data for All Devices (aka switches) from the Arista CloudVision
1505 if not self
.cvp_inventory
:
1506 self
.cvp_inventory
= self
.client
.api
.get_inventory()
1507 self
.allDeviceFacts
= []
1508 for device
in self
.cvp_inventory
:
1509 self
.allDeviceFacts
.append(device
)
1511 def __get_tags(self
, name
, value
):
1512 if not self
.cvp_tags
:
1514 url
= '/api/v1/rest/analytics/tags/labels/devices/{}/value/{}/elements'.format(name
, value
)
1515 self
.logger
.debug('get_tags: URL {}'.format(url
))
1516 data
= self
.client
.get(url
, timeout
=self
.__API
_REQUEST
_TOUT
)
1517 for dev
in data
['notifications']:
1518 for elem
in dev
['updates']:
1519 self
.cvp_tags
.append(elem
)
1520 self
.logger
.debug('Available devices with tag_name {} - value {}: {} '.format(name
, value
, self
.cvp_tags
))
1522 def __get_interface_ip(self
, device_id
, interface
):
1523 url
= '/api/v1/rest/{}/Sysdb/ip/config/ipIntfConfig/{}/'.format(device_id
, interface
)
1524 self
.logger
.debug('get_interface_ip: URL {}'.format(url
))
1526 data
= self
.client
.get(url
, timeout
=self
.__API
_REQUEST
_TOUT
)
1527 return data
['notifications'][0]['updates']['addrWithMask']['value'].split('/')[0]
1529 raise SdnConnectorError("Invalid response from url {}: data {}".format(url
, data
))
1531 def __get_device_ASN(self
, device_id
):
1532 url
= '/api/v1/rest/{}/Sysdb/routing/bgp/config/'.format(device_id
)
1533 self
.logger
.debug('get_device_ASN: URL {}'.format(url
))
1535 data
= self
.client
.get(url
, timeout
=self
.__API
_REQUEST
_TOUT
)
1536 return data
['notifications'][0]['updates']['asNumber']['value']['value']['int']
1538 raise SdnConnectorError("Invalid response from url {}: data {}".format(url
, data
))
1540 def __get_peer_MLAG(self
, device_id
):
1542 url
= '/api/v1/rest/{}/Sysdb/mlag/status/'.format(device_id
)
1543 self
.logger
.debug('get_MLAG_status: URL {}'.format(url
))
1545 data
= self
.client
.get(url
, timeout
=self
.__API
_REQUEST
_TOUT
)
1546 if data
['notifications']:
1548 for notification
in data
['notifications']:
1549 for update
in notification
['updates']:
1550 if update
== 'systemId':
1551 mlagSystemId
= notification
['updates'][update
]['value']
1556 # search the MLAG System Id
1558 for s
in self
.switches
:
1559 if self
.switches
[s
]['serialNumber'] == device_id
:
1561 url
= '/api/v1/rest/{}/Sysdb/mlag/status/'.format(self
.switches
[s
]['serialNumber'])
1562 self
.logger
.debug('Searching for MLAG system id {} in switch {}'.format(mlagSystemId
, s
))
1563 data
= self
.client
.get(url
, timeout
=self
.__API
_REQUEST
_TOUT
)
1565 for notification
in data
['notifications']:
1566 for update
in notification
['updates']:
1567 if update
== 'systemId':
1568 if mlagSystemId
== notification
['updates'][update
]['value']:
1577 self
.logger
.error('No Peer device found for device {} with MLAG address {}'.format(device_id
,
1580 self
.logger
.debug('Peer MLAG for device {} - value {}'.format(device_id
, peer
))
1583 raise SdnConnectorError("Invalid response from url {}: data {}".format(url
, data
))
1585 def is_valid_destination(self
, url
):
1586 """ Check that the provided WIM URL is correct
1588 if re
.match(self
.__regex
, url
):
1590 elif self
.is_valid_ipv4_address(url
):
1593 return self
.is_valid_ipv6_address(url
)
1595 def is_valid_ipv4_address(self
, address
):
1596 """ Checks that the given IP is IPv4 valid
1599 socket
.inet_pton(socket
.AF_INET
, address
)
1600 except AttributeError: # no inet_pton here, sorry
1602 socket
.inet_aton(address
)
1603 except socket
.error
:
1605 return address
.count('.') == 3
1606 except socket
.error
: # not a valid address
1610 def is_valid_ipv6_address(self
, address
):
1611 """ Checks that the given IP is IPv6 valid
1614 socket
.inet_pton(socket
.AF_INET6
, address
)
1615 except socket
.error
: # not a valid address
1619 def delete_keys_from_dict(self
, dict_del
, lst_keys
):
1620 if dict_del
is None:
1622 dict_copy
= {k
: v
for k
, v
in dict_del
.items() if k
not in lst_keys
}
1623 for k
, v
in dict_copy
.items():
1624 if isinstance(v
, dict):
1625 dict_copy
[k
] = self
.delete_keys_from_dict(v
, lst_keys
)