blob: 10ad1d68448d9c2e388ccf4c5aa6ec07ae38f586 [file] [log] [blame]
tiernob24258a2018-10-04 18:39:49 +02001# -*- coding: utf-8 -*-
2
tiernod125caf2018-11-22 16:05:54 +00003# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
12# implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
yshah53cc9eb2024-07-05 13:06:31 +000016import logging
rshri2d386cb2024-07-05 14:35:51 +000017import random
18import string
tiernob24258a2018-10-04 18:39:49 +020019from uuid import uuid4
20from http import HTTPStatus
21from time import time
aticig2b5e1232022-08-10 17:30:12 +030022from osm_common.dbbase import deep_update_rfc7396, DbException
rshri2d386cb2024-07-05 14:35:51 +000023from osm_common.msgbase import MsgException
24from osm_common.fsbase import FsException
tierno23acf402019-08-28 13:36:34 +000025from osm_nbi.validation import validate_input, ValidationError, is_valid_uuid
tierno1c38f2f2020-03-24 11:51:39 +000026from yaml import safe_load, YAMLError
tiernob24258a2018-10-04 18:39:49 +020027
28__author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
29
30
31class EngineException(Exception):
tiernob24258a2018-10-04 18:39:49 +020032 def __init__(self, message, http_code=HTTPStatus.BAD_REQUEST):
33 self.http_code = http_code
tierno23acf402019-08-28 13:36:34 +000034 super(Exception, self).__init__(message)
tiernob24258a2018-10-04 18:39:49 +020035
garciadeblasf2af4a12023-01-24 16:56:54 +010036
aticig2b5e1232022-08-10 17:30:12 +030037class NBIBadArgumentsException(Exception):
38 """
39 Bad argument values exception
40 """
41
42 def __init__(self, message: str = "", bad_args: list = None):
43 Exception.__init__(self, message)
44 self.message = message
45 self.bad_args = bad_args
46
47 def __str__(self):
garciadeblasf2af4a12023-01-24 16:56:54 +010048 return "{}, Bad arguments: {}".format(self.message, self.bad_args)
49
tiernob24258a2018-10-04 18:39:49 +020050
tierno714954e2019-11-29 13:43:26 +000051def deep_get(target_dict, key_list):
52 """
53 Get a value from target_dict entering in the nested keys. If keys does not exist, it returns None
54 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
55 :param target_dict: dictionary to be read
56 :param key_list: list of keys to read from target_dict
57 :return: The wanted value if exist, None otherwise
58 """
59 for key in key_list:
60 if not isinstance(target_dict, dict) or key not in target_dict:
61 return None
62 target_dict = target_dict[key]
63 return target_dict
64
65
garciadeblasf2af4a12023-01-24 16:56:54 +010066def detect_descriptor_usage(descriptor: dict, db_collection: str, db: object) -> bool:
aticig2b5e1232022-08-10 17:30:12 +030067 """Detect the descriptor usage state.
68
69 Args:
70 descriptor (dict): VNF or NS Descriptor as dictionary
71 db_collection (str): collection name which is looked for in DB
72 db (object): name of db object
73
74 Returns:
75 True if descriptor is in use else None
76
77 """
78 try:
79 if not descriptor:
80 raise NBIBadArgumentsException(
81 "Argument is mandatory and can not be empty", "descriptor"
82 )
83
84 if not db:
85 raise NBIBadArgumentsException("A valid DB object should be provided", "db")
86
87 search_dict = {
88 "vnfds": ("vnfrs", "vnfd-id"),
89 "nsds": ("nsrs", "nsd-id"),
kayal2001f71c2e82024-06-25 15:26:24 +053090 "ns_config_template": ("ns_config_template", "_id"),
aticig2b5e1232022-08-10 17:30:12 +030091 }
92
93 if db_collection not in search_dict:
garciadeblasf2af4a12023-01-24 16:56:54 +010094 raise NBIBadArgumentsException(
95 "db_collection should be equal to vnfds or nsds", "db_collection"
96 )
aticig2b5e1232022-08-10 17:30:12 +030097
98 record_list = db.get_list(
99 search_dict[db_collection][0],
100 {search_dict[db_collection][1]: descriptor["_id"]},
101 )
102
103 if record_list:
104 return True
105
106 except (DbException, KeyError, NBIBadArgumentsException) as error:
garciadeblasf2af4a12023-01-24 16:56:54 +0100107 raise EngineException(
108 f"Error occured while detecting the descriptor usage: {error}"
109 )
aticig2b5e1232022-08-10 17:30:12 +0300110
111
112def update_descriptor_usage_state(
113 descriptor: dict, db_collection: str, db: object
114) -> None:
115 """Updates the descriptor usage state.
116
117 Args:
118 descriptor (dict): VNF or NS Descriptor as dictionary
119 db_collection (str): collection name which is looked for in DB
120 db (object): name of db object
121
122 Returns:
123 None
124
125 """
126 try:
127 descriptor_update = {
128 "_admin.usageState": "NOT_IN_USE",
129 }
130
131 if detect_descriptor_usage(descriptor, db_collection, db):
132 descriptor_update = {
133 "_admin.usageState": "IN_USE",
134 }
135
garciadeblasf2af4a12023-01-24 16:56:54 +0100136 db.set_one(
137 db_collection, {"_id": descriptor["_id"]}, update_dict=descriptor_update
138 )
aticig2b5e1232022-08-10 17:30:12 +0300139
140 except (DbException, KeyError, NBIBadArgumentsException) as error:
garciadeblasf2af4a12023-01-24 16:56:54 +0100141 raise EngineException(
142 f"Error occured while updating the descriptor usage state: {error}"
143 )
aticig2b5e1232022-08-10 17:30:12 +0300144
145
tiernob24258a2018-10-04 18:39:49 +0200146def get_iterable(input_var):
147 """
148 Returns an iterable, in case input_var is None it just returns an empty tuple
149 :param input_var: can be a list, tuple or None
150 :return: input_var or () if it is None
151 """
152 if input_var is None:
153 return ()
154 return input_var
155
156
157def versiontuple(v):
158 """utility for compare dot separate versions. Fills with zeros to proper number comparison"""
159 filled = []
160 for point in v.split("."):
161 filled.append(point.zfill(8))
162 return tuple(filled)
163
164
tiernocddb07d2020-10-06 08:28:00 +0000165def increment_ip_mac(ip_mac, vm_index=1):
166 if not isinstance(ip_mac, str):
167 return ip_mac
168 try:
169 # try with ipv4 look for last dot
170 i = ip_mac.rfind(".")
171 if i > 0:
172 i += 1
173 return "{}{}".format(ip_mac[:i], int(ip_mac[i:]) + vm_index)
174 # try with ipv6 or mac look for last colon. Operate in hex
175 i = ip_mac.rfind(":")
176 if i > 0:
177 i += 1
178 # format in hex, len can be 2 for mac or 4 for ipv6
garciadeblas4568a372021-03-24 09:19:48 +0100179 return ("{}{:0" + str(len(ip_mac) - i) + "x}").format(
180 ip_mac[:i], int(ip_mac[i:], 16) + vm_index
181 )
tiernocddb07d2020-10-06 08:28:00 +0000182 except Exception:
183 pass
184 return None
185
186
tiernob24258a2018-10-04 18:39:49 +0200187class BaseTopic:
188 # static variables for all instance classes
garciadeblas4568a372021-03-24 09:19:48 +0100189 topic = None # to_override
190 topic_msg = None # to_override
191 quota_name = None # to_override. If not provided topic will be used for quota_name
192 schema_new = None # to_override
tiernob24258a2018-10-04 18:39:49 +0200193 schema_edit = None # to_override
tierno65ca36d2019-02-12 19:27:52 +0100194 multiproject = True # True if this Topic can be shared by several projects. Then it contains _admin.projects_read
tiernob24258a2018-10-04 18:39:49 +0200195
delacruzramo32bab472019-09-13 12:24:22 +0200196 default_quota = 500
197
delacruzramoc061f562019-04-05 11:00:02 +0200198 # Alternative ID Fields for some Topics
garciadeblas4568a372021-03-24 09:19:48 +0100199 alt_id_field = {"projects": "name", "users": "username", "roles": "name"}
delacruzramoc061f562019-04-05 11:00:02 +0200200
delacruzramo32bab472019-09-13 12:24:22 +0200201 def __init__(self, db, fs, msg, auth):
tiernob24258a2018-10-04 18:39:49 +0200202 self.db = db
203 self.fs = fs
204 self.msg = msg
yshah53cc9eb2024-07-05 13:06:31 +0000205 self.logger = logging.getLogger("nbi.base")
delacruzramo32bab472019-09-13 12:24:22 +0200206 self.auth = auth
tiernob24258a2018-10-04 18:39:49 +0200207
208 @staticmethod
delacruzramoc061f562019-04-05 11:00:02 +0200209 def id_field(topic, value):
tierno65ca36d2019-02-12 19:27:52 +0100210 """Returns ID Field for given topic and field value"""
delacruzramoceb8baf2019-06-21 14:25:38 +0200211 if topic in BaseTopic.alt_id_field.keys() and not is_valid_uuid(value):
delacruzramoc061f562019-04-05 11:00:02 +0200212 return BaseTopic.alt_id_field[topic]
213 else:
214 return "_id"
215
216 @staticmethod
tiernob24258a2018-10-04 18:39:49 +0200217 def _remove_envelop(indata=None):
218 if not indata:
219 return {}
220 return indata
221
delacruzramo32bab472019-09-13 12:24:22 +0200222 def check_quota(self, session):
223 """
224 Check whether topic quota is exceeded by the given project
225 Used by relevant topics' 'new' function to decide whether or not creation of the new item should be allowed
tierno6b02b052020-06-02 10:07:41 +0000226 :param session[project_id]: projects (tuple) for which quota should be checked
227 :param session[force]: boolean. If true, skip quota checking
delacruzramo32bab472019-09-13 12:24:22 +0200228 :return: None
229 :raise:
230 DbException if project not found
tierno6b02b052020-06-02 10:07:41 +0000231 ValidationError if quota exceeded in one of the projects
delacruzramo32bab472019-09-13 12:24:22 +0200232 """
tiernod7749582020-05-28 10:41:10 +0000233 if session["force"]:
delacruzramo32bab472019-09-13 12:24:22 +0200234 return
235 projects = session["project_id"]
236 for project in projects:
237 proj = self.auth.get_project(project)
238 pid = proj["_id"]
tierno6b02b052020-06-02 10:07:41 +0000239 quota_name = self.quota_name or self.topic
240 quota = proj.get("quotas", {}).get(quota_name, self.default_quota)
delacruzramo32bab472019-09-13 12:24:22 +0200241 count = self.db.count(self.topic, {"_admin.projects_read": pid})
242 if count >= quota:
243 name = proj["name"]
garciadeblas4568a372021-03-24 09:19:48 +0100244 raise ValidationError(
245 "quota ({}={}) exceeded for project {} ({})".format(
246 quota_name, quota, name, pid
247 ),
248 http_code=HTTPStatus.UNPROCESSABLE_ENTITY,
249 )
delacruzramo32bab472019-09-13 12:24:22 +0200250
tiernob24258a2018-10-04 18:39:49 +0200251 def _validate_input_new(self, input, force=False):
252 """
253 Validates input user content for a new entry. 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 if self.schema_new:
259 validate_input(input, self.schema_new)
260 return input
261
Frank Brydendeba68e2020-07-27 13:55:11 +0000262 def _validate_input_edit(self, input, content, force=False):
tiernob24258a2018-10-04 18:39:49 +0200263 """
264 Validates input user content for an edition. It uses jsonschema. Some overrides will use pyangbind
265 :param input: user input content for the new topic
266 :param force: may be used for being more tolerant
267 :return: The same input content, or a changed version of it.
268 """
269 if self.schema_edit:
270 validate_input(input, self.schema_edit)
271 return input
272
273 @staticmethod
tierno65ca36d2019-02-12 19:27:52 +0100274 def _get_project_filter(session):
tiernob24258a2018-10-04 18:39:49 +0200275 """
276 Generates a filter dictionary for querying database, so that only allowed items for this project can be
tiernof5f2e3f2020-03-23 14:42:10 +0000277 addressed. Only proprietary or public can be used. Allowed projects are at _admin.project_read/write. If it is
tiernob24258a2018-10-04 18:39:49 +0200278 not present or contains ANY mean public.
tierno65ca36d2019-02-12 19:27:52 +0100279 :param session: contains:
280 project_id: project list this session has rights to access. Can be empty, one or several
garciadeblas4568a372021-03-24 09:19:48 +0100281 set_project: items created will contain this project list
tierno65ca36d2019-02-12 19:27:52 +0100282 force: True or False
283 public: True, False or None
284 method: "list", "show", "write", "delete"
285 admin: True or False
286 :return: dictionary with project filter
tiernob24258a2018-10-04 18:39:49 +0200287 """
tierno65ca36d2019-02-12 19:27:52 +0100288 p_filter = {}
289 project_filter_n = []
290 project_filter = list(session["project_id"])
tiernob24258a2018-10-04 18:39:49 +0200291
tierno65ca36d2019-02-12 19:27:52 +0100292 if session["method"] not in ("list", "delete"):
293 if project_filter:
294 project_filter.append("ANY")
295 elif session["public"] is not None:
296 if session["public"]:
297 project_filter.append("ANY")
298 else:
299 project_filter_n.append("ANY")
300
301 if session.get("PROJECT.ne"):
302 project_filter_n.append(session["PROJECT.ne"])
303
304 if project_filter:
garciadeblas4568a372021-03-24 09:19:48 +0100305 if session["method"] in ("list", "show", "delete") or session.get(
306 "set_project"
307 ):
tierno65ca36d2019-02-12 19:27:52 +0100308 p_filter["_admin.projects_read.cont"] = project_filter
309 else:
310 p_filter["_admin.projects_write.cont"] = project_filter
311 if project_filter_n:
garciadeblas4568a372021-03-24 09:19:48 +0100312 if session["method"] in ("list", "show", "delete") or session.get(
313 "set_project"
314 ):
tierno65ca36d2019-02-12 19:27:52 +0100315 p_filter["_admin.projects_read.ncont"] = project_filter_n
316 else:
317 p_filter["_admin.projects_write.ncont"] = project_filter_n
318
319 return p_filter
320
321 def check_conflict_on_new(self, session, indata):
tiernob24258a2018-10-04 18:39:49 +0200322 """
323 Check that the data to be inserted is valid
tierno65ca36d2019-02-12 19:27:52 +0100324 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200325 :param indata: data to be inserted
tiernob24258a2018-10-04 18:39:49 +0200326 :return: None or raises EngineException
327 """
328 pass
329
tierno65ca36d2019-02-12 19:27:52 +0100330 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
tiernob24258a2018-10-04 18:39:49 +0200331 """
332 Check that the data to be edited/uploaded is valid
tierno65ca36d2019-02-12 19:27:52 +0100333 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernobdebce92019-07-01 15:36:49 +0000334 :param final_content: data once modified. This method may change it.
tiernob24258a2018-10-04 18:39:49 +0200335 :param edit_content: incremental data that contains the modifications to apply
336 :param _id: internal _id
bravofb995ea22021-02-10 10:57:52 -0300337 :return: final_content or raises EngineException
tiernob24258a2018-10-04 18:39:49 +0200338 """
tierno65ca36d2019-02-12 19:27:52 +0100339 if not self.multiproject:
bravofb995ea22021-02-10 10:57:52 -0300340 return final_content
tierno65ca36d2019-02-12 19:27:52 +0100341 # Change public status
342 if session["public"] is not None:
garciadeblas4568a372021-03-24 09:19:48 +0100343 if (
344 session["public"]
345 and "ANY" not in final_content["_admin"]["projects_read"]
346 ):
tierno65ca36d2019-02-12 19:27:52 +0100347 final_content["_admin"]["projects_read"].append("ANY")
348 final_content["_admin"]["projects_write"].clear()
garciadeblas4568a372021-03-24 09:19:48 +0100349 if (
350 not session["public"]
351 and "ANY" in final_content["_admin"]["projects_read"]
352 ):
tierno65ca36d2019-02-12 19:27:52 +0100353 final_content["_admin"]["projects_read"].remove("ANY")
354
355 # Change project status
356 if session.get("set_project"):
357 for p in session["set_project"]:
358 if p not in final_content["_admin"]["projects_read"]:
359 final_content["_admin"]["projects_read"].append(p)
tiernob24258a2018-10-04 18:39:49 +0200360
bravofb995ea22021-02-10 10:57:52 -0300361 return final_content
362
tiernob24258a2018-10-04 18:39:49 +0200363 def check_unique_name(self, session, name, _id=None):
364 """
365 Check that the name is unique for this project
tierno65ca36d2019-02-12 19:27:52 +0100366 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200367 :param name: name to be checked
368 :param _id: If not None, ignore this entry that are going to change
369 :return: None or raises EngineException
370 """
tierno1f029d82019-06-13 22:37:04 +0000371 if not self.multiproject:
372 _filter = {}
373 else:
374 _filter = self._get_project_filter(session)
tiernob24258a2018-10-04 18:39:49 +0200375 _filter["name"] = name
376 if _id:
377 _filter["_id.neq"] = _id
garciadeblas4568a372021-03-24 09:19:48 +0100378 if self.db.get_one(
379 self.topic, _filter, fail_on_empty=False, fail_on_more=False
380 ):
381 raise EngineException(
382 "name '{}' already exists for {}".format(name, self.topic),
383 HTTPStatus.CONFLICT,
384 )
tiernob24258a2018-10-04 18:39:49 +0200385
386 @staticmethod
387 def format_on_new(content, project_id=None, make_public=False):
388 """
389 Modifies content descriptor to include _admin
390 :param content: descriptor to be modified
tierno65ca36d2019-02-12 19:27:52 +0100391 :param project_id: if included, it add project read/write permissions. Can be None or a list
tiernob24258a2018-10-04 18:39:49 +0200392 :param make_public: if included it is generated as public for reading.
tiernobdebce92019-07-01 15:36:49 +0000393 :return: op_id: operation id on asynchronous operation, None otherwise. In addition content is modified
tiernob24258a2018-10-04 18:39:49 +0200394 """
395 now = time()
396 if "_admin" not in content:
397 content["_admin"] = {}
398 if not content["_admin"].get("created"):
399 content["_admin"]["created"] = now
400 content["_admin"]["modified"] = now
401 if not content.get("_id"):
402 content["_id"] = str(uuid4())
tierno65ca36d2019-02-12 19:27:52 +0100403 if project_id is not None:
tiernob24258a2018-10-04 18:39:49 +0200404 if not content["_admin"].get("projects_read"):
tierno65ca36d2019-02-12 19:27:52 +0100405 content["_admin"]["projects_read"] = list(project_id)
tiernob24258a2018-10-04 18:39:49 +0200406 if make_public:
407 content["_admin"]["projects_read"].append("ANY")
408 if not content["_admin"].get("projects_write"):
tierno65ca36d2019-02-12 19:27:52 +0100409 content["_admin"]["projects_write"] = list(project_id)
tiernobdebce92019-07-01 15:36:49 +0000410 return None
tiernob24258a2018-10-04 18:39:49 +0200411
412 @staticmethod
rshri2d386cb2024-07-05 14:35:51 +0000413 def format_on_operation(content, operation_type, operation_params):
414 if content["current_operation"] is None:
415 op_id = str(uuid4())
416 content["current_operation"] = op_id
417 else:
418 op_id = content["current_operation"]
419 now = time()
420 if "operationHistory" not in content:
421 content["operationHistory"] = []
422
423 operation = {}
424 operation["operationType"] = operation_type
425 operation["git_operation_info"] = None
426 operation["op_id"] = op_id
427 operation["result"] = None
428 operation["workflowState"] = "PROCESSING"
429 operation["resourceState"] = "NOT_READY"
430 operation["creationDate"] = now
431 operation["endDate"] = None
432 operation["operationParams"] = operation_params
433
434 content["operationHistory"].append(operation)
435 return op_id
436
437 @staticmethod
tiernob24258a2018-10-04 18:39:49 +0200438 def format_on_edit(final_content, edit_content):
tiernobdebce92019-07-01 15:36:49 +0000439 """
440 Modifies final_content to admin information upon edition
441 :param final_content: final content to be stored at database
442 :param edit_content: user requested update content
443 :return: operation id, if this edit implies an asynchronous operation; None otherwise
444 """
tiernob24258a2018-10-04 18:39:49 +0200445 if final_content.get("_admin"):
446 now = time()
447 final_content["_admin"]["modified"] = now
tiernobdebce92019-07-01 15:36:49 +0000448 return None
tiernob24258a2018-10-04 18:39:49 +0200449
tiernobee3bad2019-12-05 12:26:01 +0000450 def _send_msg(self, action, content, not_send_msg=None):
451 if self.topic_msg and not_send_msg is not False:
agarwalat53471982020-10-08 13:06:14 +0000452 content = content.copy()
tiernob24258a2018-10-04 18:39:49 +0200453 content.pop("_admin", None)
tiernobee3bad2019-12-05 12:26:01 +0000454 if isinstance(not_send_msg, list):
455 not_send_msg.append((self.topic_msg, action, content))
456 else:
457 self.msg.write(self.topic_msg, action, content)
tiernob24258a2018-10-04 18:39:49 +0200458
tiernob4844ab2019-05-23 08:42:12 +0000459 def check_conflict_on_del(self, session, _id, db_content):
tiernob24258a2018-10-04 18:39:49 +0200460 """
461 Check if deletion can be done because of dependencies if it is not force. To override
tierno65ca36d2019-02-12 19:27:52 +0100462 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
463 :param _id: internal _id
tiernob4844ab2019-05-23 08:42:12 +0000464 :param db_content: The database content of this item _id
tiernob24258a2018-10-04 18:39:49 +0200465 :return: None if ok or raises EngineException with the conflict
466 """
467 pass
468
469 @staticmethod
tierno1c38f2f2020-03-24 11:51:39 +0000470 def _update_input_with_kwargs(desc, kwargs, yaml_format=False):
tiernob24258a2018-10-04 18:39:49 +0200471 """
472 Update descriptor with the kwargs. It contains dot separated keys
473 :param desc: dictionary to be updated
474 :param kwargs: plain dictionary to be used for updating.
tierno1c38f2f2020-03-24 11:51:39 +0000475 :param yaml_format: get kwargs values as yaml format.
delacruzramoc061f562019-04-05 11:00:02 +0200476 :return: None, 'desc' is modified. It raises EngineException.
tiernob24258a2018-10-04 18:39:49 +0200477 """
478 if not kwargs:
479 return
480 try:
481 for k, v in kwargs.items():
482 update_content = desc
483 kitem_old = None
484 klist = k.split(".")
485 for kitem in klist:
486 if kitem_old is not None:
487 update_content = update_content[kitem_old]
488 if isinstance(update_content, dict):
489 kitem_old = kitem
tiernoac55f062020-06-17 07:42:30 +0000490 if not isinstance(update_content.get(kitem_old), (dict, list)):
491 update_content[kitem_old] = {}
tiernob24258a2018-10-04 18:39:49 +0200492 elif isinstance(update_content, list):
tiernoac55f062020-06-17 07:42:30 +0000493 # key must be an index of the list, must be integer
tiernob24258a2018-10-04 18:39:49 +0200494 kitem_old = int(kitem)
tiernoac55f062020-06-17 07:42:30 +0000495 # if index greater than list, extend the list
496 if kitem_old >= len(update_content):
garciadeblas4568a372021-03-24 09:19:48 +0100497 update_content += [None] * (
498 kitem_old - len(update_content) + 1
499 )
tiernoac55f062020-06-17 07:42:30 +0000500 if not isinstance(update_content[kitem_old], (dict, list)):
501 update_content[kitem_old] = {}
tiernob24258a2018-10-04 18:39:49 +0200502 else:
503 raise EngineException(
garciadeblas4568a372021-03-24 09:19:48 +0100504 "Invalid query string '{}'. Descriptor is not a list nor dict at '{}'".format(
505 k, kitem
506 )
507 )
tiernoac55f062020-06-17 07:42:30 +0000508 if v is None:
509 del update_content[kitem_old]
510 else:
511 update_content[kitem_old] = v if not yaml_format else safe_load(v)
tiernob24258a2018-10-04 18:39:49 +0200512 except KeyError:
513 raise EngineException(
garciadeblas4568a372021-03-24 09:19:48 +0100514 "Invalid query string '{}'. Descriptor does not contain '{}'".format(
515 k, kitem_old
516 )
517 )
tiernob24258a2018-10-04 18:39:49 +0200518 except ValueError:
garciadeblas4568a372021-03-24 09:19:48 +0100519 raise EngineException(
520 "Invalid query string '{}'. Expected integer index list instead of '{}'".format(
521 k, kitem
522 )
523 )
tiernob24258a2018-10-04 18:39:49 +0200524 except IndexError:
525 raise EngineException(
garciadeblas4568a372021-03-24 09:19:48 +0100526 "Invalid query string '{}'. Index '{}' out of range".format(
527 k, kitem_old
528 )
529 )
tierno1c38f2f2020-03-24 11:51:39 +0000530 except YAMLError:
531 raise EngineException("Invalid query string '{}' yaml format".format(k))
tiernob24258a2018-10-04 18:39:49 +0200532
Frank Bryden19b97522020-07-10 12:32:02 +0000533 def sol005_projection(self, data):
534 # Projection was moved to child classes
535 return data
536
K Sai Kiran57589552021-01-27 21:38:34 +0530537 def show(self, session, _id, filter_q=None, api_req=False):
tiernob24258a2018-10-04 18:39:49 +0200538 """
539 Get complete information on an topic
tierno65ca36d2019-02-12 19:27:52 +0100540 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200541 :param _id: server internal id
K Sai Kiran57589552021-01-27 21:38:34 +0530542 :param filter_q: dict: query parameter
Frank Bryden19b97522020-07-10 12:32:02 +0000543 :param api_req: True if this call is serving an external API request. False if serving internal request.
tiernob24258a2018-10-04 18:39:49 +0200544 :return: dictionary, raise exception if not found.
545 """
tierno1f029d82019-06-13 22:37:04 +0000546 if not self.multiproject:
547 filter_db = {}
548 else:
549 filter_db = self._get_project_filter(session)
delacruzramoc061f562019-04-05 11:00:02 +0200550 # To allow project&user addressing by name AS WELL AS _id
551 filter_db[BaseTopic.id_field(self.topic, _id)] = _id
Frank Bryden19b97522020-07-10 12:32:02 +0000552 data = self.db.get_one(self.topic, filter_db)
553
554 # Only perform SOL005 projection if we are serving an external request
555 if api_req:
556 self.sol005_projection(data)
Frank Bryden19b97522020-07-10 12:32:02 +0000557 return data
garciadeblas4568a372021-03-24 09:19:48 +0100558
tiernob24258a2018-10-04 18:39:49 +0200559 # TODO transform data for SOL005 URL requests
560 # TODO remove _admin if not admin
561
562 def get_file(self, session, _id, path=None, accept_header=None):
563 """
564 Only implemented for descriptor topics. Return the file content of a descriptor
tierno65ca36d2019-02-12 19:27:52 +0100565 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200566 :param _id: Identity of the item to get content
567 :param path: artifact path or "$DESCRIPTOR" or None
568 :param accept_header: Content of Accept header. Must contain applition/zip or/and text/plain
569 :return: opened file or raises an exception
570 """
garciadeblas4568a372021-03-24 09:19:48 +0100571 raise EngineException(
572 "Method get_file not valid for this topic", HTTPStatus.INTERNAL_SERVER_ERROR
573 )
tiernob24258a2018-10-04 18:39:49 +0200574
Frank Bryden19b97522020-07-10 12:32:02 +0000575 def list(self, session, filter_q=None, api_req=False):
tiernob24258a2018-10-04 18:39:49 +0200576 """
577 Get a list of the topic that matches a filter
578 :param session: contains the used login username and working project
579 :param filter_q: filter of data to be applied
Frank Bryden19b97522020-07-10 12:32:02 +0000580 :param api_req: True if this call is serving an external API request. False if serving internal request.
tiernob24258a2018-10-04 18:39:49 +0200581 :return: The list, it can be empty if no one match the filter.
582 """
583 if not filter_q:
584 filter_q = {}
tierno1f029d82019-06-13 22:37:04 +0000585 if self.multiproject:
586 filter_q.update(self._get_project_filter(session))
tiernob24258a2018-10-04 18:39:49 +0200587
588 # TODO transform data for SOL005 URL requests. Transform filtering
589 # TODO implement "field-type" query string SOL005
Frank Bryden19b97522020-07-10 12:32:02 +0000590 data = self.db.get_list(self.topic, filter_q)
591
592 # Only perform SOL005 projection if we are serving an external request
593 if api_req:
594 data = [self.sol005_projection(inst) for inst in data]
garciadeblas4568a372021-03-24 09:19:48 +0100595
Frank Bryden19b97522020-07-10 12:32:02 +0000596 return data
tiernob24258a2018-10-04 18:39:49 +0200597
tierno65ca36d2019-02-12 19:27:52 +0100598 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
tiernob24258a2018-10-04 18:39:49 +0200599 """
600 Creates a new entry into database.
601 :param rollback: list to append created items at database in case a rollback may to be done
tierno65ca36d2019-02-12 19:27:52 +0100602 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200603 :param indata: data to be inserted
604 :param kwargs: used to override the indata descriptor
605 :param headers: http request headers
tiernobdebce92019-07-01 15:36:49 +0000606 :return: _id, op_id:
607 _id: identity of the inserted data.
608 op_id: operation id if this is asynchronous, None otherwise
tiernob24258a2018-10-04 18:39:49 +0200609 """
610 try:
delacruzramo32bab472019-09-13 12:24:22 +0200611 if self.multiproject:
612 self.check_quota(session)
613
tiernob24258a2018-10-04 18:39:49 +0200614 content = self._remove_envelop(indata)
615
616 # Override descriptor with query string kwargs
617 self._update_input_with_kwargs(content, kwargs)
tierno65ca36d2019-02-12 19:27:52 +0100618 content = self._validate_input_new(content, force=session["force"])
619 self.check_conflict_on_new(session, content)
garciadeblas4568a372021-03-24 09:19:48 +0100620 op_id = self.format_on_new(
621 content, project_id=session["project_id"], make_public=session["public"]
622 )
tiernob24258a2018-10-04 18:39:49 +0200623 _id = self.db.create(self.topic, content)
624 rollback.append({"topic": self.topic, "_id": _id})
tiernobdebce92019-07-01 15:36:49 +0000625 if op_id:
626 content["op_id"] = op_id
tierno15a1f682019-10-16 09:00:13 +0000627 self._send_msg("created", content)
tiernobdebce92019-07-01 15:36:49 +0000628 return _id, op_id
tiernob24258a2018-10-04 18:39:49 +0200629 except ValidationError as e:
630 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
631
tierno65ca36d2019-02-12 19:27:52 +0100632 def upload_content(self, session, _id, indata, kwargs, headers):
tiernob24258a2018-10-04 18:39:49 +0200633 """
634 Only implemented for descriptor topics. Used for receiving content by chunks (with a transaction_id header
635 and/or gzip file. It will store and extract)
tierno65ca36d2019-02-12 19:27:52 +0100636 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200637 :param _id : the database id of entry to be updated
638 :param indata: http body request
639 :param kwargs: user query string to override parameters. NOT USED
640 :param headers: http request headers
tiernob24258a2018-10-04 18:39:49 +0200641 :return: True package has is completely uploaded or False if partial content has been uplodaed.
642 Raise exception on error
643 """
garciadeblas4568a372021-03-24 09:19:48 +0100644 raise EngineException(
645 "Method upload_content not valid for this topic",
646 HTTPStatus.INTERNAL_SERVER_ERROR,
647 )
tiernob24258a2018-10-04 18:39:49 +0200648
rshri2d386cb2024-07-05 14:35:51 +0000649 def create_gitname(self, content, session, _id=None):
650 if not self.multiproject:
651 _filter = {}
652 else:
653 _filter = self._get_project_filter(session)
garciadeblasb6025472024-08-15 09:50:55 +0200654 _filter["git_name"] = content["name"]
rshri2d386cb2024-07-05 14:35:51 +0000655 if _id:
656 _filter["_id.neq"] = _id
657 if self.db.get_one(
658 self.topic, _filter, fail_on_empty=False, fail_on_more=False
659 ):
660 n = 5
661 # using random.choices()
662 # generating random strings
663 res = "".join(random.choices(string.ascii_lowercase + string.digits, k=n))
664 res1 = content["name"]
665 new_name1 = res1 + res
666 new_name = new_name1.lower()
667 return new_name
668 else:
669 return content["name"]
670
671 def new_profile(self, rollback, session, indata=None, kwargs=None, headers=None):
672 step = "name unique check"
673 try:
674 self.check_unique_name(session, indata["name"])
675
676 step = "validating input parameters"
677 profile_request = self._remove_envelop(indata)
678 self._update_input_with_kwargs(profile_request, kwargs)
679 profile_request = self._validate_input_new(
680 profile_request, session["force"]
681 )
682 operation_params = profile_request
683
684 step = "filling profile details from input data"
685 profile_create = self._create_profile(profile_request, session)
686
687 step = "creating profile at database"
688 self.format_on_new(
689 profile_create, session["project_id"], make_public=session["public"]
690 )
691 profile_create["current_operation"] = None
692 op_id = self.format_on_operation(
693 profile_create,
694 "create",
695 operation_params,
696 )
697
698 _id = self.db.create(self.topic, profile_create)
699 rollback.append({"topic": self.topic, "_id": _id})
700 self.db.set_one(self.topic, {"_id": _id}, profile_create)
701 if op_id:
702 profile_create["op_id"] = op_id
703 self._send_msg("profile_create", {"profile_id": _id, "operation_id": op_id})
704
705 return _id, None
706 except (
707 ValidationError,
708 EngineException,
709 DbException,
710 MsgException,
711 FsException,
712 ) as e:
713 raise type(e)("{} while '{}'".format(e, step), http_code=e.http_code)
714
715 def _create_profile(self, profile_request, session):
716 profile_desc = {
717 "name": profile_request["name"],
718 "description": profile_request["description"],
719 "default": False,
720 "git_name": self.create_gitname(profile_request, session),
721 "state": "IN_CREATION",
722 "operatingState": "IN_PROGRESS",
723 "resourceState": "IN_PROGRESS.REQUEST_RECEIVED",
724 }
725 return profile_desc
726
727 def default_profile(
728 self, rollback, session, indata=None, kwargs=None, headers=None
729 ):
730 step = "validating input parameters"
731 try:
732 profile_request = self._remove_envelop(indata)
733 self._update_input_with_kwargs(profile_request, kwargs)
734 operation_params = profile_request
735
736 step = "filling profile details from input data"
737 profile_create = self._create_default_profile(profile_request, session)
738
739 step = "creating profile at database"
740 self.format_on_new(
741 profile_create, session["project_id"], make_public=session["public"]
742 )
743 profile_create["current_operation"] = None
744 self.format_on_operation(
745 profile_create,
746 "create",
747 operation_params,
748 )
749 _id = self.db.create(self.topic, profile_create)
750 rollback.append({"topic": self.topic, "_id": _id})
751 return _id
752 except (
753 ValidationError,
754 EngineException,
755 DbException,
756 MsgException,
757 FsException,
758 ) as e:
759 raise type(e)("{} while '{}'".format(e, step), http_code=e.http_code)
760
761 def _create_default_profile(self, profile_request, session):
762 profile_desc = {
763 "name": profile_request["name"],
764 "description": f"{self.topic} profile for cluster {profile_request['name']}",
765 "default": True,
766 "git_name": self.create_gitname(profile_request, session),
767 "state": "IN_CREATION",
768 "operatingState": "IN_PROGRESS",
769 "resourceState": "IN_PROGRESS.REQUEST_RECEIVED",
770 }
771 return profile_desc
772
tiernob24258a2018-10-04 18:39:49 +0200773 def delete_list(self, session, filter_q=None):
774 """
775 Delete a several entries of a topic. This is for internal usage and test only, not exposed to NBI API
tierno65ca36d2019-02-12 19:27:52 +0100776 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200777 :param filter_q: filter of data to be applied
778 :return: The deleted list, it can be empty if no one match the filter.
779 """
780 # TODO add admin to filter, validate rights
781 if not filter_q:
782 filter_q = {}
tierno1f029d82019-06-13 22:37:04 +0000783 if self.multiproject:
784 filter_q.update(self._get_project_filter(session))
tiernob24258a2018-10-04 18:39:49 +0200785 return self.db.del_list(self.topic, filter_q)
786
tiernobee3bad2019-12-05 12:26:01 +0000787 def delete_extra(self, session, _id, db_content, not_send_msg=None):
tierno65ca36d2019-02-12 19:27:52 +0100788 """
789 Delete other things apart from database entry of a item _id.
790 e.g.: other associated elements at database and other file system storage
791 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
792 :param _id: server internal id
tiernob4844ab2019-05-23 08:42:12 +0000793 :param db_content: The database content of the _id. It is already deleted when reached this method, but the
794 content is needed in same cases
tiernobee3bad2019-12-05 12:26:01 +0000795 :param not_send_msg: To not send message (False) or store content (list) instead
tiernob4844ab2019-05-23 08:42:12 +0000796 :return: None if ok or raises EngineException with the problem
tierno65ca36d2019-02-12 19:27:52 +0100797 """
798 pass
799
tiernobee3bad2019-12-05 12:26:01 +0000800 def delete(self, session, _id, dry_run=False, not_send_msg=None):
tiernob24258a2018-10-04 18:39:49 +0200801 """
802 Delete item by its internal _id
tierno65ca36d2019-02-12 19:27:52 +0100803 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
tiernob24258a2018-10-04 18:39:49 +0200804 :param _id: server internal id
tiernob24258a2018-10-04 18:39:49 +0200805 :param dry_run: make checking but do not delete
tiernobee3bad2019-12-05 12:26:01 +0000806 :param not_send_msg: To not send message (False) or store content (list) instead
tiernobdebce92019-07-01 15:36:49 +0000807 :return: operation id (None if there is not operation), raise exception if error or not found, conflict, ...
tiernob24258a2018-10-04 18:39:49 +0200808 """
tiernob4844ab2019-05-23 08:42:12 +0000809 # To allow addressing projects and users by name AS WELL AS by _id
tiernof5f2e3f2020-03-23 14:42:10 +0000810 if not self.multiproject:
811 filter_q = {}
812 else:
813 filter_q = self._get_project_filter(session)
814 filter_q[self.id_field(self.topic, _id)] = _id
rshri2d386cb2024-07-05 14:35:51 +0000815
tiernob4844ab2019-05-23 08:42:12 +0000816 item_content = self.db.get_one(self.topic, filter_q)
kayal2001f71c2e82024-06-25 15:26:24 +0530817 nsd_id = item_content.get("_id")
tiernob4844ab2019-05-23 08:42:12 +0000818
rshri2d386cb2024-07-05 14:35:51 +0000819 if (
820 self.topic == "k8sinfra_controller"
821 or self.topic == "k8sinfra_config"
822 or self.topic == "k8sapp"
823 or self.topic == "k8sresource"
824 or self.topic == "clusters"
825 ):
826 if "state" in item_content:
827 item_content["state"] = "IN_DELETION"
828 item_content["operatingState"] = "PROCESSING"
829 self.db.set_one(self.topic, {"_id": _id}, item_content)
830
831 item_content_1 = self.db.get_one(self.topic, filter_q)
832 item_content_1["current_operation"] = None
833 op_id = self.format_on_operation(
834 item_content_1,
835 "delete",
836 None,
837 )
838
tiernob4844ab2019-05-23 08:42:12 +0000839 self.check_conflict_on_del(session, _id, item_content)
kayal2001f71c2e82024-06-25 15:26:24 +0530840
841 # While deteling ns descriptor associated ns config template should also get deleted.
842 if self.topic == "nsds":
843 ns_config_template_content = self.db.get_list(
844 "ns_config_template", {"nsdId": _id}
845 )
846 for template_content in ns_config_template_content:
847 if template_content is not None:
848 if template_content.get("nsdId") == nsd_id:
849 ns_config_template_id = template_content.get("_id")
850 self.db.del_one("ns_config_template", {"nsdId": nsd_id})
851 self.delete_extra(
852 session,
853 ns_config_template_id,
854 template_content,
855 not_send_msg=not_send_msg,
856 )
tierno65ca36d2019-02-12 19:27:52 +0100857 if dry_run:
858 return None
859 if self.multiproject and session["project_id"]:
tiernof5f2e3f2020-03-23 14:42:10 +0000860 # remove reference from project_read if there are more projects referencing it. If it last one,
861 # do not remove reference, but delete
garciadeblas4568a372021-03-24 09:19:48 +0100862 other_projects_referencing = next(
863 (
864 p
865 for p in item_content["_admin"]["projects_read"]
866 if p not in session["project_id"] and p != "ANY"
867 ),
868 None,
869 )
tiernof5f2e3f2020-03-23 14:42:10 +0000870 # check if there are projects referencing it (apart from ANY, that means, public)....
871 if other_projects_referencing:
872 # remove references but not delete
garciadeblas4568a372021-03-24 09:19:48 +0100873 update_dict_pull = {
874 "_admin.projects_read": session["project_id"],
875 "_admin.projects_write": session["project_id"],
876 }
877 self.db.set_one(
878 self.topic, filter_q, update_dict=None, pull_list=update_dict_pull
879 )
tiernobdebce92019-07-01 15:36:49 +0000880 return None
tiernof5f2e3f2020-03-23 14:42:10 +0000881 else:
garciadeblas4568a372021-03-24 09:19:48 +0100882 can_write = next(
883 (
884 p
885 for p in item_content["_admin"]["projects_write"]
886 if p == "ANY" or p in session["project_id"]
887 ),
888 None,
889 )
tiernof5f2e3f2020-03-23 14:42:10 +0000890 if not can_write:
garciadeblas4568a372021-03-24 09:19:48 +0100891 raise EngineException(
892 "You have not write permission to delete it",
893 http_code=HTTPStatus.UNAUTHORIZED,
894 )
tiernof5f2e3f2020-03-23 14:42:10 +0000895 # delete
rshri2d386cb2024-07-05 14:35:51 +0000896 if (
897 self.topic == "k8sinfra_controller"
898 or self.topic == "k8sinfra_config"
899 or self.topic == "k8sapp"
900 or self.topic == "k8sresource"
901 ):
902 self.db.set_one(self.topic, {"_id": _id}, item_content_1)
903 self._send_msg(
904 "delete",
905 {"profile_id": _id, "operation_id": op_id},
906 not_send_msg=not_send_msg,
907 )
908 elif self.topic == "clusters":
909 self.db.set_one("clusters", {"_id": _id}, item_content_1)
910 self._send_msg(
911 "delete",
912 {"cluster_id": _id, "operation_id": op_id},
913 not_send_msg=not_send_msg,
914 )
915 else:
916 self.db.del_one(self.topic, filter_q)
917 self.delete_extra(session, _id, item_content, not_send_msg=not_send_msg)
918 self._send_msg("deleted", {"_id": _id}, not_send_msg=not_send_msg)
tiernobdebce92019-07-01 15:36:49 +0000919 return None
tiernob24258a2018-10-04 18:39:49 +0200920
tierno65ca36d2019-02-12 19:27:52 +0100921 def edit(self, session, _id, indata=None, kwargs=None, content=None):
922 """
923 Change the content of an item
924 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
925 :param _id: server internal id
926 :param indata: contains the changes to apply
927 :param kwargs: modifies indata
928 :param content: original content of the item
tiernobdebce92019-07-01 15:36:49 +0000929 :return: op_id: operation id if this is processed asynchronously, None otherwise
tierno65ca36d2019-02-12 19:27:52 +0100930 """
tiernob24258a2018-10-04 18:39:49 +0200931 indata = self._remove_envelop(indata)
932
933 # Override descriptor with query string kwargs
934 if kwargs:
935 self._update_input_with_kwargs(indata, kwargs)
936 try:
rshri2d386cb2024-07-05 14:35:51 +0000937 if (
938 self.topic == "k8sinfra_controller"
939 or self.topic == "k8sinfra_config"
940 or self.topic == "k8sapp"
941 or self.topic == "k8sresource"
942 ):
943 check = self.db.get_one(self.topic, {"_id": _id})
944 if check["default"] is True:
945 raise EngineException(
946 "Cannot edit default profiles",
947 HTTPStatus.UNPROCESSABLE_ENTITY,
948 )
949 if "name" in indata:
950 if check["name"] == indata["name"]:
951 pass
952 else:
953 self.check_unique_name(session, indata["name"])
tierno65ca36d2019-02-12 19:27:52 +0100954 if indata and session.get("set_project"):
garciadeblas4568a372021-03-24 09:19:48 +0100955 raise EngineException(
956 "Cannot edit content and set to project (query string SET_PROJECT) at same time",
957 HTTPStatus.UNPROCESSABLE_ENTITY,
958 )
tiernob24258a2018-10-04 18:39:49 +0200959 # TODO self._check_edition(session, indata, _id, force)
960 if not content:
961 content = self.show(session, _id)
Frank Brydendeba68e2020-07-27 13:55:11 +0000962 indata = self._validate_input_edit(indata, content, force=session["force"])
tiernob24258a2018-10-04 18:39:49 +0200963 deep_update_rfc7396(content, indata)
tiernobdebce92019-07-01 15:36:49 +0000964
965 # To allow project addressing by name AS WELL AS _id. Get the _id, just in case the provided one is a name
966 _id = content.get("_id") or _id
967
bravofb995ea22021-02-10 10:57:52 -0300968 content = self.check_conflict_on_edit(session, content, indata, _id=_id)
tiernobdebce92019-07-01 15:36:49 +0000969 op_id = self.format_on_edit(content, indata)
970
971 self.db.replace(self.topic, _id, content)
tiernob24258a2018-10-04 18:39:49 +0200972
973 indata.pop("_admin", None)
tiernobdebce92019-07-01 15:36:49 +0000974 if op_id:
975 indata["op_id"] = op_id
tiernob24258a2018-10-04 18:39:49 +0200976 indata["_id"] = _id
rshri2d386cb2024-07-05 14:35:51 +0000977 if (
978 self.topic == "k8sinfra_controller"
979 or self.topic == "k8sinfra_config"
980 or self.topic == "k8sapp"
981 or self.topic == "k8sresource"
982 ):
983 pass
984 else:
985 self._send_msg("edited", indata)
tiernobdebce92019-07-01 15:36:49 +0000986 return op_id
tiernob24258a2018-10-04 18:39:49 +0200987 except ValidationError as e:
988 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
rshri2d386cb2024-07-05 14:35:51 +0000989
990 def detach(self, session, _id, profile_type):
991 # To detach the profiles from every cluster
992 filter_q = {}
993 existing_clusters = self.db.get_list("clusters", filter_q)
994 existing_clusters_profiles = [
995 profile["_id"]
996 for profile in existing_clusters
997 if profile.get("profile_type", _id)
998 ]
999 update_dict = None
1000 for profile in existing_clusters_profiles:
1001 filter_q = {"_id": profile}
1002 data = self.db.get_one("clusters", filter_q)
1003 if profile_type in data:
1004 profile_ids = data[profile_type]
1005 if _id in profile_ids:
1006 profile_ids.remove(_id)
1007 update_dict = {profile_type: profile_ids}
1008 self.db.set_one("clusters", filter_q, update_dict)