Code Coverage

Cobertura Coverage Report > osm_nbi >

descriptor_topics.py

Trend

Classes100%
 
Lines64%
   
Conditionals100%
 

File Coverage summary

NameClassesLinesConditionals
descriptor_topics.py
100%
1/1
64%
486/765
100%
0/0

Coverage Breakdown by Class

NameLinesConditionals
descriptor_topics.py
64%
486/765
N/A

Source

osm_nbi/descriptor_topics.py
1 # -*- coding: utf-8 -*-
2
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 #    http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
12 # implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15
16 1 import tarfile
17 1 import yaml
18 1 import json
19 1 import importlib
20 # import logging
21 1 from hashlib import md5
22 1 from osm_common.dbbase import DbException, deep_update_rfc7396
23 1 from http import HTTPStatus
24 1 from time import time
25 1 from uuid import uuid4
26 1 from re import fullmatch
27 1 from osm_nbi.validation import ValidationError, pdu_new_schema, pdu_edit_schema, \
28     validate_input, vnfpkgop_new_schema
29 1 from osm_nbi.base_topic import BaseTopic, EngineException, get_iterable
30 1 etsi_nfv_vnfd = importlib.import_module("osm_im.etsi-nfv-vnfd")
31 1 etsi_nfv_nsd = importlib.import_module("osm_im.etsi-nfv-nsd")
32 1 from osm_im.nst import nst as nst_im
33 1 from pyangbind.lib.serialise import pybindJSONDecoder
34 1 import pyangbind.lib.pybindJSON as pybindJSON
35
36 1 __author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
37
38
39 1 class DescriptorTopic(BaseTopic):
40
41 1     def __init__(self, db, fs, msg, auth):
42 1         BaseTopic.__init__(self, db, fs, msg, auth)
43
44 1     def check_conflict_on_edit(self, session, final_content, edit_content, _id):
45 1         super().check_conflict_on_edit(session, final_content, edit_content, _id)
46
47 1         def _check_unique_id_name(descriptor, position=""):
48 1             for desc_key, desc_item in descriptor.items():
49 1                 if isinstance(desc_item, list) and desc_item:
50 1                     used_ids = []
51 1                     desc_item_id = None
52 1                     for index, list_item in enumerate(desc_item):
53 1                         if isinstance(list_item, dict):
54 1                             _check_unique_id_name(list_item, "{}.{}[{}]"
55                                                   .format(position, desc_key, index))
56                             # Base case
57 1                             if index == 0 and (list_item.get("id") or list_item.get("name")):
58 1                                 desc_item_id = "id" if list_item.get("id") else "name"
59 1                             if desc_item_id and list_item.get(desc_item_id):
60 1                                 if list_item[desc_item_id] in used_ids:
61 1                                     position = "{}.{}[{}]".format(position, desc_key, index)
62 1                                     raise EngineException("Error: identifier {} '{}' is not unique and repeats at '{}'"
63                                                           .format(desc_item_id, list_item[desc_item_id],
64                                                                   position), HTTPStatus.UNPROCESSABLE_ENTITY)
65 1                                 used_ids.append(list_item[desc_item_id])
66
67 1         _check_unique_id_name(final_content)
68         # 1. validate again with pyangbind
69         # 1.1. remove internal keys
70 1         internal_keys = {}
71 1         for k in ("_id", "_admin"):
72 1             if k in final_content:
73 1                 internal_keys[k] = final_content.pop(k)
74 1         storage_params = internal_keys["_admin"].get("storage")
75 1         serialized = self._validate_input_new(final_content, storage_params, session["force"])
76         # 1.2. modify final_content with a serialized version
77 1         final_content.clear()
78 1         final_content.update(serialized)
79         # 1.3. restore internal keys
80 1         for k, v in internal_keys.items():
81 1             final_content[k] = v
82 1         if session["force"]:
83 0             return
84         # 2. check that this id is not present
85 1         if "id" in edit_content:
86 1             _filter = self._get_project_filter(session)
87 1             _filter["id"] = final_content["id"]
88 1             _filter["_id.neq"] = _id
89 1             if self.db.get_one(self.topic, _filter, fail_on_empty=False):
90 1                 raise EngineException("{} with id '{}' already exists for this project".format(self.topic[:-1],
91                                                                                                final_content["id"]),
92                                       HTTPStatus.CONFLICT)
93
94 1     @staticmethod
95 1     def format_on_new(content, project_id=None, make_public=False):
96 1         BaseTopic.format_on_new(content, project_id=project_id, make_public=make_public)
97 1         content["_admin"]["onboardingState"] = "CREATED"
98 1         content["_admin"]["operationalState"] = "DISABLED"
99 1         content["_admin"]["usageState"] = "NOT_IN_USE"
100
101 1     def delete_extra(self, session, _id, db_content, not_send_msg=None):
102         """
103         Deletes file system storage associated with the descriptor
104         :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
105         :param _id: server internal id
106         :param db_content: The database content of the descriptor
107         :param not_send_msg: To not send message (False) or store content (list) instead
108         :return: None if ok or raises EngineException with the problem
109         """
110 1         self.fs.file_delete(_id, ignore_non_exist=True)
111 1         self.fs.file_delete(_id + "_", ignore_non_exist=True)  # remove temp folder
112
113 1     @staticmethod
114     def get_one_by_id(db, session, topic, id):
115         # find owned by this project
116 0         _filter = BaseTopic._get_project_filter(session)
117 0         _filter["id"] = id
118 0         desc_list = db.get_list(topic, _filter)
119 0         if len(desc_list) == 1:
120 0             return desc_list[0]
121 0         elif len(desc_list) > 1:
122 0             raise DbException("Found more than one {} with id='{}' belonging to this project".format(topic[:-1], id),
123                               HTTPStatus.CONFLICT)
124
125         # not found any: try to find public
126 0         _filter = BaseTopic._get_project_filter(session)
127 0         _filter["id"] = id
128 0         desc_list = db.get_list(topic, _filter)
129 0         if not desc_list:
130 0             raise DbException("Not found any {} with id='{}'".format(topic[:-1], id), HTTPStatus.NOT_FOUND)
131 0         elif len(desc_list) == 1:
132 0             return desc_list[0]
133         else:
134 0             raise DbException("Found more than one public {} with id='{}'; and no one belonging to this project".format(
135                 topic[:-1], id), HTTPStatus.CONFLICT)
136
137 1     def new(self, rollback, session, indata=None, kwargs=None, headers=None):
138         """
139         Creates a new almost empty DISABLED  entry into database. Due to SOL005, it does not follow normal procedure.
140         Creating a VNFD or NSD is done in two steps: 1. Creates an empty descriptor (this step) and 2) upload content
141         (self.upload_content)
142         :param rollback: list to append created items at database in case a rollback may to be done
143         :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
144         :param indata: data to be inserted
145         :param kwargs: used to override the indata descriptor
146         :param headers: http request headers
147         :return: _id, None: identity of the inserted data; and None as there is not any operation
148         """
149
150         # No needed to capture exceptions
151         # Check Quota
152 1         self.check_quota(session)
153
154         # _remove_envelop
155 1         if indata:
156 0             if "userDefinedData" in indata:
157 0                 indata = indata['userDefinedData']
158
159         # Override descriptor with query string kwargs
160 1         self._update_input_with_kwargs(indata, kwargs)
161         # uncomment when this method is implemented.
162         # Avoid override in this case as the target is userDefinedData, but not vnfd,nsd descriptors
163         # indata = DescriptorTopic._validate_input_new(self, indata, project_id=session["force"])
164
165 1         content = {"_admin": {"userDefinedData": indata}}
166 1         self.format_on_new(content, session["project_id"], make_public=session["public"])
167 1         _id = self.db.create(self.topic, content)
168 1         rollback.append({"topic": self.topic, "_id": _id})
169 1         self._send_msg("created", {"_id": _id})
170 1         return _id, None
171
172 1     def upload_content(self, session, _id, indata, kwargs, headers):
173         """
174         Used for receiving content by chunks (with a transaction_id header and/or gzip file. It will store and extract)
175         :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
176         :param _id : the nsd,vnfd is already created, this is the id
177         :param indata: http body request
178         :param kwargs: user query string to override parameters. NOT USED
179         :param headers:  http request headers
180         :return: True if package is completely uploaded or False if partial content has been uploded
181             Raise exception on error
182         """
183         # Check that _id exists and it is valid
184 1         current_desc = self.show(session, _id)
185
186 1         content_range_text = headers.get("Content-Range")
187 1         expected_md5 = headers.get("Content-File-MD5")
188 1         compressed = None
189 1         content_type = headers.get("Content-Type")
190 1         if content_type and "application/gzip" in content_type or "application/x-gzip" in content_type or \
191                 "application/zip" in content_type:
192 0             compressed = "gzip"
193 1         filename = headers.get("Content-Filename")
194 1         if not filename:
195 1             filename = "package.tar.gz" if compressed else "package"
196         # TODO change to Content-Disposition filename https://tools.ietf.org/html/rfc6266
197 1         file_pkg = None
198 1         error_text = ""
199 1         try:
200 1             if content_range_text:
201 0                 content_range = content_range_text.replace("-", " ").replace("/", " ").split()
202 0                 if content_range[0] != "bytes":  # TODO check x<y not negative < total....
203 0                     raise IndexError()
204 0                 start = int(content_range[1])
205 0                 end = int(content_range[2]) + 1
206 0                 total = int(content_range[3])
207             else:
208 1                 start = 0
209 1             temp_folder = _id + "_"  # all the content is upload here and if ok, it is rename from id_ to is folder
210
211 1             if start:
212 0                 if not self.fs.file_exists(temp_folder, 'dir'):
213 0                     raise EngineException("invalid Transaction-Id header", HTTPStatus.NOT_FOUND)
214             else:
215 1                 self.fs.file_delete(temp_folder, ignore_non_exist=True)
216 1                 self.fs.mkdir(temp_folder)
217
218 1             storage = self.fs.get_params()
219 1             storage["folder"] = _id
220
221 1             file_path = (temp_folder, filename)
222 1             if self.fs.file_exists(file_path, 'file'):
223 0                 file_size = self.fs.file_size(file_path)
224             else:
225 1                 file_size = 0
226 1             if file_size != start:
227 0                 raise EngineException("invalid Content-Range start sequence, expected '{}' but received '{}'".format(
228                     file_size, start), HTTPStatus.REQUESTED_RANGE_NOT_SATISFIABLE)
229 1             file_pkg = self.fs.file_open(file_path, 'a+b')
230 1             if isinstance(indata, dict):
231 1                 indata_text = yaml.safe_dump(indata, indent=4, default_flow_style=False)
232 1                 file_pkg.write(indata_text.encode(encoding="utf-8"))
233             else:
234 0                 indata_len = 0
235 0                 while True:
236 0                     indata_text = indata.read(4096)
237 0                     indata_len += len(indata_text)
238 0                     if not indata_text:
239 0                         break
240 0                     file_pkg.write(indata_text)
241 1             if content_range_text:
242 0                 if indata_len != end - start:
243 0                     raise EngineException("Mismatch between Content-Range header {}-{} and body length of {}".format(
244                         start, end - 1, indata_len), HTTPStatus.REQUESTED_RANGE_NOT_SATISFIABLE)
245 0                 if end != total:
246                     # TODO update to UPLOADING
247 0                     return False
248
249             # PACKAGE UPLOADED
250 1             if expected_md5:
251 0                 file_pkg.seek(0, 0)
252 0                 file_md5 = md5()
253 0                 chunk_data = file_pkg.read(1024)
254 0                 while chunk_data:
255 0                     file_md5.update(chunk_data)
256 0                     chunk_data = file_pkg.read(1024)
257 0                 if expected_md5 != file_md5.hexdigest():
258 0                     raise EngineException("Error, MD5 mismatch", HTTPStatus.CONFLICT)
259 1             file_pkg.seek(0, 0)
260 1             if compressed == "gzip":
261 0                 tar = tarfile.open(mode='r', fileobj=file_pkg)
262 0                 descriptor_file_name = None
263 0                 for tarinfo in tar:
264 0                     tarname = tarinfo.name
265 0                     tarname_path = tarname.split("/")
266 0                     if not tarname_path[0] or ".." in tarname_path:  # if start with "/" means absolute path
267 0                         raise EngineException("Absolute path or '..' are not allowed for package descriptor tar.gz")
268 0                     if len(tarname_path) == 1 and not tarinfo.isdir():
269 0                         raise EngineException("All files must be inside a dir for package descriptor tar.gz")
270 0                     if tarname.endswith(".yaml") or tarname.endswith(".json") or tarname.endswith(".yml"):
271 0                         storage["pkg-dir"] = tarname_path[0]
272 0                         if len(tarname_path) == 2:
273 0                             if descriptor_file_name:
274 0                                 raise EngineException(
275                                     "Found more than one descriptor file at package descriptor tar.gz")
276 0                             descriptor_file_name = tarname
277 0                 if not descriptor_file_name:
278 0                     raise EngineException("Not found any descriptor file at package descriptor tar.gz")
279 0                 storage["descriptor"] = descriptor_file_name
280 0                 storage["zipfile"] = filename
281 0                 self.fs.file_extract(tar, temp_folder)
282 0                 with self.fs.file_open((temp_folder, descriptor_file_name), "r") as descriptor_file:
283 0                     content = descriptor_file.read()
284             else:
285 1                 content = file_pkg.read()
286 1                 storage["descriptor"] = descriptor_file_name = filename
287
288 1             if descriptor_file_name.endswith(".json"):
289 0                 error_text = "Invalid json format "
290 0                 indata = json.load(content)
291             else:
292 1                 error_text = "Invalid yaml format "
293 1                 indata = yaml.load(content, Loader=yaml.SafeLoader)
294
295 1             current_desc["_admin"]["storage"] = storage
296 1             current_desc["_admin"]["onboardingState"] = "ONBOARDED"
297 1             current_desc["_admin"]["operationalState"] = "ENABLED"
298
299 1             indata = self._remove_envelop(indata)
300
301             # Override descriptor with query string kwargs
302 1             if kwargs:
303 0                 self._update_input_with_kwargs(indata, kwargs)
304
305 1             deep_update_rfc7396(current_desc, indata)
306 1             self.check_conflict_on_edit(session, current_desc, indata, _id=_id)
307 1             current_desc["_admin"]["modified"] = time()
308 1             self.db.replace(self.topic, _id, current_desc)
309 1             self.fs.dir_rename(temp_folder, _id)
310
311 1             indata["_id"] = _id
312 1             self._send_msg("edited", indata)
313
314             # TODO if descriptor has changed because kwargs update content and remove cached zip
315             # TODO if zip is not present creates one
316 1             return True
317
318 1         except EngineException:
319 1             raise
320 0         except IndexError:
321 0             raise EngineException("invalid Content-Range header format. Expected 'bytes start-end/total'",
322                                   HTTPStatus.REQUESTED_RANGE_NOT_SATISFIABLE)
323 0         except IOError as e:
324 0             raise EngineException("invalid upload transaction sequence: '{}'".format(e), HTTPStatus.BAD_REQUEST)
325 0         except tarfile.ReadError as e:
326 0             raise EngineException("invalid file content {}".format(e), HTTPStatus.BAD_REQUEST)
327 0         except (ValueError, yaml.YAMLError) as e:
328 0             raise EngineException(error_text + str(e))
329 0         except ValidationError as e:
330 0             raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
331         finally:
332 1             if file_pkg:
333 1                 file_pkg.close()
334
335 1     def get_file(self, session, _id, path=None, accept_header=None):
336         """
337         Return the file content of a vnfd or nsd
338         :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
339         :param _id: Identity of the vnfd, nsd
340         :param path: artifact path or "$DESCRIPTOR" or None
341         :param accept_header: Content of Accept header. Must contain applition/zip or/and text/plain
342         :return: opened file plus Accept format or raises an exception
343         """
344 0         accept_text = accept_zip = False
345 0         if accept_header:
346 0             if 'text/plain' in accept_header or '*/*' in accept_header:
347 0                 accept_text = True
348 0             if 'application/zip' in accept_header or '*/*' in accept_header:
349 0                 accept_zip = 'application/zip'
350 0             elif 'application/gzip' in accept_header:
351 0                 accept_zip = 'application/gzip'
352
353 0         if not accept_text and not accept_zip:
354 0             raise EngineException("provide request header 'Accept' with 'application/zip' or 'text/plain'",
355                                   http_code=HTTPStatus.NOT_ACCEPTABLE)
356
357 0         content = self.show(session, _id)
358 0         if content["_admin"]["onboardingState"] != "ONBOARDED":
359 0             raise EngineException("Cannot get content because this resource is not at 'ONBOARDED' state. "
360                                   "onboardingState is {}".format(content["_admin"]["onboardingState"]),
361                                   http_code=HTTPStatus.CONFLICT)
362 0         storage = content["_admin"]["storage"]
363 0         if path is not None and path != "$DESCRIPTOR":  # artifacts
364 0             if not storage.get('pkg-dir'):
365 0                 raise EngineException("Packages does not contains artifacts", http_code=HTTPStatus.BAD_REQUEST)
366 0             if self.fs.file_exists((storage['folder'], storage['pkg-dir'], *path), 'dir'):
367 0                 folder_content = self.fs.dir_ls((storage['folder'], storage['pkg-dir'], *path))
368 0                 return folder_content, "text/plain"
369                 # TODO manage folders in http
370             else:
371 0                 return self.fs.file_open((storage['folder'], storage['pkg-dir'], *path), "rb"), \
372                     "application/octet-stream"
373
374         # pkgtype   accept  ZIP  TEXT    -> result
375         # manyfiles         yes  X       -> zip
376         #                   no   yes     -> error
377         # onefile           yes  no      -> zip
378         #                   X    yes     -> text
379 0         contain_many_files = False
380 0         if storage.get('pkg-dir'):
381             # check if there are more than one file in the package, ignoring checksums.txt.
382 0             pkg_files = self.fs.dir_ls((storage['folder'], storage['pkg-dir']))
383 0             if len(pkg_files) >= 3 or (len(pkg_files) == 2 and 'checksums.txt' not in pkg_files):
384 0                 contain_many_files = True
385 0         if accept_text and (not contain_many_files or path == "$DESCRIPTOR"):
386 0             return self.fs.file_open((storage['folder'], storage['descriptor']), "r"), "text/plain"
387 0         elif contain_many_files and not accept_zip:
388 0             raise EngineException("Packages that contains several files need to be retrieved with 'application/zip'"
389                                   "Accept header", http_code=HTTPStatus.NOT_ACCEPTABLE)
390         else:
391 0             if not storage.get('zipfile'):
392                 # TODO generate zipfile if not present
393 0                 raise EngineException("Only allowed 'text/plain' Accept header for this descriptor. To be solved in "
394                                       "future versions", http_code=HTTPStatus.NOT_ACCEPTABLE)
395 0             return self.fs.file_open((storage['folder'], storage['zipfile']), "rb"), accept_zip
396
397 1     def _remove_yang_prefixes_from_descriptor(self, descriptor):
398 1         new_descriptor = {}
399 1         for k, v in descriptor.items():
400 1             new_v = v
401 1             if isinstance(v, dict):
402 1                 new_v = self._remove_yang_prefixes_from_descriptor(v)
403 1             elif isinstance(v, list):
404 1                 new_v = list()
405 1                 for x in v:
406 1                     if isinstance(x, dict):
407 1                         new_v.append(self._remove_yang_prefixes_from_descriptor(x))
408                     else:
409 1                         new_v.append(x)
410 1             new_descriptor[k.split(':')[-1]] = new_v
411 1         return new_descriptor
412
413 1     def pyangbind_validation(self, item, data, force=False):
414 0         raise EngineException("Not possible to validate '{}' item".format(item),
415                               http_code=HTTPStatus.INTERNAL_SERVER_ERROR)
416
417 1     def _validate_input_edit(self, indata, content, force=False):
418         # not needed to validate with pyangbind becuase it will be validated at check_conflict_on_edit
419 1         if "_id" in indata:
420 0             indata.pop("_id")
421 1         if "_admin" not in indata:
422 1             indata["_admin"] = {}
423
424 1         if "operationalState" in indata:
425 0             if indata["operationalState"] in ("ENABLED", "DISABLED"):
426 0                 indata["_admin"]["operationalState"] = indata.pop("operationalState")
427             else:
428 0                 raise EngineException("State '{}' is not a valid operational state"
429                                       .format(indata["operationalState"]),
430                                       http_code=HTTPStatus.BAD_REQUEST)
431
432         # In the case of user defined data, we need to put the data in the root of the object 
433         # to preserve current expected behaviour
434 1         if "userDefinedData" in indata:
435 0             data = indata.pop("userDefinedData")
436 0             if type(data) == dict:
437 0                 indata["_admin"]["userDefinedData"] = data
438             else:
439 0                 raise EngineException("userDefinedData should be an object, but is '{}' instead"
440                                       .format(type(data)),
441                                       http_code=HTTPStatus.BAD_REQUEST)
442
443 1         if ("operationalState" in indata["_admin"] and
444                 content["_admin"]["operationalState"] == indata["_admin"]["operationalState"]):
445 0             raise EngineException("operationalState already {}".format(content["_admin"]["operationalState"]),
446                                   http_code=HTTPStatus.CONFLICT)
447
448 1         return indata
449
450
451 1 class VnfdTopic(DescriptorTopic):
452 1     topic = "vnfds"
453 1     topic_msg = "vnfd"
454
455 1     def __init__(self, db, fs, msg, auth):
456 1         DescriptorTopic.__init__(self, db, fs, msg, auth)
457
458 1     def pyangbind_validation(self, item, data, force=False):
459 1         try:
460 1             virtual_compute_descriptors = data.get('virtual-compute-desc')
461 1             virtual_storage_descriptors = data.get('virtual-storage-desc')
462 1             myvnfd = etsi_nfv_vnfd.etsi_nfv_vnfd()
463 1             pybindJSONDecoder.load_ietf_json({'etsi-nfv-vnfd:vnfd': data}, None, None, obj=myvnfd,
464                                              path_helper=True, skip_unknown=force)
465 1             out = pybindJSON.dumps(myvnfd, mode="ietf")
466 1             desc_out = self._remove_envelop(yaml.safe_load(out))
467 1             desc_out = self._remove_yang_prefixes_from_descriptor(desc_out)
468 1             if virtual_compute_descriptors:
469 1                 desc_out['virtual-compute-desc'] = virtual_compute_descriptors
470 1             if virtual_storage_descriptors:
471 1                 desc_out['virtual-storage-desc'] = virtual_storage_descriptors
472 1             return desc_out
473 1         except Exception as e:
474 1             raise EngineException("Error in pyangbind validation: {}".format(str(e)),
475                                   http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
476
477 1     @staticmethod
478 1     def _remove_envelop(indata=None):
479 1         if not indata:
480 0             return {}
481 1         clean_indata = indata
482
483 1         if clean_indata.get('etsi-nfv-vnfd:vnfd'):
484 1             if not isinstance(clean_indata['etsi-nfv-vnfd:vnfd'], dict):
485 0                 raise EngineException("'etsi-nfv-vnfd:vnfd' must be a dict")
486 1             clean_indata = clean_indata['etsi-nfv-vnfd:vnfd']
487 1         elif clean_indata.get('vnfd'):
488 1             if not isinstance(clean_indata['vnfd'], dict):
489 1                 raise EngineException("'vnfd' must be dict")
490 0             clean_indata = clean_indata['vnfd']
491
492 1         return clean_indata
493
494 1     def check_conflict_on_edit(self, session, final_content, edit_content, _id):
495 1         super().check_conflict_on_edit(session, final_content, edit_content, _id)
496
497         # set type of vnfd
498 1         contains_pdu = False
499 1         contains_vdu = False
500 1         for vdu in get_iterable(final_content.get("vdu")):
501 1             if vdu.get("pdu-type"):
502 0                 contains_pdu = True
503             else:
504 1                 contains_vdu = True
505 1         if contains_pdu:
506 0             final_content["_admin"]["type"] = "hnfd" if contains_vdu else "pnfd"
507 1         elif contains_vdu:
508 1             final_content["_admin"]["type"] = "vnfd"
509         # if neither vud nor pdu do not fill type
510
511 1     def check_conflict_on_del(self, session, _id, db_content):
512         """
513         Check that there is not any NSD that uses this VNFD. Only NSDs belonging to this project are considered. Note
514         that VNFD can be public and be used by NSD of other projects. Also check there are not deployments, or vnfr
515         that uses this vnfd
516         :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
517         :param _id: vnfd internal id
518         :param db_content: The database content of the _id.
519         :return: None or raises EngineException with the conflict
520         """
521 1         if session["force"]:
522 0             return
523 1         descriptor = db_content
524 1         descriptor_id = descriptor.get("id")
525 1         if not descriptor_id:  # empty vnfd not uploaded
526 0             return
527
528 1         _filter = self._get_project_filter(session)
529
530         # check vnfrs using this vnfd
531 1         _filter["vnfd-id"] = _id
532 1         if self.db.get_list("vnfrs", _filter):
533 1             raise EngineException("There is at least one VNF using this descriptor", http_code=HTTPStatus.CONFLICT)
534
535         # check NSD referencing this VNFD
536 1         del _filter["vnfd-id"]
537 1         _filter["constituent-vnfd.ANYINDEX.vnfd-id-ref"] = descriptor_id
538 1         if self.db.get_list("nsds", _filter):
539 1             raise EngineException("There is at least one NSD referencing this descriptor",
540                                   http_code=HTTPStatus.CONFLICT)
541
542 1     def _validate_input_new(self, indata, storage_params, force=False):
543 1         indata.pop("onboardingState", None)
544 1         indata.pop("operationalState", None)
545 1         indata.pop("usageState", None)
546 1         indata.pop("links", None)
547
548 1         indata = self.pyangbind_validation("vnfds", indata, force)
549         # Cross references validation in the descriptor
550
551 1         self.validate_mgmt_interface_connection_point(indata)
552
553 1         for vdu in get_iterable(indata.get("vdu")):
554 1             self.validate_vdu_internal_connection_points(vdu)
555 1             self._validate_vdu_charms_in_package(storage_params, vdu, indata)
556 1             self._validate_vdu_cloud_init_in_package(storage_params, vdu, indata)
557
558 1         self._validate_vnf_charms_in_package(storage_params, indata)
559
560 1         self.validate_external_connection_points(indata)
561 1         self.validate_internal_virtual_links(indata)
562 1         self.validate_monitoring_params(indata)
563 1         self.validate_scaling_group_descriptor(indata)
564
565 1         return indata
566
567 1     @staticmethod
568     def validate_mgmt_interface_connection_point(indata):
569 1         if not indata.get("vdu"):
570 0             return
571 1         if not indata.get("mgmt-cp"):
572 1             raise EngineException("'mgmt-cp' is a mandatory field and it is not defined",
573                                   http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
574
575 1         for cp in get_iterable(indata.get("ext-cpd")):
576 1             if cp["id"] == indata["mgmt-cp"]:
577 1                 break
578         else:
579 1             raise EngineException("mgmt-cp='{}' must match an existing ext-cpd".format(indata["mgmt-cp"]),
580                                   http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
581
582 1     @staticmethod
583     def validate_vdu_internal_connection_points(vdu):
584 1         int_cpds = set()
585 1         for cpd in get_iterable(vdu.get("int-cpd")):
586 1             cpd_id = cpd.get("id")
587 1             if cpd_id and cpd_id in int_cpds:
588 1                 raise EngineException("vdu[id='{}']:int-cpd[id='{}'] is already used by other int-cpd"
589                                       .format(vdu["id"], cpd_id),
590                                       http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
591 1             int_cpds.add(cpd_id)
592
593 1     @staticmethod
594     def validate_external_connection_points(indata):
595 1         all_vdus_int_cpds = set()
596 1         for vdu in get_iterable(indata.get("vdu")):
597 1             for int_cpd in get_iterable(vdu.get("int-cpd")):
598 1                 all_vdus_int_cpds.add((vdu.get("id"), int_cpd.get("id")))
599
600 1         ext_cpds = set()
601 1         for cpd in get_iterable(indata.get("ext-cpd")):
602 1             cpd_id = cpd.get("id")
603 1             if cpd_id and cpd_id in ext_cpds:
604 1                 raise EngineException("ext-cpd[id='{}'] is already used by other ext-cpd".format(cpd_id),
605                                       http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
606 1             ext_cpds.add(cpd_id)
607
608 1             int_cpd = cpd.get("int-cpd")
609 1             if int_cpd:
610 1                 if (int_cpd.get("vdu-id"), int_cpd.get("cpd")) not in all_vdus_int_cpds:
611 1                     raise EngineException("ext-cpd[id='{}']:int-cpd must match an existing vdu int-cpd".format(cpd_id),
612                                           http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
613             # TODO: Validate k8s-cluster-net points to a valid k8s-cluster:nets ?
614
615 1     def _validate_vdu_charms_in_package(self, storage_params, vdu, indata):
616 1         if not vdu.get("vdu-configuration"):
617 1             return
618 0         for vdu_configuration in get_iterable(indata.get("vdu-configuration")):
619 0             if vdu_configuration.get("juju"):
620 0                 if not self._validate_package_folders(storage_params, 'charms'):
621 0                     raise EngineException("Charm defined in vnf[id={}] but not present in "
622                                           "package".format(indata["id"]))
623
624 1     def _validate_vdu_cloud_init_in_package(self, storage_params, vdu, indata):
625 1         if not vdu.get("cloud-init-file"):
626 1             return
627 1         if not self._validate_package_folders(storage_params, 'cloud_init', vdu["cloud-init-file"]):
628 1             raise EngineException("Cloud-init defined in vnf[id={}]:vdu[id={}] but not present in "
629                                   "package".format(indata["id"], vdu["id"]))
630
631 1     def _validate_vnf_charms_in_package(self, storage_params, indata):
632 1         if not indata.get("vnf-configuration"):
633 1             return
634 1         for vnf_configuration in get_iterable(indata.get("vnf-configuration")):
635 1             if vnf_configuration.get("juju"):
636 1                 if not self._validate_package_folders(storage_params, 'charms'):
637 1                     raise EngineException("Charm defined in vnf[id={}] but not present in "
638                                           "package".format(indata["id"]))
639
640 1     def _validate_package_folders(self, storage_params, folder, file=None):
641 1         if not storage_params or not storage_params.get("pkg-dir"):
642 1             return False
643         else:
644 1             if self.fs.file_exists("{}_".format(storage_params["folder"]), 'dir'):
645 1                 f = "{}_/{}/{}".format(storage_params["folder"], storage_params["pkg-dir"], folder)
646             else:
647 0                 f = "{}/{}/{}".format(storage_params["folder"], storage_params["pkg-dir"], folder)
648 1             if file:
649 1                 return self.fs.file_exists("{}/{}".format(f, file), 'file')
650             else:
651 1                 if self.fs.file_exists(f, 'dir'):
652 1                     if self.fs.dir_ls(f):
653 1                         return True
654 0             return False
655
656 1     @staticmethod
657     def validate_internal_virtual_links(indata):
658 1         all_ivld_ids = set()
659 1         for ivld in get_iterable(indata.get("int-virtual-link-desc")):
660 1             ivld_id = ivld.get("id")
661 1             if ivld_id and ivld_id in all_ivld_ids:
662 1                 raise EngineException("Duplicated VLD id in int-virtual-link-desc[id={}]".format(ivld_id),
663                                       http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
664             else:
665 1                 all_ivld_ids.add(ivld_id)
666
667 1         for vdu in get_iterable(indata.get("vdu")):
668 1             for int_cpd in get_iterable(vdu.get("int-cpd")):
669 1                 int_cpd_ivld_id = int_cpd.get("int-virtual-link-desc")
670 1                 if int_cpd_ivld_id and int_cpd_ivld_id not in all_ivld_ids:
671 1                     raise EngineException(
672                         "vdu[id='{}']:int-cpd[id='{}']:int-virtual-link-desc='{}' must match an existing "
673                         "int-virtual-link-desc".format(vdu["id"], int_cpd["id"], int_cpd_ivld_id),
674                         http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
675
676 1         for df in get_iterable(indata.get("df")):
677 1             for vlp in get_iterable(df.get("virtual-link-profile")):
678 1                 vlp_ivld_id = vlp.get("id")
679 1                 if vlp_ivld_id and vlp_ivld_id not in all_ivld_ids:
680 1                     raise EngineException("df[id='{}']:virtual-link-profile='{}' must match an existing "
681                                           "int-virtual-link-desc".format(df["id"], vlp_ivld_id),
682                                           http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
683
684 1     @staticmethod
685     def validate_monitoring_params(indata):
686 1         all_monitoring_params = set()
687 1         for ivld in get_iterable(indata.get("int-virtual-link-desc")):
688 1             for mp in get_iterable(ivld.get("monitoring-parameters")):
689 1                 mp_id = mp.get("id")
690 1                 if mp_id and mp_id in all_monitoring_params:
691 1                     raise EngineException("Duplicated monitoring-parameter id in "
692                                           "int-virtual-link-desc[id='{}']:monitoring-parameters[id='{}']"
693                                           .format(ivld["id"], mp_id),
694                                           http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
695                 else:
696 1                     all_monitoring_params.add(mp_id)
697
698 1         for vdu in get_iterable(indata.get("vdu")):
699 1             for mp in get_iterable(vdu.get("monitoring-parameter")):
700 1                 mp_id = mp.get("id")
701 1                 if mp_id and mp_id in all_monitoring_params:
702 1                     raise EngineException("Duplicated monitoring-parameter id in "
703                                           "vdu[id='{}']:monitoring-parameter[id='{}']"
704                                           .format(vdu["id"], mp_id),
705                                           http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
706                 else:
707 1                     all_monitoring_params.add(mp_id)
708
709 1         for df in get_iterable(indata.get("df")):
710 1             for mp in get_iterable(df.get("monitoring-parameter")):
711 1                 mp_id = mp.get("id")
712 1                 if mp_id and mp_id in all_monitoring_params:
713 1                     raise EngineException("Duplicated monitoring-parameter id in "
714                                           "df[id='{}']:monitoring-parameter[id='{}']"
715                                           .format(df["id"], mp_id),
716                                           http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
717                 else:
718 1                     all_monitoring_params.add(mp_id)
719
720 1     @staticmethod
721     def validate_scaling_group_descriptor(indata):
722 1         all_monitoring_params = set()
723 1         for ivld in get_iterable(indata.get("int-virtual-link-desc")):
724 1             for mp in get_iterable(ivld.get("monitoring-parameters")):
725 0                 all_monitoring_params.add(mp.get("id"))
726
727 1         for vdu in get_iterable(indata.get("vdu")):
728 1             for mp in get_iterable(vdu.get("monitoring-parameter")):
729 1                 all_monitoring_params.add(mp.get("id"))
730
731 1         for df in get_iterable(indata.get("df")):
732 1             for mp in get_iterable(df.get("monitoring-parameter")):
733 0                 all_monitoring_params.add(mp.get("id"))
734
735 1         for df in get_iterable(indata.get("df")):
736 1             for sa in get_iterable(df.get("scaling-aspect")):
737 1                 for sp in get_iterable(sa.get("scaling-policy")):
738 1                     for sc in get_iterable(sp.get("scaling-criteria")):
739 1                         sc_monitoring_param = sc.get("vnf-monitoring-param-ref")
740 1                         if sc_monitoring_param and sc_monitoring_param not in all_monitoring_params:
741 1                             raise EngineException("df[id='{}']:scaling-aspect[id='{}']:scaling-policy"
742                                                   "[name='{}']:scaling-criteria[name='{}']: "
743                                                   "vnf-monitoring-param-ref='{}' not defined in any monitoring-param"
744                                                   .format(df["id"], sa["id"], sp["name"], sc["name"],
745                                                           sc_monitoring_param),
746                                                   http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
747
748 1                 for sca in get_iterable(sa.get("scaling-config-action")):
749 1                     if not indata.get("vnf-configuration"):
750 1                         raise EngineException("'vnf-configuration' not defined in the descriptor but it is referenced "
751                                               "by df[id='{}']:scaling-aspect[id='{}']:scaling-config-action"
752                                               .format(df["id"], sa["id"]),
753                                               http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
754 1                     for configuration in get_iterable(indata["vnf-configuration"]):
755 1                         for primitive in get_iterable(configuration.get("config-primitive")):
756 1                             if primitive["name"] == sca["vnf-config-primitive-name-ref"]:
757 1                                 break
758                         else:
759 1                             raise EngineException("df[id='{}']:scaling-aspect[id='{}']:scaling-config-action:vnf-"
760                                                   "config-primitive-name-ref='{}' does not match any "
761                                                   "vnf-configuration:config-primitive:name"
762                                                   .format(df["id"], sa["id"], sca["vnf-config-primitive-name-ref"]),
763                                                   http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
764
765 1     def delete_extra(self, session, _id, db_content, not_send_msg=None):
766         """
767         Deletes associate file system storage (via super)
768         Deletes associated vnfpkgops from database.
769         :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
770         :param _id: server internal id
771         :param db_content: The database content of the descriptor
772         :return: None
773         :raises: FsException in case of error while deleting associated storage
774         """
775 1         super().delete_extra(session, _id, db_content, not_send_msg)
776 1         self.db.del_list("vnfpkgops", {"vnfPkgId": _id})
777
778 1     def sol005_projection(self, data):
779 0         data["onboardingState"] = data["_admin"]["onboardingState"]
780 0         data["operationalState"] = data["_admin"]["operationalState"]
781 0         data["usageState"] = data["_admin"]["usageState"]
782
783 0         links = {}
784 0         links["self"] = {"href": "/vnfpkgm/v1/vnf_packages/{}".format(data["_id"])}
785 0         links["vnfd"] = {"href": "/vnfpkgm/v1/vnf_packages/{}/vnfd".format(data["_id"])}
786 0         links["packageContent"] = {"href": "/vnfpkgm/v1/vnf_packages/{}/package_content".format(data["_id"])}
787 0         data["_links"] = links
788
789 0         return super().sol005_projection(data)
790
791
792 1 class NsdTopic(DescriptorTopic):
793 1     topic = "nsds"
794 1     topic_msg = "nsd"
795
796 1     def __init__(self, db, fs, msg, auth):
797 1         DescriptorTopic.__init__(self, db, fs, msg, auth)
798
799 1     def pyangbind_validation(self, item, data, force=False):
800 1         try:
801 1             mynsd = etsi_nfv_nsd.etsi_nfv_nsd()
802 1             pybindJSONDecoder.load_ietf_json({'nsd': {'nsd': [data]}}, None, None, obj=mynsd,
803                                              path_helper=True, skip_unknown=force)
804 1             out = pybindJSON.dumps(mynsd, mode="ietf")
805 1             desc_out = self._remove_envelop(yaml.safe_load(out))
806 1             desc_out = self._remove_yang_prefixes_from_descriptor(desc_out)
807 1             return desc_out
808 1         except Exception as e:
809 1             raise EngineException("Error in pyangbind validation: {}".format(str(e)),
810                                   http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
811
812 1     @staticmethod
813 1     def _remove_envelop(indata=None):
814 1         if not indata:
815 0             return {}
816 1         clean_indata = indata
817
818 1         if clean_indata.get('nsd'):
819 1             clean_indata = clean_indata['nsd']
820 1         elif clean_indata.get('etsi-nfv-nsd:nsd'):
821 1             clean_indata = clean_indata['etsi-nfv-nsd:nsd']
822 1         if clean_indata.get('nsd'):
823 1             if not isinstance(clean_indata['nsd'], list) or len(clean_indata['nsd']) != 1:
824 1                 raise EngineException("'nsd' must be a list of only one element")
825 1             clean_indata = clean_indata['nsd'][0]
826 1         return clean_indata
827
828 1     def _validate_input_new(self, indata, storage_params, force=False):
829 1         indata.pop("nsdOnboardingState", None)
830 1         indata.pop("nsdOperationalState", None)
831 1         indata.pop("nsdUsageState", None)
832
833 1         indata.pop("links", None)
834
835 1         indata = self.pyangbind_validation("nsds", indata, force)
836         # Cross references validation in the descriptor
837         # TODO validata that if contains cloud-init-file or charms, have artifacts _admin.storage."pkg-dir" is not none
838 1         for vld in get_iterable(indata.get("virtual-link-desc")):
839 1             self.validate_vld_mgmt_network_with_virtual_link_protocol_data(vld, indata)
840
841 1         self.validate_vnf_profiles_vnfd_id(indata)
842
843 1         return indata
844
845 1     @staticmethod
846     def validate_vld_mgmt_network_with_virtual_link_protocol_data(vld, indata):
847 1         if not vld.get("mgmt-network"):
848 1             return
849 1         vld_id = vld.get("id")
850 1         for df in get_iterable(indata.get("df")):
851 1             for vlp in get_iterable(df.get("virtual-link-profile")):
852 1                 if vld_id and vld_id == vlp.get("virtual-link-desc-id"):
853 1                     if vlp.get("virtual-link-protocol-data"):
854 1                         raise EngineException("Error at df[id='{}']:virtual-link-profile[id='{}']:virtual-link-"
855                                               "protocol-data You cannot set a virtual-link-protocol-data "
856                                               "when mgmt-network is True"
857                                               .format(df["id"], vlp["id"]), http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
858
859 1     @staticmethod
860     def validate_vnf_profiles_vnfd_id(indata):
861 1         all_vnfd_ids = set(get_iterable(indata.get("vnfd-id")))
862 1         for df in get_iterable(indata.get("df")):
863 1             for vnf_profile in get_iterable(df.get("vnf-profile")):
864 1                 vnfd_id = vnf_profile.get("vnfd-id")
865 1                 if vnfd_id and vnfd_id not in all_vnfd_ids:
866 1                     raise EngineException("Error at df[id='{}']:vnf_profile[id='{}']:vnfd-id='{}' "
867                                           "does not match any vnfd-id".format(df["id"], vnf_profile["id"], vnfd_id),
868                                           http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
869
870 1     def _validate_input_edit(self, indata, content, force=False):
871         # not needed to validate with pyangbind becuase it will be validated at check_conflict_on_edit
872         """
873         indata looks as follows:
874             - In the new case (conformant) 
875                 {'nsdOperationalState': 'DISABLED', 'userDefinedData': {'id': 'string23', 
876                 '_id': 'c6ddc544-cede-4b94-9ebe-be07b298a3c1', 'name': 'simon46'}}
877             - In the old case (backwards-compatible)
878                 {'id': 'string23', '_id': 'c6ddc544-cede-4b94-9ebe-be07b298a3c1', 'name': 'simon46'}
879         """
880 1         if "_admin" not in indata:
881 1             indata["_admin"] = {}
882
883 1         if "nsdOperationalState" in indata:
884 0             if indata["nsdOperationalState"] in ("ENABLED", "DISABLED"):
885 0                 indata["_admin"]["operationalState"] = indata.pop("nsdOperationalState")
886             else:
887 0                 raise EngineException("State '{}' is not a valid operational state"
888                                       .format(indata["nsdOperationalState"]),
889                                       http_code=HTTPStatus.BAD_REQUEST)
890
891         # In the case of user defined data, we need to put the data in the root of the object 
892         # to preserve current expected behaviour
893 1         if "userDefinedData" in indata:
894 0             data = indata.pop("userDefinedData")
895 0             if type(data) == dict:
896 0                 indata["_admin"]["userDefinedData"] = data
897             else:
898 0                 raise EngineException("userDefinedData should be an object, but is '{}' instead"
899                                       .format(type(data)),
900                                       http_code=HTTPStatus.BAD_REQUEST)
901 1         if ("operationalState" in indata["_admin"] and
902                 content["_admin"]["operationalState"] == indata["_admin"]["operationalState"]):
903 0             raise EngineException("nsdOperationalState already {}".format(content["_admin"]["operationalState"]),
904                                   http_code=HTTPStatus.CONFLICT)
905 1         return indata
906
907 1     def _check_descriptor_dependencies(self, session, descriptor):
908         """
909         Check that the dependent descriptors exist on a new descriptor or edition. Also checks references to vnfd
910         connection points are ok
911         :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
912         :param descriptor: descriptor to be inserted or edit
913         :return: None or raises exception
914         """
915 1         if session["force"]:
916 0             return
917 1         vnfds_index = self._get_descriptor_constituent_vnfds_index(session, descriptor)
918
919         # Cross references validation in the descriptor and vnfd connection point validation
920 1         for df in get_iterable(descriptor.get("df")):
921 1             self.validate_df_vnf_profiles_constituent_connection_points(df, vnfds_index)
922
923 1     def _get_descriptor_constituent_vnfds_index(self, session, descriptor):
924 1         vnfds_index = {}
925 1         if descriptor.get("vnfd-id") and not session["force"]:
926 1             for vnfd_id in get_iterable(descriptor.get("vnfd-id")):
927 1                 query_filter = self._get_project_filter(session)
928 1                 query_filter["id"] = vnfd_id
929 1                 vnf_list = self.db.get_list("vnfds", query_filter)
930 1                 if not vnf_list:
931 1                     raise EngineException("Descriptor error at 'vnfd-id'='{}' references a non "
932                                           "existing vnfd".format(vnfd_id), http_code=HTTPStatus.CONFLICT)
933 1                 vnfds_index[vnfd_id] = vnf_list[0]
934 1         return vnfds_index
935
936 1     @staticmethod
937     def validate_df_vnf_profiles_constituent_connection_points(df, vnfds_index):
938 1         for vnf_profile in get_iterable(df.get("vnf-profile")):
939 1             vnfd = vnfds_index.get(vnf_profile["vnfd-id"])
940 1             all_vnfd_ext_cpds = set()
941 1             for ext_cpd in get_iterable(vnfd.get("ext-cpd")):
942 1                 if ext_cpd.get('id'):
943 1                     all_vnfd_ext_cpds.add(ext_cpd.get('id'))
944
945 1             for virtual_link in get_iterable(vnf_profile.get("virtual-link-connectivity")):
946 1                 for vl_cpd in get_iterable(virtual_link.get("constituent-cpd-id")):
947 1                     vl_cpd_id = vl_cpd.get('constituent-cpd-id')
948 1                     if vl_cpd_id and vl_cpd_id not in all_vnfd_ext_cpds:
949 1                         raise EngineException("Error at df[id='{}']:vnf-profile[id='{}']:virtual-link-connectivity"
950                                               "[virtual-link-profile-id='{}']:constituent-cpd-id='{}' references a "
951                                               "non existing ext-cpd:id inside vnfd '{}'"
952                                               .format(df["id"], vnf_profile["id"],
953                                                       virtual_link["virtual-link-profile-id"], vl_cpd_id, vnfd["id"]),
954                                               http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
955
956 1     def check_conflict_on_edit(self, session, final_content, edit_content, _id):
957 1         super().check_conflict_on_edit(session, final_content, edit_content, _id)
958
959 1         self._check_descriptor_dependencies(session, final_content)
960
961 1     def check_conflict_on_del(self, session, _id, db_content):
962         """
963         Check that there is not any NSR that uses this NSD. Only NSRs belonging to this project are considered. Note
964         that NSD can be public and be used by other projects.
965         :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
966         :param _id: nsd internal id
967         :param db_content: The database content of the _id
968         :return: None or raises EngineException with the conflict
969         """
970 1         if session["force"]:
971 0             return
972 1         descriptor = db_content
973 1         descriptor_id = descriptor.get("id")
974 1         if not descriptor_id:  # empty nsd not uploaded
975 0             return
976
977         # check NSD used by NS
978 1         _filter = self._get_project_filter(session)
979 1         _filter["nsd-id"] = _id
980 1         if self.db.get_list("nsrs", _filter):
981 1             raise EngineException("There is at least one NS using this descriptor", http_code=HTTPStatus.CONFLICT)
982
983         # check NSD referenced by NST
984 1         del _filter["nsd-id"]
985 1         _filter["netslice-subnet.ANYINDEX.nsd-ref"] = descriptor_id
986 1         if self.db.get_list("nsts", _filter):
987 1             raise EngineException("There is at least one NetSlice Template referencing this descriptor",
988                                   http_code=HTTPStatus.CONFLICT)
989
990 1     def sol005_projection(self, data):
991 0         data["nsdOnboardingState"] = data["_admin"]["onboardingState"]
992 0         data["nsdOperationalState"] = data["_admin"]["operationalState"]
993 0         data["nsdUsageState"] = data["_admin"]["usageState"]
994
995 0         links = {}
996 0         links["self"] = {"href": "/nsd/v1/ns_descriptors/{}".format(data["_id"])}
997 0         links["nsd_content"] = {"href": "/nsd/v1/ns_descriptors/{}/nsd_content".format(data["_id"])}
998 0         data["_links"] = links
999
1000 0         return super().sol005_projection(data)
1001
1002
1003 1 class NstTopic(DescriptorTopic):
1004 1     topic = "nsts"
1005 1     topic_msg = "nst"
1006 1     quota_name = "slice_templates"
1007
1008 1     def __init__(self, db, fs, msg, auth):
1009 0         DescriptorTopic.__init__(self, db, fs, msg, auth)
1010
1011 1     def pyangbind_validation(self, item, data, force=False):
1012 0         try:
1013 0             mynst = nst_im()
1014 0             pybindJSONDecoder.load_ietf_json({'nst': [data]}, None, None, obj=mynst,
1015                                              path_helper=True, skip_unknown=force)
1016 0             out = pybindJSON.dumps(mynst, mode="ietf")
1017 0             desc_out = self._remove_envelop(yaml.safe_load(out))
1018 0             return desc_out
1019 0         except Exception as e:
1020 0             raise EngineException("Error in pyangbind validation: {}".format(str(e)),
1021                                   http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
1022
1023 1     @staticmethod
1024 1     def _remove_envelop(indata=None):
1025 0         if not indata:
1026 0             return {}
1027 0         clean_indata = indata
1028
1029 0         if clean_indata.get('nst'):
1030 0             if not isinstance(clean_indata['nst'], list) or len(clean_indata['nst']) != 1:
1031 0                 raise EngineException("'nst' must be a list only one element")
1032 0             clean_indata = clean_indata['nst'][0]
1033 0         elif clean_indata.get('nst:nst'):
1034 0             if not isinstance(clean_indata['nst:nst'], list) or len(clean_indata['nst:nst']) != 1:
1035 0                 raise EngineException("'nst:nst' must be a list only one element")
1036 0             clean_indata = clean_indata['nst:nst'][0]
1037 0         return clean_indata
1038
1039 1     def _validate_input_new(self, indata, storage_params, force=False):
1040 0         indata.pop("onboardingState", None)
1041 0         indata.pop("operationalState", None)
1042 0         indata.pop("usageState", None)
1043 0         indata = self.pyangbind_validation("nsts", indata, force)
1044 0         return indata.copy()
1045
1046 1     def _check_descriptor_dependencies(self, session, descriptor):
1047         """
1048         Check that the dependent descriptors exist on a new descriptor or edition
1049         :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1050         :param descriptor: descriptor to be inserted or edit
1051         :return: None or raises exception
1052         """
1053 0         if not descriptor.get("netslice-subnet"):
1054 0             return
1055 0         for nsd in descriptor["netslice-subnet"]:
1056 0             nsd_id = nsd["nsd-ref"]
1057 0             filter_q = self._get_project_filter(session)
1058 0             filter_q["id"] = nsd_id
1059 0             if not self.db.get_list("nsds", filter_q):
1060 0                 raise EngineException("Descriptor error at 'netslice-subnet':'nsd-ref'='{}' references a non "
1061                                       "existing nsd".format(nsd_id), http_code=HTTPStatus.CONFLICT)
1062
1063 1     def check_conflict_on_edit(self, session, final_content, edit_content, _id):
1064 0         super().check_conflict_on_edit(session, final_content, edit_content, _id)
1065
1066 0         self._check_descriptor_dependencies(session, final_content)
1067
1068 1     def check_conflict_on_del(self, session, _id, db_content):
1069         """
1070         Check that there is not any NSIR that uses this NST. Only NSIRs belonging to this project are considered. Note
1071         that NST can be public and be used by other projects.
1072         :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1073         :param _id: nst internal id
1074         :param db_content: The database content of the _id.
1075         :return: None or raises EngineException with the conflict
1076         """
1077         # TODO: Check this method
1078 0         if session["force"]:
1079 0             return
1080         # Get Network Slice Template from Database
1081 0         _filter = self._get_project_filter(session)
1082 0         _filter["_admin.nst-id"] = _id
1083 0         if self.db.get_list("nsis", _filter):
1084 0             raise EngineException("there is at least one Netslice Instance using this descriptor",
1085                                   http_code=HTTPStatus.CONFLICT)
1086
1087 1     def sol005_projection(self, data):
1088 0         data["onboardingState"] = data["_admin"]["onboardingState"]
1089 0         data["operationalState"] = data["_admin"]["operationalState"]
1090 0         data["usageState"] = data["_admin"]["usageState"]
1091
1092 0         links = {}
1093 0         links["self"] = {"href": "/nst/v1/netslice_templates/{}".format(data["_id"])}
1094 0         links["nst"] = {"href": "/nst/v1/netslice_templates/{}/nst".format(data["_id"])}
1095 0         data["_links"] = links
1096
1097 0         return super().sol005_projection(data)
1098
1099
1100 1 class PduTopic(BaseTopic):
1101 1     topic = "pdus"
1102 1     topic_msg = "pdu"
1103 1     quota_name = "pduds"
1104 1     schema_new = pdu_new_schema
1105 1     schema_edit = pdu_edit_schema
1106
1107 1     def __init__(self, db, fs, msg, auth):
1108 0         BaseTopic.__init__(self, db, fs, msg, auth)
1109
1110 1     @staticmethod
1111 1     def format_on_new(content, project_id=None, make_public=False):
1112 0         BaseTopic.format_on_new(content, project_id=project_id, make_public=make_public)
1113 0         content["_admin"]["onboardingState"] = "CREATED"
1114 0         content["_admin"]["operationalState"] = "ENABLED"
1115 0         content["_admin"]["usageState"] = "NOT_IN_USE"
1116
1117 1     def check_conflict_on_del(self, session, _id, db_content):
1118         """
1119         Check that there is not any vnfr that uses this PDU
1120         :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1121         :param _id: pdu internal id
1122         :param db_content: The database content of the _id.
1123         :return: None or raises EngineException with the conflict
1124         """
1125 0         if session["force"]:
1126 0             return
1127
1128 0         _filter = self._get_project_filter(session)
1129 0         _filter["vdur.pdu-id"] = _id
1130 0         if self.db.get_list("vnfrs", _filter):
1131 0             raise EngineException("There is at least one VNF using this PDU", http_code=HTTPStatus.CONFLICT)
1132
1133
1134 1 class VnfPkgOpTopic(BaseTopic):
1135 1     topic = "vnfpkgops"
1136 1     topic_msg = "vnfd"
1137 1     schema_new = vnfpkgop_new_schema
1138 1     schema_edit = None
1139
1140 1     def __init__(self, db, fs, msg, auth):
1141 0         BaseTopic.__init__(self, db, fs, msg, auth)
1142
1143 1     def edit(self, session, _id, indata=None, kwargs=None, content=None):
1144 0         raise EngineException("Method 'edit' not allowed for topic '{}'".format(self.topic),
1145                               HTTPStatus.METHOD_NOT_ALLOWED)
1146
1147 1     def delete(self, session, _id, dry_run=False):
1148 0         raise EngineException("Method 'delete' not allowed for topic '{}'".format(self.topic),
1149                               HTTPStatus.METHOD_NOT_ALLOWED)
1150
1151 1     def delete_list(self, session, filter_q=None):
1152 0         raise EngineException("Method 'delete_list' not allowed for topic '{}'".format(self.topic),
1153                               HTTPStatus.METHOD_NOT_ALLOWED)
1154
1155 1     def new(self, rollback, session, indata=None, kwargs=None, headers=None):
1156         """
1157         Creates a new entry into database.
1158         :param rollback: list to append created items at database in case a rollback may to be done
1159         :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1160         :param indata: data to be inserted
1161         :param kwargs: used to override the indata descriptor
1162         :param headers: http request headers
1163         :return: _id, op_id:
1164             _id: identity of the inserted data.
1165              op_id: None
1166         """
1167 0         self._update_input_with_kwargs(indata, kwargs)
1168 0         validate_input(indata, self.schema_new)
1169 0         vnfpkg_id = indata["vnfPkgId"]
1170 0         filter_q = BaseTopic._get_project_filter(session)
1171 0         filter_q["_id"] = vnfpkg_id
1172 0         vnfd = self.db.get_one("vnfds", filter_q)
1173 0         operation = indata["lcmOperationType"]
1174 0         kdu_name = indata["kdu_name"]
1175 0         for kdu in vnfd.get("kdu", []):
1176 0             if kdu["name"] == kdu_name:
1177 0                 helm_chart = kdu.get("helm-chart")
1178 0                 juju_bundle = kdu.get("juju-bundle")
1179 0                 break
1180         else:
1181 0             raise EngineException("Not found vnfd[id='{}']:kdu[name='{}']".format(vnfpkg_id, kdu_name))
1182 0         if helm_chart:
1183 0             indata["helm-chart"] = helm_chart
1184 0             match = fullmatch(r"([^/]*)/([^/]*)", helm_chart)
1185 0             repo_name = match.group(1) if match else None
1186 0         elif juju_bundle:
1187 0             indata["juju-bundle"] = juju_bundle
1188 0             match = fullmatch(r"([^/]*)/([^/]*)", juju_bundle)
1189 0             repo_name = match.group(1) if match else None
1190         else:
1191 0             raise EngineException("Found neither 'helm-chart' nor 'juju-bundle' in vnfd[id='{}']:kdu[name='{}']"
1192                                   .format(vnfpkg_id, kdu_name))
1193 0         if repo_name:
1194 0             del filter_q["_id"]
1195 0             filter_q["name"] = repo_name
1196 0             repo = self.db.get_one("k8srepos", filter_q)
1197 0             k8srepo_id = repo.get("_id")
1198 0             k8srepo_url = repo.get("url")
1199         else:
1200 0             k8srepo_id = None
1201 0             k8srepo_url = None
1202 0         indata["k8srepoId"] = k8srepo_id
1203 0         indata["k8srepo_url"] = k8srepo_url
1204 0         vnfpkgop_id = str(uuid4())
1205 0         vnfpkgop_desc = {
1206             "_id": vnfpkgop_id,
1207             "operationState": "PROCESSING",
1208             "vnfPkgId": vnfpkg_id,
1209             "lcmOperationType": operation,
1210             "isAutomaticInvocation": False,
1211             "isCancelPending": False,
1212             "operationParams": indata,
1213             "links": {
1214                 "self": "/osm/vnfpkgm/v1/vnfpkg_op_occs/" + vnfpkgop_id,
1215                 "vnfpkg": "/osm/vnfpkgm/v1/vnf_packages/" + vnfpkg_id,
1216             }
1217         }
1218 0         self.format_on_new(vnfpkgop_desc, session["project_id"], make_public=session["public"])
1219 0         ctime = vnfpkgop_desc["_admin"]["created"]
1220 0         vnfpkgop_desc["statusEnteredTime"] = ctime
1221 0         vnfpkgop_desc["startTime"] = ctime
1222 0         self.db.create(self.topic, vnfpkgop_desc)
1223 0         rollback.append({"topic": self.topic, "_id": vnfpkgop_id})
1224 0         self.msg.write(self.topic_msg, operation, vnfpkgop_desc)
1225 0         return vnfpkgop_id, None