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