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 |
|
|
37 |
0 |
def __init__(self, http=None, client=None): |
38 |
0 |
self._http = http |
39 |
0 |
self._client = client |
40 |
0 |
self._logger = logging.getLogger('osmclient') |
41 |
0 |
self._apiName = '/vnfpkgm' |
42 |
0 |
self._apiVersion = '/v1' |
43 |
0 |
self._apiResource = '/vnf_packages' |
44 |
0 |
self._apiBase = '{}{}{}'.format(self._apiName, |
45 |
|
self._apiVersion, self._apiResource) |
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('{}/{}/{}'.format(self._apiBase, vnfd['_id'], thing)) |
93 |
|
|
94 |
0 |
if resp: |
95 |
0 |
return json.loads(resp) |
96 |
|
|
97 |
0 |
def get_descriptor(self, name, filename): |
98 |
0 |
self._logger.debug("") |
99 |
0 |
self.get_thing(name, 'vnfd', filename) |
100 |
|
|
101 |
0 |
def get_package(self, name, filename): |
102 |
0 |
self._logger.debug("") |
103 |
0 |
self.get_thing(name, 'package_content', filename) |
104 |
|
|
105 |
0 |
def get_artifact(self, name, artifact, filename): |
106 |
0 |
self._logger.debug("") |
107 |
0 |
self.get_thing(name, 'artifacts/{}'.format(artifact), filename) |
108 |
|
|
109 |
0 |
def delete(self, name, force=False): |
110 |
0 |
self._logger.debug("") |
111 |
0 |
self._client.get_token() |
112 |
0 |
vnfd = self.get(name) |
113 |
0 |
querystring = '' |
114 |
0 |
if force: |
115 |
0 |
querystring = '?FORCE=True' |
116 |
0 |
http_code, resp = self._http.delete_cmd('{}/{}{}'.format(self._apiBase, |
117 |
|
vnfd['_id'], querystring)) |
118 |
|
|
119 |
0 |
if http_code == 202: |
120 |
0 |
print('Deletion in progress') |
121 |
0 |
elif http_code == 204: |
122 |
0 |
print('Deleted') |
123 |
|
else: |
124 |
0 |
msg = resp or "" |
125 |
0 |
raise ClientException("failed to delete vnfd {} - {}".format(name, msg)) |
126 |
|
|
127 |
0 |
def create(self, filename, overwrite=None, update_endpoint=None, skip_charm_build=False, |
128 |
|
override_epa=False, override_nonepa=False, override_paravirt=False): |
129 |
0 |
self._logger.debug("") |
130 |
0 |
if os.path.isdir(filename): |
131 |
0 |
filename = filename.rstrip('/') |
132 |
0 |
filename = self._client.package_tool.build(filename, skip_validation=False, |
133 |
|
skip_charm_build=skip_charm_build) |
134 |
|
|
135 |
0 |
print('Uploading package {}'.format(filename)) |
136 |
0 |
self.create(filename, overwrite=overwrite, update_endpoint=update_endpoint, |
137 |
|
override_epa=override_epa, override_nonepa=override_nonepa, |
138 |
|
override_paravirt=override_paravirt) |
139 |
|
else: |
140 |
0 |
self._client.get_token() |
141 |
0 |
mime_type = magic.from_file(filename, mime=True) |
142 |
0 |
if mime_type is None: |
143 |
0 |
raise ClientException( |
144 |
|
"Unexpected MIME type for file {}: MIME type {}".format( |
145 |
|
filename, mime_type) |
146 |
|
) |
147 |
0 |
headers = self._client._headers |
148 |
0 |
headers['Content-Filename'] = basename(filename) |
149 |
0 |
if mime_type in ['application/yaml', 'text/plain', 'application/json']: |
150 |
0 |
headers['Content-Type'] = 'text/plain' |
151 |
0 |
elif mime_type in ['application/gzip', 'application/x-gzip']: |
152 |
0 |
headers['Content-Type'] = 'application/gzip' |
153 |
|
else: |
154 |
0 |
raise ClientException( |
155 |
|
"Unexpected MIME type for file {}: MIME type {}".format( |
156 |
|
filename, mime_type) |
157 |
|
) |
158 |
|
|
159 |
0 |
special_override_string = '' |
160 |
0 |
if override_epa or override_nonepa or override_paravirt: |
161 |
|
# If override for EPA, non-EPA or paravirt is required, get the descriptor data |
162 |
0 |
descriptor_data = None |
163 |
0 |
if mime_type in ['application/yaml', 'text/plain', 'application/json']: |
164 |
0 |
with open(filename) as df: |
165 |
0 |
descriptor_data = df.read() |
166 |
0 |
elif mime_type in ['application/gzip', 'application/x-gzip']: |
167 |
0 |
tar_object = tarfile.open(filename, "r:gz") |
168 |
0 |
descriptor_list = [] |
169 |
0 |
for member in tar_object: |
170 |
0 |
if member.isreg(): |
171 |
0 |
if '/' not in os.path.dirname(member.name) and member.name.endswith('.yaml'): |
172 |
0 |
descriptor_list.append(member.name) |
173 |
0 |
if len(descriptor_list) > 1: |
174 |
0 |
raise ClientException('Found more than one potential descriptor in the tar.gz file') |
175 |
0 |
elif len(descriptor_list) == 0: |
176 |
0 |
raise ClientException('No descriptor was found in the tar.gz file') |
177 |
0 |
with tar_object.extractfile(descriptor_list[0]) as df: |
178 |
0 |
descriptor_data = df.read() |
179 |
0 |
tar_object.close() |
180 |
0 |
if not descriptor_data: |
181 |
0 |
raise ClientException('Descriptor could not be read') |
182 |
0 |
desc_type, vnfd = validation_im().yaml_validation(descriptor_data) |
183 |
0 |
validation_im().pyangbind_validation(desc_type, vnfd) |
184 |
|
|
185 |
0 |
vnfd = yaml.safe_load(descriptor_data) |
186 |
0 |
vcd_list = [] |
187 |
0 |
vdu_list = [] |
188 |
0 |
for k in vnfd: |
189 |
|
# Get only the first descriptor in case there are many in the yaml file |
190 |
|
# k can be vnfd or etsi-nfv-vnfd:vnfd. This check is skipped |
191 |
0 |
first_vnfd = vnfd.get(k, {}) |
192 |
0 |
vcd_list = first_vnfd.get('virtual-compute-desc', []) |
193 |
0 |
vdu_list = first_vnfd.get('vdu', []) |
194 |
0 |
break |
195 |
|
|
196 |
0 |
for vcd_number, vcd in enumerate(vcd_list): |
197 |
0 |
if override_epa: |
198 |
0 |
virtual_memory = vcd["virtual-memory"] |
199 |
0 |
virtual_memory["mempage-size"] = "LARGE" |
200 |
0 |
virtual_memory["numa-enabled"] = True |
201 |
0 |
virtual_memory["numa-node-policy"] = { |
202 |
|
"node-cnt": 1, |
203 |
|
"mem-policy": "STRICT" |
204 |
|
} |
205 |
0 |
virtual_cpu = vcd["virtual-cpu"] |
206 |
0 |
virtual_cpu["pinning"] = { |
207 |
|
"policy": "static", |
208 |
|
"thread-policy": "PREFER" |
209 |
|
} |
210 |
|
|
211 |
0 |
cpu_override_string = "virtual-compute-desc.{}.virtual-cpu={};"\ |
212 |
|
.format(vcd_number, quote(yaml.safe_dump(virtual_cpu))) |
213 |
0 |
memory_override_string = "virtual-compute-desc.{}.virtual-memory={};"\ |
214 |
|
.format(vcd_number, quote(yaml.safe_dump(virtual_memory))) |
215 |
0 |
special_override_string = "{}{}{}".format(special_override_string, |
216 |
|
cpu_override_string, memory_override_string) |
217 |
|
|
218 |
0 |
headers['Query-String-Format'] = 'yaml' |
219 |
0 |
if override_nonepa: |
220 |
0 |
virtual_memory = vcd["virtual-memory"] |
221 |
0 |
virtual_memory["mempage-size"] = "" |
222 |
0 |
virtual_memory["numa-enabled"] = "" |
223 |
0 |
virtual_memory["numa-node-policy"] = {} |
224 |
0 |
virtual_cpu = vcd["virtual-cpu"] |
225 |
0 |
virtual_cpu["pinning"] = {} |
226 |
|
|
227 |
0 |
cpu_override_string = "virtual-compute-desc.{}.virtual-cpu={};"\ |
228 |
|
.format(vcd_number, quote(yaml.safe_dump(virtual_cpu))) |
229 |
0 |
memory_override_string = "virtual-compute-desc.{}.virtual-memory={};"\ |
230 |
|
.format(vcd_number, quote(yaml.safe_dump(virtual_memory))) |
231 |
0 |
special_override_string = "{}{}{}".format(special_override_string, |
232 |
|
cpu_override_string, memory_override_string) |
233 |
|
|
234 |
0 |
if override_paravirt: |
235 |
0 |
for vdu_number, vdu in enumerate(vdu_list): |
236 |
0 |
for cpd_number, cpd in enumerate(vdu["int-cpd"]): |
237 |
0 |
for vnir_number, vnir in enumerate(cpd['virtual-network-interface-requirement']): |
238 |
0 |
special_override_string = "{}vdu.{}.int-cpd.{}.virtual-network-interface-" \ |
239 |
|
"requirement.{}.virtual-interface.type=PARAVIRT;"\ |
240 |
|
.format(special_override_string, vdu_number, cpd_number, vnir_number) |
241 |
|
|
242 |
0 |
special_override_string = special_override_string.rstrip(";") |
243 |
|
|
244 |
0 |
headers["Content-File-MD5"] = utils.md5(filename) |
245 |
0 |
http_header = ['{}: {}'.format(key, val) |
246 |
|
for (key, val) in list(headers.items())] |
247 |
|
|
248 |
0 |
self._http.set_http_header(http_header) |
249 |
0 |
if update_endpoint: |
250 |
0 |
http_code, resp = self._http.put_cmd(endpoint=update_endpoint, filename=filename) |
251 |
|
else: |
252 |
0 |
ow_string = '' |
253 |
0 |
if special_override_string: |
254 |
0 |
if overwrite: |
255 |
0 |
overwrite = "{};{}".format(overwrite, special_override_string) |
256 |
|
else: |
257 |
0 |
overwrite = special_override_string |
258 |
|
|
259 |
0 |
if overwrite: |
260 |
0 |
ow_string = '?{}'.format(overwrite) |
261 |
0 |
self._apiResource = '/vnf_packages_content' |
262 |
0 |
self._apiBase = '{}{}{}'.format(self._apiName, |
263 |
|
self._apiVersion, self._apiResource) |
264 |
0 |
endpoint = '{}{}'.format(self._apiBase, ow_string) |
265 |
0 |
http_code, resp = self._http.post_cmd(endpoint=endpoint, filename=filename) |
266 |
|
|
267 |
0 |
if http_code in (200, 201, 202): |
268 |
0 |
if resp: |
269 |
0 |
resp = json.loads(resp) |
270 |
0 |
if not resp or 'id' not in resp: |
271 |
0 |
raise ClientException('unexpected response from server: {}'.format(resp)) |
272 |
0 |
print(resp['id']) |
273 |
0 |
elif http_code == 204: |
274 |
0 |
print('Updated') |
275 |
|
|
276 |
0 |
def update(self, name, filename): |
277 |
0 |
self._logger.debug("") |
278 |
0 |
self._client.get_token() |
279 |
0 |
vnfd = self.get(name) |
280 |
0 |
endpoint = '{}/{}/package_content'.format(self._apiBase, vnfd['_id']) |
281 |
0 |
self.create(filename=filename, update_endpoint=endpoint) |