blob: 8f3529cb8376cfd5d1fe31a9df3e988de5e174c7 [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
bravofb995ea22021-02-10 10:57:52 -030019import copy
beierlmcee2ebf2022-03-29 17:42:48 -040020import os
21import shutil
aticig9cfa8162022-04-07 11:57:18 +030022import functools
garciadeblas4568a372021-03-24 09:19:48 +010023
tiernob24258a2018-10-04 18:39:49 +020024# import logging
aticig9cfa8162022-04-07 11:57:18 +030025from deepdiff import DeepDiff
tiernob24258a2018-10-04 18:39:49 +020026from hashlib import md5
27from osm_common.dbbase import DbException, deep_update_rfc7396
28from http import HTTPStatus
delacruzramo26301bb2019-11-15 14:45:32 +010029from time import time
delacruzramo271d2002019-12-02 21:00:37 +010030from uuid import uuid4
31from re import fullmatch
bravofc26740a2021-11-08 09:44:54 -030032from zipfile import ZipFile
garciadeblas4568a372021-03-24 09:19:48 +010033from osm_nbi.validation import (
34 ValidationError,
35 pdu_new_schema,
36 pdu_edit_schema,
37 validate_input,
38 vnfpkgop_new_schema,
39)
tierno23acf402019-08-28 13:36:34 +000040from osm_nbi.base_topic import BaseTopic, EngineException, get_iterable
sousaedu317b9fd2021-07-29 17:40:16 +020041from osm_im import etsi_nfv_vnfd, etsi_nfv_nsd
gcalvino70434c12018-11-27 15:17:04 +010042from osm_im.nst import nst as nst_im
gcalvino46e4cb82018-10-26 13:10:22 +020043from pyangbind.lib.serialise import pybindJSONDecoder
44import pyangbind.lib.pybindJSON as pybindJSON
bravof41a52052021-02-17 18:08:01 -030045from osm_nbi import utils
tiernob24258a2018-10-04 18:39:49 +020046
47__author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
48
49
50class DescriptorTopic(BaseTopic):
delacruzramo32bab472019-09-13 12:24:22 +020051 def __init__(self, db, fs, msg, auth):
beierlmcee2ebf2022-03-29 17:42:48 -040052
delacruzramo32bab472019-09-13 12:24:22 +020053 BaseTopic.__init__(self, db, fs, msg, auth)
tiernob24258a2018-10-04 18:39:49 +020054
tierno65ca36d2019-02-12 19:27:52 +010055 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
garciadeblas4568a372021-03-24 09:19:48 +010056 final_content = super().check_conflict_on_edit(
57 session, final_content, edit_content, _id
58 )
K Sai Kiran45bd94c2019-11-25 17:30:37 +053059
60 def _check_unique_id_name(descriptor, position=""):
61 for desc_key, desc_item in descriptor.items():
62 if isinstance(desc_item, list) and desc_item:
63 used_ids = []
64 desc_item_id = None
65 for index, list_item in enumerate(desc_item):
66 if isinstance(list_item, dict):
garciadeblas4568a372021-03-24 09:19:48 +010067 _check_unique_id_name(
68 list_item, "{}.{}[{}]".format(position, desc_key, index)
69 )
K Sai Kiran45bd94c2019-11-25 17:30:37 +053070 # Base case
garciadeblas4568a372021-03-24 09:19:48 +010071 if index == 0 and (
72 list_item.get("id") or list_item.get("name")
73 ):
K Sai Kiran45bd94c2019-11-25 17:30:37 +053074 desc_item_id = "id" if list_item.get("id") else "name"
75 if desc_item_id and list_item.get(desc_item_id):
76 if list_item[desc_item_id] in used_ids:
garciadeblas4568a372021-03-24 09:19:48 +010077 position = "{}.{}[{}]".format(
78 position, desc_key, index
79 )
80 raise EngineException(
81 "Error: identifier {} '{}' is not unique and repeats at '{}'".format(
82 desc_item_id,
83 list_item[desc_item_id],
84 position,
85 ),
86 HTTPStatus.UNPROCESSABLE_ENTITY,
87 )
K Sai Kiran45bd94c2019-11-25 17:30:37 +053088 used_ids.append(list_item[desc_item_id])
garciaale960531a2020-10-20 18:29:45 -030089
K Sai Kiran45bd94c2019-11-25 17:30:37 +053090 _check_unique_id_name(final_content)
tiernoaa1ca7b2018-11-08 19:00:20 +010091 # 1. validate again with pyangbind
92 # 1.1. remove internal keys
93 internal_keys = {}
94 for k in ("_id", "_admin"):
95 if k in final_content:
96 internal_keys[k] = final_content.pop(k)
gcalvinoa6fe0002019-01-09 13:27:11 +010097 storage_params = internal_keys["_admin"].get("storage")
garciadeblas4568a372021-03-24 09:19:48 +010098 serialized = self._validate_input_new(
99 final_content, storage_params, session["force"]
100 )
bravofb995ea22021-02-10 10:57:52 -0300101
tiernoaa1ca7b2018-11-08 19:00:20 +0100102 # 1.2. modify final_content with a serialized version
bravofb995ea22021-02-10 10:57:52 -0300103 final_content = copy.deepcopy(serialized)
tiernoaa1ca7b2018-11-08 19:00:20 +0100104 # 1.3. restore internal keys
105 for k, v in internal_keys.items():
106 final_content[k] = v
tierno65ca36d2019-02-12 19:27:52 +0100107 if session["force"]:
bravofb995ea22021-02-10 10:57:52 -0300108 return final_content
109
tiernoaa1ca7b2018-11-08 19:00:20 +0100110 # 2. check that this id is not present
111 if "id" in edit_content:
tierno65ca36d2019-02-12 19:27:52 +0100112 _filter = self._get_project_filter(session)
bravofb995ea22021-02-10 10:57:52 -0300113
tiernoaa1ca7b2018-11-08 19:00:20 +0100114 _filter["id"] = final_content["id"]
115 _filter["_id.neq"] = _id
bravofb995ea22021-02-10 10:57:52 -0300116
tiernoaa1ca7b2018-11-08 19:00:20 +0100117 if self.db.get_one(self.topic, _filter, fail_on_empty=False):
garciadeblas4568a372021-03-24 09:19:48 +0100118 raise EngineException(
119 "{} with id '{}' already exists for this project".format(
120 self.topic[:-1], final_content["id"]
121 ),
122 HTTPStatus.CONFLICT,
123 )
tiernob24258a2018-10-04 18:39:49 +0200124
bravofb995ea22021-02-10 10:57:52 -0300125 return final_content
126
tiernob24258a2018-10-04 18:39:49 +0200127 @staticmethod
128 def format_on_new(content, project_id=None, make_public=False):
129 BaseTopic.format_on_new(content, project_id=project_id, make_public=make_public)
130 content["_admin"]["onboardingState"] = "CREATED"
131 content["_admin"]["operationalState"] = "DISABLED"
tierno36ec8602018-11-02 17:27:11 +0100132 content["_admin"]["usageState"] = "NOT_IN_USE"
tiernob24258a2018-10-04 18:39:49 +0200133
tiernobee3bad2019-12-05 12:26:01 +0000134 def delete_extra(self, session, _id, db_content, not_send_msg=None):
tiernob4844ab2019-05-23 08:42:12 +0000135 """
136 Deletes file system storage associated with the descriptor
137 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
138 :param _id: server internal id
139 :param db_content: The database content of the descriptor
tiernobee3bad2019-12-05 12:26:01 +0000140 :param not_send_msg: To not send message (False) or store content (list) instead
tiernob4844ab2019-05-23 08:42:12 +0000141 :return: None if ok or raises EngineException with the problem
142 """
tiernob24258a2018-10-04 18:39:49 +0200143 self.fs.file_delete(_id, ignore_non_exist=True)
tiernof717cbe2018-12-03 16:35:42 +0000144 self.fs.file_delete(_id + "_", ignore_non_exist=True) # remove temp folder
beierlmcee2ebf2022-03-29 17:42:48 -0400145 # Remove file revisions
146 if "revision" in db_content["_admin"]:
147 revision = db_content["_admin"]["revision"]
148 while revision > 0:
149 self.fs.file_delete(_id + ":" + str(revision), ignore_non_exist=True)
150 revision = revision - 1
151
tiernob24258a2018-10-04 18:39:49 +0200152
153 @staticmethod
154 def get_one_by_id(db, session, topic, id):
155 # find owned by this project
tierno65ca36d2019-02-12 19:27:52 +0100156 _filter = BaseTopic._get_project_filter(session)
tiernob24258a2018-10-04 18:39:49 +0200157 _filter["id"] = id
158 desc_list = db.get_list(topic, _filter)
159 if len(desc_list) == 1:
160 return desc_list[0]
161 elif len(desc_list) > 1:
garciadeblas4568a372021-03-24 09:19:48 +0100162 raise DbException(
163 "Found more than one {} with id='{}' belonging to this project".format(
164 topic[:-1], id
165 ),
166 HTTPStatus.CONFLICT,
167 )
tiernob24258a2018-10-04 18:39:49 +0200168
169 # not found any: try to find public
tierno65ca36d2019-02-12 19:27:52 +0100170 _filter = BaseTopic._get_project_filter(session)
tiernob24258a2018-10-04 18:39:49 +0200171 _filter["id"] = id
172 desc_list = db.get_list(topic, _filter)
173 if not desc_list:
garciadeblas4568a372021-03-24 09:19:48 +0100174 raise DbException(
175 "Not found any {} with id='{}'".format(topic[:-1], id),
176 HTTPStatus.NOT_FOUND,
177 )
tiernob24258a2018-10-04 18:39:49 +0200178 elif len(desc_list) == 1:
179 return desc_list[0]
180 else:
garciadeblas4568a372021-03-24 09:19:48 +0100181 raise DbException(
182 "Found more than one public {} with id='{}'; and no one belonging to this project".format(
183 topic[:-1], id
184 ),
185 HTTPStatus.CONFLICT,
186 )
tiernob24258a2018-10-04 18:39:49 +0200187
tierno65ca36d2019-02-12 19:27:52 +0100188 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
tiernob24258a2018-10-04 18:39:49 +0200189 """
190 Creates a new almost empty DISABLED entry into database. Due to SOL005, it does not follow normal procedure.
191 Creating a VNFD or NSD is done in two steps: 1. Creates an empty descriptor (this step) and 2) upload content
192 (self.upload_content)
193 :param rollback: list to append created items at database in case a rollback may to be done
tierno65ca36d2019-02-12 19:27:52 +0100194 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200195 :param indata: data to be inserted
196 :param kwargs: used to override the indata descriptor
197 :param headers: http request headers
tiernobdebce92019-07-01 15:36:49 +0000198 :return: _id, None: identity of the inserted data; and None as there is not any operation
tiernob24258a2018-10-04 18:39:49 +0200199 """
200
tiernod7749582020-05-28 10:41:10 +0000201 # No needed to capture exceptions
202 # Check Quota
203 self.check_quota(session)
delacruzramo32bab472019-09-13 12:24:22 +0200204
tiernod7749582020-05-28 10:41:10 +0000205 # _remove_envelop
206 if indata:
207 if "userDefinedData" in indata:
garciadeblas4568a372021-03-24 09:19:48 +0100208 indata = indata["userDefinedData"]
tiernob24258a2018-10-04 18:39:49 +0200209
tiernod7749582020-05-28 10:41:10 +0000210 # Override descriptor with query string kwargs
211 self._update_input_with_kwargs(indata, kwargs)
212 # uncomment when this method is implemented.
213 # Avoid override in this case as the target is userDefinedData, but not vnfd,nsd descriptors
214 # indata = DescriptorTopic._validate_input_new(self, indata, project_id=session["force"])
tiernob24258a2018-10-04 18:39:49 +0200215
beierlmcee2ebf2022-03-29 17:42:48 -0400216 content = {"_admin": {
217 "userDefinedData": indata,
218 "revision": 0
219 }}
220
garciadeblas4568a372021-03-24 09:19:48 +0100221 self.format_on_new(
222 content, session["project_id"], make_public=session["public"]
223 )
tiernod7749582020-05-28 10:41:10 +0000224 _id = self.db.create(self.topic, content)
225 rollback.append({"topic": self.topic, "_id": _id})
226 self._send_msg("created", {"_id": _id})
227 return _id, None
tiernob24258a2018-10-04 18:39:49 +0200228
tierno65ca36d2019-02-12 19:27:52 +0100229 def upload_content(self, session, _id, indata, kwargs, headers):
tiernob24258a2018-10-04 18:39:49 +0200230 """
231 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 +0100232 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200233 :param _id : the nsd,vnfd is already created, this is the id
234 :param indata: http body request
235 :param kwargs: user query string to override parameters. NOT USED
236 :param headers: http request headers
tierno5a5c2182018-11-20 12:27:42 +0000237 :return: True if package is completely uploaded or False if partial content has been uploded
tiernob24258a2018-10-04 18:39:49 +0200238 Raise exception on error
239 """
240 # Check that _id exists and it is valid
241 current_desc = self.show(session, _id)
242
243 content_range_text = headers.get("Content-Range")
244 expected_md5 = headers.get("Content-File-MD5")
245 compressed = None
246 content_type = headers.get("Content-Type")
garciadeblas4568a372021-03-24 09:19:48 +0100247 if (
248 content_type
249 and "application/gzip" in content_type
250 or "application/x-gzip" in content_type
garciadeblas4568a372021-03-24 09:19:48 +0100251 ):
tiernob24258a2018-10-04 18:39:49 +0200252 compressed = "gzip"
bravofc26740a2021-11-08 09:44:54 -0300253 if (
254 content_type
255 and "application/zip" in content_type
256 ):
257 compressed = "zip"
tiernob24258a2018-10-04 18:39:49 +0200258 filename = headers.get("Content-Filename")
bravofc26740a2021-11-08 09:44:54 -0300259 if not filename and compressed:
260 filename = "package.tar.gz" if compressed == "gzip" else "package.zip"
261 elif not filename:
262 filename = "package"
263
beierlmcee2ebf2022-03-29 17:42:48 -0400264 revision = 1
265 if "revision" in current_desc["_admin"]:
266 revision = current_desc["_admin"]["revision"] + 1
267
tiernob24258a2018-10-04 18:39:49 +0200268 # TODO change to Content-Disposition filename https://tools.ietf.org/html/rfc6266
269 file_pkg = None
270 error_text = ""
beierlmbc5a5242022-05-17 21:25:29 -0400271 fs_rollback = []
272
tiernob24258a2018-10-04 18:39:49 +0200273 try:
274 if content_range_text:
garciadeblas4568a372021-03-24 09:19:48 +0100275 content_range = (
276 content_range_text.replace("-", " ").replace("/", " ").split()
277 )
278 if (
279 content_range[0] != "bytes"
280 ): # TODO check x<y not negative < total....
tiernob24258a2018-10-04 18:39:49 +0200281 raise IndexError()
282 start = int(content_range[1])
283 end = int(content_range[2]) + 1
284 total = int(content_range[3])
285 else:
286 start = 0
beierlmcee2ebf2022-03-29 17:42:48 -0400287 # Rather than using a temp folder, we will store the package in a folder based on
288 # the current revision.
289 proposed_revision_path = (
290 _id + ":" + str(revision)
garciadeblas4568a372021-03-24 09:19:48 +0100291 ) # all the content is upload here and if ok, it is rename from id_ to is folder
tiernob24258a2018-10-04 18:39:49 +0200292
293 if start:
beierlmcee2ebf2022-03-29 17:42:48 -0400294 if not self.fs.file_exists(proposed_revision_path, "dir"):
garciadeblas4568a372021-03-24 09:19:48 +0100295 raise EngineException(
296 "invalid Transaction-Id header", HTTPStatus.NOT_FOUND
297 )
tiernob24258a2018-10-04 18:39:49 +0200298 else:
beierlmcee2ebf2022-03-29 17:42:48 -0400299 self.fs.file_delete(proposed_revision_path, ignore_non_exist=True)
300 self.fs.mkdir(proposed_revision_path)
beierlmbc5a5242022-05-17 21:25:29 -0400301 fs_rollback.append(proposed_revision_path)
tiernob24258a2018-10-04 18:39:49 +0200302
303 storage = self.fs.get_params()
beierlmbc5a5242022-05-17 21:25:29 -0400304 storage["folder"] = proposed_revision_path
tiernob24258a2018-10-04 18:39:49 +0200305
beierlmcee2ebf2022-03-29 17:42:48 -0400306 file_path = (proposed_revision_path, filename)
garciadeblas4568a372021-03-24 09:19:48 +0100307 if self.fs.file_exists(file_path, "file"):
tiernob24258a2018-10-04 18:39:49 +0200308 file_size = self.fs.file_size(file_path)
309 else:
310 file_size = 0
311 if file_size != start:
garciadeblas4568a372021-03-24 09:19:48 +0100312 raise EngineException(
313 "invalid Content-Range start sequence, expected '{}' but received '{}'".format(
314 file_size, start
315 ),
316 HTTPStatus.REQUESTED_RANGE_NOT_SATISFIABLE,
317 )
318 file_pkg = self.fs.file_open(file_path, "a+b")
tiernob24258a2018-10-04 18:39:49 +0200319 if isinstance(indata, dict):
320 indata_text = yaml.safe_dump(indata, indent=4, default_flow_style=False)
321 file_pkg.write(indata_text.encode(encoding="utf-8"))
322 else:
323 indata_len = 0
324 while True:
325 indata_text = indata.read(4096)
326 indata_len += len(indata_text)
327 if not indata_text:
328 break
329 file_pkg.write(indata_text)
330 if content_range_text:
garciaale960531a2020-10-20 18:29:45 -0300331 if indata_len != end - start:
garciadeblas4568a372021-03-24 09:19:48 +0100332 raise EngineException(
333 "Mismatch between Content-Range header {}-{} and body length of {}".format(
334 start, end - 1, indata_len
335 ),
336 HTTPStatus.REQUESTED_RANGE_NOT_SATISFIABLE,
337 )
tiernob24258a2018-10-04 18:39:49 +0200338 if end != total:
339 # TODO update to UPLOADING
340 return False
341
342 # PACKAGE UPLOADED
343 if expected_md5:
344 file_pkg.seek(0, 0)
345 file_md5 = md5()
346 chunk_data = file_pkg.read(1024)
347 while chunk_data:
348 file_md5.update(chunk_data)
349 chunk_data = file_pkg.read(1024)
350 if expected_md5 != file_md5.hexdigest():
351 raise EngineException("Error, MD5 mismatch", HTTPStatus.CONFLICT)
352 file_pkg.seek(0, 0)
353 if compressed == "gzip":
garciadeblas4568a372021-03-24 09:19:48 +0100354 tar = tarfile.open(mode="r", fileobj=file_pkg)
tiernob24258a2018-10-04 18:39:49 +0200355 descriptor_file_name = None
356 for tarinfo in tar:
357 tarname = tarinfo.name
358 tarname_path = tarname.split("/")
garciadeblas4568a372021-03-24 09:19:48 +0100359 if (
360 not tarname_path[0] or ".." in tarname_path
361 ): # if start with "/" means absolute path
362 raise EngineException(
363 "Absolute path or '..' are not allowed for package descriptor tar.gz"
364 )
tiernob24258a2018-10-04 18:39:49 +0200365 if len(tarname_path) == 1 and not tarinfo.isdir():
garciadeblas4568a372021-03-24 09:19:48 +0100366 raise EngineException(
367 "All files must be inside a dir for package descriptor tar.gz"
368 )
369 if (
370 tarname.endswith(".yaml")
371 or tarname.endswith(".json")
372 or tarname.endswith(".yml")
373 ):
tiernob24258a2018-10-04 18:39:49 +0200374 storage["pkg-dir"] = tarname_path[0]
375 if len(tarname_path) == 2:
376 if descriptor_file_name:
377 raise EngineException(
garciadeblas4568a372021-03-24 09:19:48 +0100378 "Found more than one descriptor file at package descriptor tar.gz"
379 )
tiernob24258a2018-10-04 18:39:49 +0200380 descriptor_file_name = tarname
381 if not descriptor_file_name:
garciadeblas4568a372021-03-24 09:19:48 +0100382 raise EngineException(
383 "Not found any descriptor file at package descriptor tar.gz"
384 )
tiernob24258a2018-10-04 18:39:49 +0200385 storage["descriptor"] = descriptor_file_name
386 storage["zipfile"] = filename
beierlmcee2ebf2022-03-29 17:42:48 -0400387 self.fs.file_extract(tar, proposed_revision_path)
garciadeblas4568a372021-03-24 09:19:48 +0100388 with self.fs.file_open(
beierlmcee2ebf2022-03-29 17:42:48 -0400389 (proposed_revision_path, descriptor_file_name), "r"
garciadeblas4568a372021-03-24 09:19:48 +0100390 ) as descriptor_file:
tiernob24258a2018-10-04 18:39:49 +0200391 content = descriptor_file.read()
bravofc26740a2021-11-08 09:44:54 -0300392 elif compressed == "zip":
393 zipfile = ZipFile(file_pkg)
394 descriptor_file_name = None
395 for package_file in zipfile.infolist():
396 zipfilename = package_file.filename
397 file_path = zipfilename.split("/")
398 if (
399 not file_path[0] or ".." in zipfilename
400 ): # if start with "/" means absolute path
401 raise EngineException(
402 "Absolute path or '..' are not allowed for package descriptor zip"
403 )
404
405 if (
406 (
407 zipfilename.endswith(".yaml")
408 or zipfilename.endswith(".json")
409 or zipfilename.endswith(".yml")
410 ) and (
411 zipfilename.find("/") < 0
412 or zipfilename.find("Definitions") >= 0
413 )
414 ):
415 storage["pkg-dir"] = ""
416 if descriptor_file_name:
417 raise EngineException(
418 "Found more than one descriptor file at package descriptor zip"
419 )
420 descriptor_file_name = zipfilename
421 if not descriptor_file_name:
422 raise EngineException(
423 "Not found any descriptor file at package descriptor zip"
424 )
425 storage["descriptor"] = descriptor_file_name
426 storage["zipfile"] = filename
beierlmcee2ebf2022-03-29 17:42:48 -0400427 self.fs.file_extract(zipfile, proposed_revision_path)
bravofc26740a2021-11-08 09:44:54 -0300428
429 with self.fs.file_open(
beierlmcee2ebf2022-03-29 17:42:48 -0400430 (proposed_revision_path, descriptor_file_name), "r"
bravofc26740a2021-11-08 09:44:54 -0300431 ) as descriptor_file:
432 content = descriptor_file.read()
tiernob24258a2018-10-04 18:39:49 +0200433 else:
434 content = file_pkg.read()
435 storage["descriptor"] = descriptor_file_name = filename
436
437 if descriptor_file_name.endswith(".json"):
438 error_text = "Invalid json format "
439 indata = json.load(content)
440 else:
441 error_text = "Invalid yaml format "
delacruzramob19cadc2019-10-08 10:18:02 +0200442 indata = yaml.load(content, Loader=yaml.SafeLoader)
tiernob24258a2018-10-04 18:39:49 +0200443
beierlmcee2ebf2022-03-29 17:42:48 -0400444 # Need to close the file package here so it can be copied from the
445 # revision to the current, unrevisioned record
446 if file_pkg:
447 file_pkg.close()
448 file_pkg = None
449
450 # Fetch both the incoming, proposed revision and the original revision so we
451 # can call a validate method to compare them
452 current_revision_path = _id + "/"
453 self.fs.sync(from_path=current_revision_path)
454 self.fs.sync(from_path=proposed_revision_path)
455
456 if revision > 1:
457 try:
458 self._validate_descriptor_changes(
459 descriptor_file_name,
460 current_revision_path,
461 proposed_revision_path)
462 except Exception as e:
463 shutil.rmtree(self.fs.path + current_revision_path, ignore_errors=True)
464 shutil.rmtree(self.fs.path + proposed_revision_path, ignore_errors=True)
465 # Only delete the new revision. We need to keep the original version in place
466 # as it has not been changed.
467 self.fs.file_delete(proposed_revision_path, ignore_non_exist=True)
468 raise e
469
tiernob24258a2018-10-04 18:39:49 +0200470
471 indata = self._remove_envelop(indata)
472
473 # Override descriptor with query string kwargs
474 if kwargs:
475 self._update_input_with_kwargs(indata, kwargs)
tiernob24258a2018-10-04 18:39:49 +0200476
beierlmbc5a5242022-05-17 21:25:29 -0400477 current_desc["_admin"]["storage"] = storage
478 current_desc["_admin"]["onboardingState"] = "ONBOARDED"
479 current_desc["_admin"]["operationalState"] = "ENABLED"
480 current_desc["_admin"]["modified"] = time()
481 current_desc["_admin"]["revision"] = revision
482
tiernob24258a2018-10-04 18:39:49 +0200483 deep_update_rfc7396(current_desc, indata)
garciadeblas4568a372021-03-24 09:19:48 +0100484 current_desc = self.check_conflict_on_edit(
485 session, current_desc, indata, _id=_id
486 )
beierlmbc5a5242022-05-17 21:25:29 -0400487
488 # Copy the revision to the active package name by its original id
489 shutil.rmtree(self.fs.path + current_revision_path, ignore_errors=True)
490 os.rename(self.fs.path + proposed_revision_path, self.fs.path + current_revision_path)
491 self.fs.file_delete(current_revision_path, ignore_non_exist=True)
492 self.fs.mkdir(current_revision_path)
493 self.fs.reverse_sync(from_path=current_revision_path)
494
495 shutil.rmtree(self.fs.path + _id)
496
tiernob24258a2018-10-04 18:39:49 +0200497 self.db.replace(self.topic, _id, current_desc)
beierlmcee2ebf2022-03-29 17:42:48 -0400498
499 # Store a copy of the package as a point in time revision
500 revision_desc = dict(current_desc)
501 revision_desc["_id"] = _id + ":" + str(revision_desc["_admin"]["revision"])
502 self.db.create(self.topic + "_revisions", revision_desc)
beierlmbc5a5242022-05-17 21:25:29 -0400503 fs_rollback = []
tiernob24258a2018-10-04 18:39:49 +0200504
505 indata["_id"] = _id
K Sai Kiranc96fd692019-10-16 17:50:53 +0530506 self._send_msg("edited", indata)
tiernob24258a2018-10-04 18:39:49 +0200507
508 # TODO if descriptor has changed because kwargs update content and remove cached zip
509 # TODO if zip is not present creates one
510 return True
511
512 except EngineException:
513 raise
514 except IndexError:
garciadeblas4568a372021-03-24 09:19:48 +0100515 raise EngineException(
516 "invalid Content-Range header format. Expected 'bytes start-end/total'",
517 HTTPStatus.REQUESTED_RANGE_NOT_SATISFIABLE,
518 )
tiernob24258a2018-10-04 18:39:49 +0200519 except IOError as e:
garciadeblas4568a372021-03-24 09:19:48 +0100520 raise EngineException(
521 "invalid upload transaction sequence: '{}'".format(e),
522 HTTPStatus.BAD_REQUEST,
523 )
tiernob24258a2018-10-04 18:39:49 +0200524 except tarfile.ReadError as e:
garciadeblas4568a372021-03-24 09:19:48 +0100525 raise EngineException(
526 "invalid file content {}".format(e), HTTPStatus.BAD_REQUEST
527 )
tiernob24258a2018-10-04 18:39:49 +0200528 except (ValueError, yaml.YAMLError) as e:
529 raise EngineException(error_text + str(e))
530 except ValidationError as e:
531 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
532 finally:
533 if file_pkg:
534 file_pkg.close()
beierlmbc5a5242022-05-17 21:25:29 -0400535 for file in fs_rollback:
536 self.fs.file_delete(file, ignore_non_exist=True)
tiernob24258a2018-10-04 18:39:49 +0200537
538 def get_file(self, session, _id, path=None, accept_header=None):
539 """
540 Return the file content of a vnfd or nsd
tierno65ca36d2019-02-12 19:27:52 +0100541 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tierno87006042018-10-24 12:50:20 +0200542 :param _id: Identity of the vnfd, nsd
tiernob24258a2018-10-04 18:39:49 +0200543 :param path: artifact path or "$DESCRIPTOR" or None
544 :param accept_header: Content of Accept header. Must contain applition/zip or/and text/plain
tierno87006042018-10-24 12:50:20 +0200545 :return: opened file plus Accept format or raises an exception
tiernob24258a2018-10-04 18:39:49 +0200546 """
547 accept_text = accept_zip = False
548 if accept_header:
garciadeblas4568a372021-03-24 09:19:48 +0100549 if "text/plain" in accept_header or "*/*" in accept_header:
tiernob24258a2018-10-04 18:39:49 +0200550 accept_text = True
garciadeblas4568a372021-03-24 09:19:48 +0100551 if "application/zip" in accept_header or "*/*" in accept_header:
552 accept_zip = "application/zip"
553 elif "application/gzip" in accept_header:
554 accept_zip = "application/gzip"
tierno87006042018-10-24 12:50:20 +0200555
tiernob24258a2018-10-04 18:39:49 +0200556 if not accept_text and not accept_zip:
garciadeblas4568a372021-03-24 09:19:48 +0100557 raise EngineException(
558 "provide request header 'Accept' with 'application/zip' or 'text/plain'",
559 http_code=HTTPStatus.NOT_ACCEPTABLE,
560 )
tiernob24258a2018-10-04 18:39:49 +0200561
562 content = self.show(session, _id)
563 if content["_admin"]["onboardingState"] != "ONBOARDED":
garciadeblas4568a372021-03-24 09:19:48 +0100564 raise EngineException(
565 "Cannot get content because this resource is not at 'ONBOARDED' state. "
566 "onboardingState is {}".format(content["_admin"]["onboardingState"]),
567 http_code=HTTPStatus.CONFLICT,
568 )
tiernob24258a2018-10-04 18:39:49 +0200569 storage = content["_admin"]["storage"]
garciaale960531a2020-10-20 18:29:45 -0300570 if path is not None and path != "$DESCRIPTOR": # artifacts
selvi.jba5bbda2022-08-25 06:24:49 +0000571 if not storage.get("pkg-dir") and not storage.get("folder"):
garciadeblas4568a372021-03-24 09:19:48 +0100572 raise EngineException(
573 "Packages does not contains artifacts",
574 http_code=HTTPStatus.BAD_REQUEST,
575 )
576 if self.fs.file_exists(
577 (storage["folder"], storage["pkg-dir"], *path), "dir"
578 ):
579 folder_content = self.fs.dir_ls(
580 (storage["folder"], storage["pkg-dir"], *path)
581 )
tiernob24258a2018-10-04 18:39:49 +0200582 return folder_content, "text/plain"
583 # TODO manage folders in http
584 else:
garciadeblas4568a372021-03-24 09:19:48 +0100585 return (
586 self.fs.file_open(
587 (storage["folder"], storage["pkg-dir"], *path), "rb"
588 ),
589 "application/octet-stream",
590 )
tiernob24258a2018-10-04 18:39:49 +0200591
592 # pkgtype accept ZIP TEXT -> result
593 # manyfiles yes X -> zip
594 # no yes -> error
595 # onefile yes no -> zip
596 # X yes -> text
tiernoee002752020-08-04 14:14:16 +0000597 contain_many_files = False
garciadeblas4568a372021-03-24 09:19:48 +0100598 if storage.get("pkg-dir"):
tiernoee002752020-08-04 14:14:16 +0000599 # check if there are more than one file in the package, ignoring checksums.txt.
garciadeblas4568a372021-03-24 09:19:48 +0100600 pkg_files = self.fs.dir_ls((storage["folder"], storage["pkg-dir"]))
601 if len(pkg_files) >= 3 or (
602 len(pkg_files) == 2 and "checksums.txt" not in pkg_files
603 ):
tiernoee002752020-08-04 14:14:16 +0000604 contain_many_files = True
605 if accept_text and (not contain_many_files or path == "$DESCRIPTOR"):
garciadeblas4568a372021-03-24 09:19:48 +0100606 return (
607 self.fs.file_open((storage["folder"], storage["descriptor"]), "r"),
608 "text/plain",
609 )
tiernoee002752020-08-04 14:14:16 +0000610 elif contain_many_files and not accept_zip:
garciadeblas4568a372021-03-24 09:19:48 +0100611 raise EngineException(
612 "Packages that contains several files need to be retrieved with 'application/zip'"
613 "Accept header",
614 http_code=HTTPStatus.NOT_ACCEPTABLE,
615 )
tiernob24258a2018-10-04 18:39:49 +0200616 else:
garciadeblas4568a372021-03-24 09:19:48 +0100617 if not storage.get("zipfile"):
tiernob24258a2018-10-04 18:39:49 +0200618 # TODO generate zipfile if not present
garciadeblas4568a372021-03-24 09:19:48 +0100619 raise EngineException(
620 "Only allowed 'text/plain' Accept header for this descriptor. To be solved in "
621 "future versions",
622 http_code=HTTPStatus.NOT_ACCEPTABLE,
623 )
624 return (
625 self.fs.file_open((storage["folder"], storage["zipfile"]), "rb"),
626 accept_zip,
627 )
tiernob24258a2018-10-04 18:39:49 +0200628
garciaale7cbd03c2020-11-27 10:38:35 -0300629 def _remove_yang_prefixes_from_descriptor(self, descriptor):
630 new_descriptor = {}
631 for k, v in descriptor.items():
632 new_v = v
633 if isinstance(v, dict):
634 new_v = self._remove_yang_prefixes_from_descriptor(v)
635 elif isinstance(v, list):
636 new_v = list()
637 for x in v:
638 if isinstance(x, dict):
639 new_v.append(self._remove_yang_prefixes_from_descriptor(x))
640 else:
641 new_v.append(x)
garciadeblas4568a372021-03-24 09:19:48 +0100642 new_descriptor[k.split(":")[-1]] = new_v
garciaale7cbd03c2020-11-27 10:38:35 -0300643 return new_descriptor
644
gcalvino46e4cb82018-10-26 13:10:22 +0200645 def pyangbind_validation(self, item, data, force=False):
garciadeblas4568a372021-03-24 09:19:48 +0100646 raise EngineException(
647 "Not possible to validate '{}' item".format(item),
648 http_code=HTTPStatus.INTERNAL_SERVER_ERROR,
649 )
gcalvino46e4cb82018-10-26 13:10:22 +0200650
Frank Brydendeba68e2020-07-27 13:55:11 +0000651 def _validate_input_edit(self, indata, content, force=False):
652 # not needed to validate with pyangbind becuase it will be validated at check_conflict_on_edit
653 if "_id" in indata:
654 indata.pop("_id")
655 if "_admin" not in indata:
656 indata["_admin"] = {}
657
658 if "operationalState" in indata:
659 if indata["operationalState"] in ("ENABLED", "DISABLED"):
660 indata["_admin"]["operationalState"] = indata.pop("operationalState")
661 else:
garciadeblas4568a372021-03-24 09:19:48 +0100662 raise EngineException(
663 "State '{}' is not a valid operational state".format(
664 indata["operationalState"]
665 ),
666 http_code=HTTPStatus.BAD_REQUEST,
667 )
Frank Brydendeba68e2020-07-27 13:55:11 +0000668
garciadeblas4568a372021-03-24 09:19:48 +0100669 # 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 +0000670 # to preserve current expected behaviour
671 if "userDefinedData" in indata:
672 data = indata.pop("userDefinedData")
gatici3e9bd122023-07-31 14:37:32 +0300673 if isinstance(data, dict):
Frank Brydendeba68e2020-07-27 13:55:11 +0000674 indata["_admin"]["userDefinedData"] = data
675 else:
garciadeblas4568a372021-03-24 09:19:48 +0100676 raise EngineException(
677 "userDefinedData should be an object, but is '{}' instead".format(
678 type(data)
679 ),
680 http_code=HTTPStatus.BAD_REQUEST,
681 )
garciaale960531a2020-10-20 18:29:45 -0300682
garciadeblas4568a372021-03-24 09:19:48 +0100683 if (
684 "operationalState" in indata["_admin"]
685 and content["_admin"]["operationalState"]
686 == indata["_admin"]["operationalState"]
687 ):
688 raise EngineException(
689 "operationalState already {}".format(
690 content["_admin"]["operationalState"]
691 ),
692 http_code=HTTPStatus.CONFLICT,
693 )
Frank Brydendeba68e2020-07-27 13:55:11 +0000694
695 return indata
696
beierlmcee2ebf2022-03-29 17:42:48 -0400697 def _validate_descriptor_changes(self,
698 descriptor_file_name,
699 old_descriptor_directory,
700 new_descriptor_directory):
701 # Todo: compare changes and throw a meaningful exception for the user to understand
702 # Example:
703 # raise EngineException(
704 # "Error in validating new descriptor: <NODE> cannot be modified",
705 # http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
706 # )
707 pass
tiernob24258a2018-10-04 18:39:49 +0200708
709class VnfdTopic(DescriptorTopic):
710 topic = "vnfds"
711 topic_msg = "vnfd"
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
garciaale7cbd03c2020-11-27 10:38:35 -0300716 def pyangbind_validation(self, item, data, force=False):
garciaaledf718ae2020-12-03 19:17:28 -0300717 if self._descriptor_data_is_in_old_format(data):
garciadeblas4568a372021-03-24 09:19:48 +0100718 raise EngineException(
719 "ERROR: Unsupported descriptor format. Please, use an ETSI SOL006 descriptor.",
720 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
721 )
garciaale7cbd03c2020-11-27 10:38:35 -0300722 try:
garciaale7cbd03c2020-11-27 10:38:35 -0300723 myvnfd = etsi_nfv_vnfd.etsi_nfv_vnfd()
garciadeblas4568a372021-03-24 09:19:48 +0100724 pybindJSONDecoder.load_ietf_json(
725 {"etsi-nfv-vnfd:vnfd": data},
726 None,
727 None,
728 obj=myvnfd,
729 path_helper=True,
730 skip_unknown=force,
731 )
garciaale7cbd03c2020-11-27 10:38:35 -0300732 out = pybindJSON.dumps(myvnfd, mode="ietf")
733 desc_out = self._remove_envelop(yaml.safe_load(out))
734 desc_out = self._remove_yang_prefixes_from_descriptor(desc_out)
bravof41a52052021-02-17 18:08:01 -0300735 return utils.deep_update_dict(data, desc_out)
garciaale7cbd03c2020-11-27 10:38:35 -0300736 except Exception as e:
garciadeblas4568a372021-03-24 09:19:48 +0100737 raise EngineException(
738 "Error in pyangbind validation: {}".format(str(e)),
739 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
740 )
garciaale7cbd03c2020-11-27 10:38:35 -0300741
tiernob24258a2018-10-04 18:39:49 +0200742 @staticmethod
garciaaledf718ae2020-12-03 19:17:28 -0300743 def _descriptor_data_is_in_old_format(data):
garciadeblas4568a372021-03-24 09:19:48 +0100744 return ("vnfd-catalog" in data) or ("vnfd:vnfd-catalog" in data)
garciaaledf718ae2020-12-03 19:17:28 -0300745
746 @staticmethod
tiernob24258a2018-10-04 18:39:49 +0200747 def _remove_envelop(indata=None):
748 if not indata:
749 return {}
750 clean_indata = indata
garciaale7cbd03c2020-11-27 10:38:35 -0300751
garciadeblas4568a372021-03-24 09:19:48 +0100752 if clean_indata.get("etsi-nfv-vnfd:vnfd"):
753 if not isinstance(clean_indata["etsi-nfv-vnfd:vnfd"], dict):
garciaale7cbd03c2020-11-27 10:38:35 -0300754 raise EngineException("'etsi-nfv-vnfd:vnfd' must be a dict")
garciadeblas4568a372021-03-24 09:19:48 +0100755 clean_indata = clean_indata["etsi-nfv-vnfd:vnfd"]
756 elif clean_indata.get("vnfd"):
757 if not isinstance(clean_indata["vnfd"], dict):
garciaale7cbd03c2020-11-27 10:38:35 -0300758 raise EngineException("'vnfd' must be dict")
garciadeblas4568a372021-03-24 09:19:48 +0100759 clean_indata = clean_indata["vnfd"]
garciaale7cbd03c2020-11-27 10:38:35 -0300760
tiernob24258a2018-10-04 18:39:49 +0200761 return clean_indata
762
tierno65ca36d2019-02-12 19:27:52 +0100763 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
garciadeblas4568a372021-03-24 09:19:48 +0100764 final_content = super().check_conflict_on_edit(
765 session, final_content, edit_content, _id
766 )
tierno36ec8602018-11-02 17:27:11 +0100767
768 # set type of vnfd
769 contains_pdu = False
770 contains_vdu = False
771 for vdu in get_iterable(final_content.get("vdu")):
772 if vdu.get("pdu-type"):
773 contains_pdu = True
774 else:
775 contains_vdu = True
776 if contains_pdu:
777 final_content["_admin"]["type"] = "hnfd" if contains_vdu else "pnfd"
778 elif contains_vdu:
779 final_content["_admin"]["type"] = "vnfd"
780 # if neither vud nor pdu do not fill type
bravofb995ea22021-02-10 10:57:52 -0300781 return final_content
tierno36ec8602018-11-02 17:27:11 +0100782
tiernob4844ab2019-05-23 08:42:12 +0000783 def check_conflict_on_del(self, session, _id, db_content):
tiernob24258a2018-10-04 18:39:49 +0200784 """
785 Check that there is not any NSD that uses this VNFD. Only NSDs belonging to this project are considered. Note
786 that VNFD can be public and be used by NSD of other projects. Also check there are not deployments, or vnfr
787 that uses this vnfd
tierno65ca36d2019-02-12 19:27:52 +0100788 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob4844ab2019-05-23 08:42:12 +0000789 :param _id: vnfd internal id
790 :param db_content: The database content of the _id.
tiernob24258a2018-10-04 18:39:49 +0200791 :return: None or raises EngineException with the conflict
792 """
tierno65ca36d2019-02-12 19:27:52 +0100793 if session["force"]:
tiernob24258a2018-10-04 18:39:49 +0200794 return
tiernob4844ab2019-05-23 08:42:12 +0000795 descriptor = db_content
tiernob24258a2018-10-04 18:39:49 +0200796 descriptor_id = descriptor.get("id")
797 if not descriptor_id: # empty vnfd not uploaded
798 return
799
tierno65ca36d2019-02-12 19:27:52 +0100800 _filter = self._get_project_filter(session)
tiernob4844ab2019-05-23 08:42:12 +0000801
tiernob24258a2018-10-04 18:39:49 +0200802 # check vnfrs using this vnfd
803 _filter["vnfd-id"] = _id
804 if self.db.get_list("vnfrs", _filter):
garciadeblas4568a372021-03-24 09:19:48 +0100805 raise EngineException(
806 "There is at least one VNF instance using this descriptor",
807 http_code=HTTPStatus.CONFLICT,
808 )
tiernob4844ab2019-05-23 08:42:12 +0000809
810 # check NSD referencing this VNFD
tiernob24258a2018-10-04 18:39:49 +0200811 del _filter["vnfd-id"]
garciadeblasf576eb92021-04-18 20:54:13 +0000812 _filter["vnfd-id"] = descriptor_id
tiernob24258a2018-10-04 18:39:49 +0200813 if self.db.get_list("nsds", _filter):
garciadeblas4568a372021-03-24 09:19:48 +0100814 raise EngineException(
815 "There is at least one NS package referencing this descriptor",
816 http_code=HTTPStatus.CONFLICT,
817 )
tiernob24258a2018-10-04 18:39:49 +0200818
gcalvinoa6fe0002019-01-09 13:27:11 +0100819 def _validate_input_new(self, indata, storage_params, force=False):
Frank Bryden19b97522020-07-10 12:32:02 +0000820 indata.pop("onboardingState", None)
821 indata.pop("operationalState", None)
822 indata.pop("usageState", None)
Frank Bryden19b97522020-07-10 12:32:02 +0000823 indata.pop("links", None)
824
gcalvino46e4cb82018-10-26 13:10:22 +0200825 indata = self.pyangbind_validation("vnfds", indata, force)
gcalvino5e72d152018-10-23 11:46:57 +0200826 # Cross references validation in the descriptor
garciaale7cbd03c2020-11-27 10:38:35 -0300827
828 self.validate_mgmt_interface_connection_point(indata)
gcalvino5e72d152018-10-23 11:46:57 +0200829
830 for vdu in get_iterable(indata.get("vdu")):
garciaale7cbd03c2020-11-27 10:38:35 -0300831 self.validate_vdu_internal_connection_points(vdu)
garciaale960531a2020-10-20 18:29:45 -0300832 self._validate_vdu_cloud_init_in_package(storage_params, vdu, indata)
bravof41a52052021-02-17 18:08:01 -0300833 self._validate_vdu_charms_in_package(storage_params, indata)
garciaale960531a2020-10-20 18:29:45 -0300834
835 self._validate_vnf_charms_in_package(storage_params, indata)
836
garciaale7cbd03c2020-11-27 10:38:35 -0300837 self.validate_external_connection_points(indata)
838 self.validate_internal_virtual_links(indata)
garciaale960531a2020-10-20 18:29:45 -0300839 self.validate_monitoring_params(indata)
840 self.validate_scaling_group_descriptor(indata)
841
842 return indata
843
844 @staticmethod
garciaale7cbd03c2020-11-27 10:38:35 -0300845 def validate_mgmt_interface_connection_point(indata):
garciaale960531a2020-10-20 18:29:45 -0300846 if not indata.get("vdu"):
847 return
garciaale7cbd03c2020-11-27 10:38:35 -0300848 if not indata.get("mgmt-cp"):
garciadeblas4568a372021-03-24 09:19:48 +0100849 raise EngineException(
850 "'mgmt-cp' is a mandatory field and it is not defined",
851 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
852 )
garciaale7cbd03c2020-11-27 10:38:35 -0300853
854 for cp in get_iterable(indata.get("ext-cpd")):
855 if cp["id"] == indata["mgmt-cp"]:
856 break
857 else:
garciadeblas4568a372021-03-24 09:19:48 +0100858 raise EngineException(
859 "mgmt-cp='{}' must match an existing ext-cpd".format(indata["mgmt-cp"]),
860 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
861 )
garciaale960531a2020-10-20 18:29:45 -0300862
863 @staticmethod
garciaale7cbd03c2020-11-27 10:38:35 -0300864 def validate_vdu_internal_connection_points(vdu):
865 int_cpds = set()
866 for cpd in get_iterable(vdu.get("int-cpd")):
867 cpd_id = cpd.get("id")
868 if cpd_id and cpd_id in int_cpds:
garciadeblas4568a372021-03-24 09:19:48 +0100869 raise EngineException(
870 "vdu[id='{}']:int-cpd[id='{}'] is already used by other int-cpd".format(
871 vdu["id"], cpd_id
872 ),
873 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
874 )
garciaale7cbd03c2020-11-27 10:38:35 -0300875 int_cpds.add(cpd_id)
876
877 @staticmethod
878 def validate_external_connection_points(indata):
879 all_vdus_int_cpds = set()
880 for vdu in get_iterable(indata.get("vdu")):
881 for int_cpd in get_iterable(vdu.get("int-cpd")):
882 all_vdus_int_cpds.add((vdu.get("id"), int_cpd.get("id")))
883
884 ext_cpds = set()
885 for cpd in get_iterable(indata.get("ext-cpd")):
886 cpd_id = cpd.get("id")
887 if cpd_id and cpd_id in ext_cpds:
garciadeblas4568a372021-03-24 09:19:48 +0100888 raise EngineException(
889 "ext-cpd[id='{}'] is already used by other ext-cpd".format(cpd_id),
890 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
891 )
garciaale7cbd03c2020-11-27 10:38:35 -0300892 ext_cpds.add(cpd_id)
893
894 int_cpd = cpd.get("int-cpd")
895 if int_cpd:
896 if (int_cpd.get("vdu-id"), int_cpd.get("cpd")) not in all_vdus_int_cpds:
garciadeblas4568a372021-03-24 09:19:48 +0100897 raise EngineException(
898 "ext-cpd[id='{}']:int-cpd must match an existing vdu int-cpd".format(
899 cpd_id
900 ),
901 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
902 )
garciaale7cbd03c2020-11-27 10:38:35 -0300903 # TODO: Validate k8s-cluster-net points to a valid k8s-cluster:nets ?
garciaale960531a2020-10-20 18:29:45 -0300904
bravof41a52052021-02-17 18:08:01 -0300905 def _validate_vdu_charms_in_package(self, storage_params, indata):
906 for df in indata["df"]:
garciadeblas4568a372021-03-24 09:19:48 +0100907 if (
908 "lcm-operations-configuration" in df
909 and "operate-vnf-op-config" in df["lcm-operations-configuration"]
910 ):
911 configs = df["lcm-operations-configuration"][
912 "operate-vnf-op-config"
913 ].get("day1-2", [])
garciaale2c4f9ec2021-03-01 11:04:50 -0300914 vdus = df.get("vdu-profile", [])
bravof23258282021-02-22 18:04:40 -0300915 for vdu in vdus:
916 for config in configs:
917 if config["id"] == vdu["id"] and utils.find_in_list(
918 config.get("execution-environment-list", []),
garciadeblas4568a372021-03-24 09:19:48 +0100919 lambda ee: "juju" in ee,
bravof23258282021-02-22 18:04:40 -0300920 ):
garciadeblas4568a372021-03-24 09:19:48 +0100921 if not self._validate_package_folders(
922 storage_params, "charms"
bravofc26740a2021-11-08 09:44:54 -0300923 ) and not self._validate_package_folders(
924 storage_params, "Scripts/charms"
garciadeblas4568a372021-03-24 09:19:48 +0100925 ):
926 raise EngineException(
927 "Charm defined in vnf[id={}] but not present in "
928 "package".format(indata["id"])
929 )
garciaale960531a2020-10-20 18:29:45 -0300930
931 def _validate_vdu_cloud_init_in_package(self, storage_params, vdu, indata):
932 if not vdu.get("cloud-init-file"):
933 return
garciadeblas4568a372021-03-24 09:19:48 +0100934 if not self._validate_package_folders(
935 storage_params, "cloud_init", vdu["cloud-init-file"]
bravofc26740a2021-11-08 09:44:54 -0300936 ) and not self._validate_package_folders(
937 storage_params, "Scripts/cloud_init", vdu["cloud-init-file"]
garciadeblas4568a372021-03-24 09:19:48 +0100938 ):
939 raise EngineException(
940 "Cloud-init defined in vnf[id={}]:vdu[id={}] but not present in "
941 "package".format(indata["id"], vdu["id"])
942 )
garciaale960531a2020-10-20 18:29:45 -0300943
944 def _validate_vnf_charms_in_package(self, storage_params, indata):
bravof41a52052021-02-17 18:08:01 -0300945 # Get VNF configuration through new container
garciadeblas4568a372021-03-24 09:19:48 +0100946 for deployment_flavor in indata.get("df", []):
bravof41a52052021-02-17 18:08:01 -0300947 if "lcm-operations-configuration" not in deployment_flavor:
948 return
garciadeblas4568a372021-03-24 09:19:48 +0100949 if (
950 "operate-vnf-op-config"
951 not in deployment_flavor["lcm-operations-configuration"]
952 ):
bravof41a52052021-02-17 18:08:01 -0300953 return
garciadeblas4568a372021-03-24 09:19:48 +0100954 for day_1_2_config in deployment_flavor["lcm-operations-configuration"][
955 "operate-vnf-op-config"
956 ]["day1-2"]:
bravof41a52052021-02-17 18:08:01 -0300957 if day_1_2_config["id"] == indata["id"]:
bravof23258282021-02-22 18:04:40 -0300958 if utils.find_in_list(
959 day_1_2_config.get("execution-environment-list", []),
garciadeblas4568a372021-03-24 09:19:48 +0100960 lambda ee: "juju" in ee,
bravof23258282021-02-22 18:04:40 -0300961 ):
bravofc26740a2021-11-08 09:44:54 -0300962 if not self._validate_package_folders(
963 storage_params, "charms"
964 ) and not self._validate_package_folders(
965 storage_params, "Scripts/charms"
966 ):
garciadeblas4568a372021-03-24 09:19:48 +0100967 raise EngineException(
968 "Charm defined in vnf[id={}] but not present in "
969 "package".format(indata["id"])
970 )
garciaale960531a2020-10-20 18:29:45 -0300971
972 def _validate_package_folders(self, storage_params, folder, file=None):
bravofc26740a2021-11-08 09:44:54 -0300973 if not storage_params:
974 return False
975 elif not storage_params.get("pkg-dir"):
976 if self.fs.file_exists("{}_".format(storage_params["folder"]), "dir"):
977 f = "{}_/{}".format(
978 storage_params["folder"], folder
979 )
980 else:
981 f = "{}/{}".format(
982 storage_params["folder"], folder
983 )
984 if file:
985 return self.fs.file_exists("{}/{}".format(f, file), "file")
986 else:
bravofc26740a2021-11-08 09:44:54 -0300987 if self.fs.file_exists(f, "dir"):
988 if self.fs.dir_ls(f):
989 return True
garciaale960531a2020-10-20 18:29:45 -0300990 return False
991 else:
garciadeblas4568a372021-03-24 09:19:48 +0100992 if self.fs.file_exists("{}_".format(storage_params["folder"]), "dir"):
993 f = "{}_/{}/{}".format(
994 storage_params["folder"], storage_params["pkg-dir"], folder
995 )
garciaale960531a2020-10-20 18:29:45 -0300996 else:
garciadeblas4568a372021-03-24 09:19:48 +0100997 f = "{}/{}/{}".format(
998 storage_params["folder"], storage_params["pkg-dir"], folder
999 )
garciaale960531a2020-10-20 18:29:45 -03001000 if file:
garciadeblas4568a372021-03-24 09:19:48 +01001001 return self.fs.file_exists("{}/{}".format(f, file), "file")
garciaale960531a2020-10-20 18:29:45 -03001002 else:
garciadeblas4568a372021-03-24 09:19:48 +01001003 if self.fs.file_exists(f, "dir"):
garciaale960531a2020-10-20 18:29:45 -03001004 if self.fs.dir_ls(f):
1005 return True
1006 return False
1007
1008 @staticmethod
garciaale7cbd03c2020-11-27 10:38:35 -03001009 def validate_internal_virtual_links(indata):
1010 all_ivld_ids = set()
1011 for ivld in get_iterable(indata.get("int-virtual-link-desc")):
1012 ivld_id = ivld.get("id")
1013 if ivld_id and ivld_id in all_ivld_ids:
garciadeblas4568a372021-03-24 09:19:48 +01001014 raise EngineException(
1015 "Duplicated VLD id in int-virtual-link-desc[id={}]".format(ivld_id),
1016 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1017 )
garciaale960531a2020-10-20 18:29:45 -03001018 else:
garciaale7cbd03c2020-11-27 10:38:35 -03001019 all_ivld_ids.add(ivld_id)
garciaale960531a2020-10-20 18:29:45 -03001020
garciaale7cbd03c2020-11-27 10:38:35 -03001021 for vdu in get_iterable(indata.get("vdu")):
1022 for int_cpd in get_iterable(vdu.get("int-cpd")):
1023 int_cpd_ivld_id = int_cpd.get("int-virtual-link-desc")
1024 if int_cpd_ivld_id and int_cpd_ivld_id not in all_ivld_ids:
1025 raise EngineException(
1026 "vdu[id='{}']:int-cpd[id='{}']:int-virtual-link-desc='{}' must match an existing "
garciadeblas4568a372021-03-24 09:19:48 +01001027 "int-virtual-link-desc".format(
1028 vdu["id"], int_cpd["id"], int_cpd_ivld_id
1029 ),
1030 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1031 )
garciaale960531a2020-10-20 18:29:45 -03001032
garciaale7cbd03c2020-11-27 10:38:35 -03001033 for df in get_iterable(indata.get("df")):
1034 for vlp in get_iterable(df.get("virtual-link-profile")):
1035 vlp_ivld_id = vlp.get("id")
1036 if vlp_ivld_id and vlp_ivld_id not in all_ivld_ids:
garciadeblas4568a372021-03-24 09:19:48 +01001037 raise EngineException(
1038 "df[id='{}']:virtual-link-profile='{}' must match an existing "
1039 "int-virtual-link-desc".format(df["id"], vlp_ivld_id),
1040 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1041 )
garciaale7cbd03c2020-11-27 10:38:35 -03001042
garciaale960531a2020-10-20 18:29:45 -03001043 @staticmethod
1044 def validate_monitoring_params(indata):
garciaale7cbd03c2020-11-27 10:38:35 -03001045 all_monitoring_params = set()
1046 for ivld in get_iterable(indata.get("int-virtual-link-desc")):
1047 for mp in get_iterable(ivld.get("monitoring-parameters")):
1048 mp_id = mp.get("id")
1049 if mp_id and mp_id in all_monitoring_params:
garciadeblas4568a372021-03-24 09:19:48 +01001050 raise EngineException(
1051 "Duplicated monitoring-parameter id in "
1052 "int-virtual-link-desc[id='{}']:monitoring-parameters[id='{}']".format(
1053 ivld["id"], mp_id
1054 ),
1055 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1056 )
gcalvino5e72d152018-10-23 11:46:57 +02001057 else:
garciaale7cbd03c2020-11-27 10:38:35 -03001058 all_monitoring_params.add(mp_id)
1059
1060 for vdu in get_iterable(indata.get("vdu")):
1061 for mp in get_iterable(vdu.get("monitoring-parameter")):
1062 mp_id = mp.get("id")
1063 if mp_id and mp_id in all_monitoring_params:
garciadeblas4568a372021-03-24 09:19:48 +01001064 raise EngineException(
1065 "Duplicated monitoring-parameter id in "
1066 "vdu[id='{}']:monitoring-parameter[id='{}']".format(
1067 vdu["id"], mp_id
1068 ),
1069 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1070 )
garciaale7cbd03c2020-11-27 10:38:35 -03001071 else:
1072 all_monitoring_params.add(mp_id)
1073
1074 for df in get_iterable(indata.get("df")):
1075 for mp in get_iterable(df.get("monitoring-parameter")):
1076 mp_id = mp.get("id")
1077 if mp_id and mp_id in all_monitoring_params:
garciadeblas4568a372021-03-24 09:19:48 +01001078 raise EngineException(
1079 "Duplicated monitoring-parameter id in "
1080 "df[id='{}']:monitoring-parameter[id='{}']".format(
1081 df["id"], mp_id
1082 ),
1083 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1084 )
garciaale7cbd03c2020-11-27 10:38:35 -03001085 else:
1086 all_monitoring_params.add(mp_id)
gcalvino5e72d152018-10-23 11:46:57 +02001087
garciaale960531a2020-10-20 18:29:45 -03001088 @staticmethod
1089 def validate_scaling_group_descriptor(indata):
garciaale7cbd03c2020-11-27 10:38:35 -03001090 all_monitoring_params = set()
1091 for ivld in get_iterable(indata.get("int-virtual-link-desc")):
1092 for mp in get_iterable(ivld.get("monitoring-parameters")):
1093 all_monitoring_params.add(mp.get("id"))
1094
1095 for vdu in get_iterable(indata.get("vdu")):
1096 for mp in get_iterable(vdu.get("monitoring-parameter")):
1097 all_monitoring_params.add(mp.get("id"))
1098
1099 for df in get_iterable(indata.get("df")):
1100 for mp in get_iterable(df.get("monitoring-parameter")):
1101 all_monitoring_params.add(mp.get("id"))
1102
1103 for df in get_iterable(indata.get("df")):
1104 for sa in get_iterable(df.get("scaling-aspect")):
1105 for sp in get_iterable(sa.get("scaling-policy")):
1106 for sc in get_iterable(sp.get("scaling-criteria")):
1107 sc_monitoring_param = sc.get("vnf-monitoring-param-ref")
garciadeblas4568a372021-03-24 09:19:48 +01001108 if (
1109 sc_monitoring_param
1110 and sc_monitoring_param not in all_monitoring_params
1111 ):
1112 raise EngineException(
1113 "df[id='{}']:scaling-aspect[id='{}']:scaling-policy"
1114 "[name='{}']:scaling-criteria[name='{}']: "
1115 "vnf-monitoring-param-ref='{}' not defined in any monitoring-param".format(
1116 df["id"],
1117 sa["id"],
1118 sp["name"],
1119 sc["name"],
1120 sc_monitoring_param,
1121 ),
1122 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1123 )
garciaale7cbd03c2020-11-27 10:38:35 -03001124
1125 for sca in get_iterable(sa.get("scaling-config-action")):
garciadeblas4568a372021-03-24 09:19:48 +01001126 if (
1127 "lcm-operations-configuration" not in df
1128 or "operate-vnf-op-config"
1129 not in df["lcm-operations-configuration"]
bravof41a52052021-02-17 18:08:01 -03001130 or not utils.find_in_list(
garciadeblas4568a372021-03-24 09:19:48 +01001131 df["lcm-operations-configuration"][
1132 "operate-vnf-op-config"
1133 ].get("day1-2", []),
1134 lambda config: config["id"] == indata["id"],
1135 )
bravof41a52052021-02-17 18:08:01 -03001136 ):
garciadeblas4568a372021-03-24 09:19:48 +01001137 raise EngineException(
1138 "'day1-2 configuration' not defined in the descriptor but it is "
1139 "referenced by df[id='{}']:scaling-aspect[id='{}']:scaling-config-action".format(
1140 df["id"], sa["id"]
1141 ),
1142 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1143 )
1144 for configuration in get_iterable(
1145 df["lcm-operations-configuration"]["operate-vnf-op-config"].get(
1146 "day1-2", []
1147 )
1148 ):
1149 for primitive in get_iterable(
1150 configuration.get("config-primitive")
1151 ):
1152 if (
1153 primitive["name"]
1154 == sca["vnf-config-primitive-name-ref"]
1155 ):
garciaale7cbd03c2020-11-27 10:38:35 -03001156 break
1157 else:
garciadeblas4568a372021-03-24 09:19:48 +01001158 raise EngineException(
1159 "df[id='{}']:scaling-aspect[id='{}']:scaling-config-action:vnf-"
1160 "config-primitive-name-ref='{}' does not match any "
1161 "day1-2 configuration:config-primitive:name".format(
1162 df["id"],
1163 sa["id"],
1164 sca["vnf-config-primitive-name-ref"],
1165 ),
1166 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1167 )
gcalvinoa6fe0002019-01-09 13:27:11 +01001168
delacruzramo271d2002019-12-02 21:00:37 +01001169 def delete_extra(self, session, _id, db_content, not_send_msg=None):
1170 """
1171 Deletes associate file system storage (via super)
1172 Deletes associated vnfpkgops from database.
1173 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1174 :param _id: server internal id
1175 :param db_content: The database content of the descriptor
1176 :return: None
1177 :raises: FsException in case of error while deleting associated storage
1178 """
1179 super().delete_extra(session, _id, db_content, not_send_msg)
1180 self.db.del_list("vnfpkgops", {"vnfPkgId": _id})
beierlmcee2ebf2022-03-29 17:42:48 -04001181 self.db.del_list(self.topic+"_revisions", {"_id": {"$regex": _id}})
garciaale960531a2020-10-20 18:29:45 -03001182
Frank Bryden19b97522020-07-10 12:32:02 +00001183 def sol005_projection(self, data):
1184 data["onboardingState"] = data["_admin"]["onboardingState"]
1185 data["operationalState"] = data["_admin"]["operationalState"]
1186 data["usageState"] = data["_admin"]["usageState"]
1187
1188 links = {}
1189 links["self"] = {"href": "/vnfpkgm/v1/vnf_packages/{}".format(data["_id"])}
1190 links["vnfd"] = {"href": "/vnfpkgm/v1/vnf_packages/{}/vnfd".format(data["_id"])}
garciadeblas4568a372021-03-24 09:19:48 +01001191 links["packageContent"] = {
1192 "href": "/vnfpkgm/v1/vnf_packages/{}/package_content".format(data["_id"])
1193 }
Frank Bryden19b97522020-07-10 12:32:02 +00001194 data["_links"] = links
garciaale960531a2020-10-20 18:29:45 -03001195
Frank Bryden19b97522020-07-10 12:32:02 +00001196 return super().sol005_projection(data)
delacruzramo271d2002019-12-02 21:00:37 +01001197
aticig9cfa8162022-04-07 11:57:18 +03001198 @staticmethod
1199 def find_software_version(vnfd: dict) -> str:
1200 """Find the sotware version in the VNFD descriptors
1201
1202 Args:
1203 vnfd (dict): Descriptor as a dictionary
1204
1205 Returns:
1206 software-version (str)
1207 """
1208 default_sw_version = "1.0"
1209 if vnfd.get("vnfd"):
1210 vnfd = vnfd["vnfd"]
1211 if vnfd.get("software-version"):
1212 return vnfd["software-version"]
1213 else:
1214 return default_sw_version
1215
1216 @staticmethod
1217 def extract_policies(vnfd: dict) -> dict:
1218 """Removes the policies from the VNFD descriptors
1219
1220 Args:
1221 vnfd (dict): Descriptor as a dictionary
1222
1223 Returns:
1224 vnfd (dict): VNFD which does not include policies
1225 """
elumalai90e2c6b2022-07-08 12:06:27 +05301226 for df in vnfd.get("df", {}):
1227 for policy in ["scaling-aspect", "healing-aspect"]:
1228 if (df.get(policy, {})):
1229 df.pop(policy)
1230 for vdu in vnfd.get("vdu", {}):
1231 for alarm_policy in ["alarm", "monitoring-parameter"]:
1232 if (vdu.get(alarm_policy, {})):
1233 vdu.pop(alarm_policy)
aticig9cfa8162022-04-07 11:57:18 +03001234 return vnfd
1235
1236 @staticmethod
1237 def extract_day12_primitives(vnfd: dict) -> dict:
1238 """Removes the day12 primitives from the VNFD descriptors
1239
1240 Args:
1241 vnfd (dict): Descriptor as a dictionary
1242
1243 Returns:
1244 vnfd (dict)
1245 """
1246 for df_id, df in enumerate(vnfd.get("df", {})):
1247 if (
1248 df.get("lcm-operations-configuration", {})
1249 .get("operate-vnf-op-config", {})
1250 .get("day1-2")
1251 ):
1252 day12 = df["lcm-operations-configuration"]["operate-vnf-op-config"].get(
1253 "day1-2"
1254 )
1255 for config_id, config in enumerate(day12):
1256 for key in [
1257 "initial-config-primitive",
1258 "config-primitive",
1259 "terminate-config-primitive",
1260 ]:
1261 config.pop(key, None)
1262 day12[config_id] = config
1263 df["lcm-operations-configuration"]["operate-vnf-op-config"][
1264 "day1-2"
1265 ] = day12
1266 vnfd["df"][df_id] = df
1267 return vnfd
1268
1269 def remove_modifiable_items(self, vnfd: dict) -> dict:
1270 """Removes the modifiable parts from the VNFD descriptors
1271
1272 It calls different extract functions according to different update types
1273 to clear all the modifiable items from VNFD
1274
1275 Args:
1276 vnfd (dict): Descriptor as a dictionary
1277
1278 Returns:
1279 vnfd (dict): Descriptor which does not include modifiable contents
1280 """
1281 if vnfd.get("vnfd"):
1282 vnfd = vnfd["vnfd"]
1283 vnfd.pop("_admin", None)
1284 # If the other extractions need to be done from VNFD,
1285 # the new extract methods could be appended to below list.
1286 for extract_function in [self.extract_day12_primitives, self.extract_policies]:
1287 vnfd_temp = extract_function(vnfd)
1288 vnfd = vnfd_temp
1289 return vnfd
1290
1291 def _validate_descriptor_changes(
1292 self,
1293 descriptor_file_name: str,
1294 old_descriptor_directory: str,
1295 new_descriptor_directory: str,
1296 ):
1297 """Compares the old and new VNFD descriptors and validates the new descriptor.
1298
1299 Args:
1300 old_descriptor_directory (str): Directory of descriptor which is in-use
1301 new_descriptor_directory (str): Directory of directory which is proposed to update (new revision)
1302
1303 Returns:
1304 None
1305
1306 Raises:
1307 EngineException: In case of error when there are unallowed changes
1308 """
1309 try:
1310 with self.fs.file_open(
1311 (old_descriptor_directory.rstrip("/"), descriptor_file_name), "r"
1312 ) as old_descriptor_file:
1313 with self.fs.file_open(
1314 (new_descriptor_directory, descriptor_file_name), "r"
1315 ) as new_descriptor_file:
1316 old_content = yaml.load(
1317 old_descriptor_file.read(), Loader=yaml.SafeLoader
1318 )
1319 new_content = yaml.load(
1320 new_descriptor_file.read(), Loader=yaml.SafeLoader
1321 )
1322 if old_content and new_content:
1323 if self.find_software_version(
1324 old_content
1325 ) != self.find_software_version(new_content):
1326 return
1327 disallowed_change = DeepDiff(
1328 self.remove_modifiable_items(old_content),
1329 self.remove_modifiable_items(new_content),
1330 )
1331 if disallowed_change:
1332 changed_nodes = functools.reduce(
1333 lambda a, b: a + " , " + b,
1334 [
1335 node.lstrip("root")
1336 for node in disallowed_change.get(
1337 "values_changed"
1338 ).keys()
1339 ],
1340 )
1341 raise EngineException(
1342 f"Error in validating new descriptor: {changed_nodes} cannot be modified, "
1343 "there are disallowed changes in the vnf descriptor.",
1344 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1345 )
1346 except (
1347 DbException,
1348 AttributeError,
1349 IndexError,
1350 KeyError,
1351 ValueError,
1352 ) as e:
1353 raise type(e)(
1354 "VNF Descriptor could not be processed with error: {}.".format(e)
1355 )
1356
tiernob24258a2018-10-04 18:39:49 +02001357
1358class NsdTopic(DescriptorTopic):
1359 topic = "nsds"
1360 topic_msg = "nsd"
1361
delacruzramo32bab472019-09-13 12:24:22 +02001362 def __init__(self, db, fs, msg, auth):
1363 DescriptorTopic.__init__(self, db, fs, msg, auth)
tiernob24258a2018-10-04 18:39:49 +02001364
garciaale7cbd03c2020-11-27 10:38:35 -03001365 def pyangbind_validation(self, item, data, force=False):
garciaaledf718ae2020-12-03 19:17:28 -03001366 if self._descriptor_data_is_in_old_format(data):
garciadeblas4568a372021-03-24 09:19:48 +01001367 raise EngineException(
1368 "ERROR: Unsupported descriptor format. Please, use an ETSI SOL006 descriptor.",
1369 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1370 )
garciaale7cbd03c2020-11-27 10:38:35 -03001371 try:
garciadeblas4568a372021-03-24 09:19:48 +01001372 nsd_vnf_profiles = data.get("df", [{}])[0].get("vnf-profile", [])
garciaale7cbd03c2020-11-27 10:38:35 -03001373 mynsd = etsi_nfv_nsd.etsi_nfv_nsd()
garciadeblas4568a372021-03-24 09:19:48 +01001374 pybindJSONDecoder.load_ietf_json(
1375 {"nsd": {"nsd": [data]}},
1376 None,
1377 None,
1378 obj=mynsd,
1379 path_helper=True,
1380 skip_unknown=force,
1381 )
garciaale7cbd03c2020-11-27 10:38:35 -03001382 out = pybindJSON.dumps(mynsd, mode="ietf")
1383 desc_out = self._remove_envelop(yaml.safe_load(out))
1384 desc_out = self._remove_yang_prefixes_from_descriptor(desc_out)
garciaale341ac1b2020-12-11 20:04:11 -03001385 if nsd_vnf_profiles:
garciadeblas4568a372021-03-24 09:19:48 +01001386 desc_out["df"][0]["vnf-profile"] = nsd_vnf_profiles
garciaale7cbd03c2020-11-27 10:38:35 -03001387 return desc_out
1388 except Exception as e:
garciadeblas4568a372021-03-24 09:19:48 +01001389 raise EngineException(
1390 "Error in pyangbind validation: {}".format(str(e)),
1391 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1392 )
garciaale7cbd03c2020-11-27 10:38:35 -03001393
tiernob24258a2018-10-04 18:39:49 +02001394 @staticmethod
garciaaledf718ae2020-12-03 19:17:28 -03001395 def _descriptor_data_is_in_old_format(data):
garciadeblas4568a372021-03-24 09:19:48 +01001396 return ("nsd-catalog" in data) or ("nsd:nsd-catalog" in data)
garciaaledf718ae2020-12-03 19:17:28 -03001397
1398 @staticmethod
tiernob24258a2018-10-04 18:39:49 +02001399 def _remove_envelop(indata=None):
1400 if not indata:
1401 return {}
1402 clean_indata = indata
1403
garciadeblas4568a372021-03-24 09:19:48 +01001404 if clean_indata.get("nsd"):
1405 clean_indata = clean_indata["nsd"]
1406 elif clean_indata.get("etsi-nfv-nsd:nsd"):
1407 clean_indata = clean_indata["etsi-nfv-nsd:nsd"]
1408 if clean_indata.get("nsd"):
1409 if (
1410 not isinstance(clean_indata["nsd"], list)
1411 or len(clean_indata["nsd"]) != 1
1412 ):
gcalvino46e4cb82018-10-26 13:10:22 +02001413 raise EngineException("'nsd' must be a list of only one element")
garciadeblas4568a372021-03-24 09:19:48 +01001414 clean_indata = clean_indata["nsd"][0]
tiernob24258a2018-10-04 18:39:49 +02001415 return clean_indata
1416
gcalvinoa6fe0002019-01-09 13:27:11 +01001417 def _validate_input_new(self, indata, storage_params, force=False):
Frank Bryden19b97522020-07-10 12:32:02 +00001418 indata.pop("nsdOnboardingState", None)
1419 indata.pop("nsdOperationalState", None)
1420 indata.pop("nsdUsageState", None)
1421
1422 indata.pop("links", None)
1423
gcalvino46e4cb82018-10-26 13:10:22 +02001424 indata = self.pyangbind_validation("nsds", indata, force)
tierno5a5c2182018-11-20 12:27:42 +00001425 # Cross references validation in the descriptor
tiernoaa1ca7b2018-11-08 19:00:20 +01001426 # 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 -03001427 for vld in get_iterable(indata.get("virtual-link-desc")):
1428 self.validate_vld_mgmt_network_with_virtual_link_protocol_data(vld, indata)
garciaale960531a2020-10-20 18:29:45 -03001429
garciaale7cbd03c2020-11-27 10:38:35 -03001430 self.validate_vnf_profiles_vnfd_id(indata)
garciaale960531a2020-10-20 18:29:45 -03001431
tiernob24258a2018-10-04 18:39:49 +02001432 return indata
1433
garciaale960531a2020-10-20 18:29:45 -03001434 @staticmethod
garciaale7cbd03c2020-11-27 10:38:35 -03001435 def validate_vld_mgmt_network_with_virtual_link_protocol_data(vld, indata):
1436 if not vld.get("mgmt-network"):
1437 return
1438 vld_id = vld.get("id")
1439 for df in get_iterable(indata.get("df")):
1440 for vlp in get_iterable(df.get("virtual-link-profile")):
1441 if vld_id and vld_id == vlp.get("virtual-link-desc-id"):
1442 if vlp.get("virtual-link-protocol-data"):
garciadeblas4568a372021-03-24 09:19:48 +01001443 raise EngineException(
1444 "Error at df[id='{}']:virtual-link-profile[id='{}']:virtual-link-"
1445 "protocol-data You cannot set a virtual-link-protocol-data "
1446 "when mgmt-network is True".format(df["id"], vlp["id"]),
1447 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1448 )
garciaale960531a2020-10-20 18:29:45 -03001449
1450 @staticmethod
garciaale7cbd03c2020-11-27 10:38:35 -03001451 def validate_vnf_profiles_vnfd_id(indata):
1452 all_vnfd_ids = set(get_iterable(indata.get("vnfd-id")))
1453 for df in get_iterable(indata.get("df")):
1454 for vnf_profile in get_iterable(df.get("vnf-profile")):
1455 vnfd_id = vnf_profile.get("vnfd-id")
1456 if vnfd_id and vnfd_id not in all_vnfd_ids:
garciadeblas4568a372021-03-24 09:19:48 +01001457 raise EngineException(
1458 "Error at df[id='{}']:vnf_profile[id='{}']:vnfd-id='{}' "
1459 "does not match any vnfd-id".format(
1460 df["id"], vnf_profile["id"], vnfd_id
1461 ),
1462 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1463 )
garciaale960531a2020-10-20 18:29:45 -03001464
Frank Brydendeba68e2020-07-27 13:55:11 +00001465 def _validate_input_edit(self, indata, content, force=False):
tiernoaa1ca7b2018-11-08 19:00:20 +01001466 # not needed to validate with pyangbind becuase it will be validated at check_conflict_on_edit
Frank Brydendeba68e2020-07-27 13:55:11 +00001467 """
1468 indata looks as follows:
garciadeblas4568a372021-03-24 09:19:48 +01001469 - In the new case (conformant)
1470 {'nsdOperationalState': 'DISABLED', 'userDefinedData': {'id': 'string23',
Frank Brydendeba68e2020-07-27 13:55:11 +00001471 '_id': 'c6ddc544-cede-4b94-9ebe-be07b298a3c1', 'name': 'simon46'}}
1472 - In the old case (backwards-compatible)
1473 {'id': 'string23', '_id': 'c6ddc544-cede-4b94-9ebe-be07b298a3c1', 'name': 'simon46'}
1474 """
1475 if "_admin" not in indata:
1476 indata["_admin"] = {}
1477
1478 if "nsdOperationalState" in indata:
1479 if indata["nsdOperationalState"] in ("ENABLED", "DISABLED"):
1480 indata["_admin"]["operationalState"] = indata.pop("nsdOperationalState")
1481 else:
garciadeblas4568a372021-03-24 09:19:48 +01001482 raise EngineException(
1483 "State '{}' is not a valid operational state".format(
1484 indata["nsdOperationalState"]
1485 ),
1486 http_code=HTTPStatus.BAD_REQUEST,
1487 )
Frank Brydendeba68e2020-07-27 13:55:11 +00001488
garciadeblas4568a372021-03-24 09:19:48 +01001489 # 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 +00001490 # to preserve current expected behaviour
1491 if "userDefinedData" in indata:
1492 data = indata.pop("userDefinedData")
gatici3e9bd122023-07-31 14:37:32 +03001493 if isinstance(data, dict):
Frank Brydendeba68e2020-07-27 13:55:11 +00001494 indata["_admin"]["userDefinedData"] = data
1495 else:
garciadeblas4568a372021-03-24 09:19:48 +01001496 raise EngineException(
1497 "userDefinedData should be an object, but is '{}' instead".format(
1498 type(data)
1499 ),
1500 http_code=HTTPStatus.BAD_REQUEST,
1501 )
1502 if (
1503 "operationalState" in indata["_admin"]
1504 and content["_admin"]["operationalState"]
1505 == indata["_admin"]["operationalState"]
1506 ):
1507 raise EngineException(
1508 "nsdOperationalState already {}".format(
1509 content["_admin"]["operationalState"]
1510 ),
1511 http_code=HTTPStatus.CONFLICT,
1512 )
tiernob24258a2018-10-04 18:39:49 +02001513 return indata
1514
tierno65ca36d2019-02-12 19:27:52 +01001515 def _check_descriptor_dependencies(self, session, descriptor):
tiernob24258a2018-10-04 18:39:49 +02001516 """
tierno5a5c2182018-11-20 12:27:42 +00001517 Check that the dependent descriptors exist on a new descriptor or edition. Also checks references to vnfd
1518 connection points are ok
tierno65ca36d2019-02-12 19:27:52 +01001519 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +02001520 :param descriptor: descriptor to be inserted or edit
1521 :return: None or raises exception
1522 """
tierno65ca36d2019-02-12 19:27:52 +01001523 if session["force"]:
tiernob24258a2018-10-04 18:39:49 +02001524 return
garciaale7cbd03c2020-11-27 10:38:35 -03001525 vnfds_index = self._get_descriptor_constituent_vnfds_index(session, descriptor)
garciaale960531a2020-10-20 18:29:45 -03001526
1527 # Cross references validation in the descriptor and vnfd connection point validation
garciaale7cbd03c2020-11-27 10:38:35 -03001528 for df in get_iterable(descriptor.get("df")):
1529 self.validate_df_vnf_profiles_constituent_connection_points(df, vnfds_index)
garciaale960531a2020-10-20 18:29:45 -03001530
garciaale7cbd03c2020-11-27 10:38:35 -03001531 def _get_descriptor_constituent_vnfds_index(self, session, descriptor):
1532 vnfds_index = {}
1533 if descriptor.get("vnfd-id") and not session["force"]:
1534 for vnfd_id in get_iterable(descriptor.get("vnfd-id")):
garciaale960531a2020-10-20 18:29:45 -03001535 query_filter = self._get_project_filter(session)
1536 query_filter["id"] = vnfd_id
1537 vnf_list = self.db.get_list("vnfds", query_filter)
tierno5a5c2182018-11-20 12:27:42 +00001538 if not vnf_list:
garciadeblas4568a372021-03-24 09:19:48 +01001539 raise EngineException(
1540 "Descriptor error at 'vnfd-id'='{}' references a non "
1541 "existing vnfd".format(vnfd_id),
1542 http_code=HTTPStatus.CONFLICT,
1543 )
garciaale7cbd03c2020-11-27 10:38:35 -03001544 vnfds_index[vnfd_id] = vnf_list[0]
1545 return vnfds_index
garciaale960531a2020-10-20 18:29:45 -03001546
1547 @staticmethod
garciaale7cbd03c2020-11-27 10:38:35 -03001548 def validate_df_vnf_profiles_constituent_connection_points(df, vnfds_index):
1549 for vnf_profile in get_iterable(df.get("vnf-profile")):
1550 vnfd = vnfds_index.get(vnf_profile["vnfd-id"])
1551 all_vnfd_ext_cpds = set()
1552 for ext_cpd in get_iterable(vnfd.get("ext-cpd")):
garciadeblas4568a372021-03-24 09:19:48 +01001553 if ext_cpd.get("id"):
1554 all_vnfd_ext_cpds.add(ext_cpd.get("id"))
garciaale7cbd03c2020-11-27 10:38:35 -03001555
garciadeblas4568a372021-03-24 09:19:48 +01001556 for virtual_link in get_iterable(
1557 vnf_profile.get("virtual-link-connectivity")
1558 ):
garciaale7cbd03c2020-11-27 10:38:35 -03001559 for vl_cpd in get_iterable(virtual_link.get("constituent-cpd-id")):
garciadeblas4568a372021-03-24 09:19:48 +01001560 vl_cpd_id = vl_cpd.get("constituent-cpd-id")
garciaale7cbd03c2020-11-27 10:38:35 -03001561 if vl_cpd_id and vl_cpd_id not in all_vnfd_ext_cpds:
garciadeblas4568a372021-03-24 09:19:48 +01001562 raise EngineException(
1563 "Error at df[id='{}']:vnf-profile[id='{}']:virtual-link-connectivity"
1564 "[virtual-link-profile-id='{}']:constituent-cpd-id='{}' references a "
1565 "non existing ext-cpd:id inside vnfd '{}'".format(
1566 df["id"],
1567 vnf_profile["id"],
1568 virtual_link["virtual-link-profile-id"],
1569 vl_cpd_id,
1570 vnfd["id"],
1571 ),
1572 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1573 )
tiernob24258a2018-10-04 18:39:49 +02001574
tierno65ca36d2019-02-12 19:27:52 +01001575 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
garciadeblas4568a372021-03-24 09:19:48 +01001576 final_content = super().check_conflict_on_edit(
1577 session, final_content, edit_content, _id
1578 )
tiernob24258a2018-10-04 18:39:49 +02001579
tierno65ca36d2019-02-12 19:27:52 +01001580 self._check_descriptor_dependencies(session, final_content)
tiernob24258a2018-10-04 18:39:49 +02001581
bravofb995ea22021-02-10 10:57:52 -03001582 return final_content
1583
tiernob4844ab2019-05-23 08:42:12 +00001584 def check_conflict_on_del(self, session, _id, db_content):
tiernob24258a2018-10-04 18:39:49 +02001585 """
1586 Check that there is not any NSR that uses this NSD. Only NSRs belonging to this project are considered. Note
1587 that NSD can be public and be used by other projects.
tierno65ca36d2019-02-12 19:27:52 +01001588 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob4844ab2019-05-23 08:42:12 +00001589 :param _id: nsd internal id
1590 :param db_content: The database content of the _id
tiernob24258a2018-10-04 18:39:49 +02001591 :return: None or raises EngineException with the conflict
1592 """
tierno65ca36d2019-02-12 19:27:52 +01001593 if session["force"]:
tiernob24258a2018-10-04 18:39:49 +02001594 return
tiernob4844ab2019-05-23 08:42:12 +00001595 descriptor = db_content
1596 descriptor_id = descriptor.get("id")
1597 if not descriptor_id: # empty nsd not uploaded
1598 return
1599
1600 # check NSD used by NS
tierno65ca36d2019-02-12 19:27:52 +01001601 _filter = self._get_project_filter(session)
tiernob4844ab2019-05-23 08:42:12 +00001602 _filter["nsd-id"] = _id
tiernob24258a2018-10-04 18:39:49 +02001603 if self.db.get_list("nsrs", _filter):
garciadeblas4568a372021-03-24 09:19:48 +01001604 raise EngineException(
1605 "There is at least one NS instance using this descriptor",
1606 http_code=HTTPStatus.CONFLICT,
1607 )
tiernob4844ab2019-05-23 08:42:12 +00001608
1609 # check NSD referenced by NST
1610 del _filter["nsd-id"]
1611 _filter["netslice-subnet.ANYINDEX.nsd-ref"] = descriptor_id
1612 if self.db.get_list("nsts", _filter):
garciadeblas4568a372021-03-24 09:19:48 +01001613 raise EngineException(
1614 "There is at least one NetSlice Template referencing this descriptor",
1615 http_code=HTTPStatus.CONFLICT,
1616 )
garciaale960531a2020-10-20 18:29:45 -03001617
beierlmcee2ebf2022-03-29 17:42:48 -04001618 def delete_extra(self, session, _id, db_content, not_send_msg=None):
1619 """
1620 Deletes associate file system storage (via super)
1621 Deletes associated vnfpkgops from database.
1622 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1623 :param _id: server internal id
1624 :param db_content: The database content of the descriptor
1625 :return: None
1626 :raises: FsException in case of error while deleting associated storage
1627 """
1628 super().delete_extra(session, _id, db_content, not_send_msg)
1629 self.db.del_list(self.topic+"_revisions", { "_id": { "$regex": _id}})
1630
aticig9cfa8162022-04-07 11:57:18 +03001631 @staticmethod
1632 def extract_day12_primitives(nsd: dict) -> dict:
1633 """Removes the day12 primitives from the NSD descriptors
1634
1635 Args:
1636 nsd (dict): Descriptor as a dictionary
1637
1638 Returns:
1639 nsd (dict): Cleared NSD
1640 """
1641 if nsd.get("ns-configuration"):
1642 for key in [
1643 "config-primitive",
1644 "initial-config-primitive",
1645 "terminate-config-primitive",
1646 ]:
1647 nsd["ns-configuration"].pop(key, None)
1648 return nsd
1649
1650 def remove_modifiable_items(self, nsd: dict) -> dict:
1651 """Removes the modifiable parts from the VNFD descriptors
1652
1653 It calls different extract functions according to different update types
1654 to clear all the modifiable items from NSD
1655
1656 Args:
1657 nsd (dict): Descriptor as a dictionary
1658
1659 Returns:
1660 nsd (dict): Descriptor which does not include modifiable contents
1661 """
1662 while isinstance(nsd, dict) and nsd.get("nsd"):
1663 nsd = nsd["nsd"]
1664 if isinstance(nsd, list):
1665 nsd = nsd[0]
1666 nsd.pop("_admin", None)
1667 # If the more extractions need to be done from NSD,
1668 # the new extract methods could be appended to below list.
1669 for extract_function in [self.extract_day12_primitives]:
1670 nsd_temp = extract_function(nsd)
1671 nsd = nsd_temp
1672 return nsd
1673
1674 def _validate_descriptor_changes(
1675 self,
1676 descriptor_file_name: str,
1677 old_descriptor_directory: str,
1678 new_descriptor_directory: str,
1679 ):
1680 """Compares the old and new NSD descriptors and validates the new descriptor
1681
1682 Args:
1683 old_descriptor_directory: Directory of descriptor which is in-use
1684 new_descriptor_directory: Directory of directory which is proposed to update (new revision)
1685
1686 Returns:
1687 None
1688
1689 Raises:
1690 EngineException: In case of error if the changes are not allowed
1691 """
1692
1693 try:
1694 with self.fs.file_open(
1695 (old_descriptor_directory, descriptor_file_name), "r"
1696 ) as old_descriptor_file:
1697 with self.fs.file_open(
1698 (new_descriptor_directory.rstrip("/"), descriptor_file_name), "r"
1699 ) as new_descriptor_file:
1700 old_content = yaml.load(
1701 old_descriptor_file.read(), Loader=yaml.SafeLoader
1702 )
1703 new_content = yaml.load(
1704 new_descriptor_file.read(), Loader=yaml.SafeLoader
1705 )
1706 if old_content and new_content:
1707 disallowed_change = DeepDiff(
1708 self.remove_modifiable_items(old_content),
1709 self.remove_modifiable_items(new_content),
1710 )
1711 if disallowed_change:
1712 changed_nodes = functools.reduce(
1713 lambda a, b: a + ", " + b,
1714 [
1715 node.lstrip("root")
1716 for node in disallowed_change.get(
1717 "values_changed"
1718 ).keys()
1719 ],
1720 )
1721 raise EngineException(
1722 f"Error in validating new descriptor: {changed_nodes} cannot be modified, "
1723 "there are disallowed changes in the ns descriptor. ",
1724 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1725 )
1726 except (
1727 DbException,
1728 AttributeError,
1729 IndexError,
1730 KeyError,
1731 ValueError,
1732 ) as e:
1733 raise type(e)(
1734 "NS Descriptor could not be processed with error: {}.".format(e)
1735 )
1736
Frank Bryden19b97522020-07-10 12:32:02 +00001737 def sol005_projection(self, data):
1738 data["nsdOnboardingState"] = data["_admin"]["onboardingState"]
1739 data["nsdOperationalState"] = data["_admin"]["operationalState"]
1740 data["nsdUsageState"] = data["_admin"]["usageState"]
1741
1742 links = {}
1743 links["self"] = {"href": "/nsd/v1/ns_descriptors/{}".format(data["_id"])}
garciadeblas4568a372021-03-24 09:19:48 +01001744 links["nsd_content"] = {
1745 "href": "/nsd/v1/ns_descriptors/{}/nsd_content".format(data["_id"])
1746 }
Frank Bryden19b97522020-07-10 12:32:02 +00001747 data["_links"] = links
garciaale960531a2020-10-20 18:29:45 -03001748
Frank Bryden19b97522020-07-10 12:32:02 +00001749 return super().sol005_projection(data)
tiernob24258a2018-10-04 18:39:49 +02001750
1751
Felipe Vicensb57758d2018-10-16 16:00:20 +02001752class NstTopic(DescriptorTopic):
1753 topic = "nsts"
1754 topic_msg = "nst"
tierno6b02b052020-06-02 10:07:41 +00001755 quota_name = "slice_templates"
Felipe Vicensb57758d2018-10-16 16:00:20 +02001756
delacruzramo32bab472019-09-13 12:24:22 +02001757 def __init__(self, db, fs, msg, auth):
1758 DescriptorTopic.__init__(self, db, fs, msg, auth)
Felipe Vicensb57758d2018-10-16 16:00:20 +02001759
garciaale7cbd03c2020-11-27 10:38:35 -03001760 def pyangbind_validation(self, item, data, force=False):
1761 try:
1762 mynst = nst_im()
garciadeblas4568a372021-03-24 09:19:48 +01001763 pybindJSONDecoder.load_ietf_json(
1764 {"nst": [data]},
1765 None,
1766 None,
1767 obj=mynst,
1768 path_helper=True,
1769 skip_unknown=force,
1770 )
garciaale7cbd03c2020-11-27 10:38:35 -03001771 out = pybindJSON.dumps(mynst, mode="ietf")
1772 desc_out = self._remove_envelop(yaml.safe_load(out))
1773 return desc_out
1774 except Exception as e:
garciadeblas4568a372021-03-24 09:19:48 +01001775 raise EngineException(
1776 "Error in pyangbind validation: {}".format(str(e)),
1777 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1778 )
garciaale7cbd03c2020-11-27 10:38:35 -03001779
Felipe Vicensb57758d2018-10-16 16:00:20 +02001780 @staticmethod
1781 def _remove_envelop(indata=None):
1782 if not indata:
1783 return {}
1784 clean_indata = indata
1785
garciadeblas4568a372021-03-24 09:19:48 +01001786 if clean_indata.get("nst"):
1787 if (
1788 not isinstance(clean_indata["nst"], list)
1789 or len(clean_indata["nst"]) != 1
1790 ):
Felipe Vicensb57758d2018-10-16 16:00:20 +02001791 raise EngineException("'nst' must be a list only one element")
garciadeblas4568a372021-03-24 09:19:48 +01001792 clean_indata = clean_indata["nst"][0]
1793 elif clean_indata.get("nst:nst"):
1794 if (
1795 not isinstance(clean_indata["nst:nst"], list)
1796 or len(clean_indata["nst:nst"]) != 1
1797 ):
gcalvino70434c12018-11-27 15:17:04 +01001798 raise EngineException("'nst:nst' must be a list only one element")
garciadeblas4568a372021-03-24 09:19:48 +01001799 clean_indata = clean_indata["nst:nst"][0]
Felipe Vicensb57758d2018-10-16 16:00:20 +02001800 return clean_indata
1801
gcalvinoa6fe0002019-01-09 13:27:11 +01001802 def _validate_input_new(self, indata, storage_params, force=False):
Frank Bryden19b97522020-07-10 12:32:02 +00001803 indata.pop("onboardingState", None)
1804 indata.pop("operationalState", None)
1805 indata.pop("usageState", None)
gcalvino70434c12018-11-27 15:17:04 +01001806 indata = self.pyangbind_validation("nsts", indata, force)
Felipe Vicense36ab852018-11-23 14:12:09 +01001807 return indata.copy()
1808
Felipe Vicensb57758d2018-10-16 16:00:20 +02001809 def _check_descriptor_dependencies(self, session, descriptor):
1810 """
1811 Check that the dependent descriptors exist on a new descriptor or edition
tierno65ca36d2019-02-12 19:27:52 +01001812 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Felipe Vicensb57758d2018-10-16 16:00:20 +02001813 :param descriptor: descriptor to be inserted or edit
1814 :return: None or raises exception
1815 """
1816 if not descriptor.get("netslice-subnet"):
1817 return
1818 for nsd in descriptor["netslice-subnet"]:
1819 nsd_id = nsd["nsd-ref"]
tierno65ca36d2019-02-12 19:27:52 +01001820 filter_q = self._get_project_filter(session)
Felipe Vicensb57758d2018-10-16 16:00:20 +02001821 filter_q["id"] = nsd_id
1822 if not self.db.get_list("nsds", filter_q):
garciadeblas4568a372021-03-24 09:19:48 +01001823 raise EngineException(
1824 "Descriptor error at 'netslice-subnet':'nsd-ref'='{}' references a non "
1825 "existing nsd".format(nsd_id),
1826 http_code=HTTPStatus.CONFLICT,
1827 )
Felipe Vicensb57758d2018-10-16 16:00:20 +02001828
tierno65ca36d2019-02-12 19:27:52 +01001829 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
garciadeblas4568a372021-03-24 09:19:48 +01001830 final_content = super().check_conflict_on_edit(
1831 session, final_content, edit_content, _id
1832 )
Felipe Vicensb57758d2018-10-16 16:00:20 +02001833
1834 self._check_descriptor_dependencies(session, final_content)
bravofb995ea22021-02-10 10:57:52 -03001835 return final_content
Felipe Vicensb57758d2018-10-16 16:00:20 +02001836
tiernob4844ab2019-05-23 08:42:12 +00001837 def check_conflict_on_del(self, session, _id, db_content):
Felipe Vicensb57758d2018-10-16 16:00:20 +02001838 """
1839 Check that there is not any NSIR that uses this NST. Only NSIRs belonging to this project are considered. Note
1840 that NST can be public and be used by other projects.
tierno65ca36d2019-02-12 19:27:52 +01001841 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Felipe Vicens07f31722018-10-29 15:16:44 +01001842 :param _id: nst internal id
tiernob4844ab2019-05-23 08:42:12 +00001843 :param db_content: The database content of the _id.
Felipe Vicensb57758d2018-10-16 16:00:20 +02001844 :return: None or raises EngineException with the conflict
1845 """
1846 # TODO: Check this method
tierno65ca36d2019-02-12 19:27:52 +01001847 if session["force"]:
Felipe Vicensb57758d2018-10-16 16:00:20 +02001848 return
Felipe Vicens07f31722018-10-29 15:16:44 +01001849 # Get Network Slice Template from Database
tierno65ca36d2019-02-12 19:27:52 +01001850 _filter = self._get_project_filter(session)
tiernoea97c042019-09-13 09:44:42 +00001851 _filter["_admin.nst-id"] = _id
tiernob4844ab2019-05-23 08:42:12 +00001852 if self.db.get_list("nsis", _filter):
garciadeblas4568a372021-03-24 09:19:48 +01001853 raise EngineException(
1854 "there is at least one Netslice Instance using this descriptor",
1855 http_code=HTTPStatus.CONFLICT,
1856 )
Felipe Vicensb57758d2018-10-16 16:00:20 +02001857
Frank Bryden19b97522020-07-10 12:32:02 +00001858 def sol005_projection(self, data):
1859 data["onboardingState"] = data["_admin"]["onboardingState"]
1860 data["operationalState"] = data["_admin"]["operationalState"]
1861 data["usageState"] = data["_admin"]["usageState"]
1862
1863 links = {}
1864 links["self"] = {"href": "/nst/v1/netslice_templates/{}".format(data["_id"])}
1865 links["nst"] = {"href": "/nst/v1/netslice_templates/{}/nst".format(data["_id"])}
1866 data["_links"] = links
1867
1868 return super().sol005_projection(data)
1869
Felipe Vicensb57758d2018-10-16 16:00:20 +02001870
tiernob24258a2018-10-04 18:39:49 +02001871class PduTopic(BaseTopic):
1872 topic = "pdus"
1873 topic_msg = "pdu"
tierno6b02b052020-06-02 10:07:41 +00001874 quota_name = "pduds"
tiernob24258a2018-10-04 18:39:49 +02001875 schema_new = pdu_new_schema
1876 schema_edit = pdu_edit_schema
1877
delacruzramo32bab472019-09-13 12:24:22 +02001878 def __init__(self, db, fs, msg, auth):
1879 BaseTopic.__init__(self, db, fs, msg, auth)
tiernob24258a2018-10-04 18:39:49 +02001880
1881 @staticmethod
1882 def format_on_new(content, project_id=None, make_public=False):
tierno36ec8602018-11-02 17:27:11 +01001883 BaseTopic.format_on_new(content, project_id=project_id, make_public=make_public)
tiernob24258a2018-10-04 18:39:49 +02001884 content["_admin"]["onboardingState"] = "CREATED"
tierno36ec8602018-11-02 17:27:11 +01001885 content["_admin"]["operationalState"] = "ENABLED"
1886 content["_admin"]["usageState"] = "NOT_IN_USE"
tiernob24258a2018-10-04 18:39:49 +02001887
tiernob4844ab2019-05-23 08:42:12 +00001888 def check_conflict_on_del(self, session, _id, db_content):
1889 """
1890 Check that there is not any vnfr that uses this PDU
1891 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1892 :param _id: pdu internal id
1893 :param db_content: The database content of the _id.
1894 :return: None or raises EngineException with the conflict
1895 """
tierno65ca36d2019-02-12 19:27:52 +01001896 if session["force"]:
tiernob24258a2018-10-04 18:39:49 +02001897 return
tiernob4844ab2019-05-23 08:42:12 +00001898
1899 _filter = self._get_project_filter(session)
1900 _filter["vdur.pdu-id"] = _id
tiernob24258a2018-10-04 18:39:49 +02001901 if self.db.get_list("vnfrs", _filter):
garciadeblas4568a372021-03-24 09:19:48 +01001902 raise EngineException(
1903 "There is at least one VNF instance using this PDU",
1904 http_code=HTTPStatus.CONFLICT,
1905 )
delacruzramo271d2002019-12-02 21:00:37 +01001906
1907
1908class VnfPkgOpTopic(BaseTopic):
1909 topic = "vnfpkgops"
1910 topic_msg = "vnfd"
1911 schema_new = vnfpkgop_new_schema
1912 schema_edit = None
1913
1914 def __init__(self, db, fs, msg, auth):
1915 BaseTopic.__init__(self, db, fs, msg, auth)
1916
1917 def edit(self, session, _id, indata=None, kwargs=None, content=None):
garciadeblas4568a372021-03-24 09:19:48 +01001918 raise EngineException(
1919 "Method 'edit' not allowed for topic '{}'".format(self.topic),
1920 HTTPStatus.METHOD_NOT_ALLOWED,
1921 )
delacruzramo271d2002019-12-02 21:00:37 +01001922
1923 def delete(self, session, _id, dry_run=False):
garciadeblas4568a372021-03-24 09:19:48 +01001924 raise EngineException(
1925 "Method 'delete' not allowed for topic '{}'".format(self.topic),
1926 HTTPStatus.METHOD_NOT_ALLOWED,
1927 )
delacruzramo271d2002019-12-02 21:00:37 +01001928
1929 def delete_list(self, session, filter_q=None):
garciadeblas4568a372021-03-24 09:19:48 +01001930 raise EngineException(
1931 "Method 'delete_list' not allowed for topic '{}'".format(self.topic),
1932 HTTPStatus.METHOD_NOT_ALLOWED,
1933 )
delacruzramo271d2002019-12-02 21:00:37 +01001934
1935 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
1936 """
1937 Creates a new entry into database.
1938 :param rollback: list to append created items at database in case a rollback may to be done
1939 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1940 :param indata: data to be inserted
1941 :param kwargs: used to override the indata descriptor
1942 :param headers: http request headers
1943 :return: _id, op_id:
1944 _id: identity of the inserted data.
1945 op_id: None
1946 """
1947 self._update_input_with_kwargs(indata, kwargs)
1948 validate_input(indata, self.schema_new)
1949 vnfpkg_id = indata["vnfPkgId"]
1950 filter_q = BaseTopic._get_project_filter(session)
1951 filter_q["_id"] = vnfpkg_id
1952 vnfd = self.db.get_one("vnfds", filter_q)
1953 operation = indata["lcmOperationType"]
1954 kdu_name = indata["kdu_name"]
1955 for kdu in vnfd.get("kdu", []):
1956 if kdu["name"] == kdu_name:
1957 helm_chart = kdu.get("helm-chart")
1958 juju_bundle = kdu.get("juju-bundle")
1959 break
1960 else:
garciadeblas4568a372021-03-24 09:19:48 +01001961 raise EngineException(
1962 "Not found vnfd[id='{}']:kdu[name='{}']".format(vnfpkg_id, kdu_name)
1963 )
delacruzramo271d2002019-12-02 21:00:37 +01001964 if helm_chart:
1965 indata["helm-chart"] = helm_chart
1966 match = fullmatch(r"([^/]*)/([^/]*)", helm_chart)
1967 repo_name = match.group(1) if match else None
1968 elif juju_bundle:
1969 indata["juju-bundle"] = juju_bundle
1970 match = fullmatch(r"([^/]*)/([^/]*)", juju_bundle)
1971 repo_name = match.group(1) if match else None
1972 else:
garciadeblas4568a372021-03-24 09:19:48 +01001973 raise EngineException(
1974 "Found neither 'helm-chart' nor 'juju-bundle' in vnfd[id='{}']:kdu[name='{}']".format(
1975 vnfpkg_id, kdu_name
1976 )
1977 )
delacruzramo271d2002019-12-02 21:00:37 +01001978 if repo_name:
1979 del filter_q["_id"]
1980 filter_q["name"] = repo_name
1981 repo = self.db.get_one("k8srepos", filter_q)
1982 k8srepo_id = repo.get("_id")
1983 k8srepo_url = repo.get("url")
1984 else:
1985 k8srepo_id = None
1986 k8srepo_url = None
1987 indata["k8srepoId"] = k8srepo_id
1988 indata["k8srepo_url"] = k8srepo_url
1989 vnfpkgop_id = str(uuid4())
1990 vnfpkgop_desc = {
1991 "_id": vnfpkgop_id,
1992 "operationState": "PROCESSING",
1993 "vnfPkgId": vnfpkg_id,
1994 "lcmOperationType": operation,
1995 "isAutomaticInvocation": False,
1996 "isCancelPending": False,
1997 "operationParams": indata,
1998 "links": {
1999 "self": "/osm/vnfpkgm/v1/vnfpkg_op_occs/" + vnfpkgop_id,
2000 "vnfpkg": "/osm/vnfpkgm/v1/vnf_packages/" + vnfpkg_id,
garciadeblas4568a372021-03-24 09:19:48 +01002001 },
delacruzramo271d2002019-12-02 21:00:37 +01002002 }
garciadeblas4568a372021-03-24 09:19:48 +01002003 self.format_on_new(
2004 vnfpkgop_desc, session["project_id"], make_public=session["public"]
2005 )
delacruzramo271d2002019-12-02 21:00:37 +01002006 ctime = vnfpkgop_desc["_admin"]["created"]
2007 vnfpkgop_desc["statusEnteredTime"] = ctime
2008 vnfpkgop_desc["startTime"] = ctime
2009 self.db.create(self.topic, vnfpkgop_desc)
2010 rollback.append({"topic": self.topic, "_id": vnfpkgop_id})
2011 self.msg.write(self.topic_msg, operation, vnfpkgop_desc)
2012 return vnfpkgop_id, None