Compare commits

...

170 Commits
2.3.0 ... 2.3.5

Author SHA1 Message Date
pycook
8217053abf release: v2.3.5 2023-10-09 20:55:30 +08:00
pycook
8c17373e45 feat: get messenger url from common setting 2023-10-09 20:25:27 +08:00
wang-liang0615
a8e2595327 前端更新 (#192)
* fix:add package

* fix:notice_info为null的情况

* fix:2 bugs

* feat:1.common增加通知配置 2.cmdb预定义值webhook&其他模型

* fix:json 不支持预定义值

* fix:json 不支持预定义值

* fix:删除代码
2023-10-09 19:52:19 +08:00
simontigers
dfbba103cd fix: init company structure resource (#191)
* fix: init company structure resource

* fix: notice_info null
2023-10-09 19:25:49 +08:00
wang-liang0615
86b9d5a7f4 前端更新 (#189)
* fix:add package

* fix:notice_info为null的情况

* fix:2 bugs

* feat:1.common增加通知配置 2.cmdb预定义值webhook&其他模型

* fix:json 不支持预定义值

* fix:json 不支持预定义值
2023-10-09 17:43:34 +08:00
simontigers
612922a1b7 feat: notice_config access messenger (#190) 2023-10-09 17:32:20 +08:00
pycook
2758c5e468 fix: delete user role 2023-10-09 15:40:18 +08:00
pycook
d85c86a839 feat: The definition of attribute choice values supports webhook and other model attribute values. 2023-10-09 15:33:18 +08:00
wang-liang0615
8355137e43 Dev UI (#186)
* fix:add package

* fix:notice_info为null的情况

* fix:2 bugs
2023-09-28 09:45:11 +08:00
pycook
2e644233bc release 2.3.4 2023-09-27 11:37:18 +08:00
simontigers
d9b4082b46 feat: add api get_notice_by_ids (#184) 2023-09-27 09:54:30 +08:00
wang-liang0615
a07f984152 前端更新 (#183)
* fix:add package

* fix:notice_info为null的情况
2023-09-27 09:18:33 +08:00
pycook
4cab7ef6b0 feat: ci triggers 2023-09-26 21:18:34 +08:00
wang-liang0615
070c163de6 fix:add package (#182) 2023-09-26 21:12:10 +08:00
wang-liang0615
282a779fb1 Merge pull request #181 from veops/dev_ui
前端更新
2023-09-26 20:34:27 +08:00
wang-liang0615
cb6b51a84c Merge branch 'master' into dev_ui 2023-09-26 20:34:14 +08:00
wang-liang0615
34bd320e75 fix:topo图相同节点出现两次的bug 2023-09-26 20:13:12 +08:00
wang-liang0615
1eca5791f6 feat:wangeditor 注册自定义组件 2023-09-26 20:07:00 +08:00
wang-liang0615
13b1c9a30c delete:删除getwx 2023-09-26 20:04:38 +08:00
simontigers
b1a15a85d2 feat: common notice config (#180) 2023-09-26 19:44:20 +08:00
wang-liang0615
08e5a02caf feat: UI更新 触发器 (#179)
* feat:新增api&适配

* feat:触发器

* add packages & 注释代码

* feat: webhook tips
2023-09-26 18:25:04 +08:00
wang-liang0615
308827b8fc feat: webhook tips 2023-09-26 18:17:23 +08:00
wang-liang0615
dc4ccb22b9 add packages & 注释代码 2023-09-26 17:35:41 +08:00
wang-liang0615
c482e7ea43 feat:触发器 2023-09-26 17:01:31 +08:00
wang-liang0615
663c14f763 feat:新增api&适配 2023-09-26 16:26:25 +08:00
pycook
c6ee227bab fix: ci_cache 2023-09-25 15:46:07 +08:00
wang-liang0615
cb62cf2410 Merge pull request #178 from veops/dev_ui
前端更新:仪表盘优化
2023-09-25 14:52:09 +08:00
wang-liang0615
133f32a6b0 pref:仪表盘优化 2023-09-25 14:50:08 +08:00
wang-liang0615
45c48c86fe Merge branch 'master' of github.com:veops/cmdb into dev_ui 2023-09-25 14:43:34 +08:00
pycook
2321f17dae refactor: CI triggers 2023-09-22 17:39:54 +08:00
simontigers
ddb31a07a2 fix: icon svg support (#177) 2023-09-20 15:56:57 +08:00
pycook
b474914fbb fix date search 2023-09-18 18:15:02 +08:00
pycook
26099a3d69 fix dashboard compute 2023-09-18 13:04:50 +08:00
pycook
62829c885b release v2.3.3 2023-09-15 17:57:39 +08:00
pycook
260aed6462 dashboard ui update 2023-09-15 17:36:10 +08:00
simontigers
3841999cca feat: init resource for backend (#176) 2023-09-15 15:30:30 +08:00
pycook
14c03ce5d2 enhance dashboard 2023-09-15 15:26:20 +08:00
pycook
f463ecd6e6 cmdb-api/api/lib/resp_format.py 2023-09-12 20:01:30 +08:00
pycook
adc0cfd5c5 Detect circular dependencies when adding CIType relationships 2023-09-12 20:00:56 +08:00
wang-liang0615
086481657e 计算属性 触发计算 (#174) 2023-09-11 19:16:05 +08:00
pycook
d2f84ae3dc fix upload template and add /api/v0.1/attributes/<int:attr_id>/calc_computed_attribute 2023-09-11 19:15:31 +08:00
wang-liang0615
9f1b510cb3 Merge branch 'master' of github.com:veops/cmdb into dev_ui 2023-09-11 17:35:44 +08:00
wang-liang0615
61acb2483d 计算属性 触发计算 2023-09-11 17:34:51 +08:00
pycook
0196c8a82c release 2.3.2 2023-09-07 13:44:51 +08:00
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
215 changed files with 17187 additions and 10618 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` 分支在开发过程中可能处于 _不稳定的状态_ 。
@@ -21,9 +21,9 @@
### 相关文档
- <a href="https://zhuanlan.zhihu.com/p/98453732" target="_blank">设计文档</a>
- <a href="https://mp.weixin.qq.com/s/v3eANth64UBW5xdyOkK3tg" target="_blank">概要设计</a>
- <a href="https://github.com/veops/cmdb/tree/master/docs/cmdb_api.md" target="_blank">API 文档</a>
- <a href="https://mp.weixin.qq.com/s/EflmmJ-qdUkddTx2hRt3pA" target="_blank">树形视图实践</a>
- <a href="https://mp.weixin.qq.com/s/rQaf4AES7YJsyNQG_MKOLg" target="_blank">自动发现</a>
### 特点
@@ -36,7 +36,7 @@
- 多应用
1. 丰富视图展示维度
2. 提供 Restful API
3. 自定义字段触发器
3. 支持定义属性触发器、计算属性
### 主要功能
@@ -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,30 +32,29 @@ 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"
six = "==1.16.0"
bs4 = ">=0.0.1"
toposort = ">=1.5"
requests = ">=2.22.0"
requests_oauthlib = "==1.3.1"
markdownify = "==0.11.6"
PyJWT = "==2.4.0"
elasticsearch = "==7.17.9"
future = "==0.18.2"
itsdangerous = "==2.0.1"
Jinja2 = "==3.0.1"
future = "==0.18.3"
itsdangerous = "==2.1.2"
Jinja2 = "==3.1.2"
jinja2schema = "==0.1.4"
msgpack-python = "==0.5.6"
alembic = "==1.7.7"

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

@@ -9,11 +9,12 @@ import time
import click
from flask import current_app
from flask.cli import with_appcontext
from flask_login import login_user
import api.lib.cmdb.ci
from api.extensions import db
from api.extensions import rd
from api.lib.cmdb.ci_type import CITypeTriggerManager
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import REDIS_PREFIX_CI
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION
@@ -22,6 +23,7 @@ from api.lib.cmdb.const import RoleEnum
from api.lib.cmdb.const import ValueTypeEnum
from api.lib.exception import AbortException
from api.lib.perm.acl.acl import ACLManager
from api.lib.perm.acl.acl import UserCache
from api.lib.perm.acl.cache import AppCache
from api.lib.perm.acl.resource import ResourceCRUD
from api.lib.perm.acl.resource import ResourceTypeCRUD
@@ -29,6 +31,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,8 +203,13 @@ def del_user(user):
@click.command()
@with_appcontext
def cmdb_counter():
"""
Dashboard calculations
"""
from api.lib.cmdb.cache import CMDBCounterCache
current_app.test_request_context().push()
login_user(UserCache.get('worker'))
while True:
try:
db.session.remove()
@@ -217,45 +225,89 @@ def cmdb_counter():
@click.command()
@with_appcontext
def cmdb_trigger():
"""
Trigger execution for date attribute
"""
from api.lib.cmdb.ci import CITriggerManager
current_day = datetime.datetime.today().strftime("%Y-%m-%d")
trigger2cis = dict()
trigger2completed = dict()
i = 0
while True:
db.session.remove()
if datetime.datetime.today().strftime("%Y-%m-%d") != current_day:
trigger2cis = dict()
trigger2completed = dict()
current_day = datetime.datetime.today().strftime("%Y-%m-%d")
try:
db.session.remove()
if i == 360 or i == 0:
i = 0
try:
triggers = CITypeTrigger.get_by(to_dict=False)
if datetime.datetime.today().strftime("%Y-%m-%d") != current_day:
trigger2cis = dict()
trigger2completed = dict()
current_day = datetime.datetime.today().strftime("%Y-%m-%d")
if i == 3 or i == 0:
i = 0
triggers = CITypeTrigger.get_by(to_dict=False, __func_isnot__key_attr_id=None)
for trigger in triggers:
ready_cis = CITypeTriggerManager.waiting_cis(trigger)
try:
ready_cis = CITriggerManager.waiting_cis(trigger)
except Exception as e:
print(e)
continue
if trigger.id not in trigger2cis:
trigger2cis[trigger.id] = (trigger, ready_cis)
else:
cur = trigger2cis[trigger.id]
cur_ci_ids = {i.ci_id for i in cur[1]}
trigger2cis[trigger.id] = (trigger, cur[1] + [i for i in ready_cis if i.ci_id not in cur_ci_ids
and i.ci_id not in trigger2completed[trigger.id]])
trigger2cis[trigger.id] = (
trigger, cur[1] + [i for i in ready_cis if i.ci_id not in cur_ci_ids
and i.ci_id not in trigger2completed.get(trigger.id, {})])
except Exception as e:
print(e)
for tid in trigger2cis:
trigger, cis = trigger2cis[tid]
for ci in copy.deepcopy(cis):
if CITriggerManager.trigger_notify(trigger, ci):
trigger2completed.setdefault(trigger.id, set()).add(ci.ci_id)
for tid in trigger2cis:
trigger, cis = trigger2cis[tid]
for ci in copy.deepcopy(cis):
if CITypeTriggerManager.trigger_notify(trigger, ci):
trigger2completed.setdefault(trigger.id, set()).add(ci.ci_id)
for _ci in cis:
if _ci.ci_id == ci.ci_id:
cis.remove(_ci)
for _ci in cis:
if _ci.ci_id == ci.ci_id:
cis.remove(_ci)
i += 1
time.sleep(10)
except Exception as e:
import traceback
print(traceback.format_exc())
current_app.logger.error("cmdb trigger exception: {}".format(e))
time.sleep(60)
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):
@@ -144,12 +161,61 @@ class InitDepartment(object):
info = f"update department acl_rid: {acl_rid}"
current_app.logger.info(info)
def init_backend_resource(self):
acl = self.check_app('backend')
resources_types = acl.get_all_resources_types()
results = list(filter(lambda t: t['name'] == '操作权限', resources_types['groups']))
if len(results) == 0:
payload = dict(
app_id=acl.app_name,
name='操作权限',
description='',
perms=['read', 'grant', 'delete', 'update']
)
resource_type = acl.create_resources_type(payload)
else:
resource_type = results[0]
for name in ['公司信息', '公司架构', '通知设置']:
payload = dict(
type_id=resource_type['id'],
app_id=acl.app_name,
name=name,
)
try:
acl.create_resource(payload)
except Exception as e:
if '已经存在' in str(e):
pass
else:
raise Exception(e)
def check_app(self, app_name):
acl = ACLManager(app_name)
payload = dict(
name=app_name,
description=app_name
)
try:
app = acl.validate_app()
if app:
return acl
acl.create_app(payload)
except Exception as e:
current_app.logger.error(e)
if '不存在' in str(e):
acl.create_app(payload)
return acl
raise Exception(e)
@click.command()
@with_appcontext
def init_import_user_from_acl():
"""
从ACL导入用户
Import users from ACL
"""
InitEmployee().import_user_from_acl()
@@ -158,7 +224,65 @@ def init_import_user_from_acl():
@with_appcontext
def init_department():
"""
初始化 部门
Department initialization
"""
InitDepartment().init()
InitDepartment().create_acl_role_with_department()
cli = InitDepartment()
cli.init_wide_company()
cli.create_acl_role_with_department()
cli.init_backend_resource()
@click.command()
@with_appcontext
def common_check_new_columns():
"""
add new columns to tables
"""
from api.extensions import db
from sqlalchemy import inspect, text
def get_model_by_table_name(table_name):
for model in db.Model.registry._class_registry.values():
if hasattr(model, '__tablename__') and model.__tablename__ == table_name:
return model
return None
def add_new_column(table_name, new_column):
column_type = new_column.type.compile(engine.dialect)
default_value = new_column.default.arg if new_column.default else None
sql = f"ALTER TABLE {table_name} ADD COLUMN {new_column.name} {column_type} "
if new_column.comment:
sql += f" comment '{new_column.comment}'"
if column_type == 'JSON':
pass
elif default_value:
if column_type.startswith('VAR') or column_type.startswith('Text'):
if default_value is None or len(default_value) == 0:
pass
else:
sql += f" DEFAULT {default_value}"
sql = text(sql)
db.session.execute(sql)
engine = db.get_engine()
inspector = inspect(engine)
table_names = inspector.get_table_names()
for table_name in table_names:
existed_columns = inspector.get_columns(table_name)
existed_column_name_list = [c['name'] for c in existed_columns]
model = get_model_by_table_name(table_name)
if model is None:
continue
model_columns = model.__table__.columns._all_columns
for column in model_columns:
if column.name not in existed_column_name_list:
try:
add_new_column(table_name, column)
current_app.logger.info(f"add new column [{column.name}] in table [{table_name}] success.")
except Exception as e:
current_app.logger.error(f"add new column [{column.name}] in table [{table_name}] err:")
current_app.logger.error(e)

View File

@@ -1,15 +1,20 @@
# -*- coding:utf-8 -*-
# -*- coding:utf-8 -*-
import requests
from flask import abort
from flask import current_app
from flask import g
from flask import session
from flask_login import current_user
from api.extensions import db
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import CITypeAttributesCache
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.const import BUILTIN_KEYWORDS
from api.lib.cmdb.const import CITypeOperateType
from api.lib.cmdb.const import ResourceTypeEnum, RoleEnum, PermEnum
from api.lib.cmdb.const import CMDB_QUEUE
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.const import RoleEnum
from api.lib.cmdb.const import ValueTypeEnum
from api.lib.cmdb.history import CITypeHistoryManager
from api.lib.cmdb.resp_format import ErrFormat
@@ -17,6 +22,7 @@ from api.lib.cmdb.utils import ValueTypeMap
from api.lib.decorator import kwargs_required
from api.lib.perm.acl.acl import is_app_admin
from api.lib.perm.acl.acl import validate_permission
from api.lib.webhook import webhook_request
from api.models.cmdb import Attribute
from api.models.cmdb import CIType
from api.models.cmdb import CITypeAttribute
@@ -34,15 +40,11 @@ class AttributeManager(object):
pass
@staticmethod
def _get_choice_values_from_web_hook(choice_web_hook):
url = choice_web_hook.get('url')
ret_key = choice_web_hook.get('ret_key')
headers = choice_web_hook.get('headers') or {}
payload = choice_web_hook.get('payload') or {}
method = choice_web_hook.get('method', 'GET').lower()
def _get_choice_values_from_webhook(choice_webhook, payload=None):
ret_key = choice_webhook.get('ret_key')
try:
res = getattr(requests, method)(url, headers=headers, data=payload).json()
res = webhook_request(choice_webhook, payload or {}).json()
if ret_key:
ret_key_list = ret_key.strip().split("##")
for key in ret_key_list[:-1]:
@@ -54,17 +56,44 @@ 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 []
@staticmethod
def _get_choice_values_from_other_ci(choice_other):
from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci import search
type_ids = choice_other.get('type_ids')
attr_id = choice_other.get('attr_id')
other_filter = choice_other.get('filter') or ''
query = "_type:({}),{}".format(";".join(map(str, type_ids)), other_filter)
s = search(query, fl=[str(attr_id)], facet=[str(attr_id)], count=1)
try:
_, _, _, _, _, facet = s.search()
return [[i[0], {}] for i in (list(facet.values()) or [[]])[0]]
except SearchError as e:
current_app.logger.error("get choice values from other ci failed: {}".format(e))
return []
@classmethod
def get_choice_values(cls, attr_id, value_type, choice_web_hook, choice_web_hook_parse=True):
if choice_web_hook and isinstance(choice_web_hook, dict) and choice_web_hook_parse:
return cls._get_choice_values_from_web_hook(choice_web_hook)
elif choice_web_hook and not choice_web_hook_parse:
return []
def get_choice_values(cls, attr_id, value_type, choice_web_hook, choice_other,
choice_web_hook_parse=True, choice_other_parse=True):
if choice_web_hook:
if choice_web_hook_parse and isinstance(choice_web_hook, dict):
return cls._get_choice_values_from_webhook(choice_web_hook)
else:
return []
elif choice_other:
if choice_other_parse and isinstance(choice_other, dict):
return cls._get_choice_values_from_other_ci(choice_other)
else:
return []
choice_table = ValueTypeMap.choice.get(value_type)
if not choice_table:
return []
choice_values = choice_table.get_by(fl=["value", "option"], attr_id=attr_id)
return [[choice_value['value'], choice_value['option']] for choice_value in choice_values]
@@ -72,34 +101,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 +142,9 @@ 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.get("choice_other"))))
attr['is_choice'] and attr.pop('choice_web_hook', None)
res.append(attr)
@@ -123,30 +153,40 @@ 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"], attr.get("choice_other"))
return attr
def get_attribute_by_alias(self, alias):
attr = Attribute.get_by(alias=alias, first=True)
if attr and attr["is_choice"]:
attr.update(dict(choice_value=self.get_choice_values(
attr["id"], attr["value_type"], attr["choice_web_hook"])))
if attr.get("is_choice"):
attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"],
attr["choice_web_hook"], attr.get("choice_other"))
return attr
def get_attribute_by_id(self, _id):
attr = Attribute.get_by_id(_id).to_dict()
if attr and attr["is_choice"]:
attr.update(dict(choice_value=self.get_choice_values(
attr["id"], attr["value_type"], attr["choice_web_hook"])))
if attr.get("is_choice"):
attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"],
attr["choice_web_hook"], attr.get("choice_other"))
return attr
def get_attribute(self, key, choice_web_hook_parse=True):
def get_attribute(self, key, choice_web_hook_parse=True, choice_other_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"],
attr.get("choice_other"),
choice_web_hook_parse=choice_web_hook_parse,
choice_other_parse=choice_other_parse,
)
return attr
@staticmethod
@@ -154,16 +194,35 @@ 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))
@classmethod
def calc_computed_attribute(cls, attr_id):
"""
calculate computed attribute for all ci
:param attr_id:
:return:
"""
cls.can_create_computed_attribute()
from api.tasks.cmdb import calc_computed_attribute
calc_computed_attribute.apply_async(args=(attr_id, current_user.uid), queue=CMDB_QUEUE)
@classmethod
@kwargs_required("name")
def add(cls, **kwargs):
choice_value = kwargs.pop("choice_value", [])
kwargs.pop("is_choice", None)
is_choice = True if choice_value or kwargs.get('choice_web_hook') else False
is_choice = True if choice_value or kwargs.get('choice_web_hook') or kwargs.get('choice_other') else False
name = kwargs.pop("name")
if name in {'id', '_id', 'ci_id', 'type', '_type', 'ci_type'}:
if name in BUILTIN_KEYWORDS:
return abort(400, ErrFormat.attribute_name_cannot_be_builtin)
if kwargs.get('choice_other'):
if (not isinstance(kwargs['choice_other'], dict) or not kwargs['choice_other'].get('type_ids') or
not kwargs['choice_other'].get('attr_id')):
return abort(400, ErrFormat.attribute_choice_other_invalid)
alias = kwargs.pop("alias", "")
alias = name if not alias else alias
Attribute.get_by(name=name, first=True) and abort(400, ErrFormat.attribute_name_duplicate.format(name))
@@ -177,7 +236,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 +270,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 +285,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 +304,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):
@@ -273,12 +337,17 @@ class AttributeManager(object):
self._change_index(attr, attr.is_index, kwargs['is_index'])
if kwargs.get('choice_other'):
if (not isinstance(kwargs['choice_other'], dict) or not kwargs['choice_other'].get('type_ids') or
not kwargs['choice_other'].get('attr_id')):
return abort(400, ErrFormat.attribute_choice_other_invalid)
existed2 = attr.to_dict()
if not existed2['choice_web_hook'] and existed2['is_choice']:
existed2['choice_value'] = self.get_choice_values(attr.id, attr.value_type, attr.choice_web_hook)
if not existed2['choice_web_hook'] and not existed2.get('choice_other') and existed2['is_choice']:
existed2['choice_value'] = self.get_choice_values(attr.id, attr.value_type, None, None)
choice_value = kwargs.pop("choice_value", False)
is_choice = True if choice_value or kwargs.get('choice_web_hook') else False
is_choice = True if choice_value or kwargs.get('choice_web_hook') or kwargs.get('choice_other') else False
kwargs['is_choice'] = is_choice
if kwargs.get('default') and not (isinstance(kwargs['default'], dict) and 'default' in kwargs['default']):
@@ -290,7 +359,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 +378,8 @@ class AttributeManager(object):
AttributeCache.clean(attr)
self._clean_ci_type_attributes_cache(_id)
return attr.id
@staticmethod
@@ -319,25 +390,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

@@ -2,14 +2,11 @@
from __future__ import unicode_literals
import requests
from flask import current_app
from api.extensions import cache
from api.extensions import db
from api.lib.cmdb.custom_dashboard import CustomDashboardManager
from api.models.cmdb import Attribute
from api.models.cmdb import CI
from api.models.cmdb import CIType
from api.models.cmdb import CITypeAttribute
from api.models.cmdb import RelationType
@@ -34,6 +31,7 @@ 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 +65,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 +97,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 +133,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 +158,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
@@ -201,13 +207,13 @@ class CITypeAttributeCache(object):
@classmethod
def get(cls, type_id, attr_id):
attr = cache.get(cls.PREFIX_ID.format(type_id, attr_id))
attr = attr or cache.get(cls.PREFIX_ID.format(type_id, attr_id))
if not attr:
attr = CITypeAttribute.get_by(type_id=type_id, attr_id=attr_id, first=True, to_dict=False)
if attr is not None:
cls.set(type_id, attr_id, attr)
attr = attr or CITypeAttribute.get_by(type_id=type_id, attr_id=attr_id, first=True, to_dict=False)
if attr is not None:
cls.set(type_id, attr_id, attr)
return attr
@classmethod
@@ -241,53 +247,72 @@ class CMDBCounterCache(object):
result = {}
for custom in customs:
if custom['category'] == 0:
result[custom['id']] = cls.summary_counter(custom['type_id'])
res = cls.sum_counter(custom)
elif custom['category'] == 1:
result[custom['id']] = cls.attribute_counter(custom['type_id'], custom['attr_id'])
elif custom['category'] == 2:
result[custom['id']] = cls.relation_counter(custom['type_id'], custom['level'])
res = cls.attribute_counter(custom)
else:
res = cls.relation_counter(custom.get('type_id'),
custom.get('level'),
custom.get('options', {}).get('filter', ''),
custom.get('options', {}).get('type_ids', ''))
if res:
result[custom['id']] = res
cls.set(result)
return result
@classmethod
def update(cls, custom):
def update(cls, custom, flush=True):
result = cache.get(cls.KEY) or {}
if not result:
result = cls.reset()
if custom['category'] == 0:
result[custom['id']] = cls.summary_counter(custom['type_id'])
res = cls.sum_counter(custom)
elif custom['category'] == 1:
result[custom['id']] = cls.attribute_counter(custom['type_id'], custom['attr_id'])
elif custom['category'] == 2:
result[custom['id']] = cls.relation_counter(custom['type_id'], custom['level'])
res = cls.attribute_counter(custom)
else:
res = cls.relation_counter(custom.get('type_id'),
custom.get('level'),
custom.get('options', {}).get('filter', ''),
custom.get('options', {}).get('type_ids', ''))
cls.set(result)
if res and flush:
result[custom['id']] = res
cls.set(result)
return res
@staticmethod
def summary_counter(type_id):
return db.session.query(CI.id).filter(CI.deleted.is_(False)).filter(CI.type_id == type_id).count()
def relation_counter(type_id, level, other_filer, type_ids):
from api.lib.cmdb.search.ci_relation.search import Search as RelSearch
from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci import search
@staticmethod
def relation_counter(type_id, level):
query = "_type:{}".format(type_id)
s = search(query, count=1000000)
try:
type_names, _, _, _, _, _ = s.search()
except SearchError as e:
current_app.logger.error(e)
return
uri = current_app.config.get('CMDB_API')
type_names = requests.get("{}/ci/s?q=_type:{}&count=10000".format(uri, type_id)).json().get('result')
type_id_names = [(str(i.get('_id')), i.get(i.get('unique'))) for i in type_names]
url = "{}/ci_relations/statistics?root_ids={}&level={}".format(
uri, ','.join([i[0] for i in type_id_names]), level)
stats = requests.get(url).json()
s = RelSearch([i[0] for i in type_id_names], level, other_filer or '')
try:
stats = s.statistics(type_ids)
except SearchError as e:
current_app.logger.error(e)
return
id2name = dict(type_id_names)
type_ids = set()
for i in (stats.get('detail') or []):
for j in stats['detail'][i]:
type_ids.add(j)
for type_id in type_ids:
_type = CITypeCache.get(type_id)
id2name[type_id] = _type and _type.alias
@@ -307,9 +332,100 @@ class CMDBCounterCache(object):
return result
@staticmethod
def attribute_counter(type_id, attr_id):
uri = current_app.config.get('CMDB_API')
url = "{}/ci/s?q=_type:{}&fl={}&facet={}".format(uri, type_id, attr_id, attr_id)
res = requests.get(url).json()
if res.get('facet'):
return dict([i[:2] for i in list(res.get('facet').values())[0]])
def attribute_counter(custom):
from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci import search
from api.lib.cmdb.utils import ValueTypeMap
custom.setdefault('options', {})
type_id = custom.get('type_id')
attr_id = custom.get('attr_id')
type_ids = custom['options'].get('type_ids') or (type_id and [type_id])
attr_ids = list(map(str, custom['options'].get('attr_ids') or (attr_id and [attr_id])))
try:
attr2value_type = [AttributeCache.get(i).value_type for i in attr_ids]
except AttributeError:
return
other_filter = custom['options'].get('filter')
other_filter = "{}".format(other_filter) if other_filter else ''
if custom['options'].get('ret') == 'cis':
query = "_type:({}),{}".format(";".join(map(str, type_ids)), other_filter)
s = search(query, fl=attr_ids, ret_key='alias', count=100)
try:
cis, _, _, _, _, _ = s.search()
except SearchError as e:
current_app.logger.error(e)
return
return cis
result = dict()
# level = 1
query = "_type:({}),{}".format(";".join(map(str, type_ids)), other_filter)
s = search(query, fl=attr_ids, facet=[attr_ids[0]], count=1)
try:
_, _, _, _, _, facet = s.search()
except SearchError as e:
current_app.logger.error(e)
return
for i in (list(facet.values()) or [[]])[0]:
result[ValueTypeMap.serialize2[attr2value_type[0]](str(i[0]))] = i[1]
if len(attr_ids) == 1:
return result
# level = 2
for v in result:
query = "_type:({}),{},{}:{}".format(";".join(map(str, type_ids)), other_filter, attr_ids[0], v)
s = search(query, fl=attr_ids, facet=[attr_ids[1]], count=1)
try:
_, _, _, _, _, facet = s.search()
except SearchError as e:
current_app.logger.error(e)
return
result[v] = dict()
for i in (list(facet.values()) or [[]])[0]:
result[v][ValueTypeMap.serialize2[attr2value_type[1]](str(i[0]))] = i[1]
if len(attr_ids) == 2:
return result
# level = 3
for v1 in result:
if not isinstance(result[v1], dict):
continue
for v2 in result[v1]:
query = "_type:({}),{},{}:{},{}:{}".format(";".join(map(str, type_ids)), other_filter,
attr_ids[0], v1, attr_ids[1], v2)
s = search(query, fl=attr_ids, facet=[attr_ids[2]], count=1)
try:
_, _, _, _, _, facet = s.search()
except SearchError as e:
current_app.logger.error(e)
return
result[v1][v2] = dict()
for i in (list(facet.values()) or [[]])[0]:
result[v1][v2][ValueTypeMap.serialize2[attr2value_type[2]](str(i[0]))] = i[1]
return result
@staticmethod
def sum_counter(custom):
from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci import search
custom.setdefault('options', {})
type_id = custom.get('type_id')
type_ids = custom['options'].get('type_ids') or (type_id and [type_id])
other_filter = custom['options'].get('filter') or ''
query = "_type:({}),{}".format(";".join(map(str, type_ids)), other_filter)
s = search(query, count=1)
try:
_, _, _, _, numfound, _ = s.search()
except SearchError as e:
current_app.logger.error(e)
return
return numfound

View File

@@ -4,10 +4,11 @@
import copy
import datetime
import json
import threading
from flask import abort
from flask import current_app
from flask import g
from flask_login import current_user
from werkzeug.exceptions import BadRequest
from api.extensions import db
@@ -24,31 +25,42 @@ 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
from api.lib.cmdb.const import REDIS_PREFIX_CI
from api.lib.cmdb.const import ResourceTypeEnum, PermEnum
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.const import RetKey
from api.lib.cmdb.history import AttributeHistoryManger
from api.lib.cmdb.history import CIRelationHistoryManager
from api.lib.cmdb.history import CITriggerHistoryManager
from api.lib.cmdb.perms import CIFilterPermsCRUD
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.utils import TableMap
from api.lib.cmdb.utils import ValueTypeMap
from api.lib.cmdb.value import AttributeValueManager
from api.lib.decorator import kwargs_required
from api.lib.notify import notify_send
from api.lib.perm.acl.acl import ACLManager
from api.lib.perm.acl.acl import is_app_admin
from api.lib.perm.acl.acl import validate_permission
from api.lib.utils import Lock
from api.lib.utils import handle_arg_list
from api.lib.webhook import webhook_request
from api.models.cmdb import AttributeHistory
from api.models.cmdb import AutoDiscoveryCI
from api.models.cmdb import CI
from api.models.cmdb import CIRelation
from api.models.cmdb import CITypeAttribute
from api.models.cmdb import CITypeRelation
from api.models.cmdb import CITypeTrigger
from api.tasks.cmdb import ci_cache
from api.tasks.cmdb import ci_delete
from api.tasks.cmdb import ci_delete_trigger
from api.tasks.cmdb import ci_relation_add
from api.tasks.cmdb import ci_relation_cache
from api.tasks.cmdb import ci_relation_delete
PRIVILEGED_USERS = {"worker", "cmdb_agent", "agent"}
class CIManager(object):
""" manage CI interface
@@ -64,11 +76,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 +104,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 +171,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 +254,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 +299,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 +314,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 +323,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 +337,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 +367,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}
@@ -378,16 +386,20 @@ class CIManager(object):
key2attr = value_manager.valid_attr_value(ci_dict, ci_type.id, ci and ci.id,
ci_type_attrs_name, ci_type_attrs_alias, ci_attr2type_attr)
operate_type = OperateType.UPDATE if ci is not None else OperateType.ADD
try:
ci = ci or CI.create(type_id=ci_type.id, is_auto_discovery=is_auto_discovery)
record_id = value_manager.create_or_update_attr_value2(ci, ci_dict, key2attr)
record_id = value_manager.create_or_update_attr_value(ci, ci_dict, key2attr)
except BadRequest as e:
if existed is None:
cls.delete(ci.id)
raise e
if record_id: # has change
ci_cache.apply_async([ci.id], queue=CMDB_QUEUE)
ci_cache.apply_async(args=(ci.id, operate_type, record_id), queue=CMDB_QUEUE)
if ref_ci_dict: # add relations
ci_relation_add.apply_async(args=(ref_ci_dict, ci.id, current_user.uid), queue=CMDB_QUEUE)
return ci.id
@@ -411,7 +423,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)
@@ -424,20 +436,25 @@ class CIManager(object):
return abort(403, ErrFormat.ci_filter_perm_attr_no_permission.format(k))
try:
record_id = value_manager.create_or_update_attr_value2(ci, ci_dict, key2attr)
record_id = value_manager.create_or_update_attr_value(ci, ci_dict, key2attr)
except BadRequest as e:
raise e
if record_id: # has change
ci_cache.apply_async([ci_id], queue=CMDB_QUEUE)
ci_cache.apply_async(args=(ci_id, OperateType.UPDATE, record_id), queue=CMDB_QUEUE)
ref_ci_dict = {k: v for k, v in ci_dict.items() if k.startswith("$") and "." in k}
if ref_ci_dict:
ci_relation_add.apply_async(args=(ref_ci_dict, ci.id), queue=CMDB_QUEUE)
@staticmethod
def update_unique_value(ci_id, unique_name, unique_value):
ci = CI.get_by_id(ci_id) or abort(404, ErrFormat.ci_not_found.format("id={}".format(ci_id)))
AttributeValueManager().create_or_update_attr_value(unique_name, unique_value, ci)
key2attr = {unique_name: AttributeCache.get(unique_name)}
record_id = AttributeValueManager().create_or_update_attr_value(ci, {unique_name: unique_value}, key2attr)
ci_cache.apply_async([ci_id], queue=CMDB_QUEUE)
ci_cache.apply_async(args=(ci_id, OperateType.UPDATE, record_id), queue=CMDB_QUEUE)
@classmethod
def delete(cls, ci_id):
@@ -448,26 +465,42 @@ class CIManager(object):
ci_dict = cls.get_cis_by_ids([ci_id])
ci_dict = ci_dict and ci_dict[0]
triggers = CITriggerManager.get(ci_dict['_type'])
for trigger in triggers:
option = trigger['option']
if not option.get('enable') or option.get('action') != OperateType.DELETE:
continue
if option.get('filter') and not CITriggerManager.ci_filter(ci_dict.get('_id'), option['filter']):
continue
ci_delete_trigger.apply_async(args=(trigger, OperateType.DELETE, ci_dict), queue=CMDB_QUEUE)
attrs = CITypeAttribute.get_by(type_id=ci.type_id, to_dict=False)
attr_names = set([AttributeCache.get(attr.attr_id).name for attr in attrs])
for attr_name in attr_names:
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)
ci_delete.apply_async([ci.id], queue=CMDB_QUEUE)
ci_delete.apply_async(args=(ci_id,), queue=CMDB_QUEUE)
return ci_id
@@ -478,11 +511,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 +558,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 +678,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 +704,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 +776,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 +836,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 +894,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 +913,184 @@ 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)
class CITriggerManager(object):
@staticmethod
def get(type_id):
db.session.remove()
return CITypeTrigger.get_by(type_id=type_id, to_dict=True)
@staticmethod
def _update_old_attr_value(record_id, ci_dict):
attr_history = AttributeHistory.get_by(record_id=record_id, to_dict=False)
attr_dict = dict()
for attr_h in attr_history:
attr_dict['old_{}'.format(AttributeCache.get(attr_h.attr_id).name)] = attr_h.old
ci_dict.update({'old_{}'.format(k): ci_dict[k] for k in ci_dict})
ci_dict.update(attr_dict)
@classmethod
def _exec_webhook(cls, operate_type, webhook, ci_dict, trigger_id, trigger_name, record_id, ci_id=None, app=None):
app = app or current_app
with app.app_context():
if operate_type == OperateType.UPDATE:
cls._update_old_attr_value(record_id, ci_dict)
if ci_id is not None:
ci_dict = CIManager().get_ci_by_id_from_db(ci_id, need_children=False, use_master=False)
try:
response = webhook_request(webhook, ci_dict).text
is_ok = True
except Exception as e:
current_app.logger.warning("exec webhook failed: {}".format(e))
response = e
is_ok = False
CITriggerHistoryManager.add(operate_type,
record_id,
ci_dict.get('_id'),
trigger_id,
trigger_name,
is_ok=is_ok,
webhook=response)
return is_ok
@classmethod
def _exec_notify(cls, operate_type, notify, ci_dict, trigger_id, trigger_name, record_id, ci_id=None, app=None):
app = app or current_app
with app.app_context():
if ci_id is not None:
ci_dict = CIManager().get_ci_by_id_from_db(ci_id, need_children=False, use_master=False)
if operate_type == OperateType.UPDATE:
cls._update_old_attr_value(record_id, ci_dict)
is_ok = True
response = ''
for method in (notify.get('method') or []):
try:
res = notify_send(notify.get('subject'), notify.get('body'), [method],
notify.get('tos'), ci_dict)
response = "{}\n{}".format(response, res)
except Exception as e:
current_app.logger.warning("send notify failed: {}".format(e))
response = "{}\n{}".format(response, e)
is_ok = False
CITriggerHistoryManager.add(operate_type,
record_id,
ci_dict.get('_id'),
trigger_id,
trigger_name,
is_ok=is_ok,
notify=response.strip())
return is_ok
@staticmethod
def ci_filter(ci_id, other_filter):
from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci import search
query = "{},_id:{}".format(other_filter, ci_id)
try:
_, _, _, _, numfound, _ = search(query).search()
return numfound
except SearchError as e:
current_app.logger.warning("ci search failed: {}".format(e))
@classmethod
def fire(cls, operate_type, ci_dict, record_id):
type_id = ci_dict.get('_type')
triggers = cls.get(type_id) or []
for trigger in triggers:
option = trigger['option']
if not option.get('enable'):
continue
if option.get('filter') and not cls.ci_filter(ci_dict.get('_id'), option['filter']):
continue
if option.get('attr_ids') and isinstance(option['attr_ids'], list):
if not (set(option['attr_ids']) &
set([i.attr_id for i in AttributeHistory.get_by(record_id=record_id, to_dict=False)])):
continue
if option.get('action') == operate_type:
cls.fire_by_trigger(trigger, operate_type, ci_dict, record_id)
@classmethod
def fire_by_trigger(cls, trigger, operate_type, ci_dict, record_id=None):
option = trigger['option']
if option.get('webhooks'):
cls._exec_webhook(operate_type, option['webhooks'], ci_dict, trigger['id'],
option.get('name'), record_id)
elif option.get('notifies'):
cls._exec_notify(operate_type, option['notifies'], ci_dict, trigger['id'],
option.get('name'), record_id)
@classmethod
def waiting_cis(cls, trigger):
now = datetime.datetime.today()
config = trigger.option.get('notifies') or {}
delta_time = datetime.timedelta(days=(config.get('before_days', 0) or 0))
attr = AttributeCache.get(trigger.attr_id)
value_table = TableMap(attr=attr).table
values = value_table.get_by(attr_id=attr.id, to_dict=False)
result = []
for v in values:
if (isinstance(v.value, (datetime.date, datetime.datetime)) and
(v.value - delta_time).strftime('%Y%m%d') == now.strftime("%Y%m%d")):
if trigger.option.get('filter') and not cls.ci_filter(v.ci_id, trigger.option['filter']):
continue
result.append(v)
return result
@classmethod
def trigger_notify(cls, trigger, ci):
"""
only for date attribute
:param trigger:
:param ci:
:return:
"""
if (trigger.option.get('notifies', {}).get('notify_at') == datetime.datetime.now().strftime("%H:%M") or
not trigger.option.get('notifies', {}).get('notify_at')):
if trigger.option.get('webhooks'):
threading.Thread(target=cls._exec_webhook, args=(
None, trigger.option['webhooks'], None, trigger.id, trigger.option.get('name'), None, ci.ci_id,
current_app._get_current_object())).start()
elif trigger.option.get('notifies'):
threading.Thread(target=cls._exec_notify, args=(
None, trigger.option['notifies'], None, trigger.id, trigger.option.get('name'), None, ci.ci_id,
current_app._get_current_object())).start()
return True
return False

View File

@@ -1,11 +1,12 @@
# -*- coding:utf-8 -*-
# -*- coding:utf-8 -*-
import copy
import datetime
import toposort
from flask import abort
from flask import current_app
from flask import g
from flask_login import current_user
from toposort import toposort_flatten
from api.extensions import db
from api.lib.cmdb.attribute import AttributeManager
@@ -16,18 +17,22 @@ 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
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.utils import TableMap
from api.lib.cmdb.value import AttributeValueManager
from api.lib.decorator import kwargs_required
from api.lib.perm.acl.acl import ACLManager
from api.lib.perm.acl.acl import is_app_admin
from api.models.cmdb import Attribute
from api.models.cmdb import AutoDiscoveryCI
from api.models.cmdb import AutoDiscoveryCIType
from api.models.cmdb import CI
from api.models.cmdb import CIFilterPerms
from api.models.cmdb import CIType
from api.models.cmdb import CITypeAttribute
from api.models.cmdb import CITypeAttributeGroup
@@ -37,6 +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,11 +113,8 @@ 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 = kwargs.pop("unique_key", None) or kwargs.pop("unique_id", None)
unique_key = AttributeCache.get(unique_key) or abort(404, ErrFormat.unique_key_not_define)
kwargs["alias"] = kwargs["name"] if not kwargs.get("alias") else kwargs["alias"]
@@ -117,7 +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,16 +269,17 @@ 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
def update(gid, name, type_ids):
"""
update part
:param gid:
:param name:
:param type_ids:
:return:
:param gid:
:param name:
:param type_ids:
:return:
"""
existed = CITypeGroup.get_by_id(gid) or abort(
404, ErrFormat.ci_type_group_not_found.format("id={}".format(gid)))
@@ -320,28 +336,63 @@ class CITypeAttributeManager(object):
def __init__(self):
pass
@staticmethod
def get_attr_name(ci_type_name, key):
ci_type = CITypeCache.get(ci_type_name)
if ci_type is None:
return
for i in CITypeAttributesCache.get(ci_type.id):
attr = AttributeCache.get(i.attr_id)
if attr and (attr.name == key or attr.alias == key):
return attr.name
@staticmethod
def get_attr_names_by_type_id(type_id):
return [AttributeCache.get(attr.attr_id).name for attr in CITypeAttributesCache.get(type_id)]
@staticmethod
def get_attributes_by_type_id(type_id, choice_web_hook_parse=True):
def get_attributes_by_type_id(type_id, choice_web_hook_parse=True, choice_other_parse=True):
has_config_perm = ACLManager('cmdb').has_permission(
CITypeManager.get_name_by_id(type_id), ResourceTypeEnum.CI, PermEnum.CONFIG)
attrs = CITypeAttributesCache.get(type_id)
result = list()
for attr in sorted(attrs, key=lambda x: (x.order, x.id)):
attr_dict = AttributeManager().get_attribute(attr.attr_id, choice_web_hook_parse)
attr_dict = AttributeManager().get_attribute(attr.attr_id, choice_web_hook_parse, choice_other_parse)
attr_dict["is_required"] = attr.is_required
attr_dict["order"] = attr.order
attr_dict["default_show"] = attr.default_show
if not has_config_perm:
attr_dict.pop('choice_web_hook', None)
attr_dict.pop('choice_other', None)
result.append(attr_dict)
return result
@staticmethod
def get_common_attributes(type_ids):
has_config_perm = False
for type_id in type_ids:
has_config_perm |= ACLManager('cmdb').has_permission(
CITypeManager.get_name_by_id(type_id), ResourceTypeEnum.CI, PermEnum.CONFIG)
result = CITypeAttribute.get_by(__func_in___key_type_id=list(map(int, type_ids)), to_dict=False)
attr2types = {}
for i in result:
attr2types.setdefault(i.attr_id, []).append(i.type_id)
attrs = []
for attr_id in attr2types:
if len(attr2types[attr_id]) == len(type_ids):
attr = AttributeManager().get_attribute_by_id(attr_id)
if not has_config_perm:
attr.pop('choice_web_hook', None)
attrs.append(attr)
return attrs
@staticmethod
def _check(type_id, attr_ids):
ci_type = CITypeManager.check_is_existed(type_id)
@@ -358,10 +409,10 @@ class CITypeAttributeManager(object):
def add(cls, type_id, attr_ids=None, **kwargs):
"""
add attributes to CIType
:param type_id:
:param type_id:
:param attr_ids: list
:param kwargs:
:return:
:param kwargs:
:return:
"""
attr_ids = list(set(attr_ids))
@@ -388,9 +439,9 @@ class CITypeAttributeManager(object):
def update(cls, type_id, attributes):
"""
update attributes to CIType
:param type_id:
:param type_id:
:param attributes: list
:return:
:return:
"""
cls._check(type_id, [i.get('attr_id') for i in attributes])
@@ -418,9 +469,9 @@ class CITypeAttributeManager(object):
def delete(cls, type_id, attr_ids=None):
"""
delete attributes from CIType
:param type_id:
:param type_id:
:param attr_ids: list
:return:
:return:
"""
from api.tasks.cmdb import ci_cache
@@ -449,7 +500,7 @@ class CITypeAttributeManager(object):
for ci in CI.get_by(type_id=type_id, to_dict=False):
AttributeValueManager.delete_attr_value(attr_id, ci.id)
ci_cache.apply_async([ci.id], queue=CMDB_QUEUE)
ci_cache.apply_async(args=(ci.id, None, None), queue=CMDB_QUEUE)
CITypeAttributeCache.clean(type_id, attr_id)
@@ -482,7 +533,7 @@ class CITypeAttributeManager(object):
CITypeAttributesCache.clean(type_id)
from api.tasks.cmdb import ci_type_attribute_order_rebuild
ci_type_attribute_order_rebuild.apply_async(args=(type_id,), queue=CMDB_QUEUE)
ci_type_attribute_order_rebuild.apply_async(args=(type_id, current_user.uid), queue=CMDB_QUEUE)
class CITypeRelationManager(object):
@@ -527,6 +578,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
@@ -535,6 +587,23 @@ class CITypeRelationManager(object):
return [cls._wrap_relation_type_dict(child.child_id, child) for child in children]
@classmethod
def recursive_level2children(cls, parent_id):
result = dict()
def get_children(_id, level):
children = CITypeRelation.get_by(parent_id=_id, to_dict=False)
if children:
result.setdefault(level + 1, []).extend([i.child.to_dict() for i in children])
for i in children:
if i.child_id != _id:
get_children(i.child_id, level + 1)
get_children(parent_id, 0)
return result
@classmethod
def get_parents(cls, child_id):
parents = CITypeRelation.get_by(child_id=child_id, to_dict=False)
@@ -557,6 +626,17 @@ class CITypeRelationManager(object):
p = CITypeManager.check_is_existed(parent)
c = CITypeManager.check_is_existed(child)
rels = {}
for i in CITypeRelation.get_by(to_dict=False):
rels.setdefault(i.child_id, set()).add(i.parent_id)
rels.setdefault(c.id, set()).add(p.id)
try:
toposort_flatten(rels)
except toposort.CircularDependencyError as e:
current_app.logger.warning(str(e))
return abort(400, ErrFormat.circular_dependency_error)
existed = cls._get(p.id, c.id)
if existed is not None:
existed.update(relation_type_id=relation_type_id,
@@ -575,7 +655,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 +665,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 +720,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 +761,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)
@@ -777,7 +858,7 @@ class CITypeAttributeGroupManager(object):
CITypeAttributesCache.clean(type_id)
from api.tasks.cmdb import ci_type_attribute_order_rebuild
ci_type_attribute_order_rebuild.apply_async(args=(type_id,), queue=CMDB_QUEUE)
ci_type_attribute_order_rebuild.apply_async(args=(type_id, current_user.uid), queue=CMDB_QUEUE)
class CITypeTemplateManager(object):
@@ -793,6 +874,12 @@ class CITypeTemplateManager(object):
for added_id in set(id2obj_dicts.keys()) - set(existed_ids):
if cls == CIType:
CITypeManager.add(**id2obj_dicts[added_id])
elif cls == CITypeRelation:
CITypeRelationManager.add(id2obj_dicts[added_id].get('parent_id'),
id2obj_dicts[added_id].get('child_id'),
id2obj_dicts[added_id].get('relation_type_id'),
id2obj_dicts[added_id].get('constraint'),
)
else:
cls.create(flush=True, **id2obj_dicts[added_id])
@@ -809,7 +896,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 +1034,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
@@ -1016,7 +1103,7 @@ class CITypeTemplateManager(object):
for ci_type in tpt['ci_types']:
tpt['type2attributes'][ci_type['id']] = CITypeAttributeManager.get_attributes_by_type_id(
ci_type['id'], choice_web_hook_parse=False)
ci_type['id'], choice_web_hook_parse=False, choice_other_parse=False)
tpt['type2attribute_group'][ci_type['id']] = CITypeAttributeGroupManager.get_by_type_id(ci_type['id'])
@@ -1090,16 +1177,18 @@ class CITypeUniqueConstraintManager(object):
class CITypeTriggerManager(object):
@staticmethod
def get(type_id):
return CITypeTrigger.get_by(type_id=type_id, to_dict=True)
def get(type_id, to_dict=True):
return CITypeTrigger.get_by(type_id=type_id, to_dict=to_dict)
@staticmethod
def add(type_id, attr_id, notify):
CITypeTrigger.get_by(type_id=type_id, attr_id=attr_id) and abort(400, ErrFormat.ci_type_trigger_duplicate)
def add(type_id, attr_id, option):
for i in CITypeTrigger.get_by(type_id=type_id, attr_id=attr_id, to_dict=False):
if i.option == option:
return abort(400, ErrFormat.ci_type_trigger_duplicate)
not isinstance(notify, dict) and abort(400, ErrFormat.argument_invalid.format("notify"))
not isinstance(option, dict) and abort(400, ErrFormat.argument_invalid.format("option"))
trigger = CITypeTrigger.create(type_id=type_id, attr_id=attr_id, notify=notify)
trigger = CITypeTrigger.create(type_id=type_id, attr_id=attr_id, option=option)
CITypeHistoryManager.add(CITypeOperateType.ADD_TRIGGER,
type_id,
@@ -1109,12 +1198,12 @@ class CITypeTriggerManager(object):
return trigger.to_dict()
@staticmethod
def update(_id, notify):
existed = CITypeTrigger.get_by_id(_id) or \
abort(404, ErrFormat.ci_type_trigger_not_found.format("id={}".format(_id)))
def update(_id, attr_id, option):
existed = (CITypeTrigger.get_by_id(_id) or
abort(404, ErrFormat.ci_type_trigger_not_found.format("id={}".format(_id))))
existed2 = existed.to_dict()
new = existed.update(notify=notify)
new = existed.update(attr_id=attr_id or None, option=option, filter_none=False)
CITypeHistoryManager.add(CITypeOperateType.UPDATE_TRIGGER,
existed.type_id,
@@ -1125,8 +1214,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()
@@ -1134,35 +1223,3 @@ class CITypeTriggerManager(object):
existed.type_id,
trigger_id=_id,
change=existed.to_dict())
@staticmethod
def waiting_cis(trigger):
now = datetime.datetime.today()
delta_time = datetime.timedelta(days=(trigger.notify.get('before_days', 0) or 0))
attr = AttributeCache.get(trigger.attr_id)
value_table = TableMap(attr=attr).table
values = value_table.get_by(attr_id=attr.id, to_dict=False)
result = []
for v in values:
if isinstance(v.value, (datetime.date, datetime.datetime)) and \
(v.value - delta_time).strftime('%Y%m%d') == now.strftime("%Y%m%d"):
result.append(v)
return result
@staticmethod
def trigger_notify(trigger, ci):
if trigger.notify.get('notify_at') == datetime.datetime.now().strftime("%H:%M") or \
not trigger.notify.get('notify_at'):
from api.tasks.cmdb import trigger_notify
trigger_notify.apply_async(args=(trigger.notify, ci.ci_id), queue=CMDB_QUEUE)
return True
return False

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

@@ -14,6 +14,14 @@ class CustomDashboardManager(object):
def get():
return sorted(CustomDashboard.get_by(to_dict=True), key=lambda x: (x["category"], x['order']))
@staticmethod
def preview(**kwargs):
from api.lib.cmdb.cache import CMDBCounterCache
res = CMDBCounterCache.update(kwargs, flush=False)
return res
@staticmethod
def add(**kwargs):
from api.lib.cmdb.cache import CMDBCounterCache
@@ -23,9 +31,9 @@ class CustomDashboardManager(object):
new = CustomDashboard.create(**kwargs)
CMDBCounterCache.update(new.to_dict())
res = CMDBCounterCache.update(new.to_dict())
return new
return new, res
@staticmethod
def update(_id, **kwargs):
@@ -35,9 +43,9 @@ class CustomDashboardManager(object):
new = existed.update(**kwargs)
CMDBCounterCache.update(new.to_dict())
res = CMDBCounterCache.update(new.to_dict())
return new
return new, res
@staticmethod
def batch_update(id2options):

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
@@ -16,6 +16,7 @@ from api.lib.perm.acl.cache import UserCache
from api.models.cmdb import Attribute
from api.models.cmdb import AttributeHistory
from api.models.cmdb import CIRelationHistory
from api.models.cmdb import CITriggerHistory
from api.models.cmdb import CITypeHistory
from api.models.cmdb import CITypeTrigger
from api.models.cmdb import CITypeUniqueConstraint
@@ -176,8 +177,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 +202,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 +221,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,10 +280,75 @@ 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,
change=change)
CITypeHistory.create(**payload)
class CITriggerHistoryManager(object):
@staticmethod
def get(page, page_size, type_id=None, trigger_id=None, operate_type=None):
query = CITriggerHistory.get_by(only_query=True)
if type_id:
query = query.filter(CITriggerHistory.type_id == type_id)
if trigger_id:
query = query.filter(CITriggerHistory.trigger_id == trigger_id)
if operate_type:
query = query.filter(CITriggerHistory.operate_type == operate_type)
numfound = query.count()
query = query.order_by(CITriggerHistory.id.desc())
result = query.offset((page - 1) * page_size).limit(page_size)
result = [i.to_dict() for i in result]
for res in result:
if res.get('trigger_id'):
trigger = CITypeTrigger.get_by_id(res['trigger_id'])
res['trigger'] = trigger and trigger.to_dict()
return numfound, result
@staticmethod
def get_by_ci_id(ci_id):
res = db.session.query(CITriggerHistory, CITypeTrigger).join(
CITypeTrigger, CITypeTrigger.id == CITriggerHistory.trigger_id).filter(
CITriggerHistory.ci_id == ci_id).order_by(CITriggerHistory.id.desc())
result = []
id2trigger = dict()
for i in res:
hist = i.CITriggerHistory
item = dict(is_ok=hist.is_ok,
operate_type=hist.operate_type,
notify=hist.notify,
trigger_id=hist.trigger_id,
trigger_name=hist.trigger_name,
webhook=hist.webhook,
created_at=hist.created_at.strftime('%Y-%m-%d %H:%M:%S'),
record_id=hist.record_id,
hid=hist.id
)
if i.CITypeTrigger.id not in id2trigger:
id2trigger[i.CITypeTrigger.id] = i.CITypeTrigger.to_dict()
result.append(item)
return dict(items=result, id2trigger=id2trigger)
@staticmethod
def add(operate_type, record_id, ci_id, trigger_id, trigger_name, is_ok=False, notify=None, webhook=None):
CITriggerHistory.create(operate_type=operate_type,
record_id=record_id,
ci_id=ci_id,
trigger_id=trigger_id,
trigger_name=trigger_name,
is_ok=is_ok,
notify=notify,
webhook=webhook)

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()
@@ -114,13 +116,13 @@ class PreferenceManager(object):
for i in result:
if i["is_choice"]:
i.update(dict(choice_value=AttributeManager.get_choice_values(
i["id"], i["value_type"], i["choice_web_hook"])))
i["id"], i["value_type"], i["choice_web_hook"], i.get("choice_other"))))
return is_subscribed, result
@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

@@ -42,7 +42,7 @@ FACET_QUERY1 = """
FACET_QUERY = """
SELECT {0}.value,
count({0}.ci_id)
count(distinct({0}.ci_id))
FROM {0}
INNER JOIN ({1}) AS F ON F.ci_id={0}.ci_id
WHERE {0}.attr_id={2:d}

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,8 +21,9 @@ 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"
attribute_choice_other_invalid = "预定义值: 其他模型请求参数不合法!"
ci_not_found = "CI {} 不存在"
unique_constraint = "多属性联合唯一校验不通过: {}"
@@ -37,6 +39,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

@@ -1,4 +1,4 @@
# -*- coding:utf-8 -*-
# -*- coding:utf-8 -*-
from __future__ import unicode_literals
@@ -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)
@@ -140,6 +141,10 @@ class Search(object):
@staticmethod
def _in_query_handler(attr, v, is_not):
new_v = v[1:-1].split(";")
if attr.value_type == ValueTypeEnum.DATE:
new_v = ["{} 00:00:00".format(i) for i in new_v if len(i) == 10]
table_name = TableMap(attr=attr).table_name
in_query = " OR {0}.value ".format(table_name).join(['{0} "{1}"'.format(
"NOT LIKE" if is_not else "LIKE",
@@ -150,6 +155,11 @@ class Search(object):
@staticmethod
def _range_query_handler(attr, v, is_not):
start, end = [x.strip() for x in v[1:-1].split("_TO_")]
if attr.value_type == ValueTypeEnum.DATE:
start = "{} 00:00:00".format(start) if len(start) == 10 else start
end = "{} 00:00:00".format(end) if len(end) == 10 else end
table_name = TableMap(attr=attr).table_name
range_query = "{0} '{1}' AND '{2}'".format(
"NOT BETWEEN" if is_not else "BETWEEN",
@@ -161,8 +171,14 @@ class Search(object):
def _comparison_query_handler(attr, v):
table_name = TableMap(attr=attr).table_name
if v.startswith(">=") or v.startswith("<="):
if attr.value_type == ValueTypeEnum.DATE and len(v[2:]) == 10:
v = "{} 00:00:00".format(v)
comparison_query = "{0} '{1}'".format(v[:2], v[2:].replace("*", "%"))
else:
if attr.value_type == ValueTypeEnum.DATE and len(v[1:]) == 10:
v = "{} 00:00:00".format(v)
comparison_query = "{0} '{1}'".format(v[0], v[1:].replace("*", "%"))
_query_sql = QUERY_CI_BY_ATTR_NAME.format(table_name, attr.id, comparison_query)
return _query_sql
@@ -238,16 +254,14 @@ class Search(object):
attr_id = attr.id
table_name = TableMap(attr=attr).table_name
_v_query_sql = """SELECT {0}.ci_id, {1}.value
_v_query_sql = """SELECT {0}.ci_id, {1}.value
FROM ({2}) AS {0} INNER JOIN {1} ON {1}.ci_id = {0}.ci_id
WHERE {1}.attr_id = {3}""".format("ALIAS", table_name, query_sql, attr_id)
new_table = _v_query_sql
if self.only_type_query or not self.type_id_list:
return "SELECT SQL_CALC_FOUND_ROWS DISTINCT C.ci_id " \
"FROM ({0}) AS C " \
"ORDER BY C.value {2} " \
"LIMIT {1:d}, {3};".format(new_table, (self.page - 1) * self.count, sort_type, self.count)
return ("SELECT SQL_CALC_FOUND_ROWS DISTINCT C.ci_id FROM ({0}) AS C ORDER BY C.value {2} "
"LIMIT {1:d}, {3};".format(new_table, (self.page - 1) * self.count, sort_type, self.count))
elif self.type_id_list:
self.query_sql = """SELECT C.ci_id
@@ -286,7 +300,7 @@ class Search(object):
query_sql = "SELECT * FROM ({0}) as {1} UNION ALL ({2})".format(query_sql, alias, _query_sql)
elif operator == "~":
query_sql = """SELECT * FROM ({0}) as {1} LEFT JOIN ({2}) as {3} USING(ci_id)
query_sql = """SELECT * FROM ({0}) as {1} LEFT JOIN ({2}) as {3} USING(ci_id)
WHERE {3}.ci_id is NULL""".format(query_sql, alias, _query_sql, alias + "A")
return query_sql
@@ -296,7 +310,7 @@ class Search(object):
start = time.time()
execute = db.session.execute
current_app.logger.debug(v_query_sql)
# current_app.logger.debug(v_query_sql)
res = execute(v_query_sql).fetchall()
end_time = time.time()
current_app.logger.debug("query ci ids time is: {0}".format(end_time - start))
@@ -355,7 +369,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:
@@ -392,6 +406,9 @@ class Search(object):
is_not = True if operator == "|~" else False
if field_type == ValueTypeEnum.DATE and len(v) == 10:
v = "{} 00:00:00".format(v)
# in query
if v.startswith("(") and v.endswith(")"):
_query_sql = self._in_query_handler(attr, v, is_not)
@@ -507,7 +524,7 @@ class Search(object):
if k:
table_name = TableMap(attr=attr).table_name
query_sql = FACET_QUERY.format(table_name, self.query_sql, attr.id)
# current_app.logger.debug(query_sql)
# current_app.logger.warning(query_sql)
result = db.session.execute(query_sql).fetchall()
facet[k] = result

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

@@ -18,7 +18,6 @@ from api.extensions import db
from api.lib.cmdb.attribute import AttributeManager
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import CITypeAttributeCache
from api.lib.cmdb.const import ExistPolicy
from api.lib.cmdb.const import OperateType
from api.lib.cmdb.const import ValueTypeEnum
from api.lib.cmdb.history import AttributeHistoryManger
@@ -80,9 +79,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 +91,13 @@ class AttributeValueManager(object):
return abort(400, ErrFormat.attribute_value_invalid.format(value))
@staticmethod
def __check_is_choice(attr, value_type, value):
choice_values = AttributeManager.get_choice_values(attr.id, value_type, attr.choice_web_hook)
def _check_is_choice(attr, value_type, value):
choice_values = AttributeManager.get_choice_values(attr.id, value_type, attr.choice_web_hook, attr.choice_other)
if str(value) not in list(map(str, [i[0] for i in choice_values])):
return abort(400, ErrFormat.not_in_choice_values.format(value))
@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 +106,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
@@ -139,12 +139,13 @@ class AttributeValueManager(object):
try:
db.session.commit()
except Exception as e:
db.session.rollback()
current_app.logger.error("write change failed: {}".format(str(e)))
return record_id
@staticmethod
def __compute_attr_value_from_expr(expr, ci_dict):
def _compute_attr_value_from_expr(expr, ci_dict):
t = jinja2.Template(expr).render(ci_dict)
try:
@@ -154,7 +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,
@@ -234,7 +235,7 @@ class AttributeValueManager(object):
return key2attr
def create_or_update_attr_value2(self, ci, ci_dict, key2attr):
def create_or_update_attr_value(self, ci, ci_dict, key2attr):
"""
add or update attribute value, then write history
:param ci: instance object
@@ -287,66 +288,6 @@ class AttributeValueManager(object):
return self._write_change2(changed)
def create_or_update_attr_value(self, key, value, ci, _no_attribute_policy=ExistPolicy.IGNORE, record_id=None):
"""
add or update attribute value, then write history
:param key: id, name or alias
:param value:
:param ci: instance object
:param _no_attribute_policy: ignore or reject
:param record_id: op record
:return:
"""
attr = self._get_attr(key)
if attr is None:
if _no_attribute_policy == ExistPolicy.IGNORE:
return
if _no_attribute_policy == ExistPolicy.REJECT:
return abort(400, ErrFormat.attribute_not_found.format(key))
value_table = TableMap(attr=attr).table
try:
if attr.is_list:
value_list = [self._validate(attr, i, value_table, ci) for i in handle_arg_list(value)]
if not value_list:
self.__check_is_required(ci.type_id, attr, '')
existed_attrs = value_table.get_by(attr_id=attr.id, ci_id=ci.id, to_dict=False)
existed_values = [i.value for i in existed_attrs]
added = set(value_list) - set(existed_values)
deleted = set(existed_values) - set(value_list)
for v in added:
value_table.create(ci_id=ci.id, attr_id=attr.id, value=v)
record_id = self._write_change(ci.id, attr.id, OperateType.ADD, None, v, record_id, ci.type_id)
for v in deleted:
existed_attr = existed_attrs[existed_values.index(v)]
existed_attr.delete()
record_id = self._write_change(ci.id, attr.id, OperateType.DELETE, v, None, record_id, ci.type_id)
else:
value = self._validate(attr, value, value_table, ci)
existed_attr = value_table.get_by(attr_id=attr.id, ci_id=ci.id, first=True, to_dict=False)
existed_value = existed_attr and existed_attr.value
if existed_value is None and value is not None:
value_table.create(ci_id=ci.id, attr_id=attr.id, value=value)
record_id = self._write_change(ci.id, attr.id, OperateType.ADD, None, value, record_id, ci.type_id)
else:
if existed_value != value:
if value is None:
existed_attr.delete()
else:
existed_attr.update(value=value)
record_id = self._write_change(ci.id, attr.id, OperateType.UPDATE,
existed_value, value, record_id, ci.type_id)
return record_id
except Exception as e:
current_app.logger.warning(str(e))
return abort(400, ErrFormat.attribute_value_invalid2.format("{}({})".format(attr.alias, attr.name), value))
@staticmethod
def delete_attr_value(attr_id, ci_id):
attr = AttributeCache.get(attr_id)

View File

@@ -6,6 +6,7 @@ from api.lib.common_setting.resp_format import ErrFormat
from api.lib.perm.acl.cache import RoleCache, AppCache
from api.lib.perm.acl.role import RoleCRUD, RoleRelationCRUD
from api.lib.perm.acl.user import UserCRUD
from api.lib.perm.acl.resource import ResourceTypeCRUD, ResourceCRUD
class ACLManager(object):
@@ -94,3 +95,22 @@ class ACLManager(object):
avatar=user_info.get('avatar'))
return result
def validate_app(self):
return AppCache.get(self.app_name)
def get_all_resources_types(self, q=None, page=1, page_size=999999):
app_id = self.validate_app().id
numfound, res, id2perms = ResourceTypeCRUD.search(q, app_id, page, page_size)
return dict(
numfound=numfound,
groups=[i.to_dict() for i in res],
id2perms=id2perms
)
def create_resource(self, payload):
payload['app_id'] = self.validate_app().id
resource = ResourceCRUD.add(**payload)
return resource.to_dict()

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

@@ -1,5 +1,5 @@
# -*- coding:utf-8 -*-
from api.extensions import cache
from api.models.common_setting import CompanyInfo
@@ -11,14 +11,34 @@ class CompanyInfoCRUD(object):
@staticmethod
def create(**kwargs):
return CompanyInfo.create(**kwargs)
res = CompanyInfo.create(**kwargs)
CompanyInfoCache.refresh(res.info)
return res
@staticmethod
def update(_id, **kwargs):
kwargs.pop('id', None)
existed = CompanyInfo.get_by_id(_id)
if not existed:
return CompanyInfoCRUD.create(**kwargs)
existed = CompanyInfoCRUD.create(**kwargs)
else:
existed = existed.update(**kwargs)
return existed
CompanyInfoCache.refresh(existed.info)
return existed
class CompanyInfoCache(object):
key = 'CompanyInfoCache::'
@classmethod
def get(cls):
info = cache.get(cls.key)
if not info:
res = CompanyInfo.get_by(first=True) or {}
info = res.get('info', {})
cache.set(cls.key, info)
return info
@classmethod
def refresh(cls, info):
cache.set(cls.key, info)

View File

@@ -4,11 +4,18 @@ 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
BotNameMap = {
'wechatApp': 'wechatBot',
'feishuApp': 'feishuBot',
'dingdingApp': 'dingdingBot',
}

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

@@ -1,9 +1,9 @@
# -*- coding:utf-8 -*-
import copy
import traceback
from datetime import datetime
import pandas as pd
import requests
from flask import abort
from flask_login import current_user
from sqlalchemy import or_, literal_column, func, not_, and_
@@ -17,9 +17,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 +81,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 +110,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 +152,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 +213,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 +232,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 +298,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 +318,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 +332,6 @@ class EmployeeCRUD(object):
else:
abort(400, ErrFormat.not_support_operator.format(operator))
# 根据relation生成复合条件
if relation == "&":
return expr, []
elif relation == "|":
@@ -505,7 +341,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)
@@ -640,6 +475,83 @@ class EmployeeCRUD(object):
return [r.to_dict() for r in results]
@staticmethod
def remove_bind_notice_by_uid(_platform, _uid):
existed = EmployeeCRUD.get_employee_by_uid(_uid)
employee_data = existed.to_dict()
notice_info = employee_data.get('notice_info', {})
notice_info = copy.deepcopy(notice_info) if notice_info else {}
notice_info[_platform] = ''
existed.update(
notice_info=notice_info
)
return ErrFormat.notice_remove_bind_success
@staticmethod
def bind_notice_by_uid(_platform, _uid):
existed = EmployeeCRUD.get_employee_by_uid(_uid)
mobile = existed.mobile
if not mobile or len(mobile) == 0:
abort(400, ErrFormat.notice_bind_err_with_empty_mobile)
from api.lib.common_setting.notice_config import NoticeConfigCRUD
messenger = NoticeConfigCRUD.get_messenger_url()
if not messenger or len(messenger) == 0:
abort(400, ErrFormat.notice_please_config_messenger_first)
url = f"{messenger}/v1/uid/getbyphone"
try:
payload = dict(
phone=mobile,
sender=_platform
)
res = requests.post(url, json=payload)
result = res.json()
if res.status_code != 200:
raise Exception(result.get('msg', ''))
target_id = result.get('uid', '')
employee_data = existed.to_dict()
notice_info = employee_data.get('notice_info', {})
notice_info = copy.deepcopy(notice_info) if notice_info else {}
notice_info[_platform] = '' if not target_id else target_id
existed.update(
notice_info=notice_info
)
return ErrFormat.notice_bind_success
except Exception as e:
return abort(400, ErrFormat.notice_bind_failed.format(str(e)))
@staticmethod
def get_employee_notice_by_ids(employee_ids):
criterion = [
Employee.employee_id.in_(employee_ids),
Employee.deleted == 0,
]
direct_columns = ['email', 'mobile']
employees = Employee.query.filter(
*criterion
).all()
results = []
for employee in employees:
d = employee.to_dict()
tmp = dict(
employee_id=employee.employee_id,
)
for column in direct_columns:
tmp[column] = d.get(column, '')
notice_info = d.get('notice_info', {})
tmp.update(**notice_info)
results.append(tmp)
return results
def get_user_map(key='uid', acl=None):
"""
@@ -654,19 +566,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 +575,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 +599,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 +611,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 +653,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 +669,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 +687,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 +701,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 +715,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 +726,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

@@ -0,0 +1,165 @@
import requests
from api.lib.common_setting.const import BotNameMap
from api.lib.common_setting.resp_format import ErrFormat
from api.models.common_setting import CompanyInfo, NoticeConfig
from wtforms import Form
from wtforms import StringField
from wtforms import validators
from flask import abort, current_app
class NoticeConfigCRUD(object):
@staticmethod
def add_notice_config(**kwargs):
platform = kwargs.get('platform')
NoticeConfigCRUD.check_platform(platform)
info = kwargs.get('info', {})
if 'name' not in info:
info['name'] = platform
kwargs['info'] = info
try:
NoticeConfigCRUD.update_messenger_config(**info)
res = NoticeConfig.create(
**kwargs
)
return res
except Exception as e:
return abort(400, str(e))
@staticmethod
def check_platform(platform):
NoticeConfig.get_by(first=True, to_dict=False, platform=platform) and \
abort(400, ErrFormat.notice_platform_existed.format(platform))
@staticmethod
def edit_notice_config(_id, **kwargs):
existed = NoticeConfigCRUD.get_notice_config_by_id(_id)
try:
info = kwargs.get('info', {})
if 'name' not in info:
info['name'] = existed.platform
kwargs['info'] = info
NoticeConfigCRUD.update_messenger_config(**info)
res = existed.update(**kwargs)
return res
except Exception as e:
return abort(400, str(e))
@staticmethod
def get_messenger_url():
from api.lib.common_setting.company_info import CompanyInfoCache
com_info = CompanyInfoCache.get()
if not com_info:
return
messenger = com_info.get('messenger', '')
if len(messenger) == 0:
return
if messenger[-1] == '/':
messenger = messenger[:-1]
return messenger
@staticmethod
def update_messenger_config(**kwargs):
try:
messenger = NoticeConfigCRUD.get_messenger_url()
if not messenger or len(messenger) == 0:
raise Exception(ErrFormat.notice_please_config_messenger_first)
url = f"{messenger}/v1/senders"
name = kwargs.get('name')
bot_list = kwargs.pop('bot', None)
for k, v in kwargs.items():
if isinstance(v, bool):
kwargs[k] = 'true' if v else 'false'
else:
kwargs[k] = str(v)
payload = {name: [kwargs]}
current_app.logger.info(f"update_messenger_config: {url}, {payload}")
res = requests.put(url, json=payload, timeout=2)
current_app.logger.info(f"update_messenger_config: {res.status_code}, {res.text}")
if not bot_list or len(bot_list) == 0:
return
bot_name = BotNameMap.get(name)
payload = {bot_name: bot_list}
current_app.logger.info(f"update_messenger_config: {url}, {payload}")
bot_res = requests.put(url, json=payload, timeout=2)
current_app.logger.info(f"update_messenger_config: {bot_res.status_code}, {bot_res.text}")
except Exception as e:
return abort(400, str(e))
@staticmethod
def get_notice_config_by_id(_id):
return NoticeConfig.get_by(first=True, to_dict=False, id=_id) or \
abort(400,
ErrFormat.notice_not_existed.format(_id))
@staticmethod
def get_all():
return NoticeConfig.get_by(to_dict=True)
@staticmethod
def test_send_email(receive_address, **kwargs):
messenger = NoticeConfigCRUD.get_messenger_url()
if not messenger or len(messenger) == 0:
abort(400, ErrFormat.notice_please_config_messenger_first)
url = f"{messenger}/v1/message"
recipient_email = receive_address
subject = 'Test Email'
body = 'This is a test email'
payload = {
"sender": 'email',
"msgtype": "text/plain",
"title": subject,
"content": body,
"tos": [recipient_email],
}
current_app.logger.info(f"test_send_email: {url}, {payload}")
response = requests.post(url, json=payload)
if response.status_code != 200:
abort(400, response.text)
return 1
@staticmethod
def get_app_bot():
result = []
for notice_app in NoticeConfig.get_by(to_dict=False):
if notice_app.platform in ['email']:
continue
info = notice_app.info
name = info.get('name', '')
if name not in BotNameMap:
continue
result.append(dict(
name=info.get('name', ''),
label=info.get('label', ''),
bot=info.get('bot', []),
))
return result
class NoticeConfigForm(Form):
platform = StringField(validators=[
validators.DataRequired(message="平台 不能为空"),
validators.Length(max=255),
])
info = StringField(validators=[
validators.DataRequired(message="信息 不能为空"),
validators.Length(max=255),
])
class NoticeConfigUpdateForm(Form):
info = StringField(validators=[
validators.DataRequired(message="信息 不能为空"),
validators.Length(max=255),
])

View File

@@ -49,3 +49,17 @@ 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 = "邮箱格式错误"
email_send_timeout = "邮件发送超时"
common_data_not_found = "ID {} 找不到记录"
notice_platform_existed = "{} 已存在"
notice_not_existed = "{} 配置项不存在"
notice_please_config_messenger_first = "请先配置 messenger"
notice_bind_err_with_empty_mobile = "绑定失败,手机号为空"
notice_bind_failed = "绑定失败: {}"
notice_bind_success = "绑定成功"
notice_remove_bind_success = "解绑成功"

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

@@ -0,0 +1,72 @@
# -*- coding:utf-8 -*-
import json
import requests
import six
from flask import current_app
from jinja2 import Template
from markdownify import markdownify as md
from api.lib.common_setting.notice_config import NoticeConfigCRUD
from api.lib.mail import send_mail
def _request_messenger(subject, body, tos, sender, payload):
params = dict(sender=sender, title=subject,
tos=[to[sender] for to in tos if to.get(sender)])
if not params['tos']:
raise Exception("no receivers")
flat_tos = []
for i in params['tos']:
if i.strip():
to = Template(i).render(payload)
if isinstance(to, list):
flat_tos.extend(to)
elif isinstance(to, six.string_types):
flat_tos.append(to)
params['tos'] = flat_tos
if sender == "email":
params['msgtype'] = 'text/html'
params['content'] = body
else:
params['msgtype'] = 'markdown'
try:
content = md("{}\n{}".format(subject or '', body or ''))
except Exception as e:
current_app.logger.warning("html2markdown failed: {}".format(e))
content = "{}\n{}".format(subject or '', body or '')
params['content'] = json.dumps(dict(content=content))
url = current_app.config.get('MESSENGER_URL') or NoticeConfigCRUD.get_messenger_url()
if not url:
raise Exception("no messenger url")
if not url.endswith("message"):
url = "{}/v1/message".format(url)
resp = requests.post(url, json=params)
if resp.status_code != 200:
raise Exception(resp.text)
return resp.text
def notify_send(subject, body, methods, tos, payload=None):
payload = payload or {}
payload = {k: v or '' for k, v in payload.items()}
subject = Template(subject).render(payload)
body = Template(body).render(payload)
res = ''
for method in methods:
if method == "email" and not current_app.config.get('USE_MESSENGER', True):
send_mail(None, [Template(to.get('email')).render(payload) for to in tos], subject, body)
res += (_request_messenger(subject, body, tos, method, payload) + "\n")
return res

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,6 +6,7 @@ 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
@@ -212,18 +213,15 @@ class RoleCRUD(object):
@staticmethod
def search(q, app_id, page=1, page_size=None, user_role=True, is_all=False, user_only=False):
query = db.session.query(Role).filter(Role.deleted.is_(False))
query1 = query.filter(Role.app_id == app_id).filter(Role.uid.is_(None))
query2 = query.filter(Role.app_id.is_(None)).filter(Role.uid.is_(None))
query = query1.union(query2)
if user_role:
query1 = db.session.query(Role).filter(Role.deleted.is_(False)).filter(Role.uid.isnot(None))
query = query.union(query1)
if user_only:
if user_only: # only user role
query = db.session.query(Role).filter(Role.deleted.is_(False)).filter(Role.uid.isnot(None))
else:
query = db.session.query(Role).filter(Role.deleted.is_(False)).filter(
or_(Role.app_id == app_id, Role.app_id.is_(None)))
if not user_role: # only virtual role
query = query.filter(Role.uid.is_(None))
if not is_all:
role_ids = list(HasResourceRoleCache.get(app_id).keys())
query = query.filter(Role.id.in_(role_ids))
@@ -272,6 +270,13 @@ class RoleCRUD(object):
RoleCache.clean(rid)
role = role.update(**kwargs)
if origin['uid'] and kwargs.get('name') and kwargs.get('name') != origin['name']:
from api.models.acl import User
user = User.get_by(uid=origin['uid'], first=True, to_dict=False)
if user:
user.update(username=kwargs['name'])
AuditCRUD.add_role_log(role.app_id, AuditOperateType.update,
AuditScope.role, role.id, origin, role.to_dict(), {},
)
@@ -286,14 +291,15 @@ 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)))
if not role.app_id and not is_admin():
return abort(403, ErrFormat.admin_required)
not force and role.uid and abort(400, ErrFormat.user_role_delete_invalid)
origin = role.to_dict()
child_ids = []

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

@@ -9,6 +9,8 @@ class CommonErrFormat(object):
not_found = "不存在"
circular_dependency_error = "存在循环依赖!"
unknown_search_error = "未知搜索错误"
invalid_json = "json格式似乎不正确了, 请仔细确认一下!"

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')

109
cmdb-api/api/lib/webhook.py Normal file
View File

@@ -0,0 +1,109 @@
# -*- coding:utf-8 -*-
import json
from functools import partial
import requests
from jinja2 import Template
from requests.auth import HTTPBasicAuth
from requests_oauthlib import OAuth2Session
class BearerAuth(requests.auth.AuthBase):
def __init__(self, token):
self.token = token
def __call__(self, r):
r.headers["authorization"] = "Bearer {}".format(self.token)
return r
def _wrap_auth(**kwargs):
auth_type = (kwargs.get('type') or "").lower()
if auth_type == "basicauth":
return HTTPBasicAuth(kwargs.get('username'), kwargs.get('password'))
elif auth_type == "bearer":
return BearerAuth(kwargs.get('token'))
elif auth_type == 'oauth2.0':
client_id = kwargs.get('client_id')
client_secret = kwargs.get('client_secret')
authorization_base_url = kwargs.get('authorization_base_url')
token_url = kwargs.get('token_url')
redirect_url = kwargs.get('redirect_url')
scope = kwargs.get('scope')
oauth2_session = OAuth2Session(client_id, scope=scope or None)
oauth2_session.authorization_url(authorization_base_url)
oauth2_session.fetch_token(token_url, client_secret=client_secret, authorization_response=redirect_url)
return oauth2_session
elif auth_type == "apikey":
return HTTPBasicAuth(kwargs.get('key'), kwargs.get('value'))
def webhook_request(webhook, payload):
"""
:param webhook:
{
"url": "https://veops.cn"
"method": "GET|POST|PUT|DELETE"
"body": {},
"headers": {
"Content-Type": "Application/json"
},
"parameters": {
"key": "value"
},
"authorization": {
"type": "BasicAuth|Bearer|OAuth2.0|APIKey",
"password": "mmmm", # BasicAuth
"username": "bbb", # BasicAuth
"token": "xxx", # Bearer
"key": "xxx", # APIKey
"value": "xxx", # APIKey
"client_id": "xxx", # OAuth2.0
"client_secret": "xxx", # OAuth2.0
"authorization_base_url": "xxx", # OAuth2.0
"token_url": "xxx", # OAuth2.0
"redirect_url": "xxx", # OAuth2.0
"scope": "xxx" # OAuth2.0
}
}
:param payload:
:return:
"""
assert webhook.get('url') is not None
payload = {k: v or '' for k, v in payload.items()}
url = Template(webhook['url']).render(payload)
params = webhook.get('parameters') or None
if isinstance(params, dict):
params = json.loads(Template(json.dumps(params)).render(payload))
headers = json.loads(Template(json.dumps(webhook.get('headers') or {})).render(payload))
data = Template(json.dumps(webhook.get('body', ''))).render(payload)
auth = _wrap_auth(**webhook.get('authorization', {}))
if (webhook.get('authorization', {}).get("type") or '').lower() == 'oauth2.0':
request = getattr(auth, webhook.get('method', 'GET').lower())
else:
request = partial(requests.request, webhook.get('method', 'GET'))
return request(
url,
params=params,
headers=headers or None,
data=data,
auth=auth
)

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

@@ -90,6 +90,7 @@ class Attribute(Model):
compute_script = db.Column(db.Text)
choice_web_hook = db.Column(db.JSON)
choice_other = db.Column(db.JSON)
uid = db.Column(db.Integer, index=True)
@@ -125,16 +126,27 @@ class CITypeAttributeGroupItem(Model):
class CITypeTrigger(Model):
# __tablename__ = "c_ci_type_triggers"
__tablename__ = "c_c_t_t"
type_id = db.Column(db.Integer, db.ForeignKey('c_ci_types.id'), nullable=False)
attr_id = db.Column(db.Integer, db.ForeignKey("c_attributes.id"), nullable=False)
notify = db.Column(db.JSON) # {subject: x, body: x, wx_to: [], mail_to: [], before_days: 0, notify_at: 08:00}
attr_id = db.Column(db.Integer, db.ForeignKey("c_attributes.id"))
option = db.Column('notify', db.JSON)
class CITriggerHistory(Model):
__tablename__ = "c_ci_trigger_histories"
operate_type = db.Column(db.Enum(*OperateType.all(), name="operate_type"))
record_id = db.Column(db.Integer, db.ForeignKey("c_records.id"))
ci_id = db.Column(db.Integer, index=True, nullable=False)
trigger_id = db.Column(db.Integer, db.ForeignKey("c_c_t_t.id"))
trigger_name = db.Column(db.String(64))
is_ok = db.Column(db.Boolean, default=False)
notify = db.Column(db.Text)
webhook = db.Column(db.Text)
class CITypeUniqueConstraint(Model):
# __tablename__ = "c_ci_type_unique_constraints"
__tablename__ = "c_c_t_u_c"
type_id = db.Column(db.Integer, db.ForeignKey('c_ci_types.id'), nullable=False)
@@ -250,6 +262,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 +276,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 +301,9 @@ class CIValueText(Model):
class CIValueDateTime(Model):
"""
Deprecated in a future version
"""
__tablename__ = "c_value_datetime"
ci_id = db.Column(db.Integer, db.ForeignKey('c_cis.id'), nullable=False)
@@ -354,7 +375,6 @@ class CITypeHistory(Model):
# preference
class PreferenceShowAttributes(Model):
# __tablename__ = "c_preference_show_attributes"
__tablename__ = "c_psa"
uid = db.Column(db.Integer, index=True, nullable=False)
@@ -368,7 +388,6 @@ class PreferenceShowAttributes(Model):
class PreferenceTreeView(Model):
# __tablename__ = "c_preference_tree_views"
__tablename__ = "c_ptv"
uid = db.Column(db.Integer, index=True, nullable=False)
@@ -377,7 +396,6 @@ class PreferenceTreeView(Model):
class PreferenceRelationView(Model):
# __tablename__ = "c_preference_relation_views"
__tablename__ = "c_prv"
uid = db.Column(db.Integer, index=True, nullable=False)

View File

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

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

@@ -4,9 +4,8 @@
import json
import time
import jinja2
import requests
from flask import current_app
from flask_login import login_user
import api.lib.cmdb.ci
from api.extensions import celery
@@ -17,13 +16,18 @@ from api.lib.cmdb.cache import CITypeAttributesCache
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)
def ci_cache(ci_id):
def ci_cache(ci_id, operate_type, record_id):
from api.lib.cmdb.ci import CITriggerManager
time.sleep(0.01)
db.session.remove()
@@ -37,9 +41,15 @@ def ci_cache(ci_id):
current_app.logger.info("{0} flush..........".format(ci_id))
if operate_type:
current_app.test_request_context().push()
login_user(UserCache.get('worker'))
CITriggerManager.fire(operate_type, ci_dict, record_id)
@celery.task(name="cmdb.batch_ci_cache", queue=CMDB_QUEUE)
def batch_ci_cache(ci_ids):
def batch_ci_cache(ci_ids, ): # only for attribute change index
time.sleep(1)
db.session.remove()
@@ -67,6 +77,17 @@ def ci_delete(ci_id):
current_app.logger.info("{0} delete..........".format(ci_id))
@celery.task(name="cmdb.ci_delete_trigger", queue=CMDB_QUEUE)
def ci_delete_trigger(trigger, operate_type, ci_dict):
current_app.logger.info('delete ci {} trigger'.format(ci_dict['_id']))
from api.lib.cmdb.ci import CITriggerManager
current_app.test_request_context().push()
login_user(UserCache.get('worker'))
CITriggerManager.fire_by_trigger(trigger, operate_type, ci_dict)
@celery.task(name="cmdb.ci_relation_cache", queue=CMDB_QUEUE)
def ci_relation_cache(parent_id, child_id):
db.session.remove()
@@ -84,6 +105,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)):
@@ -99,7 +165,7 @@ def ci_relation_delete(parent_id, child_id):
@celery.task(name="cmdb.ci_type_attribute_order_rebuild", queue=CMDB_QUEUE)
def ci_type_attribute_order_rebuild(type_id):
def ci_type_attribute_order_rebuild(type_id, uid):
current_app.logger.info('rebuild attribute order')
db.session.remove()
@@ -108,6 +174,9 @@ def ci_type_attribute_order_rebuild(type_id):
attrs = CITypeAttributesCache.get(type_id)
id2attr = {attr.attr_id: attr for attr in attrs}
current_app.test_request_context().push()
login_user(UserCache.get(uid))
res = CITypeAttributeGroupManager.get_by_type_id(type_id, True)
order = 0
for group in res:
@@ -118,41 +187,17 @@ def ci_type_attribute_order_rebuild(type_id):
order += 1
@celery.task(name='cmdb.trigger_notify', queue=CMDB_QUEUE)
def trigger_notify(notify, ci_id):
from api.lib.perm.acl.cache import UserCache
def _wrap_mail(mail_to):
if "@" not in mail_to:
user = UserCache.get(mail_to)
if user:
return user.email
return mail_to
@celery.task(name="cmdb.calc_computed_attribute", queue=CMDB_QUEUE)
def calc_computed_attribute(attr_id, uid):
from api.lib.cmdb.ci import CIManager
db.session.remove()
m = api.lib.cmdb.ci.CIManager()
ci_dict = m.get_ci_by_id_from_db(ci_id, need_children=False, use_master=False)
current_app.test_request_context().push()
login_user(UserCache.get(uid))
subject = jinja2.Template(notify.get('subject') or "").render(ci_dict)
body = jinja2.Template(notify.get('body') or "").render(ci_dict)
if notify.get('wx_to'):
to_user = jinja2.Template('|'.join(notify['wx_to'])).render(ci_dict)
url = current_app.config.get("WX_URI")
data = {"to_user": to_user, "content": subject}
try:
requests.post(url, data=data)
except Exception as e:
current_app.logger.error(str(e))
if notify.get('mail_to'):
try:
if len(subject) > 700:
subject = subject[:600] + "..." + subject[-100:]
send_mail("", [_wrap_mail(jinja2.Template(i).render(ci_dict))
for i in notify['mail_to'] if i], subject, body)
except Exception as e:
current_app.logger.error("Send mail failed: {0}".format(str(e)))
cim = CIManager()
for i in CITypeAttribute.get_by(attr_id=attr_id, to_dict=False):
cis = CI.get_by(type_id=i.type_id, to_dict=False)
for ci in cis:
cim.update(ci.id, {})

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()
@@ -63,17 +64,25 @@ class AttributeView(APIView):
current_app.logger.debug(params)
attr_id = AttributeManager.add(**params)
return self.jsonify(attr_id=attr_id)
@args_validate(AttributeManager.cls)
def put(self, attr_id):
if request.url.endswith("/calc_computed_attribute"):
AttributeManager.calc_computed_attribute(attr_id)
return self.jsonify(attr_id=attr_id)
choice_value = handle_arg_list(request.values.get("choice_value"))
params = request.values
params["choice_value"] = choice_value
current_app.logger.debug(params)
AttributeManager().update(attr_id, **params)
return self.jsonify(attr_id=attr_id)
def delete(self, attr_id):
attr_name = AttributeManager.delete(attr_id)
return self.jsonify(message="attribute {0} deleted".format(attr_name))

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,
@@ -183,8 +185,8 @@ class CIUnique(APIView):
@has_perm_from_args("ci_id", ResourceTypeEnum.CI, PermEnum.UPDATE, CIManager.get_type_name)
def put(self, ci_id):
params = request.values
unique_name = params.keys()[0]
unique_value = params.values()[0]
unique_name = list(params.keys())[0]
unique_value = list(params.values())[0]
CIManager.update_unique_value(ci_id, unique_name, unique_value)

View File

@@ -1,4 +1,4 @@
# -*- coding:utf-8 -*-
# -*- coding:utf-8 -*-
import json
@@ -154,9 +154,15 @@ class EnableCITypeView(APIView):
class CITypeAttributeView(APIView):
url_prefix = ("/ci_types/<int:type_id>/attributes", "/ci_types/<string:type_name>/attributes")
url_prefix = ("/ci_types/<int:type_id>/attributes", "/ci_types/<string:type_name>/attributes",
"/ci_types/common_attributes")
def get(self, type_id=None, type_name=None):
if request.path.endswith("/common_attributes"):
type_ids = handle_arg_list(request.values.get('type_ids'))
return self.jsonify(attributes=CITypeAttributeManager.get_common_attributes(type_ids))
t = CITypeCache.get(type_id) or CITypeCache.get(type_name) or abort(404, ErrFormat.ci_type_not_found)
type_id = t.id
unique_id = t.unique_id
@@ -350,9 +356,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
@@ -413,22 +419,22 @@ class CITypeTriggerView(APIView):
return self.jsonify(CITypeTriggerManager.get(type_id))
@has_perm_from_args("type_id", ResourceTypeEnum.CI, PermEnum.CONFIG, CITypeManager.get_name_by_id)
@args_required("attr_id")
@args_required("notify")
@args_required("option")
def post(self, type_id):
attr_id = request.values.get('attr_id')
notify = request.values.get('notify')
attr_id = request.values.get('attr_id') or None
option = request.values.get('option')
return self.jsonify(CITypeTriggerManager().add(type_id, attr_id, notify))
return self.jsonify(CITypeTriggerManager().add(type_id, attr_id, option))
@has_perm_from_args("type_id", ResourceTypeEnum.CI, PermEnum.CONFIG, CITypeManager.get_name_by_id)
@args_required("notify")
@args_required("option")
def put(self, type_id, _id):
assert type_id is not None
notify = request.values.get('notify')
option = request.values.get('option')
attr_id = request.values.get('attr_id')
return self.jsonify(CITypeTriggerManager().update(_id, notify))
return self.jsonify(CITypeTriggerManager().update(_id, attr_id, option))
@has_perm_from_args("type_id", ResourceTypeEnum.CI, PermEnum.CONFIG, CITypeManager.get_name_by_id)
def delete(self, type_id, _id):

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
@@ -17,9 +19,14 @@ from api.resource import APIView
class GetChildrenView(APIView):
url_prefix = "/ci_type_relations/<int:parent_id>/children"
url_prefix = ("/ci_type_relations/<int:parent_id>/children",
"/ci_type_relations/<int:parent_id>/recursive_level2children",
)
def get(self, parent_id):
if request.url.endswith("recursive_level2children"):
return self.jsonify(CITypeRelationManager.recursive_level2children(parent_id))
return self.jsonify(children=CITypeRelationManager.get_children(parent_id))

View File

@@ -13,7 +13,8 @@ from api.resource import APIView
class CustomDashboardApiView(APIView):
url_prefix = ("/custom_dashboard", "/custom_dashboard/<int:_id>", "/custom_dashboard/batch")
url_prefix = ("/custom_dashboard", "/custom_dashboard/<int:_id>", "/custom_dashboard/batch",
"/custom_dashboard/preview")
def get(self):
return self.jsonify(CustomDashboardManager.get())
@@ -21,17 +22,26 @@ class CustomDashboardApiView(APIView):
@role_required(RoleEnum.CONFIG)
@args_validate(CustomDashboardManager.cls)
def post(self):
cm = CustomDashboardManager.add(**request.values)
if request.url.endswith("/preview"):
return self.jsonify(counter=CustomDashboardManager.preview(**request.values))
return self.jsonify(cm.to_dict())
cm, counter = CustomDashboardManager.add(**request.values)
res = cm.to_dict()
res.update(counter=counter)
return self.jsonify(res)
@role_required(RoleEnum.CONFIG)
@args_validate(CustomDashboardManager.cls)
def put(self, _id=None):
if _id is not None:
cm = CustomDashboardManager.update(_id, **request.values)
cm, counter = CustomDashboardManager.update(_id, **request.values)
return self.jsonify(cm.to_dict())
res = cm.to_dict()
res.update(counter=counter)
return self.jsonify(res)
CustomDashboardManager.batch_update(request.values.get("id2options"))

View File

@@ -5,14 +5,18 @@ import datetime
from flask import abort
from flask import request
from flask import session
from api.lib.cmdb.ci import CIManager
from api.lib.cmdb.const import ResourceTypeEnum, PermEnum
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.const import RoleEnum
from api.lib.cmdb.history import AttributeHistoryManger
from api.lib.cmdb.history import CITriggerHistoryManager
from api.lib.cmdb.history import CITypeHistoryManager
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.perm.acl.acl import has_perm_from_args
from api.lib.perm.acl.acl import is_app_admin
from api.lib.perm.acl.acl import role_required
from api.lib.utils import get_page
from api.lib.utils import get_page_size
@@ -75,6 +79,39 @@ class CIHistoryView(APIView):
return self.jsonify(result)
class CITriggerHistoryView(APIView):
url_prefix = ("/history/ci_triggers/<int:ci_id>", "/history/ci_triggers")
@has_perm_from_args("ci_id", ResourceTypeEnum.CI, PermEnum.READ, CIManager.get_type_name)
def get(self, ci_id=None):
if ci_id is not None:
result = CITriggerHistoryManager.get_by_ci_id(ci_id)
return self.jsonify(result)
if RoleEnum.CONFIG not in session.get("acl", {}).get("parentRoles", []) and not is_app_admin("cmdb"):
return abort(403, ErrFormat.role_required.format(RoleEnum.CONFIG))
type_id = request.values.get("type_id")
trigger_id = request.values.get("trigger_id")
operate_type = request.values.get("operate_type")
page = get_page(request.values.get('page', 1))
page_size = get_page_size(request.values.get('page_size', 1))
numfound, result = CITriggerHistoryManager.get(page,
page_size,
type_id=type_id,
trigger_id=trigger_id,
operate_type=operate_type)
return self.jsonify(page=page,
page_size=page_size,
numfound=numfound,
total=len(result),
result=result)
class CITypeHistoryView(APIView):
url_prefix = "/history/ci_types"

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

@@ -146,42 +146,25 @@ class EmployeePositionView(APIView):
return self.jsonify(result)
class EmployeeViewExportExcel(APIView):
url_prefix = (f'{prefix}/export_all',)
class GetEmployeeNoticeByIds(APIView):
url_prefix = (f'{prefix}/get_notice_by_ids',)
def get(self):
col_desc_map = {
'nickname': "姓名",
'email': '邮箱',
'sex': '性别',
'mobile': '手机号',
'department_name': '部门',
'position_name': '岗位',
'nickname_direct_supervisor': '直接上级',
'last_login': '上次登录时间',
}
def post(self):
employee_ids = request.json.get('employee_ids', [])
if not employee_ids:
result = []
else:
result = EmployeeCRUD.get_employee_notice_by_ids(employee_ids)
return self.jsonify(result)
# 规定了静态文件的存储位置
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)
class EmployeeBindNoticeWithACLID(APIView):
url_prefix = (f'{prefix}/by_uid/bind_notice/<string:platform>/<int:_uid>',)
# 改变列名为中文head
try:
df = df.rename(columns=col_desc_map)
except Exception as e:
abort(500, ErrFormat.rename_columns_failed.format(str(e)))
def put(self, platform, _uid):
data = EmployeeCRUD.bind_notice_by_uid(platform, _uid)
return self.jsonify(info=data)
# 生成静态excel文件
try:
df.to_excel(excel_path_with_filename,
sheet_name='Sheet1', index=False, encoding="utf-8")
except Exception as e:
current_app.logger.error(e)
abort(500, ErrFormat.generate_excel_failed.format(str(e)))
return send_from_directory(excel_path, excel_filename, as_attachment=True)
def delete(self, platform, _uid):
data = EmployeeCRUD.remove_bind_notice_by_uid(platform, _uid)
return self.jsonify(info=data)

View File

@@ -11,7 +11,7 @@ from api.resource import APIView
prefix = '/file'
ALLOWED_EXTENSIONS = {
'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif', 'xls', 'xlsx', 'doc', 'docx', 'ppt', 'pptx', 'csv'
'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif', 'xls', 'xlsx', 'doc', 'docx', 'ppt', 'pptx', 'csv', 'svg'
}

View File

@@ -0,0 +1,79 @@
from flask import request, abort, current_app
from werkzeug.datastructures import MultiDict
from api.lib.perm.auth import auth_with_app_token
from api.models.common_setting import NoticeConfig
from api.resource import APIView
from api.lib.common_setting.notice_config import NoticeConfigForm, NoticeConfigUpdateForm, NoticeConfigCRUD
from api.lib.decorator import args_required
from api.lib.common_setting.resp_format import ErrFormat
prefix = '/notice_config'
class NoticeConfigView(APIView):
url_prefix = (f'{prefix}',)
@args_required('platform')
@auth_with_app_token
def get(self):
platform = request.args.get('platform')
res = NoticeConfig.get_by(first=True, to_dict=True, platform=platform) or {}
return self.jsonify(res)
def post(self):
form = NoticeConfigForm(MultiDict(request.json))
if not form.validate():
abort(400, ','.join(['{}: {}'.format(filed, ','.join(msg)) for filed, msg in form.errors.items()]))
data = NoticeConfigCRUD.add_notice_config(**form.data)
return self.jsonify(data.to_dict())
class NoticeConfigUpdateView(APIView):
url_prefix = (f'{prefix}/<int:_id>',)
def put(self, _id):
form = NoticeConfigUpdateForm(MultiDict(request.json))
if not form.validate():
abort(400, ','.join(['{}: {}'.format(filed, ','.join(msg)) for filed, msg in form.errors.items()]))
data = NoticeConfigCRUD.edit_notice_config(_id, **form.data)
return self.jsonify(data.to_dict())
class CheckEmailServer(APIView):
url_prefix = (f'{prefix}/send_test_email',)
def post(self):
receive_address = request.args.get('receive_address')
info = request.values.get('info')
try:
result = NoticeConfigCRUD.test_send_email(receive_address, **info)
return self.jsonify(result=result)
except Exception as e:
current_app.logger.error('test_send_email err:')
current_app.logger.error(e)
if 'Timed Out' in str(e):
abort(400, ErrFormat.email_send_timeout)
abort(400, f"{str(e)}")
class NoticeConfigGetView(APIView):
method_decorators = []
url_prefix = (f'{prefix}/all',)
@auth_with_app_token
def get(self):
res = NoticeConfigCRUD.get_all()
return self.jsonify(res)
class NoticeAppBotView(APIView):
url_prefix = (f'{prefix}/app_bot',)
def get(self):
res = NoticeConfigCRUD.get_app_bot()
return self.jsonify(res)

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,48 @@
-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
requests_oauthlib==1.3.1
markdownify==0.11.6
six==1.16.0
SQLAlchemy==1.4.49
supervisor==4.0.3
timeout-decorator==0.5.0
toposort==1.10
treelib==1.6.1
tzlocal==5.0.1
urllib3==1.26.16
vine==1.3.0
Werkzeug==0.15.5
Werkzeug==2.3.6
WTForms==3.0.0
zipp==3.16.0

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'
@@ -90,4 +95,5 @@ USE_ES = False
BOOL_TRUE = ['true', 'TRUE', 'True', True, '1', 1, "Yes", "YES", "yes", 'Y', 'y']
CMDB_API = "http://127.0.0.1:5000/api/v0.1"
# # messenger
USE_MESSENGER = True

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

@@ -17,6 +17,8 @@
"@babel/plugin-syntax-import-meta": "^7.10.4",
"@riophae/vue-treeselect": "^0.4.0",
"@vue/composition-api": "^1.7.1",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^1.0.0",
"ant-design-vue": "^1.6.5",
"axios": "0.18.0",
"babel-eslint": "^8.2.2",
@@ -37,6 +39,7 @@
"moment": "^2.24.0",
"nprogress": "^0.2.0",
"relation-graph": "^1.1.0",
"snabbdom": "^3.5.1",
"sortablejs": "1.9.0",
"viser-vue": "^2.4.8",
"vue": "2.6.11",
@@ -53,7 +56,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

@@ -54,6 +54,126 @@
<div class="content unicode" style="display: block;">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont">&#xe88e;</span>
<div class="name">wechatApp</div>
<div class="code-name">&amp;#xe88e;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe88b;</span>
<div class="name">robot</div>
<div class="code-name">&amp;#xe88b;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe88c;</span>
<div class="name">feishuApp</div>
<div class="code-name">&amp;#xe88c;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe88d;</span>
<div class="name">dingdingApp</div>
<div class="code-name">&amp;#xe88d;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe88a;</span>
<div class="name">email</div>
<div class="code-name">&amp;#xe88a;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe887;</span>
<div class="name">setting-feishu</div>
<div class="code-name">&amp;#xe887;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe888;</span>
<div class="name">setting-feishu-selected</div>
<div class="code-name">&amp;#xe888;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe886;</span>
<div class="name">cmdb-histogram</div>
<div class="code-name">&amp;#xe886;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe883;</span>
<div class="name">cmdb-index</div>
<div class="code-name">&amp;#xe883;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe884;</span>
<div class="name">cmdb-piechart</div>
<div class="code-name">&amp;#xe884;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe885;</span>
<div class="name">cmdb-line</div>
<div class="code-name">&amp;#xe885;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe882;</span>
<div class="name">cmdb-table</div>
<div class="code-name">&amp;#xe882;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe87f;</span>
<div class="name">itsm-all</div>
<div class="code-name">&amp;#xe87f;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe87e;</span>
<div class="name">itsm-reply</div>
<div class="code-name">&amp;#xe87e;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe880;</span>
<div class="name">itsm-information</div>
<div class="code-name">&amp;#xe880;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe881;</span>
<div class="name">itsm-contact</div>
<div class="code-name">&amp;#xe881;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe87d;</span>
<div class="name">itsm-my-processed</div>
<div class="code-name">&amp;#xe87d;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe87c;</span>
<div class="name">rule_7</div>
<div class="code-name">&amp;#xe87c;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe879;</span>
<div class="name">itsm-my-completed</div>
<div class="code-name">&amp;#xe879;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe87b;</span>
<div class="name">itsm-my-plan</div>
<div class="code-name">&amp;#xe87b;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe87a;</span>
<div class="name">rule_100</div>
@@ -2022,6 +2142,12 @@
<div class="code-name">&amp;#xe738;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe889;</span>
<div class="name">ops-setting-notice-email-selected</div>
<div class="code-name">&amp;#xe889;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe72f;</span>
<div class="name">ops-setting-notice</div>
@@ -3876,9 +4002,9 @@
<pre><code class="language-css"
>@font-face {
font-family: 'iconfont';
src: url('iconfont.woff2?t=1688550067963') format('woff2'),
url('iconfont.woff?t=1688550067963') format('woff'),
url('iconfont.ttf?t=1688550067963') format('truetype');
src: url('iconfont.woff2?t=1696815443987') format('woff2'),
url('iconfont.woff?t=1696815443987') format('woff'),
url('iconfont.ttf?t=1696815443987') format('truetype');
}
</code></pre>
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
@@ -3904,6 +4030,186 @@
<div class="content font-class">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont wechatApp"></span>
<div class="name">
wechatApp
</div>
<div class="code-name">.wechatApp
</div>
</li>
<li class="dib">
<span class="icon iconfont robot"></span>
<div class="name">
robot
</div>
<div class="code-name">.robot
</div>
</li>
<li class="dib">
<span class="icon iconfont feishuApp"></span>
<div class="name">
feishuApp
</div>
<div class="code-name">.feishuApp
</div>
</li>
<li class="dib">
<span class="icon iconfont dingdingApp"></span>
<div class="name">
dingdingApp
</div>
<div class="code-name">.dingdingApp
</div>
</li>
<li class="dib">
<span class="icon iconfont email"></span>
<div class="name">
email
</div>
<div class="code-name">.email
</div>
</li>
<li class="dib">
<span class="icon iconfont ops-setting-notice-feishu"></span>
<div class="name">
setting-feishu
</div>
<div class="code-name">.ops-setting-notice-feishu
</div>
</li>
<li class="dib">
<span class="icon iconfont ops-setting-notice-feishu-selected"></span>
<div class="name">
setting-feishu-selected
</div>
<div class="code-name">.ops-setting-notice-feishu-selected
</div>
</li>
<li class="dib">
<span class="icon iconfont cmdb-bar"></span>
<div class="name">
cmdb-histogram
</div>
<div class="code-name">.cmdb-bar
</div>
</li>
<li class="dib">
<span class="icon iconfont cmdb-count"></span>
<div class="name">
cmdb-index
</div>
<div class="code-name">.cmdb-count
</div>
</li>
<li class="dib">
<span class="icon iconfont cmdb-pie"></span>
<div class="name">
cmdb-piechart
</div>
<div class="code-name">.cmdb-pie
</div>
</li>
<li class="dib">
<span class="icon iconfont cmdb-line"></span>
<div class="name">
cmdb-line
</div>
<div class="code-name">.cmdb-line
</div>
</li>
<li class="dib">
<span class="icon iconfont cmdb-table"></span>
<div class="name">
cmdb-table
</div>
<div class="code-name">.cmdb-table
</div>
</li>
<li class="dib">
<span class="icon iconfont itsm-all"></span>
<div class="name">
itsm-all
</div>
<div class="code-name">.itsm-all
</div>
</li>
<li class="dib">
<span class="icon iconfont itsm-reply"></span>
<div class="name">
itsm-reply
</div>
<div class="code-name">.itsm-reply
</div>
</li>
<li class="dib">
<span class="icon iconfont itsm-information"></span>
<div class="name">
itsm-information
</div>
<div class="code-name">.itsm-information
</div>
</li>
<li class="dib">
<span class="icon iconfont itsm-contact"></span>
<div class="name">
itsm-contact
</div>
<div class="code-name">.itsm-contact
</div>
</li>
<li class="dib">
<span class="icon iconfont itsm-my-my_already_handle"></span>
<div class="name">
itsm-my-processed
</div>
<div class="code-name">.itsm-my-my_already_handle
</div>
</li>
<li class="dib">
<span class="icon iconfont rule_7"></span>
<div class="name">
rule_7
</div>
<div class="code-name">.rule_7
</div>
</li>
<li class="dib">
<span class="icon iconfont itsm-my-completed"></span>
<div class="name">
itsm-my-completed
</div>
<div class="code-name">.itsm-my-completed
</div>
</li>
<li class="dib">
<span class="icon iconfont itsm-my-plan"></span>
<div class="name">
itsm-my-plan
</div>
<div class="code-name">.itsm-my-plan
</div>
</li>
<li class="dib">
<span class="icon iconfont rule_100"></span>
<div class="name">
@@ -5759,11 +6065,11 @@
</li>
<li class="dib">
<span class="icon iconfont itsm-node-strat"></span>
<span class="icon iconfont itsm-node-start"></span>
<div class="name">
itsm-node-strat
</div>
<div class="code-name">.itsm-node-strat
<div class="code-name">.itsm-node-start
</div>
</li>
@@ -6856,6 +7162,15 @@
</div>
</li>
<li class="dib">
<span class="icon iconfont ops-setting-notice-email-selected-copy"></span>
<div class="name">
ops-setting-notice-email-selected
</div>
<div class="code-name">.ops-setting-notice-email-selected-copy
</div>
</li>
<li class="dib">
<span class="icon iconfont ops-setting-notice"></span>
<div class="name">
@@ -9637,6 +9952,166 @@
<div class="content symbol">
<ul class="icon_lists dib-box">
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#wechatApp"></use>
</svg>
<div class="name">wechatApp</div>
<div class="code-name">#wechatApp</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#robot"></use>
</svg>
<div class="name">robot</div>
<div class="code-name">#robot</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#feishuApp"></use>
</svg>
<div class="name">feishuApp</div>
<div class="code-name">#feishuApp</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#dingdingApp"></use>
</svg>
<div class="name">dingdingApp</div>
<div class="code-name">#dingdingApp</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#email"></use>
</svg>
<div class="name">email</div>
<div class="code-name">#email</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#ops-setting-notice-feishu"></use>
</svg>
<div class="name">setting-feishu</div>
<div class="code-name">#ops-setting-notice-feishu</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#ops-setting-notice-feishu-selected"></use>
</svg>
<div class="name">setting-feishu-selected</div>
<div class="code-name">#ops-setting-notice-feishu-selected</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#cmdb-bar"></use>
</svg>
<div class="name">cmdb-histogram</div>
<div class="code-name">#cmdb-bar</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#cmdb-count"></use>
</svg>
<div class="name">cmdb-index</div>
<div class="code-name">#cmdb-count</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#cmdb-pie"></use>
</svg>
<div class="name">cmdb-piechart</div>
<div class="code-name">#cmdb-pie</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#cmdb-line"></use>
</svg>
<div class="name">cmdb-line</div>
<div class="code-name">#cmdb-line</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#cmdb-table"></use>
</svg>
<div class="name">cmdb-table</div>
<div class="code-name">#cmdb-table</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#itsm-all"></use>
</svg>
<div class="name">itsm-all</div>
<div class="code-name">#itsm-all</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#itsm-reply"></use>
</svg>
<div class="name">itsm-reply</div>
<div class="code-name">#itsm-reply</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#itsm-information"></use>
</svg>
<div class="name">itsm-information</div>
<div class="code-name">#itsm-information</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#itsm-contact"></use>
</svg>
<div class="name">itsm-contact</div>
<div class="code-name">#itsm-contact</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#itsm-my-my_already_handle"></use>
</svg>
<div class="name">itsm-my-processed</div>
<div class="code-name">#itsm-my-my_already_handle</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#rule_7"></use>
</svg>
<div class="name">rule_7</div>
<div class="code-name">#rule_7</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#itsm-my-completed"></use>
</svg>
<div class="name">itsm-my-completed</div>
<div class="code-name">#itsm-my-completed</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#itsm-my-plan"></use>
</svg>
<div class="name">itsm-my-plan</div>
<div class="code-name">#itsm-my-plan</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#rule_100"></use>
@@ -11287,10 +11762,10 @@
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#itsm-node-strat"></use>
<use xlink:href="#itsm-node-start"></use>
</svg>
<div class="name">itsm-node-strat</div>
<div class="code-name">#itsm-node-strat</div>
<div class="code-name">#itsm-node-start</div>
</li>
<li class="dib">
@@ -12261,6 +12736,14 @@
<div class="code-name">#ops-dot</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#ops-setting-notice-email-selected-copy"></use>
</svg>
<div class="name">ops-setting-notice-email-selected</div>
<div class="code-name">#ops-setting-notice-email-selected-copy</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#ops-setting-notice"></use>

View File

@@ -1,8 +1,8 @@
@font-face {
font-family: "iconfont"; /* Project id 3857903 */
src: url('iconfont.woff2?t=1688550067963') format('woff2'),
url('iconfont.woff?t=1688550067963') format('woff'),
url('iconfont.ttf?t=1688550067963') format('truetype');
src: url('iconfont.woff2?t=1696815443987') format('woff2'),
url('iconfont.woff?t=1696815443987') format('woff'),
url('iconfont.ttf?t=1696815443987') format('truetype');
}
.iconfont {
@@ -13,6 +13,86 @@
-moz-osx-font-smoothing: grayscale;
}
.wechatApp:before {
content: "\e88e";
}
.robot:before {
content: "\e88b";
}
.feishuApp:before {
content: "\e88c";
}
.dingdingApp:before {
content: "\e88d";
}
.email:before {
content: "\e88a";
}
.ops-setting-notice-feishu:before {
content: "\e887";
}
.ops-setting-notice-feishu-selected:before {
content: "\e888";
}
.cmdb-bar:before {
content: "\e886";
}
.cmdb-count:before {
content: "\e883";
}
.cmdb-pie:before {
content: "\e884";
}
.cmdb-line:before {
content: "\e885";
}
.cmdb-table:before {
content: "\e882";
}
.itsm-all:before {
content: "\e87f";
}
.itsm-reply:before {
content: "\e87e";
}
.itsm-information:before {
content: "\e880";
}
.itsm-contact:before {
content: "\e881";
}
.itsm-my-my_already_handle:before {
content: "\e87d";
}
.rule_7:before {
content: "\e87c";
}
.itsm-my-completed:before {
content: "\e879";
}
.itsm-my-plan:before {
content: "\e87b";
}
.rule_100:before {
content: "\e87a";
}
@@ -837,7 +917,7 @@
content: "\e7ad";
}
.itsm-node-strat:before {
.itsm-node-start:before {
content: "\e7ae";
}
@@ -1325,6 +1405,10 @@
content: "\e738";
}
.ops-setting-notice-email-selected-copy:before {
content: "\e889";
}
.ops-setting-notice:before {
content: "\e72f";
}

File diff suppressed because one or more lines are too long

View File

@@ -5,6 +5,146 @@
"css_prefix_text": "",
"description": "",
"glyphs": [
{
"icon_id": "37590786",
"name": "wechatApp",
"font_class": "wechatApp",
"unicode": "e88e",
"unicode_decimal": 59534
},
{
"icon_id": "37590798",
"name": "robot",
"font_class": "robot",
"unicode": "e88b",
"unicode_decimal": 59531
},
{
"icon_id": "37590794",
"name": "feishuApp",
"font_class": "feishuApp",
"unicode": "e88c",
"unicode_decimal": 59532
},
{
"icon_id": "37590791",
"name": "dingdingApp",
"font_class": "dingdingApp",
"unicode": "e88d",
"unicode_decimal": 59533
},
{
"icon_id": "37590776",
"name": "email",
"font_class": "email",
"unicode": "e88a",
"unicode_decimal": 59530
},
{
"icon_id": "37537876",
"name": "setting-feishu",
"font_class": "ops-setting-notice-feishu",
"unicode": "e887",
"unicode_decimal": 59527
},
{
"icon_id": "37537859",
"name": "setting-feishu-selected",
"font_class": "ops-setting-notice-feishu-selected",
"unicode": "e888",
"unicode_decimal": 59528
},
{
"icon_id": "37334642",
"name": "cmdb-histogram",
"font_class": "cmdb-bar",
"unicode": "e886",
"unicode_decimal": 59526
},
{
"icon_id": "37334651",
"name": "cmdb-index",
"font_class": "cmdb-count",
"unicode": "e883",
"unicode_decimal": 59523
},
{
"icon_id": "37334650",
"name": "cmdb-piechart",
"font_class": "cmdb-pie",
"unicode": "e884",
"unicode_decimal": 59524
},
{
"icon_id": "37334648",
"name": "cmdb-line",
"font_class": "cmdb-line",
"unicode": "e885",
"unicode_decimal": 59525
},
{
"icon_id": "37334627",
"name": "cmdb-table",
"font_class": "cmdb-table",
"unicode": "e882",
"unicode_decimal": 59522
},
{
"icon_id": "37310392",
"name": "itsm-all",
"font_class": "itsm-all",
"unicode": "e87f",
"unicode_decimal": 59519
},
{
"icon_id": "36998696",
"name": "itsm-reply",
"font_class": "itsm-reply",
"unicode": "e87e",
"unicode_decimal": 59518
},
{
"icon_id": "36639018",
"name": "itsm-information",
"font_class": "itsm-information",
"unicode": "e880",
"unicode_decimal": 59520
},
{
"icon_id": "36639017",
"name": "itsm-contact",
"font_class": "itsm-contact",
"unicode": "e881",
"unicode_decimal": 59521
},
{
"icon_id": "36557425",
"name": "itsm-my-processed",
"font_class": "itsm-my-my_already_handle",
"unicode": "e87d",
"unicode_decimal": 59517
},
{
"icon_id": "36488174",
"name": "rule_7",
"font_class": "rule_7",
"unicode": "e87c",
"unicode_decimal": 59516
},
{
"icon_id": "36380087",
"name": "itsm-my-completed",
"font_class": "itsm-my-completed",
"unicode": "e879",
"unicode_decimal": 59513
},
{
"icon_id": "36380096",
"name": "itsm-my-plan",
"font_class": "itsm-my-plan",
"unicode": "e87b",
"unicode_decimal": 59515
},
{
"icon_id": "36304673",
"name": "rule_100",
@@ -1450,7 +1590,7 @@
{
"icon_id": "35024980",
"name": "itsm-node-strat",
"font_class": "itsm-node-strat",
"font_class": "itsm-node-start",
"unicode": "e7ae",
"unicode_decimal": 59310
},
@@ -2301,6 +2441,13 @@
"unicode": "e738",
"unicode_decimal": 59192
},
{
"icon_id": "37575490",
"name": "ops-setting-notice-email-selected",
"font_class": "ops-setting-notice-email-selected-copy",
"unicode": "e889",
"unicode_decimal": 59529
},
{
"icon_id": "34108346",
"name": "ops-setting-notice",

Binary file not shown.

View File

@@ -12,6 +12,9 @@ import zhCN from 'ant-design-vue/lib/locale-provider/zh_CN'
import { AppDeviceEnquire } from '@/utils/mixin'
import { debounce } from './utils/util'
import { h } from 'snabbdom'
import { DomEditor, Boot } from '@wangeditor/editor'
export default {
mixins: [AppDeviceEnquire],
provide() {
@@ -47,6 +50,134 @@ export default {
this.$store.dispatch('setWindowSize')
})
)
// 注册富文本自定义元素
const resume = {
type: 'attachment',
attachmentLabel: '',
attachmentValue: '',
children: [{ text: '' }], // void 元素必须有一个 children 其中只有一个空字符串重要
}
function withAttachment(editor) {
// JS 语法
const { isInline, isVoid } = editor
const newEditor = editor
newEditor.isInline = (elem) => {
const type = DomEditor.getNodeType(elem)
if (type === 'attachment') return true // 针对 type: attachment 设置为 inline
return isInline(elem)
}
newEditor.isVoid = (elem) => {
const type = DomEditor.getNodeType(elem)
if (type === 'attachment') return true // 针对 type: attachment 设置为 void
return isVoid(elem)
}
return newEditor // 返回 newEditor 重要
}
Boot.registerPlugin(withAttachment)
/**
* 渲染附件元素到编辑器
* @param elem 附件元素即上文的 myResume
* @param children 元素子节点void 元素可忽略
* @param editor 编辑器实例
* @returns vnode 节点通过 snabbdom.js h 函数生成
*/
function renderAttachment(elem, children, editor) {
// JS 语法
// 获取附件的数据参考上文 myResume 数据结构
const { attachmentLabel = '', attachmentValue = '' } = elem
// 附件元素 vnode
const attachVnode = h(
// HTML tag
'span',
// HTML 属性样式事件
{
props: { contentEditable: false }, // HTML 属性驼峰式写法
style: {
display: 'inline-block',
margin: '0 3px',
padding: '0 3px',
backgroundColor: '#e6f7ff',
border: '1px solid #91d5ff',
borderRadius: '2px',
color: '#1890ff',
}, // style 驼峰式写法
on: {
click() {
console.log('clicked', attachmentValue)
} /* 其他... */,
},
},
// 子节点
[attachmentLabel]
)
return attachVnode
}
const renderElemConf = {
type: 'attachment', // 新元素 type 重要
renderElem: renderAttachment,
}
Boot.registerRenderElem(renderElemConf)
/**
* 生成附件元素的 HTML
* @param elem 附件元素即上文的 myResume
* @param childrenHtml 子节点的 HTML 代码void 元素可忽略
* @returns 附件元素的 HTML 字符串
*/
function attachmentToHtml(elem, childrenHtml) {
// JS 语法
// 获取附件元素的数据
const { attachmentValue = '', attachmentLabel = '' } = elem
// 生成 HTML 代码
const html = `<span data-w-e-type="attachment" data-w-e-is-void data-w-e-is-inline data-attachmentValue="${attachmentValue}" data-attachmentLabel="${attachmentLabel}">${attachmentLabel}</span>`
return html
}
const elemToHtmlConf = {
type: 'attachment', // 新元素的 type 重要
elemToHtml: attachmentToHtml,
}
Boot.registerElemToHtml(elemToHtmlConf)
/**
* 解析 HTML 字符串生成附件元素
* @param domElem HTML 对应的 DOM Element
* @param children 子节点
* @param editor editor 实例
* @returns 附件元素如上文的 myResume
*/
function parseAttachmentHtml(domElem, children, editor) {
// JS 语法
// DOM element 中获取附件的信息
const attachmentValue = domElem.getAttribute('data-attachmentValue') || ''
const attachmentLabel = domElem.getAttribute('data-attachmentLabel') || ''
// 生成附件元素按照此前约定的数据结构
const myResume = {
type: 'attachment',
attachmentValue,
attachmentLabel,
children: [{ text: '' }], // void node 必须有 children 其中有一个空字符串重要
}
return myResume
}
const parseHtmlConf = {
selector: 'span[data-w-e-type="attachment"]', // CSS 选择器匹配特定的 HTML 标签
parseElemHtml: parseAttachmentHtml,
}
Boot.registerParseElemHtml(parseHtmlConf)
},
beforeDestroy() {
clearInterval(this.timer)

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({

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