mirror of
https://github.com/veops/cmdb.git
synced 2025-09-18 02:26:53 +08:00
Compare commits
45 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
dc1a2a7632 | ||
|
153fef4918 | ||
|
e218f8e065 | ||
|
b7137b3975 | ||
|
c6a9478dbb | ||
|
af40004fe9 | ||
|
d86eb1c5eb | ||
|
fef3bfceaf | ||
|
3a4f0b248f | ||
|
ffa3d7cd43 | ||
|
6aefac98cd | ||
|
22989f8d5a | ||
|
ebe9d1e29f | ||
|
f1dd5ca074 | ||
|
4dd95f0d7e | ||
|
7e3e248c2b | ||
|
a5ff1139f7 | ||
|
00135f4644 | ||
|
8297d4c9b4 | ||
|
5143539593 | ||
|
a6eb2f0d21 | ||
|
07a63bef6e | ||
|
d69efeea25 | ||
|
0ef67360ad | ||
|
e2f993bc11 | ||
|
05d2795e79 | ||
|
6ff77a140c | ||
|
6503d32e6e | ||
|
887a69c2bd | ||
|
6d052eaffc | ||
|
d0f0bf84dd | ||
|
802fda66e7 | ||
|
c95747c88a | ||
|
ed49b238d8 | ||
|
375f0879fb | ||
|
8bc1893ca9 | ||
|
53cd2342bf | ||
|
eff6d974d4 | ||
|
8478d2f858 | ||
|
80e99cc335 | ||
|
8f64fc4aa0 | ||
|
cfc345c993 | ||
|
928116d0b5 | ||
|
4c2e6ae69f | ||
|
a2c75fd34e |
60
.github/ISSUE_TEMPLATE/1bug.yaml
vendored
Normal file
60
.github/ISSUE_TEMPLATE/1bug.yaml
vendored
Normal file
@@ -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
|
44
.github/ISSUE_TEMPLATE/2feature.yaml
vendored
Normal file
44
.github/ISSUE_TEMPLATE/2feature.yaml
vendored
Normal file
@@ -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
|
36
.github/ISSUE_TEMPLATE/3consultation.yaml
vendored
Normal file
36
.github/ISSUE_TEMPLATE/3consultation.yaml
vendored
Normal file
@@ -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
|
60
.github/ISSUE_TEMPLATE/bug.yaml
vendored
Normal file
60
.github/ISSUE_TEMPLATE/bug.yaml
vendored
Normal file
@@ -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
|
6
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
6
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -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.
|
||||||
|
|
0
.github/ISSUE_TEMPLATE/consultation.yaml
vendored
Normal file
0
.github/ISSUE_TEMPLATE/consultation.yaml
vendored
Normal file
44
.github/ISSUE_TEMPLATE/feature.yaml
vendored
Normal file
44
.github/ISSUE_TEMPLATE/feature.yaml
vendored
Normal file
@@ -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
|
0
.github/config.yml
vendored
Normal file
0
.github/config.yml
vendored
Normal file
1
.gitignore
vendored
1
.gitignore
vendored
@@ -40,6 +40,7 @@ nosetests.xml
|
|||||||
.pytest_cache
|
.pytest_cache
|
||||||
cmdb-api/test-output
|
cmdb-api/test-output
|
||||||
cmdb-api/api/uploaded_files
|
cmdb-api/api/uploaded_files
|
||||||
|
cmdb-api/migrations/versions
|
||||||
|
|
||||||
# Translations
|
# Translations
|
||||||
*.mo
|
*.mo
|
||||||
|
4
Makefile
4
Makefile
@@ -9,7 +9,7 @@ help: ## display this help
|
|||||||
|
|
||||||
env: ## create a development environment using pipenv
|
env: ## create a development environment using pipenv
|
||||||
sudo easy_install pip && \
|
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 && \
|
npm install yarn && \
|
||||||
make deps
|
make deps
|
||||||
.PHONY: env
|
.PHONY: env
|
||||||
@@ -36,7 +36,7 @@ api: ## start api server
|
|||||||
.PHONY: api
|
.PHONY: api
|
||||||
|
|
||||||
worker: ## start async tasks worker
|
worker: ## start async tasks worker
|
||||||
cd cmdb-api && pipenv run celery -A celery_worker.celery worker -E -Q one_cmdb_async --concurrency=1 -D && pipenv run celery -A celery_worker.celery worker -E -Q acl_async --concurrency=1 -D
|
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
|
.PHONY: worker
|
||||||
|
|
||||||
ui: ## start ui server
|
ui: ## start ui server
|
||||||
|
46
README.md
46
README.md
@@ -1,12 +1,20 @@
|
|||||||

|
|
||||||
|
|
||||||
[](https://github.com/veops/cmdb/blob/master/LICENSE)
|
<p align="center">
|
||||||
[](https://github.com/sendya/ant-design-pro-vue)
|
<a href="https://veops.cn"><img src="docs/images/logo.png" alt="维易CMDB" width="300"/></a>
|
||||||
[](https://github.com/pallets/flask)
|
</p>
|
||||||
|
<h3 align="center">简单、轻量、通用的运维配置管理数据库</h3>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/veops/cmdb/blob/master/LICENSE"><img src="https://img.shields.io/badge/License-AGPLv3-brightgreen" alt="License: GPLv3"></a>
|
||||||
|
<a href="https:https://github.com/sendya/ant-design-pro-vue"><img src="https://img.shields.io/badge/UI-Ant%20Design%20Pro%20Vue-brightgreen" alt="UI"></a>
|
||||||
|
<a href="https://github.com/pallets/flask"><img src="https://img.shields.io/badge/API-Flask-brightgreen" alt="API"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
------------------------------
|
||||||
|
|
||||||
[English](docs/README_en.md) / [中文](README.md)
|
[English](docs/README_en.md) / [中文](README.md)
|
||||||
- 产品文档:https://veops.cn/docs/
|
- 产品文档:https://veops.cn/docs/
|
||||||
- 在线体验: <a href="https://cmdb.veops.cn" target="_blank">CMDB</a>
|
- 在线体验:<a href="https://cmdb.veops.cn" target="_blank">CMDB</a>
|
||||||
- username: demo 或者 admin
|
- username: demo 或者 admin
|
||||||
- password: 123456
|
- password: 123456
|
||||||
|
|
||||||
@@ -15,45 +23,43 @@
|
|||||||
|
|
||||||
## 系统介绍
|
## 系统介绍
|
||||||
|
|
||||||
### 整体架构
|
### 系统概览
|
||||||
|
|
||||||
<img src=docs/images/view.jpg />
|
<img src=docs/images/dashboard.png />
|
||||||
|
|
||||||
### 相关文档
|
[查看更多展示](docs/screenshot.md)
|
||||||
|
|
||||||
|
### 相关文章
|
||||||
|
|
||||||
- <a href="https://mp.weixin.qq.com/s/v3eANth64UBW5xdyOkK3tg" target="_blank">概要设计</a>
|
- <a href="https://mp.weixin.qq.com/s/v3eANth64UBW5xdyOkK3tg" target="_blank">概要设计</a>
|
||||||
- <a href="https://github.com/veops/cmdb/tree/master/docs/cmdb_api.md" target="_blank">API 文档</a>
|
- <a href="https://github.com/veops/cmdb/tree/master/docs/cmdb_api.md" target="_blank">API 文档</a>
|
||||||
- <a href="https://mp.weixin.qq.com/s/rQaf4AES7YJsyNQG_MKOLg" target="_blank">自动发现</a>
|
- <a href="https://mp.weixin.qq.com/s/rQaf4AES7YJsyNQG_MKOLg" target="_blank">自动发现</a>
|
||||||
|
- 更多文章可以在公众号 **维易科技OneOps** 里查看
|
||||||
|
|
||||||
### 特点
|
### 特点
|
||||||
|
|
||||||
- 灵活性
|
- 灵活性
|
||||||
1. 规范并统一纳管复杂数据资产
|
1. 配置灵活,不设定任何运维场景,有内置模板
|
||||||
2. 自动发现、入库 IT 资产
|
2. 自动发现、入库 IT 资产
|
||||||
- 安全性
|
- 安全性
|
||||||
1. 细粒度访问控制
|
1. 细粒度权限控制
|
||||||
2. 完备操作日志
|
2. 完备操作日志
|
||||||
- 多应用
|
- 多应用
|
||||||
1. 丰富视图展示维度
|
1. 丰富视图展示维度
|
||||||
2. 提供 Restful API
|
2. API简单强大
|
||||||
3. 支持定义属性触发器、计算属性
|
3. 支持定义属性触发器、计算属性
|
||||||
|
|
||||||
### 主要功能
|
### 主要功能
|
||||||
|
|
||||||
- 模型属性支持索引、多值、默认排序、字体颜色,支持计算属性
|
- 模型属性支持索引、多值、默认排序、字体颜色,支持计算属性
|
||||||
- 支持自动发现、定时巡检、文件导入
|
- 支持自动发现、定时巡检、文件导入
|
||||||
- 支持资源、树形、关系视图展示
|
- 支持资源、层级、关系视图展示
|
||||||
- 支持模型间关系配置和展示
|
- 支持模型间关系配置和展示
|
||||||
- 细粒度访问控制,完备的操作日志
|
- 细粒度访问控制,完备的操作日志
|
||||||
- 支持跨模型搜索
|
- 支持跨模型搜索
|
||||||
|
|
||||||
### 系统概览
|
|
||||||
|
|
||||||
- 服务树
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
[查看更多展示](docs/screenshot.md)
|
|
||||||
|
|
||||||
|
|
||||||
### 更多功能
|
### 更多功能
|
||||||
@@ -67,7 +73,7 @@
|
|||||||
## 安装
|
## 安装
|
||||||
|
|
||||||
### Docker 一键快速构建
|
### Docker 一键快速构建
|
||||||
- 进入主目录(先安装 docker 环境)
|
- 进入主目录(先安装 docker 环境, 注意要clone整个项目)
|
||||||
|
|
||||||
```
|
```
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
@@ -83,6 +89,6 @@ docker-compose up -d
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
_**欢迎关注我们的公众号,点击联系我们,加入微信、QQ群(336164978),获得更多产品、行业相关资讯**_
|
_**欢迎关注公众号(维易科技OneOps),关注后可加入微信群,进行产品和技术交流。**_
|
||||||
|
|
||||||

|

|
||||||
|
@@ -24,18 +24,19 @@ supervisor = "==4.0.3"
|
|||||||
Flask-Login = "==0.6.2"
|
Flask-Login = "==0.6.2"
|
||||||
Flask-Bcrypt = "==1.0.1"
|
Flask-Bcrypt = "==1.0.1"
|
||||||
Flask-Cors = ">=3.0.8"
|
Flask-Cors = ">=3.0.8"
|
||||||
python-ldap = "==3.4.0"
|
ldap3 = "==2.9.1"
|
||||||
pycryptodome = "==3.12.0"
|
pycryptodome = "==3.12.0"
|
||||||
|
cryptography = ">=41.0.2"
|
||||||
# Caching
|
# Caching
|
||||||
Flask-Caching = ">=1.0.0"
|
Flask-Caching = ">=1.0.0"
|
||||||
# Environment variable parsing
|
# Environment variable parsing
|
||||||
environs = "==4.2.0"
|
environs = "==4.2.0"
|
||||||
marshmallow = "==2.20.2"
|
marshmallow = "==2.20.2"
|
||||||
# async tasks
|
# async tasks
|
||||||
celery = "==5.3.1"
|
celery = ">=5.3.1"
|
||||||
celery_once = "==3.0.1"
|
celery_once = "==3.0.1"
|
||||||
more-itertools = "==5.0.0"
|
more-itertools = "==5.0.0"
|
||||||
kombu = "==5.3.1"
|
kombu = ">=5.3.1"
|
||||||
# common setting
|
# common setting
|
||||||
timeout-decorator = "==0.5.0"
|
timeout-decorator = "==0.5.0"
|
||||||
WTForms = "==3.0.0"
|
WTForms = "==3.0.0"
|
||||||
@@ -58,6 +59,9 @@ Jinja2 = "==3.1.2"
|
|||||||
jinja2schema = "==0.1.4"
|
jinja2schema = "==0.1.4"
|
||||||
msgpack-python = "==0.5.6"
|
msgpack-python = "==0.5.6"
|
||||||
alembic = "==1.7.7"
|
alembic = "==1.7.7"
|
||||||
|
hvac = "==2.0.0"
|
||||||
|
colorama = ">=0.4.6"
|
||||||
|
pycryptodomex = ">=3.19.0"
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
# Testing
|
# Testing
|
||||||
@@ -74,4 +78,3 @@ flake8-isort = "==2.7.0"
|
|||||||
isort = "==4.3.21"
|
isort = "==4.3.21"
|
||||||
pep8-naming = "==0.8.2"
|
pep8-naming = "==0.8.2"
|
||||||
pydocstyle = "==3.0.0"
|
pydocstyle = "==3.0.0"
|
||||||
|
|
||||||
|
@@ -7,6 +7,7 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
from inspect import getmembers
|
from inspect import getmembers
|
||||||
from logging.handlers import RotatingFileHandler
|
from logging.handlers import RotatingFileHandler
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from flask import jsonify
|
from flask import jsonify
|
||||||
@@ -17,11 +18,14 @@ from flask.json.provider import DefaultJSONProvider
|
|||||||
|
|
||||||
import api.views.entry
|
import api.views.entry
|
||||||
from api.extensions import (bcrypt, cache, celery, cors, db, es, login_manager, migrate, rd)
|
from api.extensions import (bcrypt, cache, celery, cors, db, es, login_manager, migrate, rd)
|
||||||
|
from api.extensions import inner_secrets
|
||||||
from api.flask_cas import CAS
|
from api.flask_cas import CAS
|
||||||
|
from api.lib.secrets.secrets import InnerKVManger
|
||||||
from api.models.acl import User
|
from api.models.acl import User
|
||||||
|
|
||||||
HERE = os.path.abspath(os.path.dirname(__file__))
|
HERE = os.path.abspath(os.path.dirname(__file__))
|
||||||
PROJECT_ROOT = os.path.join(HERE, os.pardir)
|
PROJECT_ROOT = os.path.join(HERE, os.pardir)
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
|
||||||
@login_manager.user_loader
|
@login_manager.user_loader
|
||||||
@@ -76,15 +80,6 @@ class MyJSONEncoder(DefaultJSONProvider):
|
|||||||
return 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"):
|
def create_app(config_object="settings"):
|
||||||
"""Create application factory, as explained here: http://flask.pocoo.org/docs/patterns/appfactories/.
|
"""Create application factory, as explained here: http://flask.pocoo.org/docs/patterns/appfactories/.
|
||||||
|
|
||||||
@@ -125,7 +120,7 @@ def register_extensions(app):
|
|||||||
db.init_app(app)
|
db.init_app(app)
|
||||||
cors.init_app(app)
|
cors.init_app(app)
|
||||||
login_manager.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)
|
rd.init_app(app)
|
||||||
if app.config.get('USE_ES'):
|
if app.config.get('USE_ES'):
|
||||||
es.init_app(app)
|
es.init_app(app)
|
||||||
@@ -133,6 +128,10 @@ def register_extensions(app):
|
|||||||
app.config.update(app.config.get("CELERY"))
|
app.config.update(app.config.get("CELERY"))
|
||||||
celery.conf.update(app.config)
|
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):
|
def register_blueprints(app):
|
||||||
for item in getmembers(api.views.entry):
|
for item in getmembers(api.views.entry):
|
||||||
|
@@ -7,6 +7,7 @@ import json
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
import requests
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
from flask.cli import with_appcontext
|
from flask.cli import with_appcontext
|
||||||
from flask_login import login_user
|
from flask_login import login_user
|
||||||
@@ -29,6 +30,9 @@ from api.lib.perm.acl.resource import ResourceCRUD
|
|||||||
from api.lib.perm.acl.resource import ResourceTypeCRUD
|
from api.lib.perm.acl.resource import ResourceTypeCRUD
|
||||||
from api.lib.perm.acl.role import RoleCRUD
|
from api.lib.perm.acl.role import RoleCRUD
|
||||||
from api.lib.perm.acl.user import UserCRUD
|
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 App
|
||||||
from api.models.acl import ResourceType
|
from api.models.acl import ResourceType
|
||||||
from api.models.cmdb import Attribute
|
from api.models.cmdb import Attribute
|
||||||
@@ -53,6 +57,7 @@ def cmdb_init_cache():
|
|||||||
if relations:
|
if relations:
|
||||||
rd.create_or_update(relations, REDIS_PREFIX_CI_RELATION)
|
rd.create_or_update(relations, REDIS_PREFIX_CI_RELATION)
|
||||||
|
|
||||||
|
es = None
|
||||||
if current_app.config.get("USE_ES"):
|
if current_app.config.get("USE_ES"):
|
||||||
from api.extensions import es
|
from api.extensions import es
|
||||||
from api.models.cmdb import Attribute
|
from api.models.cmdb import Attribute
|
||||||
@@ -311,3 +316,156 @@ def cmdb_index_table_upgrade():
|
|||||||
CIIndexValueDateTime.create(ci_id=i.ci_id, attr_id=i.attr_id, value=i.value, commit=False)
|
CIIndexValueDateTime.create(ci_id=i.ci_id, attr_id=i.attr_id, value=i.value, commit=False)
|
||||||
i.delete(commit=False)
|
i.delete(commit=False)
|
||||||
db.session.commit()
|
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
|
||||||
|
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
|
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:
|
||||||
|
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))
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
|
i.delete()
|
||||||
|
@@ -165,31 +165,48 @@ class InitDepartment(object):
|
|||||||
acl = self.check_app('backend')
|
acl = self.check_app('backend')
|
||||||
resources_types = acl.get_all_resources_types()
|
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']))
|
results = list(filter(lambda t: t['name'] == '操作权限', resources_types['groups']))
|
||||||
if len(results) == 0:
|
if len(results) == 0:
|
||||||
payload = dict(
|
payload = dict(
|
||||||
app_id=acl.app_name,
|
app_id=acl.app_name,
|
||||||
name='操作权限',
|
name='操作权限',
|
||||||
description='',
|
description='',
|
||||||
perms=['read', 'grant', 'delete', 'update']
|
perms=perms
|
||||||
)
|
)
|
||||||
resource_type = acl.create_resources_type(payload)
|
resource_type = acl.create_resources_type(payload)
|
||||||
else:
|
else:
|
||||||
resource_type = results[0]
|
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 ['公司信息', '公司架构', '通知设置']:
|
for name in ['公司信息', '公司架构', '通知设置']:
|
||||||
|
target = list(filter(lambda r: r['name'] == name, resource_list))
|
||||||
|
if len(target) == 0:
|
||||||
payload = dict(
|
payload = dict(
|
||||||
type_id=resource_type['id'],
|
type_id=resource_type['id'],
|
||||||
app_id=acl.app_name,
|
app_id=acl.app_name,
|
||||||
name=name,
|
name=name,
|
||||||
)
|
)
|
||||||
try:
|
resource = acl.create_resource(payload)
|
||||||
acl.create_resource(payload)
|
|
||||||
except Exception as e:
|
|
||||||
if '已经存在' in str(e):
|
|
||||||
pass
|
|
||||||
else:
|
else:
|
||||||
raise Exception(e)
|
resource = target[0]
|
||||||
|
|
||||||
|
if acl_rid > 0:
|
||||||
|
acl.grant_resource(acl_rid, resource['id'], perms)
|
||||||
|
|
||||||
def check_app(self, app_name):
|
def check_app(self, app_name):
|
||||||
acl = ACLManager(app_name)
|
acl = ACLManager(app_name)
|
||||||
@@ -199,10 +216,9 @@ class InitDepartment(object):
|
|||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
app = acl.validate_app()
|
app = acl.validate_app()
|
||||||
if app:
|
if not app:
|
||||||
return acl
|
|
||||||
|
|
||||||
acl.create_app(payload)
|
acl.create_app(payload)
|
||||||
|
return acl
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
current_app.logger.error(e)
|
current_app.logger.error(e)
|
||||||
if '不存在' in str(e):
|
if '不存在' in str(e):
|
||||||
@@ -210,6 +226,10 @@ class InitDepartment(object):
|
|||||||
return acl
|
return acl
|
||||||
raise Exception(e)
|
raise Exception(e)
|
||||||
|
|
||||||
|
def get_admin_user_rid(self):
|
||||||
|
admin = Employee.get_by(first=True, username='admin', to_dict=False)
|
||||||
|
return admin.acl_rid if admin else 0
|
||||||
|
|
||||||
|
|
||||||
@click.command()
|
@click.command()
|
||||||
@with_appcontext
|
@with_appcontext
|
||||||
|
@@ -9,6 +9,7 @@ from flask_login import LoginManager
|
|||||||
from flask_migrate import Migrate
|
from flask_migrate import Migrate
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
|
||||||
|
from api.lib.secrets.inner import KeyManage
|
||||||
from api.lib.utils import ESHandler
|
from api.lib.utils import ESHandler
|
||||||
from api.lib.utils import RedisHandler
|
from api.lib.utils import RedisHandler
|
||||||
|
|
||||||
@@ -21,3 +22,4 @@ celery = Celery()
|
|||||||
cors = CORS(supports_credentials=True)
|
cors = CORS(supports_credentials=True)
|
||||||
rd = RedisHandler()
|
rd = RedisHandler()
|
||||||
es = ESHandler()
|
es = ESHandler()
|
||||||
|
inner_secrets = KeyManage()
|
||||||
|
@@ -60,10 +60,11 @@ class AttributeManager(object):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_choice_values_from_other_ci(choice_other):
|
def _get_choice_values_from_other(choice_other):
|
||||||
from api.lib.cmdb.search import SearchError
|
from api.lib.cmdb.search import SearchError
|
||||||
from api.lib.cmdb.search.ci import search
|
from api.lib.cmdb.search.ci import search
|
||||||
|
|
||||||
|
if choice_other.get('type_ids'):
|
||||||
type_ids = choice_other.get('type_ids')
|
type_ids = choice_other.get('type_ids')
|
||||||
attr_id = choice_other.get('attr_id')
|
attr_id = choice_other.get('attr_id')
|
||||||
other_filter = choice_other.get('filter') or ''
|
other_filter = choice_other.get('filter') or ''
|
||||||
@@ -77,6 +78,16 @@ class AttributeManager(object):
|
|||||||
current_app.logger.error("get choice values from other ci failed: {}".format(e))
|
current_app.logger.error("get choice values from other ci failed: {}".format(e))
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
elif choice_other.get('script'):
|
||||||
|
try:
|
||||||
|
x = compile(choice_other['script'], '', "exec")
|
||||||
|
exec(x)
|
||||||
|
res = locals()['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
|
@classmethod
|
||||||
def get_choice_values(cls, attr_id, value_type, choice_web_hook, choice_other,
|
def get_choice_values(cls, attr_id, value_type, choice_web_hook, choice_other,
|
||||||
choice_web_hook_parse=True, choice_other_parse=True):
|
choice_web_hook_parse=True, choice_other_parse=True):
|
||||||
@@ -87,7 +98,7 @@ class AttributeManager(object):
|
|||||||
return []
|
return []
|
||||||
elif choice_other:
|
elif choice_other:
|
||||||
if choice_other_parse and isinstance(choice_other, dict):
|
if choice_other_parse and isinstance(choice_other, dict):
|
||||||
return cls._get_choice_values_from_other_ci(choice_other)
|
return cls._get_choice_values_from_other(choice_other)
|
||||||
else:
|
else:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@@ -96,7 +107,8 @@ class AttributeManager(object):
|
|||||||
return []
|
return []
|
||||||
choice_values = choice_table.get_by(fl=["value", "option"], attr_id=attr_id)
|
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
|
@staticmethod
|
||||||
def add_choice_values(_id, value_type, choice_values):
|
def add_choice_values(_id, value_type, choice_values):
|
||||||
@@ -218,9 +230,14 @@ class AttributeManager(object):
|
|||||||
if name in BUILTIN_KEYWORDS:
|
if name in BUILTIN_KEYWORDS:
|
||||||
return abort(400, ErrFormat.attribute_name_cannot_be_builtin)
|
return abort(400, ErrFormat.attribute_name_cannot_be_builtin)
|
||||||
|
|
||||||
if kwargs.get('choice_other'):
|
while kwargs.get('choice_other'):
|
||||||
if (not isinstance(kwargs['choice_other'], dict) or not kwargs['choice_other'].get('type_ids') or
|
if isinstance(kwargs['choice_other'], dict):
|
||||||
not kwargs['choice_other'].get('attr_id')):
|
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)
|
return abort(400, ErrFormat.attribute_choice_other_invalid)
|
||||||
|
|
||||||
alias = kwargs.pop("alias", "")
|
alias = kwargs.pop("alias", "")
|
||||||
@@ -232,6 +249,8 @@ class AttributeManager(object):
|
|||||||
|
|
||||||
kwargs.get('is_computed') and cls.can_create_computed_attribute()
|
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,
|
attr = Attribute.create(flush=True,
|
||||||
name=name,
|
name=name,
|
||||||
alias=alias,
|
alias=alias,
|
||||||
@@ -337,9 +356,14 @@ class AttributeManager(object):
|
|||||||
|
|
||||||
self._change_index(attr, attr.is_index, kwargs['is_index'])
|
self._change_index(attr, attr.is_index, kwargs['is_index'])
|
||||||
|
|
||||||
if kwargs.get('choice_other'):
|
while kwargs.get('choice_other'):
|
||||||
if (not isinstance(kwargs['choice_other'], dict) or not kwargs['choice_other'].get('type_ids') or
|
if isinstance(kwargs['choice_other'], dict):
|
||||||
not kwargs['choice_other'].get('attr_id')):
|
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)
|
return abort(400, ErrFormat.attribute_choice_other_invalid)
|
||||||
|
|
||||||
existed2 = attr.to_dict()
|
existed2 = attr.to_dict()
|
||||||
|
@@ -29,6 +29,7 @@ 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
|
||||||
from api.lib.cmdb.const import ResourceTypeEnum
|
from api.lib.cmdb.const import ResourceTypeEnum
|
||||||
from api.lib.cmdb.const import RetKey
|
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 AttributeHistoryManger
|
||||||
from api.lib.cmdb.history import CIRelationHistoryManager
|
from api.lib.cmdb.history import CIRelationHistoryManager
|
||||||
from api.lib.cmdb.history import CITriggerHistoryManager
|
from api.lib.cmdb.history import CITriggerHistoryManager
|
||||||
@@ -42,8 +43,10 @@ from api.lib.notify import notify_send
|
|||||||
from api.lib.perm.acl.acl import ACLManager
|
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 is_app_admin
|
||||||
from api.lib.perm.acl.acl import validate_permission
|
from api.lib.perm.acl.acl import validate_permission
|
||||||
from api.lib.utils import Lock
|
from api.lib.secrets.inner import InnerCrypt
|
||||||
|
from api.lib.secrets.vault import VaultClient
|
||||||
from api.lib.utils import handle_arg_list
|
from api.lib.utils import handle_arg_list
|
||||||
|
from api.lib.utils import Lock
|
||||||
from api.lib.webhook import webhook_request
|
from api.lib.webhook import webhook_request
|
||||||
from api.models.cmdb import AttributeHistory
|
from api.models.cmdb import AttributeHistory
|
||||||
from api.models.cmdb import AutoDiscoveryCI
|
from api.models.cmdb import AutoDiscoveryCI
|
||||||
@@ -60,6 +63,7 @@ from api.tasks.cmdb import ci_relation_cache
|
|||||||
from api.tasks.cmdb import ci_relation_delete
|
from api.tasks.cmdb import ci_relation_delete
|
||||||
|
|
||||||
PRIVILEGED_USERS = {"worker", "cmdb_agent", "agent"}
|
PRIVILEGED_USERS = {"worker", "cmdb_agent", "agent"}
|
||||||
|
PASSWORD_DEFAULT_SHOW = "******"
|
||||||
|
|
||||||
|
|
||||||
class CIManager(object):
|
class CIManager(object):
|
||||||
@@ -323,6 +327,8 @@ class CIManager(object):
|
|||||||
ci_attr2type_attr = {type_attr.attr_id: type_attr for type_attr, _ in attrs}
|
ci_attr2type_attr = {type_attr.attr_id: type_attr for type_attr, _ in attrs}
|
||||||
|
|
||||||
ci = None
|
ci = None
|
||||||
|
record_id = None
|
||||||
|
password_dict = {}
|
||||||
need_lock = current_user.username not in current_app.config.get('PRIVILEGED_USERS', PRIVILEGED_USERS)
|
need_lock = current_user.username not in current_app.config.get('PRIVILEGED_USERS', PRIVILEGED_USERS)
|
||||||
with Lock(ci_type_name, need_lock=need_lock):
|
with Lock(ci_type_name, need_lock=need_lock):
|
||||||
existed = cls.ci_is_exist(unique_key, unique_value, ci_type.id)
|
existed = cls.ci_is_exist(unique_key, unique_value, ci_type.id)
|
||||||
@@ -351,14 +357,23 @@ class CIManager(object):
|
|||||||
ci_dict.get(attr.name) is None and ci_dict.get(attr.alias) is None)):
|
ci_dict.get(attr.name) is None and ci_dict.get(attr.alias) is None)):
|
||||||
ci_dict[attr.name] = attr.default.get('default')
|
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))
|
return abort(400, ErrFormat.attribute_value_required.format(attr.name))
|
||||||
else:
|
else:
|
||||||
for type_attr, attr in attrs:
|
for type_attr, attr in attrs:
|
||||||
if attr.default and attr.default.get('default') == AttributeDefaultValueEnum.UPDATED_AT:
|
if attr.default and attr.default.get('default') == AttributeDefaultValueEnum.UPDATED_AT:
|
||||||
ci_dict[attr.name] = now
|
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()
|
value_manager = AttributeValueManager()
|
||||||
|
|
||||||
@@ -395,6 +410,10 @@ class CIManager(object):
|
|||||||
cls.delete(ci.id)
|
cls.delete(ci.id)
|
||||||
raise e
|
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
|
if record_id: # has change
|
||||||
ci_cache.apply_async(args=(ci.id, operate_type, record_id), queue=CMDB_QUEUE)
|
ci_cache.apply_async(args=(ci.id, operate_type, record_id), queue=CMDB_QUEUE)
|
||||||
|
|
||||||
@@ -414,7 +433,16 @@ class CIManager(object):
|
|||||||
if attr.default and attr.default.get('default') == AttributeDefaultValueEnum.UPDATED_AT:
|
if attr.default and attr.default.get('default') == AttributeDefaultValueEnum.UPDATED_AT:
|
||||||
ci_dict[attr.name] = now
|
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()
|
value_manager = AttributeValueManager()
|
||||||
|
|
||||||
@@ -423,6 +451,7 @@ class CIManager(object):
|
|||||||
|
|
||||||
limit_attrs = self._valid_ci_for_no_read(ci) if not _is_admin else {}
|
limit_attrs = self._valid_ci_for_no_read(ci) if not _is_admin else {}
|
||||||
|
|
||||||
|
record_id = None
|
||||||
need_lock = current_user.username not in current_app.config.get('PRIVILEGED_USERS', PRIVILEGED_USERS)
|
need_lock = current_user.username not in current_app.config.get('PRIVILEGED_USERS', PRIVILEGED_USERS)
|
||||||
with Lock(ci.ci_type.name, need_lock=need_lock):
|
with Lock(ci.ci_type.name, need_lock=need_lock):
|
||||||
self._valid_unique_constraint(ci.type_id, ci_dict, ci_id)
|
self._valid_unique_constraint(ci.type_id, ci_dict, ci_id)
|
||||||
@@ -440,6 +469,10 @@ class CIManager(object):
|
|||||||
except BadRequest as e:
|
except BadRequest as e:
|
||||||
raise 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
|
if record_id: # has change
|
||||||
ci_cache.apply_async(args=(ci_id, OperateType.UPDATE, record_id), queue=CMDB_QUEUE)
|
ci_cache.apply_async(args=(ci_id, OperateType.UPDATE, record_id), queue=CMDB_QUEUE)
|
||||||
|
|
||||||
@@ -465,6 +498,7 @@ class CIManager(object):
|
|||||||
ci_dict = cls.get_cis_by_ids([ci_id])
|
ci_dict = cls.get_cis_by_ids([ci_id])
|
||||||
ci_dict = ci_dict and ci_dict[0]
|
ci_dict = ci_dict and ci_dict[0]
|
||||||
|
|
||||||
|
if ci_dict:
|
||||||
triggers = CITriggerManager.get(ci_dict['_type'])
|
triggers = CITriggerManager.get(ci_dict['_type'])
|
||||||
for trigger in triggers:
|
for trigger in triggers:
|
||||||
option = trigger['option']
|
option = trigger['option']
|
||||||
@@ -498,6 +532,7 @@ class CIManager(object):
|
|||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
if ci_dict:
|
||||||
AttributeHistoryManger.add(None, ci_id, [(None, OperateType.DELETE, ci_dict, None)], ci.type_id)
|
AttributeHistoryManger.add(None, ci_id, [(None, OperateType.DELETE, ci_dict, None)], ci.type_id)
|
||||||
|
|
||||||
ci_delete.apply_async(args=(ci_id,), queue=CMDB_QUEUE)
|
ci_delete.apply_async(args=(ci_id,), queue=CMDB_QUEUE)
|
||||||
@@ -600,7 +635,7 @@ class CIManager(object):
|
|||||||
_fields = list()
|
_fields = list()
|
||||||
for field in fields:
|
for field in fields:
|
||||||
attr = AttributeCache.get(field)
|
attr = AttributeCache.get(field)
|
||||||
if attr is not None:
|
if attr is not None and not attr.is_password:
|
||||||
_fields.append(str(attr.id))
|
_fields.append(str(attr.id))
|
||||||
filter_fields_sql = "WHERE A.attr_id in ({0})".format(",".join(_fields))
|
filter_fields_sql = "WHERE A.attr_id in ({0})".format(",".join(_fields))
|
||||||
|
|
||||||
@@ -618,7 +653,7 @@ class CIManager(object):
|
|||||||
ci_dict = dict()
|
ci_dict = dict()
|
||||||
unique_id2obj = dict()
|
unique_id2obj = dict()
|
||||||
excludes = excludes and set(excludes)
|
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):
|
if not fields and excludes and (attr_name in excludes or attr_alias in excludes):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -645,6 +680,9 @@ class CIManager(object):
|
|||||||
else:
|
else:
|
||||||
return abort(400, ErrFormat.argument_invalid.format("ret_key"))
|
return abort(400, ErrFormat.argument_invalid.format("ret_key"))
|
||||||
|
|
||||||
|
if is_password and value:
|
||||||
|
ci_dict[attr_key] = PASSWORD_DEFAULT_SHOW
|
||||||
|
else:
|
||||||
value = ValueTypeMap.serialize2[value_type](value)
|
value = ValueTypeMap.serialize2[value_type](value)
|
||||||
if is_list:
|
if is_list:
|
||||||
ci_dict.setdefault(attr_key, []).append(value)
|
ci_dict.setdefault(attr_key, []).append(value)
|
||||||
@@ -681,6 +719,84 @@ class CIManager(object):
|
|||||||
|
|
||||||
return cls._get_cis_from_db(ci_ids, ret_key, fields, value_tables, excludes=excludes)
|
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):
|
class CIRelationManager(object):
|
||||||
"""
|
"""
|
||||||
|
@@ -12,6 +12,8 @@ class ValueTypeEnum(BaseEnum):
|
|||||||
DATE = "4"
|
DATE = "4"
|
||||||
TIME = "5"
|
TIME = "5"
|
||||||
JSON = "6"
|
JSON = "6"
|
||||||
|
PASSWORD = TEXT
|
||||||
|
LINK = TEXT
|
||||||
|
|
||||||
|
|
||||||
class ConstraintEnum(BaseEnum):
|
class ConstraintEnum(BaseEnum):
|
||||||
|
@@ -95,3 +95,6 @@ class ErrFormat(CommonErrFormat):
|
|||||||
ci_filter_perm_cannot_or_query = "CI过滤授权 暂时不支持 或 查询"
|
ci_filter_perm_cannot_or_query = "CI过滤授权 暂时不支持 或 查询"
|
||||||
ci_filter_perm_attr_no_permission = "您没有属性 {} 的操作权限!"
|
ci_filter_perm_attr_no_permission = "您没有属性 {} 的操作权限!"
|
||||||
ci_filter_perm_ci_no_permission = "您没有该CI的操作权限!"
|
ci_filter_perm_ci_no_permission = "您没有该CI的操作权限!"
|
||||||
|
|
||||||
|
password_save_failed = "保存密码失败: {}"
|
||||||
|
password_load_failed = "获取密码失败: {}"
|
||||||
|
@@ -7,6 +7,7 @@ QUERY_CIS_BY_VALUE_TABLE = """
|
|||||||
attr.alias AS attr_alias,
|
attr.alias AS attr_alias,
|
||||||
attr.value_type,
|
attr.value_type,
|
||||||
attr.is_list,
|
attr.is_list,
|
||||||
|
attr.is_password,
|
||||||
c_cis.type_id,
|
c_cis.type_id,
|
||||||
{0}.ci_id,
|
{0}.ci_id,
|
||||||
{0}.attr_id,
|
{0}.attr_id,
|
||||||
@@ -26,7 +27,8 @@ QUERY_CIS_BY_IDS = """
|
|||||||
A.attr_alias,
|
A.attr_alias,
|
||||||
A.value,
|
A.value,
|
||||||
A.value_type,
|
A.value_type,
|
||||||
A.is_list
|
A.is_list,
|
||||||
|
A.is_password
|
||||||
FROM
|
FROM
|
||||||
({1}) AS A {0}
|
({1}) AS A {0}
|
||||||
ORDER BY A.ci_id;
|
ORDER BY A.ci_id;
|
||||||
@@ -43,7 +45,7 @@ FACET_QUERY1 = """
|
|||||||
|
|
||||||
FACET_QUERY = """
|
FACET_QUERY = """
|
||||||
SELECT {0}.value,
|
SELECT {0}.value,
|
||||||
count({0}.ci_id)
|
count(distinct {0}.ci_id)
|
||||||
FROM {0}
|
FROM {0}
|
||||||
INNER JOIN ({1}) AS F ON F.ci_id={0}.ci_id
|
INNER JOIN ({1}) AS F ON F.ci_id={0}.ci_id
|
||||||
WHERE {0}.attr_id={2:d}
|
WHERE {0}.attr_id={2:d}
|
||||||
|
@@ -28,6 +28,7 @@ from api.lib.cmdb.search.ci.db.query_sql import QUERY_CI_BY_NO_ATTR
|
|||||||
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_CI_BY_TYPE
|
||||||
from api.lib.cmdb.search.ci.db.query_sql import QUERY_UNION_CI_ATTRIBUTE_IS_NULL
|
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 TableMap
|
||||||
|
from api.lib.cmdb.utils import ValueTypeMap
|
||||||
from api.lib.perm.acl.acl import ACLManager
|
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 is_app_admin
|
||||||
from api.lib.utils import handle_arg_list
|
from api.lib.utils import handle_arg_list
|
||||||
@@ -524,15 +525,15 @@ class Search(object):
|
|||||||
if k:
|
if k:
|
||||||
table_name = TableMap(attr=attr).table_name
|
table_name = TableMap(attr=attr).table_name
|
||||||
query_sql = FACET_QUERY.format(table_name, self.query_sql, attr.id)
|
query_sql = FACET_QUERY.format(table_name, self.query_sql, attr.id)
|
||||||
# current_app.logger.warning(query_sql)
|
|
||||||
result = db.session.execute(query_sql).fetchall()
|
result = db.session.execute(query_sql).fetchall()
|
||||||
facet[k] = result
|
facet[k] = result
|
||||||
|
|
||||||
facet_result = dict()
|
facet_result = dict()
|
||||||
for k, v in facet.items():
|
for k, v in facet.items():
|
||||||
if not k.startswith('_'):
|
if not k.startswith('_'):
|
||||||
a = getattr(AttributeCache.get(k), self.ret_key)
|
attr = AttributeCache.get(k)
|
||||||
facet_result[a] = [(f[0], f[1], a) for f in v]
|
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
|
return facet_result
|
||||||
|
|
||||||
|
@@ -35,7 +35,7 @@ class Search(object):
|
|||||||
self.sort = sort or ("ci_id" if current_app.config.get("USE_ES") else None)
|
self.sort = sort or ("ci_id" if current_app.config.get("USE_ES") else None)
|
||||||
|
|
||||||
self.root_id = root_id
|
self.root_id = root_id
|
||||||
self.level = level
|
self.level = level or 0
|
||||||
self.reverse = reverse
|
self.reverse = reverse
|
||||||
|
|
||||||
def _get_ids(self):
|
def _get_ids(self):
|
||||||
@@ -104,16 +104,22 @@ class Search(object):
|
|||||||
ci_ids=merge_ids).search()
|
ci_ids=merge_ids).search()
|
||||||
|
|
||||||
def statistics(self, type_ids):
|
def statistics(self, type_ids):
|
||||||
|
self.level = int(self.level)
|
||||||
_tmp = []
|
_tmp = []
|
||||||
ids = [self.root_id] if not isinstance(self.root_id, list) else self.root_id
|
ids = [self.root_id] if not isinstance(self.root_id, list) else self.root_id
|
||||||
for l in range(0, int(self.level)):
|
for lv in range(0, self.level):
|
||||||
if not l:
|
if not lv:
|
||||||
|
if type_ids and lv == self.level - 1:
|
||||||
|
_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(ids, REDIS_PREFIX_CI_RELATION) or []]))))
|
||||||
|
else:
|
||||||
_tmp = list(map(lambda x: list(json.loads(x).items()),
|
_tmp = list(map(lambda x: list(json.loads(x).items()),
|
||||||
[i or '{}' for i in rd.get(ids, REDIS_PREFIX_CI_RELATION) or []]))
|
[i or '{}' for i in rd.get(ids, REDIS_PREFIX_CI_RELATION) or []]))
|
||||||
else:
|
else:
|
||||||
for idx, item in enumerate(_tmp):
|
for idx, item in enumerate(_tmp):
|
||||||
if item:
|
if item:
|
||||||
if type_ids and l == self.level - 1:
|
if type_ids and lv == self.level - 1:
|
||||||
__tmp = list(
|
__tmp = list(
|
||||||
map(lambda x: [(_id, type_id) for _id, type_id in json.loads(x).items()
|
map(lambda x: [(_id, type_id) for _id, type_id in json.loads(x).items()
|
||||||
if type_id in type_ids],
|
if type_id in type_ids],
|
||||||
|
@@ -12,7 +12,7 @@ import api.models.cmdb as model
|
|||||||
from api.lib.cmdb.cache import AttributeCache
|
from api.lib.cmdb.cache import AttributeCache
|
||||||
from api.lib.cmdb.const import ValueTypeEnum
|
from api.lib.cmdb.const import ValueTypeEnum
|
||||||
|
|
||||||
TIME_RE = re.compile(r"^(20|21|22|23|[0-1]\d):[0-5]\d:[0-5]\d$")
|
TIME_RE = re.compile(r"^20|21|22|23|[0-1]\d:[0-5]\d:[0-5]\d$")
|
||||||
|
|
||||||
|
|
||||||
def string2int(x):
|
def string2int(x):
|
||||||
@@ -21,7 +21,7 @@ def string2int(x):
|
|||||||
|
|
||||||
def str2datetime(x):
|
def str2datetime(x):
|
||||||
try:
|
try:
|
||||||
return datetime.datetime.strptime(x, "%Y-%m-%d")
|
return datetime.datetime.strptime(x, "%Y-%m-%d").date()
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -44,8 +44,8 @@ class ValueTypeMap(object):
|
|||||||
ValueTypeEnum.FLOAT: float,
|
ValueTypeEnum.FLOAT: float,
|
||||||
ValueTypeEnum.TEXT: lambda x: x if isinstance(x, six.string_types) else str(x),
|
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.TIME: lambda x: x if isinstance(x, six.string_types) else str(x),
|
||||||
ValueTypeEnum.DATE: lambda x: x.strftime("%Y-%m-%d"),
|
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"),
|
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,
|
ValueTypeEnum.JSON: lambda x: json.loads(x) if isinstance(x, six.string_types) and x else x,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,6 +64,8 @@ class ValueTypeMap(object):
|
|||||||
ValueTypeEnum.FLOAT: model.FloatChoice,
|
ValueTypeEnum.FLOAT: model.FloatChoice,
|
||||||
ValueTypeEnum.TEXT: model.TextChoice,
|
ValueTypeEnum.TEXT: model.TextChoice,
|
||||||
ValueTypeEnum.TIME: model.TextChoice,
|
ValueTypeEnum.TIME: model.TextChoice,
|
||||||
|
ValueTypeEnum.DATE: model.TextChoice,
|
||||||
|
ValueTypeEnum.DATETIME: model.TextChoice,
|
||||||
}
|
}
|
||||||
|
|
||||||
table = {
|
table = {
|
||||||
@@ -97,7 +99,7 @@ class ValueTypeMap(object):
|
|||||||
ValueTypeEnum.DATE: 'text',
|
ValueTypeEnum.DATE: 'text',
|
||||||
ValueTypeEnum.TIME: 'text',
|
ValueTypeEnum.TIME: 'text',
|
||||||
ValueTypeEnum.FLOAT: 'float',
|
ValueTypeEnum.FLOAT: 'float',
|
||||||
ValueTypeEnum.JSON: 'object'
|
ValueTypeEnum.JSON: 'object',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -110,7 +112,9 @@ class TableMap(object):
|
|||||||
@property
|
@property
|
||||||
def table(self):
|
def table(self):
|
||||||
attr = AttributeCache.get(self.attr_name) if not self.attr else self.attr
|
attr = AttributeCache.get(self.attr_name) if not self.attr else self.attr
|
||||||
if attr.value_type != ValueTypeEnum.TEXT and attr.value_type != ValueTypeEnum.JSON:
|
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
|
self.is_index = True
|
||||||
elif self.is_index is None:
|
elif self.is_index is None:
|
||||||
self.is_index = attr.is_index
|
self.is_index = attr.is_index
|
||||||
@@ -122,7 +126,9 @@ class TableMap(object):
|
|||||||
@property
|
@property
|
||||||
def table_name(self):
|
def table_name(self):
|
||||||
attr = AttributeCache.get(self.attr_name) if not self.attr else self.attr
|
attr = AttributeCache.get(self.attr_name) if not self.attr else self.attr
|
||||||
if attr.value_type != ValueTypeEnum.TEXT and attr.value_type != ValueTypeEnum.JSON:
|
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
|
self.is_index = True
|
||||||
elif self.is_index is None:
|
elif self.is_index is None:
|
||||||
self.is_index = attr.is_index
|
self.is_index = attr.is_index
|
||||||
|
@@ -66,9 +66,10 @@ class AttributeValueManager(object):
|
|||||||
use_master=use_master,
|
use_master=use_master,
|
||||||
to_dict=False)
|
to_dict=False)
|
||||||
field_name = getattr(attr, ret_key)
|
field_name = getattr(attr, ret_key)
|
||||||
|
|
||||||
if attr.is_list:
|
if attr.is_list:
|
||||||
res[field_name] = [ValueTypeMap.serialize[attr.value_type](i.value) for i in rs]
|
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:
|
else:
|
||||||
res[field_name] = ValueTypeMap.serialize[attr.value_type](rs[0].value) if rs else None
|
res[field_name] = ValueTypeMap.serialize[attr.value_type](rs[0].value) if rs else None
|
||||||
|
|
||||||
@@ -131,8 +132,7 @@ class AttributeValueManager(object):
|
|||||||
return AttributeHistoryManger.add(record_id, ci_id, [(attr_id, operate_type, old, new)], type_id)
|
return AttributeHistoryManger.add(record_id, ci_id, [(attr_id, operate_type, old, new)], type_id)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _write_change2(changed):
|
def write_change2(changed, record_id=None):
|
||||||
record_id = None
|
|
||||||
for ci_id, attr_id, operate_type, old, new, type_id in changed:
|
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,
|
record_id = AttributeHistoryManger.add(record_id, ci_id, [(attr_id, operate_type, old, new)], type_id,
|
||||||
commit=False, flush=False)
|
commit=False, flush=False)
|
||||||
@@ -284,9 +284,9 @@ class AttributeValueManager(object):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
current_app.logger.warning(str(e))
|
current_app.logger.warning(str(e))
|
||||||
return abort(400, ErrFormat.attribute_value_unknown_error.format(str(e)))
|
return abort(400, ErrFormat.attribute_value_unknown_error.format(e.args[0]))
|
||||||
|
|
||||||
return self._write_change2(changed)
|
return self.write_change2(changed)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def delete_attr_value(attr_id, ci_id):
|
def delete_attr_value(attr_id, ci_id):
|
||||||
|
@@ -1,12 +1,13 @@
|
|||||||
# -*- coding:utf-8 -*-
|
# -*- coding:utf-8 -*-
|
||||||
from flask import abort
|
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
|
|
||||||
from api.lib.common_setting.resp_format import ErrFormat
|
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.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.role import RoleCRUD, RoleRelationCRUD
|
||||||
from api.lib.perm.acl.user import UserCRUD
|
from api.lib.perm.acl.user import UserCRUD
|
||||||
from api.lib.perm.acl.resource import ResourceTypeCRUD, ResourceCRUD
|
|
||||||
|
|
||||||
|
|
||||||
class ACLManager(object):
|
class ACLManager(object):
|
||||||
@@ -109,8 +110,32 @@ class ACLManager(object):
|
|||||||
id2perms=id2perms
|
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):
|
def create_resource(self, payload):
|
||||||
payload['app_id'] = self.validate_app().id
|
payload['app_id'] = self.validate_app().id
|
||||||
resource = ResourceCRUD.add(**payload)
|
resource = ResourceCRUD.add(**payload)
|
||||||
|
|
||||||
return resource.to_dict()
|
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()
|
||||||
|
@@ -121,6 +121,19 @@ class EmployeeCRUD(object):
|
|||||||
employee = CreateEmployee().create_single(**data)
|
employee = CreateEmployee().create_single(**data)
|
||||||
return employee.to_dict()
|
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
|
@staticmethod
|
||||||
def add(**kwargs):
|
def add(**kwargs):
|
||||||
try:
|
try:
|
||||||
|
@@ -4,8 +4,14 @@
|
|||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
from flask import abort
|
from flask import abort
|
||||||
|
from flask import current_app
|
||||||
from flask import request
|
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
|
from api.lib.resp_format import CommonErrFormat
|
||||||
|
|
||||||
|
|
||||||
@@ -70,3 +76,43 @@ def args_validate(model_cls, exclude_args=None):
|
|||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
return decorate
|
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()
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
import msgpack
|
import msgpack
|
||||||
|
|
||||||
from api.extensions import cache
|
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.lib.utils import Lock
|
||||||
from api.models.acl import App
|
from api.models.acl import App
|
||||||
from api.models.acl import Permission
|
from api.models.acl import Permission
|
||||||
@@ -221,9 +221,9 @@ class RoleRelationCache(object):
|
|||||||
return msgpack.loads(r_g, raw=False)
|
return msgpack.loads(r_g, raw=False)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@flush_db
|
||||||
def rebuild(cls, rid, app_id):
|
def rebuild(cls, rid, app_id):
|
||||||
cls.clean(rid, app_id)
|
cls.clean(rid, app_id)
|
||||||
db.session.remove()
|
|
||||||
|
|
||||||
cls.get_parent_ids(rid, app_id)
|
cls.get_parent_ids(rid, app_id)
|
||||||
cls.get_child_ids(rid, app_id)
|
cls.get_child_ids(rid, app_id)
|
||||||
@@ -235,9 +235,9 @@ class RoleRelationCache(object):
|
|||||||
cls.get_resources2(rid, app_id)
|
cls.get_resources2(rid, app_id)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@flush_db
|
||||||
def rebuild2(cls, rid, app_id):
|
def rebuild2(cls, rid, app_id):
|
||||||
cache.delete(cls.PREFIX_RESOURCES2.format(rid, app_id))
|
cache.delete(cls.PREFIX_RESOURCES2.format(rid, app_id))
|
||||||
db.session.remove()
|
|
||||||
cls.get_resources2(rid, app_id)
|
cls.get_resources2(rid, app_id)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@@ -260,7 +260,8 @@ class ResourceCRUD(object):
|
|||||||
numfound = query.count()
|
numfound = query.count()
|
||||||
res = [i.to_dict() for i in query.offset((page - 1) * page_size).limit(page_size)]
|
res = [i.to_dict() for i in query.offset((page - 1) * page_size).limit(page_size)]
|
||||||
for i in res:
|
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
|
return numfound, res
|
||||||
|
|
||||||
|
@@ -58,10 +58,14 @@ class UserCRUD(object):
|
|||||||
kwargs['employee_id'] = '{0:04d}'.format(biggest_employee_id + 1)
|
kwargs['employee_id'] = '{0:04d}'.format(biggest_employee_id + 1)
|
||||||
user = User.create(**kwargs)
|
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,
|
AuditCRUD.add_role_log(None, AuditOperateType.create,
|
||||||
AuditScope.user, user.uid, {}, user.to_dict(), {}, {}
|
AuditScope.user, user.uid, {}, user.to_dict(), {}, {}
|
||||||
)
|
)
|
||||||
|
from api.lib.common_setting.employee import EmployeeCRUD
|
||||||
|
payload = {column: getattr(user, column) for column in ['uid', 'username', 'nickname', 'email', 'block']}
|
||||||
|
payload['rid'] = role.id
|
||||||
|
EmployeeCRUD.add_employee_from_acl_created(**payload)
|
||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
1
cmdb-api/api/lib/secrets/__init__.py
Normal file
1
cmdb-api/api/lib/secrets/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# -*- coding:utf-8 -*-
|
429
cmdb-api/api/lib/secrets/inner.py
Normal file
429
cmdb-api/api/lib/secrets/inner.py
Normal file
@@ -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] == "run"):
|
||||||
|
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 {"message": msg, "status": "failed"}
|
||||||
|
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)
|
35
cmdb-api/api/lib/secrets/secrets.py
Normal file
35
cmdb-api/api/lib/secrets/secrets.py
Normal file
@@ -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
|
141
cmdb-api/api/lib/secrets/vault.py
Normal file
141
cmdb-api/api/lib/secrets/vault.py
Normal file
@@ -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)
|
@@ -5,7 +5,8 @@ import copy
|
|||||||
import hashlib
|
import hashlib
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
import ldap
|
from ldap3 import Server, Connection, ALL
|
||||||
|
from ldap3.core.exceptions import LDAPBindError, LDAPCertificateError
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
from flask_sqlalchemy import BaseQuery
|
from flask_sqlalchemy import BaseQuery
|
||||||
|
|
||||||
@@ -57,9 +58,7 @@ class UserQuery(BaseQuery):
|
|||||||
return user, authenticated
|
return user, authenticated
|
||||||
|
|
||||||
def authenticate_with_ldap(self, username, password):
|
def authenticate_with_ldap(self, username, password):
|
||||||
ldap_conn = ldap.initialize(current_app.config.get('LDAP_SERVER'))
|
server = Server(current_app.config.get('LDAP_SERVER'), get_info=ALL)
|
||||||
ldap_conn.protocol_version = 3
|
|
||||||
ldap_conn.set_option(ldap.OPT_REFERRALS, 0)
|
|
||||||
if '@' in username:
|
if '@' in username:
|
||||||
email = username
|
email = username
|
||||||
who = current_app.config.get('LDAP_USER_DN').format(username.split('@')[0])
|
who = current_app.config.get('LDAP_USER_DN').format(username.split('@')[0])
|
||||||
@@ -70,11 +69,14 @@ class UserQuery(BaseQuery):
|
|||||||
username = username.split('@')[0]
|
username = username.split('@')[0]
|
||||||
user = self.get_by_username(username)
|
user = self.get_by_username(username)
|
||||||
try:
|
try:
|
||||||
|
|
||||||
if not password:
|
if not password:
|
||||||
raise ldap.INVALID_CREDENTIALS
|
raise LDAPCertificateError
|
||||||
|
|
||||||
ldap_conn.simple_bind_s(who, password)
|
conn = Connection(server, user=who, password=password)
|
||||||
|
conn.bind()
|
||||||
|
if conn.result['result'] != 0:
|
||||||
|
raise LDAPBindError
|
||||||
|
conn.unbind()
|
||||||
|
|
||||||
if not user:
|
if not user:
|
||||||
from api.lib.perm.acl.user import UserCRUD
|
from api.lib.perm.acl.user import UserCRUD
|
||||||
@@ -84,7 +86,7 @@ class UserQuery(BaseQuery):
|
|||||||
op_record.apply_async(args=(None, username, OperateType.LOGIN, ["ACL"]), queue=ACL_QUEUE)
|
op_record.apply_async(args=(None, username, OperateType.LOGIN, ["ACL"]), queue=ACL_QUEUE)
|
||||||
|
|
||||||
return user, True
|
return user, True
|
||||||
except ldap.INVALID_CREDENTIALS:
|
except LDAPBindError:
|
||||||
return user, False
|
return user, False
|
||||||
|
|
||||||
def search(self, key):
|
def search(self, key):
|
||||||
|
@@ -504,3 +504,10 @@ class CIFilterPerms(Model):
|
|||||||
attr_filter = db.Column(db.Text)
|
attr_filter = db.Column(db.Text)
|
||||||
|
|
||||||
rid = db.Column(db.Integer, index=True)
|
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)
|
||||||
|
@@ -46,5 +46,4 @@ def register_resources(resource_path, rest_api):
|
|||||||
resource_cls.url_prefix = ("",)
|
resource_cls.url_prefix = ("",)
|
||||||
if isinstance(resource_cls.url_prefix, six.string_types):
|
if isinstance(resource_cls.url_prefix, six.string_types):
|
||||||
resource_cls.url_prefix = (resource_cls.url_prefix,)
|
resource_cls.url_prefix = (resource_cls.url_prefix,)
|
||||||
|
|
||||||
rest_api.add_resource(resource_cls, *resource_cls.url_prefix)
|
rest_api.add_resource(resource_cls, *resource_cls.url_prefix)
|
||||||
|
@@ -9,7 +9,8 @@ from werkzeug.exceptions import BadRequest
|
|||||||
from werkzeug.exceptions import NotFound
|
from werkzeug.exceptions import NotFound
|
||||||
|
|
||||||
from api.extensions import celery
|
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 AuditCRUD
|
||||||
from api.lib.perm.acl.audit import AuditOperateSource
|
from api.lib.perm.acl.audit import AuditOperateSource
|
||||||
from api.lib.perm.acl.audit import AuditOperateType
|
from api.lib.perm.acl.audit import AuditOperateType
|
||||||
@@ -28,6 +29,7 @@ from api.models.acl import Trigger
|
|||||||
name="acl.role_rebuild",
|
name="acl.role_rebuild",
|
||||||
queue=ACL_QUEUE,
|
queue=ACL_QUEUE,
|
||||||
once={"graceful": True, "unlock_before_run": True})
|
once={"graceful": True, "unlock_before_run": True})
|
||||||
|
@reconnect_db
|
||||||
def role_rebuild(rids, app_id):
|
def role_rebuild(rids, app_id):
|
||||||
rids = rids if isinstance(rids, list) else [rids]
|
rids = rids if isinstance(rids, list) else [rids]
|
||||||
for rid in rids:
|
for rid in rids:
|
||||||
@@ -37,6 +39,7 @@ def role_rebuild(rids, app_id):
|
|||||||
|
|
||||||
|
|
||||||
@celery.task(name="acl.update_resource_to_build_role", queue=ACL_QUEUE)
|
@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):
|
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(__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)]
|
rids += [i.id for i in Role.get_by(app_id=app_id, fl='id', to_dict=False)]
|
||||||
@@ -52,9 +55,9 @@ def update_resource_to_build_role(resource_id, app_id, group_id=None):
|
|||||||
|
|
||||||
|
|
||||||
@celery.task(name="acl.apply_trigger", queue=ACL_QUEUE)
|
@celery.task(name="acl.apply_trigger", queue=ACL_QUEUE)
|
||||||
|
@flush_db
|
||||||
|
@reconnect_db
|
||||||
def apply_trigger(_id, resource_id=None, operator_uid=None):
|
def apply_trigger(_id, resource_id=None, operator_uid=None):
|
||||||
db.session.remove()
|
|
||||||
|
|
||||||
from api.lib.perm.acl.permission import PermissionCRUD
|
from api.lib.perm.acl.permission import PermissionCRUD
|
||||||
|
|
||||||
trigger = Trigger.get_by_id(_id)
|
trigger = Trigger.get_by_id(_id)
|
||||||
@@ -118,9 +121,9 @@ def apply_trigger(_id, resource_id=None, operator_uid=None):
|
|||||||
|
|
||||||
|
|
||||||
@celery.task(name="acl.cancel_trigger", queue=ACL_QUEUE)
|
@celery.task(name="acl.cancel_trigger", queue=ACL_QUEUE)
|
||||||
|
@flush_db
|
||||||
|
@reconnect_db
|
||||||
def cancel_trigger(_id, resource_id=None, operator_uid=None):
|
def cancel_trigger(_id, resource_id=None, operator_uid=None):
|
||||||
db.session.remove()
|
|
||||||
|
|
||||||
from api.lib.perm.acl.permission import PermissionCRUD
|
from api.lib.perm.acl.permission import PermissionCRUD
|
||||||
|
|
||||||
trigger = Trigger.get_by_id(_id)
|
trigger = Trigger.get_by_id(_id)
|
||||||
@@ -186,6 +189,7 @@ def cancel_trigger(_id, resource_id=None, operator_uid=None):
|
|||||||
|
|
||||||
|
|
||||||
@celery.task(name="acl.op_record", queue=ACL_QUEUE)
|
@celery.task(name="acl.op_record", queue=ACL_QUEUE)
|
||||||
|
@reconnect_db
|
||||||
def op_record(app, rolename, operate_type, obj):
|
def op_record(app, rolename, operate_type, obj):
|
||||||
if isinstance(app, int):
|
if isinstance(app, int):
|
||||||
app = AppCache.get(app)
|
app = AppCache.get(app)
|
||||||
|
@@ -16,6 +16,8 @@ from api.lib.cmdb.cache import CITypeAttributesCache
|
|||||||
from api.lib.cmdb.const import CMDB_QUEUE
|
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
|
||||||
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION
|
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION
|
||||||
|
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.perm.acl.cache import UserCache
|
||||||
from api.lib.utils import Lock
|
from api.lib.utils import Lock
|
||||||
from api.lib.utils import handle_arg_list
|
from api.lib.utils import handle_arg_list
|
||||||
@@ -25,11 +27,12 @@ from api.models.cmdb import CITypeAttribute
|
|||||||
|
|
||||||
|
|
||||||
@celery.task(name="cmdb.ci_cache", queue=CMDB_QUEUE)
|
@celery.task(name="cmdb.ci_cache", queue=CMDB_QUEUE)
|
||||||
|
@flush_db
|
||||||
|
@reconnect_db
|
||||||
def ci_cache(ci_id, operate_type, record_id):
|
def ci_cache(ci_id, operate_type, record_id):
|
||||||
from api.lib.cmdb.ci import CITriggerManager
|
from api.lib.cmdb.ci import CITriggerManager
|
||||||
|
|
||||||
time.sleep(0.01)
|
time.sleep(0.01)
|
||||||
db.session.remove()
|
|
||||||
|
|
||||||
m = api.lib.cmdb.ci.CIManager()
|
m = api.lib.cmdb.ci.CIManager()
|
||||||
ci_dict = m.get_ci_by_id_from_db(ci_id, need_children=False, use_master=False)
|
ci_dict = m.get_ci_by_id_from_db(ci_id, need_children=False, use_master=False)
|
||||||
@@ -49,9 +52,10 @@ def ci_cache(ci_id, operate_type, record_id):
|
|||||||
|
|
||||||
|
|
||||||
@celery.task(name="cmdb.batch_ci_cache", queue=CMDB_QUEUE)
|
@celery.task(name="cmdb.batch_ci_cache", queue=CMDB_QUEUE)
|
||||||
|
@flush_db
|
||||||
|
@reconnect_db
|
||||||
def batch_ci_cache(ci_ids, ): # only for attribute change index
|
def batch_ci_cache(ci_ids, ): # only for attribute change index
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
db.session.remove()
|
|
||||||
|
|
||||||
for ci_id in ci_ids:
|
for ci_id in ci_ids:
|
||||||
m = api.lib.cmdb.ci.CIManager()
|
m = api.lib.cmdb.ci.CIManager()
|
||||||
@@ -66,6 +70,7 @@ def batch_ci_cache(ci_ids, ): # only for attribute change index
|
|||||||
|
|
||||||
|
|
||||||
@celery.task(name="cmdb.ci_delete", queue=CMDB_QUEUE)
|
@celery.task(name="cmdb.ci_delete", queue=CMDB_QUEUE)
|
||||||
|
@reconnect_db
|
||||||
def ci_delete(ci_id):
|
def ci_delete(ci_id):
|
||||||
current_app.logger.info(ci_id)
|
current_app.logger.info(ci_id)
|
||||||
|
|
||||||
@@ -78,6 +83,7 @@ def ci_delete(ci_id):
|
|||||||
|
|
||||||
|
|
||||||
@celery.task(name="cmdb.ci_delete_trigger", queue=CMDB_QUEUE)
|
@celery.task(name="cmdb.ci_delete_trigger", queue=CMDB_QUEUE)
|
||||||
|
@reconnect_db
|
||||||
def ci_delete_trigger(trigger, operate_type, ci_dict):
|
def ci_delete_trigger(trigger, operate_type, ci_dict):
|
||||||
current_app.logger.info('delete ci {} trigger'.format(ci_dict['_id']))
|
current_app.logger.info('delete ci {} trigger'.format(ci_dict['_id']))
|
||||||
from api.lib.cmdb.ci import CITriggerManager
|
from api.lib.cmdb.ci import CITriggerManager
|
||||||
@@ -89,9 +95,9 @@ def ci_delete_trigger(trigger, operate_type, ci_dict):
|
|||||||
|
|
||||||
|
|
||||||
@celery.task(name="cmdb.ci_relation_cache", queue=CMDB_QUEUE)
|
@celery.task(name="cmdb.ci_relation_cache", queue=CMDB_QUEUE)
|
||||||
|
@flush_db
|
||||||
|
@reconnect_db
|
||||||
def ci_relation_cache(parent_id, child_id):
|
def ci_relation_cache(parent_id, child_id):
|
||||||
db.session.remove()
|
|
||||||
|
|
||||||
with Lock("CIRelation_{}".format(parent_id)):
|
with Lock("CIRelation_{}".format(parent_id)):
|
||||||
children = rd.get([parent_id], REDIS_PREFIX_CI_RELATION)[0]
|
children = rd.get([parent_id], REDIS_PREFIX_CI_RELATION)[0]
|
||||||
children = json.loads(children) if children is not None else {}
|
children = json.loads(children) if children is not None else {}
|
||||||
@@ -106,6 +112,8 @@ def ci_relation_cache(parent_id, child_id):
|
|||||||
|
|
||||||
|
|
||||||
@celery.task(name="cmdb.ci_relation_add", queue=CMDB_QUEUE)
|
@celery.task(name="cmdb.ci_relation_add", queue=CMDB_QUEUE)
|
||||||
|
@flush_db
|
||||||
|
@reconnect_db
|
||||||
def ci_relation_add(parent_dict, child_id, uid):
|
def ci_relation_add(parent_dict, child_id, uid):
|
||||||
"""
|
"""
|
||||||
:param parent_dict: key is '$parent_model.attr_name'
|
:param parent_dict: key is '$parent_model.attr_name'
|
||||||
@@ -121,8 +129,6 @@ def ci_relation_add(parent_dict, child_id, uid):
|
|||||||
current_app.test_request_context().push()
|
current_app.test_request_context().push()
|
||||||
login_user(UserCache.get(uid))
|
login_user(UserCache.get(uid))
|
||||||
|
|
||||||
db.session.remove()
|
|
||||||
|
|
||||||
for parent in parent_dict:
|
for parent in parent_dict:
|
||||||
parent_ci_type_name, _attr_name = parent.strip()[1:].split('.', 1)
|
parent_ci_type_name, _attr_name = parent.strip()[1:].split('.', 1)
|
||||||
attr_name = CITypeAttributeManager.get_attr_name(parent_ci_type_name, _attr_name)
|
attr_name = CITypeAttributeManager.get_attr_name(parent_ci_type_name, _attr_name)
|
||||||
@@ -147,10 +153,14 @@ def ci_relation_add(parent_dict, child_id, uid):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
current_app.logger.warning(e)
|
current_app.logger.warning(e)
|
||||||
finally:
|
finally:
|
||||||
db.session.remove()
|
try:
|
||||||
|
db.session.commit()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
@celery.task(name="cmdb.ci_relation_delete", queue=CMDB_QUEUE)
|
@celery.task(name="cmdb.ci_relation_delete", queue=CMDB_QUEUE)
|
||||||
|
@reconnect_db
|
||||||
def ci_relation_delete(parent_id, child_id):
|
def ci_relation_delete(parent_id, child_id):
|
||||||
with Lock("CIRelation_{}".format(parent_id)):
|
with Lock("CIRelation_{}".format(parent_id)):
|
||||||
children = rd.get([parent_id], REDIS_PREFIX_CI_RELATION)[0]
|
children = rd.get([parent_id], REDIS_PREFIX_CI_RELATION)[0]
|
||||||
@@ -165,9 +175,10 @@ def ci_relation_delete(parent_id, child_id):
|
|||||||
|
|
||||||
|
|
||||||
@celery.task(name="cmdb.ci_type_attribute_order_rebuild", queue=CMDB_QUEUE)
|
@celery.task(name="cmdb.ci_type_attribute_order_rebuild", queue=CMDB_QUEUE)
|
||||||
|
@flush_db
|
||||||
|
@reconnect_db
|
||||||
def ci_type_attribute_order_rebuild(type_id, uid):
|
def ci_type_attribute_order_rebuild(type_id, uid):
|
||||||
current_app.logger.info('rebuild attribute order')
|
current_app.logger.info('rebuild attribute order')
|
||||||
db.session.remove()
|
|
||||||
|
|
||||||
from api.lib.cmdb.ci_type import CITypeAttributeGroupManager
|
from api.lib.cmdb.ci_type import CITypeAttributeGroupManager
|
||||||
|
|
||||||
@@ -188,11 +199,11 @@ def ci_type_attribute_order_rebuild(type_id, uid):
|
|||||||
|
|
||||||
|
|
||||||
@celery.task(name="cmdb.calc_computed_attribute", queue=CMDB_QUEUE)
|
@celery.task(name="cmdb.calc_computed_attribute", queue=CMDB_QUEUE)
|
||||||
|
@flush_db
|
||||||
|
@reconnect_db
|
||||||
def calc_computed_attribute(attr_id, uid):
|
def calc_computed_attribute(attr_id, uid):
|
||||||
from api.lib.cmdb.ci import CIManager
|
from api.lib.cmdb.ci import CIManager
|
||||||
|
|
||||||
db.session.remove()
|
|
||||||
|
|
||||||
current_app.test_request_context().push()
|
current_app.test_request_context().push()
|
||||||
login_user(UserCache.get(uid))
|
login_user(UserCache.get(uid))
|
||||||
|
|
||||||
|
@@ -84,11 +84,10 @@ class CIView(APIView):
|
|||||||
ci_dict = self._wrap_ci_dict()
|
ci_dict = self._wrap_ci_dict()
|
||||||
|
|
||||||
manager = CIManager()
|
manager = CIManager()
|
||||||
current_app.logger.debug(ci_dict)
|
|
||||||
ci_id = manager.add(ci_type,
|
ci_id = manager.add(ci_type,
|
||||||
exist_policy=exist_policy or ExistPolicy.REJECT,
|
exist_policy=exist_policy or ExistPolicy.REJECT,
|
||||||
_no_attribute_policy=_no_attribute_policy,
|
_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)
|
**ci_dict)
|
||||||
|
|
||||||
return self.jsonify(ci_id=ci_id)
|
return self.jsonify(ci_id=ci_id)
|
||||||
@@ -96,7 +95,6 @@ class CIView(APIView):
|
|||||||
@has_perm_for_ci("ci_id", ResourceTypeEnum.CI, PermEnum.UPDATE, CIManager.get_type)
|
@has_perm_for_ci("ci_id", ResourceTypeEnum.CI, PermEnum.UPDATE, CIManager.get_type)
|
||||||
def put(self, ci_id=None):
|
def put(self, ci_id=None):
|
||||||
args = request.values
|
args = request.values
|
||||||
current_app.logger.info(args)
|
|
||||||
ci_type = args.get("ci_type")
|
ci_type = args.get("ci_type")
|
||||||
_no_attribute_policy = args.get("no_attribute_policy", ExistPolicy.IGNORE)
|
_no_attribute_policy = args.get("no_attribute_policy", ExistPolicy.IGNORE)
|
||||||
|
|
||||||
@@ -104,14 +102,14 @@ class CIView(APIView):
|
|||||||
manager = CIManager()
|
manager = CIManager()
|
||||||
if ci_id is not None:
|
if ci_id is not None:
|
||||||
manager.update(ci_id,
|
manager.update(ci_id,
|
||||||
_is_admin=request.values.pop('__is_admin', False),
|
_is_admin=request.values.pop('__is_admin', None) or False,
|
||||||
**ci_dict)
|
**ci_dict)
|
||||||
else:
|
else:
|
||||||
request.values.pop('exist_policy', None)
|
request.values.pop('exist_policy', None)
|
||||||
ci_id = manager.add(ci_type,
|
ci_id = manager.add(ci_type,
|
||||||
exist_policy=ExistPolicy.REPLACE,
|
exist_policy=ExistPolicy.REPLACE,
|
||||||
_no_attribute_policy=_no_attribute_policy,
|
_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)
|
**ci_dict)
|
||||||
|
|
||||||
return self.jsonify(ci_id=ci_id)
|
return self.jsonify(ci_id=ci_id)
|
||||||
@@ -228,11 +226,11 @@ class CIFlushView(APIView):
|
|||||||
from api.tasks.cmdb import ci_cache
|
from api.tasks.cmdb import ci_cache
|
||||||
from api.lib.cmdb.const import CMDB_QUEUE
|
from api.lib.cmdb.const import CMDB_QUEUE
|
||||||
if ci_id is not None:
|
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:
|
else:
|
||||||
cis = CI.get_by(to_dict=False)
|
cis = CI.get_by(to_dict=False)
|
||||||
for ci in cis:
|
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)
|
return self.jsonify(code=200)
|
||||||
|
|
||||||
@@ -242,3 +240,13 @@ class CIAutoDiscoveryStatisticsView(APIView):
|
|||||||
|
|
||||||
def get(self):
|
def get(self):
|
||||||
return self.jsonify(CIManager.get_ad_statistics())
|
return self.jsonify(CIManager.get_ad_statistics())
|
||||||
|
|
||||||
|
|
||||||
|
class CIPasswordView(APIView):
|
||||||
|
url_prefix = "/ci/<int:ci_id>/attributes/<int:attr_id>/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)
|
||||||
|
37
cmdb-api/api/views/cmdb/inner_secrets.py
Normal file
37
cmdb-api/api/views/cmdb/inner_secrets.py
Normal file
@@ -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)
|
1
cmdb-api/migrations/README
Normal file
1
cmdb-api/migrations/README
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Generic single-database configuration.
|
45
cmdb-api/migrations/alembic.ini
Normal file
45
cmdb-api/migrations/alembic.ini
Normal file
@@ -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
|
110
cmdb-api/migrations/env.py
Normal file
110
cmdb-api/migrations/env.py
Normal file
@@ -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()
|
24
cmdb-api/migrations/script.py.mako
Normal file
24
cmdb-api/migrations/script.py.mako
Normal file
@@ -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"}
|
360
cmdb-api/migrations/versions/6a4df2623057_.py
Normal file
360
cmdb-api/migrations/versions/6a4df2623057_.py
Normal file
@@ -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 ###
|
@@ -1,7 +1,7 @@
|
|||||||
-i https://mirrors.aliyun.com/pypi/simple
|
-i https://mirrors.aliyun.com/pypi/simple
|
||||||
alembic==1.7.7
|
alembic==1.7.7
|
||||||
bs4==0.0.1
|
bs4==0.0.1
|
||||||
celery==5.3.1
|
celery>=5.3.1
|
||||||
celery-once==3.0.1
|
celery-once==3.0.1
|
||||||
click==8.1.3
|
click==8.1.3
|
||||||
elasticsearch==7.17.9
|
elasticsearch==7.17.9
|
||||||
@@ -18,21 +18,22 @@ Flask-RESTful==0.3.10
|
|||||||
Flask-SQLAlchemy==2.5.0
|
Flask-SQLAlchemy==2.5.0
|
||||||
future==0.18.3
|
future==0.18.3
|
||||||
gunicorn==21.0.1
|
gunicorn==21.0.1
|
||||||
|
hvac==2.0.0
|
||||||
itsdangerous==2.1.2
|
itsdangerous==2.1.2
|
||||||
Jinja2==3.1.2
|
Jinja2==3.1.2
|
||||||
jinja2schema==0.1.4
|
jinja2schema==0.1.4
|
||||||
jsonschema==4.18.0
|
jsonschema==4.18.0
|
||||||
kombu==5.3.1
|
kombu>=5.3.1
|
||||||
Mako==1.2.4
|
Mako==1.2.4
|
||||||
MarkupSafe==2.1.3
|
MarkupSafe==2.1.3
|
||||||
marshmallow==2.20.2
|
marshmallow==2.20.2
|
||||||
more-itertools==5.0.0
|
more-itertools==5.0.0
|
||||||
msgpack-python==0.5.6
|
msgpack-python==0.5.6
|
||||||
Pillow==9.3.0
|
Pillow==9.3.0
|
||||||
pycryptodome==3.12.0
|
cryptography==41.0.2
|
||||||
PyJWT==2.4.0
|
PyJWT==2.4.0
|
||||||
PyMySQL==1.1.0
|
PyMySQL==1.1.0
|
||||||
python-ldap==3.4.0
|
ldap3==2.9.1
|
||||||
PyYAML==6.0
|
PyYAML==6.0
|
||||||
redis==4.6.0
|
redis==4.6.0
|
||||||
requests==2.31.0
|
requests==2.31.0
|
||||||
@@ -46,3 +47,7 @@ toposort==1.10
|
|||||||
treelib==1.6.1
|
treelib==1.6.1
|
||||||
Werkzeug==2.3.6
|
Werkzeug==2.3.6
|
||||||
WTForms==3.0.0
|
WTForms==3.0.0
|
||||||
|
shamir~=17.12.0
|
||||||
|
hvac~=2.0.0
|
||||||
|
pycryptodomex>=3.19.0
|
||||||
|
colorama>=0.4.6
|
||||||
|
@@ -97,3 +97,9 @@ BOOL_TRUE = ['true', 'TRUE', 'True', True, '1', 1, "Yes", "YES", "yes", 'Y', 'y'
|
|||||||
|
|
||||||
# # messenger
|
# # messenger
|
||||||
USE_MESSENGER = True
|
USE_MESSENGER = True
|
||||||
|
|
||||||
|
# # secrets
|
||||||
|
SECRETS_ENGINE = 'inner' # 'inner' or 'vault'
|
||||||
|
VAULT_URL = ''
|
||||||
|
VAULT_TOKEN = ''
|
||||||
|
INNER_TRIGGER_TOKEN = ''
|
||||||
|
@@ -54,6 +54,48 @@
|
|||||||
<div class="content unicode" style="display: block;">
|
<div class="content unicode" style="display: block;">
|
||||||
<ul class="icon_lists dib-box">
|
<ul class="icon_lists dib-box">
|
||||||
|
|
||||||
|
<li class="dib">
|
||||||
|
<span class="icon iconfont"></span>
|
||||||
|
<div class="name">icon-xianxing-password</div>
|
||||||
|
<div class="code-name">&#xe894;</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="dib">
|
||||||
|
<span class="icon iconfont"></span>
|
||||||
|
<div class="name">icon-xianxing-link</div>
|
||||||
|
<div class="code-name">&#xe895;</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="dib">
|
||||||
|
<span class="icon iconfont"></span>
|
||||||
|
<div class="name">itsm-oneclick download</div>
|
||||||
|
<div class="code-name">&#xe892;</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="dib">
|
||||||
|
<span class="icon iconfont"></span>
|
||||||
|
<div class="name">itsm-package download</div>
|
||||||
|
<div class="code-name">&#xe893;</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="dib">
|
||||||
|
<span class="icon iconfont"></span>
|
||||||
|
<div class="name">weixin</div>
|
||||||
|
<div class="code-name">&#xe891;</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="dib">
|
||||||
|
<span class="icon iconfont"></span>
|
||||||
|
<div class="name">itsm-again</div>
|
||||||
|
<div class="code-name">&#xe88f;</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="dib">
|
||||||
|
<span class="icon iconfont"></span>
|
||||||
|
<div class="name">itsm-next</div>
|
||||||
|
<div class="code-name">&#xe890;</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li class="dib">
|
<li class="dib">
|
||||||
<span class="icon iconfont"></span>
|
<span class="icon iconfont"></span>
|
||||||
<div class="name">wechatApp</div>
|
<div class="name">wechatApp</div>
|
||||||
@@ -4002,9 +4044,9 @@
|
|||||||
<pre><code class="language-css"
|
<pre><code class="language-css"
|
||||||
>@font-face {
|
>@font-face {
|
||||||
font-family: 'iconfont';
|
font-family: 'iconfont';
|
||||||
src: url('iconfont.woff2?t=1696815443987') format('woff2'),
|
src: url('iconfont.woff2?t=1698273699449') format('woff2'),
|
||||||
url('iconfont.woff?t=1696815443987') format('woff'),
|
url('iconfont.woff?t=1698273699449') format('woff'),
|
||||||
url('iconfont.ttf?t=1696815443987') format('truetype');
|
url('iconfont.ttf?t=1698273699449') format('truetype');
|
||||||
}
|
}
|
||||||
</code></pre>
|
</code></pre>
|
||||||
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
|
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
|
||||||
@@ -4030,6 +4072,69 @@
|
|||||||
<div class="content font-class">
|
<div class="content font-class">
|
||||||
<ul class="icon_lists dib-box">
|
<ul class="icon_lists dib-box">
|
||||||
|
|
||||||
|
<li class="dib">
|
||||||
|
<span class="icon iconfont icon-xianxing-password"></span>
|
||||||
|
<div class="name">
|
||||||
|
icon-xianxing-password
|
||||||
|
</div>
|
||||||
|
<div class="code-name">.icon-xianxing-password
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="dib">
|
||||||
|
<span class="icon iconfont icon-xianxing-link"></span>
|
||||||
|
<div class="name">
|
||||||
|
icon-xianxing-link
|
||||||
|
</div>
|
||||||
|
<div class="code-name">.icon-xianxing-link
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="dib">
|
||||||
|
<span class="icon iconfont a-itsm-oneclickdownload"></span>
|
||||||
|
<div class="name">
|
||||||
|
itsm-oneclick download
|
||||||
|
</div>
|
||||||
|
<div class="code-name">.a-itsm-oneclickdownload
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="dib">
|
||||||
|
<span class="icon iconfont a-itsm-packagedownload"></span>
|
||||||
|
<div class="name">
|
||||||
|
itsm-package download
|
||||||
|
</div>
|
||||||
|
<div class="code-name">.a-itsm-packagedownload
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="dib">
|
||||||
|
<span class="icon iconfont a-Frame4"></span>
|
||||||
|
<div class="name">
|
||||||
|
weixin
|
||||||
|
</div>
|
||||||
|
<div class="code-name">.a-Frame4
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="dib">
|
||||||
|
<span class="icon iconfont itsm-again"></span>
|
||||||
|
<div class="name">
|
||||||
|
itsm-again
|
||||||
|
</div>
|
||||||
|
<div class="code-name">.itsm-again
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="dib">
|
||||||
|
<span class="icon iconfont itsm-next"></span>
|
||||||
|
<div class="name">
|
||||||
|
itsm-next
|
||||||
|
</div>
|
||||||
|
<div class="code-name">.itsm-next
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li class="dib">
|
<li class="dib">
|
||||||
<span class="icon iconfont wechatApp"></span>
|
<span class="icon iconfont wechatApp"></span>
|
||||||
<div class="name">
|
<div class="name">
|
||||||
@@ -9952,6 +10057,62 @@
|
|||||||
<div class="content symbol">
|
<div class="content symbol">
|
||||||
<ul class="icon_lists dib-box">
|
<ul class="icon_lists dib-box">
|
||||||
|
|
||||||
|
<li class="dib">
|
||||||
|
<svg class="icon svg-icon" aria-hidden="true">
|
||||||
|
<use xlink:href="#icon-xianxing-password"></use>
|
||||||
|
</svg>
|
||||||
|
<div class="name">icon-xianxing-password</div>
|
||||||
|
<div class="code-name">#icon-xianxing-password</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="dib">
|
||||||
|
<svg class="icon svg-icon" aria-hidden="true">
|
||||||
|
<use xlink:href="#icon-xianxing-link"></use>
|
||||||
|
</svg>
|
||||||
|
<div class="name">icon-xianxing-link</div>
|
||||||
|
<div class="code-name">#icon-xianxing-link</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="dib">
|
||||||
|
<svg class="icon svg-icon" aria-hidden="true">
|
||||||
|
<use xlink:href="#a-itsm-oneclickdownload"></use>
|
||||||
|
</svg>
|
||||||
|
<div class="name">itsm-oneclick download</div>
|
||||||
|
<div class="code-name">#a-itsm-oneclickdownload</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="dib">
|
||||||
|
<svg class="icon svg-icon" aria-hidden="true">
|
||||||
|
<use xlink:href="#a-itsm-packagedownload"></use>
|
||||||
|
</svg>
|
||||||
|
<div class="name">itsm-package download</div>
|
||||||
|
<div class="code-name">#a-itsm-packagedownload</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="dib">
|
||||||
|
<svg class="icon svg-icon" aria-hidden="true">
|
||||||
|
<use xlink:href="#a-Frame4"></use>
|
||||||
|
</svg>
|
||||||
|
<div class="name">weixin</div>
|
||||||
|
<div class="code-name">#a-Frame4</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="dib">
|
||||||
|
<svg class="icon svg-icon" aria-hidden="true">
|
||||||
|
<use xlink:href="#itsm-again"></use>
|
||||||
|
</svg>
|
||||||
|
<div class="name">itsm-again</div>
|
||||||
|
<div class="code-name">#itsm-again</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="dib">
|
||||||
|
<svg class="icon svg-icon" aria-hidden="true">
|
||||||
|
<use xlink:href="#itsm-next"></use>
|
||||||
|
</svg>
|
||||||
|
<div class="name">itsm-next</div>
|
||||||
|
<div class="code-name">#itsm-next</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li class="dib">
|
<li class="dib">
|
||||||
<svg class="icon svg-icon" aria-hidden="true">
|
<svg class="icon svg-icon" aria-hidden="true">
|
||||||
<use xlink:href="#wechatApp"></use>
|
<use xlink:href="#wechatApp"></use>
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: "iconfont"; /* Project id 3857903 */
|
font-family: "iconfont"; /* Project id 3857903 */
|
||||||
src: url('iconfont.woff2?t=1696815443987') format('woff2'),
|
src: url('iconfont.woff2?t=1698273699449') format('woff2'),
|
||||||
url('iconfont.woff?t=1696815443987') format('woff'),
|
url('iconfont.woff?t=1698273699449') format('woff'),
|
||||||
url('iconfont.ttf?t=1696815443987') format('truetype');
|
url('iconfont.ttf?t=1698273699449') format('truetype');
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconfont {
|
.iconfont {
|
||||||
@@ -13,6 +13,34 @@
|
|||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon-xianxing-password:before {
|
||||||
|
content: "\e894";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-xianxing-link:before {
|
||||||
|
content: "\e895";
|
||||||
|
}
|
||||||
|
|
||||||
|
.a-itsm-oneclickdownload:before {
|
||||||
|
content: "\e892";
|
||||||
|
}
|
||||||
|
|
||||||
|
.a-itsm-packagedownload:before {
|
||||||
|
content: "\e893";
|
||||||
|
}
|
||||||
|
|
||||||
|
.a-Frame4:before {
|
||||||
|
content: "\e891";
|
||||||
|
}
|
||||||
|
|
||||||
|
.itsm-again:before {
|
||||||
|
content: "\e88f";
|
||||||
|
}
|
||||||
|
|
||||||
|
.itsm-next:before {
|
||||||
|
content: "\e890";
|
||||||
|
}
|
||||||
|
|
||||||
.wechatApp:before {
|
.wechatApp:before {
|
||||||
content: "\e88e";
|
content: "\e88e";
|
||||||
}
|
}
|
||||||
|
File diff suppressed because one or more lines are too long
@@ -5,6 +5,55 @@
|
|||||||
"css_prefix_text": "",
|
"css_prefix_text": "",
|
||||||
"description": "",
|
"description": "",
|
||||||
"glyphs": [
|
"glyphs": [
|
||||||
|
{
|
||||||
|
"icon_id": "37830610",
|
||||||
|
"name": "icon-xianxing-password",
|
||||||
|
"font_class": "icon-xianxing-password",
|
||||||
|
"unicode": "e894",
|
||||||
|
"unicode_decimal": 59540
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "37830609",
|
||||||
|
"name": "icon-xianxing-link",
|
||||||
|
"font_class": "icon-xianxing-link",
|
||||||
|
"unicode": "e895",
|
||||||
|
"unicode_decimal": 59541
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "37822199",
|
||||||
|
"name": "itsm-oneclick download",
|
||||||
|
"font_class": "a-itsm-oneclickdownload",
|
||||||
|
"unicode": "e892",
|
||||||
|
"unicode_decimal": 59538
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "37822198",
|
||||||
|
"name": "itsm-package download",
|
||||||
|
"font_class": "a-itsm-packagedownload",
|
||||||
|
"unicode": "e893",
|
||||||
|
"unicode_decimal": 59539
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "37772067",
|
||||||
|
"name": "weixin",
|
||||||
|
"font_class": "a-Frame4",
|
||||||
|
"unicode": "e891",
|
||||||
|
"unicode_decimal": 59537
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "37632784",
|
||||||
|
"name": "itsm-again",
|
||||||
|
"font_class": "itsm-again",
|
||||||
|
"unicode": "e88f",
|
||||||
|
"unicode_decimal": 59535
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "37632783",
|
||||||
|
"name": "itsm-next",
|
||||||
|
"font_class": "itsm-next",
|
||||||
|
"unicode": "e890",
|
||||||
|
"unicode_decimal": 59536
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"icon_id": "37590786",
|
"icon_id": "37590786",
|
||||||
"name": "wechatApp",
|
"name": "wechatApp",
|
||||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -12,7 +12,10 @@
|
|||||||
<a-button type="primary" ghost>条件过滤<a-icon type="filter"/></a-button>
|
<a-button type="primary" ghost>条件过滤<a-icon type="filter"/></a-button>
|
||||||
</slot>
|
</slot>
|
||||||
<template slot="content">
|
<template slot="content">
|
||||||
<Expression v-model="ruleList" :canSearchPreferenceAttrList="canSearchPreferenceAttrList" />
|
<Expression
|
||||||
|
v-model="ruleList"
|
||||||
|
:canSearchPreferenceAttrList="canSearchPreferenceAttrList.filter((attr) => !attr.is_password)"
|
||||||
|
/>
|
||||||
<a-divider :style="{ margin: '10px 0' }" />
|
<a-divider :style="{ margin: '10px 0' }" />
|
||||||
<div style="width:534px">
|
<div style="width:534px">
|
||||||
<a-space :style="{ display: 'flex', justifyContent: 'flex-end' }">
|
<a-space :style="{ display: 'flex', justifyContent: 'flex-end' }">
|
||||||
@@ -22,7 +25,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</a-popover>
|
</a-popover>
|
||||||
<Expression v-else v-model="ruleList" :canSearchPreferenceAttrList="canSearchPreferenceAttrList" />
|
<Expression
|
||||||
|
v-else
|
||||||
|
v-model="ruleList"
|
||||||
|
:canSearchPreferenceAttrList="canSearchPreferenceAttrList.filter((attr) => !attr.is_password)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -152,14 +159,15 @@ export default {
|
|||||||
})
|
})
|
||||||
this.ruleList = [...expArray]
|
this.ruleList = [...expArray]
|
||||||
} else if (open) {
|
} else if (open) {
|
||||||
|
const _canSearchPreferenceAttrList = this.canSearchPreferenceAttrList.filter((attr) => !attr.is_password)
|
||||||
this.ruleList = isInitOne
|
this.ruleList = isInitOne
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
type: 'and',
|
type: 'and',
|
||||||
property:
|
property:
|
||||||
this.canSearchPreferenceAttrList && this.canSearchPreferenceAttrList.length
|
_canSearchPreferenceAttrList && _canSearchPreferenceAttrList.length
|
||||||
? this.canSearchPreferenceAttrList[0].name
|
? _canSearchPreferenceAttrList[0].name
|
||||||
: undefined,
|
: undefined,
|
||||||
exp: 'is',
|
exp: 'is',
|
||||||
value: null,
|
value: null,
|
||||||
|
@@ -14,24 +14,6 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
getPropertyStyle(attr) {
|
|
||||||
switch (attr.value_type) {
|
|
||||||
case '0':
|
|
||||||
return { color: '#cf1322', backgroundColor: '#fff1f0' }
|
|
||||||
case '1':
|
|
||||||
return { color: '#d4b106', backgroundColor: '#feffe6' }
|
|
||||||
case '2':
|
|
||||||
return { color: '#d46b08', backgroundColor: '#fff7e6' }
|
|
||||||
case '3':
|
|
||||||
return { color: '#531dab', backgroundColor: '#f9f0ff' }
|
|
||||||
case '4':
|
|
||||||
return { color: '#389e0d', backgroundColor: '#f6ffed' }
|
|
||||||
case '5':
|
|
||||||
return { color: '#08979c', backgroundColor: '#e6fffb' }
|
|
||||||
case '6':
|
|
||||||
return { color: '#c41d7f', backgroundColor: '#fff0f6' }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getPropertyIcon(attr) {
|
getPropertyIcon(attr) {
|
||||||
switch (attr.value_type) {
|
switch (attr.value_type) {
|
||||||
case '0':
|
case '0':
|
||||||
@@ -39,6 +21,12 @@ export default {
|
|||||||
case '1':
|
case '1':
|
||||||
return 'icon-xianxing-fudianshu'
|
return 'icon-xianxing-fudianshu'
|
||||||
case '2':
|
case '2':
|
||||||
|
if (attr.is_password) {
|
||||||
|
return 'icon-xianxing-password'
|
||||||
|
}
|
||||||
|
if (attr.is_link) {
|
||||||
|
return 'icon-xianxing-link'
|
||||||
|
}
|
||||||
return 'icon-xianxing-wenben'
|
return 'icon-xianxing-wenben'
|
||||||
case '3':
|
case '3':
|
||||||
return 'icon-xianxing-datetime'
|
return 'icon-xianxing-datetime'
|
||||||
|
@@ -4,7 +4,7 @@ const appConfig = {
|
|||||||
buildAclToModules: true, // 是否在各个应用下 内联权限管理
|
buildAclToModules: true, // 是否在各个应用下 内联权限管理
|
||||||
ssoLogoutURL: '/api/sso/logout',
|
ssoLogoutURL: '/api/sso/logout',
|
||||||
showDocs: false,
|
showDocs: false,
|
||||||
useEncryption: true,
|
useEncryption: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default appConfig
|
export default appConfig
|
||||||
|
@@ -99,7 +99,7 @@
|
|||||||
align="center"
|
align="center"
|
||||||
show-overflow>
|
show-overflow>
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span v-show="row.isGroup">
|
<span v-show="isGroup">
|
||||||
<a @click="handleDisplayMember(row)">成员</a>
|
<a @click="handleDisplayMember(row)">成员</a>
|
||||||
<a-divider type="vertical" />
|
<a-divider type="vertical" />
|
||||||
<a @click="handleGroupEdit(row)">编辑</a>
|
<a @click="handleGroupEdit(row)">编辑</a>
|
||||||
|
@@ -35,8 +35,8 @@ export default {
|
|||||||
secret: '',
|
secret: '',
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
key: [{ required: true, message: 'key is required' }],
|
key: [{ required: false, message: 'key is required' }],
|
||||||
secret: [{ required: true, message: 'secret is required' }],
|
secret: [{ required: false, message: 'secret is required' }],
|
||||||
},
|
},
|
||||||
visible: false,
|
visible: false,
|
||||||
}
|
}
|
||||||
|
@@ -111,10 +111,8 @@ export default {
|
|||||||
},
|
},
|
||||||
async beforeMount() {
|
async beforeMount() {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
await getOnDutyUser().then((res) => {
|
await this.getOnDutyUser()
|
||||||
this.onDutuUids = res.map((i) => i.uid)
|
|
||||||
this.search()
|
this.search()
|
||||||
})
|
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState({
|
...mapState({
|
||||||
@@ -148,6 +146,11 @@ export default {
|
|||||||
inject: ['reload'],
|
inject: ['reload'],
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
async getOnDutyUser() {
|
||||||
|
await getOnDutyUser().then((res) => {
|
||||||
|
this.onDutuUids = res.map((i) => i.uid)
|
||||||
|
})
|
||||||
|
},
|
||||||
search() {
|
search() {
|
||||||
searchUser({ page_size: 10000 }).then((res) => {
|
searchUser({ page_size: 10000 }).then((res) => {
|
||||||
const ret = res.users.filter((u) => this.onDutuUids.includes(u.uid))
|
const ret = res.users.filter((u) => this.onDutuUids.includes(u.uid))
|
||||||
@@ -162,8 +165,9 @@ export default {
|
|||||||
handleEdit(record) {
|
handleEdit(record) {
|
||||||
this.$refs.userForm.handleEdit(record)
|
this.$refs.userForm.handleEdit(record)
|
||||||
},
|
},
|
||||||
handleOk() {
|
async handleOk() {
|
||||||
this.searchName = ''
|
this.searchName = ''
|
||||||
|
await this.getOnDutyUser()
|
||||||
this.search()
|
this.search()
|
||||||
},
|
},
|
||||||
handleCreate() {
|
handleCreate() {
|
||||||
|
@@ -168,3 +168,10 @@ export function calcComputedAttribute(attr_id) {
|
|||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getAttrPassword(ci_id, attr_id) {
|
||||||
|
return axios({
|
||||||
|
url: `/v0.1/ci/${ci_id}/attributes/${attr_id}/password`,
|
||||||
|
method: 'Get',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@@ -2,32 +2,29 @@
|
|||||||
<div>
|
<div>
|
||||||
<span v-if="!isShow && !isTableLoading">{{ showPassword }}</span>
|
<span v-if="!isShow && !isTableLoading">{{ showPassword }}</span>
|
||||||
<span v-else>{{ password }}</span>
|
<span v-else>{{ password }}</span>
|
||||||
<a
|
<a :style="{ marginLeft: '10px' }" @click="getPassword"><a-icon :type="isShow ? 'eye-invisible' : 'eye'"/></a>
|
||||||
:style="{ marginLeft: '10px' }"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
isShow = !isShow
|
|
||||||
}
|
|
||||||
"
|
|
||||||
><a-icon
|
|
||||||
:type="isShow ? 'eye-invisible' : 'eye'"
|
|
||||||
/></a>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
|
import { getAttrPassword } from '../../api/CITypeAttr'
|
||||||
export default {
|
export default {
|
||||||
name: 'PasswordField',
|
name: 'PasswordField',
|
||||||
props: {
|
props: {
|
||||||
password: {
|
ci_id: {
|
||||||
type: String,
|
type: Number,
|
||||||
default: '',
|
default: 0,
|
||||||
|
},
|
||||||
|
attr_id: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
isShow: false,
|
isShow: false,
|
||||||
|
password: '',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -36,6 +33,18 @@ export default {
|
|||||||
},
|
},
|
||||||
...mapState('cmdbStore', ['isTableLoading']),
|
...mapState('cmdbStore', ['isTableLoading']),
|
||||||
},
|
},
|
||||||
|
methods: {
|
||||||
|
getPassword() {
|
||||||
|
if (this.isShow) {
|
||||||
|
this.isShow = false
|
||||||
|
} else {
|
||||||
|
getAttrPassword(this.ci_id, this.attr_id).then((res) => {
|
||||||
|
this.password = res.value
|
||||||
|
this.isShow = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@@ -16,7 +16,7 @@
|
|||||||
</a-space> -->
|
</a-space> -->
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-top:10px">
|
<div style="margin-top:10px">
|
||||||
<vue-json-editor v-model="jsonData" :showBtns="false" :mode="'text'" />
|
<codemirror style="z-index: 9999" :options="cmOptions" v-model="jsonData"></codemirror>
|
||||||
<!-- <a-empty
|
<!-- <a-empty
|
||||||
v-else
|
v-else
|
||||||
:image-style="{
|
:image-style="{
|
||||||
@@ -31,11 +31,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import vueJsonEditor from 'vue-json-editor'
|
import { codemirror } from 'vue-codemirror'
|
||||||
|
import 'codemirror/lib/codemirror.css'
|
||||||
|
|
||||||
|
require('codemirror/mode/python/python.js')
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Body',
|
name: 'Body',
|
||||||
components: { vueJsonEditor },
|
components: { codemirror },
|
||||||
data() {
|
data() {
|
||||||
const segmentedContentTypes = [
|
const segmentedContentTypes = [
|
||||||
{
|
{
|
||||||
@@ -60,7 +63,14 @@ export default {
|
|||||||
return {
|
return {
|
||||||
segmentedContentTypes,
|
segmentedContentTypes,
|
||||||
// contentType: 'none',
|
// contentType: 'none',
|
||||||
jsonData: {},
|
jsonData: '',
|
||||||
|
cmOptions: {
|
||||||
|
lineNumbers: true,
|
||||||
|
mode: 'python',
|
||||||
|
height: '200px',
|
||||||
|
tabSize: 4,
|
||||||
|
lineWrapping: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@@ -81,7 +81,11 @@ export default {
|
|||||||
this.$refs.Parameters.parameters.forEach((item) => {
|
this.$refs.Parameters.parameters.forEach((item) => {
|
||||||
parameters[item.key] = item.value
|
parameters[item.key] = item.value
|
||||||
})
|
})
|
||||||
const body = this.$refs.Body.jsonData
|
let body = this.$refs.Body.jsonData
|
||||||
|
try {
|
||||||
|
JSON.parse(body)
|
||||||
|
body = JSON.parse(body)
|
||||||
|
} catch {}
|
||||||
const headers = {}
|
const headers = {}
|
||||||
this.$refs.Header.headers.forEach((item) => {
|
this.$refs.Header.headers.forEach((item) => {
|
||||||
headers[item.key] = item.value
|
headers[item.key] = item.value
|
||||||
@@ -99,7 +103,6 @@ export default {
|
|||||||
return { method, url, parameters, body, headers, authorization }
|
return { method, url, parameters, body, headers, authorization }
|
||||||
},
|
},
|
||||||
setParams(params) {
|
setParams(params) {
|
||||||
console.log(2222, params)
|
|
||||||
const { method, url, parameters, body, headers, authorization = {} } = params ?? {}
|
const { method, url, parameters, body, headers, authorization = {} } = params ?? {}
|
||||||
this.method = method
|
this.method = method
|
||||||
this.url = url
|
this.url = url
|
||||||
@@ -111,7 +114,11 @@ export default {
|
|||||||
value: parameters[key],
|
value: parameters[key],
|
||||||
}
|
}
|
||||||
}) || []
|
}) || []
|
||||||
|
if (body && Object.prototype.toString.call(body) === '[object Object]') {
|
||||||
|
this.$refs.Body.jsonData = JSON.stringify(body)
|
||||||
|
} else {
|
||||||
this.$refs.Body.jsonData = body
|
this.$refs.Body.jsonData = body
|
||||||
|
}
|
||||||
this.$refs.Header.headers =
|
this.$refs.Header.headers =
|
||||||
Object.keys(headers).map((key) => {
|
Object.keys(headers).map((key) => {
|
||||||
return {
|
return {
|
||||||
|
@@ -61,18 +61,18 @@ const genCmdbRoutes = async () => {
|
|||||||
name: 'cmdb_disabled2',
|
name: 'cmdb_disabled2',
|
||||||
meta: { title: '配置', disabled: true, },
|
meta: { title: '配置', disabled: true, },
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/cmdb/batch',
|
|
||||||
component: () => import('../views/batch'),
|
|
||||||
name: 'cmdb_batch',
|
|
||||||
meta: { 'title': '批量导入', icon: 'ops-cmdb-batch', selectedIcon: 'ops-cmdb-batch-selected', keepAlive: false }
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/cmdb/preference',
|
path: '/cmdb/preference',
|
||||||
component: () => import('../views/preference/index'),
|
component: () => import('../views/preference/index'),
|
||||||
name: 'cmdb_preference',
|
name: 'cmdb_preference',
|
||||||
meta: { title: '我的订阅', icon: 'ops-cmdb-preference', selectedIcon: 'ops-cmdb-preference-selected', keepAlive: false }
|
meta: { title: '我的订阅', icon: 'ops-cmdb-preference', selectedIcon: 'ops-cmdb-preference-selected', keepAlive: false }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/cmdb/batch',
|
||||||
|
component: () => import('../views/batch'),
|
||||||
|
name: 'cmdb_batch',
|
||||||
|
meta: { 'title': '批量导入', icon: 'ops-cmdb-batch', selectedIcon: 'ops-cmdb-batch-selected', keepAlive: false }
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/cmdb/ci_types',
|
path: '/cmdb/ci_types',
|
||||||
name: 'ci_type',
|
name: 'ci_type',
|
||||||
|
@@ -2,10 +2,12 @@ export const valueTypeMap = {
|
|||||||
'0': '整数',
|
'0': '整数',
|
||||||
'1': '浮点数',
|
'1': '浮点数',
|
||||||
'2': '文本',
|
'2': '文本',
|
||||||
'3': 'datetime',
|
'3': '日期时间',
|
||||||
'4': 'date',
|
'4': '日期',
|
||||||
'5': 'time',
|
'5': '时间',
|
||||||
'6': 'json'
|
'6': 'JSON',
|
||||||
|
'7': '密码',
|
||||||
|
'8': '链接'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defautValueColor = [
|
export const defautValueColor = [
|
||||||
|
@@ -85,6 +85,7 @@ export function getCITableColumns(data, attrList, width = 1600, height) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
columns.push({
|
columns.push({
|
||||||
|
attr_id:attr.id,
|
||||||
editRender,
|
editRender,
|
||||||
title: attr.alias || attr.name,
|
title: attr.alias || attr.name,
|
||||||
field: attr.name,
|
field: attr.name,
|
||||||
@@ -127,6 +128,10 @@ export const getPropertyStyle = (attr) => {
|
|||||||
return { color: '#08979c', backgroundColor: '#e6fffb' }
|
return { color: '#08979c', backgroundColor: '#e6fffb' }
|
||||||
case '6':
|
case '6':
|
||||||
return { color: '#c41d7f', backgroundColor: '#fff0f6' }
|
return { color: '#c41d7f', backgroundColor: '#fff0f6' }
|
||||||
|
case '7':
|
||||||
|
return { color: '#0390CC', backgroundColor: '#e6fffb' }
|
||||||
|
case '8':
|
||||||
|
return { color: '#144BD9', backgroundColor: '#fff0f6' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -5,7 +5,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<a-row>
|
<a-row>
|
||||||
<a-col :span="12">
|
<a-col :span="12">
|
||||||
<upload-file-form ref="uploadFileForm" @uploadDone="uploadDone"></upload-file-form>
|
<upload-file-form :ciType="ciType" ref="uploadFileForm" @uploadDone="uploadDone"></upload-file-form>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :span="24" v-if="ciType && uploadData.length">
|
<a-col :span="24" v-if="ciType && uploadData.length">
|
||||||
<CiUploadTable :ciTypeAttrs="ciTypeAttrs" ref="ciUploadTable" :uploadData="uploadData"></CiUploadTable>
|
<CiUploadTable :ciTypeAttrs="ciTypeAttrs" ref="ciUploadTable" :uploadData="uploadData"></CiUploadTable>
|
||||||
|
@@ -7,6 +7,7 @@
|
|||||||
accept=".xls,.xlsx"
|
accept=".xls,.xlsx"
|
||||||
:showUploadList="false"
|
:showUploadList="false"
|
||||||
:fileList="fileList"
|
:fileList="fileList"
|
||||||
|
:disabled="!ciType"
|
||||||
>
|
>
|
||||||
<img :style="{ width: '80px', height: '80px' }" src="@/assets/file_upload.png" />
|
<img :style="{ width: '80px', height: '80px' }" src="@/assets/file_upload.png" />
|
||||||
<p class="ant-upload-text">点击或拖拽文件至此上传!</p>
|
<p class="ant-upload-text">点击或拖拽文件至此上传!</p>
|
||||||
@@ -24,6 +25,12 @@ import { processFile } from '@/modules/cmdb/api/batch'
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'UploadFileForm',
|
name: 'UploadFileForm',
|
||||||
|
props: {
|
||||||
|
ciType: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
}
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
ciItemNum: 0,
|
ciItemNum: 0,
|
||||||
|
@@ -99,6 +99,7 @@
|
|||||||
:cell-type="col.value_type === '2' ? 'string' : 'auto'"
|
:cell-type="col.value_type === '2' ? 'string' : 'auto'"
|
||||||
:fixed="col.is_fixed ? 'left' : ''"
|
:fixed="col.is_fixed ? 'left' : ''"
|
||||||
>
|
>
|
||||||
|
<!-- <template #edit="{row}"><a-input v-model="row[col.field]"></a-input></template> -->
|
||||||
<template #header>
|
<template #header>
|
||||||
<span class="vxe-handle">
|
<span class="vxe-handle">
|
||||||
<OpsMoveIcon
|
<OpsMoveIcon
|
||||||
@@ -107,7 +108,8 @@
|
|||||||
<span>{{ col.title }}</span>
|
<span>{{ col.title }}</span>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="col.is_choice" #edit="{ row }">
|
<template v-if="col.is_choice || col.is_password" #edit="{ row }">
|
||||||
|
<vxe-input v-if="col.is_password" v-model="passwordValue[col.field]" />
|
||||||
<a-select
|
<a-select
|
||||||
:getPopupContainer="(trigger) => trigger.parentElement"
|
:getPopupContainer="(trigger) => trigger.parentElement"
|
||||||
:style="{ width: '100%', height: '32px' }"
|
:style="{ width: '100%', height: '32px' }"
|
||||||
@@ -149,8 +151,21 @@
|
|||||||
#default="{ row }"
|
#default="{ row }"
|
||||||
>
|
>
|
||||||
<span v-if="col.value_type === '6' && row[col.field]">{{ row[col.field] }}</span>
|
<span v-if="col.value_type === '6' && row[col.field]">{{ row[col.field] }}</span>
|
||||||
<a v-else-if="col.is_link" :href="`${row[col.field]}`" target="_blank">{{ row[col.field] }}</a>
|
<a
|
||||||
<PasswordField v-else-if="col.is_password && row[col.field]" :password="row[col.field]"></PasswordField>
|
v-else-if="col.is_link && row[col.field]"
|
||||||
|
:href="
|
||||||
|
row[col.field].startsWith('http') || row[col.field].startsWith('https')
|
||||||
|
? `${row[col.field]}`
|
||||||
|
: `http://${row[col.field]}`
|
||||||
|
"
|
||||||
|
target="_blank"
|
||||||
|
>{{ row[col.field] }}</a
|
||||||
|
>
|
||||||
|
<PasswordField
|
||||||
|
v-else-if="col.is_password && row[col.field]"
|
||||||
|
:ci_id="row._id"
|
||||||
|
:attr_id="col.attr_id"
|
||||||
|
></PasswordField>
|
||||||
<template v-else-if="col.is_choice">
|
<template v-else-if="col.is_choice">
|
||||||
<template v-if="col.is_list">
|
<template v-if="col.is_list">
|
||||||
<span
|
<span
|
||||||
@@ -198,8 +213,8 @@
|
|||||||
:style="{ color: getChoiceValueIcon(col, row[col.field]).color, marginRight: '5px' }"
|
:style="{ color: getChoiceValueIcon(col, row[col.field]).color, marginRight: '5px' }"
|
||||||
:type="getChoiceValueIcon(col, row[col.field]).name"
|
:type="getChoiceValueIcon(col, row[col.field]).name"
|
||||||
/>
|
/>
|
||||||
{{ row[col.field] }}</span
|
{{ row[col.field] }}
|
||||||
>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</vxe-table-column>
|
</vxe-table-column>
|
||||||
@@ -207,6 +222,7 @@
|
|||||||
<template #header>
|
<template #header>
|
||||||
<span>操作</span>
|
<span>操作</span>
|
||||||
<EditAttrsPopover :typeId="typeId" class="operation-icon" @refresh="refreshAfterEditAttrs" />
|
<EditAttrsPopover :typeId="typeId" class="operation-icon" @refresh="refreshAfterEditAttrs" />
|
||||||
|
<!-- <a-icon class="operation-icon" type="control" /> -->
|
||||||
</template>
|
</template>
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<a-space>
|
<a-space>
|
||||||
@@ -289,6 +305,7 @@ import PreferenceSearch from '../../components/preferenceSearch/preferenceSearch
|
|||||||
import MetadataDrawer from './modules/MetadataDrawer.vue'
|
import MetadataDrawer from './modules/MetadataDrawer.vue'
|
||||||
import CMDBGrant from '../../components/cmdbGrant'
|
import CMDBGrant from '../../components/cmdbGrant'
|
||||||
import { ops_move_icon as OpsMoveIcon } from '@/core/icons'
|
import { ops_move_icon as OpsMoveIcon } from '@/core/icons'
|
||||||
|
import { getAttrPassword } from '../../api/CITypeAttr'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'InstanceList',
|
name: 'InstanceList',
|
||||||
@@ -348,13 +365,18 @@ export default {
|
|||||||
tableDragClassName: [],
|
tableDragClassName: [],
|
||||||
|
|
||||||
resource_type: {},
|
resource_type: {},
|
||||||
|
|
||||||
|
initialPasswordValue: {},
|
||||||
|
passwordValue: {},
|
||||||
|
lastEditCiId: null,
|
||||||
|
isContinueCloseEdit: true,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
'$route.path': function (newPath, oldPath) {
|
'$route.path': function(newPath, oldPath) {
|
||||||
this.reloadBoard()
|
this.reloadBoard()
|
||||||
},
|
},
|
||||||
currentPage: function (newVal, oldVal) {
|
currentPage: function(newVal, oldVal) {
|
||||||
this.loadTableData(this.sortByTable)
|
this.loadTableData(this.sortByTable)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -438,6 +460,12 @@ export default {
|
|||||||
})
|
})
|
||||||
this.totalNumber = res['numfound']
|
this.totalNumber = res['numfound']
|
||||||
this.columns = this.getColumns(res.result, this.preferenceAttrList)
|
this.columns = this.getColumns(res.result, this.preferenceAttrList)
|
||||||
|
this.columns.forEach((col) => {
|
||||||
|
if (col.is_password) {
|
||||||
|
this.initialPasswordValue[col.field] = ''
|
||||||
|
this.passwordValue[col.field] = ''
|
||||||
|
}
|
||||||
|
})
|
||||||
const jsonAttrList = this.attrList.filter((attr) => attr.value_type === '6')
|
const jsonAttrList = this.attrList.filter((attr) => attr.value_type === '6')
|
||||||
this.instanceList = res['result'].map((item) => {
|
this.instanceList = res['result'].map((item) => {
|
||||||
jsonAttrList.forEach(
|
jsonAttrList.forEach(
|
||||||
@@ -492,14 +520,24 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
handleEditClose({ row, rowIndex, column }) {
|
handleEditClose({ row, rowIndex, column }) {
|
||||||
|
if (!this.isContinueCloseEdit) {
|
||||||
|
return
|
||||||
|
}
|
||||||
const $table = this.$refs['xTable'].getVxetableRef()
|
const $table = this.$refs['xTable'].getVxetableRef()
|
||||||
const data = {}
|
const data = {}
|
||||||
this.columns.forEach((item) => {
|
this.columns.forEach((item) => {
|
||||||
if (!_.isEqual(row[item.field], this.initialInstanceList[rowIndex][item.field])) {
|
if (!(item.field in this.initialPasswordValue) && !_.isEqual(row[item.field], this.initialInstanceList[rowIndex][item.field])) {
|
||||||
data[item.field] = row[item.field] || null
|
data[item.field] = row[item.field] ?? null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
Object.keys(this.initialPasswordValue).forEach((key) => {
|
||||||
|
if (this.initialPasswordValue[key] !== this.passwordValue[key]) {
|
||||||
|
data[key] = this.passwordValue[key]
|
||||||
|
row[key] = this.passwordValue[key]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
this.isEditActive = false
|
this.isEditActive = false
|
||||||
|
this.lastEditCiId = null
|
||||||
if (JSON.stringify(data) !== '{}') {
|
if (JSON.stringify(data) !== '{}') {
|
||||||
updateCI(row.ci_id || row._id, data)
|
updateCI(row.ci_id || row._id, data)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -517,6 +555,12 @@ export default {
|
|||||||
$table.revertData(row)
|
$table.revertData(row)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
this.columns.forEach((col) => {
|
||||||
|
if (col.is_password) {
|
||||||
|
this.initialPasswordValue[col.field] = ''
|
||||||
|
this.passwordValue[col.field] = ''
|
||||||
|
}
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
async openBatchDownload() {
|
async openBatchDownload() {
|
||||||
@@ -716,7 +760,7 @@ export default {
|
|||||||
},
|
},
|
||||||
onEnd: (params) => {
|
onEnd: (params) => {
|
||||||
// 由于开启了虚拟滚动,newIndex和oldIndex是虚拟的
|
// 由于开启了虚拟滚动,newIndex和oldIndex是虚拟的
|
||||||
const { newIndex, oldIndex } = params
|
const { newIndex, oldIndex, from, to } = params
|
||||||
// 从tableDragClassName拿到colid
|
// 从tableDragClassName拿到colid
|
||||||
const fromColid = this.tableDragClassName[oldIndex]
|
const fromColid = this.tableDragClassName[oldIndex]
|
||||||
const toColid = this.tableDragClassName[newIndex]
|
const toColid = this.tableDragClassName[newIndex]
|
||||||
@@ -749,6 +793,28 @@ export default {
|
|||||||
},
|
},
|
||||||
handleEditActived() {
|
handleEditActived() {
|
||||||
this.isEditActive = true
|
this.isEditActive = true
|
||||||
|
const passwordCol = this.columns.filter((col) => col.is_password)
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const editRecord = this.$refs.xTable.getVxetableRef().getEditRecord()
|
||||||
|
const { row, column } = editRecord
|
||||||
|
if (passwordCol.length && this.lastEditCiId !== row._id) {
|
||||||
|
this.$nextTick(async () => {
|
||||||
|
for (let i = 0; i < passwordCol.length; i++) {
|
||||||
|
await getAttrPassword(row._id, passwordCol[i].attr_id).then((res) => {
|
||||||
|
this.initialPasswordValue[passwordCol[i].field] = res.value
|
||||||
|
this.passwordValue[passwordCol[i].field] = res.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.isContinueCloseEdit = false
|
||||||
|
await this.$refs.xTable.getVxetableRef().clearEdit()
|
||||||
|
this.isContinueCloseEdit = true
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.xTable.getVxetableRef().setEditCell(row, column.field)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.lastEditCiId = row._id
|
||||||
|
})
|
||||||
},
|
},
|
||||||
getCellStyle({ row, rowIndex, $rowIndex, column, columnIndex, $columnIndex }) {
|
getCellStyle({ row, rowIndex, $rowIndex, column, columnIndex, $columnIndex }) {
|
||||||
const { property } = column
|
const { property } = column
|
||||||
|
@@ -30,7 +30,7 @@
|
|||||||
:key="attr.name"
|
:key="attr.name"
|
||||||
v-for="attr in group.attributes"
|
v-for="attr in group.attributes"
|
||||||
>
|
>
|
||||||
<CiDetailAttrContent :ci="ci" :attr="attr" @refresh="refresh" />
|
<CiDetailAttrContent :ci="ci" :attr="attr" @refresh="refresh" @updateCIByself="updateCIByself" />
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
</div>
|
</div>
|
||||||
@@ -97,6 +97,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import _ from 'lodash'
|
||||||
import { Descriptions, DescriptionsItem } from 'element-ui'
|
import { Descriptions, DescriptionsItem } from 'element-ui'
|
||||||
import { getCITypeGroupById, getCITypes } from '@/modules/cmdb/api/CIType'
|
import { getCITypeGroupById, getCITypes } from '@/modules/cmdb/api/CIType'
|
||||||
import { getCIHistory } from '@/modules/cmdb/api/history'
|
import { getCIHistory } from '@/modules/cmdb/api/history'
|
||||||
@@ -292,6 +293,19 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
updateCIByself(params, editAttrName) {
|
||||||
|
const _ci = { ..._.cloneDeep(this.ci), ...params }
|
||||||
|
this.ci = _ci
|
||||||
|
const _find = this.treeViewsLevels.find((level) => level.name === editAttrName)
|
||||||
|
// 修改的字段为树形视图订阅的字段 则全部reload
|
||||||
|
setTimeout(() => {
|
||||||
|
if (_find) {
|
||||||
|
this.reload()
|
||||||
|
} else {
|
||||||
|
this.handleSearch()
|
||||||
|
}
|
||||||
|
}, 500)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@@ -114,7 +114,7 @@
|
|||||||
<a-col :span="2">
|
<a-col :span="2">
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<a :style="{ color: 'red', marginTop: '2px' }" @click="handleDelete(list.name)">
|
<a :style="{ color: 'red', marginTop: '2px' }" @click="handleDelete(list.name)">
|
||||||
<a-icon type="delete"/>
|
<a-icon type="delete" />
|
||||||
</a>
|
</a>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
@@ -122,6 +122,7 @@
|
|||||||
<a-button type="primary" ghost icon="plus" @click="handleAdd">新增修改字段</a-button>
|
<a-button type="primary" ghost icon="plus" @click="handleAdd">新增修改字段</a-button>
|
||||||
</a-form>
|
</a-form>
|
||||||
</template>
|
</template>
|
||||||
|
<!-- </a-form> -->
|
||||||
<JsonEditor ref="jsonEditor" @jsonEditorOk="jsonEditorOk" />
|
<JsonEditor ref="jsonEditor" @jsonEditorOk="jsonEditorOk" />
|
||||||
</CustomDrawer>
|
</CustomDrawer>
|
||||||
</template>
|
</template>
|
||||||
@@ -222,10 +223,18 @@ export default {
|
|||||||
}
|
}
|
||||||
Object.keys(values).forEach((k) => {
|
Object.keys(values).forEach((k) => {
|
||||||
const _tempFind = this.attributeList.find((item) => item.name === k)
|
const _tempFind = this.attributeList.find((item) => item.name === k)
|
||||||
if (_tempFind.value_type === '3' && values[k]) {
|
if (
|
||||||
|
_tempFind.value_type === '3' &&
|
||||||
|
values[k] &&
|
||||||
|
Object.prototype.toString.call(values[k]) === '[object Object]'
|
||||||
|
) {
|
||||||
values[k] = values[k].format('YYYY-MM-DD HH:mm:ss')
|
values[k] = values[k].format('YYYY-MM-DD HH:mm:ss')
|
||||||
}
|
}
|
||||||
if (_tempFind.value_type === '4' && values[k]) {
|
if (
|
||||||
|
_tempFind.value_type === '4' &&
|
||||||
|
values[k] &&
|
||||||
|
Object.prototype.toString.call(values[k]) === '[object Object]'
|
||||||
|
) {
|
||||||
values[k] = values[k].format('YYYY-MM-DD')
|
values[k] = values[k].format('YYYY-MM-DD')
|
||||||
}
|
}
|
||||||
if (_tempFind.value_type === '6') {
|
if (_tempFind.value_type === '6') {
|
||||||
@@ -247,10 +256,18 @@ export default {
|
|||||||
|
|
||||||
Object.keys(values).forEach((k) => {
|
Object.keys(values).forEach((k) => {
|
||||||
const _tempFind = this.attributeList.find((item) => item.name === k)
|
const _tempFind = this.attributeList.find((item) => item.name === k)
|
||||||
if (_tempFind.value_type === '3' && values[k]) {
|
if (
|
||||||
|
_tempFind.value_type === '3' &&
|
||||||
|
values[k] &&
|
||||||
|
Object.prototype.toString.call(values[k]) === '[object Object]'
|
||||||
|
) {
|
||||||
values[k] = values[k].format('YYYY-MM-DD HH:mm:ss')
|
values[k] = values[k].format('YYYY-MM-DD HH:mm:ss')
|
||||||
}
|
}
|
||||||
if (_tempFind.value_type === '4' && values[k]) {
|
if (
|
||||||
|
_tempFind.value_type === '4' &&
|
||||||
|
values[k] &&
|
||||||
|
Object.prototype.toString.call(values[k]) === '[object Object]'
|
||||||
|
) {
|
||||||
values[k] = values[k].format('YYYY-MM-DD')
|
values[k] = values[k].format('YYYY-MM-DD')
|
||||||
}
|
}
|
||||||
if (_tempFind.value_type === '6') {
|
if (_tempFind.value_type === '6') {
|
||||||
@@ -258,6 +275,7 @@ export default {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
values.ci_type = _this.typeId
|
values.ci_type = _this.typeId
|
||||||
|
console.log(this.parentsForm)
|
||||||
Object.keys(this.parentsForm).forEach((type) => {
|
Object.keys(this.parentsForm).forEach((type) => {
|
||||||
if (this.parentsForm[type].value) {
|
if (this.parentsForm[type].value) {
|
||||||
values[`$${type}.${this.parentsForm[type].attr}`] = this.parentsForm[type].value
|
values[`$${type}.${this.parentsForm[type].attr}`] = this.parentsForm[type].value
|
||||||
|
@@ -51,8 +51,15 @@
|
|||||||
:sortable="index < 3 ? true : false"
|
:sortable="index < 3 ? true : false"
|
||||||
:title-help="column.help !== null ? { message: column.help } : null"
|
:title-help="column.help !== null ? { message: column.help } : null"
|
||||||
:filters="
|
:filters="
|
||||||
index < 2 ? null: index === 2
|
index < 2
|
||||||
? valueTypeFilters: [{ label: '是', value: true }, { label: '否', value: false },]"
|
? null
|
||||||
|
: index === 2
|
||||||
|
? valueTypeFilters
|
||||||
|
: [
|
||||||
|
{ label: '是', value: true },
|
||||||
|
{ label: '否', value: false },
|
||||||
|
]
|
||||||
|
"
|
||||||
type="html"
|
type="html"
|
||||||
>
|
>
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
@@ -133,18 +140,6 @@ export default {
|
|||||||
width: 100,
|
width: 100,
|
||||||
help: '仅针对前端',
|
help: '仅针对前端',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
field: 'is_password',
|
|
||||||
title: '是否密码',
|
|
||||||
width: 100,
|
|
||||||
help: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'is_link',
|
|
||||||
title: '是否链接',
|
|
||||||
width: 110,
|
|
||||||
help: null,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
field: 'is_computed',
|
field: 'is_computed',
|
||||||
title: '计算属性',
|
title: '计算属性',
|
||||||
@@ -168,7 +163,7 @@ export default {
|
|||||||
return this.$store.state.windowHeight
|
return this.$store.state.windowHeight
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
created: function () {
|
created: function() {
|
||||||
this.valueTypeFilters = Object.keys(this.valueTypeMap).map((key) => {
|
this.valueTypeFilters = Object.keys(this.valueTypeMap).map((key) => {
|
||||||
return { label: this.valueTypeMap[key], value: key }
|
return { label: this.valueTypeMap[key], value: key }
|
||||||
})
|
})
|
||||||
@@ -182,17 +177,32 @@ export default {
|
|||||||
async getAttrs() {
|
async getAttrs() {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
const { attributes = [] } = await getCITypeAttributesByName(this.typeId)
|
const { attributes = [] } = await getCITypeAttributesByName(this.typeId)
|
||||||
this.tableData = attributes
|
this.tableData = attributes.map((attr) => {
|
||||||
|
if (attr.is_password) {
|
||||||
|
attr.value_type = '7'
|
||||||
|
}
|
||||||
|
if (attr.is_link) {
|
||||||
|
attr.value_type = '8'
|
||||||
|
}
|
||||||
|
return attr
|
||||||
|
})
|
||||||
this.loading = false
|
this.loading = false
|
||||||
this.searchAttributes()
|
this.searchAttributes()
|
||||||
},
|
},
|
||||||
searchAttributes() {
|
searchAttributes() {
|
||||||
const filterName = XEUtils.toValueString(this.searchKey).trim().toLowerCase()
|
const filterName = XEUtils.toValueString(this.searchKey)
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
if (filterName) {
|
if (filterName) {
|
||||||
const filterRE = new RegExp(filterName, 'gi')
|
const filterRE = new RegExp(filterName, 'gi')
|
||||||
const searchProps = ['name', 'alias', 'value_type']
|
const searchProps = ['name', 'alias', 'value_type']
|
||||||
const rest = this.tableData.filter((item) =>
|
const rest = this.tableData.filter((item) =>
|
||||||
searchProps.some((key) => XEUtils.toValueString(item[key]).toLowerCase().indexOf(filterName) > -1)
|
searchProps.some(
|
||||||
|
(key) =>
|
||||||
|
XEUtils.toValueString(item[key])
|
||||||
|
.toLowerCase()
|
||||||
|
.indexOf(filterName) > -1
|
||||||
|
)
|
||||||
)
|
)
|
||||||
this.list = rest.map((row) => {
|
this.list = rest.map((row) => {
|
||||||
const item = Object.assign({}, row)
|
const item = Object.assign({}, row)
|
||||||
|
@@ -1,7 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<span :id="`ci-detail-attr-${attr.name}`">
|
<span :id="`ci-detail-attr-${attr.name}`">
|
||||||
<span v-if="!isEdit || attr.value_type === '6'">
|
<span v-if="!isEdit || attr.value_type === '6'">
|
||||||
<template v-if="attr.value_type === '6'">{{ JSON.stringify(ci[attr.name] || {}) }}</template>
|
<PasswordField
|
||||||
|
:style="{ display: 'inline-block' }"
|
||||||
|
v-if="attr.is_password && ci[attr.name]"
|
||||||
|
:ci_id="ci._id"
|
||||||
|
:attr_id="attr.id"
|
||||||
|
></PasswordField>
|
||||||
|
<template v-else-if="attr.value_type === '6'">{{ JSON.stringify(ci[attr.name] || {}) }}</template>
|
||||||
<template v-else-if="attr.is_choice">
|
<template v-else-if="attr.is_choice">
|
||||||
<template v-if="attr.is_list">
|
<template v-if="attr.is_list">
|
||||||
<span
|
<span
|
||||||
@@ -12,10 +18,18 @@
|
|||||||
padding: '1px 5px',
|
padding: '1px 5px',
|
||||||
margin: '2px',
|
margin: '2px',
|
||||||
...getChoiceValueStyle(attr, value),
|
...getChoiceValueStyle(attr, value),
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
|
<img
|
||||||
|
v-if="getChoiceValueIcon(attr, value).id && getChoiceValueIcon(attr, value).url"
|
||||||
|
:src="`/api/common-setting/v1/file/${getChoiceValueIcon(attr, value).url}`"
|
||||||
|
:style="{ maxHeight: '13px', maxWidth: '13px', marginRight: '5px' }"
|
||||||
|
/>
|
||||||
<ops-icon
|
<ops-icon
|
||||||
:style="{ color: getChoiceValueIcon(attr, value).color }"
|
v-else
|
||||||
|
:style="{ color: getChoiceValueIcon(attr, value).color, marginRight: '5px' }"
|
||||||
:type="getChoiceValueIcon(attr, value).name"
|
:type="getChoiceValueIcon(attr, value).name"
|
||||||
/>
|
/>
|
||||||
{{ value }}</span
|
{{ value }}</span
|
||||||
@@ -28,10 +42,18 @@
|
|||||||
padding: '1px 5px',
|
padding: '1px 5px',
|
||||||
margin: '2px 0',
|
margin: '2px 0',
|
||||||
...getChoiceValueStyle(attr, ci[attr.name]),
|
...getChoiceValueStyle(attr, ci[attr.name]),
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
|
<img
|
||||||
|
v-if="getChoiceValueIcon(attr, ci[attr.name]).id && getChoiceValueIcon(attr, ci[attr.name]).url"
|
||||||
|
:src="`/api/common-setting/v1/file/${getChoiceValueIcon(attr, ci[attr.name]).url}`"
|
||||||
|
:style="{ maxHeight: '13px', maxWidth: '13px', marginRight: '5px' }"
|
||||||
|
/>
|
||||||
<ops-icon
|
<ops-icon
|
||||||
:style="{ color: getChoiceValueIcon(attr, ci[attr.name]).color }"
|
v-else
|
||||||
|
:style="{ color: getChoiceValueIcon(attr, ci[attr.name]).color, marginRight: '5px' }"
|
||||||
:type="getChoiceValueIcon(attr, ci[attr.name]).name"
|
:type="getChoiceValueIcon(attr, ci[attr.name]).name"
|
||||||
/>
|
/>
|
||||||
{{ ci[attr.name] }}
|
{{ ci[attr.name] }}
|
||||||
@@ -64,12 +86,19 @@
|
|||||||
:key="'New_' + attr.name + choice_idx"
|
:key="'New_' + attr.name + choice_idx"
|
||||||
v-for="(choice, choice_idx) in attr.choice_value"
|
v-for="(choice, choice_idx) in attr.choice_value"
|
||||||
>
|
>
|
||||||
<span :style="choice[1] ? choice[1].style || {} : {}">
|
<span :style="{ ...(choice[1] ? choice[1].style : {}), display: 'inline-flex', alignItems: 'center' }">
|
||||||
|
<template v-if="choice[1] && choice[1].icon && choice[1].icon.name">
|
||||||
|
<img
|
||||||
|
v-if="choice[1].icon.id && choice[1].icon.url"
|
||||||
|
:src="`/api/common-setting/v1/file/${choice[1].icon.url}`"
|
||||||
|
:style="{ maxHeight: '13px', maxWidth: '13px', marginRight: '5px' }"
|
||||||
|
/>
|
||||||
<ops-icon
|
<ops-icon
|
||||||
:style="{ color: choice[1].icon.color }"
|
v-else
|
||||||
v-if="choice[1] && choice[1].icon && choice[1].icon.name"
|
:style="{ color: choice[1].icon.color, marginRight: '5px' }"
|
||||||
:type="choice[1].icon.name"
|
:type="choice[1].icon.name"
|
||||||
/>
|
/>
|
||||||
|
</template>
|
||||||
{{ choice[0] }}
|
{{ choice[0] }}
|
||||||
</span>
|
</span>
|
||||||
</a-select-option>
|
</a-select-option>
|
||||||
@@ -99,6 +128,19 @@
|
|||||||
v-else-if="attr.value_type === '4' || attr.value_type === '3'"
|
v-else-if="attr.value_type === '4' || attr.value_type === '3'"
|
||||||
:showTime="attr.value_type === '4' ? false : { format: 'HH:mm:ss' }"
|
:showTime="attr.value_type === '4' ? false : { format: 'HH:mm:ss' }"
|
||||||
/>
|
/>
|
||||||
|
<!-- <a-input
|
||||||
|
size="small"
|
||||||
|
@focus="(e) => handleFocusInput(e, attr)"
|
||||||
|
v-decorator="[
|
||||||
|
attr.name,
|
||||||
|
{
|
||||||
|
validateTrigger: ['submit'],
|
||||||
|
rules: [{ required: attr.is_required }],
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
style="width: 100%"
|
||||||
|
v-else-if="attr.value_type === '6'"
|
||||||
|
/> -->
|
||||||
<a-input
|
<a-input
|
||||||
size="small"
|
size="small"
|
||||||
v-decorator="[
|
v-decorator="[
|
||||||
@@ -124,9 +166,12 @@ import _ from 'lodash'
|
|||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import { updateCI } from '@/modules/cmdb/api/ci'
|
import { updateCI } from '@/modules/cmdb/api/ci'
|
||||||
import JsonEditor from '../../../components/JsonEditor/jsonEditor.vue'
|
import JsonEditor from '../../../components/JsonEditor/jsonEditor.vue'
|
||||||
|
import PasswordField from '../../../components/passwordField/index.vue'
|
||||||
|
import { getAttrPassword } from '../../../api/CITypeAttr'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'CiDetailAttrContent',
|
name: 'CiDetailAttrContent',
|
||||||
components: { JsonEditor },
|
components: { JsonEditor, PasswordField },
|
||||||
props: {
|
props: {
|
||||||
ci: {
|
ci: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -174,13 +219,21 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.isEdit = true
|
this.isEdit = true
|
||||||
this.$nextTick(() => {
|
this.$nextTick(async () => {
|
||||||
if (this.attr.is_list && !this.attr.is_choice) {
|
if (this.attr.is_list && !this.attr.is_choice) {
|
||||||
this.form.setFieldsValue({
|
this.form.setFieldsValue({
|
||||||
[`${this.attr.name}`]: this.ci[this.attr.name].join(',') || null,
|
[`${this.attr.name}`]: this.ci[this.attr.name].join(',') || null,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (this.attr.is_password) {
|
||||||
|
await getAttrPassword(this.ci._id, this.attr.id).then((res) => {
|
||||||
|
this.form.setFieldsValue({
|
||||||
|
[`${this.attr.name}`]: res.value ?? null,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
this.form.setFieldsValue({
|
this.form.setFieldsValue({
|
||||||
[`${this.attr.name}`]: this.ci[this.attr.name] ?? null,
|
[`${this.attr.name}`]: this.ci[this.attr.name] ?? null,
|
||||||
})
|
})
|
||||||
@@ -192,20 +245,32 @@ export default {
|
|||||||
await updateCI(this.ci._id, { [`${this.attr.name}`]: newData })
|
await updateCI(this.ci._id, { [`${this.attr.name}`]: newData })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.$message.success('更新成功!')
|
this.$message.success('更新成功!')
|
||||||
|
this.$emit('updateCIByself', { [`${this.attr.name}`]: newData }, this.attr.name)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.catch(() => {
|
||||||
this.$emit('refresh', this.attr.name)
|
this.$emit('refresh', this.attr.name)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
this.isEdit = false
|
this.isEdit = false
|
||||||
},
|
},
|
||||||
|
// handleFocusInput(e, attr) {
|
||||||
|
// console.log('focus')
|
||||||
|
// if (this.attr.value_type === '6') {
|
||||||
|
// e.preventDefault()
|
||||||
|
// e.stopPropagation()
|
||||||
|
// // e.srcElement.blur()
|
||||||
|
// const jsonData = this.form.getFieldValue(attr.name)
|
||||||
|
// this.$refs.jsonEditor.open(null, null, jsonData ? JSON.parse(jsonData) : {})
|
||||||
|
// }
|
||||||
|
// },
|
||||||
jsonEditorOk(jsonData) {
|
jsonEditorOk(jsonData) {
|
||||||
if (!_.isEqual(this.ci[this.attr.name], jsonData)) {
|
if (!_.isEqual(this.ci[this.attr.name], jsonData)) {
|
||||||
updateCI(this.ci._id, { [`${this.attr.name}`]: jsonData })
|
updateCI(this.ci._id, { [`${this.attr.name}`]: jsonData })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.$message.success('更新成功!')
|
this.$message.success('更新成功!')
|
||||||
|
this.$emit('updateCIByself', { [`${this.attr.name}`]: jsonData }, this.attr.name)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.catch(() => {
|
||||||
this.$emit('refresh', this.attr.name)
|
this.$emit('refresh', this.attr.name)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@@ -8,7 +8,9 @@
|
|||||||
<div :class="{ 'attribute-card-name': true, 'attribute-card-name-default-show': property.default_show }">
|
<div :class="{ 'attribute-card-name': true, 'attribute-card-name-default-show': property.default_show }">
|
||||||
{{ property.alias || property.name }}
|
{{ property.alias || property.name }}
|
||||||
</div>
|
</div>
|
||||||
<div class="attribute-card_value-type">{{ valueTypeMap[property.value_type] }}</div>
|
<div v-if="property.is_password" class="attribute-card_value-type">密码</div>
|
||||||
|
<div v-else-if="property.is_link" class="attribute-card_value-type">链接</div>
|
||||||
|
<div v-else class="attribute-card_value-type">{{ valueTypeMap[property.value_type] }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="attribute-card-trigger"
|
class="attribute-card-trigger"
|
||||||
@@ -125,14 +127,6 @@ export default {
|
|||||||
label: '是否索引',
|
label: '是否索引',
|
||||||
property: 'is_index',
|
property: 'is_index',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: '是否密码',
|
|
||||||
property: 'is_password',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '是否链接',
|
|
||||||
property: 'is_link',
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
return {
|
return {
|
||||||
valueTypeMap,
|
valueTypeMap,
|
||||||
|
@@ -84,7 +84,12 @@
|
|||||||
</a-input-number>
|
</a-input-number>
|
||||||
<a-input
|
<a-input
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
v-else-if="currentValueType === '2' || currentValueType === '5'"
|
v-else-if="
|
||||||
|
currentValueType === '2' ||
|
||||||
|
currentValueType === '5' ||
|
||||||
|
currentValueType === '7' ||
|
||||||
|
currentValueType === '8'
|
||||||
|
"
|
||||||
v-decorator="['default_value', { rules: [{ required: false }] }]"
|
v-decorator="['default_value', { rules: [{ required: false }] }]"
|
||||||
>
|
>
|
||||||
</a-input>
|
</a-input>
|
||||||
@@ -148,13 +153,13 @@
|
|||||||
label="必须"
|
label="必须"
|
||||||
>
|
>
|
||||||
<a-switch
|
<a-switch
|
||||||
@change="onChange"
|
@change="(checked) => onChange(checked, 'is_required')"
|
||||||
name="is_required"
|
name="is_required"
|
||||||
v-decorator="['is_required', { rules: [], valuePropName: 'checked' }]"
|
v-decorator="['is_required', { rules: [], valuePropName: 'checked' }]"
|
||||||
/>
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :span="6" v-if="currentValueType !== '6'">
|
<a-col :span="6" v-if="currentValueType !== '6' && currentValueType !== '7'">
|
||||||
<a-form-item :label-col="{ span: 8 }" :wrapper-col="horizontalFormItemLayout.wrapperCol" label="唯一">
|
<a-form-item :label-col="{ span: 8 }" :wrapper-col="horizontalFormItemLayout.wrapperCol" label="唯一">
|
||||||
<a-switch
|
<a-switch
|
||||||
:disabled="isShowComputedArea"
|
:disabled="isShowComputedArea"
|
||||||
@@ -228,7 +233,7 @@
|
|||||||
/>
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :span="6" v-if="currentValueType !== '6'">
|
<a-col :span="6" v-if="currentValueType !== '6' && currentValueType !== '7'">
|
||||||
<a-form-item
|
<a-form-item
|
||||||
:label-col="currentValueType === '2' ? horizontalFormItemLayout.labelCol : { span: 8 }"
|
:label-col="currentValueType === '2' ? horizontalFormItemLayout.labelCol : { span: 8 }"
|
||||||
:wrapper-col="horizontalFormItemLayout.wrapperCol"
|
:wrapper-col="horizontalFormItemLayout.wrapperCol"
|
||||||
@@ -236,14 +241,14 @@
|
|||||||
>
|
>
|
||||||
<a-switch
|
<a-switch
|
||||||
:disabled="isShowComputedArea"
|
:disabled="isShowComputedArea"
|
||||||
@change="onChange"
|
@change="(checked) => onChange(checked, 'is_sortable')"
|
||||||
name="is_sortable"
|
name="is_sortable"
|
||||||
v-decorator="['is_sortable', { rules: [], valuePropName: 'checked' }]"
|
v-decorator="['is_sortable', { rules: [], valuePropName: 'checked' }]"
|
||||||
/>
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
|
|
||||||
<a-col :span="6" v-if="currentValueType !== '6'">
|
<a-col :span="6" v-if="currentValueType !== '6' && currentValueType !== '7'">
|
||||||
<a-form-item
|
<a-form-item
|
||||||
:label-col="currentValueType === '2' ? { span: 8 } : horizontalFormItemLayout.labelCol"
|
:label-col="currentValueType === '2' ? { span: 8 } : horizontalFormItemLayout.labelCol"
|
||||||
:wrapper-col="horizontalFormItemLayout.wrapperCol"
|
:wrapper-col="horizontalFormItemLayout.wrapperCol"
|
||||||
@@ -275,31 +280,6 @@
|
|||||||
/>
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :span="6" v-if="currentValueType === '2'">
|
|
||||||
<a-form-item
|
|
||||||
:label-col="horizontalFormItemLayout.labelCol"
|
|
||||||
:wrapper-col="horizontalFormItemLayout.wrapperCol"
|
|
||||||
label="密码"
|
|
||||||
>
|
|
||||||
<a-switch
|
|
||||||
:disabled="isShowComputedArea"
|
|
||||||
@change="(checked) => onChange(checked, 'is_password')"
|
|
||||||
name="is_password"
|
|
||||||
v-decorator="['is_password', { rules: [], valuePropName: 'checked' }]"
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
|
||||||
</a-col>
|
|
||||||
<a-col :span="6" v-if="currentValueType === '2'">
|
|
||||||
<a-form-item :label-col="{ span: 8 }" :wrapper-col="horizontalFormItemLayout.wrapperCol" label="链接">
|
|
||||||
<a-switch
|
|
||||||
:disabled="isShowComputedArea"
|
|
||||||
@change="(checked) => onChange(checked, 'is_link')"
|
|
||||||
name="is_link"
|
|
||||||
v-decorator="['is_link', { rules: [], valuePropName: 'checked' }]"
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
|
||||||
</a-col>
|
|
||||||
|
|
||||||
<a-divider style="font-size:14px;margin-top:6px;">高级设置</a-divider>
|
<a-divider style="font-size:14px;margin-top:6px;">高级设置</a-divider>
|
||||||
<a-row>
|
<a-row>
|
||||||
<a-col :span="24">
|
<a-col :span="24">
|
||||||
@@ -307,12 +287,17 @@
|
|||||||
<FontArea ref="fontArea" />
|
<FontArea ref="fontArea" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :span="24" v-if="currentValueType !== '6'">
|
<a-col :span="24" v-if="!['6', '7'].includes(currentValueType)">
|
||||||
<a-form-item :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }" label="预定义值">
|
<a-form-item :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }" label="预定义值">
|
||||||
<PreValueArea v-if="drawerVisible" ref="preValueArea" :disabled="isShowComputedArea" />
|
<PreValueArea
|
||||||
|
v-if="drawerVisible"
|
||||||
|
:canDefineScript="canDefineScript"
|
||||||
|
ref="preValueArea"
|
||||||
|
:disabled="isShowComputedArea"
|
||||||
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :span="24" v-if="currentValueType !== '6'">
|
<a-col :span="24" v-if="!['6', '7'].includes(currentValueType)">
|
||||||
<a-form-item :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }">
|
<a-form-item :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }">
|
||||||
<template slot="label">
|
<template slot="label">
|
||||||
<span
|
<span
|
||||||
@@ -340,7 +325,7 @@
|
|||||||
<a-switch
|
<a-switch
|
||||||
:disabled="!canDefineComputed"
|
:disabled="!canDefineComputed"
|
||||||
@change="(checked) => onChange(checked, 'is_computed')"
|
@change="(checked) => onChange(checked, 'is_computed')"
|
||||||
name="is_password"
|
name="is_computed"
|
||||||
v-decorator="['is_computed', { rules: [], valuePropName: 'checked' }]"
|
v-decorator="['is_computed', { rules: [], valuePropName: 'checked' }]"
|
||||||
/>
|
/>
|
||||||
<ComputedArea
|
<ComputedArea
|
||||||
@@ -366,6 +351,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import _ from 'lodash'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import vueJsonEditor from 'vue-json-editor'
|
import vueJsonEditor from 'vue-json-editor'
|
||||||
import {
|
import {
|
||||||
@@ -434,6 +420,9 @@ export default {
|
|||||||
wrapperCol: { span: 4 },
|
wrapperCol: { span: 4 },
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
canDefineScript() {
|
||||||
|
return this.canDefineComputed
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mounted() {},
|
mounted() {},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -463,28 +452,30 @@ export default {
|
|||||||
})
|
})
|
||||||
if (this.currentValueType === '2') {
|
if (this.currentValueType === '2') {
|
||||||
this.form.setFieldsValue({
|
this.form.setFieldsValue({
|
||||||
is_password: false,
|
|
||||||
is_link: false,
|
|
||||||
is_index: true,
|
is_index: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (checked && property === 'is_password') {
|
|
||||||
this.form.setFieldsValue({
|
|
||||||
is_link: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (checked && property === 'is_link') {
|
|
||||||
this.form.setFieldsValue({
|
|
||||||
is_password: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (property === 'is_list') {
|
if (property === 'is_list') {
|
||||||
this.form.setFieldsValue({
|
this.form.setFieldsValue({
|
||||||
default_value: checked ? [] : '',
|
default_value: checked ? [] : '',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if (checked && property === 'is_sortable') {
|
||||||
|
this.$message.warning('选中排序,则必须也要选中!')
|
||||||
|
this.form.setFieldsValue({
|
||||||
|
is_required: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (!checked && property === 'is_required' && this.form.getFieldValue('is_sortable')) {
|
||||||
|
this.$message.warning('选中排序,则必须也要选中!')
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.form.setFieldsValue({
|
||||||
|
is_required: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async handleEdit(record, attributes) {
|
async handleEdit(record, attributes) {
|
||||||
@@ -494,90 +485,91 @@ export default {
|
|||||||
} catch {
|
} catch {
|
||||||
this.canDefineComputed = false
|
this.canDefineComputed = false
|
||||||
}
|
}
|
||||||
|
const _record = _.cloneDeep(record)
|
||||||
|
if (_record.is_password) {
|
||||||
|
_record.value_type = '7'
|
||||||
|
}
|
||||||
|
if (_record.is_link) {
|
||||||
|
_record.value_type = '8'
|
||||||
|
}
|
||||||
this.drawerTitle = '编辑属性'
|
this.drawerTitle = '编辑属性'
|
||||||
this.drawerVisible = true
|
this.drawerVisible = true
|
||||||
this.record = record
|
this.record = _record
|
||||||
this.currentValueType = record.value_type
|
this.currentValueType = _record.value_type
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.form.setFieldsValue({
|
this.form.setFieldsValue({
|
||||||
id: record.id,
|
id: _record.id,
|
||||||
alias: record.alias,
|
alias: _record.alias,
|
||||||
name: record.name,
|
name: _record.name,
|
||||||
value_type: record.value_type,
|
value_type: _record.value_type,
|
||||||
is_required: record.is_required,
|
is_required: _record.is_required,
|
||||||
default_show: record.default_show,
|
default_show: _record.default_show,
|
||||||
})
|
})
|
||||||
if (record.value_type !== '6') {
|
if (!['6', '7'].includes(_record.value_type)) {
|
||||||
this.form.setFieldsValue({
|
this.form.setFieldsValue({
|
||||||
is_list: record.is_list,
|
is_list: _record.is_list,
|
||||||
is_unique: record.is_unique,
|
is_unique: _record.is_unique,
|
||||||
is_index: record.is_index,
|
is_index: _record.is_index,
|
||||||
is_sortable: record.is_sortable,
|
is_sortable: _record.is_sortable,
|
||||||
is_computed: record.is_computed,
|
is_computed: _record.is_computed,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (record.value_type === '2') {
|
console.log(_record)
|
||||||
this.form.setFieldsValue({
|
if (_record.default) {
|
||||||
is_password: record.is_password,
|
|
||||||
is_link: record.is_link,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
console.log(record)
|
|
||||||
if (record.default) {
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
if (record.value_type === '0') {
|
if (_record.value_type === '0') {
|
||||||
this.form.setFieldsValue({
|
this.form.setFieldsValue({
|
||||||
default_value: record.default.default ? [record.default.default] : [],
|
default_value: _record.default.default ? [_record.default.default] : [],
|
||||||
})
|
})
|
||||||
} else if (record.value_type === '6') {
|
} else if (_record.value_type === '6') {
|
||||||
this.default_value_json = record?.default?.default || null
|
this.default_value_json = _record?.default?.default || null
|
||||||
} else if (record.value_type === '3' || record.value_type === '4') {
|
} else if (_record.value_type === '3' || _record.value_type === '4') {
|
||||||
if (record?.default?.default === '$created_at' || record?.default?.default === '$updated_at') {
|
if (_record?.default?.default === '$created_at' || _record?.default?.default === '$updated_at') {
|
||||||
this.defaultForDatetime = record.default.default
|
this.defaultForDatetime = _record.default.default
|
||||||
this.form.setFieldsValue({
|
this.form.setFieldsValue({
|
||||||
default_value: record?.default?.default,
|
default_value: _record?.default?.default,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
this.defaultForDatetime = '$custom_time'
|
this.defaultForDatetime = '$custom_time'
|
||||||
this.form.setFieldsValue({
|
this.form.setFieldsValue({
|
||||||
default_value: record.default && record.default.default ? moment(record.default.default) : null,
|
default_value: _record.default && _record.default.default ? moment(_record.default.default) : null,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.form.setFieldsValue({
|
this.form.setFieldsValue({
|
||||||
default_value: record.default && record.default.default ? record.default.default : null,
|
default_value: _record.default && _record.default.default ? _record.default.default : null,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
this.default_value_json = {}
|
this.default_value_json = {}
|
||||||
if (record.value_type === '0') {
|
if (_record.value_type === '0') {
|
||||||
this.form.setFieldsValue({
|
this.form.setFieldsValue({
|
||||||
default_value: [],
|
default_value: [],
|
||||||
})
|
})
|
||||||
} else if (record.value_type !== '6') {
|
} else if (_record.value_type !== '6') {
|
||||||
this.form.setFieldsValue({
|
this.form.setFieldsValue({
|
||||||
default_value: null,
|
default_value: null,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.isShowComputedArea = record.is_computed
|
this.isShowComputedArea = _record.is_computed
|
||||||
if (record.is_computed) {
|
if (_record.is_computed) {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.$refs.computedArea.setData({
|
this.$refs.computedArea.setData({
|
||||||
compute_expr: record.compute_expr,
|
compute_expr: _record.compute_expr,
|
||||||
compute_script: record.compute_script,
|
compute_script: _record.compute_script,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const _find = attributes.find((item) => item.id === record.id)
|
const _find = attributes.find((item) => item.id === _record.id)
|
||||||
if (record.value_type !== '6') {
|
if (!['6', '7'].includes(_record.value_type)) {
|
||||||
this.$refs.preValueArea.setData({
|
this.$refs.preValueArea.setData({
|
||||||
choice_value: (_find || {}).choice_value || [],
|
choice_value: (_find || {}).choice_value || [],
|
||||||
choice_web_hook: record.choice_web_hook,
|
choice_web_hook: _record.choice_web_hook,
|
||||||
choice_other: record.choice_other || undefined,
|
choice_other: _record.choice_other || undefined,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
this.$refs.fontArea.setData({
|
this.$refs.fontArea.setData({
|
||||||
@@ -633,13 +625,20 @@ export default {
|
|||||||
values = { ...values, ...computedAreaData }
|
values = { ...values, ...computedAreaData }
|
||||||
} else {
|
} else {
|
||||||
// 如果是非计算属性,就看看有没有预定义值
|
// 如果是非计算属性,就看看有没有预定义值
|
||||||
if (values.value_type !== '6') {
|
if (!['6', '7'].includes(values.value_type)) {
|
||||||
const preValueAreaData = this.$refs.preValueArea.getData()
|
const preValueAreaData = this.$refs.preValueArea.getData()
|
||||||
values = { ...values, ...preValueAreaData }
|
values = { ...values, ...preValueAreaData }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fontOptions = this.$refs.fontArea.getData()
|
const fontOptions = this.$refs.fontArea.getData()
|
||||||
|
if (values.value_type === '7') {
|
||||||
|
values.value_type = '2'
|
||||||
|
values.is_password = true
|
||||||
|
}
|
||||||
|
if (values.value_type === '8') {
|
||||||
|
values.value_type = '2'
|
||||||
|
values.is_link = true
|
||||||
|
}
|
||||||
if (values.id) {
|
if (values.id) {
|
||||||
await this.updateAttribute(values.id, { ...values, option: { fontOptions } }, isCalcComputed)
|
await this.updateAttribute(values.id, { ...values, option: { fontOptions } }, isCalcComputed)
|
||||||
} else {
|
} else {
|
||||||
@@ -660,7 +659,6 @@ export default {
|
|||||||
handleOk() {
|
handleOk() {
|
||||||
this.$emit('ok')
|
this.$emit('ok')
|
||||||
},
|
},
|
||||||
|
|
||||||
handleChangeValueType(value) {
|
handleChangeValueType(value) {
|
||||||
this.currentValueType = value
|
this.currentValueType = value
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
|
@@ -1,9 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<a-form :form="form" class="create-new-attribute">
|
<a-form
|
||||||
|
:form="form"
|
||||||
|
class="create-new-attribute"
|
||||||
|
:label-col="formItemLayout.labelCol"
|
||||||
|
:wrapper-col="formItemLayout.wrapperCol"
|
||||||
|
>
|
||||||
<a-divider style="font-size:14px;margin-top:6px;">基础设置</a-divider>
|
<a-divider style="font-size:14px;margin-top:6px;">基础设置</a-divider>
|
||||||
<a-row>
|
<a-row>
|
||||||
<a-col :span="12">
|
<a-col :span="12">
|
||||||
<a-form-item :label-col="formItemLayout.labelCol" :wrapper-col="formItemLayout.wrapperCol" label="属性名(英文)">
|
<a-form-item label="属性名(英文)">
|
||||||
<a-input
|
<a-input
|
||||||
name="name"
|
name="name"
|
||||||
placeholder="英文"
|
placeholder="英文"
|
||||||
@@ -24,28 +29,33 @@
|
|||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :span="12">
|
<a-col :span="12">
|
||||||
<a-form-item :label-col="formItemLayout.labelCol" :wrapper-col="formItemLayout.wrapperCol" label="别名">
|
<a-form-item label="别名">
|
||||||
<a-input name="alias" v-decorator="['alias', { rules: [] }]" />
|
<a-input name="alias" v-decorator="['alias', { rules: [] }]" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
<a-row>
|
<a-row>
|
||||||
<a-col :span="12">
|
<a-col :span="12">
|
||||||
<a-form-item :label-col="formItemLayout.labelCol" :wrapper-col="formItemLayout.wrapperCol" label="数据类型">
|
<a-form-item label="数据类型">
|
||||||
<a-select
|
<a-select
|
||||||
name="value_type"
|
name="value_type"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
v-decorator="['value_type', { rules: [{ required: true }], initialValue: '2' }]"
|
v-decorator="['value_type', { rules: [{ required: true }], initialValue: '2' }]"
|
||||||
@change="handleChangeValueType"
|
@change="handleChangeValueType"
|
||||||
>
|
>
|
||||||
<a-select-option :value="key" :key="key" v-for="(value, key) in valueTypeMap">{{ value }}</a-select-option>
|
<a-select-option :value="key" :key="key" v-for="(value, key) in valueTypeMap">
|
||||||
|
{{ value }}
|
||||||
|
<span class="value-type-des" v-if="key === '3'">yyyy-mm-dd HH:MM:SS</span>
|
||||||
|
<span class="value-type-des" v-if="key === '4'">yyyy-mm-dd</span>
|
||||||
|
<span class="value-type-des" v-if="key === '5'">HH:MM:SS</span>
|
||||||
|
</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :span="currentValueType === '6' ? 24 : 12">
|
<a-col :span="currentValueType === '6' ? 24 : 12">
|
||||||
<a-form-item
|
<a-form-item
|
||||||
:label-col="{ span: currentValueType === '6' ? 4 : 8 }"
|
:label-col="{ span: currentValueType === '6' ? 4 : 8 }"
|
||||||
:wrapper-col="{ span: currentValueType === '6' ? 18 : 12 }"
|
:wrapper-col="{ span: currentValueType === '6' ? 18 : 15 }"
|
||||||
label="默认值"
|
label="默认值"
|
||||||
>
|
>
|
||||||
<template>
|
<template>
|
||||||
@@ -74,7 +84,12 @@
|
|||||||
</a-select>
|
</a-select>
|
||||||
<a-input
|
<a-input
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
v-else-if="currentValueType === '2' || currentValueType === '5'"
|
v-else-if="
|
||||||
|
currentValueType === '2' ||
|
||||||
|
currentValueType === '5' ||
|
||||||
|
currentValueType === '7' ||
|
||||||
|
currentValueType === '8'
|
||||||
|
"
|
||||||
v-decorator="['default_value', { rules: [{ required: false }] }]"
|
v-decorator="['default_value', { rules: [{ required: false }] }]"
|
||||||
>
|
>
|
||||||
</a-input>
|
</a-input>
|
||||||
@@ -140,13 +155,13 @@
|
|||||||
label="必须"
|
label="必须"
|
||||||
>
|
>
|
||||||
<a-switch
|
<a-switch
|
||||||
@change="onChange"
|
@change="(checked) => onChange(checked, 'is_required')"
|
||||||
name="is_required"
|
name="is_required"
|
||||||
v-decorator="['is_required', { rules: [], valuePropName: 'checked' }]"
|
v-decorator="['is_required', { rules: [], valuePropName: 'checked' }]"
|
||||||
/>
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :span="6" v-if="currentValueType !== '6'">
|
<a-col :span="6" v-if="currentValueType !== '6' && currentValueType !== '7'">
|
||||||
<a-form-item :label-col="{ span: 8 }" :wrapper-col="horizontalFormItemLayout.wrapperCol" label="唯一">
|
<a-form-item :label-col="{ span: 8 }" :wrapper-col="horizontalFormItemLayout.wrapperCol" label="唯一">
|
||||||
<a-switch
|
<a-switch
|
||||||
:disabled="isShowComputedArea"
|
:disabled="isShowComputedArea"
|
||||||
@@ -216,7 +231,7 @@
|
|||||||
/>
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :span="6" v-if="currentValueType !== '6'">
|
<a-col :span="6" v-if="currentValueType !== '6' && currentValueType !== '7'">
|
||||||
<a-form-item
|
<a-form-item
|
||||||
:label-col="currentValueType === '2' ? horizontalFormItemLayout.labelCol : { span: 8 }"
|
:label-col="currentValueType === '2' ? horizontalFormItemLayout.labelCol : { span: 8 }"
|
||||||
:wrapper-col="horizontalFormItemLayout.wrapperCol"
|
:wrapper-col="horizontalFormItemLayout.wrapperCol"
|
||||||
@@ -224,14 +239,14 @@
|
|||||||
>
|
>
|
||||||
<a-switch
|
<a-switch
|
||||||
:disabled="isShowComputedArea"
|
:disabled="isShowComputedArea"
|
||||||
@change="onChange"
|
@change="(checked) => onChange(checked, 'is_sortable')"
|
||||||
name="is_sortable"
|
name="is_sortable"
|
||||||
v-decorator="['is_sortable', { rules: [], valuePropName: 'checked' }]"
|
v-decorator="['is_sortable', { rules: [], valuePropName: 'checked' }]"
|
||||||
/>
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
|
|
||||||
<a-col :span="6" v-if="currentValueType !== '6'">
|
<a-col :span="6" v-if="currentValueType !== '6' && currentValueType !== '7'">
|
||||||
<a-form-item
|
<a-form-item
|
||||||
:label-col="currentValueType === '2' ? { span: 8 } : horizontalFormItemLayout.labelCol"
|
:label-col="currentValueType === '2' ? { span: 8 } : horizontalFormItemLayout.labelCol"
|
||||||
:wrapper-col="horizontalFormItemLayout.wrapperCol"
|
:wrapper-col="horizontalFormItemLayout.wrapperCol"
|
||||||
@@ -263,30 +278,6 @@
|
|||||||
/>
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :span="6" v-if="currentValueType === '2'">
|
|
||||||
<a-form-item
|
|
||||||
:label-col="horizontalFormItemLayout.labelCol"
|
|
||||||
:wrapper-col="horizontalFormItemLayout.wrapperCol"
|
|
||||||
label="密码"
|
|
||||||
>
|
|
||||||
<a-switch
|
|
||||||
:disabled="isShowComputedArea"
|
|
||||||
@change="(checked) => onChange(checked, 'is_password')"
|
|
||||||
name="is_password"
|
|
||||||
v-decorator="['is_password', { rules: [], valuePropName: 'checked' }]"
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
|
||||||
</a-col>
|
|
||||||
<a-col :span="6" v-if="currentValueType === '2'">
|
|
||||||
<a-form-item :label-col="{ span: 8 }" :wrapper-col="horizontalFormItemLayout.wrapperCol" label="链接">
|
|
||||||
<a-switch
|
|
||||||
:disabled="isShowComputedArea"
|
|
||||||
@change="(checked) => onChange(checked, 'is_link')"
|
|
||||||
name="is_link"
|
|
||||||
v-decorator="['is_link', { rules: [], valuePropName: 'checked' }]"
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
|
||||||
</a-col>
|
|
||||||
<a-divider style="font-size:14px;margin-top:6px;">高级设置</a-divider>
|
<a-divider style="font-size:14px;margin-top:6px;">高级设置</a-divider>
|
||||||
<a-row>
|
<a-row>
|
||||||
<a-col :span="24">
|
<a-col :span="24">
|
||||||
@@ -294,12 +285,12 @@
|
|||||||
<FontArea ref="fontArea" />
|
<FontArea ref="fontArea" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :span="24" v-if="currentValueType !== '6'">
|
<a-col :span="24" v-if="!['6', '7'].includes(currentValueType)">
|
||||||
<a-form-item :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }" label="预定义值">
|
<a-form-item :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }" label="预定义值">
|
||||||
<PreValueArea ref="preValueArea" :disabled="isShowComputedArea" />
|
<PreValueArea ref="preValueArea" :canDefineScript="canDefineScript" :disabled="isShowComputedArea" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :span="24">
|
<a-col :span="24" v-if="!['6', '7'].includes(currentValueType)">
|
||||||
<a-form-item :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }">
|
<a-form-item :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }">
|
||||||
<template slot="label">
|
<template slot="label">
|
||||||
<span
|
<span
|
||||||
@@ -327,7 +318,7 @@
|
|||||||
<a-switch
|
<a-switch
|
||||||
:disabled="!canDefineComputed"
|
:disabled="!canDefineComputed"
|
||||||
@change="(checked) => onChange(checked, 'is_computed')"
|
@change="(checked) => onChange(checked, 'is_computed')"
|
||||||
name="is_password"
|
name="is_computed"
|
||||||
v-decorator="['is_computed', { rules: [], valuePropName: 'checked' }]"
|
v-decorator="['is_computed', { rules: [], valuePropName: 'checked' }]"
|
||||||
/>
|
/>
|
||||||
<ComputedArea ref="computedArea" v-if="isShowComputedArea" :canDefineComputed="canDefineComputed" />
|
<ComputedArea ref="computedArea" v-if="isShowComputedArea" :canDefineComputed="canDefineComputed" />
|
||||||
@@ -369,7 +360,7 @@ export default {
|
|||||||
valueTypeMap,
|
valueTypeMap,
|
||||||
formItemLayout: {
|
formItemLayout: {
|
||||||
labelCol: { span: 8 },
|
labelCol: { span: 8 },
|
||||||
wrapperCol: { span: 12 },
|
wrapperCol: { span: 15 },
|
||||||
},
|
},
|
||||||
horizontalFormItemLayout: {
|
horizontalFormItemLayout: {
|
||||||
labelCol: { span: 16 },
|
labelCol: { span: 16 },
|
||||||
@@ -386,6 +377,11 @@ export default {
|
|||||||
defaultForDatetime: '',
|
defaultForDatetime: '',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
canDefineScript() {
|
||||||
|
return this.canDefineComputed
|
||||||
|
},
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
handleSubmit(isCloseModal = true) {
|
handleSubmit(isCloseModal = true) {
|
||||||
this.form.validateFields(async (err, values) => {
|
this.form.validateFields(async (err, values) => {
|
||||||
@@ -428,7 +424,7 @@ export default {
|
|||||||
values = { ...values, ...computedAreaData }
|
values = { ...values, ...computedAreaData }
|
||||||
} else {
|
} else {
|
||||||
// 如果是非计算属性,就看看有没有预定义值
|
// 如果是非计算属性,就看看有没有预定义值
|
||||||
if (values.value_type !== '6') {
|
if (!['6', '7'].includes(values.value_type)) {
|
||||||
const preValueAreaData = this.$refs.preValueArea.getData()
|
const preValueAreaData = this.$refs.preValueArea.getData()
|
||||||
values = { ...values, ...preValueAreaData }
|
values = { ...values, ...preValueAreaData }
|
||||||
}
|
}
|
||||||
@@ -437,17 +433,25 @@ export default {
|
|||||||
|
|
||||||
// is_index进行操作,除了文本 索引隐藏掉 文本 索引默认是true
|
// is_index进行操作,除了文本 索引隐藏掉 文本 索引默认是true
|
||||||
// 框里的5种类型 is_index=true
|
// 框里的5种类型 is_index=true
|
||||||
// json类型 is_index=false
|
// json类型、密码、链接 is_index=false
|
||||||
if (values.value_type === '6') {
|
if (['6', '7', '8'].includes(values.value_type)) {
|
||||||
values.is_index = false
|
values.is_index = false
|
||||||
} else if (values.value_type !== '2') {
|
} else if (values.value_type !== '2') {
|
||||||
values.is_index = true
|
values.is_index = true
|
||||||
}
|
}
|
||||||
|
if (values.value_type === '7') {
|
||||||
|
values.value_type = '2'
|
||||||
|
values.is_password = true
|
||||||
|
}
|
||||||
|
if (values.value_type === '8') {
|
||||||
|
values.value_type = '2'
|
||||||
|
values.is_link = true
|
||||||
|
}
|
||||||
const { attr_id } = await createAttribute({ ...values, option: { fontOptions } })
|
const { attr_id } = await createAttribute({ ...values, option: { fontOptions } })
|
||||||
|
|
||||||
this.form.resetFields()
|
this.form.resetFields()
|
||||||
this.currentValueType = '2'
|
this.currentValueType = '2'
|
||||||
if (values.value_type !== '6') {
|
if (!['6'].includes(values.value_type) && !values.is_password) {
|
||||||
this.$refs.preValueArea.valueList = []
|
this.$refs.preValueArea.valueList = []
|
||||||
}
|
}
|
||||||
this.$emit('done', attr_id, data, isCloseModal)
|
this.$emit('done', attr_id, data, isCloseModal)
|
||||||
@@ -485,29 +489,27 @@ export default {
|
|||||||
is_index: false,
|
is_index: false,
|
||||||
is_sortable: false,
|
is_sortable: false,
|
||||||
})
|
})
|
||||||
if (this.currentValueType === '2') {
|
|
||||||
this.form.setFieldsValue({
|
|
||||||
is_password: false,
|
|
||||||
is_link: false,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (checked && property === 'is_password') {
|
|
||||||
this.form.setFieldsValue({
|
|
||||||
is_link: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (checked && property === 'is_link') {
|
|
||||||
this.form.setFieldsValue({
|
|
||||||
is_password: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (property === 'is_list') {
|
if (property === 'is_list') {
|
||||||
this.form.setFieldsValue({
|
this.form.setFieldsValue({
|
||||||
default_value: checked ? [] : '',
|
default_value: checked ? [] : '',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if (checked && property === 'is_sortable') {
|
||||||
|
this.$message.warning('选中排序,则必须也要选中!')
|
||||||
|
this.form.setFieldsValue({
|
||||||
|
is_required: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (!checked && property === 'is_required' && this.form.getFieldValue('is_sortable')) {
|
||||||
|
this.$message.warning('选中排序,则必须也要选中!')
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.form.setFieldsValue({
|
||||||
|
is_required: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onJsonChange(value) {
|
onJsonChange(value) {
|
||||||
this.default_value_json_right = true
|
this.default_value_json_right = true
|
||||||
@@ -548,4 +550,8 @@ export default {
|
|||||||
background-color: #2f54eb;
|
background-color: #2f54eb;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.value-type-des {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #a9a9a9;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@@ -36,7 +36,7 @@ export default {
|
|||||||
showCalcComputed: {
|
showCalcComputed: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -48,7 +48,6 @@ export default {
|
|||||||
mode: 'python',
|
mode: 'python',
|
||||||
height: '200px',
|
height: '200px',
|
||||||
theme: 'monokai',
|
theme: 'monokai',
|
||||||
smartIndent: true,
|
|
||||||
tabSize: 4,
|
tabSize: 4,
|
||||||
lineWrapping: true,
|
lineWrapping: true,
|
||||||
readOnly: !this.canDefineComputed,
|
readOnly: !this.canDefineComputed,
|
||||||
|
@@ -388,7 +388,7 @@ export default {
|
|||||||
filterAttributes() {
|
filterAttributes() {
|
||||||
// 唯一标识 排除掉choice password 计算属性 json
|
// 唯一标识 排除掉choice password 计算属性 json
|
||||||
const _attributes = this.allAttributes.filter(
|
const _attributes = this.allAttributes.filter(
|
||||||
(item) => !item.is_choice && !item.is_password && !item.is_computed && item.value_type !== '6'
|
(item) => !item.is_choice && !item.is_computed && !['6', '7'].includes(item.value_type)
|
||||||
)
|
)
|
||||||
if (this.filterInput) {
|
if (this.filterInput) {
|
||||||
return _attributes.filter(
|
return _attributes.filter(
|
||||||
|
@@ -1,5 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<a-tabs id="preValueArea" v-model="activeKey" size="small" :tabBarStyle="{ borderBottom: 'none' }">
|
<a-tabs
|
||||||
|
id="preValueArea"
|
||||||
|
v-model="activeKey"
|
||||||
|
@change="changeActiveKey"
|
||||||
|
size="small"
|
||||||
|
:tabBarStyle="{ borderBottom: 'none' }"
|
||||||
|
>
|
||||||
<a-tab-pane key="define" :disabled="disabled">
|
<a-tab-pane key="define" :disabled="disabled">
|
||||||
<span style="font-size:12px;" slot="tab">定义</span>
|
<span style="font-size:12px;" slot="tab">定义</span>
|
||||||
<PreValueTag type="add" :item="[]" @add="addNewValue" :disabled="disabled">
|
<PreValueTag type="add" :item="[]" @add="addNewValue" :disabled="disabled">
|
||||||
@@ -170,6 +176,15 @@
|
|||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
|
<a-tab-pane key="script" :disabled="disabled || !canDefineScript">
|
||||||
|
<span style="font-size:12px;" slot="tab">脚本</span>
|
||||||
|
<CustomCodeMirror
|
||||||
|
codeMirrorId="cmdb-pre-value"
|
||||||
|
ref="codemirror"
|
||||||
|
@changeCodeContent="changeCodeContent"
|
||||||
|
></CustomCodeMirror>
|
||||||
|
<!-- <codemirror style="z-index: 9999" :options="cmOptions" v-model="script"></codemirror> -->
|
||||||
|
</a-tab-pane>
|
||||||
</a-tabs>
|
</a-tabs>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -184,14 +199,22 @@ import { getCITypeGroups } from '../../api/ciTypeGroup'
|
|||||||
import { getCITypeCommonAttributesByTypeIds } from '../../api/CITypeAttr'
|
import { getCITypeCommonAttributesByTypeIds } from '../../api/CITypeAttr'
|
||||||
import FilterComp from '@/components/CMDBFilterComp'
|
import FilterComp from '@/components/CMDBFilterComp'
|
||||||
|
|
||||||
|
import CustomCodeMirror from '@/components/CustomCodeMirror'
|
||||||
|
import 'codemirror/lib/codemirror.css'
|
||||||
|
import 'codemirror/theme/monokai.css'
|
||||||
|
require('codemirror/mode/python/python.js')
|
||||||
export default {
|
export default {
|
||||||
name: 'PreValueArea',
|
name: 'PreValueArea',
|
||||||
components: { draggable, PreValueTag, ColorPicker, Webhook, FilterComp },
|
components: { draggable, PreValueTag, ColorPicker, Webhook, FilterComp, CustomCodeMirror },
|
||||||
props: {
|
props: {
|
||||||
disabled: {
|
disabled: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
canDefineScript: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -208,6 +231,17 @@ export default {
|
|||||||
ciTypeGroup: [],
|
ciTypeGroup: [],
|
||||||
typeAttrs: [],
|
typeAttrs: [],
|
||||||
filterExp: '',
|
filterExp: '',
|
||||||
|
script:
|
||||||
|
'class ChoiceValue(object):\n @staticmethod\n def values():\n """\n 执行入口, 返回预定义值\n :return: 返回一个列表, 值的类型同属性的类型\n 例如:\n return ["在线", "下线"]\n """\n return []',
|
||||||
|
cmOptions: {
|
||||||
|
lineNumbers: true,
|
||||||
|
mode: 'python',
|
||||||
|
height: '200px',
|
||||||
|
theme: 'monokai',
|
||||||
|
tabSize: 4,
|
||||||
|
lineWrapping: true,
|
||||||
|
readOnly: this.disabled || !this.canDefineScript,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -281,6 +315,14 @@ export default {
|
|||||||
const choice_web_hook = this.$refs.webhook.getParams()
|
const choice_web_hook = this.$refs.webhook.getParams()
|
||||||
choice_web_hook.ret_key = this.form.ret_key
|
choice_web_hook.ret_key = this.form.ret_key
|
||||||
return { choice_value: [], choice_web_hook, choice_other: null }
|
return { choice_value: [], choice_web_hook, choice_other: null }
|
||||||
|
} else if (this.activeKey === 'script') {
|
||||||
|
return {
|
||||||
|
choice_value: [],
|
||||||
|
choice_web_hook: null,
|
||||||
|
choice_other: {
|
||||||
|
script: this.script,
|
||||||
|
},
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
let choice_other = {}
|
let choice_other = {}
|
||||||
if (this.choice_other.type_ids && this.choice_other.type_ids.length) {
|
if (this.choice_other.type_ids && this.choice_other.type_ids.length) {
|
||||||
@@ -302,6 +344,13 @@ export default {
|
|||||||
this.form.ret_key = choice_web_hook.ret_key ?? ''
|
this.form.ret_key = choice_web_hook.ret_key ?? ''
|
||||||
})
|
})
|
||||||
} else if (choice_other) {
|
} else if (choice_other) {
|
||||||
|
if (choice_other.script) {
|
||||||
|
this.activeKey = 'script'
|
||||||
|
this.script = choice_other.script
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.codemirror.initCodeMirror(choice_other.script)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
this.activeKey = 'choice_other'
|
this.activeKey = 'choice_other'
|
||||||
const { type_ids, attr_id, filter } = choice_other
|
const { type_ids, attr_id, filter } = choice_other
|
||||||
this.choice_other = { type_ids, attr_id }
|
this.choice_other = { type_ids, attr_id }
|
||||||
@@ -311,6 +360,7 @@ export default {
|
|||||||
this.$refs.filterComp.visibleChange(true, false)
|
this.$refs.filterComp.visibleChange(true, false)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.valueList = choice_value
|
this.valueList = choice_value
|
||||||
this.activeKey = 'define'
|
this.activeKey = 'define'
|
||||||
@@ -330,6 +380,16 @@ export default {
|
|||||||
this.filterExp = ''
|
this.filterExp = ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
changeCodeContent(value) {
|
||||||
|
this.script = value && value.replace('\t', ' ')
|
||||||
|
},
|
||||||
|
changeActiveKey(value) {
|
||||||
|
if (value === 'script') {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.codemirror.initCodeMirror(this.script)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@@ -90,9 +90,12 @@
|
|||||||
mode="multiple"
|
mode="multiple"
|
||||||
show-search
|
show-search
|
||||||
>
|
>
|
||||||
<a-select-option v-for="attr in commonAttributes" :key="attr.id" :value="attr.id">{{
|
<a-select-option
|
||||||
attr.alias || attr.name
|
v-for="attr in commonAttributes.filter((attr) => !attr.is_password)"
|
||||||
}}</a-select-option>
|
:key="attr.id"
|
||||||
|
:value="attr.id"
|
||||||
|
>{{ attr.alias || attr.name }}</a-select-option
|
||||||
|
>
|
||||||
</a-select>
|
</a-select>
|
||||||
</a-form-model-item>
|
</a-form-model-item>
|
||||||
<a-form-model-item
|
<a-form-model-item
|
||||||
|
@@ -24,7 +24,7 @@ export const category_1_bar_options = (data, options) => {
|
|||||||
})
|
})
|
||||||
return {
|
return {
|
||||||
|
|
||||||
color: options.chartColor.split(','),
|
color: (options?.chartColor ?? '#6592FD,#6EE3EB,#44C2FD,#5F59F7,#1A348F,#7D8FCF,#A6D1E5,#8E56DD').split(','),
|
||||||
grid: {
|
grid: {
|
||||||
top: 15,
|
top: 15,
|
||||||
left: 'left',
|
left: 'left',
|
||||||
@@ -83,7 +83,7 @@ export const category_1_bar_options = (data, options) => {
|
|||||||
export const category_1_line_options = (data, options) => {
|
export const category_1_line_options = (data, options) => {
|
||||||
const xData = Object.keys(data)
|
const xData = Object.keys(data)
|
||||||
return {
|
return {
|
||||||
color: options.chartColor.split(','),
|
color: (options?.chartColor ?? '#6592FD,#6EE3EB,#44C2FD,#5F59F7,#1A348F,#7D8FCF,#A6D1E5,#8E56DD').split(','),
|
||||||
grid: {
|
grid: {
|
||||||
top: 15,
|
top: 15,
|
||||||
left: 'left',
|
left: 'left',
|
||||||
@@ -117,7 +117,7 @@ export const category_1_line_options = (data, options) => {
|
|||||||
x2: 0,
|
x2: 0,
|
||||||
y2: 1,
|
y2: 1,
|
||||||
colorStops: [{
|
colorStops: [{
|
||||||
offset: 0, color: options.chartColor.split(',')[0] // 0% 处的颜色
|
offset: 0, color: (options?.chartColor ?? '#6592FD,#6EE3EB,#44C2FD,#5F59F7,#1A348F,#7D8FCF,#A6D1E5,#8E56DD').split(',')[0] // 0% 处的颜色
|
||||||
}, {
|
}, {
|
||||||
offset: 1, color: '#ffffff' // 100% 处的颜色
|
offset: 1, color: '#ffffff' // 100% 处的颜色
|
||||||
}],
|
}],
|
||||||
@@ -131,7 +131,7 @@ export const category_1_line_options = (data, options) => {
|
|||||||
|
|
||||||
export const category_1_pie_options = (data, options) => {
|
export const category_1_pie_options = (data, options) => {
|
||||||
return {
|
return {
|
||||||
color: options.chartColor.split(','),
|
color: (options?.chartColor ?? '#6592FD,#6EE3EB,#44C2FD,#5F59F7,#1A348F,#7D8FCF,#A6D1E5,#8E56DD').split(','),
|
||||||
grid: {
|
grid: {
|
||||||
top: 10,
|
top: 10,
|
||||||
left: 'left',
|
left: 'left',
|
||||||
@@ -181,7 +181,7 @@ export const category_2_bar_options = (data, options, chartType) => {
|
|||||||
})
|
})
|
||||||
const legend = [...new Set(_legend)]
|
const legend = [...new Set(_legend)]
|
||||||
return {
|
return {
|
||||||
color: options.chartColor.split(','),
|
color: (options?.chartColor ?? '#6592FD,#6EE3EB,#44C2FD,#5F59F7,#1A348F,#7D8FCF,#A6D1E5,#8E56DD').split(','),
|
||||||
grid: {
|
grid: {
|
||||||
top: 15,
|
top: 15,
|
||||||
left: 'left',
|
left: 'left',
|
||||||
@@ -249,7 +249,7 @@ export const category_2_bar_options = (data, options, chartType) => {
|
|||||||
x2: 0,
|
x2: 0,
|
||||||
y2: 1,
|
y2: 1,
|
||||||
colorStops: [{
|
colorStops: [{
|
||||||
offset: 0, color: options.chartColor.split(',')[index % 8] // 0% 处的颜色
|
offset: 0, color: (options?.chartColor ?? '#6592FD,#6EE3EB,#44C2FD,#5F59F7,#1A348F,#7D8FCF,#A6D1E5,#8E56DD').split(',')[index % 8] // 0% 处的颜色
|
||||||
}, {
|
}, {
|
||||||
offset: 1, color: '#ffffff' // 100% 处的颜色
|
offset: 1, color: '#ffffff' // 100% 处的颜色
|
||||||
}],
|
}],
|
||||||
@@ -269,7 +269,7 @@ export const category_2_pie_options = (data, options) => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
return {
|
return {
|
||||||
color: options.chartColor.split(','),
|
color: (options?.chartColor ?? '#6592FD,#6EE3EB,#44C2FD,#5F59F7,#1A348F,#7D8FCF,#A6D1E5,#8E56DD').split(','),
|
||||||
grid: {
|
grid: {
|
||||||
top: 15,
|
top: 15,
|
||||||
left: 'left',
|
left: 'left',
|
||||||
|
@@ -185,7 +185,6 @@ if __name__ == "__main__":
|
|||||||
mode: 'python',
|
mode: 'python',
|
||||||
height: '200px',
|
height: '200px',
|
||||||
theme: 'monokai',
|
theme: 'monokai',
|
||||||
smartIndent: true,
|
|
||||||
tabSize: 4,
|
tabSize: 4,
|
||||||
lineWrapping: true,
|
lineWrapping: true,
|
||||||
},
|
},
|
||||||
|
@@ -25,7 +25,7 @@
|
|||||||
:expandedKeys="expandedKeys"
|
:expandedKeys="expandedKeys"
|
||||||
>
|
>
|
||||||
<a-icon slot="switcherIcon" type="down" />
|
<a-icon slot="switcherIcon" type="down" />
|
||||||
<template #title="{ key: treeKey, title, isLeaf }">
|
<template #title="{ key: treeKey, title,isLeaf }">
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
:title="title"
|
:title="title"
|
||||||
:treeKey="treeKey"
|
:treeKey="treeKey"
|
||||||
@@ -135,7 +135,8 @@
|
|||||||
{{ col.title }}</span
|
{{ col.title }}</span
|
||||||
>
|
>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="col.is_choice" #edit="{ row }">
|
<template v-if="col.is_choice || col.is_password" #edit="{ row }">
|
||||||
|
<vxe-input v-if="col.is_password" v-model="passwordValue[col.field]" />
|
||||||
<a-select
|
<a-select
|
||||||
:getPopupContainer="(trigger) => trigger.parentElement"
|
:getPopupContainer="(trigger) => trigger.parentElement"
|
||||||
:style="{ width: '100%', height: '32px' }"
|
:style="{ width: '100%', height: '32px' }"
|
||||||
@@ -177,10 +178,20 @@
|
|||||||
#default="{row}"
|
#default="{row}"
|
||||||
>
|
>
|
||||||
<span v-if="col.value_type === '6' && row[col.field]">{{ row[col.field] }}</span>
|
<span v-if="col.value_type === '6' && row[col.field]">{{ row[col.field] }}</span>
|
||||||
<a v-else-if="col.is_link" :href="`${row[col.field]}`" target="_blank">{{ row[col.field] }}</a>
|
<a
|
||||||
|
v-else-if="col.is_link && row[col.field]"
|
||||||
|
:href="
|
||||||
|
row[col.field].startsWith('http') || row[col.field].startsWith('https')
|
||||||
|
? `${row[col.field]}`
|
||||||
|
: `http://${row[col.field]}`
|
||||||
|
"
|
||||||
|
target="_blank"
|
||||||
|
>{{ row[col.field] }}</a
|
||||||
|
>
|
||||||
<PasswordField
|
<PasswordField
|
||||||
v-else-if="col.is_password && row[col.field]"
|
v-else-if="col.is_password && row[col.field]"
|
||||||
:password="row[col.field]"
|
:ci_id="row._id"
|
||||||
|
:attr_id="col.attr_id"
|
||||||
></PasswordField>
|
></PasswordField>
|
||||||
<template v-else-if="col.is_choice">
|
<template v-else-if="col.is_choice">
|
||||||
<template v-if="col.is_list">
|
<template v-if="col.is_list">
|
||||||
@@ -333,7 +344,7 @@ import {
|
|||||||
} from '@/modules/cmdb/api/CIRelation'
|
} from '@/modules/cmdb/api/CIRelation'
|
||||||
|
|
||||||
import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
|
import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
|
||||||
import { searchCI2, updateCI, deleteCI } from '@/modules/cmdb/api/ci'
|
import { searchCI2, updateCI, deleteCI, searchCI } from '@/modules/cmdb/api/ci'
|
||||||
import { getCITypes } from '../../api/CIType'
|
import { getCITypes } from '../../api/CIType'
|
||||||
import { roleHasPermissionToGrant } from '@/modules/acl/api/permission'
|
import { roleHasPermissionToGrant } from '@/modules/acl/api/permission'
|
||||||
import { searchResourceType } from '@/modules/acl/api/resource'
|
import { searchResourceType } from '@/modules/acl/api/resource'
|
||||||
@@ -347,6 +358,7 @@ import PasswordField from '../../components/passwordField/index.vue'
|
|||||||
import PreferenceSearch from '../../components/preferenceSearch/preferenceSearch.vue'
|
import PreferenceSearch from '../../components/preferenceSearch/preferenceSearch.vue'
|
||||||
import CMDBGrant from '../../components/cmdbGrant'
|
import CMDBGrant from '../../components/cmdbGrant'
|
||||||
import { ops_move_icon as OpsMoveIcon } from '@/core/icons'
|
import { ops_move_icon as OpsMoveIcon } from '@/core/icons'
|
||||||
|
import { getAttrPassword } from '../../api/CITypeAttr'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'RelationViews',
|
name: 'RelationViews',
|
||||||
@@ -407,6 +419,11 @@ export default {
|
|||||||
tableDragClassName: [],
|
tableDragClassName: [],
|
||||||
|
|
||||||
resource_type: {},
|
resource_type: {},
|
||||||
|
|
||||||
|
initialPasswordValue: {},
|
||||||
|
passwordValue: {},
|
||||||
|
lastEditCiId: null,
|
||||||
|
isContinueCloseEdit: true,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -549,11 +566,11 @@ export default {
|
|||||||
q = q.slice(1)
|
q = q.slice(1)
|
||||||
}
|
}
|
||||||
if (this.treeKeys.length === 0) {
|
if (this.treeKeys.length === 0) {
|
||||||
|
await this.judgeCITypes(q)
|
||||||
if (!refreshType) {
|
if (!refreshType) {
|
||||||
this.loadRoot()
|
this.loadRoot()
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.judgeCITypes(q)
|
|
||||||
const fuzzySearch = (this.$refs['search'] || {}).fuzzySearch || ''
|
const fuzzySearch = (this.$refs['search'] || {}).fuzzySearch || ''
|
||||||
if (fuzzySearch) {
|
if (fuzzySearch) {
|
||||||
q = `q=_type:${this.currentTypeId[0]},*${fuzzySearch}*,` + q
|
q = `q=_type:${this.currentTypeId[0]},*${fuzzySearch}*,` + q
|
||||||
@@ -635,6 +652,7 @@ export default {
|
|||||||
statisticsCIRelation({
|
statisticsCIRelation({
|
||||||
root_ids: key.split('%')[0],
|
root_ids: key.split('%')[0],
|
||||||
level: this.treeKeys.length - index,
|
level: this.treeKeys.length - index,
|
||||||
|
type_ids: this.showTypes.map((type) => type.id).join(','),
|
||||||
}).then((res) => {
|
}).then((res) => {
|
||||||
let result
|
let result
|
||||||
const getTreeItem = (data, id) => {
|
const getTreeItem = (data, id) => {
|
||||||
@@ -689,6 +707,7 @@ export default {
|
|||||||
}
|
}
|
||||||
const promises = _showTypeIds.map((typeId) => {
|
const promises = _showTypeIds.map((typeId) => {
|
||||||
const _q = (`q=_type:${typeId},` + q).replace(/count=\d*/, 'count=1')
|
const _q = (`q=_type:${typeId},` + q).replace(/count=\d*/, 'count=1')
|
||||||
|
console.log(_q)
|
||||||
if (this.treeKeys.length === 0) {
|
if (this.treeKeys.length === 0) {
|
||||||
return searchCI2(_q).then((res) => {
|
return searchCI2(_q).then((res) => {
|
||||||
if (res.numfound !== 0) {
|
if (res.numfound !== 0) {
|
||||||
@@ -739,7 +758,11 @@ export default {
|
|||||||
level = idx + 1
|
level = idx + 1
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return statisticsCIRelation({ root_ids: ciIds.join(','), level: level }).then((num) => {
|
return statisticsCIRelation({
|
||||||
|
root_ids: ciIds.join(','),
|
||||||
|
level: level,
|
||||||
|
type_ids: this.showTypes.map((type) => type.id).join(','),
|
||||||
|
}).then((num) => {
|
||||||
facet.forEach((item, idx) => {
|
facet.forEach((item, idx) => {
|
||||||
item[1] += num[ciIds[idx] + '']
|
item[1] += num[ciIds[idx] + '']
|
||||||
})
|
})
|
||||||
@@ -752,7 +775,12 @@ export default {
|
|||||||
|
|
||||||
async loadNoRoot(rootIdAndTypeId, level) {
|
async loadNoRoot(rootIdAndTypeId, level) {
|
||||||
const rootId = rootIdAndTypeId.split('%')[0]
|
const rootId = rootIdAndTypeId.split('%')[0]
|
||||||
searchCIRelation(`root_id=${rootId}&level=1&count=10000`).then(async (res) => {
|
const typeId = Number(rootIdAndTypeId.split('%')[1])
|
||||||
|
const topo_flatten = this.relationViews?.views[this.$route.meta.name]?.topo_flatten ?? []
|
||||||
|
const index = topo_flatten.findIndex((id) => id === typeId)
|
||||||
|
const _type = topo_flatten[index + 1]
|
||||||
|
if (_type) {
|
||||||
|
searchCIRelation(`q=_type:${_type}&root_id=${rootId}&level=1&count=10000`).then(async (res) => {
|
||||||
const facet = []
|
const facet = []
|
||||||
const ciIds = []
|
const ciIds = []
|
||||||
res.result.forEach((item) => {
|
res.result.forEach((item) => {
|
||||||
@@ -761,7 +789,11 @@ export default {
|
|||||||
})
|
})
|
||||||
const promises = level.map((_level) => {
|
const promises = level.map((_level) => {
|
||||||
if (_level > 1) {
|
if (_level > 1) {
|
||||||
return statisticsCIRelation({ root_ids: ciIds.join(','), level: _level - 1 }).then((num) => {
|
return statisticsCIRelation({
|
||||||
|
root_ids: ciIds.join(','),
|
||||||
|
level: _level - 1,
|
||||||
|
type_ids: this.showTypes.map((type) => type.id).join(','),
|
||||||
|
}).then((num) => {
|
||||||
facet.forEach((item, idx) => {
|
facet.forEach((item, idx) => {
|
||||||
item[1] += num[ciIds[idx] + '']
|
item[1] += num[ciIds[idx] + '']
|
||||||
})
|
})
|
||||||
@@ -771,6 +803,7 @@ export default {
|
|||||||
await Promise.all(promises)
|
await Promise.all(promises)
|
||||||
this.wrapTreeData(facet, 'loadNoRoot')
|
this.wrapTreeData(facet, 'loadNoRoot')
|
||||||
})
|
})
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onNodeClick(keys) {
|
onNodeClick(keys) {
|
||||||
@@ -874,6 +907,12 @@ export default {
|
|||||||
calcColumns() {
|
calcColumns() {
|
||||||
const width = document.getElementById('relation-views-right').clientWidth
|
const width = document.getElementById('relation-views-right').clientWidth
|
||||||
this.columns = getCITableColumns(this.instanceList, this.preferenceAttrList, width)
|
this.columns = getCITableColumns(this.instanceList, this.preferenceAttrList, width)
|
||||||
|
this.columns.forEach((col) => {
|
||||||
|
if (col.is_password) {
|
||||||
|
this.initialPasswordValue[col.field] = ''
|
||||||
|
this.passwordValue[col.field] = ''
|
||||||
|
}
|
||||||
|
})
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.$refs.xTable.refreshColumn()
|
this.$refs.xTable.refreshColumn()
|
||||||
})
|
})
|
||||||
@@ -924,10 +963,10 @@ export default {
|
|||||||
),
|
),
|
||||||
onOk() {
|
onOk() {
|
||||||
const _tempTree = that.treeKeys[that.treeKeys.length - 1].split('%')
|
const _tempTree = that.treeKeys[that.treeKeys.length - 1].split('%')
|
||||||
const firstCIObj = JSON.parse(_tempTree[2])
|
const first_ci_id = Number(_tempTree[0])
|
||||||
batchDeleteCIRelation(
|
batchDeleteCIRelation(
|
||||||
that.selectedRowKeys.map((item) => item._id),
|
that.selectedRowKeys.map((item) => item._id),
|
||||||
firstCIObj
|
[first_ci_id]
|
||||||
).then((res) => {
|
).then((res) => {
|
||||||
that.$refs.xTable.clearCheckboxRow()
|
that.$refs.xTable.clearCheckboxRow()
|
||||||
that.$refs.xTable.clearCheckboxReserve()
|
that.$refs.xTable.clearCheckboxReserve()
|
||||||
@@ -1060,15 +1099,48 @@ export default {
|
|||||||
}
|
}
|
||||||
return _editRender
|
return _editRender
|
||||||
},
|
},
|
||||||
handleEditActived() {},
|
handleEditActived() {
|
||||||
|
const passwordCol = this.columns.filter((col) => col.is_password)
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const editRecord = this.$refs.xTable.getEditRecord()
|
||||||
|
const { row, column } = editRecord
|
||||||
|
if (passwordCol.length && this.lastEditCiId !== row._id) {
|
||||||
|
this.$nextTick(async () => {
|
||||||
|
for (let i = 0; i < passwordCol.length; i++) {
|
||||||
|
await getAttrPassword(row._id, passwordCol[i].attr_id).then((res) => {
|
||||||
|
this.initialPasswordValue[passwordCol[i].field] = res.value
|
||||||
|
this.passwordValue[passwordCol[i].field] = res.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.isContinueCloseEdit = false
|
||||||
|
await this.$refs.xTable.clearEdit()
|
||||||
|
this.isContinueCloseEdit = true
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.xTable.setEditCell(row, column.field)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.lastEditCiId = row._id
|
||||||
|
})
|
||||||
|
},
|
||||||
handleEditClose({ row, rowIndex, column }) {
|
handleEditClose({ row, rowIndex, column }) {
|
||||||
|
if (!this.isContinueCloseEdit) {
|
||||||
|
return
|
||||||
|
}
|
||||||
const $table = this.$refs['xTable']
|
const $table = this.$refs['xTable']
|
||||||
const data = {}
|
const data = {}
|
||||||
this.columns.forEach((item) => {
|
this.columns.forEach((item) => {
|
||||||
if (!_.isEqual(row[item.field], this.initialInstanceList[rowIndex][item.field])) {
|
if (!(item.field in this.initialPasswordValue) && !_.isEqual(row[item.field], this.initialInstanceList[rowIndex][item.field])) {
|
||||||
data[item.field] = row[item.field] || null
|
data[item.field] = row[item.field] ?? null
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
Object.keys(this.initialPasswordValue).forEach((key) => {
|
||||||
|
if (this.initialPasswordValue[key] !== this.passwordValue[key]) {
|
||||||
|
data[key] = this.passwordValue[key]
|
||||||
|
row[key] = this.passwordValue[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.lastEditCiId = null
|
||||||
if (JSON.stringify(data) !== '{}') {
|
if (JSON.stringify(data) !== '{}') {
|
||||||
updateCI(row.ci_id || row._id, data)
|
updateCI(row.ci_id || row._id, data)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -1086,6 +1158,12 @@ export default {
|
|||||||
$table.revertData(row)
|
$table.revertData(row)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
this.columns.forEach((col) => {
|
||||||
|
if (col.is_password) {
|
||||||
|
this.initialPasswordValue[col.field] = ''
|
||||||
|
this.passwordValue[col.field] = ''
|
||||||
|
}
|
||||||
|
})
|
||||||
},
|
},
|
||||||
deleteCI(record) {
|
deleteCI(record) {
|
||||||
const that = this
|
const that = this
|
||||||
|
@@ -96,8 +96,21 @@
|
|||||||
>
|
>
|
||||||
<template v-if="col.value_type === '6' || col.is_link || col.is_password || col.is_choice" #default="{row}">
|
<template v-if="col.value_type === '6' || col.is_link || col.is_password || col.is_choice" #default="{row}">
|
||||||
<span v-if="col.value_type === '6' && row[col.field]">{{ JSON.stringify(row[col.field]) }}</span>
|
<span v-if="col.value_type === '6' && row[col.field]">{{ JSON.stringify(row[col.field]) }}</span>
|
||||||
<a v-else-if="col.is_link" :href="`${row[col.field]}`" target="_blank">{{ row[col.field] }}</a>
|
<a
|
||||||
<PasswordField v-else-if="col.is_password && row[col.field]" :password="row[col.field]"></PasswordField>
|
v-else-if="col.is_link && row[col.field]"
|
||||||
|
:href="
|
||||||
|
row[col.field].startsWith('http') || row[col.field].startsWith('https')
|
||||||
|
? `${row[col.field]}`
|
||||||
|
: `http://${row[col.field]}`
|
||||||
|
"
|
||||||
|
target="_blank"
|
||||||
|
>{{ row[col.field] }}</a
|
||||||
|
>
|
||||||
|
<PasswordField
|
||||||
|
v-else-if="col.is_password && row[col.field]"
|
||||||
|
:ci_id="row._id"
|
||||||
|
:attr_id="col.attr_id"
|
||||||
|
></PasswordField>
|
||||||
<template v-else-if="col.is_choice">
|
<template v-else-if="col.is_choice">
|
||||||
<template v-if="col.is_list">
|
<template v-if="col.is_list">
|
||||||
<span
|
<span
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
<div v-if="!subscribeTreeViewCiTypesLoading && subscribeTreeViewCiTypes.length === 0">
|
<div v-if="!subscribeTreeViewCiTypesLoading && subscribeTreeViewCiTypes.length === 0">
|
||||||
<a-alert message="请先到 我的订阅 页面完成订阅!" banner></a-alert>
|
<a-alert message="请先到 我的订阅 页面完成订阅!" banner></a-alert>
|
||||||
</div>
|
</div>
|
||||||
<div class="tree-views" v-else>
|
<div class="tree-views">
|
||||||
<div class="cmdb-views-header">
|
<div class="cmdb-views-header">
|
||||||
<span>
|
<span>
|
||||||
<span class="cmdb-views-header-title">{{ currentCiTypeName }}</span>
|
<span class="cmdb-views-header-title">{{ currentCiTypeName }}</span>
|
||||||
@@ -193,7 +193,8 @@
|
|||||||
{{ col.title }}</span
|
{{ col.title }}</span
|
||||||
>
|
>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="col.is_choice" #edit="{ row }">
|
<template v-if="col.is_choice || col.is_password" #edit="{ row }">
|
||||||
|
<vxe-input v-if="col.is_password" v-model="passwordValue[col.field]" />
|
||||||
<a-select
|
<a-select
|
||||||
:getPopupContainer="(trigger) => trigger.parentElement"
|
:getPopupContainer="(trigger) => trigger.parentElement"
|
||||||
:style="{ width: '100%', height: '32px' }"
|
:style="{ width: '100%', height: '32px' }"
|
||||||
@@ -232,13 +233,23 @@
|
|||||||
</template>
|
</template>
|
||||||
<template
|
<template
|
||||||
v-if="col.value_type === '6' || col.is_link || col.is_password || col.is_choice"
|
v-if="col.value_type === '6' || col.is_link || col.is_password || col.is_choice"
|
||||||
#default="{ row }"
|
#default="{row}"
|
||||||
>
|
>
|
||||||
<span v-if="col.value_type === '6' && row[col.field]">{{ row[col.field] }}</span>
|
<span v-if="col.value_type === '6' && row[col.field]">{{ row[col.field] }}</span>
|
||||||
<a v-else-if="col.is_link" :href="`${row[col.field]}`" target="_blank">{{ row[col.field] }}</a>
|
<a
|
||||||
|
v-else-if="col.is_link && row[col.field]"
|
||||||
|
:href="
|
||||||
|
row[col.field].startsWith('http') || row[col.field].startsWith('https')
|
||||||
|
? `${row[col.field]}`
|
||||||
|
: `http://${row[col.field]}`
|
||||||
|
"
|
||||||
|
target="_blank"
|
||||||
|
>{{ row[col.field] }}</a
|
||||||
|
>
|
||||||
<PasswordField
|
<PasswordField
|
||||||
v-else-if="col.is_password && row[col.field]"
|
v-else-if="col.is_password && row[col.field]"
|
||||||
:password="row[col.field]"
|
:ci_id="row._id"
|
||||||
|
:attr_id="col.attr_id"
|
||||||
></PasswordField>
|
></PasswordField>
|
||||||
<template v-else-if="col.is_choice">
|
<template v-else-if="col.is_choice">
|
||||||
<template v-if="col.is_list">
|
<template v-if="col.is_list">
|
||||||
@@ -400,6 +411,7 @@ import PreferenceSearch from '../../components/preferenceSearch/preferenceSearch
|
|||||||
import MetadataDrawer from '../ci/modules/MetadataDrawer.vue'
|
import MetadataDrawer from '../ci/modules/MetadataDrawer.vue'
|
||||||
import { intersection } from '@/utils/functions/set'
|
import { intersection } from '@/utils/functions/set'
|
||||||
import { ops_move_icon as OpsMoveIcon } from '@/core/icons'
|
import { ops_move_icon as OpsMoveIcon } from '@/core/icons'
|
||||||
|
import { getAttrPassword } from '../../api/CITypeAttr'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TreeViews',
|
name: 'TreeViews',
|
||||||
@@ -454,6 +466,11 @@ export default {
|
|||||||
tableDragClassName: [],
|
tableDragClassName: [],
|
||||||
// 已经设置过data的node
|
// 已经设置过data的node
|
||||||
isSetDataNodes: [],
|
isSetDataNodes: [],
|
||||||
|
|
||||||
|
initialPasswordValue: {},
|
||||||
|
passwordValue: {},
|
||||||
|
lastEditCiId: null,
|
||||||
|
isContinueCloseEdit: true,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -487,7 +504,7 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
'$route.path': function (newPath, oldPath) {
|
'$route.path': function(newPath, oldPath) {
|
||||||
this.newLoad = true
|
this.newLoad = true
|
||||||
this.typeId = this.$route.params.typeId
|
this.typeId = this.$route.params.typeId
|
||||||
this.initPage()
|
this.initPage()
|
||||||
@@ -652,6 +669,12 @@ export default {
|
|||||||
if (treeViewsRight) {
|
if (treeViewsRight) {
|
||||||
const width = treeViewsRight.clientWidth - 50
|
const width = treeViewsRight.clientWidth - 50
|
||||||
this.columns = getCITableColumns(res.result, this.currentAttrList, width)
|
this.columns = getCITableColumns(res.result, this.currentAttrList, width)
|
||||||
|
this.columns.forEach((col) => {
|
||||||
|
if (col.is_password) {
|
||||||
|
this.initialPasswordValue[col.field] = ''
|
||||||
|
this.passwordValue[col.field] = ''
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e)
|
console.log(e)
|
||||||
@@ -918,15 +941,48 @@ export default {
|
|||||||
onSelectRangeEnd({ records }) {
|
onSelectRangeEnd({ records }) {
|
||||||
this.selectedRowKeys = records.map((i) => i.ci_id || i._id)
|
this.selectedRowKeys = records.map((i) => i.ci_id || i._id)
|
||||||
},
|
},
|
||||||
handleEditActived() {},
|
handleEditActived() {
|
||||||
|
const passwordCol = this.columns.filter((col) => col.is_password)
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const editRecord = this.$refs.xTable.getVxetableRef().getEditRecord()
|
||||||
|
const { row, column } = editRecord
|
||||||
|
if (passwordCol.length && this.lastEditCiId !== row._id) {
|
||||||
|
this.$nextTick(async () => {
|
||||||
|
for (let i = 0; i < passwordCol.length; i++) {
|
||||||
|
await getAttrPassword(row._id, passwordCol[i].attr_id).then((res) => {
|
||||||
|
this.initialPasswordValue[passwordCol[i].field] = res.value
|
||||||
|
this.passwordValue[passwordCol[i].field] = res.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.isContinueCloseEdit = false
|
||||||
|
await this.$refs.xTable.getVxetableRef().clearEdit()
|
||||||
|
this.isContinueCloseEdit = true
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.xTable.getVxetableRef().setEditCell(row, column.field)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.lastEditCiId = row._id
|
||||||
|
})
|
||||||
|
},
|
||||||
handleEditClose({ row, rowIndex, column }) {
|
handleEditClose({ row, rowIndex, column }) {
|
||||||
|
if (!this.isContinueCloseEdit) {
|
||||||
|
return
|
||||||
|
}
|
||||||
const $table = this.$refs['xTable'].getVxetableRef()
|
const $table = this.$refs['xTable'].getVxetableRef()
|
||||||
const data = {}
|
const data = {}
|
||||||
this.columns.forEach((item) => {
|
this.columns.forEach((item) => {
|
||||||
if (!_.isEqual(row[item.field], this.initialInstanceList[rowIndex][item.field])) {
|
if (!(item.field in this.initialPasswordValue) && !_.isEqual(row[item.field], this.initialInstanceList[rowIndex][item.field])) {
|
||||||
data[item.field] = row[item.field] || null
|
data[item.field] = row[item.field] ?? null
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
Object.keys(this.initialPasswordValue).forEach((key) => {
|
||||||
|
if (this.initialPasswordValue[key] !== this.passwordValue[key]) {
|
||||||
|
data[key] = this.passwordValue[key]
|
||||||
|
row[key] = this.passwordValue[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.lastEditCiId = null
|
||||||
if (JSON.stringify(data) !== '{}') {
|
if (JSON.stringify(data) !== '{}') {
|
||||||
updateCI(row._id, data)
|
updateCI(row._id, data)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -953,6 +1009,12 @@ export default {
|
|||||||
$table.revertData(row)
|
$table.revertData(row)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
this.columns.forEach((col) => {
|
||||||
|
if (col.is_password) {
|
||||||
|
this.initialPasswordValue[col.field] = ''
|
||||||
|
this.passwordValue[col.field] = ''
|
||||||
|
}
|
||||||
|
})
|
||||||
},
|
},
|
||||||
jsonEditorOk(row, column, jsonData) {
|
jsonEditorOk(row, column, jsonData) {
|
||||||
// 后端写数据有快慢,不拉接口直接修改table的数据
|
// 后端写数据有快慢,不拉接口直接修改table的数据
|
||||||
|
@@ -17,9 +17,11 @@ const logo = {
|
|||||||
getCompanyInfo({ commit }) {
|
getCompanyInfo({ commit }) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
getCompanyInfo().then(res => {
|
getCompanyInfo().then(res => {
|
||||||
|
if (res.info) {
|
||||||
commit('SET_FILENAME', res.info.logoName)
|
commit('SET_FILENAME', res.info.logoName)
|
||||||
commit('SET_SMALL_FILENAME', res.info.smallLogoName)
|
commit('SET_SMALL_FILENAME', res.info.smallLogoName)
|
||||||
resolve(res.info)
|
resolve(res.info)
|
||||||
|
}
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
console.log('获取失败', err)
|
console.log('获取失败', err)
|
||||||
reject(err)
|
reject(err)
|
||||||
|
@@ -30,7 +30,7 @@ services:
|
|||||||
- redis
|
- redis
|
||||||
|
|
||||||
cmdb-api:
|
cmdb-api:
|
||||||
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-api:2.3.5
|
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-api:2.3.6
|
||||||
# build:
|
# build:
|
||||||
# context: .
|
# context: .
|
||||||
# target: cmdb-api
|
# target: cmdb-api
|
||||||
@@ -47,13 +47,15 @@ services:
|
|||||||
flask db-setup
|
flask db-setup
|
||||||
flask common-check-new-columns
|
flask common-check-new-columns
|
||||||
gunicorn --workers=3 autoapp:app -b 0.0.0.0:5000 -D
|
gunicorn --workers=3 autoapp:app -b 0.0.0.0:5000 -D
|
||||||
flask cmdb-init-cache
|
|
||||||
flask cmdb-init-acl
|
|
||||||
nohup flask cmdb-trigger > trigger.log 2>&1 &
|
|
||||||
nohup flask cmdb-counter > counter.log 2>&1 &
|
|
||||||
|
|
||||||
celery -A celery_worker.celery worker -E -Q one_cmdb_async --autoscale=5,2 --logfile=one_cmdb_async.log -D
|
celery -A celery_worker.celery worker -E -Q one_cmdb_async --autoscale=5,2 --logfile=one_cmdb_async.log -D
|
||||||
celery -A celery_worker.celery worker -E -Q acl_async --logfile=one_acl_async.log --concurrency=2
|
celery -A celery_worker.celery worker -E -Q acl_async --logfile=one_acl_async.log --concurrency=2 -D
|
||||||
|
|
||||||
|
nohup flask cmdb-trigger > trigger.log 2>&1 &
|
||||||
|
flask cmdb-init-cache
|
||||||
|
flask cmdb-init-acl
|
||||||
|
flask cmdb-counter > counter.log 2>&1
|
||||||
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- cmdb-db
|
- cmdb-db
|
||||||
- cmdb-cache
|
- cmdb-cache
|
||||||
@@ -63,7 +65,7 @@ services:
|
|||||||
- cmdb-api
|
- cmdb-api
|
||||||
|
|
||||||
cmdb-ui:
|
cmdb-ui:
|
||||||
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-ui:2.3.5
|
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-ui:2.3.6
|
||||||
# build:
|
# build:
|
||||||
# context: .
|
# context: .
|
||||||
# target: cmdb-ui
|
# target: cmdb-ui
|
||||||
|
@@ -1,8 +1,16 @@
|
|||||||

|
|
||||||
|
|
||||||
[](https://github.com/veops/cmdb/blob/master/LICENSE)
|
<p align="center">
|
||||||
[](https://github.com/sendya/ant-design-pro-vue)
|
<a href="https://veops.cn"><img src="images/logo.png" alt="维易CMDB" width="300"/></a>
|
||||||
[](https://github.com/pallets/flask)
|
</p>
|
||||||
|
<h3 align="center">Simple, lightweight, and versatile operational CMDB</h3>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/veops/cmdb/blob/master/LICENSE"><img src="https://img.shields.io/badge/License-AGPLv3-brightgreen" alt="License: GPLv3"></a>
|
||||||
|
<a href="https:https://github.com/sendya/ant-design-pro-vue"><img src="https://img.shields.io/badge/UI-Ant%20Design%20Pro%20Vue-brightgreen" alt="UI"></a>
|
||||||
|
<a href="https://github.com/pallets/flask"><img src="https://img.shields.io/badge/API-Flask-brightgreen" alt="API"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
------------------------------
|
||||||
|
|
||||||
[English](README_en.md) / [中文](../README.md)
|
[English](README_en.md) / [中文](../README.md)
|
||||||
|
|
||||||
@@ -16,9 +24,11 @@
|
|||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
### Technical Architecture
|
### System Overview
|
||||||
|
|
||||||
<img src=images/view.jpg />
|
<img src=images/dashboard.png />
|
||||||
|
|
||||||
|
[View more screenshots](screenshot.md)
|
||||||
|
|
||||||
### Document
|
### Document
|
||||||
|
|
||||||
@@ -48,12 +58,7 @@
|
|||||||
- Fine-grained access control and comprehensive operation logs.
|
- Fine-grained access control and comprehensive operation logs.
|
||||||
- Support cross-model search.
|
- Support cross-model search.
|
||||||
|
|
||||||
### System Overview
|
|
||||||
|
|
||||||
- Service Tree
|
|
||||||

|
|
||||||
|
|
||||||
[View more screenshots](screenshot.md)
|
|
||||||
|
|
||||||
### More Features
|
### More Features
|
||||||
|
|
||||||
|
@@ -33,6 +33,7 @@
|
|||||||
* **`IN`**查询: 例如: `hostname:(cmdb*;cmdb-web*)` 小括号, 分号分隔
|
* **`IN`**查询: 例如: `hostname:(cmdb*;cmdb-web*)` 小括号, 分号分隔
|
||||||
* **`范围`**查询: 例如: `hostname:[cmdb* _TO_ cmdb-web*]` `_TO_`分隔
|
* **`范围`**查询: 例如: `hostname:[cmdb* _TO_ cmdb-web*]` `_TO_`分隔
|
||||||
* **`比较`**查询: 例如: `cpu_count:>5` 支持`>, >=, <, <=`
|
* **`比较`**查询: 例如: `cpu_count:>5` 支持`>, >=, <, <=`
|
||||||
|
* 多个条件可以用`小括号`进行组合
|
||||||
|
|
||||||
* 结果字段说明
|
* 结果字段说明
|
||||||
|
|
||||||
|
@@ -25,6 +25,8 @@
|
|||||||
- `IN`查询. eg. `hostname:(cmdb*;cmdb-web*)` 小括号, 分号分隔
|
- `IN`查询. eg. `hostname:(cmdb*;cmdb-web*)` 小括号, 分号分隔
|
||||||
- `RANGE`查询. eg. `hostname:[cmdb* _TO_ cmdb-web*]` `_TO_`分隔
|
- `RANGE`查询. eg. `hostname:[cmdb* _TO_ cmdb-web*]` `_TO_`分隔
|
||||||
- `COMPARISON`查询. eg. `cpu_count:>5` 支持`>, >=, <, <=`
|
- `COMPARISON`查询. eg. `cpu_count:>5` 支持`>, >=, <, <=`
|
||||||
|
- 多个条件可以用`小括号`进行组合
|
||||||
|
|
||||||
- 返回结果
|
- 返回结果
|
||||||
- 搜索表达式 `/api/v0.1/ci/s?q=_type:kvm,status:在线,idc:南汇,private_ip:10.1.1.1*&page=1&fl=hostname,private_ip&facet=private_ip&count=1`
|
- 搜索表达式 `/api/v0.1/ci/s?q=_type:kvm,status:在线,idc:南汇,private_ip:10.1.1.1*&page=1&fl=hostname,private_ip&facet=private_ip&count=1`
|
||||||
- 返回数据(默认 json)
|
- 返回数据(默认 json)
|
||||||
@@ -91,6 +93,8 @@
|
|||||||
- `IN`查询. eg. `hostname:(cmdb*;cmdb-web*)` 小括号, 分号分隔
|
- `IN`查询. eg. `hostname:(cmdb*;cmdb-web*)` 小括号, 分号分隔
|
||||||
- `RANGE`查询. eg. `hostname:[cmdb* _TO_ cmdb-web*]` `_TO_`分隔
|
- `RANGE`查询. eg. `hostname:[cmdb* _TO_ cmdb-web*]` `_TO_`分隔
|
||||||
- `COMPARISON`查询. eg. `cpu_count:>5` 支持`>, >=, <, <=`
|
- `COMPARISON`查询. eg. `cpu_count:>5` 支持`>, >=, <, <=`
|
||||||
|
- 多个条件可以用`小括号`进行组合
|
||||||
|
|
||||||
- 返回结果
|
- 返回结果
|
||||||
- 搜索表达式 `/api/v0.1/ci_relations/s?root_id=53&level=3&q=_type:kvm,status:在线,idc:南汇,private_ip:10.1.1.1*&page=1&fl=hostname,private_ip&facet=private_ip&count=1`
|
- 搜索表达式 `/api/v0.1/ci_relations/s?root_id=53&level=3&q=_type:kvm,status:在线,idc:南汇,private_ip:10.1.1.1*&page=1&fl=hostname,private_ip&facet=private_ip&count=1`
|
||||||
- 返回数据(默认 json)
|
- 返回数据(默认 json)
|
||||||
|
15
docs/flask-migrate.md
Normal file
15
docs/flask-migrate.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
## 使用Flask-Migrate做数据库版本管理
|
||||||
|
|
||||||
|
- 首次可以删除cmdb-api/migrations/versions下的所有文件
|
||||||
|
-
|
||||||
|
|
||||||
|
### 进入cmdb-api完成下面步骤(操作可能会删除数据库中不被代码管理的表,如需保留请看文末中的tips)
|
||||||
|
|
||||||
|
- 如果是首次使用需要先删除cmdb-api/migrations/versions下的所有文件(非首次跳过)
|
||||||
|
- 执行`flask db migrate` 生成对应版本数据库表的升级文件到versions文件夹下,需要你的数据库是已经upgrade的
|
||||||
|
- 执行`flask db upgrade` 数据库表同步更新到mysql
|
||||||
|
|
||||||
|
|
||||||
|
### tips
|
||||||
|
|
||||||
|
- cmdb-api/migrations/env.py文件内的exclude_tables列表可以填写不想被flask-migrate管理的数据库表
|
BIN
docs/images/dashboard.png
Normal file
BIN
docs/images/dashboard.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 191 KiB |
Binary file not shown.
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
BIN
docs/images/wechat.png
Normal file
BIN
docs/images/wechat.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 99 KiB |
@@ -28,8 +28,8 @@ cp cmdb-api/settings.example.py cmdb-api/settings.py
|
|||||||
- 后端: 进入**cmdb-api**目录执行 `pipenv run flask run -h 0.0.0.0`
|
- 后端: 进入**cmdb-api**目录执行 `pipenv run flask run -h 0.0.0.0`
|
||||||
- 前端: 进入**cmdb-ui**目录执行`yarn run serve`
|
- 前端: 进入**cmdb-ui**目录执行`yarn run serve`
|
||||||
- worker:
|
- worker:
|
||||||
- 进入**cmdb-api**目录执行 `pipenv run celery -A celery_worker.celery worker -E -Q one_cmdb_async --concurrency=1 -D`
|
- 进入**cmdb-api**目录执行 `pipenv run celery -A celery_worker.celery worker -E -Q one_cmdb_async --autoscale=5,2 --logfile=one_cmdb_async.log -D`
|
||||||
- 进入**cmdb-api**目录执行 `pipenv run celery -A celery_worker.celery worker -E -Q acl_async --concurrency=1 -D`
|
- 进入**cmdb-api**目录执行 `pipenv run celery -A celery_worker.celery worker -E -Q acl_async --autoscale=2,1 --logfile=one_acl_async.log -D`
|
||||||
|
|
||||||
- 浏览器打开: [http://127.0.0.1:8000](http://127.0.0.1:8000)
|
- 浏览器打开: [http://127.0.0.1:8000](http://127.0.0.1:8000)
|
||||||
- 如果是非本机访问, 要修改**cmdb-ui/.env**里**VUE_APP_API_BASE_URL**里的 IP 地址为后端服务的 ip 地址
|
- 如果是非本机访问, 要修改**cmdb-ui/.env**里**VUE_APP_API_BASE_URL**里的 IP 地址为后端服务的 ip 地址
|
||||||
|
@@ -26,8 +26,8 @@
|
|||||||
- backend: in **cmdb-api** directory: `pipenv run flask run -h 0.0.0.0`
|
- backend: in **cmdb-api** directory: `pipenv run flask run -h 0.0.0.0`
|
||||||
- frontend: in **cmdb-ui** directory: `yarn run serve`
|
- frontend: in **cmdb-ui** directory: `yarn run serve`
|
||||||
- worker:
|
- worker:
|
||||||
- in **cmdb-api** directory: `pipenv run celery -A celery_worker.celery worker -E -Q one_cmdb_async --concurrency=1 -D`
|
- in **cmdb-api** directory: `pipenv run celery -A celery_worker.celery worker -E -Q one_cmdb_async --autoscale=5,2 --logfile=one_cmdb_async.log -D`
|
||||||
- in **cmdb-api** directory: `pipenv run celery -A celery_worker.celery worker -E -Q acl_async --concurrency=1 -D`
|
- in **cmdb-api** directory: `pipenv run celery -A celery_worker.celery worker -E -Q acl_async --autoscale=2,1 --logfile=one_acl_async.log -D`
|
||||||
|
|
||||||
- homepage: [http://127.0.0.1:8000](http://127.0.0.1:8000)
|
- homepage: [http://127.0.0.1:8000](http://127.0.0.1:8000)
|
||||||
- if not run localhost: please change ip address(**VUE_APP_API_BASE_URL**) in config file **cmdb-ui/.env** into your backend ip address
|
- if not run localhost: please change ip address(**VUE_APP_API_BASE_URL**) in config file **cmdb-ui/.env** into your backend ip address
|
||||||
|
Reference in New Issue
Block a user