diff --git a/cmdb-api/Pipfile b/cmdb-api/Pipfile index 1639b51..458e424 100644 --- a/cmdb-api/Pipfile +++ b/cmdb-api/Pipfile @@ -44,11 +44,12 @@ treelib = "==1.6.1" flasgger = "==0.9.5" Pillow = "==9.3.0" # other -six = "==1.12.0" +six = "==1.16.0" bs4 = ">=0.0.1" toposort = ">=1.5" requests = ">=2.22.0" requests_oauthlib = "==1.3.1" +markdownify = "==0.11.6" 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 6d41d98..a191c00 100644 --- a/cmdb-api/api/commands/click_cmdb.py +++ b/cmdb-api/api/commands/click_cmdb.py @@ -15,7 +15,6 @@ import api.lib.cmdb.ci from api.extensions import db from api.extensions import rd from api.lib.cmdb.cache import AttributeCache -from api.lib.cmdb.ci_type import CITypeTriggerManager 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 @@ -24,8 +23,8 @@ from api.lib.cmdb.const import RoleEnum 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.acl import UserCache 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 @@ -227,50 +226,60 @@ def cmdb_counter(): @with_appcontext def cmdb_trigger(): """ - Trigger execution + Trigger execution for date attribute """ + from api.lib.cmdb.ci import CITriggerManager + current_day = datetime.datetime.today().strftime("%Y-%m-%d") trigger2cis = dict() trigger2completed = dict() i = 0 while True: - db.session.remove() - if datetime.datetime.today().strftime("%Y-%m-%d") != current_day: - trigger2cis = dict() - trigger2completed = dict() - current_day = datetime.datetime.today().strftime("%Y-%m-%d") + try: + db.session.remove() - if i == 360 or i == 0: - i = 0 - try: - triggers = CITypeTrigger.get_by(to_dict=False) + if datetime.datetime.today().strftime("%Y-%m-%d") != current_day: + trigger2cis = dict() + trigger2completed = dict() + current_day = datetime.datetime.today().strftime("%Y-%m-%d") + if i == 3 or i == 0: + i = 0 + triggers = CITypeTrigger.get_by(to_dict=False, __func_isnot__key_attr_id=None) for trigger in triggers: - ready_cis = CITypeTriggerManager.waiting_cis(trigger) + try: + ready_cis = CITriggerManager.waiting_cis(trigger) + except Exception as e: + print(e) + continue + if trigger.id not in trigger2cis: trigger2cis[trigger.id] = (trigger, ready_cis) else: cur = trigger2cis[trigger.id] cur_ci_ids = {i.ci_id for i in cur[1]} - trigger2cis[trigger.id] = (trigger, cur[1] + [i for i in ready_cis if i.ci_id not in cur_ci_ids - and i.ci_id not in trigger2completed[trigger.id]]) + trigger2cis[trigger.id] = ( + trigger, cur[1] + [i for i in ready_cis if i.ci_id not in cur_ci_ids + and i.ci_id not in trigger2completed.get(trigger.id, {})]) - except Exception as e: - print(e) + for tid in trigger2cis: + trigger, cis = trigger2cis[tid] + for ci in copy.deepcopy(cis): + if CITriggerManager.trigger_notify(trigger, ci): + trigger2completed.setdefault(trigger.id, set()).add(ci.ci_id) - for tid in trigger2cis: - trigger, cis = trigger2cis[tid] - for ci in copy.deepcopy(cis): - if CITypeTriggerManager.trigger_notify(trigger, ci): - trigger2completed.setdefault(trigger.id, set()).add(ci.ci_id) + for _ci in cis: + if _ci.ci_id == ci.ci_id: + cis.remove(_ci) - for _ci in cis: - if _ci.ci_id == ci.ci_id: - cis.remove(_ci) - - i += 1 - time.sleep(10) + i += 1 + time.sleep(10) + except Exception as e: + import traceback + print(traceback.format_exc()) + current_app.logger.error("cmdb trigger exception: {}".format(e)) + time.sleep(60) @click.command() 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.py b/cmdb-api/api/lib/cmdb/ci.py index 39fee78..3f5adf1 100644 --- a/cmdb-api/api/lib/cmdb/ci.py +++ b/cmdb-api/api/lib/cmdb/ci.py @@ -54,6 +54,7 @@ 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_delete_trigger from api.tasks.cmdb import ci_relation_add from api.tasks.cmdb import ci_relation_cache from api.tasks.cmdb import ci_relation_delete @@ -464,6 +465,17 @@ class CIManager(object): ci_dict = cls.get_cis_by_ids([ci_id]) ci_dict = ci_dict and ci_dict[0] + triggers = CITriggerManager.get(ci_dict['_type']) + for trigger in triggers: + option = trigger['option'] + if not option.get('enable') or option.get('action') != OperateType.DELETE: + continue + + if option.get('filter') and not CITriggerManager.ci_filter(ci_dict.get('_id'), option['filter']): + continue + + ci_delete_trigger.apply_async(args=(trigger, OperateType.DELETE, ci_dict), queue=CMDB_QUEUE) + attrs = CITypeAttribute.get_by(type_id=ci.type_id, to_dict=False) attr_names = set([AttributeCache.get(attr.attr_id).name for attr in attrs]) for attr_name in attr_names: @@ -486,9 +498,9 @@ class CIManager(object): db.session.commit() - record_id = AttributeHistoryManger.add(None, ci_id, [(None, OperateType.DELETE, ci_dict, None)], ci.type_id) + AttributeHistoryManger.add(None, ci_id, [(None, OperateType.DELETE, ci_dict, None)], ci.type_id) - ci_delete.apply_async(args=(ci_dict, OperateType.DELETE, record_id), queue=CMDB_QUEUE) + ci_delete.apply_async(args=(ci_id,), queue=CMDB_QUEUE) return ci_id @@ -910,56 +922,89 @@ class CIRelationManager(object): class CITriggerManager(object): @staticmethod def get(type_id): - return CITypeTrigger.get_by(type_id=type_id, to_dict=False) + db.session.remove() + return CITypeTrigger.get_by(type_id=type_id, to_dict=True) @staticmethod - def _exec_webhook(operate_type, webhook, ci_dict, trigger_id, record_id): - try: - response = webhook_request(webhook, ci_dict).text + def _update_old_attr_value(record_id, ci_dict): + attr_history = AttributeHistory.get_by(record_id=record_id, to_dict=False) + attr_dict = dict() + for attr_h in attr_history: + attr_dict['old_{}'.format(AttributeCache.get(attr_h.attr_id).name)] = attr_h.old + + ci_dict.update({'old_{}'.format(k): ci_dict[k] for k in ci_dict}) + + ci_dict.update(attr_dict) + + @classmethod + def _exec_webhook(cls, operate_type, webhook, ci_dict, trigger_id, trigger_name, record_id, ci_id=None, app=None): + app = app or current_app + + with app.app_context(): + if operate_type == OperateType.UPDATE: + cls._update_old_attr_value(record_id, ci_dict) + + 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 = 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, + trigger_name, + is_ok=is_ok, + webhook=response) + + return is_ok + + @classmethod + def _exec_notify(cls, operate_type, notify, ci_dict, trigger_id, trigger_name, record_id, ci_id=None, app=None): + app = app or current_app + + with app.app_context(): + + if ci_id is not None: + ci_dict = CIManager().get_ci_by_id_from_db(ci_id, need_children=False, use_master=False) + + if operate_type == OperateType.UPDATE: + cls._update_old_attr_value(record_id, ci_dict) + is_ok = True - except Exception as e: - current_app.logger.warning("exec webhook failed: {}".format(e)) - response = e - is_ok = False + response = '' + for method in (notify.get('method') or []): + try: + res = notify_send(notify.get('subject'), notify.get('body'), [method], + notify.get('tos'), ci_dict) + response = "{}\n{}".format(response, res) + except Exception as e: + current_app.logger.warning("send notify failed: {}".format(e)) + response = "{}\n{}".format(response, e) + is_ok = False - CITriggerHistoryManager.add(operate_type, - record_id, - ci_dict.get('_id'), - trigger_id, - is_ok=is_ok, - webhook=response) + CITriggerHistoryManager.add(operate_type, + record_id, + ci_dict.get('_id'), + trigger_id, + trigger_name, + is_ok=is_ok, + notify=response.strip()) - 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 + 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) + query = "{},_id:{}".format(other_filter, ci_id) try: _, _, _, _, numfound, _ = search(query).search() @@ -973,28 +1018,40 @@ class CITriggerManager(object): triggers = cls.get(type_id) or [] for trigger in triggers: - if not trigger.option.get('enable'): + option = trigger['option'] + if not option.get('enable'): continue - if trigger.option.get('filter') and not cls.ci_filter(ci_dict.get('_id'), trigger.option['filter']): + if option.get('filter') and not cls.ci_filter(ci_dict.get('_id'), option['filter']): continue - if trigger.option.get('attr_ids') and isinstance(trigger.option['attr_ids'], list): - if not (set(trigger.option['attr_ids']) & + if option.get('attr_ids') and isinstance(option['attr_ids'], list): + if not (set(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) + if option.get('action') == operate_type: + cls.fire_by_trigger(trigger, operate_type, ci_dict, record_id) + + @classmethod + def fire_by_trigger(cls, trigger, operate_type, ci_dict, record_id=None): + option = trigger['option'] + + if option.get('webhooks'): + cls._exec_webhook(operate_type, option['webhooks'], ci_dict, trigger['id'], + option.get('name'), record_id) + + elif option.get('notifies'): + cls._exec_notify(operate_type, option['notifies'], ci_dict, trigger['id'], + option.get('name'), 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)) + config = trigger.option.get('notifies') or {} + + delta_time = datetime.timedelta(days=(config.get('before_days', 0) or 0)) attr = AttributeCache.get(trigger.attr_id) @@ -1022,10 +1079,17 @@ class CITriggerManager(object): :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() + if (trigger.option.get('notifies', {}).get('notify_at') == datetime.datetime.now().strftime("%H:%M") or + not trigger.option.get('notifies', {}).get('notify_at')): + + if trigger.option.get('webhooks'): + threading.Thread(target=cls._exec_webhook, args=( + None, trigger.option['webhooks'], None, trigger.id, trigger.option.get('name'), None, ci.ci_id, + current_app._get_current_object())).start() + elif trigger.option.get('notifies'): + threading.Thread(target=cls._exec_notify, args=( + None, trigger.option['notifies'], None, trigger.id, trigger.option.get('name'), None, ci.ci_id, + current_app._get_current_object())).start() return True diff --git a/cmdb-api/api/lib/cmdb/ci_type.py b/cmdb-api/api/lib/cmdb/ci_type.py index 38e091b..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']) @@ -1187,12 +1198,12 @@ class CITypeTriggerManager(object): return trigger.to_dict() @staticmethod - def update(_id, option): + def update(_id, attr_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(option=option) + new = existed.update(attr_id=attr_id or None, option=option, filter_none=False) CITypeHistoryManager.add(CITypeOperateType.UPDATE_TRIGGER, existed.type_id, diff --git a/cmdb-api/api/lib/cmdb/history.py b/cmdb-api/api/lib/cmdb/history.py index bb4537f..6d1f542 100644 --- a/cmdb-api/api/lib/cmdb/history.py +++ b/cmdb-api/api/lib/cmdb/history.py @@ -293,13 +293,13 @@ 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: + if type_id: 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: + if operate_type: query = query.filter(CITriggerHistory.operate_type == operate_type) numfound = query.count() @@ -316,22 +316,22 @@ class CITriggerHistoryManager(object): @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( + res = db.session.query(CITriggerHistory, CITypeTrigger).join( + CITypeTrigger, CITypeTrigger.id == CITriggerHistory.trigger_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, + trigger_id=hist.trigger_id, + trigger_name=hist.trigger_name, webhook=hist.webhook, - created_at=record.created_at.strftime('%Y-%m-%d %H:%M:%S'), - record_id=record.id, + created_at=hist.created_at.strftime('%Y-%m-%d %H:%M:%S'), + record_id=hist.record_id, hid=hist.id ) if i.CITypeTrigger.id not in id2trigger: @@ -342,12 +342,13 @@ class CITriggerHistoryManager(object): return dict(items=result, id2trigger=id2trigger) @staticmethod - def add(operate_type, record_id, ci_id, trigger_id, is_ok=False, notify=None, webhook=None): + def add(operate_type, record_id, ci_id, trigger_id, trigger_name, 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, + trigger_name=trigger_name, is_ok=is_ok, notify=notify, webhook=webhook) 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/common_setting/employee.py b/cmdb-api/api/lib/common_setting/employee.py index 72898f7..bbd3cb6 100644 --- a/cmdb-api/api/lib/common_setting/employee.py +++ b/cmdb-api/api/lib/common_setting/employee.py @@ -474,6 +474,29 @@ class EmployeeCRUD(object): return [r.to_dict() for r in results] + @staticmethod + def get_employee_notice_by_ids(employee_ids): + criterion = [ + Employee.employee_id.in_(employee_ids), + Employee.deleted == 0, + ] + direct_columns = ['email', 'mobile'] + employees = Employee.query.filter( + *criterion + ).all() + results = [] + for employee in employees: + d = employee.to_dict() + tmp = dict( + employee_id=employee.employee_id, + ) + for column in direct_columns: + tmp[column] = d.get(column, '') + notice_info = d.get('notice_info', {}) + tmp.update(**notice_info) + results.append(tmp) + return results + def get_user_map(key='uid', acl=None): """ diff --git a/cmdb-api/api/lib/notify.py b/cmdb-api/api/lib/notify.py index c32087b..5ee8894 100644 --- a/cmdb-api/api/lib/notify.py +++ b/cmdb-api/api/lib/notify.py @@ -3,25 +3,43 @@ import json import requests +import six from flask import current_app from jinja2 import Template +from markdownify import markdownify as md from api.lib.mail import send_mail -def _request_messenger(subject, body, tos, sender): +def _request_messenger(subject, body, tos, sender, payload): 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") + 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' params['content'] = body else: - params['msgtype'] = 'text' - params['content'] = json.dumps(dict(content=subject or body)) + params['msgtype'] = 'markdown' + try: + content = md("{}\n{}".format(subject or '', body or '')) + except Exception as e: + current_app.logger.warning("html2markdown failed: {}".format(e)) + content = "{}\n{}".format(subject or '', body or '') + + params['content'] = json.dumps(dict(content=content)) resp = requests.post(current_app.config.get('MESSENGER_URL'), json=params) if resp.status_code != 200: @@ -32,14 +50,15 @@ def _request_messenger(subject, body, tos, sender): def notify_send(subject, body, methods, tos, payload=None): payload = payload or {} + payload = {k: v or '' for k, v in payload.items()} 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) + send_mail(None, [Template(to.get('email')).render(payload) for to in tos], subject, body) - res += _request_messenger(subject, body, tos, method) + "\n" + res += (_request_messenger(subject, body, tos, method, payload) + "\n") return res diff --git a/cmdb-api/api/lib/perm/acl/role.py b/cmdb-api/api/lib/perm/acl/role.py index b965fd4..0c2a28c 100644 --- a/cmdb-api/api/lib/perm/acl/role.py +++ b/cmdb-api/api/lib/perm/acl/role.py @@ -10,9 +10,7 @@ from sqlalchemy import or_ from api.extensions import db from api.lib.perm.acl.app import AppCRUD -from api.lib.perm.acl.audit import AuditCRUD -from api.lib.perm.acl.audit import AuditOperateType -from api.lib.perm.acl.audit import AuditScope +from api.lib.perm.acl.audit import AuditCRUD, AuditOperateType, AuditScope from api.lib.perm.acl.cache import AppCache from api.lib.perm.acl.cache import HasResourceRoleCache from api.lib.perm.acl.cache import RoleCache @@ -71,16 +69,16 @@ class RoleRelationCRUD(object): @staticmethod def get_parent_ids(rid, app_id): if app_id is not None: - return ([i.parent_id for i in RoleRelation.get_by(child_id=rid, app_id=app_id, to_dict=False)] + - [i.parent_id for i in RoleRelation.get_by(child_id=rid, app_id=None, to_dict=False)]) + return [i.parent_id for i in RoleRelation.get_by(child_id=rid, app_id=app_id, to_dict=False)] + \ + [i.parent_id for i in RoleRelation.get_by(child_id=rid, app_id=None, to_dict=False)] else: return [i.parent_id for i in RoleRelation.get_by(child_id=rid, app_id=app_id, to_dict=False)] @staticmethod def get_child_ids(rid, app_id): if app_id is not None: - return ([i.child_id for i in RoleRelation.get_by(parent_id=rid, app_id=app_id, to_dict=False)] + - [i.child_id for i in RoleRelation.get_by(parent_id=rid, app_id=None, to_dict=False)]) + return [i.child_id for i in RoleRelation.get_by(parent_id=rid, app_id=app_id, to_dict=False)] + \ + [i.child_id for i in RoleRelation.get_by(parent_id=rid, app_id=None, to_dict=False)] else: return [i.child_id for i in RoleRelation.get_by(parent_id=rid, app_id=app_id, to_dict=False)] @@ -215,7 +213,6 @@ class RoleCRUD(object): @staticmethod def search(q, app_id, page=1, page_size=None, user_role=True, is_all=False, user_only=False): - if user_only: # only user role query = db.session.query(Role).filter(Role.deleted.is_(False)).filter(Role.uid.isnot(None)) @@ -273,6 +270,13 @@ class RoleCRUD(object): RoleCache.clean(rid) role = role.update(**kwargs) + + if origin['uid'] and kwargs.get('name') and kwargs.get('name') != origin['name']: + from api.models.acl import User + user = User.get_by(uid=origin['uid'], first=True, to_dict=False) + if user: + user.update(username=kwargs['name']) + AuditCRUD.add_role_log(role.app_id, AuditOperateType.update, AuditScope.role, role.id, origin, role.to_dict(), {}, ) @@ -291,12 +295,11 @@ class RoleCRUD(object): from api.lib.perm.acl.acl import is_admin role = Role.get_by_id(rid) or abort(404, ErrFormat.role_not_found.format("rid={}".format(rid))) - - not force and role.uid and abort(400, ErrFormat.user_role_delete_invalid) - if not role.app_id and not is_admin(): return abort(403, ErrFormat.admin_required) + not force and role.uid and abort(400, ErrFormat.user_role_delete_invalid) + origin = role.to_dict() child_ids = [] @@ -305,20 +308,18 @@ class RoleCRUD(object): for i in RoleRelation.get_by(parent_id=rid, to_dict=False): child_ids.append(i.child_id) - i.soft_delete(commit=False) + i.soft_delete() for i in RoleRelation.get_by(child_id=rid, to_dict=False): parent_ids.append(i.parent_id) - i.soft_delete(commit=False) + i.soft_delete() role_permissions = [] for i in RolePermission.get_by(rid=rid, to_dict=False): role_permissions.append(i.to_dict()) - i.soft_delete(commit=False) + i.soft_delete() - role.soft_delete(commit=False) - - db.session.commit() + role.soft_delete() role_rebuild.apply_async(args=(recursive_child_ids, role.app_id), queue=ACL_QUEUE) diff --git a/cmdb-api/api/lib/webhook.py b/cmdb-api/api/lib/webhook.py index 280be18..a5133e0 100644 --- a/cmdb-api/api/lib/webhook.py +++ b/cmdb-api/api/lib/webhook.py @@ -82,12 +82,16 @@ def webhook_request(webhook, payload): """ assert webhook.get('url') is not None + payload = {k: v or '' for k, v in payload.items()} + 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)) + headers = json.loads(Template(json.dumps(webhook.get('headers') or {})).render(payload)) + data = Template(json.dumps(webhook.get('body', ''))).render(payload) auth = _wrap_auth(**webhook.get('authorization', {})) @@ -99,7 +103,7 @@ def webhook_request(webhook, payload): return request( url, params=params, - headers=webhook.get('headers') or None, + headers=headers or None, data=data, auth=auth ) diff --git a/cmdb-api/api/models/cmdb.py b/cmdb-api/api/models/cmdb.py index 6f70fec..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) @@ -139,6 +140,7 @@ class CITriggerHistory(Model): 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")) + trigger_name = db.Column(db.String(64)) is_ok = db.Column(db.Boolean, default=False) notify = db.Column(db.Text) webhook = db.Column(db.Text) diff --git a/cmdb-api/api/tasks/cmdb.py b/cmdb-api/api/tasks/cmdb.py index 34aa315..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) @@ -65,10 +66,7 @@ def batch_ci_cache(ci_ids, ): # only for attribute change index @celery.task(name="cmdb.ci_delete", queue=CMDB_QUEUE) -def ci_delete(ci_dict, operate_type, record_id): - from api.lib.cmdb.ci import CITriggerManager - - ci_id = ci_dict.get('_id') +def ci_delete(ci_id): current_app.logger.info(ci_id) if current_app.config.get("USE_ES"): @@ -78,10 +76,16 @@ def ci_delete(ci_dict, operate_type, record_id): current_app.logger.info("{0} delete..........".format(ci_id)) + +@celery.task(name="cmdb.ci_delete_trigger", queue=CMDB_QUEUE) +def ci_delete_trigger(trigger, operate_type, ci_dict): + current_app.logger.info('delete ci {} trigger'.format(ci_dict['_id'])) + from api.lib.cmdb.ci import CITriggerManager + current_app.test_request_context().push() login_user(UserCache.get('worker')) - CITriggerManager.fire(operate_type, ci_dict, record_id) + CITriggerManager.fire_by_trigger(trigger, operate_type, ci_dict) @celery.task(name="cmdb.ci_relation_cache", queue=CMDB_QUEUE) @@ -161,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() @@ -170,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 df1ed8e..4e02d40 100644 --- a/cmdb-api/api/views/cmdb/ci_type.py +++ b/cmdb-api/api/views/cmdb/ci_type.py @@ -432,8 +432,9 @@ class CITypeTriggerView(APIView): assert type_id is not None option = request.values.get('option') + attr_id = request.values.get('attr_id') - return self.jsonify(CITypeTriggerManager().update(_id, option)) + return self.jsonify(CITypeTriggerManager().update(_id, attr_id, option)) @has_perm_from_args("type_id", ResourceTypeEnum.CI, PermEnum.CONFIG, CITypeManager.get_name_by_id) def delete(self, type_id, _id): @@ -505,4 +506,3 @@ 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/common_setting/employee.py b/cmdb-api/api/views/common_setting/employee.py index 56adf8b..ce5578b 100644 --- a/cmdb-api/api/views/common_setting/employee.py +++ b/cmdb-api/api/views/common_setting/employee.py @@ -145,3 +145,14 @@ class EmployeePositionView(APIView): result = EmployeeCRUD.get_all_position() return self.jsonify(result) + +class GetEmployeeNoticeByIds(APIView): + url_prefix = (f'{prefix}/get_notice_by_ids',) + + def post(self): + employee_ids = request.json.get('employee_ids', []) + if not employee_ids: + result = [] + else: + result = EmployeeCRUD.get_employee_notice_by_ids(employee_ids) + return self.jsonify(result) diff --git a/cmdb-api/requirements.txt b/cmdb-api/requirements.txt index 6e8a788..d98316e 100644 --- a/cmdb-api/requirements.txt +++ b/cmdb-api/requirements.txt @@ -37,11 +37,12 @@ PyYAML==6.0 redis==4.6.0 requests==2.31.0 requests_oauthlib==1.3.1 -six==1.12.0 +markdownify==0.11.6 +six==1.16.0 SQLAlchemy==1.4.49 supervisor==4.0.3 timeout-decorator==0.5.0 toposort==1.10 treelib==1.6.1 Werkzeug==2.3.6 -WTForms==3.0.0 \ No newline at end of file +WTForms==3.0.0 diff --git a/cmdb-api/settings.example.py b/cmdb-api/settings.example.py index 0102322..26f2d8f 100644 --- a/cmdb-api/settings.example.py +++ b/cmdb-api/settings.example.py @@ -94,3 +94,7 @@ ES_HOST = '127.0.0.1' USE_ES = False BOOL_TRUE = ['true', 'TRUE', 'True', True, '1', 1, "Yes", "YES", "yes", 'Y', 'y'] + +# # messenger +USE_MESSENGER = True +MESSENGER_URL = "http://{messenger_url}/v1/message" diff --git a/cmdb-ui/yarn.lock b/cmdb-ui/yarn.lock index 5602f0f..aab4c2c 100644 --- a/cmdb-ui/yarn.lock +++ b/cmdb-ui/yarn.lock @@ -1214,6 +1214,13 @@ dependencies: regenerator-runtime "^0.13.11" +"@babel/runtime@^7.12.0": + version "7.23.1" + resolved "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.23.1.tgz#72741dc4d413338a91dcb044a86f3c0bc402646d" + integrity sha512-hC2v6p8ZSI/W0HUzh3V8C5g+NwSKzKPtJwSpTjwl0o297GP9+ZLQSkdvHz46CM3LqyoXxq+5G9komY+eSqSO0g== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/template@7.0.0-beta.44": version "7.0.0-beta.44" resolved "https://mirrors.huaweicloud.com/repository/npm/@babel/template/-/template-7.0.0-beta.44.tgz#f8832f4fdcee5d59bf515e595fc5106c529b394f" @@ -1723,6 +1730,11 @@ resolved "https://mirrors.huaweicloud.com/repository/npm/@soda/get-current-script/-/get-current-script-1.0.2.tgz#a53515db25d8038374381b73af20bb4f2e508d87" integrity sha512-T7VNNlYVM1SgQ+VsMYhnDkcGmWhQdL0bDyGm5TlQ3GBXnJscEClUUOKduWTmm2zCnvNLC1hc3JpuXjs/nFOc5w== +"@transloadit/prettier-bytes@0.0.7": + version "0.0.7" + resolved "https://registry.npmmirror.com/@transloadit/prettier-bytes/-/prettier-bytes-0.0.7.tgz#cdb5399f445fdd606ed833872fa0cabdbc51686b" + integrity sha512-VeJbUb0wEKbcwaSlj5n+LscBl9IPgLPkHVGBkh00cztv6X4L/TJXK58LzFuBKX7/GAfiGhIwH67YTLTlzvIzBA== + "@types/babel__core@^7.1.0": version "7.20.1" resolved "https://mirrors.huaweicloud.com/repository/npm/@types/babel__core/-/babel__core-7.20.1.tgz#916ecea274b0c776fec721e333e55762d3a9614b" @@ -1805,6 +1817,11 @@ resolved "https://mirrors.huaweicloud.com/repository/npm/@types/estree/-/estree-1.0.1.tgz#aa22750962f3bf0e79d753d3cc067f010c95f194" integrity sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA== +"@types/event-emitter@^0.3.3": + version "0.3.3" + resolved "https://registry.npmmirror.com/@types/event-emitter/-/event-emitter-0.3.3.tgz#727032a9fc67565f96bbd78b2e2809275c97d7e7" + integrity sha512-UfnOK1pIxO7P+EgPRZXD9jMpimd8QEFcEZ5R67R1UhGbv4zghU5+NE7U8M8G9H5Jc8FI51rqDWQs6FtUfq2e/Q== + "@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.33": version "4.17.35" resolved "https://mirrors.huaweicloud.com/repository/npm/@types/express-serve-static-core/-/express-serve-static-core-4.17.35.tgz#c95dd4424f0d32e525d23812aa8ab8e4d3906c4f" @@ -2057,6 +2074,49 @@ dependencies: "@types/yargs-parser" "*" +"@uppy/companion-client@^2.2.2": + version "2.2.2" + resolved "https://registry.npmmirror.com/@uppy/companion-client/-/companion-client-2.2.2.tgz#c70b42fdcca728ef88b3eebf7ee3e2fa04b4923b" + integrity sha512-5mTp2iq97/mYSisMaBtFRry6PTgZA6SIL7LePteOV5x0/DxKfrZW3DEiQERJmYpHzy7k8johpm2gHnEKto56Og== + dependencies: + "@uppy/utils" "^4.1.2" + namespace-emitter "^2.0.1" + +"@uppy/core@^2.1.1": + version "2.3.4" + resolved "https://registry.npmmirror.com/@uppy/core/-/core-2.3.4.tgz#260b85b6bf3aa03cdc67da231f8c69cfbfdcc84a" + integrity sha512-iWAqppC8FD8mMVqewavCz+TNaet6HPXitmGXpGGREGrakZ4FeuWytVdrelydzTdXx6vVKkOmI2FLztGg73sENQ== + dependencies: + "@transloadit/prettier-bytes" "0.0.7" + "@uppy/store-default" "^2.1.1" + "@uppy/utils" "^4.1.3" + lodash.throttle "^4.1.1" + mime-match "^1.0.2" + namespace-emitter "^2.0.1" + nanoid "^3.1.25" + preact "^10.5.13" + +"@uppy/store-default@^2.1.1": + version "2.1.1" + resolved "https://registry.npmmirror.com/@uppy/store-default/-/store-default-2.1.1.tgz#62a656a099bdaa012306e054d093754cb2d36e3e" + integrity sha512-xnpTxvot2SeAwGwbvmJ899ASk5tYXhmZzD/aCFsXePh/v8rNvR2pKlcQUH7cF/y4baUGq3FHO/daKCok/mpKqQ== + +"@uppy/utils@^4.1.2", "@uppy/utils@^4.1.3": + version "4.1.3" + resolved "https://registry.npmmirror.com/@uppy/utils/-/utils-4.1.3.tgz#9d0be6ece4df25f228d30ef40be0f14208258ce3" + integrity sha512-nTuMvwWYobnJcytDO3t+D6IkVq/Qs4Xv3vyoEZ+Iaf8gegZP+rEyoaFT2CK5XLRMienPyqRqNbIfRuFaOWSIFw== + dependencies: + lodash.throttle "^4.1.1" + +"@uppy/xhr-upload@^2.0.3": + version "2.1.3" + resolved "https://registry.npmmirror.com/@uppy/xhr-upload/-/xhr-upload-2.1.3.tgz#0d4e355332fe0c6eb372d7731315e04d02aeeb18" + integrity sha512-YWOQ6myBVPs+mhNjfdWsQyMRWUlrDLMoaG7nvf/G6Y3GKZf8AyjFDjvvJ49XWQ+DaZOftGkHmF1uh/DBeGivJQ== + dependencies: + "@uppy/companion-client" "^2.2.2" + "@uppy/utils" "^4.1.2" + nanoid "^3.1.25" + "@vue/babel-helper-vue-jsx-merge-props@^1.4.0": version "1.4.0" resolved "https://mirrors.huaweicloud.com/repository/npm/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.4.0.tgz#8d53a1e21347db8edbe54d339902583176de09f2" @@ -2383,6 +2443,84 @@ resolved "https://mirrors.huaweicloud.com/repository/npm/@vue/web-component-wrapper/-/web-component-wrapper-1.3.0.tgz#b6b40a7625429d2bd7c2281ddba601ed05dc7f1a" integrity sha512-Iu8Tbg3f+emIIMmI2ycSI8QcEuAUgPTgHwesDU1eKMLE4YC/c/sFbGc70QgMq31ijRftV0R7vCm9co6rldCeOA== +"@wangeditor/basic-modules@^1.1.7": + version "1.1.7" + resolved "https://registry.npmmirror.com/@wangeditor/basic-modules/-/basic-modules-1.1.7.tgz#a9c3ccf4ef53332f29550d59d3676e15f395946f" + integrity sha512-cY9CPkLJaqF05STqfpZKWG4LpxTMeGSIIF1fHvfm/mz+JXatCagjdkbxdikOuKYlxDdeqvOeBmsUBItufDLXZg== + dependencies: + is-url "^1.2.4" + +"@wangeditor/code-highlight@^1.0.3": + version "1.0.3" + resolved "https://registry.npmmirror.com/@wangeditor/code-highlight/-/code-highlight-1.0.3.tgz#90256857714d5c0cf83ac475aea64db7bf29a7cd" + integrity sha512-iazHwO14XpCuIWJNTQTikqUhGKyqj+dUNWJ9288Oym9M2xMVHvnsOmDU2sgUDWVy+pOLojReMPgXCsvvNlOOhw== + dependencies: + prismjs "^1.23.0" + +"@wangeditor/core@^1.1.19": + version "1.1.19" + resolved "https://registry.npmmirror.com/@wangeditor/core/-/core-1.1.19.tgz#f9155f7fd92d03cb1982405b3b82e54c31f1c2b0" + integrity sha512-KevkB47+7GhVszyYF2pKGKtCSj/YzmClsD03C3zTt+9SR2XWT5T0e3yQqg8baZpcMvkjs1D8Dv4fk8ok/UaS2Q== + dependencies: + "@types/event-emitter" "^0.3.3" + event-emitter "^0.3.5" + html-void-elements "^2.0.0" + i18next "^20.4.0" + scroll-into-view-if-needed "^2.2.28" + slate-history "^0.66.0" + +"@wangeditor/editor-for-vue@^1.0.0": + version "1.0.2" + resolved "https://registry.npmmirror.com/@wangeditor/editor-for-vue/-/editor-for-vue-1.0.2.tgz#62674d56354319ff8dcc83db5c62cec4437ee906" + integrity sha512-BOENvAXJVtVXlE2X50AAvjV82YlCUeu5cbeR0cvEQHQjYtiVnJtq7HSoj85r2kTgGouI5OrpJG9BBEjSjUSPyA== + +"@wangeditor/editor@^5.1.23": + version "5.1.23" + resolved "https://registry.npmmirror.com/@wangeditor/editor/-/editor-5.1.23.tgz#c9d2007b7cb0ceef6b72692b4ee87b01ee2367b3" + integrity sha512-0RxfeVTuK1tktUaPROnCoFfaHVJpRAIE2zdS0mpP+vq1axVQpLjM8+fCvKzqYIkH0Pg+C+44hJpe3VVroSkEuQ== + dependencies: + "@uppy/core" "^2.1.1" + "@uppy/xhr-upload" "^2.0.3" + "@wangeditor/basic-modules" "^1.1.7" + "@wangeditor/code-highlight" "^1.0.3" + "@wangeditor/core" "^1.1.19" + "@wangeditor/list-module" "^1.0.5" + "@wangeditor/table-module" "^1.1.4" + "@wangeditor/upload-image-module" "^1.0.2" + "@wangeditor/video-module" "^1.1.4" + dom7 "^3.0.0" + is-hotkey "^0.2.0" + lodash.camelcase "^4.3.0" + lodash.clonedeep "^4.5.0" + lodash.debounce "^4.0.8" + lodash.foreach "^4.5.0" + lodash.isequal "^4.5.0" + lodash.throttle "^4.1.1" + lodash.toarray "^4.4.0" + nanoid "^3.2.0" + slate "^0.72.0" + snabbdom "^3.1.0" + +"@wangeditor/list-module@^1.0.5": + version "1.0.5" + resolved "https://registry.npmmirror.com/@wangeditor/list-module/-/list-module-1.0.5.tgz#3fc0b167acddf885536b45fa0c127f9c6adaea33" + integrity sha512-uDuYTP6DVhcYf7mF1pTlmNn5jOb4QtcVhYwSSAkyg09zqxI1qBqsfUnveeDeDqIuptSJhkh81cyxi+MF8sEPOQ== + +"@wangeditor/table-module@^1.1.4": + version "1.1.4" + resolved "https://registry.npmmirror.com/@wangeditor/table-module/-/table-module-1.1.4.tgz#757d4a5868b2b658041cd323854a4d707c8347e9" + integrity sha512-5saanU9xuEocxaemGdNi9t8MCDSucnykEC6jtuiT72kt+/Hhh4nERYx1J20OPsTCCdVr7hIyQenFD1iSRkIQ6w== + +"@wangeditor/upload-image-module@^1.0.2": + version "1.0.2" + resolved "https://registry.npmmirror.com/@wangeditor/upload-image-module/-/upload-image-module-1.0.2.tgz#89e9b9467e10cbc6b11dc5748e08dd23aaebee30" + integrity sha512-z81lk/v71OwPDYeQDxj6cVr81aDP90aFuywb8nPD6eQeECtOymrqRODjpO6VGvCVxVck8nUxBHtbxKtjgcwyiA== + +"@wangeditor/video-module@^1.1.4": + version "1.1.4" + resolved "https://registry.npmmirror.com/@wangeditor/video-module/-/video-module-1.1.4.tgz#b9df1b3ab2cd53f678b19b4d927e200774a6f532" + integrity sha512-ZdodDPqKQrgx3IwWu4ZiQmXI8EXZ3hm2/fM6E3t5dB8tCaIGWQZhmqd6P5knfkRAd3z2+YRSRbxOGfoRSp/rLg== + "@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5": version "1.11.6" resolved "https://mirrors.huaweicloud.com/repository/npm/@webassemblyjs/ast/-/ast-1.11.6.tgz#db046555d3c413f8966ca50a95176a0e2c642e24" @@ -4398,6 +4536,11 @@ compression@^1.7.4: safe-buffer "5.1.2" vary "~1.1.2" +compute-scroll-into-view@^1.0.20: + version "1.0.20" + resolved "https://registry.npmmirror.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz#1768b5522d1172754f5d0c9b02de3af6be506a43" + integrity sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg== + concat-map@0.0.1: version "0.0.1" resolved "https://mirrors.huaweicloud.com/repository/npm/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -5060,6 +5203,14 @@ d3-voronoi@^1.1.2: resolved "https://mirrors.huaweicloud.com/repository/npm/d3-voronoi/-/d3-voronoi-1.1.4.tgz#dd3c78d7653d2bb359284ae478645d95944c8297" integrity sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg== +d@1, d@^1.0.1: + version "1.0.1" + resolved "https://registry.npmmirror.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a" + integrity sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA== + dependencies: + es5-ext "^0.10.50" + type "^1.0.1" + dagre@^0.8.2, dagre@~0.8.5: version "0.8.5" resolved "https://mirrors.huaweicloud.com/repository/npm/dagre/-/dagre-0.8.5.tgz#ba30b0055dac12b6c1fcc247817442777d06afee" @@ -5401,6 +5552,13 @@ dom-to-image@~2.6.0: resolved "https://mirrors.huaweicloud.com/repository/npm/dom-to-image/-/dom-to-image-2.6.0.tgz#8a503608088c87b1c22f9034ae032e1898955867" integrity sha512-Dt0QdaHmLpjURjU7Tnu3AgYSF2LuOmksSGsUcE6ItvJoCWTBEmiMXcqBdNSAm9+QbbwD7JMoVsuuKX6ZVQv1qA== +dom7@^3.0.0: + version "3.0.0" + resolved "https://registry.npmmirror.com/dom7/-/dom7-3.0.0.tgz#b861ce5d67a6becd7aaa3ad02942ff14b1240331" + integrity sha512-oNlcUdHsC4zb7Msx7JN3K0Nro1dzJ48knvBOnDPKJ2GV9wl1i5vydJZUSyOfrkKFDZEud/jBsTk92S/VGSAe/g== + dependencies: + ssr-window "^3.0.0-alpha.1" + domain-browser@^1.1.1: version "1.2.0" resolved "https://mirrors.huaweicloud.com/repository/npm/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" @@ -5743,6 +5901,32 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" +es5-ext@^0.10.35, es5-ext@^0.10.50, es5-ext@~0.10.14: + version "0.10.62" + resolved "https://registry.npmmirror.com/es5-ext/-/es5-ext-0.10.62.tgz#5e6adc19a6da524bf3d1e02bbc8960e5eb49a9a5" + integrity sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA== + dependencies: + es6-iterator "^2.0.3" + es6-symbol "^3.1.3" + next-tick "^1.1.0" + +es6-iterator@^2.0.3: + version "2.0.3" + resolved "https://registry.npmmirror.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" + integrity sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g== + dependencies: + d "1" + es5-ext "^0.10.35" + es6-symbol "^3.1.1" + +es6-symbol@^3.1.1, es6-symbol@^3.1.3: + version "3.1.3" + resolved "https://registry.npmmirror.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18" + integrity sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA== + dependencies: + d "^1.0.1" + ext "^1.1.2" + escalade@^3.1.1: version "3.1.1" resolved "https://mirrors.huaweicloud.com/repository/npm/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" @@ -6007,6 +6191,14 @@ etag@~1.8.1: resolved "https://mirrors.huaweicloud.com/repository/npm/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== +event-emitter@^0.3.5: + version "0.3.5" + resolved "https://registry.npmmirror.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" + integrity sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA== + dependencies: + d "1" + es5-ext "~0.10.14" + event-pubsub@4.3.0: version "4.3.0" resolved "https://mirrors.huaweicloud.com/repository/npm/event-pubsub/-/event-pubsub-4.3.0.tgz#f68d816bc29f1ec02c539dc58c8dd40ce72cb36e" @@ -6183,6 +6375,13 @@ express@^4.16.3, express@^4.17.1: utils-merge "1.0.1" vary "~1.1.2" +ext@^1.1.2: + version "1.7.0" + resolved "https://registry.npmmirror.com/ext/-/ext-1.7.0.tgz#0ea4383c0103d60e70be99e9a7f11027a33c4f5f" + integrity sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw== + dependencies: + type "^2.7.2" + extend-shallow@^2.0.1: version "2.0.1" resolved "https://mirrors.huaweicloud.com/repository/npm/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" @@ -7090,6 +7289,11 @@ html-tags@^3.3.1: resolved "https://mirrors.huaweicloud.com/repository/npm/html-tags/-/html-tags-3.3.1.tgz#a04026a18c882e4bba8a01a3d39cfe465d40b5ce" integrity sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ== +html-void-elements@^2.0.0: + version "2.0.1" + resolved "https://registry.npmmirror.com/html-void-elements/-/html-void-elements-2.0.1.tgz#29459b8b05c200b6c5ee98743c41b979d577549f" + integrity sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A== + html-webpack-plugin@^3.2.0: version "3.2.0" resolved "https://mirrors.huaweicloud.com/repository/npm/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz#b01abbd723acaaa7b37b6af4492ebda03d9dd37b" @@ -7213,6 +7417,13 @@ human-signals@^1.1.1: resolved "https://mirrors.huaweicloud.com/repository/npm/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== +i18next@^20.4.0: + version "20.6.1" + resolved "https://registry.npmmirror.com/i18next/-/i18next-20.6.1.tgz#535e5f6e5baeb685c7d25df70db63bf3cc0aa345" + integrity sha512-yCMYTMEJ9ihCwEQQ3phLo7I/Pwycf8uAx+sRHwwk5U9Aui/IZYgQRyMqXafQOw5QQ7DM1Z+WyEXWIqSuJHhG2A== + dependencies: + "@babel/runtime" "^7.12.0" + iconv-lite@0.4, iconv-lite@0.4.24, iconv-lite@^0.4.24: version "0.4.24" resolved "https://mirrors.huaweicloud.com/repository/npm/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -7262,6 +7473,11 @@ immediate@~3.0.5: resolved "https://mirrors.huaweicloud.com/repository/npm/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== +immer@^9.0.6: + version "9.0.21" + resolved "https://registry.npmmirror.com/immer/-/immer-9.0.21.tgz#1e025ea31a40f24fb064f1fef23e931496330176" + integrity sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA== + import-cwd@^2.0.0: version "2.1.0" resolved "https://mirrors.huaweicloud.com/repository/npm/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9" @@ -7687,6 +7903,11 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" +is-hotkey@^0.2.0: + version "0.2.0" + resolved "https://registry.npmmirror.com/is-hotkey/-/is-hotkey-0.2.0.tgz#1835a68171a91e5c9460869d96336947c8340cef" + integrity sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw== + is-mobile@^2.2.1: version "2.2.2" resolved "https://mirrors.huaweicloud.com/repository/npm/is-mobile/-/is-mobile-2.2.2.tgz#f6c9c5d50ee01254ce05e739bdd835f1ed4e9954" @@ -7769,6 +7990,11 @@ is-plain-object@^2.0.3, is-plain-object@^2.0.4: dependencies: isobject "^3.0.1" +is-plain-object@^5.0.0: + version "5.0.0" + resolved "https://registry.npmmirror.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" + integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== + is-posix-bracket@^0.1.0: version "0.1.1" resolved "https://mirrors.huaweicloud.com/repository/npm/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4" @@ -7844,6 +8070,11 @@ is-typedarray@~1.0.0: resolved "https://mirrors.huaweicloud.com/repository/npm/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== +is-url@^1.2.4: + version "1.2.4" + resolved "https://registry.npmmirror.com/is-url/-/is-url-1.2.4.tgz#04a4df46d28c4cff3d73d01ff06abeb318a1aa52" + integrity sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww== + is-utf8@^0.2.0: version "0.2.1" resolved "https://mirrors.huaweicloud.com/repository/npm/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" @@ -8842,6 +9073,16 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" +lodash.camelcase@^4.3.0: + version "4.3.0" + resolved "https://registry.npmmirror.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" + integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA== + +lodash.clonedeep@^4.5.0: + version "4.5.0" + resolved "https://registry.npmmirror.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" + integrity sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ== + lodash.debounce@^4.0.8: version "4.0.8" resolved "https://mirrors.huaweicloud.com/repository/npm/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" @@ -8872,6 +9113,11 @@ lodash.flatten@^4.4.0: resolved "https://mirrors.huaweicloud.com/repository/npm/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" integrity sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g== +lodash.foreach@^4.5.0: + version "4.5.0" + resolved "https://registry.npmmirror.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz#1a6a35eace401280c7f06dddec35165ab27e3e53" + integrity sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ== + lodash.get@^4.4.2: version "4.4.2" resolved "https://mirrors.huaweicloud.com/repository/npm/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" @@ -8937,6 +9183,16 @@ lodash.sortby@^4.7.0: resolved "https://mirrors.huaweicloud.com/repository/npm/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA== +lodash.throttle@^4.1.1: + version "4.1.1" + resolved "https://registry.npmmirror.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4" + integrity sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ== + +lodash.toarray@^4.4.0: + version "4.4.0" + resolved "https://registry.npmmirror.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz#24c4bfcd6b2fba38bfd0594db1179d8e9b656561" + integrity sha512-QyffEA3i5dma5q2490+SgCvDN0pXLmRGSyAANuVi0HQ01Pkfr9fuoKQW8wm1wGBnJITs/mS7wQvS6VshUEBFCw== + lodash.transform@^4.6.0: version "4.6.0" resolved "https://mirrors.huaweicloud.com/repository/npm/lodash.transform/-/lodash.transform-4.6.0.tgz#12306422f63324aed8483d3f38332b5f670547a0" @@ -9185,6 +9441,13 @@ mime-db@1.52.0, "mime-db@>= 1.43.0 < 2": resolved "https://mirrors.huaweicloud.com/repository/npm/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== +mime-match@^1.0.2: + version "1.0.2" + resolved "https://registry.npmmirror.com/mime-match/-/mime-match-1.0.2.tgz#3f87c31e9af1a5fd485fb9db134428b23bbb7ba8" + integrity sha512-VXp/ugGDVh3eCLOBCiHZMYWQaTNUHv2IJrut+yXA6+JbLPXHglHwfS/5A5L0ll+jkCY7fIzRJcH6OIunF+c6Cg== + dependencies: + wildcard "^1.1.0" + mime-types@^2.1.12, mime-types@^2.1.27, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24, mime-types@~2.1.34: version "2.1.35" resolved "https://mirrors.huaweicloud.com/repository/npm/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" @@ -9389,14 +9652,19 @@ mz@^2.4.0: object-assign "^4.0.1" thenify-all "^1.0.0" +namespace-emitter@^2.0.1: + version "2.0.1" + resolved "https://registry.npmmirror.com/namespace-emitter/-/namespace-emitter-2.0.1.tgz#978d51361c61313b4e6b8cf6f3853d08dfa2b17c" + integrity sha512-N/sMKHniSDJBjfrkbS/tpkPj4RAbvW3mr8UAzvlMHyun93XEm83IAvhWtJVHo+RHn/oO8Job5YN4b+wRjSVp5g== + nan@^2.12.1: version "2.17.0" resolved "https://mirrors.huaweicloud.com/repository/npm/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb" integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ== -nanoid@^3.3.6: +nanoid@^3.1.25, nanoid@^3.2.0, nanoid@^3.3.6: version "3.3.6" - resolved "https://mirrors.huaweicloud.com/repository/npm/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" + resolved "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== nanomatch@^1.2.9: @@ -9441,6 +9709,11 @@ neo-async@^2.5.0, neo-async@^2.6.0, neo-async@^2.6.1, neo-async@^2.6.2: resolved "https://mirrors.huaweicloud.com/repository/npm/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== +next-tick@^1.1.0: + version "1.1.0" + resolved "https://registry.npmmirror.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" + integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== + nice-try@^1.0.4: version "1.0.5" resolved "https://mirrors.huaweicloud.com/repository/npm/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" @@ -10555,6 +10828,11 @@ postcss@^8.4.14: picocolors "^1.0.0" source-map-js "^1.0.2" +preact@^10.5.13: + version "10.17.1" + resolved "https://registry.npmmirror.com/preact/-/preact-10.17.1.tgz#0a1b3c658c019e759326b9648c62912cf5c2dde1" + integrity sha512-X9BODrvQ4Ekwv9GURm9AKAGaomqXmip7NQTZgY7gcNmr7XE83adOMJvd3N42id1tMFU7ojiynRsYnY6/BRFxLA== + prelude-ls@~1.1.2: version "1.1.2" resolved "https://mirrors.huaweicloud.com/repository/npm/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" @@ -10607,6 +10885,11 @@ printj@~1.1.0: resolved "https://mirrors.huaweicloud.com/repository/npm/printj/-/printj-1.1.2.tgz#d90deb2975a8b9f600fb3a1c94e3f4c53c78a222" integrity sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ== +prismjs@^1.23.0: + version "1.29.0" + resolved "https://registry.npmmirror.com/prismjs/-/prismjs-1.29.0.tgz#f113555a8fa9b57c35e637bba27509dcf802dd12" + integrity sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q== + process-nextick-args@~2.0.0: version "2.0.1" resolved "https://mirrors.huaweicloud.com/repository/npm/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" @@ -10921,6 +11204,11 @@ regenerator-runtime@^0.13.11, regenerator-runtime@^0.13.4: resolved "https://mirrors.huaweicloud.com/repository/npm/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== +regenerator-runtime@^0.14.0: + version "0.14.0" + resolved "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45" + integrity sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA== + regenerator-transform@^0.15.1: version "0.15.1" resolved "https://mirrors.huaweicloud.com/repository/npm/regenerator-transform/-/regenerator-transform-0.15.1.tgz#f6c4e99fc1b4591f780db2586328e4d9a9d8dc56" @@ -11355,6 +11643,13 @@ screenfull@^4.2.0: resolved "https://mirrors.huaweicloud.com/repository/npm/screenfull/-/screenfull-4.2.1.tgz#3245b7bc73d2b7c9a15bd8caaf6965db7cbc7f04" integrity sha512-PLSp6f5XdhvjCCCO8OjavRfzkSGL3Qmdm7P82bxyU8HDDDBhDV3UckRaYcRa/NDNTYt8YBpzjoLWHUAejmOjLg== +scroll-into-view-if-needed@^2.2.28: + version "2.2.31" + resolved "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.31.tgz#d3c482959dc483e37962d1521254e3295d0d1587" + integrity sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA== + dependencies: + compute-scroll-into-view "^1.0.20" + select-hose@^2.0.0: version "2.0.0" resolved "https://mirrors.huaweicloud.com/repository/npm/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" @@ -11571,6 +11866,22 @@ slash@^3.0.0: resolved "https://mirrors.huaweicloud.com/repository/npm/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +slate-history@^0.66.0: + version "0.66.0" + resolved "https://registry.npmmirror.com/slate-history/-/slate-history-0.66.0.tgz#ac63fddb903098ceb4c944433e3f75fe63acf940" + integrity sha512-6MWpxGQZiMvSINlCbMW43E2YBSVMCMCIwQfBzGssjWw4kb0qfvj0pIdblWNRQZD0hR6WHP+dHHgGSeVdMWzfng== + dependencies: + is-plain-object "^5.0.0" + +slate@^0.72.0: + version "0.72.8" + resolved "https://registry.npmmirror.com/slate/-/slate-0.72.8.tgz#5a018edf24e45448655293a68bfbcf563aa5ba81" + integrity sha512-/nJwTswQgnRurpK+bGJFH1oM7naD5qDmHd89JyiKNT2oOKD8marW0QSBtuFnwEbL5aGCS8AmrhXQgNOsn4osAw== + dependencies: + immer "^9.0.6" + is-plain-object "^5.0.0" + tiny-warning "^1.0.3" + slice-ansi@^2.1.0: version "2.1.0" resolved "https://mirrors.huaweicloud.com/repository/npm/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636" @@ -11580,6 +11891,11 @@ slice-ansi@^2.1.0: astral-regex "^1.0.0" is-fullwidth-code-point "^2.0.0" +snabbdom@^3.1.0, snabbdom@^3.5.1: + version "3.5.1" + resolved "https://registry.npmmirror.com/snabbdom/-/snabbdom-3.5.1.tgz#25f80ef15b194baea703d9d5441892e369de18e1" + integrity sha512-wHMNIOjkm/YNE5EM3RCbr/+DVgPg6AqQAX1eOxO46zYNvCXjKP5Y865tqQj3EXnaMBjkxmQA5jFuDpDK/dbfiA== + snapdragon-node@^2.0.1: version "2.1.1" resolved "https://mirrors.huaweicloud.com/repository/npm/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" @@ -11800,6 +12116,11 @@ sshpk@^1.7.0: safer-buffer "^2.0.2" tweetnacl "~0.14.0" +ssr-window@^3.0.0-alpha.1: + version "3.0.0" + resolved "https://registry.npmmirror.com/ssr-window/-/ssr-window-3.0.0.tgz#fd5b82801638943e0cc704c4691801435af7ac37" + integrity sha512-q+8UfWDg9Itrg0yWK7oe5p/XRCJpJF9OBtXfOPgSJl+u3Xd5KI328RUEvUqSMVM9CiQUEf1QdBzJMkYGErj9QA== + ssri@^6.0.1: version "6.0.2" resolved "https://mirrors.huaweicloud.com/repository/npm/ssri/-/ssri-6.0.2.tgz#157939134f20464e7301ddba3e90ffa8f7728ac5" @@ -12313,6 +12634,11 @@ tiny-emitter@^2.0.0: resolved "https://mirrors.huaweicloud.com/repository/npm/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423" integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q== +tiny-warning@^1.0.3: + version "1.0.3" + resolved "https://registry.npmmirror.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" + integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== + tinycolor2@^1.4.1: version "1.6.0" resolved "https://mirrors.huaweicloud.com/repository/npm/tinycolor2/-/tinycolor2-1.6.0.tgz#f98007460169b0263b97072c5ae92484ce02d09e" @@ -12543,6 +12869,16 @@ type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" +type@^1.0.1: + version "1.2.0" + resolved "https://registry.npmmirror.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0" + integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg== + +type@^2.7.2: + version "2.7.2" + resolved "https://registry.npmmirror.com/type/-/type-2.7.2.tgz#2376a15a3a28b1efa0f5350dcf72d24df6ef98d0" + integrity sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw== + typed-array-length@^1.0.4: version "1.0.4" resolved "https://mirrors.huaweicloud.com/repository/npm/typed-array-length/-/typed-array-length-1.0.4.tgz#89d83785e5c4098bec72e08b319651f0eac9c1bb" @@ -13413,6 +13749,11 @@ which@^2.0.1: dependencies: isexe "^2.0.0" +wildcard@^1.1.0: + version "1.1.2" + resolved "https://registry.npmmirror.com/wildcard/-/wildcard-1.1.2.tgz#a7020453084d8cd2efe70ba9d3696263de1710a5" + integrity sha512-DXukZJxpHA8LuotRwL0pP1+rS6CS7FF2qStDDE1C7DDg2rLud2PXRMuEDYIPhgEezwnlHNL4c+N6MfMTjCGTng== + window-size@0.1.0: version "0.1.0" resolved "https://mirrors.huaweicloud.com/repository/npm/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d" diff --git a/docker-compose.yml b/docker-compose.yml index 58547c6..350be70 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.3 + image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-api:2.3.4 # build: # context: . # target: cmdb-api @@ -48,10 +48,11 @@ services: gunicorn --workers=3 autoapp:app -b 0.0.0.0:5000 -D flask cmdb-init-cache flask cmdb-init-acl + nohup flask cmdb-trigger > trigger.log 2>&1 & nohup flask cmdb-counter > counter.log 2>&1 & - celery -A celery_worker.celery worker -E -Q one_cmdb_async --concurrency=2 -D - celery -A celery_worker.celery worker -E -Q acl_async --concurrency=2 + celery -A celery_worker.celery worker -E -Q one_cmdb_async --autoscale=5,2 --logfile=one_cmdb_async.log -D + celery -A celery_worker.celery worker -E -Q acl_async --logfile=one_acl_async.log --concurrency=2 depends_on: - cmdb-db - cmdb-cache @@ -61,7 +62,7 @@ services: - cmdb-api cmdb-ui: - image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-ui:2.3.3 + image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-ui:2.3.4 # build: # context: . # target: cmdb-ui diff --git a/docs/cmdb.sql b/docs/cmdb.sql index aa6d672..e86d161 100644 --- a/docs/cmdb.sql +++ b/docs/cmdb.sql @@ -843,7 +843,7 @@ CREATE TABLE `c_c_t_t` ( `updated_at` datetime DEFAULT NULL, `id` int(11) NOT NULL AUTO_INCREMENT, `type_id` int(11) NOT NULL, - `attr_id` int(11) NOT NULL, + `attr_id` int(11) DEFAULT NULL, `notify` json DEFAULT NULL, PRIMARY KEY (`id`), KEY `type_id` (`type_id`), @@ -854,15 +854,36 @@ CREATE TABLE `c_c_t_t` ( ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; + -- --- Dumping data for table `c_c_t_t` +-- Table structure for table `c_ci_trigger_histories` -- -LOCK TABLES `c_c_t_t` WRITE; -/*!40000 ALTER TABLE `c_c_t_t` DISABLE KEYS */; -INSERT INTO `c_c_t_t` VALUES (NULL,0,'2023-01-09 14:53:47',NULL,1,4,51,'{\"body\": \"bbb\", \"wx_to\": [], \"subject\": \"aaa\", \"notify_at\": \"08:00\", \"before_days\": 1}'); -/*!40000 ALTER TABLE `c_c_t_t` ENABLE KEYS */; -UNLOCK TABLES; +DROP TABLE IF EXISTS `c_ci_trigger_histories`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `c_ci_trigger_histories` ( + `deleted_at` datetime DEFAULT NULL, + `deleted` tinyint(1) DEFAULT NULL, + `created_at` datetime DEFAULT NULL, + `updated_at` datetime DEFAULT NULL, + `id` int(11) NOT NULL AUTO_INCREMENT, + `operate_type` enum('1','0','2') DEFAULT NULL, + `record_id` int(11) DEFAULT NULL, + `ci_id` int(11) NOT NULL, + `trigger_id` int(11) DEFAULT NULL, + `trigger_name` varchar(64) DEFAULT NULL, + `is_ok` tinyint(1) DEFAULT NULL, + `notify` text, + `webhook` text, + PRIMARY KEY (`id`), + KEY `record_id` (`record_id`), + KEY `trigger_id` (`trigger_id`), + KEY `ix_c_ci_trigger_histories_ci_id` (`ci_id`), + KEY `ix_c_ci_trigger_histories_deleted` (`deleted`), + CONSTRAINT `c_ci_trigger_histories_ibfk_1` FOREIGN KEY (`record_id`) REFERENCES `c_records` (`id`), + CONSTRAINT `c_ci_trigger_histories_ibfk_2` FOREIGN KEY (`trigger_id`) REFERENCES `c_c_t_t` (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=19 DEFAULT CHARSET=utf8; -- -- Table structure for table `c_c_t_u_c` @@ -2098,6 +2119,7 @@ CREATE TABLE `common_employee` ( `acl_virtual_rid` int(11) DEFAULT NULL COMMENT 'ACL中虚拟角色rid', `last_login` timestamp NULL DEFAULT NULL COMMENT '上次登录时间', `block` int(11) DEFAULT NULL COMMENT '锁定状态', + `notice_info` json DEFAULT NULL, PRIMARY KEY (`employee_id`), KEY `department_id` (`department_id`), KEY `ix_common_employee_deleted` (`deleted`), @@ -2111,7 +2133,7 @@ CREATE TABLE `common_employee` ( LOCK TABLES `common_employee` WRITE; /*!40000 ALTER TABLE `common_employee` DISABLE KEYS */; -INSERT INTO `common_employee` VALUES (NULL,0,'2023-07-11 16:28:25',NULL,1,'demo@veops.cn','demo','demo','','','','',0,0,46,0,0,'2023-07-11 08:28:24',0),(NULL,0,'2023-07-11 16:34:08',NULL,2,'admin@one-ops.com','admin','admin','','','','',0,0,1,0,0,'2023-07-11 08:34:08',0); +INSERT INTO `common_employee` VALUES (NULL,0,'2023-07-11 16:28:25',NULL,1,'demo@veops.cn','demo','demo','','','','',0,0,46,0,0,'2023-07-11 08:28:24',0, null),(NULL,0,'2023-07-11 16:34:08',NULL,2,'admin@one-ops.com','admin','admin','','','','',0,0,1,0,0,'2023-07-11 08:34:08',0, null); /*!40000 ALTER TABLE `common_employee` ENABLE KEYS */; UNLOCK TABLES;