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%
500/782
100%
0/0

Coverage Breakdown by Class

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