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