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