fixing flake8 tests
[osm/RO.git] / RO-SDN-arista_cloudvision / osm_rosdn_arista_cloudvision / wimconn_arista.py
1 # -*- coding: utf-8 -*-
2 ##
3 # Copyright 2019 Atos - CoE Telco NFV Team
4 # All Rights Reserved.
5 #
6 # Contributors: Oscar Luis Peral, Atos
7 #
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
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
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
18 # under the License.
19 #
20 # For those usages not covered by the Apache License, Version 2.0 please
21 # contact with: <oscarluis.peral@atos.net>
22 #
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.
26 #
27 # This work has been performed in the context of Arista Telefonica OSM PoC.
28 ##
29 from osm_ro_plugin.sdnconn import SdnConnectorBase, SdnConnectorError
30 import re
31 import socket
32 # Required by compare function
33 import difflib
34 # Library that uses Levenshtein Distance to calculate the differences
35 # between strings.
36 # from fuzzywuzzy import fuzz
37
38 import logging
39 import uuid
40 from enum import Enum
41 from requests import RequestException
42
43 from cvprac.cvp_client import CvpClient
44 from cvprac.cvp_api import CvpApi
45 from cvprac.cvp_client_errors import CvpLoginError, CvpSessionLogOutError, CvpApiError
46 from cvprac import __version__ as cvprac_version
47
48 from osm_rosdn_arista_cloudvision.aristaConfigLet import AristaSDNConfigLet
49 from osm_rosdn_arista_cloudvision.aristaTask import AristaCVPTask
50
51
52 class SdnError(Enum):
53 UNREACHABLE = 'Unable to reach the WIM.',
54 VLAN_INCONSISTENT = \
55 'VLAN value inconsistent between the connection points',
56 VLAN_NOT_PROVIDED = 'VLAN value not provided',
57 CONNECTION_POINTS_SIZE = \
58 'Unexpected number of connection points: 2 expected.',
59 ENCAPSULATION_TYPE = \
60 'Unexpected service_endpoint_encapsulation_type. \
61 Only "dotq1" is accepted.',
62 BANDWIDTH = 'Unable to get the bandwidth.',
63 STATUS = 'Unable to get the status for the service.',
64 DELETE = 'Unable to delete service.',
65 CLEAR_ALL = 'Unable to clear all the services',
66 UNKNOWN_ACTION = 'Unknown action invoked.',
67 BACKUP = 'Unable to get the backup parameter.',
68 UNSUPPORTED_FEATURE = "Unsupported feature",
69 UNAUTHORIZED = "Failed while authenticating",
70 INTERNAL_ERROR = "Internal error"
71
72
73 class AristaSdnConnector(SdnConnectorBase):
74 """Arista class for the SDN connectors
75
76 Arguments:
77 wim (dict): WIM record, as stored in the database
78 wim_account (dict): WIM account record, as stored in the database
79 config
80 The arguments of the constructor are converted to object attributes.
81 An extra property, ``service_endpoint_mapping`` is created from ``config``.
82
83 The access to Arista CloudVision is made through the API defined in
84 https://github.com/aristanetworks/cvprac
85 The a connectivity service consist in creating a VLAN and associate the interfaces
86 of the connection points MAC addresses to this VLAN in all the switches of the topology,
87 the BDP is also configured for this VLAN.
88
89 The Arista Cloud Vision API workflow is the following
90 -- The switch configuration is defined as a set of switch configuration commands,
91 what is called 'ConfigLet'
92 -- The ConfigLet is associated to the device (leaf switch)
93 -- Automatically a task is associated to this activity for change control, the task
94 in this stage is in 'Pending' state
95 -- The task will be executed so that the configuration is applied to the switch.
96 -- The service information is saved in the response of the creation call
97 -- All created services identification is stored in a generic ConfigLet 'OSM_metadata'
98 to keep track of the managed resources by OSM in the Arista deployment.
99 """
100 __supported_service_types = ["ELINE (L2)", "ELINE", "ELAN"]
101 __service_types_ELAN = "ELAN"
102 __service_types_ELINE = "ELINE"
103 __ELINE_num_connection_points = 2
104 __supported_service_types = ["ELINE", "ELAN"]
105 __supported_encapsulation_types = ["dot1q"]
106 __WIM_LOGGER = 'openmano.sdnconn.arista'
107 __SERVICE_ENDPOINT_MAPPING = 'service_endpoint_mapping'
108 __ENCAPSULATION_TYPE_PARAM = "service_endpoint_encapsulation_type"
109 __ENCAPSULATION_INFO_PARAM = "service_endpoint_encapsulation_info"
110 __BACKUP_PARAM = "backup"
111 __BANDWIDTH_PARAM = "bandwidth"
112 __SERVICE_ENDPOINT_PARAM = "service_endpoint_id"
113 __MAC_PARAM = "mac"
114 __WAN_SERVICE_ENDPOINT_PARAM = "service_endpoint_id"
115 __WAN_MAPPING_INFO_PARAM = "service_mapping_info"
116 __DEVICE_ID_PARAM = "device_id"
117 __DEVICE_INTERFACE_ID_PARAM = "device_interface_id"
118 __SW_ID_PARAM = "switch_dpid"
119 __SW_PORT_PARAM = "switch_port"
120 __VLAN_PARAM = "vlan"
121 __VNI_PARAM = "vni"
122 __SEPARATOR = '_'
123 __MANAGED_BY_OSM = '## Managed by OSM '
124 __OSM_PREFIX = "osm_"
125 __OSM_METADATA = "OSM_metadata"
126 __METADATA_PREFIX = '!## Service'
127 __EXC_TASK_EXEC_WAIT = 10
128 __ROLLB_TASK_EXEC_WAIT = 10
129 __API_REQUEST_TOUT = 60
130 __SWITCH_TAG_NAME = 'topology_type'
131 __SWITCH_TAG_VALUE = 'leaf'
132 __LOOPBACK_INTF = "Loopback0"
133 _VLAN = "VLAN"
134 _VXLAN = "VXLAN"
135 _VLAN_MLAG = "VLAN-MLAG"
136 _VXLAN_MLAG = "VXLAN-MLAG"
137
138 def __init__(self, wim, wim_account, config=None, logger=None):
139 """
140
141 :param wim: (dict). Contains among others 'wim_url'
142 :param wim_account: (dict). Contains among others 'uuid' (internal id), 'name',
143 'sdn' (True if is intended for SDN-assist or False if intended for WIM), 'user', 'password'.
144 :param config: (dict or None): Particular information of plugin. These keys if present have a common meaning:
145 'mapping_not_needed': (bool) False by default or if missing, indicates that mapping is not needed.
146 'service_endpoint_mapping': (list) provides the internal endpoint mapping. The meaning is:
147 KEY meaning for WIM meaning for SDN assist
148 -------- -------- --------
149 device_id pop_switch_dpid compute_id
150 device_interface_id pop_switch_port compute_pci_address
151 service_endpoint_id wan_service_endpoint_id SDN_service_endpoint_id
152 service_mapping_info wan_service_mapping_info SDN_service_mapping_info
153 contains extra information if needed. Text in Yaml format
154 switch_dpid wan_switch_dpid SDN_switch_dpid
155 switch_port wan_switch_port SDN_switch_port
156 datacenter_id vim_account vim_account
157 id: (internal, do not use)
158 wim_id: (internal, do not use)
159 :param logger (logging.Logger): optional logger object. If none is passed 'openmano.sdn.sdnconn' is used.
160 """
161 self.__regex = re.compile(
162 r'^(?:http|ftp)s?://' # http:// or https://
163 r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # domain...
164 r'localhost|' # localhost...
165 r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip
166 r'(?::\d+)?', re.IGNORECASE) # optional port
167 self.raiseException = True
168 self.logger = logger or logging.getLogger(self.__WIM_LOGGER)
169 super().__init__(wim, wim_account, config, self.logger)
170 self.__wim = wim
171 self.__wim_account = wim_account
172 self.__config = config
173 if self.is_valid_destination(self.__wim.get("wim_url")):
174 self.__wim_url = self.__wim.get("wim_url")
175 else:
176 raise SdnConnectorError(message='Invalid wim_url value',
177 http_code=500)
178 self.__user = wim_account.get("user")
179 self.__passwd = wim_account.get("password")
180 self.client = None
181 self.cvp_inventory = None
182 self.cvp_tags = None
183 self.logger.debug("Arista SDN plugin {}, cvprac version {}, user:{} and config:{}".
184 format(wim, cvprac_version, self.__user,
185 self.delete_keys_from_dict(config, ('passwd',))))
186 self.allDeviceFacts = []
187 self.taskC = None
188 try:
189 self.__load_topology()
190 self.__load_switches()
191 except SdnConnectorError as sc:
192 raise sc
193 except Exception as e:
194 raise SdnConnectorError(message="Unable to load switches from CVP",
195 http_code=500) from e
196 self.logger.debug("Using topology {} in Arista Leaf switches: {}".format(
197 self.topology,
198 self.delete_keys_from_dict(self.switches, ('passwd',))))
199 self.clC = AristaSDNConfigLet(self.topology)
200
201 def __load_topology(self):
202 self.topology = self._VXLAN_MLAG
203 if self.__config and self.__config.get('topology'):
204 topology = self.__config.get('topology')
205 if topology == "VLAN":
206 self.topology = self._VLAN
207 elif topology == "VXLAN":
208 self.topology = self._VXLAN
209 elif topology == "VLAN-MLAG":
210 self.topology = self._VLAN_MLAG
211 elif topology == "VXLAN-MLAG":
212 self.topology = self._VXLAN_MLAG
213
214 def __load_switches(self):
215 """ Retrieves the switches to configure in the following order
216 1. from incoming configuration:
217 1.1 using port mapping
218 using user and password from WIM
219 retrieving Lo0 and AS from switch
220 1.2 from 'switches' parameter,
221 if any parameter is not present
222 Lo0 and AS - it will be requested to the switch
223 2. Looking in the CloudVision inventory if not in configuration parameters
224 2.1 using the switches with the topology_type tag set to 'leaf'
225
226 All the search methods will be used
227 """
228 self.switches = {}
229 if self.__config and self.__config.get(self.__SERVICE_ENDPOINT_MAPPING):
230 for port in self.__config.get(self.__SERVICE_ENDPOINT_MAPPING):
231 switch_dpid = port.get(self.__SW_ID_PARAM)
232 if switch_dpid and switch_dpid not in self.switches:
233 self.switches[switch_dpid] = {'passwd': self.__passwd,
234 'ip': None,
235 'usr': self.__user,
236 'lo0': None,
237 'AS': None,
238 'serialNumber': None,
239 'mlagPeerDevice': None}
240
241 if self.__config and self.__config.get('switches'):
242 # Not directly from json, complete one by one
243 config_switches = self.__config.get('switches')
244 for cs, cs_content in config_switches.items():
245 if cs not in self.switches:
246 self.switches[cs] = {'passwd': self.__passwd,
247 'ip': None,
248 'usr': self.__user,
249 'lo0': None,
250 'AS': None,
251 'serialNumber': None,
252 'mlagPeerDevice': None}
253 if cs_content:
254 self.switches[cs].update(cs_content)
255
256 # Load the rest of the data
257 if self.client is None:
258 self.client = self.__connect()
259 self.__load_inventory()
260 if not self.switches:
261 self.__get_tags(self.__SWITCH_TAG_NAME, self.__SWITCH_TAG_VALUE)
262 for device in self.allDeviceFacts:
263 # get the switches whose topology_tag is 'leaf'
264 if device['serialNumber'] in self.cvp_tags:
265 if not self.switches.get(device['hostname']):
266 switch_data = {'passwd': self.__passwd,
267 'ip': device['ipAddress'],
268 'usr': self.__user,
269 'lo0': None,
270 'AS': None,
271 'serialNumber': None,
272 'mlagPeerDevice': None}
273 self.switches[device['hostname']] = switch_data
274 if len(self.switches) == 0:
275 self.logger.error("Unable to load Leaf switches from CVP")
276 return
277
278 # self.switches are switch objects, one for each switch in self.switches,
279 # used to make eAPI calls by using switch.py module
280 for s in self.switches:
281 for device in self.allDeviceFacts:
282 if device['hostname'] == s:
283 if not self.switches[s].get('ip'):
284 self.switches[s]['ip'] = device['ipAddress']
285 self.switches[s]['serialNumber'] = device['serialNumber']
286 break
287
288 # Each switch has a different loopback address,
289 # so it's a different configLet
290 if not self.switches[s].get('lo0'):
291 inf = self.__get_interface_ip(self.switches[s]['serialNumber'], self.__LOOPBACK_INTF)
292 self.switches[s]["lo0"] = inf.split('/')[0]
293 if not self.switches[s].get('AS'):
294 self.switches[s]["AS"] = self.__get_device_ASN(self.switches[s]['serialNumber'])
295 if self.topology in (self._VXLAN_MLAG, self._VLAN_MLAG):
296 for s in self.switches:
297 if not self.switches[s].get('mlagPeerDevice'):
298 self.switches[s]['mlagPeerDevice'] = self.__get_peer_MLAG(self.switches[s]['serialNumber'])
299
300 def __check_service(self, service_type, connection_points,
301 check_vlan=True, check_num_cp=True, kwargs=None):
302 """ Reviews the connection points elements looking for semantic errors in the incoming data
303 """
304 if service_type not in self.__supported_service_types:
305 raise Exception("The service '{}' is not supported. Only '{}' are accepted".format(
306 service_type,
307 self.__supported_service_types))
308
309 if check_num_cp:
310 if len(connection_points) < 2:
311 raise Exception(SdnError.CONNECTION_POINTS_SIZE)
312 if (len(connection_points) != self.__ELINE_num_connection_points and
313 service_type == self.__service_types_ELINE):
314 raise Exception(SdnError.CONNECTION_POINTS_SIZE)
315
316 if check_vlan:
317 vlan_id = ''
318 for cp in connection_points:
319 enc_type = cp.get(self.__ENCAPSULATION_TYPE_PARAM)
320 if (enc_type and
321 enc_type not in self.__supported_encapsulation_types):
322 raise Exception(SdnError.ENCAPSULATION_TYPE)
323 encap_info = cp.get(self.__ENCAPSULATION_INFO_PARAM)
324 cp_vlan_id = str(encap_info.get(self.__VLAN_PARAM))
325 if cp_vlan_id:
326 if not vlan_id:
327 vlan_id = cp_vlan_id
328 elif vlan_id != cp_vlan_id:
329 raise Exception(SdnError.VLAN_INCONSISTENT)
330 if not vlan_id:
331 raise Exception(SdnError.VLAN_NOT_PROVIDED)
332 if vlan_id in self.__get_srvVLANs():
333 raise Exception('VLAN {} already assigned to a connectivity service'.format(vlan_id))
334
335 # Commented out for as long as parameter isn't implemented
336 # bandwidth = kwargs.get(self.__BANDWIDTH_PARAM)
337 # if not isinstance(bandwidth, int):
338 # self.__exception(SdnError.BANDWIDTH, http_code=400)
339
340 # Commented out for as long as parameter isn't implemented
341 # backup = kwargs.get(self.__BACKUP_PARAM)
342 # if not isinstance(backup, bool):
343 # self.__exception(SdnError.BACKUP, http_code=400)
344
345 def check_credentials(self):
346 """Retrieves the CloudVision version information, as the easiest way
347 for testing the access to CloudVision API
348 """
349 try:
350 if self.client is None:
351 self.client = self.__connect()
352 result = self.client.api.get_cvp_info()
353 self.logger.debug(result)
354 except CvpLoginError as e:
355 self.logger.info(str(e))
356 self.client = None
357 raise SdnConnectorError(message=SdnError.UNAUTHORIZED,
358 http_code=401) from e
359 except Exception as ex:
360 self.client = None
361 self.logger.error(str(ex))
362 raise SdnConnectorError(message=SdnError.INTERNAL_ERROR,
363 http_code=500) from ex
364
365 def get_connectivity_service_status(self, service_uuid, conn_info=None):
366 """Monitor the status of the connectivity service established
367 Arguments:
368 service_uuid (str): UUID of the connectivity service
369 conn_info (dict or None): Information returned by the connector
370 during the service creation/edition and subsequently stored in
371 the database.
372
373 Returns:
374 dict: JSON/YAML-serializable dict that contains a mandatory key
375 ``sdn_status`` associated with one of the following values::
376
377 {'sdn_status': 'ACTIVE'}
378 # The service is up and running.
379
380 {'sdn_status': 'INACTIVE'}
381 # The service was created, but the connector
382 # cannot determine yet if connectivity exists
383 # (ideally, the caller needs to wait and check again).
384
385 {'sdn_status': 'DOWN'}
386 # Connection was previously established,
387 # but an error/failure was detected.
388
389 {'sdn_status': 'ERROR'}
390 # An error occurred when trying to create the service/
391 # establish the connectivity.
392
393 {'sdn_status': 'BUILD'}
394 # Still trying to create the service, the caller
395 # needs to wait and check again.
396
397 Additionally ``error_msg``(**str**) and ``sdn_info``(**dict**)
398 keys can be used to provide additional status explanation or
399 new information available for the connectivity service.
400 """
401 try:
402 self.logger.debug("invoked get_connectivity_service_status '{}'".format(service_uuid))
403 if not service_uuid:
404 raise SdnConnectorError(message='No connection service UUID',
405 http_code=500)
406
407 self.__get_Connection()
408 if conn_info is None:
409 raise SdnConnectorError(message='No connection information for service UUID {}'.format(service_uuid),
410 http_code=500)
411
412 if 'configLetPerSwitch' in conn_info.keys():
413 c_info = conn_info
414 else:
415 c_info = None
416 cls_perSw = self.__get_serviceData(service_uuid,
417 conn_info['service_type'],
418 conn_info['vlan_id'],
419 c_info)
420
421 t_isCancelled = False
422 t_isFailed = False
423 t_isPending = False
424 failed_switches = []
425 for s in self.switches:
426 if len(cls_perSw[s]) > 0:
427 for cl in cls_perSw[s]:
428 # Fix 1030 SDN-ARISTA Key error note when deploy a NS
429 # Added protection to check that 'note' exists and additionally
430 # verify that it is managed by OSM
431 if (not cls_perSw[s][0]['config'] or
432 not cl.get('note') or
433 self.__MANAGED_BY_OSM not in cl['note']):
434 continue
435 note = cl['note']
436 t_id = note.split(self.__SEPARATOR)[1]
437 result = self.client.api.get_task_by_id(t_id)
438 if result['workOrderUserDefinedStatus'] == 'Completed':
439 continue
440 elif result['workOrderUserDefinedStatus'] == 'Cancelled':
441 t_isCancelled = True
442 elif result['workOrderUserDefinedStatus'] == 'Failed':
443 t_isFailed = True
444 else:
445 t_isPending = True
446 failed_switches.append(s)
447 if t_isCancelled:
448 error_msg = 'Some works were cancelled in switches: {}'.format(str(failed_switches))
449 sdn_status = 'DOWN'
450 elif t_isFailed:
451 error_msg = 'Some works failed in switches: {}'.format(str(failed_switches))
452 sdn_status = 'ERROR'
453 elif t_isPending:
454 error_msg = 'Some works are still under execution in switches: {}'.format(str(failed_switches))
455 sdn_status = 'BUILD'
456 else:
457 error_msg = ''
458 sdn_status = 'ACTIVE'
459 sdn_info = ''
460 return {'sdn_status': sdn_status,
461 'error_msg': error_msg,
462 'sdn_info': sdn_info}
463 except CvpLoginError as e:
464 self.logger.info(str(e))
465 self.client = None
466 raise SdnConnectorError(message=SdnError.UNAUTHORIZED,
467 http_code=401) from e
468 except Exception as ex:
469 self.client = None
470 self.logger.error(str(ex), exc_info=True)
471 raise SdnConnectorError(message=str(ex),
472 http_code=500) from ex
473
474 def create_connectivity_service(self, service_type, connection_points,
475 **kwargs):
476 """Stablish SDN/WAN connectivity between the endpoints
477 :param service_type:
478 (str): ``ELINE`` (L2), ``ELAN`` (L2), ``ETREE`` (L2), ``L3``.
479 :param connection_points: (list): each point corresponds to
480 an entry point to be connected. For WIM: from the DC
481 to the transport network.
482 For SDN: Compute/PCI to the transport network. One
483 connection point serves to identify the specific access and
484 some other service parameters, such as encapsulation type.
485 Each item of the list is a dict with:
486 "service_endpoint_id": (str)(uuid) Same meaning that for
487 'service_endpoint_mapping' (see __init__)
488 In case the config attribute mapping_not_needed is True,
489 this value is not relevant. In this case
490 it will contain the string "device_id:device_interface_id"
491 "service_endpoint_encapsulation_type": None, "dot1q", ...
492 "service_endpoint_encapsulation_info": (dict) with:
493 "vlan": ..., (int, present if encapsulation is dot1q)
494 "vni": ... (int, present if encapsulation is vxlan),
495 "peers": [(ipv4_1), (ipv4_2)] (present if
496 encapsulation is vxlan)
497 "mac": ...
498 "device_id": ..., same meaning that for
499 'service_endpoint_mapping' (see __init__)
500 "device_interface_id": same meaning that for
501 'service_endpoint_mapping' (see __init__)
502 "switch_dpid": ..., present if mapping has been found
503 for this device_id,device_interface_id
504 "switch_port": ... present if mapping has been found
505 for this device_id,device_interface_id
506 "service_mapping_info": present if mapping has
507 been found for this device_id,device_interface_id
508 :param kwargs: For future versions:
509 bandwidth (int): value in kilobytes
510 latency (int): value in milliseconds
511 Other QoS might be passed as keyword arguments.
512 :return: tuple: ``(service_id, conn_info)`` containing:
513 - *service_uuid* (str): UUID of the established
514 connectivity service
515 - *conn_info* (dict or None): Information to be
516 stored at the database (or ``None``).
517 This information will be provided to the
518 :meth:`~.edit_connectivity_service` and :obj:`~.delete`.
519 **MUST** be JSON/YAML-serializable (plain data structures).
520 :raises: SdnConnectorError: In case of error. Nothing should be
521 created in this case.
522 Provide the parameter http_code
523 """
524 try:
525 self.logger.debug("invoked create_connectivity_service '{}' ports: {}".
526 format(service_type, connection_points))
527 self.__get_Connection()
528 self.__check_service(service_type,
529 connection_points,
530 check_vlan=True,
531 kwargs=kwargs)
532 service_uuid = str(uuid.uuid4())
533
534 self.logger.info("Service with uuid {} created.".
535 format(service_uuid))
536 s_uid, s_connInf = self.__processConnection(
537 service_uuid,
538 service_type,
539 connection_points,
540 kwargs)
541 try:
542 self.__addMetadata(s_uid, service_type, s_connInf['vlan_id'])
543 except Exception:
544 pass
545
546 return (s_uid, s_connInf)
547 except CvpLoginError as e:
548 self.logger.info(str(e))
549 self.client = None
550 raise SdnConnectorError(message=SdnError.UNAUTHORIZED,
551 http_code=401) from e
552 except SdnConnectorError as sde:
553 raise sde
554 except ValueError as err:
555 self.client = None
556 self.logger.error(str(err), exc_info=True)
557 raise SdnConnectorError(message=str(err),
558 http_code=500) from err
559 except Exception as ex:
560 self.client = None
561 self.logger.error(str(ex), exc_info=True)
562 if self.raiseException:
563 raise ex
564 raise SdnConnectorError(message=str(ex),
565 http_code=500) from ex
566
567 def __processConnection(self,
568 service_uuid,
569 service_type,
570 connection_points,
571 kwargs):
572 """
573 Invoked from creation and edit methods
574
575 Process the connection points array,
576 creating a set of configuration per switch where it has to be applied
577 for creating the configuration, the switches have to be queried for obtaining:
578 - the loopback address
579 - the BGP ASN (autonomous system number)
580 - the interface name of the MAC address to add in the connectivity service
581 Once the new configuration is ready, the __updateConnection method is invoked for appling the changes
582 """
583 try:
584 cls_perSw = {}
585 cls_cp = {}
586 cl_bgp = {}
587 for s in self.switches:
588 cls_perSw[s] = []
589 cls_cp[s] = []
590 vlan_processed = False
591 vlan_id = ''
592 i = 0
593 processed_connection_points = []
594 for cp in connection_points:
595 i += 1
596 encap_info = cp.get(self.__ENCAPSULATION_INFO_PARAM)
597 if not vlan_processed:
598 vlan_id = str(encap_info.get(self.__VLAN_PARAM))
599 if not vlan_id:
600 continue
601 vni_id = encap_info.get(self.__VNI_PARAM)
602 if not vni_id:
603 vni_id = str(10000 + int(vlan_id))
604
605 if service_type == self.__service_types_ELAN:
606 cl_vlan = self.clC.getElan_vlan(service_uuid,
607 vlan_id,
608 vni_id)
609 else:
610 cl_vlan = self.clC.getEline_vlan(service_uuid,
611 vlan_id,
612 vni_id)
613 vlan_processed = True
614
615 encap_type = cp.get(self.__ENCAPSULATION_TYPE_PARAM)
616 switch_id = encap_info.get(self.__SW_ID_PARAM)
617 interface = encap_info.get(self.__SW_PORT_PARAM)
618 switches = [{'name': switch_id, 'interface': interface}]
619
620 # remove those connections that are equal. This happens when several sriovs are located in the same
621 # compute node interface, that is, in the same switch and interface
622 switches = [x for x in switches if x not in processed_connection_points]
623 if not switches:
624 continue
625 processed_connection_points += switches
626 for switch in switches:
627 if not interface:
628 raise SdnConnectorError(
629 message="Connection point switch port empty for switch_dpid {}".format(switch_id),
630 http_code=406)
631 # it should be only one switch where the mac is attached
632 if encap_type == 'dot1q':
633 # SRIOV configLet for Leaf switch mac's attached to
634 if service_type == self.__service_types_ELAN:
635 cl_encap = self.clC.getElan_sriov(service_uuid, interface, vlan_id, i)
636 else:
637 cl_encap = self.clC.getEline_sriov(service_uuid, interface, vlan_id, i)
638 elif not encap_type:
639 # PT configLet for Leaf switch attached to the mac
640 if service_type == self.__service_types_ELAN:
641 cl_encap = self.clC.getElan_passthrough(service_uuid,
642 interface,
643 vlan_id, i)
644 else:
645 cl_encap = self.clC.getEline_passthrough(service_uuid,
646 interface,
647 vlan_id, i)
648 if cls_cp.get(switch['name']):
649 cls_cp[switch['name']] = str(cls_cp[switch['name']]) + cl_encap
650 else:
651 cls_cp[switch['name']] = cl_encap
652
653 # at least 1 connection point has to be received
654 if not vlan_processed:
655 raise SdnConnectorError(message=SdnError.UNSUPPORTED_FEATURE,
656 http_code=406)
657
658 for s in self.switches:
659 # for cl in cp_configLets:
660 cl_name = (self.__OSM_PREFIX +
661 s +
662 self.__SEPARATOR + service_type + str(vlan_id) +
663 self.__SEPARATOR + service_uuid)
664 cl_config = ''
665 # Apply BGP configuration only for VXLAN topologies
666 if self.topology in (self._VXLAN_MLAG, self._VXLAN):
667 if service_type == self.__service_types_ELAN:
668 cl_bgp[s] = self.clC.getElan_bgp(service_uuid,
669 vlan_id,
670 vni_id,
671 self.switches[s]['lo0'],
672 self.switches[s]['AS'])
673 else:
674 cl_bgp[s] = self.clC.getEline_bgp(service_uuid,
675 vlan_id,
676 vni_id,
677 self.switches[s]['lo0'],
678 self.switches[s]['AS'])
679 else:
680 cl_bgp[s] = ''
681
682 if not cls_cp.get(s):
683 # Apply VLAN configuration to peer MLAG switch,
684 # only necessary when there are no connection points in the switch
685 if self.topology in (self._VXLAN_MLAG, self._VLAN_MLAG):
686 for p in self.switches:
687 if self.switches[p]['mlagPeerDevice'] == s:
688 if cls_cp.get(p):
689 cl_config = str(cl_vlan)
690 else:
691 cl_config = str(cl_vlan) + str(cl_bgp[s]) + str(cls_cp[s])
692
693 cls_perSw[s] = [{'name': cl_name, 'config': cl_config}]
694
695 allLeafConfigured, allLeafModified = self.__updateConnection(cls_perSw)
696
697 conn_info = {
698 "uuid": service_uuid,
699 "status": "BUILD",
700 "service_type": service_type,
701 "vlan_id": vlan_id,
702 "connection_points": connection_points,
703 "configLetPerSwitch": cls_perSw,
704 'allLeafConfigured': allLeafConfigured,
705 'allLeafModified': allLeafModified}
706
707 return service_uuid, conn_info
708 except Exception as ex:
709 self.logger.debug("Exception processing connection {}: {}".
710 format(service_uuid, str(ex)))
711 raise ex
712
713 def __updateConnection(self, cls_perSw):
714 """ Invoked in the creation and modification
715
716 checks if the new connection points config is:
717 - already in the Cloud Vision, the configLet is modified, and applied to the switch,
718 executing the corresponding task
719 - if it has to be removed:
720 then configuration has to be removed from the switch executing the corresponding task,
721 before trying to remove the configuration
722 - created, the configuration set is created, associated to the switch, and the associated
723 task to the configLet modification executed
724 In case of any error, rollback is executed, removing the created elements, and restoring to the
725 previous state.
726 """
727 try:
728 allLeafConfigured = {}
729 allLeafModified = {}
730
731 for s in self.switches:
732 allLeafConfigured[s] = False
733 allLeafModified[s] = False
734 cl_toDelete = []
735 for s in self.switches:
736 toDelete_in_cvp = False
737 if not (cls_perSw.get(s) and cls_perSw[s][0].get('config')):
738 # when there is no configuration, means that there is no interface
739 # in the switch to be connected, so the configLet has to be removed from CloudVision
740 # after removing the ConfigLet fron the switch if it was already there
741
742 # get config let name and key
743 cl = cls_perSw[s]
744 try:
745 cvp_cl = self.client.api.get_configlet_by_name(cl[0]['name'])
746 # remove configLet
747 cl_toDelete.append(cvp_cl)
748 cl[0] = cvp_cl
749 toDelete_in_cvp = True
750 except CvpApiError as error:
751 if "Entity does not exist" in error.msg:
752 continue
753 else:
754 raise error
755 # remove configLet from device
756 else:
757 res = self.__configlet_modify(cls_perSw[s])
758 allLeafConfigured[s] = res[0]
759 if not allLeafConfigured[s]:
760 continue
761 cl = cls_perSw[s]
762 res = self.__device_modify(
763 device_to_update=s,
764 new_configlets=cl,
765 delete=toDelete_in_cvp)
766 if "errorMessage" in str(res):
767 raise Exception(str(res))
768 self.logger.info("Device {} modify result {}".format(s, res))
769 for t_id in res[1]['tasks']:
770 if not toDelete_in_cvp:
771 note_msg = "{}{}{}{}##".format(self.__MANAGED_BY_OSM,
772 self.__SEPARATOR,
773 t_id,
774 self.__SEPARATOR)
775 self.client.api.add_note_to_configlet(
776 cls_perSw[s][0]['key'],
777 note_msg)
778 cls_perSw[s][0]['note'] = note_msg
779 tasks = {t_id: {'workOrderId': t_id}}
780 self.__exec_task(tasks, self.__EXC_TASK_EXEC_WAIT)
781 # with just one configLet assigned to a device,
782 # delete all if there are errors in next loops
783 if not toDelete_in_cvp:
784 allLeafModified[s] = True
785 if len(cl_toDelete) > 0:
786 self.__configlet_modify(cl_toDelete, delete=True)
787
788 return allLeafConfigured, allLeafModified
789 except Exception as ex:
790 try:
791 self.__rollbackConnection(cls_perSw,
792 allLeafConfigured,
793 allLeafModified)
794 except Exception as e:
795 self.logger.error("Exception rolling back in updating connection: {}".
796 format(e), exc_info=True)
797 raise ex
798
799 def __rollbackConnection(self,
800 cls_perSw,
801 allLeafConfigured,
802 allLeafModified):
803 """ Removes the given configLet from the devices and then remove the configLets
804 """
805 for s in self.switches:
806 if allLeafModified[s]:
807 try:
808 res = self.__device_modify(
809 device_to_update=s,
810 new_configlets=cls_perSw[s],
811 delete=True)
812 if "errorMessage" in str(res):
813 raise Exception(str(res))
814 tasks = dict()
815 for t_id in res[1]['tasks']:
816 tasks[t_id] = {'workOrderId': t_id}
817 self.__exec_task(tasks)
818 self.logger.info("Device {} modify result {}".format(s, res))
819 except Exception as e:
820 self.logger.error('Error removing configlets from device {}: {}'.format(s, e))
821 pass
822 for s in self.switches:
823 if allLeafConfigured[s]:
824 self.__configlet_modify(cls_perSw[s], delete=True)
825
826 def __exec_task(self, tasks, tout=10):
827 if self.taskC is None:
828 self.__connect()
829 data = self.taskC.update_all_tasks(tasks).values()
830 self.taskC.task_action(data, tout, 'executed')
831
832 def __device_modify(self, device_to_update, new_configlets, delete):
833 """ Updates the devices (switches) adding or removing the configLet,
834 the tasks Id's associated to the change are returned
835 """
836 self.logger.info('Enter in __device_modify delete: {}'.format(delete))
837 updated = []
838 changed = False
839 # Task Ids that have been identified during device actions
840 newTasks = []
841
842 if (len(new_configlets) == 0 or
843 device_to_update is None or
844 len(device_to_update) == 0):
845 data = {'updated': updated, 'tasks': newTasks}
846 return [changed, data]
847
848 self.__load_inventory()
849
850 allDeviceFacts = self.allDeviceFacts
851 # Work through Devices list adding device specific information
852 device = None
853 for try_device in allDeviceFacts:
854 # Add Device Specific Configlets
855 # self.logger.debug(device)
856 if try_device['hostname'] not in device_to_update:
857 continue
858 dev_cvp_configlets = self.client.api.get_configlets_by_device_id(
859 try_device['systemMacAddress'])
860 # self.logger.debug(dev_cvp_configlets)
861 try_device['deviceSpecificConfiglets'] = []
862 for cvp_configlet in dev_cvp_configlets:
863 if int(cvp_configlet['containerCount']) == 0:
864 try_device['deviceSpecificConfiglets'].append(
865 {'name': cvp_configlet['name'],
866 'key': cvp_configlet['key']})
867 # self.logger.debug(device)
868 device = try_device
869 break
870
871 # Check assigned configlets
872 device_update = False
873 add_configlets = []
874 remove_configlets = []
875 update_devices = []
876
877 if delete:
878 for cvp_configlet in device['deviceSpecificConfiglets']:
879 for cl in new_configlets:
880 if cvp_configlet['name'] == cl['name']:
881 remove_configlets.append(cvp_configlet)
882 device_update = True
883 else:
884 for configlet in new_configlets:
885 if configlet not in device['deviceSpecificConfiglets']:
886 add_configlets.append(configlet)
887 device_update = True
888 if device_update:
889 update_devices.append({'hostname': device['hostname'],
890 'configlets': [add_configlets,
891 remove_configlets],
892 'device': device})
893 self.logger.info("Device to modify: {}".format(update_devices))
894
895 up_device = update_devices[0]
896 cl_toAdd = up_device['configlets'][0]
897 cl_toDel = up_device['configlets'][1]
898 # Update Configlets
899 try:
900 if delete and len(cl_toDel) > 0:
901 r = self.client.api.remove_configlets_from_device(
902 'OSM',
903 up_device['device'],
904 cl_toDel,
905 create_task=True)
906 dev_action = r
907 self.logger.debug("remove_configlets_from_device {} {}".format(dev_action, cl_toDel))
908 elif len(cl_toAdd) > 0:
909 r = self.client.api.apply_configlets_to_device(
910 'OSM',
911 up_device['device'],
912 cl_toAdd,
913 create_task=True)
914 dev_action = r
915 self.logger.debug("apply_configlets_to_device {} {}".format(dev_action, cl_toAdd))
916
917 except Exception as error:
918 errorMessage = str(error)
919 msg = "errorMessage: Device {} Configlets couldnot be updated: {}".format(
920 up_device['hostname'], errorMessage)
921 raise SdnConnectorError(msg) from error
922 else:
923 if "errorMessage" in str(dev_action):
924 m = "Device {} Configlets update fail: {}".format(
925 up_device['name'], dev_action['errorMessage'])
926 raise SdnConnectorError(m)
927 else:
928 changed = True
929 if 'taskIds' in str(dev_action):
930 # Fix 1030 SDN-ARISTA Key error note when deploy a NS
931 if not dev_action['data']['taskIds']:
932 raise SdnConnectorError("No taskIds found: Device {} Configlets could not be updated".format(
933 up_device['hostname']))
934 for taskId in dev_action['data']['taskIds']:
935 updated.append({
936 up_device['hostname']: "Configlets-{}".format(taskId)})
937 newTasks.append(taskId)
938 else:
939 updated.append({up_device['hostname']:
940 "Configlets-No_Specific_Tasks"})
941 data = {'updated': updated, 'tasks': newTasks}
942 return [changed, data]
943
944 def __configlet_modify(self, configletsToApply, delete=False):
945 ''' adds/update or delete the provided configLets
946 :param configletsToApply: list of configLets to apply
947 :param delete: flag to indicate if the configLets have to be deleted
948 from Cloud Vision Portal
949 :return: data: dict of module actions and taskIDs
950 '''
951 self.logger.info('Enter in __configlet_modify delete:{}'.format(
952 delete))
953
954 # Compare configlets against cvp_facts-configlets
955 changed = False
956 checked = []
957 deleted = []
958 updated = []
959 new = []
960
961 for cl in configletsToApply:
962 found_in_cvp = False
963 to_delete = False
964 to_update = False
965 to_create = False
966 to_check = False
967 try:
968 cvp_cl = self.client.api.get_configlet_by_name(cl['name'])
969 cl['key'] = cvp_cl['key']
970 cl['note'] = cvp_cl['note']
971 found_in_cvp = True
972 except CvpApiError as error:
973 if "Entity does not exist" in error.msg:
974 pass
975 else:
976 raise error
977
978 if delete:
979 if found_in_cvp:
980 to_delete = True
981 configlet = {'name': cvp_cl['name'],
982 'data': cvp_cl}
983 else:
984 if found_in_cvp:
985 cl_compare = self.__compare(cl['config'],
986 cvp_cl['config'])
987 # compare function returns a floating point number
988 if cl_compare[0] != 100.0:
989 to_update = True
990 configlet = {'name': cl['name'],
991 'data': cvp_cl,
992 'config': cl['config']}
993 else:
994 to_check = True
995 configlet = {'name': cl['name'],
996 'key': cvp_cl['key'],
997 'data': cvp_cl,
998 'config': cl['config']}
999 else:
1000 to_create = True
1001 configlet = {'name': cl['name'],
1002 'config': cl['config']}
1003 try:
1004 if to_delete:
1005 operation = 'delete'
1006 resp = self.client.api.delete_configlet(
1007 configlet['data']['name'],
1008 configlet['data']['key'])
1009 elif to_update:
1010 operation = 'update'
1011 resp = self.client.api.update_configlet(
1012 configlet['config'],
1013 configlet['data']['key'],
1014 configlet['data']['name'],
1015 wait_task_ids=True)
1016 elif to_create:
1017 operation = 'create'
1018 resp = self.client.api.add_configlet(
1019 configlet['name'],
1020 configlet['config'])
1021 else:
1022 operation = 'checked'
1023 resp = 'checked'
1024 except Exception as error:
1025 errorMessage = str(error).split(':')[-1]
1026 message = "Configlet {} cannot be {}: {}".format(
1027 cl['name'], operation, errorMessage)
1028 if to_delete:
1029 deleted.append({configlet['name']: message})
1030 elif to_update:
1031 updated.append({configlet['name']: message})
1032 elif to_create:
1033 new.append({configlet['name']: message})
1034 elif to_check:
1035 checked.append({configlet['name']: message})
1036
1037 else:
1038 if "error" in str(resp).lower():
1039 message = "Configlet {} cannot be deleted: {}".format(
1040 cl['name'], resp['errorMessage'])
1041 if to_delete:
1042 deleted.append({configlet['name']: message})
1043 elif to_update:
1044 updated.append({configlet['name']: message})
1045 elif to_create:
1046 new.append({configlet['name']: message})
1047 elif to_check:
1048 checked.append({configlet['name']: message})
1049 else:
1050 if to_delete:
1051 changed = True
1052 deleted.append({configlet['name']: "success"})
1053 elif to_update:
1054 changed = True
1055 updated.append({configlet['name']: "success"})
1056 elif to_create:
1057 changed = True
1058 cl['key'] = resp # This key is used in API call deviceApplyConfigLet FGA
1059 new.append({configlet['name']: "success"})
1060 elif to_check:
1061 changed = False
1062 checked.append({configlet['name']: "success"})
1063
1064 data = {'new': new, 'updated': updated, 'deleted': deleted, 'checked': checked}
1065 return [changed, data]
1066
1067 def __get_configletsDevices(self, configlets):
1068 for s in self.switches:
1069 configlet = configlets[s]
1070 # Add applied Devices
1071 if len(configlet) > 0:
1072 configlet['devices'] = []
1073 applied_devices = self.client.api.get_applied_devices(
1074 configlet['name'])
1075 for device in applied_devices['data']:
1076 configlet['devices'].append(device['hostName'])
1077
1078 def __get_serviceData(self, service_uuid, service_type, vlan_id, conn_info=None):
1079 cls_perSw = {}
1080 for s in self.switches:
1081 cls_perSw[s] = []
1082 if not conn_info:
1083 srv_cls = self.__get_serviceConfigLets(service_uuid,
1084 service_type,
1085 vlan_id)
1086 self.__get_configletsDevices(srv_cls)
1087 for s in self.switches:
1088 cl = srv_cls[s]
1089 if len(cl) > 0:
1090 for dev in cl['devices']:
1091 cls_perSw[dev].append(cl)
1092 else:
1093 cls_perSw = conn_info['configLetPerSwitch']
1094 return cls_perSw
1095
1096 def delete_connectivity_service(self, service_uuid, conn_info=None):
1097 """
1098 Disconnect multi-site endpoints previously connected
1099
1100 :param service_uuid: The one returned by create_connectivity_service
1101 :param conn_info: The one returned by last call to 'create_connectivity_service' or 'edit_connectivity_service'
1102 if they do not return None
1103 :return: None
1104 :raises: SdnConnectorException: In case of error. The parameter http_code must be filled
1105 """
1106 try:
1107 self.logger.debug('invoked delete_connectivity_service {}'.
1108 format(service_uuid))
1109 if not service_uuid:
1110 raise SdnConnectorError(message='No connection service UUID',
1111 http_code=500)
1112
1113 self.__get_Connection()
1114 if conn_info is None:
1115 raise SdnConnectorError(message='No connection information for service UUID {}'.format(service_uuid),
1116 http_code=500)
1117 c_info = None
1118 cls_perSw = self.__get_serviceData(service_uuid,
1119 conn_info['service_type'],
1120 conn_info['vlan_id'],
1121 c_info)
1122 allLeafConfigured = {}
1123 allLeafModified = {}
1124 for s in self.switches:
1125 allLeafConfigured[s] = True
1126 allLeafModified[s] = True
1127 found_in_cvp = False
1128 for s in self.switches:
1129 if cls_perSw[s]:
1130 found_in_cvp = True
1131 if found_in_cvp:
1132 self.__rollbackConnection(cls_perSw,
1133 allLeafConfigured,
1134 allLeafModified)
1135 else:
1136 # if the service is not defined in Cloud Vision, return a 404 - NotFound error
1137 raise SdnConnectorError(message='Service {} was not found in Arista Cloud Vision {}'.
1138 format(service_uuid, self.__wim_url),
1139 http_code=404)
1140 self.__removeMetadata(service_uuid)
1141 except CvpLoginError as e:
1142 self.logger.info(str(e))
1143 self.client = None
1144 raise SdnConnectorError(message=SdnError.UNAUTHORIZED,
1145 http_code=401) from e
1146 except SdnConnectorError as sde:
1147 raise sde
1148 except Exception as ex:
1149 self.client = None
1150 self.logger.error(ex)
1151 if self.raiseException:
1152 raise ex
1153 raise SdnConnectorError(message=SdnError.INTERNAL_ERROR,
1154 http_code=500) from ex
1155
1156 def __addMetadata(self, service_uuid, service_type, vlan_id):
1157 """ Adds the connectivity service from 'OSM_metadata' configLet
1158 """
1159 found_in_cvp = False
1160 try:
1161 cvp_cl = self.client.api.get_configlet_by_name(self.__OSM_METADATA)
1162 found_in_cvp = True
1163 except CvpApiError as error:
1164 if "Entity does not exist" in error.msg:
1165 pass
1166 else:
1167 raise error
1168 try:
1169 new_serv = '{} {} {} {}\n'.format(self.__METADATA_PREFIX, service_type, vlan_id, service_uuid)
1170
1171 if found_in_cvp:
1172 cl_config = cvp_cl['config'] + new_serv
1173 else:
1174 cl_config = new_serv
1175 cl_meta = [{'name': self.__OSM_METADATA, 'config': cl_config}]
1176 self.__configlet_modify(cl_meta)
1177 except Exception as e:
1178 self.logger.error('Error in setting metadata in CloudVision from OSM for service {}: {}'.
1179 format(service_uuid, str(e)))
1180 pass
1181
1182 def __removeMetadata(self, service_uuid):
1183 """ Removes the connectivity service from 'OSM_metadata' configLet
1184 """
1185 found_in_cvp = False
1186 try:
1187 cvp_cl = self.client.api.get_configlet_by_name(self.__OSM_METADATA)
1188 found_in_cvp = True
1189 except CvpApiError as error:
1190 if "Entity does not exist" in error.msg:
1191 pass
1192 else:
1193 raise error
1194 try:
1195 if found_in_cvp:
1196 if service_uuid in cvp_cl['config']:
1197 cl_config = ''
1198 for line in cvp_cl['config'].split('\n'):
1199 if service_uuid in line:
1200 continue
1201 else:
1202 cl_config = cl_config + line
1203 cl_meta = [{'name': self.__OSM_METADATA, 'config': cl_config}]
1204 self.__configlet_modify(cl_meta)
1205 except Exception as e:
1206 self.logger.error('Error in removing metadata in CloudVision from OSM for service {}: {}'.
1207 format(service_uuid, str(e)))
1208 pass
1209
1210 def edit_connectivity_service(self,
1211 service_uuid,
1212 conn_info=None,
1213 connection_points=None,
1214 **kwargs):
1215 """ Change an existing connectivity service.
1216
1217 This method's arguments and return value follow the same convention as
1218 :meth:`~.create_connectivity_service`.
1219
1220 :param service_uuid: UUID of the connectivity service.
1221 :param conn_info: (dict or None): Information previously returned
1222 by last call to create_connectivity_service
1223 or edit_connectivity_service
1224 :param connection_points: (list): If provided, the old list of
1225 connection points will be replaced.
1226 :param kwargs: Same meaning that create_connectivity_service
1227 :return: dict or None: Information to be updated and stored at
1228 the database.
1229 When ``None`` is returned, no information should be changed.
1230 When an empty dict is returned, the database record will
1231 be deleted.
1232 **MUST** be JSON/YAML-serializable (plain data structures).
1233 Raises:
1234 SdnConnectorError: In case of error.
1235 """
1236 try:
1237 self.logger.debug('invoked edit_connectivity_service for service {}. ports: {}'.format(service_uuid,
1238 connection_points))
1239
1240 if not service_uuid:
1241 raise SdnConnectorError(message='Unable to perform operation, missing or empty uuid',
1242 http_code=500)
1243 if not conn_info:
1244 raise SdnConnectorError(message='Unable to perform operation, missing or empty connection information',
1245 http_code=500)
1246
1247 if connection_points is None:
1248 return None
1249
1250 self.__get_Connection()
1251
1252 cls_currentPerSw = conn_info['configLetPerSwitch']
1253 service_type = conn_info['service_type']
1254
1255 self.__check_service(service_type,
1256 connection_points,
1257 check_vlan=False,
1258 check_num_cp=False,
1259 kwargs=kwargs)
1260
1261 s_uid, s_connInf = self.__processConnection(
1262 service_uuid,
1263 service_type,
1264 connection_points,
1265 kwargs)
1266 self.logger.info("Service with uuid {} configuration updated".
1267 format(s_uid))
1268 return s_connInf
1269 except CvpLoginError as e:
1270 self.logger.info(str(e))
1271 self.client = None
1272 raise SdnConnectorError(message=SdnError.UNAUTHORIZED,
1273 http_code=401) from e
1274 except SdnConnectorError as sde:
1275 raise sde
1276 except Exception as ex:
1277 try:
1278 # Add previous
1279 # TODO check if there are pending task, and cancel them before restoring
1280 self.__updateConnection(cls_currentPerSw)
1281 except Exception as e:
1282 self.logger.error("Unable to restore configuration in service {} after an error in the configuration"
1283 " updated: {}".format(service_uuid, str(e)))
1284 if self.raiseException:
1285 raise ex
1286 raise SdnConnectorError(message=str(ex),
1287 http_code=500) from ex
1288
1289 def clear_all_connectivity_services(self):
1290 """ Removes all connectivity services from Arista CloudVision with two steps:
1291 - retrives all the services from Arista CloudVision
1292 - removes each service
1293 """
1294 try:
1295 self.logger.debug('invoked AristaImpl ' +
1296 'clear_all_connectivity_services')
1297 self.__get_Connection()
1298 s_list = self.__get_srvUUIDs()
1299 for serv in s_list:
1300 conn_info = {}
1301 conn_info['service_type'] = serv['type']
1302 conn_info['vlan_id'] = serv['vlan']
1303
1304 self.delete_connectivity_service(serv['uuid'], conn_info)
1305 except CvpLoginError as e:
1306 self.logger.info(str(e))
1307 self.client = None
1308 raise SdnConnectorError(message=SdnError.UNAUTHORIZED,
1309 http_code=401) from e
1310 except SdnConnectorError as sde:
1311 raise sde
1312 except Exception as ex:
1313 self.client = None
1314 self.logger.error(ex)
1315 if self.raiseException:
1316 raise ex
1317 raise SdnConnectorError(message=SdnError.INTERNAL_ERROR,
1318 http_code=500) from ex
1319
1320 def get_all_active_connectivity_services(self):
1321 """ Return the uuid of all the active connectivity services with two steps:
1322 - retrives all the services from Arista CloudVision
1323 - retrives the status of each server
1324 """
1325 try:
1326 self.logger.debug('invoked AristaImpl {}'.format(
1327 'get_all_active_connectivity_services'))
1328 self.__get_Connection()
1329 s_list = self.__get_srvUUIDs()
1330 result = []
1331 for serv in s_list:
1332 conn_info = {}
1333 conn_info['service_type'] = serv['type']
1334 conn_info['vlan_id'] = serv['vlan']
1335
1336 status = self.get_connectivity_service_status(serv['uuid'], conn_info)
1337 if status['sdn_status'] == 'ACTIVE':
1338 result.append(serv['uuid'])
1339 return result
1340 except CvpLoginError as e:
1341 self.logger.info(str(e))
1342 self.client = None
1343 raise SdnConnectorError(message=SdnError.UNAUTHORIZED,
1344 http_code=401) from e
1345 except SdnConnectorError as sde:
1346 raise sde
1347 except Exception as ex:
1348 self.client = None
1349 self.logger.error(ex)
1350 if self.raiseException:
1351 raise ex
1352 raise SdnConnectorError(message=SdnError.INTERNAL_ERROR,
1353 http_code=500) from ex
1354
1355 def __get_serviceConfigLets(self, service_uuid, service_type, vlan_id):
1356 """ Return the configLet's associated with a connectivity service,
1357 There should be one, as maximum, per device (switch) for a given
1358 connectivity service
1359 """
1360 srv_cls = {}
1361 for s in self.switches:
1362 srv_cls[s] = []
1363 found_in_cvp = False
1364 name = (self.__OSM_PREFIX +
1365 s +
1366 self.__SEPARATOR + service_type + str(vlan_id) +
1367 self.__SEPARATOR + service_uuid)
1368 try:
1369 cvp_cl = self.client.api.get_configlet_by_name(name)
1370 found_in_cvp = True
1371 except CvpApiError as error:
1372 if "Entity does not exist" in error.msg:
1373 pass
1374 else:
1375 raise error
1376 if found_in_cvp:
1377 srv_cls[s] = cvp_cl
1378 return srv_cls
1379
1380 def __get_srvVLANs(self):
1381 """ Returns a list with all the VLAN id's used in the connectivity services managed
1382 in tha Arista CloudVision by checking the 'OSM_metadata' configLet where this
1383 information is stored
1384 """
1385 found_in_cvp = False
1386 try:
1387 cvp_cl = self.client.api.get_configlet_by_name(self.__OSM_METADATA)
1388 found_in_cvp = True
1389 except CvpApiError as error:
1390 if "Entity does not exist" in error.msg:
1391 pass
1392 else:
1393 raise error
1394 s_vlan_list = []
1395 if found_in_cvp:
1396 lines = cvp_cl['config'].split('\n')
1397 for line in lines:
1398 if self.__METADATA_PREFIX in line:
1399 s_vlan = line.split(' ')[3]
1400 else:
1401 continue
1402 if (s_vlan is not None and
1403 len(s_vlan) > 0 and
1404 s_vlan not in s_vlan_list):
1405 s_vlan_list.append(s_vlan)
1406
1407 return s_vlan_list
1408
1409 def __get_srvUUIDs(self):
1410 """ Retrieves all the connectivity services, managed in tha Arista CloudVision
1411 by checking the 'OSM_metadata' configLet where this information is stored
1412 """
1413 found_in_cvp = False
1414 try:
1415 cvp_cl = self.client.api.get_configlet_by_name(self.__OSM_METADATA)
1416 found_in_cvp = True
1417 except CvpApiError as error:
1418 if "Entity does not exist" in error.msg:
1419 pass
1420 else:
1421 raise error
1422 serv_list = []
1423 if found_in_cvp:
1424 lines = cvp_cl['config'].split('\n')
1425 for line in lines:
1426 if self.__METADATA_PREFIX in line:
1427 line = line.split(' ')
1428 serv = {'uuid': line[4], 'type': line[2], 'vlan': line[3]}
1429 else:
1430 continue
1431 if (serv is not None and
1432 len(serv) > 0 and
1433 serv not in serv_list):
1434 serv_list.append(serv)
1435
1436 return serv_list
1437
1438 def __get_Connection(self):
1439 """ Open a connection with Arista CloudVision,
1440 invoking the version retrival as test
1441 """
1442 try:
1443 if self.client is None:
1444 self.client = self.__connect()
1445 self.client.api.get_cvp_info()
1446 except (CvpSessionLogOutError, RequestException) as e:
1447 self.logger.debug("Connection error '{}'. Reconnecting".format(e))
1448 self.client = self.__connect()
1449 self.client.api.get_cvp_info()
1450
1451 def __connect(self):
1452 ''' Connects to CVP device using user provided credentials from initialization.
1453 :return: CvpClient object with connection instantiated.
1454 '''
1455 client = CvpClient()
1456 protocol, _, rest_url = self.__wim_url.rpartition("://")
1457 host, _, port = rest_url.partition(":")
1458 if port and port.endswith("/"):
1459 port = int(port[:-1])
1460 elif port:
1461 port = int(port)
1462 else:
1463 port = 443
1464
1465 client.connect([host],
1466 self.__user,
1467 self.__passwd,
1468 protocol=protocol or "https",
1469 port=port,
1470 connect_timeout=2)
1471 client.api = CvpApi(client, request_timeout=self.__API_REQUEST_TOUT)
1472 self.taskC = AristaCVPTask(client.api)
1473 return client
1474
1475 def __compare(self, fromText, toText, lines=10):
1476 """ Compare text string in 'fromText' with 'toText' and produce
1477 diffRatio - a score as a float in the range [0, 1] 2.0*M / T
1478 T is the total number of elements in both sequences,
1479 M is the number of matches.
1480 Score - 1.0 if the sequences are identical, and
1481 0.0 if they have nothing in common.
1482 unified diff list
1483 Code Meaning
1484 '- ' line unique to sequence 1
1485 '+ ' line unique to sequence 2
1486 ' ' line common to both sequences
1487 '? ' line not present in either input sequence
1488 """
1489 fromlines = fromText.splitlines(1)
1490 tolines = toText.splitlines(1)
1491 diff = list(difflib.unified_diff(fromlines, tolines, n=lines))
1492 textComp = difflib.SequenceMatcher(None, fromText, toText)
1493 diffRatio = round(textComp.quick_ratio()*100, 2)
1494 return [diffRatio, diff]
1495
1496 def __load_inventory(self):
1497 """ Get Inventory Data for All Devices (aka switches) from the Arista CloudVision
1498 """
1499 if not self.cvp_inventory:
1500 self.cvp_inventory = self.client.api.get_inventory()
1501 self.allDeviceFacts = []
1502 for device in self.cvp_inventory:
1503 self.allDeviceFacts.append(device)
1504
1505 def __get_tags(self, name, value):
1506 if not self.cvp_tags:
1507 self.cvp_tags = []
1508 url = '/api/v1/rest/analytics/tags/labels/devices/{}/value/{}/elements'.format(name, value)
1509 self.logger.debug('get_tags: URL {}'.format(url))
1510 data = self.client.get(url, timeout=self.__API_REQUEST_TOUT)
1511 for dev in data['notifications']:
1512 for elem in dev['updates']:
1513 self.cvp_tags.append(elem)
1514 self.logger.debug('Available devices with tag_name {} - value {}: {} '.format(name, value, self.cvp_tags))
1515
1516 def __get_interface_ip(self, device_id, interface):
1517 url = '/api/v1/rest/{}/Sysdb/ip/config/ipIntfConfig/{}/'.format(device_id, interface)
1518 self.logger.debug('get_interface_ip: URL {}'.format(url))
1519 try:
1520 data = self.client.get(url, timeout=self.__API_REQUEST_TOUT)
1521 return data['notifications'][0]['updates']['addrWithMask']['value'].split('/')[0]
1522 except Exception:
1523 raise SdnConnectorError("Invalid response from url {}: data {}".format(url, data))
1524
1525 def __get_device_ASN(self, device_id):
1526 url = '/api/v1/rest/{}/Sysdb/routing/bgp/config/'.format(device_id)
1527 self.logger.debug('get_device_ASN: URL {}'.format(url))
1528 try:
1529 data = self.client.get(url, timeout=self.__API_REQUEST_TOUT)
1530 return data['notifications'][0]['updates']['asNumber']['value']['value']['int']
1531 except Exception:
1532 raise SdnConnectorError("Invalid response from url {}: data {}".format(url, data))
1533
1534 def __get_peer_MLAG(self, device_id):
1535 peer = None
1536 url = '/api/v1/rest/{}/Sysdb/mlag/status/'.format(device_id)
1537 self.logger.debug('get_MLAG_status: URL {}'.format(url))
1538 try:
1539 data = self.client.get(url, timeout=self.__API_REQUEST_TOUT)
1540 if data['notifications']:
1541 found = False
1542 for notification in data['notifications']:
1543 for update in notification['updates']:
1544 if update == 'systemId':
1545 mlagSystemId = notification['updates'][update]['value']
1546 found = True
1547 break
1548 if found:
1549 break
1550 # search the MLAG System Id
1551 if found:
1552 for s in self.switches:
1553 if self.switches[s]['serialNumber'] == device_id:
1554 continue
1555 url = '/api/v1/rest/{}/Sysdb/mlag/status/'.format(self.switches[s]['serialNumber'])
1556 self.logger.debug('Searching for MLAG system id {} in switch {}'.format(mlagSystemId, s))
1557 data = self.client.get(url, timeout=self.__API_REQUEST_TOUT)
1558 found = False
1559 for notification in data['notifications']:
1560 for update in notification['updates']:
1561 if update == 'systemId':
1562 if mlagSystemId == notification['updates'][update]['value']:
1563 peer = s
1564 found = True
1565 break
1566 if found:
1567 break
1568 if found:
1569 break
1570 if peer is None:
1571 self.logger.error('No Peer device found for device {} with MLAG address {}'.format(device_id,
1572 mlagSystemId))
1573 else:
1574 self.logger.debug('Peer MLAG for device {} - value {}'.format(device_id, peer))
1575 return peer
1576 except Exception:
1577 raise SdnConnectorError("Invalid response from url {}: data {}".format(url, data))
1578
1579 def is_valid_destination(self, url):
1580 """ Check that the provided WIM URL is correct
1581 """
1582 if re.match(self.__regex, url):
1583 return True
1584 elif self.is_valid_ipv4_address(url):
1585 return True
1586 else:
1587 return self.is_valid_ipv6_address(url)
1588
1589 def is_valid_ipv4_address(self, address):
1590 """ Checks that the given IP is IPv4 valid
1591 """
1592 try:
1593 socket.inet_pton(socket.AF_INET, address)
1594 except AttributeError: # no inet_pton here, sorry
1595 try:
1596 socket.inet_aton(address)
1597 except socket.error:
1598 return False
1599 return address.count('.') == 3
1600 except socket.error: # not a valid address
1601 return False
1602 return True
1603
1604 def is_valid_ipv6_address(self, address):
1605 """ Checks that the given IP is IPv6 valid
1606 """
1607 try:
1608 socket.inet_pton(socket.AF_INET6, address)
1609 except socket.error: # not a valid address
1610 return False
1611 return True
1612
1613 def delete_keys_from_dict(self, dict_del, lst_keys):
1614 if dict_del is None:
1615 return dict_del
1616 dict_copy = {k: v for k, v in dict_del.items() if k not in lst_keys}
1617 for k, v in dict_copy.items():
1618 if isinstance(v, dict):
1619 dict_copy[k] = self.delete_keys_from_dict(v, lst_keys)
1620 return dict_copy