blob: 591d71a1b97bb0a031b2c3925986a4ddc7e014d7 [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
aticig2b5e1232022-08-10 17:30:12 +030032class NBIBadArgumentsException(Exception):
33 """
34 Bad argument values exception
35 """
36
37 def __init__(self, message: str = "", bad_args: list = None):
38 Exception.__init__(self, message)
39 self.message = message
40 self.bad_args = bad_args
41
42 def __str__(self):
43 return "{}, Bad arguments: {}".format(
44 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
aticig2b5e1232022-08-10 17:30:12 +030062def detect_descriptor_usage(
63 descriptor: dict, db_collection: str, db: object
64) -> bool:
65 """Detect the descriptor usage state.
66
67 Args:
68 descriptor (dict): VNF or NS Descriptor as dictionary
69 db_collection (str): collection name which is looked for in DB
70 db (object): name of db object
71
72 Returns:
73 True if descriptor is in use else None
74
75 """
76 try:
77 if not descriptor:
78 raise NBIBadArgumentsException(
79 "Argument is mandatory and can not be empty", "descriptor"
80 )
81
82 if not db:
83 raise NBIBadArgumentsException("A valid DB object should be provided", "db")
84
85 search_dict = {
86 "vnfds": ("vnfrs", "vnfd-id"),
87 "nsds": ("nsrs", "nsd-id"),
88 }
89
90 if db_collection not in search_dict:
91 raise NBIBadArgumentsException("db_collection should be equal to vnfds or nsds", "db_collection")
92
93 record_list = db.get_list(
94 search_dict[db_collection][0],
95 {search_dict[db_collection][1]: descriptor["_id"]},
96 )
97
98 if record_list:
99 return True
100
101 except (DbException, KeyError, NBIBadArgumentsException) as error:
102 raise EngineException(f"Error occured while detecting the descriptor usage: {error}")
103
104
105def update_descriptor_usage_state(
106 descriptor: dict, db_collection: str, db: object
107) -> None:
108 """Updates the descriptor usage state.
109
110 Args:
111 descriptor (dict): VNF or NS Descriptor as dictionary
112 db_collection (str): collection name which is looked for in DB
113 db (object): name of db object
114
115 Returns:
116 None
117
118 """
119 try:
120 descriptor_update = {
121 "_admin.usageState": "NOT_IN_USE",
122 }
123
124 if detect_descriptor_usage(descriptor, db_collection, db):
125 descriptor_update = {
126 "_admin.usageState": "IN_USE",
127 }
128
129 db.set_one(db_collection, {"_id": descriptor["_id"]}, update_dict=descriptor_update)
130
131 except (DbException, KeyError, NBIBadArgumentsException) as error:
132 raise EngineException(f"Error occured while updating the descriptor usage state: {error}")
133
134
tiernob24258a2018-10-04 18:39:49 +0200135def get_iterable(input_var):
136 """
137 Returns an iterable, in case input_var is None it just returns an empty tuple
138 :param input_var: can be a list, tuple or None
139 :return: input_var or () if it is None
140 """
141 if input_var is None:
142 return ()
143 return input_var
144
145
146def versiontuple(v):
147 """utility for compare dot separate versions. Fills with zeros to proper number comparison"""
148 filled = []
149 for point in v.split("."):
150 filled.append(point.zfill(8))
151 return tuple(filled)
152
153
tiernocddb07d2020-10-06 08:28:00 +0000154def increment_ip_mac(ip_mac, vm_index=1):
155 if not isinstance(ip_mac, str):
156 return ip_mac
157 try:
158 # try with ipv4 look for last dot
159 i = ip_mac.rfind(".")
160 if i > 0:
161 i += 1
162 return "{}{}".format(ip_mac[:i], int(ip_mac[i:]) + vm_index)
163 # try with ipv6 or mac look for last colon. Operate in hex
164 i = ip_mac.rfind(":")
165 if i > 0:
166 i += 1
167 # format in hex, len can be 2 for mac or 4 for ipv6
garciadeblas4568a372021-03-24 09:19:48 +0100168 return ("{}{:0" + str(len(ip_mac) - i) + "x}").format(
169 ip_mac[:i], int(ip_mac[i:], 16) + vm_index
170 )
tiernocddb07d2020-10-06 08:28:00 +0000171 except Exception:
172 pass
173 return None
174
175
tiernob24258a2018-10-04 18:39:49 +0200176class BaseTopic:
177 # static variables for all instance classes
garciadeblas4568a372021-03-24 09:19:48 +0100178 topic = None # to_override
179 topic_msg = None # to_override
180 quota_name = None # to_override. If not provided topic will be used for quota_name
181 schema_new = None # to_override
tiernob24258a2018-10-04 18:39:49 +0200182 schema_edit = None # to_override
tierno65ca36d2019-02-12 19:27:52 +0100183 multiproject = True # True if this Topic can be shared by several projects. Then it contains _admin.projects_read
tiernob24258a2018-10-04 18:39:49 +0200184
delacruzramo32bab472019-09-13 12:24:22 +0200185 default_quota = 500
186
delacruzramoc061f562019-04-05 11:00:02 +0200187 # Alternative ID Fields for some Topics
garciadeblas4568a372021-03-24 09:19:48 +0100188 alt_id_field = {"projects": "name", "users": "username", "roles": "name"}
delacruzramoc061f562019-04-05 11:00:02 +0200189
delacruzramo32bab472019-09-13 12:24:22 +0200190 def __init__(self, db, fs, msg, auth):
tiernob24258a2018-10-04 18:39:49 +0200191 self.db = db
192 self.fs = fs
193 self.msg = msg
194 self.logger = logging.getLogger("nbi.engine")
delacruzramo32bab472019-09-13 12:24:22 +0200195 self.auth = auth
tiernob24258a2018-10-04 18:39:49 +0200196
197 @staticmethod
delacruzramoc061f562019-04-05 11:00:02 +0200198 def id_field(topic, value):
tierno65ca36d2019-02-12 19:27:52 +0100199 """Returns ID Field for given topic and field value"""
delacruzramoceb8baf2019-06-21 14:25:38 +0200200 if topic in BaseTopic.alt_id_field.keys() and not is_valid_uuid(value):
delacruzramoc061f562019-04-05 11:00:02 +0200201 return BaseTopic.alt_id_field[topic]
202 else:
203 return "_id"
204
205 @staticmethod
tiernob24258a2018-10-04 18:39:49 +0200206 def _remove_envelop(indata=None):
207 if not indata:
208 return {}
209 return indata
210
delacruzramo32bab472019-09-13 12:24:22 +0200211 def check_quota(self, session):
212 """
213 Check whether topic quota is exceeded by the given project
214 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 +0000215 :param session[project_id]: projects (tuple) for which quota should be checked
216 :param session[force]: boolean. If true, skip quota checking
delacruzramo32bab472019-09-13 12:24:22 +0200217 :return: None
218 :raise:
219 DbException if project not found
tierno6b02b052020-06-02 10:07:41 +0000220 ValidationError if quota exceeded in one of the projects
delacruzramo32bab472019-09-13 12:24:22 +0200221 """
tiernod7749582020-05-28 10:41:10 +0000222 if session["force"]:
delacruzramo32bab472019-09-13 12:24:22 +0200223 return
224 projects = session["project_id"]
225 for project in projects:
226 proj = self.auth.get_project(project)
227 pid = proj["_id"]
tierno6b02b052020-06-02 10:07:41 +0000228 quota_name = self.quota_name or self.topic
229 quota = proj.get("quotas", {}).get(quota_name, self.default_quota)
delacruzramo32bab472019-09-13 12:24:22 +0200230 count = self.db.count(self.topic, {"_admin.projects_read": pid})
231 if count >= quota:
232 name = proj["name"]
garciadeblas4568a372021-03-24 09:19:48 +0100233 raise ValidationError(
234 "quota ({}={}) exceeded for project {} ({})".format(
235 quota_name, quota, name, pid
236 ),
237 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
238 )
delacruzramo32bab472019-09-13 12:24:22 +0200239
tiernob24258a2018-10-04 18:39:49 +0200240 def _validate_input_new(self, input, force=False):
241 """
242 Validates input user content for a new entry. It uses jsonschema. Some overrides will use pyangbind
243 :param input: user input content for the new topic
244 :param force: may be used for being more tolerant
245 :return: The same input content, or a changed version of it.
246 """
247 if self.schema_new:
248 validate_input(input, self.schema_new)
249 return input
250
Frank Brydendeba68e2020-07-27 13:55:11 +0000251 def _validate_input_edit(self, input, content, force=False):
tiernob24258a2018-10-04 18:39:49 +0200252 """
253 Validates input user content for an edition. It uses jsonschema. Some overrides will use pyangbind
254 :param input: user input content for the new topic
255 :param force: may be used for being more tolerant
256 :return: The same input content, or a changed version of it.
257 """
258 if self.schema_edit:
259 validate_input(input, self.schema_edit)
260 return input
261
262 @staticmethod
tierno65ca36d2019-02-12 19:27:52 +0100263 def _get_project_filter(session):
tiernob24258a2018-10-04 18:39:49 +0200264 """
265 Generates a filter dictionary for querying database, so that only allowed items for this project can be
tiernof5f2e3f2020-03-23 14:42:10 +0000266 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 +0200267 not present or contains ANY mean public.
tierno65ca36d2019-02-12 19:27:52 +0100268 :param session: contains:
269 project_id: project list this session has rights to access. Can be empty, one or several
garciadeblas4568a372021-03-24 09:19:48 +0100270 set_project: items created will contain this project list
tierno65ca36d2019-02-12 19:27:52 +0100271 force: True or False
272 public: True, False or None
273 method: "list", "show", "write", "delete"
274 admin: True or False
275 :return: dictionary with project filter
tiernob24258a2018-10-04 18:39:49 +0200276 """
tierno65ca36d2019-02-12 19:27:52 +0100277 p_filter = {}
278 project_filter_n = []
279 project_filter = list(session["project_id"])
tiernob24258a2018-10-04 18:39:49 +0200280
tierno65ca36d2019-02-12 19:27:52 +0100281 if session["method"] not in ("list", "delete"):
282 if project_filter:
283 project_filter.append("ANY")
284 elif session["public"] is not None:
285 if session["public"]:
286 project_filter.append("ANY")
287 else:
288 project_filter_n.append("ANY")
289
290 if session.get("PROJECT.ne"):
291 project_filter_n.append(session["PROJECT.ne"])
292
293 if project_filter:
garciadeblas4568a372021-03-24 09:19:48 +0100294 if session["method"] in ("list", "show", "delete") or session.get(
295 "set_project"
296 ):
tierno65ca36d2019-02-12 19:27:52 +0100297 p_filter["_admin.projects_read.cont"] = project_filter
298 else:
299 p_filter["_admin.projects_write.cont"] = project_filter
300 if project_filter_n:
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.ncont"] = project_filter_n
305 else:
306 p_filter["_admin.projects_write.ncont"] = project_filter_n
307
308 return p_filter
309
310 def check_conflict_on_new(self, session, indata):
tiernob24258a2018-10-04 18:39:49 +0200311 """
312 Check that the data to be inserted is valid
tierno65ca36d2019-02-12 19:27:52 +0100313 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200314 :param indata: data to be inserted
tiernob24258a2018-10-04 18:39:49 +0200315 :return: None or raises EngineException
316 """
317 pass
318
tierno65ca36d2019-02-12 19:27:52 +0100319 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
tiernob24258a2018-10-04 18:39:49 +0200320 """
321 Check that the data to be edited/uploaded is valid
tierno65ca36d2019-02-12 19:27:52 +0100322 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernobdebce92019-07-01 15:36:49 +0000323 :param final_content: data once modified. This method may change it.
tiernob24258a2018-10-04 18:39:49 +0200324 :param edit_content: incremental data that contains the modifications to apply
325 :param _id: internal _id
bravofb995ea22021-02-10 10:57:52 -0300326 :return: final_content or raises EngineException
tiernob24258a2018-10-04 18:39:49 +0200327 """
tierno65ca36d2019-02-12 19:27:52 +0100328 if not self.multiproject:
bravofb995ea22021-02-10 10:57:52 -0300329 return final_content
tierno65ca36d2019-02-12 19:27:52 +0100330 # Change public status
331 if session["public"] is not None:
garciadeblas4568a372021-03-24 09:19:48 +0100332 if (
333 session["public"]
334 and "ANY" not in final_content["_admin"]["projects_read"]
335 ):
tierno65ca36d2019-02-12 19:27:52 +0100336 final_content["_admin"]["projects_read"].append("ANY")
337 final_content["_admin"]["projects_write"].clear()
garciadeblas4568a372021-03-24 09:19:48 +0100338 if (
339 not session["public"]
340 and "ANY" in final_content["_admin"]["projects_read"]
341 ):
tierno65ca36d2019-02-12 19:27:52 +0100342 final_content["_admin"]["projects_read"].remove("ANY")
343
344 # Change project status
345 if session.get("set_project"):
346 for p in session["set_project"]:
347 if p not in final_content["_admin"]["projects_read"]:
348 final_content["_admin"]["projects_read"].append(p)
tiernob24258a2018-10-04 18:39:49 +0200349
bravofb995ea22021-02-10 10:57:52 -0300350 return final_content
351
tiernob24258a2018-10-04 18:39:49 +0200352 def check_unique_name(self, session, name, _id=None):
353 """
354 Check that the name is unique for this project
tierno65ca36d2019-02-12 19:27:52 +0100355 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200356 :param name: name to be checked
357 :param _id: If not None, ignore this entry that are going to change
358 :return: None or raises EngineException
359 """
tierno1f029d82019-06-13 22:37:04 +0000360 if not self.multiproject:
361 _filter = {}
362 else:
363 _filter = self._get_project_filter(session)
tiernob24258a2018-10-04 18:39:49 +0200364 _filter["name"] = name
365 if _id:
366 _filter["_id.neq"] = _id
garciadeblas4568a372021-03-24 09:19:48 +0100367 if self.db.get_one(
368 self.topic, _filter, fail_on_empty=False, fail_on_more=False
369 ):
370 raise EngineException(
371 "name '{}' already exists for {}".format(name, self.topic),
372 HTTPStatus.CONFLICT,
373 )
tiernob24258a2018-10-04 18:39:49 +0200374
375 @staticmethod
376 def format_on_new(content, project_id=None, make_public=False):
377 """
378 Modifies content descriptor to include _admin
379 :param content: descriptor to be modified
tierno65ca36d2019-02-12 19:27:52 +0100380 :param project_id: if included, it add project read/write permissions. Can be None or a list
tiernob24258a2018-10-04 18:39:49 +0200381 :param make_public: if included it is generated as public for reading.
tiernobdebce92019-07-01 15:36:49 +0000382 :return: op_id: operation id on asynchronous operation, None otherwise. In addition content is modified
tiernob24258a2018-10-04 18:39:49 +0200383 """
384 now = time()
385 if "_admin" not in content:
386 content["_admin"] = {}
387 if not content["_admin"].get("created"):
388 content["_admin"]["created"] = now
389 content["_admin"]["modified"] = now
390 if not content.get("_id"):
391 content["_id"] = str(uuid4())
tierno65ca36d2019-02-12 19:27:52 +0100392 if project_id is not None:
tiernob24258a2018-10-04 18:39:49 +0200393 if not content["_admin"].get("projects_read"):
tierno65ca36d2019-02-12 19:27:52 +0100394 content["_admin"]["projects_read"] = list(project_id)
tiernob24258a2018-10-04 18:39:49 +0200395 if make_public:
396 content["_admin"]["projects_read"].append("ANY")
397 if not content["_admin"].get("projects_write"):
tierno65ca36d2019-02-12 19:27:52 +0100398 content["_admin"]["projects_write"] = list(project_id)
tiernobdebce92019-07-01 15:36:49 +0000399 return None
tiernob24258a2018-10-04 18:39:49 +0200400
401 @staticmethod
402 def format_on_edit(final_content, edit_content):
tiernobdebce92019-07-01 15:36:49 +0000403 """
404 Modifies final_content to admin information upon edition
405 :param final_content: final content to be stored at database
406 :param edit_content: user requested update content
407 :return: operation id, if this edit implies an asynchronous operation; None otherwise
408 """
tiernob24258a2018-10-04 18:39:49 +0200409 if final_content.get("_admin"):
410 now = time()
411 final_content["_admin"]["modified"] = now
tiernobdebce92019-07-01 15:36:49 +0000412 return None
tiernob24258a2018-10-04 18:39:49 +0200413
tiernobee3bad2019-12-05 12:26:01 +0000414 def _send_msg(self, action, content, not_send_msg=None):
415 if self.topic_msg and not_send_msg is not False:
agarwalat53471982020-10-08 13:06:14 +0000416 content = content.copy()
tiernob24258a2018-10-04 18:39:49 +0200417 content.pop("_admin", None)
tiernobee3bad2019-12-05 12:26:01 +0000418 if isinstance(not_send_msg, list):
419 not_send_msg.append((self.topic_msg, action, content))
420 else:
421 self.msg.write(self.topic_msg, action, content)
tiernob24258a2018-10-04 18:39:49 +0200422
tiernob4844ab2019-05-23 08:42:12 +0000423 def check_conflict_on_del(self, session, _id, db_content):
tiernob24258a2018-10-04 18:39:49 +0200424 """
425 Check if deletion can be done because of dependencies if it is not force. To override
tierno65ca36d2019-02-12 19:27:52 +0100426 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
427 :param _id: internal _id
tiernob4844ab2019-05-23 08:42:12 +0000428 :param db_content: The database content of this item _id
tiernob24258a2018-10-04 18:39:49 +0200429 :return: None if ok or raises EngineException with the conflict
430 """
431 pass
432
433 @staticmethod
tierno1c38f2f2020-03-24 11:51:39 +0000434 def _update_input_with_kwargs(desc, kwargs, yaml_format=False):
tiernob24258a2018-10-04 18:39:49 +0200435 """
436 Update descriptor with the kwargs. It contains dot separated keys
437 :param desc: dictionary to be updated
438 :param kwargs: plain dictionary to be used for updating.
tierno1c38f2f2020-03-24 11:51:39 +0000439 :param yaml_format: get kwargs values as yaml format.
delacruzramoc061f562019-04-05 11:00:02 +0200440 :return: None, 'desc' is modified. It raises EngineException.
tiernob24258a2018-10-04 18:39:49 +0200441 """
442 if not kwargs:
443 return
444 try:
445 for k, v in kwargs.items():
446 update_content = desc
447 kitem_old = None
448 klist = k.split(".")
449 for kitem in klist:
450 if kitem_old is not None:
451 update_content = update_content[kitem_old]
452 if isinstance(update_content, dict):
453 kitem_old = kitem
tiernoac55f062020-06-17 07:42:30 +0000454 if not isinstance(update_content.get(kitem_old), (dict, list)):
455 update_content[kitem_old] = {}
tiernob24258a2018-10-04 18:39:49 +0200456 elif isinstance(update_content, list):
tiernoac55f062020-06-17 07:42:30 +0000457 # key must be an index of the list, must be integer
tiernob24258a2018-10-04 18:39:49 +0200458 kitem_old = int(kitem)
tiernoac55f062020-06-17 07:42:30 +0000459 # if index greater than list, extend the list
460 if kitem_old >= len(update_content):
garciadeblas4568a372021-03-24 09:19:48 +0100461 update_content += [None] * (
462 kitem_old - len(update_content) + 1
463 )
tiernoac55f062020-06-17 07:42:30 +0000464 if not isinstance(update_content[kitem_old], (dict, list)):
465 update_content[kitem_old] = {}
tiernob24258a2018-10-04 18:39:49 +0200466 else:
467 raise EngineException(
garciadeblas4568a372021-03-24 09:19:48 +0100468 "Invalid query string '{}'. Descriptor is not a list nor dict at '{}'".format(
469 k, kitem
470 )
471 )
tiernoac55f062020-06-17 07:42:30 +0000472 if v is None:
473 del update_content[kitem_old]
474 else:
475 update_content[kitem_old] = v if not yaml_format else safe_load(v)
tiernob24258a2018-10-04 18:39:49 +0200476 except KeyError:
477 raise EngineException(
garciadeblas4568a372021-03-24 09:19:48 +0100478 "Invalid query string '{}'. Descriptor does not contain '{}'".format(
479 k, kitem_old
480 )
481 )
tiernob24258a2018-10-04 18:39:49 +0200482 except ValueError:
garciadeblas4568a372021-03-24 09:19:48 +0100483 raise EngineException(
484 "Invalid query string '{}'. Expected integer index list instead of '{}'".format(
485 k, kitem
486 )
487 )
tiernob24258a2018-10-04 18:39:49 +0200488 except IndexError:
489 raise EngineException(
garciadeblas4568a372021-03-24 09:19:48 +0100490 "Invalid query string '{}'. Index '{}' out of range".format(
491 k, kitem_old
492 )
493 )
tierno1c38f2f2020-03-24 11:51:39 +0000494 except YAMLError:
495 raise EngineException("Invalid query string '{}' yaml format".format(k))
tiernob24258a2018-10-04 18:39:49 +0200496
Frank Bryden19b97522020-07-10 12:32:02 +0000497 def sol005_projection(self, data):
498 # Projection was moved to child classes
499 return data
500
K Sai Kiran57589552021-01-27 21:38:34 +0530501 def show(self, session, _id, filter_q=None, api_req=False):
tiernob24258a2018-10-04 18:39:49 +0200502 """
503 Get complete information on an topic
tierno65ca36d2019-02-12 19:27:52 +0100504 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200505 :param _id: server internal id
K Sai Kiran57589552021-01-27 21:38:34 +0530506 :param filter_q: dict: query parameter
Frank Bryden19b97522020-07-10 12:32:02 +0000507 :param api_req: True if this call is serving an external API request. False if serving internal request.
tiernob24258a2018-10-04 18:39:49 +0200508 :return: dictionary, raise exception if not found.
509 """
tierno1f029d82019-06-13 22:37:04 +0000510 if not self.multiproject:
511 filter_db = {}
512 else:
513 filter_db = self._get_project_filter(session)
delacruzramoc061f562019-04-05 11:00:02 +0200514 # To allow project&user addressing by name AS WELL AS _id
515 filter_db[BaseTopic.id_field(self.topic, _id)] = _id
Frank Bryden19b97522020-07-10 12:32:02 +0000516 data = self.db.get_one(self.topic, filter_db)
517
518 # Only perform SOL005 projection if we are serving an external request
519 if api_req:
520 self.sol005_projection(data)
521
522 return data
garciadeblas4568a372021-03-24 09:19:48 +0100523
tiernob24258a2018-10-04 18:39:49 +0200524 # TODO transform data for SOL005 URL requests
525 # TODO remove _admin if not admin
526
527 def get_file(self, session, _id, path=None, accept_header=None):
528 """
529 Only implemented for descriptor topics. Return the file content of a descriptor
tierno65ca36d2019-02-12 19:27:52 +0100530 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200531 :param _id: Identity of the item to get content
532 :param path: artifact path or "$DESCRIPTOR" or None
533 :param accept_header: Content of Accept header. Must contain applition/zip or/and text/plain
534 :return: opened file or raises an exception
535 """
garciadeblas4568a372021-03-24 09:19:48 +0100536 raise EngineException(
537 "Method get_file not valid for this topic", HTTPStatus.INTERNAL_SERVER_ERROR
538 )
tiernob24258a2018-10-04 18:39:49 +0200539
Frank Bryden19b97522020-07-10 12:32:02 +0000540 def list(self, session, filter_q=None, api_req=False):
tiernob24258a2018-10-04 18:39:49 +0200541 """
542 Get a list of the topic that matches a filter
543 :param session: contains the used login username and working project
544 :param filter_q: filter of data to be applied
Frank Bryden19b97522020-07-10 12:32:02 +0000545 :param api_req: True if this call is serving an external API request. False if serving internal request.
tiernob24258a2018-10-04 18:39:49 +0200546 :return: The list, it can be empty if no one match the filter.
547 """
548 if not filter_q:
549 filter_q = {}
tierno1f029d82019-06-13 22:37:04 +0000550 if self.multiproject:
551 filter_q.update(self._get_project_filter(session))
tiernob24258a2018-10-04 18:39:49 +0200552
553 # TODO transform data for SOL005 URL requests. Transform filtering
554 # TODO implement "field-type" query string SOL005
Frank Bryden19b97522020-07-10 12:32:02 +0000555 data = self.db.get_list(self.topic, filter_q)
556
557 # Only perform SOL005 projection if we are serving an external request
558 if api_req:
559 data = [self.sol005_projection(inst) for inst in data]
garciadeblas4568a372021-03-24 09:19:48 +0100560
Frank Bryden19b97522020-07-10 12:32:02 +0000561 return data
tiernob24258a2018-10-04 18:39:49 +0200562
tierno65ca36d2019-02-12 19:27:52 +0100563 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
tiernob24258a2018-10-04 18:39:49 +0200564 """
565 Creates a new entry into database.
566 :param rollback: list to append created items at database in case a rollback may to be done
tierno65ca36d2019-02-12 19:27:52 +0100567 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200568 :param indata: data to be inserted
569 :param kwargs: used to override the indata descriptor
570 :param headers: http request headers
tiernobdebce92019-07-01 15:36:49 +0000571 :return: _id, op_id:
572 _id: identity of the inserted data.
573 op_id: operation id if this is asynchronous, None otherwise
tiernob24258a2018-10-04 18:39:49 +0200574 """
575 try:
delacruzramo32bab472019-09-13 12:24:22 +0200576 if self.multiproject:
577 self.check_quota(session)
578
tiernob24258a2018-10-04 18:39:49 +0200579 content = self._remove_envelop(indata)
580
581 # Override descriptor with query string kwargs
582 self._update_input_with_kwargs(content, kwargs)
tierno65ca36d2019-02-12 19:27:52 +0100583 content = self._validate_input_new(content, force=session["force"])
584 self.check_conflict_on_new(session, content)
garciadeblas4568a372021-03-24 09:19:48 +0100585 op_id = self.format_on_new(
586 content, project_id=session["project_id"], make_public=session["public"]
587 )
tiernob24258a2018-10-04 18:39:49 +0200588 _id = self.db.create(self.topic, content)
589 rollback.append({"topic": self.topic, "_id": _id})
tiernobdebce92019-07-01 15:36:49 +0000590 if op_id:
591 content["op_id"] = op_id
tierno15a1f682019-10-16 09:00:13 +0000592 self._send_msg("created", content)
tiernobdebce92019-07-01 15:36:49 +0000593 return _id, op_id
tiernob24258a2018-10-04 18:39:49 +0200594 except ValidationError as e:
595 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
596
tierno65ca36d2019-02-12 19:27:52 +0100597 def upload_content(self, session, _id, indata, kwargs, headers):
tiernob24258a2018-10-04 18:39:49 +0200598 """
599 Only implemented for descriptor topics. Used for receiving content by chunks (with a transaction_id header
600 and/or gzip file. It will store and extract)
tierno65ca36d2019-02-12 19:27:52 +0100601 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200602 :param _id : the database id of entry to be updated
603 :param indata: http body request
604 :param kwargs: user query string to override parameters. NOT USED
605 :param headers: http request headers
tiernob24258a2018-10-04 18:39:49 +0200606 :return: True package has is completely uploaded or False if partial content has been uplodaed.
607 Raise exception on error
608 """
garciadeblas4568a372021-03-24 09:19:48 +0100609 raise EngineException(
610 "Method upload_content not valid for this topic",
611 HTTPStatus.INTERNAL_SERVER_ERROR,
612 )
tiernob24258a2018-10-04 18:39:49 +0200613
614 def delete_list(self, session, filter_q=None):
615 """
616 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 +0100617 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200618 :param filter_q: filter of data to be applied
619 :return: The deleted list, it can be empty if no one match the filter.
620 """
621 # TODO add admin to filter, validate rights
622 if not filter_q:
623 filter_q = {}
tierno1f029d82019-06-13 22:37:04 +0000624 if self.multiproject:
625 filter_q.update(self._get_project_filter(session))
tiernob24258a2018-10-04 18:39:49 +0200626 return self.db.del_list(self.topic, filter_q)
627
tiernobee3bad2019-12-05 12:26:01 +0000628 def delete_extra(self, session, _id, db_content, not_send_msg=None):
tierno65ca36d2019-02-12 19:27:52 +0100629 """
630 Delete other things apart from database entry of a item _id.
631 e.g.: other associated elements at database and other file system storage
632 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
633 :param _id: server internal id
tiernob4844ab2019-05-23 08:42:12 +0000634 :param db_content: The database content of the _id. It is already deleted when reached this method, but the
635 content is needed in same cases
tiernobee3bad2019-12-05 12:26:01 +0000636 :param not_send_msg: To not send message (False) or store content (list) instead
tiernob4844ab2019-05-23 08:42:12 +0000637 :return: None if ok or raises EngineException with the problem
tierno65ca36d2019-02-12 19:27:52 +0100638 """
639 pass
640
tiernobee3bad2019-12-05 12:26:01 +0000641 def delete(self, session, _id, dry_run=False, not_send_msg=None):
tiernob24258a2018-10-04 18:39:49 +0200642 """
643 Delete item by its internal _id
tierno65ca36d2019-02-12 19:27:52 +0100644 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200645 :param _id: server internal id
tiernob24258a2018-10-04 18:39:49 +0200646 :param dry_run: make checking but do not delete
tiernobee3bad2019-12-05 12:26:01 +0000647 :param not_send_msg: To not send message (False) or store content (list) instead
tiernobdebce92019-07-01 15:36:49 +0000648 :return: operation id (None if there is not operation), raise exception if error or not found, conflict, ...
tiernob24258a2018-10-04 18:39:49 +0200649 """
tiernob4844ab2019-05-23 08:42:12 +0000650
651 # To allow addressing projects and users by name AS WELL AS by _id
tiernof5f2e3f2020-03-23 14:42:10 +0000652 if not self.multiproject:
653 filter_q = {}
654 else:
655 filter_q = self._get_project_filter(session)
656 filter_q[self.id_field(self.topic, _id)] = _id
tiernob4844ab2019-05-23 08:42:12 +0000657 item_content = self.db.get_one(self.topic, filter_q)
658
tiernob4844ab2019-05-23 08:42:12 +0000659 self.check_conflict_on_del(session, _id, item_content)
tierno65ca36d2019-02-12 19:27:52 +0100660 if dry_run:
661 return None
garciadeblas4568a372021-03-24 09:19:48 +0100662
tierno65ca36d2019-02-12 19:27:52 +0100663 if self.multiproject and session["project_id"]:
tiernof5f2e3f2020-03-23 14:42:10 +0000664 # remove reference from project_read if there are more projects referencing it. If it last one,
665 # do not remove reference, but delete
garciadeblas4568a372021-03-24 09:19:48 +0100666 other_projects_referencing = next(
667 (
668 p
669 for p in item_content["_admin"]["projects_read"]
670 if p not in session["project_id"] and p != "ANY"
671 ),
672 None,
673 )
tiernof5f2e3f2020-03-23 14:42:10 +0000674
675 # check if there are projects referencing it (apart from ANY, that means, public)....
676 if other_projects_referencing:
677 # remove references but not delete
garciadeblas4568a372021-03-24 09:19:48 +0100678 update_dict_pull = {
679 "_admin.projects_read": session["project_id"],
680 "_admin.projects_write": session["project_id"],
681 }
682 self.db.set_one(
683 self.topic, filter_q, update_dict=None, pull_list=update_dict_pull
684 )
tiernobdebce92019-07-01 15:36:49 +0000685 return None
tiernof5f2e3f2020-03-23 14:42:10 +0000686 else:
garciadeblas4568a372021-03-24 09:19:48 +0100687 can_write = next(
688 (
689 p
690 for p in item_content["_admin"]["projects_write"]
691 if p == "ANY" or p in session["project_id"]
692 ),
693 None,
694 )
tiernof5f2e3f2020-03-23 14:42:10 +0000695 if not can_write:
garciadeblas4568a372021-03-24 09:19:48 +0100696 raise EngineException(
697 "You have not write permission to delete it",
698 http_code=HTTPStatus.UNAUTHORIZED,
699 )
tiernof5f2e3f2020-03-23 14:42:10 +0000700
701 # delete
702 self.db.del_one(self.topic, filter_q)
tiernobee3bad2019-12-05 12:26:01 +0000703 self.delete_extra(session, _id, item_content, not_send_msg=not_send_msg)
704 self._send_msg("deleted", {"_id": _id}, not_send_msg=not_send_msg)
tiernobdebce92019-07-01 15:36:49 +0000705 return None
tiernob24258a2018-10-04 18:39:49 +0200706
tierno65ca36d2019-02-12 19:27:52 +0100707 def edit(self, session, _id, indata=None, kwargs=None, content=None):
708 """
709 Change the content of an item
710 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
711 :param _id: server internal id
712 :param indata: contains the changes to apply
713 :param kwargs: modifies indata
714 :param content: original content of the item
tiernobdebce92019-07-01 15:36:49 +0000715 :return: op_id: operation id if this is processed asynchronously, None otherwise
tierno65ca36d2019-02-12 19:27:52 +0100716 """
tiernob24258a2018-10-04 18:39:49 +0200717 indata = self._remove_envelop(indata)
718
719 # Override descriptor with query string kwargs
720 if kwargs:
721 self._update_input_with_kwargs(indata, kwargs)
722 try:
tierno65ca36d2019-02-12 19:27:52 +0100723 if indata and session.get("set_project"):
garciadeblas4568a372021-03-24 09:19:48 +0100724 raise EngineException(
725 "Cannot edit content and set to project (query string SET_PROJECT) at same time",
726 HTTPStatus.UNPROCESSABLE_ENTITY,
727 )
tiernob24258a2018-10-04 18:39:49 +0200728 # TODO self._check_edition(session, indata, _id, force)
729 if not content:
730 content = self.show(session, _id)
Frank Brydendeba68e2020-07-27 13:55:11 +0000731 indata = self._validate_input_edit(indata, content, force=session["force"])
tiernob24258a2018-10-04 18:39:49 +0200732 deep_update_rfc7396(content, indata)
tiernobdebce92019-07-01 15:36:49 +0000733
734 # To allow project addressing by name AS WELL AS _id. Get the _id, just in case the provided one is a name
735 _id = content.get("_id") or _id
736
bravofb995ea22021-02-10 10:57:52 -0300737 content = self.check_conflict_on_edit(session, content, indata, _id=_id)
tiernobdebce92019-07-01 15:36:49 +0000738 op_id = self.format_on_edit(content, indata)
739
740 self.db.replace(self.topic, _id, content)
tiernob24258a2018-10-04 18:39:49 +0200741
742 indata.pop("_admin", None)
tiernobdebce92019-07-01 15:36:49 +0000743 if op_id:
744 indata["op_id"] = op_id
tiernob24258a2018-10-04 18:39:49 +0200745 indata["_id"] = _id
tierno15a1f682019-10-16 09:00:13 +0000746 self._send_msg("edited", indata)
tiernobdebce92019-07-01 15:36:49 +0000747 return op_id
tiernob24258a2018-10-04 18:39:49 +0200748 except ValidationError as e:
749 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)