From 6d07d9ca53cda75f80563a86c27970c0f0681a4a Mon Sep 17 00:00:00 2001 From: pycook Date: Thu, 23 Nov 2023 10:59:09 +0800 Subject: [PATCH] feat(api): issue #212 --- cmdb-api/api/commands/click_cmdb.py | 6 + cmdb-api/api/lib/cmdb/ci.py | 46 ++++-- cmdb-api/api/lib/cmdb/ci_type.py | 28 ++++ cmdb-api/api/lib/cmdb/const.py | 1 + cmdb-api/api/lib/cmdb/preference.py | 51 +++++- cmdb-api/api/lib/cmdb/resp_format.py | 1 + .../api/lib/cmdb/search/ci_relation/search.py | 148 ++++++++++++++---- cmdb-api/api/models/cmdb.py | 2 + cmdb-api/api/tasks/cmdb.py | 54 +++++-- cmdb-api/api/views/cmdb/ci_relation.py | 20 ++- cmdb-api/api/views/cmdb/ci_type_relation.py | 8 + 11 files changed, 294 insertions(+), 71 deletions(-) diff --git a/cmdb-api/api/commands/click_cmdb.py b/cmdb-api/api/commands/click_cmdb.py index 0e42804..51f54d2 100644 --- a/cmdb-api/api/commands/click_cmdb.py +++ b/cmdb-api/api/commands/click_cmdb.py @@ -19,6 +19,7 @@ from api.lib.cmdb.cache import AttributeCache from api.lib.cmdb.const import PermEnum from api.lib.cmdb.const import REDIS_PREFIX_CI from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION +from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION2 from api.lib.cmdb.const import ResourceTypeEnum from api.lib.cmdb.const import RoleEnum from api.lib.cmdb.const import ValueTypeEnum @@ -49,12 +50,17 @@ def cmdb_init_cache(): ci_relations = CIRelation.get_by(to_dict=False) relations = dict() + relations2 = dict() for cr in ci_relations: relations.setdefault(cr.first_ci_id, {}).update({cr.second_ci_id: cr.second_ci.type_id}) + if cr.ancestor_ids: + relations2.setdefault(cr.ancestor_ids, {}).update({cr.second_ci_id: cr.second_ci.type_id}) for i in relations: relations[i] = json.dumps(relations[i]) if relations: rd.create_or_update(relations, REDIS_PREFIX_CI_RELATION) + if relations2: + rd.create_or_update(relations2, REDIS_PREFIX_CI_RELATION2) es = None if current_app.config.get("USE_ES"): diff --git a/cmdb-api/api/lib/cmdb/ci.py b/cmdb-api/api/lib/cmdb/ci.py index 4e8b9ad..1876a8f 100644 --- a/cmdb-api/api/lib/cmdb/ci.py +++ b/cmdb-api/api/lib/cmdb/ci.py @@ -182,6 +182,9 @@ class CIManager(object): need_children and res.update(CIRelationManager.get_children(ci_id, ret_key=ret_key)) # one floor ci_type = CITypeCache.get(ci.type_id) + if not ci_type: + return res + res["ci_type"] = ci_type.name fields = CITypeAttributeManager.get_attr_names_by_type_id(ci.type_id) if not fields else fields @@ -518,11 +521,13 @@ class CIManager(object): item.delete(commit=False) for item in CIRelation.get_by(first_ci_id=ci_id, to_dict=False): - ci_relation_delete.apply_async(args=(item.first_ci_id, item.second_ci_id), queue=CMDB_QUEUE) + ci_relation_delete.apply_async( + args=(item.first_ci_id, item.second_ci_id, item.ancestor_ids), queue=CMDB_QUEUE) item.delete(commit=False) for item in CIRelation.get_by(second_ci_id=ci_id, to_dict=False): - ci_relation_delete.apply_async(args=(item.first_ci_id, item.second_ci_id), queue=CMDB_QUEUE) + ci_relation_delete.apply_async( + args=(item.first_ci_id, item.second_ci_id, item.ancestor_ids), queue=CMDB_QUEUE) item.delete(commit=False) ad_ci = AutoDiscoveryCI.get_by(ci_id=ci_id, to_dict=False, first=True) @@ -886,12 +891,14 @@ class CIRelationManager(object): @classmethod def get_ancestor_ids(cls, ci_ids, level=1): - for _ in range(level): - cis = db.session.query(CIRelation.first_ci_id).filter( + level2ids = dict() + for _level in range(1, level + 1): + cis = db.session.query(CIRelation.first_ci_id, CIRelation.ancestor_ids).filter( CIRelation.second_ci_id.in_(ci_ids)).filter(CIRelation.deleted.is_(False)) ci_ids = [i.first_ci_id for i in cis] + level2ids[_level + 1] = {int(i.ancestor_ids.split(',')[-1]) for i in cis if i.ancestor_ids} - return ci_ids + return ci_ids, level2ids @staticmethod def _check_constraint(first_ci_id, first_type_id, second_ci_id, second_type_id, type_relation): @@ -918,13 +925,14 @@ class CIRelationManager(object): return abort(400, ErrFormat.relation_constraint.format("1-N")) @classmethod - def add(cls, first_ci_id, second_ci_id, more=None, relation_type_id=None): + def add(cls, first_ci_id, second_ci_id, more=None, relation_type_id=None, ancestor_ids=None): first_ci = CIManager.confirm_ci_existed(first_ci_id) second_ci = CIManager.confirm_ci_existed(second_ci_id) existed = CIRelation.get_by(first_ci_id=first_ci_id, second_ci_id=second_ci_id, + ancestor_ids=ancestor_ids, to_dict=False, first=True) if existed is not None: @@ -960,11 +968,12 @@ class CIRelationManager(object): existed = CIRelation.create(first_ci_id=first_ci_id, second_ci_id=second_ci_id, - relation_type_id=relation_type_id) + relation_type_id=relation_type_id, + ancestor_ids=ancestor_ids) CIRelationHistoryManager().add(existed, OperateType.ADD) - ci_relation_cache.apply_async(args=(first_ci_id, second_ci_id), queue=CMDB_QUEUE) + ci_relation_cache.apply_async(args=(first_ci_id, second_ci_id, ancestor_ids), queue=CMDB_QUEUE) if more is not None: existed.upadte(more=more) @@ -988,53 +997,56 @@ class CIRelationManager(object): his_manager = CIRelationHistoryManager() his_manager.add(cr, operate_type=OperateType.DELETE) - ci_relation_delete.apply_async(args=(cr.first_ci_id, cr.second_ci_id), queue=CMDB_QUEUE) + ci_relation_delete.apply_async(args=(cr.first_ci_id, cr.second_ci_id, cr.ancestor_ids), queue=CMDB_QUEUE) return cr_id @classmethod - def delete_2(cls, first_ci_id, second_ci_id): + def delete_2(cls, first_ci_id, second_ci_id, ancestor_ids=None): cr = CIRelation.get_by(first_ci_id=first_ci_id, second_ci_id=second_ci_id, + ancestor_ids=ancestor_ids, to_dict=False, first=True) - ci_relation_delete.apply_async(args=(first_ci_id, second_ci_id), queue=CMDB_QUEUE) + ci_relation_delete.apply_async(args=(first_ci_id, second_ci_id, ancestor_ids), queue=CMDB_QUEUE) - return cls.delete(cr.id) + return cr and cls.delete(cr.id) @classmethod - def batch_update(cls, ci_ids, parents, children): + def batch_update(cls, ci_ids, parents, children, ancestor_ids=None): """ only for many to one :param ci_ids: :param parents: :param children: + :param ancestor_ids: :return: """ if isinstance(parents, list): for parent_id in parents: for ci_id in ci_ids: - cls.add(parent_id, ci_id) + cls.add(parent_id, ci_id, ancestor_ids=ancestor_ids) if isinstance(children, list): for child_id in children: for ci_id in ci_ids: - cls.add(ci_id, child_id) + cls.add(ci_id, child_id, ancestor_ids=ancestor_ids) @classmethod - def batch_delete(cls, ci_ids, parents): + def batch_delete(cls, ci_ids, parents, ancestor_ids=None): """ only for many to one :param ci_ids: :param parents: + :param ancestor_ids: :return: """ if isinstance(parents, list): for parent_id in parents: for ci_id in ci_ids: - cls.delete_2(parent_id, ci_id) + cls.delete_2(parent_id, ci_id, ancestor_ids=ancestor_ids) class CITriggerManager(object): diff --git a/cmdb-api/api/lib/cmdb/ci_type.py b/cmdb-api/api/lib/cmdb/ci_type.py index 4adefad..b7e74d5 100644 --- a/cmdb-api/api/lib/cmdb/ci_type.py +++ b/cmdb-api/api/lib/cmdb/ci_type.py @@ -637,6 +637,16 @@ class CITypeRelationManager(object): current_app.logger.warning(str(e)) return abort(400, ErrFormat.circular_dependency_error) + if constraint == ConstraintEnum.Many2Many: + other_c = CITypeRelation.get_by(parent_id=p.id, constraint=ConstraintEnum.Many2Many, + to_dict=False, first=True) + other_p = CITypeRelation.get_by(child_id=c.id, constraint=ConstraintEnum.Many2Many, + to_dict=False, first=True) + if other_c and other_c.child_id != c.id: + return abort(400, ErrFormat.m2m_relation_constraint.format(p.name, other_c.child.name)) + if other_p and other_p.parent_id != p.id: + return abort(400, ErrFormat.m2m_relation_constraint.format(other_p.parent.name, c.name)) + existed = cls._get(p.id, c.id) if existed is not None: existed.update(relation_type_id=relation_type_id, @@ -686,6 +696,24 @@ class CITypeRelationManager(object): cls.delete(ctr.id) + @staticmethod + def get_level2constraint(root_id, level): + level = level + 1 if level == 1 else level + ci = CI.get_by_id(root_id) + if ci is None: + return dict() + + root_id = ci.type_id + level2constraint = dict() + for lv in range(1, int(level) + 1): + for i in CITypeRelation.get_by(parent_id=root_id, to_dict=False): + if i.constraint == ConstraintEnum.Many2Many: + root_id = i.child_id + level2constraint[lv] = ConstraintEnum.Many2Many + break + + return level2constraint + class CITypeAttributeGroupManager(object): cls = CITypeAttributeGroup diff --git a/cmdb-api/api/lib/cmdb/const.py b/cmdb-api/api/lib/cmdb/const.py index dc9497a..c48fd1b 100644 --- a/cmdb-api/api/lib/cmdb/const.py +++ b/cmdb-api/api/lib/cmdb/const.py @@ -100,6 +100,7 @@ class AttributeDefaultValueEnum(BaseEnum): CMDB_QUEUE = "one_cmdb_async" REDIS_PREFIX_CI = "ONE_CMDB" REDIS_PREFIX_CI_RELATION = "CMDB_CI_RELATION" +REDIS_PREFIX_CI_RELATION2 = "CMDB_CI_RELATION2" BUILTIN_KEYWORDS = {'id', '_id', 'ci_id', 'type', '_type', 'ci_type'} diff --git a/cmdb-api/api/lib/cmdb/preference.py b/cmdb-api/api/lib/cmdb/preference.py index eec09ad..e7e06ed 100644 --- a/cmdb-api/api/lib/cmdb/preference.py +++ b/cmdb-api/api/lib/cmdb/preference.py @@ -14,7 +14,10 @@ from api.lib.cmdb.attribute import AttributeManager from api.lib.cmdb.cache import AttributeCache from api.lib.cmdb.cache import CITypeAttributesCache from api.lib.cmdb.cache import CITypeCache -from api.lib.cmdb.const import PermEnum, ResourceTypeEnum, RoleEnum +from api.lib.cmdb.const import ConstraintEnum +from api.lib.cmdb.const import PermEnum +from api.lib.cmdb.const import ResourceTypeEnum +from api.lib.cmdb.const import RoleEnum from api.lib.cmdb.perms import CIFilterPermsCRUD from api.lib.cmdb.resp_format import ErrFormat from api.lib.exception import AbortException @@ -229,14 +232,28 @@ class PreferenceManager(object): if not parents: return - for l in leaf: - _find_parent(l) + for _l in leaf: + _find_parent(_l) for node_id in node2show_types: node2show_types[node_id] = [CITypeCache.get(i).to_dict() for i in set(node2show_types[node_id])] + topo_flatten = list(toposort.toposort_flatten(topo)) + level2constraint = {} + for i, _ in enumerate(topo_flatten[1:]): + ctr = CITypeRelation.get_by( + parent_id=topo_flatten[i], child_id=topo_flatten[i + 1], first=True, to_dict=False) + level2constraint[i + 1] = ctr and ctr.constraint + + if leaf2show_types.get(topo_flatten[-1]): + ctr = CITypeRelation.get_by( + parent_id=topo_flatten[-1], + child_id=leaf2show_types[topo_flatten[-1]][0], first=True, to_dict=False) + level2constraint[len(topo_flatten)] = ctr and ctr.constraint + result[view_name] = dict(topo=list(map(list, toposort.toposort(topo))), - topo_flatten=list(toposort.toposort_flatten(topo)), + topo_flatten=topo_flatten, + level2constraint=level2constraint, leaf=leaf, leaf2show_types=leaf2show_types, node2show_types=node2show_types, @@ -338,3 +355,29 @@ class PreferenceManager(object): for i in PreferenceTreeView.get_by(type_id=type_id, uid=uid, to_dict=False): i.soft_delete() + + @staticmethod + def can_edit_relation(parent_id, child_id): + views = PreferenceRelationView.get_by(to_dict=False) + for view in views: + has_m2m = False + last_node_id = None + for cr in view.cr_ids: + _rel = CITypeRelation.get_by(parent_id=cr['parent_id'], child_id=cr['child_id'], + first=True, to_dict=False) + if _rel and _rel.constraint == ConstraintEnum.Many2Many: + has_m2m = True + + if parent_id == _rel.parent_id and child_id == _rel.child_id: + return False + + if _rel: + last_node_id = _rel.child_id + + if parent_id == last_node_id: + rels = CITypeRelation.get_by(parent_id=last_node_id, to_dict=False) + for rel in rels: + if rel.child_id == child_id and has_m2m: + return False + + return True diff --git a/cmdb-api/api/lib/cmdb/resp_format.py b/cmdb-api/api/lib/cmdb/resp_format.py index ef040be..b7a3495 100644 --- a/cmdb-api/api/lib/cmdb/resp_format.py +++ b/cmdb-api/api/lib/cmdb/resp_format.py @@ -31,6 +31,7 @@ class ErrFormat(CommonErrFormat): unique_key_required = "主键字段 {} 缺失" ci_is_already_existed = "CI 已经存在!" relation_constraint = "关系约束: {}, 校验失败 " + m2m_relation_constraint = "多对多关系 限制: 模型 {} <-> {} 已经存在多对多关系!" relation_not_found = "CI关系: {} 不存在" ci_search_Parentheses_invalid = "搜索表达式里小括号前不支持: 或、非" diff --git a/cmdb-api/api/lib/cmdb/search/ci_relation/search.py b/cmdb-api/api/lib/cmdb/search/ci_relation/search.py index 0d47e3d..627276e 100644 --- a/cmdb-api/api/lib/cmdb/search/ci_relation/search.py +++ b/cmdb-api/api/lib/cmdb/search/ci_relation/search.py @@ -1,6 +1,4 @@ # -*- coding:utf-8 -*- - - import json from collections import Counter @@ -10,11 +8,14 @@ from flask import current_app from api.extensions import rd from api.lib.cmdb.ci import CIRelationManager from api.lib.cmdb.ci_type import CITypeRelationManager +from api.lib.cmdb.const import ConstraintEnum from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION +from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION2 from api.lib.cmdb.resp_format import ErrFormat from api.lib.cmdb.search.ci.db.search import Search as SearchFromDB from api.lib.cmdb.search.ci.es.search import Search as SearchFromES from api.models.cmdb import CI +from api.models.cmdb import CIRelation class Search(object): @@ -26,7 +27,8 @@ class Search(object): page=1, count=None, sort=None, - reverse=False): + reverse=False, + ancestor_ids=None): self.orig_query = query self.fl = fl self.facet_field = facet_field @@ -38,25 +40,81 @@ class Search(object): self.level = level or 0 self.reverse = reverse - def _get_ids(self): + self.level2constraint = CITypeRelationManager.get_level2constraint( + root_id[0] if root_id and isinstance(root_id, list) else root_id, + level[0] if isinstance(level, list) and level else level) + + self.ancestor_ids = ancestor_ids + self.has_m2m = False + if self.ancestor_ids: + self.has_m2m = True + else: + level = level[0] if isinstance(level, list) and level else level + for _l, c in self.level2constraint.items(): + if _l < int(level) and c == ConstraintEnum.Many2Many: + self.has_m2m = True + + def _get_ids(self, ids): + if self.level[-1] == 1 and len(ids) == 1: + if self.ancestor_ids is None: + return [i.second_ci_id for i in CIRelation.get_by(first_ci_id=ids[0], to_dict=False)] + + else: + seconds = {i.second_ci_id for i in CIRelation.get_by(first_ci_id=ids[0], + ancestor_ids=self.ancestor_ids, + to_dict=False)} + + return list(seconds) + merge_ids = [] - ids = [self.root_id] if not isinstance(self.root_id, list) else self.root_id + key = [] + _tmp = [] for level in range(1, sorted(self.level)[-1] + 1): - _tmp = list(map(lambda x: list(json.loads(x).keys()), - filter(lambda x: x is not None, rd.get(ids, REDIS_PREFIX_CI_RELATION) or []))) + if not self.has_m2m: + _tmp = map(lambda x: json.loads(x).keys(), + filter(lambda x: x is not None, rd.get(ids, REDIS_PREFIX_CI_RELATION) or [])) + ids = [j for i in _tmp for j in i] + key, prefix = ids, REDIS_PREFIX_CI_RELATION + + else: + if not self.ancestor_ids: + if level == 1: + key, prefix = list(map(str, ids)), REDIS_PREFIX_CI_RELATION + else: + key = list(set(["{},{}".format(i, j) for idx, i in enumerate(key) for j in _tmp[idx]])) + prefix = REDIS_PREFIX_CI_RELATION2 + else: + if level == 1: + key, prefix = ["{},{}".format(self.ancestor_ids, i) for i in ids], REDIS_PREFIX_CI_RELATION2 + else: + key = list(set(["{},{}".format(i, j) for idx, i in enumerate(key) for j in _tmp[idx]])) + prefix = REDIS_PREFIX_CI_RELATION2 + + if not key: + return [] + + _tmp = list(map(lambda x: json.loads(x).keys() if x else [], rd.get(key, prefix) or [])) ids = [j for i in _tmp for j in i] + if level in self.level: merge_ids.extend(ids) return merge_ids - def _get_reverse_ids(self): + def _get_reverse_ids(self, ids): merge_ids = [] - ids = [self.root_id] if not isinstance(self.root_id, list) else self.root_id + level2ids = {} for level in range(1, sorted(self.level)[-1] + 1): - ids = CIRelationManager.get_ancestor_ids(ids, 1) + ids, _level2ids = CIRelationManager.get_ancestor_ids(ids, 1) + + if _level2ids.get(2): + level2ids[level + 1] = _level2ids[2] + if level in self.level: - merge_ids.extend(ids) + if level in level2ids and level2ids[level]: + merge_ids.extend(set(ids) & set(level2ids[level])) + else: + merge_ids.extend(ids) return merge_ids @@ -64,7 +122,7 @@ class Search(object): ids = [self.root_id] if not isinstance(self.root_id, list) else self.root_id cis = [CI.get_by_id(_id) or abort(404, ErrFormat.ci_not_found.format("id={}".format(_id))) for _id in ids] - merge_ids = self._get_ids() if not self.reverse else self._get_reverse_ids() + merge_ids = self._get_ids(ids) if not self.reverse else self._get_reverse_ids(ids) if not self.orig_query or ("_type:" not in self.orig_query and "type_id:" not in self.orig_query @@ -76,11 +134,11 @@ class Search(object): type_ids.extend(CITypeRelationManager.get_child_type_ids(ci.type_id, level)) else: type_ids.extend(CITypeRelationManager.get_parent_type_ids(ci.type_id, level)) - type_ids = list(set(type_ids)) + type_ids = set(type_ids) if self.orig_query: - self.orig_query = "_type:({0}),{1}".format(";".join(list(map(str, type_ids))), self.orig_query) + self.orig_query = "_type:({0}),{1}".format(";".join(map(str, type_ids)), self.orig_query) else: - self.orig_query = "_type:({0})".format(";".join(list(map(str, type_ids)))) + self.orig_query = "_type:({0})".format(";".join(map(str, type_ids))) if not merge_ids: # cis, counter, total, self.page, numfound, facet_ @@ -105,35 +163,65 @@ class Search(object): def statistics(self, type_ids): self.level = int(self.level) - _tmp = [] + ids = [self.root_id] if not isinstance(self.root_id, list) else self.root_id - for lv in range(0, self.level): - if not lv: - if type_ids and lv == self.level - 1: + _tmp = [] + level2ids = {} + for lv in range(1, self.level + 1): + level2ids[lv] = [] + + if lv == 1: + if not self.has_m2m: + key, prefix = ids, REDIS_PREFIX_CI_RELATION + else: + if not self.ancestor_ids: + key, prefix = ids, REDIS_PREFIX_CI_RELATION + else: + key = ["{},{}".format(self.ancestor_ids, _id) for _id in ids] + prefix = REDIS_PREFIX_CI_RELATION2 + + level2ids[lv] = [[i] for i in key] + + if not key: + _tmp = [] + continue + + if type_ids and lv == self.level: _tmp = list(map(lambda x: [i for i in x if i[1] in type_ids], (map(lambda x: list(json.loads(x).items()), - [i or '{}' for i in rd.get(ids, REDIS_PREFIX_CI_RELATION) or []])))) + [i or '{}' for i in rd.get(key, prefix) or []])))) else: _tmp = list(map(lambda x: list(json.loads(x).items()), - [i or '{}' for i in rd.get(ids, REDIS_PREFIX_CI_RELATION) or []])) + [i or '{}' for i in rd.get(key, prefix) or []])) + else: for idx, item in enumerate(_tmp): if item: - if type_ids and lv == self.level - 1: - __tmp = list( - map(lambda x: [(_id, type_id) for _id, type_id in json.loads(x).items() - if type_id in type_ids], - filter(lambda x: x is not None, - rd.get([i[0] for i in item], REDIS_PREFIX_CI_RELATION) or []))) + if not self.has_m2m: + key, prefix = [i[0] for i in item], REDIS_PREFIX_CI_RELATION else: + key = list(set(['{},{}'.format(j, i[0]) for i in item for j in level2ids[lv - 1][idx]])) + prefix = REDIS_PREFIX_CI_RELATION2 - __tmp = list(map(lambda x: list(json.loads(x).items()), - filter(lambda x: x is not None, - rd.get([i[0] for i in item], REDIS_PREFIX_CI_RELATION) or []))) + level2ids[lv].append(key) + + if key: + if type_ids and lv == self.level: + __tmp = map(lambda x: [(_id, type_id) for _id, type_id in json.loads(x).items() + if type_id in type_ids], + filter(lambda x: x is not None, + rd.get(key, prefix) or [])) + else: + __tmp = map(lambda x: list(json.loads(x).items()), + filter(lambda x: x is not None, + rd.get(key, prefix) or [])) + else: + __tmp = [] _tmp[idx] = [j for i in __tmp for j in i] else: _tmp[idx] = [] + level2ids[lv].append([]) result = {str(_id): len(_tmp[idx]) for idx, _id in enumerate(ids)} diff --git a/cmdb-api/api/models/cmdb.py b/cmdb-api/api/models/cmdb.py index a17a1d3..f91f585 100644 --- a/cmdb-api/api/models/cmdb.py +++ b/cmdb-api/api/models/cmdb.py @@ -218,6 +218,8 @@ class CIRelation(Model): relation_type_id = db.Column(db.Integer, db.ForeignKey("c_relation_types.id"), nullable=False) more = db.Column(db.Integer, db.ForeignKey("c_cis.id")) + ancestor_ids = db.Column(db.String(128), index=True) + first_ci = db.relationship("CI", primaryjoin="CI.id==CIRelation.first_ci_id") second_ci = db.relationship("CI", primaryjoin="CI.id==CIRelation.second_ci_id") relation_type = db.relationship("RelationType", backref="c_ci_relations.relation_type_id") diff --git a/cmdb-api/api/tasks/cmdb.py b/cmdb-api/api/tasks/cmdb.py index c41f556..bf24585 100644 --- a/cmdb-api/api/tasks/cmdb.py +++ b/cmdb-api/api/tasks/cmdb.py @@ -16,6 +16,7 @@ from api.lib.cmdb.cache import CITypeAttributesCache from api.lib.cmdb.const import CMDB_QUEUE from api.lib.cmdb.const import REDIS_PREFIX_CI from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION +from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION2 from api.lib.decorator import flush_db from api.lib.decorator import reconnect_db from api.lib.perm.acl.cache import UserCache @@ -97,16 +98,30 @@ def ci_delete_trigger(trigger, operate_type, ci_dict): @celery.task(name="cmdb.ci_relation_cache", queue=CMDB_QUEUE) @flush_db @reconnect_db -def ci_relation_cache(parent_id, child_id): +def ci_relation_cache(parent_id, child_id, ancestor_ids): with Lock("CIRelation_{}".format(parent_id)): - children = rd.get([parent_id], REDIS_PREFIX_CI_RELATION)[0] - children = json.loads(children) if children is not None else {} + if ancestor_ids is None: + children = rd.get([parent_id], REDIS_PREFIX_CI_RELATION)[0] + children = json.loads(children) if children is not None else {} - cr = CIRelation.get_by(first_ci_id=parent_id, second_ci_id=child_id, first=True, to_dict=False) - if str(child_id) not in children: - children[str(child_id)] = cr.second_ci.type_id + cr = CIRelation.get_by(first_ci_id=parent_id, second_ci_id=child_id, ancestor_ids=ancestor_ids, + first=True, to_dict=False) + if str(child_id) not in children: + children[str(child_id)] = cr.second_ci.type_id - rd.create_or_update({parent_id: json.dumps(children)}, REDIS_PREFIX_CI_RELATION) + rd.create_or_update({parent_id: json.dumps(children)}, REDIS_PREFIX_CI_RELATION) + + else: + key = "{},{}".format(ancestor_ids, parent_id) + grandson = rd.get([key], REDIS_PREFIX_CI_RELATION2)[0] + grandson = json.loads(grandson) if grandson is not None else {} + + cr = CIRelation.get_by(first_ci_id=parent_id, second_ci_id=child_id, ancestor_ids=ancestor_ids, + first=True, to_dict=False) + if cr and str(cr.second_ci_id) not in grandson: + grandson[str(cr.second_ci_id)] = cr.second_ci.type_id + + rd.create_or_update({key: json.dumps(grandson)}, REDIS_PREFIX_CI_RELATION2) current_app.logger.info("ADD ci relation cache: {0} -> {1}".format(parent_id, child_id)) @@ -156,20 +171,31 @@ def ci_relation_add(parent_dict, child_id, uid): try: db.session.commit() except: - pass + db.session.rollback() @celery.task(name="cmdb.ci_relation_delete", queue=CMDB_QUEUE) @reconnect_db -def ci_relation_delete(parent_id, child_id): +def ci_relation_delete(parent_id, child_id, ancestor_ids): with Lock("CIRelation_{}".format(parent_id)): - children = rd.get([parent_id], REDIS_PREFIX_CI_RELATION)[0] - children = json.loads(children) if children is not None else {} + if ancestor_ids is None: + children = rd.get([parent_id], REDIS_PREFIX_CI_RELATION)[0] + children = json.loads(children) if children is not None else {} - if str(child_id) in children: - children.pop(str(child_id)) + if str(child_id) in children: + children.pop(str(child_id)) - rd.create_or_update({parent_id: json.dumps(children)}, REDIS_PREFIX_CI_RELATION) + rd.create_or_update({parent_id: json.dumps(children)}, REDIS_PREFIX_CI_RELATION) + + else: + key = "{},{}".format(ancestor_ids, parent_id) + grandson = rd.get([key], REDIS_PREFIX_CI_RELATION2)[0] + grandson = json.loads(grandson) if grandson is not None else {} + + if str(child_id) in grandson: + grandson.pop(str(child_id)) + + rd.create_or_update({key: json.dumps(grandson)}, REDIS_PREFIX_CI_RELATION2) current_app.logger.info("DELETE ci relation cache: {0} -> {1}".format(parent_id, child_id)) diff --git a/cmdb-api/api/views/cmdb/ci_relation.py b/cmdb-api/api/views/cmdb/ci_relation.py index f469ecc..bfa56ec 100644 --- a/cmdb-api/api/views/cmdb/ci_relation.py +++ b/cmdb-api/api/views/cmdb/ci_relation.py @@ -35,6 +35,7 @@ class CIRelationSearchView(APIView): count = get_page_size(request.values.get("count") or request.values.get("page_size")) root_id = request.values.get('root_id') + ancestor_ids = request.values.get('ancestor_ids') or None # only for many to many level = list(map(int, handle_arg_list(request.values.get('level', '1')))) query = request.values.get('q', "") @@ -44,7 +45,7 @@ class CIRelationSearchView(APIView): reverse = request.values.get("reverse") in current_app.config.get('BOOL_TRUE') start = time.time() - s = Search(root_id, level, query, fl, facet, page, count, sort, reverse) + s = Search(root_id, level, query, fl, facet, page, count, sort, reverse, ancestor_ids=ancestor_ids) try: response, counter, total, page, numfound, facet = s.search() except SearchError as e: @@ -67,9 +68,10 @@ class CIRelationStatisticsView(APIView): root_ids = list(map(int, handle_arg_list(request.values.get('root_ids')))) level = request.values.get('level', 1) type_ids = set(map(int, handle_arg_list(request.values.get('type_ids', [])))) + ancestor_ids = request.values.get('ancestor_ids') or None # only for many to many start = time.time() - s = Search(root_ids, level) + s = Search(root_ids, level, ancestor_ids=ancestor_ids) try: result = s.statistics(type_ids) except SearchError as e: @@ -121,14 +123,18 @@ class CIRelationView(APIView): url_prefix = "/ci_relations//" def post(self, first_ci_id, second_ci_id): + ancestor_ids = request.values.get('ancestor_ids') or None + manager = CIRelationManager() - res = manager.add(first_ci_id, second_ci_id) + res = manager.add(first_ci_id, second_ci_id, ancestor_ids=ancestor_ids) return self.jsonify(cr_id=res) def delete(self, first_ci_id, second_ci_id): + ancestor_ids = request.values.get('ancestor_ids') or None + manager = CIRelationManager() - manager.delete_2(first_ci_id, second_ci_id) + manager.delete_2(first_ci_id, second_ci_id, ancestor_ids=ancestor_ids) return self.jsonify(message="CIType Relation is deleted") @@ -151,8 +157,9 @@ class BatchCreateOrUpdateCIRelationView(APIView): ci_ids = list(map(int, request.values.get('ci_ids'))) parents = list(map(int, request.values.get('parents', []))) children = list(map(int, request.values.get('children', []))) + ancestor_ids = request.values.get('ancestor_ids') or None - CIRelationManager.batch_update(ci_ids, parents, children) + CIRelationManager.batch_update(ci_ids, parents, children, ancestor_ids=ancestor_ids) return self.jsonify(code=200) @@ -166,7 +173,8 @@ class BatchCreateOrUpdateCIRelationView(APIView): def delete(self): ci_ids = list(map(int, request.values.get('ci_ids'))) parents = list(map(int, request.values.get('parents', []))) + ancestor_ids = request.values.get('ancestor_ids') or None - CIRelationManager.batch_delete(ci_ids, parents) + CIRelationManager.batch_delete(ci_ids, parents, ancestor_ids=ancestor_ids) return self.jsonify(code=200) diff --git a/cmdb-api/api/views/cmdb/ci_type_relation.py b/cmdb-api/api/views/cmdb/ci_type_relation.py index 20ce323..3e1dc87 100644 --- a/cmdb-api/api/views/cmdb/ci_type_relation.py +++ b/cmdb-api/api/views/cmdb/ci_type_relation.py @@ -9,6 +9,7 @@ from api.lib.cmdb.ci_type import CITypeRelationManager from api.lib.cmdb.const import PermEnum from api.lib.cmdb.const import ResourceTypeEnum from api.lib.cmdb.const import RoleEnum +from api.lib.cmdb.preference import PreferenceManager from api.lib.cmdb.resp_format import ErrFormat from api.lib.decorator import args_required from api.lib.perm.acl.acl import ACLManager @@ -109,3 +110,10 @@ class CITypeRelationRevokeView(APIView): acl.revoke_resource_from_role_by_rid(resource_name, rid, ResourceTypeEnum.CI_TYPE_RELATION, perms) return self.jsonify(code=200) + + +class CITypeRelationCanEditView(APIView): + url_prefix = "/ci_type_relations///can_edit" + + def get(self, parent_id, child_id): + return self.jsonify(result=PreferenceManager.can_edit_relation(parent_id, child_id))