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