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