blob: 8efde1e1cff3bdeb0a909c2c4ffc1cb545f1386e [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
bravofb995ea22021-02-10 10:57:52 -030020import copy
garciadeblas4568a372021-03-24 09:19:48 +010021
tiernob24258a2018-10-04 18:39:49 +020022# import logging
23from hashlib import md5
24from osm_common.dbbase import DbException, deep_update_rfc7396
25from http import HTTPStatus
delacruzramo26301bb2019-11-15 14:45:32 +010026from time import time
delacruzramo271d2002019-12-02 21:00:37 +010027from uuid import uuid4
28from re import fullmatch
garciadeblas4568a372021-03-24 09:19:48 +010029from osm_nbi.validation import (
30 ValidationError,
31 pdu_new_schema,
32 pdu_edit_schema,
33 validate_input,
34 vnfpkgop_new_schema,
35)
tierno23acf402019-08-28 13:36:34 +000036from osm_nbi.base_topic import BaseTopic, EngineException, get_iterable
sousaedu317b9fd2021-07-29 17:40:16 +020037from osm_im import etsi_nfv_vnfd, etsi_nfv_nsd
gcalvino70434c12018-11-27 15:17:04 +010038from osm_im.nst import nst as nst_im
gcalvino46e4cb82018-10-26 13:10:22 +020039from pyangbind.lib.serialise import pybindJSONDecoder
40import pyangbind.lib.pybindJSON as pybindJSON
bravof41a52052021-02-17 18:08:01 -030041from osm_nbi import utils
tiernob24258a2018-10-04 18:39:49 +020042
43__author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
44
45
46class DescriptorTopic(BaseTopic):
delacruzramo32bab472019-09-13 12:24:22 +020047 def __init__(self, db, fs, msg, auth):
48 BaseTopic.__init__(self, db, fs, msg, auth)
tiernob24258a2018-10-04 18:39:49 +020049
tierno65ca36d2019-02-12 19:27:52 +010050 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
garciadeblas4568a372021-03-24 09:19:48 +010051 final_content = super().check_conflict_on_edit(
52 session, final_content, edit_content, _id
53 )
K Sai Kiran45bd94c2019-11-25 17:30:37 +053054
55 def _check_unique_id_name(descriptor, position=""):
56 for desc_key, desc_item in descriptor.items():
57 if isinstance(desc_item, list) and desc_item:
58 used_ids = []
59 desc_item_id = None
60 for index, list_item in enumerate(desc_item):
61 if isinstance(list_item, dict):
garciadeblas4568a372021-03-24 09:19:48 +010062 _check_unique_id_name(
63 list_item, "{}.{}[{}]".format(position, desc_key, index)
64 )
K Sai Kiran45bd94c2019-11-25 17:30:37 +053065 # Base case
garciadeblas4568a372021-03-24 09:19:48 +010066 if index == 0 and (
67 list_item.get("id") or list_item.get("name")
68 ):
K Sai Kiran45bd94c2019-11-25 17:30:37 +053069 desc_item_id = "id" if list_item.get("id") else "name"
70 if desc_item_id and list_item.get(desc_item_id):
71 if list_item[desc_item_id] in used_ids:
garciadeblas4568a372021-03-24 09:19:48 +010072 position = "{}.{}[{}]".format(
73 position, desc_key, index
74 )
75 raise EngineException(
76 "Error: identifier {} '{}' is not unique and repeats at '{}'".format(
77 desc_item_id,
78 list_item[desc_item_id],
79 position,
80 ),
81 HTTPStatus.UNPROCESSABLE_ENTITY,
82 )
K Sai Kiran45bd94c2019-11-25 17:30:37 +053083 used_ids.append(list_item[desc_item_id])
garciaale960531a2020-10-20 18:29:45 -030084
K Sai Kiran45bd94c2019-11-25 17:30:37 +053085 _check_unique_id_name(final_content)
tiernoaa1ca7b2018-11-08 19:00:20 +010086 # 1. validate again with pyangbind
87 # 1.1. remove internal keys
88 internal_keys = {}
89 for k in ("_id", "_admin"):
90 if k in final_content:
91 internal_keys[k] = final_content.pop(k)
gcalvinoa6fe0002019-01-09 13:27:11 +010092 storage_params = internal_keys["_admin"].get("storage")
garciadeblas4568a372021-03-24 09:19:48 +010093 serialized = self._validate_input_new(
94 final_content, storage_params, session["force"]
95 )
bravofb995ea22021-02-10 10:57:52 -030096
tiernoaa1ca7b2018-11-08 19:00:20 +010097 # 1.2. modify final_content with a serialized version
bravofb995ea22021-02-10 10:57:52 -030098 final_content = copy.deepcopy(serialized)
tiernoaa1ca7b2018-11-08 19:00:20 +010099 # 1.3. restore internal keys
100 for k, v in internal_keys.items():
101 final_content[k] = v
tierno65ca36d2019-02-12 19:27:52 +0100102 if session["force"]:
bravofb995ea22021-02-10 10:57:52 -0300103 return final_content
104
tiernoaa1ca7b2018-11-08 19:00:20 +0100105 # 2. check that this id is not present
106 if "id" in edit_content:
tierno65ca36d2019-02-12 19:27:52 +0100107 _filter = self._get_project_filter(session)
bravofb995ea22021-02-10 10:57:52 -0300108
tiernoaa1ca7b2018-11-08 19:00:20 +0100109 _filter["id"] = final_content["id"]
110 _filter["_id.neq"] = _id
bravofb995ea22021-02-10 10:57:52 -0300111
tiernoaa1ca7b2018-11-08 19:00:20 +0100112 if self.db.get_one(self.topic, _filter, fail_on_empty=False):
garciadeblas4568a372021-03-24 09:19:48 +0100113 raise EngineException(
114 "{} with id '{}' already exists for this project".format(
115 self.topic[:-1], final_content["id"]
116 ),
117 HTTPStatus.CONFLICT,
118 )
tiernob24258a2018-10-04 18:39:49 +0200119
bravofb995ea22021-02-10 10:57:52 -0300120 return final_content
121
tiernob24258a2018-10-04 18:39:49 +0200122 @staticmethod
123 def format_on_new(content, project_id=None, make_public=False):
124 BaseTopic.format_on_new(content, project_id=project_id, make_public=make_public)
125 content["_admin"]["onboardingState"] = "CREATED"
126 content["_admin"]["operationalState"] = "DISABLED"
tierno36ec8602018-11-02 17:27:11 +0100127 content["_admin"]["usageState"] = "NOT_IN_USE"
tiernob24258a2018-10-04 18:39:49 +0200128
tiernobee3bad2019-12-05 12:26:01 +0000129 def delete_extra(self, session, _id, db_content, not_send_msg=None):
tiernob4844ab2019-05-23 08:42:12 +0000130 """
131 Deletes file system storage associated with the descriptor
132 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
133 :param _id: server internal id
134 :param db_content: The database content of the descriptor
tiernobee3bad2019-12-05 12:26:01 +0000135 :param not_send_msg: To not send message (False) or store content (list) instead
tiernob4844ab2019-05-23 08:42:12 +0000136 :return: None if ok or raises EngineException with the problem
137 """
tiernob24258a2018-10-04 18:39:49 +0200138 self.fs.file_delete(_id, ignore_non_exist=True)
tiernof717cbe2018-12-03 16:35:42 +0000139 self.fs.file_delete(_id + "_", ignore_non_exist=True) # remove temp folder
tiernob24258a2018-10-04 18:39:49 +0200140
141 @staticmethod
142 def get_one_by_id(db, session, topic, id):
143 # find owned by this project
tierno65ca36d2019-02-12 19:27:52 +0100144 _filter = BaseTopic._get_project_filter(session)
tiernob24258a2018-10-04 18:39:49 +0200145 _filter["id"] = id
146 desc_list = db.get_list(topic, _filter)
147 if len(desc_list) == 1:
148 return desc_list[0]
149 elif len(desc_list) > 1:
garciadeblas4568a372021-03-24 09:19:48 +0100150 raise DbException(
151 "Found more than one {} with id='{}' belonging to this project".format(
152 topic[:-1], id
153 ),
154 HTTPStatus.CONFLICT,
155 )
tiernob24258a2018-10-04 18:39:49 +0200156
157 # not found any: try to find public
tierno65ca36d2019-02-12 19:27:52 +0100158 _filter = BaseTopic._get_project_filter(session)
tiernob24258a2018-10-04 18:39:49 +0200159 _filter["id"] = id
160 desc_list = db.get_list(topic, _filter)
161 if not desc_list:
garciadeblas4568a372021-03-24 09:19:48 +0100162 raise DbException(
163 "Not found any {} with id='{}'".format(topic[:-1], id),
164 HTTPStatus.NOT_FOUND,
165 )
tiernob24258a2018-10-04 18:39:49 +0200166 elif len(desc_list) == 1:
167 return desc_list[0]
168 else:
garciadeblas4568a372021-03-24 09:19:48 +0100169 raise DbException(
170 "Found more than one public {} with id='{}'; and no one belonging to this project".format(
171 topic[:-1], id
172 ),
173 HTTPStatus.CONFLICT,
174 )
tiernob24258a2018-10-04 18:39:49 +0200175
tierno65ca36d2019-02-12 19:27:52 +0100176 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
tiernob24258a2018-10-04 18:39:49 +0200177 """
178 Creates a new almost empty DISABLED entry into database. Due to SOL005, it does not follow normal procedure.
179 Creating a VNFD or NSD is done in two steps: 1. Creates an empty descriptor (this step) and 2) upload content
180 (self.upload_content)
181 :param rollback: list to append created items at database in case a rollback may to be done
tierno65ca36d2019-02-12 19:27:52 +0100182 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200183 :param indata: data to be inserted
184 :param kwargs: used to override the indata descriptor
185 :param headers: http request headers
tiernobdebce92019-07-01 15:36:49 +0000186 :return: _id, None: identity of the inserted data; and None as there is not any operation
tiernob24258a2018-10-04 18:39:49 +0200187 """
188
tiernod7749582020-05-28 10:41:10 +0000189 # No needed to capture exceptions
190 # Check Quota
191 self.check_quota(session)
delacruzramo32bab472019-09-13 12:24:22 +0200192
tiernod7749582020-05-28 10:41:10 +0000193 # _remove_envelop
194 if indata:
195 if "userDefinedData" in indata:
garciadeblas4568a372021-03-24 09:19:48 +0100196 indata = indata["userDefinedData"]
tiernob24258a2018-10-04 18:39:49 +0200197
tiernod7749582020-05-28 10:41:10 +0000198 # Override descriptor with query string kwargs
199 self._update_input_with_kwargs(indata, kwargs)
200 # uncomment when this method is implemented.
201 # Avoid override in this case as the target is userDefinedData, but not vnfd,nsd descriptors
202 # indata = DescriptorTopic._validate_input_new(self, indata, project_id=session["force"])
tiernob24258a2018-10-04 18:39:49 +0200203
tiernod7749582020-05-28 10:41:10 +0000204 content = {"_admin": {"userDefinedData": indata}}
garciadeblas4568a372021-03-24 09:19:48 +0100205 self.format_on_new(
206 content, session["project_id"], make_public=session["public"]
207 )
tiernod7749582020-05-28 10:41:10 +0000208 _id = self.db.create(self.topic, content)
209 rollback.append({"topic": self.topic, "_id": _id})
210 self._send_msg("created", {"_id": _id})
211 return _id, None
tiernob24258a2018-10-04 18:39:49 +0200212
tierno65ca36d2019-02-12 19:27:52 +0100213 def upload_content(self, session, _id, indata, kwargs, headers):
tiernob24258a2018-10-04 18:39:49 +0200214 """
215 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 +0100216 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200217 :param _id : the nsd,vnfd is already created, this is the id
218 :param indata: http body request
219 :param kwargs: user query string to override parameters. NOT USED
220 :param headers: http request headers
tierno5a5c2182018-11-20 12:27:42 +0000221 :return: True if package is completely uploaded or False if partial content has been uploded
tiernob24258a2018-10-04 18:39:49 +0200222 Raise exception on error
223 """
224 # Check that _id exists and it is valid
225 current_desc = self.show(session, _id)
226
227 content_range_text = headers.get("Content-Range")
228 expected_md5 = headers.get("Content-File-MD5")
229 compressed = None
230 content_type = headers.get("Content-Type")
garciadeblas4568a372021-03-24 09:19:48 +0100231 if (
232 content_type
233 and "application/gzip" in content_type
234 or "application/x-gzip" in content_type
235 or "application/zip" in content_type
236 ):
tiernob24258a2018-10-04 18:39:49 +0200237 compressed = "gzip"
238 filename = headers.get("Content-Filename")
239 if not filename:
240 filename = "package.tar.gz" if compressed else "package"
241 # TODO change to Content-Disposition filename https://tools.ietf.org/html/rfc6266
242 file_pkg = None
243 error_text = ""
244 try:
245 if content_range_text:
garciadeblas4568a372021-03-24 09:19:48 +0100246 content_range = (
247 content_range_text.replace("-", " ").replace("/", " ").split()
248 )
249 if (
250 content_range[0] != "bytes"
251 ): # TODO check x<y not negative < total....
tiernob24258a2018-10-04 18:39:49 +0200252 raise IndexError()
253 start = int(content_range[1])
254 end = int(content_range[2]) + 1
255 total = int(content_range[3])
256 else:
257 start = 0
garciadeblas4568a372021-03-24 09:19:48 +0100258 temp_folder = (
259 _id + "_"
260 ) # all the content is upload here and if ok, it is rename from id_ to is folder
tiernob24258a2018-10-04 18:39:49 +0200261
262 if start:
garciadeblas4568a372021-03-24 09:19:48 +0100263 if not self.fs.file_exists(temp_folder, "dir"):
264 raise EngineException(
265 "invalid Transaction-Id header", HTTPStatus.NOT_FOUND
266 )
tiernob24258a2018-10-04 18:39:49 +0200267 else:
tiernof717cbe2018-12-03 16:35:42 +0000268 self.fs.file_delete(temp_folder, ignore_non_exist=True)
269 self.fs.mkdir(temp_folder)
tiernob24258a2018-10-04 18:39:49 +0200270
271 storage = self.fs.get_params()
272 storage["folder"] = _id
273
tiernof717cbe2018-12-03 16:35:42 +0000274 file_path = (temp_folder, filename)
garciadeblas4568a372021-03-24 09:19:48 +0100275 if self.fs.file_exists(file_path, "file"):
tiernob24258a2018-10-04 18:39:49 +0200276 file_size = self.fs.file_size(file_path)
277 else:
278 file_size = 0
279 if file_size != start:
garciadeblas4568a372021-03-24 09:19:48 +0100280 raise EngineException(
281 "invalid Content-Range start sequence, expected '{}' but received '{}'".format(
282 file_size, start
283 ),
284 HTTPStatus.REQUESTED_RANGE_NOT_SATISFIABLE,
285 )
286 file_pkg = self.fs.file_open(file_path, "a+b")
tiernob24258a2018-10-04 18:39:49 +0200287 if isinstance(indata, dict):
288 indata_text = yaml.safe_dump(indata, indent=4, default_flow_style=False)
289 file_pkg.write(indata_text.encode(encoding="utf-8"))
290 else:
291 indata_len = 0
292 while True:
293 indata_text = indata.read(4096)
294 indata_len += len(indata_text)
295 if not indata_text:
296 break
297 file_pkg.write(indata_text)
298 if content_range_text:
garciaale960531a2020-10-20 18:29:45 -0300299 if indata_len != end - start:
garciadeblas4568a372021-03-24 09:19:48 +0100300 raise EngineException(
301 "Mismatch between Content-Range header {}-{} and body length of {}".format(
302 start, end - 1, indata_len
303 ),
304 HTTPStatus.REQUESTED_RANGE_NOT_SATISFIABLE,
305 )
tiernob24258a2018-10-04 18:39:49 +0200306 if end != total:
307 # TODO update to UPLOADING
308 return False
309
310 # PACKAGE UPLOADED
311 if expected_md5:
312 file_pkg.seek(0, 0)
313 file_md5 = md5()
314 chunk_data = file_pkg.read(1024)
315 while chunk_data:
316 file_md5.update(chunk_data)
317 chunk_data = file_pkg.read(1024)
318 if expected_md5 != file_md5.hexdigest():
319 raise EngineException("Error, MD5 mismatch", HTTPStatus.CONFLICT)
320 file_pkg.seek(0, 0)
321 if compressed == "gzip":
garciadeblas4568a372021-03-24 09:19:48 +0100322 tar = tarfile.open(mode="r", fileobj=file_pkg)
tiernob24258a2018-10-04 18:39:49 +0200323 descriptor_file_name = None
324 for tarinfo in tar:
325 tarname = tarinfo.name
326 tarname_path = tarname.split("/")
garciadeblas4568a372021-03-24 09:19:48 +0100327 if (
328 not tarname_path[0] or ".." in tarname_path
329 ): # if start with "/" means absolute path
330 raise EngineException(
331 "Absolute path or '..' are not allowed for package descriptor tar.gz"
332 )
tiernob24258a2018-10-04 18:39:49 +0200333 if len(tarname_path) == 1 and not tarinfo.isdir():
garciadeblas4568a372021-03-24 09:19:48 +0100334 raise EngineException(
335 "All files must be inside a dir for package descriptor tar.gz"
336 )
337 if (
338 tarname.endswith(".yaml")
339 or tarname.endswith(".json")
340 or tarname.endswith(".yml")
341 ):
tiernob24258a2018-10-04 18:39:49 +0200342 storage["pkg-dir"] = tarname_path[0]
343 if len(tarname_path) == 2:
344 if descriptor_file_name:
345 raise EngineException(
garciadeblas4568a372021-03-24 09:19:48 +0100346 "Found more than one descriptor file at package descriptor tar.gz"
347 )
tiernob24258a2018-10-04 18:39:49 +0200348 descriptor_file_name = tarname
349 if not descriptor_file_name:
garciadeblas4568a372021-03-24 09:19:48 +0100350 raise EngineException(
351 "Not found any descriptor file at package descriptor tar.gz"
352 )
tiernob24258a2018-10-04 18:39:49 +0200353 storage["descriptor"] = descriptor_file_name
354 storage["zipfile"] = filename
tiernof717cbe2018-12-03 16:35:42 +0000355 self.fs.file_extract(tar, temp_folder)
garciadeblas4568a372021-03-24 09:19:48 +0100356 with self.fs.file_open(
357 (temp_folder, descriptor_file_name), "r"
358 ) as descriptor_file:
tiernob24258a2018-10-04 18:39:49 +0200359 content = descriptor_file.read()
360 else:
361 content = file_pkg.read()
362 storage["descriptor"] = descriptor_file_name = filename
363
364 if descriptor_file_name.endswith(".json"):
365 error_text = "Invalid json format "
366 indata = json.load(content)
367 else:
368 error_text = "Invalid yaml format "
delacruzramob19cadc2019-10-08 10:18:02 +0200369 indata = yaml.load(content, Loader=yaml.SafeLoader)
tiernob24258a2018-10-04 18:39:49 +0200370
371 current_desc["_admin"]["storage"] = storage
372 current_desc["_admin"]["onboardingState"] = "ONBOARDED"
373 current_desc["_admin"]["operationalState"] = "ENABLED"
374
375 indata = self._remove_envelop(indata)
376
377 # Override descriptor with query string kwargs
378 if kwargs:
379 self._update_input_with_kwargs(indata, kwargs)
tiernob24258a2018-10-04 18:39:49 +0200380
381 deep_update_rfc7396(current_desc, indata)
garciadeblas4568a372021-03-24 09:19:48 +0100382 current_desc = self.check_conflict_on_edit(
383 session, current_desc, indata, _id=_id
384 )
delacruzramo26301bb2019-11-15 14:45:32 +0100385 current_desc["_admin"]["modified"] = time()
tiernob24258a2018-10-04 18:39:49 +0200386 self.db.replace(self.topic, _id, current_desc)
tiernof717cbe2018-12-03 16:35:42 +0000387 self.fs.dir_rename(temp_folder, _id)
tiernob24258a2018-10-04 18:39:49 +0200388
389 indata["_id"] = _id
K Sai Kiranc96fd692019-10-16 17:50:53 +0530390 self._send_msg("edited", indata)
tiernob24258a2018-10-04 18:39:49 +0200391
392 # TODO if descriptor has changed because kwargs update content and remove cached zip
393 # TODO if zip is not present creates one
394 return True
395
396 except EngineException:
397 raise
398 except IndexError:
garciadeblas4568a372021-03-24 09:19:48 +0100399 raise EngineException(
400 "invalid Content-Range header format. Expected 'bytes start-end/total'",
401 HTTPStatus.REQUESTED_RANGE_NOT_SATISFIABLE,
402 )
tiernob24258a2018-10-04 18:39:49 +0200403 except IOError as e:
garciadeblas4568a372021-03-24 09:19:48 +0100404 raise EngineException(
405 "invalid upload transaction sequence: '{}'".format(e),
406 HTTPStatus.BAD_REQUEST,
407 )
tiernob24258a2018-10-04 18:39:49 +0200408 except tarfile.ReadError as e:
garciadeblas4568a372021-03-24 09:19:48 +0100409 raise EngineException(
410 "invalid file content {}".format(e), HTTPStatus.BAD_REQUEST
411 )
tiernob24258a2018-10-04 18:39:49 +0200412 except (ValueError, yaml.YAMLError) as e:
413 raise EngineException(error_text + str(e))
414 except ValidationError as e:
415 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
416 finally:
417 if file_pkg:
418 file_pkg.close()
419
420 def get_file(self, session, _id, path=None, accept_header=None):
421 """
422 Return the file content of a vnfd or nsd
tierno65ca36d2019-02-12 19:27:52 +0100423 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tierno87006042018-10-24 12:50:20 +0200424 :param _id: Identity of the vnfd, nsd
tiernob24258a2018-10-04 18:39:49 +0200425 :param path: artifact path or "$DESCRIPTOR" or None
426 :param accept_header: Content of Accept header. Must contain applition/zip or/and text/plain
tierno87006042018-10-24 12:50:20 +0200427 :return: opened file plus Accept format or raises an exception
tiernob24258a2018-10-04 18:39:49 +0200428 """
429 accept_text = accept_zip = False
430 if accept_header:
garciadeblas4568a372021-03-24 09:19:48 +0100431 if "text/plain" in accept_header or "*/*" in accept_header:
tiernob24258a2018-10-04 18:39:49 +0200432 accept_text = True
garciadeblas4568a372021-03-24 09:19:48 +0100433 if "application/zip" in accept_header or "*/*" in accept_header:
434 accept_zip = "application/zip"
435 elif "application/gzip" in accept_header:
436 accept_zip = "application/gzip"
tierno87006042018-10-24 12:50:20 +0200437
tiernob24258a2018-10-04 18:39:49 +0200438 if not accept_text and not accept_zip:
garciadeblas4568a372021-03-24 09:19:48 +0100439 raise EngineException(
440 "provide request header 'Accept' with 'application/zip' or 'text/plain'",
441 http_code=HTTPStatus.NOT_ACCEPTABLE,
442 )
tiernob24258a2018-10-04 18:39:49 +0200443
444 content = self.show(session, _id)
445 if content["_admin"]["onboardingState"] != "ONBOARDED":
garciadeblas4568a372021-03-24 09:19:48 +0100446 raise EngineException(
447 "Cannot get content because this resource is not at 'ONBOARDED' state. "
448 "onboardingState is {}".format(content["_admin"]["onboardingState"]),
449 http_code=HTTPStatus.CONFLICT,
450 )
tiernob24258a2018-10-04 18:39:49 +0200451 storage = content["_admin"]["storage"]
garciaale960531a2020-10-20 18:29:45 -0300452 if path is not None and path != "$DESCRIPTOR": # artifacts
garciadeblas4568a372021-03-24 09:19:48 +0100453 if not storage.get("pkg-dir"):
454 raise EngineException(
455 "Packages does not contains artifacts",
456 http_code=HTTPStatus.BAD_REQUEST,
457 )
458 if self.fs.file_exists(
459 (storage["folder"], storage["pkg-dir"], *path), "dir"
460 ):
461 folder_content = self.fs.dir_ls(
462 (storage["folder"], storage["pkg-dir"], *path)
463 )
tiernob24258a2018-10-04 18:39:49 +0200464 return folder_content, "text/plain"
465 # TODO manage folders in http
466 else:
garciadeblas4568a372021-03-24 09:19:48 +0100467 return (
468 self.fs.file_open(
469 (storage["folder"], storage["pkg-dir"], *path), "rb"
470 ),
471 "application/octet-stream",
472 )
tiernob24258a2018-10-04 18:39:49 +0200473
474 # pkgtype accept ZIP TEXT -> result
475 # manyfiles yes X -> zip
476 # no yes -> error
477 # onefile yes no -> zip
478 # X yes -> text
tiernoee002752020-08-04 14:14:16 +0000479 contain_many_files = False
garciadeblas4568a372021-03-24 09:19:48 +0100480 if storage.get("pkg-dir"):
tiernoee002752020-08-04 14:14:16 +0000481 # check if there are more than one file in the package, ignoring checksums.txt.
garciadeblas4568a372021-03-24 09:19:48 +0100482 pkg_files = self.fs.dir_ls((storage["folder"], storage["pkg-dir"]))
483 if len(pkg_files) >= 3 or (
484 len(pkg_files) == 2 and "checksums.txt" not in pkg_files
485 ):
tiernoee002752020-08-04 14:14:16 +0000486 contain_many_files = True
487 if accept_text and (not contain_many_files or path == "$DESCRIPTOR"):
garciadeblas4568a372021-03-24 09:19:48 +0100488 return (
489 self.fs.file_open((storage["folder"], storage["descriptor"]), "r"),
490 "text/plain",
491 )
tiernoee002752020-08-04 14:14:16 +0000492 elif contain_many_files and not accept_zip:
garciadeblas4568a372021-03-24 09:19:48 +0100493 raise EngineException(
494 "Packages that contains several files need to be retrieved with 'application/zip'"
495 "Accept header",
496 http_code=HTTPStatus.NOT_ACCEPTABLE,
497 )
tiernob24258a2018-10-04 18:39:49 +0200498 else:
garciadeblas4568a372021-03-24 09:19:48 +0100499 if not storage.get("zipfile"):
tiernob24258a2018-10-04 18:39:49 +0200500 # TODO generate zipfile if not present
garciadeblas4568a372021-03-24 09:19:48 +0100501 raise EngineException(
502 "Only allowed 'text/plain' Accept header for this descriptor. To be solved in "
503 "future versions",
504 http_code=HTTPStatus.NOT_ACCEPTABLE,
505 )
506 return (
507 self.fs.file_open((storage["folder"], storage["zipfile"]), "rb"),
508 accept_zip,
509 )
tiernob24258a2018-10-04 18:39:49 +0200510
garciaale7cbd03c2020-11-27 10:38:35 -0300511 def _remove_yang_prefixes_from_descriptor(self, descriptor):
512 new_descriptor = {}
513 for k, v in descriptor.items():
514 new_v = v
515 if isinstance(v, dict):
516 new_v = self._remove_yang_prefixes_from_descriptor(v)
517 elif isinstance(v, list):
518 new_v = list()
519 for x in v:
520 if isinstance(x, dict):
521 new_v.append(self._remove_yang_prefixes_from_descriptor(x))
522 else:
523 new_v.append(x)
garciadeblas4568a372021-03-24 09:19:48 +0100524 new_descriptor[k.split(":")[-1]] = new_v
garciaale7cbd03c2020-11-27 10:38:35 -0300525 return new_descriptor
526
gcalvino46e4cb82018-10-26 13:10:22 +0200527 def pyangbind_validation(self, item, data, force=False):
garciadeblas4568a372021-03-24 09:19:48 +0100528 raise EngineException(
529 "Not possible to validate '{}' item".format(item),
530 http_code=HTTPStatus.INTERNAL_SERVER_ERROR,
531 )
gcalvino46e4cb82018-10-26 13:10:22 +0200532
Frank Brydendeba68e2020-07-27 13:55:11 +0000533 def _validate_input_edit(self, indata, content, force=False):
534 # not needed to validate with pyangbind becuase it will be validated at check_conflict_on_edit
535 if "_id" in indata:
536 indata.pop("_id")
537 if "_admin" not in indata:
538 indata["_admin"] = {}
539
540 if "operationalState" in indata:
541 if indata["operationalState"] in ("ENABLED", "DISABLED"):
542 indata["_admin"]["operationalState"] = indata.pop("operationalState")
543 else:
garciadeblas4568a372021-03-24 09:19:48 +0100544 raise EngineException(
545 "State '{}' is not a valid operational state".format(
546 indata["operationalState"]
547 ),
548 http_code=HTTPStatus.BAD_REQUEST,
549 )
Frank Brydendeba68e2020-07-27 13:55:11 +0000550
garciadeblas4568a372021-03-24 09:19:48 +0100551 # In the case of user defined data, we need to put the data in the root of the object
Frank Brydendeba68e2020-07-27 13:55:11 +0000552 # to preserve current expected behaviour
553 if "userDefinedData" in indata:
554 data = indata.pop("userDefinedData")
555 if type(data) == dict:
556 indata["_admin"]["userDefinedData"] = data
557 else:
garciadeblas4568a372021-03-24 09:19:48 +0100558 raise EngineException(
559 "userDefinedData should be an object, but is '{}' instead".format(
560 type(data)
561 ),
562 http_code=HTTPStatus.BAD_REQUEST,
563 )
garciaale960531a2020-10-20 18:29:45 -0300564
garciadeblas4568a372021-03-24 09:19:48 +0100565 if (
566 "operationalState" in indata["_admin"]
567 and content["_admin"]["operationalState"]
568 == indata["_admin"]["operationalState"]
569 ):
570 raise EngineException(
571 "operationalState already {}".format(
572 content["_admin"]["operationalState"]
573 ),
574 http_code=HTTPStatus.CONFLICT,
575 )
Frank Brydendeba68e2020-07-27 13:55:11 +0000576
577 return indata
578
tiernob24258a2018-10-04 18:39:49 +0200579
580class VnfdTopic(DescriptorTopic):
581 topic = "vnfds"
582 topic_msg = "vnfd"
583
delacruzramo32bab472019-09-13 12:24:22 +0200584 def __init__(self, db, fs, msg, auth):
585 DescriptorTopic.__init__(self, db, fs, msg, auth)
tiernob24258a2018-10-04 18:39:49 +0200586
garciaale7cbd03c2020-11-27 10:38:35 -0300587 def pyangbind_validation(self, item, data, force=False):
garciaaledf718ae2020-12-03 19:17:28 -0300588 if self._descriptor_data_is_in_old_format(data):
garciadeblas4568a372021-03-24 09:19:48 +0100589 raise EngineException(
590 "ERROR: Unsupported descriptor format. Please, use an ETSI SOL006 descriptor.",
591 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
592 )
garciaale7cbd03c2020-11-27 10:38:35 -0300593 try:
garciaale7cbd03c2020-11-27 10:38:35 -0300594 myvnfd = etsi_nfv_vnfd.etsi_nfv_vnfd()
garciadeblas4568a372021-03-24 09:19:48 +0100595 pybindJSONDecoder.load_ietf_json(
596 {"etsi-nfv-vnfd:vnfd": data},
597 None,
598 None,
599 obj=myvnfd,
600 path_helper=True,
601 skip_unknown=force,
602 )
garciaale7cbd03c2020-11-27 10:38:35 -0300603 out = pybindJSON.dumps(myvnfd, mode="ietf")
604 desc_out = self._remove_envelop(yaml.safe_load(out))
605 desc_out = self._remove_yang_prefixes_from_descriptor(desc_out)
bravof41a52052021-02-17 18:08:01 -0300606 return utils.deep_update_dict(data, desc_out)
garciaale7cbd03c2020-11-27 10:38:35 -0300607 except Exception as e:
garciadeblas4568a372021-03-24 09:19:48 +0100608 raise EngineException(
609 "Error in pyangbind validation: {}".format(str(e)),
610 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
611 )
garciaale7cbd03c2020-11-27 10:38:35 -0300612
tiernob24258a2018-10-04 18:39:49 +0200613 @staticmethod
garciaaledf718ae2020-12-03 19:17:28 -0300614 def _descriptor_data_is_in_old_format(data):
garciadeblas4568a372021-03-24 09:19:48 +0100615 return ("vnfd-catalog" in data) or ("vnfd:vnfd-catalog" in data)
garciaaledf718ae2020-12-03 19:17:28 -0300616
617 @staticmethod
tiernob24258a2018-10-04 18:39:49 +0200618 def _remove_envelop(indata=None):
619 if not indata:
620 return {}
621 clean_indata = indata
garciaale7cbd03c2020-11-27 10:38:35 -0300622
garciadeblas4568a372021-03-24 09:19:48 +0100623 if clean_indata.get("etsi-nfv-vnfd:vnfd"):
624 if not isinstance(clean_indata["etsi-nfv-vnfd:vnfd"], dict):
garciaale7cbd03c2020-11-27 10:38:35 -0300625 raise EngineException("'etsi-nfv-vnfd:vnfd' must be a dict")
garciadeblas4568a372021-03-24 09:19:48 +0100626 clean_indata = clean_indata["etsi-nfv-vnfd:vnfd"]
627 elif clean_indata.get("vnfd"):
628 if not isinstance(clean_indata["vnfd"], dict):
garciaale7cbd03c2020-11-27 10:38:35 -0300629 raise EngineException("'vnfd' must be dict")
garciadeblas4568a372021-03-24 09:19:48 +0100630 clean_indata = clean_indata["vnfd"]
garciaale7cbd03c2020-11-27 10:38:35 -0300631
tiernob24258a2018-10-04 18:39:49 +0200632 return clean_indata
633
tierno65ca36d2019-02-12 19:27:52 +0100634 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
garciadeblas4568a372021-03-24 09:19:48 +0100635 final_content = super().check_conflict_on_edit(
636 session, final_content, edit_content, _id
637 )
tierno36ec8602018-11-02 17:27:11 +0100638
639 # set type of vnfd
640 contains_pdu = False
641 contains_vdu = False
642 for vdu in get_iterable(final_content.get("vdu")):
643 if vdu.get("pdu-type"):
644 contains_pdu = True
645 else:
646 contains_vdu = True
647 if contains_pdu:
648 final_content["_admin"]["type"] = "hnfd" if contains_vdu else "pnfd"
649 elif contains_vdu:
650 final_content["_admin"]["type"] = "vnfd"
651 # if neither vud nor pdu do not fill type
bravofb995ea22021-02-10 10:57:52 -0300652 return final_content
tierno36ec8602018-11-02 17:27:11 +0100653
tiernob4844ab2019-05-23 08:42:12 +0000654 def check_conflict_on_del(self, session, _id, db_content):
tiernob24258a2018-10-04 18:39:49 +0200655 """
656 Check that there is not any NSD that uses this VNFD. Only NSDs belonging to this project are considered. Note
657 that VNFD can be public and be used by NSD of other projects. Also check there are not deployments, or vnfr
658 that uses this vnfd
tierno65ca36d2019-02-12 19:27:52 +0100659 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob4844ab2019-05-23 08:42:12 +0000660 :param _id: vnfd internal id
661 :param db_content: The database content of the _id.
tiernob24258a2018-10-04 18:39:49 +0200662 :return: None or raises EngineException with the conflict
663 """
tierno65ca36d2019-02-12 19:27:52 +0100664 if session["force"]:
tiernob24258a2018-10-04 18:39:49 +0200665 return
tiernob4844ab2019-05-23 08:42:12 +0000666 descriptor = db_content
tiernob24258a2018-10-04 18:39:49 +0200667 descriptor_id = descriptor.get("id")
668 if not descriptor_id: # empty vnfd not uploaded
669 return
670
tierno65ca36d2019-02-12 19:27:52 +0100671 _filter = self._get_project_filter(session)
tiernob4844ab2019-05-23 08:42:12 +0000672
tiernob24258a2018-10-04 18:39:49 +0200673 # check vnfrs using this vnfd
674 _filter["vnfd-id"] = _id
675 if self.db.get_list("vnfrs", _filter):
garciadeblas4568a372021-03-24 09:19:48 +0100676 raise EngineException(
677 "There is at least one VNF instance using this descriptor",
678 http_code=HTTPStatus.CONFLICT,
679 )
tiernob4844ab2019-05-23 08:42:12 +0000680
681 # check NSD referencing this VNFD
tiernob24258a2018-10-04 18:39:49 +0200682 del _filter["vnfd-id"]
garciadeblasf576eb92021-04-18 20:54:13 +0000683 _filter["vnfd-id"] = descriptor_id
tiernob24258a2018-10-04 18:39:49 +0200684 if self.db.get_list("nsds", _filter):
garciadeblas4568a372021-03-24 09:19:48 +0100685 raise EngineException(
686 "There is at least one NS package referencing this descriptor",
687 http_code=HTTPStatus.CONFLICT,
688 )
tiernob24258a2018-10-04 18:39:49 +0200689
gcalvinoa6fe0002019-01-09 13:27:11 +0100690 def _validate_input_new(self, indata, storage_params, force=False):
Frank Bryden19b97522020-07-10 12:32:02 +0000691 indata.pop("onboardingState", None)
692 indata.pop("operationalState", None)
693 indata.pop("usageState", None)
Frank Bryden19b97522020-07-10 12:32:02 +0000694 indata.pop("links", None)
695
gcalvino46e4cb82018-10-26 13:10:22 +0200696 indata = self.pyangbind_validation("vnfds", indata, force)
gcalvino5e72d152018-10-23 11:46:57 +0200697 # Cross references validation in the descriptor
garciaale7cbd03c2020-11-27 10:38:35 -0300698
699 self.validate_mgmt_interface_connection_point(indata)
gcalvino5e72d152018-10-23 11:46:57 +0200700
701 for vdu in get_iterable(indata.get("vdu")):
garciaale7cbd03c2020-11-27 10:38:35 -0300702 self.validate_vdu_internal_connection_points(vdu)
garciaale960531a2020-10-20 18:29:45 -0300703 self._validate_vdu_cloud_init_in_package(storage_params, vdu, indata)
bravof41a52052021-02-17 18:08:01 -0300704 self._validate_vdu_charms_in_package(storage_params, indata)
garciaale960531a2020-10-20 18:29:45 -0300705
706 self._validate_vnf_charms_in_package(storage_params, indata)
707
garciaale7cbd03c2020-11-27 10:38:35 -0300708 self.validate_external_connection_points(indata)
709 self.validate_internal_virtual_links(indata)
garciaale960531a2020-10-20 18:29:45 -0300710 self.validate_monitoring_params(indata)
711 self.validate_scaling_group_descriptor(indata)
712
713 return indata
714
715 @staticmethod
garciaale7cbd03c2020-11-27 10:38:35 -0300716 def validate_mgmt_interface_connection_point(indata):
garciaale960531a2020-10-20 18:29:45 -0300717 if not indata.get("vdu"):
718 return
garciaale7cbd03c2020-11-27 10:38:35 -0300719 if not indata.get("mgmt-cp"):
garciadeblas4568a372021-03-24 09:19:48 +0100720 raise EngineException(
721 "'mgmt-cp' is a mandatory field and it is not defined",
722 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
723 )
garciaale7cbd03c2020-11-27 10:38:35 -0300724
725 for cp in get_iterable(indata.get("ext-cpd")):
726 if cp["id"] == indata["mgmt-cp"]:
727 break
728 else:
garciadeblas4568a372021-03-24 09:19:48 +0100729 raise EngineException(
730 "mgmt-cp='{}' must match an existing ext-cpd".format(indata["mgmt-cp"]),
731 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
732 )
garciaale960531a2020-10-20 18:29:45 -0300733
734 @staticmethod
garciaale7cbd03c2020-11-27 10:38:35 -0300735 def validate_vdu_internal_connection_points(vdu):
736 int_cpds = set()
737 for cpd in get_iterable(vdu.get("int-cpd")):
738 cpd_id = cpd.get("id")
739 if cpd_id and cpd_id in int_cpds:
garciadeblas4568a372021-03-24 09:19:48 +0100740 raise EngineException(
741 "vdu[id='{}']:int-cpd[id='{}'] is already used by other int-cpd".format(
742 vdu["id"], cpd_id
743 ),
744 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
745 )
garciaale7cbd03c2020-11-27 10:38:35 -0300746 int_cpds.add(cpd_id)
747
748 @staticmethod
749 def validate_external_connection_points(indata):
750 all_vdus_int_cpds = set()
751 for vdu in get_iterable(indata.get("vdu")):
752 for int_cpd in get_iterable(vdu.get("int-cpd")):
753 all_vdus_int_cpds.add((vdu.get("id"), int_cpd.get("id")))
754
755 ext_cpds = set()
756 for cpd in get_iterable(indata.get("ext-cpd")):
757 cpd_id = cpd.get("id")
758 if cpd_id and cpd_id in ext_cpds:
garciadeblas4568a372021-03-24 09:19:48 +0100759 raise EngineException(
760 "ext-cpd[id='{}'] is already used by other ext-cpd".format(cpd_id),
761 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
762 )
garciaale7cbd03c2020-11-27 10:38:35 -0300763 ext_cpds.add(cpd_id)
764
765 int_cpd = cpd.get("int-cpd")
766 if int_cpd:
767 if (int_cpd.get("vdu-id"), int_cpd.get("cpd")) not in all_vdus_int_cpds:
garciadeblas4568a372021-03-24 09:19:48 +0100768 raise EngineException(
769 "ext-cpd[id='{}']:int-cpd must match an existing vdu int-cpd".format(
770 cpd_id
771 ),
772 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
773 )
garciaale7cbd03c2020-11-27 10:38:35 -0300774 # TODO: Validate k8s-cluster-net points to a valid k8s-cluster:nets ?
garciaale960531a2020-10-20 18:29:45 -0300775
bravof41a52052021-02-17 18:08:01 -0300776 def _validate_vdu_charms_in_package(self, storage_params, indata):
777 for df in indata["df"]:
garciadeblas4568a372021-03-24 09:19:48 +0100778 if (
779 "lcm-operations-configuration" in df
780 and "operate-vnf-op-config" in df["lcm-operations-configuration"]
781 ):
782 configs = df["lcm-operations-configuration"][
783 "operate-vnf-op-config"
784 ].get("day1-2", [])
garciaale2c4f9ec2021-03-01 11:04:50 -0300785 vdus = df.get("vdu-profile", [])
bravof23258282021-02-22 18:04:40 -0300786 for vdu in vdus:
787 for config in configs:
788 if config["id"] == vdu["id"] and utils.find_in_list(
789 config.get("execution-environment-list", []),
garciadeblas4568a372021-03-24 09:19:48 +0100790 lambda ee: "juju" in ee,
bravof23258282021-02-22 18:04:40 -0300791 ):
garciadeblas4568a372021-03-24 09:19:48 +0100792 if not self._validate_package_folders(
793 storage_params, "charms"
794 ):
795 raise EngineException(
796 "Charm defined in vnf[id={}] but not present in "
797 "package".format(indata["id"])
798 )
garciaale960531a2020-10-20 18:29:45 -0300799
800 def _validate_vdu_cloud_init_in_package(self, storage_params, vdu, indata):
801 if not vdu.get("cloud-init-file"):
802 return
garciadeblas4568a372021-03-24 09:19:48 +0100803 if not self._validate_package_folders(
804 storage_params, "cloud_init", vdu["cloud-init-file"]
805 ):
806 raise EngineException(
807 "Cloud-init defined in vnf[id={}]:vdu[id={}] but not present in "
808 "package".format(indata["id"], vdu["id"])
809 )
garciaale960531a2020-10-20 18:29:45 -0300810
811 def _validate_vnf_charms_in_package(self, storage_params, indata):
bravof41a52052021-02-17 18:08:01 -0300812 # Get VNF configuration through new container
garciadeblas4568a372021-03-24 09:19:48 +0100813 for deployment_flavor in indata.get("df", []):
bravof41a52052021-02-17 18:08:01 -0300814 if "lcm-operations-configuration" not in deployment_flavor:
815 return
garciadeblas4568a372021-03-24 09:19:48 +0100816 if (
817 "operate-vnf-op-config"
818 not in deployment_flavor["lcm-operations-configuration"]
819 ):
bravof41a52052021-02-17 18:08:01 -0300820 return
garciadeblas4568a372021-03-24 09:19:48 +0100821 for day_1_2_config in deployment_flavor["lcm-operations-configuration"][
822 "operate-vnf-op-config"
823 ]["day1-2"]:
bravof41a52052021-02-17 18:08:01 -0300824 if day_1_2_config["id"] == indata["id"]:
bravof23258282021-02-22 18:04:40 -0300825 if utils.find_in_list(
826 day_1_2_config.get("execution-environment-list", []),
garciadeblas4568a372021-03-24 09:19:48 +0100827 lambda ee: "juju" in ee,
bravof23258282021-02-22 18:04:40 -0300828 ):
garciadeblas4568a372021-03-24 09:19:48 +0100829 if not self._validate_package_folders(storage_params, "charms"):
830 raise EngineException(
831 "Charm defined in vnf[id={}] but not present in "
832 "package".format(indata["id"])
833 )
garciaale960531a2020-10-20 18:29:45 -0300834
835 def _validate_package_folders(self, storage_params, folder, file=None):
836 if not storage_params or not storage_params.get("pkg-dir"):
837 return False
838 else:
garciadeblas4568a372021-03-24 09:19:48 +0100839 if self.fs.file_exists("{}_".format(storage_params["folder"]), "dir"):
840 f = "{}_/{}/{}".format(
841 storage_params["folder"], storage_params["pkg-dir"], folder
842 )
garciaale960531a2020-10-20 18:29:45 -0300843 else:
garciadeblas4568a372021-03-24 09:19:48 +0100844 f = "{}/{}/{}".format(
845 storage_params["folder"], storage_params["pkg-dir"], folder
846 )
garciaale960531a2020-10-20 18:29:45 -0300847 if file:
garciadeblas4568a372021-03-24 09:19:48 +0100848 return self.fs.file_exists("{}/{}".format(f, file), "file")
garciaale960531a2020-10-20 18:29:45 -0300849 else:
garciadeblas4568a372021-03-24 09:19:48 +0100850 if self.fs.file_exists(f, "dir"):
garciaale960531a2020-10-20 18:29:45 -0300851 if self.fs.dir_ls(f):
852 return True
853 return False
854
855 @staticmethod
garciaale7cbd03c2020-11-27 10:38:35 -0300856 def validate_internal_virtual_links(indata):
857 all_ivld_ids = set()
858 for ivld in get_iterable(indata.get("int-virtual-link-desc")):
859 ivld_id = ivld.get("id")
860 if ivld_id and ivld_id in all_ivld_ids:
garciadeblas4568a372021-03-24 09:19:48 +0100861 raise EngineException(
862 "Duplicated VLD id in int-virtual-link-desc[id={}]".format(ivld_id),
863 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
864 )
garciaale960531a2020-10-20 18:29:45 -0300865 else:
garciaale7cbd03c2020-11-27 10:38:35 -0300866 all_ivld_ids.add(ivld_id)
garciaale960531a2020-10-20 18:29:45 -0300867
garciaale7cbd03c2020-11-27 10:38:35 -0300868 for vdu in get_iterable(indata.get("vdu")):
869 for int_cpd in get_iterable(vdu.get("int-cpd")):
870 int_cpd_ivld_id = int_cpd.get("int-virtual-link-desc")
871 if int_cpd_ivld_id and int_cpd_ivld_id not in all_ivld_ids:
872 raise EngineException(
873 "vdu[id='{}']:int-cpd[id='{}']:int-virtual-link-desc='{}' must match an existing "
garciadeblas4568a372021-03-24 09:19:48 +0100874 "int-virtual-link-desc".format(
875 vdu["id"], int_cpd["id"], int_cpd_ivld_id
876 ),
877 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
878 )
garciaale960531a2020-10-20 18:29:45 -0300879
garciaale7cbd03c2020-11-27 10:38:35 -0300880 for df in get_iterable(indata.get("df")):
881 for vlp in get_iterable(df.get("virtual-link-profile")):
882 vlp_ivld_id = vlp.get("id")
883 if vlp_ivld_id and vlp_ivld_id not in all_ivld_ids:
garciadeblas4568a372021-03-24 09:19:48 +0100884 raise EngineException(
885 "df[id='{}']:virtual-link-profile='{}' must match an existing "
886 "int-virtual-link-desc".format(df["id"], vlp_ivld_id),
887 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
888 )
garciaale7cbd03c2020-11-27 10:38:35 -0300889
garciaale960531a2020-10-20 18:29:45 -0300890 @staticmethod
891 def validate_monitoring_params(indata):
garciaale7cbd03c2020-11-27 10:38:35 -0300892 all_monitoring_params = set()
893 for ivld in get_iterable(indata.get("int-virtual-link-desc")):
894 for mp in get_iterable(ivld.get("monitoring-parameters")):
895 mp_id = mp.get("id")
896 if mp_id and mp_id in all_monitoring_params:
garciadeblas4568a372021-03-24 09:19:48 +0100897 raise EngineException(
898 "Duplicated monitoring-parameter id in "
899 "int-virtual-link-desc[id='{}']:monitoring-parameters[id='{}']".format(
900 ivld["id"], mp_id
901 ),
902 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
903 )
gcalvino5e72d152018-10-23 11:46:57 +0200904 else:
garciaale7cbd03c2020-11-27 10:38:35 -0300905 all_monitoring_params.add(mp_id)
906
907 for vdu in get_iterable(indata.get("vdu")):
908 for mp in get_iterable(vdu.get("monitoring-parameter")):
909 mp_id = mp.get("id")
910 if mp_id and mp_id in all_monitoring_params:
garciadeblas4568a372021-03-24 09:19:48 +0100911 raise EngineException(
912 "Duplicated monitoring-parameter id in "
913 "vdu[id='{}']:monitoring-parameter[id='{}']".format(
914 vdu["id"], mp_id
915 ),
916 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
917 )
garciaale7cbd03c2020-11-27 10:38:35 -0300918 else:
919 all_monitoring_params.add(mp_id)
920
921 for df in get_iterable(indata.get("df")):
922 for mp in get_iterable(df.get("monitoring-parameter")):
923 mp_id = mp.get("id")
924 if mp_id and mp_id in all_monitoring_params:
garciadeblas4568a372021-03-24 09:19:48 +0100925 raise EngineException(
926 "Duplicated monitoring-parameter id in "
927 "df[id='{}']:monitoring-parameter[id='{}']".format(
928 df["id"], mp_id
929 ),
930 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
931 )
garciaale7cbd03c2020-11-27 10:38:35 -0300932 else:
933 all_monitoring_params.add(mp_id)
gcalvino5e72d152018-10-23 11:46:57 +0200934
garciaale960531a2020-10-20 18:29:45 -0300935 @staticmethod
936 def validate_scaling_group_descriptor(indata):
garciaale7cbd03c2020-11-27 10:38:35 -0300937 all_monitoring_params = set()
938 for ivld in get_iterable(indata.get("int-virtual-link-desc")):
939 for mp in get_iterable(ivld.get("monitoring-parameters")):
940 all_monitoring_params.add(mp.get("id"))
941
942 for vdu in get_iterable(indata.get("vdu")):
943 for mp in get_iterable(vdu.get("monitoring-parameter")):
944 all_monitoring_params.add(mp.get("id"))
945
946 for df in get_iterable(indata.get("df")):
947 for mp in get_iterable(df.get("monitoring-parameter")):
948 all_monitoring_params.add(mp.get("id"))
949
950 for df in get_iterable(indata.get("df")):
951 for sa in get_iterable(df.get("scaling-aspect")):
952 for sp in get_iterable(sa.get("scaling-policy")):
953 for sc in get_iterable(sp.get("scaling-criteria")):
954 sc_monitoring_param = sc.get("vnf-monitoring-param-ref")
garciadeblas4568a372021-03-24 09:19:48 +0100955 if (
956 sc_monitoring_param
957 and sc_monitoring_param not in all_monitoring_params
958 ):
959 raise EngineException(
960 "df[id='{}']:scaling-aspect[id='{}']:scaling-policy"
961 "[name='{}']:scaling-criteria[name='{}']: "
962 "vnf-monitoring-param-ref='{}' not defined in any monitoring-param".format(
963 df["id"],
964 sa["id"],
965 sp["name"],
966 sc["name"],
967 sc_monitoring_param,
968 ),
969 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
970 )
garciaale7cbd03c2020-11-27 10:38:35 -0300971
972 for sca in get_iterable(sa.get("scaling-config-action")):
garciadeblas4568a372021-03-24 09:19:48 +0100973 if (
974 "lcm-operations-configuration" not in df
975 or "operate-vnf-op-config"
976 not in df["lcm-operations-configuration"]
bravof41a52052021-02-17 18:08:01 -0300977 or not utils.find_in_list(
garciadeblas4568a372021-03-24 09:19:48 +0100978 df["lcm-operations-configuration"][
979 "operate-vnf-op-config"
980 ].get("day1-2", []),
981 lambda config: config["id"] == indata["id"],
982 )
bravof41a52052021-02-17 18:08:01 -0300983 ):
garciadeblas4568a372021-03-24 09:19:48 +0100984 raise EngineException(
985 "'day1-2 configuration' not defined in the descriptor but it is "
986 "referenced by df[id='{}']:scaling-aspect[id='{}']:scaling-config-action".format(
987 df["id"], sa["id"]
988 ),
989 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
990 )
991 for configuration in get_iterable(
992 df["lcm-operations-configuration"]["operate-vnf-op-config"].get(
993 "day1-2", []
994 )
995 ):
996 for primitive in get_iterable(
997 configuration.get("config-primitive")
998 ):
999 if (
1000 primitive["name"]
1001 == sca["vnf-config-primitive-name-ref"]
1002 ):
garciaale7cbd03c2020-11-27 10:38:35 -03001003 break
1004 else:
garciadeblas4568a372021-03-24 09:19:48 +01001005 raise EngineException(
1006 "df[id='{}']:scaling-aspect[id='{}']:scaling-config-action:vnf-"
1007 "config-primitive-name-ref='{}' does not match any "
1008 "day1-2 configuration:config-primitive:name".format(
1009 df["id"],
1010 sa["id"],
1011 sca["vnf-config-primitive-name-ref"],
1012 ),
1013 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1014 )
gcalvinoa6fe0002019-01-09 13:27:11 +01001015
delacruzramo271d2002019-12-02 21:00:37 +01001016 def delete_extra(self, session, _id, db_content, not_send_msg=None):
1017 """
1018 Deletes associate file system storage (via super)
1019 Deletes associated vnfpkgops from database.
1020 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1021 :param _id: server internal id
1022 :param db_content: The database content of the descriptor
1023 :return: None
1024 :raises: FsException in case of error while deleting associated storage
1025 """
1026 super().delete_extra(session, _id, db_content, not_send_msg)
1027 self.db.del_list("vnfpkgops", {"vnfPkgId": _id})
garciaale960531a2020-10-20 18:29:45 -03001028
Frank Bryden19b97522020-07-10 12:32:02 +00001029 def sol005_projection(self, data):
1030 data["onboardingState"] = data["_admin"]["onboardingState"]
1031 data["operationalState"] = data["_admin"]["operationalState"]
1032 data["usageState"] = data["_admin"]["usageState"]
1033
1034 links = {}
1035 links["self"] = {"href": "/vnfpkgm/v1/vnf_packages/{}".format(data["_id"])}
1036 links["vnfd"] = {"href": "/vnfpkgm/v1/vnf_packages/{}/vnfd".format(data["_id"])}
garciadeblas4568a372021-03-24 09:19:48 +01001037 links["packageContent"] = {
1038 "href": "/vnfpkgm/v1/vnf_packages/{}/package_content".format(data["_id"])
1039 }
Frank Bryden19b97522020-07-10 12:32:02 +00001040 data["_links"] = links
garciaale960531a2020-10-20 18:29:45 -03001041
Frank Bryden19b97522020-07-10 12:32:02 +00001042 return super().sol005_projection(data)
delacruzramo271d2002019-12-02 21:00:37 +01001043
tiernob24258a2018-10-04 18:39:49 +02001044
1045class NsdTopic(DescriptorTopic):
1046 topic = "nsds"
1047 topic_msg = "nsd"
1048
delacruzramo32bab472019-09-13 12:24:22 +02001049 def __init__(self, db, fs, msg, auth):
1050 DescriptorTopic.__init__(self, db, fs, msg, auth)
tiernob24258a2018-10-04 18:39:49 +02001051
garciaale7cbd03c2020-11-27 10:38:35 -03001052 def pyangbind_validation(self, item, data, force=False):
garciaaledf718ae2020-12-03 19:17:28 -03001053 if self._descriptor_data_is_in_old_format(data):
garciadeblas4568a372021-03-24 09:19:48 +01001054 raise EngineException(
1055 "ERROR: Unsupported descriptor format. Please, use an ETSI SOL006 descriptor.",
1056 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1057 )
garciaale7cbd03c2020-11-27 10:38:35 -03001058 try:
garciadeblas4568a372021-03-24 09:19:48 +01001059 nsd_vnf_profiles = data.get("df", [{}])[0].get("vnf-profile", [])
garciaale7cbd03c2020-11-27 10:38:35 -03001060 mynsd = etsi_nfv_nsd.etsi_nfv_nsd()
garciadeblas4568a372021-03-24 09:19:48 +01001061 pybindJSONDecoder.load_ietf_json(
1062 {"nsd": {"nsd": [data]}},
1063 None,
1064 None,
1065 obj=mynsd,
1066 path_helper=True,
1067 skip_unknown=force,
1068 )
garciaale7cbd03c2020-11-27 10:38:35 -03001069 out = pybindJSON.dumps(mynsd, mode="ietf")
1070 desc_out = self._remove_envelop(yaml.safe_load(out))
1071 desc_out = self._remove_yang_prefixes_from_descriptor(desc_out)
garciaale341ac1b2020-12-11 20:04:11 -03001072 if nsd_vnf_profiles:
garciadeblas4568a372021-03-24 09:19:48 +01001073 desc_out["df"][0]["vnf-profile"] = nsd_vnf_profiles
garciaale7cbd03c2020-11-27 10:38:35 -03001074 return desc_out
1075 except Exception as e:
garciadeblas4568a372021-03-24 09:19:48 +01001076 raise EngineException(
1077 "Error in pyangbind validation: {}".format(str(e)),
1078 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1079 )
garciaale7cbd03c2020-11-27 10:38:35 -03001080
tiernob24258a2018-10-04 18:39:49 +02001081 @staticmethod
garciaaledf718ae2020-12-03 19:17:28 -03001082 def _descriptor_data_is_in_old_format(data):
garciadeblas4568a372021-03-24 09:19:48 +01001083 return ("nsd-catalog" in data) or ("nsd:nsd-catalog" in data)
garciaaledf718ae2020-12-03 19:17:28 -03001084
1085 @staticmethod
tiernob24258a2018-10-04 18:39:49 +02001086 def _remove_envelop(indata=None):
1087 if not indata:
1088 return {}
1089 clean_indata = indata
1090
garciadeblas4568a372021-03-24 09:19:48 +01001091 if clean_indata.get("nsd"):
1092 clean_indata = clean_indata["nsd"]
1093 elif clean_indata.get("etsi-nfv-nsd:nsd"):
1094 clean_indata = clean_indata["etsi-nfv-nsd:nsd"]
1095 if clean_indata.get("nsd"):
1096 if (
1097 not isinstance(clean_indata["nsd"], list)
1098 or len(clean_indata["nsd"]) != 1
1099 ):
gcalvino46e4cb82018-10-26 13:10:22 +02001100 raise EngineException("'nsd' must be a list of only one element")
garciadeblas4568a372021-03-24 09:19:48 +01001101 clean_indata = clean_indata["nsd"][0]
tiernob24258a2018-10-04 18:39:49 +02001102 return clean_indata
1103
gcalvinoa6fe0002019-01-09 13:27:11 +01001104 def _validate_input_new(self, indata, storage_params, force=False):
Frank Bryden19b97522020-07-10 12:32:02 +00001105 indata.pop("nsdOnboardingState", None)
1106 indata.pop("nsdOperationalState", None)
1107 indata.pop("nsdUsageState", None)
1108
1109 indata.pop("links", None)
1110
gcalvino46e4cb82018-10-26 13:10:22 +02001111 indata = self.pyangbind_validation("nsds", indata, force)
tierno5a5c2182018-11-20 12:27:42 +00001112 # Cross references validation in the descriptor
tiernoaa1ca7b2018-11-08 19:00:20 +01001113 # 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 -03001114 for vld in get_iterable(indata.get("virtual-link-desc")):
1115 self.validate_vld_mgmt_network_with_virtual_link_protocol_data(vld, indata)
garciaale960531a2020-10-20 18:29:45 -03001116
garciaale7cbd03c2020-11-27 10:38:35 -03001117 self.validate_vnf_profiles_vnfd_id(indata)
garciaale960531a2020-10-20 18:29:45 -03001118
tiernob24258a2018-10-04 18:39:49 +02001119 return indata
1120
garciaale960531a2020-10-20 18:29:45 -03001121 @staticmethod
garciaale7cbd03c2020-11-27 10:38:35 -03001122 def validate_vld_mgmt_network_with_virtual_link_protocol_data(vld, indata):
1123 if not vld.get("mgmt-network"):
1124 return
1125 vld_id = vld.get("id")
1126 for df in get_iterable(indata.get("df")):
1127 for vlp in get_iterable(df.get("virtual-link-profile")):
1128 if vld_id and vld_id == vlp.get("virtual-link-desc-id"):
1129 if vlp.get("virtual-link-protocol-data"):
garciadeblas4568a372021-03-24 09:19:48 +01001130 raise EngineException(
1131 "Error at df[id='{}']:virtual-link-profile[id='{}']:virtual-link-"
1132 "protocol-data You cannot set a virtual-link-protocol-data "
1133 "when mgmt-network is True".format(df["id"], vlp["id"]),
1134 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1135 )
garciaale960531a2020-10-20 18:29:45 -03001136
1137 @staticmethod
garciaale7cbd03c2020-11-27 10:38:35 -03001138 def validate_vnf_profiles_vnfd_id(indata):
1139 all_vnfd_ids = set(get_iterable(indata.get("vnfd-id")))
1140 for df in get_iterable(indata.get("df")):
1141 for vnf_profile in get_iterable(df.get("vnf-profile")):
1142 vnfd_id = vnf_profile.get("vnfd-id")
1143 if vnfd_id and vnfd_id not in all_vnfd_ids:
garciadeblas4568a372021-03-24 09:19:48 +01001144 raise EngineException(
1145 "Error at df[id='{}']:vnf_profile[id='{}']:vnfd-id='{}' "
1146 "does not match any vnfd-id".format(
1147 df["id"], vnf_profile["id"], vnfd_id
1148 ),
1149 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1150 )
garciaale960531a2020-10-20 18:29:45 -03001151
Frank Brydendeba68e2020-07-27 13:55:11 +00001152 def _validate_input_edit(self, indata, content, force=False):
tiernoaa1ca7b2018-11-08 19:00:20 +01001153 # not needed to validate with pyangbind becuase it will be validated at check_conflict_on_edit
Frank Brydendeba68e2020-07-27 13:55:11 +00001154 """
1155 indata looks as follows:
garciadeblas4568a372021-03-24 09:19:48 +01001156 - In the new case (conformant)
1157 {'nsdOperationalState': 'DISABLED', 'userDefinedData': {'id': 'string23',
Frank Brydendeba68e2020-07-27 13:55:11 +00001158 '_id': 'c6ddc544-cede-4b94-9ebe-be07b298a3c1', 'name': 'simon46'}}
1159 - In the old case (backwards-compatible)
1160 {'id': 'string23', '_id': 'c6ddc544-cede-4b94-9ebe-be07b298a3c1', 'name': 'simon46'}
1161 """
1162 if "_admin" not in indata:
1163 indata["_admin"] = {}
1164
1165 if "nsdOperationalState" in indata:
1166 if indata["nsdOperationalState"] in ("ENABLED", "DISABLED"):
1167 indata["_admin"]["operationalState"] = indata.pop("nsdOperationalState")
1168 else:
garciadeblas4568a372021-03-24 09:19:48 +01001169 raise EngineException(
1170 "State '{}' is not a valid operational state".format(
1171 indata["nsdOperationalState"]
1172 ),
1173 http_code=HTTPStatus.BAD_REQUEST,
1174 )
Frank Brydendeba68e2020-07-27 13:55:11 +00001175
garciadeblas4568a372021-03-24 09:19:48 +01001176 # In the case of user defined data, we need to put the data in the root of the object
Frank Brydendeba68e2020-07-27 13:55:11 +00001177 # to preserve current expected behaviour
1178 if "userDefinedData" in indata:
1179 data = indata.pop("userDefinedData")
1180 if type(data) == dict:
1181 indata["_admin"]["userDefinedData"] = data
1182 else:
garciadeblas4568a372021-03-24 09:19:48 +01001183 raise EngineException(
1184 "userDefinedData should be an object, but is '{}' instead".format(
1185 type(data)
1186 ),
1187 http_code=HTTPStatus.BAD_REQUEST,
1188 )
1189 if (
1190 "operationalState" in indata["_admin"]
1191 and content["_admin"]["operationalState"]
1192 == indata["_admin"]["operationalState"]
1193 ):
1194 raise EngineException(
1195 "nsdOperationalState already {}".format(
1196 content["_admin"]["operationalState"]
1197 ),
1198 http_code=HTTPStatus.CONFLICT,
1199 )
tiernob24258a2018-10-04 18:39:49 +02001200 return indata
1201
tierno65ca36d2019-02-12 19:27:52 +01001202 def _check_descriptor_dependencies(self, session, descriptor):
tiernob24258a2018-10-04 18:39:49 +02001203 """
tierno5a5c2182018-11-20 12:27:42 +00001204 Check that the dependent descriptors exist on a new descriptor or edition. Also checks references to vnfd
1205 connection points are ok
tierno65ca36d2019-02-12 19:27:52 +01001206 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +02001207 :param descriptor: descriptor to be inserted or edit
1208 :return: None or raises exception
1209 """
tierno65ca36d2019-02-12 19:27:52 +01001210 if session["force"]:
tiernob24258a2018-10-04 18:39:49 +02001211 return
garciaale7cbd03c2020-11-27 10:38:35 -03001212 vnfds_index = self._get_descriptor_constituent_vnfds_index(session, descriptor)
garciaale960531a2020-10-20 18:29:45 -03001213
1214 # Cross references validation in the descriptor and vnfd connection point validation
garciaale7cbd03c2020-11-27 10:38:35 -03001215 for df in get_iterable(descriptor.get("df")):
1216 self.validate_df_vnf_profiles_constituent_connection_points(df, vnfds_index)
garciaale960531a2020-10-20 18:29:45 -03001217
garciaale7cbd03c2020-11-27 10:38:35 -03001218 def _get_descriptor_constituent_vnfds_index(self, session, descriptor):
1219 vnfds_index = {}
1220 if descriptor.get("vnfd-id") and not session["force"]:
1221 for vnfd_id in get_iterable(descriptor.get("vnfd-id")):
garciaale960531a2020-10-20 18:29:45 -03001222 query_filter = self._get_project_filter(session)
1223 query_filter["id"] = vnfd_id
1224 vnf_list = self.db.get_list("vnfds", query_filter)
tierno5a5c2182018-11-20 12:27:42 +00001225 if not vnf_list:
garciadeblas4568a372021-03-24 09:19:48 +01001226 raise EngineException(
1227 "Descriptor error at 'vnfd-id'='{}' references a non "
1228 "existing vnfd".format(vnfd_id),
1229 http_code=HTTPStatus.CONFLICT,
1230 )
garciaale7cbd03c2020-11-27 10:38:35 -03001231 vnfds_index[vnfd_id] = vnf_list[0]
1232 return vnfds_index
garciaale960531a2020-10-20 18:29:45 -03001233
1234 @staticmethod
garciaale7cbd03c2020-11-27 10:38:35 -03001235 def validate_df_vnf_profiles_constituent_connection_points(df, vnfds_index):
1236 for vnf_profile in get_iterable(df.get("vnf-profile")):
1237 vnfd = vnfds_index.get(vnf_profile["vnfd-id"])
1238 all_vnfd_ext_cpds = set()
1239 for ext_cpd in get_iterable(vnfd.get("ext-cpd")):
garciadeblas4568a372021-03-24 09:19:48 +01001240 if ext_cpd.get("id"):
1241 all_vnfd_ext_cpds.add(ext_cpd.get("id"))
garciaale7cbd03c2020-11-27 10:38:35 -03001242
garciadeblas4568a372021-03-24 09:19:48 +01001243 for virtual_link in get_iterable(
1244 vnf_profile.get("virtual-link-connectivity")
1245 ):
garciaale7cbd03c2020-11-27 10:38:35 -03001246 for vl_cpd in get_iterable(virtual_link.get("constituent-cpd-id")):
garciadeblas4568a372021-03-24 09:19:48 +01001247 vl_cpd_id = vl_cpd.get("constituent-cpd-id")
garciaale7cbd03c2020-11-27 10:38:35 -03001248 if vl_cpd_id and vl_cpd_id not in all_vnfd_ext_cpds:
garciadeblas4568a372021-03-24 09:19:48 +01001249 raise EngineException(
1250 "Error at df[id='{}']:vnf-profile[id='{}']:virtual-link-connectivity"
1251 "[virtual-link-profile-id='{}']:constituent-cpd-id='{}' references a "
1252 "non existing ext-cpd:id inside vnfd '{}'".format(
1253 df["id"],
1254 vnf_profile["id"],
1255 virtual_link["virtual-link-profile-id"],
1256 vl_cpd_id,
1257 vnfd["id"],
1258 ),
1259 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1260 )
tiernob24258a2018-10-04 18:39:49 +02001261
tierno65ca36d2019-02-12 19:27:52 +01001262 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
garciadeblas4568a372021-03-24 09:19:48 +01001263 final_content = super().check_conflict_on_edit(
1264 session, final_content, edit_content, _id
1265 )
tiernob24258a2018-10-04 18:39:49 +02001266
tierno65ca36d2019-02-12 19:27:52 +01001267 self._check_descriptor_dependencies(session, final_content)
tiernob24258a2018-10-04 18:39:49 +02001268
bravofb995ea22021-02-10 10:57:52 -03001269 return final_content
1270
tiernob4844ab2019-05-23 08:42:12 +00001271 def check_conflict_on_del(self, session, _id, db_content):
tiernob24258a2018-10-04 18:39:49 +02001272 """
1273 Check that there is not any NSR that uses this NSD. Only NSRs belonging to this project are considered. Note
1274 that NSD can be public and be used by other projects.
tierno65ca36d2019-02-12 19:27:52 +01001275 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob4844ab2019-05-23 08:42:12 +00001276 :param _id: nsd internal id
1277 :param db_content: The database content of the _id
tiernob24258a2018-10-04 18:39:49 +02001278 :return: None or raises EngineException with the conflict
1279 """
tierno65ca36d2019-02-12 19:27:52 +01001280 if session["force"]:
tiernob24258a2018-10-04 18:39:49 +02001281 return
tiernob4844ab2019-05-23 08:42:12 +00001282 descriptor = db_content
1283 descriptor_id = descriptor.get("id")
1284 if not descriptor_id: # empty nsd not uploaded
1285 return
1286
1287 # check NSD used by NS
tierno65ca36d2019-02-12 19:27:52 +01001288 _filter = self._get_project_filter(session)
tiernob4844ab2019-05-23 08:42:12 +00001289 _filter["nsd-id"] = _id
tiernob24258a2018-10-04 18:39:49 +02001290 if self.db.get_list("nsrs", _filter):
garciadeblas4568a372021-03-24 09:19:48 +01001291 raise EngineException(
1292 "There is at least one NS instance using this descriptor",
1293 http_code=HTTPStatus.CONFLICT,
1294 )
tiernob4844ab2019-05-23 08:42:12 +00001295
1296 # check NSD referenced by NST
1297 del _filter["nsd-id"]
1298 _filter["netslice-subnet.ANYINDEX.nsd-ref"] = descriptor_id
1299 if self.db.get_list("nsts", _filter):
garciadeblas4568a372021-03-24 09:19:48 +01001300 raise EngineException(
1301 "There is at least one NetSlice Template referencing this descriptor",
1302 http_code=HTTPStatus.CONFLICT,
1303 )
garciaale960531a2020-10-20 18:29:45 -03001304
Frank Bryden19b97522020-07-10 12:32:02 +00001305 def sol005_projection(self, data):
1306 data["nsdOnboardingState"] = data["_admin"]["onboardingState"]
1307 data["nsdOperationalState"] = data["_admin"]["operationalState"]
1308 data["nsdUsageState"] = data["_admin"]["usageState"]
1309
1310 links = {}
1311 links["self"] = {"href": "/nsd/v1/ns_descriptors/{}".format(data["_id"])}
garciadeblas4568a372021-03-24 09:19:48 +01001312 links["nsd_content"] = {
1313 "href": "/nsd/v1/ns_descriptors/{}/nsd_content".format(data["_id"])
1314 }
Frank Bryden19b97522020-07-10 12:32:02 +00001315 data["_links"] = links
garciaale960531a2020-10-20 18:29:45 -03001316
Frank Bryden19b97522020-07-10 12:32:02 +00001317 return super().sol005_projection(data)
tiernob24258a2018-10-04 18:39:49 +02001318
1319
Felipe Vicensb57758d2018-10-16 16:00:20 +02001320class NstTopic(DescriptorTopic):
1321 topic = "nsts"
1322 topic_msg = "nst"
tierno6b02b052020-06-02 10:07:41 +00001323 quota_name = "slice_templates"
Felipe Vicensb57758d2018-10-16 16:00:20 +02001324
delacruzramo32bab472019-09-13 12:24:22 +02001325 def __init__(self, db, fs, msg, auth):
1326 DescriptorTopic.__init__(self, db, fs, msg, auth)
Felipe Vicensb57758d2018-10-16 16:00:20 +02001327
garciaale7cbd03c2020-11-27 10:38:35 -03001328 def pyangbind_validation(self, item, data, force=False):
1329 try:
1330 mynst = nst_im()
garciadeblas4568a372021-03-24 09:19:48 +01001331 pybindJSONDecoder.load_ietf_json(
1332 {"nst": [data]},
1333 None,
1334 None,
1335 obj=mynst,
1336 path_helper=True,
1337 skip_unknown=force,
1338 )
garciaale7cbd03c2020-11-27 10:38:35 -03001339 out = pybindJSON.dumps(mynst, mode="ietf")
1340 desc_out = self._remove_envelop(yaml.safe_load(out))
1341 return desc_out
1342 except Exception as e:
garciadeblas4568a372021-03-24 09:19:48 +01001343 raise EngineException(
1344 "Error in pyangbind validation: {}".format(str(e)),
1345 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1346 )
garciaale7cbd03c2020-11-27 10:38:35 -03001347
Felipe Vicensb57758d2018-10-16 16:00:20 +02001348 @staticmethod
1349 def _remove_envelop(indata=None):
1350 if not indata:
1351 return {}
1352 clean_indata = indata
1353
garciadeblas4568a372021-03-24 09:19:48 +01001354 if clean_indata.get("nst"):
1355 if (
1356 not isinstance(clean_indata["nst"], list)
1357 or len(clean_indata["nst"]) != 1
1358 ):
Felipe Vicensb57758d2018-10-16 16:00:20 +02001359 raise EngineException("'nst' must be a list only one element")
garciadeblas4568a372021-03-24 09:19:48 +01001360 clean_indata = clean_indata["nst"][0]
1361 elif clean_indata.get("nst:nst"):
1362 if (
1363 not isinstance(clean_indata["nst:nst"], list)
1364 or len(clean_indata["nst:nst"]) != 1
1365 ):
gcalvino70434c12018-11-27 15:17:04 +01001366 raise EngineException("'nst:nst' must be a list only one element")
garciadeblas4568a372021-03-24 09:19:48 +01001367 clean_indata = clean_indata["nst:nst"][0]
Felipe Vicensb57758d2018-10-16 16:00:20 +02001368 return clean_indata
1369
gcalvinoa6fe0002019-01-09 13:27:11 +01001370 def _validate_input_new(self, indata, storage_params, force=False):
Frank Bryden19b97522020-07-10 12:32:02 +00001371 indata.pop("onboardingState", None)
1372 indata.pop("operationalState", None)
1373 indata.pop("usageState", None)
gcalvino70434c12018-11-27 15:17:04 +01001374 indata = self.pyangbind_validation("nsts", indata, force)
Felipe Vicense36ab852018-11-23 14:12:09 +01001375 return indata.copy()
1376
Felipe Vicensb57758d2018-10-16 16:00:20 +02001377 def _check_descriptor_dependencies(self, session, descriptor):
1378 """
1379 Check that the dependent descriptors exist on a new descriptor or edition
tierno65ca36d2019-02-12 19:27:52 +01001380 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Felipe Vicensb57758d2018-10-16 16:00:20 +02001381 :param descriptor: descriptor to be inserted or edit
1382 :return: None or raises exception
1383 """
1384 if not descriptor.get("netslice-subnet"):
1385 return
1386 for nsd in descriptor["netslice-subnet"]:
1387 nsd_id = nsd["nsd-ref"]
tierno65ca36d2019-02-12 19:27:52 +01001388 filter_q = self._get_project_filter(session)
Felipe Vicensb57758d2018-10-16 16:00:20 +02001389 filter_q["id"] = nsd_id
1390 if not self.db.get_list("nsds", filter_q):
garciadeblas4568a372021-03-24 09:19:48 +01001391 raise EngineException(
1392 "Descriptor error at 'netslice-subnet':'nsd-ref'='{}' references a non "
1393 "existing nsd".format(nsd_id),
1394 http_code=HTTPStatus.CONFLICT,
1395 )
Felipe Vicensb57758d2018-10-16 16:00:20 +02001396
tierno65ca36d2019-02-12 19:27:52 +01001397 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
garciadeblas4568a372021-03-24 09:19:48 +01001398 final_content = super().check_conflict_on_edit(
1399 session, final_content, edit_content, _id
1400 )
Felipe Vicensb57758d2018-10-16 16:00:20 +02001401
1402 self._check_descriptor_dependencies(session, final_content)
bravofb995ea22021-02-10 10:57:52 -03001403 return final_content
Felipe Vicensb57758d2018-10-16 16:00:20 +02001404
tiernob4844ab2019-05-23 08:42:12 +00001405 def check_conflict_on_del(self, session, _id, db_content):
Felipe Vicensb57758d2018-10-16 16:00:20 +02001406 """
1407 Check that there is not any NSIR that uses this NST. Only NSIRs belonging to this project are considered. Note
1408 that NST can be public and be used by other projects.
tierno65ca36d2019-02-12 19:27:52 +01001409 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Felipe Vicens07f31722018-10-29 15:16:44 +01001410 :param _id: nst internal id
tiernob4844ab2019-05-23 08:42:12 +00001411 :param db_content: The database content of the _id.
Felipe Vicensb57758d2018-10-16 16:00:20 +02001412 :return: None or raises EngineException with the conflict
1413 """
1414 # TODO: Check this method
tierno65ca36d2019-02-12 19:27:52 +01001415 if session["force"]:
Felipe Vicensb57758d2018-10-16 16:00:20 +02001416 return
Felipe Vicens07f31722018-10-29 15:16:44 +01001417 # Get Network Slice Template from Database
tierno65ca36d2019-02-12 19:27:52 +01001418 _filter = self._get_project_filter(session)
tiernoea97c042019-09-13 09:44:42 +00001419 _filter["_admin.nst-id"] = _id
tiernob4844ab2019-05-23 08:42:12 +00001420 if self.db.get_list("nsis", _filter):
garciadeblas4568a372021-03-24 09:19:48 +01001421 raise EngineException(
1422 "there is at least one Netslice Instance using this descriptor",
1423 http_code=HTTPStatus.CONFLICT,
1424 )
Felipe Vicensb57758d2018-10-16 16:00:20 +02001425
Frank Bryden19b97522020-07-10 12:32:02 +00001426 def sol005_projection(self, data):
1427 data["onboardingState"] = data["_admin"]["onboardingState"]
1428 data["operationalState"] = data["_admin"]["operationalState"]
1429 data["usageState"] = data["_admin"]["usageState"]
1430
1431 links = {}
1432 links["self"] = {"href": "/nst/v1/netslice_templates/{}".format(data["_id"])}
1433 links["nst"] = {"href": "/nst/v1/netslice_templates/{}/nst".format(data["_id"])}
1434 data["_links"] = links
1435
1436 return super().sol005_projection(data)
1437
Felipe Vicensb57758d2018-10-16 16:00:20 +02001438
tiernob24258a2018-10-04 18:39:49 +02001439class PduTopic(BaseTopic):
1440 topic = "pdus"
1441 topic_msg = "pdu"
tierno6b02b052020-06-02 10:07:41 +00001442 quota_name = "pduds"
tiernob24258a2018-10-04 18:39:49 +02001443 schema_new = pdu_new_schema
1444 schema_edit = pdu_edit_schema
1445
delacruzramo32bab472019-09-13 12:24:22 +02001446 def __init__(self, db, fs, msg, auth):
1447 BaseTopic.__init__(self, db, fs, msg, auth)
tiernob24258a2018-10-04 18:39:49 +02001448
1449 @staticmethod
1450 def format_on_new(content, project_id=None, make_public=False):
tierno36ec8602018-11-02 17:27:11 +01001451 BaseTopic.format_on_new(content, project_id=project_id, make_public=make_public)
tiernob24258a2018-10-04 18:39:49 +02001452 content["_admin"]["onboardingState"] = "CREATED"
tierno36ec8602018-11-02 17:27:11 +01001453 content["_admin"]["operationalState"] = "ENABLED"
1454 content["_admin"]["usageState"] = "NOT_IN_USE"
tiernob24258a2018-10-04 18:39:49 +02001455
tiernob4844ab2019-05-23 08:42:12 +00001456 def check_conflict_on_del(self, session, _id, db_content):
1457 """
1458 Check that there is not any vnfr that uses this PDU
1459 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1460 :param _id: pdu internal id
1461 :param db_content: The database content of the _id.
1462 :return: None or raises EngineException with the conflict
1463 """
tierno65ca36d2019-02-12 19:27:52 +01001464 if session["force"]:
tiernob24258a2018-10-04 18:39:49 +02001465 return
tiernob4844ab2019-05-23 08:42:12 +00001466
1467 _filter = self._get_project_filter(session)
1468 _filter["vdur.pdu-id"] = _id
tiernob24258a2018-10-04 18:39:49 +02001469 if self.db.get_list("vnfrs", _filter):
garciadeblas4568a372021-03-24 09:19:48 +01001470 raise EngineException(
1471 "There is at least one VNF instance using this PDU",
1472 http_code=HTTPStatus.CONFLICT,
1473 )
delacruzramo271d2002019-12-02 21:00:37 +01001474
1475
1476class VnfPkgOpTopic(BaseTopic):
1477 topic = "vnfpkgops"
1478 topic_msg = "vnfd"
1479 schema_new = vnfpkgop_new_schema
1480 schema_edit = None
1481
1482 def __init__(self, db, fs, msg, auth):
1483 BaseTopic.__init__(self, db, fs, msg, auth)
1484
1485 def edit(self, session, _id, indata=None, kwargs=None, content=None):
garciadeblas4568a372021-03-24 09:19:48 +01001486 raise EngineException(
1487 "Method 'edit' not allowed for topic '{}'".format(self.topic),
1488 HTTPStatus.METHOD_NOT_ALLOWED,
1489 )
delacruzramo271d2002019-12-02 21:00:37 +01001490
1491 def delete(self, session, _id, dry_run=False):
garciadeblas4568a372021-03-24 09:19:48 +01001492 raise EngineException(
1493 "Method 'delete' not allowed for topic '{}'".format(self.topic),
1494 HTTPStatus.METHOD_NOT_ALLOWED,
1495 )
delacruzramo271d2002019-12-02 21:00:37 +01001496
1497 def delete_list(self, session, filter_q=None):
garciadeblas4568a372021-03-24 09:19:48 +01001498 raise EngineException(
1499 "Method 'delete_list' not allowed for topic '{}'".format(self.topic),
1500 HTTPStatus.METHOD_NOT_ALLOWED,
1501 )
delacruzramo271d2002019-12-02 21:00:37 +01001502
1503 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
1504 """
1505 Creates a new entry into database.
1506 :param rollback: list to append created items at database in case a rollback may to be done
1507 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1508 :param indata: data to be inserted
1509 :param kwargs: used to override the indata descriptor
1510 :param headers: http request headers
1511 :return: _id, op_id:
1512 _id: identity of the inserted data.
1513 op_id: None
1514 """
1515 self._update_input_with_kwargs(indata, kwargs)
1516 validate_input(indata, self.schema_new)
1517 vnfpkg_id = indata["vnfPkgId"]
1518 filter_q = BaseTopic._get_project_filter(session)
1519 filter_q["_id"] = vnfpkg_id
1520 vnfd = self.db.get_one("vnfds", filter_q)
1521 operation = indata["lcmOperationType"]
1522 kdu_name = indata["kdu_name"]
1523 for kdu in vnfd.get("kdu", []):
1524 if kdu["name"] == kdu_name:
1525 helm_chart = kdu.get("helm-chart")
1526 juju_bundle = kdu.get("juju-bundle")
1527 break
1528 else:
garciadeblas4568a372021-03-24 09:19:48 +01001529 raise EngineException(
1530 "Not found vnfd[id='{}']:kdu[name='{}']".format(vnfpkg_id, kdu_name)
1531 )
delacruzramo271d2002019-12-02 21:00:37 +01001532 if helm_chart:
1533 indata["helm-chart"] = helm_chart
1534 match = fullmatch(r"([^/]*)/([^/]*)", helm_chart)
1535 repo_name = match.group(1) if match else None
1536 elif juju_bundle:
1537 indata["juju-bundle"] = juju_bundle
1538 match = fullmatch(r"([^/]*)/([^/]*)", juju_bundle)
1539 repo_name = match.group(1) if match else None
1540 else:
garciadeblas4568a372021-03-24 09:19:48 +01001541 raise EngineException(
1542 "Found neither 'helm-chart' nor 'juju-bundle' in vnfd[id='{}']:kdu[name='{}']".format(
1543 vnfpkg_id, kdu_name
1544 )
1545 )
delacruzramo271d2002019-12-02 21:00:37 +01001546 if repo_name:
1547 del filter_q["_id"]
1548 filter_q["name"] = repo_name
1549 repo = self.db.get_one("k8srepos", filter_q)
1550 k8srepo_id = repo.get("_id")
1551 k8srepo_url = repo.get("url")
1552 else:
1553 k8srepo_id = None
1554 k8srepo_url = None
1555 indata["k8srepoId"] = k8srepo_id
1556 indata["k8srepo_url"] = k8srepo_url
1557 vnfpkgop_id = str(uuid4())
1558 vnfpkgop_desc = {
1559 "_id": vnfpkgop_id,
1560 "operationState": "PROCESSING",
1561 "vnfPkgId": vnfpkg_id,
1562 "lcmOperationType": operation,
1563 "isAutomaticInvocation": False,
1564 "isCancelPending": False,
1565 "operationParams": indata,
1566 "links": {
1567 "self": "/osm/vnfpkgm/v1/vnfpkg_op_occs/" + vnfpkgop_id,
1568 "vnfpkg": "/osm/vnfpkgm/v1/vnf_packages/" + vnfpkg_id,
garciadeblas4568a372021-03-24 09:19:48 +01001569 },
delacruzramo271d2002019-12-02 21:00:37 +01001570 }
garciadeblas4568a372021-03-24 09:19:48 +01001571 self.format_on_new(
1572 vnfpkgop_desc, session["project_id"], make_public=session["public"]
1573 )
delacruzramo271d2002019-12-02 21:00:37 +01001574 ctime = vnfpkgop_desc["_admin"]["created"]
1575 vnfpkgop_desc["statusEnteredTime"] = ctime
1576 vnfpkgop_desc["startTime"] = ctime
1577 self.db.create(self.topic, vnfpkgop_desc)
1578 rollback.append({"topic": self.topic, "_id": vnfpkgop_id})
1579 self.msg.write(self.topic_msg, operation, vnfpkgop_desc)
1580 return vnfpkgop_id, None