1 |
|
# Copyright 2020 ETSI |
2 |
|
# |
3 |
|
# All Rights Reserved. |
4 |
|
# |
5 |
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may |
6 |
|
# not use this file except in compliance with the License. You may obtain |
7 |
|
# a copy of the License at |
8 |
|
# |
9 |
|
# http://www.apache.org/licenses/LICENSE-2.0 |
10 |
|
# |
11 |
|
# Unless required by applicable law or agreed to in writing, software |
12 |
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
13 |
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
14 |
|
# License for the specific language governing permissions and limitations |
15 |
|
# under the License. |
16 |
|
|
17 |
1 |
import json |
18 |
1 |
import logging |
19 |
|
|
20 |
1 |
from osm_ro_plugin.sdnconn import SdnConnectorError |
21 |
1 |
from osm_rosdn_juniper_contrail.rest_lib import ContrailHttp |
22 |
1 |
from osm_rosdn_juniper_contrail.rest_lib import NotFound |
23 |
|
|
24 |
|
# from osm_rosdn_juniper_contrail.rest_lib import DuplicateFound |
25 |
|
# from osm_rosdn_juniper_contrail.rest_lib import HttpException |
26 |
|
|
27 |
|
|
28 |
1 |
class UnderlayApi: |
29 |
|
"""Class with CRUD operations for the underlay API""" |
30 |
|
|
31 |
1 |
def __init__(self, url, config=None, user=None, password=None, logger=None): |
32 |
1 |
self.logger = logger or logging.getLogger("ro.sdn.junipercontrail.sdnapi") |
33 |
1 |
self.controller_url = url |
34 |
|
|
35 |
1 |
if not url: |
36 |
0 |
raise SdnConnectorError("'url' must be provided") |
37 |
|
|
38 |
1 |
if not url.startswith("http"): |
39 |
0 |
url = "http://" + url |
40 |
|
|
41 |
1 |
if not url.endswith("/"): |
42 |
0 |
url = url + "/" |
43 |
|
|
44 |
1 |
self.url = url |
45 |
1 |
self.auth_url = None |
46 |
1 |
self.project = None |
47 |
1 |
self.domain = None |
48 |
1 |
self.asn = None |
49 |
1 |
self.fabric = None |
50 |
|
|
51 |
1 |
if config: |
52 |
1 |
self.auth_url = config.get("auth_url") |
53 |
1 |
self.project = config.get("project") |
54 |
1 |
self.domain = config.get("domain") |
55 |
1 |
self.asn = config.get("asn") |
56 |
1 |
self.fabric = config.get("fabric") |
57 |
1 |
self.verify = config.get("verify") |
58 |
|
|
59 |
|
# Init http headers for all requests |
60 |
1 |
self.http_header = {"Content-Type": "application/json"} |
61 |
|
|
62 |
1 |
if user: |
63 |
1 |
self.user = user |
64 |
|
|
65 |
1 |
if password: |
66 |
1 |
self.password = password |
67 |
|
|
68 |
1 |
self.logger.debug( |
69 |
|
"Config parameters for the underlay controller: auth_url: {}, project: {}," |
70 |
|
" domain: {}, user: {}, password: {}".format( |
71 |
|
self.auth_url, self.project, self.domain, self.user, self.password |
72 |
|
) |
73 |
|
) |
74 |
|
|
75 |
1 |
auth_dict = {} |
76 |
1 |
auth_dict["auth"] = {} |
77 |
1 |
auth_dict["auth"]["scope"] = {} |
78 |
1 |
auth_dict["auth"]["scope"]["project"] = {} |
79 |
1 |
auth_dict["auth"]["scope"]["project"]["domain"] = {} |
80 |
1 |
auth_dict["auth"]["scope"]["project"]["domain"]["id"] = self.domain |
81 |
1 |
auth_dict["auth"]["scope"]["project"]["name"] = self.project |
82 |
1 |
auth_dict["auth"]["identity"] = {} |
83 |
1 |
auth_dict["auth"]["identity"]["methods"] = ["password"] |
84 |
1 |
auth_dict["auth"]["identity"]["password"] = {} |
85 |
1 |
auth_dict["auth"]["identity"]["password"]["user"] = {} |
86 |
1 |
auth_dict["auth"]["identity"]["password"]["user"]["name"] = self.user |
87 |
1 |
auth_dict["auth"]["identity"]["password"]["user"]["password"] = self.password |
88 |
1 |
auth_dict["auth"]["identity"]["password"]["user"]["domain"] = {} |
89 |
1 |
auth_dict["auth"]["identity"]["password"]["user"]["domain"]["id"] = self.domain |
90 |
1 |
self.auth_dict = auth_dict |
91 |
|
|
92 |
|
# Init http lib |
93 |
1 |
auth_info = {"auth_url": self.auth_url, "auth_dict": auth_dict} |
94 |
1 |
self.http = ContrailHttp(auth_info, self.logger, self.verify) |
95 |
|
|
96 |
1 |
def check_auth(self): |
97 |
0 |
response = self.http.get_cmd(url=self.auth_url, headers=self.http_header) |
98 |
|
|
99 |
0 |
return response |
100 |
|
|
101 |
|
# Helper methods for CRUD operations |
102 |
1 |
def get_all_by_type(self, controller_url, type): |
103 |
0 |
endpoint = controller_url + type |
104 |
0 |
response = self.http.get_cmd(url=endpoint, headers=self.http_header) |
105 |
|
|
106 |
0 |
return response.get(type) |
107 |
|
|
108 |
1 |
def get_by_uuid(self, type, uuid): |
109 |
0 |
try: |
110 |
0 |
endpoint = self.controller_url + type + "/{}".format(uuid) |
111 |
0 |
response = self.http.get_cmd(url=endpoint, headers=self.http_header) |
112 |
|
|
113 |
0 |
return response.get(type) |
114 |
0 |
except NotFound: |
115 |
0 |
return None |
116 |
|
|
117 |
1 |
def delete_by_uuid(self, controller_url, type, uuid): |
118 |
0 |
endpoint = controller_url + type + "/{}".format(uuid) |
119 |
0 |
self.http.delete_cmd(url=endpoint, headers=self.http_header) |
120 |
|
|
121 |
1 |
def get_uuid_from_fqname(self, type, fq_name): |
122 |
|
""" |
123 |
|
Obtain uuid from fqname |
124 |
|
Returns: If resource not found returns None |
125 |
|
In case of error raises an Exception |
126 |
|
""" |
127 |
0 |
payload = {"type": type, "fq_name": fq_name} |
128 |
|
|
129 |
0 |
try: |
130 |
0 |
endpoint = self.controller_url + "fqname-to-id" |
131 |
0 |
resp = self.http.post_cmd( |
132 |
|
url=endpoint, headers=self.http_header, post_fields_dict=payload |
133 |
|
) |
134 |
|
|
135 |
0 |
return json.loads(resp).get("uuid") |
136 |
0 |
except NotFound: |
137 |
0 |
return None |
138 |
|
|
139 |
1 |
def get_by_fq_name(self, type, fq_name): |
140 |
|
# Obtain uuid by fqdn and then get data by uuid |
141 |
0 |
uuid = self.get_uuid_from_fqname(type, fq_name) |
142 |
|
|
143 |
0 |
if uuid: |
144 |
0 |
return self.get_by_uuid(type, uuid) |
145 |
|
else: |
146 |
0 |
return None |
147 |
|
|
148 |
1 |
def delete_ref(self, type, uuid, ref_type, ref_uuid, ref_fq_name): |
149 |
0 |
payload = { |
150 |
|
"type": type, |
151 |
|
"uuid": uuid, |
152 |
|
"ref-type": ref_type, |
153 |
|
"ref-fq-name": ref_fq_name, |
154 |
|
"operation": "DELETE", |
155 |
|
} |
156 |
0 |
endpoint = self.controller_url + "ref-update" |
157 |
0 |
resp = self.http.post_cmd( |
158 |
|
url=endpoint, headers=self.http_header, post_fields_dict=payload |
159 |
|
) |
160 |
|
|
161 |
0 |
return resp |
162 |
|
|
163 |
|
# Aux methods to avoid code duplication of name conventions |
164 |
1 |
def get_vpg_name(self, switch_id, switch_port): |
165 |
0 |
return "{}_{}".format(switch_id, switch_port).replace(":", "_") |
166 |
|
|
167 |
1 |
def get_vmi_name(self, switch_id, switch_port, vlan): |
168 |
0 |
return "{}_{}-{}".format(switch_id, switch_port, vlan).replace(":", "_") |
169 |
|
|
170 |
|
# Virtual network operations |
171 |
|
|
172 |
1 |
def create_virtual_network(self, name, vni): |
173 |
0 |
self.logger.debug("create vname, name: {}, vni: {}".format(name, vni)) |
174 |
0 |
routetarget = "{}:{}".format(self.asn, vni) |
175 |
0 |
vnet_dict = { |
176 |
|
"virtual-network": { |
177 |
|
"virtual_network_properties": { |
178 |
|
"vxlan_network_identifier": vni, |
179 |
|
}, |
180 |
|
"parent_type": "project", |
181 |
|
"fq_name": [self.domain, self.project, name], |
182 |
|
"route_target_list": {"route_target": ["target:" + routetarget]}, |
183 |
|
} |
184 |
|
} |
185 |
0 |
endpoint = self.controller_url + "virtual-networks" |
186 |
0 |
resp = self.http.post_cmd( |
187 |
|
url=endpoint, headers=self.http_header, post_fields_dict=vnet_dict |
188 |
|
) |
189 |
|
|
190 |
0 |
if not resp: |
191 |
0 |
raise SdnConnectorError("Error creating virtual network: empty response") |
192 |
|
|
193 |
0 |
vnet_info = json.loads(resp) |
194 |
0 |
self.logger.debug("created vnet, vnet_info: {}".format(vnet_info)) |
195 |
|
|
196 |
0 |
return vnet_info.get("virtual-network").get("uuid"), vnet_info.get( |
197 |
|
"virtual-network" |
198 |
|
) |
199 |
|
|
200 |
1 |
def get_virtual_networks(self): |
201 |
0 |
return self.get_all_by_type("virtual-networks") |
202 |
|
|
203 |
1 |
def get_virtual_network(self, network_id): |
204 |
0 |
return self.get_by_uuid("virtual-network", network_id) |
205 |
|
|
206 |
1 |
def delete_virtual_network(self, network_id): |
207 |
0 |
self.logger.debug("delete vnet uuid: {}".format(network_id)) |
208 |
0 |
self.delete_by_uuid(self.controller_url, "virtual-network", network_id) |
209 |
0 |
self.logger.debug("deleted vnet uuid: {}".format(network_id)) |
210 |
|
|
211 |
|
# Vpg operations |
212 |
|
|
213 |
1 |
def create_vpg(self, switch_id, switch_port): |
214 |
0 |
self.logger.debug( |
215 |
|
"create vpg, switch_id: {}, switch_port: {}".format(switch_id, switch_port) |
216 |
|
) |
217 |
0 |
vpg_name = self.get_vpg_name(switch_id, switch_port) |
218 |
0 |
vpg_dict = { |
219 |
|
"virtual-port-group": { |
220 |
|
"parent_type": "fabric", |
221 |
|
"fq_name": ["default-global-system-config", self.fabric, vpg_name], |
222 |
|
} |
223 |
|
} |
224 |
0 |
endpoint = self.controller_url + "virtual-port-groups" |
225 |
0 |
resp = self.http.post_cmd( |
226 |
|
url=endpoint, headers=self.http_header, post_fields_dict=vpg_dict |
227 |
|
) |
228 |
|
|
229 |
0 |
if not resp: |
230 |
0 |
raise SdnConnectorError("Error creating virtual port group: empty response") |
231 |
|
|
232 |
0 |
vpg_info = json.loads(resp) |
233 |
0 |
self.logger.debug("created vpg, vpg_info: {}".format(vpg_info)) |
234 |
|
|
235 |
0 |
return vpg_info.get("virtual-port-group").get("uuid"), vpg_info.get( |
236 |
|
"virtual-port-group" |
237 |
|
) |
238 |
|
|
239 |
1 |
def get_vpgs(self): |
240 |
0 |
return self.get_all_by_type(self.controller_url, "virtual-port-groups") |
241 |
|
|
242 |
1 |
def get_vpg(self, vpg_id): |
243 |
0 |
return self.get_by_uuid(self.controller_url, "virtual-port-group", vpg_id) |
244 |
|
|
245 |
1 |
def get_vpg_by_name(self, vpg_name): |
246 |
0 |
fq_name = ["default-global-system-config", self.fabric, vpg_name] |
247 |
|
|
248 |
0 |
return self.get_by_fq_name("virtual-port-group", fq_name) |
249 |
|
|
250 |
1 |
def delete_vpg(self, vpg_id): |
251 |
0 |
self.logger.debug("delete vpg, uuid: {}".format(vpg_id)) |
252 |
0 |
self.delete_by_uuid(self.controller_url, "virtual-port-group", vpg_id) |
253 |
0 |
self.logger.debug("deleted vpg, uuid: {}".format(vpg_id)) |
254 |
|
|
255 |
1 |
def create_vmi(self, switch_id, switch_port, network, vlan): |
256 |
0 |
self.logger.debug( |
257 |
|
"create vmi, switch_id: {}, switch_port: {}, network: {}, vlan: {}".format( |
258 |
|
switch_id, switch_port, network, vlan |
259 |
|
) |
260 |
|
) |
261 |
0 |
vmi_name = self.get_vmi_name(switch_id, switch_port, vlan) |
262 |
0 |
vpg_name = self.get_vpg_name(switch_id, switch_port) |
263 |
0 |
profile_dict = { |
264 |
|
"local_link_information": [ |
265 |
|
{ |
266 |
|
"port_id": switch_port.replace(":", "_"), |
267 |
|
"switch_id": switch_port.replace(":", "_"), |
268 |
|
"switch_info": switch_id, |
269 |
|
"fabric": self.fabric, |
270 |
|
} |
271 |
|
] |
272 |
|
} |
273 |
0 |
vmi_dict = { |
274 |
|
"virtual-machine-interface": { |
275 |
|
"parent_type": "project", |
276 |
|
"fq_name": [self.domain, self.project, vmi_name], |
277 |
|
"virtual_network_refs": [{"to": [self.domain, self.project, network]}], |
278 |
|
"virtual_machine_interface_properties": { |
279 |
|
"sub_interface_vlan_tag": vlan |
280 |
|
}, |
281 |
|
"virtual_machine_interface_bindings": { |
282 |
|
"key_value_pair": [ |
283 |
|
{"key": "vnic_type", "value": "baremetal"}, |
284 |
|
{"key": "vif_type", "value": "vrouter"}, |
285 |
|
{"key": "vpg", "value": vpg_name}, |
286 |
|
{"key": "profile", "value": json.dumps(profile_dict)}, |
287 |
|
] |
288 |
|
}, |
289 |
|
} |
290 |
|
} |
291 |
0 |
endpoint = self.controller_url + "virtual-machine-interfaces" |
292 |
0 |
self.logger.debug("vmi_dict: {}".format(vmi_dict)) |
293 |
0 |
resp = self.http.post_cmd( |
294 |
|
url=endpoint, |
295 |
|
headers=self.http_header, |
296 |
|
post_fields_dict=vmi_dict, |
297 |
|
) |
298 |
|
|
299 |
0 |
if not resp: |
300 |
0 |
raise SdnConnectorError("Error creating vmi: empty response") |
301 |
|
|
302 |
0 |
vmi_info = json.loads(resp) |
303 |
0 |
self.logger.debug("created vmi, info: {}".format(vmi_info)) |
304 |
|
|
305 |
0 |
return vmi_info.get("virtual-machine-interface").get("uuid"), vmi_info.get( |
306 |
|
"virtual-machine-interface" |
307 |
|
) |
308 |
|
|
309 |
1 |
def get_vmi(self, vmi_uuid): |
310 |
0 |
return self.get_by_uuid( |
311 |
|
self.controller_url, "virtual-machine-interface", vmi_uuid |
312 |
|
) |
313 |
|
|
314 |
1 |
def delete_vmi(self, uuid): |
315 |
0 |
self.logger.debug("delete vmi uuid: {}".format(uuid)) |
316 |
0 |
self.delete_by_uuid(self.controller_url, "virtual-machine-interface", uuid) |
317 |
0 |
self.logger.debug("deleted vmi: {}".format(uuid)) |
318 |
|
|
319 |
1 |
def unref_vmi_vpg(self, vpg_id, vmi_id, vmi_fq_name): |
320 |
0 |
self.delete_ref( |
321 |
|
"virtual-port-group", |
322 |
|
vpg_id, |
323 |
|
"virtual-machine-interface", |
324 |
|
vmi_id, |
325 |
|
vmi_fq_name, |
326 |
|
) |