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%
499/781
100%
0/0

Coverage Breakdown by Class

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