blob: 5b5818c9cb714b18fb0105a3762ef7ddd7abd4e0 [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 logging
17from uuid import uuid4
18from http import HTTPStatus
19from time import time
aticig2b5e1232022-08-10 17:30:12 +030020from osm_common.dbbase import deep_update_rfc7396, DbException
tierno23acf402019-08-28 13:36:34 +000021from osm_nbi.validation import validate_input, ValidationError, is_valid_uuid
tierno1c38f2f2020-03-24 11:51:39 +000022from yaml import safe_load, YAMLError
tiernob24258a2018-10-04 18:39:49 +020023
24__author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
25
26
27class EngineException(Exception):
tiernob24258a2018-10-04 18:39:49 +020028 def __init__(self, message, http_code=HTTPStatus.BAD_REQUEST):
29 self.http_code = http_code
tierno23acf402019-08-28 13:36:34 +000030 super(Exception, self).__init__(message)
tiernob24258a2018-10-04 18:39:49 +020031
garciadeblasf2af4a12023-01-24 16:56:54 +010032
aticig2b5e1232022-08-10 17:30:12 +030033class NBIBadArgumentsException(Exception):
34 """
35 Bad argument values exception
36 """
37
38 def __init__(self, message: str = "", bad_args: list = None):
39 Exception.__init__(self, message)
40 self.message = message
41 self.bad_args = bad_args
42
43 def __str__(self):
garciadeblasf2af4a12023-01-24 16:56:54 +010044 return "{}, Bad arguments: {}".format(self.message, self.bad_args)
45
tiernob24258a2018-10-04 18:39:49 +020046
tierno714954e2019-11-29 13:43:26 +000047def deep_get(target_dict, key_list):
48 """
49 Get a value from target_dict entering in the nested keys. If keys does not exist, it returns None
50 Example target_dict={a: {b: 5}}; key_list=[a,b] returns 5; both key_list=[a,b,c] and key_list=[f,h] return None
51 :param target_dict: dictionary to be read
52 :param key_list: list of keys to read from target_dict
53 :return: The wanted value if exist, None otherwise
54 """
55 for key in key_list:
56 if not isinstance(target_dict, dict) or key not in target_dict:
57 return None
58 target_dict = target_dict[key]
59 return target_dict
60
61
garciadeblasf2af4a12023-01-24 16:56:54 +010062def detect_descriptor_usage(descriptor: dict, db_collection: str, db: object) -> bool:
aticig2b5e1232022-08-10 17:30:12 +030063 """Detect the descriptor usage state.
64
65 Args:
66 descriptor (dict): VNF or NS Descriptor as dictionary
67 db_collection (str): collection name which is looked for in DB
68 db (object): name of db object
69
70 Returns:
71 True if descriptor is in use else None
72
73 """
74 try:
75 if not descriptor:
76 raise NBIBadArgumentsException(
77 "Argument is mandatory and can not be empty", "descriptor"
78 )
79
80 if not db:
81 raise NBIBadArgumentsException("A valid DB object should be provided", "db")
82
83 search_dict = {
84 "vnfds": ("vnfrs", "vnfd-id"),
85 "nsds": ("nsrs", "nsd-id"),
kayal2001f71c2e82024-06-25 15:26:24 +053086 "ns_config_template": ("ns_config_template", "_id"),
aticig2b5e1232022-08-10 17:30:12 +030087 }
88
89 if db_collection not in search_dict:
garciadeblasf2af4a12023-01-24 16:56:54 +010090 raise NBIBadArgumentsException(
91 "db_collection should be equal to vnfds or nsds", "db_collection"
92 )
aticig2b5e1232022-08-10 17:30:12 +030093
94 record_list = db.get_list(
95 search_dict[db_collection][0],
96 {search_dict[db_collection][1]: descriptor["_id"]},
97 )
98
99 if record_list:
100 return True
101
102 except (DbException, KeyError, NBIBadArgumentsException) as error:
garciadeblasf2af4a12023-01-24 16:56:54 +0100103 raise EngineException(
104 f"Error occured while detecting the descriptor usage: {error}"
105 )
aticig2b5e1232022-08-10 17:30:12 +0300106
107
108def update_descriptor_usage_state(
109 descriptor: dict, db_collection: str, db: object
110) -> None:
111 """Updates the descriptor usage state.
112
113 Args:
114 descriptor (dict): VNF or NS Descriptor as dictionary
115 db_collection (str): collection name which is looked for in DB
116 db (object): name of db object
117
118 Returns:
119 None
120
121 """
122 try:
123 descriptor_update = {
124 "_admin.usageState": "NOT_IN_USE",
125 }
126
127 if detect_descriptor_usage(descriptor, db_collection, db):
128 descriptor_update = {
129 "_admin.usageState": "IN_USE",
130 }
131
garciadeblasf2af4a12023-01-24 16:56:54 +0100132 db.set_one(
133 db_collection, {"_id": descriptor["_id"]}, update_dict=descriptor_update
134 )
aticig2b5e1232022-08-10 17:30:12 +0300135
136 except (DbException, KeyError, NBIBadArgumentsException) as error:
garciadeblasf2af4a12023-01-24 16:56:54 +0100137 raise EngineException(
138 f"Error occured while updating the descriptor usage state: {error}"
139 )
aticig2b5e1232022-08-10 17:30:12 +0300140
141
tiernob24258a2018-10-04 18:39:49 +0200142def get_iterable(input_var):
143 """
144 Returns an iterable, in case input_var is None it just returns an empty tuple
145 :param input_var: can be a list, tuple or None
146 :return: input_var or () if it is None
147 """
148 if input_var is None:
149 return ()
150 return input_var
151
152
153def versiontuple(v):
154 """utility for compare dot separate versions. Fills with zeros to proper number comparison"""
155 filled = []
156 for point in v.split("."):
157 filled.append(point.zfill(8))
158 return tuple(filled)
159
160
tiernocddb07d2020-10-06 08:28:00 +0000161def increment_ip_mac(ip_mac, vm_index=1):
162 if not isinstance(ip_mac, str):
163 return ip_mac
164 try:
165 # try with ipv4 look for last dot
166 i = ip_mac.rfind(".")
167 if i > 0:
168 i += 1
169 return "{}{}".format(ip_mac[:i], int(ip_mac[i:]) + vm_index)
170 # try with ipv6 or mac look for last colon. Operate in hex
171 i = ip_mac.rfind(":")
172 if i > 0:
173 i += 1
174 # format in hex, len can be 2 for mac or 4 for ipv6
garciadeblas4568a372021-03-24 09:19:48 +0100175 return ("{}{:0" + str(len(ip_mac) - i) + "x}").format(
176 ip_mac[:i], int(ip_mac[i:], 16) + vm_index
177 )
tiernocddb07d2020-10-06 08:28:00 +0000178 except Exception:
179 pass
180 return None
181
182
tiernob24258a2018-10-04 18:39:49 +0200183class BaseTopic:
184 # static variables for all instance classes
garciadeblas4568a372021-03-24 09:19:48 +0100185 topic = None # to_override
186 topic_msg = None # to_override
187 quota_name = None # to_override. If not provided topic will be used for quota_name
188 schema_new = None # to_override
tiernob24258a2018-10-04 18:39:49 +0200189 schema_edit = None # to_override
tierno65ca36d2019-02-12 19:27:52 +0100190 multiproject = True # True if this Topic can be shared by several projects. Then it contains _admin.projects_read
tiernob24258a2018-10-04 18:39:49 +0200191
delacruzramo32bab472019-09-13 12:24:22 +0200192 default_quota = 500
193
delacruzramoc061f562019-04-05 11:00:02 +0200194 # Alternative ID Fields for some Topics
garciadeblas4568a372021-03-24 09:19:48 +0100195 alt_id_field = {"projects": "name", "users": "username", "roles": "name"}
delacruzramoc061f562019-04-05 11:00:02 +0200196
delacruzramo32bab472019-09-13 12:24:22 +0200197 def __init__(self, db, fs, msg, auth):
tiernob24258a2018-10-04 18:39:49 +0200198 self.db = db
199 self.fs = fs
200 self.msg = msg
201 self.logger = logging.getLogger("nbi.engine")
delacruzramo32bab472019-09-13 12:24:22 +0200202 self.auth = auth
tiernob24258a2018-10-04 18:39:49 +0200203
204 @staticmethod
delacruzramoc061f562019-04-05 11:00:02 +0200205 def id_field(topic, value):
tierno65ca36d2019-02-12 19:27:52 +0100206 """Returns ID Field for given topic and field value"""
delacruzramoceb8baf2019-06-21 14:25:38 +0200207 if topic in BaseTopic.alt_id_field.keys() and not is_valid_uuid(value):
delacruzramoc061f562019-04-05 11:00:02 +0200208 return BaseTopic.alt_id_field[topic]
209 else:
210 return "_id"
211
212 @staticmethod
tiernob24258a2018-10-04 18:39:49 +0200213 def _remove_envelop(indata=None):
214 if not indata:
215 return {}
216 return indata
217
delacruzramo32bab472019-09-13 12:24:22 +0200218 def check_quota(self, session):
219 """
220 Check whether topic quota is exceeded by the given project
221 Used by relevant topics' 'new' function to decide whether or not creation of the new item should be allowed
tierno6b02b052020-06-02 10:07:41 +0000222 :param session[project_id]: projects (tuple) for which quota should be checked
223 :param session[force]: boolean. If true, skip quota checking
delacruzramo32bab472019-09-13 12:24:22 +0200224 :return: None
225 :raise:
226 DbException if project not found
tierno6b02b052020-06-02 10:07:41 +0000227 ValidationError if quota exceeded in one of the projects
delacruzramo32bab472019-09-13 12:24:22 +0200228 """
tiernod7749582020-05-28 10:41:10 +0000229 if session["force"]:
delacruzramo32bab472019-09-13 12:24:22 +0200230 return
231 projects = session["project_id"]
232 for project in projects:
233 proj = self.auth.get_project(project)
234 pid = proj["_id"]
tierno6b02b052020-06-02 10:07:41 +0000235 quota_name = self.quota_name or self.topic
236 quota = proj.get("quotas", {}).get(quota_name, self.default_quota)
delacruzramo32bab472019-09-13 12:24:22 +0200237 count = self.db.count(self.topic, {"_admin.projects_read": pid})
238 if count >= quota:
239 name = proj["name"]
garciadeblas4568a372021-03-24 09:19:48 +0100240 raise ValidationError(
241 "quota ({}={}) exceeded for project {} ({})".format(
242 quota_name, quota, name, pid
243 ),
244 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
245 )
delacruzramo32bab472019-09-13 12:24:22 +0200246
tiernob24258a2018-10-04 18:39:49 +0200247 def _validate_input_new(self, input, force=False):
248 """
249 Validates input user content for a new entry. It uses jsonschema. Some overrides will use pyangbind
250 :param input: user input content for the new topic
251 :param force: may be used for being more tolerant
252 :return: The same input content, or a changed version of it.
253 """
254 if self.schema_new:
255 validate_input(input, self.schema_new)
256 return input
257
Frank Brydendeba68e2020-07-27 13:55:11 +0000258 def _validate_input_edit(self, input, content, force=False):
tiernob24258a2018-10-04 18:39:49 +0200259 """
260 Validates input user content for an edition. It uses jsonschema. Some overrides will use pyangbind
261 :param input: user input content for the new topic
262 :param force: may be used for being more tolerant
263 :return: The same input content, or a changed version of it.
264 """
265 if self.schema_edit:
266 validate_input(input, self.schema_edit)
267 return input
268
269 @staticmethod
tierno65ca36d2019-02-12 19:27:52 +0100270 def _get_project_filter(session):
tiernob24258a2018-10-04 18:39:49 +0200271 """
272 Generates a filter dictionary for querying database, so that only allowed items for this project can be
tiernof5f2e3f2020-03-23 14:42:10 +0000273 addressed. Only proprietary or public can be used. Allowed projects are at _admin.project_read/write. If it is
tiernob24258a2018-10-04 18:39:49 +0200274 not present or contains ANY mean public.
tierno65ca36d2019-02-12 19:27:52 +0100275 :param session: contains:
276 project_id: project list this session has rights to access. Can be empty, one or several
garciadeblas4568a372021-03-24 09:19:48 +0100277 set_project: items created will contain this project list
tierno65ca36d2019-02-12 19:27:52 +0100278 force: True or False
279 public: True, False or None
280 method: "list", "show", "write", "delete"
281 admin: True or False
282 :return: dictionary with project filter
tiernob24258a2018-10-04 18:39:49 +0200283 """
tierno65ca36d2019-02-12 19:27:52 +0100284 p_filter = {}
285 project_filter_n = []
286 project_filter = list(session["project_id"])
tiernob24258a2018-10-04 18:39:49 +0200287
tierno65ca36d2019-02-12 19:27:52 +0100288 if session["method"] not in ("list", "delete"):
289 if project_filter:
290 project_filter.append("ANY")
291 elif session["public"] is not None:
292 if session["public"]:
293 project_filter.append("ANY")
294 else:
295 project_filter_n.append("ANY")
296
297 if session.get("PROJECT.ne"):
298 project_filter_n.append(session["PROJECT.ne"])
299
300 if project_filter:
garciadeblas4568a372021-03-24 09:19:48 +0100301 if session["method"] in ("list", "show", "delete") or session.get(
302 "set_project"
303 ):
tierno65ca36d2019-02-12 19:27:52 +0100304 p_filter["_admin.projects_read.cont"] = project_filter
305 else:
306 p_filter["_admin.projects_write.cont"] = project_filter
307 if project_filter_n:
garciadeblas4568a372021-03-24 09:19:48 +0100308 if session["method"] in ("list", "show", "delete") or session.get(
309 "set_project"
310 ):
tierno65ca36d2019-02-12 19:27:52 +0100311 p_filter["_admin.projects_read.ncont"] = project_filter_n
312 else:
313 p_filter["_admin.projects_write.ncont"] = project_filter_n
314
315 return p_filter
316
317 def check_conflict_on_new(self, session, indata):
tiernob24258a2018-10-04 18:39:49 +0200318 """
319 Check that the data to be inserted is valid
tierno65ca36d2019-02-12 19:27:52 +0100320 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200321 :param indata: data to be inserted
tiernob24258a2018-10-04 18:39:49 +0200322 :return: None or raises EngineException
323 """
324 pass
325
tierno65ca36d2019-02-12 19:27:52 +0100326 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
tiernob24258a2018-10-04 18:39:49 +0200327 """
328 Check that the data to be edited/uploaded is valid
tierno65ca36d2019-02-12 19:27:52 +0100329 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernobdebce92019-07-01 15:36:49 +0000330 :param final_content: data once modified. This method may change it.
tiernob24258a2018-10-04 18:39:49 +0200331 :param edit_content: incremental data that contains the modifications to apply
332 :param _id: internal _id
bravofb995ea22021-02-10 10:57:52 -0300333 :return: final_content or raises EngineException
tiernob24258a2018-10-04 18:39:49 +0200334 """
tierno65ca36d2019-02-12 19:27:52 +0100335 if not self.multiproject:
bravofb995ea22021-02-10 10:57:52 -0300336 return final_content
tierno65ca36d2019-02-12 19:27:52 +0100337 # Change public status
338 if session["public"] is not None:
garciadeblas4568a372021-03-24 09:19:48 +0100339 if (
340 session["public"]
341 and "ANY" not in final_content["_admin"]["projects_read"]
342 ):
tierno65ca36d2019-02-12 19:27:52 +0100343 final_content["_admin"]["projects_read"].append("ANY")
344 final_content["_admin"]["projects_write"].clear()
garciadeblas4568a372021-03-24 09:19:48 +0100345 if (
346 not session["public"]
347 and "ANY" in final_content["_admin"]["projects_read"]
348 ):
tierno65ca36d2019-02-12 19:27:52 +0100349 final_content["_admin"]["projects_read"].remove("ANY")
350
351 # Change project status
352 if session.get("set_project"):
353 for p in session["set_project"]:
354 if p not in final_content["_admin"]["projects_read"]:
355 final_content["_admin"]["projects_read"].append(p)
tiernob24258a2018-10-04 18:39:49 +0200356
bravofb995ea22021-02-10 10:57:52 -0300357 return final_content
358
tiernob24258a2018-10-04 18:39:49 +0200359 def check_unique_name(self, session, name, _id=None):
360 """
361 Check that the name is unique for this project
tierno65ca36d2019-02-12 19:27:52 +0100362 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200363 :param name: name to be checked
364 :param _id: If not None, ignore this entry that are going to change
365 :return: None or raises EngineException
366 """
tierno1f029d82019-06-13 22:37:04 +0000367 if not self.multiproject:
368 _filter = {}
369 else:
370 _filter = self._get_project_filter(session)
tiernob24258a2018-10-04 18:39:49 +0200371 _filter["name"] = name
372 if _id:
373 _filter["_id.neq"] = _id
garciadeblas4568a372021-03-24 09:19:48 +0100374 if self.db.get_one(
375 self.topic, _filter, fail_on_empty=False, fail_on_more=False
376 ):
377 raise EngineException(
378 "name '{}' already exists for {}".format(name, self.topic),
379 HTTPStatus.CONFLICT,
380 )
tiernob24258a2018-10-04 18:39:49 +0200381
382 @staticmethod
383 def format_on_new(content, project_id=None, make_public=False):
384 """
385 Modifies content descriptor to include _admin
386 :param content: descriptor to be modified
tierno65ca36d2019-02-12 19:27:52 +0100387 :param project_id: if included, it add project read/write permissions. Can be None or a list
tiernob24258a2018-10-04 18:39:49 +0200388 :param make_public: if included it is generated as public for reading.
tiernobdebce92019-07-01 15:36:49 +0000389 :return: op_id: operation id on asynchronous operation, None otherwise. In addition content is modified
tiernob24258a2018-10-04 18:39:49 +0200390 """
391 now = time()
392 if "_admin" not in content:
393 content["_admin"] = {}
394 if not content["_admin"].get("created"):
395 content["_admin"]["created"] = now
396 content["_admin"]["modified"] = now
397 if not content.get("_id"):
398 content["_id"] = str(uuid4())
tierno65ca36d2019-02-12 19:27:52 +0100399 if project_id is not None:
tiernob24258a2018-10-04 18:39:49 +0200400 if not content["_admin"].get("projects_read"):
tierno65ca36d2019-02-12 19:27:52 +0100401 content["_admin"]["projects_read"] = list(project_id)
tiernob24258a2018-10-04 18:39:49 +0200402 if make_public:
403 content["_admin"]["projects_read"].append("ANY")
404 if not content["_admin"].get("projects_write"):
tierno65ca36d2019-02-12 19:27:52 +0100405 content["_admin"]["projects_write"] = list(project_id)
tiernobdebce92019-07-01 15:36:49 +0000406 return None
tiernob24258a2018-10-04 18:39:49 +0200407
408 @staticmethod
409 def format_on_edit(final_content, edit_content):
tiernobdebce92019-07-01 15:36:49 +0000410 """
411 Modifies final_content to admin information upon edition
412 :param final_content: final content to be stored at database
413 :param edit_content: user requested update content
414 :return: operation id, if this edit implies an asynchronous operation; None otherwise
415 """
tiernob24258a2018-10-04 18:39:49 +0200416 if final_content.get("_admin"):
417 now = time()
418 final_content["_admin"]["modified"] = now
tiernobdebce92019-07-01 15:36:49 +0000419 return None
tiernob24258a2018-10-04 18:39:49 +0200420
tiernobee3bad2019-12-05 12:26:01 +0000421 def _send_msg(self, action, content, not_send_msg=None):
422 if self.topic_msg and not_send_msg is not False:
agarwalat53471982020-10-08 13:06:14 +0000423 content = content.copy()
tiernob24258a2018-10-04 18:39:49 +0200424 content.pop("_admin", None)
tiernobee3bad2019-12-05 12:26:01 +0000425 if isinstance(not_send_msg, list):
426 not_send_msg.append((self.topic_msg, action, content))
427 else:
428 self.msg.write(self.topic_msg, action, content)
tiernob24258a2018-10-04 18:39:49 +0200429
tiernob4844ab2019-05-23 08:42:12 +0000430 def check_conflict_on_del(self, session, _id, db_content):
tiernob24258a2018-10-04 18:39:49 +0200431 """
432 Check if deletion can be done because of dependencies if it is not force. To override
tierno65ca36d2019-02-12 19:27:52 +0100433 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
434 :param _id: internal _id
tiernob4844ab2019-05-23 08:42:12 +0000435 :param db_content: The database content of this item _id
tiernob24258a2018-10-04 18:39:49 +0200436 :return: None if ok or raises EngineException with the conflict
437 """
438 pass
439
440 @staticmethod
tierno1c38f2f2020-03-24 11:51:39 +0000441 def _update_input_with_kwargs(desc, kwargs, yaml_format=False):
tiernob24258a2018-10-04 18:39:49 +0200442 """
443 Update descriptor with the kwargs. It contains dot separated keys
444 :param desc: dictionary to be updated
445 :param kwargs: plain dictionary to be used for updating.
tierno1c38f2f2020-03-24 11:51:39 +0000446 :param yaml_format: get kwargs values as yaml format.
delacruzramoc061f562019-04-05 11:00:02 +0200447 :return: None, 'desc' is modified. It raises EngineException.
tiernob24258a2018-10-04 18:39:49 +0200448 """
449 if not kwargs:
450 return
451 try:
452 for k, v in kwargs.items():
453 update_content = desc
454 kitem_old = None
455 klist = k.split(".")
456 for kitem in klist:
457 if kitem_old is not None:
458 update_content = update_content[kitem_old]
459 if isinstance(update_content, dict):
460 kitem_old = kitem
tiernoac55f062020-06-17 07:42:30 +0000461 if not isinstance(update_content.get(kitem_old), (dict, list)):
462 update_content[kitem_old] = {}
tiernob24258a2018-10-04 18:39:49 +0200463 elif isinstance(update_content, list):
tiernoac55f062020-06-17 07:42:30 +0000464 # key must be an index of the list, must be integer
tiernob24258a2018-10-04 18:39:49 +0200465 kitem_old = int(kitem)
tiernoac55f062020-06-17 07:42:30 +0000466 # if index greater than list, extend the list
467 if kitem_old >= len(update_content):
garciadeblas4568a372021-03-24 09:19:48 +0100468 update_content += [None] * (
469 kitem_old - len(update_content) + 1
470 )
tiernoac55f062020-06-17 07:42:30 +0000471 if not isinstance(update_content[kitem_old], (dict, list)):
472 update_content[kitem_old] = {}
tiernob24258a2018-10-04 18:39:49 +0200473 else:
474 raise EngineException(
garciadeblas4568a372021-03-24 09:19:48 +0100475 "Invalid query string '{}'. Descriptor is not a list nor dict at '{}'".format(
476 k, kitem
477 )
478 )
tiernoac55f062020-06-17 07:42:30 +0000479 if v is None:
480 del update_content[kitem_old]
481 else:
482 update_content[kitem_old] = v if not yaml_format else safe_load(v)
tiernob24258a2018-10-04 18:39:49 +0200483 except KeyError:
484 raise EngineException(
garciadeblas4568a372021-03-24 09:19:48 +0100485 "Invalid query string '{}'. Descriptor does not contain '{}'".format(
486 k, kitem_old
487 )
488 )
tiernob24258a2018-10-04 18:39:49 +0200489 except ValueError:
garciadeblas4568a372021-03-24 09:19:48 +0100490 raise EngineException(
491 "Invalid query string '{}'. Expected integer index list instead of '{}'".format(
492 k, kitem
493 )
494 )
tiernob24258a2018-10-04 18:39:49 +0200495 except IndexError:
496 raise EngineException(
garciadeblas4568a372021-03-24 09:19:48 +0100497 "Invalid query string '{}'. Index '{}' out of range".format(
498 k, kitem_old
499 )
500 )
tierno1c38f2f2020-03-24 11:51:39 +0000501 except YAMLError:
502 raise EngineException("Invalid query string '{}' yaml format".format(k))
tiernob24258a2018-10-04 18:39:49 +0200503
Frank Bryden19b97522020-07-10 12:32:02 +0000504 def sol005_projection(self, data):
505 # Projection was moved to child classes
506 return data
507
K Sai Kiran57589552021-01-27 21:38:34 +0530508 def show(self, session, _id, filter_q=None, api_req=False):
tiernob24258a2018-10-04 18:39:49 +0200509 """
510 Get complete information on an topic
tierno65ca36d2019-02-12 19:27:52 +0100511 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200512 :param _id: server internal id
K Sai Kiran57589552021-01-27 21:38:34 +0530513 :param filter_q: dict: query parameter
Frank Bryden19b97522020-07-10 12:32:02 +0000514 :param api_req: True if this call is serving an external API request. False if serving internal request.
tiernob24258a2018-10-04 18:39:49 +0200515 :return: dictionary, raise exception if not found.
516 """
tierno1f029d82019-06-13 22:37:04 +0000517 if not self.multiproject:
518 filter_db = {}
519 else:
520 filter_db = self._get_project_filter(session)
delacruzramoc061f562019-04-05 11:00:02 +0200521 # To allow project&user addressing by name AS WELL AS _id
522 filter_db[BaseTopic.id_field(self.topic, _id)] = _id
Frank Bryden19b97522020-07-10 12:32:02 +0000523 data = self.db.get_one(self.topic, filter_db)
524
525 # Only perform SOL005 projection if we are serving an external request
526 if api_req:
527 self.sol005_projection(data)
528
529 return data
garciadeblas4568a372021-03-24 09:19:48 +0100530
tiernob24258a2018-10-04 18:39:49 +0200531 # TODO transform data for SOL005 URL requests
532 # TODO remove _admin if not admin
533
534 def get_file(self, session, _id, path=None, accept_header=None):
535 """
536 Only implemented for descriptor topics. Return the file content of a descriptor
tierno65ca36d2019-02-12 19:27:52 +0100537 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200538 :param _id: Identity of the item to get content
539 :param path: artifact path or "$DESCRIPTOR" or None
540 :param accept_header: Content of Accept header. Must contain applition/zip or/and text/plain
541 :return: opened file or raises an exception
542 """
garciadeblas4568a372021-03-24 09:19:48 +0100543 raise EngineException(
544 "Method get_file not valid for this topic", HTTPStatus.INTERNAL_SERVER_ERROR
545 )
tiernob24258a2018-10-04 18:39:49 +0200546
Frank Bryden19b97522020-07-10 12:32:02 +0000547 def list(self, session, filter_q=None, api_req=False):
tiernob24258a2018-10-04 18:39:49 +0200548 """
549 Get a list of the topic that matches a filter
550 :param session: contains the used login username and working project
551 :param filter_q: filter of data to be applied
Frank Bryden19b97522020-07-10 12:32:02 +0000552 :param api_req: True if this call is serving an external API request. False if serving internal request.
tiernob24258a2018-10-04 18:39:49 +0200553 :return: The list, it can be empty if no one match the filter.
554 """
555 if not filter_q:
556 filter_q = {}
tierno1f029d82019-06-13 22:37:04 +0000557 if self.multiproject:
558 filter_q.update(self._get_project_filter(session))
tiernob24258a2018-10-04 18:39:49 +0200559
560 # TODO transform data for SOL005 URL requests. Transform filtering
561 # TODO implement "field-type" query string SOL005
Frank Bryden19b97522020-07-10 12:32:02 +0000562 data = self.db.get_list(self.topic, filter_q)
563
564 # Only perform SOL005 projection if we are serving an external request
565 if api_req:
566 data = [self.sol005_projection(inst) for inst in data]
garciadeblas4568a372021-03-24 09:19:48 +0100567
Frank Bryden19b97522020-07-10 12:32:02 +0000568 return data
tiernob24258a2018-10-04 18:39:49 +0200569
tierno65ca36d2019-02-12 19:27:52 +0100570 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
tiernob24258a2018-10-04 18:39:49 +0200571 """
572 Creates a new entry into database.
573 :param rollback: list to append created items at database in case a rollback may to be done
tierno65ca36d2019-02-12 19:27:52 +0100574 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200575 :param indata: data to be inserted
576 :param kwargs: used to override the indata descriptor
577 :param headers: http request headers
tiernobdebce92019-07-01 15:36:49 +0000578 :return: _id, op_id:
579 _id: identity of the inserted data.
580 op_id: operation id if this is asynchronous, None otherwise
tiernob24258a2018-10-04 18:39:49 +0200581 """
582 try:
delacruzramo32bab472019-09-13 12:24:22 +0200583 if self.multiproject:
584 self.check_quota(session)
585
tiernob24258a2018-10-04 18:39:49 +0200586 content = self._remove_envelop(indata)
587
588 # Override descriptor with query string kwargs
589 self._update_input_with_kwargs(content, kwargs)
tierno65ca36d2019-02-12 19:27:52 +0100590 content = self._validate_input_new(content, force=session["force"])
591 self.check_conflict_on_new(session, content)
garciadeblas4568a372021-03-24 09:19:48 +0100592 op_id = self.format_on_new(
593 content, project_id=session["project_id"], make_public=session["public"]
594 )
tiernob24258a2018-10-04 18:39:49 +0200595 _id = self.db.create(self.topic, content)
596 rollback.append({"topic": self.topic, "_id": _id})
tiernobdebce92019-07-01 15:36:49 +0000597 if op_id:
598 content["op_id"] = op_id
tierno15a1f682019-10-16 09:00:13 +0000599 self._send_msg("created", content)
tiernobdebce92019-07-01 15:36:49 +0000600 return _id, op_id
tiernob24258a2018-10-04 18:39:49 +0200601 except ValidationError as e:
602 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
603
tierno65ca36d2019-02-12 19:27:52 +0100604 def upload_content(self, session, _id, indata, kwargs, headers):
tiernob24258a2018-10-04 18:39:49 +0200605 """
606 Only implemented for descriptor topics. Used for receiving content by chunks (with a transaction_id header
607 and/or gzip file. It will store and extract)
tierno65ca36d2019-02-12 19:27:52 +0100608 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200609 :param _id : the database id of entry to be updated
610 :param indata: http body request
611 :param kwargs: user query string to override parameters. NOT USED
612 :param headers: http request headers
tiernob24258a2018-10-04 18:39:49 +0200613 :return: True package has is completely uploaded or False if partial content has been uplodaed.
614 Raise exception on error
615 """
garciadeblas4568a372021-03-24 09:19:48 +0100616 raise EngineException(
617 "Method upload_content not valid for this topic",
618 HTTPStatus.INTERNAL_SERVER_ERROR,
619 )
tiernob24258a2018-10-04 18:39:49 +0200620
621 def delete_list(self, session, filter_q=None):
622 """
623 Delete a several entries of a topic. This is for internal usage and test only, not exposed to NBI API
tierno65ca36d2019-02-12 19:27:52 +0100624 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200625 :param filter_q: filter of data to be applied
626 :return: The deleted list, it can be empty if no one match the filter.
627 """
628 # TODO add admin to filter, validate rights
629 if not filter_q:
630 filter_q = {}
tierno1f029d82019-06-13 22:37:04 +0000631 if self.multiproject:
632 filter_q.update(self._get_project_filter(session))
tiernob24258a2018-10-04 18:39:49 +0200633 return self.db.del_list(self.topic, filter_q)
634
tiernobee3bad2019-12-05 12:26:01 +0000635 def delete_extra(self, session, _id, db_content, not_send_msg=None):
tierno65ca36d2019-02-12 19:27:52 +0100636 """
637 Delete other things apart from database entry of a item _id.
638 e.g.: other associated elements at database and other file system storage
639 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
640 :param _id: server internal id
tiernob4844ab2019-05-23 08:42:12 +0000641 :param db_content: The database content of the _id. It is already deleted when reached this method, but the
642 content is needed in same cases
tiernobee3bad2019-12-05 12:26:01 +0000643 :param not_send_msg: To not send message (False) or store content (list) instead
tiernob4844ab2019-05-23 08:42:12 +0000644 :return: None if ok or raises EngineException with the problem
tierno65ca36d2019-02-12 19:27:52 +0100645 """
646 pass
647
tiernobee3bad2019-12-05 12:26:01 +0000648 def delete(self, session, _id, dry_run=False, not_send_msg=None):
tiernob24258a2018-10-04 18:39:49 +0200649 """
650 Delete item by its internal _id
tierno65ca36d2019-02-12 19:27:52 +0100651 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200652 :param _id: server internal id
tiernob24258a2018-10-04 18:39:49 +0200653 :param dry_run: make checking but do not delete
tiernobee3bad2019-12-05 12:26:01 +0000654 :param not_send_msg: To not send message (False) or store content (list) instead
tiernobdebce92019-07-01 15:36:49 +0000655 :return: operation id (None if there is not operation), raise exception if error or not found, conflict, ...
tiernob24258a2018-10-04 18:39:49 +0200656 """
tiernob4844ab2019-05-23 08:42:12 +0000657 # To allow addressing projects and users by name AS WELL AS by _id
tiernof5f2e3f2020-03-23 14:42:10 +0000658 if not self.multiproject:
659 filter_q = {}
660 else:
661 filter_q = self._get_project_filter(session)
662 filter_q[self.id_field(self.topic, _id)] = _id
tiernob4844ab2019-05-23 08:42:12 +0000663 item_content = self.db.get_one(self.topic, filter_q)
kayal2001f71c2e82024-06-25 15:26:24 +0530664 nsd_id = item_content.get("_id")
tiernob4844ab2019-05-23 08:42:12 +0000665
tiernob4844ab2019-05-23 08:42:12 +0000666 self.check_conflict_on_del(session, _id, item_content)
kayal2001f71c2e82024-06-25 15:26:24 +0530667
668 # While deteling ns descriptor associated ns config template should also get deleted.
669 if self.topic == "nsds":
670 ns_config_template_content = self.db.get_list(
671 "ns_config_template", {"nsdId": _id}
672 )
673 for template_content in ns_config_template_content:
674 if template_content is not None:
675 if template_content.get("nsdId") == nsd_id:
676 ns_config_template_id = template_content.get("_id")
677 self.db.del_one("ns_config_template", {"nsdId": nsd_id})
678 self.delete_extra(
679 session,
680 ns_config_template_id,
681 template_content,
682 not_send_msg=not_send_msg,
683 )
tierno65ca36d2019-02-12 19:27:52 +0100684 if dry_run:
685 return None
garciadeblas4568a372021-03-24 09:19:48 +0100686
tierno65ca36d2019-02-12 19:27:52 +0100687 if self.multiproject and session["project_id"]:
tiernof5f2e3f2020-03-23 14:42:10 +0000688 # remove reference from project_read if there are more projects referencing it. If it last one,
689 # do not remove reference, but delete
garciadeblas4568a372021-03-24 09:19:48 +0100690 other_projects_referencing = next(
691 (
692 p
693 for p in item_content["_admin"]["projects_read"]
694 if p not in session["project_id"] and p != "ANY"
695 ),
696 None,
697 )
tiernof5f2e3f2020-03-23 14:42:10 +0000698
699 # check if there are projects referencing it (apart from ANY, that means, public)....
700 if other_projects_referencing:
701 # remove references but not delete
garciadeblas4568a372021-03-24 09:19:48 +0100702 update_dict_pull = {
703 "_admin.projects_read": session["project_id"],
704 "_admin.projects_write": session["project_id"],
705 }
706 self.db.set_one(
707 self.topic, filter_q, update_dict=None, pull_list=update_dict_pull
708 )
tiernobdebce92019-07-01 15:36:49 +0000709 return None
tiernof5f2e3f2020-03-23 14:42:10 +0000710 else:
garciadeblas4568a372021-03-24 09:19:48 +0100711 can_write = next(
712 (
713 p
714 for p in item_content["_admin"]["projects_write"]
715 if p == "ANY" or p in session["project_id"]
716 ),
717 None,
718 )
tiernof5f2e3f2020-03-23 14:42:10 +0000719 if not can_write:
garciadeblas4568a372021-03-24 09:19:48 +0100720 raise EngineException(
721 "You have not write permission to delete it",
722 http_code=HTTPStatus.UNAUTHORIZED,
723 )
tiernof5f2e3f2020-03-23 14:42:10 +0000724
725 # delete
726 self.db.del_one(self.topic, filter_q)
tiernobee3bad2019-12-05 12:26:01 +0000727 self.delete_extra(session, _id, item_content, not_send_msg=not_send_msg)
728 self._send_msg("deleted", {"_id": _id}, not_send_msg=not_send_msg)
tiernobdebce92019-07-01 15:36:49 +0000729 return None
tiernob24258a2018-10-04 18:39:49 +0200730
tierno65ca36d2019-02-12 19:27:52 +0100731 def edit(self, session, _id, indata=None, kwargs=None, content=None):
732 """
733 Change the content of an item
734 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
735 :param _id: server internal id
736 :param indata: contains the changes to apply
737 :param kwargs: modifies indata
738 :param content: original content of the item
tiernobdebce92019-07-01 15:36:49 +0000739 :return: op_id: operation id if this is processed asynchronously, None otherwise
tierno65ca36d2019-02-12 19:27:52 +0100740 """
tiernob24258a2018-10-04 18:39:49 +0200741 indata = self._remove_envelop(indata)
742
743 # Override descriptor with query string kwargs
744 if kwargs:
745 self._update_input_with_kwargs(indata, kwargs)
746 try:
tierno65ca36d2019-02-12 19:27:52 +0100747 if indata and session.get("set_project"):
garciadeblas4568a372021-03-24 09:19:48 +0100748 raise EngineException(
749 "Cannot edit content and set to project (query string SET_PROJECT) at same time",
750 HTTPStatus.UNPROCESSABLE_ENTITY,
751 )
tiernob24258a2018-10-04 18:39:49 +0200752 # TODO self._check_edition(session, indata, _id, force)
753 if not content:
754 content = self.show(session, _id)
Frank Brydendeba68e2020-07-27 13:55:11 +0000755 indata = self._validate_input_edit(indata, content, force=session["force"])
tiernob24258a2018-10-04 18:39:49 +0200756 deep_update_rfc7396(content, indata)
tiernobdebce92019-07-01 15:36:49 +0000757
758 # To allow project addressing by name AS WELL AS _id. Get the _id, just in case the provided one is a name
759 _id = content.get("_id") or _id
760
bravofb995ea22021-02-10 10:57:52 -0300761 content = self.check_conflict_on_edit(session, content, indata, _id=_id)
tiernobdebce92019-07-01 15:36:49 +0000762 op_id = self.format_on_edit(content, indata)
763
764 self.db.replace(self.topic, _id, content)
tiernob24258a2018-10-04 18:39:49 +0200765
766 indata.pop("_admin", None)
tiernobdebce92019-07-01 15:36:49 +0000767 if op_id:
768 indata["op_id"] = op_id
tiernob24258a2018-10-04 18:39:49 +0200769 indata["_id"] = _id
tierno15a1f682019-10-16 09:00:13 +0000770 self._send_msg("edited", indata)
tiernobdebce92019-07-01 15:36:49 +0000771 return op_id
tiernob24258a2018-10-04 18:39:49 +0200772 except ValidationError as e:
773 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)