blob: 50182fd7db453de448d05d9927b4638f08630efd [file] [log] [blame]
tiernob24258a2018-10-04 18:39:49 +02001# -*- coding: utf-8 -*-
2
tiernod125caf2018-11-22 16:05:54 +00003# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
12# implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
tiernob24258a2018-10-04 18:39:49 +020016import tarfile
17import yaml
18import json
bravofb995ea22021-02-10 10:57:52 -030019import copy
beierlmcee2ebf2022-03-29 17:42:48 -040020import os
21import shutil
aticig9cfa8162022-04-07 11:57:18 +030022import functools
garciadeblas4568a372021-03-24 09:19:48 +010023
tiernob24258a2018-10-04 18:39:49 +020024# import logging
aticig9cfa8162022-04-07 11:57:18 +030025from deepdiff import DeepDiff
tiernob24258a2018-10-04 18:39:49 +020026from hashlib import md5
27from osm_common.dbbase import DbException, deep_update_rfc7396
28from http import HTTPStatus
delacruzramo26301bb2019-11-15 14:45:32 +010029from time import time
delacruzramo271d2002019-12-02 21:00:37 +010030from uuid import uuid4
31from re import fullmatch
bravofc26740a2021-11-08 09:44:54 -030032from zipfile import ZipFile
garciadeblas4568a372021-03-24 09:19:48 +010033from osm_nbi.validation import (
34 ValidationError,
35 pdu_new_schema,
36 pdu_edit_schema,
37 validate_input,
38 vnfpkgop_new_schema,
39)
aticig2b5e1232022-08-10 17:30:12 +030040from osm_nbi.base_topic import (
41 BaseTopic,
42 EngineException,
43 get_iterable,
44 detect_descriptor_usage,
45)
sousaedu317b9fd2021-07-29 17:40:16 +020046from osm_im import etsi_nfv_vnfd, etsi_nfv_nsd
gcalvino70434c12018-11-27 15:17:04 +010047from osm_im.nst import nst as nst_im
gcalvino46e4cb82018-10-26 13:10:22 +020048from pyangbind.lib.serialise import pybindJSONDecoder
49import pyangbind.lib.pybindJSON as pybindJSON
bravof41a52052021-02-17 18:08:01 -030050from osm_nbi import utils
tiernob24258a2018-10-04 18:39:49 +020051
52__author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
53
54
55class DescriptorTopic(BaseTopic):
delacruzramo32bab472019-09-13 12:24:22 +020056 def __init__(self, db, fs, msg, auth):
beierlmcee2ebf2022-03-29 17:42:48 -040057
delacruzramo32bab472019-09-13 12:24:22 +020058 BaseTopic.__init__(self, db, fs, msg, auth)
tiernob24258a2018-10-04 18:39:49 +020059
tierno65ca36d2019-02-12 19:27:52 +010060 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
garciadeblas4568a372021-03-24 09:19:48 +010061 final_content = super().check_conflict_on_edit(
62 session, final_content, edit_content, _id
63 )
K Sai Kiran45bd94c2019-11-25 17:30:37 +053064
65 def _check_unique_id_name(descriptor, position=""):
66 for desc_key, desc_item in descriptor.items():
67 if isinstance(desc_item, list) and desc_item:
68 used_ids = []
69 desc_item_id = None
70 for index, list_item in enumerate(desc_item):
71 if isinstance(list_item, dict):
garciadeblas4568a372021-03-24 09:19:48 +010072 _check_unique_id_name(
73 list_item, "{}.{}[{}]".format(position, desc_key, index)
74 )
K Sai Kiran45bd94c2019-11-25 17:30:37 +053075 # Base case
garciadeblas4568a372021-03-24 09:19:48 +010076 if index == 0 and (
77 list_item.get("id") or list_item.get("name")
78 ):
K Sai Kiran45bd94c2019-11-25 17:30:37 +053079 desc_item_id = "id" if list_item.get("id") else "name"
80 if desc_item_id and list_item.get(desc_item_id):
81 if list_item[desc_item_id] in used_ids:
garciadeblas4568a372021-03-24 09:19:48 +010082 position = "{}.{}[{}]".format(
83 position, desc_key, index
84 )
85 raise EngineException(
86 "Error: identifier {} '{}' is not unique and repeats at '{}'".format(
87 desc_item_id,
88 list_item[desc_item_id],
89 position,
90 ),
91 HTTPStatus.UNPROCESSABLE_ENTITY,
92 )
K Sai Kiran45bd94c2019-11-25 17:30:37 +053093 used_ids.append(list_item[desc_item_id])
garciaale960531a2020-10-20 18:29:45 -030094
K Sai Kiran45bd94c2019-11-25 17:30:37 +053095 _check_unique_id_name(final_content)
tiernoaa1ca7b2018-11-08 19:00:20 +010096 # 1. validate again with pyangbind
97 # 1.1. remove internal keys
98 internal_keys = {}
99 for k in ("_id", "_admin"):
100 if k in final_content:
101 internal_keys[k] = final_content.pop(k)
gcalvinoa6fe0002019-01-09 13:27:11 +0100102 storage_params = internal_keys["_admin"].get("storage")
garciadeblas4568a372021-03-24 09:19:48 +0100103 serialized = self._validate_input_new(
104 final_content, storage_params, session["force"]
105 )
bravofb995ea22021-02-10 10:57:52 -0300106
tiernoaa1ca7b2018-11-08 19:00:20 +0100107 # 1.2. modify final_content with a serialized version
bravofb995ea22021-02-10 10:57:52 -0300108 final_content = copy.deepcopy(serialized)
tiernoaa1ca7b2018-11-08 19:00:20 +0100109 # 1.3. restore internal keys
110 for k, v in internal_keys.items():
111 final_content[k] = v
tierno65ca36d2019-02-12 19:27:52 +0100112 if session["force"]:
bravofb995ea22021-02-10 10:57:52 -0300113 return final_content
114
tiernoaa1ca7b2018-11-08 19:00:20 +0100115 # 2. check that this id is not present
116 if "id" in edit_content:
tierno65ca36d2019-02-12 19:27:52 +0100117 _filter = self._get_project_filter(session)
bravofb995ea22021-02-10 10:57:52 -0300118
tiernoaa1ca7b2018-11-08 19:00:20 +0100119 _filter["id"] = final_content["id"]
120 _filter["_id.neq"] = _id
bravofb995ea22021-02-10 10:57:52 -0300121
tiernoaa1ca7b2018-11-08 19:00:20 +0100122 if self.db.get_one(self.topic, _filter, fail_on_empty=False):
garciadeblas4568a372021-03-24 09:19:48 +0100123 raise EngineException(
124 "{} with id '{}' already exists for this project".format(
125 self.topic[:-1], final_content["id"]
126 ),
127 HTTPStatus.CONFLICT,
128 )
tiernob24258a2018-10-04 18:39:49 +0200129
bravofb995ea22021-02-10 10:57:52 -0300130 return final_content
131
tiernob24258a2018-10-04 18:39:49 +0200132 @staticmethod
133 def format_on_new(content, project_id=None, make_public=False):
134 BaseTopic.format_on_new(content, project_id=project_id, make_public=make_public)
135 content["_admin"]["onboardingState"] = "CREATED"
136 content["_admin"]["operationalState"] = "DISABLED"
tierno36ec8602018-11-02 17:27:11 +0100137 content["_admin"]["usageState"] = "NOT_IN_USE"
tiernob24258a2018-10-04 18:39:49 +0200138
tiernobee3bad2019-12-05 12:26:01 +0000139 def delete_extra(self, session, _id, db_content, not_send_msg=None):
tiernob4844ab2019-05-23 08:42:12 +0000140 """
141 Deletes file system storage associated with the descriptor
142 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
143 :param _id: server internal id
144 :param db_content: The database content of the descriptor
tiernobee3bad2019-12-05 12:26:01 +0000145 :param not_send_msg: To not send message (False) or store content (list) instead
tiernob4844ab2019-05-23 08:42:12 +0000146 :return: None if ok or raises EngineException with the problem
147 """
tiernob24258a2018-10-04 18:39:49 +0200148 self.fs.file_delete(_id, ignore_non_exist=True)
tiernof717cbe2018-12-03 16:35:42 +0000149 self.fs.file_delete(_id + "_", ignore_non_exist=True) # remove temp folder
beierlmcee2ebf2022-03-29 17:42:48 -0400150 # Remove file revisions
151 if "revision" in db_content["_admin"]:
152 revision = db_content["_admin"]["revision"]
153 while revision > 0:
154 self.fs.file_delete(_id + ":" + str(revision), ignore_non_exist=True)
155 revision = revision - 1
156
tiernob24258a2018-10-04 18:39:49 +0200157
158 @staticmethod
159 def get_one_by_id(db, session, topic, id):
160 # find owned by this project
tierno65ca36d2019-02-12 19:27:52 +0100161 _filter = BaseTopic._get_project_filter(session)
tiernob24258a2018-10-04 18:39:49 +0200162 _filter["id"] = id
163 desc_list = db.get_list(topic, _filter)
164 if len(desc_list) == 1:
165 return desc_list[0]
166 elif len(desc_list) > 1:
garciadeblas4568a372021-03-24 09:19:48 +0100167 raise DbException(
168 "Found more than one {} with id='{}' belonging to this project".format(
169 topic[:-1], id
170 ),
171 HTTPStatus.CONFLICT,
172 )
tiernob24258a2018-10-04 18:39:49 +0200173
174 # not found any: try to find public
tierno65ca36d2019-02-12 19:27:52 +0100175 _filter = BaseTopic._get_project_filter(session)
tiernob24258a2018-10-04 18:39:49 +0200176 _filter["id"] = id
177 desc_list = db.get_list(topic, _filter)
178 if not desc_list:
garciadeblas4568a372021-03-24 09:19:48 +0100179 raise DbException(
180 "Not found any {} with id='{}'".format(topic[:-1], id),
181 HTTPStatus.NOT_FOUND,
182 )
tiernob24258a2018-10-04 18:39:49 +0200183 elif len(desc_list) == 1:
184 return desc_list[0]
185 else:
garciadeblas4568a372021-03-24 09:19:48 +0100186 raise DbException(
187 "Found more than one public {} with id='{}'; and no one belonging to this project".format(
188 topic[:-1], id
189 ),
190 HTTPStatus.CONFLICT,
191 )
tiernob24258a2018-10-04 18:39:49 +0200192
tierno65ca36d2019-02-12 19:27:52 +0100193 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
tiernob24258a2018-10-04 18:39:49 +0200194 """
195 Creates a new almost empty DISABLED entry into database. Due to SOL005, it does not follow normal procedure.
196 Creating a VNFD or NSD is done in two steps: 1. Creates an empty descriptor (this step) and 2) upload content
197 (self.upload_content)
198 :param rollback: list to append created items at database in case a rollback may to be done
tierno65ca36d2019-02-12 19:27:52 +0100199 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200200 :param indata: data to be inserted
201 :param kwargs: used to override the indata descriptor
202 :param headers: http request headers
tiernobdebce92019-07-01 15:36:49 +0000203 :return: _id, None: identity of the inserted data; and None as there is not any operation
tiernob24258a2018-10-04 18:39:49 +0200204 """
205
tiernod7749582020-05-28 10:41:10 +0000206 # No needed to capture exceptions
207 # Check Quota
208 self.check_quota(session)
delacruzramo32bab472019-09-13 12:24:22 +0200209
tiernod7749582020-05-28 10:41:10 +0000210 # _remove_envelop
211 if indata:
212 if "userDefinedData" in indata:
garciadeblas4568a372021-03-24 09:19:48 +0100213 indata = indata["userDefinedData"]
tiernob24258a2018-10-04 18:39:49 +0200214
tiernod7749582020-05-28 10:41:10 +0000215 # Override descriptor with query string kwargs
216 self._update_input_with_kwargs(indata, kwargs)
217 # uncomment when this method is implemented.
218 # Avoid override in this case as the target is userDefinedData, but not vnfd,nsd descriptors
219 # indata = DescriptorTopic._validate_input_new(self, indata, project_id=session["force"])
tiernob24258a2018-10-04 18:39:49 +0200220
beierlmcee2ebf2022-03-29 17:42:48 -0400221 content = {"_admin": {
222 "userDefinedData": indata,
223 "revision": 0
224 }}
225
garciadeblas4568a372021-03-24 09:19:48 +0100226 self.format_on_new(
227 content, session["project_id"], make_public=session["public"]
228 )
tiernod7749582020-05-28 10:41:10 +0000229 _id = self.db.create(self.topic, content)
230 rollback.append({"topic": self.topic, "_id": _id})
231 self._send_msg("created", {"_id": _id})
232 return _id, None
tiernob24258a2018-10-04 18:39:49 +0200233
tierno65ca36d2019-02-12 19:27:52 +0100234 def upload_content(self, session, _id, indata, kwargs, headers):
tiernob24258a2018-10-04 18:39:49 +0200235 """
236 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 +0100237 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200238 :param _id : the nsd,vnfd is already created, this is the id
239 :param indata: http body request
240 :param kwargs: user query string to override parameters. NOT USED
241 :param headers: http request headers
tierno5a5c2182018-11-20 12:27:42 +0000242 :return: True if package is completely uploaded or False if partial content has been uploded
tiernob24258a2018-10-04 18:39:49 +0200243 Raise exception on error
244 """
245 # Check that _id exists and it is valid
246 current_desc = self.show(session, _id)
247
248 content_range_text = headers.get("Content-Range")
249 expected_md5 = headers.get("Content-File-MD5")
250 compressed = None
251 content_type = headers.get("Content-Type")
garciadeblas4568a372021-03-24 09:19:48 +0100252 if (
253 content_type
254 and "application/gzip" in content_type
255 or "application/x-gzip" in content_type
garciadeblas4568a372021-03-24 09:19:48 +0100256 ):
tiernob24258a2018-10-04 18:39:49 +0200257 compressed = "gzip"
bravofc26740a2021-11-08 09:44:54 -0300258 if (
259 content_type
260 and "application/zip" in content_type
261 ):
262 compressed = "zip"
tiernob24258a2018-10-04 18:39:49 +0200263 filename = headers.get("Content-Filename")
bravofc26740a2021-11-08 09:44:54 -0300264 if not filename and compressed:
265 filename = "package.tar.gz" if compressed == "gzip" else "package.zip"
266 elif not filename:
267 filename = "package"
268
beierlmcee2ebf2022-03-29 17:42:48 -0400269 revision = 1
270 if "revision" in current_desc["_admin"]:
271 revision = current_desc["_admin"]["revision"] + 1
272
tiernob24258a2018-10-04 18:39:49 +0200273 # TODO change to Content-Disposition filename https://tools.ietf.org/html/rfc6266
274 file_pkg = None
275 error_text = ""
beierlmbc5a5242022-05-17 21:25:29 -0400276 fs_rollback = []
277
tiernob24258a2018-10-04 18:39:49 +0200278 try:
279 if content_range_text:
garciadeblas4568a372021-03-24 09:19:48 +0100280 content_range = (
281 content_range_text.replace("-", " ").replace("/", " ").split()
282 )
283 if (
284 content_range[0] != "bytes"
285 ): # TODO check x<y not negative < total....
tiernob24258a2018-10-04 18:39:49 +0200286 raise IndexError()
287 start = int(content_range[1])
288 end = int(content_range[2]) + 1
289 total = int(content_range[3])
290 else:
291 start = 0
beierlmcee2ebf2022-03-29 17:42:48 -0400292 # Rather than using a temp folder, we will store the package in a folder based on
293 # the current revision.
294 proposed_revision_path = (
295 _id + ":" + str(revision)
garciadeblas4568a372021-03-24 09:19:48 +0100296 ) # all the content is upload here and if ok, it is rename from id_ to is folder
tiernob24258a2018-10-04 18:39:49 +0200297
298 if start:
beierlmcee2ebf2022-03-29 17:42:48 -0400299 if not self.fs.file_exists(proposed_revision_path, "dir"):
garciadeblas4568a372021-03-24 09:19:48 +0100300 raise EngineException(
301 "invalid Transaction-Id header", HTTPStatus.NOT_FOUND
302 )
tiernob24258a2018-10-04 18:39:49 +0200303 else:
beierlmcee2ebf2022-03-29 17:42:48 -0400304 self.fs.file_delete(proposed_revision_path, ignore_non_exist=True)
305 self.fs.mkdir(proposed_revision_path)
beierlmbc5a5242022-05-17 21:25:29 -0400306 fs_rollback.append(proposed_revision_path)
tiernob24258a2018-10-04 18:39:49 +0200307
308 storage = self.fs.get_params()
beierlmbc5a5242022-05-17 21:25:29 -0400309 storage["folder"] = proposed_revision_path
tiernob24258a2018-10-04 18:39:49 +0200310
beierlmcee2ebf2022-03-29 17:42:48 -0400311 file_path = (proposed_revision_path, filename)
garciadeblas4568a372021-03-24 09:19:48 +0100312 if self.fs.file_exists(file_path, "file"):
tiernob24258a2018-10-04 18:39:49 +0200313 file_size = self.fs.file_size(file_path)
314 else:
315 file_size = 0
316 if file_size != start:
garciadeblas4568a372021-03-24 09:19:48 +0100317 raise EngineException(
318 "invalid Content-Range start sequence, expected '{}' but received '{}'".format(
319 file_size, start
320 ),
321 HTTPStatus.REQUESTED_RANGE_NOT_SATISFIABLE,
322 )
323 file_pkg = self.fs.file_open(file_path, "a+b")
tiernob24258a2018-10-04 18:39:49 +0200324 if isinstance(indata, dict):
325 indata_text = yaml.safe_dump(indata, indent=4, default_flow_style=False)
326 file_pkg.write(indata_text.encode(encoding="utf-8"))
327 else:
328 indata_len = 0
329 while True:
330 indata_text = indata.read(4096)
331 indata_len += len(indata_text)
332 if not indata_text:
333 break
334 file_pkg.write(indata_text)
335 if content_range_text:
garciaale960531a2020-10-20 18:29:45 -0300336 if indata_len != end - start:
garciadeblas4568a372021-03-24 09:19:48 +0100337 raise EngineException(
338 "Mismatch between Content-Range header {}-{} and body length of {}".format(
339 start, end - 1, indata_len
340 ),
341 HTTPStatus.REQUESTED_RANGE_NOT_SATISFIABLE,
342 )
tiernob24258a2018-10-04 18:39:49 +0200343 if end != total:
344 # TODO update to UPLOADING
345 return False
346
347 # PACKAGE UPLOADED
348 if expected_md5:
349 file_pkg.seek(0, 0)
350 file_md5 = md5()
351 chunk_data = file_pkg.read(1024)
352 while chunk_data:
353 file_md5.update(chunk_data)
354 chunk_data = file_pkg.read(1024)
355 if expected_md5 != file_md5.hexdigest():
356 raise EngineException("Error, MD5 mismatch", HTTPStatus.CONFLICT)
357 file_pkg.seek(0, 0)
358 if compressed == "gzip":
garciadeblas4568a372021-03-24 09:19:48 +0100359 tar = tarfile.open(mode="r", fileobj=file_pkg)
tiernob24258a2018-10-04 18:39:49 +0200360 descriptor_file_name = None
361 for tarinfo in tar:
362 tarname = tarinfo.name
363 tarname_path = tarname.split("/")
garciadeblas4568a372021-03-24 09:19:48 +0100364 if (
365 not tarname_path[0] or ".." in tarname_path
366 ): # if start with "/" means absolute path
367 raise EngineException(
368 "Absolute path or '..' are not allowed for package descriptor tar.gz"
369 )
tiernob24258a2018-10-04 18:39:49 +0200370 if len(tarname_path) == 1 and not tarinfo.isdir():
garciadeblas4568a372021-03-24 09:19:48 +0100371 raise EngineException(
372 "All files must be inside a dir for package descriptor tar.gz"
373 )
374 if (
375 tarname.endswith(".yaml")
376 or tarname.endswith(".json")
377 or tarname.endswith(".yml")
378 ):
tiernob24258a2018-10-04 18:39:49 +0200379 storage["pkg-dir"] = tarname_path[0]
380 if len(tarname_path) == 2:
381 if descriptor_file_name:
382 raise EngineException(
garciadeblas4568a372021-03-24 09:19:48 +0100383 "Found more than one descriptor file at package descriptor tar.gz"
384 )
tiernob24258a2018-10-04 18:39:49 +0200385 descriptor_file_name = tarname
386 if not descriptor_file_name:
garciadeblas4568a372021-03-24 09:19:48 +0100387 raise EngineException(
388 "Not found any descriptor file at package descriptor tar.gz"
389 )
tiernob24258a2018-10-04 18:39:49 +0200390 storage["descriptor"] = descriptor_file_name
391 storage["zipfile"] = filename
beierlmcee2ebf2022-03-29 17:42:48 -0400392 self.fs.file_extract(tar, proposed_revision_path)
garciadeblas4568a372021-03-24 09:19:48 +0100393 with self.fs.file_open(
beierlmcee2ebf2022-03-29 17:42:48 -0400394 (proposed_revision_path, descriptor_file_name), "r"
garciadeblas4568a372021-03-24 09:19:48 +0100395 ) as descriptor_file:
tiernob24258a2018-10-04 18:39:49 +0200396 content = descriptor_file.read()
bravofc26740a2021-11-08 09:44:54 -0300397 elif compressed == "zip":
398 zipfile = ZipFile(file_pkg)
399 descriptor_file_name = None
400 for package_file in zipfile.infolist():
401 zipfilename = package_file.filename
402 file_path = zipfilename.split("/")
403 if (
404 not file_path[0] or ".." in zipfilename
405 ): # if start with "/" means absolute path
406 raise EngineException(
407 "Absolute path or '..' are not allowed for package descriptor zip"
408 )
409
410 if (
411 (
412 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
418 )
419 ):
420 storage["pkg-dir"] = ""
421 if descriptor_file_name:
422 raise EngineException(
423 "Found more than one descriptor file at package descriptor zip"
424 )
425 descriptor_file_name = zipfilename
426 if not descriptor_file_name:
427 raise EngineException(
428 "Not found any descriptor file at package descriptor zip"
429 )
430 storage["descriptor"] = descriptor_file_name
431 storage["zipfile"] = filename
beierlmcee2ebf2022-03-29 17:42:48 -0400432 self.fs.file_extract(zipfile, proposed_revision_path)
bravofc26740a2021-11-08 09:44:54 -0300433
434 with self.fs.file_open(
beierlmcee2ebf2022-03-29 17:42:48 -0400435 (proposed_revision_path, descriptor_file_name), "r"
bravofc26740a2021-11-08 09:44:54 -0300436 ) as descriptor_file:
437 content = descriptor_file.read()
tiernob24258a2018-10-04 18:39:49 +0200438 else:
439 content = file_pkg.read()
440 storage["descriptor"] = descriptor_file_name = filename
441
442 if descriptor_file_name.endswith(".json"):
443 error_text = "Invalid json format "
444 indata = json.load(content)
445 else:
446 error_text = "Invalid yaml format "
delacruzramob19cadc2019-10-08 10:18:02 +0200447 indata = yaml.load(content, Loader=yaml.SafeLoader)
tiernob24258a2018-10-04 18:39:49 +0200448
beierlmcee2ebf2022-03-29 17:42:48 -0400449 # Need to close the file package here so it can be copied from the
450 # revision to the current, unrevisioned record
451 if file_pkg:
452 file_pkg.close()
453 file_pkg = None
454
455 # Fetch both the incoming, proposed revision and the original revision so we
456 # can call a validate method to compare them
457 current_revision_path = _id + "/"
458 self.fs.sync(from_path=current_revision_path)
459 self.fs.sync(from_path=proposed_revision_path)
460
461 if revision > 1:
462 try:
463 self._validate_descriptor_changes(
aticig2b5e1232022-08-10 17:30:12 +0300464 _id,
beierlmcee2ebf2022-03-29 17:42:48 -0400465 descriptor_file_name,
466 current_revision_path,
aticig2b5e1232022-08-10 17:30:12 +0300467 proposed_revision_path,
468 )
beierlmcee2ebf2022-03-29 17:42:48 -0400469 except Exception as e:
470 shutil.rmtree(self.fs.path + current_revision_path, ignore_errors=True)
471 shutil.rmtree(self.fs.path + proposed_revision_path, ignore_errors=True)
472 # Only delete the new revision. We need to keep the original version in place
473 # as it has not been changed.
474 self.fs.file_delete(proposed_revision_path, ignore_non_exist=True)
475 raise e
476
tiernob24258a2018-10-04 18:39:49 +0200477
478 indata = self._remove_envelop(indata)
479
480 # Override descriptor with query string kwargs
481 if kwargs:
482 self._update_input_with_kwargs(indata, kwargs)
tiernob24258a2018-10-04 18:39:49 +0200483
beierlmbc5a5242022-05-17 21:25:29 -0400484 current_desc["_admin"]["storage"] = storage
485 current_desc["_admin"]["onboardingState"] = "ONBOARDED"
486 current_desc["_admin"]["operationalState"] = "ENABLED"
487 current_desc["_admin"]["modified"] = time()
488 current_desc["_admin"]["revision"] = revision
489
tiernob24258a2018-10-04 18:39:49 +0200490 deep_update_rfc7396(current_desc, indata)
garciadeblas4568a372021-03-24 09:19:48 +0100491 current_desc = self.check_conflict_on_edit(
492 session, current_desc, indata, _id=_id
493 )
beierlmbc5a5242022-05-17 21:25:29 -0400494
495 # Copy the revision to the active package name by its original id
496 shutil.rmtree(self.fs.path + current_revision_path, ignore_errors=True)
497 os.rename(self.fs.path + proposed_revision_path, self.fs.path + current_revision_path)
498 self.fs.file_delete(current_revision_path, ignore_non_exist=True)
499 self.fs.mkdir(current_revision_path)
500 self.fs.reverse_sync(from_path=current_revision_path)
501
502 shutil.rmtree(self.fs.path + _id)
503
tiernob24258a2018-10-04 18:39:49 +0200504 self.db.replace(self.topic, _id, current_desc)
beierlmcee2ebf2022-03-29 17:42:48 -0400505
506 # Store a copy of the package as a point in time revision
507 revision_desc = dict(current_desc)
508 revision_desc["_id"] = _id + ":" + str(revision_desc["_admin"]["revision"])
509 self.db.create(self.topic + "_revisions", revision_desc)
beierlmbc5a5242022-05-17 21:25:29 -0400510 fs_rollback = []
tiernob24258a2018-10-04 18:39:49 +0200511
512 indata["_id"] = _id
K Sai Kiranc96fd692019-10-16 17:50:53 +0530513 self._send_msg("edited", indata)
tiernob24258a2018-10-04 18:39:49 +0200514
515 # TODO if descriptor has changed because kwargs update content and remove cached zip
516 # TODO if zip is not present creates one
517 return True
518
519 except EngineException:
520 raise
521 except IndexError:
garciadeblas4568a372021-03-24 09:19:48 +0100522 raise EngineException(
523 "invalid Content-Range header format. Expected 'bytes start-end/total'",
524 HTTPStatus.REQUESTED_RANGE_NOT_SATISFIABLE,
525 )
tiernob24258a2018-10-04 18:39:49 +0200526 except IOError as e:
garciadeblas4568a372021-03-24 09:19:48 +0100527 raise EngineException(
528 "invalid upload transaction sequence: '{}'".format(e),
529 HTTPStatus.BAD_REQUEST,
530 )
tiernob24258a2018-10-04 18:39:49 +0200531 except tarfile.ReadError as e:
garciadeblas4568a372021-03-24 09:19:48 +0100532 raise EngineException(
533 "invalid file content {}".format(e), HTTPStatus.BAD_REQUEST
534 )
tiernob24258a2018-10-04 18:39:49 +0200535 except (ValueError, yaml.YAMLError) as e:
536 raise EngineException(error_text + str(e))
537 except ValidationError as e:
538 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
539 finally:
540 if file_pkg:
541 file_pkg.close()
beierlmbc5a5242022-05-17 21:25:29 -0400542 for file in fs_rollback:
543 self.fs.file_delete(file, ignore_non_exist=True)
tiernob24258a2018-10-04 18:39:49 +0200544
545 def get_file(self, session, _id, path=None, accept_header=None):
546 """
547 Return the file content of a vnfd or nsd
tierno65ca36d2019-02-12 19:27:52 +0100548 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tierno87006042018-10-24 12:50:20 +0200549 :param _id: Identity of the vnfd, nsd
tiernob24258a2018-10-04 18:39:49 +0200550 :param path: artifact path or "$DESCRIPTOR" or None
551 :param accept_header: Content of Accept header. Must contain applition/zip or/and text/plain
tierno87006042018-10-24 12:50:20 +0200552 :return: opened file plus Accept format or raises an exception
tiernob24258a2018-10-04 18:39:49 +0200553 """
554 accept_text = accept_zip = False
555 if accept_header:
garciadeblas4568a372021-03-24 09:19:48 +0100556 if "text/plain" in accept_header or "*/*" in accept_header:
tiernob24258a2018-10-04 18:39:49 +0200557 accept_text = True
garciadeblas4568a372021-03-24 09:19:48 +0100558 if "application/zip" in accept_header or "*/*" in accept_header:
559 accept_zip = "application/zip"
560 elif "application/gzip" in accept_header:
561 accept_zip = "application/gzip"
tierno87006042018-10-24 12:50:20 +0200562
tiernob24258a2018-10-04 18:39:49 +0200563 if not accept_text and not accept_zip:
garciadeblas4568a372021-03-24 09:19:48 +0100564 raise EngineException(
565 "provide request header 'Accept' with 'application/zip' or 'text/plain'",
566 http_code=HTTPStatus.NOT_ACCEPTABLE,
567 )
tiernob24258a2018-10-04 18:39:49 +0200568
569 content = self.show(session, _id)
570 if content["_admin"]["onboardingState"] != "ONBOARDED":
garciadeblas4568a372021-03-24 09:19:48 +0100571 raise EngineException(
572 "Cannot get content because this resource is not at 'ONBOARDED' state. "
573 "onboardingState is {}".format(content["_admin"]["onboardingState"]),
574 http_code=HTTPStatus.CONFLICT,
575 )
tiernob24258a2018-10-04 18:39:49 +0200576 storage = content["_admin"]["storage"]
garciaale960531a2020-10-20 18:29:45 -0300577 if path is not None and path != "$DESCRIPTOR": # artifacts
selvi.j5be838c2022-08-25 06:24:49 +0000578 if not storage.get("pkg-dir") and not storage.get("folder"):
garciadeblas4568a372021-03-24 09:19:48 +0100579 raise EngineException(
580 "Packages does not contains artifacts",
581 http_code=HTTPStatus.BAD_REQUEST,
582 )
583 if self.fs.file_exists(
584 (storage["folder"], storage["pkg-dir"], *path), "dir"
585 ):
586 folder_content = self.fs.dir_ls(
587 (storage["folder"], storage["pkg-dir"], *path)
588 )
tiernob24258a2018-10-04 18:39:49 +0200589 return folder_content, "text/plain"
590 # TODO manage folders in http
591 else:
garciadeblas4568a372021-03-24 09:19:48 +0100592 return (
593 self.fs.file_open(
594 (storage["folder"], storage["pkg-dir"], *path), "rb"
595 ),
596 "application/octet-stream",
597 )
tiernob24258a2018-10-04 18:39:49 +0200598
599 # pkgtype accept ZIP TEXT -> result
600 # manyfiles yes X -> zip
601 # no yes -> error
602 # onefile yes no -> zip
603 # X yes -> text
tiernoee002752020-08-04 14:14:16 +0000604 contain_many_files = False
garciadeblas4568a372021-03-24 09:19:48 +0100605 if storage.get("pkg-dir"):
tiernoee002752020-08-04 14:14:16 +0000606 # check if there are more than one file in the package, ignoring checksums.txt.
garciadeblas4568a372021-03-24 09:19:48 +0100607 pkg_files = self.fs.dir_ls((storage["folder"], storage["pkg-dir"]))
608 if len(pkg_files) >= 3 or (
609 len(pkg_files) == 2 and "checksums.txt" not in pkg_files
610 ):
tiernoee002752020-08-04 14:14:16 +0000611 contain_many_files = True
612 if accept_text and (not contain_many_files or path == "$DESCRIPTOR"):
garciadeblas4568a372021-03-24 09:19:48 +0100613 return (
614 self.fs.file_open((storage["folder"], storage["descriptor"]), "r"),
615 "text/plain",
616 )
tiernoee002752020-08-04 14:14:16 +0000617 elif contain_many_files and not accept_zip:
garciadeblas4568a372021-03-24 09:19:48 +0100618 raise EngineException(
619 "Packages that contains several files need to be retrieved with 'application/zip'"
620 "Accept header",
621 http_code=HTTPStatus.NOT_ACCEPTABLE,
622 )
tiernob24258a2018-10-04 18:39:49 +0200623 else:
garciadeblas4568a372021-03-24 09:19:48 +0100624 if not storage.get("zipfile"):
tiernob24258a2018-10-04 18:39:49 +0200625 # TODO generate zipfile if not present
garciadeblas4568a372021-03-24 09:19:48 +0100626 raise EngineException(
627 "Only allowed 'text/plain' Accept header for this descriptor. To be solved in "
628 "future versions",
629 http_code=HTTPStatus.NOT_ACCEPTABLE,
630 )
631 return (
632 self.fs.file_open((storage["folder"], storage["zipfile"]), "rb"),
633 accept_zip,
634 )
tiernob24258a2018-10-04 18:39:49 +0200635
garciaale7cbd03c2020-11-27 10:38:35 -0300636 def _remove_yang_prefixes_from_descriptor(self, descriptor):
637 new_descriptor = {}
638 for k, v in descriptor.items():
639 new_v = v
640 if isinstance(v, dict):
641 new_v = self._remove_yang_prefixes_from_descriptor(v)
642 elif isinstance(v, list):
643 new_v = list()
644 for x in v:
645 if isinstance(x, dict):
646 new_v.append(self._remove_yang_prefixes_from_descriptor(x))
647 else:
648 new_v.append(x)
garciadeblas4568a372021-03-24 09:19:48 +0100649 new_descriptor[k.split(":")[-1]] = new_v
garciaale7cbd03c2020-11-27 10:38:35 -0300650 return new_descriptor
651
gcalvino46e4cb82018-10-26 13:10:22 +0200652 def pyangbind_validation(self, item, data, force=False):
garciadeblas4568a372021-03-24 09:19:48 +0100653 raise EngineException(
654 "Not possible to validate '{}' item".format(item),
655 http_code=HTTPStatus.INTERNAL_SERVER_ERROR,
656 )
gcalvino46e4cb82018-10-26 13:10:22 +0200657
Frank Brydendeba68e2020-07-27 13:55:11 +0000658 def _validate_input_edit(self, indata, content, force=False):
659 # not needed to validate with pyangbind becuase it will be validated at check_conflict_on_edit
660 if "_id" in indata:
661 indata.pop("_id")
662 if "_admin" not in indata:
663 indata["_admin"] = {}
664
665 if "operationalState" in indata:
666 if indata["operationalState"] in ("ENABLED", "DISABLED"):
667 indata["_admin"]["operationalState"] = indata.pop("operationalState")
668 else:
garciadeblas4568a372021-03-24 09:19:48 +0100669 raise EngineException(
670 "State '{}' is not a valid operational state".format(
671 indata["operationalState"]
672 ),
673 http_code=HTTPStatus.BAD_REQUEST,
674 )
Frank Brydendeba68e2020-07-27 13:55:11 +0000675
garciadeblas4568a372021-03-24 09:19:48 +0100676 # 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 +0000677 # to preserve current expected behaviour
678 if "userDefinedData" in indata:
679 data = indata.pop("userDefinedData")
680 if type(data) == dict:
681 indata["_admin"]["userDefinedData"] = data
682 else:
garciadeblas4568a372021-03-24 09:19:48 +0100683 raise EngineException(
684 "userDefinedData should be an object, but is '{}' instead".format(
685 type(data)
686 ),
687 http_code=HTTPStatus.BAD_REQUEST,
688 )
garciaale960531a2020-10-20 18:29:45 -0300689
garciadeblas4568a372021-03-24 09:19:48 +0100690 if (
691 "operationalState" in indata["_admin"]
692 and content["_admin"]["operationalState"]
693 == indata["_admin"]["operationalState"]
694 ):
695 raise EngineException(
696 "operationalState already {}".format(
697 content["_admin"]["operationalState"]
698 ),
699 http_code=HTTPStatus.CONFLICT,
700 )
Frank Brydendeba68e2020-07-27 13:55:11 +0000701
702 return indata
703
aticig2b5e1232022-08-10 17:30:12 +0300704 def _validate_descriptor_changes(
705 self,
706 descriptor_id,
beierlmcee2ebf2022-03-29 17:42:48 -0400707 descriptor_file_name,
708 old_descriptor_directory,
aticig2b5e1232022-08-10 17:30:12 +0300709 new_descriptor_directory
710 ):
beierlmcee2ebf2022-03-29 17:42:48 -0400711 # Example:
712 # raise EngineException(
713 # "Error in validating new descriptor: <NODE> cannot be modified",
714 # http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
715 # )
716 pass
tiernob24258a2018-10-04 18:39:49 +0200717
718class VnfdTopic(DescriptorTopic):
719 topic = "vnfds"
720 topic_msg = "vnfd"
721
delacruzramo32bab472019-09-13 12:24:22 +0200722 def __init__(self, db, fs, msg, auth):
723 DescriptorTopic.__init__(self, db, fs, msg, auth)
tiernob24258a2018-10-04 18:39:49 +0200724
garciaale7cbd03c2020-11-27 10:38:35 -0300725 def pyangbind_validation(self, item, data, force=False):
garciaaledf718ae2020-12-03 19:17:28 -0300726 if self._descriptor_data_is_in_old_format(data):
garciadeblas4568a372021-03-24 09:19:48 +0100727 raise EngineException(
728 "ERROR: Unsupported descriptor format. Please, use an ETSI SOL006 descriptor.",
729 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
730 )
garciaale7cbd03c2020-11-27 10:38:35 -0300731 try:
garciaale7cbd03c2020-11-27 10:38:35 -0300732 myvnfd = etsi_nfv_vnfd.etsi_nfv_vnfd()
garciadeblas4568a372021-03-24 09:19:48 +0100733 pybindJSONDecoder.load_ietf_json(
734 {"etsi-nfv-vnfd:vnfd": data},
735 None,
736 None,
737 obj=myvnfd,
738 path_helper=True,
739 skip_unknown=force,
740 )
garciaale7cbd03c2020-11-27 10:38:35 -0300741 out = pybindJSON.dumps(myvnfd, mode="ietf")
742 desc_out = self._remove_envelop(yaml.safe_load(out))
743 desc_out = self._remove_yang_prefixes_from_descriptor(desc_out)
bravof41a52052021-02-17 18:08:01 -0300744 return utils.deep_update_dict(data, desc_out)
garciaale7cbd03c2020-11-27 10:38:35 -0300745 except Exception as e:
garciadeblas4568a372021-03-24 09:19:48 +0100746 raise EngineException(
747 "Error in pyangbind validation: {}".format(str(e)),
748 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
749 )
garciaale7cbd03c2020-11-27 10:38:35 -0300750
tiernob24258a2018-10-04 18:39:49 +0200751 @staticmethod
garciaaledf718ae2020-12-03 19:17:28 -0300752 def _descriptor_data_is_in_old_format(data):
garciadeblas4568a372021-03-24 09:19:48 +0100753 return ("vnfd-catalog" in data) or ("vnfd:vnfd-catalog" in data)
garciaaledf718ae2020-12-03 19:17:28 -0300754
755 @staticmethod
tiernob24258a2018-10-04 18:39:49 +0200756 def _remove_envelop(indata=None):
757 if not indata:
758 return {}
759 clean_indata = indata
garciaale7cbd03c2020-11-27 10:38:35 -0300760
garciadeblas4568a372021-03-24 09:19:48 +0100761 if clean_indata.get("etsi-nfv-vnfd:vnfd"):
762 if not isinstance(clean_indata["etsi-nfv-vnfd:vnfd"], dict):
garciaale7cbd03c2020-11-27 10:38:35 -0300763 raise EngineException("'etsi-nfv-vnfd:vnfd' must be a dict")
garciadeblas4568a372021-03-24 09:19:48 +0100764 clean_indata = clean_indata["etsi-nfv-vnfd:vnfd"]
765 elif clean_indata.get("vnfd"):
766 if not isinstance(clean_indata["vnfd"], dict):
garciaale7cbd03c2020-11-27 10:38:35 -0300767 raise EngineException("'vnfd' must be dict")
garciadeblas4568a372021-03-24 09:19:48 +0100768 clean_indata = clean_indata["vnfd"]
garciaale7cbd03c2020-11-27 10:38:35 -0300769
tiernob24258a2018-10-04 18:39:49 +0200770 return clean_indata
771
tierno65ca36d2019-02-12 19:27:52 +0100772 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
garciadeblas4568a372021-03-24 09:19:48 +0100773 final_content = super().check_conflict_on_edit(
774 session, final_content, edit_content, _id
775 )
tierno36ec8602018-11-02 17:27:11 +0100776
777 # set type of vnfd
778 contains_pdu = False
779 contains_vdu = False
780 for vdu in get_iterable(final_content.get("vdu")):
781 if vdu.get("pdu-type"):
782 contains_pdu = True
783 else:
784 contains_vdu = True
785 if contains_pdu:
786 final_content["_admin"]["type"] = "hnfd" if contains_vdu else "pnfd"
787 elif contains_vdu:
788 final_content["_admin"]["type"] = "vnfd"
789 # if neither vud nor pdu do not fill type
bravofb995ea22021-02-10 10:57:52 -0300790 return final_content
tierno36ec8602018-11-02 17:27:11 +0100791
tiernob4844ab2019-05-23 08:42:12 +0000792 def check_conflict_on_del(self, session, _id, db_content):
tiernob24258a2018-10-04 18:39:49 +0200793 """
794 Check that there is not any NSD that uses this VNFD. Only NSDs belonging to this project are considered. Note
795 that VNFD can be public and be used by NSD of other projects. Also check there are not deployments, or vnfr
796 that uses this vnfd
tierno65ca36d2019-02-12 19:27:52 +0100797 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob4844ab2019-05-23 08:42:12 +0000798 :param _id: vnfd internal id
799 :param db_content: The database content of the _id.
tiernob24258a2018-10-04 18:39:49 +0200800 :return: None or raises EngineException with the conflict
801 """
tierno65ca36d2019-02-12 19:27:52 +0100802 if session["force"]:
tiernob24258a2018-10-04 18:39:49 +0200803 return
tiernob4844ab2019-05-23 08:42:12 +0000804 descriptor = db_content
tiernob24258a2018-10-04 18:39:49 +0200805 descriptor_id = descriptor.get("id")
806 if not descriptor_id: # empty vnfd not uploaded
807 return
808
tierno65ca36d2019-02-12 19:27:52 +0100809 _filter = self._get_project_filter(session)
tiernob4844ab2019-05-23 08:42:12 +0000810
tiernob24258a2018-10-04 18:39:49 +0200811 # check vnfrs using this vnfd
812 _filter["vnfd-id"] = _id
813 if self.db.get_list("vnfrs", _filter):
garciadeblas4568a372021-03-24 09:19:48 +0100814 raise EngineException(
815 "There is at least one VNF instance using this descriptor",
816 http_code=HTTPStatus.CONFLICT,
817 )
tiernob4844ab2019-05-23 08:42:12 +0000818
819 # check NSD referencing this VNFD
tiernob24258a2018-10-04 18:39:49 +0200820 del _filter["vnfd-id"]
garciadeblasf576eb92021-04-18 20:54:13 +0000821 _filter["vnfd-id"] = descriptor_id
tiernob24258a2018-10-04 18:39:49 +0200822 if self.db.get_list("nsds", _filter):
garciadeblas4568a372021-03-24 09:19:48 +0100823 raise EngineException(
824 "There is at least one NS package referencing this descriptor",
825 http_code=HTTPStatus.CONFLICT,
826 )
tiernob24258a2018-10-04 18:39:49 +0200827
gcalvinoa6fe0002019-01-09 13:27:11 +0100828 def _validate_input_new(self, indata, storage_params, force=False):
Frank Bryden19b97522020-07-10 12:32:02 +0000829 indata.pop("onboardingState", None)
830 indata.pop("operationalState", None)
831 indata.pop("usageState", None)
Frank Bryden19b97522020-07-10 12:32:02 +0000832 indata.pop("links", None)
833
gcalvino46e4cb82018-10-26 13:10:22 +0200834 indata = self.pyangbind_validation("vnfds", indata, force)
gcalvino5e72d152018-10-23 11:46:57 +0200835 # Cross references validation in the descriptor
garciaale7cbd03c2020-11-27 10:38:35 -0300836
837 self.validate_mgmt_interface_connection_point(indata)
gcalvino5e72d152018-10-23 11:46:57 +0200838
839 for vdu in get_iterable(indata.get("vdu")):
garciaale7cbd03c2020-11-27 10:38:35 -0300840 self.validate_vdu_internal_connection_points(vdu)
garciaale960531a2020-10-20 18:29:45 -0300841 self._validate_vdu_cloud_init_in_package(storage_params, vdu, indata)
bravof41a52052021-02-17 18:08:01 -0300842 self._validate_vdu_charms_in_package(storage_params, indata)
garciaale960531a2020-10-20 18:29:45 -0300843
844 self._validate_vnf_charms_in_package(storage_params, indata)
845
garciaale7cbd03c2020-11-27 10:38:35 -0300846 self.validate_external_connection_points(indata)
847 self.validate_internal_virtual_links(indata)
garciaale960531a2020-10-20 18:29:45 -0300848 self.validate_monitoring_params(indata)
849 self.validate_scaling_group_descriptor(indata)
850
851 return indata
852
853 @staticmethod
garciaale7cbd03c2020-11-27 10:38:35 -0300854 def validate_mgmt_interface_connection_point(indata):
garciaale960531a2020-10-20 18:29:45 -0300855 if not indata.get("vdu"):
856 return
garciaale7cbd03c2020-11-27 10:38:35 -0300857 if not indata.get("mgmt-cp"):
garciadeblas4568a372021-03-24 09:19:48 +0100858 raise EngineException(
859 "'mgmt-cp' is a mandatory field and it is not defined",
860 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
861 )
garciaale7cbd03c2020-11-27 10:38:35 -0300862
863 for cp in get_iterable(indata.get("ext-cpd")):
864 if cp["id"] == indata["mgmt-cp"]:
865 break
866 else:
garciadeblas4568a372021-03-24 09:19:48 +0100867 raise EngineException(
868 "mgmt-cp='{}' must match an existing ext-cpd".format(indata["mgmt-cp"]),
869 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
870 )
garciaale960531a2020-10-20 18:29:45 -0300871
872 @staticmethod
garciaale7cbd03c2020-11-27 10:38:35 -0300873 def validate_vdu_internal_connection_points(vdu):
874 int_cpds = set()
875 for cpd in get_iterable(vdu.get("int-cpd")):
876 cpd_id = cpd.get("id")
877 if cpd_id and cpd_id in int_cpds:
garciadeblas4568a372021-03-24 09:19:48 +0100878 raise EngineException(
879 "vdu[id='{}']:int-cpd[id='{}'] is already used by other int-cpd".format(
880 vdu["id"], cpd_id
881 ),
882 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
883 )
garciaale7cbd03c2020-11-27 10:38:35 -0300884 int_cpds.add(cpd_id)
885
886 @staticmethod
887 def validate_external_connection_points(indata):
888 all_vdus_int_cpds = set()
889 for vdu in get_iterable(indata.get("vdu")):
890 for int_cpd in get_iterable(vdu.get("int-cpd")):
891 all_vdus_int_cpds.add((vdu.get("id"), int_cpd.get("id")))
892
893 ext_cpds = set()
894 for cpd in get_iterable(indata.get("ext-cpd")):
895 cpd_id = cpd.get("id")
896 if cpd_id and cpd_id in ext_cpds:
garciadeblas4568a372021-03-24 09:19:48 +0100897 raise EngineException(
898 "ext-cpd[id='{}'] is already used by other ext-cpd".format(cpd_id),
899 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
900 )
garciaale7cbd03c2020-11-27 10:38:35 -0300901 ext_cpds.add(cpd_id)
902
903 int_cpd = cpd.get("int-cpd")
904 if int_cpd:
905 if (int_cpd.get("vdu-id"), int_cpd.get("cpd")) not in all_vdus_int_cpds:
garciadeblas4568a372021-03-24 09:19:48 +0100906 raise EngineException(
907 "ext-cpd[id='{}']:int-cpd must match an existing vdu int-cpd".format(
908 cpd_id
909 ),
910 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
911 )
garciaale7cbd03c2020-11-27 10:38:35 -0300912 # TODO: Validate k8s-cluster-net points to a valid k8s-cluster:nets ?
garciaale960531a2020-10-20 18:29:45 -0300913
bravof41a52052021-02-17 18:08:01 -0300914 def _validate_vdu_charms_in_package(self, storage_params, indata):
915 for df in indata["df"]:
garciadeblas4568a372021-03-24 09:19:48 +0100916 if (
917 "lcm-operations-configuration" in df
918 and "operate-vnf-op-config" in df["lcm-operations-configuration"]
919 ):
920 configs = df["lcm-operations-configuration"][
921 "operate-vnf-op-config"
922 ].get("day1-2", [])
garciaale2c4f9ec2021-03-01 11:04:50 -0300923 vdus = df.get("vdu-profile", [])
bravof23258282021-02-22 18:04:40 -0300924 for vdu in vdus:
925 for config in configs:
926 if config["id"] == vdu["id"] and utils.find_in_list(
927 config.get("execution-environment-list", []),
garciadeblas4568a372021-03-24 09:19:48 +0100928 lambda ee: "juju" in ee,
bravof23258282021-02-22 18:04:40 -0300929 ):
garciadeblas4568a372021-03-24 09:19:48 +0100930 if not self._validate_package_folders(
931 storage_params, "charms"
bravofc26740a2021-11-08 09:44:54 -0300932 ) and not self._validate_package_folders(
933 storage_params, "Scripts/charms"
garciadeblas4568a372021-03-24 09:19:48 +0100934 ):
935 raise EngineException(
936 "Charm defined in vnf[id={}] but not present in "
937 "package".format(indata["id"])
938 )
garciaale960531a2020-10-20 18:29:45 -0300939
940 def _validate_vdu_cloud_init_in_package(self, storage_params, vdu, indata):
941 if not vdu.get("cloud-init-file"):
942 return
garciadeblas4568a372021-03-24 09:19:48 +0100943 if not self._validate_package_folders(
944 storage_params, "cloud_init", vdu["cloud-init-file"]
bravofc26740a2021-11-08 09:44:54 -0300945 ) and not self._validate_package_folders(
946 storage_params, "Scripts/cloud_init", vdu["cloud-init-file"]
garciadeblas4568a372021-03-24 09:19:48 +0100947 ):
948 raise EngineException(
949 "Cloud-init defined in vnf[id={}]:vdu[id={}] but not present in "
950 "package".format(indata["id"], vdu["id"])
951 )
garciaale960531a2020-10-20 18:29:45 -0300952
953 def _validate_vnf_charms_in_package(self, storage_params, indata):
bravof41a52052021-02-17 18:08:01 -0300954 # Get VNF configuration through new container
garciadeblas4568a372021-03-24 09:19:48 +0100955 for deployment_flavor in indata.get("df", []):
bravof41a52052021-02-17 18:08:01 -0300956 if "lcm-operations-configuration" not in deployment_flavor:
957 return
garciadeblas4568a372021-03-24 09:19:48 +0100958 if (
959 "operate-vnf-op-config"
960 not in deployment_flavor["lcm-operations-configuration"]
961 ):
bravof41a52052021-02-17 18:08:01 -0300962 return
garciadeblas4568a372021-03-24 09:19:48 +0100963 for day_1_2_config in deployment_flavor["lcm-operations-configuration"][
964 "operate-vnf-op-config"
965 ]["day1-2"]:
bravof41a52052021-02-17 18:08:01 -0300966 if day_1_2_config["id"] == indata["id"]:
bravof23258282021-02-22 18:04:40 -0300967 if utils.find_in_list(
968 day_1_2_config.get("execution-environment-list", []),
garciadeblas4568a372021-03-24 09:19:48 +0100969 lambda ee: "juju" in ee,
bravof23258282021-02-22 18:04:40 -0300970 ):
bravofc26740a2021-11-08 09:44:54 -0300971 if not self._validate_package_folders(
972 storage_params, "charms"
973 ) and not self._validate_package_folders(
974 storage_params, "Scripts/charms"
975 ):
garciadeblas4568a372021-03-24 09:19:48 +0100976 raise EngineException(
977 "Charm defined in vnf[id={}] but not present in "
978 "package".format(indata["id"])
979 )
garciaale960531a2020-10-20 18:29:45 -0300980
981 def _validate_package_folders(self, storage_params, folder, file=None):
bravofc26740a2021-11-08 09:44:54 -0300982 if not storage_params:
983 return False
984 elif not storage_params.get("pkg-dir"):
985 if self.fs.file_exists("{}_".format(storage_params["folder"]), "dir"):
986 f = "{}_/{}".format(
987 storage_params["folder"], folder
988 )
989 else:
990 f = "{}/{}".format(
991 storage_params["folder"], folder
992 )
993 if file:
994 return self.fs.file_exists("{}/{}".format(f, file), "file")
995 else:
bravofc26740a2021-11-08 09:44:54 -0300996 if self.fs.file_exists(f, "dir"):
997 if self.fs.dir_ls(f):
998 return True
garciaale960531a2020-10-20 18:29:45 -0300999 return False
1000 else:
garciadeblas4568a372021-03-24 09:19:48 +01001001 if self.fs.file_exists("{}_".format(storage_params["folder"]), "dir"):
1002 f = "{}_/{}/{}".format(
1003 storage_params["folder"], storage_params["pkg-dir"], folder
1004 )
garciaale960531a2020-10-20 18:29:45 -03001005 else:
garciadeblas4568a372021-03-24 09:19:48 +01001006 f = "{}/{}/{}".format(
1007 storage_params["folder"], storage_params["pkg-dir"], folder
1008 )
garciaale960531a2020-10-20 18:29:45 -03001009 if file:
garciadeblas4568a372021-03-24 09:19:48 +01001010 return self.fs.file_exists("{}/{}".format(f, file), "file")
garciaale960531a2020-10-20 18:29:45 -03001011 else:
garciadeblas4568a372021-03-24 09:19:48 +01001012 if self.fs.file_exists(f, "dir"):
garciaale960531a2020-10-20 18:29:45 -03001013 if self.fs.dir_ls(f):
1014 return True
1015 return False
1016
1017 @staticmethod
garciaale7cbd03c2020-11-27 10:38:35 -03001018 def validate_internal_virtual_links(indata):
1019 all_ivld_ids = set()
1020 for ivld in get_iterable(indata.get("int-virtual-link-desc")):
1021 ivld_id = ivld.get("id")
1022 if ivld_id and ivld_id in all_ivld_ids:
garciadeblas4568a372021-03-24 09:19:48 +01001023 raise EngineException(
1024 "Duplicated VLD id in int-virtual-link-desc[id={}]".format(ivld_id),
1025 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1026 )
garciaale960531a2020-10-20 18:29:45 -03001027 else:
garciaale7cbd03c2020-11-27 10:38:35 -03001028 all_ivld_ids.add(ivld_id)
garciaale960531a2020-10-20 18:29:45 -03001029
garciaale7cbd03c2020-11-27 10:38:35 -03001030 for vdu in get_iterable(indata.get("vdu")):
1031 for int_cpd in get_iterable(vdu.get("int-cpd")):
1032 int_cpd_ivld_id = int_cpd.get("int-virtual-link-desc")
1033 if int_cpd_ivld_id and int_cpd_ivld_id not in all_ivld_ids:
1034 raise EngineException(
1035 "vdu[id='{}']:int-cpd[id='{}']:int-virtual-link-desc='{}' must match an existing "
garciadeblas4568a372021-03-24 09:19:48 +01001036 "int-virtual-link-desc".format(
1037 vdu["id"], int_cpd["id"], int_cpd_ivld_id
1038 ),
1039 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1040 )
garciaale960531a2020-10-20 18:29:45 -03001041
garciaale7cbd03c2020-11-27 10:38:35 -03001042 for df in get_iterable(indata.get("df")):
1043 for vlp in get_iterable(df.get("virtual-link-profile")):
1044 vlp_ivld_id = vlp.get("id")
1045 if vlp_ivld_id and vlp_ivld_id not in all_ivld_ids:
garciadeblas4568a372021-03-24 09:19:48 +01001046 raise EngineException(
1047 "df[id='{}']:virtual-link-profile='{}' must match an existing "
1048 "int-virtual-link-desc".format(df["id"], vlp_ivld_id),
1049 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1050 )
garciaale7cbd03c2020-11-27 10:38:35 -03001051
garciaale960531a2020-10-20 18:29:45 -03001052 @staticmethod
1053 def validate_monitoring_params(indata):
garciaale7cbd03c2020-11-27 10:38:35 -03001054 all_monitoring_params = set()
1055 for ivld in get_iterable(indata.get("int-virtual-link-desc")):
1056 for mp in get_iterable(ivld.get("monitoring-parameters")):
1057 mp_id = mp.get("id")
1058 if mp_id and mp_id in all_monitoring_params:
garciadeblas4568a372021-03-24 09:19:48 +01001059 raise EngineException(
1060 "Duplicated monitoring-parameter id in "
1061 "int-virtual-link-desc[id='{}']:monitoring-parameters[id='{}']".format(
1062 ivld["id"], mp_id
1063 ),
1064 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1065 )
gcalvino5e72d152018-10-23 11:46:57 +02001066 else:
garciaale7cbd03c2020-11-27 10:38:35 -03001067 all_monitoring_params.add(mp_id)
1068
1069 for vdu in get_iterable(indata.get("vdu")):
1070 for mp in get_iterable(vdu.get("monitoring-parameter")):
1071 mp_id = mp.get("id")
1072 if mp_id and mp_id in all_monitoring_params:
garciadeblas4568a372021-03-24 09:19:48 +01001073 raise EngineException(
1074 "Duplicated monitoring-parameter id in "
1075 "vdu[id='{}']:monitoring-parameter[id='{}']".format(
1076 vdu["id"], mp_id
1077 ),
1078 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1079 )
garciaale7cbd03c2020-11-27 10:38:35 -03001080 else:
1081 all_monitoring_params.add(mp_id)
1082
1083 for df in get_iterable(indata.get("df")):
1084 for mp in get_iterable(df.get("monitoring-parameter")):
1085 mp_id = mp.get("id")
1086 if mp_id and mp_id in all_monitoring_params:
garciadeblas4568a372021-03-24 09:19:48 +01001087 raise EngineException(
1088 "Duplicated monitoring-parameter id in "
1089 "df[id='{}']:monitoring-parameter[id='{}']".format(
1090 df["id"], mp_id
1091 ),
1092 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1093 )
garciaale7cbd03c2020-11-27 10:38:35 -03001094 else:
1095 all_monitoring_params.add(mp_id)
gcalvino5e72d152018-10-23 11:46:57 +02001096
garciaale960531a2020-10-20 18:29:45 -03001097 @staticmethod
1098 def validate_scaling_group_descriptor(indata):
garciaale7cbd03c2020-11-27 10:38:35 -03001099 all_monitoring_params = set()
1100 for ivld in get_iterable(indata.get("int-virtual-link-desc")):
1101 for mp in get_iterable(ivld.get("monitoring-parameters")):
1102 all_monitoring_params.add(mp.get("id"))
1103
1104 for vdu in get_iterable(indata.get("vdu")):
1105 for mp in get_iterable(vdu.get("monitoring-parameter")):
1106 all_monitoring_params.add(mp.get("id"))
1107
1108 for df in get_iterable(indata.get("df")):
1109 for mp in get_iterable(df.get("monitoring-parameter")):
1110 all_monitoring_params.add(mp.get("id"))
1111
1112 for df in get_iterable(indata.get("df")):
1113 for sa in get_iterable(df.get("scaling-aspect")):
1114 for sp in get_iterable(sa.get("scaling-policy")):
1115 for sc in get_iterable(sp.get("scaling-criteria")):
1116 sc_monitoring_param = sc.get("vnf-monitoring-param-ref")
garciadeblas4568a372021-03-24 09:19:48 +01001117 if (
1118 sc_monitoring_param
1119 and sc_monitoring_param not in all_monitoring_params
1120 ):
1121 raise EngineException(
1122 "df[id='{}']:scaling-aspect[id='{}']:scaling-policy"
1123 "[name='{}']:scaling-criteria[name='{}']: "
1124 "vnf-monitoring-param-ref='{}' not defined in any monitoring-param".format(
1125 df["id"],
1126 sa["id"],
1127 sp["name"],
1128 sc["name"],
1129 sc_monitoring_param,
1130 ),
1131 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1132 )
garciaale7cbd03c2020-11-27 10:38:35 -03001133
1134 for sca in get_iterable(sa.get("scaling-config-action")):
garciadeblas4568a372021-03-24 09:19:48 +01001135 if (
1136 "lcm-operations-configuration" not in df
1137 or "operate-vnf-op-config"
1138 not in df["lcm-operations-configuration"]
bravof41a52052021-02-17 18:08:01 -03001139 or not utils.find_in_list(
garciadeblas4568a372021-03-24 09:19:48 +01001140 df["lcm-operations-configuration"][
1141 "operate-vnf-op-config"
1142 ].get("day1-2", []),
1143 lambda config: config["id"] == indata["id"],
1144 )
bravof41a52052021-02-17 18:08:01 -03001145 ):
garciadeblas4568a372021-03-24 09:19:48 +01001146 raise EngineException(
1147 "'day1-2 configuration' not defined in the descriptor but it is "
1148 "referenced by df[id='{}']:scaling-aspect[id='{}']:scaling-config-action".format(
1149 df["id"], sa["id"]
1150 ),
1151 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1152 )
1153 for configuration in get_iterable(
1154 df["lcm-operations-configuration"]["operate-vnf-op-config"].get(
1155 "day1-2", []
1156 )
1157 ):
1158 for primitive in get_iterable(
1159 configuration.get("config-primitive")
1160 ):
1161 if (
1162 primitive["name"]
1163 == sca["vnf-config-primitive-name-ref"]
1164 ):
garciaale7cbd03c2020-11-27 10:38:35 -03001165 break
1166 else:
garciadeblas4568a372021-03-24 09:19:48 +01001167 raise EngineException(
1168 "df[id='{}']:scaling-aspect[id='{}']:scaling-config-action:vnf-"
1169 "config-primitive-name-ref='{}' does not match any "
1170 "day1-2 configuration:config-primitive:name".format(
1171 df["id"],
1172 sa["id"],
1173 sca["vnf-config-primitive-name-ref"],
1174 ),
1175 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1176 )
gcalvinoa6fe0002019-01-09 13:27:11 +01001177
delacruzramo271d2002019-12-02 21:00:37 +01001178 def delete_extra(self, session, _id, db_content, not_send_msg=None):
1179 """
1180 Deletes associate file system storage (via super)
1181 Deletes associated vnfpkgops from database.
1182 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1183 :param _id: server internal id
1184 :param db_content: The database content of the descriptor
1185 :return: None
1186 :raises: FsException in case of error while deleting associated storage
1187 """
1188 super().delete_extra(session, _id, db_content, not_send_msg)
1189 self.db.del_list("vnfpkgops", {"vnfPkgId": _id})
beierlmcee2ebf2022-03-29 17:42:48 -04001190 self.db.del_list(self.topic+"_revisions", {"_id": {"$regex": _id}})
garciaale960531a2020-10-20 18:29:45 -03001191
Frank Bryden19b97522020-07-10 12:32:02 +00001192 def sol005_projection(self, data):
1193 data["onboardingState"] = data["_admin"]["onboardingState"]
1194 data["operationalState"] = data["_admin"]["operationalState"]
1195 data["usageState"] = data["_admin"]["usageState"]
1196
1197 links = {}
1198 links["self"] = {"href": "/vnfpkgm/v1/vnf_packages/{}".format(data["_id"])}
1199 links["vnfd"] = {"href": "/vnfpkgm/v1/vnf_packages/{}/vnfd".format(data["_id"])}
garciadeblas4568a372021-03-24 09:19:48 +01001200 links["packageContent"] = {
1201 "href": "/vnfpkgm/v1/vnf_packages/{}/package_content".format(data["_id"])
1202 }
Frank Bryden19b97522020-07-10 12:32:02 +00001203 data["_links"] = links
garciaale960531a2020-10-20 18:29:45 -03001204
Frank Bryden19b97522020-07-10 12:32:02 +00001205 return super().sol005_projection(data)
delacruzramo271d2002019-12-02 21:00:37 +01001206
aticig9cfa8162022-04-07 11:57:18 +03001207 @staticmethod
1208 def find_software_version(vnfd: dict) -> str:
1209 """Find the sotware version in the VNFD descriptors
1210
1211 Args:
1212 vnfd (dict): Descriptor as a dictionary
1213
1214 Returns:
1215 software-version (str)
1216 """
1217 default_sw_version = "1.0"
1218 if vnfd.get("vnfd"):
1219 vnfd = vnfd["vnfd"]
1220 if vnfd.get("software-version"):
1221 return vnfd["software-version"]
1222 else:
1223 return default_sw_version
1224
1225 @staticmethod
1226 def extract_policies(vnfd: dict) -> dict:
1227 """Removes the policies from the VNFD descriptors
1228
1229 Args:
1230 vnfd (dict): Descriptor as a dictionary
1231
1232 Returns:
1233 vnfd (dict): VNFD which does not include policies
1234 """
elumalai3622f832022-07-08 12:06:27 +05301235 for df in vnfd.get("df", {}):
1236 for policy in ["scaling-aspect", "healing-aspect"]:
1237 if (df.get(policy, {})):
1238 df.pop(policy)
1239 for vdu in vnfd.get("vdu", {}):
1240 for alarm_policy in ["alarm", "monitoring-parameter"]:
1241 if (vdu.get(alarm_policy, {})):
1242 vdu.pop(alarm_policy)
aticig9cfa8162022-04-07 11:57:18 +03001243 return vnfd
1244
1245 @staticmethod
1246 def extract_day12_primitives(vnfd: dict) -> dict:
1247 """Removes the day12 primitives from the VNFD descriptors
1248
1249 Args:
1250 vnfd (dict): Descriptor as a dictionary
1251
1252 Returns:
1253 vnfd (dict)
1254 """
1255 for df_id, df in enumerate(vnfd.get("df", {})):
1256 if (
1257 df.get("lcm-operations-configuration", {})
1258 .get("operate-vnf-op-config", {})
1259 .get("day1-2")
1260 ):
1261 day12 = df["lcm-operations-configuration"]["operate-vnf-op-config"].get(
1262 "day1-2"
1263 )
1264 for config_id, config in enumerate(day12):
1265 for key in [
1266 "initial-config-primitive",
1267 "config-primitive",
1268 "terminate-config-primitive",
1269 ]:
1270 config.pop(key, None)
1271 day12[config_id] = config
1272 df["lcm-operations-configuration"]["operate-vnf-op-config"][
1273 "day1-2"
1274 ] = day12
1275 vnfd["df"][df_id] = df
1276 return vnfd
1277
1278 def remove_modifiable_items(self, vnfd: dict) -> dict:
1279 """Removes the modifiable parts from the VNFD descriptors
1280
1281 It calls different extract functions according to different update types
1282 to clear all the modifiable items from VNFD
1283
1284 Args:
1285 vnfd (dict): Descriptor as a dictionary
1286
1287 Returns:
1288 vnfd (dict): Descriptor which does not include modifiable contents
1289 """
1290 if vnfd.get("vnfd"):
1291 vnfd = vnfd["vnfd"]
1292 vnfd.pop("_admin", None)
1293 # If the other extractions need to be done from VNFD,
1294 # the new extract methods could be appended to below list.
1295 for extract_function in [self.extract_day12_primitives, self.extract_policies]:
1296 vnfd_temp = extract_function(vnfd)
1297 vnfd = vnfd_temp
1298 return vnfd
1299
1300 def _validate_descriptor_changes(
1301 self,
aticig2b5e1232022-08-10 17:30:12 +03001302 descriptor_id: str,
aticig9cfa8162022-04-07 11:57:18 +03001303 descriptor_file_name: str,
1304 old_descriptor_directory: str,
1305 new_descriptor_directory: str,
1306 ):
1307 """Compares the old and new VNFD descriptors and validates the new descriptor.
1308
1309 Args:
1310 old_descriptor_directory (str): Directory of descriptor which is in-use
aticig2b5e1232022-08-10 17:30:12 +03001311 new_descriptor_directory (str): Directory of descriptor which is proposed to update (new revision)
aticig9cfa8162022-04-07 11:57:18 +03001312
1313 Returns:
1314 None
1315
1316 Raises:
1317 EngineException: In case of error when there are unallowed changes
1318 """
1319 try:
aticig2b5e1232022-08-10 17:30:12 +03001320 # If VNFD does not exist in DB or it is not in use by any NS,
1321 # validation is not required.
1322 vnfd = self.db.get_one("vnfds", {"_id": descriptor_id})
1323 if not vnfd or not detect_descriptor_usage(vnfd, "vnfds", self.db):
1324 return
1325
1326 # Get the old and new descriptor contents in order to compare them.
aticig9cfa8162022-04-07 11:57:18 +03001327 with self.fs.file_open(
1328 (old_descriptor_directory.rstrip("/"), descriptor_file_name), "r"
1329 ) as old_descriptor_file:
aticig2b5e1232022-08-10 17:30:12 +03001330
aticig9cfa8162022-04-07 11:57:18 +03001331 with self.fs.file_open(
aticig2b5e1232022-08-10 17:30:12 +03001332 (new_descriptor_directory.rstrip("/"), descriptor_file_name), "r"
aticig9cfa8162022-04-07 11:57:18 +03001333 ) as new_descriptor_file:
aticig2b5e1232022-08-10 17:30:12 +03001334
1335 old_content = yaml.safe_load(old_descriptor_file.read())
1336 new_content = yaml.safe_load(new_descriptor_file.read())
1337
1338 # If software version has changed, we do not need to validate
1339 # the differences anymore.
aticig9cfa8162022-04-07 11:57:18 +03001340 if old_content and new_content:
1341 if self.find_software_version(
1342 old_content
1343 ) != self.find_software_version(new_content):
1344 return
aticig2b5e1232022-08-10 17:30:12 +03001345
aticig9cfa8162022-04-07 11:57:18 +03001346 disallowed_change = DeepDiff(
1347 self.remove_modifiable_items(old_content),
1348 self.remove_modifiable_items(new_content),
1349 )
aticig2b5e1232022-08-10 17:30:12 +03001350
aticig9cfa8162022-04-07 11:57:18 +03001351 if disallowed_change:
1352 changed_nodes = functools.reduce(
1353 lambda a, b: a + " , " + b,
1354 [
1355 node.lstrip("root")
1356 for node in disallowed_change.get(
1357 "values_changed"
1358 ).keys()
1359 ],
1360 )
aticig2b5e1232022-08-10 17:30:12 +03001361
aticig9cfa8162022-04-07 11:57:18 +03001362 raise EngineException(
1363 f"Error in validating new descriptor: {changed_nodes} cannot be modified, "
1364 "there are disallowed changes in the vnf descriptor.",
1365 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1366 )
1367 except (
1368 DbException,
1369 AttributeError,
1370 IndexError,
1371 KeyError,
1372 ValueError,
1373 ) as e:
1374 raise type(e)(
1375 "VNF Descriptor could not be processed with error: {}.".format(e)
1376 )
1377
tiernob24258a2018-10-04 18:39:49 +02001378
1379class NsdTopic(DescriptorTopic):
1380 topic = "nsds"
1381 topic_msg = "nsd"
1382
delacruzramo32bab472019-09-13 12:24:22 +02001383 def __init__(self, db, fs, msg, auth):
1384 DescriptorTopic.__init__(self, db, fs, msg, auth)
tiernob24258a2018-10-04 18:39:49 +02001385
garciaale7cbd03c2020-11-27 10:38:35 -03001386 def pyangbind_validation(self, item, data, force=False):
garciaaledf718ae2020-12-03 19:17:28 -03001387 if self._descriptor_data_is_in_old_format(data):
garciadeblas4568a372021-03-24 09:19:48 +01001388 raise EngineException(
1389 "ERROR: Unsupported descriptor format. Please, use an ETSI SOL006 descriptor.",
1390 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1391 )
garciaale7cbd03c2020-11-27 10:38:35 -03001392 try:
garciadeblas4568a372021-03-24 09:19:48 +01001393 nsd_vnf_profiles = data.get("df", [{}])[0].get("vnf-profile", [])
garciaale7cbd03c2020-11-27 10:38:35 -03001394 mynsd = etsi_nfv_nsd.etsi_nfv_nsd()
garciadeblas4568a372021-03-24 09:19:48 +01001395 pybindJSONDecoder.load_ietf_json(
1396 {"nsd": {"nsd": [data]}},
1397 None,
1398 None,
1399 obj=mynsd,
1400 path_helper=True,
1401 skip_unknown=force,
1402 )
garciaale7cbd03c2020-11-27 10:38:35 -03001403 out = pybindJSON.dumps(mynsd, mode="ietf")
1404 desc_out = self._remove_envelop(yaml.safe_load(out))
1405 desc_out = self._remove_yang_prefixes_from_descriptor(desc_out)
garciaale341ac1b2020-12-11 20:04:11 -03001406 if nsd_vnf_profiles:
garciadeblas4568a372021-03-24 09:19:48 +01001407 desc_out["df"][0]["vnf-profile"] = nsd_vnf_profiles
garciaale7cbd03c2020-11-27 10:38:35 -03001408 return desc_out
1409 except Exception as e:
garciadeblas4568a372021-03-24 09:19:48 +01001410 raise EngineException(
1411 "Error in pyangbind validation: {}".format(str(e)),
1412 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1413 )
garciaale7cbd03c2020-11-27 10:38:35 -03001414
tiernob24258a2018-10-04 18:39:49 +02001415 @staticmethod
garciaaledf718ae2020-12-03 19:17:28 -03001416 def _descriptor_data_is_in_old_format(data):
garciadeblas4568a372021-03-24 09:19:48 +01001417 return ("nsd-catalog" in data) or ("nsd:nsd-catalog" in data)
garciaaledf718ae2020-12-03 19:17:28 -03001418
1419 @staticmethod
tiernob24258a2018-10-04 18:39:49 +02001420 def _remove_envelop(indata=None):
1421 if not indata:
1422 return {}
1423 clean_indata = indata
1424
garciadeblas4568a372021-03-24 09:19:48 +01001425 if clean_indata.get("nsd"):
1426 clean_indata = clean_indata["nsd"]
1427 elif clean_indata.get("etsi-nfv-nsd:nsd"):
1428 clean_indata = clean_indata["etsi-nfv-nsd:nsd"]
1429 if clean_indata.get("nsd"):
1430 if (
1431 not isinstance(clean_indata["nsd"], list)
1432 or len(clean_indata["nsd"]) != 1
1433 ):
gcalvino46e4cb82018-10-26 13:10:22 +02001434 raise EngineException("'nsd' must be a list of only one element")
garciadeblas4568a372021-03-24 09:19:48 +01001435 clean_indata = clean_indata["nsd"][0]
tiernob24258a2018-10-04 18:39:49 +02001436 return clean_indata
1437
gcalvinoa6fe0002019-01-09 13:27:11 +01001438 def _validate_input_new(self, indata, storage_params, force=False):
Frank Bryden19b97522020-07-10 12:32:02 +00001439 indata.pop("nsdOnboardingState", None)
1440 indata.pop("nsdOperationalState", None)
1441 indata.pop("nsdUsageState", None)
1442
1443 indata.pop("links", None)
1444
gcalvino46e4cb82018-10-26 13:10:22 +02001445 indata = self.pyangbind_validation("nsds", indata, force)
tierno5a5c2182018-11-20 12:27:42 +00001446 # Cross references validation in the descriptor
tiernoaa1ca7b2018-11-08 19:00:20 +01001447 # 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 -03001448 for vld in get_iterable(indata.get("virtual-link-desc")):
1449 self.validate_vld_mgmt_network_with_virtual_link_protocol_data(vld, indata)
garciaale960531a2020-10-20 18:29:45 -03001450
garciaale7cbd03c2020-11-27 10:38:35 -03001451 self.validate_vnf_profiles_vnfd_id(indata)
garciaale960531a2020-10-20 18:29:45 -03001452
tiernob24258a2018-10-04 18:39:49 +02001453 return indata
1454
garciaale960531a2020-10-20 18:29:45 -03001455 @staticmethod
garciaale7cbd03c2020-11-27 10:38:35 -03001456 def validate_vld_mgmt_network_with_virtual_link_protocol_data(vld, indata):
1457 if not vld.get("mgmt-network"):
1458 return
1459 vld_id = vld.get("id")
1460 for df in get_iterable(indata.get("df")):
1461 for vlp in get_iterable(df.get("virtual-link-profile")):
1462 if vld_id and vld_id == vlp.get("virtual-link-desc-id"):
1463 if vlp.get("virtual-link-protocol-data"):
garciadeblas4568a372021-03-24 09:19:48 +01001464 raise EngineException(
1465 "Error at df[id='{}']:virtual-link-profile[id='{}']:virtual-link-"
1466 "protocol-data You cannot set a virtual-link-protocol-data "
1467 "when mgmt-network is True".format(df["id"], vlp["id"]),
1468 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1469 )
garciaale960531a2020-10-20 18:29:45 -03001470
1471 @staticmethod
garciaale7cbd03c2020-11-27 10:38:35 -03001472 def validate_vnf_profiles_vnfd_id(indata):
1473 all_vnfd_ids = set(get_iterable(indata.get("vnfd-id")))
1474 for df in get_iterable(indata.get("df")):
1475 for vnf_profile in get_iterable(df.get("vnf-profile")):
1476 vnfd_id = vnf_profile.get("vnfd-id")
1477 if vnfd_id and vnfd_id not in all_vnfd_ids:
garciadeblas4568a372021-03-24 09:19:48 +01001478 raise EngineException(
1479 "Error at df[id='{}']:vnf_profile[id='{}']:vnfd-id='{}' "
1480 "does not match any vnfd-id".format(
1481 df["id"], vnf_profile["id"], vnfd_id
1482 ),
1483 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1484 )
garciaale960531a2020-10-20 18:29:45 -03001485
Frank Brydendeba68e2020-07-27 13:55:11 +00001486 def _validate_input_edit(self, indata, content, force=False):
tiernoaa1ca7b2018-11-08 19:00:20 +01001487 # not needed to validate with pyangbind becuase it will be validated at check_conflict_on_edit
Frank Brydendeba68e2020-07-27 13:55:11 +00001488 """
1489 indata looks as follows:
garciadeblas4568a372021-03-24 09:19:48 +01001490 - In the new case (conformant)
1491 {'nsdOperationalState': 'DISABLED', 'userDefinedData': {'id': 'string23',
Frank Brydendeba68e2020-07-27 13:55:11 +00001492 '_id': 'c6ddc544-cede-4b94-9ebe-be07b298a3c1', 'name': 'simon46'}}
1493 - In the old case (backwards-compatible)
1494 {'id': 'string23', '_id': 'c6ddc544-cede-4b94-9ebe-be07b298a3c1', 'name': 'simon46'}
1495 """
1496 if "_admin" not in indata:
1497 indata["_admin"] = {}
1498
1499 if "nsdOperationalState" in indata:
1500 if indata["nsdOperationalState"] in ("ENABLED", "DISABLED"):
1501 indata["_admin"]["operationalState"] = indata.pop("nsdOperationalState")
1502 else:
garciadeblas4568a372021-03-24 09:19:48 +01001503 raise EngineException(
1504 "State '{}' is not a valid operational state".format(
1505 indata["nsdOperationalState"]
1506 ),
1507 http_code=HTTPStatus.BAD_REQUEST,
1508 )
Frank Brydendeba68e2020-07-27 13:55:11 +00001509
garciadeblas4568a372021-03-24 09:19:48 +01001510 # 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 +00001511 # to preserve current expected behaviour
1512 if "userDefinedData" in indata:
1513 data = indata.pop("userDefinedData")
1514 if type(data) == dict:
1515 indata["_admin"]["userDefinedData"] = data
1516 else:
garciadeblas4568a372021-03-24 09:19:48 +01001517 raise EngineException(
1518 "userDefinedData should be an object, but is '{}' instead".format(
1519 type(data)
1520 ),
1521 http_code=HTTPStatus.BAD_REQUEST,
1522 )
1523 if (
1524 "operationalState" in indata["_admin"]
1525 and content["_admin"]["operationalState"]
1526 == indata["_admin"]["operationalState"]
1527 ):
1528 raise EngineException(
1529 "nsdOperationalState already {}".format(
1530 content["_admin"]["operationalState"]
1531 ),
1532 http_code=HTTPStatus.CONFLICT,
1533 )
tiernob24258a2018-10-04 18:39:49 +02001534 return indata
1535
tierno65ca36d2019-02-12 19:27:52 +01001536 def _check_descriptor_dependencies(self, session, descriptor):
tiernob24258a2018-10-04 18:39:49 +02001537 """
tierno5a5c2182018-11-20 12:27:42 +00001538 Check that the dependent descriptors exist on a new descriptor or edition. Also checks references to vnfd
1539 connection points are ok
tierno65ca36d2019-02-12 19:27:52 +01001540 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +02001541 :param descriptor: descriptor to be inserted or edit
1542 :return: None or raises exception
1543 """
tierno65ca36d2019-02-12 19:27:52 +01001544 if session["force"]:
tiernob24258a2018-10-04 18:39:49 +02001545 return
garciaale7cbd03c2020-11-27 10:38:35 -03001546 vnfds_index = self._get_descriptor_constituent_vnfds_index(session, descriptor)
garciaale960531a2020-10-20 18:29:45 -03001547
1548 # Cross references validation in the descriptor and vnfd connection point validation
garciaale7cbd03c2020-11-27 10:38:35 -03001549 for df in get_iterable(descriptor.get("df")):
1550 self.validate_df_vnf_profiles_constituent_connection_points(df, vnfds_index)
garciaale960531a2020-10-20 18:29:45 -03001551
garciaale7cbd03c2020-11-27 10:38:35 -03001552 def _get_descriptor_constituent_vnfds_index(self, session, descriptor):
1553 vnfds_index = {}
1554 if descriptor.get("vnfd-id") and not session["force"]:
1555 for vnfd_id in get_iterable(descriptor.get("vnfd-id")):
garciaale960531a2020-10-20 18:29:45 -03001556 query_filter = self._get_project_filter(session)
1557 query_filter["id"] = vnfd_id
1558 vnf_list = self.db.get_list("vnfds", query_filter)
tierno5a5c2182018-11-20 12:27:42 +00001559 if not vnf_list:
garciadeblas4568a372021-03-24 09:19:48 +01001560 raise EngineException(
1561 "Descriptor error at 'vnfd-id'='{}' references a non "
1562 "existing vnfd".format(vnfd_id),
1563 http_code=HTTPStatus.CONFLICT,
1564 )
garciaale7cbd03c2020-11-27 10:38:35 -03001565 vnfds_index[vnfd_id] = vnf_list[0]
1566 return vnfds_index
garciaale960531a2020-10-20 18:29:45 -03001567
1568 @staticmethod
garciaale7cbd03c2020-11-27 10:38:35 -03001569 def validate_df_vnf_profiles_constituent_connection_points(df, vnfds_index):
1570 for vnf_profile in get_iterable(df.get("vnf-profile")):
1571 vnfd = vnfds_index.get(vnf_profile["vnfd-id"])
1572 all_vnfd_ext_cpds = set()
1573 for ext_cpd in get_iterable(vnfd.get("ext-cpd")):
garciadeblas4568a372021-03-24 09:19:48 +01001574 if ext_cpd.get("id"):
1575 all_vnfd_ext_cpds.add(ext_cpd.get("id"))
garciaale7cbd03c2020-11-27 10:38:35 -03001576
garciadeblas4568a372021-03-24 09:19:48 +01001577 for virtual_link in get_iterable(
1578 vnf_profile.get("virtual-link-connectivity")
1579 ):
garciaale7cbd03c2020-11-27 10:38:35 -03001580 for vl_cpd in get_iterable(virtual_link.get("constituent-cpd-id")):
garciadeblas4568a372021-03-24 09:19:48 +01001581 vl_cpd_id = vl_cpd.get("constituent-cpd-id")
garciaale7cbd03c2020-11-27 10:38:35 -03001582 if vl_cpd_id and vl_cpd_id not in all_vnfd_ext_cpds:
garciadeblas4568a372021-03-24 09:19:48 +01001583 raise EngineException(
1584 "Error at df[id='{}']:vnf-profile[id='{}']:virtual-link-connectivity"
1585 "[virtual-link-profile-id='{}']:constituent-cpd-id='{}' references a "
1586 "non existing ext-cpd:id inside vnfd '{}'".format(
1587 df["id"],
1588 vnf_profile["id"],
1589 virtual_link["virtual-link-profile-id"],
1590 vl_cpd_id,
1591 vnfd["id"],
1592 ),
1593 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1594 )
tiernob24258a2018-10-04 18:39:49 +02001595
tierno65ca36d2019-02-12 19:27:52 +01001596 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
garciadeblas4568a372021-03-24 09:19:48 +01001597 final_content = super().check_conflict_on_edit(
1598 session, final_content, edit_content, _id
1599 )
tiernob24258a2018-10-04 18:39:49 +02001600
tierno65ca36d2019-02-12 19:27:52 +01001601 self._check_descriptor_dependencies(session, final_content)
tiernob24258a2018-10-04 18:39:49 +02001602
bravofb995ea22021-02-10 10:57:52 -03001603 return final_content
1604
tiernob4844ab2019-05-23 08:42:12 +00001605 def check_conflict_on_del(self, session, _id, db_content):
tiernob24258a2018-10-04 18:39:49 +02001606 """
1607 Check that there is not any NSR that uses this NSD. Only NSRs belonging to this project are considered. Note
1608 that NSD can be public and be used by other projects.
tierno65ca36d2019-02-12 19:27:52 +01001609 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob4844ab2019-05-23 08:42:12 +00001610 :param _id: nsd internal id
1611 :param db_content: The database content of the _id
tiernob24258a2018-10-04 18:39:49 +02001612 :return: None or raises EngineException with the conflict
1613 """
tierno65ca36d2019-02-12 19:27:52 +01001614 if session["force"]:
tiernob24258a2018-10-04 18:39:49 +02001615 return
tiernob4844ab2019-05-23 08:42:12 +00001616 descriptor = db_content
1617 descriptor_id = descriptor.get("id")
1618 if not descriptor_id: # empty nsd not uploaded
1619 return
1620
1621 # check NSD used by NS
tierno65ca36d2019-02-12 19:27:52 +01001622 _filter = self._get_project_filter(session)
tiernob4844ab2019-05-23 08:42:12 +00001623 _filter["nsd-id"] = _id
tiernob24258a2018-10-04 18:39:49 +02001624 if self.db.get_list("nsrs", _filter):
garciadeblas4568a372021-03-24 09:19:48 +01001625 raise EngineException(
1626 "There is at least one NS instance using this descriptor",
1627 http_code=HTTPStatus.CONFLICT,
1628 )
tiernob4844ab2019-05-23 08:42:12 +00001629
1630 # check NSD referenced by NST
1631 del _filter["nsd-id"]
1632 _filter["netslice-subnet.ANYINDEX.nsd-ref"] = descriptor_id
1633 if self.db.get_list("nsts", _filter):
garciadeblas4568a372021-03-24 09:19:48 +01001634 raise EngineException(
1635 "There is at least one NetSlice Template referencing this descriptor",
1636 http_code=HTTPStatus.CONFLICT,
1637 )
garciaale960531a2020-10-20 18:29:45 -03001638
beierlmcee2ebf2022-03-29 17:42:48 -04001639 def delete_extra(self, session, _id, db_content, not_send_msg=None):
1640 """
1641 Deletes associate file system storage (via super)
1642 Deletes associated vnfpkgops from database.
1643 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1644 :param _id: server internal id
1645 :param db_content: The database content of the descriptor
1646 :return: None
1647 :raises: FsException in case of error while deleting associated storage
1648 """
1649 super().delete_extra(session, _id, db_content, not_send_msg)
1650 self.db.del_list(self.topic+"_revisions", { "_id": { "$regex": _id}})
1651
aticig9cfa8162022-04-07 11:57:18 +03001652 @staticmethod
1653 def extract_day12_primitives(nsd: dict) -> dict:
1654 """Removes the day12 primitives from the NSD descriptors
1655
1656 Args:
1657 nsd (dict): Descriptor as a dictionary
1658
1659 Returns:
1660 nsd (dict): Cleared NSD
1661 """
1662 if nsd.get("ns-configuration"):
1663 for key in [
1664 "config-primitive",
1665 "initial-config-primitive",
1666 "terminate-config-primitive",
1667 ]:
1668 nsd["ns-configuration"].pop(key, None)
1669 return nsd
1670
1671 def remove_modifiable_items(self, nsd: dict) -> dict:
1672 """Removes the modifiable parts from the VNFD descriptors
1673
1674 It calls different extract functions according to different update types
1675 to clear all the modifiable items from NSD
1676
1677 Args:
1678 nsd (dict): Descriptor as a dictionary
1679
1680 Returns:
1681 nsd (dict): Descriptor which does not include modifiable contents
1682 """
1683 while isinstance(nsd, dict) and nsd.get("nsd"):
1684 nsd = nsd["nsd"]
1685 if isinstance(nsd, list):
1686 nsd = nsd[0]
1687 nsd.pop("_admin", None)
1688 # If the more extractions need to be done from NSD,
1689 # the new extract methods could be appended to below list.
1690 for extract_function in [self.extract_day12_primitives]:
1691 nsd_temp = extract_function(nsd)
1692 nsd = nsd_temp
1693 return nsd
1694
1695 def _validate_descriptor_changes(
1696 self,
aticig2b5e1232022-08-10 17:30:12 +03001697 descriptor_id: str,
aticig9cfa8162022-04-07 11:57:18 +03001698 descriptor_file_name: str,
1699 old_descriptor_directory: str,
1700 new_descriptor_directory: str,
1701 ):
1702 """Compares the old and new NSD descriptors and validates the new descriptor
1703
1704 Args:
1705 old_descriptor_directory: Directory of descriptor which is in-use
aticig2b5e1232022-08-10 17:30:12 +03001706 new_descriptor_directory: Directory of descriptor which is proposed to update (new revision)
aticig9cfa8162022-04-07 11:57:18 +03001707
1708 Returns:
1709 None
1710
1711 Raises:
1712 EngineException: In case of error if the changes are not allowed
1713 """
1714
1715 try:
aticig2b5e1232022-08-10 17:30:12 +03001716 # If NSD does not exist in DB, or it is not in use by any NS,
1717 # validation is not required.
1718 nsd = self.db.get_one("nsds", {"_id": descriptor_id}, fail_on_empty=False)
1719 if not nsd or not detect_descriptor_usage(nsd, "nsds", self.db):
1720 return
1721
1722 # Get the old and new descriptor contents in order to compare them.
aticig9cfa8162022-04-07 11:57:18 +03001723 with self.fs.file_open(
aticig2b5e1232022-08-10 17:30:12 +03001724 (old_descriptor_directory.rstrip("/"), descriptor_file_name), "r"
aticig9cfa8162022-04-07 11:57:18 +03001725 ) as old_descriptor_file:
aticig2b5e1232022-08-10 17:30:12 +03001726
aticig9cfa8162022-04-07 11:57:18 +03001727 with self.fs.file_open(
1728 (new_descriptor_directory.rstrip("/"), descriptor_file_name), "r"
1729 ) as new_descriptor_file:
aticig2b5e1232022-08-10 17:30:12 +03001730
1731 old_content = yaml.safe_load(old_descriptor_file.read())
1732 new_content = yaml.safe_load(new_descriptor_file.read())
1733
aticig9cfa8162022-04-07 11:57:18 +03001734 if old_content and new_content:
1735 disallowed_change = DeepDiff(
1736 self.remove_modifiable_items(old_content),
1737 self.remove_modifiable_items(new_content),
1738 )
aticig2b5e1232022-08-10 17:30:12 +03001739
aticig9cfa8162022-04-07 11:57:18 +03001740 if disallowed_change:
1741 changed_nodes = functools.reduce(
1742 lambda a, b: a + ", " + b,
1743 [
1744 node.lstrip("root")
1745 for node in disallowed_change.get(
1746 "values_changed"
1747 ).keys()
1748 ],
1749 )
aticig2b5e1232022-08-10 17:30:12 +03001750
aticig9cfa8162022-04-07 11:57:18 +03001751 raise EngineException(
1752 f"Error in validating new descriptor: {changed_nodes} cannot be modified, "
1753 "there are disallowed changes in the ns descriptor. ",
1754 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1755 )
1756 except (
1757 DbException,
1758 AttributeError,
1759 IndexError,
1760 KeyError,
1761 ValueError,
1762 ) as e:
1763 raise type(e)(
1764 "NS Descriptor could not be processed with error: {}.".format(e)
1765 )
1766
Frank Bryden19b97522020-07-10 12:32:02 +00001767 def sol005_projection(self, data):
1768 data["nsdOnboardingState"] = data["_admin"]["onboardingState"]
1769 data["nsdOperationalState"] = data["_admin"]["operationalState"]
1770 data["nsdUsageState"] = data["_admin"]["usageState"]
1771
1772 links = {}
1773 links["self"] = {"href": "/nsd/v1/ns_descriptors/{}".format(data["_id"])}
garciadeblas4568a372021-03-24 09:19:48 +01001774 links["nsd_content"] = {
1775 "href": "/nsd/v1/ns_descriptors/{}/nsd_content".format(data["_id"])
1776 }
Frank Bryden19b97522020-07-10 12:32:02 +00001777 data["_links"] = links
garciaale960531a2020-10-20 18:29:45 -03001778
Frank Bryden19b97522020-07-10 12:32:02 +00001779 return super().sol005_projection(data)
tiernob24258a2018-10-04 18:39:49 +02001780
1781
Felipe Vicensb57758d2018-10-16 16:00:20 +02001782class NstTopic(DescriptorTopic):
1783 topic = "nsts"
1784 topic_msg = "nst"
tierno6b02b052020-06-02 10:07:41 +00001785 quota_name = "slice_templates"
Felipe Vicensb57758d2018-10-16 16:00:20 +02001786
delacruzramo32bab472019-09-13 12:24:22 +02001787 def __init__(self, db, fs, msg, auth):
1788 DescriptorTopic.__init__(self, db, fs, msg, auth)
Felipe Vicensb57758d2018-10-16 16:00:20 +02001789
garciaale7cbd03c2020-11-27 10:38:35 -03001790 def pyangbind_validation(self, item, data, force=False):
1791 try:
1792 mynst = nst_im()
garciadeblas4568a372021-03-24 09:19:48 +01001793 pybindJSONDecoder.load_ietf_json(
1794 {"nst": [data]},
1795 None,
1796 None,
1797 obj=mynst,
1798 path_helper=True,
1799 skip_unknown=force,
1800 )
garciaale7cbd03c2020-11-27 10:38:35 -03001801 out = pybindJSON.dumps(mynst, mode="ietf")
1802 desc_out = self._remove_envelop(yaml.safe_load(out))
1803 return desc_out
1804 except Exception as e:
garciadeblas4568a372021-03-24 09:19:48 +01001805 raise EngineException(
1806 "Error in pyangbind validation: {}".format(str(e)),
1807 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
1808 )
garciaale7cbd03c2020-11-27 10:38:35 -03001809
Felipe Vicensb57758d2018-10-16 16:00:20 +02001810 @staticmethod
1811 def _remove_envelop(indata=None):
1812 if not indata:
1813 return {}
1814 clean_indata = indata
1815
garciadeblas4568a372021-03-24 09:19:48 +01001816 if clean_indata.get("nst"):
1817 if (
1818 not isinstance(clean_indata["nst"], list)
1819 or len(clean_indata["nst"]) != 1
1820 ):
Felipe Vicensb57758d2018-10-16 16:00:20 +02001821 raise EngineException("'nst' must be a list only one element")
garciadeblas4568a372021-03-24 09:19:48 +01001822 clean_indata = clean_indata["nst"][0]
1823 elif clean_indata.get("nst:nst"):
1824 if (
1825 not isinstance(clean_indata["nst:nst"], list)
1826 or len(clean_indata["nst:nst"]) != 1
1827 ):
gcalvino70434c12018-11-27 15:17:04 +01001828 raise EngineException("'nst:nst' must be a list only one element")
garciadeblas4568a372021-03-24 09:19:48 +01001829 clean_indata = clean_indata["nst:nst"][0]
Felipe Vicensb57758d2018-10-16 16:00:20 +02001830 return clean_indata
1831
gcalvinoa6fe0002019-01-09 13:27:11 +01001832 def _validate_input_new(self, indata, storage_params, force=False):
Frank Bryden19b97522020-07-10 12:32:02 +00001833 indata.pop("onboardingState", None)
1834 indata.pop("operationalState", None)
1835 indata.pop("usageState", None)
gcalvino70434c12018-11-27 15:17:04 +01001836 indata = self.pyangbind_validation("nsts", indata, force)
Felipe Vicense36ab852018-11-23 14:12:09 +01001837 return indata.copy()
1838
Felipe Vicensb57758d2018-10-16 16:00:20 +02001839 def _check_descriptor_dependencies(self, session, descriptor):
1840 """
1841 Check that the dependent descriptors exist on a new descriptor or edition
tierno65ca36d2019-02-12 19:27:52 +01001842 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Felipe Vicensb57758d2018-10-16 16:00:20 +02001843 :param descriptor: descriptor to be inserted or edit
1844 :return: None or raises exception
1845 """
1846 if not descriptor.get("netslice-subnet"):
1847 return
1848 for nsd in descriptor["netslice-subnet"]:
1849 nsd_id = nsd["nsd-ref"]
tierno65ca36d2019-02-12 19:27:52 +01001850 filter_q = self._get_project_filter(session)
Felipe Vicensb57758d2018-10-16 16:00:20 +02001851 filter_q["id"] = nsd_id
1852 if not self.db.get_list("nsds", filter_q):
garciadeblas4568a372021-03-24 09:19:48 +01001853 raise EngineException(
1854 "Descriptor error at 'netslice-subnet':'nsd-ref'='{}' references a non "
1855 "existing nsd".format(nsd_id),
1856 http_code=HTTPStatus.CONFLICT,
1857 )
Felipe Vicensb57758d2018-10-16 16:00:20 +02001858
tierno65ca36d2019-02-12 19:27:52 +01001859 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
garciadeblas4568a372021-03-24 09:19:48 +01001860 final_content = super().check_conflict_on_edit(
1861 session, final_content, edit_content, _id
1862 )
Felipe Vicensb57758d2018-10-16 16:00:20 +02001863
1864 self._check_descriptor_dependencies(session, final_content)
bravofb995ea22021-02-10 10:57:52 -03001865 return final_content
Felipe Vicensb57758d2018-10-16 16:00:20 +02001866
tiernob4844ab2019-05-23 08:42:12 +00001867 def check_conflict_on_del(self, session, _id, db_content):
Felipe Vicensb57758d2018-10-16 16:00:20 +02001868 """
1869 Check that there is not any NSIR that uses this NST. Only NSIRs belonging to this project are considered. Note
1870 that NST can be public and be used by other projects.
tierno65ca36d2019-02-12 19:27:52 +01001871 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
Felipe Vicens07f31722018-10-29 15:16:44 +01001872 :param _id: nst internal id
tiernob4844ab2019-05-23 08:42:12 +00001873 :param db_content: The database content of the _id.
Felipe Vicensb57758d2018-10-16 16:00:20 +02001874 :return: None or raises EngineException with the conflict
1875 """
1876 # TODO: Check this method
tierno65ca36d2019-02-12 19:27:52 +01001877 if session["force"]:
Felipe Vicensb57758d2018-10-16 16:00:20 +02001878 return
Felipe Vicens07f31722018-10-29 15:16:44 +01001879 # Get Network Slice Template from Database
tierno65ca36d2019-02-12 19:27:52 +01001880 _filter = self._get_project_filter(session)
tiernoea97c042019-09-13 09:44:42 +00001881 _filter["_admin.nst-id"] = _id
tiernob4844ab2019-05-23 08:42:12 +00001882 if self.db.get_list("nsis", _filter):
garciadeblas4568a372021-03-24 09:19:48 +01001883 raise EngineException(
1884 "there is at least one Netslice Instance using this descriptor",
1885 http_code=HTTPStatus.CONFLICT,
1886 )
Felipe Vicensb57758d2018-10-16 16:00:20 +02001887
Frank Bryden19b97522020-07-10 12:32:02 +00001888 def sol005_projection(self, data):
1889 data["onboardingState"] = data["_admin"]["onboardingState"]
1890 data["operationalState"] = data["_admin"]["operationalState"]
1891 data["usageState"] = data["_admin"]["usageState"]
1892
1893 links = {}
1894 links["self"] = {"href": "/nst/v1/netslice_templates/{}".format(data["_id"])}
1895 links["nst"] = {"href": "/nst/v1/netslice_templates/{}/nst".format(data["_id"])}
1896 data["_links"] = links
1897
1898 return super().sol005_projection(data)
1899
Felipe Vicensb57758d2018-10-16 16:00:20 +02001900
tiernob24258a2018-10-04 18:39:49 +02001901class PduTopic(BaseTopic):
1902 topic = "pdus"
1903 topic_msg = "pdu"
tierno6b02b052020-06-02 10:07:41 +00001904 quota_name = "pduds"
tiernob24258a2018-10-04 18:39:49 +02001905 schema_new = pdu_new_schema
1906 schema_edit = pdu_edit_schema
1907
delacruzramo32bab472019-09-13 12:24:22 +02001908 def __init__(self, db, fs, msg, auth):
1909 BaseTopic.__init__(self, db, fs, msg, auth)
tiernob24258a2018-10-04 18:39:49 +02001910
1911 @staticmethod
1912 def format_on_new(content, project_id=None, make_public=False):
tierno36ec8602018-11-02 17:27:11 +01001913 BaseTopic.format_on_new(content, project_id=project_id, make_public=make_public)
tiernob24258a2018-10-04 18:39:49 +02001914 content["_admin"]["onboardingState"] = "CREATED"
tierno36ec8602018-11-02 17:27:11 +01001915 content["_admin"]["operationalState"] = "ENABLED"
1916 content["_admin"]["usageState"] = "NOT_IN_USE"
tiernob24258a2018-10-04 18:39:49 +02001917
tiernob4844ab2019-05-23 08:42:12 +00001918 def check_conflict_on_del(self, session, _id, db_content):
1919 """
1920 Check that there is not any vnfr that uses this PDU
1921 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1922 :param _id: pdu internal id
1923 :param db_content: The database content of the _id.
1924 :return: None or raises EngineException with the conflict
1925 """
tierno65ca36d2019-02-12 19:27:52 +01001926 if session["force"]:
tiernob24258a2018-10-04 18:39:49 +02001927 return
tiernob4844ab2019-05-23 08:42:12 +00001928
1929 _filter = self._get_project_filter(session)
1930 _filter["vdur.pdu-id"] = _id
tiernob24258a2018-10-04 18:39:49 +02001931 if self.db.get_list("vnfrs", _filter):
garciadeblas4568a372021-03-24 09:19:48 +01001932 raise EngineException(
1933 "There is at least one VNF instance using this PDU",
1934 http_code=HTTPStatus.CONFLICT,
1935 )
delacruzramo271d2002019-12-02 21:00:37 +01001936
1937
1938class VnfPkgOpTopic(BaseTopic):
1939 topic = "vnfpkgops"
1940 topic_msg = "vnfd"
1941 schema_new = vnfpkgop_new_schema
1942 schema_edit = None
1943
1944 def __init__(self, db, fs, msg, auth):
1945 BaseTopic.__init__(self, db, fs, msg, auth)
1946
1947 def edit(self, session, _id, indata=None, kwargs=None, content=None):
garciadeblas4568a372021-03-24 09:19:48 +01001948 raise EngineException(
1949 "Method 'edit' not allowed for topic '{}'".format(self.topic),
1950 HTTPStatus.METHOD_NOT_ALLOWED,
1951 )
delacruzramo271d2002019-12-02 21:00:37 +01001952
1953 def delete(self, session, _id, dry_run=False):
garciadeblas4568a372021-03-24 09:19:48 +01001954 raise EngineException(
1955 "Method 'delete' not allowed for topic '{}'".format(self.topic),
1956 HTTPStatus.METHOD_NOT_ALLOWED,
1957 )
delacruzramo271d2002019-12-02 21:00:37 +01001958
1959 def delete_list(self, session, filter_q=None):
garciadeblas4568a372021-03-24 09:19:48 +01001960 raise EngineException(
1961 "Method 'delete_list' not allowed for topic '{}'".format(self.topic),
1962 HTTPStatus.METHOD_NOT_ALLOWED,
1963 )
delacruzramo271d2002019-12-02 21:00:37 +01001964
1965 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
1966 """
1967 Creates a new entry into database.
1968 :param rollback: list to append created items at database in case a rollback may to be done
1969 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1970 :param indata: data to be inserted
1971 :param kwargs: used to override the indata descriptor
1972 :param headers: http request headers
1973 :return: _id, op_id:
1974 _id: identity of the inserted data.
1975 op_id: None
1976 """
1977 self._update_input_with_kwargs(indata, kwargs)
1978 validate_input(indata, self.schema_new)
1979 vnfpkg_id = indata["vnfPkgId"]
1980 filter_q = BaseTopic._get_project_filter(session)
1981 filter_q["_id"] = vnfpkg_id
1982 vnfd = self.db.get_one("vnfds", filter_q)
1983 operation = indata["lcmOperationType"]
1984 kdu_name = indata["kdu_name"]
1985 for kdu in vnfd.get("kdu", []):
1986 if kdu["name"] == kdu_name:
1987 helm_chart = kdu.get("helm-chart")
1988 juju_bundle = kdu.get("juju-bundle")
1989 break
1990 else:
garciadeblas4568a372021-03-24 09:19:48 +01001991 raise EngineException(
1992 "Not found vnfd[id='{}']:kdu[name='{}']".format(vnfpkg_id, kdu_name)
1993 )
delacruzramo271d2002019-12-02 21:00:37 +01001994 if helm_chart:
1995 indata["helm-chart"] = helm_chart
1996 match = fullmatch(r"([^/]*)/([^/]*)", helm_chart)
1997 repo_name = match.group(1) if match else None
1998 elif juju_bundle:
1999 indata["juju-bundle"] = juju_bundle
2000 match = fullmatch(r"([^/]*)/([^/]*)", juju_bundle)
2001 repo_name = match.group(1) if match else None
2002 else:
garciadeblas4568a372021-03-24 09:19:48 +01002003 raise EngineException(
2004 "Found neither 'helm-chart' nor 'juju-bundle' in vnfd[id='{}']:kdu[name='{}']".format(
2005 vnfpkg_id, kdu_name
2006 )
2007 )
delacruzramo271d2002019-12-02 21:00:37 +01002008 if repo_name:
2009 del filter_q["_id"]
2010 filter_q["name"] = repo_name
2011 repo = self.db.get_one("k8srepos", filter_q)
2012 k8srepo_id = repo.get("_id")
2013 k8srepo_url = repo.get("url")
2014 else:
2015 k8srepo_id = None
2016 k8srepo_url = None
2017 indata["k8srepoId"] = k8srepo_id
2018 indata["k8srepo_url"] = k8srepo_url
2019 vnfpkgop_id = str(uuid4())
2020 vnfpkgop_desc = {
2021 "_id": vnfpkgop_id,
2022 "operationState": "PROCESSING",
2023 "vnfPkgId": vnfpkg_id,
2024 "lcmOperationType": operation,
2025 "isAutomaticInvocation": False,
2026 "isCancelPending": False,
2027 "operationParams": indata,
2028 "links": {
2029 "self": "/osm/vnfpkgm/v1/vnfpkg_op_occs/" + vnfpkgop_id,
2030 "vnfpkg": "/osm/vnfpkgm/v1/vnf_packages/" + vnfpkg_id,
garciadeblas4568a372021-03-24 09:19:48 +01002031 },
delacruzramo271d2002019-12-02 21:00:37 +01002032 }
garciadeblas4568a372021-03-24 09:19:48 +01002033 self.format_on_new(
2034 vnfpkgop_desc, session["project_id"], make_public=session["public"]
2035 )
delacruzramo271d2002019-12-02 21:00:37 +01002036 ctime = vnfpkgop_desc["_admin"]["created"]
2037 vnfpkgop_desc["statusEnteredTime"] = ctime
2038 vnfpkgop_desc["startTime"] = ctime
2039 self.db.create(self.topic, vnfpkgop_desc)
2040 rollback.append({"topic": self.topic, "_id": vnfpkgop_id})
2041 self.msg.write(self.topic_msg, operation, vnfpkgop_desc)
2042 return vnfpkgop_id, None