blob: ed958b1f6578c39ec0179e5061da0c42129a8ece [file] [log] [blame]
tiernob24258a2018-10-04 18:39:49 +02001# -*- coding: utf-8 -*-
2
tiernod125caf2018-11-22 16:05:54 +00003# 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
tiernob24258a2018-10-04 18:39:49 +020016import tarfile
17import yaml
18import json
garciaale7cbd03c2020-11-27 10:38:35 -030019import importlib
tiernob24258a2018-10-04 18:39:49 +020020# import logging
21from hashlib import md5
22from osm_common.dbbase import DbException, deep_update_rfc7396
23from http import HTTPStatus
delacruzramo26301bb2019-11-15 14:45:32 +010024from time import time
delacruzramo271d2002019-12-02 21:00:37 +010025from uuid import uuid4
26from re import fullmatch
27from osm_nbi.validation import ValidationError, pdu_new_schema, pdu_edit_schema, \
28 validate_input, vnfpkgop_new_schema
tierno23acf402019-08-28 13:36:34 +000029from osm_nbi.base_topic import BaseTopic, EngineException, get_iterable
garciaale7cbd03c2020-11-27 10:38:35 -030030etsi_nfv_vnfd = importlib.import_module("osm_im.etsi-nfv-vnfd")
31etsi_nfv_nsd = importlib.import_module("osm_im.etsi-nfv-nsd")
gcalvino70434c12018-11-27 15:17:04 +010032from osm_im.nst import nst as nst_im
gcalvino46e4cb82018-10-26 13:10:22 +020033from pyangbind.lib.serialise import pybindJSONDecoder
34import pyangbind.lib.pybindJSON as pybindJSON
tiernob24258a2018-10-04 18:39:49 +020035
36__author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
37
38
39class DescriptorTopic(BaseTopic):
40
delacruzramo32bab472019-09-13 12:24:22 +020041 def __init__(self, db, fs, msg, auth):
42 BaseTopic.__init__(self, db, fs, msg, auth)
tiernob24258a2018-10-04 18:39:49 +020043
tierno65ca36d2019-02-12 19:27:52 +010044 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
45 super().check_conflict_on_edit(session, final_content, edit_content, _id)
K Sai Kiran45bd94c2019-11-25 17:30:37 +053046
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])
garciaale960531a2020-10-20 18:29:45 -030066
K Sai Kiran45bd94c2019-11-25 17:30:37 +053067 _check_unique_id_name(final_content)
tiernoaa1ca7b2018-11-08 19:00:20 +010068 # 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)
gcalvinoa6fe0002019-01-09 13:27:11 +010074 storage_params = internal_keys["_admin"].get("storage")
tierno65ca36d2019-02-12 19:27:52 +010075 serialized = self._validate_input_new(final_content, storage_params, session["force"])
tiernoaa1ca7b2018-11-08 19:00:20 +010076 # 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
tierno65ca36d2019-02-12 19:27:52 +010082 if session["force"]:
tierno5a5c2182018-11-20 12:27:42 +000083 return
tiernoaa1ca7b2018-11-08 19:00:20 +010084 # 2. check that this id is not present
85 if "id" in edit_content:
tierno65ca36d2019-02-12 19:27:52 +010086 _filter = self._get_project_filter(session)
tiernoaa1ca7b2018-11-08 19:00:20 +010087 _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)
tiernob24258a2018-10-04 18:39:49 +020093
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"
tierno36ec8602018-11-02 17:27:11 +010099 content["_admin"]["usageState"] = "NOT_IN_USE"
tiernob24258a2018-10-04 18:39:49 +0200100
tiernobee3bad2019-12-05 12:26:01 +0000101 def delete_extra(self, session, _id, db_content, not_send_msg=None):
tiernob4844ab2019-05-23 08:42:12 +0000102 """
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
tiernobee3bad2019-12-05 12:26:01 +0000107 :param not_send_msg: To not send message (False) or store content (list) instead
tiernob4844ab2019-05-23 08:42:12 +0000108 :return: None if ok or raises EngineException with the problem
109 """
tiernob24258a2018-10-04 18:39:49 +0200110 self.fs.file_delete(_id, ignore_non_exist=True)
tiernof717cbe2018-12-03 16:35:42 +0000111 self.fs.file_delete(_id + "_", ignore_non_exist=True) # remove temp folder
tiernob24258a2018-10-04 18:39:49 +0200112
113 @staticmethod
114 def get_one_by_id(db, session, topic, id):
115 # find owned by this project
tierno65ca36d2019-02-12 19:27:52 +0100116 _filter = BaseTopic._get_project_filter(session)
tiernob24258a2018-10-04 18:39:49 +0200117 _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
tierno65ca36d2019-02-12 19:27:52 +0100126 _filter = BaseTopic._get_project_filter(session)
tiernob24258a2018-10-04 18:39:49 +0200127 _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
tierno65ca36d2019-02-12 19:27:52 +0100137 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
tiernob24258a2018-10-04 18:39:49 +0200138 """
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
tierno65ca36d2019-02-12 19:27:52 +0100143 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200144 :param indata: data to be inserted
145 :param kwargs: used to override the indata descriptor
146 :param headers: http request headers
tiernobdebce92019-07-01 15:36:49 +0000147 :return: _id, None: identity of the inserted data; and None as there is not any operation
tiernob24258a2018-10-04 18:39:49 +0200148 """
149
tiernod7749582020-05-28 10:41:10 +0000150 # No needed to capture exceptions
151 # Check Quota
152 self.check_quota(session)
delacruzramo32bab472019-09-13 12:24:22 +0200153
tiernod7749582020-05-28 10:41:10 +0000154 # _remove_envelop
155 if indata:
156 if "userDefinedData" in indata:
157 indata = indata['userDefinedData']
tiernob24258a2018-10-04 18:39:49 +0200158
tiernod7749582020-05-28 10:41:10 +0000159 # 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"])
tiernob24258a2018-10-04 18:39:49 +0200164
tiernod7749582020-05-28 10:41:10 +0000165 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
tiernob24258a2018-10-04 18:39:49 +0200171
tierno65ca36d2019-02-12 19:27:52 +0100172 def upload_content(self, session, _id, indata, kwargs, headers):
tiernob24258a2018-10-04 18:39:49 +0200173 """
174 Used for receiving content by chunks (with a transaction_id header and/or gzip file. It will store and extract)
tierno65ca36d2019-02-12 19:27:52 +0100175 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200176 :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
tierno5a5c2182018-11-20 12:27:42 +0000180 :return: True if package is completely uploaded or False if partial content has been uploded
tiernob24258a2018-10-04 18:39:49 +0200181 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
tiernof717cbe2018-12-03 16:35:42 +0000209 temp_folder = _id + "_" # all the content is upload here and if ok, it is rename from id_ to is folder
tiernob24258a2018-10-04 18:39:49 +0200210
211 if start:
tiernof717cbe2018-12-03 16:35:42 +0000212 if not self.fs.file_exists(temp_folder, 'dir'):
tiernob24258a2018-10-04 18:39:49 +0200213 raise EngineException("invalid Transaction-Id header", HTTPStatus.NOT_FOUND)
214 else:
tiernof717cbe2018-12-03 16:35:42 +0000215 self.fs.file_delete(temp_folder, ignore_non_exist=True)
216 self.fs.mkdir(temp_folder)
tiernob24258a2018-10-04 18:39:49 +0200217
218 storage = self.fs.get_params()
219 storage["folder"] = _id
220
tiernof717cbe2018-12-03 16:35:42 +0000221 file_path = (temp_folder, filename)
tiernob24258a2018-10-04 18:39:49 +0200222 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:
garciaale960531a2020-10-20 18:29:45 -0300242 if indata_len != end - start:
tiernob24258a2018-10-04 18:39:49 +0200243 raise EngineException("Mismatch between Content-Range header {}-{} and body length of {}".format(
garciaale960531a2020-10-20 18:29:45 -0300244 start, end - 1, indata_len), HTTPStatus.REQUESTED_RANGE_NOT_SATISFIABLE)
tiernob24258a2018-10-04 18:39:49 +0200245 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
tiernof717cbe2018-12-03 16:35:42 +0000281 self.fs.file_extract(tar, temp_folder)
282 with self.fs.file_open((temp_folder, descriptor_file_name), "r") as descriptor_file:
tiernob24258a2018-10-04 18:39:49 +0200283 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 "
delacruzramob19cadc2019-10-08 10:18:02 +0200293 indata = yaml.load(content, Loader=yaml.SafeLoader)
tiernob24258a2018-10-04 18:39:49 +0200294
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)
tiernob24258a2018-10-04 18:39:49 +0200304
305 deep_update_rfc7396(current_desc, indata)
tierno65ca36d2019-02-12 19:27:52 +0100306 self.check_conflict_on_edit(session, current_desc, indata, _id=_id)
delacruzramo26301bb2019-11-15 14:45:32 +0100307 current_desc["_admin"]["modified"] = time()
tiernob24258a2018-10-04 18:39:49 +0200308 self.db.replace(self.topic, _id, current_desc)
tiernof717cbe2018-12-03 16:35:42 +0000309 self.fs.dir_rename(temp_folder, _id)
tiernob24258a2018-10-04 18:39:49 +0200310
311 indata["_id"] = _id
K Sai Kiranc96fd692019-10-16 17:50:53 +0530312 self._send_msg("edited", indata)
tiernob24258a2018-10-04 18:39:49 +0200313
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
tierno65ca36d2019-02-12 19:27:52 +0100338 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tierno87006042018-10-24 12:50:20 +0200339 :param _id: Identity of the vnfd, nsd
tiernob24258a2018-10-04 18:39:49 +0200340 :param path: artifact path or "$DESCRIPTOR" or None
341 :param accept_header: Content of Accept header. Must contain applition/zip or/and text/plain
tierno87006042018-10-24 12:50:20 +0200342 :return: opened file plus Accept format or raises an exception
tiernob24258a2018-10-04 18:39:49 +0200343 """
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:
tierno87006042018-10-24 12:50:20 +0200349 accept_zip = 'application/zip'
350 elif 'application/gzip' in accept_header:
351 accept_zip = 'application/gzip'
352
tiernob24258a2018-10-04 18:39:49 +0200353 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"]
garciaale960531a2020-10-20 18:29:45 -0300363 if path is not None and path != "$DESCRIPTOR": # artifacts
tiernob24258a2018-10-04 18:39:49 +0200364 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:
garciaale960531a2020-10-20 18:29:45 -0300371 return self.fs.file_open((storage['folder'], storage['pkg-dir'], *path), "rb"), \
tiernob24258a2018-10-04 18:39:49 +0200372 "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
tiernoee002752020-08-04 14:14:16 +0000379 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"):
tiernob24258a2018-10-04 18:39:49 +0200386 return self.fs.file_open((storage['folder'], storage['descriptor']), "r"), "text/plain"
tiernoee002752020-08-04 14:14:16 +0000387 elif contain_many_files and not accept_zip:
tiernob24258a2018-10-04 18:39:49 +0200388 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)
tierno87006042018-10-24 12:50:20 +0200395 return self.fs.file_open((storage['folder'], storage['zipfile']), "rb"), accept_zip
tiernob24258a2018-10-04 18:39:49 +0200396
garciaale7cbd03c2020-11-27 10:38:35 -0300397 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
gcalvino46e4cb82018-10-26 13:10:22 +0200413 def pyangbind_validation(self, item, data, force=False):
garciaale7cbd03c2020-11-27 10:38:35 -0300414 raise EngineException("Not possible to validate '{}' item".format(item),
415 http_code=HTTPStatus.INTERNAL_SERVER_ERROR)
gcalvino46e4cb82018-10-26 13:10:22 +0200416
Frank Brydendeba68e2020-07-27 13:55:11 +0000417 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)
garciaale960531a2020-10-20 18:29:45 -0300442
Frank Brydendeba68e2020-07-27 13:55:11 +0000443 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
tiernob24258a2018-10-04 18:39:49 +0200450
451class VnfdTopic(DescriptorTopic):
452 topic = "vnfds"
453 topic_msg = "vnfd"
454
delacruzramo32bab472019-09-13 12:24:22 +0200455 def __init__(self, db, fs, msg, auth):
456 DescriptorTopic.__init__(self, db, fs, msg, auth)
tiernob24258a2018-10-04 18:39:49 +0200457
garciaale7cbd03c2020-11-27 10:38:35 -0300458 def pyangbind_validation(self, item, data, force=False):
garciaaledf718ae2020-12-03 19:17:28 -0300459 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)
garciaale7cbd03c2020-11-27 10:38:35 -0300462 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
tiernob24258a2018-10-04 18:39:49 +0200480 @staticmethod
garciaaledf718ae2020-12-03 19:17:28 -0300481 def _descriptor_data_is_in_old_format(data):
482 return ('vnfd-catalog' in data) or ('vnfd:vnfd-catalog' in data)
483
484 @staticmethod
tiernob24258a2018-10-04 18:39:49 +0200485 def _remove_envelop(indata=None):
486 if not indata:
487 return {}
488 clean_indata = indata
garciaale7cbd03c2020-11-27 10:38:35 -0300489
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
tiernob24258a2018-10-04 18:39:49 +0200499 return clean_indata
500
tierno65ca36d2019-02-12 19:27:52 +0100501 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
502 super().check_conflict_on_edit(session, final_content, edit_content, _id)
tierno36ec8602018-11-02 17:27:11 +0100503
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
tiernob4844ab2019-05-23 08:42:12 +0000518 def check_conflict_on_del(self, session, _id, db_content):
tiernob24258a2018-10-04 18:39:49 +0200519 """
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
tierno65ca36d2019-02-12 19:27:52 +0100523 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob4844ab2019-05-23 08:42:12 +0000524 :param _id: vnfd internal id
525 :param db_content: The database content of the _id.
tiernob24258a2018-10-04 18:39:49 +0200526 :return: None or raises EngineException with the conflict
527 """
tierno65ca36d2019-02-12 19:27:52 +0100528 if session["force"]:
tiernob24258a2018-10-04 18:39:49 +0200529 return
tiernob4844ab2019-05-23 08:42:12 +0000530 descriptor = db_content
tiernob24258a2018-10-04 18:39:49 +0200531 descriptor_id = descriptor.get("id")
532 if not descriptor_id: # empty vnfd not uploaded
533 return
534
tierno65ca36d2019-02-12 19:27:52 +0100535 _filter = self._get_project_filter(session)
tiernob4844ab2019-05-23 08:42:12 +0000536
tiernob24258a2018-10-04 18:39:49 +0200537 # check vnfrs using this vnfd
538 _filter["vnfd-id"] = _id
539 if self.db.get_list("vnfrs", _filter):
tiernob4844ab2019-05-23 08:42:12 +0000540 raise EngineException("There is at least one VNF using this descriptor", http_code=HTTPStatus.CONFLICT)
541
542 # check NSD referencing this VNFD
tiernob24258a2018-10-04 18:39:49 +0200543 del _filter["vnfd-id"]
tiernob24258a2018-10-04 18:39:49 +0200544 _filter["constituent-vnfd.ANYINDEX.vnfd-id-ref"] = descriptor_id
545 if self.db.get_list("nsds", _filter):
tiernob4844ab2019-05-23 08:42:12 +0000546 raise EngineException("There is at least one NSD referencing this descriptor",
547 http_code=HTTPStatus.CONFLICT)
tiernob24258a2018-10-04 18:39:49 +0200548
gcalvinoa6fe0002019-01-09 13:27:11 +0100549 def _validate_input_new(self, indata, storage_params, force=False):
Frank Bryden19b97522020-07-10 12:32:02 +0000550 indata.pop("onboardingState", None)
551 indata.pop("operationalState", None)
552 indata.pop("usageState", None)
Frank Bryden19b97522020-07-10 12:32:02 +0000553 indata.pop("links", None)
554
gcalvino46e4cb82018-10-26 13:10:22 +0200555 indata = self.pyangbind_validation("vnfds", indata, force)
gcalvino5e72d152018-10-23 11:46:57 +0200556 # Cross references validation in the descriptor
garciaale7cbd03c2020-11-27 10:38:35 -0300557
558 self.validate_mgmt_interface_connection_point(indata)
gcalvino5e72d152018-10-23 11:46:57 +0200559
560 for vdu in get_iterable(indata.get("vdu")):
garciaale7cbd03c2020-11-27 10:38:35 -0300561 self.validate_vdu_internal_connection_points(vdu)
garciaale960531a2020-10-20 18:29:45 -0300562 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
garciaale7cbd03c2020-11-27 10:38:35 -0300567 self.validate_external_connection_points(indata)
568 self.validate_internal_virtual_links(indata)
garciaale960531a2020-10-20 18:29:45 -0300569 self.validate_monitoring_params(indata)
570 self.validate_scaling_group_descriptor(indata)
571
572 return indata
573
574 @staticmethod
garciaale7cbd03c2020-11-27 10:38:35 -0300575 def validate_mgmt_interface_connection_point(indata):
garciaale960531a2020-10-20 18:29:45 -0300576 if not indata.get("vdu"):
577 return
garciaale7cbd03c2020-11-27 10:38:35 -0300578 if not indata.get("mgmt-cp"):
579 raise EngineException("'mgmt-cp' is a mandatory field and it is not defined",
garciaale960531a2020-10-20 18:29:45 -0300580 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
garciaale7cbd03c2020-11-27 10:38:35 -0300581
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)
garciaale960531a2020-10-20 18:29:45 -0300588
589 @staticmethod
garciaale7cbd03c2020-11-27 10:38:35 -0300590 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),
garciaale960531a2020-10-20 18:29:45 -0300619 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
garciaale7cbd03c2020-11-27 10:38:35 -0300620 # TODO: Validate k8s-cluster-net points to a valid k8s-cluster:nets ?
garciaale960531a2020-10-20 18:29:45 -0300621
622 def _validate_vdu_charms_in_package(self, storage_params, vdu, indata):
623 if not vdu.get("vdu-configuration"):
624 return
garciaale7cbd03c2020-11-27 10:38:35 -0300625 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"]))
garciaale960531a2020-10-20 18:29:45 -0300630
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
garciaale7cbd03c2020-11-27 10:38:35 -0300641 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"]))
garciaale960531a2020-10-20 18:29:45 -0300646
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
garciaale7cbd03c2020-11-27 10:38:35 -0300664 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),
garciaale960531a2020-10-20 18:29:45 -0300670 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
671 else:
garciaale7cbd03c2020-11-27 10:38:35 -0300672 all_ivld_ids.add(ivld_id)
garciaale960531a2020-10-20 18:29:45 -0300673
garciaale7cbd03c2020-11-27 10:38:35 -0300674 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),
gcalvino5e72d152018-10-23 11:46:57 +0200681 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
garciaale960531a2020-10-20 18:29:45 -0300682
garciaale7cbd03c2020-11-27 10:38:35 -0300683 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
garciaale960531a2020-10-20 18:29:45 -0300691 @staticmethod
692 def validate_monitoring_params(indata):
garciaale7cbd03c2020-11-27 10:38:35 -0300693 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),
gcalvino5e72d152018-10-23 11:46:57 +0200701 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
gcalvino5e72d152018-10-23 11:46:57 +0200702 else:
garciaale7cbd03c2020-11-27 10:38:35 -0300703 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),
gcalvino5e72d152018-10-23 11:46:57 +0200712 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
garciaale7cbd03c2020-11-27 10:38:35 -0300713 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)
gcalvino5e72d152018-10-23 11:46:57 +0200726
garciaale960531a2020-10-20 18:29:45 -0300727 @staticmethod
728 def validate_scaling_group_descriptor(indata):
garciaale7cbd03c2020-11-27 10:38:35 -0300729 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"]),
gcalvino5e72d152018-10-23 11:46:57 +0200760 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
garciaale7cbd03c2020-11-27 10:38:35 -0300761 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)
gcalvinoa6fe0002019-01-09 13:27:11 +0100771
delacruzramo271d2002019-12-02 21:00:37 +0100772 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})
garciaale960531a2020-10-20 18:29:45 -0300784
Frank Bryden19b97522020-07-10 12:32:02 +0000785 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
garciaale960531a2020-10-20 18:29:45 -0300795
Frank Bryden19b97522020-07-10 12:32:02 +0000796 return super().sol005_projection(data)
delacruzramo271d2002019-12-02 21:00:37 +0100797
tiernob24258a2018-10-04 18:39:49 +0200798
799class NsdTopic(DescriptorTopic):
800 topic = "nsds"
801 topic_msg = "nsd"
802
delacruzramo32bab472019-09-13 12:24:22 +0200803 def __init__(self, db, fs, msg, auth):
804 DescriptorTopic.__init__(self, db, fs, msg, auth)
tiernob24258a2018-10-04 18:39:49 +0200805
garciaale7cbd03c2020-11-27 10:38:35 -0300806 def pyangbind_validation(self, item, data, force=False):
garciaaledf718ae2020-12-03 19:17:28 -0300807 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)
garciaale7cbd03c2020-11-27 10:38:35 -0300810 try:
garciaale341ac1b2020-12-11 20:04:11 -0300811 nsd_vnf_profiles = data.get('df', [{}])[0].get('vnf-profile', [])
garciaale7cbd03c2020-11-27 10:38:35 -0300812 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)
garciaale341ac1b2020-12-11 20:04:11 -0300818 if nsd_vnf_profiles:
819 desc_out['df'][0]['vnf-profile'] = nsd_vnf_profiles
garciaale7cbd03c2020-11-27 10:38:35 -0300820 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
tiernob24258a2018-10-04 18:39:49 +0200825 @staticmethod
garciaaledf718ae2020-12-03 19:17:28 -0300826 def _descriptor_data_is_in_old_format(data):
827 return ('nsd-catalog' in data) or ('nsd:nsd-catalog' in data)
828
829 @staticmethod
tiernob24258a2018-10-04 18:39:49 +0200830 def _remove_envelop(indata=None):
831 if not indata:
832 return {}
833 clean_indata = indata
834
garciaale7cbd03c2020-11-27 10:38:35 -0300835 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']
tiernob24258a2018-10-04 18:39:49 +0200839 if clean_indata.get('nsd'):
840 if not isinstance(clean_indata['nsd'], list) or len(clean_indata['nsd']) != 1:
gcalvino46e4cb82018-10-26 13:10:22 +0200841 raise EngineException("'nsd' must be a list of only one element")
tiernob24258a2018-10-04 18:39:49 +0200842 clean_indata = clean_indata['nsd'][0]
843 return clean_indata
844
gcalvinoa6fe0002019-01-09 13:27:11 +0100845 def _validate_input_new(self, indata, storage_params, force=False):
Frank Bryden19b97522020-07-10 12:32:02 +0000846 indata.pop("nsdOnboardingState", None)
847 indata.pop("nsdOperationalState", None)
848 indata.pop("nsdUsageState", None)
849
850 indata.pop("links", None)
851
gcalvino46e4cb82018-10-26 13:10:22 +0200852 indata = self.pyangbind_validation("nsds", indata, force)
tierno5a5c2182018-11-20 12:27:42 +0000853 # Cross references validation in the descriptor
tiernoaa1ca7b2018-11-08 19:00:20 +0100854 # TODO validata that if contains cloud-init-file or charms, have artifacts _admin.storage."pkg-dir" is not none
garciaale7cbd03c2020-11-27 10:38:35 -0300855 for vld in get_iterable(indata.get("virtual-link-desc")):
856 self.validate_vld_mgmt_network_with_virtual_link_protocol_data(vld, indata)
garciaale960531a2020-10-20 18:29:45 -0300857
garciaale7cbd03c2020-11-27 10:38:35 -0300858 self.validate_vnf_profiles_vnfd_id(indata)
garciaale960531a2020-10-20 18:29:45 -0300859
tiernob24258a2018-10-04 18:39:49 +0200860 return indata
861
garciaale960531a2020-10-20 18:29:45 -0300862 @staticmethod
garciaale7cbd03c2020-11-27 10:38:35 -0300863 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)
garciaale960531a2020-10-20 18:29:45 -0300875
876 @staticmethod
garciaale7cbd03c2020-11-27 10:38:35 -0300877 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)
garciaale960531a2020-10-20 18:29:45 -0300886
Frank Brydendeba68e2020-07-27 13:55:11 +0000887 def _validate_input_edit(self, indata, content, force=False):
tiernoaa1ca7b2018-11-08 19:00:20 +0100888 # not needed to validate with pyangbind becuase it will be validated at check_conflict_on_edit
Frank Brydendeba68e2020-07-27 13:55:11 +0000889 """
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)
tiernob24258a2018-10-04 18:39:49 +0200922 return indata
923
tierno65ca36d2019-02-12 19:27:52 +0100924 def _check_descriptor_dependencies(self, session, descriptor):
tiernob24258a2018-10-04 18:39:49 +0200925 """
tierno5a5c2182018-11-20 12:27:42 +0000926 Check that the dependent descriptors exist on a new descriptor or edition. Also checks references to vnfd
927 connection points are ok
tierno65ca36d2019-02-12 19:27:52 +0100928 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200929 :param descriptor: descriptor to be inserted or edit
930 :return: None or raises exception
931 """
tierno65ca36d2019-02-12 19:27:52 +0100932 if session["force"]:
tiernob24258a2018-10-04 18:39:49 +0200933 return
garciaale7cbd03c2020-11-27 10:38:35 -0300934 vnfds_index = self._get_descriptor_constituent_vnfds_index(session, descriptor)
garciaale960531a2020-10-20 18:29:45 -0300935
936 # Cross references validation in the descriptor and vnfd connection point validation
garciaale7cbd03c2020-11-27 10:38:35 -0300937 for df in get_iterable(descriptor.get("df")):
938 self.validate_df_vnf_profiles_constituent_connection_points(df, vnfds_index)
garciaale960531a2020-10-20 18:29:45 -0300939
garciaale7cbd03c2020-11-27 10:38:35 -0300940 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")):
garciaale960531a2020-10-20 18:29:45 -0300944 query_filter = self._get_project_filter(session)
945 query_filter["id"] = vnfd_id
946 vnf_list = self.db.get_list("vnfds", query_filter)
tierno5a5c2182018-11-20 12:27:42 +0000947 if not vnf_list:
garciaale7cbd03c2020-11-27 10:38:35 -0300948 raise EngineException("Descriptor error at 'vnfd-id'='{}' references a non "
tierno5a5c2182018-11-20 12:27:42 +0000949 "existing vnfd".format(vnfd_id), http_code=HTTPStatus.CONFLICT)
garciaale7cbd03c2020-11-27 10:38:35 -0300950 vnfds_index[vnfd_id] = vnf_list[0]
951 return vnfds_index
garciaale960531a2020-10-20 18:29:45 -0300952
953 @staticmethod
garciaale7cbd03c2020-11-27 10:38:35 -0300954 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)
tiernob24258a2018-10-04 18:39:49 +0200972
tierno65ca36d2019-02-12 19:27:52 +0100973 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
974 super().check_conflict_on_edit(session, final_content, edit_content, _id)
tiernob24258a2018-10-04 18:39:49 +0200975
tierno65ca36d2019-02-12 19:27:52 +0100976 self._check_descriptor_dependencies(session, final_content)
tiernob24258a2018-10-04 18:39:49 +0200977
tiernob4844ab2019-05-23 08:42:12 +0000978 def check_conflict_on_del(self, session, _id, db_content):
tiernob24258a2018-10-04 18:39:49 +0200979 """
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.
tierno65ca36d2019-02-12 19:27:52 +0100982 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob4844ab2019-05-23 08:42:12 +0000983 :param _id: nsd internal id
984 :param db_content: The database content of the _id
tiernob24258a2018-10-04 18:39:49 +0200985 :return: None or raises EngineException with the conflict
986 """
tierno65ca36d2019-02-12 19:27:52 +0100987 if session["force"]:
tiernob24258a2018-10-04 18:39:49 +0200988 return
tiernob4844ab2019-05-23 08:42:12 +0000989 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
tierno65ca36d2019-02-12 19:27:52 +0100995 _filter = self._get_project_filter(session)
tiernob4844ab2019-05-23 08:42:12 +0000996 _filter["nsd-id"] = _id
tiernob24258a2018-10-04 18:39:49 +0200997 if self.db.get_list("nsrs", _filter):
tiernob4844ab2019-05-23 08:42:12 +0000998 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)
garciaale960531a2020-10-20 18:29:45 -03001006
Frank Bryden19b97522020-07-10 12:32:02 +00001007 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
garciaale960531a2020-10-20 18:29:45 -03001016
Frank Bryden19b97522020-07-10 12:32:02 +00001017 return super().sol005_projection(data)
tiernob24258a2018-10-04 18:39:49 +02001018
1019
Felipe Vicensb57758d2018-10-16 16:00:20 +02001020class NstTopic(DescriptorTopic):
1021 topic = "nsts"
1022 topic_msg = "nst"
tierno6b02b052020-06-02 10:07:41 +00001023 quota_name = "slice_templates"
Felipe Vicensb57758d2018-10-16 16:00:20 +02001024
delacruzramo32bab472019-09-13 12:24:22 +02001025 def __init__(self, db, fs, msg, auth):
1026 DescriptorTopic.__init__(self, db, fs, msg, auth)
Felipe Vicensb57758d2018-10-16 16:00:20 +02001027
garciaale7cbd03c2020-11-27 10:38:35 -03001028 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
Felipe Vicensb57758d2018-10-16 16:00:20 +02001040 @staticmethod
1041 def _remove_envelop(indata=None):
1042 if not indata:
1043 return {}
1044 clean_indata = indata
1045
Felipe Vicensb57758d2018-10-16 16:00:20 +02001046 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]
gcalvino70434c12018-11-27 15:17:04 +01001050 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]
Felipe Vicensb57758d2018-10-16 16:00:20 +02001054 return clean_indata
1055
gcalvinoa6fe0002019-01-09 13:27:11 +01001056 def _validate_input_new(self, indata, storage_params, force=False):
Frank Bryden19b97522020-07-10 12:32:02 +00001057 indata.pop("onboardingState", None)
1058 indata.pop("operationalState", None)
1059 indata.pop("usageState", None)
gcalvino70434c12018-11-27 15:17:04 +01001060 indata = self.pyangbind_validation("nsts", indata, force)
Felipe Vicense36ab852018-11-23 14:12:09 +01001061 return indata.copy()
1062
Felipe Vicensb57758d2018-10-16 16:00:20 +02001063 def _check_descriptor_dependencies(self, session, descriptor):
1064 """
1065 Check that the dependent descriptors exist on a new descriptor or edition
tierno65ca36d2019-02-12 19:27:52 +01001066 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Felipe Vicensb57758d2018-10-16 16:00:20 +02001067 :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"]
tierno65ca36d2019-02-12 19:27:52 +01001074 filter_q = self._get_project_filter(session)
Felipe Vicensb57758d2018-10-16 16:00:20 +02001075 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
tierno65ca36d2019-02-12 19:27:52 +01001080 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
1081 super().check_conflict_on_edit(session, final_content, edit_content, _id)
Felipe Vicensb57758d2018-10-16 16:00:20 +02001082
1083 self._check_descriptor_dependencies(session, final_content)
1084
tiernob4844ab2019-05-23 08:42:12 +00001085 def check_conflict_on_del(self, session, _id, db_content):
Felipe Vicensb57758d2018-10-16 16:00:20 +02001086 """
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.
tierno65ca36d2019-02-12 19:27:52 +01001089 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Felipe Vicens07f31722018-10-29 15:16:44 +01001090 :param _id: nst internal id
tiernob4844ab2019-05-23 08:42:12 +00001091 :param db_content: The database content of the _id.
Felipe Vicensb57758d2018-10-16 16:00:20 +02001092 :return: None or raises EngineException with the conflict
1093 """
1094 # TODO: Check this method
tierno65ca36d2019-02-12 19:27:52 +01001095 if session["force"]:
Felipe Vicensb57758d2018-10-16 16:00:20 +02001096 return
Felipe Vicens07f31722018-10-29 15:16:44 +01001097 # Get Network Slice Template from Database
tierno65ca36d2019-02-12 19:27:52 +01001098 _filter = self._get_project_filter(session)
tiernoea97c042019-09-13 09:44:42 +00001099 _filter["_admin.nst-id"] = _id
tiernob4844ab2019-05-23 08:42:12 +00001100 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)
Felipe Vicensb57758d2018-10-16 16:00:20 +02001103
Frank Bryden19b97522020-07-10 12:32:02 +00001104 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
Felipe Vicensb57758d2018-10-16 16:00:20 +02001116
tiernob24258a2018-10-04 18:39:49 +02001117class PduTopic(BaseTopic):
1118 topic = "pdus"
1119 topic_msg = "pdu"
tierno6b02b052020-06-02 10:07:41 +00001120 quota_name = "pduds"
tiernob24258a2018-10-04 18:39:49 +02001121 schema_new = pdu_new_schema
1122 schema_edit = pdu_edit_schema
1123
delacruzramo32bab472019-09-13 12:24:22 +02001124 def __init__(self, db, fs, msg, auth):
1125 BaseTopic.__init__(self, db, fs, msg, auth)
tiernob24258a2018-10-04 18:39:49 +02001126
1127 @staticmethod
1128 def format_on_new(content, project_id=None, make_public=False):
tierno36ec8602018-11-02 17:27:11 +01001129 BaseTopic.format_on_new(content, project_id=project_id, make_public=make_public)
tiernob24258a2018-10-04 18:39:49 +02001130 content["_admin"]["onboardingState"] = "CREATED"
tierno36ec8602018-11-02 17:27:11 +01001131 content["_admin"]["operationalState"] = "ENABLED"
1132 content["_admin"]["usageState"] = "NOT_IN_USE"
tiernob24258a2018-10-04 18:39:49 +02001133
tiernob4844ab2019-05-23 08:42:12 +00001134 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 """
tierno65ca36d2019-02-12 19:27:52 +01001142 if session["force"]:
tiernob24258a2018-10-04 18:39:49 +02001143 return
tiernob4844ab2019-05-23 08:42:12 +00001144
1145 _filter = self._get_project_filter(session)
1146 _filter["vdur.pdu-id"] = _id
tiernob24258a2018-10-04 18:39:49 +02001147 if self.db.get_list("vnfrs", _filter):
tiernob4844ab2019-05-23 08:42:12 +00001148 raise EngineException("There is at least one VNF using this PDU", http_code=HTTPStatus.CONFLICT)
delacruzramo271d2002019-12-02 21:00:37 +01001149
1150
1151class 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