blob: f597d17a1d09e61c8ede7d4f9c9f8ed182c46fa1 [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
20from osm_common.dbbase import deep_update_rfc7396
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):
28
29 def __init__(self, message, http_code=HTTPStatus.BAD_REQUEST):
30 self.http_code = http_code
tierno23acf402019-08-28 13:36:34 +000031 super(Exception, self).__init__(message)
tiernob24258a2018-10-04 18:39:49 +020032
33
tierno714954e2019-11-29 13:43:26 +000034def deep_get(target_dict, key_list):
35 """
36 Get a value from target_dict entering in the nested keys. If keys does not exist, it returns None
37 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
38 :param target_dict: dictionary to be read
39 :param key_list: list of keys to read from target_dict
40 :return: The wanted value if exist, None otherwise
41 """
42 for key in key_list:
43 if not isinstance(target_dict, dict) or key not in target_dict:
44 return None
45 target_dict = target_dict[key]
46 return target_dict
47
48
tiernob24258a2018-10-04 18:39:49 +020049def get_iterable(input_var):
50 """
51 Returns an iterable, in case input_var is None it just returns an empty tuple
52 :param input_var: can be a list, tuple or None
53 :return: input_var or () if it is None
54 """
55 if input_var is None:
56 return ()
57 return input_var
58
59
60def versiontuple(v):
61 """utility for compare dot separate versions. Fills with zeros to proper number comparison"""
62 filled = []
63 for point in v.split("."):
64 filled.append(point.zfill(8))
65 return tuple(filled)
66
67
tiernocddb07d2020-10-06 08:28:00 +000068def increment_ip_mac(ip_mac, vm_index=1):
69 if not isinstance(ip_mac, str):
70 return ip_mac
71 try:
72 # try with ipv4 look for last dot
73 i = ip_mac.rfind(".")
74 if i > 0:
75 i += 1
76 return "{}{}".format(ip_mac[:i], int(ip_mac[i:]) + vm_index)
77 # try with ipv6 or mac look for last colon. Operate in hex
78 i = ip_mac.rfind(":")
79 if i > 0:
80 i += 1
81 # format in hex, len can be 2 for mac or 4 for ipv6
82 return ("{}{:0" + str(len(ip_mac) - i) + "x}").format(ip_mac[:i], int(ip_mac[i:], 16) + vm_index)
83 except Exception:
84 pass
85 return None
86
87
tiernob24258a2018-10-04 18:39:49 +020088class BaseTopic:
89 # static variables for all instance classes
90 topic = None # to_override
91 topic_msg = None # to_override
tierno6b02b052020-06-02 10:07:41 +000092 quota_name = None # to_override. If not provided topic will be used for quota_name
tiernob24258a2018-10-04 18:39:49 +020093 schema_new = None # to_override
94 schema_edit = None # to_override
tierno65ca36d2019-02-12 19:27:52 +010095 multiproject = True # True if this Topic can be shared by several projects. Then it contains _admin.projects_read
tiernob24258a2018-10-04 18:39:49 +020096
delacruzramo32bab472019-09-13 12:24:22 +020097 default_quota = 500
98
delacruzramoc061f562019-04-05 11:00:02 +020099 # Alternative ID Fields for some Topics
100 alt_id_field = {
101 "projects": "name",
tiernocf042d32019-06-13 09:06:40 +0000102 "users": "username",
delacruzramo01b15d32019-07-02 14:37:47 +0200103 "roles": "name"
delacruzramoc061f562019-04-05 11:00:02 +0200104 }
105
delacruzramo32bab472019-09-13 12:24:22 +0200106 def __init__(self, db, fs, msg, auth):
tiernob24258a2018-10-04 18:39:49 +0200107 self.db = db
108 self.fs = fs
109 self.msg = msg
110 self.logger = logging.getLogger("nbi.engine")
delacruzramo32bab472019-09-13 12:24:22 +0200111 self.auth = auth
tiernob24258a2018-10-04 18:39:49 +0200112
113 @staticmethod
delacruzramoc061f562019-04-05 11:00:02 +0200114 def id_field(topic, value):
tierno65ca36d2019-02-12 19:27:52 +0100115 """Returns ID Field for given topic and field value"""
delacruzramoceb8baf2019-06-21 14:25:38 +0200116 if topic in BaseTopic.alt_id_field.keys() and not is_valid_uuid(value):
delacruzramoc061f562019-04-05 11:00:02 +0200117 return BaseTopic.alt_id_field[topic]
118 else:
119 return "_id"
120
121 @staticmethod
tiernob24258a2018-10-04 18:39:49 +0200122 def _remove_envelop(indata=None):
123 if not indata:
124 return {}
125 return indata
126
delacruzramo32bab472019-09-13 12:24:22 +0200127 def check_quota(self, session):
128 """
129 Check whether topic quota is exceeded by the given project
130 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 +0000131 :param session[project_id]: projects (tuple) for which quota should be checked
132 :param session[force]: boolean. If true, skip quota checking
delacruzramo32bab472019-09-13 12:24:22 +0200133 :return: None
134 :raise:
135 DbException if project not found
tierno6b02b052020-06-02 10:07:41 +0000136 ValidationError if quota exceeded in one of the projects
delacruzramo32bab472019-09-13 12:24:22 +0200137 """
tiernod7749582020-05-28 10:41:10 +0000138 if session["force"]:
delacruzramo32bab472019-09-13 12:24:22 +0200139 return
140 projects = session["project_id"]
141 for project in projects:
142 proj = self.auth.get_project(project)
143 pid = proj["_id"]
tierno6b02b052020-06-02 10:07:41 +0000144 quota_name = self.quota_name or self.topic
145 quota = proj.get("quotas", {}).get(quota_name, self.default_quota)
delacruzramo32bab472019-09-13 12:24:22 +0200146 count = self.db.count(self.topic, {"_admin.projects_read": pid})
147 if count >= quota:
148 name = proj["name"]
tierno6b02b052020-06-02 10:07:41 +0000149 raise ValidationError("quota ({}={}) exceeded for project {} ({})".format(quota_name, quota, name, pid),
garciadeblas4ab6b3f2020-10-08 15:25:45 +0000150 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
delacruzramo32bab472019-09-13 12:24:22 +0200151
tiernob24258a2018-10-04 18:39:49 +0200152 def _validate_input_new(self, input, force=False):
153 """
154 Validates input user content for a new entry. It uses jsonschema. Some overrides will use pyangbind
155 :param input: user input content for the new topic
156 :param force: may be used for being more tolerant
157 :return: The same input content, or a changed version of it.
158 """
159 if self.schema_new:
160 validate_input(input, self.schema_new)
161 return input
162
Frank Brydendeba68e2020-07-27 13:55:11 +0000163 def _validate_input_edit(self, input, content, force=False):
tiernob24258a2018-10-04 18:39:49 +0200164 """
165 Validates input user content for an edition. It uses jsonschema. Some overrides will use pyangbind
166 :param input: user input content for the new topic
167 :param force: may be used for being more tolerant
168 :return: The same input content, or a changed version of it.
169 """
170 if self.schema_edit:
171 validate_input(input, self.schema_edit)
172 return input
173
174 @staticmethod
tierno65ca36d2019-02-12 19:27:52 +0100175 def _get_project_filter(session):
tiernob24258a2018-10-04 18:39:49 +0200176 """
177 Generates a filter dictionary for querying database, so that only allowed items for this project can be
tiernof5f2e3f2020-03-23 14:42:10 +0000178 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 +0200179 not present or contains ANY mean public.
tierno65ca36d2019-02-12 19:27:52 +0100180 :param session: contains:
181 project_id: project list this session has rights to access. Can be empty, one or several
182 set_project: items created will contain this project list
183 force: True or False
184 public: True, False or None
185 method: "list", "show", "write", "delete"
186 admin: True or False
187 :return: dictionary with project filter
tiernob24258a2018-10-04 18:39:49 +0200188 """
tierno65ca36d2019-02-12 19:27:52 +0100189 p_filter = {}
190 project_filter_n = []
191 project_filter = list(session["project_id"])
tiernob24258a2018-10-04 18:39:49 +0200192
tierno65ca36d2019-02-12 19:27:52 +0100193 if session["method"] not in ("list", "delete"):
194 if project_filter:
195 project_filter.append("ANY")
196 elif session["public"] is not None:
197 if session["public"]:
198 project_filter.append("ANY")
199 else:
200 project_filter_n.append("ANY")
201
202 if session.get("PROJECT.ne"):
203 project_filter_n.append(session["PROJECT.ne"])
204
205 if project_filter:
206 if session["method"] in ("list", "show", "delete") or session.get("set_project"):
207 p_filter["_admin.projects_read.cont"] = project_filter
208 else:
209 p_filter["_admin.projects_write.cont"] = project_filter
210 if project_filter_n:
211 if session["method"] in ("list", "show", "delete") or session.get("set_project"):
212 p_filter["_admin.projects_read.ncont"] = project_filter_n
213 else:
214 p_filter["_admin.projects_write.ncont"] = project_filter_n
215
216 return p_filter
217
218 def check_conflict_on_new(self, session, indata):
tiernob24258a2018-10-04 18:39:49 +0200219 """
220 Check that the data to be inserted is valid
tierno65ca36d2019-02-12 19:27:52 +0100221 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200222 :param indata: data to be inserted
tiernob24258a2018-10-04 18:39:49 +0200223 :return: None or raises EngineException
224 """
225 pass
226
tierno65ca36d2019-02-12 19:27:52 +0100227 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
tiernob24258a2018-10-04 18:39:49 +0200228 """
229 Check that the data to be edited/uploaded is valid
tierno65ca36d2019-02-12 19:27:52 +0100230 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernobdebce92019-07-01 15:36:49 +0000231 :param final_content: data once modified. This method may change it.
tiernob24258a2018-10-04 18:39:49 +0200232 :param edit_content: incremental data that contains the modifications to apply
233 :param _id: internal _id
bravofb995ea22021-02-10 10:57:52 -0300234 :return: final_content or raises EngineException
tiernob24258a2018-10-04 18:39:49 +0200235 """
tierno65ca36d2019-02-12 19:27:52 +0100236 if not self.multiproject:
bravofb995ea22021-02-10 10:57:52 -0300237 return final_content
tierno65ca36d2019-02-12 19:27:52 +0100238 # Change public status
239 if session["public"] is not None:
240 if session["public"] and "ANY" not in final_content["_admin"]["projects_read"]:
241 final_content["_admin"]["projects_read"].append("ANY")
242 final_content["_admin"]["projects_write"].clear()
243 if not session["public"] and "ANY" in final_content["_admin"]["projects_read"]:
244 final_content["_admin"]["projects_read"].remove("ANY")
245
246 # Change project status
247 if session.get("set_project"):
248 for p in session["set_project"]:
249 if p not in final_content["_admin"]["projects_read"]:
250 final_content["_admin"]["projects_read"].append(p)
tiernob24258a2018-10-04 18:39:49 +0200251
bravofb995ea22021-02-10 10:57:52 -0300252 return final_content
253
tiernob24258a2018-10-04 18:39:49 +0200254 def check_unique_name(self, session, name, _id=None):
255 """
256 Check that the name is unique for this project
tierno65ca36d2019-02-12 19:27:52 +0100257 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200258 :param name: name to be checked
259 :param _id: If not None, ignore this entry that are going to change
260 :return: None or raises EngineException
261 """
tierno1f029d82019-06-13 22:37:04 +0000262 if not self.multiproject:
263 _filter = {}
264 else:
265 _filter = self._get_project_filter(session)
tiernob24258a2018-10-04 18:39:49 +0200266 _filter["name"] = name
267 if _id:
268 _filter["_id.neq"] = _id
269 if self.db.get_one(self.topic, _filter, fail_on_empty=False, fail_on_more=False):
270 raise EngineException("name '{}' already exists for {}".format(name, self.topic), HTTPStatus.CONFLICT)
271
272 @staticmethod
273 def format_on_new(content, project_id=None, make_public=False):
274 """
275 Modifies content descriptor to include _admin
276 :param content: descriptor to be modified
tierno65ca36d2019-02-12 19:27:52 +0100277 :param project_id: if included, it add project read/write permissions. Can be None or a list
tiernob24258a2018-10-04 18:39:49 +0200278 :param make_public: if included it is generated as public for reading.
tiernobdebce92019-07-01 15:36:49 +0000279 :return: op_id: operation id on asynchronous operation, None otherwise. In addition content is modified
tiernob24258a2018-10-04 18:39:49 +0200280 """
281 now = time()
282 if "_admin" not in content:
283 content["_admin"] = {}
284 if not content["_admin"].get("created"):
285 content["_admin"]["created"] = now
286 content["_admin"]["modified"] = now
287 if not content.get("_id"):
288 content["_id"] = str(uuid4())
tierno65ca36d2019-02-12 19:27:52 +0100289 if project_id is not None:
tiernob24258a2018-10-04 18:39:49 +0200290 if not content["_admin"].get("projects_read"):
tierno65ca36d2019-02-12 19:27:52 +0100291 content["_admin"]["projects_read"] = list(project_id)
tiernob24258a2018-10-04 18:39:49 +0200292 if make_public:
293 content["_admin"]["projects_read"].append("ANY")
294 if not content["_admin"].get("projects_write"):
tierno65ca36d2019-02-12 19:27:52 +0100295 content["_admin"]["projects_write"] = list(project_id)
tiernobdebce92019-07-01 15:36:49 +0000296 return None
tiernob24258a2018-10-04 18:39:49 +0200297
298 @staticmethod
299 def format_on_edit(final_content, edit_content):
tiernobdebce92019-07-01 15:36:49 +0000300 """
301 Modifies final_content to admin information upon edition
302 :param final_content: final content to be stored at database
303 :param edit_content: user requested update content
304 :return: operation id, if this edit implies an asynchronous operation; None otherwise
305 """
tiernob24258a2018-10-04 18:39:49 +0200306 if final_content.get("_admin"):
307 now = time()
308 final_content["_admin"]["modified"] = now
tiernobdebce92019-07-01 15:36:49 +0000309 return None
tiernob24258a2018-10-04 18:39:49 +0200310
tiernobee3bad2019-12-05 12:26:01 +0000311 def _send_msg(self, action, content, not_send_msg=None):
312 if self.topic_msg and not_send_msg is not False:
agarwalat53471982020-10-08 13:06:14 +0000313 content = content.copy()
tiernob24258a2018-10-04 18:39:49 +0200314 content.pop("_admin", None)
tiernobee3bad2019-12-05 12:26:01 +0000315 if isinstance(not_send_msg, list):
316 not_send_msg.append((self.topic_msg, action, content))
317 else:
318 self.msg.write(self.topic_msg, action, content)
tiernob24258a2018-10-04 18:39:49 +0200319
tiernob4844ab2019-05-23 08:42:12 +0000320 def check_conflict_on_del(self, session, _id, db_content):
tiernob24258a2018-10-04 18:39:49 +0200321 """
322 Check if deletion can be done because of dependencies if it is not force. To override
tierno65ca36d2019-02-12 19:27:52 +0100323 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
324 :param _id: internal _id
tiernob4844ab2019-05-23 08:42:12 +0000325 :param db_content: The database content of this item _id
tiernob24258a2018-10-04 18:39:49 +0200326 :return: None if ok or raises EngineException with the conflict
327 """
328 pass
329
330 @staticmethod
tierno1c38f2f2020-03-24 11:51:39 +0000331 def _update_input_with_kwargs(desc, kwargs, yaml_format=False):
tiernob24258a2018-10-04 18:39:49 +0200332 """
333 Update descriptor with the kwargs. It contains dot separated keys
334 :param desc: dictionary to be updated
335 :param kwargs: plain dictionary to be used for updating.
tierno1c38f2f2020-03-24 11:51:39 +0000336 :param yaml_format: get kwargs values as yaml format.
delacruzramoc061f562019-04-05 11:00:02 +0200337 :return: None, 'desc' is modified. It raises EngineException.
tiernob24258a2018-10-04 18:39:49 +0200338 """
339 if not kwargs:
340 return
341 try:
342 for k, v in kwargs.items():
343 update_content = desc
344 kitem_old = None
345 klist = k.split(".")
346 for kitem in klist:
347 if kitem_old is not None:
348 update_content = update_content[kitem_old]
349 if isinstance(update_content, dict):
350 kitem_old = kitem
tiernoac55f062020-06-17 07:42:30 +0000351 if not isinstance(update_content.get(kitem_old), (dict, list)):
352 update_content[kitem_old] = {}
tiernob24258a2018-10-04 18:39:49 +0200353 elif isinstance(update_content, list):
tiernoac55f062020-06-17 07:42:30 +0000354 # key must be an index of the list, must be integer
tiernob24258a2018-10-04 18:39:49 +0200355 kitem_old = int(kitem)
tiernoac55f062020-06-17 07:42:30 +0000356 # if index greater than list, extend the list
357 if kitem_old >= len(update_content):
358 update_content += [None] * (kitem_old - len(update_content) + 1)
359 if not isinstance(update_content[kitem_old], (dict, list)):
360 update_content[kitem_old] = {}
tiernob24258a2018-10-04 18:39:49 +0200361 else:
362 raise EngineException(
363 "Invalid query string '{}'. Descriptor is not a list nor dict at '{}'".format(k, kitem))
tiernoac55f062020-06-17 07:42:30 +0000364 if v is None:
365 del update_content[kitem_old]
366 else:
367 update_content[kitem_old] = v if not yaml_format else safe_load(v)
tiernob24258a2018-10-04 18:39:49 +0200368 except KeyError:
369 raise EngineException(
370 "Invalid query string '{}'. Descriptor does not contain '{}'".format(k, kitem_old))
371 except ValueError:
372 raise EngineException("Invalid query string '{}'. Expected integer index list instead of '{}'".format(
373 k, kitem))
374 except IndexError:
375 raise EngineException(
376 "Invalid query string '{}'. Index '{}' out of range".format(k, kitem_old))
tierno1c38f2f2020-03-24 11:51:39 +0000377 except YAMLError:
378 raise EngineException("Invalid query string '{}' yaml format".format(k))
tiernob24258a2018-10-04 18:39:49 +0200379
Frank Bryden19b97522020-07-10 12:32:02 +0000380 def sol005_projection(self, data):
381 # Projection was moved to child classes
382 return data
383
384 def show(self, session, _id, api_req=False):
tiernob24258a2018-10-04 18:39:49 +0200385 """
386 Get complete information on an topic
tierno65ca36d2019-02-12 19:27:52 +0100387 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200388 :param _id: server internal id
Frank Bryden19b97522020-07-10 12:32:02 +0000389 :param api_req: True if this call is serving an external API request. False if serving internal request.
tiernob24258a2018-10-04 18:39:49 +0200390 :return: dictionary, raise exception if not found.
391 """
tierno1f029d82019-06-13 22:37:04 +0000392 if not self.multiproject:
393 filter_db = {}
394 else:
395 filter_db = self._get_project_filter(session)
delacruzramoc061f562019-04-05 11:00:02 +0200396 # To allow project&user addressing by name AS WELL AS _id
397 filter_db[BaseTopic.id_field(self.topic, _id)] = _id
Frank Bryden19b97522020-07-10 12:32:02 +0000398 data = self.db.get_one(self.topic, filter_db)
399
400 # Only perform SOL005 projection if we are serving an external request
401 if api_req:
402 self.sol005_projection(data)
403
404 return data
405
tiernob24258a2018-10-04 18:39:49 +0200406 # TODO transform data for SOL005 URL requests
407 # TODO remove _admin if not admin
408
409 def get_file(self, session, _id, path=None, accept_header=None):
410 """
411 Only implemented for descriptor topics. Return the file content of a descriptor
tierno65ca36d2019-02-12 19:27:52 +0100412 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200413 :param _id: Identity of the item to get content
414 :param path: artifact path or "$DESCRIPTOR" or None
415 :param accept_header: Content of Accept header. Must contain applition/zip or/and text/plain
416 :return: opened file or raises an exception
417 """
418 raise EngineException("Method get_file not valid for this topic", HTTPStatus.INTERNAL_SERVER_ERROR)
419
Frank Bryden19b97522020-07-10 12:32:02 +0000420 def list(self, session, filter_q=None, api_req=False):
tiernob24258a2018-10-04 18:39:49 +0200421 """
422 Get a list of the topic that matches a filter
423 :param session: contains the used login username and working project
424 :param filter_q: filter of data to be applied
Frank Bryden19b97522020-07-10 12:32:02 +0000425 :param api_req: True if this call is serving an external API request. False if serving internal request.
tiernob24258a2018-10-04 18:39:49 +0200426 :return: The list, it can be empty if no one match the filter.
427 """
428 if not filter_q:
429 filter_q = {}
tierno1f029d82019-06-13 22:37:04 +0000430 if self.multiproject:
431 filter_q.update(self._get_project_filter(session))
tiernob24258a2018-10-04 18:39:49 +0200432
433 # TODO transform data for SOL005 URL requests. Transform filtering
434 # TODO implement "field-type" query string SOL005
Frank Bryden19b97522020-07-10 12:32:02 +0000435 data = self.db.get_list(self.topic, filter_q)
436
437 # Only perform SOL005 projection if we are serving an external request
438 if api_req:
439 data = [self.sol005_projection(inst) for inst in data]
440
441 return data
tiernob24258a2018-10-04 18:39:49 +0200442
tierno65ca36d2019-02-12 19:27:52 +0100443 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
tiernob24258a2018-10-04 18:39:49 +0200444 """
445 Creates a new entry into database.
446 :param rollback: list to append created items at database in case a rollback may to be done
tierno65ca36d2019-02-12 19:27:52 +0100447 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200448 :param indata: data to be inserted
449 :param kwargs: used to override the indata descriptor
450 :param headers: http request headers
tiernobdebce92019-07-01 15:36:49 +0000451 :return: _id, op_id:
452 _id: identity of the inserted data.
453 op_id: operation id if this is asynchronous, None otherwise
tiernob24258a2018-10-04 18:39:49 +0200454 """
455 try:
delacruzramo32bab472019-09-13 12:24:22 +0200456 if self.multiproject:
457 self.check_quota(session)
458
tiernob24258a2018-10-04 18:39:49 +0200459 content = self._remove_envelop(indata)
460
461 # Override descriptor with query string kwargs
462 self._update_input_with_kwargs(content, kwargs)
tierno65ca36d2019-02-12 19:27:52 +0100463 content = self._validate_input_new(content, force=session["force"])
464 self.check_conflict_on_new(session, content)
tiernobdebce92019-07-01 15:36:49 +0000465 op_id = self.format_on_new(content, project_id=session["project_id"], make_public=session["public"])
tiernob24258a2018-10-04 18:39:49 +0200466 _id = self.db.create(self.topic, content)
467 rollback.append({"topic": self.topic, "_id": _id})
tiernobdebce92019-07-01 15:36:49 +0000468 if op_id:
469 content["op_id"] = op_id
tierno15a1f682019-10-16 09:00:13 +0000470 self._send_msg("created", content)
tiernobdebce92019-07-01 15:36:49 +0000471 return _id, op_id
tiernob24258a2018-10-04 18:39:49 +0200472 except ValidationError as e:
473 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
474
tierno65ca36d2019-02-12 19:27:52 +0100475 def upload_content(self, session, _id, indata, kwargs, headers):
tiernob24258a2018-10-04 18:39:49 +0200476 """
477 Only implemented for descriptor topics. Used for receiving content by chunks (with a transaction_id header
478 and/or gzip file. It will store and extract)
tierno65ca36d2019-02-12 19:27:52 +0100479 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200480 :param _id : the database id of entry to be updated
481 :param indata: http body request
482 :param kwargs: user query string to override parameters. NOT USED
483 :param headers: http request headers
tiernob24258a2018-10-04 18:39:49 +0200484 :return: True package has is completely uploaded or False if partial content has been uplodaed.
485 Raise exception on error
486 """
487 raise EngineException("Method upload_content not valid for this topic", HTTPStatus.INTERNAL_SERVER_ERROR)
488
489 def delete_list(self, session, filter_q=None):
490 """
491 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 +0100492 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200493 :param filter_q: filter of data to be applied
494 :return: The deleted list, it can be empty if no one match the filter.
495 """
496 # TODO add admin to filter, validate rights
497 if not filter_q:
498 filter_q = {}
tierno1f029d82019-06-13 22:37:04 +0000499 if self.multiproject:
500 filter_q.update(self._get_project_filter(session))
tiernob24258a2018-10-04 18:39:49 +0200501 return self.db.del_list(self.topic, filter_q)
502
tiernobee3bad2019-12-05 12:26:01 +0000503 def delete_extra(self, session, _id, db_content, not_send_msg=None):
tierno65ca36d2019-02-12 19:27:52 +0100504 """
505 Delete other things apart from database entry of a item _id.
506 e.g.: other associated elements at database and other file system storage
507 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
508 :param _id: server internal id
tiernob4844ab2019-05-23 08:42:12 +0000509 :param db_content: The database content of the _id. It is already deleted when reached this method, but the
510 content is needed in same cases
tiernobee3bad2019-12-05 12:26:01 +0000511 :param not_send_msg: To not send message (False) or store content (list) instead
tiernob4844ab2019-05-23 08:42:12 +0000512 :return: None if ok or raises EngineException with the problem
tierno65ca36d2019-02-12 19:27:52 +0100513 """
514 pass
515
tiernobee3bad2019-12-05 12:26:01 +0000516 def delete(self, session, _id, dry_run=False, not_send_msg=None):
tiernob24258a2018-10-04 18:39:49 +0200517 """
518 Delete item by its internal _id
tierno65ca36d2019-02-12 19:27:52 +0100519 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200520 :param _id: server internal id
tiernob24258a2018-10-04 18:39:49 +0200521 :param dry_run: make checking but do not delete
tiernobee3bad2019-12-05 12:26:01 +0000522 :param not_send_msg: To not send message (False) or store content (list) instead
tiernobdebce92019-07-01 15:36:49 +0000523 :return: operation id (None if there is not operation), raise exception if error or not found, conflict, ...
tiernob24258a2018-10-04 18:39:49 +0200524 """
tiernob4844ab2019-05-23 08:42:12 +0000525
526 # To allow addressing projects and users by name AS WELL AS by _id
tiernof5f2e3f2020-03-23 14:42:10 +0000527 if not self.multiproject:
528 filter_q = {}
529 else:
530 filter_q = self._get_project_filter(session)
531 filter_q[self.id_field(self.topic, _id)] = _id
tiernob4844ab2019-05-23 08:42:12 +0000532 item_content = self.db.get_one(self.topic, filter_q)
533
tiernob4844ab2019-05-23 08:42:12 +0000534 self.check_conflict_on_del(session, _id, item_content)
tierno65ca36d2019-02-12 19:27:52 +0100535 if dry_run:
536 return None
tiernob4844ab2019-05-23 08:42:12 +0000537
tierno65ca36d2019-02-12 19:27:52 +0100538 if self.multiproject and session["project_id"]:
tiernof5f2e3f2020-03-23 14:42:10 +0000539 # remove reference from project_read if there are more projects referencing it. If it last one,
540 # do not remove reference, but delete
541 other_projects_referencing = next((p for p in item_content["_admin"]["projects_read"]
tierno20e74d22020-06-22 12:17:22 +0000542 if p not in session["project_id"] and p != "ANY"), None)
tiernof5f2e3f2020-03-23 14:42:10 +0000543
544 # check if there are projects referencing it (apart from ANY, that means, public)....
545 if other_projects_referencing:
546 # remove references but not delete
tierno20e74d22020-06-22 12:17:22 +0000547 update_dict_pull = {"_admin.projects_read": session["project_id"],
548 "_admin.projects_write": session["project_id"]}
549 self.db.set_one(self.topic, filter_q, update_dict=None, pull_list=update_dict_pull)
tiernobdebce92019-07-01 15:36:49 +0000550 return None
tiernof5f2e3f2020-03-23 14:42:10 +0000551 else:
552 can_write = next((p for p in item_content["_admin"]["projects_write"] if p == "ANY" or
553 p in session["project_id"]), None)
554 if not can_write:
555 raise EngineException("You have not write permission to delete it",
556 http_code=HTTPStatus.UNAUTHORIZED)
557
558 # delete
559 self.db.del_one(self.topic, filter_q)
tiernobee3bad2019-12-05 12:26:01 +0000560 self.delete_extra(session, _id, item_content, not_send_msg=not_send_msg)
561 self._send_msg("deleted", {"_id": _id}, not_send_msg=not_send_msg)
tiernobdebce92019-07-01 15:36:49 +0000562 return None
tiernob24258a2018-10-04 18:39:49 +0200563
tierno65ca36d2019-02-12 19:27:52 +0100564 def edit(self, session, _id, indata=None, kwargs=None, content=None):
565 """
566 Change the content of an item
567 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
568 :param _id: server internal id
569 :param indata: contains the changes to apply
570 :param kwargs: modifies indata
571 :param content: original content of the item
tiernobdebce92019-07-01 15:36:49 +0000572 :return: op_id: operation id if this is processed asynchronously, None otherwise
tierno65ca36d2019-02-12 19:27:52 +0100573 """
tiernob24258a2018-10-04 18:39:49 +0200574 indata = self._remove_envelop(indata)
575
576 # Override descriptor with query string kwargs
577 if kwargs:
578 self._update_input_with_kwargs(indata, kwargs)
579 try:
tierno65ca36d2019-02-12 19:27:52 +0100580 if indata and session.get("set_project"):
581 raise EngineException("Cannot edit content and set to project (query string SET_PROJECT) at same time",
582 HTTPStatus.UNPROCESSABLE_ENTITY)
tiernob24258a2018-10-04 18:39:49 +0200583 # TODO self._check_edition(session, indata, _id, force)
584 if not content:
585 content = self.show(session, _id)
Frank Brydendeba68e2020-07-27 13:55:11 +0000586 indata = self._validate_input_edit(indata, content, force=session["force"])
tiernob24258a2018-10-04 18:39:49 +0200587 deep_update_rfc7396(content, indata)
tiernobdebce92019-07-01 15:36:49 +0000588
589 # To allow project addressing by name AS WELL AS _id. Get the _id, just in case the provided one is a name
590 _id = content.get("_id") or _id
591
bravofb995ea22021-02-10 10:57:52 -0300592 content = self.check_conflict_on_edit(session, content, indata, _id=_id)
tiernobdebce92019-07-01 15:36:49 +0000593 op_id = self.format_on_edit(content, indata)
594
595 self.db.replace(self.topic, _id, content)
tiernob24258a2018-10-04 18:39:49 +0200596
597 indata.pop("_admin", None)
tiernobdebce92019-07-01 15:36:49 +0000598 if op_id:
599 indata["op_id"] = op_id
tiernob24258a2018-10-04 18:39:49 +0200600 indata["_id"] = _id
tierno15a1f682019-10-16 09:00:13 +0000601 self._send_msg("edited", indata)
tiernobdebce92019-07-01 15:36:49 +0000602 return op_id
tiernob24258a2018-10-04 18:39:49 +0200603 except ValidationError as e:
604 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)