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 def __init__(self
, wim
, wim_account
, config
=None, logger
=None):
138 :param wim: (dict). Contains among others 'wim_url'
139 :param wim_account: (dict). Contains among others 'uuid' (internal id), 'name',
140 'sdn' (True if is intended for SDN-assist or False if intended for WIM), 'user', 'password'.
141 :param config: (dict or None): Particular information of plugin. These keys if present have a common meaning:
142 'mapping_not_needed': (bool) False by default or if missing, indicates that mapping is not needed.
143 'service_endpoint_mapping': (list) provides the internal endpoint mapping. The meaning is:
144 KEY meaning for WIM meaning for SDN assist
145 -------- -------- --------
146 device_id pop_switch_dpid compute_id
147 device_interface_id pop_switch_port compute_pci_address
148 service_endpoint_id wan_service_endpoint_id SDN_service_endpoint_id
149 service_mapping_info wan_service_mapping_info SDN_service_mapping_info
150 contains extra information if needed. Text in Yaml format
151 switch_dpid wan_switch_dpid SDN_switch_dpid
152 switch_port wan_switch_port SDN_switch_port
153 datacenter_id vim_account vim_account
154 id: (internal, do not use)
155 wim_id: (internal, do not use)
156 :param logger (logging.Logger): optional logger object. If none is passed 'openmano.sdn.sdnconn' is used.
158 self
.__regex
= re
.compile(
159 r
'^(?:http|ftp)s?://' # http:// or https://
160 r
'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # domain...
161 r
'localhost|' # localhost...
162 r
'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip
163 r
'(?::\d+)?', re
.IGNORECASE
) # optional port
164 self
.raiseException
= True
165 self
.logger
= logger
or logging
.getLogger(self
.__WIM
_LOGGER
)
166 super().__init
__(wim
, wim_account
, config
, self
.logger
)
168 self
.__wim
_account
= wim_account
169 self
.__config
= config
170 if self
.is_valid_destination(self
.__wim
.get("wim_url")):
171 self
.__wim
_url
= self
.__wim
.get("wim_url")
173 raise SdnConnectorError(message
='Invalid wim_url value',
175 self
.__user
= wim_account
.get("user")
176 self
.__passwd
= wim_account
.get("password")
178 self
.cvp_inventory
= None
180 self
.logger
.debug("Arista SDN plugin {}, cvprac version {}, user:{} and config:{}".
181 format(wim
, cvprac_version
, self
.__user
,
182 self
.delete_keys_from_dict(config
, ('passwd',))))
183 self
.allDeviceFacts
= []
184 self
.clC
= AristaSDNConfigLet()
187 self
.__load
_switches
()
188 except SdnConnectorError
as sc
:
190 except Exception as e
:
191 raise SdnConnectorError(message
="Unable to load switches from CVP",
192 http_code
=500) from e
194 def __load_switches(self
):
195 """ Retrieves the switches to configure in the following order
196 1. from incoming configuration:
197 1.1 using port mapping
198 using user and password from WIM
199 retrieving Lo0 and AS from switch
200 1.2 from 'switches' parameter,
201 if any parameter is not present
202 Lo0 and AS - it will be requested to the switch
203 usr and pass - from WIM configuration
204 2. Looking in the CloudVision inventory if not in configuration parameters
205 2.1 using the switches with the topology_type tag set to 'leaf'
206 2.2 using the switches whose parent container is 'leaf'
207 2.3 using the switches whose hostname contains with 'leaf'
209 All the search methods will be used
212 if self
.__config
and self
.__config
.get(self
.__SERVICE
_ENDPOINT
_MAPPING
):
213 for port
in self
.__config
.get(self
.__SERVICE
_ENDPOINT
_MAPPING
):
214 switch_dpid
= port
.get(self
.__SW
_ID
_PARAM
)
215 if switch_dpid
and switch_dpid
not in self
.switches
:
216 self
.switches
[switch_dpid
] = {'passwd': self
.__passwd
,
221 'serialNumber': None}
223 if self
.__config
and self
.__config
.get('switches'):
224 # Not directly from json, complete one by one
225 config_switches
= self
.__config
.get('switches')
226 for cs
, cs_content
in config_switches
.items():
227 if cs
not in self
.switches
:
228 self
.switches
[cs
] = {'passwd': self
.__passwd
,
233 'serialNumber': None}
235 self
.switches
[cs
].update(cs_content
)
237 # Load the rest of the data
238 if self
.client
is None:
239 self
.client
= self
.__connect
()
240 self
.__load
_inventory
()
241 if not self
.switches
:
242 self
.__get
_tags
(self
.__SWITCH
_TAG
_NAME
, self
.__SWITCH
_TAG
_VALUE
)
243 for device
in self
.allDeviceFacts
:
244 # get the switches whose container parent is 'leaf',
245 # or the topology_tag is 'leaf'
246 # or the hostname contains 'leaf'
247 if ((device
['serialNumber'] in self
.cvp_tags
) or
248 (self
.__SWITCH
_TAG
_VALUE
in device
['containerName'].lower()) or
249 (self
.__SWITCH
_TAG
_VALUE
in device
['hostname'].lower())):
250 if not self
.switches
.get(device
['hostname']):
251 switch_data
= {'passwd': self
.__passwd
,
252 'ip': device
['ipAddress'],
256 'serialNumber': None}
257 self
.switches
[device
['hostname']] = switch_data
258 if len(self
.switches
) == 0:
259 self
.logger
.error("Unable to load Leaf switches from CVP")
262 # self.switches are switch objects, one for each switch in self.switches,
263 # used to make eAPI calls by using switch.py module
264 for s
in self
.switches
:
265 for device
in self
.allDeviceFacts
:
266 if device
['hostname'] == s
:
267 if not self
.switches
[s
].get('ip'):
268 self
.switches
[s
]['ip'] = device
['ipAddress']
269 self
.switches
[s
]['serialNumber'] = device
['serialNumber']
272 # Each switch has a different loopback address,
273 # so it's a different configLet
274 if not self
.switches
[s
].get('lo0'):
275 self
.switches
[s
]["lo0"] = self
.__get
_interface
_ip
(self
.switches
[s
]['serialNumber'], self
.__LOOPBACK
_INTF
)
276 if not self
.switches
[s
].get('AS'):
277 self
.switches
[s
]["AS"] = self
.__get
_device
_ASN
(self
.switches
[s
]['serialNumber'])
278 self
.logger
.debug("Using Arista Leaf switches: {}".format(
279 self
.delete_keys_from_dict(self
.switches
, ('passwd',))))
281 def __check_service(self
, service_type
, connection_points
,
282 check_vlan
=True, check_num_cp
=True, kwargs
=None):
283 """ Reviews the connection points elements looking for semantic errors in the incoming data
285 if service_type
not in self
.__supported
_service
_types
:
286 raise Exception("The service '{}' is not supported. Only '{}' are accepted".format(
288 self
.__supported
_service
_types
))
291 if (len(connection_points
) < 2):
292 raise Exception(SdnError
.CONNECTION_POINTS_SIZE
)
293 if ((len(connection_points
) != self
.__ELINE
_num
_connection
_points
) and
294 (service_type
== self
.__service
_types
_ELINE
)):
295 raise Exception(SdnError
.CONNECTION_POINTS_SIZE
)
299 for cp
in connection_points
:
300 enc_type
= cp
.get(self
.__ENCAPSULATION
_TYPE
_PARAM
)
302 enc_type
not in self
.__supported
_encapsulation
_types
):
303 raise Exception(SdnError
.ENCAPSULATION_TYPE
)
304 encap_info
= cp
.get(self
.__ENCAPSULATION
_INFO
_PARAM
)
305 cp_vlan_id
= str(encap_info
.get(self
.__VLAN
_PARAM
))
309 elif vlan_id
!= cp_vlan_id
:
310 raise Exception(SdnError
.VLAN_INCONSISTENT
)
312 raise Exception(SdnError
.VLAN_NOT_PROVIDED
)
313 if vlan_id
in self
.__get
_srvVLANs
():
314 raise Exception('VLAN {} already assigned to a connectivity service'.format(vlan_id
))
316 # Commented out for as long as parameter isn't implemented
317 # bandwidth = kwargs.get(self.__BANDWIDTH_PARAM)
318 # if not isinstance(bandwidth, int):
319 # self.__exception(SdnError.BANDWIDTH, http_code=400)
321 # Commented out for as long as parameter isn't implemented
322 # backup = kwargs.get(self.__BACKUP_PARAM)
323 # if not isinstance(backup, bool):
324 # self.__exception(SdnError.BACKUP, http_code=400)
326 def check_credentials(self
):
327 """Retrieves the CloudVision version information, as the easiest way
328 for testing the access to CloudVision API
331 if self
.client
is None:
332 self
.client
= self
.__connect
()
333 result
= self
.client
.api
.get_cvp_info()
334 self
.logger
.debug(result
)
335 except CvpLoginError
as e
:
336 self
.logger
.info(str(e
))
338 raise SdnConnectorError(message
=SdnError
.UNAUTHORIZED
,
339 http_code
=401) from e
340 except Exception as ex
:
342 self
.logger
.error(str(ex
))
343 raise SdnConnectorError(message
=SdnError
.INTERNAL_ERROR
,
344 http_code
=500) from ex
346 def get_connectivity_service_status(self
, service_uuid
, conn_info
=None):
347 """Monitor the status of the connectivity service established
349 service_uuid (str): UUID of the connectivity service
350 conn_info (dict or None): Information returned by the connector
351 during the service creation/edition and subsequently stored in
355 dict: JSON/YAML-serializable dict that contains a mandatory key
356 ``sdn_status`` associated with one of the following values::
358 {'sdn_status': 'ACTIVE'}
359 # The service is up and running.
361 {'sdn_status': 'INACTIVE'}
362 # The service was created, but the connector
363 # cannot determine yet if connectivity exists
364 # (ideally, the caller needs to wait and check again).
366 {'sdn_status': 'DOWN'}
367 # Connection was previously established,
368 # but an error/failure was detected.
370 {'sdn_status': 'ERROR'}
371 # An error occurred when trying to create the service/
372 # establish the connectivity.
374 {'sdn_status': 'BUILD'}
375 # Still trying to create the service, the caller
376 # needs to wait and check again.
378 Additionally ``error_msg``(**str**) and ``sdn_info``(**dict**)
379 keys can be used to provide additional status explanation or
380 new information available for the connectivity service.
383 self
.logger
.debug("invoked get_connectivity_service_status '{}'".format(service_uuid
))
385 raise SdnConnectorError(message
='No connection service UUID',
388 self
.__get
_Connection
()
389 if conn_info
is None:
390 raise SdnConnectorError(message
='No connection information for service UUID {}'.format(service_uuid
),
393 if 'configLetPerSwitch' in conn_info
.keys():
397 cls_perSw
= self
.__get
_serviceData
(service_uuid
,
398 conn_info
['service_type'],
399 conn_info
['vlan_id'],
402 t_isCancelled
= False
406 for s
in self
.switches
:
407 if (len(cls_perSw
[s
]) > 0):
408 for cl
in cls_perSw
[s
]:
409 # Fix 1030 SDN-ARISTA Key error note when deploy a NS
410 # Added protection to check that 'note' exists and additionally
411 # verify that it is managed by OSM
412 if (not cls_perSw
[s
][0]['config'] or
413 not cl
.get('note') or
414 self
.__MANAGED
_BY
_OSM
not in cl
['note']):
417 t_id
= note
.split(self
.__SEPARATOR
)[1]
418 result
= self
.client
.api
.get_task_by_id(t_id
)
419 if result
['workOrderUserDefinedStatus'] == 'Completed':
421 elif result
['workOrderUserDefinedStatus'] == 'Cancelled':
423 elif result
['workOrderUserDefinedStatus'] == 'Failed':
427 failed_switches
.append(s
)
429 error_msg
= 'Some works were cancelled in switches: {}'.format(str(failed_switches
))
432 error_msg
= 'Some works failed in switches: {}'.format(str(failed_switches
))
435 error_msg
= 'Some works are still under execution in switches: {}'.format(str(failed_switches
))
439 sdn_status
= 'ACTIVE'
441 return {'sdn_status': sdn_status
,
442 'error_msg': error_msg
,
443 'sdn_info': sdn_info
}
444 except CvpLoginError
as e
:
445 self
.logger
.info(str(e
))
447 raise SdnConnectorError(message
=SdnError
.UNAUTHORIZED
,
448 http_code
=401) from e
449 except Exception as ex
:
451 self
.logger
.error(str(ex
), exc_info
=True)
452 raise SdnConnectorError(message
=str(ex
),
453 http_code
=500) from ex
455 def create_connectivity_service(self
, service_type
, connection_points
,
457 """Stablish SDN/WAN connectivity between the endpoints
459 (str): ``ELINE`` (L2), ``ELAN`` (L2), ``ETREE`` (L2), ``L3``.
460 :param connection_points: (list): each point corresponds to
461 an entry point to be connected. For WIM: from the DC
462 to the transport network.
463 For SDN: Compute/PCI to the transport network. One
464 connection point serves to identify the specific access and
465 some other service parameters, such as encapsulation type.
466 Each item of the list is a dict with:
467 "service_endpoint_id": (str)(uuid) Same meaning that for
468 'service_endpoint_mapping' (see __init__)
469 In case the config attribute mapping_not_needed is True,
470 this value is not relevant. In this case
471 it will contain the string "device_id:device_interface_id"
472 "service_endpoint_encapsulation_type": None, "dot1q", ...
473 "service_endpoint_encapsulation_info": (dict) with:
474 "vlan": ..., (int, present if encapsulation is dot1q)
475 "vni": ... (int, present if encapsulation is vxlan),
476 "peers": [(ipv4_1), (ipv4_2)] (present if
477 encapsulation is vxlan)
479 "device_id": ..., same meaning that for
480 'service_endpoint_mapping' (see __init__)
481 "device_interface_id": same meaning that for
482 'service_endpoint_mapping' (see __init__)
483 "switch_dpid": ..., present if mapping has been found
484 for this device_id,device_interface_id
485 "switch_port": ... present if mapping has been found
486 for this device_id,device_interface_id
487 "service_mapping_info": present if mapping has
488 been found for this device_id,device_interface_id
489 :param kwargs: For future versions:
490 bandwidth (int): value in kilobytes
491 latency (int): value in milliseconds
492 Other QoS might be passed as keyword arguments.
493 :return: tuple: ``(service_id, conn_info)`` containing:
494 - *service_uuid* (str): UUID of the established
496 - *conn_info* (dict or None): Information to be
497 stored at the database (or ``None``).
498 This information will be provided to the
499 :meth:`~.edit_connectivity_service` and :obj:`~.delete`.
500 **MUST** be JSON/YAML-serializable (plain data structures).
501 :raises: SdnConnectorError: In case of error. Nothing should be
502 created in this case.
503 Provide the parameter http_code
506 self
.logger
.debug("invoked create_connectivity_service '{}' ports: {}".
507 format(service_type
, connection_points
))
508 self
.__get
_Connection
()
509 self
.__check
_service
(service_type
,
513 service_uuid
= str(uuid
.uuid4())
515 self
.logger
.info("Service with uuid {} created.".
516 format(service_uuid
))
517 s_uid
, s_connInf
= self
.__processConnection
(
523 self
.__addMetadata
(s_uid
, service_type
, s_connInf
['vlan_id'])
524 except Exception as e
:
527 return (s_uid
, s_connInf
)
528 except CvpLoginError
as e
:
529 self
.logger
.info(str(e
))
531 raise SdnConnectorError(message
=SdnError
.UNAUTHORIZED
,
532 http_code
=401) from e
533 except SdnConnectorError
as sde
:
535 except Exception as ex
:
537 self
.logger
.error(str(ex
), exc_info
=True)
538 if self
.raiseException
:
540 raise SdnConnectorError(message
=str(ex
),
541 http_code
=500) from ex
543 def __processConnection(self
,
549 Invoked from creation and edit methods
551 Process the connection points array,
552 creating a set of configuration per switch where it has to be applied
553 for creating the configuration, the switches have to be queried for obtaining:
554 - the loopback address
555 - the BGP ASN (autonomous system number)
556 - the interface name of the MAC address to add in the connectivity service
557 Once the new configuration is ready, the __updateConnection method is invoked for appling the changes
563 for s
in self
.switches
:
566 vlan_processed
= False
569 processed_connection_points
= []
570 for cp
in connection_points
:
572 encap_info
= cp
.get(self
.__ENCAPSULATION
_INFO
_PARAM
)
573 if not vlan_processed
:
574 vlan_id
= str(encap_info
.get(self
.__VLAN
_PARAM
))
577 vni_id
= encap_info
.get(self
.__VNI
_PARAM
)
579 vni_id
= str(10000 + int(vlan_id
))
581 if service_type
== self
.__service
_types
_ELAN
:
582 cl_vlan
= self
.clC
.getElan_vlan(service_uuid
,
586 cl_vlan
= self
.clC
.getEline_vlan(service_uuid
,
589 vlan_processed
= True
591 encap_type
= cp
.get(self
.__ENCAPSULATION
_TYPE
_PARAM
)
592 switch_id
= encap_info
.get(self
.__SW
_ID
_PARAM
)
593 interface
= encap_info
.get(self
.__SW
_PORT
_PARAM
)
594 switches
= [{'name': switch_id
, 'interface': interface
}]
596 # remove those connections that are equal. This happens when several sriovs are located in the same
597 # compute node interface, that is, in the same switch and interface
598 switches
= [x
for x
in switches
if x
not in processed_connection_points
]
601 processed_connection_points
+= switches
602 for switch
in switches
:
604 raise SdnConnectorError(message
="Connection point switch port empty for switch_dpid {}".format(switch_id
),
606 # it should be only one switch where the mac is attached
607 if encap_type
== 'dot1q':
608 # SRIOV configLet for Leaf switch mac's attached to
609 if service_type
== self
.__service
_types
_ELAN
:
610 cl_encap
= self
.clC
.getElan_sriov(service_uuid
, interface
, vlan_id
, i
)
612 cl_encap
= self
.clC
.getEline_sriov(service_uuid
, interface
, vlan_id
, i
)
614 # PT configLet for Leaf switch attached to the mac
615 if service_type
== self
.__service
_types
_ELAN
:
616 cl_encap
= self
.clC
.getElan_passthrough(service_uuid
,
620 cl_encap
= self
.clC
.getEline_passthrough(service_uuid
,
623 if cls_cp
.get(switch
['name']):
624 cls_cp
[switch
['name']] = str(cls_cp
[switch
['name']]) + cl_encap
626 cls_cp
[switch
['name']] = cl_encap
628 # at least 1 connection point has to be received
629 if not vlan_processed
:
630 raise SdnConnectorError(message
=SdnError
.UNSUPPORTED_FEATURE
,
633 for s
in self
.switches
:
634 # for cl in cp_configLets:
635 cl_name
= (self
.__OSM
_PREFIX
+
637 self
.__SEPARATOR
+ service_type
+ str(vlan_id
) +
638 self
.__SEPARATOR
+ service_uuid
)
639 # apply VLAN and BGP configLet to all Leaf switches
640 if service_type
== self
.__service
_types
_ELAN
:
641 cl_bgp
[s
] = self
.clC
.getElan_bgp(service_uuid
,
644 self
.switches
[s
]['lo0'],
645 self
.switches
[s
]['AS'])
647 cl_bgp
[s
] = self
.clC
.getEline_bgp(service_uuid
,
650 self
.switches
[s
]['lo0'],
651 self
.switches
[s
]['AS'])
653 if not cls_cp
.get(s
):
656 cl_config
= str(cl_vlan
) + str(cl_bgp
[s
]) + str(cls_cp
[s
])
658 cls_perSw
[s
] = [{'name': cl_name
, 'config': cl_config
}]
660 allLeafConfigured
, allLeafModified
= self
.__updateConnection
(cls_perSw
)
663 "uuid": service_uuid
,
665 "service_type": service_type
,
667 "connection_points": connection_points
,
668 "configLetPerSwitch": cls_perSw
,
669 'allLeafConfigured': allLeafConfigured
,
670 'allLeafModified': allLeafModified
}
672 return service_uuid
, conn_info
673 except Exception as ex
:
674 self
.logger
.debug("Exception processing connection {}: {}".
675 format(service_uuid
, str(ex
)))
678 def __updateConnection(self
, cls_perSw
):
679 """ Invoked in the creation and modification
681 checks if the new connection points config is:
682 - already in the Cloud Vision, the configLet is modified, and applied to the switch,
683 executing the corresponding task
684 - if it has to be removed:
685 then configuration has to be removed from the switch executing the corresponding task,
686 before trying to remove the configuration
687 - created, the configuration set is created, associated to the switch, and the associated
688 task to the configLet modification executed
689 In case of any error, rollback is executed, removing the created elements, and restoring to the
693 allLeafConfigured
= {}
696 for s
in self
.switches
:
697 allLeafConfigured
[s
] = False
698 allLeafModified
[s
] = False
700 for s
in self
.switches
:
701 toDelete_in_cvp
= False
702 if not (cls_perSw
.get(s
) and cls_perSw
[s
][0].get('config')):
703 # when there is no configuration, means that there is no interface
704 # in the switch to be connected, so the configLet has to be removed from CloudVision
705 # after removing the ConfigLet fron the switch if it was already there
707 # get config let name and key
710 cvp_cl
= self
.client
.api
.get_configlet_by_name(cl
[0]['name'])
712 cl_toDelete
.append(cvp_cl
)
714 toDelete_in_cvp
= True
715 except CvpApiError
as error
:
716 if "Entity does not exist" in error
.msg
:
720 # remove configLet from device
722 res
= self
.__configlet
_modify
(cls_perSw
[s
])
723 allLeafConfigured
[s
] = res
[0]
724 if not allLeafConfigured
[s
]:
727 res
= self
.__device
_modify
(
730 delete
=toDelete_in_cvp
)
731 if "errorMessage" in str(res
):
732 raise Exception(str(res
))
733 self
.logger
.info("Device {} modify result {}".format(s
, res
))
734 for t_id
in res
[1]['tasks']:
735 if not toDelete_in_cvp
:
736 note_msg
= "{}{}{}{}##".format(self
.__MANAGED
_BY
_OSM
,
740 self
.client
.api
.add_note_to_configlet(
741 cls_perSw
[s
][0]['key'],
743 cls_perSw
[s
][0]['note'] = note_msg
744 tasks
= { t_id
: {'workOrderId': t_id
} }
745 self
.__exec
_task
(tasks
, self
.__EXC
_TASK
_EXEC
_WAIT
)
746 # with just one configLet assigned to a device,
747 # delete all if there are errors in next loops
748 if not toDelete_in_cvp
:
749 allLeafModified
[s
] = True
750 if len(cl_toDelete
) > 0:
751 self
.__configlet
_modify
(cl_toDelete
, delete
=True)
753 return allLeafConfigured
, allLeafModified
754 except Exception as ex
:
756 self
.__rollbackConnection
(cls_perSw
,
759 except Exception as e
:
760 self
.logger
.error("Exception rolling back in updating connection: {}".
761 format(e
), exc_info
=True)
764 def __rollbackConnection(self
,
768 """ Removes the given configLet from the devices and then remove the configLets
770 for s
in self
.switches
:
771 if allLeafModified
[s
]:
773 res
= self
.__device
_modify
(
775 new_configlets
=cls_perSw
[s
],
777 if "errorMessage" in str(res
):
778 raise Exception(str(res
))
780 for t_id
in res
[1]['tasks']:
781 tasks
[t_id
] = {'workOrderId': t_id
}
782 self
.__exec
_task
(tasks
)
783 self
.logger
.info("Device {} modify result {}".format(s
, res
))
784 except Exception as e
:
785 self
.logger
.error('Error removing configlets from device {}: {}'.format(s
, e
))
787 for s
in self
.switches
:
788 if allLeafConfigured
[s
]:
789 self
.__configlet
_modify
(cls_perSw
[s
], delete
=True)
791 def __exec_task(self
, tasks
, tout
=10):
792 if self
.taskC
is None:
794 data
= self
.taskC
.update_all_tasks(tasks
).values()
795 self
.taskC
.task_action(data
, tout
, 'executed')
797 def __device_modify(self
, device_to_update
, new_configlets
, delete
):
798 """ Updates the devices (switches) adding or removing the configLet,
799 the tasks Id's associated to the change are returned
801 self
.logger
.info('Enter in __device_modify delete: {}'.format(
805 # Task Ids that have been identified during device actions
808 if (len(new_configlets
) == 0 or
809 device_to_update
is None or
810 len(device_to_update
) == 0):
811 data
= {'updated': updated
, 'tasks': newTasks
}
812 return [changed
, data
]
814 self
.__load
_inventory
()
816 allDeviceFacts
= self
.allDeviceFacts
817 # Work through Devices list adding device specific information
819 for try_device
in allDeviceFacts
:
820 # Add Device Specific Configlets
821 # self.logger.debug(device)
822 if try_device
['hostname'] not in device_to_update
:
824 dev_cvp_configlets
= self
.client
.api
.get_configlets_by_device_id(
825 try_device
['systemMacAddress'])
826 # self.logger.debug(dev_cvp_configlets)
827 try_device
['deviceSpecificConfiglets'] = []
828 for cvp_configlet
in dev_cvp_configlets
:
829 if int(cvp_configlet
['containerCount']) == 0:
830 try_device
['deviceSpecificConfiglets'].append(
831 {'name': cvp_configlet
['name'],
832 'key': cvp_configlet
['key']})
833 # self.logger.debug(device)
837 # Check assigned configlets
838 device_update
= False
840 remove_configlets
= []
844 for cvp_configlet
in device
['deviceSpecificConfiglets']:
845 for cl
in new_configlets
:
846 if cvp_configlet
['name'] == cl
['name']:
847 remove_configlets
.append(cvp_configlet
)
850 for configlet
in new_configlets
:
851 if configlet
not in device
['deviceSpecificConfiglets']:
852 add_configlets
.append(configlet
)
855 update_devices
.append({'hostname': device
['hostname'],
856 'configlets': [add_configlets
,
859 self
.logger
.info("Device to modify: {}".format(update_devices
))
861 up_device
= update_devices
[0]
862 cl_toAdd
= up_device
['configlets'][0]
863 cl_toDel
= up_device
['configlets'][1]
866 if delete
and len(cl_toDel
) > 0:
867 r
= self
.client
.api
.remove_configlets_from_device(
873 self
.logger
.debug("remove_configlets_from_device {} {}".format(dev_action
, cl_toDel
))
874 elif len(cl_toAdd
) > 0:
875 r
= self
.client
.api
.apply_configlets_to_device(
881 self
.logger
.debug("apply_configlets_to_device {} {}".format(dev_action
, cl_toAdd
))
883 except Exception as error
:
884 errorMessage
= str(error
)
885 msg
= "errorMessage: Device {} Configlets couldnot be updated: {}".format(
886 up_device
['hostname'], errorMessage
)
887 raise SdnConnectorError(msg
) from error
889 if "errorMessage" in str(dev_action
):
890 m
= "Device {} Configlets update fail: {}".format(
891 up_device
['name'], dev_action
['errorMessage'])
892 raise SdnConnectorError(m
)
895 if 'taskIds' in str(dev_action
):
896 # Fix 1030 SDN-ARISTA Key error note when deploy a NS
897 if not dev_action
['data']['taskIds']:
898 raise SdnConnectorError("No taskIds found: Device {} Configlets couldnot be updated".format(
899 up_device
['hostname']))
900 for taskId
in dev_action
['data']['taskIds']:
901 updated
.append({up_device
['hostname']:
902 "Configlets-{}".format(
904 newTasks
.append(taskId
)
906 updated
.append({up_device
['hostname']:
907 "Configlets-No_Specific_Tasks"})
908 data
= {'updated': updated
, 'tasks': newTasks
}
909 return [changed
, data
]
911 def __configlet_modify(self
, configletsToApply
, delete
=False):
912 ''' adds/update or delete the provided configLets
913 :param configletsToApply: list of configLets to apply
914 :param delete: flag to indicate if the configLets have to be deleted
915 from Cloud Vision Portal
916 :return: data: dict of module actions and taskIDs
918 self
.logger
.info('Enter in __configlet_modify delete:{}'.format(
921 # Compare configlets against cvp_facts-configlets
928 for cl
in configletsToApply
:
935 cvp_cl
= self
.client
.api
.get_configlet_by_name(cl
['name'])
936 cl
['key'] = cvp_cl
['key']
937 cl
['note'] = cvp_cl
['note']
939 except CvpApiError
as error
:
940 if "Entity does not exist" in error
.msg
:
948 configlet
= {'name': cvp_cl
['name'],
952 cl_compare
= self
.__compare
(cl
['config'],
954 # compare function returns a floating point number
955 if cl_compare
[0] != 100.0:
957 configlet
= {'name': cl
['name'],
959 'config': cl
['config']}
962 configlet
= {'name': cl
['name'],
963 'key': cvp_cl
['key'],
965 'config': cl
['config']}
968 configlet
= {'name': cl
['name'],
969 'config': cl
['config']}
973 resp
= self
.client
.api
.delete_configlet(
974 configlet
['data']['name'],
975 configlet
['data']['key'])
978 resp
= self
.client
.api
.update_configlet(
980 configlet
['data']['key'],
981 configlet
['data']['name'],
985 resp
= self
.client
.api
.add_configlet(
989 operation
= 'checked'
991 except Exception as error
:
992 errorMessage
= str(error
).split(':')[-1]
993 message
= "Configlet {} cannot be {}: {}".format(
994 cl
['name'], operation
, errorMessage
)
996 deleted
.append({configlet
['name']: message
})
998 updated
.append({configlet
['name']: message
})
1000 new
.append({configlet
['name']: message
})
1002 checked
.append({configlet
['name']: message
})
1005 if "error" in str(resp
).lower():
1006 message
= "Configlet {} cannot be deleted: {}".format(
1007 cl
['name'], resp
['errorMessage'])
1009 deleted
.append({configlet
['name']: message
})
1011 updated
.append({configlet
['name']: message
})
1013 new
.append({configlet
['name']: message
})
1015 checked
.append({configlet
['name']: message
})
1019 deleted
.append({configlet
['name']: "success"})
1022 updated
.append({configlet
['name']: "success"})
1025 cl
['key'] = resp
# This key is used in API call deviceApplyConfigLet FGA
1026 new
.append({configlet
['name']: "success"})
1029 checked
.append({configlet
['name']: "success"})
1031 data
= {'new': new
, 'updated': updated
, 'deleted': deleted
, 'checked': checked
}
1032 return [changed
, data
]
1034 def __get_configletsDevices(self
, configlets
):
1035 for s
in self
.switches
:
1036 configlet
= configlets
[s
]
1037 # Add applied Devices
1038 if len(configlet
) > 0:
1039 configlet
['devices'] = []
1040 applied_devices
= self
.client
.api
.get_applied_devices(
1042 for device
in applied_devices
['data']:
1043 configlet
['devices'].append(device
['hostName'])
1045 def __get_serviceData(self
, service_uuid
, service_type
, vlan_id
, conn_info
=None):
1047 for s
in self
.switches
:
1050 srv_cls
= self
.__get
_serviceConfigLets
(service_uuid
,
1053 self
.__get
_configletsDevices
(srv_cls
)
1054 for s
in self
.switches
:
1057 for dev
in cl
['devices']:
1058 cls_perSw
[dev
].append(cl
)
1060 cls_perSw
= conn_info
['configLetPerSwitch']
1063 def delete_connectivity_service(self
, service_uuid
, conn_info
=None):
1065 Disconnect multi-site endpoints previously connected
1067 :param service_uuid: The one returned by create_connectivity_service
1068 :param conn_info: The one returned by last call to 'create_connectivity_service' or 'edit_connectivity_service'
1069 if they do not return None
1071 :raises: SdnConnectorException: In case of error. The parameter http_code must be filled
1074 self
.logger
.debug('invoked delete_connectivity_service {}'.
1075 format(service_uuid
))
1076 if not service_uuid
:
1077 raise SdnConnectorError(message
='No connection service UUID',
1080 self
.__get
_Connection
()
1081 if conn_info
is None:
1082 raise SdnConnectorError(message
='No connection information for service UUID {}'.format(service_uuid
),
1085 cls_perSw
= self
.__get
_serviceData
(service_uuid
,
1086 conn_info
['service_type'],
1087 conn_info
['vlan_id'],
1089 allLeafConfigured
= {}
1090 allLeafModified
= {}
1091 for s
in self
.switches
:
1092 allLeafConfigured
[s
] = True
1093 allLeafModified
[s
] = True
1094 found_in_cvp
= False
1095 for s
in self
.switches
:
1099 self
.__rollbackConnection
(cls_perSw
,
1103 # if the service is not defined in Cloud Vision, return a 404 - NotFound error
1104 raise SdnConnectorError(message
='Service {} was not found in Arista Cloud Vision {}'.
1105 format(service_uuid
, self
.__wim
_url
),
1107 self
.__removeMetadata
(service_uuid
)
1108 except CvpLoginError
as e
:
1109 self
.logger
.info(str(e
))
1111 raise SdnConnectorError(message
=SdnError
.UNAUTHORIZED
,
1112 http_code
=401) from e
1113 except SdnConnectorError
as sde
:
1115 except Exception as ex
:
1117 self
.logger
.error(ex
)
1118 if self
.raiseException
:
1120 raise SdnConnectorError(message
=SdnError
.INTERNAL_ERROR
,
1121 http_code
=500) from ex
1123 def __addMetadata(self
, service_uuid
, service_type
, vlan_id
):
1124 """ Adds the connectivity service from 'OSM_metadata' configLet
1126 found_in_cvp
= False
1128 cvp_cl
= self
.client
.api
.get_configlet_by_name(self
.__OSM
_METADATA
)
1130 except CvpApiError
as error
:
1131 if "Entity does not exist" in error
.msg
:
1136 new_serv
= '{} {} {} {}\n'.format(self
.__METADATA
_PREFIX
, service_type
, vlan_id
, service_uuid
)
1139 cl_config
= cvp_cl
['config'] + new_serv
1141 cl_config
= new_serv
1142 cl_meta
= [{'name': self
.__OSM
_METADATA
, 'config': cl_config
}]
1143 self
.__configlet
_modify
(cl_meta
)
1144 except Exception as e
:
1145 self
.logger
.error('Error in setting metadata in CloudVision from OSM for service {}: {}'.
1146 format(service_uuid
, str(e
)))
1149 def __removeMetadata(self
, service_uuid
):
1150 """ Removes the connectivity service from 'OSM_metadata' configLet
1152 found_in_cvp
= False
1154 cvp_cl
= self
.client
.api
.get_configlet_by_name(self
.__OSM
_METADATA
)
1156 except CvpApiError
as error
:
1157 if "Entity does not exist" in error
.msg
:
1163 if service_uuid
in cvp_cl
['config']:
1165 for line
in cvp_cl
['config'].split('\n'):
1166 if service_uuid
in line
:
1169 cl_config
= cl_config
+ line
1170 cl_meta
= [{'name': self
.__OSM
_METADATA
, 'config': cl_config
}]
1171 self
.__configlet
_modify
(cl_meta
)
1172 except Exception as e
:
1173 self
.logger
.error('Error in removing metadata in CloudVision from OSM for service {}: {}'.
1174 format(service_uuid
, str(e
)))
1177 def edit_connectivity_service(self
,
1180 connection_points
=None,
1182 """ Change an existing connectivity service.
1184 This method's arguments and return value follow the same convention as
1185 :meth:`~.create_connectivity_service`.
1187 :param service_uuid: UUID of the connectivity service.
1188 :param conn_info: (dict or None): Information previously returned
1189 by last call to create_connectivity_service
1190 or edit_connectivity_service
1191 :param connection_points: (list): If provided, the old list of
1192 connection points will be replaced.
1193 :param kwargs: Same meaning that create_connectivity_service
1194 :return: dict or None: Information to be updated and stored at
1196 When ``None`` is returned, no information should be changed.
1197 When an empty dict is returned, the database record will
1199 **MUST** be JSON/YAML-serializable (plain data structures).
1201 SdnConnectorError: In case of error.
1204 self
.logger
.debug('invoked edit_connectivity_service for service {}. ports: {}'.format(service_uuid
,
1207 if not service_uuid
:
1208 raise SdnConnectorError(message
='Unable to perform operation, missing or empty uuid',
1211 raise SdnConnectorError(message
='Unable to perform operation, missing or empty connection information',
1214 if connection_points
is None:
1217 self
.__get
_Connection
()
1219 cls_currentPerSw
= conn_info
['configLetPerSwitch']
1220 service_type
= conn_info
['service_type']
1222 self
.__check
_service
(service_type
,
1228 s_uid
, s_connInf
= self
.__processConnection
(
1233 self
.logger
.info("Service with uuid {} configuration updated".
1236 except CvpLoginError
as e
:
1237 self
.logger
.info(str(e
))
1239 raise SdnConnectorError(message
=SdnError
.UNAUTHORIZED
,
1240 http_code
=401) from e
1241 except SdnConnectorError
as sde
:
1243 except Exception as ex
:
1246 # TODO check if there are pending task, and cancel them before restoring
1247 self
.__updateConnection
(cls_currentPerSw
)
1248 except Exception as e
:
1249 self
.logger
.error("Unable to restore configuration in service {} after an error in the configuration updated: {}".
1250 format(service_uuid
, str(e
)))
1251 if self
.raiseException
:
1253 raise SdnConnectorError(message
=str(ex
),
1254 http_code
=500) from ex
1256 def clear_all_connectivity_services(self
):
1257 """ Removes all connectivity services from Arista CloudVision with two steps:
1258 - retrives all the services from Arista CloudVision
1259 - removes each service
1262 self
.logger
.debug('invoked AristaImpl ' +
1263 'clear_all_connectivity_services')
1264 self
.__get
_Connection
()
1265 s_list
= self
.__get
_srvUUIDs
()
1268 conn_info
['service_type'] = serv
['type']
1269 conn_info
['vlan_id'] = serv
['vlan']
1271 self
.delete_connectivity_service(serv
['uuid'], conn_info
)
1272 except CvpLoginError
as e
:
1273 self
.logger
.info(str(e
))
1275 raise SdnConnectorError(message
=SdnError
.UNAUTHORIZED
,
1276 http_code
=401) from e
1277 except SdnConnectorError
as sde
:
1279 except Exception as ex
:
1281 self
.logger
.error(ex
)
1282 if self
.raiseException
:
1284 raise SdnConnectorError(message
=SdnError
.INTERNAL_ERROR
,
1285 http_code
=500) from ex
1287 def get_all_active_connectivity_services(self
):
1288 """ Return the uuid of all the active connectivity services with two steps:
1289 - retrives all the services from Arista CloudVision
1290 - retrives the status of each server
1293 self
.logger
.debug('invoked AristaImpl {}'.format(
1294 'get_all_active_connectivity_services'))
1295 self
.__get
_Connection
()
1296 s_list
= self
.__get
_srvUUIDs
()
1300 conn_info
['service_type'] = serv
['type']
1301 conn_info
['vlan_id'] = serv
['vlan']
1303 status
= self
.get_connectivity_service_status(serv
['uuid'], conn_info
)
1304 if status
['sdn_status'] == 'ACTIVE':
1305 result
.append(serv
['uuid'])
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_serviceConfigLets(self
, service_uuid
, service_type
, vlan_id
):
1323 """ Return the configLet's associated with a connectivity service,
1324 There should be one, as maximum, per device (switch) for a given
1325 connectivity service
1328 for s
in self
.switches
:
1330 found_in_cvp
= False
1331 name
= (self
.__OSM
_PREFIX
+
1333 self
.__SEPARATOR
+ service_type
+ str(vlan_id
) +
1334 self
.__SEPARATOR
+ service_uuid
)
1336 cvp_cl
= self
.client
.api
.get_configlet_by_name(name
)
1338 except CvpApiError
as error
:
1339 if "Entity does not exist" in error
.msg
:
1347 def __get_srvVLANs(self
):
1348 """ Returns a list with all the VLAN id's used in the connectivity services managed
1349 in tha Arista CloudVision by checking the 'OSM_metadata' configLet where this
1350 information is stored
1352 found_in_cvp
= False
1354 cvp_cl
= self
.client
.api
.get_configlet_by_name(self
.__OSM
_METADATA
)
1356 except CvpApiError
as error
:
1357 if "Entity does not exist" in error
.msg
:
1363 lines
= cvp_cl
['config'].split('\n')
1365 if self
.__METADATA
_PREFIX
in line
:
1366 s_vlan
= line
.split(' ')[3]
1369 if (s_vlan
is not None and
1371 s_vlan
not in s_vlan_list
):
1372 s_vlan_list
.append(s_vlan
)
1376 def __get_srvUUIDs(self
):
1377 """ Retrieves all the connectivity services, managed in tha Arista CloudVision
1378 by checking the 'OSM_metadata' configLet where this information is stored
1380 found_in_cvp
= False
1382 cvp_cl
= self
.client
.api
.get_configlet_by_name(self
.__OSM
_METADATA
)
1384 except CvpApiError
as error
:
1385 if "Entity does not exist" in error
.msg
:
1391 lines
= cvp_cl
['config'].split('\n')
1393 if self
.__METADATA
_PREFIX
in line
:
1394 line
= line
.split(' ')
1395 serv
= {'uuid': line
[4], 'type': line
[2], 'vlan': line
[3]}
1398 if (serv
is not None and
1400 serv
not in serv_list
):
1401 serv_list
.append(serv
)
1405 def __get_Connection(self
):
1406 """ Open a connection with Arista CloudVision,
1407 invoking the version retrival as test
1410 if self
.client
is None:
1411 self
.client
= self
.__connect
()
1412 self
.client
.api
.get_cvp_info()
1413 except (CvpSessionLogOutError
, RequestException
) as e
:
1414 self
.logger
.debug("Connection error '{}'. Reconnecting".format(e
))
1415 self
.client
= self
.__connect
()
1416 self
.client
.api
.get_cvp_info()
1418 def __connect(self
):
1419 ''' Connects to CVP device using user provided credentials from initialization.
1420 :return: CvpClient object with connection instantiated.
1422 client
= CvpClient()
1423 protocol
, _
, rest_url
= self
.__wim
_url
.rpartition("://")
1424 host
, _
, port
= rest_url
.partition(":")
1425 if port
and port
.endswith("/"):
1426 port
= int(port
[:-1])
1432 client
.connect([host
],
1435 protocol
=protocol
or "https",
1438 client
.api
= CvpApi(client
, request_timeout
=self
.__API
_REQUEST
_TOUT
)
1439 self
.taskC
= AristaCVPTask(client
.api
)
1442 def __compare(self
, fromText
, toText
, lines
=10):
1443 """ Compare text string in 'fromText' with 'toText' and produce
1444 diffRatio - a score as a float in the range [0, 1] 2.0*M / T
1445 T is the total number of elements in both sequences,
1446 M is the number of matches.
1447 Score - 1.0 if the sequences are identical, and
1448 0.0 if they have nothing in common.
1451 '- ' line unique to sequence 1
1452 '+ ' line unique to sequence 2
1453 ' ' line common to both sequences
1454 '? ' line not present in either input sequence
1456 fromlines
= fromText
.splitlines(1)
1457 tolines
= toText
.splitlines(1)
1458 diff
= list(difflib
.unified_diff(fromlines
, tolines
, n
=lines
))
1459 textComp
= difflib
.SequenceMatcher(None, fromText
, toText
)
1460 diffRatio
= round(textComp
.quick_ratio()*100, 2)
1461 return [diffRatio
, diff
]
1463 def __load_inventory(self
):
1464 """ Get Inventory Data for All Devices (aka switches) from the Arista CloudVision
1466 if not self
.cvp_inventory
:
1467 self
.cvp_inventory
= self
.client
.api
.get_inventory()
1468 self
.allDeviceFacts
= []
1469 for device
in self
.cvp_inventory
:
1470 self
.allDeviceFacts
.append(device
)
1472 def __get_tags(self
, name
, value
):
1473 if not self
.cvp_tags
:
1475 url
= '/api/v1/rest/analytics/tags/labels/devices/{}/value/{}/elements'.format(name
, value
)
1476 self
.logger
.debug('get_tags: URL {}'.format(url
))
1477 data
= self
.client
.get(url
, timeout
=self
.__API
_REQUEST
_TOUT
)
1478 for dev
in data
['notifications']:
1479 for elem
in dev
['updates']:
1480 self
.cvp_tags
.append(elem
)
1481 self
.logger
.debug('Available devices with tag_name {} - value {}: {} '.format(name
, value
, self
.cvp_tags
))
1483 def __get_interface_ip(self
, device_id
, interface
):
1485 url
= '/api/v1/rest/{}/Sysdb/ip/config/ipIntfConfig/{}/'.format(device_id
, interface
)
1486 self
.logger
.debug('get_interface_ip: URL {}'.format(url
))
1488 data
= self
.client
.get(url
, timeout
=self
.__API
_REQUEST
_TOUT
)
1489 return data
['notifications'][0]['updates']['addrWithMask']['value'].split('/')[0]
1491 raise SdnConnectorError("Invalid response from url {}: data {}".format(url
, data
))
1493 def __get_device_ASN(self
, device_id
):
1494 url
= '/api/v1/rest/{}/Sysdb/routing/bgp/config/'.format(device_id
)
1495 self
.logger
.debug('get_device_ASN: URL {}'.format(url
))
1497 data
= self
.client
.get(url
, timeout
=self
.__API
_REQUEST
_TOUT
)
1498 return data
['notifications'][0]['updates']['asNumber']['value']['value']['int']
1500 raise SdnConnectorError("Invalid response from url {}: data {}".format(url
, data
))
1502 def is_valid_destination(self
, url
):
1503 """ Check that the provided WIM URL is correct
1505 if re
.match(self
.__regex
, url
):
1507 elif self
.is_valid_ipv4_address(url
):
1510 return self
.is_valid_ipv6_address(url
)
1512 def is_valid_ipv4_address(self
, address
):
1513 """ Checks that the given IP is IPv4 valid
1516 socket
.inet_pton(socket
.AF_INET
, address
)
1517 except AttributeError: # no inet_pton here, sorry
1519 socket
.inet_aton(address
)
1520 except socket
.error
:
1522 return address
.count('.') == 3
1523 except socket
.error
: # not a valid address
1527 def is_valid_ipv6_address(self
, address
):
1528 """ Checks that the given IP is IPv6 valid
1531 socket
.inet_pton(socket
.AF_INET6
, address
)
1532 except socket
.error
: # not a valid address
1536 def delete_keys_from_dict(self
, dict_del
, lst_keys
):
1537 if dict_del
is None:
1539 dict_copy
= {k
: v
for k
, v
in dict_del
.items() if k
not in lst_keys
}
1540 for k
, v
in dict_copy
.items():
1541 if isinstance(v
, dict):
1542 dict_copy
[k
] = self
.delete_keys_from_dict(v
, lst_keys
)