blob: 8cfa10b8f725e04eaa7d008afe2a1e10be7f5b15 [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
shrinithi28d887f2025-01-08 05:27:19 +000016
yshah53cc9eb2024-07-05 13:06:31 +000017import logging
rshri2d386cb2024-07-05 14:35:51 +000018import random
19import string
tiernob24258a2018-10-04 18:39:49 +020020from uuid import uuid4
21from http import HTTPStatus
22from time import time
aticig2b5e1232022-08-10 17:30:12 +030023from osm_common.dbbase import deep_update_rfc7396, DbException
tierno23acf402019-08-28 13:36:34 +000024from osm_nbi.validation import validate_input, ValidationError, is_valid_uuid
tierno1c38f2f2020-03-24 11:51:39 +000025from yaml import safe_load, YAMLError
tiernob24258a2018-10-04 18:39:49 +020026
27__author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
28
29
30class EngineException(Exception):
tiernob24258a2018-10-04 18:39:49 +020031 def __init__(self, message, http_code=HTTPStatus.BAD_REQUEST):
32 self.http_code = http_code
tierno23acf402019-08-28 13:36:34 +000033 super(Exception, self).__init__(message)
tiernob24258a2018-10-04 18:39:49 +020034
garciadeblasf2af4a12023-01-24 16:56:54 +010035
aticig2b5e1232022-08-10 17:30:12 +030036class NBIBadArgumentsException(Exception):
37 """
38 Bad argument values exception
39 """
40
41 def __init__(self, message: str = "", bad_args: list = None):
42 Exception.__init__(self, message)
43 self.message = message
44 self.bad_args = bad_args
45
46 def __str__(self):
garciadeblasf2af4a12023-01-24 16:56:54 +010047 return "{}, Bad arguments: {}".format(self.message, self.bad_args)
48
tiernob24258a2018-10-04 18:39:49 +020049
tierno714954e2019-11-29 13:43:26 +000050def deep_get(target_dict, key_list):
51 """
52 Get a value from target_dict entering in the nested keys. If keys does not exist, it returns None
53 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
54 :param target_dict: dictionary to be read
55 :param key_list: list of keys to read from target_dict
56 :return: The wanted value if exist, None otherwise
57 """
58 for key in key_list:
59 if not isinstance(target_dict, dict) or key not in target_dict:
60 return None
61 target_dict = target_dict[key]
62 return target_dict
63
64
garciadeblasf2af4a12023-01-24 16:56:54 +010065def detect_descriptor_usage(descriptor: dict, db_collection: str, db: object) -> bool:
aticig2b5e1232022-08-10 17:30:12 +030066 """Detect the descriptor usage state.
67
68 Args:
69 descriptor (dict): VNF or NS Descriptor as dictionary
70 db_collection (str): collection name which is looked for in DB
71 db (object): name of db object
72
73 Returns:
74 True if descriptor is in use else None
75
76 """
77 try:
78 if not descriptor:
79 raise NBIBadArgumentsException(
80 "Argument is mandatory and can not be empty", "descriptor"
81 )
82
83 if not db:
84 raise NBIBadArgumentsException("A valid DB object should be provided", "db")
85
86 search_dict = {
87 "vnfds": ("vnfrs", "vnfd-id"),
88 "nsds": ("nsrs", "nsd-id"),
kayal2001f71c2e82024-06-25 15:26:24 +053089 "ns_config_template": ("ns_config_template", "_id"),
aticig2b5e1232022-08-10 17:30:12 +030090 }
91
92 if db_collection not in search_dict:
garciadeblasf2af4a12023-01-24 16:56:54 +010093 raise NBIBadArgumentsException(
94 "db_collection should be equal to vnfds or nsds", "db_collection"
95 )
aticig2b5e1232022-08-10 17:30:12 +030096
97 record_list = db.get_list(
98 search_dict[db_collection][0],
99 {search_dict[db_collection][1]: descriptor["_id"]},
100 )
101
102 if record_list:
103 return True
104
105 except (DbException, KeyError, NBIBadArgumentsException) as error:
garciadeblasf2af4a12023-01-24 16:56:54 +0100106 raise EngineException(
107 f"Error occured while detecting the descriptor usage: {error}"
108 )
aticig2b5e1232022-08-10 17:30:12 +0300109
110
111def update_descriptor_usage_state(
112 descriptor: dict, db_collection: str, db: object
113) -> None:
114 """Updates the descriptor usage state.
115
116 Args:
117 descriptor (dict): VNF or NS Descriptor as dictionary
118 db_collection (str): collection name which is looked for in DB
119 db (object): name of db object
120
121 Returns:
122 None
123
124 """
125 try:
126 descriptor_update = {
127 "_admin.usageState": "NOT_IN_USE",
128 }
129
130 if detect_descriptor_usage(descriptor, db_collection, db):
131 descriptor_update = {
132 "_admin.usageState": "IN_USE",
133 }
134
garciadeblasf2af4a12023-01-24 16:56:54 +0100135 db.set_one(
136 db_collection, {"_id": descriptor["_id"]}, update_dict=descriptor_update
137 )
aticig2b5e1232022-08-10 17:30:12 +0300138
139 except (DbException, KeyError, NBIBadArgumentsException) as error:
garciadeblasf2af4a12023-01-24 16:56:54 +0100140 raise EngineException(
141 f"Error occured while updating the descriptor usage state: {error}"
142 )
aticig2b5e1232022-08-10 17:30:12 +0300143
144
tiernob24258a2018-10-04 18:39:49 +0200145def get_iterable(input_var):
146 """
147 Returns an iterable, in case input_var is None it just returns an empty tuple
148 :param input_var: can be a list, tuple or None
149 :return: input_var or () if it is None
150 """
151 if input_var is None:
152 return ()
153 return input_var
154
155
156def versiontuple(v):
157 """utility for compare dot separate versions. Fills with zeros to proper number comparison"""
158 filled = []
159 for point in v.split("."):
160 filled.append(point.zfill(8))
161 return tuple(filled)
162
163
tiernocddb07d2020-10-06 08:28:00 +0000164def increment_ip_mac(ip_mac, vm_index=1):
165 if not isinstance(ip_mac, str):
166 return ip_mac
167 try:
168 # try with ipv4 look for last dot
169 i = ip_mac.rfind(".")
170 if i > 0:
171 i += 1
172 return "{}{}".format(ip_mac[:i], int(ip_mac[i:]) + vm_index)
173 # try with ipv6 or mac look for last colon. Operate in hex
174 i = ip_mac.rfind(":")
175 if i > 0:
176 i += 1
177 # format in hex, len can be 2 for mac or 4 for ipv6
garciadeblas4568a372021-03-24 09:19:48 +0100178 return ("{}{:0" + str(len(ip_mac) - i) + "x}").format(
179 ip_mac[:i], int(ip_mac[i:], 16) + vm_index
180 )
tiernocddb07d2020-10-06 08:28:00 +0000181 except Exception:
182 pass
183 return None
184
185
tiernob24258a2018-10-04 18:39:49 +0200186class BaseTopic:
187 # static variables for all instance classes
garciadeblas4568a372021-03-24 09:19:48 +0100188 topic = None # to_override
189 topic_msg = None # to_override
190 quota_name = None # to_override. If not provided topic will be used for quota_name
191 schema_new = None # to_override
tiernob24258a2018-10-04 18:39:49 +0200192 schema_edit = None # to_override
tierno65ca36d2019-02-12 19:27:52 +0100193 multiproject = True # True if this Topic can be shared by several projects. Then it contains _admin.projects_read
tiernob24258a2018-10-04 18:39:49 +0200194
delacruzramo32bab472019-09-13 12:24:22 +0200195 default_quota = 500
196
delacruzramoc061f562019-04-05 11:00:02 +0200197 # Alternative ID Fields for some Topics
garciadeblas4568a372021-03-24 09:19:48 +0100198 alt_id_field = {"projects": "name", "users": "username", "roles": "name"}
delacruzramoc061f562019-04-05 11:00:02 +0200199
delacruzramo32bab472019-09-13 12:24:22 +0200200 def __init__(self, db, fs, msg, auth):
tiernob24258a2018-10-04 18:39:49 +0200201 self.db = db
202 self.fs = fs
203 self.msg = msg
yshah53cc9eb2024-07-05 13:06:31 +0000204 self.logger = logging.getLogger("nbi.base")
delacruzramo32bab472019-09-13 12:24:22 +0200205 self.auth = auth
tiernob24258a2018-10-04 18:39:49 +0200206
207 @staticmethod
delacruzramoc061f562019-04-05 11:00:02 +0200208 def id_field(topic, value):
tierno65ca36d2019-02-12 19:27:52 +0100209 """Returns ID Field for given topic and field value"""
delacruzramoceb8baf2019-06-21 14:25:38 +0200210 if topic in BaseTopic.alt_id_field.keys() and not is_valid_uuid(value):
delacruzramoc061f562019-04-05 11:00:02 +0200211 return BaseTopic.alt_id_field[topic]
212 else:
213 return "_id"
214
215 @staticmethod
tiernob24258a2018-10-04 18:39:49 +0200216 def _remove_envelop(indata=None):
217 if not indata:
218 return {}
219 return indata
220
delacruzramo32bab472019-09-13 12:24:22 +0200221 def check_quota(self, session):
222 """
223 Check whether topic quota is exceeded by the given project
224 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 +0000225 :param session[project_id]: projects (tuple) for which quota should be checked
226 :param session[force]: boolean. If true, skip quota checking
delacruzramo32bab472019-09-13 12:24:22 +0200227 :return: None
228 :raise:
229 DbException if project not found
tierno6b02b052020-06-02 10:07:41 +0000230 ValidationError if quota exceeded in one of the projects
delacruzramo32bab472019-09-13 12:24:22 +0200231 """
tiernod7749582020-05-28 10:41:10 +0000232 if session["force"]:
delacruzramo32bab472019-09-13 12:24:22 +0200233 return
234 projects = session["project_id"]
235 for project in projects:
236 proj = self.auth.get_project(project)
237 pid = proj["_id"]
tierno6b02b052020-06-02 10:07:41 +0000238 quota_name = self.quota_name or self.topic
239 quota = proj.get("quotas", {}).get(quota_name, self.default_quota)
delacruzramo32bab472019-09-13 12:24:22 +0200240 count = self.db.count(self.topic, {"_admin.projects_read": pid})
241 if count >= quota:
242 name = proj["name"]
garciadeblas4568a372021-03-24 09:19:48 +0100243 raise ValidationError(
244 "quota ({}={}) exceeded for project {} ({})".format(
245 quota_name, quota, name, pid
246 ),
247 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
248 )
delacruzramo32bab472019-09-13 12:24:22 +0200249
tiernob24258a2018-10-04 18:39:49 +0200250 def _validate_input_new(self, input, force=False):
251 """
252 Validates input user content for a new entry. It uses jsonschema. Some overrides will use pyangbind
253 :param input: user input content for the new topic
254 :param force: may be used for being more tolerant
255 :return: The same input content, or a changed version of it.
256 """
257 if self.schema_new:
258 validate_input(input, self.schema_new)
259 return input
260
Frank Brydendeba68e2020-07-27 13:55:11 +0000261 def _validate_input_edit(self, input, content, force=False):
tiernob24258a2018-10-04 18:39:49 +0200262 """
263 Validates input user content for an edition. It uses jsonschema. Some overrides will use pyangbind
264 :param input: user input content for the new topic
265 :param force: may be used for being more tolerant
266 :return: The same input content, or a changed version of it.
267 """
268 if self.schema_edit:
269 validate_input(input, self.schema_edit)
270 return input
271
272 @staticmethod
tierno65ca36d2019-02-12 19:27:52 +0100273 def _get_project_filter(session):
tiernob24258a2018-10-04 18:39:49 +0200274 """
275 Generates a filter dictionary for querying database, so that only allowed items for this project can be
tiernof5f2e3f2020-03-23 14:42:10 +0000276 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 +0200277 not present or contains ANY mean public.
tierno65ca36d2019-02-12 19:27:52 +0100278 :param session: contains:
279 project_id: project list this session has rights to access. Can be empty, one or several
garciadeblas4568a372021-03-24 09:19:48 +0100280 set_project: items created will contain this project list
tierno65ca36d2019-02-12 19:27:52 +0100281 force: True or False
282 public: True, False or None
283 method: "list", "show", "write", "delete"
284 admin: True or False
285 :return: dictionary with project filter
tiernob24258a2018-10-04 18:39:49 +0200286 """
tierno65ca36d2019-02-12 19:27:52 +0100287 p_filter = {}
288 project_filter_n = []
289 project_filter = list(session["project_id"])
tiernob24258a2018-10-04 18:39:49 +0200290
tierno65ca36d2019-02-12 19:27:52 +0100291 if session["method"] not in ("list", "delete"):
292 if project_filter:
293 project_filter.append("ANY")
294 elif session["public"] is not None:
295 if session["public"]:
296 project_filter.append("ANY")
297 else:
298 project_filter_n.append("ANY")
299
300 if session.get("PROJECT.ne"):
301 project_filter_n.append(session["PROJECT.ne"])
302
303 if project_filter:
garciadeblas4568a372021-03-24 09:19:48 +0100304 if session["method"] in ("list", "show", "delete") or session.get(
305 "set_project"
306 ):
tierno65ca36d2019-02-12 19:27:52 +0100307 p_filter["_admin.projects_read.cont"] = project_filter
308 else:
309 p_filter["_admin.projects_write.cont"] = project_filter
310 if project_filter_n:
garciadeblas4568a372021-03-24 09:19:48 +0100311 if session["method"] in ("list", "show", "delete") or session.get(
312 "set_project"
313 ):
tierno65ca36d2019-02-12 19:27:52 +0100314 p_filter["_admin.projects_read.ncont"] = project_filter_n
315 else:
316 p_filter["_admin.projects_write.ncont"] = project_filter_n
317
318 return p_filter
319
320 def check_conflict_on_new(self, session, indata):
tiernob24258a2018-10-04 18:39:49 +0200321 """
322 Check that the data to be inserted is valid
tierno65ca36d2019-02-12 19:27:52 +0100323 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200324 :param indata: data to be inserted
tiernob24258a2018-10-04 18:39:49 +0200325 :return: None or raises EngineException
326 """
327 pass
328
tierno65ca36d2019-02-12 19:27:52 +0100329 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
tiernob24258a2018-10-04 18:39:49 +0200330 """
331 Check that the data to be edited/uploaded is valid
tierno65ca36d2019-02-12 19:27:52 +0100332 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernobdebce92019-07-01 15:36:49 +0000333 :param final_content: data once modified. This method may change it.
tiernob24258a2018-10-04 18:39:49 +0200334 :param edit_content: incremental data that contains the modifications to apply
335 :param _id: internal _id
bravofb995ea22021-02-10 10:57:52 -0300336 :return: final_content or raises EngineException
tiernob24258a2018-10-04 18:39:49 +0200337 """
tierno65ca36d2019-02-12 19:27:52 +0100338 if not self.multiproject:
bravofb995ea22021-02-10 10:57:52 -0300339 return final_content
tierno65ca36d2019-02-12 19:27:52 +0100340 # Change public status
341 if session["public"] is not None:
garciadeblas4568a372021-03-24 09:19:48 +0100342 if (
343 session["public"]
344 and "ANY" not in final_content["_admin"]["projects_read"]
345 ):
tierno65ca36d2019-02-12 19:27:52 +0100346 final_content["_admin"]["projects_read"].append("ANY")
347 final_content["_admin"]["projects_write"].clear()
garciadeblas4568a372021-03-24 09:19:48 +0100348 if (
349 not session["public"]
350 and "ANY" in final_content["_admin"]["projects_read"]
351 ):
tierno65ca36d2019-02-12 19:27:52 +0100352 final_content["_admin"]["projects_read"].remove("ANY")
353
354 # Change project status
355 if session.get("set_project"):
356 for p in session["set_project"]:
357 if p not in final_content["_admin"]["projects_read"]:
358 final_content["_admin"]["projects_read"].append(p)
tiernob24258a2018-10-04 18:39:49 +0200359
bravofb995ea22021-02-10 10:57:52 -0300360 return final_content
361
tiernob24258a2018-10-04 18:39:49 +0200362 def check_unique_name(self, session, name, _id=None):
363 """
364 Check that the name is unique for this project
tierno65ca36d2019-02-12 19:27:52 +0100365 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200366 :param name: name to be checked
367 :param _id: If not None, ignore this entry that are going to change
368 :return: None or raises EngineException
369 """
tierno1f029d82019-06-13 22:37:04 +0000370 if not self.multiproject:
371 _filter = {}
372 else:
373 _filter = self._get_project_filter(session)
tiernob24258a2018-10-04 18:39:49 +0200374 _filter["name"] = name
375 if _id:
376 _filter["_id.neq"] = _id
garciadeblas4568a372021-03-24 09:19:48 +0100377 if self.db.get_one(
378 self.topic, _filter, fail_on_empty=False, fail_on_more=False
379 ):
380 raise EngineException(
381 "name '{}' already exists for {}".format(name, self.topic),
382 HTTPStatus.CONFLICT,
383 )
tiernob24258a2018-10-04 18:39:49 +0200384
385 @staticmethod
386 def format_on_new(content, project_id=None, make_public=False):
387 """
388 Modifies content descriptor to include _admin
389 :param content: descriptor to be modified
tierno65ca36d2019-02-12 19:27:52 +0100390 :param project_id: if included, it add project read/write permissions. Can be None or a list
tiernob24258a2018-10-04 18:39:49 +0200391 :param make_public: if included it is generated as public for reading.
tiernobdebce92019-07-01 15:36:49 +0000392 :return: op_id: operation id on asynchronous operation, None otherwise. In addition content is modified
tiernob24258a2018-10-04 18:39:49 +0200393 """
394 now = time()
395 if "_admin" not in content:
396 content["_admin"] = {}
397 if not content["_admin"].get("created"):
398 content["_admin"]["created"] = now
399 content["_admin"]["modified"] = now
400 if not content.get("_id"):
401 content["_id"] = str(uuid4())
tierno65ca36d2019-02-12 19:27:52 +0100402 if project_id is not None:
tiernob24258a2018-10-04 18:39:49 +0200403 if not content["_admin"].get("projects_read"):
tierno65ca36d2019-02-12 19:27:52 +0100404 content["_admin"]["projects_read"] = list(project_id)
tiernob24258a2018-10-04 18:39:49 +0200405 if make_public:
406 content["_admin"]["projects_read"].append("ANY")
407 if not content["_admin"].get("projects_write"):
tierno65ca36d2019-02-12 19:27:52 +0100408 content["_admin"]["projects_write"] = list(project_id)
tiernobdebce92019-07-01 15:36:49 +0000409 return None
tiernob24258a2018-10-04 18:39:49 +0200410
411 @staticmethod
412 def format_on_edit(final_content, edit_content):
tiernobdebce92019-07-01 15:36:49 +0000413 """
414 Modifies final_content to admin information upon edition
415 :param final_content: final content to be stored at database
416 :param edit_content: user requested update content
417 :return: operation id, if this edit implies an asynchronous operation; None otherwise
418 """
tiernob24258a2018-10-04 18:39:49 +0200419 if final_content.get("_admin"):
420 now = time()
421 final_content["_admin"]["modified"] = now
tiernobdebce92019-07-01 15:36:49 +0000422 return None
tiernob24258a2018-10-04 18:39:49 +0200423
tiernobee3bad2019-12-05 12:26:01 +0000424 def _send_msg(self, action, content, not_send_msg=None):
425 if self.topic_msg and not_send_msg is not False:
agarwalat53471982020-10-08 13:06:14 +0000426 content = content.copy()
tiernob24258a2018-10-04 18:39:49 +0200427 content.pop("_admin", None)
tiernobee3bad2019-12-05 12:26:01 +0000428 if isinstance(not_send_msg, list):
429 not_send_msg.append((self.topic_msg, action, content))
430 else:
431 self.msg.write(self.topic_msg, action, content)
tiernob24258a2018-10-04 18:39:49 +0200432
tiernob4844ab2019-05-23 08:42:12 +0000433 def check_conflict_on_del(self, session, _id, db_content):
tiernob24258a2018-10-04 18:39:49 +0200434 """
435 Check if deletion can be done because of dependencies if it is not force. To override
tierno65ca36d2019-02-12 19:27:52 +0100436 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
437 :param _id: internal _id
tiernob4844ab2019-05-23 08:42:12 +0000438 :param db_content: The database content of this item _id
tiernob24258a2018-10-04 18:39:49 +0200439 :return: None if ok or raises EngineException with the conflict
440 """
441 pass
442
443 @staticmethod
tierno1c38f2f2020-03-24 11:51:39 +0000444 def _update_input_with_kwargs(desc, kwargs, yaml_format=False):
tiernob24258a2018-10-04 18:39:49 +0200445 """
446 Update descriptor with the kwargs. It contains dot separated keys
447 :param desc: dictionary to be updated
448 :param kwargs: plain dictionary to be used for updating.
tierno1c38f2f2020-03-24 11:51:39 +0000449 :param yaml_format: get kwargs values as yaml format.
delacruzramoc061f562019-04-05 11:00:02 +0200450 :return: None, 'desc' is modified. It raises EngineException.
tiernob24258a2018-10-04 18:39:49 +0200451 """
452 if not kwargs:
453 return
454 try:
455 for k, v in kwargs.items():
456 update_content = desc
457 kitem_old = None
458 klist = k.split(".")
459 for kitem in klist:
460 if kitem_old is not None:
461 update_content = update_content[kitem_old]
462 if isinstance(update_content, dict):
463 kitem_old = kitem
tiernoac55f062020-06-17 07:42:30 +0000464 if not isinstance(update_content.get(kitem_old), (dict, list)):
465 update_content[kitem_old] = {}
tiernob24258a2018-10-04 18:39:49 +0200466 elif isinstance(update_content, list):
tiernoac55f062020-06-17 07:42:30 +0000467 # key must be an index of the list, must be integer
tiernob24258a2018-10-04 18:39:49 +0200468 kitem_old = int(kitem)
tiernoac55f062020-06-17 07:42:30 +0000469 # if index greater than list, extend the list
470 if kitem_old >= len(update_content):
garciadeblas4568a372021-03-24 09:19:48 +0100471 update_content += [None] * (
472 kitem_old - len(update_content) + 1
473 )
tiernoac55f062020-06-17 07:42:30 +0000474 if not isinstance(update_content[kitem_old], (dict, list)):
475 update_content[kitem_old] = {}
tiernob24258a2018-10-04 18:39:49 +0200476 else:
477 raise EngineException(
garciadeblas4568a372021-03-24 09:19:48 +0100478 "Invalid query string '{}'. Descriptor is not a list nor dict at '{}'".format(
479 k, kitem
480 )
481 )
tiernoac55f062020-06-17 07:42:30 +0000482 if v is None:
483 del update_content[kitem_old]
484 else:
485 update_content[kitem_old] = v if not yaml_format else safe_load(v)
tiernob24258a2018-10-04 18:39:49 +0200486 except KeyError:
487 raise EngineException(
garciadeblas4568a372021-03-24 09:19:48 +0100488 "Invalid query string '{}'. Descriptor does not contain '{}'".format(
489 k, kitem_old
490 )
491 )
tiernob24258a2018-10-04 18:39:49 +0200492 except ValueError:
garciadeblas4568a372021-03-24 09:19:48 +0100493 raise EngineException(
494 "Invalid query string '{}'. Expected integer index list instead of '{}'".format(
495 k, kitem
496 )
497 )
tiernob24258a2018-10-04 18:39:49 +0200498 except IndexError:
499 raise EngineException(
garciadeblas4568a372021-03-24 09:19:48 +0100500 "Invalid query string '{}'. Index '{}' out of range".format(
501 k, kitem_old
502 )
503 )
tierno1c38f2f2020-03-24 11:51:39 +0000504 except YAMLError:
505 raise EngineException("Invalid query string '{}' yaml format".format(k))
tiernob24258a2018-10-04 18:39:49 +0200506
Frank Bryden19b97522020-07-10 12:32:02 +0000507 def sol005_projection(self, data):
508 # Projection was moved to child classes
509 return data
510
K Sai Kiran57589552021-01-27 21:38:34 +0530511 def show(self, session, _id, filter_q=None, api_req=False):
tiernob24258a2018-10-04 18:39:49 +0200512 """
513 Get complete information on an topic
tierno65ca36d2019-02-12 19:27:52 +0100514 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200515 :param _id: server internal id
K Sai Kiran57589552021-01-27 21:38:34 +0530516 :param filter_q: dict: query parameter
Frank Bryden19b97522020-07-10 12:32:02 +0000517 :param api_req: True if this call is serving an external API request. False if serving internal request.
tiernob24258a2018-10-04 18:39:49 +0200518 :return: dictionary, raise exception if not found.
519 """
tierno1f029d82019-06-13 22:37:04 +0000520 if not self.multiproject:
521 filter_db = {}
522 else:
523 filter_db = self._get_project_filter(session)
delacruzramoc061f562019-04-05 11:00:02 +0200524 # To allow project&user addressing by name AS WELL AS _id
525 filter_db[BaseTopic.id_field(self.topic, _id)] = _id
Frank Bryden19b97522020-07-10 12:32:02 +0000526 data = self.db.get_one(self.topic, filter_db)
527
528 # Only perform SOL005 projection if we are serving an external request
529 if api_req:
530 self.sol005_projection(data)
Frank Bryden19b97522020-07-10 12:32:02 +0000531 return data
garciadeblas4568a372021-03-24 09:19:48 +0100532
tiernob24258a2018-10-04 18:39:49 +0200533 # TODO transform data for SOL005 URL requests
534 # TODO remove _admin if not admin
535
536 def get_file(self, session, _id, path=None, accept_header=None):
537 """
538 Only implemented for descriptor topics. Return the file content of a descriptor
tierno65ca36d2019-02-12 19:27:52 +0100539 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200540 :param _id: Identity of the item to get content
541 :param path: artifact path or "$DESCRIPTOR" or None
542 :param accept_header: Content of Accept header. Must contain applition/zip or/and text/plain
543 :return: opened file or raises an exception
544 """
garciadeblas4568a372021-03-24 09:19:48 +0100545 raise EngineException(
546 "Method get_file not valid for this topic", HTTPStatus.INTERNAL_SERVER_ERROR
547 )
tiernob24258a2018-10-04 18:39:49 +0200548
Frank Bryden19b97522020-07-10 12:32:02 +0000549 def list(self, session, filter_q=None, api_req=False):
tiernob24258a2018-10-04 18:39:49 +0200550 """
551 Get a list of the topic that matches a filter
552 :param session: contains the used login username and working project
553 :param filter_q: filter of data to be applied
Frank Bryden19b97522020-07-10 12:32:02 +0000554 :param api_req: True if this call is serving an external API request. False if serving internal request.
tiernob24258a2018-10-04 18:39:49 +0200555 :return: The list, it can be empty if no one match the filter.
556 """
557 if not filter_q:
558 filter_q = {}
tierno1f029d82019-06-13 22:37:04 +0000559 if self.multiproject:
560 filter_q.update(self._get_project_filter(session))
tiernob24258a2018-10-04 18:39:49 +0200561
562 # TODO transform data for SOL005 URL requests. Transform filtering
563 # TODO implement "field-type" query string SOL005
Frank Bryden19b97522020-07-10 12:32:02 +0000564 data = self.db.get_list(self.topic, filter_q)
565
566 # Only perform SOL005 projection if we are serving an external request
567 if api_req:
568 data = [self.sol005_projection(inst) for inst in data]
garciadeblas4568a372021-03-24 09:19:48 +0100569
Frank Bryden19b97522020-07-10 12:32:02 +0000570 return data
tiernob24258a2018-10-04 18:39:49 +0200571
tierno65ca36d2019-02-12 19:27:52 +0100572 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
tiernob24258a2018-10-04 18:39:49 +0200573 """
574 Creates a new entry into database.
575 :param rollback: list to append created items at database in case a rollback may to be done
tierno65ca36d2019-02-12 19:27:52 +0100576 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200577 :param indata: data to be inserted
578 :param kwargs: used to override the indata descriptor
579 :param headers: http request headers
tiernobdebce92019-07-01 15:36:49 +0000580 :return: _id, op_id:
581 _id: identity of the inserted data.
582 op_id: operation id if this is asynchronous, None otherwise
tiernob24258a2018-10-04 18:39:49 +0200583 """
584 try:
delacruzramo32bab472019-09-13 12:24:22 +0200585 if self.multiproject:
586 self.check_quota(session)
587
tiernob24258a2018-10-04 18:39:49 +0200588 content = self._remove_envelop(indata)
589
590 # Override descriptor with query string kwargs
591 self._update_input_with_kwargs(content, kwargs)
tierno65ca36d2019-02-12 19:27:52 +0100592 content = self._validate_input_new(content, force=session["force"])
593 self.check_conflict_on_new(session, content)
garciadeblas4568a372021-03-24 09:19:48 +0100594 op_id = self.format_on_new(
595 content, project_id=session["project_id"], make_public=session["public"]
596 )
tiernob24258a2018-10-04 18:39:49 +0200597 _id = self.db.create(self.topic, content)
598 rollback.append({"topic": self.topic, "_id": _id})
tiernobdebce92019-07-01 15:36:49 +0000599 if op_id:
600 content["op_id"] = op_id
tierno15a1f682019-10-16 09:00:13 +0000601 self._send_msg("created", content)
tiernobdebce92019-07-01 15:36:49 +0000602 return _id, op_id
tiernob24258a2018-10-04 18:39:49 +0200603 except ValidationError as e:
604 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
605
tierno65ca36d2019-02-12 19:27:52 +0100606 def upload_content(self, session, _id, indata, kwargs, headers):
tiernob24258a2018-10-04 18:39:49 +0200607 """
608 Only implemented for descriptor topics. Used for receiving content by chunks (with a transaction_id header
609 and/or gzip file. It will store and extract)
tierno65ca36d2019-02-12 19:27:52 +0100610 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200611 :param _id : the database id of entry to be updated
612 :param indata: http body request
613 :param kwargs: user query string to override parameters. NOT USED
614 :param headers: http request headers
tiernob24258a2018-10-04 18:39:49 +0200615 :return: True package has is completely uploaded or False if partial content has been uplodaed.
616 Raise exception on error
617 """
garciadeblas4568a372021-03-24 09:19:48 +0100618 raise EngineException(
619 "Method upload_content not valid for this topic",
620 HTTPStatus.INTERNAL_SERVER_ERROR,
621 )
tiernob24258a2018-10-04 18:39:49 +0200622
623 def delete_list(self, session, filter_q=None):
624 """
625 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 +0100626 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200627 :param filter_q: filter of data to be applied
628 :return: The deleted list, it can be empty if no one match the filter.
629 """
630 # TODO add admin to filter, validate rights
631 if not filter_q:
632 filter_q = {}
tierno1f029d82019-06-13 22:37:04 +0000633 if self.multiproject:
634 filter_q.update(self._get_project_filter(session))
tiernob24258a2018-10-04 18:39:49 +0200635 return self.db.del_list(self.topic, filter_q)
636
tiernobee3bad2019-12-05 12:26:01 +0000637 def delete_extra(self, session, _id, db_content, not_send_msg=None):
tierno65ca36d2019-02-12 19:27:52 +0100638 """
639 Delete other things apart from database entry of a item _id.
640 e.g.: other associated elements at database and other file system storage
641 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
642 :param _id: server internal id
tiernob4844ab2019-05-23 08:42:12 +0000643 :param db_content: The database content of the _id. It is already deleted when reached this method, but the
644 content is needed in same cases
tiernobee3bad2019-12-05 12:26:01 +0000645 :param not_send_msg: To not send message (False) or store content (list) instead
tiernob4844ab2019-05-23 08:42:12 +0000646 :return: None if ok or raises EngineException with the problem
tierno65ca36d2019-02-12 19:27:52 +0100647 """
648 pass
649
shrinithi28d887f2025-01-08 05:27:19 +0000650 def delete_extra_before(self, session, _id, db_content, not_send_msg=None):
651 """
652 Delete other things apart from database entry of a item _id.
653 """
654 return {}
655
tiernobee3bad2019-12-05 12:26:01 +0000656 def delete(self, session, _id, dry_run=False, not_send_msg=None):
tiernob24258a2018-10-04 18:39:49 +0200657 """
658 Delete item by its internal _id
tierno65ca36d2019-02-12 19:27:52 +0100659 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200660 :param _id: server internal id
tiernob24258a2018-10-04 18:39:49 +0200661 :param dry_run: make checking but do not delete
tiernobee3bad2019-12-05 12:26:01 +0000662 :param not_send_msg: To not send message (False) or store content (list) instead
tiernobdebce92019-07-01 15:36:49 +0000663 :return: operation id (None if there is not operation), raise exception if error or not found, conflict, ...
tiernob24258a2018-10-04 18:39:49 +0200664 """
tiernob4844ab2019-05-23 08:42:12 +0000665 # To allow addressing projects and users by name AS WELL AS by _id
tiernof5f2e3f2020-03-23 14:42:10 +0000666 if not self.multiproject:
667 filter_q = {}
668 else:
669 filter_q = self._get_project_filter(session)
670 filter_q[self.id_field(self.topic, _id)] = _id
rshri2d386cb2024-07-05 14:35:51 +0000671
tiernob4844ab2019-05-23 08:42:12 +0000672 item_content = self.db.get_one(self.topic, filter_q)
kayal2001f71c2e82024-06-25 15:26:24 +0530673 nsd_id = item_content.get("_id")
tiernob4844ab2019-05-23 08:42:12 +0000674
tiernob4844ab2019-05-23 08:42:12 +0000675 self.check_conflict_on_del(session, _id, item_content)
kayal2001f71c2e82024-06-25 15:26:24 +0530676
677 # While deteling ns descriptor associated ns config template should also get deleted.
678 if self.topic == "nsds":
679 ns_config_template_content = self.db.get_list(
680 "ns_config_template", {"nsdId": _id}
681 )
682 for template_content in ns_config_template_content:
683 if template_content is not None:
684 if template_content.get("nsdId") == nsd_id:
685 ns_config_template_id = template_content.get("_id")
686 self.db.del_one("ns_config_template", {"nsdId": nsd_id})
687 self.delete_extra(
688 session,
689 ns_config_template_id,
690 template_content,
691 not_send_msg=not_send_msg,
692 )
tierno65ca36d2019-02-12 19:27:52 +0100693 if dry_run:
694 return None
695 if self.multiproject and session["project_id"]:
tiernof5f2e3f2020-03-23 14:42:10 +0000696 # remove reference from project_read if there are more projects referencing it. If it last one,
697 # do not remove reference, but delete
garciadeblas4568a372021-03-24 09:19:48 +0100698 other_projects_referencing = next(
699 (
700 p
701 for p in item_content["_admin"]["projects_read"]
702 if p not in session["project_id"] and p != "ANY"
703 ),
704 None,
705 )
tiernof5f2e3f2020-03-23 14:42:10 +0000706 # check if there are projects referencing it (apart from ANY, that means, public)....
707 if other_projects_referencing:
708 # remove references but not delete
garciadeblas4568a372021-03-24 09:19:48 +0100709 update_dict_pull = {
710 "_admin.projects_read": session["project_id"],
711 "_admin.projects_write": session["project_id"],
712 }
713 self.db.set_one(
714 self.topic, filter_q, update_dict=None, pull_list=update_dict_pull
715 )
tiernobdebce92019-07-01 15:36:49 +0000716 return None
tiernof5f2e3f2020-03-23 14:42:10 +0000717 else:
garciadeblas4568a372021-03-24 09:19:48 +0100718 can_write = next(
719 (
720 p
721 for p in item_content["_admin"]["projects_write"]
722 if p == "ANY" or p in session["project_id"]
723 ),
724 None,
725 )
tiernof5f2e3f2020-03-23 14:42:10 +0000726 if not can_write:
garciadeblas4568a372021-03-24 09:19:48 +0100727 raise EngineException(
728 "You have not write permission to delete it",
729 http_code=HTTPStatus.UNAUTHORIZED,
730 )
tiernof5f2e3f2020-03-23 14:42:10 +0000731 # delete
shrinithi28d887f2025-01-08 05:27:19 +0000732 different_message = self.delete_extra_before(
733 session, _id, item_content, not_send_msg=not_send_msg
734 )
735 # self.db.del_one(self.topic, filter_q)
736 # self.delete_extra(session, _id, item_content, not_send_msg=not_send_msg)
737 if different_message:
738 self.delete_extra(session, _id, item_content, not_send_msg=not_send_msg)
739 self._send_msg("delete", different_message, not_send_msg=not_send_msg)
rshri2d386cb2024-07-05 14:35:51 +0000740 else:
741 self.db.del_one(self.topic, filter_q)
742 self.delete_extra(session, _id, item_content, not_send_msg=not_send_msg)
743 self._send_msg("deleted", {"_id": _id}, not_send_msg=not_send_msg)
shrinithi28d887f2025-01-08 05:27:19 +0000744 return None
745
746 def edit_extra_before(self, session, _id, indata=None, kwargs=None, content=None):
747 """
748 edit other things apart from database entry of a item _id.
749 """
750 return {}
tiernob24258a2018-10-04 18:39:49 +0200751
tierno65ca36d2019-02-12 19:27:52 +0100752 def edit(self, session, _id, indata=None, kwargs=None, content=None):
753 """
754 Change the content of an item
755 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
756 :param _id: server internal id
757 :param indata: contains the changes to apply
758 :param kwargs: modifies indata
759 :param content: original content of the item
tiernobdebce92019-07-01 15:36:49 +0000760 :return: op_id: operation id if this is processed asynchronously, None otherwise
tierno65ca36d2019-02-12 19:27:52 +0100761 """
tiernob24258a2018-10-04 18:39:49 +0200762 indata = self._remove_envelop(indata)
763
764 # Override descriptor with query string kwargs
765 if kwargs:
766 self._update_input_with_kwargs(indata, kwargs)
767 try:
tierno65ca36d2019-02-12 19:27:52 +0100768 if indata and session.get("set_project"):
garciadeblas4568a372021-03-24 09:19:48 +0100769 raise EngineException(
770 "Cannot edit content and set to project (query string SET_PROJECT) at same time",
771 HTTPStatus.UNPROCESSABLE_ENTITY,
772 )
tiernob24258a2018-10-04 18:39:49 +0200773 # TODO self._check_edition(session, indata, _id, force)
774 if not content:
775 content = self.show(session, _id)
Frank Brydendeba68e2020-07-27 13:55:11 +0000776 indata = self._validate_input_edit(indata, content, force=session["force"])
tiernob24258a2018-10-04 18:39:49 +0200777 deep_update_rfc7396(content, indata)
tiernobdebce92019-07-01 15:36:49 +0000778
779 # To allow project addressing by name AS WELL AS _id. Get the _id, just in case the provided one is a name
780 _id = content.get("_id") or _id
781
bravofb995ea22021-02-10 10:57:52 -0300782 content = self.check_conflict_on_edit(session, content, indata, _id=_id)
tiernobdebce92019-07-01 15:36:49 +0000783 op_id = self.format_on_edit(content, indata)
784
shrinithi28d887f2025-01-08 05:27:19 +0000785 self.logger.info(f"indata is : {indata}")
786
787 different_message = self.edit_extra_before(
788 session, _id, indata, kwargs=None, content=None
789 )
790 self.logger.info(f"different msg is : {different_message}")
791
tiernobdebce92019-07-01 15:36:49 +0000792 self.db.replace(self.topic, _id, content)
tiernob24258a2018-10-04 18:39:49 +0200793
794 indata.pop("_admin", None)
tiernobdebce92019-07-01 15:36:49 +0000795 if op_id:
796 indata["op_id"] = op_id
tiernob24258a2018-10-04 18:39:49 +0200797 indata["_id"] = _id
shrinithi28d887f2025-01-08 05:27:19 +0000798
799 if different_message:
800 self.logger.info("It is getting into if")
rshri2d386cb2024-07-05 14:35:51 +0000801 pass
802 else:
shrinithi28d887f2025-01-08 05:27:19 +0000803 self.logger.info("It is getting into else")
rshri2d386cb2024-07-05 14:35:51 +0000804 self._send_msg("edited", indata)
tiernobdebce92019-07-01 15:36:49 +0000805 return op_id
tiernob24258a2018-10-04 18:39:49 +0200806 except ValidationError as e:
807 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
rshri2d386cb2024-07-05 14:35:51 +0000808
shrinithi28d887f2025-01-08 05:27:19 +0000809 def create_gitname(self, content, session, _id=None):
810 if not self.multiproject:
811 _filter = {}
rshri50e34dc2024-12-02 03:10:39 +0000812 else:
shrinithi28d887f2025-01-08 05:27:19 +0000813 _filter = self._get_project_filter(session)
garciadeblasfefe2982025-01-24 13:36:13 +0100814 _filter["git_name"] = content["name"].lower()
shrinithi28d887f2025-01-08 05:27:19 +0000815 if _id:
816 _filter["_id.neq"] = _id
817 if self.db.get_one(
818 self.topic, _filter, fail_on_empty=False, fail_on_more=False
819 ):
820 n = 5
821 # using random.choices()
822 # generating random strings
823 res = "".join(random.choices(string.ascii_lowercase + string.digits, k=n))
garciadeblasfefe2982025-01-24 13:36:13 +0100824 new_name = (content["name"] + res).lower()
shrinithi28d887f2025-01-08 05:27:19 +0000825 return new_name
rshri50e34dc2024-12-02 03:10:39 +0000826 else:
garciadeblasfefe2982025-01-24 13:36:13 +0100827 return content["name"].lower()