Corrected bug: If vlan provided is None raise an error
[osm/RO.git] / RO-SDN-juniper_contrail / osm_rosdn_juniper_contrail / sdn_assist_juniper_contrail.py
1 # -*- coding: utf-8 -*-
2
3 # Copyright 2020 ETSI OSM
4 #
5 # All Rights Reserved.
6 #
7 # Licensed under the Apache License, Version 2.0 (the "License"); you may
8 # not use this file except in compliance with the License. You may obtain
9 # a copy of the License at
10 #
11 # http://www.apache.org/licenses/LICENSE-2.0
12 #
13 # Unless required by applicable law or agreed to in writing, software
14 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
16 # License for the specific language governing permissions and limitations
17 # under the License.
18 #
19
20 import logging
21 import json
22 import yaml
23 import random
24
25 from osm_ro.wim.sdnconn import SdnConnectorBase, SdnConnectorError
26 from osm_rosdn_juniper_contrail.rest_lib import ContrailHttp
27 from osm_rosdn_juniper_contrail.rest_lib import NotFound
28 from osm_rosdn_juniper_contrail.rest_lib import DuplicateFound
29 from osm_rosdn_juniper_contrail.rest_lib import HttpException
30
31 from osm_rosdn_juniper_contrail.sdn_api import UnderlayApi
32
33
34 class JuniperContrail(SdnConnectorBase):
35 """
36 Juniper Contrail SDN plugin. The plugin interacts with Juniper Contrail Controller,
37 whose API details can be found in these links:
38
39 - https://github.com/tonyliu0592/contrail/wiki/API-Configuration-REST
40 - https://www.juniper.net/documentation/en_US/contrail19/information-products/pathway-pages/api-guide-1910/tutorial_with_rest.html
41 - https://github.com/tonyliu0592/contrail-toolbox/blob/master/sriov/sriov
42 """
43 _WIM_LOGGER = "openmano.sdnconn.junipercontrail"
44
45 def __init__(self, wim, wim_account, config=None, logger=None):
46 """
47
48 :param wim: (dict). Contains among others 'wim_url'
49 :param wim_account: (dict). Contains among others 'uuid' (internal id), 'name',
50 'sdn' (True if is intended for SDN-assist or False if intended for WIM), 'user', 'password'.
51 :param config: (dict or None): Particular information of plugin. These keys if present have a common meaning:
52 'mapping_not_needed': (bool) False by default or if missing, indicates that mapping is not needed.
53 'service_endpoint_mapping': (list) provides the internal endpoint mapping. The meaning is:
54 KEY meaning for WIM meaning for SDN assist
55 -------- -------- --------
56 device_id pop_switch_dpid compute_id
57 device_interface_id pop_switch_port compute_pci_address
58 service_endpoint_id wan_service_endpoint_id SDN_service_endpoint_id
59 service_mapping_info wan_service_mapping_info SDN_service_mapping_info
60 contains extra information if needed. Text in Yaml format
61 switch_dpid wan_switch_dpid SDN_switch_dpid
62 switch_port wan_switch_port SDN_switch_port
63 datacenter_id vim_account vim_account
64 id: (internal, do not use)
65 wim_id: (internal, do not use)
66 :param logger (logging.Logger): optional logger object. If none is passed 'openmano.sdn.sdnconn' is used.
67 """
68 self.logger = logger or logging.getLogger(self._WIM_LOGGER)
69 self.logger.debug('wim: {}, wim_account: {}, config: {}'.format(wim, wim_account, config))
70 super().__init__(wim, wim_account, config, logger)
71
72 self.user = wim_account.get("user")
73 self.password = wim_account.get("password")
74
75 url = wim.get("wim_url") # underlay url
76 auth_url = None
77 self.project = None
78 self.domain = None
79 self.asn = None
80 self.fabric = None
81 overlay_url = None
82 self.vni_range = None
83 if config:
84 auth_url = config.get("auth_url")
85 self.project = config.get("project")
86 self.domain = config.get("domain")
87 self.asn = config.get("asn")
88 self.fabric = config.get("fabric")
89 self.overlay_url = config.get("overlay_url")
90 self.vni_range = config.get("vni_range")
91
92 if not url:
93 raise SdnConnectorError("'url' must be provided")
94 if not url.startswith("http"):
95 url = "http://" + url
96 if not url.endswith("/"):
97 url = url + "/"
98 self.url = url
99
100 if not self.vni_range:
101 self.vni_range = ['1000001-2000000']
102 self.logger.info("No vni_range was provided. Using ['1000001-2000000']")
103 self.used_vni = set()
104
105 if auth_url:
106 if not auth_url.startswith("http"):
107 auth_url = "http://" + auth_url
108 if not auth_url.endswith("/"):
109 auth_url = auth_url + "/"
110 self.auth_url = auth_url
111
112 if overlay_url:
113 if not overlay_url.startswith("http"):
114 overlay_url = "http://" + overlay_url
115 if not overlay_url.endswith("/"):
116 overlay_url = overlay_url + "/"
117 self.overlay_url = overlay_url
118
119 if not self.project:
120 raise SdnConnectorError("'project' must be provided")
121 if not self.asn:
122 # TODO: Get ASN from controller config; otherwise raise ERROR for the moment
123 raise SdnConnectorError("'asn' was not provided and it was not possible to obtain it")
124 if not self.fabric:
125 # TODO: Get FABRIC from controller config; otherwise raise ERROR for the moment
126 raise SdnConnectorError("'fabric' was not provided and was not possible to obtain it")
127 if not self.domain:
128 self.domain = 'default-domain'
129 self.logger.info("No domain was provided. Using 'default-domain'")
130
131 underlay_api_config = {
132 "auth_url": self.auth_url,
133 "project": self.project,
134 "domain": self.domain,
135 "asn": self.asn,
136 "fabric": self.fabric
137 }
138 self.underlay_api = UnderlayApi(url, underlay_api_config, user=self.user, password=self.password, logger=logger)
139
140 self._max_duplicate_retry = 2
141 self.logger.info("Juniper Contrail Connector Initialized.")
142
143 def _generate_vni(self):
144 """
145 Method to get unused VxLAN Network Identifier (VNI)
146 Args:
147 None
148 Returns:
149 VNI
150 """
151 #find unused VLAN ID
152 for vlanID_range in self.vni_range:
153 try:
154 start_vni , end_vni = map(int, vlanID_range.replace(" ", "").split("-"))
155 for i in range(start_vni, end_vni + 1):
156 vni = random.randrange(start_vni, end_vni, 1)
157 if vni not in self.used_vni:
158 return vni
159 except Exception as exp:
160 raise SdnConnectorError("Exception {} occurred while searching a free VNI.".format(exp))
161 else:
162 raise SdnConnectorError("Unable to create the virtual network."\
163 " All VNI in VNI range {} are in use.".format(self.vni_range))
164
165
166 # Aux functions for testing
167 def get_url(self):
168 return self.url
169
170 def get_overlay_url(self):
171 return self.overlay_url
172
173 def _create_port(self, switch_id, switch_port, network, vlan):
174 """
175 1 - Look for virtual port groups for provided switch_id, switch_port using name
176 2 - It the virtual port group does not exist, create it
177 3 - Create virtual machine interface for the indicated network and vlan
178 """
179 self.logger.debug("create_port: switch_id: {}, switch_port: {}, network: {}, vlan: {}".format(
180 switch_id, switch_port, network, vlan))
181
182 # 1 - Check if the vpg exists
183 vpg_name = self.underlay_api.get_vpg_name(switch_id, switch_port)
184 vpg = self.underlay_api.get_vpg_by_name(vpg_name)
185 if not vpg:
186 # 2 - If it does not exist create it
187 vpg_id, _ = self.underlay_api.create_vpg(switch_id, switch_port)
188 else:
189 # Assign vpg_id from vpg
190 vpg_id = vpg.get("uuid")
191
192 # 3 - Check if the vmi alreaady exists
193 vmi_id, _ = self.underlay_api.create_vmi(switch_id, switch_port, network, vlan)
194 self.logger.debug("port created")
195
196 return vpg_id, vmi_id
197
198 def _delete_port(self, switch_id, switch_port, vlan):
199 self.logger.debug("delete port, switch_id: {}, switch_port: {}, vlan: {}".format(switch_id, switch_port, vlan))
200
201 vpg_name = self.underlay_api.get_vpg_name(switch_id, switch_port)
202 vmi_name = self.underlay_api.get_vmi_name(switch_id, switch_port, vlan)
203
204 # 1 - Obtain vpg by id (if not vpg_id must have been error creating ig, nothing to be done)
205 vpg_fqdn = ["default-global-system-config", self.fabric, vpg_name]
206 vpg = self.underlay_api.get_by_fq_name("virtual-port-group", vpg_fqdn)
207 if not vpg:
208 self.logger.warning("vpg: {} to be deleted not found".format(vpg_name))
209 else:
210 # 2 - Get vmi interfaces from vpg
211 vmi_list = vpg.get("virtual_machine_interface_refs")
212 if not vmi_list:
213 # must have been an error during port creation when vmi is created
214 # may happen if there has been an error during creation
215 self.logger.warning("vpg: {} has not vmi, will delete nothing".format(vpg))
216 else:
217 num_vmis = len(vmi_list)
218 for vmi in vmi_list:
219 uuid = vmi.get("uuid")
220 fqdn = vmi.get("to")
221 # check by name
222 if fqdn[2] == vmi_name:
223 self.underlay_api.unref_vmi_vpg(vpg.get("uuid"), vmi.get("uuid"), fqdn)
224 self.underlay_api.delete_vmi(vmi.get("uuid"))
225 num_vmis = num_vmis - 1
226
227 # 3 - If there are no more vmi delete the vpg
228 if not vmi_list or num_vmis == 0:
229 self.underlay_api.delete_vpg(vpg.get("uuid"))
230
231 def check_credentials(self):
232 """Check if the connector itself can access the SDN/WIM with the provided url (wim.wim_url),
233 user (wim_account.user), and password (wim_account.password)
234
235 Raises:
236 SdnConnectorError: Issues regarding authorization, access to
237 external URLs, etc are detected.
238 """
239 self.logger.debug("")
240 try:
241 resp = self.underlay_api.check_auth()
242 if not resp:
243 raise SdnConnectorError('Empty response')
244 except Exception as e:
245 self.logger.error('Error checking credentials')
246 raise SdnConnectorError('Error checking credentials: {}'.format(str(e)))
247
248 def get_connectivity_service_status(self, service_uuid, conn_info=None):
249 """Monitor the status of the connectivity service established
250
251 Arguments:
252 service_uuid (str): UUID of the connectivity service
253 conn_info (dict or None): Information returned by the connector
254 during the service creation/edition and subsequently stored in
255 the database.
256
257 Returns:
258 dict: JSON/YAML-serializable dict that contains a mandatory key
259 ``sdn_status`` associated with one of the following values::
260
261 {'sdn_status': 'ACTIVE'}
262 # The service is up and running.
263
264 {'sdn_status': 'INACTIVE'}
265 # The service was created, but the connector
266 # cannot determine yet if connectivity exists
267 # (ideally, the caller needs to wait and check again).
268
269 {'sdn_status': 'DOWN'}
270 # Connection was previously established,
271 # but an error/failure was detected.
272
273 {'sdn_status': 'ERROR'}
274 # An error occurred when trying to create the service/
275 # establish the connectivity.
276
277 {'sdn_status': 'BUILD'}
278 # Still trying to create the service, the caller
279 # needs to wait and check again.
280
281 Additionally ``error_msg``(**str**) and ``sdn_info``(**dict**)
282 keys can be used to provide additional status explanation or
283 new information available for the connectivity service.
284 """
285 self.logger.debug("")
286 try:
287 resp = self.underlay_api.get_virtual_network(service_uuid)
288 if not resp:
289 raise SdnConnectorError('Empty response')
290 if resp:
291 vnet_info = resp
292
293 # Check if conn_info reports error
294 if conn_info.get("sdn_status") == "ERROR":
295 return {'sdn_status': 'ERROR', 'sdn_info': conn_info}
296 else:
297 return {'sdn_status': 'ACTIVE', 'sdn_info': vnet_info}
298 else:
299 return {'sdn_status': 'ERROR', 'sdn_info': 'not found'}
300 except SdnConnectorError:
301 raise
302 except HttpException as e:
303 self.logger.error("Error getting connectivity service: {}".format(e))
304 raise SdnConnectorError("Exception deleting connectivity service: {}".format(str(e)))
305 except Exception as e:
306 self.logger.error('Exception getting connectivity service info: %s', e, exc_info=True)
307 return {'sdn_status': 'ERROR', 'error_msg': str(e)}
308
309
310 def create_connectivity_service(self, service_type, connection_points, **kwargs):
311 """
312 Establish SDN/WAN connectivity between the endpoints
313 :param service_type: (str): ``ELINE`` (L2), ``ELAN`` (L2), ``ETREE`` (L2), ``L3``.
314 :param connection_points: (list): each point corresponds to
315 an entry point to be connected. For WIM: from the DC to the transport network.
316 For SDN: Compute/PCI to the transport network. One
317 connection point serves to identify the specific access and
318 some other service parameters, such as encapsulation type.
319 Each item of the list is a dict with:
320 "service_endpoint_id": (str)(uuid) Same meaning that for 'service_endpoint_mapping' (see __init__)
321 In case the config attribute mapping_not_needed is True, this value is not relevant. In this case
322 it will contain the string "device_id:device_interface_id"
323 "service_endpoint_encapsulation_type": None, "dot1q", ...
324 "service_endpoint_encapsulation_info": (dict) with:
325 "vlan": ..., (int, present if encapsulation is dot1q)
326 "vni": ... (int, present if encapsulation is vxlan),
327 "peers": [(ipv4_1), (ipv4_2)] (present if encapsulation is vxlan)
328 "mac": ...
329 "device_id": ..., same meaning that for 'service_endpoint_mapping' (see __init__)
330 "device_interface_id": same meaning that for 'service_endpoint_mapping' (see __init__)
331 "switch_dpid": ..., present if mapping has been found for this device_id,device_interface_id
332 "switch_port": ... present if mapping has been found for this device_id,device_interface_id
333 "service_mapping_info": present if mapping has been found for this device_id,device_interface_id
334 :param kwargs: For future versions:
335 bandwidth (int): value in kilobytes
336 latency (int): value in milliseconds
337 Other QoS might be passed as keyword arguments.
338 :return: tuple: ``(service_id, conn_info)`` containing:
339 - *service_uuid* (str): UUID of the established connectivity service
340 - *conn_info* (dict or None): Information to be stored at the database (or ``None``).
341 This information will be provided to the :meth:`~.edit_connectivity_service` and :obj:`~.delete`.
342 **MUST** be JSON/YAML-serializable (plain data structures).
343 :raises: SdnConnectorException: In case of error. Nothing should be created in this case.
344 Provide the parameter http_code
345 """
346 # Step 1. Check in the overlay controller the virtual network created by the VIM
347 # Best option: get network id of the VIM as param (if the VIM already created the network),
348 # and do a request to the controller of the virtual networks whose VIM network id is the provided
349 # Next best option: obtain the network by doing a request to the controller
350 # of the virtual networks using the VLAN ID of any service endpoint.
351 # 1.1 Read VLAN ID from a service endpoint
352 # 1.2 Look for virtual networks with "Provider Network" including a VLAN ID.
353 # 1.3 If more than one, ERROR
354 # Step 2. Modify the existing virtual network in the overlay controller
355 # 2.1 Add VNI (VxLAN Network Identifier - one free from the provided range)
356 # 2.2 Add RouteTarget (RT) ('ASN:VNI', ASN = Autonomous System Number, provided as param or read from controller config)
357 # Step 3. Create a virtual network in the underlay controller
358 # 3.1 Create virtual network (name, VNI, RT)
359 # If the network already existed in the overlay controller, we should use the same name
360 # name = 'osm-plugin-' + overlay_name
361 # Else:
362 # name = 'osm-plugin-' + VNI
363 self.logger.info("create_connectivity_service, service_type: {}, connection_points: {}".
364 format(service_type, connection_points))
365 if service_type.lower() != 'elan':
366 raise SdnConnectorError('Only ELAN network type is supported by Juniper Contrail.')
367
368 try:
369 # Initialize data
370 conn_info = None
371
372
373 # 1 - Filter connection_points (transform cp to a dictionary with no duplicates)
374 # This data will be returned even if no cp can be created if something is created
375 vlans = set()
376 work_cps = {}
377 for cp in connection_points:
378 switch_id = cp.get("service_endpoint_encapsulation_info").get("switch_dpid")
379 switch_port = cp.get("service_endpoint_encapsulation_info").get("switch_port")
380 service_endpoint_id = cp.get("service_endpoint_id")
381 vlans.add(cp.get("service_endpoint_encapsulation_info").get("vlan"))
382 cp_name = self.underlay_api.get_vpg_name(switch_id, switch_port)
383 add_cp = work_cps.get(cp_name)
384 if not add_cp:
385 # add cp to dict
386 service_endpoint_ids = []
387 service_endpoint_ids.append(service_endpoint_id)
388 add_cp = {"service_endpoint_ids": service_endpoint_ids,
389 "switch_dpid": switch_id,
390 "switch_port": switch_port}
391 work_cps[cp_name] = add_cp
392 else:
393 # add service_endpoint_id to list
394 service_endpoint_ids = add_cp["service_endpoint_ids"]
395 service_endpoint_ids.append(service_endpoint_id)
396
397 # check vlan
398 if len(vlans) == 1:
399 vlan = vlans.pop()
400 self.logger.debug("Provided vlan: {}".format(vlan))
401 if not vlan:
402 raise SdnConnectorError("No vlan provided")
403 else:
404 raise SdnConnectorError("Provided more than one vlan")
405
406 # 2 - Obtain free VNI
407 vni = self._generate_vni()
408 self.logger.debug("VNI: {}".format(vni))
409
410 # 3 - Create virtual network (name, VNI, RT), by the moment the name will use VNI
411 retry = 0
412 while retry < self._max_duplicate_retry:
413 try:
414 vnet_name = 'osm-plugin-' + str(vni)
415 vnet_id, _ = self.underlay_api.create_virtual_network(vnet_name, vni)
416 self.used_vni.add(vni)
417 break
418 except DuplicateFound as e:
419 self.logger.debug("Duplicate error for vnet_name: {}".format(vnet_name))
420 self.used_vni.add(vni)
421 retry += 1
422 if retry >= self._max_duplicate_retry:
423 raise e
424 else:
425 # Try to obtain a new vni
426 vni = self._generate_vni()
427 continue
428 conn_info = {
429 "vnet": {
430 "uuid": vnet_id,
431 "name": vnet_name,
432 "vlan": vlan
433 },
434 "connection_points": work_cps # dict with port_name as key
435 }
436
437 # 4 - Create a port for each endpoint
438 for cp in work_cps.values():
439 switch_id = cp.get("switch_dpid")
440 switch_port = cp.get("switch_port")
441 vpg_id, vmi_id = self._create_port(switch_id, switch_port, vnet_name, vlan)
442 cp["vpg_id"] = vpg_id
443 cp["vmi_id"] = vmi_id
444
445 self.logger.info("created connectivity service, uuid: {}, name: {}".format(vnet_id, vnet_name))
446 return vnet_id, conn_info
447
448 except Exception as e:
449 # Log error
450 if isinstance(e, SdnConnectorError) or isinstance(e, HttpException):
451 self.logger.error("Error creating connectivity service: {}".format(e))
452 else:
453 self.logger.error("Error creating connectivity service: {}".format(e), exc_info=True)
454
455
456 # If nothing is created raise error else return what has been created and mask as error
457 if not conn_info:
458 raise SdnConnectorError("Exception create connectivity service: {}".format(str(e)))
459 else:
460 conn_info["sdn_status"] = "ERROR"
461 conn_info["sdn_info"] = repr(e)
462 # iterate over not added connection_points and add but marking them as error
463 for cp in work_cps.values():
464 if not cp.get("vmi_id") or not cp.get("vpg_id"):
465 cp["sdn_status"] = "ERROR"
466 return vnet_id, conn_info
467
468 def delete_connectivity_service(self, service_uuid, conn_info=None):
469 """
470 Disconnect multi-site endpoints previously connected
471
472 :param service_uuid: The one returned by create_connectivity_service
473 :param conn_info: The one returned by last call to 'create_connectivity_service' or 'edit_connectivity_service'
474 if they do not return None
475 :return: None
476 :raises: SdnConnectorException: In case of error. The parameter http_code must be filled
477 """
478 self.logger.info("delete_connectivity_service vnet_name: {}, connection_points: {}".
479 format(service_uuid, conn_info))
480
481 try:
482 vnet_uuid = service_uuid
483 vnet_name = conn_info["vnet"]["name"] # always should exist as the network is the first thing created
484 work_cps = conn_info["connection_points"]
485 vlan = conn_info["vnet"]["vlan"]
486
487 # 1: For each connection point delete vlan from vpg and it is is the
488 # last one, delete vpg
489 for cp in work_cps.values():
490 self._delete_port(cp.get("switch_dpid"), cp.get("switch_port"), vlan)
491
492 # 2: Delete vnet
493 self.underlay_api.delete_virtual_network(vnet_uuid)
494 self.logger.info("deleted connectivity_service vnet_name: {}, connection_points: {}".
495 format(service_uuid, conn_info))
496 except SdnConnectorError:
497 raise
498 except HttpException as e:
499 self.logger.error("Error deleting connectivity service: {}".format(e))
500 raise SdnConnectorError("Exception deleting connectivity service: {}".format(str(e)))
501 except Exception as e:
502 self.logger.error("Error deleting connectivity service: {}".format(e), exc_info=True)
503 raise SdnConnectorError("Exception deleting connectivity service: {}".format(str(e)))
504
505 # Helper methods
506 @staticmethod
507 def _get_vlan(connection_points):
508 vlan = None
509 for cp in connection_points:
510 cp_vlan = cp.get("service_endpoint_encapsulation_info").get("vlan")
511 if not vlan:
512 vlan = cp_vlan
513 else:
514 if vlan != cp_vlan:
515 raise SdnConnectorError("More that one cp provided")
516 return vlan
517
518 def edit_connectivity_service(self, service_uuid, conn_info = None, connection_points = None, **kwargs):
519 """ Change an existing connectivity service.
520
521 This method's arguments and return value follow the same convention as
522 :meth:`~.create_connectivity_service`.
523
524 :param service_uuid: UUID of the connectivity service.
525 :param conn_info: (dict or None): Information previously returned by last call to create_connectivity_service
526 or edit_connectivity_service
527 :param connection_points: (list): If provided, the old list of connection points will be replaced.
528 :param kwargs: Same meaning that create_connectivity_service
529 :return: dict or None: Information to be updated and stored at the database.
530 When ``None`` is returned, no information should be changed.
531 When an empty dict is returned, the database record will be deleted.
532 **MUST** be JSON/YAML-serializable (plain data structures).
533 Raises:
534 SdnConnectorException: In case of error.
535 """
536 # 0 - Check if there are connection_points marked as error and delete them
537 # 1 - Compare conn_info (old connection points) and connection_points (new ones to be applied):
538 # Obtain list of connection points to be added and to be deleted
539 # Obtain vlan and check it has not changed
540 # 2 - Obtain network: Check vnet exists and obtain name
541 # 3 - Delete unnecesary ports
542 # 4 - Add new ports
543 self.logger.info("edit connectivity service, service_uuid: {}, conn_info: {}, "
544 "connection points: {} ".format(service_uuid, conn_info, connection_points))
545
546 # conn_info should always exist and have connection_points and vnet elements
547 old_cp = conn_info.get("connection_points", {})
548 old_vlan = conn_info.get("vnet", {}).get("vlan")
549
550 # Check if an element of old_cp is marked as error, in case it is delete it
551 # Not return a new conn_info in this case because it is only partial information
552 # Current conn_info already marks ports as error
553 try:
554 deleted_ports = []
555 for cp in old_cp.values():
556 if cp.get("sdn_status") == "ERROR":
557 switch_id = cp.get("switch_dpid")
558 switch_port = cp.get("switch_port")
559 self._delete_port(switch_id, switch_port, old_vlan)
560 deleted_ports.append(self.underlay_api.get_vpg_name(switch_id, switch_port))
561
562 for port in deleted_ports:
563 del old_cp[port]
564
565 # Delete sdn_status and sdn_info if exists (possibly marked as error)
566 if conn_info.get("vnet",{}).get("sdn_status"):
567 del conn_info["vnet"]["sdn_status"]
568 except HttpException as e:
569 self.logger.error("Error trying to delete old ports marked as error: {}".format(e))
570 raise SdnConnectorError(e)
571 except SdnConnectorError as e:
572 self.logger.error("Error trying to delete old ports marked as error: {}".format(e))
573 raise
574 except Exception as e:
575 self.logger.error("Error trying to delete old ports marked as error: {}".format(e), exc_info=True)
576 raise SdnConnectorError("Error trying to delete old ports marked as error: {}".format(e))
577
578 if connection_points:
579
580 # Check and obtain what should be added and deleted, if there is an error here raise an exception
581 try:
582 vlans = set()
583 work_cps = {}
584 for cp in connection_points:
585 switch_id = cp.get("service_endpoint_encapsulation_info").get("switch_dpid")
586 switch_port = cp.get("service_endpoint_encapsulation_info").get("switch_port")
587 service_endpoint_id = cp.get("service_endpoint_id")
588 vlans.add(cp.get("service_endpoint_encapsulation_info").get("vlan"))
589 cp_name = self.underlay_api.get_vpg_name(switch_id, switch_port)
590 add_cp = work_cps.get(cp_name)
591 if not add_cp:
592 # add cp to dict
593 service_endpoint_ids = []
594 service_endpoint_ids.append(service_endpoint_id)
595 add_cp = {"service_endpoint_ids": service_endpoint_ids,
596 "switch_dpid": switch_id,
597 "switch_port": switch_port}
598 work_cps[cp_name] = add_cp
599 else:
600 # add service_endpoint_id to list
601 service_endpoint_ids = add_cp["service_endpoint_ids"]
602 service_endpoint_ids.append(service_endpoint_id)
603
604 # check vlan
605 if len(vlans) == 1:
606 vlan = vlans.pop()
607 self.logger.debug("Provided vlan: {}".format(vlan))
608 else:
609 raise SdnConnectorError("Provided more than one vlan")
610
611 old_port_list = list(old_cp.keys())
612 port_list = list(work_cps.keys())
613 to_delete_ports = list(set(old_port_list) - set(port_list))
614 to_add_ports = list(set(port_list) - set(old_port_list))
615 self.logger.debug("ports to delete: {}".format(to_delete_ports))
616 self.logger.debug("ports to add: {}".format(to_add_ports))
617
618 # Obtain network (check it is correctly created)
619 vnet = self.underlay_api.get_virtual_network(service_uuid)
620 if vnet:
621 vnet_name = vnet["name"]
622 else:
623 raise SdnConnectorError("vnet uuid: {} not found".format(service_uuid))
624
625 except SdnConnectorError:
626 raise
627 except Exception as e:
628 self.logger.error("Error edit connectivity service: {}".format(e), exc_info=True)
629 raise SdnConnectorError("Exception edit connectivity service: {}".format(str(e)))
630
631 # Delete unneeded ports and add new ones: if there is an error return conn_info
632 try:
633 # Connection points returned in con_info should reflect what has (and should as ERROR) be done
634 # Start with old cp dictionary and modify it as we work
635 conn_info_cp = old_cp
636
637 # Delete unneeded ports
638 deleted_ports = []
639 for port_name in conn_info_cp.keys():
640 if port_name in to_delete_ports:
641 cp = conn_info_cp[port_name]
642 switch_id = cp.get("switch_dpid")
643 switch_port = cp.get("switch_port")
644 self.logger.debug("delete port switch_id, switch_port: {}".format(switch_id, switch_port))
645 self._delete_port(switch_id, switch_port, vlan)
646 deleted_ports.append(port_name)
647
648 # Delete ports
649 for port_name in deleted_ports:
650 del conn_info_cp[port_name]
651
652 # Add needed ports
653 for port_name, cp in work_cps.items():
654 if port_name in to_add_ports:
655 switch_id = cp.get("switch_dpid")
656 switch_port = cp.get("switch_port")
657 self.logger.debug("add port switch_id, switch_port: {}".format(switch_id, switch_port))
658 vpg_id, vmi_id = self._create_port(switch_id, switch_port, vnet_name, vlan)
659 cp_added = cp.copy()
660 cp_added["vpg_id"] = vpg_id
661 cp_added["vmi_id"] = vmi_id
662 conn_info_cp[port_name] = cp_added
663 # replace endpoints in case they have changed
664 conn_info_cp[port_name]["service_endpoint_ids"] = cp["service_endpoint_ids"]
665
666 conn_info["connection_points"] = conn_info_cp
667 return conn_info
668
669 except Exception as e:
670 # Log error
671 if isinstance(e, SdnConnectorError) or isinstance(e, HttpException):
672 self.logger.error("Error edit connectivity service: {}".format(e), exc_info=True)
673 else:
674 self.logger.error("Error edit connectivity service: {}".format(e))
675
676 # There has been an error mount conn_info_cp marking as error cp that should
677 # have been deleted but have not or should have been added
678 for port_name, cp in conn_info_cp.items():
679 if port_name in to_delete_ports:
680 cp["sdn_status"] = "ERROR"
681
682 for port_name, cp in work_cps.items():
683 curr_cp = conn_info_cp.get(port_name)
684 if not curr_cp:
685 cp_error = work_cps.get(port_name).copy()
686 cp_error["sdn_status"] = "ERROR"
687 conn_info_cp[port_name] = cp_error
688 conn_info_cp[port_name]["service_endpoint_ids"] = cp["service_endpoint_ids"]
689
690 conn_info["sdn_status"] = "ERROR"
691 conn_info["sdn_info"] = repr(e)
692 conn_info["connection_points"] = conn_info_cp
693 return conn_info
694
695 else:
696 # Connection points have not changed, so do nothing
697 self.logger.info("no new connection_points provided, nothing to be done")
698 return
699
700
701 if __name__ == '__main__':
702 # Init logger
703 log_format = "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(funcName)s(): %(message)s"
704 log_formatter = logging.Formatter(log_format, datefmt='%Y-%m-%dT%H:%M:%S')
705 handler = logging.StreamHandler()
706 handler.setFormatter(log_formatter)
707 logger = logging.getLogger('openmano.sdnconn.junipercontrail')
708 #logger.setLevel(level=logging.ERROR)
709 #logger.setLevel(level=logging.INFO)
710 logger.setLevel(level=logging.DEBUG)
711 logger.addHandler(handler)
712
713 # Read config
714 with open('test.yaml') as f:
715 config = yaml.safe_load(f.read())
716 wim = {'wim_url': config.pop('wim_url')}
717 wim_account = {'user': config.pop('user'), 'password': config.pop('password')}
718 logger.info('wim: {}, wim_account: {}, config: {}'.format(wim, wim_account, config))
719
720 # Init controller
721 juniper_contrail = JuniperContrail(wim=wim, wim_account=wim_account, config=config, logger=logger)
722
723 # Tests
724 # Generate VNI
725 for i in range(5):
726 vni = juniper_contrail._generate_vni()
727 juniper_contrail.used_vni.add(vni)
728 print(juniper_contrail.used_vni)
729 #juniper_contrail.used_vni.remove(1000003)
730 print(juniper_contrail.used_vni)
731 for i in range(2):
732 vni = juniper_contrail._generate_vni()
733 juniper_contrail.used_vni.add(vni)
734 print(juniper_contrail.used_vni)
735
736
737 # 0. Check credentials
738 print('0. Check credentials')
739 #juniper_contrail.check_credentials()
740
741 # 1 - Create and delete connectivity service
742 conn_point_0 = {
743 "service_endpoint_id": "0000:83:11.4",
744 "service_endpoint_encapsulation_type": "dot1q",
745 "service_endpoint_encapsulation_info": {
746 "switch_dpid": "LEAF-1",
747 "switch_port": "xe-0/0/17",
748 "vlan": "501"
749 }
750 }
751 conn_point_1 = {
752 "service_endpoint_id": "0000:81:10.3",
753 "service_endpoint_encapsulation_type": "dot1q",
754 "service_endpoint_encapsulation_info": {
755 "switch_dpid": "LEAF-2",
756 "switch_port": "xe-0/0/16",
757 "vlan": "501"
758 }
759 }
760 conn_point_2 = {
761 "service_endpoint_id": "0000:08:11.7",
762 "service_endpoint_encapsulation_type": "dot1q",
763 "service_endpoint_encapsulation_info": {
764 "switch_dpid": "LEAF-2",
765 "switch_port": "xe-0/0/16",
766 "vlan": "502"
767 }
768 }
769 conn_point_3 = {
770 "service_endpoint_id": "0000:83:10.4",
771 "service_endpoint_encapsulation_type": "dot1q",
772 "service_endpoint_encapsulation_info": {
773 "switch_dpid": "LEAF-1",
774 "switch_port": "xe-0/0/17",
775 "vlan": "502"
776 }
777 }
778
779 # 1 - Define connection points
780 logger.debug("create first connection service")
781 print("Create connectivity service")
782 connection_points = [conn_point_0, conn_point_1]
783 service_id, conn_info = juniper_contrail.create_connectivity_service("ELAN", connection_points)
784 logger.info("Created connectivity service 1")
785 logger.info(service_id)
786 logger.info(yaml.safe_dump(conn_info, indent=4, default_flow_style=False))
787
788 logger.debug("create second connection service")
789 print("Create connectivity service")
790 connection_points = [conn_point_2, conn_point_3]
791 service_id2, conn_info2 = juniper_contrail.create_connectivity_service("ELAN", connection_points)
792 logger.info("Created connectivity service 2")
793 logger.info(service_id2)
794 logger.info(yaml.safe_dump(conn_info2, indent=4, default_flow_style=False))
795
796 logger.debug("Delete connectivity service 1")
797 juniper_contrail.delete_connectivity_service(service_id, conn_info)
798 logger.debug("Delete Ok")
799
800 logger.debug("Delete connectivity service 2")
801 juniper_contrail.delete_connectivity_service(service_id2, conn_info2)
802 logger.debug("Delete Ok")
803