Code Coverage

Cobertura Coverage Report > osm_nbi >

base_topic.py

Trend

File Coverage summary

NameClassesLinesConditionals
base_topic.py
100%
1/1
77%
259/335
100%
0/0

Coverage Breakdown by Class

NameLinesConditionals
base_topic.py
77%
259/335
N/A

Source

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