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