blob: 517f1db76e6f21f4ae05ef735644b178b5aac980 [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
19# import logging
20from hashlib import md5
21from osm_common.dbbase import DbException, deep_update_rfc7396
22from http import HTTPStatus
delacruzramo26301bb2019-11-15 14:45:32 +010023from time import time
delacruzramo271d2002019-12-02 21:00:37 +010024from uuid import uuid4
25from re import fullmatch
26from osm_nbi.validation import ValidationError, pdu_new_schema, pdu_edit_schema, \
27 validate_input, vnfpkgop_new_schema
tierno23acf402019-08-28 13:36:34 +000028from osm_nbi.base_topic import BaseTopic, EngineException, get_iterable
gcalvino46e4cb82018-10-26 13:10:22 +020029from osm_im.vnfd import vnfd as vnfd_im
30from osm_im.nsd import nsd as nsd_im
gcalvino70434c12018-11-27 15:17:04 +010031from osm_im.nst import nst as nst_im
gcalvino46e4cb82018-10-26 13:10:22 +020032from pyangbind.lib.serialise import pybindJSONDecoder
33import pyangbind.lib.pybindJSON as pybindJSON
tiernob24258a2018-10-04 18:39:49 +020034
35__author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
36
37
38class DescriptorTopic(BaseTopic):
39
delacruzramo32bab472019-09-13 12:24:22 +020040 def __init__(self, db, fs, msg, auth):
41 BaseTopic.__init__(self, db, fs, msg, auth)
tiernob24258a2018-10-04 18:39:49 +020042
tierno65ca36d2019-02-12 19:27:52 +010043 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
44 super().check_conflict_on_edit(session, final_content, edit_content, _id)
K Sai Kiran45bd94c2019-11-25 17:30:37 +053045
46 def _check_unique_id_name(descriptor, position=""):
47 for desc_key, desc_item in descriptor.items():
48 if isinstance(desc_item, list) and desc_item:
49 used_ids = []
50 desc_item_id = None
51 for index, list_item in enumerate(desc_item):
52 if isinstance(list_item, dict):
53 _check_unique_id_name(list_item, "{}.{}[{}]"
54 .format(position, desc_key, index))
55 # Base case
56 if index == 0 and (list_item.get("id") or list_item.get("name")):
57 desc_item_id = "id" if list_item.get("id") else "name"
58 if desc_item_id and list_item.get(desc_item_id):
59 if list_item[desc_item_id] in used_ids:
60 position = "{}.{}[{}]".format(position, desc_key, index)
61 raise EngineException("Error: identifier {} '{}' is not unique and repeats at '{}'"
62 .format(desc_item_id, list_item[desc_item_id],
63 position), HTTPStatus.UNPROCESSABLE_ENTITY)
64 used_ids.append(list_item[desc_item_id])
65 _check_unique_id_name(final_content)
tiernoaa1ca7b2018-11-08 19:00:20 +010066 # 1. validate again with pyangbind
67 # 1.1. remove internal keys
68 internal_keys = {}
69 for k in ("_id", "_admin"):
70 if k in final_content:
71 internal_keys[k] = final_content.pop(k)
gcalvinoa6fe0002019-01-09 13:27:11 +010072 storage_params = internal_keys["_admin"].get("storage")
tierno65ca36d2019-02-12 19:27:52 +010073 serialized = self._validate_input_new(final_content, storage_params, session["force"])
tiernoaa1ca7b2018-11-08 19:00:20 +010074 # 1.2. modify final_content with a serialized version
75 final_content.clear()
76 final_content.update(serialized)
77 # 1.3. restore internal keys
78 for k, v in internal_keys.items():
79 final_content[k] = v
tiernob24258a2018-10-04 18:39:49 +020080
tierno65ca36d2019-02-12 19:27:52 +010081 if session["force"]:
tierno5a5c2182018-11-20 12:27:42 +000082 return
tiernoaa1ca7b2018-11-08 19:00:20 +010083 # 2. check that this id is not present
84 if "id" in edit_content:
tierno65ca36d2019-02-12 19:27:52 +010085 _filter = self._get_project_filter(session)
tiernoaa1ca7b2018-11-08 19:00:20 +010086 _filter["id"] = final_content["id"]
87 _filter["_id.neq"] = _id
88 if self.db.get_one(self.topic, _filter, fail_on_empty=False):
89 raise EngineException("{} with id '{}' already exists for this project".format(self.topic[:-1],
90 final_content["id"]),
91 HTTPStatus.CONFLICT)
tiernob24258a2018-10-04 18:39:49 +020092
93 @staticmethod
94 def format_on_new(content, project_id=None, make_public=False):
95 BaseTopic.format_on_new(content, project_id=project_id, make_public=make_public)
96 content["_admin"]["onboardingState"] = "CREATED"
97 content["_admin"]["operationalState"] = "DISABLED"
tierno36ec8602018-11-02 17:27:11 +010098 content["_admin"]["usageState"] = "NOT_IN_USE"
tiernob24258a2018-10-04 18:39:49 +020099
tiernobee3bad2019-12-05 12:26:01 +0000100 def delete_extra(self, session, _id, db_content, not_send_msg=None):
tiernob4844ab2019-05-23 08:42:12 +0000101 """
102 Deletes file system storage associated with the descriptor
103 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
104 :param _id: server internal id
105 :param db_content: The database content of the descriptor
tiernobee3bad2019-12-05 12:26:01 +0000106 :param not_send_msg: To not send message (False) or store content (list) instead
tiernob4844ab2019-05-23 08:42:12 +0000107 :return: None if ok or raises EngineException with the problem
108 """
tiernob24258a2018-10-04 18:39:49 +0200109 self.fs.file_delete(_id, ignore_non_exist=True)
tiernof717cbe2018-12-03 16:35:42 +0000110 self.fs.file_delete(_id + "_", ignore_non_exist=True) # remove temp folder
tiernob24258a2018-10-04 18:39:49 +0200111
112 @staticmethod
113 def get_one_by_id(db, session, topic, id):
114 # find owned by this project
tierno65ca36d2019-02-12 19:27:52 +0100115 _filter = BaseTopic._get_project_filter(session)
tiernob24258a2018-10-04 18:39:49 +0200116 _filter["id"] = id
117 desc_list = db.get_list(topic, _filter)
118 if len(desc_list) == 1:
119 return desc_list[0]
120 elif len(desc_list) > 1:
121 raise DbException("Found more than one {} with id='{}' belonging to this project".format(topic[:-1], id),
122 HTTPStatus.CONFLICT)
123
124 # not found any: try to find public
tierno65ca36d2019-02-12 19:27:52 +0100125 _filter = BaseTopic._get_project_filter(session)
tiernob24258a2018-10-04 18:39:49 +0200126 _filter["id"] = id
127 desc_list = db.get_list(topic, _filter)
128 if not desc_list:
129 raise DbException("Not found any {} with id='{}'".format(topic[:-1], id), HTTPStatus.NOT_FOUND)
130 elif len(desc_list) == 1:
131 return desc_list[0]
132 else:
133 raise DbException("Found more than one public {} with id='{}'; and no one belonging to this project".format(
134 topic[:-1], id), HTTPStatus.CONFLICT)
135
tierno65ca36d2019-02-12 19:27:52 +0100136 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
tiernob24258a2018-10-04 18:39:49 +0200137 """
138 Creates a new almost empty DISABLED entry into database. Due to SOL005, it does not follow normal procedure.
139 Creating a VNFD or NSD is done in two steps: 1. Creates an empty descriptor (this step) and 2) upload content
140 (self.upload_content)
141 :param rollback: list to append created items at database in case a rollback may to be done
tierno65ca36d2019-02-12 19:27:52 +0100142 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200143 :param indata: data to be inserted
144 :param kwargs: used to override the indata descriptor
145 :param headers: http request headers
tiernobdebce92019-07-01 15:36:49 +0000146 :return: _id, None: identity of the inserted data; and None as there is not any operation
tiernob24258a2018-10-04 18:39:49 +0200147 """
148
tiernod7749582020-05-28 10:41:10 +0000149 # No needed to capture exceptions
150 # Check Quota
151 self.check_quota(session)
delacruzramo32bab472019-09-13 12:24:22 +0200152
tiernod7749582020-05-28 10:41:10 +0000153 # _remove_envelop
154 if indata:
155 if "userDefinedData" in indata:
156 indata = indata['userDefinedData']
tiernob24258a2018-10-04 18:39:49 +0200157
tiernod7749582020-05-28 10:41:10 +0000158 # Override descriptor with query string kwargs
159 self._update_input_with_kwargs(indata, kwargs)
160 # uncomment when this method is implemented.
161 # Avoid override in this case as the target is userDefinedData, but not vnfd,nsd descriptors
162 # indata = DescriptorTopic._validate_input_new(self, indata, project_id=session["force"])
tiernob24258a2018-10-04 18:39:49 +0200163
tiernod7749582020-05-28 10:41:10 +0000164 content = {"_admin": {"userDefinedData": indata}}
165 self.format_on_new(content, session["project_id"], make_public=session["public"])
166 _id = self.db.create(self.topic, content)
167 rollback.append({"topic": self.topic, "_id": _id})
168 self._send_msg("created", {"_id": _id})
169 return _id, None
tiernob24258a2018-10-04 18:39:49 +0200170
tierno65ca36d2019-02-12 19:27:52 +0100171 def upload_content(self, session, _id, indata, kwargs, headers):
tiernob24258a2018-10-04 18:39:49 +0200172 """
173 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 +0100174 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200175 :param _id : the nsd,vnfd is already created, this is the id
176 :param indata: http body request
177 :param kwargs: user query string to override parameters. NOT USED
178 :param headers: http request headers
tierno5a5c2182018-11-20 12:27:42 +0000179 :return: True if package is completely uploaded or False if partial content has been uploded
tiernob24258a2018-10-04 18:39:49 +0200180 Raise exception on error
181 """
182 # Check that _id exists and it is valid
183 current_desc = self.show(session, _id)
184
185 content_range_text = headers.get("Content-Range")
186 expected_md5 = headers.get("Content-File-MD5")
187 compressed = None
188 content_type = headers.get("Content-Type")
189 if content_type and "application/gzip" in content_type or "application/x-gzip" in content_type or \
190 "application/zip" in content_type:
191 compressed = "gzip"
192 filename = headers.get("Content-Filename")
193 if not filename:
194 filename = "package.tar.gz" if compressed else "package"
195 # TODO change to Content-Disposition filename https://tools.ietf.org/html/rfc6266
196 file_pkg = None
197 error_text = ""
198 try:
199 if content_range_text:
200 content_range = content_range_text.replace("-", " ").replace("/", " ").split()
201 if content_range[0] != "bytes": # TODO check x<y not negative < total....
202 raise IndexError()
203 start = int(content_range[1])
204 end = int(content_range[2]) + 1
205 total = int(content_range[3])
206 else:
207 start = 0
tiernof717cbe2018-12-03 16:35:42 +0000208 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 +0200209
210 if start:
tiernof717cbe2018-12-03 16:35:42 +0000211 if not self.fs.file_exists(temp_folder, 'dir'):
tiernob24258a2018-10-04 18:39:49 +0200212 raise EngineException("invalid Transaction-Id header", HTTPStatus.NOT_FOUND)
213 else:
tiernof717cbe2018-12-03 16:35:42 +0000214 self.fs.file_delete(temp_folder, ignore_non_exist=True)
215 self.fs.mkdir(temp_folder)
tiernob24258a2018-10-04 18:39:49 +0200216
217 storage = self.fs.get_params()
218 storage["folder"] = _id
219
tiernof717cbe2018-12-03 16:35:42 +0000220 file_path = (temp_folder, filename)
tiernob24258a2018-10-04 18:39:49 +0200221 if self.fs.file_exists(file_path, 'file'):
222 file_size = self.fs.file_size(file_path)
223 else:
224 file_size = 0
225 if file_size != start:
226 raise EngineException("invalid Content-Range start sequence, expected '{}' but received '{}'".format(
227 file_size, start), HTTPStatus.REQUESTED_RANGE_NOT_SATISFIABLE)
228 file_pkg = self.fs.file_open(file_path, 'a+b')
229 if isinstance(indata, dict):
230 indata_text = yaml.safe_dump(indata, indent=4, default_flow_style=False)
231 file_pkg.write(indata_text.encode(encoding="utf-8"))
232 else:
233 indata_len = 0
234 while True:
235 indata_text = indata.read(4096)
236 indata_len += len(indata_text)
237 if not indata_text:
238 break
239 file_pkg.write(indata_text)
240 if content_range_text:
241 if indata_len != end-start:
242 raise EngineException("Mismatch between Content-Range header {}-{} and body length of {}".format(
243 start, end-1, indata_len), HTTPStatus.REQUESTED_RANGE_NOT_SATISFIABLE)
244 if end != total:
245 # TODO update to UPLOADING
246 return False
247
248 # PACKAGE UPLOADED
249 if expected_md5:
250 file_pkg.seek(0, 0)
251 file_md5 = md5()
252 chunk_data = file_pkg.read(1024)
253 while chunk_data:
254 file_md5.update(chunk_data)
255 chunk_data = file_pkg.read(1024)
256 if expected_md5 != file_md5.hexdigest():
257 raise EngineException("Error, MD5 mismatch", HTTPStatus.CONFLICT)
258 file_pkg.seek(0, 0)
259 if compressed == "gzip":
260 tar = tarfile.open(mode='r', fileobj=file_pkg)
261 descriptor_file_name = None
262 for tarinfo in tar:
263 tarname = tarinfo.name
264 tarname_path = tarname.split("/")
265 if not tarname_path[0] or ".." in tarname_path: # if start with "/" means absolute path
266 raise EngineException("Absolute path or '..' are not allowed for package descriptor tar.gz")
267 if len(tarname_path) == 1 and not tarinfo.isdir():
268 raise EngineException("All files must be inside a dir for package descriptor tar.gz")
269 if tarname.endswith(".yaml") or tarname.endswith(".json") or tarname.endswith(".yml"):
270 storage["pkg-dir"] = tarname_path[0]
271 if len(tarname_path) == 2:
272 if descriptor_file_name:
273 raise EngineException(
274 "Found more than one descriptor file at package descriptor tar.gz")
275 descriptor_file_name = tarname
276 if not descriptor_file_name:
277 raise EngineException("Not found any descriptor file at package descriptor tar.gz")
278 storage["descriptor"] = descriptor_file_name
279 storage["zipfile"] = filename
tiernof717cbe2018-12-03 16:35:42 +0000280 self.fs.file_extract(tar, temp_folder)
281 with self.fs.file_open((temp_folder, descriptor_file_name), "r") as descriptor_file:
tiernob24258a2018-10-04 18:39:49 +0200282 content = descriptor_file.read()
283 else:
284 content = file_pkg.read()
285 storage["descriptor"] = descriptor_file_name = filename
286
287 if descriptor_file_name.endswith(".json"):
288 error_text = "Invalid json format "
289 indata = json.load(content)
290 else:
291 error_text = "Invalid yaml format "
delacruzramob19cadc2019-10-08 10:18:02 +0200292 indata = yaml.load(content, Loader=yaml.SafeLoader)
tiernob24258a2018-10-04 18:39:49 +0200293
294 current_desc["_admin"]["storage"] = storage
295 current_desc["_admin"]["onboardingState"] = "ONBOARDED"
296 current_desc["_admin"]["operationalState"] = "ENABLED"
297
298 indata = self._remove_envelop(indata)
299
300 # Override descriptor with query string kwargs
301 if kwargs:
302 self._update_input_with_kwargs(indata, kwargs)
303 # it will call overrides method at VnfdTopic or NsdTopic
tierno65ca36d2019-02-12 19:27:52 +0100304 # indata = self._validate_input_edit(indata, force=session["force"])
tiernob24258a2018-10-04 18:39:49 +0200305
306 deep_update_rfc7396(current_desc, indata)
tierno65ca36d2019-02-12 19:27:52 +0100307 self.check_conflict_on_edit(session, current_desc, indata, _id=_id)
delacruzramo26301bb2019-11-15 14:45:32 +0100308 current_desc["_admin"]["modified"] = time()
tiernob24258a2018-10-04 18:39:49 +0200309 self.db.replace(self.topic, _id, current_desc)
tiernof717cbe2018-12-03 16:35:42 +0000310 self.fs.dir_rename(temp_folder, _id)
tiernob24258a2018-10-04 18:39:49 +0200311
312 indata["_id"] = _id
K Sai Kiranc96fd692019-10-16 17:50:53 +0530313 self._send_msg("edited", indata)
tiernob24258a2018-10-04 18:39:49 +0200314
315 # TODO if descriptor has changed because kwargs update content and remove cached zip
316 # TODO if zip is not present creates one
317 return True
318
319 except EngineException:
320 raise
321 except IndexError:
322 raise EngineException("invalid Content-Range header format. Expected 'bytes start-end/total'",
323 HTTPStatus.REQUESTED_RANGE_NOT_SATISFIABLE)
324 except IOError as e:
325 raise EngineException("invalid upload transaction sequence: '{}'".format(e), HTTPStatus.BAD_REQUEST)
326 except tarfile.ReadError as e:
327 raise EngineException("invalid file content {}".format(e), HTTPStatus.BAD_REQUEST)
328 except (ValueError, yaml.YAMLError) as e:
329 raise EngineException(error_text + str(e))
330 except ValidationError as e:
331 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
332 finally:
333 if file_pkg:
334 file_pkg.close()
335
336 def get_file(self, session, _id, path=None, accept_header=None):
337 """
338 Return the file content of a vnfd or nsd
tierno65ca36d2019-02-12 19:27:52 +0100339 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tierno87006042018-10-24 12:50:20 +0200340 :param _id: Identity of the vnfd, nsd
tiernob24258a2018-10-04 18:39:49 +0200341 :param path: artifact path or "$DESCRIPTOR" or None
342 :param accept_header: Content of Accept header. Must contain applition/zip or/and text/plain
tierno87006042018-10-24 12:50:20 +0200343 :return: opened file plus Accept format or raises an exception
tiernob24258a2018-10-04 18:39:49 +0200344 """
345 accept_text = accept_zip = False
346 if accept_header:
347 if 'text/plain' in accept_header or '*/*' in accept_header:
348 accept_text = True
349 if 'application/zip' in accept_header or '*/*' in accept_header:
tierno87006042018-10-24 12:50:20 +0200350 accept_zip = 'application/zip'
351 elif 'application/gzip' in accept_header:
352 accept_zip = 'application/gzip'
353
tiernob24258a2018-10-04 18:39:49 +0200354 if not accept_text and not accept_zip:
355 raise EngineException("provide request header 'Accept' with 'application/zip' or 'text/plain'",
356 http_code=HTTPStatus.NOT_ACCEPTABLE)
357
358 content = self.show(session, _id)
359 if content["_admin"]["onboardingState"] != "ONBOARDED":
360 raise EngineException("Cannot get content because this resource is not at 'ONBOARDED' state. "
361 "onboardingState is {}".format(content["_admin"]["onboardingState"]),
362 http_code=HTTPStatus.CONFLICT)
363 storage = content["_admin"]["storage"]
364 if path is not None and path != "$DESCRIPTOR": # artifacts
365 if not storage.get('pkg-dir'):
366 raise EngineException("Packages does not contains artifacts", http_code=HTTPStatus.BAD_REQUEST)
367 if self.fs.file_exists((storage['folder'], storage['pkg-dir'], *path), 'dir'):
368 folder_content = self.fs.dir_ls((storage['folder'], storage['pkg-dir'], *path))
369 return folder_content, "text/plain"
370 # TODO manage folders in http
371 else:
372 return self.fs.file_open((storage['folder'], storage['pkg-dir'], *path), "rb"),\
373 "application/octet-stream"
374
375 # pkgtype accept ZIP TEXT -> result
376 # manyfiles yes X -> zip
377 # no yes -> error
378 # onefile yes no -> zip
379 # X yes -> text
380
381 if accept_text and (not storage.get('pkg-dir') or path == "$DESCRIPTOR"):
382 return self.fs.file_open((storage['folder'], storage['descriptor']), "r"), "text/plain"
383 elif storage.get('pkg-dir') and not accept_zip:
384 raise EngineException("Packages that contains several files need to be retrieved with 'application/zip'"
385 "Accept header", http_code=HTTPStatus.NOT_ACCEPTABLE)
386 else:
387 if not storage.get('zipfile'):
388 # TODO generate zipfile if not present
389 raise EngineException("Only allowed 'text/plain' Accept header for this descriptor. To be solved in "
390 "future versions", http_code=HTTPStatus.NOT_ACCEPTABLE)
tierno87006042018-10-24 12:50:20 +0200391 return self.fs.file_open((storage['folder'], storage['zipfile']), "rb"), accept_zip
tiernob24258a2018-10-04 18:39:49 +0200392
gcalvino46e4cb82018-10-26 13:10:22 +0200393 def pyangbind_validation(self, item, data, force=False):
394 try:
395 if item == "vnfds":
396 myvnfd = vnfd_im()
397 pybindJSONDecoder.load_ietf_json({'vnfd:vnfd-catalog': {'vnfd': [data]}}, None, None, obj=myvnfd,
398 path_helper=True, skip_unknown=force)
399 out = pybindJSON.dumps(myvnfd, mode="ietf")
400 elif item == "nsds":
401 mynsd = nsd_im()
402 pybindJSONDecoder.load_ietf_json({'nsd:nsd-catalog': {'nsd': [data]}}, None, None, obj=mynsd,
403 path_helper=True, skip_unknown=force)
404 out = pybindJSON.dumps(mynsd, mode="ietf")
gcalvino70434c12018-11-27 15:17:04 +0100405 elif item == "nsts":
406 mynst = nst_im()
407 pybindJSONDecoder.load_ietf_json({'nst': [data]}, None, None, obj=mynst,
408 path_helper=True, skip_unknown=force)
409 out = pybindJSON.dumps(mynst, mode="ietf")
gcalvino46e4cb82018-10-26 13:10:22 +0200410 else:
411 raise EngineException("Not possible to validate '{}' item".format(item),
412 http_code=HTTPStatus.INTERNAL_SERVER_ERROR)
413
414 desc_out = self._remove_envelop(yaml.safe_load(out))
415 return desc_out
416
417 except Exception as e:
418 raise EngineException("Error in pyangbind validation: {}".format(str(e)),
419 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
420
tiernob24258a2018-10-04 18:39:49 +0200421
422class VnfdTopic(DescriptorTopic):
423 topic = "vnfds"
424 topic_msg = "vnfd"
425
delacruzramo32bab472019-09-13 12:24:22 +0200426 def __init__(self, db, fs, msg, auth):
427 DescriptorTopic.__init__(self, db, fs, msg, auth)
tiernob24258a2018-10-04 18:39:49 +0200428
429 @staticmethod
430 def _remove_envelop(indata=None):
431 if not indata:
432 return {}
433 clean_indata = indata
434 if clean_indata.get('vnfd:vnfd-catalog'):
435 clean_indata = clean_indata['vnfd:vnfd-catalog']
436 elif clean_indata.get('vnfd-catalog'):
437 clean_indata = clean_indata['vnfd-catalog']
438 if clean_indata.get('vnfd'):
439 if not isinstance(clean_indata['vnfd'], list) or len(clean_indata['vnfd']) != 1:
gcalvino46e4cb82018-10-26 13:10:22 +0200440 raise EngineException("'vnfd' must be a list of only one element")
tiernob24258a2018-10-04 18:39:49 +0200441 clean_indata = clean_indata['vnfd'][0]
gcalvino46e4cb82018-10-26 13:10:22 +0200442 elif clean_indata.get('vnfd:vnfd'):
443 if not isinstance(clean_indata['vnfd:vnfd'], list) or len(clean_indata['vnfd:vnfd']) != 1:
444 raise EngineException("'vnfd:vnfd' must be a list of only one element")
445 clean_indata = clean_indata['vnfd:vnfd'][0]
tiernob24258a2018-10-04 18:39:49 +0200446 return clean_indata
447
tierno65ca36d2019-02-12 19:27:52 +0100448 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
449 super().check_conflict_on_edit(session, final_content, edit_content, _id)
tierno36ec8602018-11-02 17:27:11 +0100450
451 # set type of vnfd
452 contains_pdu = False
453 contains_vdu = False
454 for vdu in get_iterable(final_content.get("vdu")):
455 if vdu.get("pdu-type"):
456 contains_pdu = True
457 else:
458 contains_vdu = True
459 if contains_pdu:
460 final_content["_admin"]["type"] = "hnfd" if contains_vdu else "pnfd"
461 elif contains_vdu:
462 final_content["_admin"]["type"] = "vnfd"
463 # if neither vud nor pdu do not fill type
464
tiernob4844ab2019-05-23 08:42:12 +0000465 def check_conflict_on_del(self, session, _id, db_content):
tiernob24258a2018-10-04 18:39:49 +0200466 """
467 Check that there is not any NSD that uses this VNFD. Only NSDs belonging to this project are considered. Note
468 that VNFD can be public and be used by NSD of other projects. Also check there are not deployments, or vnfr
469 that uses this vnfd
tierno65ca36d2019-02-12 19:27:52 +0100470 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob4844ab2019-05-23 08:42:12 +0000471 :param _id: vnfd internal id
472 :param db_content: The database content of the _id.
tiernob24258a2018-10-04 18:39:49 +0200473 :return: None or raises EngineException with the conflict
474 """
tierno65ca36d2019-02-12 19:27:52 +0100475 if session["force"]:
tiernob24258a2018-10-04 18:39:49 +0200476 return
tiernob4844ab2019-05-23 08:42:12 +0000477 descriptor = db_content
tiernob24258a2018-10-04 18:39:49 +0200478 descriptor_id = descriptor.get("id")
479 if not descriptor_id: # empty vnfd not uploaded
480 return
481
tierno65ca36d2019-02-12 19:27:52 +0100482 _filter = self._get_project_filter(session)
tiernob4844ab2019-05-23 08:42:12 +0000483
tiernob24258a2018-10-04 18:39:49 +0200484 # check vnfrs using this vnfd
485 _filter["vnfd-id"] = _id
486 if self.db.get_list("vnfrs", _filter):
tiernob4844ab2019-05-23 08:42:12 +0000487 raise EngineException("There is at least one VNF using this descriptor", http_code=HTTPStatus.CONFLICT)
488
489 # check NSD referencing this VNFD
tiernob24258a2018-10-04 18:39:49 +0200490 del _filter["vnfd-id"]
tiernob24258a2018-10-04 18:39:49 +0200491 _filter["constituent-vnfd.ANYINDEX.vnfd-id-ref"] = descriptor_id
492 if self.db.get_list("nsds", _filter):
tiernob4844ab2019-05-23 08:42:12 +0000493 raise EngineException("There is at least one NSD referencing this descriptor",
494 http_code=HTTPStatus.CONFLICT)
tiernob24258a2018-10-04 18:39:49 +0200495
gcalvinoa6fe0002019-01-09 13:27:11 +0100496 def _validate_input_new(self, indata, storage_params, force=False):
gcalvino46e4cb82018-10-26 13:10:22 +0200497 indata = self.pyangbind_validation("vnfds", indata, force)
gcalvino5e72d152018-10-23 11:46:57 +0200498 # Cross references validation in the descriptor
gcalvinoe45aded2018-11-13 17:17:28 +0100499 if indata.get("vdu"):
500 if not indata.get("mgmt-interface"):
501 raise EngineException("'mgmt-interface' is a mandatory field and it is not defined",
tierno40fbcad2018-10-26 10:58:15 +0200502 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
gcalvinoe45aded2018-11-13 17:17:28 +0100503 if indata["mgmt-interface"].get("cp"):
504 for cp in get_iterable(indata.get("connection-point")):
505 if cp["name"] == indata["mgmt-interface"]["cp"]:
506 break
507 else:
508 raise EngineException("mgmt-interface:cp='{}' must match an existing connection-point"
509 .format(indata["mgmt-interface"]["cp"]),
510 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
gcalvino5e72d152018-10-23 11:46:57 +0200511
512 for vdu in get_iterable(indata.get("vdu")):
delacruzramo0f9b9662020-02-06 13:37:01 +0000513 icp_refs = []
514 ecp_refs = []
gcalvino5e72d152018-10-23 11:46:57 +0200515 for interface in get_iterable(vdu.get("interface")):
516 if interface.get("external-connection-point-ref"):
delacruzramo0f9b9662020-02-06 13:37:01 +0000517 if interface.get("external-connection-point-ref") in ecp_refs:
518 raise EngineException("vdu[id='{}']:interface[name='{}']:external-connection-point-ref='{}' "
519 "is referenced by other interface"
520 .format(vdu["id"], interface["name"],
521 interface["external-connection-point-ref"]),
522 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
523 ecp_refs.append(interface.get("external-connection-point-ref"))
gcalvino5e72d152018-10-23 11:46:57 +0200524 for cp in get_iterable(indata.get("connection-point")):
tierno40fbcad2018-10-26 10:58:15 +0200525 if cp["name"] == interface["external-connection-point-ref"]:
gcalvino5e72d152018-10-23 11:46:57 +0200526 break
527 else:
tierno40fbcad2018-10-26 10:58:15 +0200528 raise EngineException("vdu[id='{}']:interface[name='{}']:external-connection-point-ref='{}' "
gcalvino5e72d152018-10-23 11:46:57 +0200529 "must match an existing connection-point"
530 .format(vdu["id"], interface["name"],
531 interface["external-connection-point-ref"]),
532 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
tierno40fbcad2018-10-26 10:58:15 +0200533 elif interface.get("internal-connection-point-ref"):
delacruzramo0f9b9662020-02-06 13:37:01 +0000534 if interface.get("internal-connection-point-ref") in icp_refs:
535 raise EngineException("vdu[id='{}']:interface[name='{}']:internal-connection-point-ref='{}' "
536 "is referenced by other interface"
537 .format(vdu["id"], interface["name"],
538 interface["internal-connection-point-ref"]),
539 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
540 icp_refs.append(interface.get("internal-connection-point-ref"))
gcalvino5e72d152018-10-23 11:46:57 +0200541 for internal_cp in get_iterable(vdu.get("internal-connection-point")):
tierno40fbcad2018-10-26 10:58:15 +0200542 if interface["internal-connection-point-ref"] == internal_cp.get("id"):
gcalvino5e72d152018-10-23 11:46:57 +0200543 break
544 else:
tierno40fbcad2018-10-26 10:58:15 +0200545 raise EngineException("vdu[id='{}']:interface[name='{}']:internal-connection-point-ref='{}' "
546 "must match an existing vdu:internal-connection-point"
547 .format(vdu["id"], interface["name"],
548 interface["internal-connection-point-ref"]),
gcalvino5e72d152018-10-23 11:46:57 +0200549 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
gcalvinoa6fe0002019-01-09 13:27:11 +0100550 # Validate that if descriptor contains charms, artifacts _admin.storage."pkg-dir" is not none
551 if vdu.get("vdu-configuration"):
552 if vdu["vdu-configuration"].get("juju"):
553 if not self._validate_package_folders(storage_params, 'charms'):
554 raise EngineException("Charm defined in vnf[id={}]:vdu[id={}] but not present in "
555 "package".format(indata["id"], vdu["id"]))
556 # Validate that if descriptor contains cloud-init, artifacts _admin.storage."pkg-dir" is not none
557 if vdu.get("cloud-init-file"):
558 if not self._validate_package_folders(storage_params, 'cloud_init', vdu["cloud-init-file"]):
559 raise EngineException("Cloud-init defined in vnf[id={}]:vdu[id={}] but not present in "
560 "package".format(indata["id"], vdu["id"]))
561 # Validate that if descriptor contains charms, artifacts _admin.storage."pkg-dir" is not none
562 if indata.get("vnf-configuration"):
563 if indata["vnf-configuration"].get("juju"):
564 if not self._validate_package_folders(storage_params, 'charms'):
565 raise EngineException("Charm defined in vnf[id={}] but not present in "
566 "package".format(indata["id"]))
delacruzramo5727a372019-03-28 12:29:04 +0100567 vld_names = [] # For detection of duplicated VLD names
gcalvino5e72d152018-10-23 11:46:57 +0200568 for ivld in get_iterable(indata.get("internal-vld")):
delacruzramo5727a372019-03-28 12:29:04 +0100569 # BEGIN Detection of duplicated VLD names
tierno75d5a4e2020-05-21 15:09:22 +0000570 ivld_name = ivld.get("name")
571 if ivld_name:
572 if ivld_name in vld_names:
573 raise EngineException("Duplicated VLD name '{}' in vnfd[id={}]:internal-vld[id={}]"
574 .format(ivld["name"], indata["id"], ivld["id"]),
575 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
576 else:
577 vld_names.append(ivld_name)
delacruzramo5727a372019-03-28 12:29:04 +0100578 # END Detection of duplicated VLD names
gcalvino5e72d152018-10-23 11:46:57 +0200579 for icp in get_iterable(ivld.get("internal-connection-point")):
580 icp_mark = False
581 for vdu in get_iterable(indata.get("vdu")):
582 for internal_cp in get_iterable(vdu.get("internal-connection-point")):
583 if icp["id-ref"] == internal_cp["id"]:
584 icp_mark = True
585 break
586 if icp_mark:
587 break
588 else:
tierno40fbcad2018-10-26 10:58:15 +0200589 raise EngineException("internal-vld[id='{}']:internal-connection-point='{}' must match an existing "
590 "vdu:internal-connection-point".format(ivld["id"], icp["id-ref"]),
gcalvino5e72d152018-10-23 11:46:57 +0200591 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
592 if ivld.get("ip-profile-ref"):
tierno40fbcad2018-10-26 10:58:15 +0200593 for ip_prof in get_iterable(indata.get("ip-profiles")):
gcalvino5e72d152018-10-23 11:46:57 +0200594 if ip_prof["name"] == get_iterable(ivld.get("ip-profile-ref")):
595 break
596 else:
tierno40fbcad2018-10-26 10:58:15 +0200597 raise EngineException("internal-vld[id='{}']:ip-profile-ref='{}' does not exist".format(
gcalvino5e72d152018-10-23 11:46:57 +0200598 ivld["id"], ivld["ip-profile-ref"]),
599 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
600 for mp in get_iterable(indata.get("monitoring-param")):
601 if mp.get("vdu-monitoring-param"):
602 mp_vmp_mark = False
603 for vdu in get_iterable(indata.get("vdu")):
604 for vmp in get_iterable(vdu.get("monitoring-param")):
tierno40fbcad2018-10-26 10:58:15 +0200605 if vmp["id"] == mp["vdu-monitoring-param"].get("vdu-monitoring-param-ref") and vdu["id"] ==\
gcalvino5e72d152018-10-23 11:46:57 +0200606 mp["vdu-monitoring-param"]["vdu-ref"]:
607 mp_vmp_mark = True
608 break
609 if mp_vmp_mark:
610 break
611 else:
612 raise EngineException("monitoring-param:vdu-monitoring-param:vdu-monitoring-param-ref='{}' not "
tierno40fbcad2018-10-26 10:58:15 +0200613 "defined at vdu[id='{}'] or vdu does not exist"
gcalvino5e72d152018-10-23 11:46:57 +0200614 .format(mp["vdu-monitoring-param"]["vdu-monitoring-param-ref"],
615 mp["vdu-monitoring-param"]["vdu-ref"]),
616 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
617 elif mp.get("vdu-metric"):
618 mp_vm_mark = False
619 for vdu in get_iterable(indata.get("vdu")):
620 if vdu.get("vdu-configuration"):
621 for metric in get_iterable(vdu["vdu-configuration"].get("metrics")):
622 if metric["name"] == mp["vdu-metric"]["vdu-metric-name-ref"] and vdu["id"] == \
623 mp["vdu-metric"]["vdu-ref"]:
624 mp_vm_mark = True
625 break
626 if mp_vm_mark:
627 break
628 else:
tierno40fbcad2018-10-26 10:58:15 +0200629 raise EngineException("monitoring-param:vdu-metric:vdu-metric-name-ref='{}' not defined at "
630 "vdu[id='{}'] or vdu does not exist"
gcalvino5e72d152018-10-23 11:46:57 +0200631 .format(mp["vdu-metric"]["vdu-metric-name-ref"],
632 mp["vdu-metric"]["vdu-ref"]),
633 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
634
635 for sgd in get_iterable(indata.get("scaling-group-descriptor")):
636 for sp in get_iterable(sgd.get("scaling-policy")):
637 for sc in get_iterable(sp.get("scaling-criteria")):
638 for mp in get_iterable(indata.get("monitoring-param")):
639 if mp["id"] == get_iterable(sc.get("vnf-monitoring-param-ref")):
640 break
641 else:
tierno40fbcad2018-10-26 10:58:15 +0200642 raise EngineException("scaling-group-descriptor[name='{}']:scaling-criteria[name='{}']:"
643 "vnf-monitoring-param-ref='{}' not defined in any monitoring-param"
644 .format(sgd["name"], sc["name"], sc["vnf-monitoring-param-ref"]),
gcalvino5e72d152018-10-23 11:46:57 +0200645 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
646 for sgd_vdu in get_iterable(sgd.get("vdu")):
647 sgd_vdu_mark = False
648 for vdu in get_iterable(indata.get("vdu")):
649 if vdu["id"] == sgd_vdu["vdu-id-ref"]:
650 sgd_vdu_mark = True
651 break
652 if sgd_vdu_mark:
653 break
654 else:
tierno40fbcad2018-10-26 10:58:15 +0200655 raise EngineException("scaling-group-descriptor[name='{}']:vdu-id-ref={} does not match any vdu"
656 .format(sgd["name"], sgd_vdu["vdu-id-ref"]),
gcalvino5e72d152018-10-23 11:46:57 +0200657 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
658 for sca in get_iterable(sgd.get("scaling-config-action")):
tierno40fbcad2018-10-26 10:58:15 +0200659 if not indata.get("vnf-configuration"):
660 raise EngineException("'vnf-configuration' not defined in the descriptor but it is referenced by "
661 "scaling-group-descriptor[name='{}']:scaling-config-action"
662 .format(sgd["name"]),
gcalvino5e72d152018-10-23 11:46:57 +0200663 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
tierno40fbcad2018-10-26 10:58:15 +0200664 for primitive in get_iterable(indata["vnf-configuration"].get("config-primitive")):
665 if primitive["name"] == sca["vnf-config-primitive-name-ref"]:
666 break
667 else:
668 raise EngineException("scaling-group-descriptor[name='{}']:scaling-config-action:vnf-config-"
669 "primitive-name-ref='{}' does not match any "
670 "vnf-configuration:config-primitive:name"
671 .format(sgd["name"], sca["vnf-config-primitive-name-ref"]),
672 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
tiernob24258a2018-10-04 18:39:49 +0200673 return indata
674
675 def _validate_input_edit(self, indata, force=False):
tiernoaa1ca7b2018-11-08 19:00:20 +0100676 # not needed to validate with pyangbind becuase it will be validated at check_conflict_on_edit
tiernob24258a2018-10-04 18:39:49 +0200677 return indata
678
gcalvinoa6fe0002019-01-09 13:27:11 +0100679 def _validate_package_folders(self, storage_params, folder, file=None):
680 if not storage_params or not storage_params.get("pkg-dir"):
681 return False
682 else:
683 if self.fs.file_exists("{}_".format(storage_params["folder"]), 'dir'):
684 f = "{}_/{}/{}".format(storage_params["folder"], storage_params["pkg-dir"], folder)
685 else:
686 f = "{}/{}/{}".format(storage_params["folder"], storage_params["pkg-dir"], folder)
687 if file:
688 return self.fs.file_exists("{}/{}".format(f, file), 'file')
689 else:
690 if self.fs.file_exists(f, 'dir'):
691 if self.fs.dir_ls(f):
692 return True
693 return False
694
delacruzramo271d2002019-12-02 21:00:37 +0100695 def delete_extra(self, session, _id, db_content, not_send_msg=None):
696 """
697 Deletes associate file system storage (via super)
698 Deletes associated vnfpkgops from database.
699 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
700 :param _id: server internal id
701 :param db_content: The database content of the descriptor
702 :return: None
703 :raises: FsException in case of error while deleting associated storage
704 """
705 super().delete_extra(session, _id, db_content, not_send_msg)
706 self.db.del_list("vnfpkgops", {"vnfPkgId": _id})
707
tiernob24258a2018-10-04 18:39:49 +0200708
709class NsdTopic(DescriptorTopic):
710 topic = "nsds"
711 topic_msg = "nsd"
712
delacruzramo32bab472019-09-13 12:24:22 +0200713 def __init__(self, db, fs, msg, auth):
714 DescriptorTopic.__init__(self, db, fs, msg, auth)
tiernob24258a2018-10-04 18:39:49 +0200715
716 @staticmethod
717 def _remove_envelop(indata=None):
718 if not indata:
719 return {}
720 clean_indata = indata
721
722 if clean_indata.get('nsd:nsd-catalog'):
723 clean_indata = clean_indata['nsd:nsd-catalog']
724 elif clean_indata.get('nsd-catalog'):
725 clean_indata = clean_indata['nsd-catalog']
726 if clean_indata.get('nsd'):
727 if not isinstance(clean_indata['nsd'], list) or len(clean_indata['nsd']) != 1:
gcalvino46e4cb82018-10-26 13:10:22 +0200728 raise EngineException("'nsd' must be a list of only one element")
tiernob24258a2018-10-04 18:39:49 +0200729 clean_indata = clean_indata['nsd'][0]
gcalvino46e4cb82018-10-26 13:10:22 +0200730 elif clean_indata.get('nsd:nsd'):
731 if not isinstance(clean_indata['nsd:nsd'], list) or len(clean_indata['nsd:nsd']) != 1:
732 raise EngineException("'nsd:nsd' must be a list of only one element")
733 clean_indata = clean_indata['nsd:nsd'][0]
tiernob24258a2018-10-04 18:39:49 +0200734 return clean_indata
735
gcalvinoa6fe0002019-01-09 13:27:11 +0100736 def _validate_input_new(self, indata, storage_params, force=False):
gcalvino46e4cb82018-10-26 13:10:22 +0200737 indata = self.pyangbind_validation("nsds", indata, force)
tierno5a5c2182018-11-20 12:27:42 +0000738 # Cross references validation in the descriptor
tiernoaa1ca7b2018-11-08 19:00:20 +0100739 # TODO validata that if contains cloud-init-file or charms, have artifacts _admin.storage."pkg-dir" is not none
tierno5a5c2182018-11-20 12:27:42 +0000740 for vld in get_iterable(indata.get("vld")):
vijay.r8ba4e052019-03-29 17:52:26 +0530741 if vld.get("mgmt-network") and vld.get("ip-profile-ref"):
742 raise EngineException("Error at vld[id='{}']:ip-profile-ref"
743 " You cannot set an ip-profile when mgmt-network is True"
744 .format(vld["id"]), http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
tierno5a5c2182018-11-20 12:27:42 +0000745 for vnfd_cp in get_iterable(vld.get("vnfd-connection-point-ref")):
746 for constituent_vnfd in get_iterable(indata.get("constituent-vnfd")):
747 if vnfd_cp["member-vnf-index-ref"] == constituent_vnfd["member-vnf-index"]:
748 if vnfd_cp.get("vnfd-id-ref") and vnfd_cp["vnfd-id-ref"] != constituent_vnfd["vnfd-id-ref"]:
749 raise EngineException("Error at vld[id='{}']:vnfd-connection-point-ref[vnfd-id-ref='{}'] "
750 "does not match constituent-vnfd[member-vnf-index='{}']:vnfd-id-ref"
751 " '{}'".format(vld["id"], vnfd_cp["vnfd-id-ref"],
752 constituent_vnfd["member-vnf-index"],
753 constituent_vnfd["vnfd-id-ref"]),
754 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
755 break
756 else:
757 raise EngineException("Error at vld[id='{}']:vnfd-connection-point-ref[member-vnf-index-ref='{}'] "
758 "does not match any constituent-vnfd:member-vnf-index"
759 .format(vld["id"], vnfd_cp["member-vnf-index-ref"]),
760 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
delacruzramo6a92a532019-11-22 15:06:43 +0100761 # Check VNFFGD
762 for fgd in get_iterable(indata.get("vnffgd")):
763 for cls in get_iterable(fgd.get("classifier")):
764 rspref = cls.get("rsp-id-ref")
765 for rsp in get_iterable(fgd.get("rsp")):
766 rspid = rsp.get("id")
767 if rspid and rspref and rspid == rspref:
768 break
769 else:
770 raise EngineException(
771 "Error at vnffgd[id='{}']:classifier[id='{}']:rsp-id-ref '{}' does not match any rsp:id"
772 .format(fgd["id"], cls["id"], rspref),
773 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
tiernob24258a2018-10-04 18:39:49 +0200774 return indata
775
776 def _validate_input_edit(self, indata, force=False):
tiernoaa1ca7b2018-11-08 19:00:20 +0100777 # not needed to validate with pyangbind becuase it will be validated at check_conflict_on_edit
tiernob24258a2018-10-04 18:39:49 +0200778 return indata
779
tierno65ca36d2019-02-12 19:27:52 +0100780 def _check_descriptor_dependencies(self, session, descriptor):
tiernob24258a2018-10-04 18:39:49 +0200781 """
tierno5a5c2182018-11-20 12:27:42 +0000782 Check that the dependent descriptors exist on a new descriptor or edition. Also checks references to vnfd
783 connection points are ok
tierno65ca36d2019-02-12 19:27:52 +0100784 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200785 :param descriptor: descriptor to be inserted or edit
786 :return: None or raises exception
787 """
tierno65ca36d2019-02-12 19:27:52 +0100788 if session["force"]:
tiernob24258a2018-10-04 18:39:49 +0200789 return
tierno5a5c2182018-11-20 12:27:42 +0000790 member_vnfd_index = {}
tierno65ca36d2019-02-12 19:27:52 +0100791 if descriptor.get("constituent-vnfd") and not session["force"]:
tierno5a5c2182018-11-20 12:27:42 +0000792 for vnf in descriptor["constituent-vnfd"]:
793 vnfd_id = vnf["vnfd-id-ref"]
tierno65ca36d2019-02-12 19:27:52 +0100794 filter_q = self._get_project_filter(session)
tierno5a5c2182018-11-20 12:27:42 +0000795 filter_q["id"] = vnfd_id
796 vnf_list = self.db.get_list("vnfds", filter_q)
797 if not vnf_list:
798 raise EngineException("Descriptor error at 'constituent-vnfd':'vnfd-id-ref'='{}' references a non "
799 "existing vnfd".format(vnfd_id), http_code=HTTPStatus.CONFLICT)
800 # elif len(vnf_list) > 1:
801 # raise EngineException("More than one vnfd found for id='{}'".format(vnfd_id),
802 # http_code=HTTPStatus.CONFLICT)
803 member_vnfd_index[vnf["member-vnf-index"]] = vnf_list[0]
804
805 # Cross references validation in the descriptor and vnfd connection point validation
806 for vld in get_iterable(descriptor.get("vld")):
807 for referenced_vnfd_cp in get_iterable(vld.get("vnfd-connection-point-ref")):
808 # look if this vnfd contains this connection point
809 vnfd = member_vnfd_index.get(referenced_vnfd_cp["member-vnf-index-ref"])
tierno5a5c2182018-11-20 12:27:42 +0000810 for vnfd_cp in get_iterable(vnfd.get("connection-point")):
811 if referenced_vnfd_cp.get("vnfd-connection-point-ref") == vnfd_cp["name"]:
812 break
813 else:
814 raise EngineException(
815 "Error at vld[id='{}']:vnfd-connection-point-ref[member-vnf-index-ref='{}']:vnfd-"
816 "connection-point-ref='{}' references a non existing conection-point:name inside vnfd '{}'"
817 .format(vld["id"], referenced_vnfd_cp["member-vnf-index-ref"],
818 referenced_vnfd_cp["vnfd-connection-point-ref"], vnfd["id"]),
819 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
tiernob24258a2018-10-04 18:39:49 +0200820
tierno65ca36d2019-02-12 19:27:52 +0100821 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
822 super().check_conflict_on_edit(session, final_content, edit_content, _id)
tiernob24258a2018-10-04 18:39:49 +0200823
tierno65ca36d2019-02-12 19:27:52 +0100824 self._check_descriptor_dependencies(session, final_content)
tiernob24258a2018-10-04 18:39:49 +0200825
tiernob4844ab2019-05-23 08:42:12 +0000826 def check_conflict_on_del(self, session, _id, db_content):
tiernob24258a2018-10-04 18:39:49 +0200827 """
828 Check that there is not any NSR that uses this NSD. Only NSRs belonging to this project are considered. Note
829 that NSD can be public and be used by other projects.
tierno65ca36d2019-02-12 19:27:52 +0100830 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob4844ab2019-05-23 08:42:12 +0000831 :param _id: nsd internal id
832 :param db_content: The database content of the _id
tiernob24258a2018-10-04 18:39:49 +0200833 :return: None or raises EngineException with the conflict
834 """
tierno65ca36d2019-02-12 19:27:52 +0100835 if session["force"]:
tiernob24258a2018-10-04 18:39:49 +0200836 return
tiernob4844ab2019-05-23 08:42:12 +0000837 descriptor = db_content
838 descriptor_id = descriptor.get("id")
839 if not descriptor_id: # empty nsd not uploaded
840 return
841
842 # check NSD used by NS
tierno65ca36d2019-02-12 19:27:52 +0100843 _filter = self._get_project_filter(session)
tiernob4844ab2019-05-23 08:42:12 +0000844 _filter["nsd-id"] = _id
tiernob24258a2018-10-04 18:39:49 +0200845 if self.db.get_list("nsrs", _filter):
tiernob4844ab2019-05-23 08:42:12 +0000846 raise EngineException("There is at least one NS using this descriptor", http_code=HTTPStatus.CONFLICT)
847
848 # check NSD referenced by NST
849 del _filter["nsd-id"]
850 _filter["netslice-subnet.ANYINDEX.nsd-ref"] = descriptor_id
851 if self.db.get_list("nsts", _filter):
852 raise EngineException("There is at least one NetSlice Template referencing this descriptor",
853 http_code=HTTPStatus.CONFLICT)
tiernob24258a2018-10-04 18:39:49 +0200854
855
Felipe Vicensb57758d2018-10-16 16:00:20 +0200856class NstTopic(DescriptorTopic):
857 topic = "nsts"
858 topic_msg = "nst"
tierno6b02b052020-06-02 10:07:41 +0000859 quota_name = "slice_templates"
Felipe Vicensb57758d2018-10-16 16:00:20 +0200860
delacruzramo32bab472019-09-13 12:24:22 +0200861 def __init__(self, db, fs, msg, auth):
862 DescriptorTopic.__init__(self, db, fs, msg, auth)
Felipe Vicensb57758d2018-10-16 16:00:20 +0200863
864 @staticmethod
865 def _remove_envelop(indata=None):
866 if not indata:
867 return {}
868 clean_indata = indata
869
Felipe Vicensb57758d2018-10-16 16:00:20 +0200870 if clean_indata.get('nst'):
871 if not isinstance(clean_indata['nst'], list) or len(clean_indata['nst']) != 1:
872 raise EngineException("'nst' must be a list only one element")
873 clean_indata = clean_indata['nst'][0]
gcalvino70434c12018-11-27 15:17:04 +0100874 elif clean_indata.get('nst:nst'):
875 if not isinstance(clean_indata['nst:nst'], list) or len(clean_indata['nst:nst']) != 1:
876 raise EngineException("'nst:nst' must be a list only one element")
877 clean_indata = clean_indata['nst:nst'][0]
Felipe Vicensb57758d2018-10-16 16:00:20 +0200878 return clean_indata
879
Felipe Vicensb57758d2018-10-16 16:00:20 +0200880 def _validate_input_edit(self, indata, force=False):
881 # TODO validate with pyangbind, serialize
882 return indata
883
gcalvinoa6fe0002019-01-09 13:27:11 +0100884 def _validate_input_new(self, indata, storage_params, force=False):
gcalvino70434c12018-11-27 15:17:04 +0100885 indata = self.pyangbind_validation("nsts", indata, force)
Felipe Vicense36ab852018-11-23 14:12:09 +0100886 return indata.copy()
887
Felipe Vicensb57758d2018-10-16 16:00:20 +0200888 def _check_descriptor_dependencies(self, session, descriptor):
889 """
890 Check that the dependent descriptors exist on a new descriptor or edition
tierno65ca36d2019-02-12 19:27:52 +0100891 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Felipe Vicensb57758d2018-10-16 16:00:20 +0200892 :param descriptor: descriptor to be inserted or edit
893 :return: None or raises exception
894 """
895 if not descriptor.get("netslice-subnet"):
896 return
897 for nsd in descriptor["netslice-subnet"]:
898 nsd_id = nsd["nsd-ref"]
tierno65ca36d2019-02-12 19:27:52 +0100899 filter_q = self._get_project_filter(session)
Felipe Vicensb57758d2018-10-16 16:00:20 +0200900 filter_q["id"] = nsd_id
901 if not self.db.get_list("nsds", filter_q):
902 raise EngineException("Descriptor error at 'netslice-subnet':'nsd-ref'='{}' references a non "
903 "existing nsd".format(nsd_id), http_code=HTTPStatus.CONFLICT)
904
tierno65ca36d2019-02-12 19:27:52 +0100905 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
906 super().check_conflict_on_edit(session, final_content, edit_content, _id)
Felipe Vicensb57758d2018-10-16 16:00:20 +0200907
908 self._check_descriptor_dependencies(session, final_content)
909
tiernob4844ab2019-05-23 08:42:12 +0000910 def check_conflict_on_del(self, session, _id, db_content):
Felipe Vicensb57758d2018-10-16 16:00:20 +0200911 """
912 Check that there is not any NSIR that uses this NST. Only NSIRs belonging to this project are considered. Note
913 that NST can be public and be used by other projects.
tierno65ca36d2019-02-12 19:27:52 +0100914 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Felipe Vicens07f31722018-10-29 15:16:44 +0100915 :param _id: nst internal id
tiernob4844ab2019-05-23 08:42:12 +0000916 :param db_content: The database content of the _id.
Felipe Vicensb57758d2018-10-16 16:00:20 +0200917 :return: None or raises EngineException with the conflict
918 """
919 # TODO: Check this method
tierno65ca36d2019-02-12 19:27:52 +0100920 if session["force"]:
Felipe Vicensb57758d2018-10-16 16:00:20 +0200921 return
Felipe Vicens07f31722018-10-29 15:16:44 +0100922 # Get Network Slice Template from Database
tierno65ca36d2019-02-12 19:27:52 +0100923 _filter = self._get_project_filter(session)
tiernoea97c042019-09-13 09:44:42 +0000924 _filter["_admin.nst-id"] = _id
tiernob4844ab2019-05-23 08:42:12 +0000925 if self.db.get_list("nsis", _filter):
926 raise EngineException("there is at least one Netslice Instance using this descriptor",
927 http_code=HTTPStatus.CONFLICT)
Felipe Vicensb57758d2018-10-16 16:00:20 +0200928
929
tiernob24258a2018-10-04 18:39:49 +0200930class PduTopic(BaseTopic):
931 topic = "pdus"
932 topic_msg = "pdu"
tierno6b02b052020-06-02 10:07:41 +0000933 quota_name = "pduds"
tiernob24258a2018-10-04 18:39:49 +0200934 schema_new = pdu_new_schema
935 schema_edit = pdu_edit_schema
936
delacruzramo32bab472019-09-13 12:24:22 +0200937 def __init__(self, db, fs, msg, auth):
938 BaseTopic.__init__(self, db, fs, msg, auth)
tiernob24258a2018-10-04 18:39:49 +0200939
940 @staticmethod
941 def format_on_new(content, project_id=None, make_public=False):
tierno36ec8602018-11-02 17:27:11 +0100942 BaseTopic.format_on_new(content, project_id=project_id, make_public=make_public)
tiernob24258a2018-10-04 18:39:49 +0200943 content["_admin"]["onboardingState"] = "CREATED"
tierno36ec8602018-11-02 17:27:11 +0100944 content["_admin"]["operationalState"] = "ENABLED"
945 content["_admin"]["usageState"] = "NOT_IN_USE"
tiernob24258a2018-10-04 18:39:49 +0200946
tiernob4844ab2019-05-23 08:42:12 +0000947 def check_conflict_on_del(self, session, _id, db_content):
948 """
949 Check that there is not any vnfr that uses this PDU
950 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
951 :param _id: pdu internal id
952 :param db_content: The database content of the _id.
953 :return: None or raises EngineException with the conflict
954 """
tierno65ca36d2019-02-12 19:27:52 +0100955 if session["force"]:
tiernob24258a2018-10-04 18:39:49 +0200956 return
tiernob4844ab2019-05-23 08:42:12 +0000957
958 _filter = self._get_project_filter(session)
959 _filter["vdur.pdu-id"] = _id
tiernob24258a2018-10-04 18:39:49 +0200960 if self.db.get_list("vnfrs", _filter):
tiernob4844ab2019-05-23 08:42:12 +0000961 raise EngineException("There is at least one VNF using this PDU", http_code=HTTPStatus.CONFLICT)
delacruzramo271d2002019-12-02 21:00:37 +0100962
963
964class VnfPkgOpTopic(BaseTopic):
965 topic = "vnfpkgops"
966 topic_msg = "vnfd"
967 schema_new = vnfpkgop_new_schema
968 schema_edit = None
969
970 def __init__(self, db, fs, msg, auth):
971 BaseTopic.__init__(self, db, fs, msg, auth)
972
973 def edit(self, session, _id, indata=None, kwargs=None, content=None):
974 raise EngineException("Method 'edit' not allowed for topic '{}'".format(self.topic),
975 HTTPStatus.METHOD_NOT_ALLOWED)
976
977 def delete(self, session, _id, dry_run=False):
978 raise EngineException("Method 'delete' not allowed for topic '{}'".format(self.topic),
979 HTTPStatus.METHOD_NOT_ALLOWED)
980
981 def delete_list(self, session, filter_q=None):
982 raise EngineException("Method 'delete_list' not allowed for topic '{}'".format(self.topic),
983 HTTPStatus.METHOD_NOT_ALLOWED)
984
985 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
986 """
987 Creates a new entry into database.
988 :param rollback: list to append created items at database in case a rollback may to be done
989 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
990 :param indata: data to be inserted
991 :param kwargs: used to override the indata descriptor
992 :param headers: http request headers
993 :return: _id, op_id:
994 _id: identity of the inserted data.
995 op_id: None
996 """
997 self._update_input_with_kwargs(indata, kwargs)
998 validate_input(indata, self.schema_new)
999 vnfpkg_id = indata["vnfPkgId"]
1000 filter_q = BaseTopic._get_project_filter(session)
1001 filter_q["_id"] = vnfpkg_id
1002 vnfd = self.db.get_one("vnfds", filter_q)
1003 operation = indata["lcmOperationType"]
1004 kdu_name = indata["kdu_name"]
1005 for kdu in vnfd.get("kdu", []):
1006 if kdu["name"] == kdu_name:
1007 helm_chart = kdu.get("helm-chart")
1008 juju_bundle = kdu.get("juju-bundle")
1009 break
1010 else:
1011 raise EngineException("Not found vnfd[id='{}']:kdu[name='{}']".format(vnfpkg_id, kdu_name))
1012 if helm_chart:
1013 indata["helm-chart"] = helm_chart
1014 match = fullmatch(r"([^/]*)/([^/]*)", helm_chart)
1015 repo_name = match.group(1) if match else None
1016 elif juju_bundle:
1017 indata["juju-bundle"] = juju_bundle
1018 match = fullmatch(r"([^/]*)/([^/]*)", juju_bundle)
1019 repo_name = match.group(1) if match else None
1020 else:
1021 raise EngineException("Found neither 'helm-chart' nor 'juju-bundle' in vnfd[id='{}']:kdu[name='{}']"
1022 .format(vnfpkg_id, kdu_name))
1023 if repo_name:
1024 del filter_q["_id"]
1025 filter_q["name"] = repo_name
1026 repo = self.db.get_one("k8srepos", filter_q)
1027 k8srepo_id = repo.get("_id")
1028 k8srepo_url = repo.get("url")
1029 else:
1030 k8srepo_id = None
1031 k8srepo_url = None
1032 indata["k8srepoId"] = k8srepo_id
1033 indata["k8srepo_url"] = k8srepo_url
1034 vnfpkgop_id = str(uuid4())
1035 vnfpkgop_desc = {
1036 "_id": vnfpkgop_id,
1037 "operationState": "PROCESSING",
1038 "vnfPkgId": vnfpkg_id,
1039 "lcmOperationType": operation,
1040 "isAutomaticInvocation": False,
1041 "isCancelPending": False,
1042 "operationParams": indata,
1043 "links": {
1044 "self": "/osm/vnfpkgm/v1/vnfpkg_op_occs/" + vnfpkgop_id,
1045 "vnfpkg": "/osm/vnfpkgm/v1/vnf_packages/" + vnfpkg_id,
1046 }
1047 }
1048 self.format_on_new(vnfpkgop_desc, session["project_id"], make_public=session["public"])
1049 ctime = vnfpkgop_desc["_admin"]["created"]
1050 vnfpkgop_desc["statusEnteredTime"] = ctime
1051 vnfpkgop_desc["startTime"] = ctime
1052 self.db.create(self.topic, vnfpkgop_desc)
1053 rollback.append({"topic": self.topic, "_id": vnfpkgop_id})
1054 self.msg.write(self.topic_msg, operation, vnfpkgop_desc)
1055 return vnfpkgop_id, None