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
.wim
.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
43 from cvprac
.cvp_client
import CvpClient
44 from cvprac
.cvp_api
import CvpApi
45 from cvprac
.cvp_client_errors
import CvpLoginError
, CvpSessionLogOutError
, CvpApiError
46 from cvprac
import __version__
as cvprac_version
48 from osm_rosdn_arista
.aristaConfigLet
import AristaSDNConfigLet
49 from osm_rosdn_arista
.aristaTask
import AristaCVPTask
53 UNREACHABLE
= 'Unable to reach the WIM.',
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"
139 def __init__(self
, wim
, wim_account
, config
=None, logger
=None):
142 :param wim: (dict). Contains among others 'wim_url'
143 :param wim_account: (dict). Contains among others 'uuid' (internal id), 'name',
144 'sdn' (True if is intended for SDN-assist or False if intended for WIM), 'user', 'password'.
145 :param config: (dict or None): Particular information of plugin. These keys if present have a common meaning:
146 'mapping_not_needed': (bool) False by default or if missing, indicates that mapping is not needed.
147 'service_endpoint_mapping': (list) provides the internal endpoint mapping. The meaning is:
148 KEY meaning for WIM meaning for SDN assist
149 -------- -------- --------
150 device_id pop_switch_dpid compute_id
151 device_interface_id pop_switch_port compute_pci_address
152 service_endpoint_id wan_service_endpoint_id SDN_service_endpoint_id
153 service_mapping_info wan_service_mapping_info SDN_service_mapping_info
154 contains extra information if needed. Text in Yaml format
155 switch_dpid wan_switch_dpid SDN_switch_dpid
156 switch_port wan_switch_port SDN_switch_port
157 datacenter_id vim_account vim_account
158 id: (internal, do not use)
159 wim_id: (internal, do not use)
160 :param logger (logging.Logger): optional logger object. If none is passed 'openmano.sdn.sdnconn' is used.
162 self
.__regex
= re
.compile(
163 r
'^(?:http|ftp)s?://' # http:// or https://
164 r
'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # domain...
165 r
'localhost|' # localhost...
166 r
'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip
167 r
'(?::\d+)?', re
.IGNORECASE
) # optional port
168 self
.raiseException
= True
169 self
.logger
= logger
or logging
.getLogger(self
.__WIM
_LOGGER
)
170 super().__init
__(wim
, wim_account
, config
, self
.logger
)
172 self
.__wim
_account
= wim_account
173 self
.__config
= config
174 if self
.is_valid_destination(self
.__wim
.get("wim_url")):
175 self
.__wim
_url
= self
.__wim
.get("wim_url")
177 raise SdnConnectorError(message
='Invalid wim_url value',
179 self
.__user
= wim_account
.get("user")
180 self
.__passwd
= wim_account
.get("password")
182 self
.cvp_inventory
= None
184 self
.logger
.debug("Arista SDN plugin {}, cvprac version {}, user:{} and config:{}".
185 format(wim
, cvprac_version
, self
.__user
,
186 self
.delete_keys_from_dict(config
, ('passwd',))))
187 self
.allDeviceFacts
= []
190 self
.__load
_topology
()
191 self
.__load
_switches
()
192 except SdnConnectorError
as sc
:
194 except Exception as e
:
195 raise SdnConnectorError(message
="Unable to load switches from CVP",
196 http_code
=500) from e
197 self
.logger
.debug("Using topology {} in Arista Leaf switches: {}".format(
199 self
.delete_keys_from_dict(self
.switches
, ('passwd',))))
200 self
.clC
= AristaSDNConfigLet(self
.topology
)
202 def __load_topology(self
):
203 self
.topology
= self
._VXLAN
_MLAG
204 if self
.__config
and self
.__config
.get('topology'):
205 topology
= self
.__config
.get('topology')
206 if topology
== "VLAN":
207 self
.topology
= self
._VLAN
208 elif topology
== "VXLAN":
209 self
.topology
= self
._VXLAN
210 elif topology
== "VLAN-MLAG":
211 self
.topology
= self
._VLAN
_MLAG
212 elif topology
== "VXLAN-MLAG":
213 self
.topology
= self
._VXLAN
_MLAG
215 def __load_switches(self
):
216 """ Retrieves the switches to configure in the following order
217 1. from incoming configuration:
218 1.1 using port mapping
219 using user and password from WIM
220 retrieving Lo0 and AS from switch
221 1.2 from 'switches' parameter,
222 if any parameter is not present
223 Lo0 and AS - it will be requested to the switch
224 2. Looking in the CloudVision inventory if not in configuration parameters
225 2.1 using the switches with the topology_type tag set to 'leaf'
227 All the search methods will be used
230 if self
.__config
and self
.__config
.get(self
.__SERVICE
_ENDPOINT
_MAPPING
):
231 for port
in self
.__config
.get(self
.__SERVICE
_ENDPOINT
_MAPPING
):
232 switch_dpid
= port
.get(self
.__SW
_ID
_PARAM
)
233 if switch_dpid
and switch_dpid
not in self
.switches
:
234 self
.switches
[switch_dpid
] = {'passwd': self
.__passwd
,
239 'serialNumber': None,
240 'mlagPeerDevice': None}
242 if self
.__config
and self
.__config
.get('switches'):
243 # Not directly from json, complete one by one
244 config_switches
= self
.__config
.get('switches')
245 for cs
, cs_content
in config_switches
.items():
246 if cs
not in self
.switches
:
247 self
.switches
[cs
] = {'passwd': self
.__passwd
,
252 'serialNumber': None,
253 'mlagPeerDevice': None}
255 self
.switches
[cs
].update(cs_content
)
257 # Load the rest of the data
258 if self
.client
== None:
259 self
.client
= self
.__connect
()
260 self
.__load
_inventory
()
261 if not self
.switches
:
262 self
.__get
_tags
(self
.__SWITCH
_TAG
_NAME
, self
.__SWITCH
_TAG
_VALUE
)
263 for device
in self
.allDeviceFacts
:
264 # get the switches whose topology_tag is 'leaf'
265 if device
['serialNumber'] in self
.cvp_tags
:
266 if not self
.switches
.get(device
['hostname']):
267 switch_data
= {'passwd': self
.__passwd
,
268 'ip': device
['ipAddress'],
272 'serialNumber': None,
273 'mlagPeerDevice': None}
274 self
.switches
[device
['hostname']] = switch_data
275 if len(self
.switches
) == 0:
276 self
.logger
.error("Unable to load Leaf switches from CVP")
279 # self.switches are switch objects, one for each switch in self.switches,
280 # used to make eAPI calls by using switch.py module
281 for s
in self
.switches
:
282 for device
in self
.allDeviceFacts
:
283 if device
['hostname'] == s
:
284 if not self
.switches
[s
].get('ip'):
285 self
.switches
[s
]['ip'] = device
['ipAddress']
286 self
.switches
[s
]['serialNumber'] = device
['serialNumber']
289 # Each switch has a different loopback address,
290 # so it's a different configLet
291 if not self
.switches
[s
].get('lo0'):
292 inf
= self
.__get
_interface
_ip
(self
.switches
[s
]['serialNumber'], self
.__LOOPBACK
_INTF
)
293 self
.switches
[s
]["lo0"] = inf
.split('/')[0]
294 if not self
.switches
[s
].get('AS'):
295 self
.switches
[s
]["AS"] = self
.__get
_device
_ASN
(self
.switches
[s
]['serialNumber'])
296 if self
.topology
in (self
._VXLAN
_MLAG
, self
._VLAN
_MLAG
):
297 for s
in self
.switches
:
298 if not self
.switches
[s
].get('mlagPeerDevice'):
299 self
.switches
[s
]['mlagPeerDevice'] = self
.__get
_peer
_MLAG
(self
.switches
[s
]['serialNumber'])
301 def __check_service(self
, service_type
, connection_points
,
302 check_vlan
=True, check_num_cp
=True, kwargs
=None):
303 """ Reviews the connection points elements looking for semantic errors in the incoming data
305 if service_type
not in self
.__supported
_service
_types
:
306 raise Exception("The service '{}' is not supported. Only '{}' are accepted".format(
308 self
.__supported
_service
_types
))
311 if len(connection_points
) < 2:
312 raise Exception(SdnError
.CONNECTION_POINTS_SIZE
)
313 if (len(connection_points
) != self
.__ELINE
_num
_connection
_points
and
314 service_type
== self
.__service
_types
_ELINE
):
315 raise Exception(SdnError
.CONNECTION_POINTS_SIZE
)
319 for cp
in connection_points
:
320 enc_type
= cp
.get(self
.__ENCAPSULATION
_TYPE
_PARAM
)
322 enc_type
not in self
.__supported
_encapsulation
_types
):
323 raise Exception(SdnError
.ENCAPSULATION_TYPE
)
324 encap_info
= cp
.get(self
.__ENCAPSULATION
_INFO
_PARAM
)
325 cp_vlan_id
= str(encap_info
.get(self
.__VLAN
_PARAM
))
329 elif vlan_id
!= cp_vlan_id
:
330 raise Exception(SdnError
.VLAN_INCONSISTENT
)
332 raise Exception(SdnError
.VLAN_NOT_PROVIDED
)
333 if vlan_id
in self
.__get
_srvVLANs
():
334 raise Exception('VLAN {} already assigned to a connectivity service'.format(vlan_id
))
336 # Commented out for as long as parameter isn't implemented
337 # bandwidth = kwargs.get(self.__BANDWIDTH_PARAM)
338 # if not isinstance(bandwidth, int):
339 # self.__exception(SdnError.BANDWIDTH, http_code=400)
341 # Commented out for as long as parameter isn't implemented
342 # backup = kwargs.get(self.__BACKUP_PARAM)
343 # if not isinstance(backup, bool):
344 # self.__exception(SdnError.BACKUP, http_code=400)
346 def check_credentials(self
):
347 """Retrieves the CloudVision version information, as the easiest way
348 for testing the access to CloudVision API
351 if self
.client
== None:
352 self
.client
= self
.__connect
()
353 result
= self
.client
.api
.get_cvp_info()
354 self
.logger
.debug(result
)
355 except CvpLoginError
as e
:
356 self
.logger
.info(str(e
))
358 raise SdnConnectorError(message
=SdnError
.UNAUTHORIZED
,
359 http_code
=401) from e
360 except Exception as ex
:
362 self
.logger
.error(str(ex
))
363 raise SdnConnectorError(message
=SdnError
.INTERNAL_ERROR
,
364 http_code
=500) from ex
366 def get_connectivity_service_status(self
, service_uuid
, conn_info
=None):
367 """Monitor the status of the connectivity service established
369 service_uuid (str): UUID of the connectivity service
370 conn_info (dict or None): Information returned by the connector
371 during the service creation/edition and subsequently stored in
375 dict: JSON/YAML-serializable dict that contains a mandatory key
376 ``sdn_status`` associated with one of the following values::
378 {'sdn_status': 'ACTIVE'}
379 # The service is up and running.
381 {'sdn_status': 'INACTIVE'}
382 # The service was created, but the connector
383 # cannot determine yet if connectivity exists
384 # (ideally, the caller needs to wait and check again).
386 {'sdn_status': 'DOWN'}
387 # Connection was previously established,
388 # but an error/failure was detected.
390 {'sdn_status': 'ERROR'}
391 # An error occurred when trying to create the service/
392 # establish the connectivity.
394 {'sdn_status': 'BUILD'}
395 # Still trying to create the service, the caller
396 # needs to wait and check again.
398 Additionally ``error_msg``(**str**) and ``sdn_info``(**dict**)
399 keys can be used to provide additional status explanation or
400 new information available for the connectivity service.
403 self
.logger
.debug("invoked get_connectivity_service_status '{}'".format(service_uuid
))
405 raise SdnConnectorError(message
='No connection service UUID',
408 self
.__get
_Connection
()
409 if conn_info
== None:
410 raise SdnConnectorError(message
='No connection information for service UUID {}'.format(service_uuid
),
413 if 'configLetPerSwitch' in conn_info
.keys():
417 cls_perSw
= self
.__get
_serviceData
(service_uuid
,
418 conn_info
['service_type'],
419 conn_info
['vlan_id'],
422 t_isCancelled
= False
426 for s
in self
.switches
:
427 if len(cls_perSw
[s
]) > 0:
428 for cl
in cls_perSw
[s
]:
429 # Fix 1030 SDN-ARISTA Key error note when deploy a NS
430 # Added protection to check that 'note' exists and additionally
431 # verify that it is managed by OSM
432 if (not cls_perSw
[s
][0]['config'] or
433 not cl
.get('note') or
434 self
.__MANAGED
_BY
_OSM
not in cl
['note']):
437 t_id
= note
.split(self
.__SEPARATOR
)[1]
438 result
= self
.client
.api
.get_task_by_id(t_id
)
439 if result
['workOrderUserDefinedStatus'] == 'Completed':
441 elif result
['workOrderUserDefinedStatus'] == 'Cancelled':
443 elif result
['workOrderUserDefinedStatus'] == 'Failed':
447 failed_switches
.append(s
)
449 error_msg
= 'Some works were cancelled in switches: {}'.format(str(failed_switches
))
452 error_msg
= 'Some works failed in switches: {}'.format(str(failed_switches
))
455 error_msg
= 'Some works are still under execution in switches: {}'.format(str(failed_switches
))
459 sdn_status
= 'ACTIVE'
461 return {'sdn_status': sdn_status
,
462 'error_msg': error_msg
,
463 'sdn_info': sdn_info
}
464 except CvpLoginError
as e
:
465 self
.logger
.info(str(e
))
467 raise SdnConnectorError(message
=SdnError
.UNAUTHORIZED
,
468 http_code
=401) from e
469 except Exception as ex
:
471 self
.logger
.error(str(ex
), exc_info
=True)
472 raise SdnConnectorError(message
=str(ex
),
473 http_code
=500) from ex
475 def create_connectivity_service(self
, service_type
, connection_points
,
477 """Stablish SDN/WAN connectivity between the endpoints
479 (str): ``ELINE`` (L2), ``ELAN`` (L2), ``ETREE`` (L2), ``L3``.
480 :param connection_points: (list): each point corresponds to
481 an entry point to be connected. For WIM: from the DC
482 to the transport network.
483 For SDN: Compute/PCI to the transport network. One
484 connection point serves to identify the specific access and
485 some other service parameters, such as encapsulation type.
486 Each item of the list is a dict with:
487 "service_endpoint_id": (str)(uuid) Same meaning that for
488 'service_endpoint_mapping' (see __init__)
489 In case the config attribute mapping_not_needed is True,
490 this value is not relevant. In this case
491 it will contain the string "device_id:device_interface_id"
492 "service_endpoint_encapsulation_type": None, "dot1q", ...
493 "service_endpoint_encapsulation_info": (dict) with:
494 "vlan": ..., (int, present if encapsulation is dot1q)
495 "vni": ... (int, present if encapsulation is vxlan),
496 "peers": [(ipv4_1), (ipv4_2)] (present if
497 encapsulation is vxlan)
499 "device_id": ..., same meaning that for
500 'service_endpoint_mapping' (see __init__)
501 "device_interface_id": same meaning that for
502 'service_endpoint_mapping' (see __init__)
503 "switch_dpid": ..., present if mapping has been found
504 for this device_id,device_interface_id
505 "switch_port": ... present if mapping has been found
506 for this device_id,device_interface_id
507 "service_mapping_info": present if mapping has
508 been found for this device_id,device_interface_id
509 :param kwargs: For future versions:
510 bandwidth (int): value in kilobytes
511 latency (int): value in milliseconds
512 Other QoS might be passed as keyword arguments.
513 :return: tuple: ``(service_id, conn_info)`` containing:
514 - *service_uuid* (str): UUID of the established
516 - *conn_info* (dict or None): Information to be
517 stored at the database (or ``None``).
518 This information will be provided to the
519 :meth:`~.edit_connectivity_service` and :obj:`~.delete`.
520 **MUST** be JSON/YAML-serializable (plain data structures).
521 :raises: SdnConnectorError: In case of error. Nothing should be
522 created in this case.
523 Provide the parameter http_code
526 self
.logger
.debug("invoked create_connectivity_service '{}' ports: {}".
527 format(service_type
, connection_points
))
528 self
.__get
_Connection
()
529 self
.__check
_service
(service_type
,
533 service_uuid
= str(uuid
.uuid4())
535 self
.logger
.info("Service with uuid {} created.".
536 format(service_uuid
))
537 s_uid
, s_connInf
= self
.__processConnection
(
543 self
.__addMetadata
(s_uid
, service_type
, s_connInf
['vlan_id'])
544 except Exception as e
:
547 return (s_uid
, s_connInf
)
548 except CvpLoginError
as e
:
549 self
.logger
.info(str(e
))
551 raise SdnConnectorError(message
=SdnError
.UNAUTHORIZED
,
552 http_code
=401) from e
553 except SdnConnectorError
as sde
:
555 except ValueError as err
:
557 self
.logger
.error(str(err
), exc_info
=True)
558 raise SdnConnectorError(message
=str(err
),
559 http_code
=500) from err
560 except Exception as ex
:
562 self
.logger
.error(str(ex
), exc_info
=True)
563 if self
.raiseException
:
565 raise SdnConnectorError(message
=str(ex
),
566 http_code
=500) from ex
568 def __processConnection(self
,
574 Invoked from creation and edit methods
576 Process the connection points array,
577 creating a set of configuration per switch where it has to be applied
578 for creating the configuration, the switches have to be queried for obtaining:
579 - the loopback address
580 - the BGP ASN (autonomous system number)
581 - the interface name of the MAC address to add in the connectivity service
582 Once the new configuration is ready, the __updateConnection method is invoked for appling the changes
588 for s
in self
.switches
:
591 vlan_processed
= False
594 processed_connection_points
= []
595 for cp
in connection_points
:
597 encap_info
= cp
.get(self
.__ENCAPSULATION
_INFO
_PARAM
)
598 if not vlan_processed
:
599 vlan_id
= str(encap_info
.get(self
.__VLAN
_PARAM
))
602 vni_id
= encap_info
.get(self
.__VNI
_PARAM
)
604 vni_id
= str(10000 + int(vlan_id
))
606 if service_type
== self
.__service
_types
_ELAN
:
607 cl_vlan
= self
.clC
.getElan_vlan(service_uuid
,
611 cl_vlan
= self
.clC
.getEline_vlan(service_uuid
,
614 vlan_processed
= True
616 encap_type
= cp
.get(self
.__ENCAPSULATION
_TYPE
_PARAM
)
617 switch_id
= encap_info
.get(self
.__SW
_ID
_PARAM
)
618 interface
= encap_info
.get(self
.__SW
_PORT
_PARAM
)
619 switches
= [{'name': switch_id
, 'interface': interface
}]
621 # remove those connections that are equal. This happens when several sriovs are located in the same
622 # compute node interface, that is, in the same switch and interface
623 switches
= [x
for x
in switches
if x
not in processed_connection_points
]
626 processed_connection_points
+= switches
627 for switch
in switches
:
629 raise SdnConnectorError(message
="Connection point switch port empty for switch_dpid {}".format(switch_id
),
631 # it should be only one switch where the mac is attached
632 if encap_type
== 'dot1q':
633 # SRIOV configLet for Leaf switch mac's attached to
634 if service_type
== self
.__service
_types
_ELAN
:
635 cl_encap
= self
.clC
.getElan_sriov(service_uuid
, interface
, vlan_id
, i
)
637 cl_encap
= self
.clC
.getEline_sriov(service_uuid
, interface
, vlan_id
, i
)
639 # PT configLet for Leaf switch attached to the mac
640 if service_type
== self
.__service
_types
_ELAN
:
641 cl_encap
= self
.clC
.getElan_passthrough(service_uuid
,
645 cl_encap
= self
.clC
.getEline_passthrough(service_uuid
,
648 if cls_cp
.get(switch
['name']):
649 cls_cp
[switch
['name']] = str(cls_cp
[switch
['name']]) + cl_encap
651 cls_cp
[switch
['name']] = cl_encap
653 # at least 1 connection point has to be received
654 if not vlan_processed
:
655 raise SdnConnectorError(message
=SdnError
.UNSUPPORTED_FEATURE
,
658 for s
in self
.switches
:
659 # for cl in cp_configLets:
660 cl_name
= (self
.__OSM
_PREFIX
+
662 self
.__SEPARATOR
+ service_type
+ str(vlan_id
) +
663 self
.__SEPARATOR
+ service_uuid
)
665 # Apply BGP configuration only for VXLAN topologies
666 if self
.topology
in (self
._VXLAN
_MLAG
, self
._VXLAN
):
667 if service_type
== self
.__service
_types
_ELAN
:
668 cl_bgp
[s
] = self
.clC
.getElan_bgp(service_uuid
,
671 self
.switches
[s
]['lo0'],
672 self
.switches
[s
]['AS'])
674 cl_bgp
[s
] = self
.clC
.getEline_bgp(service_uuid
,
677 self
.switches
[s
]['lo0'],
678 self
.switches
[s
]['AS'])
682 if not cls_cp
.get(s
):
683 # Apply VLAN configuration to peer MLAG switch,
684 # only necessary when there are no connection points in the switch
685 if self
.topology
in (self
._VXLAN
_MLAG
, self
._VLAN
_MLAG
):
686 for p
in self
.switches
:
687 if self
.switches
[p
]['mlagPeerDevice'] == s
:
689 cl_config
= str(cl_vlan
)
691 cl_config
= str(cl_vlan
) + str(cl_bgp
[s
]) + str(cls_cp
[s
])
693 cls_perSw
[s
] = [{'name': cl_name
, 'config': cl_config
}]
695 allLeafConfigured
, allLeafModified
= self
.__updateConnection
(cls_perSw
)
698 "uuid": service_uuid
,
700 "service_type": service_type
,
702 "connection_points": connection_points
,
703 "configLetPerSwitch": cls_perSw
,
704 'allLeafConfigured': allLeafConfigured
,
705 'allLeafModified': allLeafModified
}
707 return service_uuid
, conn_info
708 except Exception as ex
:
709 self
.logger
.debug("Exception processing connection {}: {}".
710 format(service_uuid
, str(ex
)))
713 def __updateConnection(self
, cls_perSw
):
714 """ Invoked in the creation and modification
716 checks if the new connection points config is:
717 - already in the Cloud Vision, the configLet is modified, and applied to the switch,
718 executing the corresponding task
719 - if it has to be removed:
720 then configuration has to be removed from the switch executing the corresponding task,
721 before trying to remove the configuration
722 - created, the configuration set is created, associated to the switch, and the associated
723 task to the configLet modification executed
724 In case of any error, rollback is executed, removing the created elements, and restoring to the
728 allLeafConfigured
= {}
731 for s
in self
.switches
:
732 allLeafConfigured
[s
] = False
733 allLeafModified
[s
] = False
735 for s
in self
.switches
:
736 toDelete_in_cvp
= False
737 if not (cls_perSw
.get(s
) and cls_perSw
[s
][0].get('config')):
738 # when there is no configuration, means that there is no interface
739 # in the switch to be connected, so the configLet has to be removed from CloudVision
740 # after removing the ConfigLet fron the switch if it was already there
742 # get config let name and key
745 cvp_cl
= self
.client
.api
.get_configlet_by_name(cl
[0]['name'])
747 cl_toDelete
.append(cvp_cl
)
749 toDelete_in_cvp
= True
750 except CvpApiError
as error
:
751 if "Entity does not exist" in error
.msg
:
755 # remove configLet from device
757 res
= self
.__configlet
_modify
(cls_perSw
[s
])
758 allLeafConfigured
[s
] = res
[0]
759 if not allLeafConfigured
[s
]:
762 res
= self
.__device
_modify
(
765 delete
=toDelete_in_cvp
)
766 if "errorMessage" in str(res
):
767 raise Exception(str(res
))
768 self
.logger
.info("Device {} modify result {}".format(s
, res
))
769 for t_id
in res
[1]['tasks']:
770 if not toDelete_in_cvp
:
771 note_msg
= "{}{}{}{}##".format(self
.__MANAGED
_BY
_OSM
,
775 self
.client
.api
.add_note_to_configlet(
776 cls_perSw
[s
][0]['key'],
778 cls_perSw
[s
][0]['note'] = note_msg
779 tasks
= { t_id
: {'workOrderId': t_id
} }
780 self
.__exec
_task
(tasks
, self
.__EXC
_TASK
_EXEC
_WAIT
)
781 # with just one configLet assigned to a device,
782 # delete all if there are errors in next loops
783 if not toDelete_in_cvp
:
784 allLeafModified
[s
] = True
785 if len(cl_toDelete
) > 0:
786 self
.__configlet
_modify
(cl_toDelete
, delete
=True)
788 return allLeafConfigured
, allLeafModified
789 except Exception as ex
:
791 self
.__rollbackConnection
(cls_perSw
,
794 except Exception as e
:
795 self
.logger
.error("Exception rolling back in updating connection: {}".
796 format(e
), exc_info
=True)
799 def __rollbackConnection(self
,
803 """ Removes the given configLet from the devices and then remove the configLets
805 for s
in self
.switches
:
806 if allLeafModified
[s
]:
808 res
= self
.__device
_modify
(
810 new_configlets
=cls_perSw
[s
],
812 if "errorMessage" in str(res
):
813 raise Exception(str(res
))
815 for t_id
in res
[1]['tasks']:
816 tasks
[t_id
] = {'workOrderId': t_id
}
817 self
.__exec
_task
(tasks
)
818 self
.logger
.info("Device {} modify result {}".format(s
, res
))
819 except Exception as e
:
820 self
.logger
.error('Error removing configlets from device {}: {}'.format(s
, e
))
822 for s
in self
.switches
:
823 if allLeafConfigured
[s
]:
824 self
.__configlet
_modify
(cls_perSw
[s
], delete
=True)
826 def __exec_task(self
, tasks
, tout
=10):
827 if self
.taskC
== None:
829 data
= self
.taskC
.update_all_tasks(tasks
).values()
830 self
.taskC
.task_action(data
, tout
, 'executed')
832 def __device_modify(self
, device_to_update
, new_configlets
, delete
):
833 """ Updates the devices (switches) adding or removing the configLet,
834 the tasks Id's associated to the change are returned
836 self
.logger
.info('Enter in __device_modify delete: {}'.format(
840 # Task Ids that have been identified during device actions
843 if (len(new_configlets
) == 0 or
844 device_to_update
== None or
845 len(device_to_update
) == 0):
846 data
= {'updated': updated
, 'tasks': newTasks
}
847 return [changed
, data
]
849 self
.__load
_inventory
()
851 allDeviceFacts
= self
.allDeviceFacts
852 # Work through Devices list adding device specific information
854 for try_device
in allDeviceFacts
:
855 # Add Device Specific Configlets
856 # self.logger.debug(device)
857 if try_device
['hostname'] not in device_to_update
:
859 dev_cvp_configlets
= self
.client
.api
.get_configlets_by_device_id(
860 try_device
['systemMacAddress'])
861 # self.logger.debug(dev_cvp_configlets)
862 try_device
['deviceSpecificConfiglets'] = []
863 for cvp_configlet
in dev_cvp_configlets
:
864 if int(cvp_configlet
['containerCount']) == 0:
865 try_device
['deviceSpecificConfiglets'].append(
866 {'name': cvp_configlet
['name'],
867 'key': cvp_configlet
['key']})
868 # self.logger.debug(device)
872 # Check assigned configlets
873 device_update
= False
875 remove_configlets
= []
879 for cvp_configlet
in device
['deviceSpecificConfiglets']:
880 for cl
in new_configlets
:
881 if cvp_configlet
['name'] == cl
['name']:
882 remove_configlets
.append(cvp_configlet
)
885 for configlet
in new_configlets
:
886 if configlet
not in device
['deviceSpecificConfiglets']:
887 add_configlets
.append(configlet
)
890 update_devices
.append({'hostname': device
['hostname'],
891 'configlets': [add_configlets
,
894 self
.logger
.info("Device to modify: {}".format(update_devices
))
896 up_device
= update_devices
[0]
897 cl_toAdd
= up_device
['configlets'][0]
898 cl_toDel
= up_device
['configlets'][1]
901 if delete
and len(cl_toDel
) > 0:
902 r
= self
.client
.api
.remove_configlets_from_device(
908 self
.logger
.debug("remove_configlets_from_device {} {}".format(dev_action
, cl_toDel
))
909 elif len(cl_toAdd
) > 0:
910 r
= self
.client
.api
.apply_configlets_to_device(
916 self
.logger
.debug("apply_configlets_to_device {} {}".format(dev_action
, cl_toAdd
))
918 except Exception as error
:
919 errorMessage
= str(error
)
920 msg
= "errorMessage: Device {} Configlets couldnot be updated: {}".format(
921 up_device
['hostname'], errorMessage
)
922 raise SdnConnectorError(msg
) from error
924 if "errorMessage" in str(dev_action
):
925 m
= "Device {} Configlets update fail: {}".format(
926 up_device
['name'], dev_action
['errorMessage'])
927 raise SdnConnectorError(m
)
930 if 'taskIds' in str(dev_action
):
931 # Fix 1030 SDN-ARISTA Key error note when deploy a NS
932 if not dev_action
['data']['taskIds']:
933 raise SdnConnectorError("No taskIds found: Device {} Configlets couldnot be updated".format(
934 up_device
['hostname']))
935 for taskId
in dev_action
['data']['taskIds']:
936 updated
.append({up_device
['hostname']:
937 "Configlets-{}".format(
939 newTasks
.append(taskId
)
941 updated
.append({up_device
['hostname']:
942 "Configlets-No_Specific_Tasks"})
943 data
= {'updated': updated
, 'tasks': newTasks
}
944 return [changed
, data
]
946 def __configlet_modify(self
, configletsToApply
, delete
=False):
947 ''' adds/update or delete the provided configLets
948 :param configletsToApply: list of configLets to apply
949 :param delete: flag to indicate if the configLets have to be deleted
950 from Cloud Vision Portal
951 :return: data: dict of module actions and taskIDs
953 self
.logger
.info('Enter in __configlet_modify delete:{}'.format(
956 # Compare configlets against cvp_facts-configlets
963 for cl
in configletsToApply
:
970 cvp_cl
= self
.client
.api
.get_configlet_by_name(cl
['name'])
971 cl
['key'] = cvp_cl
['key']
972 cl
['note'] = cvp_cl
['note']
974 except CvpApiError
as error
:
975 if "Entity does not exist" in error
.msg
:
983 configlet
= {'name': cvp_cl
['name'],
987 cl_compare
= self
.__compare
(cl
['config'],
989 # compare function returns a floating point number
990 if cl_compare
[0] != 100.0:
992 configlet
= {'name': cl
['name'],
994 'config': cl
['config']}
997 configlet
= {'name': cl
['name'],
998 'key': cvp_cl
['key'],
1000 'config': cl
['config']}
1003 configlet
= {'name': cl
['name'],
1004 'config': cl
['config']}
1007 operation
= 'delete'
1008 resp
= self
.client
.api
.delete_configlet(
1009 configlet
['data']['name'],
1010 configlet
['data']['key'])
1012 operation
= 'update'
1013 resp
= self
.client
.api
.update_configlet(
1014 configlet
['config'],
1015 configlet
['data']['key'],
1016 configlet
['data']['name'],
1019 operation
= 'create'
1020 resp
= self
.client
.api
.add_configlet(
1022 configlet
['config'])
1024 operation
= 'checked'
1026 except Exception as error
:
1027 errorMessage
= str(error
).split(':')[-1]
1028 message
= "Configlet {} cannot be {}: {}".format(
1029 cl
['name'], operation
, errorMessage
)
1031 deleted
.append({configlet
['name']: message
})
1033 updated
.append({configlet
['name']: message
})
1035 new
.append({configlet
['name']: message
})
1037 checked
.append({configlet
['name']: message
})
1040 if "error" in str(resp
).lower():
1041 message
= "Configlet {} cannot be deleted: {}".format(
1042 cl
['name'], resp
['errorMessage'])
1044 deleted
.append({configlet
['name']: message
})
1046 updated
.append({configlet
['name']: message
})
1048 new
.append({configlet
['name']: message
})
1050 checked
.append({configlet
['name']: message
})
1054 deleted
.append({configlet
['name']: "success"})
1057 updated
.append({configlet
['name']: "success"})
1060 cl
['key'] = resp
# This key is used in API call deviceApplyConfigLet FGA
1061 new
.append({configlet
['name']: "success"})
1064 checked
.append({configlet
['name']: "success"})
1066 data
= {'new': new
, 'updated': updated
, 'deleted': deleted
, 'checked': checked
}
1067 return [changed
, data
]
1069 def __get_configletsDevices(self
, configlets
):
1070 for s
in self
.switches
:
1071 configlet
= configlets
[s
]
1072 # Add applied Devices
1073 if len(configlet
) > 0:
1074 configlet
['devices'] = []
1075 applied_devices
= self
.client
.api
.get_applied_devices(
1077 for device
in applied_devices
['data']:
1078 configlet
['devices'].append(device
['hostName'])
1080 def __get_serviceData(self
, service_uuid
, service_type
, vlan_id
, conn_info
=None):
1082 for s
in self
.switches
:
1085 srv_cls
= self
.__get
_serviceConfigLets
(service_uuid
,
1088 self
.__get
_configletsDevices
(srv_cls
)
1089 for s
in self
.switches
:
1092 for dev
in cl
['devices']:
1093 cls_perSw
[dev
].append(cl
)
1095 cls_perSw
= conn_info
['configLetPerSwitch']
1098 def delete_connectivity_service(self
, service_uuid
, conn_info
=None):
1100 Disconnect multi-site endpoints previously connected
1102 :param service_uuid: The one returned by create_connectivity_service
1103 :param conn_info: The one returned by last call to 'create_connectivity_service' or 'edit_connectivity_service'
1104 if they do not return None
1106 :raises: SdnConnectorException: In case of error. The parameter http_code must be filled
1109 self
.logger
.debug('invoked delete_connectivity_service {}'.
1110 format(service_uuid
))
1111 if not service_uuid
:
1112 raise SdnConnectorError(message
='No connection service UUID',
1115 self
.__get
_Connection
()
1116 if conn_info
== None:
1117 raise SdnConnectorError(message
='No connection information for service UUID {}'.format(service_uuid
),
1120 cls_perSw
= self
.__get
_serviceData
(service_uuid
,
1121 conn_info
['service_type'],
1122 conn_info
['vlan_id'],
1124 allLeafConfigured
= {}
1125 allLeafModified
= {}
1126 for s
in self
.switches
:
1127 allLeafConfigured
[s
] = True
1128 allLeafModified
[s
] = True
1129 found_in_cvp
= False
1130 for s
in self
.switches
:
1134 self
.__rollbackConnection
(cls_perSw
,
1138 # if the service is not defined in Cloud Vision, return a 404 - NotFound error
1139 raise SdnConnectorError(message
='Service {} was not found in Arista Cloud Vision {}'.
1140 format(service_uuid
, self
.__wim
_url
),
1142 self
.__removeMetadata
(service_uuid
)
1143 except CvpLoginError
as e
:
1144 self
.logger
.info(str(e
))
1146 raise SdnConnectorError(message
=SdnError
.UNAUTHORIZED
,
1147 http_code
=401) from e
1148 except SdnConnectorError
as sde
:
1150 except Exception as ex
:
1152 self
.logger
.error(ex
)
1153 if self
.raiseException
:
1155 raise SdnConnectorError(message
=SdnError
.INTERNAL_ERROR
,
1156 http_code
=500) from ex
1158 def __addMetadata(self
, service_uuid
, service_type
, vlan_id
):
1159 """ Adds the connectivity service from 'OSM_metadata' configLet
1161 found_in_cvp
= False
1163 cvp_cl
= self
.client
.api
.get_configlet_by_name(self
.__OSM
_METADATA
)
1165 except CvpApiError
as error
:
1166 if "Entity does not exist" in error
.msg
:
1171 new_serv
= '{} {} {} {}\n'.format(self
.__METADATA
_PREFIX
, service_type
, vlan_id
, service_uuid
)
1174 cl_config
= cvp_cl
['config'] + new_serv
1176 cl_config
= new_serv
1177 cl_meta
= [{'name': self
.__OSM
_METADATA
, 'config': cl_config
}]
1178 self
.__configlet
_modify
(cl_meta
)
1179 except Exception as e
:
1180 self
.logger
.error('Error in setting metadata in CloudVision from OSM for service {}: {}'.
1181 format(service_uuid
, str(e
)))
1184 def __removeMetadata(self
, service_uuid
):
1185 """ Removes the connectivity service from 'OSM_metadata' configLet
1187 found_in_cvp
= False
1189 cvp_cl
= self
.client
.api
.get_configlet_by_name(self
.__OSM
_METADATA
)
1191 except CvpApiError
as error
:
1192 if "Entity does not exist" in error
.msg
:
1198 if service_uuid
in cvp_cl
['config']:
1200 for line
in cvp_cl
['config'].split('\n'):
1201 if service_uuid
in line
:
1204 cl_config
= cl_config
+ line
1205 cl_meta
= [{'name': self
.__OSM
_METADATA
, 'config': cl_config
}]
1206 self
.__configlet
_modify
(cl_meta
)
1207 except Exception as e
:
1208 self
.logger
.error('Error in removing metadata in CloudVision from OSM for service {}: {}'.
1209 format(service_uuid
, str(e
)))
1212 def edit_connectivity_service(self
,
1215 connection_points
=None,
1217 """ Change an existing connectivity service.
1219 This method's arguments and return value follow the same convention as
1220 :meth:`~.create_connectivity_service`.
1222 :param service_uuid: UUID of the connectivity service.
1223 :param conn_info: (dict or None): Information previously returned
1224 by last call to create_connectivity_service
1225 or edit_connectivity_service
1226 :param connection_points: (list): If provided, the old list of
1227 connection points will be replaced.
1228 :param kwargs: Same meaning that create_connectivity_service
1229 :return: dict or None: Information to be updated and stored at
1231 When ``None`` is returned, no information should be changed.
1232 When an empty dict is returned, the database record will
1234 **MUST** be JSON/YAML-serializable (plain data structures).
1236 SdnConnectorError: In case of error.
1239 self
.logger
.debug('invoked edit_connectivity_service for service {}. ports: {}'.format(service_uuid
,
1242 if not service_uuid
:
1243 raise SdnConnectorError(message
='Unable to perform operation, missing or empty uuid',
1246 raise SdnConnectorError(message
='Unable to perform operation, missing or empty connection information',
1249 if connection_points
== None:
1252 self
.__get
_Connection
()
1254 cls_currentPerSw
= conn_info
['configLetPerSwitch']
1255 service_type
= conn_info
['service_type']
1257 self
.__check
_service
(service_type
,
1263 s_uid
, s_connInf
= self
.__processConnection
(
1268 self
.logger
.info("Service with uuid {} configuration updated".
1271 except CvpLoginError
as e
:
1272 self
.logger
.info(str(e
))
1274 raise SdnConnectorError(message
=SdnError
.UNAUTHORIZED
,
1275 http_code
=401) from e
1276 except SdnConnectorError
as sde
:
1278 except Exception as ex
:
1281 # TODO check if there are pending task, and cancel them before restoring
1282 self
.__updateConnection
(cls_currentPerSw
)
1283 except Exception as e
:
1284 self
.logger
.error("Unable to restore configuration in service {} after an error in the configuration updated: {}".
1285 format(service_uuid
, str(e
)))
1286 if self
.raiseException
:
1288 raise SdnConnectorError(message
=str(ex
),
1289 http_code
=500) from ex
1291 def clear_all_connectivity_services(self
):
1292 """ Removes all connectivity services from Arista CloudVision with two steps:
1293 - retrives all the services from Arista CloudVision
1294 - removes each service
1297 self
.logger
.debug('invoked AristaImpl ' +
1298 'clear_all_connectivity_services')
1299 self
.__get
_Connection
()
1300 s_list
= self
.__get
_srvUUIDs
()
1303 conn_info
['service_type'] = serv
['type']
1304 conn_info
['vlan_id'] = serv
['vlan']
1306 self
.delete_connectivity_service(serv
['uuid'], conn_info
)
1307 except CvpLoginError
as e
:
1308 self
.logger
.info(str(e
))
1310 raise SdnConnectorError(message
=SdnError
.UNAUTHORIZED
,
1311 http_code
=401) from e
1312 except SdnConnectorError
as sde
:
1314 except Exception as ex
:
1316 self
.logger
.error(ex
)
1317 if self
.raiseException
:
1319 raise SdnConnectorError(message
=SdnError
.INTERNAL_ERROR
,
1320 http_code
=500) from ex
1322 def get_all_active_connectivity_services(self
):
1323 """ Return the uuid of all the active connectivity services with two steps:
1324 - retrives all the services from Arista CloudVision
1325 - retrives the status of each server
1328 self
.logger
.debug('invoked AristaImpl {}'.format(
1329 'get_all_active_connectivity_services'))
1330 self
.__get
_Connection
()
1331 s_list
= self
.__get
_srvUUIDs
()
1335 conn_info
['service_type'] = serv
['type']
1336 conn_info
['vlan_id'] = serv
['vlan']
1338 status
= self
.get_connectivity_service_status(serv
['uuid'], conn_info
)
1339 if status
['sdn_status'] == 'ACTIVE':
1340 result
.append(serv
['uuid'])
1342 except CvpLoginError
as e
:
1343 self
.logger
.info(str(e
))
1345 raise SdnConnectorError(message
=SdnError
.UNAUTHORIZED
,
1346 http_code
=401) from e
1347 except SdnConnectorError
as sde
:
1349 except Exception as ex
:
1351 self
.logger
.error(ex
)
1352 if self
.raiseException
:
1354 raise SdnConnectorError(message
=SdnError
.INTERNAL_ERROR
,
1355 http_code
=500) from ex
1357 def __get_serviceConfigLets(self
, service_uuid
, service_type
, vlan_id
):
1358 """ Return the configLet's associated with a connectivity service,
1359 There should be one, as maximum, per device (switch) for a given
1360 connectivity service
1363 for s
in self
.switches
:
1365 found_in_cvp
= False
1366 name
= (self
.__OSM
_PREFIX
+
1368 self
.__SEPARATOR
+ service_type
+ str(vlan_id
) +
1369 self
.__SEPARATOR
+ service_uuid
)
1371 cvp_cl
= self
.client
.api
.get_configlet_by_name(name
)
1373 except CvpApiError
as error
:
1374 if "Entity does not exist" in error
.msg
:
1382 def __get_srvVLANs(self
):
1383 """ Returns a list with all the VLAN id's used in the connectivity services managed
1384 in tha Arista CloudVision by checking the 'OSM_metadata' configLet where this
1385 information is stored
1387 found_in_cvp
= False
1389 cvp_cl
= self
.client
.api
.get_configlet_by_name(self
.__OSM
_METADATA
)
1391 except CvpApiError
as error
:
1392 if "Entity does not exist" in error
.msg
:
1398 lines
= cvp_cl
['config'].split('\n')
1400 if self
.__METADATA
_PREFIX
in line
:
1401 s_vlan
= line
.split(' ')[3]
1404 if (s_vlan
is not None and
1406 s_vlan
not in s_vlan_list
):
1407 s_vlan_list
.append(s_vlan
)
1411 def __get_srvUUIDs(self
):
1412 """ Retrieves all the connectivity services, managed in tha Arista CloudVision
1413 by checking the 'OSM_metadata' configLet where this information is stored
1415 found_in_cvp
= False
1417 cvp_cl
= self
.client
.api
.get_configlet_by_name(self
.__OSM
_METADATA
)
1419 except CvpApiError
as error
:
1420 if "Entity does not exist" in error
.msg
:
1426 lines
= cvp_cl
['config'].split('\n')
1428 if self
.__METADATA
_PREFIX
in line
:
1429 line
= line
.split(' ')
1430 serv
= {'uuid': line
[4], 'type': line
[2], 'vlan': line
[3]}
1433 if (serv
is not None and
1435 serv
not in serv_list
):
1436 serv_list
.append(serv
)
1440 def __get_Connection(self
):
1441 """ Open a connection with Arista CloudVision,
1442 invoking the version retrival as test
1445 if self
.client
== None:
1446 self
.client
= self
.__connect
()
1447 self
.client
.api
.get_cvp_info()
1448 except (CvpSessionLogOutError
, RequestException
) as e
:
1449 self
.logger
.debug("Connection error '{}'. Reconnecting".format(e
))
1450 self
.client
= self
.__connect
()
1451 self
.client
.api
.get_cvp_info()
1453 def __connect(self
):
1454 ''' Connects to CVP device using user provided credentials from initialization.
1455 :return: CvpClient object with connection instantiated.
1457 client
= CvpClient()
1458 protocol
, _
, rest_url
= self
.__wim
_url
.rpartition("://")
1459 host
, _
, port
= rest_url
.partition(":")
1460 if port
and port
.endswith("/"):
1461 port
= int(port
[:-1])
1467 client
.connect([host
],
1470 protocol
=protocol
or "https",
1473 client
.api
= CvpApi(client
, request_timeout
=self
.__API
_REQUEST
_TOUT
)
1474 self
.taskC
= AristaCVPTask(client
.api
)
1477 def __compare(self
, fromText
, toText
, lines
=10):
1478 """ Compare text string in 'fromText' with 'toText' and produce
1479 diffRatio - a score as a float in the range [0, 1] 2.0*M / T
1480 T is the total number of elements in both sequences,
1481 M is the number of matches.
1482 Score - 1.0 if the sequences are identical, and
1483 0.0 if they have nothing in common.
1486 '- ' line unique to sequence 1
1487 '+ ' line unique to sequence 2
1488 ' ' line common to both sequences
1489 '? ' line not present in either input sequence
1491 fromlines
= fromText
.splitlines(1)
1492 tolines
= toText
.splitlines(1)
1493 diff
= list(difflib
.unified_diff(fromlines
, tolines
, n
=lines
))
1494 textComp
= difflib
.SequenceMatcher(None, fromText
, toText
)
1495 diffRatio
= round(textComp
.quick_ratio()*100, 2)
1496 return [diffRatio
, diff
]
1498 def __load_inventory(self
):
1499 """ Get Inventory Data for All Devices (aka switches) from the Arista CloudVision
1501 if not self
.cvp_inventory
:
1502 self
.cvp_inventory
= self
.client
.api
.get_inventory()
1503 self
.allDeviceFacts
= []
1504 for device
in self
.cvp_inventory
:
1505 self
.allDeviceFacts
.append(device
)
1507 def __get_tags(self
, name
, value
):
1508 if not self
.cvp_tags
:
1510 url
= '/api/v1/rest/analytics/tags/labels/devices/{}/value/{}/elements'.format(name
, value
)
1511 self
.logger
.debug('get_tags: URL {}'.format(url
))
1512 data
= self
.client
.get(url
, timeout
=self
.__API
_REQUEST
_TOUT
)
1513 for dev
in data
['notifications']:
1514 for elem
in dev
['updates']:
1515 self
.cvp_tags
.append(elem
)
1516 self
.logger
.debug('Available devices with tag_name {} - value {}: {} '.format(name
, value
, self
.cvp_tags
))
1518 def __get_interface_ip(self
, device_id
, interface
):
1519 url
= '/api/v1/rest/{}/Sysdb/ip/config/ipIntfConfig/{}/'.format(device_id
, interface
)
1520 self
.logger
.debug('get_interface_ip: URL {}'.format(url
))
1522 data
= self
.client
.get(url
, timeout
=self
.__API
_REQUEST
_TOUT
)
1523 return data
['notifications'][0]['updates']['addrWithMask']['value'].split('/')[0]
1525 raise SdnConnectorError("Invalid response from url {}: data {}".format(url
, data
))
1527 def __get_device_ASN(self
, device_id
):
1528 url
= '/api/v1/rest/{}/Sysdb/routing/bgp/config/'.format(device_id
)
1529 self
.logger
.debug('get_device_ASN: URL {}'.format(url
))
1531 data
= self
.client
.get(url
, timeout
=self
.__API
_REQUEST
_TOUT
)
1532 return data
['notifications'][0]['updates']['asNumber']['value']['value']['int']
1534 raise SdnConnectorError("Invalid response from url {}: data {}".format(url
, data
))
1536 def __get_peer_MLAG(self
, device_id
):
1538 url
= '/api/v1/rest/{}/Sysdb/mlag/status/'.format(device_id
)
1539 self
.logger
.debug('get_MLAG_status: URL {}'.format(url
))
1541 data
= self
.client
.get(url
, timeout
=self
.__API
_REQUEST
_TOUT
)
1542 if data
['notifications']:
1544 for notification
in data
['notifications']:
1545 for update
in notification
['updates']:
1546 if update
== 'systemId':
1547 mlagSystemId
= notification
['updates'][update
]['value']
1552 # search the MLAG System Id
1554 for s
in self
.switches
:
1555 if self
.switches
[s
]['serialNumber'] == device_id
:
1557 url
= '/api/v1/rest/{}/Sysdb/mlag/status/'.format(self
.switches
[s
]['serialNumber'])
1558 self
.logger
.debug('Searching for MLAG system id {} in switch {}'.format(mlagSystemId
, s
))
1559 data
= self
.client
.get(url
, timeout
=self
.__API
_REQUEST
_TOUT
)
1561 for notification
in data
['notifications']:
1562 for update
in notification
['updates']:
1563 if update
== 'systemId':
1564 if mlagSystemId
== notification
['updates'][update
]['value']:
1573 self
.logger
.error('No Peer device found for device {} with MLAG address {}'.format(device_id
, mlagSystemId
))
1575 self
.logger
.debug('Peer MLAG for device {} - value {}'.format(device_id
, peer
))
1578 raise SdnConnectorError("Invalid response from url {}: data {}".format(url
, data
))
1580 def is_valid_destination(self
, url
):
1581 """ Check that the provided WIM URL is correct
1583 if re
.match(self
.__regex
, url
):
1585 elif self
.is_valid_ipv4_address(url
):
1588 return self
.is_valid_ipv6_address(url
)
1590 def is_valid_ipv4_address(self
, address
):
1591 """ Checks that the given IP is IPv4 valid
1594 socket
.inet_pton(socket
.AF_INET
, address
)
1595 except AttributeError: # no inet_pton here, sorry
1597 socket
.inet_aton(address
)
1598 except socket
.error
:
1600 return address
.count('.') == 3
1601 except socket
.error
: # not a valid address
1605 def is_valid_ipv6_address(self
, address
):
1606 """ Checks that the given IP is IPv6 valid
1609 socket
.inet_pton(socket
.AF_INET6
, address
)
1610 except socket
.error
: # not a valid address
1614 def delete_keys_from_dict(self
, dict_del
, lst_keys
):
1615 if dict_del
== None:
1617 dict_copy
= {k
: v
for k
, v
in dict_del
.items() if k
not in lst_keys
}
1618 for k
, v
in dict_copy
.items():
1619 if isinstance(v
, dict):
1620 dict_copy
[k
] = self
.delete_keys_from_dict(v
, lst_keys
)