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