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