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
18%
70/380
100%
0/0

Coverage Breakdown by Class

NameLinesConditionals
sdn_assist_juniper_contrail.py
18%
70/380
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 _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 0         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 0         vpg_name = self.underlay_api.get_vpg_name(switch_id, switch_port)
230 0         vpg = self.underlay_api.get_vpg_by_name(vpg_name)
231
232 0         if not vpg:
233             # 2 - If it does not exist create it
234 0             vpg_id, _ = self.underlay_api.create_vpg(switch_id, switch_port)
235         else:
236             # Assign vpg_id from vpg
237 0             vpg_id = vpg.get("uuid")
238
239         # 3 - Check if the vmi alreaady exists
240 0         vmi_id, _ = self.underlay_api.create_vmi(switch_id, switch_port, network, vlan)
241 0         self.logger.debug("port created")
242
243 0         return vpg_id, vmi_id
244
245 1     def _delete_port(self, switch_id, switch_port, vlan):
246 0         self.logger.debug(
247             "delete port, switch_id: {}, switch_port: {}, vlan: {}".format(
248                 switch_id, switch_port, vlan
249             )
250         )
251
252 0         vpg_name = self.underlay_api.get_vpg_name(switch_id, switch_port)
253 0         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 0         vpg_fqdn = ["default-global-system-config", self.fabric, vpg_name]
257 0         vpg = self.underlay_api.get_by_fq_name("virtual-port-group", vpg_fqdn)
258
259 0         if not vpg:
260 0             self.logger.warning("vpg: {} to be deleted not found".format(vpg_name))
261         else:
262             # 2 - Get vmi interfaces from vpg
263 0             vmi_list = vpg.get("virtual_machine_interface_refs")
264
265 0             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 0                 self.logger.warning(
269                     "vpg: {} has not vmi, will delete nothing".format(vpg)
270                 )
271             else:
272 0                 num_vmis = len(vmi_list)
273
274 0                 for vmi in vmi_list:
275 0                     fqdn = vmi.get("to")
276                     # check by name
277
278 0                     if fqdn[2] == vmi_name:
279 0                         self.underlay_api.unref_vmi_vpg(
280                             vpg.get("uuid"), vmi.get("uuid"), fqdn
281                         )
282 0                         self.underlay_api.delete_vmi(vmi.get("uuid"))
283 0                         num_vmis = num_vmis - 1
284
285             # 3 - If there are no more vmi delete the vpg
286 0             if not vmi_list or num_vmis == 0:
287 0                 self.underlay_api.delete_vpg(vpg.get("uuid"))
288
289 1     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 0         self.logger.debug("")
298
299 0         try:
300 0             resp = self.underlay_api.check_auth()
301 0             if not resp:
302 0                 raise SdnConnectorError("Empty response")
303 0         except Exception as e:
304 0             self.logger.error("Error checking credentials")
305
306 0             raise SdnConnectorError("Error checking credentials: {}".format(str(e)))
307
308 1     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 0         self.logger.debug("")
346
347 0         try:
348 0             resp = self.underlay_api.get_virtual_network(service_uuid)
349 0             if not resp:
350 0                 raise SdnConnectorError("Empty response")
351
352 0             if resp:
353 0                 vnet_info = resp
354
355                 # Check if conn_info reports error
356 0                 if conn_info.get("sdn_status") == "ERROR":
357 0                     return {"sdn_status": "ERROR", "sdn_info": conn_info}
358                 else:
359 0                     return {"sdn_status": "ACTIVE", "sdn_info": vnet_info}
360             else:
361 0                 return {"sdn_status": "ERROR", "sdn_info": "not found"}
362 0         except SdnConnectorError:
363 0             raise
364 0         except HttpException as e:
365 0             self.logger.error("Error getting connectivity service: {}".format(e))
366
367 0             raise SdnConnectorError(
368                 "Exception deleting connectivity service: {}".format(str(e))
369             )
370 0         except Exception as e:
371 0             self.logger.error(
372                 "Exception getting connectivity service info: %s", e, exc_info=True
373             )
374
375 0             return {"sdn_status": "ERROR", "error_msg": str(e)}
376
377 1     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 0         self.logger.info(
432             "create_connectivity_service, service_type: {}, connection_points: {}".format(
433                 service_type, connection_points
434             )
435         )
436
437 0         if service_type.lower() != "elan":
438 0             raise SdnConnectorError(
439                 "Only ELAN network type is supported by Juniper Contrail."
440             )
441
442 0         try:
443             # Initialize data
444 0             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 0             work_cps = {}
449 0             for cp in connection_points:
450 0                 switch_id = cp.get("service_endpoint_encapsulation_info").get(
451                     "switch_dpid"
452                 )
453 0                 switch_port = cp.get("service_endpoint_encapsulation_info").get(
454                     "switch_port"
455                 )
456 0                 service_endpoint_id = cp.get("service_endpoint_id")
457 0                 cp_name = self.underlay_api.get_vpg_name(switch_id, switch_port)
458 0                 add_cp = work_cps.get(cp_name)
459
460 0                 if not add_cp:
461                     # check cp has vlan
462 0                     vlan = cp.get("service_endpoint_encapsulation_info").get("vlan")
463
464 0                     if vlan:
465                         # add cp to dict
466 0                         service_endpoint_ids = []
467 0                         service_endpoint_ids.append(service_endpoint_id)
468 0                         add_cp = {
469                             "service_endpoint_ids": service_endpoint_ids,
470                             "switch_dpid": switch_id,
471                             "switch_port": switch_port,
472                             "vlan": vlan,
473                         }
474 0                         work_cps[cp_name] = add_cp
475                     else:
476 0                         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 0                     service_endpoint_ids = add_cp["service_endpoint_ids"]
484 0                     service_endpoint_ids.append(service_endpoint_id)
485
486             # 2 - Obtain free VNI
487 0             vni = self._generate_vni()
488 0             self.logger.debug("VNI: {}".format(vni))
489
490             # 3 - Create virtual network (name, VNI, RT), by the moment the name will use VNI
491 0             retry = 0
492 0             while retry < self._max_duplicate_retry:
493 0                 try:
494 0                     vnet_name = "osm-plugin-" + str(vni)
495 0                     vnet_id, _ = self.underlay_api.create_virtual_network(
496                         vnet_name, vni
497                     )
498 0                     self.used_vni.add(vni)
499 0                     break
500 0                 except DuplicateFound as e:
501 0                     self.logger.debug(
502                         "Duplicate error for vnet_name: {}".format(vnet_name)
503                     )
504 0                     self.used_vni.add(vni)
505 0                     retry += 1
506
507 0                     if retry >= self._max_duplicate_retry:
508 0                         raise e
509                     else:
510                         # Try to obtain a new vni
511 0                         vni = self._generate_vni()
512 0                         continue
513
514 0             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 0             for cp in work_cps.values():
524 0                 switch_id = cp.get("switch_dpid")
525 0                 switch_port = cp.get("switch_port")
526 0                 vlan = cp.get("vlan")
527 0                 vpg_id, vmi_id = self._create_port(
528                     switch_id, switch_port, vnet_name, vlan
529                 )
530 0                 cp["vpg_id"] = vpg_id
531 0                 cp["vmi_id"] = vmi_id
532
533 0             self.logger.info(
534                 "created connectivity service, uuid: {}, name: {}".format(
535                     vnet_id, vnet_name
536                 )
537             )
538
539 0             return vnet_id, conn_info
540 0         except Exception as e:
541             # Log error
542 0             if isinstance(e, SdnConnectorError) or isinstance(e, HttpException):
543 0                 self.logger.error("Error creating connectivity service: {}".format(e))
544             else:
545 0                 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 0             if not conn_info:
551 0                 raise SdnConnectorError(
552                     "Exception create connectivity service: {}".format(str(e))
553                 )
554             else:
555 0                 conn_info["sdn_status"] = "ERROR"
556 0                 conn_info["sdn_info"] = repr(e)
557                 # iterate over not added connection_points and add but marking them as error
558 0                 for cp in work_cps.values():
559 0                     if not cp.get("vmi_id") or not cp.get("vpg_id"):
560 0                         cp["sdn_status"] = "ERROR"
561
562 0                 return vnet_id, conn_info
563
564 1     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 0         self.logger.info(
575             "delete_connectivity_service vnet_name: {}, connection_points: {}".format(
576                 service_uuid, conn_info
577             )
578         )
579
580 0         try:
581 0             vnet_uuid = service_uuid
582             # vnet_name = conn_info["vnet"]["name"]
583             # always should exist as the network is the first thing created
584 0             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 0             for cp in work_cps.values():
589 0                 self._delete_port(
590                     cp.get("switch_dpid"), cp.get("switch_port"), cp.get("vlan")
591                 )
592
593             # 2: Delete vnet
594 0             self.underlay_api.delete_virtual_network(vnet_uuid)
595 0             self.logger.info(
596                 "deleted connectivity_service vnet_uuid: {}, connection_points: {}".format(
597                     service_uuid, conn_info
598                 )
599             )
600 0         except SdnConnectorError:
601 0             raise
602 0         except HttpException as e:
603 0             self.logger.error("Error deleting connectivity service: {}".format(e))
604
605 0             raise SdnConnectorError(
606                 "Exception deleting connectivity service: {}".format(str(e))
607             )
608 0         except Exception as e:
609 0             self.logger.error(
610                 "Error deleting connectivity service: {}".format(e),
611                 exc_info=True,
612             )
613
614 0             raise SdnConnectorError(
615                 "Exception deleting connectivity service: {}".format(str(e))
616             )
617
618 1     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 0         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 0         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 0         try:
657 0             deleted_ports = []
658 0             for cp in old_cp.values():
659 0                 if cp.get("sdn_status") == "ERROR":
660 0                     switch_id = cp.get("switch_dpid")
661 0                     switch_port = cp.get("switch_port")
662 0                     old_vlan = cp.get("vlan")
663 0                     self._delete_port(switch_id, switch_port, old_vlan)
664 0                     deleted_ports.append(
665                         self.underlay_api.get_vpg_name(switch_id, switch_port)
666                     )
667
668 0             for port in deleted_ports:
669 0                 del old_cp[port]
670
671             # Delete sdn_status and sdn_info if exists (possibly marked as error)
672 0             if conn_info.get("vnet", {}).get("sdn_status"):
673 0                 del conn_info["vnet"]["sdn_status"]
674 0         except HttpException as e:
675 0             self.logger.error(
676                 "Error trying to delete old ports marked as error: {}".format(e)
677             )
678
679 0             raise SdnConnectorError(e)
680 0         except SdnConnectorError as e:
681 0             self.logger.error(
682                 "Error trying to delete old ports marked as error: {}".format(e)
683             )
684
685 0             raise
686 0         except Exception as e:
687 0             self.logger.error(
688                 "Error trying to delete old ports marked as error: {}".format(e),
689                 exc_info=True,
690             )
691
692 0             raise SdnConnectorError(
693                 "Error trying to delete old ports marked as error: {}".format(e)
694             )
695
696 0         if connection_points:
697             # Check and obtain what should be added and deleted, if there is an error here raise an exception
698 0             try:
699 0                 work_cps = {}
700 0                 for cp in connection_points:
701 0                     switch_id = cp.get("service_endpoint_encapsulation_info").get(
702                         "switch_dpid"
703                     )
704 0                     switch_port = cp.get("service_endpoint_encapsulation_info").get(
705                         "switch_port"
706                     )
707 0                     service_endpoint_id = cp.get("service_endpoint_id")
708 0                     cp_name = self.underlay_api.get_vpg_name(switch_id, switch_port)
709 0                     add_cp = work_cps.get(cp_name)
710
711 0                     if not add_cp:
712                         # add cp to dict
713                         # check cp has vlan
714 0                         vlan = cp.get("service_endpoint_encapsulation_info").get("vlan")
715
716 0                         if vlan:
717 0                             service_endpoint_ids = []
718 0                             service_endpoint_ids.append(service_endpoint_id)
719 0                             add_cp = {
720                                 "service_endpoint_ids": service_endpoint_ids,
721                                 "switch_dpid": switch_id,
722                                 "switch_port": switch_port,
723                                 "vlan": vlan,
724                             }
725 0                             work_cps[cp_name] = add_cp
726                         else:
727 0                             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 0                         service_endpoint_ids = add_cp["service_endpoint_ids"]
735 0                         service_endpoint_ids.append(service_endpoint_id)
736
737 0                 old_port_list = list(old_cp.keys())
738 0                 port_list = list(work_cps.keys())
739 0                 to_delete_ports = list(set(old_port_list) - set(port_list))
740 0                 to_add_ports = list(set(port_list) - set(old_port_list))
741 0                 self.logger.debug("ports to delete: {}".format(to_delete_ports))
742 0                 self.logger.debug("ports to add: {}".format(to_add_ports))
743
744                 # Obtain network (check it is correctly created)
745 0                 vnet = self.underlay_api.get_virtual_network(service_uuid)
746 0                 if vnet:
747 0                     vnet_name = vnet["name"]
748                 else:
749 0                     raise SdnConnectorError(
750                         "vnet uuid: {} not found".format(service_uuid)
751                     )
752 0             except SdnConnectorError:
753 0                 raise
754 0             except Exception as e:
755 0                 self.logger.error(
756                     "Error edit connectivity service: {}".format(e), exc_info=True
757                 )
758
759 0                 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 0             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 0                 conn_info_cp = old_cp
768
769                 # Delete unneeded ports
770 0                 deleted_ports = []
771 0                 for port_name in conn_info_cp.keys():
772 0                     if port_name in to_delete_ports:
773 0                         cp = conn_info_cp[port_name]
774 0                         switch_id = cp.get("switch_dpid")
775 0                         switch_port = cp.get("switch_port")
776 0                         self.logger.debug(
777                             "delete port switch_id={}, switch_port={}".format(
778                                 switch_id, switch_port
779                             )
780                         )
781 0                         self._delete_port(switch_id, switch_port, vlan)
782 0                         deleted_ports.append(port_name)
783
784                 # Delete ports
785 0                 for port_name in deleted_ports:
786 0                     del conn_info_cp[port_name]
787
788                 # Add needed ports
789 0                 for port_name, cp in work_cps.items():
790 0                     if port_name in to_add_ports:
791 0                         switch_id = cp.get("switch_dpid")
792 0                         switch_port = cp.get("switch_port")
793 0                         vlan = cp.get("vlan")
794 0                         self.logger.debug(
795                             "add port switch_id={}, switch_port={}".format(
796                                 switch_id, switch_port
797                             )
798                         )
799 0                         vpg_id, vmi_id = self._create_port(
800                             switch_id, switch_port, vnet_name, vlan
801                         )
802 0                         cp_added = cp.copy()
803 0                         cp_added["vpg_id"] = vpg_id
804 0                         cp_added["vmi_id"] = vmi_id
805 0                         conn_info_cp[port_name] = cp_added
806
807                     # replace endpoints in case they have changed
808 0                     conn_info_cp[port_name]["service_endpoint_ids"] = cp[
809                         "service_endpoint_ids"
810                     ]
811
812 0                 conn_info["connection_points"] = conn_info_cp
813 0                 return conn_info
814
815 0             except Exception as e:
816                 # Log error
817 0                 if isinstance(e, SdnConnectorError) or isinstance(e, HttpException):
818 0                     self.logger.error(
819                         "Error edit connectivity service: {}".format(e), exc_info=True
820                     )
821                 else:
822 0                     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 0                 for port_name, cp in conn_info_cp.items():
827 0                     if port_name in to_delete_ports:
828 0                         cp["sdn_status"] = "ERROR"
829
830 0                 for port_name, cp in work_cps.items():
831 0                     curr_cp = conn_info_cp.get(port_name)
832
833 0                     if not curr_cp:
834 0                         cp_error = work_cps.get(port_name).copy()
835 0                         cp_error["sdn_status"] = "ERROR"
836 0                         conn_info_cp[port_name] = cp_error
837
838 0                     conn_info_cp[port_name]["service_endpoint_ids"] = cp[
839                         "service_endpoint_ids"
840                     ]
841
842 0                 conn_info["sdn_status"] = "ERROR"
843 0                 conn_info["sdn_info"] = repr(e)
844 0                 conn_info["connection_points"] = conn_info_cp
845
846 0                 return conn_info
847         else:
848             # Connection points have not changed, so do nothing
849 0             self.logger.info("no new connection_points provided, nothing to be done")
850
851 0             return
852
853
854 1 if __name__ == "__main__":
855     # Init logger
856 0     log_format = "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(funcName)s(): %(message)s"
857 0     log_formatter = logging.Formatter(log_format, datefmt="%Y-%m-%dT%H:%M:%S")
858 0     handler = logging.StreamHandler()
859 0     handler.setFormatter(log_formatter)
860 0     logger = logging.getLogger("ro.sdn.junipercontrail")
861     # logger.setLevel(level=logging.ERROR)
862     # logger.setLevel(level=logging.INFO)
863 0     logger.setLevel(level=logging.DEBUG)
864 0     logger.addHandler(handler)
865
866     # Read config
867 0     with open("test.yaml") as f:
868 0         config = yaml.safe_load(f.read())
869
870 0     wim = {"wim_url": config.pop("wim_url")}
871 0     wim_account = {"user": config.pop("user"), "password": config.pop("password")}
872 0     logger.info("wim: {}, wim_account: {}, config: {}".format(wim, wim_account, config))
873
874     # Init controller
875 0     juniper_contrail = JuniperContrail(
876         wim=wim, wim_account=wim_account, config=config, logger=logger
877     )
878
879     # Tests
880     # Generate VNI
881 0     for i in range(5):
882 0         vni = juniper_contrail._generate_vni()
883 0         juniper_contrail.used_vni.add(vni)
884
885 0     print(juniper_contrail.used_vni)
886     # juniper_contrail.used_vni.remove(1000003)
887 0     print(juniper_contrail.used_vni)
888
889 0     for i in range(2):
890 0         vni = juniper_contrail._generate_vni()
891 0         juniper_contrail.used_vni.add(vni)
892
893 0     print(juniper_contrail.used_vni)
894
895     # 0. Check credentials
896 0     print("0. Check credentials")
897     # juniper_contrail.check_credentials()
898
899     # 1 - Create and delete connectivity service
900 0     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 0     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 0     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 0     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 0     logger.debug("create first connection service")
939 0     print("Create connectivity service")
940 0     connection_points = [conn_point_0, conn_point_1]
941 0     service_id, conn_info = juniper_contrail.create_connectivity_service(
942         "ELAN", connection_points
943     )
944 0     logger.info("Created connectivity service 1")
945 0     logger.info(service_id)
946 0     logger.info(yaml.safe_dump(conn_info, indent=4, default_flow_style=False))
947
948 0     logger.debug("create second connection service")
949 0     print("Create connectivity service")
950 0     connection_points = [conn_point_2, conn_point_3]
951 0     service_id2, conn_info2 = juniper_contrail.create_connectivity_service(
952         "ELAN", connection_points
953     )
954 0     logger.info("Created connectivity service 2")
955 0     logger.info(service_id2)
956 0     logger.info(yaml.safe_dump(conn_info2, indent=4, default_flow_style=False))
957
958 0     logger.debug("Delete connectivity service 1")
959 0     juniper_contrail.delete_connectivity_service(service_id, conn_info)
960 0     logger.debug("Delete Ok")
961
962 0     logger.debug("Delete connectivity service 2")
963 0     juniper_contrail.delete_connectivity_service(service_id2, conn_info2)
964 0     logger.debug("Delete Ok")