blob: b165b76f8fb816434251e3d49ad19b8dee54736b [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
garciadeblas4568a372021-03-24 09:19:48 +010024
tiernob24258a2018-10-04 18:39:49 +020025# import logging
aticig9cfa8162022-04-07 11:57:18 +030026from deepdiff import DeepDiff
tiernob24258a2018-10-04 18:39:49 +020027from hashlib import md5
28from osm_common.dbbase import DbException, deep_update_rfc7396
29from http import HTTPStatus
delacruzramo26301bb2019-11-15 14:45:32 +010030from time import time
delacruzramo271d2002019-12-02 21:00:37 +010031from uuid import uuid4
32from re import fullmatch
bravofc26740a2021-11-08 09:44:54 -030033from zipfile import ZipFile
Gabriel Cuba646773d2023-11-20 01:43:05 -050034from urllib.parse import urlparse
garciadeblas4568a372021-03-24 09:19:48 +010035from osm_nbi.validation import (
36 ValidationError,
37 pdu_new_schema,
38 pdu_edit_schema,
39 validate_input,
40 vnfpkgop_new_schema,
41)
aticig2b5e1232022-08-10 17:30:12 +030042from osm_nbi.base_topic import (
43 BaseTopic,
44 EngineException,
45 get_iterable,
46 detect_descriptor_usage,
47)
sousaedu317b9fd2021-07-29 17:40:16 +020048from osm_im import etsi_nfv_vnfd, etsi_nfv_nsd
gcalvino70434c12018-11-27 15:17:04 +010049from osm_im.nst import nst as nst_im
gcalvino46e4cb82018-10-26 13:10:22 +020050from pyangbind.lib.serialise import pybindJSONDecoder
51import pyangbind.lib.pybindJSON as pybindJSON
bravof41a52052021-02-17 18:08:01 -030052from osm_nbi import utils
tiernob24258a2018-10-04 18:39:49 +020053
54__author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
55
Daniel Arndt00f83aa2023-06-15 16:43:33 +020056valid_helm_chart_re = re.compile(
57 r"^[a-z0-9]([-a-z0-9]*[a-z0-9]/)?([a-z0-9]([-a-z0-9]*[a-z0-9])?)*$"
58)
59
tiernob24258a2018-10-04 18:39:49 +020060
61class DescriptorTopic(BaseTopic):
delacruzramo32bab472019-09-13 12:24:22 +020062 def __init__(self, db, fs, msg, auth):
Daniel Arndt00f83aa2023-06-15 16:43:33 +020063 super().__init__(db, fs, msg, auth)
tiernob24258a2018-10-04 18:39:49 +020064
garciadeblasf2af4a12023-01-24 16:56:54 +010065 def _validate_input_new(self, indata, storage_params, force=False):
66 return indata
67
tierno65ca36d2019-02-12 19:27:52 +010068 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
garciadeblas4568a372021-03-24 09:19:48 +010069 final_content = super().check_conflict_on_edit(
70 session, final_content, edit_content, _id
71 )
K Sai Kiran45bd94c2019-11-25 17:30:37 +053072
73 def _check_unique_id_name(descriptor, position=""):
74 for desc_key, desc_item in descriptor.items():
75 if isinstance(desc_item, list) and desc_item:
76 used_ids = []
77 desc_item_id = None
78 for index, list_item in enumerate(desc_item):
79 if isinstance(list_item, dict):
garciadeblas4568a372021-03-24 09:19:48 +010080 _check_unique_id_name(
81 list_item, "{}.{}[{}]".format(position, desc_key, index)
82 )
K Sai Kiran45bd94c2019-11-25 17:30:37 +053083 # Base case
garciadeblas4568a372021-03-24 09:19:48 +010084 if index == 0 and (
85 list_item.get("id") or list_item.get("name")
86 ):
K Sai Kiran45bd94c2019-11-25 17:30:37 +053087 desc_item_id = "id" if list_item.get("id") else "name"
88 if desc_item_id and list_item.get(desc_item_id):
89 if list_item[desc_item_id] in used_ids:
garciadeblas4568a372021-03-24 09:19:48 +010090 position = "{}.{}[{}]".format(
91 position, desc_key, index
92 )
93 raise EngineException(
94 "Error: identifier {} '{}' is not unique and repeats at '{}'".format(
95 desc_item_id,
96 list_item[desc_item_id],
97 position,
98 ),
99 HTTPStatus.UNPROCESSABLE_ENTITY,
100 )
K Sai Kiran45bd94c2019-11-25 17:30:37 +0530101 used_ids.append(list_item[desc_item_id])
garciaale960531a2020-10-20 18:29:45 -0300102
K Sai Kiran45bd94c2019-11-25 17:30:37 +0530103 _check_unique_id_name(final_content)
tiernoaa1ca7b2018-11-08 19:00:20 +0100104 # 1. validate again with pyangbind
105 # 1.1. remove internal keys
106 internal_keys = {}
107 for k in ("_id", "_admin"):
108 if k in final_content:
109 internal_keys[k] = final_content.pop(k)
gcalvinoa6fe0002019-01-09 13:27:11 +0100110 storage_params = internal_keys["_admin"].get("storage")
garciadeblas4568a372021-03-24 09:19:48 +0100111 serialized = self._validate_input_new(
112 final_content, storage_params, session["force"]
113 )
bravofb995ea22021-02-10 10:57:52 -0300114
tiernoaa1ca7b2018-11-08 19:00:20 +0100115 # 1.2. modify final_content with a serialized version
bravofb995ea22021-02-10 10:57:52 -0300116 final_content = copy.deepcopy(serialized)
tiernoaa1ca7b2018-11-08 19:00:20 +0100117 # 1.3. restore internal keys
118 for k, v in internal_keys.items():
119 final_content[k] = v
tierno65ca36d2019-02-12 19:27:52 +0100120 if session["force"]:
bravofb995ea22021-02-10 10:57:52 -0300121 return final_content
122
tiernoaa1ca7b2018-11-08 19:00:20 +0100123 # 2. check that this id is not present
124 if "id" in edit_content:
tierno65ca36d2019-02-12 19:27:52 +0100125 _filter = self._get_project_filter(session)
bravofb995ea22021-02-10 10:57:52 -0300126
tiernoaa1ca7b2018-11-08 19:00:20 +0100127 _filter["id"] = final_content["id"]
128 _filter["_id.neq"] = _id
bravofb995ea22021-02-10 10:57:52 -0300129
tiernoaa1ca7b2018-11-08 19:00:20 +0100130 if self.db.get_one(self.topic, _filter, fail_on_empty=False):
garciadeblas4568a372021-03-24 09:19:48 +0100131 raise EngineException(
132 "{} with id '{}' already exists for this project".format(
garciadeblasf2af4a12023-01-24 16:56:54 +0100133 (str(self.topic))[:-1], final_content["id"]
garciadeblas4568a372021-03-24 09:19:48 +0100134 ),
135 HTTPStatus.CONFLICT,
136 )
tiernob24258a2018-10-04 18:39:49 +0200137
bravofb995ea22021-02-10 10:57:52 -0300138 return final_content
139
tiernob24258a2018-10-04 18:39:49 +0200140 @staticmethod
141 def format_on_new(content, project_id=None, make_public=False):
142 BaseTopic.format_on_new(content, project_id=project_id, make_public=make_public)
143 content["_admin"]["onboardingState"] = "CREATED"
144 content["_admin"]["operationalState"] = "DISABLED"
tierno36ec8602018-11-02 17:27:11 +0100145 content["_admin"]["usageState"] = "NOT_IN_USE"
tiernob24258a2018-10-04 18:39:49 +0200146
tiernobee3bad2019-12-05 12:26:01 +0000147 def delete_extra(self, session, _id, db_content, not_send_msg=None):
tiernob4844ab2019-05-23 08:42:12 +0000148 """
149 Deletes file system storage associated with the descriptor
150 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
151 :param _id: server internal id
152 :param db_content: The database content of the descriptor
tiernobee3bad2019-12-05 12:26:01 +0000153 :param not_send_msg: To not send message (False) or store content (list) instead
tiernob4844ab2019-05-23 08:42:12 +0000154 :return: None if ok or raises EngineException with the problem
155 """
tiernob24258a2018-10-04 18:39:49 +0200156 self.fs.file_delete(_id, ignore_non_exist=True)
tiernof717cbe2018-12-03 16:35:42 +0000157 self.fs.file_delete(_id + "_", ignore_non_exist=True) # remove temp folder
beierlmcee2ebf2022-03-29 17:42:48 -0400158 # Remove file revisions
159 if "revision" in db_content["_admin"]:
160 revision = db_content["_admin"]["revision"]
161 while revision > 0:
162 self.fs.file_delete(_id + ":" + str(revision), ignore_non_exist=True)
163 revision = revision - 1
164
tiernob24258a2018-10-04 18:39:49 +0200165 @staticmethod
166 def get_one_by_id(db, session, topic, id):
167 # find owned by this project
tierno65ca36d2019-02-12 19:27:52 +0100168 _filter = BaseTopic._get_project_filter(session)
tiernob24258a2018-10-04 18:39:49 +0200169 _filter["id"] = id
170 desc_list = db.get_list(topic, _filter)
171 if len(desc_list) == 1:
172 return desc_list[0]
173 elif len(desc_list) > 1:
garciadeblas4568a372021-03-24 09:19:48 +0100174 raise DbException(
175 "Found more than one {} with id='{}' belonging to this project".format(
176 topic[:-1], id
177 ),
178 HTTPStatus.CONFLICT,
179 )
tiernob24258a2018-10-04 18:39:49 +0200180
181 # not found any: try to find public
tierno65ca36d2019-02-12 19:27:52 +0100182 _filter = BaseTopic._get_project_filter(session)
tiernob24258a2018-10-04 18:39:49 +0200183 _filter["id"] = id
184 desc_list = db.get_list(topic, _filter)
185 if not desc_list:
garciadeblas4568a372021-03-24 09:19:48 +0100186 raise DbException(
187 "Not found any {} with id='{}'".format(topic[:-1], id),
188 HTTPStatus.NOT_FOUND,
189 )
tiernob24258a2018-10-04 18:39:49 +0200190 elif len(desc_list) == 1:
191 return desc_list[0]
192 else:
garciadeblas4568a372021-03-24 09:19:48 +0100193 raise DbException(
194 "Found more than one public {} with id='{}'; and no one belonging to this project".format(
195 topic[:-1], id
196 ),
197 HTTPStatus.CONFLICT,
198 )
tiernob24258a2018-10-04 18:39:49 +0200199
tierno65ca36d2019-02-12 19:27:52 +0100200 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
tiernob24258a2018-10-04 18:39:49 +0200201 """
202 Creates a new almost empty DISABLED entry into database. Due to SOL005, it does not follow normal procedure.
203 Creating a VNFD or NSD is done in two steps: 1. Creates an empty descriptor (this step) and 2) upload content
204 (self.upload_content)
205 :param rollback: list to append created items at database in case a rollback may to be done
tierno65ca36d2019-02-12 19:27:52 +0100206 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200207 :param indata: data to be inserted
208 :param kwargs: used to override the indata descriptor
209 :param headers: http request headers
tiernobdebce92019-07-01 15:36:49 +0000210 :return: _id, None: identity of the inserted data; and None as there is not any operation
tiernob24258a2018-10-04 18:39:49 +0200211 """
212
tiernod7749582020-05-28 10:41:10 +0000213 # No needed to capture exceptions
214 # Check Quota
215 self.check_quota(session)
delacruzramo32bab472019-09-13 12:24:22 +0200216
tiernod7749582020-05-28 10:41:10 +0000217 # _remove_envelop
218 if indata:
219 if "userDefinedData" in indata:
garciadeblas4568a372021-03-24 09:19:48 +0100220 indata = indata["userDefinedData"]
tiernob24258a2018-10-04 18:39:49 +0200221
tiernod7749582020-05-28 10:41:10 +0000222 # Override descriptor with query string kwargs
223 self._update_input_with_kwargs(indata, kwargs)
224 # uncomment when this method is implemented.
225 # Avoid override in this case as the target is userDefinedData, but not vnfd,nsd descriptors
226 # indata = DescriptorTopic._validate_input_new(self, indata, project_id=session["force"])
tiernob24258a2018-10-04 18:39:49 +0200227
garciadeblasf2af4a12023-01-24 16:56:54 +0100228 content = {"_admin": {"userDefinedData": indata, "revision": 0}}
beierlmcee2ebf2022-03-29 17:42:48 -0400229
garciadeblas4568a372021-03-24 09:19:48 +0100230 self.format_on_new(
231 content, session["project_id"], make_public=session["public"]
232 )
tiernod7749582020-05-28 10:41:10 +0000233 _id = self.db.create(self.topic, content)
234 rollback.append({"topic": self.topic, "_id": _id})
235 self._send_msg("created", {"_id": _id})
236 return _id, None
tiernob24258a2018-10-04 18:39:49 +0200237
tierno65ca36d2019-02-12 19:27:52 +0100238 def upload_content(self, session, _id, indata, kwargs, headers):
tiernob24258a2018-10-04 18:39:49 +0200239 """
240 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 +0100241 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200242 :param _id : the nsd,vnfd is already created, this is the id
243 :param indata: http body request
244 :param kwargs: user query string to override parameters. NOT USED
245 :param headers: http request headers
tierno5a5c2182018-11-20 12:27:42 +0000246 :return: True if package is completely uploaded or False if partial content has been uploded
tiernob24258a2018-10-04 18:39:49 +0200247 Raise exception on error
248 """
249 # Check that _id exists and it is valid
250 current_desc = self.show(session, _id)
251
252 content_range_text = headers.get("Content-Range")
253 expected_md5 = headers.get("Content-File-MD5")
254 compressed = None
255 content_type = headers.get("Content-Type")
garciadeblas4568a372021-03-24 09:19:48 +0100256 if (
257 content_type
258 and "application/gzip" in content_type
259 or "application/x-gzip" in content_type
garciadeblas4568a372021-03-24 09:19:48 +0100260 ):
tiernob24258a2018-10-04 18:39:49 +0200261 compressed = "gzip"
garciadeblasf2af4a12023-01-24 16:56:54 +0100262 if content_type and "application/zip" in content_type:
bravofc26740a2021-11-08 09:44:54 -0300263 compressed = "zip"
tiernob24258a2018-10-04 18:39:49 +0200264 filename = headers.get("Content-Filename")
bravofc26740a2021-11-08 09:44:54 -0300265 if not filename and compressed:
266 filename = "package.tar.gz" if compressed == "gzip" else "package.zip"
267 elif not filename:
268 filename = "package"
269
beierlmcee2ebf2022-03-29 17:42:48 -0400270 revision = 1
271 if "revision" in current_desc["_admin"]:
272 revision = current_desc["_admin"]["revision"] + 1
273
tiernob24258a2018-10-04 18:39:49 +0200274 # TODO change to Content-Disposition filename https://tools.ietf.org/html/rfc6266
275 file_pkg = None
276 error_text = ""
beierlmbc5a5242022-05-17 21:25:29 -0400277 fs_rollback = []
278
tiernob24258a2018-10-04 18:39:49 +0200279 try:
280 if content_range_text:
garciadeblas4568a372021-03-24 09:19:48 +0100281 content_range = (
282 content_range_text.replace("-", " ").replace("/", " ").split()
283 )
284 if (
285 content_range[0] != "bytes"
286 ): # TODO check x<y not negative < total....
tiernob24258a2018-10-04 18:39:49 +0200287 raise IndexError()
288 start = int(content_range[1])
289 end = int(content_range[2]) + 1
290 total = int(content_range[3])
291 else:
292 start = 0
beierlmcee2ebf2022-03-29 17:42:48 -0400293 # Rather than using a temp folder, we will store the package in a folder based on
294 # the current revision.
295 proposed_revision_path = (
296 _id + ":" + str(revision)
garciadeblas4568a372021-03-24 09:19:48 +0100297 ) # all the content is upload here and if ok, it is rename from id_ to is folder
tiernob24258a2018-10-04 18:39:49 +0200298
299 if start:
beierlmcee2ebf2022-03-29 17:42:48 -0400300 if not self.fs.file_exists(proposed_revision_path, "dir"):
garciadeblas4568a372021-03-24 09:19:48 +0100301 raise EngineException(
302 "invalid Transaction-Id header", HTTPStatus.NOT_FOUND
303 )
tiernob24258a2018-10-04 18:39:49 +0200304 else:
beierlmcee2ebf2022-03-29 17:42:48 -0400305 self.fs.file_delete(proposed_revision_path, ignore_non_exist=True)
306 self.fs.mkdir(proposed_revision_path)
beierlmbc5a5242022-05-17 21:25:29 -0400307 fs_rollback.append(proposed_revision_path)
tiernob24258a2018-10-04 18:39:49 +0200308
309 storage = self.fs.get_params()
beierlmbc5a5242022-05-17 21:25:29 -0400310 storage["folder"] = proposed_revision_path
tiernob24258a2018-10-04 18:39:49 +0200311
beierlmcee2ebf2022-03-29 17:42:48 -0400312 file_path = (proposed_revision_path, filename)
garciadeblas4568a372021-03-24 09:19:48 +0100313 if self.fs.file_exists(file_path, "file"):
tiernob24258a2018-10-04 18:39:49 +0200314 file_size = self.fs.file_size(file_path)
315 else:
316 file_size = 0
317 if file_size != start:
garciadeblas4568a372021-03-24 09:19:48 +0100318 raise EngineException(
319 "invalid Content-Range start sequence, expected '{}' but received '{}'".format(
320 file_size, start
321 ),
322 HTTPStatus.REQUESTED_RANGE_NOT_SATISFIABLE,
323 )
324 file_pkg = self.fs.file_open(file_path, "a+b")
tiernob24258a2018-10-04 18:39:49 +0200325 if isinstance(indata, dict):
326 indata_text = yaml.safe_dump(indata, indent=4, default_flow_style=False)
327 file_pkg.write(indata_text.encode(encoding="utf-8"))
328 else:
329 indata_len = 0
330 while True:
331 indata_text = indata.read(4096)
332 indata_len += len(indata_text)
333 if not indata_text:
334 break
335 file_pkg.write(indata_text)
336 if content_range_text:
garciaale960531a2020-10-20 18:29:45 -0300337 if indata_len != end - start:
garciadeblas4568a372021-03-24 09:19:48 +0100338 raise EngineException(
339 "Mismatch between Content-Range header {}-{} and body length of {}".format(
340 start, end - 1, indata_len
341 ),
342 HTTPStatus.REQUESTED_RANGE_NOT_SATISFIABLE,
343 )
tiernob24258a2018-10-04 18:39:49 +0200344 if end != total:
345 # TODO update to UPLOADING
346 return False
347
348 # PACKAGE UPLOADED
349 if expected_md5:
350 file_pkg.seek(0, 0)
351 file_md5 = md5()
352 chunk_data = file_pkg.read(1024)
353 while chunk_data:
354 file_md5.update(chunk_data)
355 chunk_data = file_pkg.read(1024)
356 if expected_md5 != file_md5.hexdigest():
357 raise EngineException("Error, MD5 mismatch", HTTPStatus.CONFLICT)
358 file_pkg.seek(0, 0)
359 if compressed == "gzip":
garciadeblas4568a372021-03-24 09:19:48 +0100360 tar = tarfile.open(mode="r", fileobj=file_pkg)
tiernob24258a2018-10-04 18:39:49 +0200361 descriptor_file_name = None
362 for tarinfo in tar:
363 tarname = tarinfo.name
364 tarname_path = tarname.split("/")
garciadeblas4568a372021-03-24 09:19:48 +0100365 if (
366 not tarname_path[0] or ".." in tarname_path
367 ): # if start with "/" means absolute path
368 raise EngineException(
369 "Absolute path or '..' are not allowed for package descriptor tar.gz"
370 )
tiernob24258a2018-10-04 18:39:49 +0200371 if len(tarname_path) == 1 and not tarinfo.isdir():
garciadeblas4568a372021-03-24 09:19:48 +0100372 raise EngineException(
373 "All files must be inside a dir for package descriptor tar.gz"
374 )
375 if (
376 tarname.endswith(".yaml")
377 or tarname.endswith(".json")
378 or tarname.endswith(".yml")
379 ):
tiernob24258a2018-10-04 18:39:49 +0200380 storage["pkg-dir"] = tarname_path[0]
381 if len(tarname_path) == 2:
382 if descriptor_file_name:
383 raise EngineException(
garciadeblas4568a372021-03-24 09:19:48 +0100384 "Found more than one descriptor file at package descriptor tar.gz"
385 )
tiernob24258a2018-10-04 18:39:49 +0200386 descriptor_file_name = tarname
387 if not descriptor_file_name:
garciadeblas4568a372021-03-24 09:19:48 +0100388 raise EngineException(
389 "Not found any descriptor file at package descriptor tar.gz"
390 )
tiernob24258a2018-10-04 18:39:49 +0200391 storage["descriptor"] = descriptor_file_name
392 storage["zipfile"] = filename
beierlmcee2ebf2022-03-29 17:42:48 -0400393 self.fs.file_extract(tar, proposed_revision_path)
garciadeblas4568a372021-03-24 09:19:48 +0100394 with self.fs.file_open(
beierlmcee2ebf2022-03-29 17:42:48 -0400395 (proposed_revision_path, descriptor_file_name), "r"
garciadeblas4568a372021-03-24 09:19:48 +0100396 ) as descriptor_file:
tiernob24258a2018-10-04 18:39:49 +0200397 content = descriptor_file.read()
bravofc26740a2021-11-08 09:44:54 -0300398 elif compressed == "zip":
399 zipfile = ZipFile(file_pkg)
400 descriptor_file_name = None
401 for package_file in zipfile.infolist():
402 zipfilename = package_file.filename
403 file_path = zipfilename.split("/")
404 if (
405 not file_path[0] or ".." in zipfilename
406 ): # if start with "/" means absolute path
407 raise EngineException(
408 "Absolute path or '..' are not allowed for package descriptor zip"
409 )
410
411 if (
garciadeblasf2af4a12023-01-24 16:56:54 +0100412 zipfilename.endswith(".yaml")
413 or zipfilename.endswith(".json")
414 or zipfilename.endswith(".yml")
415 ) and (
416 zipfilename.find("/") < 0
417 or zipfilename.find("Definitions") >= 0
bravofc26740a2021-11-08 09:44:54 -0300418 ):
419 storage["pkg-dir"] = ""
420 if descriptor_file_name:
421 raise EngineException(
422 "Found more than one descriptor file at package descriptor zip"
423 )
424 descriptor_file_name = zipfilename
425 if not descriptor_file_name:
426 raise EngineException(
427 "Not found any descriptor file at package descriptor zip"
428 )
429 storage["descriptor"] = descriptor_file_name
430 storage["zipfile"] = filename
beierlmcee2ebf2022-03-29 17:42:48 -0400431 self.fs.file_extract(zipfile, proposed_revision_path)
bravofc26740a2021-11-08 09:44:54 -0300432
433 with self.fs.file_open(
beierlmcee2ebf2022-03-29 17:42:48 -0400434 (proposed_revision_path, descriptor_file_name), "r"
bravofc26740a2021-11-08 09:44:54 -0300435 ) as descriptor_file:
436 content = descriptor_file.read()
tiernob24258a2018-10-04 18:39:49 +0200437 else:
438 content = file_pkg.read()
439 storage["descriptor"] = descriptor_file_name = filename
440
441 if descriptor_file_name.endswith(".json"):
442 error_text = "Invalid json format "
443 indata = json.load(content)
444 else:
445 error_text = "Invalid yaml format "
garciadeblas4cd875d2023-02-14 19:05:34 +0100446 indata = yaml.safe_load(content)
tiernob24258a2018-10-04 18:39:49 +0200447
beierlmcee2ebf2022-03-29 17:42:48 -0400448 # Need to close the file package here so it can be copied from the
449 # revision to the current, unrevisioned record
450 if file_pkg:
451 file_pkg.close()
452 file_pkg = None
453
454 # Fetch both the incoming, proposed revision and the original revision so we
455 # can call a validate method to compare them
456 current_revision_path = _id + "/"
457 self.fs.sync(from_path=current_revision_path)
458 self.fs.sync(from_path=proposed_revision_path)
459
460 if revision > 1:
461 try:
462 self._validate_descriptor_changes(
aticig2b5e1232022-08-10 17:30:12 +0300463 _id,
beierlmcee2ebf2022-03-29 17:42:48 -0400464 descriptor_file_name,
465 current_revision_path,
aticig2b5e1232022-08-10 17:30:12 +0300466 proposed_revision_path,
467 )
beierlmcee2ebf2022-03-29 17:42:48 -0400468 except Exception as e:
garciadeblasf2af4a12023-01-24 16:56:54 +0100469 shutil.rmtree(
470 self.fs.path + current_revision_path, ignore_errors=True
471 )
472 shutil.rmtree(
473 self.fs.path + proposed_revision_path, ignore_errors=True
474 )
beierlmcee2ebf2022-03-29 17:42:48 -0400475 # Only delete the new revision. We need to keep the original version in place
476 # as it has not been changed.
477 self.fs.file_delete(proposed_revision_path, ignore_non_exist=True)
478 raise e
479
tiernob24258a2018-10-04 18:39:49 +0200480 indata = self._remove_envelop(indata)
481
482 # Override descriptor with query string kwargs
483 if kwargs:
484 self._update_input_with_kwargs(indata, kwargs)
tiernob24258a2018-10-04 18:39:49 +0200485
beierlmbc5a5242022-05-17 21:25:29 -0400486 current_desc["_admin"]["storage"] = storage
487 current_desc["_admin"]["onboardingState"] = "ONBOARDED"
488 current_desc["_admin"]["operationalState"] = "ENABLED"
489 current_desc["_admin"]["modified"] = time()
490 current_desc["_admin"]["revision"] = revision
491
tiernob24258a2018-10-04 18:39:49 +0200492 deep_update_rfc7396(current_desc, indata)
garciadeblas4568a372021-03-24 09:19:48 +0100493 current_desc = self.check_conflict_on_edit(
494 session, current_desc, indata, _id=_id
495 )
beierlmbc5a5242022-05-17 21:25:29 -0400496
497 # Copy the revision to the active package name by its original id
498 shutil.rmtree(self.fs.path + current_revision_path, ignore_errors=True)
garciadeblasf2af4a12023-01-24 16:56:54 +0100499 os.rename(
500 self.fs.path + proposed_revision_path,
501 self.fs.path + current_revision_path,
502 )
beierlmbc5a5242022-05-17 21:25:29 -0400503 self.fs.file_delete(current_revision_path, ignore_non_exist=True)
504 self.fs.mkdir(current_revision_path)
505 self.fs.reverse_sync(from_path=current_revision_path)
506
507 shutil.rmtree(self.fs.path + _id)
508
tiernob24258a2018-10-04 18:39:49 +0200509 self.db.replace(self.topic, _id, current_desc)
beierlmcee2ebf2022-03-29 17:42:48 -0400510
511 # Store a copy of the package as a point in time revision
512 revision_desc = dict(current_desc)
513 revision_desc["_id"] = _id + ":" + str(revision_desc["_admin"]["revision"])
514 self.db.create(self.topic + "_revisions", revision_desc)
beierlmbc5a5242022-05-17 21:25:29 -0400515 fs_rollback = []
tiernob24258a2018-10-04 18:39:49 +0200516
517 indata["_id"] = _id
K Sai Kiranc96fd692019-10-16 17:50:53 +0530518 self._send_msg("edited", indata)
tiernob24258a2018-10-04 18:39:49 +0200519
520 # TODO if descriptor has changed because kwargs update content and remove cached zip
521 # TODO if zip is not present creates one
522 return True
523
524 except EngineException:
525 raise
526 except IndexError:
garciadeblas4568a372021-03-24 09:19:48 +0100527 raise EngineException(
528 "invalid Content-Range header format. Expected 'bytes start-end/total'",
529 HTTPStatus.REQUESTED_RANGE_NOT_SATISFIABLE,
530 )
tiernob24258a2018-10-04 18:39:49 +0200531 except IOError as e:
garciadeblas4568a372021-03-24 09:19:48 +0100532 raise EngineException(
533 "invalid upload transaction sequence: '{}'".format(e),
534 HTTPStatus.BAD_REQUEST,
535 )
tiernob24258a2018-10-04 18:39:49 +0200536 except tarfile.ReadError as e:
garciadeblas4568a372021-03-24 09:19:48 +0100537 raise EngineException(
538 "invalid file content {}".format(e), HTTPStatus.BAD_REQUEST
539 )
tiernob24258a2018-10-04 18:39:49 +0200540 except (ValueError, yaml.YAMLError) as e:
541 raise EngineException(error_text + str(e))
542 except ValidationError as e:
543 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
544 finally:
545 if file_pkg:
546 file_pkg.close()
beierlmbc5a5242022-05-17 21:25:29 -0400547 for file in fs_rollback:
548 self.fs.file_delete(file, ignore_non_exist=True)
tiernob24258a2018-10-04 18:39:49 +0200549
550 def get_file(self, session, _id, path=None, accept_header=None):
551 """
552 Return the file content of a vnfd or nsd
tierno65ca36d2019-02-12 19:27:52 +0100553 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tierno87006042018-10-24 12:50:20 +0200554 :param _id: Identity of the vnfd, nsd
tiernob24258a2018-10-04 18:39:49 +0200555 :param path: artifact path or "$DESCRIPTOR" or None
556 :param accept_header: Content of Accept header. Must contain applition/zip or/and text/plain
tierno87006042018-10-24 12:50:20 +0200557 :return: opened file plus Accept format or raises an exception
tiernob24258a2018-10-04 18:39:49 +0200558 """
559 accept_text = accept_zip = False
560 if accept_header:
garciadeblas4568a372021-03-24 09:19:48 +0100561 if "text/plain" in accept_header or "*/*" in accept_header:
tiernob24258a2018-10-04 18:39:49 +0200562 accept_text = True
garciadeblas4568a372021-03-24 09:19:48 +0100563 if "application/zip" in accept_header or "*/*" in accept_header:
564 accept_zip = "application/zip"
565 elif "application/gzip" in accept_header:
566 accept_zip = "application/gzip"
tierno87006042018-10-24 12:50:20 +0200567
tiernob24258a2018-10-04 18:39:49 +0200568 if not accept_text and not accept_zip:
garciadeblas4568a372021-03-24 09:19:48 +0100569 raise EngineException(
570 "provide request header 'Accept' with 'application/zip' or 'text/plain'",
571 http_code=HTTPStatus.NOT_ACCEPTABLE,
572 )
tiernob24258a2018-10-04 18:39:49 +0200573
574 content = self.show(session, _id)
575 if content["_admin"]["onboardingState"] != "ONBOARDED":
garciadeblas4568a372021-03-24 09:19:48 +0100576 raise EngineException(
577 "Cannot get content because this resource is not at 'ONBOARDED' state. "
578 "onboardingState is {}".format(content["_admin"]["onboardingState"]),
579 http_code=HTTPStatus.CONFLICT,
580 )
tiernob24258a2018-10-04 18:39:49 +0200581 storage = content["_admin"]["storage"]
garciaale960531a2020-10-20 18:29:45 -0300582 if path is not None and path != "$DESCRIPTOR": # artifacts
selvi.j5be838c2022-08-25 06:24:49 +0000583 if not storage.get("pkg-dir") and not storage.get("folder"):
garciadeblas4568a372021-03-24 09:19:48 +0100584 raise EngineException(
585 "Packages does not contains artifacts",
586 http_code=HTTPStatus.BAD_REQUEST,
587 )
588 if self.fs.file_exists(
589 (storage["folder"], storage["pkg-dir"], *path), "dir"
590 ):
591 folder_content = self.fs.dir_ls(
592 (storage["folder"], storage["pkg-dir"], *path)
593 )
tiernob24258a2018-10-04 18:39:49 +0200594 return folder_content, "text/plain"
595 # TODO manage folders in http
596 else:
garciadeblas4568a372021-03-24 09:19:48 +0100597 return (
598 self.fs.file_open(
599 (storage["folder"], storage["pkg-dir"], *path), "rb"
600 ),
601 "application/octet-stream",
602 )
tiernob24258a2018-10-04 18:39:49 +0200603
604 # pkgtype accept ZIP TEXT -> result
605 # manyfiles yes X -> zip
606 # no yes -> error
607 # onefile yes no -> zip
608 # X yes -> text
tiernoee002752020-08-04 14:14:16 +0000609 contain_many_files = False
garciadeblas4568a372021-03-24 09:19:48 +0100610 if storage.get("pkg-dir"):
tiernoee002752020-08-04 14:14:16 +0000611 # check if there are more than one file in the package, ignoring checksums.txt.
garciadeblas4568a372021-03-24 09:19:48 +0100612 pkg_files = self.fs.dir_ls((storage["folder"], storage["pkg-dir"]))
613 if len(pkg_files) >= 3 or (
614 len(pkg_files) == 2 and "checksums.txt" not in pkg_files
615 ):
tiernoee002752020-08-04 14:14:16 +0000616 contain_many_files = True
617 if accept_text and (not contain_many_files or path == "$DESCRIPTOR"):
garciadeblas4568a372021-03-24 09:19:48 +0100618 return (
619 self.fs.file_open((storage["folder"], storage["descriptor"]), "r"),
620 "text/plain",
621 )
tiernoee002752020-08-04 14:14:16 +0000622 elif contain_many_files and not accept_zip:
garciadeblas4568a372021-03-24 09:19:48 +0100623 raise EngineException(
624 "Packages that contains several files need to be retrieved with 'application/zip'"
625 "Accept header",
626 http_code=HTTPStatus.NOT_ACCEPTABLE,
627 )
tiernob24258a2018-10-04 18:39:49 +0200628 else:
garciadeblas4568a372021-03-24 09:19:48 +0100629 if not storage.get("zipfile"):
tiernob24258a2018-10-04 18:39:49 +0200630 # TODO generate zipfile if not present
garciadeblas4568a372021-03-24 09:19:48 +0100631 raise EngineException(
632 "Only allowed 'text/plain' Accept header for this descriptor. To be solved in "
633 "future versions",
634 http_code=HTTPStatus.NOT_ACCEPTABLE,
635 )
636 return (
637 self.fs.file_open((storage["folder"], storage["zipfile"]), "rb"),
638 accept_zip,
639 )
tiernob24258a2018-10-04 18:39:49 +0200640
garciaale7cbd03c2020-11-27 10:38:35 -0300641 def _remove_yang_prefixes_from_descriptor(self, descriptor):
642 new_descriptor = {}
643 for k, v in descriptor.items():
644 new_v = v
645 if isinstance(v, dict):
646 new_v = self._remove_yang_prefixes_from_descriptor(v)
647 elif isinstance(v, list):
648 new_v = list()
649 for x in v:
650 if isinstance(x, dict):
651 new_v.append(self._remove_yang_prefixes_from_descriptor(x))
652 else:
653 new_v.append(x)
garciadeblas4568a372021-03-24 09:19:48 +0100654 new_descriptor[k.split(":")[-1]] = new_v
garciaale7cbd03c2020-11-27 10:38:35 -0300655 return new_descriptor
656
gcalvino46e4cb82018-10-26 13:10:22 +0200657 def pyangbind_validation(self, item, data, force=False):
garciadeblas4568a372021-03-24 09:19:48 +0100658 raise EngineException(
659 "Not possible to validate '{}' item".format(item),
660 http_code=HTTPStatus.INTERNAL_SERVER_ERROR,
661 )
gcalvino46e4cb82018-10-26 13:10:22 +0200662
Frank Brydendeba68e2020-07-27 13:55:11 +0000663 def _validate_input_edit(self, indata, content, force=False):
664 # not needed to validate with pyangbind becuase it will be validated at check_conflict_on_edit
665 if "_id" in indata:
666 indata.pop("_id")
667 if "_admin" not in indata:
668 indata["_admin"] = {}
669
670 if "operationalState" in indata:
671 if indata["operationalState"] in ("ENABLED", "DISABLED"):
672 indata["_admin"]["operationalState"] = indata.pop("operationalState")
673 else:
garciadeblas4568a372021-03-24 09:19:48 +0100674 raise EngineException(
675 "State '{}' is not a valid operational state".format(
676 indata["operationalState"]
677 ),
678 http_code=HTTPStatus.BAD_REQUEST,
679 )
Frank Brydendeba68e2020-07-27 13:55:11 +0000680
garciadeblas4568a372021-03-24 09:19:48 +0100681 # 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 +0000682 # to preserve current expected behaviour
683 if "userDefinedData" in indata:
684 data = indata.pop("userDefinedData")
gaticid7debb92023-07-31 14:37:32 +0300685 if isinstance(data, dict):
Frank Brydendeba68e2020-07-27 13:55:11 +0000686 indata["_admin"]["userDefinedData"] = data
687 else:
garciadeblas4568a372021-03-24 09:19:48 +0100688 raise EngineException(
689 "userDefinedData should be an object, but is '{}' instead".format(
690 type(data)
691 ),
692 http_code=HTTPStatus.BAD_REQUEST,
693 )
garciaale960531a2020-10-20 18:29:45 -0300694
garciadeblas4568a372021-03-24 09:19:48 +0100695 if (
696 "operationalState" in indata["_admin"]
697 and content["_admin"]["operationalState"]
698 == indata["_admin"]["operationalState"]
699 ):
700 raise EngineException(
701 "operationalState already {}".format(
702 content["_admin"]["operationalState"]
703 ),
704 http_code=HTTPStatus.CONFLICT,
705 )
Frank Brydendeba68e2020-07-27 13:55:11 +0000706
707 return indata
708
aticig2b5e1232022-08-10 17:30:12 +0300709 def _validate_descriptor_changes(
710 self,
711 descriptor_id,
beierlmcee2ebf2022-03-29 17:42:48 -0400712 descriptor_file_name,
713 old_descriptor_directory,
garciadeblasf2af4a12023-01-24 16:56:54 +0100714 new_descriptor_directory,
aticig2b5e1232022-08-10 17:30:12 +0300715 ):
beierlmcee2ebf2022-03-29 17:42:48 -0400716 # Example:
717 # raise EngineException(
718 # "Error in validating new descriptor: <NODE> cannot be modified",
719 # http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
720 # )
721 pass
tiernob24258a2018-10-04 18:39:49 +0200722
garciadeblasf2af4a12023-01-24 16:56:54 +0100723
tiernob24258a2018-10-04 18:39:49 +0200724class VnfdTopic(DescriptorTopic):
725 topic = "vnfds"
726 topic_msg = "vnfd"
727
delacruzramo32bab472019-09-13 12:24:22 +0200728 def __init__(self, db, fs, msg, auth):
729 DescriptorTopic.__init__(self, db, fs, msg, auth)
tiernob24258a2018-10-04 18:39:49 +0200730
garciaale7cbd03c2020-11-27 10:38:35 -0300731 def pyangbind_validation(self, item, data, force=False):
garciaaledf718ae2020-12-03 19:17:28 -0300732 if self._descriptor_data_is_in_old_format(data):
garciadeblas4568a372021-03-24 09:19:48 +0100733 raise EngineException(
734 "ERROR: Unsupported descriptor format. Please, use an ETSI SOL006 descriptor.",
735 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
736 )
garciaale7cbd03c2020-11-27 10:38:35 -0300737 try:
garciaale7cbd03c2020-11-27 10:38:35 -0300738 myvnfd = etsi_nfv_vnfd.etsi_nfv_vnfd()
garciadeblas4568a372021-03-24 09:19:48 +0100739 pybindJSONDecoder.load_ietf_json(
740 {"etsi-nfv-vnfd:vnfd": data},
741 None,
742 None,
743 obj=myvnfd,
744 path_helper=True,
745 skip_unknown=force,
746 )
garciaale7cbd03c2020-11-27 10:38:35 -0300747 out = pybindJSON.dumps(myvnfd, mode="ietf")
748 desc_out = self._remove_envelop(yaml.safe_load(out))
749 desc_out = self._remove_yang_prefixes_from_descriptor(desc_out)
bravof41a52052021-02-17 18:08:01 -0300750 return utils.deep_update_dict(data, desc_out)
garciaale7cbd03c2020-11-27 10:38:35 -0300751 except Exception as e:
garciadeblas4568a372021-03-24 09:19:48 +0100752 raise EngineException(
753 "Error in pyangbind validation: {}".format(str(e)),
754 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
755 )
garciaale7cbd03c2020-11-27 10:38:35 -0300756
tiernob24258a2018-10-04 18:39:49 +0200757 @staticmethod
garciaaledf718ae2020-12-03 19:17:28 -0300758 def _descriptor_data_is_in_old_format(data):
garciadeblas4568a372021-03-24 09:19:48 +0100759 return ("vnfd-catalog" in data) or ("vnfd:vnfd-catalog" in data)
garciaaledf718ae2020-12-03 19:17:28 -0300760
761 @staticmethod
tiernob24258a2018-10-04 18:39:49 +0200762 def _remove_envelop(indata=None):
763 if not indata:
764 return {}
765 clean_indata = indata
garciaale7cbd03c2020-11-27 10:38:35 -0300766
garciadeblas4568a372021-03-24 09:19:48 +0100767 if clean_indata.get("etsi-nfv-vnfd:vnfd"):
768 if not isinstance(clean_indata["etsi-nfv-vnfd:vnfd"], dict):
garciaale7cbd03c2020-11-27 10:38:35 -0300769 raise EngineException("'etsi-nfv-vnfd:vnfd' must be a dict")
garciadeblas4568a372021-03-24 09:19:48 +0100770 clean_indata = clean_indata["etsi-nfv-vnfd:vnfd"]
771 elif clean_indata.get("vnfd"):
772 if not isinstance(clean_indata["vnfd"], dict):
garciaale7cbd03c2020-11-27 10:38:35 -0300773 raise EngineException("'vnfd' must be dict")
garciadeblas4568a372021-03-24 09:19:48 +0100774 clean_indata = clean_indata["vnfd"]
garciaale7cbd03c2020-11-27 10:38:35 -0300775
tiernob24258a2018-10-04 18:39:49 +0200776 return clean_indata
777
tierno65ca36d2019-02-12 19:27:52 +0100778 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
garciadeblas4568a372021-03-24 09:19:48 +0100779 final_content = super().check_conflict_on_edit(
780 session, final_content, edit_content, _id
781 )
tierno36ec8602018-11-02 17:27:11 +0100782
783 # set type of vnfd
784 contains_pdu = False
785 contains_vdu = False
786 for vdu in get_iterable(final_content.get("vdu")):
787 if vdu.get("pdu-type"):
788 contains_pdu = True
789 else:
790 contains_vdu = True
791 if contains_pdu:
792 final_content["_admin"]["type"] = "hnfd" if contains_vdu else "pnfd"
793 elif contains_vdu:
794 final_content["_admin"]["type"] = "vnfd"
795 # if neither vud nor pdu do not fill type
bravofb995ea22021-02-10 10:57:52 -0300796 return final_content
tierno36ec8602018-11-02 17:27:11 +0100797
tiernob4844ab2019-05-23 08:42:12 +0000798 def check_conflict_on_del(self, session, _id, db_content):
tiernob24258a2018-10-04 18:39:49 +0200799 """
800 Check that there is not any NSD that uses this VNFD. Only NSDs belonging to this project are considered. Note
801 that VNFD can be public and be used by NSD of other projects. Also check there are not deployments, or vnfr
802 that uses this vnfd
tierno65ca36d2019-02-12 19:27:52 +0100803 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob4844ab2019-05-23 08:42:12 +0000804 :param _id: vnfd internal id
805 :param db_content: The database content of the _id.
tiernob24258a2018-10-04 18:39:49 +0200806 :return: None or raises EngineException with the conflict
807 """
tierno65ca36d2019-02-12 19:27:52 +0100808 if session["force"]:
tiernob24258a2018-10-04 18:39:49 +0200809 return
tiernob4844ab2019-05-23 08:42:12 +0000810 descriptor = db_content
tiernob24258a2018-10-04 18:39:49 +0200811 descriptor_id = descriptor.get("id")
812 if not descriptor_id: # empty vnfd not uploaded
813 return
814
tierno65ca36d2019-02-12 19:27:52 +0100815 _filter = self._get_project_filter(session)
tiernob4844ab2019-05-23 08:42:12 +0000816
tiernob24258a2018-10-04 18:39:49 +0200817 # check vnfrs using this vnfd
818 _filter["vnfd-id"] = _id
819 if self.db.get_list("vnfrs", _filter):
garciadeblas4568a372021-03-24 09:19:48 +0100820 raise EngineException(
821 "There is at least one VNF instance using this descriptor",
822 http_code=HTTPStatus.CONFLICT,
823 )
tiernob4844ab2019-05-23 08:42:12 +0000824
825 # check NSD referencing this VNFD
tiernob24258a2018-10-04 18:39:49 +0200826 del _filter["vnfd-id"]
garciadeblasf576eb92021-04-18 20:54:13 +0000827 _filter["vnfd-id"] = descriptor_id
tiernob24258a2018-10-04 18:39:49 +0200828 if self.db.get_list("nsds", _filter):
garciadeblas4568a372021-03-24 09:19:48 +0100829 raise EngineException(
830 "There is at least one NS package referencing this descriptor",
831 http_code=HTTPStatus.CONFLICT,
832 )
tiernob24258a2018-10-04 18:39:49 +0200833
gcalvinoa6fe0002019-01-09 13:27:11 +0100834 def _validate_input_new(self, indata, storage_params, force=False):
Frank Bryden19b97522020-07-10 12:32:02 +0000835 indata.pop("onboardingState", None)
836 indata.pop("operationalState", None)
837 indata.pop("usageState", None)
Frank Bryden19b97522020-07-10 12:32:02 +0000838 indata.pop("links", None)
839
gcalvino46e4cb82018-10-26 13:10:22 +0200840 indata = self.pyangbind_validation("vnfds", indata, force)
gcalvino5e72d152018-10-23 11:46:57 +0200841 # Cross references validation in the descriptor
garciaale7cbd03c2020-11-27 10:38:35 -0300842
843 self.validate_mgmt_interface_connection_point(indata)
gcalvino5e72d152018-10-23 11:46:57 +0200844
845 for vdu in get_iterable(indata.get("vdu")):
garciaale7cbd03c2020-11-27 10:38:35 -0300846 self.validate_vdu_internal_connection_points(vdu)
garciaale960531a2020-10-20 18:29:45 -0300847 self._validate_vdu_cloud_init_in_package(storage_params, vdu, indata)
bravof41a52052021-02-17 18:08:01 -0300848 self._validate_vdu_charms_in_package(storage_params, indata)
garciaale960531a2020-10-20 18:29:45 -0300849
850 self._validate_vnf_charms_in_package(storage_params, indata)
851
garciaale7cbd03c2020-11-27 10:38:35 -0300852 self.validate_external_connection_points(indata)
853 self.validate_internal_virtual_links(indata)
garciaale960531a2020-10-20 18:29:45 -0300854 self.validate_monitoring_params(indata)
855 self.validate_scaling_group_descriptor(indata)
Daniel Arndt00f83aa2023-06-15 16:43:33 +0200856 self.validate_helm_chart(indata)
garciaale960531a2020-10-20 18:29:45 -0300857
858 return indata
859
860 @staticmethod
Daniel Arndt00f83aa2023-06-15 16:43:33 +0200861 def validate_helm_chart(indata):
Gabriel Cuba646773d2023-11-20 01:43:05 -0500862 def is_url(url):
863 result = urlparse(url)
864 return all([result.scheme, result.netloc])
865
Daniel Arndt00f83aa2023-06-15 16:43:33 +0200866 kdus = indata.get("kdu", [])
867 for kdu in kdus:
868 helm_chart_value = kdu.get("helm-chart")
869 if not helm_chart_value:
870 continue
Gabriel Cuba646773d2023-11-20 01:43:05 -0500871 if not (
872 valid_helm_chart_re.match(helm_chart_value) or is_url(helm_chart_value)
873 ):
Daniel Arndt00f83aa2023-06-15 16:43:33 +0200874 raise EngineException(
875 "helm-chart '{}' is not valid".format(helm_chart_value),
876 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
877 )
878
879 @staticmethod
garciaale7cbd03c2020-11-27 10:38:35 -0300880 def validate_mgmt_interface_connection_point(indata):
garciaale960531a2020-10-20 18:29:45 -0300881 if not indata.get("vdu"):
882 return
garciaale7cbd03c2020-11-27 10:38:35 -0300883 if not indata.get("mgmt-cp"):
garciadeblas4568a372021-03-24 09:19:48 +0100884 raise EngineException(
885 "'mgmt-cp' is a mandatory field and it is not defined",
886 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
887 )
garciaale7cbd03c2020-11-27 10:38:35 -0300888
889 for cp in get_iterable(indata.get("ext-cpd")):
890 if cp["id"] == indata["mgmt-cp"]:
891 break
892 else:
garciadeblas4568a372021-03-24 09:19:48 +0100893 raise EngineException(
894 "mgmt-cp='{}' must match an existing ext-cpd".format(indata["mgmt-cp"]),
895 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
896 )
garciaale960531a2020-10-20 18:29:45 -0300897
898 @staticmethod
garciaale7cbd03c2020-11-27 10:38:35 -0300899 def validate_vdu_internal_connection_points(vdu):
900 int_cpds = set()
901 for cpd in get_iterable(vdu.get("int-cpd")):
902 cpd_id = cpd.get("id")
903 if cpd_id and cpd_id in int_cpds:
garciadeblas4568a372021-03-24 09:19:48 +0100904 raise EngineException(
905 "vdu[id='{}']:int-cpd[id='{}'] is already used by other int-cpd".format(
906 vdu["id"], cpd_id
907 ),
908 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
909 )
garciaale7cbd03c2020-11-27 10:38:35 -0300910 int_cpds.add(cpd_id)
911
912 @staticmethod
913 def validate_external_connection_points(indata):
914 all_vdus_int_cpds = set()
915 for vdu in get_iterable(indata.get("vdu")):
916 for int_cpd in get_iterable(vdu.get("int-cpd")):
917 all_vdus_int_cpds.add((vdu.get("id"), int_cpd.get("id")))
918
919 ext_cpds = set()
920 for cpd in get_iterable(indata.get("ext-cpd")):
921 cpd_id = cpd.get("id")
922 if cpd_id and cpd_id in ext_cpds:
garciadeblas4568a372021-03-24 09:19:48 +0100923 raise EngineException(
924 "ext-cpd[id='{}'] is already used by other ext-cpd".format(cpd_id),
925 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
926 )
garciaale7cbd03c2020-11-27 10:38:35 -0300927 ext_cpds.add(cpd_id)
928
929 int_cpd = cpd.get("int-cpd")
930 if int_cpd:
931 if (int_cpd.get("vdu-id"), int_cpd.get("cpd")) not in all_vdus_int_cpds:
garciadeblas4568a372021-03-24 09:19:48 +0100932 raise EngineException(
933 "ext-cpd[id='{}']:int-cpd must match an existing vdu int-cpd".format(
934 cpd_id
935 ),
936 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
937 )
garciaale7cbd03c2020-11-27 10:38:35 -0300938 # TODO: Validate k8s-cluster-net points to a valid k8s-cluster:nets ?
garciaale960531a2020-10-20 18:29:45 -0300939
bravof41a52052021-02-17 18:08:01 -0300940 def _validate_vdu_charms_in_package(self, storage_params, indata):
941 for df in indata["df"]:
garciadeblas4568a372021-03-24 09:19:48 +0100942 if (
943 "lcm-operations-configuration" in df
944 and "operate-vnf-op-config" in df["lcm-operations-configuration"]
945 ):
946 configs = df["lcm-operations-configuration"][
947 "operate-vnf-op-config"
948 ].get("day1-2", [])
garciaale2c4f9ec2021-03-01 11:04:50 -0300949 vdus = df.get("vdu-profile", [])
bravof23258282021-02-22 18:04:40 -0300950 for vdu in vdus:
951 for config in configs:
952 if config["id"] == vdu["id"] and utils.find_in_list(
953 config.get("execution-environment-list", []),
garciadeblas4568a372021-03-24 09:19:48 +0100954 lambda ee: "juju" in ee,
bravof23258282021-02-22 18:04:40 -0300955 ):
garciadeblas4568a372021-03-24 09:19:48 +0100956 if not self._validate_package_folders(
957 storage_params, "charms"
bravofc26740a2021-11-08 09:44:54 -0300958 ) and not self._validate_package_folders(
959 storage_params, "Scripts/charms"
garciadeblas4568a372021-03-24 09:19:48 +0100960 ):
961 raise EngineException(
962 "Charm defined in vnf[id={}] but not present in "
963 "package".format(indata["id"])
964 )
garciaale960531a2020-10-20 18:29:45 -0300965
966 def _validate_vdu_cloud_init_in_package(self, storage_params, vdu, indata):
967 if not vdu.get("cloud-init-file"):
968 return
garciadeblas4568a372021-03-24 09:19:48 +0100969 if not self._validate_package_folders(
970 storage_params, "cloud_init", vdu["cloud-init-file"]
bravofc26740a2021-11-08 09:44:54 -0300971 ) and not self._validate_package_folders(
972 storage_params, "Scripts/cloud_init", vdu["cloud-init-file"]
garciadeblas4568a372021-03-24 09:19:48 +0100973 ):
974 raise EngineException(
975 "Cloud-init defined in vnf[id={}]:vdu[id={}] but not present in "
976 "package".format(indata["id"], vdu["id"])
977 )
garciaale960531a2020-10-20 18:29:45 -0300978
979 def _validate_vnf_charms_in_package(self, storage_params, indata):
bravof41a52052021-02-17 18:08:01 -0300980 # Get VNF configuration through new container
garciadeblas4568a372021-03-24 09:19:48 +0100981 for deployment_flavor in indata.get("df", []):
bravof41a52052021-02-17 18:08:01 -0300982 if "lcm-operations-configuration" not in deployment_flavor:
983 return
garciadeblas4568a372021-03-24 09:19:48 +0100984 if (
985 "operate-vnf-op-config"
986 not in deployment_flavor["lcm-operations-configuration"]
987 ):
bravof41a52052021-02-17 18:08:01 -0300988 return
garciadeblas4568a372021-03-24 09:19:48 +0100989 for day_1_2_config in deployment_flavor["lcm-operations-configuration"][
990 "operate-vnf-op-config"
991 ]["day1-2"]:
bravof41a52052021-02-17 18:08:01 -0300992 if day_1_2_config["id"] == indata["id"]:
bravof23258282021-02-22 18:04:40 -0300993 if utils.find_in_list(
994 day_1_2_config.get("execution-environment-list", []),
garciadeblas4568a372021-03-24 09:19:48 +0100995 lambda ee: "juju" in ee,
bravof23258282021-02-22 18:04:40 -0300996 ):
bravofc26740a2021-11-08 09:44:54 -0300997 if not self._validate_package_folders(
998 storage_params, "charms"
999 ) and not self._validate_package_folders(
1000 storage_params, "Scripts/charms"
1001 ):
garciadeblas4568a372021-03-24 09:19:48 +01001002 raise EngineException(
1003 "Charm defined in vnf[id={}] but not present in "
1004 "package".format(indata["id"])
1005 )
garciaale960531a2020-10-20 18:29:45 -03001006
1007 def _validate_package_folders(self, storage_params, folder, file=None):
bravofc26740a2021-11-08 09:44:54 -03001008 if not storage_params:
1009 return False
1010 elif not storage_params.get("pkg-dir"):
1011 if self.fs.file_exists("{}_".format(storage_params["folder"]), "dir"):
garciadeblasf2af4a12023-01-24 16:56:54 +01001012 f = "{}_/{}".format(storage_params["folder"], folder)
bravofc26740a2021-11-08 09:44:54 -03001013 else:
garciadeblasf2af4a12023-01-24 16:56:54 +01001014 f = "{}/{}".format(storage_params["folder"], folder)
bravofc26740a2021-11-08 09:44:54 -03001015 if file:
1016 return self.fs.file_exists("{}/{}".format(f, file), "file")
1017 else:
bravofc26740a2021-11-08 09:44:54 -03001018 if self.fs.file_exists(f, "dir"):
1019 if self.fs.dir_ls(f):
1020 return True
garciaale960531a2020-10-20 18:29:45 -03001021 return False
1022 else:
garciadeblas4568a372021-03-24 09:19:48 +01001023 if self.fs.file_exists("{}_".format(storage_params["folder"]), "dir"):
1024 f = "{}_/{}/{}".format(
1025 storage_params["folder"], storage_params["pkg-dir"], folder
1026 )
garciaale960531a2020-10-20 18:29:45 -03001027 else:
garciadeblas4568a372021-03-24 09:19:48 +01001028 f = "{}/{}/{}".format(
1029 storage_params["folder"], storage_params["pkg-dir"], folder
1030 )
garciaale960531a2020-10-20 18:29:45 -03001031 if file:
garciadeblas4568a372021-03-24 09:19:48 +01001032 return self.fs.file_exists("{}/{}".format(f, file), "file")
garciaale960531a2020-10-20 18:29:45 -03001033 else:
garciadeblas4568a372021-03-24 09:19:48 +01001034 if self.fs.file_exists(f, "dir"):
garciaale960531a2020-10-20 18:29:45 -03001035 if self.fs.dir_ls(f):
1036 return True
1037 return False
1038
1039 @staticmethod
garciaale7cbd03c2020-11-27 10:38:35 -03001040 def validate_internal_virtual_links(indata):
1041 all_ivld_ids = set()
1042 for ivld in get_iterable(indata.get("int-virtual-link-desc")):
1043 ivld_id = ivld.get("id")
1044 if ivld_id and ivld_id in all_ivld_ids:
garciadeblas4568a372021-03-24 09:19:48 +01001045 raise EngineException(
1046 "Duplicated VLD id in int-virtual-link-desc[id={}]".format(ivld_id),
1047 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1048 )
garciaale960531a2020-10-20 18:29:45 -03001049 else:
garciaale7cbd03c2020-11-27 10:38:35 -03001050 all_ivld_ids.add(ivld_id)
garciaale960531a2020-10-20 18:29:45 -03001051
garciaale7cbd03c2020-11-27 10:38:35 -03001052 for vdu in get_iterable(indata.get("vdu")):
1053 for int_cpd in get_iterable(vdu.get("int-cpd")):
1054 int_cpd_ivld_id = int_cpd.get("int-virtual-link-desc")
1055 if int_cpd_ivld_id and int_cpd_ivld_id not in all_ivld_ids:
1056 raise EngineException(
1057 "vdu[id='{}']:int-cpd[id='{}']:int-virtual-link-desc='{}' must match an existing "
garciadeblas4568a372021-03-24 09:19:48 +01001058 "int-virtual-link-desc".format(
1059 vdu["id"], int_cpd["id"], int_cpd_ivld_id
1060 ),
1061 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1062 )
garciaale960531a2020-10-20 18:29:45 -03001063
garciaale7cbd03c2020-11-27 10:38:35 -03001064 for df in get_iterable(indata.get("df")):
1065 for vlp in get_iterable(df.get("virtual-link-profile")):
1066 vlp_ivld_id = vlp.get("id")
1067 if vlp_ivld_id and vlp_ivld_id not in all_ivld_ids:
garciadeblas4568a372021-03-24 09:19:48 +01001068 raise EngineException(
1069 "df[id='{}']:virtual-link-profile='{}' must match an existing "
1070 "int-virtual-link-desc".format(df["id"], vlp_ivld_id),
1071 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1072 )
garciaale7cbd03c2020-11-27 10:38:35 -03001073
garciaale960531a2020-10-20 18:29:45 -03001074 @staticmethod
1075 def validate_monitoring_params(indata):
garciaale7cbd03c2020-11-27 10:38:35 -03001076 all_monitoring_params = set()
1077 for ivld in get_iterable(indata.get("int-virtual-link-desc")):
1078 for mp in get_iterable(ivld.get("monitoring-parameters")):
1079 mp_id = mp.get("id")
1080 if mp_id and mp_id in all_monitoring_params:
garciadeblas4568a372021-03-24 09:19:48 +01001081 raise EngineException(
1082 "Duplicated monitoring-parameter id in "
1083 "int-virtual-link-desc[id='{}']:monitoring-parameters[id='{}']".format(
1084 ivld["id"], mp_id
1085 ),
1086 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1087 )
gcalvino5e72d152018-10-23 11:46:57 +02001088 else:
garciaale7cbd03c2020-11-27 10:38:35 -03001089 all_monitoring_params.add(mp_id)
1090
1091 for vdu in get_iterable(indata.get("vdu")):
1092 for mp in get_iterable(vdu.get("monitoring-parameter")):
1093 mp_id = mp.get("id")
1094 if mp_id and mp_id in all_monitoring_params:
garciadeblas4568a372021-03-24 09:19:48 +01001095 raise EngineException(
1096 "Duplicated monitoring-parameter id in "
1097 "vdu[id='{}']:monitoring-parameter[id='{}']".format(
1098 vdu["id"], mp_id
1099 ),
1100 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1101 )
garciaale7cbd03c2020-11-27 10:38:35 -03001102 else:
1103 all_monitoring_params.add(mp_id)
1104
1105 for df in get_iterable(indata.get("df")):
1106 for mp in get_iterable(df.get("monitoring-parameter")):
1107 mp_id = mp.get("id")
1108 if mp_id and mp_id in all_monitoring_params:
garciadeblas4568a372021-03-24 09:19:48 +01001109 raise EngineException(
1110 "Duplicated monitoring-parameter id in "
1111 "df[id='{}']:monitoring-parameter[id='{}']".format(
1112 df["id"], mp_id
1113 ),
1114 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1115 )
garciaale7cbd03c2020-11-27 10:38:35 -03001116 else:
1117 all_monitoring_params.add(mp_id)
gcalvino5e72d152018-10-23 11:46:57 +02001118
garciaale960531a2020-10-20 18:29:45 -03001119 @staticmethod
1120 def validate_scaling_group_descriptor(indata):
garciaale7cbd03c2020-11-27 10:38:35 -03001121 all_monitoring_params = set()
1122 for ivld in get_iterable(indata.get("int-virtual-link-desc")):
1123 for mp in get_iterable(ivld.get("monitoring-parameters")):
1124 all_monitoring_params.add(mp.get("id"))
1125
1126 for vdu in get_iterable(indata.get("vdu")):
1127 for mp in get_iterable(vdu.get("monitoring-parameter")):
1128 all_monitoring_params.add(mp.get("id"))
1129
1130 for df in get_iterable(indata.get("df")):
1131 for mp in get_iterable(df.get("monitoring-parameter")):
1132 all_monitoring_params.add(mp.get("id"))
1133
1134 for df in get_iterable(indata.get("df")):
1135 for sa in get_iterable(df.get("scaling-aspect")):
1136 for sp in get_iterable(sa.get("scaling-policy")):
1137 for sc in get_iterable(sp.get("scaling-criteria")):
1138 sc_monitoring_param = sc.get("vnf-monitoring-param-ref")
garciadeblas4568a372021-03-24 09:19:48 +01001139 if (
1140 sc_monitoring_param
1141 and sc_monitoring_param not in all_monitoring_params
1142 ):
1143 raise EngineException(
1144 "df[id='{}']:scaling-aspect[id='{}']:scaling-policy"
1145 "[name='{}']:scaling-criteria[name='{}']: "
1146 "vnf-monitoring-param-ref='{}' not defined in any monitoring-param".format(
1147 df["id"],
1148 sa["id"],
1149 sp["name"],
1150 sc["name"],
1151 sc_monitoring_param,
1152 ),
1153 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1154 )
garciaale7cbd03c2020-11-27 10:38:35 -03001155
1156 for sca in get_iterable(sa.get("scaling-config-action")):
garciadeblas4568a372021-03-24 09:19:48 +01001157 if (
1158 "lcm-operations-configuration" not in df
1159 or "operate-vnf-op-config"
1160 not in df["lcm-operations-configuration"]
bravof41a52052021-02-17 18:08:01 -03001161 or not utils.find_in_list(
garciadeblas4568a372021-03-24 09:19:48 +01001162 df["lcm-operations-configuration"][
1163 "operate-vnf-op-config"
1164 ].get("day1-2", []),
1165 lambda config: config["id"] == indata["id"],
1166 )
bravof41a52052021-02-17 18:08:01 -03001167 ):
garciadeblas4568a372021-03-24 09:19:48 +01001168 raise EngineException(
1169 "'day1-2 configuration' not defined in the descriptor but it is "
1170 "referenced by df[id='{}']:scaling-aspect[id='{}']:scaling-config-action".format(
1171 df["id"], sa["id"]
1172 ),
1173 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1174 )
1175 for configuration in get_iterable(
1176 df["lcm-operations-configuration"]["operate-vnf-op-config"].get(
1177 "day1-2", []
1178 )
1179 ):
1180 for primitive in get_iterable(
1181 configuration.get("config-primitive")
1182 ):
1183 if (
1184 primitive["name"]
1185 == sca["vnf-config-primitive-name-ref"]
1186 ):
garciaale7cbd03c2020-11-27 10:38:35 -03001187 break
1188 else:
garciadeblas4568a372021-03-24 09:19:48 +01001189 raise EngineException(
1190 "df[id='{}']:scaling-aspect[id='{}']:scaling-config-action:vnf-"
1191 "config-primitive-name-ref='{}' does not match any "
1192 "day1-2 configuration:config-primitive:name".format(
1193 df["id"],
1194 sa["id"],
1195 sca["vnf-config-primitive-name-ref"],
1196 ),
1197 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1198 )
gcalvinoa6fe0002019-01-09 13:27:11 +01001199
delacruzramo271d2002019-12-02 21:00:37 +01001200 def delete_extra(self, session, _id, db_content, not_send_msg=None):
1201 """
1202 Deletes associate file system storage (via super)
1203 Deletes associated vnfpkgops from database.
1204 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1205 :param _id: server internal id
1206 :param db_content: The database content of the descriptor
1207 :return: None
1208 :raises: FsException in case of error while deleting associated storage
1209 """
1210 super().delete_extra(session, _id, db_content, not_send_msg)
1211 self.db.del_list("vnfpkgops", {"vnfPkgId": _id})
garciadeblasf2af4a12023-01-24 16:56:54 +01001212 self.db.del_list(self.topic + "_revisions", {"_id": {"$regex": _id}})
garciaale960531a2020-10-20 18:29:45 -03001213
Frank Bryden19b97522020-07-10 12:32:02 +00001214 def sol005_projection(self, data):
1215 data["onboardingState"] = data["_admin"]["onboardingState"]
1216 data["operationalState"] = data["_admin"]["operationalState"]
1217 data["usageState"] = data["_admin"]["usageState"]
1218
1219 links = {}
1220 links["self"] = {"href": "/vnfpkgm/v1/vnf_packages/{}".format(data["_id"])}
1221 links["vnfd"] = {"href": "/vnfpkgm/v1/vnf_packages/{}/vnfd".format(data["_id"])}
garciadeblas4568a372021-03-24 09:19:48 +01001222 links["packageContent"] = {
1223 "href": "/vnfpkgm/v1/vnf_packages/{}/package_content".format(data["_id"])
1224 }
Frank Bryden19b97522020-07-10 12:32:02 +00001225 data["_links"] = links
garciaale960531a2020-10-20 18:29:45 -03001226
Frank Bryden19b97522020-07-10 12:32:02 +00001227 return super().sol005_projection(data)
delacruzramo271d2002019-12-02 21:00:37 +01001228
aticig9cfa8162022-04-07 11:57:18 +03001229 @staticmethod
1230 def find_software_version(vnfd: dict) -> str:
1231 """Find the sotware version in the VNFD descriptors
1232
1233 Args:
1234 vnfd (dict): Descriptor as a dictionary
1235
1236 Returns:
1237 software-version (str)
1238 """
1239 default_sw_version = "1.0"
1240 if vnfd.get("vnfd"):
1241 vnfd = vnfd["vnfd"]
1242 if vnfd.get("software-version"):
1243 return vnfd["software-version"]
1244 else:
1245 return default_sw_version
1246
1247 @staticmethod
1248 def extract_policies(vnfd: dict) -> dict:
1249 """Removes the policies from the VNFD descriptors
1250
1251 Args:
1252 vnfd (dict): Descriptor as a dictionary
1253
1254 Returns:
1255 vnfd (dict): VNFD which does not include policies
1256 """
elumalai3622f832022-07-08 12:06:27 +05301257 for df in vnfd.get("df", {}):
1258 for policy in ["scaling-aspect", "healing-aspect"]:
garciadeblasf2af4a12023-01-24 16:56:54 +01001259 if df.get(policy, {}):
elumalai3622f832022-07-08 12:06:27 +05301260 df.pop(policy)
1261 for vdu in vnfd.get("vdu", {}):
1262 for alarm_policy in ["alarm", "monitoring-parameter"]:
garciadeblasf2af4a12023-01-24 16:56:54 +01001263 if vdu.get(alarm_policy, {}):
elumalai3622f832022-07-08 12:06:27 +05301264 vdu.pop(alarm_policy)
aticig9cfa8162022-04-07 11:57:18 +03001265 return vnfd
1266
1267 @staticmethod
1268 def extract_day12_primitives(vnfd: dict) -> dict:
1269 """Removes the day12 primitives from the VNFD descriptors
1270
1271 Args:
1272 vnfd (dict): Descriptor as a dictionary
1273
1274 Returns:
1275 vnfd (dict)
1276 """
1277 for df_id, df in enumerate(vnfd.get("df", {})):
1278 if (
1279 df.get("lcm-operations-configuration", {})
1280 .get("operate-vnf-op-config", {})
1281 .get("day1-2")
1282 ):
1283 day12 = df["lcm-operations-configuration"]["operate-vnf-op-config"].get(
1284 "day1-2"
1285 )
1286 for config_id, config in enumerate(day12):
1287 for key in [
1288 "initial-config-primitive",
1289 "config-primitive",
1290 "terminate-config-primitive",
1291 ]:
1292 config.pop(key, None)
1293 day12[config_id] = config
1294 df["lcm-operations-configuration"]["operate-vnf-op-config"][
1295 "day1-2"
1296 ] = day12
1297 vnfd["df"][df_id] = df
1298 return vnfd
1299
1300 def remove_modifiable_items(self, vnfd: dict) -> dict:
1301 """Removes the modifiable parts from the VNFD descriptors
1302
1303 It calls different extract functions according to different update types
1304 to clear all the modifiable items from VNFD
1305
1306 Args:
1307 vnfd (dict): Descriptor as a dictionary
1308
1309 Returns:
1310 vnfd (dict): Descriptor which does not include modifiable contents
1311 """
1312 if vnfd.get("vnfd"):
1313 vnfd = vnfd["vnfd"]
1314 vnfd.pop("_admin", None)
1315 # If the other extractions need to be done from VNFD,
1316 # the new extract methods could be appended to below list.
1317 for extract_function in [self.extract_day12_primitives, self.extract_policies]:
1318 vnfd_temp = extract_function(vnfd)
1319 vnfd = vnfd_temp
1320 return vnfd
1321
1322 def _validate_descriptor_changes(
1323 self,
aticig2b5e1232022-08-10 17:30:12 +03001324 descriptor_id: str,
aticig9cfa8162022-04-07 11:57:18 +03001325 descriptor_file_name: str,
1326 old_descriptor_directory: str,
1327 new_descriptor_directory: str,
1328 ):
1329 """Compares the old and new VNFD descriptors and validates the new descriptor.
1330
1331 Args:
1332 old_descriptor_directory (str): Directory of descriptor which is in-use
aticig2b5e1232022-08-10 17:30:12 +03001333 new_descriptor_directory (str): Directory of descriptor which is proposed to update (new revision)
aticig9cfa8162022-04-07 11:57:18 +03001334
1335 Returns:
1336 None
1337
1338 Raises:
1339 EngineException: In case of error when there are unallowed changes
1340 """
1341 try:
aticig2b5e1232022-08-10 17:30:12 +03001342 # If VNFD does not exist in DB or it is not in use by any NS,
1343 # validation is not required.
1344 vnfd = self.db.get_one("vnfds", {"_id": descriptor_id})
1345 if not vnfd or not detect_descriptor_usage(vnfd, "vnfds", self.db):
1346 return
1347
1348 # Get the old and new descriptor contents in order to compare them.
aticig9cfa8162022-04-07 11:57:18 +03001349 with self.fs.file_open(
1350 (old_descriptor_directory.rstrip("/"), descriptor_file_name), "r"
1351 ) as old_descriptor_file:
1352 with self.fs.file_open(
aticig2b5e1232022-08-10 17:30:12 +03001353 (new_descriptor_directory.rstrip("/"), descriptor_file_name), "r"
aticig9cfa8162022-04-07 11:57:18 +03001354 ) as new_descriptor_file:
aticig2b5e1232022-08-10 17:30:12 +03001355 old_content = yaml.safe_load(old_descriptor_file.read())
1356 new_content = yaml.safe_load(new_descriptor_file.read())
1357
1358 # If software version has changed, we do not need to validate
1359 # the differences anymore.
aticig9cfa8162022-04-07 11:57:18 +03001360 if old_content and new_content:
1361 if self.find_software_version(
1362 old_content
1363 ) != self.find_software_version(new_content):
1364 return
aticig2b5e1232022-08-10 17:30:12 +03001365
aticig9cfa8162022-04-07 11:57:18 +03001366 disallowed_change = DeepDiff(
1367 self.remove_modifiable_items(old_content),
1368 self.remove_modifiable_items(new_content),
1369 )
aticig2b5e1232022-08-10 17:30:12 +03001370
aticig9cfa8162022-04-07 11:57:18 +03001371 if disallowed_change:
1372 changed_nodes = functools.reduce(
1373 lambda a, b: a + " , " + b,
1374 [
1375 node.lstrip("root")
1376 for node in disallowed_change.get(
1377 "values_changed"
1378 ).keys()
1379 ],
1380 )
aticig2b5e1232022-08-10 17:30:12 +03001381
aticig9cfa8162022-04-07 11:57:18 +03001382 raise EngineException(
1383 f"Error in validating new descriptor: {changed_nodes} cannot be modified, "
1384 "there are disallowed changes in the vnf descriptor.",
1385 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1386 )
1387 except (
1388 DbException,
1389 AttributeError,
1390 IndexError,
1391 KeyError,
1392 ValueError,
1393 ) as e:
1394 raise type(e)(
1395 "VNF Descriptor could not be processed with error: {}.".format(e)
1396 )
1397
tiernob24258a2018-10-04 18:39:49 +02001398
1399class NsdTopic(DescriptorTopic):
1400 topic = "nsds"
1401 topic_msg = "nsd"
1402
delacruzramo32bab472019-09-13 12:24:22 +02001403 def __init__(self, db, fs, msg, auth):
Daniel Arndt00f83aa2023-06-15 16:43:33 +02001404 super().__init__(db, fs, msg, auth)
tiernob24258a2018-10-04 18:39:49 +02001405
garciaale7cbd03c2020-11-27 10:38:35 -03001406 def pyangbind_validation(self, item, data, force=False):
garciaaledf718ae2020-12-03 19:17:28 -03001407 if self._descriptor_data_is_in_old_format(data):
garciadeblas4568a372021-03-24 09:19:48 +01001408 raise EngineException(
1409 "ERROR: Unsupported descriptor format. Please, use an ETSI SOL006 descriptor.",
1410 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1411 )
garciaale7cbd03c2020-11-27 10:38:35 -03001412 try:
garciadeblas4568a372021-03-24 09:19:48 +01001413 nsd_vnf_profiles = data.get("df", [{}])[0].get("vnf-profile", [])
garciaale7cbd03c2020-11-27 10:38:35 -03001414 mynsd = etsi_nfv_nsd.etsi_nfv_nsd()
garciadeblas4568a372021-03-24 09:19:48 +01001415 pybindJSONDecoder.load_ietf_json(
1416 {"nsd": {"nsd": [data]}},
1417 None,
1418 None,
1419 obj=mynsd,
1420 path_helper=True,
1421 skip_unknown=force,
1422 )
garciaale7cbd03c2020-11-27 10:38:35 -03001423 out = pybindJSON.dumps(mynsd, mode="ietf")
1424 desc_out = self._remove_envelop(yaml.safe_load(out))
1425 desc_out = self._remove_yang_prefixes_from_descriptor(desc_out)
garciaale341ac1b2020-12-11 20:04:11 -03001426 if nsd_vnf_profiles:
garciadeblas4568a372021-03-24 09:19:48 +01001427 desc_out["df"][0]["vnf-profile"] = nsd_vnf_profiles
garciaale7cbd03c2020-11-27 10:38:35 -03001428 return desc_out
1429 except Exception as e:
garciadeblas4568a372021-03-24 09:19:48 +01001430 raise EngineException(
1431 "Error in pyangbind validation: {}".format(str(e)),
1432 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1433 )
garciaale7cbd03c2020-11-27 10:38:35 -03001434
tiernob24258a2018-10-04 18:39:49 +02001435 @staticmethod
garciaaledf718ae2020-12-03 19:17:28 -03001436 def _descriptor_data_is_in_old_format(data):
garciadeblas4568a372021-03-24 09:19:48 +01001437 return ("nsd-catalog" in data) or ("nsd:nsd-catalog" in data)
garciaaledf718ae2020-12-03 19:17:28 -03001438
1439 @staticmethod
tiernob24258a2018-10-04 18:39:49 +02001440 def _remove_envelop(indata=None):
1441 if not indata:
1442 return {}
1443 clean_indata = indata
1444
garciadeblas4568a372021-03-24 09:19:48 +01001445 if clean_indata.get("nsd"):
1446 clean_indata = clean_indata["nsd"]
1447 elif clean_indata.get("etsi-nfv-nsd:nsd"):
1448 clean_indata = clean_indata["etsi-nfv-nsd:nsd"]
1449 if clean_indata.get("nsd"):
1450 if (
1451 not isinstance(clean_indata["nsd"], list)
1452 or len(clean_indata["nsd"]) != 1
1453 ):
gcalvino46e4cb82018-10-26 13:10:22 +02001454 raise EngineException("'nsd' must be a list of only one element")
garciadeblas4568a372021-03-24 09:19:48 +01001455 clean_indata = clean_indata["nsd"][0]
tiernob24258a2018-10-04 18:39:49 +02001456 return clean_indata
1457
gcalvinoa6fe0002019-01-09 13:27:11 +01001458 def _validate_input_new(self, indata, storage_params, force=False):
Frank Bryden19b97522020-07-10 12:32:02 +00001459 indata.pop("nsdOnboardingState", None)
1460 indata.pop("nsdOperationalState", None)
1461 indata.pop("nsdUsageState", None)
1462
1463 indata.pop("links", None)
1464
gcalvino46e4cb82018-10-26 13:10:22 +02001465 indata = self.pyangbind_validation("nsds", indata, force)
tierno5a5c2182018-11-20 12:27:42 +00001466 # Cross references validation in the descriptor
tiernoaa1ca7b2018-11-08 19:00:20 +01001467 # 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 -03001468 for vld in get_iterable(indata.get("virtual-link-desc")):
1469 self.validate_vld_mgmt_network_with_virtual_link_protocol_data(vld, indata)
selvi.j828f3f22023-05-16 05:43:48 +00001470 for fg in get_iterable(indata.get("vnffgd")):
1471 self.validate_vnffgd_data(fg, indata)
garciaale960531a2020-10-20 18:29:45 -03001472
garciaale7cbd03c2020-11-27 10:38:35 -03001473 self.validate_vnf_profiles_vnfd_id(indata)
garciaale960531a2020-10-20 18:29:45 -03001474
tiernob24258a2018-10-04 18:39:49 +02001475 return indata
1476
garciaale960531a2020-10-20 18:29:45 -03001477 @staticmethod
garciaale7cbd03c2020-11-27 10:38:35 -03001478 def validate_vld_mgmt_network_with_virtual_link_protocol_data(vld, indata):
1479 if not vld.get("mgmt-network"):
1480 return
1481 vld_id = vld.get("id")
1482 for df in get_iterable(indata.get("df")):
1483 for vlp in get_iterable(df.get("virtual-link-profile")):
1484 if vld_id and vld_id == vlp.get("virtual-link-desc-id"):
1485 if vlp.get("virtual-link-protocol-data"):
garciadeblas4568a372021-03-24 09:19:48 +01001486 raise EngineException(
1487 "Error at df[id='{}']:virtual-link-profile[id='{}']:virtual-link-"
1488 "protocol-data You cannot set a virtual-link-protocol-data "
1489 "when mgmt-network is True".format(df["id"], vlp["id"]),
1490 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1491 )
garciaale960531a2020-10-20 18:29:45 -03001492
1493 @staticmethod
selvi.j828f3f22023-05-16 05:43:48 +00001494 def validate_vnffgd_data(fg, indata):
1495 position_list = []
1496 all_vnf_ids = set(get_iterable(fg.get("vnf-profile-id")))
1497 for fgposition in get_iterable(fg.get("nfp-position-element")):
1498 position_list.append(fgposition["id"])
1499
1500 for nfpd in get_iterable(fg.get("nfpd")):
1501 nfp_position = []
1502 for position in get_iterable(nfpd.get("position-desc-id")):
1503 nfp_position = position.get("nfp-position-element-id")
1504 if position == "nfp-position-element-id":
1505 nfp_position = position.get("nfp-position-element-id")
1506 if nfp_position[0] not in position_list:
1507 raise EngineException(
1508 "Error at vnffgd nfpd[id='{}']:nfp-position-element-id='{}' "
1509 "does not match any nfp-position-element".format(
1510 nfpd["id"], nfp_position[0]
1511 ),
1512 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1513 )
1514
1515 for cp in get_iterable(position.get("cp-profile-id")):
1516 for cpe in get_iterable(cp.get("constituent-profile-elements")):
1517 constituent_base_element_id = cpe.get(
1518 "constituent-base-element-id"
1519 )
1520 if (
1521 constituent_base_element_id
1522 and constituent_base_element_id not in all_vnf_ids
1523 ):
1524 raise EngineException(
1525 "Error at vnffgd constituent_profile[id='{}']:vnfd-id='{}' "
1526 "does not match any constituent-base-element-id".format(
1527 cpe["id"], constituent_base_element_id
1528 ),
1529 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1530 )
1531
1532 @staticmethod
garciaale7cbd03c2020-11-27 10:38:35 -03001533 def validate_vnf_profiles_vnfd_id(indata):
1534 all_vnfd_ids = set(get_iterable(indata.get("vnfd-id")))
1535 for df in get_iterable(indata.get("df")):
1536 for vnf_profile in get_iterable(df.get("vnf-profile")):
1537 vnfd_id = vnf_profile.get("vnfd-id")
1538 if vnfd_id and vnfd_id not in all_vnfd_ids:
garciadeblas4568a372021-03-24 09:19:48 +01001539 raise EngineException(
1540 "Error at df[id='{}']:vnf_profile[id='{}']:vnfd-id='{}' "
1541 "does not match any vnfd-id".format(
1542 df["id"], vnf_profile["id"], vnfd_id
1543 ),
1544 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1545 )
garciaale960531a2020-10-20 18:29:45 -03001546
Frank Brydendeba68e2020-07-27 13:55:11 +00001547 def _validate_input_edit(self, indata, content, force=False):
tiernoaa1ca7b2018-11-08 19:00:20 +01001548 # not needed to validate with pyangbind becuase it will be validated at check_conflict_on_edit
Frank Brydendeba68e2020-07-27 13:55:11 +00001549 """
1550 indata looks as follows:
garciadeblas4568a372021-03-24 09:19:48 +01001551 - In the new case (conformant)
1552 {'nsdOperationalState': 'DISABLED', 'userDefinedData': {'id': 'string23',
Frank Brydendeba68e2020-07-27 13:55:11 +00001553 '_id': 'c6ddc544-cede-4b94-9ebe-be07b298a3c1', 'name': 'simon46'}}
1554 - In the old case (backwards-compatible)
1555 {'id': 'string23', '_id': 'c6ddc544-cede-4b94-9ebe-be07b298a3c1', 'name': 'simon46'}
1556 """
1557 if "_admin" not in indata:
1558 indata["_admin"] = {}
1559
1560 if "nsdOperationalState" in indata:
1561 if indata["nsdOperationalState"] in ("ENABLED", "DISABLED"):
1562 indata["_admin"]["operationalState"] = indata.pop("nsdOperationalState")
1563 else:
garciadeblas4568a372021-03-24 09:19:48 +01001564 raise EngineException(
1565 "State '{}' is not a valid operational state".format(
1566 indata["nsdOperationalState"]
1567 ),
1568 http_code=HTTPStatus.BAD_REQUEST,
1569 )
Frank Brydendeba68e2020-07-27 13:55:11 +00001570
garciadeblas4568a372021-03-24 09:19:48 +01001571 # 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 +00001572 # to preserve current expected behaviour
1573 if "userDefinedData" in indata:
1574 data = indata.pop("userDefinedData")
gaticid7debb92023-07-31 14:37:32 +03001575 if isinstance(data, dict):
Frank Brydendeba68e2020-07-27 13:55:11 +00001576 indata["_admin"]["userDefinedData"] = data
1577 else:
garciadeblas4568a372021-03-24 09:19:48 +01001578 raise EngineException(
1579 "userDefinedData should be an object, but is '{}' instead".format(
1580 type(data)
1581 ),
1582 http_code=HTTPStatus.BAD_REQUEST,
1583 )
1584 if (
1585 "operationalState" in indata["_admin"]
1586 and content["_admin"]["operationalState"]
1587 == indata["_admin"]["operationalState"]
1588 ):
1589 raise EngineException(
1590 "nsdOperationalState already {}".format(
1591 content["_admin"]["operationalState"]
1592 ),
1593 http_code=HTTPStatus.CONFLICT,
1594 )
tiernob24258a2018-10-04 18:39:49 +02001595 return indata
1596
tierno65ca36d2019-02-12 19:27:52 +01001597 def _check_descriptor_dependencies(self, session, descriptor):
tiernob24258a2018-10-04 18:39:49 +02001598 """
tierno5a5c2182018-11-20 12:27:42 +00001599 Check that the dependent descriptors exist on a new descriptor or edition. Also checks references to vnfd
1600 connection points are ok
tierno65ca36d2019-02-12 19:27:52 +01001601 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +02001602 :param descriptor: descriptor to be inserted or edit
1603 :return: None or raises exception
1604 """
tierno65ca36d2019-02-12 19:27:52 +01001605 if session["force"]:
tiernob24258a2018-10-04 18:39:49 +02001606 return
garciaale7cbd03c2020-11-27 10:38:35 -03001607 vnfds_index = self._get_descriptor_constituent_vnfds_index(session, descriptor)
garciaale960531a2020-10-20 18:29:45 -03001608
1609 # Cross references validation in the descriptor and vnfd connection point validation
garciaale7cbd03c2020-11-27 10:38:35 -03001610 for df in get_iterable(descriptor.get("df")):
1611 self.validate_df_vnf_profiles_constituent_connection_points(df, vnfds_index)
garciaale960531a2020-10-20 18:29:45 -03001612
garciaale7cbd03c2020-11-27 10:38:35 -03001613 def _get_descriptor_constituent_vnfds_index(self, session, descriptor):
1614 vnfds_index = {}
1615 if descriptor.get("vnfd-id") and not session["force"]:
1616 for vnfd_id in get_iterable(descriptor.get("vnfd-id")):
garciaale960531a2020-10-20 18:29:45 -03001617 query_filter = self._get_project_filter(session)
1618 query_filter["id"] = vnfd_id
1619 vnf_list = self.db.get_list("vnfds", query_filter)
tierno5a5c2182018-11-20 12:27:42 +00001620 if not vnf_list:
garciadeblas4568a372021-03-24 09:19:48 +01001621 raise EngineException(
1622 "Descriptor error at 'vnfd-id'='{}' references a non "
1623 "existing vnfd".format(vnfd_id),
1624 http_code=HTTPStatus.CONFLICT,
1625 )
garciaale7cbd03c2020-11-27 10:38:35 -03001626 vnfds_index[vnfd_id] = vnf_list[0]
1627 return vnfds_index
garciaale960531a2020-10-20 18:29:45 -03001628
1629 @staticmethod
garciaale7cbd03c2020-11-27 10:38:35 -03001630 def validate_df_vnf_profiles_constituent_connection_points(df, vnfds_index):
1631 for vnf_profile in get_iterable(df.get("vnf-profile")):
1632 vnfd = vnfds_index.get(vnf_profile["vnfd-id"])
1633 all_vnfd_ext_cpds = set()
1634 for ext_cpd in get_iterable(vnfd.get("ext-cpd")):
garciadeblas4568a372021-03-24 09:19:48 +01001635 if ext_cpd.get("id"):
1636 all_vnfd_ext_cpds.add(ext_cpd.get("id"))
garciaale7cbd03c2020-11-27 10:38:35 -03001637
garciadeblas4568a372021-03-24 09:19:48 +01001638 for virtual_link in get_iterable(
1639 vnf_profile.get("virtual-link-connectivity")
1640 ):
garciaale7cbd03c2020-11-27 10:38:35 -03001641 for vl_cpd in get_iterable(virtual_link.get("constituent-cpd-id")):
garciadeblas4568a372021-03-24 09:19:48 +01001642 vl_cpd_id = vl_cpd.get("constituent-cpd-id")
garciaale7cbd03c2020-11-27 10:38:35 -03001643 if vl_cpd_id and vl_cpd_id not in all_vnfd_ext_cpds:
garciadeblas4568a372021-03-24 09:19:48 +01001644 raise EngineException(
1645 "Error at df[id='{}']:vnf-profile[id='{}']:virtual-link-connectivity"
1646 "[virtual-link-profile-id='{}']:constituent-cpd-id='{}' references a "
1647 "non existing ext-cpd:id inside vnfd '{}'".format(
1648 df["id"],
1649 vnf_profile["id"],
1650 virtual_link["virtual-link-profile-id"],
1651 vl_cpd_id,
1652 vnfd["id"],
1653 ),
1654 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1655 )
tiernob24258a2018-10-04 18:39:49 +02001656
tierno65ca36d2019-02-12 19:27:52 +01001657 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
garciadeblas4568a372021-03-24 09:19:48 +01001658 final_content = super().check_conflict_on_edit(
1659 session, final_content, edit_content, _id
1660 )
tiernob24258a2018-10-04 18:39:49 +02001661
tierno65ca36d2019-02-12 19:27:52 +01001662 self._check_descriptor_dependencies(session, final_content)
tiernob24258a2018-10-04 18:39:49 +02001663
bravofb995ea22021-02-10 10:57:52 -03001664 return final_content
1665
tiernob4844ab2019-05-23 08:42:12 +00001666 def check_conflict_on_del(self, session, _id, db_content):
tiernob24258a2018-10-04 18:39:49 +02001667 """
1668 Check that there is not any NSR that uses this NSD. Only NSRs belonging to this project are considered. Note
1669 that NSD can be public and be used by other projects.
tierno65ca36d2019-02-12 19:27:52 +01001670 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob4844ab2019-05-23 08:42:12 +00001671 :param _id: nsd internal id
1672 :param db_content: The database content of the _id
tiernob24258a2018-10-04 18:39:49 +02001673 :return: None or raises EngineException with the conflict
1674 """
tierno65ca36d2019-02-12 19:27:52 +01001675 if session["force"]:
tiernob24258a2018-10-04 18:39:49 +02001676 return
tiernob4844ab2019-05-23 08:42:12 +00001677 descriptor = db_content
1678 descriptor_id = descriptor.get("id")
1679 if not descriptor_id: # empty nsd not uploaded
1680 return
1681
1682 # check NSD used by NS
tierno65ca36d2019-02-12 19:27:52 +01001683 _filter = self._get_project_filter(session)
tiernob4844ab2019-05-23 08:42:12 +00001684 _filter["nsd-id"] = _id
tiernob24258a2018-10-04 18:39:49 +02001685 if self.db.get_list("nsrs", _filter):
garciadeblas4568a372021-03-24 09:19:48 +01001686 raise EngineException(
1687 "There is at least one NS instance using this descriptor",
1688 http_code=HTTPStatus.CONFLICT,
1689 )
tiernob4844ab2019-05-23 08:42:12 +00001690
1691 # check NSD referenced by NST
1692 del _filter["nsd-id"]
1693 _filter["netslice-subnet.ANYINDEX.nsd-ref"] = descriptor_id
1694 if self.db.get_list("nsts", _filter):
garciadeblas4568a372021-03-24 09:19:48 +01001695 raise EngineException(
1696 "There is at least one NetSlice Template referencing this descriptor",
1697 http_code=HTTPStatus.CONFLICT,
1698 )
garciaale960531a2020-10-20 18:29:45 -03001699
beierlmcee2ebf2022-03-29 17:42:48 -04001700 def delete_extra(self, session, _id, db_content, not_send_msg=None):
1701 """
1702 Deletes associate file system storage (via super)
1703 Deletes associated vnfpkgops from database.
1704 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1705 :param _id: server internal id
1706 :param db_content: The database content of the descriptor
1707 :return: None
1708 :raises: FsException in case of error while deleting associated storage
1709 """
1710 super().delete_extra(session, _id, db_content, not_send_msg)
garciadeblasf2af4a12023-01-24 16:56:54 +01001711 self.db.del_list(self.topic + "_revisions", {"_id": {"$regex": _id}})
beierlmcee2ebf2022-03-29 17:42:48 -04001712
aticig9cfa8162022-04-07 11:57:18 +03001713 @staticmethod
1714 def extract_day12_primitives(nsd: dict) -> dict:
1715 """Removes the day12 primitives from the NSD descriptors
1716
1717 Args:
1718 nsd (dict): Descriptor as a dictionary
1719
1720 Returns:
1721 nsd (dict): Cleared NSD
1722 """
1723 if nsd.get("ns-configuration"):
1724 for key in [
1725 "config-primitive",
1726 "initial-config-primitive",
1727 "terminate-config-primitive",
1728 ]:
1729 nsd["ns-configuration"].pop(key, None)
1730 return nsd
1731
1732 def remove_modifiable_items(self, nsd: dict) -> dict:
1733 """Removes the modifiable parts from the VNFD descriptors
1734
1735 It calls different extract functions according to different update types
1736 to clear all the modifiable items from NSD
1737
1738 Args:
1739 nsd (dict): Descriptor as a dictionary
1740
1741 Returns:
1742 nsd (dict): Descriptor which does not include modifiable contents
1743 """
1744 while isinstance(nsd, dict) and nsd.get("nsd"):
1745 nsd = nsd["nsd"]
1746 if isinstance(nsd, list):
1747 nsd = nsd[0]
1748 nsd.pop("_admin", None)
1749 # If the more extractions need to be done from NSD,
1750 # the new extract methods could be appended to below list.
1751 for extract_function in [self.extract_day12_primitives]:
1752 nsd_temp = extract_function(nsd)
1753 nsd = nsd_temp
1754 return nsd
1755
1756 def _validate_descriptor_changes(
1757 self,
aticig2b5e1232022-08-10 17:30:12 +03001758 descriptor_id: str,
aticig9cfa8162022-04-07 11:57:18 +03001759 descriptor_file_name: str,
1760 old_descriptor_directory: str,
1761 new_descriptor_directory: str,
1762 ):
1763 """Compares the old and new NSD descriptors and validates the new descriptor
1764
1765 Args:
1766 old_descriptor_directory: Directory of descriptor which is in-use
aticig2b5e1232022-08-10 17:30:12 +03001767 new_descriptor_directory: Directory of descriptor which is proposed to update (new revision)
aticig9cfa8162022-04-07 11:57:18 +03001768
1769 Returns:
1770 None
1771
1772 Raises:
1773 EngineException: In case of error if the changes are not allowed
1774 """
1775
1776 try:
aticig2b5e1232022-08-10 17:30:12 +03001777 # If NSD does not exist in DB, or it is not in use by any NS,
1778 # validation is not required.
1779 nsd = self.db.get_one("nsds", {"_id": descriptor_id}, fail_on_empty=False)
1780 if not nsd or not detect_descriptor_usage(nsd, "nsds", self.db):
1781 return
1782
1783 # Get the old and new descriptor contents in order to compare them.
aticig9cfa8162022-04-07 11:57:18 +03001784 with self.fs.file_open(
aticig2b5e1232022-08-10 17:30:12 +03001785 (old_descriptor_directory.rstrip("/"), descriptor_file_name), "r"
aticig9cfa8162022-04-07 11:57:18 +03001786 ) as old_descriptor_file:
1787 with self.fs.file_open(
1788 (new_descriptor_directory.rstrip("/"), descriptor_file_name), "r"
1789 ) as new_descriptor_file:
aticig2b5e1232022-08-10 17:30:12 +03001790 old_content = yaml.safe_load(old_descriptor_file.read())
1791 new_content = yaml.safe_load(new_descriptor_file.read())
1792
aticig9cfa8162022-04-07 11:57:18 +03001793 if old_content and new_content:
1794 disallowed_change = DeepDiff(
1795 self.remove_modifiable_items(old_content),
1796 self.remove_modifiable_items(new_content),
1797 )
aticig2b5e1232022-08-10 17:30:12 +03001798
aticig9cfa8162022-04-07 11:57:18 +03001799 if disallowed_change:
1800 changed_nodes = functools.reduce(
1801 lambda a, b: a + ", " + b,
1802 [
1803 node.lstrip("root")
1804 for node in disallowed_change.get(
1805 "values_changed"
1806 ).keys()
1807 ],
1808 )
aticig2b5e1232022-08-10 17:30:12 +03001809
aticig9cfa8162022-04-07 11:57:18 +03001810 raise EngineException(
1811 f"Error in validating new descriptor: {changed_nodes} cannot be modified, "
1812 "there are disallowed changes in the ns descriptor. ",
1813 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1814 )
1815 except (
1816 DbException,
1817 AttributeError,
1818 IndexError,
1819 KeyError,
1820 ValueError,
1821 ) as e:
1822 raise type(e)(
1823 "NS Descriptor could not be processed with error: {}.".format(e)
1824 )
1825
Frank Bryden19b97522020-07-10 12:32:02 +00001826 def sol005_projection(self, data):
1827 data["nsdOnboardingState"] = data["_admin"]["onboardingState"]
1828 data["nsdOperationalState"] = data["_admin"]["operationalState"]
1829 data["nsdUsageState"] = data["_admin"]["usageState"]
1830
1831 links = {}
1832 links["self"] = {"href": "/nsd/v1/ns_descriptors/{}".format(data["_id"])}
garciadeblas4568a372021-03-24 09:19:48 +01001833 links["nsd_content"] = {
1834 "href": "/nsd/v1/ns_descriptors/{}/nsd_content".format(data["_id"])
1835 }
Frank Bryden19b97522020-07-10 12:32:02 +00001836 data["_links"] = links
garciaale960531a2020-10-20 18:29:45 -03001837
Frank Bryden19b97522020-07-10 12:32:02 +00001838 return super().sol005_projection(data)
tiernob24258a2018-10-04 18:39:49 +02001839
1840
Felipe Vicensb57758d2018-10-16 16:00:20 +02001841class NstTopic(DescriptorTopic):
1842 topic = "nsts"
1843 topic_msg = "nst"
tierno6b02b052020-06-02 10:07:41 +00001844 quota_name = "slice_templates"
Felipe Vicensb57758d2018-10-16 16:00:20 +02001845
delacruzramo32bab472019-09-13 12:24:22 +02001846 def __init__(self, db, fs, msg, auth):
1847 DescriptorTopic.__init__(self, db, fs, msg, auth)
Felipe Vicensb57758d2018-10-16 16:00:20 +02001848
garciaale7cbd03c2020-11-27 10:38:35 -03001849 def pyangbind_validation(self, item, data, force=False):
1850 try:
1851 mynst = nst_im()
garciadeblas4568a372021-03-24 09:19:48 +01001852 pybindJSONDecoder.load_ietf_json(
1853 {"nst": [data]},
1854 None,
1855 None,
1856 obj=mynst,
1857 path_helper=True,
1858 skip_unknown=force,
1859 )
garciaale7cbd03c2020-11-27 10:38:35 -03001860 out = pybindJSON.dumps(mynst, mode="ietf")
1861 desc_out = self._remove_envelop(yaml.safe_load(out))
1862 return desc_out
1863 except Exception as e:
garciadeblas4568a372021-03-24 09:19:48 +01001864 raise EngineException(
1865 "Error in pyangbind validation: {}".format(str(e)),
1866 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1867 )
garciaale7cbd03c2020-11-27 10:38:35 -03001868
Felipe Vicensb57758d2018-10-16 16:00:20 +02001869 @staticmethod
1870 def _remove_envelop(indata=None):
1871 if not indata:
1872 return {}
1873 clean_indata = indata
1874
garciadeblas4568a372021-03-24 09:19:48 +01001875 if clean_indata.get("nst"):
1876 if (
1877 not isinstance(clean_indata["nst"], list)
1878 or len(clean_indata["nst"]) != 1
1879 ):
Felipe Vicensb57758d2018-10-16 16:00:20 +02001880 raise EngineException("'nst' must be a list only one element")
garciadeblas4568a372021-03-24 09:19:48 +01001881 clean_indata = clean_indata["nst"][0]
1882 elif clean_indata.get("nst:nst"):
1883 if (
1884 not isinstance(clean_indata["nst:nst"], list)
1885 or len(clean_indata["nst:nst"]) != 1
1886 ):
gcalvino70434c12018-11-27 15:17:04 +01001887 raise EngineException("'nst:nst' must be a list only one element")
garciadeblas4568a372021-03-24 09:19:48 +01001888 clean_indata = clean_indata["nst:nst"][0]
Felipe Vicensb57758d2018-10-16 16:00:20 +02001889 return clean_indata
1890
gcalvinoa6fe0002019-01-09 13:27:11 +01001891 def _validate_input_new(self, indata, storage_params, force=False):
Frank Bryden19b97522020-07-10 12:32:02 +00001892 indata.pop("onboardingState", None)
1893 indata.pop("operationalState", None)
1894 indata.pop("usageState", None)
gcalvino70434c12018-11-27 15:17:04 +01001895 indata = self.pyangbind_validation("nsts", indata, force)
Felipe Vicense36ab852018-11-23 14:12:09 +01001896 return indata.copy()
1897
Felipe Vicensb57758d2018-10-16 16:00:20 +02001898 def _check_descriptor_dependencies(self, session, descriptor):
1899 """
1900 Check that the dependent descriptors exist on a new descriptor or edition
tierno65ca36d2019-02-12 19:27:52 +01001901 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Felipe Vicensb57758d2018-10-16 16:00:20 +02001902 :param descriptor: descriptor to be inserted or edit
1903 :return: None or raises exception
1904 """
1905 if not descriptor.get("netslice-subnet"):
1906 return
1907 for nsd in descriptor["netslice-subnet"]:
1908 nsd_id = nsd["nsd-ref"]
tierno65ca36d2019-02-12 19:27:52 +01001909 filter_q = self._get_project_filter(session)
Felipe Vicensb57758d2018-10-16 16:00:20 +02001910 filter_q["id"] = nsd_id
1911 if not self.db.get_list("nsds", filter_q):
garciadeblas4568a372021-03-24 09:19:48 +01001912 raise EngineException(
1913 "Descriptor error at 'netslice-subnet':'nsd-ref'='{}' references a non "
1914 "existing nsd".format(nsd_id),
1915 http_code=HTTPStatus.CONFLICT,
1916 )
Felipe Vicensb57758d2018-10-16 16:00:20 +02001917
tierno65ca36d2019-02-12 19:27:52 +01001918 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
garciadeblas4568a372021-03-24 09:19:48 +01001919 final_content = super().check_conflict_on_edit(
1920 session, final_content, edit_content, _id
1921 )
Felipe Vicensb57758d2018-10-16 16:00:20 +02001922
1923 self._check_descriptor_dependencies(session, final_content)
bravofb995ea22021-02-10 10:57:52 -03001924 return final_content
Felipe Vicensb57758d2018-10-16 16:00:20 +02001925
tiernob4844ab2019-05-23 08:42:12 +00001926 def check_conflict_on_del(self, session, _id, db_content):
Felipe Vicensb57758d2018-10-16 16:00:20 +02001927 """
1928 Check that there is not any NSIR that uses this NST. Only NSIRs belonging to this project are considered. Note
1929 that NST can be public and be used by other projects.
tierno65ca36d2019-02-12 19:27:52 +01001930 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Felipe Vicens07f31722018-10-29 15:16:44 +01001931 :param _id: nst internal id
tiernob4844ab2019-05-23 08:42:12 +00001932 :param db_content: The database content of the _id.
Felipe Vicensb57758d2018-10-16 16:00:20 +02001933 :return: None or raises EngineException with the conflict
1934 """
1935 # TODO: Check this method
tierno65ca36d2019-02-12 19:27:52 +01001936 if session["force"]:
Felipe Vicensb57758d2018-10-16 16:00:20 +02001937 return
Felipe Vicens07f31722018-10-29 15:16:44 +01001938 # Get Network Slice Template from Database
tierno65ca36d2019-02-12 19:27:52 +01001939 _filter = self._get_project_filter(session)
tiernoea97c042019-09-13 09:44:42 +00001940 _filter["_admin.nst-id"] = _id
tiernob4844ab2019-05-23 08:42:12 +00001941 if self.db.get_list("nsis", _filter):
garciadeblas4568a372021-03-24 09:19:48 +01001942 raise EngineException(
1943 "there is at least one Netslice Instance using this descriptor",
1944 http_code=HTTPStatus.CONFLICT,
1945 )
Felipe Vicensb57758d2018-10-16 16:00:20 +02001946
Frank Bryden19b97522020-07-10 12:32:02 +00001947 def sol005_projection(self, data):
1948 data["onboardingState"] = data["_admin"]["onboardingState"]
1949 data["operationalState"] = data["_admin"]["operationalState"]
1950 data["usageState"] = data["_admin"]["usageState"]
1951
1952 links = {}
1953 links["self"] = {"href": "/nst/v1/netslice_templates/{}".format(data["_id"])}
1954 links["nst"] = {"href": "/nst/v1/netslice_templates/{}/nst".format(data["_id"])}
1955 data["_links"] = links
1956
1957 return super().sol005_projection(data)
1958
Felipe Vicensb57758d2018-10-16 16:00:20 +02001959
tiernob24258a2018-10-04 18:39:49 +02001960class PduTopic(BaseTopic):
1961 topic = "pdus"
1962 topic_msg = "pdu"
tierno6b02b052020-06-02 10:07:41 +00001963 quota_name = "pduds"
tiernob24258a2018-10-04 18:39:49 +02001964 schema_new = pdu_new_schema
1965 schema_edit = pdu_edit_schema
1966
delacruzramo32bab472019-09-13 12:24:22 +02001967 def __init__(self, db, fs, msg, auth):
1968 BaseTopic.__init__(self, db, fs, msg, auth)
tiernob24258a2018-10-04 18:39:49 +02001969
1970 @staticmethod
1971 def format_on_new(content, project_id=None, make_public=False):
tierno36ec8602018-11-02 17:27:11 +01001972 BaseTopic.format_on_new(content, project_id=project_id, make_public=make_public)
tiernob24258a2018-10-04 18:39:49 +02001973 content["_admin"]["onboardingState"] = "CREATED"
tierno36ec8602018-11-02 17:27:11 +01001974 content["_admin"]["operationalState"] = "ENABLED"
1975 content["_admin"]["usageState"] = "NOT_IN_USE"
tiernob24258a2018-10-04 18:39:49 +02001976
tiernob4844ab2019-05-23 08:42:12 +00001977 def check_conflict_on_del(self, session, _id, db_content):
1978 """
1979 Check that there is not any vnfr that uses this PDU
1980 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1981 :param _id: pdu internal id
1982 :param db_content: The database content of the _id.
1983 :return: None or raises EngineException with the conflict
1984 """
tierno65ca36d2019-02-12 19:27:52 +01001985 if session["force"]:
tiernob24258a2018-10-04 18:39:49 +02001986 return
tiernob4844ab2019-05-23 08:42:12 +00001987
1988 _filter = self._get_project_filter(session)
1989 _filter["vdur.pdu-id"] = _id
tiernob24258a2018-10-04 18:39:49 +02001990 if self.db.get_list("vnfrs", _filter):
garciadeblas4568a372021-03-24 09:19:48 +01001991 raise EngineException(
1992 "There is at least one VNF instance using this PDU",
1993 http_code=HTTPStatus.CONFLICT,
1994 )
delacruzramo271d2002019-12-02 21:00:37 +01001995
1996
1997class VnfPkgOpTopic(BaseTopic):
1998 topic = "vnfpkgops"
1999 topic_msg = "vnfd"
2000 schema_new = vnfpkgop_new_schema
2001 schema_edit = None
2002
2003 def __init__(self, db, fs, msg, auth):
2004 BaseTopic.__init__(self, db, fs, msg, auth)
2005
2006 def edit(self, session, _id, indata=None, kwargs=None, content=None):
garciadeblas4568a372021-03-24 09:19:48 +01002007 raise EngineException(
2008 "Method 'edit' not allowed for topic '{}'".format(self.topic),
2009 HTTPStatus.METHOD_NOT_ALLOWED,
2010 )
delacruzramo271d2002019-12-02 21:00:37 +01002011
2012 def delete(self, session, _id, dry_run=False):
garciadeblas4568a372021-03-24 09:19:48 +01002013 raise EngineException(
2014 "Method 'delete' not allowed for topic '{}'".format(self.topic),
2015 HTTPStatus.METHOD_NOT_ALLOWED,
2016 )
delacruzramo271d2002019-12-02 21:00:37 +01002017
2018 def delete_list(self, session, filter_q=None):
garciadeblas4568a372021-03-24 09:19:48 +01002019 raise EngineException(
2020 "Method 'delete_list' not allowed for topic '{}'".format(self.topic),
2021 HTTPStatus.METHOD_NOT_ALLOWED,
2022 )
delacruzramo271d2002019-12-02 21:00:37 +01002023
2024 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
2025 """
2026 Creates a new entry into database.
2027 :param rollback: list to append created items at database in case a rollback may to be done
2028 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
2029 :param indata: data to be inserted
2030 :param kwargs: used to override the indata descriptor
2031 :param headers: http request headers
2032 :return: _id, op_id:
2033 _id: identity of the inserted data.
2034 op_id: None
2035 """
2036 self._update_input_with_kwargs(indata, kwargs)
2037 validate_input(indata, self.schema_new)
2038 vnfpkg_id = indata["vnfPkgId"]
2039 filter_q = BaseTopic._get_project_filter(session)
2040 filter_q["_id"] = vnfpkg_id
2041 vnfd = self.db.get_one("vnfds", filter_q)
2042 operation = indata["lcmOperationType"]
2043 kdu_name = indata["kdu_name"]
2044 for kdu in vnfd.get("kdu", []):
2045 if kdu["name"] == kdu_name:
2046 helm_chart = kdu.get("helm-chart")
2047 juju_bundle = kdu.get("juju-bundle")
2048 break
2049 else:
garciadeblas4568a372021-03-24 09:19:48 +01002050 raise EngineException(
2051 "Not found vnfd[id='{}']:kdu[name='{}']".format(vnfpkg_id, kdu_name)
2052 )
delacruzramo271d2002019-12-02 21:00:37 +01002053 if helm_chart:
2054 indata["helm-chart"] = helm_chart
2055 match = fullmatch(r"([^/]*)/([^/]*)", helm_chart)
2056 repo_name = match.group(1) if match else None
2057 elif juju_bundle:
2058 indata["juju-bundle"] = juju_bundle
2059 match = fullmatch(r"([^/]*)/([^/]*)", juju_bundle)
2060 repo_name = match.group(1) if match else None
2061 else:
garciadeblas4568a372021-03-24 09:19:48 +01002062 raise EngineException(
2063 "Found neither 'helm-chart' nor 'juju-bundle' in vnfd[id='{}']:kdu[name='{}']".format(
2064 vnfpkg_id, kdu_name
2065 )
2066 )
delacruzramo271d2002019-12-02 21:00:37 +01002067 if repo_name:
2068 del filter_q["_id"]
2069 filter_q["name"] = repo_name
2070 repo = self.db.get_one("k8srepos", filter_q)
2071 k8srepo_id = repo.get("_id")
2072 k8srepo_url = repo.get("url")
2073 else:
2074 k8srepo_id = None
2075 k8srepo_url = None
2076 indata["k8srepoId"] = k8srepo_id
2077 indata["k8srepo_url"] = k8srepo_url
2078 vnfpkgop_id = str(uuid4())
2079 vnfpkgop_desc = {
2080 "_id": vnfpkgop_id,
2081 "operationState": "PROCESSING",
2082 "vnfPkgId": vnfpkg_id,
2083 "lcmOperationType": operation,
2084 "isAutomaticInvocation": False,
2085 "isCancelPending": False,
2086 "operationParams": indata,
2087 "links": {
2088 "self": "/osm/vnfpkgm/v1/vnfpkg_op_occs/" + vnfpkgop_id,
2089 "vnfpkg": "/osm/vnfpkgm/v1/vnf_packages/" + vnfpkg_id,
garciadeblas4568a372021-03-24 09:19:48 +01002090 },
delacruzramo271d2002019-12-02 21:00:37 +01002091 }
garciadeblas4568a372021-03-24 09:19:48 +01002092 self.format_on_new(
2093 vnfpkgop_desc, session["project_id"], make_public=session["public"]
2094 )
delacruzramo271d2002019-12-02 21:00:37 +01002095 ctime = vnfpkgop_desc["_admin"]["created"]
2096 vnfpkgop_desc["statusEnteredTime"] = ctime
2097 vnfpkgop_desc["startTime"] = ctime
2098 self.db.create(self.topic, vnfpkgop_desc)
2099 rollback.append({"topic": self.topic, "_id": vnfpkgop_id})
2100 self.msg.write(self.topic_msg, operation, vnfpkgop_desc)
2101 return vnfpkgop_id, None