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