1 |
|
# -*- coding: utf-8 -*- |
2 |
|
|
3 |
|
# 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 |
|
|
16 |
1 |
import logging |
17 |
1 |
from uuid import uuid4 |
18 |
1 |
from http import HTTPStatus |
19 |
1 |
from time import time |
20 |
1 |
from osm_common.dbbase import deep_update_rfc7396, DbException |
21 |
1 |
from osm_nbi.validation import validate_input, ValidationError, is_valid_uuid |
22 |
1 |
from yaml import safe_load, YAMLError |
23 |
|
|
24 |
1 |
__author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>" |
25 |
|
|
26 |
|
|
27 |
1 |
class EngineException(Exception): |
28 |
1 |
def __init__(self, message, http_code=HTTPStatus.BAD_REQUEST): |
29 |
1 |
self.http_code = http_code |
30 |
1 |
super(Exception, self).__init__(message) |
31 |
|
|
32 |
|
|
33 |
1 |
class NBIBadArgumentsException(Exception): |
34 |
|
""" |
35 |
|
Bad argument values exception |
36 |
|
""" |
37 |
|
|
38 |
1 |
def __init__(self, message: str = "", bad_args: list = None): |
39 |
1 |
Exception.__init__(self, message) |
40 |
1 |
self.message = message |
41 |
1 |
self.bad_args = bad_args |
42 |
|
|
43 |
1 |
def __str__(self): |
44 |
1 |
return "{}, Bad arguments: {}".format(self.message, self.bad_args) |
45 |
|
|
46 |
|
|
47 |
1 |
def 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 |
1 |
for key in key_list: |
56 |
1 |
if not isinstance(target_dict, dict) or key not in target_dict: |
57 |
1 |
return None |
58 |
0 |
target_dict = target_dict[key] |
59 |
0 |
return target_dict |
60 |
|
|
61 |
|
|
62 |
1 |
def detect_descriptor_usage(descriptor: dict, db_collection: str, db: object) -> bool: |
63 |
|
"""Detect the descriptor usage state. |
64 |
|
|
65 |
|
Args: |
66 |
|
descriptor (dict): VNF or NS Descriptor as dictionary |
67 |
|
db_collection (str): collection name which is looked for in DB |
68 |
|
db (object): name of db object |
69 |
|
|
70 |
|
Returns: |
71 |
|
True if descriptor is in use else None |
72 |
|
|
73 |
|
""" |
74 |
1 |
try: |
75 |
1 |
if not descriptor: |
76 |
1 |
raise NBIBadArgumentsException( |
77 |
|
"Argument is mandatory and can not be empty", "descriptor" |
78 |
|
) |
79 |
|
|
80 |
1 |
if not db: |
81 |
1 |
raise NBIBadArgumentsException("A valid DB object should be provided", "db") |
82 |
|
|
83 |
1 |
search_dict = { |
84 |
|
"vnfds": ("vnfrs", "vnfd-id"), |
85 |
|
"nsds": ("nsrs", "nsd-id"), |
86 |
|
} |
87 |
|
|
88 |
1 |
if db_collection not in search_dict: |
89 |
1 |
raise NBIBadArgumentsException( |
90 |
|
"db_collection should be equal to vnfds or nsds", "db_collection" |
91 |
|
) |
92 |
|
|
93 |
1 |
record_list = db.get_list( |
94 |
|
search_dict[db_collection][0], |
95 |
|
{search_dict[db_collection][1]: descriptor["_id"]}, |
96 |
|
) |
97 |
|
|
98 |
1 |
if record_list: |
99 |
1 |
return True |
100 |
|
|
101 |
1 |
except (DbException, KeyError, NBIBadArgumentsException) as error: |
102 |
1 |
raise EngineException( |
103 |
|
f"Error occured while detecting the descriptor usage: {error}" |
104 |
|
) |
105 |
|
|
106 |
|
|
107 |
1 |
def update_descriptor_usage_state( |
108 |
|
descriptor: dict, db_collection: str, db: object |
109 |
|
) -> None: |
110 |
|
"""Updates the descriptor usage state. |
111 |
|
|
112 |
|
Args: |
113 |
|
descriptor (dict): VNF or NS Descriptor as dictionary |
114 |
|
db_collection (str): collection name which is looked for in DB |
115 |
|
db (object): name of db object |
116 |
|
|
117 |
|
Returns: |
118 |
|
None |
119 |
|
|
120 |
|
""" |
121 |
1 |
try: |
122 |
1 |
descriptor_update = { |
123 |
|
"_admin.usageState": "NOT_IN_USE", |
124 |
|
} |
125 |
|
|
126 |
1 |
if detect_descriptor_usage(descriptor, db_collection, db): |
127 |
1 |
descriptor_update = { |
128 |
|
"_admin.usageState": "IN_USE", |
129 |
|
} |
130 |
|
|
131 |
1 |
db.set_one( |
132 |
|
db_collection, {"_id": descriptor["_id"]}, update_dict=descriptor_update |
133 |
|
) |
134 |
|
|
135 |
1 |
except (DbException, KeyError, NBIBadArgumentsException) as error: |
136 |
1 |
raise EngineException( |
137 |
|
f"Error occured while updating the descriptor usage state: {error}" |
138 |
|
) |
139 |
|
|
140 |
|
|
141 |
1 |
def get_iterable(input_var): |
142 |
|
""" |
143 |
|
Returns an iterable, in case input_var is None it just returns an empty tuple |
144 |
|
:param input_var: can be a list, tuple or None |
145 |
|
:return: input_var or () if it is None |
146 |
|
""" |
147 |
1 |
if input_var is None: |
148 |
1 |
return () |
149 |
1 |
return input_var |
150 |
|
|
151 |
|
|
152 |
1 |
def versiontuple(v): |
153 |
|
"""utility for compare dot separate versions. Fills with zeros to proper number comparison""" |
154 |
0 |
filled = [] |
155 |
0 |
for point in v.split("."): |
156 |
0 |
filled.append(point.zfill(8)) |
157 |
0 |
return tuple(filled) |
158 |
|
|
159 |
|
|
160 |
1 |
def increment_ip_mac(ip_mac, vm_index=1): |
161 |
1 |
if not isinstance(ip_mac, str): |
162 |
0 |
return ip_mac |
163 |
1 |
try: |
164 |
|
# try with ipv4 look for last dot |
165 |
1 |
i = ip_mac.rfind(".") |
166 |
1 |
if i > 0: |
167 |
1 |
i += 1 |
168 |
1 |
return "{}{}".format(ip_mac[:i], int(ip_mac[i:]) + vm_index) |
169 |
|
# try with ipv6 or mac look for last colon. Operate in hex |
170 |
0 |
i = ip_mac.rfind(":") |
171 |
0 |
if i > 0: |
172 |
0 |
i += 1 |
173 |
|
# format in hex, len can be 2 for mac or 4 for ipv6 |
174 |
0 |
return ("{}{:0" + str(len(ip_mac) - i) + "x}").format( |
175 |
|
ip_mac[:i], int(ip_mac[i:], 16) + vm_index |
176 |
|
) |
177 |
0 |
except Exception: |
178 |
0 |
pass |
179 |
0 |
return None |
180 |
|
|
181 |
|
|
182 |
1 |
class BaseTopic: |
183 |
|
# static variables for all instance classes |
184 |
1 |
topic = None # to_override |
185 |
1 |
topic_msg = None # to_override |
186 |
1 |
quota_name = None # to_override. If not provided topic will be used for quota_name |
187 |
1 |
schema_new = None # to_override |
188 |
1 |
schema_edit = None # to_override |
189 |
1 |
multiproject = True # True if this Topic can be shared by several projects. Then it contains _admin.projects_read |
190 |
|
|
191 |
1 |
default_quota = 500 |
192 |
|
|
193 |
|
# Alternative ID Fields for some Topics |
194 |
1 |
alt_id_field = {"projects": "name", "users": "username", "roles": "name"} |
195 |
|
|
196 |
1 |
def __init__(self, db, fs, msg, auth): |
197 |
1 |
self.db = db |
198 |
1 |
self.fs = fs |
199 |
1 |
self.msg = msg |
200 |
1 |
self.logger = logging.getLogger("nbi.engine") |
201 |
1 |
self.auth = auth |
202 |
|
|
203 |
1 |
@staticmethod |
204 |
1 |
def id_field(topic, value): |
205 |
|
"""Returns ID Field for given topic and field value""" |
206 |
1 |
if topic in BaseTopic.alt_id_field.keys() and not is_valid_uuid(value): |
207 |
0 |
return BaseTopic.alt_id_field[topic] |
208 |
|
else: |
209 |
1 |
return "_id" |
210 |
|
|
211 |
1 |
@staticmethod |
212 |
1 |
def _remove_envelop(indata=None): |
213 |
1 |
if not indata: |
214 |
0 |
return {} |
215 |
1 |
return indata |
216 |
|
|
217 |
1 |
def check_quota(self, session): |
218 |
|
""" |
219 |
|
Check whether topic quota is exceeded by the given project |
220 |
|
Used by relevant topics' 'new' function to decide whether or not creation of the new item should be allowed |
221 |
|
:param session[project_id]: projects (tuple) for which quota should be checked |
222 |
|
:param session[force]: boolean. If true, skip quota checking |
223 |
|
:return: None |
224 |
|
:raise: |
225 |
|
DbException if project not found |
226 |
|
ValidationError if quota exceeded in one of the projects |
227 |
|
""" |
228 |
1 |
if session["force"]: |
229 |
1 |
return |
230 |
0 |
projects = session["project_id"] |
231 |
0 |
for project in projects: |
232 |
0 |
proj = self.auth.get_project(project) |
233 |
0 |
pid = proj["_id"] |
234 |
0 |
quota_name = self.quota_name or self.topic |
235 |
0 |
quota = proj.get("quotas", {}).get(quota_name, self.default_quota) |
236 |
0 |
count = self.db.count(self.topic, {"_admin.projects_read": pid}) |
237 |
0 |
if count >= quota: |
238 |
0 |
name = proj["name"] |
239 |
0 |
raise ValidationError( |
240 |
|
"quota ({}={}) exceeded for project {} ({})".format( |
241 |
|
quota_name, quota, name, pid |
242 |
|
), |
243 |
|
http_code=HTTPStatus.UNPROCESSABLE_ENTITY, |
244 |
|
) |
245 |
|
|
246 |
1 |
def _validate_input_new(self, input, force=False): |
247 |
|
""" |
248 |
|
Validates input user content for a new entry. It uses jsonschema. Some overrides will use pyangbind |
249 |
|
:param input: user input content for the new topic |
250 |
|
:param force: may be used for being more tolerant |
251 |
|
:return: The same input content, or a changed version of it. |
252 |
|
""" |
253 |
1 |
if self.schema_new: |
254 |
1 |
validate_input(input, self.schema_new) |
255 |
1 |
return input |
256 |
|
|
257 |
1 |
def _validate_input_edit(self, input, content, force=False): |
258 |
|
""" |
259 |
|
Validates input user content for an edition. It uses jsonschema. Some overrides will use pyangbind |
260 |
|
:param input: user input content for the new topic |
261 |
|
:param force: may be used for being more tolerant |
262 |
|
:return: The same input content, or a changed version of it. |
263 |
|
""" |
264 |
1 |
if self.schema_edit: |
265 |
1 |
validate_input(input, self.schema_edit) |
266 |
1 |
return input |
267 |
|
|
268 |
1 |
@staticmethod |
269 |
1 |
def _get_project_filter(session): |
270 |
|
""" |
271 |
|
Generates a filter dictionary for querying database, so that only allowed items for this project can be |
272 |
|
addressed. Only proprietary or public can be used. Allowed projects are at _admin.project_read/write. If it is |
273 |
|
not present or contains ANY mean public. |
274 |
|
:param session: contains: |
275 |
|
project_id: project list this session has rights to access. Can be empty, one or several |
276 |
|
set_project: items created will contain this project list |
277 |
|
force: True or False |
278 |
|
public: True, False or None |
279 |
|
method: "list", "show", "write", "delete" |
280 |
|
admin: True or False |
281 |
|
:return: dictionary with project filter |
282 |
|
""" |
283 |
1 |
p_filter = {} |
284 |
1 |
project_filter_n = [] |
285 |
1 |
project_filter = list(session["project_id"]) |
286 |
|
|
287 |
1 |
if session["method"] not in ("list", "delete"): |
288 |
1 |
if project_filter: |
289 |
1 |
project_filter.append("ANY") |
290 |
1 |
elif session["public"] is not None: |
291 |
1 |
if session["public"]: |
292 |
0 |
project_filter.append("ANY") |
293 |
|
else: |
294 |
1 |
project_filter_n.append("ANY") |
295 |
|
|
296 |
1 |
if session.get("PROJECT.ne"): |
297 |
0 |
project_filter_n.append(session["PROJECT.ne"]) |
298 |
|
|
299 |
1 |
if project_filter: |
300 |
1 |
if session["method"] in ("list", "show", "delete") or session.get( |
301 |
|
"set_project" |
302 |
|
): |
303 |
1 |
p_filter["_admin.projects_read.cont"] = project_filter |
304 |
|
else: |
305 |
1 |
p_filter["_admin.projects_write.cont"] = project_filter |
306 |
1 |
if project_filter_n: |
307 |
1 |
if session["method"] in ("list", "show", "delete") or session.get( |
308 |
|
"set_project" |
309 |
|
): |
310 |
1 |
p_filter["_admin.projects_read.ncont"] = project_filter_n |
311 |
|
else: |
312 |
0 |
p_filter["_admin.projects_write.ncont"] = project_filter_n |
313 |
|
|
314 |
1 |
return p_filter |
315 |
|
|
316 |
1 |
def check_conflict_on_new(self, session, indata): |
317 |
|
""" |
318 |
|
Check that the data to be inserted is valid |
319 |
|
:param session: contains "username", "admin", "force", "public", "project_id", "set_project" |
320 |
|
:param indata: data to be inserted |
321 |
|
:return: None or raises EngineException |
322 |
|
""" |
323 |
0 |
pass |
324 |
|
|
325 |
1 |
def check_conflict_on_edit(self, session, final_content, edit_content, _id): |
326 |
|
""" |
327 |
|
Check that the data to be edited/uploaded is valid |
328 |
|
:param session: contains "username", "admin", "force", "public", "project_id", "set_project" |
329 |
|
:param final_content: data once modified. This method may change it. |
330 |
|
:param edit_content: incremental data that contains the modifications to apply |
331 |
|
:param _id: internal _id |
332 |
|
:return: final_content or raises EngineException |
333 |
|
""" |
334 |
1 |
if not self.multiproject: |
335 |
0 |
return final_content |
336 |
|
# Change public status |
337 |
1 |
if session["public"] is not None: |
338 |
1 |
if ( |
339 |
|
session["public"] |
340 |
|
and "ANY" not in final_content["_admin"]["projects_read"] |
341 |
|
): |
342 |
0 |
final_content["_admin"]["projects_read"].append("ANY") |
343 |
0 |
final_content["_admin"]["projects_write"].clear() |
344 |
1 |
if ( |
345 |
|
not session["public"] |
346 |
|
and "ANY" in final_content["_admin"]["projects_read"] |
347 |
|
): |
348 |
0 |
final_content["_admin"]["projects_read"].remove("ANY") |
349 |
|
|
350 |
|
# Change project status |
351 |
1 |
if session.get("set_project"): |
352 |
0 |
for p in session["set_project"]: |
353 |
0 |
if p not in final_content["_admin"]["projects_read"]: |
354 |
0 |
final_content["_admin"]["projects_read"].append(p) |
355 |
|
|
356 |
1 |
return final_content |
357 |
|
|
358 |
1 |
def check_unique_name(self, session, name, _id=None): |
359 |
|
""" |
360 |
|
Check that the name is unique for this project |
361 |
|
:param session: contains "username", "admin", "force", "public", "project_id", "set_project" |
362 |
|
:param name: name to be checked |
363 |
|
:param _id: If not None, ignore this entry that are going to change |
364 |
|
:return: None or raises EngineException |
365 |
|
""" |
366 |
1 |
if not self.multiproject: |
367 |
0 |
_filter = {} |
368 |
|
else: |
369 |
1 |
_filter = self._get_project_filter(session) |
370 |
1 |
_filter["name"] = name |
371 |
1 |
if _id: |
372 |
1 |
_filter["_id.neq"] = _id |
373 |
1 |
if self.db.get_one( |
374 |
|
self.topic, _filter, fail_on_empty=False, fail_on_more=False |
375 |
|
): |
376 |
1 |
raise EngineException( |
377 |
|
"name '{}' already exists for {}".format(name, self.topic), |
378 |
|
HTTPStatus.CONFLICT, |
379 |
|
) |
380 |
|
|
381 |
1 |
@staticmethod |
382 |
1 |
def format_on_new(content, project_id=None, make_public=False): |
383 |
|
""" |
384 |
|
Modifies content descriptor to include _admin |
385 |
|
:param content: descriptor to be modified |
386 |
|
:param project_id: if included, it add project read/write permissions. Can be None or a list |
387 |
|
:param make_public: if included it is generated as public for reading. |
388 |
|
:return: op_id: operation id on asynchronous operation, None otherwise. In addition content is modified |
389 |
|
""" |
390 |
1 |
now = time() |
391 |
1 |
if "_admin" not in content: |
392 |
1 |
content["_admin"] = {} |
393 |
1 |
if not content["_admin"].get("created"): |
394 |
1 |
content["_admin"]["created"] = now |
395 |
1 |
content["_admin"]["modified"] = now |
396 |
1 |
if not content.get("_id"): |
397 |
1 |
content["_id"] = str(uuid4()) |
398 |
1 |
if project_id is not None: |
399 |
1 |
if not content["_admin"].get("projects_read"): |
400 |
1 |
content["_admin"]["projects_read"] = list(project_id) |
401 |
1 |
if make_public: |
402 |
0 |
content["_admin"]["projects_read"].append("ANY") |
403 |
1 |
if not content["_admin"].get("projects_write"): |
404 |
1 |
content["_admin"]["projects_write"] = list(project_id) |
405 |
1 |
return None |
406 |
|
|
407 |
1 |
@staticmethod |
408 |
1 |
def format_on_edit(final_content, edit_content): |
409 |
|
""" |
410 |
|
Modifies final_content to admin information upon edition |
411 |
|
:param final_content: final content to be stored at database |
412 |
|
:param edit_content: user requested update content |
413 |
|
:return: operation id, if this edit implies an asynchronous operation; None otherwise |
414 |
|
""" |
415 |
1 |
if final_content.get("_admin"): |
416 |
1 |
now = time() |
417 |
1 |
final_content["_admin"]["modified"] = now |
418 |
1 |
return None |
419 |
|
|
420 |
1 |
def _send_msg(self, action, content, not_send_msg=None): |
421 |
1 |
if self.topic_msg and not_send_msg is not False: |
422 |
1 |
content = content.copy() |
423 |
1 |
content.pop("_admin", None) |
424 |
1 |
if isinstance(not_send_msg, list): |
425 |
0 |
not_send_msg.append((self.topic_msg, action, content)) |
426 |
|
else: |
427 |
1 |
self.msg.write(self.topic_msg, action, content) |
428 |
|
|
429 |
1 |
def check_conflict_on_del(self, session, _id, db_content): |
430 |
|
""" |
431 |
|
Check if deletion can be done because of dependencies if it is not force. To override |
432 |
|
:param session: contains "username", "admin", "force", "public", "project_id", "set_project" |
433 |
|
:param _id: internal _id |
434 |
|
:param db_content: The database content of this item _id |
435 |
|
:return: None if ok or raises EngineException with the conflict |
436 |
|
""" |
437 |
1 |
pass |
438 |
|
|
439 |
1 |
@staticmethod |
440 |
1 |
def _update_input_with_kwargs(desc, kwargs, yaml_format=False): |
441 |
|
""" |
442 |
|
Update descriptor with the kwargs. It contains dot separated keys |
443 |
|
:param desc: dictionary to be updated |
444 |
|
:param kwargs: plain dictionary to be used for updating. |
445 |
|
:param yaml_format: get kwargs values as yaml format. |
446 |
|
:return: None, 'desc' is modified. It raises EngineException. |
447 |
|
""" |
448 |
1 |
if not kwargs: |
449 |
1 |
return |
450 |
1 |
try: |
451 |
1 |
for k, v in kwargs.items(): |
452 |
1 |
update_content = desc |
453 |
1 |
kitem_old = None |
454 |
1 |
klist = k.split(".") |
455 |
1 |
for kitem in klist: |
456 |
1 |
if kitem_old is not None: |
457 |
1 |
update_content = update_content[kitem_old] |
458 |
1 |
if isinstance(update_content, dict): |
459 |
1 |
kitem_old = kitem |
460 |
1 |
if not isinstance(update_content.get(kitem_old), (dict, list)): |
461 |
1 |
update_content[kitem_old] = {} |
462 |
1 |
elif isinstance(update_content, list): |
463 |
|
# key must be an index of the list, must be integer |
464 |
1 |
kitem_old = int(kitem) |
465 |
|
# if index greater than list, extend the list |
466 |
1 |
if kitem_old >= len(update_content): |
467 |
1 |
update_content += [None] * ( |
468 |
|
kitem_old - len(update_content) + 1 |
469 |
|
) |
470 |
1 |
if not isinstance(update_content[kitem_old], (dict, list)): |
471 |
1 |
update_content[kitem_old] = {} |
472 |
|
else: |
473 |
0 |
raise EngineException( |
474 |
|
"Invalid query string '{}'. Descriptor is not a list nor dict at '{}'".format( |
475 |
|
k, kitem |
476 |
|
) |
477 |
|
) |
478 |
1 |
if v is None: |
479 |
1 |
del update_content[kitem_old] |
480 |
|
else: |
481 |
1 |
update_content[kitem_old] = v if not yaml_format else safe_load(v) |
482 |
1 |
except KeyError: |
483 |
0 |
raise EngineException( |
484 |
|
"Invalid query string '{}'. Descriptor does not contain '{}'".format( |
485 |
|
k, kitem_old |
486 |
|
) |
487 |
|
) |
488 |
1 |
except ValueError: |
489 |
1 |
raise EngineException( |
490 |
|
"Invalid query string '{}'. Expected integer index list instead of '{}'".format( |
491 |
|
k, kitem |
492 |
|
) |
493 |
|
) |
494 |
0 |
except IndexError: |
495 |
0 |
raise EngineException( |
496 |
|
"Invalid query string '{}'. Index '{}' out of range".format( |
497 |
|
k, kitem_old |
498 |
|
) |
499 |
|
) |
500 |
0 |
except YAMLError: |
501 |
0 |
raise EngineException("Invalid query string '{}' yaml format".format(k)) |
502 |
|
|
503 |
1 |
def sol005_projection(self, data): |
504 |
|
# Projection was moved to child classes |
505 |
0 |
return data |
506 |
|
|
507 |
1 |
def show(self, session, _id, filter_q=None, api_req=False): |
508 |
|
""" |
509 |
|
Get complete information on an topic |
510 |
|
:param session: contains "username", "admin", "force", "public", "project_id", "set_project" |
511 |
|
:param _id: server internal id |
512 |
|
:param filter_q: dict: query parameter |
513 |
|
:param api_req: True if this call is serving an external API request. False if serving internal request. |
514 |
|
:return: dictionary, raise exception if not found. |
515 |
|
""" |
516 |
1 |
if not self.multiproject: |
517 |
0 |
filter_db = {} |
518 |
|
else: |
519 |
1 |
filter_db = self._get_project_filter(session) |
520 |
|
# To allow project&user addressing by name AS WELL AS _id |
521 |
1 |
filter_db[BaseTopic.id_field(self.topic, _id)] = _id |
522 |
1 |
data = self.db.get_one(self.topic, filter_db) |
523 |
|
|
524 |
|
# Only perform SOL005 projection if we are serving an external request |
525 |
1 |
if api_req: |
526 |
0 |
self.sol005_projection(data) |
527 |
|
|
528 |
1 |
return data |
529 |
|
|
530 |
|
# TODO transform data for SOL005 URL requests |
531 |
|
# TODO remove _admin if not admin |
532 |
|
|
533 |
1 |
def get_file(self, session, _id, path=None, accept_header=None): |
534 |
|
""" |
535 |
|
Only implemented for descriptor topics. Return the file content of a descriptor |
536 |
|
:param session: contains "username", "admin", "force", "public", "project_id", "set_project" |
537 |
|
:param _id: Identity of the item to get content |
538 |
|
:param path: artifact path or "$DESCRIPTOR" or None |
539 |
|
:param accept_header: Content of Accept header. Must contain applition/zip or/and text/plain |
540 |
|
:return: opened file or raises an exception |
541 |
|
""" |
542 |
0 |
raise EngineException( |
543 |
|
"Method get_file not valid for this topic", HTTPStatus.INTERNAL_SERVER_ERROR |
544 |
|
) |
545 |
|
|
546 |
1 |
def list(self, session, filter_q=None, api_req=False): |
547 |
|
""" |
548 |
|
Get a list of the topic that matches a filter |
549 |
|
:param session: contains the used login username and working project |
550 |
|
:param filter_q: filter of data to be applied |
551 |
|
:param api_req: True if this call is serving an external API request. False if serving internal request. |
552 |
|
:return: The list, it can be empty if no one match the filter. |
553 |
|
""" |
554 |
0 |
if not filter_q: |
555 |
0 |
filter_q = {} |
556 |
0 |
if self.multiproject: |
557 |
0 |
filter_q.update(self._get_project_filter(session)) |
558 |
|
|
559 |
|
# TODO transform data for SOL005 URL requests. Transform filtering |
560 |
|
# TODO implement "field-type" query string SOL005 |
561 |
0 |
data = self.db.get_list(self.topic, filter_q) |
562 |
|
|
563 |
|
# Only perform SOL005 projection if we are serving an external request |
564 |
0 |
if api_req: |
565 |
0 |
data = [self.sol005_projection(inst) for inst in data] |
566 |
|
|
567 |
0 |
return data |
568 |
|
|
569 |
1 |
def new(self, rollback, session, indata=None, kwargs=None, headers=None): |
570 |
|
""" |
571 |
|
Creates a new entry into database. |
572 |
|
:param rollback: list to append created items at database in case a rollback may to be done |
573 |
|
:param session: contains "username", "admin", "force", "public", "project_id", "set_project" |
574 |
|
:param indata: data to be inserted |
575 |
|
:param kwargs: used to override the indata descriptor |
576 |
|
:param headers: http request headers |
577 |
|
:return: _id, op_id: |
578 |
|
_id: identity of the inserted data. |
579 |
|
op_id: operation id if this is asynchronous, None otherwise |
580 |
|
""" |
581 |
1 |
try: |
582 |
1 |
if self.multiproject: |
583 |
1 |
self.check_quota(session) |
584 |
|
|
585 |
1 |
content = self._remove_envelop(indata) |
586 |
|
|
587 |
|
# Override descriptor with query string kwargs |
588 |
1 |
self._update_input_with_kwargs(content, kwargs) |
589 |
1 |
content = self._validate_input_new(content, force=session["force"]) |
590 |
1 |
self.check_conflict_on_new(session, content) |
591 |
1 |
op_id = self.format_on_new( |
592 |
|
content, project_id=session["project_id"], make_public=session["public"] |
593 |
|
) |
594 |
1 |
_id = self.db.create(self.topic, content) |
595 |
1 |
rollback.append({"topic": self.topic, "_id": _id}) |
596 |
1 |
if op_id: |
597 |
1 |
content["op_id"] = op_id |
598 |
1 |
self._send_msg("created", content) |
599 |
1 |
return _id, op_id |
600 |
1 |
except ValidationError as e: |
601 |
0 |
raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY) |
602 |
|
|
603 |
1 |
def upload_content(self, session, _id, indata, kwargs, headers): |
604 |
|
""" |
605 |
|
Only implemented for descriptor topics. Used for receiving content by chunks (with a transaction_id header |
606 |
|
and/or gzip file. It will store and extract) |
607 |
|
:param session: contains "username", "admin", "force", "public", "project_id", "set_project" |
608 |
|
:param _id : the database id of entry to be updated |
609 |
|
:param indata: http body request |
610 |
|
:param kwargs: user query string to override parameters. NOT USED |
611 |
|
:param headers: http request headers |
612 |
|
:return: True package has is completely uploaded or False if partial content has been uplodaed. |
613 |
|
Raise exception on error |
614 |
|
""" |
615 |
0 |
raise EngineException( |
616 |
|
"Method upload_content not valid for this topic", |
617 |
|
HTTPStatus.INTERNAL_SERVER_ERROR, |
618 |
|
) |
619 |
|
|
620 |
1 |
def delete_list(self, session, filter_q=None): |
621 |
|
""" |
622 |
|
Delete a several entries of a topic. This is for internal usage and test only, not exposed to NBI API |
623 |
|
:param session: contains "username", "admin", "force", "public", "project_id", "set_project" |
624 |
|
:param filter_q: filter of data to be applied |
625 |
|
:return: The deleted list, it can be empty if no one match the filter. |
626 |
|
""" |
627 |
|
# TODO add admin to filter, validate rights |
628 |
0 |
if not filter_q: |
629 |
0 |
filter_q = {} |
630 |
0 |
if self.multiproject: |
631 |
0 |
filter_q.update(self._get_project_filter(session)) |
632 |
0 |
return self.db.del_list(self.topic, filter_q) |
633 |
|
|
634 |
1 |
def delete_extra(self, session, _id, db_content, not_send_msg=None): |
635 |
|
""" |
636 |
|
Delete other things apart from database entry of a item _id. |
637 |
|
e.g.: other associated elements at database and other file system storage |
638 |
|
:param session: contains "username", "admin", "force", "public", "project_id", "set_project" |
639 |
|
:param _id: server internal id |
640 |
|
:param db_content: The database content of the _id. It is already deleted when reached this method, but the |
641 |
|
content is needed in same cases |
642 |
|
:param not_send_msg: To not send message (False) or store content (list) instead |
643 |
|
:return: None if ok or raises EngineException with the problem |
644 |
|
""" |
645 |
0 |
pass |
646 |
|
|
647 |
1 |
def delete(self, session, _id, dry_run=False, not_send_msg=None): |
648 |
|
""" |
649 |
|
Delete item by its internal _id |
650 |
|
:param session: contains "username", "admin", "force", "public", "project_id", "set_project" |
651 |
|
:param _id: server internal id |
652 |
|
:param dry_run: make checking but do not delete |
653 |
|
:param not_send_msg: To not send message (False) or store content (list) instead |
654 |
|
:return: operation id (None if there is not operation), raise exception if error or not found, conflict, ... |
655 |
|
""" |
656 |
|
|
657 |
|
# To allow addressing projects and users by name AS WELL AS by _id |
658 |
1 |
if not self.multiproject: |
659 |
0 |
filter_q = {} |
660 |
|
else: |
661 |
1 |
filter_q = self._get_project_filter(session) |
662 |
1 |
filter_q[self.id_field(self.topic, _id)] = _id |
663 |
1 |
item_content = self.db.get_one(self.topic, filter_q) |
664 |
|
|
665 |
1 |
self.check_conflict_on_del(session, _id, item_content) |
666 |
1 |
if dry_run: |
667 |
0 |
return None |
668 |
|
|
669 |
1 |
if self.multiproject and session["project_id"]: |
670 |
|
# remove reference from project_read if there are more projects referencing it. If it last one, |
671 |
|
# do not remove reference, but delete |
672 |
1 |
other_projects_referencing = next( |
673 |
|
( |
674 |
|
p |
675 |
|
for p in item_content["_admin"]["projects_read"] |
676 |
|
if p not in session["project_id"] and p != "ANY" |
677 |
|
), |
678 |
|
None, |
679 |
|
) |
680 |
|
|
681 |
|
# check if there are projects referencing it (apart from ANY, that means, public).... |
682 |
1 |
if other_projects_referencing: |
683 |
|
# remove references but not delete |
684 |
1 |
update_dict_pull = { |
685 |
|
"_admin.projects_read": session["project_id"], |
686 |
|
"_admin.projects_write": session["project_id"], |
687 |
|
} |
688 |
1 |
self.db.set_one( |
689 |
|
self.topic, filter_q, update_dict=None, pull_list=update_dict_pull |
690 |
|
) |
691 |
1 |
return None |
692 |
|
else: |
693 |
1 |
can_write = next( |
694 |
|
( |
695 |
|
p |
696 |
|
for p in item_content["_admin"]["projects_write"] |
697 |
|
if p == "ANY" or p in session["project_id"] |
698 |
|
), |
699 |
|
None, |
700 |
|
) |
701 |
1 |
if not can_write: |
702 |
0 |
raise EngineException( |
703 |
|
"You have not write permission to delete it", |
704 |
|
http_code=HTTPStatus.UNAUTHORIZED, |
705 |
|
) |
706 |
|
|
707 |
|
# delete |
708 |
1 |
self.db.del_one(self.topic, filter_q) |
709 |
1 |
self.delete_extra(session, _id, item_content, not_send_msg=not_send_msg) |
710 |
1 |
self._send_msg("deleted", {"_id": _id}, not_send_msg=not_send_msg) |
711 |
1 |
return None |
712 |
|
|
713 |
1 |
def edit(self, session, _id, indata=None, kwargs=None, content=None): |
714 |
|
""" |
715 |
|
Change the content of an item |
716 |
|
:param session: contains "username", "admin", "force", "public", "project_id", "set_project" |
717 |
|
:param _id: server internal id |
718 |
|
:param indata: contains the changes to apply |
719 |
|
:param kwargs: modifies indata |
720 |
|
:param content: original content of the item |
721 |
|
:return: op_id: operation id if this is processed asynchronously, None otherwise |
722 |
|
""" |
723 |
1 |
indata = self._remove_envelop(indata) |
724 |
|
|
725 |
|
# Override descriptor with query string kwargs |
726 |
1 |
if kwargs: |
727 |
0 |
self._update_input_with_kwargs(indata, kwargs) |
728 |
1 |
try: |
729 |
1 |
if indata and session.get("set_project"): |
730 |
0 |
raise EngineException( |
731 |
|
"Cannot edit content and set to project (query string SET_PROJECT) at same time", |
732 |
|
HTTPStatus.UNPROCESSABLE_ENTITY, |
733 |
|
) |
734 |
|
# TODO self._check_edition(session, indata, _id, force) |
735 |
1 |
if not content: |
736 |
1 |
content = self.show(session, _id) |
737 |
1 |
indata = self._validate_input_edit(indata, content, force=session["force"]) |
738 |
1 |
deep_update_rfc7396(content, indata) |
739 |
|
|
740 |
|
# To allow project addressing by name AS WELL AS _id. Get the _id, just in case the provided one is a name |
741 |
1 |
_id = content.get("_id") or _id |
742 |
|
|
743 |
1 |
content = self.check_conflict_on_edit(session, content, indata, _id=_id) |
744 |
1 |
op_id = self.format_on_edit(content, indata) |
745 |
|
|
746 |
1 |
self.db.replace(self.topic, _id, content) |
747 |
|
|
748 |
1 |
indata.pop("_admin", None) |
749 |
1 |
if op_id: |
750 |
1 |
indata["op_id"] = op_id |
751 |
1 |
indata["_id"] = _id |
752 |
1 |
self._send_msg("edited", indata) |
753 |
1 |
return op_id |
754 |
1 |
except ValidationError as e: |
755 |
1 |
raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY) |