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