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