blob: 7a034fa9c4dc8f23beb47d85d04b18f56da24988 [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
Daniel Arndt00f83aa2023-06-15 16:43:33 +020023import re
garciadeblasdcfa3d62025-06-25 16:44:25 +020024import base64
garciadeblas4568a372021-03-24 09:19:48 +010025
tiernob24258a2018-10-04 18:39:49 +020026# import logging
aticig9cfa8162022-04-07 11:57:18 +030027from deepdiff import DeepDiff
garciadeblasdcfa3d62025-06-25 16:44:25 +020028from hashlib import md5, sha384
tiernob24258a2018-10-04 18:39:49 +020029from osm_common.dbbase import DbException, deep_update_rfc7396
30from http import HTTPStatus
delacruzramo26301bb2019-11-15 14:45:32 +010031from time import time
delacruzramo271d2002019-12-02 21:00:37 +010032from uuid import uuid4
33from re import fullmatch
bravofc26740a2021-11-08 09:44:54 -030034from zipfile import ZipFile
Gabriel Cuba646773d2023-11-20 01:43:05 -050035from urllib.parse import urlparse
garciadeblas4568a372021-03-24 09:19:48 +010036from osm_nbi.validation import (
37 ValidationError,
38 pdu_new_schema,
39 pdu_edit_schema,
40 validate_input,
41 vnfpkgop_new_schema,
kayal2001f71c2e82024-06-25 15:26:24 +053042 ns_config_template,
43 vnf_schema,
44 vld_schema,
45 additional_params_for_vnf,
garciadeblas4568a372021-03-24 09:19:48 +010046)
aticig2b5e1232022-08-10 17:30:12 +030047from osm_nbi.base_topic import (
48 BaseTopic,
49 EngineException,
50 get_iterable,
51 detect_descriptor_usage,
52)
sousaedu317b9fd2021-07-29 17:40:16 +020053from osm_im import etsi_nfv_vnfd, etsi_nfv_nsd
Adurti014a6302024-05-08 05:03:01 +000054from osm_im.validation import Validation as validation_im
gcalvino70434c12018-11-27 15:17:04 +010055from osm_im.nst import nst as nst_im
gcalvino46e4cb82018-10-26 13:10:22 +020056from pyangbind.lib.serialise import pybindJSONDecoder
57import pyangbind.lib.pybindJSON as pybindJSON
bravof41a52052021-02-17 18:08:01 -030058from osm_nbi import utils
tiernob24258a2018-10-04 18:39:49 +020059
60__author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
61
Daniel Arndt00f83aa2023-06-15 16:43:33 +020062valid_helm_chart_re = re.compile(
63 r"^[a-z0-9]([-a-z0-9]*[a-z0-9]/)?([a-z0-9]([-a-z0-9]*[a-z0-9])?)*$"
64)
65
tiernob24258a2018-10-04 18:39:49 +020066
67class DescriptorTopic(BaseTopic):
delacruzramo32bab472019-09-13 12:24:22 +020068 def __init__(self, db, fs, msg, auth):
Daniel Arndt00f83aa2023-06-15 16:43:33 +020069 super().__init__(db, fs, msg, auth)
tiernob24258a2018-10-04 18:39:49 +020070
garciadeblasf2af4a12023-01-24 16:56:54 +010071 def _validate_input_new(self, indata, storage_params, force=False):
72 return indata
73
tierno65ca36d2019-02-12 19:27:52 +010074 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
garciadeblas4568a372021-03-24 09:19:48 +010075 final_content = super().check_conflict_on_edit(
76 session, final_content, edit_content, _id
77 )
K Sai Kiran45bd94c2019-11-25 17:30:37 +053078
79 def _check_unique_id_name(descriptor, position=""):
80 for desc_key, desc_item in descriptor.items():
81 if isinstance(desc_item, list) and desc_item:
82 used_ids = []
83 desc_item_id = None
84 for index, list_item in enumerate(desc_item):
85 if isinstance(list_item, dict):
garciadeblas4568a372021-03-24 09:19:48 +010086 _check_unique_id_name(
87 list_item, "{}.{}[{}]".format(position, desc_key, index)
88 )
K Sai Kiran45bd94c2019-11-25 17:30:37 +053089 # Base case
garciadeblas4568a372021-03-24 09:19:48 +010090 if index == 0 and (
91 list_item.get("id") or list_item.get("name")
92 ):
K Sai Kiran45bd94c2019-11-25 17:30:37 +053093 desc_item_id = "id" if list_item.get("id") else "name"
94 if desc_item_id and list_item.get(desc_item_id):
95 if list_item[desc_item_id] in used_ids:
garciadeblas4568a372021-03-24 09:19:48 +010096 position = "{}.{}[{}]".format(
97 position, desc_key, index
98 )
99 raise EngineException(
100 "Error: identifier {} '{}' is not unique and repeats at '{}'".format(
101 desc_item_id,
102 list_item[desc_item_id],
103 position,
104 ),
105 HTTPStatus.UNPROCESSABLE_ENTITY,
106 )
K Sai Kiran45bd94c2019-11-25 17:30:37 +0530107 used_ids.append(list_item[desc_item_id])
garciaale960531a2020-10-20 18:29:45 -0300108
K Sai Kiran45bd94c2019-11-25 17:30:37 +0530109 _check_unique_id_name(final_content)
tiernoaa1ca7b2018-11-08 19:00:20 +0100110 # 1. validate again with pyangbind
111 # 1.1. remove internal keys
112 internal_keys = {}
113 for k in ("_id", "_admin"):
114 if k in final_content:
115 internal_keys[k] = final_content.pop(k)
gcalvinoa6fe0002019-01-09 13:27:11 +0100116 storage_params = internal_keys["_admin"].get("storage")
garciadeblas4568a372021-03-24 09:19:48 +0100117 serialized = self._validate_input_new(
118 final_content, storage_params, session["force"]
119 )
bravofb995ea22021-02-10 10:57:52 -0300120
tiernoaa1ca7b2018-11-08 19:00:20 +0100121 # 1.2. modify final_content with a serialized version
bravofb995ea22021-02-10 10:57:52 -0300122 final_content = copy.deepcopy(serialized)
tiernoaa1ca7b2018-11-08 19:00:20 +0100123 # 1.3. restore internal keys
124 for k, v in internal_keys.items():
125 final_content[k] = v
tierno65ca36d2019-02-12 19:27:52 +0100126 if session["force"]:
bravofb995ea22021-02-10 10:57:52 -0300127 return final_content
128
tiernoaa1ca7b2018-11-08 19:00:20 +0100129 # 2. check that this id is not present
130 if "id" in edit_content:
tierno65ca36d2019-02-12 19:27:52 +0100131 _filter = self._get_project_filter(session)
bravofb995ea22021-02-10 10:57:52 -0300132
tiernoaa1ca7b2018-11-08 19:00:20 +0100133 _filter["id"] = final_content["id"]
134 _filter["_id.neq"] = _id
bravofb995ea22021-02-10 10:57:52 -0300135
tiernoaa1ca7b2018-11-08 19:00:20 +0100136 if self.db.get_one(self.topic, _filter, fail_on_empty=False):
garciadeblas4568a372021-03-24 09:19:48 +0100137 raise EngineException(
138 "{} with id '{}' already exists for this project".format(
garciadeblasf2af4a12023-01-24 16:56:54 +0100139 (str(self.topic))[:-1], final_content["id"]
garciadeblas4568a372021-03-24 09:19:48 +0100140 ),
141 HTTPStatus.CONFLICT,
142 )
tiernob24258a2018-10-04 18:39:49 +0200143
bravofb995ea22021-02-10 10:57:52 -0300144 return final_content
145
tiernob24258a2018-10-04 18:39:49 +0200146 @staticmethod
147 def format_on_new(content, project_id=None, make_public=False):
148 BaseTopic.format_on_new(content, project_id=project_id, make_public=make_public)
149 content["_admin"]["onboardingState"] = "CREATED"
150 content["_admin"]["operationalState"] = "DISABLED"
tierno36ec8602018-11-02 17:27:11 +0100151 content["_admin"]["usageState"] = "NOT_IN_USE"
tiernob24258a2018-10-04 18:39:49 +0200152
tiernobee3bad2019-12-05 12:26:01 +0000153 def delete_extra(self, session, _id, db_content, not_send_msg=None):
tiernob4844ab2019-05-23 08:42:12 +0000154 """
155 Deletes file system storage associated with the descriptor
156 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
157 :param _id: server internal id
158 :param db_content: The database content of the descriptor
tiernobee3bad2019-12-05 12:26:01 +0000159 :param not_send_msg: To not send message (False) or store content (list) instead
tiernob4844ab2019-05-23 08:42:12 +0000160 :return: None if ok or raises EngineException with the problem
161 """
tiernob24258a2018-10-04 18:39:49 +0200162 self.fs.file_delete(_id, ignore_non_exist=True)
tiernof717cbe2018-12-03 16:35:42 +0000163 self.fs.file_delete(_id + "_", ignore_non_exist=True) # remove temp folder
beierlmcee2ebf2022-03-29 17:42:48 -0400164 # Remove file revisions
165 if "revision" in db_content["_admin"]:
166 revision = db_content["_admin"]["revision"]
167 while revision > 0:
168 self.fs.file_delete(_id + ":" + str(revision), ignore_non_exist=True)
169 revision = revision - 1
170
tiernob24258a2018-10-04 18:39:49 +0200171 @staticmethod
172 def get_one_by_id(db, session, topic, id):
173 # find owned by this project
tierno65ca36d2019-02-12 19:27:52 +0100174 _filter = BaseTopic._get_project_filter(session)
tiernob24258a2018-10-04 18:39:49 +0200175 _filter["id"] = id
176 desc_list = db.get_list(topic, _filter)
177 if len(desc_list) == 1:
178 return desc_list[0]
179 elif len(desc_list) > 1:
garciadeblas4568a372021-03-24 09:19:48 +0100180 raise DbException(
181 "Found more than one {} with id='{}' belonging to this project".format(
182 topic[:-1], id
183 ),
184 HTTPStatus.CONFLICT,
185 )
tiernob24258a2018-10-04 18:39:49 +0200186
187 # not found any: try to find public
tierno65ca36d2019-02-12 19:27:52 +0100188 _filter = BaseTopic._get_project_filter(session)
tiernob24258a2018-10-04 18:39:49 +0200189 _filter["id"] = id
190 desc_list = db.get_list(topic, _filter)
191 if not desc_list:
garciadeblas4568a372021-03-24 09:19:48 +0100192 raise DbException(
193 "Not found any {} with id='{}'".format(topic[:-1], id),
194 HTTPStatus.NOT_FOUND,
195 )
tiernob24258a2018-10-04 18:39:49 +0200196 elif len(desc_list) == 1:
197 return desc_list[0]
198 else:
garciadeblas4568a372021-03-24 09:19:48 +0100199 raise DbException(
200 "Found more than one public {} with id='{}'; and no one belonging to this project".format(
201 topic[:-1], id
202 ),
203 HTTPStatus.CONFLICT,
204 )
tiernob24258a2018-10-04 18:39:49 +0200205
tierno65ca36d2019-02-12 19:27:52 +0100206 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
tiernob24258a2018-10-04 18:39:49 +0200207 """
208 Creates a new almost empty DISABLED entry into database. Due to SOL005, it does not follow normal procedure.
209 Creating a VNFD or NSD is done in two steps: 1. Creates an empty descriptor (this step) and 2) upload content
210 (self.upload_content)
211 :param rollback: list to append created items at database in case a rollback may to be done
tierno65ca36d2019-02-12 19:27:52 +0100212 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200213 :param indata: data to be inserted
214 :param kwargs: used to override the indata descriptor
215 :param headers: http request headers
tiernobdebce92019-07-01 15:36:49 +0000216 :return: _id, None: identity of the inserted data; and None as there is not any operation
tiernob24258a2018-10-04 18:39:49 +0200217 """
218
tiernod7749582020-05-28 10:41:10 +0000219 # No needed to capture exceptions
220 # Check Quota
221 self.check_quota(session)
delacruzramo32bab472019-09-13 12:24:22 +0200222
tiernod7749582020-05-28 10:41:10 +0000223 # _remove_envelop
224 if indata:
225 if "userDefinedData" in indata:
garciadeblas4568a372021-03-24 09:19:48 +0100226 indata = indata["userDefinedData"]
tiernob24258a2018-10-04 18:39:49 +0200227
tiernod7749582020-05-28 10:41:10 +0000228 # Override descriptor with query string kwargs
229 self._update_input_with_kwargs(indata, kwargs)
230 # uncomment when this method is implemented.
231 # Avoid override in this case as the target is userDefinedData, but not vnfd,nsd descriptors
232 # indata = DescriptorTopic._validate_input_new(self, indata, project_id=session["force"])
tiernob24258a2018-10-04 18:39:49 +0200233
garciadeblasf2af4a12023-01-24 16:56:54 +0100234 content = {"_admin": {"userDefinedData": indata, "revision": 0}}
beierlmcee2ebf2022-03-29 17:42:48 -0400235
garciadeblas4568a372021-03-24 09:19:48 +0100236 self.format_on_new(
237 content, session["project_id"], make_public=session["public"]
238 )
tiernod7749582020-05-28 10:41:10 +0000239 _id = self.db.create(self.topic, content)
240 rollback.append({"topic": self.topic, "_id": _id})
241 self._send_msg("created", {"_id": _id})
242 return _id, None
tiernob24258a2018-10-04 18:39:49 +0200243
tierno65ca36d2019-02-12 19:27:52 +0100244 def upload_content(self, session, _id, indata, kwargs, headers):
tiernob24258a2018-10-04 18:39:49 +0200245 """
246 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 +0100247 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200248 :param _id : the nsd,vnfd is already created, this is the id
249 :param indata: http body request
250 :param kwargs: user query string to override parameters. NOT USED
251 :param headers: http request headers
tierno5a5c2182018-11-20 12:27:42 +0000252 :return: True if package is completely uploaded or False if partial content has been uploded
tiernob24258a2018-10-04 18:39:49 +0200253 Raise exception on error
254 """
255 # Check that _id exists and it is valid
256 current_desc = self.show(session, _id)
257
258 content_range_text = headers.get("Content-Range")
259 expected_md5 = headers.get("Content-File-MD5")
garciadeblasdcfa3d62025-06-25 16:44:25 +0200260 digest_header = headers.get("Digest")
tiernob24258a2018-10-04 18:39:49 +0200261 compressed = None
262 content_type = headers.get("Content-Type")
garciadeblas4568a372021-03-24 09:19:48 +0100263 if (
264 content_type
265 and "application/gzip" in content_type
266 or "application/x-gzip" in content_type
garciadeblas4568a372021-03-24 09:19:48 +0100267 ):
tiernob24258a2018-10-04 18:39:49 +0200268 compressed = "gzip"
garciadeblasf2af4a12023-01-24 16:56:54 +0100269 if content_type and "application/zip" in content_type:
bravofc26740a2021-11-08 09:44:54 -0300270 compressed = "zip"
tiernob24258a2018-10-04 18:39:49 +0200271 filename = headers.get("Content-Filename")
bravofc26740a2021-11-08 09:44:54 -0300272 if not filename and compressed:
273 filename = "package.tar.gz" if compressed == "gzip" else "package.zip"
274 elif not filename:
275 filename = "package"
276
beierlmcee2ebf2022-03-29 17:42:48 -0400277 revision = 1
278 if "revision" in current_desc["_admin"]:
279 revision = current_desc["_admin"]["revision"] + 1
280
tiernob24258a2018-10-04 18:39:49 +0200281 # TODO change to Content-Disposition filename https://tools.ietf.org/html/rfc6266
282 file_pkg = None
283 error_text = ""
beierlmbc5a5242022-05-17 21:25:29 -0400284 fs_rollback = []
285
tiernob24258a2018-10-04 18:39:49 +0200286 try:
287 if content_range_text:
garciadeblas4568a372021-03-24 09:19:48 +0100288 content_range = (
289 content_range_text.replace("-", " ").replace("/", " ").split()
290 )
291 if (
292 content_range[0] != "bytes"
293 ): # TODO check x<y not negative < total....
tiernob24258a2018-10-04 18:39:49 +0200294 raise IndexError()
295 start = int(content_range[1])
296 end = int(content_range[2]) + 1
297 total = int(content_range[3])
298 else:
299 start = 0
beierlmcee2ebf2022-03-29 17:42:48 -0400300 # Rather than using a temp folder, we will store the package in a folder based on
301 # the current revision.
302 proposed_revision_path = (
303 _id + ":" + str(revision)
garciadeblas4568a372021-03-24 09:19:48 +0100304 ) # all the content is upload here and if ok, it is rename from id_ to is folder
tiernob24258a2018-10-04 18:39:49 +0200305
306 if start:
beierlmcee2ebf2022-03-29 17:42:48 -0400307 if not self.fs.file_exists(proposed_revision_path, "dir"):
garciadeblas4568a372021-03-24 09:19:48 +0100308 raise EngineException(
309 "invalid Transaction-Id header", HTTPStatus.NOT_FOUND
310 )
tiernob24258a2018-10-04 18:39:49 +0200311 else:
beierlmcee2ebf2022-03-29 17:42:48 -0400312 self.fs.file_delete(proposed_revision_path, ignore_non_exist=True)
313 self.fs.mkdir(proposed_revision_path)
beierlmbc5a5242022-05-17 21:25:29 -0400314 fs_rollback.append(proposed_revision_path)
tiernob24258a2018-10-04 18:39:49 +0200315
316 storage = self.fs.get_params()
beierlmbc5a5242022-05-17 21:25:29 -0400317 storage["folder"] = proposed_revision_path
tiernob24258a2018-10-04 18:39:49 +0200318
beierlmcee2ebf2022-03-29 17:42:48 -0400319 file_path = (proposed_revision_path, filename)
garciadeblas4568a372021-03-24 09:19:48 +0100320 if self.fs.file_exists(file_path, "file"):
tiernob24258a2018-10-04 18:39:49 +0200321 file_size = self.fs.file_size(file_path)
322 else:
323 file_size = 0
324 if file_size != start:
garciadeblas4568a372021-03-24 09:19:48 +0100325 raise EngineException(
326 "invalid Content-Range start sequence, expected '{}' but received '{}'".format(
327 file_size, start
328 ),
329 HTTPStatus.REQUESTED_RANGE_NOT_SATISFIABLE,
330 )
331 file_pkg = self.fs.file_open(file_path, "a+b")
yshah53cc9eb2024-07-05 13:06:31 +0000332
tiernob24258a2018-10-04 18:39:49 +0200333 if isinstance(indata, dict):
334 indata_text = yaml.safe_dump(indata, indent=4, default_flow_style=False)
335 file_pkg.write(indata_text.encode(encoding="utf-8"))
336 else:
337 indata_len = 0
338 while True:
339 indata_text = indata.read(4096)
340 indata_len += len(indata_text)
341 if not indata_text:
342 break
343 file_pkg.write(indata_text)
344 if content_range_text:
garciaale960531a2020-10-20 18:29:45 -0300345 if indata_len != end - start:
garciadeblas4568a372021-03-24 09:19:48 +0100346 raise EngineException(
347 "Mismatch between Content-Range header {}-{} and body length of {}".format(
348 start, end - 1, indata_len
349 ),
350 HTTPStatus.REQUESTED_RANGE_NOT_SATISFIABLE,
351 )
tiernob24258a2018-10-04 18:39:49 +0200352 if end != total:
353 # TODO update to UPLOADING
354 return False
355
356 # PACKAGE UPLOADED
357 if expected_md5:
358 file_pkg.seek(0, 0)
359 file_md5 = md5()
360 chunk_data = file_pkg.read(1024)
361 while chunk_data:
362 file_md5.update(chunk_data)
363 chunk_data = file_pkg.read(1024)
364 if expected_md5 != file_md5.hexdigest():
365 raise EngineException("Error, MD5 mismatch", HTTPStatus.CONFLICT)
garciadeblasdcfa3d62025-06-25 16:44:25 +0200366 if digest_header:
367 alg, b64_digest = digest_header.split("=", 1)
368 if alg.strip().lower() != "sha-384":
369 raise ValueError(f"Unsupported digest algorithm: {alg}")
370 expected_digest = base64.b64decode(b64_digest)
371 # Get real digest
372 file_pkg.seek(0, 0)
373 file_sha384 = sha384()
374 chunk_data = file_pkg.read(1024)
375 while chunk_data:
376 file_sha384.update(chunk_data)
377 chunk_data = file_pkg.read(1024)
378 if expected_digest != file_sha384.digest():
379 raise EngineException("Error, SHA384 mismatch", HTTPStatus.CONFLICT)
tiernob24258a2018-10-04 18:39:49 +0200380 file_pkg.seek(0, 0)
381 if compressed == "gzip":
garciadeblas4568a372021-03-24 09:19:48 +0100382 tar = tarfile.open(mode="r", fileobj=file_pkg)
tiernob24258a2018-10-04 18:39:49 +0200383 descriptor_file_name = None
384 for tarinfo in tar:
385 tarname = tarinfo.name
386 tarname_path = tarname.split("/")
garciadeblas4568a372021-03-24 09:19:48 +0100387 if (
388 not tarname_path[0] or ".." in tarname_path
389 ): # if start with "/" means absolute path
390 raise EngineException(
391 "Absolute path or '..' are not allowed for package descriptor tar.gz"
392 )
tiernob24258a2018-10-04 18:39:49 +0200393 if len(tarname_path) == 1 and not tarinfo.isdir():
garciadeblas4568a372021-03-24 09:19:48 +0100394 raise EngineException(
395 "All files must be inside a dir for package descriptor tar.gz"
396 )
397 if (
398 tarname.endswith(".yaml")
399 or tarname.endswith(".json")
400 or tarname.endswith(".yml")
401 ):
tiernob24258a2018-10-04 18:39:49 +0200402 storage["pkg-dir"] = tarname_path[0]
403 if len(tarname_path) == 2:
404 if descriptor_file_name:
405 raise EngineException(
garciadeblas4568a372021-03-24 09:19:48 +0100406 "Found more than one descriptor file at package descriptor tar.gz"
407 )
tiernob24258a2018-10-04 18:39:49 +0200408 descriptor_file_name = tarname
409 if not descriptor_file_name:
garciadeblas4568a372021-03-24 09:19:48 +0100410 raise EngineException(
411 "Not found any descriptor file at package descriptor tar.gz"
412 )
tiernob24258a2018-10-04 18:39:49 +0200413 storage["descriptor"] = descriptor_file_name
414 storage["zipfile"] = filename
beierlmcee2ebf2022-03-29 17:42:48 -0400415 self.fs.file_extract(tar, proposed_revision_path)
garciadeblas4568a372021-03-24 09:19:48 +0100416 with self.fs.file_open(
beierlmcee2ebf2022-03-29 17:42:48 -0400417 (proposed_revision_path, descriptor_file_name), "r"
garciadeblas4568a372021-03-24 09:19:48 +0100418 ) as descriptor_file:
tiernob24258a2018-10-04 18:39:49 +0200419 content = descriptor_file.read()
bravofc26740a2021-11-08 09:44:54 -0300420 elif compressed == "zip":
421 zipfile = ZipFile(file_pkg)
422 descriptor_file_name = None
423 for package_file in zipfile.infolist():
424 zipfilename = package_file.filename
425 file_path = zipfilename.split("/")
426 if (
427 not file_path[0] or ".." in zipfilename
428 ): # if start with "/" means absolute path
429 raise EngineException(
430 "Absolute path or '..' are not allowed for package descriptor zip"
431 )
432
433 if (
garciadeblasf2af4a12023-01-24 16:56:54 +0100434 zipfilename.endswith(".yaml")
435 or zipfilename.endswith(".json")
436 or zipfilename.endswith(".yml")
437 ) and (
438 zipfilename.find("/") < 0
439 or zipfilename.find("Definitions") >= 0
bravofc26740a2021-11-08 09:44:54 -0300440 ):
441 storage["pkg-dir"] = ""
442 if descriptor_file_name:
443 raise EngineException(
444 "Found more than one descriptor file at package descriptor zip"
445 )
446 descriptor_file_name = zipfilename
447 if not descriptor_file_name:
448 raise EngineException(
449 "Not found any descriptor file at package descriptor zip"
450 )
451 storage["descriptor"] = descriptor_file_name
452 storage["zipfile"] = filename
beierlmcee2ebf2022-03-29 17:42:48 -0400453 self.fs.file_extract(zipfile, proposed_revision_path)
bravofc26740a2021-11-08 09:44:54 -0300454
455 with self.fs.file_open(
beierlmcee2ebf2022-03-29 17:42:48 -0400456 (proposed_revision_path, descriptor_file_name), "r"
bravofc26740a2021-11-08 09:44:54 -0300457 ) as descriptor_file:
458 content = descriptor_file.read()
tiernob24258a2018-10-04 18:39:49 +0200459 else:
460 content = file_pkg.read()
461 storage["descriptor"] = descriptor_file_name = filename
462
463 if descriptor_file_name.endswith(".json"):
464 error_text = "Invalid json format "
465 indata = json.load(content)
466 else:
467 error_text = "Invalid yaml format "
garciadeblas4cd875d2023-02-14 19:05:34 +0100468 indata = yaml.safe_load(content)
tiernob24258a2018-10-04 18:39:49 +0200469
beierlmcee2ebf2022-03-29 17:42:48 -0400470 # Need to close the file package here so it can be copied from the
471 # revision to the current, unrevisioned record
472 if file_pkg:
473 file_pkg.close()
474 file_pkg = None
475
476 # Fetch both the incoming, proposed revision and the original revision so we
477 # can call a validate method to compare them
478 current_revision_path = _id + "/"
479 self.fs.sync(from_path=current_revision_path)
480 self.fs.sync(from_path=proposed_revision_path)
481
482 if revision > 1:
483 try:
484 self._validate_descriptor_changes(
aticig2b5e1232022-08-10 17:30:12 +0300485 _id,
beierlmcee2ebf2022-03-29 17:42:48 -0400486 descriptor_file_name,
487 current_revision_path,
aticig2b5e1232022-08-10 17:30:12 +0300488 proposed_revision_path,
489 )
beierlmcee2ebf2022-03-29 17:42:48 -0400490 except Exception as e:
garciadeblasf2af4a12023-01-24 16:56:54 +0100491 shutil.rmtree(
492 self.fs.path + current_revision_path, ignore_errors=True
493 )
494 shutil.rmtree(
495 self.fs.path + proposed_revision_path, ignore_errors=True
496 )
beierlmcee2ebf2022-03-29 17:42:48 -0400497 # Only delete the new revision. We need to keep the original version in place
498 # as it has not been changed.
499 self.fs.file_delete(proposed_revision_path, ignore_non_exist=True)
500 raise e
501
tiernob24258a2018-10-04 18:39:49 +0200502 indata = self._remove_envelop(indata)
503
504 # Override descriptor with query string kwargs
505 if kwargs:
506 self._update_input_with_kwargs(indata, kwargs)
tiernob24258a2018-10-04 18:39:49 +0200507
beierlmbc5a5242022-05-17 21:25:29 -0400508 current_desc["_admin"]["storage"] = storage
509 current_desc["_admin"]["onboardingState"] = "ONBOARDED"
510 current_desc["_admin"]["operationalState"] = "ENABLED"
511 current_desc["_admin"]["modified"] = time()
512 current_desc["_admin"]["revision"] = revision
513
tiernob24258a2018-10-04 18:39:49 +0200514 deep_update_rfc7396(current_desc, indata)
garciadeblas4568a372021-03-24 09:19:48 +0100515 current_desc = self.check_conflict_on_edit(
516 session, current_desc, indata, _id=_id
517 )
beierlmbc5a5242022-05-17 21:25:29 -0400518
519 # Copy the revision to the active package name by its original id
520 shutil.rmtree(self.fs.path + current_revision_path, ignore_errors=True)
garciadeblasf2af4a12023-01-24 16:56:54 +0100521 os.rename(
522 self.fs.path + proposed_revision_path,
523 self.fs.path + current_revision_path,
524 )
beierlmbc5a5242022-05-17 21:25:29 -0400525 self.fs.file_delete(current_revision_path, ignore_non_exist=True)
526 self.fs.mkdir(current_revision_path)
527 self.fs.reverse_sync(from_path=current_revision_path)
528
529 shutil.rmtree(self.fs.path + _id)
530
tiernob24258a2018-10-04 18:39:49 +0200531 self.db.replace(self.topic, _id, current_desc)
beierlmcee2ebf2022-03-29 17:42:48 -0400532
533 # Store a copy of the package as a point in time revision
534 revision_desc = dict(current_desc)
535 revision_desc["_id"] = _id + ":" + str(revision_desc["_admin"]["revision"])
536 self.db.create(self.topic + "_revisions", revision_desc)
beierlmbc5a5242022-05-17 21:25:29 -0400537 fs_rollback = []
tiernob24258a2018-10-04 18:39:49 +0200538
539 indata["_id"] = _id
K Sai Kiranc96fd692019-10-16 17:50:53 +0530540 self._send_msg("edited", indata)
tiernob24258a2018-10-04 18:39:49 +0200541
542 # TODO if descriptor has changed because kwargs update content and remove cached zip
543 # TODO if zip is not present creates one
544 return True
545
546 except EngineException:
547 raise
548 except IndexError:
garciadeblas4568a372021-03-24 09:19:48 +0100549 raise EngineException(
550 "invalid Content-Range header format. Expected 'bytes start-end/total'",
551 HTTPStatus.REQUESTED_RANGE_NOT_SATISFIABLE,
552 )
tiernob24258a2018-10-04 18:39:49 +0200553 except IOError as e:
garciadeblas4568a372021-03-24 09:19:48 +0100554 raise EngineException(
555 "invalid upload transaction sequence: '{}'".format(e),
556 HTTPStatus.BAD_REQUEST,
557 )
tiernob24258a2018-10-04 18:39:49 +0200558 except tarfile.ReadError as e:
garciadeblas4568a372021-03-24 09:19:48 +0100559 raise EngineException(
560 "invalid file content {}".format(e), HTTPStatus.BAD_REQUEST
561 )
tiernob24258a2018-10-04 18:39:49 +0200562 except (ValueError, yaml.YAMLError) as e:
563 raise EngineException(error_text + str(e))
564 except ValidationError as e:
565 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
566 finally:
567 if file_pkg:
568 file_pkg.close()
beierlmbc5a5242022-05-17 21:25:29 -0400569 for file in fs_rollback:
570 self.fs.file_delete(file, ignore_non_exist=True)
tiernob24258a2018-10-04 18:39:49 +0200571
572 def get_file(self, session, _id, path=None, accept_header=None):
573 """
574 Return the file content of a vnfd or nsd
tierno65ca36d2019-02-12 19:27:52 +0100575 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tierno87006042018-10-24 12:50:20 +0200576 :param _id: Identity of the vnfd, nsd
tiernob24258a2018-10-04 18:39:49 +0200577 :param path: artifact path or "$DESCRIPTOR" or None
578 :param accept_header: Content of Accept header. Must contain applition/zip or/and text/plain
tierno87006042018-10-24 12:50:20 +0200579 :return: opened file plus Accept format or raises an exception
tiernob24258a2018-10-04 18:39:49 +0200580 """
581 accept_text = accept_zip = False
582 if accept_header:
garciadeblas4568a372021-03-24 09:19:48 +0100583 if "text/plain" in accept_header or "*/*" in accept_header:
tiernob24258a2018-10-04 18:39:49 +0200584 accept_text = True
garciadeblas4568a372021-03-24 09:19:48 +0100585 if "application/zip" in accept_header or "*/*" in accept_header:
586 accept_zip = "application/zip"
587 elif "application/gzip" in accept_header:
588 accept_zip = "application/gzip"
tierno87006042018-10-24 12:50:20 +0200589
tiernob24258a2018-10-04 18:39:49 +0200590 if not accept_text and not accept_zip:
garciadeblas4568a372021-03-24 09:19:48 +0100591 raise EngineException(
592 "provide request header 'Accept' with 'application/zip' or 'text/plain'",
593 http_code=HTTPStatus.NOT_ACCEPTABLE,
594 )
tiernob24258a2018-10-04 18:39:49 +0200595
596 content = self.show(session, _id)
597 if content["_admin"]["onboardingState"] != "ONBOARDED":
garciadeblas4568a372021-03-24 09:19:48 +0100598 raise EngineException(
599 "Cannot get content because this resource is not at 'ONBOARDED' state. "
600 "onboardingState is {}".format(content["_admin"]["onboardingState"]),
601 http_code=HTTPStatus.CONFLICT,
602 )
tiernob24258a2018-10-04 18:39:49 +0200603 storage = content["_admin"]["storage"]
garciaale960531a2020-10-20 18:29:45 -0300604 if path is not None and path != "$DESCRIPTOR": # artifacts
selvi.j5be838c2022-08-25 06:24:49 +0000605 if not storage.get("pkg-dir") and not storage.get("folder"):
garciadeblas4568a372021-03-24 09:19:48 +0100606 raise EngineException(
607 "Packages does not contains artifacts",
608 http_code=HTTPStatus.BAD_REQUEST,
609 )
610 if self.fs.file_exists(
611 (storage["folder"], storage["pkg-dir"], *path), "dir"
612 ):
613 folder_content = self.fs.dir_ls(
614 (storage["folder"], storage["pkg-dir"], *path)
615 )
tiernob24258a2018-10-04 18:39:49 +0200616 return folder_content, "text/plain"
617 # TODO manage folders in http
618 else:
garciadeblas4568a372021-03-24 09:19:48 +0100619 return (
620 self.fs.file_open(
621 (storage["folder"], storage["pkg-dir"], *path), "rb"
622 ),
623 "application/octet-stream",
624 )
tiernob24258a2018-10-04 18:39:49 +0200625
626 # pkgtype accept ZIP TEXT -> result
627 # manyfiles yes X -> zip
628 # no yes -> error
629 # onefile yes no -> zip
630 # X yes -> text
tiernoee002752020-08-04 14:14:16 +0000631 contain_many_files = False
garciadeblas4568a372021-03-24 09:19:48 +0100632 if storage.get("pkg-dir"):
tiernoee002752020-08-04 14:14:16 +0000633 # check if there are more than one file in the package, ignoring checksums.txt.
garciadeblas4568a372021-03-24 09:19:48 +0100634 pkg_files = self.fs.dir_ls((storage["folder"], storage["pkg-dir"]))
635 if len(pkg_files) >= 3 or (
636 len(pkg_files) == 2 and "checksums.txt" not in pkg_files
637 ):
tiernoee002752020-08-04 14:14:16 +0000638 contain_many_files = True
639 if accept_text and (not contain_many_files or path == "$DESCRIPTOR"):
garciadeblas4568a372021-03-24 09:19:48 +0100640 return (
641 self.fs.file_open((storage["folder"], storage["descriptor"]), "r"),
642 "text/plain",
643 )
tiernoee002752020-08-04 14:14:16 +0000644 elif contain_many_files and not accept_zip:
garciadeblas4568a372021-03-24 09:19:48 +0100645 raise EngineException(
646 "Packages that contains several files need to be retrieved with 'application/zip'"
647 "Accept header",
648 http_code=HTTPStatus.NOT_ACCEPTABLE,
649 )
tiernob24258a2018-10-04 18:39:49 +0200650 else:
garciadeblas4568a372021-03-24 09:19:48 +0100651 if not storage.get("zipfile"):
tiernob24258a2018-10-04 18:39:49 +0200652 # TODO generate zipfile if not present
garciadeblas4568a372021-03-24 09:19:48 +0100653 raise EngineException(
654 "Only allowed 'text/plain' Accept header for this descriptor. To be solved in "
655 "future versions",
656 http_code=HTTPStatus.NOT_ACCEPTABLE,
657 )
658 return (
659 self.fs.file_open((storage["folder"], storage["zipfile"]), "rb"),
660 accept_zip,
661 )
tiernob24258a2018-10-04 18:39:49 +0200662
garciaale7cbd03c2020-11-27 10:38:35 -0300663 def _remove_yang_prefixes_from_descriptor(self, descriptor):
664 new_descriptor = {}
665 for k, v in descriptor.items():
666 new_v = v
667 if isinstance(v, dict):
668 new_v = self._remove_yang_prefixes_from_descriptor(v)
669 elif isinstance(v, list):
670 new_v = list()
671 for x in v:
672 if isinstance(x, dict):
673 new_v.append(self._remove_yang_prefixes_from_descriptor(x))
674 else:
675 new_v.append(x)
garciadeblas4568a372021-03-24 09:19:48 +0100676 new_descriptor[k.split(":")[-1]] = new_v
garciaale7cbd03c2020-11-27 10:38:35 -0300677 return new_descriptor
678
gcalvino46e4cb82018-10-26 13:10:22 +0200679 def pyangbind_validation(self, item, data, force=False):
garciadeblas4568a372021-03-24 09:19:48 +0100680 raise EngineException(
681 "Not possible to validate '{}' item".format(item),
682 http_code=HTTPStatus.INTERNAL_SERVER_ERROR,
683 )
gcalvino46e4cb82018-10-26 13:10:22 +0200684
Frank Brydendeba68e2020-07-27 13:55:11 +0000685 def _validate_input_edit(self, indata, content, force=False):
686 # not needed to validate with pyangbind becuase it will be validated at check_conflict_on_edit
687 if "_id" in indata:
688 indata.pop("_id")
689 if "_admin" not in indata:
690 indata["_admin"] = {}
691
692 if "operationalState" in indata:
693 if indata["operationalState"] in ("ENABLED", "DISABLED"):
694 indata["_admin"]["operationalState"] = indata.pop("operationalState")
695 else:
garciadeblas4568a372021-03-24 09:19:48 +0100696 raise EngineException(
697 "State '{}' is not a valid operational state".format(
698 indata["operationalState"]
699 ),
700 http_code=HTTPStatus.BAD_REQUEST,
701 )
Frank Brydendeba68e2020-07-27 13:55:11 +0000702
garciadeblas4568a372021-03-24 09:19:48 +0100703 # 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 +0000704 # to preserve current expected behaviour
705 if "userDefinedData" in indata:
706 data = indata.pop("userDefinedData")
gaticid7debb92023-07-31 14:37:32 +0300707 if isinstance(data, dict):
Frank Brydendeba68e2020-07-27 13:55:11 +0000708 indata["_admin"]["userDefinedData"] = data
709 else:
garciadeblas4568a372021-03-24 09:19:48 +0100710 raise EngineException(
711 "userDefinedData should be an object, but is '{}' instead".format(
712 type(data)
713 ),
714 http_code=HTTPStatus.BAD_REQUEST,
715 )
garciaale960531a2020-10-20 18:29:45 -0300716
garciadeblas4568a372021-03-24 09:19:48 +0100717 if (
718 "operationalState" in indata["_admin"]
719 and content["_admin"]["operationalState"]
720 == indata["_admin"]["operationalState"]
721 ):
722 raise EngineException(
723 "operationalState already {}".format(
724 content["_admin"]["operationalState"]
725 ),
726 http_code=HTTPStatus.CONFLICT,
727 )
Frank Brydendeba68e2020-07-27 13:55:11 +0000728
729 return indata
730
aticig2b5e1232022-08-10 17:30:12 +0300731 def _validate_descriptor_changes(
732 self,
733 descriptor_id,
beierlmcee2ebf2022-03-29 17:42:48 -0400734 descriptor_file_name,
735 old_descriptor_directory,
garciadeblasf2af4a12023-01-24 16:56:54 +0100736 new_descriptor_directory,
aticig2b5e1232022-08-10 17:30:12 +0300737 ):
beierlmcee2ebf2022-03-29 17:42:48 -0400738 # Example:
739 # raise EngineException(
740 # "Error in validating new descriptor: <NODE> cannot be modified",
741 # http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
742 # )
743 pass
tiernob24258a2018-10-04 18:39:49 +0200744
garciadeblasf2af4a12023-01-24 16:56:54 +0100745
tiernob24258a2018-10-04 18:39:49 +0200746class VnfdTopic(DescriptorTopic):
747 topic = "vnfds"
748 topic_msg = "vnfd"
749
delacruzramo32bab472019-09-13 12:24:22 +0200750 def __init__(self, db, fs, msg, auth):
751 DescriptorTopic.__init__(self, db, fs, msg, auth)
tiernob24258a2018-10-04 18:39:49 +0200752
garciaale7cbd03c2020-11-27 10:38:35 -0300753 def pyangbind_validation(self, item, data, force=False):
garciaaledf718ae2020-12-03 19:17:28 -0300754 if self._descriptor_data_is_in_old_format(data):
garciadeblas4568a372021-03-24 09:19:48 +0100755 raise EngineException(
756 "ERROR: Unsupported descriptor format. Please, use an ETSI SOL006 descriptor.",
757 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
758 )
garciaale7cbd03c2020-11-27 10:38:35 -0300759 try:
garciaale7cbd03c2020-11-27 10:38:35 -0300760 myvnfd = etsi_nfv_vnfd.etsi_nfv_vnfd()
garciadeblas4568a372021-03-24 09:19:48 +0100761 pybindJSONDecoder.load_ietf_json(
762 {"etsi-nfv-vnfd:vnfd": data},
763 None,
764 None,
765 obj=myvnfd,
766 path_helper=True,
767 skip_unknown=force,
768 )
garciaale7cbd03c2020-11-27 10:38:35 -0300769 out = pybindJSON.dumps(myvnfd, mode="ietf")
770 desc_out = self._remove_envelop(yaml.safe_load(out))
771 desc_out = self._remove_yang_prefixes_from_descriptor(desc_out)
bravof41a52052021-02-17 18:08:01 -0300772 return utils.deep_update_dict(data, desc_out)
garciaale7cbd03c2020-11-27 10:38:35 -0300773 except Exception as e:
garciadeblas4568a372021-03-24 09:19:48 +0100774 raise EngineException(
775 "Error in pyangbind validation: {}".format(str(e)),
776 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
777 )
garciaale7cbd03c2020-11-27 10:38:35 -0300778
tiernob24258a2018-10-04 18:39:49 +0200779 @staticmethod
garciaaledf718ae2020-12-03 19:17:28 -0300780 def _descriptor_data_is_in_old_format(data):
garciadeblas4568a372021-03-24 09:19:48 +0100781 return ("vnfd-catalog" in data) or ("vnfd:vnfd-catalog" in data)
garciaaledf718ae2020-12-03 19:17:28 -0300782
783 @staticmethod
tiernob24258a2018-10-04 18:39:49 +0200784 def _remove_envelop(indata=None):
785 if not indata:
786 return {}
787 clean_indata = indata
garciaale7cbd03c2020-11-27 10:38:35 -0300788
garciadeblas4568a372021-03-24 09:19:48 +0100789 if clean_indata.get("etsi-nfv-vnfd:vnfd"):
790 if not isinstance(clean_indata["etsi-nfv-vnfd:vnfd"], dict):
garciaale7cbd03c2020-11-27 10:38:35 -0300791 raise EngineException("'etsi-nfv-vnfd:vnfd' must be a dict")
garciadeblas4568a372021-03-24 09:19:48 +0100792 clean_indata = clean_indata["etsi-nfv-vnfd:vnfd"]
793 elif clean_indata.get("vnfd"):
794 if not isinstance(clean_indata["vnfd"], dict):
garciaale7cbd03c2020-11-27 10:38:35 -0300795 raise EngineException("'vnfd' must be dict")
garciadeblas4568a372021-03-24 09:19:48 +0100796 clean_indata = clean_indata["vnfd"]
garciaale7cbd03c2020-11-27 10:38:35 -0300797
tiernob24258a2018-10-04 18:39:49 +0200798 return clean_indata
799
tierno65ca36d2019-02-12 19:27:52 +0100800 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
garciadeblas4568a372021-03-24 09:19:48 +0100801 final_content = super().check_conflict_on_edit(
802 session, final_content, edit_content, _id
803 )
tierno36ec8602018-11-02 17:27:11 +0100804
805 # set type of vnfd
806 contains_pdu = False
807 contains_vdu = False
808 for vdu in get_iterable(final_content.get("vdu")):
809 if vdu.get("pdu-type"):
810 contains_pdu = True
811 else:
812 contains_vdu = True
813 if contains_pdu:
814 final_content["_admin"]["type"] = "hnfd" if contains_vdu else "pnfd"
815 elif contains_vdu:
816 final_content["_admin"]["type"] = "vnfd"
817 # if neither vud nor pdu do not fill type
bravofb995ea22021-02-10 10:57:52 -0300818 return final_content
tierno36ec8602018-11-02 17:27:11 +0100819
tiernob4844ab2019-05-23 08:42:12 +0000820 def check_conflict_on_del(self, session, _id, db_content):
tiernob24258a2018-10-04 18:39:49 +0200821 """
822 Check that there is not any NSD that uses this VNFD. Only NSDs belonging to this project are considered. Note
823 that VNFD can be public and be used by NSD of other projects. Also check there are not deployments, or vnfr
824 that uses this vnfd
tierno65ca36d2019-02-12 19:27:52 +0100825 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob4844ab2019-05-23 08:42:12 +0000826 :param _id: vnfd internal id
827 :param db_content: The database content of the _id.
tiernob24258a2018-10-04 18:39:49 +0200828 :return: None or raises EngineException with the conflict
829 """
tierno65ca36d2019-02-12 19:27:52 +0100830 if session["force"]:
tiernob24258a2018-10-04 18:39:49 +0200831 return
tiernob4844ab2019-05-23 08:42:12 +0000832 descriptor = db_content
tiernob24258a2018-10-04 18:39:49 +0200833 descriptor_id = descriptor.get("id")
834 if not descriptor_id: # empty vnfd not uploaded
835 return
836
tierno65ca36d2019-02-12 19:27:52 +0100837 _filter = self._get_project_filter(session)
tiernob24258a2018-10-04 18:39:49 +0200838 # check vnfrs using this vnfd
839 _filter["vnfd-id"] = _id
yshah53cc9eb2024-07-05 13:06:31 +0000840
tiernob24258a2018-10-04 18:39:49 +0200841 if self.db.get_list("vnfrs", _filter):
garciadeblas4568a372021-03-24 09:19:48 +0100842 raise EngineException(
843 "There is at least one VNF instance using this descriptor",
844 http_code=HTTPStatus.CONFLICT,
845 )
tiernob4844ab2019-05-23 08:42:12 +0000846
847 # check NSD referencing this VNFD
tiernob24258a2018-10-04 18:39:49 +0200848 del _filter["vnfd-id"]
garciadeblasf576eb92021-04-18 20:54:13 +0000849 _filter["vnfd-id"] = descriptor_id
yshah53cc9eb2024-07-05 13:06:31 +0000850
tiernob24258a2018-10-04 18:39:49 +0200851 if self.db.get_list("nsds", _filter):
garciadeblas4568a372021-03-24 09:19:48 +0100852 raise EngineException(
853 "There is at least one NS package referencing this descriptor",
854 http_code=HTTPStatus.CONFLICT,
855 )
tiernob24258a2018-10-04 18:39:49 +0200856
gcalvinoa6fe0002019-01-09 13:27:11 +0100857 def _validate_input_new(self, indata, storage_params, force=False):
Frank Bryden19b97522020-07-10 12:32:02 +0000858 indata.pop("onboardingState", None)
859 indata.pop("operationalState", None)
860 indata.pop("usageState", None)
Frank Bryden19b97522020-07-10 12:32:02 +0000861 indata.pop("links", None)
862
Adurti014a6302024-05-08 05:03:01 +0000863 validation = validation_im()
gcalvino46e4cb82018-10-26 13:10:22 +0200864 indata = self.pyangbind_validation("vnfds", indata, force)
gcalvino5e72d152018-10-23 11:46:57 +0200865 # Cross references validation in the descriptor
garciaale7cbd03c2020-11-27 10:38:35 -0300866
867 self.validate_mgmt_interface_connection_point(indata)
gcalvino5e72d152018-10-23 11:46:57 +0200868
869 for vdu in get_iterable(indata.get("vdu")):
garciaale7cbd03c2020-11-27 10:38:35 -0300870 self.validate_vdu_internal_connection_points(vdu)
garciaale960531a2020-10-20 18:29:45 -0300871 self._validate_vdu_cloud_init_in_package(storage_params, vdu, indata)
bravof41a52052021-02-17 18:08:01 -0300872 self._validate_vdu_charms_in_package(storage_params, indata)
garciaale960531a2020-10-20 18:29:45 -0300873
874 self._validate_vnf_charms_in_package(storage_params, indata)
875
garciaale7cbd03c2020-11-27 10:38:35 -0300876 self.validate_external_connection_points(indata)
877 self.validate_internal_virtual_links(indata)
garciaale960531a2020-10-20 18:29:45 -0300878 self.validate_monitoring_params(indata)
879 self.validate_scaling_group_descriptor(indata)
Adurti10f814e2023-11-08 06:26:04 +0000880 self.validate_healing_group_descriptor(indata)
881 self.validate_alarm_group_descriptor(indata)
882 self.validate_storage_compute_descriptor(indata)
Adurti014a6302024-05-08 05:03:01 +0000883 validation.validate_vdu_profile_in_descriptor(indata)
884 validation.validate_instantiation_level_descriptor(indata)
Daniel Arndt00f83aa2023-06-15 16:43:33 +0200885 self.validate_helm_chart(indata)
garciaale960531a2020-10-20 18:29:45 -0300886
887 return indata
888
889 @staticmethod
Daniel Arndt00f83aa2023-06-15 16:43:33 +0200890 def validate_helm_chart(indata):
Gabriel Cuba646773d2023-11-20 01:43:05 -0500891 def is_url(url):
892 result = urlparse(url)
893 return all([result.scheme, result.netloc])
894
Daniel Arndt00f83aa2023-06-15 16:43:33 +0200895 kdus = indata.get("kdu", [])
896 for kdu in kdus:
897 helm_chart_value = kdu.get("helm-chart")
898 if not helm_chart_value:
899 continue
Gabriel Cuba646773d2023-11-20 01:43:05 -0500900 if not (
901 valid_helm_chart_re.match(helm_chart_value) or is_url(helm_chart_value)
902 ):
Daniel Arndt00f83aa2023-06-15 16:43:33 +0200903 raise EngineException(
904 "helm-chart '{}' is not valid".format(helm_chart_value),
905 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
906 )
907
908 @staticmethod
garciaale7cbd03c2020-11-27 10:38:35 -0300909 def validate_mgmt_interface_connection_point(indata):
garciaale960531a2020-10-20 18:29:45 -0300910 if not indata.get("vdu"):
911 return
garciaale7cbd03c2020-11-27 10:38:35 -0300912 if not indata.get("mgmt-cp"):
garciadeblas4568a372021-03-24 09:19:48 +0100913 raise EngineException(
914 "'mgmt-cp' is a mandatory field and it is not defined",
915 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
916 )
garciaale7cbd03c2020-11-27 10:38:35 -0300917
918 for cp in get_iterable(indata.get("ext-cpd")):
919 if cp["id"] == indata["mgmt-cp"]:
920 break
921 else:
garciadeblas4568a372021-03-24 09:19:48 +0100922 raise EngineException(
923 "mgmt-cp='{}' must match an existing ext-cpd".format(indata["mgmt-cp"]),
924 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
925 )
garciaale960531a2020-10-20 18:29:45 -0300926
927 @staticmethod
garciaale7cbd03c2020-11-27 10:38:35 -0300928 def validate_vdu_internal_connection_points(vdu):
929 int_cpds = set()
930 for cpd in get_iterable(vdu.get("int-cpd")):
931 cpd_id = cpd.get("id")
932 if cpd_id and cpd_id in int_cpds:
garciadeblas4568a372021-03-24 09:19:48 +0100933 raise EngineException(
934 "vdu[id='{}']:int-cpd[id='{}'] is already used by other int-cpd".format(
935 vdu["id"], cpd_id
936 ),
937 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
938 )
garciaale7cbd03c2020-11-27 10:38:35 -0300939 int_cpds.add(cpd_id)
940
941 @staticmethod
942 def validate_external_connection_points(indata):
943 all_vdus_int_cpds = set()
944 for vdu in get_iterable(indata.get("vdu")):
945 for int_cpd in get_iterable(vdu.get("int-cpd")):
946 all_vdus_int_cpds.add((vdu.get("id"), int_cpd.get("id")))
947
948 ext_cpds = set()
949 for cpd in get_iterable(indata.get("ext-cpd")):
950 cpd_id = cpd.get("id")
951 if cpd_id and cpd_id in ext_cpds:
garciadeblas4568a372021-03-24 09:19:48 +0100952 raise EngineException(
953 "ext-cpd[id='{}'] is already used by other ext-cpd".format(cpd_id),
954 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
955 )
garciaale7cbd03c2020-11-27 10:38:35 -0300956 ext_cpds.add(cpd_id)
957
958 int_cpd = cpd.get("int-cpd")
959 if int_cpd:
960 if (int_cpd.get("vdu-id"), int_cpd.get("cpd")) not in all_vdus_int_cpds:
garciadeblas4568a372021-03-24 09:19:48 +0100961 raise EngineException(
962 "ext-cpd[id='{}']:int-cpd must match an existing vdu int-cpd".format(
963 cpd_id
964 ),
965 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
966 )
garciaale7cbd03c2020-11-27 10:38:35 -0300967 # TODO: Validate k8s-cluster-net points to a valid k8s-cluster:nets ?
garciaale960531a2020-10-20 18:29:45 -0300968
bravof41a52052021-02-17 18:08:01 -0300969 def _validate_vdu_charms_in_package(self, storage_params, indata):
970 for df in indata["df"]:
garciadeblas4568a372021-03-24 09:19:48 +0100971 if (
972 "lcm-operations-configuration" in df
973 and "operate-vnf-op-config" in df["lcm-operations-configuration"]
974 ):
975 configs = df["lcm-operations-configuration"][
976 "operate-vnf-op-config"
977 ].get("day1-2", [])
garciaale2c4f9ec2021-03-01 11:04:50 -0300978 vdus = df.get("vdu-profile", [])
bravof23258282021-02-22 18:04:40 -0300979 for vdu in vdus:
980 for config in configs:
981 if config["id"] == vdu["id"] and utils.find_in_list(
982 config.get("execution-environment-list", []),
garciadeblas4568a372021-03-24 09:19:48 +0100983 lambda ee: "juju" in ee,
bravof23258282021-02-22 18:04:40 -0300984 ):
garciadeblas4568a372021-03-24 09:19:48 +0100985 if not self._validate_package_folders(
986 storage_params, "charms"
bravofc26740a2021-11-08 09:44:54 -0300987 ) and not self._validate_package_folders(
988 storage_params, "Scripts/charms"
garciadeblas4568a372021-03-24 09:19:48 +0100989 ):
990 raise EngineException(
991 "Charm defined in vnf[id={}] but not present in "
992 "package".format(indata["id"])
993 )
garciaale960531a2020-10-20 18:29:45 -0300994
995 def _validate_vdu_cloud_init_in_package(self, storage_params, vdu, indata):
996 if not vdu.get("cloud-init-file"):
997 return
garciadeblas4568a372021-03-24 09:19:48 +0100998 if not self._validate_package_folders(
999 storage_params, "cloud_init", vdu["cloud-init-file"]
bravofc26740a2021-11-08 09:44:54 -03001000 ) and not self._validate_package_folders(
1001 storage_params, "Scripts/cloud_init", vdu["cloud-init-file"]
garciadeblas4568a372021-03-24 09:19:48 +01001002 ):
1003 raise EngineException(
1004 "Cloud-init defined in vnf[id={}]:vdu[id={}] but not present in "
1005 "package".format(indata["id"], vdu["id"])
1006 )
garciaale960531a2020-10-20 18:29:45 -03001007
1008 def _validate_vnf_charms_in_package(self, storage_params, indata):
bravof41a52052021-02-17 18:08:01 -03001009 # Get VNF configuration through new container
garciadeblas4568a372021-03-24 09:19:48 +01001010 for deployment_flavor in indata.get("df", []):
bravof41a52052021-02-17 18:08:01 -03001011 if "lcm-operations-configuration" not in deployment_flavor:
1012 return
garciadeblas4568a372021-03-24 09:19:48 +01001013 if (
1014 "operate-vnf-op-config"
1015 not in deployment_flavor["lcm-operations-configuration"]
1016 ):
bravof41a52052021-02-17 18:08:01 -03001017 return
garciadeblas4568a372021-03-24 09:19:48 +01001018 for day_1_2_config in deployment_flavor["lcm-operations-configuration"][
1019 "operate-vnf-op-config"
1020 ]["day1-2"]:
bravof41a52052021-02-17 18:08:01 -03001021 if day_1_2_config["id"] == indata["id"]:
bravof23258282021-02-22 18:04:40 -03001022 if utils.find_in_list(
1023 day_1_2_config.get("execution-environment-list", []),
garciadeblas4568a372021-03-24 09:19:48 +01001024 lambda ee: "juju" in ee,
bravof23258282021-02-22 18:04:40 -03001025 ):
bravofc26740a2021-11-08 09:44:54 -03001026 if not self._validate_package_folders(
1027 storage_params, "charms"
1028 ) and not self._validate_package_folders(
1029 storage_params, "Scripts/charms"
1030 ):
garciadeblas4568a372021-03-24 09:19:48 +01001031 raise EngineException(
1032 "Charm defined in vnf[id={}] but not present in "
1033 "package".format(indata["id"])
1034 )
garciaale960531a2020-10-20 18:29:45 -03001035
1036 def _validate_package_folders(self, storage_params, folder, file=None):
bravofc26740a2021-11-08 09:44:54 -03001037 if not storage_params:
1038 return False
1039 elif not storage_params.get("pkg-dir"):
1040 if self.fs.file_exists("{}_".format(storage_params["folder"]), "dir"):
garciadeblasf2af4a12023-01-24 16:56:54 +01001041 f = "{}_/{}".format(storage_params["folder"], folder)
bravofc26740a2021-11-08 09:44:54 -03001042 else:
garciadeblasf2af4a12023-01-24 16:56:54 +01001043 f = "{}/{}".format(storage_params["folder"], folder)
bravofc26740a2021-11-08 09:44:54 -03001044 if file:
1045 return self.fs.file_exists("{}/{}".format(f, file), "file")
1046 else:
bravofc26740a2021-11-08 09:44:54 -03001047 if self.fs.file_exists(f, "dir"):
1048 if self.fs.dir_ls(f):
1049 return True
garciaale960531a2020-10-20 18:29:45 -03001050 return False
1051 else:
garciadeblas4568a372021-03-24 09:19:48 +01001052 if self.fs.file_exists("{}_".format(storage_params["folder"]), "dir"):
1053 f = "{}_/{}/{}".format(
1054 storage_params["folder"], storage_params["pkg-dir"], folder
1055 )
garciaale960531a2020-10-20 18:29:45 -03001056 else:
garciadeblas4568a372021-03-24 09:19:48 +01001057 f = "{}/{}/{}".format(
1058 storage_params["folder"], storage_params["pkg-dir"], folder
1059 )
garciaale960531a2020-10-20 18:29:45 -03001060 if file:
garciadeblas4568a372021-03-24 09:19:48 +01001061 return self.fs.file_exists("{}/{}".format(f, file), "file")
garciaale960531a2020-10-20 18:29:45 -03001062 else:
garciadeblas4568a372021-03-24 09:19:48 +01001063 if self.fs.file_exists(f, "dir"):
garciaale960531a2020-10-20 18:29:45 -03001064 if self.fs.dir_ls(f):
1065 return True
1066 return False
1067
1068 @staticmethod
garciaale7cbd03c2020-11-27 10:38:35 -03001069 def validate_internal_virtual_links(indata):
1070 all_ivld_ids = set()
1071 for ivld in get_iterable(indata.get("int-virtual-link-desc")):
1072 ivld_id = ivld.get("id")
1073 if ivld_id and ivld_id in all_ivld_ids:
garciadeblas4568a372021-03-24 09:19:48 +01001074 raise EngineException(
1075 "Duplicated VLD id in int-virtual-link-desc[id={}]".format(ivld_id),
1076 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1077 )
garciaale960531a2020-10-20 18:29:45 -03001078 else:
garciaale7cbd03c2020-11-27 10:38:35 -03001079 all_ivld_ids.add(ivld_id)
garciaale960531a2020-10-20 18:29:45 -03001080
garciaale7cbd03c2020-11-27 10:38:35 -03001081 for vdu in get_iterable(indata.get("vdu")):
1082 for int_cpd in get_iterable(vdu.get("int-cpd")):
1083 int_cpd_ivld_id = int_cpd.get("int-virtual-link-desc")
1084 if int_cpd_ivld_id and int_cpd_ivld_id not in all_ivld_ids:
1085 raise EngineException(
1086 "vdu[id='{}']:int-cpd[id='{}']:int-virtual-link-desc='{}' must match an existing "
garciadeblas4568a372021-03-24 09:19:48 +01001087 "int-virtual-link-desc".format(
1088 vdu["id"], int_cpd["id"], int_cpd_ivld_id
1089 ),
1090 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1091 )
garciaale960531a2020-10-20 18:29:45 -03001092
garciaale7cbd03c2020-11-27 10:38:35 -03001093 for df in get_iterable(indata.get("df")):
1094 for vlp in get_iterable(df.get("virtual-link-profile")):
1095 vlp_ivld_id = vlp.get("id")
1096 if vlp_ivld_id and vlp_ivld_id not in all_ivld_ids:
garciadeblas4568a372021-03-24 09:19:48 +01001097 raise EngineException(
1098 "df[id='{}']:virtual-link-profile='{}' must match an existing "
1099 "int-virtual-link-desc".format(df["id"], vlp_ivld_id),
1100 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1101 )
garciaale7cbd03c2020-11-27 10:38:35 -03001102
garciaale960531a2020-10-20 18:29:45 -03001103 @staticmethod
1104 def validate_monitoring_params(indata):
garciaale7cbd03c2020-11-27 10:38:35 -03001105 all_monitoring_params = set()
1106 for ivld in get_iterable(indata.get("int-virtual-link-desc")):
1107 for mp in get_iterable(ivld.get("monitoring-parameters")):
1108 mp_id = mp.get("id")
1109 if mp_id and mp_id in all_monitoring_params:
garciadeblas4568a372021-03-24 09:19:48 +01001110 raise EngineException(
1111 "Duplicated monitoring-parameter id in "
1112 "int-virtual-link-desc[id='{}']:monitoring-parameters[id='{}']".format(
1113 ivld["id"], mp_id
1114 ),
1115 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1116 )
gcalvino5e72d152018-10-23 11:46:57 +02001117 else:
garciaale7cbd03c2020-11-27 10:38:35 -03001118 all_monitoring_params.add(mp_id)
1119
1120 for vdu in get_iterable(indata.get("vdu")):
1121 for mp in get_iterable(vdu.get("monitoring-parameter")):
1122 mp_id = mp.get("id")
1123 if mp_id and mp_id in all_monitoring_params:
garciadeblas4568a372021-03-24 09:19:48 +01001124 raise EngineException(
1125 "Duplicated monitoring-parameter id in "
1126 "vdu[id='{}']:monitoring-parameter[id='{}']".format(
1127 vdu["id"], mp_id
1128 ),
1129 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1130 )
garciaale7cbd03c2020-11-27 10:38:35 -03001131 else:
1132 all_monitoring_params.add(mp_id)
1133
1134 for df in get_iterable(indata.get("df")):
1135 for mp in get_iterable(df.get("monitoring-parameter")):
1136 mp_id = mp.get("id")
1137 if mp_id and mp_id in all_monitoring_params:
garciadeblas4568a372021-03-24 09:19:48 +01001138 raise EngineException(
1139 "Duplicated monitoring-parameter id in "
1140 "df[id='{}']:monitoring-parameter[id='{}']".format(
1141 df["id"], mp_id
1142 ),
1143 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1144 )
garciaale7cbd03c2020-11-27 10:38:35 -03001145 else:
1146 all_monitoring_params.add(mp_id)
gcalvino5e72d152018-10-23 11:46:57 +02001147
garciaale960531a2020-10-20 18:29:45 -03001148 @staticmethod
1149 def validate_scaling_group_descriptor(indata):
garciaale7cbd03c2020-11-27 10:38:35 -03001150 all_monitoring_params = set()
Adurti10f814e2023-11-08 06:26:04 +00001151 all_vdu_ids = set()
1152 for df in get_iterable(indata.get("df")):
1153 for il in get_iterable(df.get("instantiation-level")):
1154 for vl in get_iterable(il.get("vdu-level")):
1155 all_vdu_ids.add(vl.get("vdu-id"))
1156
garciaale7cbd03c2020-11-27 10:38:35 -03001157 for ivld in get_iterable(indata.get("int-virtual-link-desc")):
1158 for mp in get_iterable(ivld.get("monitoring-parameters")):
1159 all_monitoring_params.add(mp.get("id"))
1160
1161 for vdu in get_iterable(indata.get("vdu")):
1162 for mp in get_iterable(vdu.get("monitoring-parameter")):
1163 all_monitoring_params.add(mp.get("id"))
1164
1165 for df in get_iterable(indata.get("df")):
1166 for mp in get_iterable(df.get("monitoring-parameter")):
1167 all_monitoring_params.add(mp.get("id"))
1168
1169 for df in get_iterable(indata.get("df")):
1170 for sa in get_iterable(df.get("scaling-aspect")):
Adurti10f814e2023-11-08 06:26:04 +00001171 for deltas in get_iterable(
1172 sa.get("aspect-delta-details").get("deltas")
1173 ):
1174 for vds in get_iterable(deltas.get("vdu-delta")):
1175 sa_vdu_id = vds.get("id")
1176 if sa_vdu_id and sa_vdu_id not in all_vdu_ids:
1177 raise EngineException(
1178 "df[id='{}']:scaling-aspect[id='{}']:aspect-delta-details"
1179 "[delta='{}']: "
1180 "vdu-id='{}' not defined in vdu".format(
1181 df["id"],
1182 sa["id"],
1183 deltas["id"],
1184 sa_vdu_id,
1185 ),
1186 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1187 )
1188
1189 for df in get_iterable(indata.get("df")):
1190 for sa in get_iterable(df.get("scaling-aspect")):
garciaale7cbd03c2020-11-27 10:38:35 -03001191 for sp in get_iterable(sa.get("scaling-policy")):
1192 for sc in get_iterable(sp.get("scaling-criteria")):
1193 sc_monitoring_param = sc.get("vnf-monitoring-param-ref")
garciadeblas4568a372021-03-24 09:19:48 +01001194 if (
1195 sc_monitoring_param
1196 and sc_monitoring_param not in all_monitoring_params
1197 ):
1198 raise EngineException(
1199 "df[id='{}']:scaling-aspect[id='{}']:scaling-policy"
1200 "[name='{}']:scaling-criteria[name='{}']: "
1201 "vnf-monitoring-param-ref='{}' not defined in any monitoring-param".format(
1202 df["id"],
1203 sa["id"],
1204 sp["name"],
1205 sc["name"],
1206 sc_monitoring_param,
1207 ),
1208 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1209 )
garciaale7cbd03c2020-11-27 10:38:35 -03001210
1211 for sca in get_iterable(sa.get("scaling-config-action")):
garciadeblas4568a372021-03-24 09:19:48 +01001212 if (
1213 "lcm-operations-configuration" not in df
1214 or "operate-vnf-op-config"
1215 not in df["lcm-operations-configuration"]
bravof41a52052021-02-17 18:08:01 -03001216 or not utils.find_in_list(
garciadeblas4568a372021-03-24 09:19:48 +01001217 df["lcm-operations-configuration"][
1218 "operate-vnf-op-config"
1219 ].get("day1-2", []),
1220 lambda config: config["id"] == indata["id"],
1221 )
bravof41a52052021-02-17 18:08:01 -03001222 ):
garciadeblas4568a372021-03-24 09:19:48 +01001223 raise EngineException(
1224 "'day1-2 configuration' not defined in the descriptor but it is "
1225 "referenced by df[id='{}']:scaling-aspect[id='{}']:scaling-config-action".format(
1226 df["id"], sa["id"]
1227 ),
1228 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1229 )
1230 for configuration in get_iterable(
1231 df["lcm-operations-configuration"]["operate-vnf-op-config"].get(
1232 "day1-2", []
1233 )
1234 ):
1235 for primitive in get_iterable(
1236 configuration.get("config-primitive")
1237 ):
1238 if (
1239 primitive["name"]
1240 == sca["vnf-config-primitive-name-ref"]
1241 ):
garciaale7cbd03c2020-11-27 10:38:35 -03001242 break
1243 else:
garciadeblas4568a372021-03-24 09:19:48 +01001244 raise EngineException(
1245 "df[id='{}']:scaling-aspect[id='{}']:scaling-config-action:vnf-"
1246 "config-primitive-name-ref='{}' does not match any "
1247 "day1-2 configuration:config-primitive:name".format(
1248 df["id"],
1249 sa["id"],
1250 sca["vnf-config-primitive-name-ref"],
1251 ),
1252 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1253 )
gcalvinoa6fe0002019-01-09 13:27:11 +01001254
Adurti10f814e2023-11-08 06:26:04 +00001255 @staticmethod
1256 def validate_healing_group_descriptor(indata):
1257 all_vdu_ids = set()
1258 for df in get_iterable(indata.get("df")):
1259 for il in get_iterable(df.get("instantiation-level")):
1260 for vl in get_iterable(il.get("vdu-level")):
1261 all_vdu_ids.add(vl.get("vdu-id"))
1262
1263 for df in get_iterable(indata.get("df")):
1264 for ha in get_iterable(df.get("healing-aspect")):
1265 for hp in get_iterable(ha.get("healing-policy")):
1266 hp_monitoring_param = hp.get("vdu-id")
1267 if hp_monitoring_param and hp_monitoring_param not in all_vdu_ids:
1268 raise EngineException(
1269 "df[id='{}']:healing-aspect[id='{}']:healing-policy"
1270 "[name='{}']: "
1271 "vdu-id='{}' not defined in vdu".format(
1272 df["id"],
1273 ha["id"],
1274 hp["event-name"],
1275 hp_monitoring_param,
1276 ),
1277 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1278 )
1279
1280 @staticmethod
1281 def validate_alarm_group_descriptor(indata):
1282 all_monitoring_params = set()
1283 for ivld in get_iterable(indata.get("int-virtual-link-desc")):
1284 for mp in get_iterable(ivld.get("monitoring-parameters")):
1285 all_monitoring_params.add(mp.get("id"))
1286
1287 for vdu in get_iterable(indata.get("vdu")):
1288 for mp in get_iterable(vdu.get("monitoring-parameter")):
1289 all_monitoring_params.add(mp.get("id"))
1290
1291 for df in get_iterable(indata.get("df")):
1292 for mp in get_iterable(df.get("monitoring-parameter")):
1293 all_monitoring_params.add(mp.get("id"))
1294
1295 for vdus in get_iterable(indata.get("vdu")):
1296 for alarms in get_iterable(vdus.get("alarm")):
1297 alarm_monitoring_param = alarms.get("vnf-monitoring-param-ref")
1298 if (
1299 alarm_monitoring_param
1300 and alarm_monitoring_param not in all_monitoring_params
1301 ):
1302 raise EngineException(
1303 "vdu[id='{}']:alarm[id='{}']:"
1304 "vnf-monitoring-param-ref='{}' not defined in any monitoring-param".format(
1305 vdus["id"],
1306 alarms["alarm-id"],
1307 alarm_monitoring_param,
1308 ),
1309 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1310 )
1311
1312 @staticmethod
1313 def validate_storage_compute_descriptor(indata):
1314 all_vsd_ids = set()
1315 for vsd in get_iterable(indata.get("virtual-storage-desc")):
1316 all_vsd_ids.add(vsd.get("id"))
1317
1318 all_vcd_ids = set()
1319 for vcd in get_iterable(indata.get("virtual-compute-desc")):
1320 all_vcd_ids.add(vcd.get("id"))
1321
1322 for vdus in get_iterable(indata.get("vdu")):
1323 for vsd_ref in vdus.get("virtual-storage-desc"):
1324 if vsd_ref and vsd_ref not in all_vsd_ids:
1325 raise EngineException(
1326 "vdu[virtual-storage-desc='{}']"
1327 "not defined in vnfd".format(
1328 vsd_ref,
1329 ),
1330 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1331 )
1332
1333 for vdus in get_iterable(indata.get("vdu")):
1334 vcd_ref = vdus.get("virtual-compute-desc")
1335 if vcd_ref and vcd_ref not in all_vcd_ids:
1336 raise EngineException(
1337 "vdu[virtual-compute-desc='{}']"
1338 "not defined in vnfd".format(
1339 vdus["virtual-compute-desc"],
1340 ),
1341 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1342 )
1343
delacruzramo271d2002019-12-02 21:00:37 +01001344 def delete_extra(self, session, _id, db_content, not_send_msg=None):
1345 """
1346 Deletes associate file system storage (via super)
1347 Deletes associated vnfpkgops from database.
1348 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1349 :param _id: server internal id
1350 :param db_content: The database content of the descriptor
1351 :return: None
1352 :raises: FsException in case of error while deleting associated storage
1353 """
1354 super().delete_extra(session, _id, db_content, not_send_msg)
1355 self.db.del_list("vnfpkgops", {"vnfPkgId": _id})
garciadeblasf2af4a12023-01-24 16:56:54 +01001356 self.db.del_list(self.topic + "_revisions", {"_id": {"$regex": _id}})
garciaale960531a2020-10-20 18:29:45 -03001357
Frank Bryden19b97522020-07-10 12:32:02 +00001358 def sol005_projection(self, data):
1359 data["onboardingState"] = data["_admin"]["onboardingState"]
1360 data["operationalState"] = data["_admin"]["operationalState"]
1361 data["usageState"] = data["_admin"]["usageState"]
1362
1363 links = {}
1364 links["self"] = {"href": "/vnfpkgm/v1/vnf_packages/{}".format(data["_id"])}
1365 links["vnfd"] = {"href": "/vnfpkgm/v1/vnf_packages/{}/vnfd".format(data["_id"])}
garciadeblas4568a372021-03-24 09:19:48 +01001366 links["packageContent"] = {
1367 "href": "/vnfpkgm/v1/vnf_packages/{}/package_content".format(data["_id"])
1368 }
Frank Bryden19b97522020-07-10 12:32:02 +00001369 data["_links"] = links
garciaale960531a2020-10-20 18:29:45 -03001370
Frank Bryden19b97522020-07-10 12:32:02 +00001371 return super().sol005_projection(data)
delacruzramo271d2002019-12-02 21:00:37 +01001372
aticig9cfa8162022-04-07 11:57:18 +03001373 @staticmethod
1374 def find_software_version(vnfd: dict) -> str:
1375 """Find the sotware version in the VNFD descriptors
1376
1377 Args:
1378 vnfd (dict): Descriptor as a dictionary
1379
1380 Returns:
1381 software-version (str)
1382 """
1383 default_sw_version = "1.0"
1384 if vnfd.get("vnfd"):
1385 vnfd = vnfd["vnfd"]
1386 if vnfd.get("software-version"):
1387 return vnfd["software-version"]
1388 else:
1389 return default_sw_version
1390
1391 @staticmethod
1392 def extract_policies(vnfd: dict) -> dict:
1393 """Removes the policies from the VNFD descriptors
1394
1395 Args:
1396 vnfd (dict): Descriptor as a dictionary
1397
1398 Returns:
1399 vnfd (dict): VNFD which does not include policies
1400 """
elumalai3622f832022-07-08 12:06:27 +05301401 for df in vnfd.get("df", {}):
1402 for policy in ["scaling-aspect", "healing-aspect"]:
garciadeblasf2af4a12023-01-24 16:56:54 +01001403 if df.get(policy, {}):
elumalai3622f832022-07-08 12:06:27 +05301404 df.pop(policy)
1405 for vdu in vnfd.get("vdu", {}):
1406 for alarm_policy in ["alarm", "monitoring-parameter"]:
garciadeblasf2af4a12023-01-24 16:56:54 +01001407 if vdu.get(alarm_policy, {}):
elumalai3622f832022-07-08 12:06:27 +05301408 vdu.pop(alarm_policy)
aticig9cfa8162022-04-07 11:57:18 +03001409 return vnfd
1410
1411 @staticmethod
1412 def extract_day12_primitives(vnfd: dict) -> dict:
1413 """Removes the day12 primitives from the VNFD descriptors
1414
1415 Args:
1416 vnfd (dict): Descriptor as a dictionary
1417
1418 Returns:
1419 vnfd (dict)
1420 """
1421 for df_id, df in enumerate(vnfd.get("df", {})):
1422 if (
1423 df.get("lcm-operations-configuration", {})
1424 .get("operate-vnf-op-config", {})
1425 .get("day1-2")
1426 ):
1427 day12 = df["lcm-operations-configuration"]["operate-vnf-op-config"].get(
1428 "day1-2"
1429 )
1430 for config_id, config in enumerate(day12):
1431 for key in [
1432 "initial-config-primitive",
1433 "config-primitive",
1434 "terminate-config-primitive",
1435 ]:
1436 config.pop(key, None)
1437 day12[config_id] = config
1438 df["lcm-operations-configuration"]["operate-vnf-op-config"][
1439 "day1-2"
1440 ] = day12
1441 vnfd["df"][df_id] = df
1442 return vnfd
1443
1444 def remove_modifiable_items(self, vnfd: dict) -> dict:
1445 """Removes the modifiable parts from the VNFD descriptors
1446
1447 It calls different extract functions according to different update types
1448 to clear all the modifiable items from VNFD
1449
1450 Args:
1451 vnfd (dict): Descriptor as a dictionary
1452
1453 Returns:
1454 vnfd (dict): Descriptor which does not include modifiable contents
1455 """
1456 if vnfd.get("vnfd"):
1457 vnfd = vnfd["vnfd"]
1458 vnfd.pop("_admin", None)
1459 # If the other extractions need to be done from VNFD,
1460 # the new extract methods could be appended to below list.
1461 for extract_function in [self.extract_day12_primitives, self.extract_policies]:
1462 vnfd_temp = extract_function(vnfd)
1463 vnfd = vnfd_temp
1464 return vnfd
1465
1466 def _validate_descriptor_changes(
1467 self,
aticig2b5e1232022-08-10 17:30:12 +03001468 descriptor_id: str,
aticig9cfa8162022-04-07 11:57:18 +03001469 descriptor_file_name: str,
1470 old_descriptor_directory: str,
1471 new_descriptor_directory: str,
1472 ):
1473 """Compares the old and new VNFD descriptors and validates the new descriptor.
1474
1475 Args:
1476 old_descriptor_directory (str): Directory of descriptor which is in-use
aticig2b5e1232022-08-10 17:30:12 +03001477 new_descriptor_directory (str): Directory of descriptor which is proposed to update (new revision)
aticig9cfa8162022-04-07 11:57:18 +03001478
1479 Returns:
1480 None
1481
1482 Raises:
1483 EngineException: In case of error when there are unallowed changes
1484 """
1485 try:
aticig2b5e1232022-08-10 17:30:12 +03001486 # If VNFD does not exist in DB or it is not in use by any NS,
1487 # validation is not required.
1488 vnfd = self.db.get_one("vnfds", {"_id": descriptor_id})
1489 if not vnfd or not detect_descriptor_usage(vnfd, "vnfds", self.db):
1490 return
1491
1492 # Get the old and new descriptor contents in order to compare them.
aticig9cfa8162022-04-07 11:57:18 +03001493 with self.fs.file_open(
1494 (old_descriptor_directory.rstrip("/"), descriptor_file_name), "r"
1495 ) as old_descriptor_file:
1496 with self.fs.file_open(
aticig2b5e1232022-08-10 17:30:12 +03001497 (new_descriptor_directory.rstrip("/"), descriptor_file_name), "r"
aticig9cfa8162022-04-07 11:57:18 +03001498 ) as new_descriptor_file:
aticig2b5e1232022-08-10 17:30:12 +03001499 old_content = yaml.safe_load(old_descriptor_file.read())
1500 new_content = yaml.safe_load(new_descriptor_file.read())
1501
1502 # If software version has changed, we do not need to validate
1503 # the differences anymore.
aticig9cfa8162022-04-07 11:57:18 +03001504 if old_content and new_content:
1505 if self.find_software_version(
1506 old_content
1507 ) != self.find_software_version(new_content):
1508 return
aticig2b5e1232022-08-10 17:30:12 +03001509
aticig9cfa8162022-04-07 11:57:18 +03001510 disallowed_change = DeepDiff(
1511 self.remove_modifiable_items(old_content),
1512 self.remove_modifiable_items(new_content),
1513 )
aticig2b5e1232022-08-10 17:30:12 +03001514
aticig9cfa8162022-04-07 11:57:18 +03001515 if disallowed_change:
1516 changed_nodes = functools.reduce(
1517 lambda a, b: a + " , " + b,
1518 [
1519 node.lstrip("root")
1520 for node in disallowed_change.get(
1521 "values_changed"
1522 ).keys()
1523 ],
1524 )
aticig2b5e1232022-08-10 17:30:12 +03001525
aticig9cfa8162022-04-07 11:57:18 +03001526 raise EngineException(
1527 f"Error in validating new descriptor: {changed_nodes} cannot be modified, "
1528 "there are disallowed changes in the vnf descriptor.",
1529 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1530 )
1531 except (
1532 DbException,
1533 AttributeError,
1534 IndexError,
1535 KeyError,
1536 ValueError,
1537 ) as e:
1538 raise type(e)(
1539 "VNF Descriptor could not be processed with error: {}.".format(e)
1540 )
1541
tiernob24258a2018-10-04 18:39:49 +02001542
1543class NsdTopic(DescriptorTopic):
1544 topic = "nsds"
1545 topic_msg = "nsd"
1546
delacruzramo32bab472019-09-13 12:24:22 +02001547 def __init__(self, db, fs, msg, auth):
Daniel Arndt00f83aa2023-06-15 16:43:33 +02001548 super().__init__(db, fs, msg, auth)
tiernob24258a2018-10-04 18:39:49 +02001549
garciaale7cbd03c2020-11-27 10:38:35 -03001550 def pyangbind_validation(self, item, data, force=False):
garciaaledf718ae2020-12-03 19:17:28 -03001551 if self._descriptor_data_is_in_old_format(data):
garciadeblas4568a372021-03-24 09:19:48 +01001552 raise EngineException(
1553 "ERROR: Unsupported descriptor format. Please, use an ETSI SOL006 descriptor.",
1554 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1555 )
garciaale7cbd03c2020-11-27 10:38:35 -03001556 try:
garciadeblas4568a372021-03-24 09:19:48 +01001557 nsd_vnf_profiles = data.get("df", [{}])[0].get("vnf-profile", [])
garciaale7cbd03c2020-11-27 10:38:35 -03001558 mynsd = etsi_nfv_nsd.etsi_nfv_nsd()
garciadeblas4568a372021-03-24 09:19:48 +01001559 pybindJSONDecoder.load_ietf_json(
1560 {"nsd": {"nsd": [data]}},
1561 None,
1562 None,
1563 obj=mynsd,
1564 path_helper=True,
1565 skip_unknown=force,
1566 )
garciaale7cbd03c2020-11-27 10:38:35 -03001567 out = pybindJSON.dumps(mynsd, mode="ietf")
1568 desc_out = self._remove_envelop(yaml.safe_load(out))
1569 desc_out = self._remove_yang_prefixes_from_descriptor(desc_out)
garciaale341ac1b2020-12-11 20:04:11 -03001570 if nsd_vnf_profiles:
garciadeblas4568a372021-03-24 09:19:48 +01001571 desc_out["df"][0]["vnf-profile"] = nsd_vnf_profiles
garciaale7cbd03c2020-11-27 10:38:35 -03001572 return desc_out
1573 except Exception as e:
garciadeblas4568a372021-03-24 09:19:48 +01001574 raise EngineException(
1575 "Error in pyangbind validation: {}".format(str(e)),
1576 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1577 )
garciaale7cbd03c2020-11-27 10:38:35 -03001578
tiernob24258a2018-10-04 18:39:49 +02001579 @staticmethod
garciaaledf718ae2020-12-03 19:17:28 -03001580 def _descriptor_data_is_in_old_format(data):
garciadeblas4568a372021-03-24 09:19:48 +01001581 return ("nsd-catalog" in data) or ("nsd:nsd-catalog" in data)
garciaaledf718ae2020-12-03 19:17:28 -03001582
1583 @staticmethod
tiernob24258a2018-10-04 18:39:49 +02001584 def _remove_envelop(indata=None):
1585 if not indata:
1586 return {}
1587 clean_indata = indata
1588
garciadeblas4568a372021-03-24 09:19:48 +01001589 if clean_indata.get("nsd"):
1590 clean_indata = clean_indata["nsd"]
1591 elif clean_indata.get("etsi-nfv-nsd:nsd"):
1592 clean_indata = clean_indata["etsi-nfv-nsd:nsd"]
1593 if clean_indata.get("nsd"):
1594 if (
1595 not isinstance(clean_indata["nsd"], list)
1596 or len(clean_indata["nsd"]) != 1
1597 ):
gcalvino46e4cb82018-10-26 13:10:22 +02001598 raise EngineException("'nsd' must be a list of only one element")
garciadeblas4568a372021-03-24 09:19:48 +01001599 clean_indata = clean_indata["nsd"][0]
tiernob24258a2018-10-04 18:39:49 +02001600 return clean_indata
1601
gcalvinoa6fe0002019-01-09 13:27:11 +01001602 def _validate_input_new(self, indata, storage_params, force=False):
Frank Bryden19b97522020-07-10 12:32:02 +00001603 indata.pop("nsdOnboardingState", None)
1604 indata.pop("nsdOperationalState", None)
1605 indata.pop("nsdUsageState", None)
1606
1607 indata.pop("links", None)
1608
gcalvino46e4cb82018-10-26 13:10:22 +02001609 indata = self.pyangbind_validation("nsds", indata, force)
tierno5a5c2182018-11-20 12:27:42 +00001610 # Cross references validation in the descriptor
tiernoaa1ca7b2018-11-08 19:00:20 +01001611 # 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 -03001612 for vld in get_iterable(indata.get("virtual-link-desc")):
1613 self.validate_vld_mgmt_network_with_virtual_link_protocol_data(vld, indata)
selvi.j828f3f22023-05-16 05:43:48 +00001614 for fg in get_iterable(indata.get("vnffgd")):
1615 self.validate_vnffgd_data(fg, indata)
garciaale960531a2020-10-20 18:29:45 -03001616
garciaale7cbd03c2020-11-27 10:38:35 -03001617 self.validate_vnf_profiles_vnfd_id(indata)
garciaale960531a2020-10-20 18:29:45 -03001618
tiernob24258a2018-10-04 18:39:49 +02001619 return indata
1620
garciaale960531a2020-10-20 18:29:45 -03001621 @staticmethod
garciaale7cbd03c2020-11-27 10:38:35 -03001622 def validate_vld_mgmt_network_with_virtual_link_protocol_data(vld, indata):
1623 if not vld.get("mgmt-network"):
1624 return
1625 vld_id = vld.get("id")
1626 for df in get_iterable(indata.get("df")):
1627 for vlp in get_iterable(df.get("virtual-link-profile")):
1628 if vld_id and vld_id == vlp.get("virtual-link-desc-id"):
1629 if vlp.get("virtual-link-protocol-data"):
garciadeblas4568a372021-03-24 09:19:48 +01001630 raise EngineException(
1631 "Error at df[id='{}']:virtual-link-profile[id='{}']:virtual-link-"
1632 "protocol-data You cannot set a virtual-link-protocol-data "
1633 "when mgmt-network is True".format(df["id"], vlp["id"]),
1634 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1635 )
garciaale960531a2020-10-20 18:29:45 -03001636
1637 @staticmethod
selvi.j828f3f22023-05-16 05:43:48 +00001638 def validate_vnffgd_data(fg, indata):
1639 position_list = []
1640 all_vnf_ids = set(get_iterable(fg.get("vnf-profile-id")))
1641 for fgposition in get_iterable(fg.get("nfp-position-element")):
1642 position_list.append(fgposition["id"])
1643
1644 for nfpd in get_iterable(fg.get("nfpd")):
1645 nfp_position = []
1646 for position in get_iterable(nfpd.get("position-desc-id")):
1647 nfp_position = position.get("nfp-position-element-id")
1648 if position == "nfp-position-element-id":
1649 nfp_position = position.get("nfp-position-element-id")
1650 if nfp_position[0] not in position_list:
1651 raise EngineException(
1652 "Error at vnffgd nfpd[id='{}']:nfp-position-element-id='{}' "
1653 "does not match any nfp-position-element".format(
1654 nfpd["id"], nfp_position[0]
1655 ),
1656 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1657 )
1658
1659 for cp in get_iterable(position.get("cp-profile-id")):
1660 for cpe in get_iterable(cp.get("constituent-profile-elements")):
1661 constituent_base_element_id = cpe.get(
1662 "constituent-base-element-id"
1663 )
1664 if (
1665 constituent_base_element_id
1666 and constituent_base_element_id not in all_vnf_ids
1667 ):
1668 raise EngineException(
1669 "Error at vnffgd constituent_profile[id='{}']:vnfd-id='{}' "
1670 "does not match any constituent-base-element-id".format(
1671 cpe["id"], constituent_base_element_id
1672 ),
1673 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1674 )
Adurti809a7cd2024-05-07 10:37:30 +00001675 vnf_ip_list = set()
1676 for ma in get_iterable(position.get("match-attributes")):
1677 ma_source_ip = ma.get("source-ip-address")
1678 ma_dest_ip = ma.get("destination-ip-address")
1679 ma_vp_id = ""
1680 if ma_source_ip and ma_dest_ip:
1681 if ma_source_ip == ma_dest_ip:
1682 raise EngineException(
1683 "Error at vnffgd match-attributes:source-ip-address and destination-ip-address should not match",
1684 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1685 )
1686 if ma_source_ip:
1687 for df in get_iterable(indata.get("df")):
1688 for vp in get_iterable(df.get("vnf-profile")):
1689 for vlc in get_iterable(
1690 vp.get("virtual-link-connectivity")
1691 ):
1692 for cpd in get_iterable(
1693 vlc.get("constituent-cpd-id")
1694 ):
1695 vnf_ip_list.add(cpd.get("ip-address"))
1696 if ma_source_ip == cpd.get("ip-address"):
1697 ma_vp_id = vp.get("id")
1698 if ma_source_ip not in vnf_ip_list:
1699 raise EngineException(
1700 "Error at vnffgd match-attributes:source-ip-address='{}' "
1701 "does not match any ip-address".format(ma_source_ip),
1702 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1703 )
1704 if ma_dest_ip not in vnf_ip_list:
1705 raise EngineException(
1706 "Error at vnffgd match-attributes:destination-ip-address='{}' "
1707 "does not match any ip-address".format(ma_dest_ip),
1708 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1709 )
1710 constituent_base_element_id = ma.get("constituent-base-element-id")
1711 if (
1712 constituent_base_element_id
1713 and constituent_base_element_id not in ma_vp_id
1714 ):
1715 raise EngineException(
1716 "Error at vnffgd match-attributes:vnfd-id='{}' "
1717 "does not match source constituent-base-element-id".format(
1718 constituent_base_element_id
1719 ),
1720 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1721 )
selvi.j828f3f22023-05-16 05:43:48 +00001722
1723 @staticmethod
garciaale7cbd03c2020-11-27 10:38:35 -03001724 def validate_vnf_profiles_vnfd_id(indata):
1725 all_vnfd_ids = set(get_iterable(indata.get("vnfd-id")))
1726 for df in get_iterable(indata.get("df")):
1727 for vnf_profile in get_iterable(df.get("vnf-profile")):
1728 vnfd_id = vnf_profile.get("vnfd-id")
1729 if vnfd_id and vnfd_id not in all_vnfd_ids:
garciadeblas4568a372021-03-24 09:19:48 +01001730 raise EngineException(
1731 "Error at df[id='{}']:vnf_profile[id='{}']:vnfd-id='{}' "
1732 "does not match any vnfd-id".format(
1733 df["id"], vnf_profile["id"], vnfd_id
1734 ),
1735 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1736 )
garciaale960531a2020-10-20 18:29:45 -03001737
Frank Brydendeba68e2020-07-27 13:55:11 +00001738 def _validate_input_edit(self, indata, content, force=False):
tiernoaa1ca7b2018-11-08 19:00:20 +01001739 # not needed to validate with pyangbind becuase it will be validated at check_conflict_on_edit
Frank Brydendeba68e2020-07-27 13:55:11 +00001740 """
1741 indata looks as follows:
garciadeblas4568a372021-03-24 09:19:48 +01001742 - In the new case (conformant)
1743 {'nsdOperationalState': 'DISABLED', 'userDefinedData': {'id': 'string23',
Frank Brydendeba68e2020-07-27 13:55:11 +00001744 '_id': 'c6ddc544-cede-4b94-9ebe-be07b298a3c1', 'name': 'simon46'}}
1745 - In the old case (backwards-compatible)
1746 {'id': 'string23', '_id': 'c6ddc544-cede-4b94-9ebe-be07b298a3c1', 'name': 'simon46'}
1747 """
1748 if "_admin" not in indata:
1749 indata["_admin"] = {}
1750
1751 if "nsdOperationalState" in indata:
1752 if indata["nsdOperationalState"] in ("ENABLED", "DISABLED"):
1753 indata["_admin"]["operationalState"] = indata.pop("nsdOperationalState")
1754 else:
garciadeblas4568a372021-03-24 09:19:48 +01001755 raise EngineException(
1756 "State '{}' is not a valid operational state".format(
1757 indata["nsdOperationalState"]
1758 ),
1759 http_code=HTTPStatus.BAD_REQUEST,
1760 )
Frank Brydendeba68e2020-07-27 13:55:11 +00001761
garciadeblas4568a372021-03-24 09:19:48 +01001762 # 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 +00001763 # to preserve current expected behaviour
1764 if "userDefinedData" in indata:
1765 data = indata.pop("userDefinedData")
gaticid7debb92023-07-31 14:37:32 +03001766 if isinstance(data, dict):
Frank Brydendeba68e2020-07-27 13:55:11 +00001767 indata["_admin"]["userDefinedData"] = data
1768 else:
garciadeblas4568a372021-03-24 09:19:48 +01001769 raise EngineException(
1770 "userDefinedData should be an object, but is '{}' instead".format(
1771 type(data)
1772 ),
1773 http_code=HTTPStatus.BAD_REQUEST,
1774 )
1775 if (
1776 "operationalState" in indata["_admin"]
1777 and content["_admin"]["operationalState"]
1778 == indata["_admin"]["operationalState"]
1779 ):
1780 raise EngineException(
1781 "nsdOperationalState already {}".format(
1782 content["_admin"]["operationalState"]
1783 ),
1784 http_code=HTTPStatus.CONFLICT,
1785 )
tiernob24258a2018-10-04 18:39:49 +02001786 return indata
1787
tierno65ca36d2019-02-12 19:27:52 +01001788 def _check_descriptor_dependencies(self, session, descriptor):
tiernob24258a2018-10-04 18:39:49 +02001789 """
tierno5a5c2182018-11-20 12:27:42 +00001790 Check that the dependent descriptors exist on a new descriptor or edition. Also checks references to vnfd
1791 connection points are ok
tierno65ca36d2019-02-12 19:27:52 +01001792 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +02001793 :param descriptor: descriptor to be inserted or edit
1794 :return: None or raises exception
1795 """
tierno65ca36d2019-02-12 19:27:52 +01001796 if session["force"]:
tiernob24258a2018-10-04 18:39:49 +02001797 return
garciaale7cbd03c2020-11-27 10:38:35 -03001798 vnfds_index = self._get_descriptor_constituent_vnfds_index(session, descriptor)
garciaale960531a2020-10-20 18:29:45 -03001799
1800 # Cross references validation in the descriptor and vnfd connection point validation
garciaale7cbd03c2020-11-27 10:38:35 -03001801 for df in get_iterable(descriptor.get("df")):
1802 self.validate_df_vnf_profiles_constituent_connection_points(df, vnfds_index)
garciaale960531a2020-10-20 18:29:45 -03001803
garciaale7cbd03c2020-11-27 10:38:35 -03001804 def _get_descriptor_constituent_vnfds_index(self, session, descriptor):
1805 vnfds_index = {}
1806 if descriptor.get("vnfd-id") and not session["force"]:
1807 for vnfd_id in get_iterable(descriptor.get("vnfd-id")):
garciaale960531a2020-10-20 18:29:45 -03001808 query_filter = self._get_project_filter(session)
1809 query_filter["id"] = vnfd_id
1810 vnf_list = self.db.get_list("vnfds", query_filter)
tierno5a5c2182018-11-20 12:27:42 +00001811 if not vnf_list:
garciadeblas4568a372021-03-24 09:19:48 +01001812 raise EngineException(
1813 "Descriptor error at 'vnfd-id'='{}' references a non "
1814 "existing vnfd".format(vnfd_id),
1815 http_code=HTTPStatus.CONFLICT,
1816 )
garciaale7cbd03c2020-11-27 10:38:35 -03001817 vnfds_index[vnfd_id] = vnf_list[0]
1818 return vnfds_index
garciaale960531a2020-10-20 18:29:45 -03001819
1820 @staticmethod
garciaale7cbd03c2020-11-27 10:38:35 -03001821 def validate_df_vnf_profiles_constituent_connection_points(df, vnfds_index):
1822 for vnf_profile in get_iterable(df.get("vnf-profile")):
1823 vnfd = vnfds_index.get(vnf_profile["vnfd-id"])
1824 all_vnfd_ext_cpds = set()
1825 for ext_cpd in get_iterable(vnfd.get("ext-cpd")):
garciadeblas4568a372021-03-24 09:19:48 +01001826 if ext_cpd.get("id"):
1827 all_vnfd_ext_cpds.add(ext_cpd.get("id"))
garciaale7cbd03c2020-11-27 10:38:35 -03001828
garciadeblas4568a372021-03-24 09:19:48 +01001829 for virtual_link in get_iterable(
1830 vnf_profile.get("virtual-link-connectivity")
1831 ):
garciaale7cbd03c2020-11-27 10:38:35 -03001832 for vl_cpd in get_iterable(virtual_link.get("constituent-cpd-id")):
garciadeblas4568a372021-03-24 09:19:48 +01001833 vl_cpd_id = vl_cpd.get("constituent-cpd-id")
garciaale7cbd03c2020-11-27 10:38:35 -03001834 if vl_cpd_id and vl_cpd_id not in all_vnfd_ext_cpds:
garciadeblas4568a372021-03-24 09:19:48 +01001835 raise EngineException(
1836 "Error at df[id='{}']:vnf-profile[id='{}']:virtual-link-connectivity"
1837 "[virtual-link-profile-id='{}']:constituent-cpd-id='{}' references a "
1838 "non existing ext-cpd:id inside vnfd '{}'".format(
1839 df["id"],
1840 vnf_profile["id"],
1841 virtual_link["virtual-link-profile-id"],
1842 vl_cpd_id,
1843 vnfd["id"],
1844 ),
1845 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1846 )
tiernob24258a2018-10-04 18:39:49 +02001847
tierno65ca36d2019-02-12 19:27:52 +01001848 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
garciadeblas4568a372021-03-24 09:19:48 +01001849 final_content = super().check_conflict_on_edit(
1850 session, final_content, edit_content, _id
1851 )
tiernob24258a2018-10-04 18:39:49 +02001852
tierno65ca36d2019-02-12 19:27:52 +01001853 self._check_descriptor_dependencies(session, final_content)
tiernob24258a2018-10-04 18:39:49 +02001854
bravofb995ea22021-02-10 10:57:52 -03001855 return final_content
1856
tiernob4844ab2019-05-23 08:42:12 +00001857 def check_conflict_on_del(self, session, _id, db_content):
tiernob24258a2018-10-04 18:39:49 +02001858 """
1859 Check that there is not any NSR that uses this NSD. Only NSRs belonging to this project are considered. Note
1860 that NSD can be public and be used by other projects.
tierno65ca36d2019-02-12 19:27:52 +01001861 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob4844ab2019-05-23 08:42:12 +00001862 :param _id: nsd internal id
1863 :param db_content: The database content of the _id
tiernob24258a2018-10-04 18:39:49 +02001864 :return: None or raises EngineException with the conflict
1865 """
tierno65ca36d2019-02-12 19:27:52 +01001866 if session["force"]:
tiernob24258a2018-10-04 18:39:49 +02001867 return
tiernob4844ab2019-05-23 08:42:12 +00001868 descriptor = db_content
1869 descriptor_id = descriptor.get("id")
1870 if not descriptor_id: # empty nsd not uploaded
1871 return
1872
1873 # check NSD used by NS
tierno65ca36d2019-02-12 19:27:52 +01001874 _filter = self._get_project_filter(session)
tiernob4844ab2019-05-23 08:42:12 +00001875 _filter["nsd-id"] = _id
tiernob24258a2018-10-04 18:39:49 +02001876 if self.db.get_list("nsrs", _filter):
garciadeblas4568a372021-03-24 09:19:48 +01001877 raise EngineException(
1878 "There is at least one NS instance using this descriptor",
1879 http_code=HTTPStatus.CONFLICT,
1880 )
tiernob4844ab2019-05-23 08:42:12 +00001881
1882 # check NSD referenced by NST
1883 del _filter["nsd-id"]
1884 _filter["netslice-subnet.ANYINDEX.nsd-ref"] = descriptor_id
1885 if self.db.get_list("nsts", _filter):
garciadeblas4568a372021-03-24 09:19:48 +01001886 raise EngineException(
1887 "There is at least one NetSlice Template referencing this descriptor",
1888 http_code=HTTPStatus.CONFLICT,
1889 )
garciaale960531a2020-10-20 18:29:45 -03001890
beierlmcee2ebf2022-03-29 17:42:48 -04001891 def delete_extra(self, session, _id, db_content, not_send_msg=None):
1892 """
1893 Deletes associate file system storage (via super)
1894 Deletes associated vnfpkgops from database.
1895 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1896 :param _id: server internal id
1897 :param db_content: The database content of the descriptor
1898 :return: None
1899 :raises: FsException in case of error while deleting associated storage
1900 """
1901 super().delete_extra(session, _id, db_content, not_send_msg)
garciadeblasf2af4a12023-01-24 16:56:54 +01001902 self.db.del_list(self.topic + "_revisions", {"_id": {"$regex": _id}})
beierlmcee2ebf2022-03-29 17:42:48 -04001903
aticig9cfa8162022-04-07 11:57:18 +03001904 @staticmethod
1905 def extract_day12_primitives(nsd: dict) -> dict:
1906 """Removes the day12 primitives from the NSD descriptors
1907
1908 Args:
1909 nsd (dict): Descriptor as a dictionary
1910
1911 Returns:
1912 nsd (dict): Cleared NSD
1913 """
1914 if nsd.get("ns-configuration"):
1915 for key in [
1916 "config-primitive",
1917 "initial-config-primitive",
1918 "terminate-config-primitive",
1919 ]:
1920 nsd["ns-configuration"].pop(key, None)
1921 return nsd
1922
1923 def remove_modifiable_items(self, nsd: dict) -> dict:
1924 """Removes the modifiable parts from the VNFD descriptors
1925
1926 It calls different extract functions according to different update types
1927 to clear all the modifiable items from NSD
1928
1929 Args:
1930 nsd (dict): Descriptor as a dictionary
1931
1932 Returns:
1933 nsd (dict): Descriptor which does not include modifiable contents
1934 """
1935 while isinstance(nsd, dict) and nsd.get("nsd"):
1936 nsd = nsd["nsd"]
1937 if isinstance(nsd, list):
1938 nsd = nsd[0]
1939 nsd.pop("_admin", None)
1940 # If the more extractions need to be done from NSD,
1941 # the new extract methods could be appended to below list.
1942 for extract_function in [self.extract_day12_primitives]:
1943 nsd_temp = extract_function(nsd)
1944 nsd = nsd_temp
1945 return nsd
1946
1947 def _validate_descriptor_changes(
1948 self,
aticig2b5e1232022-08-10 17:30:12 +03001949 descriptor_id: str,
aticig9cfa8162022-04-07 11:57:18 +03001950 descriptor_file_name: str,
1951 old_descriptor_directory: str,
1952 new_descriptor_directory: str,
1953 ):
1954 """Compares the old and new NSD descriptors and validates the new descriptor
1955
1956 Args:
1957 old_descriptor_directory: Directory of descriptor which is in-use
aticig2b5e1232022-08-10 17:30:12 +03001958 new_descriptor_directory: Directory of descriptor which is proposed to update (new revision)
aticig9cfa8162022-04-07 11:57:18 +03001959
1960 Returns:
1961 None
1962
1963 Raises:
1964 EngineException: In case of error if the changes are not allowed
1965 """
1966
1967 try:
aticig2b5e1232022-08-10 17:30:12 +03001968 # If NSD does not exist in DB, or it is not in use by any NS,
1969 # validation is not required.
1970 nsd = self.db.get_one("nsds", {"_id": descriptor_id}, fail_on_empty=False)
1971 if not nsd or not detect_descriptor_usage(nsd, "nsds", self.db):
1972 return
1973
1974 # Get the old and new descriptor contents in order to compare them.
aticig9cfa8162022-04-07 11:57:18 +03001975 with self.fs.file_open(
aticig2b5e1232022-08-10 17:30:12 +03001976 (old_descriptor_directory.rstrip("/"), descriptor_file_name), "r"
aticig9cfa8162022-04-07 11:57:18 +03001977 ) as old_descriptor_file:
1978 with self.fs.file_open(
1979 (new_descriptor_directory.rstrip("/"), descriptor_file_name), "r"
1980 ) as new_descriptor_file:
aticig2b5e1232022-08-10 17:30:12 +03001981 old_content = yaml.safe_load(old_descriptor_file.read())
1982 new_content = yaml.safe_load(new_descriptor_file.read())
1983
aticig9cfa8162022-04-07 11:57:18 +03001984 if old_content and new_content:
1985 disallowed_change = DeepDiff(
1986 self.remove_modifiable_items(old_content),
1987 self.remove_modifiable_items(new_content),
1988 )
aticig2b5e1232022-08-10 17:30:12 +03001989
aticig9cfa8162022-04-07 11:57:18 +03001990 if disallowed_change:
1991 changed_nodes = functools.reduce(
1992 lambda a, b: a + ", " + b,
1993 [
1994 node.lstrip("root")
1995 for node in disallowed_change.get(
1996 "values_changed"
1997 ).keys()
1998 ],
1999 )
aticig2b5e1232022-08-10 17:30:12 +03002000
aticig9cfa8162022-04-07 11:57:18 +03002001 raise EngineException(
2002 f"Error in validating new descriptor: {changed_nodes} cannot be modified, "
2003 "there are disallowed changes in the ns descriptor. ",
2004 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
2005 )
2006 except (
2007 DbException,
2008 AttributeError,
2009 IndexError,
2010 KeyError,
2011 ValueError,
2012 ) as e:
2013 raise type(e)(
2014 "NS Descriptor could not be processed with error: {}.".format(e)
2015 )
2016
Frank Bryden19b97522020-07-10 12:32:02 +00002017 def sol005_projection(self, data):
2018 data["nsdOnboardingState"] = data["_admin"]["onboardingState"]
2019 data["nsdOperationalState"] = data["_admin"]["operationalState"]
2020 data["nsdUsageState"] = data["_admin"]["usageState"]
2021
2022 links = {}
2023 links["self"] = {"href": "/nsd/v1/ns_descriptors/{}".format(data["_id"])}
garciadeblas4568a372021-03-24 09:19:48 +01002024 links["nsd_content"] = {
2025 "href": "/nsd/v1/ns_descriptors/{}/nsd_content".format(data["_id"])
2026 }
Frank Bryden19b97522020-07-10 12:32:02 +00002027 data["_links"] = links
garciaale960531a2020-10-20 18:29:45 -03002028
Frank Bryden19b97522020-07-10 12:32:02 +00002029 return super().sol005_projection(data)
tiernob24258a2018-10-04 18:39:49 +02002030
2031
Felipe Vicensb57758d2018-10-16 16:00:20 +02002032class NstTopic(DescriptorTopic):
2033 topic = "nsts"
2034 topic_msg = "nst"
tierno6b02b052020-06-02 10:07:41 +00002035 quota_name = "slice_templates"
Felipe Vicensb57758d2018-10-16 16:00:20 +02002036
delacruzramo32bab472019-09-13 12:24:22 +02002037 def __init__(self, db, fs, msg, auth):
2038 DescriptorTopic.__init__(self, db, fs, msg, auth)
Felipe Vicensb57758d2018-10-16 16:00:20 +02002039
garciaale7cbd03c2020-11-27 10:38:35 -03002040 def pyangbind_validation(self, item, data, force=False):
2041 try:
2042 mynst = nst_im()
garciadeblas4568a372021-03-24 09:19:48 +01002043 pybindJSONDecoder.load_ietf_json(
2044 {"nst": [data]},
2045 None,
2046 None,
2047 obj=mynst,
2048 path_helper=True,
2049 skip_unknown=force,
2050 )
garciaale7cbd03c2020-11-27 10:38:35 -03002051 out = pybindJSON.dumps(mynst, mode="ietf")
2052 desc_out = self._remove_envelop(yaml.safe_load(out))
2053 return desc_out
2054 except Exception as e:
garciadeblas4568a372021-03-24 09:19:48 +01002055 raise EngineException(
2056 "Error in pyangbind validation: {}".format(str(e)),
2057 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
2058 )
garciaale7cbd03c2020-11-27 10:38:35 -03002059
Felipe Vicensb57758d2018-10-16 16:00:20 +02002060 @staticmethod
2061 def _remove_envelop(indata=None):
2062 if not indata:
2063 return {}
2064 clean_indata = indata
2065
garciadeblas4568a372021-03-24 09:19:48 +01002066 if clean_indata.get("nst"):
2067 if (
2068 not isinstance(clean_indata["nst"], list)
2069 or len(clean_indata["nst"]) != 1
2070 ):
Felipe Vicensb57758d2018-10-16 16:00:20 +02002071 raise EngineException("'nst' must be a list only one element")
garciadeblas4568a372021-03-24 09:19:48 +01002072 clean_indata = clean_indata["nst"][0]
2073 elif clean_indata.get("nst:nst"):
2074 if (
2075 not isinstance(clean_indata["nst:nst"], list)
2076 or len(clean_indata["nst:nst"]) != 1
2077 ):
gcalvino70434c12018-11-27 15:17:04 +01002078 raise EngineException("'nst:nst' must be a list only one element")
garciadeblas4568a372021-03-24 09:19:48 +01002079 clean_indata = clean_indata["nst:nst"][0]
Felipe Vicensb57758d2018-10-16 16:00:20 +02002080 return clean_indata
2081
gcalvinoa6fe0002019-01-09 13:27:11 +01002082 def _validate_input_new(self, indata, storage_params, force=False):
Frank Bryden19b97522020-07-10 12:32:02 +00002083 indata.pop("onboardingState", None)
2084 indata.pop("operationalState", None)
2085 indata.pop("usageState", None)
gcalvino70434c12018-11-27 15:17:04 +01002086 indata = self.pyangbind_validation("nsts", indata, force)
Felipe Vicense36ab852018-11-23 14:12:09 +01002087 return indata.copy()
2088
Felipe Vicensb57758d2018-10-16 16:00:20 +02002089 def _check_descriptor_dependencies(self, session, descriptor):
2090 """
2091 Check that the dependent descriptors exist on a new descriptor or edition
tierno65ca36d2019-02-12 19:27:52 +01002092 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Felipe Vicensb57758d2018-10-16 16:00:20 +02002093 :param descriptor: descriptor to be inserted or edit
2094 :return: None or raises exception
2095 """
2096 if not descriptor.get("netslice-subnet"):
2097 return
2098 for nsd in descriptor["netslice-subnet"]:
2099 nsd_id = nsd["nsd-ref"]
tierno65ca36d2019-02-12 19:27:52 +01002100 filter_q = self._get_project_filter(session)
Felipe Vicensb57758d2018-10-16 16:00:20 +02002101 filter_q["id"] = nsd_id
2102 if not self.db.get_list("nsds", filter_q):
garciadeblas4568a372021-03-24 09:19:48 +01002103 raise EngineException(
2104 "Descriptor error at 'netslice-subnet':'nsd-ref'='{}' references a non "
2105 "existing nsd".format(nsd_id),
2106 http_code=HTTPStatus.CONFLICT,
2107 )
Felipe Vicensb57758d2018-10-16 16:00:20 +02002108
tierno65ca36d2019-02-12 19:27:52 +01002109 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
garciadeblas4568a372021-03-24 09:19:48 +01002110 final_content = super().check_conflict_on_edit(
2111 session, final_content, edit_content, _id
2112 )
Felipe Vicensb57758d2018-10-16 16:00:20 +02002113
2114 self._check_descriptor_dependencies(session, final_content)
bravofb995ea22021-02-10 10:57:52 -03002115 return final_content
Felipe Vicensb57758d2018-10-16 16:00:20 +02002116
tiernob4844ab2019-05-23 08:42:12 +00002117 def check_conflict_on_del(self, session, _id, db_content):
Felipe Vicensb57758d2018-10-16 16:00:20 +02002118 """
2119 Check that there is not any NSIR that uses this NST. Only NSIRs belonging to this project are considered. Note
2120 that NST can be public and be used by other projects.
tierno65ca36d2019-02-12 19:27:52 +01002121 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Felipe Vicens07f31722018-10-29 15:16:44 +01002122 :param _id: nst internal id
tiernob4844ab2019-05-23 08:42:12 +00002123 :param db_content: The database content of the _id.
Felipe Vicensb57758d2018-10-16 16:00:20 +02002124 :return: None or raises EngineException with the conflict
2125 """
2126 # TODO: Check this method
tierno65ca36d2019-02-12 19:27:52 +01002127 if session["force"]:
Felipe Vicensb57758d2018-10-16 16:00:20 +02002128 return
Felipe Vicens07f31722018-10-29 15:16:44 +01002129 # Get Network Slice Template from Database
tierno65ca36d2019-02-12 19:27:52 +01002130 _filter = self._get_project_filter(session)
tiernoea97c042019-09-13 09:44:42 +00002131 _filter["_admin.nst-id"] = _id
tiernob4844ab2019-05-23 08:42:12 +00002132 if self.db.get_list("nsis", _filter):
garciadeblas4568a372021-03-24 09:19:48 +01002133 raise EngineException(
2134 "there is at least one Netslice Instance using this descriptor",
2135 http_code=HTTPStatus.CONFLICT,
2136 )
Felipe Vicensb57758d2018-10-16 16:00:20 +02002137
Frank Bryden19b97522020-07-10 12:32:02 +00002138 def sol005_projection(self, data):
2139 data["onboardingState"] = data["_admin"]["onboardingState"]
2140 data["operationalState"] = data["_admin"]["operationalState"]
2141 data["usageState"] = data["_admin"]["usageState"]
2142
2143 links = {}
2144 links["self"] = {"href": "/nst/v1/netslice_templates/{}".format(data["_id"])}
2145 links["nst"] = {"href": "/nst/v1/netslice_templates/{}/nst".format(data["_id"])}
2146 data["_links"] = links
2147
2148 return super().sol005_projection(data)
2149
Felipe Vicensb57758d2018-10-16 16:00:20 +02002150
tiernob24258a2018-10-04 18:39:49 +02002151class PduTopic(BaseTopic):
2152 topic = "pdus"
2153 topic_msg = "pdu"
tierno6b02b052020-06-02 10:07:41 +00002154 quota_name = "pduds"
tiernob24258a2018-10-04 18:39:49 +02002155 schema_new = pdu_new_schema
2156 schema_edit = pdu_edit_schema
2157
delacruzramo32bab472019-09-13 12:24:22 +02002158 def __init__(self, db, fs, msg, auth):
2159 BaseTopic.__init__(self, db, fs, msg, auth)
tiernob24258a2018-10-04 18:39:49 +02002160
2161 @staticmethod
2162 def format_on_new(content, project_id=None, make_public=False):
tierno36ec8602018-11-02 17:27:11 +01002163 BaseTopic.format_on_new(content, project_id=project_id, make_public=make_public)
tiernob24258a2018-10-04 18:39:49 +02002164 content["_admin"]["onboardingState"] = "CREATED"
tierno36ec8602018-11-02 17:27:11 +01002165 content["_admin"]["operationalState"] = "ENABLED"
2166 content["_admin"]["usageState"] = "NOT_IN_USE"
tiernob24258a2018-10-04 18:39:49 +02002167
tiernob4844ab2019-05-23 08:42:12 +00002168 def check_conflict_on_del(self, session, _id, db_content):
2169 """
2170 Check that there is not any vnfr that uses this PDU
2171 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
2172 :param _id: pdu internal id
2173 :param db_content: The database content of the _id.
2174 :return: None or raises EngineException with the conflict
2175 """
tierno65ca36d2019-02-12 19:27:52 +01002176 if session["force"]:
tiernob24258a2018-10-04 18:39:49 +02002177 return
tiernob4844ab2019-05-23 08:42:12 +00002178
2179 _filter = self._get_project_filter(session)
2180 _filter["vdur.pdu-id"] = _id
tiernob24258a2018-10-04 18:39:49 +02002181 if self.db.get_list("vnfrs", _filter):
garciadeblas4568a372021-03-24 09:19:48 +01002182 raise EngineException(
2183 "There is at least one VNF instance using this PDU",
2184 http_code=HTTPStatus.CONFLICT,
2185 )
delacruzramo271d2002019-12-02 21:00:37 +01002186
2187
2188class VnfPkgOpTopic(BaseTopic):
2189 topic = "vnfpkgops"
2190 topic_msg = "vnfd"
2191 schema_new = vnfpkgop_new_schema
2192 schema_edit = None
2193
2194 def __init__(self, db, fs, msg, auth):
2195 BaseTopic.__init__(self, db, fs, msg, auth)
2196
2197 def edit(self, session, _id, indata=None, kwargs=None, content=None):
garciadeblas4568a372021-03-24 09:19:48 +01002198 raise EngineException(
2199 "Method 'edit' not allowed for topic '{}'".format(self.topic),
2200 HTTPStatus.METHOD_NOT_ALLOWED,
2201 )
delacruzramo271d2002019-12-02 21:00:37 +01002202
2203 def delete(self, session, _id, dry_run=False):
garciadeblas4568a372021-03-24 09:19:48 +01002204 raise EngineException(
2205 "Method 'delete' not allowed for topic '{}'".format(self.topic),
2206 HTTPStatus.METHOD_NOT_ALLOWED,
2207 )
delacruzramo271d2002019-12-02 21:00:37 +01002208
2209 def delete_list(self, session, filter_q=None):
garciadeblas4568a372021-03-24 09:19:48 +01002210 raise EngineException(
2211 "Method 'delete_list' not allowed for topic '{}'".format(self.topic),
2212 HTTPStatus.METHOD_NOT_ALLOWED,
2213 )
delacruzramo271d2002019-12-02 21:00:37 +01002214
2215 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
2216 """
2217 Creates a new entry into database.
2218 :param rollback: list to append created items at database in case a rollback may to be done
2219 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
2220 :param indata: data to be inserted
2221 :param kwargs: used to override the indata descriptor
2222 :param headers: http request headers
2223 :return: _id, op_id:
2224 _id: identity of the inserted data.
2225 op_id: None
2226 """
2227 self._update_input_with_kwargs(indata, kwargs)
2228 validate_input(indata, self.schema_new)
2229 vnfpkg_id = indata["vnfPkgId"]
2230 filter_q = BaseTopic._get_project_filter(session)
2231 filter_q["_id"] = vnfpkg_id
2232 vnfd = self.db.get_one("vnfds", filter_q)
2233 operation = indata["lcmOperationType"]
2234 kdu_name = indata["kdu_name"]
2235 for kdu in vnfd.get("kdu", []):
2236 if kdu["name"] == kdu_name:
2237 helm_chart = kdu.get("helm-chart")
2238 juju_bundle = kdu.get("juju-bundle")
2239 break
2240 else:
garciadeblas4568a372021-03-24 09:19:48 +01002241 raise EngineException(
2242 "Not found vnfd[id='{}']:kdu[name='{}']".format(vnfpkg_id, kdu_name)
2243 )
delacruzramo271d2002019-12-02 21:00:37 +01002244 if helm_chart:
2245 indata["helm-chart"] = helm_chart
2246 match = fullmatch(r"([^/]*)/([^/]*)", helm_chart)
2247 repo_name = match.group(1) if match else None
2248 elif juju_bundle:
2249 indata["juju-bundle"] = juju_bundle
2250 match = fullmatch(r"([^/]*)/([^/]*)", juju_bundle)
2251 repo_name = match.group(1) if match else None
2252 else:
garciadeblas4568a372021-03-24 09:19:48 +01002253 raise EngineException(
2254 "Found neither 'helm-chart' nor 'juju-bundle' in vnfd[id='{}']:kdu[name='{}']".format(
2255 vnfpkg_id, kdu_name
2256 )
2257 )
delacruzramo271d2002019-12-02 21:00:37 +01002258 if repo_name:
2259 del filter_q["_id"]
2260 filter_q["name"] = repo_name
2261 repo = self.db.get_one("k8srepos", filter_q)
2262 k8srepo_id = repo.get("_id")
2263 k8srepo_url = repo.get("url")
2264 else:
2265 k8srepo_id = None
2266 k8srepo_url = None
2267 indata["k8srepoId"] = k8srepo_id
2268 indata["k8srepo_url"] = k8srepo_url
2269 vnfpkgop_id = str(uuid4())
2270 vnfpkgop_desc = {
2271 "_id": vnfpkgop_id,
2272 "operationState": "PROCESSING",
2273 "vnfPkgId": vnfpkg_id,
2274 "lcmOperationType": operation,
2275 "isAutomaticInvocation": False,
2276 "isCancelPending": False,
2277 "operationParams": indata,
2278 "links": {
2279 "self": "/osm/vnfpkgm/v1/vnfpkg_op_occs/" + vnfpkgop_id,
2280 "vnfpkg": "/osm/vnfpkgm/v1/vnf_packages/" + vnfpkg_id,
garciadeblas4568a372021-03-24 09:19:48 +01002281 },
delacruzramo271d2002019-12-02 21:00:37 +01002282 }
garciadeblas4568a372021-03-24 09:19:48 +01002283 self.format_on_new(
2284 vnfpkgop_desc, session["project_id"], make_public=session["public"]
2285 )
delacruzramo271d2002019-12-02 21:00:37 +01002286 ctime = vnfpkgop_desc["_admin"]["created"]
2287 vnfpkgop_desc["statusEnteredTime"] = ctime
2288 vnfpkgop_desc["startTime"] = ctime
2289 self.db.create(self.topic, vnfpkgop_desc)
2290 rollback.append({"topic": self.topic, "_id": vnfpkgop_id})
2291 self.msg.write(self.topic_msg, operation, vnfpkgop_desc)
2292 return vnfpkgop_id, None
kayal2001f71c2e82024-06-25 15:26:24 +05302293
2294
2295class NsConfigTemplateTopic(DescriptorTopic):
2296 topic = "ns_config_template"
2297 topic_msg = "nsd"
2298 schema_new = ns_config_template
2299 instantiation_params = {
2300 "vnf": vnf_schema,
2301 "vld": vld_schema,
2302 "additionalParamsForVnf": additional_params_for_vnf,
2303 }
2304
2305 def __init__(self, db, fs, msg, auth):
2306 super().__init__(db, fs, msg, auth)
2307
2308 def check_conflict_on_del(self, session, _id, db_content):
2309 """
2310 Check that there is not any NSR that uses this NS CONFIG TEMPLATE. Only NSRs belonging to this project are considered.
2311 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
2312 :param _id: ns config template internal id
2313 :param db_content: The database content of the _id
2314 :return: None or raises EngineException with the conflict
2315 """
2316 if session["force"]:
2317 return
2318 descriptor = db_content
2319 descriptor_id = descriptor.get("nsdId")
2320 if not descriptor_id: # empty nsd not uploaded
2321 return
2322
2323 # check NS CONFIG TEMPLATE used by NS
2324 ns_config_template_id = _id
yshah53cc9eb2024-07-05 13:06:31 +00002325
kayal2001f71c2e82024-06-25 15:26:24 +05302326 if self.db.get_list(
2327 "nsrs", {"instantiate_params.nsConfigTemplateId": ns_config_template_id}
2328 ):
2329 raise EngineException(
2330 "There is at least one NS instance using this template",
2331 http_code=HTTPStatus.CONFLICT,
2332 )
2333
2334 def check_unique_template_name(self, edit_content, _id, session):
2335 """
2336 Check whether the name of the template is unique or not
2337 """
2338
2339 if edit_content.get("name"):
2340 name = edit_content.get("name")
2341 db_content = self.db.get_one(
2342 "ns_config_template", {"name": name}, fail_on_empty=False
2343 )
2344 if db_content is not None:
2345 if db_content.get("_id") == _id:
2346 if db_content.get("name") == name:
2347 return
2348 elif db_content.get("_id") != _id:
2349 raise EngineException(
2350 "{} of the template already exist".format(name)
2351 )
2352 else:
2353 return
2354
2355 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
2356 """
2357 Check the input data format
2358 And the edit content data too.
2359 """
2360 final_content = super().check_conflict_on_edit(
2361 session, final_content, edit_content, _id
2362 )
2363 db_content_id = self.db.get_one(
2364 "ns_config_template", {"_id": _id}, fail_on_empty=False
2365 )
2366 if not (
2367 db_content_id.get("name")
2368 and db_content_id.get("nsdId")
2369 and db_content_id.get("config")
2370 ):
2371 validate_input(edit_content, self.schema_new)
2372
2373 try:
2374 for key, value in edit_content.items():
2375 if key == "name":
2376 self.check_unique_template_name(edit_content, _id, session)
2377 elif key == "nsdId":
2378 ns_config_template = self.db.get_one(
2379 "ns_config_template", {"_id": _id}, fail_on_empty=False
2380 )
2381 if not ns_config_template.get("nsdId"):
2382 pass
2383 else:
2384 raise EngineException("Nsd id cannot be edited")
2385 elif key == "config":
2386 edit_content_param = edit_content.get("config")
2387 for key, value in edit_content_param.items():
2388 param = key
2389 param_content = value
kayal2001b16cf252024-11-28 10:47:32 +05302390 if param == "vnf":
2391 for content in param_content:
2392 for vdu in content.get("vdu"):
2393 if vdu.get("vim-flavor-name") and vdu.get(
2394 "vim-flavor-id"
2395 ):
2396 raise EngineException(
2397 "Instantiation parameters vim-flavor-name and vim-flavor-id are mutually exclusive"
2398 )
kayal2001f71c2e82024-06-25 15:26:24 +05302399 validate_input(param_content, self.instantiation_params[param])
kayal2001770c0af2024-11-04 22:18:55 +05302400 final_content.update({"config": edit_content_param})
kayal2001f71c2e82024-06-25 15:26:24 +05302401 return final_content
2402 except Exception as e:
2403 raise EngineException(
2404 "Error in instantiation parameters validation: {}".format(str(e)),
2405 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
2406 )