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