diff --git a/cmdb-api/Pipfile b/cmdb-api/Pipfile index 901072e..1639b51 100644 --- a/cmdb-api/Pipfile +++ b/cmdb-api/Pipfile @@ -48,6 +48,7 @@ six = "==1.12.0" bs4 = ">=0.0.1" toposort = ">=1.5" requests = ">=2.22.0" +requests_oauthlib = "==1.3.1" PyJWT = "==2.4.0" elasticsearch = "==7.17.9" future = "==0.18.3" diff --git a/cmdb-api/api/commands/click_cmdb.py b/cmdb-api/api/commands/click_cmdb.py index 1362806..6d41d98 100644 --- a/cmdb-api/api/commands/click_cmdb.py +++ b/cmdb-api/api/commands/click_cmdb.py @@ -9,6 +9,7 @@ import time import click from flask import current_app from flask.cli import with_appcontext +from flask_login import login_user import api.lib.cmdb.ci from api.extensions import db @@ -24,6 +25,7 @@ from api.lib.cmdb.const import ValueTypeEnum from api.lib.exception import AbortException from api.lib.perm.acl.acl import ACLManager from api.lib.perm.acl.cache import AppCache +from api.lib.perm.acl.cache import UserCache from api.lib.perm.acl.resource import ResourceCRUD from api.lib.perm.acl.resource import ResourceTypeCRUD from api.lib.perm.acl.role import RoleCRUD @@ -207,6 +209,8 @@ def cmdb_counter(): """ from api.lib.cmdb.cache import CMDBCounterCache + current_app.test_request_context().push() + login_user(UserCache.get('worker')) while True: try: db.session.remove() diff --git a/cmdb-api/api/commands/init_common_setting.py b/cmdb-api/api/commands/click_common_setting.py similarity index 77% rename from cmdb-api/api/commands/init_common_setting.py rename to cmdb-api/api/commands/click_common_setting.py index ea5afc9..e0fea63 100644 --- a/cmdb-api/api/commands/init_common_setting.py +++ b/cmdb-api/api/commands/click_common_setting.py @@ -161,6 +161,55 @@ class InitDepartment(object): info = f"update department acl_rid: {acl_rid}" current_app.logger.info(info) + def init_backend_resource(self): + acl = self.check_app('backend') + resources_types = acl.get_all_resources_types() + + results = list(filter(lambda t: t['name'] == '操作权限', resources_types['groups'])) + if len(results) == 0: + payload = dict( + app_id=acl.app_name, + name='操作权限', + description='', + perms=['read', 'grant', 'delete', 'update'] + ) + resource_type = acl.create_resources_type(payload) + else: + resource_type = results[0] + + for name in ['公司信息']: + payload = dict( + type_id=resource_type['id'], + app_id=acl.app_name, + name=name, + ) + try: + acl.create_resource(payload) + except Exception as e: + if '已经存在' in str(e): + pass + else: + raise Exception(e) + + def check_app(self, app_name): + acl = ACLManager(app_name) + payload = dict( + name=app_name, + description=app_name + ) + try: + app = acl.validate_app() + if app: + return acl + + acl.create_app(payload) + except Exception as e: + current_app.logger.error(e) + if '不存在' in str(e): + acl.create_app(payload) + return acl + raise Exception(e) + @click.command() @with_appcontext @@ -177,5 +226,7 @@ def init_department(): """ Department initialization """ - InitDepartment().init() - InitDepartment().create_acl_role_with_department() + cli = InitDepartment() + cli.init_wide_company() + cli.create_acl_role_with_department() + cli.init_backend_resource() diff --git a/cmdb-api/api/lib/cmdb/attribute.py b/cmdb-api/api/lib/cmdb/attribute.py index be0654c..f80927a 100644 --- a/cmdb-api/api/lib/cmdb/attribute.py +++ b/cmdb-api/api/lib/cmdb/attribute.py @@ -163,13 +163,15 @@ class AttributeManager(object): if RoleEnum.CONFIG not in session.get("acl", {}).get("parentRoles", []) and not is_app_admin('cmdb'): return abort(403, ErrFormat.role_required.format(RoleEnum.CONFIG)) - @staticmethod - def calc_computed_attribute(attr_id): + @classmethod + def calc_computed_attribute(cls, attr_id): """ calculate computed attribute for all ci :param attr_id: :return: """ + cls.can_create_computed_attribute() + from api.tasks.cmdb import calc_computed_attribute calc_computed_attribute.apply_async(args=(attr_id, current_user.uid), queue=CMDB_QUEUE) diff --git a/cmdb-api/api/lib/cmdb/cache.py b/cmdb-api/api/lib/cmdb/cache.py index d166d42..c19812f 100644 --- a/cmdb-api/api/lib/cmdb/cache.py +++ b/cmdb-api/api/lib/cmdb/cache.py @@ -2,14 +2,11 @@ from __future__ import unicode_literals -import requests from flask import current_app from api.extensions import cache -from api.extensions import db from api.lib.cmdb.custom_dashboard import CustomDashboardManager from api.models.cmdb import Attribute -from api.models.cmdb import CI from api.models.cmdb import CIType from api.models.cmdb import CITypeAttribute from api.models.cmdb import RelationType @@ -210,7 +207,6 @@ class CITypeAttributeCache(object): @classmethod def get(cls, type_id, attr_id): - attr = cache.get(cls.PREFIX_ID.format(type_id, attr_id)) attr = attr or cache.get(cls.PREFIX_ID.format(type_id, attr_id)) attr = attr or CITypeAttribute.get_by(type_id=type_id, attr_id=attr_id, first=True, to_dict=False) @@ -251,53 +247,72 @@ class CMDBCounterCache(object): result = {} for custom in customs: if custom['category'] == 0: - result[custom['id']] = cls.summary_counter(custom['type_id']) + res = cls.sum_counter(custom) elif custom['category'] == 1: - result[custom['id']] = cls.attribute_counter(custom['type_id'], custom['attr_id']) - elif custom['category'] == 2: - result[custom['id']] = cls.relation_counter(custom['type_id'], custom['level']) + res = cls.attribute_counter(custom) + else: + res = cls.relation_counter(custom.get('type_id'), + custom.get('level'), + custom.get('options', {}).get('filter', ''), + custom.get('options', {}).get('type_ids', '')) + + if res: + result[custom['id']] = res cls.set(result) return result @classmethod - def update(cls, custom): + def update(cls, custom, flush=True): result = cache.get(cls.KEY) or {} if not result: result = cls.reset() if custom['category'] == 0: - result[custom['id']] = cls.summary_counter(custom['type_id']) + res = cls.sum_counter(custom) elif custom['category'] == 1: - result[custom['id']] = cls.attribute_counter(custom['type_id'], custom['attr_id']) - elif custom['category'] == 2: - result[custom['id']] = cls.relation_counter(custom['type_id'], custom['level']) + res = cls.attribute_counter(custom) + else: + res = cls.relation_counter(custom.get('type_id'), + custom.get('level'), + custom.get('options', {}).get('filter', ''), + custom.get('options', {}).get('type_ids', '')) - cls.set(result) + if res and flush: + result[custom['id']] = res + cls.set(result) + + return res @staticmethod - def summary_counter(type_id): - return db.session.query(CI.id).filter(CI.deleted.is_(False)).filter(CI.type_id == type_id).count() + def relation_counter(type_id, level, other_filer, type_ids): + from api.lib.cmdb.search.ci_relation.search import Search as RelSearch + from api.lib.cmdb.search import SearchError + from api.lib.cmdb.search.ci import search - @staticmethod - def relation_counter(type_id, level): + query = "_type:{}".format(type_id) + s = search(query, count=1000000) + try: + type_names, _, _, _, _, _ = s.search() + except SearchError as e: + current_app.logger.error(e) + return - uri = current_app.config.get('CMDB_API') - - type_names = requests.get("{}/ci/s?q=_type:{}&count=10000".format(uri, type_id)).json().get('result') type_id_names = [(str(i.get('_id')), i.get(i.get('unique'))) for i in type_names] - url = "{}/ci_relations/statistics?root_ids={}&level={}".format( - uri, ','.join([i[0] for i in type_id_names]), level) - stats = requests.get(url).json() + s = RelSearch([i[0] for i in type_id_names], level, other_filer or '') + try: + stats = s.statistics(type_ids) + except SearchError as e: + current_app.logger.error(e) + return id2name = dict(type_id_names) type_ids = set() for i in (stats.get('detail') or []): for j in stats['detail'][i]: type_ids.add(j) - for type_id in type_ids: _type = CITypeCache.get(type_id) id2name[type_id] = _type and _type.alias @@ -317,9 +332,100 @@ class CMDBCounterCache(object): return result @staticmethod - def attribute_counter(type_id, attr_id): - uri = current_app.config.get('CMDB_API') - url = "{}/ci/s?q=_type:{}&fl={}&facet={}".format(uri, type_id, attr_id, attr_id) - res = requests.get(url).json() - if res.get('facet'): - return dict([i[:2] for i in list(res.get('facet').values())[0]]) + def attribute_counter(custom): + from api.lib.cmdb.search import SearchError + from api.lib.cmdb.search.ci import search + from api.lib.cmdb.utils import ValueTypeMap + + custom.setdefault('options', {}) + type_id = custom.get('type_id') + attr_id = custom.get('attr_id') + type_ids = custom['options'].get('type_ids') or (type_id and [type_id]) + attr_ids = list(map(str, custom['options'].get('attr_ids') or (attr_id and [attr_id]))) + try: + attr2value_type = [AttributeCache.get(i).value_type for i in attr_ids] + except AttributeError: + return + + other_filter = custom['options'].get('filter') + other_filter = "{}".format(other_filter) if other_filter else '' + + if custom['options'].get('ret') == 'cis': + query = "_type:({}),{}".format(";".join(map(str, type_ids)), other_filter) + s = search(query, fl=attr_ids, ret_key='alias', count=100) + try: + cis, _, _, _, _, _ = s.search() + except SearchError as e: + current_app.logger.error(e) + return + + return cis + + result = dict() + # level = 1 + query = "_type:({}),{}".format(";".join(map(str, type_ids)), other_filter) + s = search(query, fl=attr_ids, facet=[attr_ids[0]], count=1) + try: + _, _, _, _, _, facet = s.search() + except SearchError as e: + current_app.logger.error(e) + return + for i in (list(facet.values()) or [[]])[0]: + result[ValueTypeMap.serialize2[attr2value_type[0]](str(i[0]))] = i[1] + if len(attr_ids) == 1: + return result + + # level = 2 + for v in result: + query = "_type:({}),{},{}:{}".format(";".join(map(str, type_ids)), other_filter, attr_ids[0], v) + s = search(query, fl=attr_ids, facet=[attr_ids[1]], count=1) + try: + _, _, _, _, _, facet = s.search() + except SearchError as e: + current_app.logger.error(e) + return + result[v] = dict() + for i in (list(facet.values()) or [[]])[0]: + result[v][ValueTypeMap.serialize2[attr2value_type[1]](str(i[0]))] = i[1] + + if len(attr_ids) == 2: + return result + + # level = 3 + for v1 in result: + if not isinstance(result[v1], dict): + continue + for v2 in result[v1]: + query = "_type:({}),{},{}:{},{}:{}".format(";".join(map(str, type_ids)), other_filter, + attr_ids[0], v1, attr_ids[1], v2) + s = search(query, fl=attr_ids, facet=[attr_ids[2]], count=1) + try: + _, _, _, _, _, facet = s.search() + except SearchError as e: + current_app.logger.error(e) + return + result[v1][v2] = dict() + for i in (list(facet.values()) or [[]])[0]: + result[v1][v2][ValueTypeMap.serialize2[attr2value_type[2]](str(i[0]))] = i[1] + + return result + + @staticmethod + def sum_counter(custom): + from api.lib.cmdb.search import SearchError + from api.lib.cmdb.search.ci import search + + custom.setdefault('options', {}) + type_id = custom.get('type_id') + type_ids = custom['options'].get('type_ids') or (type_id and [type_id]) + other_filter = custom['options'].get('filter') or '' + + query = "_type:({}),{}".format(";".join(map(str, type_ids)), other_filter) + s = search(query, count=1) + try: + _, _, _, _, numfound, _ = s.search() + except SearchError as e: + current_app.logger.error(e) + return + + return numfound diff --git a/cmdb-api/api/lib/cmdb/ci.py b/cmdb-api/api/lib/cmdb/ci.py index 9189cd9..39fee78 100644 --- a/cmdb-api/api/lib/cmdb/ci.py +++ b/cmdb-api/api/lib/cmdb/ci.py @@ -4,6 +4,7 @@ import copy import datetime import json +import threading from flask import abort from flask import current_app @@ -24,27 +25,33 @@ from api.lib.cmdb.const import CMDB_QUEUE from api.lib.cmdb.const import ConstraintEnum from api.lib.cmdb.const import ExistPolicy from api.lib.cmdb.const import OperateType -from api.lib.cmdb.const import PermEnum, ResourceTypeEnum +from api.lib.cmdb.const import PermEnum from api.lib.cmdb.const import REDIS_PREFIX_CI +from api.lib.cmdb.const import ResourceTypeEnum from api.lib.cmdb.const import RetKey from api.lib.cmdb.history import AttributeHistoryManger from api.lib.cmdb.history import CIRelationHistoryManager +from api.lib.cmdb.history import CITriggerHistoryManager from api.lib.cmdb.perms import CIFilterPermsCRUD from api.lib.cmdb.resp_format import ErrFormat from api.lib.cmdb.utils import TableMap from api.lib.cmdb.utils import ValueTypeMap from api.lib.cmdb.value import AttributeValueManager from api.lib.decorator import kwargs_required +from api.lib.notify import notify_send from api.lib.perm.acl.acl import ACLManager from api.lib.perm.acl.acl import is_app_admin from api.lib.perm.acl.acl import validate_permission from api.lib.utils import Lock from api.lib.utils import handle_arg_list +from api.lib.webhook import webhook_request +from api.models.cmdb import AttributeHistory from api.models.cmdb import AutoDiscoveryCI from api.models.cmdb import CI from api.models.cmdb import CIRelation from api.models.cmdb import CITypeAttribute from api.models.cmdb import CITypeRelation +from api.models.cmdb import CITypeTrigger from api.tasks.cmdb import ci_cache from api.tasks.cmdb import ci_delete from api.tasks.cmdb import ci_relation_add @@ -378,16 +385,17 @@ class CIManager(object): key2attr = value_manager.valid_attr_value(ci_dict, ci_type.id, ci and ci.id, ci_type_attrs_name, ci_type_attrs_alias, ci_attr2type_attr) + operate_type = OperateType.UPDATE if ci is not None else OperateType.ADD try: ci = ci or CI.create(type_id=ci_type.id, is_auto_discovery=is_auto_discovery) - record_id = value_manager.create_or_update_attr_value2(ci, ci_dict, key2attr) + record_id = value_manager.create_or_update_attr_value(ci, ci_dict, key2attr) except BadRequest as e: if existed is None: cls.delete(ci.id) raise e if record_id: # has change - ci_cache.apply_async([ci.id], queue=CMDB_QUEUE) + ci_cache.apply_async(args=(ci.id, operate_type, record_id), queue=CMDB_QUEUE) if ref_ci_dict: # add relations ci_relation_add.apply_async(args=(ref_ci_dict, ci.id, current_user.uid), queue=CMDB_QUEUE) @@ -427,12 +435,12 @@ class CIManager(object): return abort(403, ErrFormat.ci_filter_perm_attr_no_permission.format(k)) try: - record_id = value_manager.create_or_update_attr_value2(ci, ci_dict, key2attr) + record_id = value_manager.create_or_update_attr_value(ci, ci_dict, key2attr) except BadRequest as e: raise e if record_id: # has change - ci_cache.apply_async([ci_id], queue=CMDB_QUEUE) + ci_cache.apply_async(args=(ci_id, OperateType.UPDATE, record_id), queue=CMDB_QUEUE) ref_ci_dict = {k: v for k, v in ci_dict.items() if k.startswith("$") and "." in k} if ref_ci_dict: @@ -442,9 +450,10 @@ class CIManager(object): def update_unique_value(ci_id, unique_name, unique_value): ci = CI.get_by_id(ci_id) or abort(404, ErrFormat.ci_not_found.format("id={}".format(ci_id))) - AttributeValueManager().create_or_update_attr_value(unique_name, unique_value, ci) + key2attr = {unique_name: AttributeCache.get(unique_name)} + record_id = AttributeValueManager().create_or_update_attr_value(ci, {unique_name: unique_value}, key2attr) - ci_cache.apply_async([ci_id], queue=CMDB_QUEUE) + ci_cache.apply_async(args=(ci_id, OperateType.UPDATE, record_id), queue=CMDB_QUEUE) @classmethod def delete(cls, ci_id): @@ -477,9 +486,9 @@ class CIManager(object): db.session.commit() - AttributeHistoryManger.add(None, ci_id, [(None, OperateType.DELETE, ci_dict, None)], ci.type_id) + record_id = AttributeHistoryManger.add(None, ci_id, [(None, OperateType.DELETE, ci_dict, None)], ci.type_id) - ci_delete.apply_async([ci.id], queue=CMDB_QUEUE) + ci_delete.apply_async(args=(ci_dict, OperateType.DELETE, record_id), queue=CMDB_QUEUE) return ci_id @@ -896,3 +905,128 @@ class CIRelationManager(object): for parent_id in parents: for ci_id in ci_ids: cls.delete_2(parent_id, ci_id) + + +class CITriggerManager(object): + @staticmethod + def get(type_id): + return CITypeTrigger.get_by(type_id=type_id, to_dict=False) + + @staticmethod + def _exec_webhook(operate_type, webhook, ci_dict, trigger_id, record_id): + try: + response = webhook_request(webhook, ci_dict).text + is_ok = True + except Exception as e: + current_app.logger.warning("exec webhook failed: {}".format(e)) + response = e + is_ok = False + + CITriggerHistoryManager.add(operate_type, + record_id, + ci_dict.get('_id'), + trigger_id, + is_ok=is_ok, + webhook=response) + + return is_ok + + @staticmethod + def _exec_notify(operate_type, notify, ci_dict, trigger_id, record_id, ci_id=None): + + if ci_id is not None: + ci_dict = CIManager().get_ci_by_id_from_db(ci_id, need_children=False, use_master=False) + + try: + response = notify_send(notify.get('subject'), notify.get('body'), notify.get('tos'), ci_dict) + is_ok = True + except Exception as e: + current_app.logger.warning("send notify failed: {}".format(e)) + response = e + is_ok = False + + CITriggerHistoryManager.add(operate_type, + record_id, + ci_dict.get('_id'), + trigger_id, + is_ok=is_ok, + notify=response) + + return is_ok + + @staticmethod + def ci_filter(ci_id, other_filter): + from api.lib.cmdb.search import SearchError + from api.lib.cmdb.search.ci import search + + query = "_id:{},{}".format(ci_id, other_filter) + + try: + _, _, _, _, numfound, _ = search(query).search() + return numfound + except SearchError as e: + current_app.logger.warning("ci search failed: {}".format(e)) + + @classmethod + def fire(cls, operate_type, ci_dict, record_id): + type_id = ci_dict.get('_type') + triggers = cls.get(type_id) or [] + + for trigger in triggers: + if not trigger.option.get('enable'): + continue + + if trigger.option.get('filter') and not cls.ci_filter(ci_dict.get('_id'), trigger.option['filter']): + continue + + if trigger.option.get('attr_ids') and isinstance(trigger.option['attr_ids'], list): + if not (set(trigger.option['attr_ids']) & + set([i.attr_id for i in AttributeHistory.get_by(record_id=record_id, to_dict=False)])): + continue + + if trigger.option.get('action') == operate_type: + if trigger.option.get('webhooks'): + cls._exec_webhook(operate_type, trigger.option['webhooks'], ci_dict, trigger.id, record_id) + elif trigger.option.get('notifies'): + cls._exec_notify(operate_type, trigger.option['notifies'], ci_dict, trigger.id, record_id) + + @classmethod + def waiting_cis(cls, trigger): + now = datetime.datetime.today() + + delta_time = datetime.timedelta(days=(trigger.option.get('before_days', 0) or 0)) + + attr = AttributeCache.get(trigger.attr_id) + + value_table = TableMap(attr=attr).table + + values = value_table.get_by(attr_id=attr.id, to_dict=False) + + result = [] + for v in values: + if (isinstance(v.value, (datetime.date, datetime.datetime)) and + (v.value - delta_time).strftime('%Y%m%d') == now.strftime("%Y%m%d")): + + if trigger.option.get('filter') and not cls.ci_filter(v.ci_id, trigger.option['filter']): + continue + + result.append(v) + + return result + + @classmethod + def trigger_notify(cls, trigger, ci): + """ + only for date attribute + :param trigger: + :param ci: + :return: + """ + if (trigger.notify.get('notify_at') == datetime.datetime.now().strftime("%H:%M") or + not trigger.option.get('notify_at')): + threading.Thread(target=cls._exec_notify, args=( + None, trigger.option['notifies'], None, trigger.id, None, ci.id)).start() + + return True + + return False diff --git a/cmdb-api/api/lib/cmdb/ci_type.py b/cmdb-api/api/lib/cmdb/ci_type.py index 0f28d97..38e091b 100644 --- a/cmdb-api/api/lib/cmdb/ci_type.py +++ b/cmdb-api/api/lib/cmdb/ci_type.py @@ -1,11 +1,13 @@ -# -*- coding:utf-8 -*- +# -*- coding:utf-8 -*- import copy import datetime +import toposort from flask import abort from flask import current_app from flask_login import current_user +from toposort import toposort_flatten from api.extensions import db from api.lib.cmdb.attribute import AttributeManager @@ -114,7 +116,7 @@ class CITypeManager(object): @kwargs_required("name") def add(cls, **kwargs): - unique_key = kwargs.pop("unique_key", None) + unique_key = kwargs.pop("unique_key", None) or kwargs.pop("unique_id", None) unique_key = AttributeCache.get(unique_key) or abort(404, ErrFormat.unique_key_not_define) kwargs["alias"] = kwargs["name"] if not kwargs.get("alias") else kwargs["alias"] @@ -276,10 +278,10 @@ class CITypeGroupManager(object): def update(gid, name, type_ids): """ update part - :param gid: - :param name: - :param type_ids: - :return: + :param gid: + :param name: + :param type_ids: + :return: """ existed = CITypeGroup.get_by_id(gid) or abort( 404, ErrFormat.ci_type_group_not_found.format("id={}".format(gid))) @@ -370,6 +372,16 @@ class CITypeAttributeManager(object): return result + @staticmethod + def get_common_attributes(type_ids): + 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)] + @staticmethod def _check(type_id, attr_ids): ci_type = CITypeManager.check_is_existed(type_id) @@ -386,10 +398,10 @@ class CITypeAttributeManager(object): def add(cls, type_id, attr_ids=None, **kwargs): """ add attributes to CIType - :param type_id: + :param type_id: :param attr_ids: list - :param kwargs: - :return: + :param kwargs: + :return: """ attr_ids = list(set(attr_ids)) @@ -416,9 +428,9 @@ class CITypeAttributeManager(object): def update(cls, type_id, attributes): """ update attributes to CIType - :param type_id: + :param type_id: :param attributes: list - :return: + :return: """ cls._check(type_id, [i.get('attr_id') for i in attributes]) @@ -446,9 +458,9 @@ class CITypeAttributeManager(object): def delete(cls, type_id, attr_ids=None): """ delete attributes from CIType - :param type_id: + :param type_id: :param attr_ids: list - :return: + :return: """ from api.tasks.cmdb import ci_cache @@ -564,6 +576,23 @@ class CITypeRelationManager(object): return [cls._wrap_relation_type_dict(child.child_id, child) for child in children] + @classmethod + def recursive_level2children(cls, parent_id): + result = dict() + + def get_children(_id, level): + children = CITypeRelation.get_by(parent_id=_id, to_dict=False) + if children: + result.setdefault(level + 1, []).extend([i.child.to_dict() for i in children]) + + for i in children: + if i.child_id != _id: + get_children(i.child_id, level + 1) + + get_children(parent_id, 0) + + return result + @classmethod def get_parents(cls, child_id): parents = CITypeRelation.get_by(child_id=child_id, to_dict=False) @@ -586,6 +615,17 @@ class CITypeRelationManager(object): p = CITypeManager.check_is_existed(parent) c = CITypeManager.check_is_existed(child) + rels = {} + for i in CITypeRelation.get_by(to_dict=False): + rels.setdefault(i.child_id, set()).add(i.parent_id) + rels.setdefault(c.id, set()).add(p.id) + + try: + toposort_flatten(rels) + except toposort.CircularDependencyError as e: + current_app.logger.warning(str(e)) + return abort(400, ErrFormat.circular_dependency_error) + existed = cls._get(p.id, c.id) if existed is not None: existed.update(relation_type_id=relation_type_id, @@ -823,6 +863,12 @@ class CITypeTemplateManager(object): for added_id in set(id2obj_dicts.keys()) - set(existed_ids): if cls == CIType: CITypeManager.add(**id2obj_dicts[added_id]) + elif cls == CITypeRelation: + CITypeRelationManager.add(id2obj_dicts[added_id].get('parent_id'), + id2obj_dicts[added_id].get('child_id'), + id2obj_dicts[added_id].get('relation_type_id'), + id2obj_dicts[added_id].get('constraint'), + ) else: cls.create(flush=True, **id2obj_dicts[added_id]) @@ -1120,16 +1166,18 @@ class CITypeUniqueConstraintManager(object): class CITypeTriggerManager(object): @staticmethod - def get(type_id): - return CITypeTrigger.get_by(type_id=type_id, to_dict=True) + def get(type_id, to_dict=True): + return CITypeTrigger.get_by(type_id=type_id, to_dict=to_dict) @staticmethod - def add(type_id, attr_id, notify): - CITypeTrigger.get_by(type_id=type_id, attr_id=attr_id) and abort(400, ErrFormat.ci_type_trigger_duplicate) + def add(type_id, attr_id, option): + for i in CITypeTrigger.get_by(type_id=type_id, attr_id=attr_id, to_dict=False): + if i.option == option: + return abort(400, ErrFormat.ci_type_trigger_duplicate) - not isinstance(notify, dict) and abort(400, ErrFormat.argument_invalid.format("notify")) + not isinstance(option, dict) and abort(400, ErrFormat.argument_invalid.format("option")) - trigger = CITypeTrigger.create(type_id=type_id, attr_id=attr_id, notify=notify) + trigger = CITypeTrigger.create(type_id=type_id, attr_id=attr_id, option=option) CITypeHistoryManager.add(CITypeOperateType.ADD_TRIGGER, type_id, @@ -1139,12 +1187,12 @@ class CITypeTriggerManager(object): return trigger.to_dict() @staticmethod - def update(_id, notify): + def update(_id, option): existed = (CITypeTrigger.get_by_id(_id) or abort(404, ErrFormat.ci_type_trigger_not_found.format("id={}".format(_id)))) existed2 = existed.to_dict() - new = existed.update(notify=notify) + new = existed.update(option=option) CITypeHistoryManager.add(CITypeOperateType.UPDATE_TRIGGER, existed.type_id, @@ -1164,35 +1212,3 @@ class CITypeTriggerManager(object): existed.type_id, trigger_id=_id, change=existed.to_dict()) - - @staticmethod - def waiting_cis(trigger): - now = datetime.datetime.today() - - delta_time = datetime.timedelta(days=(trigger.notify.get('before_days', 0) or 0)) - - attr = AttributeCache.get(trigger.attr_id) - - value_table = TableMap(attr=attr).table - - values = value_table.get_by(attr_id=attr.id, to_dict=False) - - result = [] - for v in values: - if (isinstance(v.value, (datetime.date, datetime.datetime)) and - (v.value - delta_time).strftime('%Y%m%d') == now.strftime("%Y%m%d")): - result.append(v) - - return result - - @staticmethod - def trigger_notify(trigger, ci): - if (trigger.notify.get('notify_at') == datetime.datetime.now().strftime("%H:%M") or - not trigger.notify.get('notify_at')): - from api.tasks.cmdb import trigger_notify - - trigger_notify.apply_async(args=(trigger.notify, ci.ci_id), queue=CMDB_QUEUE) - - return True - - return False diff --git a/cmdb-api/api/lib/cmdb/custom_dashboard.py b/cmdb-api/api/lib/cmdb/custom_dashboard.py index 8153eec..133769a 100644 --- a/cmdb-api/api/lib/cmdb/custom_dashboard.py +++ b/cmdb-api/api/lib/cmdb/custom_dashboard.py @@ -14,6 +14,14 @@ class CustomDashboardManager(object): def get(): return sorted(CustomDashboard.get_by(to_dict=True), key=lambda x: (x["category"], x['order'])) + @staticmethod + def preview(**kwargs): + from api.lib.cmdb.cache import CMDBCounterCache + + res = CMDBCounterCache.update(kwargs, flush=False) + + return res + @staticmethod def add(**kwargs): from api.lib.cmdb.cache import CMDBCounterCache @@ -23,9 +31,9 @@ class CustomDashboardManager(object): new = CustomDashboard.create(**kwargs) - CMDBCounterCache.update(new.to_dict()) + res = CMDBCounterCache.update(new.to_dict()) - return new + return new, res @staticmethod def update(_id, **kwargs): @@ -35,9 +43,9 @@ class CustomDashboardManager(object): new = existed.update(**kwargs) - CMDBCounterCache.update(new.to_dict()) + res = CMDBCounterCache.update(new.to_dict()) - return new + return new, res @staticmethod def batch_update(id2options): diff --git a/cmdb-api/api/lib/cmdb/history.py b/cmdb-api/api/lib/cmdb/history.py index 6580737..bb4537f 100644 --- a/cmdb-api/api/lib/cmdb/history.py +++ b/cmdb-api/api/lib/cmdb/history.py @@ -16,6 +16,7 @@ from api.lib.perm.acl.cache import UserCache from api.models.cmdb import Attribute from api.models.cmdb import AttributeHistory from api.models.cmdb import CIRelationHistory +from api.models.cmdb import CITriggerHistory from api.models.cmdb import CITypeHistory from api.models.cmdb import CITypeTrigger from api.models.cmdb import CITypeUniqueConstraint @@ -286,3 +287,67 @@ class CITypeHistoryManager(object): change=change) CITypeHistory.create(**payload) + + +class CITriggerHistoryManager(object): + @staticmethod + def get(page, page_size, type_id=None, trigger_id=None, operate_type=None): + query = CITriggerHistory.get_by(only_query=True) + if type_id is not None: + query = query.filter(CITriggerHistory.type_id == type_id) + + if trigger_id: + query = query.filter(CITriggerHistory.trigger_id == trigger_id) + + if operate_type is not None: + query = query.filter(CITriggerHistory.operate_type == operate_type) + + numfound = query.count() + + query = query.order_by(CITriggerHistory.id.desc()) + result = query.offset((page - 1) * page_size).limit(page_size) + result = [i.to_dict() for i in result] + for res in result: + if res.get('trigger_id'): + trigger = CITypeTrigger.get_by_id(res['trigger_id']) + res['trigger'] = trigger and trigger.to_dict() + + return numfound, result + + @staticmethod + def get_by_ci_id(ci_id): + res = db.session.query(CITriggerHistory, CITypeTrigger, OperationRecord).join( + CITypeTrigger, CITypeTrigger.id == CITriggerHistory.trigger_id).join( + OperationRecord, OperationRecord.id == CITriggerHistory.record_id).filter( + CITriggerHistory.ci_id == ci_id).order_by(CITriggerHistory.id.desc()) + + result = [] + id2trigger = dict() + for i in res: + hist = i.CITriggerHistory + record = i.OperationRecord + item = dict(is_ok=hist.is_ok, + operate_type=hist.operate_type, + notify=hist.notify, + webhook=hist.webhook, + created_at=record.created_at.strftime('%Y-%m-%d %H:%M:%S'), + record_id=record.id, + hid=hist.id + ) + if i.CITypeTrigger.id not in id2trigger: + id2trigger[i.CITypeTrigger.id] = i.CITypeTrigger.to_dict() + + result.append(item) + + return dict(items=result, id2trigger=id2trigger) + + @staticmethod + def add(operate_type, record_id, ci_id, trigger_id, is_ok=False, notify=None, webhook=None): + + CITriggerHistory.create(operate_type=operate_type, + record_id=record_id, + ci_id=ci_id, + trigger_id=trigger_id, + is_ok=is_ok, + notify=notify, + webhook=webhook) diff --git a/cmdb-api/api/lib/cmdb/query_sql.py b/cmdb-api/api/lib/cmdb/query_sql.py index c84fd64..f5c598b 100644 --- a/cmdb-api/api/lib/cmdb/query_sql.py +++ b/cmdb-api/api/lib/cmdb/query_sql.py @@ -42,7 +42,7 @@ FACET_QUERY1 = """ FACET_QUERY = """ SELECT {0}.value, - count({0}.ci_id) + count(distinct({0}.ci_id)) FROM {0} INNER JOIN ({1}) AS F ON F.ci_id={0}.ci_id WHERE {0}.attr_id={2:d} diff --git a/cmdb-api/api/lib/cmdb/search/ci/db/search.py b/cmdb-api/api/lib/cmdb/search/ci/db/search.py index 3dc5792..0e26453 100644 --- a/cmdb-api/api/lib/cmdb/search/ci/db/search.py +++ b/cmdb-api/api/lib/cmdb/search/ci/db/search.py @@ -1,4 +1,4 @@ -# -*- coding:utf-8 -*- +# -*- coding:utf-8 -*- from __future__ import unicode_literals @@ -141,6 +141,10 @@ class Search(object): @staticmethod def _in_query_handler(attr, v, is_not): new_v = v[1:-1].split(";") + + if attr.value_type == ValueTypeEnum.DATE: + new_v = ["{} 00:00:00".format(i) for i in new_v if len(i) == 10] + table_name = TableMap(attr=attr).table_name in_query = " OR {0}.value ".format(table_name).join(['{0} "{1}"'.format( "NOT LIKE" if is_not else "LIKE", @@ -151,6 +155,11 @@ class Search(object): @staticmethod def _range_query_handler(attr, v, is_not): start, end = [x.strip() for x in v[1:-1].split("_TO_")] + + if attr.value_type == ValueTypeEnum.DATE: + start = "{} 00:00:00".format(start) if len(start) == 10 else start + end = "{} 00:00:00".format(end) if len(end) == 10 else end + table_name = TableMap(attr=attr).table_name range_query = "{0} '{1}' AND '{2}'".format( "NOT BETWEEN" if is_not else "BETWEEN", @@ -162,8 +171,14 @@ class Search(object): def _comparison_query_handler(attr, v): table_name = TableMap(attr=attr).table_name if v.startswith(">=") or v.startswith("<="): + if attr.value_type == ValueTypeEnum.DATE and len(v[2:]) == 10: + v = "{} 00:00:00".format(v) + comparison_query = "{0} '{1}'".format(v[:2], v[2:].replace("*", "%")) else: + if attr.value_type == ValueTypeEnum.DATE and len(v[1:]) == 10: + v = "{} 00:00:00".format(v) + comparison_query = "{0} '{1}'".format(v[0], v[1:].replace("*", "%")) _query_sql = QUERY_CI_BY_ATTR_NAME.format(table_name, attr.id, comparison_query) return _query_sql @@ -239,7 +254,7 @@ class Search(object): attr_id = attr.id table_name = TableMap(attr=attr).table_name - _v_query_sql = """SELECT {0}.ci_id, {1}.value + _v_query_sql = """SELECT {0}.ci_id, {1}.value FROM ({2}) AS {0} INNER JOIN {1} ON {1}.ci_id = {0}.ci_id WHERE {1}.attr_id = {3}""".format("ALIAS", table_name, query_sql, attr_id) new_table = _v_query_sql @@ -285,7 +300,7 @@ class Search(object): query_sql = "SELECT * FROM ({0}) as {1} UNION ALL ({2})".format(query_sql, alias, _query_sql) elif operator == "~": - query_sql = """SELECT * FROM ({0}) as {1} LEFT JOIN ({2}) as {3} USING(ci_id) + query_sql = """SELECT * FROM ({0}) as {1} LEFT JOIN ({2}) as {3} USING(ci_id) WHERE {3}.ci_id is NULL""".format(query_sql, alias, _query_sql, alias + "A") return query_sql @@ -295,7 +310,7 @@ class Search(object): start = time.time() execute = db.session.execute - current_app.logger.debug(v_query_sql) + # current_app.logger.debug(v_query_sql) res = execute(v_query_sql).fetchall() end_time = time.time() current_app.logger.debug("query ci ids time is: {0}".format(end_time - start)) @@ -391,6 +406,9 @@ class Search(object): is_not = True if operator == "|~" else False + if field_type == ValueTypeEnum.DATE and len(v) == 10: + v = "{} 00:00:00".format(v) + # in query if v.startswith("(") and v.endswith(")"): _query_sql = self._in_query_handler(attr, v, is_not) @@ -506,7 +524,7 @@ class Search(object): if k: table_name = TableMap(attr=attr).table_name query_sql = FACET_QUERY.format(table_name, self.query_sql, attr.id) - # current_app.logger.debug(query_sql) + # current_app.logger.warning(query_sql) result = db.session.execute(query_sql).fetchall() facet[k] = result diff --git a/cmdb-api/api/lib/cmdb/value.py b/cmdb-api/api/lib/cmdb/value.py index aca53a5..3e1cb86 100644 --- a/cmdb-api/api/lib/cmdb/value.py +++ b/cmdb-api/api/lib/cmdb/value.py @@ -18,7 +18,6 @@ from api.extensions import db from api.lib.cmdb.attribute import AttributeManager from api.lib.cmdb.cache import AttributeCache from api.lib.cmdb.cache import CITypeAttributeCache -from api.lib.cmdb.const import ExistPolicy from api.lib.cmdb.const import OperateType from api.lib.cmdb.const import ValueTypeEnum from api.lib.cmdb.history import AttributeHistoryManger @@ -140,6 +139,7 @@ class AttributeValueManager(object): try: db.session.commit() except Exception as e: + db.session.rollback() current_app.logger.error("write change failed: {}".format(str(e))) return record_id @@ -235,7 +235,7 @@ class AttributeValueManager(object): return key2attr - def create_or_update_attr_value2(self, ci, ci_dict, key2attr): + def create_or_update_attr_value(self, ci, ci_dict, key2attr): """ add or update attribute value, then write history :param ci: instance object @@ -288,66 +288,6 @@ class AttributeValueManager(object): return self._write_change2(changed) - def create_or_update_attr_value(self, key, value, ci, _no_attribute_policy=ExistPolicy.IGNORE, record_id=None): - """ - add or update attribute value, then write history - :param key: id, name or alias - :param value: - :param ci: instance object - :param _no_attribute_policy: ignore or reject - :param record_id: op record - :return: - """ - attr = self._get_attr(key) - if attr is None: - if _no_attribute_policy == ExistPolicy.IGNORE: - return - if _no_attribute_policy == ExistPolicy.REJECT: - return abort(400, ErrFormat.attribute_not_found.format(key)) - - value_table = TableMap(attr=attr).table - - try: - if attr.is_list: - value_list = [self._validate(attr, i, value_table, ci) for i in handle_arg_list(value)] - if not value_list: - self._check_is_required(ci.type_id, attr, '') - - existed_attrs = value_table.get_by(attr_id=attr.id, ci_id=ci.id, to_dict=False) - existed_values = [i.value for i in existed_attrs] - added = set(value_list) - set(existed_values) - deleted = set(existed_values) - set(value_list) - for v in added: - value_table.create(ci_id=ci.id, attr_id=attr.id, value=v) - record_id = self._write_change(ci.id, attr.id, OperateType.ADD, None, v, record_id, ci.type_id) - - for v in deleted: - existed_attr = existed_attrs[existed_values.index(v)] - existed_attr.delete() - record_id = self._write_change(ci.id, attr.id, OperateType.DELETE, v, None, record_id, ci.type_id) - else: - value = self._validate(attr, value, value_table, ci) - existed_attr = value_table.get_by(attr_id=attr.id, ci_id=ci.id, first=True, to_dict=False) - existed_value = existed_attr and existed_attr.value - if existed_value is None and value is not None: - value_table.create(ci_id=ci.id, attr_id=attr.id, value=value) - - record_id = self._write_change(ci.id, attr.id, OperateType.ADD, None, value, record_id, ci.type_id) - else: - if existed_value != value: - if value is None: - existed_attr.delete() - else: - existed_attr.update(value=value) - - record_id = self._write_change(ci.id, attr.id, OperateType.UPDATE, - existed_value, value, record_id, ci.type_id) - - return record_id - except Exception as e: - current_app.logger.warning(str(e)) - return abort(400, ErrFormat.attribute_value_invalid2.format("{}({})".format(attr.alias, attr.name), value)) - @staticmethod def delete_attr_value(attr_id, ci_id): attr = AttributeCache.get(attr_id) diff --git a/cmdb-api/api/lib/common_setting/acl.py b/cmdb-api/api/lib/common_setting/acl.py index 163a373..99e1860 100644 --- a/cmdb-api/api/lib/common_setting/acl.py +++ b/cmdb-api/api/lib/common_setting/acl.py @@ -6,6 +6,7 @@ from api.lib.common_setting.resp_format import ErrFormat from api.lib.perm.acl.cache import RoleCache, AppCache from api.lib.perm.acl.role import RoleCRUD, RoleRelationCRUD from api.lib.perm.acl.user import UserCRUD +from api.lib.perm.acl.resource import ResourceTypeCRUD, ResourceCRUD class ACLManager(object): @@ -94,3 +95,22 @@ class ACLManager(object): avatar=user_info.get('avatar')) return result + + def validate_app(self): + return AppCache.get(self.app_name) + + def get_all_resources_types(self, q=None, page=1, page_size=999999): + app_id = self.validate_app().id + numfound, res, id2perms = ResourceTypeCRUD.search(q, app_id, page, page_size) + + return dict( + numfound=numfound, + groups=[i.to_dict() for i in res], + id2perms=id2perms + ) + + def create_resource(self, payload): + payload['app_id'] = self.validate_app().id + resource = ResourceCRUD.add(**payload) + + return resource.to_dict() diff --git a/cmdb-api/api/lib/notify.py b/cmdb-api/api/lib/notify.py new file mode 100644 index 0000000..c32087b --- /dev/null +++ b/cmdb-api/api/lib/notify.py @@ -0,0 +1,45 @@ +# -*- coding:utf-8 -*- + +import json + +import requests +from flask import current_app +from jinja2 import Template + +from api.lib.mail import send_mail + + +def _request_messenger(subject, body, tos, sender): + params = dict(sender=sender, title=subject, + tos=[to[sender] for to in tos if to.get(sender)]) + + if not params['tos']: + raise Exception("no receivers") + + if sender == "email": + params['msgtype'] = 'text/html' + params['content'] = body + else: + params['msgtype'] = 'text' + params['content'] = json.dumps(dict(content=subject or body)) + + resp = requests.post(current_app.config.get('MESSENGER_URL'), json=params) + if resp.status_code != 200: + raise Exception(resp.text) + + return resp.text + + +def notify_send(subject, body, methods, tos, payload=None): + payload = payload or {} + subject = Template(subject).render(payload) + body = Template(body).render(payload) + + res = '' + for method in methods: + if method == "email" and not current_app.config.get('USE_MESSENGER', True): + send_mail(None, [to.get('email') for to in tos], subject, body) + + res += _request_messenger(subject, body, tos, method) + "\n" + + return res diff --git a/cmdb-api/api/lib/resp_format.py b/cmdb-api/api/lib/resp_format.py index e3cf4da..5a7c852 100644 --- a/cmdb-api/api/lib/resp_format.py +++ b/cmdb-api/api/lib/resp_format.py @@ -9,6 +9,8 @@ class CommonErrFormat(object): not_found = "不存在" + circular_dependency_error = "存在循环依赖!" + unknown_search_error = "未知搜索错误" invalid_json = "json格式似乎不正确了, 请仔细确认一下!" diff --git a/cmdb-api/api/lib/webhook.py b/cmdb-api/api/lib/webhook.py new file mode 100644 index 0000000..280be18 --- /dev/null +++ b/cmdb-api/api/lib/webhook.py @@ -0,0 +1,105 @@ +# -*- coding:utf-8 -*- + +import json +from functools import partial + +import requests +from jinja2 import Template +from requests.auth import HTTPBasicAuth +from requests_oauthlib import OAuth2Session + + +class BearerAuth(requests.auth.AuthBase): + def __init__(self, token): + self.token = token + + def __call__(self, r): + r.headers["authorization"] = "Bearer {}".format(self.token) + return r + + +def _wrap_auth(**kwargs): + auth_type = (kwargs.get('type') or "").lower() + if auth_type == "basicauth": + return HTTPBasicAuth(kwargs.get('username'), kwargs.get('password')) + + elif auth_type == "bearer": + return BearerAuth(kwargs.get('token')) + + elif auth_type == 'oauth2.0': + client_id = kwargs.get('client_id') + client_secret = kwargs.get('client_secret') + authorization_base_url = kwargs.get('authorization_base_url') + token_url = kwargs.get('token_url') + redirect_url = kwargs.get('redirect_url') + scope = kwargs.get('scope') + + oauth2_session = OAuth2Session(client_id, scope=scope or None) + oauth2_session.authorization_url(authorization_base_url) + + oauth2_session.fetch_token(token_url, client_secret=client_secret, authorization_response=redirect_url) + + return oauth2_session + + elif auth_type == "apikey": + return HTTPBasicAuth(kwargs.get('key'), kwargs.get('value')) + + +def webhook_request(webhook, payload): + """ + + :param webhook: + { + "url": "https://veops.cn" + "method": "GET|POST|PUT|DELETE" + "body": {}, + "headers": { + "Content-Type": "Application/json" + }, + "parameters": { + "key": "value" + }, + "authorization": { + "type": "BasicAuth|Bearer|OAuth2.0|APIKey", + "password": "mmmm", # BasicAuth + "username": "bbb", # BasicAuth + + "token": "xxx", # Bearer + + "key": "xxx", # APIKey + "value": "xxx", # APIKey + + "client_id": "xxx", # OAuth2.0 + "client_secret": "xxx", # OAuth2.0 + "authorization_base_url": "xxx", # OAuth2.0 + "token_url": "xxx", # OAuth2.0 + "redirect_url": "xxx", # OAuth2.0 + "scope": "xxx" # OAuth2.0 + } + } + :param payload: + :return: + """ + assert webhook.get('url') is not None + + url = Template(webhook['url']).render(payload) + + params = webhook.get('parameters') or None + if isinstance(params, dict): + params = json.loads(Template(json.dumps(params)).render(payload)) + + data = Template(json.dumps(webhook.get('body', ''))).render(payload) + auth = _wrap_auth(**webhook.get('authorization', {})) + + if (webhook.get('authorization', {}).get("type") or '').lower() == 'oauth2.0': + request = getattr(auth, webhook.get('method', 'GET').lower()) + else: + request = partial(requests.request, webhook.get('method', 'GET')) + + return request( + url, + params=params, + headers=webhook.get('headers') or None, + data=data, + auth=auth + ) diff --git a/cmdb-api/api/models/cmdb.py b/cmdb-api/api/models/cmdb.py index 052957f..6f70fec 100644 --- a/cmdb-api/api/models/cmdb.py +++ b/cmdb-api/api/models/cmdb.py @@ -125,16 +125,26 @@ class CITypeAttributeGroupItem(Model): class CITypeTrigger(Model): - # __tablename__ = "c_ci_type_triggers" __tablename__ = "c_c_t_t" type_id = db.Column(db.Integer, db.ForeignKey('c_ci_types.id'), nullable=False) - attr_id = db.Column(db.Integer, db.ForeignKey("c_attributes.id"), nullable=False) - notify = db.Column(db.JSON) # {subject: x, body: x, wx_to: [], mail_to: [], before_days: 0, notify_at: 08:00} + attr_id = db.Column(db.Integer, db.ForeignKey("c_attributes.id")) + option = db.Column('notify', db.JSON) + + +class CITriggerHistory(Model): + __tablename__ = "c_ci_trigger_histories" + + operate_type = db.Column(db.Enum(*OperateType.all(), name="operate_type")) + record_id = db.Column(db.Integer, db.ForeignKey("c_records.id")) + ci_id = db.Column(db.Integer, index=True, nullable=False) + trigger_id = db.Column(db.Integer, db.ForeignKey("c_c_t_t.id")) + is_ok = db.Column(db.Boolean, default=False) + notify = db.Column(db.Text) + webhook = db.Column(db.Text) class CITypeUniqueConstraint(Model): - # __tablename__ = "c_ci_type_unique_constraints" __tablename__ = "c_c_t_u_c" type_id = db.Column(db.Integer, db.ForeignKey('c_ci_types.id'), nullable=False) @@ -363,7 +373,6 @@ class CITypeHistory(Model): # preference class PreferenceShowAttributes(Model): - # __tablename__ = "c_preference_show_attributes" __tablename__ = "c_psa" uid = db.Column(db.Integer, index=True, nullable=False) @@ -377,7 +386,6 @@ class PreferenceShowAttributes(Model): class PreferenceTreeView(Model): - # __tablename__ = "c_preference_tree_views" __tablename__ = "c_ptv" uid = db.Column(db.Integer, index=True, nullable=False) @@ -386,7 +394,6 @@ class PreferenceTreeView(Model): class PreferenceRelationView(Model): - # __tablename__ = "c_preference_relation_views" __tablename__ = "c_prv" uid = db.Column(db.Integer, index=True, nullable=False) diff --git a/cmdb-api/api/views/cmdb/attribute.py b/cmdb-api/api/views/cmdb/attribute.py index 2a3edf2..fffbcdc 100644 --- a/cmdb-api/api/views/cmdb/attribute.py +++ b/cmdb-api/api/views/cmdb/attribute.py @@ -56,12 +56,7 @@ class AttributeView(APIView): @args_required("name") @args_validate(AttributeManager.cls) - def post(self, attr_id=None): - if request.url.endswith("/calc_computed_attribute"): - AttributeManager.calc_computed_attribute(attr_id) - - return self.jsonify(attr_id=attr_id) - + def post(self): choice_value = handle_arg_list(request.values.get("choice_value")) params = request.values params["choice_value"] = choice_value @@ -74,6 +69,11 @@ class AttributeView(APIView): @args_validate(AttributeManager.cls) def put(self, attr_id): + if request.url.endswith("/calc_computed_attribute"): + AttributeManager.calc_computed_attribute(attr_id) + + return self.jsonify(attr_id=attr_id) + choice_value = handle_arg_list(request.values.get("choice_value")) params = request.values params["choice_value"] = choice_value diff --git a/cmdb-api/api/views/cmdb/ci.py b/cmdb-api/api/views/cmdb/ci.py index 47c0147..6ce16ac 100644 --- a/cmdb-api/api/views/cmdb/ci.py +++ b/cmdb-api/api/views/cmdb/ci.py @@ -185,8 +185,8 @@ class CIUnique(APIView): @has_perm_from_args("ci_id", ResourceTypeEnum.CI, PermEnum.UPDATE, CIManager.get_type_name) def put(self, ci_id): params = request.values - unique_name = params.keys()[0] - unique_value = params.values()[0] + unique_name = list(params.keys())[0] + unique_value = list(params.values())[0] CIManager.update_unique_value(ci_id, unique_name, unique_value) diff --git a/cmdb-api/api/views/cmdb/ci_type.py b/cmdb-api/api/views/cmdb/ci_type.py index ba2686f..df1ed8e 100644 --- a/cmdb-api/api/views/cmdb/ci_type.py +++ b/cmdb-api/api/views/cmdb/ci_type.py @@ -1,4 +1,4 @@ -# -*- coding:utf-8 -*- +# -*- coding:utf-8 -*- import json @@ -154,9 +154,15 @@ class EnableCITypeView(APIView): class CITypeAttributeView(APIView): - url_prefix = ("/ci_types//attributes", "/ci_types//attributes") + url_prefix = ("/ci_types//attributes", "/ci_types//attributes", + "/ci_types/common_attributes") def get(self, type_id=None, type_name=None): + if request.path.endswith("/common_attributes"): + type_ids = handle_arg_list(request.values.get('type_ids')) + + return self.jsonify(attributes=CITypeAttributeManager.get_common_attributes(type_ids)) + t = CITypeCache.get(type_id) or CITypeCache.get(type_name) or abort(404, ErrFormat.ci_type_not_found) type_id = t.id unique_id = t.unique_id @@ -413,22 +419,21 @@ class CITypeTriggerView(APIView): return self.jsonify(CITypeTriggerManager.get(type_id)) @has_perm_from_args("type_id", ResourceTypeEnum.CI, PermEnum.CONFIG, CITypeManager.get_name_by_id) - @args_required("attr_id") - @args_required("notify") + @args_required("option") def post(self, type_id): - attr_id = request.values.get('attr_id') - notify = request.values.get('notify') + attr_id = request.values.get('attr_id') or None + option = request.values.get('option') - return self.jsonify(CITypeTriggerManager().add(type_id, attr_id, notify)) + return self.jsonify(CITypeTriggerManager().add(type_id, attr_id, option)) @has_perm_from_args("type_id", ResourceTypeEnum.CI, PermEnum.CONFIG, CITypeManager.get_name_by_id) - @args_required("notify") + @args_required("option") def put(self, type_id, _id): assert type_id is not None - notify = request.values.get('notify') + option = request.values.get('option') - return self.jsonify(CITypeTriggerManager().update(_id, notify)) + return self.jsonify(CITypeTriggerManager().update(_id, option)) @has_perm_from_args("type_id", ResourceTypeEnum.CI, PermEnum.CONFIG, CITypeManager.get_name_by_id) def delete(self, type_id, _id): @@ -500,3 +505,4 @@ class CITypeFilterPermissionView(APIView): @auth_with_app_token def get(self, type_id): return self.jsonify(CIFilterPermsCRUD().get(type_id)) + diff --git a/cmdb-api/api/views/cmdb/ci_type_relation.py b/cmdb-api/api/views/cmdb/ci_type_relation.py index 55d5d97..20ce323 100644 --- a/cmdb-api/api/views/cmdb/ci_type_relation.py +++ b/cmdb-api/api/views/cmdb/ci_type_relation.py @@ -19,9 +19,14 @@ from api.resource import APIView class GetChildrenView(APIView): - url_prefix = "/ci_type_relations//children" + url_prefix = ("/ci_type_relations//children", + "/ci_type_relations//recursive_level2children", + ) def get(self, parent_id): + if request.url.endswith("recursive_level2children"): + return self.jsonify(CITypeRelationManager.recursive_level2children(parent_id)) + return self.jsonify(children=CITypeRelationManager.get_children(parent_id)) diff --git a/cmdb-api/api/views/cmdb/custom_dashboard.py b/cmdb-api/api/views/cmdb/custom_dashboard.py index 4991ceb..1c20ff9 100644 --- a/cmdb-api/api/views/cmdb/custom_dashboard.py +++ b/cmdb-api/api/views/cmdb/custom_dashboard.py @@ -13,7 +13,8 @@ from api.resource import APIView class CustomDashboardApiView(APIView): - url_prefix = ("/custom_dashboard", "/custom_dashboard/", "/custom_dashboard/batch") + url_prefix = ("/custom_dashboard", "/custom_dashboard/", "/custom_dashboard/batch", + "/custom_dashboard/preview") def get(self): return self.jsonify(CustomDashboardManager.get()) @@ -21,17 +22,26 @@ class CustomDashboardApiView(APIView): @role_required(RoleEnum.CONFIG) @args_validate(CustomDashboardManager.cls) def post(self): - cm = CustomDashboardManager.add(**request.values) + if request.url.endswith("/preview"): + return self.jsonify(counter=CustomDashboardManager.preview(**request.values)) - return self.jsonify(cm.to_dict()) + cm, counter = CustomDashboardManager.add(**request.values) + + res = cm.to_dict() + res.update(counter=counter) + + return self.jsonify(res) @role_required(RoleEnum.CONFIG) @args_validate(CustomDashboardManager.cls) def put(self, _id=None): if _id is not None: - cm = CustomDashboardManager.update(_id, **request.values) + cm, counter = CustomDashboardManager.update(_id, **request.values) - return self.jsonify(cm.to_dict()) + res = cm.to_dict() + res.update(counter=counter) + + return self.jsonify(res) CustomDashboardManager.batch_update(request.values.get("id2options")) diff --git a/cmdb-api/api/views/cmdb/history.py b/cmdb-api/api/views/cmdb/history.py index b21f9fd..ceaa2db 100644 --- a/cmdb-api/api/views/cmdb/history.py +++ b/cmdb-api/api/views/cmdb/history.py @@ -5,15 +5,18 @@ import datetime from flask import abort from flask import request +from flask import session from api.lib.cmdb.ci import CIManager 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.history import AttributeHistoryManger +from api.lib.cmdb.history import CITriggerHistoryManager from api.lib.cmdb.history import CITypeHistoryManager from api.lib.cmdb.resp_format import ErrFormat from api.lib.perm.acl.acl import has_perm_from_args +from api.lib.perm.acl.acl import is_app_admin from api.lib.perm.acl.acl import role_required from api.lib.utils import get_page from api.lib.utils import get_page_size @@ -76,6 +79,39 @@ class CIHistoryView(APIView): return self.jsonify(result) +class CITriggerHistoryView(APIView): + url_prefix = ("/history/ci_triggers/", "/history/ci_triggers") + + @has_perm_from_args("ci_id", ResourceTypeEnum.CI, PermEnum.READ, CIManager.get_type_name) + def get(self, ci_id=None): + if ci_id is not None: + result = CITriggerHistoryManager.get_by_ci_id(ci_id) + + return self.jsonify(result) + + if RoleEnum.CONFIG not in session.get("acl", {}).get("parentRoles", []) and not is_app_admin("cmdb"): + return abort(403, ErrFormat.role_required.format(RoleEnum.CONFIG)) + + type_id = request.values.get("type_id") + trigger_id = request.values.get("trigger_id") + operate_type = request.values.get("operate_type") + + page = get_page(request.values.get('page', 1)) + page_size = get_page_size(request.values.get('page_size', 1)) + + numfound, result = CITriggerHistoryManager.get(page, + page_size, + type_id=type_id, + trigger_id=trigger_id, + operate_type=operate_type) + + return self.jsonify(page=page, + page_size=page_size, + numfound=numfound, + total=len(result), + result=result) + + class CITypeHistoryView(APIView): url_prefix = "/history/ci_types" diff --git a/cmdb-api/api/views/common_setting/file_manage.py b/cmdb-api/api/views/common_setting/file_manage.py index 23e0047..7150365 100644 --- a/cmdb-api/api/views/common_setting/file_manage.py +++ b/cmdb-api/api/views/common_setting/file_manage.py @@ -11,7 +11,7 @@ from api.resource import APIView prefix = '/file' ALLOWED_EXTENSIONS = { - 'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif', 'xls', 'xlsx', 'doc', 'docx', 'ppt', 'pptx', 'csv' + 'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif', 'xls', 'xlsx', 'doc', 'docx', 'ppt', 'pptx', 'csv', 'svg' } diff --git a/cmdb-api/requirements.txt b/cmdb-api/requirements.txt index ac5cf22..6e8a788 100644 --- a/cmdb-api/requirements.txt +++ b/cmdb-api/requirements.txt @@ -36,6 +36,7 @@ python-ldap==3.4.0 PyYAML==6.0 redis==4.6.0 requests==2.31.0 +requests_oauthlib==1.3.1 six==1.12.0 SQLAlchemy==1.4.49 supervisor==4.0.3 diff --git a/cmdb-api/settings.example.py b/cmdb-api/settings.example.py index 84c34ad..0102322 100644 --- a/cmdb-api/settings.example.py +++ b/cmdb-api/settings.example.py @@ -94,5 +94,3 @@ ES_HOST = '127.0.0.1' USE_ES = False BOOL_TRUE = ['true', 'TRUE', 'True', True, '1', 1, "Yes", "YES", "yes", 'Y', 'y'] - -CMDB_API = "http://127.0.0.1:5000/api/v0.1" diff --git a/cmdb-ui/public/iconfont/demo_index.html b/cmdb-ui/public/iconfont/demo_index.html index f7d3d47..2a0be2d 100644 --- a/cmdb-ui/public/iconfont/demo_index.html +++ b/cmdb-ui/public/iconfont/demo_index.html @@ -54,6 +54,84 @@
    +
  • + +
    cmdb-histogram
    +
    &#xe886;
    +
  • + +
  • + +
    cmdb-index
    +
    &#xe883;
    +
  • + +
  • + +
    cmdb-piechart
    +
    &#xe884;
    +
  • + +
  • + +
    cmdb-line
    +
    &#xe885;
    +
  • + +
  • + +
    cmdb-table
    +
    &#xe882;
    +
  • + +
  • + +
    itsm-all
    +
    &#xe87f;
    +
  • + +
  • + +
    itsm-reply
    +
    &#xe87e;
    +
  • + +
  • + +
    itsm-information
    +
    &#xe880;
    +
  • + +
  • + +
    itsm-contact
    +
    &#xe881;
    +
  • + +
  • + +
    itsm-my-processed
    +
    &#xe87d;
    +
  • + +
  • + +
    rule_7
    +
    &#xe87c;
    +
  • + +
  • + +
    itsm-my-completed
    +
    &#xe879;
    +
  • + +
  • + +
    itsm-my-plan
    +
    &#xe87b;
    +
  • +
  • rule_100
    @@ -3876,9 +3954,9 @@
    @font-face {
       font-family: 'iconfont';
    -  src: url('iconfont.woff2?t=1688550067963') format('woff2'),
    -       url('iconfont.woff?t=1688550067963') format('woff'),
    -       url('iconfont.ttf?t=1688550067963') format('truetype');
    +  src: url('iconfont.woff2?t=1694508259411') format('woff2'),
    +       url('iconfont.woff?t=1694508259411') format('woff'),
    +       url('iconfont.ttf?t=1694508259411') format('truetype');
     }
     

    第二步:定义使用 iconfont 的样式

    @@ -3904,6 +3982,123 @@
      +
    • + +
      + cmdb-histogram +
      +
      .cmdb-bar +
      +
    • + +
    • + +
      + cmdb-index +
      +
      .cmdb-count +
      +
    • + +
    • + +
      + cmdb-piechart +
      +
      .cmdb-pie +
      +
    • + +
    • + +
      + cmdb-line +
      +
      .cmdb-line +
      +
    • + +
    • + +
      + cmdb-table +
      +
      .cmdb-table +
      +
    • + +
    • + +
      + itsm-all +
      +
      .itsm-all +
      +
    • + +
    • + +
      + itsm-reply +
      +
      .itsm-reply +
      +
    • + +
    • + +
      + itsm-information +
      +
      .itsm-information +
      +
    • + +
    • + +
      + itsm-contact +
      +
      .itsm-contact +
      +
    • + +
    • + +
      + itsm-my-processed +
      +
      .itsm-my-my_already_handle +
      +
    • + +
    • + +
      + rule_7 +
      +
      .rule_7 +
      +
    • + +
    • + +
      + itsm-my-completed +
      +
      .itsm-my-completed +
      +
    • + +
    • + +
      + itsm-my-plan +
      +
      .itsm-my-plan +
      +
    • +
    • @@ -5759,11 +5954,11 @@
    • - +
      itsm-node-strat
      -
      .itsm-node-strat +
      .itsm-node-start
    • @@ -9637,6 +9832,110 @@
        +
      • + +
        cmdb-histogram
        +
        #cmdb-bar
        +
      • + +
      • + +
        cmdb-index
        +
        #cmdb-count
        +
      • + +
      • + +
        cmdb-piechart
        +
        #cmdb-pie
        +
      • + +
      • + +
        cmdb-line
        +
        #cmdb-line
        +
      • + +
      • + +
        cmdb-table
        +
        #cmdb-table
        +
      • + +
      • + +
        itsm-all
        +
        #itsm-all
        +
      • + +
      • + +
        itsm-reply
        +
        #itsm-reply
        +
      • + +
      • + +
        itsm-information
        +
        #itsm-information
        +
      • + +
      • + +
        itsm-contact
        +
        #itsm-contact
        +
      • + +
      • + +
        itsm-my-processed
        +
        #itsm-my-my_already_handle
        +
      • + +
      • + +
        rule_7
        +
        #rule_7
        +
      • + +
      • + +
        itsm-my-completed
        +
        #itsm-my-completed
        +
      • + +
      • + +
        itsm-my-plan
        +
        #itsm-my-plan
        +
      • +
      • itsm-node-strat
        -
        #itsm-node-strat
        +
        #itsm-node-start
      • diff --git a/cmdb-ui/public/iconfont/iconfont.css b/cmdb-ui/public/iconfont/iconfont.css index 0eaa8ac..e68f673 100644 --- a/cmdb-ui/public/iconfont/iconfont.css +++ b/cmdb-ui/public/iconfont/iconfont.css @@ -1,8 +1,8 @@ @font-face { font-family: "iconfont"; /* Project id 3857903 */ - src: url('iconfont.woff2?t=1688550067963') format('woff2'), - url('iconfont.woff?t=1688550067963') format('woff'), - url('iconfont.ttf?t=1688550067963') format('truetype'); + src: url('iconfont.woff2?t=1694508259411') format('woff2'), + url('iconfont.woff?t=1694508259411') format('woff'), + url('iconfont.ttf?t=1694508259411') format('truetype'); } .iconfont { @@ -13,6 +13,58 @@ -moz-osx-font-smoothing: grayscale; } +.cmdb-bar:before { + content: "\e886"; +} + +.cmdb-count:before { + content: "\e883"; +} + +.cmdb-pie:before { + content: "\e884"; +} + +.cmdb-line:before { + content: "\e885"; +} + +.cmdb-table:before { + content: "\e882"; +} + +.itsm-all:before { + content: "\e87f"; +} + +.itsm-reply:before { + content: "\e87e"; +} + +.itsm-information:before { + content: "\e880"; +} + +.itsm-contact:before { + content: "\e881"; +} + +.itsm-my-my_already_handle:before { + content: "\e87d"; +} + +.rule_7:before { + content: "\e87c"; +} + +.itsm-my-completed:before { + content: "\e879"; +} + +.itsm-my-plan:before { + content: "\e87b"; +} + .rule_100:before { content: "\e87a"; } @@ -837,7 +889,7 @@ content: "\e7ad"; } -.itsm-node-strat:before { +.itsm-node-start:before { content: "\e7ae"; } diff --git a/cmdb-ui/public/iconfont/iconfont.js b/cmdb-ui/public/iconfont/iconfont.js index d09af95..941640d 100644 --- a/cmdb-ui/public/iconfont/iconfont.js +++ b/cmdb-ui/public/iconfont/iconfont.js @@ -1 +1 @@ -window._iconfont_svg_string_3857903='',function(h){var c=(c=document.getElementsByTagName("script"))[c.length-1],a=c.getAttribute("data-injectcss"),c=c.getAttribute("data-disable-injectsvg");if(!c){var l,v,t,z,i,p=function(c,a){a.parentNode.insertBefore(c,a)};if(a&&!h.__iconfont__svg__cssinject__){h.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(c){console&&console.log(c)}}l=function(){var c,a=document.createElement("div");a.innerHTML=h._iconfont_svg_string_3857903,(a=a.getElementsByTagName("svg")[0])&&(a.setAttribute("aria-hidden","true"),a.style.position="absolute",a.style.width=0,a.style.height=0,a.style.overflow="hidden",a=a,(c=document.body).firstChild?p(a,c.firstChild):c.appendChild(a))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(l,0):(v=function(){document.removeEventListener("DOMContentLoaded",v,!1),l()},document.addEventListener("DOMContentLoaded",v,!1)):document.attachEvent&&(t=l,z=h.document,i=!1,m(),z.onreadystatechange=function(){"complete"==z.readyState&&(z.onreadystatechange=null,s())})}function s(){i||(i=!0,t())}function m(){try{z.documentElement.doScroll("left")}catch(c){return void setTimeout(m,50)}s()}}(window); \ No newline at end of file +window._iconfont_svg_string_3857903='',function(h){var c=(c=document.getElementsByTagName("script"))[c.length-1],a=c.getAttribute("data-injectcss"),c=c.getAttribute("data-disable-injectsvg");if(!c){var l,v,t,z,i,p=function(c,a){a.parentNode.insertBefore(c,a)};if(a&&!h.__iconfont__svg__cssinject__){h.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(c){console&&console.log(c)}}l=function(){var c,a=document.createElement("div");a.innerHTML=h._iconfont_svg_string_3857903,(a=a.getElementsByTagName("svg")[0])&&(a.setAttribute("aria-hidden","true"),a.style.position="absolute",a.style.width=0,a.style.height=0,a.style.overflow="hidden",a=a,(c=document.body).firstChild?p(a,c.firstChild):c.appendChild(a))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(l,0):(v=function(){document.removeEventListener("DOMContentLoaded",v,!1),l()},document.addEventListener("DOMContentLoaded",v,!1)):document.attachEvent&&(t=l,z=h.document,i=!1,m(),z.onreadystatechange=function(){"complete"==z.readyState&&(z.onreadystatechange=null,s())})}function s(){i||(i=!0,t())}function m(){try{z.documentElement.doScroll("left")}catch(c){return void setTimeout(m,50)}s()}}(window); \ No newline at end of file diff --git a/cmdb-ui/public/iconfont/iconfont.json b/cmdb-ui/public/iconfont/iconfont.json index 974b6d4..16b26ac 100644 --- a/cmdb-ui/public/iconfont/iconfont.json +++ b/cmdb-ui/public/iconfont/iconfont.json @@ -5,6 +5,97 @@ "css_prefix_text": "", "description": "", "glyphs": [ + { + "icon_id": "37334642", + "name": "cmdb-histogram", + "font_class": "cmdb-bar", + "unicode": "e886", + "unicode_decimal": 59526 + }, + { + "icon_id": "37334651", + "name": "cmdb-index", + "font_class": "cmdb-count", + "unicode": "e883", + "unicode_decimal": 59523 + }, + { + "icon_id": "37334650", + "name": "cmdb-piechart", + "font_class": "cmdb-pie", + "unicode": "e884", + "unicode_decimal": 59524 + }, + { + "icon_id": "37334648", + "name": "cmdb-line", + "font_class": "cmdb-line", + "unicode": "e885", + "unicode_decimal": 59525 + }, + { + "icon_id": "37334627", + "name": "cmdb-table", + "font_class": "cmdb-table", + "unicode": "e882", + "unicode_decimal": 59522 + }, + { + "icon_id": "37310392", + "name": "itsm-all", + "font_class": "itsm-all", + "unicode": "e87f", + "unicode_decimal": 59519 + }, + { + "icon_id": "36998696", + "name": "itsm-reply", + "font_class": "itsm-reply", + "unicode": "e87e", + "unicode_decimal": 59518 + }, + { + "icon_id": "36639018", + "name": "itsm-information", + "font_class": "itsm-information", + "unicode": "e880", + "unicode_decimal": 59520 + }, + { + "icon_id": "36639017", + "name": "itsm-contact", + "font_class": "itsm-contact", + "unicode": "e881", + "unicode_decimal": 59521 + }, + { + "icon_id": "36557425", + "name": "itsm-my-processed", + "font_class": "itsm-my-my_already_handle", + "unicode": "e87d", + "unicode_decimal": 59517 + }, + { + "icon_id": "36488174", + "name": "rule_7", + "font_class": "rule_7", + "unicode": "e87c", + "unicode_decimal": 59516 + }, + { + "icon_id": "36380087", + "name": "itsm-my-completed", + "font_class": "itsm-my-completed", + "unicode": "e879", + "unicode_decimal": 59513 + }, + { + "icon_id": "36380096", + "name": "itsm-my-plan", + "font_class": "itsm-my-plan", + "unicode": "e87b", + "unicode_decimal": 59515 + }, { "icon_id": "36304673", "name": "rule_100", @@ -1450,7 +1541,7 @@ { "icon_id": "35024980", "name": "itsm-node-strat", - "font_class": "itsm-node-strat", + "font_class": "itsm-node-start", "unicode": "e7ae", "unicode_decimal": 59310 }, diff --git a/cmdb-ui/public/iconfont/iconfont.ttf b/cmdb-ui/public/iconfont/iconfont.ttf index 201196d..1974179 100644 Binary files a/cmdb-ui/public/iconfont/iconfont.ttf and b/cmdb-ui/public/iconfont/iconfont.ttf differ diff --git a/cmdb-ui/public/iconfont/iconfont.woff b/cmdb-ui/public/iconfont/iconfont.woff index 8d13c6d..ff3ffbb 100644 Binary files a/cmdb-ui/public/iconfont/iconfont.woff and b/cmdb-ui/public/iconfont/iconfont.woff differ diff --git a/cmdb-ui/public/iconfont/iconfont.woff2 b/cmdb-ui/public/iconfont/iconfont.woff2 index 2de75bb..d7b4a21 100644 Binary files a/cmdb-ui/public/iconfont/iconfont.woff2 and b/cmdb-ui/public/iconfont/iconfont.woff2 differ diff --git a/cmdb-ui/src/components/CMDBFilterComp/index.vue b/cmdb-ui/src/components/CMDBFilterComp/index.vue index 6c347a6..07c0456 100644 --- a/cmdb-ui/src/components/CMDBFilterComp/index.vue +++ b/cmdb-ui/src/components/CMDBFilterComp/index.vue @@ -68,7 +68,8 @@ export default { }, methods: { - visibleChange(open) { + visibleChange(open, isInitOne = true) { + // isInitOne 初始化exp为空时,ruleList是否默认给一条 // const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g const exp = this.expression.match(new RegExp(this.regQ, 'g')) ? this.expression.match(new RegExp(this.regQ, 'g'))[0] @@ -151,15 +152,20 @@ export default { }) this.ruleList = [...expArray] } else if (open) { - this.ruleList = [ - { - id: uuidv4(), - type: 'and', - property: this.canSearchPreferenceAttrList[0].name, - exp: 'is', - value: null, - }, - ] + this.ruleList = isInitOne + ? [ + { + id: uuidv4(), + type: 'and', + property: + this.canSearchPreferenceAttrList && this.canSearchPreferenceAttrList.length + ? this.canSearchPreferenceAttrList[0].name + : undefined, + exp: 'is', + value: null, + }, + ] + : [] } }, handleClear() { diff --git a/cmdb-ui/src/modules/cmdb/api/CITypeAttr.js b/cmdb-ui/src/modules/cmdb/api/CITypeAttr.js index 3720c52..1a963b1 100644 --- a/cmdb-ui/src/modules/cmdb/api/CITypeAttr.js +++ b/cmdb-ui/src/modules/cmdb/api/CITypeAttr.js @@ -77,6 +77,14 @@ export function getCITypeAttributesByTypeIds(params) { }) } +export function getCITypeCommonAttributesByTypeIds(params) { + return axios({ + url: `/v0.1/ci_types/common_attributes`, + method: 'get', + params: params + }) +} + /** * 删除属性 * @param attrId diff --git a/cmdb-ui/src/modules/cmdb/api/CITypeRelation.js b/cmdb-ui/src/modules/cmdb/api/CITypeRelation.js index 20d4f5e..3aaf654 100644 --- a/cmdb-ui/src/modules/cmdb/api/CITypeRelation.js +++ b/cmdb-ui/src/modules/cmdb/api/CITypeRelation.js @@ -61,3 +61,10 @@ export function revokeTypeRelation(first_type_id, second_type_id, rid, data) { data }) } + +export function getRecursive_level2children(type_id) { + return axios({ + url: `/v0.1/ci_type_relations/${type_id}/recursive_level2children`, + method: 'GET' + }) +} diff --git a/cmdb-ui/src/modules/cmdb/api/customDashboard.js b/cmdb-ui/src/modules/cmdb/api/customDashboard.js index dcdd1f6..863609c 100644 --- a/cmdb-ui/src/modules/cmdb/api/customDashboard.js +++ b/cmdb-ui/src/modules/cmdb/api/customDashboard.js @@ -37,3 +37,11 @@ export function batchUpdateCustomDashboard(data) { data }) } + +export function postCustomDashboardPreview(data) { + return axios({ + url: '/v0.1/custom_dashboard/preview', + method: 'post', + data + }) +} diff --git a/cmdb-ui/src/modules/cmdb/views/custom_dashboard/chart.vue b/cmdb-ui/src/modules/cmdb/views/custom_dashboard/chart.vue index 0b896c2..8808e6d 100644 --- a/cmdb-ui/src/modules/cmdb/views/custom_dashboard/chart.vue +++ b/cmdb-ui/src/modules/cmdb/views/custom_dashboard/chart.vue @@ -1,11 +1,55 @@ @@ -127,12 +168,14 @@ export default { }, } }, - mounted() { - this.getLayout() + created() { getCITypes().then((res) => { this.ci_types = res.ci_types }) }, + mounted() { + this.getLayout() + }, methods: { async getLayout() { const res = await getCustomDashboard() @@ -196,6 +239,13 @@ export default { }) } }, + getCiType(item) { + if (item.type_id || item.options?.type_ids) { + const _find = this.ci_types.find((type) => type.id === item.type_id || type.id === item.options?.type_ids[0]) + return _find || null + } + return null + }, }, } @@ -206,15 +256,18 @@ export default { text-align: center; } .cmdb-dashboard-grid-item { - border-radius: 15px; + border-radius: 8px; + padding: 6px 12px; .cmdb-dashboard-grid-item-title { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; font-weight: 700; - padding-left: 6px; - color: #000000bd; + color: #000000; } .cmdb-dashboard-grid-item-operation { position: absolute; - right: 6px; + right: 12px; top: 6px; } .cmdb-dashboard-grid-item-chart-type { @@ -224,3 +277,26 @@ export default { } } + + diff --git a/docker-compose.yml b/docker-compose.yml index 6f66afd..58547c6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -30,7 +30,7 @@ services: - redis cmdb-api: - image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-api:2.3.2 + image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-api:2.3.3 # build: # context: . # target: cmdb-api @@ -61,7 +61,7 @@ services: - cmdb-api cmdb-ui: - image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-ui:2.3.2 + image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-ui:2.3.3 # build: # context: . # target: cmdb-ui