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 __OSM_PREFIX
= "osm_"
122 __OSM_METADATA
= "OSM_metadata"
123 __METADATA_PREFIX
= '!## Service'
124 __EXC_TASK_EXEC_WAIT
= 1
125 __ROLLB_TASK_EXEC_WAIT
= 5
127 def __init__(self
, wim
, wim_account
, config
=None, logger
=None):
130 :param wim: (dict). Contains among others 'wim_url'
131 :param wim_account: (dict). Contains among others 'uuid' (internal id), 'name',
132 'sdn' (True if is intended for SDN-assist or False if intended for WIM), 'user', 'password'.
133 :param config: (dict or None): Particular information of plugin. These keys if present have a common meaning:
134 'mapping_not_needed': (bool) False by default or if missing, indicates that mapping is not needed.
135 'service_endpoint_mapping': (list) provides the internal endpoint mapping. The meaning is:
136 KEY meaning for WIM meaning for SDN assist
137 -------- -------- --------
138 device_id pop_switch_dpid compute_id
139 device_interface_id pop_switch_port compute_pci_address
140 service_endpoint_id wan_service_endpoint_id SDN_service_endpoint_id
141 service_mapping_info wan_service_mapping_info SDN_service_mapping_info
142 contains extra information if needed. Text in Yaml format
143 switch_dpid wan_switch_dpid SDN_switch_dpid
144 switch_port wan_switch_port SDN_switch_port
145 datacenter_id vim_account vim_account
146 id: (internal, do not use)
147 wim_id: (internal, do not use)
148 :param logger (logging.Logger): optional logger object. If none is passed 'openmano.sdn.sdnconn' is used.
150 self
.__regex
= re
.compile(
151 r
'^(?:http|ftp)s?://' # http:// or https://
152 r
'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # domain...
153 r
'localhost|' # localhost...
154 r
'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip
155 r
'(?::\d+)?', re
.IGNORECASE
) # optional port
156 self
.raiseException
= True
157 self
.logger
= logger
or logging
.getLogger(self
.__WIM
_LOGGER
)
158 super().__init
__(wim
, wim_account
, config
, self
.logger
)
160 self
.__wim
_account
= wim_account
161 self
.__config
= config
162 if self
.is_valid_destination(self
.__wim
.get("wim_url")):
163 self
.__wim
_url
= self
.__wim
.get("wim_url")
165 raise SdnConnectorError(message
='Invalid wim_url value',
167 self
.__user
= wim_account
.get("user")
168 self
.__passwd
= wim_account
.get("password")
170 self
.cvp_inventory
= None
171 self
.logger
.debug("Arista SDN assist {}, user:{} and conf:{}".
172 format(wim
, self
.__user
, config
))
173 self
.allDeviceFacts
= []
174 self
.__load
_switches
()
175 self
.clC
= AristaSDNConfigLet()
177 self
.sw_loopback0
= {}
181 # Each switch has a different loopback address,
182 # so it's a different configLet
183 inf
= self
.__get
_switch
_interface
_ip
(s
, 'Loopback0')
184 self
.sw_loopback0
[s
] = inf
.split('/')[0]
185 self
.bgp
[s
] = self
.__get
_switch
_asn
(s
)
187 def __load_switches(self
):
188 """ Retrieves the switches to configure in the following order
189 1.- from incomming configuration
190 2.- Looking in the CloudVision inventory for those switches whose hostname starts with 'leaf'
192 if not self
.__config
or not self
.__config
.get('switches'):
193 if self
.client
is None:
194 self
.client
= self
.__connect
()
195 self
.__load
_inventory
()
197 for device
in self
.allDeviceFacts
:
198 if device
['hostname'].startswith('Leaf'):
199 switch_data
= {"passwd": self
.__passwd
,
200 "ip": device
['ipAddress'],
202 self
.switches
[device
['hostname']] = switch_data
203 if len(self
.switches
) == 0:
204 self
.logger
.error("Unable to load Leaf switches from CVP")
208 self
.switches
= self
.__config
.get
['switches']
209 # self.s_api are switch objects, one for each switch in self.switches,
210 # used to make eAPI calls by using switch.py module
212 for s
in self
.switches
:
213 self
.logger
.debug("Using Arista Leaf switch: {} {} {}".format(
215 self
.switches
[s
]["ip"],
216 self
.switches
[s
]["usr"]))
217 if self
.is_valid_destination(self
.switches
[s
]["ip"]):
218 self
.s_api
[s
] = AristaSwitch(host
=self
.switches
[s
]["ip"],
219 user
=self
.switches
[s
]["usr"],
220 passwd
=self
.switches
[s
]["passwd"],
223 def __lldp_find_neighbor(self
, tlv_name
=None, tlv_value
=None):
224 """Returns a list of dicts where a mathing LLDP neighbor has been found
226 switch -> switch name
227 interface -> switch interface
232 # Get LLDP info from each switch
234 result
= self
.s_api
[s
].run("show lldp neighbors detail")
235 lldp_info
[s
] = result
[0]["lldpNeighbors"]
236 # Look LLDP match on each interface
237 # Note that eAPI returns [] for an interface with no LLDP neighbors
238 # in the corresponding interface lldpNeighborInfo field
239 for interface
in lldp_info
[s
]:
240 if lldp_info
[s
][interface
]["lldpNeighborInfo"]:
241 lldp_nInf
= lldp_info
[s
][interface
]["lldpNeighborInfo"][0]
242 if tlv_name
in lldp_nInf
:
243 if lldp_nInf
[tlv_name
] == tlv_value
:
244 r
.append({"name": s
, "interface": interface
})
248 def __get_switch_asn(self
, switch
):
249 """Returns switch ASN in default VRF
251 bgp_info
= self
.s_api
[switch
].run("show ip bgp summary")[0]
252 return(bgp_info
["vrfs"]["default"]["asn"])
254 def __get_switch_po(self
, switch
, interface
=None):
255 """Returns Port-Channels for a given interface
256 If interface is None returns a list with all PO interfaces
257 Note that if specified, interface should be exact name
258 for instance: Ethernet3 and not e3 eth3 and so on
260 po_inf
= self
.s_api
[switch
].run("show port-channel")[0]["portChannels"]
263 r
= [x
for x
in po_inf
if interface
in po_inf
[x
]["activePorts"]]
269 def __get_switch_interface_ip(self
, switch
, interface
=None):
270 """Returns interface primary ip
271 interface should be exact name
272 for instance: Ethernet3 and not ethernet 3, e3 eth3 and so on
274 cmd
= "show ip interface {}".format(interface
)
275 ip_info
= self
.s_api
[switch
].run(cmd
)[0]["interfaces"][interface
]
277 ip
= ip_info
["interfaceAddress"]["primaryIp"]["address"]
278 mask
= ip_info
["interfaceAddress"]["primaryIp"]["maskLen"]
280 return "{}/{}".format(ip
, mask
)
282 def __check_service(self
, service_type
, connection_points
,
283 check_vlan
=True, check_num_cp
=True, kwargs
=None):
284 """ Reviews the connection points elements looking for semantic errors in the incoming data
286 if service_type
not in self
.__supported
_service
_types
:
287 raise Exception("The service '{}' is not supported. Only '{}' are accepted".format(
289 self
.__supported
_service
_types
))
292 if (len(connection_points
) < 2):
293 raise Exception(SdnError
.CONNECTION_POINTS_SIZE
)
294 if ((len(connection_points
) != self
.__ELINE
_num
_connection
_points
) and
295 (service_type
== self
.__service
_types
_ELINE
)):
296 raise Exception(SdnError
.CONNECTION_POINTS_SIZE
)
300 for cp
in connection_points
:
301 enc_type
= cp
.get(self
.__ENCAPSULATION
_TYPE
_PARAM
)
303 enc_type
not in self
.__supported
_encapsulation
_types
):
304 raise Exception(SdnError
.ENCAPSULATION_TYPE
)
305 encap_info
= cp
.get(self
.__ENCAPSULATION
_INFO
_PARAM
)
306 cp_vlan_id
= str(encap_info
.get(self
.__VLAN
_PARAM
))
310 elif vlan_id
!= cp_vlan_id
:
311 raise Exception(SdnError
.VLAN_INCONSISTENT
)
313 raise Exception(SdnError
.VLAN_NOT_PROVIDED
)
314 if vlan_id
in self
.__get
_srvVLANs
():
315 raise Exception('VLAN {} already assigned to a connectivity service'.format(vlan_id
))
317 # Commented out for as long as parameter isn't implemented
318 # bandwidth = kwargs.get(self.__BANDWIDTH_PARAM)
319 # if not isinstance(bandwidth, int):
320 # self.__exception(SdnError.BANDWIDTH, http_code=400)
322 # Commented out for as long as parameter isn't implemented
323 # backup = kwargs.get(self.__BACKUP_PARAM)
324 # if not isinstance(backup, bool):
325 # self.__exception(SdnError.BACKUP, http_code=400)
327 def check_credentials(self
):
328 """Retrieves the CloudVision version information, as the easiest way
329 for testing the access to CloudVision API
332 if self
.client
is None:
333 self
.client
= self
.__connect
()
334 result
= self
.client
.api
.get_cvp_info()
335 self
.logger
.debug(result
)
336 except CvpLoginError
as e
:
337 self
.logger
.info(str(e
))
339 raise SdnConnectorError(message
=SdnError
.UNAUTHORIZED
,
340 http_code
=401) from e
341 except Exception as ex
:
343 self
.logger
.error(str(ex
))
344 raise SdnConnectorError(message
=SdnError
.INTERNAL_ERROR
,
345 http_code
=500) from ex
347 def get_connectivity_service_status(self
, service_uuid
, conn_info
=None):
348 """Monitor the status of the connectivity service established
350 service_uuid (str): UUID of the connectivity service
351 conn_info (dict or None): Information returned by the connector
352 during the service creation/edition and subsequently stored in
356 dict: JSON/YAML-serializable dict that contains a mandatory key
357 ``sdn_status`` associated with one of the following values::
359 {'sdn_status': 'ACTIVE'}
360 # The service is up and running.
362 {'sdn_status': 'INACTIVE'}
363 # The service was created, but the connector
364 # cannot determine yet if connectivity exists
365 # (ideally, the caller needs to wait and check again).
367 {'sdn_status': 'DOWN'}
368 # Connection was previously established,
369 # but an error/failure was detected.
371 {'sdn_status': 'ERROR'}
372 # An error occurred when trying to create the service/
373 # establish the connectivity.
375 {'sdn_status': 'BUILD'}
376 # Still trying to create the service, the caller
377 # needs to wait and check again.
379 Additionally ``error_msg``(**str**) and ``sdn_info``(**dict**)
380 keys can be used to provide additional status explanation or
381 new information available for the connectivity service.
385 raise SdnConnectorError(message
='No connection service UUID',
388 self
.__get
_Connection
()
389 if conn_info
is None:
390 raise SdnConnectorError(message
='No connection information for service UUID {}'.format(service_uuid
),
393 if 'configLetPerSwitch' in conn_info
.keys():
397 cls_perSw
= self
.__get
_serviceData
(service_uuid
,
398 conn_info
['service_type'],
399 conn_info
['vlan_id'],
402 t_isCancelled
= False
407 if (len(cls_perSw
[s
]) > 0):
408 for cl
in cls_perSw
[s
]:
409 if len(cls_perSw
[s
][0]['config']) == 0:
412 t_id
= note
.split(self
.__SEPARATOR
)[1]
413 result
= self
.client
.api
.get_task_by_id(t_id
)
414 if result
['workOrderUserDefinedStatus'] == 'Completed':
416 elif result
['workOrderUserDefinedStatus'] == 'Cancelled':
418 elif result
['workOrderUserDefinedStatus'] == 'Failed':
422 failed_switches
.append(s
)
424 error_msg
= 'Some works were cancelled in switches: {}'.format(str(failed_switches
))
427 error_msg
= 'Some works failed in switches: {}'.format(str(failed_switches
))
430 error_msg
= 'Some works are still under execution in switches: {}'.format(str(failed_switches
))
434 sdn_status
= 'ACTIVE'
436 return {'sdn_status': sdn_status
,
437 'error_msg': error_msg
,
438 'sdn_info': sdn_info
}
439 except CvpLoginError
as e
:
440 self
.logger
.info(str(e
))
442 raise SdnConnectorError(message
=SdnError
.UNAUTHORIZED
,
443 http_code
=401) from e
444 except Exception as ex
:
446 self
.logger
.error(str(ex
), exc_info
=True)
447 raise SdnConnectorError(message
=str(ex
),
448 http_code
=500) from ex
450 def create_connectivity_service(self
, service_type
, connection_points
,
452 """Stablish SDN/WAN connectivity between the endpoints
454 (str): ``ELINE`` (L2), ``ELAN`` (L2), ``ETREE`` (L2), ``L3``.
455 :param connection_points: (list): each point corresponds to
456 an entry point to be connected. For WIM: from the DC
457 to the transport network.
458 For SDN: Compute/PCI to the transport network. One
459 connection point serves to identify the specific access and
460 some other service parameters, such as encapsulation type.
461 Each item of the list is a dict with:
462 "service_endpoint_id": (str)(uuid) Same meaning that for
463 'service_endpoint_mapping' (see __init__)
464 In case the config attribute mapping_not_needed is True,
465 this value is not relevant. In this case
466 it will contain the string "device_id:device_interface_id"
467 "service_endpoint_encapsulation_type": None, "dot1q", ...
468 "service_endpoint_encapsulation_info": (dict) with:
469 "vlan": ..., (int, present if encapsulation is dot1q)
470 "vni": ... (int, present if encapsulation is vxlan),
471 "peers": [(ipv4_1), (ipv4_2)] (present if
472 encapsulation is vxlan)
474 "device_id": ..., same meaning that for
475 'service_endpoint_mapping' (see __init__)
476 "device_interface_id": same meaning that for
477 'service_endpoint_mapping' (see __init__)
478 "switch_dpid": ..., present if mapping has been found
479 for this device_id,device_interface_id
480 "swith_port": ... present if mapping has been found
481 for this device_id,device_interface_id
482 "service_mapping_info": present if mapping has
483 been found for this device_id,device_interface_id
484 :param kwargs: For future versions:
485 bandwidth (int): value in kilobytes
486 latency (int): value in milliseconds
487 Other QoS might be passed as keyword arguments.
488 :return: tuple: ``(service_id, conn_info)`` containing:
489 - *service_uuid* (str): UUID of the established
491 - *conn_info* (dict or None): Information to be
492 stored at the database (or ``None``).
493 This information will be provided to the
494 :meth:`~.edit_connectivity_service` and :obj:`~.delete`.
495 **MUST** be JSON/YAML-serializable (plain data structures).
496 :raises: SdnConnectorError: In case of error. Nothing should be
497 created in this case.
498 Provide the parameter http_code
501 self
.logger
.debug("invoked create_connectivity_service '{}' ports: {}".
502 format(service_type
, connection_points
))
503 self
.__get
_Connection
()
504 self
.__check
_service
(service_type
,
508 service_uuid
= str(uuid
.uuid4())
510 self
.logger
.info("Service with uuid {} created.".
511 format(service_uuid
))
512 s_uid
, s_connInf
= self
.__processConnection
(
518 self
.__addMetadata
(s_uid
, service_type
, s_connInf
['vlan_id'])
519 except Exception as e
:
522 return (s_uid
, s_connInf
)
523 except CvpLoginError
as e
:
524 self
.logger
.info(str(e
))
526 raise SdnConnectorError(message
=SdnError
.UNAUTHORIZED
,
527 http_code
=401) from e
528 except SdnConnectorError
as sde
:
530 except Exception as ex
:
532 self
.logger
.error(str(ex
), exc_info
=True)
533 if self
.raiseException
:
535 raise SdnConnectorError(message
=str(ex
),
536 http_code
=500) from ex
538 def __processConnection(self
,
544 Invoked from creation and edit methods
546 Process the connection points array,
547 creating a set of configuration per switch where it has to be applied
548 for creating the configuration, the switches have to be queried for obtaining:
549 - the loopback address
550 - the BGP ASN (autonomous system number)
551 - the interface name of the MAC address to add in the connectivity service
552 Once the new configuration is ready, the __updateConnection method is invoked for appling the changes
561 vlan_processed
= False
564 processed_connection_points
= []
565 for cp
in connection_points
:
567 encap_info
= cp
.get(self
.__ENCAPSULATION
_INFO
_PARAM
)
568 if not vlan_processed
:
569 vlan_id
= str(encap_info
.get(self
.__VLAN
_PARAM
))
572 vni_id
= encap_info
.get(self
.__VNI
_PARAM
)
574 vni_id
= str(10000 + int(vlan_id
))
576 if service_type
== self
.__service
_types
_ELAN
:
577 cl_vlan
= self
.clC
.getElan_vlan(service_uuid
,
581 cl_vlan
= self
.clC
.getEline_vlan(service_uuid
,
584 vlan_processed
= True
586 encap_type
= cp
.get(self
.__ENCAPSULATION
_TYPE
_PARAM
)
587 switch_id
= encap_info
.get(self
.__SW
_ID
_PARAM
)
589 point_mac
= encap_info
.get(self
.__MAC
_PARAM
)
590 switches
= self
.__lldp
_find
_neighbor
("chassisId", point_mac
)
592 if len(switches
) == 0:
593 raise SdnConnectorError(message
="Connection point MAC address {} not found in the switches".format(point_mac
),
595 self
.logger
.debug("Found connection point for MAC {}: {}".
596 format(point_mac
, switches
))
597 port_channel
= self
.__get
_switch
_po
(switch
['name'],
599 if len(port_channel
) > 0:
600 interface
= port_channel
[0]
602 interface
= switch
['interface']
604 interface
= encap_info
.get(self
.__SW
_PORT
_PARAM
)
605 switches
= [{'name': switch_id
, 'interface': interface
}]
608 raise SdnConnectorError(message
="Connection point switch port empty for switch_dpid {}".format(switch_id
),
610 # remove those connections that are equal. This happens when several sriovs are located in the same
611 # compute node interface, that is, in the same switch and interface
612 switches
= [x
for x
in switches
if x
not in processed_connection_points
]
615 processed_connection_points
+= switches
616 for switch
in switches
:
617 # it should be only one switch where the mac is attached
618 if encap_type
== 'dot1q':
619 # SRIOV configLet for Leaf switch mac's attached to
620 if service_type
== self
.__service
_types
_ELAN
:
621 cl_encap
= self
.clC
.getElan_sriov(service_uuid
, interface
, vlan_id
, i
)
623 cl_encap
= self
.clC
.getEline_sriov(service_uuid
, interface
, vlan_id
, i
)
625 # PT configLet for Leaf switch attached to the mac
626 if service_type
== self
.__service
_types
_ELAN
:
627 cl_encap
= self
.clC
.getElan_passthrough(service_uuid
,
631 cl_encap
= self
.clC
.getEline_passthrough(service_uuid
,
634 if cls_cp
.get(switch
['name']):
635 cls_cp
[switch
['name']] = str(cls_cp
[switch
['name']]) + cl_encap
637 cls_cp
[switch
['name']] = cl_encap
639 # at least 1 connection point has to be received
640 if not vlan_processed
:
641 raise SdnConnectorError(message
=SdnError
.UNSUPPORTED_FEATURE
,
645 # for cl in cp_configLets:
646 cl_name
= (self
.__OSM
_PREFIX
+
648 self
.__SEPARATOR
+ service_type
+ str(vlan_id
) +
649 self
.__SEPARATOR
+ service_uuid
)
650 # apply VLAN and BGP configLet to all Leaf switches
651 if service_type
== self
.__service
_types
_ELAN
:
652 cl_bgp
[s
] = self
.clC
.getElan_bgp(service_uuid
,
655 self
.sw_loopback0
[s
],
658 cl_bgp
[s
] = self
.clC
.getEline_bgp(service_uuid
,
661 self
.sw_loopback0
[s
],
664 if not cls_cp
.get(s
):
667 cl_config
= str(cl_vlan
) + str(cl_bgp
[s
]) + str(cls_cp
[s
])
669 cls_perSw
[s
] = [{'name': cl_name
, 'config': cl_config
}]
671 allLeafConfigured
, allLeafModified
= self
.__updateConnection
(cls_perSw
)
674 "uuid": service_uuid
,
676 "service_type": service_type
,
678 "connection_points": connection_points
,
679 "configLetPerSwitch": cls_perSw
,
680 'allLeafConfigured': allLeafConfigured
,
681 'allLeafModified': allLeafModified
}
683 return service_uuid
, conn_info
684 except Exception as ex
:
685 self
.logger
.debug("Exception processing connection {}: {}".
686 format(service_uuid
, str(ex
)))
689 def __updateConnection(self
, cls_perSw
):
690 """ Invoked in the creation and modification
692 checks if the new connection points config is:
693 - already in the Cloud Vision, the configLet is modified, and applied to the switch,
694 executing the corresponding task
695 - if it has to be removed:
696 then configuration has to be removed from the switch executing the corresponding task,
697 before trying to remove the configuration
698 - created, the configuration set is created, associated to the switch, and the associated
699 task to the configLet modification executed
700 In case of any error, rollback is executed, removing the created elements, and restoring to the
704 allLeafConfigured
= {}
708 allLeafConfigured
[s
] = False
709 allLeafModified
[s
] = False
713 toDelete_in_cvp
= False
714 if not (cls_perSw
.get(s
) and cls_perSw
[s
][0].get('config')):
715 # when there is no configuration, means that there is no interface
716 # in the switch to be connected, so the configLet has to be removed from CloudVision
717 # after removing the ConfigLet fron the switch if it was already there
719 # get config let name and key
722 cvp_cl
= self
.client
.api
.get_configlet_by_name(cl
[0]['name'])
724 cl_toDelete
.append(cvp_cl
)
726 toDelete_in_cvp
= True
727 except CvpApiError
as error
:
728 if "Entity does not exist" in error
.msg
:
732 # remove configLet from device
734 res
= self
.__configlet
_modify
(cls_perSw
[s
])
735 allLeafConfigured
[s
] = res
[0]
736 if not allLeafConfigured
[s
]:
739 res
= self
.__device
_modify
(
742 delete
=toDelete_in_cvp
)
743 if "errorMessage" in str(res
):
744 raise Exception(str(res
))
745 self
.logger
.info("Device {} modify result {}".format(s
, res
))
746 for t_id
in res
[1]['tasks']:
747 tasks
[t_id
] = {'workOrderId': t_id
}
748 if not toDelete_in_cvp
:
749 note_msg
= "## Managed by OSM {}{}{}##".format(self
.__SEPARATOR
,
752 self
.client
.api
.add_note_to_configlet(
753 cls_perSw
[s
][0]['key'],
755 cls_perSw
[s
][0].update([('note', note_msg
)])
756 # with just one configLet assigned to a device,
757 # delete all if there are errors in next loops
758 if not toDelete_in_cvp
:
759 allLeafModified
[s
] = True
760 if self
.taskC
is None:
762 data
= self
.taskC
.update_all_tasks(tasks
).values()
763 self
.taskC
.task_action(data
,
764 self
.__EXC
_TASK
_EXEC
_WAIT
,
766 if len(cl_toDelete
) > 0:
767 self
.__configlet
_modify
(cl_toDelete
, delete
=True)
768 return allLeafConfigured
, allLeafModified
769 except Exception as ex
:
771 self
.__rollbackConnection
(cls_perSw
,
772 allLeafConfigured
=True,
773 allLeafModified
=True)
774 except Exception as e
:
775 self
.logger
.info("Exception rolling back in updating connection: {}".
779 def __rollbackConnection(self
,
783 """ Removes the given configLet from the devices and then remove the configLets
787 if allLeafModified
[s
]:
789 res
= self
.__device
_modify
(
791 new_configlets
=cls_perSw
[s
],
793 if "errorMessage" in str(res
):
794 raise Exception(str(res
))
795 for t_id
in res
[1]['tasks']:
796 tasks
[t_id
] = {'workOrderId': t_id
}
797 self
.logger
.info("Device {} modify result {}".format(s
, res
))
798 except Exception as e
:
799 self
.logger
.info('Error removing configlets from device {}: {}'.format(s
, e
))
801 if self
.taskC
is None:
803 data
= self
.taskC
.update_all_tasks(tasks
).values()
804 self
.taskC
.task_action(data
,
805 self
.__ROLLB
_TASK
_EXEC
_WAIT
,
808 if allLeafConfigured
[s
]:
809 self
.__configlet
_modify
(cls_perSw
[s
], delete
=True)
811 def __device_modify(self
, device_to_update
, new_configlets
, delete
):
812 """ Updates the devices (switches) adding or removing the configLet,
813 the tasks Id's associated to the change are returned
815 self
.logger
.info('Enter in __device_modify delete: {}'.format(
819 # Task Ids that have been identified during device actions
822 if (len(new_configlets
) == 0 or
823 device_to_update
is None or
824 len(device_to_update
) == 0):
825 data
= {'updated': updated
, 'tasks': newTasks
}
826 return [changed
, data
]
828 self
.__load
_inventory
()
830 allDeviceFacts
= self
.allDeviceFacts
831 # Work through Devices list adding device specific information
833 for try_device
in allDeviceFacts
:
834 # Add Device Specific Configlets
835 # self.logger.debug(device)
836 if try_device
['hostname'] not in device_to_update
:
838 dev_cvp_configlets
= self
.client
.api
.get_configlets_by_device_id(
839 try_device
['systemMacAddress'])
840 # self.logger.debug(dev_cvp_configlets)
841 try_device
['deviceSpecificConfiglets'] = []
842 for cvp_configlet
in dev_cvp_configlets
:
843 if int(cvp_configlet
['containerCount']) == 0:
844 try_device
['deviceSpecificConfiglets'].append(
845 {'name': cvp_configlet
['name'],
846 'key': cvp_configlet
['key']})
847 # self.logger.debug(device)
851 # Check assigned configlets
852 device_update
= False
854 remove_configlets
= []
858 for cvp_configlet
in device
['deviceSpecificConfiglets']:
859 for cl
in new_configlets
:
860 if cvp_configlet
['name'] == cl
['name']:
861 remove_configlets
.append(cvp_configlet
)
864 for configlet
in new_configlets
:
865 if configlet
not in device
['deviceSpecificConfiglets']:
866 add_configlets
.append(configlet
)
869 update_devices
.append({'hostname': device
['hostname'],
870 'configlets': [add_configlets
,
873 self
.logger
.info("Device to modify: {}".format(update_devices
))
875 up_device
= update_devices
[0]
876 cl_toAdd
= up_device
['configlets'][0]
877 cl_toDel
= up_device
['configlets'][1]
880 if delete
and len(cl_toDel
) > 0:
881 r
= self
.client
.api
.remove_configlets_from_device(
887 self
.logger
.debug("remove_configlets_from_device {} {}".format(dev_action
, cl_toDel
))
888 elif len(cl_toAdd
) > 0:
889 r
= self
.client
.api
.apply_configlets_to_device(
895 self
.logger
.debug("apply_configlets_to_device {} {}".format(dev_action
, cl_toAdd
))
897 except Exception as error
:
898 errorMessage
= str(error
)
899 msg
= "errorMessage: Device {} Configlets couldnot be updated: {}".format(
900 up_device
['hostname'], errorMessage
)
901 raise SdnConnectorError(msg
) from error
903 if "errorMessage" in str(dev_action
):
904 m
= "Device {} Configlets update fail: {}".format(
905 up_device
['name'], dev_action
['errorMessage'])
906 raise SdnConnectorError(m
)
909 if 'taskIds' in str(dev_action
):
910 for taskId
in dev_action
['data']['taskIds']:
911 updated
.append({up_device
['hostname']:
912 "Configlets-{}".format(
914 newTasks
.append(taskId
)
916 updated
.append({up_device
['hostname']:
917 "Configlets-No_Specific_Tasks"})
918 data
= {'updated': updated
, 'tasks': newTasks
}
919 return [changed
, data
]
921 def __configlet_modify(self
, configletsToApply
, delete
=False):
922 ''' adds/update or delete the provided configLets
923 :param configletsToApply: list of configLets to apply
924 :param delete: flag to indicate if the configLets have to be deleted
925 from Cloud Vision Portal
926 :return: data: dict of module actions and taskIDs
928 self
.logger
.info('Enter in __configlet_modify delete:{}'.format(
931 # Compare configlets against cvp_facts-configlets
938 for cl
in configletsToApply
:
945 cvp_cl
= self
.client
.api
.get_configlet_by_name(cl
['name'])
946 cl
['key'] = cvp_cl
['key']
947 cl
['note'] = cvp_cl
['note']
949 except CvpApiError
as error
:
950 if "Entity does not exist" in error
.msg
:
958 configlet
= {'name': cvp_cl
['name'],
962 cl_compare
= self
.__compare
(cl
['config'],
964 # compare function returns a floating point number
965 if cl_compare
[0] != 100.0:
967 configlet
= {'name': cl
['name'],
969 'config': cl
['config']}
972 configlet
= {'name': cl
['name'],
973 'key': cvp_cl
['key'],
975 'config': cl
['config']}
978 configlet
= {'name': cl
['name'],
979 'config': cl
['config']}
983 resp
= self
.client
.api
.delete_configlet(
984 configlet
['data']['name'],
985 configlet
['data']['key'])
988 resp
= self
.client
.api
.update_configlet(
990 configlet
['data']['key'],
991 configlet
['data']['name'])
994 resp
= self
.client
.api
.add_configlet(
998 operation
= 'checked'
1000 except Exception as error
:
1001 errorMessage
= str(error
).split(':')[-1]
1002 message
= "Configlet {} cannot be {}: {}".format(
1003 cl
['name'], operation
, errorMessage
)
1005 deleted
.append({configlet
['name']: message
})
1007 updated
.append({configlet
['name']: message
})
1009 new
.append({configlet
['name']: message
})
1011 checked
.append({configlet
['name']: message
})
1014 if "error" in str(resp
).lower():
1015 message
= "Configlet {} cannot be deleted: {}".format(
1016 cl
['name'], resp
['errorMessage'])
1018 deleted
.append({configlet
['name']: message
})
1020 updated
.append({configlet
['name']: message
})
1022 new
.append({configlet
['name']: message
})
1024 checked
.append({configlet
['name']: message
})
1028 deleted
.append({configlet
['name']: "success"})
1031 updated
.append({configlet
['name']: "success"})
1034 cl
['key'] = resp
# This key is used in API call deviceApplyConfigLet FGA
1035 new
.append({configlet
['name']: "success"})
1038 checked
.append({configlet
['name']: "success"})
1040 data
= {'new': new
, 'updated': updated
, 'deleted': deleted
, 'checked': checked
}
1041 return [changed
, data
]
1043 def __get_configletsDevices(self
, configlets
):
1044 for s
in self
.s_api
:
1045 configlet
= configlets
[s
]
1046 # Add applied Devices
1047 if len(configlet
) > 0:
1048 configlet
['devices'] = []
1049 applied_devices
= self
.client
.api
.get_applied_devices(
1051 for device
in applied_devices
['data']:
1052 configlet
['devices'].append(device
['hostName'])
1054 def __get_serviceData(self
, service_uuid
, service_type
, vlan_id
, conn_info
=None):
1056 for s
in self
.s_api
:
1059 srv_cls
= self
.__get
_serviceConfigLets
(service_uuid
,
1062 self
.__get
_configletsDevices
(srv_cls
)
1063 for s
in self
.s_api
:
1066 for dev
in cl
['devices']:
1067 cls_perSw
[dev
].append(cl
)
1069 cls_perSw
= conn_info
['configLetPerSwitch']
1072 def delete_connectivity_service(self
, service_uuid
, conn_info
=None):
1074 Disconnect multi-site endpoints previously connected
1076 :param service_uuid: The one returned by create_connectivity_service
1077 :param conn_info: The one returned by last call to 'create_connectivity_service' or 'edit_connectivity_service'
1078 if they do not return None
1080 :raises: SdnConnectorException: In case of error. The parameter http_code must be filled
1083 self
.logger
.debug('invoked delete_connectivity_service {}'.
1084 format(service_uuid
))
1085 if not service_uuid
:
1086 raise SdnConnectorError(message
='No connection service UUID',
1089 self
.__get
_Connection
()
1090 if conn_info
is None:
1091 raise SdnConnectorError(message
='No connection information for service UUID {}'.format(service_uuid
),
1094 cls_perSw
= self
.__get
_serviceData
(service_uuid
,
1095 conn_info
['service_type'],
1096 conn_info
['vlan_id'],
1098 allLeafConfigured
= {}
1099 allLeafModified
= {}
1100 for s
in self
.s_api
:
1101 allLeafConfigured
[s
] = True
1102 allLeafModified
[s
] = True
1103 found_in_cvp
= False
1104 for s
in self
.s_api
:
1108 self
.__rollbackConnection
(cls_perSw
,
1112 # if the service is not defined in Cloud Vision, return a 404 - NotFound error
1113 raise SdnConnectorError(message
='Service {} was not found in Arista Cloud Vision {}'.
1114 format(service_uuid
, self
.__wim
_url
),
1116 self
.__removeMetadata
(service_uuid
)
1117 except CvpLoginError
as e
:
1118 self
.logger
.info(str(e
))
1120 raise SdnConnectorError(message
=SdnError
.UNAUTHORIZED
,
1121 http_code
=401) from e
1122 except SdnConnectorError
as sde
:
1124 except Exception as ex
:
1126 self
.logger
.error(ex
)
1127 if self
.raiseException
:
1129 raise SdnConnectorError(message
=SdnError
.INTERNAL_ERROR
,
1130 http_code
=500) from ex
1132 def __addMetadata(self
, service_uuid
, service_type
, vlan_id
):
1133 """ Adds the connectivity service from 'OSM_metadata' configLet
1135 found_in_cvp
= False
1137 cvp_cl
= self
.client
.api
.get_configlet_by_name(self
.__OSM
_METADATA
)
1139 except CvpApiError
as error
:
1140 if "Entity does not exist" in error
.msg
:
1145 new_serv
= '{} {} {} {}\n'.format(self
.__METADATA
_PREFIX
, service_type
, vlan_id
, service_uuid
)
1148 cl_config
= cvp_cl
['config'] + new_serv
1150 cl_config
= new_serv
1151 cl_meta
= [{'name': self
.__OSM
_METADATA
, 'config': cl_config
}]
1152 self
.__configlet
_modify
(cl_meta
)
1153 except Exception as e
:
1154 self
.logger
.error('Error in setting metadata in CloudVision from OSM for service {}: {}'.
1155 format(service_uuid
, str(e
)))
1158 def __removeMetadata(self
, service_uuid
):
1159 """ Removes the connectivity service from 'OSM_metadata' configLet
1161 found_in_cvp
= False
1163 cvp_cl
= self
.client
.api
.get_configlet_by_name(self
.__OSM
_METADATA
)
1165 except CvpApiError
as error
:
1166 if "Entity does not exist" in error
.msg
:
1172 if service_uuid
in cvp_cl
['config']:
1174 for line
in cvp_cl
['config'].split('\n'):
1175 if service_uuid
in line
:
1178 cl_config
= cl_config
+ line
1179 cl_meta
= [{'name': self
.__OSM
_METADATA
, 'config': cl_config
}]
1180 self
.__configlet
_modify
(cl_meta
)
1181 except Exception as e
:
1182 self
.logger
.error('Error in removing metadata in CloudVision from OSM for service {}: {}'.
1183 format(service_uuid
, str(e
)))
1186 def edit_connectivity_service(self
,
1189 connection_points
=None,
1191 """ Change an existing connectivity service.
1193 This method's arguments and return value follow the same convention as
1194 :meth:`~.create_connectivity_service`.
1196 :param service_uuid: UUID of the connectivity service.
1197 :param conn_info: (dict or None): Information previously returned
1198 by last call to create_connectivity_service
1199 or edit_connectivity_service
1200 :param connection_points: (list): If provided, the old list of
1201 connection points will be replaced.
1202 :param kwargs: Same meaning that create_connectivity_service
1203 :return: dict or None: Information to be updated and stored at
1205 When ``None`` is returned, no information should be changed.
1206 When an empty dict is returned, the database record will
1208 **MUST** be JSON/YAML-serializable (plain data structures).
1210 SdnConnectorError: In case of error.
1213 self
.logger
.debug('invoked edit_connectivity_service for service {}. ports: {}'.format(service_uuid
,
1216 if not service_uuid
:
1217 raise SdnConnectorError(message
='Unable to perform operation, missing or empty uuid',
1220 raise SdnConnectorError(message
='Unable to perform operation, missing or empty connection information',
1223 if connection_points
is None:
1226 self
.__get
_Connection
()
1228 cls_currentPerSw
= conn_info
['configLetPerSwitch']
1229 service_type
= conn_info
['service_type']
1231 self
.__check
_service
(service_type
,
1237 s_uid
, s_connInf
= self
.__processConnection
(
1242 self
.logger
.info("Service with uuid {} configuration updated".
1245 except CvpLoginError
as e
:
1246 self
.logger
.info(str(e
))
1248 raise SdnConnectorError(message
=SdnError
.UNAUTHORIZED
,
1249 http_code
=401) from e
1250 except SdnConnectorError
as sde
:
1252 except Exception as ex
:
1255 # TODO check if there are pending task, and cancel them before restoring
1256 self
.__updateConnection
(cls_currentPerSw
)
1257 except Exception as e
:
1258 self
.logger
.error("Unable to restore configuration in service {} after an error in the configuration updated: {}".
1259 format(service_uuid
, str(e
)))
1260 if self
.raiseException
:
1262 raise SdnConnectorError(message
=str(ex
),
1263 http_code
=500) from ex
1265 def clear_all_connectivity_services(self
):
1266 """ Removes all connectivity services from Arista CloudVision with two steps:
1267 - retrives all the services from Arista CloudVision
1268 - removes each service
1271 self
.logger
.debug('invoked AristaImpl ' +
1272 'clear_all_connectivity_services')
1273 self
.__get
_Connection
()
1274 s_list
= self
.__get
_srvUUIDs
()
1277 conn_info
['service_type'] = serv
['type']
1278 conn_info
['vlan_id'] = serv
['vlan']
1280 self
.delete_connectivity_service(serv
['uuid'], conn_info
)
1281 except CvpLoginError
as e
:
1282 self
.logger
.info(str(e
))
1284 raise SdnConnectorError(message
=SdnError
.UNAUTHORIZED
,
1285 http_code
=401) from e
1286 except SdnConnectorError
as sde
:
1288 except Exception as ex
:
1290 self
.logger
.error(ex
)
1291 if self
.raiseException
:
1293 raise SdnConnectorError(message
=SdnError
.INTERNAL_ERROR
,
1294 http_code
=500) from ex
1296 def get_all_active_connectivity_services(self
):
1297 """ Return the uuid of all the active connectivity services with two steps:
1298 - retrives all the services from Arista CloudVision
1299 - retrives the status of each server
1302 self
.logger
.debug('invoked AristaImpl {}'.format(
1303 'get_all_active_connectivity_services'))
1304 self
.__get
_Connection
()
1305 s_list
= self
.__get
_srvUUIDs
()
1309 conn_info
['service_type'] = serv
['type']
1310 conn_info
['vlan_id'] = serv
['vlan']
1312 status
= self
.get_connectivity_service_status(serv
['uuid'], conn_info
)
1313 if status
['sdn_status'] == 'ACTIVE':
1314 result
.append(serv
['uuid'])
1316 except CvpLoginError
as e
:
1317 self
.logger
.info(str(e
))
1319 raise SdnConnectorError(message
=SdnError
.UNAUTHORIZED
,
1320 http_code
=401) from e
1321 except SdnConnectorError
as sde
:
1323 except Exception as ex
:
1325 self
.logger
.error(ex
)
1326 if self
.raiseException
:
1328 raise SdnConnectorError(message
=SdnError
.INTERNAL_ERROR
,
1329 http_code
=500) from ex
1331 def __get_serviceConfigLets(self
, service_uuid
, service_type
, vlan_id
):
1332 """ Return the configLet's associated with a connectivity service,
1333 There should be one, as maximum, per device (switch) for a given
1334 connectivity service
1337 for s
in self
.s_api
:
1339 found_in_cvp
= False
1340 name
= (self
.__OSM
_PREFIX
+
1342 self
.__SEPARATOR
+ service_type
+ str(vlan_id
) +
1343 self
.__SEPARATOR
+ service_uuid
)
1345 cvp_cl
= self
.client
.api
.get_configlet_by_name(name
)
1347 except CvpApiError
as error
:
1348 if "Entity does not exist" in error
.msg
:
1356 def __get_srvVLANs(self
):
1357 """ Returns a list with all the VLAN id's used in the connectivity services managed
1358 in tha Arista CloudVision by checking the 'OSM_metadata' configLet where this
1359 information is stored
1361 found_in_cvp
= False
1363 cvp_cl
= self
.client
.api
.get_configlet_by_name(self
.__OSM
_METADATA
)
1365 except CvpApiError
as error
:
1366 if "Entity does not exist" in error
.msg
:
1372 lines
= cvp_cl
['config'].split('\n')
1374 if self
.__METADATA
_PREFIX
in line
:
1375 s_vlan
= line
.split(' ')[3]
1378 if (s_vlan
is not None and
1380 s_vlan
not in s_vlan_list
):
1381 s_vlan_list
.append(s_vlan
)
1385 def __get_srvUUIDs(self
):
1386 """ Retrieves all the connectivity services, managed in tha Arista CloudVision
1387 by checking the 'OSM_metadata' configLet where this information is stored
1389 found_in_cvp
= False
1391 cvp_cl
= self
.client
.api
.get_configlet_by_name(self
.__OSM
_METADATA
)
1393 except CvpApiError
as error
:
1394 if "Entity does not exist" in error
.msg
:
1400 lines
= cvp_cl
['config'].split('\n')
1402 if self
.__METADATA
_PREFIX
in line
:
1403 line
= line
.split(' ')
1404 serv
= {'uuid': line
[4], 'type': line
[2], 'vlan': line
[3]}
1407 if (serv
is not None and
1409 serv
not in serv_list
):
1410 serv_list
.append(serv
)
1414 def __get_Connection(self
):
1415 """ Open a connection with Arista CloudVision,
1416 invoking the version retrival as test
1419 if self
.client
is None:
1420 self
.client
= self
.__connect
()
1421 self
.client
.api
.get_cvp_info()
1422 except (CvpSessionLogOutError
, RequestException
) as e
:
1423 self
.logger
.debug("Connection error '{}'. Reconnencting".format(e
))
1424 self
.client
= self
.__connect
()
1425 self
.client
.api
.get_cvp_info()
1427 def __connect(self
):
1428 ''' Connects to CVP device using user provided credentials from initialization.
1429 :return: CvpClient object with connection instantiated.
1431 client
= CvpClient()
1432 protocol
, _
, rest_url
= self
.__wim
_url
.rpartition("://")
1433 host
, _
, port
= rest_url
.partition(":")
1434 if port
and port
.endswith("/"):
1435 port
= int(port
[:-1])
1441 client
.connect([host
],
1444 protocol
=protocol
or "https",
1447 self
.taskC
= AristaCVPTask(client
.api
)
1450 def __compare(self
, fromText
, toText
, lines
=10):
1451 """ Compare text string in 'fromText' with 'toText' and produce
1452 diffRatio - a score as a float in the range [0, 1] 2.0*M / T
1453 T is the total number of elements in both sequences,
1454 M is the number of matches.
1455 Score - 1.0 if the sequences are identical, and
1456 0.0 if they have nothing in common.
1459 '- ' line unique to sequence 1
1460 '+ ' line unique to sequence 2
1461 ' ' line common to both sequences
1462 '? ' line not present in either input sequence
1464 fromlines
= fromText
.splitlines(1)
1465 tolines
= toText
.splitlines(1)
1466 diff
= list(difflib
.unified_diff(fromlines
, tolines
, n
=lines
))
1467 textComp
= difflib
.SequenceMatcher(None, fromText
, toText
)
1468 diffRatio
= round(textComp
.quick_ratio()*100, 2)
1469 return [diffRatio
, diff
]
1471 def __load_inventory(self
):
1472 """ Get Inventory Data for All Devices (aka switches) from the Arista CloudVision
1474 if not self
.cvp_inventory
:
1475 self
.cvp_inventory
= self
.client
.api
.get_inventory()
1476 self
.allDeviceFacts
= []
1477 for device
in self
.cvp_inventory
:
1478 self
.allDeviceFacts
.append(device
)
1480 def is_valid_destination(self
, url
):
1481 """ Check that the provided WIM URL is correct
1483 if re
.match(self
.__regex
, url
):
1485 elif self
.is_valid_ipv4_address(url
):
1488 return self
.is_valid_ipv6_address(url
)
1490 def is_valid_ipv4_address(self
, address
):
1491 """ Checks that the given IP is IPv4 valid
1494 socket
.inet_pton(socket
.AF_INET
, address
)
1495 except AttributeError: # no inet_pton here, sorry
1497 socket
.inet_aton(address
)
1498 except socket
.error
:
1500 return address
.count('.') == 3
1501 except socket
.error
: # not a valid address
1505 def is_valid_ipv6_address(self
, address
):
1506 """ Checks that the given IP is IPv6 valid
1509 socket
.inet_pton(socket
.AF_INET6
, address
)
1510 except socket
.error
: # not a valid address