1 |
|
# Copyright 2018 Telefonica |
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 |
0 |
""" |
18 |
|
OSM vnfd API handling |
19 |
|
""" |
20 |
|
|
21 |
0 |
from osmclient.common.exceptions import NotFound |
22 |
0 |
from osmclient.common.exceptions import ClientException |
23 |
0 |
from osmclient.common import utils |
24 |
0 |
import json |
25 |
0 |
import yaml |
26 |
0 |
import magic |
27 |
0 |
from os.path import basename |
28 |
0 |
import logging |
29 |
0 |
import os.path |
30 |
0 |
from urllib.parse import quote |
31 |
0 |
import tarfile |
32 |
0 |
from osm_im.validation import Validation as validation_im |
33 |
|
|
34 |
|
|
35 |
0 |
class Vnfd(object): |
36 |
0 |
def __init__(self, http=None, client=None): |
37 |
0 |
self._http = http |
38 |
0 |
self._client = client |
39 |
0 |
self._logger = logging.getLogger("osmclient") |
40 |
0 |
self._apiName = "/vnfpkgm" |
41 |
0 |
self._apiVersion = "/v1" |
42 |
0 |
self._apiResource = "/vnf_packages" |
43 |
0 |
self._apiBase = "{}{}{}".format( |
44 |
|
self._apiName, self._apiVersion, self._apiResource |
45 |
|
) |
46 |
|
|
47 |
0 |
def list(self, filter=None): |
48 |
0 |
self._logger.debug("") |
49 |
0 |
self._client.get_token() |
50 |
0 |
filter_string = "" |
51 |
0 |
if filter: |
52 |
0 |
filter_string = "?{}".format(filter) |
53 |
0 |
_, resp = self._http.get2_cmd("{}{}".format(self._apiBase, filter_string)) |
54 |
|
|
55 |
0 |
if resp: |
56 |
0 |
return json.loads(resp) |
57 |
0 |
return list() |
58 |
|
|
59 |
0 |
def get(self, name): |
60 |
0 |
self._logger.debug("") |
61 |
0 |
self._client.get_token() |
62 |
0 |
if utils.validate_uuid4(name): |
63 |
0 |
for vnfd in self.list(): |
64 |
0 |
if name == vnfd["_id"]: |
65 |
0 |
return vnfd |
66 |
|
else: |
67 |
0 |
for vnfd in self.list(): |
68 |
0 |
if "product-name" in vnfd and name == vnfd["product-name"]: |
69 |
0 |
return vnfd |
70 |
0 |
elif "name" in vnfd and name == vnfd["name"]: |
71 |
0 |
return vnfd |
72 |
0 |
raise NotFound("vnfd {} not found".format(name)) |
73 |
|
|
74 |
0 |
def get_individual(self, name): |
75 |
0 |
self._logger.debug("") |
76 |
0 |
vnfd = self.get(name) |
77 |
|
# It is redundant, since the previous one already gets the whole vnfpkginfo |
78 |
|
# The only difference is that a different primitive is exercised |
79 |
0 |
try: |
80 |
0 |
_, resp = self._http.get2_cmd("{}/{}".format(self._apiBase, vnfd["_id"])) |
81 |
0 |
if resp: |
82 |
0 |
return json.loads(resp) |
83 |
0 |
except NotFound: |
84 |
0 |
raise NotFound("vnfd '{}' not found".format(name)) |
85 |
0 |
raise NotFound("vnfd '{}' not found".format(name)) |
86 |
|
|
87 |
0 |
def get_thing(self, name, thing, filename): |
88 |
0 |
self._logger.debug("") |
89 |
0 |
vnfd = self.get(name) |
90 |
0 |
headers = self._client._headers |
91 |
0 |
headers["Accept"] = "application/binary" |
92 |
0 |
http_code, resp = self._http.get2_cmd( |
93 |
|
"{}/{}/{}".format(self._apiBase, vnfd["_id"], thing) |
94 |
|
) |
95 |
|
|
96 |
0 |
if resp: |
97 |
0 |
return json.loads(resp) |
98 |
|
|
99 |
0 |
def get_descriptor(self, name, filename): |
100 |
0 |
self._logger.debug("") |
101 |
0 |
self.get_thing(name, "vnfd", filename) |
102 |
|
|
103 |
0 |
def get_package(self, name, filename): |
104 |
0 |
self._logger.debug("") |
105 |
0 |
self.get_thing(name, "package_content", filename) |
106 |
|
|
107 |
0 |
def get_artifact(self, name, artifact, filename): |
108 |
0 |
self._logger.debug("") |
109 |
0 |
self.get_thing(name, "artifacts/{}".format(artifact), filename) |
110 |
|
|
111 |
0 |
def delete(self, name, force=False): |
112 |
0 |
self._logger.debug("") |
113 |
0 |
self._client.get_token() |
114 |
0 |
vnfd = self.get(name) |
115 |
0 |
querystring = "" |
116 |
0 |
if force: |
117 |
0 |
querystring = "?FORCE=True" |
118 |
0 |
http_code, resp = self._http.delete_cmd( |
119 |
|
"{}/{}{}".format(self._apiBase, vnfd["_id"], querystring) |
120 |
|
) |
121 |
|
|
122 |
0 |
if http_code == 202: |
123 |
0 |
print("Deletion in progress") |
124 |
0 |
elif http_code == 204: |
125 |
0 |
print("Deleted") |
126 |
|
else: |
127 |
0 |
msg = resp or "" |
128 |
0 |
raise ClientException("failed to delete vnfd {} - {}".format(name, msg)) |
129 |
|
|
130 |
0 |
def create( |
131 |
|
self, |
132 |
|
filename, |
133 |
|
overwrite=None, |
134 |
|
update_endpoint=None, |
135 |
|
skip_charm_build=False, |
136 |
|
override_epa=False, |
137 |
|
override_nonepa=False, |
138 |
|
override_paravirt=False, |
139 |
|
): |
140 |
0 |
self._logger.debug("") |
141 |
0 |
if os.path.isdir(filename): |
142 |
0 |
filename = filename.rstrip("/") |
143 |
0 |
filename = self._client.package_tool.build( |
144 |
|
filename, skip_validation=False, skip_charm_build=skip_charm_build |
145 |
|
) |
146 |
|
|
147 |
0 |
print("Uploading package {}".format(filename)) |
148 |
0 |
self.create( |
149 |
|
filename, |
150 |
|
overwrite=overwrite, |
151 |
|
update_endpoint=update_endpoint, |
152 |
|
override_epa=override_epa, |
153 |
|
override_nonepa=override_nonepa, |
154 |
|
override_paravirt=override_paravirt, |
155 |
|
) |
156 |
|
else: |
157 |
0 |
self._client.get_token() |
158 |
0 |
mime_type = magic.from_file(filename, mime=True) |
159 |
0 |
if mime_type is None: |
160 |
0 |
raise ClientException( |
161 |
|
"Unexpected MIME type for file {}: MIME type {}".format( |
162 |
|
filename, mime_type |
163 |
|
) |
164 |
|
) |
165 |
0 |
headers = self._client._headers |
166 |
0 |
headers["Content-Filename"] = basename(filename) |
167 |
0 |
if mime_type in ["application/yaml", "text/plain", "application/json"]: |
168 |
0 |
headers["Content-Type"] = "text/plain" |
169 |
0 |
elif mime_type in ["application/gzip", "application/x-gzip"]: |
170 |
0 |
headers["Content-Type"] = "application/gzip" |
171 |
|
else: |
172 |
0 |
raise ClientException( |
173 |
|
"Unexpected MIME type for file {}: MIME type {}".format( |
174 |
|
filename, mime_type |
175 |
|
) |
176 |
|
) |
177 |
|
|
178 |
0 |
special_override_string = "" |
179 |
0 |
if override_epa or override_nonepa or override_paravirt: |
180 |
|
# If override for EPA, non-EPA or paravirt is required, get the descriptor data |
181 |
0 |
descriptor_data = None |
182 |
0 |
if mime_type in ["application/yaml", "text/plain", "application/json"]: |
183 |
0 |
with open(filename) as df: |
184 |
0 |
descriptor_data = df.read() |
185 |
0 |
elif mime_type in ["application/gzip", "application/x-gzip"]: |
186 |
0 |
tar_object = tarfile.open(filename, "r:gz") |
187 |
0 |
descriptor_list = [] |
188 |
0 |
for member in tar_object: |
189 |
0 |
if member.isreg(): |
190 |
0 |
if "/" not in os.path.dirname( |
191 |
|
member.name |
192 |
|
) and member.name.endswith(".yaml"): |
193 |
0 |
descriptor_list.append(member.name) |
194 |
0 |
if len(descriptor_list) > 1: |
195 |
0 |
raise ClientException( |
196 |
|
"Found more than one potential descriptor in the tar.gz file" |
197 |
|
) |
198 |
0 |
elif len(descriptor_list) == 0: |
199 |
0 |
raise ClientException( |
200 |
|
"No descriptor was found in the tar.gz file" |
201 |
|
) |
202 |
0 |
with tar_object.extractfile(descriptor_list[0]) as df: |
203 |
0 |
descriptor_data = df.read() |
204 |
0 |
tar_object.close() |
205 |
0 |
if not descriptor_data: |
206 |
0 |
raise ClientException("Descriptor could not be read") |
207 |
0 |
desc_type, vnfd = validation_im().yaml_validation(descriptor_data) |
208 |
0 |
validation_im().pyangbind_validation(desc_type, vnfd) |
209 |
|
|
210 |
0 |
vnfd = yaml.safe_load(descriptor_data) |
211 |
0 |
vcd_list = [] |
212 |
0 |
vdu_list = [] |
213 |
0 |
for k in vnfd: |
214 |
|
# Get only the first descriptor in case there are many in the yaml file |
215 |
|
# k can be vnfd or etsi-nfv-vnfd:vnfd. This check is skipped |
216 |
0 |
first_vnfd = vnfd.get(k, {}) |
217 |
0 |
vcd_list = first_vnfd.get("virtual-compute-desc", []) |
218 |
0 |
vdu_list = first_vnfd.get("vdu", []) |
219 |
0 |
break |
220 |
|
|
221 |
0 |
for vcd_number, vcd in enumerate(vcd_list): |
222 |
0 |
if override_epa: |
223 |
0 |
virtual_memory = vcd["virtual-memory"] |
224 |
0 |
virtual_memory["mempage-size"] = "LARGE" |
225 |
0 |
virtual_memory["numa-enabled"] = True |
226 |
0 |
virtual_memory["numa-node-policy"] = { |
227 |
|
"node-cnt": 1, |
228 |
|
"mem-policy": "STRICT", |
229 |
|
} |
230 |
0 |
virtual_cpu = vcd["virtual-cpu"] |
231 |
0 |
virtual_cpu["pinning"] = { |
232 |
|
"policy": "static", |
233 |
|
"thread-policy": "PREFER", |
234 |
|
} |
235 |
|
|
236 |
0 |
cpu_override_string = ( |
237 |
|
"virtual-compute-desc.{}.virtual-cpu={};".format( |
238 |
|
vcd_number, quote(yaml.safe_dump(virtual_cpu)) |
239 |
|
) |
240 |
|
) |
241 |
0 |
memory_override_string = ( |
242 |
|
"virtual-compute-desc.{}.virtual-memory={};".format( |
243 |
|
vcd_number, quote(yaml.safe_dump(virtual_memory)) |
244 |
|
) |
245 |
|
) |
246 |
0 |
special_override_string = "{}{}{}".format( |
247 |
|
special_override_string, |
248 |
|
cpu_override_string, |
249 |
|
memory_override_string, |
250 |
|
) |
251 |
|
|
252 |
0 |
headers["Query-String-Format"] = "yaml" |
253 |
0 |
if override_nonepa: |
254 |
0 |
virtual_memory = vcd["virtual-memory"] |
255 |
0 |
virtual_memory["mempage-size"] = "" |
256 |
0 |
virtual_memory["numa-enabled"] = "" |
257 |
0 |
virtual_memory["numa-node-policy"] = {} |
258 |
0 |
virtual_cpu = vcd["virtual-cpu"] |
259 |
0 |
virtual_cpu["pinning"] = {} |
260 |
|
|
261 |
0 |
cpu_override_string = ( |
262 |
|
"virtual-compute-desc.{}.virtual-cpu={};".format( |
263 |
|
vcd_number, quote(yaml.safe_dump(virtual_cpu)) |
264 |
|
) |
265 |
|
) |
266 |
0 |
memory_override_string = ( |
267 |
|
"virtual-compute-desc.{}.virtual-memory={};".format( |
268 |
|
vcd_number, quote(yaml.safe_dump(virtual_memory)) |
269 |
|
) |
270 |
|
) |
271 |
0 |
special_override_string = "{}{}{}".format( |
272 |
|
special_override_string, |
273 |
|
cpu_override_string, |
274 |
|
memory_override_string, |
275 |
|
) |
276 |
|
|
277 |
0 |
if override_paravirt: |
278 |
0 |
for vdu_number, vdu in enumerate(vdu_list): |
279 |
0 |
for cpd_number, cpd in enumerate(vdu["int-cpd"]): |
280 |
0 |
for vnir_number, vnir in enumerate( |
281 |
|
cpd["virtual-network-interface-requirement"] |
282 |
|
): |
283 |
0 |
special_override_string = ( |
284 |
|
"{}vdu.{}.int-cpd.{}.virtual-network-interface-" |
285 |
|
"requirement.{}.virtual-interface.type=" |
286 |
|
"PARAVIRT;".format( |
287 |
|
special_override_string, |
288 |
|
vdu_number, |
289 |
|
cpd_number, |
290 |
|
vnir_number, |
291 |
|
) |
292 |
|
) |
293 |
|
|
294 |
0 |
special_override_string = special_override_string.rstrip(";") |
295 |
|
|
296 |
0 |
headers["Content-File-MD5"] = utils.md5(filename) |
297 |
0 |
http_header = [ |
298 |
|
"{}: {}".format(key, val) for (key, val) in list(headers.items()) |
299 |
|
] |
300 |
|
|
301 |
0 |
self._http.set_http_header(http_header) |
302 |
0 |
if update_endpoint: |
303 |
0 |
http_code, resp = self._http.put_cmd( |
304 |
|
endpoint=update_endpoint, filename=filename |
305 |
|
) |
306 |
|
else: |
307 |
0 |
ow_string = "" |
308 |
0 |
if special_override_string: |
309 |
0 |
if overwrite: |
310 |
0 |
overwrite = "{};{}".format(overwrite, special_override_string) |
311 |
|
else: |
312 |
0 |
overwrite = special_override_string |
313 |
|
|
314 |
0 |
if overwrite: |
315 |
0 |
ow_string = "?{}".format(overwrite) |
316 |
0 |
self._apiResource = "/vnf_packages_content" |
317 |
0 |
self._apiBase = "{}{}{}".format( |
318 |
|
self._apiName, self._apiVersion, self._apiResource |
319 |
|
) |
320 |
0 |
endpoint = "{}{}".format(self._apiBase, ow_string) |
321 |
0 |
http_code, resp = self._http.post_cmd( |
322 |
|
endpoint=endpoint, filename=filename |
323 |
|
) |
324 |
|
|
325 |
0 |
if http_code in (200, 201, 202): |
326 |
0 |
if resp: |
327 |
0 |
resp = json.loads(resp) |
328 |
0 |
if not resp or "id" not in resp: |
329 |
0 |
raise ClientException( |
330 |
|
"unexpected response from server: {}".format(resp) |
331 |
|
) |
332 |
0 |
print(resp["id"]) |
333 |
0 |
elif http_code == 204: |
334 |
0 |
print("Updated") |
335 |
|
|
336 |
0 |
def update(self, name, filename): |
337 |
0 |
self._logger.debug("") |
338 |
0 |
self._client.get_token() |
339 |
0 |
vnfd = self.get(name) |
340 |
0 |
endpoint = "{}/{}/package_content".format(self._apiBase, vnfd["_id"]) |
341 |
0 |
self.create(filename=filename, update_endpoint=endpoint) |