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