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 for cp
in connection_points
:
566 encap_info
= cp
.get(self
.__ENCAPSULATION
_INFO
_PARAM
)
567 if not vlan_processed
:
568 vlan_id
= str(encap_info
.get(self
.__VLAN
_PARAM
))
571 vni_id
= encap_info
.get(self
.__VNI
_PARAM
)
573 vni_id
= str(10000 + int(vlan_id
))
575 if service_type
== self
.__service
_types
_ELAN
:
576 cl_vlan
= self
.clC
.getElan_vlan(service_uuid
,
580 cl_vlan
= self
.clC
.getEline_vlan(service_uuid
,
583 vlan_processed
= True
585 encap_type
= cp
.get(self
.__ENCAPSULATION
_TYPE
_PARAM
)
586 switch_id
= encap_info
.get(self
.__SW
_ID
_PARAM
)
588 point_mac
= encap_info
.get(self
.__MAC
_PARAM
)
589 switches
= self
.__lldp
_find
_neighbor
("chassisId", point_mac
)
591 if len(switches
) == 0:
592 raise SdnConnectorError(message
="Connection point MAC address {} not found in the switches".format(point_mac
),
594 self
.logger
.debug("Found connection point for MAC {}: {}".
595 format(point_mac
, switches
))
596 port_channel
= self
.__get
_switch
_po
(switch
['name'],
598 if len(port_channel
) > 0:
599 interface
= port_channel
[0]
601 interface
= switch
['interface']
603 interface
= encap_info
.get(self
.__SW
_PORT
_PARAM
)
604 switches
= [{'name': switch_id
, 'interface': interface
}]
607 raise SdnConnectorError(message
="Connection point switch port empty for switch_dpid {}".format(switch_id
),
609 for switch
in switches
:
610 # it should be only one switch where the mac is attached
611 if encap_type
== 'dot1q':
612 # SRIOV configLet for Leaf switch mac's attached to
613 if service_type
== self
.__service
_types
_ELAN
:
614 cl_encap
= self
.clC
.getElan_sriov(service_uuid
, interface
, vlan_id
, i
)
616 cl_encap
= self
.clC
.getEline_sriov(service_uuid
, interface
, vlan_id
, i
)
618 # PT configLet for Leaf switch attached to the mac
619 if service_type
== self
.__service
_types
_ELAN
:
620 cl_encap
= self
.clC
.getElan_passthrough(service_uuid
,
624 cl_encap
= self
.clC
.getEline_passthrough(service_uuid
,
627 if cls_cp
.get(switch
['name']):
628 cls_cp
[switch
['name']] = str(cls_cp
[switch
['name']]) + cl_encap
630 cls_cp
[switch
['name']] = cl_encap
632 # at least 1 connection point has to be received
633 if not vlan_processed
:
634 raise SdnConnectorError(message
=SdnError
.UNSUPPORTED_FEATURE
,
638 # for cl in cp_configLets:
639 cl_name
= (self
.__OSM
_PREFIX
+
641 self
.__SEPARATOR
+ service_type
+ str(vlan_id
) +
642 self
.__SEPARATOR
+ service_uuid
)
643 # apply VLAN and BGP configLet to all Leaf switches
644 if service_type
== self
.__service
_types
_ELAN
:
645 cl_bgp
[s
] = self
.clC
.getElan_bgp(service_uuid
,
648 self
.sw_loopback0
[s
],
651 cl_bgp
[s
] = self
.clC
.getEline_bgp(service_uuid
,
654 self
.sw_loopback0
[s
],
657 if not cls_cp
.get(s
):
660 cl_config
= str(cl_vlan
) + str(cl_bgp
[s
]) + str(cls_cp
[s
])
662 cls_perSw
[s
] = [{'name': cl_name
, 'config': cl_config
}]
664 allLeafConfigured
, allLeafModified
= self
.__updateConnection
(cls_perSw
)
667 "uuid": service_uuid
,
669 "service_type": service_type
,
671 "connection_points": connection_points
,
672 "configLetPerSwitch": cls_perSw
,
673 'allLeafConfigured': allLeafConfigured
,
674 'allLeafModified': allLeafModified
}
676 return service_uuid
, conn_info
677 except Exception as ex
:
678 self
.logger
.debug("Exception processing connection {}: {}".
679 format(service_uuid
, str(ex
)))
682 def __updateConnection(self
, cls_perSw
):
683 """ Invoked in the creation and modification
685 checks if the new connection points config is:
686 - already in the Cloud Vision, the configLet is modified, and applied to the switch,
687 executing the corresponding task
688 - if it has to be removed:
689 then configuration has to be removed from the switch executing the corresponding task,
690 before trying to remove the configuration
691 - created, the configuration set is created, associated to the switch, and the associated
692 task to the configLet modification executed
693 In case of any error, rollback is executed, removing the created elements, and restoring to the
697 allLeafConfigured
= {}
701 allLeafConfigured
[s
] = False
702 allLeafModified
[s
] = False
706 toDelete_in_cvp
= False
707 if not (cls_perSw
.get(s
) and cls_perSw
[s
][0].get('config')):
708 # when there is no configuration, means that there is no interface
709 # in the switch to be connected, so the configLet has to be removed from CloudVision
710 # after removing the ConfigLet fron the switch if it was already there
712 # get config let name and key
715 cvp_cl
= self
.client
.api
.get_configlet_by_name(cl
[0]['name'])
717 cl_toDelete
.append(cvp_cl
)
719 toDelete_in_cvp
= True
720 except CvpApiError
as error
:
721 if "Entity does not exist" in error
.msg
:
725 # remove configLet from device
727 res
= self
.__configlet
_modify
(cls_perSw
[s
])
728 allLeafConfigured
[s
] = res
[0]
729 if not allLeafConfigured
[s
]:
732 res
= self
.__device
_modify
(
735 delete
=toDelete_in_cvp
)
736 if "errorMessage" in str(res
):
737 raise Exception(str(res
))
738 self
.logger
.info("Device {} modify result {}".format(s
, res
))
739 for t_id
in res
[1]['tasks']:
740 tasks
[t_id
] = {'workOrderId': t_id
}
741 if not toDelete_in_cvp
:
742 note_msg
= "## Managed by OSM {}{}{}##".format(self
.__SEPARATOR
,
745 self
.client
.api
.add_note_to_configlet(
746 cls_perSw
[s
][0]['key'],
748 cls_perSw
[s
][0].update([('note', note_msg
)])
749 # with just one configLet assigned to a device,
750 # delete all if there are errors in next loops
751 if not toDelete_in_cvp
:
752 allLeafModified
[s
] = True
753 if self
.taskC
is None:
755 data
= self
.taskC
.update_all_tasks(tasks
).values()
756 self
.taskC
.task_action(data
,
757 self
.__EXC
_TASK
_EXEC
_WAIT
,
759 if len(cl_toDelete
) > 0:
760 self
.__configlet
_modify
(cl_toDelete
, delete
=True)
761 return allLeafConfigured
, allLeafModified
762 except Exception as ex
:
764 self
.__rollbackConnection
(cls_perSw
,
765 allLeafConfigured
=True,
766 allLeafModified
=True)
767 except Exception as e
:
768 self
.logger
.info("Exception rolling back in updating connection: {}".
772 def __rollbackConnection(self
,
776 """ Removes the given configLet from the devices and then remove the configLets
780 if allLeafModified
[s
]:
782 res
= self
.__device
_modify
(
784 new_configlets
=cls_perSw
[s
],
786 if "errorMessage" in str(res
):
787 raise Exception(str(res
))
788 for t_id
in res
[1]['tasks']:
789 tasks
[t_id
] = {'workOrderId': t_id
}
790 self
.logger
.info("Device {} modify result {}".format(s
, res
))
791 except Exception as e
:
792 self
.logger
.info('Error removing configlets from device {}: {}'.format(s
, e
))
794 if self
.taskC
is None:
796 data
= self
.taskC
.update_all_tasks(tasks
).values()
797 self
.taskC
.task_action(data
,
798 self
.__ROLLB
_TASK
_EXEC
_WAIT
,
801 if allLeafConfigured
[s
]:
802 self
.__configlet
_modify
(cls_perSw
[s
], delete
=True)
804 def __device_modify(self
, device_to_update
, new_configlets
, delete
):
805 """ Updates the devices (switches) adding or removing the configLet,
806 the tasks Id's associated to the change are returned
808 self
.logger
.info('Enter in __device_modify delete: {}'.format(
812 # Task Ids that have been identified during device actions
815 if (len(new_configlets
) == 0 or
816 device_to_update
is None or
817 len(device_to_update
) == 0):
818 data
= {'updated': updated
, 'tasks': newTasks
}
819 return [changed
, data
]
821 self
.__load
_inventory
()
823 allDeviceFacts
= self
.allDeviceFacts
824 # Work through Devices list adding device specific information
826 for try_device
in allDeviceFacts
:
827 # Add Device Specific Configlets
828 # self.logger.debug(device)
829 if try_device
['hostname'] not in device_to_update
:
831 dev_cvp_configlets
= self
.client
.api
.get_configlets_by_device_id(
832 try_device
['systemMacAddress'])
833 # self.logger.debug(dev_cvp_configlets)
834 try_device
['deviceSpecificConfiglets'] = []
835 for cvp_configlet
in dev_cvp_configlets
:
836 if int(cvp_configlet
['containerCount']) == 0:
837 try_device
['deviceSpecificConfiglets'].append(
838 {'name': cvp_configlet
['name'],
839 'key': cvp_configlet
['key']})
840 # self.logger.debug(device)
844 # Check assigned configlets
845 device_update
= False
847 remove_configlets
= []
851 for cvp_configlet
in device
['deviceSpecificConfiglets']:
852 for cl
in new_configlets
:
853 if cvp_configlet
['name'] == cl
['name']:
854 remove_configlets
.append(cvp_configlet
)
857 for configlet
in new_configlets
:
858 if configlet
not in device
['deviceSpecificConfiglets']:
859 add_configlets
.append(configlet
)
862 update_devices
.append({'hostname': device
['hostname'],
863 'configlets': [add_configlets
,
866 self
.logger
.info("Device to modify: {}".format(update_devices
))
868 up_device
= update_devices
[0]
869 cl_toAdd
= up_device
['configlets'][0]
870 cl_toDel
= up_device
['configlets'][1]
873 if delete
and len(cl_toDel
) > 0:
874 r
= self
.client
.api
.remove_configlets_from_device(
880 self
.logger
.debug("remove_configlets_from_device {} {}".format(dev_action
, cl_toDel
))
881 elif len(cl_toAdd
) > 0:
882 r
= self
.client
.api
.apply_configlets_to_device(
888 self
.logger
.debug("apply_configlets_to_device {} {}".format(dev_action
, cl_toAdd
))
890 except Exception as error
:
891 errorMessage
= str(error
)
892 msg
= "errorMessage: Device {} Configlets couldnot be updated: {}".format(
893 up_device
['hostname'], errorMessage
)
894 raise SdnConnectorError(msg
) from error
896 if "errorMessage" in str(dev_action
):
897 m
= "Device {} Configlets update fail: {}".format(
898 up_device
['name'], dev_action
['errorMessage'])
899 raise SdnConnectorError(m
)
902 if 'taskIds' in str(dev_action
):
903 for taskId
in dev_action
['data']['taskIds']:
904 updated
.append({up_device
['hostname']:
905 "Configlets-{}".format(
907 newTasks
.append(taskId
)
909 updated
.append({up_device
['hostname']:
910 "Configlets-No_Specific_Tasks"})
911 data
= {'updated': updated
, 'tasks': newTasks
}
912 return [changed
, data
]
914 def __configlet_modify(self
, configletsToApply
, delete
=False):
915 ''' adds/update or delete the provided configLets
916 :param configletsToApply: list of configLets to apply
917 :param delete: flag to indicate if the configLets have to be deleted
918 from Cloud Vision Portal
919 :return: data: dict of module actions and taskIDs
921 self
.logger
.info('Enter in __configlet_modify delete:{}'.format(
924 # Compare configlets against cvp_facts-configlets
931 for cl
in configletsToApply
:
938 cvp_cl
= self
.client
.api
.get_configlet_by_name(cl
['name'])
939 cl
['key'] = cvp_cl
['key']
940 cl
['note'] = cvp_cl
['note']
942 except CvpApiError
as error
:
943 if "Entity does not exist" in error
.msg
:
951 configlet
= {'name': cvp_cl
['name'],
955 cl_compare
= self
.__compare
(cl
['config'],
957 # compare function returns a floating point number
958 if cl_compare
[0] != 100.0:
960 configlet
= {'name': cl
['name'],
962 'config': cl
['config']}
965 configlet
= {'name': cl
['name'],
966 'key': cvp_cl
['key'],
968 'config': cl
['config']}
971 configlet
= {'name': cl
['name'],
972 'config': cl
['config']}
976 resp
= self
.client
.api
.delete_configlet(
977 configlet
['data']['name'],
978 configlet
['data']['key'])
981 resp
= self
.client
.api
.update_configlet(
983 configlet
['data']['key'],
984 configlet
['data']['name'])
987 resp
= self
.client
.api
.add_configlet(
991 operation
= 'checked'
993 except Exception as error
:
994 errorMessage
= str(error
).split(':')[-1]
995 message
= "Configlet {} cannot be {}: {}".format(
996 cl
['name'], operation
, errorMessage
)
998 deleted
.append({configlet
['name']: message
})
1000 updated
.append({configlet
['name']: message
})
1002 new
.append({configlet
['name']: message
})
1004 checked
.append({configlet
['name']: message
})
1007 if "error" in str(resp
).lower():
1008 message
= "Configlet {} cannot be deleted: {}".format(
1009 cl
['name'], resp
['errorMessage'])
1011 deleted
.append({configlet
['name']: message
})
1013 updated
.append({configlet
['name']: message
})
1015 new
.append({configlet
['name']: message
})
1017 checked
.append({configlet
['name']: message
})
1021 deleted
.append({configlet
['name']: "success"})
1024 updated
.append({configlet
['name']: "success"})
1027 cl
['key'] = resp
# This key is used in API call deviceApplyConfigLet FGA
1028 new
.append({configlet
['name']: "success"})
1031 checked
.append({configlet
['name']: "success"})
1033 data
= {'new': new
, 'updated': updated
, 'deleted': deleted
, 'checked': checked
}
1034 return [changed
, data
]
1036 def __get_configletsDevices(self
, configlets
):
1037 for s
in self
.s_api
:
1038 configlet
= configlets
[s
]
1039 # Add applied Devices
1040 if len(configlet
) > 0:
1041 configlet
['devices'] = []
1042 applied_devices
= self
.client
.api
.get_applied_devices(
1044 for device
in applied_devices
['data']:
1045 configlet
['devices'].append(device
['hostName'])
1047 def __get_serviceData(self
, service_uuid
, service_type
, vlan_id
, conn_info
=None):
1049 for s
in self
.s_api
:
1052 srv_cls
= self
.__get
_serviceConfigLets
(service_uuid
,
1055 self
.__get
_configletsDevices
(srv_cls
)
1056 for s
in self
.s_api
:
1059 for dev
in cl
['devices']:
1060 cls_perSw
[dev
].append(cl
)
1062 cls_perSw
= conn_info
['configLetPerSwitch']
1065 def delete_connectivity_service(self
, service_uuid
, conn_info
=None):
1067 Disconnect multi-site endpoints previously connected
1069 :param service_uuid: The one returned by create_connectivity_service
1070 :param conn_info: The one returned by last call to 'create_connectivity_service' or 'edit_connectivity_service'
1071 if they do not return None
1073 :raises: SdnConnectorException: In case of error. The parameter http_code must be filled
1076 self
.logger
.debug('invoked delete_connectivity_service {}'.
1077 format(service_uuid
))
1078 if not service_uuid
:
1079 raise SdnConnectorError(message
='No connection service UUID',
1082 self
.__get
_Connection
()
1083 if conn_info
is None:
1084 raise SdnConnectorError(message
='No connection information for service UUID {}'.format(service_uuid
),
1087 cls_perSw
= self
.__get
_serviceData
(service_uuid
,
1088 conn_info
['service_type'],
1089 conn_info
['vlan_id'],
1091 allLeafConfigured
= {}
1092 allLeafModified
= {}
1093 for s
in self
.s_api
:
1094 allLeafConfigured
[s
] = True
1095 allLeafModified
[s
] = True
1096 found_in_cvp
= False
1097 for s
in self
.s_api
:
1101 self
.__rollbackConnection
(cls_perSw
,
1105 # if the service is not defined in Cloud Vision, return a 404 - NotFound error
1106 raise SdnConnectorError(message
='Service {} was not found in Arista Cloud Vision {}'.
1107 format(service_uuid
, self
.__wim
_url
),
1109 self
.__removeMetadata
(service_uuid
)
1110 except CvpLoginError
as e
:
1111 self
.logger
.info(str(e
))
1113 raise SdnConnectorError(message
=SdnError
.UNAUTHORIZED
,
1114 http_code
=401) from e
1115 except SdnConnectorError
as sde
:
1117 except Exception as ex
:
1119 self
.logger
.error(ex
)
1120 if self
.raiseException
:
1122 raise SdnConnectorError(message
=SdnError
.INTERNAL_ERROR
,
1123 http_code
=500) from ex
1125 def __addMetadata(self
, service_uuid
, service_type
, vlan_id
):
1126 """ Adds the connectivity service from 'OSM_metadata' configLet
1128 found_in_cvp
= False
1130 cvp_cl
= self
.client
.api
.get_configlet_by_name(self
.__OSM
_METADATA
)
1132 except CvpApiError
as error
:
1133 if "Entity does not exist" in error
.msg
:
1138 new_serv
= '{} {} {} {}\n'.format(self
.__METADATA
_PREFIX
, service_type
, vlan_id
, service_uuid
)
1141 cl_config
= cvp_cl
['config'] + new_serv
1143 cl_config
= new_serv
1144 cl_meta
= [{'name': self
.__OSM
_METADATA
, 'config': cl_config
}]
1145 self
.__configlet
_modify
(cl_meta
)
1146 except Exception as e
:
1147 self
.logger
.error('Error in setting metadata in CloudVision from OSM for service {}: {}'.
1148 format(service_uuid
, str(e
)))
1151 def __removeMetadata(self
, service_uuid
):
1152 """ Removes the connectivity service from 'OSM_metadata' configLet
1154 found_in_cvp
= False
1156 cvp_cl
= self
.client
.api
.get_configlet_by_name(self
.__OSM
_METADATA
)
1158 except CvpApiError
as error
:
1159 if "Entity does not exist" in error
.msg
:
1165 if service_uuid
in cvp_cl
['config']:
1167 for line
in cvp_cl
['config'].split('\n'):
1168 if service_uuid
in line
:
1171 cl_config
= cl_config
+ line
1172 cl_meta
= [{'name': self
.__OSM
_METADATA
, 'config': cl_config
}]
1173 self
.__configlet
_modify
(cl_meta
)
1174 except Exception as e
:
1175 self
.logger
.error('Error in removing metadata in CloudVision from OSM for service {}: {}'.
1176 format(service_uuid
, str(e
)))
1179 def edit_connectivity_service(self
,
1182 connection_points
=None,
1184 """ Change an existing connectivity service.
1186 This method's arguments and return value follow the same convention as
1187 :meth:`~.create_connectivity_service`.
1189 :param service_uuid: UUID of the connectivity service.
1190 :param conn_info: (dict or None): Information previously returned
1191 by last call to create_connectivity_service
1192 or edit_connectivity_service
1193 :param connection_points: (list): If provided, the old list of
1194 connection points will be replaced.
1195 :param kwargs: Same meaning that create_connectivity_service
1196 :return: dict or None: Information to be updated and stored at
1198 When ``None`` is returned, no information should be changed.
1199 When an empty dict is returned, the database record will
1201 **MUST** be JSON/YAML-serializable (plain data structures).
1203 SdnConnectorError: In case of error.
1206 self
.logger
.debug('invoked edit_connectivity_service for service {}. ports: {}'.format(service_uuid
,
1209 if not service_uuid
:
1210 raise SdnConnectorError(message
='Unable to perform operation, missing or empty uuid',
1213 raise SdnConnectorError(message
='Unable to perform operation, missing or empty connection information',
1216 if connection_points
is None:
1219 self
.__get
_Connection
()
1221 cls_currentPerSw
= conn_info
['configLetPerSwitch']
1222 service_type
= conn_info
['service_type']
1224 self
.__check
_service
(service_type
,
1230 s_uid
, s_connInf
= self
.__processConnection
(
1235 self
.logger
.info("Service with uuid {} configuration updated".
1238 except CvpLoginError
as e
:
1239 self
.logger
.info(str(e
))
1241 raise SdnConnectorError(message
=SdnError
.UNAUTHORIZED
,
1242 http_code
=401) from e
1243 except SdnConnectorError
as sde
:
1245 except Exception as ex
:
1248 # TODO check if there are pending task, and cancel them before restoring
1249 self
.__updateConnection
(cls_currentPerSw
)
1250 except Exception as e
:
1251 self
.logger
.error("Unable to restore configuration in service {} after an error in the configuration updated: {}".
1252 format(service_uuid
, str(e
)))
1253 if self
.raiseException
:
1255 raise SdnConnectorError(message
=str(ex
),
1256 http_code
=500) from ex
1258 def clear_all_connectivity_services(self
):
1259 """ Removes all connectivity services from Arista CloudVision with two steps:
1260 - retrives all the services from Arista CloudVision
1261 - removes each service
1264 self
.logger
.debug('invoked AristaImpl ' +
1265 'clear_all_connectivity_services')
1266 self
.__get
_Connection
()
1267 s_list
= self
.__get
_srvUUIDs
()
1270 conn_info
['service_type'] = serv
['type']
1271 conn_info
['vlan_id'] = serv
['vlan']
1273 self
.delete_connectivity_service(serv
['uuid'], conn_info
)
1274 except CvpLoginError
as e
:
1275 self
.logger
.info(str(e
))
1277 raise SdnConnectorError(message
=SdnError
.UNAUTHORIZED
,
1278 http_code
=401) from e
1279 except SdnConnectorError
as sde
:
1281 except Exception as ex
:
1283 self
.logger
.error(ex
)
1284 if self
.raiseException
:
1286 raise SdnConnectorError(message
=SdnError
.INTERNAL_ERROR
,
1287 http_code
=500) from ex
1289 def get_all_active_connectivity_services(self
):
1290 """ Return the uuid of all the active connectivity services with two steps:
1291 - retrives all the services from Arista CloudVision
1292 - retrives the status of each server
1295 self
.logger
.debug('invoked AristaImpl {}'.format(
1296 'get_all_active_connectivity_services'))
1297 self
.__get
_Connection
()
1298 s_list
= self
.__get
_srvUUIDs
()
1302 conn_info
['service_type'] = serv
['type']
1303 conn_info
['vlan_id'] = serv
['vlan']
1305 status
= self
.get_connectivity_service_status(serv
['uuid'], conn_info
)
1306 if status
['sdn_status'] == 'ACTIVE':
1307 result
.append(serv
['uuid'])
1309 except CvpLoginError
as e
:
1310 self
.logger
.info(str(e
))
1312 raise SdnConnectorError(message
=SdnError
.UNAUTHORIZED
,
1313 http_code
=401) from e
1314 except SdnConnectorError
as sde
:
1316 except Exception as ex
:
1318 self
.logger
.error(ex
)
1319 if self
.raiseException
:
1321 raise SdnConnectorError(message
=SdnError
.INTERNAL_ERROR
,
1322 http_code
=500) from ex
1324 def __get_serviceConfigLets(self
, service_uuid
, service_type
, vlan_id
):
1325 """ Return the configLet's associated with a connectivity service,
1326 There should be one, as maximum, per device (switch) for a given
1327 connectivity service
1330 for s
in self
.s_api
:
1332 found_in_cvp
= False
1333 name
= (self
.__OSM
_PREFIX
+
1335 self
.__SEPARATOR
+ service_type
+ str(vlan_id
) +
1336 self
.__SEPARATOR
+ service_uuid
)
1338 cvp_cl
= self
.client
.api
.get_configlet_by_name(name
)
1340 except CvpApiError
as error
:
1341 if "Entity does not exist" in error
.msg
:
1349 def __get_srvVLANs(self
):
1350 """ Returns a list with all the VLAN id's used in the connectivity services managed
1351 in tha Arista CloudVision by checking the 'OSM_metadata' configLet where this
1352 information is stored
1354 found_in_cvp
= False
1356 cvp_cl
= self
.client
.api
.get_configlet_by_name(self
.__OSM
_METADATA
)
1358 except CvpApiError
as error
:
1359 if "Entity does not exist" in error
.msg
:
1365 lines
= cvp_cl
['config'].split('\n')
1367 if self
.__METADATA
_PREFIX
in line
:
1368 s_vlan
= line
.split(' ')[3]
1371 if (s_vlan
is not None and
1373 s_vlan
not in s_vlan_list
):
1374 s_vlan_list
.append(s_vlan
)
1378 def __get_srvUUIDs(self
):
1379 """ Retrieves all the connectivity services, managed in tha Arista CloudVision
1380 by checking the 'OSM_metadata' configLet where this information is stored
1382 found_in_cvp
= False
1384 cvp_cl
= self
.client
.api
.get_configlet_by_name(self
.__OSM
_METADATA
)
1386 except CvpApiError
as error
:
1387 if "Entity does not exist" in error
.msg
:
1393 lines
= cvp_cl
['config'].split('\n')
1395 if self
.__METADATA
_PREFIX
in line
:
1396 line
= line
.split(' ')
1397 serv
= {'uuid': line
[4], 'type': line
[2], 'vlan': line
[3]}
1400 if (serv
is not None and
1402 serv
not in serv_list
):
1403 serv_list
.append(serv
)
1407 def __get_Connection(self
):
1408 """ Open a connection with Arista CloudVision,
1409 invoking the version retrival as test
1412 if self
.client
is None:
1413 self
.client
= self
.__connect
()
1414 self
.client
.api
.get_cvp_info()
1415 except (CvpSessionLogOutError
, RequestException
) as e
:
1416 self
.logger
.debug("Connection error '{}'. Reconnencting".format(e
))
1417 self
.client
= self
.__connect
()
1418 self
.client
.api
.get_cvp_info()
1420 def __connect(self
):
1421 ''' Connects to CVP device using user provided credentials from initialization.
1422 :return: CvpClient object with connection instantiated.
1424 client
= CvpClient()
1425 protocol
, _
, rest_url
= self
.__wim
_url
.rpartition("://")
1426 host
, _
, port
= rest_url
.partition(":")
1427 if port
and port
.endswith("/"):
1428 port
= int(port
[:-1])
1434 client
.connect([host
],
1437 protocol
=protocol
or "https",
1440 self
.taskC
= AristaCVPTask(client
.api
)
1443 def __compare(self
, fromText
, toText
, lines
=10):
1444 """ Compare text string in 'fromText' with 'toText' and produce
1445 diffRatio - a score as a float in the range [0, 1] 2.0*M / T
1446 T is the total number of elements in both sequences,
1447 M is the number of matches.
1448 Score - 1.0 if the sequences are identical, and
1449 0.0 if they have nothing in common.
1452 '- ' line unique to sequence 1
1453 '+ ' line unique to sequence 2
1454 ' ' line common to both sequences
1455 '? ' line not present in either input sequence
1457 fromlines
= fromText
.splitlines(1)
1458 tolines
= toText
.splitlines(1)
1459 diff
= list(difflib
.unified_diff(fromlines
, tolines
, n
=lines
))
1460 textComp
= difflib
.SequenceMatcher(None, fromText
, toText
)
1461 diffRatio
= round(textComp
.quick_ratio()*100, 2)
1462 return [diffRatio
, diff
]
1464 def __load_inventory(self
):
1465 """ Get Inventory Data for All Devices (aka switches) from the Arista CloudVision
1467 if not self
.cvp_inventory
:
1468 self
.cvp_inventory
= self
.client
.api
.get_inventory()
1469 self
.allDeviceFacts
= []
1470 for device
in self
.cvp_inventory
:
1471 self
.allDeviceFacts
.append(device
)
1473 def is_valid_destination(self
, url
):
1474 """ Check that the provided WIM URL is correct
1476 if re
.match(self
.__regex
, url
):
1478 elif self
.is_valid_ipv4_address(url
):
1481 return self
.is_valid_ipv6_address(url
)
1483 def is_valid_ipv4_address(self
, address
):
1484 """ Checks that the given IP is IPv4 valid
1487 socket
.inet_pton(socket
.AF_INET
, address
)
1488 except AttributeError: # no inet_pton here, sorry
1490 socket
.inet_aton(address
)
1491 except socket
.error
:
1493 return address
.count('.') == 3
1494 except socket
.error
: # not a valid address
1498 def is_valid_ipv6_address(self
, address
):
1499 """ Checks that the given IP is IPv6 valid
1502 socket
.inet_pton(socket
.AF_INET6
, address
)
1503 except socket
.error
: # not a valid address