diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..6e51dee2 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.vue linguist-language=python diff --git a/.github/ISSUE_TEMPLATE/1bug.yaml b/.github/ISSUE_TEMPLATE/1bug.yaml new file mode 100644 index 00000000..b7e98c65 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/1bug.yaml @@ -0,0 +1,60 @@ +name: Bug Report +description: File a bug report +title: "[Bug]: " +labels: ["☢️ bug"] +assignees: + - Selina316 +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! + - type: input + id: contact + attributes: + label: Contact Details + description: How can we get in touch with you if we need more info? + placeholder: ex. email@example.com + validations: + required: false + - type: dropdown + id: aspects + attributes: + label: This bug is related to UI or API? + multiple: true + options: + - UI + - API + - type: textarea + id: happened + attributes: + label: What happened? + description: Also tell us, what did you expect to happen? + placeholder: Tell us what you see! + value: "A bug happened!" + validations: + required: true + - type: input + id: version + attributes: + label: Version + description: What version of our software are you running? + value: "newest" + validations: + required: true + - type: dropdown + id: browsers + attributes: + label: What browsers are you seeing the problem on? + multiple: true + options: + - Firefox + - Chrome + - Safari + - Microsoft Edge + - type: textarea + id: logs + attributes: + label: Relevant log output + description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. + render: shell diff --git a/.github/ISSUE_TEMPLATE/2feature.yaml b/.github/ISSUE_TEMPLATE/2feature.yaml new file mode 100644 index 00000000..e5ce3230 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/2feature.yaml @@ -0,0 +1,44 @@ +name: Feature wanted +description: A new feature would be good +title: "[Feature]: " +labels: ["✏️ feature"] +assignees: + - pycook +body: + - type: markdown + attributes: + value: | + Thank you for your feature suggestion; we will evaluate it carefully! + - type: input + id: contact + attributes: + label: Contact Details + description: How can we get in touch with you if we need more info? + placeholder: ex. email@example.com + validations: + required: false + - type: dropdown + id: aspects + attributes: + label: feature is related to UI or API aspects? + multiple: true + options: + - UI + - API + - type: textarea + id: feature + attributes: + label: What is your advice? + description: Also tell us, what did you expect to happen? + placeholder: Tell us what you want! + value: "everyone wants this feature!" + validations: + required: true + - type: input + id: version + attributes: + label: Version + description: What version of our software are you running? + value: "newest" + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/3consultation.yaml b/.github/ISSUE_TEMPLATE/3consultation.yaml new file mode 100644 index 00000000..1e85ad64 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/3consultation.yaml @@ -0,0 +1,36 @@ +name: Help wanted +description: I have a question +title: "[help wanted]: " +labels: ["help wanted"] +assignees: + - ivonGwy +body: + - type: markdown + attributes: + value: | + Please tell us what's you need! + - type: input + id: contact + attributes: + label: Contact Details + description: How can we get in touch with you if we need more info? + placeholder: ex. email@example.com + validations: + required: false + - type: textarea + id: question + attributes: + label: What is your question? + description: Also tell us, how can we help? + placeholder: Tell us what you need! + value: "i have a question!" + validations: + required: true + - type: input + id: version + attributes: + label: Version + description: What version of our software are you running? + value: "newest" + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/bug.yaml b/.github/ISSUE_TEMPLATE/bug.yaml new file mode 100644 index 00000000..c21d7d89 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yaml @@ -0,0 +1,60 @@ +name: Bug Report +description: File a bug report +title: "[Bug]: " +labels: ["bug"] +assignees: + - pycook +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! + - type: input + id: contact + attributes: + label: Contact Details + description: How can we get in touch with you if we need more info? + placeholder: ex. email@example.com + validations: + required: false + - type: dropdown + id: type + attributes: + label: bug is related to UI or API aspects? + multiple: true + options: + - UI + - API + - type: textarea + id: what-happened + attributes: + label: What happened? + description: Also tell us, what did you expect to happen? + placeholder: Tell us what you see! + value: "A bug happened!" + validations: + required: true + - type: textarea + id: version + attributes: + label: Version + description: What version of our software are you running? + default: 2.3.5 + validations: + required: true + - type: dropdown + id: browsers + attributes: + label: What browsers are you seeing the problem on? + multiple: true + options: + - Firefox + - Chrome + - Safari + - Microsoft Edge + - type: textarea + id: logs + attributes: + label: Relevant log output + description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. + render: shell diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..76847109 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,6 @@ +blank_issues_enabled: false +contact_links: + - name: veops official website + url: https://veops.cn/#hero + about: you can contact us here. + diff --git a/cmdb-api/README.md b/.github/ISSUE_TEMPLATE/consultation.yaml similarity index 100% rename from cmdb-api/README.md rename to .github/ISSUE_TEMPLATE/consultation.yaml diff --git a/.github/ISSUE_TEMPLATE/feature.yaml b/.github/ISSUE_TEMPLATE/feature.yaml new file mode 100644 index 00000000..4e830583 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.yaml @@ -0,0 +1,44 @@ +name: Feature wanted +description: A new feature would be good +title: "[Feature]: " +labels: ["feature"] +assignees: + - pycook +body: + - type: markdown + attributes: + value: | + Thank you for your feature suggestion; we will evaluate it carefully! + - type: input + id: contact + attributes: + label: Contact Details + description: How can we get in touch with you if we need more info? + placeholder: ex. email@example.com + validations: + required: false + - type: dropdown + id: type + attributes: + label: feature is related to UI or API aspects? + multiple: true + options: + - UI + - API + - type: textarea + id: describe the feature + attributes: + label: What is your advice? + description: Also tell us, what did you expect to happen? + placeholder: Tell us what you want! + value: "everyone wants this feature!" + validations: + required: true + - type: textarea + id: version + attributes: + label: Version + description: What version of our software are you running? + default: 2.3.5 + validations: + required: true diff --git a/.github/config.yml b/.github/config.yml new file mode 100644 index 00000000..e69de29b diff --git a/.gitignore b/.gitignore index a53b01e6..e43370de 100755 --- a/.gitignore +++ b/.gitignore @@ -39,9 +39,12 @@ pip-log.txt nosetests.xml .pytest_cache cmdb-api/test-output +cmdb-api/api/uploaded_files +cmdb-api/migrations/versions # Translations -*.mo +#*.mo +messages.pot # Mr Developer .mr.developer.cfg @@ -68,6 +71,7 @@ settings.py # UI cmdb-ui/node_modules cmdb-ui/dist +cmdb-ui/yarn.lock # Log files cmdb-ui/npm-debug.log* diff --git a/Makefile b/Makefile index 99b1122e..8a597427 100644 --- a/Makefile +++ b/Makefile @@ -1,37 +1,52 @@ -.PHONY: env clean api ui worker - -help: - @echo " env create a development environment using pipenv" - @echo " deps install dependencies using pip" - @echo " clean remove unwanted files like .pyc's" - @echo " lint check style with flake8" - @echo " api start api server" - @echo " ui start ui server" - @echo " worker start async tasks worker" - -env: +MYSQL_ROOT_PASSWORD ?= root +MYSQL_PORT ?= 3306 +REDIS_PORT ?= 6379 + +default: help +help: ## display this help + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z0-9_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) +.PHONY: help + +env: ## create a development environment using pipenv sudo easy_install pip && \ - pip install pipenv -i https://pypi.douban.com/simple && \ + pip install pipenv -i https://repo.huaweicloud.com/repository/pypi/simple && \ npm install yarn && \ make deps +.PHONY: env + +docker-mysql: ## deploy MySQL use docker + @docker run --name mysql -p ${MYSQL_PORT}:3306 -e MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} -d mysql:latest +.PHONY: docker-mysql + +docker-redis: ## deploy Redis use docker + @docker run --name redis -p ${REDIS_PORT}:6379 -d redis:latest +.PHONY: docker-redis -deps: +deps: ## install dependencies using pip + cd cmdb-api && \ pipenv install --dev && \ pipenv run flask db-setup && \ pipenv run flask cmdb-init-cache && \ + cd .. && \ cd cmdb-ui && yarn install && cd .. +.PHONY: deps -api: +api: ## start api server cd cmdb-api && pipenv run flask run -h 0.0.0.0 +.PHONY: api -worker: - cd cmdb-api && pipenv run celery worker -A celery_worker.celery -E -Q one_cmdb_async --concurrency=1 -D && pipenv run celery worker -A celery_worker.celery -E -Q acl_async --concurrency=1 -D +worker: ## start async tasks worker + cd cmdb-api && pipenv run celery -A celery_worker.celery worker -E -Q one_cmdb_async --autoscale=5,2 --logfile=one_cmdb_async.log -D && pipenv run celery -A celery_worker.celery worker -E -Q acl_async --autoscale=2,1 --logfile=one_acl_async.log -D +.PHONY: worker -ui: +ui: ## start ui server cd cmdb-ui && yarn run serve +.PHONY: ui -clean: +clean: ## remove unwanted files like .pyc's pipenv run flask clean +.PHONY: clean -lint: +lint: ## check style with flake8 flake8 --exclude=env . +.PHONY: lint diff --git a/README.md b/README.md index d85e02bb..d1af9354 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,21 @@ -![基础资源视图](docs/logo.png) -[![License](https://img.shields.io/badge/License-AGPLv3-brightgreen)](https://github.com/veops/cmdb/blob/master/LICENSE) -[![UI](https://img.shields.io/badge/UI-Ant%20Design%20Pro%20Vue-brightgreen)](https://github.com/sendya/ant-design-pro-vue) -[![API](https://img.shields.io/badge/API-Flask-brightgreen)](https://github.com/pallets/flask) +

+ 维易CMDB +

+

简单、轻量、通用的运维配置管理数据库

+

+ License: GPLv3 + UI + API +

-[English](README_en.md) / [中文](README.md) -- 在线体验: CMDB - - username: demo +------------------------------ + +[English](docs/README_en.md) / [中文](README.md) +- 产品文档:https://veops.cn/docs/ +- 在线体验:CMDB + - username: demo 或者 admin - password: 123456 > **重要提示**: `master` 分支在开发过程中可能处于 _不稳定的状态_ 。 @@ -15,45 +23,42 @@ ## 系统介绍 -### 整体架构 +### 系统概览 + + - +[查看更多展示](docs/screenshot.md) -### 相关文档 +### 相关文章 -- 设计文档 +- 概要设计 - API 文档 -- 树形视图实践 +- 自动发现 +- 更多文章可以在公众号 **维易科技OneOps** 里查看 ### 特点 - 灵活性 - 1. 规范并统一纳管复杂数据资产 - 2. 自动发现、入库 IT 资产 + 1. 配置灵活,不设定任何运维场景,有内置模板 + 2. 自动发现、入库 IT 资产 - 安全性 - 1. 细粒度访问控制 + 1. 细粒度权限控制 2. 完备操作日志 - 多应用 1. 丰富视图展示维度 - 2. 提供 Restful API - 3. 自定义字段触发器 + 2. API简单强大 + 3. 支持定义属性触发器、计算属性 ### 主要功能 - 模型属性支持索引、多值、默认排序、字体颜色,支持计算属性 - 支持自动发现、定时巡检、文件导入 -- 支持资源、树形、关系视图展示 +- 支持资源、层级、关系视图展示 - 支持模型间关系配置和展示 - 细粒度访问控制,完备的操作日志 - 支持跨模型搜索 -### 系统概览 -- 服务树 - -![1](docs/0.png "首页展示") - -[查看更多展示](docs/screenshot.md) @@ -63,18 +68,41 @@ ## 接入公司 -> 欢迎使用CMDB的公司,在 [#112](https://github.com/veops/cmdb/issues/112) 登记 +> 欢迎使用开源CMDB的公司,在 [#112](https://github.com/veops/cmdb/issues/112) 登记 ## 安装 -### [Docker 一键快速构建](docs/docker.md) - -### [本地搭建](docs/local.md) +### Docker 一键快速构建 +> 方法一 +- 第一步: 先安装 docker 环境, 以及docker-compose +- 第二步: 拷贝项目 +```shell +git clone https://github.com/veops/cmdb.git +``` +- 第三步:进入主目录,执行: +``` +docker-compose up -d +``` +> 方法二, 该方法适用于linux系统 +- 第一步: 先安装 docker 环境, 以及docker-compose +- 第二步: 直接使用项目根目录下的install.sh 文件进行 `安装`、`启动`、`暂停`、`查状态`、`删除`、`卸载` +```shell +curl -so install.sh https://raw.githubusercontent.com/veops/cmdb/master/install.sh +sh install.sh install +``` + +### [本地开发环境搭建](docs/local.md) ### [Makefile 安装](docs/makefile.md) +## 验证 +- 浏览器打开: [http://127.0.0.1:8000](http://127.0.0.1:8000) +- username: demo 或者 admin +- password: 123456 + + --- -_**欢迎关注我们的公众号,点击联系我们,加入微信、qq运维群,获得更多产品、行业相关资讯**_ +_**欢迎关注公众号(维易科技OneOps),关注后可加入微信群,进行产品和技术交流。**_ -![公众号](docs/qrcode_for_gzh.jpg) +![公众号: 维易科技OneOps](docs/images/wechat.png) diff --git a/README_en.md b/README_en.md deleted file mode 100644 index 1cb32e83..00000000 --- a/README_en.md +++ /dev/null @@ -1,82 +0,0 @@ -![基础资源视图](docs/logo.png) - -[![License](https://img.shields.io/badge/License-AGPLv3-brightgreen)](https://github.com/veops/cmdb/blob/master/LICENSE) -[![UI](https://img.shields.io/badge/UI-Ant%20Design%20Pro%20Vue-brightgreen)](https://github.com/sendya/ant-design-pro-vue) -[![API](https://img.shields.io/badge/API-Flask-brightgreen)](https://github.com/pallets/flask) - -[English](README_en.md) / [中文](README.md) - -## DEMO ONLINE - -- Preview online: CMDB - - username: demo - - password: 123456 - -> **ATTENTION**: branch `master` may be unstable as the result of continued development, please pull code from [releases](https://github.com/veops/cmdb/releases) - -## Overview - -### Technical Architecture - - - -### Document - -- Design Document -- API Documentation -- Practice of Tree View - -### Features - -- Flexibility - 1. Standardize and manage complex data assets - 2. Automatically discover and inventory IT assets -- Security - 1. Fine-grained access control - 2. Comprehensive operation logs -- Multi-application - 1. Rich view display dimensions - 2. Provide Restful API - 3. Custom field triggers - -### Main Features - -- Model attributes support indexing, multiple values, default sorting, font color, and computed properties. -- Support automatic discovery, scheduled inspections, and file import. -- Support resource, tree view, and relationship view display. -- Support configuration and display of relationships between models. -- Fine-grained access control and comprehensive operation logs. -- Support cross-model search. - -### System Overview - -- Service Tree - ![1](docs/0.png "首页展示") - -[View more screenshots](docs/screenshot.md) - -### More Features - -> Welcome to visit VeOps official website to discover more free operations and maintenance systems. - -## Installation - -### [One-Click Docker Quick Build](docs/docker_en.md) - -### [Local Setup](docs/local_en.md) - -### [Installation with Makefile](docs/makefile_en.md) - -## Contributing - -1. Fork it -1. Create your feature branch (`git checkout -b my-feature`) -1. Commit your changes (`git commit -am 'Add some feature'`) -1. Push to the branch (`git push origin my-feature`) -1. Create new Pull Request - ---- - -_**Welcome to join us through QQ group(336164978)**_ - -![QQgroup](docs/qr_code.jpg) diff --git a/cmdb-api/Makefile b/cmdb-api/Makefile deleted file mode 100644 index a289b86d..00000000 --- a/cmdb-api/Makefile +++ /dev/null @@ -1,16 +0,0 @@ -default: help - -test: ## test in local environment - pytest -s --html=test-output/test/index.html --cov-report html:test-output/coverage --cov=api tests - -clean_test: ## clean test output - rm -f .coverage - rm -rf .pytest_cache - rm -rf test-output - - -docker_test: ## test all case in docker container - @echo "TODO" - -help: - @grep -E '^[a-zA-Z_-]+:.*?## .*$$' ./Makefile | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' diff --git a/cmdb-api/Pipfile b/cmdb-api/Pipfile index 3402e75f..b0b8c69b 100644 --- a/cmdb-api/Pipfile +++ b/cmdb-api/Pipfile @@ -5,60 +5,66 @@ name = "pypi" [packages] # Flask -Flask = "==1.0.3" -Werkzeug = "==0.15.5" +Flask = "==2.3.2" +Werkzeug = ">=2.3.6" click = ">=5.0" # Api -Flask-RESTful = "==0.3.7" +Flask-RESTful = "==0.3.10" # Database -Flask-SQLAlchemy = "==2.4.0" -SQLAlchemy = "==1.3.5" -PyMySQL = "==0.9.3" -redis = "==3.2.1" +Flask-SQLAlchemy = "==2.5.0" +SQLAlchemy = "==1.4.49" +PyMySQL = "==1.1.0" +redis = "==4.6.0" # Migrations Flask-Migrate = "==2.5.2" # Deployment -gunicorn = "==19.5.0" +gunicorn = "==21.0.1" supervisor = "==4.0.3" # Auth -Flask-Login = "==0.4.1" -Flask-Bcrypt = "==0.7.1" +Flask-Login = ">=0.6.2" +Flask-Bcrypt = "==1.0.1" Flask-Cors = ">=3.0.8" -python-ldap = "==3.2.0" -pycryptodome = "==3.12.0" +ldap3 = "==2.9.1" +pycryptodome = "==3.19.1" +cryptography = ">=41.0.2" +# i18n +flask-babel = "==4.0.0" # Caching Flask-Caching = ">=1.0.0" # Environment variable parsing environs = "==4.2.0" marshmallow = "==2.20.2" # async tasks -celery = "==4.3.0" +celery = ">=5.3.1" celery_once = "==3.0.1" more-itertools = "==5.0.0" -kombu = "==4.4.0" +kombu = ">=5.3.1" # common setting -Flask-APScheduler = "==1.12.4" timeout-decorator = "==0.5.0" -numpy = "==1.18.5" -pandas = "==1.3.2" WTForms = "==3.0.0" email-validator = "==1.3.1" treelib = "==1.6.1" flasgger = "==0.9.5" -Pillow = "==8.3.2" +Pillow = ">=10.0.1" # other -six = "==1.12.0" +six = "==1.16.0" bs4 = ">=0.0.1" toposort = ">=1.5" requests = ">=2.22.0" +requests_oauthlib = "==1.3.1" +markdownify = "==0.11.6" PyJWT = "==2.4.0" elasticsearch = "==7.17.9" -future = "==0.18.2" -itsdangerous = "==2.0.1" -Jinja2 = "==3.0.1" +future = "==0.18.3" +itsdangerous = "==2.1.2" +Jinja2 = "==3.1.2" jinja2schema = "==0.1.4" msgpack-python = "==0.5.6" alembic = "==1.7.7" +hvac = "==2.0.0" +colorama = ">=0.4.6" +pycryptodomex = ">=3.19.0" +lz4 = ">=4.3.2" [dev-packages] # Testing @@ -75,4 +81,3 @@ flake8-isort = "==2.7.0" isort = "==4.3.21" pep8-naming = "==0.8.2" pydocstyle = "==3.0.0" - diff --git a/cmdb-api/api/app.py b/cmdb-api/api/app.py index be828fe8..b6857ba8 100644 --- a/cmdb-api/api/app.py +++ b/cmdb-api/api/app.py @@ -7,31 +7,28 @@ import sys from inspect import getmembers from logging.handlers import RotatingFileHandler +from pathlib import Path from flask import Flask -from flask import make_response, jsonify +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 import JSONEncoder +from flask.json.provider import DefaultJSONProvider +from flask_babel.speaklater import LazyString import api.views.entry -from api.extensions import ( - bcrypt, - cors, - cache, - db, - login_manager, - migrate, - celery, - rd, - es, -) -from api.flask_cas import CAS +from api.extensions import (bcrypt, babel, cache, celery, cors, db, es, login_manager, migrate, rd) +from api.extensions import inner_secrets +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 HERE = os.path.abspath(os.path.dirname(__file__)) PROJECT_ROOT = os.path.join(HERE, os.pardir) -API_PACKAGE = "api" +BASE_DIR = Path(__file__).resolve().parent.parent @login_manager.user_loader @@ -75,9 +72,9 @@ def __call__(self, environ, start_response): return self.app(environ, start_response) -class MyJSONEncoder(JSONEncoder): +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): @@ -86,15 +83,6 @@ def default(self, o): return o -def create_acl_app(config_object="settings"): - app = Flask(__name__.split(".")[0]) - app.config.from_object(config_object) - - register_extensions(app) - - return app - - def create_app(config_object="settings"): """Create application factory, as explained here: http://flask.pocoo.org/docs/patterns/appfactories/. @@ -103,7 +91,7 @@ def create_app(config_object="settings"): app = Flask(__name__.split(".")[0]) app.config.from_object(config_object) - app.json_encoder = MyJSONEncoder + app.json = MyJSONEncoder(app) configure_logger(app) register_extensions(app) register_blueprints(app) @@ -111,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) @@ -130,17 +119,29 @@ 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) login_manager.init_app(app) - migrate.init_app(app, db) + migrate.init_app(app, db, directory=f"{BASE_DIR}/migrations") rd.init_app(app) if app.config.get('USE_ES'): es.init_app(app) + + app.config.update(app.config.get("CELERY")) celery.conf.update(app.config) + if app.config.get('SECRETS_ENGINE') == 'inner': + with app.app_context(): + inner_secrets.init_app(app, InnerKVManger()) + def register_blueprints(app): for item in getmembers(api.views.entry): @@ -158,10 +159,8 @@ def render_error(error): error_code = getattr(error, "code", 500) if not str(error_code).isdigit(): error_code = 400 - if error_code != 500: - return make_response(jsonify(message=str(error)), error_code) - else: - return make_response(jsonify(message=traceback.format_exc(-1)), error_code) + + return make_response(jsonify(message=str(error)), error_code) for errcode in app.config.get("ERROR_CODES") or [400, 401, 403, 404, 405, 500, 502]: app.errorhandler(errcode)(render_error) @@ -184,9 +183,8 @@ def register_commands(app): for root, _, files in os.walk(os.path.join(HERE, "commands")): for filename in files: if not filename.startswith("_") and filename.endswith("py"): - module_path = os.path.join(API_PACKAGE, root[root.index("commands"):]) - if module_path not in sys.path: - sys.path.insert(1, module_path) + if root not in sys.path: + sys.path.insert(1, root) command = __import__(os.path.splitext(filename)[0]) func_list = [o[0] for o in getmembers(command) if isinstance(o[1], click.core.Command)] for func_name in func_list: @@ -204,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 d35c6a36..395e06c3 100644 --- a/cmdb-api/api/commands/click_acl.py +++ b/cmdb-api/api/commands/click_acl.py @@ -1,10 +1,15 @@ import click from flask.cli import with_appcontext +from api.lib.perm.acl.user import UserCRUD + @click.command() @with_appcontext def init_acl(): + """ + acl init + """ from api.models.acl import Role from api.models.acl import App from api.tasks.acl import role_rebuild @@ -20,50 +25,32 @@ def init_acl(): role_rebuild.apply_async(args=(role.id, app.id), queue=ACL_QUEUE) -# @click.command() -# @with_appcontext -# def acl_clean(): -# from api.models.acl import Resource -# from api.models.acl import Permission -# from api.models.acl import RolePermission -# -# perms = RolePermission.get_by(to_dict=False) -# -# for r in perms: -# perm = Permission.get_by_id(r.perm_id) -# if perm and perm.app_id != r.app_id: -# resource_id = r.resource_id -# resource = Resource.get_by_id(resource_id) -# perm_name = perm.name -# existed = Permission.get_by(resource_type_id=resource.resource_type_id, name=perm_name, first=True, -# to_dict=False) -# if existed is not None: -# other = RolePermission.get_by(rid=r.rid, perm_id=existed.id, resource_id=resource_id) -# if not other: -# r.update(perm_id=existed.id) -# else: -# r.soft_delete() -# else: -# r.soft_delete() -# -# -# @click.command() -# @with_appcontext -# def acl_has_resource_role(): -# from api.models.acl import Role -# from api.models.acl import App -# from api.lib.perm.acl.cache import HasResourceRoleCache -# from api.lib.perm.acl.role import RoleCRUD -# -# roles = Role.get_by(to_dict=False) -# apps = App.get_by(to_dict=False) -# for role in roles: -# if role.app_id: -# res = RoleCRUD.recursive_resources(role.id, role.app_id) -# if res.get('resources') or res.get('groups'): -# HasResourceRoleCache.add(role.id, role.app_id) -# else: -# for app in apps: -# res = RoleCRUD.recursive_resources(role.id, app.id) -# if res.get('resources') or res.get('groups'): -# HasResourceRoleCache.add(role.id, app.id) +@click.command() +@with_appcontext +def add_user(): + """ + create a user + + is_admin: default is False + + """ + + 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('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 735464d8..e7ad1ea8 100644 --- a/cmdb-api/api/commands/click_cmdb.py +++ b/cmdb-api/api/commands/click_cmdb.py @@ -5,30 +5,38 @@ import datetime import json import time +import uuid import click +import requests from flask import current_app from flask.cli import with_appcontext +from flask_login import login_user import api.lib.cmdb.ci from api.extensions import db from api.extensions import rd -from api.lib.cmdb.ci_type import CITypeTriggerManager +from api.lib.cmdb.cache import AttributeCache from api.lib.cmdb.const import PermEnum from api.lib.cmdb.const import REDIS_PREFIX_CI from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION +from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION2 from api.lib.cmdb.const import ResourceTypeEnum from api.lib.cmdb.const import RoleEnum from api.lib.cmdb.const import ValueTypeEnum from api.lib.exception import AbortException from api.lib.perm.acl.acl import ACLManager +from api.lib.perm.acl.acl import UserCache from api.lib.perm.acl.cache import AppCache from api.lib.perm.acl.resource import ResourceCRUD from api.lib.perm.acl.resource import ResourceTypeCRUD from api.lib.perm.acl.role import RoleCRUD -from api.lib.perm.acl.user import UserCRUD +from api.lib.secrets.inner import KeyManage +from api.lib.secrets.inner import global_key_threshold +from api.lib.secrets.secrets import InnerKVManger from api.models.acl import App from api.models.acl import ResourceType +from api.models.cmdb import Attribute from api.models.cmdb import CI from api.models.cmdb import CIRelation from api.models.cmdb import CIType @@ -43,13 +51,19 @@ def cmdb_init_cache(): ci_relations = CIRelation.get_by(to_dict=False) relations = dict() + relations2 = dict() for cr in ci_relations: relations.setdefault(cr.first_ci_id, {}).update({cr.second_ci_id: cr.second_ci.type_id}) + if cr.ancestor_ids: + relations2.setdefault(cr.ancestor_ids, {}).update({cr.second_ci_id: cr.second_ci.type_id}) for i in relations: relations[i] = json.dumps(relations[i]) if relations: rd.create_or_update(relations, REDIS_PREFIX_CI_RELATION) + if relations2: + rd.create_or_update(relations2, REDIS_PREFIX_CI_RELATION2) + es = None if current_app.config.get("USE_ES"): from api.extensions import es from api.models.cmdb import Attribute @@ -104,7 +118,15 @@ def cmdb_init_acl(): # 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 @@ -120,10 +142,10 @@ def cmdb_init_acl(): # 3. add resource and grant ci_types = CIType.get_by(to_dict=False) - type_id = ResourceType.get_by(name=ResourceTypeEnum.CI, first=True, to_dict=False).id + resource_type_id = ResourceType.get_by(name=ResourceTypeEnum.CI, first=True, to_dict=False).id for ci_type in ci_types: try: - ResourceCRUD.add(ci_type.name, type_id, app_id) + ResourceCRUD.add(ci_type.name, resource_type_id, app_id) except AbortException: pass @@ -133,10 +155,10 @@ def cmdb_init_acl(): [PermEnum.READ]) relation_views = PreferenceRelationView.get_by(to_dict=False) - type_id = ResourceType.get_by(name=ResourceTypeEnum.RELATION_VIEW, first=True, to_dict=False).id + resource_type_id = ResourceType.get_by(name=ResourceTypeEnum.RELATION_VIEW, first=True, to_dict=False).id for view in relation_views: try: - ResourceCRUD.add(view.name, type_id, app_id) + ResourceCRUD.add(view.name, resource_type_id, app_id) except AbortException: pass @@ -147,61 +169,20 @@ def cmdb_init_acl(): @click.command() -@click.option( - '-u', - '--user', - help='username' -) -@click.option( - '-p', - '--password', - help='password' -) -@click.option( - '-m', - '--mail', - help='mail' -) @with_appcontext -def add_user(user, password, mail): - """ - create a user - - is_admin: default is False - - Example: flask add-user -u -p -m - """ - assert user is not None - assert password is not None - assert mail is not None - UserCRUD.add(username=user, password=password, email=mail) - - -@click.command() -@click.option( - '-u', - '--user', - help='username' -) -@with_appcontext -def del_user(user): +def cmdb_counter(): """ - delete a user - - Example: flask del-user -u + Dashboard calculations """ - assert user is not None - from api.models.acl import User + from api.lib.cmdb.cache import CMDBCounterCache - u = User.get_by(username=user, first=True, to_dict=False) - u and UserCRUD.delete(u.uid) + 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') -@click.command() -@with_appcontext -def cmdb_counter(): - from api.lib.cmdb.cache import CMDBCounterCache - + login_user(UserCache.get('worker')) while True: try: db.session.remove() @@ -217,45 +198,283 @@ def cmdb_counter(): @click.command() @with_appcontext def cmdb_trigger(): + """ + Trigger execution for date attribute + """ + from api.lib.cmdb.ci import CITriggerManager + current_day = datetime.datetime.today().strftime("%Y-%m-%d") trigger2cis = dict() trigger2completed = dict() i = 0 while True: - db.session.remove() - if datetime.datetime.today().strftime("%Y-%m-%d") != current_day: - trigger2cis = dict() - trigger2completed = dict() - current_day = datetime.datetime.today().strftime("%Y-%m-%d") - - if i == 360 or i == 0: - i = 0 - try: - triggers = CITypeTrigger.get_by(to_dict=False) + try: + db.session.remove() + if datetime.datetime.today().strftime("%Y-%m-%d") != current_day: + trigger2cis = dict() + trigger2completed = dict() + current_day = datetime.datetime.today().strftime("%Y-%m-%d") + + if i == 3 or i == 0: + i = 0 + triggers = CITypeTrigger.get_by(to_dict=False, __func_isnot__key_attr_id=None) for trigger in triggers: - ready_cis = CITypeTriggerManager.waiting_cis(trigger) + try: + ready_cis = CITriggerManager.waiting_cis(trigger) + except Exception as e: + print(e) + continue + if trigger.id not in trigger2cis: trigger2cis[trigger.id] = (trigger, ready_cis) else: cur = trigger2cis[trigger.id] cur_ci_ids = {i.ci_id for i in cur[1]} - trigger2cis[trigger.id] = (trigger, cur[1] + [i for i in ready_cis if i.ci_id not in cur_ci_ids - and i.ci_id not in trigger2completed[trigger.id]]) + trigger2cis[trigger.id] = ( + trigger, cur[1] + [i for i in ready_cis if i.ci_id not in cur_ci_ids + and i.ci_id not in trigger2completed.get(trigger.id, {})]) + + for tid in trigger2cis: + trigger, cis = trigger2cis[tid] + for ci in copy.deepcopy(cis): + if CITriggerManager.trigger_notify(trigger, ci): + trigger2completed.setdefault(trigger.id, set()).add(ci.ci_id) + + for _ci in cis: + if _ci.ci_id == ci.ci_id: + cis.remove(_ci) + + i += 1 + time.sleep(10) + except Exception as e: + import traceback + print(traceback.format_exc()) + current_app.logger.error("cmdb trigger exception: {}".format(e)) + time.sleep(60) - except Exception as e: - print(e) - for tid in trigger2cis: - trigger, cis = trigger2cis[tid] - for ci in copy.deepcopy(cis): - if CITypeTriggerManager.trigger_notify(trigger, ci): - trigger2completed.setdefault(trigger.id, set()).add(ci.ci_id) +@click.command() +@with_appcontext +def cmdb_index_table_upgrade(): + """ + Migrate data from tables c_value_integers, c_value_floats, and c_value_datetime + """ + for attr in Attribute.get_by(to_dict=False): + if attr.value_type not in {ValueTypeEnum.TEXT, ValueTypeEnum.JSON} and not attr.is_index: + attr.update(is_index=True) + AttributeCache.clean(attr) + + from api.models.cmdb import CIValueInteger, CIIndexValueInteger + from api.models.cmdb import CIValueFloat, CIIndexValueFloat + from api.models.cmdb import CIValueDateTime, CIIndexValueDateTime + + for i in CIValueInteger.get_by(to_dict=False): + CIIndexValueInteger.create(ci_id=i.ci_id, attr_id=i.attr_id, value=i.value, commit=False) + i.delete(commit=False) + db.session.commit() + + for i in CIValueFloat.get_by(to_dict=False): + CIIndexValueFloat.create(ci_id=i.ci_id, attr_id=i.attr_id, value=i.value, commit=False) + i.delete(commit=False) + db.session.commit() + + for i in CIValueDateTime.get_by(to_dict=False): + CIIndexValueDateTime.create(ci_id=i.ci_id, attr_id=i.attr_id, value=i.value, commit=False) + i.delete(commit=False) + db.session.commit() + + +def valid_address(address): + if not address: + return False + + if not address.startswith(("http://127.0.0.1", "https://127.0.0.1")): + response = { + "message": "Address should start with http://127.0.0.1 or https://127.0.0.1", + "status": "failed" + } + KeyManage.print_response(response) + return False + return True + + +@click.command() +@click.option( + '-a', + '--address', + help='inner cmdb api, http://127.0.0.1:8000', +) +@with_appcontext +def cmdb_inner_secrets_init(address): + """ + init inner secrets for password feature + """ + res, ok = KeyManage(backend=InnerKVManger).init() + if not ok: + if res.get("status") == "failed": + KeyManage.print_response(res) + return + + token = res.get("details", {}).get("root_token", "") + if valid_address(address): + token = current_app.config.get("INNER_TRIGGER_TOKEN", "") if not token else token + if not token: + token = click.prompt(f'Enter root token', hide_input=True, confirmation_prompt=False) + assert token is not None + resp = requests.post("{}/api/v0.1/secrets/auto_seal".format(address.strip("/")), + headers={"Inner-Token": token}) + if resp.status_code == 200: + KeyManage.print_response(resp.json()) + else: + KeyManage.print_response({"message": resp.text or resp.status_code, "status": "failed"}) + else: + KeyManage.print_response(res) + + +@click.command() +@click.option( + '-a', + '--address', + help='inner cmdb api, http://127.0.0.1:8000', + required=True, +) +@with_appcontext +def cmdb_inner_secrets_unseal(address): + """ + unseal the secrets feature + """ + if not valid_address(address): + return + address = "{}/api/v0.1/secrets/unseal".format(address.strip("/")) + for i in range(global_key_threshold): + token = click.prompt(f'Enter unseal token {i + 1}', hide_input=True, confirmation_prompt=False) + assert token is not None + resp = requests.post(address, headers={"Unseal-Token": token}) + if resp.status_code == 200: + KeyManage.print_response(resp.json()) + if resp.json().get("status") in ["success", "skip"]: + return + else: + KeyManage.print_response({"message": resp.status_code, "status": "failed"}) + return + - for _ci in cis: - if _ci.ci_id == ci.ci_id: - cis.remove(_ci) +@click.command() +@click.option( + '-a', + '--address', + help='inner cmdb api, http://127.0.0.1:8000', + required=True, +) +@click.option( + '-k', + '--token', + help='root token', + prompt=True, + hide_input=True, +) +@with_appcontext +def cmdb_inner_secrets_seal(address, token): + """ + seal the secrets feature + """ + assert address is not None + assert token is not None + if not valid_address(address): + return + address = "{}/api/v0.1/secrets/seal".format(address.strip("/")) + resp = requests.post(address, headers={ + "Inner-Token": token, + }) + if resp.status_code == 200: + KeyManage.print_response(resp.json()) + else: + KeyManage.print_response({"message": resp.status_code, "status": "failed"}) + + +@click.command() +@with_appcontext +def cmdb_password_data_migrate(): + """ + Migrate CI password data, version >= v2.3.6 + """ + from api.models.cmdb import CIIndexValueText + from api.models.cmdb import CIValueText + from api.lib.secrets.inner import InnerCrypt + from api.lib.secrets.vault import VaultClient + + attrs = Attribute.get_by(to_dict=False) + for attr in attrs: + if attr.is_password: + + value_table = CIIndexValueText if attr.is_index else CIValueText + + failed = False + for i in value_table.get_by(attr_id=attr.id, to_dict=False): + if current_app.config.get("SECRETS_ENGINE", 'inner') == 'inner': + _, status = InnerCrypt().decrypt(i.value) + if status: + continue + + encrypt_value, status = InnerCrypt().encrypt(i.value) + if status: + CIValueText.create(ci_id=i.ci_id, attr_id=attr.id, value=encrypt_value) + else: + failed = True + continue + elif current_app.config.get("SECRETS_ENGINE") == 'vault': + if i.value == '******': + continue + + vault = VaultClient(current_app.config.get('VAULT_URL'), current_app.config.get('VAULT_TOKEN')) + try: + vault.update("/{}/{}".format(i.ci_id, i.attr_id), dict(v=i.value)) + except Exception as e: + print('save password to vault failed: {}'.format(e)) + failed = True + continue + else: + continue + + i.delete() + + if not failed and attr.is_index: + attr.update(is_index=False) + + +@click.command() +@with_appcontext +def cmdb_agent_init(): + """ + Initialize the agent's permissions and obtain the key and secret + """ + + from api.models.acl import User + + user = User.get_by(username="cmdb_agent", first=True, to_dict=False) + if user is None: + click.echo( + click.style('user cmdb_agent does not exist, please use flask add-user to create it first', fg='red')) + return + + # grant + _app = AppCache.get('cmdb') or App.create(name='cmdb') + app_id = _app.id + + ci_types = CIType.get_by(to_dict=False) + resource_type_id = ResourceType.get_by(name=ResourceTypeEnum.CI, first=True, to_dict=False).id + for ci_type in ci_types: + try: + ResourceCRUD.add(ci_type.name, resource_type_id, app_id) + except AbortException: + pass + + ACLManager().grant_resource_to_role(ci_type.name, + "cmdb_agent", + ResourceTypeEnum.CI, + [PermEnum.READ, PermEnum.UPDATE, PermEnum.ADD, PermEnum.DELETE]) - i += 1 - time.sleep(10) + click.echo("Key : {}".format(click.style(user.key, bg='red'))) + click.echo("Secret: {}".format(click.style(user.secret, bg='red'))) diff --git a/cmdb-api/api/commands/click_common_setting.py b/cmdb-api/api/commands/click_common_setting.py new file mode 100644 index 00000000..ddd370bc --- /dev/null +++ b/cmdb-api/api/commands/click_common_setting.py @@ -0,0 +1,318 @@ +import click +from flask import current_app +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.resp_format import ErrFormat +from api.models.common_setting import Employee, Department + + +class InitEmployee(object): + + def __init__(self): + self.log = current_app.logger + + def import_user_from_acl(self): + """ + Import users from ACL + """ + + InitDepartment().init() + acl = ACLManager('acl') + user_list = acl.get_all_users() + + username_list = [e['username'] for e in Employee.get_by()] + + for user in user_list: + acl_uid = user['uid'] + block = 1 if user['block'] else 0 + acl_rid = self.get_rid_by_uid(acl_uid) + if user['username'] in username_list: + existed = Employee.get_by(first=True, username=user['username'], to_dict=False) + if existed: + existed.update( + acl_uid=acl_uid, + acl_rid=acl_rid, + block=block, + ) + continue + try: + form = EmployeeAddForm(MultiDict(user)) + if not form.validate(): + raise Exception( + ','.join(['{}: {}'.format(filed, ','.join(msg)) for filed, msg in form.errors.items()])) + data = form.data + data['acl_uid'] = acl_uid + data['acl_rid'] = acl_rid + data['block'] = block + data.pop('password') + Employee.create( + **data + ) + except Exception as e: + self.log.error(ErrFormat.acl_import_user_failed.format(user['username'], str(e))) + self.log.error(e) + + @staticmethod + def get_rid_by_uid(uid): + from api.models.acl import Role + role = Role.get_by(first=True, uid=uid) + return role['id'] if role is not None else 0 + + +class InitDepartment(object): + def __init__(self): + self.log = current_app.logger + + def init(self): + self.init_wide_company() + + @staticmethod + def hard_delete(department_id, department_name): + existed_deleted_list = Department.query.filter( + Department.department_name == department_name, + Department.department_id == department_id, + Department.deleted == 1, + ).all() + for existed in existed_deleted_list: + existed.delete() + + @staticmethod + def get_department(department_name): + return Department.query.filter( + Department.department_name == department_name, + Department.deleted == 0, + ).first() + + def run(self, department_id, department_name, department_parent_id): + self.hard_delete(department_id, department_name) + + res = self.get_department(department_name) + if res: + if res.department_id == department_id: + return + else: + res.update( + department_id=department_id, + department_parent_id=department_parent_id, + ) + return + + Department.create( + department_id=department_id, + department_name=department_name, + department_parent_id=department_parent_id, + ) + new_d = self.get_department(department_name) + + if new_d.department_id != department_id: + new_d.update( + department_id=department_id, + department_parent_id=department_parent_id, + ) + self.log.info(f"init {department_name} success.") + + def run_common(self, department_id, department_name, department_parent_id): + try: + self.run(department_id, department_name, department_parent_id) + except Exception as e: + current_app.logger.error(f"init {department_name} err:") + current_app.logger.error(e) + raise Exception(e) + + def init_wide_company(self): + department_id = 0 + department_name = '全公司' + department_parent_id = -1 + + self.run_common(department_id, department_name, department_parent_id) + + @staticmethod + def create_acl_role_with_department(): + acl = ACLManager('acl') + role_name_map = {role['name']: role for role in acl.get_all_roles()} + + d_list = Department.query.filter( + Department.deleted == 0, Department.department_parent_id != -1).all() + for department in d_list: + if department.acl_rid > 0: + continue + + role = role_name_map.get(department.department_name) + if not role: + payload = { + 'app_id': 'acl', + 'name': department.department_name, + } + role = acl.create_role(payload) + + acl_rid = role.get('id') if role else 0 + + department.update( + acl_rid=acl_rid + ) + info = f"update department acl_rid: {acl_rid}" + current_app.logger.info(info) + + def init_backend_resource(self): + acl = self.check_app('backend') + resources_types = acl.get_all_resources_types() + + 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) + + @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 + + @staticmethod + def get_admin_user_rid(): + admin = Employee.get_by(first=True, username='admin', to_dict=False) + return admin.acl_rid if admin else 0 + + +@click.command() +@with_appcontext +def init_import_user_from_acl(): + """ + Import users from ACL + """ + InitEmployee().import_user_from_acl() + + +@click.command() +@with_appcontext +def init_department(): + """ + Department initialization + """ + cli = InitDepartment() + cli.init_wide_company() + cli.create_acl_role_with_department() + cli.init_backend_resource() + + +@click.command() +@with_appcontext +def common_check_new_columns(): + """ + add new columns to tables + """ + from api.extensions import db + from sqlalchemy import inspect, text + + def get_model_by_table_name(_table_name): + registry = getattr(db.Model, 'registry', None) + class_registry = getattr(registry, '_class_registry', None) + for _model in class_registry.values(): + if hasattr(_model, '__tablename__') and _model.__tablename__ == _table_name: + return _model + return None + + def add_new_column(target_table_name, new_column): + 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 + if new_column.comment: + sql += f" comment '{new_column.comment}'" + + if column_type == 'JSON': + pass + elif default_value: + if column_type.startswith('VAR') or column_type.startswith('Text'): + if default_value is None or len(default_value) == 0: + pass + else: + sql += f" DEFAULT {default_value}" + + sql = text(sql) + db.session.execute(sql) + + engine = db.get_engine() + inspector = inspect(engine) + table_names = inspector.get_table_names() + for table_name in table_names: + existed_columns = inspector.get_columns(table_name) + existed_column_name_list = [c['name'] for c in existed_columns] + + model = get_model_by_table_name(table_name) + if model is None: + continue + + model_columns = getattr(getattr(getattr(model, '__table__'), 'columns'), '_all_columns') + for column in model_columns: + if column.name not in existed_column_name_list: + try: + add_new_column(table_name, column) + current_app.logger.info(f"add new column [{column.name}] in table [{table_name}] success.") + 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 1d10f1cf..7d9029f1 100644 --- a/cmdb-api/api/commands/common.py +++ b/cmdb-api/api/commands/common.py @@ -5,9 +5,7 @@ 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 @@ -84,69 +82,46 @@ def clean(): os.remove(full_pathname) -@click.command() -@click.option("--url", default=None, help="Url to test (ex. /static/image.png)") -@click.option( - "--order", default="rule", help="Property on Rule to order by (default: rule)" -) -@with_appcontext -def urls(url, order): - """Display all of the url matching routes for the project. - - Borrowed from Flask-Script, converted to use Click. - """ - rows = [] - column_headers = ("Rule", "Endpoint", "Arguments") - - if url: - try: - rule, arguments = current_app.url_map.bind("localhost").match( - url, return_rule=True - ) - rows.append((rule.rule, rule.endpoint, arguments)) - column_length = 3 - except (NotFound, MethodNotAllowed) as e: - rows.append(("<{}>".format(e), None, None)) - column_length = 1 - else: - rules = sorted( - current_app.url_map.iter_rules(), key=lambda rule: getattr(rule, order) - ) - for rule in rules: - rows.append((rule.rule, rule.endpoint, None)) - column_length = 2 - - str_template = "" - table_width = 0 - - if column_length >= 1: - max_rule_length = max(len(r[0]) for r in rows) - max_rule_length = max_rule_length if max_rule_length > 4 else 4 - str_template += "{:" + str(max_rule_length) + "}" - table_width += max_rule_length - - if column_length >= 2: - max_endpoint_length = max(len(str(r[1])) for r in rows) - max_endpoint_length = max_endpoint_length if max_endpoint_length > 8 else 8 - str_template += " {:" + str(max_endpoint_length) + "}" - table_width += 2 + max_endpoint_length - - if column_length >= 3: - max_arguments_length = max(len(str(r[2])) for r in rows) - max_arguments_length = max_arguments_length if max_arguments_length > 9 else 9 - str_template += " {:" + str(max_arguments_length) + "}" - table_width += 2 + max_arguments_length - - click.echo(str_template.format(*column_headers[:column_length])) - click.echo("-" * table_width) - - for row in rows: - click.echo(str_template.format(*row[:column_length])) - - @click.command() @with_appcontext def db_setup(): """create tables """ db.create_all() + + +@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/commands/init_common_setting.py b/cmdb-api/api/commands/init_common_setting.py deleted file mode 100644 index ddc67bcc..00000000 --- a/cmdb-api/api/commands/init_common_setting.py +++ /dev/null @@ -1,164 +0,0 @@ -import click -from flask import current_app -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.resp_format import ErrFormat -from api.models.common_setting import Employee, Department - - -class InitEmployee(object): - """ - 初始化员工 - """ - - def __init__(self): - self.log = current_app.logger - - def import_user_from_acl(self): - """ - 从ACL导入用户 - """ - - acl = ACLManager('acl') - user_list = acl.get_all_users() - - username_list = [e['username'] for e in Employee.get_by()] - - for user in user_list: - if user['username'] in username_list: - continue - try: - form = EmployeeAddForm(MultiDict(user)) - if not form.validate(): - raise Exception( - ','.join(['{}: {}'.format(filed, ','.join(msg)) for filed, msg in form.errors.items()])) - data = form.data - data['acl_uid'] = user['uid'] - data['block'] = 1 if user['block'] else 0 - data.pop('password') - Employee.create( - **data - ) - except Exception as e: - self.log.error(ErrFormat.acl_import_user_failed.format(user['username'], str(e))) - self.log.error(e) - - -class InitDepartment(object): - def __init__(self): - self.log = current_app.logger - - def init(self): - self.init_wide_company() - - def hard_delete(self, department_id, department_name): - existed_deleted_list = Department.query.filter( - Department.department_name == department_name, - Department.department_id == department_id, - Department.deleted == 1, - ).all() - for existed in existed_deleted_list: - existed.delete() - - def get_department(self, department_name): - return Department.query.filter( - Department.department_name == department_name, - Department.deleted == 0, - ).order_by(Department.created_at.asc()).first() - - def run(self, department_id, department_name, department_parent_id): - self.hard_delete(department_id, department_name) - - res = self.get_department(department_name) - if res: - if res.department_id == department_id: - return - else: - new_d = res.update( - department_id=department_id, - department_parent_id=department_parent_id, - ) - return - - Department.create( - department_id=department_id, - department_name=department_name, - department_parent_id=department_parent_id, - ) - new_d = self.get_department(department_name) - - if new_d.department_id != department_id: - new_d = new_d.update( - department_id=department_id, - department_parent_id=department_parent_id, - ) - self.log.info(f"初始化 {department_name} 部门成功.") - - def run_common(self, department_id, department_name, department_parent_id): - try: - self.run(department_id, department_name, department_parent_id) - except Exception as e: - current_app.logger.error(f"init {department_name} err:") - current_app.logger.error(e) - raise Exception(e) - - def init_wide_company(self): - """ - 创建 id 0, name 全公司 的部门 - """ - department_id = 0 - department_name = '全公司' - department_parent_id = -1 - - self.run_common(department_id, department_name, department_parent_id) - - def create_acl_role_with_department(self): - """ - 当前所有部门,在ACL创建 role - """ - acl = ACLManager('acl') - role_name_map = {role['name']: role for role in acl.get_all_roles()} - - d_list = Department.query.filter( - Department.deleted == 0, Department.department_parent_id != -1).all() - for department in d_list: - if department.acl_rid > 0: - continue - - role = role_name_map.get(department.department_name) - if role is None: - payload = { - 'app_id': 'acl', - 'name': department.department_name, - } - role = acl.create_role(payload) - - acl_rid = role.get('id') if role else 0 - - department.update( - acl_rid=acl_rid - ) - info = f"update department acl_rid: {acl_rid}" - current_app.logger.info(info) - - -@click.command() -@with_appcontext -def init_import_user_from_acl(): - """ - 从ACL导入用户 - """ - InitEmployee().import_user_from_acl() - - -@click.command() -@with_appcontext -def init_department(): - """ - 初始化 部门 - """ - InitDepartment().init() - InitDepartment().create_acl_role_with_department() diff --git a/cmdb-api/api/extensions.py b/cmdb-api/api/extensions.py index f540c21b..96b9ac62 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 @@ -9,10 +10,12 @@ from flask_migrate import Migrate from flask_sqlalchemy import SQLAlchemy +from api.lib.secrets.inner import KeyManage 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() @@ -21,3 +24,4 @@ cors = CORS(supports_credentials=True) rd = RedisHandler() es = ESHandler() +inner_secrets = KeyManage() diff --git a/cmdb-api/api/lib/cmdb/attribute.py b/cmdb-api/api/lib/cmdb/attribute.py index 97b2aaa7..37aa31a6 100644 --- a/cmdb-api/api/lib/cmdb/attribute.py +++ b/cmdb-api/api/lib/cmdb/attribute.py @@ -1,15 +1,20 @@ -# -*- coding:utf-8 -*- +# -*- coding:utf-8 -*- -import requests from flask import abort from flask import current_app -from flask import g from flask import session +from flask_login import current_user from api.extensions import db from api.lib.cmdb.cache import AttributeCache +from api.lib.cmdb.cache import CITypeAttributesCache +from api.lib.cmdb.cache import CITypeCache +from api.lib.cmdb.const import BUILTIN_KEYWORDS from api.lib.cmdb.const import CITypeOperateType -from api.lib.cmdb.const import ResourceTypeEnum, RoleEnum, PermEnum +from api.lib.cmdb.const import CMDB_QUEUE +from api.lib.cmdb.const import PermEnum +from api.lib.cmdb.const import ResourceTypeEnum +from api.lib.cmdb.const import RoleEnum from api.lib.cmdb.const import ValueTypeEnum from api.lib.cmdb.history import CITypeHistoryManager from api.lib.cmdb.resp_format import ErrFormat @@ -17,7 +22,9 @@ from api.lib.decorator import kwargs_required from api.lib.perm.acl.acl import is_app_admin from api.lib.perm.acl.acl import validate_permission +from api.lib.webhook import webhook_request from api.models.cmdb import Attribute +from api.models.cmdb import CIType from api.models.cmdb import CITypeAttribute from api.models.cmdb import CITypeAttributeGroupItem from api.models.cmdb import PreferenceShowAttributes @@ -33,15 +40,11 @@ def __init__(self): pass @staticmethod - def _get_choice_values_from_web_hook(choice_web_hook): - url = choice_web_hook.get('url') - ret_key = choice_web_hook.get('ret_key') - headers = choice_web_hook.get('headers') or {} - payload = choice_web_hook.get('payload') or {} - method = choice_web_hook.get('method', 'GET').lower() + def _get_choice_values_from_webhook(choice_webhook, payload=None): + ret_key = choice_webhook.get('ret_key') try: - res = getattr(requests, method)(url, headers=headers, data=payload).json() + res = webhook_request(choice_webhook, payload or {}).json() if ret_key: ret_key_list = ret_key.strip().split("##") for key in ret_key_list[:-1]: @@ -53,52 +56,92 @@ def _get_choice_values_from_web_hook(choice_web_hook): return [[i, {}] for i in (res.get(ret_key_list[-1]) or [])] except Exception as e: - current_app.logger.error(str(e)) + current_app.logger.error("get choice values failed: {}".format(e)) return [] + @staticmethod + def _get_choice_values_from_other(choice_other): + from api.lib.cmdb.search import SearchError + from api.lib.cmdb.search.ci import search + + if choice_other.get('type_ids'): + type_ids = choice_other.get('type_ids') + attr_id = choice_other.get('attr_id') + other_filter = choice_other.get('filter') or '' + + query = "_type:({}),{}".format(";".join(map(str, type_ids)), other_filter) + s = search(query, fl=[str(attr_id)], facet=[str(attr_id)], count=1) + try: + _, _, _, _, _, facet = s.search() + return [[i[0], {}] for i in (list(facet.values()) or [[]])[0]] + except SearchError as e: + current_app.logger.error("get choice values from other ci failed: {}".format(e)) + return [] + + elif choice_other.get('script'): + try: + x = compile(choice_other['script'], '', "exec") + local_ns = {} + exec(x, {}, local_ns) + res = local_ns['ChoiceValue']().values() or [] + return [[i, {}] for i in res] + except Exception as e: + current_app.logger.error("get choice values from script: {}".format(e)) + return [] + @classmethod - def get_choice_values(cls, attr_id, value_type, choice_web_hook, choice_web_hook_parse=True): - if choice_web_hook and isinstance(choice_web_hook, dict) and choice_web_hook_parse: - return cls._get_choice_values_from_web_hook(choice_web_hook) - elif choice_web_hook and not choice_web_hook_parse: - return [] + def get_choice_values(cls, attr_id, value_type, choice_web_hook, choice_other, + choice_web_hook_parse=True, choice_other_parse=True): + if choice_web_hook: + if choice_web_hook_parse and isinstance(choice_web_hook, dict): + return cls._get_choice_values_from_webhook(choice_web_hook) + else: + return [] + elif choice_other: + if choice_other_parse and isinstance(choice_other, dict): + return cls._get_choice_values_from_other(choice_other) + else: + return [] choice_table = ValueTypeMap.choice.get(value_type) + if not choice_table: + return [] choice_values = choice_table.get_by(fl=["value", "option"], attr_id=attr_id) - return [[choice_value['value'], choice_value['option']] for choice_value in choice_values] + return [[ValueTypeMap.serialize[value_type](choice_value['value']), choice_value['option']] + for choice_value in choice_values] @staticmethod def add_choice_values(_id, value_type, choice_values): choice_table = ValueTypeMap.choice.get(value_type) + if choice_table is None: + return - db.session.query(choice_table).filter(choice_table.attr_id == _id).delete() - db.session.flush() - choice_values = choice_values - for v, option in choice_values: - table = choice_table(attr_id=_id, value=v, option=option) + choice_table.get_by(attr_id=_id, only_query=True).delete() - db.session.add(table) + for v, option in choice_values: + choice_table.create(attr_id=_id, value=v, option=option, commit=False) try: db.session.flush() - except: + except Exception as e: + current_app.logger.warning("add choice values failed: {}".format(e)) return abort(400, ErrFormat.invalid_choice_values) @staticmethod def _del_choice_values(_id, value_type): choice_table = ValueTypeMap.choice.get(value_type) - db.session.query(choice_table).filter(choice_table.attr_id == _id).delete() + choice_table and choice_table.get_by(attr_id=_id, only_query=True).delete() db.session.flush() @classmethod def search_attributes(cls, name=None, alias=None, page=1, page_size=None): """ - :param name: - :param alias: - :param page: - :param page_size: + :param name: + :param alias: + :param page: + :param page_size: :return: attribute, if name is None, then return all attributes """ if name is not None: @@ -112,8 +155,9 @@ def search_attributes(cls, name=None, alias=None, page=1, page_size=None): attrs = attrs[(page - 1) * page_size:][:page_size] res = list() for attr in attrs: - attr["is_choice"] and attr.update(dict(choice_value=cls.get_choice_values( - attr["id"], attr["value_type"], attr["choice_web_hook"]))) + attr["is_choice"] and attr.update( + dict(choice_value=cls.get_choice_values(attr["id"], attr["value_type"], + attr["choice_web_hook"], attr.get("choice_other")))) attr['is_choice'] and attr.pop('choice_web_hook', None) res.append(attr) @@ -122,30 +166,41 @@ def search_attributes(cls, name=None, alias=None, page=1, page_size=None): def get_attribute_by_name(self, name): attr = Attribute.get_by(name=name, first=True) - if attr and attr["is_choice"]: - attr.update(dict(choice_value=self.get_choice_values( - attr["id"], attr["value_type"], attr["choice_web_hook"]))) + if attr.get("is_choice"): + attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"], + attr["choice_web_hook"], attr.get("choice_other")) + return attr def get_attribute_by_alias(self, alias): attr = Attribute.get_by(alias=alias, first=True) - if attr and attr["is_choice"]: - attr.update(dict(choice_value=self.get_choice_values( - attr["id"], attr["value_type"], attr["choice_web_hook"]))) + if attr.get("is_choice"): + attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"], + attr["choice_web_hook"], attr.get("choice_other")) + return attr def get_attribute_by_id(self, _id): attr = Attribute.get_by_id(_id).to_dict() - if attr and attr["is_choice"]: - attr.update(dict(choice_value=self.get_choice_values( - attr["id"], attr["value_type"], attr["choice_web_hook"]))) + if attr.get("is_choice"): + attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"], + attr["choice_web_hook"], attr.get("choice_other")) + return attr - def get_attribute(self, key, choice_web_hook_parse=True): - attr = AttributeCache.get(key).to_dict() - if attr and attr["is_choice"]: - attr.update(dict(choice_value=self.get_choice_values( - attr["id"], attr["value_type"], attr["choice_web_hook"])), choice_web_hook_parse=choice_web_hook_parse) + def get_attribute(self, key, choice_web_hook_parse=True, choice_other_parse=True): + 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"], + attr["value_type"], + attr["choice_web_hook"], + attr.get("choice_other"), + choice_web_hook_parse=choice_web_hook_parse, + choice_other_parse=choice_other_parse, + ) + return attr @staticmethod @@ -153,16 +208,40 @@ def can_create_computed_attribute(): 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)) + @classmethod + def calc_computed_attribute(cls, attr_id): + """ + calculate computed attribute for all ci + :param attr_id: + :return: + """ + cls.can_create_computed_attribute() + + from api.tasks.cmdb import calc_computed_attribute + + calc_computed_attribute.apply_async(args=(attr_id, current_user.uid), queue=CMDB_QUEUE) + @classmethod @kwargs_required("name") def add(cls, **kwargs): choice_value = kwargs.pop("choice_value", []) kwargs.pop("is_choice", None) - is_choice = True if choice_value or kwargs.get('choice_web_hook') else False + is_choice = True if choice_value or kwargs.get('choice_web_hook') or kwargs.get('choice_other') else False name = kwargs.pop("name") - if name in {'id', '_id', 'ci_id', 'type', '_type', 'ci_type'}: + if name in BUILTIN_KEYWORDS: return abort(400, ErrFormat.attribute_name_cannot_be_builtin) + + while kwargs.get('choice_other'): + if isinstance(kwargs['choice_other'], dict): + if kwargs['choice_other'].get('script'): + break + + if kwargs['choice_other'].get('type_ids') and kwargs['choice_other'].get('attr_id'): + break + + return abort(400, ErrFormat.attribute_choice_other_invalid) + alias = kwargs.pop("alias", "") alias = name if not alias else alias Attribute.get_by(name=name, first=True) and abort(400, ErrFormat.attribute_name_duplicate.format(name)) @@ -172,11 +251,13 @@ def add(cls, **kwargs): kwargs.get('is_computed') and cls.can_create_computed_attribute() + kwargs.get('choice_other') and kwargs['choice_other'].get('script') and cls.can_create_computed_attribute() + attr = Attribute.create(flush=True, name=name, alias=alias, is_choice=is_choice, - uid=g.user.uid, + uid=current_user.uid, **kwargs) if choice_value: @@ -210,6 +291,11 @@ def add(cls, **kwargs): return attr.id + @staticmethod + def _clean_ci_type_attributes_cache(attr_id): + for i in CITypeAttribute.get_by(attr_id=attr_id, to_dict=False): + CITypeAttributesCache.clean(i.type_id) + @staticmethod def _change_index(attr, old, new): from api.lib.cmdb.utils import TableMap @@ -220,11 +306,11 @@ def _change_index(attr, old, new): new_table = TableMap(attr=attr, is_index=new).table ci_ids = [] - for i in db.session.query(old_table).filter(getattr(old_table, 'attr_id') == attr.id): + for i in old_table.get_by(attr_id=attr.id, to_dict=False): new_table.create(ci_id=i.ci_id, attr_id=attr.id, value=i.value, flush=True) ci_ids.append(i.ci_id) - db.session.query(old_table).filter(getattr(old_table, 'attr_id') == attr.id).delete() + old_table.get_by(attr_id=attr.id, only_query=True).delete() try: db.session.commit() @@ -239,7 +325,7 @@ def _change_index(attr, old, new): def _can_edit_attribute(attr): from api.lib.cmdb.ci_type import CITypeManager - if attr.uid == g.user.uid: + if attr.uid == current_user.uid: return True for i in CITypeAttribute.get_by(attr_id=attr.id, to_dict=False): @@ -252,9 +338,6 @@ def _can_edit_attribute(attr): def update(self, _id, **kwargs): attr = Attribute.get_by_id(_id) or abort(404, ErrFormat.attribute_not_found.format("id={}".format(_id))) - if not self._can_edit_attribute(attr): - return abort(403, ErrFormat.cannot_edit_attribute) - if kwargs.get("name"): other = Attribute.get_by(name=kwargs['name'], first=True, to_dict=False) if other and other.id != attr.id: @@ -272,12 +355,22 @@ def update(self, _id, **kwargs): self._change_index(attr, attr.is_index, kwargs['is_index']) + while kwargs.get('choice_other'): + if isinstance(kwargs['choice_other'], dict): + if kwargs['choice_other'].get('script'): + break + + if kwargs['choice_other'].get('type_ids') and kwargs['choice_other'].get('attr_id'): + break + + return abort(400, ErrFormat.attribute_choice_other_invalid) + existed2 = attr.to_dict() - if not existed2['choice_web_hook'] and existed2['is_choice']: - existed2['choice_value'] = self.get_choice_values(attr.id, attr.value_type, attr.choice_web_hook) + if not existed2['choice_web_hook'] and not existed2.get('choice_other') and existed2['is_choice']: + existed2['choice_value'] = self.get_choice_values(attr.id, attr.value_type, None, None) choice_value = kwargs.pop("choice_value", False) - is_choice = True if choice_value or kwargs.get('choice_web_hook') else False + is_choice = True if choice_value or kwargs.get('choice_web_hook') or kwargs.get('choice_other') else False kwargs['is_choice'] = is_choice if kwargs.get('default') and not (isinstance(kwargs['default'], dict) and 'default' in kwargs['default']): @@ -285,11 +378,19 @@ def update(self, _id, **kwargs): kwargs.get('is_computed') and self.can_create_computed_attribute() + is_changed = False + for k in kwargs: + if kwargs[k] != getattr(attr, k, None): + is_changed = True + + if is_changed and not self._can_edit_attribute(attr): + return abort(403, ErrFormat.cannot_edit_attribute) + attr.update(flush=True, filter_none=False, **kwargs) if is_choice and choice_value: self.add_choice_values(attr.id, attr.value_type, choice_value) - elif is_choice: + elif existed2['is_choice']: self._del_choice_values(attr.id, attr.value_type) try: @@ -308,6 +409,8 @@ def update(self, _id, **kwargs): AttributeCache.clean(attr) + self._clean_ci_type_attributes_cache(_id) + return attr.id @staticmethod @@ -315,25 +418,31 @@ def delete(_id): attr = Attribute.get_by_id(_id) or abort(404, ErrFormat.attribute_not_found.format("id={}".format(_id))) name = attr.name - if attr.uid and attr.uid != g.user.uid: + if CIType.get_by(unique_id=attr.id, first=True, to_dict=False) is not None: + return abort(400, ErrFormat.attribute_is_unique_id) + + ref = CITypeAttribute.get_by(attr_id=_id, to_dict=False, first=True) + if ref is not None: + ci_type = CITypeCache.get(ref.type_id) + return abort(400, ErrFormat.attribute_is_ref_by_type.format(ci_type and ci_type.alias or ref.type_id)) + + if attr.uid != current_user.uid and not is_app_admin('cmdb'): return abort(403, ErrFormat.cannot_delete_attribute) if attr.is_choice: choice_table = ValueTypeMap.choice.get(attr.value_type) - db.session.query(choice_table).filter(choice_table.attr_id == _id).delete() # FIXME: session conflict - db.session.flush() - - AttributeCache.clean(attr) + choice_table.get_by(attr_id=_id, only_query=True).delete() attr.soft_delete() - for i in CITypeAttribute.get_by(attr_id=_id, to_dict=False): - i.soft_delete() + AttributeCache.clean(attr) for i in PreferenceShowAttributes.get_by(attr_id=_id, to_dict=False): - i.soft_delete() + i.soft_delete(commit=False) for i in CITypeAttributeGroupItem.get_by(attr_id=_id, to_dict=False): - i.soft_delete() + i.soft_delete(commit=False) + + db.session.commit() return name 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 54a84486..cda985d2 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 json import os -from flask import abort -from flask import current_app -from flask import g -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.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__)) @@ -36,9 +35,10 @@ def parse_plugin_script(script): attributes = [] try: x = compile(script, '', "exec") - exec(x) - unique_key = locals()['AutoDiscovery']().unique_key - attrs = locals()['AutoDiscovery']().attributes() or [] + local_ns = {} + exec(x, {}, local_ns) + unique_key = local_ns['AutoDiscovery']().unique_key + attrs = local_ns['AutoDiscovery']().attributes() or [] except Exception as e: return abort(400, str(e)) @@ -156,7 +156,7 @@ def get(cls, ci_id, oneagent_id, last_update_at=None): continue if isinstance(rule.get("extra_option"), dict) and rule['extra_option'].get('secret'): - if not (g.user.username == "cmdb_agent" or g.user.uid == rule['uid']): + if not (current_user.username == "cmdb_agent" or current_user.uid == rule['uid']): rule['extra_option'].pop('secret', None) else: rule['extra_option']['secret'] = AESCrypto.decrypt(rule['extra_option']['secret']) @@ -213,7 +213,7 @@ def __valid_exec_target(agent_id, query_expr): agent_id = agent_id.strip() q = "op_duty:{0},-rd_duty:{0},oneagent_id:{1}" - s = search(q.format(g.user.username, agent_id.strip())) + s = search(q.format(current_user.username, agent_id.strip())) try: response, _, _, _, _, _ = s.search() if response: @@ -222,7 +222,7 @@ def __valid_exec_target(agent_id, query_expr): current_app.logger.warning(e) return abort(400, str(e)) - s = search(q.format(g.user.nickname, agent_id.strip())) + s = search(q.format(current_user.nickname, agent_id.strip())) try: response, _, _, _, _, _ = s.search() if response: @@ -240,29 +240,27 @@ def __valid_exec_target(agent_id, query_expr): try: response, _, _, _, _, _ = s.search() for i in response: - if g.user.username not in (i.get('rd_duty') or []) and g.user.username not in \ - (i.get('op_duty') or []) and g.user.nickname not in (i.get('rd_duty') or []) and \ - g.user.nickname not in (i.get('op_duty') or []): + if (current_user.username not in (i.get('rd_duty') or []) and + current_user.username not in (i.get('op_duty') or []) and + current_user.nickname not in (i.get('rd_duty') or []) and + current_user.nickname not in (i.get('op_duty') or [])): return abort(403, ErrFormat.adt_target_expr_no_permission.format( i.get("{}_name".format(i.get('ci_type'))))) except SearchError as e: 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) @@ -270,7 +268,7 @@ def _can_add(self, **kwargs): if isinstance(kwargs.get('extra_option'), dict) and kwargs['extra_option'].get('secret'): kwargs['extra_option']['secret'] = AESCrypto.encrypt(kwargs['extra_option']['secret']) - kwargs['uid'] = g.user.uid + kwargs['uid'] = current_user.uid return kwargs @@ -281,7 +279,7 @@ def _can_update(self, **kwargs): self.__valid_exec_target(kwargs.get('agent_id'), kwargs.get('query_expr')) if isinstance(kwargs.get('extra_option'), dict) and kwargs['extra_option'].get('secret'): - if g.user.uid != existed.uid: + if current_user.uid != existed.uid: return abort(403, ErrFormat.adt_secret_no_permission) return existed @@ -453,10 +451,12 @@ def accept(cls, adc, adc_id=None, nickname=None): relation_adts = AutoDiscoveryCIType.get_by(type_id=adt.type_id, adr_id=None, to_dict=False) for r_adt in relation_adts: - if r_adt.relation and ci_id is not None: - ad_key, cmdb_key = None, {} - for ad_key in r_adt.relation: - cmdb_key = r_adt.relation[ad_key] + if not r_adt.relation or ci_id is None: + continue + for ad_key in r_adt.relation: + if not adc.instance.get(ad_key): + continue + cmdb_key = r_adt.relation[ad_key] query = "_type:{},{}:{}".format(cmdb_key.get('type_name'), cmdb_key.get('attr_name'), adc.instance.get(ad_key)) s = search(query) @@ -476,7 +476,10 @@ def accept(cls, adc, adc_id=None, nickname=None): except: pass - adc.update(is_accept=True, accept_by=nickname or g.user.nickname, accept_time=datetime.datetime.now()) + adc.update(is_accept=True, + accept_by=nickname or current_user.nickname, + accept_time=datetime.datetime.now(), + ci_id=ci_id) class AutoDiscoveryHTTPManager(object): 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 ca9285f8..e570b2df 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 90fbf941..a4ba9a7d 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 af68387a..854ed13c 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 9d5f461e..1f7c3f9f 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 68f0905b..c19812fe 100644 --- a/cmdb-api/api/lib/cmdb/cache.py +++ b/cmdb-api/api/lib/cmdb/cache.py @@ -2,14 +2,11 @@ from __future__ import unicode_literals -import requests from flask import current_app from api.extensions import cache -from api.extensions import db from api.lib.cmdb.custom_dashboard import CustomDashboardManager from api.models.cmdb import Attribute -from api.models.cmdb import CI from api.models.cmdb import CIType from api.models.cmdb import CITypeAttribute from api.models.cmdb import RelationType @@ -34,6 +31,7 @@ def get(cls, key): attr = attr or Attribute.get_by(alias=key, first=True, to_dict=False) if attr is not None: cls.set(attr) + return attr @classmethod @@ -67,6 +65,7 @@ def get(cls, key): ct = ct or CIType.get_by(alias=key, first=True, to_dict=False) if ct is not None: cls.set(ct) + return ct @classmethod @@ -98,6 +97,7 @@ def get(cls, key): ct = RelationType.get_by(name=key, first=True, to_dict=False) or RelationType.get_by_id(key) if ct is not None: cls.set(ct) + return ct @classmethod @@ -133,12 +133,15 @@ def get(cls, key): attrs = attrs or cache.get(cls.PREFIX_ID.format(key)) if not attrs: attrs = CITypeAttribute.get_by(type_id=key, to_dict=False) + if not attrs: ci_type = CIType.get_by(name=key, first=True, to_dict=False) if ci_type is not None: attrs = CITypeAttribute.get_by(type_id=ci_type.id, to_dict=False) + if attrs is not None: cls.set(key, attrs) + return attrs @classmethod @@ -155,13 +158,16 @@ def get2(cls, key): attrs = attrs or cache.get(cls.PREFIX_ID2.format(key)) if not attrs: attrs = CITypeAttribute.get_by(type_id=key, to_dict=False) + if not attrs: ci_type = CIType.get_by(name=key, first=True, to_dict=False) if ci_type is not None: attrs = CITypeAttribute.get_by(type_id=ci_type.id, to_dict=False) + if attrs is not None: attrs = [(i, AttributeCache.get(i.attr_id)) for i in attrs] cls.set2(key, attrs) + return attrs @classmethod @@ -201,13 +207,13 @@ class CITypeAttributeCache(object): @classmethod def get(cls, type_id, attr_id): - attr = cache.get(cls.PREFIX_ID.format(type_id, attr_id)) attr = attr or cache.get(cls.PREFIX_ID.format(type_id, attr_id)) - if not attr: - attr = CITypeAttribute.get_by(type_id=type_id, attr_id=attr_id, first=True, to_dict=False) - if attr is not None: - cls.set(type_id, attr_id, attr) + attr = attr or CITypeAttribute.get_by(type_id=type_id, attr_id=attr_id, first=True, to_dict=False) + + if attr is not None: + cls.set(type_id, attr_id, attr) + return attr @classmethod @@ -241,53 +247,72 @@ def reset(cls): result = {} for custom in customs: if custom['category'] == 0: - result[custom['id']] = cls.summary_counter(custom['type_id']) + res = cls.sum_counter(custom) elif custom['category'] == 1: - result[custom['id']] = cls.attribute_counter(custom['type_id'], custom['attr_id']) - elif custom['category'] == 2: - result[custom['id']] = cls.relation_counter(custom['type_id'], custom['level']) + res = cls.attribute_counter(custom) + else: + res = cls.relation_counter(custom.get('type_id'), + custom.get('level'), + custom.get('options', {}).get('filter', ''), + custom.get('options', {}).get('type_ids', '')) + + if res: + result[custom['id']] = res cls.set(result) return result @classmethod - def update(cls, custom): + def update(cls, custom, flush=True): result = cache.get(cls.KEY) or {} if not result: result = cls.reset() if custom['category'] == 0: - result[custom['id']] = cls.summary_counter(custom['type_id']) + res = cls.sum_counter(custom) elif custom['category'] == 1: - result[custom['id']] = cls.attribute_counter(custom['type_id'], custom['attr_id']) - elif custom['category'] == 2: - result[custom['id']] = cls.relation_counter(custom['type_id'], custom['level']) + res = cls.attribute_counter(custom) + else: + res = cls.relation_counter(custom.get('type_id'), + custom.get('level'), + custom.get('options', {}).get('filter', ''), + custom.get('options', {}).get('type_ids', '')) - cls.set(result) + if res and flush: + result[custom['id']] = res + cls.set(result) - @staticmethod - def summary_counter(type_id): - return db.session.query(CI.id).filter(CI.deleted.is_(False)).filter(CI.type_id == type_id).count() + return res @staticmethod - def relation_counter(type_id, level): - - uri = current_app.config.get('CMDB_API') + def relation_counter(type_id, level, other_filer, type_ids): + from api.lib.cmdb.search.ci_relation.search import Search as RelSearch + from api.lib.cmdb.search import SearchError + from api.lib.cmdb.search.ci import search + + query = "_type:{}".format(type_id) + s = search(query, count=1000000) + try: + type_names, _, _, _, _, _ = s.search() + except SearchError as e: + current_app.logger.error(e) + return - type_names = requests.get("{}/ci/s?q=_type:{}&count=10000".format(uri, type_id)).json().get('result') type_id_names = [(str(i.get('_id')), i.get(i.get('unique'))) for i in type_names] - url = "{}/ci_relations/statistics?root_ids={}&level={}".format( - uri, ','.join([i[0] for i in type_id_names]), level) - stats = requests.get(url).json() + s = RelSearch([i[0] for i in type_id_names], level, other_filer or '') + try: + stats = s.statistics(type_ids) + except SearchError as e: + current_app.logger.error(e) + return id2name = dict(type_id_names) type_ids = set() for i in (stats.get('detail') or []): for j in stats['detail'][i]: type_ids.add(j) - for type_id in type_ids: _type = CITypeCache.get(type_id) id2name[type_id] = _type and _type.alias @@ -307,9 +332,100 @@ def relation_counter(type_id, level): return result @staticmethod - def attribute_counter(type_id, attr_id): - uri = current_app.config.get('CMDB_API') - url = "{}/ci/s?q=_type:{}&fl={}&facet={}".format(uri, type_id, attr_id, attr_id) - res = requests.get(url).json() - if res.get('facet'): - return dict([i[:2] for i in list(res.get('facet').values())[0]]) + def attribute_counter(custom): + from api.lib.cmdb.search import SearchError + from api.lib.cmdb.search.ci import search + from api.lib.cmdb.utils import ValueTypeMap + + custom.setdefault('options', {}) + type_id = custom.get('type_id') + attr_id = custom.get('attr_id') + type_ids = custom['options'].get('type_ids') or (type_id and [type_id]) + attr_ids = list(map(str, custom['options'].get('attr_ids') or (attr_id and [attr_id]))) + try: + attr2value_type = [AttributeCache.get(i).value_type for i in attr_ids] + except AttributeError: + return + + other_filter = custom['options'].get('filter') + other_filter = "{}".format(other_filter) if other_filter else '' + + if custom['options'].get('ret') == 'cis': + query = "_type:({}),{}".format(";".join(map(str, type_ids)), other_filter) + s = search(query, fl=attr_ids, ret_key='alias', count=100) + try: + cis, _, _, _, _, _ = s.search() + except SearchError as e: + current_app.logger.error(e) + return + + return cis + + result = dict() + # level = 1 + query = "_type:({}),{}".format(";".join(map(str, type_ids)), other_filter) + s = search(query, fl=attr_ids, facet=[attr_ids[0]], count=1) + try: + _, _, _, _, _, facet = s.search() + except SearchError as e: + current_app.logger.error(e) + return + for i in (list(facet.values()) or [[]])[0]: + result[ValueTypeMap.serialize2[attr2value_type[0]](str(i[0]))] = i[1] + if len(attr_ids) == 1: + return result + + # level = 2 + for v in result: + query = "_type:({}),{},{}:{}".format(";".join(map(str, type_ids)), other_filter, attr_ids[0], v) + s = search(query, fl=attr_ids, facet=[attr_ids[1]], count=1) + try: + _, _, _, _, _, facet = s.search() + except SearchError as e: + current_app.logger.error(e) + return + result[v] = dict() + for i in (list(facet.values()) or [[]])[0]: + result[v][ValueTypeMap.serialize2[attr2value_type[1]](str(i[0]))] = i[1] + + if len(attr_ids) == 2: + return result + + # level = 3 + for v1 in result: + if not isinstance(result[v1], dict): + continue + for v2 in result[v1]: + query = "_type:({}),{},{}:{},{}:{}".format(";".join(map(str, type_ids)), other_filter, + attr_ids[0], v1, attr_ids[1], v2) + s = search(query, fl=attr_ids, facet=[attr_ids[2]], count=1) + try: + _, _, _, _, _, facet = s.search() + except SearchError as e: + current_app.logger.error(e) + return + result[v1][v2] = dict() + for i in (list(facet.values()) or [[]])[0]: + result[v1][v2][ValueTypeMap.serialize2[attr2value_type[2]](str(i[0]))] = i[1] + + return result + + @staticmethod + def sum_counter(custom): + from api.lib.cmdb.search import SearchError + from api.lib.cmdb.search.ci import search + + custom.setdefault('options', {}) + type_id = custom.get('type_id') + type_ids = custom['options'].get('type_ids') or (type_id and [type_id]) + other_filter = custom['options'].get('filter') or '' + + query = "_type:({}),{}".format(";".join(map(str, type_ids)), other_filter) + s = search(query, count=1) + try: + _, _, _, _, numfound, _ = s.search() + except SearchError as e: + current_app.logger.error(e) + return + + return numfound diff --git a/cmdb-api/api/lib/cmdb/ci.py b/cmdb-api/api/lib/cmdb/ci.py index e2c3cf31..3aed0d70 100644 --- a/cmdb-api/api/lib/cmdb/ci.py +++ b/cmdb-api/api/lib/cmdb/ci.py @@ -4,10 +4,11 @@ import copy import datetime import json +import threading from flask import abort from flask import current_app -from flask import g +from flask_login import current_user from werkzeug.exceptions import BadRequest from api.extensions import db @@ -24,31 +25,46 @@ from api.lib.cmdb.const import ConstraintEnum from api.lib.cmdb.const import ExistPolicy from api.lib.cmdb.const import OperateType +from api.lib.cmdb.const import PermEnum from api.lib.cmdb.const import REDIS_PREFIX_CI -from api.lib.cmdb.const import ResourceTypeEnum, PermEnum +from api.lib.cmdb.const import ResourceTypeEnum from api.lib.cmdb.const import RetKey +from api.lib.cmdb.const import ValueTypeEnum from api.lib.cmdb.history import AttributeHistoryManger from api.lib.cmdb.history import CIRelationHistoryManager +from api.lib.cmdb.history import CITriggerHistoryManager from api.lib.cmdb.perms import CIFilterPermsCRUD from api.lib.cmdb.resp_format import ErrFormat from api.lib.cmdb.utils import TableMap from api.lib.cmdb.utils import ValueTypeMap from api.lib.cmdb.value import AttributeValueManager from api.lib.decorator import kwargs_required +from api.lib.notify import notify_send from api.lib.perm.acl.acl import ACLManager from api.lib.perm.acl.acl import is_app_admin from api.lib.perm.acl.acl import validate_permission +from api.lib.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 from api.tasks.cmdb import ci_delete +from api.tasks.cmdb import ci_delete_trigger +from api.tasks.cmdb import ci_relation_add from api.tasks.cmdb import ci_relation_cache from api.tasks.cmdb import ci_relation_delete +PRIVILEGED_USERS = {"worker", "cmdb_agent", "agent"} +PASSWORD_DEFAULT_SHOW = "******" + class CIManager(object): """ manage CI interface @@ -64,11 +80,13 @@ def get_by_id(ci_id): @staticmethod def get_type_name(ci_id): ci = CI.get_by_id(ci_id) or abort(404, ErrFormat.ci_not_found.format("id={}".format(ci_id))) + return CITypeCache.get(ci.type_id).name @staticmethod def get_type(ci_id): ci = CI.get_by_id(ci_id) or abort(404, ErrFormat.ci_not_found.format("id={}".format(ci_id))) + return CITypeCache.get(ci.type_id) @staticmethod @@ -90,9 +108,7 @@ def get_ci_by_id(cls, ci_id, ret_key=RetKey.NAME, fields=None, need_children=Tru res = dict() - if need_children: - children = CIRelationManager.get_children(ci_id, ret_key=ret_key) # one floor - res.update(children) + need_children and res.update(CIRelationManager.get_children(ci_id, ret_key=ret_key)) # one floor ci_type = CITypeCache.get(ci.type_id) res["ci_type"] = ci_type.name @@ -159,16 +175,16 @@ def get_ci_by_id_from_db(cls, ci_id, ret_key=RetKey.NAME, fields=None, need_chil ci = CI.get_by_id(ci_id) or abort(404, ErrFormat.ci_not_found.format("id={}".format(ci_id))) - if valid: - cls.valid_ci_only_read(ci) + valid and cls.valid_ci_only_read(ci) res = dict() - if need_children: - children = CIRelationManager.get_children(ci_id, ret_key=ret_key) # one floor - res.update(children) + need_children and res.update(CIRelationManager.get_children(ci_id, ret_key=ret_key)) # one floor ci_type = CITypeCache.get(ci.type_id) + if not ci_type: + return res + res["ci_type"] = ci_type.name fields = CITypeAttributeManager.get_attr_names_by_type_id(ci.type_id) if not fields else fields @@ -245,7 +261,7 @@ def _valid_unique_constraint(type_id, ci_dict, ci_id=None): for i in unique_constraints: attr_ids.extend(i.attr_ids) - attrs = [AttributeCache.get(i) for i in list(set(attr_ids))] + attrs = [AttributeCache.get(i) for i in set(attr_ids)] id2name = {i.id: i.name for i in attrs if i} not_existed_fields = list(set(id2name.values()) - set(ci_dict.keys())) if not_existed_fields and ci_id is not None: @@ -290,7 +306,7 @@ def add(cls, ci_type_name, _is_admin=False, **ci_dict): """ - + add ci :param ci_type_name: :param exist_policy: replace or reject or need :param _no_attribute_policy: ignore or reject @@ -305,9 +321,7 @@ def add(cls, ci_type_name, 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) - unique_value = unique_value or ci_dict.get(unique_key.alias) - unique_value = unique_value or ci_dict.get(unique_key.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) @@ -316,7 +330,9 @@ def add(cls, ci_type_name, ci_attr2type_attr = {type_attr.attr_id: type_attr for type_attr, _ in attrs} ci = None - need_lock = g.user.username not in ("worker", "cmdb_agent", "agent") + 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): existed = cls.ci_is_exist(unique_key, unique_value, ci_type.id) if existed is not None: @@ -330,10 +346,6 @@ def add(cls, ci_type_name, if exist_policy == ExistPolicy.NEED: return abort(404, ErrFormat.ci_not_found.format("{}={}".format(unique_key.name, unique_value))) - from api.lib.cmdb.const import L_CI - if L_CI and len(CI.get_by(type_id=ci_type.id)) > L_CI * 2: - return abort(400, ErrFormat.limit_ci.format(L_CI * 2)) - limit_attrs = cls._valid_ci_for_no_read(ci, ci_type) if not _is_admin else {} if existed is None: # set default @@ -348,14 +360,23 @@ def add(cls, ci_type_name, ci_dict.get(attr.name) is None and ci_dict.get(attr.alias) is None)): ci_dict[attr.name] = attr.default.get('default') - if type_attr.is_required and (attr.name not in ci_dict and attr.alias not in ci_dict): + if (type_attr.is_required and not attr.is_computed and + (attr.name not in ci_dict and attr.alias not in ci_dict)): return abort(400, ErrFormat.attribute_value_required.format(attr.name)) else: for type_attr, attr in attrs: if attr.default and attr.default.get('default') == AttributeDefaultValueEnum.UPDATED_AT: ci_dict[attr.name] = now - computed_attrs = [attr.to_dict() for _, attr in attrs if attr.is_computed] or None + computed_attrs = [] + for _, attr in attrs: + if attr.is_computed: + computed_attrs.append(attr.to_dict()) + elif attr.is_password: + if attr.name in ci_dict: + password_dict[attr.id] = ci_dict.pop(attr.name) + elif attr.alias in ci_dict: + password_dict[attr.id] = ci_dict.pop(attr.alias) value_manager = AttributeValueManager() @@ -364,13 +385,19 @@ def add(cls, ci_type_name, cls._valid_unique_constraint(ci_type.id, ci_dict, ci and ci.id) + ref_ci_dict = dict() for k in ci_dict: - if k not in ci_type_attrs_name and k not in ci_type_attrs_alias and \ - _no_attribute_policy == ExistPolicy.REJECT: + if k.startswith("$") and "." in k: + ref_ci_dict[k] = ci_dict[k] + continue + + if k not in ci_type_attrs_name and ( + 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: + _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: return abort(403, ErrFormat.ci_filter_perm_attr_no_permission.format(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} @@ -378,16 +405,24 @@ def add(cls, ci_type_name, key2attr = value_manager.valid_attr_value(ci_dict, ci_type.id, ci and ci.id, ci_type_attrs_name, ci_type_attrs_alias, ci_attr2type_attr) + operate_type = OperateType.UPDATE if ci is not None else OperateType.ADD try: ci = ci or CI.create(type_id=ci_type.id, is_auto_discovery=is_auto_discovery) - record_id = value_manager.create_or_update_attr_value2(ci, ci_dict, key2attr) + record_id = value_manager.create_or_update_attr_value(ci, ci_dict, key2attr) except BadRequest as e: if existed is None: cls.delete(ci.id) raise e + if password_dict: + for attr_id in password_dict: + record_id = cls.save_password(ci.id, attr_id, password_dict[attr_id], record_id, ci_type.id) + if record_id: # has change - ci_cache.apply_async([ci.id], queue=CMDB_QUEUE) + ci_cache.apply_async(args=(ci.id, operate_type, record_id), queue=CMDB_QUEUE) + + if ref_ci_dict: # add relations + ci_relation_add.apply_async(args=(ref_ci_dict, ci.id, current_user.uid), queue=CMDB_QUEUE) return ci.id @@ -402,7 +437,16 @@ def update(self, ci_id, _is_admin=False, **ci_dict): if attr.default and attr.default.get('default') == AttributeDefaultValueEnum.UPDATED_AT: ci_dict[attr.name] = now - computed_attrs = [attr.to_dict() for _, attr in attrs if attr.is_computed] or None + password_dict = dict() + computed_attrs = list() + for _, attr in attrs: + if attr.is_computed: + computed_attrs.append(attr.to_dict()) + elif attr.is_password: + if attr.name in ci_dict: + password_dict[attr.id] = ci_dict.pop(attr.name) + elif attr.alias in ci_dict: + password_dict[attr.id] = ci_dict.pop(attr.alias) value_manager = AttributeValueManager() @@ -411,7 +455,8 @@ def update(self, ci_id, _is_admin=False, **ci_dict): limit_attrs = self._valid_ci_for_no_read(ci) if not _is_admin else {} - need_lock = g.user.username not in ("worker", "cmdb_agent", "agent") + 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): self._valid_unique_constraint(ci.type_id, ci_dict, ci_id) @@ -424,20 +469,29 @@ def update(self, ci_id, _is_admin=False, **ci_dict): return abort(403, ErrFormat.ci_filter_perm_attr_no_permission.format(k)) try: - record_id = value_manager.create_or_update_attr_value2(ci, ci_dict, key2attr) + record_id = value_manager.create_or_update_attr_value(ci, ci_dict, key2attr) except BadRequest as e: raise e + if password_dict: + for attr_id in password_dict: + record_id = self.save_password(ci.id, attr_id, password_dict[attr_id], record_id, ci.type_id) + if record_id: # has change - ci_cache.apply_async([ci_id], queue=CMDB_QUEUE) + ci_cache.apply_async(args=(ci_id, OperateType.UPDATE, record_id), queue=CMDB_QUEUE) + + ref_ci_dict = {k: v for k, v in ci_dict.items() if k.startswith("$") and "." in k} + if ref_ci_dict: + ci_relation_add.apply_async(args=(ref_ci_dict, ci.id), queue=CMDB_QUEUE) @staticmethod def update_unique_value(ci_id, unique_name, unique_value): ci = CI.get_by_id(ci_id) or abort(404, ErrFormat.ci_not_found.format("id={}".format(ci_id))) - AttributeValueManager().create_or_update_attr_value(unique_name, unique_value, ci) + key2attr = {unique_name: AttributeCache.get(unique_name)} + record_id = AttributeValueManager().create_or_update_attr_value(ci, {unique_name: unique_value}, key2attr) - ci_cache.apply_async([ci_id], queue=CMDB_QUEUE) + ci_cache.apply_async(args=(ci_id, OperateType.UPDATE, record_id), queue=CMDB_QUEUE) @classmethod def delete(cls, ci_id): @@ -448,26 +502,46 @@ def delete(cls, ci_id): ci_dict = cls.get_cis_by_ids([ci_id]) ci_dict = ci_dict and ci_dict[0] + if ci_dict: + triggers = CITriggerManager.get(ci_dict['_type']) + for trigger in triggers: + option = trigger['option'] + if not option.get('enable') or option.get('action') != OperateType.DELETE: + continue + + if option.get('filter') and not CITriggerManager.ci_filter(ci_dict.get('_id'), option['filter']): + continue + + ci_delete_trigger.apply_async(args=(trigger, OperateType.DELETE, ci_dict), queue=CMDB_QUEUE) + attrs = CITypeAttribute.get_by(type_id=ci.type_id, to_dict=False) - attr_names = set([AttributeCache.get(attr.attr_id).name for attr in attrs]) - for attr_name in attr_names: - value_table = TableMap(attr_name=attr_name).table + attrs = [AttributeCache.get(attr.attr_id) for attr in attrs] + 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() + item.delete(commit=False) for item in CIRelation.get_by(first_ci_id=ci_id, to_dict=False): - ci_relation_delete.apply_async(args=(item.first_ci_id, item.second_ci_id), queue=CMDB_QUEUE) - item.delete() + ci_relation_delete.apply_async( + args=(item.first_ci_id, item.second_ci_id, item.ancestor_ids), queue=CMDB_QUEUE) + item.delete(commit=False) for item in CIRelation.get_by(second_ci_id=ci_id, to_dict=False): - ci_relation_delete.apply_async(args=(item.first_ci_id, item.second_ci_id), queue=CMDB_QUEUE) - item.delete() + ci_relation_delete.apply_async( + args=(item.first_ci_id, item.second_ci_id, item.ancestor_ids), queue=CMDB_QUEUE) + item.delete(commit=False) - ci.delete() # TODO: soft delete + ad_ci = AutoDiscoveryCI.get_by(ci_id=ci_id, to_dict=False, first=True) + ad_ci and ad_ci.update(is_accept=False, accept_by=None, accept_time=None, filter_none=False, commit=False) - AttributeHistoryManger.add(None, ci_id, [(None, OperateType.DELETE, ci_dict, None)], ci.type_id) + ci.delete(commit=False) # TODO: soft delete - ci_delete.apply_async([ci.id], queue=CMDB_QUEUE) + db.session.commit() + + if ci_dict: + AttributeHistoryManger.add(None, ci_id, [(None, OperateType.DELETE, ci_dict, None)], ci.type_id) + + ci_delete.apply_async(args=(ci_id,), queue=CMDB_QUEUE) return ci_id @@ -478,11 +552,8 @@ def add_heartbeat(ci_type, unique_value): unique_key = AttributeCache.get(ci_type.unique_id) value_table = TableMap(attr=unique_key).table - v = value_table.get_by(attr_id=unique_key.id, - value=unique_value, - to_dict=False, - first=True) \ - or abort(404, ErrFormat.not_found) + v = (value_table.get_by(attr_id=unique_key.id, value=unique_value, to_dict=False, first=True) or + abort(404, ErrFormat.not_found)) ci = CI.get_by_id(v.ci_id) or abort(404, ErrFormat.ci_not_found.format("id={}".format(v.ci_id))) @@ -528,6 +599,7 @@ def get_heartbeat(cls, **kwargs): result = [(i.get("hostname"), i.get("private_ip")[0], i.get("ci_type"), heartbeat_dict.get(i.get("_id"))) for i in res if i.get("private_ip")] + return numfound, result @staticmethod @@ -569,10 +641,13 @@ def _get_cis_from_db(ci_ids, ret_key=RetKey.NAME, fields=None, value_tables=None _fields = list() for field in fields: attr = AttributeCache.get(field) - if attr is not None: + if attr is not None and not attr.is_password: _fields.append(str(attr.id)) filter_fields_sql = "WHERE A.attr_id in ({0})".format(",".join(_fields)) + ci2pos = {int(_id): _pos for _pos, _id in enumerate(ci_ids)} + res = [None] * len(ci_ids) + ci_ids = ",".join(map(str, ci_ids)) if value_tables is None: value_tables = ValueTypeMap.table_name.values() @@ -583,11 +658,10 @@ def _get_cis_from_db(ci_ids, ret_key=RetKey.NAME, fields=None, value_tables=None # current_app.logger.debug(query_sql) cis = db.session.execute(query_sql).fetchall() ci_set = set() - res = list() ci_dict = dict() unique_id2obj = dict() excludes = excludes and set(excludes) - for ci_id, type_id, attr_id, attr_name, attr_alias, value, value_type, is_list in cis: + for ci_id, type_id, attr_id, attr_name, attr_alias, value, value_type, is_list, is_password in cis: if not fields and excludes and (attr_name in excludes or attr_alias in excludes): continue @@ -603,7 +677,7 @@ def _get_cis_from_db(ci_ids, ret_key=RetKey.NAME, fields=None, value_tables=None ci_dict["unique"] = unique_id2obj[ci_type.unique_id] and unique_id2obj[ci_type.unique_id].name ci_dict["unique_alias"] = unique_id2obj[ci_type.unique_id] and unique_id2obj[ci_type.unique_id].alias ci_set.add(ci_id) - res.append(ci_dict) + res[ci2pos[ci_id]] = ci_dict if ret_key == RetKey.NAME: attr_key = attr_name @@ -614,11 +688,14 @@ def _get_cis_from_db(ci_ids, ret_key=RetKey.NAME, fields=None, value_tables=None else: return abort(400, ErrFormat.argument_invalid.format("ret_key")) - value = ValueTypeMap.serialize2[value_type](value) - if is_list: - ci_dict.setdefault(attr_key, []).append(value) + if is_password and value: + ci_dict[attr_key] = PASSWORD_DEFAULT_SHOW else: - ci_dict[attr_key] = value + value = ValueTypeMap.serialize2[value_type](value) + if is_list: + ci_dict.setdefault(attr_key, []).append(value) + else: + ci_dict[attr_key] = value return res @@ -647,8 +724,87 @@ def get_cis_by_ids(cls, ci_ids, ret_key=RetKey.NAME, return res current_app.logger.warning("cache not hit...............") + return cls._get_cis_from_db(ci_ids, ret_key, fields, value_tables, excludes=excludes) + @classmethod + def save_password(cls, ci_id, attr_id, value, record_id, type_id): + changed = None + encrypt_value = None + value_table = ValueTypeMap.table[ValueTypeEnum.PASSWORD] + if current_app.config.get('SECRETS_ENGINE') == 'inner': + if value: + encrypt_value, status = InnerCrypt().encrypt(value) + if not status: + current_app.logger.error('save password failed: {}'.format(encrypt_value)) + return abort(400, ErrFormat.password_save_failed.format(encrypt_value)) + else: + encrypt_value = PASSWORD_DEFAULT_SHOW + + existed = value_table.get_by(ci_id=ci_id, attr_id=attr_id, first=True, to_dict=False) + if existed is None: + if value: + value_table.create(ci_id=ci_id, attr_id=attr_id, value=encrypt_value) + changed = [(ci_id, attr_id, OperateType.ADD, '', PASSWORD_DEFAULT_SHOW, type_id)] + elif existed.value != encrypt_value: + if value: + existed.update(ci_id=ci_id, attr_id=attr_id, value=encrypt_value) + changed = [(ci_id, attr_id, OperateType.UPDATE, PASSWORD_DEFAULT_SHOW, PASSWORD_DEFAULT_SHOW, type_id)] + else: + existed.delete() + changed = [(ci_id, attr_id, OperateType.DELETE, PASSWORD_DEFAULT_SHOW, '', type_id)] + + if current_app.config.get('SECRETS_ENGINE') == 'vault': + vault = VaultClient(current_app.config.get('VAULT_URL'), current_app.config.get('VAULT_TOKEN')) + if value: + try: + vault.update("/{}/{}".format(ci_id, attr_id), dict(v=value)) + except Exception as e: + current_app.logger.error('save password to vault failed: {}'.format(e)) + return abort(400, ErrFormat.password_save_failed.format('write vault failed')) + else: + try: + vault.delete("/{}/{}".format(ci_id, attr_id)) + except Exception as e: + current_app.logger.warning('delete password to vault failed: {}'.format(e)) + + if changed is not None: + return AttributeValueManager.write_change2(changed, record_id) + + @classmethod + def load_password(cls, ci_id, attr_id): + ci = CI.get_by_id(ci_id) or abort(404, ErrFormat.ci_not_found.format(ci_id)) + + limit_attrs = cls._valid_ci_for_no_read(ci, ci.ci_type) + if limit_attrs: + attr = AttributeCache.get(attr_id) + if attr and attr.name not in limit_attrs: + return abort(403, ErrFormat.no_permission2) + + if current_app.config.get('SECRETS_ENGINE', 'inner') == 'inner': + value_table = ValueTypeMap.table[ValueTypeEnum.PASSWORD] + v = value_table.get_by(ci_id=ci_id, attr_id=attr_id, first=True, to_dict=False) + + v = v and v.value + if not v: + return + + decrypt_value, status = InnerCrypt().decrypt(v) + if not status: + current_app.logger.error('load password failed: {}'.format(decrypt_value)) + return abort(400, ErrFormat.password_load_failed.format(decrypt_value)) + + return decrypt_value + + elif current_app.config.get('SECRETS_ENGINE') == 'vault': + vault = VaultClient(current_app.config.get('VAULT_URL'), current_app.config.get('VAULT_TOKEN')) + data, status = vault.read("/{}/{}".format(ci_id, attr_id)) + if not status: + current_app.logger.error('read password from vault failed: {}'.format(data)) + return abort(400, ErrFormat.password_load_failed.format(data)) + + return data.get('v') + class CIRelationManager(object): """ @@ -672,6 +828,7 @@ def get_children(cls, ci_id, ret_key=RetKey.NAME): ci_type = CITypeCache.get(type_id) children = CIManager.get_cis_by_ids(list(map(str, ci_type2ci_ids[type_id])), ret_key=ret_key) res[ci_type.name] = children + return res @staticmethod @@ -735,34 +892,48 @@ def get_first_cis(cls, second_ci, relation_type_id=None, page=1, per_page=None): @classmethod def get_ancestor_ids(cls, ci_ids, level=1): - for _ in range(level): - cis = db.session.query(CIRelation.first_ci_id).filter( + level2ids = dict() + for _level in range(1, level + 1): + cis = db.session.query(CIRelation.first_ci_id, CIRelation.ancestor_ids).filter( CIRelation.second_ci_id.in_(ci_ids)).filter(CIRelation.deleted.is_(False)) ci_ids = [i.first_ci_id for i in cis] + level2ids[_level + 1] = {int(i.ancestor_ids.split(',')[-1]) for i in cis if i.ancestor_ids} - return ci_ids + return ci_ids, level2ids @staticmethod - def _check_constraint(first_ci_id, second_ci_id, type_relation): + def _check_constraint(first_ci_id, first_type_id, second_ci_id, second_type_id, type_relation): + db.session.remove() if type_relation.constraint == ConstraintEnum.Many2Many: return - first_existed = CIRelation.get_by(first_ci_id=first_ci_id, relation_type_id=type_relation.relation_type_id) - second_existed = CIRelation.get_by(second_ci_id=second_ci_id, relation_type_id=type_relation.relation_type_id) - if type_relation.constraint == ConstraintEnum.One2One and (first_existed or second_existed): - return abort(400, ErrFormat.relation_constraint.format("1对1")) + first_existed = CIRelation.get_by(first_ci_id=first_ci_id, + relation_type_id=type_relation.relation_type_id, to_dict=False) + second_existed = CIRelation.get_by(second_ci_id=second_ci_id, + relation_type_id=type_relation.relation_type_id, to_dict=False) + if type_relation.constraint == ConstraintEnum.One2One: + for i in first_existed: + if i.second_ci.type_id == second_type_id: + return abort(400, ErrFormat.relation_constraint.format("1-1")) + + for i in second_existed: + if i.first_ci.type_id == first_type_id: + return abort(400, ErrFormat.relation_constraint.format("1-1")) - if type_relation.constraint == ConstraintEnum.One2Many and second_existed: - return abort(400, ErrFormat.relation_constraint.format("1对多")) + if type_relation.constraint == ConstraintEnum.One2Many: + for i in second_existed: + if i.first_ci.type_id == first_type_id: + return abort(400, ErrFormat.relation_constraint.format("1-N")) @classmethod - def add(cls, first_ci_id, second_ci_id, more=None, relation_type_id=None): + def add(cls, first_ci_id, second_ci_id, more=None, relation_type_id=None, ancestor_ids=None): first_ci = CIManager.confirm_ci_existed(first_ci_id) second_ci = CIManager.confirm_ci_existed(second_ci_id) existed = CIRelation.get_by(first_ci_id=first_ci_id, second_ci_id=second_ci_id, + ancestor_ids=ancestor_ids, to_dict=False, first=True) if existed is not None: @@ -792,15 +963,18 @@ def add(cls, first_ci_id, second_ci_id, more=None, relation_type_id=None): else: type_relation = CITypeRelation.get_by_id(relation_type_id) - cls._check_constraint(first_ci_id, second_ci_id, type_relation) + with Lock("ci_relation_add_{}_{}".format(first_ci.type_id, second_ci.type_id), need_lock=True): + + cls._check_constraint(first_ci_id, first_ci.type_id, second_ci_id, second_ci.type_id, type_relation) - existed = CIRelation.create(first_ci_id=first_ci_id, - second_ci_id=second_ci_id, - relation_type_id=relation_type_id) + existed = CIRelation.create(first_ci_id=first_ci_id, + second_ci_id=second_ci_id, + relation_type_id=relation_type_id, + ancestor_ids=ancestor_ids) - CIRelationHistoryManager().add(existed, OperateType.ADD) + CIRelationHistoryManager().add(existed, OperateType.ADD) - ci_relation_cache.apply_async(args=(first_ci_id, second_ci_id), queue=CMDB_QUEUE) + ci_relation_cache.apply_async(args=(first_ci_id, second_ci_id, ancestor_ids), queue=CMDB_QUEUE) if more is not None: existed.upadte(more=more) @@ -824,50 +998,230 @@ def delete(cr_id): his_manager = CIRelationHistoryManager() his_manager.add(cr, operate_type=OperateType.DELETE) - ci_relation_delete.apply_async(args=(cr.first_ci_id, cr.second_ci_id), queue=CMDB_QUEUE) + ci_relation_delete.apply_async(args=(cr.first_ci_id, cr.second_ci_id, cr.ancestor_ids), queue=CMDB_QUEUE) return cr_id @classmethod - def delete_2(cls, first_ci_id, second_ci_id): + def delete_2(cls, first_ci_id, second_ci_id, ancestor_ids=None): cr = CIRelation.get_by(first_ci_id=first_ci_id, second_ci_id=second_ci_id, + ancestor_ids=ancestor_ids, to_dict=False, first=True) - ci_relation_delete.apply_async(args=(first_ci_id, second_ci_id), queue=CMDB_QUEUE) + ci_relation_delete.apply_async(args=(first_ci_id, second_ci_id, ancestor_ids), queue=CMDB_QUEUE) - return cls.delete(cr.id) + return cr and cls.delete(cr.id) @classmethod - def batch_update(cls, ci_ids, parents, children): + def batch_update(cls, ci_ids, parents, children, ancestor_ids=None): """ only for many to one :param ci_ids: :param parents: :param children: + :param ancestor_ids: :return: """ - if parents is not None and isinstance(parents, list): + if isinstance(parents, list): for parent_id in parents: for ci_id in ci_ids: - cls.add(parent_id, ci_id) + cls.add(parent_id, ci_id, ancestor_ids=ancestor_ids) - if children is not None and isinstance(children, list): + if isinstance(children, list): for child_id in children: for ci_id in ci_ids: - cls.add(ci_id, child_id) + cls.add(ci_id, child_id, ancestor_ids=ancestor_ids) @classmethod - def batch_delete(cls, ci_ids, parents): + def batch_delete(cls, ci_ids, parents, ancestor_ids=None): """ only for many to one :param ci_ids: :param parents: + :param ancestor_ids: :return: """ - if parents is not None and isinstance(parents, list): + if isinstance(parents, list): for parent_id in parents: for ci_id in ci_ids: - cls.delete_2(parent_id, ci_id) + cls.delete_2(parent_id, ci_id, ancestor_ids=ancestor_ids) + + +class CITriggerManager(object): + @staticmethod + def get(type_id): + db.session.remove() + return CITypeTrigger.get_by(type_id=type_id, to_dict=True) + + @staticmethod + def _update_old_attr_value(record_id, ci_dict): + attr_history = AttributeHistory.get_by(record_id=record_id, to_dict=False) + attr_dict = dict() + for attr_h in attr_history: + attr_dict['old_{}'.format(AttributeCache.get(attr_h.attr_id).name)] = attr_h.old + + ci_dict.update({'old_{}'.format(k): ci_dict[k] for k in ci_dict}) + + ci_dict.update(attr_dict) + + @classmethod + def _exec_webhook(cls, operate_type, webhook, ci_dict, trigger_id, trigger_name, record_id, ci_id=None, app=None): + app = app or current_app + + with app.app_context(): + if operate_type == OperateType.UPDATE: + cls._update_old_attr_value(record_id, ci_dict) + + if ci_id is not None: + ci_dict = CIManager().get_ci_by_id_from_db(ci_id, need_children=False, use_master=False) + + try: + response = webhook_request(webhook, ci_dict).text + is_ok = True + except Exception as e: + current_app.logger.warning("exec webhook failed: {}".format(e)) + response = e + is_ok = False + + CITriggerHistoryManager.add(operate_type, + record_id, + ci_dict.get('_id'), + trigger_id, + trigger_name, + is_ok=is_ok, + webhook=response) + + return is_ok + + @classmethod + def _exec_notify(cls, operate_type, notify, ci_dict, trigger_id, trigger_name, record_id, ci_id=None, app=None): + app = app or current_app + + with app.app_context(): + + if ci_id is not None: + ci_dict = CIManager().get_ci_by_id_from_db(ci_id, need_children=False, use_master=False) + + if operate_type == OperateType.UPDATE: + cls._update_old_attr_value(record_id, ci_dict) + + is_ok = True + response = '' + for method in (notify.get('method') or []): + try: + res = notify_send(notify.get('subject'), notify.get('body'), [method], + notify.get('tos'), ci_dict) + response = "{}\n{}".format(response, res) + except Exception as e: + current_app.logger.warning("send notify failed: {}".format(e)) + response = "{}\n{}".format(response, e) + is_ok = False + + CITriggerHistoryManager.add(operate_type, + record_id, + ci_dict.get('_id'), + trigger_id, + trigger_name, + is_ok=is_ok, + notify=response.strip()) + + return is_ok + + @staticmethod + def ci_filter(ci_id, other_filter): + from api.lib.cmdb.search import SearchError + from api.lib.cmdb.search.ci import search + + query = "{},_id:{}".format(other_filter, ci_id) + + try: + _, _, _, _, numfound, _ = search(query).search() + return numfound + except SearchError as e: + current_app.logger.warning("ci search failed: {}".format(e)) + + @classmethod + def fire(cls, operate_type, ci_dict, record_id): + type_id = ci_dict.get('_type') + triggers = cls.get(type_id) or [] + + for trigger in triggers: + option = trigger['option'] + if not option.get('enable'): + continue + + if option.get('filter') and not cls.ci_filter(ci_dict.get('_id'), option['filter']): + continue + + if option.get('attr_ids') and isinstance(option['attr_ids'], list): + if not (set(option['attr_ids']) & + set([i.attr_id for i in AttributeHistory.get_by(record_id=record_id, to_dict=False)])): + continue + + if option.get('action') == operate_type: + cls.fire_by_trigger(trigger, operate_type, ci_dict, record_id) + + @classmethod + def fire_by_trigger(cls, trigger, operate_type, ci_dict, record_id=None): + option = trigger['option'] + + if option.get('webhooks'): + cls._exec_webhook(operate_type, option['webhooks'], ci_dict, trigger['id'], + option.get('name'), record_id) + + elif option.get('notifies'): + cls._exec_notify(operate_type, option['notifies'], ci_dict, trigger['id'], + option.get('name'), record_id) + + @classmethod + def waiting_cis(cls, trigger): + now = datetime.datetime.today() + + config = trigger.option.get('notifies') or {} + + delta_time = datetime.timedelta(days=(config.get('before_days', 0) or 0)) + + attr = AttributeCache.get(trigger.attr_id) + + value_table = TableMap(attr=attr).table + + values = value_table.get_by(attr_id=attr.id, to_dict=False) + + result = [] + for v in values: + if (isinstance(v.value, (datetime.date, datetime.datetime)) and + (v.value - delta_time).strftime('%Y%m%d') == now.strftime("%Y%m%d")): + + if trigger.option.get('filter') and not cls.ci_filter(v.ci_id, trigger.option['filter']): + continue + + result.append(v) + + return result + + @classmethod + def trigger_notify(cls, trigger, ci): + """ + only for date attribute + :param trigger: + :param ci: + :return: + """ + if (trigger.option.get('notifies', {}).get('notify_at') == datetime.datetime.now().strftime("%H:%M") or + not trigger.option.get('notifies', {}).get('notify_at')): + + if trigger.option.get('webhooks'): + threading.Thread(target=cls._exec_webhook, args=( + None, trigger.option['webhooks'], None, trigger.id, trigger.option.get('name'), None, ci.ci_id, + current_app._get_current_object())).start() + elif trigger.option.get('notifies'): + threading.Thread(target=cls._exec_notify, args=( + None, trigger.option['notifies'], None, trigger.id, trigger.option.get('name'), None, ci.ci_id, + current_app._get_current_object())).start() + + return True + + return False diff --git a/cmdb-api/api/lib/cmdb/ci_type.py b/cmdb-api/api/lib/cmdb/ci_type.py index a88a16b8..e241da8b 100644 --- a/cmdb-api/api/lib/cmdb/ci_type.py +++ b/cmdb-api/api/lib/cmdb/ci_type.py @@ -1,11 +1,14 @@ -# -*- coding:utf-8 -*- +# -*- coding:utf-8 -*- import copy -import datetime +import toposort from flask import abort from flask import current_app -from flask import g +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 @@ -16,18 +19,23 @@ from api.lib.cmdb.const import CITypeOperateType from api.lib.cmdb.const import CMDB_QUEUE from api.lib.cmdb.const import ConstraintEnum -from api.lib.cmdb.const import PermEnum, ResourceTypeEnum, RoleEnum +from api.lib.cmdb.const import PermEnum +from api.lib.cmdb.const import ResourceTypeEnum +from api.lib.cmdb.const import RoleEnum from api.lib.cmdb.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.utils import TableMap from api.lib.cmdb.value import AttributeValueManager from api.lib.decorator import kwargs_required from api.lib.perm.acl.acl import ACLManager from api.lib.perm.acl.acl import is_app_admin from api.models.cmdb import Attribute +from api.models.cmdb import AutoDiscoveryCI +from api.models.cmdb import AutoDiscoveryCIType from api.models.cmdb import CI +from api.models.cmdb import CIFilterPerms from api.models.cmdb import CIType from api.models.cmdb import CITypeAttribute from api.models.cmdb import CITypeAttributeGroup @@ -37,6 +45,9 @@ 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 PreferenceRelationView +from api.models.cmdb import PreferenceSearchOption from api.models.cmdb import PreferenceShowAttributes from api.models.cmdb import PreferenceTreeView from api.models.cmdb import RelationType @@ -54,6 +65,7 @@ def __init__(self): @staticmethod def get_name_by_id(type_id): ci_type = CITypeCache.get(type_id) + return ci_type and ci_type.name @staticmethod @@ -65,13 +77,14 @@ def check_is_existed(key): @staticmethod def get_ci_types(type_name=None): resources = None - if current_app.config.get('USE_ACL') and not is_app_admin(): - resources = set([i.get('name') for i in ACLManager().get_resources("CIType")]) + if current_app.config.get('USE_ACL') and not is_app_admin('cmdb'): + 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) 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 if resources is None or type_dict['name'] in resources: res.append(type_dict) @@ -104,11 +117,11 @@ def _validate_unique(type_id=None, name=None, alias=None): @classmethod @kwargs_required("name") def add(cls, **kwargs): - from api.lib.cmdb.const import L_TYPE - if L_TYPE and len(CIType.get_by()) > L_TYPE * 2: - return abort(400, ErrFormat.limit_ci_type.format(L_TYPE * 2)) + 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) + unique_key = kwargs.pop("unique_key", None) or kwargs.pop("unique_id", None) unique_key = AttributeCache.get(unique_key) or abort(404, ErrFormat.unique_key_not_define) kwargs["alias"] = kwargs["name"] if not kwargs.get("alias") else kwargs["alias"] @@ -117,7 +130,7 @@ def add(cls, **kwargs): cls._validate_unique(alias=kwargs['alias']) kwargs["unique_id"] = unique_key.id - kwargs['uid'] = g.user.uid + kwargs['uid'] = current_user.uid ci_type = CIType.create(**kwargs) CITypeAttributeManager.add(ci_type.id, [unique_key.id], is_required=True) @@ -125,13 +138,17 @@ def add(cls, **kwargs): 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, permissions=[PermEnum.READ]) ACLManager().grant_resource_to_role(ci_type.name, - g.user.username, + current_user.username, ResourceTypeEnum.CI) CITypeHistoryManager.add(CITypeOperateType.ADD, ci_type.id, change=ci_type.to_dict()) @@ -178,32 +195,41 @@ def update(cls, type_id, **kwargs): def set_enabled(cls, type_id, enabled=True): ci_type = cls.check_is_existed(type_id) ci_type.update(enabled=enabled) + return type_id @classmethod def delete(cls, type_id): ci_type = cls.check_is_existed(type_id) - if ci_type.uid and ci_type.uid != g.user.uid: + if ci_type.uid and ci_type.uid != current_user.uid: return abort(403, ErrFormat.only_owner_can_delete) if CI.get_by(type_id=type_id, first=True, to_dict=False) is not None: return abort(400, ErrFormat.ci_exists_and_cannot_delete_type) + relation_views = PreferenceRelationView.get_by(to_dict=False) + for rv in relation_views: + for item in (rv.cr_ids or []): + 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() + item.soft_delete(commit=False) for item in CITypeRelation.get_by(child_id=type_id, to_dict=False): - item.soft_delete() + item.soft_delete(commit=False) - for item in PreferenceTreeView.get_by(type_id=type_id, to_dict=False): - item.soft_delete() + for table in [PreferenceTreeView, PreferenceShowAttributes, PreferenceSearchOption, CustomDashboard, + CITypeGroupItem, CITypeAttributeGroup, CITypeAttribute, CITypeUniqueConstraint, CITypeTrigger, + AutoDiscoveryCIType, CIFilterPerms]: + for item in table.get_by(type_id=type_id, to_dict=False): + item.soft_delete(commit=False) - for item in PreferenceShowAttributes.get_by(type_id=type_id, to_dict=False): - item.soft_delete() + for item in AutoDiscoveryCI.get_by(type_id=type_id, to_dict=False): + item.delete(commit=False) - for item in CITypeGroupItem.get_by(type_id=type_id, to_dict=False): - item.soft_delete() + db.session.commit() ci_type.soft_delete() @@ -228,7 +254,6 @@ def get(need_other=None, config_required=True): 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: @@ -254,20 +279,24 @@ def get(need_other=None, config_required=True): @staticmethod def add(name): CITypeGroup.get_by(name=name, first=True) and abort(400, ErrFormat.ci_type_group_exists.format(name)) + return CITypeGroup.create(name=name) @staticmethod def update(gid, name, type_ids): """ update part - :param gid: - :param name: - :param type_ids: - :return: + :param gid: + :param name: + :param type_ids: + :return: """ existed = CITypeGroup.get_by_id(gid) or abort( 404, ErrFormat.ci_type_group_not_found.format("id={}".format(gid))) - 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]) @@ -320,28 +349,63 @@ class CITypeAttributeManager(object): def __init__(self): pass + @staticmethod + def get_attr_name(ci_type_name, key): + ci_type = CITypeCache.get(ci_type_name) + if ci_type is None: + return + + for i in CITypeAttributesCache.get(ci_type.id): + attr = AttributeCache.get(i.attr_id) + if attr and (attr.name == key or attr.alias == key): + 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)] @staticmethod - def get_attributes_by_type_id(type_id, choice_web_hook_parse=True): + def get_attributes_by_type_id(type_id, choice_web_hook_parse=True, choice_other_parse=True): has_config_perm = ACLManager('cmdb').has_permission( CITypeManager.get_name_by_id(type_id), ResourceTypeEnum.CI, PermEnum.CONFIG) attrs = CITypeAttributesCache.get(type_id) result = list() for attr in sorted(attrs, key=lambda x: (x.order, x.id)): - attr_dict = AttributeManager().get_attribute(attr.attr_id, choice_web_hook_parse) + attr_dict = AttributeManager().get_attribute(attr.attr_id, choice_web_hook_parse, choice_other_parse) attr_dict["is_required"] = attr.is_required attr_dict["order"] = attr.order attr_dict["default_show"] = attr.default_show if not has_config_perm: attr_dict.pop('choice_web_hook', None) + attr_dict.pop('choice_other', None) result.append(attr_dict) + return result + @staticmethod + def get_common_attributes(type_ids): + has_config_perm = False + for type_id in type_ids: + has_config_perm |= ACLManager('cmdb').has_permission( + CITypeManager.get_name_by_id(type_id), ResourceTypeEnum.CI, PermEnum.CONFIG) + + result = CITypeAttribute.get_by(__func_in___key_type_id=list(map(int, type_ids)), to_dict=False) + attr2types = {} + for i in result: + attr2types.setdefault(i.attr_id, []).append(i.type_id) + + attrs = [] + for attr_id in attr2types: + if len(attr2types[attr_id]) == len(type_ids): + attr = AttributeManager().get_attribute_by_id(attr_id) + if not has_config_perm: + attr.pop('choice_web_hook', None) + attrs.append(attr) + + return attrs + @staticmethod def _check(type_id, attr_ids): ci_type = CITypeManager.check_is_existed(type_id) @@ -358,10 +422,10 @@ def _check(type_id, attr_ids): def add(cls, type_id, attr_ids=None, **kwargs): """ add attributes to CIType - :param type_id: + :param type_id: :param attr_ids: list - :param kwargs: - :return: + :param kwargs: + :return: """ attr_ids = list(set(attr_ids)) @@ -388,9 +452,9 @@ def add(cls, type_id, attr_ids=None, **kwargs): def update(cls, type_id, attributes): """ update attributes to CIType - :param type_id: + :param type_id: :param attributes: list - :return: + :return: """ cls._check(type_id, [i.get('attr_id') for i in attributes]) @@ -418,9 +482,9 @@ def update(cls, type_id, attributes): def delete(cls, type_id, attr_ids=None): """ delete attributes from CIType - :param type_id: + :param type_id: :param attr_ids: list - :return: + :return: """ from api.tasks.cmdb import ci_cache @@ -449,7 +513,7 @@ def delete(cls, type_id, attr_ids=None): for ci in CI.get_by(type_id=type_id, to_dict=False): AttributeValueManager.delete_attr_value(attr_id, ci.id) - ci_cache.apply_async([ci.id], queue=CMDB_QUEUE) + ci_cache.apply_async(args=(ci.id, None, None), queue=CMDB_QUEUE) CITypeAttributeCache.clean(type_id, attr_id) @@ -482,7 +546,7 @@ def transfer(cls, type_id, _from, _to): CITypeAttributesCache.clean(type_id) from api.tasks.cmdb import ci_type_attribute_order_rebuild - ci_type_attribute_order_rebuild.apply_async(args=(type_id,), queue=CMDB_QUEUE) + ci_type_attribute_order_rebuild.apply_async(args=(type_id, current_user.uid), queue=CMDB_QUEUE) class CITypeRelationManager(object): @@ -525,8 +589,14 @@ def _wrap_relation_type_dict(type_id, relation_inst): 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 + return ci_type_dict @classmethod @@ -535,6 +605,23 @@ def get_children(cls, parent_id): return [cls._wrap_relation_type_dict(child.child_id, child) for child in children] + @classmethod + def recursive_level2children(cls, parent_id): + result = dict() + + def get_children(_id, level): + children = CITypeRelation.get_by(parent_id=_id, to_dict=False) + if children: + result.setdefault(level + 1, []).extend([i.child.to_dict() for i in children]) + + for i in children: + if i.child_id != _id: + get_children(i.child_id, level + 1) + + get_children(parent_id, 0) + + return result + @classmethod def get_parents(cls, child_id): parents = CITypeRelation.get_by(child_id=child_id, to_dict=False) @@ -557,6 +644,27 @@ def add(cls, parent, child, relation_type_id, constraint=ConstraintEnum.One2Many p = CITypeManager.check_is_existed(parent) c = CITypeManager.check_is_existed(child) + rels = {} + for i in CITypeRelation.get_by(to_dict=False): + rels.setdefault(i.child_id, set()).add(i.parent_id) + rels.setdefault(c.id, set()).add(p.id) + + try: + toposort_flatten(rels) + except toposort.CircularDependencyError as e: + current_app.logger.warning(str(e)) + return abort(400, ErrFormat.circular_dependency_error) + + if constraint == ConstraintEnum.Many2Many: + other_c = CITypeRelation.get_by(parent_id=p.id, constraint=ConstraintEnum.Many2Many, + to_dict=False, first=True) + other_p = CITypeRelation.get_by(child_id=c.id, constraint=ConstraintEnum.Many2Many, + to_dict=False, first=True) + if other_c and other_c.child_id != c.id: + return abort(400, ErrFormat.m2m_relation_constraint.format(p.name, other_c.child.name)) + if other_p and other_p.parent_id != p.id: + return abort(400, ErrFormat.m2m_relation_constraint.format(other_p.parent.name, c.name)) + existed = cls._get(p.id, c.id) if existed is not None: existed.update(relation_type_id=relation_type_id, @@ -575,7 +683,7 @@ def add(cls, parent, child, relation_type_id, constraint=ConstraintEnum.One2Many ResourceTypeEnum.CI_TYPE_RELATION, permissions=[PermEnum.READ]) ACLManager().grant_resource_to_role(resource_name, - g.user.username, + current_user.username, ResourceTypeEnum.CI_TYPE_RELATION) CITypeHistoryManager.add(CITypeOperateType.ADD_RELATION, p.id, @@ -585,8 +693,8 @@ def add(cls, parent, child, relation_type_id, constraint=ConstraintEnum.One2Many @classmethod def delete(cls, _id): - ctr = CITypeRelation.get_by_id(_id) or \ - abort(404, ErrFormat.ci_type_relation_not_found.format("id={}".format(_id))) + ctr = (CITypeRelation.get_by_id(_id) or + abort(404, ErrFormat.ci_type_relation_not_found.format("id={}".format(_id)))) ctr.soft_delete() CITypeHistoryManager.add(CITypeOperateType.DELETE_RELATION, ctr.parent_id, @@ -606,6 +714,24 @@ def delete_2(cls, parent, child): cls.delete(ctr.id) + @staticmethod + def get_level2constraint(root_id, level): + level = level + 1 if level == 1 else level + ci = CI.get_by_id(root_id) + if ci is None: + return dict() + + root_id = ci.type_id + level2constraint = dict() + for lv in range(1, int(level) + 1): + for i in CITypeRelation.get_by(parent_id=root_id, to_dict=False): + if i.constraint == ConstraintEnum.Many2Many: + root_id = i.child_id + level2constraint[lv] = ConstraintEnum.Many2Many + break + + return level2constraint + class CITypeAttributeGroupManager(object): cls = CITypeAttributeGroup @@ -617,7 +743,7 @@ def get_by_type_id(type_id, need_other=False): grouped = list() 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} for group in groups: items = CITypeAttributeGroupItem.get_by(group_id=group["id"], to_dict=False) @@ -640,6 +766,7 @@ def create_or_update(type_id, name, attr_order, group_order=0, is_update=False): :param name: :param group_order: group order :param attr_order: + :param is_update: :return: """ existed = CITypeAttributeGroup.get_by(type_id=type_id, name=name, first=True, to_dict=False) @@ -680,8 +807,8 @@ def update(cls, group_id, name, attr_order, group_order=0): @staticmethod def delete(group_id): - group = CITypeAttributeGroup.get_by_id(group_id) \ - or abort(404, ErrFormat.ci_type_attribute_group_not_found.format("id={}".format(group_id))) + group = (CITypeAttributeGroup.get_by_id(group_id) or + abort(404, ErrFormat.ci_type_attribute_group_not_found.format("id={}".format(group_id)))) group.soft_delete() items = CITypeAttributeGroupItem.get_by(group_id=group_id, to_dict=False) @@ -777,220 +904,327 @@ def transfer(cls, type_id, _from, _to): CITypeAttributesCache.clean(type_id) from api.tasks.cmdb import ci_type_attribute_order_rebuild - ci_type_attribute_order_rebuild.apply_async(args=(type_id,), queue=CMDB_QUEUE) + ci_type_attribute_order_rebuild.apply_async(args=(type_id, current_user.uid), queue=CMDB_QUEUE) class CITypeTemplateManager(object): @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: + __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'), + ) 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): - 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, - g.user.username, - ResourceTypeEnum.CI) + _id = id2obj_dicts[updated_id].pop('id', None) - else: - id2existed[updated_id].update(flush=True, **id2obj_dicts[updated_id]) + 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) + id_map[_id] = id2existed[updated_id].id - CITypeCache.clean(deleted_id) + if cls == Attribute: + AttributeCache.clean(id2existed[updated_id]) - CITypeHistoryManager.add(CITypeOperateType.DELETE, deleted_id, change=id2existed[deleted_id].to_dict()) + if cls == CIType: + CITypeCache.clean(id2existed[updated_id].id) - 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): + return id_map + + def _import_attributes(self, type2attributes): + attributes = [attr for type_id in type2attributes for attr in type2attributes[type_id]] + attrs = [] + for i in copy.deepcopy(attributes): + 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) + choice_value = i.pop('choice_value', None) + if not choice_value: + i['is_choice'] = False + + attrs.append((i, choice_value)) + + attr_id_map = self.__import(Attribute, [i[0] for i in copy.deepcopy(attrs)]) + + for i, choice_value in attrs: + 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 - self.__import(CIType, ci_types) + return self.__import(CIType, ci_types) - def _import_ci_type_groups(self, ci_type_groups): + 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) - self.__import(CITypeGroup, _ci_type_groups) + group_id_map = 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) + 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): - self.__import(RelationType, relation_types) + return self.__import(RelationType, relation_types) - def _import_ci_type_relations(self, ci_type_relations): + @staticmethod + 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) - self.__import(CITypeRelation, ci_type_relations) - - def _import_attributes(self, type2attributes): - attributes = [attr for type_id in type2attributes for attr in type2attributes[type_id]] - attrs = [] - for i in copy.deepcopy(attributes): - i.pop('default_show', None) - i.pop('is_required', None) - i.pop('order', None) - choice_value = i.pop('choice_value', None) - - attrs.append((i, choice_value)) - - self.__import(Attribute, [i[0] for i in attrs]) + 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']) - for i, choice_value in attrs: - if choice_value: - AttributeManager.add_choice_values(i['id'], i['value_type'], choice_value) + 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): - # add type attribute + 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) + + handled.add(attr['name']) + + try: + db.session.commit() + except Exception as e: + db.session.rollback() + raise Exception(str(e)) - # delete - for i in existed: - if i.attr_id not in new_attr_ids: - i.soft_delete() + 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) + 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['uid'] = g.user.uid - try: - AutoDiscoveryCITypeCRUD.add(**rule) - except: - pass + rule['uid'] = current_user.uid + + 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(), + 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 = [] @@ -1001,22 +1235,90 @@ def export_template(): 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 + + @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(), - 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, + ci_types=CITypeManager.get_ci_types(type_name=ci_type.name), + ci_type_auto_discovery_rules=list(), type2attributes=dict(), - type2attribute_group=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) + 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']) @@ -1090,16 +1392,18 @@ def delete(_id): class CITypeTriggerManager(object): @staticmethod - def get(type_id): - return CITypeTrigger.get_by(type_id=type_id, to_dict=True) + def get(type_id, to_dict=True): + return CITypeTrigger.get_by(type_id=type_id, to_dict=to_dict) @staticmethod - def add(type_id, attr_id, notify): - CITypeTrigger.get_by(type_id=type_id, attr_id=attr_id) and abort(400, ErrFormat.ci_type_trigger_duplicate) + def add(type_id, attr_id, option): + for i in CITypeTrigger.get_by(type_id=type_id, attr_id=attr_id, to_dict=False): + if i.option == option: + return abort(400, ErrFormat.ci_type_trigger_duplicate) - not isinstance(notify, dict) and abort(400, ErrFormat.argument_invalid.format("notify")) + not isinstance(option, dict) and abort(400, ErrFormat.argument_invalid.format("option")) - trigger = CITypeTrigger.create(type_id=type_id, attr_id=attr_id, notify=notify) + trigger = CITypeTrigger.create(type_id=type_id, attr_id=attr_id, option=option) CITypeHistoryManager.add(CITypeOperateType.ADD_TRIGGER, type_id, @@ -1109,12 +1413,12 @@ def add(type_id, attr_id, notify): return trigger.to_dict() @staticmethod - def update(_id, notify): - existed = CITypeTrigger.get_by_id(_id) or \ - abort(404, ErrFormat.ci_type_trigger_not_found.format("id={}".format(_id))) + def update(_id, attr_id, option): + existed = (CITypeTrigger.get_by_id(_id) or + abort(404, ErrFormat.ci_type_trigger_not_found.format("id={}".format(_id)))) existed2 = existed.to_dict() - new = existed.update(notify=notify) + new = existed.update(attr_id=attr_id or None, option=option, filter_none=False) CITypeHistoryManager.add(CITypeOperateType.UPDATE_TRIGGER, existed.type_id, @@ -1125,8 +1429,8 @@ def update(_id, notify): @staticmethod def delete(_id): - existed = CITypeTrigger.get_by_id(_id) or \ - abort(404, ErrFormat.ci_type_trigger_not_found.format("id={}".format(_id))) + existed = (CITypeTrigger.get_by_id(_id) or + abort(404, ErrFormat.ci_type_trigger_not_found.format("id={}".format(_id)))) existed.soft_delete() @@ -1134,35 +1438,3 @@ def delete(_id): existed.type_id, trigger_id=_id, change=existed.to_dict()) - - @staticmethod - def waiting_cis(trigger): - now = datetime.datetime.today() - - delta_time = datetime.timedelta(days=(trigger.notify.get('before_days', 0) or 0)) - - attr = AttributeCache.get(trigger.attr_id) - - value_table = TableMap(attr=attr).table - - values = value_table.get_by(attr_id=attr.id, to_dict=False) - - result = [] - for v in values: - if isinstance(v.value, (datetime.date, datetime.datetime)) and \ - (v.value - delta_time).strftime('%Y%m%d') == now.strftime("%Y%m%d"): - result.append(v) - - return result - - @staticmethod - def trigger_notify(trigger, ci): - if trigger.notify.get('notify_at') == datetime.datetime.now().strftime("%H:%M") or \ - not trigger.notify.get('notify_at'): - from api.tasks.cmdb import trigger_notify - - trigger_notify.apply_async(args=(trigger.notify, ci.ci_id), queue=CMDB_QUEUE) - - return True - - return False diff --git a/cmdb-api/api/lib/cmdb/const.py b/cmdb-api/api/lib/cmdb/const.py index 118e053f..31cc6e59 100644 --- a/cmdb-api/api/lib/cmdb/const.py +++ b/cmdb-api/api/lib/cmdb/const.py @@ -12,6 +12,8 @@ class ValueTypeEnum(BaseEnum): DATE = "4" TIME = "5" JSON = "6" + PASSWORD = TEXT + LINK = TEXT class ConstraintEnum(BaseEnum): @@ -67,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): @@ -98,6 +101,9 @@ class AttributeDefaultValueEnum(BaseEnum): CMDB_QUEUE = "one_cmdb_async" REDIS_PREFIX_CI = "ONE_CMDB" REDIS_PREFIX_CI_RELATION = "CMDB_CI_RELATION" +REDIS_PREFIX_CI_RELATION2 = "CMDB_CI_RELATION2" + +BUILTIN_KEYWORDS = {'id', '_id', 'ci_id', 'type', '_type', 'ci_type'} L_TYPE = None L_CI = None diff --git a/cmdb-api/api/lib/cmdb/custom_dashboard.py b/cmdb-api/api/lib/cmdb/custom_dashboard.py index 8153eec3..133769ac 100644 --- a/cmdb-api/api/lib/cmdb/custom_dashboard.py +++ b/cmdb-api/api/lib/cmdb/custom_dashboard.py @@ -14,6 +14,14 @@ class CustomDashboardManager(object): def get(): return sorted(CustomDashboard.get_by(to_dict=True), key=lambda x: (x["category"], x['order'])) + @staticmethod + def preview(**kwargs): + from api.lib.cmdb.cache import CMDBCounterCache + + res = CMDBCounterCache.update(kwargs, flush=False) + + return res + @staticmethod def add(**kwargs): from api.lib.cmdb.cache import CMDBCounterCache @@ -23,9 +31,9 @@ def add(**kwargs): new = CustomDashboard.create(**kwargs) - CMDBCounterCache.update(new.to_dict()) + res = CMDBCounterCache.update(new.to_dict()) - return new + return new, res @staticmethod def update(_id, **kwargs): @@ -35,9 +43,9 @@ def update(_id, **kwargs): new = existed.update(**kwargs) - CMDBCounterCache.update(new.to_dict()) + res = CMDBCounterCache.update(new.to_dict()) - return new + return new, res @staticmethod def batch_update(id2options): diff --git a/cmdb-api/api/lib/cmdb/history.py b/cmdb-api/api/lib/cmdb/history.py index 43474ec2..adc6d404 100644 --- a/cmdb-api/api/lib/cmdb/history.py +++ b/cmdb-api/api/lib/cmdb/history.py @@ -4,7 +4,7 @@ import json from flask import abort -from flask import g +from flask_login import current_user from api.extensions import db from api.lib.cmdb.cache import AttributeCache @@ -16,6 +16,7 @@ from api.models.cmdb import Attribute from api.models.cmdb import AttributeHistory from api.models.cmdb import CIRelationHistory +from api.models.cmdb import CITriggerHistory from api.models.cmdb import CITypeHistory from api.models.cmdb import CITypeTrigger from api.models.cmdb import CITypeUniqueConstraint @@ -134,7 +135,7 @@ def get_records_for_relation(start, end, username, page, page_size, operate_type 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 @@ -176,8 +177,8 @@ def get_by_ci_id(ci_id): def get_record_detail(record_id): from api.lib.cmdb.ci import CIManager - record = OperationRecord.get_by_id(record_id) or \ - abort(404, ErrFormat.record_not_found.format("id={}".format(record_id))) + record = (OperationRecord.get_by_id(record_id) or + abort(404, ErrFormat.record_not_found.format("id={}".format(record_id)))) username = UserCache.get(record.uid).nickname or UserCache.get(record.uid).username timestamp = record.created_at.strftime("%Y-%m-%d %H:%M:%S") @@ -201,7 +202,7 @@ def get_record_detail(record_id): @staticmethod def add(record_id, ci_id, history_list, type_id=None, flush=False, commit=True): if record_id is None: - record = OperationRecord.create(uid=g.user.uid, type_id=type_id) + record = OperationRecord.create(uid=current_user.uid, type_id=type_id) record_id = record.id for attr_id, operate_type, old, new in history_list or []: @@ -220,7 +221,7 @@ def add(record_id, ci_id, history_list, type_id=None, flush=False, commit=True): class CIRelationHistoryManager(object): @staticmethod def add(rel_obj, operate_type=OperateType.ADD): - record = OperationRecord.create(uid=g.user.uid) + record = OperationRecord.create(uid=current_user.uid) CIRelationHistory.create(relation_id=rel_obj.id, record_id=record.id, @@ -279,10 +280,75 @@ def add(operate_type, type_id, attr_id=None, trigger_id=None, unique_constraint_ for _type_id in type_ids: payload = dict(operate_type=operate_type, type_id=_type_id, - uid=g.user.uid, + uid=current_user.uid, attr_id=attr_id, trigger_id=trigger_id, unique_constraint_id=unique_constraint_id, change=change) CITypeHistory.create(**payload) + + +class CITriggerHistoryManager(object): + @staticmethod + def get(page, page_size, type_id=None, trigger_id=None, operate_type=None): + query = CITriggerHistory.get_by(only_query=True) + if type_id: + query = query.filter(CITriggerHistory.type_id == type_id) + + if trigger_id: + query = query.filter(CITriggerHistory.trigger_id == trigger_id) + + if operate_type: + query = query.filter(CITriggerHistory.operate_type == operate_type) + + numfound = query.count() + + query = query.order_by(CITriggerHistory.id.desc()) + result = query.offset((page - 1) * page_size).limit(page_size) + result = [i.to_dict() for i in result] + for res in result: + if res.get('trigger_id'): + trigger = CITypeTrigger.get_by_id(res['trigger_id']) + res['trigger'] = trigger and trigger.to_dict() + + return numfound, result + + @staticmethod + def get_by_ci_id(ci_id): + res = db.session.query(CITriggerHistory, CITypeTrigger).join( + CITypeTrigger, CITypeTrigger.id == CITriggerHistory.trigger_id).filter( + CITriggerHistory.ci_id == ci_id).order_by(CITriggerHistory.id.desc()) + + result = [] + id2trigger = dict() + for i in res: + hist = i.CITriggerHistory + item = dict(is_ok=hist.is_ok, + operate_type=hist.operate_type, + notify=hist.notify, + trigger_id=hist.trigger_id, + trigger_name=hist.trigger_name, + webhook=hist.webhook, + created_at=hist.created_at.strftime('%Y-%m-%d %H:%M:%S'), + record_id=hist.record_id, + hid=hist.id + ) + if i.CITypeTrigger.id not in id2trigger: + id2trigger[i.CITypeTrigger.id] = i.CITypeTrigger.to_dict() + + result.append(item) + + return dict(items=result, id2trigger=id2trigger) + + @staticmethod + def add(operate_type, record_id, ci_id, trigger_id, trigger_name, is_ok=False, notify=None, webhook=None): + + CITriggerHistory.create(operate_type=operate_type, + record_id=record_id, + ci_id=ci_id, + trigger_id=trigger_id, + trigger_name=trigger_name, + is_ok=is_ok, + notify=notify, + webhook=webhook) diff --git a/cmdb-api/api/lib/cmdb/perms.py b/cmdb-api/api/lib/cmdb/perms.py index b4b149e9..f034f37d 100644 --- a/cmdb-api/api/lib/cmdb/perms.py +++ b/cmdb-api/api/lib/cmdb/perms.py @@ -4,8 +4,8 @@ from flask import abort from flask import current_app -from flask import g from flask import request +from flask_login import current_user from api.lib.cmdb.const import ResourceTypeEnum from api.lib.cmdb.resp_format import ErrFormat @@ -74,7 +74,7 @@ def get_by_ids(self, _ids, type_id=None): @classmethod def get_attr_filter(cls, type_id): - if is_app_admin('cmdb') or g.user.username in ('worker', 'cmdb_agent'): + if is_app_admin('cmdb') or current_user.username in ('worker', 'cmdb_agent'): return [] res2 = ACLManager('cmdb').get_resources(ResourceTypeEnum.CI_FILTER) @@ -143,11 +143,14 @@ def delete(self, **kwargs): first=True, to_dict=False) if obj is not None: + resource = None if current_app.config.get('USE_ACL'): - ACLManager().del_resource(str(obj.id), ResourceTypeEnum.CI_FILTER) + resource = ACLManager().del_resource(str(obj.id), ResourceTypeEnum.CI_FILTER) obj.soft_delete() + return resource + def has_perm_for_ci(arg_name, resource_type, perm, callback=None, app=None): def decorator_has_perm(func): @@ -160,7 +163,7 @@ def wrapper_has_perm(*args, **kwargs): resource = callback(resource) if current_app.config.get("USE_ACL") and resource: - if g.user.username == "worker" or g.user.username == "cmdb_agent": + if current_user.username == "worker" or current_user.username == "cmdb_agent": request.values['__is_admin'] = True return func(*args, **kwargs) diff --git a/cmdb-api/api/lib/cmdb/preference.py b/cmdb-api/api/lib/cmdb/preference.py index 2c07880c..e7e06edb 100644 --- a/cmdb-api/api/lib/cmdb/preference.py +++ b/cmdb-api/api/lib/cmdb/preference.py @@ -7,14 +7,17 @@ import toposort from flask import abort from flask import current_app -from flask import g +from flask_login import current_user from api.extensions import db from api.lib.cmdb.attribute import AttributeManager from api.lib.cmdb.cache import AttributeCache from api.lib.cmdb.cache import CITypeAttributesCache from api.lib.cmdb.cache import CITypeCache -from api.lib.cmdb.const import PermEnum, ResourceTypeEnum, RoleEnum +from api.lib.cmdb.const import ConstraintEnum +from api.lib.cmdb.const import PermEnum +from api.lib.cmdb.const import ResourceTypeEnum +from api.lib.cmdb.const import RoleEnum from api.lib.cmdb.perms import CIFilterPermsCRUD from api.lib.cmdb.resp_format import ErrFormat from api.lib.exception import AbortException @@ -36,11 +39,13 @@ class PreferenceManager(object): @staticmethod def get_types(instance=False, tree=False): types = db.session.query(PreferenceShowAttributes.type_id).filter( - PreferenceShowAttributes.uid == g.user.uid).filter( - PreferenceShowAttributes.deleted.is_(False)).group_by(PreferenceShowAttributes.type_id).all() \ - if instance else [] - tree_types = PreferenceTreeView.get_by(uid=g.user.uid, to_dict=False) if tree else [] - type_ids = list(set([i.type_id for i in types + tree_types])) + PreferenceShowAttributes.uid == current_user.uid).filter( + PreferenceShowAttributes.deleted.is_(False)).group_by( + PreferenceShowAttributes.type_id).all() if instance else [] + + 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]) + return [CITypeCache.get(type_id).to_dict() for type_id in type_ids] @staticmethod @@ -62,7 +67,7 @@ def get_types2(instance=False, tree=False): PreferenceShowAttributes.deleted.is_(False)).group_by( PreferenceShowAttributes.uid, PreferenceShowAttributes.type_id) for i in types: - if i.uid == g.user.uid: + 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 @@ -72,7 +77,7 @@ def get_types2(instance=False, tree=False): if tree: types = PreferenceTreeView.get_by(to_dict=False) for i in types: - if i.uid == g.user.uid: + 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 @@ -91,7 +96,7 @@ def get_show_attributes(type_id): attrs = db.session.query(PreferenceShowAttributes, CITypeAttribute.order).join( CITypeAttribute, CITypeAttribute.attr_id == PreferenceShowAttributes.attr_id).filter( - PreferenceShowAttributes.uid == g.user.uid).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() @@ -114,13 +119,13 @@ def get_show_attributes(type_id): for i in result: if i["is_choice"]: i.update(dict(choice_value=AttributeManager.get_choice_values( - i["id"], i["value_type"], i["choice_web_hook"]))) + i["id"], i["value_type"], i["choice_web_hook"], i.get("choice_other")))) return is_subscribed, result @classmethod def create_or_update_show_attributes(cls, type_id, attr_order): - existed_all = PreferenceShowAttributes.get_by(type_id=type_id, uid=g.user.uid, to_dict=False) + existed_all = PreferenceShowAttributes.get_by(type_id=type_id, uid=current_user.uid, to_dict=False) for x, order in attr_order: if isinstance(x, list): _attr, is_fixed = x @@ -128,13 +133,13 @@ def create_or_update_show_attributes(cls, type_id, attr_order): _attr, is_fixed = x, False attr = AttributeCache.get(_attr) or abort(404, ErrFormat.attribute_not_found.format("id={}".format(_attr))) existed = PreferenceShowAttributes.get_by(type_id=type_id, - uid=g.user.uid, + uid=current_user.uid, attr_id=attr.id, first=True, to_dict=False) if existed is None: PreferenceShowAttributes.create(type_id=type_id, - uid=g.user.uid, + uid=current_user.uid, attr_id=attr.id, order=order, is_fixed=is_fixed) @@ -148,7 +153,7 @@ def create_or_update_show_attributes(cls, type_id, attr_order): @staticmethod def get_tree_view(): - res = PreferenceTreeView.get_by(uid=g.user.uid, to_dict=True) + res = PreferenceTreeView.get_by(uid=current_user.uid, to_dict=True) for item in res: if item["levels"]: ci_type = CITypeCache.get(item['type_id']).to_dict() @@ -176,14 +181,14 @@ def create_or_update_tree_view(type_id, levels): if i == attr.id or i == attr.name or i == attr.alias: levels[idx] = attr.id - existed = PreferenceTreeView.get_by(uid=g.user.uid, type_id=type_id, to_dict=False, first=True) + existed = PreferenceTreeView.get_by(uid=current_user.uid, type_id=type_id, to_dict=False, first=True) if existed is not None: if not levels: existed.soft_delete() return existed return existed.update(levels=levels) elif levels: - return PreferenceTreeView.create(levels=levels, type_id=type_id, uid=g.user.uid) + return PreferenceTreeView.create(levels=levels, type_id=type_id, uid=current_user.uid) @staticmethod def get_relation_view(): @@ -227,14 +232,28 @@ def _find_parent(_node_id): if not parents: return - for l in leaf: - _find_parent(l) + for _l in leaf: + _find_parent(_l) for node_id in node2show_types: node2show_types[node_id] = [CITypeCache.get(i).to_dict() for i in set(node2show_types[node_id])] + topo_flatten = list(toposort.toposort_flatten(topo)) + level2constraint = {} + for i, _ in enumerate(topo_flatten[1:]): + ctr = CITypeRelation.get_by( + parent_id=topo_flatten[i], child_id=topo_flatten[i + 1], first=True, to_dict=False) + level2constraint[i + 1] = ctr and ctr.constraint + + if leaf2show_types.get(topo_flatten[-1]): + ctr = CITypeRelation.get_by( + parent_id=topo_flatten[-1], + child_id=leaf2show_types[topo_flatten[-1]][0], first=True, to_dict=False) + level2constraint[len(topo_flatten)] = ctr and ctr.constraint + result[view_name] = dict(topo=list(map(list, toposort.toposort(topo))), - topo_flatten=list(toposort.toposort_flatten(topo)), + topo_flatten=topo_flatten, + level2constraint=level2constraint, leaf=leaf, leaf2show_types=leaf2show_types, node2show_types=node2show_types, @@ -254,7 +273,7 @@ def create_or_update_relation_view(cls, name, cr_ids, is_public=False): existed = PreferenceRelationView.get_by(name=name, to_dict=False, first=True) current_app.logger.debug(existed) if existed is None: - PreferenceRelationView.create(name=name, cr_ids=cr_ids, uid=g.user.uid, is_public=is_public) + PreferenceRelationView.create(name=name, cr_ids=cr_ids, uid=current_user.uid, is_public=is_public) if current_app.config.get("USE_ACL"): ACLManager().add_resource(name, ResourceTypeEnum.RELATION_VIEW) @@ -278,7 +297,7 @@ def delete_relation_view(name): @staticmethod def get_search_option(**kwargs): query = PreferenceSearchOption.get_by(only_query=True) - query = query.filter(PreferenceSearchOption.uid == g.user.uid) + query = query.filter(PreferenceSearchOption.uid == current_user.uid) for k in kwargs: if hasattr(PreferenceSearchOption, k) and kwargs[k]: @@ -288,9 +307,9 @@ def get_search_option(**kwargs): @staticmethod def add_search_option(**kwargs): - kwargs['uid'] = g.user.uid + kwargs['uid'] = current_user.uid - existed = PreferenceSearchOption.get_by(uid=g.user.uid, + existed = PreferenceSearchOption.get_by(uid=current_user.uid, name=kwargs.get('name'), prv_id=kwargs.get('prv_id'), ptv_id=kwargs.get('ptv_id'), @@ -306,10 +325,10 @@ def update_search_option(_id, **kwargs): existed = PreferenceSearchOption.get_by_id(_id) or abort(404, ErrFormat.preference_search_option_not_found) - if g.user.uid != existed.uid: + if current_user.uid != existed.uid: return abort(400, ErrFormat.no_permission2) - other = PreferenceSearchOption.get_by(uid=g.user.uid, + other = PreferenceSearchOption.get_by(uid=current_user.uid, name=kwargs.get('name'), prv_id=kwargs.get('prv_id'), ptv_id=kwargs.get('ptv_id'), @@ -324,7 +343,7 @@ def update_search_option(_id, **kwargs): def delete_search_option(_id): existed = PreferenceSearchOption.get_by_id(_id) or abort(404, ErrFormat.preference_search_option_not_found) - if g.user.uid != existed.uid: + if current_user.uid != existed.uid: return abort(400, ErrFormat.no_permission2) existed.soft_delete() @@ -336,3 +355,29 @@ def delete_by_type_id(type_id, uid): for i in PreferenceTreeView.get_by(type_id=type_id, uid=uid, to_dict=False): i.soft_delete() + + @staticmethod + def can_edit_relation(parent_id, child_id): + views = PreferenceRelationView.get_by(to_dict=False) + for view in views: + has_m2m = False + last_node_id = None + for cr in view.cr_ids: + _rel = CITypeRelation.get_by(parent_id=cr['parent_id'], child_id=cr['child_id'], + first=True, to_dict=False) + if _rel and _rel.constraint == ConstraintEnum.Many2Many: + has_m2m = True + + if parent_id == _rel.parent_id and child_id == _rel.child_id: + return False + + if _rel: + last_node_id = _rel.child_id + + if parent_id == last_node_id: + rels = CITypeRelation.get_by(parent_id=last_node_id, to_dict=False) + for rel in rels: + if rel.child_id == child_id and has_m2m: + return False + + return True diff --git a/cmdb-api/api/lib/cmdb/query_sql.py b/cmdb-api/api/lib/cmdb/query_sql.py index c84fd649..f5c598bb 100644 --- a/cmdb-api/api/lib/cmdb/query_sql.py +++ b/cmdb-api/api/lib/cmdb/query_sql.py @@ -42,7 +42,7 @@ FACET_QUERY = """ SELECT {0}.value, - count({0}.ci_id) + count(distinct({0}.ci_id)) FROM {0} INNER JOIN ({1}) AS F ON F.ci_id={0}.ci_id WHERE {0}.attr_id={2:d} diff --git a/cmdb-api/api/lib/cmdb/relation_type.py b/cmdb-api/api/lib/cmdb/relation_type.py index 1fd6c9bf..2eb58617 100644 --- a/cmdb-api/api/lib/cmdb/relation_type.py +++ b/cmdb-api/api/lib/cmdb/relation_type.py @@ -24,21 +24,21 @@ def get_pairs(cls): @staticmethod def add(name): - RelationType.get_by(name=name, first=True, to_dict=False) and \ - abort(400, ErrFormat.relation_type_exists.format(name)) + RelationType.get_by(name=name, first=True, to_dict=False) and abort( + 400, ErrFormat.relation_type_exists.format(name)) return RelationType.create(name=name) @staticmethod def update(rel_id, name): - existed = RelationType.get_by_id(rel_id) or \ - abort(404, ErrFormat.relation_type_not_found.format("id={}".format(rel_id))) + existed = RelationType.get_by_id(rel_id) or abort( + 404, ErrFormat.relation_type_not_found.format("id={}".format(rel_id))) return existed.update(name=name) @staticmethod def delete(rel_id): - existed = RelationType.get_by_id(rel_id) or \ - abort(404, ErrFormat.relation_type_not_found.format("id={}".format(rel_id))) + existed = RelationType.get_by_id(rel_id) or abort( + 404, ErrFormat.relation_type_not_found.format("id={}".format(rel_id))) existed.soft_delete() diff --git a/cmdb-api/api/lib/cmdb/resp_format.py b/cmdb-api/api/lib/cmdb/resp_format.py index ed21e86d..320deb31 100644 --- a/cmdb-api/api/lib/cmdb/resp_format.py +++ b/cmdb-api/api/lib/cmdb/resp_format.py @@ -1,93 +1,138 @@ # -*- 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 = "文件似乎并未上传" - - attribute_not_found = "属性 {} 不存在!" - 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" - - ci_not_found = "CI {} 不存在" - unique_constraint = "多属性联合唯一校验不通过: {}" - unique_value_not_found = "模型的主键 {} 不存在!" - unique_key_required = "主键字段 {} 缺失" - ci_is_already_existed = "CI 已经存在!" - relation_constraint = "关系约束: {}, 校验失败 " - relation_not_found = "CI关系: {} 不存在" - ci_search_Parentheses_invalid = "搜索表达式里小括号前不支持: 或、非" - - 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_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 = "触发器 {} 不存在" - - record_not_found = "操作记录 {} 不存在" - cannot_delete_unique = "不能删除唯一标识" - cannot_delete_default_order_attr = "不能删除默认排序的属性" - - preference_relation_view_node_required = "没有选择节点" - preference_search_option_not_found = "该搜索选项不存在!" - preference_search_option_exists = "该搜索选项命名重复!" - - relation_type_exists = "关系类型 {} 已经存在" - relation_type_not_found = "关系类型 {} 不存在" - - attribute_value_invalid = "无效的属性值: {}" - attribute_value_invalid2 = "{} 无效的值: {}" - not_in_choice_values = "{} 不在预定义值里" - attribute_value_unique_required = "属性 {} 的值必须是唯一的, 当前值 {} 已存在" - attribute_value_required = "属性 {} 值必须存在" - attribute_value_unknown_error = "新增或者修改属性值未知错误: {}" - - custom_name_duplicate = "订制名重复" - - limit_ci_type = "模型数超过限制: {}" - limit_ci = "CI数超过限制: {}" - - 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 = "执行机器权限检查不通过: {}" - - 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的操作权限!" + ci_type_config = _l("CI Model") # 模型配置 + + invalid_relation_type = _l("Invalid relation type: {}") # 无效的关系类型: {} + ci_type_not_found = _l("CIType is not found") # 模型不存在! + + # 参数 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") # 文件似乎并未上传 + + 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!") # 预定义值: 其他模型请求参数不合法! + + 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!") + + relation_not_found = _l("CI relationship: {} does not exist") # CI关系: {} 不存在 + + # 搜索表达式里小括号前不支持: 或、非 + ci_search_Parentheses_invalid = _l("In search expressions, not supported before parentheses: or, not") + + 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_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") # 触发器 {} 不存在 + + 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") # 不能删除默认排序的属性 + + 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!") # 该搜索选项命名重复! + + relation_type_exists = _l("Relationship type {} already exists") # 关系类型 {} 已经存在 + relation_type_not_found = _l("Relationship type {} does not exist") # 关系类型 {} 不存在 + + 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_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/db/query_sql.py b/cmdb-api/api/lib/cmdb/search/ci/db/query_sql.py index 78e43e75..24aa0cb3 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 @@ -7,6 +7,7 @@ attr.alias AS attr_alias, attr.value_type, attr.is_list, + attr.is_password, c_cis.type_id, {0}.ci_id, {0}.attr_id, @@ -26,7 +27,8 @@ A.attr_alias, A.value, A.value_type, - A.is_list + A.is_list, + A.is_password FROM ({1}) AS A {0} ORDER BY A.ci_id; @@ -43,7 +45,7 @@ FACET_QUERY = """ SELECT {0}.value, - count({0}.ci_id) + count(distinct {0}.ci_id) FROM {0} INNER JOIN ({1}) AS F ON F.ci_id={0}.ci_id WHERE {0}.attr_id={2:d} diff --git a/cmdb-api/api/lib/cmdb/search/ci/db/search.py b/cmdb-api/api/lib/cmdb/search/ci/db/search.py index 48e2fb9b..206e9214 100644 --- a/cmdb-api/api/lib/cmdb/search/ci/db/search.py +++ b/cmdb-api/api/lib/cmdb/search/ci/db/search.py @@ -1,4 +1,4 @@ -# -*- coding:utf-8 -*- +# -*- coding:utf-8 -*- from __future__ import unicode_literals @@ -7,8 +7,10 @@ import time from flask import current_app -from flask import g +from flask_login import current_user from jinja2 import Template +from sqlalchemy import text + from api.extensions import db from api.lib.cmdb.cache import AttributeCache from api.lib.cmdb.cache import CITypeCache @@ -27,6 +29,7 @@ from api.lib.cmdb.search.ci.db.query_sql import QUERY_CI_BY_TYPE from api.lib.cmdb.search.ci.db.query_sql import QUERY_UNION_CI_ATTRIBUTE_IS_NULL from api.lib.cmdb.utils import TableMap +from api.lib.cmdb.utils import ValueTypeMap from api.lib.perm.acl.acl import ACLManager from api.lib.perm.acl.acl import is_app_admin from api.lib.utils import handle_arg_list @@ -105,7 +108,7 @@ def _type_query_handler(self, v, queries): ci_filter = self.type2filter_perms[ci_type.id].get('ci_filter') if ci_filter: sub = [] - ci_filter = Template(ci_filter).render(user=g.user) + ci_filter = Template(ci_filter).render(user=current_user) for i in ci_filter.split(','): if i.startswith("~") and not sub: queries.append(i) @@ -140,6 +143,10 @@ def _id_query_handler(v): @staticmethod def _in_query_handler(attr, v, is_not): new_v = v[1:-1].split(";") + + if attr.value_type == ValueTypeEnum.DATE: + new_v = ["{} 00:00:00".format(i) for i in new_v if len(i) == 10] + table_name = TableMap(attr=attr).table_name in_query = " OR {0}.value ".format(table_name).join(['{0} "{1}"'.format( "NOT LIKE" if is_not else "LIKE", @@ -150,6 +157,11 @@ def _in_query_handler(attr, v, is_not): @staticmethod def _range_query_handler(attr, v, is_not): start, end = [x.strip() for x in v[1:-1].split("_TO_")] + + if attr.value_type == ValueTypeEnum.DATE: + start = "{} 00:00:00".format(start) if len(start) == 10 else start + end = "{} 00:00:00".format(end) if len(end) == 10 else end + table_name = TableMap(attr=attr).table_name range_query = "{0} '{1}' AND '{2}'".format( "NOT BETWEEN" if is_not else "BETWEEN", @@ -161,8 +173,14 @@ def _range_query_handler(attr, v, is_not): def _comparison_query_handler(attr, v): table_name = TableMap(attr=attr).table_name if v.startswith(">=") or v.startswith("<="): + if attr.value_type == ValueTypeEnum.DATE and len(v[2:]) == 10: + v = "{} 00:00:00".format(v) + comparison_query = "{0} '{1}'".format(v[:2], v[2:].replace("*", "%")) else: + if attr.value_type == ValueTypeEnum.DATE and len(v[1:]) == 10: + v = "{} 00:00:00".format(v) + comparison_query = "{0} '{1}'".format(v[0], v[1:].replace("*", "%")) _query_sql = QUERY_CI_BY_ATTR_NAME.format(table_name, attr.id, comparison_query) return _query_sql @@ -238,16 +256,14 @@ def __sort_by_field(self, field, sort_type, query_sql): attr_id = attr.id table_name = TableMap(attr=attr).table_name - _v_query_sql = """SELECT {0}.ci_id, {1}.value + _v_query_sql = """SELECT {0}.ci_id, {1}.value FROM ({2}) AS {0} INNER JOIN {1} ON {1}.ci_id = {0}.ci_id WHERE {1}.attr_id = {3}""".format("ALIAS", table_name, query_sql, attr_id) new_table = _v_query_sql if self.only_type_query or not self.type_id_list: - return "SELECT SQL_CALC_FOUND_ROWS DISTINCT C.ci_id " \ - "FROM ({0}) AS C " \ - "ORDER BY C.value {2} " \ - "LIMIT {1:d}, {3};".format(new_table, (self.page - 1) * self.count, sort_type, self.count) + return ("SELECT SQL_CALC_FOUND_ROWS DISTINCT C.ci_id FROM ({0}) AS C ORDER BY C.value {2} " + "LIMIT {1:d}, {3};".format(new_table, (self.page - 1) * self.count, sort_type, self.count)) elif self.type_id_list: self.query_sql = """SELECT C.ci_id @@ -286,7 +302,7 @@ def _wrap_sql(operator, alias, _query_sql, query_sql): query_sql = "SELECT * FROM ({0}) as {1} UNION ALL ({2})".format(query_sql, alias, _query_sql) elif operator == "~": - query_sql = """SELECT * FROM ({0}) as {1} LEFT JOIN ({2}) as {3} USING(ci_id) + query_sql = """SELECT * FROM ({0}) as {1} LEFT JOIN ({2}) as {3} USING(ci_id) WHERE {3}.ci_id is NULL""".format(query_sql, alias, _query_sql, alias + "A") return query_sql @@ -296,8 +312,8 @@ def _execute_sql(self, query_sql): start = time.time() execute = db.session.execute - current_app.logger.debug(v_query_sql) - res = execute(v_query_sql).fetchall() + # current_app.logger.debug(v_query_sql) + res = execute(text(v_query_sql)).fetchall() end_time = time.time() current_app.logger.debug("query ci ids time is: {0}".format(end_time - start)) @@ -355,7 +371,7 @@ def __confirm_type_first(self, queries): else: result.append(q) - _is_app_admin = is_app_admin('cmdb') or g.user.username == "worker" + _is_app_admin = is_app_admin('cmdb') or current_user.username == "worker" if result and not has_type and not _is_app_admin: type_q = self.__get_types_has_read() if id_query: @@ -392,6 +408,9 @@ def __query_by_attr(self, q, queries, alias): is_not = True if operator == "|~" else False + if field_type == ValueTypeEnum.DATE and len(v) == 10: + v = "{} 00:00:00".format(v) + # in query if v.startswith("(") and v.endswith(")"): _query_sql = self._in_query_handler(attr, v, is_not) @@ -507,15 +526,15 @@ def _facet_build(self): if k: table_name = TableMap(attr=attr).table_name query_sql = FACET_QUERY.format(table_name, self.query_sql, attr.id) - # current_app.logger.debug(query_sql) - result = db.session.execute(query_sql).fetchall() + result = db.session.execute(text(query_sql)).fetchall() facet[k] = result facet_result = dict() for k, v in facet.items(): if not k.startswith('_'): - a = getattr(AttributeCache.get(k), self.ret_key) - facet_result[a] = [(f[0], f[1], a) for f in v] + attr = AttributeCache.get(k) + a = getattr(attr, self.ret_key) + facet_result[a] = [(ValueTypeMap.serialize[attr.value_type](f[0]), f[1], a) for f in v] return facet_result diff --git a/cmdb-api/api/lib/cmdb/search/ci/es/search.py b/cmdb-api/api/lib/cmdb/search/ci/es/search.py index e83cf690..235e5bb7 100644 --- a/cmdb-api/api/lib/cmdb/search/ci/es/search.py +++ b/cmdb-api/api/lib/cmdb/search/ci/es/search.py @@ -297,8 +297,8 @@ def _sort_build(self): if not attr: raise SearchError(ErrFormat.attribute_not_found.format(field)) - sort_by = "{0}.keyword".format(field) \ - if attr.value_type not in (ValueTypeEnum.INT, ValueTypeEnum.FLOAT) else field + sort_by = ("{0}.keyword".format(field) + if attr.value_type not in (ValueTypeEnum.INT, ValueTypeEnum.FLOAT) else field) sorts.append({sort_by: {"order": sort_type}}) self.query.update(dict(sort=sorts)) 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 3a22f3be..b7513c46 100644 --- a/cmdb-api/api/lib/cmdb/search/ci_relation/search.py +++ b/cmdb-api/api/lib/cmdb/search/ci_relation/search.py @@ -1,6 +1,4 @@ # -*- coding:utf-8 -*- - - import json from collections import Counter @@ -10,11 +8,14 @@ from api.extensions import rd from api.lib.cmdb.ci import CIRelationManager from api.lib.cmdb.ci_type import CITypeRelationManager +from api.lib.cmdb.const import ConstraintEnum from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION +from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION2 from api.lib.cmdb.resp_format import ErrFormat from api.lib.cmdb.search.ci.db.search import Search as SearchFromDB from api.lib.cmdb.search.ci.es.search import Search as SearchFromES from api.models.cmdb import CI +from api.models.cmdb import CIRelation class Search(object): @@ -26,7 +27,9 @@ def __init__(self, root_id, page=1, count=None, sort=None, - reverse=False): + reverse=False, + ancestor_ids=None, + has_m2m=None): self.orig_query = query self.fl = fl self.facet_field = facet_field @@ -35,28 +38,85 @@ def __init__(self, root_id, self.sort = sort or ("ci_id" if current_app.config.get("USE_ES") else None) self.root_id = root_id - self.level = level + self.level = level or 0 self.reverse = reverse - def _get_ids(self): + self.level2constraint = CITypeRelationManager.get_level2constraint( + root_id[0] if root_id and isinstance(root_id, list) else root_id, + level[0] if isinstance(level, list) and level else level) + + self.ancestor_ids = ancestor_ids + self.has_m2m = 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 + + def _get_ids(self, ids): + if self.level[-1] == 1 and len(ids) == 1: + if self.ancestor_ids is None: + return [i.second_ci_id for i in CIRelation.get_by(first_ci_id=ids[0], to_dict=False)] + + else: + seconds = {i.second_ci_id for i in CIRelation.get_by(first_ci_id=ids[0], + ancestor_ids=self.ancestor_ids, + to_dict=False)} + + return list(seconds) + merge_ids = [] - ids = [self.root_id] if not isinstance(self.root_id, list) else self.root_id + key = [] + _tmp = [] for level in range(1, sorted(self.level)[-1] + 1): - _tmp = list(map(lambda x: list(json.loads(x).keys()), - filter(lambda x: x is not None, rd.get(ids, REDIS_PREFIX_CI_RELATION) or []))) - ids = [j for i in _tmp for j in i] + if not self.has_m2m: + _tmp = map(lambda x: json.loads(x).keys(), + filter(lambda x: x is not None, rd.get(ids, REDIS_PREFIX_CI_RELATION) or [])) + ids = [j for i in _tmp for j in i] + key, prefix = ids, REDIS_PREFIX_CI_RELATION + + else: + if not self.ancestor_ids: + if level == 1: + key, prefix = list(map(str, ids)), REDIS_PREFIX_CI_RELATION + else: + key = list(set(["{},{}".format(i, j) for idx, i in enumerate(key) for j in _tmp[idx]])) + prefix = REDIS_PREFIX_CI_RELATION2 + else: + if level == 1: + key, prefix = ["{},{}".format(self.ancestor_ids, i) for i in ids], REDIS_PREFIX_CI_RELATION2 + else: + key = list(set(["{},{}".format(i, j) for idx, i in enumerate(key) for j in _tmp[idx]])) + prefix = REDIS_PREFIX_CI_RELATION2 + + _tmp = list(map(lambda x: json.loads(x).keys() if x else [], rd.get(key, prefix) or [])) + ids = [j for i in _tmp for j in i] + + if not key: + return [] + if level in self.level: merge_ids.extend(ids) return merge_ids - def _get_reverse_ids(self): + def _get_reverse_ids(self, ids): merge_ids = [] - ids = [self.root_id] if not isinstance(self.root_id, list) else self.root_id + level2ids = {} for level in range(1, sorted(self.level)[-1] + 1): - ids = CIRelationManager.get_ancestor_ids(ids, 1) + ids, _level2ids = CIRelationManager.get_ancestor_ids(ids, 1) + + if _level2ids.get(2): + level2ids[level + 1] = _level2ids[2] + if level in self.level: - merge_ids.extend(ids) + if level in level2ids and level2ids[level]: + merge_ids.extend(set(ids) & set(level2ids[level])) + else: + merge_ids.extend(ids) return merge_ids @@ -64,7 +124,7 @@ def search(self): ids = [self.root_id] if not isinstance(self.root_id, list) else self.root_id cis = [CI.get_by_id(_id) or abort(404, ErrFormat.ci_not_found.format("id={}".format(_id))) for _id in ids] - merge_ids = self._get_ids() if not self.reverse else self._get_reverse_ids() + merge_ids = self._get_ids(ids) if not self.reverse else self._get_reverse_ids(ids) if not self.orig_query or ("_type:" not in self.orig_query and "type_id:" not in self.orig_query @@ -76,11 +136,11 @@ def search(self): type_ids.extend(CITypeRelationManager.get_child_type_ids(ci.type_id, level)) else: type_ids.extend(CITypeRelationManager.get_parent_type_ids(ci.type_id, level)) - type_ids = list(set(type_ids)) + type_ids = set(type_ids) if self.orig_query: - self.orig_query = "_type:({0}),{1}".format(";".join(list(map(str, type_ids))), self.orig_query) + self.orig_query = "_type:({0}),{1}".format(";".join(map(str, type_ids)), self.orig_query) else: - self.orig_query = "_type:({0})".format(";".join(list(map(str, type_ids)))) + self.orig_query = "_type:({0})".format(";".join(map(str, type_ids))) if not merge_ids: # cis, counter, total, self.page, numfound, facet_ @@ -104,30 +164,66 @@ def search(self): ci_ids=merge_ids).search() def statistics(self, type_ids): - _tmp = [] + self.level = int(self.level) + ids = [self.root_id] if not isinstance(self.root_id, list) else self.root_id - for l in range(0, int(self.level)): - if not l: - _tmp = list(map(lambda x: list(json.loads(x).items()), - [i or '{}' for i in rd.get(ids, REDIS_PREFIX_CI_RELATION) or []])) + _tmp = [] + level2ids = {} + for lv in range(1, self.level + 1): + level2ids[lv] = [] + + if lv == 1: + if not self.has_m2m: + key, prefix = ids, REDIS_PREFIX_CI_RELATION + else: + if not self.ancestor_ids: + key, prefix = ids, REDIS_PREFIX_CI_RELATION + else: + key = ["{},{}".format(self.ancestor_ids, _id) for _id in ids] + prefix = REDIS_PREFIX_CI_RELATION2 + + level2ids[lv] = [[i] for i in key] + + if not key: + _tmp = [] + continue + + if type_ids and lv == self.level: + _tmp = list(map(lambda x: [i for i in x if i[1] in type_ids], + (map(lambda x: list(json.loads(x).items()), + [i or '{}' for i in rd.get(key, prefix) or []])))) + else: + _tmp = list(map(lambda x: list(json.loads(x).items()), + [i or '{}' for i in rd.get(key, prefix) or []])) + else: for idx, item in enumerate(_tmp): if item: - if type_ids and l == self.level - 1: - __tmp = list( - map(lambda x: [(_id, type_id) for _id, type_id in json.loads(x).items() - if type_id in type_ids], - filter(lambda x: x is not None, - rd.get([i[0] for i in item], REDIS_PREFIX_CI_RELATION) or []))) + if not self.has_m2m: + key, prefix = [i[0] for i in item], REDIS_PREFIX_CI_RELATION else: - - __tmp = list(map(lambda x: list(json.loads(x).items()), - filter(lambda x: x is not None, - rd.get([i[0] for i in item], REDIS_PREFIX_CI_RELATION) or []))) + key = list(set(['{},{}'.format(j, i[0]) for i in item for j in level2ids[lv - 1][idx]])) + prefix = REDIS_PREFIX_CI_RELATION2 + + level2ids[lv].append(key) + + if key: + if type_ids and lv == self.level: + __tmp = map(lambda x: [(_id, type_id) for _id, type_id in json.loads(x).items() + if type_id in type_ids], + filter(lambda x: x is not None, + rd.get(key, prefix) or [])) + else: + __tmp = map(lambda x: list(json.loads(x).items()), + filter(lambda x: x is not None, + rd.get(key, prefix) or [])) + else: + __tmp = [] _tmp[idx] = [j for i in __tmp for j in i] else: _tmp[idx] = [] + level2ids[lv].append([]) result = {str(_id): len(_tmp[idx]) for idx, _id in enumerate(ids)} diff --git a/cmdb-api/api/lib/cmdb/utils.py b/cmdb-api/api/lib/cmdb/utils.py index 1ea82af6..1239b1c9 100644 --- a/cmdb-api/api/lib/cmdb/utils.py +++ b/cmdb-api/api/lib/cmdb/utils.py @@ -4,14 +4,16 @@ import datetime import json +import re import six -from markupsafe import escape import api.models.cmdb as model from api.lib.cmdb.cache import AttributeCache from api.lib.cmdb.const import ValueTypeEnum +TIME_RE = re.compile(r'(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d') + def string2int(x): return int(float(x)) @@ -19,7 +21,7 @@ def string2int(x): def str2datetime(x): try: - return datetime.datetime.strptime(x, "%Y-%m-%d") + return datetime.datetime.strptime(x, "%Y-%m-%d").date() except ValueError: pass @@ -30,8 +32,8 @@ class ValueTypeMap(object): deserialize = { ValueTypeEnum.INT: string2int, ValueTypeEnum.FLOAT: float, - ValueTypeEnum.TEXT: lambda x: escape(x).encode('utf-8').decode('utf-8'), - ValueTypeEnum.TIME: lambda x: escape(x).encode('utf-8').decode('utf-8'), + ValueTypeEnum.TEXT: lambda x: x, + ValueTypeEnum.TIME: lambda x: TIME_RE.findall(x)[0], ValueTypeEnum.DATETIME: str2datetime, ValueTypeEnum.DATE: str2datetime, ValueTypeEnum.JSON: lambda x: json.loads(x) if isinstance(x, six.string_types) and x else x, @@ -42,8 +44,8 @@ class ValueTypeMap(object): ValueTypeEnum.FLOAT: float, ValueTypeEnum.TEXT: lambda x: x if isinstance(x, six.string_types) else str(x), ValueTypeEnum.TIME: lambda x: x if isinstance(x, six.string_types) else str(x), - ValueTypeEnum.DATE: lambda x: x.strftime("%Y-%m-%d"), - ValueTypeEnum.DATETIME: lambda x: x.strftime("%Y-%m-%d %H:%M:%S"), + ValueTypeEnum.DATE: lambda x: x.strftime("%Y-%m-%d") if not isinstance(x, six.string_types) else x, + ValueTypeEnum.DATETIME: lambda x: x.strftime("%Y-%m-%d %H:%M:%S") if not isinstance(x, six.string_types) else x, ValueTypeEnum.JSON: lambda x: json.loads(x) if isinstance(x, six.string_types) and x else x, } @@ -61,15 +63,13 @@ class ValueTypeMap(object): ValueTypeEnum.INT: model.IntegerChoice, ValueTypeEnum.FLOAT: model.FloatChoice, ValueTypeEnum.TEXT: model.TextChoice, + ValueTypeEnum.TIME: model.TextChoice, + ValueTypeEnum.DATE: model.TextChoice, + ValueTypeEnum.DATETIME: model.TextChoice, } table = { - ValueTypeEnum.INT: model.CIValueInteger, ValueTypeEnum.TEXT: model.CIValueText, - ValueTypeEnum.DATETIME: model.CIValueDateTime, - ValueTypeEnum.DATE: model.CIValueDateTime, - ValueTypeEnum.TIME: model.CIValueText, - ValueTypeEnum.FLOAT: model.CIValueFloat, ValueTypeEnum.JSON: model.CIValueJson, 'index_{0}'.format(ValueTypeEnum.INT): model.CIIndexValueInteger, 'index_{0}'.format(ValueTypeEnum.TEXT): model.CIIndexValueText, @@ -81,12 +81,7 @@ class ValueTypeMap(object): } table_name = { - ValueTypeEnum.INT: 'c_value_integers', ValueTypeEnum.TEXT: 'c_value_texts', - ValueTypeEnum.DATETIME: 'c_value_datetime', - ValueTypeEnum.DATE: 'c_value_datetime', - ValueTypeEnum.TIME: 'c_value_texts', - ValueTypeEnum.FLOAT: 'c_value_floats', ValueTypeEnum.JSON: 'c_value_json', 'index_{0}'.format(ValueTypeEnum.INT): 'c_value_index_integers', 'index_{0}'.format(ValueTypeEnum.TEXT): 'c_value_index_texts', @@ -104,7 +99,7 @@ class ValueTypeMap(object): ValueTypeEnum.DATE: 'text', ValueTypeEnum.TIME: 'text', ValueTypeEnum.FLOAT: 'float', - ValueTypeEnum.JSON: 'object' + ValueTypeEnum.JSON: 'object', } @@ -117,8 +112,13 @@ def __init__(self, attr_name=None, attr=None, is_index=None): @property def table(self): attr = AttributeCache.get(self.attr_name) if not self.attr else self.attr - if self.is_index is None: + if attr.is_password or attr.is_link: + self.is_index = False + elif attr.value_type not in {ValueTypeEnum.TEXT, ValueTypeEnum.JSON}: + self.is_index = True + elif self.is_index is None: self.is_index = attr.is_index + i = "index_{0}".format(attr.value_type) if self.is_index else attr.value_type return ValueTypeMap.table.get(i) @@ -126,8 +126,13 @@ def table(self): @property def table_name(self): attr = AttributeCache.get(self.attr_name) if not self.attr else self.attr - if self.is_index is None: + if attr.is_password or attr.is_link: + self.is_index = False + elif attr.value_type not in {ValueTypeEnum.TEXT, ValueTypeEnum.JSON}: + self.is_index = True + elif self.is_index is None: self.is_index = attr.is_index + i = "index_{0}".format(attr.value_type) if self.is_index else attr.value_type return ValueTypeMap.table_name.get(i) diff --git a/cmdb-api/api/lib/cmdb/value.py b/cmdb-api/api/lib/cmdb/value.py index 5b1c25d3..34cebc4b 100644 --- a/cmdb-api/api/lib/cmdb/value.py +++ b/cmdb-api/api/lib/cmdb/value.py @@ -18,7 +18,6 @@ from api.lib.cmdb.attribute import AttributeManager from api.lib.cmdb.cache import AttributeCache from api.lib.cmdb.cache import CITypeAttributeCache -from api.lib.cmdb.const import ExistPolicy from api.lib.cmdb.const import OperateType from api.lib.cmdb.const import ValueTypeEnum from api.lib.cmdb.history import AttributeHistoryManger @@ -67,9 +66,10 @@ def get_attr_values(self, fields, ci_id, ret_key="name", unique_key=None, use_ma use_master=use_master, to_dict=False) field_name = getattr(attr, ret_key) - if attr.is_list: res[field_name] = [ValueTypeMap.serialize[attr.value_type](i.value) for i in rs] + elif attr.is_password and rs: + res[field_name] = '******' if rs[0].value else '' else: res[field_name] = ValueTypeMap.serialize[attr.value_type](rs[0].value) if rs else None @@ -80,9 +80,10 @@ def get_attr_values(self, fields, ci_id, ret_key="name", unique_key=None, use_ma return res @staticmethod - def __deserialize_value(value_type, value): + def _deserialize_value(value_type, value): if not value: return value + deserialize = ValueTypeMap.deserialize[value_type] try: v = deserialize(value) @@ -91,13 +92,18 @@ def __deserialize_value(value_type, value): 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) - 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)) + 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 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): + def _check_is_unique(value_table, attr, ci_id, type_id, value): existed = db.session.query(value_table.attr_id).join(CI, CI.id == value_table.ci_id).filter( CI.type_id == type_id).filter( value_table.attr_id == attr.id).filter(value_table.deleted.is_(False)).filter( @@ -106,20 +112,20 @@ def __check_is_unique(value_table, attr, ci_id, type_id, value): existed and abort(400, ErrFormat.attribute_value_unique_required.format(attr.alias, value)) @staticmethod - def __check_is_required(type_id, attr, value, type_attr=None): + def _check_is_required(type_id, attr, value, type_attr=None): type_attr = type_attr or CITypeAttributeCache.get(type_id, attr.id) if type_attr and type_attr.is_required and not value and value != 0: return abort(400, ErrFormat.attribute_value_required.format(attr.alias)) 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.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( + attr.is_choice and value and self._check_is_choice(attr, attr.value_type, v) + attr.is_unique and self._check_is_unique( value_table, attr, ci and ci.id or ci_id, ci and ci.type_id or type_id, v) - self.__check_is_required(ci and ci.type_id or type_id, attr, v, type_attr=type_attr) + self._check_is_required(ci and ci.type_id or type_id, attr, v, type_attr=type_attr) if v == "" and attr.value_type not in (ValueTypeEnum.TEXT,): v = None @@ -131,20 +137,20 @@ def _write_change(ci_id, attr_id, operate_type, old, new, record_id, type_id): 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): 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, commit=False, flush=False) try: db.session.commit() except Exception as e: + db.session.rollback() current_app.logger.error("write change failed: {}".format(str(e))) return record_id @staticmethod - def __compute_attr_value_from_expr(expr, ci_dict): + def _compute_attr_value_from_expr(expr, ci_dict): t = jinja2.Template(expr).render(ci_dict) try: @@ -154,7 +160,7 @@ def __compute_attr_value_from_expr(expr, ci_dict): return t @staticmethod - def __compute_attr_value_from_script(script, ci_dict): + def _compute_attr_value_from_script(script, ci_dict): script = jinja2.Template(script).render(ci_dict) script_f = tempfile.NamedTemporaryFile(delete=False, suffix=".py") @@ -183,22 +189,22 @@ def _jinja2_parse(content): return [var for var in schema.get("properties")] - def _compute_attr_value(self, attr, payload, ci): - attrs = self._jinja2_parse(attr['compute_expr']) if attr.get('compute_expr') else \ - self._jinja2_parse(attr['compute_script']) + def _compute_attr_value(self, attr, payload, ci_id): + attrs = (self._jinja2_parse(attr['compute_expr']) if attr.get('compute_expr') + else self._jinja2_parse(attr['compute_script'])) not_existed = [i for i in attrs if i not in payload] - if ci is not None: - payload.update(self.get_attr_values(not_existed, ci.id)) + if ci_id is not None: + payload.update(self.get_attr_values(not_existed, ci_id)) if attr['compute_expr']: - return self.__compute_attr_value_from_expr(attr['compute_expr'], payload) + return self._compute_attr_value_from_expr(attr['compute_expr'], payload) elif attr['compute_script']: - return self.__compute_attr_value_from_script(attr['compute_script'], payload) + return self._compute_attr_value_from_script(attr['compute_script'], payload) def handle_ci_compute_attributes(self, ci_dict, computed_attrs, ci): payload = copy.deepcopy(ci_dict) for attr in computed_attrs: - computed_value = self._compute_attr_value(attr, payload, ci) + computed_value = self._compute_attr_value(attr, payload, ci and ci.id) if computed_value is not None: ci_dict[attr['name']] = computed_value @@ -220,7 +226,7 @@ def valid_attr_value(self, ci_dict, type_id, ci_id, name2attr, alias2attr=None, for i in handle_arg_list(value)] ci_dict[key] = value_list if not value_list: - self.__check_is_required(type_id, attr, '') + self._check_is_required(type_id, attr, '') else: value = self._validate(attr, value, value_table, ci=None, type_id=type_id, ci_id=ci_id, @@ -234,7 +240,7 @@ def valid_attr_value(self, ci_dict, type_id, ci_id, name2attr, alias2attr=None, return key2attr - def create_or_update_attr_value2(self, ci, ci_dict, key2attr): + def create_or_update_attr_value(self, ci, ci_dict, key2attr): """ add or update attribute value, then write history :param ci: instance object @@ -283,69 +289,9 @@ def create_or_update_attr_value2(self, ci, ci_dict, key2attr): except Exception as e: db.session.rollback() current_app.logger.warning(str(e)) - return abort(400, ErrFormat.attribute_value_unknown_error.format(str(e))) - - return self._write_change2(changed) - - def create_or_update_attr_value(self, key, value, ci, _no_attribute_policy=ExistPolicy.IGNORE, record_id=None): - """ - add or update attribute value, then write history - :param key: id, name or alias - :param value: - :param ci: instance object - :param _no_attribute_policy: ignore or reject - :param record_id: op record - :return: - """ - attr = self._get_attr(key) - if attr is None: - if _no_attribute_policy == ExistPolicy.IGNORE: - return - if _no_attribute_policy == ExistPolicy.REJECT: - return abort(400, ErrFormat.attribute_not_found.format(key)) + return abort(400, ErrFormat.attribute_value_unknown_error.format(e.args[0])) - value_table = TableMap(attr=attr).table - - try: - if attr.is_list: - value_list = [self._validate(attr, i, value_table, ci) for i in handle_arg_list(value)] - if not value_list: - self.__check_is_required(ci.type_id, attr, '') - - existed_attrs = value_table.get_by(attr_id=attr.id, ci_id=ci.id, to_dict=False) - existed_values = [i.value for i in existed_attrs] - added = set(value_list) - set(existed_values) - deleted = set(existed_values) - set(value_list) - for v in added: - value_table.create(ci_id=ci.id, attr_id=attr.id, value=v) - record_id = self._write_change(ci.id, attr.id, OperateType.ADD, None, v, record_id, ci.type_id) - - for v in deleted: - existed_attr = existed_attrs[existed_values.index(v)] - existed_attr.delete() - record_id = self._write_change(ci.id, attr.id, OperateType.DELETE, v, None, record_id, ci.type_id) - else: - value = self._validate(attr, value, value_table, ci) - existed_attr = value_table.get_by(attr_id=attr.id, ci_id=ci.id, first=True, to_dict=False) - existed_value = existed_attr and existed_attr.value - if existed_value is None and value is not None: - value_table.create(ci_id=ci.id, attr_id=attr.id, value=value) - - record_id = self._write_change(ci.id, attr.id, OperateType.ADD, None, value, record_id, ci.type_id) - else: - if existed_value != value: - if value is None: - existed_attr.delete() - else: - existed_attr.update(value=value) - - record_id = self._write_change(ci.id, attr.id, OperateType.UPDATE, - existed_value, value, record_id, ci.type_id) - - return record_id - except Exception as e: - current_app.logger.warning(str(e)) - return abort(400, ErrFormat.attribute_value_invalid2.format("{}({})".format(attr.alias, attr.name), value)) + return self.write_change2(changed) @staticmethod def delete_attr_value(attr_id, ci_id): diff --git a/cmdb-api/api/lib/common_setting/acl.py b/cmdb-api/api/lib/common_setting/acl.py index 163a3732..67774cec 100644 --- a/cmdb-api/api/lib/common_setting/acl.py +++ b/cmdb-api/api/lib/common_setting/acl.py @@ -1,9 +1,11 @@ # -*- coding:utf-8 -*- -from flask import abort from flask import current_app from api.lib.common_setting.resp_format import ErrFormat +from api.lib.perm.acl.app import AppCRUD from api.lib.perm.acl.cache import RoleCache, AppCache +from api.lib.perm.acl.permission import PermissionCRUD +from api.lib.perm.acl.resource import ResourceTypeCRUD, ResourceCRUD from api.lib.perm.acl.role import RoleCRUD, RoleRelationCRUD from api.lib.perm.acl.user import UserCRUD @@ -78,19 +80,64 @@ def edit_role(_id, payload): return role.to_dict() @staticmethod - def delete_role(_id, payload): + def delete_role(_id): RoleCRUD.delete_role(_id) return dict(rid=_id) def get_user_info(self, username): from api.lib.perm.acl.acl import ACLManager as ACL user_info = ACL().get_user_info(username, self.app_name) - result = dict(name=user_info.get('nickname') or username, - username=user_info.get('username') or username, - email=user_info.get('email'), - uid=user_info.get('uid'), - rid=user_info.get('rid'), - role=dict(permissions=user_info.get('parents')), - avatar=user_info.get('avatar')) + result = dict( + name=user_info.get('nickname') or username, + username=user_info.get('username') or username, + email=user_info.get('email'), + uid=user_info.get('uid'), + rid=user_info.get('rid'), + role=dict(permissions=user_info.get('parents')), + avatar=user_info.get('avatar') + ) return result + + def validate_app(self): + return AppCache.get(self.app_name) + + def get_all_resources_types(self, q=None, page=1, page_size=999999): + app_id = self.validate_app().id + numfound, res, id2perms = ResourceTypeCRUD.search(q, app_id, page, page_size) + + return dict( + numfound=numfound, + groups=[i.to_dict() for i in res], + id2perms=id2perms + ) + + def create_resources_type(self, payload): + payload['app_id'] = self.validate_app().id + rt = ResourceTypeCRUD.add(**payload) + + return rt.to_dict() + + def update_resources_type(self, _id, payload): + rt = ResourceTypeCRUD.update(_id, **payload) + + return rt.to_dict() + + def create_resource(self, payload): + payload['app_id'] = self.validate_app().id + resource = ResourceCRUD.add(**payload) + + return resource.to_dict() + + def get_resource_by_type(self, q, u, rt_id, page=1, page_size=999999): + numfound, res = ResourceCRUD.search(q, u, self.validate_app().id, rt_id, page, page_size) + return res + + def grant_resource(self, rid, resource_id, perms): + PermissionCRUD.grant(rid, perms, resource_id=resource_id, group_id=None) + + @staticmethod + def create_app(payload): + rt = AppCRUD.add(**payload) + + return rt.to_dict() diff --git a/cmdb-api/api/lib/common_setting/common_data.py b/cmdb-api/api/lib/common_setting/common_data.py new file mode 100644 index 00000000..93c6f6d8 --- /dev/null +++ b/cmdb-api/api/lib/common_setting/common_data.py @@ -0,0 +1,282 @@ +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 + def get_data_by_id(_id, to_dict=True): + return CommonData.get_by(first=True, id=_id, to_dict=to_dict) + + @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() + abort(400, str(e)) + + @staticmethod + def update_data(_id, **kwargs): + existed = CommonDataCRUD.get_data_by_id(_id, to_dict=False) + 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() + abort(400, str(e)) + + @staticmethod + def delete(_id): + existed = CommonDataCRUD.get_data_by_id(_id, to_dict=False) + 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 861bb3fe..7031a2f6 100644 --- a/cmdb-api/api/lib/common_setting/company_info.py +++ b/cmdb-api/api/lib/common_setting/company_info.py @@ -1,5 +1,7 @@ # -*- coding:utf-8 -*- +from urllib.parse import urlparse +from api.extensions import cache from api.models.common_setting import CompanyInfo @@ -11,14 +13,51 @@ def get(): @staticmethod def create(**kwargs): - return CompanyInfo.create(**kwargs) + CompanyInfoCRUD.check_data(**kwargs) + res = CompanyInfo.create(**kwargs) + CompanyInfoCache.refresh(res.info) + return res @staticmethod def update(_id, **kwargs): kwargs.pop('id', None) existed = CompanyInfo.get_by_id(_id) if not existed: - return CompanyInfoCRUD.create(**kwargs) + existed = CompanyInfoCRUD.create(**kwargs) else: + CompanyInfoCRUD.check_data(**kwargs) existed = existed.update(**kwargs) - return existed + 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::' + + @classmethod + def get(cls): + info = cache.get(cls.key) + if not info: + res = CompanyInfo.get_by(first=True) or {} + info = res.get('info', {}) + cache.set(cls.key, info) + return info + + @classmethod + def refresh(cls, info): + 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 c9edccde..f2f2f34e 100644 --- a/cmdb-api/api/lib/common_setting/const.py +++ b/cmdb-api/api/lib/common_setting/const.py @@ -4,11 +4,34 @@ class OperatorType(BaseEnum): - EQUAL = 1 # 等于 - NOT_EQUAL = 2 # 不等于 - IN = 3 # 包含 - NOT_IN = 4 # 不包含 - GREATER_THAN = 5 # 大于 - LESS_THAN = 6 # 小于 - IS_EMPTY = 7 # 为空 - IS_NOT_EMPTY = 8 # 不为空 + EQUAL = 1 + NOT_EQUAL = 2 + IN = 3 + NOT_IN = 4 + GREATER_THAN = 5 + LESS_THAN = 6 + IS_EMPTY = 7 + IS_NOT_EMPTY = 8 + + +BotNameMap = { + 'wechatApp': 'wechatBot', + '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' diff --git a/cmdb-api/api/lib/common_setting/department.py b/cmdb-api/api/lib/common_setting/department.py index 6115014e..2795d5fd 100644 --- a/cmdb-api/api/lib/common_setting/department.py +++ b/cmdb-api/api/lib/common_setting/department.py @@ -1,47 +1,41 @@ # -*- coding:utf-8 -*- -from flask import abort +from flask import abort, current_app from treelib import Tree from wtforms import Form from wtforms import IntegerField from wtforms import StringField from wtforms import validators +from api.extensions import db from api.lib.common_setting.resp_format import ErrFormat -from api.lib.common_setting.utils import get_df_from_read_sql +from api.lib.common_setting.acl import ACLManager from api.lib.perm.acl.role import RoleCRUD from api.models.common_setting import Department, Employee sub_departments_column_name = 'sub_departments' -def drop_ts_column(df): - columns = list(df.columns) - remove_columns = [] - for column in ['created_at', 'updated_at', 'deleted_at', 'last_login']: - targets = list(filter(lambda c: c.startswith(column), columns)) - if targets: - remove_columns.extend(targets) - - remove_columns = list(set(remove_columns)) - - return df.drop(remove_columns, axis=1) if len(remove_columns) > 0 else df - - -def get_department_df(): +def get_all_department_list(to_dict=True): criterion = [ Department.deleted == 0, ] query = Department.query.filter( *criterion - ) - df = get_df_from_read_sql(query) - if df.empty: - return - return drop_ts_column(df) - - -def get_all_employee_df(block=0): + ).order_by(Department.department_id.asc()) + results = query.all() + 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): criterion = [ Employee.deleted == 0, ] @@ -50,112 +44,107 @@ def get_all_employee_df(block=0): Employee.block == block ) - entities = [getattr(Employee, c) for c in Employee.get_columns( - ).keys() if c not in ['deleted', 'deleted_at']] - query = Employee.query.with_entities( - *entities - ).filter( - *criterion - ) - df = get_df_from_read_sql(query) - if df.empty: - return df - return drop_ts_column(df) + results = db.session.query(Employee).filter(*criterion).all() + + DepartmentTreeEmployeeColumns = [ + 'acl_rid', + 'employee_id', + 'username', + 'nickname', + 'email', + 'mobile', + 'direct_supervisor_id', + 'block', + 'department_id', + ] + + def format_columns(e): + return {column: getattr(e, column) for column in DepartmentTreeEmployeeColumns} + + return [format_columns(r) for r in results] if to_dict else results class DepartmentTree(object): def __init__(self, append_employee=False, block=-1): self.append_employee = append_employee self.block = block - self.d_df = get_department_df() - self.employee_df = get_all_employee_df( + self.all_department_list = get_all_department_list() + self.all_employee_list = get_all_employee_list( block) if append_employee else None def prepare(self): pass def get_employees_by_d_id(self, d_id): - _df = self.employee_df[ - self.employee_df['department_id'].eq(d_id) - ].sort_values(by=['direct_supervisor_id'], ascending=True) - if _df.empty: - return [] + block = self.block + + def filter_department_id(e): + if self.block != -1: + return e['department_id'] == d_id and e['block'] == block + return e.department_id == d_id + + results = list(filter(lambda e: filter_department_id(e), self.all_employee_list)) - if self.block != -1: - _df = _df[ - _df['block'].eq(self.block) - ] + return results - return _df.to_dict('records') + def get_department_by_parent_id(self, parent_id): + results = list(filter(lambda d: d['department_parent_id'] == parent_id, self.all_department_list)) + if not results: + return [] + return results def get_tree_departments(self): # 一级部门 - top_df = self.d_df[self.d_df['department_parent_id'].eq(-1)] - if top_df.empty: + top_departments = self.get_department_by_parent_id(-1) + if len(top_departments) == 0: return [] d_list = [] - for index in top_df.index: - top_d = top_df.loc[index].to_dict() - + for top_d in top_departments: department_id = top_d['department_id'] - - # 检查 department_id 是否作为其他部门的 parent - sub_df = self.d_df[ - self.d_df['department_parent_id'].eq(department_id) - ].sort_values(by=['sort_value'], ascending=True) - + sub_deps = self.get_department_by_parent_id(department_id) employees = [] - if self.append_employee: - # 要包含员工 employees = self.get_employees_by_d_id(department_id) top_d['employees'] = employees - - if sub_df.empty: + top_d['department_name'] = ErrFormat.company_wide + if len(sub_deps) == 0: top_d[sub_departments_column_name] = [] d_list.append(top_d) continue - self.parse_sub_department(sub_df, top_d) + self.parse_sub_department(sub_deps, top_d) d_list.append(top_d) return d_list def get_all_departments(self, is_tree=1): - if self.d_df.empty: + if len(self.all_department_list) == 0: return [] if is_tree != 1: - return self.d_df.to_dict('records') + return self.all_department_list return self.get_tree_departments() - def parse_sub_department(self, df, top_d): + def parse_sub_department(self, deps, top_d): sub_departments = [] - for s_index in df.index: - d = df.loc[s_index].to_dict() - sub_df = self.d_df[ - self.d_df['department_parent_id'].eq( - df.at[s_index, 'department_id']) - ].sort_values(by=['sort_value'], ascending=True) + for d in deps: + sub_deps = self.get_department_by_parent_id(d['department_id']) employees = [] - if self.append_employee: - # 要包含员工 - employees = self.get_employees_by_d_id( - df.at[s_index, 'department_id']) + employees = self.get_employees_by_d_id(d['department_id']) d['employees'] = employees - if sub_df.empty: + if len(sub_deps) == 0: d[sub_departments_column_name] = [] sub_departments.append(d) continue - self.parse_sub_department(sub_df, d) + self.parse_sub_department(sub_deps, d) sub_departments.append(d) top_d[sub_departments_column_name] = sub_departments @@ -173,6 +162,10 @@ class DepartmentForm(Form): class DepartmentCRUD(object): + @staticmethod + def get_department_by_id(d_id, to_dict=True): + return Department.get_by(first=True, department_id=d_id, to_dict=to_dict) + @staticmethod def add(**kwargs): DepartmentCRUD.check_department_name_unique(kwargs['department_name']) @@ -202,16 +195,16 @@ def add(**kwargs): def check_department_parent_id_allow(d_id, department_parent_id): if department_parent_id == 0: return - # 检查 department_parent_id 是否在许可范围内 allow_p_d_id_list = DepartmentCRUD.get_allow_parent_d_id_by(d_id) target = list( filter(lambda d: d['department_id'] == department_parent_id, allow_p_d_id_list)) if len(target) == 0: try: - d = Department.get_by( + dep = Department.get_by( first=True, to_dict=False, department_id=department_parent_id) - name = d.department_name if d else ErrFormat.department_id_not_found.format(department_parent_id) + name = dep.department_name if dep else ErrFormat.department_id_not_found.format(department_parent_id) except Exception as e: + current_app.logger.error(str(e)) name = ErrFormat.department_id_not_found.format(department_parent_id) abort(400, ErrFormat.cannot_to_be_parent_department.format(name)) @@ -262,7 +255,7 @@ def edit(_id, **kwargs): 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)) @@ -275,15 +268,12 @@ def delete(_id): try: RoleCRUD.delete_role(existed.acl_rid) except Exception as e: - pass + current_app.logger.error(str(e)) return existed.soft_delete() @staticmethod def get_allow_parent_d_id_by(department_id): - """ - 获取可以成为 department_id 的 department_parent_id 的 list - """ tree_list = DepartmentCRUD.get_department_tree_list() allow_d_id_list = [] @@ -293,7 +283,7 @@ def get_allow_parent_d_id_by(department_id): try: tree.remove_subtree(department_id) except Exception as e: - pass + current_app.logger.error(str(e)) [allow_d_id_list.append({'department_id': int(n.identifier), 'department_name': n.tag}) for n in tree.all_nodes()] @@ -321,58 +311,58 @@ def get_all_departments_with_employee(block): @staticmethod def get_department_tree_list(): - df = get_department_df() - if df.empty: + all_deps = get_all_department_list() + if len(all_deps) == 0: return [] - # 一级部门 - top_df = df[df['department_parent_id'].eq(-1)] - if top_df.empty: + top_deps = list(filter(lambda d: d['department_parent_id'] == -1, all_deps)) + if len(top_deps) == 0: return [] tree_list = [] - for index in top_df.index: + for top_d in top_deps: + top_d['department_name'] = ErrFormat.company_wide tree = Tree() - identifier_root = top_df.at[index, 'department_id'] + identifier_root = top_d['department_id'] tree.create_node( - top_df.at[index, 'department_name'], + top_d['department_name'], identifier_root ) - - # 检查 department_id 是否作为其他部门的 parent - sub_df = df[ - df['department_parent_id'].eq(identifier_root) - ] - if sub_df.empty: + sub_ds = list(filter(lambda d: d['department_parent_id'] == identifier_root, all_deps)) + if len(sub_ds) == 0: tree_list.append(tree) continue DepartmentCRUD.parse_sub_department_node( - sub_df, df, tree, identifier_root) + sub_ds, all_deps, tree, identifier_root) tree_list.append(tree) return tree_list @staticmethod - def parse_sub_department_node(df, all_df, tree, parent_id): - for s_index in df.index: + def parse_sub_department_node(sub_ds, all_ds, tree, parent_id): + for d in sub_ds: tree.create_node( - df.at[s_index, 'department_name'], - df.at[s_index, 'department_id'], + d['department_name'], + d['department_id'], parent=parent_id ) - sub_df = all_df[ - all_df['department_parent_id'].eq( - df.at[s_index, 'department_id']) - ] - if sub_df.empty: + next_sub_ds = list(filter(lambda item_d: item_d['department_parent_id'] == d['department_id'], all_ds)) + if len(next_sub_ds) == 0: continue DepartmentCRUD.parse_sub_department_node( - sub_df, all_df, tree, df.at[s_index, 'department_id']) + next_sub_ds, all_ds, tree, d['department_id']) + + @staticmethod + def get_department_by_query(query, to_dict=True): + results = query.all() + if not results: + return [] + return results if not to_dict else [r.to_dict() for r in results] @staticmethod def get_departments_and_ids(department_parent_id, block): @@ -380,44 +370,33 @@ def get_departments_and_ids(department_parent_id, block): Department.department_parent_id == department_parent_id, Department.deleted == 0, ).order_by(Department.sort_value.asc()) - df = get_df_from_read_sql(query) - if df.empty: + all_departments = DepartmentCRUD.get_department_by_query(query) + if len(all_departments) == 0: return [], [] tree_list = DepartmentCRUD.get_department_tree_list() - employee_df = get_all_employee_df(block) + all_employee_list = get_all_employee_list(block) - department_id_list = list(df['department_id'].values) + department_id_list = [d['department_id'] for d in all_departments] query = Department.query.filter( Department.department_parent_id.in_(department_id_list), Department.deleted == 0, ).order_by(Department.sort_value.asc()).group_by(Department.department_id) - sub_df = get_df_from_read_sql(query) - if sub_df.empty: - df['has_sub'] = 0 - - def handle_row_employee_count(row): - return len(employee_df[employee_df['department_id'] == row['department_id']]) + sub_deps = DepartmentCRUD.get_department_by_query(query) - df['employee_count'] = df.apply( - lambda row: handle_row_employee_count(row), axis=1) + sub_map = {d['department_parent_id']: 1 for d in sub_deps} - else: - sub_map = {d['department_parent_id']: 1 for d in sub_df.to_dict('records')} + for d in all_departments: + d['has_sub'] = sub_map.get(d['department_id'], 0) - def handle_row(row): - d_ids = DepartmentCRUD.get_department_id_list_by_root( - row['department_id'], tree_list) - row['employee_count'] = len( - employee_df[employee_df['department_id'].isin(d_ids)]) + d_ids = DepartmentCRUD.get_department_id_list_by_root(d['department_id'], tree_list) - row['has_sub'] = sub_map.get(row['department_id'], 0) + d['employee_count'] = len(list(filter(lambda e: e['department_id'] in d_ids, all_employee_list))) - return row + if int(department_parent_id) == -1: + d['department_name'] = ErrFormat.company_wide - df = df.apply(lambda row: handle_row(row), axis=1) - - return df.to_dict('records'), department_id_list + return all_departments, department_id_list @staticmethod def get_department_id_list_by_root(root_department_id, tree_list=None): @@ -430,6 +409,125 @@ def get_department_id_list_by_root(root_department_id, tree_list=None): [id_list.append(int(n.identifier)) for n in tmp_tree.all_nodes()] except Exception as e: - pass + current_app.logger.error(str(e)) return id_list + + +class EditDepartmentInACL(object): + + @staticmethod + def add_department_to_acl(department_id, op_uid): + db_department = DepartmentCRUD.get_department_by_id(department_id, to_dict=False) + if not db_department: + return + + from api.models.acl import Role + role = Role.get_by(first=True, name=db_department.department_name, app_id=None) + + acl = ACLManager('acl', str(op_uid)) + if role is None: + payload = { + 'app_id': 'acl', + 'name': db_department.department_name, + } + role = acl.create_role(payload) + + acl_rid = role.get('id') if role else 0 + + db_department.update( + acl_rid=acl_rid + ) + info = f"add_department_to_acl, acl_rid: {acl_rid}" + current_app.logger.info(info) + return info + + @staticmethod + def delete_department_from_acl(department_rids, op_uid): + acl = ACLManager('acl', str(op_uid)) + + result = [] + + for rid in department_rids: + try: + acl.delete_role(rid) + except Exception as e: + result.append(f"delete_department_in_acl, rid: {rid}, error: {e}") + continue + + return result + + @staticmethod + def edit_department_name_in_acl(d_rid: int, d_name: str, op_uid: int): + acl = ACLManager('acl', str(op_uid)) + payload = { + 'name': d_name + } + try: + acl.edit_role(d_rid, payload) + except Exception as e: + return f"edit_department_name_in_acl, rid: {d_rid}, error: {e}" + + return f"edit_department_name_in_acl, rid: {d_rid}, success" + + @staticmethod + def edit_employee_department_in_acl(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: + result.append(f"{new_d_id} new_department is None") + return result + + 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 + if new_d_rid_in_acl == 0: + return + + if new_d_rid_in_acl != new_department.acl_rid: + new_department.update( + acl_rid=new_d_rid_in_acl + ) + 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}") + + 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}") + + return result diff --git a/cmdb-api/api/lib/common_setting/employee.py b/cmdb-api/api/lib/common_setting/employee.py index cf150ca5..28369198 100644 --- a/cmdb-api/api/lib/common_setting/employee.py +++ b/cmdb-api/api/lib/common_setting/employee.py @@ -1,9 +1,9 @@ # -*- coding:utf-8 -*- - +import copy import traceback from datetime import datetime -import pandas as pd +import requests from flask import abort from flask_login import current_user from sqlalchemy import or_, literal_column, func, not_, and_ @@ -15,11 +15,25 @@ 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.cmdb.const import CMDB_QUEUE from api.lib.common_setting.resp_format import ErrFormat -from api.lib.common_setting.utils import get_df_from_read_sql 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', + 'nickname', + 'username', + 'password', + 'block', + 'avatar', +] +employee_pop_columns = ['password'] +can_not_edit_columns = ['email'] + def edit_acl_user(uid, **kwargs): user_data = {column: kwargs.get( @@ -70,9 +84,6 @@ def get_employee_by_id(_id): @staticmethod def get_employee_by_uid_with_create(_uid): - """ - 根据 uid 获取员工信息,不存在则创建 - """ try: return EmployeeCRUD.get_employee_by_uid(_uid).to_dict() except Exception as e: @@ -102,7 +113,6 @@ def check_acl_user_and_create(user_info): acl_uid=user_info['uid'], ) return existed.to_dict() - # 创建员工 if not user_info.get('nickname', None): user_info['nickname'] = user_info['name'] @@ -114,10 +124,25 @@ def check_acl_user_and_create(user_info): employee = CreateEmployee().create_single(**data) return employee.to_dict() + @staticmethod + def add_employee_from_acl_created(**kwargs): + try: + kwargs['acl_uid'] = kwargs.pop('uid') + kwargs['acl_rid'] = kwargs.pop('rid') + kwargs['department_id'] = 0 + + Employee.create( + **kwargs + ) + except Exception as e: + abort(400, str(e)) + @staticmethod def add(**kwargs): try: - return CreateEmployee().create_single(**kwargs) + res = CreateEmployee().create_single(**kwargs) + refresh_employee_acl_info.apply_async(args=(), queue=CMDB_QUEUE) + return res except Exception as e: abort(400, str(e)) @@ -144,13 +169,9 @@ def update(_id, **kwargs): existed.update(**kwargs) if len(e_list) > 0: - from api.tasks.common_setting import edit_employee_department_in_acl - # fixme: comment next line - # edit_employee_department_in_acl(e_list, new_department_id, current_user.uid) - edit_employee_department_in_acl.apply_async( args=(e_list, new_department_id, current_user.uid), - queue=COMMON_SETTING_QUEUE + queue=CMDB_QUEUE ) return existed @@ -161,7 +182,7 @@ def update(_id, **kwargs): def edit_employee_by_uid(_uid, **kwargs): existed = EmployeeCRUD.get_employee_by_uid(_uid) try: - user = edit_acl_user(_uid, **kwargs) + edit_acl_user(_uid, **kwargs) for column in employee_pop_columns: if kwargs.get(column): @@ -173,9 +194,9 @@ def edit_employee_by_uid(_uid, **kwargs): @staticmethod def change_password_by_uid(_uid, password): - existed = EmployeeCRUD.get_employee_by_uid(_uid) + EmployeeCRUD.get_employee_by_uid(_uid) try: - user = edit_acl_user(_uid, password=password) + edit_acl_user(_uid, password=password) except Exception as e: return abort(400, str(e)) @@ -209,173 +230,6 @@ def get_employee_count(block_status): *criterion ).count() - @staticmethod - def import_employee(employee_list): - return CreateEmployee().batch_create(employee_list) - - @staticmethod - def get_export_employee_df(block_status): - criterion = [ - Employee.deleted == 0 - ] - if block_status >= 0: - criterion.append( - Employee.block == block_status - ) - - query = Employee.query.with_entities( - Employee.employee_id, - Employee.nickname, - Employee.email, - Employee.sex, - Employee.mobile, - Employee.position_name, - Employee.last_login, - Employee.department_id, - Employee.direct_supervisor_id, - ).filter(*criterion) - df = get_df_from_read_sql(query) - if df.empty: - return df - - query = Department.query.filter( - *criterion - ) - department_df = get_df_from_read_sql(query) - - def find_name(row): - department_id = row['department_id'] - _df = department_df[department_df['department_id'] - == department_id] - row['department_name'] = '' if _df.empty else _df.iloc[0]['department_name'] - - direct_supervisor_id = row['direct_supervisor_id'] - _df = df[df['employee_id'] == direct_supervisor_id] - row['nickname_direct_supervisor'] = '' if _df.empty else _df.iloc[0]['nickname'] - - if isinstance(row['last_login'], pd.Timestamp): - try: - row['last_login'] = str(row['last_login']) - except: - row['last_login'] = '' - else: - row['last_login'] = '' - - return row - - df = df.apply(find_name, axis=1) - df.drop(['department_id', 'direct_supervisor_id', - 'employee_id'], axis=1, inplace=True) - return df - - @staticmethod - def batch_employee(column_name, column_value, employee_id_list): - if not column_value: - abort(400, ErrFormat.value_is_required) - if column_name in ['password', 'block']: - return EmployeeCRUD.batch_edit_password_or_block_column(column_name, employee_id_list, column_value, True) - - elif column_name in ['department_id']: - return EmployeeCRUD.batch_edit_employee_department(employee_id_list, column_value) - - elif column_name in [ - 'direct_supervisor_id', 'position_name' - ]: - return EmployeeCRUD.batch_edit_column(column_name, employee_id_list, column_value, False) - - else: - abort(400, ErrFormat.column_name_not_support) - - @staticmethod - def batch_edit_employee_department(employee_id_list, column_value): - err_list = [] - employee_list = [] - for _id in employee_id_list: - try: - existed = EmployeeCRUD.get_employee_by_id(_id) - employee = dict( - e_acl_rid=existed.acl_rid, - department_id=existed.department_id - ) - employee_list.append(employee) - existed.update(department_id=column_value) - - except Exception as e: - err_list.append({ - 'employee_id': _id, - 'err': str(e), - }) - from api.tasks.common_setting import edit_employee_department_in_acl - edit_employee_department_in_acl.apply_async( - args=(employee_list, column_value, current_user.uid), - queue=COMMON_SETTING_QUEUE - ) - return err_list - - @staticmethod - def batch_edit_password_or_block_column(column_name, employee_id_list, column_value, is_acl=False): - if column_name == 'block': - err_list = [] - success_list = [] - for _id in employee_id_list: - try: - employee = EmployeeCRUD.edit_employee_block_column( - _id, is_acl, **{column_name: column_value}) - success_list.append(employee) - except Exception as e: - err_list.append({ - 'employee_id': _id, - 'err': str(e), - }) - return err_list - else: - return EmployeeCRUD.batch_edit_column(column_name, employee_id_list, column_value, is_acl) - - @staticmethod - def batch_edit_column(column_name, employee_id_list, column_value, is_acl=False): - err_list = [] - for _id in employee_id_list: - try: - EmployeeCRUD.edit_employee_single_column( - _id, is_acl, **{column_name: column_value}) - except Exception as e: - err_list.append({ - 'employee_id': _id, - 'err': str(e), - }) - - return err_list - - @staticmethod - def edit_employee_single_column(_id, is_acl=False, **kwargs): - existed = EmployeeCRUD.get_employee_by_id(_id) - - if is_acl: - return edit_acl_user(existed.acl_uid, **kwargs) - - try: - for column in employee_pop_columns: - if kwargs.get(column): - kwargs.pop(column) - - return existed.update(**kwargs) - except Exception as e: - return abort(400, str(e)) - - @staticmethod - def edit_employee_block_column(_id, is_acl=False, **kwargs): - existed = EmployeeCRUD.get_employee_by_id(_id) - value = get_block_value(kwargs.get('block')) - if value is True: - # 判断该用户是否为 部门负责人,或者员工的直接上级 - check_department_director_id_or_direct_supervisor_id(_id) - - if is_acl: - kwargs['block'] = value - edit_acl_user(existed.acl_uid, **kwargs) - data = existed.to_dict() - return data - @staticmethod def check_email_unique(email, _id=0): criterion = [ @@ -395,7 +249,7 @@ def check_email_unique(email, _id=0): raise Exception(err) @staticmethod - def get_employee_list_by_body(department_id, block_status, search='', order='', conditions=[], page=1, + def get_employee_list_by_body(department_id, block_status, search='', order='', conditions=None, page=1, page_size=10): criterion = [ Employee.deleted == 0 @@ -441,7 +295,9 @@ def get_employee_list_by_body(department_id, block_status, search='', order='', 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 { @@ -461,7 +317,7 @@ def parse_condition_list_to_query(condition_list): @staticmethod def get_expr_by_condition(column, operator, value, relation): """ - 根据conditions返回expr: (and_list, or_list) + get expr: (and_list, or_list) """ attr = EmployeeCRUD.get_attr_by_column(column) # 根据operator生成条件表达式 @@ -481,7 +337,7 @@ def get_expr_by_condition(column, operator, value, relation): if value: abort(400, ErrFormat.query_column_none_keep_value_empty.format(column)) expr = [attr.is_(None)] - if column not in ["entry_date", "leave_date", "dfc_entry_date", "last_login"]: + if column not in ["last_login"]: expr += [attr == ''] expr = [or_(*expr)] elif operator == OperatorType.IS_NOT_EMPTY: @@ -495,7 +351,6 @@ def get_expr_by_condition(column, operator, value, relation): else: abort(400, ErrFormat.not_support_operator.format(operator)) - # 根据relation生成复合条件 if relation == "&": return expr, [] elif relation == "|": @@ -505,15 +360,16 @@ def get_expr_by_condition(column, operator, value, relation): @staticmethod def check_condition(column, operator, value, relation): - # 对于condition中column为空的,报错 if column is None or operator is None or relation is None: return abort(400, ErrFormat.conditions_field_missing) if value and column == "last_login": try: - value = datetime.strptime(value, "%Y-%m-%d %H:%M:%S") + return datetime.strptime(value, "%Y-%m-%d %H:%M:%S") except Exception as e: - abort(400, ErrFormat.datetime_format_error.format(column)) + err = f"{ErrFormat.datetime_format_error.format(column)}: {str(e)}" + abort(400, err) + return value @staticmethod def get_attr_by_column(column): @@ -534,7 +390,7 @@ def get_query_by_conditions(query, conditions): relation = condition.get("relation", None) value = condition.get("value", None) - EmployeeCRUD.check_condition(column, operator, value, relation) + value = EmployeeCRUD.check_condition(column, operator, value, relation) a, o = EmployeeCRUD.get_expr_by_condition( column, operator, value, relation) and_list += a @@ -587,7 +443,7 @@ def get_employee_list_by(department_id, block_status, search='', order='', page= 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 { @@ -640,6 +496,225 @@ def get_employees_by_department_id(department_id, block): return [r.to_dict() for r in results] + @staticmethod + def remove_bind_notice_by_uid(_platform, _uid): + existed = EmployeeCRUD.get_employee_by_uid(_uid) + employee_data = existed.to_dict() + + notice_info = employee_data.get('notice_info', {}) + notice_info = copy.deepcopy(notice_info) if notice_info else {} + + notice_info[_platform] = '' + + existed.update( + notice_info=notice_info + ) + return ErrFormat.notice_remove_bind_success + + @staticmethod + def bind_notice_by_uid(_platform, _uid): + existed = EmployeeCRUD.get_employee_by_uid(_uid) + mobile = existed.mobile + if not mobile or len(mobile) == 0: + abort(400, ErrFormat.notice_bind_err_with_empty_mobile) + + from api.lib.common_setting.notice_config import NoticeConfigCRUD + messenger = NoticeConfigCRUD.get_messenger_url() + if not messenger or len(messenger) == 0: + abort(400, ErrFormat.notice_please_config_messenger_first) + + url = f"{messenger}/v1/uid/getbyphone" + try: + payload = dict( + phone=mobile, + sender=_platform + ) + res = requests.post(url, json=payload) + result = res.json() + if res.status_code != 200: + raise Exception(result.get('msg', '')) + target_id = result.get('uid', '') + + employee_data = existed.to_dict() + + notice_info = employee_data.get('notice_info', {}) + notice_info = copy.deepcopy(notice_info) if notice_info else {} + + notice_info[_platform] = '' if not target_id else target_id + + existed.update( + notice_info=notice_info + ) + return ErrFormat.notice_bind_success + + except Exception as e: + return abort(400, ErrFormat.notice_bind_failed.format(str(e))) + + @staticmethod + def get_employee_notice_by_ids(employee_ids): + criterion = [ + Employee.employee_id.in_(employee_ids), + Employee.deleted == 0, + ] + direct_columns = ['email', 'mobile'] + employees = Employee.query.filter( + *criterion + ).all() + results = [] + for employee in employees: + d = employee.to_dict() + tmp = dict( + employee_id=employee.employee_id, + ) + for column in direct_columns: + tmp[column] = d.get(column, '') + notice_info = d.get('notice_info', {}) + notice_info = copy.deepcopy(notice_info) if notice_info else {} + tmp.update(**notice_info) + results.append(tmp) + return results + + @staticmethod + def import_employee(employee_list): + res = CreateEmployee().batch_create(employee_list) + refresh_employee_acl_info.apply_async(args=(), queue=CMDB_QUEUE) + return res + + @staticmethod + def batch_edit_employee_department(employee_id_list, column_value): + err_list = [] + employee_list = [] + for _id in employee_id_list: + try: + existed = EmployeeCRUD.get_employee_by_id(_id) + employee = dict( + e_acl_rid=existed.acl_rid, + department_id=existed.department_id + ) + employee_list.append(employee) + existed.update(department_id=column_value) + + except Exception as e: + err_list.append({ + 'employee_id': _id, + 'err': str(e), + }) + from api.lib.common_setting.department import EditDepartmentInACL + EditDepartmentInACL.edit_employee_department_in_acl( + employee_list, column_value, current_user.uid + ) + return err_list + + @staticmethod + def batch_edit_password_or_block_column(column_name, employee_id_list, column_value, is_acl=False): + if column_name == 'block': + err_list = [] + success_list = [] + for _id in employee_id_list: + try: + employee = EmployeeCRUD.edit_employee_block_column( + _id, is_acl, **{column_name: column_value}) + success_list.append(employee) + except Exception as e: + err_list.append({ + 'employee_id': _id, + 'err': str(e), + }) + return err_list + else: + return EmployeeCRUD.batch_edit_column(column_name, employee_id_list, column_value, is_acl) + + @staticmethod + def batch_edit_column(column_name, employee_id_list, column_value, is_acl=False): + err_list = [] + for _id in employee_id_list: + try: + EmployeeCRUD.edit_employee_single_column( + _id, is_acl, **{column_name: column_value}) + except Exception as e: + err_list.append({ + 'employee_id': _id, + 'err': str(e), + }) + + return err_list + + @staticmethod + def edit_employee_single_column(_id, is_acl=False, **kwargs): + existed = EmployeeCRUD.get_employee_by_id(_id) + if 'direct_supervisor_id' in kwargs.keys(): + if kwargs['direct_supervisor_id'] == existed.direct_supervisor_id: + raise Exception(ErrFormat.direct_supervisor_is_not_self) + + if is_acl: + return edit_acl_user(existed.acl_uid, **kwargs) + + try: + for column in employee_pop_columns: + if kwargs.get(column): + kwargs.pop(column) + + return existed.update(**kwargs) + except Exception as e: + return abort(400, str(e)) + + @staticmethod + def edit_employee_block_column(_id, is_acl=False, **kwargs): + existed = EmployeeCRUD.get_employee_by_id(_id) + value = get_block_value(kwargs.get('block')) + if value is True: + check_department_director_id_or_direct_supervisor_id(_id) + value = 1 + else: + value = 0 + + if is_acl: + kwargs['block'] = value + edit_acl_user(existed.acl_uid, **kwargs) + + existed.update(block=value) + data = existed.to_dict() + return data + + @staticmethod + def batch_employee(column_name, column_value, employee_id_list): + if column_value is None: + abort(400, ErrFormat.value_is_required) + if column_name in ['password', 'block']: + return EmployeeCRUD.batch_edit_password_or_block_column(column_name, employee_id_list, column_value, True) + + elif column_name in ['department_id']: + return EmployeeCRUD.batch_edit_employee_department(employee_id_list, column_value) + + elif column_name in [ + 'direct_supervisor_id', 'position_name' + ]: + return EmployeeCRUD.batch_edit_column(column_name, employee_id_list, column_value, False) + + 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): """ @@ -654,19 +729,6 @@ def get_user_map(key='uid', acl=None): return data -acl_user_columns = [ - 'email', - 'mobile', - 'nickname', - 'username', - 'password', - 'block', - 'avatar', -] -employee_pop_columns = ['password'] -can_not_edit_columns = ['email'] - - def format_params(params): for k in ['_key', '_secret']: params.pop(k, None) @@ -676,20 +738,24 @@ def format_params(params): class CreateEmployee(object): def __init__(self): self.acl = ACLManager() - self.useremail_map = {} + self.all_acl_users = self.acl.get_all_users() - def check_acl_user(self, email): - user_info = self.useremail_map.get(email, None) - if user_info: - return user_info - return None + def check_acl_user(self, user_data): + target_email = list(filter(lambda x: x['email'] == user_data['email'], self.all_acl_users)) + if target_email: + return target_email[0] + + target_username = list(filter(lambda x: x['username'] == user_data['username'], self.all_acl_users)) + if target_username: + return target_username[0] def add_acl_user(self, **kwargs): user_data = {column: kwargs.get( column, '') for column in acl_user_columns if kwargs.get(column, '')} try: - existed = self.check_acl_user(user_data['email']) + 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: @@ -697,8 +763,6 @@ def add_acl_user(self, **kwargs): def create_single(self, **kwargs): EmployeeCRUD.check_email_unique(kwargs['email']) - self.useremail_map = self.useremail_map if self.useremail_map else get_user_map( - 'email', self.acl) user = self.add_acl_user(**kwargs) kwargs['acl_uid'] = user['uid'] kwargs['last_login'] = user['last_login'] @@ -711,8 +775,6 @@ def create_single(self, **kwargs): ) def create_single_with_import(self, **kwargs): - self.useremail_map = self.useremail_map if self.useremail_map else get_user_map( - 'email', self.acl) user = self.add_acl_user(**kwargs) kwargs['acl_uid'] = user['uid'] kwargs['last_login'] = user['last_login'] @@ -730,7 +792,8 @@ def create_single_with_import(self, **kwargs): **kwargs ) - def get_department_by_name(self, d_name): + @staticmethod + def get_department_by_name(d_name): return Department.get_by(first=True, department_name=d_name) def get_end_department_id(self, department_name_list, department_name_map): @@ -755,9 +818,6 @@ def get_end_department_id(self, department_name_list, department_name_map): return end_d_id def format_department_id(self, employee): - """ - 部门名称转化为ID,不存在则创建 - """ department_name_map = {} try: department_name = employee.get('department_name', '') @@ -774,16 +834,13 @@ def format_department_id(self, employee): def batch_create(self, employee_list): err_list = [] - self.useremail_map = get_user_map('email', self.acl) for employee in employee_list: try: - # 获取username username = employee.get('username', None) if username is None: employee['username'] = employee['email'] - # 校验通过后获取department_id employee = self.format_department_id(employee) err = employee.get('err', None) if err: @@ -795,7 +852,7 @@ def batch_create(self, employee_list): raise Exception( ','.join(['{}: {}'.format(filed, ','.join(msg)) for filed, msg in form.errors.items()])) - data = self.create_single_with_import(**form.data) + self.create_single_with_import(**form.data) except Exception as e: err_list.append({ 'email': employee.get('email', ''), @@ -809,12 +866,12 @@ def batch_create(self, employee_list): class EmployeeAddForm(Form): username = StringField(validators=[ - validators.DataRequired(message="username不能为空"), + validators.DataRequired(message=ErrFormat.username_is_required), validators.Length(max=255), ]) email = StringField(validators=[ - validators.DataRequired(message="邮箱不能为空"), - validators.Email(message="邮箱格式不正确"), + validators.DataRequired(message=ErrFormat.email_is_required), + validators.Email(message=ErrFormat.email_format_error), validators.Length(max=255), ]) password = StringField(validators=[ @@ -823,7 +880,7 @@ class EmployeeAddForm(Form): position_name = StringField(validators=[]) nickname = StringField(validators=[ - validators.DataRequired(message="用户名不能为空"), + validators.DataRequired(message=ErrFormat.nickname_is_required), validators.Length(max=255), ]) sex = StringField(validators=[]) @@ -834,7 +891,7 @@ class EmployeeAddForm(Form): class EmployeeUpdateByUidForm(Form): nickname = StringField(validators=[ - validators.DataRequired(message="用户名不能为空"), + validators.DataRequired(message=ErrFormat.nickname_is_required), validators.Length(max=255), ]) avatar = StringField(validators=[]) diff --git a/cmdb-api/api/lib/common_setting/notice_config.py b/cmdb-api/api/lib/common_setting/notice_config.py new file mode 100644 index 00000000..152eb53d --- /dev/null +++ b/cmdb-api/api/lib/common_setting/notice_config.py @@ -0,0 +1,165 @@ +import requests + +from api.lib.common_setting.const import BotNameMap +from api.lib.common_setting.resp_format import ErrFormat +from api.models.common_setting import CompanyInfo, NoticeConfig +from wtforms import Form +from wtforms import StringField +from wtforms import validators +from flask import abort, current_app + + +class NoticeConfigCRUD(object): + + @staticmethod + def add_notice_config(**kwargs): + platform = kwargs.get('platform') + NoticeConfigCRUD.check_platform(platform) + info = kwargs.get('info', {}) + if 'name' not in info: + info['name'] = platform + kwargs['info'] = info + try: + NoticeConfigCRUD.update_messenger_config(**info) + res = NoticeConfig.create( + **kwargs + ) + return res + + except Exception as e: + return abort(400, str(e)) + + @staticmethod + def check_platform(platform): + NoticeConfig.get_by(first=True, to_dict=False, platform=platform) and \ + abort(400, ErrFormat.notice_platform_existed.format(platform)) + + @staticmethod + def edit_notice_config(_id, **kwargs): + existed = NoticeConfigCRUD.get_notice_config_by_id(_id) + try: + info = kwargs.get('info', {}) + if 'name' not in info: + info['name'] = existed.platform + kwargs['info'] = info + NoticeConfigCRUD.update_messenger_config(**info) + + res = existed.update(**kwargs) + return res + except Exception as e: + return abort(400, str(e)) + + @staticmethod + def get_messenger_url(): + from api.lib.common_setting.company_info import CompanyInfoCache + com_info = CompanyInfoCache.get() + if not com_info: + return + messenger = com_info.get('messenger', '') + if len(messenger) == 0: + return + if messenger[-1] == '/': + messenger = messenger[:-1] + return messenger + + @staticmethod + def update_messenger_config(**kwargs): + try: + messenger = NoticeConfigCRUD.get_messenger_url() + if not messenger or len(messenger) == 0: + raise Exception(ErrFormat.notice_please_config_messenger_first) + + url = f"{messenger}/v1/senders" + name = kwargs.get('name') + bot_list = kwargs.pop('bot', None) + for k, v in kwargs.items(): + if isinstance(v, bool): + kwargs[k] = 'true' if v else 'false' + else: + kwargs[k] = str(v) + + payload = {name: [kwargs]} + current_app.logger.info(f"update_messenger_config: {url}, {payload}") + res = requests.put(url, json=payload, timeout=2) + current_app.logger.info(f"update_messenger_config: {res.status_code}, {res.text}") + + if not bot_list or len(bot_list) == 0: + return + bot_name = BotNameMap.get(name) + payload = {bot_name: bot_list} + current_app.logger.info(f"update_messenger_config: {url}, {payload}") + bot_res = requests.put(url, json=payload, timeout=2) + current_app.logger.info(f"update_messenger_config: {bot_res.status_code}, {bot_res.text}") + + except Exception as e: + return abort(400, str(e)) + + @staticmethod + def get_notice_config_by_id(_id): + return NoticeConfig.get_by(first=True, to_dict=False, id=_id) or \ + abort(400, + ErrFormat.notice_not_existed.format(_id)) + + @staticmethod + def get_all(): + return NoticeConfig.get_by(to_dict=True) + + @staticmethod + def test_send_email(receive_address, **kwargs): + messenger = NoticeConfigCRUD.get_messenger_url() + if not messenger or len(messenger) == 0: + abort(400, ErrFormat.notice_please_config_messenger_first) + url = f"{messenger}/v1/message" + + recipient_email = receive_address + + subject = 'Test Email' + body = 'This is a test email' + payload = { + "sender": 'email', + "msgtype": "text/plain", + "title": subject, + "content": body, + "tos": [recipient_email], + } + current_app.logger.info(f"test_send_email: {url}, {payload}") + response = requests.post(url, json=payload) + if response.status_code != 200: + abort(400, response.text) + + return 1 + + @staticmethod + def get_app_bot(): + result = [] + for notice_app in NoticeConfig.get_by(to_dict=False): + if notice_app.platform in ['email']: + continue + info = notice_app.info + name = info.get('name', '') + if name not in BotNameMap: + continue + result.append(dict( + name=info.get('name', ''), + label=info.get('label', ''), + bot=info.get('bot', []), + )) + return result + + +class NoticeConfigForm(Form): + platform = StringField(validators=[ + validators.DataRequired(message="平台 不能为空"), + validators.Length(max=255), + ]) + info = StringField(validators=[ + validators.DataRequired(message="信息 不能为空"), + validators.Length(max=255), + ]) + + +class NoticeConfigUpdateForm(Form): + info = StringField(validators=[ + validators.DataRequired(message="信息 不能为空"), + validators.Length(max=255), + ]) diff --git a/cmdb-api/api/lib/common_setting/resp_format.py b/cmdb-api/api/lib/common_setting/resp_format.py index 88f5284c..676e17b9 100644 --- a/cmdb-api/api/lib/common_setting/resp_format.py +++ b/cmdb-api/api/lib/common_setting/resp_format.py @@ -1,51 +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 = "公司信息已存在!无法创建" - - no_file_part = "没有文件部分" - file_is_required = "文件是必须的" - - direct_supervisor_is_not_self = "直属上级不能是自己" - parent_department_is_not_self = "上级部门不能是自己" - employee_list_is_empty = "员工列表为空" - - column_name_not_support = "不支持的列名" - password_is_required = "密码不能为空" - 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 = "新部门是空的" - - 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 导入用户[{}]失败: {}" + company_info_is_already_existed = _l("Company info already existed") # 公司信息已存在!无法创建 + 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 = _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 = _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 = _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 = _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 = _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 = _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 d7eca290..f63bfc09 100644 --- a/cmdb-api/api/lib/common_setting/upload_file.py +++ b/cmdb-api/api/lib/common_setting/upload_file.py @@ -1,11 +1,18 @@ +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): - return '.' in filename and \ - filename.rsplit('.', 1)[1].lower() in allowed_extensions + return '.' in filename and filename.rsplit('.', 1)[1].lower() in allowed_extensions def generate_new_file_name(name): @@ -13,4 +20,75 @@ def generate_new_file_name(name): prev_name = ''.join(name.split(f".{ext}")[:-1]) uid = str(uuid.uuid4()) 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/common_setting/utils.py b/cmdb-api/api/lib/common_setting/utils.py index 4446a05a..138a3155 100644 --- a/cmdb-api/api/lib/common_setting/utils.py +++ b/cmdb-api/api/lib/common_setting/utils.py @@ -1,23 +1,6 @@ # -*- coding:utf-8 -*- from datetime import datetime -import pandas as pd -from sqlalchemy import text - -from api.extensions import db - - -def get_df_from_read_sql(query, to_dict=False): - bind = query.session.bind - query = query.statement.compile(dialect=bind.dialect if bind else None, - compile_kwargs={"literal_binds": True}).string - a = db.engine - df = pd.read_sql(sql=text(query), con=a.connect()) - - if to_dict: - return df.to_dict('records') - return df - def get_cur_time_str(split_flag='-'): f = f"%Y{split_flag}%m{split_flag}%d{split_flag}%H{split_flag}%M{split_flag}%S{split_flag}%f" diff --git a/cmdb-api/api/lib/database.py b/cmdb-api/api/lib/database.py index 5145bd49..634a99f8 100644 --- a/cmdb-api/api/lib/database.py +++ b/cmdb-api/api/lib/database.py @@ -10,14 +10,18 @@ class FormatMixin(object): def to_dict(self): - res = dict([(k, getattr(self, k) if not isinstance( - getattr(self, k), (datetime.datetime, datetime.date, datetime.time)) else str( - getattr(self, k))) for k in getattr(self, "__mapper__").c.keys()]) - # FIXME: getattr(cls, "__table__").columns k.name + res = dict() + for k in getattr(self, "__mapper__").c.keys(): + if k in {'password', '_password', 'secret', '_secret'}: + continue - res.pop('password', None) - res.pop('_password', None) - res.pop('secret', None) + if k.startswith('_'): + k = k[1:] + + if not isinstance(getattr(self, k), (datetime.datetime, datetime.date, datetime.time)): + res[k] = getattr(self, k) + else: + res[k] = str(getattr(self, k)) return res @@ -80,17 +84,17 @@ def delete(self, flush=False, commit=True): db.session.rollback() raise CommitException(str(e)) - def soft_delete(self, flush=False): + def soft_delete(self, flush=False, commit=True): setattr(self, "deleted", True) setattr(self, "deleted_at", datetime.datetime.now()) - self.save(flush=flush) + self.save(flush=flush, commit=commit) @classmethod def get_by_id(cls, _id): 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 @@ -138,8 +142,11 @@ def get_by(cls, first=False, return result[0] if first and result else (None if first else result) @classmethod - def get_by_like(cls, to_dict=True, **kwargs): + def get_by_like(cls, to_dict=True, deleted=False, **kwargs): query = db.session.query(cls) + if hasattr(cls, "deleted") and deleted is not None: + query = query.filter(cls.deleted.is_(deleted)) + for k, v in kwargs.items(): query = query.filter(getattr(cls, k).ilike('%{0}%'.format(v))) return [i.to_dict() if to_dict else i for i in query] diff --git a/cmdb-api/api/lib/decorator.py b/cmdb-api/api/lib/decorator.py index 94b0ce58..26247e6a 100644 --- a/cmdb-api/api/lib/decorator.py +++ b/cmdb-api/api/lib/decorator.py @@ -4,8 +4,14 @@ from functools import wraps from flask import abort +from flask import current_app from flask import request +from sqlalchemy.exc import InvalidRequestError +from sqlalchemy.exc import OperationalError +from sqlalchemy.exc import PendingRollbackError +from sqlalchemy.exc import StatementError +from api.extensions import db from api.lib.resp_format import CommonErrFormat @@ -55,8 +61,8 @@ def wrapper(*args, **kwargs): if exclude_args and arg in exclude_args: continue - if attr.type.python_type == str and attr.type.length and \ - len(request.values[arg] or '') > attr.type.length: + if attr.type.python_type == str and attr.type.length and ( + len(request.values[arg] or '') > attr.type.length): return abort(400, CommonErrFormat.argument_str_length_limit.format(arg, attr.type.length)) elif attr.type.python_type in (int, float) and request.values[arg]: @@ -70,3 +76,43 @@ def wrapper(*args, **kwargs): return wrapper return decorate + + +def reconnect_db(func): + @wraps(func) + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except (StatementError, OperationalError, InvalidRequestError) as e: + error_msg = str(e) + if 'Lost connection' in error_msg or 'reconnect until invalid transaction' in error_msg or \ + 'can be emitted within this transaction' in error_msg: + current_app.logger.info('[reconnect_db] lost connect rollback then retry') + db.session.rollback() + return func(*args, **kwargs) + else: + raise e + except Exception as e: + raise e + + return wrapper + + +def _flush_db(): + try: + db.session.commit() + except (StatementError, OperationalError, InvalidRequestError, PendingRollbackError): + db.session.rollback() + + +def flush_db(func): + @wraps(func) + def wrapper(*args, **kwargs): + _flush_db() + return func(*args, **kwargs) + + return wrapper + + +def run_flush_db(): + _flush_db() diff --git a/cmdb-api/api/lib/http_cli.py b/cmdb-api/api/lib/http_cli.py index 6c59777c..9fcdd280 100644 --- a/cmdb-api/api/lib/http_cli.py +++ b/cmdb-api/api/lib/http_cli.py @@ -4,21 +4,22 @@ import hashlib import requests -from future.moves.urllib.parse import urlparse from flask import abort -from flask import g from flask import current_app +from flask_login import current_user +from future.moves.urllib.parse import urlparse def build_api_key(path, params): - g.user is not None or abort(403, u"您得登陆才能进行该操作") - key = g.user.key - secret = g.user.secret + current_user is not None or abort(403, u"您得登陆才能进行该操作") + key = current_user.key + secret = current_user.secret values = "".join([str(params[k]) for k in sorted(params.keys()) if params[k] is not None]) if params.keys() else "" _secret = "".join([path, secret, values]).encode("utf-8") params["_secret"] = hashlib.sha1(_secret).hexdigest() params["_key"] = key + return params diff --git a/cmdb-api/api/lib/notify.py b/cmdb-api/api/lib/notify.py new file mode 100644 index 00000000..8d6e0701 --- /dev/null +++ b/cmdb-api/api/lib/notify.py @@ -0,0 +1,72 @@ +# -*- coding:utf-8 -*- + +import json + +import requests +import six +from flask import current_app +from jinja2 import Template +from markdownify import markdownify as md + +from api.lib.common_setting.notice_config import NoticeConfigCRUD +from api.lib.mail import send_mail + + +def _request_messenger(subject, body, tos, sender, payload): + params = dict(sender=sender, title=subject, + tos=[to[sender] for to in tos if to.get(sender)]) + + if not params['tos']: + raise Exception("no receivers") + + flat_tos = [] + for i in params['tos']: + if i.strip(): + to = Template(i).render(payload) + if isinstance(to, list): + flat_tos.extend(to) + elif isinstance(to, six.string_types): + flat_tos.append(to) + params['tos'] = flat_tos + + if sender == "email": + params['msgtype'] = 'text/html' + params['content'] = body + else: + params['msgtype'] = 'markdown' + try: + content = md("{}\n{}".format(subject or '', body or '')) + except Exception as e: + current_app.logger.warning("html2markdown failed: {}".format(e)) + content = "{}\n{}".format(subject or '', body or '') + + params['content'] = json.dumps(dict(content=content)) + + url = current_app.config.get('MESSENGER_URL') or NoticeConfigCRUD.get_messenger_url() + if not url: + raise Exception("no messenger url") + + if not url.endswith("message"): + url = "{}/v1/message".format(url) + + resp = requests.post(url, json=params) + if resp.status_code != 200: + raise Exception(resp.text) + + return resp.text + + +def notify_send(subject, body, methods, tos, payload=None): + payload = payload or {} + payload = {k: v or '' for k, v in payload.items()} + subject = Template(subject).render(payload) + body = Template(body).render(payload) + + res = '' + for method in methods: + if method == "email" and not current_app.config.get('USE_MESSENGER', True): + send_mail(None, [Template(to.get('email')).render(payload) for to in tos], subject, body) + + res += (_request_messenger(subject, body, tos, method, payload) + "\n") + + return res diff --git a/cmdb-api/api/lib/perm/acl/acl.py b/cmdb-api/api/lib/perm/acl/acl.py index 66dc7a24..16cc2835 100644 --- a/cmdb-api/api/lib/perm/acl/acl.py +++ b/cmdb-api/api/lib/perm/acl/acl.py @@ -5,8 +5,11 @@ import requests import six -from flask import current_app, g, request -from flask import session, abort +from flask import abort +from flask import current_app +from flask import request +from flask import session +from flask_login import current_user from api.extensions import cache from api.lib.perm.acl.audit import AuditCRUD @@ -84,8 +87,8 @@ def _get_role(self, name): if user: return Role.get_by(name=name, uid=user.uid, first=True, to_dict=False) - return Role.get_by(name=name, app_id=self.app_id, first=True, to_dict=False) or \ - Role.get_by(name=name, first=True, to_dict=False) + return (Role.get_by(name=name, app_id=self.app_id, first=True, to_dict=False) or + Role.get_by(name=name, first=True, to_dict=False)) def add_resource(self, name, resource_type_name=None): resource_type = ResourceType.get_by(name=resource_type_name, first=True, to_dict=False) @@ -114,15 +117,15 @@ def grant_resource_to_role(self, name, role, resource_type_name=None, permission 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) @@ -135,28 +138,28 @@ def revoke_resource_from_role(self, name, role, resource_type_name=None, permiss 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): resource = self._get_resource(name, resource_type_name) if resource: - ResourceCRUD.delete(resource.id) + return ResourceCRUD.delete(resource.id) def has_permission(self, resource_name, resource_type, perm, resource_id=None): if is_app_admin(self.app_id): return True - role = self._get_role(g.user.username) + role = self._get_role(current_user.username) - role or abort(404, ErrFormat.role_not_found.format(g.user.username)) + role or abort(404, ErrFormat.role_not_found.format(current_user.username)) return RoleCRUD.has_permission(role.id, resource_name, resource_type, self.app_id, perm, resource_id=resource_id) @@ -193,9 +196,9 @@ def get_user_info(username, app_id=None): return user def get_resources(self, resource_type_name=None): - role = self._get_role(g.user.username) + role = self._get_role(current_user.username) - role or abort(404, ErrFormat.role_not_found.format(g.user.username)) + role or abort(404, ErrFormat.role_not_found.format(current_user.username)) rid = role.id return RoleCRUD.recursive_resources(rid, self.app_id, resource_type_name).get('resources') @@ -215,7 +218,7 @@ def validate_permission(resources, resource_type, perm, app=None): return if current_app.config.get("USE_ACL"): - if g.user.username == "worker": + if current_user.username == "worker": return resources = [resources] if isinstance(resources, six.string_types) else resources @@ -313,7 +316,7 @@ def wrapper_role_required(*args, **kwargs): return if current_app.config.get("USE_ACL"): - if getattr(g.user, 'username', None) == "worker": + if getattr(current_user, 'username', None) == "worker": return func(*args, **kwargs) if role_name not in session.get("acl", {}).get("parentRoles", []) and not is_app_admin(app): diff --git a/cmdb-api/api/lib/perm/acl/app.py b/cmdb-api/api/lib/perm/acl/app.py index 93772bc0..cf364326 100644 --- a/cmdb-api/api/lib/perm/acl/app.py +++ b/cmdb-api/api/lib/perm/acl/app.py @@ -8,7 +8,9 @@ from flask import current_app from api.extensions import db -from api.lib.perm.acl.audit import AuditCRUD, AuditOperateType, AuditScope +from api.lib.perm.acl.audit import AuditCRUD +from api.lib.perm.acl.audit import AuditOperateType +from api.lib.perm.acl.audit import AuditScope from api.lib.perm.acl.resp_format import ErrFormat from api.models.acl import App diff --git a/cmdb-api/api/lib/perm/acl/audit.py b/cmdb-api/api/lib/perm/acl/audit.py index 819c9d3e..f7413450 100644 --- a/cmdb-api/api/lib/perm/acl/audit.py +++ b/cmdb-api/api/lib/perm/acl/audit.py @@ -1,16 +1,29 @@ # -*- coding:utf-8 -*- + +import datetime import itertools import json from enum import Enum from typing import List -from flask import g, 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 AuditRoleLog, AuditResourceLog, AuditPermissionLog, AuditTriggerLog, RolePermission, \ - Resource, ResourceGroup, Permission, Role, ResourceType +from api.models.acl import AuditLoginLog +from api.models.acl import AuditPermissionLog +from api.models.acl import AuditResourceLog +from api.models.acl import AuditRoleLog +from api.models.acl import AuditTriggerLog +from api.models.acl import Permission +from api.models.acl import Resource +from api.models.acl import ResourceGroup +from api.models.acl import ResourceType +from api.models.acl import Role +from api.models.acl import RolePermission class AuditScope(str, Enum): @@ -49,9 +62,7 @@ class AuditCRUD(object): @staticmethod def get_current_operate_uid(uid=None): - - user_id = uid or (hasattr(g, 'user') and getattr(g.user, 'uid', None)) \ - or getattr(current_user, 'user_id', None) + user_id = uid or (getattr(current_user, 'uid', None)) or getattr(current_user, 'user_id', None) if has_request_context() and request.headers.get('X-User-Id'): _user_id = request.headers['X-User-Id'] @@ -93,11 +104,8 @@ def search_permission(app_id, q=None, page=1, page_size=10, start=None, end=None criterion.append(AuditPermissionLog.operate_type == v) records = AuditPermissionLog.query.filter( - AuditPermissionLog.deleted == 0, - *criterion) \ - .order_by(AuditPermissionLog.id.desc()) \ - .offset((page - 1) * page_size) \ - .limit(page_size).all() + AuditPermissionLog.deleted == 0, *criterion).order_by( + AuditPermissionLog.id.desc()).offset((page - 1) * page_size).limit(page_size).all() data = { 'data': [r.to_dict() for r in records], @@ -160,10 +168,8 @@ def search_role(app_id, q=None, page=1, page_size=10, start=None, end=None): elif k == 'operate_type': criterion.append(AuditRoleLog.operate_type == v) - records = AuditRoleLog.query.filter(AuditRoleLog.deleted == 0, *criterion) \ - .order_by(AuditRoleLog.id.desc()) \ - .offset((page - 1) * page_size) \ - .limit(page_size).all() + records = AuditRoleLog.query.filter(AuditRoleLog.deleted == 0, *criterion).order_by( + AuditRoleLog.id.desc()).offset((page - 1) * page_size).limit(page_size).all() data = { 'data': [r.to_dict() for r in records], @@ -225,11 +231,8 @@ def search_resource(app_id, q=None, page=1, page_size=10, start=None, end=None): criterion.append(AuditResourceLog.operate_type == v) records = AuditResourceLog.query.filter( - AuditResourceLog.deleted == 0, - *criterion) \ - .order_by(AuditResourceLog.id.desc()) \ - .offset((page - 1) * page_size) \ - .limit(page_size).all() + AuditResourceLog.deleted == 0, *criterion).order_by( + AuditResourceLog.id.desc()).offset((page - 1) * page_size).limit(page_size).all() data = { 'data': [r.to_dict() for r in records], @@ -259,11 +262,8 @@ def search_trigger(app_id, q=None, page=1, page_size=10, start=None, end=None): criterion.append(AuditTriggerLog.operate_type == v) records = AuditTriggerLog.query.filter( - AuditTriggerLog.deleted == 0, - *criterion) \ - .order_by(AuditTriggerLog.id.desc()) \ - .offset((page - 1) * page_size) \ - .limit(page_size).all() + AuditTriggerLog.deleted == 0, *criterion).order_by( + AuditTriggerLog.id.desc()).offset((page - 1) * page_size).limit(page_size).all() data = { 'data': [r.to_dict() for r in records], @@ -288,6 +288,27 @@ def search_trigger(app_id, q=None, page=1, page_size=10, start=None, end=None): 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, @@ -353,3 +374,30 @@ def add_trigger_log(cls, app_id, trigger_id, operate_type: AuditOperateType, 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'), + ) + + 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 b4907593..7204dcab 100644 --- a/cmdb-api/api/lib/perm/acl/cache.py +++ b/cmdb-api/api/lib/perm/acl/cache.py @@ -4,7 +4,7 @@ import msgpack from api.extensions import cache -from api.extensions import db +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 @@ -60,15 +60,15 @@ class UserCache(object): @classmethod def get(cls, key): - user = cache.get(cls.PREFIX_ID.format(key)) or \ - cache.get(cls.PREFIX_NAME.format(key)) or \ - cache.get(cls.PREFIX_NICK.format(key)) or \ - cache.get(cls.PREFIX_WXID.format(key)) + user = (cache.get(cls.PREFIX_ID.format(key)) or + cache.get(cls.PREFIX_NAME.format(key)) or + cache.get(cls.PREFIX_NICK.format(key)) or + cache.get(cls.PREFIX_WXID.format(key))) if not user: - user = User.query.get(key) or \ - User.query.get_by_username(key) or \ - User.query.get_by_nickname(key) or \ - User.query.get_by_wxid(key) + user = (User.query.get(key) or + User.query.get_by_username(key) or + User.query.get_by_nickname(key) or + User.query.get_by_wxid(key)) if user: cls.set(user) @@ -221,9 +221,9 @@ def get_resources2(cls, rid, app_id): return msgpack.loads(r_g, raw=False) @classmethod + @flush_db def rebuild(cls, rid, app_id): cls.clean(rid, app_id) - db.session.remove() cls.get_parent_ids(rid, app_id) cls.get_child_ids(rid, app_id) @@ -235,9 +235,9 @@ def rebuild(cls, rid, app_id): cls.get_resources2(rid, app_id) @classmethod + @flush_db def rebuild2(cls, rid, app_id): cache.delete(cls.PREFIX_RESOURCES2.format(rid, app_id)) - db.session.remove() cls.get_resources2(rid, app_id) @classmethod diff --git a/cmdb-api/api/lib/perm/acl/permission.py b/cmdb-api/api/lib/perm/acl/permission.py index f0259cc7..0169ddd4 100644 --- a/cmdb-api/api/lib/perm/acl/permission.py +++ b/cmdb-api/api/lib/perm/acl/permission.py @@ -4,7 +4,9 @@ from flask import abort from api.extensions import db -from api.lib.perm.acl.audit import AuditCRUD, AuditOperateType, AuditOperateSource +from api.lib.perm.acl.audit import AuditCRUD +from api.lib.perm.acl.audit import AuditOperateSource +from api.lib.perm.acl.audit import AuditOperateType from api.lib.perm.acl.cache import PermissionCache from api.lib.perm.acl.cache import RoleCache from api.lib.perm.acl.cache import UserCache @@ -97,8 +99,8 @@ def grant(rid, perms, resource_id=None, group_id=None, rebuild=True, source=Audi elif group_id is not None: from api.models.acl import ResourceGroup - group = ResourceGroup.get_by_id(group_id) or \ - abort(404, ErrFormat.resource_group_not_found.format("id={}".format(group_id))) + group = ResourceGroup.get_by_id(group_id) or abort( + 404, ErrFormat.resource_group_not_found.format("id={}".format(group_id))) app_id = group.app_id rt_id = group.resource_type_id if not perms: @@ -206,8 +208,8 @@ def revoke(rid, perms, resource_id=None, group_id=None, rebuild=True, source=Aud if resource_id is not None: from api.models.acl import Resource - resource = Resource.get_by_id(resource_id) or \ - abort(404, ErrFormat.resource_not_found.format("id={}".format(resource_id))) + resource = Resource.get_by_id(resource_id) or abort( + 404, ErrFormat.resource_not_found.format("id={}".format(resource_id))) app_id = resource.app_id rt_id = resource.resource_type_id if not perms: @@ -216,8 +218,8 @@ def revoke(rid, perms, resource_id=None, group_id=None, rebuild=True, source=Aud elif group_id is not None: from api.models.acl import ResourceGroup - group = ResourceGroup.get_by_id(group_id) or \ - abort(404, ErrFormat.resource_group_not_found.format("id={}".format(group_id))) + group = ResourceGroup.get_by_id(group_id) or abort( + 404, ErrFormat.resource_group_not_found.format("id={}".format(group_id))) app_id = group.app_id rt_id = group.resource_type_id diff --git a/cmdb-api/api/lib/perm/acl/resource.py b/cmdb-api/api/lib/perm/acl/resource.py index c37f6935..7c03e634 100644 --- a/cmdb-api/api/lib/perm/acl/resource.py +++ b/cmdb-api/api/lib/perm/acl/resource.py @@ -5,7 +5,9 @@ from flask import current_app from api.extensions import db -from api.lib.perm.acl.audit import AuditCRUD, AuditOperateType, AuditScope +from api.lib.perm.acl.audit import AuditCRUD +from api.lib.perm.acl.audit import AuditOperateType +from api.lib.perm.acl.audit import AuditScope from api.lib.perm.acl.cache import ResourceCache from api.lib.perm.acl.cache import ResourceGroupCache from api.lib.perm.acl.cache import UserCache @@ -102,8 +104,8 @@ def update(cls, rt_id, **kwargs): @classmethod def delete(cls, rt_id): - rt = ResourceType.get_by_id(rt_id) or \ - abort(404, ErrFormat.resource_type_not_found.format("id={}".format(rt_id))) + rt = ResourceType.get_by_id(rt_id) or abort( + 404, ErrFormat.resource_type_not_found.format("id={}".format(rt_id))) Resource.get_by(resource_type_id=rt_id) and abort(400, ErrFormat.resource_type_cannot_delete) @@ -165,8 +167,8 @@ def get_items(rg_id): @staticmethod def add(name, type_id, app_id, uid=None): - ResourceGroup.get_by(name=name, resource_type_id=type_id, app_id=app_id) and \ - abort(400, ErrFormat.resource_group_exists.format(name)) + ResourceGroup.get_by(name=name, resource_type_id=type_id, app_id=app_id) and abort( + 400, ErrFormat.resource_group_exists.format(name)) rg = ResourceGroup.create(name=name, resource_type_id=type_id, app_id=app_id, uid=uid) AuditCRUD.add_resource_log(app_id, AuditOperateType.create, @@ -175,8 +177,8 @@ def add(name, type_id, app_id, uid=None): @staticmethod def update(rg_id, items): - rg = ResourceGroup.get_by_id(rg_id) or \ - abort(404, ErrFormat.resource_group_not_found.format("id={}".format(rg_id))) + rg = ResourceGroup.get_by_id(rg_id) or abort( + 404, ErrFormat.resource_group_not_found.format("id={}".format(rg_id))) existed = ResourceGroupItems.get_by(group_id=rg_id, to_dict=False) existed_ids = [i.resource_id for i in existed] @@ -196,8 +198,8 @@ def update(rg_id, items): @staticmethod def delete(rg_id): - rg = ResourceGroup.get_by_id(rg_id) or \ - abort(404, ErrFormat.resource_group_not_found.format("id={}".format(rg_id))) + rg = ResourceGroup.get_by_id(rg_id) or abort( + 404, ErrFormat.resource_group_not_found.format("id={}".format(rg_id))) origin = rg.to_dict() rg.soft_delete() @@ -258,7 +260,8 @@ def search(cls, q, u, app_id, resource_type_id=None, page=1, page_size=None): numfound = query.count() res = [i.to_dict() for i in query.offset((page - 1) * page_size).limit(page_size)] for i in res: - i['user'] = UserCache.get(i['uid']).nickname if i['uid'] else '' + user = UserCache.get(i['uid']) if i['uid'] else '' + i['user'] = user and user.nickname return numfound, res @@ -266,14 +269,13 @@ def search(cls, q, u, app_id, resource_type_id=None, page=1, page_size=None): def add(cls, name, type_id, app_id, uid=None): type_id = cls._parse_resource_type_id(type_id, app_id) - Resource.get_by(name=name, resource_type_id=type_id, app_id=app_id) and \ - abort(400, ErrFormat.resource_exists.format(name)) + Resource.get_by(name=name, resource_type_id=type_id, app_id=app_id) and abort( + 400, ErrFormat.resource_exists.format(name)) r = Resource.create(name=name, resource_type_id=type_id, app_id=app_id, uid=uid) from api.tasks.acl import apply_trigger triggers = TriggerCRUD.match_triggers(app_id, r.name, r.resource_type_id, uid) - current_app.logger.info(triggers) for trigger in triggers: # auto trigger should be no uid apply_trigger.apply_async(args=(trigger.id,), @@ -326,6 +328,8 @@ def delete(_id): 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 114010d1..8304f879 100644 --- a/cmdb-api/api/lib/perm/acl/resp_format.py +++ b/cmdb-api/api/lib/perm/acl/resp_format.py @@ -1,42 +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失效(尝试一下退出重新登录)" - - resource_type_not_found = "资源类型 {} 不存在!" - resource_type_exists = "资源类型 {} 已经存在!" - resource_type_cannot_delete = "因为该类型下有资源的存在, 不能删除!" - - user_not_found = "用户 {} 不存在!" - user_exists = "用户 {} 已经存在!" - role_not_found = "角色 {} 不存在!" - role_exists = "角色 {} 已经存在!" - global_role_not_found = "全局角色 {} 不存在!" - global_role_exists = "全局角色 {} 已经存在!" - - resource_no_permission = "您没有资源: {} 的 {} 权限" - admin_required = "需要管理员权限" - role_required = "需要角色: {}" - - app_is_ready_existed = "应用 {} 已经存在" - app_not_found = "应用 {} 不存在!" - app_secret_invalid = "应用的Secret无效" - - resource_not_found = "资源 {} 不存在!" - resource_exists = "资源 {} 已经存在!" - - resource_group_not_found = "资源组 {} 不存在!" - resource_group_exists = "资源组 {} 已经存在!" - - inheritance_dead_loop = "继承检测到了死循环" - role_relation_not_found = "角色关系 {} 不存在!" - - trigger_not_found = "触发器 {} 不存在!" - trigger_exists = "触发器 {} 已经存在!" - trigger_disabled = "触发器 {} 已经被禁用!" - - invalid_password = "密码不正确!" + 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 = _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 = _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 = _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 = _l("Application {} already exists") # 应用 {} 已经存在 + app_not_found = _l("Application {} does not exist!") # 应用 {} 不存在! + app_secret_invalid = _l("The Secret is invalid") # 应用的Secret无效 + + resource_not_found = _l("Resource {} does not exist!") # 资源 {} 不存在! + resource_exists = _l("Resource {} already exists!") # 资源 {} 已经存在! + + resource_group_not_found = _l("Resource group {} does not exist!") # 资源组 {} 不存在! + resource_group_exists = _l("Resource group {} already exists!") # 资源组 {} 已经存在! + + inheritance_dead_loop = _l("Inheritance detected infinite loop") # 继承检测到了死循环 + role_relation_not_found = _l("Role relationship {} does not exist!") # 角色关系 {} 不存在! + + 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/role.py b/cmdb-api/api/lib/perm/acl/role.py index 470e748a..0c2a28c9 100644 --- a/cmdb-api/api/lib/perm/acl/role.py +++ b/cmdb-api/api/lib/perm/acl/role.py @@ -6,6 +6,7 @@ import six from flask import abort from flask import current_app +from sqlalchemy import or_ from api.extensions import db from api.lib.perm.acl.app import AppCRUD @@ -212,18 +213,15 @@ class RoleCRUD(object): @staticmethod def search(q, app_id, page=1, page_size=None, user_role=True, is_all=False, user_only=False): - query = db.session.query(Role).filter(Role.deleted.is_(False)) - query1 = query.filter(Role.app_id == app_id).filter(Role.uid.is_(None)) - query2 = query.filter(Role.app_id.is_(None)).filter(Role.uid.is_(None)) - query = query1.union(query2) - - if user_role: - query1 = db.session.query(Role).filter(Role.deleted.is_(False)).filter(Role.uid.isnot(None)) - query = query.union(query1) - - if user_only: + if user_only: # only user role query = db.session.query(Role).filter(Role.deleted.is_(False)).filter(Role.uid.isnot(None)) + else: + query = db.session.query(Role).filter(Role.deleted.is_(False)).filter( + or_(Role.app_id == app_id, Role.app_id.is_(None))) + if not user_role: # only virtual role + query = query.filter(Role.uid.is_(None)) + if not is_all: role_ids = list(HasResourceRoleCache.get(app_id).keys()) query = query.filter(Role.id.in_(role_ids)) @@ -272,6 +270,13 @@ def update_role(rid, **kwargs): RoleCache.clean(rid) role = role.update(**kwargs) + + if origin['uid'] and kwargs.get('name') and kwargs.get('name') != origin['name']: + from api.models.acl import User + user = User.get_by(uid=origin['uid'], first=True, to_dict=False) + if user: + user.update(username=kwargs['name']) + AuditCRUD.add_role_log(role.app_id, AuditOperateType.update, AuditScope.role, role.id, origin, role.to_dict(), {}, ) @@ -286,14 +291,15 @@ def get_by_name(name, app_id): return role @classmethod - def delete_role(cls, rid): + def delete_role(cls, rid, force=False): from api.lib.perm.acl.acl import is_admin role = Role.get_by_id(rid) or abort(404, ErrFormat.role_not_found.format("rid={}".format(rid))) - if not role.app_id and not is_admin(): return abort(403, ErrFormat.admin_required) + not force and role.uid and abort(400, ErrFormat.user_role_delete_invalid) + origin = role.to_dict() child_ids = [] diff --git a/cmdb-api/api/lib/perm/acl/trigger.py b/cmdb-api/api/lib/perm/acl/trigger.py index f5a5d3f9..8035e4fe 100644 --- a/cmdb-api/api/lib/perm/acl/trigger.py +++ b/cmdb-api/api/lib/perm/acl/trigger.py @@ -6,9 +6,10 @@ import re from fnmatch import fnmatch -from flask import abort, current_app +from flask import abort -from api.lib.perm.acl.audit import AuditCRUD, AuditOperateType +from api.lib.perm.acl.audit import AuditCRUD +from api.lib.perm.acl.audit import AuditOperateType from api.lib.perm.acl.cache import UserCache from api.lib.perm.acl.const import ACL_QUEUE from api.lib.perm.acl.resp_format import ErrFormat diff --git a/cmdb-api/api/lib/perm/acl/user.py b/cmdb-api/api/lib/perm/acl/user.py index 733cb0f6..d17af58c 100644 --- a/cmdb-api/api/lib/perm/acl/user.py +++ b/cmdb-api/api/lib/perm/acl/user.py @@ -6,10 +6,12 @@ import uuid from flask import abort -from flask import g +from flask_login import current_user from api.extensions import db -from api.lib.perm.acl.audit import AuditCRUD, AuditOperateType, AuditScope +from api.lib.perm.acl.audit import AuditCRUD +from api.lib.perm.acl.audit import AuditOperateType +from api.lib.perm.acl.audit import AuditScope from api.lib.perm.acl.cache import UserCache from api.lib.perm.acl.resp_format import ErrFormat from api.lib.perm.acl.role import RoleCRUD @@ -39,27 +41,35 @@ def gen_key_secret(): @classmethod def add(cls, **kwargs): - existed = User.get_by(username=kwargs['username'], email=kwargs['email']) + add_from = kwargs.pop('add_from', None) + existed = User.get_by(username=kwargs['username']) existed and abort(400, ErrFormat.user_exists.format(kwargs['username'])) + existed = User.get_by(username=kwargs['email']) + existed and abort(400, ErrFormat.user_exists.format(kwargs['email'])) + kwargs['nickname'] = kwargs.get('nickname') or kwargs['username'] kwargs['block'] = 0 kwargs['key'], kwargs['secret'] = cls.gen_key_secret() - user_employee = db.session.query(User).filter(User.deleted.is_(False)).order_by( - User.employee_id.desc()).first() + user_employee = db.session.query(User).filter(User.deleted.is_(False)).order_by(User.employee_id.desc()).first() - biggest_employee_id = int(float(user_employee.employee_id)) \ - if user_employee is not None else 0 + biggest_employee_id = int(float(user_employee.employee_id)) if user_employee is not None else 0 kwargs['employee_id'] = '{0:04d}'.format(biggest_employee_id + 1) user = User.create(**kwargs) - RoleCRUD.add_role(user.username, uid=user.uid) + role = RoleCRUD.add_role(user.username, uid=user.uid) AuditCRUD.add_role_log(None, AuditOperateType.create, AuditScope.user, user.uid, {}, user.to_dict(), {}, {} ) + 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 @staticmethod @@ -90,9 +100,9 @@ def update(uid, **kwargs): @classmethod def reset_key_secret(cls): key, secret = cls.gen_key_secret() - g.user.update(key=key, secret=secret) + current_user.update(key=key, secret=secret) - UserCache.clean(g.user) + UserCache.clean(current_user) return key, secret @@ -103,10 +113,14 @@ def delete(cls, uid): origin = user.to_dict() - user.soft_delete() + user.delete() UserCache.clean(user) + role = RoleCRUD.get_by_name(user.username, app_id=None) + if role: + RoleCRUD.delete_role(role[0]['id'], force=True) + AuditCRUD.add_role_log(None, AuditOperateType.delete, AuditScope.user, user.uid, origin, {}, {}, {}) diff --git a/cmdb-api/api/lib/perm/auth.py b/cmdb-api/api/lib/perm/auth.py index 7f53e773..c00f8b06 100644 --- a/cmdb-api/api/lib/perm/auth.py +++ b/cmdb-api/api/lib/perm/auth.py @@ -7,7 +7,6 @@ import jwt from flask import abort from flask import current_app -from flask import g from flask import request from flask import session from flask_login import login_user @@ -64,12 +63,10 @@ def _auth_with_key(): def _auth_with_session(): - if isinstance(getattr(g, 'user', None), User): - login_user(g.user) - return True if "acl" in session and "userName" in (session["acl"] or {}): login_user(UserCache.get(session["acl"]["userName"])) return True + return False @@ -96,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') @@ -108,7 +108,7 @@ def _auth_with_ip_white_list(): def _auth_with_app_token(): - if _auth_with_session(): + if _auth_with_session() or _auth_with_token(): if not is_app_admin(request.values.get('app_id')) and request.method != "GET": return False elif is_app_admin(request.values.get('app_id')): @@ -157,7 +157,7 @@ def _auth_with_acl_token(): def auth_required(func): - if request.json is not None: + if request.get_json(silent=True) is not None: setattr(request, 'values', request.json) else: setattr(request, 'values', request.values.to_dict()) 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 00000000..380474e0 --- /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 dc31cf57..5f0fd526 100644 --- a/cmdb-api/api/flask_cas/__init__.py +++ b/cmdb-api/api/lib/perm/authentication/cas/__init__.py @@ -15,7 +15,7 @@ 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 34e15d39..4cbba477 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 65% rename from cmdb-api/api/flask_cas/routing.py rename to cmdb-api/api/lib/perm/authentication/cas/routing.py index 7341027b..68b92413 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 @@ 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) @@ -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 00000000..64e32393 --- /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 00000000..1b7d02e1 --- /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 00000000..dfc42d86 --- /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 e3cf4da6..48eca6fc 100644 --- a/cmdb-api/api/lib/resp_format.py +++ b/cmdb-api/api/lib/resp_format.py @@ -1,27 +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 = _l("Illegal request") # 不合法的请求 + invalid_operation = _l("Invalid operation") # 无效的操作 - invalid_request = "不合法的请求" - invalid_operation = "无效的操作" + not_found = _l("does not exist") # 不存在 - not_found = "不存在" + 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/secrets/__init__.py b/cmdb-api/api/lib/secrets/__init__.py new file mode 100644 index 00000000..380474e0 --- /dev/null +++ b/cmdb-api/api/lib/secrets/__init__.py @@ -0,0 +1 @@ +# -*- coding:utf-8 -*- diff --git a/cmdb-api/api/lib/secrets/inner.py b/cmdb-api/api/lib/secrets/inner.py new file mode 100644 index 00000000..33e86141 --- /dev/null +++ b/cmdb-api/api/lib/secrets/inner.py @@ -0,0 +1,429 @@ +import os +import secrets +import sys +from base64 import b64decode, b64encode + +from Cryptodome.Protocol.SecretSharing import Shamir +from colorama import Back +from colorama import Fore +from colorama import Style +from colorama import init as colorama_init +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives import padding +from cryptography.hazmat.primitives.ciphers import Cipher +from cryptography.hazmat.primitives.ciphers import algorithms +from cryptography.hazmat.primitives.ciphers import modes +from cryptography.hazmat.primitives.ciphers.aead import AESGCM +from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC +from flask import current_app + +global_iv_length = 16 +global_key_shares = 5 # Number of generated key shares +global_key_threshold = 3 # Minimum number of shares required to rebuild the key + +backend_root_key_name = "root_key" +backend_encrypt_key_name = "encrypt_key" +backend_root_key_salt_name = "root_key_salt" +backend_encrypt_key_salt_name = "encrypt_key_salt" +backend_seal_key = "seal_status" +success = "success" +seal_status = True + + +def string_to_bytes(value): + if isinstance(value, bytes): + return value + if sys.version_info.major == 2: + byte_string = value + else: + byte_string = value.encode("utf-8") + return byte_string + + +class Backend: + def __init__(self, backend=None): + self.backend = backend + + def get(self, key): + return self.backend.get(key) + + def add(self, key, value): + return self.backend.add(key, value) + + def update(self, key, value): + return self.backend.update(key, value) + + +class KeyManage: + + def __init__(self, trigger=None, backend=None): + self.trigger = trigger + self.backend = backend + if backend: + self.backend = Backend(backend) + + def init_app(self, app, backend=None): + if (sys.argv[0].endswith("gunicorn") or + (len(sys.argv) > 1 and sys.argv[1] in ("run", "cmdb-password-data-migrate"))): + self.trigger = app.config.get("INNER_TRIGGER_TOKEN") + if not self.trigger: + return + + self.backend = backend + resp = self.auto_unseal() + self.print_response(resp) + + def hash_root_key(self, value): + algorithm = hashes.SHA256() + salt = self.backend.get(backend_root_key_salt_name) + if not salt: + salt = secrets.token_hex(16) + msg, ok = self.backend.add(backend_root_key_salt_name, salt) + if not ok: + return msg, ok + + kdf = PBKDF2HMAC( + algorithm=algorithm, + length=32, + salt=string_to_bytes(salt), + iterations=100000, + ) + key = kdf.derive(string_to_bytes(value)) + + return b64encode(key).decode('utf-8'), True + + def generate_encrypt_key(self, key): + algorithm = hashes.SHA256() + salt = self.backend.get(backend_encrypt_key_salt_name) + if not salt: + salt = secrets.token_hex(32) + + kdf = PBKDF2HMAC( + algorithm=algorithm, + length=32, + salt=string_to_bytes(salt), + iterations=100000, + backend=default_backend() + ) + key = kdf.derive(string_to_bytes(key)) + msg, ok = self.backend.add(backend_encrypt_key_salt_name, salt) + if ok: + return b64encode(key).decode('utf-8'), ok + else: + return msg, ok + + @classmethod + def generate_keys(cls, secret): + shares = Shamir.split(global_key_threshold, global_key_shares, secret, False) + new_shares = [] + for share in shares: + t = [i for i in share[1]] + [ord(i) for i in "{:0>2}".format(share[0])] + new_shares.append(b64encode(bytes(t))) + + return new_shares + + def is_valid_root_key(self, root_key): + root_key_hash, ok = self.hash_root_key(root_key) + if not ok: + return root_key_hash, ok + backend_root_key_hash = self.backend.get(backend_root_key_name) + if not backend_root_key_hash: + return "should init firstly", False + elif backend_root_key_hash != root_key_hash: + return "invalid root key", False + else: + return "", True + + def auth_root_secret(self, root_key): + msg, ok = self.is_valid_root_key(root_key) + if not ok: + return { + "message": msg, + "status": "failed" + } + + encrypt_key_aes = self.backend.get(backend_encrypt_key_name) + if not encrypt_key_aes: + return { + "message": "encrypt key is empty", + "status": "failed" + } + + secrets_encrypt_key, ok = InnerCrypt.aes_decrypt(string_to_bytes(root_key), encrypt_key_aes) + if ok: + msg, ok = self.backend.update(backend_seal_key, "open") + if ok: + current_app.config["secrets_encrypt_key"] = secrets_encrypt_key + current_app.config["secrets_root_key"] = root_key + current_app.config["secrets_shares"] = [] + return {"message": success, "status": success} + return {"message": msg, "status": "failed"} + else: + return { + "message": secrets_encrypt_key, + "status": "failed" + } + + def unseal(self, key): + if not self.is_seal(): + return { + "message": "current status is unseal, skip", + "status": "skip" + } + + try: + t = [i for i in b64decode(key)] + v = (int("".join([chr(i) for i in t[-2:]])), bytes(t[:-2])) + shares = current_app.config.get("secrets_shares", []) + if v not in shares: + shares.append(v) + current_app.config["secrets_shares"] = shares + + if len(shares) >= global_key_threshold: + recovered_secret = Shamir.combine(shares[:global_key_threshold], False) + return self.auth_root_secret(b64encode(recovered_secret)) + else: + return { + "message": "waiting for inputting other unseal key {0}/{1}".format(len(shares), + global_key_threshold), + "status": "waiting" + } + except Exception as e: + return { + "message": "invalid token: " + str(e), + "status": "failed" + } + + def generate_unseal_keys(self): + info = self.backend.get(backend_root_key_name) + if info: + return "already exist", [], False + + secret = AESGCM.generate_key(128) + shares = self.generate_keys(secret) + + return b64encode(secret), shares, True + + def init(self): + """ + init the master key, unseal key and store in backend + :return: + """ + root_key = self.backend.get(backend_root_key_name) + if root_key: + return {"message": "already init, skip", "status": "skip"}, False + else: + root_key, shares, status = self.generate_unseal_keys() + if not status: + return {"message": root_key, "status": "failed"}, False + + # hash root key and store in backend + root_key_hash, ok = self.hash_root_key(root_key) + if not ok: + return {"message": root_key_hash, "status": "failed"}, False + + msg, ok = self.backend.add(backend_root_key_name, root_key_hash) + if not ok: + return {"message": msg, "status": "failed"}, False + + # generate encrypt key from root_key and store in backend + encrypt_key, ok = self.generate_encrypt_key(root_key) + if not ok: + return {"message": encrypt_key, "status": "failed"} + + encrypt_key_aes, status = InnerCrypt.aes_encrypt(root_key, encrypt_key) + if not status: + return {"message": encrypt_key_aes, "status": "failed"} + + msg, ok = self.backend.add(backend_encrypt_key_name, encrypt_key_aes) + if not ok: + return {"message": msg, "status": "failed"}, False + msg, ok = self.backend.add(backend_seal_key, "open") + if not ok: + return {"message": msg, "status": "failed"}, False + current_app.config["secrets_root_key"] = root_key + current_app.config["secrets_encrypt_key"] = encrypt_key + self.print_token(shares, root_token=root_key) + + return {"message": "OK", + "details": { + "root_token": root_key, + "seal_tokens": shares, + }}, True + + def auto_unseal(self): + if not self.trigger: + return { + "message": "trigger config is empty, skip", + "status": "skip" + } + + if self.trigger.startswith("http"): + return { + "message": "todo in next step, skip", + "status": "skip" + } + # TODO + elif len(self.trigger.strip()) == 24: + res = self.auth_root_secret(self.trigger.encode()) + if res.get("status") == success: + return { + "message": success, + "status": success + } + else: + return { + "message": res.get("message"), + "status": "failed" + } + else: + return { + "message": "trigger config is invalid, skip", + "status": "skip" + } + + def seal(self, root_key): + root_key = root_key.encode() + msg, ok = self.is_valid_root_key(root_key) + if not ok: + return { + "message": msg, + "status": "failed" + } + else: + msg, ok = self.backend.update(backend_seal_key, "block") + if not ok: + return { + "message": msg, + "status": "failed", + } + current_app.config["secrets_root_key"] = '' + current_app.config["secrets_encrypt_key"] = '' + return { + "message": success, + "status": success + } + + def is_seal(self): + """ + If there is no initialization or the root key is inconsistent, it is considered to be in a sealed state. + :return: + """ + secrets_root_key = current_app.config.get("secrets_root_key") + msg, ok = self.is_valid_root_key(secrets_root_key) + if not ok: + return true + status = self.backend.get(backend_seal_key) + return status == "block" + + @classmethod + def print_token(cls, shares, root_token): + """ + data: {"message": "OK", + "details": { + "root_token": root_key, + "seal_tokens": shares, + }} + """ + colorama_init() + print(Style.BRIGHT, "Please be sure to store the Unseal Key in a secure location and avoid losing it." + " The Unseal Key is required to unseal the system every time when it restarts." + " Successful unsealing is necessary to enable the password feature." + Style.RESET_ALL) + + for i, v in enumerate(shares): + print( + "unseal token " + str(i + 1) + ": " + Fore.RED + Back.BLACK + v.decode("utf-8") + Style.RESET_ALL) + print() + + print(Fore.GREEN + "root token: " + root_token.decode("utf-8") + Style.RESET_ALL) + + @classmethod + def print_response(cls, data): + status = data.get("status", "") + message = data.get("message", "") + status_colors = { + "skip": Style.BRIGHT, + "failed": Fore.RED, + "waiting": Fore.YELLOW, + } + print(status_colors.get(status, Fore.GREEN), message, Style.RESET_ALL) + + +class InnerCrypt: + def __init__(self): + secrets_encrypt_key = current_app.config.get("secrets_encrypt_key", "") + self.encrypt_key = b64decode(secrets_encrypt_key.encode("utf-8")) + + def encrypt(self, plaintext): + """ + encrypt method contain aes currently + """ + return self.aes_encrypt(self.encrypt_key, plaintext) + + def decrypt(self, ciphertext): + """ + decrypt method contain aes currently + """ + return self.aes_decrypt(self.encrypt_key, ciphertext) + + @classmethod + def aes_encrypt(cls, key, plaintext): + if isinstance(plaintext, str): + plaintext = string_to_bytes(plaintext) + iv = os.urandom(global_iv_length) + try: + cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend()) + encryptor = cipher.encryptor() + v_padder = padding.PKCS7(algorithms.AES.block_size).padder() + padded_plaintext = v_padder.update(plaintext) + v_padder.finalize() + ciphertext = encryptor.update(padded_plaintext) + encryptor.finalize() + + return b64encode(iv + ciphertext).decode("utf-8"), True + except Exception as e: + return str(e), False + + @classmethod + def aes_decrypt(cls, key, ciphertext): + try: + s = b64decode(ciphertext.encode("utf-8")) + iv = s[:global_iv_length] + ciphertext = s[global_iv_length:] + cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend()) + decrypter = cipher.decryptor() + decrypted_padded_plaintext = decrypter.update(ciphertext) + decrypter.finalize() + unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder() + plaintext = unpadder.update(decrypted_padded_plaintext) + unpadder.finalize() + + return plaintext.decode('utf-8'), True + except Exception as e: + return str(e), False + + +if __name__ == "__main__": + + km = KeyManage() + # info, shares, status = km.generate_unseal_keys() + # print(info, shares, status) + # print("..................") + # for i in shares: + # print(b64encode(i[1]).decode()) + + res1, ok1 = km.init() + if not ok1: + print(res1) + # for j in res["details"]["seal_tokens"]: + # r = km.unseal(j) + # if r["status"] != "waiting": + # if r["status"] != "success": + # print("r........", r) + # else: + # print(r) + # break + + t_plaintext = b"Hello, World!" # The plaintext to encrypt + c = InnerCrypt() + t_ciphertext, status1 = c.encrypt(t_plaintext) + print("Ciphertext:", t_ciphertext) + decrypted_plaintext, status2 = c.decrypt(t_ciphertext) + print("Decrypted plaintext:", decrypted_plaintext) diff --git a/cmdb-api/api/lib/secrets/secrets.py b/cmdb-api/api/lib/secrets/secrets.py new file mode 100644 index 00000000..674f570c --- /dev/null +++ b/cmdb-api/api/lib/secrets/secrets.py @@ -0,0 +1,35 @@ +from api.models.cmdb import InnerKV + + +class InnerKVManger(object): + def __init__(self): + pass + + @classmethod + def add(cls, key, value): + data = {"key": key, "value": value} + res = InnerKV.create(**data) + if res.key == key: + return "success", True + + return "add failed", False + + @classmethod + def get(cls, key): + res = InnerKV.get_by(first=True, to_dict=False, key=key) + if not res: + return None + + return res.value + + @classmethod + def update(cls, key, value): + res = InnerKV.get_by(first=True, to_dict=False, key=key) + if not res: + return cls.add(key, value) + + t = res.update(value=value) + if t.key == key: + return "success", True + + return "update failed", True diff --git a/cmdb-api/api/lib/secrets/vault.py b/cmdb-api/api/lib/secrets/vault.py new file mode 100644 index 00000000..a5746f55 --- /dev/null +++ b/cmdb-api/api/lib/secrets/vault.py @@ -0,0 +1,141 @@ +from base64 import b64decode +from base64 import b64encode + +import hvac + + +class VaultClient: + def __init__(self, base_url, token, mount_path='cmdb'): + self.client = hvac.Client(url=base_url, token=token) + self.mount_path = mount_path + + def create_app_role(self, role_name, policies): + resp = self.client.create_approle(role_name, policies=policies) + + return resp == 200 + + def delete_app_role(self, role_name): + resp = self.client.delete_approle(role_name) + + return resp == 204 + + def update_app_role_policies(self, role_name, policies): + resp = self.client.update_approle_role(role_name, policies=policies) + + return resp == 204 + + def get_app_role(self, role_name): + resp = self.client.get_approle(role_name) + resp.json() + if resp.status_code == 200: + return resp.json + else: + return {} + + def enable_secrets_engine(self): + resp = self.client.sys.enable_secrets_engine('kv', path=self.mount_path) + resp_01 = self.client.sys.enable_secrets_engine('transit') + + if resp.status_code == 200 and resp_01.status_code == 200: + return resp.json + else: + return {} + + def encrypt(self, plaintext): + response = self.client.secrets.transit.encrypt_data(name='transit-key', plaintext=plaintext) + ciphertext = response['data']['ciphertext'] + + return ciphertext + + # decrypt data + def decrypt(self, ciphertext): + response = self.client.secrets.transit.decrypt_data(name='transit-key', ciphertext=ciphertext) + plaintext = response['data']['plaintext'] + + return plaintext + + def write(self, path, data, encrypt=None): + if encrypt: + for k, v in data.items(): + data[k] = self.encrypt(self.encode_base64(v)) + response = self.client.secrets.kv.v2.create_or_update_secret( + path=path, + secret=data, + mount_point=self.mount_path + ) + + return response + + # read data + def read(self, path, decrypt=True): + try: + response = self.client.secrets.kv.v2.read_secret_version( + path=path, raise_on_deleted_version=False, mount_point=self.mount_path + ) + except Exception as e: + return str(e), False + data = response['data']['data'] + if decrypt: + try: + for k, v in data.items(): + data[k] = self.decode_base64(self.decrypt(v)) + except: + return data, True + + return data, True + + # update data + def update(self, path, data, overwrite=True, encrypt=True): + if encrypt: + for k, v in data.items(): + data[k] = self.encrypt(self.encode_base64(v)) + if overwrite: + response = self.client.secrets.kv.v2.create_or_update_secret( + path=path, + secret=data, + mount_point=self.mount_path + ) + else: + response = self.client.secrets.kv.v2.patch(path=path, secret=data, mount_point=self.mount_path) + + return response + + # delete data + def delete(self, path): + response = self.client.secrets.kv.v2.delete_metadata_and_all_versions( + path=path, + mount_point=self.mount_path + ) + + return response + + # Base64 encode + @classmethod + def encode_base64(cls, data): + encoded_bytes = b64encode(data.encode()) + encoded_string = encoded_bytes.decode() + + return encoded_string + + # Base64 decode + @classmethod + def decode_base64(cls, encoded_string): + decoded_bytes = b64decode(encoded_string) + decoded_string = decoded_bytes.decode() + + return decoded_string + + +if __name__ == "__main__": + _base_url = "http://localhost:8200" + _token = "your token" + + _path = "test001" + # Example + sdk = VaultClient(_base_url, _token) + # sdk.enable_secrets_engine() + _data = {"key1": "value1", "key2": "value2", "key3": "value3"} + _data = sdk.update(_path, _data, overwrite=True, encrypt=True) + print(_data) + _data = sdk.read(_path, decrypt=True) + print(_data) diff --git a/cmdb-api/api/lib/utils.py b/cmdb-api/api/lib/utils.py index b4dd7f84..eddc7b8f 100644 --- a/cmdb-api/api/lib/utils.py +++ b/cmdb-api/api/lib/utils.py @@ -1,7 +1,6 @@ # -*- coding:utf-8 -*- import base64 -import json import sys import time from typing import Set @@ -13,6 +12,9 @@ from elasticsearch import Elasticsearch from flask import current_app +from api.lib.secrets.inner import InnerCrypt +from api.lib.secrets.inner import KeyManage + class BaseEnum(object): _ALL_ = set() # type: Set[str] @@ -113,7 +115,7 @@ def delete(self, key_id, prefix): try: ret = self.r.hdel(prefix, key_id) if not ret: - current_app.logger.warn("[{0}] is not in redis".format(key_id)) + current_app.logger.warning("[{0}] is not in redis".format(key_id)) except Exception as e: current_app.logger.error("delete redis key error, {0}".format(str(e))) @@ -204,9 +206,9 @@ def read(self, query, filter_path=None): res = self.es.search(index=self.index, body=query, filter_path=filter_path) if res['hits'].get('hits'): - return res['hits']['total']['value'], \ - [i['_source'] for i in res['hits']['hits']], \ - res.get("aggregations", {}) + return (res['hits']['total']['value'], + [i['_source'] for i in res['hits']['hits']], + res.get("aggregations", {})) else: return 0, [], {} @@ -257,93 +259,10 @@ def __exit__(self, exc_type, exc_val, exc_tb): self.release() -class Redis2Handler(object): - def __init__(self, flask_app=None, prefix=None): - self.flask_app = flask_app - self.prefix = prefix - self.r = None - - def init_app(self, app): - self.flask_app = app - config = self.flask_app.config - try: - pool = redis.ConnectionPool( - max_connections=config.get("REDIS_MAX_CONN"), - host=config.get("ONEAGENT_REDIS_HOST"), - port=config.get("ONEAGENT_REDIS_PORT"), - db=config.get("ONEAGENT_REDIS_DB"), - password=config.get("ONEAGENT_REDIS_PASSWORD") - ) - self.r = redis.Redis(connection_pool=pool) - except Exception as e: - current_app.logger.warning(str(e)) - current_app.logger.error("init redis connection failed") - - def get(self, key): - try: - value = json.loads(self.r.get(key)) - except: - return - - return value - - def lrange(self, key, start=0, end=-1): - try: - value = "".join(map(redis_decode, self.r.lrange(key, start, end) or [])) - except: - return - - return value - - def lrange2(self, key, start=0, end=-1): - try: - return list(map(redis_decode, self.r.lrange(key, start, end) or [])) - except: - return [] - - def llen(self, key): - try: - return self.r.llen(key) or 0 - except: - return 0 - - def hget(self, key, field): - try: - return self.r.hget(key, field) - except Exception as e: - current_app.logger.warning("hget redis failed, %s" % str(e)) - return - - def hset(self, key, field, value): - try: - self.r.hset(key, field, value) - except Exception as e: - current_app.logger.warning("hset redis failed, %s" % str(e)) - return - - def expire(self, key, timeout): - try: - self.r.expire(key, timeout) - except Exception as e: - current_app.logger.warning("expire redis failed, %s" % str(e)) - return - - -def redis_decode(x): - try: - return x.decode() - except Exception as e: - print(x, e) - try: - return x.decode("gb18030") - except: - return "decode failed" - - class AESCrypto(object): BLOCK_SIZE = 16 # Bytes - pad = lambda s: s + (AESCrypto.BLOCK_SIZE - len(s) % AESCrypto.BLOCK_SIZE) * \ - chr(AESCrypto.BLOCK_SIZE - len(s) % AESCrypto.BLOCK_SIZE) + pad = lambda s: s + ((AESCrypto.BLOCK_SIZE - len(s) % AESCrypto.BLOCK_SIZE) * + chr(AESCrypto.BLOCK_SIZE - len(s) % AESCrypto.BLOCK_SIZE)) unpad = lambda s: s[:-ord(s[len(s) - 1:])] iv = '0102030405060708' @@ -352,7 +271,7 @@ class AESCrypto(object): def key(): key = current_app.config.get("SECRET_KEY")[:16] if len(key) < 16: - key = "{}{}".format(key, (16 - len(key) * "x")) + key = "{}{}".format(key, (16 - len(key)) * "x") return key.encode('utf8') @@ -370,3 +289,33 @@ def decrypt(cls, data): text_decrypted = cipher.decrypt(encode_bytes) return cls.unpad(text_decrypted).decode('utf8') + + +class Crypto(AESCrypto): + @classmethod + def encrypt(cls, data): + from api.lib.secrets.secrets import InnerKVManger + + if not KeyManage(backend=InnerKVManger()).is_seal(): + res, status = InnerCrypt().encrypt(data) + if status: + return res + + return AESCrypto().encrypt(data) + + @classmethod + def decrypt(cls, data): + from api.lib.secrets.secrets import InnerKVManger + + if not KeyManage(backend=InnerKVManger()).is_seal(): + try: + res, status = InnerCrypt().decrypt(data) + if status: + return res + except: + pass + + try: + return AESCrypto().decrypt(data) + except: + return data diff --git a/cmdb-api/api/lib/webhook.py b/cmdb-api/api/lib/webhook.py new file mode 100644 index 00000000..a5133e09 --- /dev/null +++ b/cmdb-api/api/lib/webhook.py @@ -0,0 +1,109 @@ +# -*- coding:utf-8 -*- + +import json +from functools import partial + +import requests +from jinja2 import Template +from requests.auth import HTTPBasicAuth +from requests_oauthlib import OAuth2Session + + +class BearerAuth(requests.auth.AuthBase): + def __init__(self, token): + self.token = token + + def __call__(self, r): + r.headers["authorization"] = "Bearer {}".format(self.token) + return r + + +def _wrap_auth(**kwargs): + auth_type = (kwargs.get('type') or "").lower() + if auth_type == "basicauth": + return HTTPBasicAuth(kwargs.get('username'), kwargs.get('password')) + + elif auth_type == "bearer": + return BearerAuth(kwargs.get('token')) + + elif auth_type == 'oauth2.0': + client_id = kwargs.get('client_id') + client_secret = kwargs.get('client_secret') + authorization_base_url = kwargs.get('authorization_base_url') + token_url = kwargs.get('token_url') + redirect_url = kwargs.get('redirect_url') + scope = kwargs.get('scope') + + oauth2_session = OAuth2Session(client_id, scope=scope or None) + oauth2_session.authorization_url(authorization_base_url) + + oauth2_session.fetch_token(token_url, client_secret=client_secret, authorization_response=redirect_url) + + return oauth2_session + + elif auth_type == "apikey": + return HTTPBasicAuth(kwargs.get('key'), kwargs.get('value')) + + +def webhook_request(webhook, payload): + """ + + :param webhook: + { + "url": "https://veops.cn" + "method": "GET|POST|PUT|DELETE" + "body": {}, + "headers": { + "Content-Type": "Application/json" + }, + "parameters": { + "key": "value" + }, + "authorization": { + "type": "BasicAuth|Bearer|OAuth2.0|APIKey", + "password": "mmmm", # BasicAuth + "username": "bbb", # BasicAuth + + "token": "xxx", # Bearer + + "key": "xxx", # APIKey + "value": "xxx", # APIKey + + "client_id": "xxx", # OAuth2.0 + "client_secret": "xxx", # OAuth2.0 + "authorization_base_url": "xxx", # OAuth2.0 + "token_url": "xxx", # OAuth2.0 + "redirect_url": "xxx", # OAuth2.0 + "scope": "xxx" # OAuth2.0 + } + } + :param payload: + :return: + """ + assert webhook.get('url') is not None + + payload = {k: v or '' for k, v in payload.items()} + + url = Template(webhook['url']).render(payload) + + params = webhook.get('parameters') or None + if isinstance(params, dict): + params = json.loads(Template(json.dumps(params)).render(payload)) + + headers = json.loads(Template(json.dumps(webhook.get('headers') or {})).render(payload)) + + data = Template(json.dumps(webhook.get('body', ''))).render(payload) + auth = _wrap_auth(**webhook.get('authorization', {})) + + if (webhook.get('authorization', {}).get("type") or '').lower() == 'oauth2.0': + request = getattr(auth, webhook.get('method', 'GET').lower()) + else: + request = partial(requests.request, webhook.get('method', 'GET')) + + return request( + url, + params=params, + headers=headers or None, + data=data, + auth=auth + ) diff --git a/cmdb-api/api/models/acl.py b/cmdb-api/api/models/acl.py index ebf02fff..e3396c3b 100644 --- a/cmdb-api/api/models/acl.py +++ b/cmdb-api/api/models/acl.py @@ -5,16 +5,18 @@ import hashlib from datetime import datetime -import ldap 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): @@ -27,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): @@ -56,37 +63,6 @@ def authenticate_with_key(self, key, secret, args, path): return user, authenticated - def authenticate_with_ldap(self, username, password): - ldap_conn = ldap.initialize(current_app.config.get('LDAP_SERVER')) - ldap_conn.protocol_version = 3 - ldap_conn.set_option(ldap.OPT_REFERRALS, 0) - if '@' in username: - email = username - who = '{0}@{1}'.format(username.split('@')[0], current_app.config.get('LDAP_DOMAIN')) - else: - who = '{0}@{1}'.format(username, current_app.config.get('LDAP_DOMAIN')) - email = who - - username = username.split('@')[0] - user = self.get_by_username(username) - try: - - if not password: - raise ldap.INVALID_CREDENTIALS - - ldap_conn.simple_bind_s(who, password) - - 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 ldap.INVALID_CREDENTIALS: - return user, False - def search(self, key): query = self.filter(db.or_(User.email == key, User.nickname.ilike('%' + key + '%'), @@ -136,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): @@ -166,11 +143,9 @@ def check_password(self, password): 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) @@ -375,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'), 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 e1693cb8..f91f585f 100644 --- a/cmdb-api/api/models/cmdb.py +++ b/cmdb-api/api/models/cmdb.py @@ -12,7 +12,9 @@ from api.lib.cmdb.const import ConstraintEnum from api.lib.cmdb.const import OperateType from api.lib.cmdb.const import ValueTypeEnum -from api.lib.database import Model, Model2 +from api.lib.database import Model +from api.lib.database import Model2 +from api.lib.utils import Crypto # template @@ -89,12 +91,37 @@ class Attribute(Model): compute_expr = db.Column(db.Text) compute_script = db.Column(db.Text) - choice_web_hook = db.Column(db.JSON) + _choice_web_hook = db.Column('choice_web_hook', db.JSON) + choice_other = db.Column(db.JSON) uid = db.Column(db.Integer, index=True) option = db.Column(db.JSON) + def _get_webhook(self): + if self._choice_web_hook: + if self._choice_web_hook.get('headers') and "Cookie" in self._choice_web_hook['headers']: + self._choice_web_hook['headers']['Cookie'] = Crypto.decrypt(self._choice_web_hook['headers']['Cookie']) + + if self._choice_web_hook.get('authorization'): + for k, v in self._choice_web_hook['authorization'].items(): + self._choice_web_hook['authorization'][k] = Crypto.decrypt(v) + + return self._choice_web_hook + + def _set_webhook(self, data): + if data: + if data.get('headers') and "Cookie" in data['headers']: + data['headers']['Cookie'] = Crypto.encrypt(data['headers']['Cookie']) + + if data.get('authorization'): + for k, v in data['authorization'].items(): + data['authorization'][k] = Crypto.encrypt(v) + + self._choice_web_hook = data + + choice_web_hook = db.synonym("_choice_web_hook", descriptor=property(_get_webhook, _set_webhook)) + class CITypeAttribute(Model): __tablename__ = "c_ci_type_attributes" @@ -125,16 +152,45 @@ class CITypeAttributeGroupItem(Model): class CITypeTrigger(Model): - # __tablename__ = "c_ci_type_triggers" __tablename__ = "c_c_t_t" type_id = db.Column(db.Integer, db.ForeignKey('c_ci_types.id'), nullable=False) - attr_id = db.Column(db.Integer, db.ForeignKey("c_attributes.id"), nullable=False) - notify = db.Column(db.JSON) # {subject: x, body: x, wx_to: [], mail_to: [], before_days: 0, notify_at: 08:00} + attr_id = db.Column(db.Integer, db.ForeignKey("c_attributes.id")) + _option = db.Column('notify', db.JSON) + + def _get_option(self): + if self._option and self._option.get('webhooks'): + if self._option['webhooks'].get('authorization'): + for k, v in self._option['webhooks']['authorization'].items(): + self._option['webhooks']['authorization'][k] = Crypto.decrypt(v) + + return self._option + + def _set_option(self, data): + if data and data.get('webhooks'): + if data['webhooks'].get('authorization'): + for k, v in data['webhooks']['authorization'].items(): + data['webhooks']['authorization'][k] = Crypto.encrypt(v) + + self._option = data + + option = db.synonym("_option", descriptor=property(_get_option, _set_option)) + + +class CITriggerHistory(Model): + __tablename__ = "c_ci_trigger_histories" + + operate_type = db.Column(db.Enum(*OperateType.all(), name="operate_type")) + record_id = db.Column(db.Integer, db.ForeignKey("c_records.id")) + ci_id = db.Column(db.Integer, index=True, nullable=False) + trigger_id = db.Column(db.Integer, db.ForeignKey("c_c_t_t.id")) + trigger_name = db.Column(db.String(64)) + is_ok = db.Column(db.Boolean, default=False) + notify = db.Column(db.Text) + webhook = db.Column(db.Text) class CITypeUniqueConstraint(Model): - # __tablename__ = "c_ci_type_unique_constraints" __tablename__ = "c_c_t_u_c" type_id = db.Column(db.Integer, db.ForeignKey('c_ci_types.id'), nullable=False) @@ -162,6 +218,8 @@ class CIRelation(Model): relation_type_id = db.Column(db.Integer, db.ForeignKey("c_relation_types.id"), nullable=False) more = db.Column(db.Integer, db.ForeignKey("c_cis.id")) + ancestor_ids = db.Column(db.String(128), index=True) + first_ci = db.relationship("CI", primaryjoin="CI.id==CIRelation.first_ci_id") second_ci = db.relationship("CI", primaryjoin="CI.id==CIRelation.second_ci_id") relation_type = db.relationship("RelationType", backref="c_ci_relations.relation_type_id") @@ -250,6 +308,9 @@ class CIIndexValueDateTime(Model): class CIValueInteger(Model): + """ + Deprecated in a future version + """ __tablename__ = "c_value_integers" ci_id = db.Column(db.Integer, db.ForeignKey('c_cis.id'), nullable=False) @@ -261,6 +322,9 @@ class CIValueInteger(Model): class CIValueFloat(Model): + """ + Deprecated in a future version + """ __tablename__ = "c_value_floats" ci_id = db.Column(db.Integer, db.ForeignKey('c_cis.id'), nullable=False) @@ -283,6 +347,9 @@ class CIValueText(Model): class CIValueDateTime(Model): + """ + Deprecated in a future version + """ __tablename__ = "c_value_datetime" ci_id = db.Column(db.Integer, db.ForeignKey('c_cis.id'), nullable=False) @@ -354,7 +421,6 @@ class CITypeHistory(Model): # preference class PreferenceShowAttributes(Model): - # __tablename__ = "c_preference_show_attributes" __tablename__ = "c_psa" uid = db.Column(db.Integer, index=True, nullable=False) @@ -368,7 +434,6 @@ class PreferenceShowAttributes(Model): class PreferenceTreeView(Model): - # __tablename__ = "c_preference_tree_views" __tablename__ = "c_ptv" uid = db.Column(db.Integer, index=True, nullable=False) @@ -377,7 +442,6 @@ class PreferenceTreeView(Model): class PreferenceRelationView(Model): - # __tablename__ = "c_preference_relation_views" __tablename__ = "c_prv" uid = db.Column(db.Integer, index=True, nullable=False) @@ -486,3 +550,10 @@ class CIFilterPerms(Model): attr_filter = db.Column(db.Text) rid = db.Column(db.Integer, index=True) + + +class InnerKV(Model): + __tablename__ = "c_kv" + + key = db.Column(db.String(128), index=True) + value = db.Column(db.Text) diff --git a/cmdb-api/api/models/common_setting.py b/cmdb-api/api/models/common_setting.py index 741f6644..f1f5404e 100644 --- a/cmdb-api/api/models/common_setting.py +++ b/cmdb-api/api/models/common_setting.py @@ -13,40 +13,41 @@ class Department(ModelWithoutPK): __tablename__ = 'common_department' department_id = db.Column(db.Integer, primary_key=True, autoincrement=True) - department_name = db.Column(db.VARCHAR(255), default='', comment='部门名称') + department_name = db.Column(db.VARCHAR(255), default='') department_director_id = db.Column( - db.Integer, default=0, comment='部门负责人ID') - department_parent_id = db.Column(db.Integer, default=1, comment='上级部门ID') + db.Integer, default=0) + department_parent_id = db.Column(db.Integer, default=1) - sort_value = db.Column(db.Integer, default=0, comment='排序值') + sort_value = db.Column(db.Integer, default=0) - acl_rid = db.Column(db.Integer, comment='ACL中rid', default=0) + acl_rid = db.Column(db.Integer, default=0) class Employee(ModelWithoutPK): __tablename__ = 'common_employee' employee_id = db.Column(db.Integer, primary_key=True, autoincrement=True) - email = db.Column(db.VARCHAR(255), default='', comment='邮箱') - username = db.Column(db.VARCHAR(255), default='', comment='用户名') - nickname = db.Column(db.VARCHAR(255), default='', comment='姓名') - sex = db.Column(db.VARCHAR(64), default='', comment='性别') - position_name = db.Column(db.VARCHAR(255), default='', comment='职位名称') - mobile = db.Column(db.VARCHAR(255), default='', comment='电话号码') - avatar = db.Column(db.VARCHAR(255), default='', comment='头像') + email = db.Column(db.VARCHAR(255), default='') + username = db.Column(db.VARCHAR(255), default='') + nickname = db.Column(db.VARCHAR(255), default='') + sex = db.Column(db.VARCHAR(64), default='') + position_name = db.Column(db.VARCHAR(255), default='') + mobile = db.Column(db.VARCHAR(255), default='') + avatar = db.Column(db.VARCHAR(255), default='') - direct_supervisor_id = db.Column(db.Integer, default=0, comment='直接上级ID') + direct_supervisor_id = db.Column(db.Integer, default=0) department_id = db.Column(db.Integer, - db.ForeignKey('common_department.department_id'), - comment='部门ID', + db.ForeignKey('common_department.department_id') ) - acl_uid = db.Column(db.Integer, comment='ACL中uid', default=0) - acl_rid = db.Column(db.Integer, comment='ACL中rid', default=0) - acl_virtual_rid = db.Column(db.Integer, comment='ACL中虚拟角色rid', default=0) - last_login = db.Column(db.TIMESTAMP, nullable=True, comment='上次登录时间') - block = db.Column(db.Integer, comment='锁定状态', default=0) + acl_uid = db.Column(db.Integer, default=0) + acl_rid = db.Column(db.Integer, default=0) + acl_virtual_rid = db.Column(db.Integer, default=0) + last_login = db.Column(db.TIMESTAMP, nullable=True) + block = db.Column(db.Integer, default=0) + + notice_info = db.Column(db.JSON, default={}) _department = db.relationship( 'Department', backref='common_employee.department_id', @@ -55,14 +56,11 @@ class Employee(ModelWithoutPK): class EmployeeInfo(Model): - """ - 员工信息 - """ __tablename__ = 'common_employee_info' - info = db.Column(db.JSON, default={}, comment='员工信息') + info = db.Column(db.JSON, default={}) employee_id = db.Column(db.Integer, db.ForeignKey( - 'common_employee.employee_id'), comment='员工ID') + 'common_employee.employee_id')) employee = db.relationship( 'Employee', backref='common_employee.employee_id', lazy='joined') @@ -74,16 +72,35 @@ class CompanyInfo(Model): class InternalMessage(Model): - """ - 内部消息 - """ __tablename__ = "common_internal_message" - title = db.Column(db.VARCHAR(255), nullable=True, comment='标题') - content = db.Column(db.TEXT, nullable=True, comment='内容') - path = db.Column(db.VARCHAR(255), nullable=True, comment='跳转路径') - is_read = db.Column(db.Boolean, default=False, comment='是否已读') - app_name = db.Column(db.VARCHAR(128), nullable=False, comment='应用名称') - category = db.Column(db.VARCHAR(128), nullable=False, comment='分类') - message_data = db.Column(db.JSON, nullable=True, comment='数据') + title = db.Column(db.VARCHAR(255), nullable=True) + content = db.Column(db.TEXT, nullable=True) + path = db.Column(db.VARCHAR(255), nullable=True) + is_read = db.Column(db.Boolean, default=False) + app_name = db.Column(db.VARCHAR(128), nullable=False) + category = db.Column(db.VARCHAR(128), nullable=False) + message_data = db.Column(db.JSON, nullable=True) employee_id = db.Column(db.Integer, db.ForeignKey('common_employee.employee_id'), comment='ID') + + +class CommonData(Model): + __table_name__ = 'common_data' + + data_type = db.Column(db.VARCHAR(255), default='') + data = db.Column(db.JSON) + + +class NoticeConfig(Model): + __tablename__ = "common_notice_config" + + 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/resource.py b/cmdb-api/api/resource.py index 3bed92b5..1fc79f55 100644 --- a/cmdb-api/api/resource.py +++ b/cmdb-api/api/resource.py @@ -2,7 +2,8 @@ import os import sys -from inspect import getmembers, isclass +from inspect import getmembers +from inspect import isclass import six from flask import jsonify @@ -27,16 +28,15 @@ def send_file(*args, **kwargs): return send_file(*args, **kwargs) -API_PACKAGE = "api" +API_PACKAGE = os.path.abspath(os.path.dirname(__file__)) def register_resources(resource_path, rest_api): for root, _, files in os.walk(os.path.join(resource_path)): for filename in files: if not filename.startswith("_") and filename.endswith("py"): - module_path = os.path.join(API_PACKAGE, root[root.index("views"):]) - if module_path not in sys.path: - sys.path.insert(1, module_path) + if root not in sys.path: + sys.path.insert(1, root) view = __import__(os.path.splitext(filename)[0]) resource_list = [o[0] for o in getmembers(view) if isclass(o[1]) and issubclass(o[1], Resource)] resource_list = [i for i in resource_list if i != "APIView"] @@ -46,5 +46,4 @@ def register_resources(resource_path, rest_api): resource_cls.url_prefix = ("",) if isinstance(resource_cls.url_prefix, six.string_types): resource_cls.url_prefix = (resource_cls.url_prefix,) - rest_api.add_resource(resource_cls, *resource_cls.url_prefix) diff --git a/cmdb-api/api/tasks/acl.py b/cmdb-api/api/tasks/acl.py index e083ffe6..d9eb6a92 100644 --- a/cmdb-api/api/tasks/acl.py +++ b/cmdb-api/api/tasks/acl.py @@ -5,26 +5,30 @@ from celery_once import QueueOnce from flask import current_app -from werkzeug.exceptions import BadRequest, NotFound +from werkzeug.exceptions import BadRequest +from werkzeug.exceptions import NotFound from api.extensions import celery -from api.extensions import db +from api.lib.decorator import flush_db +from api.lib.decorator import reconnect_db +from api.lib.perm.acl.audit import AuditCRUD +from api.lib.perm.acl.audit import AuditOperateSource +from api.lib.perm.acl.audit import AuditOperateType from api.lib.perm.acl.cache import AppCache from api.lib.perm.acl.cache import RoleCache from api.lib.perm.acl.cache import RoleRelationCache from api.lib.perm.acl.cache import UserCache from api.lib.perm.acl.const import ACL_QUEUE from api.lib.perm.acl.record import OperateRecordCRUD -from api.lib.perm.acl.audit import AuditCRUD, AuditOperateType, AuditOperateSource from api.models.acl import Resource 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] for rid in rids: @@ -34,6 +38,7 @@ def role_rebuild(rids, app_id): @celery.task(name="acl.update_resource_to_build_role", queue=ACL_QUEUE) +@reconnect_db def update_resource_to_build_role(resource_id, app_id, group_id=None): rids = [i.id for i in Role.get_by(__func_isnot__key_uid=None, fl='id', to_dict=False)] rids += [i.id for i in Role.get_by(app_id=app_id, fl='id', to_dict=False)] @@ -49,9 +54,9 @@ def update_resource_to_build_role(resource_id, app_id, group_id=None): @celery.task(name="acl.apply_trigger", queue=ACL_QUEUE) +@flush_db +@reconnect_db def apply_trigger(_id, resource_id=None, operator_uid=None): - db.session.remove() - from api.lib.perm.acl.permission import PermissionCRUD trigger = Trigger.get_by_id(_id) @@ -115,9 +120,9 @@ def apply_trigger(_id, resource_id=None, operator_uid=None): @celery.task(name="acl.cancel_trigger", queue=ACL_QUEUE) +@flush_db +@reconnect_db def cancel_trigger(_id, resource_id=None, operator_uid=None): - db.session.remove() - from api.lib.perm.acl.permission import PermissionCRUD trigger = Trigger.get_by_id(_id) @@ -183,18 +188,19 @@ def cancel_trigger(_id, resource_id=None, operator_uid=None): @celery.task(name="acl.op_record", queue=ACL_QUEUE) -def op_record(app, rolename, operate_type, obj): +@reconnect_db +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 4d120ed9..bf245854 100644 --- a/cmdb-api/api/tasks/cmdb.py +++ b/cmdb-api/api/tasks/cmdb.py @@ -4,9 +4,8 @@ import json import time -import jinja2 -import requests from flask import current_app +from flask_login import login_user import api.lib.cmdb.ci from api.extensions import celery @@ -17,15 +16,24 @@ 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.mail import send_mail +from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION2 +from api.lib.decorator import flush_db +from api.lib.decorator import reconnect_db +from api.lib.perm.acl.cache import UserCache 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 +from api.models.cmdb import CITypeAttribute @celery.task(name="cmdb.ci_cache", queue=CMDB_QUEUE) -def ci_cache(ci_id): +@flush_db +@reconnect_db +def ci_cache(ci_id, operate_type, record_id): + from api.lib.cmdb.ci import CITriggerManager + time.sleep(0.01) - db.session.remove() m = api.lib.cmdb.ci.CIManager() ci_dict = m.get_ci_by_id_from_db(ci_id, need_children=False, use_master=False) @@ -37,11 +45,18 @@ def ci_cache(ci_id): current_app.logger.info("{0} flush..........".format(ci_id)) + if operate_type: + current_app.test_request_context().push() + login_user(UserCache.get('worker')) + + CITriggerManager.fire(operate_type, ci_dict, record_id) + @celery.task(name="cmdb.batch_ci_cache", queue=CMDB_QUEUE) -def batch_ci_cache(ci_ids): +@flush_db +@reconnect_db +def batch_ci_cache(ci_ids, ): # only for attribute change index time.sleep(1) - db.session.remove() for ci_id in ci_ids: m = api.lib.cmdb.ci.CIManager() @@ -56,6 +71,7 @@ def batch_ci_cache(ci_ids): @celery.task(name="cmdb.ci_delete", queue=CMDB_QUEUE) +@reconnect_db def ci_delete(ci_id): current_app.logger.info(ci_id) @@ -67,47 +83,137 @@ def ci_delete(ci_id): current_app.logger.info("{0} delete..........".format(ci_id)) -@celery.task(name="cmdb.ci_relation_cache", queue=CMDB_QUEUE) -def ci_relation_cache(parent_id, child_id): - db.session.remove() +@celery.task(name="cmdb.ci_delete_trigger", queue=CMDB_QUEUE) +@reconnect_db +def ci_delete_trigger(trigger, operate_type, ci_dict): + current_app.logger.info('delete ci {} trigger'.format(ci_dict['_id'])) + from api.lib.cmdb.ci import CITriggerManager + + current_app.test_request_context().push() + login_user(UserCache.get('worker')) + + CITriggerManager.fire_by_trigger(trigger, operate_type, ci_dict) + +@celery.task(name="cmdb.ci_relation_cache", queue=CMDB_QUEUE) +@flush_db +@reconnect_db +def ci_relation_cache(parent_id, child_id, ancestor_ids): with Lock("CIRelation_{}".format(parent_id)): - children = rd.get([parent_id], REDIS_PREFIX_CI_RELATION)[0] - children = json.loads(children) if children is not None else {} + if ancestor_ids is None: + children = rd.get([parent_id], REDIS_PREFIX_CI_RELATION)[0] + children = json.loads(children) if children is not None else {} + + cr = CIRelation.get_by(first_ci_id=parent_id, second_ci_id=child_id, ancestor_ids=ancestor_ids, + first=True, to_dict=False) + if str(child_id) not in children: + children[str(child_id)] = cr.second_ci.type_id + + rd.create_or_update({parent_id: json.dumps(children)}, REDIS_PREFIX_CI_RELATION) + + else: + key = "{},{}".format(ancestor_ids, parent_id) + grandson = rd.get([key], REDIS_PREFIX_CI_RELATION2)[0] + grandson = json.loads(grandson) if grandson is not None else {} - cr = CIRelation.get_by(first_ci_id=parent_id, second_ci_id=child_id, first=True, to_dict=False) - if str(child_id) not in children: - children[str(child_id)] = cr.second_ci.type_id + cr = CIRelation.get_by(first_ci_id=parent_id, second_ci_id=child_id, ancestor_ids=ancestor_ids, + first=True, to_dict=False) + if cr and str(cr.second_ci_id) not in grandson: + grandson[str(cr.second_ci_id)] = cr.second_ci.type_id - rd.create_or_update({parent_id: json.dumps(children)}, REDIS_PREFIX_CI_RELATION) + rd.create_or_update({key: json.dumps(grandson)}, REDIS_PREFIX_CI_RELATION2) current_app.logger.info("ADD ci relation cache: {0} -> {1}".format(parent_id, child_id)) +@celery.task(name="cmdb.ci_relation_add", queue=CMDB_QUEUE) +@flush_db +@reconnect_db +def ci_relation_add(parent_dict, child_id, uid): + """ + :param parent_dict: key is '$parent_model.attr_name' + :param child_id: + :param uid: + :return: + """ + from api.lib.cmdb.ci import CIRelationManager + from api.lib.cmdb.ci_type import CITypeAttributeManager + from api.lib.cmdb.search import SearchError + from api.lib.cmdb.search.ci import search + + current_app.test_request_context().push() + login_user(UserCache.get(uid)) + + for parent in parent_dict: + parent_ci_type_name, _attr_name = parent.strip()[1:].split('.', 1) + attr_name = CITypeAttributeManager.get_attr_name(parent_ci_type_name, _attr_name) + if attr_name is None: + current_app.logger.warning("attr name {} does not exist".format(_attr_name)) + continue + + parent_dict[parent] = handle_arg_list(parent_dict[parent]) + for v in parent_dict[parent]: + query = "_type:{},{}:{}".format(parent_ci_type_name, attr_name, v) + s = search(query) + try: + response, _, _, _, _, _ = s.search() + except SearchError as e: + current_app.logger.error('ci relation add failed: {}'.format(e)) + continue + + for ci in response: + try: + CIRelationManager.add(ci['_id'], child_id) + ci_relation_cache(ci['_id'], child_id) + except Exception as e: + current_app.logger.warning(e) + finally: + try: + db.session.commit() + except: + db.session.rollback() + + @celery.task(name="cmdb.ci_relation_delete", queue=CMDB_QUEUE) -def ci_relation_delete(parent_id, child_id): +@reconnect_db +def ci_relation_delete(parent_id, child_id, ancestor_ids): with Lock("CIRelation_{}".format(parent_id)): - children = rd.get([parent_id], REDIS_PREFIX_CI_RELATION)[0] - children = json.loads(children) if children is not None else {} + if ancestor_ids is None: + children = rd.get([parent_id], REDIS_PREFIX_CI_RELATION)[0] + children = json.loads(children) if children is not None else {} + + if str(child_id) in children: + children.pop(str(child_id)) + + rd.create_or_update({parent_id: json.dumps(children)}, REDIS_PREFIX_CI_RELATION) + + else: + key = "{},{}".format(ancestor_ids, parent_id) + grandson = rd.get([key], REDIS_PREFIX_CI_RELATION2)[0] + grandson = json.loads(grandson) if grandson is not None else {} - if str(child_id) in children: - children.pop(str(child_id)) + if str(child_id) in grandson: + grandson.pop(str(child_id)) - rd.create_or_update({parent_id: json.dumps(children)}, REDIS_PREFIX_CI_RELATION) + rd.create_or_update({key: json.dumps(grandson)}, REDIS_PREFIX_CI_RELATION2) current_app.logger.info("DELETE ci relation cache: {0} -> {1}".format(parent_id, child_id)) @celery.task(name="cmdb.ci_type_attribute_order_rebuild", queue=CMDB_QUEUE) -def ci_type_attribute_order_rebuild(type_id): +@flush_db +@reconnect_db +def ci_type_attribute_order_rebuild(type_id, uid): current_app.logger.info('rebuild attribute order') - db.session.remove() from api.lib.cmdb.ci_type import CITypeAttributeGroupManager attrs = CITypeAttributesCache.get(type_id) id2attr = {attr.attr_id: attr for attr in attrs} + current_app.test_request_context().push() + login_user(UserCache.get(uid)) + res = CITypeAttributeGroupManager.get_by_type_id(type_id, True) order = 0 for group in res: @@ -118,41 +224,17 @@ def ci_type_attribute_order_rebuild(type_id): order += 1 -@celery.task(name='cmdb.trigger_notify', queue=CMDB_QUEUE) -def trigger_notify(notify, ci_id): - from api.lib.perm.acl.cache import UserCache - - def _wrap_mail(mail_to): - if "@" not in mail_to: - user = UserCache.get(mail_to) - if user: - return user.email - - return mail_to +@celery.task(name="cmdb.calc_computed_attribute", queue=CMDB_QUEUE) +@flush_db +@reconnect_db +def calc_computed_attribute(attr_id, uid): + from api.lib.cmdb.ci import CIManager - db.session.remove() - - m = api.lib.cmdb.ci.CIManager() - ci_dict = m.get_ci_by_id_from_db(ci_id, need_children=False, use_master=False) + current_app.test_request_context().push() + login_user(UserCache.get(uid)) - subject = jinja2.Template(notify.get('subject') or "").render(ci_dict) - body = jinja2.Template(notify.get('body') or "").render(ci_dict) - - if notify.get('wx_to'): - to_user = jinja2.Template('|'.join(notify['wx_to'])).render(ci_dict) - url = current_app.config.get("WX_URI") - data = {"to_user": to_user, "content": subject} - try: - requests.post(url, data=data) - except Exception as e: - current_app.logger.error(str(e)) - - if notify.get('mail_to'): - try: - if len(subject) > 700: - subject = subject[:600] + "..." + subject[-100:] - - send_mail("", [_wrap_mail(jinja2.Template(i).render(ci_dict)) - for i in notify['mail_to'] if i], subject, body) - except Exception as e: - current_app.logger.error("Send mail failed: {0}".format(str(e))) + cim = CIManager() + for i in CITypeAttribute.get_by(attr_id=attr_id, to_dict=False): + cis = CI.get_by(type_id=i.type_id, to_dict=False) + for ci in cis: + cim.update(ci.id, {}) diff --git a/cmdb-api/api/tasks/common_setting.py b/cmdb-api/api/tasks/common_setting.py index 45942189..efb2c03a 100644 --- a/cmdb-api/api/tasks/common_setting.py +++ b/cmdb-api/api/tasks/common_setting.py @@ -1,28 +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.cmdb.const import CMDB_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=CMDB_QUEUE) +@flush_db +@reconnect_db def edit_employee_department_in_acl(e_list, new_d_id, op_uid): """ - 在 ACL 员工更换部门 - :param e_list: 员工列表 {acl_rid: 11, department_id: 22} - :param new_d_id: 新部门 ID - :param op_uid: 操作人 ID - - 在老部门中删除员工 - 在新部门中添加员工 + :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) @@ -43,7 +39,6 @@ def edit_employee_department_in_acl(e_list, new_d_id, op_uid): new_department_acl_rid = new_department.acl_rid if new_d_rid_in_acl == new_department.acl_rid else new_d_rid_in_acl for employee in e_list: - # 根据 部门ID获取部门 acl_rid old_department = Department.get_by( first=True, department_id=employee.get('department_id'), to_dict=False) if not old_department: @@ -61,7 +56,6 @@ def edit_employee_department_in_acl(e_list, new_d_id, op_uid): 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, @@ -71,7 +65,6 @@ def edit_employee_department_in_acl(e_list, new_d_id, op_uid): except Exception as e: result.append(ErrFormat.acl_remove_user_from_role_failed.format(str(e))) - # 在新部门中添加员工 payload = { 'app_id': 'acl', 'child_ids': [employee_acl_rid], @@ -82,3 +75,41 @@ 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=CMDB_QUEUE) +@flush_db +@reconnect_db +def refresh_employee_acl_info(): + 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() + ) + + for em in query.all(): + 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) + + 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 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 00000000..8620fafd 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 00000000..1eeb4676 --- /dev/null +++ b/cmdb-api/api/translations/zh/LC_MESSAGES/messages.po @@ -0,0 +1,819 @@ +# 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-01-03 11:39+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:65 +msgid "" +"The model cannot be deleted because the model is referenced by the " +"relational view {}" +msgstr "因为关系视图 {} 引用了该模型,不能删除模型" + +#: api/lib/cmdb/resp_format.py:67 +msgid "Model group {} does not exist" +msgstr "模型分组 {} 不存在" + +#: api/lib/cmdb/resp_format.py:68 +msgid "Model group {} already exists" +msgstr "模型分组 {} 已经存在" + +#: api/lib/cmdb/resp_format.py:69 +msgid "Model relationship {} does not exist" +msgstr "模型关系 {} 不存在" + +#: api/lib/cmdb/resp_format.py:70 +msgid "Attribute group {} already exists" +msgstr "属性分组 {} 已存在" + +#: api/lib/cmdb/resp_format.py:71 +msgid "Attribute group {} does not exist" +msgstr "属性分组 {} 不存在" + +#: api/lib/cmdb/resp_format.py:73 +msgid "Attribute group <{0}> - attribute <{1}> does not exist" +msgstr "属性组<{0}> - 属性<{1}> 不存在" + +#: api/lib/cmdb/resp_format.py:74 +msgid "The unique constraint already exists!" +msgstr "唯一约束已经存在!" + +#: api/lib/cmdb/resp_format.py:76 +msgid "Uniquely constrained attributes cannot be JSON and multi-valued" +msgstr "唯一约束的属性不能是 JSON 和 多值" + +#: api/lib/cmdb/resp_format.py:77 +msgid "Duplicated trigger" +msgstr "重复的触发器" + +#: api/lib/cmdb/resp_format.py:78 +msgid "Trigger {} does not exist" +msgstr "触发器 {} 不存在" + +#: api/lib/cmdb/resp_format.py:80 +msgid "Operation record {} does not exist" +msgstr "操作记录 {} 不存在" + +#: api/lib/cmdb/resp_format.py:81 +msgid "Unique identifier cannot be deleted" +msgstr "不能删除唯一标识" + +#: api/lib/cmdb/resp_format.py:82 +msgid "Cannot delete default sorted attributes" +msgstr "不能删除默认排序的属性" + +#: api/lib/cmdb/resp_format.py:84 +msgid "No node selected" +msgstr "没有选择节点" + +#: api/lib/cmdb/resp_format.py:85 +msgid "This search option does not exist!" +msgstr "该搜索选项不存在!" + +#: api/lib/cmdb/resp_format.py:86 +msgid "This search option has a duplicate name!" +msgstr "该搜索选项命名重复!" + +#: api/lib/cmdb/resp_format.py:88 +msgid "Relationship type {} already exists" +msgstr "关系类型 {} 已经存在" + +#: api/lib/cmdb/resp_format.py:89 +msgid "Relationship type {} does not exist" +msgstr "关系类型 {} 不存在" + +#: api/lib/cmdb/resp_format.py:91 +msgid "Invalid attribute value: {}" +msgstr "无效的属性值: {}" + +#: api/lib/cmdb/resp_format.py:92 +msgid "{} Invalid value: {}" +msgstr "无效的值: {}" + +#: api/lib/cmdb/resp_format.py:93 +msgid "{} is not in the predefined values" +msgstr "{} 不在预定义值里" + +#: api/lib/cmdb/resp_format.py:95 +msgid "The value of attribute {} must be unique, {} already exists" +msgstr "属性 {} 的值必须是唯一的, 当前值 {} 已存在" + +#: api/lib/cmdb/resp_format.py:96 +msgid "Attribute {} value must exist" +msgstr "属性 {} 值必须存在" + +#: api/lib/cmdb/resp_format.py:99 +msgid "Unknown error when adding or modifying attribute value: {}" +msgstr "新增或者修改属性值未知错误: {}" + +#: api/lib/cmdb/resp_format.py:101 +msgid "Duplicate custom name" +msgstr "订制名重复" + +#: api/lib/cmdb/resp_format.py:103 +msgid "Number of models exceeds limit: {}" +msgstr "模型数超过限制: {}" + +#: api/lib/cmdb/resp_format.py:104 +msgid "The number of CIs exceeds the limit: {}" +msgstr "CI数超过限制: {}" + +#: api/lib/cmdb/resp_format.py:106 +msgid "Auto-discovery rule: {} already exists!" +msgstr "自动发现规则: {} 已经存在!" + +#: api/lib/cmdb/resp_format.py:107 +msgid "Auto-discovery rule: {} does not exist!" +msgstr "自动发现规则: {} 不存在!" + +#: api/lib/cmdb/resp_format.py:109 +msgid "This auto-discovery rule is referenced by the model and cannot be deleted!" +msgstr "该自动发现规则被模型引用, 不能删除!" + +#: api/lib/cmdb/resp_format.py:111 +msgid "The application of auto-discovery rules cannot be defined repeatedly!" +msgstr "自动发现规则的应用不能重复定义!" + +#: api/lib/cmdb/resp_format.py:112 +msgid "The auto-discovery you want to modify: {} does not exist!" +msgstr "您要修改的自动发现: {} 不存在!" + +#: api/lib/cmdb/resp_format.py:113 +msgid "Attribute does not include unique identifier: {}" +msgstr "属性字段没有包括唯一标识: {}" + +#: api/lib/cmdb/resp_format.py:114 +msgid "The auto-discovery instance does not exist!" +msgstr "自动发现的实例不存在!" + +#: api/lib/cmdb/resp_format.py:115 +msgid "The model is not associated with this auto-discovery!" +msgstr "模型并未关联该自动发现!" + +#: api/lib/cmdb/resp_format.py:116 +msgid "Only the creator can modify the Secret!" +msgstr "只有创建人才能修改Secret!" + +#: api/lib/cmdb/resp_format.py:118 +msgid "This rule already has auto-discovery instances and cannot be deleted!" +msgstr "该规则已经有自动发现的实例, 不能被删除!" + +#: api/lib/cmdb/resp_format.py:120 +msgid "The default auto-discovery rule is already referenced by model {}!" +msgstr "该默认的自动发现规则 已经被模型 {} 引用!" + +#: api/lib/cmdb/resp_format.py:122 +msgid "The unique_key method must return a non-empty string!" +msgstr "unique_key方法必须返回非空字符串!" + +#: api/lib/cmdb/resp_format.py:123 +msgid "The attributes method must return a list" +msgstr "attributes方法必须返回的是list" + +#: api/lib/cmdb/resp_format.py:125 +msgid "The list returned by the attributes method cannot be empty!" +msgstr "attributes方法返回的list不能为空!" + +#: api/lib/cmdb/resp_format.py:127 +msgid "Only administrators can define execution targets as: all nodes!" +msgstr "只有管理员才可以定义执行机器为: 所有节点!" + +#: api/lib/cmdb/resp_format.py:128 +msgid "Execute targets permission check failed: {}" +msgstr "执行机器权限检查不通过: {}" + +#: api/lib/cmdb/resp_format.py:130 +msgid "CI filter authorization must be named!" +msgstr "CI过滤授权 必须命名!" + +#: api/lib/cmdb/resp_format.py:131 +msgid "CI filter authorization is currently not supported or query" +msgstr "CI过滤授权 暂时不支持 或 查询" + +#: api/lib/cmdb/resp_format.py:134 +msgid "You do not have permission to operate attribute {}!" +msgstr "您没有属性 {} 的操作权限!" + +#: api/lib/cmdb/resp_format.py:135 +msgid "You do not have permission to operate this CI!" +msgstr "您没有该CI的操作权限!" + +#: api/lib/cmdb/resp_format.py:137 +msgid "Failed to save password: {}" +msgstr "保存密码失败: {}" + +#: api/lib/cmdb/resp_format.py:138 +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/account.py b/cmdb-api/api/views/account.py index e2740c14..2b8d0016 100644 --- a/cmdb-api/api/views/account.py +++ b/cmdb-api/api/views/account.py @@ -2,12 +2,13 @@ import datetime -import six import jwt +import six from flask import abort from flask import current_app from flask import request -from flask_login import login_user, logout_user +from flask_login import login_user +from flask_login import logout_user from api.lib.decorator import args_required from api.lib.perm.acl.cache import User diff --git a/cmdb-api/api/views/acl/audit.py b/cmdb-api/api/views/acl/audit.py index ae4c20e6..9826bb98 100644 --- a/cmdb-api/api/views/acl/audit.py +++ b/cmdb-api/api/views/acl/audit.py @@ -24,6 +24,7 @@ def get(self, name): '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 09ee89a4..ed459de8 100644 --- a/cmdb-api/api/views/acl/login.py +++ b/cmdb-api/api/views/acl/login.py @@ -8,11 +8,15 @@ 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,10 @@ def post(self): 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) + config = AuthenticateDataCRUD(AuthenticateType.LDAP).get() + if config.get('enabled') or config.get('enable'): + 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 +182,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/acl/resources.py b/cmdb-api/api/views/acl/resources.py index da5fc8af..4a15a0bf 100644 --- a/cmdb-api/api/views/acl/resources.py +++ b/cmdb-api/api/views/acl/resources.py @@ -1,7 +1,7 @@ # -*- coding:utf-8 -*- -from flask import g from flask import request +from flask_login import current_user from api.lib.decorator import args_required from api.lib.decorator import args_validate @@ -103,8 +103,8 @@ def post(self): type_id = request.values.get('type_id') app_id = request.values.get('app_id') uid = request.values.get('uid') - if not uid and hasattr(g, "user") and hasattr(g.user, "uid"): - uid = g.user.uid + if not uid and hasattr(current_user, "uid"): + uid = current_user.uid resource = ResourceCRUD.add(name, type_id, app_id, uid) diff --git a/cmdb-api/api/views/acl/role.py b/cmdb-api/api/views/acl/role.py index 1afad374..03a45e15 100644 --- a/cmdb-api/api/views/acl/role.py +++ b/cmdb-api/api/views/acl/role.py @@ -2,8 +2,8 @@ from flask import abort from flask import current_app -from flask import g from flask import request +from flask_login import current_user from api.lib.decorator import args_required from api.lib.decorator import args_validate @@ -31,12 +31,9 @@ def get(self): page_size = get_page_size(request.values.get("page_size")) q = request.values.get('q') app_id = request.values.get('app_id') - is_all = request.values.get('is_all', True) - is_all = True if is_all in current_app.config.get("BOOL_TRUE") else False - user_role = request.values.get('user_role', True) - user_only = request.values.get('user_only', False) - user_role = True if user_role in current_app.config.get("BOOL_TRUE") else False - user_only = True if user_only in current_app.config.get("BOOL_TRUE") else False + is_all = request.values.get('is_all', True) in current_app.config.get("BOOL_TRUE") + user_role = request.values.get('user_role', True) in current_app.config.get("BOOL_TRUE") + user_only = request.values.get('user_only', False) in current_app.config.get("BOOL_TRUE") numfound, roles = RoleCRUD.search(q, app_id, page, page_size, user_role, is_all, user_only) @@ -160,8 +157,8 @@ class RoleHasPermissionView(APIView): @auth_with_app_token def get(self): if not request.values.get('rid'): - role = RoleCache.get_by_name(None, g.user.username) - role or abort(404, ErrFormat.role_not_found.format(g.user.username)) + role = RoleCache.get_by_name(None, current_user.username) + role or abort(404, ErrFormat.role_not_found.format(current_user.username)) else: role = RoleCache.get(int(request.values.get('rid'))) diff --git a/cmdb-api/api/views/acl/user.py b/cmdb-api/api/views/acl/user.py index dc681a4f..fcf8a6a9 100644 --- a/cmdb-api/api/views/acl/user.py +++ b/cmdb-api/api/views/acl/user.py @@ -4,7 +4,6 @@ import requests from flask import abort from flask import current_app -from flask import g from flask import request from flask import session from flask_login import current_user @@ -13,7 +12,6 @@ from api.lib.decorator import args_validate from api.lib.perm.acl.acl import ACLManager from api.lib.perm.acl.acl import role_required -from api.lib.perm.acl.audit import AuditCRUD, AuditOperateType from api.lib.perm.acl.cache import AppCache from api.lib.perm.acl.cache import UserCache from api.lib.perm.acl.resp_format import ErrFormat @@ -116,7 +114,7 @@ def put(self, uid): @role_required("acl_admin") def delete(self, uid): - if g.user.uid == uid: + if current_user.uid == uid: return abort(400, ErrFormat.invalid_operation) UserCRUD.delete(uid) @@ -162,8 +160,8 @@ def post(self): if app.name not in ('cas-server', 'acl'): return abort(403, ErrFormat.invalid_request) - elif hasattr(g, 'user'): - if g.user.username != request.values['username']: + elif hasattr(current_user, 'username'): + if current_user.username != request.values['username']: return abort(403, ErrFormat.invalid_request) else: diff --git a/cmdb-api/api/views/cmdb/attribute.py b/cmdb-api/api/views/cmdb/attribute.py index 08864f1c..fffbcdce 100644 --- a/cmdb-api/api/views/cmdb/attribute.py +++ b/cmdb-api/api/views/cmdb/attribute.py @@ -33,7 +33,8 @@ def get(self): class AttributeView(APIView): - url_prefix = ("/attributes", "/attributes/", "/attributes/") + url_prefix = ("/attributes", "/attributes/", "/attributes/", + "/attributes//calc_computed_attribute") def get(self, attr_name=None, attr_id=None): attr_manager = AttributeManager() @@ -63,17 +64,25 @@ def post(self): current_app.logger.debug(params) attr_id = AttributeManager.add(**params) + return self.jsonify(attr_id=attr_id) @args_validate(AttributeManager.cls) def put(self, attr_id): + if request.url.endswith("/calc_computed_attribute"): + AttributeManager.calc_computed_attribute(attr_id) + + return self.jsonify(attr_id=attr_id) + choice_value = handle_arg_list(request.values.get("choice_value")) params = request.values params["choice_value"] = choice_value current_app.logger.debug(params) AttributeManager().update(attr_id, **params) + return self.jsonify(attr_id=attr_id) def delete(self, attr_id): attr_name = AttributeManager.delete(attr_id) + return self.jsonify(message="attribute {0} deleted".format(attr_name)) diff --git a/cmdb-api/api/views/cmdb/auto_discovery.py b/cmdb-api/api/views/cmdb/auto_discovery.py index 2a4e6009..958d8313 100644 --- a/cmdb-api/api/views/cmdb/auto_discovery.py +++ b/cmdb-api/api/views/cmdb/auto_discovery.py @@ -5,8 +5,8 @@ from flask import abort from flask import current_app -from flask import g from flask import request +from flask_login import current_user from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryCICRUD from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryCITypeCRUD @@ -75,9 +75,9 @@ def get(self): # export return self.send_file(bf, as_attachment=True, - attachment_filename="cmdb_auto_discovery.json", + download_name="cmdb_auto_discovery.json", mimetype='application/json', - cache_timeout=0) + max_age=0) def post(self): f = request.files.get('file') @@ -119,7 +119,7 @@ def get(self, type_id): _, res = AutoDiscoveryCITypeCRUD.search(page=1, page_size=100000, type_id=type_id, **request.values) for i in res: if isinstance(i.get("extra_option"), dict) and i['extra_option'].get('secret'): - if not (g.user.username == "cmdb_agent" or g.user.uid == i['uid']): + if not (current_user.username == "cmdb_agent" or current_user.uid == i['uid']): i['extra_option'].pop('secret', None) else: i['extra_option']['secret'] = AESCrypto.decrypt(i['extra_option']['secret']) @@ -213,7 +213,7 @@ class AutoDiscoveryRuleSyncView(APIView): url_prefix = ("/adt/sync",) def get(self): - if g.user.username not in ("cmdb_agent", "worker", "admin"): + if current_user.username not in ("cmdb_agent", "worker", "admin"): return abort(403) oneagent_name = request.values.get('oneagent_name') diff --git a/cmdb-api/api/views/cmdb/ci.py b/cmdb-api/api/views/cmdb/ci.py index cb285b59..ce39962a 100644 --- a/cmdb-api/api/views/cmdb/ci.py +++ b/cmdb-api/api/views/cmdb/ci.py @@ -11,7 +11,8 @@ 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 ResourceTypeEnum, PermEnum +from api.lib.cmdb.const import PermEnum +from api.lib.cmdb.const import ResourceTypeEnum from api.lib.cmdb.const import RetKey from api.lib.cmdb.perms import has_perm_for_ci from api.lib.cmdb.search import SearchError @@ -83,11 +84,10 @@ def post(self): ci_dict = self._wrap_ci_dict() manager = CIManager() - current_app.logger.debug(ci_dict) ci_id = manager.add(ci_type, exist_policy=exist_policy or ExistPolicy.REJECT, _no_attribute_policy=_no_attribute_policy, - _is_admin=request.values.pop('__is_admin', False), + _is_admin=request.values.pop('__is_admin', None) or False, **ci_dict) return self.jsonify(ci_id=ci_id) @@ -95,7 +95,6 @@ def post(self): @has_perm_for_ci("ci_id", ResourceTypeEnum.CI, PermEnum.UPDATE, CIManager.get_type) def put(self, ci_id=None): args = request.values - current_app.logger.info(args) ci_type = args.get("ci_type") _no_attribute_policy = args.get("no_attribute_policy", ExistPolicy.IGNORE) @@ -103,13 +102,14 @@ def put(self, ci_id=None): manager = CIManager() if ci_id is not None: manager.update(ci_id, - _is_admin=request.values.pop('__is_admin', False), + _is_admin=request.values.pop('__is_admin', None) or False, **ci_dict) else: + request.values.pop('exist_policy', None) ci_id = manager.add(ci_type, exist_policy=ExistPolicy.REPLACE, _no_attribute_policy=_no_attribute_policy, - _is_admin=request.values.pop('__is_admin', False), + _is_admin=request.values.pop('__is_admin', None) or False, **ci_dict) return self.jsonify(ci_id=ci_id) @@ -183,8 +183,8 @@ class CIUnique(APIView): @has_perm_from_args("ci_id", ResourceTypeEnum.CI, PermEnum.UPDATE, CIManager.get_type_name) def put(self, ci_id): params = request.values - unique_name = params.keys()[0] - unique_value = params.values()[0] + unique_name = list(params.keys())[0] + unique_value = list(params.values())[0] CIManager.update_unique_value(ci_id, unique_name, unique_value) @@ -226,11 +226,11 @@ def get(self, ci_id=None): from api.tasks.cmdb import ci_cache from api.lib.cmdb.const import CMDB_QUEUE if ci_id is not None: - ci_cache.apply_async([ci_id], queue=CMDB_QUEUE) + ci_cache.apply_async(args=(ci_id, None, None), queue=CMDB_QUEUE) else: cis = CI.get_by(to_dict=False) for ci in cis: - ci_cache.apply_async([ci.id], queue=CMDB_QUEUE) + ci_cache.apply_async(args=(ci.id, None, None), queue=CMDB_QUEUE) return self.jsonify(code=200) @@ -240,3 +240,13 @@ class CIAutoDiscoveryStatisticsView(APIView): def get(self): return self.jsonify(CIManager.get_ad_statistics()) + + +class CIPasswordView(APIView): + url_prefix = "/ci//attributes//password" + + def get(self, ci_id, attr_id): + return self.jsonify(ci_id=ci_id, attr_id=attr_id, value=CIManager.load_password(ci_id, attr_id)) + + def post(self, ci_id, attr_id): + return self.get(ci_id, attr_id) diff --git a/cmdb-api/api/views/cmdb/ci_relation.py b/cmdb-api/api/views/cmdb/ci_relation.py index f469eccd..43689609 100644 --- a/cmdb-api/api/views/cmdb/ci_relation.py +++ b/cmdb-api/api/views/cmdb/ci_relation.py @@ -35,6 +35,7 @@ def get(self): count = get_page_size(request.values.get("count") or request.values.get("page_size")) root_id = request.values.get('root_id') + ancestor_ids = request.values.get('ancestor_ids') or None # only for many to many level = list(map(int, handle_arg_list(request.values.get('level', '1')))) query = request.values.get('q', "") @@ -42,9 +43,11 @@ def get(self): 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) + s = Search(root_id, level, query, fl, facet, page, count, sort, reverse, + ancestor_ids=ancestor_ids, has_m2m=has_m2m) try: response, counter, total, page, numfound, facet = s.search() except SearchError as e: @@ -67,9 +70,11 @@ 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 + has_m2m = request.values.get("has_m2m") in current_app.config.get('BOOL_TRUE') start = time.time() - s = Search(root_ids, level) + s = Search(root_ids, level, ancestor_ids=ancestor_ids, has_m2m=has_m2m) try: result = s.statistics(type_ids) except SearchError as e: @@ -121,14 +126,18 @@ class CIRelationView(APIView): url_prefix = "/ci_relations//" def post(self, first_ci_id, second_ci_id): + ancestor_ids = request.values.get('ancestor_ids') or None + manager = CIRelationManager() - res = manager.add(first_ci_id, second_ci_id) + res = manager.add(first_ci_id, second_ci_id, ancestor_ids=ancestor_ids) return self.jsonify(cr_id=res) def delete(self, first_ci_id, second_ci_id): + ancestor_ids = request.values.get('ancestor_ids') or None + manager = CIRelationManager() - manager.delete_2(first_ci_id, second_ci_id) + manager.delete_2(first_ci_id, second_ci_id, ancestor_ids=ancestor_ids) return self.jsonify(message="CIType Relation is deleted") @@ -151,8 +160,9 @@ def post(self): ci_ids = list(map(int, request.values.get('ci_ids'))) parents = list(map(int, request.values.get('parents', []))) children = list(map(int, request.values.get('children', []))) + ancestor_ids = request.values.get('ancestor_ids') or None - CIRelationManager.batch_update(ci_ids, parents, children) + CIRelationManager.batch_update(ci_ids, parents, children, ancestor_ids=ancestor_ids) return self.jsonify(code=200) @@ -166,7 +176,8 @@ def put(self): def delete(self): ci_ids = list(map(int, request.values.get('ci_ids'))) parents = list(map(int, request.values.get('parents', []))) + ancestor_ids = request.values.get('ancestor_ids') or None - CIRelationManager.batch_delete(ci_ids, parents) + CIRelationManager.batch_delete(ci_ids, parents, ancestor_ids=ancestor_ids) return self.jsonify(code=200) diff --git a/cmdb-api/api/views/cmdb/ci_type.py b/cmdb-api/api/views/cmdb/ci_type.py index 7d748e16..65dadcce 100644 --- a/cmdb-api/api/views/cmdb/ci_type.py +++ b/cmdb-api/api/views/cmdb/ci_type.py @@ -1,4 +1,4 @@ -# -*- coding:utf-8 -*- +# -*- coding:utf-8 -*- import json @@ -154,13 +154,20 @@ def post(self, type_id): class CITypeAttributeView(APIView): - url_prefix = ("/ci_types//attributes", "/ci_types//attributes") + url_prefix = ("/ci_types//attributes", "/ci_types//attributes", + "/ci_types/common_attributes") def get(self, type_id=None, type_name=None): + if request.path.endswith("/common_attributes"): + type_ids = handle_arg_list(request.values.get('type_ids')) + + return self.jsonify(attributes=CITypeAttributeManager.get_common_attributes(type_ids)) + t = CITypeCache.get(type_id) or CITypeCache.get(type_name) or abort(404, ErrFormat.ci_type_not_found) type_id = t.id unique_id = t.unique_id - 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) @@ -312,12 +319,14 @@ def delete(self, 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 @@ -350,9 +359,9 @@ def get(self): # export return self.send_file(bf, as_attachment=True, - attachment_filename="cmdb_template.json", + download_name="cmdb_template.json", mimetype='application/json', - cache_timeout=0) + max_age=0) @role_required(RoleEnum.CONFIG) def post(self): # import @@ -413,22 +422,22 @@ def get(self, type_id): return self.jsonify(CITypeTriggerManager.get(type_id)) @has_perm_from_args("type_id", ResourceTypeEnum.CI, PermEnum.CONFIG, CITypeManager.get_name_by_id) - @args_required("attr_id") - @args_required("notify") + @args_required("option") def post(self, type_id): - attr_id = request.values.get('attr_id') - notify = request.values.get('notify') + attr_id = request.values.get('attr_id') or None + option = request.values.get('option') - return self.jsonify(CITypeTriggerManager().add(type_id, attr_id, notify)) + return self.jsonify(CITypeTriggerManager().add(type_id, attr_id, option)) @has_perm_from_args("type_id", ResourceTypeEnum.CI, PermEnum.CONFIG, CITypeManager.get_name_by_id) - @args_required("notify") + @args_required("option") def put(self, type_id, _id): assert type_id is not None - notify = request.values.get('notify') + option = request.values.get('option') + attr_id = request.values.get('attr_id') - return self.jsonify(CITypeTriggerManager().update(_id, notify)) + return self.jsonify(CITypeTriggerManager().update(_id, attr_id, option)) @has_perm_from_args("type_id", ResourceTypeEnum.CI, PermEnum.CONFIG, CITypeManager.get_name_by_id) def delete(self, type_id, _id): @@ -451,13 +460,21 @@ def post(self, type_id, rid): _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) + acl.grant_resource_to_role_by_rid(type_name, rid, ResourceTypeEnum.CI_TYPE, perms, rebuild=False) + + if request.values.get('ci_filter') or request.values.get('attr_filter'): + CIFilterPermsCRUD().add(type_id=type_id, rid=rid, **request.values) + else: + from api.tasks.acl import role_rebuild + from api.lib.perm.acl.const import ACL_QUEUE - CIFilterPermsCRUD().add(type_id=type_id, rid=rid, **request.values) + app_id = AppCache.get('cmdb').id + current_app.logger.info((rid, app_id)) + role_rebuild.apply_async(args=(rid, app_id), queue=ACL_QUEUE) + current_app.logger.info('done') return self.jsonify(code=200) @@ -475,21 +492,27 @@ def post(self, type_id, rid): _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) + acl.revoke_resource_from_role_by_rid(type_name, rid, ResourceTypeEnum.CI_TYPE, perms, rebuild=False) - if PermEnum.READ in perms: - CIFilterPermsCRUD().delete(type_id=type_id, rid=rid) + app_id = AppCache.get('cmdb').id + resource = None + if PermEnum.READ in perms or not perms: + resource = CIFilterPermsCRUD().delete(type_id=type_id, rid=rid) - 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')) + 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 a78889eb..3e1dc87e 100644 --- a/cmdb-api/api/views/cmdb/ci_type_relation.py +++ b/cmdb-api/api/views/cmdb/ci_type_relation.py @@ -6,7 +6,10 @@ from api.lib.cmdb.ci_type import CITypeManager from api.lib.cmdb.ci_type import CITypeRelationManager -from api.lib.cmdb.const import PermEnum, ResourceTypeEnum, RoleEnum +from api.lib.cmdb.const import PermEnum +from api.lib.cmdb.const import ResourceTypeEnum +from api.lib.cmdb.const import RoleEnum +from api.lib.cmdb.preference import PreferenceManager from api.lib.cmdb.resp_format import ErrFormat from api.lib.decorator import args_required from api.lib.perm.acl.acl import ACLManager @@ -17,9 +20,14 @@ class GetChildrenView(APIView): - url_prefix = "/ci_type_relations//children" + url_prefix = ("/ci_type_relations//children", + "/ci_type_relations//recursive_level2children", + ) def get(self, parent_id): + if request.url.endswith("recursive_level2children"): + return self.jsonify(CITypeRelationManager.recursive_level2children(parent_id)) + return self.jsonify(children=CITypeRelationManager.get_children(parent_id)) @@ -102,3 +110,10 @@ def post(self, parent_id, child_id, rid): acl.revoke_resource_from_role_by_rid(resource_name, rid, ResourceTypeEnum.CI_TYPE_RELATION, perms) return self.jsonify(code=200) + + +class CITypeRelationCanEditView(APIView): + url_prefix = "/ci_type_relations///can_edit" + + def get(self, parent_id, child_id): + return self.jsonify(result=PreferenceManager.can_edit_relation(parent_id, child_id)) diff --git a/cmdb-api/api/views/cmdb/custom_dashboard.py b/cmdb-api/api/views/cmdb/custom_dashboard.py index 4991ceb7..1c20ff93 100644 --- a/cmdb-api/api/views/cmdb/custom_dashboard.py +++ b/cmdb-api/api/views/cmdb/custom_dashboard.py @@ -13,7 +13,8 @@ class CustomDashboardApiView(APIView): - url_prefix = ("/custom_dashboard", "/custom_dashboard/", "/custom_dashboard/batch") + url_prefix = ("/custom_dashboard", "/custom_dashboard/", "/custom_dashboard/batch", + "/custom_dashboard/preview") def get(self): return self.jsonify(CustomDashboardManager.get()) @@ -21,17 +22,26 @@ def get(self): @role_required(RoleEnum.CONFIG) @args_validate(CustomDashboardManager.cls) def post(self): - cm = CustomDashboardManager.add(**request.values) + if request.url.endswith("/preview"): + return self.jsonify(counter=CustomDashboardManager.preview(**request.values)) - return self.jsonify(cm.to_dict()) + cm, counter = CustomDashboardManager.add(**request.values) + + res = cm.to_dict() + res.update(counter=counter) + + return self.jsonify(res) @role_required(RoleEnum.CONFIG) @args_validate(CustomDashboardManager.cls) def put(self, _id=None): if _id is not None: - cm = CustomDashboardManager.update(_id, **request.values) + cm, counter = CustomDashboardManager.update(_id, **request.values) + + res = cm.to_dict() + res.update(counter=counter) - return self.jsonify(cm.to_dict()) + return self.jsonify(res) CustomDashboardManager.batch_update(request.values.get("id2options")) diff --git a/cmdb-api/api/views/cmdb/history.py b/cmdb-api/api/views/cmdb/history.py index 867d18ac..ceaa2dbf 100644 --- a/cmdb-api/api/views/cmdb/history.py +++ b/cmdb-api/api/views/cmdb/history.py @@ -5,14 +5,18 @@ from flask import abort from flask import request +from flask import session from api.lib.cmdb.ci import CIManager -from api.lib.cmdb.const import ResourceTypeEnum, PermEnum +from api.lib.cmdb.const import PermEnum +from api.lib.cmdb.const import ResourceTypeEnum from api.lib.cmdb.const import RoleEnum from api.lib.cmdb.history import AttributeHistoryManger +from api.lib.cmdb.history import CITriggerHistoryManager from api.lib.cmdb.history import CITypeHistoryManager from api.lib.cmdb.resp_format import ErrFormat from api.lib.perm.acl.acl import has_perm_from_args +from api.lib.perm.acl.acl import is_app_admin from api.lib.perm.acl.acl import role_required from api.lib.utils import get_page from api.lib.utils import get_page_size @@ -75,6 +79,39 @@ def get(self, ci_id): return self.jsonify(result) +class CITriggerHistoryView(APIView): + url_prefix = ("/history/ci_triggers/", "/history/ci_triggers") + + @has_perm_from_args("ci_id", ResourceTypeEnum.CI, PermEnum.READ, CIManager.get_type_name) + def get(self, ci_id=None): + if ci_id is not None: + result = CITriggerHistoryManager.get_by_ci_id(ci_id) + + return self.jsonify(result) + + if RoleEnum.CONFIG not in session.get("acl", {}).get("parentRoles", []) and not is_app_admin("cmdb"): + return abort(403, ErrFormat.role_required.format(RoleEnum.CONFIG)) + + type_id = request.values.get("type_id") + trigger_id = request.values.get("trigger_id") + operate_type = request.values.get("operate_type") + + page = get_page(request.values.get('page', 1)) + page_size = get_page_size(request.values.get('page_size', 1)) + + numfound, result = CITriggerHistoryManager.get(page, + page_size, + type_id=type_id, + trigger_id=trigger_id, + operate_type=operate_type) + + return self.jsonify(page=page, + page_size=page_size, + numfound=numfound, + total=len(result), + result=result) + + class CITypeHistoryView(APIView): url_prefix = "/history/ci_types" diff --git a/cmdb-api/api/views/cmdb/inner_secrets.py b/cmdb-api/api/views/cmdb/inner_secrets.py new file mode 100644 index 00000000..573ededa --- /dev/null +++ b/cmdb-api/api/views/cmdb/inner_secrets.py @@ -0,0 +1,37 @@ +from flask import request + +from api.lib.perm.auth import auth_abandoned +from api.lib.secrets.inner import KeyManage +from api.lib.secrets.secrets import InnerKVManger +from api.resource import APIView + + +class InnerSecretUnSealView(APIView): + url_prefix = "/secrets/unseal" + + @auth_abandoned + def post(self): + unseal_key = request.headers.get("Unseal-Token") + res = KeyManage(backend=InnerKVManger()).unseal(unseal_key) + return self.jsonify(**res) + + +class InnerSecretSealView(APIView): + url_prefix = "/secrets/seal" + + @auth_abandoned + def post(self): + unseal_key = request.headers.get("Inner-Token") + res = KeyManage(backend=InnerKVManger()).seal(unseal_key) + return self.jsonify(**res) + + +class InnerSecretAutoSealView(APIView): + url_prefix = "/secrets/auto_seal" + + @auth_abandoned + def post(self): + root_key = request.headers.get("Inner-Token") + res = KeyManage(trigger=root_key, + backend=InnerKVManger()).auto_unseal() + return self.jsonify(**res) diff --git a/cmdb-api/api/views/cmdb/preference.py b/cmdb-api/api/views/cmdb/preference.py index 67846941..cc3dab6d 100644 --- a/cmdb-api/api/views/cmdb/preference.py +++ b/cmdb-api/api/views/cmdb/preference.py @@ -5,7 +5,9 @@ from flask import request from api.lib.cmdb.ci_type import CITypeManager -from api.lib.cmdb.const import PermEnum, ResourceTypeEnum, RoleEnum +from api.lib.cmdb.const import PermEnum +from api.lib.cmdb.const import ResourceTypeEnum +from api.lib.cmdb.const import RoleEnum from api.lib.cmdb.perms import CIFilterPermsCRUD from api.lib.cmdb.preference import PreferenceManager from api.lib.cmdb.resp_format import ErrFormat 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 00000000..5ec28400 --- /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 new file mode 100644 index 00000000..6d44ba10 --- /dev/null +++ b/cmdb-api/api/views/common_setting/common_data.py @@ -0,0 +1,35 @@ +from flask import request + +from api.lib.common_setting.common_data import CommonDataCRUD +from api.resource import APIView + +prefix = '/data' + + +class DataView(APIView): + url_prefix = (f'{prefix}/',) + + def get(self, data_type): + data_list = CommonDataCRUD.get_data_by_type(data_type) + + return self.jsonify(data_list) + + def post(self, data_type): + params = request.json + CommonDataCRUD.create_new_data(data_type, **params) + + return self.jsonify(params) + + +class DataViewWithId(APIView): + url_prefix = (f'{prefix}//',) + + def put(self, data_type, _id): + params = request.json + res = CommonDataCRUD.update_data(_id, **params) + + return self.jsonify(res.to_dict()) + + def delete(self, data_type, _id): + CommonDataCRUD.delete(_id) + return self.jsonify({}) diff --git a/cmdb-api/api/views/common_setting/company_info.py b/cmdb-api/api/views/common_setting/company_info.py index d2aca2ad..4298cfd2 100644 --- a/cmdb-api/api/views/common_setting/company_info.py +++ b/cmdb-api/api/views/common_setting/company_info.py @@ -1,9 +1,7 @@ # -*- coding:utf-8 -*- -from flask import abort from flask import request from api.lib.common_setting.company_info import CompanyInfoCRUD -from api.lib.common_setting.resp_format import ErrFormat from api.resource import APIView prefix = '/company' @@ -16,15 +14,16 @@ def get(self): return self.jsonify(CompanyInfoCRUD.get()) def post(self): - info = CompanyInfoCRUD.get() - if info: - abort(400, ErrFormat.company_info_is_already_existed) data = { 'info': { **request.values } } - d = CompanyInfoCRUD.create(**data) + info = CompanyInfoCRUD.get() + if info: + d = CompanyInfoCRUD.update(info.get('id'), **data) + else: + d = CompanyInfoCRUD.create(**data) res = d.to_dict() return self.jsonify(res) diff --git a/cmdb-api/api/views/common_setting/department.py b/cmdb-api/api/views/common_setting/department.py index f9d9c7bf..ba4091a0 100644 --- a/cmdb-api/api/views/common_setting/department.py +++ b/cmdb-api/api/views/common_setting/department.py @@ -62,7 +62,7 @@ def post(self): 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)) @@ -100,7 +100,7 @@ class DepartmentSortView(APIView): def put(self): """ - 修改部门排序,只能在同一个上级内排序 + only can sort in the same parent """ department_list = request.json.get('department_list', None) if department_list is None: diff --git a/cmdb-api/api/views/common_setting/employee.py b/cmdb-api/api/views/common_setting/employee.py index a3c95a87..173dc132 100644 --- a/cmdb-api/api/views/common_setting/employee.py +++ b/cmdb-api/api/views/common_setting/employee.py @@ -1,7 +1,5 @@ # -*- coding:utf-8 -*- -import os - -from flask import abort, current_app, send_from_directory +from flask import abort from flask import request from werkzeug.datastructures import MultiDict @@ -146,42 +144,25 @@ def get(self): return self.jsonify(result) -class EmployeeViewExportExcel(APIView): - url_prefix = (f'{prefix}/export_all',) +class GetEmployeeNoticeByIds(APIView): + url_prefix = (f'{prefix}/get_notice_by_ids',) - def get(self): - col_desc_map = { - 'nickname': "姓名", - 'email': '邮箱', - 'sex': '性别', - 'mobile': '手机号', - 'department_name': '部门', - 'position_name': '岗位', - 'nickname_direct_supervisor': '直接上级', - 'last_login': '上次登录时间', - } - - # 规定了静态文件的存储位置 - excel_filename = 'all_employee_info.xlsx' - excel_path = current_app.config['UPLOAD_DIRECTORY_FULL'] - excel_path_with_filename = os.path.join(excel_path, excel_filename) - - # 根据parameter查表,自连接通过上级id获取上级名字列 - block_status = int(request.args.get('block_status', -1)) - df = EmployeeCRUD.get_export_employee_df(block_status) - - # 改变列名为中文head - try: - df = df.rename(columns=col_desc_map) - except Exception as e: - abort(500, ErrFormat.rename_columns_failed.format(str(e))) - - # 生成静态excel文件 - try: - df.to_excel(excel_path_with_filename, - sheet_name='Sheet1', index=False, encoding="utf-8") - except Exception as e: - current_app.logger.error(e) - abort(500, ErrFormat.generate_excel_failed.format(str(e))) - - return send_from_directory(excel_path, excel_filename, as_attachment=True) + def post(self): + employee_ids = request.json.get('employee_ids', []) + if not employee_ids: + result = [] + else: + result = EmployeeCRUD.get_employee_notice_by_ids(employee_ids) + return self.jsonify(result) + + +class EmployeeBindNoticeWithACLID(APIView): + url_prefix = (f'{prefix}/by_uid/bind_notice//',) + + def put(self, platform, _uid): + data = EmployeeCRUD.bind_notice_by_uid(platform, _uid) + return self.jsonify(info=data) + + def delete(self, platform, _uid): + data = EmployeeCRUD.remove_bind_notice_by_uid(platform, _uid) + return self.jsonify(info=data) diff --git a/cmdb-api/api/views/common_setting/file_manage.py b/cmdb-api/api/views/common_setting/file_manage.py index 23e00479..d66d409a 100644 --- a/cmdb-api/api/views/common_setting/file_manage.py +++ b/cmdb-api/api/views/common_setting/file_manage.py @@ -3,15 +3,16 @@ from flask import request, abort, current_app, send_from_directory from werkzeug.utils import secure_filename +import lz4.frame 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' ALLOWED_EXTENSIONS = { - 'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif', 'xls', 'xlsx', 'doc', 'docx', 'ppt', 'pptx', 'csv' + 'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif', 'xls', 'xlsx', 'doc', 'docx', 'ppt', 'pptx', 'csv', 'svg' } @@ -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): @@ -44,6 +46,8 @@ def post(self): if not file: abort(400, ErrFormat.file_is_required) extension = file.mimetype.split('/')[-1] + if '+' in extension: + extension = file.filename.split('.')[-1] if file.filename == '': filename = f'.{extension}' else: @@ -53,11 +57,20 @@ def post(self): 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)) - - return self.jsonify(file_name=filename) - - abort(400, 'Extension not allow') + 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)) + + abort(400, ErrFormat.file_type_not_allowed.format(filename)) diff --git a/cmdb-api/api/views/common_setting/notice_config.py b/cmdb-api/api/views/common_setting/notice_config.py new file mode 100644 index 00000000..e5dea630 --- /dev/null +++ b/cmdb-api/api/views/common_setting/notice_config.py @@ -0,0 +1,79 @@ +from flask import request, abort, current_app +from werkzeug.datastructures import MultiDict + +from api.lib.perm.auth import auth_with_app_token +from api.models.common_setting import NoticeConfig +from api.resource import APIView +from api.lib.common_setting.notice_config import NoticeConfigForm, NoticeConfigUpdateForm, NoticeConfigCRUD +from api.lib.decorator import args_required +from api.lib.common_setting.resp_format import ErrFormat + +prefix = '/notice_config' + + +class NoticeConfigView(APIView): + url_prefix = (f'{prefix}',) + + @args_required('platform') + @auth_with_app_token + def get(self): + platform = request.args.get('platform') + res = NoticeConfig.get_by(first=True, to_dict=True, platform=platform) or {} + return self.jsonify(res) + + def post(self): + form = NoticeConfigForm(MultiDict(request.json)) + if not form.validate(): + abort(400, ','.join(['{}: {}'.format(filed, ','.join(msg)) for filed, msg in form.errors.items()])) + + data = NoticeConfigCRUD.add_notice_config(**form.data) + return self.jsonify(data.to_dict()) + + +class NoticeConfigUpdateView(APIView): + url_prefix = (f'{prefix}/',) + + def put(self, _id): + form = NoticeConfigUpdateForm(MultiDict(request.json)) + if not form.validate(): + abort(400, ','.join(['{}: {}'.format(filed, ','.join(msg)) for filed, msg in form.errors.items()])) + + data = NoticeConfigCRUD.edit_notice_config(_id, **form.data) + return self.jsonify(data.to_dict()) + + +class CheckEmailServer(APIView): + url_prefix = (f'{prefix}/send_test_email',) + + def post(self): + receive_address = request.args.get('receive_address') + info = request.values.get('info', {}) + + try: + + result = NoticeConfigCRUD.test_send_email(receive_address, **info) + return self.jsonify(result=result) + except Exception as e: + current_app.logger.error('test_send_email err:') + current_app.logger.error(e) + if 'Timed Out' in str(e): + abort(400, ErrFormat.email_send_timeout) + abort(400, f"{str(e)}") + + +class NoticeConfigGetView(APIView): + method_decorators = [] + url_prefix = (f'{prefix}/all',) + + @auth_with_app_token + def get(self): + res = NoticeConfigCRUD.get_all() + return self.jsonify(res) + + +class NoticeAppBotView(APIView): + url_prefix = (f'{prefix}/app_bot',) + + def get(self): + res = NoticeConfigCRUD.get_app_bot() + return self.jsonify(res) diff --git a/cmdb-api/api/views/entry.py b/cmdb-api/api/views/entry.py index 8c5e43ea..9d27d23e 100644 --- a/cmdb-api/api/views/entry.py +++ b/cmdb-api/api/views/entry.py @@ -6,7 +6,9 @@ from flask_restful import Api from api.resource import register_resources -from .account import LoginView, LogoutView, AuthWithKeyView +from .account import AuthWithKeyView +from .account import LoginView +from .account import LogoutView HERE = os.path.abspath(os.path.dirname(__file__)) diff --git a/cmdb-api/autoapp.py b/cmdb-api/autoapp.py index 9ff83cc2..d16e79dd 100644 --- a/cmdb-api/autoapp.py +++ b/cmdb-api/autoapp.py @@ -1,14 +1,7 @@ # -*- coding: utf-8 -*- """Create an application instance.""" -from flask import g -from flask_login import current_user from api.app import create_app app = create_app() - - -@app.before_request -def before_request(): - g.user = current_user diff --git a/cmdb-api/babel.cfg b/cmdb-api/babel.cfg new file mode 100644 index 00000000..991e57e1 --- /dev/null +++ b/cmdb-api/babel.cfg @@ -0,0 +1 @@ +[python: api/**.py] diff --git a/cmdb-api/celery_worker.py b/cmdb-api/celery_worker.py index 56921410..5f6dbbd9 100644 --- a/cmdb-api/celery_worker.py +++ b/cmdb-api/celery_worker.py @@ -3,7 +3,7 @@ from api.app import create_app from api.extensions import celery -# celery worker -A celery_worker.celery -l DEBUG -E -Q xxxx +# celery -A celery_worker.celery worker -l DEBUG -E -Q xxxx app = create_app() app.app_context().push() diff --git a/cmdb-api/migrations/README b/cmdb-api/migrations/README new file mode 100644 index 00000000..98e4f9c4 --- /dev/null +++ b/cmdb-api/migrations/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/cmdb-api/migrations/alembic.ini b/cmdb-api/migrations/alembic.ini new file mode 100644 index 00000000..f8ed4801 --- /dev/null +++ b/cmdb-api/migrations/alembic.ini @@ -0,0 +1,45 @@ +# A generic, single database configuration. + +[alembic] +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/cmdb-api/migrations/env.py b/cmdb-api/migrations/env.py new file mode 100644 index 00000000..666f4220 --- /dev/null +++ b/cmdb-api/migrations/env.py @@ -0,0 +1,110 @@ +from __future__ import with_statement + +import logging +from logging.config import fileConfig + +from sqlalchemy import engine_from_config +from sqlalchemy import pool + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +from flask import current_app +config.set_main_option( + 'sqlalchemy.url', current_app.config.get( + 'SQLALCHEMY_DATABASE_URI').replace('%', '%%')) +target_metadata = current_app.extensions['migrate'].db.metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + +# 添加要屏蔽的table列表 +exclude_tables = ["c_cfp"] + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, target_metadata=target_metadata, literal_binds=True, + include_name=include_name + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, 'autogenerate', False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info('No changes in schema detected.') + + connectable = engine_from_config( + config.get_section(config.config_ini_section), + prefix='sqlalchemy.', + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=target_metadata, + process_revision_directives=process_revision_directives, + include_name=include_name, + **current_app.extensions['migrate'].configure_args + ) + + with context.begin_transaction(): + context.run_migrations() + + +def include_name(name, type_, parent_names): + if type_ == "table": + return name not in exclude_tables + elif parent_names.get("table_name") in exclude_tables: + return False + + return True + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/cmdb-api/migrations/script.py.mako b/cmdb-api/migrations/script.py.mako new file mode 100644 index 00000000..2c015630 --- /dev/null +++ b/cmdb-api/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/cmdb-api/migrations/versions/6a4df2623057_.py b/cmdb-api/migrations/versions/6a4df2623057_.py new file mode 100644 index 00000000..ab6b330e --- /dev/null +++ b/cmdb-api/migrations/versions/6a4df2623057_.py @@ -0,0 +1,360 @@ +"""empty message + +Revision ID: 6a4df2623057 +Revises: +Create Date: 2023-10-13 15:17:00.066858 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = '6a4df2623057' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('common_data', + sa.Column('deleted_at', sa.DateTime(), nullable=True), + sa.Column('deleted', sa.Boolean(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('data_type', sa.VARCHAR(length=255), nullable=True), + sa.Column('data', sa.JSON(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_common_data_deleted'), 'common_data', ['deleted'], unique=False) + op.create_table('common_notice_config', + sa.Column('deleted_at', sa.DateTime(), nullable=True), + sa.Column('deleted', sa.Boolean(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('platform', sa.VARCHAR(length=255), nullable=False), + sa.Column('info', sa.JSON(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_common_notice_config_deleted'), 'common_notice_config', ['deleted'], unique=False) + op.add_column('c_attributes', sa.Column('choice_other', sa.JSON(), nullable=True)) + op.drop_index('idx_c_attributes_uid', table_name='c_attributes') + op.create_index(op.f('ix_c_attributes_uid'), 'c_attributes', ['uid'], unique=False) + op.drop_index('ix_c_custom_dashboard_deleted', table_name='c_c_d') + op.create_index(op.f('ix_c_c_d_deleted'), 'c_c_d', ['deleted'], unique=False) + op.drop_index('ix_c_ci_type_triggers_deleted', table_name='c_c_t_t') + op.create_index(op.f('ix_c_c_t_t_deleted'), 'c_c_t_t', ['deleted'], unique=False) + op.drop_index('ix_c_ci_type_unique_constraints_deleted', table_name='c_c_t_u_c') + op.create_index(op.f('ix_c_c_t_u_c_deleted'), 'c_c_t_u_c', ['deleted'], unique=False) + op.drop_index('c_ci_types_uid', table_name='c_ci_types') + op.create_index(op.f('ix_c_ci_types_uid'), 'c_ci_types', ['uid'], unique=False) + op.alter_column('c_prv', 'uid', + existing_type=mysql.INTEGER(), + nullable=False) + op.drop_index('ix_c_preference_relation_views_deleted', table_name='c_prv') + op.drop_index('ix_c_preference_relation_views_name', table_name='c_prv') + op.create_index(op.f('ix_c_prv_deleted'), 'c_prv', ['deleted'], unique=False) + op.create_index(op.f('ix_c_prv_name'), 'c_prv', ['name'], unique=False) + op.create_index(op.f('ix_c_prv_uid'), 'c_prv', ['uid'], unique=False) + op.drop_index('ix_c_preference_show_attributes_deleted', table_name='c_psa') + op.drop_index('ix_c_preference_show_attributes_uid', table_name='c_psa') + op.create_index(op.f('ix_c_psa_deleted'), 'c_psa', ['deleted'], unique=False) + op.create_index(op.f('ix_c_psa_uid'), 'c_psa', ['uid'], unique=False) + op.drop_index('ix_c_preference_tree_views_deleted', table_name='c_ptv') + op.drop_index('ix_c_preference_tree_views_uid', table_name='c_ptv') + op.create_index(op.f('ix_c_ptv_deleted'), 'c_ptv', ['deleted'], unique=False) + op.create_index(op.f('ix_c_ptv_uid'), 'c_ptv', ['uid'], unique=False) + op.alter_column('common_department', 'department_name', + existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255), + comment=None, + existing_comment='部门名称', + existing_nullable=True) + op.alter_column('common_department', 'department_director_id', + existing_type=mysql.INTEGER(), + comment=None, + existing_comment='部门负责人ID', + existing_nullable=True) + op.alter_column('common_department', 'department_parent_id', + existing_type=mysql.INTEGER(), + comment=None, + existing_comment='上级部门ID', + existing_nullable=True) + op.alter_column('common_department', 'sort_value', + existing_type=mysql.INTEGER(), + comment=None, + existing_comment='排序值', + existing_nullable=True) + op.alter_column('common_department', 'acl_rid', + existing_type=mysql.INTEGER(), + comment=None, + existing_comment='ACL中rid', + existing_nullable=True) + op.alter_column('common_employee', 'email', + existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255), + comment=None, + existing_comment='邮箱', + existing_nullable=True) + op.alter_column('common_employee', 'username', + existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255), + comment=None, + existing_comment='用户名', + existing_nullable=True) + op.alter_column('common_employee', 'nickname', + existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255), + comment=None, + existing_comment='姓名', + existing_nullable=True) + op.alter_column('common_employee', 'sex', + existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=64), + comment=None, + existing_comment='性别', + existing_nullable=True) + op.alter_column('common_employee', 'position_name', + existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255), + comment=None, + existing_comment='职位名称', + existing_nullable=True) + op.alter_column('common_employee', 'mobile', + existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255), + comment=None, + existing_comment='电话号码', + existing_nullable=True) + op.alter_column('common_employee', 'avatar', + existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255), + comment=None, + existing_comment='头像', + existing_nullable=True) + op.alter_column('common_employee', 'direct_supervisor_id', + existing_type=mysql.INTEGER(), + comment=None, + existing_comment='直接上级ID', + existing_nullable=True) + op.alter_column('common_employee', 'department_id', + existing_type=mysql.INTEGER(), + comment=None, + existing_comment='部门ID', + existing_nullable=True) + op.alter_column('common_employee', 'acl_uid', + existing_type=mysql.INTEGER(), + comment=None, + existing_comment='ACL中uid', + existing_nullable=True) + op.alter_column('common_employee', 'acl_rid', + existing_type=mysql.INTEGER(), + comment=None, + existing_comment='ACL中rid', + existing_nullable=True) + op.alter_column('common_employee', 'acl_virtual_rid', + existing_type=mysql.INTEGER(), + comment=None, + existing_comment='ACL中虚拟角色rid', + existing_nullable=True) + op.alter_column('common_employee', 'last_login', + existing_type=mysql.TIMESTAMP(), + comment=None, + existing_comment='上次登录时间', + existing_nullable=True) + op.alter_column('common_employee', 'block', + existing_type=mysql.INTEGER(), + comment=None, + existing_comment='锁定状态', + existing_nullable=True) + op.alter_column('common_employee_info', 'info', + existing_type=mysql.JSON(), + comment=None, + existing_comment='员工信息', + existing_nullable=True) + op.alter_column('common_employee_info', 'employee_id', + existing_type=mysql.INTEGER(), + comment=None, + existing_comment='员工ID', + existing_nullable=True) + op.alter_column('common_internal_message', 'title', + existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255), + comment=None, + existing_comment='标题', + existing_nullable=True) + op.alter_column('common_internal_message', 'content', + existing_type=mysql.TEXT(charset='utf8mb3', collation='utf8mb3_unicode_ci'), + comment=None, + existing_comment='内容', + existing_nullable=True) + op.alter_column('common_internal_message', 'path', + existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255), + comment=None, + existing_comment='跳转路径', + existing_nullable=True) + op.alter_column('common_internal_message', 'is_read', + existing_type=mysql.TINYINT(display_width=1), + comment=None, + existing_comment='是否已读', + existing_nullable=True) + op.alter_column('common_internal_message', 'app_name', + existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=128), + comment=None, + existing_comment='应用名称', + existing_nullable=False) + op.alter_column('common_internal_message', 'category', + existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=128), + comment=None, + existing_comment='分类', + existing_nullable=False) + op.alter_column('common_internal_message', 'message_data', + existing_type=mysql.JSON(), + comment=None, + existing_comment='数据', + existing_nullable=True) + op.drop_column('users', 'apps') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('users', sa.Column('apps', mysql.JSON(), nullable=True)) + op.alter_column('common_internal_message', 'message_data', + existing_type=mysql.JSON(), + comment='数据', + existing_nullable=True) + op.alter_column('common_internal_message', 'category', + existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=128), + comment='分类', + existing_nullable=False) + op.alter_column('common_internal_message', 'app_name', + existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=128), + comment='应用名称', + existing_nullable=False) + op.alter_column('common_internal_message', 'is_read', + existing_type=mysql.TINYINT(display_width=1), + comment='是否已读', + existing_nullable=True) + op.alter_column('common_internal_message', 'path', + existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255), + comment='跳转路径', + existing_nullable=True) + op.alter_column('common_internal_message', 'content', + existing_type=mysql.TEXT(charset='utf8mb3', collation='utf8mb3_unicode_ci'), + comment='内容', + existing_nullable=True) + op.alter_column('common_internal_message', 'title', + existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255), + comment='标题', + existing_nullable=True) + op.alter_column('common_employee_info', 'employee_id', + existing_type=mysql.INTEGER(), + comment='员工ID', + existing_nullable=True) + op.alter_column('common_employee_info', 'info', + existing_type=mysql.JSON(), + comment='员工信息', + existing_nullable=True) + op.alter_column('common_employee', 'block', + existing_type=mysql.INTEGER(), + comment='锁定状态', + existing_nullable=True) + op.alter_column('common_employee', 'last_login', + existing_type=mysql.TIMESTAMP(), + comment='上次登录时间', + existing_nullable=True) + op.alter_column('common_employee', 'acl_virtual_rid', + existing_type=mysql.INTEGER(), + comment='ACL中虚拟角色rid', + existing_nullable=True) + op.alter_column('common_employee', 'acl_rid', + existing_type=mysql.INTEGER(), + comment='ACL中rid', + existing_nullable=True) + op.alter_column('common_employee', 'acl_uid', + existing_type=mysql.INTEGER(), + comment='ACL中uid', + existing_nullable=True) + op.alter_column('common_employee', 'department_id', + existing_type=mysql.INTEGER(), + comment='部门ID', + existing_nullable=True) + op.alter_column('common_employee', 'direct_supervisor_id', + existing_type=mysql.INTEGER(), + comment='直接上级ID', + existing_nullable=True) + op.alter_column('common_employee', 'avatar', + existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255), + comment='头像', + existing_nullable=True) + op.alter_column('common_employee', 'mobile', + existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255), + comment='电话号码', + existing_nullable=True) + op.alter_column('common_employee', 'position_name', + existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255), + comment='职位名称', + existing_nullable=True) + op.alter_column('common_employee', 'sex', + existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=64), + comment='性别', + existing_nullable=True) + op.alter_column('common_employee', 'nickname', + existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255), + comment='姓名', + existing_nullable=True) + op.alter_column('common_employee', 'username', + existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255), + comment='用户名', + existing_nullable=True) + op.alter_column('common_employee', 'email', + existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255), + comment='邮箱', + existing_nullable=True) + op.alter_column('common_department', 'acl_rid', + existing_type=mysql.INTEGER(), + comment='ACL中rid', + existing_nullable=True) + op.alter_column('common_department', 'sort_value', + existing_type=mysql.INTEGER(), + comment='排序值', + existing_nullable=True) + op.alter_column('common_department', 'department_parent_id', + existing_type=mysql.INTEGER(), + comment='上级部门ID', + existing_nullable=True) + op.alter_column('common_department', 'department_director_id', + existing_type=mysql.INTEGER(), + comment='部门负责人ID', + existing_nullable=True) + op.alter_column('common_department', 'department_name', + existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255), + comment='部门名称', + existing_nullable=True) + op.drop_index(op.f('ix_c_ptv_uid'), table_name='c_ptv') + op.drop_index(op.f('ix_c_ptv_deleted'), table_name='c_ptv') + op.create_index('ix_c_preference_tree_views_uid', 'c_ptv', ['uid'], unique=False) + op.create_index('ix_c_preference_tree_views_deleted', 'c_ptv', ['deleted'], unique=False) + op.drop_index(op.f('ix_c_psa_uid'), table_name='c_psa') + op.drop_index(op.f('ix_c_psa_deleted'), table_name='c_psa') + op.create_index('ix_c_preference_show_attributes_uid', 'c_psa', ['uid'], unique=False) + op.create_index('ix_c_preference_show_attributes_deleted', 'c_psa', ['deleted'], unique=False) + op.drop_index(op.f('ix_c_prv_uid'), table_name='c_prv') + op.drop_index(op.f('ix_c_prv_name'), table_name='c_prv') + op.drop_index(op.f('ix_c_prv_deleted'), table_name='c_prv') + op.create_index('ix_c_preference_relation_views_name', 'c_prv', ['name'], unique=False) + op.create_index('ix_c_preference_relation_views_deleted', 'c_prv', ['deleted'], unique=False) + op.alter_column('c_prv', 'uid', + existing_type=mysql.INTEGER(), + nullable=True) + op.drop_index(op.f('ix_c_ci_types_uid'), table_name='c_ci_types') + op.create_index('c_ci_types_uid', 'c_ci_types', ['uid'], unique=False) + op.drop_index(op.f('ix_c_c_t_u_c_deleted'), table_name='c_c_t_u_c') + op.create_index('ix_c_ci_type_unique_constraints_deleted', 'c_c_t_u_c', ['deleted'], unique=False) + op.drop_index(op.f('ix_c_c_t_t_deleted'), table_name='c_c_t_t') + op.create_index('ix_c_ci_type_triggers_deleted', 'c_c_t_t', ['deleted'], unique=False) + op.drop_index(op.f('ix_c_c_d_deleted'), table_name='c_c_d') + op.create_index('ix_c_custom_dashboard_deleted', 'c_c_d', ['deleted'], unique=False) + op.drop_index(op.f('ix_c_attributes_uid'), table_name='c_attributes') + op.create_index('idx_c_attributes_uid', 'c_attributes', ['uid'], unique=False) + op.drop_column('c_attributes', 'choice_other') + op.drop_index(op.f('ix_common_notice_config_deleted'), table_name='common_notice_config') + op.drop_table('common_notice_config') + op.drop_index(op.f('ix_common_data_deleted'), table_name='common_data') + op.drop_table('common_data') + # ### end Alembic commands ### diff --git a/cmdb-api/requirements.txt b/cmdb-api/requirements.txt index 39b32cbe..516f93dd 100644 --- a/cmdb-api/requirements.txt +++ b/cmdb-api/requirements.txt @@ -1,80 +1,54 @@ -i https://mirrors.aliyun.com/pypi/simple alembic==1.7.7 -amqp==2.6.1 -aniso8601==9.0.1 -APScheduler==3.10.1 -attrs==23.1.0 -backports.zoneinfo==0.2.1 -bcrypt==4.0.1 -beautifulsoup4==4.12.2 -billiard==3.6.4.0 bs4==0.0.1 -cachelib==0.9.0 -celery==4.3.0 +celery>=5.3.1 celery-once==3.0.1 -certifi==2023.5.7 -charset-normalizer==3.1.0 click==8.1.3 -dnspython==2.3.0 elasticsearch==7.17.9 email-validator==1.3.1 environs==4.2.0 flasgger==0.9.5 -Flask==1.0.3 -Flask-APScheduler==1.12.4 -Flask-Bcrypt==0.7.1 +Flask==2.3.2 +Flask-Bcrypt==1.0.1 +flask-babel==4.0.0 Flask-Caching==2.0.2 Flask-Cors==4.0.0 -Flask-Login==0.4.1 +Flask-Login>=0.6.2 Flask-Migrate==2.5.2 -Flask-RESTful==0.3.7 -Flask-SQLAlchemy==2.4.0 -future==0.18.2 -gunicorn==19.5.0 -idna==3.4 -importlib-metadata==6.8.0 -importlib-resources==6.0.0 -itsdangerous==2.0.1 -Jinja2==3.0.1 +Flask-RESTful==0.3.10 +Flask-SQLAlchemy==2.5.0 +future==0.18.3 +gunicorn==21.0.1 +hvac==2.0.0 +itsdangerous==2.1.2 +Jinja2==3.1.2 jinja2schema==0.1.4 jsonschema==4.18.0 -jsonschema-specifications==2023.6.1 -kombu==4.4.0 +kombu>=5.3.1 Mako==1.2.4 MarkupSafe==2.1.3 marshmallow==2.20.2 -meld3==2.0.1 -mistune==3.0.1 more-itertools==5.0.0 msgpack-python==0.5.6 -numpy==1.18.5 -pandas==1.3.2 -Pillow==8.3.2 -pkgutil_resolve_name==1.3.10 -pyasn1==0.5.0 -pyasn1-modules==0.3.0 -pycryptodome==3.12.0 +Pillow>=10.0.1 +cryptography>=41.0.2 PyJWT==2.4.0 -PyMySQL==0.9.3 -python-dateutil==2.8.2 -python-dotenv==1.0.0 -python-ldap==3.2.0 -pytz==2023.3 -PyYAML==6.0 -redis==3.2.1 -referencing==0.29.1 +PyMySQL==1.1.0 +ldap3==2.9.1 +PyYAML==6.0.1 +redis==4.6.0 requests==2.31.0 -rpds-py==0.8.8 -six==1.12.0 -soupsieve==2.4.1 -SQLAlchemy==1.3.5 +requests_oauthlib==1.3.1 +markdownify==0.11.6 +six==1.16.0 +SQLAlchemy==1.4.49 supervisor==4.0.3 timeout-decorator==0.5.0 toposort==1.10 treelib==1.6.1 -tzlocal==5.0.1 -urllib3==1.26.16 -vine==1.3.0 -Werkzeug==0.15.5 +Werkzeug>=2.3.6 WTForms==3.0.0 -zipp==3.16.0 \ No newline at end of file +shamir~=17.12.0 +pycryptodomex>=3.19.0 +colorama>=0.4.6 +lz4>=4.3.2 \ No newline at end of file diff --git a/cmdb-api/settings.example.py b/cmdb-api/settings.example.py index 3f735170..48870922 100644 --- a/cmdb-api/settings.example.py +++ b/cmdb-api/settings.example.py @@ -11,10 +11,10 @@ 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 @@ # # 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,10 +32,11 @@ } # # 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_KEY_PREFIX = "CMDB::" +CACHE_REDIS_PASSWORD = '' +CACHE_KEY_PREFIX = 'CMDB::' CACHE_DEFAULT_TIMEOUT = 3000 # # log @@ -53,41 +54,100 @@ DEFAULT_MAIL_SENDER = '' # # queue -CELERY_RESULT_BACKEND = "redis://127.0.0.1:6379/2" -BROKER_URL = 'redis://127.0.0.1:6379/2' -BROKER_VHOST = '/' +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 +} ONCE = { 'backend': 'celery_once.backends.Redis', 'settings': { - 'url': BROKER_URL, + 'url': CELERY['broker_url'], } } -# # 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" - -# # ldap -AUTH_WITH_LDAP = False -LDAP_SERVER = '' -LDAP_DOMAIN = '' +# =============================== Authentication =========================================================== + +# # 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"] -USE_ACL = False +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 -CMDB_API = "http://127.0.0.1:5000/api/v0.1" +# # secrets +SECRETS_ENGINE = 'inner' # 'inner' or 'vault' +VAULT_URL = '' +VAULT_TOKEN = '' +INNER_TRIGGER_TOKEN = '' diff --git a/cmdb-api/tests/sample.py b/cmdb-api/tests/sample.py index 4e7548be..557ccd70 100644 --- a/cmdb-api/tests/sample.py +++ b/cmdb-api/tests/sample.py @@ -1,8 +1,11 @@ # -*- coding: utf-8 -*- """provide some sample data in database""" -import uuid import random +import uuid +from api.lib.cmdb.ci import CIManager, CIRelationManager +from api.lib.cmdb.ci_type import CITypeAttributeManager +from api.models.acl import User from api.models.cmdb import ( Attribute, CIType, @@ -11,16 +14,12 @@ CITypeRelation, RelationType ) -from api.models.acl import User - -from api.lib.cmdb.ci_type import CITypeAttributeManager -from api.lib.cmdb.ci import CIManager, CIRelationManager def force_add_user(): - from flask import g - if not getattr(g, "user", None): - g.user = User.query.first() + from flask_login import current_user, login_user + if not getattr(current_user, "username", None): + login_user(User.query.first()) def init_attributes(num=1): @@ -77,12 +76,12 @@ def init_relation_type(num=1): def init_ci_type_relation(num=1): result = [] - ci_types = init_ci_types(num+1) + ci_types = init_ci_types(num + 1) relation_types = init_relation_type(num) for i in range(num): result.append(CITypeRelation.create( parent_id=ci_types[i].id, - child_id=ci_types[i+1].id, + child_id=ci_types[i + 1].id, relation_type_id=relation_types[i].id )) return result diff --git a/cmdb-ui/.env b/cmdb-ui/.env index ee9f4c75..1be71b79 100644 --- a/cmdb-ui/.env +++ b/cmdb-ui/.env @@ -1,5 +1,5 @@ NODE_ENV=production VUE_APP_PREVIEW=false -VUE_APP_API_BASE_URL=/api +VUE_APP_API_BASE_URL=http://127.0.0.1:5000/api VUE_APP_BUILD_PACKAGES="ticket,calendar,acl" VUE_APP_IS_OUTER=true diff --git a/cmdb-ui/.gitattributes b/cmdb-ui/.gitattributes deleted file mode 100644 index e5073192..00000000 --- a/cmdb-ui/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -public/* linguist-vendored \ No newline at end of file diff --git a/cmdb-ui/.gitignore b/cmdb-ui/.gitignore deleted file mode 100644 index 666e7499..00000000 --- a/cmdb-ui/.gitignore +++ /dev/null @@ -1,26 +0,0 @@ -.DS_Store -node_modules -/dist -/dist.zip -/temp - -# local env files -.env.local -.env.*.local - -# Log files -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# Editor directories and files -.idea -.vscode -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw* -*.css.map - -.env.development \ No newline at end of file diff --git a/cmdb-ui/README.zh-CN.md b/cmdb-ui/README.zh-CN.md deleted file mode 100644 index 835f5530..00000000 --- a/cmdb-ui/README.zh-CN.md +++ /dev/null @@ -1,11 +0,0 @@ -#Oneops-UI - -```shell -## build -yarn run build - -## develop -yarn run serve - - -``` \ No newline at end of file diff --git a/cmdb-ui/package.json b/cmdb-ui/package.json index f3c6a510..04bab42e 100644 --- a/cmdb-ui/package.json +++ b/cmdb-ui/package.json @@ -17,6 +17,8 @@ "@babel/plugin-syntax-import-meta": "^7.10.4", "@riophae/vue-treeselect": "^0.4.0", "@vue/composition-api": "^1.7.1", + "@wangeditor/editor": "^5.1.23", + "@wangeditor/editor-for-vue": "^1.0.0", "ant-design-vue": "^1.6.5", "axios": "0.18.0", "babel-eslint": "^8.2.2", @@ -25,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", @@ -37,6 +40,7 @@ "moment": "^2.24.0", "nprogress": "^0.2.0", "relation-graph": "^1.1.0", + "snabbdom": "^3.5.1", "sortablejs": "1.9.0", "viser-vue": "^2.4.8", "vue": "2.6.11", @@ -44,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", @@ -53,21 +58,22 @@ "vuedraggable": "^2.23.0", "vuex": "^3.1.1", "vxe-table": "3.6.9", - "vxe-table-plugin-export-xlsx": "^3.0.4", + "vxe-table-plugin-export-xlsx": "2.0.0", "xe-utils": "3", "xlsx": "0.15.0", "xlsx-js-style": "^1.2.0" }, "devDependencies": { "@ant-design/colors": "^3.2.2", + "@babel/core": "^7.23.2", "@babel/polyfill": "^7.2.5", + "@babel/preset-env": "^7.23.2", "@vue/cli-plugin-babel": "4.5.17", "@vue/cli-plugin-eslint": "^4.0.5", "@vue/cli-plugin-unit-jest": "^4.0.5", "@vue/cli-service": "^4.0.5", "@vue/eslint-config-standard": "^4.0.0", "@vue/test-utils": "^1.0.0-beta.30", - "babel-core": "7.0.0-bridge.0", "babel-jest": "^23.6.0", "babel-plugin-import": "^1.11.0", "babel-plugin-transform-remove-console": "^6.9.4", diff --git a/cmdb-ui/public/iconfont/demo_index.html b/cmdb-ui/public/iconfont/demo_index.html index f7d3d470..1f80a614 100644 --- a/cmdb-ui/public/iconfont/demo_index.html +++ b/cmdb-ui/public/iconfont/demo_index.html @@ -55,9587 +55,11764 @@

  • - -
    rule_100
    -
    &#xe87a;
    + +
    OAuth2.0
    +
    &#xe8d8;
  • - -
    itsm-flag
    -
    &#xe878;
    + +
    OIDC
    +
    &#xe8d6;
  • - -
    itsm-recommend
    -
    &#xe872;
    + +
    cas
    +
    &#xe8d7;
  • - -
    ops-help
    -
    &#xe877;
    + +
    setting-authentication
    +
    &#xe8d5;
  • - -
    ops-help-hover
    -
    &#xe876;
    + +
    setting-authentication-selected
    +
    &#xe8d4;
  • - -
    itsm-knowledge-pending_examine
    -
    &#xe875;
    + +
    itsm-knowledge (2)
    +
    &#xe8d2;
  • - -
    itsm-knowledge-published
    -
    &#xe874;
    + +
    itsm-QRcode
    +
    &#xe8d3;
  • - -
    itsm-knowledge-submitted
    -
    &#xe871;
    + +
    oneterm-playback
    +
    &#xe8d1;
  • - -
    itsm-knowledge-deleted
    -
    &#xe873;
    + +
    oneterm-disconnect
    +
    &#xe8d0;
  • - -
    itsm-knowledge
    -
    &#xe870;
    + +
    oneterm-key-selected
    +
    &#xe8cf;
  • - -
    ops-itsm-ticketsetting-selected
    -
    &#xe860;
    + +
    oneterm-key
    +
    &#xe8ce;
  • - -
    ops-itsm-reports-selected
    -
    &#xe861;
    + +
    oneterm-gateway
    +
    &#xe8b9;
  • - -
    ops-itsm-servicecatalog-selected
    -
    &#xe862;
    + +
    oneterm-gateway-selected
    +
    &#xe8bf;
  • - -
    ops-itsm-ticketmanage-selected
    -
    &#xe863;
    + +
    oneterm-account
    +
    &#xe8c0;
  • - -
    ops-itsm-knowledge-selected
    -
    &#xe864;
    + +
    oneterm-account-selected
    +
    &#xe8c1;
  • - -
    ops-itsm-workstation-selected
    -
    &#xe865;
    + +
    oneterm-command
    +
    &#xe8c2;
  • - -
    ops-itsm-servicedesk-selected
    -
    &#xe866;
    + +
    oneterm-command-selected
    +
    &#xe8c3;
  • - -
    ops-itsm-planticket-selected
    -
    &#xe867;
    + +
    oneterm-asset_list
    +
    &#xe8c4;
  • - -
    ops-itsm-servicecatalog
    -
    &#xe868;
    + +
    oneterm-asset_list-selected
    +
    &#xe8c5;
  • - -
    ops-itsm-ticketmanage
    -
    &#xe869;
    + +
    oneterm-online
    +
    &#xe8c6;
  • - -
    ops-itsm-reports
    -
    &#xe86a;
    + +
    oneterm-online-selected
    +
    &#xe8c7;
  • - -
    ops-itsm-knowledge
    -
    &#xe86b;
    + +
    oneterm-history-selected
    +
    &#xe8c8;
  • - -
    ops-itsm-planticket
    -
    &#xe86c;
    + +
    oneterm-history
    +
    &#xe8c9;
  • - -
    ops-itsm-ticketsetting
    -
    &#xe86d;
    + +
    oneterm-entry_log
    +
    &#xe8ca;
  • - -
    ops-itsm-servicedesk
    -
    &#xe86e;
    + +
    oneterm-entry_log-selected
    +
    &#xe8cb;
  • - -
    ops-itsm-workstation
    -
    &#xe86f;
    + +
    oneterm-operation_log
    +
    &#xe8cc;
  • - -
    monitor-webPerf
    -
    &#xe84f;
    + +
    oneterm-operation_log-selected
    +
    &#xe8cd;
  • - -
    monitor-image (1)
    -
    &#xe857;
    + +
    oneterm-workstation-selected
    +
    &#xe8b7;
  • - -
    monitor-other
    -
    &#xe85c;
    + +
    oneterm-workstation
    +
    &#xe8b8;
  • - -
    monitor-font
    -
    &#xe85d;
    + +
    oneterm-file-selected
    +
    &#xe8be;
  • - -
    monitor-css
    -
    &#xe85a;
    + +
    oneterm-file
    +
    &#xe8bc;
  • - -
    monitor-html
    -
    &#xe85b;
    + +
    oneterm-time
    +
    &#xe8bd;
  • - -
    monitor-video
    -
    &#xe855;
    + +
    oneterm-download
    +
    &#xe8bb;
  • - -
    monitor-js
    -
    &#xe856;
    + +
    oneterm-command record
    +
    &#xe8ba;
  • - -
    monitor-audio
    -
    &#xe858;
    + +
    oneterm-connected assets
    +
    &#xe8b6;
  • - -
    monitor-text
    -
    &#xe859;
    + +
    oneterm-total assets
    +
    &#xe8b5;
  • - -
    monitor-jiancedian
    -
    &#xe853;
    + +
    oneterm-switch (3)
    +
    &#xe8b4;
  • - -
    monitor-zongfenhegexiangpingfen
    -
    &#xe854;
    + +
    oneterm-session
    +
    &#xe8b3;
  • - -
    monitor-dig
    -
    &#xe84d;
    + +
    oneterm-connection
    +
    &#xe8b2;
  • - -
    monitor-dns
    -
    &#xe84e;
    + +
    oneterm-log in
    +
    &#xe8b1;
  • - -
    monitor-traceroute
    -
    &#xe850;
    + +
    oneterm-dashboard
    +
    &#xe8af;
  • - -
    monitor-mtr
    -
    &#xe851;
    + +
    oneterm-dashboard-selected
    +
    &#xe8b0;
  • - -
    monitor-websocket
    -
    &#xe852;
    + +
    oneterm-recent session
    +
    &#xe8ae;
  • - -
    monitor-performance_monitor-copy
    -
    &#xe85e;
    + +
    oneterm-my assets
    +
    &#xe8ad;
  • - -
    yuansuxingneng
    -
    &#xe841;
    + +
    oneterm-log
    +
    &#xe8aa;
  • - -
    zhujijiankong
    -
    &#xe842;
    + +
    oneterm-conversation-selected
    +
    &#xe8ab;
  • - -
    zhujiqiang
    -
    &#xe843;
    + +
    oneterm-conversation
    +
    &#xe8ac;
  • - -
    xingnengpinggu
    -
    &#xe844;
    + +
    oneterm-log-selected
    +
    &#xe8a9;
  • - -
    wodekanban
    -
    &#xe845;
    + +
    oneterm-assets
    +
    &#xe8a7;
  • - -
    wangzhanjiankong
    -
    &#xe846;
    + +
    oneterm-assets-selected
    +
    &#xe8a8;
  • - -
    tongji
    -
    &#xe847;
    + +
    itsm-down
    +
    &#xe8a5;
  • - -
    wangyexingneng
    -
    &#xe848;
    + +
    itsm-up
    +
    &#xe8a6;
  • - -
    wangluotuobu
    -
    &#xe849;
    + +
    itsm-download
    +
    &#xe8a4;
  • - -
    shishizhuangtai
    -
    &#xe84a;
    + +
    itsm-print
    +
    &#xe8a3;
  • - -
    gailan
    -
    &#xe84b;
    + +
    itsm-view
    +
    &#xe8a2;
  • - -
    zonglan
    -
    &#xe84c;
    + +
    itsm-word
    +
    &#xe8a1;
  • - -
    itsm-workstation-fast
    -
    &#xe838;
    + +
    datainsight-custom
    +
    &#xe89e;
  • - -
    itsm-workstation-handle
    -
    &#xe839;
    + +
    datainsight-prometheus
    +
    &#xe89f;
  • - -
    itsm-stop_hang_up
    -
    &#xe83a;
    + +
    datainsight-zabbix
    +
    &#xe8a0;
  • - -
    itsm-workstation-overview
    -
    &#xe83b;
    + +
    setting-main people
    +
    &#xe89a;
  • - -
    itsm-workstation-inform
    -
    &#xe83c;
    + +
    setting-deputy people
    +
    &#xe89d;
  • - -
    itsm-workstation-notice
    -
    &#xe83d;
    + +
    ops-setting-duty
    +
    &#xe89c;
  • - -
    itsm-workstation-todo list
    -
    &#xe83e;
    + +
    ops-setting-duty-selected
    +
    &#xe89b;
  • - -
    itsm-workstation-duty
    -
    &#xe83f;
    + +
    datainsight-sequential
    +
    &#xe899;
  • - -
    itsm-workstation-initiate
    -
    &#xe840;
    + +
    datainsight-close
    +
    &#xe898;
  • - -
    itsm-my-my_initiate
    -
    &#xe835;
    + +
    datainsight-handle
    +
    &#xe897;
  • - -
    itsm-my-my_todo-copy
    -
    &#xe85f;
    + +
    datainsight-table
    +
    &#xe896;
  • - -
    itsm-my-draft
    -
    &#xe831;
    + +
    icon-xianxing-password
    +
    &#xe894;
  • - -
    itsm-my-all
    -
    &#xe832;
    + +
    icon-xianxing-link
    +
    &#xe895;
  • - -
    itsm-my-in_process
    -
    &#xe833;
    + +
    itsm-oneclick download
    +
    &#xe892;
  • - -
    itsm-my-my_todo
    -
    &#xe834;
    + +
    itsm-package download
    +
    &#xe893;
  • - -
    itsm-my-share
    -
    &#xe836;
    + +
    weixin
    +
    &#xe891;
  • - -
    itsm-my-pending_claim
    -
    &#xe837;
    + +
    itsm-again
    +
    &#xe88f;
  • - -
    itsm-log-ABORTED
    -
    &#xe830;
    + +
    itsm-next
    +
    &#xe890;
  • - -
    itsm-log-FAILED
    -
    &#xe82d;
    + +
    wechatApp
    +
    &#xe88e;
  • - -
    itsm-log-SUCCESS
    -
    &#xe82e;
    + +
    robot
    +
    &#xe88b;
  • - -
    itsm-log-ERROR
    -
    &#xe82f;
    + +
    feishuApp
    +
    &#xe88c;
  • - -
    itsm-service-type
    -
    &#xe82b;
    + +
    dingdingApp
    +
    &#xe88d;
  • - -
    itsm-my
    -
    &#xe82c;
    + +
    email
    +
    &#xe88a;
  • - -
    ops-monitor-hostwall
    -
    &#xe829;
    + +
    setting-feishu
    +
    &#xe887;
  • - -
    ops-monitor-hostwall-setting
    -
    &#xe82a;
    + +
    setting-feishu-selected
    +
    &#xe888;
  • - -
    授权用户部门图标
    -
    &#xe828;
    + +
    cmdb-histogram
    +
    &#xe886;
  • - -
    itsm-baseInfo
    -
    &#xe81f;
    + +
    cmdb-index
    +
    &#xe883;
  • - -
    itsm-association
    -
    &#xe820;
    + +
    cmdb-piechart
    +
    &#xe884;
  • - -
    itsm-handleInfo
    -
    &#xe821;
    + +
    cmdb-line
    +
    &#xe885;
  • - -
    itsm-intelligence
    -
    &#xe822;
    + +
    cmdb-table
    +
    &#xe882;
  • - -
    itsm-leaveMess
    -
    &#xe823;
    + +
    itsm-all
    +
    &#xe87f;
  • - -
    itsm-log
    -
    &#xe824;
    + +
    itsm-reply
    +
    &#xe87e;
  • - -
    itsm-solution
    -
    &#xe825;
    + +
    itsm-information
    +
    &#xe880;
  • - -
    itsm-sla
    -
    &#xe826;
    + +
    itsm-contact
    +
    &#xe881;
  • - -
    itsm-naire
    -
    &#xe827;
    + +
    itsm-my-processed
    +
    &#xe87d;
  • - -
    itsm-claim
    -
    &#xe817;
    + +
    rule_7
    +
    &#xe87c;
  • - -
    itsm-hang_up
    -
    &#xe818;
    + +
    itsm-my-completed
    +
    &#xe879;
  • - -
    itsm-flow_chart
    -
    &#xe819;
    + +
    itsm-my-plan
    +
    &#xe87b;
  • - -
    itsm-export
    -
    &#xe81a;
    + +
    rule_100
    +
    &#xe87a;
  • - -
    itsm-reminder
    -
    &#xe81b;
    + +
    itsm-flag
    +
    &#xe878;
  • - -
    itsm-share
    -
    &#xe81c;
    + +
    itsm-recommend
    +
    &#xe872;
  • - -
    itsm-terminate
    -
    &#xe81d;
    + +
    ops-help
    +
    &#xe877;
  • - -
    itsm-transfer
    -
    &#xe81e;
    + +
    ops-help-hover
    +
    &#xe876;
  • - -
    ops-cmdb-customdashboard-selected
    -
    &#xe80b;
    + +
    itsm-knowledge-pending_examine
    +
    &#xe875;
  • - -
    ops-cmdb-adr-selected
    -
    &#xe80c;
    + +
    itsm-knowledge-published
    +
    &#xe874;
  • - -
    ops-cmdb-operation
    -
    &#xe80d;
    + +
    itsm-knowledge-submitted
    +
    &#xe871;
  • - -
    ops-cmdb-customdashboard
    -
    &#xe80e;
    + +
    itsm-knowledge-deleted
    +
    &#xe873;
  • - -
    ops-cmdb-adr
    -
    &#xe80f;
    + +
    itsm-knowledge
    +
    &#xe870;
  • - -
    ops-cmdb-preferencerelation-selected
    -
    &#xe810;
    + +
    ops-itsm-ticketsetting-selected
    +
    &#xe860;
  • - -
    ops-cmdb-operation-selected
    -
    &#xe811;
    + +
    ops-itsm-reports-selected
    +
    &#xe861;
  • - -
    ops-cmdb-preferencerelation
    -
    &#xe812;
    + +
    ops-itsm-servicecatalog-selected
    +
    &#xe862;
  • - -
    ops-cmdb-modelrelation
    -
    &#xe813;
    + +
    ops-itsm-ticketmanage-selected
    +
    &#xe863;
  • - -
    ops-cmdb-modelrelation-selected
    -
    &#xe814;
    + +
    ops-itsm-knowledge-selected
    +
    &#xe864;
  • - -
    ops-cmdb-relationtype-selected
    -
    &#xe815;
    + +
    ops-itsm-workstation-selected
    +
    &#xe865;
  • - -
    ops-cmdb-relationtype
    -
    &#xe816;
    + +
    ops-itsm-servicedesk-selected
    +
    &#xe866;
  • - -
    ops-cmdb-batch-selected
    -
    &#xe803;
    + +
    ops-itsm-planticket-selected
    +
    &#xe867;
  • - -
    ops-cmdb-batch
    -
    &#xe80a;
    + +
    ops-itsm-servicecatalog
    +
    &#xe868;
  • - -
    ops-cmdb-adc-selected
    -
    &#xe7f7;
    + +
    ops-itsm-ticketmanage
    +
    &#xe869;
  • - -
    ops-cmdb-resource-selected
    -
    &#xe7f8;
    + +
    ops-itsm-reports
    +
    &#xe86a;
  • - -
    ops-cmdb-preference-selected
    -
    &#xe7f9;
    + +
    ops-itsm-knowledge
    +
    &#xe86b;
  • - -
    ops-cmdb-preference
    -
    &#xe7fa;
    + +
    ops-itsm-planticket
    +
    &#xe86c;
  • - -
    ops-cmdb-screen
    -
    &#xe7fb;
    + +
    ops-itsm-ticketsetting
    +
    &#xe86d;
  • - -
    ops-cmdb-tree-selected
    -
    &#xe7fc;
    + +
    ops-itsm-servicedesk
    +
    &#xe86e;
  • - -
    ops-cmdb-relation-selected
    -
    &#xe7fd;
    + +
    ops-itsm-workstation
    +
    &#xe86f;
  • - -
    ops-cmdb-adc
    -
    &#xe7fe;
    + +
    monitor-webPerf
    +
    &#xe84f;
  • - -
    ops-cmdb-search-selected
    -
    &#xe7ff;
    + +
    monitor-image (1)
    +
    &#xe857;
  • - -
    ops-cmdb-relation
    -
    &#xe800;
    + +
    monitor-other
    +
    &#xe85c;
  • - -
    ops-cmdb-tree
    -
    &#xe801;
    + +
    monitor-font
    +
    &#xe85d;
  • - -
    ops-cmdb-citype-selected
    -
    &#xe802;
    + +
    monitor-css
    +
    &#xe85a;
  • - -
    ops-cmdb-dashboard-selected
    -
    &#xe804;
    + +
    monitor-html
    +
    &#xe85b;
  • - -
    ops-cmdb-citype
    -
    &#xe805;
    + +
    monitor-video
    +
    &#xe855;
  • - -
    ops-cmdb-dashboard
    -
    &#xe806;
    + +
    monitor-js
    +
    &#xe856;
  • - -
    ops-cmdb-screen-selected
    -
    &#xe807;
    + +
    monitor-audio
    +
    &#xe858;
  • - -
    ops-cmdb-resource
    -
    &#xe808;
    + +
    monitor-text
    +
    &#xe859;
  • - -
    ops-cmdb-search
    -
    &#xe809;
    + +
    monitor-jiancedian
    +
    &#xe853;
  • - -
    icon-itsm-product redemption
    -
    &#xe7ed;
    + +
    monitor-zongfenhegexiangpingfen
    +
    &#xe854;
  • - -
    icon-itsm-creat portfolio
    -
    &#xe7ee;
    + +
    monitor-dig
    +
    &#xe84d;
  • - -
    icon-itsm-product foreclosure process
    -
    &#xe7ef;
    + +
    monitor-dns
    +
    &#xe84e;
  • - -
    icon-itsm-money in and out
    -
    &#xe7f0;
    + +
    monitor-traceroute
    +
    &#xe850;
  • - -
    icon-itsm-new signal light
    -
    &#xe7f1;
    + +
    monitor-mtr
    +
    &#xe851;
  • - -
    icon-itsm-libra permission application
    -
    &#xe7f2;
    + +
    monitor-websocket
    +
    &#xe852;
  • - -
    icon-itsm-default service work order
    -
    &#xe7f3;
    + +
    monitor-performance_monitor-copy
    +
    &#xe85e;
  • - -
    icon-itsm-default problem work order
    -
    &#xe7f4;
    + +
    yuansuxingneng
    +
    &#xe841;
  • - -
    icon-itsm-default event work order
    -
    &#xe7f5;
    + +
    zhujijiankong
    +
    &#xe842;
  • - -
    icon-itsm-default change work order
    -
    &#xe7f6;
    + +
    zhujiqiang
    +
    &#xe843;
  • - -
    icon-itsm-request for classified data
    -
    &#xe7e9;
    + +
    xingnengpinggu
    +
    &#xe844;
  • - -
    icon-itsm-external procurement
    -
    &#xe7ea;
    + +
    wodekanban
    +
    &#xe845;
  • - -
    icon-itsm-application for payment of funds
    -
    &#xe7eb;
    + +
    wangzhanjiankong
    +
    &#xe846;
  • - -
    icon-itsm-product subscription
    -
    &#xe7ec;
    + +
    tongji
    +
    &#xe847;
  • - -
    icon-itsm-claims reimbursement process (1)
    -
    &#xe7e2;
    + +
    wangyexingneng
    +
    &#xe848;
  • - -
    icon-itsm-open an account
    -
    &#xe7e3;
    + +
    wangluotuobu
    +
    &#xe849;
  • - -
    icon-itsm-seal application
    -
    &#xe7e4;
    + +
    shishizhuangtai
    +
    &#xe84a;
  • - -
    icon-itsm-external reimbursement process
    -
    &#xe7e5;
    + +
    gailan
    +
    &#xe84b;
  • - -
    icon-itsm-item approval
    -
    &#xe7e6;
    + +
    zonglan
    +
    &#xe84c;
  • - -
    icon-itsm-servers and other devices application
    -
    &#xe7e7;
    + +
    itsm-workstation-fast
    +
    &#xe838;
  • - -
    icon-itsm-payment process
    -
    &#xe7e8;
    + +
    itsm-workstation-handle
    +
    &#xe839;
  • - -
    icon-itsm-employee income certificate
    -
    &#xe7dd;
    + +
    itsm-stop_hang_up
    +
    &#xe83a;
  • - -
    incumbency certification
    -
    &#xe7de;
    + +
    itsm-workstation-overview
    +
    &#xe83b;
  • - -
    icon-itsm-intern dimission process
    -
    &#xe7df;
    + +
    itsm-workstation-inform
    +
    &#xe83c;
  • - -
    icon-itsm-intern attendance
    -
    &#xe7e0;
    + +
    itsm-workstation-notice
    +
    &#xe83d;
  • - -
    icon-itsm-new employees' induction process
    -
    &#xe7e1;
    + +
    itsm-workstation-todo list
    +
    &#xe83e;
  • - -
    icon-itsm-field application
    -
    &#xe7dc;
    + +
    itsm-workstation-duty
    +
    &#xe83f;
  • - -
    icon-itsm-leave process
    -
    &#xe7da;
    + +
    itsm-workstation-initiate
    +
    &#xe840;
  • - -
    icon-itsm-new onboarding process
    -
    &#xe7db;
    + +
    itsm-my-my_initiate
    +
    &#xe835;
  • - -
    icon-shidi-aws
    -
    &#xe7d8;
    + +
    itsm-my-my_todo-copy
    +
    &#xe85f;
  • - -
    icon-xianxing-aws
    -
    &#xe7d9;
    + +
    itsm-my-draft
    +
    &#xe831;
  • - -
    caise-aws
    -
    &#xe7d7;
    + +
    itsm-my-all
    +
    &#xe832;
  • - -
    icon-xianxing-dayinji
    -
    &#xe7d3;
    + +
    itsm-my-in_process
    +
    &#xe833;
  • - -
    icon-shiti-dayinji
    -
    &#xe7d4;
    + +
    itsm-my-my_todo
    +
    &#xe834;
  • - -
    icon-shiti-chajian
    -
    &#xe7d5;
    + +
    itsm-my-share
    +
    &#xe836;
  • - -
    caise-chajian
    -
    &#xe7d6;
    + +
    itsm-my-pending_claim
    +
    &#xe837;
  • - -
    caise-dayinji
    -
    &#xe7d1;
    + +
    itsm-log-ABORTED
    +
    &#xe830;
  • - -
    caise-chajian
    -
    &#xe7d2;
    + +
    itsm-log-FAILED
    +
    &#xe82d;
  • - -
    itsm-service-all
    -
    &#xe7cf;
    + +
    itsm-log-SUCCESS
    +
    &#xe82e;
  • - -
    itsm-service-common
    -
    &#xe7d0;
    + +
    itsm-log-ERROR
    +
    &#xe82f;
  • - -
    itsm-upload
    -
    &#xe7bc;
    + +
    itsm-service-type
    +
    &#xe82b;
  • - -
    itsm-code
    -
    &#xe7bd;
    + +
    itsm-my
    +
    &#xe82c;
  • - -
    itsm-paragraph
    -
    &#xe7be;
    + +
    ops-monitor-hostwall
    +
    &#xe829;
  • - -
    itsm-department
    -
    &#xe7bf;
    + +
    ops-monitor-hostwall-setting
    +
    &#xe82a;
  • - -
    itsm-phone
    -
    &#xe7c0;
    + +
    授权用户部门图标
    +
    &#xe828;
  • - -
    itsm-employee
    -
    &#xe7c1;
    + +
    itsm-baseInfo
    +
    &#xe81f;
  • - -
    itsm-description
    -
    &#xe7c2;
    + +
    itsm-association
    +
    &#xe820;
  • - -
    itsm-rich
    -
    &#xe7c3;
    + +
    itsm-handleInfo
    +
    &#xe821;
  • - -
    itsm-image
    -
    &#xe7c4;
    + +
    itsm-intelligence
    +
    &#xe822;
  • - -
    itsm-start-end
    -
    &#xe7c5;
    + +
    itsm-leaveMess
    +
    &#xe823;
  • - -
    itsm-single-select
    -
    &#xe7c6;
    + +
    itsm-log
    +
    &#xe824;
  • - -
    itsm-link
    -
    &#xe7c7;
    + +
    itsm-solution
    +
    &#xe825;
  • - -
    itsm-date
    -
    &#xe7c8;
    + +
    itsm-sla
    +
    &#xe826;
  • - -
    itsm-input
    -
    &#xe7c9;
    + +
    itsm-naire
    +
    &#xe827;
  • - -
    itsm-textarea
    -
    &#xe7ca;
    + +
    itsm-claim
    +
    &#xe817;
  • - -
    itsm-input-number
    -
    &#xe7cb;
    + +
    itsm-hang_up
    +
    &#xe818;
  • - -
    itsm-multiple-select
    -
    &#xe7cc;
    + +
    itsm-flow_chart
    +
    &#xe819;
  • - -
    itsm-email
    -
    &#xe7cd;
    + +
    itsm-export
    +
    &#xe81a;
  • - -
    itsm-table
    -
    &#xe7ce;
    + +
    itsm-reminder
    +
    &#xe81b;
  • - -
    itsm-service
    -
    &#xe7ba;
    + +
    itsm-share
    +
    &#xe81c;
  • - -
    itsm-change
    -
    &#xe7b8;
    + +
    itsm-terminate
    +
    &#xe81d;
  • - -
    itsm-problem
    -
    &#xe7b9;
    + +
    itsm-transfer
    +
    &#xe81e;
  • - -
    itsm-event
    -
    &#xe7bb;
    + +
    ops-cmdb-customdashboard-selected
    +
    &#xe80b;
  • - -
    itsm_approve_config
    -
    &#xe7b1;
    + +
    ops-cmdb-adr-selected
    +
    &#xe80c;
  • - -
    itsm_execute_user
    -
    &#xe7b2;
    + +
    ops-cmdb-operation
    +
    &#xe80d;
  • - -
    itsm_notice_config
    -
    &#xe7b3;
    + +
    ops-cmdb-customdashboard
    +
    &#xe80e;
  • - -
    itsm_timeout
    -
    &#xe7b4;
    + +
    ops-cmdb-adr
    +
    &#xe80f;
  • - -
    itsm_func_config
    -
    &#xe7b5;
    + +
    ops-cmdb-preferencerelation-selected
    +
    &#xe810;
  • - -
    itsm_time_config
    -
    &#xe7b6;
    + +
    ops-cmdb-operation-selected
    +
    &#xe811;
  • - -
    itsm_auto_trigger
    -
    &#xe7b7;
    + +
    ops-cmdb-preferencerelation
    +
    &#xe812;
  • - -
    icon-xianxing-复制
    -
    &#xe7b0;
    + +
    ops-cmdb-modelrelation
    +
    &#xe813;
  • - -
    itsm-node-examine
    -
    &#xe7aa;
    + +
    ops-cmdb-modelrelation-selected
    +
    &#xe814;
  • - -
    itsm-node-branch
    -
    &#xe7ab;
    + +
    ops-cmdb-relationtype-selected
    +
    &#xe815;
  • - -
    itsm-node-auto
    -
    &#xe7ac;
    + +
    ops-cmdb-relationtype
    +
    &#xe816;
  • - -
    itsm-node-end
    -
    &#xe7ad;
    + +
    ops-cmdb-batch-selected
    +
    &#xe803;
  • - -
    itsm-node-strat
    -
    &#xe7ae;
    + +
    ops-cmdb-batch
    +
    &#xe80a;
  • - -
    itsm-node-manual
    -
    &#xe7af;
    + +
    ops-cmdb-adc-selected
    +
    &#xe7f7;
  • - -
    icon-xianxing-删除
    -
    &#xe7a9;
    + +
    ops-cmdb-resource-selected
    +
    &#xe7f8;
  • - -
    icon-xianxing-编辑
    -
    &#xe7a8;
    + +
    ops-cmdb-preference-selected
    +
    &#xe7f9;
  • - -
    monitor-base
    -
    &#xe721;
    + +
    ops-cmdb-preference
    +
    &#xe7fa;
  • - -
    setting-structure-depart1
    -
    &#xe71e;
    + +
    ops-cmdb-screen
    +
    &#xe7fb;
  • - -
    setting-structure-depart2
    -
    &#xe71f;
    + +
    ops-cmdb-tree-selected
    +
    &#xe7fc;
  • - -
    bat
    -
    &#xe7a4;
    + +
    ops-cmdb-relation-selected
    +
    &#xe7fd;
  • - -
    power shell
    -
    &#xe7a5;
    + +
    ops-cmdb-adc
    +
    &#xe7fe;
  • - -
    shell
    -
    &#xe7a6;
    + +
    ops-cmdb-search-selected
    +
    &#xe7ff;
  • - -
    ops-setting-role-selected
    -
    &#xe7a0;
    + +
    ops-cmdb-relation
    +
    &#xe800;
  • - -
    ops-setting-group-selected
    -
    &#xe7a1;
    -
  • + +
    ops-cmdb-tree
    +
    &#xe801;
    +
  • - -
    ops-setting-role
    -
    &#xe7a2;
    + +
    ops-cmdb-citype-selected
    +
    &#xe802;
  • - -
    ops-setting-group
    -
    &#xe7a3;
    + +
    ops-cmdb-dashboard-selected
    +
    &#xe804;
  • - -
    ops-setting-technician
    -
    &#xe79e;
    + +
    ops-cmdb-citype
    +
    &#xe805;
  • - -
    ops-setting-user
    -
    &#xe79f;
    + +
    ops-cmdb-dashboard
    +
    &#xe806;
  • - -
    click house
    -
    &#xe78e;
    + +
    ops-cmdb-screen-selected
    +
    &#xe807;
  • - -
    ping监控
    -
    &#xe78f;
    + +
    ops-cmdb-resource
    +
    &#xe808;
  • - -
    zabbix
    -
    &#xe790;
    + +
    ops-cmdb-search
    +
    &#xe809;
  • - -
    SNMP
    -
    &#xe791;
    + +
    icon-itsm-product redemption
    +
    &#xe7ed;
  • - -
    网页
    -
    &#xe792;
    + +
    icon-itsm-creat portfolio
    +
    &#xe7ee;
  • - -
    后台运行
    -
    &#xe793;
    + +
    icon-itsm-product foreclosure process
    +
    &#xe7ef;
  • - -
    端口探测
    -
    &#xe794;
    + +
    icon-itsm-money in and out
    +
    &#xe7f0;
  • - -
    ipmi带外
    -
    &#xe795;
    + +
    icon-itsm-new signal light
    +
    &#xe7f1;
  • - -
    前台运行
    -
    &#xe796;
    + +
    icon-itsm-libra permission application
    +
    &#xe7f2;
  • - -
    Group 2289
    -
    &#xe797;
    + +
    icon-itsm-default service work order
    +
    &#xe7f3;
  • - -
    mongodb
    -
    &#xe798;
    + +
    icon-itsm-default problem work order
    +
    &#xe7f4;
  • - -
    MYSQL
    -
    &#xe799;
    + +
    icon-itsm-default event work order
    +
    &#xe7f5;
  • - -
    HTTP API
    -
    &#xe79a;
    + +
    icon-itsm-default change work order
    +
    &#xe7f6;
  • - -
    日志扫描
    -
    &#xe79b;
    + +
    icon-itsm-request for classified data
    +
    &#xe7e9;
  • - -
    进程管理
    -
    &#xe79c;
    + +
    icon-itsm-external procurement
    +
    &#xe7ea;
  • - -
    elasticsearch
    -
    &#xe79d;
    + +
    icon-itsm-application for payment of funds
    +
    &#xe7eb;
  • - -
    ops-setting-role-system
    -
    &#xe78c;
    + +
    icon-itsm-product subscription
    +
    &#xe7ec;
  • - -
    ops-setting-role-system-selected
    -
    &#xe78d;
    + +
    icon-itsm-claims reimbursement process (1)
    +
    &#xe7e2;
  • - -
    ops-datainsight-audit
    -
    &#xe780;
    + +
    icon-itsm-open an account
    +
    &#xe7e3;
  • - -
    ops-datainsight-audit-selected
    -
    &#xe781;
    + +
    icon-itsm-seal application
    +
    &#xe7e4;
  • - -
    ops-datainsight-dashboard-selected
    -
    &#xe782;
    + +
    icon-itsm-external reimbursement process
    +
    &#xe7e5;
  • - -
    ops-datainsight-dashboard
    -
    &#xe783;
    + +
    icon-itsm-item approval
    +
    &#xe7e6;
  • - -
    ops-datainsight-method
    -
    &#xe784;
    + +
    icon-itsm-servers and other devices application
    +
    &#xe7e7;
  • - -
    ops-datainsight-method-selected
    -
    &#xe785;
    + +
    icon-itsm-payment process
    +
    &#xe7e8;
  • - -
    ops-datainsight-rule-selected
    -
    &#xe786;
    + +
    icon-itsm-employee income certificate
    +
    &#xe7dd;
  • - -
    ops-datainsight-rule
    -
    &#xe787;
    + +
    incumbency certification
    +
    &#xe7de;
  • - -
    ops-datainsight-origin-selected
    -
    &#xe788;
    + +
    icon-itsm-intern dimission process
    +
    &#xe7df;
  • - -
    ops-datainsight-origin
    -
    &#xe789;
    + +
    icon-itsm-intern attendance
    +
    &#xe7e0;
  • - -
    ops-datainsight-alert
    -
    &#xe78a;
    + +
    icon-itsm-new employees' induction process
    +
    &#xe7e1;
  • - -
    ops-datainsight-alert-selected
    -
    &#xe78b;
    + +
    icon-itsm-field application
    +
    &#xe7dc;
  • - -
    rule_6
    -
    &#xe77e;
    + +
    icon-itsm-leave process
    +
    &#xe7da;
  • - -
    icon-xianxing-shenji
    -
    &#xe77f;
    + +
    icon-itsm-new onboarding process
    +
    &#xe7db;
  • - -
    rule_3
    -
    &#xe77c;
    + +
    icon-shidi-aws
    +
    &#xe7d8;
  • - -
    rule_5
    -
    &#xe77d;
    + +
    icon-xianxing-aws
    +
    &#xe7d9;
  • - -
    rule_1
    -
    &#xe778;
    + +
    caise-aws
    +
    &#xe7d7;
  • - -
    rule_8
    -
    &#xe779;
    + +
    icon-xianxing-dayinji
    +
    &#xe7d3;
  • - -
    rule_2
    -
    &#xe77a;
    + +
    icon-shiti-dayinji
    +
    &#xe7d4;
  • - -
    rule_4
    -
    &#xe77b;
    + +
    icon-shiti-chajian
    +
    &#xe7d5;
  • - -
    level_4
    -
    &#xe774;
    + +
    caise-chajian
    +
    &#xe7d6;
  • - -
    level_3
    -
    &#xe775;
    + +
    caise-dayinji
    +
    &#xe7d1;
  • - -
    level_1
    -
    &#xe776;
    + +
    caise-chajian
    +
    &#xe7d2;
  • - -
    level_2
    -
    &#xe777;
    + +
    itsm-service-all
    +
    &#xe7cf;
  • - -
    系统管理
    -
    &#xe773;
    + +
    itsm-service-common
    +
    &#xe7d0;
  • - -
    caise-华为云
    -
    &#xe75f;
    + +
    itsm-upload
    +
    &#xe7bc;
  • - -
    caise-负载均衡
    -
    &#xe760;
    + +
    itsm-code
    +
    &#xe7bd;
  • - -
    caise-交换机
    -
    &#xe761;
    + +
    itsm-paragraph
    +
    &#xe7be;
  • - -
    caise-路由器
    -
    &#xe762;
    + +
    itsm-department
    +
    &#xe7bf;
  • - -
    caise-部门
    -
    &#xe763;
    + +
    itsm-phone
    +
    &#xe7c0;
  • - -
    caise-虚拟机
    -
    &#xe764;
    + +
    itsm-employee
    +
    &#xe7c1;
  • - -
    caise-应用
    -
    &#xe765;
    + +
    itsm-description
    +
    &#xe7c2;
  • - -
    caise-Nginx
    -
    &#xe766;
    + +
    itsm-rich
    +
    &#xe7c3;
  • - -
    caise-腾讯云
    -
    &#xe767;
    + +
    itsm-image
    +
    &#xe7c4;
  • - -
    caise-产品
    -
    &#xe768;
    + +
    itsm-start-end
    +
    &#xe7c5;
  • - -
    caise-防火墙
    -
    &#xe769;
    + +
    itsm-single-select
    +
    &#xe7c6;
  • - -
    caise-docker
    -
    &#xe76a;
    + +
    itsm-link
    +
    &#xe7c7;
  • - -
    caise-硬盘
    -
    &#xe76b;
    + +
    itsm-date
    +
    &#xe7c8;
  • - -
    caise-物理机
    -
    &#xe76c;
    + +
    itsm-input
    +
    &#xe7c9;
  • - -
    caise-网卡
    -
    &#xe76d;
    + +
    itsm-textarea
    +
    &#xe7ca;
  • - -
    caise-内存
    -
    &#xe76e;
    + +
    itsm-input-number
    +
    &#xe7cb;
  • - -
    caise-阿里云
    -
    &#xe76f;
    + +
    itsm-multiple-select
    +
    &#xe7cc;
  • - -
    caise-Apache
    -
    &#xe770;
    + +
    itsm-email
    +
    &#xe7cd;
  • - -
    caise-redis
    -
    &#xe771;
    + +
    itsm-table
    +
    &#xe7ce;
  • - -
    caise-Tomcat
    -
    &#xe772;
    + +
    itsm-service
    +
    &#xe7ba;
  • - -
    icon-防火墙
    -
    &#xe746;
    + +
    itsm-change
    +
    &#xe7b8;
  • - -
    icon-防火墙
    -
    &#xe750;
    + +
    itsm-problem
    +
    &#xe7b9;
  • - -
    icon-交换机
    -
    &#xe74b;
    + +
    itsm-event
    +
    &#xe7bb;
  • - -
    icon-负载均衡
    -
    &#xe74c;
    + +
    itsm_approve_config
    +
    &#xe7b1;
  • - -
    icon-内存
    -
    &#xe74d;
    + +
    itsm_execute_user
    +
    &#xe7b2;
  • - -
    icon-物理机
    -
    &#xe74e;
    + +
    itsm_notice_config
    +
    &#xe7b3;
  • - -
    icon-路由器
    -
    &#xe74f;
    + +
    itsm_timeout
    +
    &#xe7b4;
  • - -
    icon-硬盘
    -
    &#xe751;
    + +
    itsm_func_config
    +
    &#xe7b5;
  • - -
    icon-产品
    -
    &#xe752;
    + +
    itsm_time_config
    +
    &#xe7b6;
  • - -
    icon-应用
    -
    &#xe753;
    + +
    itsm_auto_trigger
    +
    &#xe7b7;
  • - -
    icon-Nginx
    -
    &#xe754;
    + +
    icon-xianxing-复制
    +
    &#xe7b0;
  • - -
    icon-docker
    -
    &#xe755;
    + +
    itsm-node-examine
    +
    &#xe7aa;
  • - -
    icon-网卡
    -
    &#xe756;
    + +
    itsm-node-branch
    +
    &#xe7ab;
  • - -
    icon-Apache
    -
    &#xe757;
    + +
    itsm-node-auto
    +
    &#xe7ac;
  • - -
    icon-redis
    -
    &#xe758;
    + +
    itsm-node-end
    +
    &#xe7ad;
  • - -
    icon-Tomcat
    -
    &#xe759;
    + +
    itsm-node-strat
    +
    &#xe7ae;
  • - -
    icon-虚拟机
    -
    &#xe75a;
    + +
    itsm-node-manual
    +
    &#xe7af;
  • - -
    icon-部门
    -
    &#xe75b;
    -
  • + +
    icon-xianxing-删除
    +
    &#xe7a9;
    +
  • - -
    icon-华为云
    -
    &#xe75c;
    + +
    icon-xianxing-编辑
    +
    &#xe7a8;
  • - -
    icon-腾讯云
    -
    &#xe75d;
    + +
    monitor-base
    +
    &#xe721;
  • - -
    icon-阿里云
    -
    &#xe75e;
    + +
    setting-structure-depart1
    +
    &#xe71e;
  • - -
    icon-部门
    -
    &#xe668;
    + +
    setting-structure-depart2
    +
    &#xe71f;
  • - -
    icon-负载均衡
    -
    &#xe669;
    + +
    bat
    +
    &#xe7a4;
  • - -
    icon-交换机
    -
    &#xe739;
    + +
    power shell
    +
    &#xe7a5;
  • - -
    icon-产品
    -
    &#xe73a;
    + +
    shell
    +
    &#xe7a6;
  • - -
    icon-华为云
    -
    &#xe73b;
    + +
    ops-setting-role-selected
    +
    &#xe7a0;
  • - -
    icon-物理机
    -
    &#xe73c;
    + +
    ops-setting-group-selected
    +
    &#xe7a1;
  • - -
    icon-应用
    -
    &#xe73d;
    + +
    ops-setting-role
    +
    &#xe7a2;
  • - -
    icon-路由器
    -
    &#xe73e;
    + +
    ops-setting-group
    +
    &#xe7a3;
  • - -
    icon-网卡
    -
    &#xe73f;
    + +
    ops-setting-technician
    +
    &#xe79e;
  • - -
    icon-Nginx
    -
    &#xe740;
    + +
    ops-setting-user
    +
    &#xe79f;
  • - -
    icon-阿里云
    -
    &#xe741;
    + +
    click house
    +
    &#xe78e;
  • - -
    icon-虚拟机
    -
    &#xe742;
    + +
    ping监控
    +
    &#xe78f;
  • - -
    icon-硬盘
    -
    &#xe743;
    + +
    zabbix
    +
    &#xe790;
  • - -
    icon-Apache
    -
    &#xe744;
    + +
    SNMP
    +
    &#xe791;
  • - -
    icon-docker
    -
    &#xe745;
    + +
    网页
    +
    &#xe792;
  • - -
    icon-redis
    -
    &#xe747;
    + +
    后台运行
    +
    &#xe793;
  • - -
    icon-内存
    -
    &#xe748;
    + +
    端口探测
    +
    &#xe794;
  • - -
    icon-Tomcat
    -
    &#xe749;
    + +
    ipmi带外
    +
    &#xe795;
  • - -
    icon-腾讯云
    -
    &#xe74a;
    + +
    前台运行
    +
    &#xe796;
  • - -
    ops-dot-copy
    -
    &#xe7a7;
    + +
    Group 2289
    +
    &#xe797;
  • - -
    ops-review
    -
    &#xe737;
    + +
    mongodb
    +
    &#xe798;
  • - -
    ops-dot
    -
    &#xe738;
    + +
    MYSQL
    +
    &#xe799;
  • - -
    ops-setting-notice
    -
    &#xe72f;
    + +
    HTTP API
    +
    &#xe79a;
  • - -
    ops-setting-notice-selected
    -
    &#xe730;
    + +
    日志扫描
    +
    &#xe79b;
  • - -
    ops-setting-notice-email-selected
    -
    &#xe731;
    + +
    进程管理
    +
    &#xe79c;
  • - -
    ops-setting-notice-email
    -
    &#xe732;
    + +
    elasticsearch
    +
    &#xe79d;
  • - -
    ops-setting-notice-dingding-selected
    -
    &#xe733;
    + +
    ops-setting-role-system
    +
    &#xe78c;
  • - -
    ops-setting-notice-dingding
    -
    &#xe734;
    + +
    ops-setting-role-system-selected
    +
    &#xe78d;
  • - -
    ops-setting-notice-wx-selected
    -
    &#xe735;
    + +
    ops-datainsight-audit
    +
    &#xe780;
  • - -
    ops-setting-notice-wx
    -
    &#xe736;
    + +
    ops-datainsight-audit-selected
    +
    &#xe781;
  • - -
    ops-setting-companyStructure-selected
    -
    &#xe72b;
    + +
    ops-datainsight-dashboard-selected
    +
    &#xe782;
  • - -
    ops-setting-companyStructure
    -
    &#xe72c;
    + +
    ops-datainsight-dashboard
    +
    &#xe783;
  • - -
    ops-setting-companyInfo
    -
    &#xe72d;
    + +
    ops-datainsight-method
    +
    &#xe784;
  • - -
    ops-setting-companyInfo-selected
    -
    &#xe72e;
    + +
    ops-datainsight-method-selected
    +
    &#xe785;
  • - -
    ops-email
    -
    &#xe61a;
    + +
    ops-datainsight-rule-selected
    +
    &#xe786;
  • - -
    ops-history
    -
    &#xe61d;
    + +
    ops-datainsight-rule
    +
    &#xe787;
  • - -
    ops-menu
    -
    &#xe725;
    + +
    ops-datainsight-origin-selected
    +
    &#xe788;
  • - -
    ops-run
    -
    &#xe726;
    + +
    ops-datainsight-origin
    +
    &#xe789;
  • - -
    ops-save
    -
    &#xe727;
    + +
    ops-datainsight-alert
    +
    &#xe78a;
  • - -
    ops-environment
    -
    &#xe728;
    + +
    ops-datainsight-alert-selected
    +
    &#xe78b;
  • - -
    ops-plus
    -
    &#xe729;
    + +
    rule_6
    +
    &#xe77e;
  • - -
    ops-type_setting
    -
    &#xe72a;
    + +
    icon-xianxing-shenji
    +
    &#xe77f;
  • - -
    icon-shell
    -
    &#xe722;
    + +
    rule_3
    +
    &#xe77c;
  • - -
    icon-bat
    -
    &#xe723;
    + +
    rule_5
    +
    &#xe77d;
  • - -
    icon-powershell
    -
    &#xe724;
    + +
    rule_1
    +
    &#xe778;
  • - -
    icon-bat
    -
    &#xe70f;
    + +
    rule_8
    +
    &#xe779;
  • - -
    icon-powershell
    -
    &#xe710;
    + +
    rule_2
    +
    &#xe77a;
  • - -
    icon-shell
    -
    &#xe711;
    + +
    rule_4
    +
    &#xe77b;
  • - -
    icon-Redhat
    -
    &#xe717;
    + +
    level_4
    +
    &#xe774;
  • - -
    icon-Ubuntu
    -
    &#xe71b;
    + +
    level_3
    +
    &#xe775;
  • - -
    icon-在线
    -
    &#xe71c;
    + +
    level_1
    +
    &#xe776;
  • - -
    icon-下线
    -
    &#xe71d;
    + +
    level_2
    +
    &#xe777;
  • - -
    icon-Centos
    -
    &#xe720;
    + +
    系统管理
    +
    &#xe773;
  • - -
    icon-在线
    -
    &#xe712;
    + +
    caise-华为云
    +
    &#xe75f;
  • - -
    icon-Redhat
    -
    &#xe713;
    + +
    caise-负载均衡
    +
    &#xe760;
  • - -
    icon-bat
    -
    &#xe714;
    + +
    caise-交换机
    +
    &#xe761;
  • - -
    icon-shell
    -
    &#xe715;
    + +
    caise-路由器
    +
    &#xe762;
  • - -
    icon-下线
    -
    &#xe716;
    + +
    caise-部门
    +
    &#xe763;
  • - -
    icon-Ubuntu
    -
    &#xe718;
    + +
    caise-虚拟机
    +
    &#xe764;
  • - -
    icon-Centos
    -
    &#xe719;
    + +
    caise-应用
    +
    &#xe765;
  • - -
    icon-powershell-copy
    -
    &#xe71a;
    + +
    caise-Nginx
    +
    &#xe766;
  • - -
    icon-在线
    -
    &#xe70b;
    + +
    caise-腾讯云
    +
    &#xe767;
  • - -
    icon-Ubuntu
    -
    &#xe70c;
    + +
    caise-产品
    +
    &#xe768;
  • - -
    icon-下线
    -
    &#xe70d;
    + +
    caise-防火墙
    +
    &#xe769;
  • - -
    icon-centos
    -
    &#xe70e;
    + +
    caise-docker
    +
    &#xe76a;
  • - -
    icon-redhat
    -
    &#xe70a;
    + +
    caise-硬盘
    +
    &#xe76b;
  • - -
    icon-实数
    -
    &#xe705;
    + +
    caise-物理机
    +
    &#xe76c;
  • - -
    icon-文本
    -
    &#xe706;
    + +
    caise-网卡
    +
    &#xe76d;
  • - -
    icon-json
    -
    &#xe707;
    + +
    caise-内存
    +
    &#xe76e;
  • - -
    icon-datetime
    -
    &#xe708;
    + +
    caise-阿里云
    +
    &#xe76f;
  • - -
    icon-浮点数
    -
    &#xe709;
    + +
    caise-Apache
    +
    &#xe770;
  • - -
    icon-time
    -
    &#xe703;
    + +
    caise-redis
    +
    &#xe771;
  • - -
    icon-date
    -
    &#xe704;
    + +
    caise-Tomcat
    +
    &#xe772;
  • - -
    icon-浮点数
    -
    &#xe6fc;
    + +
    icon-防火墙
    +
    &#xe746;
  • - -
    icon-json
    -
    &#xe6fd;
    + +
    icon-防火墙
    +
    &#xe750;
  • - -
    icon-time
    -
    &#xe6fe;
    + +
    icon-交换机
    +
    &#xe74b;
  • - -
    icon-文本
    -
    &#xe6ff;
    + +
    icon-负载均衡
    +
    &#xe74c;
  • - -
    icon-date
    -
    &#xe700;
    + +
    icon-内存
    +
    &#xe74d;
  • - -
    icon-datetime
    -
    &#xe701;
    + +
    icon-物理机
    +
    &#xe74e;
  • - -
    icon-实数
    -
    &#xe702;
    + +
    icon-路由器
    +
    &#xe74f;
  • - -
    icon-time
    -
    &#xe6f5;
    + +
    icon-硬盘
    +
    &#xe751;
  • - -
    icon-date
    -
    &#xe6f6;
    -
  • + +
    icon-产品
    +
    &#xe752;
    +
  • - -
    icon-浮点数
    -
    &#xe6f7;
    + +
    icon-应用
    +
    &#xe753;
  • - -
    icon-文本
    -
    &#xe6f8;
    + +
    icon-Nginx
    +
    &#xe754;
  • - -
    icon-实数
    -
    &#xe6f9;
    + +
    icon-docker
    +
    &#xe755;
  • - -
    icon-datetime
    -
    &#xe6fa;
    + +
    icon-网卡
    +
    &#xe756;
  • - -
    icon-json
    -
    &#xe6fb;
    + +
    icon-Apache
    +
    &#xe757;
  • - -
    ops-is_choice-disabled
    -
    &#xe611;
    + +
    icon-redis
    +
    &#xe758;
  • - -
    ops-is_password-disabled
    -
    &#xe612;
    + +
    icon-Tomcat
    +
    &#xe759;
  • - -
    ops-is_index-disabled
    -
    &#xe613;
    + +
    icon-虚拟机
    +
    &#xe75a;
  • - -
    ops-is_sortable-disabled
    -
    &#xe614;
    + +
    icon-部门
    +
    &#xe75b;
  • - -
    ops-is_unique-disabled
    -
    &#xe617;
    + +
    icon-华为云
    +
    &#xe75c;
  • - -
    ops-is_link-disabled
    -
    &#xe619;
    + +
    icon-腾讯云
    +
    &#xe75d;
  • - -
    ops-trigger
    -
    &#xe607;
    + +
    icon-阿里云
    +
    &#xe75e;
  • - -
    ops-default_show-disabled
    -
    &#xe610;
    + +
    icon-部门
    +
    &#xe668;
  • - -
    icon-添加
    -
    &#xe6eb;
    + +
    icon-负载均衡
    +
    &#xe669;
  • - -
    icon-说明
    -
    &#xe6ec;
    + +
    icon-交换机
    +
    &#xe739;
  • - -
    icon-暂停
    -
    &#xe6ed;
    + +
    icon-产品
    +
    &#xe73a;
  • - -
    icon-确认
    -
    &#xe6ee;
    + +
    icon-华为云
    +
    &#xe73b;
  • - -
    icon-警告
    -
    &#xe6ef;
    + +
    icon-物理机
    +
    &#xe73c;
  • - -
    icon-减少
    -
    &#xe6f0;
    + +
    icon-应用
    +
    &#xe73d;
  • - -
    icon-处理中
    -
    &#xe6f1;
    + +
    icon-路由器
    +
    &#xe73e;
  • - -
    icon-取消
    -
    &#xe6f2;
    + +
    icon-网卡
    +
    &#xe73f;
  • - -
    icon-疑问
    -
    &#xe6f3;
    + +
    icon-Nginx
    +
    &#xe740;
  • - -
    icon-禁止
    -
    &#xe6f4;
    + +
    icon-阿里云
    +
    &#xe741;
  • - -
    icon-收藏
    -
    &#xe6d6;
    + +
    icon-虚拟机
    +
    &#xe742;
  • - -
    icon-维修中
    -
    &#xe6d7;
    + +
    icon-硬盘
    +
    &#xe743;
  • - -
    icon-未认证
    -
    &#xe6d8;
    + +
    icon-Apache
    +
    &#xe744;
  • - -
    icon-二维码
    -
    &#xe6d9;
    + +
    icon-docker
    +
    &#xe745;
  • - -
    icon-用户
    -
    &#xe6da;
    + +
    icon-redis
    +
    &#xe747;
  • - -
    icon-报警
    -
    &#xe6db;
    + +
    icon-内存
    +
    &#xe748;
  • - -
    icon-添加用户
    -
    &#xe6dc;
    + +
    icon-Tomcat
    +
    &#xe749;
  • - -
    icon-云数据
    -
    &#xe6dd;
    + +
    icon-腾讯云
    +
    &#xe74a;
  • - -
    icon-消息
    -
    &#xe6de;
    + +
    ops-dot-copy
    +
    &#xe7a7;
  • - -
    icon-云上传
    -
    &#xe6df;
    + +
    ops-review
    +
    &#xe737;
  • - -
    icon-观看
    -
    &#xe6e0;
    + +
    ops-dot
    +
    &#xe738;
  • - -
    icon-定位
    -
    &#xe6e1;
    + +
    ops-setting-notice-email-selected
    +
    &#xe889;
  • - -
    icon-已认证
    -
    &#xe6e2;
    + +
    ops-setting-notice
    +
    &#xe72f;
  • - -
    icon-已连接
    -
    &#xe6e3;
    + +
    ops-setting-notice-selected
    +
    &#xe730;
  • - -
    icon-云下载
    -
    &#xe6e4;
    + +
    ops-setting-notice-email-selected
    +
    &#xe731;
  • - -
    icon-禁止观看
    -
    &#xe6e5;
    + +
    ops-setting-notice-email
    +
    &#xe732;
  • - -
    icon-标签
    -
    &#xe6e6;
    + +
    ops-setting-notice-dingding-selected
    +
    &#xe733;
  • - -
    icon-用户切换
    -
    &#xe6e7;
    + +
    ops-setting-notice-dingding
    +
    &#xe734;
  • - -
    icon-删除用户
    -
    &#xe6e8;
    + +
    ops-setting-notice-wx-selected
    +
    &#xe735;
  • - -
    icon-分支
    -
    &#xe6e9;
    + +
    ops-setting-notice-wx
    +
    &#xe736;
  • - -
    icon-连接断开
    -
    &#xe6ea;
    + +
    ops-setting-companyStructure-selected
    +
    &#xe72b;
  • - -
    icon-Mac
    -
    &#xe6c3;
    + +
    ops-setting-companyStructure
    +
    &#xe72c;
  • - -
    icon-Oracle
    -
    &#xe6c4;
    + +
    ops-setting-companyInfo
    +
    &#xe72d;
  • - -
    icon-Java
    -
    &#xe6c5;
    + +
    ops-setting-companyInfo-selected
    +
    &#xe72e;
  • - -
    icon-Unix
    -
    &#xe6c6;
    + +
    ops-email
    +
    &#xe61a;
  • - -
    icon-python
    -
    &#xe6c7;
    + +
    ops-history
    +
    &#xe61d;
  • - -
    icon-PHP
    -
    &#xe6c8;
    + +
    ops-menu
    +
    &#xe725;
  • - -
    icon-Sybase
    -
    &#xe6c9;
    + +
    ops-run
    +
    &#xe726;
  • - -
    icon-swift
    -
    &#xe6ca;
    + +
    ops-save
    +
    &#xe727;
  • - -
    icon-mySQL
    -
    &#xe6cb;
    + +
    ops-environment
    +
    &#xe728;
  • - -
    icon-c++
    -
    &#xe6cc;
    + +
    ops-plus
    +
    &#xe729;
  • - -
    icon-Informix
    -
    &#xe6cd;
    + +
    ops-type_setting
    +
    &#xe72a;
  • - -
    icon-access
    -
    &#xe6ce;
    + +
    icon-shell
    +
    &#xe722;
  • - -
    icon-mongodb
    -
    &#xe6cf;
    + +
    icon-bat
    +
    &#xe723;
  • - -
    icon-PostgreSQL
    -
    &#xe6d0;
    + +
    icon-powershell
    +
    &#xe724;
  • - -
    icon-SQL Server
    -
    &#xe6d1;
    + +
    icon-bat
    +
    &#xe70f;
  • - -
    icon-c#
    -
    &#xe6d2;
    + +
    icon-powershell
    +
    &#xe710;
  • - -
    icon-Linux
    -
    &#xe6d3;
    + +
    icon-shell
    +
    &#xe711;
  • - -
    icon-DB2
    -
    &#xe6d4;
    + +
    icon-Redhat
    +
    &#xe717;
  • - -
    icon-Windows
    -
    &#xe6d5;
    + +
    icon-Ubuntu
    +
    &#xe71b;
  • - -
    icon-面积图
    -
    &#xe6b3;
    + +
    icon-在线
    +
    &#xe71c;
  • - -
    icon-瀑布图
    -
    &#xe6b4;
    + +
    icon-下线
    +
    &#xe71d;
  • - -
    icon-条形图
    -
    &#xe6b5;
    + +
    icon-Centos
    +
    &#xe720;
  • - -
    icon-箱型图
    -
    &#xe6b6;
    + +
    icon-在线
    +
    &#xe712;
  • - -
    icon-上升
    -
    &#xe6b7;
    + +
    icon-Redhat
    +
    &#xe713;
  • - -
    icon-下降
    -
    &#xe6b8;
    + +
    icon-bat
    +
    &#xe714;
  • - -
    icon-柱状图
    -
    &#xe6b9;
    + +
    icon-shell
    +
    &#xe715;
  • - -
    icon-基金图
    -
    &#xe6ba;
    + +
    icon-下线
    +
    &#xe716;
  • - -
    icon-点状图
    -
    &#xe6bb;
    + +
    icon-Ubuntu
    +
    &#xe718;
  • - -
    icon-热度图
    -
    &#xe6bc;
    + +
    icon-Centos
    +
    &#xe719;
  • - -
    icon-饼状图
    -
    &#xe6bd;
    + +
    icon-powershell-copy
    +
    &#xe71a;
  • - -
    icon-雷达图
    -
    &#xe6be;
    + +
    icon-在线
    +
    &#xe70b;
  • - -
    icon-滑块图
    -
    &#xe6bf;
    + +
    icon-Ubuntu
    +
    &#xe70c;
  • - -
    icon-环形图
    -
    &#xe6c0;
    + +
    icon-下线
    +
    &#xe70d;
  • - -
    icon-折线图
    -
    &#xe6c1;
    + +
    icon-centos
    +
    &#xe70e;
  • - -
    icon-股票图
    -
    &#xe6c2;
    + +
    icon-redhat
    +
    &#xe70a;
  • - -
    icon-Mac
    -
    &#xe6a9;
    + +
    icon-实数
    +
    &#xe705;
  • - -
    icon-unix
    -
    &#xe6aa;
    + +
    icon-文本
    +
    &#xe706;
  • - -
    icon-Windows
    -
    &#xe6ab;
    + +
    icon-json
    +
    &#xe707;
  • - -
    icon-SWIFT
    -
    &#xe6ac;
    + +
    icon-datetime
    +
    &#xe708;
  • - -
    icon-php
    -
    &#xe6ad;
    + +
    icon-浮点数
    +
    &#xe709;
  • - -
    icon-Java
    -
    &#xe6ae;
    + +
    icon-time
    +
    &#xe703;
  • - -
    icon-python
    -
    &#xe6af;
    + +
    icon-date
    +
    &#xe704;
  • - -
    icon-c++
    -
    &#xe6b0;
    + +
    icon-浮点数
    +
    &#xe6fc;
  • - -
    icon-c#
    -
    &#xe6b1;
    + +
    icon-json
    +
    &#xe6fd;
  • - -
    icon-Linux
    -
    &#xe6b2;
    + +
    icon-time
    +
    &#xe6fe;
  • - -
    icon-mongodb
    -
    &#xe6a0;
    + +
    icon-文本
    +
    &#xe6ff;
  • - -
    icon-informix
    -
    &#xe6a1;
    + +
    icon-date
    +
    &#xe700;
  • - -
    icon-Oracle
    -
    &#xe6a2;
    + +
    icon-datetime
    +
    &#xe701;
  • - -
    icon-Sybase
    -
    &#xe6a3;
    + +
    icon-实数
    +
    &#xe702;
  • - -
    icon-access
    -
    &#xe6a4;
    + +
    icon-time
    +
    &#xe6f5;
  • - -
    icon-PostgreSQL
    -
    &#xe6a5;
    + +
    icon-date
    +
    &#xe6f6;
  • - -
    icon-mySQL
    -
    &#xe6a6;
    + +
    icon-浮点数
    +
    &#xe6f7;
  • - -
    icon-DB2
    -
    &#xe6a7;
    + +
    icon-文本
    +
    &#xe6f8;
  • - -
    icon-SQL Server
    -
    &#xe6a8;
    + +
    icon-实数
    +
    &#xe6f9;
  • - -
    icon-维修中
    -
    &#xe68b;
    + +
    icon-datetime
    +
    &#xe6fa;
  • - -
    icon-已连接
    -
    &#xe68c;
    + +
    icon-json
    +
    &#xe6fb;
  • - -
    icon-已认证
    -
    &#xe68d;
    + +
    ops-is_choice-disabled
    +
    &#xe611;
  • - -
    icon-用户切换
    -
    &#xe68e;
    + +
    ops-is_password-disabled
    +
    &#xe612;
  • - -
    icon-云下载
    -
    &#xe68f;
    + +
    ops-is_index-disabled
    +
    &#xe613;
  • - -
    icon-云数据
    -
    &#xe690;
    + +
    ops-is_sortable-disabled
    +
    &#xe614;
  • - -
    icon-云上传
    -
    &#xe691;
    + +
    ops-is_unique-disabled
    +
    &#xe617;
  • - -
    icon-标签
    -
    &#xe692;
    + +
    ops-is_link-disabled
    +
    &#xe619;
  • - -
    icon-二维码
    -
    &#xe693;
    + +
    ops-trigger
    +
    &#xe607;
  • - -
    icon-消息
    -
    &#xe694;
    + +
    ops-default_show-disabled
    +
    &#xe610;
  • - -
    icon-用户
    -
    &#xe695;
    + +
    icon-添加
    +
    &#xe6eb;
  • - -
    icon-定位
    -
    &#xe696;
    + +
    icon-说明
    +
    &#xe6ec;
  • - -
    icon-分支
    -
    &#xe697;
    + +
    icon-暂停
    +
    &#xe6ed;
  • - -
    icon-报警
    -
    &#xe698;
    + +
    icon-确认
    +
    &#xe6ee;
  • - -
    icon-禁止观看
    -
    &#xe699;
    + +
    icon-警告
    +
    &#xe6ef;
  • - -
    icon-观看
    -
    &#xe69a;
    + +
    icon-减少
    +
    &#xe6f0;
  • - -
    icon-添加用户
    -
    &#xe69b;
    + +
    icon-处理中
    +
    &#xe6f1;
  • - -
    icon-删除用户
    -
    &#xe69c;
    + +
    icon-取消
    +
    &#xe6f2;
  • - -
    icon-收藏
    -
    &#xe69d;
    + +
    icon-疑问
    +
    &#xe6f3;
  • - -
    icon-未认证
    -
    &#xe69e;
    + +
    icon-禁止
    +
    &#xe6f4;
  • - -
    icon-断开连接
    -
    &#xe69f;
    + +
    icon-收藏
    +
    &#xe6d6;
  • - -
    icon-滑块图
    -
    &#xe64c;
    + +
    icon-维修中
    +
    &#xe6d7;
  • - -
    icon-折现图
    -
    &#xe64d;
    + +
    icon-未认证
    +
    &#xe6d8;
  • - -
    icon-面积图
    -
    &#xe64e;
    + +
    icon-二维码
    +
    &#xe6d9;
  • - -
    icon-雷达图
    -
    &#xe64f;
    + +
    icon-用户
    +
    &#xe6da;
  • - -
    icon-基金图
    -
    &#xe650;
    + +
    icon-报警
    +
    &#xe6db;
  • - -
    icon-环形图
    -
    &#xe651;
    + +
    icon-添加用户
    +
    &#xe6dc;
  • - -
    icon-箱型图
    -
    &#xe652;
    + +
    icon-云数据
    +
    &#xe6dd;
  • - -
    icon-条形图
    -
    &#xe653;
    + +
    icon-消息
    +
    &#xe6de;
  • - -
    icon-热度图
    -
    &#xe654;
    -
  • + +
    icon-云上传
    +
    &#xe6df;
    +
  • - -
    icon-柱状图
    -
    &#xe684;
    + +
    icon-观看
    +
    &#xe6e0;
  • - -
    icon-下降
    -
    &#xe685;
    + +
    icon-定位
    +
    &#xe6e1;
  • - -
    icon-上升
    -
    &#xe686;
    + +
    icon-已认证
    +
    &#xe6e2;
  • - -
    icon-股票图
    -
    &#xe687;
    + +
    icon-已连接
    +
    &#xe6e3;
  • - -
    icon-瀑布图
    -
    &#xe688;
    + +
    icon-云下载
    +
    &#xe6e4;
  • - -
    icon-点状图
    -
    &#xe689;
    + +
    icon-禁止观看
    +
    &#xe6e5;
  • - -
    icon-饼状图
    -
    &#xe68a;
    + +
    icon-标签
    +
    &#xe6e6;
  • - -
    icon-确认
    -
    &#xe642;
    + +
    icon-用户切换
    +
    &#xe6e7;
  • - -
    icon-说明
    -
    &#xe643;
    + +
    icon-删除用户
    +
    &#xe6e8;
  • - -
    icon-取消
    -
    &#xe644;
    + +
    icon-分支
    +
    &#xe6e9;
  • - -
    icon-禁止
    -
    &#xe645;
    + +
    icon-连接断开
    +
    &#xe6ea;
  • - -
    icon-暂停
    -
    &#xe646;
    + +
    icon-Mac
    +
    &#xe6c3;
  • - -
    icon-减少
    -
    &#xe647;
    + +
    icon-Oracle
    +
    &#xe6c4;
  • - -
    icon-疑问
    -
    &#xe648;
    + +
    icon-Java
    +
    &#xe6c5;
  • - -
    icon-警告
    -
    &#xe649;
    + +
    icon-Unix
    +
    &#xe6c6;
  • - -
    icon-处理中
    -
    &#xe64a;
    + +
    icon-python
    +
    &#xe6c7;
  • - -
    icon-添加
    -
    &#xe64b;
    + +
    icon-PHP
    +
    &#xe6c8;
  • - -
    icon-access
    -
    &#xe675;
    + +
    icon-Sybase
    +
    &#xe6c9;
  • - -
    icon-Java
    -
    &#xe676;
    + +
    icon-swift
    +
    &#xe6ca;
  • - -
    icon-unix
    -
    &#xe677;
    + +
    icon-mySQL
    +
    &#xe6cb;
  • - -
    icon-SWIFT
    -
    &#xe678;
    + +
    icon-c++
    +
    &#xe6cc;
  • - -
    icon-Mac
    -
    &#xe679;
    + +
    icon-Informix
    +
    &#xe6cd;
  • - -
    icon-informix
    -
    &#xe67a;
    + +
    icon-access
    +
    &#xe6ce;
  • - -
    icon-c#
    -
    &#xe67b;
    + +
    icon-mongodb
    +
    &#xe6cf;
  • - -
    icon-mySQL
    -
    &#xe67c;
    + +
    icon-PostgreSQL
    +
    &#xe6d0;
  • - -
    icon-Linux
    -
    &#xe67d;
    + +
    icon-SQL Server
    +
    &#xe6d1;
  • - -
    icon-PostgreSQL
    -
    &#xe67e;
    + +
    icon-c#
    +
    &#xe6d2;
  • - -
    icon-Sybase
    -
    &#xe67f;
    + +
    icon-Linux
    +
    &#xe6d3;
  • - +
    icon-DB2
    -
    &#xe680;
    +
    &#xe6d4;
  • - -
    icon-SQL Server
    -
    &#xe681;
    + +
    icon-Windows
    +
    &#xe6d5;
  • - -
    icon-c++
    -
    &#xe682;
    + +
    icon-面积图
    +
    &#xe6b3;
  • - -
    icon-python
    -
    &#xe683;
    + +
    icon-瀑布图
    +
    &#xe6b4;
  • - -
    icon-oracle
    -
    &#xe671;
    + +
    icon-条形图
    +
    &#xe6b5;
  • - -
    icon-php
    -
    &#xe672;
    + +
    icon-箱型图
    +
    &#xe6b6;
  • - -
    icon-Windows
    -
    &#xe673;
    + +
    icon-上升
    +
    &#xe6b7;
  • - -
    icon-mongodb
    -
    &#xe674;
    + +
    icon-下降
    +
    &#xe6b8;
  • - -
    icon-瀑布图
    -
    &#xe632;
    + +
    icon-柱状图
    +
    &#xe6b9;
  • - -
    icon-滑块图
    -
    &#xe633;
    + +
    icon-基金图
    +
    &#xe6ba;
  • - -
    icon-条形图
    -
    &#xe634;
    + +
    icon-点状图
    +
    &#xe6bb;
  • - -
    icon-基金图
    -
    &#xe635;
    + +
    icon-热度图
    +
    &#xe6bc;
  • - -
    icon-下降
    -
    &#xe636;
    + +
    icon-饼状图
    +
    &#xe6bd;
  • - -
    icon-股票图
    -
    &#xe637;
    + +
    icon-雷达图
    +
    &#xe6be;
  • - -
    icon-环形图
    -
    &#xe638;
    + +
    icon-滑块图
    +
    &#xe6bf;
  • - -
    icon-箱型图
    -
    &#xe639;
    + +
    icon-环形图
    +
    &#xe6c0;
  • - -
    icon-柱状图
    -
    &#xe63a;
    + +
    icon-折线图
    +
    &#xe6c1;
  • - -
    icon-饼状图
    -
    &#xe63b;
    + +
    icon-股票图
    +
    &#xe6c2;
  • - -
    icon-折线图
    -
    &#xe63c;
    + +
    icon-Mac
    +
    &#xe6a9;
  • - -
    icon-雷达图
    -
    &#xe63d;
    + +
    icon-unix
    +
    &#xe6aa;
  • - -
    icon-点状图
    -
    &#xe63e;
    + +
    icon-Windows
    +
    &#xe6ab;
  • - -
    icon-上升
    -
    &#xe63f;
    + +
    icon-SWIFT
    +
    &#xe6ac;
  • - -
    icon-面积图
    -
    &#xe640;
    + +
    icon-php
    +
    &#xe6ad;
  • - -
    icon-热度图
    -
    &#xe641;
    + +
    icon-Java
    +
    &#xe6ae;
  • - -
    icon-报警
    -
    &#xe622;
    + +
    icon-python
    +
    &#xe6af;
  • - -
    icon-观看
    -
    &#xe61e;
    + +
    icon-c++
    +
    &#xe6b0;
  • - -
    icon-未认证
    -
    &#xe61f;
    + +
    icon-c#
    +
    &#xe6b1;
  • - -
    icon-删除用户
    -
    &#xe620;
    + +
    icon-Linux
    +
    &#xe6b2;
  • - -
    icon-用户切换
    -
    &#xe621;
    + +
    icon-mongodb
    +
    &#xe6a0;
  • - -
    icon-用户
    -
    &#xe623;
    + +
    icon-informix
    +
    &#xe6a1;
  • - -
    icon-云下载
    -
    &#xe624;
    + +
    icon-Oracle
    +
    &#xe6a2;
  • - -
    icon-云上传
    -
    &#xe625;
    + +
    icon-Sybase
    +
    &#xe6a3;
  • - -
    icon-维修中
    -
    &#xe626;
    + +
    icon-access
    +
    &#xe6a4;
  • - -
    icon-连接断开
    -
    &#xe627;
    + +
    icon-PostgreSQL
    +
    &#xe6a5;
  • - -
    icon-禁止观看
    -
    &#xe628;
    + +
    icon-mySQL
    +
    &#xe6a6;
  • - -
    icon-定位
    -
    &#xe629;
    + +
    icon-DB2
    +
    &#xe6a7;
  • - -
    icon-添加用户
    -
    &#xe62a;
    + +
    icon-SQL Server
    +
    &#xe6a8;
  • - -
    icon-云数据
    -
    &#xe62b;
    + +
    icon-维修中
    +
    &#xe68b;
  • - -
    icon-消息
    -
    &#xe62c;
    + +
    icon-已连接
    +
    &#xe68c;
  • - -
    icon-标签
    -
    &#xe62d;
    + +
    icon-已认证
    +
    &#xe68d;
  • - -
    icon-二维码
    -
    &#xe62e;
    + +
    icon-用户切换
    +
    &#xe68e;
  • - -
    icon-分支
    -
    &#xe62f;
    + +
    icon-云下载
    +
    &#xe68f;
  • - -
    icon-收藏
    -
    &#xe630;
    + +
    icon-云数据
    +
    &#xe690;
  • - -
    icon-已认证
    -
    &#xe631;
    + +
    icon-云上传
    +
    &#xe691;
  • - -
    icon-已连接
    -
    &#xe670;
    + +
    icon-标签
    +
    &#xe692;
  • - -
    icon-减去
    -
    &#xe61b;
    + +
    icon-二维码
    +
    &#xe693;
  • - -
    icon-添加
    -
    &#xe61c;
    + +
    icon-消息
    +
    &#xe694;
  • - -
    icon-禁止
    -
    &#xe655;
    + +
    icon-用户
    +
    &#xe695;
  • - -
    icon-确认
    -
    &#xe656;
    + +
    icon-定位
    +
    &#xe696;
  • - -
    icon-取消
    -
    &#xe658;
    + +
    icon-分支
    +
    &#xe697;
  • - -
    icon-处理中
    -
    &#xe659;
    + +
    icon-报警
    +
    &#xe698;
  • - -
    icon-暂停
    -
    &#xe65a;
    + +
    icon-禁止观看
    +
    &#xe699;
  • - -
    icon-说明
    -
    &#xe65b;
    + +
    icon-观看
    +
    &#xe69a;
  • - -
    icon-警告
    -
    &#xe66c;
    + +
    icon-添加用户
    +
    &#xe69b;
  • - -
    icon-疑问3
    -
    &#xe657;
    + +
    icon-删除用户
    +
    &#xe69c;
  • - -
    ops-dag-dashboard
    -
    &#xe600;
    + +
    icon-收藏
    +
    &#xe69d;
  • - -
    ops-dag-dashboard-selected
    -
    &#xe601;
    + +
    icon-未认证
    +
    &#xe69e;
  • - -
    ops-dag-applet-selected
    -
    &#xe602;
    + +
    icon-断开连接
    +
    &#xe69f;
  • - -
    ops-dag-applet
    -
    &#xe603;
    + +
    icon-滑块图
    +
    &#xe64c;
  • - -
    ops-dag-terminal
    -
    &#xe604;
    + +
    icon-折现图
    +
    &#xe64d;
  • - -
    ops-dag-terminal-selected
    -
    &#xe605;
    + +
    icon-面积图
    +
    &#xe64e;
  • - -
    ops-dag-cron
    -
    &#xe606;
    + +
    icon-雷达图
    +
    &#xe64f;
  • - -
    ops-dag-cron-selected
    -
    &#xe608;
    + +
    icon-基金图
    +
    &#xe650;
  • - -
    ops-dag-history
    -
    &#xe609;
    + +
    icon-环形图
    +
    &#xe651;
  • - -
    ops-dag-history-selected
    -
    &#xe60a;
    + +
    icon-箱型图
    +
    &#xe652;
  • - -
    ops-dag-dags-selected
    -
    &#xe60c;
    + +
    icon-条形图
    +
    &#xe653;
  • - -
    ops-dag-dagreview
    -
    &#xe60d;
    + +
    icon-热度图
    +
    &#xe654;
  • - -
    ops-dag-dagreview-selected
    -
    &#xe60e;
    + +
    icon-柱状图
    +
    &#xe684;
  • - -
    ops-dag-panel
    -
    &#xe60f;
    + +
    icon-下降
    +
    &#xe685;
  • - -
    ops-dag-panel-selected
    -
    &#xe615;
    + +
    icon-上升
    +
    &#xe686;
  • - -
    ops-dag-variables
    -
    &#xe616;
    + +
    icon-股票图
    +
    &#xe687;
  • - -
    ops-dag-variables-selected
    -
    &#xe618;
    + +
    icon-瀑布图
    +
    &#xe688;
  • - -
    ops-dag-appletadmin
    -
    &#xe65c;
    + +
    icon-点状图
    +
    &#xe689;
  • - -
    ops-dag-appletadmin-selected
    -
    &#xe65d;
    + +
    icon-饼状图
    +
    &#xe68a;
  • - -
    ops-dag-dags
    -
    &#xe60b;
    + +
    icon-确认
    +
    &#xe642;
  • - -
    ops-dag-targetroute
    -
    &#xe65f;
    + +
    icon-说明
    +
    &#xe643;
  • - -
    ops-dag-holiday
    -
    &#xe660;
    + +
    icon-取消
    +
    &#xe644;
  • - -
    ops-dag-holiday-selected
    -
    &#xe661;
    + +
    icon-禁止
    +
    &#xe645;
  • - -
    ops-dag-targetshortcut-selected
    -
    &#xe662;
    + +
    icon-暂停
    +
    &#xe646;
  • - -
    ops-dag-targetroute-selected
    -
    &#xe663;
    + +
    icon-减少
    +
    &#xe647;
  • - -
    ops-dag-targetshortcut
    -
    &#xe664;
    + +
    icon-疑问
    +
    &#xe648;
  • - -
    ops-dag-admingroup
    -
    &#xe65e;
    + +
    icon-警告
    +
    &#xe649;
  • - -
    ops-dag-admingroup-selected
    -
    &#xe665;
    + +
    icon-处理中
    +
    &#xe64a;
  • - -
    ops-admin
    -
    &#xe666;
    + +
    icon-添加
    +
    &#xe64b;
  • - -
    applet-untop
    -
    &#xe667;
    + +
    icon-access
    +
    &#xe675;
  • - -
    applet-top
    -
    &#xe66a;
    + +
    icon-Java
    +
    &#xe676;
  • - -
    资源层级
    -
    &#xe66b;
    + +
    icon-unix
    +
    &#xe677;
  • - -
    资源数据
    -
    &#xe66d;
    + +
    icon-SWIFT
    +
    &#xe678;
  • - -
    订阅
    -
    &#xe66e;
    + +
    icon-Mac
    +
    &#xe679;
  • - -
    取消订阅
    -
    &#xe66f;
    + +
    icon-informix
    +
    &#xe67a;
  • - -
    -

    Unicode 引用

    -
    - -

    Unicode 是字体在网页端最原始的应用方式,特点是:

    -
      -
    • 支持按字体的方式去动态调整图标大小,颜色等等。
    • -
    • 默认情况下不支持多色,直接添加多色图标会自动去色。
    • -
    -
    -

    注意:新版 iconfont 支持两种方式引用多色图标:SVG symbol 引用方式和彩色字体图标模式。(使用彩色字体图标需要在「编辑项目」中开启「彩色」选项后并重新生成。)

    -
    -

    Unicode 使用步骤如下:

    -

    第一步:拷贝项目下面生成的 @font-face

    -
    @font-face {
    -  font-family: 'iconfont';
    -  src: url('iconfont.woff2?t=1688550067963') format('woff2'),
    -       url('iconfont.woff?t=1688550067963') format('woff'),
    -       url('iconfont.ttf?t=1688550067963') format('truetype');
    -}
    -
    -

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

    -
    .iconfont {
    -  font-family: "iconfont" !important;
    -  font-size: 16px;
    -  font-style: normal;
    -  -webkit-font-smoothing: antialiased;
    -  -moz-osx-font-smoothing: grayscale;
    -}
    -
    -

    第三步:挑选相应图标并获取字体编码,应用于页面

    -
    -<span class="iconfont">&#x33;</span>
    -
    -
    -

    "iconfont" 是你项目下的 font-family。可以通过编辑项目查看,默认是 "iconfont"。

    -
    -
    - -
    -
      - -
    • - -
      - rule_100 -
      -
      .rule_100 -
      -
    • +
    • + +
      icon-c#
      +
      &#xe67b;
      +
    • -
    • - +
    • + +
      icon-mySQL
      +
      &#xe67c;
      +
    • + +
    • + +
      icon-Linux
      +
      &#xe67d;
      +
    • + +
    • + +
      icon-PostgreSQL
      +
      &#xe67e;
      +
    • + +
    • + +
      icon-Sybase
      +
      &#xe67f;
      +
    • + +
    • + +
      icon-DB2
      +
      &#xe680;
      +
    • + +
    • + +
      icon-SQL Server
      +
      &#xe681;
      +
    • + +
    • + +
      icon-c++
      +
      &#xe682;
      +
    • + +
    • + +
      icon-python
      +
      &#xe683;
      +
    • + +
    • + +
      icon-oracle
      +
      &#xe671;
      +
    • + +
    • + +
      icon-php
      +
      &#xe672;
      +
    • + +
    • + +
      icon-Windows
      +
      &#xe673;
      +
    • + +
    • + +
      icon-mongodb
      +
      &#xe674;
      +
    • + +
    • + +
      icon-瀑布图
      +
      &#xe632;
      +
    • + +
    • + +
      icon-滑块图
      +
      &#xe633;
      +
    • + +
    • + +
      icon-条形图
      +
      &#xe634;
      +
    • + +
    • + +
      icon-基金图
      +
      &#xe635;
      +
    • + +
    • + +
      icon-下降
      +
      &#xe636;
      +
    • + +
    • + +
      icon-股票图
      +
      &#xe637;
      +
    • + +
    • + +
      icon-环形图
      +
      &#xe638;
      +
    • + +
    • + +
      icon-箱型图
      +
      &#xe639;
      +
    • + +
    • + +
      icon-柱状图
      +
      &#xe63a;
      +
    • + +
    • + +
      icon-饼状图
      +
      &#xe63b;
      +
    • + +
    • + +
      icon-折线图
      +
      &#xe63c;
      +
    • + +
    • + +
      icon-雷达图
      +
      &#xe63d;
      +
    • + +
    • + +
      icon-点状图
      +
      &#xe63e;
      +
    • + +
    • + +
      icon-上升
      +
      &#xe63f;
      +
    • + +
    • + +
      icon-面积图
      +
      &#xe640;
      +
    • + +
    • + +
      icon-热度图
      +
      &#xe641;
      +
    • + +
    • + +
      icon-报警
      +
      &#xe622;
      +
    • + +
    • + +
      icon-观看
      +
      &#xe61e;
      +
    • + +
    • + +
      icon-未认证
      +
      &#xe61f;
      +
    • + +
    • + +
      icon-删除用户
      +
      &#xe620;
      +
    • + +
    • + +
      icon-用户切换
      +
      &#xe621;
      +
    • + +
    • + +
      icon-用户
      +
      &#xe623;
      +
    • + +
    • + +
      icon-云下载
      +
      &#xe624;
      +
    • + +
    • + +
      icon-云上传
      +
      &#xe625;
      +
    • + +
    • + +
      icon-维修中
      +
      &#xe626;
      +
    • + +
    • + +
      icon-连接断开
      +
      &#xe627;
      +
    • + +
    • + +
      icon-禁止观看
      +
      &#xe628;
      +
    • + +
    • + +
      icon-定位
      +
      &#xe629;
      +
    • + +
    • + +
      icon-添加用户
      +
      &#xe62a;
      +
    • + +
    • + +
      icon-云数据
      +
      &#xe62b;
      +
    • + +
    • + +
      icon-消息
      +
      &#xe62c;
      +
    • + +
    • + +
      icon-标签
      +
      &#xe62d;
      +
    • + +
    • + +
      icon-二维码
      +
      &#xe62e;
      +
    • + +
    • + +
      icon-分支
      +
      &#xe62f;
      +
    • + +
    • + +
      icon-收藏
      +
      &#xe630;
      +
    • + +
    • + +
      icon-已认证
      +
      &#xe631;
      +
    • + +
    • + +
      icon-已连接
      +
      &#xe670;
      +
    • + +
    • + +
      icon-减去
      +
      &#xe61b;
      +
    • + +
    • + +
      icon-添加
      +
      &#xe61c;
      +
    • + +
    • + +
      icon-禁止
      +
      &#xe655;
      +
    • + +
    • + +
      icon-确认
      +
      &#xe656;
      +
    • + +
    • + +
      icon-取消
      +
      &#xe658;
      +
    • + +
    • + +
      icon-处理中
      +
      &#xe659;
      +
    • + +
    • + +
      icon-暂停
      +
      &#xe65a;
      +
    • + +
    • + +
      icon-说明
      +
      &#xe65b;
      +
    • + +
    • + +
      icon-警告
      +
      &#xe66c;
      +
    • + +
    • + +
      icon-疑问3
      +
      &#xe657;
      +
    • + +
    • + +
      ops-dag-dashboard
      +
      &#xe600;
      +
    • + +
    • + +
      ops-dag-dashboard-selected
      +
      &#xe601;
      +
    • + +
    • + +
      ops-dag-applet-selected
      +
      &#xe602;
      +
    • + +
    • + +
      ops-dag-applet
      +
      &#xe603;
      +
    • + +
    • + +
      ops-dag-terminal
      +
      &#xe604;
      +
    • + +
    • + +
      ops-dag-terminal-selected
      +
      &#xe605;
      +
    • + +
    • + +
      ops-dag-cron
      +
      &#xe606;
      +
    • + +
    • + +
      ops-dag-cron-selected
      +
      &#xe608;
      +
    • + +
    • + +
      ops-dag-history
      +
      &#xe609;
      +
    • + +
    • + +
      ops-dag-history-selected
      +
      &#xe60a;
      +
    • + +
    • + +
      ops-dag-dags-selected
      +
      &#xe60c;
      +
    • + +
    • + +
      ops-dag-dagreview
      +
      &#xe60d;
      +
    • + +
    • + +
      ops-dag-dagreview-selected
      +
      &#xe60e;
      +
    • + +
    • + +
      ops-dag-panel
      +
      &#xe60f;
      +
    • + +
    • + +
      ops-dag-panel-selected
      +
      &#xe615;
      +
    • + +
    • + +
      ops-dag-variables
      +
      &#xe616;
      +
    • + +
    • + +
      ops-dag-variables-selected
      +
      &#xe618;
      +
    • + +
    • + +
      ops-dag-appletadmin
      +
      &#xe65c;
      +
    • + +
    • + +
      ops-dag-appletadmin-selected
      +
      &#xe65d;
      +
    • + +
    • + +
      ops-dag-dags
      +
      &#xe60b;
      +
    • + +
    • + +
      ops-dag-targetroute
      +
      &#xe65f;
      +
    • + +
    • + +
      ops-dag-holiday
      +
      &#xe660;
      +
    • + +
    • + +
      ops-dag-holiday-selected
      +
      &#xe661;
      +
    • + +
    • + +
      ops-dag-targetshortcut-selected
      +
      &#xe662;
      +
    • + +
    • + +
      ops-dag-targetroute-selected
      +
      &#xe663;
      +
    • + +
    • + +
      ops-dag-targetshortcut
      +
      &#xe664;
      +
    • + +
    • + +
      ops-dag-admingroup
      +
      &#xe65e;
      +
    • + +
    • + +
      ops-dag-admingroup-selected
      +
      &#xe665;
      +
    • + +
    • + +
      ops-admin
      +
      &#xe666;
      +
    • + +
    • + +
      applet-untop
      +
      &#xe667;
      +
    • + +
    • + +
      applet-top
      +
      &#xe66a;
      +
    • + +
    • + +
      资源层级
      +
      &#xe66b;
      +
    • + +
    • + +
      资源数据
      +
      &#xe66d;
      +
    • + +
    • + +
      订阅
      +
      &#xe66e;
      +
    • + +
    • + +
      取消订阅
      +
      &#xe66f;
      +
    • + +
    +
    +

    Unicode 引用

    +
    + +

    Unicode 是字体在网页端最原始的应用方式,特点是:

    +
      +
    • 支持按字体的方式去动态调整图标大小,颜色等等。
    • +
    • 默认情况下不支持多色,直接添加多色图标会自动去色。
    • +
    +
    +

    注意:新版 iconfont 支持两种方式引用多色图标:SVG symbol 引用方式和彩色字体图标模式。(使用彩色字体图标需要在「编辑项目」中开启「彩色」选项后并重新生成。)

    +
    +

    Unicode 使用步骤如下:

    +

    第一步:拷贝项目下面生成的 @font-face

    +
    @font-face {
    +  font-family: 'iconfont';
    +  src: url('iconfont.woff2?t=1702544951995') format('woff2'),
    +       url('iconfont.woff?t=1702544951995') format('woff'),
    +       url('iconfont.ttf?t=1702544951995') format('truetype');
    +}
    +
    +

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

    +
    .iconfont {
    +  font-family: "iconfont" !important;
    +  font-size: 16px;
    +  font-style: normal;
    +  -webkit-font-smoothing: antialiased;
    +  -moz-osx-font-smoothing: grayscale;
    +}
    +
    +

    第三步:挑选相应图标并获取字体编码,应用于页面

    +
    +<span class="iconfont">&#x33;</span>
    +
    +
    +

    "iconfont" 是你项目下的 font-family。可以通过编辑项目查看,默认是 "iconfont"。

    +
    +
    +
    +
    +
      + +
    • + +
      + OAuth2.0 +
      +
      .OAUTH2 +
      +
    • + +
    • + +
      + OIDC +
      +
      .OIDC +
      +
    • + +
    • + +
      + cas +
      +
      .CAS +
      +
    • + +
    • + +
      + setting-authentication +
      +
      .ops-setting-auth +
      +
    • + +
    • + +
      + setting-authentication-selected +
      +
      .ops-setting-auth-selected +
      +
    • + +
    • + +
      + itsm-knowledge (2) +
      +
      .a-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 +
      +
    • + +
    • + +
      + icon-xianxing-password +
      +
      .icon-xianxing-password +
      +
    • + +
    • + +
      + icon-xianxing-link +
      +
      .icon-xianxing-link +
      +
    • + +
    • + +
      + itsm-oneclick download +
      +
      .itsm-download-all +
      +
    • + +
    • + +
      + itsm-package download +
      +
      .itsm-download-package +
      +
    • + +
    • + +
      + weixin +
      +
      .a-Frame4 +
      +
    • + +
    • + +
      + itsm-again +
      +
      .itsm-again +
      +
    • + +
    • + +
      + itsm-next +
      +
      .itsm-next +
      +
    • + +
    • + +
      + wechatApp +
      +
      .wechatApp +
      +
    • + +
    • + +
      + robot +
      +
      .robot +
      +
    • + +
    • + +
      + feishuApp +
      +
      .feishuApp +
      +
    • + +
    • + +
      + dingdingApp +
      +
      .dingdingApp +
      +
    • + +
    • + +
      + email +
      +
      .email +
      +
    • + +
    • + +
      + setting-feishu +
      +
      .ops-setting-notice-feishu +
      +
    • + +
    • + +
      + setting-feishu-selected +
      +
      .ops-setting-notice-feishu-selected +
      +
    • + +
    • + +
      + cmdb-histogram +
      +
      .cmdb-bar +
      +
    • + +
    • + +
      + cmdb-index +
      +
      .cmdb-count +
      +
    • + +
    • + +
      + cmdb-piechart +
      +
      .cmdb-pie +
      +
    • + +
    • + +
      + cmdb-line +
      +
      .cmdb-line +
      +
    • + +
    • + +
      + cmdb-table +
      +
      .cmdb-table +
      +
    • + +
    • + +
      + itsm-all +
      +
      .itsm-all +
      +
    • + +
    • + +
      + itsm-reply +
      +
      .itsm-reply +
      +
    • + +
    • + +
      + itsm-information +
      +
      .itsm-information +
      +
    • + +
    • + +
      + itsm-contact +
      +
      .itsm-contact +
      +
    • + +
    • + +
      + itsm-my-processed +
      +
      .itsm-my-my_already_handle +
      +
    • + +
    • + +
      + rule_7 +
      +
      .rule_7 +
      +
    • + +
    • + +
      + itsm-my-completed +
      +
      .itsm-my-completed +
      +
    • + +
    • + +
      + itsm-my-plan +
      +
      .itsm-my-plan +
      +
    • + +
    • + +
      + rule_100 +
      +
      .rule_100 +
      +
    • + +
    • + +
      + itsm-flag +
      +
      .itsm-flag +
      +
    • + +
    • + +
      + itsm-recommend +
      +
      .itsm-recommend +
      +
    • + +
    • + +
      + ops-help +
      +
      .ops-help +
      +
    • + +
    • + +
      + ops-help-hover +
      +
      .ops-help-hover +
      +
    • + +
    • + +
      + itsm-knowledge-pending_examine +
      +
      .itsm-knowledge-pending_examine +
      +
    • + +
    • + +
      + itsm-knowledge-published +
      +
      .itsm-knowledge-published +
      +
    • + +
    • + +
      + itsm-knowledge-submitted +
      +
      .itsm-knowledge-submitted +
      +
    • + +
    • + +
      + itsm-knowledge-deleted +
      +
      .itsm-knowledge-deleted +
      +
    • + +
    • + +
      + itsm-knowledge +
      +
      .itsm-knowledge +
      +
    • + +
    • + +
      + ops-itsm-ticketsetting-selected +
      +
      .ops-itsm-ticketsetting-selected +
      +
    • + +
    • + +
      + ops-itsm-reports-selected +
      +
      .ops-itsm-reports-selected +
      +
    • + +
    • + +
      + ops-itsm-servicecatalog-selected +
      +
      .ops-itsm-servicecatalog-selected +
      +
    • + +
    • + +
      + ops-itsm-ticketmanage-selected +
      +
      .ops-itsm-ticketmanage-selected +
      +
    • + +
    • + +
      + ops-itsm-knowledge-selected +
      +
      .ops-itsm-knowledge-selected +
      +
    • + +
    • + +
      + ops-itsm-workstation-selected +
      +
      .ops-itsm-workstation-selected +
      +
    • + +
    • + +
      + ops-itsm-servicedesk-selected +
      +
      .ops-itsm-servicedesk-selected +
      +
    • + +
    • + +
      + ops-itsm-planticket-selected +
      +
      .ops-itsm-planticket-selected +
      +
    • + +
    • + +
      + ops-itsm-servicecatalog +
      +
      .ops-itsm-servicecatalog +
      +
    • + +
    • + +
      + ops-itsm-ticketmanage +
      +
      .ops-itsm-ticketmanage +
      +
    • + +
    • + +
      + ops-itsm-reports +
      +
      .ops-itsm-reports +
      +
    • + +
    • + +
      + ops-itsm-knowledge +
      +
      .ops-itsm-knowledge +
      +
    • + +
    • + +
      + ops-itsm-planticket +
      +
      .ops-itsm-planticket +
      +
    • + +
    • + +
      + ops-itsm-ticketsetting +
      +
      .ops-itsm-ticketsetting +
      +
    • + +
    • + +
      + ops-itsm-servicedesk +
      +
      .ops-itsm-servicedesk +
      +
    • + +
    • + +
      + ops-itsm-workstation +
      +
      .ops-itsm-workstation +
      +
    • + +
    • + +
      + monitor-webPerf +
      +
      .monitor-webPerf +
      +
    • + +
    • + +
      + monitor-image (1) +
      +
      .a-monitor-image1 +
      +
    • + +
    • + +
      + monitor-other +
      +
      .monitor-other +
      +
    • + +
    • + +
      + monitor-font +
      +
      .monitor-font +
      +
    • + +
    • + +
      + monitor-css +
      +
      .monitor-css +
      +
    • + +
    • + +
      + monitor-html +
      +
      .monitor-html +
      +
    • + +
    • + +
      + monitor-video +
      +
      .monitor-video +
      +
    • + +
    • + +
      + monitor-js +
      +
      .monitor-js +
      +
    • + +
    • + +
      + monitor-audio +
      +
      .monitor-audio +
      +
    • + +
    • + +
      + monitor-text +
      +
      .monitor-text +
      +
    • + +
    • +
      - itsm-flag + monitor-jiancedian
      -
      .itsm-flag +
      .monitor-jiancedian
    • - +
      - itsm-recommend + monitor-zongfenhegexiangpingfen
      -
      .itsm-recommend +
      .monitor-zongfenhegexiangpingfen
    • - +
      - ops-help + monitor-dig
      -
      .ops-help +
      .monitor-dig
    • - + +
      + monitor-dns +
      +
      .monitor-dns +
      +
    • + +
    • + +
      + monitor-traceroute +
      +
      .monitor-traceroute +
      +
    • + +
    • + +
      + monitor-mtr +
      +
      .monitor-mtr +
      +
    • + +
    • + +
      + monitor-websocket +
      +
      .monitor-websocket +
      +
    • + +
    • + +
      + monitor-performance_monitor-copy +
      +
      .webPerf-copy +
      +
    • + +
    • + +
      + yuansuxingneng +
      +
      .yuansuxingneng +
      +
    • + +
    • + +
      + zhujijiankong +
      +
      .zhujijiankong +
      +
    • + +
    • + +
      + zhujiqiang +
      +
      .zhujiqiang +
      +
    • + +
    • + +
      + xingnengpinggu +
      +
      .xingnengpinggu +
      +
    • + +
    • + +
      + wodekanban +
      +
      .wodekanban +
      +
    • + +
    • + +
      + wangzhanjiankong +
      +
      .wangzhanjiankong +
      +
    • + +
    • + +
      + tongji +
      +
      .tongji +
      +
    • + +
    • + +
      + wangyexingneng +
      +
      .wangyexingneng +
      +
    • + +
    • + +
      + wangluotuobu +
      +
      .wangluotuobu +
      +
    • + +
    • + +
      + shishizhuangtai +
      +
      .shishizhuangtai +
      +
    • + +
    • + +
      + gailan +
      +
      .gailan +
      +
    • + +
    • + +
      + zonglan +
      +
      .zonglan +
      +
    • + +
    • + +
      + itsm-workstation-fast +
      +
      .itsm-workstation-fast +
      +
    • + +
    • + +
      + itsm-workstation-handle +
      +
      .itsm-workstation-handle +
      +
    • + +
    • + +
      + itsm-stop_hang_up +
      +
      .itsm-stop_hang_up +
      +
    • + +
    • + +
      + itsm-workstation-overview +
      +
      .itsm-workstation-overview +
      +
    • + +
    • + +
      + itsm-workstation-inform +
      +
      .itsm-workstation-inform +
      +
    • + +
    • + +
      + itsm-workstation-notice +
      +
      .itsm-workstation-notice +
      +
    • + +
    • + +
      + itsm-workstation-todo list +
      +
      .itsm-workstation-todolist +
      +
    • + +
    • + +
      + itsm-workstation-duty +
      +
      .itsm-workstation-duty +
      +
    • + +
    • + +
      + itsm-workstation-initiate +
      +
      .itsm-workstation-initiate +
      +
    • + +
    • +
      - ops-help-hover + itsm-my-my_initiate
      -
      .ops-help-hover +
      .itsm-my-my_initiate
    • - +
      - itsm-knowledge-pending_examine + itsm-my-my_todo-copy
      -
      .itsm-knowledge-pending_examine +
      .itsm-my-my_handle
    • - +
      - itsm-knowledge-published + itsm-my-draft
      -
      .itsm-knowledge-published +
      .itsm-my-draft
    • - +
      - itsm-knowledge-submitted + itsm-my-all
      -
      .itsm-knowledge-submitted +
      .itsm-my-all
    • - +
      - itsm-knowledge-deleted + itsm-my-in_process
      -
      .itsm-knowledge-deleted +
      .itsm-my-in_process
    • - +
      - itsm-knowledge + itsm-my-my_todo
      -
      .itsm-knowledge +
      .itsm-my-my_todo
    • - +
      - ops-itsm-ticketsetting-selected + itsm-my-share
      -
      .ops-itsm-ticketsetting-selected +
      .itsm-my-share
    • - +
      - ops-itsm-reports-selected + itsm-my-pending_claim
      -
      .ops-itsm-reports-selected +
      .itsm-my-pending_claim
    • - +
      - ops-itsm-servicecatalog-selected + itsm-log-ABORTED
      -
      .ops-itsm-servicecatalog-selected +
      .itsm-log-ABORTED
    • - +
      - ops-itsm-ticketmanage-selected + itsm-log-FAILED
      -
      .ops-itsm-ticketmanage-selected +
      .itsm-log-FAILED
    • - +
      - ops-itsm-knowledge-selected + itsm-log-SUCCESS
      -
      .ops-itsm-knowledge-selected +
      .itsm-log-SUCCESS
    • - +
      - ops-itsm-workstation-selected + itsm-log-ERROR
      -
      .ops-itsm-workstation-selected +
      .itsm-log-ERROR
    • - +
      - ops-itsm-servicedesk-selected + itsm-service-type
      -
      .ops-itsm-servicedesk-selected +
      .itsm-service-type
    • - +
      - ops-itsm-planticket-selected + itsm-my
      -
      .ops-itsm-planticket-selected +
      .itsm-my
    • - +
      - ops-itsm-servicecatalog + ops-monitor-hostwall
      -
      .ops-itsm-servicecatalog +
      .ops-monitor-hostwall
    • - +
      - ops-itsm-ticketmanage + ops-monitor-hostwall-setting
      -
      .ops-itsm-ticketmanage +
      .ops-monitor-hostwall-setting
    • - +
      - ops-itsm-reports + 授权用户部门图标
      -
      .ops-itsm-reports +
      .shouquanyonghubumentubiao
    • - +
      - ops-itsm-knowledge + itsm-baseInfo
      -
      .ops-itsm-knowledge +
      .itsm-baseInfo
    • - +
      - ops-itsm-planticket + itsm-association
      -
      .ops-itsm-planticket +
      .itsm-association
    • - +
      - ops-itsm-ticketsetting + itsm-handleInfo
      -
      .ops-itsm-ticketsetting +
      .itsm-handleInfo
    • - +
      - ops-itsm-servicedesk + itsm-intelligence
      -
      .ops-itsm-servicedesk +
      .itsm-intelligence
    • - +
      - ops-itsm-workstation + itsm-leaveMess
      -
      .ops-itsm-workstation +
      .itsm-leaveMess
    • - +
      - monitor-webPerf + itsm-log
      -
      .monitor-webPerf +
      .itsm-log
    • - +
      - monitor-image (1) + itsm-solution
      -
      .a-monitor-image1 +
      .itsm-solution
    • - +
      - monitor-other + itsm-sla
      -
      .monitor-other +
      .itsm-sla
    • - +
      - monitor-font + itsm-naire
      -
      .monitor-font +
      .itsm-naire
    • - +
      - monitor-css + itsm-claim
      -
      .monitor-css +
      .itsm-claim
    • - +
      - monitor-html + itsm-hang_up
      -
      .monitor-html +
      .itsm-hang_up
    • - +
      - monitor-video + itsm-flow_chart
      -
      .monitor-video +
      .itsm-flow_chart
    • - +
      - monitor-js + itsm-export
      -
      .monitor-js +
      .itsm-export
    • - +
      - monitor-audio + itsm-reminder
      -
      .monitor-audio +
      .itsm-reminder
    • - +
      - monitor-text + itsm-share
      -
      .monitor-text +
      .itsm-share
    • - +
      - monitor-jiancedian + itsm-terminate
      -
      .monitor-jiancedian +
      .itsm-terminate
    • - +
      - monitor-zongfenhegexiangpingfen + itsm-transfer
      -
      .monitor-zongfenhegexiangpingfen +
      .itsm-transfer
    • - +
      - monitor-dig + ops-cmdb-customdashboard-selected
      -
      .monitor-dig +
      .ops-cmdb-customdashboard-selected
    • - +
      - monitor-dns + ops-cmdb-adr-selected
      -
      .monitor-dns +
      .ops-cmdb-adr-selected
    • - +
      - monitor-traceroute + ops-cmdb-operation
      -
      .monitor-traceroute +
      .ops-cmdb-operation
    • - +
      - monitor-mtr + ops-cmdb-customdashboard
      -
      .monitor-mtr +
      .ops-cmdb-customdashboard
    • - +
      - monitor-websocket + ops-cmdb-adr
      -
      .monitor-websocket +
      .ops-cmdb-adr
    • - +
      - monitor-performance_monitor-copy + ops-cmdb-preferencerelation-selected
      -
      .webPerf-copy +
      .ops-cmdb-preferencerelation-selected
    • - +
      - yuansuxingneng + ops-cmdb-operation-selected
      -
      .yuansuxingneng +
      .ops-cmdb-operation-selected
    • - +
      - zhujijiankong + ops-cmdb-preferencerelation
      -
      .zhujijiankong +
      .ops-cmdb-preferencerelation
    • - +
      - zhujiqiang + ops-cmdb-modelrelation
      -
      .zhujiqiang +
      .ops-cmdb-modelrelation
    • - +
      - xingnengpinggu + ops-cmdb-modelrelation-selected
      -
      .xingnengpinggu +
      .ops-cmdb-modelrelation-selected
    • - +
      - wodekanban + ops-cmdb-relationtype-selected
      -
      .wodekanban +
      .ops-cmdb-relationtype-selected
    • - +
      - wangzhanjiankong + ops-cmdb-relationtype
      -
      .wangzhanjiankong +
      .ops-cmdb-relationtype
    • - +
      - tongji + ops-cmdb-batch-selected
      -
      .tongji +
      .ops-cmdb-batch-selected
    • - +
      - wangyexingneng + ops-cmdb-batch
      -
      .wangyexingneng +
      .ops-cmdb-batch
    • - +
      - wangluotuobu + ops-cmdb-adc-selected
      -
      .wangluotuobu +
      .ops-cmdb-adc-selected
    • - +
      - shishizhuangtai + ops-cmdb-resource-selected
      -
      .shishizhuangtai +
      .ops-cmdb-resource-selected
    • - +
      - gailan + ops-cmdb-preference-selected
      -
      .gailan +
      .ops-cmdb-preference-selected
    • - +
      - zonglan + ops-cmdb-preference
      -
      .zonglan +
      .ops-cmdb-preference
    • - +
      - itsm-workstation-fast + ops-cmdb-screen
      -
      .itsm-workstation-fast +
      .ops-cmdb-screen
    • - +
      - itsm-workstation-handle + ops-cmdb-tree-selected
      -
      .itsm-workstation-handle +
      .ops-cmdb-tree-selected
    • - +
      - itsm-stop_hang_up + ops-cmdb-relation-selected
      -
      .itsm-stop_hang_up +
      .ops-cmdb-relation-selected
    • - +
      - itsm-workstation-overview + ops-cmdb-adc
      -
      .itsm-workstation-overview +
      .ops-cmdb-adc
    • - +
      - itsm-workstation-inform + ops-cmdb-search-selected
      -
      .itsm-workstation-inform +
      .ops-cmdb-search-selected
    • - +
      - itsm-workstation-notice + ops-cmdb-relation
      -
      .itsm-workstation-notice +
      .ops-cmdb-relation
    • - +
      - itsm-workstation-todo list + ops-cmdb-tree
      -
      .itsm-workstation-todolist +
      .ops-cmdb-tree
    • - +
      - itsm-workstation-duty + ops-cmdb-citype-selected
      -
      .itsm-workstation-duty +
      .ops-cmdb-citype-selected
    • - +
      - itsm-workstation-initiate + ops-cmdb-dashboard-selected
      -
      .itsm-workstation-initiate +
      .ops-cmdb-dashboard-selected
    • - +
      - itsm-my-my_initiate + ops-cmdb-citype
      -
      .itsm-my-my_initiate +
      .ops-cmdb-citype
    • - +
      - itsm-my-my_todo-copy + ops-cmdb-dashboard
      -
      .itsm-my-my_handle +
      .ops-cmdb-dashboard
    • - +
      - itsm-my-draft + ops-cmdb-screen-selected
      -
      .itsm-my-draft +
      .ops-cmdb-screen-selected
    • - +
      - itsm-my-all + ops-cmdb-resource
      -
      .itsm-my-all +
      .ops-cmdb-resource
    • - +
      - itsm-my-in_process + ops-cmdb-search
      -
      .itsm-my-in_process +
      .ops-cmdb-search
    • - +
      - itsm-my-my_todo + icon-itsm-product redemption
      -
      .itsm-my-my_todo +
      .icon-itsm-20
    • - +
      - itsm-my-share + icon-itsm-creat portfolio
      -
      .itsm-my-share +
      .icon-itsm-23
    • - +
      - itsm-my-pending_claim + icon-itsm-product foreclosure process
      -
      .itsm-my-pending_claim +
      .icon-itsm-22
    • - +
      - itsm-log-ABORTED + icon-itsm-money in and out
      -
      .itsm-log-ABORTED +
      .icon-itsm-21
    • - +
      - itsm-log-FAILED + icon-itsm-new signal light
      -
      .itsm-log-FAILED +
      .icon-itsm-24
    • - +
      - itsm-log-SUCCESS + icon-itsm-libra permission application
      -
      .itsm-log-SUCCESS +
      .icon-itsm-25
    • - +
      - itsm-log-ERROR + icon-itsm-default service work order
      -
      .itsm-log-ERROR +
      .icon-itsm-26
    • - +
      - itsm-service-type + icon-itsm-default problem work order
      -
      .itsm-service-type +
      .icon-itsm-28
    • - +
      - itsm-my + icon-itsm-default event work order
      -
      .itsm-my +
      .icon-itsm-27
    • - +
      - ops-monitor-hostwall + icon-itsm-default change work order
      -
      .ops-monitor-hostwall +
      .icon-itsm-29
    • - +
      - ops-monitor-hostwall-setting + icon-itsm-request for classified data
      -
      .ops-monitor-hostwall-setting +
      .icon-itsm-16
    • - +
      - 授权用户部门图标 + icon-itsm-external procurement
      -
      .shouquanyonghubumentubiao +
      .icon-itsm-17
    • - +
      - itsm-baseInfo + icon-itsm-application for payment of funds
      -
      .itsm-baseInfo +
      .icon-itsm-18
    • - +
      - itsm-association + icon-itsm-product subscription
      -
      .itsm-association +
      .icon-itsm-19
    • - +
      - itsm-handleInfo + icon-itsm-claims reimbursement process (1)
      -
      .itsm-handleInfo +
      .icon-itsm-9
    • - +
      - itsm-intelligence + icon-itsm-open an account
      -
      .itsm-intelligence +
      .icon-itsm-10
    • - +
      - itsm-leaveMess + icon-itsm-seal application
      -
      .itsm-leaveMess +
      .icon-itsm-11
    • - +
      - itsm-log + icon-itsm-external reimbursement process
      -
      .itsm-log +
      .icon-itsm-12
    • - +
      - itsm-solution + icon-itsm-item approval
      -
      .itsm-solution +
      .icon-itsm-13
    • - +
      - itsm-sla + icon-itsm-servers and other devices application
      -
      .itsm-sla +
      .icon-itsm-14
    • - +
      - itsm-naire + icon-itsm-payment process
      -
      .itsm-naire +
      .icon-itsm-15
    • - +
      - itsm-claim + icon-itsm-employee income certificate
      -
      .itsm-claim +
      .icon-itsm-4
    • - +
      - itsm-hang_up + incumbency certification
      -
      .itsm-hang_up +
      .icon-itsm-6
    • - +
      - itsm-flow_chart + icon-itsm-intern dimission process
      -
      .itsm-flow_chart +
      .icon-itsm-8
    • - +
      - itsm-export + icon-itsm-intern attendance
      -
      .itsm-export +
      .icon-itsm-5
    • - +
      - itsm-reminder + icon-itsm-new employees' induction process
      -
      .itsm-reminder +
      .icon-itsm-7
    • - +
      - itsm-share + icon-itsm-field application
      -
      .itsm-share +
      .icon-itsm-3
    • - +
      - itsm-terminate + icon-itsm-leave process
      -
      .itsm-terminate +
      .icon-itsm-1
    • - +
      - itsm-transfer + icon-itsm-new onboarding process
      -
      .itsm-transfer +
      .icon-itsm-2
    • - +
      - ops-cmdb-customdashboard-selected + icon-shidi-aws
      -
      .ops-cmdb-customdashboard-selected +
      .icon-shidi-aws
    • - +
      - ops-cmdb-adr-selected + icon-xianxing-aws
      -
      .ops-cmdb-adr-selected +
      .icon-xianxing-aws
    • - +
      - ops-cmdb-operation + caise-aws
      -
      .ops-cmdb-operation +
      .caise-aws
    • - +
      - ops-cmdb-customdashboard + icon-xianxing-dayinji
      -
      .ops-cmdb-customdashboard +
      .icon-xianxing-dayinji
    • - +
      - ops-cmdb-adr + icon-shiti-dayinji
      -
      .ops-cmdb-adr +
      .icon-shidi-dayinji
    • - +
      - ops-cmdb-preferencerelation-selected + icon-shiti-chajian
      -
      .ops-cmdb-preferencerelation-selected +
      .icon-shidi-chajian
    • - +
      - ops-cmdb-operation-selected + caise-chajian
      -
      .ops-cmdb-operation-selected +
      .icon-xianxing-chajian
    • - +
      - ops-cmdb-preferencerelation + caise-dayinji
      -
      .ops-cmdb-preferencerelation +
      .caise-dayinji
    • - +
      - ops-cmdb-modelrelation + caise-chajian
      -
      .ops-cmdb-modelrelation +
      .caise-chajian
    • - +
      - ops-cmdb-modelrelation-selected + itsm-service-all
      -
      .ops-cmdb-modelrelation-selected +
      .itsm-service-all
    • - +
      - ops-cmdb-relationtype-selected + itsm-service-common
      -
      .ops-cmdb-relationtype-selected +
      .itsm-service-common
    • - +
      - ops-cmdb-relationtype + itsm-upload
      -
      .ops-cmdb-relationtype +
      .itsm-upload
    • - +
      - ops-cmdb-batch-selected + itsm-code
      -
      .ops-cmdb-batch-selected +
      .itsm-code
    • - +
      - ops-cmdb-batch + itsm-paragraph
      -
      .ops-cmdb-batch +
      .itsm-paragraph
    • - +
      - ops-cmdb-adc-selected + itsm-department
      -
      .ops-cmdb-adc-selected +
      .itsm-department
    • - +
      - ops-cmdb-resource-selected + itsm-phone
      -
      .ops-cmdb-resource-selected +
      .itsm-phone
    • - +
      - ops-cmdb-preference-selected + itsm-employee
      -
      .ops-cmdb-preference-selected +
      .itsm-employee
    • - +
      - ops-cmdb-preference + itsm-description
      -
      .ops-cmdb-preference +
      .itsm-description
    • - +
      - ops-cmdb-screen + itsm-rich
      -
      .ops-cmdb-screen +
      .itsm-rich
    • - +
      - ops-cmdb-tree-selected + itsm-image
      -
      .ops-cmdb-tree-selected +
      .itsm-image
    • - +
      - ops-cmdb-relation-selected + itsm-start-end
      -
      .ops-cmdb-relation-selected +
      .itsm-start-end
    • - +
      - ops-cmdb-adc + itsm-single-select
      -
      .ops-cmdb-adc +
      .itsm-single-select
    • - +
      - ops-cmdb-search-selected + itsm-link
      -
      .ops-cmdb-search-selected +
      .itsm-link
    • - +
      - ops-cmdb-relation + itsm-date
      -
      .ops-cmdb-relation +
      .itsm-date
    • - +
      - ops-cmdb-tree + itsm-input
      -
      .ops-cmdb-tree +
      .itsm-input
    • - +
      - ops-cmdb-citype-selected + itsm-textarea
      -
      .ops-cmdb-citype-selected +
      .itsm-textarea
    • - +
      - ops-cmdb-dashboard-selected + itsm-input-number
      -
      .ops-cmdb-dashboard-selected +
      .itsm-input-number
    • - +
      - ops-cmdb-citype + itsm-multiple-select
      -
      .ops-cmdb-citype +
      .itsm-multiple-select
    • - +
      - ops-cmdb-dashboard + itsm-email
      -
      .ops-cmdb-dashboard +
      .itsm-email
    • - +
      - ops-cmdb-screen-selected + itsm-table
      -
      .ops-cmdb-screen-selected +
      .itsm-table
    • - +
      - ops-cmdb-resource + itsm-service
      -
      .ops-cmdb-resource +
      .itsm-service
    • - +
      - ops-cmdb-search + itsm-change
      -
      .ops-cmdb-search +
      .itsm-change
    • - +
      - icon-itsm-product redemption + itsm-problem
      -
      .icon-itsm-20 +
      .itsm-problem
    • - +
      - icon-itsm-creat portfolio + itsm-event
      -
      .icon-itsm-23 +
      .itsm-event
    • - +
      - icon-itsm-product foreclosure process + itsm_approve_config
      -
      .icon-itsm-22 +
      .itsm_approve_config
    • - +
      - icon-itsm-money in and out + itsm_execute_user
      -
      .icon-itsm-21 +
      .itsm_execute_user
    • - +
      - icon-itsm-new signal light + itsm_notice_config
      -
      .icon-itsm-24 +
      .itsm_notice_config
    • - +
      - icon-itsm-libra permission application + itsm_timeout
      -
      .icon-itsm-25 +
      .itsm_timeout
    • - +
      - icon-itsm-default service work order + itsm_func_config
      -
      .icon-itsm-26 +
      .itsm_func_config
    • - +
      - icon-itsm-default problem work order + itsm_time_config
      -
      .icon-itsm-28 +
      .itsm_time_config
    • - +
      - icon-itsm-default event work order + itsm_auto_trigger
      -
      .icon-itsm-27 +
      .itsm_auto_trigger
    • - +
      - icon-itsm-default change work order + icon-xianxing-复制
      -
      .icon-itsm-29 +
      .icon-xianxing-copy
    • - +
      - icon-itsm-request for classified data + itsm-node-examine
      -
      .icon-itsm-16 +
      .itsm-node-examine
    • - +
      - icon-itsm-external procurement + itsm-node-branch
      -
      .icon-itsm-17 +
      .itsm-node-branch
    • - +
      - icon-itsm-application for payment of funds + itsm-node-auto
      -
      .icon-itsm-18 +
      .itsm-node-auto
    • - +
      - icon-itsm-product subscription + itsm-node-end
      -
      .icon-itsm-19 +
      .itsm-node-end
    • - +
      - icon-itsm-claims reimbursement process (1) + itsm-node-strat
      -
      .icon-itsm-9 +
      .itsm-node-start
    • - +
      - icon-itsm-open an account + itsm-node-manual
      -
      .icon-itsm-10 +
      .itsm-node-manual
    • - +
      - icon-itsm-seal application + icon-xianxing-删除
      -
      .icon-itsm-11 +
      .icon-xianxing-delete
    • - +
      - icon-itsm-external reimbursement process + icon-xianxing-编辑
      -
      .icon-itsm-12 +
      .icon-xianxing-edit
    • - +
      - icon-itsm-item approval + monitor-base
      -
      .icon-itsm-13 +
      .monitor-base
    • - +
      - icon-itsm-servers and other devices application + setting-structure-depart1
      -
      .icon-itsm-14 +
      .setting-structure-depart1
    • - +
      - icon-itsm-payment process + setting-structure-depart2
      -
      .icon-itsm-15 +
      .setting-structure-depart2
    • - +
      - icon-itsm-employee income certificate + bat
      -
      .icon-itsm-4 +
      .caise-bat
    • - +
      - incumbency certification + power shell
      -
      .icon-itsm-6 +
      .caise-powershell
    • - +
      - icon-itsm-intern dimission process + shell
      -
      .icon-itsm-8 +
      .caise-shell
    • - +
      - icon-itsm-intern attendance + ops-setting-role-selected
      -
      .icon-itsm-5 +
      .ops-setting-role-selected
    • - +
      - icon-itsm-new employees' induction process + ops-setting-group-selected
      -
      .icon-itsm-7 +
      .ops-setting-group-selected
    • - +
      - icon-itsm-field application + ops-setting-role
      -
      .icon-itsm-3 +
      .ops-setting-role
    • - +
      - icon-itsm-leave process + ops-setting-group
      -
      .icon-itsm-1 +
      .ops-setting-group
    • - +
      - icon-itsm-new onboarding process + ops-setting-technician
      -
      .icon-itsm-2 +
      .ops-setting-technician
    • - +
      - icon-shidi-aws + ops-setting-user
      -
      .icon-shidi-aws +
      .ops-setting-user
    • - +
      - icon-xianxing-aws + click house
      -
      .icon-xianxing-aws +
      .monitor-clickhouse
    • - +
      - caise-aws + ping监控
      -
      .caise-aws +
      .monitor-ping
    • - +
      - icon-xianxing-dayinji + zabbix
      -
      .icon-xianxing-dayinji +
      .monitor-zabbix
    • - +
      - icon-shiti-dayinji + SNMP
      -
      .icon-shidi-dayinji +
      .monitor-snmp
    • - +
      - icon-shiti-chajian + 网页
      -
      .icon-shidi-chajian +
      .monitor-http_response
    • - +
      - caise-chajian + 后台运行
      -
      .icon-xianxing-chajian +
      .monitor-execd
    • - +
      - caise-dayinji + 端口探测
      -
      .caise-dayinji +
      .monitor-net_response
    • - +
      - caise-chajian + ipmi带外
      -
      .caise-chajian +
      .monitor-ipmi
    • - +
      - itsm-service-all + 前台运行
      -
      .itsm-service-all +
      .monitor-exec
    • - +
      - itsm-service-common + Group 2289
      -
      .itsm-service-common +
      .monitor-redis
    • - +
      - itsm-upload + mongodb
      -
      .itsm-upload +
      .monitor-mongodb
    • - +
      - itsm-code + MYSQL
      -
      .itsm-code +
      .monitor-mysql
    • - +
      - itsm-paragraph + HTTP API
      -
      .itsm-paragraph +
      .monitor-http
    • - +
      - itsm-department + 日志扫描
      -
      .itsm-department +
      .monitor-log
    • - +
      - itsm-phone + 进程管理
      -
      .itsm-phone +
      .monitor-process
    • - +
      - itsm-employee + elasticsearch
      -
      .itsm-employee +
      .monitor-elasticsearch
    • - +
      - itsm-description + ops-setting-role-system
      -
      .itsm-description +
      .ops-setting-role-system
    • - +
      - itsm-rich + ops-setting-role-system-selected
      -
      .itsm-rich +
      .ops-setting-role-system-selected
    • - +
      - itsm-image + ops-datainsight-audit
      -
      .itsm-image +
      .ops-datainsight-audit
    • - +
      - itsm-start-end + ops-datainsight-audit-selected
      -
      .itsm-start-end +
      .ops-datainsight-audit-selected
    • - +
      - itsm-single-select + ops-datainsight-dashboard-selected
      -
      .itsm-single-select +
      .ops-datainsight-dashboard-selected
    • - +
      - itsm-link + ops-datainsight-dashboard
      -
      .itsm-link +
      .ops-datainsight-dashboard
    • - +
      - itsm-date + ops-datainsight-method
      -
      .itsm-date +
      .ops-datainsight-method
    • - +
      - itsm-input + ops-datainsight-method-selected
      -
      .itsm-input +
      .ops-datainsight-method-selected
    • - +
      - itsm-textarea + ops-datainsight-rule-selected
      -
      .itsm-textarea +
      .ops-datainsight-rule-selected
    • - +
      - itsm-input-number + ops-datainsight-rule
      -
      .itsm-input-number +
      .ops-datainsight-rule
    • - +
      - itsm-multiple-select + ops-datainsight-origin-selected
      -
      .itsm-multiple-select +
      .ops-datainsight-origin-selected
    • - +
      - itsm-email + ops-datainsight-origin
      -
      .itsm-email +
      .ops-datainsight-origin
    • - +
      - itsm-table + ops-datainsight-alert
      -
      .itsm-table +
      .ops-datainsight-alert
    • - +
      - itsm-service + ops-datainsight-alert-selected
      -
      .itsm-service +
      .ops-datainsight-alert-selected
    • - +
      - itsm-change + rule_6
      -
      .itsm-change +
      .rule_6
    • - +
      - itsm-problem + icon-xianxing-shenji
      -
      .itsm-problem +
      .icon-xianxing-shenji
    • - +
      - itsm-event + rule_3
      -
      .itsm-event +
      .rule_3
    • - +
      - itsm_approve_config + rule_5
      -
      .itsm_approve_config +
      .rule_5
    • - +
      - itsm_execute_user + rule_1
      -
      .itsm_execute_user +
      .rule_1
    • - +
      - itsm_notice_config + rule_8
      -
      .itsm_notice_config +
      .rule_8
    • - +
      - itsm_timeout + rule_2
      -
      .itsm_timeout +
      .rule_2
    • - +
      - itsm_func_config + rule_4
      -
      .itsm_func_config +
      .rule_4
    • - +
      - itsm_time_config + level_4
      -
      .itsm_time_config +
      .level_4
    • - +
      - itsm_auto_trigger + level_3
      -
      .itsm_auto_trigger +
      .level_3
    • - +
      - icon-xianxing-复制 + level_1
      -
      .icon-xianxing-copy +
      .level_1
    • - +
      - itsm-node-examine + level_2
      -
      .itsm-node-examine +
      .level_2
    • - +
      - itsm-node-branch + 系统管理
      -
      .itsm-node-branch +
      .ops-setting-system
    • - +
      - itsm-node-auto + caise-华为云
      -
      .itsm-node-auto +
      .caise-huaweiyun
    • - +
      - itsm-node-end + caise-负载均衡
      -
      .itsm-node-end +
      .caise-fuzaijunheng
    • - +
      - itsm-node-strat + caise-交换机
      -
      .itsm-node-strat +
      .caise-jiaohuanji
    • - +
      - itsm-node-manual + caise-路由器
      -
      .itsm-node-manual +
      .caise-luyouqi
    • - +
      - icon-xianxing-删除 + caise-部门
      -
      .icon-xianxing-delete +
      .caise-bumen
    • - +
      - icon-xianxing-编辑 + caise-虚拟机
      -
      .icon-xianxing-edit +
      .caise-xuniji
    • - +
      - monitor-base + caise-应用
      -
      .monitor-base +
      .caise-yingyong
    • - +
      - setting-structure-depart1 + caise-Nginx
      -
      .setting-structure-depart1 +
      .caise-nginx
    • - +
      - setting-structure-depart2 + caise-腾讯云
      -
      .setting-structure-depart2 +
      .caise-tengxunyun
    • - +
      - bat + caise-产品
      -
      .caise-bat +
      .caise-chanpin
    • - +
      - power shell + caise-防火墙
      -
      .caise-powershell +
      .caise-fanghuoqiang
    • - +
      - shell + caise-docker
      -
      .caise-shell +
      .caise-docker
    • - +
      - ops-setting-role-selected + caise-硬盘
      -
      .ops-setting-role-selected +
      .caise-yingpan
    • - +
      - ops-setting-group-selected + caise-物理机
      -
      .ops-setting-group-selected +
      .caise-wuliji
    • - +
      - ops-setting-role + caise-网卡
      -
      .ops-setting-role +
      .caise-wangka
    • - +
      - ops-setting-group + caise-内存
      -
      .ops-setting-group +
      .caise-neicun
    • - +
      - ops-setting-technician + caise-阿里云
      -
      .ops-setting-technician +
      .caise-aliyun
    • - +
      - ops-setting-user + caise-Apache
      -
      .ops-setting-user +
      .caise-apache
    • - +
      - click house + caise-redis
      -
      .monitor-clickhouse +
      .caise-redis
    • - +
      - ping监控 + caise-Tomcat
      -
      .monitor-ping +
      .caise-tomcat
    • - +
      - zabbix + icon-防火墙
      -
      .monitor-zabbix +
      .icon-xianxing-fanghuoqiang
    • - +
      - SNMP + icon-防火墙
      -
      .monitor-snmp +
      .icon-shidi-fanghuoqiang
    • - +
      - 网页 + icon-交换机
      -
      .monitor-http_response +
      .icon-shidi-jiaohuanji
    • - +
      - 后台运行 + icon-负载均衡
      -
      .monitor-execd +
      .icon-shidi-fuzaijunheng
    • - +
      - 端口探测 + icon-内存
      -
      .monitor-net_response +
      .icon-shidi-neicun
    • - +
      - ipmi带外 + icon-物理机
      -
      .monitor-ipmi +
      .icon-shidi-wuliji
    • - +
      - 前台运行 + icon-路由器
      -
      .monitor-exec +
      .icon-shidi-luyouqi
    • - +
      - Group 2289 + icon-硬盘
      -
      .monitor-redis +
      .icon-shidi-yingpan
    • - +
      - mongodb + icon-产品
      -
      .monitor-mongodb +
      .icon-shidi-chanpin
    • - +
      - MYSQL + icon-应用
      -
      .monitor-mysql +
      .icon-shidi-yingyong
    • - +
      - HTTP API + icon-Nginx
      -
      .monitor-http +
      .icon-shidi-nginx
    • - +
      - 日志扫描 + icon-docker
      -
      .monitor-log +
      .icon-shidi-docker
    • - +
      - 进程管理 + icon-网卡
      -
      .monitor-process +
      .icon-shidi-wangka
    • - +
      - elasticsearch + icon-Apache
      -
      .monitor-elasticsearch +
      .icon-shidi-apache
    • - +
      - ops-setting-role-system + icon-redis
      -
      .ops-setting-role-system +
      .icon-shidi-redis
    • - +
      - ops-setting-role-system-selected + icon-Tomcat
      -
      .ops-setting-role-system-selected +
      .icon-shidi-tomcat
    • - +
      - ops-datainsight-audit + icon-虚拟机
      -
      .ops-datainsight-audit +
      .icon-shidi-xuniji
    • - +
      - ops-datainsight-audit-selected + icon-部门
      -
      .ops-datainsight-audit-selected +
      .icon-shidi-bumen
    • - +
      - ops-datainsight-dashboard-selected + icon-华为云
      -
      .ops-datainsight-dashboard-selected +
      .icon-shidi-huaweiyun
    • - +
      - ops-datainsight-dashboard + icon-腾讯云
      -
      .ops-datainsight-dashboard +
      .icon-shidi-tengxunyun
    • - +
      - ops-datainsight-method + icon-阿里云
      -
      .ops-datainsight-method +
      .icon-shidi-aliyun
    • - +
      - ops-datainsight-method-selected + icon-部门
      -
      .ops-datainsight-method-selected +
      .icon-xianxing-bumen
    • - +
      - ops-datainsight-rule-selected + icon-负载均衡
      -
      .ops-datainsight-rule-selected +
      .icon-xianxing-fuzaijunheng
    • - +
      - ops-datainsight-rule + icon-交换机
      -
      .ops-datainsight-rule +
      .icon-xianxing-jiaohuanji
    • - +
      - ops-datainsight-origin-selected + icon-产品
      -
      .ops-datainsight-origin-selected +
      .icon-xianxing-chanpin
    • - +
      - ops-datainsight-origin + icon-华为云
      -
      .ops-datainsight-origin +
      .icon-xianxing-huaweiyun
    • - +
      - ops-datainsight-alert + icon-物理机
      -
      .ops-datainsight-alert +
      .icon-xianxing-wuliji
    • - +
      - ops-datainsight-alert-selected + icon-应用
      -
      .ops-datainsight-alert-selected +
      .icon-xianxing-yingyong
    • - +
      - rule_6 + icon-路由器
      -
      .rule_6 +
      .icon-xianxing-luyouqi
    • - +
      - icon-xianxing-shenji + icon-网卡
      -
      .icon-xianxing-shenji +
      .icon-xianxing-wangka
    • - +
      - rule_3 + icon-Nginx
      -
      .rule_3 +
      .icon-xianxing-nginx
    • - +
      - rule_5 + icon-阿里云
      -
      .rule_5 +
      .icon-xianxing-aliyun
    • - +
      - rule_1 + icon-虚拟机
      -
      .rule_1 +
      .icon-xianxing-xuniji
    • - +
      - rule_8 + icon-硬盘
      -
      .rule_8 +
      .icon-xianxing-yingpan
    • - +
      - rule_2 + icon-Apache
      -
      .rule_2 +
      .icon-xianxing-apache
    • - +
      - rule_4 + icon-docker
      -
      .rule_4 +
      .icon-xianxing-docker
    • - +
      - level_4 + icon-redis
      -
      .level_4 +
      .icon-xianxing-redis
    • - +
      - level_3 + icon-内存
      -
      .level_3 +
      .icon-xianxing-neicun
    • - +
      - level_1 + icon-Tomcat
      -
      .level_1 +
      .icon-xianxing-tomcat
    • - +
      - level_2 + icon-腾讯云
      -
      .level_2 +
      .icon-xianxing-tengxunyun
    • - +
      - 系统管理 + ops-dot-copy
      -
      .ops-setting-system +
      .ops-dot-copy
    • - +
      - caise-华为云 + ops-review
      -
      .caise-huaweiyun +
      .ops-review
    • - +
      - caise-负载均衡 + ops-dot
      -
      .caise-fuzaijunheng +
      .ops-dot
    • - +
      - caise-交换机 + ops-setting-notice-email-selected
      -
      .caise-jiaohuanji +
      .ops-setting-notice-email-selected-copy
    • - +
      - caise-路由器 + ops-setting-notice
      -
      .caise-luyouqi +
      .ops-setting-notice
    • - +
      - caise-部门 + ops-setting-notice-selected
      -
      .caise-bumen +
      .ops-setting-notice-selected
    • - +
      - caise-虚拟机 + ops-setting-notice-email-selected
      -
      .caise-xuniji +
      .ops-setting-notice-email-selected
    • - +
      - caise-应用 + ops-setting-notice-email
      -
      .caise-yingyong +
      .ops-setting-notice-email
    • - +
      - caise-Nginx + ops-setting-notice-dingding-selected
      -
      .caise-nginx +
      .ops-setting-notice-dingding-selected
    • - +
      - caise-腾讯云 + ops-setting-notice-dingding
      -
      .caise-tengxunyun +
      .ops-setting-notice-dingding
    • - +
      - caise-产品 + ops-setting-notice-wx-selected
      -
      .caise-chanpin +
      .ops-setting-notice-wx-selected
    • - +
      - caise-防火墙 + ops-setting-notice-wx
      -
      .caise-fanghuoqiang +
      .ops-setting-notice-wx
    • - +
      - caise-docker + ops-setting-companyStructure-selected
      -
      .caise-docker +
      .ops-setting-companyStructure-selected
    • - +
      - caise-硬盘 + ops-setting-companyStructure
      -
      .caise-yingpan +
      .ops-setting-companyStructure
    • - +
      - caise-物理机 + ops-setting-companyInfo
      -
      .caise-wuliji +
      .ops-setting-companyInfo
    • - +
      - caise-网卡 + ops-setting-companyInfo-selected
      -
      .caise-wangka +
      .ops-setting-companyInfo-selected
    • - +
      - caise-内存 + ops-email
      -
      .caise-neicun +
      .ops-email
    • - +
      - caise-阿里云 + ops-history
      -
      .caise-aliyun +
      .ops-history
    • - +
      - caise-Apache + ops-menu
      -
      .caise-apache +
      .ops-menu
    • - +
      - caise-redis + ops-run
      -
      .caise-redis +
      .ops-run
    • - +
      - caise-Tomcat + ops-save
      -
      .caise-tomcat +
      .ops-save
    • - +
      - icon-防火墙 + ops-environment
      -
      .icon-xianxing-fanghuoqiang +
      .ops-environment
    • - +
      - icon-防火墙 + ops-plus
      -
      .icon-shidi-fanghuoqiang +
      .ops-plus
    • - +
      - icon-交换机 + ops-type_setting
      -
      .icon-shidi-jiaohuanji +
      .ops-type_setting
    • - +
      - icon-负载均衡 + icon-shell
      -
      .icon-shidi-fuzaijunheng +
      .icon-shidi-shell1
    • - +
      - icon-内存 + icon-bat
      -
      .icon-shidi-neicun +
      .icon-shidi-bat1
    • - +
      - icon-物理机 + icon-powershell
      -
      .icon-shidi-wuliji +
      .icon-shidi-powershell1
    • - +
      - icon-路由器 + icon-bat
      -
      .icon-shidi-luyouqi +
      .icon-xianxing-bat
    • - +
      - icon-硬盘 + icon-powershell
      -
      .icon-shidi-yingpan +
      .icon-xianxing-powershell
    • - +
      - icon-产品 + icon-shell
      -
      .icon-shidi-chanpin +
      .icon-xianxing-shell
    • - +
      - icon-应用 + icon-Redhat
      -
      .icon-shidi-yingyong +
      .caise-redhat
    • - +
      - icon-Nginx + icon-Ubuntu
      -
      .icon-shidi-nginx +
      .caise-Ubuntu
    • - +
      - icon-docker + icon-在线
      -
      .icon-shidi-docker +
      .caise-zaixian
    • - +
      - icon-网卡 + icon-下线
      -
      .icon-shidi-wangka +
      .caise-xiaxian
    • - +
      - icon-Apache + icon-Centos
      -
      .icon-shidi-apache +
      .caise-centos
    • - +
      - icon-redis + icon-在线
      -
      .icon-shidi-redis +
      .icon-shidi-zaixian
    • - +
      - icon-Tomcat + icon-Redhat
      -
      .icon-shidi-tomcat +
      .icon-shidi-redhat
    • - +
      - icon-虚拟机 + icon-bat
      -
      .icon-shidi-xuniji +
      .icon-shidi-bat
    • - +
      - icon-部门 + icon-shell
      -
      .icon-shidi-bumen +
      .icon-shidi-shell
    • - +
      - icon-华为云 + icon-下线
      -
      .icon-shidi-huaweiyun +
      .icon-shidi-xiaxian
    • - +
      - icon-腾讯云 + icon-Ubuntu
      -
      .icon-shidi-tengxunyun +
      .icon-shidi-Ubuntu
    • - +
      - icon-阿里云 + icon-Centos
      -
      .icon-shidi-aliyun +
      .icon-shidi-centos
    • - +
      - icon-部门 + icon-powershell-copy
      -
      .icon-xianxing-bumen +
      .icon-shidi-powershell
    • - +
      - icon-负载均衡 + icon-在线
      -
      .icon-xianxing-fuzaijunheng +
      .icon-xianxing-zaixian
    • - +
      - icon-交换机 + icon-Ubuntu
      -
      .icon-xianxing-jiaohuanji +
      .icon-xianxing-Ubuntu
    • - +
      - icon-产品 + icon-下线
      -
      .icon-xianxing-chanpin +
      .icon-xianxing-xiaxian
    • - +
      - icon-华为云 + icon-centos
      -
      .icon-xianxing-huaweiyun +
      .icon-xianxing-centos
    • - +
      - icon-物理机 + icon-redhat
      -
      .icon-xianxing-wuliji +
      .icon-xianxing-redhat
    • - +
      - icon-应用 + icon-实数
      -
      .icon-xianxing-yingyong +
      .caise-shishu
    • - +
      - icon-路由器 + icon-文本
      -
      .icon-xianxing-luyouqi +
      .caise-wenben
    • - +
      - icon-网卡 + icon-json
      -
      .icon-xianxing-wangka +
      .caise-json
    • - +
      - icon-Nginx + icon-datetime
      -
      .icon-xianxing-nginx +
      .caise-datetime
    • - +
      - icon-阿里云 + icon-浮点数
      -
      .icon-xianxing-aliyun +
      .caise-fudianshu
    • - +
      - icon-虚拟机 + icon-time
      -
      .icon-xianxing-xuniji +
      .caise-time
    • - +
      - icon-硬盘 + icon-date
      -
      .icon-xianxing-yingpan +
      .caise-date
    • - +
      - icon-Apache + icon-浮点数
      -
      .icon-xianxing-apache +
      .icon-shidi-fudianshu
    • - +
      - icon-docker + icon-json
      -
      .icon-xianxing-docker +
      .icon-shidi-json
    • - +
      - icon-redis + icon-time
      -
      .icon-xianxing-redis +
      .icon-shidi-time
    • - +
      - icon-内存 + icon-文本
      -
      .icon-xianxing-neicun +
      .icon-shidi-wenben
    • - +
      - icon-Tomcat + icon-date
      -
      .icon-xianxing-tomcat +
      .icon-shidi-date
    • - +
      - icon-腾讯云 + icon-datetime
      -
      .icon-xianxing-tengxunyun +
      .icon-shidi-datetime
    • - +
      - ops-dot-copy + icon-实数
      -
      .ops-dot-copy +
      .icon-shidi-shishu
    • - +
      - ops-review + icon-time
      -
      .ops-review +
      .icon-xianxing-time
    • - +
      - ops-dot + icon-date
      -
      .ops-dot +
      .icon-xianxing-date
    • - +
      - ops-setting-notice + icon-浮点数
      -
      .ops-setting-notice +
      .icon-xianxing-fudianshu
    • - +
      - ops-setting-notice-selected + icon-文本
      -
      .ops-setting-notice-selected +
      .icon-xianxing-wenben
    • - +
      - ops-setting-notice-email-selected + icon-实数
      -
      .ops-setting-notice-email-selected +
      .icon-xianxing-shishu
    • - +
      - ops-setting-notice-email + icon-datetime
      -
      .ops-setting-notice-email +
      .icon-xianxing-datetime
    • - +
      - ops-setting-notice-dingding-selected + icon-json
      -
      .ops-setting-notice-dingding-selected +
      .icon-xianxing-json
    • - +
      - ops-setting-notice-dingding + ops-is_choice-disabled
      -
      .ops-setting-notice-dingding +
      .ops-is_choice-disabled
    • - +
      - ops-setting-notice-wx-selected + ops-is_password-disabled
      -
      .ops-setting-notice-wx-selected +
      .ops-is_password-disabled
    • - +
      - ops-setting-notice-wx + ops-is_index-disabled
      -
      .ops-setting-notice-wx +
      .ops-is_index-disabled
    • - +
      - ops-setting-companyStructure-selected + ops-is_sortable-disabled
      -
      .ops-setting-companyStructure-selected +
      .ops-is_sortable-disabled
    • - +
      - ops-setting-companyStructure + ops-is_unique-disabled
      -
      .ops-setting-companyStructure +
      .ops-is_unique-disabled
    • - +
      - ops-setting-companyInfo + ops-is_link-disabled
      -
      .ops-setting-companyInfo +
      .ops-is_link-disabled
    • - +
      - ops-setting-companyInfo-selected + ops-trigger
      -
      .ops-setting-companyInfo-selected +
      .ops-trigger
    • - +
      - ops-email + ops-default_show-disabled
      -
      .ops-email +
      .ops-default_show-disabled
    • - +
      - ops-history + icon-添加
      -
      .ops-history +
      .caise-tianjia
    • - +
      - ops-menu + icon-说明
      -
      .ops-menu +
      .caise-shuoming
    • - +
      - ops-run + icon-暂停
      -
      .ops-run +
      .caise-zanting
    • - +
      - ops-save + icon-确认
      -
      .ops-save +
      .caise-queren
    • - +
      - ops-environment + icon-警告
      -
      .ops-environment +
      .caise-jinggao
    • - +
      - ops-plus + icon-减少
      -
      .ops-plus +
      .caise-jianqu
    • - +
      - ops-type_setting + icon-处理中
      -
      .ops-type_setting +
      .caise-chulizhong
    • - +
      - icon-shell + icon-取消
      -
      .icon-shidi-shell1 +
      .caise-quxiao
    • - +
      - icon-bat + icon-疑问
      -
      .icon-shidi-bat1 +
      .caise-yiwen
    • - +
      - icon-powershell + icon-禁止
      -
      .icon-shidi-powershell1 +
      .caise-jinzhi
    • - +
      - icon-bat + icon-收藏
      -
      .icon-xianxing-bat +
      .caise-shoucang
    • - +
      - icon-powershell + icon-维修中
      -
      .icon-xianxing-powershell +
      .caise-weixiu
    • - +
      - icon-shell + icon-未认证
      -
      .icon-xianxing-shell +
      .caise-weirenzheng
    • - +
      - icon-Redhat + icon-二维码
      -
      .caise-redhat +
      .caise-erweima
    • - +
      - icon-Ubuntu + icon-用户
      -
      .caise-Ubuntu +
      .caise-yonghu
    • - +
      - icon-在线 + icon-报警
      -
      .caise-zaixian +
      .caise-baojing
    • - +
      - icon-下线 + icon-添加用户
      -
      .caise-xiaxian +
      .caise-tianjiayonghu
    • - +
      - icon-Centos + icon-云数据
      -
      .caise-centos +
      .caise-yunshuju
    • - +
      - icon-在线 + icon-消息
      -
      .icon-shidi-zaixian +
      .caise-xiaoxi
    • - +
      - icon-Redhat + icon-云上传
      -
      .icon-shidi-redhat +
      .caise-yunshangchuan
    • - +
      - icon-bat + icon-观看
      -
      .icon-shidi-bat +
      .caise-guankan
    • - +
      - icon-shell + icon-定位
      -
      .icon-shidi-shell +
      .caise-dingwei
    • - +
      - icon-下线 + icon-已认证
      -
      .icon-shidi-xiaxian +
      .caise-yirenzheng
    • - +
      - icon-Ubuntu + icon-已连接
      -
      .icon-shidi-Ubuntu +
      .caise-yilianjie
    • - +
      - icon-Centos + icon-云下载
      -
      .icon-shidi-centos +
      .caise-yunxiazai
    • - +
      - icon-powershell-copy + icon-禁止观看
      -
      .icon-shidi-powershell +
      .caise-jinzhiguankan
    • - +
      - icon-在线 + icon-标签
      -
      .icon-xianxing-zaixian +
      .caise-biaoqian
    • - +
      - icon-Ubuntu + icon-用户切换
      -
      .icon-xianxing-Ubuntu +
      .caise-qiehuanyonghu
    • - +
      - icon-下线 + icon-删除用户
      -
      .icon-xianxing-xiaxian +
      .caise-shanchuyonghu
    • - +
      - icon-centos + icon-分支
      -
      .icon-xianxing-centos +
      .caise-fenzhi
    • - +
      - icon-redhat + icon-连接断开
      -
      .icon-xianxing-redhat +
      .caise-weilianjie
    • - +
      - icon-实数 + icon-Mac
      -
      .caise-shishu +
      .caise-Mac
    • - +
      - icon-文本 + icon-Oracle
      -
      .caise-wenben +
      .caise-oracle
    • - +
      - icon-json + icon-Java
      -
      .caise-json +
      .caise-Java
    • - +
      - icon-datetime + icon-Unix
      -
      .caise-datetime +
      .caise-unix
    • - +
      - icon-浮点数 + icon-python
      -
      .caise-fudianshu +
      .caise-python
    • - +
      - icon-time + icon-PHP
      -
      .caise-time +
      .caise-php
    • - +
      - icon-date + icon-Sybase
      -
      .caise-date +
      .caise-Sybase
    • - +
      - icon-浮点数 + icon-swift
      -
      .icon-shidi-fudianshu +
      .caise-swift
    • - +
      - icon-json + icon-mySQL
      -
      .icon-shidi-json +
      .caise-mySQL
    • - +
      - icon-time + icon-c++
      -
      .icon-shidi-time +
      .caise-c1
    • - +
      - icon-文本 + icon-Informix
      -
      .icon-shidi-wenben +
      .caise-informix
    • - +
      - icon-date + icon-access
      -
      .icon-shidi-date +
      .caise-access
    • - +
      - icon-datetime + icon-mongodb
      -
      .icon-shidi-datetime +
      .caise-mongodb
    • - +
      - icon-实数 + icon-PostgreSQL
      -
      .icon-shidi-shishu +
      .caise-PostgreSQL
    • - +
      - icon-time + icon-SQL Server
      -
      .icon-xianxing-time +
      .caise-SQLServer
    • - +
      - icon-date + icon-c#
      -
      .icon-xianxing-date +
      .caise-c2
    • - +
      - icon-浮点数 + icon-Linux
      -
      .icon-xianxing-fudianshu +
      .caise-Linux
    • - +
      - icon-文本 + icon-DB2
      -
      .icon-xianxing-wenben +
      .caise-DB2
    • - +
      - icon-实数 + icon-Windows
      -
      .icon-xianxing-shishu +
      .caise-Windows
    • - +
      - icon-datetime + icon-面积图
      -
      .icon-xianxing-datetime +
      .caise-mianjitu
    • - +
      - icon-json + icon-瀑布图
      -
      .icon-xianxing-json +
      .caise-pubutu
    • - +
      - ops-is_choice-disabled + icon-条形图
      -
      .ops-is_choice-disabled +
      .caise-tiaoxingtu
    • - +
      - ops-is_password-disabled + icon-箱型图
      -
      .ops-is_password-disabled +
      .caise-xiangxingtu
    • - +
      - ops-is_index-disabled + icon-上升
      -
      .ops-is_index-disabled +
      .caise-shangsheng
    • - +
      - ops-is_sortable-disabled + icon-下降
      -
      .ops-is_sortable-disabled +
      .caise-xiajiang
    • - +
      - ops-is_unique-disabled + icon-柱状图
      -
      .ops-is_unique-disabled +
      .caise-zhuzhuangtu
    • - +
      - ops-is_link-disabled + icon-基金图
      -
      .ops-is_link-disabled +
      .caise-jijintu
    • - +
      - ops-trigger + icon-点状图
      -
      .ops-trigger +
      .caise-dianzhuangtu
    • - +
      - ops-default_show-disabled + icon-热度图
      -
      .ops-default_show-disabled +
      .caise-redutu
    • - +
      - icon-添加 + icon-饼状图
      -
      .caise-tianjia +
      .caise-bingzhuangtu
    • - +
      - icon-说明 + icon-雷达图
      -
      .caise-shuoming +
      .caise-leidatu
    • - +
      - icon-暂停 + icon-滑块图
      -
      .caise-zanting +
      .caise-huakuaitu
    • - +
      - icon-确认 + icon-环形图
      -
      .caise-queren +
      .caise-huanxingtu
    • - +
      - icon-警告 + icon-折线图
      -
      .caise-jinggao +
      .caise-zhexiantu
    • - +
      - icon-减少 + icon-股票图
      -
      .caise-jianqu +
      .caise-gupiaotu
    • - +
      - icon-处理中 + icon-Mac
      -
      .caise-chulizhong +
      .icon-shidi-Mac
    • - +
      - icon-取消 + icon-unix
      -
      .caise-quxiao +
      .icon-shidi-unix
    • - +
      - icon-疑问 + icon-Windows
      -
      .caise-yiwen +
      .icon-shidi-Windows
    • - +
      - icon-禁止 + icon-SWIFT
      -
      .caise-jinzhi +
      .icon-shidi-swift
    • - +
      - icon-收藏 + icon-php
      -
      .caise-shoucang +
      .icon-shidi-php
    • - +
      - icon-维修中 + icon-Java
      -
      .caise-weixiu +
      .icon-shidi-Java
    • - +
      - icon-未认证 + icon-python
      -
      .caise-weirenzheng +
      .icon-shidi-python
    • - +
      - icon-二维码 + icon-c++
      -
      .caise-erweima +
      .icon-shidi-c1
    • - +
      - icon-用户 + icon-c#
      -
      .caise-yonghu +
      .icon-shidi-c2
    • - +
      - icon-报警 + icon-Linux
      -
      .caise-baojing +
      .icon-shidi-Linux
    • - +
      - icon-添加用户 + icon-mongodb
      -
      .caise-tianjiayonghu +
      .icon-shidi-mongodb
    • - +
      - icon-云数据 + icon-informix
      -
      .caise-yunshuju +
      .icon-shidi-informix
    • - +
      - icon-消息 + icon-Oracle
      -
      .caise-xiaoxi +
      .icon-shidi-oracle
    • - +
      - icon-云上传 + icon-Sybase
      -
      .caise-yunshangchuan +
      .icon-shidi-Sybase
    • - +
      - icon-观看 + icon-access
      -
      .caise-guankan +
      .icon-shidi-access
    • - +
      - icon-定位 + icon-PostgreSQL
      -
      .caise-dingwei +
      .icon-shidi-PostgreSQL
    • - +
      - icon-已认证 + icon-mySQL
      -
      .caise-yirenzheng +
      .icon-shidi-mySQL
    • - +
      - icon-已连接 + icon-DB2
      -
      .caise-yilianjie +
      .icon-shidi-DB2
    • - +
      - icon-云下载 + icon-SQL Server
      -
      .caise-yunxiazai +
      .icon-shidi-SQLServer
    • - +
      - icon-禁止观看 + icon-维修中
      -
      .caise-jinzhiguankan +
      .icon-shidi-weixiu
    • - +
      - icon-标签 + icon-已连接
      -
      .caise-biaoqian +
      .icon-shidi-yilianjie
    • - +
      - icon-用户切换 + icon-已认证
      -
      .caise-qiehuanyonghu +
      .icon-shidi-yirenzheng
    • - +
      - icon-删除用户 + icon-用户切换
      -
      .caise-shanchuyonghu +
      .icon-shidi-qiehuanyonghu
    • - +
      - icon-分支 + icon-云下载
      -
      .caise-fenzhi +
      .icon-shidi-yunxiazai
    • - +
      - icon-连接断开 + icon-云数据
      -
      .caise-weilianjie +
      .icon-shidi-yunshuju
    • - +
      - icon-Mac + icon-云上传
      -
      .caise-Mac +
      .icon-shidi-yunshangchuan
    • - +
      - icon-Oracle + icon-标签
      -
      .caise-oracle +
      .icon-shidi-biaoqian
    • - +
      - icon-Java + icon-二维码
      -
      .caise-Java +
      .icon-shidi-erweima
    • - +
      - icon-Unix + icon-消息
      -
      .caise-unix +
      .icon-shidi-xiaoxi
    • - +
      - icon-python + icon-用户
      -
      .caise-python +
      .icon-shidi-yonghu
    • - +
      - icon-PHP + icon-定位
      -
      .caise-php +
      .icon-shidi-dingwei
    • - +
      - icon-Sybase + icon-分支
      -
      .caise-Sybase +
      .icon-shidi-fenzhi
    • - +
      - icon-swift + icon-报警
      -
      .caise-swift +
      .icon-shidi-baojing
    • - +
      - icon-mySQL + icon-禁止观看
      -
      .caise-mySQL +
      .icon-shidi-jinzhiguankan
    • - +
      - icon-c++ + icon-观看
      -
      .caise-c1 +
      .icon-shidi-guankan
    • - +
      - icon-Informix + icon-添加用户
      -
      .caise-informix +
      .icon-shidi-tianjiayonghu
    • - +
      - icon-access + icon-删除用户
      -
      .caise-access +
      .icon-shidi-shanchuyonghu
    • - +
      - icon-mongodb + icon-收藏
      -
      .caise-mongodb +
      .icon-shidi-shoucang
    • - +
      - icon-PostgreSQL + icon-未认证
      -
      .caise-PostgreSQL +
      .icon-shidi-weirenzheng
    • - +
      - icon-SQL Server + icon-断开连接
      -
      .caise-SQLServer +
      .icon-shidi-weilianjie
    • - +
      - icon-c# + icon-滑块图
      -
      .caise-c2 +
      .icon-shidi-huakuaitu
    • - +
      - icon-Linux + icon-折现图
      -
      .caise-Linux +
      .icon-shidi-zhexiantu
    • - +
      - icon-DB2 + icon-面积图
      -
      .caise-DB2 +
      .icon-shidi-mianjitu
    • - +
      - icon-Windows + icon-雷达图
      -
      .caise-Windows +
      .icon-shidi-leidatu
    • - +
      - icon-面积图 + icon-基金图
      -
      .caise-mianjitu +
      .icon-shidi-jijintu
    • - +
      - icon-瀑布图 + icon-环形图
      -
      .caise-pubutu +
      .icon-shidi-huanxingtu
    • - +
      - icon-条形图 + icon-箱型图
      -
      .caise-tiaoxingtu +
      .icon-shidi-xiangxingtu
    • - +
      - icon-箱型图 + icon-条形图
      -
      .caise-xiangxingtu +
      .icon-shidi-tiaoxingtu
    • - +
      - icon-上升 + icon-热度图
      -
      .caise-shangsheng +
      .icon-shidi-redutu
    • - +
      - icon-下降 + icon-柱状图
      -
      .caise-xiajiang +
      .icon-shidi-zhuzhuangtu
    • - +
      - icon-柱状图 + icon-下降
      -
      .caise-zhuzhuangtu +
      .icon-shidi-xiajiang
    • - +
      - icon-基金图 + icon-上升
      -
      .caise-jijintu +
      .icon-shidi-shangsheng
    • - +
      - icon-点状图 + icon-股票图
      -
      .caise-dianzhuangtu +
      .icon-shidi-gupiaotu
    • - +
      - icon-热度图 + icon-瀑布图
      -
      .caise-redutu +
      .icon-shidi-pubutu
    • - +
      - icon-饼状图 + icon-点状图
      -
      .caise-bingzhuangtu +
      .icon-shidi-dianzhuangtu
    • - +
      - icon-雷达图 + icon-饼状图
      -
      .caise-leidatu +
      .icon-shidi-bingzhuangtu
    • - +
      - icon-滑块图 + icon-确认
      -
      .caise-huakuaitu +
      .icon-shidi-queren
    • - +
      - icon-环形图 + icon-说明
      -
      .caise-huanxingtu +
      .icon-shidi-shuoming
    • - +
      - icon-折线图 + icon-取消
      -
      .caise-zhexiantu +
      .icon-shidi-quxiao
    • - +
      - icon-股票图 + icon-禁止
      -
      .caise-gupiaotu +
      .icon-shidi-jinzhi
    • - +
      - icon-Mac + icon-暂停
      -
      .icon-shidi-Mac +
      .icon-shidi-zanting
    • - +
      - icon-unix + icon-减少
      -
      .icon-shidi-unix +
      .icon-shidi-jianqu
    • - +
      - icon-Windows + icon-疑问
      -
      .icon-shidi-Windows +
      .icon-shidi-yiwen
    • - +
      - icon-SWIFT + icon-警告
      -
      .icon-shidi-swift +
      .icon-shidi-jinggao
    • - +
      - icon-php + icon-处理中
      -
      .icon-shidi-php +
      .icon-shidi-chulizhong
    • - +
      - icon-Java + icon-添加
      -
      .icon-shidi-Java +
      .icon-shidi-tianjia
    • - +
      - icon-python + icon-access
      -
      .icon-shidi-python +
      .icon-xianxing-access
    • - +
      - icon-c++ + icon-Java
      -
      .icon-shidi-c1 +
      .icon-xianxing-Java
    • - +
      - icon-c# + icon-unix
      -
      .icon-shidi-c2 +
      .icon-xianxing-unix
    • - +
      - icon-Linux + icon-SWIFT
      -
      .icon-shidi-Linux +
      .icon-xianxing-swift
    • - +
      - icon-mongodb + icon-Mac
      -
      .icon-shidi-mongodb +
      .icon-xianxing-Mac
    • - +
      icon-informix
      -
      .icon-shidi-informix +
      .icon-xianxing-informix
    • - +
      - icon-Oracle + icon-c#
      -
      .icon-shidi-oracle +
      .icon-xianxing-c2
    • - +
      - icon-Sybase + icon-mySQL
      -
      .icon-shidi-Sybase +
      .icon-xianxing-mySQL
    • - +
      - icon-access + icon-Linux
      -
      .icon-shidi-access +
      .icon-xianxing-Linux
    • - +
      icon-PostgreSQL
      -
      .icon-shidi-PostgreSQL +
      .icon-xianxing-PostgreSQL
    • - +
      - icon-mySQL + icon-Sybase
      -
      .icon-shidi-mySQL +
      .icon-xianxing-Sybase
    • - +
      icon-DB2
      -
      .icon-shidi-DB2 +
      .icon-xianxing-DB2
    • - +
      icon-SQL Server
      -
      .icon-shidi-SQLServer +
      .icon-xianxing-SQLServer
    • - +
      - icon-维修中 + icon-c++
      -
      .icon-shidi-weixiu +
      .icon-xianxing-c1
    • - +
      - icon-已连接 + icon-python
      -
      .icon-shidi-yilianjie +
      .icon-xianxing-python
    • - +
      - icon-已认证 + icon-oracle
      -
      .icon-shidi-yirenzheng +
      .icon-xianxing-oracle
    • - +
      - icon-用户切换 + icon-php
      -
      .icon-shidi-qiehuanyonghu +
      .icon-xianxing-php
    • - +
      - icon-云下载 + icon-Windows
      -
      .icon-shidi-yunxiazai +
      .icon-xianxing-Windows
    • - +
      - icon-云数据 + icon-mongodb
      -
      .icon-shidi-yunshuju +
      .icon-xianxing-mongodb
    • - +
      - icon-云上传 + icon-瀑布图
      -
      .icon-shidi-yunshangchuan +
      .icon-xianxing-pubutu
    • - +
      - icon-标签 + icon-滑块图
      -
      .icon-shidi-biaoqian +
      .icon-xianxing-huakuaitu
    • - +
      - icon-二维码 + icon-条形图
      -
      .icon-shidi-erweima +
      .icon-xianxing-tiaoxingtu
    • - +
      - icon-消息 + icon-基金图
      -
      .icon-shidi-xiaoxi +
      .icon-xianxing-jijintu
    • - +
      - icon-用户 + icon-下降
      -
      .icon-shidi-yonghu +
      .icon-xianxing-xiajiang
    • - +
      - icon-定位 + icon-股票图
      -
      .icon-shidi-dingwei +
      .icon-xianxing-gupiaotu
    • - +
      - icon-分支 + icon-环形图
      -
      .icon-shidi-fenzhi +
      .icon-xianxing-huanxingtu
    • - +
      - icon-报警 + icon-箱型图
      -
      .icon-shidi-baojing +
      .icon-xianxing-xiangxingtu
    • - +
      - icon-禁止观看 + icon-柱状图
      -
      .icon-shidi-jinzhiguankan +
      .icon-xianxing-zhuzhuangtu
    • - +
      - icon-观看 + icon-饼状图
      -
      .icon-shidi-guankan +
      .icon-xianxing-bingzhuangtu
    • - +
      - icon-添加用户 + icon-折线图
      -
      .icon-shidi-tianjiayonghu +
      .icon-xianxing-zhexiantu
    • - +
      - icon-删除用户 + icon-雷达图
      -
      .icon-shidi-shanchuyonghu +
      .icon-xianxing-leidatu
    • - +
      - icon-收藏 + icon-点状图
      -
      .icon-shidi-shoucang +
      .icon-xianxing-dianzhuangtu
    • - +
      - icon-未认证 + icon-上升
      -
      .icon-shidi-weirenzheng +
      .icon-xianxing-shangsheng
    • - +
      - icon-断开连接 + icon-面积图
      -
      .icon-shidi-weilianjie +
      .icon-xianxing-mianjitu
    • - +
      - icon-滑块图 + icon-热度图
      -
      .icon-shidi-huakuaitu +
      .icon-xianxing-redutu
    • - +
      - icon-折现图 + icon-报警
      -
      .icon-shidi-zhexiantu +
      .icon-xianxing-baojing
    • - +
      - icon-面积图 + icon-观看
      -
      .icon-shidi-mianjitu +
      .icon-xianxing-guankan
    • - +
      - icon-雷达图 + icon-未认证
      -
      .icon-shidi-leidatu +
      .icon-xianxing-weirenzheng
    • - +
      - icon-基金图 + icon-删除用户
      -
      .icon-shidi-jijintu +
      .icon-xianxing-shanchuyonghu
    • - +
      - icon-环形图 + icon-用户切换
      -
      .icon-shidi-huanxingtu +
      .icon-xianxing-qiehuanyonghu
    • - +
      - icon-箱型图 + icon-用户
      -
      .icon-shidi-xiangxingtu +
      .icon-xianxing-yonghu
    • - +
      - icon-条形图 + icon-云下载
      -
      .icon-shidi-tiaoxingtu +
      .icon-xianxing-yunxiazai
    • - +
      - icon-热度图 + icon-云上传
      -
      .icon-shidi-redutu +
      .icon-xianxing-yunshangchuan
    • - +
      - icon-柱状图 + icon-维修中
      -
      .icon-shidi-zhuzhuangtu +
      .icon-xianxing-weixiu
    • - +
      - icon-下降 + icon-连接断开
      -
      .icon-shidi-xiajiang +
      .icon-xianxing-weilianjie
    • - +
      - icon-上升 + icon-禁止观看
      -
      .icon-shidi-shangsheng +
      .icon-xianxing-jinzhiguankan
    • - +
      - icon-股票图 + icon-定位
      -
      .icon-shidi-gupiaotu +
      .icon-xianxing-dingwei
    • - +
      - icon-瀑布图 + icon-添加用户
      -
      .icon-shidi-pubutu +
      .icon-xianxing-tianjiayonghu
    • - +
      - icon-点状图 + icon-云数据
      -
      .icon-shidi-dianzhuangtu +
      .icon-xianxing-yunshuju
    • - +
      - icon-饼状图 + icon-消息
      -
      .icon-shidi-bingzhuangtu +
      .icon-xianxing-xiaoxi
    • - +
      - icon-确认 + icon-标签
      -
      .icon-shidi-queren +
      .icon-xianxing-biaoqian
    • - +
      - icon-说明 + icon-二维码
      -
      .icon-shidi-shuoming +
      .icon-xianxing-erweima
    • - +
      - icon-取消 + icon-分支
      -
      .icon-shidi-quxiao +
      .icon-xianxing-fenzhi
    • - +
      - icon-禁止 + icon-收藏
      -
      .icon-shidi-jinzhi +
      .icon-xianxing-shoucang
    • - +
      - icon-暂停 + icon-已认证
      -
      .icon-shidi-zanting +
      .icon-xianxing-yirenzheng
    • - +
      - icon-减少 + icon-已连接
      -
      .icon-shidi-jianqu +
      .icon-xianxing-yilianjie
    • - +
      - icon-疑问 + icon-减去
      -
      .icon-shidi-yiwen +
      .icon-xianxing-jianqu
    • - +
      - icon-警告 + icon-添加
      -
      .icon-shidi-jinggao +
      .icon-xianxing-tianjia
    • - +
      - icon-处理中 + icon-禁止
      -
      .icon-shidi-chulizhong +
      .icon-xianxing-jinzhi
    • - +
      - icon-添加 + icon-确认
      -
      .icon-shidi-tianjia +
      .icon-xianxing-queren
    • - +
      - icon-access + icon-取消
      -
      .icon-xianxing-access +
      .icon-xianxing-quxiao
    • - +
      - icon-Java + icon-处理中
      -
      .icon-xianxing-Java +
      .icon-xianxing-chulizhong
    • - +
      - icon-unix + icon-暂停
      -
      .icon-xianxing-unix +
      .icon-xianxing-zanting
    • - +
      - icon-SWIFT + icon-说明
      -
      .icon-xianxing-swift +
      .icon-xianxing-shuoming
    • - +
      - icon-Mac + icon-警告
      -
      .icon-xianxing-Mac +
      .icon-xianxing-jinggao
    • - +
      - icon-informix + icon-疑问3
      -
      .icon-xianxing-informix +
      .icon-xianxing-yiwen
    • - +
      - icon-c# + ops-dag-dashboard
      -
      .icon-xianxing-c2 +
      .ops-dag-dashboard
    • - +
      - icon-mySQL + ops-dag-dashboard-selected
      -
      .icon-xianxing-mySQL +
      .ops-dag-dashboard-selected
    • - +
      - icon-Linux + ops-dag-applet-selected
      -
      .icon-xianxing-Linux +
      .ops-dag-applet-selected
    • - +
      - icon-PostgreSQL + ops-dag-applet
      -
      .icon-xianxing-PostgreSQL +
      .ops-dag-applet
    • - +
      - icon-Sybase + ops-dag-terminal
      -
      .icon-xianxing-Sybase +
      .ops-dag-terminal
    • - +
      - icon-DB2 + ops-dag-terminal-selected
      -
      .icon-xianxing-DB2 +
      .ops-dag-terminal-selected
    • - +
      - icon-SQL Server + ops-dag-cron
      -
      .icon-xianxing-SQLServer +
      .ops-dag-cron
    • - +
      - icon-c++ + ops-dag-cron-selected
      -
      .icon-xianxing-c1 +
      .ops-dag-cron-selected
    • - +
      - icon-python + ops-dag-history
      -
      .icon-xianxing-python +
      .ops-dag-history
    • - +
      - icon-oracle + ops-dag-history-selected
      -
      .icon-xianxing-oracle +
      .ops-dag-history-selected
    • - +
      - icon-php + ops-dag-dags-selected
      -
      .icon-xianxing-php +
      .ops-dag-dags-selected
    • - +
      - icon-Windows + ops-dag-dagreview
      -
      .icon-xianxing-Windows +
      .ops-dag-dagreview
    • - +
      - icon-mongodb + ops-dag-dagreview-selected
      -
      .icon-xianxing-mongodb +
      .ops-dag-dagreview-selected
    • - +
      - icon-瀑布图 + ops-dag-panel
      -
      .icon-xianxing-pubutu +
      .ops-dag-panel
    • - +
      - icon-滑块图 + ops-dag-panel-selected
      -
      .icon-xianxing-huakuaitu +
      .ops-dag-panel-selected
    • - +
      - icon-条形图 + ops-dag-variables
      -
      .icon-xianxing-tiaoxingtu +
      .ops-dag-variables
    • - +
      - icon-基金图 + ops-dag-variables-selected
      -
      .icon-xianxing-jijintu +
      .ops-dag-variables-selected
    • - +
      - icon-下降 + ops-dag-appletadmin
      -
      .icon-xianxing-xiajiang +
      .ops-dag-appletadmin
    • - +
      - icon-股票图 + ops-dag-appletadmin-selected
      -
      .icon-xianxing-gupiaotu +
      .ops-dag-appletadmin-selected
    • - +
      - icon-环形图 + ops-dag-dags
      -
      .icon-xianxing-huanxingtu +
      .ops-dag-dags
    • - +
      - icon-箱型图 + ops-dag-targetroute
      -
      .icon-xianxing-xiangxingtu +
      .ops-dag-targetroute
    • - +
      - icon-柱状图 + ops-dag-holiday
      -
      .icon-xianxing-zhuzhuangtu +
      .ops-dag-holiday
    • - +
      - icon-饼状图 + ops-dag-holiday-selected
      -
      .icon-xianxing-bingzhuangtu +
      .ops-dag-holiday-selected
    • - +
      - icon-折线图 + ops-dag-targetshortcut-selected
      -
      .icon-xianxing-zhexiantu +
      .ops-dag-targetshortcut-selected
    • - +
      - icon-雷达图 + ops-dag-targetroute-selected
      -
      .icon-xianxing-leidatu +
      .ops-dag-targetroute-selected
    • - +
      - icon-点状图 + ops-dag-targetshortcut
      -
      .icon-xianxing-dianzhuangtu +
      .ops-dag-targetshortcut
    • - +
      - icon-上升 + ops-dag-admingroup
      -
      .icon-xianxing-shangsheng +
      .ops-dag-admingroup
    • - +
      - icon-面积图 + ops-dag-admingroup-selected
      -
      .icon-xianxing-mianjitu +
      .ops-dag-admingroup-selected
    • - +
      - icon-热度图 + ops-admin
      -
      .icon-xianxing-redutu +
      .ops-admin
    • - +
      - icon-报警 + applet-untop
      -
      .icon-xianxing-baojing +
      .dag-applet-untop
    • - +
      - icon-观看 + applet-top
      -
      .icon-xianxing-guankan +
      .dag-applet-top
    • - +
      - icon-未认证 + 资源层级
      -
      .icon-xianxing-weirenzheng +
      .cmdb-tree
    • - +
      - icon-删除用户 + 资源数据
      -
      .icon-xianxing-shanchuyonghu +
      .cmdb-ci
    • - +
      - icon-用户切换 + 订阅
      -
      .icon-xianxing-qiehuanyonghu +
      .cmdb-preference-subscribe
    • - +
      - icon-用户 + 取消订阅
      -
      .icon-xianxing-yonghu +
      .cmdb-preference-cancel-subscribe
    • -
    • - -
      - icon-云下载 -
      -
      .icon-xianxing-yunxiazai -
      -
    • +
    +
    +

    font-class 引用

    +
    + +

    font-class 是 Unicode 使用方式的一种变种,主要是解决 Unicode 书写不直观,语意不明确的问题。

    +

    与 Unicode 使用方式相比,具有如下特点:

    +
      +
    • 相比于 Unicode 语意明确,书写更直观。可以很容易分辨这个 icon 是什么。
    • +
    • 因为使用 class 来定义图标,所以当要替换图标时,只需要修改 class 里面的 Unicode 引用。
    • +
    +

    使用步骤如下:

    +

    第一步:引入项目下面生成的 fontclass 代码:

    +
    <link rel="stylesheet" href="./iconfont.css">
    +
    +

    第二步:挑选相应图标并获取类名,应用于页面:

    +
    <span class="iconfont xxx"></span>
    +
    +
    +

    " + iconfont" 是你项目下的 font-family。可以通过编辑项目查看,默认是 "iconfont"。

    +
    +
    +
    +
    +
      + +
    • + +
      OAuth2.0
      +
      #OAUTH2
      +
    • + +
    • + +
      OIDC
      +
      #OIDC
      +
    • + +
    • + +
      cas
      +
      #CAS
      +
    • + +
    • + +
      setting-authentication
      +
      #ops-setting-auth
      +
    • + +
    • + +
      setting-authentication-selected
      +
      #ops-setting-auth-selected
      +
    • + +
    • + +
      itsm-knowledge (2)
      +
      #a-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
      +
    • -
    • - -
      - icon-云上传 -
      -
      .icon-xianxing-yunshangchuan -
      -
    • +
    • + +
      oneterm-download
      +
      #oneterm-download
      +
    • -
    • - -
      - icon-维修中 -
      -
      .icon-xianxing-weixiu -
      -
    • +
    • + +
      oneterm-command record
      +
      #oneterm-commandrecord
      +
    • -
    • - -
      - icon-连接断开 -
      -
      .icon-xianxing-weilianjie -
      -
    • +
    • + +
      oneterm-connected assets
      +
      #oneterm-asset
      +
    • -
    • - -
      - icon-禁止观看 -
      -
      .icon-xianxing-jinzhiguankan -
      -
    • +
    • + +
      oneterm-total assets
      +
      #oneterm-total_asset
      +
    • -
    • - -
      - icon-定位 -
      -
      .icon-xianxing-dingwei -
      -
    • +
    • + +
      oneterm-switch (3)
      +
      #oneterm-switch
      +
    • -
    • - -
      - icon-添加用户 -
      -
      .icon-xianxing-tianjiayonghu -
      -
    • +
    • + +
      oneterm-session
      +
      #oneterm-session
      +
    • -
    • - -
      - icon-云数据 -
      -
      .icon-xianxing-yunshuju -
      -
    • +
    • + +
      oneterm-connection
      +
      #oneterm-connect
      +
    • -
    • - -
      - icon-消息 -
      -
      .icon-xianxing-xiaoxi -
      -
    • +
    • + +
      oneterm-log in
      +
      #oneterm-login
      +
    • -
    • - -
      - icon-标签 -
      -
      .icon-xianxing-biaoqian -
      -
    • +
    • + +
      oneterm-dashboard
      +
      #ops-oneterm-dashboard
      +
    • -
    • - -
      - icon-二维码 -
      -
      .icon-xianxing-erweima -
      -
    • +
    • + +
      oneterm-dashboard-selected
      +
      #ops-oneterm-dashboard-selected
      +
    • -
    • - -
      - icon-分支 -
      -
      .icon-xianxing-fenzhi -
      -
    • +
    • + +
      oneterm-recent session
      +
      #oneterm-recentsession
      +
    • -
    • - -
      - icon-收藏 -
      -
      .icon-xianxing-shoucang -
      -
    • +
    • + +
      oneterm-my assets
      +
      #oneterm-myassets
      +
    • -
    • - -
      - icon-已认证 -
      -
      .icon-xianxing-yirenzheng -
      -
    • +
    • + +
      oneterm-log
      +
      #ops-oneterm-log
      +
    • -
    • - -
      - icon-已连接 -
      -
      .icon-xianxing-yilianjie -
      -
    • +
    • + +
      oneterm-conversation-selected
      +
      #ops-oneterm-session-selected
      +
    • -
    • - -
      - icon-减去 -
      -
      .icon-xianxing-jianqu -
      -
    • +
    • + +
      oneterm-conversation
      +
      #ops-oneterm-session
      +
    • -
    • - -
      - icon-添加 -
      -
      .icon-xianxing-tianjia -
      -
    • +
    • + +
      oneterm-log-selected
      +
      #ops-oneterm-log-selected
      +
    • -
    • - -
      - icon-禁止 -
      -
      .icon-xianxing-jinzhi -
      -
    • +
    • + +
      oneterm-assets
      +
      #ops-oneterm-assets
      +
    • -
    • - -
      - icon-确认 -
      -
      .icon-xianxing-queren -
      -
    • +
    • + +
      oneterm-assets-selected
      +
      #ops-oneterm-assets-selected
      +
    • -
    • - -
      - icon-取消 -
      -
      .icon-xianxing-quxiao -
      -
    • +
    • + +
      itsm-down
      +
      #itsm-down
      +
    • -
    • - -
      - icon-处理中 -
      -
      .icon-xianxing-chulizhong -
      -
    • +
    • + +
      itsm-up
      +
      #itsm-up
      +
    • + +
    • + +
      itsm-download
      +
      #itsm-download
      +
    • -
    • - -
      - icon-暂停 -
      -
      .icon-xianxing-zanting -
      -
    • +
    • + +
      itsm-print
      +
      #itsm-print
      +
    • -
    • - -
      - icon-说明 -
      -
      .icon-xianxing-shuoming -
      -
    • +
    • + +
      itsm-view
      +
      #itsm-view
      +
    • -
    • - -
      - icon-警告 -
      -
      .icon-xianxing-jinggao -
      -
    • +
    • + +
      itsm-word
      +
      #itsm-word
      +
    • -
    • - -
      - icon-疑问3 -
      -
      .icon-xianxing-yiwen -
      -
    • +
    • + +
      datainsight-custom
      +
      #datainsight-custom
      +
    • -
    • - -
      - ops-dag-dashboard -
      -
      .ops-dag-dashboard -
      -
    • +
    • + +
      datainsight-prometheus
      +
      #datainsight-prometheus
      +
    • -
    • - -
      - ops-dag-dashboard-selected -
      -
      .ops-dag-dashboard-selected -
      -
    • +
    • + +
      datainsight-zabbix
      +
      #datainsight-zabbix
      +
    • -
    • - -
      - ops-dag-applet-selected -
      -
      .ops-dag-applet-selected -
      -
    • +
    • + +
      setting-main people
      +
      #setting-mainpeople
      +
    • -
    • - -
      - ops-dag-applet -
      -
      .ops-dag-applet -
      -
    • +
    • + +
      setting-deputy people
      +
      #setting-deputypeople
      +
    • -
    • - -
      - ops-dag-terminal -
      -
      .ops-dag-terminal -
      -
    • +
    • + +
      ops-setting-duty
      +
      #ops-setting-duty
      +
    • -
    • - -
      - ops-dag-terminal-selected -
      -
      .ops-dag-terminal-selected -
      -
    • +
    • + +
      ops-setting-duty-selected
      +
      #ops-setting-duty-selected
      +
    • -
    • - -
      - ops-dag-cron -
      -
      .ops-dag-cron -
      -
    • +
    • + +
      datainsight-sequential
      +
      #datainsight-sequential
      +
    • -
    • - -
      - ops-dag-cron-selected -
      -
      .ops-dag-cron-selected -
      -
    • +
    • + +
      datainsight-close
      +
      #datainsight-close
      +
    • -
    • - -
      - ops-dag-history -
      -
      .ops-dag-history -
      -
    • +
    • + +
      datainsight-handle
      +
      #datainsight-handle
      +
    • -
    • - -
      - ops-dag-history-selected -
      -
      .ops-dag-history-selected -
      -
    • +
    • + +
      datainsight-table
      +
      #datainsight-table
      +
    • -
    • - -
      - ops-dag-dags-selected -
      -
      .ops-dag-dags-selected -
      -
    • +
    • + +
      icon-xianxing-password
      +
      #icon-xianxing-password
      +
    • -
    • - -
      - ops-dag-dagreview -
      -
      .ops-dag-dagreview -
      -
    • +
    • + +
      icon-xianxing-link
      +
      #icon-xianxing-link
      +
    • -
    • - -
      - ops-dag-dagreview-selected -
      -
      .ops-dag-dagreview-selected -
      -
    • +
    • + +
      itsm-oneclick download
      +
      #itsm-download-all
      +
    • -
    • - -
      - ops-dag-panel -
      -
      .ops-dag-panel -
      -
    • +
    • + +
      itsm-package download
      +
      #itsm-download-package
      +
    • -
    • - -
      - ops-dag-panel-selected -
      -
      .ops-dag-panel-selected -
      -
    • +
    • + +
      weixin
      +
      #a-Frame4
      +
    • -
    • - -
      - ops-dag-variables -
      -
      .ops-dag-variables -
      -
    • +
    • + +
      itsm-again
      +
      #itsm-again
      +
    • -
    • - -
      - ops-dag-variables-selected -
      -
      .ops-dag-variables-selected -
      -
    • +
    • + +
      itsm-next
      +
      #itsm-next
      +
    • + +
    • + +
      wechatApp
      +
      #wechatApp
      +
    • -
    • - -
      - ops-dag-appletadmin -
      -
      .ops-dag-appletadmin -
      -
    • +
    • + +
      robot
      +
      #robot
      +
    • -
    • - -
      - ops-dag-appletadmin-selected -
      -
      .ops-dag-appletadmin-selected -
      -
    • +
    • + +
      feishuApp
      +
      #feishuApp
      +
    • -
    • - -
      - ops-dag-dags -
      -
      .ops-dag-dags -
      -
    • +
    • + +
      dingdingApp
      +
      #dingdingApp
      +
    • -
    • - -
      - ops-dag-targetroute -
      -
      .ops-dag-targetroute -
      -
    • +
    • + +
      email
      +
      #email
      +
    • -
    • - -
      - ops-dag-holiday -
      -
      .ops-dag-holiday -
      -
    • +
    • + +
      setting-feishu
      +
      #ops-setting-notice-feishu
      +
    • -
    • - -
      - ops-dag-holiday-selected -
      -
      .ops-dag-holiday-selected -
      -
    • +
    • + +
      setting-feishu-selected
      +
      #ops-setting-notice-feishu-selected
      +
    • -
    • - -
      - ops-dag-targetshortcut-selected -
      -
      .ops-dag-targetshortcut-selected -
      -
    • +
    • + +
      cmdb-histogram
      +
      #cmdb-bar
      +
    • -
    • - -
      - ops-dag-targetroute-selected -
      -
      .ops-dag-targetroute-selected -
      -
    • +
    • + +
      cmdb-index
      +
      #cmdb-count
      +
    • -
    • - -
      - ops-dag-targetshortcut -
      -
      .ops-dag-targetshortcut -
      -
    • +
    • + +
      cmdb-piechart
      +
      #cmdb-pie
      +
    • -
    • - -
      - ops-dag-admingroup -
      -
      .ops-dag-admingroup -
      -
    • +
    • + +
      cmdb-line
      +
      #cmdb-line
      +
    • -
    • - -
      - ops-dag-admingroup-selected -
      -
      .ops-dag-admingroup-selected -
      -
    • +
    • + +
      cmdb-table
      +
      #cmdb-table
      +
    • -
    • - -
      - ops-admin -
      -
      .ops-admin -
      -
    • +
    • + +
      itsm-all
      +
      #itsm-all
      +
    • -
    • - -
      - applet-untop -
      -
      .dag-applet-untop -
      -
    • +
    • + +
      itsm-reply
      +
      #itsm-reply
      +
    • -
    • - -
      - applet-top -
      -
      .dag-applet-top -
      -
    • +
    • + +
      itsm-information
      +
      #itsm-information
      +
    • -
    • - -
      - 资源层级 -
      -
      .cmdb-tree -
      -
    • +
    • + +
      itsm-contact
      +
      #itsm-contact
      +
    • -
    • - -
      - 资源数据 -
      -
      .cmdb-ci -
      -
    • +
    • + +
      itsm-my-processed
      +
      #itsm-my-my_already_handle
      +
    • -
    • - -
      - 订阅 -
      -
      .cmdb-preference-subscribe -
      -
    • +
    • + +
      rule_7
      +
      #rule_7
      +
    • -
    • - -
      - 取消订阅 -
      -
      .cmdb-preference-cancel-subscribe -
      -
    • +
    • + +
      itsm-my-completed
      +
      #itsm-my-completed
      +
    • -
    -
    -

    font-class 引用

    -
    - -

    font-class 是 Unicode 使用方式的一种变种,主要是解决 Unicode 书写不直观,语意不明确的问题。

    -

    与 Unicode 使用方式相比,具有如下特点:

    -
      -
    • 相比于 Unicode 语意明确,书写更直观。可以很容易分辨这个 icon 是什么。
    • -
    • 因为使用 class 来定义图标,所以当要替换图标时,只需要修改 class 里面的 Unicode 引用。
    • -
    -

    使用步骤如下:

    -

    第一步:引入项目下面生成的 fontclass 代码:

    -
    <link rel="stylesheet" href="./iconfont.css">
    -
    -

    第二步:挑选相应图标并获取类名,应用于页面:

    -
    <span class="iconfont xxx"></span>
    -
    -
    -

    " - iconfont" 是你项目下的 font-family。可以通过编辑项目查看,默认是 "iconfont"。

    -
    -
    -
    -
    -
      +
    • + +
      itsm-my-plan
      +
      #itsm-my-plan
      +
    • 第二步:挑选相应图标并获取类名,应用于页面:
      itsm-node-strat
      -
      #itsm-node-strat
      +
      #itsm-node-start

    • @@ -12261,6 +14438,14 @@

      第二步:挑选相应图标并获取类名,应用于页面:#ops-dot

    +
  • + +
    ops-setting-notice-email-selected
    +
    #ops-setting-notice-email-selected-copy
    +
  • +
  • @@ -7,11 +7,15 @@ - - + + + + + diff --git a/cmdb-ui/src/components/CustomCodeMirror/index.vue b/cmdb-ui/src/components/CustomCodeMirror/index.vue index 17b9635d..4c412813 100644 --- a/cmdb-ui/src/components/CustomCodeMirror/index.vue +++ b/cmdb-ui/src/components/CustomCodeMirror/index.vue @@ -89,20 +89,23 @@ export default { }, }, data() { - const keyMapList = [ - { value: 'default', label: '默认' }, - { value: 'vim', label: 'vim' }, - { value: 'emacs', label: 'emacs' }, - { value: 'sublime', label: 'sublime' }, - ] return { - keyMapList, coder: null, fontSize: 14, keyMap: 'default', fullscreenExitVisible: false, } }, + computed: { + keyMapList() { + return [ + { value: 'default', label: this.$t('default') }, + { value: 'vim', label: 'vim' }, + { value: 'emacs', label: 'emacs' }, + { value: 'sublime', label: 'sublime' }, + ] + }, + }, mounted() {}, methods: { initCodeMirror(codeContent) { diff --git a/cmdb-ui/src/components/CustomIconSelect/constants.js b/cmdb-ui/src/components/CustomIconSelect/constants.js index 587436f8..cb49c52f 100644 --- a/cmdb-ui/src/components/CustomIconSelect/constants.js +++ b/cmdb-ui/src/components/CustomIconSelect/constants.js @@ -1,8 +1,10 @@ -export const iconTypeList = [ +import i18n from '@/lang' + +export const iconTypeList = () => [ // { value: '0', label: '常用' }, - { value: '1', label: '线性' }, - { value: '2', label: '实底' }, - { value: '3', label: '多色' } + { value: '1', label: i18n.t('customIconSelect.outlined') }, + { value: '2', label: i18n.t('customIconSelect.filled') }, + { value: '3', label: i18n.t('customIconSelect.multicolor') } ] export const commonIconList = ['changyong-ubuntu', diff --git a/cmdb-ui/src/components/CustomIconSelect/index.vue b/cmdb-ui/src/components/CustomIconSelect/index.vue index beac81fa..292b5d2a 100644 --- a/cmdb-ui/src/components/CustomIconSelect/index.vue +++ b/cmdb-ui/src/components/CustomIconSelect/index.vue @@ -15,31 +15,117 @@ > {{ item.label }} +
    + {{ this.$t('customIconSelect.custom') }} +
    + + {{ $t('add') }} +
    -
    -

    {{ category.label }}

    -
    - @@ -176,7 +346,7 @@ export default { padding: 4px 6px; } .custom-icon-select-popover-content { - max-height: 400px; + height: 400px; overflow: auto; .category { font-size: 14px; @@ -197,12 +367,43 @@ export default { padding: 5px 5px 2px 5px; margin: 0 2px 6px; color: #666; + position: relative; .custom-icon-select-popover-item-label { margin-top: 6px; font-size: 11px; + width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + text-align: center; } &:hover { background-color: #eeeeee; + .custom-icon-select-popover-content-img-box > i { + display: inline; + } + } + .custom-icon-select-popover-content-img-box { + width: 26px; + height: 26px; + display: flex; + align-items: center; + justify-content: center; + > img { + max-width: 26px; + max-height: 26px; + } + + > i { + display: none; + position: absolute; + top: 2px; + right: 2px; + font-size: 12px; + &:hover { + color: #2f54eb; + } + } } } .selected { @@ -212,6 +413,8 @@ export default { } .custom-icon-select-popover-icon-type { display: inline-block; + width: 100%; + position: relative; > div { cursor: pointer; display: inline-block; @@ -224,6 +427,16 @@ export default { .selected { border-color: #2f54eb; } + .ant-btn { + position: absolute; + right: 0; + top: 50%; + transform: translateY(-50%); + } + } + + .custom-icon-select-confirm-popover .ant-popover-inner-content { + width: 150px; } } @@ -234,15 +447,39 @@ export default { width: 28px; height: 28px; border-radius: 4px; - border: 1px solid #eeeeee; + border: 1px solid #d9d9d9; display: inline-block; cursor: pointer; - > i { + > i, + > img { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); + } + > img { + max-width: 26px; + max-height: 26px; + } + > i { font-size: 18px; } } +.custom-icon-select-form { + .custom-icon-select-form-img { + width: 28px; + height: 28px; + border-radius: 4px; + border: 1px solid #d9d9d9; + display: inline-flex; + margin-top: 5px; + justify-content: center; + align-items: center; + overflow: hidden; + img { + max-width: 26px; + max-height: 26px; + } + } +} diff --git a/cmdb-ui/src/components/EmployeeTransfer/index.vue b/cmdb-ui/src/components/EmployeeTransfer/index.vue index bdc87428..c09580fc 100644 --- a/cmdb-ui/src/components/EmployeeTransfer/index.vue +++ b/cmdb-ui/src/components/EmployeeTransfer/index.vue @@ -6,17 +6,17 @@ :flat="true" :multiple="true" :options="employeeTreeSelectOption" - placeholder="请输入搜索内容" + :placeholder="$t('placeholderSearch')" v-model="treeValue" :max-height="height - 50" noChildrenText="空" noOptionsText="空" :clearable="false" :always-open="true" - :default-expand-level="1" + :default-expand-level="showInternship ? 0 : 1" :class="{ 'employee-transfer': true, 'employee-transfer-has-input': !!inputValue }" @search-change="changeInputValue" - noResultsText="暂无数据" + :noResultsText="$t('noData')" openDirection="below" > @@ -85,6 +85,10 @@ export default { type: Boolean, default: false, }, + showInternship: { + type: Boolean, + default: false, + }, }, data() { return { @@ -99,13 +103,22 @@ export default { }, computed: { employeeTreeSelectOption() { - return formatOption( + const formatOptions = formatOption( this.allTreeDepAndEmp, 2, this.isDisabledAllCompany, this.uniqueKey || 'department_id', this.uniqueKey || 'employee_id' ) + if (this.showInternship) { + formatOptions.push( + ...[ + { id: -2, label: '全职' }, + { id: -3, label: '实习生' }, + ] + ) + } + return formatOptions }, allTreeDepAndEmp() { if (this.getDataBySelf) { @@ -148,11 +161,15 @@ export default { const department = [] const user = [] this.rightData.forEach((item) => { - const _split = item.split('-') - if (_split[0] === 'department') { - department.push(Number(_split[1])) + if (item === -2 || item === -3) { + department.push(item) } else { - user.push(Number(_split[1])) + const _split = item.split('-') + if (_split[0] === 'department') { + department.push(Number(_split[1])) + } else { + user.push(Number(_split[1])) + } } }) const _idx = department.findIndex((item) => item === 0) @@ -191,6 +208,12 @@ export default { } }, getLabel(id) { + if (id === -2) { + return '全职' + } + if (id === -3) { + return '实习生' + } const _split = id.split('-') const type = _split[0] const _id = Number(_split[1]) diff --git a/cmdb-ui/src/components/Menu/menu.js b/cmdb-ui/src/components/Menu/menu.js index db7cdd67..5fd203ad 100644 --- a/cmdb-ui/src/components/Menu/menu.js +++ b/cmdb-ui/src/components/Menu/menu.js @@ -81,21 +81,23 @@ export default { }, inject: ['reload'], methods: { - // 取消订阅 cancelAttributes(e, menu) { const that = this e.preventDefault() e.stopPropagation() this.$confirm({ - title: '警告', - content: `确认取消订阅 ${menu.meta.title}?`, + title: this.$t('alert'), + content: this.$t('cmdb.preference.confirmcancelSub2', { name: menu.meta.title }), onOk() { const citypeId = menu.meta.typeId const unsubCIType = subscribeCIType(citypeId, '') const unsubTree = subscribeTreeView(citypeId, '') Promise.all([unsubCIType, unsubTree]).then(() => { - that.$message.success('取消订阅成功') - // 删除路由 + that.$message.success(that.$t('cmdb.preference.cancelSubSuccess')) + const lastTypeId = window.localStorage.getItem('ops_ci_typeid') || undefined + if (Number(citypeId) === Number(lastTypeId)) { + localStorage.setItem('ops_ci_typeid', '') + } const href = window.location.href const hrefSplit = href.split('/') if (Number(hrefSplit[hrefSplit.length - 1]) === Number(citypeId)) { @@ -115,12 +117,10 @@ export default { }, // select menu item onOpenChange(openKeys) { - // 在水平模式下时执行,并且不再执行后续 if (this.mode === 'horizontal') { this.openKeys = openKeys return } - // 非水平模式时 const latestOpenKey = openKeys.find(key => !this.openKeys.includes(key)) if (!this.rootSubmenuKeys.includes(latestOpenKey)) { this.openKeys = openKeys @@ -157,6 +157,12 @@ export default { } return null }, + renderI18n(title) { + if (Object.prototype.toString.call(this.$t(`${title}`)) === '[object Object]') { + return title + } + return this.$t(`${title}`) + }, renderMenuItem(menu) { const isShowDot = menu.path.substr(0, 22) === '/cmdb/instances/types/' const isShowGrant = menu.path.substr(0, 20) === '/cmdb/relationviews/' @@ -166,9 +172,6 @@ export default { const attrs = { href: menu.meta.targetHref || menu.path, target: menu.meta.target } if (menu.children && menu.hideChildrenInMenu) { - // 把有子菜单的 并且 父菜单是要隐藏子菜单的 - // 都给子菜单增加一个 hidden 属性 - // 用来给刷新页面时, selectedKeys 做控制用 menu.children.forEach(item => { item.meta = Object.assign(item.meta, { hidden: true }) }) @@ -179,7 +182,7 @@ export default { {this.renderIcon({ icon: menu.meta.icon, customIcon: menu.meta.customIcon, name: menu.meta.name, typeId: menu.meta.typeId, routeName: menu.name, selectedIcon: menu.meta.selectedIcon, })} - 10 ? 'scroll' : ''}>{menu.meta.title} + 10 ? 'scroll' : ''}>{this.renderI18n(menu.meta.title)} {isShowDot && trigger} content={() =>
    -
    this.handlePerm(e, menu, 'CIType')} class="custom-menu-extra-submenu-item">授权
    -
    this.cancelAttributes(e, menu)} class="custom-menu-extra-submenu-item">取消订阅
    +
    this.handlePerm(e, menu, 'CIType')} class="custom-menu-extra-submenu-item">{ this.renderI18n('grant') }
    +
    this.cancelAttributes(e, menu)} class="custom-menu-extra-submenu-item">{ this.renderI18n('cmdb.preference.cancelSub') }
    } > @@ -213,7 +216,7 @@ export default { {this.renderIcon({ icon: menu.meta.icon, selectedIcon: menu.meta.selectedIcon, routeName: menu.name })} - {menu.meta.title} + {this.renderI18n(menu.meta.title)} {itemArr} @@ -222,6 +225,9 @@ export default { renderIcon({ icon, selectedIcon, customIcon = undefined, name = undefined, typeId = undefined, routeName }) { if (typeId) { if (customIcon) { + if (customIcon.split('$$')[2]) { + return + } return diff --git a/cmdb-ui/src/components/Pager/index.js b/cmdb-ui/src/components/Pager/index.js new file mode 100644 index 00000000..79690bb6 --- /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 00000000..da95a046 --- /dev/null +++ b/cmdb-ui/src/components/Pager/index.vue @@ -0,0 +1,138 @@ + + + + + + \ No newline at end of file diff --git a/cmdb-ui/src/components/RoleTransfer/index.vue b/cmdb-ui/src/components/RoleTransfer/index.vue index d08bf627..93c2680f 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/tools/UserMenu.vue b/cmdb-ui/src/components/tools/UserMenu.vue index 1ba3481e..9cc8566f 100644 --- a/cmdb-ui/src/components/tools/UserMenu.vue +++ b/cmdb-ui/src/components/tools/UserMenu.vue @@ -10,9 +10,10 @@ > + {{ locale === 'zh' ? 'English' : '中文' }} @@ -20,12 +21,12 @@
    - 个人中心 + {{ $t('topMenu.personalCenter') }}
    - 退出登录 + {{ $t('topMenu.logout') }}
    @@ -44,8 +45,9 @@ + diff --git a/cmdb-ui/src/config/app.js b/cmdb-ui/src/config/app.js index ebf79dc4..2633f9b3 100644 --- a/cmdb-ui/src/config/app.js +++ b/cmdb-ui/src/config/app.js @@ -2,8 +2,8 @@ const appConfig = { buildModules: ['cmdb', 'acl'], // 需要编译的模块 redirectTo: '/cmdb', // 首页的重定向路径 buildAclToModules: true, // 是否在各个应用下 内联权限管理 - ssoLogoutURL: '/api/sso/logout', showDocs: false, + useEncryption: false, } export default appConfig diff --git a/cmdb-ui/src/config/setting.js b/cmdb-ui/src/config/setting.js index 4b7b85b3..26595395 100644 --- a/cmdb-ui/src/config/setting.js +++ b/cmdb-ui/src/config/setting.js @@ -1,6 +1,5 @@ /** * 项目默认配置项 - * useSSO - 是否启用单点登录, 默认为否, 可以根据需要接入到公司的单点登录系统 * primaryColor - 默认主题色, 如果修改颜色不生效,请清理 localStorage * navTheme - sidebar theme ['dark', 'light'] 两种主题 * colorWeak - 色盲模式 @@ -15,8 +14,6 @@ */ export default { - useSSO: false, - ssoLoginUrl: '/api/sso/login', primaryColor: '#1890ff', // primary color of ant design navTheme: 'dark', // theme for nav menu layout: 'sidemenu', // nav menu position: sidemenu or topmenu diff --git a/cmdb-ui/src/core/use.js b/cmdb-ui/src/core/use.js index e91306e6..283889ab 100644 --- a/cmdb-ui/src/core/use.js +++ b/cmdb-ui/src/core/use.js @@ -38,11 +38,18 @@ import CardTitle from '@/components/CardTitle' import ElementUI from 'element-ui' import Treeselect from '@riophae/vue-treeselect' import OpsTable from '@/components/OpsTable' +import VueI18n from 'vue-i18n' +import i18n from '@/lang' Vue.config.productionTip = false Vue.prototype.$bus = EventBus + +VXETable.setup({ + i18n: (key, args) => i18n.t(key, args) +}) Vue.use(VXETable) VXETable.use(VXETablePluginExportXLSX) +Vue.use(VueI18n) Vue.config.productionTip = false @@ -75,4 +82,3 @@ Vue.component('CustomRadio', CustomRadio) Vue.component('CardTitle', CardTitle) Vue.component('Treeselect', Treeselect) Vue.component('OpsTable', OpsTable) - diff --git a/cmdb-ui/src/guard.js b/cmdb-ui/src/guard.js index cb77e648..5a70c30b 100644 --- a/cmdb-ui/src/guard.js +++ b/cmdb-ui/src/guard.js @@ -6,8 +6,8 @@ import store from './store' import NProgress from 'nprogress' import 'nprogress/nprogress.css' import { setDocumentTitle, domTitle } from '@/utils/domUtil' -import config from '@/config/setting' import { ACCESS_TOKEN } from './store/global/mutation-types' +import i18n from '@/lang' NProgress.configure({ showSpinner: false }) @@ -16,16 +16,16 @@ const whitePath = ['/user/login', '/user/logout', '/user/register', '/api/sso/lo // 此处不处理登录, 只处理 是否有用户信息的认证 前端permission的处理 axios处理401 -> 登录 // 登录页面处理处理 是否使用单点登录 -router.beforeEach((to, from, next) => { +router.beforeEach(async (to, from, next) => { NProgress.start() // start progress bar - to.meta && (!!to.meta.title && setDocumentTitle(`${to.meta.title} - ${domTitle}`)) + to.meta && (!!to.meta.title && setDocumentTitle(`${i18n.t(to.meta.title)} - ${domTitle}`)) const authed = store.state.authed - - + const auth_type = localStorage.getItem('ops_auth_type') if (whitePath.includes(to.path)) { next() - } else if ((config.useSSO || (!config.useSSO && Vue.ls.get(ACCESS_TOKEN))) && store.getters.roles.length === 0) { + } else if ((auth_type || (!auth_type && Vue.ls.get(ACCESS_TOKEN))) && store.getters.roles.length === 0) { + store.dispatch('GetAuthDataEnable') store.dispatch('GetInfo').then(res => { const roles = res.result && res.result.role store.dispatch("loadAllUsers") @@ -46,10 +46,17 @@ router.beforeEach((to, from, next) => { }).catch((e) => { setTimeout(() => { store.dispatch('Logout') }, 3000) }) - } else if (to.path === '/user/login' && !config.useSSO && store.getters.roles.length !== 0) { + } else if (to.path === '/user/login' && !auth_type && store.getters.roles.length !== 0) { next({ path: '/' }) - } else if (!config.useSSO && !Vue.ls.get(ACCESS_TOKEN) && to.path !== '/user/login') { - next({ path: '/user/login', query: { redirect: to.fullPath } }) + } else if (!auth_type && !Vue.ls.get(ACCESS_TOKEN) && to.path !== '/user/login') { + await store.dispatch('GetAuthDataEnable') + const { enable_list = [] } = store?.state?.user?.auth_enable ?? {} + const _enable_list = enable_list.filter(en => en.auth_type !== 'LDAP') + if (_enable_list.length === 1) { + next({ path: '/user/logout', query: { redirect: to.fullPath } }) + } else { + next({ path: '/user/login', query: { redirect: to.fullPath } }) + } } else { next() } diff --git a/cmdb-ui/src/lang/en.js b/cmdb-ui/src/lang/en.js new file mode 100644 index 00000000..d0a47590 --- /dev/null +++ b/cmdb-ui/src/lang/en.js @@ -0,0 +1,151 @@ +import cmdb_en from '@/modules/cmdb/lang/en.js' +import cs_en from '../views/setting/lang/en.js' +import acl_en from '@/modules/acl/lang/en.js' + +export default { + commonMenu: { + permission: 'Permission', + role: 'Roles', + resource: 'Resources', + resourceType: 'Resource Types', + trigger: 'Triggers', + }, + screen: 'Big Screen', + dashboard: 'Dashboard', + admin: 'Admin', + user: 'User', + role: 'Role', + operation: 'Operation', + login: 'Login', + refresh: 'Refresh', + cancel: 'Cancel', + confirm: 'Confirm', + create: 'Create', + edit: 'Edit', + deleting: 'Deleting', + deletingTip: 'Deleting, total of {total}, {successNum} succeeded, {errorNum} failed', + grant: 'Grant', + login_at: 'Login At', + logout_at: 'Logout At', + createSuccess: 'Create Success', + editSuccess: 'edit Success', + warning: 'Warning', + export: 'Export', + placeholderSearch: 'Please Search', + success: 'Success', + fail: 'Fail', + browser: 'Browser', + status: 'Status', + type: 'Type', + description: 'Description', + new: 'New', + add: 'Add', + define: 'Define', + update: 'Update', + clear: 'Clear', + delete: 'Delete', + copy: 'Copy', + created_at: 'Created At', + updated_at: 'Updated At', + placeholder1: 'Please Input', + placeholder2: 'Please Select', + confirmDelete: 'Confirm delete?', + confirmDelete2: 'Confirm delete [{name}]?', + query: 'Query', + search: 'Search', + hide: 'Hide', + expand: 'Expand', + save: 'Save', + submit: 'Submit', + upload: 'Import', + download: 'Export', + name: 'Name', + alias: 'Alias', + desc: 'Description', + other: 'Other', + icon: 'Icon', + addSuccess: 'Added successfully', + uploadSuccess: 'Import successfully', + saveSuccess: 'Save successfully', + copySuccess: 'Copy successfully', + updateSuccess: 'Updated successfully', + deleteSuccess: 'Deleted successfully', + operateSuccess: 'The operation was successful', + noPermission: 'No Permission', + noData: 'No Data', + seconds: 'Seconds', + createdAt: 'Created At', + updatedAt: 'Updated At', + deletedAt: 'Deleted At', + required: 'required', + email: 'Email', + wechat: 'Wechat', + dingding: 'DingTalk', + feishu: 'Feishu', + bot: 'Robot', + checkAll: 'Select All', + loading: 'Loading...', + view: 'View', + reset: 'Reset', + yes: 'Yes', + no: 'No', + all: 'All', + selectRows: 'Selected: {rows} items', + itemsPerPage: '/page', + '星期一': 'Monday', + '星期二': 'Tuesday', + '星期三': 'Wednesday', + '星期四': 'Thursday', + '星期五': 'Friday', + '星期六': 'Saturday', + '星期日': 'Sunday', + hour: 'hour', + 'items/page': '{items} items/page', + max: 'Max', + min: 'Min', + visual: 'Visual', + default: 'default', + tip: 'Tip', + pagination: { + total: '{range0}-{range1} of {total} items' + }, + topMenu: { + personalCenter: 'My Profile', + logout: 'Logout', + confirmLogout: 'Are you sure to log out?' + }, + cmdbFilterComp: { + conditionFilter: 'Conditional filtering', + and: 'and', + or: 'or', + is: 'equal', + '~is': 'not equal', + contain: 'contain', + '~contain': 'not contain', + start_with: 'start_with', + '~start_with': 'not start_with', + end_with: 'end_with', + '~end_with': 'not end_with', + '~value': 'null', + value: 'not null', + in: 'in', + '~in': 'not in', + range: 'range', + '~range': 'out of range', + compare: 'compare', + addHere: 'Add Here', + split: 'split by {separator}' + }, + customIconSelect: { + outlined: 'Outlined', + filled: 'Filled', + multicolor: 'Multicolor', + custom: 'Custom', + preview: 'Preview', + sizeLimit: 'The image size cannot exceed 2MB!', + nodata: 'There are currently no custom icons available. Click here to upload' + }, + cmdb: cmdb_en, + cs: cs_en, + acl: acl_en, +} diff --git a/cmdb-ui/src/lang/index.js b/cmdb-ui/src/lang/index.js new file mode 100644 index 00000000..d8ece0eb --- /dev/null +++ b/cmdb-ui/src/lang/index.js @@ -0,0 +1,18 @@ +import VueI18n from 'vue-i18n' +import zh from './zh' +import en from './en' +import Vue from 'vue' +import zhCN from 'vxe-table/lib/locale/lang/zh-CN' +import enUS from 'vxe-table/lib/locale/lang/en-US' + +Vue.use(VueI18n) +const i18n = new VueI18n({ + locale: 'zh', // 初始化中文 + messages: { + 'zh': { ...zh, ...zhCN }, + 'en': { ...en, ...enUS }, + }, + silentTranslationWarn: true +}) + +export default i18n diff --git a/cmdb-ui/src/lang/zh.js b/cmdb-ui/src/lang/zh.js new file mode 100644 index 00000000..b31e5558 --- /dev/null +++ b/cmdb-ui/src/lang/zh.js @@ -0,0 +1,151 @@ +import cmdb_zh from '@/modules/cmdb/lang/zh.js' +import cs_zh from '../views/setting/lang/zh.js' +import acl_zh from '@/modules/acl/lang/zh.js' + +export default { + commonMenu: { + permission: '权限管理', + role: '角色管理', + resource: '资源管理', + resourceType: '资源类型', + trigger: '触发器', + }, + screen: '大屏', + dashboard: '仪表盘', + admin: '管理员', + user: '用户', + role: '角色', + operation: '操作', + login: '登录', + refresh: '刷新', + cancel: '取消', + confirm: '确定', + create: '创建', + edit: '编辑', + deleting: '正在删除', + deletingTip: '正在删除,共{total}个,成功{successNum}个,失败{errorNum}个', + grant: '授权', + login_at: '登录时间', + logout_at: '登出时间', + createSuccess: '创建成功', + editSuccess: '修改成功', + warning: '警告', + export: '导出', + placeholderSearch: '请查找', + success: '成功', + fail: '失败', + browser: '浏览器', + status: '状态', + type: '类型', + description: '描述', + new: '新增', + add: '添加', + define: '定义', + update: '修改', + clear: '清空', + delete: '删除', + copy: '复制', + created_at: '创建日期', + updated_at: '更新日期', + placeholder1: '请输入', + placeholder2: '请选择', + confirmDelete: '确认删除?', + confirmDelete2: '确认删除【{name}】?', + query: '查询', + search: '搜索', + hide: '隐藏', + expand: '展开', + save: '保存', + submit: '提交', + upload: '导入', + download: '导出', + name: '名称', + alias: '别名', + desc: '描述', + other: '其他', + icon: '图标', + addSuccess: '新增成功', + uploadSuccess: '导入成功', + saveSuccess: '保存成功', + copySuccess: '复制成功', + updateSuccess: '更新成功', + deleteSuccess: '删除成功', + operateSuccess: '操作成功', + noPermission: '权限不足', + noData: '暂无数据', + seconds: '秒', + createdAt: '创建时间', + updatedAt: '更新时间', + deletedAt: '删除时间', + required: '必须', + email: '邮件', + wechat: '企业微信', + dingding: '钉钉', + feishu: '飞书', + bot: '机器人', + checkAll: '全选', + loading: '加载中...', + view: '查看', + reset: '重置', + yes: '是', + no: '否', + all: '全部', + selectRows: '选取:{rows} 项', + itemsPerPage: '/页', + '星期一': '星期一', + '星期二': '星期二', + '星期三': '星期三', + '星期四': '星期四', + '星期五': '星期五', + '星期六': '星期六', + '星期日': '星期日', + hour: '小时', + 'items/page': '{items} 条/页', + max: '最大值', + min: '最小值', + visual: '虚拟', + default: '默认', + tip: '提示', + pagination: { + total: '当前展示 {range0}-{range1} 条数据, 共 {total} 条' + }, + topMenu: { + personalCenter: '个人中心', + logout: '退出登录', + confirmLogout: '确认退出登录吗?' + }, + cmdbFilterComp: { + conditionFilter: '条件过滤', + and: '与', + or: '或', + is: '等于', + '~is': '不等于', + contain: '包含', + '~contain': '不包含', + start_with: '以...开始', + '~start_with': '不以...开始', + end_with: '以...结束', + '~end_with': '不以...结束', + '~value': '为空', + value: '不为空', + in: 'in查询', + '~in': '非in查询', + range: '范围', + '~range': '范围外', + compare: '比较', + addHere: '在此处添加', + split: '以 {separator} 分隔' + }, + customIconSelect: { + outlined: '线框', + filled: '实底', + multicolor: '多色', + custom: '自定义', + preview: '预览', + sizeLimit: '图片大小不可超过2MB!', + nodata: '暂无自定义图标,点击此处上传' + }, + cmdb: cmdb_zh, + cs: cs_zh, + acl: acl_zh, +} diff --git a/cmdb-ui/src/main.js b/cmdb-ui/src/main.js index 04139168..aa7bc8ac 100644 --- a/cmdb-ui/src/main.js +++ b/cmdb-ui/src/main.js @@ -10,6 +10,7 @@ import './guard' // guard permission control import './utils/filter' // global filter import Setting from './config/setting' import { Icon } from 'ant-design-vue' +import i18n from './lang' import iconFont from '../public/iconfont/iconfont' @@ -22,6 +23,7 @@ async function start() { const _vue = new Vue({ router, store, + i18n, created: bootstrap, render: h => h(App) }).$mount('#app') diff --git a/cmdb-ui/src/modules/acl/lang/en.js b/cmdb-ui/src/modules/acl/lang/en.js new file mode 100644 index 00000000..ddde5a85 --- /dev/null +++ b/cmdb-ui/src/modules/acl/lang/en.js @@ -0,0 +1,125 @@ +const acl_en = { + date: 'Date', + operator: 'Operator', + resource: 'Resource', + resourceType: 'Resource Type', + addResourceType: 'Add Resource Type', + app: 'App', + operateTime: 'Operate Time', + permission: 'Permission', + permission_placeholder: 'please select permission', + permissionList: 'Permission List', + summaryPermissions: 'Summary of permissions', + source: 'Source', + username: 'Username', + username_placeholder: 'please input username', + userList: 'User List', + groupUser: 'Group User', + addUser: 'Add User', + subordinateUsers: 'Subordinate Users', + nickname: 'Nickname', + nickname_placeholder: 'please input nickname', + password: 'Password', + password_placeholder: 'please input password', + department: 'Department', + group: 'Group', + email: 'Email', + email_placeholder: 'please input email', + mobile: 'Mobile', + isBlock: 'Is Block', + block: 'Block', + joined_at: 'Joined At', + role: 'Role', + role_placeholder1: 'please input role', + role_placeholder2: 'please select role', + role_placeholder3: 'please select a role name, multiple choices are allowed', + allRole: 'All Roles', + visualRole: 'Virtual Role', + addVisualRole: 'Add Virtual Role', + inheritedFrom: 'Inherited from', + heir: 'Inherit Roles', + permissionChange: 'Permissions', + roleChange: 'Roles', + resourceChange: 'Resources', + resourceTypeChange: 'Resource Type', + trigger: 'Triggers', + triggerNameInput: 'Please enter trigger name', + triggerChange: 'Triggers', + roleManage: 'Roles', + userManage: 'Users', + appManage: 'Applications', + resourceManage: 'Resources', + history: 'Audits', + userSecret: 'Secrets', + none: 'none', + danger: 'Dangerous', + confirmDeleteApp: 'Are you sure you want to delete this app?', + revoke: 'Revoke', + convenient: 'Quick Grant', + group2: 'Group', + groupName: 'Group Name', + resourceName: 'Resource Name', + creator: 'Creator', + member: 'Members', + viewAuth: 'view Auth', + addTypeTips: 'There is no type information yet, please add the resource type first!', + addResource: 'Add Resource', + resourceList: 'Resource List', + confirmResetSecret: 'Are you sure you want to reset the user secrets?', + addTrigger: 'Add Trigger', + deleteTrigger: 'Delete Trigger', + applyTrigger: 'Apply Trigger', + cancelTrigger: 'Cancel Trigger', + enable: 'Enable', + disable: 'Disable', + viewMatchResult: 'View regular matching results', + confirmDeleteTrigger: 'Are you sure you want to delete this trigger?', + ruleApply: 'Apply', + triggerTip1: 'Are you sure you want to apply this trigger?', + triggerTip2: 'Cancel applying this trigger?', + appNameInput: 'Please enter an application name', + descInput: 'Please enter a description', + addApp: 'Add', + updateApp: 'Update', + cancel: 'Cancel', + typeName: 'Name', + typeNameInput: 'Please enter a type name', + resourceNameInput: 'Please enter resource name', + pressEnter: 'Press Enter to confirm filtering', + groupMember: 'Group Members:', + isGroup: 'Group?', + errorTips: 'Error message', + roleList: 'Role List', + virtual: 'Virtual', + resourceBatchTips: 'Please enter the resource name, separated by newlines', + memberManage: 'Members: ', + newResource: 'New Resource: ', + deleteResource: 'Delete Resource: ', + deleteResourceType: 'Delete Resource Type: ', + noChange: 'No change', + batchOperate: 'Batch Operations', + batchGrant: 'Batch Grant', + batchRevoke: 'Batch Revoke', + editPerm: 'Add authorization: ', + permInput: 'Please enter permission name', + resourceTypeName: 'Resource Type Name', + selectedParents: 'Optionally inherit roles', + isAppAdmin: 'is app admin', + addRole: 'Add Role', + roleRelation: 'Role Relation', + roleRelationAdd: 'Add Role Relation', + roleRelationDelete: 'Delete Role Relation', + role2: 'Role', + admin: 'Admin', + involvingRP: 'Involving resources and permissions', + startAt: 'Start Time', + endAt: 'End Time', + triggerTips1: 'Priority regular pattern (secondary wildcard)', + pleaseSelectType: 'Please select resource type', + apply: 'Apply', + mobileTips: 'Please enter the correct phone number', + remove: 'Remove', + deleteUserConfirm: 'Are you sure you want to remove this user?', + copyResource: 'Copy resource name' +} +export default acl_en diff --git a/cmdb-ui/src/modules/acl/lang/zh.js b/cmdb-ui/src/modules/acl/lang/zh.js new file mode 100644 index 00000000..626911fd --- /dev/null +++ b/cmdb-ui/src/modules/acl/lang/zh.js @@ -0,0 +1,125 @@ +const acl_zh = { + date: '日期', + operator: '操作员', + resource: '资源', + resourceType: '资源类型', + addResourceType: '新增资源类型', + app: '应用', + operateTime: '操作时间', + permission: '权限', + permission_placeholder: '请选择权限', + permissionList: '权限列表', + summaryPermissions: '权限汇总', + source: '来源', + username: '用户名', + username_placeholder: '请输入用户名', + userList: '用户列表', + groupUser: '组用户', + addUser: '新增用户', + subordinateUsers: '下属用户', + nickname: '中文名', + nickname_placeholder: '请输入中文名', + password: '密码', + password_placeholder: '请输入密码', + department: '部门', + group: '小组', + email: '邮箱', + email_placeholder: '请输入邮箱', + mobile: '手机号', + isBlock: '是否锁定', + block: '锁定', + joined_at: '加入时间', + role: '角色名', + role_placeholder1: '请输入角色名', + role_placeholder2: '请选择角色名称', + role_placeholder3: '请选择角色名称,可多选', + allRole: '所有角色', + visualRole: '虚拟角色', + addVisualRole: '新增虚拟角色', + inheritedFrom: '继承自', + heir: '继承者', + permissionChange: '权限变更', + roleChange: '角色变更', + resourceChange: '资源变更', + resourceTypeChange: '资源类型变更', + trigger: '触发器', + triggerNameInput: '请输入触发器名', + triggerChange: '触发器变更', + roleManage: '角色管理', + userManage: '用户管理', + appManage: '应用管理', + resourceManage: '资源管理', + history: '操作审计', + userSecret: '用户密钥', + none: '无', + danger: '危险操作', + confirmDeleteApp: '确定要删除该App吗?', + revoke: '权限回收', + convenient: '便捷授权', + group2: '组', + groupName: '资源组名', + resourceName: '资源名', + creator: '创建者', + member: '成员', + viewAuth: '查看授权', + addTypeTips: '暂无类型信息,请先添加资源类型!', + addResource: '新增资源', + resourceList: '资源列表', + confirmResetSecret: '确定重置用户密钥?', + addTrigger: '新增触发器', + deleteTrigger: '删除触发器', + applyTrigger: '应用触发器', + cancelTrigger: '取消触发器', + enable: '启用', + disable: '禁用', + viewMatchResult: '查看正则匹配结果', + confirmDeleteTrigger: '确认删除该触发器吗?', + ruleApply: '规则应用', + triggerTip1: '是否确定应用该触发器?', + triggerTip2: '是否取消应用该触发器?', + appNameInput: '请输入应用名称', + descInput: '请输入描述', + addApp: '创建应用', + updateApp: '更新应用', + cancel: '撤销', + typeName: '类型名', + typeNameInput: '请输入类型名', + resourceNameInput: '请输入资源名', + pressEnter: '按回车确认筛选', + groupMember: '组成员:', + isGroup: '是否组', + errorTips: '错误提示', + roleList: '角色列表', + virtual: '虚拟', + resourceBatchTips: '请输入资源名,换行分隔', + memberManage: '成员管理:', + newResource: '新建资源:', + deleteResource: '删除资源:', + deleteResourceType: '删除资源类型:', + noChange: '没有修改', + batchOperate: '批量操作', + batchGrant: '批量授权', + batchRevoke: '批量权限回收', + editPerm: '添加授权:', + permInput: '请输入权限名', + resourceTypeName: '资源类型名', + selectedParents: '可选择继承角色', + isAppAdmin: '是否应用管理员', + addRole: '新增角色', + roleRelation: '角色关系', + roleRelationAdd: '添加角色关系', + roleRelationDelete: '删除角色关系', + role2: '角色', + admin: '管理员', + involvingRP: '涉及资源及权限', + startAt: '开始时间', + endAt: '结束时间', + triggerTips1: '优先正则模式(次通配符)', + pleaseSelectType: '请选择资源类型', + apply: '应用', + mobileTips: '请输入正确的手机号码', + remove: '移除', + deleteUserConfirm: '是否确定要移除该用户', + copyResource: '复制资源名' +} +export default acl_zh diff --git a/cmdb-ui/src/modules/acl/router/index.js b/cmdb-ui/src/modules/acl/router/index.js index 214af80d..7e125cea 100644 --- a/cmdb-ui/src/modules/acl/router/index.js +++ b/cmdb-ui/src/modules/acl/router/index.js @@ -12,35 +12,35 @@ const genAppRoute = ({ name }) => { name: `${name}_roles_acl`, hideChildrenInMenu: true, component: () => import('../views/roles'), - meta: { title: '角色管理', icon: 'team', keepAlive: true } + meta: { title: 'acl.roleManage', icon: 'team', keepAlive: true } }, { path: `/acl/${name}/resources`, name: `${name}_resources_acl`, hideChildrenInMenu: true, component: () => import('../views/resources'), - meta: { title: '资源管理', icon: 'credit-card', keepAlive: false } + meta: { title: 'acl.resourceManage', icon: 'credit-card', keepAlive: false } }, { path: `/acl/${name}/resource_types`, name: `${name}_resource_types_acl`, hideChildrenInMenu: true, component: () => import('../views/resource_types'), - meta: { title: '资源类型', icon: 'file-text', keepAlive: true } + meta: { title: 'acl.resourceType', icon: 'file-text', keepAlive: true } }, { path: `/acl/${name}/trigger`, name: `${name}_trigger_acl`, hideChildrenInMenu: true, component: () => import('../views/trigger'), - meta: { title: '触发器', icon: 'clock-circle', keepAlive: true } + meta: { title: 'acl.trigger', icon: 'clock-circle', keepAlive: true } }, { path: `/acl/${name}/history`, name: `${name}_history_acl`, hideChildrenInMenu: true, component: () => import('../views/history'), - meta: { title: '操作审计', icon: 'search', keepAlive: false } + meta: { title: 'acl.history', icon: 'search', keepAlive: false } } ] } @@ -59,32 +59,31 @@ const genAclRoutes = async () => { path: `/acl/secret_key`, name: 'acl_secret_key', component: () => import('../views/secretKey'), - meta: { title: '用户密钥', icon: 'key' } + meta: { title: 'acl.userSecret', icon: 'key' } }, { path: `/acl/operate_history`, name: 'acl_operate_history', component: () => import('../views/operation_history/index.vue'), - // meta: { title: '操作审计', icon: 'search', permission: ['acl_admin'] }, - meta: { title: '操作审计', icon: 'search' } + meta: { title: 'acl.history', icon: 'search', permission: ['acl_admin'] } }, { path: `/acl/user`, name: 'acl_user', component: () => import('../views/users'), - meta: { title: '用户管理', icon: 'user', permission: ['acl_admin'] } + meta: { title: 'acl.userManage', icon: 'user', permission: ['acl_admin'] } }, { path: `/acl/roles`, name: `acl_roles`, component: () => import('../views/roles'), - meta: { title: '角色管理', icon: 'team', keepAlive: true, permission: ['acl_admin'] } + meta: { title: 'acl.roleManage', icon: 'team', keepAlive: true, permission: ['acl_admin'] } }, { path: `/acl/apps`, name: 'acl_apps', component: () => import('../views/apps'), - meta: { title: '应用管理', icon: 'appstore', permission: ['acl_admin'] } + meta: { title: 'acl.appManage', icon: 'appstore', permission: ['acl_admin'] } } ] } diff --git a/cmdb-ui/src/modules/acl/views/apps.vue b/cmdb-ui/src/modules/acl/views/apps.vue index b6f7b348..ff7cf896 100644 --- a/cmdb-ui/src/modules/acl/views/apps.vue +++ b/cmdb-ui/src/modules/acl/views/apps.vue @@ -11,7 +11,7 @@ :xs="24"> -
    {{ app.description || '无' }}
    +
    {{ app.description || $t('none') }}
    {{ app.name[0].toUpperCase() }}
    - + - @@ -122,33 +77,6 @@ export default { resourceTypes: [], resourceTypePerms: [], resources: [], - // tableColumns: [ - // { - // title: '资源名', - // dataIndex: 'name', - // sorter: false, - // width: 150, - // // scopedSlots: { - // // customRender: 'nameSearchRender', - // // filterDropdown: 'filterDropdown', - // // filterIcon: 'filterIcon' - // // }, - // // onFilter: (value, record) => record.name && record.name.toLowerCase().includes(value.toLowerCase()), - // // onFilterDropdownVisibleChange: (visible) => { - // // if (visible) { - // // setTimeout(() => { - // // this.searchInput.focus() - // // }, 0) - // // } - // // } - // }, - // { - // title: '权限列表', - // dataIndex: 'permissions', - // width: 300, - // scopedSlots: { customRender: 'permissions' }, - // }, - // ], columnSearchText: { name: '', }, @@ -156,14 +84,14 @@ export default { }, computed: { ...mapState({ - windowHeight: state => state.windowHeight, + windowHeight: (state) => state.windowHeight, }), displayApps() { const roles = this.$store.state.user.roles.permissions if (roles.includes('acl_admin')) { return this.apps } - return this.apps.filter(item => { + return this.apps.filter((item) => { if (roles.includes(`${item.name}_admin`)) { return true } @@ -192,7 +120,7 @@ export default { }, async loadRoles(_appId) { const res = await searchRole({ app_id: _appId, page_size: 9999, is_all: true }) - this.roles = res.roles.filter(item => item.uid) + this.roles = res.roles.filter((item) => item.uid) }, async handleSwitchApp(appId) { this.currentAppId = appId @@ -218,7 +146,7 @@ export default { }, async loadResource() { this.spinning = true - const fil = this.roles.filter(role => role.uid === this.user.uid) + const fil = this.roles.filter((role) => role.uid === this.user.uid) if (!fil[0]) { return } diff --git a/cmdb-ui/src/modules/acl/views/module/permissionForm.vue b/cmdb-ui/src/modules/acl/views/module/permissionForm.vue index 59eb25a9..6c1eb6e3 100644 --- a/cmdb-ui/src/modules/acl/views/module/permissionForm.vue +++ b/cmdb-ui/src/modules/acl/views/module/permissionForm.vue @@ -1,29 +1,33 @@ - + diff --git a/cmdb-ui/src/modules/cmdb/views/ci_types/triggerForm.vue b/cmdb-ui/src/modules/cmdb/views/ci_types/triggerForm.vue index 9125d6d4..dac013c0 100644 --- a/cmdb-ui/src/modules/cmdb/views/ci_types/triggerForm.vue +++ b/cmdb-ui/src/modules/cmdb/views/ci_types/triggerForm.vue @@ -1,59 +1,289 @@ - + + + diff --git a/cmdb-ui/src/modules/cmdb/views/ci_types/triggerTable.vue b/cmdb-ui/src/modules/cmdb/views/ci_types/triggerTable.vue index 0bbc0a19..501270b4 100644 --- a/cmdb-ui/src/modules/cmdb/views/ci_types/triggerTable.vue +++ b/cmdb-ui/src/modules/cmdb/views/ci_types/triggerTable.vue @@ -7,11 +7,10 @@ size="small" class="ops-button-primary" icon="plus" - >新增触发器{{ $t('cmdb.ciType.newTrigger') }} - {{ tips }}
    - - - - - + + + - + - - - - - + - +
    @@ -106,14 +261,28 @@ export default { width: 100%; height: 100%; position: relative; - padding: 10px; + display: flex; + justify-content: space-between; + align-items: center; > span { font-size: 50px; font-weight: 700; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); + } + .cmdb-dashboard-grid-item-chart-icon { + > i { + font-size: 40px; + } + > img { + width: 40px; + } + > span { + display: inline-block; + width: 40px; + height: 40px; + font-size: 50px; + text-align: center; + line-height: 50px; + } } } diff --git a/cmdb-ui/src/modules/cmdb/views/custom_dashboard/chartForm.vue b/cmdb-ui/src/modules/cmdb/views/custom_dashboard/chartForm.vue index 4c1e2ce7..18f22761 100644 --- a/cmdb-ui/src/modules/cmdb/views/custom_dashboard/chartForm.vue +++ b/cmdb-ui/src/modules/cmdb/views/custom_dashboard/chartForm.vue @@ -1,65 +1,323 @@ - + + diff --git a/cmdb-ui/src/modules/cmdb/views/custom_dashboard/chartOptions.js b/cmdb-ui/src/modules/cmdb/views/custom_dashboard/chartOptions.js index 380597e8..45fe798a 100644 --- a/cmdb-ui/src/modules/cmdb/views/custom_dashboard/chartOptions.js +++ b/cmdb-ui/src/modules/cmdb/views/custom_dashboard/chartOptions.js @@ -1,62 +1,148 @@ -export const colorList = ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272', '#fc8452', '#9a60b4', '#ea7ccc'] +import i18n from '@/lang' -export const category_1_bar_options = (data) => { +export const category_1_bar_options = (data, options) => { + // Calculate first level classification + const xData = Object.keys(data) + // Calculate how many secondary categories there are + const secondCategory = {} + Object.keys(data).forEach(key => { + if (Object.prototype.toString.call(data[key]) === '[object Object]') { + Object.keys(data[key]).forEach(key1 => { + secondCategory[key1] = Array.from({ length: xData.length }).fill(0) + }) + } else { + secondCategory[i18n.t('other')] = Array.from({ length: xData.length }).fill(0) + } + }) + Object.keys(secondCategory).forEach(key => { + xData.forEach((x, idx) => { + if (data[x][key]) { + secondCategory[key][idx] = data[x][key] + } + if (typeof data[x] === 'number') { + secondCategory[i18n.t('other')][idx] = data[x] + } + }) + }) return { + + color: (options?.chartColor ?? '#5DADF2,#86DFB7,#5A6F96,#7BD5FF,#FFB980,#4D58D6,#D9B6E9,#8054FF').split(','), grid: { top: 15, left: 'left', - right: 0, - bottom: 0, + right: 10, + bottom: 20, containLabel: true, }, - xAxis: { - type: 'category', - data: Object.keys(data) + legend: { + data: Object.keys(secondCategory), + bottom: 0, + type: 'scroll', }, - yAxis: { + xAxis: options.barDirection === 'y' ? { + type: 'category', + axisTick: { show: false }, + data: xData + } + : { + type: 'value', + splitLine: { + show: false + } + }, + yAxis: options.barDirection === 'y' ? { type: 'value', splitLine: { show: false } + } : { + type: 'category', + axisTick: { show: false }, + data: xData }, tooltip: { + appendToBody: true, trigger: 'axis', axisPointer: { type: 'shadow' } }, + series: Object.keys(secondCategory).map(key => { + return { + name: options.attr_ids.length === 1 ? '' : key, + type: 'bar', + stack: options?.barStack ?? 'total', + barGap: 0, + emphasis: { + focus: 'series' + }, + data: secondCategory[key] + } + }) + } +} + +export const category_1_line_options = (data, options) => { + const xData = Object.keys(data) + return { + color: (options?.chartColor ?? '#5DADF2,#86DFB7,#5A6F96,#7BD5FF,#FFB980,#4D58D6,#D9B6E9,#8054FF').split(','), + grid: { + top: 15, + left: 'left', + right: 10, + bottom: 20, + containLabel: true, + }, + tooltip: { + appendToBody: true, + trigger: 'axis' + }, + xAxis: { + type: 'category', + data: xData + }, + yAxis: { + type: 'value' + }, series: [ { - data: Object.keys(data).map((key, index) => { - return { - value: data[key], - itemStyle: { color: colorList[0] } + data: xData.map(item => data[item]), + type: 'line', + smooth: true, + showSymbol: false, + areaStyle: options?.isShadow ? { + opacity: 0.5, + color: { + type: 'linear', + x: 0, + y: 0, + x2: 0, + y2: 1, + colorStops: [{ + offset: 0, color: (options?.chartColor ?? '#5DADF2,#86DFB7,#5A6F96,#7BD5FF,#FFB980,#4D58D6,#D9B6E9,#8054FF').split(',')[0] // 0% 处的颜色 + }, { + offset: 1, color: '#ffffff' // 100% 处的颜色 + }], + global: false // default is false } - }), - type: 'bar', - label: { - show: true, - position: 'top', - fontSize: 10, - formatter(data) { - return `${data.value || ''}` - } - }, + } : null } ] } } -export const category_1_pie_options = (data) => { +export const category_1_pie_options = (data, options) => { return { + color: (options?.chartColor ?? '#5DADF2,#86DFB7,#5A6F96,#7BD5FF,#FFB980,#4D58D6,#D9B6E9,#8054FF').split(','), grid: { top: 10, left: 'left', - right: 0, + right: 10, bottom: 0, containLabel: true, }, tooltip: { + appendToBody: true, trigger: 'item' }, legend: { @@ -89,7 +175,7 @@ export const category_1_pie_options = (data) => { } } -export const category_2_bar_options = (data) => { +export const category_2_bar_options = (data, options, chartType) => { const xAxisData = Object.keys(data.detail) const _legend = [] xAxisData.forEach(key => { @@ -97,14 +183,16 @@ export const category_2_bar_options = (data) => { }) const legend = [...new Set(_legend)] return { + color: (options?.chartColor ?? '#5DADF2,#86DFB7,#5A6F96,#7BD5FF,#FFB980,#4D58D6,#D9B6E9,#8054FF').split(','), grid: { top: 15, left: 'left', - right: 0, + right: 10, bottom: 20, containLabel: true, }, tooltip: { + appendToBody: true, trigger: 'axis', axisPointer: { type: 'shadow' @@ -116,41 +204,110 @@ export const category_2_bar_options = (data) => { type: 'scroll', data: legend }, - xAxis: [ - { - type: 'category', - axisTick: { show: false }, - data: xAxisData - } - ], - yAxis: [ - { + xAxis: options.barDirection === 'y' || chartType === 'line' ? { + type: 'category', + axisTick: { show: false }, + data: xAxisData + } + : { type: 'value', splitLine: { show: false } + }, + yAxis: options.barDirection === 'y' || chartType === 'line' ? { + type: 'value', + splitLine: { + show: false } - ], - series: legend.map(le => { + } : { + type: 'category', + axisTick: { show: false }, + data: xAxisData + }, + series: legend.map((le, index) => { return { name: le, - type: 'bar', + type: chartType, barGap: 0, emphasis: { focus: 'series' }, + stack: chartType === 'line' ? '' : options?.barStack ?? 'total', data: xAxisData.map(x => { return data.detail[x][le] || 0 }), + smooth: true, + showSymbol: false, label: { - show: true, - position: 'top', - fontSize: 10, - formatter(data) { - return `${data.value || ''}` - } + show: false, }, + areaStyle: chartType === 'line' && options?.isShadow ? { + opacity: 0.5, + color: { + type: 'linear', + x: 0, + y: 0, + x2: 0, + y2: 1, + colorStops: [{ + offset: 0, color: (options?.chartColor ?? '#5DADF2,#86DFB7,#5A6F96,#7BD5FF,#FFB980,#4D58D6,#D9B6E9,#8054FF').split(',')[index % 8] // 0% 处的颜色 + }, { + offset: 1, color: '#ffffff' // 100% 处的颜色 + }], + global: false // 缺省为 false + } + } : null } }) } } + +export const category_2_pie_options = (data, options) => { + const _legend = [] + Object.keys(data.detail).forEach(key => { + Object.keys(data.detail[key]).forEach(key2 => { + _legend.push({ value: data.detail[key][key2], name: `${key}-${key2}` }) + }) + }) + return { + color: (options?.chartColor ?? '#5DADF2,#86DFB7,#5A6F96,#7BD5FF,#FFB980,#4D58D6,#D9B6E9,#8054FF').split(','), + grid: { + top: 15, + left: 'left', + right: 10, + bottom: 20, + containLabel: true, + }, + tooltip: { + appendToBody: true, + trigger: 'item' + }, + legend: { + orient: 'vertical', + left: 'left', + type: 'scroll', + formatter: function (name) { + const _find = _legend.find(item => item.name === name) + return `${name}:${_find.value}` + } + }, + series: [ + { + type: 'pie', + radius: '90%', + data: _legend, + label: { + show: false, + }, + emphasis: { + itemStyle: { + shadowBlur: 10, + shadowOffsetX: 0, + shadowColor: 'rgba(0, 0, 0, 0.5)' + } + } + } + ] + } +} diff --git a/cmdb-ui/src/modules/cmdb/views/custom_dashboard/colorListPicker.vue b/cmdb-ui/src/modules/cmdb/views/custom_dashboard/colorListPicker.vue new file mode 100644 index 00000000..400717bd --- /dev/null +++ b/cmdb-ui/src/modules/cmdb/views/custom_dashboard/colorListPicker.vue @@ -0,0 +1,52 @@ + + + + + diff --git a/cmdb-ui/src/modules/cmdb/views/custom_dashboard/colorPicker.vue b/cmdb-ui/src/modules/cmdb/views/custom_dashboard/colorPicker.vue new file mode 100644 index 00000000..e68dc29c --- /dev/null +++ b/cmdb-ui/src/modules/cmdb/views/custom_dashboard/colorPicker.vue @@ -0,0 +1,79 @@ + + + + + diff --git a/cmdb-ui/src/modules/cmdb/views/custom_dashboard/constant.js b/cmdb-ui/src/modules/cmdb/views/custom_dashboard/constant.js index a68241f0..6bf0eb4c 100644 --- a/cmdb-ui/src/modules/cmdb/views/custom_dashboard/constant.js +++ b/cmdb-ui/src/modules/cmdb/views/custom_dashboard/constant.js @@ -1,5 +1,8 @@ -export const dashboardCategory = { - 0: { label: 'CI数统计' }, - 1: { label: '按属性值分类统计' }, - 2: { label: '关系统计' } +import i18n from '@/lang' + +export const dashboardCategory = () => { + return { + 1: { label: i18n.t('cmdb.custom_dashboard.default') }, + 2: { label: i18n.t('cmdb.custom_dashboard.relation') } + } } diff --git a/cmdb-ui/src/modules/cmdb/views/custom_dashboard/customLayout.vue b/cmdb-ui/src/modules/cmdb/views/custom_dashboard/customLayout.vue index e8dc7dc0..58b22375 100644 --- a/cmdb-ui/src/modules/cmdb/views/custom_dashboard/customLayout.vue +++ b/cmdb-ui/src/modules/cmdb/views/custom_dashboard/customLayout.vue @@ -11,13 +11,12 @@
    - - 定制仪表盘 + + {{ $t('cmdb.menu.customDashboard') }} - 管理员暂未定制仪表盘 + {{ $t('cmdb.custom_dashboard.noCustomDashboard') }}
    - +
    @@ -127,12 +167,14 @@ export default { }, } }, - mounted() { - this.getLayout() + created() { getCITypes().then((res) => { this.ci_types = res.ci_types }) }, + mounted() { + this.getLayout() + }, methods: { async getLayout() { const res = await getCustomDashboard() @@ -156,8 +198,14 @@ export default { console.log(type, item) this.$refs.chartForm.open(type, item) }, - refresh() { - this.getLayout() + refresh(id) { + if (id) { + setTimeout(() => { + this.$refs[`chart_${id}`][0].resizeChart() + }, 100) + } else { + this.getLayout() + } }, deleteChart(item) { const that = this @@ -196,6 +244,13 @@ export default { }) } }, + getCiType(item) { + if (item.type_id || item.options?.type_ids) { + const _find = this.ci_types.find((type) => type.id === item.type_id || type.id === item.options?.type_ids[0]) + return _find || null + } + return null + }, }, } @@ -206,15 +261,18 @@ export default { text-align: center; } .cmdb-dashboard-grid-item { - border-radius: 15px; + border-radius: 8px; + padding: 6px 12px; .cmdb-dashboard-grid-item-title { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; font-weight: 700; - padding-left: 6px; - color: #000000bd; + color: #000000; } .cmdb-dashboard-grid-item-operation { position: absolute; - right: 6px; + right: 12px; top: 6px; } .cmdb-dashboard-grid-item-chart-type { @@ -224,3 +282,29 @@ export default { } } + + diff --git a/cmdb-ui/src/modules/cmdb/views/discovery/discoveryCard.vue b/cmdb-ui/src/modules/cmdb/views/discovery/discoveryCard.vue index 96195a7d..bd098a4a 100644 --- a/cmdb-ui/src/modules/cmdb/views/discovery/discoveryCard.vue +++ b/cmdb-ui/src/modules/cmdb/views/discovery/discoveryCard.vue @@ -10,7 +10,12 @@
    - + + {{ rule.name }}
    @@ -52,7 +57,7 @@ export default { return this.rule?.option?.icon ?? { color: '', name: 'caise-wuliji' } }, isDeletable() { - return !['物理机', '虚拟机', '网卡', '硬盘', 'server', 'vserver', 'NIC', 'harddisk'].includes(this.rule.name) + return ![this.$t('cmdb.ad.server'), this.$t('cmdb.ad.vserver'), this.$t('cmdb.ad.nic'), this.$t('cmdb.ad.disk'), 'server', 'vserver', 'NIC', 'harddisk'].includes(this.rule.name) }, }, inject: { diff --git a/cmdb-ui/src/modules/cmdb/views/discovery/editDrawer.vue b/cmdb-ui/src/modules/cmdb/views/discovery/editDrawer.vue index 6b88a021..b18c4d1b 100644 --- a/cmdb-ui/src/modules/cmdb/views/discovery/editDrawer.vue +++ b/cmdb-ui/src/modules/cmdb/views/discovery/editDrawer.vue @@ -8,21 +8,21 @@ :label-col="{ span: 2 }" :wrapper-col="{ span: 20 }" > - 基础设置 - + {{ $t('cmdb.ciType.basicConfig') }} + - + - + - 默认 + {{ $t('cmdb.custom_dashboard.default') }} plugin - 采集设置 + {{ $t('cmdb.ad.collectSettings') }} 更新字段{{ $t('cmdb.ad.updateFields') }}
    新增{{ $t('new') }} - + - + - + - + - + diff --git a/cmdb-ui/src/modules/cmdb/views/model_relation/modules/modelRelationTable.vue b/cmdb-ui/src/modules/cmdb/views/model_relation/modules/modelRelationTable.vue index ca6e1e5c..69c40ae0 100644 --- a/cmdb-ui/src/modules/cmdb/views/model_relation/modules/modelRelationTable.vue +++ b/cmdb-ui/src/modules/cmdb/views/model_relation/modules/modelRelationTable.vue @@ -1,47 +1,40 @@ diff --git a/cmdb-ui/src/modules/cmdb/views/operation_history/modules/ciTable.vue b/cmdb-ui/src/modules/cmdb/views/operation_history/modules/ciTable.vue index 5745bafc..8178e277 100644 --- a/cmdb-ui/src/modules/cmdb/views/operation_history/modules/ciTable.vue +++ b/cmdb-ui/src/modules/cmdb/views/operation_history/modules/ciTable.vue @@ -20,32 +20,39 @@ :data="tableData" :max-height="`${windowHeight - windowHeightMinus}px`" :span-method="mergeRowMethod" - :scroll-y="{enabled: false}" + :scroll-y="{ enabled: false }" + class="ops-unstripe-table" > - - + + - - + + - - - + + + diff --git a/cmdb-ui/src/modules/cmdb/views/operation_history/modules/relation.vue b/cmdb-ui/src/modules/cmdb/views/operation_history/modules/relation.vue index bb8eadb0..540b0747 100644 --- a/cmdb-ui/src/modules/cmdb/views/operation_history/modules/relation.vue +++ b/cmdb-ui/src/modules/cmdb/views/operation_history/modules/relation.vue @@ -9,7 +9,6 @@ - - + + - + - +