Code Coverage

Cobertura Coverage Report > RO-SDN-juniper_contrail.osm_rosdn_juniper_contrail >

sdn_assist_juniper_contrail.py

Trend

File Coverage summary

NameClassesLinesConditionals
sdn_assist_juniper_contrail.py
100%
1/1
19%
71/382
100%
0/0

Coverage Breakdown by Class

NameLinesConditionals
sdn_assist_juniper_contrail.py
19%
71/382
N/A

Source

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