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_client_errors
import CvpLoginError
, CvpSessionLogOutError
, CvpApiError
46 from osm_rosdn_arista
.aristaSwitch
import AristaSwitch
47 from osm_rosdn_arista
.aristaConfigLet
import AristaSDNConfigLet
48 from osm_rosdn_arista
.aristaTask
import AristaCVPTask
52 UNREACHABLE
= 'Unable to reach the WIM.',
54 'VLAN value inconsistent between the connection points',
55 VLAN_NOT_PROVIDED
= 'VLAN value not provided',
56 CONNECTION_POINTS_SIZE
= \
57 'Unexpected number of connection points: 2 expected.',
58 ENCAPSULATION_TYPE
= \
59 'Unexpected service_endpoint_encapsulation_type. \
60 Only "dotq1" is accepted.',
61 BANDWIDTH
= 'Unable to get the bandwidth.',
62 STATUS
= 'Unable to get the status for the service.',
63 DELETE
= 'Unable to delete service.',
64 CLEAR_ALL
= 'Unable to clear all the services',
65 UNKNOWN_ACTION
= 'Unknown action invoked.',
66 BACKUP
= 'Unable to get the backup parameter.',
67 UNSUPPORTED_FEATURE
= "Unsupported feature",
68 UNAUTHORIZED
= "Failed while authenticating",
69 INTERNAL_ERROR
= "Internal error"
72 class AristaSdnConnector(SdnConnectorBase
):
73 """Arista class for the SDN connectors
76 wim (dict): WIM record, as stored in the database
77 wim_account (dict): WIM account record, as stored in the database
79 The arguments of the constructor are converted to object attributes.
80 An extra property, ``service_endpoint_mapping`` is created from ``config``.
82 The access to Arista CloudVision is made through the API defined in
83 https://github.com/aristanetworks/cvprac
84 The a connectivity service consist in creating a VLAN and associate the interfaces
85 of the connection points MAC addresses to this VLAN in all the switches of the topology,
86 the BDP is also configured for this VLAN.
88 The Arista Cloud Vision API workflow is the following
89 -- The switch configuration is defined as a set of switch configuration commands,
90 what is called 'ConfigLet'
91 -- The ConfigLet is associated to the device (leaf switch)
92 -- Automatically a task is associated to this activity for change control, the task
93 in this stage is in 'Pending' state
94 -- The task will be executed so that the configuration is applied to the switch.
95 -- The service information is saved in the response of the creation call
96 -- All created services identification is stored in a generic ConfigLet 'OSM_metadata'
97 to keep track of the managed resources by OSM in the Arista deployment.
99 __supported_service_types
= ["ELINE (L2)", "ELINE", "ELAN"]
100 __service_types_ELAN
= "ELAN"
101 __service_types_ELINE
= "ELINE"
102 __ELINE_num_connection_points
= 2
103 __supported_service_types
= ["ELINE", "ELAN"]
104 __supported_encapsulation_types
= ["dot1q"]
105 __WIM_LOGGER
= 'openmano.sdnconn.arista'
106 __ENCAPSULATION_TYPE_PARAM
= "service_endpoint_encapsulation_type"
107 __ENCAPSULATION_INFO_PARAM
= "service_endpoint_encapsulation_info"
108 __BACKUP_PARAM
= "backup"
109 __BANDWIDTH_PARAM
= "bandwidth"
110 __SERVICE_ENDPOINT_PARAM
= "service_endpoint_id"
112 __WAN_SERVICE_ENDPOINT_PARAM
= "service_endpoint_id"
113 __WAN_MAPPING_INFO_PARAM
= "service_mapping_info"
114 __DEVICE_ID_PARAM
= "device_id"
115 __DEVICE_INTERFACE_ID_PARAM
= "device_interface_id"
116 __SW_ID_PARAM
= "switch_dpid"
117 __SW_PORT_PARAM
= "switch_port"
118 __VLAN_PARAM
= "vlan"
121 __MANAGED_BY_OSM
= '## Managed by OSM '
122 __OSM_PREFIX
= "osm_"
123 __OSM_METADATA
= "OSM_metadata"
124 __METADATA_PREFIX
= '!## Service'
125 __EXC_TASK_EXEC_WAIT
= 1
126 __ROLLB_TASK_EXEC_WAIT
= 5
128 def __init__(self
, wim
, wim_account
, config
=None, logger
=None):
131 :param wim: (dict). Contains among others 'wim_url'
132 :param wim_account: (dict). Contains among others 'uuid' (internal id), 'name',
133 'sdn' (True if is intended for SDN-assist or False if intended for WIM), 'user', 'password'.
134 :param config: (dict or None): Particular information of plugin. These keys if present have a common meaning:
135 'mapping_not_needed': (bool) False by default or if missing, indicates that mapping is not needed.
136 'service_endpoint_mapping': (list) provides the internal endpoint mapping. The meaning is:
137 KEY meaning for WIM meaning for SDN assist
138 -------- -------- --------
139 device_id pop_switch_dpid compute_id
140 device_interface_id pop_switch_port compute_pci_address
141 service_endpoint_id wan_service_endpoint_id SDN_service_endpoint_id
142 service_mapping_info wan_service_mapping_info SDN_service_mapping_info
143 contains extra information if needed. Text in Yaml format
144 switch_dpid wan_switch_dpid SDN_switch_dpid
145 switch_port wan_switch_port SDN_switch_port
146 datacenter_id vim_account vim_account
147 id: (internal, do not use)
148 wim_id: (internal, do not use)
149 :param logger (logging.Logger): optional logger object. If none is passed 'openmano.sdn.sdnconn' is used.
151 self
.__regex
= re
.compile(
152 r
'^(?:http|ftp)s?://' # http:// or https://
153 r
'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # domain...
154 r
'localhost|' # localhost...
155 r
'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip
156 r
'(?::\d+)?', re
.IGNORECASE
) # optional port
157 self
.raiseException
= True
158 self
.logger
= logger
or logging
.getLogger(self
.__WIM
_LOGGER
)
159 super().__init
__(wim
, wim_account
, config
, self
.logger
)
161 self
.__wim
_account
= wim_account
162 self
.__config
= config
163 if self
.is_valid_destination(self
.__wim
.get("wim_url")):
164 self
.__wim
_url
= self
.__wim
.get("wim_url")
166 raise SdnConnectorError(message
='Invalid wim_url value',
168 self
.__user
= wim_account
.get("user")
169 self
.__passwd
= wim_account
.get("password")
171 self
.cvp_inventory
= None
172 self
.logger
.debug("Arista SDN assist {}, user:{} and conf:{}".
173 format(wim
, self
.__user
, config
))
174 self
.allDeviceFacts
= []
175 self
.__load
_switches
()
176 self
.clC
= AristaSDNConfigLet()
178 self
.sw_loopback0
= {}
182 # Each switch has a different loopback address,
183 # so it's a different configLet
184 inf
= self
.__get
_switch
_interface
_ip
(s
, 'Loopback0')
185 self
.sw_loopback0
[s
] = inf
.split('/')[0]
186 self
.bgp
[s
] = self
.__get
_switch
_asn
(s
)
188 def __load_switches(self
):
189 """ Retrieves the switches to configure in the following order
190 1.- from incomming configuration
191 2.- Looking in the CloudVision inventory for those switches whose hostname starts with 'leaf'
193 if not self
.__config
or not self
.__config
.get('switches'):
194 if self
.client
is None:
195 self
.client
= self
.__connect
()
196 self
.__load
_inventory
()
198 for device
in self
.allDeviceFacts
:
199 if device
['hostname'].startswith('Leaf'):
200 switch_data
= {"passwd": self
.__passwd
,
201 "ip": device
['ipAddress'],
203 self
.switches
[device
['hostname']] = switch_data
204 if len(self
.switches
) == 0:
205 self
.logger
.error("Unable to load Leaf switches from CVP")
209 self
.switches
= self
.__config
.get
['switches']
210 # self.s_api are switch objects, one for each switch in self.switches,
211 # used to make eAPI calls by using switch.py module
213 for s
in self
.switches
:
214 self
.logger
.debug("Using Arista Leaf switch: {} {} {}".format(
216 self
.switches
[s
]["ip"],
217 self
.switches
[s
]["usr"]))
218 if self
.is_valid_destination(self
.switches
[s
]["ip"]):
219 self
.s_api
[s
] = AristaSwitch(host
=self
.switches
[s
]["ip"],
220 user
=self
.switches
[s
]["usr"],
221 passwd
=self
.switches
[s
]["passwd"],
224 def __lldp_find_neighbor(self
, tlv_name
=None, tlv_value
=None):
225 """Returns a list of dicts where a mathing LLDP neighbor has been found
227 switch -> switch name
228 interface -> switch interface
233 # Get LLDP info from each switch
235 result
= self
.s_api
[s
].run("show lldp neighbors detail")
236 lldp_info
[s
] = result
[0]["lldpNeighbors"]
237 # Look LLDP match on each interface
238 # Note that eAPI returns [] for an interface with no LLDP neighbors
239 # in the corresponding interface lldpNeighborInfo field
240 for interface
in lldp_info
[s
]:
241 if lldp_info
[s
][interface
]["lldpNeighborInfo"]:
242 lldp_nInf
= lldp_info
[s
][interface
]["lldpNeighborInfo"][0]
243 if tlv_name
in lldp_nInf
:
244 if lldp_nInf
[tlv_name
] == tlv_value
:
245 r
.append({"name": s
, "interface": interface
})
249 def __get_switch_asn(self
, switch
):
250 """Returns switch ASN in default VRF
252 bgp_info
= self
.s_api
[switch
].run("show ip bgp summary")[0]
253 return(bgp_info
["vrfs"]["default"]["asn"])
255 def __get_switch_po(self
, switch
, interface
=None):
256 """Returns Port-Channels for a given interface
257 If interface is None returns a list with all PO interfaces
258 Note that if specified, interface should be exact name
259 for instance: Ethernet3 and not e3 eth3 and so on
261 po_inf
= self
.s_api
[switch
].run("show port-channel")[0]["portChannels"]
264 r
= [x
for x
in po_inf
if interface
in po_inf
[x
]["activePorts"]]
270 def __get_switch_interface_ip(self
, switch
, interface
=None):
271 """Returns interface primary ip
272 interface should be exact name
273 for instance: Ethernet3 and not ethernet 3, e3 eth3 and so on
275 cmd
= "show ip interface {}".format(interface
)
276 ip_info
= self
.s_api
[switch
].run(cmd
)[0]["interfaces"][interface
]
278 ip
= ip_info
["interfaceAddress"]["primaryIp"]["address"]
279 mask
= ip_info
["interfaceAddress"]["primaryIp"]["maskLen"]
281 return "{}/{}".format(ip
, mask
)
283 def __check_service(self
, service_type
, connection_points
,
284 check_vlan
=True, check_num_cp
=True, kwargs
=None):
285 """ Reviews the connection points elements looking for semantic errors in the incoming data
287 if service_type
not in self
.__supported
_service
_types
:
288 raise Exception("The service '{}' is not supported. Only '{}' are accepted".format(
290 self
.__supported
_service
_types
))
293 if (len(connection_points
) < 2):
294 raise Exception(SdnError
.CONNECTION_POINTS_SIZE
)
295 if ((len(connection_points
) != self
.__ELINE
_num
_connection
_points
) and
296 (service_type
== self
.__service
_types
_ELINE
)):
297 raise Exception(SdnError
.CONNECTION_POINTS_SIZE
)
301 for cp
in connection_points
:
302 enc_type
= cp
.get(self
.__ENCAPSULATION
_TYPE
_PARAM
)
304 enc_type
not in self
.__supported
_encapsulation
_types
):
305 raise Exception(SdnError
.ENCAPSULATION_TYPE
)
306 encap_info
= cp
.get(self
.__ENCAPSULATION
_INFO
_PARAM
)
307 cp_vlan_id
= str(encap_info
.get(self
.__VLAN
_PARAM
))
311 elif vlan_id
!= cp_vlan_id
:
312 raise Exception(SdnError
.VLAN_INCONSISTENT
)
314 raise Exception(SdnError
.VLAN_NOT_PROVIDED
)
315 if vlan_id
in self
.__get
_srvVLANs
():
316 raise Exception('VLAN {} already assigned to a connectivity service'.format(vlan_id
))
318 # Commented out for as long as parameter isn't implemented
319 # bandwidth = kwargs.get(self.__BANDWIDTH_PARAM)
320 # if not isinstance(bandwidth, int):
321 # self.__exception(SdnError.BANDWIDTH, http_code=400)
323 # Commented out for as long as parameter isn't implemented
324 # backup = kwargs.get(self.__BACKUP_PARAM)
325 # if not isinstance(backup, bool):
326 # self.__exception(SdnError.BACKUP, http_code=400)
328 def check_credentials(self
):
329 """Retrieves the CloudVision version information, as the easiest way
330 for testing the access to CloudVision API
333 if self
.client
is None:
334 self
.client
= self
.__connect
()
335 result
= self
.client
.api
.get_cvp_info()
336 self
.logger
.debug(result
)
337 except CvpLoginError
as e
:
338 self
.logger
.info(str(e
))
340 raise SdnConnectorError(message
=SdnError
.UNAUTHORIZED
,
341 http_code
=401) from e
342 except Exception as ex
:
344 self
.logger
.error(str(ex
))
345 raise SdnConnectorError(message
=SdnError
.INTERNAL_ERROR
,
346 http_code
=500) from ex
348 def get_connectivity_service_status(self
, service_uuid
, conn_info
=None):
349 """Monitor the status of the connectivity service established
351 service_uuid (str): UUID of the connectivity service
352 conn_info (dict or None): Information returned by the connector
353 during the service creation/edition and subsequently stored in
357 dict: JSON/YAML-serializable dict that contains a mandatory key
358 ``sdn_status`` associated with one of the following values::
360 {'sdn_status': 'ACTIVE'}
361 # The service is up and running.
363 {'sdn_status': 'INACTIVE'}
364 # The service was created, but the connector
365 # cannot determine yet if connectivity exists
366 # (ideally, the caller needs to wait and check again).
368 {'sdn_status': 'DOWN'}
369 # Connection was previously established,
370 # but an error/failure was detected.
372 {'sdn_status': 'ERROR'}
373 # An error occurred when trying to create the service/
374 # establish the connectivity.
376 {'sdn_status': 'BUILD'}
377 # Still trying to create the service, the caller
378 # needs to wait and check again.
380 Additionally ``error_msg``(**str**) and ``sdn_info``(**dict**)
381 keys can be used to provide additional status explanation or
382 new information available for the connectivity service.
386 raise SdnConnectorError(message
='No connection service UUID',
389 self
.__get
_Connection
()
390 if conn_info
is None:
391 raise SdnConnectorError(message
='No connection information for service UUID {}'.format(service_uuid
),
394 if 'configLetPerSwitch' in conn_info
.keys():
398 cls_perSw
= self
.__get
_serviceData
(service_uuid
,
399 conn_info
['service_type'],
400 conn_info
['vlan_id'],
403 t_isCancelled
= False
408 if (len(cls_perSw
[s
]) > 0):
409 for cl
in cls_perSw
[s
]:
410 # Fix 1030 SDN-ARISTA Key error note when deploy a NS
411 # Added protection to check that 'note' exists and additionally
412 # verify that it is managed by OSM
413 if (not cls_perSw
[s
][0]['config'] or
414 not cl
.get('note') or
415 self
.__MANAGED
_BY
_OSM
not in cl
['note']):
418 t_id
= note
.split(self
.__SEPARATOR
)[1]
419 result
= self
.client
.api
.get_task_by_id(t_id
)
420 if result
['workOrderUserDefinedStatus'] == 'Completed':
422 elif result
['workOrderUserDefinedStatus'] == 'Cancelled':
424 elif result
['workOrderUserDefinedStatus'] == 'Failed':
428 failed_switches
.append(s
)
430 error_msg
= 'Some works were cancelled in switches: {}'.format(str(failed_switches
))
433 error_msg
= 'Some works failed in switches: {}'.format(str(failed_switches
))
436 error_msg
= 'Some works are still under execution in switches: {}'.format(str(failed_switches
))
440 sdn_status
= 'ACTIVE'
442 return {'sdn_status': sdn_status
,
443 'error_msg': error_msg
,
444 'sdn_info': sdn_info
}
445 except CvpLoginError
as e
:
446 self
.logger
.info(str(e
))
448 raise SdnConnectorError(message
=SdnError
.UNAUTHORIZED
,
449 http_code
=401) from e
450 except Exception as ex
:
452 self
.logger
.error(str(ex
), exc_info
=True)
453 raise SdnConnectorError(message
=str(ex
),
454 http_code
=500) from ex
456 def create_connectivity_service(self
, service_type
, connection_points
,
458 """Stablish SDN/WAN connectivity between the endpoints
460 (str): ``ELINE`` (L2), ``ELAN`` (L2), ``ETREE`` (L2), ``L3``.
461 :param connection_points: (list): each point corresponds to
462 an entry point to be connected. For WIM: from the DC
463 to the transport network.
464 For SDN: Compute/PCI to the transport network. One
465 connection point serves to identify the specific access and
466 some other service parameters, such as encapsulation type.
467 Each item of the list is a dict with:
468 "service_endpoint_id": (str)(uuid) Same meaning that for
469 'service_endpoint_mapping' (see __init__)
470 In case the config attribute mapping_not_needed is True,
471 this value is not relevant. In this case
472 it will contain the string "device_id:device_interface_id"
473 "service_endpoint_encapsulation_type": None, "dot1q", ...
474 "service_endpoint_encapsulation_info": (dict) with:
475 "vlan": ..., (int, present if encapsulation is dot1q)
476 "vni": ... (int, present if encapsulation is vxlan),
477 "peers": [(ipv4_1), (ipv4_2)] (present if
478 encapsulation is vxlan)
480 "device_id": ..., same meaning that for
481 'service_endpoint_mapping' (see __init__)
482 "device_interface_id": same meaning that for
483 'service_endpoint_mapping' (see __init__)
484 "switch_dpid": ..., present if mapping has been found
485 for this device_id,device_interface_id
486 "swith_port": ... present if mapping has been found
487 for this device_id,device_interface_id
488 "service_mapping_info": present if mapping has
489 been found for this device_id,device_interface_id
490 :param kwargs: For future versions:
491 bandwidth (int): value in kilobytes
492 latency (int): value in milliseconds
493 Other QoS might be passed as keyword arguments.
494 :return: tuple: ``(service_id, conn_info)`` containing:
495 - *service_uuid* (str): UUID of the established
497 - *conn_info* (dict or None): Information to be
498 stored at the database (or ``None``).
499 This information will be provided to the
500 :meth:`~.edit_connectivity_service` and :obj:`~.delete`.
501 **MUST** be JSON/YAML-serializable (plain data structures).
502 :raises: SdnConnectorError: In case of error. Nothing should be
503 created in this case.
504 Provide the parameter http_code
507 self
.logger
.debug("invoked create_connectivity_service '{}' ports: {}".
508 format(service_type
, connection_points
))
509 self
.__get
_Connection
()
510 self
.__check
_service
(service_type
,
514 service_uuid
= str(uuid
.uuid4())
516 self
.logger
.info("Service with uuid {} created.".
517 format(service_uuid
))
518 s_uid
, s_connInf
= self
.__processConnection
(
524 self
.__addMetadata
(s_uid
, service_type
, s_connInf
['vlan_id'])
525 except Exception as e
:
528 return (s_uid
, s_connInf
)
529 except CvpLoginError
as e
:
530 self
.logger
.info(str(e
))
532 raise SdnConnectorError(message
=SdnError
.UNAUTHORIZED
,
533 http_code
=401) from e
534 except SdnConnectorError
as sde
:
536 except Exception as ex
:
538 self
.logger
.error(str(ex
), exc_info
=True)
539 if self
.raiseException
:
541 raise SdnConnectorError(message
=str(ex
),
542 http_code
=500) from ex
544 def __processConnection(self
,
550 Invoked from creation and edit methods
552 Process the connection points array,
553 creating a set of configuration per switch where it has to be applied
554 for creating the configuration, the switches have to be queried for obtaining:
555 - the loopback address
556 - the BGP ASN (autonomous system number)
557 - the interface name of the MAC address to add in the connectivity service
558 Once the new configuration is ready, the __updateConnection method is invoked for appling the changes
567 vlan_processed
= False
570 processed_connection_points
= []
571 for cp
in connection_points
:
573 encap_info
= cp
.get(self
.__ENCAPSULATION
_INFO
_PARAM
)
574 if not vlan_processed
:
575 vlan_id
= str(encap_info
.get(self
.__VLAN
_PARAM
))
578 vni_id
= encap_info
.get(self
.__VNI
_PARAM
)
580 vni_id
= str(10000 + int(vlan_id
))
582 if service_type
== self
.__service
_types
_ELAN
:
583 cl_vlan
= self
.clC
.getElan_vlan(service_uuid
,
587 cl_vlan
= self
.clC
.getEline_vlan(service_uuid
,
590 vlan_processed
= True
592 encap_type
= cp
.get(self
.__ENCAPSULATION
_TYPE
_PARAM
)
593 switch_id
= encap_info
.get(self
.__SW
_ID
_PARAM
)
595 point_mac
= encap_info
.get(self
.__MAC
_PARAM
)
596 switches
= self
.__lldp
_find
_neighbor
("chassisId", point_mac
)
597 self
.logger
.debug("Found connection point for MAC {}: {}".
598 format(point_mac
, switches
))
600 interface
= encap_info
.get(self
.__SW
_PORT
_PARAM
)
601 switches
= [{'name': switch_id
, 'interface': interface
}]
603 if len(switches
) == 0:
604 raise SdnConnectorError(message
="Connection point MAC address {} not found in the switches".format(point_mac
),
607 # remove those connections that are equal. This happens when several sriovs are located in the same
608 # compute node interface, that is, in the same switch and interface
609 switches
= [x
for x
in switches
if x
not in processed_connection_points
]
612 processed_connection_points
+= switches
613 for switch
in switches
:
615 port_channel
= self
.__get
_switch
_po
(switch
['name'],
617 if len(port_channel
) > 0:
618 interface
= port_channel
[0]
620 interface
= switch
['interface']
622 raise SdnConnectorError(message
="Connection point switch port empty for switch_dpid {}".format(switch_id
),
624 # it should be only one switch where the mac is attached
625 if encap_type
== 'dot1q':
626 # SRIOV configLet for Leaf switch mac's attached to
627 if service_type
== self
.__service
_types
_ELAN
:
628 cl_encap
= self
.clC
.getElan_sriov(service_uuid
, interface
, vlan_id
, i
)
630 cl_encap
= self
.clC
.getEline_sriov(service_uuid
, interface
, vlan_id
, i
)
632 # PT configLet for Leaf switch attached to the mac
633 if service_type
== self
.__service
_types
_ELAN
:
634 cl_encap
= self
.clC
.getElan_passthrough(service_uuid
,
638 cl_encap
= self
.clC
.getEline_passthrough(service_uuid
,
641 if cls_cp
.get(switch
['name']):
642 cls_cp
[switch
['name']] = str(cls_cp
[switch
['name']]) + cl_encap
644 cls_cp
[switch
['name']] = cl_encap
646 # at least 1 connection point has to be received
647 if not vlan_processed
:
648 raise SdnConnectorError(message
=SdnError
.UNSUPPORTED_FEATURE
,
652 # for cl in cp_configLets:
653 cl_name
= (self
.__OSM
_PREFIX
+
655 self
.__SEPARATOR
+ service_type
+ str(vlan_id
) +
656 self
.__SEPARATOR
+ service_uuid
)
657 # apply VLAN and BGP configLet to all Leaf switches
658 if service_type
== self
.__service
_types
_ELAN
:
659 cl_bgp
[s
] = self
.clC
.getElan_bgp(service_uuid
,
662 self
.sw_loopback0
[s
],
665 cl_bgp
[s
] = self
.clC
.getEline_bgp(service_uuid
,
668 self
.sw_loopback0
[s
],
671 if not cls_cp
.get(s
):
674 cl_config
= str(cl_vlan
) + str(cl_bgp
[s
]) + str(cls_cp
[s
])
676 cls_perSw
[s
] = [{'name': cl_name
, 'config': cl_config
}]
678 allLeafConfigured
, allLeafModified
= self
.__updateConnection
(cls_perSw
)
681 "uuid": service_uuid
,
683 "service_type": service_type
,
685 "connection_points": connection_points
,
686 "configLetPerSwitch": cls_perSw
,
687 'allLeafConfigured': allLeafConfigured
,
688 'allLeafModified': allLeafModified
}
690 return service_uuid
, conn_info
691 except Exception as ex
:
692 self
.logger
.debug("Exception processing connection {}: {}".
693 format(service_uuid
, str(ex
)))
696 def __updateConnection(self
, cls_perSw
):
697 """ Invoked in the creation and modification
699 checks if the new connection points config is:
700 - already in the Cloud Vision, the configLet is modified, and applied to the switch,
701 executing the corresponding task
702 - if it has to be removed:
703 then configuration has to be removed from the switch executing the corresponding task,
704 before trying to remove the configuration
705 - created, the configuration set is created, associated to the switch, and the associated
706 task to the configLet modification executed
707 In case of any error, rollback is executed, removing the created elements, and restoring to the
711 allLeafConfigured
= {}
715 allLeafConfigured
[s
] = False
716 allLeafModified
[s
] = False
720 toDelete_in_cvp
= False
721 if not (cls_perSw
.get(s
) and cls_perSw
[s
][0].get('config')):
722 # when there is no configuration, means that there is no interface
723 # in the switch to be connected, so the configLet has to be removed from CloudVision
724 # after removing the ConfigLet fron the switch if it was already there
726 # get config let name and key
729 cvp_cl
= self
.client
.api
.get_configlet_by_name(cl
[0]['name'])
731 cl_toDelete
.append(cvp_cl
)
733 toDelete_in_cvp
= True
734 except CvpApiError
as error
:
735 if "Entity does not exist" in error
.msg
:
739 # remove configLet from device
741 res
= self
.__configlet
_modify
(cls_perSw
[s
])
742 allLeafConfigured
[s
] = res
[0]
743 if not allLeafConfigured
[s
]:
746 res
= self
.__device
_modify
(
749 delete
=toDelete_in_cvp
)
750 if "errorMessage" in str(res
):
751 raise Exception(str(res
))
752 self
.logger
.info("Device {} modify result {}".format(s
, res
))
753 for t_id
in res
[1]['tasks']:
754 tasks
[t_id
] = {'workOrderId': t_id
}
755 if not toDelete_in_cvp
:
756 note_msg
= "{}{}{}{}##".format(self
.__MANAGED
_BY
_OSM
,
760 self
.client
.api
.add_note_to_configlet(
761 cls_perSw
[s
][0]['key'],
763 cls_perSw
[s
][0].update([('note', note_msg
)])
764 # with just one configLet assigned to a device,
765 # delete all if there are errors in next loops
766 if not toDelete_in_cvp
:
767 allLeafModified
[s
] = True
768 if self
.taskC
is None:
770 data
= self
.taskC
.update_all_tasks(tasks
).values()
771 self
.taskC
.task_action(data
,
772 self
.__EXC
_TASK
_EXEC
_WAIT
,
774 if len(cl_toDelete
) > 0:
775 self
.__configlet
_modify
(cl_toDelete
, delete
=True)
776 return allLeafConfigured
, allLeafModified
777 except Exception as ex
:
779 self
.__rollbackConnection
(cls_perSw
,
780 allLeafConfigured
=True,
781 allLeafModified
=True)
782 except Exception as e
:
783 self
.logger
.info("Exception rolling back in updating connection: {}".
787 def __rollbackConnection(self
,
791 """ Removes the given configLet from the devices and then remove the configLets
795 if allLeafModified
[s
]:
797 res
= self
.__device
_modify
(
799 new_configlets
=cls_perSw
[s
],
801 if "errorMessage" in str(res
):
802 raise Exception(str(res
))
803 for t_id
in res
[1]['tasks']:
804 tasks
[t_id
] = {'workOrderId': t_id
}
805 self
.logger
.info("Device {} modify result {}".format(s
, res
))
806 except Exception as e
:
807 self
.logger
.info('Error removing configlets from device {}: {}'.format(s
, e
))
809 if self
.taskC
is None:
811 data
= self
.taskC
.update_all_tasks(tasks
).values()
812 self
.taskC
.task_action(data
,
813 self
.__ROLLB
_TASK
_EXEC
_WAIT
,
816 if allLeafConfigured
[s
]:
817 self
.__configlet
_modify
(cls_perSw
[s
], delete
=True)
819 def __device_modify(self
, device_to_update
, new_configlets
, delete
):
820 """ Updates the devices (switches) adding or removing the configLet,
821 the tasks Id's associated to the change are returned
823 self
.logger
.info('Enter in __device_modify delete: {}'.format(
827 # Task Ids that have been identified during device actions
830 if (len(new_configlets
) == 0 or
831 device_to_update
is None or
832 len(device_to_update
) == 0):
833 data
= {'updated': updated
, 'tasks': newTasks
}
834 return [changed
, data
]
836 self
.__load
_inventory
()
838 allDeviceFacts
= self
.allDeviceFacts
839 # Work through Devices list adding device specific information
841 for try_device
in allDeviceFacts
:
842 # Add Device Specific Configlets
843 # self.logger.debug(device)
844 if try_device
['hostname'] not in device_to_update
:
846 dev_cvp_configlets
= self
.client
.api
.get_configlets_by_device_id(
847 try_device
['systemMacAddress'])
848 # self.logger.debug(dev_cvp_configlets)
849 try_device
['deviceSpecificConfiglets'] = []
850 for cvp_configlet
in dev_cvp_configlets
:
851 if int(cvp_configlet
['containerCount']) == 0:
852 try_device
['deviceSpecificConfiglets'].append(
853 {'name': cvp_configlet
['name'],
854 'key': cvp_configlet
['key']})
855 # self.logger.debug(device)
859 # Check assigned configlets
860 device_update
= False
862 remove_configlets
= []
866 for cvp_configlet
in device
['deviceSpecificConfiglets']:
867 for cl
in new_configlets
:
868 if cvp_configlet
['name'] == cl
['name']:
869 remove_configlets
.append(cvp_configlet
)
872 for configlet
in new_configlets
:
873 if configlet
not in device
['deviceSpecificConfiglets']:
874 add_configlets
.append(configlet
)
877 update_devices
.append({'hostname': device
['hostname'],
878 'configlets': [add_configlets
,
881 self
.logger
.info("Device to modify: {}".format(update_devices
))
883 up_device
= update_devices
[0]
884 cl_toAdd
= up_device
['configlets'][0]
885 cl_toDel
= up_device
['configlets'][1]
888 if delete
and len(cl_toDel
) > 0:
889 r
= self
.client
.api
.remove_configlets_from_device(
895 self
.logger
.debug("remove_configlets_from_device {} {}".format(dev_action
, cl_toDel
))
896 elif len(cl_toAdd
) > 0:
897 r
= self
.client
.api
.apply_configlets_to_device(
903 self
.logger
.debug("apply_configlets_to_device {} {}".format(dev_action
, cl_toAdd
))
905 except Exception as error
:
906 errorMessage
= str(error
)
907 msg
= "errorMessage: Device {} Configlets couldnot be updated: {}".format(
908 up_device
['hostname'], errorMessage
)
909 raise SdnConnectorError(msg
) from error
911 if "errorMessage" in str(dev_action
):
912 m
= "Device {} Configlets update fail: {}".format(
913 up_device
['name'], dev_action
['errorMessage'])
914 raise SdnConnectorError(m
)
917 if 'taskIds' in str(dev_action
):
918 # Fix 1030 SDN-ARISTA Key error note when deploy a NS
919 if not dev_action
['data']['taskIds']:
920 raise Exception("No taskIds found: Device {} Configlets couldnot be updated".format(
921 up_device
['hostname']))
922 for taskId
in dev_action
['data']['taskIds']:
923 updated
.append({up_device
['hostname']:
924 "Configlets-{}".format(
926 newTasks
.append(taskId
)
928 updated
.append({up_device
['hostname']:
929 "Configlets-No_Specific_Tasks"})
930 data
= {'updated': updated
, 'tasks': newTasks
}
931 return [changed
, data
]
933 def __configlet_modify(self
, configletsToApply
, delete
=False):
934 ''' adds/update or delete the provided configLets
935 :param configletsToApply: list of configLets to apply
936 :param delete: flag to indicate if the configLets have to be deleted
937 from Cloud Vision Portal
938 :return: data: dict of module actions and taskIDs
940 self
.logger
.info('Enter in __configlet_modify delete:{}'.format(
943 # Compare configlets against cvp_facts-configlets
950 for cl
in configletsToApply
:
957 cvp_cl
= self
.client
.api
.get_configlet_by_name(cl
['name'])
958 cl
['key'] = cvp_cl
['key']
959 cl
['note'] = cvp_cl
['note']
961 except CvpApiError
as error
:
962 if "Entity does not exist" in error
.msg
:
970 configlet
= {'name': cvp_cl
['name'],
974 cl_compare
= self
.__compare
(cl
['config'],
976 # compare function returns a floating point number
977 if cl_compare
[0] != 100.0:
979 configlet
= {'name': cl
['name'],
981 'config': cl
['config']}
984 configlet
= {'name': cl
['name'],
985 'key': cvp_cl
['key'],
987 'config': cl
['config']}
990 configlet
= {'name': cl
['name'],
991 'config': cl
['config']}
995 resp
= self
.client
.api
.delete_configlet(
996 configlet
['data']['name'],
997 configlet
['data']['key'])
1000 resp
= self
.client
.api
.update_configlet(
1001 configlet
['config'],
1002 configlet
['data']['key'],
1003 configlet
['data']['name'])
1005 operation
= 'create'
1006 resp
= self
.client
.api
.add_configlet(
1008 configlet
['config'])
1010 operation
= 'checked'
1012 except Exception as error
:
1013 errorMessage
= str(error
).split(':')[-1]
1014 message
= "Configlet {} cannot be {}: {}".format(
1015 cl
['name'], operation
, errorMessage
)
1017 deleted
.append({configlet
['name']: message
})
1019 updated
.append({configlet
['name']: message
})
1021 new
.append({configlet
['name']: message
})
1023 checked
.append({configlet
['name']: message
})
1026 if "error" in str(resp
).lower():
1027 message
= "Configlet {} cannot be deleted: {}".format(
1028 cl
['name'], resp
['errorMessage'])
1030 deleted
.append({configlet
['name']: message
})
1032 updated
.append({configlet
['name']: message
})
1034 new
.append({configlet
['name']: message
})
1036 checked
.append({configlet
['name']: message
})
1040 deleted
.append({configlet
['name']: "success"})
1043 updated
.append({configlet
['name']: "success"})
1046 cl
['key'] = resp
# This key is used in API call deviceApplyConfigLet FGA
1047 new
.append({configlet
['name']: "success"})
1050 checked
.append({configlet
['name']: "success"})
1052 data
= {'new': new
, 'updated': updated
, 'deleted': deleted
, 'checked': checked
}
1053 return [changed
, data
]
1055 def __get_configletsDevices(self
, configlets
):
1056 for s
in self
.s_api
:
1057 configlet
= configlets
[s
]
1058 # Add applied Devices
1059 if len(configlet
) > 0:
1060 configlet
['devices'] = []
1061 applied_devices
= self
.client
.api
.get_applied_devices(
1063 for device
in applied_devices
['data']:
1064 configlet
['devices'].append(device
['hostName'])
1066 def __get_serviceData(self
, service_uuid
, service_type
, vlan_id
, conn_info
=None):
1068 for s
in self
.s_api
:
1071 srv_cls
= self
.__get
_serviceConfigLets
(service_uuid
,
1074 self
.__get
_configletsDevices
(srv_cls
)
1075 for s
in self
.s_api
:
1078 for dev
in cl
['devices']:
1079 cls_perSw
[dev
].append(cl
)
1081 cls_perSw
= conn_info
['configLetPerSwitch']
1084 def delete_connectivity_service(self
, service_uuid
, conn_info
=None):
1086 Disconnect multi-site endpoints previously connected
1088 :param service_uuid: The one returned by create_connectivity_service
1089 :param conn_info: The one returned by last call to 'create_connectivity_service' or 'edit_connectivity_service'
1090 if they do not return None
1092 :raises: SdnConnectorException: In case of error. The parameter http_code must be filled
1095 self
.logger
.debug('invoked delete_connectivity_service {}'.
1096 format(service_uuid
))
1097 if not service_uuid
:
1098 raise SdnConnectorError(message
='No connection service UUID',
1101 self
.__get
_Connection
()
1102 if conn_info
is None:
1103 raise SdnConnectorError(message
='No connection information for service UUID {}'.format(service_uuid
),
1106 cls_perSw
= self
.__get
_serviceData
(service_uuid
,
1107 conn_info
['service_type'],
1108 conn_info
['vlan_id'],
1110 allLeafConfigured
= {}
1111 allLeafModified
= {}
1112 for s
in self
.s_api
:
1113 allLeafConfigured
[s
] = True
1114 allLeafModified
[s
] = True
1115 found_in_cvp
= False
1116 for s
in self
.s_api
:
1120 self
.__rollbackConnection
(cls_perSw
,
1124 # if the service is not defined in Cloud Vision, return a 404 - NotFound error
1125 raise SdnConnectorError(message
='Service {} was not found in Arista Cloud Vision {}'.
1126 format(service_uuid
, self
.__wim
_url
),
1128 self
.__removeMetadata
(service_uuid
)
1129 except CvpLoginError
as e
:
1130 self
.logger
.info(str(e
))
1132 raise SdnConnectorError(message
=SdnError
.UNAUTHORIZED
,
1133 http_code
=401) from e
1134 except SdnConnectorError
as sde
:
1136 except Exception as ex
:
1138 self
.logger
.error(ex
)
1139 if self
.raiseException
:
1141 raise SdnConnectorError(message
=SdnError
.INTERNAL_ERROR
,
1142 http_code
=500) from ex
1144 def __addMetadata(self
, service_uuid
, service_type
, vlan_id
):
1145 """ Adds the connectivity service from 'OSM_metadata' configLet
1147 found_in_cvp
= False
1149 cvp_cl
= self
.client
.api
.get_configlet_by_name(self
.__OSM
_METADATA
)
1151 except CvpApiError
as error
:
1152 if "Entity does not exist" in error
.msg
:
1157 new_serv
= '{} {} {} {}\n'.format(self
.__METADATA
_PREFIX
, service_type
, vlan_id
, service_uuid
)
1160 cl_config
= cvp_cl
['config'] + new_serv
1162 cl_config
= new_serv
1163 cl_meta
= [{'name': self
.__OSM
_METADATA
, 'config': cl_config
}]
1164 self
.__configlet
_modify
(cl_meta
)
1165 except Exception as e
:
1166 self
.logger
.error('Error in setting metadata in CloudVision from OSM for service {}: {}'.
1167 format(service_uuid
, str(e
)))
1170 def __removeMetadata(self
, service_uuid
):
1171 """ Removes the connectivity service from 'OSM_metadata' configLet
1173 found_in_cvp
= False
1175 cvp_cl
= self
.client
.api
.get_configlet_by_name(self
.__OSM
_METADATA
)
1177 except CvpApiError
as error
:
1178 if "Entity does not exist" in error
.msg
:
1184 if service_uuid
in cvp_cl
['config']:
1186 for line
in cvp_cl
['config'].split('\n'):
1187 if service_uuid
in line
:
1190 cl_config
= cl_config
+ line
1191 cl_meta
= [{'name': self
.__OSM
_METADATA
, 'config': cl_config
}]
1192 self
.__configlet
_modify
(cl_meta
)
1193 except Exception as e
:
1194 self
.logger
.error('Error in removing metadata in CloudVision from OSM for service {}: {}'.
1195 format(service_uuid
, str(e
)))
1198 def edit_connectivity_service(self
,
1201 connection_points
=None,
1203 """ Change an existing connectivity service.
1205 This method's arguments and return value follow the same convention as
1206 :meth:`~.create_connectivity_service`.
1208 :param service_uuid: UUID of the connectivity service.
1209 :param conn_info: (dict or None): Information previously returned
1210 by last call to create_connectivity_service
1211 or edit_connectivity_service
1212 :param connection_points: (list): If provided, the old list of
1213 connection points will be replaced.
1214 :param kwargs: Same meaning that create_connectivity_service
1215 :return: dict or None: Information to be updated and stored at
1217 When ``None`` is returned, no information should be changed.
1218 When an empty dict is returned, the database record will
1220 **MUST** be JSON/YAML-serializable (plain data structures).
1222 SdnConnectorError: In case of error.
1225 self
.logger
.debug('invoked edit_connectivity_service for service {}. ports: {}'.format(service_uuid
,
1228 if not service_uuid
:
1229 raise SdnConnectorError(message
='Unable to perform operation, missing or empty uuid',
1232 raise SdnConnectorError(message
='Unable to perform operation, missing or empty connection information',
1235 if connection_points
is None:
1238 self
.__get
_Connection
()
1240 cls_currentPerSw
= conn_info
['configLetPerSwitch']
1241 service_type
= conn_info
['service_type']
1243 self
.__check
_service
(service_type
,
1249 s_uid
, s_connInf
= self
.__processConnection
(
1254 self
.logger
.info("Service with uuid {} configuration updated".
1257 except CvpLoginError
as e
:
1258 self
.logger
.info(str(e
))
1260 raise SdnConnectorError(message
=SdnError
.UNAUTHORIZED
,
1261 http_code
=401) from e
1262 except SdnConnectorError
as sde
:
1264 except Exception as ex
:
1267 # TODO check if there are pending task, and cancel them before restoring
1268 self
.__updateConnection
(cls_currentPerSw
)
1269 except Exception as e
:
1270 self
.logger
.error("Unable to restore configuration in service {} after an error in the configuration updated: {}".
1271 format(service_uuid
, str(e
)))
1272 if self
.raiseException
:
1274 raise SdnConnectorError(message
=str(ex
),
1275 http_code
=500) from ex
1277 def clear_all_connectivity_services(self
):
1278 """ Removes all connectivity services from Arista CloudVision with two steps:
1279 - retrives all the services from Arista CloudVision
1280 - removes each service
1283 self
.logger
.debug('invoked AristaImpl ' +
1284 'clear_all_connectivity_services')
1285 self
.__get
_Connection
()
1286 s_list
= self
.__get
_srvUUIDs
()
1289 conn_info
['service_type'] = serv
['type']
1290 conn_info
['vlan_id'] = serv
['vlan']
1292 self
.delete_connectivity_service(serv
['uuid'], conn_info
)
1293 except CvpLoginError
as e
:
1294 self
.logger
.info(str(e
))
1296 raise SdnConnectorError(message
=SdnError
.UNAUTHORIZED
,
1297 http_code
=401) from e
1298 except SdnConnectorError
as sde
:
1300 except Exception as ex
:
1302 self
.logger
.error(ex
)
1303 if self
.raiseException
:
1305 raise SdnConnectorError(message
=SdnError
.INTERNAL_ERROR
,
1306 http_code
=500) from ex
1308 def get_all_active_connectivity_services(self
):
1309 """ Return the uuid of all the active connectivity services with two steps:
1310 - retrives all the services from Arista CloudVision
1311 - retrives the status of each server
1314 self
.logger
.debug('invoked AristaImpl {}'.format(
1315 'get_all_active_connectivity_services'))
1316 self
.__get
_Connection
()
1317 s_list
= self
.__get
_srvUUIDs
()
1321 conn_info
['service_type'] = serv
['type']
1322 conn_info
['vlan_id'] = serv
['vlan']
1324 status
= self
.get_connectivity_service_status(serv
['uuid'], conn_info
)
1325 if status
['sdn_status'] == 'ACTIVE':
1326 result
.append(serv
['uuid'])
1328 except CvpLoginError
as e
:
1329 self
.logger
.info(str(e
))
1331 raise SdnConnectorError(message
=SdnError
.UNAUTHORIZED
,
1332 http_code
=401) from e
1333 except SdnConnectorError
as sde
:
1335 except Exception as ex
:
1337 self
.logger
.error(ex
)
1338 if self
.raiseException
:
1340 raise SdnConnectorError(message
=SdnError
.INTERNAL_ERROR
,
1341 http_code
=500) from ex
1343 def __get_serviceConfigLets(self
, service_uuid
, service_type
, vlan_id
):
1344 """ Return the configLet's associated with a connectivity service,
1345 There should be one, as maximum, per device (switch) for a given
1346 connectivity service
1349 for s
in self
.s_api
:
1351 found_in_cvp
= False
1352 name
= (self
.__OSM
_PREFIX
+
1354 self
.__SEPARATOR
+ service_type
+ str(vlan_id
) +
1355 self
.__SEPARATOR
+ service_uuid
)
1357 cvp_cl
= self
.client
.api
.get_configlet_by_name(name
)
1359 except CvpApiError
as error
:
1360 if "Entity does not exist" in error
.msg
:
1368 def __get_srvVLANs(self
):
1369 """ Returns a list with all the VLAN id's used in the connectivity services managed
1370 in tha Arista CloudVision by checking the 'OSM_metadata' configLet where this
1371 information is stored
1373 found_in_cvp
= False
1375 cvp_cl
= self
.client
.api
.get_configlet_by_name(self
.__OSM
_METADATA
)
1377 except CvpApiError
as error
:
1378 if "Entity does not exist" in error
.msg
:
1384 lines
= cvp_cl
['config'].split('\n')
1386 if self
.__METADATA
_PREFIX
in line
:
1387 s_vlan
= line
.split(' ')[3]
1390 if (s_vlan
is not None and
1392 s_vlan
not in s_vlan_list
):
1393 s_vlan_list
.append(s_vlan
)
1397 def __get_srvUUIDs(self
):
1398 """ Retrieves all the connectivity services, managed in tha Arista CloudVision
1399 by checking the 'OSM_metadata' configLet where this information is stored
1401 found_in_cvp
= False
1403 cvp_cl
= self
.client
.api
.get_configlet_by_name(self
.__OSM
_METADATA
)
1405 except CvpApiError
as error
:
1406 if "Entity does not exist" in error
.msg
:
1412 lines
= cvp_cl
['config'].split('\n')
1414 if self
.__METADATA
_PREFIX
in line
:
1415 line
= line
.split(' ')
1416 serv
= {'uuid': line
[4], 'type': line
[2], 'vlan': line
[3]}
1419 if (serv
is not None and
1421 serv
not in serv_list
):
1422 serv_list
.append(serv
)
1426 def __get_Connection(self
):
1427 """ Open a connection with Arista CloudVision,
1428 invoking the version retrival as test
1431 if self
.client
is None:
1432 self
.client
= self
.__connect
()
1433 self
.client
.api
.get_cvp_info()
1434 except (CvpSessionLogOutError
, RequestException
) as e
:
1435 self
.logger
.debug("Connection error '{}'. Reconnecting".format(e
))
1436 self
.client
= self
.__connect
()
1437 self
.client
.api
.get_cvp_info()
1439 def __connect(self
):
1440 ''' Connects to CVP device using user provided credentials from initialization.
1441 :return: CvpClient object with connection instantiated.
1443 client
= CvpClient()
1444 protocol
, _
, rest_url
= self
.__wim
_url
.rpartition("://")
1445 host
, _
, port
= rest_url
.partition(":")
1446 if port
and port
.endswith("/"):
1447 port
= int(port
[:-1])
1453 client
.connect([host
],
1456 protocol
=protocol
or "https",
1459 self
.taskC
= AristaCVPTask(client
.api
)
1462 def __compare(self
, fromText
, toText
, lines
=10):
1463 """ Compare text string in 'fromText' with 'toText' and produce
1464 diffRatio - a score as a float in the range [0, 1] 2.0*M / T
1465 T is the total number of elements in both sequences,
1466 M is the number of matches.
1467 Score - 1.0 if the sequences are identical, and
1468 0.0 if they have nothing in common.
1471 '- ' line unique to sequence 1
1472 '+ ' line unique to sequence 2
1473 ' ' line common to both sequences
1474 '? ' line not present in either input sequence
1476 fromlines
= fromText
.splitlines(1)
1477 tolines
= toText
.splitlines(1)
1478 diff
= list(difflib
.unified_diff(fromlines
, tolines
, n
=lines
))
1479 textComp
= difflib
.SequenceMatcher(None, fromText
, toText
)
1480 diffRatio
= round(textComp
.quick_ratio()*100, 2)
1481 return [diffRatio
, diff
]
1483 def __load_inventory(self
):
1484 """ Get Inventory Data for All Devices (aka switches) from the Arista CloudVision
1486 if not self
.cvp_inventory
:
1487 self
.cvp_inventory
= self
.client
.api
.get_inventory()
1488 self
.allDeviceFacts
= []
1489 for device
in self
.cvp_inventory
:
1490 self
.allDeviceFacts
.append(device
)
1492 def is_valid_destination(self
, url
):
1493 """ Check that the provided WIM URL is correct
1495 if re
.match(self
.__regex
, url
):
1497 elif self
.is_valid_ipv4_address(url
):
1500 return self
.is_valid_ipv6_address(url
)
1502 def is_valid_ipv4_address(self
, address
):
1503 """ Checks that the given IP is IPv4 valid
1506 socket
.inet_pton(socket
.AF_INET
, address
)
1507 except AttributeError: # no inet_pton here, sorry
1509 socket
.inet_aton(address
)
1510 except socket
.error
:
1512 return address
.count('.') == 3
1513 except socket
.error
: # not a valid address
1517 def is_valid_ipv6_address(self
, address
):
1518 """ Checks that the given IP is IPv6 valid
1521 socket
.inet_pton(socket
.AF_INET6
, address
)
1522 except socket
.error
: # not a valid address