From 31d24b19a86538439859a27d76751e33a771db09 Mon Sep 17 00:00:00 2001 From: pycook Date: Mon, 9 Oct 2023 15:33:18 +0800 Subject: [PATCH] feat: The definition of attribute choice values supports webhook and other model attribute values. --- cmdb-api/api/lib/cmdb/attribute.py | 87 ++++++++++++++++++++-------- cmdb-api/api/lib/cmdb/ci_type.py | 31 ++++++---- cmdb-api/api/lib/cmdb/preference.py | 2 +- cmdb-api/api/lib/cmdb/resp_format.py | 1 + cmdb-api/api/lib/cmdb/value.py | 2 +- cmdb-api/api/lib/notify.py | 11 +++- cmdb-api/api/models/cmdb.py | 1 + cmdb-api/api/tasks/cmdb.py | 12 ++-- cmdb-api/api/views/cmdb/ci_type.py | 1 - 9 files changed, 107 insertions(+), 41 deletions(-) diff --git a/cmdb-api/api/lib/cmdb/attribute.py b/cmdb-api/api/lib/cmdb/attribute.py index f80927a..2384d5c 100644 --- a/cmdb-api/api/lib/cmdb/attribute.py +++ b/cmdb-api/api/lib/cmdb/attribute.py @@ -1,6 +1,5 @@ -# -*- coding:utf-8 -*- +# -*- coding:utf-8 -*- -import requests from flask import abort from flask import current_app from flask import session @@ -23,6 +22,7 @@ from api.lib.cmdb.utils import ValueTypeMap from api.lib.decorator import kwargs_required from api.lib.perm.acl.acl import is_app_admin from api.lib.perm.acl.acl import validate_permission +from api.lib.webhook import webhook_request from api.models.cmdb import Attribute from api.models.cmdb import CIType from api.models.cmdb import CITypeAttribute @@ -40,15 +40,11 @@ class AttributeManager(object): pass @staticmethod - def _get_choice_values_from_web_hook(choice_web_hook): - url = choice_web_hook.get('url') - ret_key = choice_web_hook.get('ret_key') - headers = choice_web_hook.get('headers') or {} - payload = choice_web_hook.get('payload') or {} - method = (choice_web_hook.get('method') or 'GET').lower() + def _get_choice_values_from_webhook(choice_webhook, payload=None): + ret_key = choice_webhook.get('ret_key') try: - res = getattr(requests, method)(url, headers=headers, data=payload).json() + res = webhook_request(choice_webhook, payload or {}).json() if ret_key: ret_key_list = ret_key.strip().split("##") for key in ret_key_list[:-1]: @@ -63,16 +59,41 @@ class AttributeManager(object): current_app.logger.error("get choice values failed: {}".format(e)) return [] + @staticmethod + def _get_choice_values_from_other_ci(choice_other): + from api.lib.cmdb.search import SearchError + from api.lib.cmdb.search.ci import search + + type_ids = choice_other.get('type_ids') + attr_id = choice_other.get('attr_id') + other_filter = choice_other.get('filter') or '' + + query = "_type:({}),{}".format(";".join(map(str, type_ids)), other_filter) + s = search(query, fl=[str(attr_id)], facet=[str(attr_id)], count=1) + try: + _, _, _, _, _, facet = s.search() + return [[i[0], {}] for i in (list(facet.values()) or [[]])[0]] + except SearchError as e: + current_app.logger.error("get choice values from other ci failed: {}".format(e)) + return [] + @classmethod - def get_choice_values(cls, attr_id, value_type, choice_web_hook, choice_web_hook_parse=True): + def get_choice_values(cls, attr_id, value_type, choice_web_hook, choice_other, + choice_web_hook_parse=True, choice_other_parse=True): if choice_web_hook: - if choice_web_hook_parse: - if isinstance(choice_web_hook, dict): - return cls._get_choice_values_from_web_hook(choice_web_hook) + if choice_web_hook_parse and isinstance(choice_web_hook, dict): + return cls._get_choice_values_from_webhook(choice_web_hook) + else: + return [] + elif choice_other: + if choice_other_parse and isinstance(choice_other, dict): + return cls._get_choice_values_from_other_ci(choice_other) else: return [] choice_table = ValueTypeMap.choice.get(value_type) + if not choice_table: + return [] choice_values = choice_table.get_by(fl=["value", "option"], attr_id=attr_id) return [[choice_value['value'], choice_value['option']] for choice_value in choice_values] @@ -122,7 +143,8 @@ class AttributeManager(object): res = list() for attr in attrs: attr["is_choice"] and attr.update( - dict(choice_value=cls.get_choice_values(attr["id"], attr["value_type"], attr["choice_web_hook"]))) + dict(choice_value=cls.get_choice_values(attr["id"], attr["value_type"], + attr["choice_web_hook"], attr.get("choice_other")))) attr['is_choice'] and attr.pop('choice_web_hook', None) res.append(attr) @@ -132,29 +154,38 @@ class AttributeManager(object): def get_attribute_by_name(self, name): attr = Attribute.get_by(name=name, first=True) if attr.get("is_choice"): - attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"], attr["choice_web_hook"]) + attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"], + attr["choice_web_hook"], attr.get("choice_other")) return attr def get_attribute_by_alias(self, alias): attr = Attribute.get_by(alias=alias, first=True) if attr.get("is_choice"): - attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"], attr["choice_web_hook"]) + attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"], + attr["choice_web_hook"], attr.get("choice_other")) return attr def get_attribute_by_id(self, _id): attr = Attribute.get_by_id(_id).to_dict() if attr.get("is_choice"): - attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"], attr["choice_web_hook"]) + attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"], + attr["choice_web_hook"], attr.get("choice_other")) return attr - def get_attribute(self, key, choice_web_hook_parse=True): + def get_attribute(self, key, choice_web_hook_parse=True, choice_other_parse=True): attr = AttributeCache.get(key).to_dict() if attr.get("is_choice"): attr["choice_value"] = self.get_choice_values( - attr["id"], attr["value_type"], attr["choice_web_hook"], choice_web_hook_parse=choice_web_hook_parse) + attr["id"], + attr["value_type"], + attr["choice_web_hook"], + attr.get("choice_other"), + choice_web_hook_parse=choice_web_hook_parse, + choice_other_parse=choice_other_parse, + ) return attr @@ -181,12 +212,17 @@ class AttributeManager(object): def add(cls, **kwargs): choice_value = kwargs.pop("choice_value", []) kwargs.pop("is_choice", None) - is_choice = True if choice_value or kwargs.get('choice_web_hook') else False + is_choice = True if choice_value or kwargs.get('choice_web_hook') or kwargs.get('choice_other') else False name = kwargs.pop("name") if name in BUILTIN_KEYWORDS: return abort(400, ErrFormat.attribute_name_cannot_be_builtin) + if kwargs.get('choice_other'): + if (not isinstance(kwargs['choice_other'], dict) or not kwargs['choice_other'].get('type_ids') or + not kwargs['choice_other'].get('attr_id')): + return abort(400, ErrFormat.attribute_choice_other_invalid) + alias = kwargs.pop("alias", "") alias = name if not alias else alias Attribute.get_by(name=name, first=True) and abort(400, ErrFormat.attribute_name_duplicate.format(name)) @@ -301,12 +337,17 @@ class AttributeManager(object): self._change_index(attr, attr.is_index, kwargs['is_index']) + if kwargs.get('choice_other'): + if (not isinstance(kwargs['choice_other'], dict) or not kwargs['choice_other'].get('type_ids') or + not kwargs['choice_other'].get('attr_id')): + return abort(400, ErrFormat.attribute_choice_other_invalid) + existed2 = attr.to_dict() - if not existed2['choice_web_hook'] and existed2['is_choice']: - existed2['choice_value'] = self.get_choice_values(attr.id, attr.value_type, attr.choice_web_hook) + if not existed2['choice_web_hook'] and not existed2.get('choice_other') and existed2['is_choice']: + existed2['choice_value'] = self.get_choice_values(attr.id, attr.value_type, None, None) choice_value = kwargs.pop("choice_value", False) - is_choice = True if choice_value or kwargs.get('choice_web_hook') else False + is_choice = True if choice_value or kwargs.get('choice_web_hook') or kwargs.get('choice_other') else False kwargs['is_choice'] = is_choice if kwargs.get('default') and not (isinstance(kwargs['default'], dict) and 'default' in kwargs['default']): diff --git a/cmdb-api/api/lib/cmdb/ci_type.py b/cmdb-api/api/lib/cmdb/ci_type.py index d787c12..4adefad 100644 --- a/cmdb-api/api/lib/cmdb/ci_type.py +++ b/cmdb-api/api/lib/cmdb/ci_type.py @@ -1,7 +1,6 @@ # -*- coding:utf-8 -*- import copy -import datetime import toposort from flask import abort @@ -25,7 +24,6 @@ from api.lib.cmdb.const import ValueTypeEnum from api.lib.cmdb.history import CITypeHistoryManager from api.lib.cmdb.relation_type import RelationTypeManager from api.lib.cmdb.resp_format import ErrFormat -from api.lib.cmdb.utils import TableMap from api.lib.cmdb.value import AttributeValueManager from api.lib.decorator import kwargs_required from api.lib.perm.acl.acl import ACLManager @@ -354,19 +352,20 @@ class CITypeAttributeManager(object): return [AttributeCache.get(attr.attr_id).name for attr in CITypeAttributesCache.get(type_id)] @staticmethod - def get_attributes_by_type_id(type_id, choice_web_hook_parse=True): + def get_attributes_by_type_id(type_id, choice_web_hook_parse=True, choice_other_parse=True): has_config_perm = ACLManager('cmdb').has_permission( CITypeManager.get_name_by_id(type_id), ResourceTypeEnum.CI, PermEnum.CONFIG) attrs = CITypeAttributesCache.get(type_id) result = list() for attr in sorted(attrs, key=lambda x: (x.order, x.id)): - attr_dict = AttributeManager().get_attribute(attr.attr_id, choice_web_hook_parse) + attr_dict = AttributeManager().get_attribute(attr.attr_id, choice_web_hook_parse, choice_other_parse) attr_dict["is_required"] = attr.is_required attr_dict["order"] = attr.order attr_dict["default_show"] = attr.default_show if not has_config_perm: attr_dict.pop('choice_web_hook', None) + attr_dict.pop('choice_other', None) result.append(attr_dict) @@ -374,13 +373,25 @@ class CITypeAttributeManager(object): @staticmethod def get_common_attributes(type_ids): + has_config_perm = False + for type_id in type_ids: + has_config_perm |= ACLManager('cmdb').has_permission( + CITypeManager.get_name_by_id(type_id), ResourceTypeEnum.CI, PermEnum.CONFIG) + result = CITypeAttribute.get_by(__func_in___key_type_id=list(map(int, type_ids)), to_dict=False) attr2types = {} for i in result: attr2types.setdefault(i.attr_id, []).append(i.type_id) - return [AttributeCache.get(attr_id).to_dict() for attr_id in attr2types - if len(attr2types[attr_id]) == len(type_ids)] + attrs = [] + for attr_id in attr2types: + if len(attr2types[attr_id]) == len(type_ids): + attr = AttributeManager().get_attribute_by_id(attr_id) + if not has_config_perm: + attr.pop('choice_web_hook', None) + attrs.append(attr) + + return attrs @staticmethod def _check(type_id, attr_ids): @@ -489,7 +500,7 @@ class CITypeAttributeManager(object): for ci in CI.get_by(type_id=type_id, to_dict=False): AttributeValueManager.delete_attr_value(attr_id, ci.id) - ci_cache.apply_async([ci.id], queue=CMDB_QUEUE) + ci_cache.apply_async(args=(ci.id, None, None), queue=CMDB_QUEUE) CITypeAttributeCache.clean(type_id, attr_id) @@ -522,7 +533,7 @@ class CITypeAttributeManager(object): CITypeAttributesCache.clean(type_id) from api.tasks.cmdb import ci_type_attribute_order_rebuild - ci_type_attribute_order_rebuild.apply_async(args=(type_id,), queue=CMDB_QUEUE) + ci_type_attribute_order_rebuild.apply_async(args=(type_id, current_user.uid), queue=CMDB_QUEUE) class CITypeRelationManager(object): @@ -847,7 +858,7 @@ class CITypeAttributeGroupManager(object): CITypeAttributesCache.clean(type_id) from api.tasks.cmdb import ci_type_attribute_order_rebuild - ci_type_attribute_order_rebuild.apply_async(args=(type_id,), queue=CMDB_QUEUE) + ci_type_attribute_order_rebuild.apply_async(args=(type_id, current_user.uid), queue=CMDB_QUEUE) class CITypeTemplateManager(object): @@ -1092,7 +1103,7 @@ class CITypeTemplateManager(object): for ci_type in tpt['ci_types']: tpt['type2attributes'][ci_type['id']] = CITypeAttributeManager.get_attributes_by_type_id( - ci_type['id'], choice_web_hook_parse=False) + ci_type['id'], choice_web_hook_parse=False, choice_other_parse=False) tpt['type2attribute_group'][ci_type['id']] = CITypeAttributeGroupManager.get_by_type_id(ci_type['id']) diff --git a/cmdb-api/api/lib/cmdb/preference.py b/cmdb-api/api/lib/cmdb/preference.py index b94557c..eec09ad 100644 --- a/cmdb-api/api/lib/cmdb/preference.py +++ b/cmdb-api/api/lib/cmdb/preference.py @@ -116,7 +116,7 @@ class PreferenceManager(object): for i in result: if i["is_choice"]: i.update(dict(choice_value=AttributeManager.get_choice_values( - i["id"], i["value_type"], i["choice_web_hook"]))) + i["id"], i["value_type"], i["choice_web_hook"], i.get("choice_other")))) return is_subscribed, result diff --git a/cmdb-api/api/lib/cmdb/resp_format.py b/cmdb-api/api/lib/cmdb/resp_format.py index 27208ad..5c64abb 100644 --- a/cmdb-api/api/lib/cmdb/resp_format.py +++ b/cmdb-api/api/lib/cmdb/resp_format.py @@ -23,6 +23,7 @@ class ErrFormat(CommonErrFormat): cannot_edit_attribute = "您没有权限修改该属性!" cannot_delete_attribute = "目前只允许 属性创建人、管理员 删除属性!" attribute_name_cannot_be_builtin = "属性字段名不能是内置字段: id, _id, ci_id, type, _type, ci_type" + attribute_choice_other_invalid = "预定义值: 其他模型请求参数不合法!" ci_not_found = "CI {} 不存在" unique_constraint = "多属性联合唯一校验不通过: {}" diff --git a/cmdb-api/api/lib/cmdb/value.py b/cmdb-api/api/lib/cmdb/value.py index 3e1cb86..4354106 100644 --- a/cmdb-api/api/lib/cmdb/value.py +++ b/cmdb-api/api/lib/cmdb/value.py @@ -92,7 +92,7 @@ class AttributeValueManager(object): @staticmethod def _check_is_choice(attr, value_type, value): - choice_values = AttributeManager.get_choice_values(attr.id, value_type, attr.choice_web_hook) + choice_values = AttributeManager.get_choice_values(attr.id, value_type, attr.choice_web_hook, attr.choice_other) if str(value) not in list(map(str, [i[0] for i in choice_values])): return abort(400, ErrFormat.not_in_choice_values.format(value)) diff --git a/cmdb-api/api/lib/notify.py b/cmdb-api/api/lib/notify.py index 93c8e1f..5ee8894 100644 --- a/cmdb-api/api/lib/notify.py +++ b/cmdb-api/api/lib/notify.py @@ -3,6 +3,7 @@ import json import requests +import six from flask import current_app from jinja2 import Template from markdownify import markdownify as md @@ -17,7 +18,15 @@ def _request_messenger(subject, body, tos, sender, payload): if not params['tos']: raise Exception("no receivers") - params['tos'] = [Template(i).render(payload) for i in params['tos'] if i.strip()] + flat_tos = [] + for i in params['tos']: + if i.strip(): + to = Template(i).render(payload) + if isinstance(to, list): + flat_tos.extend(to) + elif isinstance(to, six.string_types): + flat_tos.append(to) + params['tos'] = flat_tos if sender == "email": params['msgtype'] = 'text/html' diff --git a/cmdb-api/api/models/cmdb.py b/cmdb-api/api/models/cmdb.py index ccc4084..ae15db0 100644 --- a/cmdb-api/api/models/cmdb.py +++ b/cmdb-api/api/models/cmdb.py @@ -90,6 +90,7 @@ class Attribute(Model): compute_script = db.Column(db.Text) choice_web_hook = db.Column(db.JSON) + choice_other = db.Column(db.JSON) uid = db.Column(db.Integer, index=True) diff --git a/cmdb-api/api/tasks/cmdb.py b/cmdb-api/api/tasks/cmdb.py index 03e4fd5..83a767a 100644 --- a/cmdb-api/api/tasks/cmdb.py +++ b/cmdb-api/api/tasks/cmdb.py @@ -41,10 +41,11 @@ def ci_cache(ci_id, operate_type, record_id): current_app.logger.info("{0} flush..........".format(ci_id)) - current_app.test_request_context().push() - login_user(UserCache.get('worker')) + if operate_type: + current_app.test_request_context().push() + login_user(UserCache.get('worker')) - CITriggerManager.fire(operate_type, ci_dict, record_id) + CITriggerManager.fire(operate_type, ci_dict, record_id) @celery.task(name="cmdb.batch_ci_cache", queue=CMDB_QUEUE) @@ -164,7 +165,7 @@ def ci_relation_delete(parent_id, child_id): @celery.task(name="cmdb.ci_type_attribute_order_rebuild", queue=CMDB_QUEUE) -def ci_type_attribute_order_rebuild(type_id): +def ci_type_attribute_order_rebuild(type_id, uid): current_app.logger.info('rebuild attribute order') db.session.remove() @@ -173,6 +174,9 @@ def ci_type_attribute_order_rebuild(type_id): attrs = CITypeAttributesCache.get(type_id) id2attr = {attr.attr_id: attr for attr in attrs} + current_app.test_request_context().push() + login_user(UserCache.get(uid)) + res = CITypeAttributeGroupManager.get_by_type_id(type_id, True) order = 0 for group in res: diff --git a/cmdb-api/api/views/cmdb/ci_type.py b/cmdb-api/api/views/cmdb/ci_type.py index d036405..4e02d40 100644 --- a/cmdb-api/api/views/cmdb/ci_type.py +++ b/cmdb-api/api/views/cmdb/ci_type.py @@ -506,4 +506,3 @@ class CITypeFilterPermissionView(APIView): @auth_with_app_token def get(self, type_id): return self.jsonify(CIFilterPermsCRUD().get(type_id)) -