diff --git a/.gitignore b/.gitignore index 6c63d75..e43370d 100755 --- a/.gitignore +++ b/.gitignore @@ -43,7 +43,8 @@ cmdb-api/api/uploaded_files cmdb-api/migrations/versions # Translations -*.mo +#*.mo +messages.pot # Mr Developer .mr.developer.cfg diff --git a/README.md b/README.md index 7d4294f..06a66ed 100644 --- a/README.md +++ b/README.md @@ -74,17 +74,17 @@ ### Docker 一键快速构建 > 方法一 -- 第一步: 先安装 docker 环境, 以及docker-compose +- 第一步: 先安装 Docker 环境, 以及Docker Compose (v2) - 第二步: 拷贝项目 ```shell git clone https://github.com/veops/cmdb.git ``` - 第三步:进入主目录,执行: ``` -docker-compose up -d +docker compose up -d ``` > 方法二, 该方法适用于linux系统 -- 第一步: 先安装 docker 环境, 以及docker-compose +- 第一步: 先安装 Docker 环境, 以及Docker Compose (v2) - 第二步: 直接使用项目根目录下的install.sh 文件进行 `安装`、`启动`、`暂停`、`查状态`、`删除`、`卸载` ```shell curl -so install.sh https://raw.githubusercontent.com/veops/cmdb/deploy_on_kylin_docker/install.sh diff --git a/cmdb-api/Pipfile b/cmdb-api/Pipfile index 4ae8224..2e024a1 100644 --- a/cmdb-api/Pipfile +++ b/cmdb-api/Pipfile @@ -5,8 +5,8 @@ name = "pypi" [packages] # Flask -Flask = "==2.3.2" -Werkzeug = ">=2.3.6" +Flask = "==2.2.5" +Werkzeug = "==2.2.3" click = ">=5.0" # Api Flask-RESTful = "==0.3.10" @@ -15,6 +15,7 @@ Flask-SQLAlchemy = "==2.5.0" SQLAlchemy = "==1.4.49" PyMySQL = "==1.1.0" redis = "==4.6.0" +python-redis-lock = "==4.0.0" # Migrations Flask-Migrate = "==2.5.2" # Deployment @@ -27,6 +28,8 @@ Flask-Cors = ">=3.0.8" ldap3 = "==2.9.1" pycryptodome = "==3.12.0" cryptography = ">=41.0.2" +# i18n +flask-babel = "==4.0.0" # Caching Flask-Caching = ">=1.0.0" # Environment variable parsing @@ -62,6 +65,8 @@ alembic = "==1.7.7" hvac = "==2.0.0" colorama = ">=0.4.6" pycryptodomex = ">=3.19.0" +lz4 = ">=4.3.2" +python-magic = "==0.4.27" [dev-packages] # Testing diff --git a/cmdb-api/api/app.py b/cmdb-api/api/app.py index 6ea299d..b6857ba 100644 --- a/cmdb-api/api/app.py +++ b/cmdb-api/api/app.py @@ -12,14 +12,17 @@ from pathlib import Path from flask import Flask from flask import jsonify from flask import make_response +from flask import request from flask.blueprints import Blueprint from flask.cli import click from flask.json.provider import DefaultJSONProvider +from flask_babel.speaklater import LazyString import api.views.entry -from api.extensions import (bcrypt, cache, celery, cors, db, es, login_manager, migrate, rd) +from api.extensions import (bcrypt, babel, cache, celery, cors, db, es, login_manager, migrate, rd) from api.extensions import inner_secrets -from api.flask_cas import CAS +from api.lib.perm.authentication.cas import CAS +from api.lib.perm.authentication.oauth2 import OAuth2 from api.lib.secrets.secrets import InnerKVManger from api.models.acl import User @@ -71,7 +74,7 @@ class ReverseProxy(object): class MyJSONEncoder(DefaultJSONProvider): def default(self, o): - if isinstance(o, (decimal.Decimal, datetime.date, datetime.time)): + if isinstance(o, (decimal.Decimal, datetime.date, datetime.time, LazyString)): return str(o) if isinstance(o, datetime.datetime): @@ -96,6 +99,7 @@ def create_app(config_object="settings"): register_shell_context(app) register_commands(app) CAS(app) + OAuth2(app) app.wsgi_app = ReverseProxy(app.wsgi_app) configure_upload_dir(app) @@ -115,7 +119,13 @@ def configure_upload_dir(app): def register_extensions(app): """Register Flask extensions.""" + + def get_locale(): + accept_languages = app.config.get('ACCEPT_LANGUAGES', ['en', 'zh']) + return request.accept_languages.best_match(accept_languages) + bcrypt.init_app(app) + babel.init_app(app, locale_selector=get_locale) cache.init_app(app) db.init_app(app) cors.init_app(app) @@ -192,10 +202,11 @@ def configure_logger(app): app.logger.addHandler(handler) log_file = app.config['LOG_PATH'] - file_handler = RotatingFileHandler(log_file, - maxBytes=2 ** 30, - backupCount=7) - file_handler.setLevel(getattr(logging, app.config['LOG_LEVEL'])) - file_handler.setFormatter(formatter) - app.logger.addHandler(file_handler) + if log_file and log_file != "/dev/stdout": + file_handler = RotatingFileHandler(log_file, + maxBytes=2 ** 30, + backupCount=7) + file_handler.setLevel(getattr(logging, app.config['LOG_LEVEL'])) + file_handler.setFormatter(formatter) + app.logger.addHandler(file_handler) app.logger.setLevel(getattr(logging, app.config['LOG_LEVEL'])) diff --git a/cmdb-api/api/commands/click_acl.py b/cmdb-api/api/commands/click_acl.py index 3588d8c..75303ca 100644 --- a/cmdb-api/api/commands/click_acl.py +++ b/cmdb-api/api/commands/click_acl.py @@ -35,8 +35,22 @@ def add_user(): """ + from api.models.acl import App + from api.lib.perm.acl.cache import AppCache + from api.lib.perm.acl.cache import RoleCache + from api.lib.perm.acl.role import RoleCRUD + from api.lib.perm.acl.role import RoleRelationCRUD + username = click.prompt('Enter username', confirmation_prompt=False) password = click.prompt('Enter password', hide_input=True, confirmation_prompt=True) email = click.prompt('Enter email ', confirmation_prompt=False) + is_admin = click.prompt('Admin (Y/N) ', confirmation_prompt=False, type=bool, default=False) UserCRUD.add(username=username, password=password, email=email) + + if is_admin: + app = AppCache.get('acl') or App.create(name='acl') + acl_admin = RoleCache.get_by_name(app.id, 'acl_admin') or RoleCRUD.add_role('acl_admin', app.id, True) + rid = RoleCache.get_by_name(None, username).id + + RoleRelationCRUD.add(acl_admin, acl_admin.id, [rid], app.id) diff --git a/cmdb-api/api/commands/click_cmdb.py b/cmdb-api/api/commands/click_cmdb.py index 16b6ae8..fef71c4 100644 --- a/cmdb-api/api/commands/click_cmdb.py +++ b/cmdb-api/api/commands/click_cmdb.py @@ -5,6 +5,7 @@ import copy import datetime import json import time +import uuid import click import requests @@ -114,10 +115,20 @@ def cmdb_init_acl(): _app = AppCache.get('cmdb') or App.create(name='cmdb') app_id = _app.id + current_app.test_request_context().push() + # 1. add resource type for resource_type in ResourceTypeEnum.all(): try: - ResourceTypeCRUD.add(app_id, resource_type, '', PermEnum.all()) + perms = PermEnum.all() + if resource_type in (ResourceTypeEnum.CI_FILTER, ResourceTypeEnum.PAGE): + perms = [PermEnum.READ] + elif resource_type == ResourceTypeEnum.CI_TYPE_RELATION: + perms = [PermEnum.ADD, PermEnum.DELETE, PermEnum.GRANT] + elif resource_type == ResourceTypeEnum.RELATION_VIEW: + perms = [PermEnum.READ, PermEnum.UPDATE, PermEnum.DELETE, PermEnum.GRANT] + + ResourceTypeCRUD.add(app_id, resource_type, '', perms) except AbortException: pass @@ -168,12 +179,27 @@ def cmdb_counter(): from api.lib.cmdb.cache import CMDBCounterCache current_app.test_request_context().push() + if not UserCache.get('worker'): + from api.lib.perm.acl.user import UserCRUD + + UserCRUD.add(username='worker', password=uuid.uuid4().hex, email='worker@xxx.com') + login_user(UserCache.get('worker')) + + i = 0 while True: try: db.session.remove() CMDBCounterCache.reset() + + if i % 5 == 0: + CMDBCounterCache.flush_adc_counter() + i = 0 + + CMDBCounterCache.flush_sub_counter() + + i += 1 except: import traceback print(traceback.format_exc()) diff --git a/cmdb-api/api/commands/click_common_setting.py b/cmdb-api/api/commands/click_common_setting.py index a1f325e..8c2f52d 100644 --- a/cmdb-api/api/commands/click_common_setting.py +++ b/cmdb-api/api/commands/click_common_setting.py @@ -4,7 +4,7 @@ from flask.cli import with_appcontext from werkzeug.datastructures import MultiDict from api.lib.common_setting.acl import ACLManager -from api.lib.common_setting.employee import EmployeeAddForm +from api.lib.common_setting.employee import EmployeeAddForm, GrantEmployeeACLPerm from api.lib.common_setting.resp_format import ErrFormat from api.models.common_setting import Employee, Department @@ -158,50 +158,11 @@ class InitDepartment(object): def init_backend_resource(self): acl = self.check_app('backend') - resources_types = acl.get_all_resources_types() - - perms = ['read', 'grant', 'delete', 'update'] - acl_rid = self.get_admin_user_rid() - results = list(filter(lambda t: t['name'] == '操作权限', resources_types['groups'])) - if len(results) == 0: - payload = dict( - app_id=acl.app_name, - name='操作权限', - description='', - perms=perms - ) - resource_type = acl.create_resources_type(payload) - else: - resource_type = results[0] - resource_type_id = resource_type['id'] - existed_perms = resources_types.get('id2perms', {}).get(resource_type_id, []) - existed_perms = [p['name'] for p in existed_perms] - new_perms = [] - for perm in perms: - if perm not in existed_perms: - new_perms.append(perm) - if len(new_perms) > 0: - resource_type['perms'] = existed_perms + new_perms - acl.update_resources_type(resource_type_id, resource_type) - - resource_list = acl.get_resource_by_type(None, None, resource_type['id']) - - for name in ['公司信息', '公司架构', '通知设置']: - target = list(filter(lambda r: r['name'] == name, resource_list)) - if len(target) == 0: - payload = dict( - type_id=resource_type['id'], - app_id=acl.app_name, - name=name, - ) - resource = acl.create_resource(payload) - else: - resource = target[0] - - if acl_rid > 0: - acl.grant_resource(acl_rid, resource['id'], perms) + if acl_rid == 0: + return + GrantEmployeeACLPerm(acl).grant_by_rid(acl_rid, True) @staticmethod def check_app(app_name): @@ -263,7 +224,7 @@ def common_check_new_columns(): column_type = new_column.type.compile(engine.dialect) default_value = new_column.default.arg if new_column.default else None - sql = "ALTER TABLE " + target_table_name + " ADD COLUMN " + new_column.name + " " + column_type + sql = "ALTER TABLE " + target_table_name + " ADD COLUMN " + f"`{new_column.name}`" + " " + column_type if new_column.comment: sql += f" comment '{new_column.comment}'" @@ -299,3 +260,20 @@ def common_check_new_columns(): except Exception as e: current_app.logger.error(f"add new column [{column.name}] in table [{table_name}] err:") current_app.logger.error(e) + + +@click.command() +@with_appcontext +def common_sync_file_to_db(): + from api.lib.common_setting.upload_file import CommonFileCRUD + CommonFileCRUD.sync_file_to_db() + + +@click.command() +@with_appcontext +@click.option('--value', type=click.INT, default=-1) +def set_auth_auto_redirect_enable(value): + if value < 0: + return + from api.lib.common_setting.common_data import CommonDataCRUD + CommonDataCRUD.set_auth_auto_redirect_enable(value) diff --git a/cmdb-api/api/commands/common.py b/cmdb-api/api/commands/common.py index 6313ef6..c7ccf25 100644 --- a/cmdb-api/api/commands/common.py +++ b/cmdb-api/api/commands/common.py @@ -5,9 +5,7 @@ from glob import glob from subprocess import call import click -from flask import current_app from flask.cli import with_appcontext -from werkzeug.exceptions import MethodNotAllowed, NotFound from api.extensions import db @@ -90,3 +88,53 @@ def db_setup(): """create tables """ db.create_all() + + try: + db.session.execute("set global sql_mode='STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE," + "ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION'") + db.session.commit() + except: + pass + + try: + db.session.execute("set global tidb_enable_noop_functions='ON'") + db.session.commit() + except: + pass + + +@click.group() +def translate(): + """Translation and localization commands.""" + + +@translate.command() +@click.argument('lang') +def init(lang): + """Initialize a new language.""" + + if os.system('pybabel extract -F babel.cfg -k _l -o messages.pot .'): + raise RuntimeError('extract command failed') + if os.system( + 'pybabel init -i messages.pot -d api/translations -l ' + lang): + raise RuntimeError('init command failed') + os.remove('messages.pot') + + +@translate.command() +def update(): + """Update all languages.""" + + if os.system('pybabel extract -F babel.cfg -k _l -o messages.pot .'): + raise RuntimeError('extract command failed') + if os.system('pybabel update -i messages.pot -d api/translations'): + raise RuntimeError('update command failed') + os.remove('messages.pot') + + +@translate.command() +def compile(): + """Compile all languages.""" + + if os.system('pybabel compile -d api/translations'): + raise RuntimeError('compile command failed') diff --git a/cmdb-api/api/extensions.py b/cmdb-api/api/extensions.py index 2c3ff5f..96b9ac6 100644 --- a/cmdb-api/api/extensions.py +++ b/cmdb-api/api/extensions.py @@ -2,6 +2,7 @@ from celery import Celery +from flask_babel import Babel from flask_bcrypt import Bcrypt from flask_caching import Cache from flask_cors import CORS @@ -14,6 +15,7 @@ from api.lib.utils import ESHandler from api.lib.utils import RedisHandler bcrypt = Bcrypt() +babel = Babel() login_manager = LoginManager() db = SQLAlchemy(session_options={"autoflush": False}) migrate = Migrate() diff --git a/cmdb-api/api/lib/cmdb/attribute.py b/cmdb-api/api/lib/cmdb/attribute.py index 817bac2..37aa31a 100644 --- a/cmdb-api/api/lib/cmdb/attribute.py +++ b/cmdb-api/api/lib/cmdb/attribute.py @@ -189,7 +189,8 @@ class AttributeManager(object): return attr def get_attribute(self, key, choice_web_hook_parse=True, choice_other_parse=True): - attr = AttributeCache.get(key).to_dict() + attr = AttributeCache.get(key) or dict() + attr = attr and attr.to_dict() if attr.get("is_choice"): attr["choice_value"] = self.get_choice_values( attr["id"], diff --git a/cmdb-api/api/lib/cmdb/auto_discovery/auto_discovery.py b/cmdb-api/api/lib/cmdb/auto_discovery/auto_discovery.py index cec57f5..a0323fb 100644 --- a/cmdb-api/api/lib/cmdb/auto_discovery/auto_discovery.py +++ b/cmdb-api/api/lib/cmdb/auto_discovery/auto_discovery.py @@ -3,11 +3,6 @@ import datetime import json import os -from flask import abort -from flask import current_app -from flask_login import current_user -from sqlalchemy import func - from api.extensions import db from api.lib.cmdb.auto_discovery.const import ClOUD_MAP from api.lib.cmdb.cache import CITypeAttributeCache @@ -28,6 +23,10 @@ from api.lib.utils import AESCrypto from api.models.cmdb import AutoDiscoveryCI from api.models.cmdb import AutoDiscoveryCIType from api.models.cmdb import AutoDiscoveryRule +from flask import abort +from flask import current_app +from flask_login import current_user +from sqlalchemy import func PWD = os.path.abspath(os.path.dirname(__file__)) @@ -251,20 +250,17 @@ class AutoDiscoveryCITypeCRUD(DBMixin): current_app.logger.warning(e) return abort(400, str(e)) - def _can_add(self, **kwargs): - self.cls.get_by(type_id=kwargs['type_id'], adr_id=kwargs.get('adr_id') or None) and abort( - 400, ErrFormat.ad_duplicate) - - # self.__valid_exec_target(kwargs.get('agent_id'), kwargs.get('query_expr')) + @staticmethod + def _can_add(**kwargs): if kwargs.get('adr_id'): - adr = AutoDiscoveryRule.get_by_id(kwargs['adr_id']) or abort( + AutoDiscoveryRule.get_by_id(kwargs['adr_id']) or abort( 404, ErrFormat.adr_not_found.format("id={}".format(kwargs['adr_id']))) - if not adr.is_plugin: - other = self.cls.get_by(adr_id=adr.id, first=True, to_dict=False) - if other: - ci_type = CITypeCache.get(other.type_id) - return abort(400, ErrFormat.adr_default_ref_once.format(ci_type.alias)) + # if not adr.is_plugin: + # other = self.cls.get_by(adr_id=adr.id, first=True, to_dict=False) + # if other: + # ci_type = CITypeCache.get(other.type_id) + # return abort(400, ErrFormat.adr_default_ref_once.format(ci_type.alias)) if kwargs.get('is_plugin') and kwargs.get('plugin_script'): kwargs = check_plugin_script(**kwargs) @@ -334,15 +330,15 @@ class AutoDiscoveryCICRUD(DBMixin): @staticmethod def get_attributes_by_type_id(type_id): - from api.lib.cmdb.cache import CITypeAttributesCache - attributes = [i[1] for i in CITypeAttributesCache.get2(type_id) or []] + from api.lib.cmdb.ci_type import CITypeAttributeManager + attributes = [i for i in CITypeAttributeManager.get_attributes_by_type_id(type_id) or []] attr_names = set() adts = AutoDiscoveryCITypeCRUD.get_by_type_id(type_id) for adt in adts: attr_names |= set((adt.attributes or {}).values()) - return [attr.to_dict() for attr in attributes if attr.name in attr_names] + return [attr for attr in attributes if attr['name'] in attr_names] @classmethod def search(cls, page, page_size, fl=None, **kwargs): diff --git a/cmdb-api/api/lib/cmdb/auto_discovery/templates/aliyun_ecs.json b/cmdb-api/api/lib/cmdb/auto_discovery/templates/aliyun_ecs.json index ca9285f..e570b2d 100644 --- a/cmdb-api/api/lib/cmdb/auto_discovery/templates/aliyun_ecs.json +++ b/cmdb-api/api/lib/cmdb/auto_discovery/templates/aliyun_ecs.json @@ -1,647 +1,386 @@ -[ - { - "name": "CreationTime", - "type": "文本", - "example": "2017-12-10T04:04Z", - "desc": "\u5b9e\u4f8b\u521b\u5efa\u65f6\u95f4\u3002\u4ee5ISO 8601\u4e3a\u6807\u51c6\uff0c\u5e76\u4f7f\u7528UTC+0\u65f6\u95f4\uff0c\u683c\u5f0f\u4e3ayyyy-MM-ddTHH:mmZ\u3002\u66f4\u591a\u4fe1\u606f\uff0c\u8bf7\u53c2\u89c1[ISO 8601](~~25696~~)\u3002" - }, - { - "name": "SerialNumber", - "type": "文本", - "example": "51d1353b-22bf-4567-a176-8b3e12e4****", - "desc": "\u5b9e\u4f8b\u5e8f\u5217\u53f7\u3002" - }, - { - "name": "Status", - "type": "文本", - "example": "Running", - "desc": "\u5b9e\u4f8b\u72b6\u6001\u3002" - }, - { - "name": "DeploymentSetId", - "type": "文本", - "example": "ds-bp67acfmxazb4p****", - "desc": "\u90e8\u7f72\u96c6ID\u3002" - }, - { - "name": "KeyPairName", - "type": "文本", - "example": "testKeyPairName", - "desc": "\u5bc6\u94a5\u5bf9\u540d\u79f0\u3002" - }, - { - "name": "SaleCycle", - "type": "文本", - "example": "month", - "desc": "> \u8be5\u53c2\u6570\u5df2\u5f03\u7528\uff0c\u4e0d\u518d\u8fd4\u56de\u6709\u610f\u4e49\u7684\u6570\u636e\u3002" - }, - { - "name": "SpotStrategy", - "type": "文本", - "example": "NoSpot", - "desc": "\u6309\u91cf\u5b9e\u4f8b\u7684\u7ade\u4ef7\u7b56\u7565\u3002\u53ef\u80fd\u503c\uff1a\n\n- NoSpot\uff1a\u6b63\u5e38\u6309\u91cf\u4ed8\u8d39\u5b9e\u4f8b\u3002\n- SpotWithPriceLimit\uff1a\u8bbe\u7f6e\u4e0a\u9650\u4ef7\u683c\u7684\u62a2\u5360\u5f0f\u5b9e\u4f8b\u3002\n- SpotAsPriceGo\uff1a\u7cfb\u7edf\u81ea\u52a8\u51fa\u4ef7\uff0c\u6700\u9ad8\u6309\u91cf\u4ed8\u8d39\u4ef7\u683c\u7684\u62a2\u5360\u5f0f\u5b9e\u4f8b\u3002" - }, - { - "name": "DeviceAvailable", - "type": "boolean", - "example": "true", - "desc": "\u5b9e\u4f8b\u662f\u5426\u53ef\u4ee5\u6302\u8f7d\u6570\u636e\u76d8\u3002" - }, - { - "name": "LocalStorageCapacity", - "type": "整数", - "example": "1000", - "desc": "\u5b9e\u4f8b\u6302\u8f7d\u7684\u672c\u5730\u5b58\u50a8\u5bb9\u91cf\u3002" - }, - { - "name": "Description", - "type": "文本", - "example": "testDescription", - "desc": "\u5b9e\u4f8b\u63cf\u8ff0\u3002" - }, - { - "name": "SpotDuration", - "type": "整数", - "example": "1", - "desc": "\u62a2\u5360\u5f0f\u5b9e\u4f8b\u7684\u4fdd\u7559\u65f6\u957f\uff0c\u5355\u4f4d\u4e3a\u5c0f\u65f6\u3002\u53ef\u80fd\u503c\u4e3a0~6\u3002\n\n- \u4fdd\u7559\u65f6\u957f2~6\u6b63\u5728\u9080\u6d4b\u4e2d\uff0c\u5982\u9700\u5f00\u901a\u8bf7\u63d0\u4ea4\u5de5\u5355\u3002\n- \u503c\u4e3a0\uff0c\u5219\u4e3a\u65e0\u4fdd\u62a4\u671f\u6a21\u5f0f\u3002\n\n>\u5f53SpotStrategy\u503c\u4e3aSpotWithPriceLimit\u6216SpotAsPriceGo\u65f6\u8fd4\u56de\u8be5\u53c2\u6570\u3002" - }, - { - "name": "InstanceNetworkType", - "type": "文本", - "example": "vpc", - "desc": "\u5b9e\u4f8b\u7f51\u7edc\u7c7b\u578b\u3002\u53ef\u80fd\u503c\uff1a\n\n- classic\uff1a\u7ecf\u5178\u7f51\u7edc\u3002\n- vpc\uff1a\u4e13\u6709\u7f51\u7edcVPC\u3002" - }, - { - "name": "InstanceName", - "type": "文本", - "example": "InstanceNameTest", - "desc": "\u5b9e\u4f8b\u540d\u79f0\u3002" - }, - { - "name": "OSNameEn", - "type": "文本", - "example": "CentOS 7.4 64 bit", - "desc": "\u5b9e\u4f8b\u64cd\u4f5c\u7cfb\u7edf\u7684\u82f1\u6587\u540d\u79f0\u3002" - }, - { - "name": "HpcClusterId", - "type": "文本", - "example": "hpc-bp67acfmxazb4p****", - "desc": "\u5b9e\u4f8b\u6240\u5c5e\u7684HPC\u96c6\u7fa4ID\u3002" - }, - { - "name": "SpotPriceLimit", - "type": "float", - "example": "0.98", - "desc": "\u5b9e\u4f8b\u7684\u6bcf\u5c0f\u65f6\u6700\u9ad8\u4ef7\u683c\u3002\u652f\u6301\u6700\u59273\u4f4d\u5c0f\u6570\uff0c\u53c2\u6570SpotStrategy=SpotWithPriceLimit\u65f6\uff0c\u8be5\u53c2\u6570\u751f\u6548\u3002" - }, - { - "name": "Memory", - "type": "整数", - "example": "16384", - "desc": "\u5185\u5b58\u5927\u5c0f\uff0c\u5355\u4f4d\u4e3aMiB\u3002" - }, - { - "name": "OSName", - "type": "文本", - "example": "CentOS 7.4 64 \u4f4d", - "desc": "\u5b9e\u4f8b\u7684\u64cd\u4f5c\u7cfb\u7edf\u540d\u79f0\u3002" - }, - { - "name": "DeploymentSetGroupNo", - "type": "整数", - "example": "1", - "desc": "ECS\u5b9e\u4f8b\u7ed1\u5b9a\u90e8\u7f72\u96c6\u5206\u6563\u90e8\u7f72\u65f6\uff0c\u5b9e\u4f8b\u5728\u90e8\u7f72\u96c6\u4e2d\u7684\u5206\u7ec4\u4f4d\u7f6e\u3002" - }, - { - "name": "ImageId", - "type": "文本", - "example": "m-bp67acfmxazb4p****", - "desc": "\u5b9e\u4f8b\u8fd0\u884c\u7684\u955c\u50cfID\u3002" - }, - { - "name": "VlanId", - "type": "文本", - "example": "10", - "desc": "\u5b9e\u4f8b\u7684VLAN ID\u3002\n\n>\u8be5\u53c2\u6570\u5373\u5c06\u88ab\u5f03\u7528\uff0c\u4e3a\u63d0\u9ad8\u517c\u5bb9\u6027\uff0c\u8bf7\u5c3d\u91cf\u4f7f\u7528\u5176\u4ed6\u53c2\u6570\u3002" - }, - { - "name": "ClusterId", - "type": "文本", - "example": "c-bp67acfmxazb4p****", - "desc": "\u5b9e\u4f8b\u6240\u5728\u7684\u96c6\u7fa4ID\u3002\n\n>\u8be5\u53c2\u6570\u5373\u5c06\u88ab\u5f03\u7528\uff0c\u4e3a\u63d0\u9ad8\u517c\u5bb9\u6027\uff0c\u8bf7\u5c3d\u91cf\u4f7f\u7528\u5176\u4ed6\u53c2\u6570\u3002" - }, - { - "name": "GPUSpec", - "type": "文本", - "example": "NVIDIA V100", - "desc": "\u5b9e\u4f8b\u89c4\u683c\u9644\u5e26\u7684GPU\u7c7b\u578b\u3002" - }, - { - "name": "AutoReleaseTime", - "type": "文本", - "example": "2017-12-10T04:04Z", - "desc": "\u6309\u91cf\u4ed8\u8d39\u5b9e\u4f8b\u7684\u81ea\u52a8\u91ca\u653e\u65f6\u95f4\u3002" - }, - { - "name": "DeletionProtection", - "type": "boolean", - "example": "false", - "desc": "\u5b9e\u4f8b\u91ca\u653e\u4fdd\u62a4\u5c5e\u6027\uff0c\u6307\u5b9a\u662f\u5426\u652f\u6301\u901a\u8fc7\u63a7\u5236\u53f0\u6216API\uff08DeleteInstance\uff09\u91ca\u653e\u5b9e\u4f8b\u3002\n\n- true\uff1a\u5df2\u5f00\u542f\u5b9e\u4f8b\u91ca\u653e\u4fdd\u62a4\u3002\n- false\uff1a\u672a\u5f00\u542f\u5b9e\u4f8b\u91ca\u653e\u4fdd\u62a4\u3002\n\n> \u8be5\u5c5e\u6027\u4ec5\u9002\u7528\u4e8e\u6309\u91cf\u4ed8\u8d39\u5b9e\u4f8b\uff0c\u4e14\u53ea\u80fd\u9650\u5236\u624b\u52a8\u91ca\u653e\u64cd\u4f5c\uff0c\u5bf9\u7cfb\u7edf\u91ca\u653e\u64cd\u4f5c\u4e0d\u751f\u6548\u3002" - }, - { - "name": "StoppedMode", - "type": "文本", - "example": "KeepCharging", - "desc": "\u5b9e\u4f8b\u505c\u673a\u540e\u662f\u5426\u7ee7\u7eed\u6536\u8d39\u3002\u53ef\u80fd\u503c\uff1a\n\n- KeepCharging\uff1a\u505c\u673a\u540e\u7ee7\u7eed\u6536\u8d39\uff0c\u4e3a\u60a8\u7ee7\u7eed\u4fdd\u7559\u5e93\u5b58\u8d44\u6e90\u3002\n- StopCharging\uff1a\u505c\u673a\u540e\u4e0d\u6536\u8d39\u3002\u505c\u673a\u540e\uff0c\u6211\u4eec\u91ca\u653e\u5b9e\u4f8b\u5bf9\u5e94\u7684\u8d44\u6e90\uff0c\u4f8b\u5982vCPU\u3001\u5185\u5b58\u548c\u516c\u7f51IP\u7b49\u8d44\u6e90\u3002\u91cd\u542f\u662f\u5426\u6210\u529f\u4f9d\u8d56\u4e8e\u5f53\u524d\u5730\u57df\u4e2d\u662f\u5426\u4ecd\u6709\u8d44\u6e90\u5e93\u5b58\u3002\n- Not-applicable\uff1a\u672c\u5b9e\u4f8b\u4e0d\u652f\u6301\u505c\u673a\u4e0d\u6536\u8d39\u529f\u80fd\u3002" - }, - { - "name": "GPUAmount", - "type": "整数", - "example": "4", - "desc": "\u5b9e\u4f8b\u89c4\u683c\u9644\u5e26\u7684GPU\u6570\u91cf\u3002" - }, - { - "name": "HostName", - "type": "文本", - "example": "testHostName", - "desc": "\u5b9e\u4f8b\u4e3b\u673a\u540d\u3002" - }, - { - "name": "InstanceId", - "type": "文本", - "example": "i-bp67acfmxazb4p****", - "desc": "\u5b9e\u4f8bID\u3002" - }, - { - "name": "InternetMaxBandwidthOut", - "type": "整数", - "example": "5", - "desc": "\u516c\u7f51\u51fa\u5e26\u5bbd\u6700\u5927\u503c\uff0c\u5355\u4f4d\u4e3aMbit/s\u3002" - }, - { - "name": "InternetMaxBandwidthIn", - "type": "整数", - "example": "50", - "desc": "\u516c\u7f51\u5165\u5e26\u5bbd\u6700\u5927\u503c\uff0c\u5355\u4f4d\u4e3aMbit/s\u3002" - }, - { - "name": "InstanceType", - "type": "文本", - "example": "ecs.g5.large", - "desc": "\u5b9e\u4f8b\u89c4\u683c\u3002" - }, - { - "name": "InstanceChargeType", - "type": "文本", - "example": "PostPaid", - "desc": "\u5b9e\u4f8b\u7684\u8ba1\u8d39\u65b9\u5f0f\u3002\u53ef\u80fd\u503c\uff1a\n\n- PrePaid\uff1a\u5305\u5e74\u5305\u6708\u3002\n- PostPaid\uff1a\u6309\u91cf\u4ed8\u8d39\u3002" - }, - { - "name": "RegionId", - "type": "文本", - "example": "cn-hangzhou", - "desc": "\u5b9e\u4f8b\u6240\u5c5e\u5730\u57dfID\u3002" - }, - { - "name": "IoOptimized", - "type": "boolean", - "example": "true", - "desc": "\u662f\u5426\u4e3aI/O\u4f18\u5316\u578b\u5b9e\u4f8b\u3002" - }, - { - "name": "StartTime", - "type": "文本", - "example": "2017-12-10T04:04Z", - "desc": "\u5b9e\u4f8b\u6700\u8fd1\u4e00\u6b21\u7684\u542f\u52a8\u65f6\u95f4\u3002\u4ee5ISO8601\u4e3a\u6807\u51c6\uff0c\u5e76\u4f7f\u7528UTC+0\u65f6\u95f4\uff0c\u683c\u5f0f\u4e3ayyyy-MM-ddTHH:mmZ\u3002\u66f4\u591a\u4fe1\u606f\uff0c\u8bf7\u53c2\u89c1[ISO8601](~~25696~~)\u3002" - }, - { - "name": "Cpu", - "type": "整数", - "example": "8", - "desc": "vCPU\u6570\u3002" - }, - { - "name": "LocalStorageAmount", - "type": "整数", - "example": "2", - "desc": "\u5b9e\u4f8b\u6302\u8f7d\u7684\u672c\u5730\u5b58\u50a8\u6570\u91cf\u3002" - }, - { - "name": "ExpiredTime", - "type": "文本", - "example": "2017-12-10T04:04Z", - "desc": "\u8fc7\u671f\u65f6\u95f4\u3002\u4ee5ISO8601\u4e3a\u6807\u51c6\uff0c\u5e76\u4f7f\u7528UTC+0\u65f6\u95f4\uff0c\u683c\u5f0f\u4e3ayyyy-MM-ddTHH:mmZ\u3002\u66f4\u591a\u4fe1\u606f\uff0c\u8bf7\u53c2\u89c1[ISO8601](~~25696~~)\u3002" - }, - { - "name": "ResourceGroupId", - "type": "文本", - "example": "rg-bp67acfmxazb4p****", - "desc": "\u5b9e\u4f8b\u6240\u5c5e\u7684\u4f01\u4e1a\u8d44\u6e90\u7ec4ID\u3002" - }, - { - "name": "InternetChargeType", - "type": "文本", - "example": "PayByTraffic", - "desc": "\u7f51\u7edc\u8ba1\u8d39\u7c7b\u578b\u3002\u53ef\u80fd\u503c\uff1a\n\n- PayByBandwidth\uff1a\u6309\u56fa\u5b9a\u5e26\u5bbd\u8ba1\u8d39\u3002\n- PayByTraffic\uff1a\u6309\u4f7f\u7528\u6d41\u91cf\u8ba1\u8d39\u3002" - }, - { - "name": "ZoneId", - "type": "文本", - "example": "cn-hangzhou-g", - "desc": "\u5b9e\u4f8b\u6240\u5c5e\u53ef\u7528\u533a\u3002" - }, - { - "name": "Recyclable", - "type": "boolean", - "example": "false", - "desc": "\u5b9e\u4f8b\u662f\u5426\u53ef\u4ee5\u56de\u6536\u3002" - }, - { - "name": "ISP", - "type": "文本", - "example": "null", - "desc": "> \u8be5\u53c2\u6570\u6b63\u5728\u9080\u6d4b\u4e2d\uff0c\u6682\u672a\u5f00\u653e\u4f7f\u7528\u3002" - }, - { - "name": "CreditSpecification", - "type": "文本", - "example": "Standard", - "desc": "\u4fee\u6539\u7a81\u53d1\u6027\u80fd\u5b9e\u4f8b\u7684\u8fd0\u884c\u6a21\u5f0f\u3002\u53ef\u80fd\u503c\uff1a\n\n- Standard\uff1a\u6807\u51c6\u6a21\u5f0f\u3002\u6709\u5173\u5b9e\u4f8b\u6027\u80fd\u7684\u66f4\u591a\u4fe1\u606f\uff0c\u8bf7\u53c2\u89c1[\u4ec0\u4e48\u662f\u7a81\u53d1\u6027\u80fd\u5b9e\u4f8b](~~59977~~)\u4e2d\u7684\u6027\u80fd\u7ea6\u675f\u6a21\u5f0f\u7ae0\u8282\u3002\n- Unlimited\uff1a\u65e0\u6027\u80fd\u7ea6\u675f\u6a21\u5f0f\uff0c\u6709\u5173\u5b9e\u4f8b\u6027\u80fd\u7684\u66f4\u591a\u4fe1\u606f\uff0c\u8bf7\u53c2\u89c1[\u4ec0\u4e48\u662f\u7a81\u53d1\u6027\u80fd\u5b9e\u4f8b](~~59977~~)\u4e2d\u7684\u65e0\u6027\u80fd\u7ea6\u675f\u6a21\u5f0f\u7ae0\u8282\u3002" - }, - { - "name": "InstanceTypeFamily", - "type": "文本", - "example": "ecs.g5", - "desc": "\u5b9e\u4f8b\u89c4\u683c\u65cf\u3002" - }, - { - "name": "OSType", - "type": "文本", - "example": "linux", - "desc": "\u5b9e\u4f8b\u7684\u64cd\u4f5c\u7cfb\u7edf\u7c7b\u578b\uff0c\u5206\u4e3aWindows Server\u548cLinux\u4e24\u79cd\u3002\u53ef\u80fd\u503c\uff1a\n\n- windows\u3002\n- linux\u3002" - }, - { - "name": "NetworkInterfaces", - "type": "json", - "example": { - "type": "json", - "properties": { - "Type": { - "description": "\u5f39\u6027\u7f51\u5361\u7c7b\u578b\u3002\u53ef\u80fd\u503c\uff1a\n- Primary\uff1a\u4e3b\u7f51\u5361\u3002\n- Secondary\uff1a\u8f85\u52a9\u5f39\u6027\u7f51\u5361\u3002", - "type": "文本", - "example": "Primary" - }, - "MacAddress": { - "description": "\u5f39\u6027\u7f51\u5361\u7684MAC\u5730\u5740\u3002", - "type": "文本", - "example": "00:16:3e:32:b4:**" - }, - "PrimaryIpAddress": { - "description": "\u5f39\u6027\u7f51\u5361\u4e3b\u79c1\u6709IP\u5730\u5740\u3002", - "type": "文本", - "example": "172.17.**.***" - }, - "NetworkInterfaceId": { - "description": "\u5f39\u6027\u7f51\u5361\u7684ID\u3002", - "type": "文本", - "example": "eni-2zeh9atclduxvf1z****" - }, - "PrivateIpSets": { - "type": "array", - "items": { - "type": "json", - "properties": { - "PrivateIpAddress": { - "description": "\u5b9e\u4f8b\u7684\u79c1\u7f51IP\u5730\u5740\u3002", - "type": "文本", - "example": "172.17.**.**" - }, - "Primary": { - "description": "\u662f\u5426\u662f\u4e3b\u79c1\u7f51IP\u5730\u5740\u3002", - "type": "boolean", - "example": "true" - } - } - }, - "description": "PrivateIpSet\u7ec4\u6210\u7684\u96c6\u5408\u3002" - }, - "Ipv6Sets": { - "type": "array", - "items": { - "type": "json", - "properties": { - "Ipv6Address": { - "description": "\u4e3a\u5f39\u6027\u7f51\u5361\u6307\u5b9a\u7684IPv6\u5730\u5740\u3002", - "type": "文本", - "example": "2408:4321:180:1701:94c7:bc38:3bfa:***" - } - } - }, - "description": "\u4e3a\u5f39\u6027\u7f51\u5361\u5206\u914d\u7684IPv6\u5730\u5740\u96c6\u5408\u3002\u4ec5\u5f53\u8bf7\u6c42\u53c2\u6570`AdditionalAttributes.N`\u53d6\u503c\u4e3a`NETWORK_PRIMARY_ENI_IP`\u65f6\uff0c\u624d\u4f1a\u8fd4\u56de\u8be5\u53c2\u6570\u503c\u3002" - }, - "Ipv4PrefixSets": { - "type": "array", - "items": { - "type": "json", - "properties": { - "Ipv4Prefix": { - "description": "IPv4\u524d\u7f00\u3002", - "type": "文本", - "example": "47.122.*.*/19" - } - } - }, - "description": "IPv4\u524d\u7f00\u96c6\u5408\u3002" - }, - "Ipv6PrefixSets": { - "type": "array", - "items": { - "type": "json", - "properties": { - "Ipv6Prefix": { - "description": "IPv6\u524d\u7f00\u3002", - "type": "文本", - "example": "2001:1111:*:*::/64" - } - } - }, - "description": "IPv6\u524d\u7f00\u96c6\u5408\u3002" - } - }, - "description": "\u5b9e\u4f8b\u5305\u542b\u7684\u5f39\u6027\u7f51\u5361\u96c6\u5408\u3002" - }, - "desc": "\u5b9e\u4f8b\u5305\u542b\u7684\u5f39\u6027\u7f51\u5361\u96c6\u5408\u3002" - }, - { - "name": "OperationLocks", - "type": "文本、多值", - "example": { - "type": "json", - "properties": { - "LockMsg": { - "description": "\u5b9e\u4f8b\u88ab\u9501\u5b9a\u7684\u63cf\u8ff0\u4fe1\u606f\u3002", - "type": "文本", - "example": "The specified instance is locked due to financial reason." - }, - "LockReason": { - "description": "\u9501\u5b9a\u7c7b\u578b\u3002\u53ef\u80fd\u503c\uff1a\n\n- financial\uff1a\u56e0\u6b20\u8d39\u88ab\u9501\u5b9a\u3002\n- security\uff1a\u56e0\u5b89\u5168\u539f\u56e0\u88ab\u9501\u5b9a\u3002\n- Recycling\uff1a\u62a2\u5360\u5f0f\u5b9e\u4f8b\u7684\u5f85\u91ca\u653e\u9501\u5b9a\u72b6\u6001\u3002\n- dedicatedhostfinancial\uff1a\u56e0\u4e3a\u4e13\u6709\u5bbf\u4e3b\u673a\u6b20\u8d39\u5bfc\u81f4ECS\u5b9e\u4f8b\u88ab\u9501\u5b9a\u3002\n- refunded\uff1a\u56e0\u9000\u6b3e\u88ab\u9501\u5b9a\u3002", - "type": "文本", - "example": "Recycling" - } - } - }, - "desc": "\u5b9e\u4f8b\u7684\u9501\u5b9a\u539f\u56e0\u3002" - }, - { - "name": "Tags", - "type": "json", - "example": { - "type": "json", - "properties": { - "TagValue": { - "description": "\u5b9e\u4f8b\u7684\u6807\u7b7e\u503c\u3002", - "type": "文本", - "example": "TestValue" - }, - "TagKey": { - "description": "\u5b9e\u4f8b\u7684\u6807\u7b7e\u952e\u3002", - "type": "文本", - "example": "TestKey" - } - } - }, - "desc": "\u5b9e\u4f8b\u7684\u6807\u7b7e\u96c6\u5408\u3002" - }, - { - "name": "RdmaIpAddress", - "type": "文本、多值", - "example": { - "description": "HPC\u5b9e\u4f8b\u7684Rdma\u7f51\u7edcIP\u3002", - "type": "文本", - "example": "10.10.10.102" - }, - "desc": "HPC\u5b9e\u4f8b\u7684Rdma\u7f51\u7edcIP\u5217\u8868\u3002" - }, - { - "name": "SecurityGroupIds", - "type": "文本、多值", - "example": { - "description": "\u5b89\u5168\u7ec4ID\u3002", - "type": "文本", - "example": "sg-bp67acfmxazb4p****" - }, - "desc": "\u5b9e\u4f8b\u6240\u5c5e\u5b89\u5168\u7ec4ID\u5217\u8868\u3002" - }, - { - "name": "PublicIpAddress", - "type": "文本、多值", - "example": { - "description": "\u5b9e\u4f8b\u516c\u7f51IP\u5730\u5740\u3002", - "type": "文本", - "example": "121.40.**.**" - }, - "desc": "\u5b9e\u4f8b\u516c\u7f51IP\u5730\u5740\u5217\u8868\u3002" - }, - { - "name": "InnerIpAddress", - "type": "文本、多值", - "example": { - "description": "\u7ecf\u5178\u7f51\u7edc\u7c7b\u578b\u5b9e\u4f8b\u7684\u5185\u7f51IP\u5730\u5740\u3002", - "type": "文本", - "example": "10.170.**.**" - }, - "desc": "\u7ecf\u5178\u7f51\u7edc\u7c7b\u578b\u5b9e\u4f8b\u7684\u5185\u7f51IP\u5730\u5740\u5217\u8868\u3002" - }, - { - "name": "VpcAttributes", - "type": "json", - "example": { - "VpcId": { - "description": "\u4e13\u6709\u7f51\u7edcVPC ID\u3002", - "type": "文本", - "example": "vpc-2zeuphj08tt7q3brd****" - }, - "NatIpAddress": { - "description": "\u4e91\u4ea7\u54c1\u7684IP\uff0c\u7528\u4e8eVPC\u4e91\u4ea7\u54c1\u4e4b\u95f4\u7684\u7f51\u7edc\u4e92\u901a\u3002", - "type": "文本", - "example": "172.17.**.**" - }, - "VSwitchId": { - "description": "\u865a\u62df\u4ea4\u6362\u673aID\u3002", - "type": "文本", - "example": "vsw-2zeh0r1pabwtg6wcs****" - }, - "PrivateIpAddress": { - "type": "array", - "items": { - "description": "\u79c1\u6709IP\u5730\u5740\u3002", - "type": "文本", - "example": "172.17.**.**" - }, - "description": "\u79c1\u6709IP\u5730\u5740\u5217\u8868\u3002" - } - }, - "desc": "\u4e13\u6709\u7f51\u7edcVPC\u5c5e\u6027\u3002" - }, - { - "name": "EipAddress", - "type": "json", - "example": { - "IsSupportUnassociate": { - "description": "\u662f\u5426\u53ef\u4ee5\u89e3\u7ed1\u5f39\u6027\u516c\u7f51IP\u3002", - "type": "boolean", - "example": "true" - }, - "InternetChargeType": { - "description": "\u5f39\u6027\u516c\u7f51IP\u7684\u8ba1\u8d39\u65b9\u5f0f\u3002\n\n- PayByBandwidth\uff1a\u6309\u5e26\u5bbd\u8ba1\u8d39\u3002\n\n- PayByTraffic\uff1a\u6309\u6d41\u91cf\u8ba1\u8d39\u3002", - "type": "文本", - "example": "PayByTraffic" - }, - "IpAddress": { - "description": "\u5f39\u6027\u516c\u7f51IP\u3002", - "type": "文本", - "example": "42.112.**.**" - }, - "Bandwidth": { - "description": "\u5f39\u6027\u516c\u7f51IP\u7684\u516c\u7f51\u5e26\u5bbd\u9650\u901f\uff0c\u5355\u4f4d\u4e3aMbit/s\u3002", - "type": "整数", - "format": "int32", - "example": "5" - }, - "AllocationId": { - "description": "\u5f39\u6027\u516c\u7f51IP\u7684ID\u3002", - "type": "文本", - "example": "eip-2ze88m67qx5z****" - } - }, - "desc": "\u5f39\u6027\u516c\u7f51IP\u7ed1\u5b9a\u4fe1\u606f\u3002" - }, - { - "name": "HibernationOptions", - "type": "json", - "example": { - "Configured": { - "description": "> \u8be5\u53c2\u6570\u6b63\u5728\u9080\u6d4b\u4e2d\uff0c\u6682\u672a\u5f00\u653e\u4f7f\u7528\u3002", - "type": "boolean", - "example": "false" - } - }, - "desc": "> \u8be5\u53c2\u6570\u6b63\u5728\u9080\u6d4b\u4e2d\uff0c\u6682\u672a\u5f00\u653e\u4f7f\u7528\u3002" - }, - { - "name": "DedicatedHostAttribute", - "type": "json", - "example": { - "DedicatedHostId": { - "description": "\u4e13\u6709\u5bbf\u4e3b\u673aID\u3002", - "type": "文本", - "example": "dh-bp67acfmxazb4p****" - }, - "DedicatedHostName": { - "description": "\u4e13\u6709\u5bbf\u4e3b\u673a\u540d\u79f0\u3002", - "type": "文本", - "example": "testDedicatedHostName" - }, - "DedicatedHostClusterId": { - "description": "\u4e13\u6709\u5bbf\u4e3b\u673a\u96c6\u7fa4ID\u3002", - "type": "文本", - "example": "dc-bp67acfmxazb4h****" - } - }, - "desc": "\u7531\u4e13\u6709\u5bbf\u4e3b\u673a\u96c6\u7fa4ID\uff08DedicatedHostClusterId\uff09\u3001\u4e13\u6709\u5bbf\u4e3b\u673aID\uff08DedicatedHostId\uff09\u548c\u540d\u79f0\uff08DedicatedHostName\uff09\u7ec4\u6210\u7684\u5bbf\u4e3b\u673a\u5c5e\u6027\u6570\u7ec4\u3002" - }, - { - "name": "EcsCapacityReservationAttr", - "type": "json", - "example": { - "CapacityReservationPreference": { - "description": "\u5bb9\u91cf\u9884\u7559\u504f\u597d\u3002", - "type": "文本", - "example": "cr-bp67acfmxazb4p****" - }, - "CapacityReservationId": { - "description": "\u5bb9\u91cf\u9884\u7559ID\u3002", - "type": "文本", - "example": "cr-bp67acfmxazb4p****" - } - }, - "desc": "\u4e91\u670d\u52a1\u5668ECS\u7684\u5bb9\u91cf\u9884\u7559\u76f8\u5173\u53c2\u6570\u3002" - }, - { - "name": "DedicatedInstanceAttribute", - "type": "json", - "example": { - "Affinity": { - "description": "\u4e13\u6709\u5bbf\u4e3b\u673a\u5b9e\u4f8b\u662f\u5426\u4e0e\u4e13\u6709\u5bbf\u4e3b\u673a\u5173\u8054\u3002\u53ef\u80fd\u503c\uff1a\n\n- default\uff1a\u4e13\u6709\u5bbf\u4e3b\u673a\u5b9e\u4f8b\u4e0d\u4e0e\u4e13\u6709\u5bbf\u4e3b\u673a\u5173\u8054\u3002\u505c\u673a\u4e0d\u6536\u8d39\u5b9e\u4f8b\u91cd\u542f\u540e\uff0c\u53ef\u80fd\u4f1a\u653e\u7f6e\u5728\u81ea\u52a8\u8d44\u6e90\u90e8\u7f72\u6c60\u4e2d\u7684\u5176\u5b83\u4e13\u6709\u5bbf\u4e3b\u673a\u4e0a\u3002\n\n- host\uff1a\u4e13\u6709\u5bbf\u4e3b\u673a\u5b9e\u4f8b\u4e0e\u4e13\u6709\u5bbf\u4e3b\u673a\u5173\u8054\u3002\u505c\u673a\u4e0d\u6536\u8d39\u5b9e\u4f8b\u91cd\u542f\u540e\uff0c\u4ecd\u653e\u7f6e\u5728\u539f\u4e13\u6709\u5bbf\u4e3b\u673a\u4e0a\u3002", - "type": "文本", - "example": "default" - }, - "Tenancy": { - "description": "\u5b9e\u4f8b\u7684\u5bbf\u4e3b\u673a\u7c7b\u578b\u662f\u5426\u4e3a\u4e13\u6709\u5bbf\u4e3b\u673a\u3002\u53ef\u80fd\u503c\uff1a\n\n- default\uff1a\u5b9e\u4f8b\u7684\u5bbf\u4e3b\u673a\u7c7b\u578b\u4e0d\u662f\u4e13\u6709\u5bbf\u4e3b\u673a\u3002\n\n- host\uff1a\u5b9e\u4f8b\u7684\u5bbf\u4e3b\u673a\u7c7b\u578b\u4e3a\u4e13\u6709\u5bbf\u4e3b\u673a\u3002", - "type": "文本", - "example": "default" - } - }, - "desc": "\u4e13\u6709\u5bbf\u4e3b\u673a\u5b9e\u4f8b\u7684\u5c5e\u6027\u3002" - }, - { - "name": "CpuOptions", - "type": "json", - "example": { - "Numa": { - "description": "\u5206\u914d\u7684\u7ebf\u7a0b\u6570\u3002\u53ef\u80fd\u503c\u4e3a2\u3002", - "type": "文本", - "example": "2" - }, - "CoreCount": { - "description": "\u7269\u7406CPU\u6838\u5fc3\u6570\u3002", - "type": "整数", - "format": "int32", - "example": "2" - }, - "ThreadsPerCore": { - "description": "CPU\u7ebf\u7a0b\u6570\u3002", - "type": "整数", - "format": "int32", - "example": "4" - } - }, - "desc": "CPU\u914d\u7f6e\u8be6\u60c5\u3002" - }, - { - "name": "MetadataOptions", - "type": "json", - "example": { - "HttpEndpoint": { - "description": "\u662f\u5426\u542f\u7528\u5b9e\u4f8b\u5143\u6570\u636e\u7684\u8bbf\u95ee\u901a\u9053\u3002\u53ef\u80fd\u503c\uff1a\n- enabled\uff1a\u542f\u7528\u3002\n- disabled\uff1a\u7981\u7528\u3002", - "type": "文本", - "example": "enabled" - }, - "HttpPutResponseHopLimit": { - "description": "> \u8be5\u53c2\u6570\u6682\u672a\u5f00\u653e\u4f7f\u7528\u3002", - "type": "整数", - "format": "int32", - "example": "0" - }, - "HttpTokens": { - "description": "\u8bbf\u95ee\u5b9e\u4f8b\u5143\u6570\u636e\u65f6\u662f\u5426\u5f3a\u5236\u4f7f\u7528\u52a0\u56fa\u6a21\u5f0f\uff08IMDSv2\uff09\u3002\u53ef\u80fd\u503c\uff1a\n- optional\uff1a\u4e0d\u5f3a\u5236\u4f7f\u7528\u3002\n- required\uff1a\u5f3a\u5236\u4f7f\u7528\u3002", - "type": "文本", - "example": "optional" - } - }, - "desc": "\u5143\u6570\u636e\u9009\u9879\u96c6\u5408\u3002" - }, - { - "name": "ImageOptions", - "type": "json", - "example": { - "LoginAsNonRoot": { - "description": "\u4f7f\u7528\u8be5\u955c\u50cf\u7684\u5b9e\u4f8b\u662f\u5426\u652f\u6301\u4f7f\u7528ecs-user\u7528\u6237\u767b\u5f55\u3002\u53ef\u80fd\u503c\uff1a\n\n- true\uff1a\u662f\n\n- false\uff1a\u5426", - "type": "boolean", - "example": "false" - } - }, - "desc": "\u955c\u50cf\u76f8\u5173\u5c5e\u6027\u4fe1\u606f\u3002" - } +[ + { + "name": "CreationTime", + "type": "string", + "desc": "实例创建时间。以ISO 8601为标准,并使用UTC+0时间,格式为yyyy-MM-ddTHH:mmZ。更多信息,请参见[ISO 8601](~~25696~~)。", + "example": "2017-12-10T04:04Z" + }, + { + "name": "SerialNumber", + "type": "string", + "desc": "实例序列号。", + "example": "51d1353b-22bf-4567-a176-8b3e12e4****" + }, + { + "name": "Status", + "type": "string", + "desc": "实例状态。", + "example": "Running" + }, + { + "name": "DeploymentSetId", + "type": "string", + "desc": "部署集ID。", + "example": "ds-bp67acfmxazb4p****" + }, + { + "name": "KeyPairName", + "type": "string", + "desc": "密钥对名称。", + "example": "testKeyPairName" + }, + { + "name": "SaleCycle", + "type": "string", + "desc": "> 该参数已弃用,不再返回有意义的数据。", + "example": "month" + }, + { + "name": "SpotStrategy", + "type": "string", + "desc": "按量实例的竞价策略。可能值:\n\n- NoSpot:正常按量付费实例。\n- SpotWithPriceLimit:设置上限价格的抢占式实例。\n- SpotAsPriceGo:系统自动出价,最高按量付费价格的抢占式实例。", + "example": "NoSpot" + }, + { + "name": "DeviceAvailable", + "type": "boolean", + "desc": "实例是否可以挂载数据盘。\n\n- true:可以挂载数据盘。\n- false:不可以挂载数据盘。", + "example": "true" + }, + { + "name": "LocalStorageCapacity", + "type": "integer", + "desc": "实例挂载的本地存储容量。单位:GiB。", + "example": "1000" + }, + { + "name": "Description", + "type": "string", + "desc": "实例描述。", + "example": "testDescription" + }, + { + "name": "SpotDuration", + "type": "integer", + "desc": "抢占式实例的保留时长,单位为小时。可能值:\n\n- 1:创建后阿里云会保证实例运行1小时不会被自动释放;超过1小时后,系统会自动比较出价与市场价格、检查资源库存,来决定实例的持有和回收。\n- 0:创建后,阿里云不保证实例运行1小时,系统会自动比较出价与市场价格、检查资源库存,来决定实例的持有和回收。\n\n实例回收前5分钟阿里云会通过ECS系统事件向您发送通知。抢占式实例按秒计费,建议您结合具体任务执行耗时来选择合适的保留时长。\n\n>当SpotStrategy值为SpotWithPriceLimit或SpotAsPriceGo时返回该参数。", + "example": "1" + }, + { + "name": "InstanceNetworkType", + "type": "string", + "desc": "实例网络类型。可能值:\n\n- classic:经典网络。\n- vpc:专有网络VPC。", + "example": "vpc" + }, + { + "name": "InstanceName", + "type": "string", + "desc": "实例名称。", + "example": "InstanceNameTest" + }, + { + "name": "OSNameEn", + "type": "string", + "desc": "实例操作系统的英文名称。", + "example": "CentOS 7.4 64 bit" + }, + { + "name": "HpcClusterId", + "type": "string", + "desc": "实例所属的HPC集群ID。", + "example": "hpc-bp67acfmxazb4p****" + }, + { + "name": "SpotPriceLimit", + "type": "number", + "desc": "实例的每小时最高价格。支持最大3位小数,参数SpotStrategy=SpotWithPriceLimit时,该参数生效。", + "example": "0.98" + }, + { + "name": "Memory", + "type": "integer", + "desc": "内存大小,单位为MiB。", + "example": "16384" + }, + { + "name": "OSName", + "type": "string", + "desc": "实例的操作系统名称。", + "example": "CentOS 7.4 64 位" + }, + { + "name": "DeploymentSetGroupNo", + "type": "integer", + "desc": "ECS实例绑定部署集分散部署时,实例在部署集中的分组位置。", + "example": "1" + }, + { + "name": "ImageId", + "type": "string", + "desc": "实例运行的镜像ID。", + "example": "m-bp67acfmxazb4p****" + }, + { + "name": "VlanId", + "type": "string", + "desc": "实例的VLAN ID。\n\n>该参数即将被弃用,为提高兼容性,请尽量使用其他参数。", + "example": "10" + }, + { + "name": "ClusterId", + "type": "string", + "desc": "实例所在的集群ID。\n\n>该参数即将被弃用,为提高兼容性,请尽量使用其他参数。", + "example": "c-bp67acfmxazb4p****" + }, + { + "name": "GPUSpec", + "type": "string", + "desc": "实例规格附带的GPU类型。", + "example": "NVIDIA V100" + }, + { + "name": "AutoReleaseTime", + "type": "string", + "desc": "按量付费实例的自动释放时间。", + "example": "2017-12-10T04:04Z" + }, + { + "name": "DeletionProtection", + "type": "boolean", + "desc": "实例释放保护属性,指定是否支持通过控制台或API(DeleteInstance)释放实例。\n\n- true:已开启实例释放保护。\n- false:未开启实例释放保护。\n\n> 该属性仅适用于按量付费实例,且只能限制手动释放操作,对系统释放操作不生效。", + "example": "false" + }, + { + "name": "StoppedMode", + "type": "string", + "desc": "实例停机后是否继续收费。可能值:\n\n- KeepCharging:停机后继续收费,为您继续保留库存资源。\n- StopCharging:停机后不收费。停机后,我们释放实例对应的资源,例如vCPU、内存和公网IP等资源。重启是否成功依赖于当前地域中是否仍有资源库存。\n- Not-applicable:本实例不支持停机不收费功能。", + "example": "KeepCharging" + }, + { + "name": "GPUAmount", + "type": "integer", + "desc": "实例规格附带的GPU数量。", + "example": "4" + }, + { + "name": "HostName", + "type": "string", + "desc": "实例主机名。", + "example": "testHostName" + }, + { + "name": "InstanceId", + "type": "string", + "desc": "实例ID。", + "example": "i-bp67acfmxazb4p****" + }, + { + "name": "InternetMaxBandwidthOut", + "type": "integer", + "desc": "公网出带宽最大值,单位:Mbit/s。", + "example": "5" + }, + { + "name": "InternetMaxBandwidthIn", + "type": "integer", + "desc": "公网入带宽最大值,单位:Mbit/s。", + "example": "50" + }, + { + "name": "InstanceType", + "type": "string", + "desc": "实例规格。", + "example": "ecs.g5.large" + }, + { + "name": "InstanceChargeType", + "type": "string", + "desc": "实例的计费方式。可能值:\n\n- PrePaid:包年包月。\n- PostPaid:按量付费。", + "example": "PostPaid" + }, + { + "name": "RegionId", + "type": "string", + "desc": "实例所属地域ID。", + "example": "cn-hangzhou" + }, + { + "name": "IoOptimized", + "type": "boolean", + "desc": "是否为I/O优化型实例。\n\n- true:是。\n- false:否。", + "example": "true" + }, + { + "name": "StartTime", + "type": "string", + "desc": "实例最近一次的启动时间。以ISO 8601为标准,并使用UTC+0时间,格式为yyyy-MM-ddTHH:mmZ。更多信息,请参见[ISO 8601](~~25696~~)。", + "example": "2017-12-10T04:04Z" + }, + { + "name": "Cpu", + "type": "integer", + "desc": "vCPU数。", + "example": "8" + }, + { + "name": "LocalStorageAmount", + "type": "integer", + "desc": "实例挂载的本地存储数量。", + "example": "2" + }, + { + "name": "ExpiredTime", + "type": "string", + "desc": "过期时间。以ISO 8601为标准,并使用UTC+0时间,格式为yyyy-MM-ddTHH:mmZ。更多信息,请参见[ISO 8601](~~25696~~)。", + "example": "2017-12-10T04:04Z" + }, + { + "name": "ResourceGroupId", + "type": "string", + "desc": "实例所属的企业资源组ID。", + "example": "rg-bp67acfmxazb4p****" + }, + { + "name": "InternetChargeType", + "type": "string", + "desc": "网络计费类型。可能值:\n\n- PayByBandwidth:按固定带宽计费。\n- PayByTraffic:按使用流量计费。", + "example": "PayByTraffic" + }, + { + "name": "ZoneId", + "type": "string", + "desc": "实例所属可用区。", + "example": "cn-hangzhou-g" + }, + { + "name": "Recyclable", + "type": "boolean", + "desc": "实例是否可以回收。", + "example": "false" + }, + { + "name": "ISP", + "type": "string", + "desc": "> 该参数正在邀测中,暂未开放使用。", + "example": "null" + }, + { + "name": "CreditSpecification", + "type": "string", + "desc": "突发性能实例的运行模式。可能值:\n\n- Standard:标准模式。有关实例性能的更多信息,请参见[什么是突发性能实例](~~59977~~)中的性能约束模式章节。\n- Unlimited:无性能约束模式,有关实例性能的更多信息,请参见[什么是突发性能实例](~~59977~~)中的无性能约束模式章节。", + "example": "Standard" + }, + { + "name": "InstanceTypeFamily", + "type": "string", + "desc": "实例规格族。", + "example": "ecs.g5" + }, + { + "name": "OSType", + "type": "string", + "desc": "实例的操作系统类型,分为Windows Server和Linux两种。可能值:\n\n- windows。\n- linux。", + "example": "linux" + }, + { + "name": "NetworkInterfaces", + "type": "array", + "desc": "实例包含的弹性网卡集合。", + "example": "" + }, + { + "name": "OperationLocks", + "type": "array", + "desc": "实例的锁定原因。", + "example": "" + }, + { + "name": "Tags", + "type": "array", + "desc": "实例的标签集合。", + "example": "" + }, + { + "name": "RdmaIpAddress", + "type": "array", + "desc": "HPC实例的RDMA网络IP列表。", + "example": "" + }, + { + "name": "SecurityGroupIds", + "type": "array", + "desc": "实例所属安全组ID列表。", + "example": "" + }, + { + "name": "PublicIpAddress", + "type": "array", + "desc": "实例公网IP地址列表。", + "example": "" + }, + { + "name": "InnerIpAddress", + "type": "array", + "desc": "经典网络类型实例的内网IP地址列表。", + "example": "" + }, + { + "name": "VpcAttributes", + "type": "object", + "desc": "专有网络VPC属性。", + "example": "" + }, + { + "name": "EipAddress", + "type": "object", + "desc": "弹性公网IP绑定信息。", + "example": "" + }, + { + "name": "HibernationOptions", + "type": "object", + "desc": "> 该参数正在邀测中,暂未开放使用。", + "example": "" + }, + { + "name": "DedicatedHostAttribute", + "type": "object", + "desc": "由专有宿主机集群ID(DedicatedHostClusterId)、专有宿主机ID(DedicatedHostId)和名称(DedicatedHostName)组成的宿主机属性数组。", + "example": "" + }, + { + "name": "EcsCapacityReservationAttr", + "type": "object", + "desc": "云服务器ECS的容量预留相关参数。", + "example": "" + }, + { + "name": "DedicatedInstanceAttribute", + "type": "object", + "desc": "专有宿主机实例的属性。", + "example": "" + }, + { + "name": "CpuOptions", + "type": "object", + "desc": "CPU配置详情。", + "example": "" + }, + { + "name": "MetadataOptions", + "type": "object", + "desc": "元数据选项集合。", + "example": "" + }, + { + "name": "ImageOptions", + "type": "object", + "desc": "镜像相关属性信息。", + "example": "" + }, + { + "name": "SpotInterruptionBehavior", + "type": "string", + "desc": "平台发起抢占式实例中断时,抢占式实例的中断模式。可能值:\n\n- Terminate:释放。\n\n- Stop:节省停机。", + "example": "Terminate" + } ] \ No newline at end of file diff --git a/cmdb-api/api/lib/cmdb/auto_discovery/templates/aws_ec2.json b/cmdb-api/api/lib/cmdb/auto_discovery/templates/aws_ec2.json index 90fbf94..a4ba9a7 100644 --- a/cmdb-api/api/lib/cmdb/auto_discovery/templates/aws_ec2.json +++ b/cmdb-api/api/lib/cmdb/auto_discovery/templates/aws_ec2.json @@ -1,427 +1,344 @@ -[ - { - "name": "amiLaunchIndex", - "type": "整数", - "desc": "The AMI launch index, which can be used to find this instance in the launch group.", - "example": "0" - }, - { - "name": "architecture", - "type": "文本", - "desc": "The architecture of the image.", - "example": "x86_64" - }, - { - "name": "blockDeviceMapping", - "type": "json", - "desc": "Any block device mapping entries for the instance.", - "example": { - "item": { - "deviceName": "/dev/xvda", - "ebs": { - "volumeId": "vol-1234567890abcdef0", - "status": "attached", - "attachTime": "2015-12-22T10:44:09.000Z", - "deleteOnTermination": "true" - } - } - } - }, - { - "name": "bootMode", - "type": "文本", - "desc": "The boot mode that was specified by the AMI. If the value is uefi-preferred, the AMI supports both UEFI and Legacy BIOS. The currentInstanceBootMode parameter is the boot mode that is used to boot the instance at launch or start.", - "example": null - }, - { - "name": "capacityReservationId", - "type": "文本", - "desc": "The ID of the Capacity Reservation.", - "example": null - }, - { - "name": "capacityReservationSpecification", - "type": "json", - "desc": "Information about the Capacity Reservation targeting option.", - "example": null - }, - { - "name": "clientToken", - "type": "文本", - "desc": "The idempotency token you provided when you launched the instance, if applicable.", - "example": "xMcwG14507example" - }, - { - "name": "cpuOptions", - "type": "json", - "desc": "The CPU options for the instance.", - "example": { - "coreCount": "1", - "threadsPerCore": "1" - } - }, - { - "name": "currentInstanceBootMode", - "type": "文本", - "desc": "The boot mode that is used to boot the instance at launch or start. For more information, see Boot modes in the Amazon EC2 User Guide.", - "example": null - }, - { - "name": "dnsName", - "type": "文本", - "desc": "[IPv4 only] The public DNS name assigned to the instance. This name is not available until the instance enters the running state. This name is only available if you've enabled DNS hostnames for your VPC.", - "example": "ec2-54-194-252-215.eu-west-1.compute.amazonaws.com" - }, - { - "name": "ebsOptimized", - "type": "Boolean", - "desc": "Indicates whether the instance is optimized for Amazon EBS I/O. This optimization provides dedicated throughput to Amazon EBS and an optimized configuration stack to provide optimal I/O performance. This optimization isn't available with all instance types. Additional usage charges apply when using an EBS Optimized instance.", - "example": "false" - }, - { - "name": "elasticGpuAssociationSet", - "type": "json", - "desc": "The Elastic GPU associated with the instance.", - "example": null - }, - { - "name": "elasticInferenceAcceleratorAssociationSet", - "type": "json", - "desc": "The elastic inference accelerator associated with the instance.", - "example": null - }, - { - "name": "enaSupport", - "type": "Boolean", - "desc": "Specifies whether enhanced networking with ENA is enabled.", - "example": null - }, - { - "name": "enclaveOptions", - "type": "json", - "desc": "Indicates whether the instance is enabled for AWS Nitro Enclaves.", - "example": null - }, - { - "name": "groupSet", - "type": "json", - "desc": "The security groups for the instance.", - "example": { - "item": { - "groupId": "sg-e4076980", - "groupName": "SecurityGroup1" - } - } - }, - { - "name": "hibernationOptions", - "type": "json", - "desc": "Indicates whether the instance is enabled for hibernation.", - "example": null - }, - { - "name": "hypervisor", - "type": "文本", - "desc": "The hypervisor type of the instance. The value xen is used for both Xen and Nitro hypervisors.", - "example": "xen" - }, - { - "name": "iamInstanceProfile", - "type": "json", - "desc": "The IAM instance profile associated with the instance, if applicable.", - "example": { - "arn": "arn:aws:iam::123456789012:instance-profile/AdminRole", - "id": "ABCAJEDNCAA64SSD123AB" - } - }, - { - "name": "imageId", - "type": "文本", - "desc": "The ID of the AMI used to launch the instance.", - "example": "ami-bff32ccc" - }, - { - "name": "instanceId", - "type": "文本", - "desc": "The ID of the instance.", - "example": "i-1234567890abcdef0" - }, - { - "name": "instanceLifecycle", - "type": "文本", - "desc": "Indicates whether this is a Spot Instance or a Scheduled Instance.", - "example": null - }, - { - "name": "instanceState", - "type": "json", - "desc": "The current state of the instance.", - "example": { - "code": "16", - "name": "running" - } - }, - { - "name": "instanceType", - "type": "文本", - "desc": "The instance type.", - "example": "t2.micro" - }, - { - "name": "ipAddress", - "type": "文本", - "desc": "The public IPv4 address, or the Carrier IP address assigned to the instance, if applicable.", - "example": "54.194.252.215" - }, - { - "name": "ipv6Address", - "type": "文本", - "desc": "The IPv6 address assigned to the instance.", - "example": null - }, - { - "name": "kernelId", - "type": "文本", - "desc": "The kernel associated with this instance, if applicable.", - "example": null - }, - { - "name": "keyName", - "type": "文本", - "desc": "The name of the key pair, if this instance was launched with an associated key pair.", - "example": "my_keypair" - }, - { - "name": "launchTime", - "type": "Time", - "desc": "The time the instance was launched.", - "example": "2018-05-08T16:46:19.000Z" - }, - { - "name": "licenseSet", - "type": "json", - "desc": "The license configurations for the instance.", - "example": null - }, - { - "name": "maintenanceOptions", - "type": "json", - "desc": "Provides information on the recovery and maintenance options of your instance.", - "example": null - }, - { - "name": "metadataOptions", - "type": "json", - "desc": "The metadata options for the instance.", - "example": null - }, - { - "name": "monitoring", - "type": "json", - "desc": "The monitoring for the instance.", - "example": { - "state": "disabled" - } - }, - { - "name": "networkInterfaceSet", - "type": "json", - "desc": "The network interfaces for the instance.", - "example": { - "item": { - "networkInterfaceId": "eni-551ba033", - "subnetId": "subnet-56f5f633", - "vpcId": "vpc-11112222", - "description": "Primary network interface", - "ownerId": "123456789012", - "status": "in-use", - "macAddress": "02:dd:2c:5e:01:69", - "privateIpAddress": "192.168.1.88", - "privateDnsName": "ip-192-168-1-88.eu-west-1.compute.internal", - "sourceDestCheck": "true", - "groupSet": { - "item": { - "groupId": "sg-e4076980", - "groupName": "SecurityGroup1" - } - }, - "attachment": { - "attachmentId": "eni-attach-39697adc", - "deviceIndex": "0", - "status": "attached", - "attachTime": "2018-05-08T16:46:19.000Z", - "deleteOnTermination": "true" - }, - "association": { - "publicIp": "54.194.252.215", - "publicDnsName": "ec2-54-194-252-215.eu-west-1.compute.amazonaws.com", - "ipOwnerId": "amazon" - }, - "privateIpAddressesSet": { - "item": { - "privateIpAddress": "192.168.1.88", - "privateDnsName": "ip-192-168-1-88.eu-west-1.compute.internal", - "primary": "true", - "association": { - "publicIp": "54.194.252.215", - "publicDnsName": "ec2-54-194-252-215.eu-west-1.compute.amazonaws.com", - "ipOwnerId": "amazon" - } - } - }, - "ipv6AddressesSet": { - "item": { - "ipv6Address": "2001:db8:1234:1a2b::123" - } - } - } - } - }, - { - "name": "outpostArn", - "type": "文本", - "desc": "The Amazon Resource Name (ARN) of the Outpost.", - "example": null - }, - { - "name": "placement", - "type": "json", - "desc": "The location where the instance launched, if applicable.", - "example": { - "availabilityZone": "eu-west-1c", - "groupName": null, - "tenancy": "default" - } - }, - { - "name": "platform", - "type": "文本", - "desc": "The value is Windows for Windows instances; otherwise blank.", - "example": null - }, - { - "name": "platformDetails", - "type": "文本", - "desc": "The platform details value for the instance. For more information, see AMI billing information fields in the Amazon EC2 User Guide.", - "example": null - }, - { - "name": "privateDnsName", - "type": "文本", - "desc": "[IPv4 only] The private DNS hostname name assigned to the instance. This DNS hostname can only be used inside the Amazon EC2 network. This name is not available until the instance enters the running state.", - "example": "ip-192-168-1-88.eu-west-1.compute.internal" - }, - { - "name": "privateDnsNameOptions", - "type": "json", - "desc": "The options for the instance hostname.", - "example": null - }, - { - "name": "privateIpAddress", - "type": "文本", - "desc": "The private IPv4 address assigned to the instance.", - "example": "192.168.1.88" - }, - { - "name": "productCodes", - "type": "json", - "desc": "The product codes attached to this instance, if applicable.", - "example": null - }, - { - "name": "ramdiskId", - "type": "文本", - "desc": "The RAM disk associated with this instance, if applicable.", - "example": null - }, - { - "name": "reason", - "type": "文本", - "desc": "The reason for the most recent state transition. This might be an empty string.", - "example": null - }, - { - "name": "rootDeviceName", - "type": "文本", - "desc": "The device name of the root device volume (for example, /dev/sda1).", - "example": "/dev/xvda" - }, - { - "name": "rootDeviceType", - "type": "文本", - "desc": "The root device type used by the AMI. The AMI can use an EBS volume or an instance store volume.", - "example": "ebs" - }, - { - "name": "sourceDestCheck", - "type": "Boolean", - "desc": "Indicates whether source/destination checking is enabled.", - "example": "true" - }, - { - "name": "spotInstanceRequestId", - "type": "文本", - "desc": "If the request is a Spot Instance request, the ID of the request.", - "example": null - }, - { - "name": "sriovNetSupport", - "type": "文本", - "desc": "Specifies whether enhanced networking with the Intel 82599 Virtual Function interface is enabled.", - "example": null - }, - { - "name": "stateReason", - "type": "json", - "desc": "The reason for the most recent state transition.", - "example": null - }, - { - "name": "subnetId", - "type": "文本", - "desc": "The ID of the subnet in which the instance is running.", - "example": "subnet-56f5f633" - }, - { - "name": "tagSet", - "type": "json", - "desc": "Any tags assigned to the instance.", - "example": { - "item": { - "key": "Name", - "value": "Server_1" - } - } - }, - { - "name": "tpmSupport", - "type": "文本", - "desc": "If the instance is configured for NitroTPM support, the value is v2.0. For more information, see NitroTPM in the Amazon EC2 User Guide.", - "example": null - }, - { - "name": "usageOperation", - "type": "文本", - "desc": "The usage operation value for the instance. For more information, see AMI billing information fields in the Amazon EC2 User Guide.", - "example": null - }, - { - "name": "usageOperationUpdateTime", - "type": "Time", - "desc": "The time that the usage operation was last updated.", - "example": null - }, - { - "name": "virtualizationType", - "type": "文本", - "desc": "The virtualization type of the instance.", - "example": "hvm" - }, - { - "name": "vpcId", - "type": "文本", - "desc": "The ID of the VPC in which the instance is running.", - "example": "vpc-11112222" - } +[ + { + "name": "amiLaunchIndex", + "type": "Integer", + "desc": "The AMI launch index, which can be used to find this instance in the launch group.", + "example": "" + }, + { + "name": "architecture", + "type": "String", + "desc": "The architecture of the image.", + "example": "i386" + }, + { + "name": "blockDeviceMapping", + "type": "Array of InstanceBlockDeviceMapping objects", + "desc": "Any block device mapping entries for the instance.", + "example": "" + }, + { + "name": "bootMode", + "type": "String", + "desc": "The boot mode that was specified by the AMI. If the value is uefi-preferred, the AMI supports both UEFI and Legacy BIOS. The currentInstanceBootMode parameter is the boot mode that is used to boot the instance at launch or start. For more information, see Boot modes in the Amazon EC2 User Guide.", + "example": "legacy-bios" + }, + { + "name": "capacityReservationId", + "type": "String", + "desc": "The ID of the Capacity Reservation.", + "example": "" + }, + { + "name": "capacityReservationSpecification", + "type": "CapacityReservationSpecificationResponse object", + "desc": "Information about the Capacity Reservation targeting option.", + "example": "" + }, + { + "name": "clientToken", + "type": "String", + "desc": "The idempotency token you provided when you launched the instance, if applicable.", + "example": "" + }, + { + "name": "cpuOptions", + "type": "CpuOptions object", + "desc": "The CPU options for the instance.", + "example": "" + }, + { + "name": "currentInstanceBootMode", + "type": "String", + "desc": "The boot mode that is used to boot the instance at launch or start. For more information, see Boot modes in the Amazon EC2 User Guide.", + "example": "legacy-bios" + }, + { + "name": "dnsName", + "type": "String", + "desc": "[IPv4 only] The public DNS name assigned to the instance. This name is not available until the instance enters the running state. This name is only available if you've enabled DNS hostnames for your VPC.", + "example": "" + }, + { + "name": "ebsOptimized", + "type": "Boolean", + "desc": "Indicates whether the instance is optimized for Amazon EBS I/O. This optimization provides dedicated throughput to Amazon EBS and an optimized configuration stack to provide optimal I/O performance. This optimization isn't available with all instance types. Additional usage charges apply when using an EBS Optimized instance.", + "example": "" + }, + { + "name": "elasticGpuAssociationSet", + "type": "Array of ElasticGpuAssociation objects", + "desc": "The Elastic GPU associated with the instance.", + "example": "" + }, + { + "name": "elasticInferenceAcceleratorAssociationSet", + "type": "Array of ElasticInferenceAcceleratorAssociation objects", + "desc": "The elastic inference accelerator associated with the instance.", + "example": "" + }, + { + "name": "enaSupport", + "type": "Boolean", + "desc": "Specifies whether enhanced networking with ENA is enabled.", + "example": "" + }, + { + "name": "enclaveOptions", + "type": "EnclaveOptions object", + "desc": "Indicates whether the instance is enabled for AWS Nitro Enclaves.", + "example": "" + }, + { + "name": "groupSet", + "type": "Array of GroupIdentifier objects", + "desc": "The security groups for the instance.", + "example": "" + }, + { + "name": "hibernationOptions", + "type": "HibernationOptions object", + "desc": "Indicates whether the instance is enabled for hibernation.", + "example": "" + }, + { + "name": "hypervisor", + "type": "String", + "desc": "The hypervisor type of the instance. The value xen is used for both Xen and Nitro hypervisors.", + "example": "ovm" + }, + { + "name": "iamInstanceProfile", + "type": "IamInstanceProfile object", + "desc": "The IAM instance profile associated with the instance, if applicable.", + "example": "" + }, + { + "name": "imageId", + "type": "String", + "desc": "The ID of the AMI used to launch the instance.", + "example": "" + }, + { + "name": "instanceId", + "type": "String", + "desc": "The ID of the instance.", + "example": "" + }, + { + "name": "instanceLifecycle", + "type": "String", + "desc": "Indicates whether this is a Spot Instance or a Scheduled Instance.", + "example": "spot" + }, + { + "name": "instanceState", + "type": "InstanceState object", + "desc": "The current state of the instance.", + "example": "" + }, + { + "name": "instanceType", + "type": "String", + "desc": "The instance type.", + "example": "a1.medium" + }, + { + "name": "ipAddress", + "type": "String", + "desc": "The public IPv4 address, or the Carrier IP address assigned to the instance, if applicable. A Carrier IP address only applies to an instance launched in a subnet associated with a Wavelength Zone.", + "example": "Required: No" + }, + { + "name": "ipv6Address", + "type": "String", + "desc": "The IPv6 address assigned to the instance.", + "example": "" + }, + { + "name": "kernelId", + "type": "String", + "desc": "The kernel associated with this instance, if applicable.", + "example": "" + }, + { + "name": "keyName", + "type": "String", + "desc": "The name of the key pair, if this instance was launched with an associated key pair.", + "example": "" + }, + { + "name": "launchTime", + "type": "Timestamp", + "desc": "The time the instance was launched.", + "example": "" + }, + { + "name": "licenseSet", + "type": "Array of LicenseConfiguration objects", + "desc": "The license configurations for the instance.", + "example": "" + }, + { + "name": "maintenanceOptions", + "type": "InstanceMaintenanceOptions object", + "desc": "Provides information on the recovery and maintenance options of your instance.", + "example": "" + }, + { + "name": "metadataOptions", + "type": "InstanceMetadataOptionsResponse object", + "desc": "The metadata options for the instance.", + "example": "" + }, + { + "name": "monitoring", + "type": "Monitoring object", + "desc": "The monitoring for the instance.", + "example": "" + }, + { + "name": "networkInterfaceSet", + "type": "Array of InstanceNetworkInterface objects", + "desc": "The network interfaces for the instance.", + "example": "" + }, + { + "name": "outpostArn", + "type": "String", + "desc": "The Amazon Resource Name (ARN) of the Outpost.", + "example": "" + }, + { + "name": "placement", + "type": "Placement object", + "desc": "The location where the instance launched, if applicable.", + "example": "" + }, + { + "name": "platform", + "type": "String", + "desc": "The platform. This value is windows for Windows instances; otherwise, it is empty.", + "example": "windows" + }, + { + "name": "platformDetails", + "type": "String", + "desc": "The platform details value for the instance. For more information, see AMI billing information fields in the Amazon EC2 User Guide.", + "example": "" + }, + { + "name": "privateDnsName", + "type": "String", + "desc": "[IPv4 only] The private DNS hostname name assigned to the instance. This DNS hostname can only be used inside the Amazon EC2 network. This name is not available until the instance enters the running state. The Amazon-provided DNS server resolves Amazon-provided private DNS hostnames if you've enabled DNS resolution and DNS hostnames in your VPC. If you are not using the Amazon-provided DNS server in your VPC, your custom domain name servers must resolve the hostname as appropriate.", + "example": "Required: No" + }, + { + "name": "privateDnsNameOptions", + "type": "PrivateDnsNameOptionsResponse object", + "desc": "The options for the instance hostname.", + "example": "" + }, + { + "name": "privateIpAddress", + "type": "String", + "desc": "The private IPv4 address assigned to the instance.", + "example": "" + }, + { + "name": "productCodes", + "type": "Array of ProductCode objects", + "desc": "The product codes attached to this instance, if applicable.", + "example": "" + }, + { + "name": "ramdiskId", + "type": "String", + "desc": "The RAM disk associated with this instance, if applicable.", + "example": "" + }, + { + "name": "reason", + "type": "String", + "desc": "The reason for the most recent state transition. This might be an empty string.", + "example": "" + }, + { + "name": "rootDeviceName", + "type": "String", + "desc": "The device name of the root device volume (for example, /dev/sda1).", + "example": "" + }, + { + "name": "rootDeviceType", + "type": "String", + "desc": "The root device type used by the AMI. The AMI can use an EBS volume or an instance store volume.", + "example": "ebs" + }, + { + "name": "sourceDestCheck", + "type": "Boolean", + "desc": "Indicates whether source/destination checking is enabled.", + "example": "" + }, + { + "name": "spotInstanceRequestId", + "type": "String", + "desc": "If the request is a Spot Instance request, the ID of the request.", + "example": "" + }, + { + "name": "sriovNetSupport", + "type": "String", + "desc": "Specifies whether enhanced networking with the Intel 82599 Virtual Function interface is enabled.", + "example": "" + }, + { + "name": "stateReason", + "type": "StateReason object", + "desc": "The reason for the most recent state transition.", + "example": "" + }, + { + "name": "subnetId", + "type": "String", + "desc": "The ID of the subnet in which the instance is running.", + "example": "" + }, + { + "name": "tagSet", + "type": "Array of Tag objects", + "desc": "Any tags assigned to the instance.", + "example": "" + }, + { + "name": "tpmSupport", + "type": "String", + "desc": "If the instance is configured for NitroTPM support, the value is v2.0. For more information, see NitroTPM in the Amazon EC2 User Guide.", + "example": "" + }, + { + "name": "usageOperation", + "type": "String", + "desc": "The usage operation value for the instance. For more information, see AMI billing information fields in the Amazon EC2 User Guide.", + "example": "" + }, + { + "name": "usageOperationUpdateTime", + "type": "Timestamp", + "desc": "The time that the usage operation was last updated.", + "example": "" + }, + { + "name": "virtualizationType", + "type": "String", + "desc": "The virtualization type of the instance.", + "example": "hvm" + }, + { + "name": "vpcId", + "type": "String", + "desc": "The ID of the VPC in which the instance is running.", + "example": "" + } ] \ No newline at end of file diff --git a/cmdb-api/api/lib/cmdb/auto_discovery/templates/huaweicloud_ecs.json b/cmdb-api/api/lib/cmdb/auto_discovery/templates/huaweicloud_ecs.json index af68387..854ed13 100644 --- a/cmdb-api/api/lib/cmdb/auto_discovery/templates/huaweicloud_ecs.json +++ b/cmdb-api/api/lib/cmdb/auto_discovery/templates/huaweicloud_ecs.json @@ -1,292 +1,284 @@ -[ - { - "name": "status", - "type": "文本", - "example": "ACTIVE", - "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u72b6\u6001\u3002\n\n\u53d6\u503c\u8303\u56f4:\n\nACTIVE\u3001BUILD\u3001DELETED\u3001ERROR\u3001HARD_REBOOT\u3001MIGRATING\u3001PAUSED\u3001REBOOT\u3001REBUILD\u3001RESIZE\u3001REVERT_RESIZE\u3001SHUTOFF\u3001SHELVED\u3001SHELVED_OFFLOADED\u3001SOFT_DELETED\u3001SUSPENDED\u3001VERIFY_RESIZE\n\n\u5f39\u6027\u4e91\u670d\u52a1\u5668\u72b6\u6001\u8bf4\u660e\u8bf7\u53c2\u8003[\u4e91\u670d\u52a1\u5668\u72b6\u6001](https://support.huaweicloud.com/api-ecs/ecs_08_0002.html)" - }, - { - "name": "updated", - "type": "文本", - "example": "2019-05-22T03:30:52Z", - "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u66f4\u65b0\u65f6\u95f4\u3002\n\n\u65f6\u95f4\u683c\u5f0f\u4f8b\u5982:2019-05-22T03:30:52Z" - }, - { - "name": "auto_terminate_time", - "type": "文本", - "example": "2020-01-19T03:30:52Z", - "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u81ea\u52a8\u91ca\u653e\u65f6\u95f4\u3002\n\n\u65f6\u95f4\u683c\u5f0f\u4f8b\u5982:2020-01-19T03:30:52Z" - }, - { - "name": "hostId", - "type": "文本", - "example": "c7145889b2e3202cd295ceddb1742ff8941b827b586861fd0acedf64", - "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u6240\u5728\u4e3b\u673a\u7684\u4e3b\u673aID\u3002" - }, - { - "name": "OS-EXT-SRV-ATTR:host", - "type": "文本", - "example": "pod01.cn-north-1c", - "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u6240\u5728\u4e3b\u673a\u7684\u4e3b\u673a\u540d\u79f0\u3002" - }, - { - "name": "addresses", - "type": "json", - "example": null, - "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u7684\u7f51\u7edc\u5c5e\u6027\u3002" - }, - { - "name": "key_name", - "type": "文本", - "example": "KeyPair-test", - "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u4f7f\u7528\u7684\u5bc6\u94a5\u5bf9\u540d\u79f0\u3002" - }, - { - "name": "image", - "type": "json", - "example": null, - "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u955c\u50cf\u4fe1\u606f\u3002" - }, - { - "name": "OS-EXT-STS:task_state", - "type": "文本", - "example": "rebooting", - "desc": "\u6269\u5c55\u5c5e\u6027,\u5f39\u6027\u4e91\u670d\u52a1\u5668\u5f53\u524d\u4efb\u52a1\u7684\u72b6\u6001\u3002\n\n\u53d6\u503c\u8303\u56f4\u8bf7\u53c2\u8003[\u4e91\u670d\u52a1\u5668\u72b6\u6001](https://support.huaweicloud.com/api-ecs/ecs_08_0002.html)\u88683\u3002" - }, - { - "name": "OS-EXT-STS:vm_state", - "type": "文本", - "example": "active", - "desc": "\u6269\u5c55\u5c5e\u6027,\u5f39\u6027\u4e91\u670d\u52a1\u5668\u5f53\u524d\u72b6\u6001\u3002\n\n\u4e91\u670d\u52a1\u5668\u72b6\u6001\u8bf4\u660e\u8bf7\u53c2\u8003[\u4e91\u670d\u52a1\u5668\u72b6\u6001](https://support.huaweicloud.com/api-ecs/ecs_08_0002.html)\u3002" - }, - { - "name": "OS-EXT-SRV-ATTR:instance_name", - "type": "文本", - "example": "instance-0048a91b", - "desc": "\u6269\u5c55\u5c5e\u6027,\u5f39\u6027\u4e91\u670d\u52a1\u5668\u522b\u540d\u3002" - }, - { - "name": "OS-EXT-SRV-ATTR:hypervisor_hostname", - "type": "文本", - "example": "nova022@36", - "desc": "\u6269\u5c55\u5c5e\u6027,\u5f39\u6027\u4e91\u670d\u52a1\u5668\u6240\u5728\u865a\u62df\u5316\u4e3b\u673a\u540d\u3002" - }, - { - "name": "flavor", - "type": "json", - "example": null, - "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u89c4\u683c\u4fe1\u606f\u3002" - }, - { - "name": "id", - "type": "文本", - "example": "4f4b3dfa-eb70-47cf-a60a-998a53bd6666", - "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668ID,\u683c\u5f0f\u4e3aUUID\u3002" - }, - { - "name": "security_groups", - "type": "json", - "example": { - "$ref": "#/definitions/ServerSecurityGroup" - }, - "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u6240\u5c5e\u5b89\u5168\u7ec4\u5217\u8868\u3002" - }, - { - "name": "OS-EXT-AZ:availability_zone", - "type": "文本", - "example": "cn-north-1c", - "desc": "\u6269\u5c55\u5c5e\u6027,\u5f39\u6027\u4e91\u670d\u52a1\u5668\u6240\u5728\u53ef\u7528\u533a\u540d\u79f0\u3002" - }, - { - "name": "user_id", - "type": "文本", - "example": "05498fe56b8010d41f7fc01e280b6666", - "desc": "\u521b\u5efa\u5f39\u6027\u4e91\u670d\u52a1\u5668\u7684\u7528\u6237ID,\u683c\u5f0f\u4e3aUUID\u3002" - }, - { - "name": "name", - "type": "文本", - "example": "ecs-test-server", - "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u540d\u79f0\u3002" - }, - { - "name": "created", - "type": "文本", - "example": "2017-07-15T11:30:52Z", - "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u521b\u5efa\u65f6\u95f4\u3002\n\n\u65f6\u95f4\u683c\u5f0f\u4f8b\u5982:2019-05-22T03:19:19Z" - }, - { - "name": "tenant_id", - "type": "文本", - "example": "743b4c0428d94531b9f2add666646666", - "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u6240\u5c5e\u79df\u6237ID,\u5373\u9879\u76eeid,\u548cproject_id\u8868\u793a\u76f8\u540c\u7684\u6982\u5ff5,\u683c\u5f0f\u4e3aUUID\u3002" - }, - { - "name": "OS-DCF:diskConfig", - "type": "文本", - "example": "AUTO", - "desc": "\u6269\u5c55\u5c5e\u6027, diskConfig\u7684\u7c7b\u578b\u3002\n\n- MANUAL,\u955c\u50cf\u7a7a\u95f4\u4e0d\u4f1a\u6269\u5c55\u3002\n- AUTO,\u7cfb\u7edf\u76d8\u955c\u50cf\u7a7a\u95f4\u4f1a\u81ea\u52a8\u6269\u5c55\u4e3a\u4e0eflavor\u5927\u5c0f\u4e00\u81f4\u3002" - }, - { - "name": "accessIPv4", - "type": "文本", - "example": null, - "desc": "\u9884\u7559\u5c5e\u6027\u3002" - }, - { - "name": "accessIPv6", - "type": "文本", - "example": null, - "desc": "\u9884\u7559\u5c5e\u6027\u3002" - }, - { - "name": "fault", - "type": "文本", - "example": null, - "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u6545\u969c\u4fe1\u606f\u3002\n\n\u53ef\u9009\u53c2\u6570,\u5728\u5f39\u6027\u4e91\u670d\u52a1\u5668\u72b6\u6001\u4e3aERROR\u4e14\u5b58\u5728\u5f02\u5e38\u7684\u60c5\u51b5\u4e0b\u8fd4\u56de\u3002" - }, - { - "name": "progress", - "type": "整数", - "example": null, - "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u8fdb\u5ea6\u3002" - }, - { - "name": "OS-EXT-STS:power_state", - "type": "整数", - "example": 4, - "desc": "\u6269\u5c55\u5c5e\u6027,\u5f39\u6027\u4e91\u670d\u52a1\u5668\u7535\u6e90\u72b6\u6001\u3002" - }, - { - "name": "config_drive", - "type": "文本", - "example": null, - "desc": "config drive\u4fe1\u606f\u3002" - }, - { - "name": "metadata", - "type": "json", - "example": null, - "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u5143\u6570\u636e\u3002\n\n> \u8bf4\u660e:\n> \n> \u5143\u6570\u636e\u5305\u542b\u7cfb\u7edf\u9ed8\u8ba4\u6dfb\u52a0\u5b57\u6bb5\u548c\u7528\u6237\u8bbe\u7f6e\u7684\u5b57\u6bb5\u3002\n\n\u7cfb\u7edf\u9ed8\u8ba4\u6dfb\u52a0\u5b57\u6bb5\n\n1. charging_mode\n\u4e91\u670d\u52a1\u5668\u7684\u8ba1\u8d39\u7c7b\u578b\u3002\n\n- \u201c0\u201d:\u6309\u9700\u8ba1\u8d39(\u5373postPaid-\u540e\u4ed8\u8d39\u65b9\u5f0f)\u3002\n- \u201c1\u201d:\u6309\u5305\u5e74\u5305\u6708\u8ba1\u8d39(\u5373prePaid-\u9884\u4ed8\u8d39\u65b9\u5f0f)\u3002\"2\":\u7ade\u4ef7\u5b9e\u4f8b\u8ba1\u8d39\n\n2. metering.order_id\n\u6309\u201c\u5305\u5e74/\u5305\u6708\u201d\u8ba1\u8d39\u7684\u4e91\u670d\u52a1\u5668\u5bf9\u5e94\u7684\u8ba2\u5355ID\u3002\n\n3. metering.product_id\n\u6309\u201c\u5305\u5e74/\u5305\u6708\u201d\u8ba1\u8d39\u7684\u4e91\u670d\u52a1\u5668\u5bf9\u5e94\u7684\u4ea7\u54c1ID\u3002\n\n4. vpc_id\n\u4e91\u670d\u52a1\u5668\u6240\u5c5e\u7684\u865a\u62df\u79c1\u6709\u4e91ID\u3002\n\n5. EcmResStatus\n\u4e91\u670d\u52a1\u5668\u7684\u51bb\u7ed3\u72b6\u6001\u3002\n\n- normal:\u4e91\u670d\u52a1\u5668\u6b63\u5e38\u72b6\u6001(\u672a\u88ab\u51bb\u7ed3)\u3002\n- freeze:\u4e91\u670d\u52a1\u5668\u88ab\u51bb\u7ed3\u3002\n\n> \u5f53\u4e91\u670d\u52a1\u5668\u88ab\u51bb\u7ed3\u6216\u8005\u89e3\u51bb\u540e,\u7cfb\u7edf\u9ed8\u8ba4\u6dfb\u52a0\u8be5\u5b57\u6bb5,\u4e14\u8be5\u5b57\u6bb5\u5fc5\u9009\u3002\n\n6. metering.image_id\n\u4e91\u670d\u52a1\u5668\u64cd\u4f5c\u7cfb\u7edf\u5bf9\u5e94\u7684\u955c\u50cfID\n\n7. metering.imagetype\n\u955c\u50cf\u7c7b\u578b,\u76ee\u524d\u652f\u6301:\n\n- \u516c\u5171\u955c\u50cf(gold)\n- \u79c1\u6709\u955c\u50cf(private)\n- \u5171\u4eab\u955c\u50cf(shared)\n\n8. metering.resourcespeccode\n\u4e91\u670d\u52a1\u5668\u5bf9\u5e94\u7684\u8d44\u6e90\u89c4\u683c\u3002\n\n9. image_name\n\u4e91\u670d\u52a1\u5668\u64cd\u4f5c\u7cfb\u7edf\u5bf9\u5e94\u7684\u955c\u50cf\u540d\u79f0\u3002\n\n10. os_bit\n\u64cd\u4f5c\u7cfb\u7edf\u4f4d\u6570,\u4e00\u822c\u53d6\u503c\u4e3a\u201c32\u201d\u6216\u8005\u201c64\u201d\u3002\n\n11. lockCheckEndpoint\n\u56de\u8c03URL,\u7528\u4e8e\u68c0\u67e5\u5f39\u6027\u4e91\u670d\u52a1\u5668\u7684\u52a0\u9501\u662f\u5426\u6709\u6548\u3002\n\n- \u5982\u679c\u6709\u6548,\u5219\u4e91\u670d\u52a1\u5668\u4fdd\u6301\u9501\u5b9a\u72b6\u6001\u3002\n- \u5982\u679c\u65e0\u6548,\u89e3\u9664\u9501\u5b9a\u72b6\u6001,\u5220\u9664\u5931\u6548\u7684\u9501\u3002\n\n12. lockSource\n\u5f39\u6027\u4e91\u670d\u52a1\u5668\u6765\u81ea\u54ea\u4e2a\u670d\u52a1\u3002\u8ba2\u5355\u52a0\u9501(ORDER)\n\n13. lockSourceId\n\u5f39\u6027\u4e91\u670d\u52a1\u5668\u7684\u52a0\u9501\u6765\u81ea\u54ea\u4e2aID\u3002lockSource\u4e3a\u201cORDER\u201d\u65f6,lockSourceId\u4e3a\u8ba2\u5355ID\u3002\n\n14. lockScene\n\u5f39\u6027\u4e91\u670d\u52a1\u5668\u7684\u52a0\u9501\u7c7b\u578b\u3002\n\n- \u6309\u9700\u8f6c\u5305\u5468\u671f(TO_PERIOD_LOCK)\n\n15. virtual_env_type\n\n- IOS\u955c\u50cf\u521b\u5efa\u865a\u62df\u673a,\"virtual_env_type\": \"IsoImage\" \u5c5e\u6027;\n- \u975eIOS\u955c\u50cf\u521b\u5efa\u865a\u62df\u673a,\u572819.5.0\u7248\u672c\u4ee5\u540e\u521b\u5efa\u7684\u865a\u62df\u673a\u5c06\u4e0d\u4f1a\u6dfb\u52a0virtual_env_type \u5c5e\u6027,\u800c\u5728\u6b64\u4e4b\u524d\u7684\u7248\u672c\u521b\u5efa\u7684\u865a\u62df\u673a\u53ef\u80fd\u4f1a\u8fd4\u56de\"virtual_env_type\": \"FusionCompute\"\u5c5e\u6027 \u3002\n\n> virtual_env_type\u5c5e\u6027\u4e0d\u5141\u8bb8\u7528\u6237\u589e\u52a0\u3001\u5220\u9664\u548c\u4fee\u6539\u3002\n\n16. metering.resourcetype\n\u4e91\u670d\u52a1\u5668\u5bf9\u5e94\u7684\u8d44\u6e90\u7c7b\u578b\u3002\n\n17. os_type\n\u64cd\u4f5c\u7cfb\u7edf\u7c7b\u578b,\u53d6\u503c\u4e3a:Linux\u3001Windows\u3002\n\n18. cascaded.instance_extrainfo\n\u7cfb\u7edf\u5185\u90e8\u865a\u62df\u673a\u6269\u5c55\u4fe1\u606f\u3002\n\n19. __support_agent_list\n\u4e91\u670d\u52a1\u5668\u662f\u5426\u652f\u6301\u4f01\u4e1a\u4e3b\u673a\u5b89\u5168\u3001\u4e3b\u673a\u76d1\u63a7\u3002\n\n- \u201chss\u201d:\u4f01\u4e1a\u4e3b\u673a\u5b89\u5168\n- \u201cces\u201d:\u4e3b\u673a\u76d1\u63a7\n\n20. agency_name\n\u59d4\u6258\u7684\u540d\u79f0\u3002\n\n\u59d4\u6258\u662f\u7531\u79df\u6237\u7ba1\u7406\u5458\u5728\u7edf\u4e00\u8eab\u4efd\u8ba4\u8bc1\u670d\u52a1(Identity and Access Management,IAM)\u4e0a\u521b\u5efa\u7684,\u53ef\u4ee5\u4e3a\u5f39\u6027\u4e91\u670d\u52a1\u5668\u63d0\u4f9b\u8bbf\u95ee\u4e91\u670d\u52a1\u7684\u4e34\u65f6\u51ed\u8bc1\u3002" - }, - { - "name": "OS-SRV-USG:launched_at", - "type": "文本", - "example": "2018-08-15T14:21:22.000000", - "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u542f\u52a8\u65f6\u95f4\u3002\u65f6\u95f4\u683c\u5f0f\u4f8b\u5982:2019-05-22T03:23:59.000000" - }, - { - "name": "OS-SRV-USG:terminated_at", - "type": "文本", - "example": "2019-05-22T03:23:59.000000", - "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u5220\u9664\u65f6\u95f4\u3002\n\n\u65f6\u95f4\u683c\u5f0f\u4f8b\u5982:2019-05-22T03:23:59.000000" - }, - { - "name": "os-extended-volumes:volumes_attached", - "type": "json", - "example": { - "$ref": "#/definitions/ServerExtendVolumeAttachment" - }, - "desc": "\u6302\u8f7d\u5230\u5f39\u6027\u4e91\u670d\u52a1\u5668\u4e0a\u7684\u78c1\u76d8\u3002" - }, - { - "name": "description", - "type": "文本", - "example": "ecs description", - "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u7684\u63cf\u8ff0\u4fe1\u606f\u3002" - }, - { - "name": "host_status", - "type": "文本", - "example": "UP", - "desc": "nova-compute\u72b6\u6001\u3002\n\n- UP:\u670d\u52a1\u6b63\u5e38\n- UNKNOWN:\u72b6\u6001\u672a\u77e5\n- DOWN:\u670d\u52a1\u5f02\u5e38\n- MAINTENANCE:\u7ef4\u62a4\u72b6\u6001\n- \u7a7a\u5b57\u7b26\u4e32:\u5f39\u6027\u4e91\u670d\u52a1\u5668\u65e0\u4e3b\u673a\u4fe1\u606f" - }, - { - "name": "OS-EXT-SRV-ATTR:hostname", - "type": "文本", - "example": null, - "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u7684\u4e3b\u673a\u540d\u3002" - }, - { - "name": "OS-EXT-SRV-ATTR:reservation_id", - "type": "文本", - "example": "r-f06p3js8", - "desc": "\u6279\u91cf\u521b\u5efa\u573a\u666f,\u5f39\u6027\u4e91\u670d\u52a1\u5668\u7684\u9884\u7559ID\u3002" - }, - { - "name": "OS-EXT-SRV-ATTR:launch_index", - "type": "整数", - "example": null, - "desc": "\u6279\u91cf\u521b\u5efa\u573a\u666f,\u5f39\u6027\u4e91\u670d\u52a1\u5668\u7684\u542f\u52a8\u987a\u5e8f\u3002" - }, - { - "name": "OS-EXT-SRV-ATTR:kernel_id", - "type": "文本", - "example": null, - "desc": "\u82e5\u4f7f\u7528AMI\u683c\u5f0f\u7684\u955c\u50cf,\u5219\u8868\u793akernel image\u7684UUID;\u5426\u5219,\u7559\u7a7a\u3002" - }, - { - "name": "OS-EXT-SRV-ATTR:ramdisk_id", - "type": "文本", - "example": null, - "desc": "\u82e5\u4f7f\u7528AMI\u683c\u5f0f\u955c\u50cf,\u5219\u8868\u793aramdisk image\u7684UUID;\u5426\u5219,\u7559\u7a7a\u3002" - }, - { - "name": "OS-EXT-SRV-ATTR:root_device_name", - "type": "文本", - "example": "/dev/vda", - "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u7cfb\u7edf\u76d8\u7684\u8bbe\u5907\u540d\u79f0\u3002" - }, - { - "name": "OS-EXT-SRV-ATTR:user_data", - "type": "文本", - "example": "IyEvYmluL2Jhc2gKZWNobyAncm9vdDokNiRjcGRkSjckWm5WZHNiR253Z0l0SGlxUjZxbWtLTlJaeU9lZUtKd3dPbG9XSFdUeGFzWjA1STYwdnJYRTdTUTZGbEpFbWlXZ21WNGNmZ1pac1laN1BkMTBLRndyeC8nIHwgY2hwYXNzd2Q6666", - "desc": "\u521b\u5efa\u5f39\u6027\u4e91\u670d\u52a1\u5668\u65f6\u6307\u5b9a\u7684user_data\u3002" - }, - { - "name": "locked", - "type": "boolean", - "example": null, - "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u662f\u5426\u4e3a\u9501\u5b9a\u72b6\u6001\u3002\n\n- true:\u9501\u5b9a\n- false:\u672a\u9501\u5b9a" - }, - { - "name": "tags", - "type": "文本、多值", - "example": { - "type": "文本" - }, - "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u6807\u7b7e\u3002" - }, - { - "name": "os:scheduler_hints", - "type": "json", - "example": null, - "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u8c03\u5ea6\u4fe1\u606f" - }, - { - "name": "enterprise_project_id", - "type": "文本", - "example": "0", - "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u6240\u5c5e\u7684\u4f01\u4e1a\u9879\u76eeID\u3002" - }, - { - "name": "sys_tags", - "type": "文本、多值", - "example": { - "$ref": "#/definitions/ServerSystemTag" - }, - "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u7cfb\u7edf\u6807\u7b7e\u3002" - }, - { - "name": "cpu_options", - "type": "json", - "example": null, - "desc": "\u81ea\u5b9a\u4e49CPU\u9009\u9879\u3002" - }, - { - "name": "hypervisor", - "type": "文本", - "example": null, - "desc": "hypervisor\u4fe1\u606f\u3002" - } +[ + { + "name": "status", + "type": "string", + "desc": "弹性云服务器状态。\n\n取值范围:\n\nACTIVE、BUILD、DELETED、ERROR、HARD_REBOOT、MIGRATING、PAUSED、REBOOT、REBUILD、RESIZE、REVERT_RESIZE、SHUTOFF、SHELVED、SHELVED_OFFLOADED、SOFT_DELETED、SUSPENDED、VERIFY_RESIZE\n\n弹性云服务器状态说明请参考[云服务器状态](https://support.huaweicloud.com/api-ecs/ecs_08_0002.html)", + "example": "ACTIVE" + }, + { + "name": "updated", + "type": "string", + "desc": "弹性云服务器更新时间。\n\n时间格式例如:2019-05-22T03:30:52Z", + "example": "2019-05-22T03:30:52Z" + }, + { + "name": "auto_terminate_time", + "type": "string", + "desc": "弹性云服务器定时删除时间。\n\n时间格式例如:2020-01-19T03:30:52Z", + "example": "2020-01-19T03:30:52Z" + }, + { + "name": "hostId", + "type": "string", + "desc": "弹性云服务器所在主机的主机ID。", + "example": "c7145889b2e3202cd295ceddb1742ff8941b827b586861fd0acedf64" + }, + { + "name": "OS-EXT-SRV-ATTR:host", + "type": "string", + "desc": "弹性云服务器所在主机的主机名称。", + "example": "pod01.cn-north-1c" + }, + { + "name": "addresses", + "type": "object", + "desc": "弹性云服务器的网络属性。", + "example": "" + }, + { + "name": "key_name", + "type": "string", + "desc": "弹性云服务器使用的密钥对名称。", + "example": "KeyPair-test" + }, + { + "name": "image", + "type": "", + "desc": "弹性云服务器镜像信息。", + "example": "" + }, + { + "name": "OS-EXT-STS:task_state", + "type": "string", + "desc": "扩展属性,弹性云服务器当前任务的状态。\n\n取值范围请参考[云服务器状态](https://support.huaweicloud.com/api-ecs/ecs_08_0002.html)表3。", + "example": "rebooting" + }, + { + "name": "OS-EXT-STS:vm_state", + "type": "string", + "desc": "扩展属性,弹性云服务器当前状态。\n\n云服务器状态说明请参考[云服务器状态](https://support.huaweicloud.com/api-ecs/ecs_08_0002.html)。", + "example": "active" + }, + { + "name": "OS-EXT-SRV-ATTR:instance_name", + "type": "string", + "desc": "扩展属性,弹性云服务器别名。", + "example": "instance-0048a91b" + }, + { + "name": "OS-EXT-SRV-ATTR:hypervisor_hostname", + "type": "string", + "desc": "扩展属性,弹性云服务器所在虚拟化主机名。", + "example": "nova022@36" + }, + { + "name": "flavor", + "type": "", + "desc": "弹性云服务器规格信息。", + "example": "" + }, + { + "name": "id", + "type": "string", + "desc": "弹性云服务器ID,格式为UUID。", + "example": "4f4b3dfa-eb70-47cf-a60a-998a53bd6666" + }, + { + "name": "security_groups", + "type": "array", + "desc": "弹性云服务器所属安全组列表。", + "example": "" + }, + { + "name": "OS-EXT-AZ:availability_zone", + "type": "string", + "desc": "扩展属性,弹性云服务器所在可用区名称。", + "example": "cn-north-1c" + }, + { + "name": "user_id", + "type": "string", + "desc": "创建弹性云服务器的用户ID,格式为UUID。", + "example": "05498fe56b8010d41f7fc01e280b6666" + }, + { + "name": "name", + "type": "string", + "desc": "弹性云服务器名称。", + "example": "ecs-test-server" + }, + { + "name": "created", + "type": "string", + "desc": "弹性云服务器创建时间。\n\n时间格式例如:2019-05-22T03:19:19Z", + "example": "2017-07-15T11:30:52Z" + }, + { + "name": "tenant_id", + "type": "string", + "desc": "弹性云服务器所属租户ID,即项目id,和project_id表示相同的概念,格式为UUID。", + "example": "743b4c0428d94531b9f2add666646666" + }, + { + "name": "OS-DCF:diskConfig", + "type": "string", + "desc": "扩展属性, diskConfig的类型。\n\n- MANUAL,镜像空间不会扩展。\n- AUTO,系统盘镜像空间会自动扩展为与flavor大小一致。", + "example": "AUTO" + }, + { + "name": "accessIPv4", + "type": "string", + "desc": "预留属性。", + "example": "" + }, + { + "name": "accessIPv6", + "type": "string", + "desc": "预留属性。", + "example": "" + }, + { + "name": "fault", + "type": "", + "desc": "弹性云服务器故障信息。\n\n可选参数,在弹性云服务器状态为ERROR且存在异常的情况下返回。", + "example": "" + }, + { + "name": "progress", + "type": "integer", + "desc": "弹性云服务器进度。", + "example": 0 + }, + { + "name": "OS-EXT-STS:power_state", + "type": "integer", + "desc": "扩展属性,弹性云服务器电源状态。", + "example": 4 + }, + { + "name": "config_drive", + "type": "string", + "desc": "config drive信息。", + "example": "" + }, + { + "name": "metadata", + "type": "object", + "desc": "弹性云服务器元数据。\n\n> 说明:\n> \n> 元数据包含系统默认添加字段和用户设置的字段。\n\n系统默认添加字段\n\n1. charging_mode\n云服务器的计费类型。\n\n- “0”:按需计费(即postPaid-后付费方式)。\n- “1”:按包年包月计费(即prePaid-预付费方式)。\"2\":竞价实例计费\n\n2. metering.order_id\n按“包年/包月”计费的云服务器对应的订单ID。\n\n3. metering.product_id\n按“包年/包月”计费的云服务器对应的产品ID。\n\n4. vpc_id\n云服务器所属的虚拟私有云ID。\n\n5. EcmResStatus\n云服务器的冻结状态。\n\n- normal:云服务器正常状态(未被冻结)。\n- freeze:云服务器被冻结。\n\n> 当云服务器被冻结或者解冻后,系统默认添加该字段,且该字段必选。\n\n6. metering.image_id\n云服务器操作系统对应的镜像ID\n\n7. metering.imagetype\n镜像类型,目前支持:\n\n- 公共镜像(gold)\n- 私有镜像(private)\n- 共享镜像(shared)\n\n8. metering.resourcespeccode\n云服务器对应的资源规格。\n\n9. image_name\n云服务器操作系统对应的镜像名称。\n\n10. os_bit\n操作系统位数,一般取值为“32”或者“64”。\n\n11. lockCheckEndpoint\n回调URL,用于检查弹性云服务器的加锁是否有效。\n\n- 如果有效,则云服务器保持锁定状态。\n- 如果无效,解除锁定状态,删除失效的锁。\n\n12. lockSource\n弹性云服务器来自哪个服务。订单加锁(ORDER)\n\n13. lockSourceId\n弹性云服务器的加锁来自哪个ID。lockSource为“ORDER”时,lockSourceId为订单ID。\n\n14. lockScene\n弹性云服务器的加锁类型。\n\n- 按需转包周期(TO_PERIOD_LOCK)\n\n15. virtual_env_type\n\n- IOS镜像创建虚拟机,\"virtual_env_type\": \"IsoImage\" 属性;\n- 非IOS镜像创建虚拟机,在19.5.0版本以后创建的虚拟机将不会添加virtual_env_type 属性,而在此之前的版本创建的虚拟机可能会返回\"virtual_env_type\": \"FusionCompute\"属性 。\n\n> virtual_env_type属性不允许用户增加、删除和修改。\n\n16. metering.resourcetype\n云服务器对应的资源类型。\n\n17. os_type\n操作系统类型,取值为:Linux、Windows。\n\n18. cascaded.instance_extrainfo\n系统内部虚拟机扩展信息。\n\n19. __support_agent_list\n云服务器是否支持企业主机安全、主机监控。\n\n- “hss”:企业主机安全\n- “ces”:主机监控\n\n20. agency_name\n委托的名称。\n\n委托是由租户管理员在统一身份认证服务(Identity and Access Management,IAM)上创建的,可以为弹性云服务器提供访问云服务的临时凭证。", + "example": "" + }, + { + "name": "OS-SRV-USG:launched_at", + "type": "string", + "desc": "弹性云服务器启动时间。时间格式例如:2019-05-22T03:23:59.000000", + "example": "2018-08-15T14:21:22.000000" + }, + { + "name": "OS-SRV-USG:terminated_at", + "type": "string", + "desc": "弹性云服务器删除时间。\n\n时间格式例如:2019-05-22T03:23:59.000000", + "example": "2019-05-22T03:23:59.000000" + }, + { + "name": "os-extended-volumes:volumes_attached", + "type": "array", + "desc": "挂载到弹性云服务器上的磁盘。", + "example": "" + }, + { + "name": "description", + "type": "string", + "desc": "弹性云服务器的描述信息。", + "example": "ecs description" + }, + { + "name": "host_status", + "type": "string", + "desc": "nova-compute状态。\n\n- UP:服务正常\n- UNKNOWN:状态未知\n- DOWN:服务异常\n- MAINTENANCE:维护状态\n- 空字符串:弹性云服务器无主机信息", + "example": "UP" + }, + { + "name": "OS-EXT-SRV-ATTR:hostname", + "type": "string", + "desc": "弹性云服务器的主机名。", + "example": "" + }, + { + "name": "OS-EXT-SRV-ATTR:reservation_id", + "type": "string", + "desc": "批量创建场景,弹性云服务器的预留ID。", + "example": "r-f06p3js8" + }, + { + "name": "OS-EXT-SRV-ATTR:launch_index", + "type": "integer", + "desc": "批量创建场景,弹性云服务器的启动顺序。", + "example": 0 + }, + { + "name": "OS-EXT-SRV-ATTR:kernel_id", + "type": "string", + "desc": "若使用AMI格式的镜像,则表示kernel image的UUID;否则,留空。", + "example": "" + }, + { + "name": "OS-EXT-SRV-ATTR:ramdisk_id", + "type": "string", + "desc": "若使用AMI格式镜像,则表示ramdisk image的UUID;否则,留空。", + "example": "" + }, + { + "name": "OS-EXT-SRV-ATTR:root_device_name", + "type": "string", + "desc": "弹性云服务器系统盘的设备名称。", + "example": "/dev/vda" + }, + { + "name": "OS-EXT-SRV-ATTR:user_data", + "type": "string", + "desc": "创建弹性云服务器时指定的user_data。", + "example": "IyEvYmluL2Jhc2gKZWNobyAncm9vdDokNiRjcGRkSjckWm5WZHNiR253Z0l0SGlxUjZxbWtLTlJaeU9lZUtKd3dPbG9XSFdUeGFzWjA1STYwdnJYRTdTUTZGbEpFbWlXZ21WNGNmZ1pac1laN1BkMTBLRndyeC8nIHwgY2hwYXNzd2Q6666" + }, + { + "name": "locked", + "type": "boolean", + "desc": "弹性云服务器是否为锁定状态。\n\n- true:锁定\n- false:未锁定", + "example": false + }, + { + "name": "tags", + "type": "array", + "desc": "弹性云服务器标签。", + "example": "" + }, + { + "name": "os:scheduler_hints", + "type": "", + "desc": "弹性云服务器调度信息", + "example": "" + }, + { + "name": "enterprise_project_id", + "type": "string", + "desc": "弹性云服务器所属的企业项目ID。", + "example": "0" + }, + { + "name": "sys_tags", + "type": "array", + "desc": "弹性云服务器系统标签。", + "example": "" + }, + { + "name": "cpu_options", + "type": "", + "desc": "自定义CPU选项。", + "example": "" + }, + { + "name": "hypervisor", + "type": "", + "desc": "hypervisor信息。", + "example": "" + } ] \ No newline at end of file diff --git a/cmdb-api/api/lib/cmdb/auto_discovery/templates/tencent_cvm.json b/cmdb-api/api/lib/cmdb/auto_discovery/templates/tencent_cvm.json index 9d5f461..1f7c3f9 100644 --- a/cmdb-api/api/lib/cmdb/auto_discovery/templates/tencent_cvm.json +++ b/cmdb-api/api/lib/cmdb/auto_discovery/templates/tencent_cvm.json @@ -1,297 +1,248 @@ -[ - { - "name": "Placement", - "type": "json", - "desc": "实例所在的位置。", - "example": { - "HostId": "host-h3m57oik", - "ProjectId": 1174660, - "HostIds": [], - "Zone": "ap-guangzhou-1", - "HostIps": [] - } - }, - { - "name": "InstanceId", - "type": "文本", - "desc": "实例ID。", - "example": "ins-xlsyru2j" - }, - { - "name": "InstanceType", - "type": "文本", - "desc": "实例机型。", - "example": "S2.SMALL2" - }, - { - "name": "CPU", - "type": "整数", - "desc": "实例的CPU核数,单位:核。", - "example": 1 - }, - { - "name": "Memory", - "type": "整数", - "desc": "实例内存容量,单位:GB。", - "example": 1 - }, - { - "name": "RestrictState", - "type": "文本", - "desc": "实例业务状态。取值范围: NORMAL:表示正常状态的实例 EXPIRED:表示过期的实例 PROTECTIVELY_ISOLATED:表示被安全隔离的实例。", - "example": "PROTECTIVELY_ISOLATED" - }, - { - "name": "InstanceName", - "type": "文本", - "desc": "实例名称。", - "example": "test" - }, - { - "name": "InstanceChargeType", - "type": "文本", - "desc": "实例计费模式。取值范围: PREPAID:表示预付费,即包年包月 POSTPAID_BY_HOUR:表示后付费,即按量计费 CDHPAID:专用宿主机付费,即只对专用宿主机计费,不对专用宿主机上的实例计费。 SPOTPAID:表示竞价实例付费。", - "example": "POSTPAID_BY_HOUR" - }, - { - "name": "SystemDisk", - "type": "json", - "desc": "实例系统盘信息。", - "example": { - "DiskSize": 50, - "CdcId": null, - "DiskId": "disk-czsodtl1", - "DiskType": "CLOUD_SSD" - } - }, - { - "name": "DataDisks", - "type": "json", - "desc": "实例数据盘信息。", - "example": [ - { - "DeleteWithInstance": true, - "Encrypt": true, - "CdcId": null, - "DiskType": "CLOUD_SSD", - "ThroughputPerformance": 0, - "KmsKeyId": null, - "DiskSize": 50, - "SnapshotId": null, - "DiskId": "disk-bzsodtn1" - } - ] - }, - { - "name": "PrivateIpAddresses", - "type": "文本、多值", - "desc": "实例主网卡的内网IP列表。", - "example": [ - "172.16.32.78" - ] - }, - { - "name": "PublicIpAddresses", - "type": "文本、多值", - "desc": "实例主网卡的公网IP列表。 注意:此字段可能返回 null,表示取不到有效值。", - "example": [ - "123.207.11.190" - ] - }, - { - "name": "InternetAccessible", - "type": "json", - "desc": "实例带宽信息。", - "example": { - "PublicIpAssigned": true, - "InternetChargeType": "TRAFFIC_POSTPAID_BY_HOUR", - "BandwidthPackageId": null, - "InternetMaxBandwidthOut": 1 - } - }, - { - "name": "VirtualPrivateCloud", - "type": "json", - "desc": "实例所属虚拟私有网络信息。", - "example": { - "SubnetId": "subnet-mv4sn55k", - "AsVpcGateway": false, - "Ipv6AddressCount": 1, - "VpcId": "vpc-m0cnatxj", - "PrivateIpAddresses": [ - "172.16.3.59" - ] - } - }, - { - "name": "ImageId", - "type": "文本", - "desc": "生产实例所使用的镜像ID。", - "example": "img-8toqc6s3" - }, - { - "name": "RenewFlag", - "type": "文本", - "desc": "自动续费标识。取值范围: NOTIFY_AND_MANUAL_RENEW:表示通知即将过期,但不自动续费 NOTIFY_AND_AUTO_RENEW:表示通知即将过期,而且自动续费 DISABLE_NOTIFY_AND_MANUAL_RENEW:表示不通知即将过期,也不自动续费。 注意:后付费模式本项为null", - "example": "NOTIFY_AND_MANUAL_RENEW" - }, - { - "name": "CreatedTime", - "type": "json", - "desc": "创建时间。按照ISO8601标准表示,并且使用UTC时间。格式为:YYYY-MM-DDThh:mm:ssZ。", - "example": "2020-09-22T00:00:00+00:00" - }, - { - "name": "ExpiredTime", - "type": "json", - "desc": "到期时间。按照ISO8601标准表示,并且使用UTC时间。格式为:YYYY-MM-DDThh:mm:ssZ。注意:后付费模式本项为null", - "example": "2020-09-22T00:00:00+00:00" - }, - { - "name": "OsName", - "type": "文本", - "desc": "操作系统名称。", - "example": "CentOS 7.4 64bit" - }, - { - "name": "SecurityGroupIds", - "type": "文本、多值", - "desc": "实例所属安全组。该参数可以通过调用 DescribeSecurityGroups 的返回值中的sgId字段来获取。", - "example": [ - "sg-p1ezv4wz" - ] - }, - { - "name": "LoginSettings", - "type": "json", - "desc": "实例登录设置。目前只返回实例所关联的密钥。", - "example": { - "Password": "123qwe!@#QWE", - "KeepImageLogin": "False", - "KeyIds": [ - "skey-b4vakk62" - ] - } - }, - { - "name": "InstanceState", - "type": "文本", - "desc": "实例状态。取值范围: PENDING:表示创建中 LAUNCH_FAILED:表示创建失败 RUNNING:表示运行中 STOPPED:表示关机 STARTING:表示开机中 STOPPING:表示关机中 REBOOTING:表示重启中 SHUTDOWN:表示停止待销毁 TERMINATING:表示销毁中。", - "example": "RUNNING" - }, - { - "name": "Tags", - "type": "json", - "desc": "实例关联的标签列表。", - "example": [ - { - "Value": "test", - "Key": "test" - } - ] - }, - { - "name": "StopChargingMode", - "type": "文本", - "desc": "实例的关机计费模式。 取值范围: KEEP_CHARGING:关机继续收费 STOP_CHARGING:关机停止收费NOT_APPLICABLE:实例处于非关机状态或者不适用关机停止计费的条件", - "example": "NOT_APPLICABLE" - }, - { - "name": "Uuid", - "type": "文本", - "desc": "实例全局唯一ID", - "example": "e85f1388-0422-410d-8e50-bef540e78c18" - }, - { - "name": "LatestOperation", - "type": "文本", - "desc": "实例的最新操作。例:StopInstances、ResetInstance。 注意:此字段可能返回 null,表示取不到有效值。", - "example": "ResetInstancesType" - }, - { - "name": "LatestOperationState", - "type": "文本", - "desc": "实例的最新操作状态。取值范围: SUCCESS:表示操作成功 OPERATING:表示操作执行中 FAILED:表示操作失败 注意:此字段可能返回 null,表示取不到有效值。", - "example": "SUCCESS" - }, - { - "name": "LatestOperationRequestId", - "type": "文本", - "desc": "实例最新操作的唯一请求 ID。 注意:此字段可能返回 null,表示取不到有效值。", - "example": "c7de1287-061d-4ace-8caf-6ad8e5a2f29a" - }, - { - "name": "DisasterRecoverGroupId", - "type": "文本", - "desc": "分散置放群组ID。 注意:此字段可能返回 null,表示取不到有效值。", - "example": "" - }, - { - "name": "IPv6Addresses", - "type": "文本、多值", - "desc": "实例的IPv6地址。 注意:此字段可能返回 null,表示取不到有效值。", - "example": [ - "2001:0db8:86a3:08d3:1319:8a2e:0370:7344" - ] - }, - { - "name": "CamRoleName", - "type": "文本", - "desc": "CAM角色名。 注意:此字段可能返回 null,表示取不到有效值。", - "example": "" - }, - { - "name": "HpcClusterId", - "type": "文本", - "desc": "高性能计算集群ID。 注意:此字段可能返回 null,表示取不到有效值。", - "example": "" - }, - { - "name": "RdmaIpAddresses", - "type": "文本、多值", - "desc": "高性能计算集群IP列表。 注意:此字段可能返回 null,表示取不到有效值。", - "example": [] - }, - { - "name": "IsolatedSource", - "type": "文本", - "desc": "实例隔离类型。取值范围: ARREAR:表示欠费隔离 EXPIRE:表示到期隔离 MANMADE:表示主动退还隔离 NOTISOLATED:表示未隔离 注意:此字段可能返回 null,表示取不到有效值。", - "example": "NOTISOLATED" - }, - { - "name": "GPUInfo", - "type": "json", - "desc": "GPU信息。如果是gpu类型子机,该值会返回GPU信息,如果是其他类型子机则不返回。 注意:此字段可能返回 null,表示取不到有效值。", - "example": null - }, - { - "name": "LicenseType", - "type": "文本", - "desc": "实例的操作系统许可类型,默认为TencentCloud", - "example": null - }, - { - "name": "DisableApiTermination", - "type": "Boolean", - "desc": "实例销毁保护标志,表示是否允许通过api接口删除实例。取值范围: TRUE:表示开启实例保护,不允许通过api接口删除实例 FALSE:表示关闭实例保护,允许通过api接口删除实例 默认取值:FALSE。", - "example": null - }, - { - "name": "DefaultLoginUser", - "type": "文本", - "desc": "默认登录用户。", - "example": null - }, - { - "name": "DefaultLoginPort", - "type": "整数", - "desc": "默认登录端口。", - "example": null - }, - { - "name": "LatestOperationErrorMsg", - "type": "文本", - "desc": "实例的最新操作错误信息。 注意:此字段可能返回 null,表示取不到有效值。", - "example": null - } +[ + { + "name": "Placement", + "type": "Placement", + "desc": "实例所在的位置。", + "example": "" + }, + { + "name": "InstanceId", + "type": "String", + "desc": "实例ID。", + "example": "ins-9bxebleo" + }, + { + "name": "InstanceType", + "type": "String", + "desc": "实例机型。", + "example": "S1.SMALL1" + }, + { + "name": "CPU", + "type": "Integer", + "desc": "实例的CPU核数,单位:核。", + "example": "1" + }, + { + "name": "Memory", + "type": "Integer", + "desc": "实例内存容量,单位:GB。", + "example": "1" + }, + { + "name": "RestrictState", + "type": "String", + "desc": "NORMAL:表示正常状态的实例\nEXPIRED:表示过期的实例\nPROTECTIVELY_ISOLATED:表示被安全隔离的实例。", + "example": "NORMAL" + }, + { + "name": "InstanceName", + "type": "String", + "desc": "实例名称。", + "example": "测试实例" + }, + { + "name": "InstanceChargeType", + "type": "String", + "desc": "PREPAID:表示预付费,即包年包月\nPOSTPAID_BY_HOUR:表示后付费,即按量计费\nCDHPAID:专用宿主机付费,即只对专用宿主机计费,不对专用宿主机上的实例计费。\nSPOTPAID:表示竞价实例付费。", + "example": "PREPAID" + }, + { + "name": "SystemDisk", + "type": "SystemDisk", + "desc": "实例系统盘信息。", + "example": "" + }, + { + "name": "DataDisks", + "type": "Array of DataDisk", + "desc": "实例数据盘信息。", + "example": "" + }, + { + "name": "PrivateIpAddresses", + "type": "Array of String", + "desc": "实例主网卡的内网IP列表。", + "example": "[\"172.16.32.78\"]" + }, + { + "name": "PublicIpAddresses", + "type": "Array of String", + "desc": "实例主网卡的公网IP列表。注意:此字段可能返回 null,表示取不到有效值。", + "example": "[\"123.207.11.190\"]" + }, + { + "name": "InternetAccessible", + "type": "InternetAccessible", + "desc": "实例带宽信息。", + "example": "" + }, + { + "name": "VirtualPrivateCloud", + "type": "VirtualPrivateCloud", + "desc": "实例所属虚拟私有网络信息。", + "example": "" + }, + { + "name": "ImageId", + "type": "String", + "desc": "生产实例所使用的镜像ID。", + "example": "img-9qabwvbn" + }, + { + "name": "RenewFlag", + "type": "String", + "desc": "NOTIFY_AND_MANUAL_RENEW:表示通知即将过期,但不自动续费\nNOTIFY_AND_AUTO_RENEW:表示通知即将过期,而且自动续费\nDISABLE_NOTIFY_AND_MANUAL_RENEW:表示不通知即将过期,也不自动续费。\n注意:后付费模式本项为null", + "example": "NOTIFY_AND_MANUAL_RENEW" + }, + { + "name": "CreatedTime", + "type": "Timestamp ISO8601", + "desc": "创建时间。按照ISO8601标准表示,并且使用UTC时间。格式为:YYYY-MM-DDThh:mm:ssZ。", + "example": "2020-03-10T02:43:51Z" + }, + { + "name": "ExpiredTime", + "type": "Timestamp ISO8601", + "desc": "到期时间。按照ISO8601标准表示,并且使用UTC时间。格式为:YYYY-MM-DDThh:mm:ssZ。注意:后付费模式本项为null", + "example": "2020-04-10T02:47:36Z" + }, + { + "name": "OsName", + "type": "String", + "desc": "操作系统名称。", + "example": "CentOS 7.6 64bit" + }, + { + "name": "SecurityGroupIds", + "type": "Array of String", + "desc": "实例所属安全组。该参数可以通过调用 DescribeSecurityGroups 的返回值中的sgId字段来获取。", + "example": "[\"sg-p1ezv4wz\"]" + }, + { + "name": "LoginSettings", + "type": "LoginSettings", + "desc": "实例登录设置。目前只返回实例所关联的密钥。", + "example": "" + }, + { + "name": "InstanceState", + "type": "String", + "desc": "PENDING:表示创建中\nLAUNCH_FAILED:表示创建失败\nRUNNING:表示运行中\nSTOPPED:表示关机\nSTARTING:表示开机中\nSTOPPING:表示关机中\nREBOOTING:表示重启中\nSHUTDOWN:表示停止待销毁\nTERMINATING:表示销毁中。", + "example": "" + }, + { + "name": "Tags", + "type": "Array of Tag", + "desc": "实例关联的标签列表。", + "example": "" + }, + { + "name": "StopChargingMode", + "type": "String", + "desc": "KEEP_CHARGING:关机继续收费\nSTOP_CHARGING:关机停止收费\nNOT_APPLICABLE:实例处于非关机状态或者不适用关机停止计费的条件", + "example": "NOT_APPLICABLE" + }, + { + "name": "Uuid", + "type": "String", + "desc": "实例全局唯一ID", + "example": "68b510db-b4c1-4630-a62b-73d0c7c970f9" + }, + { + "name": "LatestOperation", + "type": "String", + "desc": "实例的最新操作。例:StopInstances、ResetInstance。注意:此字段可能返回 null,表示取不到有效值。", + "example": "RenewInstances" + }, + { + "name": "LatestOperationState", + "type": "String", + "desc": "SUCCESS:表示操作成功\nOPERATING:表示操作执行中\nFAILED:表示操作失败注意:此字段可能返回 null,表示取不到有效值。", + "example": "SUCCESS" + }, + { + "name": "LatestOperationRequestId", + "type": "String", + "desc": "实例最新操作的唯一请求 ID。注意:此字段可能返回 null,表示取不到有效值。", + "example": "3554eb5b-1cfa-471a-ae76-dc436c9d43e8" + }, + { + "name": "DisasterRecoverGroupId", + "type": "String", + "desc": "分散置放群组ID。注意:此字段可能返回 null,表示取不到有效值。", + "example": "null" + }, + { + "name": "IPv6Addresses", + "type": "Array of String", + "desc": "实例的IPv6地址。注意:此字段可能返回 null,表示取不到有效值。", + "example": "null" + }, + { + "name": "CamRoleName", + "type": "String", + "desc": "CAM角色名。注意:此字段可能返回 null,表示取不到有效值。", + "example": "null" + }, + { + "name": "HpcClusterId", + "type": "String", + "desc": "高性能计算集群ID。注意:此字段可能返回 null,表示取不到有效值。", + "example": "null" + }, + { + "name": "RdmaIpAddresses", + "type": "Array of String", + "desc": "高性能计算集群IP列表。注意:此字段可能返回 null,表示取不到有效值。", + "example": "null" + }, + { + "name": "DedicatedClusterId", + "type": "String", + "desc": "实例所在的专用集群ID。注意:此字段可能返回 null,表示取不到有效值。", + "example": "cluster-du3jken" + }, + { + "name": "IsolatedSource", + "type": "String", + "desc": "ARREAR:表示欠费隔离\nEXPIRE:表示到期隔离\nMANMADE:表示主动退还隔离\nNOTISOLATED:表示未隔离", + "example": "" + }, + { + "name": "GPUInfo", + "type": "GPUInfo", + "desc": "GPU信息。如果是gpu类型子机,该值会返回GPU信息,如果是其他类型子机则不返回。注意:此字段可能返回 null,表示取不到有效值。", + "example": "" + }, + { + "name": "LicenseType", + "type": "String", + "desc": "实例的操作系统许可类型,默认为TencentCloud", + "example": "TencentCloud" + }, + { + "name": "DisableApiTermination", + "type": "Boolean", + "desc": "TRUE:表示开启实例保护,不允许通过api接口删除实例\nFALSE:表示关闭实例保护,允许通过api接口删除实例默认取值:FALSE。", + "example": "false" + }, + { + "name": "DefaultLoginUser", + "type": "String", + "desc": "默认登录用户。", + "example": "root" + }, + { + "name": "DefaultLoginPort", + "type": "Integer", + "desc": "默认登录端口。", + "example": "22" + }, + { + "name": "LatestOperationErrorMsg", + "type": "String", + "desc": "实例的最新操作错误信息。注意:此字段可能返回 null,表示取不到有效值。", + "example": "None" + } ] \ No newline at end of file diff --git a/cmdb-api/api/lib/cmdb/cache.py b/cmdb-api/api/lib/cmdb/cache.py index c19812f..1ed6797 100644 --- a/cmdb-api/api/lib/cmdb/cache.py +++ b/cmdb-api/api/lib/cmdb/cache.py @@ -5,10 +5,14 @@ from __future__ import unicode_literals 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 PreferenceShowAttributes +from api.models.cmdb import PreferenceTreeView from api.models.cmdb import RelationType @@ -226,7 +230,9 @@ class CITypeAttributeCache(object): class CMDBCounterCache(object): - KEY = 'CMDB::Counter' + KEY = 'CMDB::Counter::dashboard' + KEY2 = 'CMDB::Counter::adc' + KEY3 = 'CMDB::Counter::sub' @classmethod def get(cls): @@ -303,7 +309,7 @@ class CMDBCounterCache(object): s = RelSearch([i[0] for i in type_id_names], level, other_filer or '') try: - stats = s.statistics(type_ids) + stats = s.statistics(type_ids, need_filter=False) except SearchError as e: current_app.logger.error(e) return @@ -429,3 +435,47 @@ class CMDBCounterCache(object): return return numfound + + @classmethod + def flush_adc_counter(cls): + res = db.session.query(CI.type_id, CI.is_auto_discovery) + result = dict() + for i in res: + result.setdefault(i.type_id, dict(total=0, auto_discovery=0)) + result[i.type_id]['total'] += 1 + if i.is_auto_discovery: + result[i.type_id]['auto_discovery'] += 1 + + cache.set(cls.KEY2, result, timeout=0) + + return result + + @classmethod + def get_adc_counter(cls): + return cache.get(cls.KEY2) or cls.flush_adc_counter() + + @classmethod + def flush_sub_counter(cls): + result = dict(type_id2users=dict()) + + types = db.session.query(PreferenceShowAttributes.type_id, + PreferenceShowAttributes.uid, PreferenceShowAttributes.created_at).filter( + PreferenceShowAttributes.deleted.is_(False)).group_by( + PreferenceShowAttributes.uid, PreferenceShowAttributes.type_id) + for i in types: + result['type_id2users'].setdefault(i.type_id, []).append(i.uid) + + types = PreferenceTreeView.get_by(to_dict=False) + for i in types: + + result['type_id2users'].setdefault(i.type_id, []) + if i.uid not in result['type_id2users'][i.type_id]: + result['type_id2users'][i.type_id].append(i.uid) + + cache.set(cls.KEY3, result, timeout=0) + + return result + + @classmethod + def get_sub_counter(cls): + return cache.get(cls.KEY3) or cls.flush_sub_counter() diff --git a/cmdb-api/api/lib/cmdb/ci.py b/cmdb-api/api/lib/cmdb/ci.py index 1876a8f..3e7c181 100644 --- a/cmdb-api/api/lib/cmdb/ci.py +++ b/cmdb-api/api/lib/cmdb/ci.py @@ -6,6 +6,7 @@ import datetime import json import threading +import redis_lock from flask import abort from flask import current_app from flask_login import current_user @@ -14,8 +15,8 @@ from werkzeug.exceptions import BadRequest from api.extensions import db from api.extensions import rd from api.lib.cmdb.cache import AttributeCache -from api.lib.cmdb.cache import CITypeAttributesCache from api.lib.cmdb.cache import CITypeCache +from api.lib.cmdb.cache import CMDBCounterCache from api.lib.cmdb.ci_type import CITypeAttributeManager from api.lib.cmdb.ci_type import CITypeManager from api.lib.cmdb.ci_type import CITypeRelationManager @@ -45,14 +46,12 @@ from api.lib.perm.acl.acl import is_app_admin from api.lib.perm.acl.acl import validate_permission from api.lib.secrets.inner import InnerCrypt from api.lib.secrets.vault import VaultClient -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 @@ -61,8 +60,8 @@ 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 +from api.tasks.cmdb import delete_id_filter -PRIVILEGED_USERS = {"worker", "cmdb_agent", "agent"} PASSWORD_DEFAULT_SHOW = "******" @@ -218,15 +217,7 @@ class CIManager(object): @classmethod def get_ad_statistics(cls): - res = CI.get_by(to_dict=False) - result = dict() - for i in res: - result.setdefault(i.type_id, dict(total=0, auto_discovery=0)) - result[i.type_id]['total'] += 1 - if i.is_auto_discovery: - result[i.type_id]['auto_discovery'] += 1 - - return result + return CMDBCounterCache.get_adc_counter() @staticmethod def ci_is_exist(unique_key, unique_value, type_id): @@ -287,16 +278,16 @@ class CIManager(object): @staticmethod def _auto_inc_id(attr): - db.session.remove() + db.session.commit() value_table = TableMap(attr_name=attr.name).table - with Lock("auto_inc_id_{}".format(attr.name), need_lock=True): + with redis_lock.Lock(rd.r, "auto_inc_id_{}".format(attr.name)): max_v = value_table.get_by(attr_id=attr.id, only_query=True).order_by( getattr(value_table, 'value').desc()).first() if max_v is not None: return int(max_v.value) + 1 - return 1 + return 1 @classmethod def add(cls, ci_type_name, @@ -304,6 +295,7 @@ class CIManager(object): _no_attribute_policy=ExistPolicy.IGNORE, is_auto_discovery=False, _is_admin=False, + ticket_id=None, **ci_dict): """ add ci @@ -312,19 +304,24 @@ class CIManager(object): :param _no_attribute_policy: ignore or reject :param is_auto_discovery: default is False :param _is_admin: default is False + :param ticket_id: :param ci_dict: :return: """ now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') ci_type = CITypeManager.check_is_existed(ci_type_name) + raw_dict = copy.deepcopy(ci_dict) unique_key = AttributeCache.get(ci_type.unique_id) or abort( 400, ErrFormat.unique_value_not_found.format("unique_id={}".format(ci_type.unique_id))) - unique_value = ci_dict.get(unique_key.name) or ci_dict.get(unique_key.alias) or ci_dict.get(unique_key.id) - unique_value = unique_value or abort(400, ErrFormat.unique_key_required.format(unique_key.name)) + unique_value = None + if not (unique_key.default and unique_key.default.get('default') == AttributeDefaultValueEnum.AUTO_INC_ID and + not ci_dict.get(unique_key.name)): # primary key is not auto inc id + unique_value = ci_dict.get(unique_key.name) or ci_dict.get(unique_key.alias) or ci_dict.get(unique_key.id) + unique_value = unique_value or abort(400, ErrFormat.unique_key_required.format(unique_key.name)) - attrs = CITypeAttributesCache.get2(ci_type_name) + attrs = CITypeAttributeManager.get_all_attributes(ci_type.id) ci_type_attrs_name = {attr.name: attr for _, attr in attrs} ci_type_attrs_alias = {attr.alias: attr for _, attr in attrs} ci_attr2type_attr = {type_attr.attr_id: type_attr for type_attr, _ in attrs} @@ -332,8 +329,15 @@ class CIManager(object): ci = None record_id = None password_dict = {} - need_lock = current_user.username not in current_app.config.get('PRIVILEGED_USERS', PRIVILEGED_USERS) - with Lock(ci_type_name, need_lock=need_lock): + with redis_lock.Lock(rd.r, ci_type.name): + db.session.commit() + + if (unique_key.default and unique_key.default.get('default') == AttributeDefaultValueEnum.AUTO_INC_ID and + not ci_dict.get(unique_key.name)): + ci_dict[unique_key.name] = cls._auto_inc_id(unique_key) + current_app.logger.info(ci_dict[unique_key.name]) + unique_value = ci_dict[unique_key.name] + existed = cls.ci_is_exist(unique_key, unique_value, ci_type.id) if existed is not None: if exist_policy == ExistPolicy.REJECT: @@ -354,7 +358,8 @@ class CIManager(object): if attr.default.get('default') and attr.default.get('default') in ( AttributeDefaultValueEnum.CREATED_AT, AttributeDefaultValueEnum.UPDATED_AT): ci_dict[attr.name] = now - elif attr.default.get('default') == AttributeDefaultValueEnum.AUTO_INC_ID: + elif (attr.default.get('default') == AttributeDefaultValueEnum.AUTO_INC_ID and + not ci_dict.get(attr.name)): ci_dict[attr.name] = cls._auto_inc_id(attr) elif ((attr.name not in ci_dict and attr.alias not in ci_dict) or ( ci_dict.get(attr.name) is None and ci_dict.get(attr.alias) is None)): @@ -368,6 +373,8 @@ class CIManager(object): if attr.default and attr.default.get('default') == AttributeDefaultValueEnum.UPDATED_AT: ci_dict[attr.name] = now + value_manager = AttributeValueManager() + computed_attrs = [] for _, attr in attrs: if attr.is_computed: @@ -378,7 +385,8 @@ class CIManager(object): elif attr.alias in ci_dict: password_dict[attr.id] = ci_dict.pop(attr.alias) - value_manager = AttributeValueManager() + if attr.re_check and password_dict.get(attr.id): + value_manager.check_re(attr.re_check, password_dict[attr.id]) if computed_attrs: value_manager.handle_ci_compute_attributes(ci_dict, computed_attrs, ci) @@ -386,7 +394,7 @@ class CIManager(object): cls._valid_unique_constraint(ci_type.id, ci_dict, ci and ci.id) ref_ci_dict = dict() - for k in ci_dict: + for k in copy.deepcopy(ci_dict): if k.startswith("$") and "." in k: ref_ci_dict[k] = ci_dict[k] continue @@ -395,9 +403,13 @@ class CIManager(object): k not in ci_type_attrs_alias and _no_attribute_policy == ExistPolicy.REJECT): return abort(400, ErrFormat.attribute_not_found.format(k)) - if limit_attrs and ci_type_attrs_name.get(k) not in limit_attrs and ( - ci_type_attrs_alias.get(k) not in limit_attrs): - return abort(403, ErrFormat.ci_filter_perm_attr_no_permission.format(k)) + _attr_name = ((ci_type_attrs_name.get(k) and ci_type_attrs_name[k].name) or + (ci_type_attrs_alias.get(k) and ci_type_attrs_alias[k].name)) + if limit_attrs and _attr_name not in limit_attrs: + if k in raw_dict: + return abort(403, ErrFormat.ci_filter_perm_attr_no_permission.format(k)) + else: + ci_dict.pop(k) ci_dict = {k: v for k, v in ci_dict.items() if k in ci_type_attrs_name or k in ci_type_attrs_alias} @@ -407,7 +419,7 @@ class CIManager(object): 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_value(ci, ci_dict, key2attr) + record_id = value_manager.create_or_update_attr_value(ci, ci_dict, key2attr, ticket_id=ticket_id) except BadRequest as e: if existed is None: cls.delete(ci.id) @@ -425,17 +437,21 @@ class CIManager(object): return ci.id - def update(self, ci_id, _is_admin=False, **ci_dict): + def update(self, ci_id, _is_admin=False, ticket_id=None, **ci_dict): now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') ci = self.confirm_ci_existed(ci_id) - attrs = CITypeAttributesCache.get2(ci.type_id) + raw_dict = copy.deepcopy(ci_dict) + + attrs = CITypeAttributeManager.get_all_attributes(ci.type_id) ci_type_attrs_name = {attr.name: attr for _, attr in attrs} ci_attr2type_attr = {type_attr.attr_id: type_attr for type_attr, _ in attrs} for _, attr in attrs: if attr.default and attr.default.get('default') == AttributeDefaultValueEnum.UPDATED_AT: ci_dict[attr.name] = now + value_manager = AttributeValueManager() + password_dict = dict() computed_attrs = list() for _, attr in attrs: @@ -447,7 +463,8 @@ class CIManager(object): elif attr.alias in ci_dict: password_dict[attr.id] = ci_dict.pop(attr.alias) - value_manager = AttributeValueManager() + if attr.re_check and password_dict.get(attr.id): + value_manager.check_re(attr.re_check, password_dict[attr.id]) if computed_attrs: value_manager.handle_ci_compute_attributes(ci_dict, computed_attrs, ci) @@ -455,20 +472,24 @@ class CIManager(object): limit_attrs = self._valid_ci_for_no_read(ci) if not _is_admin else {} record_id = None - need_lock = current_user.username not in current_app.config.get('PRIVILEGED_USERS', PRIVILEGED_USERS) - with Lock(ci.ci_type.name, need_lock=need_lock): + with redis_lock.Lock(rd.r, ci.ci_type.name): + db.session.commit() + self._valid_unique_constraint(ci.type_id, ci_dict, ci_id) ci_dict = {k: v for k, v in ci_dict.items() if k in ci_type_attrs_name} key2attr = value_manager.valid_attr_value(ci_dict, ci.type_id, ci.id, ci_type_attrs_name, ci_attr2type_attr=ci_attr2type_attr) if limit_attrs: - for k in ci_dict: + for k in copy.deepcopy(ci_dict): if k not in limit_attrs: - return abort(403, ErrFormat.ci_filter_perm_attr_no_permission.format(k)) + if k in raw_dict: + return abort(403, ErrFormat.ci_filter_perm_attr_no_permission.format(k)) + else: + ci_dict.pop(k) try: - record_id = value_manager.create_or_update_attr_value(ci, ci_dict, key2attr) + record_id = value_manager.create_or_update_attr_value(ci, ci_dict, key2attr, ticket_id=ticket_id) except BadRequest as e: raise e @@ -513,10 +534,9 @@ class CIManager(object): 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: - value_table = TableMap(attr_name=attr_name).table + attrs = [i for _, i in CITypeAttributeManager.get_all_attributes(type_id=ci.type_id)] + for attr in attrs: + value_table = TableMap(attr=attr).table for item in value_table.get_by(ci_id=ci_id, to_dict=False): item.delete(commit=False) @@ -541,6 +561,7 @@ class CIManager(object): AttributeHistoryManger.add(None, ci_id, [(None, OperateType.DELETE, ci_dict, None)], ci.type_id) ci_delete.apply_async(args=(ci_id,), queue=CMDB_QUEUE) + delete_id_filter.apply_async(args=(ci_id,), queue=CMDB_QUEUE) return ci_id @@ -847,6 +868,20 @@ class CIRelationManager(object): return numfound, len(ci_ids), result + @staticmethod + def recursive_children(ci_id): + result = [] + + def _get_children(_id): + children = CIRelation.get_by(first_ci_id=_id, to_dict=False) + result.extend([i.second_ci_id for i in children]) + for child in children: + _get_children(child.second_ci_id) + + _get_children(ci_id) + + return result + @staticmethod def _sort_handler(sort_by, query_sql): @@ -902,7 +937,7 @@ class CIRelationManager(object): @staticmethod def _check_constraint(first_ci_id, first_type_id, second_ci_id, second_type_id, type_relation): - db.session.remove() + db.session.commit() if type_relation.constraint == ConstraintEnum.Many2Many: return @@ -925,7 +960,7 @@ class CIRelationManager(object): return abort(400, ErrFormat.relation_constraint.format("1-N")) @classmethod - def add(cls, first_ci_id, second_ci_id, more=None, relation_type_id=None, ancestor_ids=None): + def add(cls, first_ci_id, second_ci_id, more=None, relation_type_id=None, ancestor_ids=None, valid=True): first_ci = CIManager.confirm_ci_existed(first_ci_id) second_ci = CIManager.confirm_ci_existed(second_ci_id) @@ -950,7 +985,7 @@ class CIRelationManager(object): relation_type_id or abort(404, ErrFormat.relation_not_found.format("{} -> {}".format( first_ci.ci_type.name, second_ci.ci_type.name))) - if current_app.config.get('USE_ACL'): + if current_app.config.get('USE_ACL') and valid: resource_name = CITypeRelationManager.acl_resource_name(first_ci.ci_type.name, second_ci.ci_type.name) if not ACLManager().has_permission( @@ -962,7 +997,7 @@ class CIRelationManager(object): else: type_relation = CITypeRelation.get_by_id(relation_type_id) - with Lock("ci_relation_add_{}_{}".format(first_ci.type_id, second_ci.type_id), need_lock=True): + with redis_lock.Lock(rd.r, "ci_relation_add_{}_{}".format(first_ci.type_id, second_ci.type_id)): cls._check_constraint(first_ci_id, first_ci.type_id, second_ci_id, second_ci.type_id, type_relation) @@ -998,6 +1033,7 @@ class CIRelationManager(object): his_manager.add(cr, operate_type=OperateType.DELETE) ci_relation_delete.apply_async(args=(cr.first_ci_id, cr.second_ci_id, cr.ancestor_ids), queue=CMDB_QUEUE) + delete_id_filter.apply_async(args=(cr.second_ci_id,), queue=CMDB_QUEUE) return cr_id @@ -1009,9 +1045,13 @@ class CIRelationManager(object): to_dict=False, first=True) - ci_relation_delete.apply_async(args=(first_ci_id, second_ci_id, ancestor_ids), queue=CMDB_QUEUE) + if cr is not None: + cls.delete(cr.id) - return cr and cls.delete(cr.id) + ci_relation_delete.apply_async(args=(first_ci_id, second_ci_id, ancestor_ids), queue=CMDB_QUEUE) + delete_id_filter.apply_async(args=(second_ci_id,), queue=CMDB_QUEUE) + + return cr @classmethod def batch_update(cls, ci_ids, parents, children, ancestor_ids=None): @@ -1048,11 +1088,62 @@ class CIRelationManager(object): for ci_id in ci_ids: cls.delete_2(parent_id, ci_id, ancestor_ids=ancestor_ids) + @classmethod + def build_by_attribute(cls, ci_dict): + type_id = ci_dict['_type'] + child_items = CITypeRelation.get_by(parent_id=type_id, only_query=True).filter( + CITypeRelation.parent_attr_id.isnot(None)) + for item in child_items: + parent_attr = AttributeCache.get(item.parent_attr_id) + child_attr = AttributeCache.get(item.child_attr_id) + attr_value = ci_dict.get(parent_attr.name) + value_table = TableMap(attr=child_attr).table + for child in value_table.get_by(attr_id=child_attr.id, value=attr_value, only_query=True).join( + CI, CI.id == value_table.ci_id).filter(CI.type_id == item.child_id): + CIRelationManager.add(ci_dict['_id'], child.ci_id, valid=False) + + parent_items = CITypeRelation.get_by(child_id=type_id, only_query=True).filter( + CITypeRelation.child_attr_id.isnot(None)) + for item in parent_items: + parent_attr = AttributeCache.get(item.parent_attr_id) + child_attr = AttributeCache.get(item.child_attr_id) + attr_value = ci_dict.get(child_attr.name) + value_table = TableMap(attr=parent_attr).table + for parent in value_table.get_by(attr_id=parent_attr.id, value=attr_value, only_query=True).join( + CI, CI.id == value_table.ci_id).filter(CI.type_id == item.parent_id): + CIRelationManager.add(parent.ci_id, ci_dict['_id'], valid=False) + + @classmethod + def rebuild_all_by_attribute(cls, ci_type_relation): + parent_attr = AttributeCache.get(ci_type_relation['parent_attr_id']) + child_attr = AttributeCache.get(ci_type_relation['child_attr_id']) + if not parent_attr or not child_attr: + return + + parent_value_table = TableMap(attr=parent_attr).table + child_value_table = TableMap(attr=child_attr).table + + parent_values = parent_value_table.get_by(attr_id=parent_attr.id, only_query=True).join( + CI, CI.id == parent_value_table.ci_id).filter(CI.type_id == ci_type_relation['parent_id']) + child_values = child_value_table.get_by(attr_id=child_attr.id, only_query=True).join( + CI, CI.id == child_value_table.ci_id).filter(CI.type_id == ci_type_relation['child_id']) + + child_value2ci_ids = {} + for child in child_values: + child_value2ci_ids.setdefault(child.value, []).append(child.ci_id) + + for parent in parent_values: + for child_ci_id in child_value2ci_ids.get(parent.value, []): + try: + cls.add(parent.ci_id, child_ci_id, valid=False) + except: + pass + class CITriggerManager(object): @staticmethod def get(type_id): - db.session.remove() + db.session.commit() return CITypeTrigger.get_by(type_id=type_id, to_dict=True) @staticmethod diff --git a/cmdb-api/api/lib/cmdb/ci_type.py b/cmdb-api/api/lib/cmdb/ci_type.py index b7e74d5..cee1e7b 100644 --- a/cmdb-api/api/lib/cmdb/ci_type.py +++ b/cmdb-api/api/lib/cmdb/ci_type.py @@ -5,8 +5,10 @@ import copy import toposort from flask import abort from flask import current_app +from flask import session from flask_login import current_user from toposort import toposort_flatten +from werkzeug.exceptions import BadRequest from api.extensions import db from api.lib.cmdb.attribute import AttributeManager @@ -22,6 +24,7 @@ from api.lib.cmdb.const import ResourceTypeEnum from api.lib.cmdb.const import RoleEnum from api.lib.cmdb.const import ValueTypeEnum from api.lib.cmdb.history import CITypeHistoryManager +from api.lib.cmdb.perms import CIFilterPermsCRUD from api.lib.cmdb.relation_type import RelationTypeManager from api.lib.cmdb.resp_format import ErrFormat from api.lib.cmdb.value import AttributeValueManager @@ -39,10 +42,12 @@ from api.models.cmdb import CITypeAttributeGroup from api.models.cmdb import CITypeAttributeGroupItem from api.models.cmdb import CITypeGroup from api.models.cmdb import CITypeGroupItem +from api.models.cmdb import CITypeInheritance from api.models.cmdb import CITypeRelation from api.models.cmdb import CITypeTrigger from api.models.cmdb import CITypeUniqueConstraint from api.models.cmdb import CustomDashboard +from api.models.cmdb import PreferenceCITypeOrder from api.models.cmdb import PreferenceRelationView from api.models.cmdb import PreferenceSearchOption from api.models.cmdb import PreferenceShowAttributes @@ -71,16 +76,23 @@ class CITypeManager(object): return CIType.get_by_id(ci_type.id) + def get_icons(self): + return {i.id: i.icon or i.name for i in db.session.query( + self.cls.id, self.cls.icon, self.cls.name).filter(self.cls.deleted.is_(False))} + @staticmethod - def get_ci_types(type_name=None): + def get_ci_types(type_name=None, like=True): resources = None if current_app.config.get('USE_ACL') and not is_app_admin('cmdb'): - resources = set([i.get('name') for i in ACLManager().get_resources("CIType")]) + resources = set([i.get('name') for i in ACLManager().get_resources(ResourceTypeEnum.CI_TYPE)]) - ci_types = CIType.get_by() if type_name is None else CIType.get_by_like(name=type_name) + ci_types = CIType.get_by() if type_name is None else ( + CIType.get_by_like(name=type_name) if like else CIType.get_by(name=type_name)) res = list() for type_dict in ci_types: - type_dict["unique_key"] = AttributeCache.get(type_dict["unique_id"]).name + attr = AttributeCache.get(type_dict["unique_id"]) + type_dict["unique_key"] = attr and attr.name + type_dict['parent_ids'] = CITypeInheritanceManager.get_parents(type_dict['id']) if resources is None or type_dict['name'] in resources: res.append(type_dict) @@ -113,6 +125,9 @@ class CITypeManager(object): @classmethod @kwargs_required("name") def add(cls, **kwargs): + if current_app.config.get('USE_ACL') and not is_app_admin('cmdb'): + if ErrFormat.ci_type_config not in {i['name'] for i in ACLManager().get_resources(ResourceTypeEnum.PAGE)}: + return abort(403, ErrFormat.no_permission2) 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) @@ -124,14 +139,23 @@ class CITypeManager(object): kwargs["unique_id"] = unique_key.id kwargs['uid'] = current_user.uid + + parent_ids = kwargs.pop('parent_ids', None) + ci_type = CIType.create(**kwargs) + CITypeInheritanceManager.add(parent_ids, ci_type.id) + CITypeAttributeManager.add(ci_type.id, [unique_key.id], is_required=True) CITypeCache.clean(ci_type.name) if current_app.config.get("USE_ACL"): - ACLManager().add_resource(ci_type.name, ResourceTypeEnum.CI) + try: + ACLManager().add_resource(ci_type.name, ResourceTypeEnum.CI) + except BadRequest: + pass + ACLManager().grant_resource_to_role(ci_type.name, RoleEnum.CMDB_READ_ALL, ResourceTypeEnum.CI, @@ -203,21 +227,29 @@ class CITypeManager(object): if item.get('parent_id') == type_id or item.get('child_id') == type_id: return abort(400, ErrFormat.ci_relation_view_exists_and_cannot_delete_type.format(rv.name)) - for item in CITypeRelation.get_by(parent_id=type_id, to_dict=False): - item.soft_delete(commit=False) + for item in (CITypeRelation.get_by(parent_id=type_id, to_dict=False) + + CITypeRelation.get_by(child_id=type_id, to_dict=False)): + if current_app.config.get('USE_ACL'): + resource_name = CITypeRelationManager.acl_resource_name(item.parent.name, item.child.name) + ACLManager().del_resource(resource_name, ResourceTypeEnum.CI_TYPE_RELATION) - for item in CITypeRelation.get_by(child_id=type_id, to_dict=False): item.soft_delete(commit=False) for table in [PreferenceTreeView, PreferenceShowAttributes, PreferenceSearchOption, CustomDashboard, CITypeGroupItem, CITypeAttributeGroup, CITypeAttribute, CITypeUniqueConstraint, CITypeTrigger, - AutoDiscoveryCIType, CIFilterPerms]: + AutoDiscoveryCIType, CIFilterPerms, PreferenceCITypeOrder]: for item in table.get_by(type_id=type_id, to_dict=False): item.soft_delete(commit=False) for item in AutoDiscoveryCI.get_by(type_id=type_id, to_dict=False): item.delete(commit=False) + for item in CITypeInheritance.get_by(parent_id=type_id, to_dict=False): + item.delete(commit=False) + + for item in CITypeInheritance.get_by(child_id=type_id, to_dict=False): + item.delete(commit=False) + db.session.commit() ci_type.soft_delete() @@ -230,6 +262,100 @@ class CITypeManager(object): ACLManager().del_resource(ci_type.name, ResourceTypeEnum.CI) +class CITypeInheritanceManager(object): + cls = CITypeInheritance + + @classmethod + def get_parents(cls, type_id): + return [i.parent_id for i in cls.cls.get_by(child_id=type_id, to_dict=False)] + + @classmethod + def recursive_children(cls, type_id): + result = [] + + def _get_child(_id): + children = [i.child_id for i in cls.cls.get_by(parent_id=_id, to_dict=False)] + result.extend(children) + for child_id in children: + _get_child(child_id) + + _get_child(type_id) + + return result + + @classmethod + def base(cls, type_id): + result = [] + q = [] + + def _get_parents(_type_id): + parents = [i.parent_id for i in cls.cls.get_by(child_id=_type_id, to_dict=False)] + for i in parents[::-1]: + q.append(i) + try: + out = q.pop(0) + except IndexError: + return + + result.append(out) + + _get_parents(out) + + _get_parents(type_id) + + return result[::-1] + + @classmethod + def add(cls, parent_ids, child_id): + + rels = {} + for i in cls.cls.get_by(to_dict=False): + rels.setdefault(i.child_id, set()).add(i.parent_id) + + try: + toposort_flatten(rels) + except toposort.CircularDependencyError as e: + current_app.logger.warning(str(e)) + return abort(400, ErrFormat.circular_dependency_error) + + for parent_id in parent_ids or []: + if parent_id == child_id: + return abort(400, ErrFormat.circular_dependency_error) + + existed = cls.cls.get_by(parent_id=parent_id, child_id=child_id, first=True, to_dict=False) + if existed is None: + rels.setdefault(child_id, set()).add(parent_id) + try: + toposort_flatten(rels) + except toposort.CircularDependencyError as e: + current_app.logger.warning(str(e)) + return abort(400, ErrFormat.circular_dependency_error) + + cls.cls.create(parent_id=parent_id, child_id=child_id, commit=False) + + db.session.commit() + + @classmethod + def delete(cls, parent_id, child_id): + + existed = cls.cls.get_by(parent_id=parent_id, child_id=child_id, first=True, to_dict=False) + + if existed is not None: + children = cls.recursive_children(child_id) + [child_id] + for _id in children: + if CI.get_by(type_id=_id, to_dict=False, first=True) is not None: + return abort(400, ErrFormat.ci_exists_and_cannot_delete_inheritance) + + attr_ids = set([i.id for _, i in CITypeAttributeManager.get_all_attributes(parent_id)]) + for _id in children: + for attr_id in attr_ids: + for i in PreferenceShowAttributes.get_by(type_id=_id, attr_id=attr_id, to_dict=False): + i.soft_delete(commit=False) + db.session.commit() + + existed.soft_delete() + + class CITypeGroupManager(object): cls = CITypeGroup @@ -243,7 +369,6 @@ class CITypeGroupManager(object): else: resources = {i['name']: i['permissions'] for i in resources if PermEnum.READ in i.get("permissions")} - current_app.logger.info(resources) groups = sorted(CITypeGroup.get_by(), key=lambda x: x['order'] or 0) group_types = set() for group in groups: @@ -251,6 +376,7 @@ class CITypeGroupManager(object): ci_type = CITypeCache.get(t['type_id']).to_dict() if resources is None or (ci_type and ci_type['name'] in resources): ci_type['permissions'] = resources[ci_type['name']] if resources is not None else None + ci_type['inherited'] = True if CITypeInheritanceManager.get_parents(ci_type['id']) else False group.setdefault("ci_types", []).append(ci_type) group_types.add(t["type_id"]) @@ -260,6 +386,7 @@ class CITypeGroupManager(object): for ci_type in ci_types: if ci_type["id"] not in group_types and (resources is None or ci_type['name'] in resources): ci_type['permissions'] = resources.get(ci_type['name']) if resources is not None else None + ci_type['inherited'] = True if CITypeInheritanceManager.get_parents(ci_type['id']) else False other_types['ci_types'].append(ci_type) groups.append(other_types) @@ -283,7 +410,10 @@ class CITypeGroupManager(object): """ existed = CITypeGroup.get_by_id(gid) or abort( 404, ErrFormat.ci_type_group_not_found.format("id={}".format(gid))) - if name is not None: + if name is not None and name != existed.name: + 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)) + existed.update(name=name) max_order = max([i.order or 0 for i in CITypeGroupItem.get_by(group_id=gid, to_dict=False)] or [0]) @@ -348,40 +478,62 @@ class CITypeAttributeManager(object): return attr.name @staticmethod - def get_attr_names_by_type_id(type_id): - return [AttributeCache.get(attr.attr_id).name for attr in CITypeAttributesCache.get(type_id)] + def get_all_attributes(type_id): + parent_ids = CITypeInheritanceManager.base(type_id) + + result = [] + for _type_id in parent_ids + [type_id]: + result.extend(CITypeAttributesCache.get2(_type_id)) + + return result + + @classmethod + def get_attr_names_by_type_id(cls, type_id): + return [attr.name for _, attr in cls.get_all_attributes(type_id)] @staticmethod 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, 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) + parent_ids = CITypeInheritanceManager.base(type_id) - result.append(attr_dict) + result = list() + id2pos = dict() + type2name = {i: CITypeCache.get(i) for i in parent_ids} + for _type_id in parent_ids + [type_id]: + attrs = CITypeAttributesCache.get(_type_id) + for attr in sorted(attrs, key=lambda x: (x.order, x.id)): + 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 + attr_dict["inherited"] = False if _type_id == type_id else True + attr_dict["inherited_from"] = type2name.get(_type_id) and type2name[_type_id].alias + if not has_config_perm: + attr_dict.pop('choice_web_hook', None) + attr_dict.pop('choice_other', None) + + if attr_dict['id'] not in id2pos: + id2pos[attr_dict['id']] = len(result) + result.append(attr_dict) + else: + result[id2pos[attr_dict['id']]] = attr_dict return result - @staticmethod - def get_common_attributes(type_ids): + @classmethod + def get_common_attributes(cls, 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) + result = {type_id: [i for _, i in cls.get_all_attributes(type_id)] for type_id in type_ids} attr2types = {} - for i in result: - attr2types.setdefault(i.attr_id, []).append(i.type_id) + for type_id in result: + for i in result[type_id]: + attr2types.setdefault(i.id, []).append(type_id) attrs = [] for attr_id in attr2types: @@ -498,10 +650,34 @@ class CITypeAttributeManager(object): existed.soft_delete() for ci in CI.get_by(type_id=type_id, to_dict=False): - AttributeValueManager.delete_attr_value(attr_id, ci.id) + AttributeValueManager.delete_attr_value(attr_id, ci.id, commit=False) ci_cache.apply_async(args=(ci.id, None, None), queue=CMDB_QUEUE) + for item in PreferenceShowAttributes.get_by(type_id=type_id, attr_id=attr_id, to_dict=False): + item.soft_delete(commit=False) + + child_ids = CITypeInheritanceManager.recursive_children(type_id) + for _type_id in [type_id] + child_ids: + for item in CITypeUniqueConstraint.get_by(type_id=_type_id, to_dict=False): + if attr_id in item.attr_ids: + attr_ids = copy.deepcopy(item.attr_ids) + attr_ids.remove(attr_id) + + if attr_ids: + item.update(attr_ids=attr_ids, commit=False) + else: + item.soft_delete(commit=False) + + item = CITypeTrigger.get_by(type_id=_type_id, attr_id=attr_id, to_dict=False, first=True) + item and item.soft_delete(commit=False) + + for item in (CITypeRelation.get_by(parent_id=type_id, parent_attr_id=attr_id, to_dict=False) + + CITypeRelation.get_by(child_id=type_id, child_attr_id=attr_id, to_dict=False)): + item.soft_delete(commit=False) + + db.session.commit() + CITypeAttributeCache.clean(type_id, attr_id) CITypeHistoryManager.add(CITypeOperateType.DELETE_ATTRIBUTE, type_id, attr_id=attr.id, @@ -515,8 +691,22 @@ class CITypeAttributeManager(object): attr_id = _from.get('attr_id') from_group_id = _from.get('group_id') to_group_id = _to.get('group_id') + from_group_name = _from.get('group_name') + to_group_name = _to.get('group_name') order = _to.get('order') + if from_group_name: + from_group = CITypeAttributeGroup.get_by(type_id=type_id, name=from_group_name, first=True, to_dict=False) + from_group_id = from_group and from_group.id + + if to_group_name: + to_group = CITypeAttributeGroup.get_by(type_id=type_id, name=to_group_name, first=True, to_dict=False) + to_group_id = to_group and to_group.id + + if not to_group_id and CITypeInheritance.get_by(child_id=type_id, to_dict=False): + to_group = CITypeAttributeGroup.create(type_id=type_id, name=to_group_name) + to_group_id = to_group and to_group.id + if from_group_id != to_group_id: if from_group_id is not None: CITypeAttributeGroupManager.delete_item(from_group_id, attr_id) @@ -544,14 +734,19 @@ class CITypeRelationManager(object): @staticmethod def get(): res = CITypeRelation.get_by(to_dict=False) + type2attributes = dict() for idx, item in enumerate(res): _item = item.to_dict() res[idx] = _item res[idx]['parent'] = item.parent.to_dict() + if item.parent_id not in type2attributes: + type2attributes[item.parent_id] = [i[1].to_dict() for i in CITypeAttributesCache.get2(item.parent_id)] res[idx]['child'] = item.child.to_dict() + if item.child_id not in type2attributes: + type2attributes[item.child_id] = [i[1].to_dict() for i in CITypeAttributesCache.get2(item.child_id)] res[idx]['relation_type'] = item.relation_type.to_dict() - return res + return res, type2attributes @staticmethod def get_child_type_ids(type_id, level): @@ -576,8 +771,15 @@ class CITypeRelationManager(object): ci_type_dict = CITypeCache.get(type_id).to_dict() ci_type_dict["ctr_id"] = relation_inst.id ci_type_dict["attributes"] = CITypeAttributeManager.get_attributes_by_type_id(ci_type_dict["id"]) + attr_filter = CIFilterPermsCRUD.get_attr_filter(type_id) + if attr_filter: + ci_type_dict["attributes"] = [attr for attr in (ci_type_dict["attributes"] or []) + if attr['name'] in attr_filter] + ci_type_dict["relation_type"] = relation_inst.relation_type.name ci_type_dict["constraint"] = relation_inst.constraint + ci_type_dict["parent_attr_id"] = relation_inst.parent_attr_id + ci_type_dict["child_attr_id"] = relation_inst.child_attr_id return ci_type_dict @@ -622,7 +824,8 @@ class CITypeRelationManager(object): return "{} -> {}".format(first_name, second_name) @classmethod - def add(cls, parent, child, relation_type_id, constraint=ConstraintEnum.One2Many): + def add(cls, parent, child, relation_type_id, constraint=ConstraintEnum.One2Many, + parent_attr_id=None, child_attr_id=None): p = CITypeManager.check_is_existed(parent) c = CITypeManager.check_is_existed(child) @@ -637,24 +840,21 @@ class CITypeRelationManager(object): current_app.logger.warning(str(e)) return abort(400, ErrFormat.circular_dependency_error) - if constraint == ConstraintEnum.Many2Many: - other_c = CITypeRelation.get_by(parent_id=p.id, constraint=ConstraintEnum.Many2Many, - to_dict=False, first=True) - other_p = CITypeRelation.get_by(child_id=c.id, constraint=ConstraintEnum.Many2Many, - to_dict=False, first=True) - if other_c and other_c.child_id != c.id: - return abort(400, ErrFormat.m2m_relation_constraint.format(p.name, other_c.child.name)) - if other_p and other_p.parent_id != p.id: - return abort(400, ErrFormat.m2m_relation_constraint.format(other_p.parent.name, c.name)) - + old_parent_attr_id = None existed = cls._get(p.id, c.id) if existed is not None: - existed.update(relation_type_id=relation_type_id, - constraint=constraint) + old_parent_attr_id = existed.parent_attr_id + existed = existed.update(relation_type_id=relation_type_id, + constraint=constraint, + parent_attr_id=parent_attr_id, + child_attr_id=child_attr_id, + filter_none=False) else: existed = CITypeRelation.create(parent_id=p.id, child_id=c.id, relation_type_id=relation_type_id, + parent_attr_id=parent_attr_id, + child_attr_id=child_attr_id, constraint=constraint) if current_app.config.get("USE_ACL"): @@ -668,6 +868,11 @@ class CITypeRelationManager(object): current_user.username, ResourceTypeEnum.CI_TYPE_RELATION) + if parent_attr_id and parent_attr_id != old_parent_attr_id: + if parent_attr_id and parent_attr_id != existed.parent_attr_id: + from api.tasks.cmdb import rebuild_relation_for_attribute_changed + rebuild_relation_for_attribute_changed.apply_async(args=(existed.to_dict())) + CITypeHistoryManager.add(CITypeOperateType.ADD_RELATION, p.id, change=dict(parent=p.to_dict(), child=c.to_dict(), relation_type_id=relation_type_id)) @@ -720,25 +925,66 @@ class CITypeAttributeGroupManager(object): @staticmethod def get_by_type_id(type_id, need_other=False): - groups = CITypeAttributeGroup.get_by(type_id=type_id) - groups = sorted(groups, key=lambda x: x["order"] or 0) - grouped = list() + parent_ids = CITypeInheritanceManager.base(type_id) + + groups = [] + id2type = {i: CITypeCache.get(i).alias for i in parent_ids} + for _type_id in parent_ids + [type_id]: + _groups = CITypeAttributeGroup.get_by(type_id=_type_id) + _groups = sorted(_groups, key=lambda x: x["order"] or 0) + for i in _groups: + if type_id != _type_id: + i['inherited'] = True + i['inherited_from'] = id2type[_type_id] + else: + i['inherited'] = False + + groups.extend(_groups) + + grouped = set() attributes = CITypeAttributeManager.get_attributes_by_type_id(type_id) - id2attr = {i['id']: i for i in attributes} + id2attr = {i.get('id'): i for i in attributes} + group2pos = dict() + attr2pos = dict() + result = [] for group in groups: items = CITypeAttributeGroupItem.get_by(group_id=group["id"], to_dict=False) items = sorted(items, key=lambda x: x.order or 0) - group["attributes"] = [id2attr.get(i.attr_id) for i in items if i.attr_id in id2attr] - grouped.extend([i.attr_id for i in items]) + + if group['name'] not in group2pos: + group_pos = len(result) + group['attributes'] = [] + result.append(group) + + group2pos[group['name']] = group_pos + else: + group_pos = group2pos[group['name']] + + attr = None + for i in items: + if i.attr_id in id2attr: + attr = id2attr[i.attr_id] + attr['inherited'] = group['inherited'] + attr['inherited_from'] = group.get('inherited_from') + result[group_pos]['attributes'].append(attr) + + if i.attr_id in attr2pos: + result[attr2pos[i.attr_id][0]]['attributes'].remove(attr2pos[i.attr_id][1]) + + attr2pos[i.attr_id] = [group_pos, attr] + + group.pop('inherited_from', None) + + grouped |= set([i.attr_id for i in items]) if need_other: grouped = set(grouped) other_attributes = [attr for attr in attributes if attr["id"] not in grouped] - groups.append(dict(attributes=other_attributes)) + result.append(dict(attributes=other_attributes)) - return groups + return result @staticmethod def create_or_update(type_id, name, attr_order, group_order=0, is_update=False): @@ -872,10 +1118,16 @@ class CITypeAttributeGroupManager(object): @classmethod def transfer(cls, type_id, _from, _to): current_app.logger.info("CIType[{0}] {1} -> {2}".format(type_id, _from, _to)) - from_group = CITypeAttributeGroup.get_by_id(_from) + if isinstance(_from, int): + from_group = CITypeAttributeGroup.get_by_id(_from) + else: + from_group = CITypeAttributeGroup.get_by(name=_from, first=True, to_dict=False) from_group or abort(404, ErrFormat.ci_type_attribute_group_not_found.format("id={}".format(_from))) - to_group = CITypeAttributeGroup.get_by_id(_to) + if isinstance(_to, int): + to_group = CITypeAttributeGroup.get_by_id(_to) + else: + to_group = CITypeAttributeGroup.get_by(name=_to, first=True, to_dict=False) to_group or abort(404, ErrFormat.ci_type_attribute_group_not_found.format("id={}".format(_to))) from_order, to_order = from_group.order, to_group.order @@ -891,97 +1143,60 @@ class CITypeAttributeGroupManager(object): class CITypeTemplateManager(object): @staticmethod - def __import(cls, data): - id2obj_dicts = {i['id']: i for i in data} - existed = cls.get_by(deleted=None, to_dict=False) - id2existed = {i.id: i for i in existed} - existed_ids = [i.id for i in existed] - existed_no_delete_ids = [i.id for i in existed if not i.deleted] + def __import(cls, data, unique_key='name'): + id2obj_dicts = {i[unique_key]: i for i in data} + existed = cls.get_by(to_dict=False) + id2existed = {getattr(i, unique_key): i for i in existed} + existed_ids = [getattr(i, unique_key) for i in existed] + id_map = dict() # add for added_id in set(id2obj_dicts.keys()) - set(existed_ids): + _id = id2obj_dicts[added_id].pop('id', None) + id2obj_dicts[added_id].pop('created_at', None) + id2obj_dicts[added_id].pop('updated_at', None) + id2obj_dicts[added_id].pop('uid', None) + if cls == CIType: - CITypeManager.add(**id2obj_dicts[added_id]) + __id = CITypeManager.add(**id2obj_dicts[added_id]) + CITypeCache.clean(__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'), - ) + __id = 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'), + id2obj_dicts[added_id].get('parent_attr_id'), + id2obj_dicts[added_id].get('child_attr_id'), + ) else: - cls.create(flush=True, **id2obj_dicts[added_id]) + obj = cls.create(flush=True, **id2obj_dicts[added_id]) + if cls == Attribute: + AttributeCache.clean(obj) + __id = obj.id + + id_map[_id] = __id # update for updated_id in set(id2obj_dicts.keys()) & set(existed_ids): + _id = id2obj_dicts[updated_id].pop('id', None) + + id2existed[updated_id].update(flush=True, **id2obj_dicts[updated_id]) + + id_map[_id] = id2existed[updated_id].id + + if cls == Attribute: + AttributeCache.clean(id2existed[updated_id]) + if cls == CIType: - deleted = id2existed[updated_id].deleted - CITypeManager.update(updated_id, **id2obj_dicts[updated_id]) - if deleted and current_app.config.get("USE_ACL"): - type_name = id2obj_dicts[updated_id]['name'] - ACLManager().add_resource(type_name, ResourceTypeEnum.CI) - ACLManager().grant_resource_to_role(type_name, - RoleEnum.CMDB_READ_ALL, - ResourceTypeEnum.CI, - permissions=[PermEnum.READ]) - ACLManager().grant_resource_to_role(type_name, - current_user.username, - ResourceTypeEnum.CI) + CITypeCache.clean(id2existed[updated_id].id) - else: - id2existed[updated_id].update(flush=True, **id2obj_dicts[updated_id]) - - # delete - for deleted_id in set(existed_no_delete_ids) - set(id2obj_dicts.keys()): - if cls == CIType: - id2existed[deleted_id].soft_delete(flush=True) - - CITypeCache.clean(deleted_id) - - CITypeHistoryManager.add(CITypeOperateType.DELETE, deleted_id, change=id2existed[deleted_id].to_dict()) - - if current_app.config.get("USE_ACL"): - ACLManager().del_resource(id2existed[deleted_id].name, ResourceTypeEnum.CI) - else: - id2existed[deleted_id].soft_delete(flush=True) try: db.session.commit() except Exception as e: db.session.rollback() raise Exception(str(e)) - def _import_ci_types(self, ci_types): - for i in ci_types: - i.pop("unique_key", None) - - self.__import(CIType, ci_types) - - def _import_ci_type_groups(self, ci_type_groups): - _ci_type_groups = copy.deepcopy(ci_type_groups) - for i in _ci_type_groups: - i.pop('ci_types', None) - - self.__import(CITypeGroup, _ci_type_groups) - - # import group type items - for group in ci_type_groups: - existed = CITypeGroupItem.get_by(group_id=group['id'], to_dict=False) - for i in existed: - i.soft_delete() - - for order, ci_type in enumerate(group.get('ci_types') or []): - payload = dict(group_id=group['id'], type_id=ci_type['id'], order=order) - CITypeGroupItem.create(**payload) - - def _import_relation_types(self, relation_types): - self.__import(RelationType, relation_types) - - def _import_ci_type_relations(self, ci_type_relations): - for i in ci_type_relations: - i.pop('parent', None) - i.pop('child', None) - i.pop('relation_type', None) - - self.__import(CITypeRelation, ci_type_relations) + return id_map def _import_attributes(self, type2attributes): attributes = [attr for type_id in type2attributes for attr in type2attributes[type_id]] @@ -990,122 +1205,270 @@ class CITypeTemplateManager(object): i.pop('default_show', None) i.pop('is_required', None) i.pop('order', None) + i.pop('choice_web_hook', None) + i.pop('choice_other', None) + i.pop('order', None) + i.pop('inherited', None) + i.pop('inherited_from', None) choice_value = i.pop('choice_value', None) + if not choice_value: + i['is_choice'] = False attrs.append((i, choice_value)) - self.__import(Attribute, [i[0] for i in attrs]) + attr_id_map = self.__import(Attribute, [i[0] for i in copy.deepcopy(attrs)]) for i, choice_value in attrs: - if choice_value: - AttributeManager.add_choice_values(i['id'], i['value_type'], choice_value) + if choice_value and not i.get('choice_web_hook') and not i.get('choice_other'): + AttributeManager.add_choice_values(attr_id_map.get(i['id'], i['id']), i['value_type'], choice_value) + + return attr_id_map + + def _import_ci_types(self, ci_types, attr_id_map): + for i in ci_types: + i.pop("unique_key", None) + i['unique_id'] = attr_id_map.get(i['unique_id'], i['unique_id']) + i['uid'] = current_user.uid + + return self.__import(CIType, ci_types) + + def _import_ci_type_groups(self, ci_type_groups, type_id_map): + _ci_type_groups = copy.deepcopy(ci_type_groups) + for i in _ci_type_groups: + i.pop('ci_types', None) + + group_id_map = self.__import(CITypeGroup, _ci_type_groups) + + # import group type items + for group in ci_type_groups: + for order, ci_type in enumerate(group.get('ci_types') or []): + payload = dict(group_id=group_id_map.get(group['id'], group['id']), + type_id=type_id_map.get(ci_type['id'], ci_type['id']), + order=order) + existed = CITypeGroupItem.get_by(group_id=payload['group_id'], type_id=payload['type_id'], + first=True, to_dict=False) + if existed is None: + CITypeGroupItem.create(flush=True, **payload) + else: + existed.update(flush=True, **payload) + + try: + db.session.commit() + except Exception as e: + db.session.rollback() + raise Exception(str(e)) + + def _import_relation_types(self, relation_types): + return self.__import(RelationType, relation_types) @staticmethod - def _import_type_attributes(type2attributes): - # add type attribute + def _import_ci_type_relations(ci_type_relations, type_id_map, relation_type_id_map): + for i in ci_type_relations: + i.pop('parent', None) + i.pop('child', None) + i.pop('relation_type', None) + + i['parent_id'] = type_id_map.get(i['parent_id'], i['parent_id']) + i['child_id'] = type_id_map.get(i['child_id'], i['child_id']) + i['relation_type_id'] = relation_type_id_map.get(i['relation_type_id'], i['relation_type_id']) + + try: + CITypeRelationManager.add(i.get('parent_id'), + i.get('child_id'), + i.get('relation_type_id'), + i.get('constraint'), + ) + except BadRequest: + pass + + @staticmethod + def _import_type_attributes(type2attributes, type_id_map, attr_id_map): + for type_id in type2attributes: + CITypeAttributesCache.clean(type_id_map.get(int(type_id), type_id)) for type_id in type2attributes: - existed = CITypeAttribute.get_by(type_id=type_id, to_dict=False) - existed_attr_ids = {i.attr_id: i for i in existed} - new_attr_ids = {i['id']: i for i in type2attributes[type_id]} + existed = CITypeAttributesCache.get2(type_id_map.get(int(type_id), type_id)) + existed_attr_names = {attr.name: ta for ta, attr in existed} + handled = set() for attr in type2attributes[type_id]: - payload = dict(type_id=type_id, - attr_id=attr['id'], + payload = dict(type_id=type_id_map.get(int(type_id), type_id), + attr_id=attr_id_map.get(attr['id'], attr['id']), default_show=attr['default_show'], is_required=attr['is_required'], order=attr['order']) - if attr['id'] not in existed_attr_ids: # new - CITypeAttribute.create(flush=True, **payload) - else: # update - existed_attr_ids[attr['id']].update(**payload) + if attr['name'] not in handled: + if attr['name'] not in existed_attr_names: # new + CITypeAttribute.create(flush=True, **payload) + else: # update + existed_attr_names[attr['name']].update(flush=True, **payload) - # delete - for i in existed: - if i.attr_id not in new_attr_ids: - i.soft_delete() + handled.add(attr['name']) + + try: + db.session.commit() + except Exception as e: + db.session.rollback() + raise Exception(str(e)) + + for type_id in type2attributes: + CITypeAttributesCache.clean(type_id_map.get(int(type_id), type_id)) @staticmethod - def _import_attribute_group(type2attribute_group): + def _import_attribute_group(type2attribute_group, type_id_map, attr_id_map): for type_id in type2attribute_group: - existed = CITypeAttributeGroup.get_by(type_id=type_id, to_dict=False) - for i in existed: - i.soft_delete() - for group in type2attribute_group[type_id] or []: _group = copy.deepcopy(group) _group.pop('attributes', None) _group.pop('id', None) - new = CITypeAttributeGroup.create(**_group) + _group.pop('inherited', None) + _group.pop('inherited_from', None) + existed = CITypeAttributeGroup.get_by(name=_group['name'], + type_id=type_id_map.get(_group['type_id'], _group['type_id']), + first=True, to_dict=False) + if existed is None: + _group['type_id'] = type_id_map.get(_group['type_id'], _group['type_id']) - existed = CITypeAttributeGroupItem.get_by(group_id=new.id, to_dict=False) - for i in existed: - i.soft_delete() + existed = CITypeAttributeGroup.create(flush=True, **_group) for order, attr in enumerate(group['attributes'] or []): - CITypeAttributeGroupItem.create(group_id=new.id, attr_id=attr['id'], order=order) + item_existed = CITypeAttributeGroupItem.get_by(group_id=existed.id, + attr_id=attr_id_map.get(attr['id'], attr['id']), + first=True, to_dict=False) + if item_existed is None: + CITypeAttributeGroupItem.create(group_id=existed.id, + attr_id=attr_id_map.get(attr['id'], attr['id']), + order=order) + else: + item_existed.update(flush=True, order=order) + + try: + db.session.commit() + except Exception as e: + db.session.rollback() + raise Exception(str(e)) @staticmethod def _import_auto_discovery_rules(rules): from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryRuleCRUD from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryCITypeCRUD + for rule in rules: ci_type = CITypeCache.get(rule.pop('type_name', None)) + adr = rule.pop('adr', {}) or {} + if ci_type: rule['type_id'] = ci_type.id if rule.get('adr_name'): ad_rule = AutoDiscoveryRuleCRUD.get_by_name(rule.pop("adr_name")) + adr.pop('created_at', None) + adr.pop('updated_at', None) + adr.pop('id', None) + if ad_rule: rule['adr_id'] = ad_rule.id + ad_rule.update(**adr) + + elif adr: + ad_rule = AutoDiscoveryRuleCRUD().add(**adr) + rule['adr_id'] = ad_rule.id + else: + continue rule.pop("id", None) rule.pop("created_at", None) rule.pop("updated_at", None) + rule.pop("relation", None) rule['uid'] = current_user.uid - try: - AutoDiscoveryCITypeCRUD.add(**rule) - except Exception as e: - current_app.logger.warning("import auto discovery rules failed: {}".format(e)) + + if not rule.get('attributes'): + continue + + existed = False + for i in AutoDiscoveryCIType.get_by(type_id=ci_type.id, adr_id=rule['adr_id'], to_dict=False): + if ((i.extra_option or {}).get('alias') or None) == ( + (rule.get('extra_option') or {}).get('alias') or None): + existed = True + AutoDiscoveryCITypeCRUD().update(i.id, **rule) + break + + if not existed: + try: + AutoDiscoveryCITypeCRUD().add(**rule) + except Exception as e: + current_app.logger.warning("import auto discovery rules failed: {}".format(e)) + + @staticmethod + def _import_icons(icons): + from api.lib.common_setting.upload_file import CommonFileCRUD + for icon_name in icons: + if icons[icon_name]: + try: + CommonFileCRUD().save_str_to_file(icon_name, icons[icon_name]) + except Exception as e: + current_app.logger.warning("save icon failed: {}".format(e)) def import_template(self, tpt): import time s = time.time() - self._import_attributes(tpt.get('type2attributes') or {}) + attr_id_map = self._import_attributes(tpt.get('type2attributes') or {}) current_app.logger.info('import attributes cost: {}'.format(time.time() - s)) s = time.time() - self._import_ci_types(tpt.get('ci_types') or []) + ci_type_id_map = self._import_ci_types(tpt.get('ci_types') or [], attr_id_map) current_app.logger.info('import ci_types cost: {}'.format(time.time() - s)) s = time.time() - self._import_ci_type_groups(tpt.get('ci_type_groups') or []) + self._import_ci_type_groups(tpt.get('ci_type_groups') or [], ci_type_id_map) current_app.logger.info('import ci_type_groups cost: {}'.format(time.time() - s)) s = time.time() - self._import_relation_types(tpt.get('relation_types') or []) + relation_type_id_map = self._import_relation_types(tpt.get('relation_types') or []) current_app.logger.info('import relation_types cost: {}'.format(time.time() - s)) s = time.time() - self._import_ci_type_relations(tpt.get('ci_type_relations') or []) + self._import_ci_type_relations(tpt.get('ci_type_relations') or [], ci_type_id_map, relation_type_id_map) current_app.logger.info('import ci_type_relations cost: {}'.format(time.time() - s)) s = time.time() - self._import_type_attributes(tpt.get('type2attributes') or {}) + self._import_type_attributes(tpt.get('type2attributes') or {}, ci_type_id_map, attr_id_map) current_app.logger.info('import type2attributes cost: {}'.format(time.time() - s)) s = time.time() - self._import_attribute_group(tpt.get('type2attribute_group') or {}) + self._import_attribute_group(tpt.get('type2attribute_group') or {}, ci_type_id_map, attr_id_map) current_app.logger.info('import type2attribute_group cost: {}'.format(time.time() - s)) s = time.time() self._import_auto_discovery_rules(tpt.get('ci_type_auto_discovery_rules') or []) current_app.logger.info('import ci_type_auto_discovery_rules cost: {}'.format(time.time() - s)) + s = time.time() + self._import_icons(tpt.get('icons') or {}) + current_app.logger.info('import icons cost: {}'.format(time.time() - s)) + @staticmethod def export_template(): from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryCITypeCRUD from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryRuleCRUD + from api.lib.common_setting.upload_file import CommonFileCRUD + + tpt = dict( + ci_types=CITypeManager.get_ci_types(), + ci_type_groups=CITypeGroupManager.get(), + relation_types=[i.to_dict() for i in RelationTypeManager.get_all()], + ci_type_relations=CITypeRelationManager.get()[0], + ci_type_auto_discovery_rules=list(), + type2attributes=dict(), + type2attribute_group=dict(), + icons=dict() + ) + + def get_icon_value(icon): + try: + return CommonFileCRUD().get_file_binary_str(icon) + except: + return "" ad_rules = AutoDiscoveryCITypeCRUD.get_all() rules = [] @@ -1116,23 +1479,91 @@ class CITypeTemplateManager(object): if r.get('adr_id'): adr = AutoDiscoveryRuleCRUD.get_by_id(r.pop('adr_id')) r['adr_name'] = adr and adr.name + r['adr'] = adr and adr.to_dict() or {} + + icon_url = r['adr'].get('option', {}).get('icon', {}).get('url') + if icon_url and icon_url not in tpt['icons']: + tpt['icons'][icon_url] = get_icon_value(icon_url) rules.append(r) - tpt = dict( - ci_types=CITypeManager.get_ci_types(), - ci_type_groups=CITypeGroupManager.get(), - relation_types=[i.to_dict() for i in RelationTypeManager.get_all()], - ci_type_relations=CITypeRelationManager.get(), - ci_type_auto_discovery_rules=rules, - type2attributes=dict(), - type2attribute_group=dict() - ) + tpt['ci_type_auto_discovery_rules'] = rules for ci_type in tpt['ci_types']: + if ci_type['icon'] and len(ci_type['icon'].split('$$')) > 3: + icon_url = ci_type['icon'].split('$$')[3] + if icon_url not in tpt['icons']: + tpt['icons'][icon_url] = get_icon_value(icon_url) + tpt['type2attributes'][ci_type['id']] = CITypeAttributeManager.get_attributes_by_type_id( ci_type['id'], choice_web_hook_parse=False, choice_other_parse=False) + for attr in tpt['type2attributes'][ci_type['id']]: + for i in (attr.get('choice_value') or []): + if (i[1] or {}).get('icon', {}).get('url') and len(i[1]['icon']['url'].split('$$')) > 3: + icon_url = i[1]['icon']['url'].split('$$')[3] + if icon_url not in tpt['icons']: + tpt['icons'][icon_url] = get_icon_value(icon_url) + + tpt['type2attribute_group'][ci_type['id']] = CITypeAttributeGroupManager.get_by_type_id(ci_type['id']) + + return tpt + + @staticmethod + def export_template_by_type(type_id): + ci_type = CITypeCache.get(type_id) or abort(404, ErrFormat.ci_type_not_found2.format("id={}".format(type_id))) + + from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryCITypeCRUD + from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryRuleCRUD + from api.lib.common_setting.upload_file import CommonFileCRUD + + tpt = dict( + ci_types=CITypeManager.get_ci_types(type_name=ci_type.name, like=False), + ci_type_auto_discovery_rules=list(), + type2attributes=dict(), + type2attribute_group=dict(), + icons=dict() + ) + + def get_icon_value(icon): + try: + return CommonFileCRUD().get_file_binary_str(icon) + except: + return "" + + ad_rules = AutoDiscoveryCITypeCRUD.get_by_type_id(ci_type.id) + rules = [] + for r in ad_rules: + r = r.to_dict() + r['type_name'] = ci_type and ci_type.name + if r.get('adr_id'): + adr = AutoDiscoveryRuleCRUD.get_by_id(r.pop('adr_id')) + r['adr_name'] = adr and adr.name + r['adr'] = adr and adr.to_dict() or {} + + icon_url = r['adr'].get('option', {}).get('icon', {}).get('url') + if icon_url and icon_url not in tpt['icons']: + tpt['icons'][icon_url] = get_icon_value(icon_url) + + rules.append(r) + tpt['ci_type_auto_discovery_rules'] = rules + + for ci_type in tpt['ci_types']: + if ci_type['icon'] and len(ci_type['icon'].split('$$')) > 3: + icon_url = ci_type['icon'].split('$$')[3] + if icon_url not in tpt['icons']: + tpt['icons'][icon_url] = get_icon_value(icon_url) + + tpt['type2attributes'][ci_type['id']] = CITypeAttributeManager.get_attributes_by_type_id( + ci_type['id'], choice_web_hook_parse=False, choice_other_parse=False) + + for attr in tpt['type2attributes'][ci_type['id']]: + for i in (attr.get('choice_value') or []): + if (i[1] or {}).get('icon', {}).get('url') and len(i[1]['icon']['url'].split('$$')) > 3: + icon_url = i[1]['icon']['url'].split('$$')[3] + if icon_url not in tpt['icons']: + tpt['icons'][icon_url] = get_icon_value(icon_url) + tpt['type2attribute_group'][ci_type['id']] = CITypeAttributeGroupManager.get_by_type_id(ci_type['id']) return tpt diff --git a/cmdb-api/api/lib/cmdb/const.py b/cmdb-api/api/lib/cmdb/const.py index c48fd1b..31cc6e5 100644 --- a/cmdb-api/api/lib/cmdb/const.py +++ b/cmdb-api/api/lib/cmdb/const.py @@ -69,6 +69,7 @@ class ResourceTypeEnum(BaseEnum): CI_TYPE_RELATION = "CITypeRelation" # create/delete/grant RELATION_VIEW = "RelationView" # read/update/delete/grant CI_FILTER = "CIFilter" # read + PAGE = "page" # read class PermEnum(BaseEnum): diff --git a/cmdb-api/api/lib/cmdb/history.py b/cmdb-api/api/lib/cmdb/history.py index 6d1f542..2e793f6 100644 --- a/cmdb-api/api/lib/cmdb/history.py +++ b/cmdb-api/api/lib/cmdb/history.py @@ -135,7 +135,7 @@ class AttributeHistoryManger(object): from api.lib.cmdb.ci import CIManager cis = CIManager().get_cis_by_ids(list(ci_ids), unique_required=True) - cis = {i['_id']: i for i in cis} + cis = {i['_id']: i for i in cis if i} return total, res, cis @@ -167,6 +167,7 @@ class AttributeHistoryManger(object): new=hist.new, created_at=record.created_at.strftime('%Y-%m-%d %H:%M:%S'), record_id=record.id, + ticket_id=record.ticket_id, hid=hist.id ) result.append(item) @@ -200,9 +201,9 @@ class AttributeHistoryManger(object): return username, timestamp, attr_dict, rel_dict @staticmethod - def add(record_id, ci_id, history_list, type_id=None, flush=False, commit=True): + def add(record_id, ci_id, history_list, type_id=None, ticket_id=None, flush=False, commit=True): if record_id is None: - record = OperationRecord.create(uid=current_user.uid, type_id=type_id) + record = OperationRecord.create(uid=current_user.uid, type_id=type_id, ticket_id=ticket_id) record_id = record.id for attr_id, operate_type, old, new in history_list or []: diff --git a/cmdb-api/api/lib/cmdb/perms.py b/cmdb-api/api/lib/cmdb/perms.py index 3b5ad6f..34f5782 100644 --- a/cmdb-api/api/lib/cmdb/perms.py +++ b/cmdb-api/api/lib/cmdb/perms.py @@ -1,12 +1,15 @@ # -*- coding:utf-8 -*- - +import copy import functools +import redis_lock from flask import abort from flask import current_app from flask import request from flask_login import current_user +from api.extensions import db +from api.extensions import rd from api.lib.cmdb.const import ResourceTypeEnum from api.lib.cmdb.resp_format import ErrFormat from api.lib.mixin import DBMixin @@ -40,6 +43,11 @@ class CIFilterPermsCRUD(DBMixin): result[i['rid']]['ci_filter'] = "" result[i['rid']]['ci_filter'] += (i['ci_filter'] or "") + if i['id_filter']: + if not result[i['rid']]['id_filter']: + result[i['rid']]['id_filter'] = {} + result[i['rid']]['id_filter'].update(i['id_filter'] or {}) + return result def get_by_ids(self, _ids, type_id=None): @@ -70,6 +78,11 @@ class CIFilterPermsCRUD(DBMixin): result[i['type_id']]['ci_filter'] = "" result[i['type_id']]['ci_filter'] += (i['ci_filter'] or "") + if i['id_filter']: + if not result[i['type_id']]['id_filter']: + result[i['type_id']]['id_filter'] = {} + result[i['type_id']]['id_filter'].update(i['id_filter'] or {}) + return result @classmethod @@ -82,6 +95,54 @@ class CIFilterPermsCRUD(DBMixin): type2filter_perms = cls().get_by_ids(list(map(int, [i['name'] for i in res2])), type_id=type_id) return type2filter_perms.get(type_id, {}).get('attr_filter') or [] + def _revoke_children(self, rid, id_filter, rebuild=True): + items = self.cls.get_by(rid=rid, ci_filter=None, attr_filter=None, to_dict=False) + for item in items: + changed, item_id_filter = False, copy.deepcopy(item.id_filter) + for prefix in id_filter: + for k, v in copy.deepcopy((item.id_filter or {})).items(): + if k.startswith(prefix) and k != prefix: + item_id_filter.pop(k) + changed = True + + if not item_id_filter and current_app.config.get('USE_ACL'): + item.soft_delete(commit=False) + ACLManager().del_resource(str(item.id), ResourceTypeEnum.CI_FILTER, rebuild=rebuild) + elif changed: + item.update(id_filter=item_id_filter, commit=False) + + db.session.commit() + + def _revoke_parent(self, rid, parent_path, rebuild=True): + parent_path = [i for i in parent_path.split(',') if i] or [] + revoke_nodes = [','.join(parent_path[:i]) for i in range(len(parent_path), 0, -1)] + for node_path in revoke_nodes: + delete_item, can_deleted = None, True + items = self.cls.get_by(rid=rid, ci_filter=None, attr_filter=None, to_dict=False) + for item in items: + if node_path in item.id_filter: + delete_item = item + if any(filter(lambda x: x.startswith(node_path) and x != node_path, item.id_filter.keys())): + can_deleted = False + break + + if can_deleted and delete_item: + id_filter = copy.deepcopy(delete_item.id_filter) + id_filter.pop(node_path) + delete_item = delete_item.update(id_filter=id_filter, filter_none=False) + + if current_app.config.get('USE_ACL') and not id_filter: + ACLManager().del_resource(str(delete_item.id), ResourceTypeEnum.CI_FILTER, rebuild=False) + delete_item.soft_delete() + items.remove(delete_item) + + if rebuild: + from api.tasks.acl import role_rebuild + from api.lib.perm.acl.const import ACL_QUEUE + from api.lib.perm.acl.cache import AppCache + + role_rebuild.apply_async(args=(rid, AppCache.get('cmdb').id), queue=ACL_QUEUE) + def _can_add(self, **kwargs): ci_filter = kwargs.get('ci_filter') attr_filter = kwargs.get('attr_filter') or "" @@ -102,34 +163,67 @@ class CIFilterPermsCRUD(DBMixin): def add(self, **kwargs): kwargs = self._can_add(**kwargs) or kwargs + with redis_lock.Lock(rd.r, 'CMDB_FILTER_{}_{}'.format(kwargs['type_id'], kwargs['rid'])): + request_id_filter = {} + if kwargs.get('id_filter'): + obj = self.cls.get_by(type_id=kwargs.get('type_id'), + rid=kwargs.get('rid'), + ci_filter=None, + attr_filter=None, + first=True, to_dict=False) - obj = self.cls.get_by(type_id=kwargs.get('type_id'), - rid=kwargs.get('rid'), - first=True, to_dict=False) - if obj is not None: - obj = obj.update(filter_none=False, **kwargs) - if not obj.attr_filter and not obj.ci_filter: - if current_app.config.get('USE_ACL'): - ACLManager().del_resource(str(obj.id), ResourceTypeEnum.CI_FILTER) + for _id, v in (kwargs.get('id_filter') or {}).items(): + key = ",".join(([v['parent_path']] if v.get('parent_path') else []) + [str(_id)]) + request_id_filter[key] = v['name'] - obj.soft_delete() + else: + obj = self.cls.get_by(type_id=kwargs.get('type_id'), + rid=kwargs.get('rid'), + id_filter=None, + first=True, to_dict=False) + + is_recursive = kwargs.pop('is_recursive', 0) + if obj is not None: + if obj.id_filter and isinstance(kwargs.get('id_filter'), dict): + obj_id_filter = copy.deepcopy(obj.id_filter) + + for k, v in request_id_filter.items(): + obj_id_filter[k] = v + + kwargs['id_filter'] = obj_id_filter + + obj = obj.update(filter_none=False, **kwargs) + + if not obj.attr_filter and not obj.ci_filter and not obj.id_filter: + if current_app.config.get('USE_ACL'): + ACLManager().del_resource(str(obj.id), ResourceTypeEnum.CI_FILTER, rebuild=False) + + obj.soft_delete() + + if not is_recursive and request_id_filter: + self._revoke_children(obj.rid, request_id_filter, rebuild=False) - else: - if not kwargs.get('ci_filter') and not kwargs.get('attr_filter'): return - obj = self.cls.create(**kwargs) + else: + if not kwargs.get('ci_filter') and not kwargs.get('attr_filter') and not kwargs.get('id_filter'): + return - if current_app.config.get('USE_ACL'): - try: - ACLManager().add_resource(obj.id, ResourceTypeEnum.CI_FILTER) - except: - pass - ACLManager().grant_resource_to_role_by_rid(obj.id, - kwargs.get('rid'), - ResourceTypeEnum.CI_FILTER) + if request_id_filter: + kwargs['id_filter'] = request_id_filter - return obj + obj = self.cls.create(**kwargs) + + if current_app.config.get('USE_ACL'): # new resource + try: + ACLManager().add_resource(obj.id, ResourceTypeEnum.CI_FILTER) + except: + pass + ACLManager().grant_resource_to_role_by_rid(obj.id, + kwargs.get('rid'), + ResourceTypeEnum.CI_FILTER) + + return obj def _can_update(self, **kwargs): pass @@ -138,15 +232,83 @@ class CIFilterPermsCRUD(DBMixin): pass def delete(self, **kwargs): - obj = self.cls.get_by(type_id=kwargs.get('type_id'), - rid=kwargs.get('rid'), - first=True, to_dict=False) + with redis_lock.Lock(rd.r, 'CMDB_FILTER_{}_{}'.format(kwargs['type_id'], kwargs['rid'])): + obj = self.cls.get_by(type_id=kwargs.get('type_id'), + rid=kwargs.get('rid'), + id_filter=None, + first=True, to_dict=False) - if obj is not None: - if current_app.config.get('USE_ACL'): - ACLManager().del_resource(str(obj.id), ResourceTypeEnum.CI_FILTER) + if obj is not None: + resource = None + if current_app.config.get('USE_ACL'): + resource = ACLManager().del_resource(str(obj.id), ResourceTypeEnum.CI_FILTER) - obj.soft_delete() + obj.soft_delete() + + return resource + + def delete2(self, **kwargs): + + with redis_lock.Lock(rd.r, 'CMDB_FILTER_{}_{}'.format(kwargs['type_id'], kwargs['rid'])): + obj = self.cls.get_by(type_id=kwargs.get('type_id'), + rid=kwargs.get('rid'), + ci_filter=None, + attr_filter=None, + first=True, to_dict=False) + + request_id_filter = {} + for _id, v in (kwargs.get('id_filter') or {}).items(): + key = ",".join(([v['parent_path']] if v.get('parent_path') else []) + [str(_id)]) + request_id_filter[key] = v['name'] + + resource = None + if obj is not None: + + id_filter = {} + for k, v in copy.deepcopy(obj.id_filter or {}).items(): # important + if k not in request_id_filter: + id_filter[k] = v + + if not id_filter and current_app.config.get('USE_ACL'): + resource = ACLManager().del_resource(str(obj.id), ResourceTypeEnum.CI_FILTER, rebuild=False) + obj.soft_delete() + db.session.commit() + + else: + obj.update(id_filter=id_filter) + + self._revoke_children(kwargs.get('rid'), request_id_filter, rebuild=False) + self._revoke_parent(kwargs.get('rid'), kwargs.get('parent_path')) + + return resource + + def delete_id_filter_by_ci_id(self, ci_id): + items = self.cls.get_by(ci_filter=None, attr_filter=None, to_dict=False) + + rebuild_roles = set() + for item in items: + id_filter = copy.deepcopy(item.id_filter) + changed = False + for node_path in item.id_filter: + if str(ci_id) in node_path: + id_filter.pop(node_path) + changed = True + + if changed: + rebuild_roles.add(item.rid) + if not id_filter: + item.soft_delete(commit=False) + else: + item.update(id_filter=id_filter, commit=False) + + db.session.commit() + + if rebuild_roles: + from api.tasks.acl import role_rebuild + from api.lib.perm.acl.const import ACL_QUEUE + from api.lib.perm.acl.cache import AppCache + for rid in rebuild_roles: + role_rebuild.apply_async(args=(rid, AppCache.get('cmdb').id), queue=ACL_QUEUE) def has_perm_for_ci(arg_name, resource_type, perm, callback=None, app=None): diff --git a/cmdb-api/api/lib/cmdb/preference.py b/cmdb-api/api/lib/cmdb/preference.py index e7e06ed..f5659d5 100644 --- a/cmdb-api/api/lib/cmdb/preference.py +++ b/cmdb-api/api/lib/cmdb/preference.py @@ -14,6 +14,8 @@ from api.lib.cmdb.attribute import AttributeManager from api.lib.cmdb.cache import AttributeCache from api.lib.cmdb.cache import CITypeAttributesCache from api.lib.cmdb.cache import CITypeCache +from api.lib.cmdb.cache import CMDBCounterCache +from api.lib.cmdb.ci_type import CITypeAttributeManager from api.lib.cmdb.const import ConstraintEnum from api.lib.cmdb.const import PermEnum from api.lib.cmdb.const import ResourceTypeEnum @@ -24,6 +26,7 @@ from api.lib.exception import AbortException from api.lib.perm.acl.acl import ACLManager from api.models.cmdb import CITypeAttribute from api.models.cmdb import CITypeRelation +from api.models.cmdb import PreferenceCITypeOrder from api.models.cmdb import PreferenceRelationView from api.models.cmdb import PreferenceSearchOption from api.models.cmdb import PreferenceShowAttributes @@ -38,13 +41,22 @@ class PreferenceManager(object): @staticmethod def get_types(instance=False, tree=False): + ci_type_order = sorted(PreferenceCITypeOrder.get_by(uid=current_user.uid, to_dict=False), key=lambda x: x.order) + types = db.session.query(PreferenceShowAttributes.type_id).filter( PreferenceShowAttributes.uid == current_user.uid).filter( PreferenceShowAttributes.deleted.is_(False)).group_by( PreferenceShowAttributes.type_id).all() if instance else [] + types = sorted(types, key=lambda x: {i.type_id: idx for idx, i in enumerate( + ci_type_order) if not i.is_tree}.get(x.type_id, 1)) tree_types = PreferenceTreeView.get_by(uid=current_user.uid, to_dict=False) if tree else [] - type_ids = set([i.type_id for i in types + tree_types]) + tree_types = sorted(tree_types, key=lambda x: {i.type_id: idx for idx, i in enumerate( + ci_type_order) if i.is_tree}.get(x.type_id, 1)) + + type_ids = [i.type_id for i in types + tree_types] + if types and tree_types: + type_ids = set(type_ids) return [CITypeCache.get(type_id).to_dict() for type_id in type_ids] @@ -59,32 +71,36 @@ class PreferenceManager(object): :param tree: :return: """ - result = dict(self=dict(instance=[], tree=[], type_id2subs_time=dict()), - type_id2users=dict()) + result = dict(self=dict(instance=[], tree=[], type_id2subs_time=dict())) + + result.update(CMDBCounterCache.get_sub_counter()) + + ci_type_order = sorted(PreferenceCITypeOrder.get_by(uid=current_user.uid, to_dict=False), key=lambda x: x.order) if instance: types = db.session.query(PreferenceShowAttributes.type_id, PreferenceShowAttributes.uid, PreferenceShowAttributes.created_at).filter( - PreferenceShowAttributes.deleted.is_(False)).group_by( + PreferenceShowAttributes.deleted.is_(False)).filter( + PreferenceShowAttributes.uid == current_user.uid).group_by( PreferenceShowAttributes.uid, PreferenceShowAttributes.type_id) for i in types: - if i.uid == current_user.uid: - result['self']['instance'].append(i.type_id) - if str(i.created_at) > str(result['self']['type_id2subs_time'].get(i.type_id, "")): - result['self']['type_id2subs_time'][i.type_id] = i.created_at + result['self']['instance'].append(i.type_id) + if str(i.created_at) > str(result['self']['type_id2subs_time'].get(i.type_id, "")): + result['self']['type_id2subs_time'][i.type_id] = i.created_at - result['type_id2users'].setdefault(i.type_id, []).append(i.uid) + instance_order = [i.type_id for i in ci_type_order if not i.is_tree] + if len(instance_order) == len(result['self']['instance']): + result['self']['instance'] = instance_order if tree: - types = PreferenceTreeView.get_by(to_dict=False) + types = PreferenceTreeView.get_by(uid=current_user.uid, to_dict=False) for i in types: - if i.uid == current_user.uid: - result['self']['tree'].append(i.type_id) - if str(i.created_at) > str(result['self']['type_id2subs_time'].get(i.type_id, "")): - result['self']['type_id2subs_time'][i.type_id] = i.created_at + result['self']['tree'].append(i.type_id) + if str(i.created_at) > str(result['self']['type_id2subs_time'].get(i.type_id, "")): + result['self']['type_id2subs_time'][i.type_id] = i.created_at - result['type_id2users'].setdefault(i.type_id, []) - if i.uid not in result['type_id2users'][i.type_id]: - result['type_id2users'][i.type_id].append(i.uid) + tree_order = [i.type_id for i in ci_type_order if i.is_tree] + if len(tree_order) == len(result['self']['tree']): + result['self']['tree'] = tree_order return result @@ -98,8 +114,8 @@ class PreferenceManager(object): CITypeAttribute, CITypeAttribute.attr_id == PreferenceShowAttributes.attr_id).filter( PreferenceShowAttributes.uid == current_user.uid).filter( PreferenceShowAttributes.type_id == type_id).filter( - PreferenceShowAttributes.deleted.is_(False)).filter(CITypeAttribute.deleted.is_(False)).filter( - CITypeAttribute.type_id == type_id).all() + PreferenceShowAttributes.deleted.is_(False)).filter(CITypeAttribute.deleted.is_(False)).group_by( + CITypeAttribute.attr_id).all() result = [] for i in sorted(attrs, key=lambda x: x.PreferenceShowAttributes.order): @@ -109,17 +125,16 @@ class PreferenceManager(object): is_subscribed = True if not attrs: - attrs = db.session.query(CITypeAttribute).filter( - CITypeAttribute.type_id == type_id).filter( - CITypeAttribute.deleted.is_(False)).filter( - CITypeAttribute.default_show.is_(True)).order_by(CITypeAttribute.order) - result = [i.attr.to_dict() for i in attrs] + result = CITypeAttributeManager.get_attributes_by_type_id(type_id, + choice_web_hook_parse=False, + choice_other_parse=False) + result = [i for i in result if i['default_show']] is_subscribed = False 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.get("choice_other")))) + i["id"], i["value_type"], i.get("choice_web_hook"), i.get("choice_other")))) return is_subscribed, result @@ -151,9 +166,22 @@ class PreferenceManager(object): if i.attr_id not in attr_dict: i.soft_delete() + if not existed_all and attr_order: + cls.add_ci_type_order_item(type_id, is_tree=False) + + elif not PreferenceShowAttributes.get_by(type_id=type_id, uid=current_user.uid, to_dict=False): + cls.delete_ci_type_order_item(type_id, is_tree=False) + @staticmethod def get_tree_view(): + ci_type_order = sorted(PreferenceCITypeOrder.get_by(uid=current_user.uid, is_tree=True, to_dict=False), + key=lambda x: x.order) + res = PreferenceTreeView.get_by(uid=current_user.uid, to_dict=True) + if ci_type_order: + res = sorted(res, key=lambda x: {ii.type_id: idx for idx, ii in enumerate( + ci_type_order)}.get(x['type_id'], 1)) + for item in res: if item["levels"]: ci_type = CITypeCache.get(item['type_id']).to_dict() @@ -172,8 +200,8 @@ class PreferenceManager(object): return res - @staticmethod - def create_or_update_tree_view(type_id, levels): + @classmethod + def create_or_update_tree_view(cls, type_id, levels): attrs = CITypeAttributesCache.get(type_id) for idx, i in enumerate(levels): for attr in attrs: @@ -185,9 +213,12 @@ class PreferenceManager(object): if existed is not None: if not levels: existed.soft_delete() + cls.delete_ci_type_order_item(type_id, is_tree=True) return existed return existed.update(levels=levels) elif levels: + cls.add_ci_type_order_item(type_id, is_tree=True) + return PreferenceTreeView.create(levels=levels, type_id=type_id, uid=current_user.uid) @staticmethod @@ -207,11 +238,13 @@ class PreferenceManager(object): views = _views view2cr_ids = dict() + name2view = dict() result = dict() name2id = list() for view in views: view2cr_ids.setdefault(view['name'], []).extend(view['cr_ids']) name2id.append([view['name'], view['id']]) + name2view[view['name']] = view id2type = dict() for view_name in view2cr_ids: @@ -255,6 +288,8 @@ class PreferenceManager(object): topo_flatten=topo_flatten, level2constraint=level2constraint, leaf=leaf, + option=name2view[view_name]['option'], + is_public=name2view[view_name]['is_public'], leaf2show_types=leaf2show_types, node2show_types=node2show_types, show_types=[CITypeCache.get(j).to_dict() @@ -266,14 +301,18 @@ class PreferenceManager(object): return result, id2type, sorted(name2id, key=lambda x: x[1]) @classmethod - def create_or_update_relation_view(cls, name, cr_ids, is_public=False): + def create_or_update_relation_view(cls, name=None, cr_ids=None, _id=None, is_public=False, option=None): if not cr_ids: return abort(400, ErrFormat.preference_relation_view_node_required) - existed = PreferenceRelationView.get_by(name=name, to_dict=False, first=True) + if _id is None: + existed = PreferenceRelationView.get_by(name=name, to_dict=False, first=True) + else: + existed = PreferenceRelationView.get_by_id(_id) current_app.logger.debug(existed) if existed is None: - PreferenceRelationView.create(name=name, cr_ids=cr_ids, uid=current_user.uid, is_public=is_public) + PreferenceRelationView.create(name=name, cr_ids=cr_ids, uid=current_user.uid, + is_public=is_public, option=option) if current_app.config.get("USE_ACL"): ACLManager().add_resource(name, ResourceTypeEnum.RELATION_VIEW) @@ -281,6 +320,11 @@ class PreferenceManager(object): RoleEnum.CMDB_READ_ALL, ResourceTypeEnum.RELATION_VIEW, permissions=[PermEnum.READ]) + else: + if existed.name != name and current_app.config.get("USE_ACL"): + ACLManager().update_resource(existed.name, name, ResourceTypeEnum.RELATION_VIEW) + + existed.update(name=name, cr_ids=cr_ids, is_public=is_public, option=option) return cls.get_relation_view() @@ -356,6 +400,9 @@ class PreferenceManager(object): for i in PreferenceTreeView.get_by(type_id=type_id, uid=uid, to_dict=False): i.soft_delete() + for i in PreferenceCITypeOrder.get_by(type_id=type_id, uid=uid, to_dict=False): + i.soft_delete() + @staticmethod def can_edit_relation(parent_id, child_id): views = PreferenceRelationView.get_by(to_dict=False) @@ -381,3 +428,36 @@ class PreferenceManager(object): return False return True + + @staticmethod + def add_ci_type_order_item(type_id, is_tree=False): + max_order = PreferenceCITypeOrder.get_by( + uid=current_user.uid, is_tree=is_tree, only_query=True).order_by(PreferenceCITypeOrder.order.desc()).first() + order = (max_order and max_order.order + 1) or 1 + + PreferenceCITypeOrder.create(type_id=type_id, is_tree=is_tree, uid=current_user.uid, order=order) + + @staticmethod + def delete_ci_type_order_item(type_id, is_tree=False): + existed = PreferenceCITypeOrder.get_by(uid=current_user.uid, type_id=type_id, is_tree=is_tree, + first=True, to_dict=False) + + existed and existed.soft_delete() + + @staticmethod + def upsert_ci_type_order(type_ids, is_tree=False): + for idx, type_id in enumerate(type_ids): + order = idx + 1 + existed = PreferenceCITypeOrder.get_by(uid=current_user.uid, type_id=type_id, is_tree=is_tree, + to_dict=False, first=True) + if existed is not None: + existed.update(order=order, flush=True) + else: + PreferenceCITypeOrder.create(uid=current_user.uid, type_id=type_id, is_tree=is_tree, order=order, + flush=True) + try: + db.session.commit() + except Exception as e: + db.session.rollback() + current_app.logger.error("upsert citype order failed: {}".format(e)) + return abort(400, ErrFormat.unknown_error) diff --git a/cmdb-api/api/lib/cmdb/resp_format.py b/cmdb-api/api/lib/cmdb/resp_format.py index b7a3495..7e89cbe 100644 --- a/cmdb-api/api/lib/cmdb/resp_format.py +++ b/cmdb-api/api/lib/cmdb/resp_format.py @@ -1,101 +1,140 @@ # -*- coding:utf-8 -*- +from flask_babel import lazy_gettext as _l + from api.lib.resp_format import CommonErrFormat class ErrFormat(CommonErrFormat): - invalid_relation_type = "无效的关系类型: {}" - ci_type_not_found = "模型不存在!" - argument_attributes_must_be_list = "参数 attributes 类型必须是列表" - argument_file_not_found = "文件似乎并未上传" + ci_type_config = _l("CI Model") # 模型配置 - attribute_not_found = "属性 {} 不存在!" - attribute_is_unique_id = "该属性是模型的唯一标识,不能被删除!" - attribute_is_ref_by_type = "该属性被模型 {} 引用, 不能删除!" - attribute_value_type_cannot_change = "属性的值类型不允许修改!" - attribute_list_value_cannot_change = "多值不被允许修改!" - attribute_index_cannot_change = "修改索引 非管理员不被允许!" - attribute_index_change_failed = "索引切换失败!" - invalid_choice_values = "预定义值的类型不对!" - attribute_name_duplicate = "重复的属性名 {}" - add_attribute_failed = "创建属性 {} 失败!" - update_attribute_failed = "修改属性 {} 失败!" - cannot_edit_attribute = "您没有权限修改该属性!" - cannot_delete_attribute = "目前只允许 属性创建人、管理员 删除属性!" - attribute_name_cannot_be_builtin = "属性字段名不能是内置字段: id, _id, ci_id, type, _type, ci_type" - attribute_choice_other_invalid = "预定义值: 其他模型请求参数不合法!" + invalid_relation_type = _l("Invalid relation type: {}") # 无效的关系类型: {} + ci_type_not_found = _l("CIType is not found") # 模型不存在! - ci_not_found = "CI {} 不存在" - unique_constraint = "多属性联合唯一校验不通过: {}" - unique_value_not_found = "模型的主键 {} 不存在!" - unique_key_required = "主键字段 {} 缺失" - ci_is_already_existed = "CI 已经存在!" - relation_constraint = "关系约束: {}, 校验失败 " - m2m_relation_constraint = "多对多关系 限制: 模型 {} <-> {} 已经存在多对多关系!" - relation_not_found = "CI关系: {} 不存在" - ci_search_Parentheses_invalid = "搜索表达式里小括号前不支持: 或、非" + # 参数 attributes 类型必须是列表 + argument_attributes_must_be_list = _l("The type of parameter attributes must be a list") + argument_file_not_found = _l("The file doesn't seem to be uploaded") # 文件似乎并未上传 - ci_type_not_found2 = "模型 {} 不存在" - ci_type_is_already_existed = "模型 {} 已经存在" - unique_key_not_define = "主键未定义或者已被删除" - only_owner_can_delete = "只有创建人才能删除它!" - ci_exists_and_cannot_delete_type = "因为CI已经存在,不能删除模型" - ci_relation_view_exists_and_cannot_delete_type = "因为关系视图 {} 引用了该模型,不能删除模型" - ci_type_group_not_found = "模型分组 {} 不存在" - ci_type_group_exists = "模型分组 {} 已经存在" - ci_type_relation_not_found = "模型关系 {} 不存在" - ci_type_attribute_group_duplicate = "属性分组 {} 已存在" - ci_type_attribute_group_not_found = "属性分组 {} 不存在" - ci_type_group_attribute_not_found = "属性组<{0}> - 属性<{1}> 不存在" - unique_constraint_duplicate = "唯一约束已经存在!" - unique_constraint_invalid = "唯一约束的属性不能是 JSON 和 多值" - ci_type_trigger_duplicate = "重复的触发器" - ci_type_trigger_not_found = "触发器 {} 不存在" + attribute_not_found = _l("Attribute {} does not exist!") # 属性 {} 不存在! + attribute_is_unique_id = _l( + "This attribute is the unique identifier of the model and cannot be deleted!") # 该属性是模型的唯一标识,不能被删除! + attribute_is_ref_by_type = _l( + "This attribute is referenced by model {} and cannot be deleted!") # 该属性被模型 {} 引用, 不能删除! + attribute_value_type_cannot_change = _l( + "The value type of the attribute is not allowed to be modified!") # 属性的值类型不允许修改! + attribute_list_value_cannot_change = _l("Multiple values are not allowed to be modified!") # 多值不被允许修改! + # 修改索引 非管理员不被允许! + attribute_index_cannot_change = _l("Modifying the index is not allowed for non-administrators!") + attribute_index_change_failed = _l("Index switching failed!") # 索引切换失败! + invalid_choice_values = _l("The predefined value is of the wrong type!") # 预定义值的类型不对! + attribute_name_duplicate = _l("Duplicate attribute name {}") # 重复的属性名 {} + add_attribute_failed = _l("Failed to create attribute {}!") # 创建属性 {} 失败! + update_attribute_failed = _l("Modify attribute {} failed!") # 修改属性 {} 失败! + cannot_edit_attribute = _l("You do not have permission to modify this attribute!") # 您没有权限修改该属性! + cannot_delete_attribute = _l( + "Only creators and administrators are allowed to delete attributes!") # 目前只允许 属性创建人、管理员 删除属性! + # 属性字段名不能是内置字段: id, _id, ci_id, type, _type, ci_type + attribute_name_cannot_be_builtin = _l( + "Attribute field names cannot be built-in fields: id, _id, ci_id, type, _type, ci_type") + attribute_choice_other_invalid = _l( + "Predefined value: Other model request parameters are illegal!") # 预定义值: 其他模型请求参数不合法! - record_not_found = "操作记录 {} 不存在" - cannot_delete_unique = "不能删除唯一标识" - cannot_delete_default_order_attr = "不能删除默认排序的属性" + ci_not_found = _l("CI {} does not exist") # CI {} 不存在 + unique_constraint = _l("Multiple attribute joint unique verification failed: {}") # 多属性联合唯一校验不通过: {} + unique_value_not_found = _l("The model's primary key {} does not exist!") # 模型的主键 {} 不存在! + unique_key_required = _l("Primary key {} is missing") # 主键字段 {} 缺失 + ci_is_already_existed = _l("CI already exists!") # CI 已经存在! + relation_constraint = _l("Relationship constraint: {}, verification failed") # 关系约束: {}, 校验失败 + # 多对多关系 限制: 模型 {} <-> {} 已经存在多对多关系! + m2m_relation_constraint = _l( + "Many-to-many relationship constraint: Model {} <-> {} already has a many-to-many relationship!") - preference_relation_view_node_required = "没有选择节点" - preference_search_option_not_found = "该搜索选项不存在!" - preference_search_option_exists = "该搜索选项命名重复!" + relation_not_found = _l("CI relationship: {} does not exist") # CI关系: {} 不存在 - relation_type_exists = "关系类型 {} 已经存在" - relation_type_not_found = "关系类型 {} 不存在" + # 搜索表达式里小括号前不支持: 或、非 + ci_search_Parentheses_invalid = _l("In search expressions, not supported before parentheses: or, not") - attribute_value_invalid = "无效的属性值: {}" - attribute_value_invalid2 = "{} 无效的值: {}" - not_in_choice_values = "{} 不在预定义值里" - attribute_value_unique_required = "属性 {} 的值必须是唯一的, 当前值 {} 已存在" - attribute_value_required = "属性 {} 值必须存在" - attribute_value_unknown_error = "新增或者修改属性值未知错误: {}" + ci_type_not_found2 = _l("Model {} does not exist") # 模型 {} 不存在 + ci_type_is_already_existed = _l("Model {} already exists") # 模型 {} 已经存在 + unique_key_not_define = _l("The primary key is undefined or has been deleted") # 主键未定义或者已被删除 + only_owner_can_delete = _l("Only the creator can delete it!") # 只有创建人才能删除它! + ci_exists_and_cannot_delete_type = _l( + "The model cannot be deleted because the CI already exists") # 因为CI已经存在,不能删除模型 + ci_exists_and_cannot_delete_inheritance = _l( + "The inheritance cannot be deleted because the CI already exists") # 因为CI已经存在,不能删除继承关系 - custom_name_duplicate = "订制名重复" + # 因为关系视图 {} 引用了该模型,不能删除模型 + ci_relation_view_exists_and_cannot_delete_type = _l( + "The model cannot be deleted because the model is referenced by the relational view {}") + ci_type_group_not_found = _l("Model group {} does not exist") # 模型分组 {} 不存在 + ci_type_group_exists = _l("Model group {} already exists") # 模型分组 {} 已经存在 + ci_type_relation_not_found = _l("Model relationship {} does not exist") # 模型关系 {} 不存在 + ci_type_attribute_group_duplicate = _l("Attribute group {} already exists") # 属性分组 {} 已存在 + ci_type_attribute_group_not_found = _l("Attribute group {} does not exist") # 属性分组 {} 不存在 + # 属性组<{0}> - 属性<{1}> 不存在 + ci_type_group_attribute_not_found = _l("Attribute group <{0}> - attribute <{1}> does not exist") + unique_constraint_duplicate = _l("The unique constraint already exists!") # 唯一约束已经存在! + # 唯一约束的属性不能是 JSON 和 多值 + unique_constraint_invalid = _l("Uniquely constrained attributes cannot be JSON and multi-valued") + ci_type_trigger_duplicate = _l("Duplicated trigger") # 重复的触发器 + ci_type_trigger_not_found = _l("Trigger {} does not exist") # 触发器 {} 不存在 - limit_ci_type = "模型数超过限制: {}" - limit_ci = "CI数超过限制: {}" + record_not_found = _l("Operation record {} does not exist") # 操作记录 {} 不存在 + cannot_delete_unique = _l("Unique identifier cannot be deleted") # 不能删除唯一标识 + cannot_delete_default_order_attr = _l("Cannot delete default sorted attributes") # 不能删除默认排序的属性 - adr_duplicate = "自动发现规则: {} 已经存在!" - adr_not_found = "自动发现规则: {} 不存在!" - adr_referenced = "该自动发现规则被模型引用, 不能删除!" - ad_duplicate = "自动发现规则的应用不能重复定义!" - ad_not_found = "您要修改的自动发现: {} 不存在!" - ad_not_unique_key = "属性字段没有包括唯一标识: {}" - adc_not_found = "自动发现的实例不存在!" - adt_not_found = "模型并未关联该自动发现!" - adt_secret_no_permission = "只有创建人才能修改Secret!" - cannot_delete_adt = "该规则已经有自动发现的实例, 不能被删除!" - adr_default_ref_once = "该默认的自动发现规则 已经被模型 {} 引用!" - adr_unique_key_required = "unique_key方法必须返回非空字符串!" - adr_plugin_attributes_list_required = "attributes方法必须返回的是list" - adr_plugin_attributes_list_no_empty = "attributes方法返回的list不能为空!" - adt_target_all_no_permission = "只有管理员才可以定义执行机器为: 所有节点!" - adt_target_expr_no_permission = "执行机器权限检查不通过: {}" + preference_relation_view_node_required = _l("No node selected") # 没有选择节点 + preference_search_option_not_found = _l("This search option does not exist!") # 该搜索选项不存在! + preference_search_option_exists = _l("This search option has a duplicate name!") # 该搜索选项命名重复! - ci_filter_name_cannot_be_empty = "CI过滤授权 必须命名!" - ci_filter_perm_cannot_or_query = "CI过滤授权 暂时不支持 或 查询" - ci_filter_perm_attr_no_permission = "您没有属性 {} 的操作权限!" - ci_filter_perm_ci_no_permission = "您没有该CI的操作权限!" + relation_type_exists = _l("Relationship type {} already exists") # 关系类型 {} 已经存在 + relation_type_not_found = _l("Relationship type {} does not exist") # 关系类型 {} 不存在 - password_save_failed = "保存密码失败: {}" - password_load_failed = "获取密码失败: {}" + attribute_value_invalid = _l("Invalid attribute value: {}") # 无效的属性值: {} + attribute_value_invalid2 = _l("{} Invalid value: {}") # {} 无效的值: {} + not_in_choice_values = _l("{} is not in the predefined values") # {} 不在预定义值里 + # 属性 {} 的值必须是唯一的, 当前值 {} 已存在 + attribute_value_unique_required = _l("The value of attribute {} must be unique, {} already exists") + attribute_value_required = _l("Attribute {} value must exist") # 属性 {} 值必须存在 + attribute_value_out_of_range = _l("Out of range value, the maximum value is 2147483647") + # 新增或者修改属性值未知错误: {} + attribute_value_unknown_error = _l("Unknown error when adding or modifying attribute value: {}") + + custom_name_duplicate = _l("Duplicate custom name") # 订制名重复 + + limit_ci_type = _l("Number of models exceeds limit: {}") # 模型数超过限制: {} + limit_ci = _l("The number of CIs exceeds the limit: {}") # CI数超过限制: {} + + adr_duplicate = _l("Auto-discovery rule: {} already exists!") # 自动发现规则: {} 已经存在! + adr_not_found = _l("Auto-discovery rule: {} does not exist!") # 自动发现规则: {} 不存在! + # 该自动发现规则被模型引用, 不能删除! + adr_referenced = _l("This auto-discovery rule is referenced by the model and cannot be deleted!") + # 自动发现规则的应用不能重复定义! + ad_duplicate = _l("The application of auto-discovery rules cannot be defined repeatedly!") + ad_not_found = _l("The auto-discovery you want to modify: {} does not exist!") # 您要修改的自动发现: {} 不存在! + ad_not_unique_key = _l("Attribute does not include unique identifier: {}") # 属性字段没有包括唯一标识: {} + adc_not_found = _l("The auto-discovery instance does not exist!") # 自动发现的实例不存在! + adt_not_found = _l("The model is not associated with this auto-discovery!") # 模型并未关联该自动发现! + adt_secret_no_permission = _l("Only the creator can modify the Secret!") # 只有创建人才能修改Secret! + # 该规则已经有自动发现的实例, 不能被删除! + cannot_delete_adt = _l("This rule already has auto-discovery instances and cannot be deleted!") + # 该默认的自动发现规则 已经被模型 {} 引用! + adr_default_ref_once = _l("The default auto-discovery rule is already referenced by model {}!") + # unique_key方法必须返回非空字符串! + adr_unique_key_required = _l("The unique_key method must return a non-empty string!") + adr_plugin_attributes_list_required = _l("The attributes method must return a list") # attributes方法必须返回的是list + # attributes方法返回的list不能为空! + adr_plugin_attributes_list_no_empty = _l("The list returned by the attributes method cannot be empty!") + # 只有管理员才可以定义执行机器为: 所有节点! + adt_target_all_no_permission = _l("Only administrators can define execution targets as: all nodes!") + adt_target_expr_no_permission = _l("Execute targets permission check failed: {}") # 执行机器权限检查不通过: {} + + ci_filter_name_cannot_be_empty = _l("CI filter authorization must be named!") # CI过滤授权 必须命名! + ci_filter_perm_cannot_or_query = _l( + "CI filter authorization is currently not supported or query") # CI过滤授权 暂时不支持 或 查询 + # 您没有属性 {} 的操作权限! + ci_filter_perm_attr_no_permission = _l("You do not have permission to operate attribute {}!") + ci_filter_perm_ci_no_permission = _l("You do not have permission to operate this CI!") # 您没有该CI的操作权限! + + password_save_failed = _l("Failed to save password: {}") # 保存密码失败: {} + password_load_failed = _l("Failed to get password: {}") # 获取密码失败: {} diff --git a/cmdb-api/api/lib/cmdb/search/ci/__init__.py b/cmdb-api/api/lib/cmdb/search/ci/__init__.py index 4951a69..f4a2b18 100644 --- a/cmdb-api/api/lib/cmdb/search/ci/__init__.py +++ b/cmdb-api/api/lib/cmdb/search/ci/__init__.py @@ -16,10 +16,11 @@ def search(query=None, ret_key=RetKey.NAME, count=1, sort=None, - excludes=None): + excludes=None, + use_id_filter=False): if current_app.config.get("USE_ES"): s = SearchFromES(query, fl, facet, page, ret_key, count, sort) else: - s = SearchFromDB(query, fl, facet, page, ret_key, count, sort, excludes=excludes) + s = SearchFromDB(query, fl, facet, page, ret_key, count, sort, excludes=excludes, use_id_filter=use_id_filter) return s diff --git a/cmdb-api/api/lib/cmdb/search/ci/db/query_sql.py b/cmdb-api/api/lib/cmdb/search/ci/db/query_sql.py index 24aa0cb..f3e76bf 100644 --- a/cmdb-api/api/lib/cmdb/search/ci/db/query_sql.py +++ b/cmdb-api/api/lib/cmdb/search/ci/db/query_sql.py @@ -62,7 +62,7 @@ QUERY_CI_BY_ATTR_NAME = """ QUERY_CI_BY_ID = """ SELECT c_cis.id as ci_id FROM c_cis - WHERE c_cis.id={} + WHERE c_cis.id {} """ QUERY_CI_BY_TYPE = """ 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 206e921..dfc806b 100644 --- a/cmdb-api/api/lib/cmdb/search/ci/db/search.py +++ b/cmdb-api/api/lib/cmdb/search/ci/db/search.py @@ -44,7 +44,10 @@ class Search(object): count=1, sort=None, ci_ids=None, - excludes=None): + excludes=None, + parent_node_perm_passed=False, + use_id_filter=False, + use_ci_filter=True): self.orig_query = query self.fl = fl or [] self.excludes = excludes or [] @@ -54,12 +57,17 @@ class Search(object): self.count = count self.sort = sort self.ci_ids = ci_ids or [] + self.raw_ci_ids = copy.deepcopy(self.ci_ids) self.query_sql = "" self.type_id_list = [] self.only_type_query = False + self.parent_node_perm_passed = parent_node_perm_passed + self.use_id_filter = use_id_filter + self.use_ci_filter = use_ci_filter self.valid_type_names = [] self.type2filter_perms = dict() + self.is_app_admin = is_app_admin('cmdb') or current_user.username == "worker" @staticmethod def _operator_proc(key): @@ -106,7 +114,7 @@ class Search(object): self.type_id_list.append(str(ci_type.id)) if ci_type.id in self.type2filter_perms: ci_filter = self.type2filter_perms[ci_type.id].get('ci_filter') - if ci_filter: + if ci_filter and self.use_ci_filter and not self.use_id_filter: sub = [] ci_filter = Template(ci_filter).render(user=current_user) for i in ci_filter.split(','): @@ -122,6 +130,14 @@ class Search(object): self.fl = set(self.type2filter_perms[ci_type.id]['attr_filter']) else: self.fl = set(self.fl) & set(self.type2filter_perms[ci_type.id]['attr_filter']) + + if self.type2filter_perms[ci_type.id].get('id_filter') and self.use_id_filter: + + if not self.raw_ci_ids: + self.ci_ids = list(self.type2filter_perms[ci_type.id]['id_filter'].keys()) + + if self.use_id_filter and not self.ci_ids and not self.is_app_admin: + self.raw_ci_ids = [0] else: raise SearchError(ErrFormat.no_permission.format(ci_type.alias, PermEnum.READ)) else: @@ -138,7 +154,10 @@ class Search(object): @staticmethod def _id_query_handler(v): - return QUERY_CI_BY_ID.format(v) + if ";" in v: + return QUERY_CI_BY_ID.format("in {}".format(v.replace(';', ','))) + else: + return QUERY_CI_BY_ID.format("= {}".format(v)) @staticmethod def _in_query_handler(attr, v, is_not): @@ -152,6 +171,7 @@ class Search(object): "NOT LIKE" if is_not else "LIKE", _v.replace("*", "%")) for _v in new_v]) _query_sql = QUERY_CI_BY_ATTR_NAME.format(table_name, attr.id, in_query) + return _query_sql @staticmethod @@ -167,6 +187,7 @@ class Search(object): "NOT BETWEEN" if is_not else "BETWEEN", start.replace("*", "%"), end.replace("*", "%")) _query_sql = QUERY_CI_BY_ATTR_NAME.format(table_name, attr.id, range_query) + return _query_sql @staticmethod @@ -183,6 +204,7 @@ class Search(object): 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 @staticmethod @@ -194,6 +216,7 @@ class Search(object): elif field.startswith("-"): field = field[1:] sort_type = "DESC" + return field, sort_type def __sort_by_id(self, sort_type, query_sql): @@ -322,6 +345,11 @@ class Search(object): return numfound, res + def __get_type2filter_perms(self): + res2 = ACLManager('cmdb').get_resources(ResourceTypeEnum.CI_FILTER) + if res2: + self.type2filter_perms = CIFilterPermsCRUD().get_by_ids(list(map(int, [i['name'] for i in res2]))) + def __get_types_has_read(self): """ :return: _type:(type1;type2) @@ -331,14 +359,23 @@ class Search(object): self.valid_type_names = {i['name'] for i in res if PermEnum.READ in i['permissions']} - res2 = acl.get_resources(ResourceTypeEnum.CI_FILTER) - if res2: - self.type2filter_perms = CIFilterPermsCRUD().get_by_ids(list(map(int, [i['name'] for i in res2]))) + self.__get_type2filter_perms() + + for type_id in self.type2filter_perms: + ci_type = CITypeCache.get(type_id) + if ci_type: + if self.type2filter_perms[type_id].get('id_filter'): + if self.use_id_filter: + self.valid_type_names.add(ci_type.name) + elif self.type2filter_perms[type_id].get('ci_filter'): + if self.use_ci_filter: + self.valid_type_names.add(ci_type.name) + else: + self.valid_type_names.add(ci_type.name) return "_type:({})".format(";".join(self.valid_type_names)) def __confirm_type_first(self, queries): - has_type = False result = [] @@ -371,8 +408,10 @@ class Search(object): else: result.append(q) - _is_app_admin = is_app_admin('cmdb') or current_user.username == "worker" - if result and not has_type and not _is_app_admin: + if self.parent_node_perm_passed: + self.__get_type2filter_perms() + self.valid_type_names = "ALL" + elif result and not has_type and not self.is_app_admin: type_q = self.__get_types_has_read() if id_query: ci = CIManager.get_by_id(id_query) @@ -381,13 +420,11 @@ class Search(object): result.insert(0, "_type:{}".format(ci.type_id)) else: result.insert(0, type_q) - elif _is_app_admin: + elif self.is_app_admin: self.valid_type_names = "ALL" else: self.__get_types_has_read() - current_app.logger.warning(result) - return result def __query_by_attr(self, q, queries, alias): @@ -479,7 +516,7 @@ class Search(object): def _filter_ids(self, query_sql): if self.ci_ids: return "SELECT * FROM ({0}) AS IN_QUERY WHERE IN_QUERY.ci_id IN ({1})".format( - query_sql, ",".join(list(map(str, self.ci_ids)))) + query_sql, ",".join(list(set(map(str, self.ci_ids))))) return query_sql @@ -511,6 +548,9 @@ class Search(object): s = time.time() if query_sql: query_sql = self._filter_ids(query_sql) + if self.raw_ci_ids and not self.ci_ids: + return 0, [] + self.query_sql = query_sql # current_app.logger.debug(query_sql) numfound, res = self._execute_sql(query_sql) @@ -569,3 +609,8 @@ class Search(object): total = len(response) return response, counter, total, self.page, numfound, facet + + def get_ci_ids(self): + _, ci_ids = self._query_build_raw() + + return ci_ids diff --git a/cmdb-api/api/lib/cmdb/search/ci_relation/search.py b/cmdb-api/api/lib/cmdb/search/ci_relation/search.py index 627276e..5c890a1 100644 --- a/cmdb-api/api/lib/cmdb/search/ci_relation/search.py +++ b/cmdb-api/api/lib/cmdb/search/ci_relation/search.py @@ -1,9 +1,11 @@ # -*- coding:utf-8 -*- import json +import sys from collections import Counter from flask import abort from flask import current_app +from flask_login import current_user from api.extensions import rd from api.lib.cmdb.ci import CIRelationManager @@ -11,11 +13,14 @@ from api.lib.cmdb.ci_type import CITypeRelationManager from api.lib.cmdb.const import ConstraintEnum from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION2 +from api.lib.cmdb.const import ResourceTypeEnum +from api.lib.cmdb.perms import CIFilterPermsCRUD from api.lib.cmdb.resp_format import ErrFormat from api.lib.cmdb.search.ci.db.search import Search as SearchFromDB from api.lib.cmdb.search.ci.es.search import Search as SearchFromES +from api.lib.perm.acl.acl import ACLManager +from api.lib.perm.acl.acl import is_app_admin from api.models.cmdb import CI -from api.models.cmdb import CIRelation class Search(object): @@ -28,7 +33,10 @@ class Search(object): count=None, sort=None, reverse=False, - ancestor_ids=None): + ancestor_ids=None, + descendant_ids=None, + has_m2m=None, + root_parent_path=None): self.orig_query = query self.fl = fl self.facet_field = facet_field @@ -45,36 +53,35 @@ class Search(object): level[0] if isinstance(level, list) and level else level) self.ancestor_ids = ancestor_ids - self.has_m2m = False - if self.ancestor_ids: - self.has_m2m = True - else: - level = level[0] if isinstance(level, list) and level else level - for _l, c in self.level2constraint.items(): - if _l < int(level) and c == ConstraintEnum.Many2Many: - self.has_m2m = True + self.descendant_ids = descendant_ids + self.root_parent_path = root_parent_path + self.has_m2m = has_m2m or False + if not self.has_m2m: + if self.ancestor_ids: + self.has_m2m = True + else: + level = level[0] if isinstance(level, list) and level else level + for _l, c in self.level2constraint.items(): + if _l < int(level) and c == ConstraintEnum.Many2Many: + self.has_m2m = True + + self.type2filter_perms = None + + self.is_app_admin = is_app_admin('cmdb') or current_user.username == "worker" def _get_ids(self, ids): - if self.level[-1] == 1 and len(ids) == 1: - if self.ancestor_ids is None: - return [i.second_ci_id for i in CIRelation.get_by(first_ci_id=ids[0], to_dict=False)] - - else: - seconds = {i.second_ci_id for i in CIRelation.get_by(first_ci_id=ids[0], - ancestor_ids=self.ancestor_ids, - to_dict=False)} - - return list(seconds) merge_ids = [] key = [] _tmp = [] for level in range(1, sorted(self.level)[-1] + 1): + if len(self.descendant_ids) >= level and self.type2filter_perms.get(self.descendant_ids[level - 1]): + id_filter_limit, _ = self._get_ci_filter(self.type2filter_perms[self.descendant_ids[level - 1]]) + else: + id_filter_limit = {} + if not self.has_m2m: - _tmp = map(lambda x: json.loads(x).keys(), - filter(lambda x: x is not None, rd.get(ids, REDIS_PREFIX_CI_RELATION) or [])) - ids = [j for i in _tmp for j in i] - key, prefix = ids, REDIS_PREFIX_CI_RELATION + key, prefix = list(map(str, ids)), REDIS_PREFIX_CI_RELATION else: if not self.ancestor_ids: @@ -90,10 +97,14 @@ class Search(object): key = list(set(["{},{}".format(i, j) for idx, i in enumerate(key) for j in _tmp[idx]])) prefix = REDIS_PREFIX_CI_RELATION2 - if not key: + if not key or id_filter_limit is None: return [] - _tmp = list(map(lambda x: json.loads(x).keys() if x else [], rd.get(key, prefix) or [])) + res = [json.loads(x).items() for x in [i or '{}' for i in rd.get(key, prefix) or []]] + _tmp = [[i[0] for i in x if (not id_filter_limit or ( + key[idx] not in id_filter_limit or int(i[0]) in id_filter_limit[key[idx]]) or + int(i[0]) in id_filter_limit)] for idx, x in enumerate(res)] + ids = [j for i in _tmp for j in i] if level in self.level: @@ -118,7 +129,28 @@ class Search(object): return merge_ids + def _has_read_perm_from_parent_nodes(self): + self.root_parent_path = list(map(str, self.root_parent_path)) + if str(self.root_id).isdigit() and str(self.root_id) not in self.root_parent_path: + self.root_parent_path.append(str(self.root_id)) + self.root_parent_path = set(self.root_parent_path) + + if self.is_app_admin: + self.type2filter_perms = {} + return True + + res = ACLManager().get_resources(ResourceTypeEnum.CI_FILTER) or {} + self.type2filter_perms = CIFilterPermsCRUD().get_by_ids(list(map(int, [i['name'] for i in res]))) or {} + for _, filters in self.type2filter_perms.items(): + if set((filters.get('id_filter') or {}).keys()) & self.root_parent_path: + return True + + return True + def search(self): + use_ci_filter = len(self.descendant_ids) == self.level[0] - 1 + parent_node_perm_passed = self._has_read_perm_from_parent_nodes() + ids = [self.root_id] if not isinstance(self.root_id, list) else self.root_id cis = [CI.get_by_id(_id) or abort(404, ErrFormat.ci_not_found.format("id={}".format(_id))) for _id in ids] @@ -159,42 +191,105 @@ class Search(object): page=self.page, count=self.count, sort=self.sort, - ci_ids=merge_ids).search() + ci_ids=merge_ids, + parent_node_perm_passed=parent_node_perm_passed, + use_ci_filter=use_ci_filter).search() - def statistics(self, type_ids): + def _get_ci_filter(self, filter_perms, ci_filters=None): + ci_filters = ci_filters or [] + if ci_filters: + result = {} + for item in ci_filters: + res = SearchFromDB('_type:{},{}'.format(item['type_id'], item['ci_filter']), + count=sys.maxsize, parent_node_perm_passed=True).get_ci_ids() + if res: + result[item['type_id']] = set(res) + + return {}, result if result else None + + result = dict() + if filter_perms.get('id_filter'): + for k in filter_perms['id_filter']: + node_path = k.split(',') + if len(node_path) == 1: + result[int(node_path[0])] = 1 + elif not self.has_m2m: + result.setdefault(node_path[-2], set()).add(int(node_path[-1])) + else: + result.setdefault(','.join(node_path[:-1]), set()).add(int(node_path[-1])) + if result: + return result, None + else: + return None, None + + return {}, None + + def statistics(self, type_ids, need_filter=True): self.level = int(self.level) + acl = ACLManager('cmdb') + + type2filter_perms = dict() + if not self.is_app_admin: + res2 = acl.get_resources(ResourceTypeEnum.CI_FILTER) + if res2: + type2filter_perms = CIFilterPermsCRUD().get_by_ids(list(map(int, [i['name'] for i in res2]))) + ids = [self.root_id] if not isinstance(self.root_id, list) else self.root_id _tmp = [] level2ids = {} for lv in range(1, self.level + 1): level2ids[lv] = [] + if need_filter: + id_filter_limit, ci_filter_limit = None, None + if len(self.descendant_ids or []) >= lv and type2filter_perms.get(self.descendant_ids[lv - 1]): + id_filter_limit, _ = self._get_ci_filter(type2filter_perms[self.descendant_ids[lv - 1]]) + elif type_ids and self.level == lv: + ci_filters = [type2filter_perms[type_id] for type_id in type_ids if type_id in type2filter_perms] + if ci_filters: + id_filter_limit, ci_filter_limit = self._get_ci_filter({}, ci_filters=ci_filters) + else: + id_filter_limit = {} + else: + id_filter_limit = {} + else: + id_filter_limit, ci_filter_limit = {}, {} + if lv == 1: if not self.has_m2m: - key, prefix = ids, REDIS_PREFIX_CI_RELATION + key, prefix = [str(i) for i in ids], REDIS_PREFIX_CI_RELATION else: + key = ["{},{}".format(self.ancestor_ids, _id) for _id in ids] if not self.ancestor_ids: - key, prefix = ids, REDIS_PREFIX_CI_RELATION + key, prefix = [str(i) for i in ids], REDIS_PREFIX_CI_RELATION else: - key = ["{},{}".format(self.ancestor_ids, _id) for _id in ids] prefix = REDIS_PREFIX_CI_RELATION2 level2ids[lv] = [[i] for i in key] - if not key: - _tmp = [] + if not key or id_filter_limit is None: + _tmp = [[]] * len(ids) continue + res = [json.loads(x).items() for x in [i or '{}' for i in rd.get(key, prefix) or []]] + _tmp = [] if type_ids and lv == self.level: - _tmp = list(map(lambda x: [i for i in x if i[1] in type_ids], - (map(lambda x: list(json.loads(x).items()), - [i or '{}' for i in rd.get(key, prefix) or []])))) + _tmp = [[i for i in x if i[1] in type_ids and + (not id_filter_limit or (key[idx] not in id_filter_limit or + int(i[0]) in id_filter_limit[key[idx]]) or + int(i[0]) in id_filter_limit)] for idx, x in enumerate(res)] else: - _tmp = list(map(lambda x: list(json.loads(x).items()), - [i or '{}' for i in rd.get(key, prefix) or []])) + _tmp = [[i for i in x if (not id_filter_limit or (key[idx] not in id_filter_limit or + int(i[0]) in id_filter_limit[key[idx]]) or + int(i[0]) in id_filter_limit)] for idx, x in enumerate(res)] + + if ci_filter_limit: + _tmp = [[j for j in i if j[1] not in ci_filter_limit or int(j[0]) in ci_filter_limit[j[1]]] + for i in _tmp] else: + for idx, item in enumerate(_tmp): if item: if not self.has_m2m: @@ -206,19 +301,27 @@ class Search(object): level2ids[lv].append(key) if key: + res = [json.loads(x).items() for x in [i or '{}' for i in rd.get(key, prefix) or []]] if type_ids and lv == self.level: - __tmp = map(lambda x: [(_id, type_id) for _id, type_id in json.loads(x).items() - if type_id in type_ids], - filter(lambda x: x is not None, - rd.get(key, prefix) or [])) + __tmp = [[i for i in x if i[1] in type_ids and + (not id_filter_limit or ( + key[idx] not in id_filter_limit or + int(i[0]) in id_filter_limit[key[idx]]) or + int(i[0]) in id_filter_limit)] for idx, x in enumerate(res)] else: - __tmp = map(lambda x: list(json.loads(x).items()), - filter(lambda x: x is not None, - rd.get(key, prefix) or [])) + __tmp = [[i for i in x if (not id_filter_limit or ( + key[idx] not in id_filter_limit or + int(i[0]) in id_filter_limit[key[idx]]) or + int(i[0]) in id_filter_limit)] for idx, x in enumerate(res)] + + if ci_filter_limit: + __tmp = [[j for j in i if j[1] not in ci_filter_limit or + int(j[0]) in ci_filter_limit[j[1]]] for i in __tmp] else: __tmp = [] - _tmp[idx] = [j for i in __tmp for j in i] + if __tmp: + _tmp[idx] = [j for i in __tmp for j in i] else: _tmp[idx] = [] level2ids[lv].append([]) diff --git a/cmdb-api/api/lib/cmdb/utils.py b/cmdb-api/api/lib/cmdb/utils.py index 9ee096e..9358d23 100644 --- a/cmdb-api/api/lib/cmdb/utils.py +++ b/cmdb-api/api/lib/cmdb/utils.py @@ -11,12 +11,21 @@ import six import api.models.cmdb as model from api.lib.cmdb.cache import AttributeCache from api.lib.cmdb.const import ValueTypeEnum +from api.lib.cmdb.resp_format import ErrFormat -TIME_RE = re.compile(r"^20|21|22|23|[0-1]\d:[0-5]\d:[0-5]\d$") +TIME_RE = re.compile(r'(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d') + + +class ValueDeserializeError(Exception): + pass def string2int(x): - return int(float(x)) + v = int(float(x)) + if v > 2147483647: + raise ValueDeserializeError(ErrFormat.attribute_value_out_of_range) + + return v def str2datetime(x): diff --git a/cmdb-api/api/lib/cmdb/value.py b/cmdb-api/api/lib/cmdb/value.py index 7a2e6f4..8f98edd 100644 --- a/cmdb-api/api/lib/cmdb/value.py +++ b/cmdb-api/api/lib/cmdb/value.py @@ -6,6 +6,7 @@ from __future__ import unicode_literals import copy import imp import os +import re import tempfile import jinja2 @@ -13,6 +14,7 @@ from flask import abort from flask import current_app from jinja2schema import infer from jinja2schema import to_json_schema +from werkzeug.exceptions import BadRequest from api.extensions import db from api.lib.cmdb.attribute import AttributeManager @@ -23,6 +25,7 @@ from api.lib.cmdb.const import ValueTypeEnum from api.lib.cmdb.history import AttributeHistoryManger from api.lib.cmdb.resp_format import ErrFormat from api.lib.cmdb.utils import TableMap +from api.lib.cmdb.utils import ValueDeserializeError from api.lib.cmdb.utils import ValueTypeMap from api.lib.utils import handle_arg_list from api.models.cmdb import CI @@ -80,7 +83,7 @@ class AttributeValueManager(object): return res @staticmethod - def _deserialize_value(value_type, value): + def _deserialize_value(alias, value_type, value): if not value: return value @@ -88,14 +91,21 @@ class AttributeValueManager(object): try: v = deserialize(value) return v + except ValueDeserializeError as e: + return abort(400, ErrFormat.attribute_value_invalid2.format(alias, e)) except ValueError: return abort(400, ErrFormat.attribute_value_invalid.format(value)) @staticmethod def _check_is_choice(attr, value_type, value): 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)) + if value_type == ValueTypeEnum.FLOAT: + if float(value) not in list(map(float, [i[0] for i in choice_values])): + return abort(400, ErrFormat.not_in_choice_values.format(value)) + + else: + 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)) @staticmethod def _check_is_unique(value_table, attr, ci_id, type_id, value): @@ -112,9 +122,14 @@ class AttributeValueManager(object): if type_attr and type_attr.is_required and not value and value != 0: return abort(400, ErrFormat.attribute_value_required.format(attr.alias)) + @staticmethod + def check_re(expr, value): + if not re.compile(expr).match(str(value)): + return abort(400, ErrFormat.attribute_value_invalid.format(value)) + def _validate(self, attr, value, value_table, ci=None, type_id=None, ci_id=None, type_attr=None): ci = ci or {} - v = self._deserialize_value(attr.value_type, value) + v = self._deserialize_value(attr.alias, attr.value_type, value) attr.is_choice and value and self._check_is_choice(attr, attr.value_type, v) attr.is_unique and self._check_is_unique( @@ -125,6 +140,9 @@ class AttributeValueManager(object): if v == "" and attr.value_type not in (ValueTypeEnum.TEXT,): v = None + if attr.re_check and value: + self.check_re(attr.re_check, value) + return v @staticmethod @@ -132,9 +150,10 @@ class AttributeValueManager(object): return AttributeHistoryManger.add(record_id, ci_id, [(attr_id, operate_type, old, new)], type_id) @staticmethod - def write_change2(changed, record_id=None): + def write_change2(changed, record_id=None, ticket_id=None): for ci_id, attr_id, operate_type, old, new, type_id in changed: record_id = AttributeHistoryManger.add(record_id, ci_id, [(attr_id, operate_type, old, new)], type_id, + ticket_id=ticket_id, commit=False, flush=False) try: db.session.commit() @@ -227,6 +246,8 @@ class AttributeValueManager(object): value = self._validate(attr, value, value_table, ci=None, type_id=type_id, ci_id=ci_id, type_attr=ci_attr2type_attr.get(attr.id)) ci_dict[key] = value + except BadRequest as e: + raise except Exception as e: current_app.logger.warning(str(e)) @@ -235,12 +256,13 @@ class AttributeValueManager(object): return key2attr - def create_or_update_attr_value(self, ci, ci_dict, key2attr): + def create_or_update_attr_value(self, ci, ci_dict, key2attr, ticket_id=None): """ add or update attribute value, then write history :param ci: instance object :param ci_dict: attribute dict :param key2attr: attr key to attr + :param ticket_id: :return: """ changed = [] @@ -286,12 +308,12 @@ class AttributeValueManager(object): current_app.logger.warning(str(e)) return abort(400, ErrFormat.attribute_value_unknown_error.format(e.args[0])) - return self.write_change2(changed) + return self.write_change2(changed, ticket_id=ticket_id) @staticmethod - def delete_attr_value(attr_id, ci_id): + def delete_attr_value(attr_id, ci_id, commit=True): attr = AttributeCache.get(attr_id) if attr is not None: value_table = TableMap(attr=attr).table for item in value_table.get_by(attr_id=attr.id, ci_id=ci_id, to_dict=False): - item.delete() + item.delete(commit=commit) diff --git a/cmdb-api/api/lib/common_setting/common_data.py b/cmdb-api/api/lib/common_setting/common_data.py index 00c7398..93c6f6d 100644 --- a/cmdb-api/api/lib/common_setting/common_data.py +++ b/cmdb-api/api/lib/common_setting/common_data.py @@ -1,14 +1,24 @@ -from flask import abort +import copy +import json + +from flask import abort, current_app +from ldap3 import Connection +from ldap3 import Server +from ldap3.core.exceptions import LDAPBindError, LDAPSocketOpenError +from ldap3 import AUTO_BIND_NO_TLS from api.extensions import db from api.lib.common_setting.resp_format import ErrFormat from api.models.common_setting import CommonData +from api.lib.utils import AESCrypto +from api.lib.common_setting.const import AuthCommonConfig, AuthenticateType, AuthCommonConfigAutoRedirect, TestType class CommonDataCRUD(object): @staticmethod def get_data_by_type(data_type): + CommonDataCRUD.check_auth_type(data_type) return CommonData.get_by(data_type=data_type) @staticmethod @@ -18,6 +28,8 @@ class CommonDataCRUD(object): @staticmethod def create_new_data(data_type, **kwargs): try: + CommonDataCRUD.check_auth_type(data_type) + return CommonData.create(data_type=data_type, **kwargs) except Exception as e: db.session.rollback() @@ -29,6 +41,7 @@ class CommonDataCRUD(object): if not existed: abort(404, ErrFormat.common_data_not_found.format(_id)) try: + CommonDataCRUD.check_auth_type(existed.data_type) return existed.update(**kwargs) except Exception as e: db.session.rollback() @@ -40,7 +53,230 @@ class CommonDataCRUD(object): if not existed: abort(404, ErrFormat.common_data_not_found.format(_id)) try: + CommonDataCRUD.check_auth_type(existed.data_type) existed.soft_delete() except Exception as e: db.session.rollback() abort(400, str(e)) + + @staticmethod + def check_auth_type(data_type): + if data_type in list(AuthenticateType.all()) + [AuthCommonConfig]: + abort(400, ErrFormat.common_data_not_support_auth_type.format(data_type)) + + @staticmethod + def set_auth_auto_redirect_enable(_value: int): + existed = CommonData.get_by(first=True, data_type=AuthCommonConfig, to_dict=False) + if not existed: + CommonDataCRUD.create_new_data(AuthCommonConfig, data={AuthCommonConfigAutoRedirect: _value}) + else: + data = existed.data + data = copy.deepcopy(existed.data) if data else {} + data[AuthCommonConfigAutoRedirect] = _value + CommonDataCRUD.update_data(existed.id, data=data) + return True + + @staticmethod + def get_auth_auto_redirect_enable(): + existed = CommonData.get_by(first=True, data_type=AuthCommonConfig) + if not existed: + return 0 + data = existed.get('data', {}) + if not data: + return 0 + return data.get(AuthCommonConfigAutoRedirect, 0) + + +class AuthenticateDataCRUD(object): + common_type_list = [AuthCommonConfig] + + def __init__(self, _type): + self._type = _type + self.record = None + self.decrypt_data = {} + + def get_support_type_list(self): + return list(AuthenticateType.all()) + self.common_type_list + + def get(self): + if not self.decrypt_data: + self.decrypt_data = self.get_decrypt_data() + + return self.decrypt_data + + def get_by_key(self, _key): + if not self.decrypt_data: + self.decrypt_data = self.get_decrypt_data() + + return self.decrypt_data.get(_key, None) + + def get_record(self, to_dict=False) -> CommonData: + return CommonData.get_by(first=True, data_type=self._type, to_dict=to_dict) + + def get_record_with_decrypt(self) -> dict: + record = CommonData.get_by(first=True, data_type=self._type, to_dict=True) + if not record: + return {} + data = self.get_decrypt_dict(record.get('data', '')) + record['data'] = data + return record + + def get_decrypt_dict(self, data): + decrypt_str = self.decrypt(data) + try: + return json.loads(decrypt_str) + except Exception as e: + abort(400, str(e)) + + def get_decrypt_data(self) -> dict: + self.record = self.get_record() + if not self.record: + return self.get_from_config() + return self.get_decrypt_dict(self.record.data) + + def get_from_config(self): + return current_app.config.get(self._type, {}) + + def check_by_type(self) -> None: + existed = self.get_record() + if existed: + abort(400, ErrFormat.common_data_already_existed.format(self._type)) + + def create(self, data) -> CommonData: + self.check_by_type() + encrypt = data.pop('encrypt', None) + if encrypt is False: + return CommonData.create(data_type=self._type, data=data) + encrypted_data = self.encrypt(data) + try: + return CommonData.create(data_type=self._type, data=encrypted_data) + except Exception as e: + db.session.rollback() + abort(400, str(e)) + + def update_by_record(self, record, data) -> CommonData: + encrypt = data.pop('encrypt', None) + if encrypt is False: + return record.update(data=data) + encrypted_data = self.encrypt(data) + try: + return record.update(data=encrypted_data) + except Exception as e: + db.session.rollback() + abort(400, str(e)) + + def update(self, _id, data) -> CommonData: + existed = CommonData.get_by(first=True, to_dict=False, id=_id) + if not existed: + abort(404, ErrFormat.common_data_not_found.format(_id)) + + return self.update_by_record(existed, data) + + @staticmethod + def delete(_id) -> None: + existed = CommonData.get_by(first=True, to_dict=False, id=_id) + if not existed: + abort(404, ErrFormat.common_data_not_found.format(_id)) + try: + existed.soft_delete() + except Exception as e: + db.session.rollback() + abort(400, str(e)) + + @staticmethod + def encrypt(data) -> str: + if type(data) is dict: + try: + data = json.dumps(data) + except Exception as e: + abort(400, str(e)) + return AESCrypto().encrypt(data) + + @staticmethod + def decrypt(data) -> str: + return AESCrypto().decrypt(data) + + @staticmethod + def get_enable_list(): + all_records = CommonData.query.filter( + CommonData.data_type.in_(AuthenticateType.all()), + CommonData.deleted == 0 + ).all() + enable_list = [] + for auth_type in AuthenticateType.all(): + record = list(filter(lambda x: x.data_type == auth_type, all_records)) + if not record: + config = current_app.config.get(auth_type, None) + if not config: + continue + + if config.get('enable', False): + enable_list.append(dict( + auth_type=auth_type, + )) + + continue + + try: + decrypt_data = json.loads(AuthenticateDataCRUD.decrypt(record[0].data)) + except Exception as e: + current_app.logger.error(e) + continue + + if decrypt_data.get('enable', 0) == 1: + enable_list.append(dict( + auth_type=auth_type, + )) + + auth_auto_redirect = CommonDataCRUD.get_auth_auto_redirect_enable() + + return dict( + enable_list=enable_list, + auth_auto_redirect=auth_auto_redirect, + ) + + def test(self, test_type, data): + type_lower = self._type.lower() + func_name = f'test_{type_lower}' + if hasattr(self, func_name): + try: + return getattr(self, f'test_{type_lower}')(test_type, data) + except Exception as e: + abort(400, str(e)) + abort(400, ErrFormat.not_support_test.format(self._type)) + + @staticmethod + def test_ldap(test_type, data): + ldap_server = data.get('ldap_server') + ldap_user_dn = data.get('ldap_user_dn', '{}') + + server = Server(ldap_server, connect_timeout=2) + if not server.check_availability(): + raise Exception(ErrFormat.ldap_server_connect_not_available) + else: + if test_type == TestType.Connect: + return True + + username = data.get('username', None) + if not username: + raise Exception(ErrFormat.ldap_test_username_required) + user = ldap_user_dn.format(username) + password = data.get('password', None) + + try: + Connection(server, user=user, password=password, auto_bind=AUTO_BIND_NO_TLS) + except LDAPBindError: + ldap_domain = data.get('ldap_domain') + user_with_domain = f"{username}@{ldap_domain}" + try: + Connection(server, user=user_with_domain, password=password, auto_bind=AUTO_BIND_NO_TLS) + except Exception as e: + raise Exception(ErrFormat.ldap_test_unknown_error.format(str(e))) + + except LDAPSocketOpenError: + raise Exception(ErrFormat.ldap_server_connect_timeout) + + except Exception as e: + raise Exception(ErrFormat.ldap_test_unknown_error.format(str(e))) + + return True diff --git a/cmdb-api/api/lib/common_setting/company_info.py b/cmdb-api/api/lib/common_setting/company_info.py index c700fea..7031a2f 100644 --- a/cmdb-api/api/lib/common_setting/company_info.py +++ b/cmdb-api/api/lib/common_setting/company_info.py @@ -1,4 +1,6 @@ # -*- coding:utf-8 -*- +from urllib.parse import urlparse + from api.extensions import cache from api.models.common_setting import CompanyInfo @@ -11,6 +13,7 @@ class CompanyInfoCRUD(object): @staticmethod def create(**kwargs): + CompanyInfoCRUD.check_data(**kwargs) res = CompanyInfo.create(**kwargs) CompanyInfoCache.refresh(res.info) return res @@ -22,10 +25,26 @@ class CompanyInfoCRUD(object): if not existed: existed = CompanyInfoCRUD.create(**kwargs) else: + CompanyInfoCRUD.check_data(**kwargs) existed = existed.update(**kwargs) CompanyInfoCache.refresh(existed.info) return existed + @staticmethod + def check_data(**kwargs): + info = kwargs.get('info', {}) + info['messenger'] = CompanyInfoCRUD.check_messenger(info.get('messenger', None)) + + kwargs['info'] = info + + @staticmethod + def check_messenger(messenger): + if not messenger: + return messenger + + parsed_url = urlparse(messenger) + return f"{parsed_url.scheme}://{parsed_url.netloc}" + class CompanyInfoCache(object): key = 'CompanyInfoCache::' @@ -41,4 +60,4 @@ class CompanyInfoCache(object): @classmethod def refresh(cls, info): - cache.set(cls.key, info) \ No newline at end of file + cache.set(cls.key, info) diff --git a/cmdb-api/api/lib/common_setting/const.py b/cmdb-api/api/lib/common_setting/const.py index bb585d6..a68dcbf 100644 --- a/cmdb-api/api/lib/common_setting/const.py +++ b/cmdb-api/api/lib/common_setting/const.py @@ -19,3 +19,48 @@ BotNameMap = { 'feishuApp': 'feishuBot', 'dingdingApp': 'dingdingBot', } + + +class AuthenticateType(BaseEnum): + CAS = 'CAS' + OAUTH2 = 'OAUTH2' + OIDC = 'OIDC' + LDAP = 'LDAP' + + +AuthCommonConfig = 'AuthCommonConfig' +AuthCommonConfigAutoRedirect = 'auto_redirect' + + +class TestType(BaseEnum): + Connect = 'connect' + Login = 'login' + + +MIMEExtMap = { + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx', + 'application/msword': '.doc', + 'application/vnd.ms-word.document.macroEnabled.12': '.docm', + 'application/vnd.ms-excel': '.xls', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx', + 'application/vnd.ms-excel.sheet.macroEnabled.12': '.xlsm', + 'application/vnd.ms-powerpoint': '.ppt', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation': '.pptx', + 'application/vnd.ms-powerpoint.presentation.macroEnabled.12': '.pptm', + 'application/zip': '.zip', + 'application/x-7z-compressed': '.7z', + 'application/json': '.json', + 'application/pdf': '.pdf', + 'image/png': '.png', + 'image/bmp': '.bmp', + 'image/prs.btif': '.btif', + 'image/gif': '.gif', + 'image/jpeg': '.jpg', + 'image/tiff': '.tif', + 'image/vnd.microsoft.icon': '.ico', + 'image/webp': '.webp', + 'image/svg+xml': '.svg', + 'image/vnd.adobe.photoshop': '.psd', + 'text/plain': '.txt', + 'text/csv': '.csv', +} diff --git a/cmdb-api/api/lib/common_setting/department.py b/cmdb-api/api/lib/common_setting/department.py index f068b53..7d72bc6 100644 --- a/cmdb-api/api/lib/common_setting/department.py +++ b/cmdb-api/api/lib/common_setting/department.py @@ -24,7 +24,15 @@ def get_all_department_list(to_dict=True): *criterion ).order_by(Department.department_id.asc()) results = query.all() - return [r.to_dict() for r in results] if to_dict else results + if to_dict: + datas = [] + for r in results: + d = r.to_dict() + if r.department_id == 0: + d['department_name'] = ErrFormat.company_wide + datas.append(d) + return datas + return results def get_all_employee_list(block=0, to_dict=True): @@ -101,6 +109,7 @@ class DepartmentTree(object): employees = self.get_employees_by_d_id(department_id) top_d['employees'] = employees + top_d['department_name'] = ErrFormat.company_wide if len(sub_deps) == 0: top_d[sub_departments_column_name] = [] d_list.append(top_d) @@ -246,7 +255,7 @@ class DepartmentCRUD(object): return abort(400, ErrFormat.acl_update_role_failed.format(str(e))) try: - existed.update(**kwargs) + return existed.update(**kwargs) except Exception as e: return abort(400, str(e)) @@ -313,6 +322,7 @@ class DepartmentCRUD(object): tree_list = [] for top_d in top_deps: + top_d['department_name'] = ErrFormat.company_wide tree = Tree() identifier_root = top_d['department_id'] tree.create_node( @@ -383,6 +393,9 @@ class DepartmentCRUD(object): d['employee_count'] = len(list(filter(lambda e: e['department_id'] in d_ids, all_employee_list))) + if int(department_parent_id) == -1: + d['department_name'] = ErrFormat.company_wide + return all_departments, department_id_list @staticmethod @@ -457,8 +470,58 @@ class EditDepartmentInACL(object): return f"edit_department_name_in_acl, rid: {d_rid}, success" + @classmethod + def remove_from_old_department_role(cls, e_list, acl): + result = [] + for employee in e_list: + employee_acl_rid = employee.get('e_acl_rid') + if employee_acl_rid == 0: + result.append(f"employee_acl_rid == 0") + continue + cls.remove_single_employee_from_old_department(acl, employee, result) + @staticmethod - def edit_employee_department_in_acl(e_list: list, new_d_id: int, op_uid: int): + def remove_single_employee_from_old_department(acl, employee, result): + from api.models.acl import Role + old_department = DepartmentCRUD.get_department_by_id(employee.get('department_id'), False) + if not old_department: + return False + + old_role = Role.get_by(first=True, name=old_department.department_name, app_id=None) + old_d_rid_in_acl = old_role.get('id') if old_role else 0 + if old_d_rid_in_acl == 0: + return False + + d_acl_rid = old_department.acl_rid if old_d_rid_in_acl == old_department.acl_rid else old_d_rid_in_acl + payload = { + 'app_id': 'acl', + 'parent_id': d_acl_rid, + } + try: + acl.remove_user_from_role(employee.get('e_acl_rid'), payload) + current_app.logger.info(f"remove {employee.get('e_acl_rid')} from {d_acl_rid}") + except Exception as e: + result.append( + f"remove_user_from_role employee_acl_rid: {employee.get('e_acl_rid')}, parent_id: {d_acl_rid}, err: {e}") + + return True + + @staticmethod + def add_employee_to_new_department(acl, employee_acl_rid, new_department_acl_rid, result): + payload = { + 'app_id': 'acl', + 'child_ids': [employee_acl_rid], + } + try: + acl.add_user_to_role(new_department_acl_rid, payload) + current_app.logger.info(f"add {employee_acl_rid} to {new_department_acl_rid}") + except Exception as e: + result.append( + f"add_user_to_role employee_acl_rid: {employee_acl_rid}, parent_id: {new_department_acl_rid}, \ + err: {e}") + + @classmethod + def edit_employee_department_in_acl(cls, e_list: list, new_d_id: int, op_uid: int): result = [] new_department = DepartmentCRUD.get_department_by_id(new_d_id, False) if not new_department: @@ -468,7 +531,11 @@ class EditDepartmentInACL(object): from api.models.acl import Role new_role = Role.get_by(first=True, name=new_department.department_name, app_id=None) new_d_rid_in_acl = new_role.get('id') if new_role else 0 + acl = ACLManager('acl', str(op_uid)) + if new_d_rid_in_acl == 0: + # only remove from old department role + cls.remove_from_old_department_role(e_list, acl) return if new_d_rid_in_acl != new_department.acl_rid: @@ -478,43 +545,15 @@ class EditDepartmentInACL(object): new_department_acl_rid = new_department.acl_rid if new_d_rid_in_acl == new_department.acl_rid else \ new_d_rid_in_acl - acl = ACLManager('acl', str(op_uid)) for employee in e_list: - old_department = DepartmentCRUD.get_department_by_id(employee.get('department_id'), False) - if not old_department: - continue employee_acl_rid = employee.get('e_acl_rid') if employee_acl_rid == 0: result.append(f"employee_acl_rid == 0") continue - old_role = Role.get_by(first=True, name=old_department.department_name, app_id=None) - old_d_rid_in_acl = old_role.get('id') if old_role else 0 - if old_d_rid_in_acl == 0: - return - if old_d_rid_in_acl != old_department.acl_rid: - old_department.update( - acl_rid=old_d_rid_in_acl - ) - d_acl_rid = old_department.acl_rid if old_d_rid_in_acl == old_department.acl_rid else old_d_rid_in_acl - payload = { - 'app_id': 'acl', - 'parent_id': d_acl_rid, - } - try: - acl.remove_user_from_role(employee_acl_rid, payload) - except Exception as e: - result.append( - f"remove_user_from_role employee_acl_rid: {employee_acl_rid}, parent_id: {d_acl_rid}, err: {e}") + cls.remove_single_employee_from_old_department(acl, employee, result) - payload = { - 'app_id': 'acl', - 'child_ids': [employee_acl_rid], - } - try: - acl.add_user_to_role(new_department_acl_rid, payload) - except Exception as e: - result.append( - f"add_user_to_role employee_acl_rid: {employee_acl_rid}, parent_id: {d_acl_rid}, err: {e}") + # 在新部门中添加员工 + cls.add_employee_to_new_department(acl, employee_acl_rid, new_department_acl_rid, result) return result diff --git a/cmdb-api/api/lib/common_setting/employee.py b/cmdb-api/api/lib/common_setting/employee.py index cb75d4c..ca7173f 100644 --- a/cmdb-api/api/lib/common_setting/employee.py +++ b/cmdb-api/api/lib/common_setting/employee.py @@ -15,10 +15,13 @@ from wtforms import validators from api.extensions import db from api.lib.common_setting.acl import ACLManager -from api.lib.common_setting.const import COMMON_SETTING_QUEUE, OperatorType +from api.lib.common_setting.const import OperatorType +from api.lib.perm.acl.const import ACL_QUEUE from api.lib.common_setting.resp_format import ErrFormat from api.models.common_setting import Employee, Department +from api.tasks.common_setting import refresh_employee_acl_info, edit_employee_department_in_acl + acl_user_columns = [ 'email', 'mobile', @@ -137,7 +140,9 @@ class EmployeeCRUD(object): @staticmethod def add(**kwargs): try: - return CreateEmployee().create_single(**kwargs) + res = CreateEmployee().create_single(**kwargs) + refresh_employee_acl_info.apply_async(args=(res.employee_id,), queue=ACL_QUEUE) + return res except Exception as e: abort(400, str(e)) @@ -164,10 +169,9 @@ class EmployeeCRUD(object): existed.update(**kwargs) if len(e_list) > 0: - from api.tasks.common_setting import edit_employee_department_in_acl edit_employee_department_in_acl.apply_async( args=(e_list, new_department_id, current_user.uid), - queue=COMMON_SETTING_QUEUE + queue=ACL_QUEUE ) return existed @@ -291,7 +295,9 @@ class EmployeeCRUD(object): employees = [] for r in pagination.items: d = r.Employee.to_dict() - d['department_name'] = r.Department.department_name + d['department_name'] = r.Department.department_name if r.Department else '' + if r.Employee.department_id == 0: + d['department_name'] = ErrFormat.company_wide employees.append(d) return { @@ -437,7 +443,7 @@ class EmployeeCRUD(object): employees = [] for r in pagination.items: d = r.Employee.to_dict() - d['department_name'] = r.Department.department_name + d['department_name'] = r.Department.department_name if r.Department else '' employees.append(d) return { @@ -563,6 +569,7 @@ class EmployeeCRUD(object): for column in direct_columns: tmp[column] = d.get(column, '') notice_info = d.get('notice_info', {}) + notice_info = copy.deepcopy(notice_info) if notice_info else {} tmp.update(**notice_info) results.append(tmp) return results @@ -686,6 +693,27 @@ class EmployeeCRUD(object): else: abort(400, ErrFormat.column_name_not_support) + @staticmethod + def update_last_login_by_uid(uid, last_login=None): + employee = Employee.get_by(acl_uid=uid, first=True, to_dict=False) + if not employee: + return + if last_login: + try: + last_login = datetime.strptime(last_login, '%Y-%m-%d %H:%M:%S') + except Exception as e: + last_login = datetime.now() + else: + last_login = datetime.now() + + try: + employee.update( + last_login=last_login + ) + return last_login + except Exception as e: + return + def get_user_map(key='uid', acl=None): """ @@ -726,6 +754,7 @@ class CreateEmployee(object): try: existed = self.check_acl_user(user_data) if not existed: + user_data['add_from'] = 'common' return self.acl.create_user(user_data) return existed except Exception as e: @@ -758,9 +787,11 @@ class CreateEmployee(object): if existed: return existed - return Employee.create( + res = Employee.create( **kwargs ) + refresh_employee_acl_info.apply_async(args=(res.employee_id,), queue=ACL_QUEUE) + return res @staticmethod def get_department_by_name(d_name): @@ -867,3 +898,75 @@ class EmployeeUpdateByUidForm(Form): avatar = StringField(validators=[]) sex = StringField(validators=[]) mobile = StringField(validators=[]) + + +class GrantEmployeeACLPerm(object): + """ + Grant ACL Permission After Create New Employee + """ + + def __init__(self, acl=None): + self.perms_by_create_resources_type = ['read', 'grant', 'delete', 'update'] + self.perms_by_common_grant = ['read'] + self.resource_name_list = ['公司信息', '公司架构', '通知设置'] + + self.acl = acl if acl else self.check_app('backend') + self.resources_types = self.acl.get_all_resources_types() + self.resources_type = self.get_resources_type() + self.resource_list = self.acl.get_resource_by_type(None, None, self.resources_type['id']) + + @staticmethod + def check_app(app_name): + acl = ACLManager(app_name) + payload = dict( + name=app_name, + description=app_name + ) + app = acl.validate_app() + if not app: + acl.create_app(payload) + return acl + + def get_resources_type(self): + results = list(filter(lambda t: t['name'] == '操作权限', self.resources_types['groups'])) + if len(results) == 0: + payload = dict( + app_id=self.acl.app_name, + name='操作权限', + description='', + perms=self.perms_by_create_resources_type + ) + resource_type = self.acl.create_resources_type(payload) + else: + resource_type = results[0] + resource_type_id = resource_type['id'] + existed_perms = self.resources_types.get('id2perms', {}).get(resource_type_id, []) + existed_perms = [p['name'] for p in existed_perms] + new_perms = [] + for perm in self.perms_by_create_resources_type: + if perm not in existed_perms: + new_perms.append(perm) + if len(new_perms) > 0: + resource_type['perms'] = existed_perms + new_perms + self.acl.update_resources_type(resource_type_id, resource_type) + + return resource_type + + def grant(self, rid_list): + [self.grant_by_rid(rid) for rid in rid_list if rid > 0] + + def grant_by_rid(self, rid, is_admin=False): + for name in self.resource_name_list: + resource = list(filter(lambda r: r['name'] == name, self.resource_list)) + if len(resource) == 0: + payload = dict( + type_id=self.resources_type['id'], + app_id=self.acl.app_name, + name=name, + ) + resource = self.acl.create_resource(payload) + else: + resource = resource[0] + + perms = self.perms_by_create_resources_type if is_admin else self.perms_by_common_grant + self.acl.grant_resource(rid, resource['id'], perms) diff --git a/cmdb-api/api/lib/common_setting/resp_format.py b/cmdb-api/api/lib/common_setting/resp_format.py index 4c2d6f7..676e17b 100644 --- a/cmdb-api/api/lib/common_setting/resp_format.py +++ b/cmdb-api/api/lib/common_setting/resp_format.py @@ -1,65 +1,82 @@ # -*- coding:utf-8 -*- +from flask_babel import lazy_gettext as _l from api.lib.resp_format import CommonErrFormat class ErrFormat(CommonErrFormat): - company_info_is_already_existed = "公司信息已存在!无法创建" + company_info_is_already_existed = _l("Company info already existed") # 公司信息已存在!无法创建 - no_file_part = "没有文件部分" - file_is_required = "文件是必须的" + no_file_part = _l("No file part") # 没有文件部分 + file_is_required = _l("File is required") # 文件是必须的 + file_not_found = _l("File not found") # 文件不存在 + file_type_not_allowed = _l("File type not allowed") # 文件类型不允许 + upload_failed = _l("Upload failed: {}") # 上传失败: {} - direct_supervisor_is_not_self = "直属上级不能是自己" - parent_department_is_not_self = "上级部门不能是自己" - employee_list_is_empty = "员工列表为空" + direct_supervisor_is_not_self = _l("Direct supervisor is not self") # 直属上级不能是自己 + parent_department_is_not_self = _l("Parent department is not self") # 上级部门不能是自己 + employee_list_is_empty = _l("Employee list is empty") # 员工列表为空 - column_name_not_support = "不支持的列名" - password_is_required = "密码不能为空" - employee_acl_rid_is_zero = "员工ACL角色ID不能为0" + column_name_not_support = _l("Column name not support") # 不支持的列名 + password_is_required = _l("Password is required") # 密码是必须的 + employee_acl_rid_is_zero = _l("Employee acl rid is zero") # 员工ACL角色ID不能为0 - generate_excel_failed = "生成excel失败: {}" - rename_columns_failed = "字段转换为中文失败: {}" - cannot_block_this_employee_is_other_direct_supervisor = "该员工是其他员工的直属上级, 不能禁用" - cannot_block_this_employee_is_department_manager = "该员工是部门负责人, 不能禁用" - employee_id_not_found = "员工ID [{}] 不存在" - value_is_required = "值是必须的" - email_already_exists = "邮箱 [{}] 已存在" - query_column_none_keep_value_empty = "查询 {} 空值时请保持value为空" - not_support_operator = "不支持的操作符: {}" - not_support_relation = "不支持的关系: {}" - conditions_field_missing = "conditions内元素字段缺失,请检查!" - datetime_format_error = "{} 格式错误,应该为:%Y-%m-%d %H:%M:%S" - department_level_relation_error = "部门层级关系不正确" - delete_reserved_department_name = "保留部门,无法删除!" - department_id_is_required = "部门ID是必须的" - department_list_is_required = "部门列表是必须的" - cannot_to_be_parent_department = "{} 不能设置为上级部门" - department_id_not_found = "部门ID [{}] 不存在" - parent_department_id_must_more_than_zero = "上级部门ID必须大于0" - department_name_already_exists = "部门名称 [{}] 已存在" - new_department_is_none = "新部门是空的" + generate_excel_failed = _l("Generate excel failed: {}") # 生成excel失败: {} + rename_columns_failed = _l("Rename columns failed: {}") # 重命名字段失败: {} + cannot_block_this_employee_is_other_direct_supervisor = _l( + "Cannot block this employee is other direct supervisor") # 该员工是其他员工的直属上级, 不能禁用 + cannot_block_this_employee_is_department_manager = _l( + "Cannot block this employee is department manager") # 该员工是部门负责人, 不能禁用 + employee_id_not_found = _l("Employee id [{}] not found") # 员工ID [{}] 不存在 + value_is_required = _l("Value is required") # 值是必须的 + email_already_exists = _l("Email already exists") # 邮箱已存在 + query_column_none_keep_value_empty = _l("Query {} none keep value empty") # 查询 {} 空值时请保持value为空" + not_support_operator = _l("Not support operator: {}") # 不支持的操作符: {} + not_support_relation = _l("Not support relation: {}") # 不支持的关系: {} + conditions_field_missing = _l("Conditions field missing") # conditions内元素字段缺失,请检查! + datetime_format_error = _l("Datetime format error: {}") # {} 格式错误,应该为:%Y-%m-%d %H:%M:%S + department_level_relation_error = _l("Department level relation error") # 部门层级关系不正确 + delete_reserved_department_name = _l("Delete reserved department name") # 保留部门,无法删除! + department_id_is_required = _l("Department id is required") # 部门ID是必须的 + department_list_is_required = _l("Department list is required") # 部门列表是必须的 + cannot_to_be_parent_department = _l("{} Cannot to be parent department") # 不能设置为上级部门 + department_id_not_found = _l("Department id [{}] not found") # 部门ID [{}] 不存在 + parent_department_id_must_more_than_zero = _l("Parent department id must more than zero") # 上级部门ID必须大于0 + department_name_already_exists = _l("Department name [{}] already exists") # 部门名称 [{}] 已存在 + new_department_is_none = _l("New department is none") # 新部门是空的 - acl_edit_user_failed = "ACL 修改用户失败: {}" - acl_uid_not_found = "ACL 用户UID [{}] 不存在" - acl_add_user_failed = "ACL 添加用户失败: {}" - acl_add_role_failed = "ACL 添加角色失败: {}" - acl_update_role_failed = "ACL 更新角色失败: {}" - acl_get_all_users_failed = "ACL 获取所有用户失败: {}" - acl_remove_user_from_role_failed = "ACL 从角色中移除用户失败: {}" - acl_add_user_to_role_failed = "ACL 添加用户到角色失败: {}" - acl_import_user_failed = "ACL 导入用户[{}]失败: {}" + acl_edit_user_failed = _l("ACL edit user failed: {}") # ACL 修改用户失败: {} + acl_uid_not_found = _l("ACL uid not found: {}") # ACL 用户UID [{}] 不存在 + acl_add_user_failed = _l("ACL add user failed: {}") # ACL 添加用户失败: {} + acl_add_role_failed = _l("ACL add role failed: {}") # ACL 添加角色失败: {} + acl_update_role_failed = _l("ACL update role failed: {}") # ACL 更新角色失败: {} + acl_get_all_users_failed = _l("ACL get all users failed: {}") # ACL 获取所有用户失败: {} + acl_remove_user_from_role_failed = _l("ACL remove user from role failed: {}") # ACL 从角色中移除用户失败: {} + acl_add_user_to_role_failed = _l("ACL add user to role failed: {}") # ACL 添加用户到角色失败: {} + acl_import_user_failed = _l("ACL import user failed: {}") # ACL 导入用户失败: {} - nickname_is_required = "用户名不能为空" - username_is_required = "username不能为空" - email_is_required = "邮箱不能为空" - email_format_error = "邮箱格式错误" - email_send_timeout = "邮件发送超时" + nickname_is_required = _l("Nickname is required") # 昵称不能为空 + username_is_required = _l("Username is required") # 用户名不能为空 + email_is_required = _l("Email is required") # 邮箱不能为空 + email_format_error = _l("Email format error") # 邮箱格式错误 + email_send_timeout = _l("Email send timeout") # 邮件发送超时 - common_data_not_found = "ID {} 找不到记录" - notice_platform_existed = "{} 已存在" - notice_not_existed = "{} 配置项不存在" - notice_please_config_messenger_first = "请先配置 messenger" - notice_bind_err_with_empty_mobile = "绑定失败,手机号为空" - notice_bind_failed = "绑定失败: {}" - notice_bind_success = "绑定成功" - notice_remove_bind_success = "解绑成功" + common_data_not_found = _l("Common data not found {} ") # ID {} 找不到记录 + common_data_already_existed = _l("Common data {} already existed") # {} 已存在 + notice_platform_existed = _l("Notice platform {} existed") # {} 已存在 + notice_not_existed = _l("Notice {} not existed") # {} 配置项不存在 + notice_please_config_messenger_first = _l("Notice please config messenger first") # 请先配置messenger URL + notice_bind_err_with_empty_mobile = _l("Notice bind err with empty mobile") # 绑定错误,手机号为空 + notice_bind_failed = _l("Notice bind failed: {}") # 绑定失败: {} + notice_bind_success = _l("Notice bind success") # 绑定成功 + notice_remove_bind_success = _l("Notice remove bind success") # 解绑成功 + + not_support_test = _l("Not support test type: {}") # 不支持的测试类型: {} + not_support_auth_type = _l("Not support auth type: {}") # 不支持的认证类型: {} + ldap_server_connect_timeout = _l("LDAP server connect timeout") # LDAP服务器连接超时 + ldap_server_connect_not_available = _l("LDAP server connect not available") # LDAP服务器连接不可用 + ldap_test_unknown_error = _l("LDAP test unknown error: {}") # LDAP测试未知错误: {} + common_data_not_support_auth_type = _l("Common data not support auth type: {}") # 通用数据不支持auth类型: {} + ldap_test_username_required = _l("LDAP test username required") # LDAP测试用户名必填 + + company_wide = _l("Company wide") # 全公司 diff --git a/cmdb-api/api/lib/common_setting/upload_file.py b/cmdb-api/api/lib/common_setting/upload_file.py index 85fe697..f63bfc0 100644 --- a/cmdb-api/api/lib/common_setting/upload_file.py +++ b/cmdb-api/api/lib/common_setting/upload_file.py @@ -1,6 +1,14 @@ +import base64 import uuid +import os +from io import BytesIO + +from flask import abort, current_app +import lz4.frame from api.lib.common_setting.utils import get_cur_time_str +from api.models.common_setting import CommonFile +from api.lib.common_setting.resp_format import ErrFormat def allowed_file(filename, allowed_extensions): @@ -14,3 +22,73 @@ def generate_new_file_name(name): cur_str = get_cur_time_str('_') return f"{prev_name}_{cur_str}_{uid}.{ext}" + + +class CommonFileCRUD: + @staticmethod + def add_file(**kwargs): + return CommonFile.create(**kwargs) + + @staticmethod + def get_file(file_name, to_str=False): + existed = CommonFile.get_by(file_name=file_name, first=True, to_dict=False) + if not existed: + abort(400, ErrFormat.file_not_found) + + uncompressed_data = lz4.frame.decompress(existed.binary) + + return base64.b64encode(uncompressed_data).decode('utf-8') if to_str else BytesIO(uncompressed_data) + + @staticmethod + def sync_file_to_db(): + for p in ['UPLOAD_DIRECTORY_FULL']: + upload_path = current_app.config.get(p, None) + if not upload_path: + continue + for root, dirs, files in os.walk(upload_path): + for file in files: + file_path = os.path.join(root, file) + if not os.path.isfile(file_path): + continue + + existed = CommonFile.get_by(file_name=file, first=True, to_dict=False) + if existed: + continue + with open(file_path, 'rb') as f: + data = f.read() + compressed_data = lz4.frame.compress(data) + try: + CommonFileCRUD.add_file( + origin_name=file, + file_name=file, + binary=compressed_data + ) + + current_app.logger.info(f'sync file {file} to db') + except Exception as e: + current_app.logger.error(f'sync file {file} to db error: {e}') + + def get_file_binary_str(self, file_name): + return self.get_file(file_name, True) + + def save_str_to_file(self, file_name, str_data): + try: + self.get_file(file_name) + current_app.logger.info(f'file {file_name} already exists') + return + except Exception as e: + # file not found + pass + + bytes_data = base64.b64decode(str_data) + compressed_data = lz4.frame.compress(bytes_data) + + try: + self.add_file( + origin_name=file_name, + file_name=file_name, + binary=compressed_data + ) + current_app.logger.info(f'save_str_to_file {file_name} success') + except Exception as e: + current_app.logger.error(f"save_str_to_file error: {e}") diff --git a/cmdb-api/api/lib/database.py b/cmdb-api/api/lib/database.py index d991d1a..634a99f 100644 --- a/cmdb-api/api/lib/database.py +++ b/cmdb-api/api/lib/database.py @@ -94,7 +94,7 @@ class CRUDMixin(FormatMixin): if any((isinstance(_id, six.string_types) and _id.isdigit(), isinstance(_id, (six.integer_types, float))), ): obj = getattr(cls, "query").get(int(_id)) - if obj and not obj.deleted: + if obj and not getattr(obj, 'deleted', False): return obj @classmethod diff --git a/cmdb-api/api/lib/perm/acl/acl.py b/cmdb-api/api/lib/perm/acl/acl.py index 423d3be..85bf1e9 100644 --- a/cmdb-api/api/lib/perm/acl/acl.py +++ b/cmdb-api/api/lib/perm/acl/acl.py @@ -117,15 +117,15 @@ class ACLManager(object): if group: PermissionCRUD.grant(role.id, permissions, group_id=group.id) - def grant_resource_to_role_by_rid(self, name, rid, resource_type_name=None, permissions=None): + def grant_resource_to_role_by_rid(self, name, rid, resource_type_name=None, permissions=None, rebuild=True): resource = self._get_resource(name, resource_type_name) if resource: - PermissionCRUD.grant(rid, permissions, resource_id=resource.id) + PermissionCRUD.grant(rid, permissions, resource_id=resource.id, rebuild=rebuild) else: group = self._get_resource_group(name) if group: - PermissionCRUD.grant(rid, permissions, group_id=group.id) + PermissionCRUD.grant(rid, permissions, group_id=group.id, rebuild=rebuild) def revoke_resource_from_role(self, name, role, resource_type_name=None, permissions=None): resource = self._get_resource(name, resource_type_name) @@ -138,20 +138,20 @@ class ACLManager(object): if group: PermissionCRUD.revoke(role.id, permissions, group_id=group.id) - def revoke_resource_from_role_by_rid(self, name, rid, resource_type_name=None, permissions=None): + def revoke_resource_from_role_by_rid(self, name, rid, resource_type_name=None, permissions=None, rebuild=True): resource = self._get_resource(name, resource_type_name) if resource: - PermissionCRUD.revoke(rid, permissions, resource_id=resource.id) + PermissionCRUD.revoke(rid, permissions, resource_id=resource.id, rebuild=rebuild) else: group = self._get_resource_group(name) if group: - PermissionCRUD.revoke(rid, permissions, group_id=group.id) + PermissionCRUD.revoke(rid, permissions, group_id=group.id, rebuild=rebuild) - def del_resource(self, name, resource_type_name=None): + def del_resource(self, name, resource_type_name=None, rebuild=True): resource = self._get_resource(name, resource_type_name) if resource: - ResourceCRUD.delete(resource.id) + return ResourceCRUD.delete(resource.id, rebuild=rebuild) def has_permission(self, resource_name, resource_type, perm, resource_id=None): if is_app_admin(self.app_id): diff --git a/cmdb-api/api/lib/perm/acl/audit.py b/cmdb-api/api/lib/perm/acl/audit.py index 4732b9c..1682e36 100644 --- a/cmdb-api/api/lib/perm/acl/audit.py +++ b/cmdb-api/api/lib/perm/acl/audit.py @@ -1,14 +1,19 @@ # -*- coding:utf-8 -*- + +import datetime import itertools import json from enum import Enum from typing import List -from flask import has_request_context, request +from flask import has_request_context +from flask import request from flask_login import current_user from sqlalchemy import func +from api.extensions import db from api.lib.perm.acl import AppCache +from api.models.acl import AuditLoginLog from api.models.acl import AuditPermissionLog from api.models.acl import AuditResourceLog from api.models.acl import AuditRoleLog @@ -283,6 +288,27 @@ class AuditCRUD(object): return data + @staticmethod + def search_login(_, q=None, page=1, page_size=10, start=None, end=None): + query = db.session.query(AuditLoginLog) + + if start: + query = query.filter(AuditLoginLog.login_at >= start) + if end: + query = query.filter(AuditLoginLog.login_at <= end) + + if q: + query = query.filter(AuditLoginLog.username == q) + + records = query.order_by( + AuditLoginLog.id.desc()).offset((page - 1) * page_size).limit(page_size).all() + + data = { + 'data': [r.to_dict() for r in records], + } + + return data + @classmethod def add_role_log(cls, app_id, operate_type: AuditOperateType, scope: AuditScope, link_id: int, origin: dict, current: dict, extra: dict, @@ -348,3 +374,31 @@ class AuditCRUD(object): AuditTriggerLog.create(app_id=app_id, trigger_id=trigger_id, operate_uid=user_id, operate_type=operate_type.value, origin=origin, current=current, extra=extra, source=source.value) + + @classmethod + def add_login_log(cls, username, is_ok, description, _id=None, logout_at=None): + if _id is not None: + existed = AuditLoginLog.get_by_id(_id) + if existed is not None: + existed.update(logout_at=logout_at) + return + + payload = dict(username=username, + is_ok=is_ok, + description=description, + logout_at=logout_at, + ip=request.headers.get('X-Real-IP') or request.remote_addr, + browser=request.headers.get('User-Agent'), + channel=request.values.get('channel', 'web'), + ) + + if logout_at is None: + payload['login_at'] = datetime.datetime.now() + + try: + from api.lib.common_setting.employee import EmployeeCRUD + EmployeeCRUD.update_last_login_by_uid(current_user.uid) + except: + pass + + return AuditLoginLog.create(**payload).id diff --git a/cmdb-api/api/lib/perm/acl/cache.py b/cmdb-api/api/lib/perm/acl/cache.py index 7204dca..1f6dadd 100644 --- a/cmdb-api/api/lib/perm/acl/cache.py +++ b/cmdb-api/api/lib/perm/acl/cache.py @@ -2,10 +2,11 @@ import msgpack +import redis_lock from api.extensions import cache +from api.extensions import rd from api.lib.decorator import flush_db -from api.lib.utils import Lock from api.models.acl import App from api.models.acl import Permission from api.models.acl import Resource @@ -136,14 +137,14 @@ class HasResourceRoleCache(object): @classmethod def add(cls, rid, app_id): - with Lock('HasResourceRoleCache'): + with redis_lock.Lock(rd.r, 'HasResourceRoleCache'): c = cls.get(app_id) c[rid] = 1 cache.set(cls.PREFIX_KEY.format(app_id), c, timeout=0) @classmethod def remove(cls, rid, app_id): - with Lock('HasResourceRoleCache'): + with redis_lock.Lock(rd.r, 'HasResourceRoleCache'): c = cls.get(app_id) c.pop(rid, None) cache.set(cls.PREFIX_KEY.format(app_id), c, timeout=0) diff --git a/cmdb-api/api/lib/perm/acl/resource.py b/cmdb-api/api/lib/perm/acl/resource.py index f5128d4..8b1135a 100644 --- a/cmdb-api/api/lib/perm/acl/resource.py +++ b/cmdb-api/api/lib/perm/acl/resource.py @@ -309,7 +309,7 @@ class ResourceCRUD(object): return resource @staticmethod - def delete(_id): + def delete(_id, rebuild=True): resource = Resource.get_by_id(_id) or abort(404, ErrFormat.resource_not_found.format("id={}".format(_id))) origin = resource.to_dict() @@ -322,12 +322,15 @@ class ResourceCRUD(object): i.soft_delete() rebuilds.append((i.rid, i.app_id)) - for rid, app_id in set(rebuilds): - role_rebuild.apply_async(args=(rid, app_id), queue=ACL_QUEUE) + if rebuild: + for rid, app_id in set(rebuilds): + role_rebuild.apply_async(args=(rid, app_id), queue=ACL_QUEUE) AuditCRUD.add_resource_log(resource.app_id, AuditOperateType.delete, AuditScope.resource, resource.id, origin, {}, {}) + return rebuilds + @classmethod def delete_by_name(cls, name, type_id, app_id): resource = Resource.get_by(name=name, resource_type_id=type_id, app_id=app_id) or abort( diff --git a/cmdb-api/api/lib/perm/acl/resp_format.py b/cmdb-api/api/lib/perm/acl/resp_format.py index 25f6bdc..8304f87 100644 --- a/cmdb-api/api/lib/perm/acl/resp_format.py +++ b/cmdb-api/api/lib/perm/acl/resp_format.py @@ -1,43 +1,50 @@ # -*- coding:utf-8 -*- +from flask_babel import lazy_gettext as _l + from api.lib.resp_format import CommonErrFormat class ErrFormat(CommonErrFormat): - auth_only_with_app_token_failed = "应用 Token验证失败" - session_invalid = "您不是应用管理员 或者 session失效(尝试一下退出重新登录)" + login_succeed = _l("login successful") # 登录成功 + ldap_connection_failed = _l("Failed to connect to LDAP service") # 连接LDAP服务失败 + invalid_password = _l("Password verification failed") # 密码验证失败 + auth_only_with_app_token_failed = _l("Application Token verification failed") # 应用 Token验证失败 + # 您不是应用管理员 或者 session失效(尝试一下退出重新登录) + session_invalid = _l( + "You are not the application administrator or the session has expired (try logging out and logging in again)") - resource_type_not_found = "资源类型 {} 不存在!" - resource_type_exists = "资源类型 {} 已经存在!" - resource_type_cannot_delete = "因为该类型下有资源的存在, 不能删除!" + resource_type_not_found = _l("Resource type {} does not exist!") # 资源类型 {} 不存在! + resource_type_exists = _l("Resource type {} already exists!") # 资源类型 {} 已经存在! + # 因为该类型下有资源的存在, 不能删除! + resource_type_cannot_delete = _l("Because there are resources under this type, they cannot be deleted!") - user_not_found = "用户 {} 不存在!" - user_exists = "用户 {} 已经存在!" - role_not_found = "角色 {} 不存在!" - role_exists = "角色 {} 已经存在!" - global_role_not_found = "全局角色 {} 不存在!" - global_role_exists = "全局角色 {} 已经存在!" - user_role_delete_invalid = "删除用户角色, 请在 用户管理 页面操作!" + user_not_found = _l("User {} does not exist!") # 用户 {} 不存在! + user_exists = _l("User {} already exists!") # 用户 {} 已经存在! + role_not_found = _l("Role {} does not exist!") # 角色 {} 不存在! + role_exists = _l("Role {} already exists!") # 角色 {} 已经存在! + global_role_not_found = _l("Global role {} does not exist!") # 全局角色 {} 不存在! + global_role_exists = _l("Global role {} already exists!") # 全局角色 {} 已经存在! - resource_no_permission = "您没有资源: {} 的 {} 权限" - admin_required = "需要管理员权限" - role_required = "需要角色: {}" + resource_no_permission = _l("You do not have {} permission on resource: {}") # 您没有资源: {} 的 {} 权限 + admin_required = _l("Requires administrator permissions") # 需要管理员权限 + role_required = _l("Requires role: {}") # 需要角色: {} + # 删除用户角色, 请在 用户管理 页面操作! + user_role_delete_invalid = _l("To delete a user role, please operate on the User Management page!") - app_is_ready_existed = "应用 {} 已经存在" - app_not_found = "应用 {} 不存在!" - app_secret_invalid = "应用的Secret无效" + app_is_ready_existed = _l("Application {} already exists") # 应用 {} 已经存在 + app_not_found = _l("Application {} does not exist!") # 应用 {} 不存在! + app_secret_invalid = _l("The Secret is invalid") # 应用的Secret无效 - resource_not_found = "资源 {} 不存在!" - resource_exists = "资源 {} 已经存在!" + resource_not_found = _l("Resource {} does not exist!") # 资源 {} 不存在! + resource_exists = _l("Resource {} already exists!") # 资源 {} 已经存在! - resource_group_not_found = "资源组 {} 不存在!" - resource_group_exists = "资源组 {} 已经存在!" + resource_group_not_found = _l("Resource group {} does not exist!") # 资源组 {} 不存在! + resource_group_exists = _l("Resource group {} already exists!") # 资源组 {} 已经存在! - inheritance_dead_loop = "继承检测到了死循环" - role_relation_not_found = "角色关系 {} 不存在!" + inheritance_dead_loop = _l("Inheritance detected infinite loop") # 继承检测到了死循环 + role_relation_not_found = _l("Role relationship {} does not exist!") # 角色关系 {} 不存在! - trigger_not_found = "触发器 {} 不存在!" - trigger_exists = "触发器 {} 已经存在!" - trigger_disabled = "触发器 {} 已经被禁用!" - - invalid_password = "密码不正确!" + trigger_not_found = _l("Trigger {} does not exist!") # 触发器 {} 不存在! + trigger_exists = _l("Trigger {} already exists!") # 触发器 {} 已经存在! + trigger_disabled = _l("Trigger {} has been disabled!") # Trigger {} has been disabled! diff --git a/cmdb-api/api/lib/perm/acl/user.py b/cmdb-api/api/lib/perm/acl/user.py index d6f2fdd..d17af58 100644 --- a/cmdb-api/api/lib/perm/acl/user.py +++ b/cmdb-api/api/lib/perm/acl/user.py @@ -41,6 +41,7 @@ class UserCRUD(object): @classmethod def add(cls, **kwargs): + add_from = kwargs.pop('add_from', None) existed = User.get_by(username=kwargs['username']) existed and abort(400, ErrFormat.user_exists.format(kwargs['username'])) @@ -62,10 +63,12 @@ class UserCRUD(object): AuditCRUD.add_role_log(None, AuditOperateType.create, AuditScope.user, user.uid, {}, user.to_dict(), {}, {} ) - from api.lib.common_setting.employee import EmployeeCRUD - payload = {column: getattr(user, column) for column in ['uid', 'username', 'nickname', 'email', 'block']} - payload['rid'] = role.id - EmployeeCRUD.add_employee_from_acl_created(**payload) + + if add_from != 'common': + from api.lib.common_setting.employee import EmployeeCRUD + payload = {column: getattr(user, column) for column in ['uid', 'username', 'nickname', 'email', 'block']} + payload['rid'] = role.id + EmployeeCRUD.add_employee_from_acl_created(**payload) return user diff --git a/cmdb-api/api/lib/perm/auth.py b/cmdb-api/api/lib/perm/auth.py index 76e2481..c00f8b0 100644 --- a/cmdb-api/api/lib/perm/auth.py +++ b/cmdb-api/api/lib/perm/auth.py @@ -93,6 +93,9 @@ def _auth_with_token(): def _auth_with_ip_white_list(): + if request.url.endswith("acl/users/info"): + return False + ip = request.headers.get('X-Real-IP') or request.remote_addr key = request.values.get('_key') secret = request.values.get('_secret') diff --git a/cmdb-api/api/lib/perm/authentication/__init__.py b/cmdb-api/api/lib/perm/authentication/__init__.py new file mode 100644 index 0000000..380474e --- /dev/null +++ b/cmdb-api/api/lib/perm/authentication/__init__.py @@ -0,0 +1 @@ +# -*- coding:utf-8 -*- diff --git a/cmdb-api/api/flask_cas/__init__.py b/cmdb-api/api/lib/perm/authentication/cas/__init__.py similarity index 98% rename from cmdb-api/api/flask_cas/__init__.py rename to cmdb-api/api/lib/perm/authentication/cas/__init__.py index dc31cf5..5f0fd52 100644 --- a/cmdb-api/api/flask_cas/__init__.py +++ b/cmdb-api/api/lib/perm/authentication/cas/__init__.py @@ -15,7 +15,7 @@ try: except ImportError: from flask import _request_ctx_stack as stack -from api.flask_cas import routing +from . import routing class CAS(object): diff --git a/cmdb-api/api/flask_cas/cas_urls.py b/cmdb-api/api/lib/perm/authentication/cas/cas_urls.py similarity index 99% rename from cmdb-api/api/flask_cas/cas_urls.py rename to cmdb-api/api/lib/perm/authentication/cas/cas_urls.py index 34e15d3..4cbba47 100644 --- a/cmdb-api/api/flask_cas/cas_urls.py +++ b/cmdb-api/api/lib/perm/authentication/cas/cas_urls.py @@ -119,4 +119,4 @@ def create_cas_validate_url(cas_url, cas_route, service, ticket, ('service', service), ('ticket', ticket), ('renew', renew), - ) \ No newline at end of file + ) diff --git a/cmdb-api/api/flask_cas/routing.py b/cmdb-api/api/lib/perm/authentication/cas/routing.py similarity index 64% rename from cmdb-api/api/flask_cas/routing.py rename to cmdb-api/api/lib/perm/authentication/cas/routing.py index 7341027..612ad89 100644 --- a/cmdb-api/api/flask_cas/routing.py +++ b/cmdb-api/api/lib/perm/authentication/cas/routing.py @@ -1,14 +1,24 @@ # -*- coding:utf-8 -*- - -import json +import datetime +import uuid import bs4 from flask import Blueprint -from flask import current_app, session, request, url_for, redirect -from flask_login import login_user, logout_user +from flask import current_app +from flask import redirect +from flask import request +from flask import session +from flask import url_for +from flask_login import login_user +from flask_login import logout_user +from six.moves.urllib.parse import urlparse from six.moves.urllib_request import urlopen +from api.lib.common_setting.common_data import AuthenticateDataCRUD +from api.lib.common_setting.const import AuthenticateType +from api.lib.perm.acl.audit import AuditCRUD from api.lib.perm.acl.cache import UserCache +from api.lib.perm.acl.resp_format import ErrFormat from .cas_urls import create_cas_login_url from .cas_urls import create_cas_logout_url from .cas_urls import create_cas_validate_url @@ -16,6 +26,7 @@ from .cas_urls import create_cas_validate_url blueprint = Blueprint('cas', __name__) +@blueprint.route('/api/cas/login') @blueprint.route('/api/sso/login') def login(): """ @@ -29,16 +40,20 @@ def login(): If validation was successful the logged in username is saved in the user's session under the key `CAS_USERNAME_SESSION_KEY`. """ + config = AuthenticateDataCRUD(AuthenticateType.CAS).get() cas_token_session_key = current_app.config['CAS_TOKEN_SESSION_KEY'] if request.values.get("next"): session["next"] = request.values.get("next") - _service = url_for('cas.login', _external=True, next=session["next"]) \ - if session.get("next") else url_for('cas.login', _external=True) + # _service = url_for('cas.login', _external=True) + _service = "{}://{}{}".format(urlparse(request.referrer).scheme, + urlparse(request.referrer).netloc, + url_for('cas.login')) + redirect_url = create_cas_login_url( - current_app.config['CAS_SERVER'], - current_app.config['CAS_LOGIN_ROUTE'], + config['cas_server'], + config['cas_login_route'], _service) if 'ticket' in request.args: @@ -47,30 +62,38 @@ def login(): if request.args.get('ticket'): if validate(request.args['ticket']): - redirect_url = session.get("next") or \ - current_app.config.get("CAS_AFTER_LOGIN") + redirect_url = session.get("next") or config.get("cas_after_login") or "/" username = session.get("CAS_USERNAME") user = UserCache.get(username) login_user(user) session.permanent = True + _id = AuditCRUD.add_login_log(username, True, ErrFormat.login_succeed) + session['LOGIN_ID'] = _id + else: del session[cas_token_session_key] redirect_url = create_cas_login_url( - current_app.config['CAS_SERVER'], - current_app.config['CAS_LOGIN_ROUTE'], + config['cas_server'], + config['cas_login_route'], url_for('cas.login', _external=True), renew=True) + + AuditCRUD.add_login_log(session.get("CAS_USERNAME"), False, ErrFormat.invalid_password) + current_app.logger.info("redirect to: {0}".format(redirect_url)) return redirect(redirect_url) +@blueprint.route('/api/cas/logout') @blueprint.route('/api/sso/logout') def logout(): """ When the user accesses this route they are logged out. """ + config = AuthenticateDataCRUD(AuthenticateType.CAS).get() + current_app.logger.info(config) cas_username_session_key = current_app.config['CAS_USERNAME_SESSION_KEY'] cas_token_session_key = current_app.config['CAS_TOKEN_SESSION_KEY'] @@ -82,12 +105,14 @@ def logout(): "next" in session and session.pop("next") redirect_url = create_cas_logout_url( - current_app.config['CAS_SERVER'], - current_app.config['CAS_LOGOUT_ROUTE'], + config['cas_server'], + config['cas_logout_route'], url_for('cas.login', _external=True, next=request.referrer)) logout_user() + AuditCRUD.add_login_log(None, None, None, _id=session.get('LOGIN_ID'), logout_at=datetime.datetime.now()) + current_app.logger.debug('Redirecting to: {0}'.format(redirect_url)) return redirect(redirect_url) @@ -100,14 +125,15 @@ def validate(ticket): and the validated username is saved in the session under the key `CAS_USERNAME_SESSION_KEY`. """ + config = AuthenticateDataCRUD(AuthenticateType.CAS).get() cas_username_session_key = current_app.config['CAS_USERNAME_SESSION_KEY'] current_app.logger.debug("validating token {0}".format(ticket)) cas_validate_url = create_cas_validate_url( - current_app.config['CAS_VALIDATE_SERVER'], - current_app.config['CAS_VALIDATE_ROUTE'], + config['cas_validate_server'], + config['cas_validate_route'], url_for('cas.login', _external=True), ticket) @@ -115,23 +141,35 @@ def validate(ticket): try: response = urlopen(cas_validate_url).read() - ticketid = _parse_tag(response, "cas:user") - strs = [s.strip() for s in ticketid.split('|') if s.strip()] + ticket_id = _parse_tag(response, "cas:user") + strs = [s.strip() for s in ticket_id.split('|') if s.strip()] username, is_valid = None, False if len(strs) == 1: username = strs[0] is_valid = True - user_info = json.loads(_parse_tag(response, "cas:other")) - current_app.logger.info(user_info) except ValueError: current_app.logger.error("CAS returned unexpected result") is_valid = False return is_valid if is_valid: - current_app.logger.debug("valid") + current_app.logger.debug("{}: {}".format(cas_username_session_key, username)) session[cas_username_session_key] = username user = UserCache.get(username) + if user is None: + current_app.logger.info("create user: {}".format(username)) + from api.lib.perm.acl.user import UserCRUD + soup = bs4.BeautifulSoup(response) + cas_user_map = config.get('cas_user_map') + user_dict = dict() + for k in cas_user_map: + v = soup.find(cas_user_map[k]['tag'], cas_user_map[k].get('attrs', {})) + user_dict[k] = v and v.text or None + user_dict['password'] = uuid.uuid4().hex + if "email" not in user_dict: + user_dict['email'] = username + + UserCRUD.add(**user_dict) from api.lib.perm.acl.acl import ACLManager user_info = ACLManager.get_user_info(username) @@ -156,7 +194,7 @@ def validate(ticket): def _parse_tag(string, tag): """ - Used for parsing xml. Search string for the first occurence of + Used for parsing xml. Search string for the first occurrence of ..... and return text (stripped of leading and tailing whitespace) between tags. Return "" if tag not found. """ @@ -164,4 +202,5 @@ def _parse_tag(string, tag): if soup.find(tag) is None: return '' + return soup.find(tag).string.strip() diff --git a/cmdb-api/api/lib/perm/authentication/ldap.py b/cmdb-api/api/lib/perm/authentication/ldap.py new file mode 100644 index 0000000..64e3239 --- /dev/null +++ b/cmdb-api/api/lib/perm/authentication/ldap.py @@ -0,0 +1,67 @@ +# -*- coding:utf-8 -*- + +import uuid + +from flask import abort +from flask import current_app +from flask import session +from ldap3 import ALL +from ldap3 import AUTO_BIND_NO_TLS +from ldap3 import Connection +from ldap3 import Server +from ldap3.core.exceptions import LDAPBindError +from ldap3.core.exceptions import LDAPCertificateError +from ldap3.core.exceptions import LDAPSocketOpenError + +from api.lib.common_setting.common_data import AuthenticateDataCRUD +from api.lib.common_setting.const import AuthenticateType +from api.lib.perm.acl.audit import AuditCRUD +from api.lib.perm.acl.resp_format import ErrFormat +from api.models.acl import User + + +def authenticate_with_ldap(username, password): + config = AuthenticateDataCRUD(AuthenticateType.LDAP).get() + + server = Server(config.get('ldap_server'), get_info=ALL, connect_timeout=3) + if '@' in username: + email = username + who = config.get('ldap_user_dn').format(username.split('@')[0]) + else: + who = config.get('ldap_user_dn').format(username) + email = "{}@{}".format(who, config.get('ldap_domain')) + + username = username.split('@')[0] + user = User.query.get_by_username(username) + try: + if not password: + raise LDAPCertificateError + + try: + conn = Connection(server, user=who, password=password, auto_bind=AUTO_BIND_NO_TLS) + except LDAPBindError: + conn = Connection(server, + user=f"{username}@{config.get('ldap_domain')}", + password=password, + auto_bind=AUTO_BIND_NO_TLS) + + if conn.result['result'] != 0: + AuditCRUD.add_login_log(username, False, ErrFormat.invalid_password) + raise LDAPBindError + else: + _id = AuditCRUD.add_login_log(username, True, ErrFormat.login_succeed) + session['LOGIN_ID'] = _id + + if not user: + from api.lib.perm.acl.user import UserCRUD + user = UserCRUD.add(username=username, email=email, password=uuid.uuid4().hex) + + return user, True + + except LDAPBindError as e: + current_app.logger.info(e) + return user, False + + except LDAPSocketOpenError as e: + current_app.logger.info(e) + return abort(403, ErrFormat.ldap_connection_failed) diff --git a/cmdb-api/api/lib/perm/authentication/oauth2/__init__.py b/cmdb-api/api/lib/perm/authentication/oauth2/__init__.py new file mode 100644 index 0000000..1b7d02e --- /dev/null +++ b/cmdb-api/api/lib/perm/authentication/oauth2/__init__.py @@ -0,0 +1,30 @@ +# -*- coding:utf-8 -*- + +from flask import current_app + +from . import routing + + +class OAuth2(object): + def __init__(self, app=None, url_prefix=None): + self._app = app + if app is not None: + self.init_app(app, url_prefix) + + @staticmethod + def init_app(app, url_prefix=None): + # Configuration defaults + app.config.setdefault('OAUTH2_GRANT_TYPE', 'authorization_code') + app.config.setdefault('OAUTH2_RESPONSE_TYPE', 'code') + app.config.setdefault('OAUTH2_AFTER_LOGIN', '/') + + app.config.setdefault('OIDC_GRANT_TYPE', 'authorization_code') + app.config.setdefault('OIDC_RESPONSE_TYPE', 'code') + app.config.setdefault('OIDC_AFTER_LOGIN', '/') + + # Register Blueprint + app.register_blueprint(routing.blueprint, url_prefix=url_prefix) + + @property + def app(self): + return self._app or current_app diff --git a/cmdb-api/api/lib/perm/authentication/oauth2/routing.py b/cmdb-api/api/lib/perm/authentication/oauth2/routing.py new file mode 100644 index 0000000..dfc42d8 --- /dev/null +++ b/cmdb-api/api/lib/perm/authentication/oauth2/routing.py @@ -0,0 +1,139 @@ +# -*- coding:utf-8 -*- + +import datetime +import secrets +import uuid + +import requests +from flask import Blueprint +from flask import abort +from flask import current_app +from flask import redirect +from flask import request +from flask import session +from flask import url_for +from flask_login import login_user +from flask_login import logout_user +from six.moves.urllib.parse import urlencode +from six.moves.urllib.parse import urlparse + +from api.lib.common_setting.common_data import AuthenticateDataCRUD +from api.lib.perm.acl.audit import AuditCRUD +from api.lib.perm.acl.cache import UserCache +from api.lib.perm.acl.resp_format import ErrFormat + +blueprint = Blueprint('oauth2', __name__) + + +@blueprint.route('/api//login') +def login(auth_type): + config = AuthenticateDataCRUD(auth_type.upper()).get() + + if request.values.get("next"): + session["next"] = request.values.get("next") + + session[f'{auth_type}_state'] = secrets.token_urlsafe(16) + + auth_type = auth_type.upper() + + redirect_uri = "{}://{}{}".format(urlparse(request.referrer).scheme, + urlparse(request.referrer).netloc, + url_for('oauth2.callback', auth_type=auth_type.lower())) + qs = urlencode({ + 'client_id': config['client_id'], + 'redirect_uri': redirect_uri, + 'response_type': current_app.config[f'{auth_type}_RESPONSE_TYPE'], + 'scope': ' '.join(config['scopes'] or []), + 'state': session[f'{auth_type.lower()}_state'], + }) + + return redirect("{}?{}".format(config['authorize_url'].split('?')[0], qs)) + + +@blueprint.route('/api//callback') +def callback(auth_type): + auth_type = auth_type.upper() + config = AuthenticateDataCRUD(auth_type).get() + + redirect_url = session.get("next") or config.get('after_login') or '/' + + if request.values['state'] != session.get(f'{auth_type.lower()}_state'): + return abort(401, "state is invalid") + + if 'code' not in request.values: + return abort(401, 'code is invalid') + + response = requests.post(config['token_url'], data={ + 'client_id': config['client_id'], + 'client_secret': config['client_secret'], + 'code': request.values['code'], + 'grant_type': current_app.config[f'{auth_type}_GRANT_TYPE'], + 'redirect_uri': url_for('oauth2.callback', auth_type=auth_type.lower(), _external=True), + }, headers={'Accept': 'application/json'}) + if response.status_code != 200: + current_app.logger.error(response.text) + return abort(401) + access_token = response.json().get('access_token') + if not access_token: + return abort(401) + + response = requests.get(config['user_info']['url'], headers={ + 'Authorization': 'Bearer {}'.format(access_token), + 'Accept': 'application/json', + }) + if response.status_code != 200: + return abort(401) + + res = response.json() + email = res.get(config['user_info']['email']) + username = res.get(config['user_info']['username']) + avatar = res.get(config['user_info'].get('avatar')) + user = UserCache.get(username) + if user is None: + current_app.logger.info("create user: {}".format(username)) + from api.lib.perm.acl.user import UserCRUD + + user_dict = dict(username=username, email=email, avatar=avatar) + user_dict['password'] = uuid.uuid4().hex + + user = UserCRUD.add(**user_dict) + + # log the user in + login_user(user) + + from api.lib.perm.acl.acl import ACLManager + user_info = ACLManager.get_user_info(username) + + session["acl"] = dict(uid=user_info.get("uid"), + avatar=user.avatar if user else user_info.get("avatar"), + userId=user_info.get("uid"), + rid=user_info.get("rid"), + userName=user_info.get("username"), + nickName=user_info.get("nickname") or user_info.get("username"), + parentRoles=user_info.get("parents"), + childRoles=user_info.get("children"), + roleName=user_info.get("role")) + session["uid"] = user_info.get("uid") + + _id = AuditCRUD.add_login_log(username, True, ErrFormat.login_succeed) + session['LOGIN_ID'] = _id + + return redirect(redirect_url) + + +@blueprint.route('/api//logout') +def logout(auth_type): + "acl" in session and session.pop("acl") + "uid" in session and session.pop("uid") + f'{auth_type}_state' in session and session.pop(f'{auth_type}_state') + "next" in session and session.pop("next") + + redirect_url = url_for('oauth2.login', auth_type=auth_type, _external=True, next=request.referrer) + + logout_user() + + current_app.logger.debug('Redirecting to: {0}'.format(redirect_url)) + + AuditCRUD.add_login_log(None, None, None, _id=session.get('LOGIN_ID'), logout_at=datetime.datetime.now()) + + return redirect(redirect_url) diff --git a/cmdb-api/api/lib/resp_format.py b/cmdb-api/api/lib/resp_format.py index 5a7c852..48eca6f 100644 --- a/cmdb-api/api/lib/resp_format.py +++ b/cmdb-api/api/lib/resp_format.py @@ -1,29 +1,34 @@ # -*- coding:utf-8 -*- +from flask_babel import lazy_gettext as _l + + class CommonErrFormat(object): - unauthorized = "未认证" - unknown_error = "未知错误" + unauthorized = _l("unauthorized") # 未认证 + unknown_error = _l("unknown error") # 未知错误 - invalid_request = "不合法的请求" - invalid_operation = "无效的操作" + invalid_request = _l("Illegal request") # 不合法的请求 + invalid_operation = _l("Invalid operation") # 无效的操作 - not_found = "不存在" + not_found = _l("does not exist") # 不存在 - circular_dependency_error = "存在循环依赖!" + circular_dependency_error = _l("There is a circular dependency!") # 存在循环依赖! - unknown_search_error = "未知搜索错误" + unknown_search_error = _l("Unknown search error") # 未知搜索错误 - invalid_json = "json格式似乎不正确了, 请仔细确认一下!" + # json格式似乎不正确了, 请仔细确认一下! + invalid_json = _l("The json format seems to be incorrect, please confirm carefully!") - datetime_argument_invalid = "参数 {} 格式不正确, 格式必须是: yyyy-mm-dd HH:MM:SS" + # 参数 {} 格式不正确, 格式必须是: yyyy-mm-dd HH:MM:SS + datetime_argument_invalid = _l("The format of parameter {} is incorrect, the format must be: yyyy-mm-dd HH:MM:SS") - argument_value_required = "参数 {} 的值不能为空!" - argument_required = "请求缺少参数 {}" - argument_invalid = "参数 {} 的值无效" - argument_str_length_limit = "参数 {} 的长度必须 <= {}" + argument_value_required = _l("The value of parameter {} cannot be empty!") # 参数 {} 的值不能为空! + argument_required = _l("The request is missing parameters {}") # 请求缺少参数 {} + argument_invalid = _l("Invalid value for parameter {}") # 参数 {} 的值无效 + argument_str_length_limit = _l("The length of parameter {} must be <= {}") # 参数 {} 的长度必须 <= {} - role_required = "角色 {} 才能操作!" - user_not_found = "用户 {} 不存在" - no_permission = "您没有资源: {} 的{}权限!" - no_permission2 = "您没有操作权限!" - no_permission_only_owner = "只有创建人或者管理员才有权限!" + role_required = _l("Role {} can only operate!") # 角色 {} 才能操作! + user_not_found = _l("User {} does not exist") # 用户 {} 不存在 + no_permission = _l("You do not have {} permission for resource: {}!") # 您没有资源: {} 的{}权限! + no_permission2 = _l("You do not have permission to operate!") # 您没有操作权限! + no_permission_only_owner = _l("Only the creator or administrator has permission!") # 只有创建人或者管理员才有权限! diff --git a/cmdb-api/api/lib/utils.py b/cmdb-api/api/lib/utils.py index 9505d19..8f79358 100644 --- a/cmdb-api/api/lib/utils.py +++ b/cmdb-api/api/lib/utils.py @@ -1,8 +1,6 @@ # -*- coding:utf-8 -*- import base64 -import sys -import time from typing import Set import elasticsearch @@ -230,52 +228,6 @@ class ESHandler(object): return 0, [], {} -class Lock(object): - def __init__(self, name, timeout=10, app=None, need_lock=True): - self.lock_key = name - self.need_lock = need_lock - self.timeout = timeout - if not app: - app = current_app - self.app = app - try: - self.redis = redis.Redis(host=self.app.config.get('CACHE_REDIS_HOST'), - port=self.app.config.get('CACHE_REDIS_PORT'), - password=self.app.config.get('CACHE_REDIS_PASSWORD')) - except: - self.app.logger.error("cannot connect redis") - raise Exception("cannot connect redis") - - def lock(self, timeout=None): - if not timeout: - timeout = self.timeout - retry = 0 - while retry < 100: - timestamp = time.time() + timeout + 1 - _lock = self.redis.setnx(self.lock_key, timestamp) - if _lock == 1 or ( - time.time() > float(self.redis.get(self.lock_key) or sys.maxsize) and - time.time() > float(self.redis.getset(self.lock_key, timestamp) or sys.maxsize)): - break - else: - retry += 1 - time.sleep(0.6) - if retry >= 100: - raise Exception("get lock failed...") - - def release(self): - if time.time() < float(self.redis.get(self.lock_key)): - self.redis.delete(self.lock_key) - - def __enter__(self): - if self.need_lock: - self.lock() - - def __exit__(self, exc_type, exc_val, exc_tb): - if self.need_lock: - self.release() - - class AESCrypto(object): BLOCK_SIZE = 16 # Bytes pad = lambda s: s + ((AESCrypto.BLOCK_SIZE - len(s) % AESCrypto.BLOCK_SIZE) * diff --git a/cmdb-api/api/models/acl.py b/cmdb-api/api/models/acl.py index b0683ec..730a012 100644 --- a/cmdb-api/api/models/acl.py +++ b/cmdb-api/api/models/acl.py @@ -5,17 +5,18 @@ import copy import hashlib from datetime import datetime -from ldap3 import Server, Connection, ALL -from ldap3.core.exceptions import LDAPBindError, LDAPCertificateError from flask import current_app +from flask import session from flask_sqlalchemy import BaseQuery from api.extensions import db from api.lib.database import CRUDModel from api.lib.database import Model +from api.lib.database import Model2 from api.lib.database import SoftDeleteMixin from api.lib.perm.acl.const import ACL_QUEUE from api.lib.perm.acl.const import OperateType +from api.lib.perm.acl.resp_format import ErrFormat class App(Model): @@ -28,21 +29,26 @@ class App(Model): class UserQuery(BaseQuery): - def _join(self, *args, **kwargs): - super(UserQuery, self)._join(*args, **kwargs) def authenticate(self, login, password): + from api.lib.perm.acl.audit import AuditCRUD + user = self.filter(db.or_(User.username == login, User.email == login)).filter(User.deleted.is_(False)).filter(User.block == 0).first() if user: - current_app.logger.info(user) authenticated = user.check_password(password) if authenticated: - from api.tasks.acl import op_record - op_record.apply_async(args=(None, login, OperateType.LOGIN, ["ACL"]), queue=ACL_QUEUE) + _id = AuditCRUD.add_login_log(login, True, ErrFormat.login_succeed) + session['LOGIN_ID'] = _id + else: + AuditCRUD.add_login_log(login, False, ErrFormat.invalid_password) else: authenticated = False + AuditCRUD.add_login_log(login, False, ErrFormat.user_not_found.format(login)) + + current_app.logger.info(("login", login, user, authenticated)) + return user, authenticated def authenticate_with_key(self, key, secret, args, path): @@ -57,38 +63,6 @@ class UserQuery(BaseQuery): return user, authenticated - def authenticate_with_ldap(self, username, password): - server = Server(current_app.config.get('LDAP_SERVER'), get_info=ALL) - if '@' in username: - email = username - who = current_app.config.get('LDAP_USER_DN').format(username.split('@')[0]) - else: - who = current_app.config.get('LDAP_USER_DN').format(username) - email = "{}@{}".format(who, current_app.config.get('LDAP_DOMAIN')) - - username = username.split('@')[0] - user = self.get_by_username(username) - try: - if not password: - raise LDAPCertificateError - - conn = Connection(server, user=who, password=password) - conn.bind() - if conn.result['result'] != 0: - raise LDAPBindError - conn.unbind() - - if not user: - from api.lib.perm.acl.user import UserCRUD - user = UserCRUD.add(username=username, email=email) - - from api.tasks.acl import op_record - op_record.apply_async(args=(None, username, OperateType.LOGIN, ["ACL"]), queue=ACL_QUEUE) - - return user, True - except LDAPBindError: - return user, False - def search(self, key): query = self.filter(db.or_(User.email == key, User.nickname.ilike('%' + key + '%'), @@ -138,6 +112,7 @@ class User(CRUDModel, SoftDeleteMixin): wx_id = db.Column(db.String(32)) employee_id = db.Column(db.String(16), index=True) avatar = db.Column(db.String(128)) + # apps = db.Column(db.JSON) def __str__(self): @@ -168,11 +143,9 @@ class User(CRUDModel, SoftDeleteMixin): class RoleQuery(BaseQuery): - def _join(self, *args, **kwargs): - super(RoleQuery, self)._join(*args, **kwargs) def authenticate(self, login, password): - role = self.filter(Role.name == login).first() + role = self.filter(Role.name == login).filter(Role.deleted.is_(False)).first() if role: authenticated = role.check_password(password) @@ -377,3 +350,16 @@ class AuditTriggerLog(Model): current = db.Column(db.JSON, default=dict(), comment='当前数据') extra = db.Column(db.JSON, default=dict(), comment='权限名') source = db.Column(db.String(16), default='', comment='来源') + + +class AuditLoginLog(Model2): + __tablename__ = "acl_audit_login_logs" + + username = db.Column(db.String(64), index=True) + channel = db.Column(db.Enum('web', 'api', 'ssh'), default="web") + ip = db.Column(db.String(15)) + browser = db.Column(db.String(256)) + description = db.Column(db.String(128)) + is_ok = db.Column(db.Boolean) + login_at = db.Column(db.DateTime) + logout_at = db.Column(db.DateTime) diff --git a/cmdb-api/api/models/cmdb.py b/cmdb-api/api/models/cmdb.py index f91f585..193c94b 100644 --- a/cmdb-api/api/models/cmdb.py +++ b/cmdb-api/api/models/cmdb.py @@ -57,6 +57,16 @@ class CIType(Model): uid = db.Column(db.Integer, index=True) +class CITypeInheritance(Model): + __tablename__ = "c_ci_type_inheritance" + + parent_id = db.Column(db.Integer, db.ForeignKey("c_ci_types.id"), nullable=False) + child_id = db.Column(db.Integer, db.ForeignKey("c_ci_types.id"), nullable=False) + + parent = db.relationship("CIType", primaryjoin="CIType.id==CITypeInheritance.parent_id") + child = db.relationship("CIType", primaryjoin="CIType.id==CITypeInheritance.child_id") + + class CITypeRelation(Model): __tablename__ = "c_ci_type_relations" @@ -65,6 +75,9 @@ class CITypeRelation(Model): relation_type_id = db.Column(db.Integer, db.ForeignKey("c_relation_types.id"), nullable=False) constraint = db.Column(db.Enum(*ConstraintEnum.all()), default=ConstraintEnum.One2Many) + parent_attr_id = db.Column(db.Integer, db.ForeignKey("c_attributes.id")) + child_attr_id = db.Column(db.Integer, db.ForeignKey("c_attributes.id")) + parent = db.relationship("CIType", primaryjoin="CIType.id==CITypeRelation.parent_id") child = db.relationship("CIType", primaryjoin="CIType.id==CITypeRelation.child_id") relation_type = db.relationship("RelationType", backref="c_ci_type_relations.relation_type_id") @@ -94,6 +107,8 @@ class Attribute(Model): _choice_web_hook = db.Column('choice_web_hook', db.JSON) choice_other = db.Column(db.JSON) + re_check = db.Column(db.Text) + uid = db.Column(db.Integer, index=True) option = db.Column(db.JSON) @@ -448,6 +463,7 @@ class PreferenceRelationView(Model): name = db.Column(db.String(64), index=True, nullable=False) cr_ids = db.Column(db.JSON) # [{parent_id: x, child_id: y}] is_public = db.Column(db.Boolean, default=False) + option = db.Column(db.JSON) class PreferenceSearchOption(Model): @@ -464,6 +480,15 @@ class PreferenceSearchOption(Model): option = db.Column(db.JSON) +class PreferenceCITypeOrder(Model): + __tablename__ = "c_pcto" + + uid = db.Column(db.Integer, index=True, nullable=False) + type_id = db.Column(db.Integer, db.ForeignKey('c_ci_types.id')) + order = db.Column(db.SmallInteger, default=0) + is_tree = db.Column(db.Boolean, default=False) # True is tree view, False is resource view + + # custom class CustomDashboard(Model): __tablename__ = "c_c_d" @@ -548,6 +573,7 @@ class CIFilterPerms(Model): type_id = db.Column(db.Integer, db.ForeignKey('c_ci_types.id')) ci_filter = db.Column(db.Text) attr_filter = db.Column(db.Text) + id_filter = db.Column(db.JSON) # {node_path: unique_value} rid = db.Column(db.Integer, index=True) diff --git a/cmdb-api/api/models/common_setting.py b/cmdb-api/api/models/common_setting.py index a141ee8..f1f5404 100644 --- a/cmdb-api/api/models/common_setting.py +++ b/cmdb-api/api/models/common_setting.py @@ -96,3 +96,11 @@ class NoticeConfig(Model): platform = db.Column(db.VARCHAR(255), nullable=False) info = db.Column(db.JSON) + + +class CommonFile(Model): + __tablename__ = 'common_file' + + file_name = db.Column(db.VARCHAR(512), nullable=False, index=True) + origin_name = db.Column(db.VARCHAR(512), nullable=False) + binary = db.Column(db.LargeBinary(16777216), nullable=False) diff --git a/cmdb-api/api/tasks/acl.py b/cmdb-api/api/tasks/acl.py index 750eb2e..d9eb6a9 100644 --- a/cmdb-api/api/tasks/acl.py +++ b/cmdb-api/api/tasks/acl.py @@ -25,10 +25,9 @@ from api.models.acl import Role from api.models.acl import Trigger -@celery.task(base=QueueOnce, - name="acl.role_rebuild", - queue=ACL_QUEUE, - once={"graceful": True, "unlock_before_run": True}) +@celery.task(name="acl.role_rebuild", + queue=ACL_QUEUE,) +@flush_db @reconnect_db def role_rebuild(rids, app_id): rids = rids if isinstance(rids, list) else [rids] @@ -190,18 +189,18 @@ def cancel_trigger(_id, resource_id=None, operator_uid=None): @celery.task(name="acl.op_record", queue=ACL_QUEUE) @reconnect_db -def op_record(app, rolename, operate_type, obj): +def op_record(app, role_name, operate_type, obj): if isinstance(app, int): app = AppCache.get(app) app = app and app.name - if isinstance(rolename, int): - u = UserCache.get(rolename) + if isinstance(role_name, int): + u = UserCache.get(role_name) if u: - rolename = u.username + role_name = u.username if not u: - r = RoleCache.get(rolename) + r = RoleCache.get(role_name) if r: - rolename = r.name + role_name = r.name - OperateRecordCRUD.add(app, rolename, operate_type, obj) + OperateRecordCRUD.add(app, role_name, operate_type, obj) diff --git a/cmdb-api/api/tasks/cmdb.py b/cmdb-api/api/tasks/cmdb.py index bf24585..6586909 100644 --- a/cmdb-api/api/tasks/cmdb.py +++ b/cmdb-api/api/tasks/cmdb.py @@ -2,8 +2,8 @@ import json -import time +import redis_lock from flask import current_app from flask_login import login_user @@ -17,10 +17,10 @@ from api.lib.cmdb.const import CMDB_QUEUE from api.lib.cmdb.const import REDIS_PREFIX_CI from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION2 +from api.lib.cmdb.perms import CIFilterPermsCRUD from api.lib.decorator import flush_db from api.lib.decorator import reconnect_db from api.lib.perm.acl.cache import UserCache -from api.lib.utils import Lock from api.lib.utils import handle_arg_list from api.models.cmdb import CI from api.models.cmdb import CIRelation @@ -32,8 +32,7 @@ from api.models.cmdb import CITypeAttribute @reconnect_db def ci_cache(ci_id, operate_type, record_id): from api.lib.cmdb.ci import CITriggerManager - - time.sleep(0.01) + from api.lib.cmdb.ci import CIRelationManager m = api.lib.cmdb.ci.CIManager() ci_dict = m.get_ci_by_id_from_db(ci_id, need_children=False, use_master=False) @@ -51,13 +50,21 @@ def ci_cache(ci_id, operate_type, record_id): CITriggerManager.fire(operate_type, ci_dict, record_id) + ci_dict and CIRelationManager.build_by_attribute(ci_dict) + + +@celery.task(name="cmdb.rebuild_relation_for_attribute_changed", queue=CMDB_QUEUE) +@reconnect_db +def rebuild_relation_for_attribute_changed(ci_type_relation): + from api.lib.cmdb.ci import CIRelationManager + + CIRelationManager.rebuild_all_by_attribute(ci_type_relation) + @celery.task(name="cmdb.batch_ci_cache", queue=CMDB_QUEUE) @flush_db @reconnect_db def batch_ci_cache(ci_ids, ): # only for attribute change index - time.sleep(1) - for ci_id in ci_ids: m = api.lib.cmdb.ci.CIManager() ci_dict = m.get_ci_by_id_from_db(ci_id, need_children=False, use_master=False) @@ -83,6 +90,12 @@ def ci_delete(ci_id): current_app.logger.info("{0} delete..........".format(ci_id)) +@celery.task(name="cmdb.delete_id_filter", queue=CMDB_QUEUE) +@reconnect_db +def delete_id_filter(ci_id): + CIFilterPermsCRUD().delete_id_filter_by_ci_id(ci_id) + + @celery.task(name="cmdb.ci_delete_trigger", queue=CMDB_QUEUE) @reconnect_db def ci_delete_trigger(trigger, operate_type, ci_dict): @@ -99,7 +112,7 @@ def ci_delete_trigger(trigger, operate_type, ci_dict): @flush_db @reconnect_db def ci_relation_cache(parent_id, child_id, ancestor_ids): - with Lock("CIRelation_{}".format(parent_id)): + with redis_lock.Lock(rd.r, "CIRelation_{}".format(parent_id)): if ancestor_ids is None: children = rd.get([parent_id], REDIS_PREFIX_CI_RELATION)[0] children = json.loads(children) if children is not None else {} @@ -177,7 +190,7 @@ def ci_relation_add(parent_dict, child_id, uid): @celery.task(name="cmdb.ci_relation_delete", queue=CMDB_QUEUE) @reconnect_db def ci_relation_delete(parent_id, child_id, ancestor_ids): - with Lock("CIRelation_{}".format(parent_id)): + with redis_lock.Lock(rd.r, "CIRelation_{}".format(parent_id)): if ancestor_ids is None: children = rd.get([parent_id], REDIS_PREFIX_CI_RELATION)[0] children = json.loads(children) if children is not None else {} diff --git a/cmdb-api/api/tasks/common_setting.py b/cmdb-api/api/tasks/common_setting.py index ca0f669..053e70f 100644 --- a/cmdb-api/api/tasks/common_setting.py +++ b/cmdb-api/api/tasks/common_setting.py @@ -1,24 +1,24 @@ # -*- coding:utf-8 -*- -import requests from flask import current_app from api.extensions import celery -from api.extensions import db from api.lib.common_setting.acl import ACLManager -from api.lib.common_setting.const import COMMON_SETTING_QUEUE +from api.lib.perm.acl.const import ACL_QUEUE from api.lib.common_setting.resp_format import ErrFormat -from api.models.common_setting import Department +from api.models.common_setting import Department, Employee +from api.lib.decorator import flush_db +from api.lib.decorator import reconnect_db -@celery.task(name="common_setting.edit_employee_department_in_acl", queue=COMMON_SETTING_QUEUE) +@celery.task(name="common_setting.edit_employee_department_in_acl", queue=ACL_QUEUE) +@flush_db +@reconnect_db def edit_employee_department_in_acl(e_list, new_d_id, op_uid): """ :param e_list:{acl_rid: 11, department_id: 22} :param new_d_id :param op_uid """ - db.session.remove() - result = [] new_department = Department.get_by( first=True, department_id=new_d_id, to_dict=False) @@ -49,21 +49,20 @@ def edit_employee_department_in_acl(e_list, new_d_id, op_uid): continue old_d_rid_in_acl = role_map.get(old_department.department_name, 0) - if old_d_rid_in_acl == 0: - return - if old_d_rid_in_acl != old_department.acl_rid: - old_department.update( - acl_rid=old_d_rid_in_acl - ) - d_acl_rid = old_department.acl_rid if old_d_rid_in_acl == old_department.acl_rid else old_d_rid_in_acl - payload = { - 'app_id': 'acl', - 'parent_id': d_acl_rid, - } - try: - acl.remove_user_from_role(employee_acl_rid, payload) - except Exception as e: - result.append(ErrFormat.acl_remove_user_from_role_failed.format(str(e))) + if old_d_rid_in_acl > 0: + if old_d_rid_in_acl != old_department.acl_rid: + old_department.update( + acl_rid=old_d_rid_in_acl + ) + d_acl_rid = old_department.acl_rid if old_d_rid_in_acl == old_department.acl_rid else old_d_rid_in_acl + payload = { + 'app_id': 'acl', + 'parent_id': d_acl_rid, + } + try: + acl.remove_user_from_role(employee_acl_rid, payload) + except Exception as e: + result.append(ErrFormat.acl_remove_user_from_role_failed.format(str(e))) payload = { 'app_id': 'acl', @@ -75,3 +74,57 @@ def edit_employee_department_in_acl(e_list, new_d_id, op_uid): result.append(ErrFormat.acl_add_user_to_role_failed.format(str(e))) return result + + +@celery.task(name="common_setting.refresh_employee_acl_info", queue=ACL_QUEUE) +@flush_db +@reconnect_db +def refresh_employee_acl_info(current_employee_id=None): + acl = ACLManager('acl') + role_map = {role['name']: role for role in acl.get_all_roles()} + + criterion = [ + Employee.deleted == 0 + ] + query = Employee.query.filter(*criterion).order_by( + Employee.created_at.desc() + ) + current_employee_rid = 0 + + for em in query.all(): + if current_employee_id and em.employee_id == current_employee_id: + current_employee_rid = em.acl_rid if em.acl_rid else 0 + + if em.acl_uid and em.acl_rid: + continue + role = role_map.get(em.username, None) + if not role: + continue + + params = dict() + if not em.acl_uid: + params['acl_uid'] = role.get('uid', 0) + + if not em.acl_rid: + params['acl_rid'] = role.get('id', 0) + + if current_employee_id and em.employee_id == current_employee_id: + current_employee_rid = params['acl_rid'] if params.get('acl_rid', 0) else 0 + + try: + em.update(**params) + current_app.logger.info( + f"refresh_employee_acl_info success, employee_id: {em.employee_id}, uid: {em.acl_uid}, " + f"rid: {em.acl_rid}") + except Exception as e: + current_app.logger.error(str(e)) + continue + + if current_employee_rid and current_employee_rid > 0: + try: + from api.lib.common_setting.employee import GrantEmployeeACLPerm + + GrantEmployeeACLPerm().grant_by_rid(current_employee_rid, False) + current_app.logger.info(f"GrantEmployeeACLPerm success, current_employee_rid: {current_employee_rid}") + except Exception as e: + current_app.logger.error(str(e)) diff --git a/cmdb-api/api/translations/zh/LC_MESSAGES/messages.mo b/cmdb-api/api/translations/zh/LC_MESSAGES/messages.mo new file mode 100644 index 0000000..c742637 Binary files /dev/null and b/cmdb-api/api/translations/zh/LC_MESSAGES/messages.mo differ diff --git a/cmdb-api/api/translations/zh/LC_MESSAGES/messages.po b/cmdb-api/api/translations/zh/LC_MESSAGES/messages.po new file mode 100644 index 0000000..a668075 --- /dev/null +++ b/cmdb-api/api/translations/zh/LC_MESSAGES/messages.po @@ -0,0 +1,827 @@ +# Chinese translations for PROJECT. +# Copyright (C) 2023 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# FIRST AUTHOR , 2023. +# +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2024-03-29 10:42+0800\n" +"PO-Revision-Date: 2023-12-25 20:21+0800\n" +"Last-Translator: FULL NAME \n" +"Language: zh\n" +"Language-Team: zh \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.14.0\n" + +#: api/lib/resp_format.py:7 +msgid "unauthorized" +msgstr "未认证" + +#: api/lib/resp_format.py:8 +msgid "unknown error" +msgstr "未知错误" + +#: api/lib/resp_format.py:10 +msgid "Illegal request" +msgstr "不合法的请求" + +#: api/lib/resp_format.py:11 +msgid "Invalid operation" +msgstr "无效的操作" + +#: api/lib/resp_format.py:13 +msgid "does not exist" +msgstr "不存在" + +#: api/lib/resp_format.py:15 +msgid "There is a circular dependency!" +msgstr "存在循环依赖!" + +#: api/lib/resp_format.py:17 +msgid "Unknown search error" +msgstr "未知搜索错误" + +#: api/lib/resp_format.py:20 +msgid "The json format seems to be incorrect, please confirm carefully!" +msgstr "# json格式似乎不正确了, 请仔细确认一下!" + +#: api/lib/resp_format.py:23 +msgid "" +"The format of parameter {} is incorrect, the format must be: yyyy-mm-dd " +"HH:MM:SS" +msgstr "参数 {} 格式不正确, 格式必须是: yyyy-mm-dd HH:MM:SS" + +#: api/lib/resp_format.py:25 +msgid "The value of parameter {} cannot be empty!" +msgstr "参数 {} 的值不能为空!" + +#: api/lib/resp_format.py:26 +msgid "The request is missing parameters {}" +msgstr "请求缺少参数 {}" + +#: api/lib/resp_format.py:27 +msgid "Invalid value for parameter {}" +msgstr "参数 {} 的值无效" + +#: api/lib/resp_format.py:28 +msgid "The length of parameter {} must be <= {}" +msgstr "参数 {} 的长度必须 <= {}" + +#: api/lib/resp_format.py:30 +msgid "Role {} can only operate!" +msgstr "角色 {} 才能操作!" + +#: api/lib/resp_format.py:31 +msgid "User {} does not exist" +msgstr "用户 {} 不存在" + +#: api/lib/resp_format.py:32 +msgid "You do not have {} permission for resource: {}!" +msgstr "您没有资源: {} 的{}权限!" + +#: api/lib/resp_format.py:33 +msgid "You do not have permission to operate!" +msgstr "您没有操作权限!" + +#: api/lib/resp_format.py:34 +msgid "Only the creator or administrator has permission!" +msgstr "只有创建人或者管理员才有权限!" + +#: api/lib/cmdb/resp_format.py:9 +msgid "CI Model" +msgstr "模型配置" + +#: api/lib/cmdb/resp_format.py:11 +msgid "Invalid relation type: {}" +msgstr "无效的关系类型: {}" + +#: api/lib/cmdb/resp_format.py:12 +msgid "CIType is not found" +msgstr "模型不存在!" + +#: api/lib/cmdb/resp_format.py:15 +msgid "The type of parameter attributes must be a list" +msgstr "参数 attributes 类型必须是列表" + +#: api/lib/cmdb/resp_format.py:16 +msgid "The file doesn't seem to be uploaded" +msgstr "文件似乎并未上传" + +#: api/lib/cmdb/resp_format.py:18 +msgid "Attribute {} does not exist!" +msgstr "属性 {} 不存在!" + +#: api/lib/cmdb/resp_format.py:19 +msgid "" +"This attribute is the unique identifier of the model and cannot be " +"deleted!" +msgstr "该属性是模型的唯一标识,不能被删除!" + +#: api/lib/cmdb/resp_format.py:21 +msgid "This attribute is referenced by model {} and cannot be deleted!" +msgstr "该属性被模型 {} 引用, 不能删除!" + +#: api/lib/cmdb/resp_format.py:23 +msgid "The value type of the attribute is not allowed to be modified!" +msgstr "属性的值类型不允许修改!" + +#: api/lib/cmdb/resp_format.py:25 +msgid "Multiple values are not allowed to be modified!" +msgstr "多值不被允许修改!" + +#: api/lib/cmdb/resp_format.py:27 +msgid "Modifying the index is not allowed for non-administrators!" +msgstr "修改索引 非管理员不被允许!" + +#: api/lib/cmdb/resp_format.py:28 +msgid "Index switching failed!" +msgstr "索引切换失败!" + +#: api/lib/cmdb/resp_format.py:29 +msgid "The predefined value is of the wrong type!" +msgstr "预定义值的类型不对!" + +#: api/lib/cmdb/resp_format.py:30 +msgid "Duplicate attribute name {}" +msgstr "重复的属性名 {}" + +#: api/lib/cmdb/resp_format.py:31 +msgid "Failed to create attribute {}!" +msgstr "创建属性 {} 失败!" + +#: api/lib/cmdb/resp_format.py:32 +msgid "Modify attribute {} failed!" +msgstr "修改属性 {} 失败!" + +#: api/lib/cmdb/resp_format.py:33 +msgid "You do not have permission to modify this attribute!" +msgstr "您没有权限修改该属性!" + +#: api/lib/cmdb/resp_format.py:34 +msgid "Only creators and administrators are allowed to delete attributes!" +msgstr "目前只允许 属性创建人、管理员 删除属性!" + +#: api/lib/cmdb/resp_format.py:37 +msgid "" +"Attribute field names cannot be built-in fields: id, _id, ci_id, type, " +"_type, ci_type" +msgstr "属性字段名不能是内置字段: id, _id, ci_id, type, _type, ci_type" + +#: api/lib/cmdb/resp_format.py:39 +msgid "Predefined value: Other model request parameters are illegal!" +msgstr "预定义值: 其他模型请求参数不合法!" + +#: api/lib/cmdb/resp_format.py:42 +msgid "CI {} does not exist" +msgstr "CI {} 不存在" + +#: api/lib/cmdb/resp_format.py:43 +msgid "Multiple attribute joint unique verification failed: {}" +msgstr "多属性联合唯一校验不通过: {}" + +#: api/lib/cmdb/resp_format.py:44 +msgid "The model's primary key {} does not exist!" +msgstr "模型的主键 {} 不存在!" + +#: api/lib/cmdb/resp_format.py:45 +msgid "Primary key {} is missing" +msgstr "主键字段 {} 缺失" + +#: api/lib/cmdb/resp_format.py:46 +msgid "CI already exists!" +msgstr "CI 已经存在!" + +#: api/lib/cmdb/resp_format.py:47 +msgid "Relationship constraint: {}, verification failed" +msgstr "关系约束: {}, 校验失败" + +#: api/lib/cmdb/resp_format.py:49 +msgid "" +"Many-to-many relationship constraint: Model {} <-> {} already has a many-" +"to-many relationship!" +msgstr "多对多关系 限制: 模型 {} <-> {} 已经存在多对多关系!" + +#: api/lib/cmdb/resp_format.py:52 +msgid "CI relationship: {} does not exist" +msgstr "CI关系: {} 不存在" + +#: api/lib/cmdb/resp_format.py:55 +msgid "In search expressions, not supported before parentheses: or, not" +msgstr "搜索表达式里小括号前不支持: 或、非" + +#: api/lib/cmdb/resp_format.py:57 +msgid "Model {} does not exist" +msgstr "模型 {} 不存在" + +#: api/lib/cmdb/resp_format.py:58 +msgid "Model {} already exists" +msgstr "模型 {} 已经存在" + +#: api/lib/cmdb/resp_format.py:59 +msgid "The primary key is undefined or has been deleted" +msgstr "主键未定义或者已被删除" + +#: api/lib/cmdb/resp_format.py:60 +msgid "Only the creator can delete it!" +msgstr "只有创建人才能删除它!" + +#: api/lib/cmdb/resp_format.py:61 +msgid "The model cannot be deleted because the CI already exists" +msgstr "因为CI已经存在,不能删除模型" + +#: api/lib/cmdb/resp_format.py:63 +msgid "The inheritance cannot be deleted because the CI already exists" +msgstr "因为CI已经存在,不能删除继承关系" + +#: api/lib/cmdb/resp_format.py:67 +msgid "" +"The model cannot be deleted because the model is referenced by the " +"relational view {}" +msgstr "因为关系视图 {} 引用了该模型,不能删除模型" + +#: api/lib/cmdb/resp_format.py:69 +msgid "Model group {} does not exist" +msgstr "模型分组 {} 不存在" + +#: api/lib/cmdb/resp_format.py:70 +msgid "Model group {} already exists" +msgstr "模型分组 {} 已经存在" + +#: api/lib/cmdb/resp_format.py:71 +msgid "Model relationship {} does not exist" +msgstr "模型关系 {} 不存在" + +#: api/lib/cmdb/resp_format.py:72 +msgid "Attribute group {} already exists" +msgstr "属性分组 {} 已存在" + +#: api/lib/cmdb/resp_format.py:73 +msgid "Attribute group {} does not exist" +msgstr "属性分组 {} 不存在" + +#: api/lib/cmdb/resp_format.py:75 +msgid "Attribute group <{0}> - attribute <{1}> does not exist" +msgstr "属性组<{0}> - 属性<{1}> 不存在" + +#: api/lib/cmdb/resp_format.py:76 +msgid "The unique constraint already exists!" +msgstr "唯一约束已经存在!" + +#: api/lib/cmdb/resp_format.py:78 +msgid "Uniquely constrained attributes cannot be JSON and multi-valued" +msgstr "唯一约束的属性不能是 JSON 和 多值" + +#: api/lib/cmdb/resp_format.py:79 +msgid "Duplicated trigger" +msgstr "重复的触发器" + +#: api/lib/cmdb/resp_format.py:80 +msgid "Trigger {} does not exist" +msgstr "触发器 {} 不存在" + +#: api/lib/cmdb/resp_format.py:82 +msgid "Operation record {} does not exist" +msgstr "操作记录 {} 不存在" + +#: api/lib/cmdb/resp_format.py:83 +msgid "Unique identifier cannot be deleted" +msgstr "不能删除唯一标识" + +#: api/lib/cmdb/resp_format.py:84 +msgid "Cannot delete default sorted attributes" +msgstr "不能删除默认排序的属性" + +#: api/lib/cmdb/resp_format.py:86 +msgid "No node selected" +msgstr "没有选择节点" + +#: api/lib/cmdb/resp_format.py:87 +msgid "This search option does not exist!" +msgstr "该搜索选项不存在!" + +#: api/lib/cmdb/resp_format.py:88 +msgid "This search option has a duplicate name!" +msgstr "该搜索选项命名重复!" + +#: api/lib/cmdb/resp_format.py:90 +msgid "Relationship type {} already exists" +msgstr "关系类型 {} 已经存在" + +#: api/lib/cmdb/resp_format.py:91 +msgid "Relationship type {} does not exist" +msgstr "关系类型 {} 不存在" + +#: api/lib/cmdb/resp_format.py:93 +msgid "Invalid attribute value: {}" +msgstr "无效的属性值: {}" + +#: api/lib/cmdb/resp_format.py:94 +msgid "{} Invalid value: {}" +msgstr "{} 无效的值: {}" + +#: api/lib/cmdb/resp_format.py:95 +msgid "{} is not in the predefined values" +msgstr "{} 不在预定义值里" + +#: api/lib/cmdb/resp_format.py:97 +msgid "The value of attribute {} must be unique, {} already exists" +msgstr "属性 {} 的值必须是唯一的, 当前值 {} 已存在" + +#: api/lib/cmdb/resp_format.py:98 +msgid "Attribute {} value must exist" +msgstr "属性 {} 值必须存在" + +#: api/lib/cmdb/resp_format.py:99 +msgid "Out of range value, the maximum value is 2147483647" +msgstr "超过最大值限制, 最大值是2147483647" + +#: api/lib/cmdb/resp_format.py:101 +msgid "Unknown error when adding or modifying attribute value: {}" +msgstr "新增或者修改属性值未知错误: {}" + +#: api/lib/cmdb/resp_format.py:103 +msgid "Duplicate custom name" +msgstr "订制名重复" + +#: api/lib/cmdb/resp_format.py:105 +msgid "Number of models exceeds limit: {}" +msgstr "模型数超过限制: {}" + +#: api/lib/cmdb/resp_format.py:106 +msgid "The number of CIs exceeds the limit: {}" +msgstr "CI数超过限制: {}" + +#: api/lib/cmdb/resp_format.py:108 +msgid "Auto-discovery rule: {} already exists!" +msgstr "自动发现规则: {} 已经存在!" + +#: api/lib/cmdb/resp_format.py:109 +msgid "Auto-discovery rule: {} does not exist!" +msgstr "自动发现规则: {} 不存在!" + +#: api/lib/cmdb/resp_format.py:111 +msgid "This auto-discovery rule is referenced by the model and cannot be deleted!" +msgstr "该自动发现规则被模型引用, 不能删除!" + +#: api/lib/cmdb/resp_format.py:113 +msgid "The application of auto-discovery rules cannot be defined repeatedly!" +msgstr "自动发现规则的应用不能重复定义!" + +#: api/lib/cmdb/resp_format.py:114 +msgid "The auto-discovery you want to modify: {} does not exist!" +msgstr "您要修改的自动发现: {} 不存在!" + +#: api/lib/cmdb/resp_format.py:115 +msgid "Attribute does not include unique identifier: {}" +msgstr "属性字段没有包括唯一标识: {}" + +#: api/lib/cmdb/resp_format.py:116 +msgid "The auto-discovery instance does not exist!" +msgstr "自动发现的实例不存在!" + +#: api/lib/cmdb/resp_format.py:117 +msgid "The model is not associated with this auto-discovery!" +msgstr "模型并未关联该自动发现!" + +#: api/lib/cmdb/resp_format.py:118 +msgid "Only the creator can modify the Secret!" +msgstr "只有创建人才能修改Secret!" + +#: api/lib/cmdb/resp_format.py:120 +msgid "This rule already has auto-discovery instances and cannot be deleted!" +msgstr "该规则已经有自动发现的实例, 不能被删除!" + +#: api/lib/cmdb/resp_format.py:122 +msgid "The default auto-discovery rule is already referenced by model {}!" +msgstr "该默认的自动发现规则 已经被模型 {} 引用!" + +#: api/lib/cmdb/resp_format.py:124 +msgid "The unique_key method must return a non-empty string!" +msgstr "unique_key方法必须返回非空字符串!" + +#: api/lib/cmdb/resp_format.py:125 +msgid "The attributes method must return a list" +msgstr "attributes方法必须返回的是list" + +#: api/lib/cmdb/resp_format.py:127 +msgid "The list returned by the attributes method cannot be empty!" +msgstr "attributes方法返回的list不能为空!" + +#: api/lib/cmdb/resp_format.py:129 +msgid "Only administrators can define execution targets as: all nodes!" +msgstr "只有管理员才可以定义执行机器为: 所有节点!" + +#: api/lib/cmdb/resp_format.py:130 +msgid "Execute targets permission check failed: {}" +msgstr "执行机器权限检查不通过: {}" + +#: api/lib/cmdb/resp_format.py:132 +msgid "CI filter authorization must be named!" +msgstr "CI过滤授权 必须命名!" + +#: api/lib/cmdb/resp_format.py:133 +msgid "CI filter authorization is currently not supported or query" +msgstr "CI过滤授权 暂时不支持 或 查询" + +#: api/lib/cmdb/resp_format.py:136 +msgid "You do not have permission to operate attribute {}!" +msgstr "您没有属性 {} 的操作权限!" + +#: api/lib/cmdb/resp_format.py:137 +msgid "You do not have permission to operate this CI!" +msgstr "您没有该CI的操作权限!" + +#: api/lib/cmdb/resp_format.py:139 +msgid "Failed to save password: {}" +msgstr "保存密码失败: {}" + +#: api/lib/cmdb/resp_format.py:140 +msgid "Failed to get password: {}" +msgstr "获取密码失败: {}" + +#: api/lib/common_setting/resp_format.py:8 +msgid "Company info already existed" +msgstr "公司信息已存在,无法创建!" + +#: api/lib/common_setting/resp_format.py:10 +msgid "No file part" +msgstr "没有文件部分" + +#: api/lib/common_setting/resp_format.py:11 +msgid "File is required" +msgstr "文件是必须的" + +#: api/lib/common_setting/resp_format.py:12 +msgid "File not found" +msgstr "文件不存在!" + +#: api/lib/common_setting/resp_format.py:13 +msgid "File type not allowed" +msgstr "文件类型不允许!" + +#: api/lib/common_setting/resp_format.py:14 +msgid "Upload failed: {}" +msgstr "上传失败: {}" + +#: api/lib/common_setting/resp_format.py:16 +msgid "Direct supervisor is not self" +msgstr "直属上级不能是自己" + +#: api/lib/common_setting/resp_format.py:17 +msgid "Parent department is not self" +msgstr "上级部门不能是自己" + +#: api/lib/common_setting/resp_format.py:18 +msgid "Employee list is empty" +msgstr "员工列表为空" + +#: api/lib/common_setting/resp_format.py:20 +msgid "Column name not support" +msgstr "不支持的列名" + +#: api/lib/common_setting/resp_format.py:21 +msgid "Password is required" +msgstr "密码是必须的" + +#: api/lib/common_setting/resp_format.py:22 +msgid "Employee acl rid is zero" +msgstr "员工ACL角色ID不能为0" + +#: api/lib/common_setting/resp_format.py:24 +msgid "Generate excel failed: {}" +msgstr "生成excel失败: {}" + +#: api/lib/common_setting/resp_format.py:25 +msgid "Rename columns failed: {}" +msgstr "重命名字段失败: {}" + +#: api/lib/common_setting/resp_format.py:26 +msgid "Cannot block this employee is other direct supervisor" +msgstr "该员工是其他员工的直属上级, 不能禁用" + +#: api/lib/common_setting/resp_format.py:28 +msgid "Cannot block this employee is department manager" +msgstr "该员工是部门负责人, 不能禁用" + +#: api/lib/common_setting/resp_format.py:30 +msgid "Employee id [{}] not found" +msgstr "员工ID [{}] 不存在!" + +#: api/lib/common_setting/resp_format.py:31 +msgid "Value is required" +msgstr "值是必须的" + +#: api/lib/common_setting/resp_format.py:32 +msgid "Email already exists" +msgstr "邮箱已存在!" + +#: api/lib/common_setting/resp_format.py:33 +msgid "Query {} none keep value empty" +msgstr "查询 {} 空值时请保持value为空" + +#: api/lib/common_setting/resp_format.py:34 +msgid "Not support operator: {}" +msgstr "不支持的操作符: {}" + +#: api/lib/common_setting/resp_format.py:35 +msgid "Not support relation: {}" +msgstr "不支持的关系: {}" + +#: api/lib/common_setting/resp_format.py:36 +msgid "Conditions field missing" +msgstr " conditions内元素字段缺失,请检查!" + +#: api/lib/common_setting/resp_format.py:37 +msgid "Datetime format error: {}" +msgstr "{}格式错误,应该为:%Y-%m-%d %H:%M:%S" + +#: api/lib/common_setting/resp_format.py:38 +msgid "Department level relation error" +msgstr "部门层级关系不正确" + +#: api/lib/common_setting/resp_format.py:39 +msgid "Delete reserved department name" +msgstr "保留部门,无法删除!" + +#: api/lib/common_setting/resp_format.py:40 +msgid "Department id is required" +msgstr "部门ID是必须的" + +#: api/lib/common_setting/resp_format.py:41 +msgid "Department list is required" +msgstr "部门列表是必须的" + +#: api/lib/common_setting/resp_format.py:42 +msgid "{} Cannot to be parent department" +msgstr "{} 不能设置为上级部门" + +#: api/lib/common_setting/resp_format.py:43 +msgid "Department id [{}] not found" +msgstr "部门ID [{}] 不存在" + +#: api/lib/common_setting/resp_format.py:44 +msgid "Parent department id must more than zero" +msgstr "上级部门ID必须大于0" + +#: api/lib/common_setting/resp_format.py:45 +msgid "Department name [{}] already exists" +msgstr "部门名称 [{}] 已存在" + +#: api/lib/common_setting/resp_format.py:46 +msgid "New department is none" +msgstr "新部门是空的" + +#: api/lib/common_setting/resp_format.py:48 +msgid "ACL edit user failed: {}" +msgstr "ACL 修改用户失败: {}" + +#: api/lib/common_setting/resp_format.py:49 +msgid "ACL uid not found: {}" +msgstr "ACL 用户UID [{}] 不存在" + +#: api/lib/common_setting/resp_format.py:50 +msgid "ACL add user failed: {}" +msgstr "ACL 添加用户失败: {}" + +#: api/lib/common_setting/resp_format.py:51 +msgid "ACL add role failed: {}" +msgstr "ACL 添加角色失败: {}" + +#: api/lib/common_setting/resp_format.py:52 +msgid "ACL update role failed: {}" +msgstr "ACL 更新角色失败: {}" + +#: api/lib/common_setting/resp_format.py:53 +msgid "ACL get all users failed: {}" +msgstr "ACL 获取所有用户失败: {}" + +#: api/lib/common_setting/resp_format.py:54 +msgid "ACL remove user from role failed: {}" +msgstr "ACL 从角色中移除用户失败: {}" + +#: api/lib/common_setting/resp_format.py:55 +msgid "ACL add user to role failed: {}" +msgstr "ACL 添加用户到角色失败: {}" + +#: api/lib/common_setting/resp_format.py:56 +msgid "ACL import user failed: {}" +msgstr "ACL 导入用户失败: {}" + +#: api/lib/common_setting/resp_format.py:58 +msgid "Nickname is required" +msgstr "昵称不能为空" + +#: api/lib/common_setting/resp_format.py:59 +msgid "Username is required" +msgstr "用户名不能为空" + +#: api/lib/common_setting/resp_format.py:60 +msgid "Email is required" +msgstr "邮箱不能为空" + +#: api/lib/common_setting/resp_format.py:61 +msgid "Email format error" +msgstr "邮箱格式错误" + +#: api/lib/common_setting/resp_format.py:62 +msgid "Email send timeout" +msgstr "邮件发送超时" + +#: api/lib/common_setting/resp_format.py:64 +msgid "Common data not found {} " +msgstr "ID {} 找不到记录" + +#: api/lib/common_setting/resp_format.py:65 +msgid "Common data {} already existed" +msgstr "{} 已经存在" + +#: api/lib/common_setting/resp_format.py:66 +msgid "Notice platform {} existed" +msgstr "{} 已经存在" + +#: api/lib/common_setting/resp_format.py:67 +msgid "Notice {} not existed" +msgstr "{} 配置项不存在" + +#: api/lib/common_setting/resp_format.py:68 +msgid "Notice please config messenger first" +msgstr "请先配置messenger URL" + +#: api/lib/common_setting/resp_format.py:69 +msgid "Notice bind err with empty mobile" +msgstr "绑定错误,手机号为空" + +#: api/lib/common_setting/resp_format.py:70 +msgid "Notice bind failed: {}" +msgstr "绑定失败: {}" + +#: api/lib/common_setting/resp_format.py:71 +msgid "Notice bind success" +msgstr "绑定成功" + +#: api/lib/common_setting/resp_format.py:72 +msgid "Notice remove bind success" +msgstr "解绑成功" + +#: api/lib/common_setting/resp_format.py:74 +msgid "Not support test type: {}" +msgstr "不支持的测试类型: {}" + +#: api/lib/common_setting/resp_format.py:75 +msgid "Not support auth type: {}" +msgstr "不支持的认证类型: {}" + +#: api/lib/common_setting/resp_format.py:76 +msgid "LDAP server connect timeout" +msgstr "LDAP服务器连接超时" + +#: api/lib/common_setting/resp_format.py:77 +msgid "LDAP server connect not available" +msgstr "LDAP服务器连接不可用" + +#: api/lib/common_setting/resp_format.py:78 +msgid "LDAP test unknown error: {}" +msgstr "LDAP测试未知错误: {}" + +#: api/lib/common_setting/resp_format.py:79 +msgid "Common data not support auth type: {}" +msgstr "通用数据不支持auth类型: {}" + +#: api/lib/common_setting/resp_format.py:80 +msgid "LDAP test username required" +msgstr "LDAP测试用户名必填" + +#: api/lib/common_setting/resp_format.py:82 +msgid "Company wide" +msgstr "全公司" + +#: api/lib/perm/acl/resp_format.py:9 +msgid "login successful" +msgstr "登录成功" + +#: api/lib/perm/acl/resp_format.py:10 +msgid "Failed to connect to LDAP service" +msgstr "连接LDAP服务失败" + +#: api/lib/perm/acl/resp_format.py:11 +msgid "Password verification failed" +msgstr "密码验证失败" + +#: api/lib/perm/acl/resp_format.py:12 +msgid "Application Token verification failed" +msgstr "应用 Token验证失败" + +#: api/lib/perm/acl/resp_format.py:14 +msgid "" +"You are not the application administrator or the session has expired (try" +" logging out and logging in again)" +msgstr "您不是应用管理员 或者 session失效(尝试一下退出重新登录)" + +#: api/lib/perm/acl/resp_format.py:17 +msgid "Resource type {} does not exist!" +msgstr "资源类型 {} 不存在!" + +#: api/lib/perm/acl/resp_format.py:18 +msgid "Resource type {} already exists!" +msgstr "资源类型 {} 已经存在!" + +#: api/lib/perm/acl/resp_format.py:20 +msgid "Because there are resources under this type, they cannot be deleted!" +msgstr "因为该类型下有资源的存在, 不能删除!" + +#: api/lib/perm/acl/resp_format.py:22 +msgid "User {} does not exist!" +msgstr "用户 {} 不存在!" + +#: api/lib/perm/acl/resp_format.py:23 +msgid "User {} already exists!" +msgstr "用户 {} 已经存在!" + +#: api/lib/perm/acl/resp_format.py:24 +msgid "Role {} does not exist!" +msgstr "角色 {} 不存在!" + +#: api/lib/perm/acl/resp_format.py:25 +msgid "Role {} already exists!" +msgstr "角色 {} 已经存在!" + +#: api/lib/perm/acl/resp_format.py:26 +msgid "Global role {} does not exist!" +msgstr "全局角色 {} 不存在!" + +#: api/lib/perm/acl/resp_format.py:27 +msgid "Global role {} already exists!" +msgstr "全局角色 {} 已经存在!" + +#: api/lib/perm/acl/resp_format.py:29 +msgid "You do not have {} permission on resource: {}" +msgstr "您没有资源: {} 的 {} 权限" + +#: api/lib/perm/acl/resp_format.py:30 +msgid "Requires administrator permissions" +msgstr "需要管理员权限" + +#: api/lib/perm/acl/resp_format.py:31 +msgid "Requires role: {}" +msgstr "需要角色: {}" + +#: api/lib/perm/acl/resp_format.py:33 +msgid "To delete a user role, please operate on the User Management page!" +msgstr "删除用户角色, 请在 用户管理 页面操作!" + +#: api/lib/perm/acl/resp_format.py:35 +msgid "Application {} already exists" +msgstr "应用 {} 已经存在" + +#: api/lib/perm/acl/resp_format.py:36 +msgid "Application {} does not exist!" +msgstr "应用 {} 不存在!" + +#: api/lib/perm/acl/resp_format.py:37 +msgid "The Secret is invalid" +msgstr "应用的Secret无效" + +#: api/lib/perm/acl/resp_format.py:39 +msgid "Resource {} does not exist!" +msgstr "资源 {} 不存在!" + +#: api/lib/perm/acl/resp_format.py:40 +msgid "Resource {} already exists!" +msgstr "资源 {} 已经存在!" + +#: api/lib/perm/acl/resp_format.py:42 +msgid "Resource group {} does not exist!" +msgstr "资源组 {} 不存在!" + +#: api/lib/perm/acl/resp_format.py:43 +msgid "Resource group {} already exists!" +msgstr "资源组 {} 已经存在!" + +#: api/lib/perm/acl/resp_format.py:45 +msgid "Inheritance detected infinite loop" +msgstr "继承检测到了死循环" + +#: api/lib/perm/acl/resp_format.py:46 +msgid "Role relationship {} does not exist!" +msgstr "角色关系 {} 不存在!" + +#: api/lib/perm/acl/resp_format.py:48 +msgid "Trigger {} does not exist!" +msgstr "触发器 {} 不存在!" + +#: api/lib/perm/acl/resp_format.py:49 +msgid "Trigger {} already exists!" +msgstr "触发器 {} 已经存在!" + +#: api/lib/perm/acl/resp_format.py:50 +msgid "Trigger {} has been disabled!" +msgstr "Trigger {} has been disabled!" + +#~ msgid "Not a valid date value." +#~ msgstr "" + diff --git a/cmdb-api/api/views/acl/audit.py b/cmdb-api/api/views/acl/audit.py index ae4c20e..9826bb9 100644 --- a/cmdb-api/api/views/acl/audit.py +++ b/cmdb-api/api/views/acl/audit.py @@ -24,6 +24,7 @@ class AuditLogView(APIView): 'role': AuditCRUD.search_role, 'trigger': AuditCRUD.search_trigger, 'resource': AuditCRUD.search_resource, + 'login': AuditCRUD.search_login, } if name not in func_map: abort(400, f'wrong {name}, please use {func_map.keys()}') diff --git a/cmdb-api/api/views/acl/login.py b/cmdb-api/api/views/acl/login.py index 09ee89a..f2fd246 100644 --- a/cmdb-api/api/views/acl/login.py +++ b/cmdb-api/api/views/acl/login.py @@ -8,11 +8,15 @@ from flask import abort from flask import current_app from flask import request from flask import session -from flask_login import login_user, logout_user +from flask_login import login_user +from flask_login import logout_user +from api.lib.common_setting.common_data import AuthenticateDataCRUD +from api.lib.common_setting.const import AuthenticateType from api.lib.decorator import args_required from api.lib.decorator import args_validate from api.lib.perm.acl.acl import ACLManager +from api.lib.perm.acl.audit import AuditCRUD from api.lib.perm.acl.cache import RoleCache from api.lib.perm.acl.cache import User from api.lib.perm.acl.cache import UserCache @@ -34,8 +38,11 @@ class LoginView(APIView): username = request.values.get("username") or request.values.get("email") password = request.values.get("password") _role = None - if current_app.config.get('AUTH_WITH_LDAP'): - user, authenticated = User.query.authenticate_with_ldap(username, password) + auth_with_ldap = request.values.get('auth_with_ldap', True) + config = AuthenticateDataCRUD(AuthenticateType.LDAP).get() + if (config.get('enabled') or config.get('enable')) and auth_with_ldap: + from api.lib.perm.authentication.ldap import authenticate_with_ldap + user, authenticated = authenticate_with_ldap(username, password) else: user, authenticated = User.query.authenticate(username, password) if not user: @@ -176,4 +183,7 @@ class LogoutView(APIView): @auth_abandoned def post(self): logout_user() + + AuditCRUD.add_login_log(None, None, None, _id=session.get('LOGIN_ID'), logout_at=datetime.datetime.now()) + self.jsonify(code=200) diff --git a/cmdb-api/api/views/cmdb/ci.py b/cmdb-api/api/views/cmdb/ci.py index ce39962..c568b27 100644 --- a/cmdb-api/api/views/cmdb/ci.py +++ b/cmdb-api/api/views/cmdb/ci.py @@ -11,8 +11,7 @@ from api.lib.cmdb.cache import CITypeCache from api.lib.cmdb.ci import CIManager from api.lib.cmdb.ci import CIRelationManager from api.lib.cmdb.const import ExistPolicy -from api.lib.cmdb.const import PermEnum -from api.lib.cmdb.const import ResourceTypeEnum +from api.lib.cmdb.const import ResourceTypeEnum, PermEnum from api.lib.cmdb.const import RetKey from api.lib.cmdb.perms import has_perm_for_ci from api.lib.cmdb.search import SearchError @@ -77,6 +76,7 @@ class CIView(APIView): @has_perm_for_ci("ci_type", ResourceTypeEnum.CI, PermEnum.ADD, lambda x: CITypeCache.get(x)) def post(self): ci_type = request.values.get("ci_type") + ticket_id = request.values.pop("ticket_id", None) _no_attribute_policy = request.values.get("no_attribute_policy", ExistPolicy.IGNORE) exist_policy = request.values.pop('exist_policy', None) @@ -88,6 +88,7 @@ class CIView(APIView): exist_policy=exist_policy or ExistPolicy.REJECT, _no_attribute_policy=_no_attribute_policy, _is_admin=request.values.pop('__is_admin', None) or False, + ticket_id=ticket_id, **ci_dict) return self.jsonify(ci_id=ci_id) @@ -96,6 +97,7 @@ class CIView(APIView): def put(self, ci_id=None): args = request.values ci_type = args.get("ci_type") + ticket_id = request.values.pop("ticket_id", None) _no_attribute_policy = args.get("no_attribute_policy", ExistPolicy.IGNORE) ci_dict = self._wrap_ci_dict() @@ -103,6 +105,7 @@ class CIView(APIView): if ci_id is not None: manager.update(ci_id, _is_admin=request.values.pop('__is_admin', None) or False, + ticket_id=ticket_id, **ci_dict) else: request.values.pop('exist_policy', None) @@ -110,6 +113,7 @@ class CIView(APIView): exist_policy=ExistPolicy.REPLACE, _no_attribute_policy=_no_attribute_policy, _is_admin=request.values.pop('__is_admin', None) or False, + ticket_id=ticket_id, **ci_dict) return self.jsonify(ci_id=ci_id) @@ -152,9 +156,10 @@ class CISearchView(APIView): ret_key = RetKey.NAME facet = handle_arg_list(request.values.get("facet", "")) sort = request.values.get("sort") + use_id_filter = request.values.get("use_id_filter", False) in current_app.config.get('BOOL_TRUE') start = time.time() - s = search(query, fl, facet, page, ret_key, count, sort, excludes) + s = search(query, fl, facet, page, ret_key, count, sort, excludes, use_id_filter=use_id_filter) try: response, counter, total, page, numfound, facet = s.search() except SearchError as e: @@ -221,7 +226,6 @@ class CIHeartbeatView(APIView): class CIFlushView(APIView): url_prefix = ("/ci/flush", "/ci//flush") - # @auth_abandoned def get(self, ci_id=None): from api.tasks.cmdb import ci_cache from api.lib.cmdb.const import CMDB_QUEUE diff --git a/cmdb-api/api/views/cmdb/ci_relation.py b/cmdb-api/api/views/cmdb/ci_relation.py index bfa56ec..c03379d 100644 --- a/cmdb-api/api/views/cmdb/ci_relation.py +++ b/cmdb-api/api/views/cmdb/ci_relation.py @@ -13,7 +13,6 @@ from api.lib.cmdb.resp_format import ErrFormat from api.lib.cmdb.search import SearchError from api.lib.cmdb.search.ci_relation.search import Search from api.lib.decorator import args_required -from api.lib.perm.auth import auth_abandoned from api.lib.utils import get_page from api.lib.utils import get_page_size from api.lib.utils import handle_arg_list @@ -36,6 +35,8 @@ class CIRelationSearchView(APIView): root_id = request.values.get('root_id') ancestor_ids = request.values.get('ancestor_ids') or None # only for many to many + root_parent_path = handle_arg_list(request.values.get('root_parent_path') or '') + descendant_ids = list(map(int, handle_arg_list(request.values.get('descendant_ids', [])))) level = list(map(int, handle_arg_list(request.values.get('level', '1')))) query = request.values.get('q', "") @@ -43,9 +44,12 @@ class CIRelationSearchView(APIView): facet = handle_arg_list(request.values.get("facet", "")) sort = request.values.get("sort") reverse = request.values.get("reverse") in current_app.config.get('BOOL_TRUE') + has_m2m = request.values.get("has_m2m") in current_app.config.get('BOOL_TRUE') start = time.time() - s = Search(root_id, level, query, fl, facet, page, count, sort, reverse, ancestor_ids=ancestor_ids) + s = Search(root_id, level, query, fl, facet, page, count, sort, reverse, + ancestor_ids=ancestor_ids, has_m2m=has_m2m, root_parent_path=root_parent_path, + descendant_ids=descendant_ids) try: response, counter, total, page, numfound, facet = s.search() except SearchError as e: @@ -63,15 +67,16 @@ class CIRelationSearchView(APIView): class CIRelationStatisticsView(APIView): url_prefix = "/ci_relations/statistics" - @auth_abandoned def get(self): root_ids = list(map(int, handle_arg_list(request.values.get('root_ids')))) level = request.values.get('level', 1) type_ids = set(map(int, handle_arg_list(request.values.get('type_ids', [])))) ancestor_ids = request.values.get('ancestor_ids') or None # only for many to many + descendant_ids = list(map(int, handle_arg_list(request.values.get('descendant_ids', [])))) + has_m2m = request.values.get("has_m2m") in current_app.config.get('BOOL_TRUE') start = time.time() - s = Search(root_ids, level, ancestor_ids=ancestor_ids) + s = Search(root_ids, level, ancestor_ids=ancestor_ids, descendant_ids=descendant_ids, has_m2m=has_m2m) try: result = s.statistics(type_ids) except SearchError as e: diff --git a/cmdb-api/api/views/cmdb/ci_type.py b/cmdb-api/api/views/cmdb/ci_type.py index 4e02d40..27963cf 100644 --- a/cmdb-api/api/views/cmdb/ci_type.py +++ b/cmdb-api/api/views/cmdb/ci_type.py @@ -14,6 +14,7 @@ from api.lib.cmdb.cache import CITypeCache from api.lib.cmdb.ci_type import CITypeAttributeGroupManager from api.lib.cmdb.ci_type import CITypeAttributeManager from api.lib.cmdb.ci_type import CITypeGroupManager +from api.lib.cmdb.ci_type import CITypeInheritanceManager from api.lib.cmdb.ci_type import CITypeManager from api.lib.cmdb.ci_type import CITypeTemplateManager from api.lib.cmdb.ci_type import CITypeTriggerManager @@ -37,15 +38,23 @@ from api.resource import APIView class CITypeView(APIView): - url_prefix = ("/ci_types", "/ci_types/", "/ci_types/") + url_prefix = ("/ci_types", "/ci_types/", "/ci_types/", + "/ci_types/icons") def get(self, type_id=None, type_name=None): + if request.url.endswith("icons"): + return self.jsonify(CITypeManager().get_icons()) + q = request.args.get("type_name") if type_id is not None: - ci_types = [CITypeCache.get(type_id).to_dict()] + ci_type = CITypeCache.get(type_id).to_dict() + ci_type['parent_ids'] = CITypeInheritanceManager.get_parents(type_id) + ci_types = [ci_type] elif type_name is not None: - ci_types = [CITypeCache.get(type_name).to_dict()] + ci_type = CITypeCache.get(type_name).to_dict() + ci_type['parent_ids'] = CITypeInheritanceManager.get_parents(ci_type['id']) + ci_types = [ci_type] else: ci_types = CITypeManager().get_ci_types(q) count = len(ci_types) @@ -53,7 +62,7 @@ class CITypeView(APIView): return self.jsonify(numfound=count, ci_types=ci_types) @args_required("name") - @args_validate(CITypeManager.cls) + @args_validate(CITypeManager.cls, exclude_args=['parent_ids']) def post(self): params = request.values @@ -84,6 +93,26 @@ class CITypeView(APIView): return self.jsonify(type_id=type_id) +class CITypeInheritanceView(APIView): + url_prefix = ("/ci_types/inheritance",) + + @args_required("parent_ids") + @args_required("child_id") + @has_perm_from_args("child_id", ResourceTypeEnum.CI, PermEnum.CONFIG, CITypeManager.get_name_by_id) + def post(self): + CITypeInheritanceManager.add(request.values['parent_ids'], request.values['child_id']) + + return self.jsonify(**request.values) + + @args_required("parent_id") + @args_required("child_id") + @has_perm_from_args("child_id", ResourceTypeEnum.CI, PermEnum.CONFIG, CITypeManager.get_name_by_id) + def delete(self): + CITypeInheritanceManager.delete(request.values['parent_id'], request.values['child_id']) + + return self.jsonify(**request.values) + + class CITypeGroupView(APIView): url_prefix = ("/ci_types/groups", "/ci_types/groups/config", @@ -166,7 +195,8 @@ class CITypeAttributeView(APIView): 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 - unique = AttributeCache.get(unique_id).name + unique = AttributeCache.get(unique_id) + unique = unique and unique.name attr_filter = CIFilterPermsCRUD.get_attr_filter(type_id) attributes = CITypeAttributeManager.get_attributes_by_type_id(type_id) @@ -247,8 +277,8 @@ class CITypeAttributeTransferView(APIView): @args_required('from') @args_required('to') def post(self, type_id): - _from = request.values.get('from') # {'attr_id': xx, 'group_id': xx} - _to = request.values.get('to') # {'group_id': xx, 'order': xxx} + _from = request.values.get('from') # {'attr_id': xx, 'group_id': xx, 'group_name': xx} + _to = request.values.get('to') # {'group_id': xx, 'group_name': xx, 'order': xxx} CITypeAttributeManager.transfer(type_id, _from, _to) @@ -261,8 +291,8 @@ class CITypeAttributeGroupTransferView(APIView): @args_required('from') @args_required('to') def post(self, type_id): - _from = request.values.get('from') # group_id - _to = request.values.get('to') # group_id + _from = request.values.get('from') # group_id or group_name + _to = request.values.get('to') # group_id or group_name CITypeAttributeGroupManager.transfer(type_id, _from, _to) @@ -295,7 +325,7 @@ class CITypeAttributeGroupView(APIView): attr_order = list(zip(attrs, orders)) group = CITypeAttributeGroupManager.create_or_update(type_id, name, attr_order, order) - current_app.logger.warning(group.id) + return self.jsonify(group_id=group.id) @has_perm_from_args("type_id", ResourceTypeEnum.CI, PermEnum.CONFIG, CITypeManager.get_name_by_id) @@ -309,21 +339,25 @@ class CITypeAttributeGroupView(APIView): attr_order = list(zip(attrs, orders)) CITypeAttributeGroupManager.update(group_id, name, attr_order, order) + return self.jsonify(group_id=group_id) @has_perm_from_args("type_id", ResourceTypeEnum.CI, PermEnum.CONFIG, CITypeManager.get_name_by_id) def delete(self, group_id): CITypeAttributeGroupManager.delete(group_id) + return self.jsonify(group_id=group_id) class CITypeTemplateView(APIView): - url_prefix = ("/ci_types/template/import", "/ci_types/template/export") + url_prefix = ("/ci_types/template/import", "/ci_types/template/export", "/ci_types//template/export") @role_required(RoleEnum.CONFIG) - def get(self): # export - return self.jsonify( - dict(ci_type_template=CITypeTemplateManager.export_template())) + def get(self, type_id=None): # export + if type_id is not None: + return self.jsonify(dict(ci_type_template=CITypeTemplateManager.export_template_by_type(type_id))) + + return self.jsonify(dict(ci_type_template=CITypeTemplateManager.export_template())) @role_required(RoleEnum.CONFIG) def post(self): # import @@ -457,13 +491,22 @@ class CITypeGrantView(APIView): _type = CITypeCache.get(type_id) type_name = _type and _type.name or abort(404, ErrFormat.ci_type_not_found) acl = ACLManager('cmdb') - if not acl.has_permission(type_name, ResourceTypeEnum.CI_TYPE, PermEnum.GRANT) and \ - not is_app_admin('cmdb'): + if not acl.has_permission(type_name, ResourceTypeEnum.CI_TYPE, PermEnum.GRANT) and not is_app_admin('cmdb'): return abort(403, ErrFormat.no_permission.format(type_name, PermEnum.GRANT)) - acl.grant_resource_to_role_by_rid(type_name, rid, ResourceTypeEnum.CI_TYPE, perms) + if perms and not request.values.get('id_filter'): + acl.grant_resource_to_role_by_rid(type_name, rid, ResourceTypeEnum.CI_TYPE, perms, rebuild=False) - CIFilterPermsCRUD().add(type_id=type_id, rid=rid, **request.values) + new_resource = None + if 'ci_filter' in request.values or 'attr_filter' in request.values or 'id_filter' in request.values: + new_resource = CIFilterPermsCRUD().add(type_id=type_id, rid=rid, **request.values) + + if not new_resource: + from api.tasks.acl import role_rebuild + from api.lib.perm.acl.const import ACL_QUEUE + + app_id = AppCache.get('cmdb').id + role_rebuild.apply_async(args=(rid, app_id), queue=ACL_QUEUE) return self.jsonify(code=200) @@ -481,21 +524,35 @@ class CITypeRevokeView(APIView): _type = CITypeCache.get(type_id) type_name = _type and _type.name or abort(404, ErrFormat.ci_type_not_found) acl = ACLManager('cmdb') - if not acl.has_permission(type_name, ResourceTypeEnum.CI_TYPE, PermEnum.GRANT) and \ - not is_app_admin('cmdb'): + if not acl.has_permission(type_name, ResourceTypeEnum.CI_TYPE, PermEnum.GRANT) and not is_app_admin('cmdb'): return abort(403, ErrFormat.no_permission.format(type_name, PermEnum.GRANT)) - acl.revoke_resource_from_role_by_rid(type_name, rid, ResourceTypeEnum.CI_TYPE, perms) + app_id = AppCache.get('cmdb').id + resource = None - if PermEnum.READ in perms: - CIFilterPermsCRUD().delete(type_id=type_id, rid=rid) + if request.values.get('id_filter'): + CIFilterPermsCRUD().delete2( + type_id=type_id, rid=rid, id_filter=request.values['id_filter'], + parent_path=request.values.get('parent_path')) - app_id = AppCache.get('cmdb').id - users = RoleRelationCRUD.get_users_by_rid(rid, app_id) - for i in (users or []): - if i.get('role', {}).get('id') and not RoleCRUD.has_permission( - i.get('role').get('id'), type_name, ResourceTypeEnum.CI_TYPE, app_id, PermEnum.READ): - PreferenceManager.delete_by_type_id(type_id, i.get('uid')) + return self.jsonify(type_id=type_id, rid=rid) + + acl.revoke_resource_from_role_by_rid(type_name, rid, ResourceTypeEnum.CI_TYPE, perms, rebuild=False) + + if PermEnum.READ in perms or not perms: + resource = CIFilterPermsCRUD().delete(type_id=type_id, rid=rid) + + if not resource: + from api.tasks.acl import role_rebuild + from api.lib.perm.acl.const import ACL_QUEUE + + role_rebuild.apply_async(args=(rid, app_id), queue=ACL_QUEUE) + + users = RoleRelationCRUD.get_users_by_rid(rid, app_id) + for i in (users or []): + if i.get('role', {}).get('id') and not RoleCRUD.has_permission( + i.get('role').get('id'), type_name, ResourceTypeEnum.CI_TYPE, app_id, PermEnum.READ): + PreferenceManager.delete_by_type_id(type_id, i.get('uid')) return self.jsonify(type_id=type_id, rid=rid) diff --git a/cmdb-api/api/views/cmdb/ci_type_relation.py b/cmdb-api/api/views/cmdb/ci_type_relation.py index 3e1dc87..410a977 100644 --- a/cmdb-api/api/views/cmdb/ci_type_relation.py +++ b/cmdb-api/api/views/cmdb/ci_type_relation.py @@ -43,16 +43,19 @@ class CITypeRelationView(APIView): @role_required(RoleEnum.CONFIG) def get(self): - res = CITypeRelationManager.get() + res, type2attributes = CITypeRelationManager.get() - return self.jsonify(res) + return self.jsonify(relations=res, type2attributes=type2attributes) @has_perm_from_args("parent_id", ResourceTypeEnum.CI, PermEnum.CONFIG, CITypeManager.get_name_by_id) @args_required("relation_type_id") def post(self, parent_id, child_id): relation_type_id = request.values.get("relation_type_id") constraint = request.values.get("constraint") - ctr_id = CITypeRelationManager.add(parent_id, child_id, relation_type_id, constraint) + parent_attr_id = request.values.get("parent_attr_id") + child_attr_id = request.values.get("child_attr_id") + ctr_id = CITypeRelationManager.add(parent_id, child_id, relation_type_id, constraint, + parent_attr_id, child_attr_id) return self.jsonify(ctr_id=ctr_id) diff --git a/cmdb-api/api/views/cmdb/preference.py b/cmdb-api/api/views/cmdb/preference.py index cc3dab6..3da2fa7 100644 --- a/cmdb-api/api/views/cmdb/preference.py +++ b/cmdb-api/api/views/cmdb/preference.py @@ -2,6 +2,7 @@ from flask import abort +from flask import current_app from flask import request from api.lib.cmdb.ci_type import CITypeManager @@ -96,7 +97,7 @@ class PreferenceTreeApiView(APIView): class PreferenceRelationApiView(APIView): - url_prefix = "/preference/relation/view" + url_prefix = ("/preference/relation/view", "/preference/relation/view/") def get(self): views, id2type, name2id = PreferenceManager.get_relation_view() @@ -109,14 +110,20 @@ class PreferenceRelationApiView(APIView): @args_validate(PreferenceManager.pref_rel_cls) def post(self): name = request.values.get("name") + is_public = request.values.get("is_public") in current_app.config.get('BOOL_TRUE') cr_ids = request.values.get("cr_ids") - views, id2type, name2id = PreferenceManager.create_or_update_relation_view(name, cr_ids) + option = request.values.get("option") or None + views, id2type, name2id = PreferenceManager.create_or_update_relation_view(name, cr_ids, is_public=is_public, + option=option) return self.jsonify(views=views, id2type=id2type, name2id=name2id) @role_required(RoleEnum.CONFIG) - def put(self): - return self.post() + @args_required("name") + def put(self, _id): + views, id2type, name2id = PreferenceManager.create_or_update_relation_view(_id=_id, **request.values) + + return self.jsonify(views=views, id2type=id2type, name2id=name2id) @role_required(RoleEnum.CONFIG) @args_required("name") @@ -187,3 +194,15 @@ class PreferenceRelationRevokeView(APIView): acl.revoke_resource_from_role_by_rid(name, rid, ResourceTypeEnum.RELATION_VIEW, perms) return self.jsonify(code=200) + + +class PreferenceCITypeOrderView(APIView): + url_prefix = ("/preference/ci_types/order",) + + def post(self): + type_ids = request.values.get("type_ids") + is_tree = request.values.get("is_tree") in current_app.config.get('BOOL_TRUE') + + PreferenceManager.upsert_ci_type_order(type_ids, is_tree) + + return self.jsonify(type_ids=type_ids, is_tree=is_tree) diff --git a/cmdb-api/api/views/common_setting/auth_config.py b/cmdb-api/api/views/common_setting/auth_config.py new file mode 100644 index 0000000..5ec2840 --- /dev/null +++ b/cmdb-api/api/views/common_setting/auth_config.py @@ -0,0 +1,88 @@ +from flask import abort, request + +from api.lib.common_setting.common_data import AuthenticateDataCRUD +from api.lib.common_setting.const import TestType +from api.lib.common_setting.resp_format import ErrFormat +from api.lib.perm.acl.acl import role_required +from api.resource import APIView + +prefix = '/auth_config' + + +class AuthConfigView(APIView): + url_prefix = (f'{prefix}/',) + + @role_required("acl_admin") + def get(self, auth_type): + cli = AuthenticateDataCRUD(auth_type) + + if auth_type not in cli.get_support_type_list(): + abort(400, ErrFormat.not_support_auth_type.format(auth_type)) + + if auth_type in cli.common_type_list: + data = cli.get_record(True) + else: + data = cli.get_record_with_decrypt() + return self.jsonify(data) + + @role_required("acl_admin") + def post(self, auth_type): + cli = AuthenticateDataCRUD(auth_type) + + if auth_type not in cli.get_support_type_list(): + abort(400, ErrFormat.not_support_auth_type.format(auth_type)) + + params = request.json + data = params.get('data', {}) + if auth_type in cli.common_type_list: + data['encrypt'] = False + cli.create(data) + + return self.jsonify(params) + + +class AuthConfigViewWithId(APIView): + url_prefix = (f'{prefix}//',) + + @role_required("acl_admin") + def put(self, auth_type, _id): + cli = AuthenticateDataCRUD(auth_type) + + if auth_type not in cli.get_support_type_list(): + abort(400, ErrFormat.not_support_auth_type.format(auth_type)) + + params = request.json + data = params.get('data', {}) + if auth_type in cli.common_type_list: + data['encrypt'] = False + + res = cli.update(_id, data) + + return self.jsonify(res.to_dict()) + + @role_required("acl_admin") + def delete(self, auth_type, _id): + cli = AuthenticateDataCRUD(auth_type) + + if auth_type not in cli.get_support_type_list(): + abort(400, ErrFormat.not_support_auth_type.format(auth_type)) + cli.delete(_id) + return self.jsonify({}) + + +class AuthEnableListView(APIView): + url_prefix = (f'{prefix}/enable_list',) + + method_decorators = [] + + def get(self): + return self.jsonify(AuthenticateDataCRUD.get_enable_list()) + + +class AuthConfigTestView(APIView): + url_prefix = (f'{prefix}//test',) + + def post(self, auth_type): + test_type = request.values.get('test_type', TestType.Connect) + params = request.json + return self.jsonify(AuthenticateDataCRUD(auth_type).test(test_type, params.get('data'))) diff --git a/cmdb-api/api/views/common_setting/common_data.py b/cmdb-api/api/views/common_setting/common_data.py index 3793a5e..6d44ba1 100644 --- a/cmdb-api/api/views/common_setting/common_data.py +++ b/cmdb-api/api/views/common_setting/common_data.py @@ -24,12 +24,12 @@ class DataView(APIView): class DataViewWithId(APIView): url_prefix = (f'{prefix}//',) - def put(self, _id): + def put(self, data_type, _id): params = request.json res = CommonDataCRUD.update_data(_id, **params) return self.jsonify(res.to_dict()) - def delete(self, _id): + def delete(self, data_type, _id): CommonDataCRUD.delete(_id) return self.jsonify({}) diff --git a/cmdb-api/api/views/common_setting/department.py b/cmdb-api/api/views/common_setting/department.py index 9a8dd4a..ba4091a 100644 --- a/cmdb-api/api/views/common_setting/department.py +++ b/cmdb-api/api/views/common_setting/department.py @@ -62,7 +62,7 @@ class DepartmentView(APIView): class DepartmentIDView(APIView): url_prefix = (f'{prefix}/',) - def get(self, _id): + def put(self, _id): form = DepartmentForm(MultiDict(request.json)) if not form.validate(): abort(400, ','.join(['{}: {}'.format(filed, ','.join(msg)) diff --git a/cmdb-api/api/views/common_setting/file_manage.py b/cmdb-api/api/views/common_setting/file_manage.py index 7150365..fb304df 100644 --- a/cmdb-api/api/views/common_setting/file_manage.py +++ b/cmdb-api/api/views/common_setting/file_manage.py @@ -1,11 +1,12 @@ # -*- coding:utf-8 -*- -import os - -from flask import request, abort, current_app, send_from_directory +from flask import request, abort, current_app from werkzeug.utils import secure_filename +import lz4.frame +import magic +from api.lib.common_setting.const import MIMEExtMap from api.lib.common_setting.resp_format import ErrFormat -from api.lib.common_setting.upload_file import allowed_file, generate_new_file_name +from api.lib.common_setting.upload_file import allowed_file, generate_new_file_name, CommonFileCRUD from api.resource import APIView prefix = '/file' @@ -28,7 +29,8 @@ class GetFileView(APIView): url_prefix = (f'{prefix}/',) def get(self, _filename): - return send_from_directory(current_app.config['UPLOAD_DIRECTORY_FULL'], _filename, as_attachment=True) + file_stream = CommonFileCRUD.get_file(_filename) + return self.send_file(file_stream, as_attachment=True, download_name=_filename) class PostFileView(APIView): @@ -43,21 +45,35 @@ class PostFileView(APIView): if not file: abort(400, ErrFormat.file_is_required) - extension = file.mimetype.split('/')[-1] - if file.filename == '': - filename = f'.{extension}' - else: - if extension not in file.filename: - filename = file.filename + f".{extension}" - else: - filename = file.filename - if allowed_file(filename, current_app.config.get('ALLOWED_EXTENSIONS', ALLOWED_EXTENSIONS)): - filename = generate_new_file_name(filename) - filename = secure_filename(filename) - file.save(os.path.join( - current_app.config['UPLOAD_DIRECTORY_FULL'], filename)) + m_type = magic.from_buffer(file.read(2048), mime=True) + file.seek(0) - return self.jsonify(file_name=filename) + if m_type == 'application/octet-stream': + m_type = file.mimetype + elif m_type == 'text/plain': + # https://github.com/ahupp/python-magic/issues/193 + m_type = m_type if file.mimetype == m_type else file.mimetype - abort(400, 'Extension not allow') + extension = MIMEExtMap.get(m_type, None) + + if extension is None: + abort(400, f"不支持的文件类型: {m_type}") + + filename = file.filename if file.filename and file.filename.endswith(extension) else file.filename + extension + + new_filename = generate_new_file_name(filename) + new_filename = secure_filename(new_filename) + file_content = file.read() + compressed_data = lz4.frame.compress(file_content) + try: + CommonFileCRUD.add_file( + origin_name=filename, + file_name=new_filename, + binary=compressed_data, + ) + + return self.jsonify(file_name=new_filename) + except Exception as e: + current_app.logger.error(e) + abort(400, ErrFormat.upload_failed.format(e)) diff --git a/cmdb-api/babel.cfg b/cmdb-api/babel.cfg new file mode 100644 index 0000000..991e57e --- /dev/null +++ b/cmdb-api/babel.cfg @@ -0,0 +1 @@ +[python: api/**.py] diff --git a/cmdb-api/requirements.txt b/cmdb-api/requirements.txt index 1776a05..c624b59 100644 --- a/cmdb-api/requirements.txt +++ b/cmdb-api/requirements.txt @@ -8,8 +8,9 @@ elasticsearch==7.17.9 email-validator==1.3.1 environs==4.2.0 flasgger==0.9.5 -Flask==2.3.2 +Flask==2.2.5 Flask-Bcrypt==1.0.1 +flask-babel==4.0.0 Flask-Caching==2.0.2 Flask-Cors==4.0.0 Flask-Login>=0.6.2 @@ -34,8 +35,9 @@ cryptography>=41.0.2 PyJWT==2.4.0 PyMySQL==1.1.0 ldap3==2.9.1 -PyYAML==6.0 +PyYAML==6.0.1 redis==4.6.0 +python-redis-lock==4.0.0 requests==2.31.0 requests_oauthlib==1.3.1 markdownify==0.11.6 @@ -45,9 +47,10 @@ supervisor==4.0.3 timeout-decorator==0.5.0 toposort==1.10 treelib==1.6.1 -Werkzeug>=2.3.6 +Werkzeug==2.2.3 WTForms==3.0.0 shamir~=17.12.0 -hvac~=2.0.0 pycryptodomex>=3.19.0 colorama>=0.4.6 +lz4>=4.3.2 +python-magic==0.4.27 \ No newline at end of file diff --git a/cmdb-api/settings.example.py b/cmdb-api/settings.example.py index 373e6b7..4887092 100644 --- a/cmdb-api/settings.example.py +++ b/cmdb-api/settings.example.py @@ -11,10 +11,10 @@ from environs import Env env = Env() env.read_env() -ENV = env.str("FLASK_ENV", default="production") -DEBUG = ENV == "development" -SECRET_KEY = env.str("SECRET_KEY") -BCRYPT_LOG_ROUNDS = env.int("BCRYPT_LOG_ROUNDS", default=13) +ENV = env.str('FLASK_ENV', default='production') +DEBUG = ENV == 'development' +SECRET_KEY = env.str('SECRET_KEY') +BCRYPT_LOG_ROUNDS = env.int('BCRYPT_LOG_ROUNDS', default=13) DEBUG_TB_ENABLED = DEBUG DEBUG_TB_INTERCEPT_REDIRECTS = False @@ -23,7 +23,7 @@ ERROR_CODES = [400, 401, 403, 404, 405, 500, 502] # # database SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://{user}:{password}@127.0.0.1:3306/{db}?charset=utf8' SQLALCHEMY_BINDS = { - "user": 'mysql+pymysql://{user}:{password}@127.0.0.1:3306/{db}?charset=utf8' + 'user': 'mysql+pymysql://{user}:{password}@127.0.0.1:3306/{db}?charset=utf8' } SQLALCHEMY_ECHO = False SQLALCHEMY_TRACK_MODIFICATIONS = False @@ -32,11 +32,11 @@ SQLALCHEMY_ENGINE_OPTIONS = { } # # cache -CACHE_TYPE = "redis" -CACHE_REDIS_HOST = "127.0.0.1" +CACHE_TYPE = 'redis' +CACHE_REDIS_HOST = '127.0.0.1' CACHE_REDIS_PORT = 6379 -CACHE_REDIS_PASSWORD = "" -CACHE_KEY_PREFIX = "CMDB::" +CACHE_REDIS_PASSWORD = '' +CACHE_KEY_PREFIX = 'CMDB::' CACHE_DEFAULT_TIMEOUT = 3000 # # log @@ -55,10 +55,10 @@ DEFAULT_MAIL_SENDER = '' # # queue CELERY = { - "broker_url": 'redis://127.0.0.1:6379/2', - "result_backend": "redis://127.0.0.1:6379/2", - "broker_vhost": "/", - "broker_connection_retry_on_startup": True + 'broker_url': 'redis://127.0.0.1:6379/2', + 'result_backend': 'redis://127.0.0.1:6379/2', + 'broker_vhost': '/', + 'broker_connection_retry_on_startup': True } ONCE = { 'backend': 'celery_once.backends.Redis', @@ -67,33 +67,81 @@ ONCE = { } } -# # SSO -CAS_SERVER = "http://sso.xxx.com" -CAS_VALIDATE_SERVER = "http://sso.xxx.com" -CAS_LOGIN_ROUTE = "/cas/login" -CAS_LOGOUT_ROUTE = "/cas/logout" -CAS_VALIDATE_ROUTE = "/cas/serviceValidate" -CAS_AFTER_LOGIN = "/" -DEFAULT_SERVICE = "http://127.0.0.1:8000" +# =============================== Authentication =========================================================== -# # ldap -AUTH_WITH_LDAP = False -LDAP_SERVER = '' -LDAP_DOMAIN = '' -LDAP_USER_DN = 'cn={},ou=users,dc=xxx,dc=com' +# # CAS +CAS = dict( + enabled=False, + cas_server='https://{your-CASServer-hostname}', + cas_validate_server='https://{your-CASServer-hostname}', + cas_login_route='/cas/built-in/cas/login', + cas_logout_route='/cas/built-in/cas/logout', + cas_validate_route='/cas/built-in/cas/serviceValidate', + cas_after_login='/', + cas_user_map={ + 'username': {'tag': 'cas:user'}, + 'nickname': {'tag': 'cas:attribute', 'attrs': {'name': 'displayName'}}, + 'email': {'tag': 'cas:attribute', 'attrs': {'name': 'email'}}, + 'mobile': {'tag': 'cas:attribute', 'attrs': {'name': 'phone'}}, + 'avatar': {'tag': 'cas:attribute', 'attrs': {'name': 'avatar'}}, + } +) + +# # OAuth2.0 +OAUTH2 = dict( + enabled=False, + client_id='', + client_secret='', + authorize_url='https://{your-OAuth2Server-hostname}/login/oauth/authorize', + token_url='https://{your-OAuth2Server-hostname}/api/login/oauth/access_token', + scopes=['profile', 'email'], + user_info={ + 'url': 'https://{your-OAuth2Server-hostname}/api/userinfo', + 'email': 'email', + 'username': 'name', + 'avatar': 'picture' + }, + after_login='/' +) + +# # OIDC +OIDC = dict( + enabled=False, + client_id='', + client_secret='', + authorize_url='https://{your-OIDCServer-hostname}/login/oauth/authorize', + token_url='https://{your-OIDCServer-hostname}/api/login/oauth/access_token', + scopes=['openid', 'profile', 'email'], + user_info={ + 'url': 'https://{your-OIDCServer-hostname}/api/userinfo', + 'email': 'email', + 'username': 'name', + 'avatar': 'picture' + }, + after_login='/' +) + +# # LDAP +LDAP = dict( + enabled=False, + ldap_server='', + ldap_domain='', + ldap_user_dn='cn={},ou=users,dc=xxx,dc=com' +) +# ========================================================================================================== # # pagination DEFAULT_PAGE_COUNT = 50 # # permission -WHITE_LIST = ["127.0.0.1"] +WHITE_LIST = ['127.0.0.1'] USE_ACL = True # # elastic search ES_HOST = '127.0.0.1' USE_ES = False -BOOL_TRUE = ['true', 'TRUE', 'True', True, '1', 1, "Yes", "YES", "yes", 'Y', 'y'] +BOOL_TRUE = ['true', 'TRUE', 'True', True, '1', 1, 'Yes', 'YES', 'yes', 'Y', 'y'] # # messenger USE_MESSENGER = True diff --git a/cmdb-ui/config/plugin.config.js b/cmdb-ui/config/plugin.config.js index 2ad9b19..a299497 100644 --- a/cmdb-ui/config/plugin.config.js +++ b/cmdb-ui/config/plugin.config.js @@ -13,7 +13,7 @@ const getAntdSerials = (color) => { const themePluginOption = { fileName: 'css/theme-colors-[contenthash:8].css', - matchColors: getAntdSerials('#1890ff'), // 主色系列 + matchColors: getAntdSerials('#2f54eb'), // 主色系列 // 改变样式选择器,解决样式覆盖问题 changeSelector (selector) { switch (selector) { diff --git a/cmdb-ui/package.json b/cmdb-ui/package.json index e8928a1..04bab42 100644 --- a/cmdb-ui/package.json +++ b/cmdb-ui/package.json @@ -27,7 +27,8 @@ "core-js": "^3.31.0", "echarts": "^5.3.2", "element-ui": "^2.15.10", - "exceljs": "^4.3.0", + "exceljs": "^4.4.0", + "file-saver": "^2.0.5", "html2canvas": "^1.0.0-rc.5", "is-buffer": "^2.0.5", "jquery": "^3.6.0", @@ -47,6 +48,7 @@ "vue-codemirror": "^4.0.6", "vue-cropper": "^0.6.2", "vue-grid-layout": "2.3.12", + "vue-i18n": "8.28.2", "vue-infinite-scroll": "^2.0.2", "vue-json-editor": "^1.4.3", "vue-ls": "^3.2.1", diff --git a/cmdb-ui/public/iconfont/demo_index.html b/cmdb-ui/public/iconfont/demo_index.html index ccdb1b8..9aeb09b 100644 --- a/cmdb-ui/public/iconfont/demo_index.html +++ b/cmdb-ui/public/iconfont/demo_index.html @@ -54,6 +54,750 @@
    +
  • + +
    VPC
    +
    &#xe910;
    +
  • + +
  • + +
    CDN
    +
    &#xe911;
    +
  • + +
  • + +
    OOS
    +
    &#xe90f;
    +
  • + +
  • + +
    Google Cloud Platform
    +
    &#xe90b;
    +
  • + +
  • + +
    Ctyun
    +
    &#xe90c;
    +
  • + +
  • + +
    Alibaba Cloud
    +
    &#xe90d;
    +
  • + +
  • + +
    Azure
    +
    &#xe90e;
    +
  • + +
  • + +
    ZStack
    +
    &#xe904;
    +
  • + +
  • + +
    Tencent Cloud
    +
    &#xe905;
    +
  • + +
  • + +
    Nutanix
    +
    &#xe906;
    +
  • + +
  • + +
    OpenStack
    +
    &#xe907;
    +
  • + +
  • + +
    Huawei Cloud
    +
    &#xe908;
    +
  • + +
  • + +
    Bytecloud
    +
    &#xe909;
    +
  • + +
  • + +
    UCloud
    +
    &#xe90a;
    +
  • + +
  • + +
    AWS
    +
    &#xe901;
    +
  • + +
  • + +
    ECloud
    +
    &#xe902;
    +
  • + +
  • + +
    JDCloud
    +
    &#xe903;
    +
  • + +
  • + +
    veops-more
    +
    &#xe900;
    +
  • + +
  • + +
    duose-date
    +
    &#xe8ff;
    +
  • + +
  • + +
    duose-shishu
    +
    &#xe8fd;
    +
  • + +
  • + +
    duose-wenben
    +
    &#xe8fe;
    +
  • + +
  • + +
    duose-json
    +
    &#xe8f7;
    +
  • + +
  • + +
    duose-fudianshu
    +
    &#xe8f8;
    +
  • + +
  • + +
    duose-time
    +
    &#xe8f9;
    +
  • + +
  • + +
    duose-password
    +
    &#xe8fa;
    +
  • + +
  • + +
    duose-link
    +
    &#xe8fb;
    +
  • + +
  • + +
    duose-datetime
    +
    &#xe8fc;
    +
  • + +
  • + +
    veops-setting2
    +
    &#xe8f6;
    +
  • + +
  • + +
    veops-search
    +
    &#xe8f5;
    +
  • + +
  • + +
    veops-delete
    +
    &#xe8f4;
    +
  • + +
  • + +
    veops-refresh
    +
    &#xe8f3;
    +
  • + +
  • + +
    veops-filter
    +
    &#xe8f2;
    +
  • + +
  • + +
    veops-reduce
    +
    &#xe8ed;
    +
  • + +
  • + +
    veops-increase
    +
    &#xe8ee;
    +
  • + +
  • + +
    veops-configuration_table
    +
    &#xe8ef;
    +
  • + +
  • + +
    veops-copy
    +
    &#xe8f0;
    +
  • + +
  • + +
    veops-save
    +
    &#xe8f1;
    +
  • + +
  • + +
    veops-setting
    +
    &#xe8ec;
    +
  • + +
  • + +
    veops-default_avatar
    +
    &#xe8ea;
    +
  • + +
  • + +
    veops-notice
    +
    &#xe8eb;
    +
  • + +
  • + +
    quickly_initiate
    +
    &#xe8e9;
    +
  • + +
  • + +
    itsm-associated
    +
    &#xe8e8;
    +
  • + +
  • + +
    itsm-folder
    +
    &#xe8e7;
    +
  • + +
  • + +
    report
    +
    &#xe8e5;
    +
  • + +
  • + +
    folder
    +
    &#xe8e6;
    +
  • + +
  • + +
    itsm-refresh (1)
    +
    &#xe8e4;
    +
  • + +
  • + +
    itsm-add_table (1)
    +
    &#xe8e2;
    +
  • + +
  • + +
    itsm-delete_page
    +
    &#xe8e3;
    +
  • + +
  • + +
    oneterm-secret_key
    +
    &#xe8e0;
    +
  • + +
  • + +
    oneterm-password
    +
    &#xe8e1;
    +
  • + +
  • + +
    itsm-unprocessed
    +
    &#xe8dd;
    +
  • + +
  • + +
    itsm-not_timeout
    +
    &#xe8de;
    +
  • + +
  • + +
    itsm-SLA
    +
    &#xe8df;
    +
  • + +
  • + +
    itsm-processed
    +
    &#xe8dc;
    +
  • + +
  • + +
    itsm-all_SLA
    +
    &#xe8da;
    +
  • + +
  • + +
    itsm-generate_by_node_id
    +
    &#xe8db;
    +
  • + +
  • + +
    cmdb-MySQL
    +
    &#xe8d9;
    +
  • + +
  • + +
    OAuth2.0
    +
    &#xe8d8;
    +
  • + +
  • + +
    OIDC
    +
    &#xe8d6;
    +
  • + +
  • + +
    cas
    +
    &#xe8d7;
    +
  • + +
  • + +
    setting-authentication
    +
    &#xe8d5;
    +
  • + +
  • + +
    setting-authentication-selected
    +
    &#xe8d4;
    +
  • + +
  • + +
    itsm-knowledge (2)
    +
    &#xe8d2;
    +
  • + +
  • + +
    itsm-QRcode
    +
    &#xe8d3;
    +
  • + +
  • + +
    oneterm-playback
    +
    &#xe8d1;
    +
  • + +
  • + +
    oneterm-disconnect
    +
    &#xe8d0;
    +
  • + +
  • + +
    oneterm-key-selected
    +
    &#xe8cf;
    +
  • + +
  • + +
    oneterm-key
    +
    &#xe8ce;
    +
  • + +
  • + +
    oneterm-gateway
    +
    &#xe8b9;
    +
  • + +
  • + +
    oneterm-gateway-selected
    +
    &#xe8bf;
    +
  • + +
  • + +
    oneterm-account
    +
    &#xe8c0;
    +
  • + +
  • + +
    oneterm-account-selected
    +
    &#xe8c1;
    +
  • + +
  • + +
    oneterm-command
    +
    &#xe8c2;
    +
  • + +
  • + +
    oneterm-command-selected
    +
    &#xe8c3;
    +
  • + +
  • + +
    oneterm-asset_list
    +
    &#xe8c4;
    +
  • + +
  • + +
    oneterm-asset_list-selected
    +
    &#xe8c5;
    +
  • + +
  • + +
    oneterm-online
    +
    &#xe8c6;
    +
  • + +
  • + +
    oneterm-online-selected
    +
    &#xe8c7;
    +
  • + +
  • + +
    oneterm-history-selected
    +
    &#xe8c8;
    +
  • + +
  • + +
    oneterm-history
    +
    &#xe8c9;
    +
  • + +
  • + +
    oneterm-entry_log
    +
    &#xe8ca;
    +
  • + +
  • + +
    oneterm-entry_log-selected
    +
    &#xe8cb;
    +
  • + +
  • + +
    oneterm-operation_log
    +
    &#xe8cc;
    +
  • + +
  • + +
    oneterm-operation_log-selected
    +
    &#xe8cd;
    +
  • + +
  • + +
    oneterm-workstation-selected
    +
    &#xe8b7;
    +
  • + +
  • + +
    oneterm-workstation
    +
    &#xe8b8;
    +
  • + +
  • + +
    oneterm-file-selected
    +
    &#xe8be;
    +
  • + +
  • + +
    oneterm-file
    +
    &#xe8bc;
    +
  • + +
  • + +
    oneterm-time
    +
    &#xe8bd;
    +
  • + +
  • + +
    oneterm-download
    +
    &#xe8bb;
    +
  • + +
  • + +
    oneterm-command record
    +
    &#xe8ba;
    +
  • + +
  • + +
    oneterm-connected assets
    +
    &#xe8b6;
    +
  • + +
  • + +
    oneterm-total assets
    +
    &#xe8b5;
    +
  • + +
  • + +
    oneterm-switch (3)
    +
    &#xe8b4;
    +
  • + +
  • + +
    oneterm-session
    +
    &#xe8b3;
    +
  • + +
  • + +
    oneterm-connection
    +
    &#xe8b2;
    +
  • + +
  • + +
    oneterm-log in
    +
    &#xe8b1;
    +
  • + +
  • + +
    oneterm-dashboard
    +
    &#xe8af;
    +
  • + +
  • + +
    oneterm-dashboard-selected
    +
    &#xe8b0;
    +
  • + +
  • + +
    oneterm-recent session
    +
    &#xe8ae;
    +
  • + +
  • + +
    oneterm-my assets
    +
    &#xe8ad;
    +
  • + +
  • + +
    oneterm-log
    +
    &#xe8aa;
    +
  • + +
  • + +
    oneterm-conversation-selected
    +
    &#xe8ab;
    +
  • + +
  • + +
    oneterm-conversation
    +
    &#xe8ac;
    +
  • + +
  • + +
    oneterm-log-selected
    +
    &#xe8a9;
    +
  • + +
  • + +
    oneterm-assets
    +
    &#xe8a7;
    +
  • + +
  • + +
    oneterm-assets-selected
    +
    &#xe8a8;
    +
  • + +
  • + +
    itsm-down
    +
    &#xe8a5;
    +
  • + +
  • + +
    itsm-up
    +
    &#xe8a6;
    +
  • + +
  • + +
    itsm-download
    +
    &#xe8a4;
    +
  • + +
  • + +
    itsm-print
    +
    &#xe8a3;
    +
  • + +
  • + +
    itsm-view
    +
    &#xe8a2;
    +
  • + +
  • + +
    itsm-word
    +
    &#xe8a1;
    +
  • + +
  • + +
    datainsight-custom
    +
    &#xe89e;
    +
  • + +
  • + +
    datainsight-prometheus
    +
    &#xe89f;
    +
  • + +
  • + +
    datainsight-zabbix
    +
    &#xe8a0;
    +
  • + +
  • + +
    setting-main people
    +
    &#xe89a;
    +
  • + +
  • + +
    setting-deputy people
    +
    &#xe89d;
    +
  • + +
  • + +
    ops-setting-duty
    +
    &#xe89c;
    +
  • + +
  • + +
    ops-setting-duty-selected
    +
    &#xe89b;
    +
  • + +
  • + +
    datainsight-sequential
    +
    &#xe899;
    +
  • + +
  • + +
    datainsight-close
    +
    &#xe898;
    +
  • + +
  • + +
    datainsight-handle
    +
    &#xe897;
    +
  • + +
  • + +
    datainsight-table
    +
    &#xe896;
    +
  • +
  • icon-xianxing-password
    @@ -1226,7 +1970,7 @@
  • -
    caise-chajian
    +
    caise-chajian
    &#xe7d2;
  • @@ -4044,9 +4788,9 @@
    @font-face {
       font-family: 'iconfont';
    -  src: url('iconfont.woff2?t=1698273699449') format('woff2'),
    -       url('iconfont.woff?t=1698273699449') format('woff'),
    -       url('iconfont.ttf?t=1698273699449') format('truetype');
    +  src: url('iconfont.woff2?t=1711963254221') format('woff2'),
    +       url('iconfont.woff?t=1711963254221') format('woff'),
    +       url('iconfont.ttf?t=1711963254221') format('truetype');
     }
     

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

    @@ -4072,6 +4816,1122 @@
      +
    • + +
      + VPC +
      +
      .caise-VPC +
      +
    • + +
    • + +
      + CDN +
      +
      .caise-CDN +
      +
    • + +
    • + +
      + OOS +
      +
      .caise-OOS +
      +
    • + +
    • + +
      + Google Cloud Platform +
      +
      .Google_Cloud_Platform +
      +
    • + +
    • + +
      + Ctyun +
      +
      .Ctyun +
      +
    • + +
    • + +
      + Alibaba Cloud +
      +
      .Alibaba_Cloud +
      +
    • + +
    • + +
      + Azure +
      +
      .Azure +
      +
    • + +
    • + +
      + ZStack +
      +
      .ZStack +
      +
    • + +
    • + +
      + Tencent Cloud +
      +
      .Tencent_Cloud +
      +
    • + +
    • + +
      + Nutanix +
      +
      .Nutanix +
      +
    • + +
    • + +
      + OpenStack +
      +
      .OpenStack +
      +
    • + +
    • + +
      + Huawei Cloud +
      +
      .Huawei_Cloud +
      +
    • + +
    • + +
      + Bytecloud +
      +
      .Bytecloud +
      +
    • + +
    • + +
      + UCloud +
      +
      .UCloud +
      +
    • + +
    • + +
      + AWS +
      +
      .AWS +
      +
    • + +
    • + +
      + ECloud +
      +
      .ECloud +
      +
    • + +
    • + +
      + JDCloud +
      +
      .JDCloud +
      +
    • + +
    • + +
      + veops-more +
      +
      .veops-more +
      +
    • + +
    • + +
      + duose-date +
      +
      .duose-date +
      +
    • + +
    • + +
      + duose-shishu +
      +
      .duose-shishu +
      +
    • + +
    • + +
      + duose-wenben +
      +
      .duose-wenben +
      +
    • + +
    • + +
      + duose-json +
      +
      .duose-json +
      +
    • + +
    • + +
      + duose-fudianshu +
      +
      .duose-fudianshu +
      +
    • + +
    • + +
      + duose-time +
      +
      .duose-time +
      +
    • + +
    • + +
      + duose-password +
      +
      .duose-password +
      +
    • + +
    • + +
      + duose-link +
      +
      .duose-link +
      +
    • + +
    • + +
      + duose-datetime +
      +
      .duose-datetime +
      +
    • + +
    • + +
      + veops-setting2 +
      +
      .veops-setting2 +
      +
    • + +
    • + +
      + veops-search +
      +
      .veops-search +
      +
    • + +
    • + +
      + veops-delete +
      +
      .veops-delete +
      +
    • + +
    • + +
      + veops-refresh +
      +
      .veops-refresh +
      +
    • + +
    • + +
      + veops-filter +
      +
      .veops-filter +
      +
    • + +
    • + +
      + veops-reduce +
      +
      .veops-reduce +
      +
    • + +
    • + +
      + veops-increase +
      +
      .veops-increase +
      +
    • + +
    • + +
      + veops-configuration_table +
      +
      .veops-configuration_table +
      +
    • + +
    • + +
      + veops-copy +
      +
      .veops-copy +
      +
    • + +
    • + +
      + veops-save +
      +
      .veops-save +
      +
    • + +
    • + +
      + veops-setting +
      +
      .veops-setting +
      +
    • + +
    • + +
      + veops-default_avatar +
      +
      .veops-default_avatar +
      +
    • + +
    • + +
      + veops-notice +
      +
      .veops-notice +
      +
    • + +
    • + +
      + quickly_initiate +
      +
      .itsm-quickStart +
      +
    • + +
    • + +
      + itsm-associated +
      +
      .itsm-associatedWith +
      +
    • + +
    • + +
      + itsm-folder +
      +
      .itsm-folder +
      +
    • + +
    • + +
      + report +
      +
      .report +
      +
    • + +
    • + +
      + folder +
      +
      .folder +
      +
    • + +
    • + +
      + itsm-refresh (1) +
      +
      .itsm-refresh +
      +
    • + +
    • + +
      + itsm-add_table (1) +
      +
      .itsm-add_table +
      +
    • + +
    • + +
      + itsm-delete_page +
      +
      .itsm-delete_page +
      +
    • + +
    • + +
      + oneterm-secret_key +
      +
      .oneterm-secret_key +
      +
    • + +
    • + +
      + oneterm-password +
      +
      .oneterm-password +
      +
    • + +
    • + +
      + itsm-unprocessed +
      +
      .itsm-sla_timeout_not_handled +
      +
    • + +
    • + +
      + itsm-not_timeout +
      +
      .itsm-sla_not_timeout +
      +
    • + +
    • + +
      + itsm-SLA +
      +
      .itsm-SLA +
      +
    • + +
    • + +
      + itsm-processed +
      +
      .itsm-sla_timeout_handled +
      +
    • + +
    • + +
      + itsm-all_SLA +
      +
      .itsm-sla_all +
      +
    • + +
    • + +
      + itsm-generate_by_node_id +
      +
      .itsm-generate_by_node_id +
      +
    • + +
    • + +
      + cmdb-MySQL +
      +
      .cmdb-MySQL +
      +
    • + +
    • + +
      + OAuth2.0 +
      +
      .OAUTH2 +
      +
    • + +
    • + +
      + OIDC +
      +
      .OIDC +
      +
    • + +
    • + +
      + cas +
      +
      .CAS +
      +
    • + +
    • + +
      + setting-authentication +
      +
      .ops-setting-auth +
      +
    • + +
    • + +
      + setting-authentication-selected +
      +
      .ops-setting-auth-selected +
      +
    • + +
    • + +
      + itsm-knowledge (2) +
      +
      .itsm-knowledge2 +
      +
    • + +
    • + +
      + itsm-QRcode +
      +
      .itsm-qrdownload +
      +
    • + +
    • + +
      + oneterm-playback +
      +
      .oneterm-playback +
      +
    • + +
    • + +
      + oneterm-disconnect +
      +
      .oneterm-disconnect +
      +
    • + +
    • + +
      + oneterm-key-selected +
      +
      .ops-oneterm-publickey-selected +
      +
    • + +
    • + +
      + oneterm-key +
      +
      .ops-oneterm-publickey +
      +
    • + +
    • + +
      + oneterm-gateway +
      +
      .ops-oneterm-gateway +
      +
    • + +
    • + +
      + oneterm-gateway-selected +
      +
      .ops-oneterm-gateway-selected +
      +
    • + +
    • + +
      + oneterm-account +
      +
      .ops-oneterm-account +
      +
    • + +
    • + +
      + oneterm-account-selected +
      +
      .ops-oneterm-account-selected +
      +
    • + +
    • + +
      + oneterm-command +
      +
      .ops-oneterm-command +
      +
    • + +
    • + +
      + oneterm-command-selected +
      +
      .ops-oneterm-command-selected +
      +
    • + +
    • + +
      + oneterm-asset_list +
      +
      .ops-oneterm-assetlist +
      +
    • + +
    • + +
      + oneterm-asset_list-selected +
      +
      .ops-oneterm-assetlist-selected +
      +
    • + +
    • + +
      + oneterm-online +
      +
      .ops-oneterm-sessiononline +
      +
    • + +
    • + +
      + oneterm-online-selected +
      +
      .ops-oneterm-sessiononline-selected +
      +
    • + +
    • + +
      + oneterm-history-selected +
      +
      .ops-oneterm-sessionhistory-selected +
      +
    • + +
    • + +
      + oneterm-history +
      +
      .ops-oneterm-sessionhistory +
      +
    • + +
    • + +
      + oneterm-entry_log +
      +
      .ops-oneterm-login +
      +
    • + +
    • + +
      + oneterm-entry_log-selected +
      +
      .ops-oneterm-login-selected +
      +
    • + +
    • + +
      + oneterm-operation_log +
      +
      .ops-oneterm-operation +
      +
    • + +
    • + +
      + oneterm-operation_log-selected +
      +
      .ops-oneterm-operation-selected +
      +
    • + +
    • + +
      + oneterm-workstation-selected +
      +
      .ops-oneterm-workstation-selected +
      +
    • + +
    • + +
      + oneterm-workstation +
      +
      .ops-oneterm-workstation +
      +
    • + +
    • + +
      + oneterm-file-selected +
      +
      .oneterm-file-selected +
      +
    • + +
    • + +
      + oneterm-file +
      +
      .oneterm-file +
      +
    • + +
    • + +
      + oneterm-time +
      +
      .oneterm-time +
      +
    • + +
    • + +
      + oneterm-download +
      +
      .oneterm-download +
      +
    • + +
    • + +
      + oneterm-command record +
      +
      .oneterm-commandrecord +
      +
    • + +
    • + +
      + oneterm-connected assets +
      +
      .oneterm-asset +
      +
    • + +
    • + +
      + oneterm-total assets +
      +
      .oneterm-total_asset +
      +
    • + +
    • + +
      + oneterm-switch (3) +
      +
      .oneterm-switch +
      +
    • + +
    • + +
      + oneterm-session +
      +
      .oneterm-session +
      +
    • + +
    • + +
      + oneterm-connection +
      +
      .oneterm-connect +
      +
    • + +
    • + +
      + oneterm-log in +
      +
      .oneterm-login +
      +
    • + +
    • + +
      + oneterm-dashboard +
      +
      .ops-oneterm-dashboard +
      +
    • + +
    • + +
      + oneterm-dashboard-selected +
      +
      .ops-oneterm-dashboard-selected +
      +
    • + +
    • + +
      + oneterm-recent session +
      +
      .oneterm-recentsession +
      +
    • + +
    • + +
      + oneterm-my assets +
      +
      .oneterm-myassets +
      +
    • + +
    • + +
      + oneterm-log +
      +
      .ops-oneterm-log +
      +
    • + +
    • + +
      + oneterm-conversation-selected +
      +
      .ops-oneterm-session-selected +
      +
    • + +
    • + +
      + oneterm-conversation +
      +
      .ops-oneterm-session +
      +
    • + +
    • + +
      + oneterm-log-selected +
      +
      .ops-oneterm-log-selected +
      +
    • + +
    • + +
      + oneterm-assets +
      +
      .ops-oneterm-assets +
      +
    • + +
    • + +
      + oneterm-assets-selected +
      +
      .ops-oneterm-assets-selected +
      +
    • + +
    • + +
      + itsm-down +
      +
      .itsm-down +
      +
    • + +
    • + +
      + itsm-up +
      +
      .itsm-up +
      +
    • + +
    • + +
      + itsm-download +
      +
      .itsm-download +
      +
    • + +
    • + +
      + itsm-print +
      +
      .itsm-print +
      +
    • + +
    • + +
      + itsm-view +
      +
      .itsm-view +
      +
    • + +
    • + +
      + itsm-word +
      +
      .itsm-word +
      +
    • + +
    • + +
      + datainsight-custom +
      +
      .datainsight-custom +
      +
    • + +
    • + +
      + datainsight-prometheus +
      +
      .datainsight-prometheus +
      +
    • + +
    • + +
      + datainsight-zabbix +
      +
      .datainsight-zabbix +
      +
    • + +
    • + +
      + setting-main people +
      +
      .setting-mainpeople +
      +
    • + +
    • + +
      + setting-deputy people +
      +
      .setting-deputypeople +
      +
    • + +
    • + +
      + ops-setting-duty +
      +
      .ops-setting-duty +
      +
    • + +
    • + +
      + ops-setting-duty-selected +
      +
      .ops-setting-duty-selected +
      +
    • + +
    • + +
      + datainsight-sequential +
      +
      .datainsight-sequential +
      +
    • + +
    • + +
      + datainsight-close +
      +
      .datainsight-close +
      +
    • + +
    • + +
      + datainsight-handle +
      +
      .datainsight-handle +
      +
    • + +
    • + +
      + datainsight-table +
      +
      .datainsight-table +
      +
    • +
    • @@ -4091,20 +5951,20 @@
    • - +
      itsm-oneclick download
      -
      .a-itsm-oneclickdownload +
      .itsm-download-all
    • - +
      itsm-package download
      -
      .a-itsm-packagedownload +
      .itsm-download-package
    • @@ -5830,7 +7690,7 @@
    • - caise-chajian + caise-chajian
      .caise-chajian
      @@ -10057,6 +11917,998 @@
        +
      • + +
        VPC
        +
        #caise-VPC
        +
      • + +
      • + +
        CDN
        +
        #caise-CDN
        +
      • + +
      • + +
        OOS
        +
        #caise-OOS
        +
      • + +
      • + +
        Google Cloud Platform
        +
        #Google_Cloud_Platform
        +
      • + +
      • + +
        Ctyun
        +
        #Ctyun
        +
      • + +
      • + +
        Alibaba Cloud
        +
        #Alibaba_Cloud
        +
      • + +
      • + +
        Azure
        +
        #Azure
        +
      • + +
      • + +
        ZStack
        +
        #ZStack
        +
      • + +
      • + +
        Tencent Cloud
        +
        #Tencent_Cloud
        +
      • + +
      • + +
        Nutanix
        +
        #Nutanix
        +
      • + +
      • + +
        OpenStack
        +
        #OpenStack
        +
      • + +
      • + +
        Huawei Cloud
        +
        #Huawei_Cloud
        +
      • + +
      • + +
        Bytecloud
        +
        #Bytecloud
        +
      • + +
      • + +
        UCloud
        +
        #UCloud
        +
      • + +
      • + +
        AWS
        +
        #AWS
        +
      • + +
      • + +
        ECloud
        +
        #ECloud
        +
      • + +
      • + +
        JDCloud
        +
        #JDCloud
        +
      • + +
      • + +
        veops-more
        +
        #veops-more
        +
      • + +
      • + +
        duose-date
        +
        #duose-date
        +
      • + +
      • + +
        duose-shishu
        +
        #duose-shishu
        +
      • + +
      • + +
        duose-wenben
        +
        #duose-wenben
        +
      • + +
      • + +
        duose-json
        +
        #duose-json
        +
      • + +
      • + +
        duose-fudianshu
        +
        #duose-fudianshu
        +
      • + +
      • + +
        duose-time
        +
        #duose-time
        +
      • + +
      • + +
        duose-password
        +
        #duose-password
        +
      • + +
      • + +
        duose-link
        +
        #duose-link
        +
      • + +
      • + +
        duose-datetime
        +
        #duose-datetime
        +
      • + +
      • + +
        veops-setting2
        +
        #veops-setting2
        +
      • + +
      • + +
        veops-search
        +
        #veops-search
        +
      • + +
      • + +
        veops-delete
        +
        #veops-delete
        +
      • + +
      • + +
        veops-refresh
        +
        #veops-refresh
        +
      • + +
      • + +
        veops-filter
        +
        #veops-filter
        +
      • + +
      • + +
        veops-reduce
        +
        #veops-reduce
        +
      • + +
      • + +
        veops-increase
        +
        #veops-increase
        +
      • + +
      • + +
        veops-configuration_table
        +
        #veops-configuration_table
        +
      • + +
      • + +
        veops-copy
        +
        #veops-copy
        +
      • + +
      • + +
        veops-save
        +
        #veops-save
        +
      • + +
      • + +
        veops-setting
        +
        #veops-setting
        +
      • + +
      • + +
        veops-default_avatar
        +
        #veops-default_avatar
        +
      • + +
      • + +
        veops-notice
        +
        #veops-notice
        +
      • + +
      • + +
        quickly_initiate
        +
        #itsm-quickStart
        +
      • + +
      • + +
        itsm-associated
        +
        #itsm-associatedWith
        +
      • + +
      • + +
        itsm-folder
        +
        #itsm-folder
        +
      • + +
      • + +
        report
        +
        #report
        +
      • + +
      • + +
        folder
        +
        #folder
        +
      • + +
      • + +
        itsm-refresh (1)
        +
        #itsm-refresh
        +
      • + +
      • + +
        itsm-add_table (1)
        +
        #itsm-add_table
        +
      • + +
      • + +
        itsm-delete_page
        +
        #itsm-delete_page
        +
      • + +
      • + +
        oneterm-secret_key
        +
        #oneterm-secret_key
        +
      • + +
      • + +
        oneterm-password
        +
        #oneterm-password
        +
      • + +
      • + +
        itsm-unprocessed
        +
        #itsm-sla_timeout_not_handled
        +
      • + +
      • + +
        itsm-not_timeout
        +
        #itsm-sla_not_timeout
        +
      • + +
      • + +
        itsm-SLA
        +
        #itsm-SLA
        +
      • + +
      • + +
        itsm-processed
        +
        #itsm-sla_timeout_handled
        +
      • + +
      • + +
        itsm-all_SLA
        +
        #itsm-sla_all
        +
      • + +
      • + +
        itsm-generate_by_node_id
        +
        #itsm-generate_by_node_id
        +
      • + +
      • + +
        cmdb-MySQL
        +
        #cmdb-MySQL
        +
      • + +
      • + +
        OAuth2.0
        +
        #OAUTH2
        +
      • + +
      • + +
        OIDC
        +
        #OIDC
        +
      • + +
      • + +
        cas
        +
        #CAS
        +
      • + +
      • + +
        setting-authentication
        +
        #ops-setting-auth
        +
      • + +
      • + +
        setting-authentication-selected
        +
        #ops-setting-auth-selected
        +
      • + +
      • + +
        itsm-knowledge (2)
        +
        #itsm-knowledge2
        +
      • + +
      • + +
        itsm-QRcode
        +
        #itsm-qrdownload
        +
      • + +
      • + +
        oneterm-playback
        +
        #oneterm-playback
        +
      • + +
      • + +
        oneterm-disconnect
        +
        #oneterm-disconnect
        +
      • + +
      • + +
        oneterm-key-selected
        +
        #ops-oneterm-publickey-selected
        +
      • + +
      • + +
        oneterm-key
        +
        #ops-oneterm-publickey
        +
      • + +
      • + +
        oneterm-gateway
        +
        #ops-oneterm-gateway
        +
      • + +
      • + +
        oneterm-gateway-selected
        +
        #ops-oneterm-gateway-selected
        +
      • + +
      • + +
        oneterm-account
        +
        #ops-oneterm-account
        +
      • + +
      • + +
        oneterm-account-selected
        +
        #ops-oneterm-account-selected
        +
      • + +
      • + +
        oneterm-command
        +
        #ops-oneterm-command
        +
      • + +
      • + +
        oneterm-command-selected
        +
        #ops-oneterm-command-selected
        +
      • + +
      • + +
        oneterm-asset_list
        +
        #ops-oneterm-assetlist
        +
      • + +
      • + +
        oneterm-asset_list-selected
        +
        #ops-oneterm-assetlist-selected
        +
      • + +
      • + +
        oneterm-online
        +
        #ops-oneterm-sessiononline
        +
      • + +
      • + +
        oneterm-online-selected
        +
        #ops-oneterm-sessiononline-selected
        +
      • + +
      • + +
        oneterm-history-selected
        +
        #ops-oneterm-sessionhistory-selected
        +
      • + +
      • + +
        oneterm-history
        +
        #ops-oneterm-sessionhistory
        +
      • + +
      • + +
        oneterm-entry_log
        +
        #ops-oneterm-login
        +
      • + +
      • + +
        oneterm-entry_log-selected
        +
        #ops-oneterm-login-selected
        +
      • + +
      • + +
        oneterm-operation_log
        +
        #ops-oneterm-operation
        +
      • + +
      • + +
        oneterm-operation_log-selected
        +
        #ops-oneterm-operation-selected
        +
      • + +
      • + +
        oneterm-workstation-selected
        +
        #ops-oneterm-workstation-selected
        +
      • + +
      • + +
        oneterm-workstation
        +
        #ops-oneterm-workstation
        +
      • + +
      • + +
        oneterm-file-selected
        +
        #oneterm-file-selected
        +
      • + +
      • + +
        oneterm-file
        +
        #oneterm-file
        +
      • + +
      • + +
        oneterm-time
        +
        #oneterm-time
        +
      • + +
      • + +
        oneterm-download
        +
        #oneterm-download
        +
      • + +
      • + +
        oneterm-command record
        +
        #oneterm-commandrecord
        +
      • + +
      • + +
        oneterm-connected assets
        +
        #oneterm-asset
        +
      • + +
      • + +
        oneterm-total assets
        +
        #oneterm-total_asset
        +
      • + +
      • + +
        oneterm-switch (3)
        +
        #oneterm-switch
        +
      • + +
      • + +
        oneterm-session
        +
        #oneterm-session
        +
      • + +
      • + +
        oneterm-connection
        +
        #oneterm-connect
        +
      • + +
      • + +
        oneterm-log in
        +
        #oneterm-login
        +
      • + +
      • + +
        oneterm-dashboard
        +
        #ops-oneterm-dashboard
        +
      • + +
      • + +
        oneterm-dashboard-selected
        +
        #ops-oneterm-dashboard-selected
        +
      • + +
      • + +
        oneterm-recent session
        +
        #oneterm-recentsession
        +
      • + +
      • + +
        oneterm-my assets
        +
        #oneterm-myassets
        +
      • + +
      • + +
        oneterm-log
        +
        #ops-oneterm-log
        +
      • + +
      • + +
        oneterm-conversation-selected
        +
        #ops-oneterm-session-selected
        +
      • + +
      • + +
        oneterm-conversation
        +
        #ops-oneterm-session
        +
      • + +
      • + +
        oneterm-log-selected
        +
        #ops-oneterm-log-selected
        +
      • + +
      • + +
        oneterm-assets
        +
        #ops-oneterm-assets
        +
      • + +
      • + +
        oneterm-assets-selected
        +
        #ops-oneterm-assets-selected
        +
      • + +
      • + +
        itsm-down
        +
        #itsm-down
        +
      • + +
      • + +
        itsm-up
        +
        #itsm-up
        +
      • + +
      • + +
        itsm-download
        +
        #itsm-download
        +
      • + +
      • + +
        itsm-print
        +
        #itsm-print
        +
      • + +
      • + +
        itsm-view
        +
        #itsm-view
        +
      • + +
      • + +
        itsm-word
        +
        #itsm-word
        +
      • + +
      • + +
        datainsight-custom
        +
        #datainsight-custom
        +
      • + +
      • + +
        datainsight-prometheus
        +
        #datainsight-prometheus
        +
      • + +
      • + +
        datainsight-zabbix
        +
        #datainsight-zabbix
        +
      • + +
      • + +
        setting-main people
        +
        #setting-mainpeople
        +
      • + +
      • + +
        setting-deputy people
        +
        #setting-deputypeople
        +
      • + +
      • + +
        ops-setting-duty
        +
        #ops-setting-duty
        +
      • + +
      • + +
        ops-setting-duty-selected
        +
        #ops-setting-duty-selected
        +
      • + +
      • + +
        datainsight-sequential
        +
        #datainsight-sequential
        +
      • + +
      • + +
        datainsight-close
        +
        #datainsight-close
        +
      • + +
      • + +
        datainsight-handle
        +
        #datainsight-handle
        +
      • + +
      • + +
        datainsight-table
        +
        #datainsight-table
        +
      • +
      • itsm-oneclick download
        -
        #a-itsm-oneclickdownload
        +
        #itsm-download-all
      • itsm-package download
        -
        #a-itsm-packagedownload
        +
        #itsm-download-package
      • @@ -11621,7 +14473,7 @@ -
        caise-chajian
        +
        caise-chajian
        #caise-chajian
      • diff --git a/cmdb-ui/public/iconfont/iconfont.css b/cmdb-ui/public/iconfont/iconfont.css index 36548c3..a66ed77 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=1698273699449') format('woff2'), - url('iconfont.woff?t=1698273699449') format('woff'), - url('iconfont.ttf?t=1698273699449') format('truetype'); + src: url('iconfont.woff2?t=1711963254221') format('woff2'), + url('iconfont.woff?t=1711963254221') format('woff'), + url('iconfont.ttf?t=1711963254221') format('truetype'); } .iconfont { @@ -13,6 +13,502 @@ -moz-osx-font-smoothing: grayscale; } +.caise-VPC:before { + content: "\e910"; +} + +.caise-CDN:before { + content: "\e911"; +} + +.caise-OOS:before { + content: "\e90f"; +} + +.Google_Cloud_Platform:before { + content: "\e90b"; +} + +.Ctyun:before { + content: "\e90c"; +} + +.Alibaba_Cloud:before { + content: "\e90d"; +} + +.Azure:before { + content: "\e90e"; +} + +.ZStack:before { + content: "\e904"; +} + +.Tencent_Cloud:before { + content: "\e905"; +} + +.Nutanix:before { + content: "\e906"; +} + +.OpenStack:before { + content: "\e907"; +} + +.Huawei_Cloud:before { + content: "\e908"; +} + +.Bytecloud:before { + content: "\e909"; +} + +.UCloud:before { + content: "\e90a"; +} + +.AWS:before { + content: "\e901"; +} + +.ECloud:before { + content: "\e902"; +} + +.JDCloud:before { + content: "\e903"; +} + +.veops-more:before { + content: "\e900"; +} + +.duose-date:before { + content: "\e8ff"; +} + +.duose-shishu:before { + content: "\e8fd"; +} + +.duose-wenben:before { + content: "\e8fe"; +} + +.duose-json:before { + content: "\e8f7"; +} + +.duose-fudianshu:before { + content: "\e8f8"; +} + +.duose-time:before { + content: "\e8f9"; +} + +.duose-password:before { + content: "\e8fa"; +} + +.duose-link:before { + content: "\e8fb"; +} + +.duose-datetime:before { + content: "\e8fc"; +} + +.veops-setting2:before { + content: "\e8f6"; +} + +.veops-search:before { + content: "\e8f5"; +} + +.veops-delete:before { + content: "\e8f4"; +} + +.veops-refresh:before { + content: "\e8f3"; +} + +.veops-filter:before { + content: "\e8f2"; +} + +.veops-reduce:before { + content: "\e8ed"; +} + +.veops-increase:before { + content: "\e8ee"; +} + +.veops-configuration_table:before { + content: "\e8ef"; +} + +.veops-copy:before { + content: "\e8f0"; +} + +.veops-save:before { + content: "\e8f1"; +} + +.veops-setting:before { + content: "\e8ec"; +} + +.veops-default_avatar:before { + content: "\e8ea"; +} + +.veops-notice:before { + content: "\e8eb"; +} + +.itsm-quickStart:before { + content: "\e8e9"; +} + +.itsm-associatedWith:before { + content: "\e8e8"; +} + +.itsm-folder:before { + content: "\e8e7"; +} + +.report:before { + content: "\e8e5"; +} + +.folder:before { + content: "\e8e6"; +} + +.itsm-refresh:before { + content: "\e8e4"; +} + +.itsm-add_table:before { + content: "\e8e2"; +} + +.itsm-delete_page:before { + content: "\e8e3"; +} + +.oneterm-secret_key:before { + content: "\e8e0"; +} + +.oneterm-password:before { + content: "\e8e1"; +} + +.itsm-sla_timeout_not_handled:before { + content: "\e8dd"; +} + +.itsm-sla_not_timeout:before { + content: "\e8de"; +} + +.itsm-SLA:before { + content: "\e8df"; +} + +.itsm-sla_timeout_handled:before { + content: "\e8dc"; +} + +.itsm-sla_all:before { + content: "\e8da"; +} + +.itsm-generate_by_node_id:before { + content: "\e8db"; +} + +.cmdb-MySQL:before { + content: "\e8d9"; +} + +.OAUTH2:before { + content: "\e8d8"; +} + +.OIDC:before { + content: "\e8d6"; +} + +.CAS:before { + content: "\e8d7"; +} + +.ops-setting-auth:before { + content: "\e8d5"; +} + +.ops-setting-auth-selected:before { + content: "\e8d4"; +} + +.itsm-knowledge2:before { + content: "\e8d2"; +} + +.itsm-qrdownload:before { + content: "\e8d3"; +} + +.oneterm-playback:before { + content: "\e8d1"; +} + +.oneterm-disconnect:before { + content: "\e8d0"; +} + +.ops-oneterm-publickey-selected:before { + content: "\e8cf"; +} + +.ops-oneterm-publickey:before { + content: "\e8ce"; +} + +.ops-oneterm-gateway:before { + content: "\e8b9"; +} + +.ops-oneterm-gateway-selected:before { + content: "\e8bf"; +} + +.ops-oneterm-account:before { + content: "\e8c0"; +} + +.ops-oneterm-account-selected:before { + content: "\e8c1"; +} + +.ops-oneterm-command:before { + content: "\e8c2"; +} + +.ops-oneterm-command-selected:before { + content: "\e8c3"; +} + +.ops-oneterm-assetlist:before { + content: "\e8c4"; +} + +.ops-oneterm-assetlist-selected:before { + content: "\e8c5"; +} + +.ops-oneterm-sessiononline:before { + content: "\e8c6"; +} + +.ops-oneterm-sessiononline-selected:before { + content: "\e8c7"; +} + +.ops-oneterm-sessionhistory-selected:before { + content: "\e8c8"; +} + +.ops-oneterm-sessionhistory:before { + content: "\e8c9"; +} + +.ops-oneterm-login:before { + content: "\e8ca"; +} + +.ops-oneterm-login-selected:before { + content: "\e8cb"; +} + +.ops-oneterm-operation:before { + content: "\e8cc"; +} + +.ops-oneterm-operation-selected:before { + content: "\e8cd"; +} + +.ops-oneterm-workstation-selected:before { + content: "\e8b7"; +} + +.ops-oneterm-workstation:before { + content: "\e8b8"; +} + +.oneterm-file-selected:before { + content: "\e8be"; +} + +.oneterm-file:before { + content: "\e8bc"; +} + +.oneterm-time:before { + content: "\e8bd"; +} + +.oneterm-download:before { + content: "\e8bb"; +} + +.oneterm-commandrecord:before { + content: "\e8ba"; +} + +.oneterm-asset:before { + content: "\e8b6"; +} + +.oneterm-total_asset:before { + content: "\e8b5"; +} + +.oneterm-switch:before { + content: "\e8b4"; +} + +.oneterm-session:before { + content: "\e8b3"; +} + +.oneterm-connect:before { + content: "\e8b2"; +} + +.oneterm-login:before { + content: "\e8b1"; +} + +.ops-oneterm-dashboard:before { + content: "\e8af"; +} + +.ops-oneterm-dashboard-selected:before { + content: "\e8b0"; +} + +.oneterm-recentsession:before { + content: "\e8ae"; +} + +.oneterm-myassets:before { + content: "\e8ad"; +} + +.ops-oneterm-log:before { + content: "\e8aa"; +} + +.ops-oneterm-session-selected:before { + content: "\e8ab"; +} + +.ops-oneterm-session:before { + content: "\e8ac"; +} + +.ops-oneterm-log-selected:before { + content: "\e8a9"; +} + +.ops-oneterm-assets:before { + content: "\e8a7"; +} + +.ops-oneterm-assets-selected:before { + content: "\e8a8"; +} + +.itsm-down:before { + content: "\e8a5"; +} + +.itsm-up:before { + content: "\e8a6"; +} + +.itsm-download:before { + content: "\e8a4"; +} + +.itsm-print:before { + content: "\e8a3"; +} + +.itsm-view:before { + content: "\e8a2"; +} + +.itsm-word:before { + content: "\e8a1"; +} + +.datainsight-custom:before { + content: "\e89e"; +} + +.datainsight-prometheus:before { + content: "\e89f"; +} + +.datainsight-zabbix:before { + content: "\e8a0"; +} + +.setting-mainpeople:before { + content: "\e89a"; +} + +.setting-deputypeople:before { + content: "\e89d"; +} + +.ops-setting-duty:before { + content: "\e89c"; +} + +.ops-setting-duty-selected:before { + content: "\e89b"; +} + +.datainsight-sequential:before { + content: "\e899"; +} + +.datainsight-close:before { + content: "\e898"; +} + +.datainsight-handle:before { + content: "\e897"; +} + +.datainsight-table:before { + content: "\e896"; +} + .icon-xianxing-password:before { content: "\e894"; } @@ -21,11 +517,11 @@ content: "\e895"; } -.a-itsm-oneclickdownload:before { +.itsm-download-all:before { content: "\e892"; } -.a-itsm-packagedownload:before { +.itsm-download-package:before { content: "\e893"; } diff --git a/cmdb-ui/public/iconfont/iconfont.js b/cmdb-ui/public/iconfont/iconfont.js index a83d94a..6814692 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 9d39068..345eab4 100644 --- a/cmdb-ui/public/iconfont/iconfont.json +++ b/cmdb-ui/public/iconfont/iconfont.json @@ -5,6 +5,874 @@ "css_prefix_text": "", "description": "", "glyphs": [ + { + "icon_id": "39782649", + "name": "VPC", + "font_class": "caise-VPC", + "unicode": "e910", + "unicode_decimal": 59664 + }, + { + "icon_id": "39782643", + "name": "CDN", + "font_class": "caise-CDN", + "unicode": "e911", + "unicode_decimal": 59665 + }, + { + "icon_id": "39782632", + "name": "OOS", + "font_class": "caise-OOS", + "unicode": "e90f", + "unicode_decimal": 59663 + }, + { + "icon_id": "39729980", + "name": "Google Cloud Platform", + "font_class": "Google_Cloud_Platform", + "unicode": "e90b", + "unicode_decimal": 59659 + }, + { + "icon_id": "39729978", + "name": "Ctyun", + "font_class": "Ctyun", + "unicode": "e90c", + "unicode_decimal": 59660 + }, + { + "icon_id": "39729977", + "name": "Alibaba Cloud", + "font_class": "Alibaba_Cloud", + "unicode": "e90d", + "unicode_decimal": 59661 + }, + { + "icon_id": "39729976", + "name": "Azure", + "font_class": "Azure", + "unicode": "e90e", + "unicode_decimal": 59662 + }, + { + "icon_id": "39729985", + "name": "ZStack", + "font_class": "ZStack", + "unicode": "e904", + "unicode_decimal": 59652 + }, + { + "icon_id": "39729986", + "name": "Tencent Cloud", + "font_class": "Tencent_Cloud", + "unicode": "e905", + "unicode_decimal": 59653 + }, + { + "icon_id": "39729981", + "name": "Nutanix", + "font_class": "Nutanix", + "unicode": "e906", + "unicode_decimal": 59654 + }, + { + "icon_id": "39729983", + "name": "OpenStack", + "font_class": "OpenStack", + "unicode": "e907", + "unicode_decimal": 59655 + }, + { + "icon_id": "39729982", + "name": "Huawei Cloud", + "font_class": "Huawei_Cloud", + "unicode": "e908", + "unicode_decimal": 59656 + }, + { + "icon_id": "39729979", + "name": "Bytecloud", + "font_class": "Bytecloud", + "unicode": "e909", + "unicode_decimal": 59657 + }, + { + "icon_id": "39729984", + "name": "UCloud", + "font_class": "UCloud", + "unicode": "e90a", + "unicode_decimal": 59658 + }, + { + "icon_id": "39729988", + "name": "AWS", + "font_class": "AWS", + "unicode": "e901", + "unicode_decimal": 59649 + }, + { + "icon_id": "39729989", + "name": "ECloud", + "font_class": "ECloud", + "unicode": "e902", + "unicode_decimal": 59650 + }, + { + "icon_id": "39729987", + "name": "JDCloud", + "font_class": "JDCloud", + "unicode": "e903", + "unicode_decimal": 59651 + }, + { + "icon_id": "39713390", + "name": "veops-more", + "font_class": "veops-more", + "unicode": "e900", + "unicode_decimal": 59648 + }, + { + "icon_id": "39712276", + "name": "duose-date", + "font_class": "duose-date", + "unicode": "e8ff", + "unicode_decimal": 59647 + }, + { + "icon_id": "39712289", + "name": "duose-shishu", + "font_class": "duose-shishu", + "unicode": "e8fd", + "unicode_decimal": 59645 + }, + { + "icon_id": "39712286", + "name": "duose-wenben", + "font_class": "duose-wenben", + "unicode": "e8fe", + "unicode_decimal": 59646 + }, + { + "icon_id": "39712314", + "name": "duose-json", + "font_class": "duose-json", + "unicode": "e8f7", + "unicode_decimal": 59639 + }, + { + "icon_id": "39712315", + "name": "duose-fudianshu", + "font_class": "duose-fudianshu", + "unicode": "e8f8", + "unicode_decimal": 59640 + }, + { + "icon_id": "39712312", + "name": "duose-time", + "font_class": "duose-time", + "unicode": "e8f9", + "unicode_decimal": 59641 + }, + { + "icon_id": "39712313", + "name": "duose-password", + "font_class": "duose-password", + "unicode": "e8fa", + "unicode_decimal": 59642 + }, + { + "icon_id": "39712311", + "name": "duose-link", + "font_class": "duose-link", + "unicode": "e8fb", + "unicode_decimal": 59643 + }, + { + "icon_id": "39712310", + "name": "duose-datetime", + "font_class": "duose-datetime", + "unicode": "e8fc", + "unicode_decimal": 59644 + }, + { + "icon_id": "39705895", + "name": "veops-setting2", + "font_class": "veops-setting2", + "unicode": "e8f6", + "unicode_decimal": 59638 + }, + { + "icon_id": "39692404", + "name": "veops-search", + "font_class": "veops-search", + "unicode": "e8f5", + "unicode_decimal": 59637 + }, + { + "icon_id": "39680289", + "name": "veops-delete", + "font_class": "veops-delete", + "unicode": "e8f4", + "unicode_decimal": 59636 + }, + { + "icon_id": "39677378", + "name": "veops-refresh", + "font_class": "veops-refresh", + "unicode": "e8f3", + "unicode_decimal": 59635 + }, + { + "icon_id": "39677152", + "name": "veops-filter", + "font_class": "veops-filter", + "unicode": "e8f2", + "unicode_decimal": 59634 + }, + { + "icon_id": "39677216", + "name": "veops-reduce", + "font_class": "veops-reduce", + "unicode": "e8ed", + "unicode_decimal": 59629 + }, + { + "icon_id": "39677215", + "name": "veops-increase", + "font_class": "veops-increase", + "unicode": "e8ee", + "unicode_decimal": 59630 + }, + { + "icon_id": "39677211", + "name": "veops-configuration_table", + "font_class": "veops-configuration_table", + "unicode": "e8ef", + "unicode_decimal": 59631 + }, + { + "icon_id": "39677210", + "name": "veops-copy", + "font_class": "veops-copy", + "unicode": "e8f0", + "unicode_decimal": 59632 + }, + { + "icon_id": "39677207", + "name": "veops-save", + "font_class": "veops-save", + "unicode": "e8f1", + "unicode_decimal": 59633 + }, + { + "icon_id": "39664281", + "name": "veops-setting", + "font_class": "veops-setting", + "unicode": "e8ec", + "unicode_decimal": 59628 + }, + { + "icon_id": "39664295", + "name": "veops-default_avatar", + "font_class": "veops-default_avatar", + "unicode": "e8ea", + "unicode_decimal": 59626 + }, + { + "icon_id": "39664288", + "name": "veops-notice", + "font_class": "veops-notice", + "unicode": "e8eb", + "unicode_decimal": 59627 + }, + { + "icon_id": "39603135", + "name": "quickly_initiate", + "font_class": "itsm-quickStart", + "unicode": "e8e9", + "unicode_decimal": 59625 + }, + { + "icon_id": "39588554", + "name": "itsm-associated", + "font_class": "itsm-associatedWith", + "unicode": "e8e8", + "unicode_decimal": 59624 + }, + { + "icon_id": "39517726", + "name": "itsm-folder", + "font_class": "itsm-folder", + "unicode": "e8e7", + "unicode_decimal": 59623 + }, + { + "icon_id": "39517542", + "name": "report", + "font_class": "report", + "unicode": "e8e5", + "unicode_decimal": 59621 + }, + { + "icon_id": "39517539", + "name": "folder", + "font_class": "folder", + "unicode": "e8e6", + "unicode_decimal": 59622 + }, + { + "icon_id": "39123642", + "name": "itsm-refresh (1)", + "font_class": "itsm-refresh", + "unicode": "e8e4", + "unicode_decimal": 59620 + }, + { + "icon_id": "39123405", + "name": "itsm-add_table (1)", + "font_class": "itsm-add_table", + "unicode": "e8e2", + "unicode_decimal": 59618 + }, + { + "icon_id": "39123409", + "name": "itsm-delete_page", + "font_class": "itsm-delete_page", + "unicode": "e8e3", + "unicode_decimal": 59619 + }, + { + "icon_id": "39117681", + "name": "oneterm-secret_key", + "font_class": "oneterm-secret_key", + "unicode": "e8e0", + "unicode_decimal": 59616 + }, + { + "icon_id": "39117679", + "name": "oneterm-password", + "font_class": "oneterm-password", + "unicode": "e8e1", + "unicode_decimal": 59617 + }, + { + "icon_id": "39079529", + "name": "itsm-unprocessed", + "font_class": "itsm-sla_timeout_not_handled", + "unicode": "e8dd", + "unicode_decimal": 59613 + }, + { + "icon_id": "39079522", + "name": "itsm-not_timeout", + "font_class": "itsm-sla_not_timeout", + "unicode": "e8de", + "unicode_decimal": 59614 + }, + { + "icon_id": "39079520", + "name": "itsm-SLA", + "font_class": "itsm-SLA", + "unicode": "e8df", + "unicode_decimal": 59615 + }, + { + "icon_id": "39079538", + "name": "itsm-processed", + "font_class": "itsm-sla_timeout_handled", + "unicode": "e8dc", + "unicode_decimal": 59612 + }, + { + "icon_id": "39079519", + "name": "itsm-all_SLA", + "font_class": "itsm-sla_all", + "unicode": "e8da", + "unicode_decimal": 59610 + }, + { + "icon_id": "38970103", + "name": "itsm-generate_by_node_id", + "font_class": "itsm-generate_by_node_id", + "unicode": "e8db", + "unicode_decimal": 59611 + }, + { + "icon_id": "38806676", + "name": "cmdb-MySQL", + "font_class": "cmdb-MySQL", + "unicode": "e8d9", + "unicode_decimal": 59609 + }, + { + "icon_id": "38566548", + "name": "OAuth2.0", + "font_class": "OAUTH2", + "unicode": "e8d8", + "unicode_decimal": 59608 + }, + { + "icon_id": "38566584", + "name": "OIDC", + "font_class": "OIDC", + "unicode": "e8d6", + "unicode_decimal": 59606 + }, + { + "icon_id": "38566578", + "name": "cas", + "font_class": "CAS", + "unicode": "e8d7", + "unicode_decimal": 59607 + }, + { + "icon_id": "38547395", + "name": "setting-authentication", + "font_class": "ops-setting-auth", + "unicode": "e8d5", + "unicode_decimal": 59605 + }, + { + "icon_id": "38547389", + "name": "setting-authentication-selected", + "font_class": "ops-setting-auth-selected", + "unicode": "e8d4", + "unicode_decimal": 59604 + }, + { + "icon_id": "38533133", + "name": "itsm-knowledge (2)", + "font_class": "itsm-knowledge2", + "unicode": "e8d2", + "unicode_decimal": 59602 + }, + { + "icon_id": "38531868", + "name": "itsm-QRcode", + "font_class": "itsm-qrdownload", + "unicode": "e8d3", + "unicode_decimal": 59603 + }, + { + "icon_id": "38413515", + "name": "oneterm-playback", + "font_class": "oneterm-playback", + "unicode": "e8d1", + "unicode_decimal": 59601 + }, + { + "icon_id": "38413481", + "name": "oneterm-disconnect", + "font_class": "oneterm-disconnect", + "unicode": "e8d0", + "unicode_decimal": 59600 + }, + { + "icon_id": "38407867", + "name": "oneterm-key-selected", + "font_class": "ops-oneterm-publickey-selected", + "unicode": "e8cf", + "unicode_decimal": 59599 + }, + { + "icon_id": "38407915", + "name": "oneterm-key", + "font_class": "ops-oneterm-publickey", + "unicode": "e8ce", + "unicode_decimal": 59598 + }, + { + "icon_id": "38311855", + "name": "oneterm-gateway", + "font_class": "ops-oneterm-gateway", + "unicode": "e8b9", + "unicode_decimal": 59577 + }, + { + "icon_id": "38311938", + "name": "oneterm-gateway-selected", + "font_class": "ops-oneterm-gateway-selected", + "unicode": "e8bf", + "unicode_decimal": 59583 + }, + { + "icon_id": "38311957", + "name": "oneterm-account", + "font_class": "ops-oneterm-account", + "unicode": "e8c0", + "unicode_decimal": 59584 + }, + { + "icon_id": "38311961", + "name": "oneterm-account-selected", + "font_class": "ops-oneterm-account-selected", + "unicode": "e8c1", + "unicode_decimal": 59585 + }, + { + "icon_id": "38311974", + "name": "oneterm-command", + "font_class": "ops-oneterm-command", + "unicode": "e8c2", + "unicode_decimal": 59586 + }, + { + "icon_id": "38311976", + "name": "oneterm-command-selected", + "font_class": "ops-oneterm-command-selected", + "unicode": "e8c3", + "unicode_decimal": 59587 + }, + { + "icon_id": "38311979", + "name": "oneterm-asset_list", + "font_class": "ops-oneterm-assetlist", + "unicode": "e8c4", + "unicode_decimal": 59588 + }, + { + "icon_id": "38311985", + "name": "oneterm-asset_list-selected", + "font_class": "ops-oneterm-assetlist-selected", + "unicode": "e8c5", + "unicode_decimal": 59589 + }, + { + "icon_id": "38312030", + "name": "oneterm-online", + "font_class": "ops-oneterm-sessiononline", + "unicode": "e8c6", + "unicode_decimal": 59590 + }, + { + "icon_id": "38312152", + "name": "oneterm-online-selected", + "font_class": "ops-oneterm-sessiononline-selected", + "unicode": "e8c7", + "unicode_decimal": 59591 + }, + { + "icon_id": "38312154", + "name": "oneterm-history-selected", + "font_class": "ops-oneterm-sessionhistory-selected", + "unicode": "e8c8", + "unicode_decimal": 59592 + }, + { + "icon_id": "38312155", + "name": "oneterm-history", + "font_class": "ops-oneterm-sessionhistory", + "unicode": "e8c9", + "unicode_decimal": 59593 + }, + { + "icon_id": "38312404", + "name": "oneterm-entry_log", + "font_class": "ops-oneterm-login", + "unicode": "e8ca", + "unicode_decimal": 59594 + }, + { + "icon_id": "38312423", + "name": "oneterm-entry_log-selected", + "font_class": "ops-oneterm-login-selected", + "unicode": "e8cb", + "unicode_decimal": 59595 + }, + { + "icon_id": "38312426", + "name": "oneterm-operation_log", + "font_class": "ops-oneterm-operation", + "unicode": "e8cc", + "unicode_decimal": 59596 + }, + { + "icon_id": "38312445", + "name": "oneterm-operation_log-selected", + "font_class": "ops-oneterm-operation-selected", + "unicode": "e8cd", + "unicode_decimal": 59597 + }, + { + "icon_id": "38307876", + "name": "oneterm-workstation-selected", + "font_class": "ops-oneterm-workstation-selected", + "unicode": "e8b7", + "unicode_decimal": 59575 + }, + { + "icon_id": "38307871", + "name": "oneterm-workstation", + "font_class": "ops-oneterm-workstation", + "unicode": "e8b8", + "unicode_decimal": 59576 + }, + { + "icon_id": "38302246", + "name": "oneterm-file-selected", + "font_class": "oneterm-file-selected", + "unicode": "e8be", + "unicode_decimal": 59582 + }, + { + "icon_id": "38302255", + "name": "oneterm-file", + "font_class": "oneterm-file", + "unicode": "e8bc", + "unicode_decimal": 59580 + }, + { + "icon_id": "38203528", + "name": "oneterm-time", + "font_class": "oneterm-time", + "unicode": "e8bd", + "unicode_decimal": 59581 + }, + { + "icon_id": "38203331", + "name": "oneterm-download", + "font_class": "oneterm-download", + "unicode": "e8bb", + "unicode_decimal": 59579 + }, + { + "icon_id": "38201351", + "name": "oneterm-command record", + "font_class": "oneterm-commandrecord", + "unicode": "e8ba", + "unicode_decimal": 59578 + }, + { + "icon_id": "38199341", + "name": "oneterm-connected assets", + "font_class": "oneterm-asset", + "unicode": "e8b6", + "unicode_decimal": 59574 + }, + { + "icon_id": "38199350", + "name": "oneterm-total assets", + "font_class": "oneterm-total_asset", + "unicode": "e8b5", + "unicode_decimal": 59573 + }, + { + "icon_id": "38199303", + "name": "oneterm-switch (3)", + "font_class": "oneterm-switch", + "unicode": "e8b4", + "unicode_decimal": 59572 + }, + { + "icon_id": "38199317", + "name": "oneterm-session", + "font_class": "oneterm-session", + "unicode": "e8b3", + "unicode_decimal": 59571 + }, + { + "icon_id": "38199339", + "name": "oneterm-connection", + "font_class": "oneterm-connect", + "unicode": "e8b2", + "unicode_decimal": 59570 + }, + { + "icon_id": "38198321", + "name": "oneterm-log in", + "font_class": "oneterm-login", + "unicode": "e8b1", + "unicode_decimal": 59569 + }, + { + "icon_id": "38194554", + "name": "oneterm-dashboard", + "font_class": "ops-oneterm-dashboard", + "unicode": "e8af", + "unicode_decimal": 59567 + }, + { + "icon_id": "38194525", + "name": "oneterm-dashboard-selected", + "font_class": "ops-oneterm-dashboard-selected", + "unicode": "e8b0", + "unicode_decimal": 59568 + }, + { + "icon_id": "38194352", + "name": "oneterm-recent session", + "font_class": "oneterm-recentsession", + "unicode": "e8ae", + "unicode_decimal": 59566 + }, + { + "icon_id": "38194383", + "name": "oneterm-my assets", + "font_class": "oneterm-myassets", + "unicode": "e8ad", + "unicode_decimal": 59565 + }, + { + "icon_id": "38194089", + "name": "oneterm-log", + "font_class": "ops-oneterm-log", + "unicode": "e8aa", + "unicode_decimal": 59562 + }, + { + "icon_id": "38194088", + "name": "oneterm-conversation-selected", + "font_class": "ops-oneterm-session-selected", + "unicode": "e8ab", + "unicode_decimal": 59563 + }, + { + "icon_id": "38194065", + "name": "oneterm-conversation", + "font_class": "ops-oneterm-session", + "unicode": "e8ac", + "unicode_decimal": 59564 + }, + { + "icon_id": "38194105", + "name": "oneterm-log-selected", + "font_class": "ops-oneterm-log-selected", + "unicode": "e8a9", + "unicode_decimal": 59561 + }, + { + "icon_id": "38194054", + "name": "oneterm-assets", + "font_class": "ops-oneterm-assets", + "unicode": "e8a7", + "unicode_decimal": 59559 + }, + { + "icon_id": "38194055", + "name": "oneterm-assets-selected", + "font_class": "ops-oneterm-assets-selected", + "unicode": "e8a8", + "unicode_decimal": 59560 + }, + { + "icon_id": "38123087", + "name": "itsm-down", + "font_class": "itsm-down", + "unicode": "e8a5", + "unicode_decimal": 59557 + }, + { + "icon_id": "38123084", + "name": "itsm-up", + "font_class": "itsm-up", + "unicode": "e8a6", + "unicode_decimal": 59558 + }, + { + "icon_id": "38105374", + "name": "itsm-download", + "font_class": "itsm-download", + "unicode": "e8a4", + "unicode_decimal": 59556 + }, + { + "icon_id": "38105235", + "name": "itsm-print", + "font_class": "itsm-print", + "unicode": "e8a3", + "unicode_decimal": 59555 + }, + { + "icon_id": "38104997", + "name": "itsm-view", + "font_class": "itsm-view", + "unicode": "e8a2", + "unicode_decimal": 59554 + }, + { + "icon_id": "38105129", + "name": "itsm-word", + "font_class": "itsm-word", + "unicode": "e8a1", + "unicode_decimal": 59553 + }, + { + "icon_id": "38095730", + "name": "datainsight-custom", + "font_class": "datainsight-custom", + "unicode": "e89e", + "unicode_decimal": 59550 + }, + { + "icon_id": "38095729", + "name": "datainsight-prometheus", + "font_class": "datainsight-prometheus", + "unicode": "e89f", + "unicode_decimal": 59551 + }, + { + "icon_id": "38095728", + "name": "datainsight-zabbix", + "font_class": "datainsight-zabbix", + "unicode": "e8a0", + "unicode_decimal": 59552 + }, + { + "icon_id": "37944507", + "name": "setting-main people", + "font_class": "setting-mainpeople", + "unicode": "e89a", + "unicode_decimal": 59546 + }, + { + "icon_id": "37944503", + "name": "setting-deputy people", + "font_class": "setting-deputypeople", + "unicode": "e89d", + "unicode_decimal": 59549 + }, + { + "icon_id": "37940080", + "name": "ops-setting-duty", + "font_class": "ops-setting-duty", + "unicode": "e89c", + "unicode_decimal": 59548 + }, + { + "icon_id": "37940033", + "name": "ops-setting-duty-selected", + "font_class": "ops-setting-duty-selected", + "unicode": "e89b", + "unicode_decimal": 59547 + }, + { + "icon_id": "37841524", + "name": "datainsight-sequential", + "font_class": "datainsight-sequential", + "unicode": "e899", + "unicode_decimal": 59545 + }, + { + "icon_id": "37841535", + "name": "datainsight-close", + "font_class": "datainsight-close", + "unicode": "e898", + "unicode_decimal": 59544 + }, + { + "icon_id": "37841537", + "name": "datainsight-handle", + "font_class": "datainsight-handle", + "unicode": "e897", + "unicode_decimal": 59543 + }, + { + "icon_id": "37841515", + "name": "datainsight-table", + "font_class": "datainsight-table", + "unicode": "e896", + "unicode_decimal": 59542 + }, { "icon_id": "37830610", "name": "icon-xianxing-password", @@ -22,14 +890,14 @@ { "icon_id": "37822199", "name": "itsm-oneclick download", - "font_class": "a-itsm-oneclickdownload", + "font_class": "itsm-download-all", "unicode": "e892", "unicode_decimal": 59538 }, { "icon_id": "37822198", "name": "itsm-package download", - "font_class": "a-itsm-packagedownload", + "font_class": "itsm-download-package", "unicode": "e893", "unicode_decimal": 59539 }, @@ -1372,7 +2240,7 @@ }, { "icon_id": "35341667", - "name": "caise-chajian ", + "name": "caise-chajian", "font_class": "caise-chajian", "unicode": "e7d2", "unicode_decimal": 59346 diff --git a/cmdb-ui/public/iconfont/iconfont.ttf b/cmdb-ui/public/iconfont/iconfont.ttf index 623d04f..49b7074 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 05874c8..f331b51 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 9048d01..63d2b7f 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/App.vue b/cmdb-ui/src/App.vue index 21a0c08..2ee1de5 100644 --- a/cmdb-ui/src/App.vue +++ b/cmdb-ui/src/App.vue @@ -1,5 +1,5 @@ - - + + + + + diff --git a/cmdb-ui/src/components/Pager/index.js b/cmdb-ui/src/components/Pager/index.js new file mode 100644 index 0000000..79690bb --- /dev/null +++ b/cmdb-ui/src/components/Pager/index.js @@ -0,0 +1,2 @@ +import Pager from './index.vue' +export default Pager diff --git a/cmdb-ui/src/components/Pager/index.vue b/cmdb-ui/src/components/Pager/index.vue new file mode 100644 index 0000000..e67985a --- /dev/null +++ b/cmdb-ui/src/components/Pager/index.vue @@ -0,0 +1,137 @@ + + + + + diff --git a/cmdb-ui/src/components/RegexSelect/constants.js b/cmdb-ui/src/components/RegexSelect/constants.js new file mode 100644 index 0000000..56d1a95 --- /dev/null +++ b/cmdb-ui/src/components/RegexSelect/constants.js @@ -0,0 +1,19 @@ +/* eslint-disable no-useless-escape */ + +import i18n from '@/lang' +export const regList = () => { + return [ + { id: 'letter', label: i18n.t('regexSelect.letter'), value: '^[A-Za-z]+$', message: '请输入字母' }, + { id: 'number', label: i18n.t('regexSelect.number'), value: '^-?(?!0\\d+)\\d+(\\.\\d+)?$', message: '请输入数字' }, + { id: 'letterAndNumber', label: i18n.t('regexSelect.letterAndNumber'), value: '^[A-Za-z0-9.]+$', message: '请输入字母和数字' }, + { id: 'phone', label: i18n.t('regexSelect.phone'), value: '^1[3-9]\\d{9}$', message: '请输入正确手机号码' }, + { id: 'landline', label: i18n.t('regexSelect.landline'), value: '^(?:(?:\\d{3}-)?\\d{8}|^(?:\\d{4}-)?\\d{7,8})(?:-\\d+)?$', message: '请输入正确座机' }, + { id: 'zipCode', label: i18n.t('regexSelect.zipCode'), value: '^(0[1-7]|1[0-356]|2[0-7]|3[0-6]|4[0-7]|5[1-7]|6[1-7]|7[0-5]|8[013-6])\\d{4}$', message: '请输入正确邮政编码' }, + { id: 'IDCard', label: i18n.t('regexSelect.IDCard'), value: '(^[1-9]\\d{5}(18|19|([23]\\d))\\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$)|(^[1-9]\\d{5}\\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\\d{3}$)', message: '请输入正确身份证号' }, + { id: 'ip', label: i18n.t('regexSelect.ip'), value: '^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$', message: '请输入正确IP地址' }, + { id: 'email', label: i18n.t('regexSelect.email'), value: '^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\.\\w+([-.]\\w+)*$', message: '请输入正确邮箱' }, + { id: 'link', label: i18n.t('regexSelect.link'), value: '^(https:\/\/www\.|http:\/\/www\.|https:\/\/|http:\/\/)?[a-zA-Z0-9]{2,}(\.[a-zA-Z0-9]{2,})(\.[a-zA-Z0-9]{2,})?$', message: '请输入链接' }, + { id: 'monetaryAmount', label: i18n.t('regexSelect.monetaryAmount'), value: '^-?\\d+(,\\d{3})*(\.\\d{1,2})?$', message: '请输入货币金额' }, + { id: 'custom', label: i18n.t('regexSelect.custom'), value: '', message: '' } + ] +} diff --git a/cmdb-ui/src/components/RegexSelect/index.js b/cmdb-ui/src/components/RegexSelect/index.js new file mode 100644 index 0000000..232ecb4 --- /dev/null +++ b/cmdb-ui/src/components/RegexSelect/index.js @@ -0,0 +1,2 @@ +import RegexSelect from './regexSelect.vue' +export default RegexSelect diff --git a/cmdb-ui/src/components/RegexSelect/regexSelect.vue b/cmdb-ui/src/components/RegexSelect/regexSelect.vue new file mode 100644 index 0000000..6831bd4 --- /dev/null +++ b/cmdb-ui/src/components/RegexSelect/regexSelect.vue @@ -0,0 +1,208 @@ + + + + + + + diff --git a/cmdb-ui/src/components/RoleTransfer/index.vue b/cmdb-ui/src/components/RoleTransfer/index.vue index d08bf62..93c2680 100644 --- a/cmdb-ui/src/components/RoleTransfer/index.vue +++ b/cmdb-ui/src/components/RoleTransfer/index.vue @@ -3,12 +3,12 @@
        - +
        {{ item.name }}
        diff --git a/cmdb-ui/src/components/SplitPane/SplitPane.vue b/cmdb-ui/src/components/SplitPane/SplitPane.vue index e0e6440..62a9a4e 100644 --- a/cmdb-ui/src/components/SplitPane/SplitPane.vue +++ b/cmdb-ui/src/components/SplitPane/SplitPane.vue @@ -1,179 +1,183 @@ - - - - - + + + + + diff --git a/cmdb-ui/src/components/SplitPane/index.js b/cmdb-ui/src/components/SplitPane/index.js index 6443dbd..aae5c29 100644 --- a/cmdb-ui/src/components/SplitPane/index.js +++ b/cmdb-ui/src/components/SplitPane/index.js @@ -1,2 +1,2 @@ -import SplitPane from './SplitPane' -export default SplitPane +import SplitPane from './SplitPane' +export default SplitPane diff --git a/cmdb-ui/src/components/SplitPane/index.less b/cmdb-ui/src/components/SplitPane/index.less index 4cd81c9..4bf5d7d 100644 --- a/cmdb-ui/src/components/SplitPane/index.less +++ b/cmdb-ui/src/components/SplitPane/index.less @@ -1,48 +1,48 @@ -.split-pane { - height: 100%; - display: flex; -} - -.split-pane .pane-two { - flex: 1; -} - -.split-pane .pane-trigger { - user-select: none; -} - -.split-pane.row .pane-one { - width: 20%; - height: 100%; - // overflow-y: auto; -} - -.split-pane.column .pane { - width: 100%; -} - -.split-pane.row .pane-trigger { - width: 8px; - height: 100%; - cursor: e-resize; - background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAPCAYAAADDNm69AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAAeSURBVBhXY/4PBMzMzA379u1rANFMDGhgGAswMAAAn6EH6K9ktYAAAAAASUVORK5CYII=') - 1px 50% no-repeat #f0f2f5; -} - -.split-pane .collapse-btn { - width: 25px; - height: 70px; - position: absolute; - right: 8px; - top: calc(50% - 35px); - background-color: #f0f2f5; - border-color: transparent; - border-radius: 8px 0px 0px 8px; - .anticon { - color: #7cb0fe; - } -} - -.split-pane .spliter-wrap { - position: relative; -} +.split-pane { + height: 100%; + display: flex; +} + +.split-pane .pane-two { + flex: 1; +} + +.split-pane .pane-trigger { + user-select: none; +} + +.split-pane.row .pane-one { + width: 20%; + height: 100%; + // overflow-y: auto; +} + +.split-pane.column .pane { + width: 100%; +} + +.split-pane.row .pane-trigger { + width: 8px; + height: 100%; + cursor: e-resize; + background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAPCAYAAADDNm69AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAAeSURBVBhXY/4PBMzMzA379u1rANFMDGhgGAswMAAAn6EH6K9ktYAAAAAASUVORK5CYII=') + 1px 50% no-repeat #f0f2f5; +} + +.split-pane .collapse-btn { + width: 25px; + height: 70px; + position: absolute; + right: 8px; + top: calc(50% - 35px); + background-color: #f0f2f5; + border-color: transparent; + border-radius: 8px 0px 0px 8px; + .anticon { + color: #7cb0fe; + } +} + +.split-pane .spliter-wrap { + position: relative; +} diff --git a/cmdb-ui/src/components/TwoColumnLayout/TwoColumnLayout.vue b/cmdb-ui/src/components/TwoColumnLayout/TwoColumnLayout.vue index 351e83e..44d888d 100644 --- a/cmdb-ui/src/components/TwoColumnLayout/TwoColumnLayout.vue +++ b/cmdb-ui/src/components/TwoColumnLayout/TwoColumnLayout.vue @@ -35,7 +35,7 @@ export default { }, triggerColor: { type: String, - default: '#F0F5FF', + default: '#f7f8fa', }, }, data() { @@ -52,22 +52,21 @@ export default { diff --git a/cmdb-ui/src/components/tools/TopMenu.vue b/cmdb-ui/src/components/tools/TopMenu.vue index 3a59c1f..f87a0a2 100644 --- a/cmdb-ui/src/components/tools/TopMenu.vue +++ b/cmdb-ui/src/components/tools/TopMenu.vue @@ -1,15 +1,11 @@