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
.aristaSwitch
import AristaSwitch
49 from osm_rosdn_arista
.aristaConfigLet
import AristaSDNConfigLet
50 from osm_rosdn_arista
.aristaTask
import AristaCVPTask
54 UNREACHABLE
= 'Unable to reach the WIM.',
56 'VLAN value inconsistent between the connection points',
57 VLAN_NOT_PROVIDED
= 'VLAN value not provided',
58 CONNECTION_POINTS_SIZE
= \
59 'Unexpected number of connection points: 2 expected.',
60 ENCAPSULATION_TYPE
= \
61 'Unexpected service_endpoint_encapsulation_type. \
62 Only "dotq1" is accepted.',
63 BANDWIDTH
= 'Unable to get the bandwidth.',
64 STATUS
= 'Unable to get the status for the service.',
65 DELETE
= 'Unable to delete service.',
66 CLEAR_ALL
= 'Unable to clear all the services',
67 UNKNOWN_ACTION
= 'Unknown action invoked.',
68 BACKUP
= 'Unable to get the backup parameter.',
69 UNSUPPORTED_FEATURE
= "Unsupported feature",
70 UNAUTHORIZED
= "Failed while authenticating",
71 INTERNAL_ERROR
= "Internal error"
74 class AristaSdnConnector(SdnConnectorBase
):
75 """Arista class for the SDN connectors
78 wim (dict): WIM record, as stored in the database
79 wim_account (dict): WIM account record, as stored in the database
81 The arguments of the constructor are converted to object attributes.
82 An extra property, ``service_endpoint_mapping`` is created from ``config``.
84 The access to Arista CloudVision is made through the API defined in
85 https://github.com/aristanetworks/cvprac
86 The a connectivity service consist in creating a VLAN and associate the interfaces
87 of the connection points MAC addresses to this VLAN in all the switches of the topology,
88 the BDP is also configured for this VLAN.
90 The Arista Cloud Vision API workflow is the following
91 -- The switch configuration is defined as a set of switch configuration commands,
92 what is called 'ConfigLet'
93 -- The ConfigLet is associated to the device (leaf switch)
94 -- Automatically a task is associated to this activity for change control, the task
95 in this stage is in 'Pending' state
96 -- The task will be executed so that the configuration is applied to the switch.
97 -- The service information is saved in the response of the creation call
98 -- All created services identification is stored in a generic ConfigLet 'OSM_metadata'
99 to keep track of the managed resources by OSM in the Arista deployment.
101 __supported_service_types
= ["ELINE (L2)", "ELINE", "ELAN"]
102 __service_types_ELAN
= "ELAN"
103 __service_types_ELINE
= "ELINE"
104 __ELINE_num_connection_points
= 2
105 __supported_service_types
= ["ELINE", "ELAN"]
106 __supported_encapsulation_types
= ["dot1q"]
107 __WIM_LOGGER
= 'openmano.sdnconn.arista'
108 __SERVICE_ENDPOINT_MAPPING
= 'service_endpoint_mapping'
109 __ENCAPSULATION_TYPE_PARAM
= "service_endpoint_encapsulation_type"
110 __ENCAPSULATION_INFO_PARAM
= "service_endpoint_encapsulation_info"
111 __BACKUP_PARAM
= "backup"
112 __BANDWIDTH_PARAM
= "bandwidth"
113 __SERVICE_ENDPOINT_PARAM
= "service_endpoint_id"
115 __WAN_SERVICE_ENDPOINT_PARAM
= "service_endpoint_id"
116 __WAN_MAPPING_INFO_PARAM
= "service_mapping_info"
117 __DEVICE_ID_PARAM
= "device_id"
118 __DEVICE_INTERFACE_ID_PARAM
= "device_interface_id"
119 __SW_ID_PARAM
= "switch_dpid"
120 __SW_PORT_PARAM
= "switch_port"
121 __VLAN_PARAM
= "vlan"
124 __MANAGED_BY_OSM
= '## Managed by OSM '
125 __OSM_PREFIX
= "osm_"
126 __OSM_METADATA
= "OSM_metadata"
127 __METADATA_PREFIX
= '!## Service'
128 __EXC_TASK_EXEC_WAIT
= 10
129 __ROLLB_TASK_EXEC_WAIT
= 10
130 __API_REQUEST_TOUT
= 60
131 __SWITCH_TAG_NAME
= 'topology_type'
132 __SWITCH_TAG_VALUE
= 'leaf'
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()
186 self
.__load
_switches
()
188 def __load_switches(self
):
189 """ Retrieves the switches to configure in the following order
190 1. from incoming configuration:
191 1.1 using port mapping
192 using user and password from WIM
193 retrieving Lo0 and AS from switch
194 1.2 from 'switches' parameter,
195 if any parameter is not present
196 Lo0 and AS - it will be requested to the switch
197 usr and pass - from WIM configuration
198 2. Looking in the CloudVision inventory if not in configuration parameters
199 2.1 using the switches with the topology_type tag set to 'leaf'
200 2.2 using the switches whose parent container is 'leaf'
201 2.3 using the switches whose hostname contains with 'leaf'
203 All the search methods will be used
206 if self
.__config
and self
.__config
.get(self
.__SERVICE
_ENDPOINT
_MAPPING
):
207 for port
in self
.__config
.get(self
.__SERVICE
_ENDPOINT
_MAPPING
):
208 switch_dpid
= port
.get(self
.__SW
_ID
_PARAM
)
209 if switch_dpid
and switch_dpid
not in self
.switches
:
210 self
.switches
[switch_dpid
] = {'passwd': self
.__passwd
,
216 if self
.__config
and self
.__config
.get('switches'):
217 # Not directly from json, complete one by one
218 config_switches
= self
.__config
.get('switches')
219 for cs
, cs_content
in config_switches
.items():
220 if cs
not in self
.switches
:
221 self
.switches
[cs
] = {'passwd': self
.__passwd
, 'ip': None, 'usr': self
.__user
, 'lo0': None,'AS': None}
223 self
.switches
[cs
].update(cs_content
)
225 # Load the rest of the data
226 if self
.client
is None:
227 self
.client
= self
.__connect
()
228 self
.__load
_inventory
()
229 if not self
.switches
:
230 self
.__get
_tags
(self
.__SWITCH
_TAG
_NAME
, self
.__SWITCH
_TAG
_VALUE
)
231 for device
in self
.allDeviceFacts
:
232 # get the switches whose container parent is 'leaf',
233 # or the topology_tag is 'leaf'
234 # or the hostname contains 'leaf'
235 if ((device
['serialNumber'] in self
.cvp_tags
) or
236 (self
.__SWITCH
_TAG
_VALUE
in device
['containerName'].lower()) or
237 (self
.__SWITCH
_TAG
_VALUE
in device
['hostname'].lower())):
238 if not self
.switches
.get(device
['hostname']):
239 switch_data
= {'passwd': self
.__passwd
,
240 'ip': device
['ipAddress'],
244 self
.switches
[device
['hostname']] = switch_data
245 if len(self
.switches
) == 0:
246 self
.logger
.error("Unable to load Leaf switches from CVP")
249 # self.s_api are switch objects, one for each switch in self.switches,
250 # used to make eAPI calls by using switch.py module
252 for s
in self
.switches
:
253 if not self
.switches
[s
].get('ip'):
254 for device
in self
.allDeviceFacts
:
255 if device
['hostname'] == s
:
256 self
.switches
[s
]['ip'] = device
['ipAddress']
257 if self
.is_valid_destination(self
.switches
[s
].get('ip')):
258 self
.s_api
[s
] = AristaSwitch(host
=self
.switches
[s
]['ip'],
259 user
=self
.switches
[s
]['usr'],
260 passwd
=self
.switches
[s
]['passwd'],
262 # Each switch has a different loopback address,
263 # so it's a different configLet
264 if not self
.switches
[s
].get('lo0'):
265 inf
= self
.__get
_switch
_interface
_ip
(s
, 'Loopback0')
266 self
.switches
[s
]["lo0"] = inf
.split('/')[0]
267 if not self
.switches
[s
].get('AS'):
268 self
.switches
[s
]["AS"] = self
.__get
_switch
_asn
(s
)
269 self
.logger
.debug("Using Arista Leaf switches: {}".format(
270 self
.delete_keys_from_dict(self
.switches
, ('passwd',))))
272 def __lldp_find_neighbor(self
, tlv_name
=None, tlv_value
=None):
273 """Returns a list of dicts where a mathing LLDP neighbor has been found
275 switch -> switch name
276 interface -> switch interface
281 # Get LLDP info from each switch
283 result
= self
.s_api
[s
].run("show lldp neighbors detail")
284 lldp_info
[s
] = result
[0]["lldpNeighbors"]
285 # Look LLDP match on each interface
286 # Note that eAPI returns [] for an interface with no LLDP neighbors
287 # in the corresponding interface lldpNeighborInfo field
288 for interface
in lldp_info
[s
]:
289 if lldp_info
[s
][interface
]["lldpNeighborInfo"]:
290 lldp_nInf
= lldp_info
[s
][interface
]["lldpNeighborInfo"][0]
291 if tlv_name
in lldp_nInf
:
292 if lldp_nInf
[tlv_name
] == tlv_value
:
293 r
.append({"name": s
, "interface": interface
})
297 def __get_switch_asn(self
, switch
):
298 """Returns switch ASN in default VRF
300 bgp_info
= self
.s_api
[switch
].run("show ip bgp summary")[0]
301 return(bgp_info
["vrfs"]["default"]["asn"])
303 def __get_switch_po(self
, switch
, interface
=None):
304 """Returns Port-Channels for a given interface
305 If interface is None returns a list with all PO interfaces
306 Note that if specified, interface should be exact name
307 for instance: Ethernet3 and not e3 eth3 and so on
309 po_inf
= self
.s_api
[switch
].run("show port-channel")[0]["portChannels"]
312 r
= [x
for x
in po_inf
if interface
in po_inf
[x
]["activePorts"]]
318 def __get_switch_interface_ip(self
, switch
, interface
=None):
319 """Returns interface primary ip
320 interface should be exact name
321 for instance: Ethernet3 and not ethernet 3, e3 eth3 and so on
323 cmd
= "show ip interface {}".format(interface
)
324 ip_info
= self
.s_api
[switch
].run(cmd
)[0]["interfaces"][interface
]
326 ip
= ip_info
["interfaceAddress"]["primaryIp"]["address"]
327 mask
= ip_info
["interfaceAddress"]["primaryIp"]["maskLen"]
329 return "{}/{}".format(ip
, mask
)
331 def __check_service(self
, service_type
, connection_points
,
332 check_vlan
=True, check_num_cp
=True, kwargs
=None):
333 """ Reviews the connection points elements looking for semantic errors in the incoming data
335 if service_type
not in self
.__supported
_service
_types
:
336 raise Exception("The service '{}' is not supported. Only '{}' are accepted".format(
338 self
.__supported
_service
_types
))
341 if (len(connection_points
) < 2):
342 raise Exception(SdnError
.CONNECTION_POINTS_SIZE
)
343 if ((len(connection_points
) != self
.__ELINE
_num
_connection
_points
) and
344 (service_type
== self
.__service
_types
_ELINE
)):
345 raise Exception(SdnError
.CONNECTION_POINTS_SIZE
)
349 for cp
in connection_points
:
350 enc_type
= cp
.get(self
.__ENCAPSULATION
_TYPE
_PARAM
)
352 enc_type
not in self
.__supported
_encapsulation
_types
):
353 raise Exception(SdnError
.ENCAPSULATION_TYPE
)
354 encap_info
= cp
.get(self
.__ENCAPSULATION
_INFO
_PARAM
)
355 cp_vlan_id
= str(encap_info
.get(self
.__VLAN
_PARAM
))
359 elif vlan_id
!= cp_vlan_id
:
360 raise Exception(SdnError
.VLAN_INCONSISTENT
)
362 raise Exception(SdnError
.VLAN_NOT_PROVIDED
)
363 if vlan_id
in self
.__get
_srvVLANs
():
364 raise Exception('VLAN {} already assigned to a connectivity service'.format(vlan_id
))
366 # Commented out for as long as parameter isn't implemented
367 # bandwidth = kwargs.get(self.__BANDWIDTH_PARAM)
368 # if not isinstance(bandwidth, int):
369 # self.__exception(SdnError.BANDWIDTH, http_code=400)
371 # Commented out for as long as parameter isn't implemented
372 # backup = kwargs.get(self.__BACKUP_PARAM)
373 # if not isinstance(backup, bool):
374 # self.__exception(SdnError.BACKUP, http_code=400)
376 def check_credentials(self
):
377 """Retrieves the CloudVision version information, as the easiest way
378 for testing the access to CloudVision API
381 if self
.client
is None:
382 self
.client
= self
.__connect
()
383 result
= self
.client
.api
.get_cvp_info()
384 self
.logger
.debug(result
)
385 except CvpLoginError
as e
:
386 self
.logger
.info(str(e
))
388 raise SdnConnectorError(message
=SdnError
.UNAUTHORIZED
,
389 http_code
=401) from e
390 except Exception as ex
:
392 self
.logger
.error(str(ex
))
393 raise SdnConnectorError(message
=SdnError
.INTERNAL_ERROR
,
394 http_code
=500) from ex
396 def get_connectivity_service_status(self
, service_uuid
, conn_info
=None):
397 """Monitor the status of the connectivity service established
399 service_uuid (str): UUID of the connectivity service
400 conn_info (dict or None): Information returned by the connector
401 during the service creation/edition and subsequently stored in
405 dict: JSON/YAML-serializable dict that contains a mandatory key
406 ``sdn_status`` associated with one of the following values::
408 {'sdn_status': 'ACTIVE'}
409 # The service is up and running.
411 {'sdn_status': 'INACTIVE'}
412 # The service was created, but the connector
413 # cannot determine yet if connectivity exists
414 # (ideally, the caller needs to wait and check again).
416 {'sdn_status': 'DOWN'}
417 # Connection was previously established,
418 # but an error/failure was detected.
420 {'sdn_status': 'ERROR'}
421 # An error occurred when trying to create the service/
422 # establish the connectivity.
424 {'sdn_status': 'BUILD'}
425 # Still trying to create the service, the caller
426 # needs to wait and check again.
428 Additionally ``error_msg``(**str**) and ``sdn_info``(**dict**)
429 keys can be used to provide additional status explanation or
430 new information available for the connectivity service.
433 self
.logger
.debug("invoked get_connectivity_service_status '{}'".format(service_uuid
))
435 raise SdnConnectorError(message
='No connection service UUID',
438 self
.__get
_Connection
()
439 if conn_info
is None:
440 raise SdnConnectorError(message
='No connection information for service UUID {}'.format(service_uuid
),
443 if 'configLetPerSwitch' in conn_info
.keys():
447 cls_perSw
= self
.__get
_serviceData
(service_uuid
,
448 conn_info
['service_type'],
449 conn_info
['vlan_id'],
452 t_isCancelled
= False
457 if (len(cls_perSw
[s
]) > 0):
458 for cl
in cls_perSw
[s
]:
459 # Fix 1030 SDN-ARISTA Key error note when deploy a NS
460 # Added protection to check that 'note' exists and additionally
461 # verify that it is managed by OSM
462 if (not cls_perSw
[s
][0]['config'] or
463 not cl
.get('note') or
464 self
.__MANAGED
_BY
_OSM
not in cl
['note']):
467 t_id
= note
.split(self
.__SEPARATOR
)[1]
468 result
= self
.client
.api
.get_task_by_id(t_id
)
469 if result
['workOrderUserDefinedStatus'] == 'Completed':
471 elif result
['workOrderUserDefinedStatus'] == 'Cancelled':
473 elif result
['workOrderUserDefinedStatus'] == 'Failed':
477 failed_switches
.append(s
)
479 error_msg
= 'Some works were cancelled in switches: {}'.format(str(failed_switches
))
482 error_msg
= 'Some works failed in switches: {}'.format(str(failed_switches
))
485 error_msg
= 'Some works are still under execution in switches: {}'.format(str(failed_switches
))
489 sdn_status
= 'ACTIVE'
491 return {'sdn_status': sdn_status
,
492 'error_msg': error_msg
,
493 'sdn_info': sdn_info
}
494 except CvpLoginError
as e
:
495 self
.logger
.info(str(e
))
497 raise SdnConnectorError(message
=SdnError
.UNAUTHORIZED
,
498 http_code
=401) from e
499 except Exception as ex
:
501 self
.logger
.error(str(ex
), exc_info
=True)
502 raise SdnConnectorError(message
=str(ex
),
503 http_code
=500) from ex
505 def create_connectivity_service(self
, service_type
, connection_points
,
507 """Stablish SDN/WAN connectivity between the endpoints
509 (str): ``ELINE`` (L2), ``ELAN`` (L2), ``ETREE`` (L2), ``L3``.
510 :param connection_points: (list): each point corresponds to
511 an entry point to be connected. For WIM: from the DC
512 to the transport network.
513 For SDN: Compute/PCI to the transport network. One
514 connection point serves to identify the specific access and
515 some other service parameters, such as encapsulation type.
516 Each item of the list is a dict with:
517 "service_endpoint_id": (str)(uuid) Same meaning that for
518 'service_endpoint_mapping' (see __init__)
519 In case the config attribute mapping_not_needed is True,
520 this value is not relevant. In this case
521 it will contain the string "device_id:device_interface_id"
522 "service_endpoint_encapsulation_type": None, "dot1q", ...
523 "service_endpoint_encapsulation_info": (dict) with:
524 "vlan": ..., (int, present if encapsulation is dot1q)
525 "vni": ... (int, present if encapsulation is vxlan),
526 "peers": [(ipv4_1), (ipv4_2)] (present if
527 encapsulation is vxlan)
529 "device_id": ..., same meaning that for
530 'service_endpoint_mapping' (see __init__)
531 "device_interface_id": same meaning that for
532 'service_endpoint_mapping' (see __init__)
533 "switch_dpid": ..., present if mapping has been found
534 for this device_id,device_interface_id
535 "switch_port": ... present if mapping has been found
536 for this device_id,device_interface_id
537 "service_mapping_info": present if mapping has
538 been found for this device_id,device_interface_id
539 :param kwargs: For future versions:
540 bandwidth (int): value in kilobytes
541 latency (int): value in milliseconds
542 Other QoS might be passed as keyword arguments.
543 :return: tuple: ``(service_id, conn_info)`` containing:
544 - *service_uuid* (str): UUID of the established
546 - *conn_info* (dict or None): Information to be
547 stored at the database (or ``None``).
548 This information will be provided to the
549 :meth:`~.edit_connectivity_service` and :obj:`~.delete`.
550 **MUST** be JSON/YAML-serializable (plain data structures).
551 :raises: SdnConnectorError: In case of error. Nothing should be
552 created in this case.
553 Provide the parameter http_code
556 self
.logger
.debug("invoked create_connectivity_service '{}' ports: {}".
557 format(service_type
, connection_points
))
558 self
.__get
_Connection
()
559 self
.__check
_service
(service_type
,
563 service_uuid
= str(uuid
.uuid4())
565 self
.logger
.info("Service with uuid {} created.".
566 format(service_uuid
))
567 s_uid
, s_connInf
= self
.__processConnection
(
573 self
.__addMetadata
(s_uid
, service_type
, s_connInf
['vlan_id'])
574 except Exception as e
:
577 return (s_uid
, s_connInf
)
578 except CvpLoginError
as e
:
579 self
.logger
.info(str(e
))
581 raise SdnConnectorError(message
=SdnError
.UNAUTHORIZED
,
582 http_code
=401) from e
583 except SdnConnectorError
as sde
:
585 except Exception as ex
:
587 self
.logger
.error(str(ex
), exc_info
=True)
588 if self
.raiseException
:
590 raise SdnConnectorError(message
=str(ex
),
591 http_code
=500) from ex
593 def __processConnection(self
,
599 Invoked from creation and edit methods
601 Process the connection points array,
602 creating a set of configuration per switch where it has to be applied
603 for creating the configuration, the switches have to be queried for obtaining:
604 - the loopback address
605 - the BGP ASN (autonomous system number)
606 - the interface name of the MAC address to add in the connectivity service
607 Once the new configuration is ready, the __updateConnection method is invoked for appling the changes
616 vlan_processed
= False
619 processed_connection_points
= []
620 for cp
in connection_points
:
622 encap_info
= cp
.get(self
.__ENCAPSULATION
_INFO
_PARAM
)
623 if not vlan_processed
:
624 vlan_id
= str(encap_info
.get(self
.__VLAN
_PARAM
))
627 vni_id
= encap_info
.get(self
.__VNI
_PARAM
)
629 vni_id
= str(10000 + int(vlan_id
))
631 if service_type
== self
.__service
_types
_ELAN
:
632 cl_vlan
= self
.clC
.getElan_vlan(service_uuid
,
636 cl_vlan
= self
.clC
.getEline_vlan(service_uuid
,
639 vlan_processed
= True
641 encap_type
= cp
.get(self
.__ENCAPSULATION
_TYPE
_PARAM
)
642 switch_id
= encap_info
.get(self
.__SW
_ID
_PARAM
)
644 point_mac
= encap_info
.get(self
.__MAC
_PARAM
)
645 switches
= self
.__lldp
_find
_neighbor
("chassisId", point_mac
)
646 self
.logger
.debug("Found connection point for MAC {}: {}".
647 format(point_mac
, switches
))
649 interface
= encap_info
.get(self
.__SW
_PORT
_PARAM
)
650 switches
= [{'name': switch_id
, 'interface': interface
}]
652 if len(switches
) == 0:
653 raise SdnConnectorError(message
="Connection point MAC address {} not found in the switches".format(point_mac
),
656 # remove those connections that are equal. This happens when several sriovs are located in the same
657 # compute node interface, that is, in the same switch and interface
658 switches
= [x
for x
in switches
if x
not in processed_connection_points
]
661 processed_connection_points
+= switches
662 for switch
in switches
:
664 port_channel
= self
.__get
_switch
_po
(switch
['name'],
666 if len(port_channel
) > 0:
667 interface
= port_channel
[0]
669 interface
= switch
['interface']
671 raise SdnConnectorError(message
="Connection point switch port empty for switch_dpid {}".format(switch_id
),
673 # it should be only one switch where the mac is attached
674 if encap_type
== 'dot1q':
675 # SRIOV configLet for Leaf switch mac's attached to
676 if service_type
== self
.__service
_types
_ELAN
:
677 cl_encap
= self
.clC
.getElan_sriov(service_uuid
, interface
, vlan_id
, i
)
679 cl_encap
= self
.clC
.getEline_sriov(service_uuid
, interface
, vlan_id
, i
)
681 # PT configLet for Leaf switch attached to the mac
682 if service_type
== self
.__service
_types
_ELAN
:
683 cl_encap
= self
.clC
.getElan_passthrough(service_uuid
,
687 cl_encap
= self
.clC
.getEline_passthrough(service_uuid
,
690 if cls_cp
.get(switch
['name']):
691 cls_cp
[switch
['name']] = str(cls_cp
[switch
['name']]) + cl_encap
693 cls_cp
[switch
['name']] = cl_encap
695 # at least 1 connection point has to be received
696 if not vlan_processed
:
697 raise SdnConnectorError(message
=SdnError
.UNSUPPORTED_FEATURE
,
701 # for cl in cp_configLets:
702 cl_name
= (self
.__OSM
_PREFIX
+
704 self
.__SEPARATOR
+ service_type
+ str(vlan_id
) +
705 self
.__SEPARATOR
+ service_uuid
)
706 # apply VLAN and BGP configLet to all Leaf switches
707 if service_type
== self
.__service
_types
_ELAN
:
708 cl_bgp
[s
] = self
.clC
.getElan_bgp(service_uuid
,
711 self
.switches
[s
]['lo0'],
712 self
.switches
[s
]['AS'])
714 cl_bgp
[s
] = self
.clC
.getEline_bgp(service_uuid
,
717 self
.switches
[s
]['lo0'],
718 self
.switches
[s
]['AS'])
720 if not cls_cp
.get(s
):
723 cl_config
= str(cl_vlan
) + str(cl_bgp
[s
]) + str(cls_cp
[s
])
725 cls_perSw
[s
] = [{'name': cl_name
, 'config': cl_config
}]
727 allLeafConfigured
, allLeafModified
= self
.__updateConnection
(cls_perSw
)
730 "uuid": service_uuid
,
732 "service_type": service_type
,
734 "connection_points": connection_points
,
735 "configLetPerSwitch": cls_perSw
,
736 'allLeafConfigured': allLeafConfigured
,
737 'allLeafModified': allLeafModified
}
739 return service_uuid
, conn_info
740 except Exception as ex
:
741 self
.logger
.debug("Exception processing connection {}: {}".
742 format(service_uuid
, str(ex
)))
745 def __updateConnection(self
, cls_perSw
):
746 """ Invoked in the creation and modification
748 checks if the new connection points config is:
749 - already in the Cloud Vision, the configLet is modified, and applied to the switch,
750 executing the corresponding task
751 - if it has to be removed:
752 then configuration has to be removed from the switch executing the corresponding task,
753 before trying to remove the configuration
754 - created, the configuration set is created, associated to the switch, and the associated
755 task to the configLet modification executed
756 In case of any error, rollback is executed, removing the created elements, and restoring to the
760 allLeafConfigured
= {}
764 allLeafConfigured
[s
] = False
765 allLeafModified
[s
] = False
768 toDelete_in_cvp
= False
769 if not (cls_perSw
.get(s
) and cls_perSw
[s
][0].get('config')):
770 # when there is no configuration, means that there is no interface
771 # in the switch to be connected, so the configLet has to be removed from CloudVision
772 # after removing the ConfigLet fron the switch if it was already there
774 # get config let name and key
777 cvp_cl
= self
.client
.api
.get_configlet_by_name(cl
[0]['name'])
779 cl_toDelete
.append(cvp_cl
)
781 toDelete_in_cvp
= True
782 except CvpApiError
as error
:
783 if "Entity does not exist" in error
.msg
:
787 # remove configLet from device
789 res
= self
.__configlet
_modify
(cls_perSw
[s
])
790 allLeafConfigured
[s
] = res
[0]
791 if not allLeafConfigured
[s
]:
794 res
= self
.__device
_modify
(
797 delete
=toDelete_in_cvp
)
798 if "errorMessage" in str(res
):
799 raise Exception(str(res
))
800 self
.logger
.info("Device {} modify result {}".format(s
, res
))
801 for t_id
in res
[1]['tasks']:
802 if not toDelete_in_cvp
:
803 note_msg
= "{}{}{}{}##".format(self
.__MANAGED
_BY
_OSM
,
807 self
.client
.api
.add_note_to_configlet(
808 cls_perSw
[s
][0]['key'],
810 cls_perSw
[s
][0]['note'] = note_msg
811 tasks
= { t_id
: {'workOrderId': t_id
} }
812 self
.__exec
_task
(tasks
, self
.__EXC
_TASK
_EXEC
_WAIT
)
813 # with just one configLet assigned to a device,
814 # delete all if there are errors in next loops
815 if not toDelete_in_cvp
:
816 allLeafModified
[s
] = True
817 if len(cl_toDelete
) > 0:
818 self
.__configlet
_modify
(cl_toDelete
, delete
=True)
820 return allLeafConfigured
, allLeafModified
821 except Exception as ex
:
823 self
.__rollbackConnection
(cls_perSw
,
826 except Exception as e
:
827 self
.logger
.error("Exception rolling back in updating connection: {}".
828 format(e
), exc_info
=True)
831 def __rollbackConnection(self
,
835 """ Removes the given configLet from the devices and then remove the configLets
838 if allLeafModified
[s
]:
840 res
= self
.__device
_modify
(
842 new_configlets
=cls_perSw
[s
],
844 if "errorMessage" in str(res
):
845 raise Exception(str(res
))
847 for t_id
in res
[1]['tasks']:
848 tasks
[t_id
] = {'workOrderId': t_id
}
849 self
.__exec
_task
(tasks
)
850 self
.logger
.info("Device {} modify result {}".format(s
, res
))
851 except Exception as e
:
852 self
.logger
.error('Error removing configlets from device {}: {}'.format(s
, e
))
855 if allLeafConfigured
[s
]:
856 self
.__configlet
_modify
(cls_perSw
[s
], delete
=True)
858 def __exec_task(self
, tasks
, tout
=10):
859 if self
.taskC
is None:
861 data
= self
.taskC
.update_all_tasks(tasks
).values()
862 self
.taskC
.task_action(data
, tout
, 'executed')
864 def __device_modify(self
, device_to_update
, new_configlets
, delete
):
865 """ Updates the devices (switches) adding or removing the configLet,
866 the tasks Id's associated to the change are returned
868 self
.logger
.info('Enter in __device_modify delete: {}'.format(
872 # Task Ids that have been identified during device actions
875 if (len(new_configlets
) == 0 or
876 device_to_update
is None or
877 len(device_to_update
) == 0):
878 data
= {'updated': updated
, 'tasks': newTasks
}
879 return [changed
, data
]
881 self
.__load
_inventory
()
883 allDeviceFacts
= self
.allDeviceFacts
884 # Work through Devices list adding device specific information
886 for try_device
in allDeviceFacts
:
887 # Add Device Specific Configlets
888 # self.logger.debug(device)
889 if try_device
['hostname'] not in device_to_update
:
891 dev_cvp_configlets
= self
.client
.api
.get_configlets_by_device_id(
892 try_device
['systemMacAddress'])
893 # self.logger.debug(dev_cvp_configlets)
894 try_device
['deviceSpecificConfiglets'] = []
895 for cvp_configlet
in dev_cvp_configlets
:
896 if int(cvp_configlet
['containerCount']) == 0:
897 try_device
['deviceSpecificConfiglets'].append(
898 {'name': cvp_configlet
['name'],
899 'key': cvp_configlet
['key']})
900 # self.logger.debug(device)
904 # Check assigned configlets
905 device_update
= False
907 remove_configlets
= []
911 for cvp_configlet
in device
['deviceSpecificConfiglets']:
912 for cl
in new_configlets
:
913 if cvp_configlet
['name'] == cl
['name']:
914 remove_configlets
.append(cvp_configlet
)
917 for configlet
in new_configlets
:
918 if configlet
not in device
['deviceSpecificConfiglets']:
919 add_configlets
.append(configlet
)
922 update_devices
.append({'hostname': device
['hostname'],
923 'configlets': [add_configlets
,
926 self
.logger
.info("Device to modify: {}".format(update_devices
))
928 up_device
= update_devices
[0]
929 cl_toAdd
= up_device
['configlets'][0]
930 cl_toDel
= up_device
['configlets'][1]
933 if delete
and len(cl_toDel
) > 0:
934 r
= self
.client
.api
.remove_configlets_from_device(
940 self
.logger
.debug("remove_configlets_from_device {} {}".format(dev_action
, cl_toDel
))
941 elif len(cl_toAdd
) > 0:
942 r
= self
.client
.api
.apply_configlets_to_device(
948 self
.logger
.debug("apply_configlets_to_device {} {}".format(dev_action
, cl_toAdd
))
950 except Exception as error
:
951 errorMessage
= str(error
)
952 msg
= "errorMessage: Device {} Configlets couldnot be updated: {}".format(
953 up_device
['hostname'], errorMessage
)
954 raise SdnConnectorError(msg
) from error
956 if "errorMessage" in str(dev_action
):
957 m
= "Device {} Configlets update fail: {}".format(
958 up_device
['name'], dev_action
['errorMessage'])
959 raise SdnConnectorError(m
)
962 if 'taskIds' in str(dev_action
):
963 # Fix 1030 SDN-ARISTA Key error note when deploy a NS
964 if not dev_action
['data']['taskIds']:
965 raise SdnConnectorError("No taskIds found: Device {} Configlets couldnot be updated".format(
966 up_device
['hostname']))
967 for taskId
in dev_action
['data']['taskIds']:
968 updated
.append({up_device
['hostname']:
969 "Configlets-{}".format(
971 newTasks
.append(taskId
)
973 updated
.append({up_device
['hostname']:
974 "Configlets-No_Specific_Tasks"})
975 data
= {'updated': updated
, 'tasks': newTasks
}
976 return [changed
, data
]
978 def __configlet_modify(self
, configletsToApply
, delete
=False):
979 ''' adds/update or delete the provided configLets
980 :param configletsToApply: list of configLets to apply
981 :param delete: flag to indicate if the configLets have to be deleted
982 from Cloud Vision Portal
983 :return: data: dict of module actions and taskIDs
985 self
.logger
.info('Enter in __configlet_modify delete:{}'.format(
988 # Compare configlets against cvp_facts-configlets
995 for cl
in configletsToApply
:
1002 cvp_cl
= self
.client
.api
.get_configlet_by_name(cl
['name'])
1003 cl
['key'] = cvp_cl
['key']
1004 cl
['note'] = cvp_cl
['note']
1006 except CvpApiError
as error
:
1007 if "Entity does not exist" in error
.msg
:
1015 configlet
= {'name': cvp_cl
['name'],
1019 cl_compare
= self
.__compare
(cl
['config'],
1021 # compare function returns a floating point number
1022 if cl_compare
[0] != 100.0:
1024 configlet
= {'name': cl
['name'],
1026 'config': cl
['config']}
1029 configlet
= {'name': cl
['name'],
1030 'key': cvp_cl
['key'],
1032 'config': cl
['config']}
1035 configlet
= {'name': cl
['name'],
1036 'config': cl
['config']}
1039 operation
= 'delete'
1040 resp
= self
.client
.api
.delete_configlet(
1041 configlet
['data']['name'],
1042 configlet
['data']['key'])
1044 operation
= 'update'
1045 resp
= self
.client
.api
.update_configlet(
1046 configlet
['config'],
1047 configlet
['data']['key'],
1048 configlet
['data']['name'],
1051 operation
= 'create'
1052 resp
= self
.client
.api
.add_configlet(
1054 configlet
['config'])
1056 operation
= 'checked'
1058 except Exception as error
:
1059 errorMessage
= str(error
).split(':')[-1]
1060 message
= "Configlet {} cannot be {}: {}".format(
1061 cl
['name'], operation
, errorMessage
)
1063 deleted
.append({configlet
['name']: message
})
1065 updated
.append({configlet
['name']: message
})
1067 new
.append({configlet
['name']: message
})
1069 checked
.append({configlet
['name']: message
})
1072 if "error" in str(resp
).lower():
1073 message
= "Configlet {} cannot be deleted: {}".format(
1074 cl
['name'], resp
['errorMessage'])
1076 deleted
.append({configlet
['name']: message
})
1078 updated
.append({configlet
['name']: message
})
1080 new
.append({configlet
['name']: message
})
1082 checked
.append({configlet
['name']: message
})
1086 deleted
.append({configlet
['name']: "success"})
1089 updated
.append({configlet
['name']: "success"})
1092 cl
['key'] = resp
# This key is used in API call deviceApplyConfigLet FGA
1093 new
.append({configlet
['name']: "success"})
1096 checked
.append({configlet
['name']: "success"})
1098 data
= {'new': new
, 'updated': updated
, 'deleted': deleted
, 'checked': checked
}
1099 return [changed
, data
]
1101 def __get_configletsDevices(self
, configlets
):
1102 for s
in self
.s_api
:
1103 configlet
= configlets
[s
]
1104 # Add applied Devices
1105 if len(configlet
) > 0:
1106 configlet
['devices'] = []
1107 applied_devices
= self
.client
.api
.get_applied_devices(
1109 for device
in applied_devices
['data']:
1110 configlet
['devices'].append(device
['hostName'])
1112 def __get_serviceData(self
, service_uuid
, service_type
, vlan_id
, conn_info
=None):
1114 for s
in self
.s_api
:
1117 srv_cls
= self
.__get
_serviceConfigLets
(service_uuid
,
1120 self
.__get
_configletsDevices
(srv_cls
)
1121 for s
in self
.s_api
:
1124 for dev
in cl
['devices']:
1125 cls_perSw
[dev
].append(cl
)
1127 cls_perSw
= conn_info
['configLetPerSwitch']
1130 def delete_connectivity_service(self
, service_uuid
, conn_info
=None):
1132 Disconnect multi-site endpoints previously connected
1134 :param service_uuid: The one returned by create_connectivity_service
1135 :param conn_info: The one returned by last call to 'create_connectivity_service' or 'edit_connectivity_service'
1136 if they do not return None
1138 :raises: SdnConnectorException: In case of error. The parameter http_code must be filled
1141 self
.logger
.debug('invoked delete_connectivity_service {}'.
1142 format(service_uuid
))
1143 if not service_uuid
:
1144 raise SdnConnectorError(message
='No connection service UUID',
1147 self
.__get
_Connection
()
1148 if conn_info
is None:
1149 raise SdnConnectorError(message
='No connection information for service UUID {}'.format(service_uuid
),
1152 cls_perSw
= self
.__get
_serviceData
(service_uuid
,
1153 conn_info
['service_type'],
1154 conn_info
['vlan_id'],
1156 allLeafConfigured
= {}
1157 allLeafModified
= {}
1158 for s
in self
.s_api
:
1159 allLeafConfigured
[s
] = True
1160 allLeafModified
[s
] = True
1161 found_in_cvp
= False
1162 for s
in self
.s_api
:
1166 self
.__rollbackConnection
(cls_perSw
,
1170 # if the service is not defined in Cloud Vision, return a 404 - NotFound error
1171 raise SdnConnectorError(message
='Service {} was not found in Arista Cloud Vision {}'.
1172 format(service_uuid
, self
.__wim
_url
),
1174 self
.__removeMetadata
(service_uuid
)
1175 except CvpLoginError
as e
:
1176 self
.logger
.info(str(e
))
1178 raise SdnConnectorError(message
=SdnError
.UNAUTHORIZED
,
1179 http_code
=401) from e
1180 except SdnConnectorError
as sde
:
1182 except Exception as ex
:
1184 self
.logger
.error(ex
)
1185 if self
.raiseException
:
1187 raise SdnConnectorError(message
=SdnError
.INTERNAL_ERROR
,
1188 http_code
=500) from ex
1190 def __addMetadata(self
, service_uuid
, service_type
, vlan_id
):
1191 """ Adds the connectivity service from 'OSM_metadata' configLet
1193 found_in_cvp
= False
1195 cvp_cl
= self
.client
.api
.get_configlet_by_name(self
.__OSM
_METADATA
)
1197 except CvpApiError
as error
:
1198 if "Entity does not exist" in error
.msg
:
1203 new_serv
= '{} {} {} {}\n'.format(self
.__METADATA
_PREFIX
, service_type
, vlan_id
, service_uuid
)
1206 cl_config
= cvp_cl
['config'] + new_serv
1208 cl_config
= new_serv
1209 cl_meta
= [{'name': self
.__OSM
_METADATA
, 'config': cl_config
}]
1210 self
.__configlet
_modify
(cl_meta
)
1211 except Exception as e
:
1212 self
.logger
.error('Error in setting metadata in CloudVision from OSM for service {}: {}'.
1213 format(service_uuid
, str(e
)))
1216 def __removeMetadata(self
, service_uuid
):
1217 """ Removes the connectivity service from 'OSM_metadata' configLet
1219 found_in_cvp
= False
1221 cvp_cl
= self
.client
.api
.get_configlet_by_name(self
.__OSM
_METADATA
)
1223 except CvpApiError
as error
:
1224 if "Entity does not exist" in error
.msg
:
1230 if service_uuid
in cvp_cl
['config']:
1232 for line
in cvp_cl
['config'].split('\n'):
1233 if service_uuid
in line
:
1236 cl_config
= cl_config
+ line
1237 cl_meta
= [{'name': self
.__OSM
_METADATA
, 'config': cl_config
}]
1238 self
.__configlet
_modify
(cl_meta
)
1239 except Exception as e
:
1240 self
.logger
.error('Error in removing metadata in CloudVision from OSM for service {}: {}'.
1241 format(service_uuid
, str(e
)))
1244 def edit_connectivity_service(self
,
1247 connection_points
=None,
1249 """ Change an existing connectivity service.
1251 This method's arguments and return value follow the same convention as
1252 :meth:`~.create_connectivity_service`.
1254 :param service_uuid: UUID of the connectivity service.
1255 :param conn_info: (dict or None): Information previously returned
1256 by last call to create_connectivity_service
1257 or edit_connectivity_service
1258 :param connection_points: (list): If provided, the old list of
1259 connection points will be replaced.
1260 :param kwargs: Same meaning that create_connectivity_service
1261 :return: dict or None: Information to be updated and stored at
1263 When ``None`` is returned, no information should be changed.
1264 When an empty dict is returned, the database record will
1266 **MUST** be JSON/YAML-serializable (plain data structures).
1268 SdnConnectorError: In case of error.
1271 self
.logger
.debug('invoked edit_connectivity_service for service {}. ports: {}'.format(service_uuid
,
1274 if not service_uuid
:
1275 raise SdnConnectorError(message
='Unable to perform operation, missing or empty uuid',
1278 raise SdnConnectorError(message
='Unable to perform operation, missing or empty connection information',
1281 if connection_points
is None:
1284 self
.__get
_Connection
()
1286 cls_currentPerSw
= conn_info
['configLetPerSwitch']
1287 service_type
= conn_info
['service_type']
1289 self
.__check
_service
(service_type
,
1295 s_uid
, s_connInf
= self
.__processConnection
(
1300 self
.logger
.info("Service with uuid {} configuration updated".
1303 except CvpLoginError
as e
:
1304 self
.logger
.info(str(e
))
1306 raise SdnConnectorError(message
=SdnError
.UNAUTHORIZED
,
1307 http_code
=401) from e
1308 except SdnConnectorError
as sde
:
1310 except Exception as ex
:
1313 # TODO check if there are pending task, and cancel them before restoring
1314 self
.__updateConnection
(cls_currentPerSw
)
1315 except Exception as e
:
1316 self
.logger
.error("Unable to restore configuration in service {} after an error in the configuration updated: {}".
1317 format(service_uuid
, str(e
)))
1318 if self
.raiseException
:
1320 raise SdnConnectorError(message
=str(ex
),
1321 http_code
=500) from ex
1323 def clear_all_connectivity_services(self
):
1324 """ Removes all connectivity services from Arista CloudVision with two steps:
1325 - retrives all the services from Arista CloudVision
1326 - removes each service
1329 self
.logger
.debug('invoked AristaImpl ' +
1330 'clear_all_connectivity_services')
1331 self
.__get
_Connection
()
1332 s_list
= self
.__get
_srvUUIDs
()
1335 conn_info
['service_type'] = serv
['type']
1336 conn_info
['vlan_id'] = serv
['vlan']
1338 self
.delete_connectivity_service(serv
['uuid'], conn_info
)
1339 except CvpLoginError
as e
:
1340 self
.logger
.info(str(e
))
1342 raise SdnConnectorError(message
=SdnError
.UNAUTHORIZED
,
1343 http_code
=401) from e
1344 except SdnConnectorError
as sde
:
1346 except Exception as ex
:
1348 self
.logger
.error(ex
)
1349 if self
.raiseException
:
1351 raise SdnConnectorError(message
=SdnError
.INTERNAL_ERROR
,
1352 http_code
=500) from ex
1354 def get_all_active_connectivity_services(self
):
1355 """ Return the uuid of all the active connectivity services with two steps:
1356 - retrives all the services from Arista CloudVision
1357 - retrives the status of each server
1360 self
.logger
.debug('invoked AristaImpl {}'.format(
1361 'get_all_active_connectivity_services'))
1362 self
.__get
_Connection
()
1363 s_list
= self
.__get
_srvUUIDs
()
1367 conn_info
['service_type'] = serv
['type']
1368 conn_info
['vlan_id'] = serv
['vlan']
1370 status
= self
.get_connectivity_service_status(serv
['uuid'], conn_info
)
1371 if status
['sdn_status'] == 'ACTIVE':
1372 result
.append(serv
['uuid'])
1374 except CvpLoginError
as e
:
1375 self
.logger
.info(str(e
))
1377 raise SdnConnectorError(message
=SdnError
.UNAUTHORIZED
,
1378 http_code
=401) from e
1379 except SdnConnectorError
as sde
:
1381 except Exception as ex
:
1383 self
.logger
.error(ex
)
1384 if self
.raiseException
:
1386 raise SdnConnectorError(message
=SdnError
.INTERNAL_ERROR
,
1387 http_code
=500) from ex
1389 def __get_serviceConfigLets(self
, service_uuid
, service_type
, vlan_id
):
1390 """ Return the configLet's associated with a connectivity service,
1391 There should be one, as maximum, per device (switch) for a given
1392 connectivity service
1395 for s
in self
.s_api
:
1397 found_in_cvp
= False
1398 name
= (self
.__OSM
_PREFIX
+
1400 self
.__SEPARATOR
+ service_type
+ str(vlan_id
) +
1401 self
.__SEPARATOR
+ service_uuid
)
1403 cvp_cl
= self
.client
.api
.get_configlet_by_name(name
)
1405 except CvpApiError
as error
:
1406 if "Entity does not exist" in error
.msg
:
1414 def __get_srvVLANs(self
):
1415 """ Returns a list with all the VLAN id's used in the connectivity services managed
1416 in tha Arista CloudVision by checking the 'OSM_metadata' configLet where this
1417 information is stored
1419 found_in_cvp
= False
1421 cvp_cl
= self
.client
.api
.get_configlet_by_name(self
.__OSM
_METADATA
)
1423 except CvpApiError
as error
:
1424 if "Entity does not exist" in error
.msg
:
1430 lines
= cvp_cl
['config'].split('\n')
1432 if self
.__METADATA
_PREFIX
in line
:
1433 s_vlan
= line
.split(' ')[3]
1436 if (s_vlan
is not None and
1438 s_vlan
not in s_vlan_list
):
1439 s_vlan_list
.append(s_vlan
)
1443 def __get_srvUUIDs(self
):
1444 """ Retrieves all the connectivity services, managed in tha Arista CloudVision
1445 by checking the 'OSM_metadata' configLet where this information is stored
1447 found_in_cvp
= False
1449 cvp_cl
= self
.client
.api
.get_configlet_by_name(self
.__OSM
_METADATA
)
1451 except CvpApiError
as error
:
1452 if "Entity does not exist" in error
.msg
:
1458 lines
= cvp_cl
['config'].split('\n')
1460 if self
.__METADATA
_PREFIX
in line
:
1461 line
= line
.split(' ')
1462 serv
= {'uuid': line
[4], 'type': line
[2], 'vlan': line
[3]}
1465 if (serv
is not None and
1467 serv
not in serv_list
):
1468 serv_list
.append(serv
)
1472 def __get_Connection(self
):
1473 """ Open a connection with Arista CloudVision,
1474 invoking the version retrival as test
1477 if self
.client
is None:
1478 self
.client
= self
.__connect
()
1479 self
.client
.api
.get_cvp_info()
1480 except (CvpSessionLogOutError
, RequestException
) as e
:
1481 self
.logger
.debug("Connection error '{}'. Reconnecting".format(e
))
1482 self
.client
= self
.__connect
()
1483 self
.client
.api
.get_cvp_info()
1485 def __connect(self
):
1486 ''' Connects to CVP device using user provided credentials from initialization.
1487 :return: CvpClient object with connection instantiated.
1489 client
= CvpClient()
1490 protocol
, _
, rest_url
= self
.__wim
_url
.rpartition("://")
1491 host
, _
, port
= rest_url
.partition(":")
1492 if port
and port
.endswith("/"):
1493 port
= int(port
[:-1])
1499 client
.connect([host
],
1502 protocol
=protocol
or "https",
1505 client
.api
= CvpApi(client
, request_timeout
=self
.__API
_REQUEST
_TOUT
)
1506 self
.taskC
= AristaCVPTask(client
.api
)
1509 def __compare(self
, fromText
, toText
, lines
=10):
1510 """ Compare text string in 'fromText' with 'toText' and produce
1511 diffRatio - a score as a float in the range [0, 1] 2.0*M / T
1512 T is the total number of elements in both sequences,
1513 M is the number of matches.
1514 Score - 1.0 if the sequences are identical, and
1515 0.0 if they have nothing in common.
1518 '- ' line unique to sequence 1
1519 '+ ' line unique to sequence 2
1520 ' ' line common to both sequences
1521 '? ' line not present in either input sequence
1523 fromlines
= fromText
.splitlines(1)
1524 tolines
= toText
.splitlines(1)
1525 diff
= list(difflib
.unified_diff(fromlines
, tolines
, n
=lines
))
1526 textComp
= difflib
.SequenceMatcher(None, fromText
, toText
)
1527 diffRatio
= round(textComp
.quick_ratio()*100, 2)
1528 return [diffRatio
, diff
]
1530 def __load_inventory(self
):
1531 """ Get Inventory Data for All Devices (aka switches) from the Arista CloudVision
1533 if not self
.cvp_inventory
:
1534 self
.cvp_inventory
= self
.client
.api
.get_inventory()
1535 self
.allDeviceFacts
= []
1536 for device
in self
.cvp_inventory
:
1537 self
.allDeviceFacts
.append(device
)
1539 def __get_tags(self
, name
, value
):
1540 if not self
.cvp_tags
:
1542 url
= '/api/v1/rest/analytics/tags/labels/devices/{}/value/{}/elements'.format(name
, value
)
1543 self
.logger
.debug('get_tags: URL {}'.format(url
))
1544 data
= self
.client
.get(url
, timeout
=self
.__API
_REQUEST
_TOUT
)
1545 for dev
in data
['notifications']:
1546 for elem
in dev
['updates']:
1547 self
.cvp_tags
.append(elem
)
1548 self
.logger
.debug('Available devices with tag_name {} - value {}: {} '.format(name
, value
, self
.cvp_tags
))
1550 def is_valid_destination(self
, url
):
1551 """ Check that the provided WIM URL is correct
1553 if re
.match(self
.__regex
, url
):
1555 elif self
.is_valid_ipv4_address(url
):
1558 return self
.is_valid_ipv6_address(url
)
1560 def is_valid_ipv4_address(self
, address
):
1561 """ Checks that the given IP is IPv4 valid
1564 socket
.inet_pton(socket
.AF_INET
, address
)
1565 except AttributeError: # no inet_pton here, sorry
1567 socket
.inet_aton(address
)
1568 except socket
.error
:
1570 return address
.count('.') == 3
1571 except socket
.error
: # not a valid address
1575 def is_valid_ipv6_address(self
, address
):
1576 """ Checks that the given IP is IPv6 valid
1579 socket
.inet_pton(socket
.AF_INET6
, address
)
1580 except socket
.error
: # not a valid address
1584 def delete_keys_from_dict(self
, dict_del
, lst_keys
):
1585 dict_copy
= {k
: v
for k
, v
in dict_del
.items() if k
not in lst_keys
}
1586 for k
, v
in dict_copy
.items():
1587 if isinstance(v
, dict):
1588 dict_copy
[k
] = self
.delete_keys_from_dict(v
, lst_keys
)