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

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 

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 

26 

27__author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>" 

28 

29 

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) 

34 

35 

36class NBIBadArgumentsException(Exception): 

37 """ 

38 Bad argument values exception 

39 """ 

40 

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 

45 

46 def __str__(self): 

47 return "{}, Bad arguments: {}".format(self.message, self.bad_args) 

48 

49 

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 

63 

64 

65def detect_descriptor_usage(descriptor: dict, db_collection: str, db: object) -> bool: 

66 """Detect the descriptor usage state. 

67 

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 

72 

73 Returns: 

74 True if descriptor is in use else None 

75 

76 """ 

77 try: 

78 if not descriptor: 

79 raise NBIBadArgumentsException( 

80 "Argument is mandatory and can not be empty", "descriptor" 

81 ) 

82 

83 if not db: 

84 raise NBIBadArgumentsException("A valid DB object should be provided", "db") 

85 

86 search_dict = { 

87 "vnfds": ("vnfrs", "vnfd-id"), 

88 "nsds": ("nsrs", "nsd-id"), 

89 "ns_config_template": ("ns_config_template", "_id"), 

90 } 

91 

92 if db_collection not in search_dict: 

93 raise NBIBadArgumentsException( 

94 "db_collection should be equal to vnfds or nsds", "db_collection" 

95 ) 

96 

97 record_list = db.get_list( 

98 search_dict[db_collection][0], 

99 {search_dict[db_collection][1]: descriptor["_id"]}, 

100 ) 

101 

102 if record_list: 

103 return True 

104 

105 except (DbException, KeyError, NBIBadArgumentsException) as error: 

106 raise EngineException( 

107 f"Error occured while detecting the descriptor usage: {error}" 

108 ) 

109 

110 

111def update_descriptor_usage_state( 

112 descriptor: dict, db_collection: str, db: object 

113) -> None: 

114 """Updates the descriptor usage state. 

115 

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 

120 

121 Returns: 

122 None 

123 

124 """ 

125 try: 

126 descriptor_update = { 

127 "_admin.usageState": "NOT_IN_USE", 

128 } 

129 

130 if detect_descriptor_usage(descriptor, db_collection, db): 

131 descriptor_update = { 

132 "_admin.usageState": "IN_USE", 

133 } 

134 

135 db.set_one( 

136 db_collection, {"_id": descriptor["_id"]}, update_dict=descriptor_update 

137 ) 

138 

139 except (DbException, KeyError, NBIBadArgumentsException) as error: 

140 raise EngineException( 

141 f"Error occured while updating the descriptor usage state: {error}" 

142 ) 

143 

144 

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 

154 

155 

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) 

162 

163 

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 

184 

185 

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 

194 

195 default_quota = 500 

196 

197 # Alternative ID Fields for some Topics 

198 alt_id_field = {"projects": "name", "users": "username", "roles": "name"} 

199 

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 

206 

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" 

214 

215 @staticmethod 

216 def _remove_envelop(indata=None): 

217 if not indata: 

218 return {} 

219 return indata 

220 

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 ) 

249 

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 

260 

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 

271 

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"]) 

290 

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") 

299 

300 if session.get("PROJECT.ne"): 

301 project_filter_n.append(session["PROJECT.ne"]) 

302 

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 

317 

318 return p_filter 

319 

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 

328 

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") 

353 

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) 

359 

360 return final_content 

361 

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 ) 

384 

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 

410 

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 

423 

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) 

432 

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 

442 

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)) 

506 

507 def sol005_projection(self, data): 

508 # Projection was moved to child classes 

509 return data 

510 

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) 

527 

528 # Only perform SOL005 projection if we are serving an external request 

529 if api_req: 

530 self.sol005_projection(data) 

531 return data 

532 

533 # TODO transform data for SOL005 URL requests 

534 # TODO remove _admin if not admin 

535 

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 ) 

548 

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)) 

561 

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) 

565 

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] 

569 

570 return data 

571 

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) 

587 

588 content = self._remove_envelop(indata) 

589 

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) 

605 

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 ) 

622 

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) 

636 

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 

649 

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 {} 

655 

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 

671 

672 item_content = self.db.get_one(self.topic, filter_q) 

673 nsd_id = item_content.get("_id") 

674 

675 self.check_conflict_on_del(session, _id, item_content) 

676 

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 

745 

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 {} 

751 

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) 

763 

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) 

778 

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 

781 

782 content = self.check_conflict_on_edit(session, content, indata, _id=_id) 

783 op_id = self.format_on_edit(content, indata) 

784 

785 self.logger.info(f"indata is : {indata}") 

786 

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}") 

791 

792 self.db.replace(self.topic, _id, content) 

793 

794 indata.pop("_admin", None) 

795 if op_id: 

796 indata["op_id"] = op_id 

797 indata["_id"] = _id 

798 

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) 

808 

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()