Compare commits

...

126 Commits
2.3.0 ... 2.3.2

Author SHA1 Message Date
pycook
bed2323fc1 Merge pull request #172 from veops/dev_ui
新建ci及批量导入时,新建关系
2023-09-07 11:04:49 +08:00
wang-liang0615
be9b308f56 新建ci及批量导入时,新建关系 2023-09-07 10:25:18 +08:00
pycook
8ba658ea1b Merge branch 'master' of github.com:veops/cmdb 2023-09-07 10:12:55 +08:00
pycook
0aa668cfa0 Add CI relationship when creating CI, the text value removes the escape 2023-09-07 10:12:42 +08:00
pycook
e20fd33a53 Merge pull request #171 from ronething/fix/makefile
optimize: makefile help
2023-09-05 20:34:16 +08:00
ashing
7462de63de fix: review
Signed-off-by: ashing <axingfly@gmail.com>
2023-09-05 20:33:07 +08:00
ashing
5f9ba069ad fix: review
Signed-off-by: ashing <axingfly@gmail.com>
2023-09-05 20:29:29 +08:00
ashing
5dc0d95ff8 fix: review
Signed-off-by: ashing <axingfly@gmail.com>
2023-09-05 20:21:20 +08:00
ashing
e5536b76e6 optimize: makefile help
Signed-off-by: ashing <axingfly@gmail.com>
2023-09-05 20:06:31 +08:00
pycook
8b044efd4e Merge pull request #170 from ronething/feat/xx
feat: support docker deploy mysql and redis
2023-09-05 19:28:47 +08:00
ivonGwy
747b5bf494 Merge pull request #169 from veops/doc
add document link
2023-09-05 15:41:52 +08:00
ivonGwy
21067022f6 add document link 2023-09-05 15:40:31 +08:00
ashing
4102c44fb2 feat: support docker deploy mysql and redis
Signed-off-by: ashing <axingfly@gmail.com>
2023-09-05 15:26:50 +08:00
wang-liang0615
600f95ce18 Merge pull request #168 from veops/dev_ui
UI更新
2023-09-05 15:23:43 +08:00
wang-liang0615
950fd38044 Merge branch 'master' of github.com:veops/cmdb into dev_ui 2023-09-05 15:22:18 +08:00
wang-liang0615
01085615b5 模型关联 展示反向关系 2023-09-05 15:22:08 +08:00
pycook
734f1940f9 Merge branch 'master' of github.com:veops/cmdb 2023-09-05 14:49:53 +08:00
pycook
c25c1e4e4b move Dockerfile to docs 2023-09-05 14:49:34 +08:00
wang-liang0615
826a8306d3 Merge pull request #167 from veops/dev_ui
sub menu color
2023-09-04 16:34:26 +08:00
wang-liang0615
740aae573e sub menu color 2023-09-04 16:33:35 +08:00
wang-liang0615
17828a7631 Merge pull request #166 from veops/dev_ui
ui更新
2023-09-04 13:15:27 +08:00
wang-liang0615
02cb497bdc Merge branch 'master' of github.com:veops/cmdb into dev_ui 2023-09-04 13:14:35 +08:00
wang-liang0615
05a7dc41ee sidebar 2023-09-04 13:14:11 +08:00
pycook
459c70ba2d import format 2023-09-02 12:09:41 +08:00
pycook
774f42ac34 format 2023-09-01 18:07:44 +08:00
pycook
420029a5e2 fix delete choice values 2023-08-31 16:02:24 +08:00
pycook
ab8acbfd20 fix delete choice values 2023-08-31 15:18:15 +08:00
wang-liang0615
4468b6a8de Merge pull request #165 from veops/dev_ui
proxy
2023-08-31 13:31:26 +08:00
wang-liang0615
6bf145d085 proxy 2023-08-31 13:28:15 +08:00
pycook
42b1e47e76 Merge pull request #162 from simontigers/cmdb_icon_manage
feat: add cmdb custom icon manage
2023-08-31 11:15:09 +08:00
pycook
673134003a Merge pull request #163 from veops/dev_ui
支持上传自定义图标
2023-08-31 11:14:42 +08:00
hu.sima
ef67885571 feat: add cmdb custom icon manage 2023-08-31 10:49:56 +08:00
wang-liang0615
075bf7217f 支持上传自定义图标 2023-08-31 10:05:11 +08:00
pycook
3b7b8f435c fix update attribute 2023-08-30 13:34:10 +08:00
pycook
2b7f6aeef3 Merge branch 'master' of github.com:veops/cmdb 2023-08-29 14:49:21 +08:00
pycook
544fac8aca The default value of USE_ACL is set to True 2023-08-29 14:49:09 +08:00
pycook
3d0a56ec8c Merge pull request #161 from simontigers/common_setting_format
fix: company info create
2023-08-29 11:01:25 +08:00
hu.sima
d2d8482052 fix: company info create 2023-08-29 10:56:48 +08:00
pycook
a0afae8d2e Merge branch 'master' of github.com:veops/cmdb 2023-08-25 11:01:24 +08:00
pycook
9f3da68636 update ad_ci when deleting ci 2023-08-25 10:59:38 +08:00
wang-liang0615
24b955c288 Merge pull request #160 from veops/dev_ui
前端更新
2023-08-25 10:12:31 +08:00
wang-liang0615
a07b2d37ec fix 新增类型回车键发送两次请求 2023-08-25 10:11:09 +08:00
wang-liang0615
c86fcb4e7b fix 新增类型回车键发送两次请求 2023-08-25 10:08:04 +08:00
pycook
ca7964f24b Merge pull request #158 from EvanSung/perf_20230824_optimize_ad_ci_relation
perf(ad_ci_relation): optimize ad_ci relation
2023-08-24 16:26:43 +08:00
EvanSung
c42ac634fb perf(ad_ci_relation): optimize ad_ci relation 2023-08-24 14:16:12 +08:00
pycook
a6fc3341ce docker-compose add flask db-setup 2023-08-24 11:32:09 +08:00
pycook
fc3f2e25f3 vxe-table-plugin-export-xlsx==2.0.0 2023-08-24 11:06:28 +08:00
pycook
511a5f70c6 add config CACHE_REDIS_PASSWORD and fix delete ci_type 2023-08-23 18:05:28 +08:00
pycook
f8ff4d5e45 fix update ci 2023-08-22 11:34:40 +08:00
pycook
3ab72cceaf Register api and commands with absolute paths 2023-08-21 20:08:23 +08:00
pycook
4ab7e3c70c fix merge conflict 2023-08-21 11:55:49 +08:00
pycook
a7fe75f7df fix g.user 2023-08-21 11:54:33 +08:00
pycook
3474a71a75 version: 2.3.1 2023-08-20 11:24:53 +08:00
pycook
6531baff64 lint 2023-08-20 11:23:55 +08:00
pycook
ed5936250f Merge pull request #157 from EvanSung/fix_20230817_guser_issue
fix(acl): g user issue
2023-08-17 22:12:45 +08:00
EvanSung
52c32e2ab1 fix(acl): g user issue 2023-08-17 18:40:45 +08:00
pycook
d3224625b6 fix MyJSONEncoder 2023-08-16 21:28:27 +08:00
pycook
f158c7e33a Merge pull request #155 from veops/dev_ui
前端更新
2023-08-16 13:01:13 +08:00
pycook
6dc12bb6ac Merge branch 'master' of github.com:veops/cmdb 2023-08-16 13:00:44 +08:00
pycook
b33ae16c00 Delete user without soft delete 2023-08-16 13:00:30 +08:00
wang-liang0615
2caffc2670 Merge branch 'master' of github.com:veops/cmdb into dev_ui 2023-08-16 10:09:47 +08:00
wang-liang0615
f28af51007 delete user 2023-08-16 10:09:25 +08:00
pycook
3a0369559f Merge pull request #154 from simontigers/common_setting_format
fix: init-import-user-from-acl
2023-08-15 20:47:36 +08:00
hu.sima
a74a2c5a94 fix: init-import-user-from-acl 2023-08-15 20:45:28 +08:00
pycook
9fbcb2838e Merge pull request #153 from simontigers/common_setting_format
fix: import_user_from_acl
2023-08-15 20:25:09 +08:00
hu.sima
60a445b972 fix: import_user_from_acl 2023-08-15 20:19:45 +08:00
pycook
bfdd7b6a0e Merge branch 'master' of github.com:veops/cmdb 2023-08-15 19:48:11 +08:00
pycook
ab093d2493 [update] delete roles, users, attributes 2023-08-15 19:47:59 +08:00
wang-liang0615
315a578a31 Merge pull request #152 from veops/dev_ui
前端更新
2023-08-15 19:47:03 +08:00
wang-liang0615
1e16dc5e5b 属性库 2023-08-15 19:34:17 +08:00
wang-liang0615
f67e196acf 属性库 2023-08-15 19:26:49 +08:00
wang-liang0615
439e25d5dd 属性库 2023-08-15 19:21:09 +08:00
wang-liang0615
ea59c0d71f 属性库 2023-08-15 19:10:26 +08:00
wang-liang0615
1137127aab 后台管理-模型关联 关系删除&&筛选 2023-08-15 15:02:46 +08:00
pycook
4ad1b5282e update gitattributes 2023-08-15 13:41:45 +08:00
wang-liang0615
cdd5e4d9aa Merge pull request #150 from EvanSung/optimize_20230810_acl_resource_fe
refactor(fe): reduce the width of resource mgt table
2023-08-10 19:32:49 +08:00
pycook
432de5e847 Merge pull request #148 from simontigers/common_setting_format
fix: default arg value
2023-08-10 19:31:18 +08:00
pycook
3a2339765a Merge pull request #149 from veops/dev_ui
ui更新:password
2023-08-10 19:28:24 +08:00
EvanSung
b5a2af7420 refactor(fe): reduce the width of resource mgt table 2023-08-10 19:23:41 +08:00
wang-liang0615
8b267613d6 Merge branch 'master' of github.com:veops/cmdb into dev_ui 2023-08-10 19:21:28 +08:00
wang-liang0615
b365eb27f6 增加密码明文传输 2023-08-10 19:21:10 +08:00
hu.sima
2125f020b5 fix: default arg value 2023-08-10 19:05:56 +08:00
pycook
ea762e35a0 Merge pull request #147 from simontigers/common_setting_format
fix: remove useless
2023-08-10 19:01:25 +08:00
hu.sima
f11aadf6d4 fix: remove useless 2023-08-10 18:55:32 +08:00
pycook
9cbf133b9f Merge pull request #146 from simontigers/common_setting_format
Common setting format
2023-08-10 18:23:24 +08:00
hu.sima
95e8f9de74 fix: remove unused column 2023-08-10 16:29:52 +08:00
hu.sima
26792147ae style: format common setting 2023-08-10 15:30:01 +08:00
pycook
4f9b581c2e Merge pull request #145 from EvanSung/optimize_20230810_auth_require
optimize(auth): auth request json
2023-08-10 11:24:23 +08:00
EvanSung
e2b1cb3003 optimize(auth): auth request json 2023-08-10 10:43:59 +08:00
pycook
f75a85b48a fix celery config 2023-08-08 16:33:24 +08:00
pycook
313fc80e54 Merge branch 'master' of github.com:veops/cmdb 2023-08-08 13:16:14 +08:00
pycook
e0666689e5 upgrade celery 2023-08-08 13:16:07 +08:00
pycook
7a9fd4f9d6 Merge pull request #144 from veops/dev_ui
UI更新:fix preferenceList=>attrList
2023-08-08 09:21:10 +08:00
wang-liang0615
2fd706be85 Merge branch 'master' of github.com:veops/cmdb into dev_ui 2023-08-08 09:11:24 +08:00
wang-liang0615
3df51bb670 fix preferenceList=>attrList 2023-08-08 09:11:03 +08:00
pycook
9bbbcbe6dc upgrade flask to 2.3.2 and replace g.user with current_user 2023-08-06 21:54:18 +08:00
pycook
16d6b40e8d Merge pull request #138 from lovvvve/fix_ldap
fix ldap login
2023-08-04 11:31:58 +08:00
pycook
ef2d3812a2 Merge pull request #142 from veops/dev_ui
ci 批量更新和删除的异步处理
2023-08-04 09:27:55 +08:00
wang-liang0615
bc653efd04 Merge branch 'master' of github.com:veops/cmdb into dev_ui 2023-08-03 16:54:47 +08:00
wang-liang0615
d891d7365d ci 批量更新和删除的异步处理 2023-08-03 16:54:27 +08:00
pycook
9953b2fc98 Merge pull request #139 from EvanSung/fix-post-acltrigger-session-invalid
fix(trigger): session invalid issue
2023-08-02 19:33:05 +08:00
songbing01249
8de54812dc fix(trigger): session invalid issue 2023-08-02 18:22:42 +08:00
lovvvve
eb7d52cf35 fix ldap login 2023-08-01 11:27:29 +00:00
pycook
6c4a5f2f6b Merge pull request #134 from veops/dependabot/pip/cmdb-api/pillow-9.3.0
Bump pillow from 9.2.0 to 9.3.0 in /cmdb-api
2023-08-01 15:57:02 +08:00
pycook
17c5d4538b Merge pull request #135 from simontigers/remove_pandas
fix: remove pandas
2023-08-01 15:55:15 +08:00
hu.sima
6c3e3f9eed fix: remove pandas 2023-08-01 15:32:44 +08:00
dependabot[bot]
b0494adc17 Bump pillow from 9.2.0 to 9.3.0 in /cmdb-api
Bumps [pillow](https://github.com/python-pillow/Pillow) from 9.2.0 to 9.3.0.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/9.2.0...9.3.0)

---
updated-dependencies:
- dependency-name: pillow
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-01 05:51:16 +00:00
pycook
fc133f2ae9 Merge branch 'master' of github.com:veops/cmdb 2023-08-01 13:47:34 +08:00
pycook
ac6e3a0318 fix dependabot alerts 2023-08-01 13:47:11 +08:00
pycook
404ec976cc fix dependabot alerts 2023-08-01 13:46:47 +08:00
pycook
4211bbcbc9 Merge pull request #130 from veops/dependabot/pip/cmdb-api/certifi-2023.7.22
Bump certifi from 2023.5.7 to 2023.7.22 in /cmdb-api
2023-08-01 13:14:25 +08:00
pycook
0158636671 Merge pull request #132 from veops/dev_ui
删除角色相关
2023-07-31 19:54:19 +08:00
wang-liang0615
d986bc3bbc 删除角色相关 2023-07-31 19:52:06 +08:00
pycook
044b820548 Merge branch 'master' of github.com:veops/cmdb 2023-07-31 18:39:46 +08:00
pycook
536daa6d4f fix delete ci_type 2023-07-31 18:39:33 +08:00
pycook
b0620b043b Merge pull request #131 from veops/dev_ui
前端acl
2023-07-28 18:03:36 +08:00
wang-liang0615
a88c9cf7f7 common-setting 2023-07-27 15:47:13 +08:00
wang-liang0615
be50f505d1 acl 2023-07-27 15:30:27 +08:00
wang-liang0615
0bb4f633d6 fix acl change page size 2023-07-27 15:08:25 +08:00
dependabot[bot]
78b521f3af Bump certifi from 2023.5.7 to 2023.7.22 in /cmdb-api
Bumps [certifi](https://github.com/certifi/python-certifi) from 2023.5.7 to 2023.7.22.
- [Commits](https://github.com/certifi/python-certifi/compare/2023.05.07...2023.07.22)

---
updated-dependencies:
- dependency-name: certifi
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-25 23:35:30 +00:00
pycook
77bc850d4a Merge pull request #129 from veops/dev_ui
前端更新
2023-07-25 18:19:47 +08:00
wang-liang0615
e52f201ba1 Merge branch 'master' of github.com:veops/cmdb into dev_ui 2023-07-25 13:11:03 +08:00
wang-liang0615
64aea424dc 授权高亮提示 2023-07-25 13:10:45 +08:00
pycook
0655b0e9eb add command cmdb-index-table-upgrade 2023-07-25 10:31:30 +08:00
wang-liang0615
cce0649299 style 新建属性行错乱 2023-07-25 10:18:22 +08:00
pycook
52574c64cc 废弃3个表: c_value_datetime c_value_floats c_value_integers, time类型属性值增加写入校验 2023-07-24 21:55:00 +08:00
149 changed files with 3021 additions and 2735 deletions

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
*.vue linguist-language=python

1
.gitignore vendored
View File

@@ -39,6 +39,7 @@ pip-log.txt
nosetests.xml
.pytest_cache
cmdb-api/test-output
cmdb-api/api/uploaded_files
# Translations
*.mo

View File

@@ -1,37 +1,52 @@
.PHONY: env clean api ui worker
MYSQL_ROOT_PASSWORD ?= root
MYSQL_PORT ?= 3306
REDIS_PORT ?= 6379
help:
@echo " env create a development environment using pipenv"
@echo " deps install dependencies using pip"
@echo " clean remove unwanted files like .pyc's"
@echo " lint check style with flake8"
@echo " api start api server"
@echo " ui start ui server"
@echo " worker start async tasks worker"
default: help
help: ## display this help
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z0-9_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
.PHONY: help
env:
env: ## create a development environment using pipenv
sudo easy_install pip && \
pip install pipenv -i https://pypi.douban.com/simple && \
npm install yarn && \
make deps
.PHONY: env
deps:
docker-mysql: ## deploy MySQL use docker
@docker run --name mysql -p ${MYSQL_PORT}:3306 -e MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} -d mysql:latest
.PHONY: docker-mysql
docker-redis: ## deploy Redis use docker
@docker run --name redis -p ${REDIS_PORT}:6379 -d redis:latest
.PHONY: docker-redis
deps: ## install dependencies using pip
cd cmdb-api && \
pipenv install --dev && \
pipenv run flask db-setup && \
pipenv run flask cmdb-init-cache && \
cd .. && \
cd cmdb-ui && yarn install && cd ..
.PHONY: deps
api:
api: ## start api server
cd cmdb-api && pipenv run flask run -h 0.0.0.0
.PHONY: api
worker:
cd cmdb-api && pipenv run celery worker -A celery_worker.celery -E -Q one_cmdb_async --concurrency=1 -D && pipenv run celery worker -A celery_worker.celery -E -Q acl_async --concurrency=1 -D
worker: ## start async tasks worker
cd cmdb-api && pipenv run celery -A celery_worker.celery worker -E -Q one_cmdb_async --concurrency=1 -D && pipenv run celery -A celery_worker.celery worker -E -Q acl_async --concurrency=1 -D
.PHONY: worker
ui:
ui: ## start ui server
cd cmdb-ui && yarn run serve
.PHONY: ui
clean:
clean: ## remove unwanted files like .pyc's
pipenv run flask clean
.PHONY: clean
lint:
lint: ## check style with flake8
flake8 --exclude=env .
.PHONY: lint

View File

@@ -1,13 +1,13 @@
![维易CMDB](docs/images/logo.png)
![维易开源CMDB](docs/images/logo.png)
[![License](https://img.shields.io/badge/License-AGPLv3-brightgreen)](https://github.com/veops/cmdb/blob/master/LICENSE)
[![UI](https://img.shields.io/badge/UI-Ant%20Design%20Pro%20Vue-brightgreen)](https://github.com/sendya/ant-design-pro-vue)
[![API](https://img.shields.io/badge/API-Flask-brightgreen)](https://github.com/pallets/flask)
[English](README_en.md) / [中文](README.md)
[English](docs/README_en.md) / [中文](README.md)
- 产品文档https://veops.cn/docs/
- 在线体验: <a href="https://cmdb.veops.cn" target="_blank">CMDB</a>
- username: demo
- username: demo 或者 admin
- password: 123456
> **重要提示**: `master` 分支在开发过程中可能处于 _不稳定的状态_ 。
@@ -51,7 +51,7 @@
- 服务树
![1](docs/images/0.png "首页展示")
![服务树](docs/images/0.png "首页展示")
[查看更多展示](docs/screenshot.md)
@@ -62,7 +62,7 @@
## 接入公司
> 欢迎使用CMDB的公司在 [#112](https://github.com/veops/cmdb/issues/112) 登记
> 欢迎使用开源CMDB的公司在 [#112](https://github.com/veops/cmdb/issues/112) 登记
## 安装
@@ -83,6 +83,6 @@ docker-compose up -d
---
_**欢迎关注我们的公众号,点击联系我们,加入微信、qq运维群(336164978),获得更多产品、行业相关资讯**_
_**欢迎关注我们的公众号,点击联系我们,加入微信、QQ群(336164978),获得更多产品、行业相关资讯**_
![公众号](docs/images/qrcode_for_gzh.jpg)
![公众号: 维易科技OneOps](docs/images/qrcode_for_gzh.jpg)

View File

@@ -1,16 +0,0 @@
default: help
test: ## test in local environment
pytest -s --html=test-output/test/index.html --cov-report html:test-output/coverage --cov=api tests
clean_test: ## clean test output
rm -f .coverage
rm -rf .pytest_cache
rm -rf test-output
docker_test: ## test all case in docker container
@echo "TODO"
help:
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' ./Makefile | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'

View File

@@ -5,26 +5,26 @@ name = "pypi"
[packages]
# Flask
Flask = "==1.0.3"
Werkzeug = "==0.15.5"
Flask = "==2.3.2"
Werkzeug = "==2.3.6"
click = ">=5.0"
# Api
Flask-RESTful = "==0.3.7"
Flask-RESTful = "==0.3.10"
# Database
Flask-SQLAlchemy = "==2.4.0"
SQLAlchemy = "==1.3.5"
PyMySQL = "==0.9.3"
redis = "==3.2.1"
Flask-SQLAlchemy = "==2.5.0"
SQLAlchemy = "==1.4.49"
PyMySQL = "==1.1.0"
redis = "==4.6.0"
# Migrations
Flask-Migrate = "==2.5.2"
# Deployment
gunicorn = "==19.5.0"
gunicorn = "==21.0.1"
supervisor = "==4.0.3"
# Auth
Flask-Login = "==0.4.1"
Flask-Bcrypt = "==0.7.1"
Flask-Login = "==0.6.2"
Flask-Bcrypt = "==1.0.1"
Flask-Cors = ">=3.0.8"
python-ldap = "==3.2.0"
python-ldap = "==3.4.0"
pycryptodome = "==3.12.0"
# Caching
Flask-Caching = ">=1.0.0"
@@ -32,20 +32,17 @@ Flask-Caching = ">=1.0.0"
environs = "==4.2.0"
marshmallow = "==2.20.2"
# async tasks
celery = "==4.3.0"
celery = "==5.3.1"
celery_once = "==3.0.1"
more-itertools = "==5.0.0"
kombu = "==4.4.0"
kombu = "==5.3.1"
# common setting
Flask-APScheduler = "==1.12.4"
timeout-decorator = "==0.5.0"
numpy = "==1.18.5"
pandas = "==1.3.2"
WTForms = "==3.0.0"
email-validator = "==1.3.1"
treelib = "==1.6.1"
flasgger = "==0.9.5"
Pillow = "==8.3.2"
Pillow = "==9.3.0"
# other
six = "==1.12.0"
bs4 = ">=0.0.1"
@@ -53,9 +50,9 @@ toposort = ">=1.5"
requests = ">=2.22.0"
PyJWT = "==2.4.0"
elasticsearch = "==7.17.9"
future = "==0.18.2"
itsdangerous = "==2.0.1"
Jinja2 = "==3.0.1"
future = "==0.18.3"
itsdangerous = "==2.1.2"
Jinja2 = "==3.1.2"
jinja2schema = "==0.1.4"
msgpack-python = "==0.5.6"
alembic = "==1.7.7"

View File

View File

@@ -9,29 +9,19 @@ from inspect import getmembers
from logging.handlers import RotatingFileHandler
from flask import Flask
from flask import make_response, jsonify
from flask import jsonify
from flask import make_response
from flask.blueprints import Blueprint
from flask.cli import click
from flask.json import JSONEncoder
from flask.json.provider import DefaultJSONProvider
import api.views.entry
from api.extensions import (
bcrypt,
cors,
cache,
db,
login_manager,
migrate,
celery,
rd,
es,
)
from api.extensions import (bcrypt, cache, celery, cors, db, es, login_manager, migrate, rd)
from api.flask_cas import CAS
from api.models.acl import User
HERE = os.path.abspath(os.path.dirname(__file__))
PROJECT_ROOT = os.path.join(HERE, os.pardir)
API_PACKAGE = "api"
@login_manager.user_loader
@@ -75,7 +65,7 @@ class ReverseProxy(object):
return self.app(environ, start_response)
class MyJSONEncoder(JSONEncoder):
class MyJSONEncoder(DefaultJSONProvider):
def default(self, o):
if isinstance(o, (decimal.Decimal, datetime.date, datetime.time)):
return str(o)
@@ -103,7 +93,7 @@ def create_app(config_object="settings"):
app = Flask(__name__.split(".")[0])
app.config.from_object(config_object)
app.json_encoder = MyJSONEncoder
app.json = MyJSONEncoder(app)
configure_logger(app)
register_extensions(app)
register_blueprints(app)
@@ -139,6 +129,8 @@ def register_extensions(app):
rd.init_app(app)
if app.config.get('USE_ES'):
es.init_app(app)
app.config.update(app.config.get("CELERY"))
celery.conf.update(app.config)
@@ -158,10 +150,8 @@ def register_error_handlers(app):
error_code = getattr(error, "code", 500)
if not str(error_code).isdigit():
error_code = 400
if error_code != 500:
return make_response(jsonify(message=str(error)), error_code)
else:
return make_response(jsonify(message=traceback.format_exc(-1)), error_code)
return make_response(jsonify(message=str(error)), error_code)
for errcode in app.config.get("ERROR_CODES") or [400, 401, 403, 404, 405, 500, 502]:
app.errorhandler(errcode)(render_error)
@@ -184,9 +174,8 @@ def register_commands(app):
for root, _, files in os.walk(os.path.join(HERE, "commands")):
for filename in files:
if not filename.startswith("_") and filename.endswith("py"):
module_path = os.path.join(API_PACKAGE, root[root.index("commands"):])
if module_path not in sys.path:
sys.path.insert(1, module_path)
if root not in sys.path:
sys.path.insert(1, root)
command = __import__(os.path.splitext(filename)[0])
func_list = [o[0] for o in getmembers(command) if isinstance(o[1], click.core.Command)]
for func_name in func_list:

View File

@@ -5,6 +5,9 @@ from flask.cli import with_appcontext
@click.command()
@with_appcontext
def init_acl():
"""
acl init
"""
from api.models.acl import Role
from api.models.acl import App
from api.tasks.acl import role_rebuild

View File

@@ -13,6 +13,7 @@ from flask.cli import with_appcontext
import api.lib.cmdb.ci
from api.extensions import db
from api.extensions import rd
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.ci_type import CITypeTriggerManager
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import REDIS_PREFIX_CI
@@ -29,6 +30,7 @@ from api.lib.perm.acl.role import RoleCRUD
from api.lib.perm.acl.user import UserCRUD
from api.models.acl import App
from api.models.acl import ResourceType
from api.models.cmdb import Attribute
from api.models.cmdb import CI
from api.models.cmdb import CIRelation
from api.models.cmdb import CIType
@@ -200,6 +202,9 @@ def del_user(user):
@click.command()
@with_appcontext
def cmdb_counter():
"""
Dashboard calculations
"""
from api.lib.cmdb.cache import CMDBCounterCache
while True:
@@ -217,6 +222,9 @@ def cmdb_counter():
@click.command()
@with_appcontext
def cmdb_trigger():
"""
Trigger execution
"""
current_day = datetime.datetime.today().strftime("%Y-%m-%d")
trigger2cis = dict()
trigger2completed = dict()
@@ -259,3 +267,34 @@ def cmdb_trigger():
i += 1
time.sleep(10)
@click.command()
@with_appcontext
def cmdb_index_table_upgrade():
"""
Migrate data from tables c_value_integers, c_value_floats, and c_value_datetime
"""
for attr in Attribute.get_by(to_dict=False):
if attr.value_type not in {ValueTypeEnum.TEXT, ValueTypeEnum.JSON} and not attr.is_index:
attr.update(is_index=True)
AttributeCache.clean(attr)
from api.models.cmdb import CIValueInteger, CIIndexValueInteger
from api.models.cmdb import CIValueFloat, CIIndexValueFloat
from api.models.cmdb import CIValueDateTime, CIIndexValueDateTime
for i in CIValueInteger.get_by(to_dict=False):
CIIndexValueInteger.create(ci_id=i.ci_id, attr_id=i.attr_id, value=i.value, commit=False)
i.delete(commit=False)
db.session.commit()
for i in CIValueFloat.get_by(to_dict=False):
CIIndexValueFloat.create(ci_id=i.ci_id, attr_id=i.attr_id, value=i.value, commit=False)
i.delete(commit=False)
db.session.commit()
for i in CIValueDateTime.get_by(to_dict=False):
CIIndexValueDateTime.create(ci_id=i.ci_id, attr_id=i.attr_id, value=i.value, commit=False)
i.delete(commit=False)
db.session.commit()

View File

@@ -19,16 +19,27 @@ class InitEmployee(object):
def import_user_from_acl(self):
"""
从ACL导入用户
Import users from ACL
"""
InitDepartment().init()
acl = ACLManager('acl')
user_list = acl.get_all_users()
username_list = [e['username'] for e in Employee.get_by()]
for user in user_list:
acl_uid = user['uid']
block = 1 if user['block'] else 0
acl_rid = self.get_rid_by_uid(acl_uid)
if user['username'] in username_list:
existed = Employee.get_by(first=True, username=user['username'], to_dict=False)
if existed:
existed.update(
acl_uid=acl_uid,
acl_rid=acl_rid,
block=block,
)
continue
try:
form = EmployeeAddForm(MultiDict(user))
@@ -36,8 +47,9 @@ class InitEmployee(object):
raise Exception(
','.join(['{}: {}'.format(filed, ','.join(msg)) for filed, msg in form.errors.items()]))
data = form.data
data['acl_uid'] = user['uid']
data['block'] = 1 if user['block'] else 0
data['acl_uid'] = acl_uid
data['acl_rid'] = acl_rid
data['block'] = block
data.pop('password')
Employee.create(
**data
@@ -46,6 +58,11 @@ class InitEmployee(object):
self.log.error(ErrFormat.acl_import_user_failed.format(user['username'], str(e)))
self.log.error(e)
def get_rid_by_uid(self, uid):
from api.models.acl import Role
role = Role.get_by(first=True, uid=uid)
return role['id'] if role is not None else 0
class InitDepartment(object):
def __init__(self):
@@ -149,7 +166,7 @@ class InitDepartment(object):
@with_appcontext
def init_import_user_from_acl():
"""
从ACL导入用户
Import users from ACL
"""
InitEmployee().import_user_from_acl()
@@ -158,7 +175,7 @@ def init_import_user_from_acl():
@with_appcontext
def init_department():
"""
初始化 部门
Department initialization
"""
InitDepartment().init()
InitDepartment().create_acl_role_with_department()

View File

@@ -3,13 +3,19 @@
import requests
from flask import abort
from flask import current_app
from flask import g
from flask import session
from flask_login import current_user
from api.extensions import db
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import CITypeAttributesCache
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.const import BUILTIN_KEYWORDS
from api.lib.cmdb.const import CITypeOperateType
from api.lib.cmdb.const import ResourceTypeEnum, RoleEnum, PermEnum
from api.lib.cmdb.const import CMDB_QUEUE
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.const import RoleEnum
from api.lib.cmdb.const import ValueTypeEnum
from api.lib.cmdb.history import CITypeHistoryManager
from api.lib.cmdb.resp_format import ErrFormat
@@ -39,7 +45,7 @@ class AttributeManager(object):
ret_key = choice_web_hook.get('ret_key')
headers = choice_web_hook.get('headers') or {}
payload = choice_web_hook.get('payload') or {}
method = choice_web_hook.get('method', 'GET').lower()
method = (choice_web_hook.get('method') or 'GET').lower()
try:
res = getattr(requests, method)(url, headers=headers, data=payload).json()
@@ -54,15 +60,17 @@ class AttributeManager(object):
return [[i, {}] for i in (res.get(ret_key_list[-1]) or [])]
except Exception as e:
current_app.logger.error(str(e))
current_app.logger.error("get choice values failed: {}".format(e))
return []
@classmethod
def get_choice_values(cls, attr_id, value_type, choice_web_hook, choice_web_hook_parse=True):
if choice_web_hook and isinstance(choice_web_hook, dict) and choice_web_hook_parse:
return cls._get_choice_values_from_web_hook(choice_web_hook)
elif choice_web_hook and not choice_web_hook_parse:
return []
if choice_web_hook:
if choice_web_hook_parse:
if isinstance(choice_web_hook, dict):
return cls._get_choice_values_from_web_hook(choice_web_hook)
else:
return []
choice_table = ValueTypeMap.choice.get(value_type)
choice_values = choice_table.get_by(fl=["value", "option"], attr_id=attr_id)
@@ -72,34 +80,34 @@ class AttributeManager(object):
@staticmethod
def add_choice_values(_id, value_type, choice_values):
choice_table = ValueTypeMap.choice.get(value_type)
if choice_table is None:
return
choice_table.get_by(attr_id=_id, only_query=True).delete()
db.session.query(choice_table).filter(choice_table.attr_id == _id).delete()
db.session.flush()
choice_values = choice_values
for v, option in choice_values:
table = choice_table(attr_id=_id, value=v, option=option)
db.session.add(table)
choice_table.create(attr_id=_id, value=v, option=option, commit=False)
try:
db.session.flush()
except:
except Exception as e:
current_app.logger.warning("add choice values failed: {}".format(e))
return abort(400, ErrFormat.invalid_choice_values)
@staticmethod
def _del_choice_values(_id, value_type):
choice_table = ValueTypeMap.choice.get(value_type)
db.session.query(choice_table).filter(choice_table.attr_id == _id).delete()
choice_table and choice_table.get_by(attr_id=_id, only_query=True).delete()
db.session.flush()
@classmethod
def search_attributes(cls, name=None, alias=None, page=1, page_size=None):
"""
:param name:
:param alias:
:param page:
:param page_size:
:param name:
:param alias:
:param page:
:param page_size:
:return: attribute, if name is None, then return all attributes
"""
if name is not None:
@@ -113,8 +121,8 @@ class AttributeManager(object):
attrs = attrs[(page - 1) * page_size:][:page_size]
res = list()
for attr in attrs:
attr["is_choice"] and attr.update(dict(choice_value=cls.get_choice_values(
attr["id"], attr["value_type"], attr["choice_web_hook"])))
attr["is_choice"] and attr.update(
dict(choice_value=cls.get_choice_values(attr["id"], attr["value_type"], attr["choice_web_hook"])))
attr['is_choice'] and attr.pop('choice_web_hook', None)
res.append(attr)
@@ -123,30 +131,31 @@ class AttributeManager(object):
def get_attribute_by_name(self, name):
attr = Attribute.get_by(name=name, first=True)
if attr and attr["is_choice"]:
attr.update(dict(choice_value=self.get_choice_values(
attr["id"], attr["value_type"], attr["choice_web_hook"])))
if attr.get("is_choice"):
attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"], attr["choice_web_hook"])
return attr
def get_attribute_by_alias(self, alias):
attr = Attribute.get_by(alias=alias, first=True)
if attr and attr["is_choice"]:
attr.update(dict(choice_value=self.get_choice_values(
attr["id"], attr["value_type"], attr["choice_web_hook"])))
if attr.get("is_choice"):
attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"], attr["choice_web_hook"])
return attr
def get_attribute_by_id(self, _id):
attr = Attribute.get_by_id(_id).to_dict()
if attr and attr["is_choice"]:
attr.update(dict(choice_value=self.get_choice_values(
attr["id"], attr["value_type"], attr["choice_web_hook"])))
if attr.get("is_choice"):
attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"], attr["choice_web_hook"])
return attr
def get_attribute(self, key, choice_web_hook_parse=True):
attr = AttributeCache.get(key).to_dict()
if attr and attr["is_choice"]:
attr.update(dict(choice_value=self.get_choice_values(
attr["id"], attr["value_type"], attr["choice_web_hook"])), choice_web_hook_parse=choice_web_hook_parse)
if attr.get("is_choice"):
attr["choice_value"] = self.get_choice_values(
attr["id"], attr["value_type"], attr["choice_web_hook"], choice_web_hook_parse=choice_web_hook_parse)
return attr
@staticmethod
@@ -154,6 +163,17 @@ class AttributeManager(object):
if RoleEnum.CONFIG not in session.get("acl", {}).get("parentRoles", []) and not is_app_admin('cmdb'):
return abort(403, ErrFormat.role_required.format(RoleEnum.CONFIG))
@staticmethod
def calc_computed_attribute(attr_id):
"""
calculate computed attribute for all ci
:param attr_id:
:return:
"""
from api.tasks.cmdb import calc_computed_attribute
calc_computed_attribute.apply_async(args=(attr_id, current_user.uid), queue=CMDB_QUEUE)
@classmethod
@kwargs_required("name")
def add(cls, **kwargs):
@@ -162,8 +182,9 @@ class AttributeManager(object):
is_choice = True if choice_value or kwargs.get('choice_web_hook') else False
name = kwargs.pop("name")
if name in {'id', '_id', 'ci_id', 'type', '_type', 'ci_type'}:
if name in BUILTIN_KEYWORDS:
return abort(400, ErrFormat.attribute_name_cannot_be_builtin)
alias = kwargs.pop("alias", "")
alias = name if not alias else alias
Attribute.get_by(name=name, first=True) and abort(400, ErrFormat.attribute_name_duplicate.format(name))
@@ -177,7 +198,7 @@ class AttributeManager(object):
name=name,
alias=alias,
is_choice=is_choice,
uid=g.user.uid,
uid=current_user.uid,
**kwargs)
if choice_value:
@@ -211,6 +232,11 @@ class AttributeManager(object):
return attr.id
@staticmethod
def _clean_ci_type_attributes_cache(attr_id):
for i in CITypeAttribute.get_by(attr_id=attr_id, to_dict=False):
CITypeAttributesCache.clean(i.type_id)
@staticmethod
def _change_index(attr, old, new):
from api.lib.cmdb.utils import TableMap
@@ -221,11 +247,11 @@ class AttributeManager(object):
new_table = TableMap(attr=attr, is_index=new).table
ci_ids = []
for i in db.session.query(old_table).filter(getattr(old_table, 'attr_id') == attr.id):
for i in old_table.get_by(attr_id=attr.id, to_dict=False):
new_table.create(ci_id=i.ci_id, attr_id=attr.id, value=i.value, flush=True)
ci_ids.append(i.ci_id)
db.session.query(old_table).filter(getattr(old_table, 'attr_id') == attr.id).delete()
old_table.get_by(attr_id=attr.id, only_query=True).delete()
try:
db.session.commit()
@@ -240,7 +266,7 @@ class AttributeManager(object):
def _can_edit_attribute(attr):
from api.lib.cmdb.ci_type import CITypeManager
if attr.uid == g.user.uid:
if attr.uid == current_user.uid:
return True
for i in CITypeAttribute.get_by(attr_id=attr.id, to_dict=False):
@@ -290,7 +316,7 @@ class AttributeManager(object):
if is_choice and choice_value:
self.add_choice_values(attr.id, attr.value_type, choice_value)
elif is_choice:
elif existed2['is_choice']:
self._del_choice_values(attr.id, attr.value_type)
try:
@@ -309,6 +335,8 @@ class AttributeManager(object):
AttributeCache.clean(attr)
self._clean_ci_type_attributes_cache(_id)
return attr.id
@staticmethod
@@ -319,25 +347,28 @@ class AttributeManager(object):
if CIType.get_by(unique_id=attr.id, first=True, to_dict=False) is not None:
return abort(400, ErrFormat.attribute_is_unique_id)
if attr.uid and attr.uid != g.user.uid:
ref = CITypeAttribute.get_by(attr_id=_id, to_dict=False, first=True)
if ref is not None:
ci_type = CITypeCache.get(ref.type_id)
return abort(400, ErrFormat.attribute_is_ref_by_type.format(ci_type and ci_type.alias or ref.type_id))
if attr.uid != current_user.uid and not is_app_admin('cmdb'):
return abort(403, ErrFormat.cannot_delete_attribute)
if attr.is_choice:
choice_table = ValueTypeMap.choice.get(attr.value_type)
db.session.query(choice_table).filter(choice_table.attr_id == _id).delete() # FIXME: session conflict
db.session.flush()
AttributeCache.clean(attr)
choice_table.get_by(attr_id=_id, only_query=True).delete()
attr.soft_delete()
for i in CITypeAttribute.get_by(attr_id=_id, to_dict=False):
i.soft_delete()
AttributeCache.clean(attr)
for i in PreferenceShowAttributes.get_by(attr_id=_id, to_dict=False):
i.soft_delete()
i.soft_delete(commit=False)
for i in CITypeAttributeGroupItem.get_by(attr_id=_id, to_dict=False):
i.soft_delete()
i.soft_delete(commit=False)
db.session.commit()
return name

View File

@@ -5,7 +5,7 @@ import os
from flask import abort
from flask import current_app
from flask import g
from flask_login import current_user
from sqlalchemy import func
from api.extensions import db
@@ -156,7 +156,7 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
continue
if isinstance(rule.get("extra_option"), dict) and rule['extra_option'].get('secret'):
if not (g.user.username == "cmdb_agent" or g.user.uid == rule['uid']):
if not (current_user.username == "cmdb_agent" or current_user.uid == rule['uid']):
rule['extra_option'].pop('secret', None)
else:
rule['extra_option']['secret'] = AESCrypto.decrypt(rule['extra_option']['secret'])
@@ -213,7 +213,7 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
agent_id = agent_id.strip()
q = "op_duty:{0},-rd_duty:{0},oneagent_id:{1}"
s = search(q.format(g.user.username, agent_id.strip()))
s = search(q.format(current_user.username, agent_id.strip()))
try:
response, _, _, _, _, _ = s.search()
if response:
@@ -222,7 +222,7 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
current_app.logger.warning(e)
return abort(400, str(e))
s = search(q.format(g.user.nickname, agent_id.strip()))
s = search(q.format(current_user.nickname, agent_id.strip()))
try:
response, _, _, _, _, _ = s.search()
if response:
@@ -240,9 +240,10 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
try:
response, _, _, _, _, _ = s.search()
for i in response:
if g.user.username not in (i.get('rd_duty') or []) and g.user.username not in \
(i.get('op_duty') or []) and g.user.nickname not in (i.get('rd_duty') or []) and \
g.user.nickname not in (i.get('op_duty') or []):
if (current_user.username not in (i.get('rd_duty') or []) and
current_user.username not in (i.get('op_duty') or []) and
current_user.nickname not in (i.get('rd_duty') or []) and
current_user.nickname not in (i.get('op_duty') or [])):
return abort(403, ErrFormat.adt_target_expr_no_permission.format(
i.get("{}_name".format(i.get('ci_type')))))
except SearchError as e:
@@ -270,7 +271,7 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
if isinstance(kwargs.get('extra_option'), dict) and kwargs['extra_option'].get('secret'):
kwargs['extra_option']['secret'] = AESCrypto.encrypt(kwargs['extra_option']['secret'])
kwargs['uid'] = g.user.uid
kwargs['uid'] = current_user.uid
return kwargs
@@ -281,7 +282,7 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
self.__valid_exec_target(kwargs.get('agent_id'), kwargs.get('query_expr'))
if isinstance(kwargs.get('extra_option'), dict) and kwargs['extra_option'].get('secret'):
if g.user.uid != existed.uid:
if current_user.uid != existed.uid:
return abort(403, ErrFormat.adt_secret_no_permission)
return existed
@@ -453,10 +454,12 @@ class AutoDiscoveryCICRUD(DBMixin):
relation_adts = AutoDiscoveryCIType.get_by(type_id=adt.type_id, adr_id=None, to_dict=False)
for r_adt in relation_adts:
if r_adt.relation and ci_id is not None:
ad_key, cmdb_key = None, {}
for ad_key in r_adt.relation:
cmdb_key = r_adt.relation[ad_key]
if not r_adt.relation or ci_id is None:
continue
for ad_key in r_adt.relation:
if not adc.instance.get(ad_key):
continue
cmdb_key = r_adt.relation[ad_key]
query = "_type:{},{}:{}".format(cmdb_key.get('type_name'), cmdb_key.get('attr_name'),
adc.instance.get(ad_key))
s = search(query)
@@ -476,7 +479,10 @@ class AutoDiscoveryCICRUD(DBMixin):
except:
pass
adc.update(is_accept=True, accept_by=nickname or g.user.nickname, accept_time=datetime.datetime.now())
adc.update(is_accept=True,
accept_by=nickname or current_user.nickname,
accept_time=datetime.datetime.now(),
ci_id=ci_id)
class AutoDiscoveryHTTPManager(object):

View File

@@ -34,6 +34,7 @@ class AttributeCache(object):
attr = attr or Attribute.get_by(alias=key, first=True, to_dict=False)
if attr is not None:
cls.set(attr)
return attr
@classmethod
@@ -67,6 +68,7 @@ class CITypeCache(object):
ct = ct or CIType.get_by(alias=key, first=True, to_dict=False)
if ct is not None:
cls.set(ct)
return ct
@classmethod
@@ -98,6 +100,7 @@ class RelationTypeCache(object):
ct = RelationType.get_by(name=key, first=True, to_dict=False) or RelationType.get_by_id(key)
if ct is not None:
cls.set(ct)
return ct
@classmethod
@@ -133,12 +136,15 @@ class CITypeAttributesCache(object):
attrs = attrs or cache.get(cls.PREFIX_ID.format(key))
if not attrs:
attrs = CITypeAttribute.get_by(type_id=key, to_dict=False)
if not attrs:
ci_type = CIType.get_by(name=key, first=True, to_dict=False)
if ci_type is not None:
attrs = CITypeAttribute.get_by(type_id=ci_type.id, to_dict=False)
if attrs is not None:
cls.set(key, attrs)
return attrs
@classmethod
@@ -155,13 +161,16 @@ class CITypeAttributesCache(object):
attrs = attrs or cache.get(cls.PREFIX_ID2.format(key))
if not attrs:
attrs = CITypeAttribute.get_by(type_id=key, to_dict=False)
if not attrs:
ci_type = CIType.get_by(name=key, first=True, to_dict=False)
if ci_type is not None:
attrs = CITypeAttribute.get_by(type_id=ci_type.id, to_dict=False)
if attrs is not None:
attrs = [(i, AttributeCache.get(i.attr_id)) for i in attrs]
cls.set2(key, attrs)
return attrs
@classmethod
@@ -204,10 +213,11 @@ class CITypeAttributeCache(object):
attr = cache.get(cls.PREFIX_ID.format(type_id, attr_id))
attr = attr or cache.get(cls.PREFIX_ID.format(type_id, attr_id))
if not attr:
attr = CITypeAttribute.get_by(type_id=type_id, attr_id=attr_id, first=True, to_dict=False)
if attr is not None:
cls.set(type_id, attr_id, attr)
attr = attr or CITypeAttribute.get_by(type_id=type_id, attr_id=attr_id, first=True, to_dict=False)
if attr is not None:
cls.set(type_id, attr_id, attr)
return attr
@classmethod

View File

@@ -7,7 +7,7 @@ import json
from flask import abort
from flask import current_app
from flask import g
from flask_login import current_user
from werkzeug.exceptions import BadRequest
from api.extensions import db
@@ -24,8 +24,8 @@ from api.lib.cmdb.const import CMDB_QUEUE
from api.lib.cmdb.const import ConstraintEnum
from api.lib.cmdb.const import ExistPolicy
from api.lib.cmdb.const import OperateType
from api.lib.cmdb.const import PermEnum, ResourceTypeEnum
from api.lib.cmdb.const import REDIS_PREFIX_CI
from api.lib.cmdb.const import ResourceTypeEnum, PermEnum
from api.lib.cmdb.const import RetKey
from api.lib.cmdb.history import AttributeHistoryManger
from api.lib.cmdb.history import CIRelationHistoryManager
@@ -40,15 +40,19 @@ from api.lib.perm.acl.acl import is_app_admin
from api.lib.perm.acl.acl import validate_permission
from api.lib.utils import Lock
from api.lib.utils import handle_arg_list
from api.models.cmdb import AutoDiscoveryCI
from api.models.cmdb import CI
from api.models.cmdb import CIRelation
from api.models.cmdb import CITypeAttribute
from api.models.cmdb import CITypeRelation
from api.tasks.cmdb import ci_cache
from api.tasks.cmdb import ci_delete
from api.tasks.cmdb import ci_relation_add
from api.tasks.cmdb import ci_relation_cache
from api.tasks.cmdb import ci_relation_delete
PRIVILEGED_USERS = {"worker", "cmdb_agent", "agent"}
class CIManager(object):
""" manage CI interface
@@ -64,11 +68,13 @@ class CIManager(object):
@staticmethod
def get_type_name(ci_id):
ci = CI.get_by_id(ci_id) or abort(404, ErrFormat.ci_not_found.format("id={}".format(ci_id)))
return CITypeCache.get(ci.type_id).name
@staticmethod
def get_type(ci_id):
ci = CI.get_by_id(ci_id) or abort(404, ErrFormat.ci_not_found.format("id={}".format(ci_id)))
return CITypeCache.get(ci.type_id)
@staticmethod
@@ -90,9 +96,7 @@ class CIManager(object):
res = dict()
if need_children:
children = CIRelationManager.get_children(ci_id, ret_key=ret_key) # one floor
res.update(children)
need_children and res.update(CIRelationManager.get_children(ci_id, ret_key=ret_key)) # one floor
ci_type = CITypeCache.get(ci.type_id)
res["ci_type"] = ci_type.name
@@ -159,14 +163,11 @@ class CIManager(object):
ci = CI.get_by_id(ci_id) or abort(404, ErrFormat.ci_not_found.format("id={}".format(ci_id)))
if valid:
cls.valid_ci_only_read(ci)
valid and cls.valid_ci_only_read(ci)
res = dict()
if need_children:
children = CIRelationManager.get_children(ci_id, ret_key=ret_key) # one floor
res.update(children)
need_children and res.update(CIRelationManager.get_children(ci_id, ret_key=ret_key)) # one floor
ci_type = CITypeCache.get(ci.type_id)
res["ci_type"] = ci_type.name
@@ -245,7 +246,7 @@ class CIManager(object):
for i in unique_constraints:
attr_ids.extend(i.attr_ids)
attrs = [AttributeCache.get(i) for i in list(set(attr_ids))]
attrs = [AttributeCache.get(i) for i in set(attr_ids)]
id2name = {i.id: i.name for i in attrs if i}
not_existed_fields = list(set(id2name.values()) - set(ci_dict.keys()))
if not_existed_fields and ci_id is not None:
@@ -290,7 +291,7 @@ class CIManager(object):
_is_admin=False,
**ci_dict):
"""
add ci
:param ci_type_name:
:param exist_policy: replace or reject or need
:param _no_attribute_policy: ignore or reject
@@ -305,9 +306,7 @@ class CIManager(object):
unique_key = AttributeCache.get(ci_type.unique_id) or abort(
400, ErrFormat.unique_value_not_found.format("unique_id={}".format(ci_type.unique_id)))
unique_value = ci_dict.get(unique_key.name)
unique_value = unique_value or ci_dict.get(unique_key.alias)
unique_value = unique_value or ci_dict.get(unique_key.id)
unique_value = ci_dict.get(unique_key.name) or ci_dict.get(unique_key.alias) or ci_dict.get(unique_key.id)
unique_value = unique_value or abort(400, ErrFormat.unique_key_required.format(unique_key.name))
attrs = CITypeAttributesCache.get2(ci_type_name)
@@ -316,7 +315,7 @@ class CIManager(object):
ci_attr2type_attr = {type_attr.attr_id: type_attr for type_attr, _ in attrs}
ci = None
need_lock = g.user.username not in ("worker", "cmdb_agent", "agent")
need_lock = current_user.username not in current_app.config.get('PRIVILEGED_USERS', PRIVILEGED_USERS)
with Lock(ci_type_name, need_lock=need_lock):
existed = cls.ci_is_exist(unique_key, unique_value, ci_type.id)
if existed is not None:
@@ -330,10 +329,6 @@ class CIManager(object):
if exist_policy == ExistPolicy.NEED:
return abort(404, ErrFormat.ci_not_found.format("{}={}".format(unique_key.name, unique_value)))
from api.lib.cmdb.const import L_CI
if L_CI and len(CI.get_by(type_id=ci_type.id)) > L_CI * 2:
return abort(400, ErrFormat.limit_ci.format(L_CI * 2))
limit_attrs = cls._valid_ci_for_no_read(ci, ci_type) if not _is_admin else {}
if existed is None: # set default
@@ -364,13 +359,18 @@ class CIManager(object):
cls._valid_unique_constraint(ci_type.id, ci_dict, ci and ci.id)
ref_ci_dict = dict()
for k in ci_dict:
if k not in ci_type_attrs_name and k not in ci_type_attrs_alias and \
_no_attribute_policy == ExistPolicy.REJECT:
if k.startswith("$") and "." in k:
ref_ci_dict[k] = ci_dict[k]
continue
if k not in ci_type_attrs_name and (
k not in ci_type_attrs_alias and _no_attribute_policy == ExistPolicy.REJECT):
return abort(400, ErrFormat.attribute_not_found.format(k))
if limit_attrs and ci_type_attrs_name.get(k) not in limit_attrs and \
ci_type_attrs_alias.get(k) not in limit_attrs:
if limit_attrs and ci_type_attrs_name.get(k) not in limit_attrs and (
ci_type_attrs_alias.get(k) not in limit_attrs):
return abort(403, ErrFormat.ci_filter_perm_attr_no_permission.format(k))
ci_dict = {k: v for k, v in ci_dict.items() if k in ci_type_attrs_name or k in ci_type_attrs_alias}
@@ -389,6 +389,9 @@ class CIManager(object):
if record_id: # has change
ci_cache.apply_async([ci.id], queue=CMDB_QUEUE)
if ref_ci_dict: # add relations
ci_relation_add.apply_async(args=(ref_ci_dict, ci.id, current_user.uid), queue=CMDB_QUEUE)
return ci.id
def update(self, ci_id, _is_admin=False, **ci_dict):
@@ -411,7 +414,7 @@ class CIManager(object):
limit_attrs = self._valid_ci_for_no_read(ci) if not _is_admin else {}
need_lock = g.user.username not in ("worker", "cmdb_agent", "agent")
need_lock = current_user.username not in current_app.config.get('PRIVILEGED_USERS', PRIVILEGED_USERS)
with Lock(ci.ci_type.name, need_lock=need_lock):
self._valid_unique_constraint(ci.type_id, ci_dict, ci_id)
@@ -431,6 +434,10 @@ class CIManager(object):
if record_id: # has change
ci_cache.apply_async([ci_id], queue=CMDB_QUEUE)
ref_ci_dict = {k: v for k, v in ci_dict.items() if k.startswith("$") and "." in k}
if ref_ci_dict:
ci_relation_add.apply_async(args=(ref_ci_dict, ci.id), queue=CMDB_QUEUE)
@staticmethod
def update_unique_value(ci_id, unique_name, unique_value):
ci = CI.get_by_id(ci_id) or abort(404, ErrFormat.ci_not_found.format("id={}".format(ci_id)))
@@ -453,17 +460,22 @@ class CIManager(object):
for attr_name in attr_names:
value_table = TableMap(attr_name=attr_name).table
for item in value_table.get_by(ci_id=ci_id, to_dict=False):
item.delete()
item.delete(commit=False)
for item in CIRelation.get_by(first_ci_id=ci_id, to_dict=False):
ci_relation_delete.apply_async(args=(item.first_ci_id, item.second_ci_id), queue=CMDB_QUEUE)
item.delete()
item.delete(commit=False)
for item in CIRelation.get_by(second_ci_id=ci_id, to_dict=False):
ci_relation_delete.apply_async(args=(item.first_ci_id, item.second_ci_id), queue=CMDB_QUEUE)
item.delete()
item.delete(commit=False)
ci.delete() # TODO: soft delete
ad_ci = AutoDiscoveryCI.get_by(ci_id=ci_id, to_dict=False, first=True)
ad_ci and ad_ci.update(is_accept=False, accept_by=None, accept_time=None, filter_none=False, commit=False)
ci.delete(commit=False) # TODO: soft delete
db.session.commit()
AttributeHistoryManger.add(None, ci_id, [(None, OperateType.DELETE, ci_dict, None)], ci.type_id)
@@ -478,11 +490,8 @@ class CIManager(object):
unique_key = AttributeCache.get(ci_type.unique_id)
value_table = TableMap(attr=unique_key).table
v = value_table.get_by(attr_id=unique_key.id,
value=unique_value,
to_dict=False,
first=True) \
or abort(404, ErrFormat.not_found)
v = (value_table.get_by(attr_id=unique_key.id, value=unique_value, to_dict=False, first=True) or
abort(404, ErrFormat.not_found))
ci = CI.get_by_id(v.ci_id) or abort(404, ErrFormat.ci_not_found.format("id={}".format(v.ci_id)))
@@ -528,6 +537,7 @@ class CIManager(object):
result = [(i.get("hostname"), i.get("private_ip")[0], i.get("ci_type"),
heartbeat_dict.get(i.get("_id"))) for i in res
if i.get("private_ip")]
return numfound, result
@staticmethod
@@ -647,6 +657,7 @@ class CIManager(object):
return res
current_app.logger.warning("cache not hit...............")
return cls._get_cis_from_db(ci_ids, ret_key, fields, value_tables, excludes=excludes)
@@ -672,6 +683,7 @@ class CIRelationManager(object):
ci_type = CITypeCache.get(type_id)
children = CIManager.get_cis_by_ids(list(map(str, ci_type2ci_ids[type_id])), ret_key=ret_key)
res[ci_type.name] = children
return res
@staticmethod
@@ -743,17 +755,28 @@ class CIRelationManager(object):
return ci_ids
@staticmethod
def _check_constraint(first_ci_id, second_ci_id, type_relation):
def _check_constraint(first_ci_id, first_type_id, second_ci_id, second_type_id, type_relation):
db.session.remove()
if type_relation.constraint == ConstraintEnum.Many2Many:
return
first_existed = CIRelation.get_by(first_ci_id=first_ci_id, relation_type_id=type_relation.relation_type_id)
second_existed = CIRelation.get_by(second_ci_id=second_ci_id, relation_type_id=type_relation.relation_type_id)
if type_relation.constraint == ConstraintEnum.One2One and (first_existed or second_existed):
return abort(400, ErrFormat.relation_constraint.format("1对1"))
first_existed = CIRelation.get_by(first_ci_id=first_ci_id,
relation_type_id=type_relation.relation_type_id, to_dict=False)
second_existed = CIRelation.get_by(second_ci_id=second_ci_id,
relation_type_id=type_relation.relation_type_id, to_dict=False)
if type_relation.constraint == ConstraintEnum.One2One:
for i in first_existed:
if i.second_ci.type_id == second_type_id:
return abort(400, ErrFormat.relation_constraint.format("1-1"))
if type_relation.constraint == ConstraintEnum.One2Many and second_existed:
return abort(400, ErrFormat.relation_constraint.format("1对多"))
for i in second_existed:
if i.first_ci.type_id == first_type_id:
return abort(400, ErrFormat.relation_constraint.format("1-1"))
if type_relation.constraint == ConstraintEnum.One2Many:
for i in second_existed:
if i.first_ci.type_id == first_type_id:
return abort(400, ErrFormat.relation_constraint.format("1-N"))
@classmethod
def add(cls, first_ci_id, second_ci_id, more=None, relation_type_id=None):
@@ -792,15 +815,17 @@ class CIRelationManager(object):
else:
type_relation = CITypeRelation.get_by_id(relation_type_id)
cls._check_constraint(first_ci_id, second_ci_id, type_relation)
with Lock("ci_relation_add_{}_{}".format(first_ci.type_id, second_ci.type_id), need_lock=True):
existed = CIRelation.create(first_ci_id=first_ci_id,
second_ci_id=second_ci_id,
relation_type_id=relation_type_id)
cls._check_constraint(first_ci_id, first_ci.type_id, second_ci_id, second_ci.type_id, type_relation)
CIRelationHistoryManager().add(existed, OperateType.ADD)
existed = CIRelation.create(first_ci_id=first_ci_id,
second_ci_id=second_ci_id,
relation_type_id=relation_type_id)
ci_relation_cache.apply_async(args=(first_ci_id, second_ci_id), queue=CMDB_QUEUE)
CIRelationHistoryManager().add(existed, OperateType.ADD)
ci_relation_cache.apply_async(args=(first_ci_id, second_ci_id), queue=CMDB_QUEUE)
if more is not None:
existed.upadte(more=more)
@@ -848,12 +873,12 @@ class CIRelationManager(object):
:param children:
:return:
"""
if parents is not None and isinstance(parents, list):
if isinstance(parents, list):
for parent_id in parents:
for ci_id in ci_ids:
cls.add(parent_id, ci_id)
if children is not None and isinstance(children, list):
if isinstance(children, list):
for child_id in children:
for ci_id in ci_ids:
cls.add(ci_id, child_id)
@@ -867,7 +892,7 @@ class CIRelationManager(object):
:return:
"""
if parents is not None and isinstance(parents, list):
if isinstance(parents, list):
for parent_id in parents:
for ci_id in ci_ids:
cls.delete_2(parent_id, ci_id)

View File

@@ -5,7 +5,7 @@ import datetime
from flask import abort
from flask import current_app
from flask import g
from flask_login import current_user
from api.extensions import db
from api.lib.cmdb.attribute import AttributeManager
@@ -16,7 +16,9 @@ from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.const import CITypeOperateType
from api.lib.cmdb.const import CMDB_QUEUE
from api.lib.cmdb.const import ConstraintEnum
from api.lib.cmdb.const import PermEnum, ResourceTypeEnum, RoleEnum
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.const import RoleEnum
from api.lib.cmdb.const import ValueTypeEnum
from api.lib.cmdb.history import CITypeHistoryManager
from api.lib.cmdb.relation_type import RelationTypeManager
@@ -27,7 +29,10 @@ from api.lib.decorator import kwargs_required
from api.lib.perm.acl.acl import ACLManager
from api.lib.perm.acl.acl import is_app_admin
from api.models.cmdb import Attribute
from api.models.cmdb import AutoDiscoveryCI
from api.models.cmdb import AutoDiscoveryCIType
from api.models.cmdb import CI
from api.models.cmdb import CIFilterPerms
from api.models.cmdb import CIType
from api.models.cmdb import CITypeAttribute
from api.models.cmdb import CITypeAttributeGroup
@@ -37,6 +42,9 @@ from api.models.cmdb import CITypeGroupItem
from api.models.cmdb import CITypeRelation
from api.models.cmdb import CITypeTrigger
from api.models.cmdb import CITypeUniqueConstraint
from api.models.cmdb import CustomDashboard
from api.models.cmdb import PreferenceRelationView
from api.models.cmdb import PreferenceSearchOption
from api.models.cmdb import PreferenceShowAttributes
from api.models.cmdb import PreferenceTreeView
from api.models.cmdb import RelationType
@@ -54,6 +62,7 @@ class CITypeManager(object):
@staticmethod
def get_name_by_id(type_id):
ci_type = CITypeCache.get(type_id)
return ci_type and ci_type.name
@staticmethod
@@ -65,7 +74,7 @@ class CITypeManager(object):
@staticmethod
def get_ci_types(type_name=None):
resources = None
if current_app.config.get('USE_ACL') and not is_app_admin():
if current_app.config.get('USE_ACL') and not is_app_admin('cmdb'):
resources = set([i.get('name') for i in ACLManager().get_resources("CIType")])
ci_types = CIType.get_by() if type_name is None else CIType.get_by_like(name=type_name)
@@ -104,9 +113,6 @@ class CITypeManager(object):
@classmethod
@kwargs_required("name")
def add(cls, **kwargs):
from api.lib.cmdb.const import L_TYPE
if L_TYPE and len(CIType.get_by()) > L_TYPE * 2:
return abort(400, ErrFormat.limit_ci_type.format(L_TYPE * 2))
unique_key = kwargs.pop("unique_key", None)
unique_key = AttributeCache.get(unique_key) or abort(404, ErrFormat.unique_key_not_define)
@@ -117,7 +123,7 @@ class CITypeManager(object):
cls._validate_unique(alias=kwargs['alias'])
kwargs["unique_id"] = unique_key.id
kwargs['uid'] = g.user.uid
kwargs['uid'] = current_user.uid
ci_type = CIType.create(**kwargs)
CITypeAttributeManager.add(ci_type.id, [unique_key.id], is_required=True)
@@ -131,7 +137,7 @@ class CITypeManager(object):
ResourceTypeEnum.CI,
permissions=[PermEnum.READ])
ACLManager().grant_resource_to_role(ci_type.name,
g.user.username,
current_user.username,
ResourceTypeEnum.CI)
CITypeHistoryManager.add(CITypeOperateType.ADD, ci_type.id, change=ci_type.to_dict())
@@ -178,32 +184,41 @@ class CITypeManager(object):
def set_enabled(cls, type_id, enabled=True):
ci_type = cls.check_is_existed(type_id)
ci_type.update(enabled=enabled)
return type_id
@classmethod
def delete(cls, type_id):
ci_type = cls.check_is_existed(type_id)
if ci_type.uid and ci_type.uid != g.user.uid:
if ci_type.uid and ci_type.uid != current_user.uid:
return abort(403, ErrFormat.only_owner_can_delete)
if CI.get_by(type_id=type_id, first=True, to_dict=False) is not None:
return abort(400, ErrFormat.ci_exists_and_cannot_delete_type)
relation_views = PreferenceRelationView.get_by(to_dict=False)
for rv in relation_views:
for item in (rv.cr_ids or []):
if item.get('parent_id') == type_id or item.get('child_id') == type_id:
return abort(400, ErrFormat.ci_relation_view_exists_and_cannot_delete_type.format(rv.name))
for item in CITypeRelation.get_by(parent_id=type_id, to_dict=False):
item.soft_delete()
item.soft_delete(commit=False)
for item in CITypeRelation.get_by(child_id=type_id, to_dict=False):
item.soft_delete()
item.soft_delete(commit=False)
for item in PreferenceTreeView.get_by(type_id=type_id, to_dict=False):
item.soft_delete()
for table in [PreferenceTreeView, PreferenceShowAttributes, PreferenceSearchOption, CustomDashboard,
CITypeGroupItem, CITypeAttributeGroup, CITypeAttribute, CITypeUniqueConstraint, CITypeTrigger,
AutoDiscoveryCIType, CIFilterPerms]:
for item in table.get_by(type_id=type_id, to_dict=False):
item.soft_delete(commit=False)
for item in PreferenceShowAttributes.get_by(type_id=type_id, to_dict=False):
item.soft_delete()
for item in AutoDiscoveryCI.get_by(type_id=type_id, to_dict=False):
item.delete(commit=False)
for item in CITypeGroupItem.get_by(type_id=type_id, to_dict=False):
item.soft_delete()
db.session.commit()
ci_type.soft_delete()
@@ -254,6 +269,7 @@ class CITypeGroupManager(object):
@staticmethod
def add(name):
CITypeGroup.get_by(name=name, first=True) and abort(400, ErrFormat.ci_type_group_exists.format(name))
return CITypeGroup.create(name=name)
@staticmethod
@@ -320,6 +336,17 @@ class CITypeAttributeManager(object):
def __init__(self):
pass
@staticmethod
def get_attr_name(ci_type_name, key):
ci_type = CITypeCache.get(ci_type_name)
if ci_type is None:
return
for i in CITypeAttributesCache.get(ci_type.id):
attr = AttributeCache.get(i.attr_id)
if attr and (attr.name == key or attr.alias == key):
return attr.name
@staticmethod
def get_attr_names_by_type_id(type_id):
return [AttributeCache.get(attr.attr_id).name for attr in CITypeAttributesCache.get(type_id)]
@@ -340,6 +367,7 @@ class CITypeAttributeManager(object):
attr_dict.pop('choice_web_hook', None)
result.append(attr_dict)
return result
@staticmethod
@@ -527,6 +555,7 @@ class CITypeRelationManager(object):
ci_type_dict["attributes"] = CITypeAttributeManager.get_attributes_by_type_id(ci_type_dict["id"])
ci_type_dict["relation_type"] = relation_inst.relation_type.name
ci_type_dict["constraint"] = relation_inst.constraint
return ci_type_dict
@classmethod
@@ -575,7 +604,7 @@ class CITypeRelationManager(object):
ResourceTypeEnum.CI_TYPE_RELATION,
permissions=[PermEnum.READ])
ACLManager().grant_resource_to_role(resource_name,
g.user.username,
current_user.username,
ResourceTypeEnum.CI_TYPE_RELATION)
CITypeHistoryManager.add(CITypeOperateType.ADD_RELATION, p.id,
@@ -585,8 +614,8 @@ class CITypeRelationManager(object):
@classmethod
def delete(cls, _id):
ctr = CITypeRelation.get_by_id(_id) or \
abort(404, ErrFormat.ci_type_relation_not_found.format("id={}".format(_id)))
ctr = (CITypeRelation.get_by_id(_id) or
abort(404, ErrFormat.ci_type_relation_not_found.format("id={}".format(_id))))
ctr.soft_delete()
CITypeHistoryManager.add(CITypeOperateType.DELETE_RELATION, ctr.parent_id,
@@ -640,6 +669,7 @@ class CITypeAttributeGroupManager(object):
:param name:
:param group_order: group order
:param attr_order:
:param is_update:
:return:
"""
existed = CITypeAttributeGroup.get_by(type_id=type_id, name=name, first=True, to_dict=False)
@@ -680,8 +710,8 @@ class CITypeAttributeGroupManager(object):
@staticmethod
def delete(group_id):
group = CITypeAttributeGroup.get_by_id(group_id) \
or abort(404, ErrFormat.ci_type_attribute_group_not_found.format("id={}".format(group_id)))
group = (CITypeAttributeGroup.get_by_id(group_id) or
abort(404, ErrFormat.ci_type_attribute_group_not_found.format("id={}".format(group_id))))
group.soft_delete()
items = CITypeAttributeGroupItem.get_by(group_id=group_id, to_dict=False)
@@ -809,7 +839,7 @@ class CITypeTemplateManager(object):
ResourceTypeEnum.CI,
permissions=[PermEnum.READ])
ACLManager().grant_resource_to_role(type_name,
g.user.username,
current_user.username,
ResourceTypeEnum.CI)
else:
@@ -947,11 +977,11 @@ class CITypeTemplateManager(object):
rule.pop("created_at", None)
rule.pop("updated_at", None)
rule['uid'] = g.user.uid
rule['uid'] = current_user.uid
try:
AutoDiscoveryCITypeCRUD.add(**rule)
except:
pass
except Exception as e:
current_app.logger.warning("import auto discovery rules failed: {}".format(e))
def import_template(self, tpt):
import time
@@ -1110,8 +1140,8 @@ class CITypeTriggerManager(object):
@staticmethod
def update(_id, notify):
existed = CITypeTrigger.get_by_id(_id) or \
abort(404, ErrFormat.ci_type_trigger_not_found.format("id={}".format(_id)))
existed = (CITypeTrigger.get_by_id(_id) or
abort(404, ErrFormat.ci_type_trigger_not_found.format("id={}".format(_id))))
existed2 = existed.to_dict()
new = existed.update(notify=notify)
@@ -1125,8 +1155,8 @@ class CITypeTriggerManager(object):
@staticmethod
def delete(_id):
existed = CITypeTrigger.get_by_id(_id) or \
abort(404, ErrFormat.ci_type_trigger_not_found.format("id={}".format(_id)))
existed = (CITypeTrigger.get_by_id(_id) or
abort(404, ErrFormat.ci_type_trigger_not_found.format("id={}".format(_id))))
existed.soft_delete()
@@ -1149,16 +1179,16 @@ class CITypeTriggerManager(object):
result = []
for v in values:
if isinstance(v.value, (datetime.date, datetime.datetime)) and \
(v.value - delta_time).strftime('%Y%m%d') == now.strftime("%Y%m%d"):
if (isinstance(v.value, (datetime.date, datetime.datetime)) and
(v.value - delta_time).strftime('%Y%m%d') == now.strftime("%Y%m%d")):
result.append(v)
return result
@staticmethod
def trigger_notify(trigger, ci):
if trigger.notify.get('notify_at') == datetime.datetime.now().strftime("%H:%M") or \
not trigger.notify.get('notify_at'):
if (trigger.notify.get('notify_at') == datetime.datetime.now().strftime("%H:%M") or
not trigger.notify.get('notify_at')):
from api.tasks.cmdb import trigger_notify
trigger_notify.apply_async(args=(trigger.notify, ci.ci_id), queue=CMDB_QUEUE)

View File

@@ -99,5 +99,7 @@ CMDB_QUEUE = "one_cmdb_async"
REDIS_PREFIX_CI = "ONE_CMDB"
REDIS_PREFIX_CI_RELATION = "CMDB_CI_RELATION"
BUILTIN_KEYWORDS = {'id', '_id', 'ci_id', 'type', '_type', 'ci_type'}
L_TYPE = None
L_CI = None

View File

@@ -4,7 +4,7 @@
import json
from flask import abort
from flask import g
from flask_login import current_user
from api.extensions import db
from api.lib.cmdb.cache import AttributeCache
@@ -176,8 +176,8 @@ class AttributeHistoryManger(object):
def get_record_detail(record_id):
from api.lib.cmdb.ci import CIManager
record = OperationRecord.get_by_id(record_id) or \
abort(404, ErrFormat.record_not_found.format("id={}".format(record_id)))
record = (OperationRecord.get_by_id(record_id) or
abort(404, ErrFormat.record_not_found.format("id={}".format(record_id))))
username = UserCache.get(record.uid).nickname or UserCache.get(record.uid).username
timestamp = record.created_at.strftime("%Y-%m-%d %H:%M:%S")
@@ -201,7 +201,7 @@ class AttributeHistoryManger(object):
@staticmethod
def add(record_id, ci_id, history_list, type_id=None, flush=False, commit=True):
if record_id is None:
record = OperationRecord.create(uid=g.user.uid, type_id=type_id)
record = OperationRecord.create(uid=current_user.uid, type_id=type_id)
record_id = record.id
for attr_id, operate_type, old, new in history_list or []:
@@ -220,7 +220,7 @@ class AttributeHistoryManger(object):
class CIRelationHistoryManager(object):
@staticmethod
def add(rel_obj, operate_type=OperateType.ADD):
record = OperationRecord.create(uid=g.user.uid)
record = OperationRecord.create(uid=current_user.uid)
CIRelationHistory.create(relation_id=rel_obj.id,
record_id=record.id,
@@ -279,7 +279,7 @@ class CITypeHistoryManager(object):
for _type_id in type_ids:
payload = dict(operate_type=operate_type,
type_id=_type_id,
uid=g.user.uid,
uid=current_user.uid,
attr_id=attr_id,
trigger_id=trigger_id,
unique_constraint_id=unique_constraint_id,

View File

@@ -4,8 +4,8 @@ import functools
from flask import abort
from flask import current_app
from flask import g
from flask import request
from flask_login import current_user
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.resp_format import ErrFormat
@@ -74,7 +74,7 @@ class CIFilterPermsCRUD(DBMixin):
@classmethod
def get_attr_filter(cls, type_id):
if is_app_admin('cmdb') or g.user.username in ('worker', 'cmdb_agent'):
if is_app_admin('cmdb') or current_user.username in ('worker', 'cmdb_agent'):
return []
res2 = ACLManager('cmdb').get_resources(ResourceTypeEnum.CI_FILTER)
@@ -160,7 +160,7 @@ def has_perm_for_ci(arg_name, resource_type, perm, callback=None, app=None):
resource = callback(resource)
if current_app.config.get("USE_ACL") and resource:
if g.user.username == "worker" or g.user.username == "cmdb_agent":
if current_user.username == "worker" or current_user.username == "cmdb_agent":
request.values['__is_admin'] = True
return func(*args, **kwargs)

View File

@@ -7,7 +7,7 @@ import six
import toposort
from flask import abort
from flask import current_app
from flask import g
from flask_login import current_user
from api.extensions import db
from api.lib.cmdb.attribute import AttributeManager
@@ -36,11 +36,13 @@ class PreferenceManager(object):
@staticmethod
def get_types(instance=False, tree=False):
types = db.session.query(PreferenceShowAttributes.type_id).filter(
PreferenceShowAttributes.uid == g.user.uid).filter(
PreferenceShowAttributes.deleted.is_(False)).group_by(PreferenceShowAttributes.type_id).all() \
if instance else []
tree_types = PreferenceTreeView.get_by(uid=g.user.uid, to_dict=False) if tree else []
type_ids = list(set([i.type_id for i in types + tree_types]))
PreferenceShowAttributes.uid == current_user.uid).filter(
PreferenceShowAttributes.deleted.is_(False)).group_by(
PreferenceShowAttributes.type_id).all() if instance else []
tree_types = PreferenceTreeView.get_by(uid=current_user.uid, to_dict=False) if tree else []
type_ids = set([i.type_id for i in types + tree_types])
return [CITypeCache.get(type_id).to_dict() for type_id in type_ids]
@staticmethod
@@ -62,7 +64,7 @@ class PreferenceManager(object):
PreferenceShowAttributes.deleted.is_(False)).group_by(
PreferenceShowAttributes.uid, PreferenceShowAttributes.type_id)
for i in types:
if i.uid == g.user.uid:
if i.uid == current_user.uid:
result['self']['instance'].append(i.type_id)
if str(i.created_at) > str(result['self']['type_id2subs_time'].get(i.type_id, "")):
result['self']['type_id2subs_time'][i.type_id] = i.created_at
@@ -72,7 +74,7 @@ class PreferenceManager(object):
if tree:
types = PreferenceTreeView.get_by(to_dict=False)
for i in types:
if i.uid == g.user.uid:
if i.uid == current_user.uid:
result['self']['tree'].append(i.type_id)
if str(i.created_at) > str(result['self']['type_id2subs_time'].get(i.type_id, "")):
result['self']['type_id2subs_time'][i.type_id] = i.created_at
@@ -91,7 +93,7 @@ class PreferenceManager(object):
attrs = db.session.query(PreferenceShowAttributes, CITypeAttribute.order).join(
CITypeAttribute, CITypeAttribute.attr_id == PreferenceShowAttributes.attr_id).filter(
PreferenceShowAttributes.uid == g.user.uid).filter(
PreferenceShowAttributes.uid == current_user.uid).filter(
PreferenceShowAttributes.type_id == type_id).filter(
PreferenceShowAttributes.deleted.is_(False)).filter(CITypeAttribute.deleted.is_(False)).filter(
CITypeAttribute.type_id == type_id).all()
@@ -120,7 +122,7 @@ class PreferenceManager(object):
@classmethod
def create_or_update_show_attributes(cls, type_id, attr_order):
existed_all = PreferenceShowAttributes.get_by(type_id=type_id, uid=g.user.uid, to_dict=False)
existed_all = PreferenceShowAttributes.get_by(type_id=type_id, uid=current_user.uid, to_dict=False)
for x, order in attr_order:
if isinstance(x, list):
_attr, is_fixed = x
@@ -128,13 +130,13 @@ class PreferenceManager(object):
_attr, is_fixed = x, False
attr = AttributeCache.get(_attr) or abort(404, ErrFormat.attribute_not_found.format("id={}".format(_attr)))
existed = PreferenceShowAttributes.get_by(type_id=type_id,
uid=g.user.uid,
uid=current_user.uid,
attr_id=attr.id,
first=True,
to_dict=False)
if existed is None:
PreferenceShowAttributes.create(type_id=type_id,
uid=g.user.uid,
uid=current_user.uid,
attr_id=attr.id,
order=order,
is_fixed=is_fixed)
@@ -148,7 +150,7 @@ class PreferenceManager(object):
@staticmethod
def get_tree_view():
res = PreferenceTreeView.get_by(uid=g.user.uid, to_dict=True)
res = PreferenceTreeView.get_by(uid=current_user.uid, to_dict=True)
for item in res:
if item["levels"]:
ci_type = CITypeCache.get(item['type_id']).to_dict()
@@ -176,14 +178,14 @@ class PreferenceManager(object):
if i == attr.id or i == attr.name or i == attr.alias:
levels[idx] = attr.id
existed = PreferenceTreeView.get_by(uid=g.user.uid, type_id=type_id, to_dict=False, first=True)
existed = PreferenceTreeView.get_by(uid=current_user.uid, type_id=type_id, to_dict=False, first=True)
if existed is not None:
if not levels:
existed.soft_delete()
return existed
return existed.update(levels=levels)
elif levels:
return PreferenceTreeView.create(levels=levels, type_id=type_id, uid=g.user.uid)
return PreferenceTreeView.create(levels=levels, type_id=type_id, uid=current_user.uid)
@staticmethod
def get_relation_view():
@@ -254,7 +256,7 @@ class PreferenceManager(object):
existed = PreferenceRelationView.get_by(name=name, to_dict=False, first=True)
current_app.logger.debug(existed)
if existed is None:
PreferenceRelationView.create(name=name, cr_ids=cr_ids, uid=g.user.uid, is_public=is_public)
PreferenceRelationView.create(name=name, cr_ids=cr_ids, uid=current_user.uid, is_public=is_public)
if current_app.config.get("USE_ACL"):
ACLManager().add_resource(name, ResourceTypeEnum.RELATION_VIEW)
@@ -278,7 +280,7 @@ class PreferenceManager(object):
@staticmethod
def get_search_option(**kwargs):
query = PreferenceSearchOption.get_by(only_query=True)
query = query.filter(PreferenceSearchOption.uid == g.user.uid)
query = query.filter(PreferenceSearchOption.uid == current_user.uid)
for k in kwargs:
if hasattr(PreferenceSearchOption, k) and kwargs[k]:
@@ -288,9 +290,9 @@ class PreferenceManager(object):
@staticmethod
def add_search_option(**kwargs):
kwargs['uid'] = g.user.uid
kwargs['uid'] = current_user.uid
existed = PreferenceSearchOption.get_by(uid=g.user.uid,
existed = PreferenceSearchOption.get_by(uid=current_user.uid,
name=kwargs.get('name'),
prv_id=kwargs.get('prv_id'),
ptv_id=kwargs.get('ptv_id'),
@@ -306,10 +308,10 @@ class PreferenceManager(object):
existed = PreferenceSearchOption.get_by_id(_id) or abort(404, ErrFormat.preference_search_option_not_found)
if g.user.uid != existed.uid:
if current_user.uid != existed.uid:
return abort(400, ErrFormat.no_permission2)
other = PreferenceSearchOption.get_by(uid=g.user.uid,
other = PreferenceSearchOption.get_by(uid=current_user.uid,
name=kwargs.get('name'),
prv_id=kwargs.get('prv_id'),
ptv_id=kwargs.get('ptv_id'),
@@ -324,7 +326,7 @@ class PreferenceManager(object):
def delete_search_option(_id):
existed = PreferenceSearchOption.get_by_id(_id) or abort(404, ErrFormat.preference_search_option_not_found)
if g.user.uid != existed.uid:
if current_user.uid != existed.uid:
return abort(400, ErrFormat.no_permission2)
existed.soft_delete()

View File

@@ -24,21 +24,21 @@ class RelationTypeManager(object):
@staticmethod
def add(name):
RelationType.get_by(name=name, first=True, to_dict=False) and \
abort(400, ErrFormat.relation_type_exists.format(name))
RelationType.get_by(name=name, first=True, to_dict=False) and abort(
400, ErrFormat.relation_type_exists.format(name))
return RelationType.create(name=name)
@staticmethod
def update(rel_id, name):
existed = RelationType.get_by_id(rel_id) or \
abort(404, ErrFormat.relation_type_not_found.format("id={}".format(rel_id)))
existed = RelationType.get_by_id(rel_id) or abort(
404, ErrFormat.relation_type_not_found.format("id={}".format(rel_id)))
return existed.update(name=name)
@staticmethod
def delete(rel_id):
existed = RelationType.get_by_id(rel_id) or \
abort(404, ErrFormat.relation_type_not_found.format("id={}".format(rel_id)))
existed = RelationType.get_by_id(rel_id) or abort(
404, ErrFormat.relation_type_not_found.format("id={}".format(rel_id)))
existed.soft_delete()

View File

@@ -11,6 +11,7 @@ class ErrFormat(CommonErrFormat):
attribute_not_found = "属性 {} 不存在!"
attribute_is_unique_id = "该属性是模型的唯一标识,不能被删除!"
attribute_is_ref_by_type = "该属性被模型 {} 引用, 不能删除!"
attribute_value_type_cannot_change = "属性的值类型不允许修改!"
attribute_list_value_cannot_change = "多值不被允许修改!"
attribute_index_cannot_change = "修改索引 非管理员不被允许!"
@@ -20,7 +21,7 @@ class ErrFormat(CommonErrFormat):
add_attribute_failed = "创建属性 {} 失败!"
update_attribute_failed = "修改属性 {} 失败!"
cannot_edit_attribute = "您没有权限修改该属性!"
cannot_delete_attribute = "您没有权限删除属性!"
cannot_delete_attribute = "目前只允许 属性创建人、管理员 删除属性!"
attribute_name_cannot_be_builtin = "属性字段名不能是内置字段: id, _id, ci_id, type, _type, ci_type"
ci_not_found = "CI {} 不存在"
@@ -37,6 +38,7 @@ class ErrFormat(CommonErrFormat):
unique_key_not_define = "主键未定义或者已被删除"
only_owner_can_delete = "只有创建人才能删除它!"
ci_exists_and_cannot_delete_type = "因为CI已经存在不能删除模型"
ci_relation_view_exists_and_cannot_delete_type = "因为关系视图 {} 引用了该模型,不能删除模型"
ci_type_group_not_found = "模型分组 {} 不存在"
ci_type_group_exists = "模型分组 {} 已经存在"
ci_type_relation_not_found = "模型关系 {} 不存在"

View File

@@ -7,8 +7,9 @@ import copy
import time
from flask import current_app
from flask import g
from flask_login import current_user
from jinja2 import Template
from api.extensions import db
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import CITypeCache
@@ -105,7 +106,7 @@ class Search(object):
ci_filter = self.type2filter_perms[ci_type.id].get('ci_filter')
if ci_filter:
sub = []
ci_filter = Template(ci_filter).render(user=g.user)
ci_filter = Template(ci_filter).render(user=current_user)
for i in ci_filter.split(','):
if i.startswith("~") and not sub:
queries.append(i)
@@ -244,10 +245,8 @@ class Search(object):
new_table = _v_query_sql
if self.only_type_query or not self.type_id_list:
return "SELECT SQL_CALC_FOUND_ROWS DISTINCT C.ci_id " \
"FROM ({0}) AS C " \
"ORDER BY C.value {2} " \
"LIMIT {1:d}, {3};".format(new_table, (self.page - 1) * self.count, sort_type, self.count)
return ("SELECT SQL_CALC_FOUND_ROWS DISTINCT C.ci_id FROM ({0}) AS C ORDER BY C.value {2} "
"LIMIT {1:d}, {3};".format(new_table, (self.page - 1) * self.count, sort_type, self.count))
elif self.type_id_list:
self.query_sql = """SELECT C.ci_id
@@ -355,7 +354,7 @@ class Search(object):
else:
result.append(q)
_is_app_admin = is_app_admin('cmdb') or g.user.username == "worker"
_is_app_admin = is_app_admin('cmdb') or current_user.username == "worker"
if result and not has_type and not _is_app_admin:
type_q = self.__get_types_has_read()
if id_query:

View File

@@ -297,8 +297,8 @@ class Search(object):
if not attr:
raise SearchError(ErrFormat.attribute_not_found.format(field))
sort_by = "{0}.keyword".format(field) \
if attr.value_type not in (ValueTypeEnum.INT, ValueTypeEnum.FLOAT) else field
sort_by = ("{0}.keyword".format(field)
if attr.value_type not in (ValueTypeEnum.INT, ValueTypeEnum.FLOAT) else field)
sorts.append({sort_by: {"order": sort_type}})
self.query.update(dict(sort=sorts))

View File

@@ -4,14 +4,16 @@ from __future__ import unicode_literals
import datetime
import json
import re
import six
from markupsafe import escape
import api.models.cmdb as model
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.const import ValueTypeEnum
TIME_RE = re.compile(r"^(20|21|22|23|[0-1]\d):[0-5]\d:[0-5]\d$")
def string2int(x):
return int(float(x))
@@ -30,8 +32,8 @@ class ValueTypeMap(object):
deserialize = {
ValueTypeEnum.INT: string2int,
ValueTypeEnum.FLOAT: float,
ValueTypeEnum.TEXT: lambda x: escape(x).encode('utf-8').decode('utf-8'),
ValueTypeEnum.TIME: lambda x: escape(x).encode('utf-8').decode('utf-8'),
ValueTypeEnum.TEXT: lambda x: x,
ValueTypeEnum.TIME: lambda x: TIME_RE.findall(x)[0],
ValueTypeEnum.DATETIME: str2datetime,
ValueTypeEnum.DATE: str2datetime,
ValueTypeEnum.JSON: lambda x: json.loads(x) if isinstance(x, six.string_types) and x else x,
@@ -61,15 +63,11 @@ class ValueTypeMap(object):
ValueTypeEnum.INT: model.IntegerChoice,
ValueTypeEnum.FLOAT: model.FloatChoice,
ValueTypeEnum.TEXT: model.TextChoice,
ValueTypeEnum.TIME: model.TextChoice,
}
table = {
ValueTypeEnum.INT: model.CIValueInteger,
ValueTypeEnum.TEXT: model.CIValueText,
ValueTypeEnum.DATETIME: model.CIValueDateTime,
ValueTypeEnum.DATE: model.CIValueDateTime,
ValueTypeEnum.TIME: model.CIValueText,
ValueTypeEnum.FLOAT: model.CIValueFloat,
ValueTypeEnum.JSON: model.CIValueJson,
'index_{0}'.format(ValueTypeEnum.INT): model.CIIndexValueInteger,
'index_{0}'.format(ValueTypeEnum.TEXT): model.CIIndexValueText,
@@ -81,12 +79,7 @@ class ValueTypeMap(object):
}
table_name = {
ValueTypeEnum.INT: 'c_value_integers',
ValueTypeEnum.TEXT: 'c_value_texts',
ValueTypeEnum.DATETIME: 'c_value_datetime',
ValueTypeEnum.DATE: 'c_value_datetime',
ValueTypeEnum.TIME: 'c_value_texts',
ValueTypeEnum.FLOAT: 'c_value_floats',
ValueTypeEnum.JSON: 'c_value_json',
'index_{0}'.format(ValueTypeEnum.INT): 'c_value_index_integers',
'index_{0}'.format(ValueTypeEnum.TEXT): 'c_value_index_texts',
@@ -117,8 +110,11 @@ class TableMap(object):
@property
def table(self):
attr = AttributeCache.get(self.attr_name) if not self.attr else self.attr
if self.is_index is None:
if attr.value_type != ValueTypeEnum.TEXT and attr.value_type != ValueTypeEnum.JSON:
self.is_index = True
elif self.is_index is None:
self.is_index = attr.is_index
i = "index_{0}".format(attr.value_type) if self.is_index else attr.value_type
return ValueTypeMap.table.get(i)
@@ -126,8 +122,11 @@ class TableMap(object):
@property
def table_name(self):
attr = AttributeCache.get(self.attr_name) if not self.attr else self.attr
if self.is_index is None:
if attr.value_type != ValueTypeEnum.TEXT and attr.value_type != ValueTypeEnum.JSON:
self.is_index = True
elif self.is_index is None:
self.is_index = attr.is_index
i = "index_{0}".format(attr.value_type) if self.is_index else attr.value_type
return ValueTypeMap.table_name.get(i)

View File

@@ -80,9 +80,10 @@ class AttributeValueManager(object):
return res
@staticmethod
def __deserialize_value(value_type, value):
def _deserialize_value(value_type, value):
if not value:
return value
deserialize = ValueTypeMap.deserialize[value_type]
try:
v = deserialize(value)
@@ -91,13 +92,13 @@ class AttributeValueManager(object):
return abort(400, ErrFormat.attribute_value_invalid.format(value))
@staticmethod
def __check_is_choice(attr, value_type, value):
def _check_is_choice(attr, value_type, value):
choice_values = AttributeManager.get_choice_values(attr.id, value_type, attr.choice_web_hook)
if str(value) not in list(map(str, [i[0] for i in choice_values])):
return abort(400, ErrFormat.not_in_choice_values.format(value))
@staticmethod
def __check_is_unique(value_table, attr, ci_id, type_id, value):
def _check_is_unique(value_table, attr, ci_id, type_id, value):
existed = db.session.query(value_table.attr_id).join(CI, CI.id == value_table.ci_id).filter(
CI.type_id == type_id).filter(
value_table.attr_id == attr.id).filter(value_table.deleted.is_(False)).filter(
@@ -106,20 +107,20 @@ class AttributeValueManager(object):
existed and abort(400, ErrFormat.attribute_value_unique_required.format(attr.alias, value))
@staticmethod
def __check_is_required(type_id, attr, value, type_attr=None):
def _check_is_required(type_id, attr, value, type_attr=None):
type_attr = type_attr or CITypeAttributeCache.get(type_id, attr.id)
if type_attr and type_attr.is_required and not value and value != 0:
return abort(400, ErrFormat.attribute_value_required.format(attr.alias))
def _validate(self, attr, value, value_table, ci=None, type_id=None, ci_id=None, type_attr=None):
ci = ci or {}
v = self.__deserialize_value(attr.value_type, value)
v = self._deserialize_value(attr.value_type, value)
attr.is_choice and value and self.__check_is_choice(attr, attr.value_type, v)
attr.is_unique and self.__check_is_unique(
attr.is_choice and value and self._check_is_choice(attr, attr.value_type, v)
attr.is_unique and self._check_is_unique(
value_table, attr, ci and ci.id or ci_id, ci and ci.type_id or type_id, v)
self.__check_is_required(ci and ci.type_id or type_id, attr, v, type_attr=type_attr)
self._check_is_required(ci and ci.type_id or type_id, attr, v, type_attr=type_attr)
if v == "" and attr.value_type not in (ValueTypeEnum.TEXT,):
v = None
@@ -144,7 +145,7 @@ class AttributeValueManager(object):
return record_id
@staticmethod
def __compute_attr_value_from_expr(expr, ci_dict):
def _compute_attr_value_from_expr(expr, ci_dict):
t = jinja2.Template(expr).render(ci_dict)
try:
@@ -154,7 +155,7 @@ class AttributeValueManager(object):
return t
@staticmethod
def __compute_attr_value_from_script(script, ci_dict):
def _compute_attr_value_from_script(script, ci_dict):
script = jinja2.Template(script).render(ci_dict)
script_f = tempfile.NamedTemporaryFile(delete=False, suffix=".py")
@@ -183,22 +184,22 @@ class AttributeValueManager(object):
return [var for var in schema.get("properties")]
def _compute_attr_value(self, attr, payload, ci):
attrs = self._jinja2_parse(attr['compute_expr']) if attr.get('compute_expr') else \
self._jinja2_parse(attr['compute_script'])
def _compute_attr_value(self, attr, payload, ci_id):
attrs = (self._jinja2_parse(attr['compute_expr']) if attr.get('compute_expr')
else self._jinja2_parse(attr['compute_script']))
not_existed = [i for i in attrs if i not in payload]
if ci is not None:
payload.update(self.get_attr_values(not_existed, ci.id))
if ci_id is not None:
payload.update(self.get_attr_values(not_existed, ci_id))
if attr['compute_expr']:
return self.__compute_attr_value_from_expr(attr['compute_expr'], payload)
return self._compute_attr_value_from_expr(attr['compute_expr'], payload)
elif attr['compute_script']:
return self.__compute_attr_value_from_script(attr['compute_script'], payload)
return self._compute_attr_value_from_script(attr['compute_script'], payload)
def handle_ci_compute_attributes(self, ci_dict, computed_attrs, ci):
payload = copy.deepcopy(ci_dict)
for attr in computed_attrs:
computed_value = self._compute_attr_value(attr, payload, ci)
computed_value = self._compute_attr_value(attr, payload, ci and ci.id)
if computed_value is not None:
ci_dict[attr['name']] = computed_value
@@ -220,7 +221,7 @@ class AttributeValueManager(object):
for i in handle_arg_list(value)]
ci_dict[key] = value_list
if not value_list:
self.__check_is_required(type_id, attr, '')
self._check_is_required(type_id, attr, '')
else:
value = self._validate(attr, value, value_table, ci=None, type_id=type_id, ci_id=ci_id,
@@ -310,7 +311,7 @@ class AttributeValueManager(object):
if attr.is_list:
value_list = [self._validate(attr, i, value_table, ci) for i in handle_arg_list(value)]
if not value_list:
self.__check_is_required(ci.type_id, attr, '')
self._check_is_required(ci.type_id, attr, '')
existed_attrs = value_table.get_by(attr_id=attr.id, ci_id=ci.id, to_dict=False)
existed_values = [i.value for i in existed_attrs]

View File

@@ -0,0 +1,46 @@
from flask import abort
from api.extensions import db
from api.lib.common_setting.resp_format import ErrFormat
from api.models.common_setting import CommonData
class CommonDataCRUD(object):
@staticmethod
def get_data_by_type(data_type):
return CommonData.get_by(data_type=data_type)
@staticmethod
def get_data_by_id(_id, to_dict=True):
return CommonData.get_by(first=True, id=_id, to_dict=to_dict)
@staticmethod
def create_new_data(data_type, **kwargs):
try:
return CommonData.create(data_type=data_type, **kwargs)
except Exception as e:
db.session.rollback()
abort(400, str(e))
@staticmethod
def update_data(_id, **kwargs):
existed = CommonDataCRUD.get_data_by_id(_id, to_dict=False)
if not existed:
abort(404, ErrFormat.common_data_not_found.format(_id))
try:
return existed.update(**kwargs)
except Exception as e:
db.session.rollback()
abort(400, str(e))
@staticmethod
def delete(_id):
existed = CommonDataCRUD.get_data_by_id(_id, to_dict=False)
if not existed:
abort(404, ErrFormat.common_data_not_found.format(_id))
try:
existed.soft_delete()
except Exception as e:
db.session.rollback()
abort(400, str(e))

View File

@@ -4,11 +4,11 @@ COMMON_SETTING_QUEUE = "common_setting_async"
class OperatorType(BaseEnum):
EQUAL = 1 # 等于
NOT_EQUAL = 2 # 不等于
IN = 3 # 包含
NOT_IN = 4 # 不包含
GREATER_THAN = 5 # 大于
LESS_THAN = 6 # 小于
IS_EMPTY = 7 # 为空
IS_NOT_EMPTY = 8 # 不为空
EQUAL = 1
NOT_EQUAL = 2
IN = 3
NOT_IN = 4
GREATER_THAN = 5
LESS_THAN = 6
IS_EMPTY = 7
IS_NOT_EMPTY = 8

View File

@@ -7,41 +7,26 @@ from wtforms import IntegerField
from wtforms import StringField
from wtforms import validators
from api.extensions import db
from api.lib.common_setting.resp_format import ErrFormat
from api.lib.common_setting.utils import get_df_from_read_sql
from api.lib.perm.acl.role import RoleCRUD
from api.models.common_setting import Department, Employee
sub_departments_column_name = 'sub_departments'
def drop_ts_column(df):
columns = list(df.columns)
remove_columns = []
for column in ['created_at', 'updated_at', 'deleted_at', 'last_login']:
targets = list(filter(lambda c: c.startswith(column), columns))
if targets:
remove_columns.extend(targets)
remove_columns = list(set(remove_columns))
return df.drop(remove_columns, axis=1) if len(remove_columns) > 0 else df
def get_department_df():
def get_all_department_list(to_dict=True):
criterion = [
Department.deleted == 0,
]
query = Department.query.filter(
*criterion
)
df = get_df_from_read_sql(query)
if df.empty:
return
return drop_ts_column(df)
).order_by(Department.department_id.asc())
results = query.all()
return [r.to_dict() for r in results] if to_dict else results
def get_all_employee_df(block=0):
def get_all_employee_list(block=0, to_dict=True):
criterion = [
Employee.deleted == 0,
]
@@ -50,112 +35,106 @@ def get_all_employee_df(block=0):
Employee.block == block
)
entities = [getattr(Employee, c) for c in Employee.get_columns(
).keys() if c not in ['deleted', 'deleted_at']]
query = Employee.query.with_entities(
*entities
).filter(
*criterion
)
df = get_df_from_read_sql(query)
if df.empty:
return df
return drop_ts_column(df)
results = db.session.query(Employee).filter(*criterion).all()
DepartmentTreeEmployeeColumns = [
'acl_rid',
'employee_id',
'username',
'nickname',
'email',
'mobile',
'direct_supervisor_id',
'block',
'department_id',
]
def format_columns(e):
return {column: getattr(e, column) for column in DepartmentTreeEmployeeColumns}
return [format_columns(r) for r in results] if to_dict else results
class DepartmentTree(object):
def __init__(self, append_employee=False, block=-1):
self.append_employee = append_employee
self.block = block
self.d_df = get_department_df()
self.employee_df = get_all_employee_df(
self.all_department_list = get_all_department_list()
self.all_employee_list = get_all_employee_list(
block) if append_employee else None
def prepare(self):
pass
def get_employees_by_d_id(self, d_id):
_df = self.employee_df[
self.employee_df['department_id'].eq(d_id)
].sort_values(by=['direct_supervisor_id'], ascending=True)
if _df.empty:
block = self.block
def filter_department_id(e):
if self.block != -1:
return e['department_id'] == d_id and e['block'] == block
return e.department_id == d_id
results = list(filter(lambda e: filter_department_id(e), self.all_employee_list))
return results
def get_department_by_parent_id(self, parent_id):
results = list(filter(lambda d: d['department_parent_id'] == parent_id, self.all_department_list))
if not results:
return []
if self.block != -1:
_df = _df[
_df['block'].eq(self.block)
]
return _df.to_dict('records')
return results
def get_tree_departments(self):
# 一级部门
top_df = self.d_df[self.d_df['department_parent_id'].eq(-1)]
if top_df.empty:
top_departments = self.get_department_by_parent_id(-1)
if len(top_departments) == 0:
return []
d_list = []
for index in top_df.index:
top_d = top_df.loc[index].to_dict()
for top_d in top_departments:
department_id = top_d['department_id']
# 检查 department_id 是否作为其他部门的 parent
sub_df = self.d_df[
self.d_df['department_parent_id'].eq(department_id)
].sort_values(by=['sort_value'], ascending=True)
sub_deps = self.get_department_by_parent_id(department_id)
employees = []
if self.append_employee:
# 要包含员工
employees = self.get_employees_by_d_id(department_id)
top_d['employees'] = employees
if sub_df.empty:
if len(sub_deps) == 0:
top_d[sub_departments_column_name] = []
d_list.append(top_d)
continue
self.parse_sub_department(sub_df, top_d)
self.parse_sub_department(sub_deps, top_d)
d_list.append(top_d)
return d_list
def get_all_departments(self, is_tree=1):
if self.d_df.empty:
if len(self.all_department_list) == 0:
return []
if is_tree != 1:
return self.d_df.to_dict('records')
return self.all_department_list
return self.get_tree_departments()
def parse_sub_department(self, df, top_d):
def parse_sub_department(self, deps, top_d):
sub_departments = []
for s_index in df.index:
d = df.loc[s_index].to_dict()
sub_df = self.d_df[
self.d_df['department_parent_id'].eq(
df.at[s_index, 'department_id'])
].sort_values(by=['sort_value'], ascending=True)
for d in deps:
sub_deps = self.get_department_by_parent_id(d['department_id'])
employees = []
if self.append_employee:
# 要包含员工
employees = self.get_employees_by_d_id(
df.at[s_index, 'department_id'])
employees = self.get_employees_by_d_id(d['department_id'])
d['employees'] = employees
if sub_df.empty:
if len(sub_deps) == 0:
d[sub_departments_column_name] = []
sub_departments.append(d)
continue
self.parse_sub_department(sub_df, d)
self.parse_sub_department(sub_deps, d)
sub_departments.append(d)
top_d[sub_departments_column_name] = sub_departments
@@ -202,7 +181,6 @@ class DepartmentCRUD(object):
def check_department_parent_id_allow(d_id, department_parent_id):
if department_parent_id == 0:
return
# 检查 department_parent_id 是否在许可范围内
allow_p_d_id_list = DepartmentCRUD.get_allow_parent_d_id_by(d_id)
target = list(
filter(lambda d: d['department_id'] == department_parent_id, allow_p_d_id_list))
@@ -281,9 +259,6 @@ class DepartmentCRUD(object):
@staticmethod
def get_allow_parent_d_id_by(department_id):
"""
获取可以成为 department_id 的 department_parent_id 的 list
"""
tree_list = DepartmentCRUD.get_department_tree_list()
allow_d_id_list = []
@@ -321,58 +296,57 @@ class DepartmentCRUD(object):
@staticmethod
def get_department_tree_list():
df = get_department_df()
if df.empty:
all_deps = get_all_department_list()
if len(all_deps) == 0:
return []
# 一级部门
top_df = df[df['department_parent_id'].eq(-1)]
if top_df.empty:
top_deps = list(filter(lambda d: d['department_parent_id'] == -1, all_deps))
if len(top_deps) == 0:
return []
tree_list = []
for index in top_df.index:
for top_d in top_deps:
tree = Tree()
identifier_root = top_df.at[index, 'department_id']
identifier_root = top_d['department_id']
tree.create_node(
top_df.at[index, 'department_name'],
top_d['department_name'],
identifier_root
)
# 检查 department_id 是否作为其他部门的 parent
sub_df = df[
df['department_parent_id'].eq(identifier_root)
]
if sub_df.empty:
sub_ds = list(filter(lambda d: d['department_parent_id'] == identifier_root, all_deps))
if len(sub_ds) == 0:
tree_list.append(tree)
continue
DepartmentCRUD.parse_sub_department_node(
sub_df, df, tree, identifier_root)
sub_ds, all_deps, tree, identifier_root)
tree_list.append(tree)
return tree_list
@staticmethod
def parse_sub_department_node(df, all_df, tree, parent_id):
for s_index in df.index:
def parse_sub_department_node(sub_ds, all_ds, tree, parent_id):
for d in sub_ds:
tree.create_node(
df.at[s_index, 'department_name'],
df.at[s_index, 'department_id'],
d['department_name'],
d['department_id'],
parent=parent_id
)
sub_df = all_df[
all_df['department_parent_id'].eq(
df.at[s_index, 'department_id'])
]
if sub_df.empty:
next_sub_ds = list(filter(lambda item_d: item_d['department_parent_id'] == d['department_id'], all_ds))
if len(next_sub_ds) == 0:
continue
DepartmentCRUD.parse_sub_department_node(
sub_df, all_df, tree, df.at[s_index, 'department_id'])
next_sub_ds, all_ds, tree, d['department_id'])
@staticmethod
def get_department_by_query(query, to_dict=True):
results = query.all()
if not results:
return []
return results if not to_dict else [r.to_dict() for r in results]
@staticmethod
def get_departments_and_ids(department_parent_id, block):
@@ -380,44 +354,30 @@ class DepartmentCRUD(object):
Department.department_parent_id == department_parent_id,
Department.deleted == 0,
).order_by(Department.sort_value.asc())
df = get_df_from_read_sql(query)
if df.empty:
all_departments = DepartmentCRUD.get_department_by_query(query)
if len(all_departments) == 0:
return [], []
tree_list = DepartmentCRUD.get_department_tree_list()
employee_df = get_all_employee_df(block)
all_employee_list = get_all_employee_list(block)
department_id_list = list(df['department_id'].values)
department_id_list = [d['department_id'] for d in all_departments]
query = Department.query.filter(
Department.department_parent_id.in_(department_id_list),
Department.deleted == 0,
).order_by(Department.sort_value.asc()).group_by(Department.department_id)
sub_df = get_df_from_read_sql(query)
if sub_df.empty:
df['has_sub'] = 0
sub_deps = DepartmentCRUD.get_department_by_query(query)
def handle_row_employee_count(row):
return len(employee_df[employee_df['department_id'] == row['department_id']])
sub_map = {d['department_parent_id']: 1 for d in sub_deps}
df['employee_count'] = df.apply(
lambda row: handle_row_employee_count(row), axis=1)
for d in all_departments:
d['has_sub'] = sub_map.get(d['department_id'], 0)
else:
sub_map = {d['department_parent_id']: 1 for d in sub_df.to_dict('records')}
d_ids = DepartmentCRUD.get_department_id_list_by_root(d['department_id'], tree_list)
def handle_row(row):
d_ids = DepartmentCRUD.get_department_id_list_by_root(
row['department_id'], tree_list)
row['employee_count'] = len(
employee_df[employee_df['department_id'].isin(d_ids)])
d['employee_count'] = len(list(filter(lambda e: e['department_id'] in d_ids, all_employee_list)))
row['has_sub'] = sub_map.get(row['department_id'], 0)
return row
df = df.apply(lambda row: handle_row(row), axis=1)
return df.to_dict('records'), department_id_list
return all_departments, department_id_list
@staticmethod
def get_department_id_list_by_root(root_department_id, tree_list=None):

View File

@@ -3,7 +3,6 @@
import traceback
from datetime import datetime
import pandas as pd
from flask import abort
from flask_login import current_user
from sqlalchemy import or_, literal_column, func, not_, and_
@@ -17,9 +16,20 @@ from api.extensions import db
from api.lib.common_setting.acl import ACLManager
from api.lib.common_setting.const import COMMON_SETTING_QUEUE, OperatorType
from api.lib.common_setting.resp_format import ErrFormat
from api.lib.common_setting.utils import get_df_from_read_sql
from api.models.common_setting import Employee, Department
acl_user_columns = [
'email',
'mobile',
'nickname',
'username',
'password',
'block',
'avatar',
]
employee_pop_columns = ['password']
can_not_edit_columns = ['email']
def edit_acl_user(uid, **kwargs):
user_data = {column: kwargs.get(
@@ -70,9 +80,6 @@ class EmployeeCRUD(object):
@staticmethod
def get_employee_by_uid_with_create(_uid):
"""
根据 uid 获取员工信息,不存在则创建
"""
try:
return EmployeeCRUD.get_employee_by_uid(_uid).to_dict()
except Exception as e:
@@ -102,7 +109,6 @@ class EmployeeCRUD(object):
acl_uid=user_info['uid'],
)
return existed.to_dict()
# 创建员工
if not user_info.get('nickname', None):
user_info['nickname'] = user_info['name']
@@ -145,9 +151,6 @@ class EmployeeCRUD(object):
if len(e_list) > 0:
from api.tasks.common_setting import edit_employee_department_in_acl
# fixme: comment next line
# edit_employee_department_in_acl(e_list, new_department_id, current_user.uid)
edit_employee_department_in_acl.apply_async(
args=(e_list, new_department_id, current_user.uid),
queue=COMMON_SETTING_QUEUE
@@ -209,173 +212,6 @@ class EmployeeCRUD(object):
*criterion
).count()
@staticmethod
def import_employee(employee_list):
return CreateEmployee().batch_create(employee_list)
@staticmethod
def get_export_employee_df(block_status):
criterion = [
Employee.deleted == 0
]
if block_status >= 0:
criterion.append(
Employee.block == block_status
)
query = Employee.query.with_entities(
Employee.employee_id,
Employee.nickname,
Employee.email,
Employee.sex,
Employee.mobile,
Employee.position_name,
Employee.last_login,
Employee.department_id,
Employee.direct_supervisor_id,
).filter(*criterion)
df = get_df_from_read_sql(query)
if df.empty:
return df
query = Department.query.filter(
*criterion
)
department_df = get_df_from_read_sql(query)
def find_name(row):
department_id = row['department_id']
_df = department_df[department_df['department_id']
== department_id]
row['department_name'] = '' if _df.empty else _df.iloc[0]['department_name']
direct_supervisor_id = row['direct_supervisor_id']
_df = df[df['employee_id'] == direct_supervisor_id]
row['nickname_direct_supervisor'] = '' if _df.empty else _df.iloc[0]['nickname']
if isinstance(row['last_login'], pd.Timestamp):
try:
row['last_login'] = str(row['last_login'])
except:
row['last_login'] = ''
else:
row['last_login'] = ''
return row
df = df.apply(find_name, axis=1)
df.drop(['department_id', 'direct_supervisor_id',
'employee_id'], axis=1, inplace=True)
return df
@staticmethod
def batch_employee(column_name, column_value, employee_id_list):
if not column_value:
abort(400, ErrFormat.value_is_required)
if column_name in ['password', 'block']:
return EmployeeCRUD.batch_edit_password_or_block_column(column_name, employee_id_list, column_value, True)
elif column_name in ['department_id']:
return EmployeeCRUD.batch_edit_employee_department(employee_id_list, column_value)
elif column_name in [
'direct_supervisor_id', 'position_name'
]:
return EmployeeCRUD.batch_edit_column(column_name, employee_id_list, column_value, False)
else:
abort(400, ErrFormat.column_name_not_support)
@staticmethod
def batch_edit_employee_department(employee_id_list, column_value):
err_list = []
employee_list = []
for _id in employee_id_list:
try:
existed = EmployeeCRUD.get_employee_by_id(_id)
employee = dict(
e_acl_rid=existed.acl_rid,
department_id=existed.department_id
)
employee_list.append(employee)
existed.update(department_id=column_value)
except Exception as e:
err_list.append({
'employee_id': _id,
'err': str(e),
})
from api.tasks.common_setting import edit_employee_department_in_acl
edit_employee_department_in_acl.apply_async(
args=(employee_list, column_value, current_user.uid),
queue=COMMON_SETTING_QUEUE
)
return err_list
@staticmethod
def batch_edit_password_or_block_column(column_name, employee_id_list, column_value, is_acl=False):
if column_name == 'block':
err_list = []
success_list = []
for _id in employee_id_list:
try:
employee = EmployeeCRUD.edit_employee_block_column(
_id, is_acl, **{column_name: column_value})
success_list.append(employee)
except Exception as e:
err_list.append({
'employee_id': _id,
'err': str(e),
})
return err_list
else:
return EmployeeCRUD.batch_edit_column(column_name, employee_id_list, column_value, is_acl)
@staticmethod
def batch_edit_column(column_name, employee_id_list, column_value, is_acl=False):
err_list = []
for _id in employee_id_list:
try:
EmployeeCRUD.edit_employee_single_column(
_id, is_acl, **{column_name: column_value})
except Exception as e:
err_list.append({
'employee_id': _id,
'err': str(e),
})
return err_list
@staticmethod
def edit_employee_single_column(_id, is_acl=False, **kwargs):
existed = EmployeeCRUD.get_employee_by_id(_id)
if is_acl:
return edit_acl_user(existed.acl_uid, **kwargs)
try:
for column in employee_pop_columns:
if kwargs.get(column):
kwargs.pop(column)
return existed.update(**kwargs)
except Exception as e:
return abort(400, str(e))
@staticmethod
def edit_employee_block_column(_id, is_acl=False, **kwargs):
existed = EmployeeCRUD.get_employee_by_id(_id)
value = get_block_value(kwargs.get('block'))
if value is True:
# 判断该用户是否为 部门负责人,或者员工的直接上级
check_department_director_id_or_direct_supervisor_id(_id)
if is_acl:
kwargs['block'] = value
edit_acl_user(existed.acl_uid, **kwargs)
data = existed.to_dict()
return data
@staticmethod
def check_email_unique(email, _id=0):
criterion = [
@@ -395,7 +231,7 @@ class EmployeeCRUD(object):
raise Exception(err)
@staticmethod
def get_employee_list_by_body(department_id, block_status, search='', order='', conditions=[], page=1,
def get_employee_list_by_body(department_id, block_status, search='', order='', conditions=None, page=1,
page_size=10):
criterion = [
Employee.deleted == 0
@@ -461,7 +297,7 @@ class EmployeeCRUD(object):
@staticmethod
def get_expr_by_condition(column, operator, value, relation):
"""
根据conditions返回expr: (and_list, or_list)
get expr: (and_list, or_list)
"""
attr = EmployeeCRUD.get_attr_by_column(column)
# 根据operator生成条件表达式
@@ -481,7 +317,7 @@ class EmployeeCRUD(object):
if value:
abort(400, ErrFormat.query_column_none_keep_value_empty.format(column))
expr = [attr.is_(None)]
if column not in ["entry_date", "leave_date", "dfc_entry_date", "last_login"]:
if column not in ["last_login"]:
expr += [attr == '']
expr = [or_(*expr)]
elif operator == OperatorType.IS_NOT_EMPTY:
@@ -495,7 +331,6 @@ class EmployeeCRUD(object):
else:
abort(400, ErrFormat.not_support_operator.format(operator))
# 根据relation生成复合条件
if relation == "&":
return expr, []
elif relation == "|":
@@ -505,7 +340,6 @@ class EmployeeCRUD(object):
@staticmethod
def check_condition(column, operator, value, relation):
# 对于condition中column为空的报错
if column is None or operator is None or relation is None:
return abort(400, ErrFormat.conditions_field_missing)
@@ -654,19 +488,6 @@ def get_user_map(key='uid', acl=None):
return data
acl_user_columns = [
'email',
'mobile',
'nickname',
'username',
'password',
'block',
'avatar',
]
employee_pop_columns = ['password']
can_not_edit_columns = ['email']
def format_params(params):
for k in ['_key', '_secret']:
params.pop(k, None)
@@ -676,19 +497,22 @@ def format_params(params):
class CreateEmployee(object):
def __init__(self):
self.acl = ACLManager()
self.useremail_map = {}
self.all_acl_users = self.acl.get_all_users()
def check_acl_user(self, email):
user_info = self.useremail_map.get(email, None)
if user_info:
return user_info
return None
def check_acl_user(self, user_data):
target_email = list(filter(lambda x: x['email'] == user_data['email'], self.all_acl_users))
if target_email:
return target_email[0]
target_username = list(filter(lambda x: x['username'] == user_data['username'], self.all_acl_users))
if target_username:
return target_username[0]
def add_acl_user(self, **kwargs):
user_data = {column: kwargs.get(
column, '') for column in acl_user_columns if kwargs.get(column, '')}
try:
existed = self.check_acl_user(user_data['email'])
existed = self.check_acl_user(user_data)
if not existed:
return self.acl.create_user(user_data)
return existed
@@ -697,8 +521,6 @@ class CreateEmployee(object):
def create_single(self, **kwargs):
EmployeeCRUD.check_email_unique(kwargs['email'])
self.useremail_map = self.useremail_map if self.useremail_map else get_user_map(
'email', self.acl)
user = self.add_acl_user(**kwargs)
kwargs['acl_uid'] = user['uid']
kwargs['last_login'] = user['last_login']
@@ -711,8 +533,6 @@ class CreateEmployee(object):
)
def create_single_with_import(self, **kwargs):
self.useremail_map = self.useremail_map if self.useremail_map else get_user_map(
'email', self.acl)
user = self.add_acl_user(**kwargs)
kwargs['acl_uid'] = user['uid']
kwargs['last_login'] = user['last_login']
@@ -755,9 +575,6 @@ class CreateEmployee(object):
return end_d_id
def format_department_id(self, employee):
"""
部门名称转化为ID不存在则创建
"""
department_name_map = {}
try:
department_name = employee.get('department_name', '')
@@ -774,16 +591,13 @@ class CreateEmployee(object):
def batch_create(self, employee_list):
err_list = []
self.useremail_map = get_user_map('email', self.acl)
for employee in employee_list:
try:
# 获取username
username = employee.get('username', None)
if username is None:
employee['username'] = employee['email']
# 校验通过后获取department_id
employee = self.format_department_id(employee)
err = employee.get('err', None)
if err:
@@ -795,7 +609,7 @@ class CreateEmployee(object):
raise Exception(
','.join(['{}: {}'.format(filed, ','.join(msg)) for filed, msg in form.errors.items()]))
data = self.create_single_with_import(**form.data)
self.create_single_with_import(**form.data)
except Exception as e:
err_list.append({
'email': employee.get('email', ''),
@@ -809,12 +623,12 @@ class CreateEmployee(object):
class EmployeeAddForm(Form):
username = StringField(validators=[
validators.DataRequired(message="username不能为空"),
validators.DataRequired(message=ErrFormat.username_is_required),
validators.Length(max=255),
])
email = StringField(validators=[
validators.DataRequired(message="邮箱不能为空"),
validators.Email(message="邮箱格式不正确"),
validators.DataRequired(message=ErrFormat.email_is_required),
validators.Email(message=ErrFormat.email_format_error),
validators.Length(max=255),
])
password = StringField(validators=[
@@ -823,7 +637,7 @@ class EmployeeAddForm(Form):
position_name = StringField(validators=[])
nickname = StringField(validators=[
validators.DataRequired(message="用户名不能为空"),
validators.DataRequired(message=ErrFormat.nickname_is_required),
validators.Length(max=255),
])
sex = StringField(validators=[])
@@ -834,7 +648,7 @@ class EmployeeAddForm(Form):
class EmployeeUpdateByUidForm(Form):
nickname = StringField(validators=[
validators.DataRequired(message="用户名不能为空"),
validators.DataRequired(message=ErrFormat.nickname_is_required),
validators.Length(max=255),
])
avatar = StringField(validators=[])

View File

@@ -49,3 +49,9 @@ class ErrFormat(CommonErrFormat):
acl_add_user_to_role_failed = "ACL 添加用户到角色失败: {}"
acl_import_user_failed = "ACL 导入用户[{}]失败: {}"
nickname_is_required = "用户名不能为空"
username_is_required = "username不能为空"
email_is_required = "邮箱不能为空"
email_format_error = "邮箱格式错误"
common_data_not_found = "ID {} 找不到记录"

View File

@@ -4,8 +4,7 @@ from api.lib.common_setting.utils import get_cur_time_str
def allowed_file(filename, allowed_extensions):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in allowed_extensions
return '.' in filename and filename.rsplit('.', 1)[1].lower() in allowed_extensions
def generate_new_file_name(name):
@@ -13,4 +12,5 @@ def generate_new_file_name(name):
prev_name = ''.join(name.split(f".{ext}")[:-1])
uid = str(uuid.uuid4())
cur_str = get_cur_time_str('_')
return f"{prev_name}_{cur_str}_{uid}.{ext}"

View File

@@ -1,23 +1,6 @@
# -*- coding:utf-8 -*-
from datetime import datetime
import pandas as pd
from sqlalchemy import text
from api.extensions import db
def get_df_from_read_sql(query, to_dict=False):
bind = query.session.bind
query = query.statement.compile(dialect=bind.dialect if bind else None,
compile_kwargs={"literal_binds": True}).string
a = db.engine
df = pd.read_sql(sql=text(query), con=a.connect())
if to_dict:
return df.to_dict('records')
return df
def get_cur_time_str(split_flag='-'):
f = f"%Y{split_flag}%m{split_flag}%d{split_flag}%H{split_flag}%M{split_flag}%S{split_flag}%f"

View File

@@ -80,10 +80,10 @@ class CRUDMixin(FormatMixin):
db.session.rollback()
raise CommitException(str(e))
def soft_delete(self, flush=False):
def soft_delete(self, flush=False, commit=True):
setattr(self, "deleted", True)
setattr(self, "deleted_at", datetime.datetime.now())
self.save(flush=flush)
self.save(flush=flush, commit=commit)
@classmethod
def get_by_id(cls, _id):
@@ -138,8 +138,11 @@ class CRUDMixin(FormatMixin):
return result[0] if first and result else (None if first else result)
@classmethod
def get_by_like(cls, to_dict=True, **kwargs):
def get_by_like(cls, to_dict=True, deleted=False, **kwargs):
query = db.session.query(cls)
if hasattr(cls, "deleted") and deleted is not None:
query = query.filter(cls.deleted.is_(deleted))
for k, v in kwargs.items():
query = query.filter(getattr(cls, k).ilike('%{0}%'.format(v)))
return [i.to_dict() if to_dict else i for i in query]

View File

@@ -55,8 +55,8 @@ def args_validate(model_cls, exclude_args=None):
if exclude_args and arg in exclude_args:
continue
if attr.type.python_type == str and attr.type.length and \
len(request.values[arg] or '') > attr.type.length:
if attr.type.python_type == str and attr.type.length and (
len(request.values[arg] or '') > attr.type.length):
return abort(400, CommonErrFormat.argument_str_length_limit.format(arg, attr.type.length))
elif attr.type.python_type in (int, float) and request.values[arg]:

View File

@@ -4,21 +4,22 @@
import hashlib
import requests
from future.moves.urllib.parse import urlparse
from flask import abort
from flask import g
from flask import current_app
from flask_login import current_user
from future.moves.urllib.parse import urlparse
def build_api_key(path, params):
g.user is not None or abort(403, u"您得登陆才能进行该操作")
key = g.user.key
secret = g.user.secret
current_user is not None or abort(403, u"您得登陆才能进行该操作")
key = current_user.key
secret = current_user.secret
values = "".join([str(params[k]) for k in sorted(params.keys())
if params[k] is not None]) if params.keys() else ""
_secret = "".join([path, secret, values]).encode("utf-8")
params["_secret"] = hashlib.sha1(_secret).hexdigest()
params["_key"] = key
return params

View File

@@ -5,8 +5,11 @@ import hashlib
import requests
import six
from flask import current_app, g, request
from flask import session, abort
from flask import abort
from flask import current_app
from flask import request
from flask import session
from flask_login import current_user
from api.extensions import cache
from api.lib.perm.acl.audit import AuditCRUD
@@ -84,8 +87,8 @@ class ACLManager(object):
if user:
return Role.get_by(name=name, uid=user.uid, first=True, to_dict=False)
return Role.get_by(name=name, app_id=self.app_id, first=True, to_dict=False) or \
Role.get_by(name=name, first=True, to_dict=False)
return (Role.get_by(name=name, app_id=self.app_id, first=True, to_dict=False) or
Role.get_by(name=name, first=True, to_dict=False))
def add_resource(self, name, resource_type_name=None):
resource_type = ResourceType.get_by(name=resource_type_name, first=True, to_dict=False)
@@ -154,9 +157,9 @@ class ACLManager(object):
if is_app_admin(self.app_id):
return True
role = self._get_role(g.user.username)
role = self._get_role(current_user.username)
role or abort(404, ErrFormat.role_not_found.format(g.user.username))
role or abort(404, ErrFormat.role_not_found.format(current_user.username))
return RoleCRUD.has_permission(role.id, resource_name, resource_type, self.app_id, perm,
resource_id=resource_id)
@@ -193,9 +196,9 @@ class ACLManager(object):
return user
def get_resources(self, resource_type_name=None):
role = self._get_role(g.user.username)
role = self._get_role(current_user.username)
role or abort(404, ErrFormat.role_not_found.format(g.user.username))
role or abort(404, ErrFormat.role_not_found.format(current_user.username))
rid = role.id
return RoleCRUD.recursive_resources(rid, self.app_id, resource_type_name).get('resources')
@@ -215,7 +218,7 @@ def validate_permission(resources, resource_type, perm, app=None):
return
if current_app.config.get("USE_ACL"):
if g.user.username == "worker":
if current_user.username == "worker":
return
resources = [resources] if isinstance(resources, six.string_types) else resources
@@ -313,7 +316,7 @@ def role_required(role_name, app=None):
return
if current_app.config.get("USE_ACL"):
if getattr(g.user, 'username', None) == "worker":
if getattr(current_user, 'username', None) == "worker":
return func(*args, **kwargs)
if role_name not in session.get("acl", {}).get("parentRoles", []) and not is_app_admin(app):

View File

@@ -8,7 +8,9 @@ from flask import abort
from flask import current_app
from api.extensions import db
from api.lib.perm.acl.audit import AuditCRUD, AuditOperateType, AuditScope
from api.lib.perm.acl.audit import AuditCRUD
from api.lib.perm.acl.audit import AuditOperateType
from api.lib.perm.acl.audit import AuditScope
from api.lib.perm.acl.resp_format import ErrFormat
from api.models.acl import App

View File

@@ -4,13 +4,21 @@ import json
from enum import Enum
from typing import List
from flask import g, has_request_context, request
from flask import has_request_context, request
from flask_login import current_user
from sqlalchemy import func
from api.lib.perm.acl import AppCache
from api.models.acl import AuditRoleLog, AuditResourceLog, AuditPermissionLog, AuditTriggerLog, RolePermission, \
Resource, ResourceGroup, Permission, Role, ResourceType
from api.models.acl import AuditPermissionLog
from api.models.acl import AuditResourceLog
from api.models.acl import AuditRoleLog
from api.models.acl import AuditTriggerLog
from api.models.acl import Permission
from api.models.acl import Resource
from api.models.acl import ResourceGroup
from api.models.acl import ResourceType
from api.models.acl import Role
from api.models.acl import RolePermission
class AuditScope(str, Enum):
@@ -49,9 +57,7 @@ class AuditCRUD(object):
@staticmethod
def get_current_operate_uid(uid=None):
user_id = uid or (hasattr(g, 'user') and getattr(g.user, 'uid', None)) \
or getattr(current_user, 'user_id', None)
user_id = uid or (getattr(current_user, 'uid', None)) or getattr(current_user, 'user_id', None)
if has_request_context() and request.headers.get('X-User-Id'):
_user_id = request.headers['X-User-Id']
@@ -93,11 +99,8 @@ class AuditCRUD(object):
criterion.append(AuditPermissionLog.operate_type == v)
records = AuditPermissionLog.query.filter(
AuditPermissionLog.deleted == 0,
*criterion) \
.order_by(AuditPermissionLog.id.desc()) \
.offset((page - 1) * page_size) \
.limit(page_size).all()
AuditPermissionLog.deleted == 0, *criterion).order_by(
AuditPermissionLog.id.desc()).offset((page - 1) * page_size).limit(page_size).all()
data = {
'data': [r.to_dict() for r in records],
@@ -160,10 +163,8 @@ class AuditCRUD(object):
elif k == 'operate_type':
criterion.append(AuditRoleLog.operate_type == v)
records = AuditRoleLog.query.filter(AuditRoleLog.deleted == 0, *criterion) \
.order_by(AuditRoleLog.id.desc()) \
.offset((page - 1) * page_size) \
.limit(page_size).all()
records = AuditRoleLog.query.filter(AuditRoleLog.deleted == 0, *criterion).order_by(
AuditRoleLog.id.desc()).offset((page - 1) * page_size).limit(page_size).all()
data = {
'data': [r.to_dict() for r in records],
@@ -225,11 +226,8 @@ class AuditCRUD(object):
criterion.append(AuditResourceLog.operate_type == v)
records = AuditResourceLog.query.filter(
AuditResourceLog.deleted == 0,
*criterion) \
.order_by(AuditResourceLog.id.desc()) \
.offset((page - 1) * page_size) \
.limit(page_size).all()
AuditResourceLog.deleted == 0, *criterion).order_by(
AuditResourceLog.id.desc()).offset((page - 1) * page_size).limit(page_size).all()
data = {
'data': [r.to_dict() for r in records],
@@ -259,11 +257,8 @@ class AuditCRUD(object):
criterion.append(AuditTriggerLog.operate_type == v)
records = AuditTriggerLog.query.filter(
AuditTriggerLog.deleted == 0,
*criterion) \
.order_by(AuditTriggerLog.id.desc()) \
.offset((page - 1) * page_size) \
.limit(page_size).all()
AuditTriggerLog.deleted == 0, *criterion).order_by(
AuditTriggerLog.id.desc()).offset((page - 1) * page_size).limit(page_size).all()
data = {
'data': [r.to_dict() for r in records],

View File

@@ -60,15 +60,15 @@ class UserCache(object):
@classmethod
def get(cls, key):
user = cache.get(cls.PREFIX_ID.format(key)) or \
cache.get(cls.PREFIX_NAME.format(key)) or \
cache.get(cls.PREFIX_NICK.format(key)) or \
cache.get(cls.PREFIX_WXID.format(key))
user = (cache.get(cls.PREFIX_ID.format(key)) or
cache.get(cls.PREFIX_NAME.format(key)) or
cache.get(cls.PREFIX_NICK.format(key)) or
cache.get(cls.PREFIX_WXID.format(key)))
if not user:
user = User.query.get(key) or \
User.query.get_by_username(key) or \
User.query.get_by_nickname(key) or \
User.query.get_by_wxid(key)
user = (User.query.get(key) or
User.query.get_by_username(key) or
User.query.get_by_nickname(key) or
User.query.get_by_wxid(key))
if user:
cls.set(user)

View File

@@ -4,7 +4,9 @@ import datetime
from flask import abort
from api.extensions import db
from api.lib.perm.acl.audit import AuditCRUD, AuditOperateType, AuditOperateSource
from api.lib.perm.acl.audit import AuditCRUD
from api.lib.perm.acl.audit import AuditOperateSource
from api.lib.perm.acl.audit import AuditOperateType
from api.lib.perm.acl.cache import PermissionCache
from api.lib.perm.acl.cache import RoleCache
from api.lib.perm.acl.cache import UserCache
@@ -97,8 +99,8 @@ class PermissionCRUD(object):
elif group_id is not None:
from api.models.acl import ResourceGroup
group = ResourceGroup.get_by_id(group_id) or \
abort(404, ErrFormat.resource_group_not_found.format("id={}".format(group_id)))
group = ResourceGroup.get_by_id(group_id) or abort(
404, ErrFormat.resource_group_not_found.format("id={}".format(group_id)))
app_id = group.app_id
rt_id = group.resource_type_id
if not perms:
@@ -206,8 +208,8 @@ class PermissionCRUD(object):
if resource_id is not None:
from api.models.acl import Resource
resource = Resource.get_by_id(resource_id) or \
abort(404, ErrFormat.resource_not_found.format("id={}".format(resource_id)))
resource = Resource.get_by_id(resource_id) or abort(
404, ErrFormat.resource_not_found.format("id={}".format(resource_id)))
app_id = resource.app_id
rt_id = resource.resource_type_id
if not perms:
@@ -216,8 +218,8 @@ class PermissionCRUD(object):
elif group_id is not None:
from api.models.acl import ResourceGroup
group = ResourceGroup.get_by_id(group_id) or \
abort(404, ErrFormat.resource_group_not_found.format("id={}".format(group_id)))
group = ResourceGroup.get_by_id(group_id) or abort(
404, ErrFormat.resource_group_not_found.format("id={}".format(group_id)))
app_id = group.app_id
rt_id = group.resource_type_id

View File

@@ -5,7 +5,9 @@ from flask import abort
from flask import current_app
from api.extensions import db
from api.lib.perm.acl.audit import AuditCRUD, AuditOperateType, AuditScope
from api.lib.perm.acl.audit import AuditCRUD
from api.lib.perm.acl.audit import AuditOperateType
from api.lib.perm.acl.audit import AuditScope
from api.lib.perm.acl.cache import ResourceCache
from api.lib.perm.acl.cache import ResourceGroupCache
from api.lib.perm.acl.cache import UserCache
@@ -102,8 +104,8 @@ class ResourceTypeCRUD(object):
@classmethod
def delete(cls, rt_id):
rt = ResourceType.get_by_id(rt_id) or \
abort(404, ErrFormat.resource_type_not_found.format("id={}".format(rt_id)))
rt = ResourceType.get_by_id(rt_id) or abort(
404, ErrFormat.resource_type_not_found.format("id={}".format(rt_id)))
Resource.get_by(resource_type_id=rt_id) and abort(400, ErrFormat.resource_type_cannot_delete)
@@ -165,8 +167,8 @@ class ResourceGroupCRUD(object):
@staticmethod
def add(name, type_id, app_id, uid=None):
ResourceGroup.get_by(name=name, resource_type_id=type_id, app_id=app_id) and \
abort(400, ErrFormat.resource_group_exists.format(name))
ResourceGroup.get_by(name=name, resource_type_id=type_id, app_id=app_id) and abort(
400, ErrFormat.resource_group_exists.format(name))
rg = ResourceGroup.create(name=name, resource_type_id=type_id, app_id=app_id, uid=uid)
AuditCRUD.add_resource_log(app_id, AuditOperateType.create,
@@ -175,8 +177,8 @@ class ResourceGroupCRUD(object):
@staticmethod
def update(rg_id, items):
rg = ResourceGroup.get_by_id(rg_id) or \
abort(404, ErrFormat.resource_group_not_found.format("id={}".format(rg_id)))
rg = ResourceGroup.get_by_id(rg_id) or abort(
404, ErrFormat.resource_group_not_found.format("id={}".format(rg_id)))
existed = ResourceGroupItems.get_by(group_id=rg_id, to_dict=False)
existed_ids = [i.resource_id for i in existed]
@@ -196,8 +198,8 @@ class ResourceGroupCRUD(object):
@staticmethod
def delete(rg_id):
rg = ResourceGroup.get_by_id(rg_id) or \
abort(404, ErrFormat.resource_group_not_found.format("id={}".format(rg_id)))
rg = ResourceGroup.get_by_id(rg_id) or abort(
404, ErrFormat.resource_group_not_found.format("id={}".format(rg_id)))
origin = rg.to_dict()
rg.soft_delete()
@@ -266,8 +268,8 @@ class ResourceCRUD(object):
def add(cls, name, type_id, app_id, uid=None):
type_id = cls._parse_resource_type_id(type_id, app_id)
Resource.get_by(name=name, resource_type_id=type_id, app_id=app_id) and \
abort(400, ErrFormat.resource_exists.format(name))
Resource.get_by(name=name, resource_type_id=type_id, app_id=app_id) and abort(
400, ErrFormat.resource_exists.format(name))
r = Resource.create(name=name, resource_type_id=type_id, app_id=app_id, uid=uid)

View File

@@ -17,6 +17,7 @@ class ErrFormat(CommonErrFormat):
role_exists = "角色 {} 已经存在!"
global_role_not_found = "全局角色 {} 不存在!"
global_role_exists = "全局角色 {} 已经存在!"
user_role_delete_invalid = "删除用户角色, 请在 用户管理 页面操作!"
resource_no_permission = "您没有资源: {}{} 权限"
admin_required = "需要管理员权限"

View File

@@ -6,10 +6,13 @@ import time
import six
from flask import abort
from flask import current_app
from sqlalchemy import or_
from api.extensions import db
from api.lib.perm.acl.app import AppCRUD
from api.lib.perm.acl.audit import AuditCRUD, AuditOperateType, AuditScope
from api.lib.perm.acl.audit import AuditCRUD
from api.lib.perm.acl.audit import AuditOperateType
from api.lib.perm.acl.audit import AuditScope
from api.lib.perm.acl.cache import AppCache
from api.lib.perm.acl.cache import HasResourceRoleCache
from api.lib.perm.acl.cache import RoleCache
@@ -68,16 +71,16 @@ class RoleRelationCRUD(object):
@staticmethod
def get_parent_ids(rid, app_id):
if app_id is not None:
return [i.parent_id for i in RoleRelation.get_by(child_id=rid, app_id=app_id, to_dict=False)] + \
[i.parent_id for i in RoleRelation.get_by(child_id=rid, app_id=None, to_dict=False)]
return ([i.parent_id for i in RoleRelation.get_by(child_id=rid, app_id=app_id, to_dict=False)] +
[i.parent_id for i in RoleRelation.get_by(child_id=rid, app_id=None, to_dict=False)])
else:
return [i.parent_id for i in RoleRelation.get_by(child_id=rid, app_id=app_id, to_dict=False)]
@staticmethod
def get_child_ids(rid, app_id):
if app_id is not None:
return [i.child_id for i in RoleRelation.get_by(parent_id=rid, app_id=app_id, to_dict=False)] + \
[i.child_id for i in RoleRelation.get_by(parent_id=rid, app_id=None, to_dict=False)]
return ([i.child_id for i in RoleRelation.get_by(parent_id=rid, app_id=app_id, to_dict=False)] +
[i.child_id for i in RoleRelation.get_by(parent_id=rid, app_id=None, to_dict=False)])
else:
return [i.child_id for i in RoleRelation.get_by(parent_id=rid, app_id=app_id, to_dict=False)]
@@ -212,18 +215,16 @@ class RoleCRUD(object):
@staticmethod
def search(q, app_id, page=1, page_size=None, user_role=True, is_all=False, user_only=False):
query = db.session.query(Role).filter(Role.deleted.is_(False))
query1 = query.filter(Role.app_id == app_id).filter(Role.uid.is_(None))
query2 = query.filter(Role.app_id.is_(None)).filter(Role.uid.is_(None))
query = query1.union(query2)
if user_role:
query1 = db.session.query(Role).filter(Role.deleted.is_(False)).filter(Role.uid.isnot(None))
query = query.union(query1)
if user_only:
if user_only: # only user role
query = db.session.query(Role).filter(Role.deleted.is_(False)).filter(Role.uid.isnot(None))
else:
query = db.session.query(Role).filter(Role.deleted.is_(False)).filter(
or_(Role.app_id == app_id, Role.app_id.is_(None)))
if not user_role: # only virtual role
query = query.filter(Role.uid.is_(None))
if not is_all:
role_ids = list(HasResourceRoleCache.get(app_id).keys())
query = query.filter(Role.id.in_(role_ids))
@@ -286,11 +287,13 @@ class RoleCRUD(object):
return role
@classmethod
def delete_role(cls, rid):
def delete_role(cls, rid, force=False):
from api.lib.perm.acl.acl import is_admin
role = Role.get_by_id(rid) or abort(404, ErrFormat.role_not_found.format("rid={}".format(rid)))
not force and role.uid and abort(400, ErrFormat.user_role_delete_invalid)
if not role.app_id and not is_admin():
return abort(403, ErrFormat.admin_required)
@@ -302,18 +305,20 @@ class RoleCRUD(object):
for i in RoleRelation.get_by(parent_id=rid, to_dict=False):
child_ids.append(i.child_id)
i.soft_delete()
i.soft_delete(commit=False)
for i in RoleRelation.get_by(child_id=rid, to_dict=False):
parent_ids.append(i.parent_id)
i.soft_delete()
i.soft_delete(commit=False)
role_permissions = []
for i in RolePermission.get_by(rid=rid, to_dict=False):
role_permissions.append(i.to_dict())
i.soft_delete()
i.soft_delete(commit=False)
role.soft_delete()
role.soft_delete(commit=False)
db.session.commit()
role_rebuild.apply_async(args=(recursive_child_ids, role.app_id), queue=ACL_QUEUE)

View File

@@ -6,9 +6,10 @@ import json
import re
from fnmatch import fnmatch
from flask import abort, current_app
from flask import abort
from api.lib.perm.acl.audit import AuditCRUD, AuditOperateType
from api.lib.perm.acl.audit import AuditCRUD
from api.lib.perm.acl.audit import AuditOperateType
from api.lib.perm.acl.cache import UserCache
from api.lib.perm.acl.const import ACL_QUEUE
from api.lib.perm.acl.resp_format import ErrFormat

View File

@@ -6,10 +6,12 @@ import string
import uuid
from flask import abort
from flask import g
from flask_login import current_user
from api.extensions import db
from api.lib.perm.acl.audit import AuditCRUD, AuditOperateType, AuditScope
from api.lib.perm.acl.audit import AuditCRUD
from api.lib.perm.acl.audit import AuditOperateType
from api.lib.perm.acl.audit import AuditScope
from api.lib.perm.acl.cache import UserCache
from api.lib.perm.acl.resp_format import ErrFormat
from api.lib.perm.acl.role import RoleCRUD
@@ -39,18 +41,19 @@ class UserCRUD(object):
@classmethod
def add(cls, **kwargs):
existed = User.get_by(username=kwargs['username'], email=kwargs['email'])
existed = User.get_by(username=kwargs['username'])
existed and abort(400, ErrFormat.user_exists.format(kwargs['username']))
existed = User.get_by(username=kwargs['email'])
existed and abort(400, ErrFormat.user_exists.format(kwargs['email']))
kwargs['nickname'] = kwargs.get('nickname') or kwargs['username']
kwargs['block'] = 0
kwargs['key'], kwargs['secret'] = cls.gen_key_secret()
user_employee = db.session.query(User).filter(User.deleted.is_(False)).order_by(
User.employee_id.desc()).first()
user_employee = db.session.query(User).filter(User.deleted.is_(False)).order_by(User.employee_id.desc()).first()
biggest_employee_id = int(float(user_employee.employee_id)) \
if user_employee is not None else 0
biggest_employee_id = int(float(user_employee.employee_id)) if user_employee is not None else 0
kwargs['employee_id'] = '{0:04d}'.format(biggest_employee_id + 1)
user = User.create(**kwargs)
@@ -90,9 +93,9 @@ class UserCRUD(object):
@classmethod
def reset_key_secret(cls):
key, secret = cls.gen_key_secret()
g.user.update(key=key, secret=secret)
current_user.update(key=key, secret=secret)
UserCache.clean(g.user)
UserCache.clean(current_user)
return key, secret
@@ -103,10 +106,14 @@ class UserCRUD(object):
origin = user.to_dict()
user.soft_delete()
user.delete()
UserCache.clean(user)
role = RoleCRUD.get_by_name(user.username, app_id=None)
if role:
RoleCRUD.delete_role(role[0]['id'], force=True)
AuditCRUD.add_role_log(None, AuditOperateType.delete,
AuditScope.user, user.uid, origin, {}, {}, {})

View File

@@ -7,7 +7,6 @@ from functools import wraps
import jwt
from flask import abort
from flask import current_app
from flask import g
from flask import request
from flask import session
from flask_login import login_user
@@ -64,12 +63,10 @@ def _auth_with_key():
def _auth_with_session():
if isinstance(getattr(g, 'user', None), User):
login_user(g.user)
return True
if "acl" in session and "userName" in (session["acl"] or {}):
login_user(UserCache.get(session["acl"]["userName"]))
return True
return False
@@ -108,7 +105,7 @@ def _auth_with_ip_white_list():
def _auth_with_app_token():
if _auth_with_session():
if _auth_with_session() or _auth_with_token():
if not is_app_admin(request.values.get('app_id')) and request.method != "GET":
return False
elif is_app_admin(request.values.get('app_id')):
@@ -157,7 +154,7 @@ def _auth_with_acl_token():
def auth_required(func):
if request.json is not None:
if request.get_json(silent=True) is not None:
setattr(request, 'values', request.json)
else:
setattr(request, 'values', request.values.to_dict())

View File

@@ -1,7 +1,6 @@
# -*- coding:utf-8 -*-
import base64
import json
import sys
import time
from typing import Set
@@ -113,7 +112,7 @@ class RedisHandler(object):
try:
ret = self.r.hdel(prefix, key_id)
if not ret:
current_app.logger.warn("[{0}] is not in redis".format(key_id))
current_app.logger.warning("[{0}] is not in redis".format(key_id))
except Exception as e:
current_app.logger.error("delete redis key error, {0}".format(str(e)))
@@ -204,9 +203,9 @@ class ESHandler(object):
res = self.es.search(index=self.index, body=query, filter_path=filter_path)
if res['hits'].get('hits'):
return res['hits']['total']['value'], \
[i['_source'] for i in res['hits']['hits']], \
res.get("aggregations", {})
return (res['hits']['total']['value'],
[i['_source'] for i in res['hits']['hits']],
res.get("aggregations", {}))
else:
return 0, [], {}
@@ -257,93 +256,10 @@ class Lock(object):
self.release()
class Redis2Handler(object):
def __init__(self, flask_app=None, prefix=None):
self.flask_app = flask_app
self.prefix = prefix
self.r = None
def init_app(self, app):
self.flask_app = app
config = self.flask_app.config
try:
pool = redis.ConnectionPool(
max_connections=config.get("REDIS_MAX_CONN"),
host=config.get("ONEAGENT_REDIS_HOST"),
port=config.get("ONEAGENT_REDIS_PORT"),
db=config.get("ONEAGENT_REDIS_DB"),
password=config.get("ONEAGENT_REDIS_PASSWORD")
)
self.r = redis.Redis(connection_pool=pool)
except Exception as e:
current_app.logger.warning(str(e))
current_app.logger.error("init redis connection failed")
def get(self, key):
try:
value = json.loads(self.r.get(key))
except:
return
return value
def lrange(self, key, start=0, end=-1):
try:
value = "".join(map(redis_decode, self.r.lrange(key, start, end) or []))
except:
return
return value
def lrange2(self, key, start=0, end=-1):
try:
return list(map(redis_decode, self.r.lrange(key, start, end) or []))
except:
return []
def llen(self, key):
try:
return self.r.llen(key) or 0
except:
return 0
def hget(self, key, field):
try:
return self.r.hget(key, field)
except Exception as e:
current_app.logger.warning("hget redis failed, %s" % str(e))
return
def hset(self, key, field, value):
try:
self.r.hset(key, field, value)
except Exception as e:
current_app.logger.warning("hset redis failed, %s" % str(e))
return
def expire(self, key, timeout):
try:
self.r.expire(key, timeout)
except Exception as e:
current_app.logger.warning("expire redis failed, %s" % str(e))
return
def redis_decode(x):
try:
return x.decode()
except Exception as e:
print(x, e)
try:
return x.decode("gb18030")
except:
return "decode failed"
class AESCrypto(object):
BLOCK_SIZE = 16 # Bytes
pad = lambda s: s + (AESCrypto.BLOCK_SIZE - len(s) % AESCrypto.BLOCK_SIZE) * \
chr(AESCrypto.BLOCK_SIZE - len(s) % AESCrypto.BLOCK_SIZE)
pad = lambda s: s + ((AESCrypto.BLOCK_SIZE - len(s) % AESCrypto.BLOCK_SIZE) *
chr(AESCrypto.BLOCK_SIZE - len(s) % AESCrypto.BLOCK_SIZE))
unpad = lambda s: s[:-ord(s[len(s) - 1:])]
iv = '0102030405060708'
@@ -352,7 +268,7 @@ class AESCrypto(object):
def key():
key = current_app.config.get("SECRET_KEY")[:16]
if len(key) < 16:
key = "{}{}".format(key, (16 - len(key) * "x"))
key = "{}{}".format(key, (16 - len(key)) * "x")
return key.encode('utf8')

View File

@@ -62,10 +62,10 @@ class UserQuery(BaseQuery):
ldap_conn.set_option(ldap.OPT_REFERRALS, 0)
if '@' in username:
email = username
who = '{0}@{1}'.format(username.split('@')[0], current_app.config.get('LDAP_DOMAIN'))
who = current_app.config.get('LDAP_USER_DN').format(username.split('@')[0])
else:
who = '{0}@{1}'.format(username, current_app.config.get('LDAP_DOMAIN'))
email = who
who = current_app.config.get('LDAP_USER_DN').format(username)
email = "{}@{}".format(who, current_app.config.get('LDAP_DOMAIN'))
username = username.split('@')[0]
user = self.get_by_username(username)

View File

@@ -250,6 +250,9 @@ class CIIndexValueDateTime(Model):
class CIValueInteger(Model):
"""
Deprecated in a future version
"""
__tablename__ = "c_value_integers"
ci_id = db.Column(db.Integer, db.ForeignKey('c_cis.id'), nullable=False)
@@ -261,6 +264,9 @@ class CIValueInteger(Model):
class CIValueFloat(Model):
"""
Deprecated in a future version
"""
__tablename__ = "c_value_floats"
ci_id = db.Column(db.Integer, db.ForeignKey('c_cis.id'), nullable=False)
@@ -283,6 +289,9 @@ class CIValueText(Model):
class CIValueDateTime(Model):
"""
Deprecated in a future version
"""
__tablename__ = "c_value_datetime"
ci_id = db.Column(db.Integer, db.ForeignKey('c_cis.id'), nullable=False)

View File

@@ -13,40 +13,39 @@ class Department(ModelWithoutPK):
__tablename__ = 'common_department'
department_id = db.Column(db.Integer, primary_key=True, autoincrement=True)
department_name = db.Column(db.VARCHAR(255), default='', comment='部门名称')
department_name = db.Column(db.VARCHAR(255), default='')
department_director_id = db.Column(
db.Integer, default=0, comment='部门负责人ID')
department_parent_id = db.Column(db.Integer, default=1, comment='上级部门ID')
db.Integer, default=0)
department_parent_id = db.Column(db.Integer, default=1)
sort_value = db.Column(db.Integer, default=0, comment='排序值')
sort_value = db.Column(db.Integer, default=0)
acl_rid = db.Column(db.Integer, comment='ACL中rid', default=0)
acl_rid = db.Column(db.Integer, default=0)
class Employee(ModelWithoutPK):
__tablename__ = 'common_employee'
employee_id = db.Column(db.Integer, primary_key=True, autoincrement=True)
email = db.Column(db.VARCHAR(255), default='', comment='邮箱')
username = db.Column(db.VARCHAR(255), default='', comment='用户名')
nickname = db.Column(db.VARCHAR(255), default='', comment='姓名')
sex = db.Column(db.VARCHAR(64), default='', comment='性别')
position_name = db.Column(db.VARCHAR(255), default='', comment='职位名称')
mobile = db.Column(db.VARCHAR(255), default='', comment='电话号码')
avatar = db.Column(db.VARCHAR(255), default='', comment='头像')
email = db.Column(db.VARCHAR(255), default='')
username = db.Column(db.VARCHAR(255), default='')
nickname = db.Column(db.VARCHAR(255), default='')
sex = db.Column(db.VARCHAR(64), default='')
position_name = db.Column(db.VARCHAR(255), default='')
mobile = db.Column(db.VARCHAR(255), default='')
avatar = db.Column(db.VARCHAR(255), default='')
direct_supervisor_id = db.Column(db.Integer, default=0, comment='直接上级ID')
direct_supervisor_id = db.Column(db.Integer, default=0)
department_id = db.Column(db.Integer,
db.ForeignKey('common_department.department_id'),
comment='部门ID',
db.ForeignKey('common_department.department_id')
)
acl_uid = db.Column(db.Integer, comment='ACL中uid', default=0)
acl_rid = db.Column(db.Integer, comment='ACL中rid', default=0)
acl_virtual_rid = db.Column(db.Integer, comment='ACL中虚拟角色rid', default=0)
last_login = db.Column(db.TIMESTAMP, nullable=True, comment='上次登录时间')
block = db.Column(db.Integer, comment='锁定状态', default=0)
acl_uid = db.Column(db.Integer, default=0)
acl_rid = db.Column(db.Integer, default=0)
acl_virtual_rid = db.Column(db.Integer, default=0)
last_login = db.Column(db.TIMESTAMP, nullable=True)
block = db.Column(db.Integer, default=0)
_department = db.relationship(
'Department', backref='common_employee.department_id',
@@ -55,14 +54,11 @@ class Employee(ModelWithoutPK):
class EmployeeInfo(Model):
"""
员工信息
"""
__tablename__ = 'common_employee_info'
info = db.Column(db.JSON, default={}, comment='员工信息')
info = db.Column(db.JSON, default={})
employee_id = db.Column(db.Integer, db.ForeignKey(
'common_employee.employee_id'), comment='员工ID')
'common_employee.employee_id'))
employee = db.relationship(
'Employee', backref='common_employee.employee_id', lazy='joined')
@@ -74,16 +70,20 @@ class CompanyInfo(Model):
class InternalMessage(Model):
"""
内部消息
"""
__tablename__ = "common_internal_message"
title = db.Column(db.VARCHAR(255), nullable=True, comment='标题')
content = db.Column(db.TEXT, nullable=True, comment='内容')
path = db.Column(db.VARCHAR(255), nullable=True, comment='跳转路径')
is_read = db.Column(db.Boolean, default=False, comment='是否已读')
app_name = db.Column(db.VARCHAR(128), nullable=False, comment='应用名称')
category = db.Column(db.VARCHAR(128), nullable=False, comment='分类')
message_data = db.Column(db.JSON, nullable=True, comment='数据')
title = db.Column(db.VARCHAR(255), nullable=True)
content = db.Column(db.TEXT, nullable=True)
path = db.Column(db.VARCHAR(255), nullable=True)
is_read = db.Column(db.Boolean, default=False)
app_name = db.Column(db.VARCHAR(128), nullable=False)
category = db.Column(db.VARCHAR(128), nullable=False)
message_data = db.Column(db.JSON, nullable=True)
employee_id = db.Column(db.Integer, db.ForeignKey('common_employee.employee_id'), comment='ID')
class CommonData(Model):
__table_name__ = 'common_data'
data_type = db.Column(db.VARCHAR(255), default='')
data = db.Column(db.JSON)

View File

@@ -2,7 +2,8 @@
import os
import sys
from inspect import getmembers, isclass
from inspect import getmembers
from inspect import isclass
import six
from flask import jsonify
@@ -27,16 +28,15 @@ class APIView(Resource):
return send_file(*args, **kwargs)
API_PACKAGE = "api"
API_PACKAGE = os.path.abspath(os.path.dirname(__file__))
def register_resources(resource_path, rest_api):
for root, _, files in os.walk(os.path.join(resource_path)):
for filename in files:
if not filename.startswith("_") and filename.endswith("py"):
module_path = os.path.join(API_PACKAGE, root[root.index("views"):])
if module_path not in sys.path:
sys.path.insert(1, module_path)
if root not in sys.path:
sys.path.insert(1, root)
view = __import__(os.path.splitext(filename)[0])
resource_list = [o[0] for o in getmembers(view) if isclass(o[1]) and issubclass(o[1], Resource)]
resource_list = [i for i in resource_list if i != "APIView"]

View File

@@ -5,17 +5,20 @@ import re
from celery_once import QueueOnce
from flask import current_app
from werkzeug.exceptions import BadRequest, NotFound
from werkzeug.exceptions import BadRequest
from werkzeug.exceptions import NotFound
from api.extensions import celery
from api.extensions import db
from api.lib.perm.acl.audit import AuditCRUD
from api.lib.perm.acl.audit import AuditOperateSource
from api.lib.perm.acl.audit import AuditOperateType
from api.lib.perm.acl.cache import AppCache
from api.lib.perm.acl.cache import RoleCache
from api.lib.perm.acl.cache import RoleRelationCache
from api.lib.perm.acl.cache import UserCache
from api.lib.perm.acl.const import ACL_QUEUE
from api.lib.perm.acl.record import OperateRecordCRUD
from api.lib.perm.acl.audit import AuditCRUD, AuditOperateType, AuditOperateSource
from api.models.acl import Resource
from api.models.acl import Role
from api.models.acl import Trigger

View File

@@ -7,6 +7,7 @@ import time
import jinja2
import requests
from flask import current_app
from flask_login import login_user
import api.lib.cmdb.ci
from api.extensions import celery
@@ -18,8 +19,12 @@ from api.lib.cmdb.const import CMDB_QUEUE
from api.lib.cmdb.const import REDIS_PREFIX_CI
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION
from api.lib.mail import send_mail
from api.lib.perm.acl.cache import UserCache
from api.lib.utils import Lock
from api.lib.utils import handle_arg_list
from api.models.cmdb import CI
from api.models.cmdb import CIRelation
from api.models.cmdb import CITypeAttribute
@celery.task(name="cmdb.ci_cache", queue=CMDB_QUEUE)
@@ -84,6 +89,51 @@ def ci_relation_cache(parent_id, child_id):
current_app.logger.info("ADD ci relation cache: {0} -> {1}".format(parent_id, child_id))
@celery.task(name="cmdb.ci_relation_add", queue=CMDB_QUEUE)
def ci_relation_add(parent_dict, child_id, uid):
"""
:param parent_dict: key is '$parent_model.attr_name'
:param child_id:
:param uid:
:return:
"""
from api.lib.cmdb.ci import CIRelationManager
from api.lib.cmdb.ci_type import CITypeAttributeManager
from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci import search
current_app.test_request_context().push()
login_user(UserCache.get(uid))
db.session.remove()
for parent in parent_dict:
parent_ci_type_name, _attr_name = parent.strip()[1:].split('.', 1)
attr_name = CITypeAttributeManager.get_attr_name(parent_ci_type_name, _attr_name)
if attr_name is None:
current_app.logger.warning("attr name {} does not exist".format(_attr_name))
continue
parent_dict[parent] = handle_arg_list(parent_dict[parent])
for v in parent_dict[parent]:
query = "_type:{},{}:{}".format(parent_ci_type_name, attr_name, v)
s = search(query)
try:
response, _, _, _, _, _ = s.search()
except SearchError as e:
current_app.logger.error('ci relation add failed: {}'.format(e))
continue
for ci in response:
try:
CIRelationManager.add(ci['_id'], child_id)
ci_relation_cache(ci['_id'], child_id)
except Exception as e:
current_app.logger.warning(e)
finally:
db.session.remove()
@celery.task(name="cmdb.ci_relation_delete", queue=CMDB_QUEUE)
def ci_relation_delete(parent_id, child_id):
with Lock("CIRelation_{}".format(parent_id)):
@@ -156,3 +206,18 @@ def trigger_notify(notify, ci_id):
for i in notify['mail_to'] if i], subject, body)
except Exception as e:
current_app.logger.error("Send mail failed: {0}".format(str(e)))
@celery.task(name="cmdb.calc_computed_attribute", queue=CMDB_QUEUE)
def calc_computed_attribute(attr_id, uid):
from api.lib.cmdb.ci import CIManager
db.session.remove()
current_app.test_request_context().push()
login_user(UserCache.get(uid))
for i in CITypeAttribute.get_by(attr_id=attr_id, to_dict=False):
cis = CI.get_by(type_id=i.type_id, to_dict=False)
for ci in cis:
CIManager.update(ci.id, {})

View File

@@ -13,13 +13,9 @@ from api.models.common_setting import Department
@celery.task(name="common_setting.edit_employee_department_in_acl", queue=COMMON_SETTING_QUEUE)
def edit_employee_department_in_acl(e_list, new_d_id, op_uid):
"""
在 ACL 员工更换部门
:param e_list: 员工列表 {acl_rid: 11, department_id: 22}
:param new_d_id: 新部门 ID
:param op_uid: 操作人 ID
在老部门中删除员工
在新部门中添加员工
:param e_list:{acl_rid: 11, department_id: 22}
:param new_d_id
:param op_uid
"""
db.session.remove()
@@ -43,7 +39,6 @@ def edit_employee_department_in_acl(e_list, new_d_id, op_uid):
new_department_acl_rid = new_department.acl_rid if new_d_rid_in_acl == new_department.acl_rid else new_d_rid_in_acl
for employee in e_list:
# 根据 部门ID获取部门 acl_rid
old_department = Department.get_by(
first=True, department_id=employee.get('department_id'), to_dict=False)
if not old_department:
@@ -61,7 +56,6 @@ def edit_employee_department_in_acl(e_list, new_d_id, op_uid):
acl_rid=old_d_rid_in_acl
)
d_acl_rid = old_department.acl_rid if old_d_rid_in_acl == old_department.acl_rid else old_d_rid_in_acl
# 在老部门中删除员工
payload = {
'app_id': 'acl',
'parent_id': d_acl_rid,
@@ -71,7 +65,6 @@ def edit_employee_department_in_acl(e_list, new_d_id, op_uid):
except Exception as e:
result.append(ErrFormat.acl_remove_user_from_role_failed.format(str(e)))
# 在新部门中添加员工
payload = {
'app_id': 'acl',
'child_ids': [employee_acl_rid],

View File

@@ -2,12 +2,13 @@
import datetime
import six
import jwt
import six
from flask import abort
from flask import current_app
from flask import request
from flask_login import login_user, logout_user
from flask_login import login_user
from flask_login import logout_user
from api.lib.decorator import args_required
from api.lib.perm.acl.cache import User

View File

@@ -1,7 +1,7 @@
# -*- coding:utf-8 -*-
from flask import g
from flask import request
from flask_login import current_user
from api.lib.decorator import args_required
from api.lib.decorator import args_validate
@@ -103,8 +103,8 @@ class ResourceView(APIView):
type_id = request.values.get('type_id')
app_id = request.values.get('app_id')
uid = request.values.get('uid')
if not uid and hasattr(g, "user") and hasattr(g.user, "uid"):
uid = g.user.uid
if not uid and hasattr(current_user, "uid"):
uid = current_user.uid
resource = ResourceCRUD.add(name, type_id, app_id, uid)

View File

@@ -2,8 +2,8 @@
from flask import abort
from flask import current_app
from flask import g
from flask import request
from flask_login import current_user
from api.lib.decorator import args_required
from api.lib.decorator import args_validate
@@ -31,12 +31,9 @@ class RoleView(APIView):
page_size = get_page_size(request.values.get("page_size"))
q = request.values.get('q')
app_id = request.values.get('app_id')
is_all = request.values.get('is_all', True)
is_all = True if is_all in current_app.config.get("BOOL_TRUE") else False
user_role = request.values.get('user_role', True)
user_only = request.values.get('user_only', False)
user_role = True if user_role in current_app.config.get("BOOL_TRUE") else False
user_only = True if user_only in current_app.config.get("BOOL_TRUE") else False
is_all = request.values.get('is_all', True) in current_app.config.get("BOOL_TRUE")
user_role = request.values.get('user_role', True) in current_app.config.get("BOOL_TRUE")
user_only = request.values.get('user_only', False) in current_app.config.get("BOOL_TRUE")
numfound, roles = RoleCRUD.search(q, app_id, page, page_size, user_role, is_all, user_only)
@@ -160,8 +157,8 @@ class RoleHasPermissionView(APIView):
@auth_with_app_token
def get(self):
if not request.values.get('rid'):
role = RoleCache.get_by_name(None, g.user.username)
role or abort(404, ErrFormat.role_not_found.format(g.user.username))
role = RoleCache.get_by_name(None, current_user.username)
role or abort(404, ErrFormat.role_not_found.format(current_user.username))
else:
role = RoleCache.get(int(request.values.get('rid')))

View File

@@ -4,7 +4,6 @@
import requests
from flask import abort
from flask import current_app
from flask import g
from flask import request
from flask import session
from flask_login import current_user
@@ -13,7 +12,6 @@ from api.lib.decorator import args_required
from api.lib.decorator import args_validate
from api.lib.perm.acl.acl import ACLManager
from api.lib.perm.acl.acl import role_required
from api.lib.perm.acl.audit import AuditCRUD, AuditOperateType
from api.lib.perm.acl.cache import AppCache
from api.lib.perm.acl.cache import UserCache
from api.lib.perm.acl.resp_format import ErrFormat
@@ -116,7 +114,7 @@ class UserView(APIView):
@role_required("acl_admin")
def delete(self, uid):
if g.user.uid == uid:
if current_user.uid == uid:
return abort(400, ErrFormat.invalid_operation)
UserCRUD.delete(uid)
@@ -162,8 +160,8 @@ class UserResetPasswordView(APIView):
if app.name not in ('cas-server', 'acl'):
return abort(403, ErrFormat.invalid_request)
elif hasattr(g, 'user'):
if g.user.username != request.values['username']:
elif hasattr(current_user, 'username'):
if current_user.username != request.values['username']:
return abort(403, ErrFormat.invalid_request)
else:

View File

@@ -33,7 +33,8 @@ class AttributeSearchView(APIView):
class AttributeView(APIView):
url_prefix = ("/attributes", "/attributes/<string:attr_name>", "/attributes/<int:attr_id>")
url_prefix = ("/attributes", "/attributes/<string:attr_name>", "/attributes/<int:attr_id>",
"/attributes/<int:attr_id>/calc_computed_attribute")
def get(self, attr_name=None, attr_id=None):
attr_manager = AttributeManager()
@@ -55,7 +56,12 @@ class AttributeView(APIView):
@args_required("name")
@args_validate(AttributeManager.cls)
def post(self):
def post(self, attr_id=None):
if request.url.endswith("/calc_computed_attribute"):
AttributeManager.calc_computed_attribute(attr_id)
return self.jsonify(attr_id=attr_id)
choice_value = handle_arg_list(request.values.get("choice_value"))
params = request.values
params["choice_value"] = choice_value
@@ -63,6 +69,7 @@ class AttributeView(APIView):
current_app.logger.debug(params)
attr_id = AttributeManager.add(**params)
return self.jsonify(attr_id=attr_id)
@args_validate(AttributeManager.cls)
@@ -72,8 +79,10 @@ class AttributeView(APIView):
params["choice_value"] = choice_value
current_app.logger.debug(params)
AttributeManager().update(attr_id, **params)
return self.jsonify(attr_id=attr_id)
def delete(self, attr_id):
attr_name = AttributeManager.delete(attr_id)
return self.jsonify(message="attribute {0} deleted".format(attr_name))

View File

@@ -5,8 +5,8 @@ from io import BytesIO
from flask import abort
from flask import current_app
from flask import g
from flask import request
from flask_login import current_user
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryCICRUD
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryCITypeCRUD
@@ -75,9 +75,9 @@ class AutoDiscoveryRuleTemplateFileView(APIView):
return self.send_file(bf,
as_attachment=True,
attachment_filename="cmdb_auto_discovery.json",
download_name="cmdb_auto_discovery.json",
mimetype='application/json',
cache_timeout=0)
max_age=0)
def post(self):
f = request.files.get('file')
@@ -119,7 +119,7 @@ class AutoDiscoveryCITypeView(APIView):
_, res = AutoDiscoveryCITypeCRUD.search(page=1, page_size=100000, type_id=type_id, **request.values)
for i in res:
if isinstance(i.get("extra_option"), dict) and i['extra_option'].get('secret'):
if not (g.user.username == "cmdb_agent" or g.user.uid == i['uid']):
if not (current_user.username == "cmdb_agent" or current_user.uid == i['uid']):
i['extra_option'].pop('secret', None)
else:
i['extra_option']['secret'] = AESCrypto.decrypt(i['extra_option']['secret'])
@@ -213,7 +213,7 @@ class AutoDiscoveryRuleSyncView(APIView):
url_prefix = ("/adt/sync",)
def get(self):
if g.user.username not in ("cmdb_agent", "worker", "admin"):
if current_user.username not in ("cmdb_agent", "worker", "admin"):
return abort(403)
oneagent_name = request.values.get('oneagent_name')

View File

@@ -11,7 +11,8 @@ from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.ci import CIManager
from api.lib.cmdb.ci import CIRelationManager
from api.lib.cmdb.const import ExistPolicy
from api.lib.cmdb.const import ResourceTypeEnum, PermEnum
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.const import RetKey
from api.lib.cmdb.perms import has_perm_for_ci
from api.lib.cmdb.search import SearchError
@@ -106,6 +107,7 @@ class CIView(APIView):
_is_admin=request.values.pop('__is_admin', False),
**ci_dict)
else:
request.values.pop('exist_policy', None)
ci_id = manager.add(ci_type,
exist_policy=ExistPolicy.REPLACE,
_no_attribute_policy=_no_attribute_policy,

View File

@@ -350,9 +350,9 @@ class CITypeTemplateFileView(APIView):
return self.send_file(bf,
as_attachment=True,
attachment_filename="cmdb_template.json",
download_name="cmdb_template.json",
mimetype='application/json',
cache_timeout=0)
max_age=0)
@role_required(RoleEnum.CONFIG)
def post(self): # import

View File

@@ -6,7 +6,9 @@ from flask import request
from api.lib.cmdb.ci_type import CITypeManager
from api.lib.cmdb.ci_type import CITypeRelationManager
from api.lib.cmdb.const import PermEnum, ResourceTypeEnum, RoleEnum
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.const import RoleEnum
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.decorator import args_required
from api.lib.perm.acl.acl import ACLManager

View File

@@ -7,7 +7,8 @@ from flask import abort
from flask import request
from api.lib.cmdb.ci import CIManager
from api.lib.cmdb.const import ResourceTypeEnum, PermEnum
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.const import RoleEnum
from api.lib.cmdb.history import AttributeHistoryManger
from api.lib.cmdb.history import CITypeHistoryManager

View File

@@ -5,7 +5,9 @@ from flask import abort
from flask import request
from api.lib.cmdb.ci_type import CITypeManager
from api.lib.cmdb.const import PermEnum, ResourceTypeEnum, RoleEnum
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.const import RoleEnum
from api.lib.cmdb.perms import CIFilterPermsCRUD
from api.lib.cmdb.preference import PreferenceManager
from api.lib.cmdb.resp_format import ErrFormat

View File

@@ -0,0 +1,35 @@
from flask import request
from api.lib.common_setting.common_data import CommonDataCRUD
from api.resource import APIView
prefix = '/data'
class DataView(APIView):
url_prefix = (f'{prefix}/<string:data_type>',)
def get(self, data_type):
data_list = CommonDataCRUD.get_data_by_type(data_type)
return self.jsonify(data_list)
def post(self, data_type):
params = request.json
CommonDataCRUD.create_new_data(data_type, **params)
return self.jsonify(params)
class DataViewWithId(APIView):
url_prefix = (f'{prefix}/<string:data_type>/<int:_id>',)
def put(self, data_type, _id):
params = request.json
res = CommonDataCRUD.update_data(_id, **params)
return self.jsonify(res.to_dict())
def delete(self, data_type, _id):
CommonDataCRUD.delete(_id)
return self.jsonify({})

View File

@@ -16,15 +16,16 @@ class CompanyInfoView(APIView):
return self.jsonify(CompanyInfoCRUD.get())
def post(self):
info = CompanyInfoCRUD.get()
if info:
abort(400, ErrFormat.company_info_is_already_existed)
data = {
'info': {
**request.values
}
}
d = CompanyInfoCRUD.create(**data)
info = CompanyInfoCRUD.get()
if info:
d = CompanyInfoCRUD.update(info.get('id'), **data)
else:
d = CompanyInfoCRUD.create(**data)
res = d.to_dict()
return self.jsonify(res)

View File

@@ -100,7 +100,7 @@ class DepartmentSortView(APIView):
def put(self):
"""
修改部门排序,只能在同一个上级内排序
only can sort in the same parent
"""
department_list = request.json.get('department_list', None)
if department_list is None:

View File

@@ -145,43 +145,3 @@ class EmployeePositionView(APIView):
result = EmployeeCRUD.get_all_position()
return self.jsonify(result)
class EmployeeViewExportExcel(APIView):
url_prefix = (f'{prefix}/export_all',)
def get(self):
col_desc_map = {
'nickname': "姓名",
'email': '邮箱',
'sex': '性别',
'mobile': '手机号',
'department_name': '部门',
'position_name': '岗位',
'nickname_direct_supervisor': '直接上级',
'last_login': '上次登录时间',
}
# 规定了静态文件的存储位置
excel_filename = 'all_employee_info.xlsx'
excel_path = current_app.config['UPLOAD_DIRECTORY_FULL']
excel_path_with_filename = os.path.join(excel_path, excel_filename)
# 根据parameter查表自连接通过上级id获取上级名字列
block_status = int(request.args.get('block_status', -1))
df = EmployeeCRUD.get_export_employee_df(block_status)
# 改变列名为中文head
try:
df = df.rename(columns=col_desc_map)
except Exception as e:
abort(500, ErrFormat.rename_columns_failed.format(str(e)))
# 生成静态excel文件
try:
df.to_excel(excel_path_with_filename,
sheet_name='Sheet1', index=False, encoding="utf-8")
except Exception as e:
current_app.logger.error(e)
abort(500, ErrFormat.generate_excel_failed.format(str(e)))
return send_from_directory(excel_path, excel_filename, as_attachment=True)

View File

@@ -6,7 +6,9 @@ from flask import Blueprint
from flask_restful import Api
from api.resource import register_resources
from .account import LoginView, LogoutView, AuthWithKeyView
from .account import AuthWithKeyView
from .account import LoginView
from .account import LogoutView
HERE = os.path.abspath(os.path.dirname(__file__))

View File

@@ -1,14 +1,7 @@
# -*- coding: utf-8 -*-
"""Create an application instance."""
from flask import g
from flask_login import current_user
from api.app import create_app
app = create_app()
@app.before_request
def before_request():
g.user = current_user

View File

@@ -3,7 +3,7 @@
from api.app import create_app
from api.extensions import celery
# celery worker -A celery_worker.celery -l DEBUG -E -Q xxxx
# celery -A celery_worker.celery worker -l DEBUG -E -Q xxxx
app = create_app()
app.app_context().push()

View File

@@ -1,80 +1,46 @@
-i https://mirrors.aliyun.com/pypi/simple
alembic==1.7.7
amqp==2.6.1
aniso8601==9.0.1
APScheduler==3.10.1
attrs==23.1.0
backports.zoneinfo==0.2.1
bcrypt==4.0.1
beautifulsoup4==4.12.2
billiard==3.6.4.0
bs4==0.0.1
cachelib==0.9.0
celery==4.3.0
celery==5.3.1
celery-once==3.0.1
certifi==2023.5.7
charset-normalizer==3.1.0
click==8.1.3
dnspython==2.3.0
elasticsearch==7.17.9
email-validator==1.3.1
environs==4.2.0
flasgger==0.9.5
Flask==1.0.3
Flask-APScheduler==1.12.4
Flask-Bcrypt==0.7.1
Flask==2.3.2
Flask-Bcrypt==1.0.1
Flask-Caching==2.0.2
Flask-Cors==4.0.0
Flask-Login==0.4.1
Flask-Login==0.6.2
Flask-Migrate==2.5.2
Flask-RESTful==0.3.7
Flask-SQLAlchemy==2.4.0
future==0.18.2
gunicorn==19.5.0
idna==3.4
importlib-metadata==6.8.0
importlib-resources==6.0.0
itsdangerous==2.0.1
Jinja2==3.0.1
Flask-RESTful==0.3.10
Flask-SQLAlchemy==2.5.0
future==0.18.3
gunicorn==21.0.1
itsdangerous==2.1.2
Jinja2==3.1.2
jinja2schema==0.1.4
jsonschema==4.18.0
jsonschema-specifications==2023.6.1
kombu==4.4.0
kombu==5.3.1
Mako==1.2.4
MarkupSafe==2.1.3
marshmallow==2.20.2
meld3==2.0.1
mistune==3.0.1
more-itertools==5.0.0
msgpack-python==0.5.6
numpy==1.18.5
pandas==1.3.2
Pillow==8.3.2
pkgutil_resolve_name==1.3.10
pyasn1==0.5.0
pyasn1-modules==0.3.0
Pillow==9.3.0
pycryptodome==3.12.0
PyJWT==2.4.0
PyMySQL==0.9.3
python-dateutil==2.8.2
python-dotenv==1.0.0
python-ldap==3.2.0
pytz==2023.3
PyMySQL==1.1.0
python-ldap==3.4.0
PyYAML==6.0
redis==3.2.1
referencing==0.29.1
redis==4.6.0
requests==2.31.0
rpds-py==0.8.8
six==1.12.0
soupsieve==2.4.1
SQLAlchemy==1.3.5
SQLAlchemy==1.4.49
supervisor==4.0.3
timeout-decorator==0.5.0
toposort==1.10
treelib==1.6.1
tzlocal==5.0.1
urllib3==1.26.16
vine==1.3.0
Werkzeug==0.15.5
WTForms==3.0.0
zipp==3.16.0
Werkzeug==2.3.6
WTForms==3.0.0

View File

@@ -35,6 +35,7 @@ SQLALCHEMY_ENGINE_OPTIONS = {
CACHE_TYPE = "redis"
CACHE_REDIS_HOST = "127.0.0.1"
CACHE_REDIS_PORT = 6379
CACHE_REDIS_PASSWORD = ""
CACHE_KEY_PREFIX = "CMDB::"
CACHE_DEFAULT_TIMEOUT = 3000
@@ -53,13 +54,16 @@ MAIL_PASSWORD = ''
DEFAULT_MAIL_SENDER = ''
# # queue
CELERY_RESULT_BACKEND = "redis://127.0.0.1:6379/2"
BROKER_URL = 'redis://127.0.0.1:6379/2'
BROKER_VHOST = '/'
CELERY = {
"broker_url": 'redis://127.0.0.1:6379/2',
"result_backend": "redis://127.0.0.1:6379/2",
"broker_vhost": "/",
"broker_connection_retry_on_startup": True
}
ONCE = {
'backend': 'celery_once.backends.Redis',
'settings': {
'url': BROKER_URL,
'url': CELERY['broker_url'],
}
}
@@ -76,13 +80,14 @@ DEFAULT_SERVICE = "http://127.0.0.1:8000"
AUTH_WITH_LDAP = False
LDAP_SERVER = ''
LDAP_DOMAIN = ''
LDAP_USER_DN = 'cn={},ou=users,dc=xxx,dc=com'
# # pagination
DEFAULT_PAGE_COUNT = 50
# # permission
WHITE_LIST = ["127.0.0.1"]
USE_ACL = False
USE_ACL = True
# # elastic search
ES_HOST = '127.0.0.1'

View File

@@ -1,8 +1,11 @@
# -*- coding: utf-8 -*-
"""provide some sample data in database"""
import uuid
import random
import uuid
from api.lib.cmdb.ci import CIManager, CIRelationManager
from api.lib.cmdb.ci_type import CITypeAttributeManager
from api.models.acl import User
from api.models.cmdb import (
Attribute,
CIType,
@@ -11,16 +14,12 @@ from api.models.cmdb import (
CITypeRelation,
RelationType
)
from api.models.acl import User
from api.lib.cmdb.ci_type import CITypeAttributeManager
from api.lib.cmdb.ci import CIManager, CIRelationManager
def force_add_user():
from flask import g
if not getattr(g, "user", None):
g.user = User.query.first()
from flask_login import current_user, login_user
if not getattr(current_user, "username", None):
login_user(User.query.first())
def init_attributes(num=1):
@@ -77,12 +76,12 @@ def init_relation_type(num=1):
def init_ci_type_relation(num=1):
result = []
ci_types = init_ci_types(num+1)
ci_types = init_ci_types(num + 1)
relation_types = init_relation_type(num)
for i in range(num):
result.append(CITypeRelation.create(
parent_id=ci_types[i].id,
child_id=ci_types[i+1].id,
child_id=ci_types[i + 1].id,
relation_type_id=relation_types[i].id
))
return result

View File

@@ -1 +0,0 @@
public/* linguist-vendored

26
cmdb-ui/.gitignore vendored
View File

@@ -1,26 +0,0 @@
.DS_Store
node_modules
/dist
/dist.zip
/temp
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw*
*.css.map
.env.development

View File

@@ -1,11 +0,0 @@
#Oneops-UI
```shell
## build
yarn run build
## develop
yarn run serve
```

View File

@@ -53,7 +53,7 @@
"vuedraggable": "^2.23.0",
"vuex": "^3.1.1",
"vxe-table": "3.6.9",
"vxe-table-plugin-export-xlsx": "^3.0.4",
"vxe-table-plugin-export-xlsx": "2.0.0",
"xe-utils": "3",
"xlsx": "0.15.0",
"xlsx-js-style": "^1.2.0"

View File

@@ -20,13 +20,7 @@ export function putCompanyInfo(id, parameter) {
data: parameter,
})
}
export function postImageFile(parameter) {
return axios({
url: '/common-setting/v1/file',
method: 'post',
data: parameter,
})
}
export function getDepartmentList(params) {
// ?department_parent_id=-1 查询第一级部门下面的id根据实际的传
return axios({

31
cmdb-ui/src/api/file.js Normal file
View File

@@ -0,0 +1,31 @@
import { axios } from '@/utils/request'
export function postImageFile(parameter) {
return axios({
url: '/common-setting/v1/file',
method: 'post',
data: parameter,
})
}
export function getFileData(data_type) {
return axios({
url: `/common-setting/v1/data/${data_type}`,
method: 'get',
})
}
export function addFileData(data_type, data) {
return axios({
url: `/common-setting/v1/data/${data_type}`,
method: 'post',
data,
})
}
export function deleteFileData(data_type, id) {
return axios({
url: `/common-setting/v1/data/${data_type}/${id}`,
method: 'delete',
})
}

View File

@@ -15,31 +15,117 @@
>
{{ item.label }}
</div>
<div :class="`${currentIconType === '4' ? 'selected' : ''}`" @click="handleChangeIconType('4')">
自定义
</div>
<a-upload
slot="description"
name="avatar"
:before-upload="beforeUpload"
:show-upload-list="false"
accept=".svg,.png,.jpg,.jpeg"
v-if="currentIconType === '4'"
>
<a-button icon="plus" size="small" type="primary">添加</a-button>
</a-upload>
</div>
<div class="custom-icon-select-popover-content">
<div v-for="category in iconList" :key="category.value">
<h4 class="category">{{ category.label }}</h4>
<div class="custom-icon-select-popover-content-wrapper">
<template v-if="iconList && iconList.length">
<template v-if="currentIconType !== '4'">
<div v-for="category in iconList" :key="category.value">
<h4 class="category">{{ category.label }}</h4>
<div class="custom-icon-select-popover-content-wrapper">
<div
v-for="name in category.list"
:key="name.value"
:class="`custom-icon-select-popover-item ${value.name === name.value ? 'selected' : ''}`"
@click="clickIcon(name.value)"
>
<ops-icon :type="name.value" />
<span class="custom-icon-select-popover-item-label">{{ name.label }}</span>
</div>
</div>
</div>
</template>
<div class="custom-icon-select-popover-content-wrapper" :style="{ marginTop: '10px' }" v-else>
<div
v-for="name in category.list"
:key="name.value"
:class="`custom-icon-select-popover-item ${value.name === name.value ? 'selected' : ''}`"
@click="clickIcon(name.value)"
v-for="icon in iconList"
:key="icon.id"
:class="`custom-icon-select-popover-item ${value.id === icon.id ? 'selected' : ''}`"
@click="clickCustomIcon(icon)"
>
<ops-icon :type="name.value" />
<span class="custom-icon-select-popover-item-label">{{ name.label }}</span>
<div class="custom-icon-select-popover-content-img-box">
<img :src="`/api/common-setting/v1/file/${icon.data.url}`" />
<a-popconfirm
overlayClassName="custom-icon-select-confirm-popover"
:getPopupContainer="(trigger) => trigger.parentNode"
title="确认删除?"
@confirm="(e) => deleteIcon(e, icon)"
@cancel="
(e) => {
e.stopPropagation()
e.preventDefault()
}
"
>
<a-icon
type="close"
@click="
(e) => {
e.stopPropagation()
e.preventDefault()
}
"
/>
</a-popconfirm>
</div>
<span class="custom-icon-select-popover-item-label" :title="icon.data.name">{{ icon.data.name }}</span>
</div>
</div>
</div>
</template>
<a-empty v-else :style="{ marginTop: '15%' }">
<img slot="image" :src="require('@/assets/data_empty.png')" />
<a-upload
slot="description"
name="avatar"
:before-upload="beforeUpload"
:show-upload-list="false"
accept=".svg,.png,.jpg,.jpeg"
>
<a> 暂无自定义图标点击此处上传 </a>
</a-upload>
</a-empty>
</div>
<template v-if="currentIconType !== '0' && currentIconType !== '3'">
<template v-if="!['0', '3', '4'].includes(currentIconType)">
<a-divider :style="{ margin: '5px 0' }" />
<el-color-picker size="mini" v-model="value.color"> </el-color-picker>
</template>
<a-form class="custom-icon-select-form" :form="form" v-show="currentIconType === '4' && formVisible">
<a-form-item
label="名称"
:labelCol="{ span: 4 }"
:wrapperCol="{ span: 16 }"
><a-input
v-decorator="['name', { rules: [{ required: true, message: '请输入名称' }] }]"
/></a-form-item>
<a-form-item label="预览" :labelCol="{ span: 4 }">
<div class="custom-icon-select-form-img">
<img :src="formImg" />
</div>
</a-form-item>
<a-form-item label=" " :colon="false" :labelCol="{ span: 16 }">
<a-space>
<a-button size="small" @click="handleCancel">取消</a-button>
<a-button size="small" type="primary" @click="handleOk">确定</a-button>
</a-space>
</a-form-item>
</a-form>
</div>
<div class="custom-icon-select-block" id="custom-icon-select-block" @click="showSelect">
<img v-if="value.id && value.url" :src="`/api/common-setting/v1/file/${value.url}`" />
<ops-icon
v-else
:type="value.name"
:style="{ color: value.name && value.name.startsWith('icon-') ? value.color || '' : '' }"
/>
@@ -56,6 +142,8 @@ import {
fillIconList,
multicolorIconList,
} from './constants'
import { postImageFile, getFileData, addFileData, deleteFileData } from '@/api/file'
export default {
name: 'CustomIconSelect',
components: { ElColorPicker: ColorPicker },
@@ -77,13 +165,18 @@ export default {
},
data() {
return {
form: this.$form.createForm(this),
iconTypeList,
commonIconList,
linearIconList,
fillIconList,
multicolorIconList,
visible: false,
currentIconType: '1',
currentIconType: '3',
customIconList: [],
formVisible: false,
formImg: null,
file: null,
}
},
computed: {
@@ -97,18 +190,30 @@ export default {
return this.fillIconList
case '3': // 多色
return this.multicolorIconList
case '4': // 自定义
return this.customIconList
default:
return this.linearIconList
}
},
fileName() {
const splitFileName = this.file.name.split('.')
return splitFileName.splice(0, splitFileName.length - 1).join('')
},
},
mounted() {
document.addEventListener('click', this.eventListener)
this.getFileData()
},
beforeDestroy() {
document.removeEventListener('click', this.eventListener)
},
methods: {
getFileData() {
getFileData('ops-custom-icon').then((res) => {
this.customIconList = res
})
},
eventListener(e) {
if (this.visible) {
const dom = document.getElementById(`custom-icon-select-popover`)
@@ -137,25 +242,87 @@ export default {
})
}
},
clickCustomIcon(icon) {
if (icon.id === this.value.id) {
this.$emit('change', {
name: '',
color: '',
})
} else {
this.$emit('change', { name: icon.data.name, id: icon.id, url: icon.data.url })
}
},
showSelect() {
this.visible = true
console.log(this.value)
if (!this.value.name) {
this.currentIconType = '1'
this.currentIconType = '3'
return
}
// changyong已废弃
if (this.value.name.startsWith('changyong-')) {
this.currentIconType = '0'
} else if (this.value.name.startsWith('icon-xianxing')) {
this.currentIconType = '1'
} else if (this.value.name.startsWith('icon-shidi')) {
this.currentIconType = '2'
} else {
} else if (this.value.name.startsWith('caise')) {
this.currentIconType = '3'
} else {
this.currentIconType = '4'
}
},
handleChangeIconType(value) {
this.currentIconType = value
},
beforeUpload(file) {
const isLt2M = file.size / 1024 / 1024 < 2
if (!isLt2M) {
this.$message.error('图片大小不可超过2MB!')
return false
}
const reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = () => {
this.formVisible = true
this.$nextTick(() => {
this.file = file
this.formImg = reader.result
this.form.setFieldsValue({ name: this.fileName })
})
}
return false
},
handleCancel() {
this.formVisible = false
this.form.setFieldsValue({ name: '' })
this.formImg = null
},
handleOk() {
const fm = new FormData()
fm.append('file', this.file)
postImageFile(fm).then((res) => {
this.form.validateFields((err, values) => {
if (!err) {
addFileData('ops-custom-icon', { data: { name: values.name, url: res.file_name } }).then(() => {
this.$message.success('上传成功!')
this.handleCancel()
this.getFileData()
})
}
})
})
},
deleteIcon(e, icon) {
e.stopPropagation()
e.preventDefault()
deleteFileData('ops-custom-icon', icon.id).then(() => {
this.$message.success('删除成功!')
this.handleCancel()
this.getFileData()
})
},
},
}
</script>
@@ -176,7 +343,7 @@ export default {
padding: 4px 6px;
}
.custom-icon-select-popover-content {
max-height: 400px;
height: 400px;
overflow: auto;
.category {
font-size: 14px;
@@ -197,12 +364,43 @@ export default {
padding: 5px 5px 2px 5px;
margin: 0 2px 6px;
color: #666;
position: relative;
.custom-icon-select-popover-item-label {
margin-top: 6px;
font-size: 11px;
width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: center;
}
&:hover {
background-color: #eeeeee;
.custom-icon-select-popover-content-img-box > i {
display: inline;
}
}
.custom-icon-select-popover-content-img-box {
width: 26px;
height: 26px;
display: flex;
align-items: center;
justify-content: center;
> img {
max-width: 26px;
max-height: 26px;
}
> i {
display: none;
position: absolute;
top: 2px;
right: 2px;
font-size: 12px;
&:hover {
color: #2f54eb;
}
}
}
}
.selected {
@@ -212,6 +410,8 @@ export default {
}
.custom-icon-select-popover-icon-type {
display: inline-block;
width: 100%;
position: relative;
> div {
cursor: pointer;
display: inline-block;
@@ -224,6 +424,16 @@ export default {
.selected {
border-color: #2f54eb;
}
.ant-btn {
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
}
}
.custom-icon-select-confirm-popover .ant-popover-inner-content {
width: 150px;
}
}
</style>
@@ -234,15 +444,39 @@ export default {
width: 28px;
height: 28px;
border-radius: 4px;
border: 1px solid #eeeeee;
border: 1px solid #d9d9d9;
display: inline-block;
cursor: pointer;
> i {
> i,
> img {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
> img {
max-width: 26px;
max-height: 26px;
}
> i {
font-size: 18px;
}
}
.custom-icon-select-form {
.custom-icon-select-form-img {
width: 28px;
height: 28px;
border-radius: 4px;
border: 1px solid #d9d9d9;
display: inline-flex;
margin-top: 5px;
justify-content: center;
align-items: center;
overflow: hidden;
img {
max-width: 26px;
max-height: 26px;
}
}
}
</style>

View File

@@ -222,6 +222,9 @@ export default {
renderIcon({ icon, selectedIcon, customIcon = undefined, name = undefined, typeId = undefined, routeName }) {
if (typeId) {
if (customIcon) {
if (customIcon.split('$$')[2]) {
return <img style={{ maxHeight: '14px', maxWidth: '14px', marginRight: '10px' }} src={`/api/common-setting/v1/file/${customIcon.split('$$')[3]}`}></img >
}
return <ops-icon
style={{
color: customIcon.split('$$')[1],

View File

@@ -4,6 +4,7 @@ const appConfig = {
buildAclToModules: true, // 是否在各个应用下 内联权限管理
ssoLogoutURL: '/api/sso/logout',
showDocs: false,
useEncryption: true,
}
export default appConfig

View File

@@ -78,6 +78,7 @@
</template>
<script>
import _ from 'lodash'
import Pager from './pager.vue'
import SearchForm from './searchForm.vue'
import { searchPermissonHistory } from '@/modules/acl/api/history'
@@ -251,23 +252,25 @@ export default {
},
// 处理查询参数
handleQueryParams(queryParams) {
const _queryParams = _.cloneDeep(queryParams)
let q = ''
for (const key in queryParams) {
for (const key in _queryParams) {
if (
key !== 'page' &&
key !== 'page_size' &&
key !== 'app_id' &&
key !== 'start' &&
key !== 'end' &&
queryParams[key] !== undefined
_queryParams[key] !== undefined
) {
if (q) q += `,${key}:${queryParams[key]}`
else q += `${key}:${queryParams[key]}`
delete queryParams[key]
if (q) q += `,${key}:${_queryParams[key]}`
else q += `${key}:${_queryParams[key]}`
delete _queryParams[key]
}
}
const newQueryParams = { ...queryParams, q }
return q ? newQueryParams : queryParams
const newQueryParams = { ..._queryParams, q }
return q ? newQueryParams : _queryParams
},
// searchForm相关
@@ -283,7 +286,7 @@ export default {
this.getTable(this.queryParams)
},
handleSearch(queryParams) {
this.queryParams = { ...queryParams, app_id: this.app_id }
this.queryParams = { ...this.queryParams, ...queryParams, app_id: this.app_id }
this.getTable(this.queryParams)
},

View File

@@ -212,7 +212,12 @@ export default {
// searchForm相关
handleSearch(queryParams) {
this.queryParams = { ...queryParams, app_id: this.app_id, scope: this.checked ? 'resource_group' : 'resource' }
this.queryParams = {
...this.queryParams,
...queryParams,
app_id: this.app_id,
scope: this.checked ? 'resource_group' : 'resource',
}
this.getTable(this.queryParams)
},
searchFormReset() {
@@ -245,9 +250,11 @@ export default {
},
handleQueryParams(queryParams) {
const _queryParams = _.cloneDeep(queryParams)
let flag = false
let q = queryParams.q ? queryParams.q : ''
for (const key in queryParams) {
let q = _queryParams.q ? _queryParams.q : ''
for (const key in _queryParams) {
if (
key !== 'page' &&
key !== 'page_size' &&
@@ -255,16 +262,16 @@ export default {
key !== 'q' &&
key !== 'start' &&
key !== 'end' &&
queryParams[key] !== undefined
_queryParams[key] !== undefined
) {
flag = true
if (q) q += `,${key}:${queryParams[key]}`
else q += `${key}:${queryParams[key]}`
delete queryParams[key]
if (q) q += `,${key}:${_queryParams[key]}`
else q += `${key}:${_queryParams[key]}`
delete _queryParams[key]
}
}
const newQueryParams = { ...queryParams, q }
return flag ? newQueryParams : queryParams
const newQueryParams = { ..._queryParams, q }
return flag ? newQueryParams : _queryParams
},
handleTagColor(operateType) {
return this.colorMap.get(operateType)

View File

@@ -179,7 +179,7 @@ export default {
// searchForm相关
handleSearch(queryParams) {
this.queryParams = { ...queryParams, app_id: this.app_id, scope: 'resource_type' }
this.queryParams = { ...this.queryParams, ...queryParams, app_id: this.app_id, scope: 'resource_type' }
this.getTable(this.queryParams)
},
searchFormReset() {
@@ -206,9 +206,11 @@ export default {
},
handleQueryParams(queryParams) {
const _queryParams = _.cloneDeep(queryParams)
let flag = false
let q = queryParams.q ? queryParams.q : ''
for (const key in queryParams) {
let q = _queryParams.q ? _queryParams.q : ''
for (const key in _queryParams) {
if (
key !== 'page' &&
key !== 'page_size' &&
@@ -216,16 +218,16 @@ export default {
key !== 'q' &&
key !== 'start' &&
key !== 'end' &&
queryParams[key] !== undefined
_queryParams[key] !== undefined
) {
flag = true
if (q) q += `,${key}:${queryParams[key]}`
else q += `${key}:${queryParams[key]}`
delete queryParams[key]
if (q) q += `,${key}:${_queryParams[key]}`
else q += `${key}:${_queryParams[key]}`
delete _queryParams[key]
}
}
const newQueryParams = { ...queryParams, q }
return flag ? newQueryParams : queryParams
const newQueryParams = { ..._queryParams, q }
return flag ? newQueryParams : _queryParams
},
handleTagColor(operateType) {
return this.colorMap.get(operateType)

View File

@@ -220,7 +220,12 @@ export default {
this.getTable(this.queryParams)
},
handleSearch(queryParams) {
this.queryParams = { ...queryParams, app_id: this.app_id, scope: this.checked ? 'role_relation' : 'role' }
this.queryParams = {
...this.queryParams,
...queryParams,
app_id: this.app_id,
scope: this.checked ? 'role_relation' : 'role',
}
this.getTable(this.queryParams)
},
searchFormReset() {
@@ -237,9 +242,11 @@ export default {
// 处理查询参数
handleQueryParams(queryParams) {
const _queryParams = _.cloneDeep(queryParams)
let flag = false
let q = queryParams.q ? queryParams.q : ''
for (const key in queryParams) {
let q = _queryParams.q ? _queryParams.q : ''
for (const key in _queryParams) {
if (
key !== 'page' &&
key !== 'page_size' &&
@@ -247,16 +254,16 @@ export default {
key !== 'q' &&
key !== 'start' &&
key !== 'end' &&
queryParams[key] !== undefined
_queryParams[key] !== undefined
) {
flag = true
if (q) q += `,${key}:${queryParams[key]}`
else q += `${key}:${queryParams[key]}`
delete queryParams[key]
if (q) q += `,${key}:${_queryParams[key]}`
else q += `${key}:${_queryParams[key]}`
delete _queryParams[key]
}
}
const newQueryParams = { ...queryParams, q }
return flag ? newQueryParams : queryParams
const newQueryParams = { ..._queryParams, q }
return flag ? newQueryParams : _queryParams
},
// 处理tag颜色
handleTagColor(operateType) {
@@ -280,7 +287,7 @@ export default {
item.description += str
} else {
const str = ` ${key} : ${oldVal} 改为 ${newVal} `
item.description += ` ${key} : ${oldVal} 改为 ${newVal} `
item.description += str
}
}
}

View File

@@ -147,7 +147,6 @@ export default {
expand: false,
queryParams: {
page: 1,
page_size: 50,
},
date: undefined,
checked: false,
@@ -188,7 +187,6 @@ export default {
handleReset() {
this.queryParams = {
page: 1,
page_size: 50,
}
this.date = undefined
this.$emit('searchFormReset')

View File

@@ -200,7 +200,7 @@ export default {
// searchForm相关
handleSearch(queryParams) {
this.queryParams = queryParams
this.queryParams = { ...this.queryParams, ...queryParams }
this.queryParams.app_id = this.app_id
this.getTable(this.queryParams)
},
@@ -238,7 +238,7 @@ export default {
item.changeDescription += str
} else {
const str = ` ${key} : ${oldVal} 改为 ${newVal} `
item.changeDescription += ` ${key} : ${oldVal} 改为 ${newVal} `
item.changeDescription += str
}
}
}
@@ -281,25 +281,27 @@ export default {
}
},
handleQueryParams(queryParams) {
const _queryParams = _.cloneDeep(queryParams)
let q = ''
for (const key in queryParams) {
for (const key in _queryParams) {
if (
key !== 'page' &&
key !== 'page_size' &&
key !== 'app_id' &&
key !== 'start' &&
key !== 'end' &&
queryParams[key] !== undefined
_queryParams[key] !== undefined
) {
if (q) {
q += `,${key}:${queryParams[key]}`
q += `,${key}:${_queryParams[key]}`
} else {
q += `${key}:${queryParams[key]}`
q += `${key}:${_queryParams[key]}`
}
}
}
const newQueryParams = { ...queryParams, q }
return q ? newQueryParams : queryParams
const newQueryParams = { ..._queryParams, q }
return q ? newQueryParams : _queryParams
},
handleTagColor(operateType) {
return this.colorMap.get(operateType)

View File

@@ -79,6 +79,7 @@
</template>
<script>
import _ from 'lodash'
import debounce from 'lodash/debounce'
import Pager from '../../module/pager.vue'
import SearchForm from '../../module/searchForm.vue'
@@ -347,7 +348,7 @@ export default {
this.getTable(this.queryParams)
},
handleSearch(queryParams) {
this.queryParams = queryParams
this.queryParams = { ...this.queryParams, ...queryParams }
this.getTable(this.queryParams)
},
handleExpandChange(expand) {
@@ -387,23 +388,24 @@ export default {
},
handleQueryParams(queryParams) {
const _queryParams = _.cloneDeep(queryParams)
let q = ''
for (const key in queryParams) {
for (const key in _queryParams) {
if (
key !== 'page' &&
key !== 'page_size' &&
key !== 'app_id' &&
key !== 'start' &&
key !== 'end' &&
queryParams[key] !== undefined
_queryParams[key] !== undefined
) {
if (q) q += `,${key}:${queryParams[key]}`
else q += `${key}:${queryParams[key]}`
delete queryParams[key]
if (q) q += `,${key}:${_queryParams[key]}`
else q += `${key}:${_queryParams[key]}`
delete _queryParams[key]
}
}
const newQueryParams = { ...queryParams, q }
return q ? newQueryParams : queryParams
const newQueryParams = { ..._queryParams, q }
return q ? newQueryParams : _queryParams
},
},
}

View File

@@ -267,7 +267,7 @@ export default {
this.isExpand = expand
},
handleSearch(queryParams) {
this.queryParams = { ...queryParams, scope: this.checked ? 'resource_group' : 'resource' }
this.queryParams = { ...this.queryParams, ...queryParams, scope: this.checked ? 'resource_group' : 'resource' }
this.getTable(this.queryParams)
},
searchFormReset() {
@@ -317,9 +317,11 @@ export default {
},
handleQueryParams(queryParams) {
const _queryParams = _.cloneDeep(queryParams)
let flag = false
let q = queryParams.q ? queryParams.q : ''
for (const key in queryParams) {
let q = _queryParams.q ? _queryParams.q : ''
for (const key in _queryParams) {
if (
key !== 'page' &&
key !== 'page_size' &&
@@ -327,16 +329,16 @@ export default {
key !== 'q' &&
key !== 'start' &&
key !== 'end' &&
queryParams[key] !== undefined
_queryParams[key] !== undefined
) {
flag = true
if (q) q += `,${key}:${queryParams[key]}`
else q += `${key}:${queryParams[key]}`
delete queryParams[key]
if (q) q += `,${key}:${_queryParams[key]}`
else q += `${key}:${_queryParams[key]}`
delete _queryParams[key]
}
}
const newQueryParams = { ...queryParams, q }
return flag ? newQueryParams : queryParams
const newQueryParams = { ..._queryParams, q }
return flag ? newQueryParams : _queryParams
},
handleTagColor(operateType) {
return this.colorMap.get(operateType)

View File

@@ -217,7 +217,7 @@ export default {
this.isExpand = expand
},
handleSearch(queryParams) {
this.queryParams = { ...queryParams, scope: 'resource_type' }
this.queryParams = { ...this.queryParams, ...queryParams, scope: 'resource_type' }
this.getTable(this.queryParams)
},
searchFormReset() {
@@ -253,9 +253,11 @@ export default {
},
handleQueryParams(queryParams) {
const _queryParams = _.cloneDeep(queryParams)
let flag = false
let q = queryParams.q ? queryParams.q : ''
for (const key in queryParams) {
let q = _queryParams.q ? _queryParams.q : ''
for (const key in _queryParams) {
if (
key !== 'page' &&
key !== 'page_size' &&
@@ -263,16 +265,16 @@ export default {
key !== 'q' &&
key !== 'start' &&
key !== 'end' &&
queryParams[key] !== undefined
_queryParams[key] !== undefined
) {
flag = true
if (q) q += `,${key}:${queryParams[key]}`
else q += `${key}:${queryParams[key]}`
delete queryParams[key]
if (q) q += `,${key}:${_queryParams[key]}`
else q += `${key}:${_queryParams[key]}`
delete _queryParams[key]
}
}
const newQueryParams = { ...queryParams, q }
return flag ? newQueryParams : queryParams
const newQueryParams = { ..._queryParams, q }
return flag ? newQueryParams : _queryParams
},
handleTagColor(operateType) {
return this.colorMap.get(operateType)

View File

@@ -231,7 +231,7 @@ export default {
this.getTable(this.queryParams)
},
handleSearch(queryParams) {
this.queryParams = { ...queryParams, scope: this.checked ? 'role_relation' : 'role' }
this.queryParams = { ...this.queryParams, ...queryParams, scope: this.checked ? 'role_relation' : 'role' }
this.getTable(this.queryParams)
},
searchFormReset() {
@@ -247,9 +247,10 @@ export default {
// 处理查询参数
handleQueryParams(queryParams) {
const _queryParams = _.cloneDeep(queryParams)
let flag = false
let q = queryParams.q ? queryParams.q : ''
for (const key in queryParams) {
let q = _queryParams.q ? _queryParams.q : ''
for (const key in _queryParams) {
if (
key !== 'page' &&
key !== 'page_size' &&
@@ -257,16 +258,16 @@ export default {
key !== 'q' &&
key !== 'start' &&
key !== 'end' &&
queryParams[key] !== undefined
_queryParams[key] !== undefined
) {
flag = true
if (q) q += `,${key}:${queryParams[key]}`
else q += `${key}:${queryParams[key]}`
delete queryParams[key]
if (q) q += `,${key}:${_queryParams[key]}`
else q += `${key}:${_queryParams[key]}`
delete _queryParams[key]
}
}
const newQueryParams = { ...queryParams, q }
return flag ? newQueryParams : queryParams
const newQueryParams = { ..._queryParams, q }
return flag ? newQueryParams : _queryParams
},
// 处理tag颜色
handleTagColor(operateType) {

View File

@@ -241,7 +241,7 @@ export default {
this.isExpand = expand
},
handleSearch(queryParams) {
this.queryParams = queryParams
this.queryParams = { ...this.queryParams, ...queryParams }
this.getTable(this.queryParams)
},
searchFormReset() {
@@ -286,7 +286,7 @@ export default {
item.changeDescription += str
} else {
const str = ` ${key} : ${oldVal} 改为 ${newVal} `
item.changeDescription += ` ${key} : ${oldVal} 改为 ${newVal} `
item.changeDescription += str
}
}
}
@@ -326,25 +326,27 @@ export default {
}
},
handleQueryParams(queryParams) {
const _queryParams = _.cloneDeep(queryParams)
let q = ''
for (const key in queryParams) {
for (const key in _queryParams) {
if (
key !== 'page' &&
key !== 'page_size' &&
key !== 'app_id' &&
key !== 'start' &&
key !== 'end' &&
queryParams[key] !== undefined
_queryParams[key] !== undefined
) {
if (q) {
q += `,${key}:${queryParams[key]}`
q += `,${key}:${_queryParams[key]}`
} else {
q += `${key}:${queryParams[key]}`
q += `${key}:${_queryParams[key]}`
}
}
}
const newQueryParams = { ...queryParams, q }
return q ? newQueryParams : queryParams
const newQueryParams = { ..._queryParams, q }
return q ? newQueryParams : _queryParams
},
handleTagColor(operateType) {
return this.colorMap.get(operateType)

View File

@@ -70,7 +70,7 @@
show-overflow
>
<!-- 1 -->
<vxe-table-column type="checkbox" fixed="left"></vxe-table-column>
<vxe-table-column type="checkbox" fixed="left" :width="45"></vxe-table-column>
<!-- 2 -->
@@ -186,49 +186,6 @@ export default {
pageSize: 50,
},
tableData: [],
tableColumns: [
{
type: 'checkbox',
fixed: 'left',
},
{
title: '资源名',
field: 'name',
minWidth: '150px',
showOverflow: 'tooltip',
fixed: 'left',
slots: {
header: 'name_header',
},
},
{
title: '创建者',
minWidth: '100px',
field: 'user',
},
{
title: '创建时间',
minWidth: '220px',
field: 'created_at',
align: 'center',
},
{
title: '最后修改时间',
minWidth: '220px',
field: 'updated_at',
align: 'center',
},
{
title: '操作',
field: 'action',
width: '200px',
fixed: 'right',
align: 'center',
slots: {
default: 'action_default',
},
},
],
btnName: '新增资源',
isGroup: false,
allResourceTypes: [],

View File

@@ -16,6 +16,7 @@
class="ops-stripe-table"
:columns="tableColumns"
:data="tableData"
show-overflow
highlight-hover-row
:height="`${windowHeight - 165}px`"
size="small"
@@ -24,15 +25,17 @@
<a-icon type="lock" v-if="row.block" />
</template>
<template #action_default="{row}">
<template>
<a-space>
<a :disabled="isAclAdmin ? false : true" @click="handleEdit(row)">
<a-icon type="edit" />
</a>
<a-divider type="vertical" />
<a-tooltip title="权限汇总">
<a @click="handlePermCollect(row)"><a-icon type="solution"/></a>
</a-tooltip>
</template>
<a-popconfirm :title="`确认删除【${row.nickname || row.username}】?`" @confirm="deleteUser(row.uid)">
<a :style="{ color: 'red' }"><ops-icon type="icon-xianxing-delete"/></a>
</a-popconfirm>
</a-space>
</template>
</vxe-grid>
</a-spin>
@@ -73,11 +76,14 @@ export default {
title: '加入时间',
field: 'date_joined',
minWidth: '160px',
align: 'center',
sortable: true,
},
{
title: '锁定',
field: 'block',
minWidth: '100px',
width: '150px',
align: 'center',
slots: {
default: 'block_default',
},
@@ -85,8 +91,9 @@ export default {
{
title: '操作',
field: 'action',
minWidth: '180px',
width: '150px',
fixed: 'right',
align: 'center',
slots: {
default: 'action_default',
},
@@ -155,9 +162,6 @@ export default {
handleEdit(record) {
this.$refs.userForm.handleEdit(record)
},
handleDelete(record) {
this.deleteUser(record.uid)
},
handleOk() {
this.searchName = ''
this.search()
@@ -165,9 +169,9 @@ export default {
handleCreate() {
this.$refs.userForm.handleCreate()
},
deleteUser(attrId) {
deleteUserById(attrId).then((res) => {
this.$message.success(`删除成功`)
deleteUser(uid) {
deleteUserById(uid).then((res) => {
this.$message.success(`删除成功`)
this.handleOk()
})
},

View File

@@ -25,18 +25,20 @@ export function addCI(params) {
})
}
export function updateCI(id, params) {
export function updateCI(id, params, isShowMessage = true) {
return axios({
url: urlPrefix + `/ci/${id}`,
method: 'PUT',
data: params
data: params,
isShowMessage
})
}
export function deleteCI(ciId) {
export function deleteCI(ciId, isShowMessage = true) {
return axios({
url: urlPrefix + `/ci/${ciId}`,
method: 'DELETE'
method: 'DELETE',
isShowMessage
})
}

View File

@@ -1,6 +1,14 @@
<template>
<div class="ci-type-grant">
<vxe-table size="mini" stripe class="ops-stripe-table" :data="filterTableData" :max-height="`${tableHeight}px`">
<vxe-table
ref="xTable"
size="mini"
stripe
class="ops-stripe-table"
:data="filterTableData"
:max-height="`${tableHeight}px`"
:row-style="(params) => getCurrentRowStyle(params, addedRids)"
>
<vxe-column field="name"></vxe-column>
<vxe-column v-for="col in columns" :key="col" :field="col" :title="permMap[col]">
<template #default="{row}">
@@ -37,6 +45,7 @@ import _ from 'lodash'
import { permMap } from './constants.js'
import { grantCiType, revokeCiType } from '../../api/CIType'
import ReadCheckbox from './readCheckbox.vue'
import { getCurrentRowStyle } from './utils'
export default {
name: 'CiTypeGrant',
@@ -55,10 +64,13 @@ export default {
type: String,
default: 'ci_type',
},
addedRids: {
type: Array,
default: () => [],
},
},
computed: {
filterTableData() {
console.log(_.cloneDeep(this.tableData))
const _tableData = this.tableData.filter((data) => {
const _intersection = _.intersection(
Object.keys(data),
@@ -90,6 +102,7 @@ export default {
}
},
methods: {
getCurrentRowStyle,
async handleChange(e, col, row) {
if (e.target.checked) {
await grantCiType(this.CITypeId, row.rid, { perms: [col] }).catch(() => {

Some files were not shown because too many files have changed in this diff Show More