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
42 from cvprac
.cvp_client
import CvpClient
43 from cvprac
.cvp_client_errors
import CvpLoginError
, CvpSessionLogOutError
, CvpApiError
45 from osm_rosdn_arista
.aristaSwitch
import AristaSwitch
46 from osm_rosdn_arista
.aristaConfigLet
import AristaSDNConfigLet
47 from osm_rosdn_arista
.aristaTask
import AristaCVPTask
51 UNREACHABLE
= 'Unable to reach the WIM.',
53 'VLAN value inconsistent between the connection points',
54 VLAN_NOT_PROVIDED
= 'VLAN value not provided',
55 CONNECTION_POINTS_SIZE
= \
56 'Unexpected number of connection points: 2 expected.',
57 ENCAPSULATION_TYPE
= \
58 'Unexpected service_endpoint_encapsulation_type. \
59 Only "dotq1" is accepted.',
60 BANDWIDTH
= 'Unable to get the bandwidth.',
61 STATUS
= 'Unable to get the status for the service.',
62 DELETE
= 'Unable to delete service.',
63 CLEAR_ALL
= 'Unable to clear all the services',
64 UNKNOWN_ACTION
= 'Unknown action invoked.',
65 BACKUP
= 'Unable to get the backup parameter.',
66 UNSUPPORTED_FEATURE
= "Unsupported feature",
67 UNAUTHORIZED
= "Failed while authenticating",
68 INTERNAL_ERROR
= "Internal error"
71 class AristaSdnConnector(SdnConnectorBase
):
72 """Arista class for the SDN connectors
75 wim (dict): WIM record, as stored in the database
76 wim_account (dict): WIM account record, as stored in the database
78 The arguments of the constructor are converted to object attributes.
79 An extra property, ``service_endpoint_mapping`` is created from ``config``.
81 The access to Arista CloudVision is made through the API defined in
82 https://github.com/aristanetworks/cvprac
83 The a connectivity service consist in creating a VLAN and associate the interfaces
84 of the connection points MAC addresses to this VLAN in all the switches of the topology,
85 the BDP is also configured for this VLAN.
87 The Arista Cloud Vision API workflow is the following
88 -- The switch configuration is defined as a set of switch configuration commands,
89 what is called 'ConfigLet'
90 -- The ConfigLet is associated to the device (leaf switch)
91 -- Automatically a task is associated to this activity for change control, the task
92 in this stage is in 'Pending' state
93 -- The task will be executed so that the configuration is applied to the switch.
94 -- The service information is saved in the response of the creation call
95 -- All created services identification is stored in a generic ConfigLet 'OSM_metadata'
96 to keep track of the managed resources by OSM in the Arista deployment.
98 __supported_service_types
= ["ELINE (L2)", "ELINE", "ELAN"]
99 __service_types_ELAN
= "ELAN"
100 __service_types_ELINE
= "ELINE"
101 __ELINE_num_connection_points
= 2
102 __supported_service_types
= ["ELINE", "ELAN"]
103 __supported_encapsulation_types
= ["dot1q"]
104 __WIM_LOGGER
= 'openmano.sdnconn.arista'
105 __ENCAPSULATION_TYPE_PARAM
= "service_endpoint_encapsulation_type"
106 __ENCAPSULATION_INFO_PARAM
= "service_endpoint_encapsulation_info"
107 __BACKUP_PARAM
= "backup"
108 __BANDWIDTH_PARAM
= "bandwidth"
109 __SERVICE_ENDPOINT_PARAM
= "service_endpoint_id"
111 __WAN_SERVICE_ENDPOINT_PARAM
= "service_endpoint_id"
112 __WAN_MAPPING_INFO_PARAM
= "service_mapping_info"
113 __DEVICE_ID_PARAM
= "device_id"
114 __DEVICE_INTERFACE_ID_PARAM
= "device_interface_id"
115 __SW_ID_PARAM
= "switch_dpid"
116 __SW_PORT_PARAM
= "switch_port"
117 __VLAN_PARAM
= "vlan"
120 __OSM_PREFIX
= "osm_"
121 __OSM_METADATA
= "OSM_metadata"
122 __METADATA_PREFIX
= '!## Service'
123 __EXC_TASK_EXEC_WAIT
= 1
124 __ROLLB_TASK_EXEC_WAIT
= 5
126 def __init__(self
, wim
, wim_account
, config
=None, logger
=None):
129 :param wim: (dict). Contains among others 'wim_url'
130 :param wim_account: (dict). Contains among others 'uuid' (internal id), 'name',
131 'sdn' (True if is intended for SDN-assist or False if intended for WIM), 'user', 'password'.
132 :param config: (dict or None): Particular information of plugin. These keys if present have a common meaning:
133 'mapping_not_needed': (bool) False by default or if missing, indicates that mapping is not needed.
134 'service_endpoint_mapping': (list) provides the internal endpoint mapping. The meaning is:
135 KEY meaning for WIM meaning for SDN assist
136 -------- -------- --------
137 device_id pop_switch_dpid compute_id
138 device_interface_id pop_switch_port compute_pci_address
139 service_endpoint_id wan_service_endpoint_id SDN_service_endpoint_id
140 service_mapping_info wan_service_mapping_info SDN_service_mapping_info
141 contains extra information if needed. Text in Yaml format
142 switch_dpid wan_switch_dpid SDN_switch_dpid
143 switch_port wan_switch_port SDN_switch_port
144 datacenter_id vim_account vim_account
145 id: (internal, do not use)
146 wim_id: (internal, do not use)
147 :param logger (logging.Logger): optional logger object. If none is passed 'openmano.sdn.sdnconn' is used.
149 self
.__regex
= re
.compile(
150 r
'^(?:http|ftp)s?://' # http:// or https://
151 r
'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # domain...
152 r
'localhost|' # localhost...
153 r
'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip
154 r
'(?::\d+)?', re
.IGNORECASE
) # optional port
155 self
.raiseException
= True
156 self
.logger
= logger
or logging
.getLogger(self
.__WIM
_LOGGER
)
157 super().__init
__(wim
, wim_account
, config
, self
.logger
)
159 self
.__wim
_account
= wim_account
160 self
.__config
= config
161 if self
.is_valid_destination(self
.__wim
.get("wim_url")):
162 self
.__wim
_url
= self
.__wim
.get("wim_url")
164 raise SdnConnectorError(message
='Invalid wim_url value',
166 self
.__user
= wim_account
.get("user")
167 self
.__passwd
= wim_account
.get("password")
169 self
.cvp_inventory
= None
170 self
.logger
.debug("Arista SDN assist {}, user:{} and conf:{}".
171 format(wim
, self
.__user
, config
))
172 self
.allDeviceFacts
= []
173 self
.__load
_switches
()
174 self
.clC
= AristaSDNConfigLet()
176 self
.sw_loopback0
= {}
180 # Each switch has a different loopback address,
181 # so it's a different configLet
182 inf
= self
.__get
_switch
_interface
_ip
(s
, 'Loopback0')
183 self
.sw_loopback0
[s
] = inf
.split('/')[0]
184 self
.bgp
[s
] = self
.__get
_switch
_asn
(s
)
186 def __load_switches(self
):
187 """ Retrieves the switches to configure in the following order
188 1.- from incomming configuration
189 2.- Looking in the CloudVision inventory for those switches whose hostname starts with 'leaf'
191 if not self
.__config
or not self
.__config
.get('switches'):
192 if self
.client
is None:
193 self
.client
= self
.__connect
()
194 self
.__load
_inventory
()
196 for device
in self
.allDeviceFacts
:
197 if device
['hostname'].startswith('Leaf'):
198 switch_data
= {"passwd": self
.__passwd
,
199 "ip": device
['ipAddress'],
201 self
.switches
[device
['hostname']] = switch_data
202 if len(self
.switches
) == 0:
203 self
.logger
.error("Unable to load Leaf switches from CVP")
207 self
.switches
= self
.__config
.get
['switches']
208 # self.s_api are switch objects, one for each switch in self.switches,
209 # used to make eAPI calls by using switch.py module
211 for s
in self
.switches
:
212 self
.logger
.debug("Using Arista Leaf switch: {} {} {}".format(
214 self
.switches
[s
]["ip"],
215 self
.switches
[s
]["usr"]))
216 if self
.is_valid_destination(self
.switches
[s
]["ip"]):
217 self
.s_api
[s
] = AristaSwitch(host
=self
.switches
[s
]["ip"],
218 user
=self
.switches
[s
]["usr"],
219 passwd
=self
.switches
[s
]["passwd"],
222 def __lldp_find_neighbor(self
, tlv_name
=None, tlv_value
=None):
223 """Returns a list of dicts where a mathing LLDP neighbor has been found
225 switch -> switch name
226 interface -> switch interface
231 # Get LLDP info from each switch
233 result
= self
.s_api
[s
].run("show lldp neighbors detail")
234 lldp_info
[s
] = result
[0]["lldpNeighbors"]
235 # Look LLDP match on each interface
236 # Note that eAPI returns [] for an interface with no LLDP neighbors
237 # in the corresponding interface lldpNeighborInfo field
238 for interface
in lldp_info
[s
]:
239 if lldp_info
[s
][interface
]["lldpNeighborInfo"]:
240 lldp_nInf
= lldp_info
[s
][interface
]["lldpNeighborInfo"][0]
241 if tlv_name
in lldp_nInf
:
242 if lldp_nInf
[tlv_name
] == tlv_value
:
243 r
.append({"name": s
, "interface": interface
})
247 def __get_switch_asn(self
, switch
):
248 """Returns switch ASN in default VRF
250 bgp_info
= self
.s_api
[switch
].run("show ip bgp summary")[0]
251 return(bgp_info
["vrfs"]["default"]["asn"])
253 def __get_switch_po(self
, switch
, interface
=None):
254 """Returns Port-Channels for a given interface
255 If interface is None returns a list with all PO interfaces
256 Note that if specified, interface should be exact name
257 for instance: Ethernet3 and not e3 eth3 and so on
259 po_inf
= self
.s_api
[switch
].run("show port-channel")[0]["portChannels"]
262 r
= [x
for x
in po_inf
if interface
in po_inf
[x
]["activePorts"]]
268 def __get_switch_interface_ip(self
, switch
, interface
=None):
269 """Returns interface primary ip
270 interface should be exact name
271 for instance: Ethernet3 and not ethernet 3, e3 eth3 and so on
273 cmd
= "show ip interface {}".format(interface
)
274 ip_info
= self
.s_api
[switch
].run(cmd
)[0]["interfaces"][interface
]
276 ip
= ip_info
["interfaceAddress"]["primaryIp"]["address"]
277 mask
= ip_info
["interfaceAddress"]["primaryIp"]["maskLen"]
279 return "{}/{}".format(ip
, mask
)
281 def __check_service(self
, service_type
, connection_points
,
282 check_vlan
=True, check_num_cp
=True, kwargs
=None):
283 """ Reviews the connection points elements looking for semantic errors in the incoming data
285 if service_type
not in self
.__supported
_service
_types
:
286 raise Exception("The service '{}' is not supported. Only '{}' are accepted".format(
288 self
.__supported
_service
_types
))
291 if (len(connection_points
) < 2):
292 raise Exception(SdnError
.CONNECTION_POINTS_SIZE
)
293 if ((len(connection_points
) != self
.__ELINE
_num
_connection
_points
) and
294 (service_type
== self
.__service
_types
_ELINE
)):
295 raise Exception(SdnError
.CONNECTION_POINTS_SIZE
)
299 for cp
in connection_points
:
300 enc_type
= cp
.get(self
.__ENCAPSULATION
_TYPE
_PARAM
)
302 enc_type
not in self
.__supported
_encapsulation
_types
):
303 raise Exception(SdnError
.ENCAPSULATION_TYPE
)
304 encap_info
= cp
.get(self
.__ENCAPSULATION
_INFO
_PARAM
)
305 cp_vlan_id
= str(encap_info
.get(self
.__VLAN
_PARAM
))
309 elif vlan_id
!= cp_vlan_id
:
310 raise Exception(SdnError
.VLAN_INCONSISTENT
)
312 raise Exception(SdnError
.VLAN_NOT_PROVIDED
)
313 if vlan_id
in self
.__get
_srvVLANs
():
314 raise Exception('VLAN {} already assigned to a connectivity service'.format(vlan_id
))
316 # Commented out for as long as parameter isn't implemented
317 # bandwidth = kwargs.get(self.__BANDWIDTH_PARAM)
318 # if not isinstance(bandwidth, int):
319 # self.__exception(SdnError.BANDWIDTH, http_code=400)
321 # Commented out for as long as parameter isn't implemented
322 # backup = kwargs.get(self.__BACKUP_PARAM)
323 # if not isinstance(backup, bool):
324 # self.__exception(SdnError.BACKUP, http_code=400)
326 def check_credentials(self
):
327 """Retrieves the CloudVision version information, as the easiest way
328 for testing the access to CloudVision API
331 if self
.client
is None:
332 self
.client
= self
.__connect
()
333 result
= self
.client
.api
.get_cvp_info()
334 self
.logger
.debug(result
)
335 except CvpLoginError
as e
:
336 self
.logger
.info(str(e
))
338 raise SdnConnectorError(message
=SdnError
.UNAUTHORIZED
,
339 http_code
=401) from e
340 except Exception as ex
:
342 self
.logger
.error(str(ex
))
343 raise SdnConnectorError(message
=SdnError
.INTERNAL_ERROR
,
344 http_code
=500) from ex
346 def get_connectivity_service_status(self
, service_uuid
, conn_info
=None):
347 """Monitor the status of the connectivity service established
349 service_uuid (str): UUID of the connectivity service
350 conn_info (dict or None): Information returned by the connector
351 during the service creation/edition and subsequently stored in
355 dict: JSON/YAML-serializable dict that contains a mandatory key
356 ``sdn_status`` associated with one of the following values::
358 {'sdn_status': 'ACTIVE'}
359 # The service is up and running.
361 {'sdn_status': 'INACTIVE'}
362 # The service was created, but the connector
363 # cannot determine yet if connectivity exists
364 # (ideally, the caller needs to wait and check again).
366 {'sdn_status': 'DOWN'}
367 # Connection was previously established,
368 # but an error/failure was detected.
370 {'sdn_status': 'ERROR'}
371 # An error occurred when trying to create the service/
372 # establish the connectivity.
374 {'sdn_status': 'BUILD'}
375 # Still trying to create the service, the caller
376 # needs to wait and check again.
378 Additionally ``error_msg``(**str**) and ``sdn_info``(**dict**)
379 keys can be used to provide additional status explanation or
380 new information available for the connectivity service.
384 raise SdnConnectorError(message
='No connection service UUID',
387 self
.__get
_Connection
()
388 if conn_info
is None:
389 raise SdnConnectorError(message
='No connection information for service UUID {}'.format(service_uuid
),
392 if 'configLetPerSwitch' in conn_info
.keys():
396 cls_perSw
= self
.__get
_serviceData
(service_uuid
,
397 conn_info
['service_type'],
398 conn_info
['vlan_id'],
401 t_isCancelled
= False
406 if (len(cls_perSw
[s
]) > 0):
407 for cl
in cls_perSw
[s
]:
408 if len(cls_perSw
[s
][0]['config']) == 0:
411 t_id
= note
.split(self
.__SEPARATOR
)[1]
412 result
= self
.client
.api
.get_task_by_id(t_id
)
413 if result
['workOrderUserDefinedStatus'] == 'Completed':
415 elif result
['workOrderUserDefinedStatus'] == 'Cancelled':
417 elif result
['workOrderUserDefinedStatus'] == 'Failed':
421 failed_switches
.append(s
)
423 error_msg
= 'Some works were cancelled in switches: {}'.format(str(failed_switches
))
426 error_msg
= 'Some works failed in switches: {}'.format(str(failed_switches
))
429 error_msg
= 'Some works are still under execution in switches: {}'.format(str(failed_switches
))
433 sdn_status
= 'ACTIVE'
435 return {'sdn_status': sdn_status
,
436 'error_msg': error_msg
,
437 'sdn_info': sdn_info
}
438 except CvpLoginError
as e
:
439 self
.logger
.info(str(e
))
441 raise SdnConnectorError(message
=SdnError
.UNAUTHORIZED
,
442 http_code
=401) from e
443 except Exception as ex
:
445 self
.logger
.error(str(ex
), exc_info
=True)
446 raise SdnConnectorError(message
=str(ex
),
447 http_code
=500) from ex
449 def create_connectivity_service(self
, service_type
, connection_points
,
451 """Stablish SDN/WAN connectivity between the endpoints
453 (str): ``ELINE`` (L2), ``ELAN`` (L2), ``ETREE`` (L2), ``L3``.
454 :param connection_points: (list): each point corresponds to
455 an entry point to be connected. For WIM: from the DC
456 to the transport network.
457 For SDN: Compute/PCI to the transport network. One
458 connection point serves to identify the specific access and
459 some other service parameters, such as encapsulation type.
460 Each item of the list is a dict with:
461 "service_endpoint_id": (str)(uuid) Same meaning that for
462 'service_endpoint_mapping' (see __init__)
463 In case the config attribute mapping_not_needed is True,
464 this value is not relevant. In this case
465 it will contain the string "device_id:device_interface_id"
466 "service_endpoint_encapsulation_type": None, "dot1q", ...
467 "service_endpoint_encapsulation_info": (dict) with:
468 "vlan": ..., (int, present if encapsulation is dot1q)
469 "vni": ... (int, present if encapsulation is vxlan),
470 "peers": [(ipv4_1), (ipv4_2)] (present if
471 encapsulation is vxlan)
473 "device_id": ..., same meaning that for
474 'service_endpoint_mapping' (see __init__)
475 "device_interface_id": same meaning that for
476 'service_endpoint_mapping' (see __init__)
477 "switch_dpid": ..., present if mapping has been found
478 for this device_id,device_interface_id
479 "swith_port": ... present if mapping has been found
480 for this device_id,device_interface_id
481 "service_mapping_info": present if mapping has
482 been found for this device_id,device_interface_id
483 :param kwargs: For future versions:
484 bandwidth (int): value in kilobytes
485 latency (int): value in milliseconds
486 Other QoS might be passed as keyword arguments.
487 :return: tuple: ``(service_id, conn_info)`` containing:
488 - *service_uuid* (str): UUID of the established
490 - *conn_info* (dict or None): Information to be
491 stored at the database (or ``None``).
492 This information will be provided to the
493 :meth:`~.edit_connectivity_service` and :obj:`~.delete`.
494 **MUST** be JSON/YAML-serializable (plain data structures).
495 :raises: SdnConnectorError: In case of error. Nothing should be
496 created in this case.
497 Provide the parameter http_code
500 self
.__get
_Connection
()
501 self
.__check
_service
(service_type
,
505 service_uuid
= str(uuid
.uuid4())
507 self
.logger
.info("Service with uuid {} created.".
508 format(service_uuid
))
509 s_uid
, s_connInf
= self
.__processConnection
(
515 self
.__addMetadata
(s_uid
, service_type
, s_connInf
['vlan_id'])
516 except Exception as e
:
519 return (s_uid
, s_connInf
)
520 except CvpLoginError
as e
:
521 self
.logger
.info(str(e
))
523 raise SdnConnectorError(message
=SdnError
.UNAUTHORIZED
,
524 http_code
=401) from e
525 except SdnConnectorError
as sde
:
527 except Exception as ex
:
529 self
.logger
.error(str(ex
), exc_info
=True)
530 if self
.raiseException
:
532 raise SdnConnectorError(message
=str(ex
),
533 http_code
=500) from ex
535 def __processConnection(self
,
541 Invoked from creation and edit methods
543 Process the connection points array,
544 creating a set of configuration per switch where it has to be applied
545 for creating the configuration, the switches have to be queried for obtaining:
546 - the loopback address
547 - the BGP ASN (autonomous system number)
548 - the interface name of the MAC address to add in the connectivity service
549 Once the new configuration is ready, the __updateConnection method is invoked for appling the changes
558 vlan_processed
= False
561 for cp
in connection_points
:
563 encap_info
= cp
.get(self
.__ENCAPSULATION
_INFO
_PARAM
)
564 if not vlan_processed
:
565 vlan_id
= str(encap_info
.get(self
.__VLAN
_PARAM
))
568 vni_id
= encap_info
.get(self
.__VNI
_PARAM
)
570 vni_id
= str(10000 + int(vlan_id
))
572 if service_type
== self
.__service
_types
_ELAN
:
573 cl_vlan
= self
.clC
.getElan_vlan(service_uuid
,
577 cl_vlan
= self
.clC
.getEline_vlan(service_uuid
,
580 vlan_processed
= True
582 encap_type
= cp
.get(self
.__ENCAPSULATION
_TYPE
_PARAM
)
583 switch_id
= encap_info
.get(self
.__SW
_ID
_PARAM
)
585 point_mac
= encap_info
.get(self
.__MAC
_PARAM
)
586 switches
= self
.__lldp
_find
_neighbor
("chassisId", point_mac
)
588 if len(switches
) == 0:
589 raise SdnConnectorError(message
="Connection point MAC address {} not found in the switches".format(point_mac
),
591 self
.logger
.debug("Found connection point for MAC {}: {}".
592 format(point_mac
, switches
))
593 port_channel
= self
.__get
_switch
_po
(switch
['name'],
595 if len(port_channel
) > 0:
596 interface
= port_channel
[0]
598 interface
= switch
['interface']
600 interface
= encap_info
.get(self
.__SW
_PORT
_PARAM
)
601 switches
= [{'name': switch_id
, 'interface': interface
}]
604 raise SdnConnectorError(message
="Connection point switch port empty for switch_dpid {}".format(switch_id
),
606 for switch
in switches
:
607 # it should be only one switch where the mac is attached
608 if encap_type
== 'dot1q':
609 # SRIOV configLet for Leaf switch mac's attached to
610 if service_type
== self
.__service
_types
_ELAN
:
611 cl_encap
= self
.clC
.getElan_sriov(service_uuid
, interface
, vlan_id
, i
)
613 cl_encap
= self
.clC
.getEline_sriov(service_uuid
, interface
, vlan_id
, i
)
615 # PT configLet for Leaf switch attached to the mac
616 if service_type
== self
.__service
_types
_ELAN
:
617 cl_encap
= self
.clC
.getElan_passthrough(service_uuid
,
621 cl_encap
= self
.clC
.getEline_passthrough(service_uuid
,
624 if cls_cp
.get(switch
['name']):
625 cls_cp
[switch
['name']] = str(cls_cp
[switch
['name']]) + cl_encap
627 cls_cp
[switch
['name']] = cl_encap
629 # at least 1 connection point has to be received
630 if not vlan_processed
:
631 raise SdnConnectorError(message
=SdnError
.UNSUPPORTED_FEATURE
,
635 # for cl in cp_configLets:
636 cl_name
= (self
.__OSM
_PREFIX
+
638 self
.__SEPARATOR
+ service_type
+ str(vlan_id
) +
639 self
.__SEPARATOR
+ service_uuid
)
640 # apply VLAN and BGP configLet to all Leaf switches
641 if service_type
== self
.__service
_types
_ELAN
:
642 cl_bgp
[s
] = self
.clC
.getElan_bgp(service_uuid
,
645 self
.sw_loopback0
[s
],
648 cl_bgp
[s
] = self
.clC
.getEline_bgp(service_uuid
,
651 self
.sw_loopback0
[s
],
654 if not cls_cp
.get(s
):
657 cl_config
= str(cl_vlan
) + str(cl_bgp
[s
]) + str(cls_cp
[s
])
659 cls_perSw
[s
] = [{'name': cl_name
, 'config': cl_config
}]
661 allLeafConfigured
, allLeafModified
= self
.__updateConnection
(cls_perSw
)
664 "uuid": service_uuid
,
666 "service_type": service_type
,
668 "connection_points": connection_points
,
669 "configLetPerSwitch": cls_perSw
,
670 'allLeafConfigured': allLeafConfigured
,
671 'allLeafModified': allLeafModified
}
673 return service_uuid
, conn_info
674 except Exception as ex
:
675 self
.logger
.debug("Exception processing connection {}: {}".
676 format(service_uuid
, str(ex
)))
679 def __updateConnection(self
, cls_perSw
):
680 """ Invoked in the creation and modification
682 checks if the new connection points config is:
683 - already in the Cloud Vision, the configLet is modified, and applied to the switch,
684 executing the corresponding task
685 - if it has to be removed:
686 then configuration has to be removed from the switch executing the corresponding task,
687 before trying to remove the configuration
688 - created, the configuration set is created, associated to the switch, and the associated
689 task to the configLet modification executed
690 In case of any error, rollback is executed, removing the created elements, and restoring to the
694 allLeafConfigured
= {}
698 allLeafConfigured
[s
] = False
699 allLeafModified
[s
] = False
703 toDelete_in_cvp
= False
704 if not (cls_perSw
.get(s
) and cls_perSw
[s
][0].get('config')):
705 # when there is no configuration, means that there is no interface
706 # in the switch to be connected, so the configLet has to be removed from CloudVision
707 # after removing the ConfigLet fron the switch if it was already there
709 # get config let name and key
712 cvp_cl
= self
.client
.api
.get_configlet_by_name(cl
[0]['name'])
714 cl_toDelete
.append(cvp_cl
)
716 toDelete_in_cvp
= True
717 except CvpApiError
as error
:
718 if "Entity does not exist" in error
.msg
:
722 # remove configLet from device
724 res
= self
.__configlet
_modify
(cls_perSw
[s
])
725 allLeafConfigured
[s
] = res
[0]
726 if not allLeafConfigured
[s
]:
729 res
= self
.__device
_modify
(
732 delete
=toDelete_in_cvp
)
733 if "errorMessage" in str(res
):
734 raise Exception(str(res
))
735 self
.logger
.info("Device {} modify result {}".format(s
, res
))
736 for t_id
in res
[1]['tasks']:
737 tasks
[t_id
] = {'workOrderId': t_id
}
738 if not toDelete_in_cvp
:
739 note_msg
= "## Managed by OSM {}{}{}##".format(self
.__SEPARATOR
,
742 self
.client
.api
.add_note_to_configlet(
743 cls_perSw
[s
][0]['key'],
745 cls_perSw
[s
][0].update([('note', note_msg
)])
746 # with just one configLet assigned to a device,
747 # delete all if there are errors in next loops
748 if not toDelete_in_cvp
:
749 allLeafModified
[s
] = True
750 if self
.taskC
is None:
752 data
= self
.taskC
.update_all_tasks(tasks
).values()
753 self
.taskC
.task_action(data
,
754 self
.__EXC
_TASK
_EXEC
_WAIT
,
756 if len(cl_toDelete
) > 0:
757 self
.__configlet
_modify
(cl_toDelete
, delete
=True)
758 return allLeafConfigured
, allLeafModified
759 except Exception as ex
:
761 self
.__rollbackConnection
(cls_perSw
,
762 allLeafConfigured
=True,
763 allLeafModified
=True)
764 except Exception as e
:
765 self
.logger
.info("Exception rolling back in updating connection: {}".
769 def __rollbackConnection(self
,
773 """ Removes the given configLet from the devices and then remove the configLets
777 if allLeafModified
[s
]:
779 res
= self
.__device
_modify
(
781 new_configlets
=cls_perSw
[s
],
783 if "errorMessage" in str(res
):
784 raise Exception(str(res
))
785 for t_id
in res
[1]['tasks']:
786 tasks
[t_id
] = {'workOrderId': t_id
}
787 self
.logger
.info("Device {} modify result {}".format(s
, res
))
788 except Exception as e
:
789 self
.logger
.info('Error removing configlets from device {}: {}'.format(s
, e
))
791 if self
.taskC
is None:
793 data
= self
.taskC
.update_all_tasks(tasks
).values()
794 self
.taskC
.task_action(data
,
795 self
.__ROLLB
_TASK
_EXEC
_WAIT
,
798 if allLeafConfigured
[s
]:
799 self
.__configlet
_modify
(cls_perSw
[s
], delete
=True)
801 def __device_modify(self
, device_to_update
, new_configlets
, delete
):
802 """ Updates the devices (switches) adding or removing the configLet,
803 the tasks Id's associated to the change are returned
805 self
.logger
.info('Enter in __device_modify delete: {}'.format(
809 # Task Ids that have been identified during device actions
812 if (len(new_configlets
) == 0 or
813 device_to_update
is None or
814 len(device_to_update
) == 0):
815 data
= {'updated': updated
, 'tasks': newTasks
}
816 return [changed
, data
]
818 self
.__load
_inventory
()
820 allDeviceFacts
= self
.allDeviceFacts
821 # Work through Devices list adding device specific information
823 for try_device
in allDeviceFacts
:
824 # Add Device Specific Configlets
825 # self.logger.debug(device)
826 if try_device
['hostname'] not in device_to_update
:
828 dev_cvp_configlets
= self
.client
.api
.get_configlets_by_device_id(
829 try_device
['systemMacAddress'])
830 # self.logger.debug(dev_cvp_configlets)
831 try_device
['deviceSpecificConfiglets'] = []
832 for cvp_configlet
in dev_cvp_configlets
:
833 if int(cvp_configlet
['containerCount']) == 0:
834 try_device
['deviceSpecificConfiglets'].append(
835 {'name': cvp_configlet
['name'],
836 'key': cvp_configlet
['key']})
837 # self.logger.debug(device)
841 # Check assigned configlets
842 device_update
= False
844 remove_configlets
= []
848 for cvp_configlet
in device
['deviceSpecificConfiglets']:
849 for cl
in new_configlets
:
850 if cvp_configlet
['name'] == cl
['name']:
851 remove_configlets
.append(cvp_configlet
)
854 for configlet
in new_configlets
:
855 if configlet
not in device
['deviceSpecificConfiglets']:
856 add_configlets
.append(configlet
)
859 update_devices
.append({'hostname': device
['hostname'],
860 'configlets': [add_configlets
,
863 self
.logger
.info("Device to modify: {}".format(update_devices
))
865 up_device
= update_devices
[0]
866 cl_toAdd
= up_device
['configlets'][0]
867 cl_toDel
= up_device
['configlets'][1]
870 if delete
and len(cl_toDel
) > 0:
871 r
= self
.client
.api
.remove_configlets_from_device(
877 self
.logger
.debug("remove_configlets_from_device {} {}".format(dev_action
, cl_toDel
))
878 elif len(cl_toAdd
) > 0:
879 r
= self
.client
.api
.apply_configlets_to_device(
885 self
.logger
.debug("apply_configlets_to_device {} {}".format(dev_action
, cl_toAdd
))
887 except Exception as error
:
888 errorMessage
= str(error
)
889 msg
= "errorMessage: Device {} Configlets couldnot be updated: {}".format(
890 up_device
['hostname'], errorMessage
)
891 raise SdnConnectorError(msg
) from error
893 if "errorMessage" in str(dev_action
):
894 m
= "Device {} Configlets update fail: {}".format(
895 up_device
['name'], dev_action
['errorMessage'])
896 raise SdnConnectorError(m
)
899 if 'taskIds' in str(dev_action
):
900 for taskId
in dev_action
['data']['taskIds']:
901 updated
.append({up_device
['hostname']:
902 "Configlets-{}".format(
904 newTasks
.append(taskId
)
906 updated
.append({up_device
['hostname']:
907 "Configlets-No_Specific_Tasks"})
908 data
= {'updated': updated
, 'tasks': newTasks
}
909 return [changed
, data
]
911 def __configlet_modify(self
, configletsToApply
, delete
=False):
912 ''' adds/update or delete the provided configLets
913 :param configletsToApply: list of configLets to apply
914 :param delete: flag to indicate if the configLets have to be deleted
915 from Cloud Vision Portal
916 :return: data: dict of module actions and taskIDs
918 self
.logger
.info('Enter in __configlet_modify delete:{}'.format(
921 # Compare configlets against cvp_facts-configlets
928 for cl
in configletsToApply
:
935 cvp_cl
= self
.client
.api
.get_configlet_by_name(cl
['name'])
936 cl
['key'] = cvp_cl
['key']
937 cl
['note'] = cvp_cl
['note']
939 except CvpApiError
as error
:
940 if "Entity does not exist" in error
.msg
:
948 configlet
= {'name': cvp_cl
['name'],
952 cl_compare
= self
.__compare
(cl
['config'],
954 # compare function returns a floating point number
955 if cl_compare
[0] != 100.0:
957 configlet
= {'name': cl
['name'],
959 'config': cl
['config']}
962 configlet
= {'name': cl
['name'],
963 'key': cvp_cl
['key'],
965 'config': cl
['config']}
968 configlet
= {'name': cl
['name'],
969 'config': cl
['config']}
973 resp
= self
.client
.api
.delete_configlet(
974 configlet
['data']['name'],
975 configlet
['data']['key'])
978 resp
= self
.client
.api
.update_configlet(
980 configlet
['data']['key'],
981 configlet
['data']['name'])
984 resp
= self
.client
.api
.add_configlet(
988 operation
= 'checked'
990 except Exception as error
:
991 errorMessage
= str(error
).split(':')[-1]
992 message
= "Configlet {} cannot be {}: {}".format(
993 cl
['name'], operation
, errorMessage
)
995 deleted
.append({configlet
['name']: message
})
997 updated
.append({configlet
['name']: message
})
999 new
.append({configlet
['name']: message
})
1001 checked
.append({configlet
['name']: message
})
1004 if "error" in str(resp
).lower():
1005 message
= "Configlet {} cannot be deleted: {}".format(
1006 cl
['name'], resp
['errorMessage'])
1008 deleted
.append({configlet
['name']: message
})
1010 updated
.append({configlet
['name']: message
})
1012 new
.append({configlet
['name']: message
})
1014 checked
.append({configlet
['name']: message
})
1018 deleted
.append({configlet
['name']: "success"})
1021 updated
.append({configlet
['name']: "success"})
1024 cl
['key'] = resp
# This key is used in API call deviceApplyConfigLet FGA
1025 new
.append({configlet
['name']: "success"})
1028 checked
.append({configlet
['name']: "success"})
1030 data
= {'new': new
, 'updated': updated
, 'deleted': deleted
, 'checked': checked
}
1031 return [changed
, data
]
1033 def __get_configletsDevices(self
, configlets
):
1034 for s
in self
.s_api
:
1035 configlet
= configlets
[s
]
1036 # Add applied Devices
1037 if len(configlet
) > 0:
1038 configlet
['devices'] = []
1039 applied_devices
= self
.client
.api
.get_applied_devices(
1041 for device
in applied_devices
['data']:
1042 configlet
['devices'].append(device
['hostName'])
1044 def __get_serviceData(self
, service_uuid
, service_type
, vlan_id
, conn_info
=None):
1046 for s
in self
.s_api
:
1049 srv_cls
= self
.__get
_serviceConfigLets
(service_uuid
,
1052 self
.__get
_configletsDevices
(srv_cls
)
1053 for s
in self
.s_api
:
1056 for dev
in cl
['devices']:
1057 cls_perSw
[dev
].append(cl
)
1059 cls_perSw
= conn_info
['configLetPerSwitch']
1062 def delete_connectivity_service(self
, service_uuid
, conn_info
=None):
1064 Disconnect multi-site endpoints previously connected
1066 :param service_uuid: The one returned by create_connectivity_service
1067 :param conn_info: The one returned by last call to 'create_connectivity_service' or 'edit_connectivity_service'
1068 if they do not return None
1070 :raises: SdnConnectorException: In case of error. The parameter http_code must be filled
1073 self
.logger
.debug('invoked delete_connectivity_service {}'.
1074 format(service_uuid
))
1075 if not service_uuid
:
1076 raise SdnConnectorError(message
='No connection service UUID',
1079 self
.__get
_Connection
()
1080 if conn_info
is None:
1081 raise SdnConnectorError(message
='No connection information for service UUID {}'.format(service_uuid
),
1084 cls_perSw
= self
.__get
_serviceData
(service_uuid
,
1085 conn_info
['service_type'],
1086 conn_info
['vlan_id'],
1088 allLeafConfigured
= {}
1089 allLeafModified
= {}
1090 for s
in self
.s_api
:
1091 allLeafConfigured
[s
] = True
1092 allLeafModified
[s
] = True
1093 found_in_cvp
= False
1094 for s
in self
.s_api
:
1098 self
.__rollbackConnection
(cls_perSw
,
1102 # if the service is not defined in Cloud Vision, return a 404 - NotFound error
1103 raise SdnConnectorError(message
='Service {} was not found in Arista Cloud Vision {}'.
1104 format(service_uuid
, self
.__wim
_url
),
1106 self
.__removeMetadata
(service_uuid
)
1107 except CvpLoginError
as e
:
1108 self
.logger
.info(str(e
))
1110 raise SdnConnectorError(message
=SdnError
.UNAUTHORIZED
,
1111 http_code
=401) from e
1112 except SdnConnectorError
as sde
:
1114 except Exception as ex
:
1116 self
.logger
.error(ex
)
1117 if self
.raiseException
:
1119 raise SdnConnectorError(message
=SdnError
.INTERNAL_ERROR
,
1120 http_code
=500) from ex
1122 def __addMetadata(self
, service_uuid
, service_type
, vlan_id
):
1123 """ Adds the connectivity service from 'OSM_metadata' configLet
1125 found_in_cvp
= False
1127 cvp_cl
= self
.client
.api
.get_configlet_by_name(self
.__OSM
_METADATA
)
1129 except CvpApiError
as error
:
1130 if "Entity does not exist" in error
.msg
:
1135 new_serv
= '{} {} {} {}\n'.format(self
.__METADATA
_PREFIX
, service_type
, vlan_id
, service_uuid
)
1138 cl_config
= cvp_cl
['config'] + new_serv
1140 cl_config
= new_serv
1141 cl_meta
= [{'name': self
.__OSM
_METADATA
, 'config': cl_config
}]
1142 self
.__configlet
_modify
(cl_meta
)
1143 except Exception as e
:
1144 self
.logger
.error('Error in setting metadata in CloudVision from OSM for service {}: {}'.
1145 format(service_uuid
, str(e
)))
1148 def __removeMetadata(self
, service_uuid
):
1149 """ Removes the connectivity service from 'OSM_metadata' configLet
1151 found_in_cvp
= False
1153 cvp_cl
= self
.client
.api
.get_configlet_by_name(self
.__OSM
_METADATA
)
1155 except CvpApiError
as error
:
1156 if "Entity does not exist" in error
.msg
:
1162 if service_uuid
in cvp_cl
['config']:
1164 for line
in cvp_cl
['config'].split('\n'):
1165 if service_uuid
in line
:
1168 cl_config
= cl_config
+ line
1169 cl_meta
= [{'name': self
.__OSM
_METADATA
, 'config': cl_config
}]
1170 self
.__configlet
_modify
(cl_meta
)
1171 except Exception as e
:
1172 self
.logger
.error('Error in removing metadata in CloudVision from OSM for service {}: {}'.
1173 format(service_uuid
, str(e
)))
1176 def edit_connectivity_service(self
,
1179 connection_points
=None,
1181 """ Change an existing connectivity service.
1183 This method's arguments and return value follow the same convention as
1184 :meth:`~.create_connectivity_service`.
1186 :param service_uuid: UUID of the connectivity service.
1187 :param conn_info: (dict or None): Information previously returned
1188 by last call to create_connectivity_service
1189 or edit_connectivity_service
1190 :param connection_points: (list): If provided, the old list of
1191 connection points will be replaced.
1192 :param kwargs: Same meaning that create_connectivity_service
1193 :return: dict or None: Information to be updated and stored at
1195 When ``None`` is returned, no information should be changed.
1196 When an empty dict is returned, the database record will
1198 **MUST** be JSON/YAML-serializable (plain data structures).
1200 SdnConnectorError: In case of error.
1203 self
.logger
.debug('invoked edit_connectivity_service for service {}'.format(service_uuid
))
1205 if not service_uuid
:
1206 raise SdnConnectorError(message
='Unable to perform operation, missing or empty uuid',
1209 raise SdnConnectorError(message
='Unable to perform operation, missing or empty connection information',
1212 if connection_points
is None:
1215 self
.__get
_Connection
()
1217 cls_currentPerSw
= conn_info
['configLetPerSwitch']
1218 service_type
= conn_info
['service_type']
1220 self
.__check
_service
(service_type
,
1226 s_uid
, s_connInf
= self
.__processConnection
(
1231 self
.logger
.info("Service with uuid {} configuration updated".
1234 except CvpLoginError
as e
:
1235 self
.logger
.info(str(e
))
1237 raise SdnConnectorError(message
=SdnError
.UNAUTHORIZED
,
1238 http_code
=401) from e
1239 except SdnConnectorError
as sde
:
1241 except Exception as ex
:
1244 # TODO check if there are pending task, and cancel them before restoring
1245 self
.__updateConnection
(cls_currentPerSw
)
1246 except Exception as e
:
1247 self
.logger
.error("Unable to restore configuration in service {} after an error in the configuration updated: {}".
1248 format(service_uuid
, str(e
)))
1249 if self
.raiseException
:
1251 raise SdnConnectorError(message
=str(ex
),
1252 http_code
=500) from ex
1254 def clear_all_connectivity_services(self
):
1255 """ Removes all connectivity services from Arista CloudVision with two steps:
1256 - retrives all the services from Arista CloudVision
1257 - removes each service
1260 self
.logger
.debug('invoked AristaImpl ' +
1261 'clear_all_connectivity_services')
1262 self
.__get
_Connection
()
1263 s_list
= self
.__get
_srvUUIDs
()
1266 conn_info
['service_type'] = serv
['type']
1267 conn_info
['vlan_id'] = serv
['vlan']
1269 self
.delete_connectivity_service(serv
['uuid'], conn_info
)
1270 except CvpLoginError
as e
:
1271 self
.logger
.info(str(e
))
1273 raise SdnConnectorError(message
=SdnError
.UNAUTHORIZED
,
1274 http_code
=401) from e
1275 except SdnConnectorError
as sde
:
1277 except Exception as ex
:
1279 self
.logger
.error(ex
)
1280 if self
.raiseException
:
1282 raise SdnConnectorError(message
=SdnError
.INTERNAL_ERROR
,
1283 http_code
=500) from ex
1285 def get_all_active_connectivity_services(self
):
1286 """ Return the uuid of all the active connectivity services with two steps:
1287 - retrives all the services from Arista CloudVision
1288 - retrives the status of each server
1291 self
.logger
.debug('invoked AristaImpl {}'.format(
1292 'get_all_active_connectivity_services'))
1293 self
.__get
_Connection
()
1294 s_list
= self
.__get
_srvUUIDs
()
1298 conn_info
['service_type'] = serv
['type']
1299 conn_info
['vlan_id'] = serv
['vlan']
1301 status
= self
.get_connectivity_service_status(serv
['uuid'], conn_info
)
1302 if status
['sdn_status'] == 'ACTIVE':
1303 result
.append(serv
['uuid'])
1305 except CvpLoginError
as e
:
1306 self
.logger
.info(str(e
))
1308 raise SdnConnectorError(message
=SdnError
.UNAUTHORIZED
,
1309 http_code
=401) from e
1310 except SdnConnectorError
as sde
:
1312 except Exception as ex
:
1314 self
.logger
.error(ex
)
1315 if self
.raiseException
:
1317 raise SdnConnectorError(message
=SdnError
.INTERNAL_ERROR
,
1318 http_code
=500) from ex
1320 def __get_serviceConfigLets(self
, service_uuid
, service_type
, vlan_id
):
1321 """ Return the configLet's associated with a connectivity service,
1322 There should be one, as maximum, per device (switch) for a given
1323 connectivity service
1326 for s
in self
.s_api
:
1328 found_in_cvp
= False
1329 name
= (self
.__OSM
_PREFIX
+
1331 self
.__SEPARATOR
+ service_type
+ str(vlan_id
) +
1332 self
.__SEPARATOR
+ service_uuid
)
1334 cvp_cl
= self
.client
.api
.get_configlet_by_name(name
)
1336 except CvpApiError
as error
:
1337 if "Entity does not exist" in error
.msg
:
1345 def __get_srvVLANs(self
):
1346 """ Returns a list with all the VLAN id's used in the connectivity services managed
1347 in tha Arista CloudVision by checking the 'OSM_metadata' configLet where this
1348 information is stored
1350 found_in_cvp
= False
1352 cvp_cl
= self
.client
.api
.get_configlet_by_name(self
.__OSM
_METADATA
)
1354 except CvpApiError
as error
:
1355 if "Entity does not exist" in error
.msg
:
1361 lines
= cvp_cl
['config'].split('\n')
1363 if self
.__METADATA
_PREFIX
in line
:
1364 s_vlan
= line
.split(' ')[3]
1367 if (s_vlan
is not None and
1369 s_vlan
not in s_vlan_list
):
1370 s_vlan_list
.append(s_vlan
)
1374 def __get_srvUUIDs(self
):
1375 """ Retrieves all the connectivity services, managed in tha Arista CloudVision
1376 by checking the 'OSM_metadata' configLet where this information is stored
1378 found_in_cvp
= False
1380 cvp_cl
= self
.client
.api
.get_configlet_by_name(self
.__OSM
_METADATA
)
1382 except CvpApiError
as error
:
1383 if "Entity does not exist" in error
.msg
:
1389 lines
= cvp_cl
['config'].split('\n')
1391 if self
.__METADATA
_PREFIX
in line
:
1392 line
= line
.split(' ')
1393 serv
= {'uuid': line
[4], 'type': line
[2], 'vlan': line
[3]}
1396 if (serv
is not None and
1398 serv
not in serv_list
):
1399 serv_list
.append(serv
)
1403 def __get_Connection(self
):
1404 """ Open a connection with Arista CloudVision,
1405 invoking the version retrival as test
1408 if self
.client
is None:
1409 self
.client
= self
.__connect
()
1410 self
.client
.api
.get_cvp_info()
1411 except CvpSessionLogOutError
:
1412 self
.client
= self
.__connect
()
1413 self
.client
.api
.get_cvp_info()
1415 def __connect(self
):
1416 ''' Connects to CVP device using user provided credentials from initialization.
1417 :return: CvpClient object with connection instantiated.
1419 client
= CvpClient()
1420 protocol
, _
, rest_url
= self
.__wim
_url
.rpartition("://")
1421 host
, _
, port
= rest_url
.partition(":")
1422 if port
and port
.endswith("/"):
1423 port
= int(port
[:-1])
1429 client
.connect([host
],
1432 protocol
=protocol
or "https",
1435 self
.taskC
= AristaCVPTask(client
.api
)
1438 def __compare(self
, fromText
, toText
, lines
=10):
1439 """ Compare text string in 'fromText' with 'toText' and produce
1440 diffRatio - a score as a float in the range [0, 1] 2.0*M / T
1441 T is the total number of elements in both sequences,
1442 M is the number of matches.
1443 Score - 1.0 if the sequences are identical, and
1444 0.0 if they have nothing in common.
1447 '- ' line unique to sequence 1
1448 '+ ' line unique to sequence 2
1449 ' ' line common to both sequences
1450 '? ' line not present in either input sequence
1452 fromlines
= fromText
.splitlines(1)
1453 tolines
= toText
.splitlines(1)
1454 diff
= list(difflib
.unified_diff(fromlines
, tolines
, n
=lines
))
1455 textComp
= difflib
.SequenceMatcher(None, fromText
, toText
)
1456 diffRatio
= round(textComp
.quick_ratio()*100, 2)
1457 return [diffRatio
, diff
]
1459 def __load_inventory(self
):
1460 """ Get Inventory Data for All Devices (aka switches) from the Arista CloudVision
1462 if not self
.cvp_inventory
:
1463 self
.cvp_inventory
= self
.client
.api
.get_inventory()
1464 self
.allDeviceFacts
= []
1465 for device
in self
.cvp_inventory
:
1466 self
.allDeviceFacts
.append(device
)
1468 def is_valid_destination(self
, url
):
1469 """ Check that the provided WIM URL is correct
1471 if re
.match(self
.__regex
, url
):
1473 elif self
.is_valid_ipv4_address(url
):
1476 return self
.is_valid_ipv6_address(url
)
1478 def is_valid_ipv4_address(self
, address
):
1479 """ Checks that the given IP is IPv4 valid
1482 socket
.inet_pton(socket
.AF_INET
, address
)
1483 except AttributeError: # no inet_pton here, sorry
1485 socket
.inet_aton(address
)
1486 except socket
.error
:
1488 return address
.count('.') == 3
1489 except socket
.error
: # not a valid address
1493 def is_valid_ipv6_address(self
, address
):
1494 """ Checks that the given IP is IPv6 valid
1497 socket
.inet_pton(socket
.AF_INET6
, address
)
1498 except socket
.error
: # not a valid address