Compare commits

..

440 Commits

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-25 23:35:30 +00:00
pycook 77bc850d4a
Merge pull request #129 from veops/dev_ui
前端更新
2023-07-25 18:19:47 +08:00
wang-liang0615 e52f201ba1 Merge branch 'master' of github.com:veops/cmdb into dev_ui 2023-07-25 13:11:03 +08:00
wang-liang0615 64aea424dc 授权高亮提示 2023-07-25 13:10:45 +08:00
pycook 0655b0e9eb add command cmdb-index-table-upgrade 2023-07-25 10:31:30 +08:00
wang-liang0615 cce0649299 style 新建属性行错乱 2023-07-25 10:18:22 +08:00
pycook 52574c64cc 废弃3个表: c_value_datetime c_value_floats c_value_integers, time类型属性值增加写入校验 2023-07-24 21:55:00 +08:00
pycook fb904b01a6 禁止删除唯一标识的属性 2023-07-21 15:58:41 +08:00
pycook 63af79ec45 Merge branch 'master' of github.com:veops/cmdb 2023-07-20 18:37:14 +08:00
pycook 38af86317a fix docker-compose 2023-07-20 18:36:32 +08:00
pycook 03bac86588
Merge pull request #127 from veops/dev_ui
fix currentValueType
2023-07-20 15:39:11 +08:00
wang-liang0615 130b68cadd fix currentValueType 2023-07-20 15:30:12 +08:00
pycook 65000f8141 更新架构图 2023-07-20 11:01:25 +08:00
pycook 23692ad50b update readme 2023-07-20 11:01:25 +08:00
pycook 16cd34e8b8 update readme 2023-07-20 11:01:25 +08:00
pycook 985f67ee47 lint 2023-07-20 11:01:25 +08:00
wang-liang0615 8d95f8d57d 角色授权 2023-07-20 11:01:25 +08:00
pycook cf6230008d 清理空间 2023-07-20 11:01:25 +08:00
songbing01249 ec97fa84d8 fix(ci_type_group_manager): fix resources issues 2023-07-20 11:01:25 +08:00
pycook 76f074704b update cmdb_api.md 2023-07-20 10:56:58 +08:00
wang-liang0615 e5addab3af 删除fullscreen相关代码 2023-07-19 17:46:27 +08:00
wang-liang0615 1c6be9e281 format 2023-07-19 15:36:46 +08:00
wang-liang0615 9552892c68 ops table getVxetableRef 2023-07-19 14:39:57 +08:00
wang-liang0615 b59e1af318 编译 acl 2023-07-19 13:52:24 +08:00
wang-liang0615 d164d883ab Merge branch 'master' of github.com:veops/cmdb into dev_ui 2023-07-18 15:16:50 +08:00
wang-liang0615 1fef160d9e 删除不必要文件 2023-07-18 15:16:32 +08:00
wang-liang0615 2e537d390a acl 样式升级 2023-07-18 15:14:35 +08:00
pycook 5b9fe15afa Merge pull request #119 from veops/dev_ui
模型属性 is_index
2023-07-17 18:15:28 +08:00
wang-liang0615 89fa5f2243 Merge branch 'master' of github.com:veops/cmdb into dev_ui 2023-07-17 17:21:26 +08:00
wang-liang0615 652a5c7fb8 模型属性 is_index 2023-07-17 17:19:44 +08:00
pycook afb6adec89 update local.md 2023-07-17 13:32:34 +08:00
pycook a9db4285ab Merge pull request #118 from veops/dev_ui
Dev UI
2023-07-14 18:06:02 +08:00
wang-liang0615 a04bdc29a5 删除usedfc 2023-07-14 15:20:58 +08:00
wang-liang0615 91e0e076a7 删除usedfc 2023-07-14 15:20:36 +08:00
wang-liang0615 339a7b857e acl 前端 2023-07-14 14:34:35 +08:00
pycook e86e5ad1fd PyJWT==2.4.0 2023-07-13 17:07:33 +08:00
pycook c50a69de77 Merge pull request #117 from lovvvve/patch-4
Update click_cmdb.py
2023-07-13 15:51:36 +08:00
lovvvve 4d16e9e6d9 Update click_cmdb.py
add-user remove --is_admin
2023-07-13 15:49:35 +08:00
pycook fcea4dcb9f Merge pull request #116 from lovvvve/patch-3
Update click_cmdb.py
2023-07-13 15:23:24 +08:00
lovvvve f98fd24c62 Update click_cmdb.py
add-user  remove --is_admin
2023-07-13 15:18:47 +08:00
pycook f10eeb8439 update README 2023-07-13 09:34:17 +08:00
pycook f070948122 Merge pull request #115 from veops/doc
change screenshot image
2023-07-12 17:22:58 +08:00
ivonGwy 4112bcf547 change screenshot image 2023-07-12 17:21:10 +08:00
pycook 2292756bf7 Merge pull request #114 from veops/doc
change image size
2023-07-12 17:11:20 +08:00
ivonGwy 93e2483974 change image size 2023-07-12 17:07:17 +08:00
pycook fbb4fcc255 Merge pull request #113 from veops/doc
add qrcode for gzh
2023-07-12 16:49:12 +08:00
ivonGwy fc77241006 add qrcode for gzh 2023-07-12 16:12:17 +08:00
pycook 0d04ad7d90 update requirements 2023-07-12 15:32:46 +08:00
pycook e6290e49ea update docker-compose 2023-07-12 11:59:51 +08:00
pycook 97aa2e0ebe remove .gitattributes 2023-07-12 11:50:05 +08:00
pycook 939d9dc3cd md format 2023-07-12 10:14:47 +08:00
pycook 576d2e3bc4 Update README.md 2023-07-12 10:09:11 +08:00
pycook 9a40246d29 Update README.md 2023-07-12 10:01:15 +08:00
pycook 044f95c3be docker-compose 构建后的默认账号密码 2023-07-11 19:40:40 +08:00
pycook a386de355e docker-compose is ok 2023-07-11 18:22:17 +08:00
pycook b93afc1790 docker-compose is ok 2023-07-11 18:12:22 +08:00
pycook 77d89677ef 前后端全面升级 2023-07-10 20:13:39 +08:00
pycook 7ec6775f03 友链Spug 2023-07-10 20:07:31 +08:00
pycook 98cc853dbc 前后端全面升级 2023-07-10 20:07:20 +08:00
pycook f57ff80099 Merge pull request #90 from lovvvve/patch-2
fix: 🐛 db search
2021-11-10 20:18:18 +08:00
lovvvve 51e4b5dd8f fix: 🐛 db search
Escape ":" character in SQLAlchemy
2021-11-10 18:56:42 +08:00
pycook dbf44a020b Merge pull request #77 from x-7/x-7-patch-1
cmdb-api:add attr check in ci_manager update method
2021-04-22 09:14:33 +08:00
x-7 8e578797ef Update ci.py
cmdb-api:add attr check in ci_manager update method
2021-04-21 19:17:05 +08:00
pycook 158de4b946 Merge pull request #69 from lovvvve/patch-1
CiManager.add and AttributeValueManager.create_or_update_attr_value update
2021-01-28 17:09:01 +08:00
lovvvve 3cf234d49e Update value.py
value type 是 int 或 float 时 value 值等于 0 是会删除 的 BUG
2021-01-28 16:57:20 +08:00
lovvvve a7debc1b3b Update ci.py
兼容 py2
2021-01-28 16:56:04 +08:00
lovvvve 9268da2ffa Update value.py 2021-01-28 16:46:19 +08:00
lovvvve cfcb092478 Update value.py
feat(AttributeValueManager.create_or_update_attr_value()): AttributeValue update skip The same value
2021-01-28 16:32:53 +08:00
lovvvve 0d8b41b64a Update value.py
feat(AttributeValueManager.create_or_update_attr_value()): AttributeValue update skip The same value
2021-01-28 16:30:17 +08:00
lovvvve d85715793f Update ci.py
feat(CiManager.add()): Check the attribute is in the ci_type attributes list
2021-01-28 16:27:56 +08:00
pycook afbdbe4682 Merge pull request #65 from shaohaojiecoder/stable
Stable
2020-12-14 09:20:15 +08:00
shaohaojiecoder e629abebb7 remove weeds 2020-12-13 16:58:06 +08:00
shaohaojiecoder 029c12365a delay render 2020-12-13 16:42:17 +08:00
pycook 4d000d9805 yarn.lock update 2020-11-22 11:45:33 +08:00
pycook f1fc66bd2c Fix github security 2020-11-22 11:42:17 +08:00
pycook d6af4af1d1 upgrade ui packages 2020-11-22 11:13:46 +08:00
pycook 7fe2bdca5f Create codeql-analysis.yml 2020-11-22 10:45:45 +08:00
pycook 1432131d2b Merge pull request #59 from pycook/dependabot/npm_and_yarn/cmdb-ui/dot-prop-4.2.1
Bump dot-prop from 4.2.0 to 4.2.1 in /cmdb-ui
2020-11-22 10:28:34 +08:00
pycook bc94d039f5 Merge pull request #52 from pycook/dependabot/npm_and_yarn/cmdb-ui/elliptic-6.5.3
Bump elliptic from 6.4.1 to 6.5.3 in /cmdb-ui
2020-11-22 10:27:49 +08:00
pycook 5abafed9c8 Merge pull request #54 from pycook/dependabot/npm_and_yarn/cmdb-ui/quill-1.3.7
Bump quill from 1.3.6 to 1.3.7 in /cmdb-ui
2020-11-22 10:27:15 +08:00
dependabot[bot] 04e249feac Bump dot-prop from 4.2.0 to 4.2.1 in /cmdb-ui
Bumps [dot-prop](https://github.com/sindresorhus/dot-prop) from 4.2.0 to 4.2.1.
- [Release notes](https://github.com/sindresorhus/dot-prop/releases)
- [Commits](https://github.com/sindresorhus/dot-prop/compare/v4.2.0...v4.2.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-11-22 02:26:41 +00:00
pycook ef3e6bc6b0 Merge pull request #55 from pycook/dependabot/npm_and_yarn/cmdb-ui/handlebars-4.7.6
Bump handlebars from 4.4.5 to 4.7.6 in /cmdb-ui
2020-11-22 10:26:38 +08:00
pycook d9d5f8f818 Merge pull request #56 from pycook/dependabot/npm_and_yarn/cmdb-ui/http-proxy-1.18.1
Bump http-proxy from 1.17.0 to 1.18.1 in /cmdb-ui
2020-11-22 10:25:58 +08:00
dependabot[bot] 578da0807c Bump http-proxy from 1.17.0 to 1.18.1 in /cmdb-ui
Bumps [http-proxy](https://github.com/http-party/node-http-proxy) from 1.17.0 to 1.18.1.
- [Release notes](https://github.com/http-party/node-http-proxy/releases)
- [Changelog](https://github.com/http-party/node-http-proxy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/http-party/node-http-proxy/compare/1.17.0...1.18.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-09-11 00:19:27 +00:00
dependabot[bot] 3eb35f5497 Bump handlebars from 4.4.5 to 4.7.6 in /cmdb-ui
Bumps [handlebars](https://github.com/wycats/handlebars.js) from 4.4.5 to 4.7.6.
- [Release notes](https://github.com/wycats/handlebars.js/releases)
- [Changelog](https://github.com/handlebars-lang/handlebars.js/blob/master/release-notes.md)
- [Commits](https://github.com/wycats/handlebars.js/compare/v4.4.5...v4.7.6)

Signed-off-by: dependabot[bot] <support@github.com>
2020-09-10 03:19:57 +00:00
dependabot[bot] 9669ad04cd Bump quill from 1.3.6 to 1.3.7 in /cmdb-ui
Bumps [quill](https://github.com/quilljs/quill) from 1.3.6 to 1.3.7.
- [Release notes](https://github.com/quilljs/quill/releases)
- [Changelog](https://github.com/quilljs/quill/blob/v1.3.7/CHANGELOG.md)
- [Commits](https://github.com/quilljs/quill/compare/v1.3.6...v1.3.7)

Signed-off-by: dependabot[bot] <support@github.com>
2020-09-04 00:38:10 +00:00
dependabot[bot] 70214807ca Bump elliptic from 6.4.1 to 6.5.3 in /cmdb-ui
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.4.1 to 6.5.3.
- [Release notes](https://github.com/indutny/elliptic/releases)
- [Commits](https://github.com/indutny/elliptic/compare/v6.4.1...v6.5.3)

Signed-off-by: dependabot[bot] <support@github.com>
2020-08-01 08:41:43 +00:00
pycook 7c1c309f7a Merge pull request #51 from pycook/dependabot/npm_and_yarn/cmdb-ui/lodash-4.17.19
Bump lodash from 4.17.14 to 4.17.19 in /cmdb-ui
2020-07-21 18:17:46 +08:00
dependabot[bot] 9b9799ff5e Bump lodash from 4.17.14 to 4.17.19 in /cmdb-ui
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.14 to 4.17.19.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.14...4.17.19)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-19 03:35:06 +00:00
pycook b2578b61fa add command: add-user | del-user 2020-06-11 21:37:41 +08:00
pycook 619f47ae13 Merge pull request #49 from pycook/dependabot/npm_and_yarn/cmdb-ui/websocket-extensions-0.1.4
Bump websocket-extensions from 0.1.3 to 0.1.4 in /cmdb-ui
2020-06-08 18:37:31 +08:00
dependabot[bot] 37c5e31799 Bump websocket-extensions from 0.1.3 to 0.1.4 in /cmdb-ui
Bumps [websocket-extensions](https://github.com/faye/websocket-extensions-node) from 0.1.3 to 0.1.4.
- [Release notes](https://github.com/faye/websocket-extensions-node/releases)
- [Changelog](https://github.com/faye/websocket-extensions-node/blob/master/CHANGELOG.md)
- [Commits](https://github.com/faye/websocket-extensions-node/compare/0.1.3...0.1.4)

Signed-off-by: dependabot[bot] <support@github.com>
2020-06-07 15:54:30 +00:00
pycook ab70b2a655 Merge pull request #48 from lovvvve/master
fix(sso login): sso login redirect
2020-06-01 12:05:05 +08:00
Lovvvve c285606f4a fix(sso login): sso login redirect 2020-06-01 12:01:55 +08:00
Lovvvve 6d3611bd73 fix(sso login): sso login redirect 2020-06-01 11:48:21 +08:00
pycook 764f6a07e0 fix merge conflict 2020-05-28 20:39:47 +08:00
pycook ae8d487af4 Readme is in Chinese by default
Committer: pycook <pycook@126.com>

Author:    pycook <pycook@126.com>
2020-05-28 20:35:11 +08:00
pycook 87c6554555 update readme 2020-05-28 20:28:49 +08:00
pycook f5671c2a2a Fix: spelling mistakes 2020-05-28 20:28:49 +08:00
pycook 43ad3dfa7b release version 2.1 2020-05-28 20:28:49 +08:00
pycook 29fa17a0b8 update readme 2020-04-10 17:22:33 +08:00
pycook 5191d6ed73 Merge branch 'master' of https://github.com/pycook/cmdb 2020-04-07 18:03:03 +08:00
pycook 8348f8e7b1 Fix the judgment of app admin 2020-04-07 18:02:26 +08:00
pycook 75c48a0807 Fix: spelling mistakes 2020-04-01 21:40:51 +08:00
pycook 5b38385f7e release version 2.1 2020-04-01 21:20:47 +08:00
pycook 036e1d236b auth with ldap 2020-04-01 20:30:44 +08:00
pycook c31be0f753 UI: batch update relation 2020-04-01 11:09:41 +08:00
pycook 764d2fac3f add .eslintrc.js 2020-03-26 17:35:26 +08:00
pycook f4079e9c3e Merge pull request #42 from pycook/dependabot/npm_and_yarn/cmdb-ui/yarn-1.22.0
Bump yarn from 1.21.1 to 1.22.0 in /cmdb-ui
2020-03-26 17:29:53 +08:00
pycook 2a0ed72235 fix: delete attribute 2020-03-23 15:49:33 +08:00
dependabot[bot] 9e803ae4c7 Bump yarn from 1.21.1 to 1.22.0 in /cmdb-ui
Bumps [yarn](https://github.com/yarnpkg/yarn) from 1.21.1 to 1.22.0.
- [Release notes](https://github.com/yarnpkg/yarn/releases)
- [Changelog](https://github.com/yarnpkg/yarn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/yarnpkg/yarn/compare/v1.21.1...v1.22.0)

Signed-off-by: dependabot[bot] <support@github.com>
2020-03-22 03:14:30 +00:00
pycook bebdb61adf update deps 2020-03-22 11:14:09 +08:00
pycook f49cad771b Merge pull request #41 from pycook/dependabot/npm_and_yarn/cmdb-ui/acorn-5.7.4
Bump acorn from 5.7.3 to 5.7.4 in /cmdb-ui
2020-03-15 13:06:51 +08:00
dependabot[bot] a5b4fbda40 Bump acorn from 5.7.3 to 5.7.4 in /cmdb-ui
Bumps [acorn](https://github.com/acornjs/acorn) from 5.7.3 to 5.7.4.
- [Release notes](https://github.com/acornjs/acorn/releases)
- [Commits](https://github.com/acornjs/acorn/compare/5.7.3...5.7.4)

Signed-off-by: dependabot[bot] <support@github.com>
2020-03-14 18:55:18 +00:00
pycook 2cce2d5cf2 Fix: permission management 2020-03-13 10:30:21 +08:00
pycook e720b7af66 Merge branch 'develop' of https://github.com/pycook/cmdb into develop 2020-03-10 17:03:23 +08:00
pycook 09e4a5111b Merge pull request #40 from OhBonsai/develop
chore: use wait script to hang api before cache/db/es started
2020-03-10 17:01:58 +08:00
penzai 3539b12503 chore: use wait script to hang api before cache/db/es started 2020-03-10 16:49:40 +08:00
pycook 21d8673b5d Merge pull request #38 from AngrygrayWolf/dev
Change the requirements to support python3.8
2020-03-07 22:06:25 +08:00
what 7154426dc7 Change Pipfile 2020-03-07 21:51:37 +08:00
what ca75c7dcd0 Change the requirements to support python3.8 2020-03-07 21:20:46 +08:00
pycook 194a2254a6 fix case sensitive of ES search 2020-02-28 14:32:51 +08:00
pycook 26abad14d0 Merge branch 'develop' of https://github.com/pycook/cmdb into develop 2020-02-23 23:13:59 +08:00
pycook 1521a71f9c alter table c_preference_relation_views column name varchar(64) 2020-02-23 23:13:22 +08:00
pycook d425b455f1 Merge pull request #36 from OhBonsai/develop
test: add ci and ci relation crud test cases
2020-02-23 23:12:36 +08:00
pycook 230307474b version 2.1 and update readme 2020-02-23 20:21:13 +08:00
pycook 69d6b40e39 / redirect to /relation_views 2020-02-23 18:53:28 +08:00
pycook 5dc2f89e7f fix i18n 2020-02-23 18:41:23 +08:00
pycook 9eaca4d6a0 add library future 2020-02-21 23:29:20 +08:00
pycook 3680a462f5 Remove Chinese comments 2020-02-21 23:14:26 +08:00
pycook 3ac50e7cd8 lint 2020-02-21 22:46:12 +08:00
pycook 21b2cc1d5d The resource view is made into a two-level menu 2020-02-21 22:44:10 +08:00
penzai cd5448cc7d test: add ci and ci relation crud test cases 2020-02-18 22:05:13 +08:00
pycook 10610bdb4b logo left justify 2020-02-16 19:18:51 +08:00
pycook b5c2156387 Merge pull request #35 from shaohaojiecoder/i18n
I18n
2020-02-16 19:06:58 +08:00
pycook b05ae0d1a7 Merge pull request #34 from OhBonsai/develop
add test cases
2020-02-16 19:06:24 +08:00
shaohaojiecoder bbf6138d43 Merge branch 'develop' into i18n 2020-02-16 18:19:09 +08:00
shaohaojiecoder 1ba3e6a680 add a log pic 2020-02-16 18:18:15 +08:00
penzai 64045c1f93 test: add some test cases 2020-02-16 18:03:33 +08:00
penzai 5a3e55813c model: allow origin and ticket_id nullable in OperationRecord 2020-02-16 18:03:08 +08:00
penzai bc72e58886 auth: add user in flask.g when auth by jwt 2020-02-16 18:02:24 +08:00
pycook 9e78955ba1 ACL i18n 2020-02-16 17:36:03 +08:00
pycook 136853d9a4 Merge pull request #33 from shaohaojiecoder/i18n
fix local storage for defalut lang
2020-02-16 14:51:59 +08:00
pycook 036e3ad00d modeling i18n 2020-02-16 14:50:17 +08:00
shaohaojiecoder 5ce6c93237 fix local storage for defalut lang 2020-02-16 13:47:30 +08:00
pycook 43dba7f7ed Merge pull request #32 from OhBonsai/develop
fix: recycle import by celery task
2020-02-16 10:38:27 +08:00
penzai f4879d20d6 fix: recycle import by celery task 2020-02-16 09:39:33 +08:00
pycook 740e4c6034 i18n 2020-02-15 20:57:47 +08:00
pycook 0f2baa1d94 Merge pull request #31 from shaohaojiecoder/i18n
I18n
2020-02-11 09:50:50 +08:00
pycook 405b0af72c Merge branch 'develop' into i18n 2020-02-11 09:50:11 +08:00
shaohaojiecoder a4e5178979 fix meta title 2020-02-09 19:50:23 +08:00
shaohaojiecoder c14fe23283 add i18n basic structure 2020-02-09 17:54:57 +08:00
shaohaojiecoder b3a058f908 add something 2020-02-09 17:22:17 +08:00
shaohaojiecoder bd82a0e27c add some 2020-02-08 22:27:56 +08:00
pycook f22a5c3543 Define display fields 2020-02-08 17:39:42 +08:00
pycook ed81c3f091 Define display fields 2020-02-08 17:36:54 +08:00
shaohaojiecoder 07814b85f9 add basic 2020-02-07 22:05:52 +08:00
pycook db52b28d6b fix jwt decode 2020-02-06 09:59:24 +08:00
pycook fc85ba21c8 Merge pull request #29 from OhBonsai/master
fix ci_type_attr_group update bug and add ci_type test case
2020-02-04 12:47:15 +08:00
Bonsai 6c5ee3fcd9 Merge pull request #2 from pycook/master
merge from main repo
2020-02-04 10:56:03 +08:00
penzai 40f1ef88a9 test: add ci_type test cases 2020-02-04 10:54:16 +08:00
penzai bce422ffc8 fix: update attribute group without name params will fail. #tests/test_cmdb_ci_type.py::test_update_attribute_group_ci_type 2020-02-04 10:53:07 +08:00
pycook 7c79066532 Merge branch 'master' of https://github.com/pycook/cmdb 2020-01-19 18:18:43 +08:00
pycook 1129ac93fb Merge pull request #28 from shaohaojiecoder/master
fix drag group and attrs
2020-01-19 18:18:23 +08:00
haojie.shao 5ab0e7e737 fix drag group and attrs 2020-01-19 18:14:53 +08:00
pycook 23319c7417 /ci_types/<int:type_id>/attributes/transfer and /ci_types/<int:type_id>/attribute_groups/transfer 2020-01-19 17:59:32 +08:00
pycook c74f85cabb Merge pull request #26 from OhBonsai/master
test: add basic test code and attribute create api test case
2020-01-17 15:34:02 +08:00
penzai fce2b689fb Merge remote-tracking branch 'origin/master' 2020-01-17 15:10:16 +08:00
penzai 105327bb0c test: add basic test code and attribute create api test case 2020-01-17 15:08:46 +08:00
Bonsai 745c43d0a4 Merge pull request #1 from pycook/master
merge master
2020-01-17 10:05:05 +08:00
pycook 3130d94568 [fix] cycle import 2020-01-15 11:52:33 +08:00
pycook 04a66eb239 flush cache when delete attribute 2020-01-15 09:06:31 +08:00
pycook 68390ec6f1 [fix] delete CIType's attribute 2020-01-14 20:52:36 +08:00
pycook 17392be138 api docs update 2020-01-06 21:58:06 +08:00
pycook f2fdb29221 api docs update 2020-01-06 21:54:33 +08:00
pycook 4a18698423 update README 2019-12-31 22:53:21 +08:00
pycook 95ccee04f9 update README 2019-12-31 22:50:01 +08:00
pycook b60628247b Update README.md
update
2019-12-31 22:37:36 +08:00
pycook a6d7699ab4 Merge pull request #24 from fxiang21/master
add Readme of English
2019-12-31 22:32:48 +08:00
fxiang21 4b21bcc438 add Readme of English 2019-12-31 22:25:42 +08:00
pycook 33dce2f0f3 Update README.md
[fix] flask db-setup
2019-12-31 11:06:59 +08:00
pycook d43b827fe5 [fix] fuzzy search 2019-12-25 13:36:43 +08:00
pycook aec8bade41 [fix] security alerts 2019-12-25 10:19:03 +08:00
pycook 89ae89a449 [fix] validate attribute is required 2019-12-24 20:37:32 +08:00
pycook 945f90e386 disable eslint warning 2019-12-24 15:15:03 +08:00
pycook 2ba6a16613 support JSON type 2019-12-23 18:51:33 +08:00
pycook 6089039366 fix sidebar menu in mobile 2019-12-23 11:58:41 +08:00
pycook e1e5307084 add yarn.lock 2019-12-23 11:27:47 +08:00
pycook 2ff7fce9dd flask init-acl 2019-12-20 12:57:39 +09:00
pycook fc4d3e0c1a update makefile 2019-12-18 23:36:58 +09:00
pycook f66a94712e Modify code organization 2019-12-18 23:33:22 +09:00
pycook 24664c7686 catch abort exception when getting relation views 2019-12-13 09:59:38 +08:00
pycook 1d668bab6e update 2019-12-12 21:45:19 +08:00
pycook 3d4b84909e fix delete relation view 2019-12-12 21:36:33 +08:00
pycook 8341e742eb [fix] update attribute which is list 2019-12-11 18:12:10 +08:00
pycook a71ba83de0 release 2.0 2019-12-11 12:43:55 +08:00
pycook 9668131c18 V2.0 2019-12-11 12:14:23 +08:00
pycook 4a744dcad9 fix relation tree 2019-12-10 15:35:59 +08:00
pycook 2a420225e2 Merge pull request #22 from lovvvve/FixDelCi_type
fix(ci_type api): fix the judgment condition of deleting ci_type
2019-12-10 14:41:27 +08:00
Lovvvve ff67785618 fix(ci_type api): fix the judgment condition of deleting ci_type 2019-12-10 14:31:27 +08:00
pycook dfe1ba55d5 sidebar scroll 2019-12-09 17:16:38 +08:00
pycook 90b1b6b7af fix relation view 2019-12-09 12:03:58 +08:00
pycook d5fbe42ed7 relation view bugfix 2019-12-08 00:20:55 +08:00
pycook f424ad6864 acl done and bugfix 2019-12-06 22:33:31 +08:00
pycook 16b724bd40 ACL: permission management [doing] 2019-12-04 18:14:09 +08:00
pycook f70ed54cad update readme 2019-12-04 09:26:01 +08:00
pycook dd64564160 remove print 2019-12-03 22:13:14 +08:00
pycook cc2cdbcc9f fix delete ci relation 2019-12-03 21:57:44 +08:00
pycook 81fe850627 fix get second cis api 2019-12-03 20:10:27 +08:00
pycook 487d9f76f6 关系视图定义支持两只方式 2019-12-03 19:54:01 +08:00
pycook 92dd4c5dfe relation view has been optimised 2019-12-03 19:10:54 +08:00
pycook 8ee7c6daf8 version 1.5: update docker file 2019-11-30 23:07:12 +08:00
pycook 882b158d18 cmdb.sql update 2019-11-29 22:21:41 +08:00
pycook 85222443c0 relation view [done] 2019-11-29 18:11:18 +08:00
pycook 1696ecf49d relation view [doing] 2019-11-28 21:17:06 +08:00
pycook 73b92ff533 relation view define [done] 2019-11-27 18:25:53 +08:00
pycook e977bb15a5 GPLv2 2019-11-25 20:35:05 +08:00
pycook 7c46d6cdbf change to GPLv2 2019-11-25 20:33:56 +08:00
pycook 4d11c1f7db License change to GPLv3 2019-11-25 19:42:37 +08:00
pycook 0a563deb11 UI: relation type define [done] 2019-11-25 19:23:51 +08:00
pycook ba80ec4403 /acl/resources add param resource_type_id 2019-11-24 22:33:57 +08:00
pycook 3b7cc4595b fix grant 2019-11-24 22:29:51 +08:00
pycook 9fe47657a6 Merge pull request #20 from kdyq007/master
[更新] 新增角色、资源、权限页面
2019-11-24 17:29:58 +08:00
kdyq007 5a4a6caa07 Merge branch 'master' of https://github.com/kdyq007/cmdb 2019-11-24 17:21:27 +08:00
kdyq007 9dadbe1599 Merge pull request #6 from pycook/master
fix acl api
2019-11-24 16:43:53 +08:00
pycook 40d016f513 fix acl api 2019-11-24 16:35:28 +08:00
kdyq007 655edaa7c8 [更新] 完成权限管理 2019-11-24 15:40:38 +08:00
kdyq007 7fa5cff919 [更新] 完成权限管理页面 2019-11-24 15:22:18 +08:00
kdyq007 d19834ed5d Merge pull request #5 from pycook/master
同步
2019-11-23 21:53:46 +08:00
pycook b6be430aa3 fix 2019-11-23 21:50:45 +08:00
kdyq007 63792c242f [更新] 完成资源类型页面 2019-11-23 20:16:31 +08:00
kdyq007 10f7029722 [保存] 完成资源类型权限显示 2019-11-23 18:08:52 +08:00
pycook ba176542dc fix acl resource 2019-11-23 17:42:33 +08:00
kdyq007 aae3b6e2ff Merge pull request #4 from pycook/master
fix acl resource_type
2019-11-23 17:36:42 +08:00
pycook b370c7d46e fix acl resource_type 2019-11-23 17:24:43 +08:00
kdyq007 efa5a8ea5d Merge pull request #3 from pycook/master
同步
2019-11-23 14:52:41 +08:00
pycook fd532626ac relative view api [done] 2019-11-22 18:18:22 +08:00
pycook 617337c614 Realize /api/v0.1/ci_relations/s [done] 2019-11-21 18:21:03 +08:00
kdyq007 9a3d24ac81 [更新] 保存一下 2019-11-20 19:02:36 +08:00
kdyq007 454dd4c56b Merge branch 'master' of https://github.com/kdyq007/cmdb 2019-11-19 21:52:33 +08:00
kdyq007 88ad72d4dc Merge pull request #2 from pycook/master
update
2019-11-19 21:52:02 +08:00
kdyq007 8d1517d550 [更新] 完成基础role和user管理 2019-11-19 21:49:51 +08:00
pycook d3a8ef5966 fix get user by uid 2019-11-19 21:46:53 +08:00
pycook e5baa5012d acl: resource type api 2019-11-19 21:41:46 +08:00
pycook a1f63b00dd fix search 2019-11-19 18:32:35 +08:00
pycook 47ded84231 elastic search [done] 2019-11-19 18:16:31 +08:00
kdyq007 224a48a5f3 [更新] 去除app_id 2019-11-18 22:22:38 +08:00
pycook 0e7c52df71 es search update 2019-11-18 22:05:59 +08:00
pycook ff701cc770 search by elasticsearch [doing] 2019-11-18 20:02:25 +08:00
kdyq007 6a7bb725cc Merge pull request #1 from pycook/master
怎么玩的?反向pull request
2019-11-18 18:31:14 +08:00
pycook 0a13186c13 fix acl api 2019-11-18 12:02:02 +08:00
kdyq007 a0ffeb9950 [更新] 完成角色管理页面 2019-11-17 21:08:04 +08:00
kdyq007 6c70ec6d53 [更新] 完成roles基本接口 2019-11-17 17:09:24 +08:00
qiqi 4b5f82699a [更新] 完成用户管理页面 2019-11-17 09:32:39 +08:00
pycook f78c3b928b pep8 2019-11-15 18:03:06 +08:00
pycook 332659c1d5 update acl 2019-11-15 16:54:56 +08:00
pycook 3beb2706dc Merge pull request #18 from kdyq007/master
[更新] 修改图片路径、压缩图片
2019-11-14 21:59:38 +08:00
qiqi a14111e1ce [更新] 优化格式 2019-11-14 21:51:58 +08:00
qiqi c4320c14f9 [更新] 更换图片位置、压缩图片 2019-11-14 21:48:36 +08:00
qiqi 4c5442748f [更新] 优化说明文件格式 2019-11-14 21:00:24 +08:00
qiqi a81750acba [更新] 新增Q群 README.md 2019-11-14 20:55:48 +08:00
pycook 0439e2462b update acl 2019-11-14 18:35:31 +08:00
pycook 3b62bd7ac9 update readme 2019-11-13 14:02:02 +08:00
pycook f6add52721 python3.7 timezone fix 2019-11-13 13:56:44 +08:00
pycook c85e535288 update acl 2019-11-13 13:25:42 +08:00
pycook c0c6d116b5 docker images use aliyun 2019-11-13 11:56:17 +08:00
pycook 39153e92d1 update Makefile and support for install by make 2019-11-12 11:55:04 +08:00
pycook 42bcc2e510 fix py3 2019-11-12 11:15:25 +08:00
pycook 398fbb25dc merge Dockerfile 2019-11-12 10:40:37 +08:00
pycook 4b312d4f99 delete docs/Dockerfile 2019-11-11 23:12:50 +08:00
pycook 10414155a5 fix timezone 2019-11-11 23:11:12 +08:00
pycook feda0c37e7 update README 2019-11-11 16:10:02 +08:00
pycook 173c120b64 flask init-cache 2019-11-11 15:46:57 +08:00
pycook 5f2a0d1a7b Remove package-lock.json and remove some compile warnings 2019-11-11 13:16:07 +08:00
pycook 50f894a01d add command init-cache 2019-11-11 11:27:43 +08:00
pycook 66e93e73af Merge branch 'master' of https://github.com/pycook/cmdb 2019-11-11 09:20:07 +08:00
pycook 58ad9d3f05 vue lint 2019-11-11 00:25:22 +08:00
pycook 08c96039e9 gunicorn==19.5.0 2019-11-10 19:10:23 +08:00
pycook ca0dd97626 Docker to production 2019-11-10 19:06:38 +08:00
pycook 7810ee3974 Partially completed backend development of permissions management 2019-11-08 17:42:13 +08:00
pycook 2cfea7ef08 Update README.md
docker 一键安装说明补充
2019-11-08 15:26:22 +08:00
pycook 0cee6cea25 fix py2.7 unicode encoding error 2019-11-08 15:15:31 +08:00
pycook 5d13ba2f26 users drop is_admin 2019-11-08 14:58:21 +08:00
pycook a583433530 fix unicode encode error 2019-11-08 14:37:53 +08:00
fxiang21 733ac3b2b4 移除多余的docker-start目录 2019-11-08 09:20:34 +08:00
fxiang21 ef6300255a 修复nginx转发问题 2019-11-08 09:20:27 +08:00
fxiang21 aad37dcf0b 添加容器化部署方式 2019-11-08 09:20:09 +08:00
pycook cce10d39ea code format 2019-11-07 19:18:31 +08:00
pycook c521dd447e Update README.md
pipenv run flask run -h 0.0.0.0
2019-11-05 17:52:45 +08:00
pycook 4d0cd4ba56 Update README.md
如果是非本机访问, 要修改ui/.env里VUE_APP_API_BASE_URL里的IP地址
2019-11-05 17:44:48 +08:00
pycook 7291274cb1 Update README.md
如果是非本机访问, 要修改ui/.env里VUE_APP_API_BASE_URL里的IP地址
2019-11-05 17:43:40 +08:00
pycook 44f2e383c3 update overview jpeg url 2019-11-01 11:37:16 +08:00
pycook 1f8219b418 fix add integer list 2019-11-01 11:27:24 +08:00
pycook cb2f170ded mkdir logs, ignore *.log 2019-11-01 10:45:35 +08:00
pycook 1241a23ba8 Update README.md
add cmdb.sql
2019-10-28 21:48:46 +08:00
pycook 7d7744b7dc add docs/cmdb.sql 2019-10-28 21:46:41 +08:00
pycook 9c7d51127a fix date picker 2019-10-24 20:43:59 +08:00
pycook b5a987f6b4 choice value tip fix 2019-10-24 20:43:58 +08:00
pycook 7bbc68bfd5 fix delete ci type 2019-10-24 20:43:58 +08:00
pycook 99d11e11ce fix ci types show 2019-10-24 20:43:58 +08:00
pycook 7b96ac4638 attribute alias must be unique 2019-10-24 20:43:58 +08:00
pycook 0a36330852 fix attributes paginate 2019-10-24 20:43:54 +08:00
pycook 9105f92c82 update README 2019-10-24 20:43:51 +08:00
pycook 57541ab486 Update README.md
create tables fix
2019-10-24 20:43:51 +08:00
pycook a0fcbd220e attributes paginate and fix update value 2019-10-24 20:43:51 +08:00
pycook d54b404eb6 add docs 2019-10-24 20:43:51 +08:00
pycook 620c5bb5eb ci search return unique key 2019-10-24 20:43:51 +08:00
pycook 0fde1d699d invalid username or password -> 403 2019-10-24 20:43:51 +08:00
shaohaojiecoder 61f77cf311 add batch module 2019-10-24 20:43:51 +08:00
lilixiang 13476128d5 add 添加属性库和模型模块 2019-10-24 20:43:51 +08:00
pycook 5cdb4ecd2a Revert "add 添加属性库和模型模块" 2019-10-24 20:43:51 +08:00
lilixiang 64c3b9da3b add 添加属性库和模型模块 2019-10-24 20:43:51 +08:00
pycook 55dad7a58c cache强制unicode编码 2019-08-30 09:46:24 +08:00
pycook 38dabc35e5 add .gitattributes 2019-08-28 21:08:28 +08:00
pycook 5b4f95a50e add ui 2019-08-28 20:51:51 +08:00
pycook f3046d3c91 remove ui 2019-08-28 20:48:23 +08:00
pycook 5faae9af67 remove ui 2019-08-28 20:48:04 +08:00
pycook c0b50642e0 update README 2019-08-28 20:45:59 +08:00
pycook 12ca296879 升级后端并开源UI 2019-08-28 20:34:10 +08:00
pycook 420c6cea2b delete。。。 2016-08-26 13:46:03 +08:00
pycook ccc4bb48fa pep8 2016-06-27 10:50:32 +08:00
599 changed files with 29358 additions and 82803 deletions

6
.env
View File

@ -1,6 +0,0 @@
MYSQL_ROOT_PASSWORD='123456'
MYSQL_HOST='mysql'
MYSQL_PORT=3306
MYSQL_USER='cmdb'
MYSQL_DATABASE='cmdb'
MYSQL_PASSWORD='123456'

View File

@ -1,60 +0,0 @@
name: Bug Report
description: File a bug report
title: "[Bug]: "
labels: ["☢️ bug"]
assignees:
- Selina316
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: input
id: contact
attributes:
label: Contact Details
description: How can we get in touch with you if we need more info?
placeholder: ex. email@example.com
validations:
required: false
- type: dropdown
id: aspects
attributes:
label: This bug is related to UI or API?
multiple: true
options:
- UI
- API
- type: textarea
id: happened
attributes:
label: What happened?
description: Also tell us, what did you expect to happen?
placeholder: Tell us what you see!
value: "A bug happened!"
validations:
required: true
- type: input
id: version
attributes:
label: Version
description: What version of our software are you running?
value: "newest"
validations:
required: true
- type: dropdown
id: browsers
attributes:
label: What browsers are you seeing the problem on?
multiple: true
options:
- Firefox
- Chrome
- Safari
- Microsoft Edge
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
render: shell

View File

@ -1,44 +0,0 @@
name: Feature wanted
description: A new feature would be good
title: "[Feature]: "
labels: ["✏️ feature"]
assignees:
- pycook
body:
- type: markdown
attributes:
value: |
Thank you for your feature suggestion; we will evaluate it carefully!
- type: input
id: contact
attributes:
label: Contact Details
description: How can we get in touch with you if we need more info?
placeholder: ex. email@example.com
validations:
required: false
- type: dropdown
id: aspects
attributes:
label: feature is related to UI or API aspects?
multiple: true
options:
- UI
- API
- type: textarea
id: feature
attributes:
label: What is your advice?
description: Also tell us, what did you expect to happen?
placeholder: Tell us what you want!
value: "everyone wants this feature!"
validations:
required: true
- type: input
id: version
attributes:
label: Version
description: What version of our software are you running?
value: "newest"
validations:
required: true

View File

@ -1,36 +0,0 @@
name: Help wanted
description: I have a question
title: "[help wanted]: "
labels: ["help wanted"]
assignees:
- ivonGwy
body:
- type: markdown
attributes:
value: |
Please tell us what's you need!
- type: input
id: contact
attributes:
label: Contact Details
description: How can we get in touch with you if we need more info?
placeholder: ex. email@example.com
validations:
required: false
- type: textarea
id: question
attributes:
label: What is your question?
description: Also tell us, how can we help?
placeholder: Tell us what you need!
value: "i have a question!"
validations:
required: true
- type: input
id: version
attributes:
label: Version
description: What version of our software are you running?
value: "newest"
validations:
required: true

View File

@ -1,60 +0,0 @@
name: Bug Report
description: File a bug report
title: "[Bug]: "
labels: ["bug"]
assignees:
- pycook
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: input
id: contact
attributes:
label: Contact Details
description: How can we get in touch with you if we need more info?
placeholder: ex. email@example.com
validations:
required: false
- type: dropdown
id: type
attributes:
label: bug is related to UI or API aspects?
multiple: true
options:
- UI
- API
- type: textarea
id: what-happened
attributes:
label: What happened?
description: Also tell us, what did you expect to happen?
placeholder: Tell us what you see!
value: "A bug happened!"
validations:
required: true
- type: textarea
id: version
attributes:
label: Version
description: What version of our software are you running?
default: 2.3.5
validations:
required: true
- type: dropdown
id: browsers
attributes:
label: What browsers are you seeing the problem on?
multiple: true
options:
- Firefox
- Chrome
- Safari
- Microsoft Edge
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
render: shell

View File

@ -1,6 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: veops official website
url: https://veops.cn/#hero
about: you can contact us here.

View File

@ -1,44 +0,0 @@
name: Feature wanted
description: A new feature would be good
title: "[Feature]: "
labels: ["feature"]
assignees:
- pycook
body:
- type: markdown
attributes:
value: |
Thank you for your feature suggestion; we will evaluate it carefully!
- type: input
id: contact
attributes:
label: Contact Details
description: How can we get in touch with you if we need more info?
placeholder: ex. email@example.com
validations:
required: false
- type: dropdown
id: type
attributes:
label: feature is related to UI or API aspects?
multiple: true
options:
- UI
- API
- type: textarea
id: describe the feature
attributes:
label: What is your advice?
description: Also tell us, what did you expect to happen?
placeholder: Tell us what you want!
value: "everyone wants this feature!"
validations:
required: true
- type: textarea
id: version
attributes:
label: Version
description: What version of our software are you running?
default: 2.3.5
validations:
required: true

View File

@ -1,79 +0,0 @@
name: docker-images-build-and-release
on:
push:
branches:
- master
tags: ["v*"]
# pull_request:
# branches:
# - master
env:
# Use docker.io for Docker Hub if empty
REGISTRY_SERVER_ADDRESS: ghcr.io/veops
TAG: ${{ github.sha }}
jobs:
setup-environment:
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v4
release-api-images:
runs-on: ubuntu-latest
needs: [setup-environment]
permissions:
contents: read
packages: write
timeout-minutes: 90
steps:
- name: Checkout Repo
uses: actions/checkout@v4
- name: Login to GitHub Package Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push CMDB-API Docker image
uses: docker/build-push-action@v6
with:
file: docker/Dockerfile-API
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ env.REGISTRY_SERVER_ADDRESS }}/cmdb-api:${{ env.TAG }}
# release-ui-images:
# runs-on: ubuntu-latest
# needs: [setup-environment]
# permissions:
# contents: read
# packages: write
# timeout-minutes: 90
# steps:
# - name: Checkout Repo
# uses: actions/checkout@v4
# - name: Login to GitHub Package Registry
# uses: docker/login-action@v2
# with:
# registry: ghcr.io
# username: ${{ github.repository_owner }}
# password: ${{ secrets.GITHUB_TOKEN }}
# - name: Set up QEMU
# uses: docker/setup-qemu-action@v3
# - name: Set up Docker Buildx
# uses: docker/setup-buildx-action@v3
# - name: Build and push CMDB-UI Docker image
# uses: docker/build-push-action@v6
# with:
# file: docker/Dockerfile-UI
# context: .
# platforms: linux/amd64,linux/arm64
# push: true
# tags: ${{ env.REGISTRY_SERVER_ADDRESS }}/cmdb-ui:${{ env.TAG }}

5
.gitignore vendored
View File

@ -40,11 +40,9 @@ nosetests.xml
.pytest_cache .pytest_cache
cmdb-api/test-output cmdb-api/test-output
cmdb-api/api/uploaded_files cmdb-api/api/uploaded_files
cmdb-api/migrations/versions
# Translations # Translations
#*.mo *.mo
messages.pot
# Mr Developer # Mr Developer
.mr.developer.cfg .mr.developer.cfg
@ -71,7 +69,6 @@ settings.py
# UI # UI
cmdb-ui/node_modules cmdb-ui/node_modules
cmdb-ui/dist cmdb-ui/dist
cmdb-ui/yarn.lock
# Log files # Log files
cmdb-ui/npm-debug.log* cmdb-ui/npm-debug.log*

View File

@ -1,13 +0,0 @@
# Contributor Code of Conduct
As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
We are committed to making participation in this project a harassment-free experience for everyone, regardless of the level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion.
Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
This Code of Conduct is adapted from the [Contributor Covenant](https://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/)

View File

@ -1,4 +1,6 @@
include ./Makefile.variable MYSQL_ROOT_PASSWORD ?= root
MYSQL_PORT ?= 3306
REDIS_PORT ?= 6379
default: help default: help
help: ## display this help help: ## display this help
@ -7,7 +9,7 @@ help: ## display this help
env: ## create a development environment using pipenv env: ## create a development environment using pipenv
sudo easy_install pip && \ sudo easy_install pip && \
pip install pipenv -i https://repo.huaweicloud.com/repository/pypi/simple && \ pip install pipenv -i https://pypi.douban.com/simple && \
npm install yarn && \ npm install yarn && \
make deps make deps
.PHONY: env .PHONY: env
@ -34,7 +36,7 @@ api: ## start api server
.PHONY: api .PHONY: api
worker: ## start async tasks worker worker: ## start async tasks worker
cd cmdb-api && pipenv run celery -A celery_worker.celery worker -E -Q one_cmdb_async --autoscale=5,2 --logfile=one_cmdb_async.log -D && pipenv run celery -A celery_worker.celery worker -E -Q acl_async --autoscale=2,1 --logfile=one_acl_async.log -D 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 .PHONY: worker
ui: ## start ui server ui: ## start ui server
@ -48,25 +50,3 @@ clean: ## remove unwanted files like .pyc's
lint: ## check style with flake8 lint: ## check style with flake8
flake8 --exclude=env . flake8 --exclude=env .
.PHONY: lint .PHONY: lint
api-docker-build:
export DOCKER_CLI_EXPERIMENTAL=enabled ;\
! ( docker buildx ls | grep multi-platform-builder ) && docker buildx create --use --platform=$(BUILD_ARCH) --name multi-platform-builder ;\
docker buildx build \
--builder multi-platform-builder \
--platform=$(BUILD_ARCH) \
--tag $(REGISTRY)/cmdb-api:$(CMDB_DOCKER_VERSION) \
--tag $(REGISTRY)/cmdb-api:latest \
-f docker/Dockerfile-API \
.
ui-docker-build:
export DOCKER_CLI_EXPERIMENTAL=enabled ;\
! ( docker buildx ls | grep multi-platform-builder ) && docker buildx create --use --platform=$(BUILD_ARCH) --name multi-platform-builder ;\
docker buildx build \
--builder multi-platform-builder \
--platform=$(BUILD_ARCH) \
--tag $(REGISTRY)/cmdb-ui:$(CMDB_DOCKER_VERSION) \
--tag $(REGISTRY)/cmdb-ui:latest \
-f docker/Dockerfile-UI \
.

View File

@ -1,21 +0,0 @@
SHELL := /bin/bash -o pipefail
MYSQL_ROOT_PASSWORD ?= root
MYSQL_PORT ?= 3306
REDIS_PORT ?= 6379
LATEST_TAG_DIFF:=$(shell git describe --tags --abbrev=8)
LATEST_COMMIT:=$(VERSION)-dev-$(shell git rev-parse --short=8 HEAD)
BUILD_ARCH ?= linux/amd64,linux/arm64
# Set your version by env or using latest tags from git
CMDB_VERSION?=$(LATEST_TAG_DIFF)
ifeq ($(CMDB_VERSION),)
#fall back to last commit
CMDB_VERSION=$(LATEST_COMMIT)
endif
COMMIT_VERSION:=$(LATEST_COMMIT)
CMDB_DOCKER_VERSION:=${CMDB_VERSION}
CMDB_CHART_VERSION:=$(shell echo ${CMDB_VERSION} | sed 's/^v//g' )
REGISTRY ?= local

166
README.md
View File

@ -1,138 +1,88 @@
![维易开源CMDB](docs/images/logo.png)
<p align="center"> [![License](https://img.shields.io/badge/License-AGPLv3-brightgreen)](https://github.com/veops/cmdb/blob/master/LICENSE)
<a href="https://veops.cn"> [![UI](https://img.shields.io/badge/UI-Ant%20Design%20Pro%20Vue-brightgreen)](https://github.com/sendya/ant-design-pro-vue)
<img src="https://github.com/user-attachments/assets/c5cfb272-899b-418d-9e69-8e1dd07db0f6" alt="维易CMDB"/> [![API](https://img.shields.io/badge/API-Flask-brightgreen)](https://github.com/pallets/flask)
</a>
</p>
<h4 align="center">简单、轻量、通用的运维配置管理数据库</h4> [English](docs/README_en.md) / [中文](README.md)
- 产品文档https://veops.cn/docs/
- 在线体验: <a href="https://cmdb.veops.cn" target="_blank">CMDB</a>
- username: demo 或者 admin
- password: 123456
<p align="center"> > **重要提示**: `master` 分支在开发过程中可能处于 _不稳定的状态_
<a href="https://github.com/veops/cmdb/blob/master/LICENSE"><img src="https://img.shields.io/badge/License-AGPLv3-brightgreen" alt="License: GPLv3"></a> > 请通过[releases](https://github.com/veops/cmdb/releases)获取
<a href="https://github.com/veops/cmdb/releases"><img alt="the latest release version" src="https://img.shields.io/github/v/release/veops/cmdb?color=75C1C4&include_prereleases&label=Release&logo=github&logoColor=white"></a>
<a href="https:https://github.com/sendya/ant-design-pro-vue"><img src="https://img.shields.io/badge/UI-Ant%20Design%20Pro%20Vue-green" alt="UI"></a>
<a href="https://github.com/pallets/flask"><img src="https://img.shields.io/badge/API-Flask-bright" alt="API"></a>
<a href="https://github.com/veops/cmdb/stargazers"><img src="https://img.shields.io/github/stars/veops/cmdb" alt="Stars Badge"/></a>
<a href="https://github.com/veops/cmdb"><img src="https://img.shields.io/github/forks/veops/cmdb" alt="Forks Badge"/></a>
</p>
<p align="center">
中文(简体) · <a href="docs/README_en.md">English</a>
</p>
## 系统介绍 ## 系统介绍
维易CMDB是一个简洁、轻量且高度可定制的运维配置管理数据库CMDB。它支持灵活的模型配置和资源自动发现旨在为企业提供便捷的资产管理解决方案帮助运维团队高效地管理 IT 基础设施和服务。 ### 整体架构
- 产品文档:[https://veops.cn/docs/](https://veops.cn/docs/) <img src=docs/images/view.jpg />
- 在线体验:[https://cmdb.veops.cn](https://cmdb.veops.cn)
- 用户名demo 或者 admin ### 相关文档
- 密码123456
- **重要提示**`master` 分支在开发过程中可能处于**不稳定的状态**。请通过 [releases](https://github.com/veops/cmdb/releases) 获取最新稳定版本。 - <a href="https://zhuanlan.zhihu.com/p/98453732" 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>
### 特点
- 灵活性
1. 规范并统一纳管复杂数据资产
2. 自动发现、入库 IT 资产
- 安全性
1. 细粒度访问控制
2. 完备操作日志
- 多应用
1. 丰富视图展示维度
2. 提供 Restful API
3. 自定义字段触发器
### 主要功能 ### 主要功能
- **自定义模型和模型关系**:支持模型属性的自定义,包括下拉列表、字体颜色、计算属性等高级功能,满足不同业务需求。 - 模型属性支持索引、多值、默认排序、字体颜色,支持计算属性
- **自动发现资源**:支持计算机、网络设备、存储设备、数据库、中间件、公有云资源等自动发现。 - 支持自动发现、定时巡检、文件导入
- **多维度视图展示**:包括资源视图、层级视图、关系视图等,帮助运维人员全面管理资源。 - 支持资源、树形、关系视图展示
- **细粒度权限控制**:通过精确的访问控制和完备的操作日志保障系统的安全性。 - 支持模型间关系配置和展示
- **全面的资源搜索功能**:支持灵活的资源和关系搜索,快速定位和操作资源。 - 细粒度访问控制,完备的操作日志
- **集成 IP 地址管理IPAM和数据中心基础设施管理DCIM**:简化网络资源和数据中心设备的管理。 - 支持跨模型搜索
更多详细功能,请移步 [维易科技官网](https://veops.cn) 进行了解。
### 系统优势
- 灵活性
+ 无需指定固定运维场景,支持自由配置并内置多种模板
+ 支持自动发现和入库 IT 资产,快速搭建资产管理系统
- 安全性
+ 细粒度的权限控制机制,确保资源管理的安全性
+ 完整的操作日志记录,便于审计和问题追踪
- 多应用
+ 提供多种视图展示方式,满足不同场景的需求
+ 强大的 API 接口,支持深度集成
+ 支持定义属性触发器和计算属性,增强数据处理能力
### 技术栈
+ 后端Python [3.8-3.11]
+ 数据存储MySQL、Redis
+ 前端Vue.js
+ UI组件库Ant Design Vue
### 系统概览 ### 系统概览
<table style="border-collapse: collapse;"> - 服务树
<tr>
<td style="padding: 5px;background-color:#fff;">
<img width="400" src="https://github.com/user-attachments/assets/6d2df835-ae93-4d91-9bd9-213c270eca7a"/>
</td>
<td style="padding: 5px;background-color:#fff;">
<img width="400" src="https://github.com/user-attachments/assets/cb8b598a-a1f9-4c74-adf1-6e59aea2c9b3"/>
</td>
</tr>
<tr> ![服务树](docs/images/0.png "首页展示")
<td style="padding: 5px;background-color:#fff;">
<img width="400" src="https://github.com/user-attachments/assets/b440224f-53c3-4b7f-a9be-285d7a4b848f"/>
</td>
<td style="padding: 5px;background-color:#fff;">
<img width="400" src="https://github.com/user-attachments/assets/f457d5a0-b60b-4949-b94e-020f4c61444b"/>
</td>
</tr>
</table>
## 关注我们 [查看更多展示](docs/screenshot.md)
欢迎 Star 加关注,第一时间获取更新动态!
![star us](https://github.com/user-attachments/assets/f9056d5a-171c-4f53-9fec-d40c9e5ff94d) ### 更多功能
## 快速开始 > 也欢迎移步[维易科技官网](https://veops.cn),发现更多免费运维系统。
### 1. 搭建
+ 方案一Docker 一键快速构建
- 第1步: 安装 Docker 环境和 Docker Composev2
- 第2步: 拷贝项目代码, `git clone https://github.com/veops/cmdb.git`
- 第3步进入主目录并启动, `docker compose up -d`
+ 方案二:[本地开发环境搭建](docs/local.md)
+ 方案三:[Makefile 安装](docs/makefile.md)
### 2. 访问
- 打开浏览器并访问: [http://127.0.0.1:8000](http://127.0.0.1:8000)
- 用户名: demo 或者 admin
- 密码: 123456
## 接入公司 ## 接入公司
+ 欢迎使用开源CMDB的公司和团队,在 [#112](https://github.com/veops/cmdb/issues/112) 登记 > 欢迎使用开源CMDB的公司在 [#112](https://github.com/veops/cmdb/issues/112) 登记
## 代码贡献 ## 安装
我们欢迎所有开发者贡献代码,改善和扩展这个项目。请先阅读我们的[贡献指南](docs/CONTRIBUTING.md)。此外,您还可以通过社交媒体、活动和分享来支持 Veops 的开源。
<a href="https://github.com/veops/cmdb/graphs/contributors"> ### Docker 一键快速构建
<img src="https://contrib.rocks/image?repo=veops/cmdb" /> - 进入主目录(先安装 docker 环境)
</a>
## 更多开源 ```
docker-compose up -d
```
- [OneTerm](https://github.com/veops/oneterm): 一款简单、轻量、灵活的堡垒机服务。 - 浏览器打开: [http://127.0.0.1:8000](http://127.0.0.1:8000)
- [messenger](https://github.com/veops/messenger): 一个简单轻量的消息发送服务。 - username: demo 或者 admin
- [ACL](https://github.com/veops/acl): 一个简单通用的权限管理系统设计与实践。 - password: 123456
## 相关文章 ### [本地开发环境搭建](docs/local.md)
- <a href="https://mp.weixin.qq.com/s/v3eANth64UBW5xdyOkK3tg" target="_blank">尽可能通用的运维CMDB的设计与实践() - 概览</a> ### [Makefile 安装](docs/makefile.md)
- <a href="https://mp.weixin.qq.com/s/rQaf4AES7YJsyNQG_MKOLg" target="_blank">尽可能通用的运维CMDB的设计与实践() - 自动发现</a>
- <a href="https://github.com/veops/cmdb/tree/master/docs/cmdb_api.md" target="_blank">CMDB接口文档</a>
更多文章可以在公众号 **维易科技OneOps** 里查看 ---
## 与我联系 _**欢迎关注我们的公众号点击联系我们加入微信、QQ群(336164978),获得更多产品、行业相关资讯**_
+ 邮箱: <a href="mailto:bd@veops.cn">bd@veops.cn</a> ![公众号: 维易科技OneOps](docs/images/qrcode_for_gzh.jpg)
+ 公众号:**维易科技OneOps**。关注后可以加入微信群,参与产品和技术交流
<img src="docs/images/wechat.png" alt="公众号: 维易科技OneOps" />

View File

@ -1,79 +0,0 @@
line-length = 120
cache-dir = ".ruff_cache"
target-version = "py310"
unsafe-fixes = true
show-fixes = true
[lint]
select = [
"E",
"F",
"I",
"TCH",
# W
"W505",
# PT
"PT018",
# SIM
"SIM101",
"SIM114",
# PGH
"PGH004",
# PL
"PLE1142",
# RUF
"RUF100",
# UP
"UP007"
]
preview = true
ignore = ["FURB101"]
[lint.flake8-pytest-style]
mark-parentheses = false
parametrize-names-type = "list"
parametrize-values-row-type = "list"
parametrize-values-type = "tuple"
[lint.flake8-unused-arguments]
ignore-variadic-names = true
[lint.isort]
lines-between-types = 1
order-by-type = true
[lint.per-file-ignores]
"**/api/v1/*.py" = ["TCH"]
"**/model/*.py" = ["TCH003"]
"**/models/__init__.py" = ["F401", "F403"]
"**/tests/*.py" = ["E402"]
"celery_worker.py" = ["F401"]
"api/views/entry.py" = ["I001"]
"migrations/*.py" = ["I001", "E402"]
"*.py" = ["I001"]
"api/views/common_setting/department.py" = ["F841"]
"api/lib/common_setting/upload_file.py" = ["F841"]
"api/lib/common_setting/acl.py" = ["F841"]
"**/__init__.py" = ["F822"]
"api/tasks/*.py" = ["E722"]
"api/views/cmdb/*.py" = ["E722"]
"api/views/acl/*.py" = ["E722"]
"api/lib/secrets/*.py" = ["E722", "F841"]
"api/lib/utils.py" = ["E722", "E731"]
"api/lib/perm/authentication/cas/*" = ["E113", "F841"]
"api/lib/perm/acl/*" = ["E722"]
"api/lib/*" = ["E721", "F722"]
"api/lib/cmdb/*" = ["F722", "E722"]
"api/lib/cmdb/search/ci/es/search.py" = ["F841", "SIM114"]
"api/lib/cmdb/search/ci/db/search.py" = ["F841"]
"api/lib/cmdb/value.py" = ["F841"]
"api/lib/cmdb/history.py" = ["E501"]
"api/commands/common.py" = ["E722"]
"api/commands/click_cmdb.py" = ["E722"]
"api/lib/perm/auth.py" = ["SIM114"]
[format]
preview = true
quote-style = "single"
docstring-code-format = true
skip-magic-trailing-comma = false

View File

@ -5,31 +5,27 @@ name = "pypi"
[packages] [packages]
# Flask # Flask
Flask = "==2.2.5" Flask = "==2.3.2"
Werkzeug = "==2.2.3" Werkzeug = "==2.3.6"
click = ">=5.0" click = ">=5.0"
# Api # Api
Flask-RESTful = "==0.3.10" Flask-RESTful = "==0.3.10"
# Database # Database
Flask-SQLAlchemy = "==3.0.5" Flask-SQLAlchemy = "==2.5.0"
SQLAlchemy = "==1.4.49" SQLAlchemy = "==1.4.49"
PyMySQL = "==1.1.0" PyMySQL = "==1.1.0"
redis = "==4.6.0" redis = "==4.6.0"
python-redis-lock = "==4.0.0"
# Migrations # Migrations
Flask-Migrate = "==2.5.2" Flask-Migrate = "==2.5.2"
# Deployment # Deployment
gunicorn = "==21.0.1" gunicorn = "==21.0.1"
supervisor = "==4.0.3" supervisor = "==4.0.3"
# Auth # Auth
Flask-Login = ">=0.6.2" Flask-Login = "==0.6.2"
Flask-Bcrypt = "==1.0.1" Flask-Bcrypt = "==1.0.1"
Flask-Cors = ">=3.0.8" Flask-Cors = ">=3.0.8"
ldap3 = "==2.9.1" python-ldap = "==3.4.0"
pycryptodome = "==3.12.0" pycryptodome = "==3.12.0"
cryptography = ">=41.0.2"
# i18n
flask-babel = "==4.0.0"
# Caching # Caching
Flask-Caching = ">=1.0.0" Flask-Caching = ">=1.0.0"
# Environment variable parsing # Environment variable parsing
@ -39,21 +35,19 @@ marshmallow = "==2.20.2"
celery = "==5.3.1" celery = "==5.3.1"
celery_once = "==3.0.1" celery_once = "==3.0.1"
more-itertools = "==5.0.0" more-itertools = "==5.0.0"
kombu = ">=5.3.1" kombu = "==5.3.1"
# common setting # common setting
timeout-decorator = "==0.5.0" timeout-decorator = "==0.5.0"
WTForms = "==3.0.0" WTForms = "==3.0.0"
email-validator = "==1.3.1" email-validator = "==1.3.1"
treelib = "==1.6.1" treelib = "==1.6.1"
flasgger = "==0.9.5" flasgger = "==0.9.5"
Pillow = ">=10.0.1" Pillow = "==9.3.0"
# other # other
six = "==1.16.0" six = "==1.12.0"
bs4 = ">=0.0.1" bs4 = ">=0.0.1"
toposort = ">=1.5" toposort = ">=1.5"
requests = ">=2.22.0" requests = ">=2.22.0"
requests_oauthlib = "==1.3.1"
markdownify = "==0.11.6"
PyJWT = "==2.4.0" PyJWT = "==2.4.0"
elasticsearch = "==7.17.9" elasticsearch = "==7.17.9"
future = "==0.18.3" future = "==0.18.3"
@ -62,14 +56,6 @@ Jinja2 = "==3.1.2"
jinja2schema = "==0.1.4" jinja2schema = "==0.1.4"
msgpack-python = "==0.5.6" msgpack-python = "==0.5.6"
alembic = "==1.7.7" alembic = "==1.7.7"
hvac = "==2.0.0"
colorama = ">=0.4.6"
pycryptodomex = ">=3.19.0"
lz4 = ">=4.3.2"
python-magic = "==0.4.27"
jsonpath = "==0.82.2"
networkx = ">=3.1"
ipaddress = ">=1.0.23"
[dev-packages] [dev-packages]
# Testing # Testing
@ -86,3 +72,4 @@ flake8-isort = "==2.7.0"
isort = "==4.3.21" isort = "==4.3.21"
pep8-naming = "==0.8.2" pep8-naming = "==0.8.2"
pydocstyle = "==3.0.0" pydocstyle = "==3.0.0"

View File

@ -7,28 +7,21 @@ import os
import sys import sys
from inspect import getmembers from inspect import getmembers
from logging.handlers import RotatingFileHandler from logging.handlers import RotatingFileHandler
from pathlib import Path
from flask import Flask from flask import Flask
from flask import jsonify from flask import jsonify
from flask import make_response from flask import make_response
from flask import request
from flask.blueprints import Blueprint from flask.blueprints import Blueprint
from flask.cli import click from flask.cli import click
from flask.json.provider import DefaultJSONProvider from flask.json.provider import DefaultJSONProvider
from flask_babel.speaklater import LazyString
import api.views.entry import api.views.entry
from api.extensions import (bcrypt, babel, cache, celery, cors, db, es, login_manager, migrate, rd) from api.extensions import (bcrypt, cache, celery, cors, db, es, login_manager, migrate, rd)
from api.extensions import inner_secrets from api.flask_cas import CAS
from api.lib.perm.authentication.cas import CAS
from api.lib.perm.authentication.oauth2 import OAuth2
from api.lib.secrets.secrets import InnerKVManger
from api.models.acl import User from api.models.acl import User
HERE = os.path.abspath(os.path.dirname(__file__)) HERE = os.path.abspath(os.path.dirname(__file__))
PROJECT_ROOT = os.path.join(HERE, os.pardir) PROJECT_ROOT = os.path.join(HERE, os.pardir)
BASE_DIR = Path(__file__).resolve().parent.parent
@login_manager.user_loader @login_manager.user_loader
@ -74,7 +67,7 @@ class ReverseProxy(object):
class MyJSONEncoder(DefaultJSONProvider): class MyJSONEncoder(DefaultJSONProvider):
def default(self, o): def default(self, o):
if isinstance(o, (decimal.Decimal, datetime.date, datetime.time, LazyString)): if isinstance(o, (decimal.Decimal, datetime.date, datetime.time)):
return str(o) return str(o)
if isinstance(o, datetime.datetime): if isinstance(o, datetime.datetime):
@ -83,6 +76,15 @@ class MyJSONEncoder(DefaultJSONProvider):
return o return o
def create_acl_app(config_object="settings"):
app = Flask(__name__.split(".")[0])
app.config.from_object(config_object)
register_extensions(app)
return app
def create_app(config_object="settings"): def create_app(config_object="settings"):
"""Create application factory, as explained here: http://flask.pocoo.org/docs/patterns/appfactories/. """Create application factory, as explained here: http://flask.pocoo.org/docs/patterns/appfactories/.
@ -99,7 +101,6 @@ def create_app(config_object="settings"):
register_shell_context(app) register_shell_context(app)
register_commands(app) register_commands(app)
CAS(app) CAS(app)
OAuth2(app)
app.wsgi_app = ReverseProxy(app.wsgi_app) app.wsgi_app = ReverseProxy(app.wsgi_app)
configure_upload_dir(app) configure_upload_dir(app)
@ -119,18 +120,12 @@ def configure_upload_dir(app):
def register_extensions(app): def register_extensions(app):
"""Register Flask extensions.""" """Register Flask extensions."""
def get_locale():
accept_languages = app.config.get('ACCEPT_LANGUAGES', ['en', 'zh'])
return request.accept_languages.best_match(accept_languages)
bcrypt.init_app(app) bcrypt.init_app(app)
babel.init_app(app, locale_selector=get_locale)
cache.init_app(app) cache.init_app(app)
db.init_app(app) db.init_app(app)
cors.init_app(app) cors.init_app(app)
login_manager.init_app(app) login_manager.init_app(app)
migrate.init_app(app, db, directory=f"{BASE_DIR}/migrations") migrate.init_app(app, db)
rd.init_app(app) rd.init_app(app)
if app.config.get('USE_ES'): if app.config.get('USE_ES'):
es.init_app(app) es.init_app(app)
@ -138,10 +133,6 @@ def register_extensions(app):
app.config.update(app.config.get("CELERY")) app.config.update(app.config.get("CELERY"))
celery.conf.update(app.config) celery.conf.update(app.config)
if app.config.get('SECRETS_ENGINE') == 'inner':
with app.app_context():
inner_secrets.init_app(app, InnerKVManger())
def register_blueprints(app): def register_blueprints(app):
for item in getmembers(api.views.entry): for item in getmembers(api.views.entry):
@ -202,11 +193,10 @@ def configure_logger(app):
app.logger.addHandler(handler) app.logger.addHandler(handler)
log_file = app.config['LOG_PATH'] log_file = app.config['LOG_PATH']
if log_file and log_file != "/dev/stdout": file_handler = RotatingFileHandler(log_file,
file_handler = RotatingFileHandler(log_file, maxBytes=2 ** 30,
maxBytes=2 ** 30, backupCount=7)
backupCount=7) file_handler.setLevel(getattr(logging, app.config['LOG_LEVEL']))
file_handler.setLevel(getattr(logging, app.config['LOG_LEVEL'])) file_handler.setFormatter(formatter)
file_handler.setFormatter(formatter) app.logger.addHandler(file_handler)
app.logger.addHandler(file_handler)
app.logger.setLevel(getattr(logging, app.config['LOG_LEVEL'])) app.logger.setLevel(getattr(logging, app.config['LOG_LEVEL']))

View File

@ -1,8 +1,6 @@
import click import click
from flask.cli import with_appcontext from flask.cli import with_appcontext
from api.lib.perm.acl.user import UserCRUD
@click.command() @click.command()
@with_appcontext @with_appcontext
@ -25,32 +23,50 @@ def init_acl():
role_rebuild.apply_async(args=(role.id, app.id), queue=ACL_QUEUE) role_rebuild.apply_async(args=(role.id, app.id), queue=ACL_QUEUE)
@click.command() # @click.command()
@with_appcontext # @with_appcontext
def add_user(): # def acl_clean():
""" # from api.models.acl import Resource
create a user # from api.models.acl import Permission
# from api.models.acl import RolePermission
is_admin: default is False #
# perms = RolePermission.get_by(to_dict=False)
""" #
# for r in perms:
from api.models.acl import App # perm = Permission.get_by_id(r.perm_id)
from api.lib.perm.acl.cache import AppCache # if perm and perm.app_id != r.app_id:
from api.lib.perm.acl.cache import RoleCache # resource_id = r.resource_id
from api.lib.perm.acl.role import RoleCRUD # resource = Resource.get_by_id(resource_id)
from api.lib.perm.acl.role import RoleRelationCRUD # perm_name = perm.name
# existed = Permission.get_by(resource_type_id=resource.resource_type_id, name=perm_name, first=True,
username = click.prompt('Enter username', confirmation_prompt=False) # to_dict=False)
password = click.prompt('Enter password', hide_input=True, confirmation_prompt=True) # if existed is not None:
email = click.prompt('Enter email ', confirmation_prompt=False) # other = RolePermission.get_by(rid=r.rid, perm_id=existed.id, resource_id=resource_id)
is_admin = click.prompt('Admin (Y/N) ', confirmation_prompt=False, type=bool, default=False) # if not other:
# r.update(perm_id=existed.id)
UserCRUD.add(username=username, password=password, email=email) # else:
# r.soft_delete()
if is_admin: # else:
app = AppCache.get('acl') or App.create(name='acl') # r.soft_delete()
acl_admin = RoleCache.get_by_name(app.id, 'acl_admin') or RoleCRUD.add_role('acl_admin', app.id, True) #
rid = RoleCache.get_by_name(None, username).id #
# @click.command()
RoleRelationCRUD.add(acl_admin, acl_admin.id, [rid], app.id) # @with_appcontext
# def acl_has_resource_role():
# from api.models.acl import Role
# from api.models.acl import App
# from api.lib.perm.acl.cache import HasResourceRoleCache
# from api.lib.perm.acl.role import RoleCRUD
#
# roles = Role.get_by(to_dict=False)
# apps = App.get_by(to_dict=False)
# for role in roles:
# if role.app_id:
# res = RoleCRUD.recursive_resources(role.id, role.app_id)
# if res.get('resources') or res.get('groups'):
# HasResourceRoleCache.add(role.id, role.app_id)
# else:
# for app in apps:
# res = RoleCRUD.recursive_resources(role.id, app.id)
# if res.get('resources') or res.get('groups'):
# HasResourceRoleCache.add(role.id, app.id)

View File

@ -1,50 +1,41 @@
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
import click
import copy import copy
import datetime import datetime
import json import json
import requests
import time import time
import uuid
import click
from flask import current_app from flask import current_app
from flask.cli import with_appcontext from flask.cli import with_appcontext
from flask_login import login_user
import api.lib.cmdb.ci import api.lib.cmdb.ci
from api.extensions import db from api.extensions import db
from api.extensions import rd from api.extensions import rd
from api.lib.cmdb.cache import AttributeCache from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.ci_type import CITypeTriggerManager
from api.lib.cmdb.const import PermEnum from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import REDIS_PREFIX_CI from api.lib.cmdb.const import REDIS_PREFIX_CI
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION2
from api.lib.cmdb.const import ResourceTypeEnum from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.const import RoleEnum from api.lib.cmdb.const import RoleEnum
from api.lib.cmdb.const import ValueTypeEnum from api.lib.cmdb.const import ValueTypeEnum
from api.lib.cmdb.dcim.rack import RackManager
from api.lib.exception import AbortException from api.lib.exception import AbortException
from api.lib.perm.acl.acl import ACLManager 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.cache import AppCache
from api.lib.perm.acl.resource import ResourceCRUD from api.lib.perm.acl.resource import ResourceCRUD
from api.lib.perm.acl.resource import ResourceTypeCRUD from api.lib.perm.acl.resource import ResourceTypeCRUD
from api.lib.perm.acl.role import RoleCRUD from api.lib.perm.acl.role import RoleCRUD
from api.lib.secrets.inner import KeyManage from api.lib.perm.acl.user import UserCRUD
from api.lib.secrets.inner import global_key_threshold
from api.lib.secrets.secrets import InnerKVManger
from api.models.acl import App from api.models.acl import App
from api.models.acl import ResourceType from api.models.acl import ResourceType
from api.models.cmdb import Attribute from api.models.cmdb import Attribute
from api.models.cmdb import AttributeHistory
from api.models.cmdb import CI from api.models.cmdb import CI
from api.models.cmdb import CIRelation from api.models.cmdb import CIRelation
from api.models.cmdb import CIType from api.models.cmdb import CIType
from api.models.cmdb import CITypeTrigger from api.models.cmdb import CITypeTrigger
from api.models.cmdb import OperationRecord
from api.models.cmdb import PreferenceRelationView from api.models.cmdb import PreferenceRelationView
from api.tasks.cmdb import batch_ci_cache
@click.command() @click.command()
@ -54,22 +45,13 @@ def cmdb_init_cache():
ci_relations = CIRelation.get_by(to_dict=False) ci_relations = CIRelation.get_by(to_dict=False)
relations = dict() relations = dict()
relations2 = dict()
for cr in ci_relations: for cr in ci_relations:
relations.setdefault(cr.first_ci_id, {}).update({cr.second_ci_id: cr.second_ci.type_id}) relations.setdefault(cr.first_ci_id, {}).update({cr.second_ci_id: cr.second_ci.type_id})
if cr.ancestor_ids:
relations2.setdefault('{},{}'.format(cr.ancestor_ids, cr.first_ci_id), {}).update(
{cr.second_ci_id: cr.second_ci.type_id})
for i in relations: for i in relations:
relations[i] = json.dumps(relations[i]) relations[i] = json.dumps(relations[i])
for i in relations2:
relations2[i] = json.dumps(relations2[i])
if relations: if relations:
rd.create_or_update(relations, REDIS_PREFIX_CI_RELATION) rd.create_or_update(relations, REDIS_PREFIX_CI_RELATION)
if relations2:
rd.create_or_update(relations2, REDIS_PREFIX_CI_RELATION2)
es = None
if current_app.config.get("USE_ES"): if current_app.config.get("USE_ES"):
from api.extensions import es from api.extensions import es
from api.models.cmdb import Attribute from api.models.cmdb import Attribute
@ -121,20 +103,10 @@ def cmdb_init_acl():
_app = AppCache.get('cmdb') or App.create(name='cmdb') _app = AppCache.get('cmdb') or App.create(name='cmdb')
app_id = _app.id app_id = _app.id
current_app.test_request_context().push()
# 1. add resource type # 1. add resource type
for resource_type in ResourceTypeEnum.all(): for resource_type in ResourceTypeEnum.all():
try: try:
perms = PermEnum.all() ResourceTypeCRUD.add(app_id, resource_type, '', PermEnum.all())
if resource_type in (ResourceTypeEnum.CI_FILTER, ResourceTypeEnum.PAGE):
perms = [PermEnum.READ]
elif resource_type == ResourceTypeEnum.CI_TYPE_RELATION:
perms = [PermEnum.ADD, PermEnum.DELETE, PermEnum.GRANT]
elif resource_type in (ResourceTypeEnum.RELATION_VIEW, ResourceTypeEnum.TOPOLOGY_VIEW):
perms = [PermEnum.READ, PermEnum.UPDATE, PermEnum.DELETE, PermEnum.GRANT]
ResourceTypeCRUD.add(app_id, resource_type, '', perms)
except AbortException: except AbortException:
pass pass
@ -150,10 +122,10 @@ def cmdb_init_acl():
# 3. add resource and grant # 3. add resource and grant
ci_types = CIType.get_by(to_dict=False) ci_types = CIType.get_by(to_dict=False)
resource_type_id = ResourceType.get_by(name=ResourceTypeEnum.CI, first=True, to_dict=False).id type_id = ResourceType.get_by(name=ResourceTypeEnum.CI, first=True, to_dict=False).id
for ci_type in ci_types: for ci_type in ci_types:
try: try:
ResourceCRUD.add(ci_type.name, resource_type_id, app_id) ResourceCRUD.add(ci_type.name, type_id, app_id)
except AbortException: except AbortException:
pass pass
@ -163,10 +135,10 @@ def cmdb_init_acl():
[PermEnum.READ]) [PermEnum.READ])
relation_views = PreferenceRelationView.get_by(to_dict=False) relation_views = PreferenceRelationView.get_by(to_dict=False)
resource_type_id = ResourceType.get_by(name=ResourceTypeEnum.RELATION_VIEW, first=True, to_dict=False).id type_id = ResourceType.get_by(name=ResourceTypeEnum.RELATION_VIEW, first=True, to_dict=False).id
for view in relation_views: for view in relation_views:
try: try:
ResourceCRUD.add(view.name, resource_type_id, app_id) ResourceCRUD.add(view.name, type_id, app_id)
except AbortException: except AbortException:
pass pass
@ -176,6 +148,57 @@ def cmdb_init_acl():
[PermEnum.READ]) [PermEnum.READ])
@click.command()
@click.option(
'-u',
'--user',
help='username'
)
@click.option(
'-p',
'--password',
help='password'
)
@click.option(
'-m',
'--mail',
help='mail'
)
@with_appcontext
def add_user(user, password, mail):
"""
create a user
is_admin: default is False
Example: flask add-user -u <username> -p <password> -m <mail>
"""
assert user is not None
assert password is not None
assert mail is not None
UserCRUD.add(username=user, password=password, email=mail)
@click.command()
@click.option(
'-u',
'--user',
help='username'
)
@with_appcontext
def del_user(user):
"""
delete a user
Example: flask del-user -u <username>
"""
assert user is not None
from api.models.acl import User
u = User.get_by(username=user, first=True, to_dict=False)
u and UserCRUD.delete(u.uid)
@click.command() @click.command()
@with_appcontext @with_appcontext
def cmdb_counter(): def cmdb_counter():
@ -184,35 +207,11 @@ def cmdb_counter():
""" """
from api.lib.cmdb.cache import CMDBCounterCache from api.lib.cmdb.cache import CMDBCounterCache
current_app.test_request_context().push()
if not UserCache.get('worker'):
from api.lib.perm.acl.user import UserCRUD
UserCRUD.add(username='worker', password=uuid.uuid4().hex, email='worker@xxx.com')
login_user(UserCache.get('worker'))
i = 0
today = datetime.date.today()
while True: while True:
try: try:
db.session.commit() db.session.remove()
CMDBCounterCache.reset() CMDBCounterCache.reset()
if i % 5 == 0:
CMDBCounterCache.flush_adc_counter()
i = 0
if datetime.date.today() != today:
CMDBCounterCache.clear_ad_exec_history()
today = datetime.date.today()
CMDBCounterCache.flush_sub_counter()
RackManager().check_u_slot()
i += 1
except: except:
import traceback import traceback
print(traceback.format_exc()) print(traceback.format_exc())
@ -224,66 +223,50 @@ def cmdb_counter():
@with_appcontext @with_appcontext
def cmdb_trigger(): def cmdb_trigger():
""" """
Trigger execution for date attribute Trigger execution
""" """
from api.lib.cmdb.ci import CITriggerManager
current_app.test_request_context().push()
if not UserCache.get('worker'):
from api.lib.perm.acl.user import UserCRUD
UserCRUD.add(username='worker', password=uuid.uuid4().hex, email='worker@xxx.com')
login_user(UserCache.get('worker'))
current_day = datetime.datetime.today().strftime("%Y-%m-%d") current_day = datetime.datetime.today().strftime("%Y-%m-%d")
trigger2cis = dict() trigger2cis = dict()
trigger2completed = dict() trigger2completed = dict()
i = 0 i = 0
while True: while True:
try: db.session.remove()
db.session.remove() if datetime.datetime.today().strftime("%Y-%m-%d") != current_day:
trigger2cis = dict()
trigger2completed = dict()
current_day = datetime.datetime.today().strftime("%Y-%m-%d")
if datetime.datetime.today().strftime("%Y-%m-%d") != current_day: if i == 360 or i == 0:
trigger2cis = dict() i = 0
trigger2completed = dict() try:
current_day = datetime.datetime.today().strftime("%Y-%m-%d") triggers = CITypeTrigger.get_by(to_dict=False)
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: for trigger in triggers:
try: ready_cis = CITypeTriggerManager.waiting_cis(trigger)
ready_cis = CITriggerManager.waiting_cis(trigger)
except Exception as e:
print(e)
continue
if trigger.id not in trigger2cis: if trigger.id not in trigger2cis:
trigger2cis[trigger.id] = (trigger, ready_cis) trigger2cis[trigger.id] = (trigger, ready_cis)
else: else:
cur = trigger2cis[trigger.id] cur = trigger2cis[trigger.id]
cur_ci_ids = {_ci.ci_id for _ci in cur[1]} cur_ci_ids = {i.ci_id for i in cur[1]}
trigger2cis[trigger.id] = ( trigger2cis[trigger.id] = (trigger, cur[1] + [i for i in ready_cis if i.ci_id not in cur_ci_ids
trigger, cur[1] + [_ci for _ci in ready_cis if _ci.ci_id not in cur_ci_ids and i.ci_id not in trigger2completed[trigger.id]])
and _ci.ci_id not in trigger2completed.get(trigger.id, {})])
for tid in trigger2cis: except Exception as e:
trigger, cis = trigger2cis[tid] print(e)
for ci in copy.deepcopy(cis):
if CITriggerManager.trigger_notify(trigger, ci):
trigger2completed.setdefault(trigger.id, set()).add(ci.ci_id)
for _ci in cis: for tid in trigger2cis:
if _ci.ci_id == ci.ci_id: trigger, cis = trigger2cis[tid]
cis.remove(_ci) for ci in copy.deepcopy(cis):
if CITypeTriggerManager.trigger_notify(trigger, ci):
trigger2completed.setdefault(trigger.id, set()).add(ci.ci_id)
i += 1 for _ci in cis:
time.sleep(10) if _ci.ci_id == ci.ci_id:
except Exception as e: cis.remove(_ci)
import traceback
print(traceback.format_exc()) i += 1
current_app.logger.error("cmdb trigger exception: {}".format(e)) time.sleep(10)
time.sleep(60)
@click.command() @click.command()
@ -315,273 +298,3 @@ def cmdb_index_table_upgrade():
CIIndexValueDateTime.create(ci_id=i.ci_id, attr_id=i.attr_id, value=i.value, commit=False) CIIndexValueDateTime.create(ci_id=i.ci_id, attr_id=i.attr_id, value=i.value, commit=False)
i.delete(commit=False) i.delete(commit=False)
db.session.commit() db.session.commit()
def valid_address(address):
if not address:
return False
if not address.startswith(("http://127.0.0.1", "https://127.0.0.1")):
response = {
"message": "Address should start with http://127.0.0.1 or https://127.0.0.1",
"status": "failed"
}
KeyManage.print_response(response)
return False
return True
@click.command()
@click.option(
'-a',
'--address',
help='inner cmdb api, http://127.0.0.1:8000',
)
@with_appcontext
def cmdb_inner_secrets_init(address):
"""
init inner secrets for password feature
"""
res, ok = KeyManage(backend=InnerKVManger()).init()
if not ok:
if res.get("status") == "failed":
KeyManage.print_response(res)
return
token = res.get("details", {}).get("root_token", "")
if valid_address(address):
token = current_app.config.get("INNER_TRIGGER_TOKEN", "") if not token else token
if not token:
token = click.prompt('Enter root token', hide_input=True, confirmation_prompt=False)
assert token is not None
resp = requests.post("{}/api/v0.1/secrets/auto_seal".format(address.strip("/")),
headers={"Inner-Token": token})
if resp.status_code == 200:
KeyManage.print_response(resp.json())
else:
KeyManage.print_response({"message": resp.text or resp.status_code, "status": "failed"})
else:
KeyManage.print_response(res)
@click.command()
@click.option(
'-a',
'--address',
help='inner cmdb api, http://127.0.0.1:8000',
required=True,
)
@with_appcontext
def cmdb_inner_secrets_unseal(address):
"""
unseal the secrets feature
"""
# if not valid_address(address):
# return
address = "{}/api/v0.1/secrets/unseal".format(address.strip("/"))
for i in range(global_key_threshold):
token = click.prompt(f'Enter unseal token {i + 1}', hide_input=True, confirmation_prompt=False)
assert token is not None
resp = requests.post(address, headers={"Unseal-Token": token}, timeout=5)
if resp.status_code == 200:
KeyManage.print_response(resp.json())
if resp.json().get("status") in ["success", "skip"]:
return
else:
KeyManage.print_response({"message": resp.status_code, "status": "failed"})
return
@click.command()
@click.option(
'-a',
'--address',
help='inner cmdb api, http://127.0.0.1:8000',
required=True,
)
@click.option(
'-k',
'--token',
help='root token',
prompt=True,
hide_input=True,
)
@with_appcontext
def cmdb_inner_secrets_seal(address, token):
"""
seal the secrets feature
"""
assert address is not None
assert token is not None
if not valid_address(address):
return
address = "{}/api/v0.1/secrets/seal".format(address.strip("/"))
resp = requests.post(address, headers={
"Inner-Token": token,
})
if resp.status_code == 200:
KeyManage.print_response(resp.json())
else:
KeyManage.print_response({"message": resp.status_code, "status": "failed"})
@click.command()
@with_appcontext
def cmdb_password_data_migrate():
"""
Migrate CI password data, version >= v2.3.6
"""
from api.models.cmdb import CIIndexValueText
from api.models.cmdb import CIValueText
from api.lib.secrets.inner import InnerCrypt
from api.lib.secrets.vault import VaultClient
attrs = Attribute.get_by(to_dict=False)
for attr in attrs:
if attr.is_password:
value_table = CIIndexValueText if attr.is_index else CIValueText
failed = False
for i in value_table.get_by(attr_id=attr.id, to_dict=False):
if current_app.config.get("SECRETS_ENGINE", 'inner') == 'inner':
_, status = InnerCrypt().decrypt(i.value)
if status:
continue
encrypt_value, status = InnerCrypt().encrypt(i.value)
if status:
CIValueText.create(ci_id=i.ci_id, attr_id=attr.id, value=encrypt_value)
else:
failed = True
continue
elif current_app.config.get("SECRETS_ENGINE") == 'vault':
if i.value == '******':
continue
vault = VaultClient(current_app.config.get('VAULT_URL'), current_app.config.get('VAULT_TOKEN'))
try:
vault.update("/{}/{}".format(i.ci_id, i.attr_id), dict(v=i.value))
except Exception as e:
print('save password to vault failed: {}'.format(e))
failed = True
continue
else:
continue
i.delete()
if not failed and attr.is_index:
attr.update(is_index=False)
@click.command()
@with_appcontext
def cmdb_agent_init():
"""
Initialize the agent's permissions and obtain the key and secret
"""
from api.models.acl import User
user = User.get_by(username="cmdb_agent", first=True, to_dict=False)
if user is None:
click.echo(
click.style('user cmdb_agent does not exist, please use flask add-user to create it first', fg='red'))
return
# grant
_app = AppCache.get('cmdb') or App.create(name='cmdb')
app_id = _app.id
ci_types = CIType.get_by(to_dict=False)
resource_type_id = ResourceType.get_by(name=ResourceTypeEnum.CI, first=True, to_dict=False).id
for ci_type in ci_types:
try:
ResourceCRUD.add(ci_type.name, resource_type_id, app_id)
except AbortException:
pass
ACLManager().grant_resource_to_role(ci_type.name,
"cmdb_agent",
ResourceTypeEnum.CI,
[PermEnum.READ, PermEnum.UPDATE, PermEnum.ADD, PermEnum.DELETE])
click.echo("Key : {}".format(click.style(user.key, bg='red')))
click.echo("Secret: {}".format(click.style(user.secret, bg='red')))
@click.command()
@click.option(
'-v',
'--version',
help='input cmdb version, e.g. 2.4.6',
required=True,
)
@with_appcontext
def cmdb_patch(version):
"""
CMDB upgrade patch
"""
version = version[1:] if version.lower().startswith("v") else version
try:
if version >= '2.4.6':
from api.models.cmdb import CITypeRelation
for cr in CITypeRelation.get_by(to_dict=False):
if hasattr(cr, 'parent_attr_id') and cr.parent_attr_id and not cr.parent_attr_ids:
parent_attr_ids, child_attr_ids = [cr.parent_attr_id], [cr.child_attr_id]
cr.update(parent_attr_ids=parent_attr_ids, child_attr_ids=child_attr_ids, commit=False)
db.session.commit()
from api.models.cmdb import AutoDiscoveryCIType, AutoDiscoveryCITypeRelation
from api.lib.cmdb.cache import CITypeCache, AttributeCache
for adt in AutoDiscoveryCIType.get_by(to_dict=False):
if adt.relation:
if not AutoDiscoveryCITypeRelation.get_by(ad_type_id=adt.type_id):
peer_type = CITypeCache.get(list(adt.relation.values())[0]['type_name'])
peer_type_id = peer_type and peer_type.id
peer_attr = AttributeCache.get(list(adt.relation.values())[0]['attr_name'])
peer_attr_id = peer_attr and peer_attr.id
if peer_type_id and peer_attr_id:
AutoDiscoveryCITypeRelation.create(ad_type_id=adt.type_id,
ad_key=list(adt.relation.keys())[0],
peer_type_id=peer_type_id,
peer_attr_id=peer_attr_id,
commit=False)
if hasattr(adt, 'interval') and adt.interval and not adt.cron:
adt.cron = "*/{} * * * *".format(adt.interval // 60 or 1)
db.session.commit()
if version >= "2.4.7":
from api.lib.cmdb.auto_discovery.const import DEFAULT_INNER
from api.models.cmdb import AutoDiscoveryRule
for i in DEFAULT_INNER:
existed = AutoDiscoveryRule.get_by(name=i['name'], first=True, to_dict=False)
if existed is not None:
if "en" in i['option'] and 'en' not in (existed.option or {}):
option = copy.deepcopy(existed.option)
option['en'] = i['option']['en']
existed.update(option=option, commit=False)
db.session.commit()
if version >= "2.4.14": # update ci columns: updated_at and updated_by
ci_ids = []
for i in CI.get_by(only_query=True).filter(CI.updated_at.is_(None)):
hist = AttributeHistory.get_by(ci_id=i.id, only_query=True).order_by(AttributeHistory.id.desc()).first()
if hist is not None:
record = OperationRecord.get_by_id(hist.record_id)
if record is not None:
u = UserCache.get(record.uid)
i.update(updated_at=record.created_at, updated_by=u and u.nickname, flush=True)
ci_ids.append(i.id)
db.session.commit()
batch_ci_cache.apply_async(args=(ci_ids,))
except Exception as e:
print("cmdb patch failed: {}".format(e))

View File

@ -5,7 +5,9 @@ from glob import glob
from subprocess import call from subprocess import call
import click import click
from flask import current_app
from flask.cli import with_appcontext from flask.cli import with_appcontext
from werkzeug.exceptions import MethodNotAllowed, NotFound
from api.extensions import db from api.extensions import db
@ -82,59 +84,69 @@ def clean():
os.remove(full_pathname) os.remove(full_pathname)
@click.command()
@click.option("--url", default=None, help="Url to test (ex. /static/image.png)")
@click.option(
"--order", default="rule", help="Property on Rule to order by (default: rule)"
)
@with_appcontext
def urls(url, order):
"""Display all of the url matching routes for the project.
Borrowed from Flask-Script, converted to use Click.
"""
rows = []
column_headers = ("Rule", "Endpoint", "Arguments")
if url:
try:
rule, arguments = current_app.url_map.bind("localhost").match(
url, return_rule=True
)
rows.append((rule.rule, rule.endpoint, arguments))
column_length = 3
except (NotFound, MethodNotAllowed) as e:
rows.append(("<{}>".format(e), None, None))
column_length = 1
else:
rules = sorted(
current_app.url_map.iter_rules(), key=lambda rule: getattr(rule, order)
)
for rule in rules:
rows.append((rule.rule, rule.endpoint, None))
column_length = 2
str_template = ""
table_width = 0
if column_length >= 1:
max_rule_length = max(len(r[0]) for r in rows)
max_rule_length = max_rule_length if max_rule_length > 4 else 4
str_template += "{:" + str(max_rule_length) + "}"
table_width += max_rule_length
if column_length >= 2:
max_endpoint_length = max(len(str(r[1])) for r in rows)
max_endpoint_length = max_endpoint_length if max_endpoint_length > 8 else 8
str_template += " {:" + str(max_endpoint_length) + "}"
table_width += 2 + max_endpoint_length
if column_length >= 3:
max_arguments_length = max(len(str(r[2])) for r in rows)
max_arguments_length = max_arguments_length if max_arguments_length > 9 else 9
str_template += " {:" + str(max_arguments_length) + "}"
table_width += 2 + max_arguments_length
click.echo(str_template.format(*column_headers[:column_length]))
click.echo("-" * table_width)
for row in rows:
click.echo(str_template.format(*row[:column_length]))
@click.command() @click.command()
@with_appcontext @with_appcontext
def db_setup(): def db_setup():
"""create tables """create tables
""" """
db.create_all() db.create_all()
try:
db.session.execute("set global sql_mode='STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,"
"ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION'")
db.session.commit()
except:
pass
try:
db.session.execute("set global tidb_enable_noop_functions='ON'")
db.session.commit()
except:
pass
@click.group()
def translate():
"""Translation and localization commands."""
@translate.command()
@click.argument('lang')
def init(lang):
"""Initialize a new language."""
if os.system('pybabel extract -F babel.cfg -k _l -o messages.pot .'):
raise RuntimeError('extract command failed')
if os.system(
'pybabel init -i messages.pot -d api/translations -l ' + lang):
raise RuntimeError('init command failed')
os.remove('messages.pot')
@translate.command()
def update():
"""Update all languages."""
if os.system('pybabel extract -F babel.cfg -k _l -o messages.pot .'):
raise RuntimeError('extract command failed')
if os.system('pybabel update -i messages.pot -d api/translations'):
raise RuntimeError('update command failed')
os.remove('messages.pot')
@translate.command()
def compile():
"""Compile all languages."""
if os.system('pybabel compile -d api/translations'):
raise RuntimeError('compile command failed')

View File

@ -4,13 +4,15 @@ from flask.cli import with_appcontext
from werkzeug.datastructures import MultiDict from werkzeug.datastructures import MultiDict
from api.lib.common_setting.acl import ACLManager from api.lib.common_setting.acl import ACLManager
from api.lib.common_setting.employee import EmployeeAddForm, GrantEmployeeACLPerm from api.lib.common_setting.employee import EmployeeAddForm
from api.lib.common_setting.resp_format import ErrFormat from api.lib.common_setting.resp_format import ErrFormat
from api.lib.common_setting.utils import CheckNewColumn
from api.models.common_setting import Employee, Department from api.models.common_setting import Employee, Department
class InitEmployee(object): class InitEmployee(object):
"""
初始化员工
"""
def __init__(self): def __init__(self):
self.log = current_app.logger self.log = current_app.logger
@ -56,8 +58,7 @@ class InitEmployee(object):
self.log.error(ErrFormat.acl_import_user_failed.format(user['username'], str(e))) self.log.error(ErrFormat.acl_import_user_failed.format(user['username'], str(e)))
self.log.error(e) self.log.error(e)
@staticmethod def get_rid_by_uid(self, uid):
def get_rid_by_uid(uid):
from api.models.acl import Role from api.models.acl import Role
role = Role.get_by(first=True, uid=uid) role = Role.get_by(first=True, uid=uid)
return role['id'] if role is not None else 0 return role['id'] if role is not None else 0
@ -70,8 +71,7 @@ class InitDepartment(object):
def init(self): def init(self):
self.init_wide_company() self.init_wide_company()
@staticmethod def hard_delete(self, department_id, department_name):
def hard_delete(department_id, department_name):
existed_deleted_list = Department.query.filter( existed_deleted_list = Department.query.filter(
Department.department_name == department_name, Department.department_name == department_name,
Department.department_id == department_id, Department.department_id == department_id,
@ -80,12 +80,11 @@ class InitDepartment(object):
for existed in existed_deleted_list: for existed in existed_deleted_list:
existed.delete() existed.delete()
@staticmethod def get_department(self, department_name):
def get_department(department_name):
return Department.query.filter( return Department.query.filter(
Department.department_name == department_name, Department.department_name == department_name,
Department.deleted == 0, Department.deleted == 0,
).first() ).order_by(Department.created_at.asc()).first()
def run(self, department_id, department_name, department_parent_id): def run(self, department_id, department_name, department_parent_id):
self.hard_delete(department_id, department_name) self.hard_delete(department_id, department_name)
@ -95,7 +94,7 @@ class InitDepartment(object):
if res.department_id == department_id: if res.department_id == department_id:
return return
else: else:
res.update( new_d = res.update(
department_id=department_id, department_id=department_id,
department_parent_id=department_parent_id, department_parent_id=department_parent_id,
) )
@ -109,11 +108,11 @@ class InitDepartment(object):
new_d = self.get_department(department_name) new_d = self.get_department(department_name)
if new_d.department_id != department_id: if new_d.department_id != department_id:
new_d.update( new_d = new_d.update(
department_id=department_id, department_id=department_id,
department_parent_id=department_parent_id, department_parent_id=department_parent_id,
) )
self.log.info(f"init {department_name} success.") self.log.info(f"初始化 {department_name} 部门成功.")
def run_common(self, department_id, department_name, department_parent_id): def run_common(self, department_id, department_name, department_parent_id):
try: try:
@ -124,14 +123,19 @@ class InitDepartment(object):
raise Exception(e) raise Exception(e)
def init_wide_company(self): def init_wide_company(self):
"""
创建 id 0, name 全公司 的部门
"""
department_id = 0 department_id = 0
department_name = '全公司' department_name = '全公司'
department_parent_id = -1 department_parent_id = -1
self.run_common(department_id, department_name, department_parent_id) self.run_common(department_id, department_name, department_parent_id)
@staticmethod def create_acl_role_with_department(self):
def create_acl_role_with_department(): """
当前所有部门在ACL创建 role
"""
acl = ACLManager('acl') acl = ACLManager('acl')
role_name_map = {role['name']: role for role in acl.get_all_roles()} role_name_map = {role['name']: role for role in acl.get_all_roles()}
@ -142,7 +146,7 @@ class InitDepartment(object):
continue continue
role = role_name_map.get(department.department_name) role = role_name_map.get(department.department_name)
if not role: if role is None:
payload = { payload = {
'app_id': 'acl', 'app_id': 'acl',
'name': department.department_name, 'name': department.department_name,
@ -157,31 +161,6 @@ class InitDepartment(object):
info = f"update department acl_rid: {acl_rid}" info = f"update department acl_rid: {acl_rid}"
current_app.logger.info(info) current_app.logger.info(info)
def init_backend_resource(self):
acl = self.check_app('backend')
acl_rid = self.get_admin_user_rid()
if acl_rid == 0:
return
GrantEmployeeACLPerm(acl).grant_by_rid(acl_rid, True)
@staticmethod
def check_app(app_name):
acl = ACLManager(app_name)
payload = dict(
name=app_name,
description=app_name
)
app = acl.validate_app()
if not app:
acl.create_app(payload)
return acl
@staticmethod
def get_admin_user_rid():
admin = Employee.get_by(first=True, username='admin', to_dict=False)
return admin.acl_rid if admin else 0
@click.command() @click.command()
@with_appcontext @with_appcontext
@ -198,33 +177,5 @@ def init_department():
""" """
Department initialization Department initialization
""" """
cli = InitDepartment() InitDepartment().init()
cli.init_wide_company() InitDepartment().create_acl_role_with_department()
cli.create_acl_role_with_department()
cli.init_backend_resource()
@click.command()
@with_appcontext
def common_check_new_columns():
"""
add new columns to tables
"""
CheckNewColumn().run()
@click.command()
@with_appcontext
def common_sync_file_to_db():
from api.lib.common_setting.upload_file import CommonFileCRUD
CommonFileCRUD.sync_file_to_db()
@click.command()
@with_appcontext
@click.option('--value', type=click.INT, default=-1)
def set_auth_auto_redirect_enable(value):
if value < 0:
return
from api.lib.common_setting.common_data import CommonDataCRUD
CommonDataCRUD.set_auth_auto_redirect_enable(value)

View File

@ -2,7 +2,6 @@
from celery import Celery from celery import Celery
from flask_babel import Babel
from flask_bcrypt import Bcrypt from flask_bcrypt import Bcrypt
from flask_caching import Cache from flask_caching import Cache
from flask_cors import CORS from flask_cors import CORS
@ -10,12 +9,10 @@ from flask_login import LoginManager
from flask_migrate import Migrate from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from api.lib.secrets.inner import KeyManage
from api.lib.utils import ESHandler from api.lib.utils import ESHandler
from api.lib.utils import RedisHandler from api.lib.utils import RedisHandler
bcrypt = Bcrypt() bcrypt = Bcrypt()
babel = Babel()
login_manager = LoginManager() login_manager = LoginManager()
db = SQLAlchemy(session_options={"autoflush": False}) db = SQLAlchemy(session_options={"autoflush": False})
migrate = Migrate() migrate = Migrate()
@ -24,4 +21,3 @@ celery = Celery()
cors = CORS(supports_credentials=True) cors = CORS(supports_credentials=True)
rd = RedisHandler() rd = RedisHandler()
es = ESHandler() es = ESHandler()
inner_secrets = KeyManage()

View File

@ -15,7 +15,7 @@ try:
except ImportError: except ImportError:
from flask import _request_ctx_stack as stack from flask import _request_ctx_stack as stack
from . import routing from api.flask_cas import routing
class CAS(object): class CAS(object):

View File

@ -1,24 +1,14 @@
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
import datetime
import uuid import json
import bs4 import bs4
from flask import Blueprint from flask import Blueprint
from flask import current_app from flask import current_app, session, request, url_for, redirect
from flask import redirect from flask_login import login_user, logout_user
from flask import request
from flask import session
from flask import url_for
from flask_login import login_user
from flask_login import logout_user
from six.moves.urllib.parse import urlparse
from six.moves.urllib_request import urlopen from six.moves.urllib_request import urlopen
from api.lib.common_setting.common_data import AuthenticateDataCRUD
from api.lib.common_setting.const import AuthenticateType
from api.lib.perm.acl.audit import AuditCRUD
from api.lib.perm.acl.cache import UserCache from api.lib.perm.acl.cache import UserCache
from api.lib.perm.acl.resp_format import ErrFormat
from .cas_urls import create_cas_login_url from .cas_urls import create_cas_login_url
from .cas_urls import create_cas_logout_url from .cas_urls import create_cas_logout_url
from .cas_urls import create_cas_validate_url from .cas_urls import create_cas_validate_url
@ -26,7 +16,6 @@ from .cas_urls import create_cas_validate_url
blueprint = Blueprint('cas', __name__) blueprint = Blueprint('cas', __name__)
@blueprint.route('/api/cas/login')
@blueprint.route('/api/sso/login') @blueprint.route('/api/sso/login')
def login(): def login():
""" """
@ -40,20 +29,16 @@ def login():
If validation was successful the logged in username is saved in If validation was successful the logged in username is saved in
the user's session under the key `CAS_USERNAME_SESSION_KEY`. the user's session under the key `CAS_USERNAME_SESSION_KEY`.
""" """
config = AuthenticateDataCRUD(AuthenticateType.CAS).get()
cas_token_session_key = current_app.config['CAS_TOKEN_SESSION_KEY'] cas_token_session_key = current_app.config['CAS_TOKEN_SESSION_KEY']
if request.values.get("next"): if request.values.get("next"):
session["next"] = request.values.get("next") session["next"] = request.values.get("next")
# _service = url_for('cas.login', _external=True) _service = url_for('cas.login', _external=True, next=session["next"]) \
_service = "{}://{}{}".format(urlparse(request.referrer).scheme, if session.get("next") else url_for('cas.login', _external=True)
urlparse(request.referrer).netloc,
url_for('cas.login'))
redirect_url = create_cas_login_url( redirect_url = create_cas_login_url(
config['cas_server'], current_app.config['CAS_SERVER'],
config['cas_login_route'], current_app.config['CAS_LOGIN_ROUTE'],
_service) _service)
if 'ticket' in request.args: if 'ticket' in request.args:
@ -62,38 +47,30 @@ def login():
if request.args.get('ticket'): if request.args.get('ticket'):
if validate(request.args['ticket']): if validate(request.args['ticket']):
redirect_url = session.get("next") or config.get("cas_after_login") or "/" redirect_url = session.get("next") or \
current_app.config.get("CAS_AFTER_LOGIN")
username = session.get("CAS_USERNAME") username = session.get("CAS_USERNAME")
user = UserCache.get(username) user = UserCache.get(username)
login_user(user) login_user(user)
session.permanent = True session.permanent = True
_id = AuditCRUD.add_login_log(username, True, ErrFormat.login_succeed)
session['LOGIN_ID'] = _id
else: else:
del session[cas_token_session_key] del session[cas_token_session_key]
redirect_url = create_cas_login_url( redirect_url = create_cas_login_url(
config['cas_server'], current_app.config['CAS_SERVER'],
config['cas_login_route'], current_app.config['CAS_LOGIN_ROUTE'],
url_for('cas.login', _external=True), url_for('cas.login', _external=True),
renew=True) renew=True)
AuditCRUD.add_login_log(session.get("CAS_USERNAME"), False, ErrFormat.invalid_password)
current_app.logger.info("redirect to: {0}".format(redirect_url)) current_app.logger.info("redirect to: {0}".format(redirect_url))
return redirect(redirect_url) return redirect(redirect_url)
@blueprint.route('/api/cas/logout')
@blueprint.route('/api/sso/logout') @blueprint.route('/api/sso/logout')
def logout(): def logout():
""" """
When the user accesses this route they are logged out. When the user accesses this route they are logged out.
""" """
config = AuthenticateDataCRUD(AuthenticateType.CAS).get()
current_app.logger.info(config)
cas_username_session_key = current_app.config['CAS_USERNAME_SESSION_KEY'] cas_username_session_key = current_app.config['CAS_USERNAME_SESSION_KEY']
cas_token_session_key = current_app.config['CAS_TOKEN_SESSION_KEY'] cas_token_session_key = current_app.config['CAS_TOKEN_SESSION_KEY']
@ -105,14 +82,12 @@ def logout():
"next" in session and session.pop("next") "next" in session and session.pop("next")
redirect_url = create_cas_logout_url( redirect_url = create_cas_logout_url(
config['cas_server'], current_app.config['CAS_SERVER'],
config['cas_logout_route'], current_app.config['CAS_LOGOUT_ROUTE'],
url_for('cas.login', _external=True, next=request.referrer)) url_for('cas.login', _external=True, next=request.referrer))
logout_user() logout_user()
AuditCRUD.add_login_log(None, None, None, _id=session.get('LOGIN_ID'), logout_at=datetime.datetime.now())
current_app.logger.debug('Redirecting to: {0}'.format(redirect_url)) current_app.logger.debug('Redirecting to: {0}'.format(redirect_url))
return redirect(redirect_url) return redirect(redirect_url)
@ -125,15 +100,14 @@ def validate(ticket):
and the validated username is saved in the session under the and the validated username is saved in the session under the
key `CAS_USERNAME_SESSION_KEY`. key `CAS_USERNAME_SESSION_KEY`.
""" """
config = AuthenticateDataCRUD(AuthenticateType.CAS).get()
cas_username_session_key = current_app.config['CAS_USERNAME_SESSION_KEY'] cas_username_session_key = current_app.config['CAS_USERNAME_SESSION_KEY']
current_app.logger.debug("validating token {0}".format(ticket)) current_app.logger.debug("validating token {0}".format(ticket))
cas_validate_url = create_cas_validate_url( cas_validate_url = create_cas_validate_url(
config['cas_validate_server'], current_app.config['CAS_VALIDATE_SERVER'],
config['cas_validate_route'], current_app.config['CAS_VALIDATE_ROUTE'],
url_for('cas.login', _external=True), url_for('cas.login', _external=True),
ticket) ticket)
@ -141,35 +115,23 @@ def validate(ticket):
try: try:
response = urlopen(cas_validate_url).read() response = urlopen(cas_validate_url).read()
ticket_id = _parse_tag(response, "cas:user") ticketid = _parse_tag(response, "cas:user")
strs = [s.strip() for s in ticket_id.split('|') if s.strip()] strs = [s.strip() for s in ticketid.split('|') if s.strip()]
username, is_valid = None, False username, is_valid = None, False
if len(strs) == 1: if len(strs) == 1:
username = strs[0] username = strs[0]
is_valid = True is_valid = True
user_info = json.loads(_parse_tag(response, "cas:other"))
current_app.logger.info(user_info)
except ValueError: except ValueError:
current_app.logger.error("CAS returned unexpected result") current_app.logger.error("CAS returned unexpected result")
is_valid = False is_valid = False
return is_valid return is_valid
if is_valid: if is_valid:
current_app.logger.debug("{}: {}".format(cas_username_session_key, username)) current_app.logger.debug("valid")
session[cas_username_session_key] = username session[cas_username_session_key] = username
user = UserCache.get(username) user = UserCache.get(username)
if user is None:
current_app.logger.info("create user: {}".format(username))
from api.lib.perm.acl.user import UserCRUD
soup = bs4.BeautifulSoup(response)
cas_user_map = config.get('cas_user_map')
user_dict = dict()
for k in cas_user_map:
v = soup.find(cas_user_map[k]['tag'], cas_user_map[k].get('attrs', {}))
user_dict[k] = v and v.text or None
user_dict['password'] = uuid.uuid4().hex
if "email" not in user_dict:
user_dict['email'] = username
UserCRUD.add(**user_dict)
from api.lib.perm.acl.acl import ACLManager from api.lib.perm.acl.acl import ACLManager
user_info = ACLManager.get_user_info(username) user_info = ACLManager.get_user_info(username)
@ -194,7 +156,7 @@ def validate(ticket):
def _parse_tag(string, tag): def _parse_tag(string, tag):
""" """
Used for parsing xml. Search string for the first occurrence of Used for parsing xml. Search string for the first occurence of
<tag>.....</tag> and return text (stripped of leading and tailing <tag>.....</tag> and return text (stripped of leading and tailing
whitespace) between tags. Return "" if tag not found. whitespace) between tags. Return "" if tag not found.
""" """
@ -202,5 +164,4 @@ def _parse_tag(string, tag):
if soup.find(tag) is None: if soup.find(tag) is None:
return '' return ''
return soup.find(tag).string.strip() return soup.find(tag).string.strip()

View File

@ -1,5 +1,6 @@
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
import requests
from flask import abort from flask import abort
from flask import current_app from flask import current_app
from flask import session from flask import session
@ -22,7 +23,6 @@ from api.lib.cmdb.utils import ValueTypeMap
from api.lib.decorator import kwargs_required from api.lib.decorator import kwargs_required
from api.lib.perm.acl.acl import is_app_admin from api.lib.perm.acl.acl import is_app_admin
from api.lib.perm.acl.acl import validate_permission from api.lib.perm.acl.acl import validate_permission
from api.lib.webhook import webhook_request
from api.models.cmdb import Attribute from api.models.cmdb import Attribute
from api.models.cmdb import CIType from api.models.cmdb import CIType
from api.models.cmdb import CITypeAttribute from api.models.cmdb import CITypeAttribute
@ -40,11 +40,15 @@ class AttributeManager(object):
pass pass
@staticmethod @staticmethod
def _get_choice_values_from_webhook(choice_webhook, payload=None): def _get_choice_values_from_web_hook(choice_web_hook):
ret_key = choice_webhook.get('ret_key') 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') or 'GET').lower()
try: try:
res = webhook_request(choice_webhook, payload or {}).json() res = getattr(requests, method)(url, headers=headers, data=payload).json()
if ret_key: if ret_key:
ret_key_list = ret_key.strip().split("##") ret_key_list = ret_key.strip().split("##")
for key in ret_key_list[:-1]: for key in ret_key_list[:-1]:
@ -59,58 +63,19 @@ class AttributeManager(object):
current_app.logger.error("get choice values failed: {}".format(e)) current_app.logger.error("get choice values failed: {}".format(e))
return [] return []
@staticmethod
def _get_choice_values_from_other(choice_other):
from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci import search
if choice_other.get('type_ids'):
type_ids = choice_other.get('type_ids')
attr_id = choice_other.get('attr_id')
other_filter = choice_other.get('filter') or ''
query = "_type:({}),{}".format(";".join(map(str, type_ids)), other_filter)
s = search(query, fl=[str(attr_id)], facet=[str(attr_id)], count=1)
try:
_, _, _, _, _, facet = s.search()
return [[i[0], {}] for i in (list(facet.values()) or [[]])[0]]
except SearchError as e:
current_app.logger.error("get choice values from other ci failed: {}".format(e))
return []
elif choice_other.get('script'):
try:
x = compile(choice_other['script'], '', "exec")
local_ns = {}
exec(x, {}, local_ns)
res = local_ns['ChoiceValue']().values() or []
return [[i, {}] for i in res]
except Exception as e:
current_app.logger.error("get choice values from script: {}".format(e))
return []
@classmethod @classmethod
def get_choice_values(cls, attr_id, value_type, choice_web_hook, choice_other, def get_choice_values(cls, attr_id, value_type, choice_web_hook, choice_web_hook_parse=True):
choice_web_hook_parse=True, choice_other_parse=True):
if choice_web_hook: if choice_web_hook:
if choice_web_hook_parse and isinstance(choice_web_hook, dict): if choice_web_hook_parse:
return cls._get_choice_values_from_webhook(choice_web_hook) if isinstance(choice_web_hook, dict):
else: return cls._get_choice_values_from_web_hook(choice_web_hook)
return []
elif choice_other:
if choice_other_parse and isinstance(choice_other, dict):
return cls._get_choice_values_from_other(choice_other)
else: else:
return [] return []
choice_table = ValueTypeMap.choice.get(value_type) 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) choice_values = choice_table.get_by(fl=["value", "option"], attr_id=attr_id)
return [[ValueTypeMap.serialize[value_type](choice_value['value']), choice_value['option'] or return [[choice_value['value'], choice_value['option']] for choice_value in choice_values]
{"label": ValueTypeMap.serialize[value_type](choice_value['value'])}]
for choice_value in choice_values]
@staticmethod @staticmethod
def add_choice_values(_id, value_type, choice_values): def add_choice_values(_id, value_type, choice_values):
@ -136,15 +101,6 @@ class AttributeManager(object):
choice_table and choice_table.get_by(attr_id=_id, only_query=True).delete() choice_table and choice_table.get_by(attr_id=_id, only_query=True).delete()
db.session.flush() db.session.flush()
@classmethod
def get_enum_map(cls, _attr_id, _attr=None):
attr = AttributeCache.get(_attr_id) if _attr_id else _attr
if attr and attr.is_choice:
choice_values = cls.get_choice_values(attr.id, attr.value_type, None, None)
return {i[0]: i[1]['label'] for i in choice_values if i[1] and i[1].get('label')}
return {}
@classmethod @classmethod
def search_attributes(cls, name=None, alias=None, page=1, page_size=None): def search_attributes(cls, name=None, alias=None, page=1, page_size=None):
""" """
@ -166,8 +122,7 @@ class AttributeManager(object):
res = list() res = list()
for attr in attrs: for attr in attrs:
attr["is_choice"] and attr.update( attr["is_choice"] and attr.update(
dict(choice_value=cls.get_choice_values(attr["id"], attr["value_type"], dict(choice_value=cls.get_choice_values(attr["id"], attr["value_type"], attr["choice_web_hook"])))
attr["choice_web_hook"], attr.get("choice_other"))))
attr['is_choice'] and attr.pop('choice_web_hook', None) attr['is_choice'] and attr.pop('choice_web_hook', None)
res.append(attr) res.append(attr)
@ -177,45 +132,29 @@ class AttributeManager(object):
def get_attribute_by_name(self, name): def get_attribute_by_name(self, name):
attr = Attribute.get_by(name=name, first=True) attr = Attribute.get_by(name=name, first=True)
if attr.get("is_choice"): if attr.get("is_choice"):
attr["choice_value"] = self.get_choice_values(attr["id"], attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"], attr["choice_web_hook"])
attr["value_type"],
attr["choice_web_hook"],
attr.get("choice_other"))
return attr return attr
def get_attribute_by_alias(self, alias): def get_attribute_by_alias(self, alias):
attr = Attribute.get_by(alias=alias, first=True) attr = Attribute.get_by(alias=alias, first=True)
if attr.get("is_choice"): if attr.get("is_choice"):
attr["choice_value"] = self.get_choice_values(attr["id"], attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"], attr["choice_web_hook"])
attr["value_type"],
attr["choice_web_hook"],
attr.get("choice_other"))
return attr return attr
def get_attribute_by_id(self, _id): def get_attribute_by_id(self, _id):
attr = Attribute.get_by_id(_id).to_dict() attr = Attribute.get_by_id(_id).to_dict()
if attr.get("is_choice"): if attr.get("is_choice"):
attr["choice_value"] = self.get_choice_values(attr["id"], attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"], attr["choice_web_hook"])
attr["value_type"],
attr["choice_web_hook"],
attr.get("choice_other"))
return attr return attr
def get_attribute(self, key, choice_web_hook_parse=True, choice_other_parse=True): def get_attribute(self, key, choice_web_hook_parse=True):
attr = AttributeCache.get(key) or dict() attr = AttributeCache.get(key).to_dict()
attr = attr and attr.to_dict()
if attr.get("is_choice"): if attr.get("is_choice"):
attr["choice_value"] = self.get_choice_values( attr["choice_value"] = self.get_choice_values(
attr["id"], attr["id"], attr["value_type"], attr["choice_web_hook"], choice_web_hook_parse=choice_web_hook_parse)
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 return attr
@ -224,15 +163,13 @@ class AttributeManager(object):
if RoleEnum.CONFIG not in session.get("acl", {}).get("parentRoles", []) and not is_app_admin('cmdb'): 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)) return abort(403, ErrFormat.role_required.format(RoleEnum.CONFIG))
@classmethod @staticmethod
def calc_computed_attribute(cls, attr_id): def calc_computed_attribute(attr_id):
""" """
calculate computed attribute for all ci calculate computed attribute for all ci
:param attr_id: :param attr_id:
:return: :return:
""" """
cls.can_create_computed_attribute()
from api.tasks.cmdb import calc_computed_attribute from api.tasks.cmdb import calc_computed_attribute
calc_computed_attribute.apply_async(args=(attr_id, current_user.uid), queue=CMDB_QUEUE) calc_computed_attribute.apply_async(args=(attr_id, current_user.uid), queue=CMDB_QUEUE)
@ -242,22 +179,12 @@ class AttributeManager(object):
def add(cls, **kwargs): def add(cls, **kwargs):
choice_value = kwargs.pop("choice_value", []) choice_value = kwargs.pop("choice_value", [])
kwargs.pop("is_choice", None) kwargs.pop("is_choice", None)
is_choice = True if choice_value or kwargs.get('choice_web_hook') or kwargs.get('choice_other') else False is_choice = True if choice_value or kwargs.get('choice_web_hook') else False
name = kwargs.pop("name") name = kwargs.pop("name")
if name in BUILTIN_KEYWORDS or kwargs.get('alias') in BUILTIN_KEYWORDS: if name in BUILTIN_KEYWORDS:
return abort(400, ErrFormat.attribute_name_cannot_be_builtin) return abort(400, ErrFormat.attribute_name_cannot_be_builtin)
while kwargs.get('choice_other'):
if isinstance(kwargs['choice_other'], dict):
if kwargs['choice_other'].get('script'):
break
if kwargs['choice_other'].get('type_ids') and kwargs['choice_other'].get('attr_id'):
break
return abort(400, ErrFormat.attribute_choice_other_invalid)
alias = kwargs.pop("alias", "") alias = kwargs.pop("alias", "")
alias = name if not alias else alias alias = name if not alias else alias
Attribute.get_by(name=name, first=True) and abort(400, ErrFormat.attribute_name_duplicate.format(name)) Attribute.get_by(name=name, first=True) and abort(400, ErrFormat.attribute_name_duplicate.format(name))
@ -267,8 +194,6 @@ class AttributeManager(object):
kwargs.get('is_computed') and cls.can_create_computed_attribute() kwargs.get('is_computed') and cls.can_create_computed_attribute()
kwargs.get('choice_other') and kwargs['choice_other'].get('script') and cls.can_create_computed_attribute()
attr = Attribute.create(flush=True, attr = Attribute.create(flush=True,
name=name, name=name,
alias=alias, alias=alias,
@ -354,6 +279,9 @@ class AttributeManager(object):
def update(self, _id, **kwargs): def update(self, _id, **kwargs):
attr = Attribute.get_by_id(_id) or abort(404, ErrFormat.attribute_not_found.format("id={}".format(_id))) attr = Attribute.get_by_id(_id) or abort(404, ErrFormat.attribute_not_found.format("id={}".format(_id)))
if not self._can_edit_attribute(attr):
return abort(403, ErrFormat.cannot_edit_attribute)
if kwargs.get("name"): if kwargs.get("name"):
other = Attribute.get_by(name=kwargs['name'], first=True, to_dict=False) other = Attribute.get_by(name=kwargs['name'], first=True, to_dict=False)
if other and other.id != attr.id: if other and other.id != attr.id:
@ -371,22 +299,12 @@ class AttributeManager(object):
self._change_index(attr, attr.is_index, kwargs['is_index']) self._change_index(attr, attr.is_index, kwargs['is_index'])
while kwargs.get('choice_other'):
if isinstance(kwargs['choice_other'], dict):
if kwargs['choice_other'].get('script'):
break
if kwargs['choice_other'].get('type_ids') and kwargs['choice_other'].get('attr_id'):
break
return abort(400, ErrFormat.attribute_choice_other_invalid)
existed2 = attr.to_dict() existed2 = attr.to_dict()
if not existed2['choice_web_hook'] and not existed2.get('choice_other') and existed2['is_choice']: if not existed2['choice_web_hook'] and existed2['is_choice']:
existed2['choice_value'] = self.get_choice_values(attr.id, attr.value_type, None, None) existed2['choice_value'] = self.get_choice_values(attr.id, attr.value_type, attr.choice_web_hook)
choice_value = kwargs.pop("choice_value", False) choice_value = kwargs.pop("choice_value", False)
is_choice = True if choice_value or kwargs.get('choice_web_hook') or kwargs.get('choice_other') else False is_choice = True if choice_value or kwargs.get('choice_web_hook') else False
kwargs['is_choice'] = is_choice kwargs['is_choice'] = is_choice
if kwargs.get('default') and not (isinstance(kwargs['default'], dict) and 'default' in kwargs['default']): if kwargs.get('default') and not (isinstance(kwargs['default'], dict) and 'default' in kwargs['default']):
@ -394,14 +312,6 @@ class AttributeManager(object):
kwargs.get('is_computed') and self.can_create_computed_attribute() kwargs.get('is_computed') and self.can_create_computed_attribute()
is_changed = False
for k in kwargs:
if kwargs[k] != getattr(attr, k, None):
is_changed = True
if is_changed and not self._can_edit_attribute(attr):
return abort(403, ErrFormat.cannot_edit_attribute)
attr.update(flush=True, filter_none=False, **kwargs) attr.update(flush=True, filter_none=False, **kwargs)
if is_choice and choice_value: if is_choice and choice_value:
@ -415,7 +325,7 @@ class AttributeManager(object):
db.session.rollback() db.session.rollback()
current_app.logger.error("update attribute error, {0}".format(str(e))) current_app.logger.error("update attribute error, {0}".format(str(e)))
return abort(400, ErrFormat.update_attribute_failed.format(("id={}".format(_id)))) return abort(400, ErrFormat.update_attribute_failed.format(("id=".format(_id))))
new = attr.to_dict() new = attr.to_dict()
if not new['choice_web_hook'] and new['is_choice']: if not new['choice_web_hook'] and new['is_choice']:

View File

@ -1,61 +1,44 @@
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
import copy
import datetime import datetime
import json import json
import jsonpath
import os import os
from flask import abort from flask import abort
from flask import current_app from flask import current_app
from flask_login import current_user from flask_login import current_user
from sqlalchemy import func from sqlalchemy import func
from api.extensions import db from api.extensions import db
from api.lib.cmdb.auto_discovery.const import CLOUD_MAP from api.lib.cmdb.auto_discovery.const import ClOUD_MAP
from api.lib.cmdb.auto_discovery.const import DEFAULT_INNER
from api.lib.cmdb.auto_discovery.const import PRIVILEGED_USERS
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import AutoDiscoveryMappingCache
from api.lib.cmdb.cache import CITypeAttributeCache from api.lib.cmdb.cache import CITypeAttributeCache
from api.lib.cmdb.cache import CITypeCache from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.ci import CIManager from api.lib.cmdb.ci import CIManager
from api.lib.cmdb.ci import CIRelationManager
from api.lib.cmdb.ci_type import CITypeGroupManager from api.lib.cmdb.ci_type import CITypeGroupManager
from api.lib.cmdb.const import AutoDiscoveryType from api.lib.cmdb.const import AutoDiscoveryType
from api.lib.cmdb.const import CMDB_QUEUE
from api.lib.cmdb.const import PermEnum from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import ResourceTypeEnum from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.custom_dashboard import SystemConfigManager
from api.lib.cmdb.resp_format import ErrFormat from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.search import SearchError from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci import search as ci_search from api.lib.cmdb.search.ci import search
from api.lib.common_setting.role_perm_base import CMDBApp
from api.lib.mixin import DBMixin from api.lib.mixin import DBMixin
from api.lib.perm.acl.acl import ACLManager
from api.lib.perm.acl.acl import is_app_admin from api.lib.perm.acl.acl import is_app_admin
from api.lib.perm.acl.acl import validate_permission from api.lib.perm.acl.acl import validate_permission
from api.lib.utils import AESCrypto from api.lib.utils import AESCrypto
from api.models.cmdb import AutoDiscoveryAccount
from api.models.cmdb import AutoDiscoveryCI from api.models.cmdb import AutoDiscoveryCI
from api.models.cmdb import AutoDiscoveryCIType from api.models.cmdb import AutoDiscoveryCIType
from api.models.cmdb import AutoDiscoveryCITypeRelation
from api.models.cmdb import AutoDiscoveryCounter
from api.models.cmdb import AutoDiscoveryExecHistory
from api.models.cmdb import AutoDiscoveryRule from api.models.cmdb import AutoDiscoveryRule
from api.models.cmdb import AutoDiscoveryRuleSyncHistory
from api.tasks.cmdb import build_relations_for_ad_accept
from api.tasks.cmdb import write_ad_rule_sync_history
PWD = os.path.abspath(os.path.dirname(__file__)) PWD = os.path.abspath(os.path.dirname(__file__))
app_cli = CMDBApp()
def parse_plugin_script(script): def parse_plugin_script(script):
attributes = [] attributes = []
try: try:
x = compile(script, '', "exec") x = compile(script, '', "exec")
local_ns = {} exec(x)
exec(x, {}, local_ns) unique_key = locals()['AutoDiscovery']().unique_key
unique_key = local_ns['AutoDiscovery']().unique_key attrs = locals()['AutoDiscovery']().attributes() or []
attrs = local_ns['AutoDiscovery']().attributes() or []
except Exception as e: except Exception as e:
return abort(400, str(e)) return abort(400, str(e))
@ -113,30 +96,14 @@ class AutoDiscoveryRuleCRUD(DBMixin):
else: else:
self.cls.create(**rule) self.cls.create(**rule)
def _can_add(self, valid=True, **kwargs): def _can_add(self, **kwargs):
self.cls.get_by(name=kwargs['name']) and abort(400, ErrFormat.adr_duplicate.format(kwargs['name'])) self.cls.get_by(name=kwargs['name']) and abort(400, ErrFormat.adr_duplicate.format(kwargs['name']))
if kwargs.get('is_plugin') and kwargs.get('plugin_script') and valid: if kwargs.get('is_plugin') and kwargs.get('plugin_script'):
kwargs = check_plugin_script(**kwargs) kwargs = check_plugin_script(**kwargs)
acl = ACLManager(app_cli.app_name)
has_perm = True
try:
if not acl.has_permission(app_cli.op.Auto_Discovery,
app_cli.resource_type_name,
app_cli.op.create_plugin) and not is_app_admin(app_cli.app_name):
has_perm = False
except Exception:
if not is_app_admin(app_cli.app_name):
return abort(403, ErrFormat.role_required.format(app_cli.admin_name))
if not has_perm:
return abort(403, ErrFormat.no_permission.format(
app_cli.op.Auto_Discovery, app_cli.op.create_plugin))
kwargs['owner'] = current_user.uid
return kwargs return kwargs
def _can_update(self, valid=True, **kwargs): def _can_update(self, **kwargs):
existed = self.cls.get_by_id(kwargs['_id']) or abort( existed = self.cls.get_by_id(kwargs['_id']) or abort(
404, ErrFormat.adr_not_found.format("id={}".format(kwargs['_id']))) 404, ErrFormat.adr_not_found.format("id={}".format(kwargs['_id'])))
@ -148,22 +115,6 @@ class AutoDiscoveryRuleCRUD(DBMixin):
if other and other.id != existed.id: if other and other.id != existed.id:
return abort(400, ErrFormat.adr_duplicate.format(kwargs['name'])) return abort(400, ErrFormat.adr_duplicate.format(kwargs['name']))
if existed.is_plugin and valid:
acl = ACLManager(app_cli.app_name)
has_perm = True
try:
if not acl.has_permission(app_cli.op.Auto_Discovery,
app_cli.resource_type_name,
app_cli.op.update_plugin) and not is_app_admin(app_cli.app_name):
has_perm = False
except Exception:
if not is_app_admin(app_cli.app_name):
return abort(403, ErrFormat.role_required.format(app_cli.admin_name))
if not has_perm:
return abort(403, ErrFormat.no_permission.format(
app_cli.op.Auto_Discovery, app_cli.op.update_plugin))
return existed return existed
def update(self, _id, **kwargs): def update(self, _id, **kwargs):
@ -171,44 +122,21 @@ class AutoDiscoveryRuleCRUD(DBMixin):
if kwargs.get('is_plugin') and kwargs.get('plugin_script'): if kwargs.get('is_plugin') and kwargs.get('plugin_script'):
kwargs = check_plugin_script(**kwargs) kwargs = check_plugin_script(**kwargs)
for item in AutoDiscoveryCIType.get_by(adr_id=_id, to_dict=False):
item.update(updated_at=datetime.datetime.now())
return super(AutoDiscoveryRuleCRUD, self).update(_id, filter_none=False, **kwargs) return super(AutoDiscoveryRuleCRUD, self).update(_id, filter_none=False, **kwargs)
def _can_delete(self, **kwargs): def _can_delete(self, **kwargs):
if AutoDiscoveryCIType.get_by(adr_id=kwargs['_id'], first=True): if AutoDiscoveryCIType.get_by(adr_id=kwargs['_id'], first=True):
return abort(400, ErrFormat.adr_referenced) return abort(400, ErrFormat.adr_referenced)
existed = self.cls.get_by_id(kwargs['_id']) or abort( return self._can_update(**kwargs)
404, ErrFormat.adr_not_found.format("id={}".format(kwargs['_id'])))
if existed.is_plugin:
acl = ACLManager(app_cli.app_name)
has_perm = True
try:
if not acl.has_permission(app_cli.op.Auto_Discovery,
app_cli.resource_type_name,
app_cli.op.delete_plugin) and not is_app_admin(app_cli.app_name):
has_perm = False
except Exception:
if not is_app_admin(app_cli.app_name):
return abort(403, ErrFormat.role_required.format(app_cli.admin_name))
if not has_perm:
return abort(403, ErrFormat.no_permission.format(
app_cli.op.Auto_Discovery, app_cli.op.delete_plugin))
return existed
class AutoDiscoveryCITypeCRUD(DBMixin): class AutoDiscoveryCITypeCRUD(DBMixin):
cls = AutoDiscoveryCIType cls = AutoDiscoveryCIType
@classmethod @classmethod
def get_all(cls, type_ids=None): def get_all(cls):
res = cls.cls.get_by(to_dict=False) return cls.cls.get_by(to_dict=False)
return [i for i in res if type_ids is None or i.type_id in type_ids]
@classmethod @classmethod
def get_by_id(cls, _id): def get_by_id(cls, _id):
@ -219,59 +147,25 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
return cls.cls.get_by(type_id=type_id, to_dict=False) return cls.cls.get_by(type_id=type_id, to_dict=False)
@classmethod @classmethod
def get_ad_attributes(cls, type_id): def get(cls, ci_id, oneagent_id, last_update_at=None):
result = []
adts = cls.get_by_type_id(type_id)
for adt in adts:
adr = AutoDiscoveryRuleCRUD.get_by_id(adt.adr_id)
if not adr:
continue
if adr.type == AutoDiscoveryType.HTTP:
for i in DEFAULT_INNER:
if adr.name == i['name']:
attrs = AutoDiscoveryHTTPManager.get_attributes(
i['en'], (adt.extra_option or {}).get('category')) or []
result.extend([i.get('name') for i in attrs])
break
elif adr.type == AutoDiscoveryType.SNMP:
attributes = AutoDiscoverySNMPManager.get_attributes()
result.extend([i.get('name') for i in (attributes or [])])
else:
result.extend([i.get('name') for i in (adr.attributes or [])])
return sorted(list(set(result)))
@classmethod
def get(cls, ci_id, oneagent_id, oneagent_name, last_update_at=None):
"""
OneAgent sync rules
:param ci_id:
:param oneagent_id:
:param oneagent_name:
:param last_update_at:
:return:
"""
result = [] result = []
rules = cls.cls.get_by(to_dict=True) rules = cls.cls.get_by(to_dict=True)
for rule in rules: for rule in rules:
if not rule['enabled']: if rule.get('relation'):
continue continue
if isinstance(rule.get("extra_option"), dict): if isinstance(rule.get("extra_option"), dict) and rule['extra_option'].get('secret'):
decrypt_account(rule['extra_option'], rule['uid']) if not (current_user.username == "cmdb_agent" or current_user.uid == rule['uid']):
if rule['extra_option'].get('_reference'):
rule['extra_option'].pop('password', None)
rule['extra_option'].pop('secret', None) rule['extra_option'].pop('secret', None)
rule['extra_option'].update( else:
AutoDiscoveryAccountCRUD().get_config_by_id(rule['extra_option']['_reference'])) rule['extra_option']['secret'] = AESCrypto.decrypt(rule['extra_option']['secret'])
if oneagent_id and rule['agent_id'] == oneagent_id: if oneagent_id and rule['agent_id'] == oneagent_id:
result.append(rule) result.append(rule)
elif rule['query_expr']: elif rule['query_expr']:
query = rule['query_expr'].lstrip('q').lstrip('=') query = rule['query_expr'].lstrip('q').lstrip('=')
s = ci_search(query, fl=['_id'], count=1000000) s = search(query, fl=['_id'], count=1000000)
try: try:
response, _, _, _, _, _ = s.search() response, _, _, _, _, _ = s.search()
except SearchError as e: except SearchError as e:
@ -282,32 +176,25 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
result.append(rule) result.append(rule)
break break
elif not rule['agent_id'] and not rule['query_expr'] and rule['adr_id']: elif not rule['agent_id'] and not rule['query_expr'] and rule['adr_id']:
try:
if not int(oneagent_id, 16): # excludes master
continue
except Exception:
pass
adr = AutoDiscoveryRuleCRUD.get_by_id(rule['adr_id']) adr = AutoDiscoveryRuleCRUD.get_by_id(rule['adr_id'])
if not adr: if not adr:
continue continue
if adr.type in (AutoDiscoveryType.SNMP, AutoDiscoveryType.HTTP): if adr.type in (AutoDiscoveryType.SNMP, AutoDiscoveryType.HTTP):
continue continue
if not rule['updated_at']:
continue
result.append(rule) result.append(rule)
ad_rules_updated_at = (SystemConfigManager.get('ad_rules_updated_at') or {}).get('option', {}).get('v') or ""
new_last_update_at = "" new_last_update_at = ""
for i in result: for i in result:
i['adr'] = AutoDiscoveryRule.get_by_id(i['adr_id']).to_dict() i['adr'] = AutoDiscoveryRule.get_by_id(i['adr_id']).to_dict()
i['adr'].pop("attributes", None)
__last_update_at = max([i['updated_at'] or "", i['created_at'] or "", __last_update_at = max([i['updated_at'] or "", i['created_at'] or "",
i['adr']['created_at'] or "", i['adr']['updated_at'] or "", ad_rules_updated_at]) i['adr']['created_at'] or "", i['adr']['updated_at'] or ""])
if new_last_update_at < __last_update_at: if new_last_update_at < __last_update_at:
new_last_update_at = __last_update_at new_last_update_at = __last_update_at
write_ad_rule_sync_history.apply_async(args=(result, oneagent_id, oneagent_name, datetime.datetime.now()),
queue=CMDB_QUEUE)
if not last_update_at or new_last_update_at > last_update_at: if not last_update_at or new_last_update_at > last_update_at:
return result, new_last_update_at return result, new_last_update_at
else: else:
@ -326,7 +213,7 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
agent_id = agent_id.strip() agent_id = agent_id.strip()
q = "op_duty:{0},-rd_duty:{0},oneagent_id:{1}" q = "op_duty:{0},-rd_duty:{0},oneagent_id:{1}"
s = ci_search(q.format(current_user.username, agent_id.strip())) s = search(q.format(current_user.username, agent_id.strip()))
try: try:
response, _, _, _, _, _ = s.search() response, _, _, _, _, _ = s.search()
if response: if response:
@ -335,7 +222,7 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
current_app.logger.warning(e) current_app.logger.warning(e)
return abort(400, str(e)) return abort(400, str(e))
s = ci_search(q.format(current_user.nickname, agent_id.strip())) s = search(q.format(current_user.nickname, agent_id.strip()))
try: try:
response, _, _, _, _, _ = s.search() response, _, _, _, _, _ = s.search()
if response: if response:
@ -349,7 +236,7 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
if query_expr.startswith('q='): if query_expr.startswith('q='):
query_expr = query_expr[2:] query_expr = query_expr[2:]
s = ci_search(query_expr, count=1000000) s = search(query_expr, count=1000000)
try: try:
response, _, _, _, _, _ = s.search() response, _, _, _, _, _ = s.search()
for i in response: for i in response:
@ -363,43 +250,26 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
current_app.logger.warning(e) current_app.logger.warning(e)
return abort(400, str(e)) return abort(400, str(e))
@staticmethod def _can_add(self, **kwargs):
def _can_add(**kwargs): self.cls.get_by(type_id=kwargs['type_id'], adr_id=kwargs.get('adr_id') or None) and abort(
400, ErrFormat.ad_duplicate)
# self.__valid_exec_target(kwargs.get('agent_id'), kwargs.get('query_expr'))
if kwargs.get('adr_id'): if kwargs.get('adr_id'):
adr = AutoDiscoveryRule.get_by_id(kwargs['adr_id']) or abort( adr = AutoDiscoveryRule.get_by_id(kwargs['adr_id']) or abort(
404, ErrFormat.adr_not_found.format("id={}".format(kwargs['adr_id']))) 404, ErrFormat.adr_not_found.format("id={}".format(kwargs['adr_id'])))
if adr.type == AutoDiscoveryType.HTTP: if not adr.is_plugin:
kwargs.setdefault('extra_option', dict()) other = self.cls.get_by(adr_id=adr.id, first=True, to_dict=False)
en_name = None if other:
for i in DEFAULT_INNER: ci_type = CITypeCache.get(other.type_id)
if i['name'] == adr.name: return abort(400, ErrFormat.adr_default_ref_once.format(ci_type.alias))
en_name = i['en']
break
if en_name and kwargs['extra_option'].get('category'):
for item in CLOUD_MAP[en_name]:
if item["collect_key_map"].get(kwargs['extra_option']['category']):
kwargs["extra_option"]["collect_key"] = item["collect_key_map"][
kwargs['extra_option']['category']]
kwargs["extra_option"]["provider"] = en_name
break
if adr.type == AutoDiscoveryType.COMPONENTS and kwargs.get('extra_option'):
for i in DEFAULT_INNER:
if i['name'] == adr.name:
kwargs['extra_option']['collect_key'] = i['option'].get('collect_key')
break
if kwargs.get('is_plugin') and kwargs.get('plugin_script'): if kwargs.get('is_plugin') and kwargs.get('plugin_script'):
kwargs = check_plugin_script(**kwargs) kwargs = check_plugin_script(**kwargs)
encrypt_account(kwargs.get('extra_option')) if isinstance(kwargs.get('extra_option'), dict) and kwargs['extra_option'].get('secret'):
kwargs['extra_option']['secret'] = AESCrypto.encrypt(kwargs['extra_option']['secret'])
ci_type = CITypeCache.get(kwargs['type_id'])
unique = AttributeCache.get(ci_type.unique_id)
if unique and unique.name not in (kwargs.get('attributes') or {}).values():
current_app.logger.warning((unique.name, kwargs.get('attributes'), ci_type.alias))
return abort(400, ErrFormat.ad_not_unique_key.format(unique.name))
kwargs['uid'] = current_user.uid kwargs['uid'] = current_user.uid
@ -409,44 +279,11 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
existed = self.cls.get_by_id(kwargs['_id']) or abort( existed = self.cls.get_by_id(kwargs['_id']) or abort(
404, ErrFormat.ad_not_found.format("id={}".format(kwargs['_id']))) 404, ErrFormat.ad_not_found.format("id={}".format(kwargs['_id'])))
adr = AutoDiscoveryRule.get_by_id(existed.adr_id) or abort( self.__valid_exec_target(kwargs.get('agent_id'), kwargs.get('query_expr'))
404, ErrFormat.adr_not_found.format("id={}".format(existed.adr_id)))
if adr.type == AutoDiscoveryType.HTTP:
kwargs.setdefault('extra_option', dict())
en_name = None
for i in DEFAULT_INNER:
if i['name'] == adr.name:
en_name = i['en']
break
if en_name and kwargs['extra_option'].get('category'):
for item in CLOUD_MAP[en_name]:
if item["collect_key_map"].get(kwargs['extra_option']['category']):
kwargs["extra_option"]["collect_key"] = item["collect_key_map"][
kwargs['extra_option']['category']]
kwargs["extra_option"]["provider"] = en_name
break
if adr.type == AutoDiscoveryType.COMPONENTS and kwargs.get('extra_option'):
for i in DEFAULT_INNER:
if i['name'] == adr.name:
kwargs['extra_option']['collect_key'] = i['option'].get('collect_key')
break
if 'attributes' in kwargs:
self.__valid_exec_target(kwargs.get('agent_id'), kwargs.get('query_expr'))
ci_type = CITypeCache.get(existed.type_id)
unique = AttributeCache.get(ci_type.unique_id)
if unique and unique.name not in (kwargs.get('attributes') or {}).values():
current_app.logger.warning((unique.name, kwargs.get('attributes'), ci_type.alias))
return abort(400, ErrFormat.ad_not_unique_key.format(unique.name))
if isinstance(kwargs.get('extra_option'), dict) and kwargs['extra_option'].get('secret'): if isinstance(kwargs.get('extra_option'), dict) and kwargs['extra_option'].get('secret'):
if current_user.uid != existed.uid: if current_user.uid != existed.uid:
return abort(403, ErrFormat.adt_secret_no_permission) return abort(403, ErrFormat.adt_secret_no_permission)
if isinstance(kwargs.get('extra_option'), dict) and kwargs['extra_option'].get('password'):
if current_user.uid != existed.uid:
return abort(403, ErrFormat.adt_secret_no_permission)
return existed return existed
@ -455,22 +292,10 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
if kwargs.get('is_plugin') and kwargs.get('plugin_script'): if kwargs.get('is_plugin') and kwargs.get('plugin_script'):
kwargs = check_plugin_script(**kwargs) kwargs = check_plugin_script(**kwargs)
encrypt_account(kwargs.get('extra_option')) if isinstance(kwargs.get('extra_option'), dict) and kwargs['extra_option'].get('secret'):
kwargs['extra_option']['secret'] = AESCrypto.encrypt(kwargs['extra_option']['secret'])
inst = self._can_update(_id=_id, **kwargs) return super(AutoDiscoveryCITypeCRUD, self).update(_id, filter_none=False, **kwargs)
if len(kwargs) == 1 and 'enabled' in kwargs: # enable or disable
pass
elif inst.agent_id != kwargs.get('agent_id') or inst.query_expr != kwargs.get('query_expr'):
for item in AutoDiscoveryRuleSyncHistory.get_by(adt_id=inst.id, to_dict=False):
item.delete(commit=False)
db.session.commit()
SystemConfigManager.create_or_update("ad_rules_updated_at",
dict(v=datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")))
obj = inst.update(_id=_id, filter_none=False, **kwargs)
return obj
def _can_delete(self, **kwargs): def _can_delete(self, **kwargs):
if AutoDiscoveryCICRUD.get_by_adt_id(kwargs['_id']): if AutoDiscoveryCICRUD.get_by_adt_id(kwargs['_id']):
@ -481,61 +306,6 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
return existed return existed
def delete(self, _id):
inst = self._can_delete(_id=_id)
inst.soft_delete()
for item in AutoDiscoveryRuleSyncHistory.get_by(adt_id=inst.id, to_dict=False):
item.delete(commit=False)
db.session.commit()
attributes = self.get_ad_attributes(inst.type_id)
for item in AutoDiscoveryCITypeRelationCRUD.get_by_type_id(inst.type_id):
if item.ad_key not in attributes:
item.soft_delete()
return inst
class AutoDiscoveryCITypeRelationCRUD(DBMixin):
cls = AutoDiscoveryCITypeRelation
@classmethod
def get_all(cls, type_ids=None):
res = cls.cls.get_by(to_dict=False)
return [i for i in res if type_ids is None or i.ad_type_id in type_ids]
@classmethod
def get_by_type_id(cls, type_id, to_dict=False):
return cls.cls.get_by(ad_type_id=type_id, to_dict=to_dict)
def upsert(self, ad_type_id, relations):
existed = self.cls.get_by(ad_type_id=ad_type_id, to_dict=False)
existed = {(i.ad_key, i.peer_type_id, i.peer_attr_id): i for i in existed}
new = []
for r in relations:
k = (r.get('ad_key'), r.get('peer_type_id'), r.get('peer_attr_id'))
if len(list(filter(lambda x: x, k))) == 3 and k not in existed:
self.cls.create(ad_type_id=ad_type_id, **r)
new.append(k)
for deleted in set(existed.keys()) - set(new):
existed[deleted].soft_delete()
return self.get_by_type_id(ad_type_id, to_dict=True)
def _can_add(self, **kwargs):
pass
def _can_update(self, **kwargs):
pass
def _can_delete(self, **kwargs):
pass
class AutoDiscoveryCICRUD(DBMixin): class AutoDiscoveryCICRUD(DBMixin):
cls = AutoDiscoveryCI cls = AutoDiscoveryCI
@ -563,14 +333,15 @@ class AutoDiscoveryCICRUD(DBMixin):
@staticmethod @staticmethod
def get_attributes_by_type_id(type_id): def get_attributes_by_type_id(type_id):
from api.lib.cmdb.ci_type import CITypeAttributeManager from api.lib.cmdb.cache import CITypeAttributesCache
attributes = [i for i in CITypeAttributeManager.get_attributes_by_type_id(type_id) or []] attributes = [i[1] for i in CITypeAttributesCache.get2(type_id) or []]
attr_names = set() attr_names = set()
adts = AutoDiscoveryCITypeCRUD.get_by_type_id(type_id) adts = AutoDiscoveryCITypeCRUD.get_by_type_id(type_id)
for adt in adts: for adt in adts:
attr_names |= set((adt.attributes or {}).values()) attr_names |= set((adt.attributes or {}).values())
return [attr for attr in attributes if attr['name'] in attr_names]
return [attr.to_dict() for attr in attributes if attr.name in attr_names]
@classmethod @classmethod
def search(cls, page, page_size, fl=None, **kwargs): def search(cls, page, page_size, fl=None, **kwargs):
@ -623,24 +394,16 @@ class AutoDiscoveryCICRUD(DBMixin):
changed = False changed = False
if existed is not None: if existed is not None:
if existed.instance != kwargs['instance']: if existed.instance != kwargs['instance']:
instance = copy.deepcopy(existed.instance) or {}
instance.update(kwargs['instance'])
kwargs['instance'] = instance
existed.update(filter_none=False, **kwargs) existed.update(filter_none=False, **kwargs)
AutoDiscoveryExecHistoryCRUD().add(type_id=adt.type_id,
stdout="update resource: {}".format(kwargs.get('unique_value')))
changed = True changed = True
else: else:
existed = self.cls.create(**kwargs) existed = self.cls.create(**kwargs)
AutoDiscoveryExecHistoryCRUD().add(type_id=adt.type_id,
stdout="add resource: {}".format(kwargs.get('unique_value')))
changed = True changed = True
if adt.auto_accept and changed: if adt.auto_accept and changed:
try: try:
self.accept(existed) self.accept(existed)
except Exception as e: except Exception as e:
current_app.logger.error(e)
return abort(400, str(e)) return abort(400, str(e))
elif changed: elif changed:
existed.update(is_accept=False, accept_time=None, accept_by=None, filter_none=False) existed.update(is_accept=False, accept_time=None, accept_by=None, filter_none=False)
@ -660,13 +423,6 @@ class AutoDiscoveryCICRUD(DBMixin):
inst.delete() inst.delete()
adt = AutoDiscoveryCIType.get_by_id(inst.adt_id)
if adt:
adt.update(updated_at=datetime.datetime.now())
AutoDiscoveryExecHistoryCRUD().add(type_id=inst.type_id,
stdout="delete resource: {}".format(inst.unique_value))
self._after_delete(inst) self._after_delete(inst)
return inst return inst
@ -682,13 +438,6 @@ class AutoDiscoveryCICRUD(DBMixin):
not is_app_admin("cmdb") and validate_permission(ci_type.name, ResourceTypeEnum.CI, PermEnum.DELETE, "cmdb") not is_app_admin("cmdb") and validate_permission(ci_type.name, ResourceTypeEnum.CI, PermEnum.DELETE, "cmdb")
existed.delete() existed.delete()
adt = AutoDiscoveryCIType.get_by_id(existed.adt_id)
if adt:
adt.update(updated_at=datetime.datetime.now())
AutoDiscoveryExecHistoryCRUD().add(type_id=type_id,
stdout="delete resource: {}".format(unique_value))
# TODO: delete ci # TODO: delete ci
@classmethod @classmethod
@ -699,24 +448,36 @@ class AutoDiscoveryCICRUD(DBMixin):
adt = AutoDiscoveryCITypeCRUD.get_by_id(adc.adt_id) or abort(404, ErrFormat.adt_not_found) adt = AutoDiscoveryCITypeCRUD.get_by_id(adc.adt_id) or abort(404, ErrFormat.adt_not_found)
ci_id = None ci_id = None
if adt.attributes:
ci_dict = {adt.attributes[k]: v for k, v in adc.instance.items() if k in adt.attributes}
ci_id = CIManager.add(adc.type_id, is_auto_discovery=True, **ci_dict)
ad_key2attr = adt.attributes or {} relation_adts = AutoDiscoveryCIType.get_by(type_id=adt.type_id, adr_id=None, to_dict=False)
if ad_key2attr: for r_adt in relation_adts:
ci_dict = {ad_key2attr[k]: None if not v and isinstance(v, (list, dict)) else v if not r_adt.relation or ci_id is None:
for k, v in adc.instance.items() if k in ad_key2attr} continue
extra_option = adt.extra_option or {} for ad_key in r_adt.relation:
mapping, path_mapping = AutoDiscoveryHTTPManager.get_predefined_value_mapping( if not adc.instance.get(ad_key):
extra_option.get('provider'), extra_option.get('category')) continue
if mapping: cmdb_key = r_adt.relation[ad_key]
ci_dict = {k: (mapping.get(k) or {}).get(str(v), v) for k, v in ci_dict.items()} query = "_type:{},{}:{}".format(cmdb_key.get('type_name'), cmdb_key.get('attr_name'),
if path_mapping: adc.instance.get(ad_key))
ci_dict = {k: jsonpath.jsonpath(v, path_mapping[k]) if k in path_mapping else v s = search(query)
for k, v in ci_dict.items()} try:
ci_id = CIManager.add(adc.type_id, is_auto_discovery=True, _is_admin=True, **ci_dict) response, _, _, _, _, _ = s.search()
AutoDiscoveryExecHistoryCRUD().add(type_id=adt.type_id, except SearchError as e:
stdout="accept resource: {}".format(adc.unique_value)) current_app.logger.warning(e)
return abort(400, str(e))
build_relations_for_ad_accept.apply_async(args=(adc.to_dict(), ci_id, ad_key2attr), queue=CMDB_QUEUE) relation_ci_id = response and response[0]['_id']
if relation_ci_id:
try:
CIRelationManager.add(ci_id, relation_ci_id)
except:
try:
CIRelationManager.add(relation_ci_id, ci_id)
except:
pass
adc.update(is_accept=True, adc.update(is_accept=True,
accept_by=nickname or current_user.nickname, accept_by=nickname or current_user.nickname,
@ -727,75 +488,17 @@ class AutoDiscoveryCICRUD(DBMixin):
class AutoDiscoveryHTTPManager(object): class AutoDiscoveryHTTPManager(object):
@staticmethod @staticmethod
def get_categories(name): def get_categories(name):
categories = (CLOUD_MAP.get(name) or {}) or [] return (ClOUD_MAP.get(name) or {}).get('categories') or []
for item in copy.deepcopy(categories):
item.pop('map', None)
item.pop('collect_key_map', None)
return categories @staticmethod
def get_attributes(name, category):
def get_resources(self, name): tpt = ((ClOUD_MAP.get(name) or {}).get('map') or {}).get(category)
en_name = None if tpt and os.path.exists(os.path.join(PWD, tpt)):
for i in DEFAULT_INNER: with open(os.path.join(PWD, tpt)) as f:
if i['name'] == name: return json.loads(f.read())
en_name = i['en']
break
if en_name:
categories = self.get_categories(en_name)
return [j for i in categories for j in i['items']]
return [] return []
@staticmethod
def get_attributes(provider, resource):
for item in (CLOUD_MAP.get(provider) or {}):
for _resource in (item.get('map') or {}):
if _resource == resource:
tpt = item['map'][_resource]
if isinstance(tpt, dict):
tpt = tpt.get('template')
if tpt and os.path.exists(os.path.join(PWD, tpt)):
with open(os.path.join(PWD, tpt)) as f:
return json.loads(f.read())
return []
@staticmethod
def get_mapping(provider, resource):
for item in (CLOUD_MAP.get(provider) or {}):
for _resource in (item.get('map') or {}):
if _resource == resource:
mapping = item['map'][_resource]
if not isinstance(mapping, dict):
return {}
name = mapping.get('mapping')
mapping = AutoDiscoveryMappingCache.get(name)
if isinstance(mapping, dict):
return {mapping[key][provider]['key'].split('.')[0]: key for key in mapping if
(mapping[key].get(provider) or {}).get('key')}
return {}
@staticmethod
def get_predefined_value_mapping(provider, resource):
for item in (CLOUD_MAP.get(provider) or {}):
for _resource in (item.get('map') or {}):
if _resource == resource:
mapping = item['map'][_resource]
if not isinstance(mapping, dict):
return {}, {}
name = mapping.get('mapping')
mapping = AutoDiscoveryMappingCache.get(name)
if isinstance(mapping, dict):
return ({key: mapping[key][provider].get('map') for key in mapping if
mapping[key].get(provider, {}).get('map')},
{key: mapping[key][provider]['key'].split('.', 1)[1] for key in mapping if
((mapping[key].get(provider) or {}).get('key') or '').split('.')[1:]})
return {}, {}
class AutoDiscoverySNMPManager(object): class AutoDiscoverySNMPManager(object):
@ -806,191 +509,3 @@ class AutoDiscoverySNMPManager(object):
return json.loads(f.read()) return json.loads(f.read())
return [] return []
class AutoDiscoveryComponentsManager(object):
@staticmethod
def get_attributes(name):
if os.path.exists(os.path.join(PWD, "templates/{}.json".format(name))):
with open(os.path.join(PWD, "templates/{}.json".format(name))) as f:
return json.loads(f.read())
return []
class AutoDiscoveryRuleSyncHistoryCRUD(DBMixin):
cls = AutoDiscoveryRuleSyncHistory
def _can_add(self, **kwargs):
pass
def _can_update(self, **kwargs):
pass
def _can_delete(self, **kwargs):
pass
def upsert(self, **kwargs):
existed = self.cls.get_by(adt_id=kwargs.get('adt_id'),
oneagent_id=kwargs.get('oneagent_id'),
oneagent_name=kwargs.get('oneagent_name'),
first=True,
to_dict=False)
if existed is not None:
existed.update(**kwargs)
else:
self.cls.create(**kwargs)
class AutoDiscoveryExecHistoryCRUD(DBMixin):
cls = AutoDiscoveryExecHistory
def _can_add(self, **kwargs):
pass
def _can_update(self, **kwargs):
pass
def _can_delete(self, **kwargs):
pass
class AutoDiscoveryCounterCRUD(DBMixin):
cls = AutoDiscoveryCounter
def get(self, type_id):
res = self.cls.get_by(type_id=type_id, first=True, to_dict=True)
if res is None:
return dict(rule_count=0, exec_target_count=0, instance_count=0, accept_count=0,
this_month_count=0, this_week_count=0, last_month_count=0, last_week_count=0)
return res
def _can_add(self, **kwargs):
pass
def _can_update(self, **kwargs):
pass
def _can_delete(self, **kwargs):
pass
def encrypt_account(config):
if isinstance(config, dict):
if config.get('secret'):
config['secret'] = AESCrypto.encrypt(config['secret'])
if config.get('password'):
config['password'] = AESCrypto.encrypt(config['password'])
def decrypt_account(config, uid):
if isinstance(config, dict):
if config.get('password'):
if not (current_user.username in PRIVILEGED_USERS or current_user.uid == uid):
config.pop('password', None)
else:
try:
config['password'] = AESCrypto.decrypt(config['password'])
except Exception as e:
current_app.logger.error('decrypt account failed: {}'.format(e))
if config.get('secret'):
if not (current_user.username in PRIVILEGED_USERS or current_user.uid == uid):
config.pop('secret', None)
else:
try:
config['secret'] = AESCrypto.decrypt(config['secret'])
except Exception as e:
current_app.logger.error('decrypt account failed: {}'.format(e))
class AutoDiscoveryAccountCRUD(DBMixin):
cls = AutoDiscoveryAccount
def get(self, adr_id):
res = self.cls.get_by(adr_id=adr_id, to_dict=True)
for i in res:
decrypt_account(i.get('config'), i['uid'])
return res
def get_config_by_id(self, _id):
res = self.cls.get_by_id(_id)
if not res:
return {}
config = res.to_dict().get('config') or {}
decrypt_account(config, res.uid)
return config
def _can_add(self, **kwargs):
encrypt_account(kwargs.get('config'))
kwargs['uid'] = current_user.uid
return kwargs
def upsert(self, adr_id, accounts):
existed_all = self.cls.get_by(adr_id=adr_id, to_dict=False)
account_names = {i['name'] for i in accounts}
name_changed = dict()
for account in accounts:
existed = None
if account.get('id'):
existed = self.cls.get_by_id(account.get('id'))
if existed is None:
continue
account.pop('id')
name_changed[existed.name] = account.get('name')
else:
account = self._can_add(**account)
if existed is not None:
if current_user.uid == existed.uid:
config = copy.deepcopy(existed.config) or {}
config.update(account.get('config') or {})
account['config'] = config
existed.update(**account)
else:
self.cls.create(adr_id=adr_id, **account)
for item in existed_all:
if name_changed.get(item.name, item.name) not in account_names:
if current_user.uid == item.uid:
item.soft_delete()
def _can_update(self, **kwargs):
existed = self.cls.get_by_id(kwargs['_id']) or abort(404, ErrFormat.not_found)
if isinstance(kwargs.get('config'), dict) and kwargs['config'].get('secret'):
if current_user.uid != existed.uid:
return abort(403, ErrFormat.adt_secret_no_permission)
if isinstance(kwargs.get('config'), dict) and kwargs['config'].get('password'):
if current_user.uid != existed.uid:
return abort(403, ErrFormat.adt_secret_no_permission)
return existed
def update(self, _id, **kwargs):
if kwargs.get('is_plugin') and kwargs.get('plugin_script'):
kwargs = check_plugin_script(**kwargs)
encrypt_account(kwargs.get('config'))
inst = self._can_update(_id=_id, **kwargs)
obj = inst.update(_id=_id, filter_none=False, **kwargs)
return obj
def _can_delete(self, **kwargs):
pass

View File

@ -2,38 +2,15 @@
from api.lib.cmdb.const import AutoDiscoveryType from api.lib.cmdb.const import AutoDiscoveryType
PRIVILEGED_USERS = ("cmdb_agent", "worker", "admin") DEFAULT_HTTP = [
dict(name="阿里云", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False,
DEFAULT_INNER = [ option={'icon': {'name': 'caise-aliyun'}}),
dict(name="阿里云", en="aliyun", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False, dict(name="腾讯云", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-aliyun'}, "en": "aliyun"}), option={'icon': {'name': 'caise-tengxunyun'}}),
dict(name="腾讯云", en="tencentcloud", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False, dict(name="华为云", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-tengxunyun'}, "en": "tencentcloud"}), option={'icon': {'name': 'caise-huaweiyun'}}),
dict(name="华为云", en="huaweicloud", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False, dict(name="AWS", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-huaweiyun'}, "en": "huaweicloud"}), option={'icon': {'name': 'caise-aws'}}),
dict(name="AWS", en="aws", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-aws'}, "en": "aws"}),
dict(name="VCenter", en="vcenter", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False,
option={'icon': {'name': 'cmdb-vcenter'}, "category": "private_cloud", "en": "vcenter"}),
dict(name="KVM", en="kvm", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False,
option={'icon': {'name': 'ops-KVM'}, "category": "private_cloud", "en": "kvm"}),
dict(name="Nginx", en="nginx", type=AutoDiscoveryType.COMPONENTS, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-nginx'}, "en": "nginx", "collect_key": "nginx"}),
dict(name="Apache", en="apache", type=AutoDiscoveryType.COMPONENTS, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-apache'}, "en": "apache", "collect_key": "apache"}),
dict(name="Tomcat", en="tomcat", type=AutoDiscoveryType.COMPONENTS, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-tomcat'}, "en": "tomcat", "collect_key": "tomcat"}),
dict(name="MySQL", en="mysql", type=AutoDiscoveryType.COMPONENTS, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-mySQL'}, "en": "mysql", "collect_key": "mysql"}),
dict(name="MSSQL", en="mssql", type=AutoDiscoveryType.COMPONENTS, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-SQLServer'}, "en": "mssql", "collect_key": "sqlserver"}),
dict(name="Oracle", en="oracle", type=AutoDiscoveryType.COMPONENTS, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-oracle'}, "en": "oracle", "collect_key": "oracle"}),
dict(name="Redis", en="redis", type=AutoDiscoveryType.COMPONENTS, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-redis'}, "en": "redis", "collect_key": "redis"}),
dict(name="交换机", type=AutoDiscoveryType.SNMP, is_inner=True, is_plugin=False, dict(name="交换机", type=AutoDiscoveryType.SNMP, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-jiaohuanji'}}), option={'icon': {'name': 'caise-jiaohuanji'}}),
@ -45,307 +22,32 @@ DEFAULT_INNER = [
option={'icon': {'name': 'caise-dayinji'}}), option={'icon': {'name': 'caise-dayinji'}}),
] ]
CLOUD_MAP = { ClOUD_MAP = {
"aliyun": [ "aliyun": {
{ "categories": ["云服务器 ECS"],
"category": "计算", "map": {
"items": ["云服务器 ECS", "云服务器 Disk"], "云服务器 ECS": "templates/aliyun_ecs.json",
"map": { }
"云服务器 ECS": {"template": "templates/aliyun_ecs.json", "mapping": "ecs"}, },
"云服务器 Disk": {"template": "templates/aliyun_ecs_disk.json", "mapping": "evs"},
}, "tencentcloud": {
"collect_key_map": { "categories": ["云服务器 CVM"],
"云服务器 ECS": "ali.ecs", "map": {
"云服务器 Disk": "ali.ecs_disk", "云服务器 CVM": "templates/tencent_cvm.json",
}, }
}, },
{
"category": "网络与CDN", "huaweicloud": {
"items": [ "categories": ["云服务器 ECS"],
"内容分发CDN", "map": {
"负载均衡SLB", "云服务器 ECS": "templates/huaweicloud_ecs.json",
"专有网络VPC", }
"交换机Switch", },
],
"map": { "aws": {
"内容分发CDN": {"template": "templates/aliyun_cdn.json", "mapping": "CDN"}, "categories": ["云服务器 EC2"],
"负载均衡SLB": {"template": "templates/aliyun_slb.json", "mapping": "loadbalancer"}, "map": {
"专有网络VPC": {"template": "templates/aliyun_vpc.json", "mapping": "vpc"}, "云服务器 EC2": "templates/aws_ec2.json",
"交换机Switch": {"template": "templates/aliyun_switch.json", "mapping": "vswitch"}, }
}, },
"collect_key_map": {
"内容分发CDN": "ali.cdn",
"负载均衡SLB": "ali.slb",
"专有网络VPC": "ali.vpc",
"交换机Switch": "ali.switch",
},
},
{
"category": "存储",
"items": ["块存储EBS", "对象存储OSS"],
"map": {
"块存储EBS": {"template": "templates/aliyun_ebs.json", "mapping": "evs"},
"对象存储OSS": {"template": "templates/aliyun_oss.json", "mapping": "objectStorage"},
},
"collect_key_map": {
"块存储EBS": "ali.ebs",
"对象存储OSS": "ali.oss",
},
},
{
"category": "数据库",
"items": ["云数据库RDS MySQL", "云数据库RDS PostgreSQL", "云数据库 Redis"],
"map": {
"云数据库RDS MySQL": {"template": "templates/aliyun_rds_mysql.json", "mapping": "mysql"},
"云数据库RDS PostgreSQL": {"template": "templates/aliyun_rds_postgre.json", "mapping": "postgresql"},
"云数据库 Redis": {"template": "templates/aliyun_redis.json", "mapping": "redis"},
},
"collect_key_map": {
"云数据库RDS MySQL": "ali.rds_mysql",
"云数据库RDS PostgreSQL": "ali.rds_postgre",
"云数据库 Redis": "ali.redis",
},
},
],
"tencentcloud": [
{
"category": "计算",
"items": ["云服务器 CVM"],
"map": {
"云服务器 CVM": {"template": "templates/tencent_cvm.json", "mapping": "ecs"},
},
"collect_key_map": {
"云服务器 CVM": "tencent.cvm",
},
},
{
"category": "CDN与边缘",
"items": ["内容分发CDN"],
"map": {
"内容分发CDN": {"template": "templates/tencent_cdn.json", "mapping": "CDN"},
},
"collect_key_map": {
"内容分发CDN": "tencent.cdn",
},
},
{
"category": "网络",
"items": ["负载均衡CLB", "私有网络VPC", "子网"],
"map": {
"负载均衡CLB": {"template": "templates/tencent_clb.json", "mapping": "loadbalancer"},
"私有网络VPC": {"template": "templates/tencent_vpc.json", "mapping": "vpc"},
"子网": {"template": "templates/tencent_subnet.json", "mapping": "vswitch"},
},
"collect_key_map": {
"负载均衡CLB": "tencent.clb",
"私有网络VPC": "tencent.vpc",
"子网": "tencent.subnet",
},
},
{
"category": "存储",
"items": ["云硬盘CBS", "对象存储COS"],
"map": {
"云硬盘CBS": {"template": "templates/tencent_cbs.json", "mapping": "evs"},
"对象存储COS": {"template": "templates/tencent_cos.json", "mapping": "objectStorage"},
},
"collect_key_map": {
"云硬盘CBS": "tencent.cbs",
"对象存储COS": "tencent.cos",
},
},
{
"category": "数据库",
"items": ["云数据库 MySQL", "云数据库 PostgreSQL", "云数据库 Redis"],
"map": {
"云数据库 MySQL": {"template": "templates/tencent_rdb.json", "mapping": "mysql"},
"云数据库 PostgreSQL": {"template": "templates/tencent_postgres.json", "mapping": "postgresql"},
"云数据库 Redis": {"template": "templates/tencent_redis.json", "mapping": "redis"},
},
"collect_key_map": {
"云数据库 MySQL": "tencent.rdb",
"云数据库 PostgreSQL": "tencent.rds_postgres",
"云数据库 Redis": "tencent.redis",
},
},
],
"huaweicloud": [
{
"category": "计算",
"items": ["云服务器 ECS"],
"map": {
"云服务器 ECS": {"template": "templates/huaweicloud_ecs.json", "mapping": "ecs"},
},
"collect_key_map": {
"云服务器 ECS": "huawei.ecs",
},
},
{
"category": "CDN与智能边缘",
"items": ["内容分发网络CDN"],
"map": {
"内容分发网络CDN": {"template": "templates/huawei_cdn.json", "mapping": "CDN"},
},
"collect_key_map": {
"内容分发网络CDN": "huawei.cdn",
},
},
{
"category": "网络",
"items": ["弹性负载均衡ELB", "虚拟私有云VPC", "子网"],
"map": {
"弹性负载均衡ELB": {"template": "templates/huawei_elb.json", "mapping": "loadbalancer"},
"虚拟私有云VPC": {"template": "templates/huawei_vpc.json", "mapping": "vpc"},
"子网": {"template": "templates/huawei_subnet.json", "mapping": "vswitch"},
},
"collect_key_map": {
"弹性负载均衡ELB": "huawei.elb",
"虚拟私有云VPC": "huawei.vpc",
"子网": "huawei.subnet",
},
},
{
"category": "存储",
"items": ["云硬盘EVS", "对象存储OBS"],
"map": {
"云硬盘EVS": {"template": "templates/huawei_evs.json", "mapping": "evs"},
"对象存储OBS": {"template": "templates/huawei_obs.json", "mapping": "objectStorage"},
},
"collect_key_map": {
"云硬盘EVS": "huawei.evs",
"对象存储OBS": "huawei.obs",
},
},
{
"category": "数据库",
"items": ["云数据库RDS MySQL", "云数据库RDS PostgreSQL"],
"map": {
"云数据库RDS MySQL": {"template": "templates/huawei_rds_mysql.json", "mapping": "mysql"},
"云数据库RDS PostgreSQL": {"template": "templates/huawei_rds_postgre.json", "mapping": "postgresql"},
},
"collect_key_map": {
"云数据库RDS MySQL": "huawei.rds_mysql",
"云数据库RDS PostgreSQL": "huawei.rds_postgre",
},
},
{
"category": "应用中间件",
"items": ["分布式缓存Redis"],
"map": {
"分布式缓存Redis": {"template": "templates/huawei_dcs.json", "mapping": "redis"},
},
"collect_key_map": {
"分布式缓存Redis": "huawei.dcs",
},
},
],
"aws": [
{
"category": "计算",
"items": ["云服务器 EC2"],
"map": {
"云服务器 EC2": {"template": "templates/aws_ec2.json", "mapping": "ecs"},
},
"collect_key_map": {
"云服务器 EC2": "aws.ec2",
},
},
{"category": "网络与CDN", "items": [], "map": {}, "collect_key_map": {}},
],
"vcenter": [
{
"category": "计算",
"items": [
"主机",
"虚拟机",
"主机集群"
],
"map": {
"主机": "templates/vsphere_host.json",
"虚拟机": "templates/vsphere_vm.json",
"主机集群": "templates/vsphere_cluster.json",
},
"collect_key_map": {
"主机": "vsphere.host",
"虚拟机": "vsphere.vm",
"主机集群": "vsphere.cluster",
},
},
{
"category": "网络",
"items": [
"网络",
"标准交换机",
"分布式交换机",
],
"map": {
"网络": "templates/vsphere_network.json",
"标准交换机": "templates/vsphere_standard_switch.json",
"分布式交换机": "templates/vsphere_distributed_switch.json",
},
"collect_key_map": {
"网络": "vsphere.network",
"标准交换机": "vsphere.standard_switch",
"分布式交换机": "vsphere.distributed_switch",
},
},
{
"category": "存储",
"items": ["数据存储", "数据存储集群"],
"map": {
"数据存储": "templates/vsphere_datastore.json",
"数据存储集群": "templates/vsphere_storage_pod.json",
},
"collect_key_map": {
"数据存储": "vsphere.datastore",
"数据存储集群": "vsphere.storage_pod",
},
},
{
"category": "其他",
"items": ["资源池", "数据中心", "文件夹"],
"map": {
"资源池": "templates/vsphere_pool.json",
"数据中心": "templates/vsphere_datacenter.json",
"文件夹": "templates/vsphere_folder.json",
},
"collect_key_map": {
"资源池": "vsphere.pool",
"数据中心": "vsphere.datacenter",
"文件夹": "vsphere.folder",
},
},
],
"kvm": [
{
"category": "计算",
"items": ["虚拟机"],
"map": {
"虚拟机": "templates/kvm_vm.json",
},
"collect_key_map": {
"虚拟机": "kvm.vm",
},
},
{
"category": "存储",
"items": ["存储"],
"map": {
"存储": "templates/kvm_storage.json",
},
"collect_key_map": {
"存储": "kvm.storage",
},
},
{
"category": "network",
"items": ["网络"],
"map": {
"网络": "templates/kvm_network.json",
},
"collect_key_map": {
"网络": "kvm.network",
},
},
],
} }

View File

@ -1,277 +1,355 @@
[ [
{ {
"name": "amiLaunchIndex", "name": "amiLaunchIndex",
"type": "Integer", "type": "整数",
"desc": "The AMI launch index, which can be used to find this instance in the launch group.", "desc": "The AMI launch index, which can be used to find this instance in the launch group.",
"example": "" "example": "0"
}, },
{ {
"name": "architecture", "name": "architecture",
"type": "String", "type": "文本",
"desc": "The architecture of the image.", "desc": "The architecture of the image.",
"example": "i386" "example": "x86_64"
}, },
{ {
"name": "blockDeviceMapping", "name": "blockDeviceMapping",
"type": "Array of InstanceBlockDeviceMapping objects", "type": "json",
"desc": "Any block device mapping entries for the instance.", "desc": "Any block device mapping entries for the instance.",
"example": "" "example": {
"item": {
"deviceName": "/dev/xvda",
"ebs": {
"volumeId": "vol-1234567890abcdef0",
"status": "attached",
"attachTime": "2015-12-22T10:44:09.000Z",
"deleteOnTermination": "true"
}
}
}
}, },
{ {
"name": "bootMode", "name": "bootMode",
"type": "String", "type": "文本",
"desc": "The boot mode that was specified by the AMI. If the value is uefi-preferred, the AMI supports both UEFI and Legacy BIOS. The currentInstanceBootMode parameter is the boot mode that is used to boot the instance at launch or start. For more information, see Boot modes in the Amazon EC2 User Guide.", "desc": "The boot mode that was specified by the AMI. If the value is uefi-preferred, the AMI supports both UEFI and Legacy BIOS. The currentInstanceBootMode parameter is the boot mode that is used to boot the instance at launch or start.",
"example": "legacy-bios" "example": null
}, },
{ {
"name": "capacityReservationId", "name": "capacityReservationId",
"type": "String", "type": "文本",
"desc": "The ID of the Capacity Reservation.", "desc": "The ID of the Capacity Reservation.",
"example": "" "example": null
}, },
{ {
"name": "capacityReservationSpecification", "name": "capacityReservationSpecification",
"type": "CapacityReservationSpecificationResponse object", "type": "json",
"desc": "Information about the Capacity Reservation targeting option.", "desc": "Information about the Capacity Reservation targeting option.",
"example": "" "example": null
}, },
{ {
"name": "clientToken", "name": "clientToken",
"type": "String", "type": "文本",
"desc": "The idempotency token you provided when you launched the instance, if applicable.", "desc": "The idempotency token you provided when you launched the instance, if applicable.",
"example": "" "example": "xMcwG14507example"
}, },
{ {
"name": "cpuOptions", "name": "cpuOptions",
"type": "CpuOptions object", "type": "json",
"desc": "The CPU options for the instance.", "desc": "The CPU options for the instance.",
"example": "" "example": {
"coreCount": "1",
"threadsPerCore": "1"
}
}, },
{ {
"name": "currentInstanceBootMode", "name": "currentInstanceBootMode",
"type": "String", "type": "文本",
"desc": "The boot mode that is used to boot the instance at launch or start. For more information, see Boot modes in the Amazon EC2 User Guide.", "desc": "The boot mode that is used to boot the instance at launch or start. For more information, see Boot modes in the Amazon EC2 User Guide.",
"example": "legacy-bios" "example": null
}, },
{ {
"name": "dnsName", "name": "dnsName",
"type": "String", "type": "文本",
"desc": "[IPv4 only] The public DNS name assigned to the instance. This name is not available until the instance enters the running state. This name is only available if you've enabled DNS hostnames for your VPC.", "desc": "[IPv4 only] The public DNS name assigned to the instance. This name is not available until the instance enters the running state. This name is only available if you've enabled DNS hostnames for your VPC.",
"example": "" "example": "ec2-54-194-252-215.eu-west-1.compute.amazonaws.com"
}, },
{ {
"name": "ebsOptimized", "name": "ebsOptimized",
"type": "Boolean", "type": "Boolean",
"desc": "Indicates whether the instance is optimized for Amazon EBS I/O. This optimization provides dedicated throughput to Amazon EBS and an optimized configuration stack to provide optimal I/O performance. This optimization isn't available with all instance types. Additional usage charges apply when using an EBS Optimized instance.", "desc": "Indicates whether the instance is optimized for Amazon EBS I/O. This optimization provides dedicated throughput to Amazon EBS and an optimized configuration stack to provide optimal I/O performance. This optimization isn't available with all instance types. Additional usage charges apply when using an EBS Optimized instance.",
"example": "" "example": "false"
}, },
{ {
"name": "elasticGpuAssociationSet", "name": "elasticGpuAssociationSet",
"type": "Array of ElasticGpuAssociation objects", "type": "json",
"desc": "The Elastic GPU associated with the instance.", "desc": "The Elastic GPU associated with the instance.",
"example": "" "example": null
}, },
{ {
"name": "elasticInferenceAcceleratorAssociationSet", "name": "elasticInferenceAcceleratorAssociationSet",
"type": "Array of ElasticInferenceAcceleratorAssociation objects", "type": "json",
"desc": "The elastic inference accelerator associated with the instance.", "desc": "The elastic inference accelerator associated with the instance.",
"example": "" "example": null
}, },
{ {
"name": "enaSupport", "name": "enaSupport",
"type": "Boolean", "type": "Boolean",
"desc": "Specifies whether enhanced networking with ENA is enabled.", "desc": "Specifies whether enhanced networking with ENA is enabled.",
"example": "" "example": null
}, },
{ {
"name": "enclaveOptions", "name": "enclaveOptions",
"type": "EnclaveOptions object", "type": "json",
"desc": "Indicates whether the instance is enabled for AWS Nitro Enclaves.", "desc": "Indicates whether the instance is enabled for AWS Nitro Enclaves.",
"example": "" "example": null
}, },
{ {
"name": "groupSet", "name": "groupSet",
"type": "Array of GroupIdentifier objects", "type": "json",
"desc": "The security groups for the instance.", "desc": "The security groups for the instance.",
"example": "" "example": {
"item": {
"groupId": "sg-e4076980",
"groupName": "SecurityGroup1"
}
}
}, },
{ {
"name": "hibernationOptions", "name": "hibernationOptions",
"type": "HibernationOptions object", "type": "json",
"desc": "Indicates whether the instance is enabled for hibernation.", "desc": "Indicates whether the instance is enabled for hibernation.",
"example": "" "example": null
}, },
{ {
"name": "hypervisor", "name": "hypervisor",
"type": "String", "type": "文本",
"desc": "The hypervisor type of the instance. The value xen is used for both Xen and Nitro hypervisors.", "desc": "The hypervisor type of the instance. The value xen is used for both Xen and Nitro hypervisors.",
"example": "ovm" "example": "xen"
}, },
{ {
"name": "iamInstanceProfile", "name": "iamInstanceProfile",
"type": "IamInstanceProfile object", "type": "json",
"desc": "The IAM instance profile associated with the instance, if applicable.", "desc": "The IAM instance profile associated with the instance, if applicable.",
"example": "" "example": {
"arn": "arn:aws:iam::123456789012:instance-profile/AdminRole",
"id": "ABCAJEDNCAA64SSD123AB"
}
}, },
{ {
"name": "imageId", "name": "imageId",
"type": "String", "type": "文本",
"desc": "The ID of the AMI used to launch the instance.", "desc": "The ID of the AMI used to launch the instance.",
"example": "" "example": "ami-bff32ccc"
}, },
{ {
"name": "instanceId", "name": "instanceId",
"type": "String", "type": "文本",
"desc": "The ID of the instance.", "desc": "The ID of the instance.",
"example": "" "example": "i-1234567890abcdef0"
}, },
{ {
"name": "instanceLifecycle", "name": "instanceLifecycle",
"type": "String", "type": "文本",
"desc": "Indicates whether this is a Spot Instance or a Scheduled Instance.", "desc": "Indicates whether this is a Spot Instance or a Scheduled Instance.",
"example": "spot" "example": null
}, },
{ {
"name": "instanceState", "name": "instanceState",
"type": "InstanceState object", "type": "json",
"desc": "The current state of the instance.", "desc": "The current state of the instance.",
"example": "" "example": {
"code": "16",
"name": "running"
}
}, },
{ {
"name": "instanceType", "name": "instanceType",
"type": "String", "type": "文本",
"desc": "The instance type.", "desc": "The instance type.",
"example": "a1.medium" "example": "t2.micro"
}, },
{ {
"name": "ipAddress", "name": "ipAddress",
"type": "String", "type": "文本",
"desc": "The public IPv4 address, or the Carrier IP address assigned to the instance, if applicable. A Carrier IP address only applies to an instance launched in a subnet associated with a Wavelength Zone.", "desc": "The public IPv4 address, or the Carrier IP address assigned to the instance, if applicable.",
"example": "Required: No" "example": "54.194.252.215"
}, },
{ {
"name": "ipv6Address", "name": "ipv6Address",
"type": "String", "type": "文本",
"desc": "The IPv6 address assigned to the instance.", "desc": "The IPv6 address assigned to the instance.",
"example": "" "example": null
}, },
{ {
"name": "kernelId", "name": "kernelId",
"type": "String", "type": "文本",
"desc": "The kernel associated with this instance, if applicable.", "desc": "The kernel associated with this instance, if applicable.",
"example": "" "example": null
}, },
{ {
"name": "keyName", "name": "keyName",
"type": "String", "type": "文本",
"desc": "The name of the key pair, if this instance was launched with an associated key pair.", "desc": "The name of the key pair, if this instance was launched with an associated key pair.",
"example": "" "example": "my_keypair"
}, },
{ {
"name": "launchTime", "name": "launchTime",
"type": "Timestamp", "type": "Time",
"desc": "The time the instance was launched.", "desc": "The time the instance was launched.",
"example": "" "example": "2018-05-08T16:46:19.000Z"
}, },
{ {
"name": "licenseSet", "name": "licenseSet",
"type": "Array of LicenseConfiguration objects", "type": "json",
"desc": "The license configurations for the instance.", "desc": "The license configurations for the instance.",
"example": "" "example": null
}, },
{ {
"name": "maintenanceOptions", "name": "maintenanceOptions",
"type": "InstanceMaintenanceOptions object", "type": "json",
"desc": "Provides information on the recovery and maintenance options of your instance.", "desc": "Provides information on the recovery and maintenance options of your instance.",
"example": "" "example": null
}, },
{ {
"name": "metadataOptions", "name": "metadataOptions",
"type": "InstanceMetadataOptionsResponse object", "type": "json",
"desc": "The metadata options for the instance.", "desc": "The metadata options for the instance.",
"example": "" "example": null
}, },
{ {
"name": "monitoring", "name": "monitoring",
"type": "Monitoring object", "type": "json",
"desc": "The monitoring for the instance.", "desc": "The monitoring for the instance.",
"example": "" "example": {
"state": "disabled"
}
}, },
{ {
"name": "networkInterfaceSet", "name": "networkInterfaceSet",
"type": "Array of InstanceNetworkInterface objects", "type": "json",
"desc": "The network interfaces for the instance.", "desc": "The network interfaces for the instance.",
"example": "" "example": {
"item": {
"networkInterfaceId": "eni-551ba033",
"subnetId": "subnet-56f5f633",
"vpcId": "vpc-11112222",
"description": "Primary network interface",
"ownerId": "123456789012",
"status": "in-use",
"macAddress": "02:dd:2c:5e:01:69",
"privateIpAddress": "192.168.1.88",
"privateDnsName": "ip-192-168-1-88.eu-west-1.compute.internal",
"sourceDestCheck": "true",
"groupSet": {
"item": {
"groupId": "sg-e4076980",
"groupName": "SecurityGroup1"
}
},
"attachment": {
"attachmentId": "eni-attach-39697adc",
"deviceIndex": "0",
"status": "attached",
"attachTime": "2018-05-08T16:46:19.000Z",
"deleteOnTermination": "true"
},
"association": {
"publicIp": "54.194.252.215",
"publicDnsName": "ec2-54-194-252-215.eu-west-1.compute.amazonaws.com",
"ipOwnerId": "amazon"
},
"privateIpAddressesSet": {
"item": {
"privateIpAddress": "192.168.1.88",
"privateDnsName": "ip-192-168-1-88.eu-west-1.compute.internal",
"primary": "true",
"association": {
"publicIp": "54.194.252.215",
"publicDnsName": "ec2-54-194-252-215.eu-west-1.compute.amazonaws.com",
"ipOwnerId": "amazon"
}
}
},
"ipv6AddressesSet": {
"item": {
"ipv6Address": "2001:db8:1234:1a2b::123"
}
}
}
}
}, },
{ {
"name": "outpostArn", "name": "outpostArn",
"type": "String", "type": "文本",
"desc": "The Amazon Resource Name (ARN) of the Outpost.", "desc": "The Amazon Resource Name (ARN) of the Outpost.",
"example": "" "example": null
}, },
{ {
"name": "placement", "name": "placement",
"type": "Placement object", "type": "json",
"desc": "The location where the instance launched, if applicable.", "desc": "The location where the instance launched, if applicable.",
"example": "" "example": {
"availabilityZone": "eu-west-1c",
"groupName": null,
"tenancy": "default"
}
}, },
{ {
"name": "platform", "name": "platform",
"type": "String", "type": "文本",
"desc": "The platform. This value is windows for Windows instances; otherwise, it is empty.", "desc": "The value is Windows for Windows instances; otherwise blank.",
"example": "windows" "example": null
}, },
{ {
"name": "platformDetails", "name": "platformDetails",
"type": "String", "type": "文本",
"desc": "The platform details value for the instance. For more information, see AMI billing information fields in the Amazon EC2 User Guide.", "desc": "The platform details value for the instance. For more information, see AMI billing information fields in the Amazon EC2 User Guide.",
"example": "" "example": null
}, },
{ {
"name": "privateDnsName", "name": "privateDnsName",
"type": "String", "type": "文本",
"desc": "[IPv4 only] The private DNS hostname name assigned to the instance. This DNS hostname can only be used inside the Amazon EC2 network. This name is not available until the instance enters the running state. The Amazon-provided DNS server resolves Amazon-provided private DNS hostnames if you've enabled DNS resolution and DNS hostnames in your VPC. If you are not using the Amazon-provided DNS server in your VPC, your custom domain name servers must resolve the hostname as appropriate.", "desc": "[IPv4 only] The private DNS hostname name assigned to the instance. This DNS hostname can only be used inside the Amazon EC2 network. This name is not available until the instance enters the running state.",
"example": "Required: No" "example": "ip-192-168-1-88.eu-west-1.compute.internal"
}, },
{ {
"name": "privateDnsNameOptions", "name": "privateDnsNameOptions",
"type": "PrivateDnsNameOptionsResponse object", "type": "json",
"desc": "The options for the instance hostname.", "desc": "The options for the instance hostname.",
"example": "" "example": null
}, },
{ {
"name": "privateIpAddress", "name": "privateIpAddress",
"type": "String", "type": "文本",
"desc": "The private IPv4 address assigned to the instance.", "desc": "The private IPv4 address assigned to the instance.",
"example": "" "example": "192.168.1.88"
}, },
{ {
"name": "productCodes", "name": "productCodes",
"type": "Array of ProductCode objects", "type": "json",
"desc": "The product codes attached to this instance, if applicable.", "desc": "The product codes attached to this instance, if applicable.",
"example": "" "example": null
}, },
{ {
"name": "ramdiskId", "name": "ramdiskId",
"type": "String", "type": "文本",
"desc": "The RAM disk associated with this instance, if applicable.", "desc": "The RAM disk associated with this instance, if applicable.",
"example": "" "example": null
}, },
{ {
"name": "reason", "name": "reason",
"type": "String", "type": "文本",
"desc": "The reason for the most recent state transition. This might be an empty string.", "desc": "The reason for the most recent state transition. This might be an empty string.",
"example": "" "example": null
}, },
{ {
"name": "rootDeviceName", "name": "rootDeviceName",
"type": "String", "type": "文本",
"desc": "The device name of the root device volume (for example, /dev/sda1).", "desc": "The device name of the root device volume (for example, /dev/sda1).",
"example": "" "example": "/dev/xvda"
}, },
{ {
"name": "rootDeviceType", "name": "rootDeviceType",
"type": "String", "type": "文本",
"desc": "The root device type used by the AMI. The AMI can use an EBS volume or an instance store volume.", "desc": "The root device type used by the AMI. The AMI can use an EBS volume or an instance store volume.",
"example": "ebs" "example": "ebs"
}, },
@ -279,66 +357,71 @@
"name": "sourceDestCheck", "name": "sourceDestCheck",
"type": "Boolean", "type": "Boolean",
"desc": "Indicates whether source/destination checking is enabled.", "desc": "Indicates whether source/destination checking is enabled.",
"example": "" "example": "true"
}, },
{ {
"name": "spotInstanceRequestId", "name": "spotInstanceRequestId",
"type": "String", "type": "文本",
"desc": "If the request is a Spot Instance request, the ID of the request.", "desc": "If the request is a Spot Instance request, the ID of the request.",
"example": "" "example": null
}, },
{ {
"name": "sriovNetSupport", "name": "sriovNetSupport",
"type": "String", "type": "文本",
"desc": "Specifies whether enhanced networking with the Intel 82599 Virtual Function interface is enabled.", "desc": "Specifies whether enhanced networking with the Intel 82599 Virtual Function interface is enabled.",
"example": "" "example": null
}, },
{ {
"name": "stateReason", "name": "stateReason",
"type": "StateReason object", "type": "json",
"desc": "The reason for the most recent state transition.", "desc": "The reason for the most recent state transition.",
"example": "" "example": null
}, },
{ {
"name": "subnetId", "name": "subnetId",
"type": "String", "type": "文本",
"desc": "The ID of the subnet in which the instance is running.", "desc": "The ID of the subnet in which the instance is running.",
"example": "" "example": "subnet-56f5f633"
}, },
{ {
"name": "tagSet", "name": "tagSet",
"type": "Array of Tag objects", "type": "json",
"desc": "Any tags assigned to the instance.", "desc": "Any tags assigned to the instance.",
"example": "" "example": {
"item": {
"key": "Name",
"value": "Server_1"
}
}
}, },
{ {
"name": "tpmSupport", "name": "tpmSupport",
"type": "String", "type": "文本",
"desc": "If the instance is configured for NitroTPM support, the value is v2.0. For more information, see NitroTPM in the Amazon EC2 User Guide.", "desc": "If the instance is configured for NitroTPM support, the value is v2.0. For more information, see NitroTPM in the Amazon EC2 User Guide.",
"example": "" "example": null
}, },
{ {
"name": "usageOperation", "name": "usageOperation",
"type": "String", "type": "文本",
"desc": "The usage operation value for the instance. For more information, see AMI billing information fields in the Amazon EC2 User Guide.", "desc": "The usage operation value for the instance. For more information, see AMI billing information fields in the Amazon EC2 User Guide.",
"example": "" "example": null
}, },
{ {
"name": "usageOperationUpdateTime", "name": "usageOperationUpdateTime",
"type": "Timestamp", "type": "Time",
"desc": "The time that the usage operation was last updated.", "desc": "The time that the usage operation was last updated.",
"example": "" "example": null
}, },
{ {
"name": "virtualizationType", "name": "virtualizationType",
"type": "String", "type": "文本",
"desc": "The virtualization type of the instance.", "desc": "The virtualization type of the instance.",
"example": "hvm" "example": "hvm"
}, },
{ {
"name": "vpcId", "name": "vpcId",
"type": "String", "type": "文本",
"desc": "The ID of the VPC in which the instance is running.", "desc": "The ID of the VPC in which the instance is running.",
"example": "" "example": "vpc-11112222"
} }
] ]

View File

@ -1,284 +1,292 @@
[ [
{ {
"name": "status", "name": "status",
"type": "string", "type": "文本",
"desc": "弹性云服务器状态。\n\n取值范围:\n\nACTIVE、BUILD、DELETED、ERROR、HARD_REBOOT、MIGRATING、PAUSED、REBOOT、REBUILD、RESIZE、REVERT_RESIZE、SHUTOFF、SHELVED、SHELVED_OFFLOADED、SOFT_DELETED、SUSPENDED、VERIFY_RESIZE\n\n弹性云服务器状态说明请参考[云服务器状态](https://support.huaweicloud.com/api-ecs/ecs_08_0002.html)", "example": "ACTIVE",
"example": "ACTIVE" "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u72b6\u6001\u3002\n\n\u53d6\u503c\u8303\u56f4:\n\nACTIVE\u3001BUILD\u3001DELETED\u3001ERROR\u3001HARD_REBOOT\u3001MIGRATING\u3001PAUSED\u3001REBOOT\u3001REBUILD\u3001RESIZE\u3001REVERT_RESIZE\u3001SHUTOFF\u3001SHELVED\u3001SHELVED_OFFLOADED\u3001SOFT_DELETED\u3001SUSPENDED\u3001VERIFY_RESIZE\n\n\u5f39\u6027\u4e91\u670d\u52a1\u5668\u72b6\u6001\u8bf4\u660e\u8bf7\u53c2\u8003[\u4e91\u670d\u52a1\u5668\u72b6\u6001](https://support.huaweicloud.com/api-ecs/ecs_08_0002.html)"
},
{
"name": "updated",
"type": "文本",
"example": "2019-05-22T03:30:52Z",
"desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u66f4\u65b0\u65f6\u95f4\u3002\n\n\u65f6\u95f4\u683c\u5f0f\u4f8b\u5982:2019-05-22T03:30:52Z"
},
{
"name": "auto_terminate_time",
"type": "文本",
"example": "2020-01-19T03:30:52Z",
"desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u81ea\u52a8\u91ca\u653e\u65f6\u95f4\u3002\n\n\u65f6\u95f4\u683c\u5f0f\u4f8b\u5982:2020-01-19T03:30:52Z"
},
{
"name": "hostId",
"type": "文本",
"example": "c7145889b2e3202cd295ceddb1742ff8941b827b586861fd0acedf64",
"desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u6240\u5728\u4e3b\u673a\u7684\u4e3b\u673aID\u3002"
},
{
"name": "OS-EXT-SRV-ATTR:host",
"type": "文本",
"example": "pod01.cn-north-1c",
"desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u6240\u5728\u4e3b\u673a\u7684\u4e3b\u673a\u540d\u79f0\u3002"
},
{
"name": "addresses",
"type": "json",
"example": null,
"desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u7684\u7f51\u7edc\u5c5e\u6027\u3002"
},
{
"name": "key_name",
"type": "文本",
"example": "KeyPair-test",
"desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u4f7f\u7528\u7684\u5bc6\u94a5\u5bf9\u540d\u79f0\u3002"
},
{
"name": "image",
"type": "json",
"example": null,
"desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u955c\u50cf\u4fe1\u606f\u3002"
},
{
"name": "OS-EXT-STS:task_state",
"type": "文本",
"example": "rebooting",
"desc": "\u6269\u5c55\u5c5e\u6027,\u5f39\u6027\u4e91\u670d\u52a1\u5668\u5f53\u524d\u4efb\u52a1\u7684\u72b6\u6001\u3002\n\n\u53d6\u503c\u8303\u56f4\u8bf7\u53c2\u8003[\u4e91\u670d\u52a1\u5668\u72b6\u6001](https://support.huaweicloud.com/api-ecs/ecs_08_0002.html)\u88683\u3002"
},
{
"name": "OS-EXT-STS:vm_state",
"type": "文本",
"example": "active",
"desc": "\u6269\u5c55\u5c5e\u6027,\u5f39\u6027\u4e91\u670d\u52a1\u5668\u5f53\u524d\u72b6\u6001\u3002\n\n\u4e91\u670d\u52a1\u5668\u72b6\u6001\u8bf4\u660e\u8bf7\u53c2\u8003[\u4e91\u670d\u52a1\u5668\u72b6\u6001](https://support.huaweicloud.com/api-ecs/ecs_08_0002.html)\u3002"
},
{
"name": "OS-EXT-SRV-ATTR:instance_name",
"type": "文本",
"example": "instance-0048a91b",
"desc": "\u6269\u5c55\u5c5e\u6027,\u5f39\u6027\u4e91\u670d\u52a1\u5668\u522b\u540d\u3002"
},
{
"name": "OS-EXT-SRV-ATTR:hypervisor_hostname",
"type": "文本",
"example": "nova022@36",
"desc": "\u6269\u5c55\u5c5e\u6027,\u5f39\u6027\u4e91\u670d\u52a1\u5668\u6240\u5728\u865a\u62df\u5316\u4e3b\u673a\u540d\u3002"
},
{
"name": "flavor",
"type": "json",
"example": null,
"desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u89c4\u683c\u4fe1\u606f\u3002"
},
{
"name": "id",
"type": "文本",
"example": "4f4b3dfa-eb70-47cf-a60a-998a53bd6666",
"desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668ID,\u683c\u5f0f\u4e3aUUID\u3002"
},
{
"name": "security_groups",
"type": "json",
"example": {
"$ref": "#/definitions/ServerSecurityGroup"
}, },
{ "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u6240\u5c5e\u5b89\u5168\u7ec4\u5217\u8868\u3002"
"name": "updated", },
"type": "string", {
"desc": "弹性云服务器更新时间。\n\n时间格式例如:2019-05-22T03:30:52Z", "name": "OS-EXT-AZ:availability_zone",
"example": "2019-05-22T03:30:52Z" "type": "文本",
"example": "cn-north-1c",
"desc": "\u6269\u5c55\u5c5e\u6027,\u5f39\u6027\u4e91\u670d\u52a1\u5668\u6240\u5728\u53ef\u7528\u533a\u540d\u79f0\u3002"
},
{
"name": "user_id",
"type": "文本",
"example": "05498fe56b8010d41f7fc01e280b6666",
"desc": "\u521b\u5efa\u5f39\u6027\u4e91\u670d\u52a1\u5668\u7684\u7528\u6237ID,\u683c\u5f0f\u4e3aUUID\u3002"
},
{
"name": "name",
"type": "文本",
"example": "ecs-test-server",
"desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u540d\u79f0\u3002"
},
{
"name": "created",
"type": "文本",
"example": "2017-07-15T11:30:52Z",
"desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u521b\u5efa\u65f6\u95f4\u3002\n\n\u65f6\u95f4\u683c\u5f0f\u4f8b\u5982:2019-05-22T03:19:19Z"
},
{
"name": "tenant_id",
"type": "文本",
"example": "743b4c0428d94531b9f2add666646666",
"desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u6240\u5c5e\u79df\u6237ID,\u5373\u9879\u76eeid,\u548cproject_id\u8868\u793a\u76f8\u540c\u7684\u6982\u5ff5,\u683c\u5f0f\u4e3aUUID\u3002"
},
{
"name": "OS-DCF:diskConfig",
"type": "文本",
"example": "AUTO",
"desc": "\u6269\u5c55\u5c5e\u6027, diskConfig\u7684\u7c7b\u578b\u3002\n\n- MANUAL,\u955c\u50cf\u7a7a\u95f4\u4e0d\u4f1a\u6269\u5c55\u3002\n- AUTO,\u7cfb\u7edf\u76d8\u955c\u50cf\u7a7a\u95f4\u4f1a\u81ea\u52a8\u6269\u5c55\u4e3a\u4e0eflavor\u5927\u5c0f\u4e00\u81f4\u3002"
},
{
"name": "accessIPv4",
"type": "文本",
"example": null,
"desc": "\u9884\u7559\u5c5e\u6027\u3002"
},
{
"name": "accessIPv6",
"type": "文本",
"example": null,
"desc": "\u9884\u7559\u5c5e\u6027\u3002"
},
{
"name": "fault",
"type": "文本",
"example": null,
"desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u6545\u969c\u4fe1\u606f\u3002\n\n\u53ef\u9009\u53c2\u6570,\u5728\u5f39\u6027\u4e91\u670d\u52a1\u5668\u72b6\u6001\u4e3aERROR\u4e14\u5b58\u5728\u5f02\u5e38\u7684\u60c5\u51b5\u4e0b\u8fd4\u56de\u3002"
},
{
"name": "progress",
"type": "整数",
"example": null,
"desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u8fdb\u5ea6\u3002"
},
{
"name": "OS-EXT-STS:power_state",
"type": "整数",
"example": 4,
"desc": "\u6269\u5c55\u5c5e\u6027,\u5f39\u6027\u4e91\u670d\u52a1\u5668\u7535\u6e90\u72b6\u6001\u3002"
},
{
"name": "config_drive",
"type": "文本",
"example": null,
"desc": "config drive\u4fe1\u606f\u3002"
},
{
"name": "metadata",
"type": "json",
"example": null,
"desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u5143\u6570\u636e\u3002\n\n> \u8bf4\u660e:\n> \n> \u5143\u6570\u636e\u5305\u542b\u7cfb\u7edf\u9ed8\u8ba4\u6dfb\u52a0\u5b57\u6bb5\u548c\u7528\u6237\u8bbe\u7f6e\u7684\u5b57\u6bb5\u3002\n\n\u7cfb\u7edf\u9ed8\u8ba4\u6dfb\u52a0\u5b57\u6bb5\n\n1. charging_mode\n\u4e91\u670d\u52a1\u5668\u7684\u8ba1\u8d39\u7c7b\u578b\u3002\n\n- \u201c0\u201d:\u6309\u9700\u8ba1\u8d39(\u5373postPaid-\u540e\u4ed8\u8d39\u65b9\u5f0f)\u3002\n- \u201c1\u201d:\u6309\u5305\u5e74\u5305\u6708\u8ba1\u8d39(\u5373prePaid-\u9884\u4ed8\u8d39\u65b9\u5f0f)\u3002\"2\":\u7ade\u4ef7\u5b9e\u4f8b\u8ba1\u8d39\n\n2. metering.order_id\n\u6309\u201c\u5305\u5e74/\u5305\u6708\u201d\u8ba1\u8d39\u7684\u4e91\u670d\u52a1\u5668\u5bf9\u5e94\u7684\u8ba2\u5355ID\u3002\n\n3. metering.product_id\n\u6309\u201c\u5305\u5e74/\u5305\u6708\u201d\u8ba1\u8d39\u7684\u4e91\u670d\u52a1\u5668\u5bf9\u5e94\u7684\u4ea7\u54c1ID\u3002\n\n4. vpc_id\n\u4e91\u670d\u52a1\u5668\u6240\u5c5e\u7684\u865a\u62df\u79c1\u6709\u4e91ID\u3002\n\n5. EcmResStatus\n\u4e91\u670d\u52a1\u5668\u7684\u51bb\u7ed3\u72b6\u6001\u3002\n\n- normal:\u4e91\u670d\u52a1\u5668\u6b63\u5e38\u72b6\u6001(\u672a\u88ab\u51bb\u7ed3)\u3002\n- freeze:\u4e91\u670d\u52a1\u5668\u88ab\u51bb\u7ed3\u3002\n\n> \u5f53\u4e91\u670d\u52a1\u5668\u88ab\u51bb\u7ed3\u6216\u8005\u89e3\u51bb\u540e,\u7cfb\u7edf\u9ed8\u8ba4\u6dfb\u52a0\u8be5\u5b57\u6bb5,\u4e14\u8be5\u5b57\u6bb5\u5fc5\u9009\u3002\n\n6. metering.image_id\n\u4e91\u670d\u52a1\u5668\u64cd\u4f5c\u7cfb\u7edf\u5bf9\u5e94\u7684\u955c\u50cfID\n\n7. metering.imagetype\n\u955c\u50cf\u7c7b\u578b,\u76ee\u524d\u652f\u6301:\n\n- \u516c\u5171\u955c\u50cf(gold)\n- \u79c1\u6709\u955c\u50cf(private)\n- \u5171\u4eab\u955c\u50cf(shared)\n\n8. metering.resourcespeccode\n\u4e91\u670d\u52a1\u5668\u5bf9\u5e94\u7684\u8d44\u6e90\u89c4\u683c\u3002\n\n9. image_name\n\u4e91\u670d\u52a1\u5668\u64cd\u4f5c\u7cfb\u7edf\u5bf9\u5e94\u7684\u955c\u50cf\u540d\u79f0\u3002\n\n10. os_bit\n\u64cd\u4f5c\u7cfb\u7edf\u4f4d\u6570,\u4e00\u822c\u53d6\u503c\u4e3a\u201c32\u201d\u6216\u8005\u201c64\u201d\u3002\n\n11. lockCheckEndpoint\n\u56de\u8c03URL,\u7528\u4e8e\u68c0\u67e5\u5f39\u6027\u4e91\u670d\u52a1\u5668\u7684\u52a0\u9501\u662f\u5426\u6709\u6548\u3002\n\n- \u5982\u679c\u6709\u6548,\u5219\u4e91\u670d\u52a1\u5668\u4fdd\u6301\u9501\u5b9a\u72b6\u6001\u3002\n- \u5982\u679c\u65e0\u6548,\u89e3\u9664\u9501\u5b9a\u72b6\u6001,\u5220\u9664\u5931\u6548\u7684\u9501\u3002\n\n12. lockSource\n\u5f39\u6027\u4e91\u670d\u52a1\u5668\u6765\u81ea\u54ea\u4e2a\u670d\u52a1\u3002\u8ba2\u5355\u52a0\u9501(ORDER)\n\n13. lockSourceId\n\u5f39\u6027\u4e91\u670d\u52a1\u5668\u7684\u52a0\u9501\u6765\u81ea\u54ea\u4e2aID\u3002lockSource\u4e3a\u201cORDER\u201d\u65f6,lockSourceId\u4e3a\u8ba2\u5355ID\u3002\n\n14. lockScene\n\u5f39\u6027\u4e91\u670d\u52a1\u5668\u7684\u52a0\u9501\u7c7b\u578b\u3002\n\n- \u6309\u9700\u8f6c\u5305\u5468\u671f(TO_PERIOD_LOCK)\n\n15. virtual_env_type\n\n- IOS\u955c\u50cf\u521b\u5efa\u865a\u62df\u673a,\"virtual_env_type\": \"IsoImage\" \u5c5e\u6027;\n- \u975eIOS\u955c\u50cf\u521b\u5efa\u865a\u62df\u673a,\u572819.5.0\u7248\u672c\u4ee5\u540e\u521b\u5efa\u7684\u865a\u62df\u673a\u5c06\u4e0d\u4f1a\u6dfb\u52a0virtual_env_type \u5c5e\u6027,\u800c\u5728\u6b64\u4e4b\u524d\u7684\u7248\u672c\u521b\u5efa\u7684\u865a\u62df\u673a\u53ef\u80fd\u4f1a\u8fd4\u56de\"virtual_env_type\": \"FusionCompute\"\u5c5e\u6027 \u3002\n\n> virtual_env_type\u5c5e\u6027\u4e0d\u5141\u8bb8\u7528\u6237\u589e\u52a0\u3001\u5220\u9664\u548c\u4fee\u6539\u3002\n\n16. metering.resourcetype\n\u4e91\u670d\u52a1\u5668\u5bf9\u5e94\u7684\u8d44\u6e90\u7c7b\u578b\u3002\n\n17. os_type\n\u64cd\u4f5c\u7cfb\u7edf\u7c7b\u578b,\u53d6\u503c\u4e3a:Linux\u3001Windows\u3002\n\n18. cascaded.instance_extrainfo\n\u7cfb\u7edf\u5185\u90e8\u865a\u62df\u673a\u6269\u5c55\u4fe1\u606f\u3002\n\n19. __support_agent_list\n\u4e91\u670d\u52a1\u5668\u662f\u5426\u652f\u6301\u4f01\u4e1a\u4e3b\u673a\u5b89\u5168\u3001\u4e3b\u673a\u76d1\u63a7\u3002\n\n- \u201chss\u201d:\u4f01\u4e1a\u4e3b\u673a\u5b89\u5168\n- \u201cces\u201d:\u4e3b\u673a\u76d1\u63a7\n\n20. agency_name\n\u59d4\u6258\u7684\u540d\u79f0\u3002\n\n\u59d4\u6258\u662f\u7531\u79df\u6237\u7ba1\u7406\u5458\u5728\u7edf\u4e00\u8eab\u4efd\u8ba4\u8bc1\u670d\u52a1(Identity and Access Management,IAM)\u4e0a\u521b\u5efa\u7684,\u53ef\u4ee5\u4e3a\u5f39\u6027\u4e91\u670d\u52a1\u5668\u63d0\u4f9b\u8bbf\u95ee\u4e91\u670d\u52a1\u7684\u4e34\u65f6\u51ed\u8bc1\u3002"
},
{
"name": "OS-SRV-USG:launched_at",
"type": "文本",
"example": "2018-08-15T14:21:22.000000",
"desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u542f\u52a8\u65f6\u95f4\u3002\u65f6\u95f4\u683c\u5f0f\u4f8b\u5982:2019-05-22T03:23:59.000000"
},
{
"name": "OS-SRV-USG:terminated_at",
"type": "文本",
"example": "2019-05-22T03:23:59.000000",
"desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u5220\u9664\u65f6\u95f4\u3002\n\n\u65f6\u95f4\u683c\u5f0f\u4f8b\u5982:2019-05-22T03:23:59.000000"
},
{
"name": "os-extended-volumes:volumes_attached",
"type": "json",
"example": {
"$ref": "#/definitions/ServerExtendVolumeAttachment"
}, },
{ "desc": "\u6302\u8f7d\u5230\u5f39\u6027\u4e91\u670d\u52a1\u5668\u4e0a\u7684\u78c1\u76d8\u3002"
"name": "auto_terminate_time", },
"type": "string", {
"desc": "弹性云服务器定时删除时间。\n\n时间格式例如:2020-01-19T03:30:52Z", "name": "description",
"example": "2020-01-19T03:30:52Z" "type": "文本",
"example": "ecs description",
"desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u7684\u63cf\u8ff0\u4fe1\u606f\u3002"
},
{
"name": "host_status",
"type": "文本",
"example": "UP",
"desc": "nova-compute\u72b6\u6001\u3002\n\n- UP:\u670d\u52a1\u6b63\u5e38\n- UNKNOWN:\u72b6\u6001\u672a\u77e5\n- DOWN:\u670d\u52a1\u5f02\u5e38\n- MAINTENANCE:\u7ef4\u62a4\u72b6\u6001\n- \u7a7a\u5b57\u7b26\u4e32:\u5f39\u6027\u4e91\u670d\u52a1\u5668\u65e0\u4e3b\u673a\u4fe1\u606f"
},
{
"name": "OS-EXT-SRV-ATTR:hostname",
"type": "文本",
"example": null,
"desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u7684\u4e3b\u673a\u540d\u3002"
},
{
"name": "OS-EXT-SRV-ATTR:reservation_id",
"type": "文本",
"example": "r-f06p3js8",
"desc": "\u6279\u91cf\u521b\u5efa\u573a\u666f,\u5f39\u6027\u4e91\u670d\u52a1\u5668\u7684\u9884\u7559ID\u3002"
},
{
"name": "OS-EXT-SRV-ATTR:launch_index",
"type": "整数",
"example": null,
"desc": "\u6279\u91cf\u521b\u5efa\u573a\u666f,\u5f39\u6027\u4e91\u670d\u52a1\u5668\u7684\u542f\u52a8\u987a\u5e8f\u3002"
},
{
"name": "OS-EXT-SRV-ATTR:kernel_id",
"type": "文本",
"example": null,
"desc": "\u82e5\u4f7f\u7528AMI\u683c\u5f0f\u7684\u955c\u50cf,\u5219\u8868\u793akernel image\u7684UUID;\u5426\u5219,\u7559\u7a7a\u3002"
},
{
"name": "OS-EXT-SRV-ATTR:ramdisk_id",
"type": "文本",
"example": null,
"desc": "\u82e5\u4f7f\u7528AMI\u683c\u5f0f\u955c\u50cf,\u5219\u8868\u793aramdisk image\u7684UUID;\u5426\u5219,\u7559\u7a7a\u3002"
},
{
"name": "OS-EXT-SRV-ATTR:root_device_name",
"type": "文本",
"example": "/dev/vda",
"desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u7cfb\u7edf\u76d8\u7684\u8bbe\u5907\u540d\u79f0\u3002"
},
{
"name": "OS-EXT-SRV-ATTR:user_data",
"type": "文本",
"example": "IyEvYmluL2Jhc2gKZWNobyAncm9vdDokNiRjcGRkSjckWm5WZHNiR253Z0l0SGlxUjZxbWtLTlJaeU9lZUtKd3dPbG9XSFdUeGFzWjA1STYwdnJYRTdTUTZGbEpFbWlXZ21WNGNmZ1pac1laN1BkMTBLRndyeC8nIHwgY2hwYXNzd2Q6666",
"desc": "\u521b\u5efa\u5f39\u6027\u4e91\u670d\u52a1\u5668\u65f6\u6307\u5b9a\u7684user_data\u3002"
},
{
"name": "locked",
"type": "boolean",
"example": null,
"desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u662f\u5426\u4e3a\u9501\u5b9a\u72b6\u6001\u3002\n\n- true:\u9501\u5b9a\n- false:\u672a\u9501\u5b9a"
},
{
"name": "tags",
"type": "文本、多值",
"example": {
"type": "文本"
}, },
{ "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u6807\u7b7e\u3002"
"name": "hostId", },
"type": "string", {
"desc": "弹性云服务器所在主机的主机ID。", "name": "os:scheduler_hints",
"example": "c7145889b2e3202cd295ceddb1742ff8941b827b586861fd0acedf64" "type": "json",
"example": null,
"desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u8c03\u5ea6\u4fe1\u606f"
},
{
"name": "enterprise_project_id",
"type": "文本",
"example": "0",
"desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u6240\u5c5e\u7684\u4f01\u4e1a\u9879\u76eeID\u3002"
},
{
"name": "sys_tags",
"type": "文本、多值",
"example": {
"$ref": "#/definitions/ServerSystemTag"
}, },
{ "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u7cfb\u7edf\u6807\u7b7e\u3002"
"name": "OS-EXT-SRV-ATTR:host", },
"type": "string", {
"desc": "弹性云服务器所在主机的主机名称。", "name": "cpu_options",
"example": "pod01.cn-north-1c" "type": "json",
}, "example": null,
{ "desc": "\u81ea\u5b9a\u4e49CPU\u9009\u9879\u3002"
"name": "addresses", },
"type": "object", {
"desc": "弹性云服务器的网络属性。", "name": "hypervisor",
"example": "" "type": "文本",
}, "example": null,
{ "desc": "hypervisor\u4fe1\u606f\u3002"
"name": "key_name", }
"type": "string",
"desc": "弹性云服务器使用的密钥对名称。",
"example": "KeyPair-test"
},
{
"name": "image",
"type": "",
"desc": "弹性云服务器镜像信息。",
"example": ""
},
{
"name": "OS-EXT-STS:task_state",
"type": "string",
"desc": "扩展属性,弹性云服务器当前任务的状态。\n\n取值范围请参考[云服务器状态](https://support.huaweicloud.com/api-ecs/ecs_08_0002.html)表3。",
"example": "rebooting"
},
{
"name": "OS-EXT-STS:vm_state",
"type": "string",
"desc": "扩展属性,弹性云服务器当前状态。\n\n云服务器状态说明请参考[云服务器状态](https://support.huaweicloud.com/api-ecs/ecs_08_0002.html)。",
"example": "active"
},
{
"name": "OS-EXT-SRV-ATTR:instance_name",
"type": "string",
"desc": "扩展属性,弹性云服务器别名。",
"example": "instance-0048a91b"
},
{
"name": "OS-EXT-SRV-ATTR:hypervisor_hostname",
"type": "string",
"desc": "扩展属性,弹性云服务器所在虚拟化主机名。",
"example": "nova022@36"
},
{
"name": "flavor",
"type": "",
"desc": "弹性云服务器规格信息。",
"example": ""
},
{
"name": "id",
"type": "string",
"desc": "弹性云服务器ID,格式为UUID。",
"example": "4f4b3dfa-eb70-47cf-a60a-998a53bd6666"
},
{
"name": "security_groups",
"type": "array",
"desc": "弹性云服务器所属安全组列表。",
"example": ""
},
{
"name": "OS-EXT-AZ:availability_zone",
"type": "string",
"desc": "扩展属性,弹性云服务器所在可用区名称。",
"example": "cn-north-1c"
},
{
"name": "user_id",
"type": "string",
"desc": "创建弹性云服务器的用户ID,格式为UUID。",
"example": "05498fe56b8010d41f7fc01e280b6666"
},
{
"name": "name",
"type": "string",
"desc": "弹性云服务器名称。",
"example": "ecs-test-server"
},
{
"name": "created",
"type": "string",
"desc": "弹性云服务器创建时间。\n\n时间格式例如:2019-05-22T03:19:19Z",
"example": "2017-07-15T11:30:52Z"
},
{
"name": "tenant_id",
"type": "string",
"desc": "弹性云服务器所属租户ID,即项目id,和project_id表示相同的概念,格式为UUID。",
"example": "743b4c0428d94531b9f2add666646666"
},
{
"name": "OS-DCF:diskConfig",
"type": "string",
"desc": "扩展属性, diskConfig的类型。\n\n- MANUAL,镜像空间不会扩展。\n- AUTO,系统盘镜像空间会自动扩展为与flavor大小一致。",
"example": "AUTO"
},
{
"name": "accessIPv4",
"type": "string",
"desc": "预留属性。",
"example": ""
},
{
"name": "accessIPv6",
"type": "string",
"desc": "预留属性。",
"example": ""
},
{
"name": "fault",
"type": "",
"desc": "弹性云服务器故障信息。\n\n可选参数,在弹性云服务器状态为ERROR且存在异常的情况下返回。",
"example": ""
},
{
"name": "progress",
"type": "integer",
"desc": "弹性云服务器进度。",
"example": 0
},
{
"name": "OS-EXT-STS:power_state",
"type": "integer",
"desc": "扩展属性,弹性云服务器电源状态。",
"example": 4
},
{
"name": "config_drive",
"type": "string",
"desc": "config drive信息。",
"example": ""
},
{
"name": "metadata",
"type": "object",
"desc": "弹性云服务器元数据。\n\n> 说明:\n> \n> 元数据包含系统默认添加字段和用户设置的字段。\n\n系统默认添加字段\n\n1. charging_mode\n云服务器的计费类型。\n\n- “0”:按需计费(即postPaid-后付费方式)。\n- “1”:按包年包月计费(即prePaid-预付费方式)。\"2\":竞价实例计费\n\n2. metering.order_id\n按“包年/包月”计费的云服务器对应的订单ID。\n\n3. metering.product_id\n按“包年/包月”计费的云服务器对应的产品ID。\n\n4. vpc_id\n云服务器所属的虚拟私有云ID。\n\n5. EcmResStatus\n云服务器的冻结状态。\n\n- normal:云服务器正常状态(未被冻结)。\n- freeze:云服务器被冻结。\n\n> 当云服务器被冻结或者解冻后,系统默认添加该字段,且该字段必选。\n\n6. metering.image_id\n云服务器操作系统对应的镜像ID\n\n7. metering.imagetype\n镜像类型,目前支持:\n\n- 公共镜像(gold)\n- 私有镜像(private)\n- 共享镜像(shared)\n\n8. metering.resourcespeccode\n云服务器对应的资源规格。\n\n9. image_name\n云服务器操作系统对应的镜像名称。\n\n10. os_bit\n操作系统位数,一般取值为“32”或者“64”。\n\n11. lockCheckEndpoint\n回调URL,用于检查弹性云服务器的加锁是否有效。\n\n- 如果有效,则云服务器保持锁定状态。\n- 如果无效,解除锁定状态,删除失效的锁。\n\n12. lockSource\n弹性云服务器来自哪个服务。订单加锁(ORDER)\n\n13. lockSourceId\n弹性云服务器的加锁来自哪个ID。lockSource为“ORDER”时,lockSourceId为订单ID。\n\n14. lockScene\n弹性云服务器的加锁类型。\n\n- 按需转包周期(TO_PERIOD_LOCK)\n\n15. virtual_env_type\n\n- IOS镜像创建虚拟机,\"virtual_env_type\": \"IsoImage\" 属性;\n- 非IOS镜像创建虚拟机,在19.5.0版本以后创建的虚拟机将不会添加virtual_env_type 属性,而在此之前的版本创建的虚拟机可能会返回\"virtual_env_type\": \"FusionCompute\"属性 。\n\n> virtual_env_type属性不允许用户增加、删除和修改。\n\n16. metering.resourcetype\n云服务器对应的资源类型。\n\n17. os_type\n操作系统类型,取值为:Linux、Windows。\n\n18. cascaded.instance_extrainfo\n系统内部虚拟机扩展信息。\n\n19. __support_agent_list\n云服务器是否支持企业主机安全、主机监控。\n\n- “hss”:企业主机安全\n- “ces”:主机监控\n\n20. agency_name\n委托的名称。\n\n委托是由租户管理员在统一身份认证服务(Identity and Access Management,IAM)上创建的,可以为弹性云服务器提供访问云服务的临时凭证。",
"example": ""
},
{
"name": "OS-SRV-USG:launched_at",
"type": "string",
"desc": "弹性云服务器启动时间。时间格式例如:2019-05-22T03:23:59.000000",
"example": "2018-08-15T14:21:22.000000"
},
{
"name": "OS-SRV-USG:terminated_at",
"type": "string",
"desc": "弹性云服务器删除时间。\n\n时间格式例如:2019-05-22T03:23:59.000000",
"example": "2019-05-22T03:23:59.000000"
},
{
"name": "os-extended-volumes:volumes_attached",
"type": "array",
"desc": "挂载到弹性云服务器上的磁盘。",
"example": ""
},
{
"name": "description",
"type": "string",
"desc": "弹性云服务器的描述信息。",
"example": "ecs description"
},
{
"name": "host_status",
"type": "string",
"desc": "nova-compute状态。\n\n- UP:服务正常\n- UNKNOWN:状态未知\n- DOWN:服务异常\n- MAINTENANCE:维护状态\n- 空字符串:弹性云服务器无主机信息",
"example": "UP"
},
{
"name": "OS-EXT-SRV-ATTR:hostname",
"type": "string",
"desc": "弹性云服务器的主机名。",
"example": ""
},
{
"name": "OS-EXT-SRV-ATTR:reservation_id",
"type": "string",
"desc": "批量创建场景,弹性云服务器的预留ID。",
"example": "r-f06p3js8"
},
{
"name": "OS-EXT-SRV-ATTR:launch_index",
"type": "integer",
"desc": "批量创建场景,弹性云服务器的启动顺序。",
"example": 0
},
{
"name": "OS-EXT-SRV-ATTR:kernel_id",
"type": "string",
"desc": "若使用AMI格式的镜像,则表示kernel image的UUID;否则,留空。",
"example": ""
},
{
"name": "OS-EXT-SRV-ATTR:ramdisk_id",
"type": "string",
"desc": "若使用AMI格式镜像,则表示ramdisk image的UUID;否则,留空。",
"example": ""
},
{
"name": "OS-EXT-SRV-ATTR:root_device_name",
"type": "string",
"desc": "弹性云服务器系统盘的设备名称。",
"example": "/dev/vda"
},
{
"name": "OS-EXT-SRV-ATTR:user_data",
"type": "string",
"desc": "创建弹性云服务器时指定的user_data。",
"example": "IyEvYmluL2Jhc2gKZWNobyAncm9vdDokNiRjcGRkSjckWm5WZHNiR253Z0l0SGlxUjZxbWtLTlJaeU9lZUtKd3dPbG9XSFdUeGFzWjA1STYwdnJYRTdTUTZGbEpFbWlXZ21WNGNmZ1pac1laN1BkMTBLRndyeC8nIHwgY2hwYXNzd2Q6666"
},
{
"name": "locked",
"type": "boolean",
"desc": "弹性云服务器是否为锁定状态。\n\n- true:锁定\n- false:未锁定",
"example": false
},
{
"name": "tags",
"type": "array",
"desc": "弹性云服务器标签。",
"example": ""
},
{
"name": "os:scheduler_hints",
"type": "",
"desc": "弹性云服务器调度信息",
"example": ""
},
{
"name": "enterprise_project_id",
"type": "string",
"desc": "弹性云服务器所属的企业项目ID。",
"example": "0"
},
{
"name": "sys_tags",
"type": "array",
"desc": "弹性云服务器系统标签。",
"example": ""
},
{
"name": "cpu_options",
"type": "",
"desc": "自定义CPU选项。",
"example": ""
},
{
"name": "hypervisor",
"type": "",
"desc": "hypervisor信息。",
"example": ""
}
] ]

View File

@ -1,248 +1,297 @@
[ [
{ {
"name": "Placement", "name": "Placement",
"type": "Placement", "type": "json",
"desc": "实例所在的位置。", "desc": "实例所在的位置。",
"example": "" "example": {
"HostId": "host-h3m57oik",
"ProjectId": 1174660,
"HostIds": [],
"Zone": "ap-guangzhou-1",
"HostIps": []
}
}, },
{ {
"name": "InstanceId", "name": "InstanceId",
"type": "String", "type": "文本",
"desc": "实例ID。", "desc": "实例ID。",
"example": "ins-9bxebleo" "example": "ins-xlsyru2j"
}, },
{ {
"name": "InstanceType", "name": "InstanceType",
"type": "String", "type": "文本",
"desc": "实例机型。", "desc": "实例机型。",
"example": "S1.SMALL1" "example": "S2.SMALL2"
}, },
{ {
"name": "CPU", "name": "CPU",
"type": "Integer", "type": "整数",
"desc": "实例的CPU核数单位核。", "desc": "实例的CPU核数单位核。",
"example": "1" "example": 1
}, },
{ {
"name": "Memory", "name": "Memory",
"type": "Integer", "type": "整数",
"desc": "实例内存容量单位GB。", "desc": "实例内存容量单位GB。",
"example": "1" "example": 1
}, },
{ {
"name": "RestrictState", "name": "RestrictState",
"type": "String", "type": "文本",
"desc": "NORMAL表示正常状态的实例\nEXPIRED表示过期的实例\nPROTECTIVELY_ISOLATED表示被安全隔离的实例。", "desc": "实例业务状态。取值范围: NORMAL表示正常状态的实例 EXPIRED表示过期的实例 PROTECTIVELY_ISOLATED表示被安全隔离的实例。",
"example": "NORMAL" "example": "PROTECTIVELY_ISOLATED"
}, },
{ {
"name": "InstanceName", "name": "InstanceName",
"type": "String", "type": "文本",
"desc": "实例名称。", "desc": "实例名称。",
"example": "测试实例" "example": "test"
}, },
{ {
"name": "InstanceChargeType", "name": "InstanceChargeType",
"type": "String", "type": "文本",
"desc": "PREPAID表示预付费即包年包月\nPOSTPAID_BY_HOUR表示后付费即按量计费\nCDHPAID专用宿主机付费即只对专用宿主机计费不对专用宿主机上的实例计费。\nSPOTPAID表示竞价实例付费。", "desc": "实例计费模式。取值范围: PREPAID表示预付费即包年包月 POSTPAID_BY_HOUR表示后付费即按量计费 CDHPAID专用宿主机付费即只对专用宿主机计费不对专用宿主机上的实例计费。 SPOTPAID表示竞价实例付费。",
"example": "PREPAID" "example": "POSTPAID_BY_HOUR"
}, },
{ {
"name": "SystemDisk", "name": "SystemDisk",
"type": "SystemDisk", "type": "json",
"desc": "实例系统盘信息。", "desc": "实例系统盘信息。",
"example": "" "example": {
"DiskSize": 50,
"CdcId": null,
"DiskId": "disk-czsodtl1",
"DiskType": "CLOUD_SSD"
}
}, },
{ {
"name": "DataDisks", "name": "DataDisks",
"type": "Array of DataDisk", "type": "json",
"desc": "实例数据盘信息。", "desc": "实例数据盘信息。",
"example": "" "example": [
{
"DeleteWithInstance": true,
"Encrypt": true,
"CdcId": null,
"DiskType": "CLOUD_SSD",
"ThroughputPerformance": 0,
"KmsKeyId": null,
"DiskSize": 50,
"SnapshotId": null,
"DiskId": "disk-bzsodtn1"
}
]
}, },
{ {
"name": "PrivateIpAddresses", "name": "PrivateIpAddresses",
"type": "Array of String", "type": "文本、多值",
"desc": "实例主网卡的内网IP列表。", "desc": "实例主网卡的内网IP列表。",
"example": "[\"172.16.32.78\"]" "example": [
"172.16.32.78"
]
}, },
{ {
"name": "PublicIpAddresses", "name": "PublicIpAddresses",
"type": "Array of String", "type": "文本、多值",
"desc": "实例主网卡的公网IP列表。注意此字段可能返回 null表示取不到有效值。", "desc": "实例主网卡的公网IP列表。 注意:此字段可能返回 null表示取不到有效值。",
"example": "[\"123.207.11.190\"]" "example": [
"123.207.11.190"
]
}, },
{ {
"name": "InternetAccessible", "name": "InternetAccessible",
"type": "InternetAccessible", "type": "json",
"desc": "实例带宽信息。", "desc": "实例带宽信息。",
"example": "" "example": {
"PublicIpAssigned": true,
"InternetChargeType": "TRAFFIC_POSTPAID_BY_HOUR",
"BandwidthPackageId": null,
"InternetMaxBandwidthOut": 1
}
}, },
{ {
"name": "VirtualPrivateCloud", "name": "VirtualPrivateCloud",
"type": "VirtualPrivateCloud", "type": "json",
"desc": "实例所属虚拟私有网络信息。", "desc": "实例所属虚拟私有网络信息。",
"example": "" "example": {
"SubnetId": "subnet-mv4sn55k",
"AsVpcGateway": false,
"Ipv6AddressCount": 1,
"VpcId": "vpc-m0cnatxj",
"PrivateIpAddresses": [
"172.16.3.59"
]
}
}, },
{ {
"name": "ImageId", "name": "ImageId",
"type": "String", "type": "文本",
"desc": "生产实例所使用的镜像ID。", "desc": "生产实例所使用的镜像ID。",
"example": "img-9qabwvbn" "example": "img-8toqc6s3"
}, },
{ {
"name": "RenewFlag", "name": "RenewFlag",
"type": "String", "type": "文本",
"desc": "NOTIFY_AND_MANUAL_RENEW表示通知即将过期但不自动续费\nNOTIFY_AND_AUTO_RENEW表示通知即将过期而且自动续费\nDISABLE_NOTIFY_AND_MANUAL_RENEW表示不通知即将过期也不自动续费。\n注意后付费模式本项为null", "desc": "自动续费标识。取值范围: NOTIFY_AND_MANUAL_RENEW表示通知即将过期但不自动续费 NOTIFY_AND_AUTO_RENEW表示通知即将过期而且自动续费 DISABLE_NOTIFY_AND_MANUAL_RENEW表示不通知即将过期也不自动续费。 注意后付费模式本项为null",
"example": "NOTIFY_AND_MANUAL_RENEW" "example": "NOTIFY_AND_MANUAL_RENEW"
}, },
{ {
"name": "CreatedTime", "name": "CreatedTime",
"type": "Timestamp ISO8601", "type": "json",
"desc": "创建时间。按照ISO8601标准表示并且使用UTC时间。格式为YYYY-MM-DDThh:mm:ssZ。", "desc": "创建时间。按照ISO8601标准表示并且使用UTC时间。格式为YYYY-MM-DDThh:mm:ssZ。",
"example": "2020-03-10T02:43:51Z" "example": "2020-09-22T00:00:00+00:00"
}, },
{ {
"name": "ExpiredTime", "name": "ExpiredTime",
"type": "Timestamp ISO8601", "type": "json",
"desc": "到期时间。按照ISO8601标准表示并且使用UTC时间。格式为YYYY-MM-DDThh:mm:ssZ。注意后付费模式本项为null", "desc": "到期时间。按照ISO8601标准表示并且使用UTC时间。格式为YYYY-MM-DDThh:mm:ssZ。注意后付费模式本项为null",
"example": "2020-04-10T02:47:36Z" "example": "2020-09-22T00:00:00+00:00"
}, },
{ {
"name": "OsName", "name": "OsName",
"type": "String", "type": "文本",
"desc": "操作系统名称。", "desc": "操作系统名称。",
"example": "CentOS 7.6 64bit" "example": "CentOS 7.4 64bit"
}, },
{ {
"name": "SecurityGroupIds", "name": "SecurityGroupIds",
"type": "Array of String", "type": "文本、多值",
"desc": "实例所属安全组。该参数可以通过调用 DescribeSecurityGroups 的返回值中的sgId字段来获取。", "desc": "实例所属安全组。该参数可以通过调用 DescribeSecurityGroups 的返回值中的sgId字段来获取。",
"example": "[\"sg-p1ezv4wz\"]" "example": [
"sg-p1ezv4wz"
]
}, },
{ {
"name": "LoginSettings", "name": "LoginSettings",
"type": "LoginSettings", "type": "json",
"desc": "实例登录设置。目前只返回实例所关联的密钥。", "desc": "实例登录设置。目前只返回实例所关联的密钥。",
"example": "" "example": {
"Password": "123qwe!@#QWE",
"KeepImageLogin": "False",
"KeyIds": [
"skey-b4vakk62"
]
}
}, },
{ {
"name": "InstanceState", "name": "InstanceState",
"type": "String", "type": "文本",
"desc": "PENDING表示创建中\nLAUNCH_FAILED表示创建失败\nRUNNING表示运行中\nSTOPPED表示关机\nSTARTING表示开机中\nSTOPPING表示关机中\nREBOOTING表示重启中\nSHUTDOWN表示停止待销毁\nTERMINATING表示销毁中。", "desc": "实例状态。取值范围: PENDING表示创建中 LAUNCH_FAILED表示创建失败 RUNNING表示运行中 STOPPED表示关机 STARTING表示开机中 STOPPING表示关机中 REBOOTING表示重启中 SHUTDOWN表示停止待销毁 TERMINATING表示销毁中。",
"example": "" "example": "RUNNING"
}, },
{ {
"name": "Tags", "name": "Tags",
"type": "Array of Tag", "type": "json",
"desc": "实例关联的标签列表。", "desc": "实例关联的标签列表。",
"example": "" "example": [
{
"Value": "test",
"Key": "test"
}
]
}, },
{ {
"name": "StopChargingMode", "name": "StopChargingMode",
"type": "String", "type": "文本",
"desc": "KEEP_CHARGING关机继续收费\nSTOP_CHARGING关机停止收费\nNOT_APPLICABLE实例处于非关机状态或者不适用关机停止计费的条件", "desc": "实例的关机计费模式。 取值范围: KEEP_CHARGING关机继续收费 STOP_CHARGING关机停止收费NOT_APPLICABLE实例处于非关机状态或者不适用关机停止计费的条件",
"example": "NOT_APPLICABLE" "example": "NOT_APPLICABLE"
}, },
{ {
"name": "Uuid", "name": "Uuid",
"type": "String", "type": "文本",
"desc": "实例全局唯一ID", "desc": "实例全局唯一ID",
"example": "68b510db-b4c1-4630-a62b-73d0c7c970f9" "example": "e85f1388-0422-410d-8e50-bef540e78c18"
}, },
{ {
"name": "LatestOperation", "name": "LatestOperation",
"type": "String", "type": "文本",
"desc": "实例的最新操作。例StopInstances、ResetInstance。注意:此字段可能返回 null表示取不到有效值。", "desc": "实例的最新操作。例StopInstances、ResetInstance。 注意:此字段可能返回 null表示取不到有效值。",
"example": "RenewInstances" "example": "ResetInstancesType"
}, },
{ {
"name": "LatestOperationState", "name": "LatestOperationState",
"type": "String", "type": "文本",
"desc": "SUCCESS表示操作成功\nOPERATING表示操作执行中\nFAILED表示操作失败注意:此字段可能返回 null表示取不到有效值。", "desc": "实例的最新操作状态。取值范围: SUCCESS表示操作成功 OPERATING表示操作执行中 FAILED表示操作失败 注意:此字段可能返回 null表示取不到有效值。",
"example": "SUCCESS" "example": "SUCCESS"
}, },
{ {
"name": "LatestOperationRequestId", "name": "LatestOperationRequestId",
"type": "String", "type": "文本",
"desc": "实例最新操作的唯一请求 ID。注意:此字段可能返回 null表示取不到有效值。", "desc": "实例最新操作的唯一请求 ID。 注意:此字段可能返回 null表示取不到有效值。",
"example": "3554eb5b-1cfa-471a-ae76-dc436c9d43e8" "example": "c7de1287-061d-4ace-8caf-6ad8e5a2f29a"
}, },
{ {
"name": "DisasterRecoverGroupId", "name": "DisasterRecoverGroupId",
"type": "String", "type": "文本",
"desc": "分散置放群组ID。注意:此字段可能返回 null表示取不到有效值。", "desc": "分散置放群组ID。 注意:此字段可能返回 null表示取不到有效值。",
"example": "null" "example": ""
}, },
{ {
"name": "IPv6Addresses", "name": "IPv6Addresses",
"type": "Array of String", "type": "文本、多值",
"desc": "实例的IPv6地址。注意此字段可能返回 null表示取不到有效值。", "desc": "实例的IPv6地址。 注意:此字段可能返回 null表示取不到有效值。",
"example": "null" "example": [
"2001:0db8:86a3:08d3:1319:8a2e:0370:7344"
]
}, },
{ {
"name": "CamRoleName", "name": "CamRoleName",
"type": "String", "type": "文本",
"desc": "CAM角色名。注意:此字段可能返回 null表示取不到有效值。", "desc": "CAM角色名。 注意:此字段可能返回 null表示取不到有效值。",
"example": "null" "example": ""
}, },
{ {
"name": "HpcClusterId", "name": "HpcClusterId",
"type": "String", "type": "文本",
"desc": "高性能计算集群ID。注意:此字段可能返回 null表示取不到有效值。", "desc": "高性能计算集群ID。 注意:此字段可能返回 null表示取不到有效值。",
"example": "null" "example": ""
}, },
{ {
"name": "RdmaIpAddresses", "name": "RdmaIpAddresses",
"type": "Array of String", "type": "文本、多值",
"desc": "高性能计算集群IP列表。注意此字段可能返回 null表示取不到有效值。", "desc": "高性能计算集群IP列表。 注意:此字段可能返回 null表示取不到有效值。",
"example": "null" "example": []
},
{
"name": "DedicatedClusterId",
"type": "String",
"desc": "实例所在的专用集群ID。注意此字段可能返回 null表示取不到有效值。",
"example": "cluster-du3jken"
}, },
{ {
"name": "IsolatedSource", "name": "IsolatedSource",
"type": "String", "type": "文本",
"desc": "ARREAR表示欠费隔离\nEXPIRE表示到期隔离\nMANMADE表示主动退还隔离\nNOTISOLATED表示未隔离", "desc": "实例隔离类型。取值范围: ARREAR表示欠费隔离 EXPIRE表示到期隔离 MANMADE表示主动退还隔离 NOTISOLATED表示未隔离 注意:此字段可能返回 null表示取不到有效值。",
"example": "" "example": "NOTISOLATED"
}, },
{ {
"name": "GPUInfo", "name": "GPUInfo",
"type": "GPUInfo", "type": "json",
"desc": "GPU信息。如果是gpu类型子机该值会返回GPU信息如果是其他类型子机则不返回。注意:此字段可能返回 null表示取不到有效值。", "desc": "GPU信息。如果是gpu类型子机该值会返回GPU信息如果是其他类型子机则不返回。 注意:此字段可能返回 null表示取不到有效值。",
"example": "" "example": null
}, },
{ {
"name": "LicenseType", "name": "LicenseType",
"type": "String", "type": "文本",
"desc": "实例的操作系统许可类型默认为TencentCloud", "desc": "实例的操作系统许可类型默认为TencentCloud",
"example": "TencentCloud" "example": null
}, },
{ {
"name": "DisableApiTermination", "name": "DisableApiTermination",
"type": "Boolean", "type": "Boolean",
"desc": "TRUE表示开启实例保护不允许通过api接口删除实例\nFALSE表示关闭实例保护允许通过api接口删除实例默认取值FALSE。", "desc": "实例销毁保护标志表示是否允许通过api接口删除实例。取值范围 TRUE表示开启实例保护不允许通过api接口删除实例 FALSE表示关闭实例保护允许通过api接口删除实例 默认取值FALSE。",
"example": "false" "example": null
}, },
{ {
"name": "DefaultLoginUser", "name": "DefaultLoginUser",
"type": "String", "type": "文本",
"desc": "默认登录用户。", "desc": "默认登录用户。",
"example": "root" "example": null
}, },
{ {
"name": "DefaultLoginPort", "name": "DefaultLoginPort",
"type": "Integer", "type": "整数",
"desc": "默认登录端口。", "desc": "默认登录端口。",
"example": "22" "example": null
}, },
{ {
"name": "LatestOperationErrorMsg", "name": "LatestOperationErrorMsg",
"type": "String", "type": "文本",
"desc": "实例的最新操作错误信息。注意:此字段可能返回 null表示取不到有效值。", "desc": "实例的最新操作错误信息。 注意:此字段可能返回 null表示取不到有效值。",
"example": "None" "example": null
} }
] ]

View File

@ -2,27 +2,16 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from collections import defaultdict import requests
import datetime
import os
import yaml
from flask import current_app from flask import current_app
import json
from api.extensions import cache from api.extensions import cache
from api.extensions import db from api.extensions import db
from api.lib.cmdb.custom_dashboard import CustomDashboardManager from api.lib.cmdb.custom_dashboard import CustomDashboardManager
from api.models.cmdb import Attribute, AutoDiscoveryExecHistory from api.models.cmdb import Attribute
from api.models.cmdb import AutoDiscoveryCI
from api.models.cmdb import AutoDiscoveryCIType
from api.models.cmdb import AutoDiscoveryCITypeRelation
from api.models.cmdb import AutoDiscoveryCounter
from api.models.cmdb import AutoDiscoveryRuleSyncHistory
from api.models.cmdb import CI from api.models.cmdb import CI
from api.models.cmdb import CIType from api.models.cmdb import CIType
from api.models.cmdb import CITypeAttribute from api.models.cmdb import CITypeAttribute
from api.models.cmdb import PreferenceShowAttributes
from api.models.cmdb import PreferenceTreeView
from api.models.cmdb import RelationType from api.models.cmdb import RelationType
@ -221,6 +210,7 @@ class CITypeAttributeCache(object):
@classmethod @classmethod
def get(cls, type_id, attr_id): def get(cls, type_id, attr_id):
attr = cache.get(cls.PREFIX_ID.format(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)) attr = attr or cache.get(cls.PREFIX_ID.format(type_id, attr_id))
attr = attr or CITypeAttribute.get_by(type_id=type_id, attr_id=attr_id, first=True, to_dict=False) attr = attr or CITypeAttribute.get_by(type_id=type_id, attr_id=attr_id, first=True, to_dict=False)
@ -240,9 +230,7 @@ class CITypeAttributeCache(object):
class CMDBCounterCache(object): class CMDBCounterCache(object):
KEY = 'CMDB::Counter::dashboard' KEY = 'CMDB::Counter'
KEY2 = 'CMDB::Counter::adc'
KEY3 = 'CMDB::Counter::sub'
@classmethod @classmethod
def get(cls): def get(cls):
@ -255,7 +243,7 @@ class CMDBCounterCache(object):
@classmethod @classmethod
def set(cls, result): def set(cls, result):
cache.set(cls.KEY, json.loads(json.dumps(result)), timeout=0) cache.set(cls.KEY, result, timeout=0)
@classmethod @classmethod
def reset(cls): def reset(cls):
@ -263,83 +251,53 @@ class CMDBCounterCache(object):
result = {} result = {}
for custom in customs: for custom in customs:
if custom['category'] == 0: if custom['category'] == 0:
res = cls.sum_counter(custom) result[custom['id']] = cls.summary_counter(custom['type_id'])
elif custom['category'] == 1: elif custom['category'] == 1:
res = cls.attribute_counter(custom) result[custom['id']] = cls.attribute_counter(custom['type_id'], custom['attr_id'])
else: elif custom['category'] == 2:
res = cls.relation_counter(custom.get('type_id'), result[custom['id']] = cls.relation_counter(custom['type_id'], custom['level'])
custom.get('level'),
custom.get('options', {}).get('filter', ''),
custom.get('options', {}).get('type_ids', ''))
if res:
result[custom['id']] = res
cls.set(result) cls.set(result)
return json.loads(json.dumps(result)) return result
@classmethod @classmethod
def update(cls, custom, flush=True): def update(cls, custom):
result = cache.get(cls.KEY) or {} result = cache.get(cls.KEY) or {}
if not result: if not result:
result = cls.reset() result = cls.reset()
if custom['category'] == 0: if custom['category'] == 0:
res = cls.sum_counter(custom) result[custom['id']] = cls.summary_counter(custom['type_id'])
elif custom['category'] == 1: elif custom['category'] == 1:
res = cls.attribute_counter(custom) result[custom['id']] = cls.attribute_counter(custom['type_id'], custom['attr_id'])
else: elif custom['category'] == 2:
res = cls.relation_counter(custom.get('type_id'), result[custom['id']] = cls.relation_counter(custom['type_id'], custom['level'])
custom.get('level'),
custom.get('options', {}).get('filter', ''),
custom.get('options', {}).get('type_ids', ''))
if res and flush: cls.set(result)
result[custom['id']] = res
cls.set(result)
return json.loads(json.dumps(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()
@classmethod @staticmethod
def relation_counter(cls, type_id, level, other_filer, type_ids): def relation_counter(type_id, level):
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
from api.lib.cmdb.attribute import AttributeManager
query = "_type:{}".format(type_id) uri = current_app.config.get('CMDB_API')
if other_filer:
query = "{},{}".format(query, other_filer)
s = search(query, count=1000000)
try:
type_names, _, _, _, _, _ = s.search()
except SearchError as e:
current_app.logger.error(e)
return
root_type = CITypeCache.get(type_id)
show_attr_id = root_type and root_type.show_id
show_attr = AttributeCache.get(show_attr_id)
type_id_names = [] type_names = requests.get("{}/ci/s?q=_type:{}&count=10000".format(uri, type_id)).json().get('result')
for i in type_names: type_id_names = [(str(i.get('_id')), i.get(i.get('unique'))) for i in type_names]
attr_value = i.get(show_attr and show_attr.name) or i.get(i.get('unique'))
enum_map = AttributeManager.get_enum_map(show_attr_id or i.get('unique'))
type_id_names.append((str(i.get('_id')), enum_map.get(attr_value, attr_value))) url = "{}/ci_relations/statistics?root_ids={}&level={}".format(
uri, ','.join([i[0] for i in type_id_names]), level)
s = RelSearch([i[0] for i in type_id_names], level) stats = requests.get(url).json()
try:
stats = s.statistics(type_ids, need_filter=False)
except SearchError as e:
current_app.logger.error(e)
return
id2name = dict(type_id_names) id2name = dict(type_id_names)
type_ids = set() type_ids = set()
for i in (stats.get('detail') or []): for i in (stats.get('detail') or []):
for j in stats['detail'][i]: for j in stats['detail'][i]:
type_ids.add(j) type_ids.add(j)
for type_id in type_ids: for type_id in type_ids:
_type = CITypeCache.get(type_id) _type = CITypeCache.get(type_id)
id2name[type_id] = _type and _type.alias id2name[type_id] = _type and _type.alias
@ -358,241 +316,10 @@ class CMDBCounterCache(object):
return result return result
@classmethod
def attribute_counter(cls, custom):
from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci import search
from api.lib.cmdb.utils import ValueTypeMap
from api.lib.cmdb.attribute import AttributeManager
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':
enum_map = {}
for _attr_id in attr_ids:
_attr = AttributeCache.get(_attr_id)
if _attr:
enum_map[_attr.alias] = AttributeManager.get_enum_map(_attr_id)
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()
cis = [{k: (enum_map.get(k) or {}).get(v, v) for k, v in ci.items()} for ci in cis]
except SearchError as e:
current_app.logger.error(e)
return
return cis
origin_result = dict()
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
enum_map1 = AttributeManager.get_enum_map(attr_ids[0])
for i in (list(facet.values()) or [[]])[0]:
k = ValueTypeMap.serialize2[attr2value_type[0]](str(i[0]))
result[enum_map1.get(k, k)] = i[1]
origin_result[k] = i[1]
if len(attr_ids) == 1:
return result
# level = 2
enum_map2 = AttributeManager.get_enum_map(attr_ids[1])
for v in origin_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[enum_map1.get(v, v)] = dict()
origin_result[v] = dict()
for i in (list(facet.values()) or [[]])[0]:
k = ValueTypeMap.serialize2[attr2value_type[1]](str(i[0]))
result[enum_map1.get(v, v)][enum_map2.get(k, k)] = i[1]
origin_result[v][k] = i[1]
if len(attr_ids) == 2:
return result
# level = 3
enum_map3 = AttributeManager.get_enum_map(attr_ids[2])
for v1 in origin_result:
if not isinstance(result[enum_map1.get(v1, v1)], dict):
continue
for v2 in origin_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[enum_map1.get(v1, v1)][enum_map2.get(v2, v2)] = dict()
for i in (list(facet.values()) or [[]])[0]:
k = ValueTypeMap.serialize2[attr2value_type[2]](str(i[0]))
result[enum_map1.get(v1, v1)][enum_map2.get(v2, v2)][enum_map3.get(k, k)] = i[1]
return result
@staticmethod @staticmethod
def sum_counter(custom): def attribute_counter(type_id, attr_id):
from api.lib.cmdb.search import SearchError uri = current_app.config.get('CMDB_API')
from api.lib.cmdb.search.ci import search url = "{}/ci/s?q=_type:{}&fl={}&facet={}".format(uri, type_id, attr_id, attr_id)
res = requests.get(url).json()
custom.setdefault('options', {}) if res.get('facet'):
type_id = custom.get('type_id') return dict([i[:2] for i in list(res.get('facet').values())[0]])
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
@classmethod
def flush_adc_counter(cls):
res = db.session.query(CI.type_id, CI.is_auto_discovery)
result = dict()
for i in res:
result.setdefault(i.type_id, dict(total=0, auto_discovery=0))
result[i.type_id]['total'] += 1
if i.is_auto_discovery:
result[i.type_id]['auto_discovery'] += 1
cache.set(cls.KEY2, result, timeout=0)
res = db.session.query(AutoDiscoveryCI.created_at,
AutoDiscoveryCI.updated_at,
AutoDiscoveryCI.adt_id,
AutoDiscoveryCI.type_id,
AutoDiscoveryCI.is_accept).filter(AutoDiscoveryCI.deleted.is_(False))
today = datetime.datetime.today()
this_month = datetime.datetime(today.year, today.month, 1)
last_month = this_month - datetime.timedelta(days=1)
last_month = datetime.datetime(last_month.year, last_month.month, 1)
this_week = today - datetime.timedelta(days=datetime.date.weekday(today))
this_week = datetime.datetime(this_week.year, this_week.month, this_week.day)
last_week = this_week - datetime.timedelta(days=7)
last_week = datetime.datetime(last_week.year, last_week.month, last_week.day)
result = dict()
for i in res:
if i.type_id not in result:
result[i.type_id] = dict(instance_count=0, accept_count=0,
this_month_count=0, this_week_count=0, last_month_count=0, last_week_count=0)
adts = AutoDiscoveryCIType.get_by(type_id=i.type_id, to_dict=False)
result[i.type_id]['rule_count'] = len(adts) + AutoDiscoveryCITypeRelation.get_by(
ad_type_id=i.type_id, only_query=True).count()
result[i.type_id]['exec_target_count'] = len(
set([j.oneagent_id for adt in adts for j in db.session.query(
AutoDiscoveryRuleSyncHistory.oneagent_id).filter(
AutoDiscoveryRuleSyncHistory.adt_id == adt.id)]))
result[i.type_id]['instance_count'] += 1
if i.is_accept:
result[i.type_id]['accept_count'] += 1
if last_month <= i.created_at < this_month:
result[i.type_id]['last_month_count'] += 1
elif i.created_at >= this_month:
result[i.type_id]['this_month_count'] += 1
if last_week <= i.created_at < this_week:
result[i.type_id]['last_week_count'] += 1
elif i.created_at >= this_week:
result[i.type_id]['this_week_count'] += 1
for type_id in result:
existed = AutoDiscoveryCounter.get_by(type_id=type_id, first=True, to_dict=False)
if existed is None:
AutoDiscoveryCounter.create(type_id=type_id, **result[type_id])
else:
existed.update(**result[type_id])
for i in AutoDiscoveryCounter.get_by(to_dict=False):
if i.type_id not in result:
i.delete()
@classmethod
def clear_ad_exec_history(cls):
ci_types = CIType.get_by(to_dict=False)
for ci_type in ci_types:
for i in AutoDiscoveryExecHistory.get_by(type_id=ci_type.id, only_query=True).order_by(
AutoDiscoveryExecHistory.id.desc()).offset(50000):
i.delete(commit=False)
db.session.commit()
@classmethod
def get_adc_counter(cls):
return cache.get(cls.KEY2) or cls.flush_adc_counter()
@classmethod
def flush_sub_counter(cls):
result = dict(type_id2users=defaultdict(list))
types = db.session.query(PreferenceShowAttributes.type_id,
PreferenceShowAttributes.uid, PreferenceShowAttributes.created_at).filter(
PreferenceShowAttributes.deleted.is_(False)).group_by(
PreferenceShowAttributes.uid, PreferenceShowAttributes.type_id)
for i in types:
result['type_id2users'][i.type_id].append(i.uid)
types = PreferenceTreeView.get_by(to_dict=False)
for i in types:
if i.uid not in result['type_id2users'][i.type_id]:
result['type_id2users'][i.type_id].append(i.uid)
cache.set(cls.KEY3, result, timeout=0)
return result
@classmethod
def get_sub_counter(cls):
return cache.get(cls.KEY3) or cls.flush_sub_counter()
class AutoDiscoveryMappingCache(object):
PREFIX = 'CMDB::AutoDiscovery::Mapping::{}'
@classmethod
def get(cls, name):
res = cache.get(cls.PREFIX.format(name)) or {}
if not res:
path = os.path.join(os.path.abspath(os.path.dirname(__file__)),
"auto_discovery/mapping/{}.yaml".format(name))
if os.path.exists(path):
with open(path, 'r') as f:
mapping = yaml.safe_load(f)
res = mapping.get('mapping') or {}
res and cache.set(cls.PREFIX.format(name), res, timeout=0)
return res

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,6 @@
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
from flask_babel import lazy_gettext as _l
from api.lib.utils import BaseEnum from api.lib.utils import BaseEnum
@ -14,10 +12,6 @@ class ValueTypeEnum(BaseEnum):
DATE = "4" DATE = "4"
TIME = "5" TIME = "5"
JSON = "6" JSON = "6"
PASSWORD = TEXT
LINK = TEXT
BOOL = "7"
REFERENCE = INT
class ConstraintEnum(BaseEnum): class ConstraintEnum(BaseEnum):
@ -45,23 +39,20 @@ class OperateType(BaseEnum):
class CITypeOperateType(BaseEnum): class CITypeOperateType(BaseEnum):
ADD = "0" # add CIType ADD = "0" # 新增模型
UPDATE = "1" # update CIType UPDATE = "1" # 修改模型
DELETE = "2" # delete CIType DELETE = "2" # 删除模型
ADD_ATTRIBUTE = "3" ADD_ATTRIBUTE = "3" # 新增属性
UPDATE_ATTRIBUTE = "4" UPDATE_ATTRIBUTE = "4" # 修改属性
DELETE_ATTRIBUTE = "5" DELETE_ATTRIBUTE = "5" # 删除属性
ADD_TRIGGER = "6" ADD_TRIGGER = "6" # 新增触发器
UPDATE_TRIGGER = "7" UPDATE_TRIGGER = "7" # 修改触发器
DELETE_TRIGGER = "8" DELETE_TRIGGER = "8" # 删除触发器
ADD_UNIQUE_CONSTRAINT = "9" ADD_UNIQUE_CONSTRAINT = "9" # 新增联合唯一
UPDATE_UNIQUE_CONSTRAINT = "10" UPDATE_UNIQUE_CONSTRAINT = "10" # 修改联合唯一
DELETE_UNIQUE_CONSTRAINT = "11" DELETE_UNIQUE_CONSTRAINT = "11" # 删除联合唯一
ADD_RELATION = "12" ADD_RELATION = "12" # 新增关系
DELETE_RELATION = "13" DELETE_RELATION = "13" # 删除关系
ADD_RECONCILIATION = "14"
UPDATE_RECONCILIATION = "15"
DELETE_RECONCILIATION = "16"
class RetKey(BaseEnum): class RetKey(BaseEnum):
@ -76,8 +67,6 @@ class ResourceTypeEnum(BaseEnum):
CI_TYPE_RELATION = "CITypeRelation" # create/delete/grant CI_TYPE_RELATION = "CITypeRelation" # create/delete/grant
RELATION_VIEW = "RelationView" # read/update/delete/grant RELATION_VIEW = "RelationView" # read/update/delete/grant
CI_FILTER = "CIFilter" # read CI_FILTER = "CIFilter" # read
PAGE = "page" # read
TOPOLOGY_VIEW = "TopologyView" # read/update/delete/grant
class PermEnum(BaseEnum): class PermEnum(BaseEnum):
@ -97,8 +86,7 @@ class RoleEnum(BaseEnum):
class AutoDiscoveryType(BaseEnum): class AutoDiscoveryType(BaseEnum):
AGENT = "agent" AGENT = "agent"
SNMP = "snmp" SNMP = "snmp"
HTTP = "http" # cloud HTTP = "http"
COMPONENTS = "components"
class AttributeDefaultValueEnum(BaseEnum): class AttributeDefaultValueEnum(BaseEnum):
@ -107,52 +95,11 @@ class AttributeDefaultValueEnum(BaseEnum):
AUTO_INC_ID = "$auto_inc_id" AUTO_INC_ID = "$auto_inc_id"
class ExecuteStatusEnum(BaseEnum):
COMPLETED = '0'
FAILED = '1'
RUNNING = '2'
class RelationSourceEnum(BaseEnum):
ATTRIBUTE_VALUES = "0"
AUTO_DISCOVERY = "1"
class BuiltinModelEnum(BaseEnum):
IPAM_SUBNET = "ipam_subnet"
IPAM_ADDRESS = "ipam_address"
IPAM_SCOPE = "ipam_scope"
DCIM_REGION = "dcim_region"
DCIM_IDC = "dcim_idc"
DCIM_SERVER_ROOM = "dcim_server_room"
DCIM_RACK = "dcim_rack"
BUILTIN_ATTRIBUTES = {
"_updated_at": _l("Update Time"),
"_updated_by": _l("Updated By"),
}
CMDB_QUEUE = "one_cmdb_async" CMDB_QUEUE = "one_cmdb_async"
REDIS_PREFIX_CI = "ONE_CMDB" REDIS_PREFIX_CI = "ONE_CMDB"
REDIS_PREFIX_CI_RELATION = "CMDB_CI_RELATION" REDIS_PREFIX_CI_RELATION = "CMDB_CI_RELATION"
REDIS_PREFIX_CI_RELATION2 = "CMDB_CI_RELATION2"
BUILTIN_KEYWORDS = {'id', '_id', 'ci_id', 'type', '_type', 'ci_type', 'ticket_id', *BUILTIN_ATTRIBUTES.keys()}
class SysComputedAttributes(object):
from api.lib.cmdb.ipam.const import SubnetBuiltinAttributes
type2attr = {
BuiltinModelEnum.IPAM_SUBNET: {
SubnetBuiltinAttributes.HOSTS_COUNT,
SubnetBuiltinAttributes.ASSIGN_COUNT,
SubnetBuiltinAttributes.USED_COUNT,
SubnetBuiltinAttributes.FREE_COUNT
}
}
BUILTIN_KEYWORDS = {'id', '_id', 'ci_id', 'type', '_type', 'ci_type'}
L_TYPE = None L_TYPE = None
L_CI = None L_CI = None

View File

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

View File

@ -1 +0,0 @@
# -*- coding:utf-8 -*-

View File

@ -1,33 +0,0 @@
# -*- coding:utf-8 -*-
from api.lib.cmdb.ci import CIManager
from api.lib.cmdb.ci import CIRelationManager
from api.lib.cmdb.const import ExistPolicy
class DCIMBase(object):
def __init__(self):
self.type_id = None
@staticmethod
def add_relation(parent_id, child_id):
if not parent_id or not child_id:
return
CIRelationManager().add(parent_id, child_id, valid=False, apply_async=False)
def add(self, parent_id, **kwargs):
ci_id = CIManager().add(self.type_id, exist_policy=ExistPolicy.REJECT, **kwargs)
if parent_id:
self.add_relation(parent_id, ci_id)
return ci_id
@classmethod
def update(cls, _id, **kwargs):
CIManager().update(_id, **kwargs)
@classmethod
def delete(cls, _id):
CIManager().delete(_id)

View File

@ -1,17 +0,0 @@
# -*- coding:utf-8 -*-
from api.lib.utils import BaseEnum
class RackBuiltinAttributes(BaseEnum):
U_COUNT = 'u_count'
U_START = 'u_start'
FREE_U_COUNT = 'free_u_count'
U_SLOT_ABNORMAL = 'u_slot_abnormal'
class OperateTypeEnum(BaseEnum):
ADD_DEVICE = "0"
REMOVE_DEVICE = "1"
MOVE_DEVICE = "2"

View File

@ -1,40 +0,0 @@
from flask_login import current_user
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.ci import CIManager
from api.lib.mixin import DBMixin
from api.models.cmdb import DCIMOperationHistory
class OperateHistoryManager(DBMixin):
cls = DCIMOperationHistory
@classmethod
def search(cls, page, page_size, fl=None, only_query=False, reverse=False, count_query=False,
last_size=None, **kwargs):
numfound, result = super(OperateHistoryManager, cls).search(page, page_size, fl, only_query, reverse,
count_query, last_size, **kwargs)
ci_ids = [i['ci_id'] for i in result]
id2ci = {i['_id']: i for i in (CIManager.get_cis_by_ids(ci_ids) or []) if i}
type2show_key = dict()
for i in id2ci.values():
if i.get('_type') not in type2show_key:
ci_type = CITypeCache.get(i.get('_type'))
if ci_type:
show_key = AttributeCache.get(ci_type.show_id or ci_type.unique_id)
type2show_key[i['_type']] = show_key and show_key.name
return numfound, result, id2ci, type2show_key
def _can_add(self, **kwargs):
kwargs['uid'] = current_user.uid
return kwargs
def _can_update(self, **kwargs):
pass
def _can_delete(self, **kwargs):
pass

View File

@ -1,19 +0,0 @@
# -*- coding:utf-8 -*-
from flask import abort
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.const import BuiltinModelEnum
from api.lib.cmdb.dcim.base import DCIMBase
from api.lib.cmdb.resp_format import ErrFormat
class IDCManager(DCIMBase):
def __init__(self):
super(IDCManager, self).__init__()
self.ci_type = CITypeCache.get(BuiltinModelEnum.DCIM_IDC) or abort(
404, ErrFormat.dcim_builtin_model_not_found.format(BuiltinModelEnum.DCIM_IDC))
self.type_id = self.ci_type.id

View File

@ -1,182 +0,0 @@
# -*- coding:utf-8 -*-
import itertools
import redis_lock
from flask import abort
from api.extensions import rd
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 BuiltinModelEnum
from api.lib.cmdb.dcim.base import DCIMBase
from api.lib.cmdb.dcim.const import OperateTypeEnum
from api.lib.cmdb.dcim.const import RackBuiltinAttributes
from api.lib.cmdb.dcim.history import OperateHistoryManager
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.search.ci.db.search import Search as SearchFromDB
from api.lib.cmdb.search.ci_relation.search import Search as RelationSearch
class RackManager(DCIMBase):
def __init__(self):
super(RackManager, self).__init__()
self.ci_type = CITypeCache.get(BuiltinModelEnum.DCIM_RACK) or abort(
404, ErrFormat.dcim_builtin_model_not_found.format(BuiltinModelEnum.DCIM_RACK))
self.type_id = self.ci_type.id
@classmethod
def update(cls, _id, **kwargs):
if RackBuiltinAttributes.U_COUNT in kwargs:
devices, _, _, _, _, _ = RelationSearch(
[_id],
level=[1],
fl=[RackBuiltinAttributes.U_COUNT, RackBuiltinAttributes.U_START],
count=1000000).search()
for device in devices:
u_start = device.get(RackBuiltinAttributes.U_START)
u_count = device.get(RackBuiltinAttributes.U_COUNT) or 2
if u_start and u_start + u_count - 1 > kwargs[RackBuiltinAttributes.U_COUNT]:
return abort(400, ErrFormat.dcim_rack_u_count_invalid)
CIManager().update(_id, _sync=True, **kwargs)
if RackBuiltinAttributes.U_COUNT in kwargs:
payload = {RackBuiltinAttributes.FREE_U_COUNT: cls.calc_u_free_count(_id)}
CIManager().update(_id, _sync=True, **payload)
def delete(self, _id):
super(RackManager, self).delete(_id)
payload = {RackBuiltinAttributes.U_START: None}
_, _, second_cis = CIRelationManager.get_second_cis(_id, per_page='all')
for ci in second_cis:
CIManager().update(ci['_id'], **payload)
@staticmethod
def calc_u_free_count(rack_id, device_id=None, u_start=None, u_count=None):
rack = CIManager.get_ci_by_id(rack_id, need_children=False)
if not rack.get(RackBuiltinAttributes.U_COUNT):
return 0
if device_id is not None and u_count is None:
ci = CIManager().get_ci_by_id(device_id, need_children=False)
u_count = ci.get(RackBuiltinAttributes.U_COUNT) or 2
if u_start and u_start + u_count - 1 > rack.get(RackBuiltinAttributes.U_COUNT):
return abort(400, ErrFormat.dcim_rack_u_slot_invalid)
devices, _, _, _, _, _ = RelationSearch(
[rack_id],
level=[1],
fl=[RackBuiltinAttributes.U_COUNT, RackBuiltinAttributes.U_START],
count=1000000).search()
u_count_sum = 0
for device in devices:
u_count_sum += (device.get(RackBuiltinAttributes.U_COUNT) or 2)
if device_id is not None:
_u_start = device.get(RackBuiltinAttributes.U_START)
_u_count = device.get(RackBuiltinAttributes.U_COUNT) or 2
if not _u_start:
continue
if device.get('_id') != device_id and set(range(u_start, u_start + u_count)) & set(
range(_u_start, _u_start + _u_count)):
return abort(400, ErrFormat.dcim_rack_u_slot_invalid)
return rack[RackBuiltinAttributes.U_COUNT] - u_count_sum
def check_u_slot(self):
racks, _, _, _, _, _ = SearchFromDB(
"_type:{}".format(self.type_id),
count=10000000,
fl=[RackBuiltinAttributes.U_START, RackBuiltinAttributes.U_COUNT, RackBuiltinAttributes.U_SLOT_ABNORMAL],
parent_node_perm_passed=True).search()
for rack in racks:
devices, _, _, _, _, _ = RelationSearch(
[rack['_id']],
level=[1],
fl=[RackBuiltinAttributes.U_COUNT, RackBuiltinAttributes.U_START],
count=1000000).search()
u_slot_sets = []
for device in devices:
u_start = device.get(RackBuiltinAttributes.U_START)
u_count = device.get(RackBuiltinAttributes.U_COUNT) or 2
if u_start is not None and str(u_start).isdigit():
u_slot_sets.append(set(range(u_start, u_start + u_count)))
if len(u_slot_sets) > 1:
u_slot_abnormal = False
for a, b in itertools.combinations(u_slot_sets, 2):
if a.intersection(b):
u_slot_abnormal = True
break
if u_slot_abnormal != rack.get(RackBuiltinAttributes.U_SLOT_ABNORMAL):
payload = {RackBuiltinAttributes.U_SLOT_ABNORMAL: u_slot_abnormal}
CIManager().update(rack['_id'], **payload)
def add_device(self, rack_id, device_id, u_start, u_count=None):
with (redis_lock.Lock(rd.r, "DCIM_RACK_OPERATE_{}".format(rack_id), expire=10)):
self.calc_u_free_count(rack_id, device_id, u_start, u_count)
self.add_relation(rack_id, device_id)
payload = {RackBuiltinAttributes.U_START: u_start}
if u_count:
payload[RackBuiltinAttributes.U_COUNT] = u_count
CIManager().update(device_id, _sync=True, **payload)
payload = {
RackBuiltinAttributes.FREE_U_COUNT: self.calc_u_free_count(rack_id, device_id, u_start, u_count)}
CIManager().update(rack_id, _sync=True, **payload)
OperateHistoryManager().add(operate_type=OperateTypeEnum.ADD_DEVICE, rack_id=rack_id, ci_id=device_id)
def remove_device(self, rack_id, device_id):
with (redis_lock.Lock(rd.r, "DCIM_RACK_OPERATE_{}".format(rack_id), expire=10)):
CIRelationManager.delete_3(rack_id, device_id, apply_async=False, valid=False)
payload = {RackBuiltinAttributes.FREE_U_COUNT: self.calc_u_free_count(rack_id)}
CIManager().update(rack_id, _sync=True, **payload)
payload = {RackBuiltinAttributes.U_START: None}
CIManager().update(device_id, _sync=True, **payload)
OperateHistoryManager().add(operate_type=OperateTypeEnum.REMOVE_DEVICE, rack_id=rack_id, ci_id=device_id)
def move_device(self, rack_id, device_id, to_u_start):
with (redis_lock.Lock(rd.r, "DCIM_RACK_OPERATE_{}".format(rack_id), expire=10)):
payload = {RackBuiltinAttributes.FREE_U_COUNT: self.calc_u_free_count(rack_id, device_id, to_u_start)}
CIManager().update(rack_id, _sync=True, **payload)
CIManager().update(device_id, _sync=True, **{RackBuiltinAttributes.U_START: to_u_start})
OperateHistoryManager().add(operate_type=OperateTypeEnum.MOVE_DEVICE, rack_id=rack_id, ci_id=device_id)
def migrate_device(self, rack_id, device_id, to_rack_id, to_u_start):
with (redis_lock.Lock(rd.r, "DCIM_RACK_OPERATE_{}".format(rack_id), expire=10)):
self.calc_u_free_count(to_rack_id, device_id, to_u_start)
if rack_id != to_rack_id:
CIRelationManager.delete_3(rack_id, device_id, apply_async=False, valid=False)
self.add_relation(to_rack_id, device_id)
payload = {
RackBuiltinAttributes.FREE_U_COUNT: self.calc_u_free_count(to_rack_id, device_id, to_u_start)}
CIManager().update(to_rack_id, _sync=True, **payload)
CIManager().update(device_id, _sync=True, **{RackBuiltinAttributes.U_START: to_u_start})
if rack_id != to_rack_id:
payload = {RackBuiltinAttributes.FREE_U_COUNT: self.calc_u_free_count(rack_id)}
CIManager().update(rack_id, _sync=True, **payload)
OperateHistoryManager().add(operate_type=OperateTypeEnum.REMOVE_DEVICE, rack_id=rack_id, ci_id=device_id)
OperateHistoryManager().add(operate_type=OperateTypeEnum.ADD_DEVICE, rack_id=to_rack_id, ci_id=device_id)

View File

@ -1,29 +0,0 @@
# -*- coding:utf-8 -*-
from flask import abort
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.ci import CIManager
from api.lib.cmdb.const import BuiltinModelEnum
from api.lib.cmdb.const import ExistPolicy
from api.lib.cmdb.resp_format import ErrFormat
class RegionManager(object):
def __init__(self):
self.ci_type = CITypeCache.get(BuiltinModelEnum.DCIM_REGION) or abort(
404, ErrFormat.dcim_builtin_model_not_found.format(BuiltinModelEnum.DCIM_REGION))
self.type_id = self.ci_type.id
def add(self, **kwargs):
return CIManager().add(self.type_id, exist_policy=ExistPolicy.REJECT, **kwargs)
@classmethod
def update(cls, _id, **kwargs):
CIManager().update(_id, **kwargs)
@classmethod
def delete(cls, _id):
CIManager().delete(_id)

View File

@ -1,56 +0,0 @@
# -*- coding:utf-8 -*-
from flask import abort
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.const import BuiltinModelEnum
from api.lib.cmdb.dcim.base import DCIMBase
from api.lib.cmdb.dcim.const import RackBuiltinAttributes
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.search.ci.db.search import Search as SearchFromDB
from api.models.cmdb import CI
from api.models.cmdb import CIRelation
class ServerRoomManager(DCIMBase):
def __init__(self):
super(ServerRoomManager, self).__init__()
self.ci_type = CITypeCache.get(BuiltinModelEnum.DCIM_SERVER_ROOM) or abort(
404, ErrFormat.dcim_builtin_model_not_found.format(BuiltinModelEnum.DCIM_SERVER_ROOM))
self.type_id = self.ci_type.id
@staticmethod
def get_racks(_id, q=None):
rack_type = CITypeCache.get(BuiltinModelEnum.DCIM_RACK) or abort(
404, ErrFormat.dcim_builtin_model_not_found.format(BuiltinModelEnum.DCIM_RACK))
relations = CIRelation.get_by(first_ci_id=_id, only_query=True).join(
CI, CI.id == CIRelation.second_ci_id).filter(CI.type_id == rack_type.id)
rack_ids = [i.second_ci_id for i in relations]
q = "_type:{}".format(rack_type.id) if not q else "_type:{},{}".format(rack_type.id, q)
if rack_ids:
response, _, _, _, numfound, _ = SearchFromDB(
q,
ci_ids=list(rack_ids),
count=1000000,
parent_node_perm_passed=True).search()
else:
response, numfound = [], 0
counter = dict(rack_count=numfound)
u_count = 0
free_u_count = 0
for i in response:
_u_count = i.get(RackBuiltinAttributes.U_COUNT) or 0
u_count += _u_count
free_u_count += (_u_count if i.get(RackBuiltinAttributes.FREE_U_COUNT) is None else
i.get(RackBuiltinAttributes.FREE_U_COUNT))
counter["u_count"] = u_count
counter["u_used_count"] = u_count - free_u_count
counter["device_count"] = CIRelation.get_by(only_query=True).filter(
CIRelation.first_ci_id.in_(rack_ids)).count()
return counter, response

View File

@ -1,85 +0,0 @@
# -*- coding:utf-8 -*-
from collections import defaultdict
from flask import abort
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.const import BuiltinModelEnum
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.search.ci.db.search import Search as SearchFromDB
from api.models.cmdb import CI
from api.models.cmdb import CIRelation
class TreeViewManager(object):
@classmethod
def get(cls):
region_type = CITypeCache.get(BuiltinModelEnum.DCIM_REGION) or abort(
404, ErrFormat.dcim_builtin_model_not_found.format(BuiltinModelEnum.DCIM_REGION))
idc_type = CITypeCache.get(BuiltinModelEnum.DCIM_IDC) or abort(
404, ErrFormat.dcim_builtin_model_not_found.format(BuiltinModelEnum.DCIM_IDC))
server_room_type = CITypeCache.get(BuiltinModelEnum.DCIM_SERVER_ROOM) or abort(
404, ErrFormat.dcim_builtin_model_not_found.format(BuiltinModelEnum.DCIM_SERVER_ROOM))
rack_type = CITypeCache.get(BuiltinModelEnum.DCIM_RACK) or abort(
404, ErrFormat.dcim_builtin_model_not_found.format(BuiltinModelEnum.DCIM_RACK))
relations = defaultdict(set)
ids = set()
has_parent_ids = set()
for i in CIRelation.get_by(only_query=True).join(CI, CI.id == CIRelation.first_ci_id).filter(
CI.type_id.in_([region_type.id, idc_type.id])):
relations[i.first_ci_id].add(i.second_ci_id)
ids.add(i.first_ci_id)
ids.add(i.second_ci_id)
has_parent_ids.add(i.second_ci_id)
for i in CIRelation.get_by(only_query=True).join(
CI, CI.id == CIRelation.second_ci_id).filter(CI.type_id.in_([idc_type.id, server_room_type.id])):
relations[i.first_ci_id].add(i.second_ci_id)
ids.add(i.first_ci_id)
ids.add(i.second_ci_id)
has_parent_ids.add(i.second_ci_id)
for i in CI.get_by(only_query=True).filter(CI.type_id.in_([region_type.id, idc_type.id])):
ids.add(i.id)
for _id in ids:
if _id not in has_parent_ids:
relations[None].add(_id)
type2name = dict()
type2name[region_type.id] = AttributeCache.get(region_type.show_id or region_type.unique_id).name
type2name[idc_type.id] = AttributeCache.get(idc_type.show_id or idc_type.unique_id).name
type2name[server_room_type.id] = AttributeCache.get(server_room_type.show_id or server_room_type.unique_id).name
response, _, _, _, _, _ = SearchFromDB(
"_type:({})".format(";".join(map(str, [region_type.id, idc_type.id, server_room_type.id]))),
ci_ids=list(ids),
count=1000000,
fl=list(type2name.values()),
parent_node_perm_passed=True).search()
id2ci = {i['_id']: i for i in response}
def _build_tree(_tree, parent_id=None):
tree = []
for child_id in _tree.get(parent_id, []):
children = sorted(_build_tree(_tree, child_id), key=lambda x: x['_id'])
if not id2ci.get(child_id):
continue
ci = id2ci[child_id]
if ci['ci_type'] == BuiltinModelEnum.DCIM_SERVER_ROOM:
ci['rack_count'] = CIRelation.get_by(first_ci_id=child_id, only_query=True).join(
CI, CI.id == CIRelation.second_ci_id).filter(CI.type_id == rack_type.id).count()
tree.append({'children': children, **ci})
return tree
result = sorted(_build_tree(relations), key=lambda x: x['_id'])
return result, type2name

View File

@ -10,26 +10,22 @@ from api.extensions import db
from api.lib.cmdb.cache import AttributeCache from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import RelationTypeCache from api.lib.cmdb.cache import RelationTypeCache
from api.lib.cmdb.const import OperateType from api.lib.cmdb.const import OperateType
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.perms import CIFilterPermsCRUD from api.lib.cmdb.perms import CIFilterPermsCRUD
from api.lib.cmdb.resp_format import ErrFormat from api.lib.cmdb.resp_format import ErrFormat
from api.lib.perm.acl.cache import UserCache from api.lib.perm.acl.cache import UserCache
from api.models.cmdb import CI
from api.models.cmdb import Attribute from api.models.cmdb import Attribute
from api.models.cmdb import AttributeHistory from api.models.cmdb import AttributeHistory
from api.models.cmdb import CIRelationHistory from api.models.cmdb import CIRelationHistory
from api.models.cmdb import CITriggerHistory
from api.models.cmdb import CITypeHistory from api.models.cmdb import CITypeHistory
from api.models.cmdb import CITypeTrigger from api.models.cmdb import CITypeTrigger
from api.models.cmdb import CITypeUniqueConstraint from api.models.cmdb import CITypeUniqueConstraint
from api.models.cmdb import OperationRecord from api.models.cmdb import OperationRecord
from api.lib.cmdb.utils import TableMap
class AttributeHistoryManger(object): class AttributeHistoryManger(object):
@staticmethod @staticmethod
def get_records_for_attributes(start, end, username, page, page_size, operate_type, type_id, def get_records_for_attributes(start, end, username, page, page_size, operate_type, type_id,
ci_id=None, attr_id=None, ci_ids=None, more=False): ci_id=None, attr_id=None):
records = db.session.query(OperationRecord, AttributeHistory).join( records = db.session.query(OperationRecord, AttributeHistory).join(
AttributeHistory, OperationRecord.id == AttributeHistory.record_id) AttributeHistory, OperationRecord.id == AttributeHistory.record_id)
@ -51,9 +47,6 @@ class AttributeHistoryManger(object):
if ci_id is not None: if ci_id is not None:
records = records.filter(AttributeHistory.ci_id == ci_id) records = records.filter(AttributeHistory.ci_id == ci_id)
if ci_ids and isinstance(ci_ids, list):
records = records.filter(AttributeHistory.ci_id.in_(ci_ids))
if attr_id is not None: if attr_id is not None:
records = records.filter(AttributeHistory.attr_id == attr_id) records = records.filter(AttributeHistory.attr_id == attr_id)
@ -61,39 +54,17 @@ class AttributeHistoryManger(object):
total = len(records) total = len(records)
res = {} res = {}
show_attr_set = {}
show_attr_cache = {}
for record in records: for record in records:
record_id = record.OperationRecord.id record_id = record.OperationRecord.id
type_id = record.OperationRecord.type_id
ci_id = record.AttributeHistory.ci_id
show_attr_set[ci_id] = None
show_attr = show_attr_cache.setdefault(
type_id,
AttributeCache.get(
CITypeCache.get(type_id).show_id or CITypeCache.get(type_id).unique_id) if CITypeCache.get(type_id) else None
)
if show_attr:
attr_table = TableMap(attr=show_attr).table
attr_record = attr_table.get_by(attr_id=show_attr.id, ci_id=ci_id, first=True, to_dict=False)
show_attr_set[ci_id] = attr_record.value if attr_record else None
attr_hist = record.AttributeHistory.to_dict() attr_hist = record.AttributeHistory.to_dict()
attr_hist['attr'] = AttributeCache.get(attr_hist['attr_id']) attr_hist['attr'] = AttributeCache.get(attr_hist['attr_id'])
if attr_hist['attr']: if attr_hist['attr']:
attr_hist['attr_name'] = attr_hist['attr'].name attr_hist['attr_name'] = attr_hist['attr'].name
attr_hist['attr_alias'] = attr_hist['attr'].alias attr_hist['attr_alias'] = attr_hist['attr'].alias
if more:
attr_hist['is_list'] = attr_hist['attr'].is_list
attr_hist['is_computed'] = attr_hist['attr'].is_computed
attr_hist['is_password'] = attr_hist['attr'].is_password
attr_hist['default'] = attr_hist['attr'].default
attr_hist['value_type'] = attr_hist['attr'].value_type
attr_hist.pop("attr") attr_hist.pop("attr")
if record_id not in res: if record_id not in res:
record_dict = record.OperationRecord.to_dict() record_dict = record.OperationRecord.to_dict()
record_dict['show_attr_value'] = show_attr_set.get(ci_id)
record_dict["user"] = UserCache.get(record_dict.get("uid")) record_dict["user"] = UserCache.get(record_dict.get("uid"))
if record_dict["user"]: if record_dict["user"]:
record_dict['user'] = record_dict['user'].nickname record_dict['user'] = record_dict['user'].nickname
@ -163,7 +134,7 @@ class AttributeHistoryManger(object):
from api.lib.cmdb.ci import CIManager from api.lib.cmdb.ci import CIManager
cis = CIManager().get_cis_by_ids(list(ci_ids), cis = CIManager().get_cis_by_ids(list(ci_ids),
unique_required=True) unique_required=True)
cis = {i['_id']: i for i in cis if i} cis = {i['_id']: i for i in cis}
return total, res, cis return total, res, cis
@ -189,14 +160,12 @@ class AttributeHistoryManger(object):
record = i.OperationRecord record = i.OperationRecord
item = dict(attr_name=attr.name, item = dict(attr_name=attr.name,
attr_alias=attr.alias, attr_alias=attr.alias,
value_type=attr.value_type,
operate_type=hist.operate_type, operate_type=hist.operate_type,
username=user and user.nickname, username=user and user.nickname,
old=hist.old, old=hist.old,
new=hist.new, new=hist.new,
created_at=record.created_at.strftime('%Y-%m-%d %H:%M:%S'), created_at=record.created_at.strftime('%Y-%m-%d %H:%M:%S'),
record_id=record.id, record_id=record.id,
ticket_id=record.ticket_id,
hid=hist.id hid=hist.id
) )
result.append(item) result.append(item)
@ -230,9 +199,9 @@ class AttributeHistoryManger(object):
return username, timestamp, attr_dict, rel_dict return username, timestamp, attr_dict, rel_dict
@staticmethod @staticmethod
def add(record_id, ci_id, history_list, type_id=None, ticket_id=None, flush=False, commit=True): def add(record_id, ci_id, history_list, type_id=None, flush=False, commit=True):
if record_id is None: if record_id is None:
record = OperationRecord.create(uid=current_user.uid, type_id=type_id, ticket_id=ticket_id) record = OperationRecord.create(uid=current_user.uid, type_id=type_id)
record_id = record.id record_id = record.id
for attr_id, operate_type, old, new in history_list or []: for attr_id, operate_type, old, new in history_list or []:
@ -250,8 +219,8 @@ class AttributeHistoryManger(object):
class CIRelationHistoryManager(object): class CIRelationHistoryManager(object):
@staticmethod @staticmethod
def add(rel_obj, operate_type=OperateType.ADD, uid=None): def add(rel_obj, operate_type=OperateType.ADD):
record = OperationRecord.create(uid=uid or current_user.uid) record = OperationRecord.create(uid=current_user.uid)
CIRelationHistory.create(relation_id=rel_obj.id, CIRelationHistory.create(relation_id=rel_obj.id,
record_id=record.id, record_id=record.id,
@ -300,7 +269,7 @@ class CITypeHistoryManager(object):
return numfound, result return numfound, result
@staticmethod @staticmethod
def add(operate_type, type_id, attr_id=None, trigger_id=None, unique_constraint_id=None, change=None, rc_id=None): def add(operate_type, type_id, attr_id=None, trigger_id=None, unique_constraint_id=None, change=None):
if type_id is None and attr_id is not None: if type_id is None and attr_id is not None:
from api.models.cmdb import CITypeAttribute from api.models.cmdb import CITypeAttribute
type_ids = [i.type_id for i in CITypeAttribute.get_by(attr_id=attr_id, to_dict=False)] type_ids = [i.type_id for i in CITypeAttribute.get_by(attr_id=attr_id, to_dict=False)]
@ -313,73 +282,7 @@ class CITypeHistoryManager(object):
uid=current_user.uid, uid=current_user.uid,
attr_id=attr_id, attr_id=attr_id,
trigger_id=trigger_id, trigger_id=trigger_id,
rc_id=rc_id,
unique_constraint_id=unique_constraint_id, unique_constraint_id=unique_constraint_id,
change=change) change=change)
CITypeHistory.create(**payload) 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.join(CI, CI.id == CITriggerHistory.ci_id).filter(CI.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

@ -1 +0,0 @@
# -*- coding:utf-8 -*-

View File

@ -1,132 +0,0 @@
# -*- coding:utf-8 -*-
import redis_lock
from flask import abort
from api.extensions import rd
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 BuiltinModelEnum
from api.lib.cmdb.ipam.const import IPAddressAssignStatus
from api.lib.cmdb.ipam.const import IPAddressBuiltinAttributes
from api.lib.cmdb.ipam.const import OperateTypeEnum
from api.lib.cmdb.ipam.const import SubnetBuiltinAttributes
from api.lib.cmdb.ipam.history import OperateHistoryManager
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.search.ci.db.search import Search as SearchFromDB
from api.lib.cmdb.search.ci_relation.search import Search as RelationSearch
class IpAddressManager(object):
def __init__(self):
self.ci_type = CITypeCache.get(BuiltinModelEnum.IPAM_ADDRESS) or abort(
404, ErrFormat.ipam_address_model_not_found.format(BuiltinModelEnum.IPAM_ADDRESS))
self.type_id = self.ci_type.id
@staticmethod
def list_ip_address(parent_id):
numfound, _, result = CIRelationManager.get_second_cis(parent_id, per_page="all")
return numfound, result
def _get_cis(self, subnet_id, ips):
q = "_type:{},{}:({})".format(self.type_id, IPAddressBuiltinAttributes.IP, ";".join(ips or []))
response, _, _, _, _, _ = RelationSearch([subnet_id], level=[1], query=q, count=1000000).search()
return response
@staticmethod
def _add_relation(parent_id, child_id):
if not parent_id or not child_id:
return
CIRelationManager().add(parent_id, child_id, valid=False, apply_async=False)
@staticmethod
def calc_used_count(subnet_id):
q = "{}:(0;2),-{}:true".format(IPAddressBuiltinAttributes.ASSIGN_STATUS, IPAddressBuiltinAttributes.IS_USED)
return len(set(RelationSearch([subnet_id], level=[1], query=q, count=1000000).search(only_ids=True) or []))
@staticmethod
def _calc_assign_count(subnet_id):
q = "{}:(0;2)".format(IPAddressBuiltinAttributes.ASSIGN_STATUS)
return len(set(RelationSearch([subnet_id], level=[1], query=q, count=1000000).search(only_ids=True) or []))
def _update_subnet_count(self, subnet_id, assign_count_computed, used_count=None):
payload = {}
cur = CIManager.get_ci_by_id(subnet_id, need_children=False)
if assign_count_computed:
payload[SubnetBuiltinAttributes.ASSIGN_COUNT] = self._calc_assign_count(subnet_id)
if used_count is not None:
payload[SubnetBuiltinAttributes.USED_COUNT] = used_count
payload[SubnetBuiltinAttributes.FREE_COUNT] = (cur[SubnetBuiltinAttributes.HOSTS_COUNT] -
self.calc_used_count(subnet_id))
CIManager().update(subnet_id, **payload)
def assign_ips(self, ips, subnet_id, cidr, **kwargs):
"""
:param ips: ip list
:param subnet_id: subnet id
:param cidr: subnet cidr
:param kwargs: other attributes for ip address
:return:
"""
if subnet_id is not None:
subnet = CIManager.get_ci_by_id(subnet_id)
else:
cis, _, _, _, _, _ = SearchFromDB("_type:{},{}:{}".format(
BuiltinModelEnum.IPAM_SUBNET, SubnetBuiltinAttributes.CIDR, cidr),
parent_node_perm_passed=True).search()
if cis:
subnet = cis[0]
subnet_id = subnet['_id']
else:
return abort(400, ErrFormat.ipam_address_model_not_found)
with (redis_lock.Lock(rd.r, "IPAM_ASSIGN_ADDRESS_{}".format(subnet_id), expire=10)):
cis = self._get_cis(subnet_id, ips)
ip2ci = {ci[IPAddressBuiltinAttributes.IP]: ci for ci in cis}
ci_ids = []
for ip in ips:
kwargs['name'] = ip
kwargs[IPAddressBuiltinAttributes.IP] = ip
if ip not in ip2ci:
ci_id = CIManager.add(self.type_id, _sync=True, **kwargs)
else:
ci_id = ip2ci[ip]['_id']
CIManager().update(ci_id, _sync=True, **kwargs)
ci_ids.append(ci_id)
self._add_relation(subnet_id, ci_id)
if ips and IPAddressBuiltinAttributes.ASSIGN_STATUS in kwargs:
self._update_subnet_count(subnet_id, True)
if ips and IPAddressBuiltinAttributes.IS_USED in kwargs:
q = "{}:true".format(IPAddressBuiltinAttributes.IS_USED)
cur_used_ids = RelationSearch([subnet_id], level=[1], query=q).search(only_ids=True)
for _id in set(cur_used_ids) - set(ci_ids):
CIManager().update(_id, **{IPAddressBuiltinAttributes.IS_USED: False})
self._update_subnet_count(subnet_id, False, used_count=len(ips))
if kwargs.get(IPAddressBuiltinAttributes.ASSIGN_STATUS) in (
IPAddressAssignStatus.ASSIGNED, IPAddressAssignStatus.RESERVED):
OperateHistoryManager().add(operate_type=OperateTypeEnum.ASSIGN_ADDRESS,
cidr=subnet.get(SubnetBuiltinAttributes.CIDR),
description=" | ".join(ips))
elif kwargs.get(IPAddressBuiltinAttributes.ASSIGN_STATUS) == IPAddressAssignStatus.UNASSIGNED:
OperateHistoryManager().add(operate_type=OperateTypeEnum.REVOKE_ADDRESS,
cidr=subnet.get(SubnetBuiltinAttributes.CIDR),
description=" | ".join(ips))

View File

@ -1,35 +0,0 @@
# -*- coding:utf-8 -*-
from api.lib.utils import BaseEnum
class IPAddressAssignStatus(BaseEnum):
ASSIGNED = 0
UNASSIGNED = 1
RESERVED = 2
class OperateTypeEnum(BaseEnum):
ADD_SCOPE = "0"
UPDATE_SCOPE = "1"
DELETE_SCOPE = "2"
ADD_SUBNET = "3"
UPDATE_SUBNET = "4"
DELETE_SUBNET = "5"
ASSIGN_ADDRESS = "6"
REVOKE_ADDRESS = "7"
class SubnetBuiltinAttributes(BaseEnum):
NAME = 'name'
CIDR = 'cidr'
HOSTS_COUNT = 'hosts_count'
ASSIGN_COUNT = 'assign_count'
USED_COUNT = 'used_count'
FREE_COUNT = 'free_count'
class IPAddressBuiltinAttributes(BaseEnum):
IP = 'ip'
ASSIGN_STATUS = 'assign_status' # enum: 0 - assigned 1 - unassigned 2 - reserved
IS_USED = 'is_used' # bool

View File

@ -1,61 +0,0 @@
# -*- coding:utf-8 -*-
from flask_login import current_user
from api.lib.cmdb.ipam.const import IPAddressBuiltinAttributes
from api.lib.mixin import DBMixin
from api.models.cmdb import IPAMOperationHistory
from api.models.cmdb import IPAMSubnetScan
from api.models.cmdb import IPAMSubnetScanHistory
class OperateHistoryManager(DBMixin):
cls = IPAMOperationHistory
def _can_add(self, **kwargs):
kwargs['uid'] = current_user.uid
return kwargs
def _can_update(self, **kwargs):
pass
def _can_delete(self, **kwargs):
pass
class ScanHistoryManager(DBMixin):
cls = IPAMSubnetScanHistory
def _can_add(self, **kwargs):
return kwargs
def add(self, **kwargs):
kwargs.pop('_key', None)
kwargs.pop('_secret', None)
ci_id = kwargs.pop('ci_id', None)
existed = self.cls.get_by(exec_id=kwargs['exec_id'], first=True, to_dict=False)
if existed is None:
self.cls.create(**kwargs)
else:
existed.update(**kwargs)
if kwargs.get('ips'):
from api.lib.cmdb.ipam.address import IpAddressManager
IpAddressManager().assign_ips(kwargs['ips'], ci_id, kwargs.get('cidr'),
**{IPAddressBuiltinAttributes.IS_USED: 1})
scan_rule = IPAMSubnetScan.get_by(ci_id=ci_id, first=True, to_dict=False)
if scan_rule is not None:
scan_rule.update(last_scan_time=kwargs.get('start_at'))
for i in self.cls.get_by(subnet_scan_id=kwargs.get('subnet_scan_id'), only_query=True).order_by(
self.cls.id.desc()).offset(100):
i.delete()
def _can_update(self, **kwargs):
pass
def _can_delete(self, **kwargs):
pass

View File

@ -1,104 +0,0 @@
# -*- coding:utf-8 -*-
import json
from flask import abort
from api.extensions import rd
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.ci import CIManager
from api.lib.cmdb.const import BuiltinModelEnum
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.search.ci.db.search import Search as SearchFromDB
from api.models.cmdb import CI
from api.models.cmdb import CIRelation
from api.models.cmdb import IPAMSubnetScan
class Stats(object):
def __init__(self):
self.address_type = CITypeCache.get(BuiltinModelEnum.IPAM_ADDRESS) or abort(
404, ErrFormat.ipam_address_model_not_found.format(BuiltinModelEnum.IPAM_ADDRESS))
self.address_type_id = self.address_type.id
self.subnet_type = CITypeCache.get(BuiltinModelEnum.IPAM_SUBNET) or abort(
404, ErrFormat.ipam_address_model_not_found.format(BuiltinModelEnum.IPAM_ADDRESS))
self.subnet_type_id = self.subnet_type.id
def leaf_nodes(self, parent_id):
if str(parent_id) == '0': # all
ci_ids = [i.id for i in CI.get_by(type_id=self.subnet_type_id, to_dict=False)]
has_children_ci_ids = [i.first_ci_id for i in CIRelation.get_by(
only_query=True).join(CI, CIRelation.second_ci_id == CI.id).filter(
CIRelation.first_ci_id.in_(ci_ids)).filter(CI.type_id == self.subnet_type_id)]
return list(set(ci_ids) - set(has_children_ci_ids))
else:
_type = CIManager().get_by_id(parent_id)
if not _type:
return abort(404, ErrFormat.ipam_subnet_not_found)
key = [(str(parent_id), _type.type_id)]
result = []
while True:
res = [json.loads(x).items() for x in [i or '{}' for i in rd.get(
[i[0] for i in key], REDIS_PREFIX_CI_RELATION) or []]]
for idx, i in enumerate(res):
if (not i or list(i)[0][1] == self.address_type_id) and key[idx][1] == self.subnet_type_id:
result.append(int(key[idx][0]))
res = [j for i in res for j in i] # [(id, type_id)]
if not res:
return result
key = res
def statistic_subnets(self, subnet_ids):
if subnet_ids:
response, _, _, _, _, _ = SearchFromDB(
"_type:{}".format(self.subnet_type_id),
ci_ids=subnet_ids,
count=1000000,
parent_node_perm_passed=True,
).search()
else:
response = []
scans = IPAMSubnetScan.get_by(only_query=True).filter(IPAMSubnetScan.ci_id.in_(list(map(int, subnet_ids))))
id2scan = {i.ci_id: i for i in scans}
address_num, address_free_num, address_assign_num, address_used_num = 0, 0, 0, 0
for subnet in response:
address_num += (subnet.get('hosts_count') or 0)
address_free_num += (subnet.get('free_count') or 0)
address_assign_num += (subnet.get('assign_count') or 0)
address_used_num += (subnet.get('used_count') or 0)
if id2scan.get(subnet['_id']):
subnet['scan_enabled'] = id2scan[subnet['_id']].scan_enabled
subnet['last_scan_time'] = id2scan[subnet['_id']].last_scan_time
else:
subnet['scan_enabled'] = False
subnet['last_scan_time'] = None
return response, address_num, address_free_num, address_assign_num, address_used_num
def summary(self, parent_id):
subnet_ids = self.leaf_nodes(parent_id)
subnets, address_num, address_free_num, address_assign_num, address_used_num = (
self.statistic_subnets(subnet_ids))
return dict(subnet_num=len(subnets),
address_num=address_num,
address_free_num=address_free_num,
address_assign_num=address_assign_num,
address_unassign_num=address_num - address_assign_num,
address_used_num=address_used_num,
address_used_free_num=address_num - address_used_num,
subnets=subnets)

View File

@ -1,355 +0,0 @@
# -*- coding:utf-8 -*-
from collections import defaultdict
import datetime
import ipaddress
from flask import abort
from api.lib.cmdb.cache import AttributeCache
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 BuiltinModelEnum
from api.lib.cmdb.ipam.const import OperateTypeEnum
from api.lib.cmdb.ipam.const import SubnetBuiltinAttributes
from api.lib.cmdb.ipam.history import OperateHistoryManager
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.search.ci.db.search import Search as SearchFromDB
from api.models.cmdb import CI
from api.models.cmdb import CIRelation
from api.models.cmdb import IPAMSubnetScan
class SubnetManager(object):
def __init__(self):
self.ci_type = CITypeCache.get(BuiltinModelEnum.IPAM_SUBNET) or abort(
404, ErrFormat.ipam_subnet_model_not_found.format(BuiltinModelEnum.IPAM_SUBNET))
self.type_id = self.ci_type.id
def scan_rules(self, oneagent_id, last_update_at=None):
result = []
rules = IPAMSubnetScan.get_by(agent_id=oneagent_id, to_dict=True)
ci_ids = [i['ci_id'] for i in rules]
if ci_ids:
response, _, _, _, _, _ = SearchFromDB("_type:{}".format(self.type_id),
ci_ids=list(ci_ids),
count=1000000,
fl=[SubnetBuiltinAttributes.CIDR],
parent_node_perm_passed=True).search()
id2ci = {i['_id']: i for i in response}
for rule in rules:
if rule['ci_id'] in id2ci:
rule[SubnetBuiltinAttributes.CIDR] = id2ci[rule['ci_id']][SubnetBuiltinAttributes.CIDR]
result.append(rule)
new_last_update_at = ""
for i in result:
__last_update_at = max([i['rule_updated_at'] or "", i['created_at'] or ""])
if new_last_update_at < __last_update_at:
new_last_update_at = __last_update_at
if not last_update_at or new_last_update_at > last_update_at:
return result, new_last_update_at
else:
return [], new_last_update_at
@staticmethod
def get_hosts(cidr):
try:
return list(map(str, ipaddress.ip_network(cidr).hosts()))
except ValueError:
return []
def get_by_id(self, subnet_id):
response, _, _, _, _, _ = SearchFromDB("_type:{}".format(self.type_id),
ci_ids=[subnet_id],
parent_node_perm_passed=True).search()
scan_rule = IPAMSubnetScan.get_by(ci_id=subnet_id, first=True, to_dict=True)
if scan_rule and response:
scan_rule.update(response[0])
return scan_rule
def tree_view(self):
scope = CITypeCache.get(BuiltinModelEnum.IPAM_SCOPE)
ci_types = scope and [scope.id, self.type_id] or [self.type_id]
relations = defaultdict(set)
ids = set()
has_parent_ids = set()
for i in CIRelation.get_by(only_query=True).join(
CI, CI.id == CIRelation.first_ci_id).filter(CI.type_id.in_(ci_types)):
relations[i.first_ci_id].add(i.second_ci_id)
ids.add(i.first_ci_id)
ids.add(i.second_ci_id)
has_parent_ids.add(i.second_ci_id)
for i in CIRelation.get_by(only_query=True).join(
CI, CI.id == CIRelation.second_ci_id).filter(CI.type_id.in_(ci_types)):
relations[i.first_ci_id].add(i.second_ci_id)
ids.add(i.first_ci_id)
ids.add(i.second_ci_id)
has_parent_ids.add(i.second_ci_id)
for i in CI.get_by(only_query=True).filter(CI.type_id.in_(ci_types)):
ids.add(i.id)
for _id in ids:
if _id not in has_parent_ids:
relations[None].add(_id)
type2name = dict()
type2name[self.type_id] = AttributeCache.get(self.ci_type.show_id or self.ci_type.unique_id).name
fl = [type2name[self.type_id]]
if scope:
type2name[scope.id] = AttributeCache.get(scope.show_id or scope.unique_id).name
fl.append(type2name[scope.id])
response, _, _, _, _, _ = SearchFromDB("_type:({})".format(";".join(map(str, ci_types))),
ci_ids=list(ids),
count=1000000,
fl=list(set(fl + [SubnetBuiltinAttributes.CIDR])),
parent_node_perm_passed=True).search()
id2ci = {i['_id']: i for i in response}
def _build_tree(_tree, parent_id=None):
tree = []
for child_id in _tree.get(parent_id, []):
children = sorted(_build_tree(_tree, child_id), key=lambda x: x['_id'])
if not id2ci.get(child_id):
continue
tree.append({'children': children, **id2ci[child_id]})
return tree
result = sorted(_build_tree(relations), key=lambda x: x['_id'])
return result, type2name
@staticmethod
def _is_valid_cidr(cidr):
try:
cidr = ipaddress.ip_network(cidr)
if not (8 <= cidr.prefixlen <= 31):
raise ValueError
return str(cidr)
except ValueError:
return abort(400, ErrFormat.ipam_cidr_invalid_notation.format(cidr))
def _check_root_node_is_overlapping(self, cidr, _id=None):
none_root_nodes = [i.id for i in CI.get_by(only_query=True).join(
CIRelation, CIRelation.second_ci_id == CI.id).filter(CI.type_id == self.type_id)]
all_nodes = [i.id for i in CI.get_by(type_id=self.type_id, to_dict=False, fl=['id'])]
root_nodes = set(all_nodes) - set(none_root_nodes) - set(_id and [_id] or [])
response, _, _, _, _, _ = SearchFromDB("_type:{}".format(self.type_id),
ci_ids=list(root_nodes),
count=1000000,
parent_node_perm_passed=True).search()
cur_subnet = ipaddress.ip_network(cidr)
for item in response:
if item['_id'] == _id:
continue
if cur_subnet.overlaps(ipaddress.ip_network(item.get(SubnetBuiltinAttributes.CIDR))):
return abort(400, ErrFormat.ipam_subnet_overlapped.format(cidr, item.get(SubnetBuiltinAttributes.CIDR)))
return cidr
def _check_child_node_is_overlapping(self, parent_id, cidr, _id=None):
child_nodes = [i.second_ci_id for i in CIRelation.get_by(
first_ci_id=parent_id, to_dict=False, fl=['second_ci_id']) if i.second_ci_id != _id]
if not child_nodes:
return
response, _, _, _, _, _ = SearchFromDB("_type:{}".format(self.type_id),
ci_ids=list(child_nodes),
count=1000000,
parent_node_perm_passed=True).search()
cur_subnet = ipaddress.ip_network(cidr)
for item in response:
if item['_id'] == _id:
continue
if cur_subnet.overlaps(ipaddress.ip_network(item.get(SubnetBuiltinAttributes.CIDR))):
return abort(400, ErrFormat.ipam_subnet_overlapped.format(cidr, item.get(SubnetBuiltinAttributes.CIDR)))
def validate_cidr(self, parent_id, cidr, _id=None):
cidr = self._is_valid_cidr(cidr)
if not parent_id:
return self._check_root_node_is_overlapping(cidr, _id)
parent_subnet = CIManager().get_ci_by_id(parent_id, need_children=False)
if parent_subnet['ci_type'] == BuiltinModelEnum.IPAM_SUBNET:
if parent_subnet.get(SubnetBuiltinAttributes.CIDR):
prefix = int(cidr.split('/')[1])
if int(parent_subnet[SubnetBuiltinAttributes.CIDR].split('/')[1]) >= prefix:
return abort(400, ErrFormat.ipam_subnet_prefix_length_invalid.format(prefix))
valid_subnets = [str(i) for i in
ipaddress.ip_network(parent_subnet[SubnetBuiltinAttributes.CIDR]).subnets(
new_prefix=prefix)]
if cidr not in valid_subnets:
return abort(400, ErrFormat.ipam_cidr_invalid_subnet.format(cidr, valid_subnets))
else:
return abort(400, ErrFormat.ipam_parent_subnet_node_cidr_cannot_empty)
self._check_child_node_is_overlapping(parent_id, cidr, _id)
return cidr
def _add_subnet(self, cidr, **kwargs):
kwargs[SubnetBuiltinAttributes.HOSTS_COUNT] = len(list(ipaddress.ip_network(cidr).hosts()))
kwargs[SubnetBuiltinAttributes.USED_COUNT] = 0
kwargs[SubnetBuiltinAttributes.ASSIGN_COUNT] = 0
kwargs[SubnetBuiltinAttributes.FREE_COUNT] = kwargs[SubnetBuiltinAttributes.HOSTS_COUNT]
return CIManager().add(self.type_id, cidr=cidr, **kwargs)
@staticmethod
def _add_scan_rule(ci_id, agent_id, cron, scan_enabled=True):
IPAMSubnetScan.create(ci_id=ci_id, agent_id=agent_id, cron=cron, scan_enabled=scan_enabled)
@staticmethod
def _add_relation(parent_id, child_id):
if not parent_id or not child_id:
return
CIRelationManager().add(parent_id, child_id, valid=False)
def add(self, cidr, parent_id, agent_id, cron, scan_enabled=True, **kwargs):
cidr = self.validate_cidr(parent_id, cidr)
ci_id = self._add_subnet(cidr, **kwargs)
self._add_scan_rule(ci_id, agent_id, cron, scan_enabled)
self._add_relation(parent_id, ci_id)
OperateHistoryManager().add(operate_type=OperateTypeEnum.ADD_SUBNET,
cidr=cidr,
description=cidr)
return ci_id
@staticmethod
def _update_subnet(_id, **kwargs):
return CIManager().update(_id, **kwargs)
@staticmethod
def _update_scan_rule(ci_id, agent_id, cron, scan_enabled=True):
existed = IPAMSubnetScan.get_by(ci_id=ci_id, first=True, to_dict=False)
if existed is not None:
existed.update(ci_id=ci_id, agent_id=agent_id, cron=cron, scan_enabled=scan_enabled,
rule_updated_at=datetime.datetime.now())
else:
IPAMSubnetScan.create(ci_id=ci_id, agent_id=agent_id, cron=cron, scan_enabled=scan_enabled)
def update(self, _id, **kwargs):
kwargs[SubnetBuiltinAttributes.CIDR] = self.validate_cidr(kwargs.pop('parent_id', None),
kwargs.get(SubnetBuiltinAttributes.CIDR), _id)
agent_id = kwargs.pop('agent_id', None)
cron = kwargs.pop('cron', None)
scan_enabled = kwargs.pop('scan_enabled', True)
cur = CIManager.get_ci_by_id(_id, need_children=False)
self._update_subnet(_id, **kwargs)
self._update_scan_rule(_id, agent_id, cron, scan_enabled)
OperateHistoryManager().add(operate_type=OperateTypeEnum.UPDATE_SUBNET,
cidr=cur.get(SubnetBuiltinAttributes.CIDR),
description="{} -> {}".format(cur.get(SubnetBuiltinAttributes.CIDR),
kwargs.get(SubnetBuiltinAttributes.CIDR)))
return _id
@classmethod
def delete(cls, _id):
if CIRelation.get_by(only_query=True).filter(CIRelation.first_ci_id == _id).first():
return abort(400, ErrFormat.ipam_subnet_cannot_delete)
existed = IPAMSubnetScan.get_by(ci_id=_id, first=True, to_dict=False)
existed and existed.delete()
delete_ci_ids = []
for i in CIRelation.get_by(first_ci_id=_id, to_dict=False):
delete_ci_ids.append(i.second_ci_id)
i.delete()
cur = CIManager.get_ci_by_id(_id, need_children=False)
CIManager().delete(_id)
OperateHistoryManager().add(operate_type=OperateTypeEnum.DELETE_SUBNET,
cidr=cur.get(SubnetBuiltinAttributes.CIDR),
description=cur.get(SubnetBuiltinAttributes.CIDR))
# batch_delete_ci.apply_async(args=(delete_ci_ids,))
return _id
class SubnetScopeManager(object):
def __init__(self):
self.ci_type = CITypeCache.get(BuiltinModelEnum.IPAM_SCOPE)
not self.ci_type and abort(400, ErrFormat.ipam_subnet_model_not_found.format(
BuiltinModelEnum.IPAM_SCOPE))
self.type_id = self.ci_type.id
def _add_scope(self, name):
return CIManager().add(self.type_id, name=name)
@staticmethod
def _add_relation(parent_id, child_id):
if not parent_id or not child_id:
return
CIRelationManager().add(parent_id, child_id, valid=False)
def add(self, parent_id, name):
ci_id = self._add_scope(name)
self._add_relation(parent_id, ci_id)
OperateHistoryManager().add(operate_type=OperateTypeEnum.ADD_SCOPE,
description=name)
return ci_id
@staticmethod
def _update_scope(_id, name):
return CIManager().update(_id, name=name)
def update(self, _id, name):
cur = CIManager.get_ci_by_id(_id, need_children=False)
res = self._update_scope(_id, name)
OperateHistoryManager().add(operate_type=OperateTypeEnum.UPDATE_SCOPE,
description="{} -> {}".format(cur.get('name'), name))
return res
@staticmethod
def delete(_id):
if CIRelation.get_by(first_ci_id=_id, first=True, to_dict=False):
return abort(400, ErrFormat.ipam_scope_cannot_delete)
cur = CIManager.get_ci_by_id(_id, need_children=False)
CIManager().delete(_id)
OperateHistoryManager().add(operate_type=OperateTypeEnum.DELETE_SCOPE,
description=cur.get('name'))
return _id

View File

@ -1,15 +1,12 @@
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
import copy
import functools import functools
import redis_lock
from flask import abort from flask import abort
from flask import current_app from flask import current_app
from flask import request from flask import request
from flask_login import current_user from flask_login import current_user
from api.extensions import db
from api.extensions import rd
from api.lib.cmdb.const import BUILTIN_ATTRIBUTES
from api.lib.cmdb.const import ResourceTypeEnum from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.resp_format import ErrFormat from api.lib.cmdb.resp_format import ErrFormat
from api.lib.mixin import DBMixin from api.lib.mixin import DBMixin
@ -27,7 +24,7 @@ class CIFilterPermsCRUD(DBMixin):
result = {} result = {}
for i in res: for i in res:
if i['attr_filter']: if i['attr_filter']:
i['attr_filter'] = i['attr_filter'].split(',') + list(BUILTIN_ATTRIBUTES.keys()) i['attr_filter'] = i['attr_filter'].split(',')
if i['rid'] not in result: if i['rid'] not in result:
result[i['rid']] = i result[i['rid']] = i
@ -43,11 +40,6 @@ class CIFilterPermsCRUD(DBMixin):
result[i['rid']]['ci_filter'] = "" result[i['rid']]['ci_filter'] = ""
result[i['rid']]['ci_filter'] += (i['ci_filter'] or "") result[i['rid']]['ci_filter'] += (i['ci_filter'] or "")
if i['id_filter']:
if not result[i['rid']]['id_filter']:
result[i['rid']]['id_filter'] = {}
result[i['rid']]['id_filter'].update(i['id_filter'] or {})
return result return result
def get_by_ids(self, _ids, type_id=None): def get_by_ids(self, _ids, type_id=None):
@ -62,7 +54,7 @@ class CIFilterPermsCRUD(DBMixin):
result = {} result = {}
for i in res: for i in res:
if i['attr_filter']: if i['attr_filter']:
i['attr_filter'] = i['attr_filter'].split(',') + list(BUILTIN_ATTRIBUTES.keys()) i['attr_filter'] = i['attr_filter'].split(',')
if i['type_id'] not in result: if i['type_id'] not in result:
result[i['type_id']] = i result[i['type_id']] = i
@ -78,11 +70,6 @@ class CIFilterPermsCRUD(DBMixin):
result[i['type_id']]['ci_filter'] = "" result[i['type_id']]['ci_filter'] = ""
result[i['type_id']]['ci_filter'] += (i['ci_filter'] or "") result[i['type_id']]['ci_filter'] += (i['ci_filter'] or "")
if i['id_filter']:
if not result[i['type_id']]['id_filter']:
result[i['type_id']]['id_filter'] = {}
result[i['type_id']]['id_filter'].update(i['id_filter'] or {})
return result return result
@classmethod @classmethod
@ -95,54 +82,6 @@ class CIFilterPermsCRUD(DBMixin):
type2filter_perms = cls().get_by_ids(list(map(int, [i['name'] for i in res2])), type_id=type_id) type2filter_perms = cls().get_by_ids(list(map(int, [i['name'] for i in res2])), type_id=type_id)
return type2filter_perms.get(type_id, {}).get('attr_filter') or [] return type2filter_perms.get(type_id, {}).get('attr_filter') or []
def _revoke_children(self, rid, id_filter, rebuild=True):
items = self.cls.get_by(rid=rid, ci_filter=None, attr_filter=None, to_dict=False)
for item in items:
changed, item_id_filter = False, copy.deepcopy(item.id_filter)
for prefix in id_filter:
for k, v in copy.deepcopy((item.id_filter or {})).items():
if k.startswith(prefix) and k != prefix:
item_id_filter.pop(k)
changed = True
if not item_id_filter and current_app.config.get('USE_ACL'):
item.soft_delete(commit=False)
ACLManager().del_resource(str(item.id), ResourceTypeEnum.CI_FILTER, rebuild=rebuild)
elif changed:
item.update(id_filter=item_id_filter, commit=False)
db.session.commit()
def _revoke_parent(self, rid, parent_path, rebuild=True):
parent_path = [i for i in parent_path.split(',') if i] or []
revoke_nodes = [','.join(parent_path[:i]) for i in range(len(parent_path), 0, -1)]
for node_path in revoke_nodes:
delete_item, can_deleted = None, True
items = self.cls.get_by(rid=rid, ci_filter=None, attr_filter=None, to_dict=False)
for item in items:
if node_path in item.id_filter:
delete_item = item
if any(filter(lambda x: x.startswith(node_path) and x != node_path, item.id_filter.keys())):
can_deleted = False
break
if can_deleted and delete_item:
id_filter = copy.deepcopy(delete_item.id_filter)
id_filter.pop(node_path)
delete_item = delete_item.update(id_filter=id_filter, filter_none=False)
if current_app.config.get('USE_ACL') and not id_filter:
ACLManager().del_resource(str(delete_item.id), ResourceTypeEnum.CI_FILTER, rebuild=False)
delete_item.soft_delete()
items.remove(delete_item)
if rebuild:
from api.tasks.acl import role_rebuild
from api.lib.perm.acl.const import ACL_QUEUE
from api.lib.perm.acl.cache import AppCache
role_rebuild.apply_async(args=(rid, AppCache.get('cmdb').id), queue=ACL_QUEUE)
def _can_add(self, **kwargs): def _can_add(self, **kwargs):
ci_filter = kwargs.get('ci_filter') ci_filter = kwargs.get('ci_filter')
attr_filter = kwargs.get('attr_filter') or "" attr_filter = kwargs.get('attr_filter') or ""
@ -163,67 +102,34 @@ class CIFilterPermsCRUD(DBMixin):
def add(self, **kwargs): def add(self, **kwargs):
kwargs = self._can_add(**kwargs) or kwargs kwargs = self._can_add(**kwargs) or kwargs
with redis_lock.Lock(rd.r, 'CMDB_FILTER_{}_{}'.format(kwargs['type_id'], kwargs['rid']), expire=10):
request_id_filter = {}
if kwargs.get('id_filter'):
obj = self.cls.get_by(type_id=kwargs.get('type_id'),
rid=kwargs.get('rid'),
ci_filter=None,
attr_filter=None,
first=True, to_dict=False)
for _id, v in (kwargs.get('id_filter') or {}).items(): obj = self.cls.get_by(type_id=kwargs.get('type_id'),
key = ",".join(([v['parent_path']] if v.get('parent_path') else []) + [str(_id)]) rid=kwargs.get('rid'),
request_id_filter[key] = v['name'] first=True, to_dict=False)
if obj is not None:
obj = obj.update(filter_none=False, **kwargs)
if not obj.attr_filter and not obj.ci_filter:
if current_app.config.get('USE_ACL'):
ACLManager().del_resource(str(obj.id), ResourceTypeEnum.CI_FILTER)
else: obj.soft_delete()
obj = self.cls.get_by(type_id=kwargs.get('type_id'),
rid=kwargs.get('rid'),
id_filter=None,
first=True, to_dict=False)
is_recursive = kwargs.pop('is_recursive', 0)
if obj is not None:
if obj.id_filter and isinstance(kwargs.get('id_filter'), dict):
obj_id_filter = copy.deepcopy(obj.id_filter)
for k, v in request_id_filter.items():
obj_id_filter[k] = v
kwargs['id_filter'] = obj_id_filter
obj = obj.update(filter_none=False, **kwargs)
if not obj.attr_filter and not obj.ci_filter and not obj.id_filter:
if current_app.config.get('USE_ACL'):
ACLManager().del_resource(str(obj.id), ResourceTypeEnum.CI_FILTER, rebuild=False)
obj.soft_delete()
if not is_recursive and request_id_filter:
self._revoke_children(obj.rid, request_id_filter, rebuild=False)
else:
if not kwargs.get('ci_filter') and not kwargs.get('attr_filter'):
return return
else: obj = self.cls.create(**kwargs)
if not kwargs.get('ci_filter') and not kwargs.get('attr_filter') and not kwargs.get('id_filter'):
return
if request_id_filter: if current_app.config.get('USE_ACL'):
kwargs['id_filter'] = request_id_filter try:
ACLManager().add_resource(obj.id, ResourceTypeEnum.CI_FILTER)
except:
pass
ACLManager().grant_resource_to_role_by_rid(obj.id,
kwargs.get('rid'),
ResourceTypeEnum.CI_FILTER)
obj = self.cls.create(**kwargs) return obj
if current_app.config.get('USE_ACL'): # new resource
try:
ACLManager().add_resource(obj.id, ResourceTypeEnum.CI_FILTER)
except:
pass
ACLManager().grant_resource_to_role_by_rid(obj.id,
kwargs.get('rid'),
ResourceTypeEnum.CI_FILTER)
return obj
def _can_update(self, **kwargs): def _can_update(self, **kwargs):
pass pass
@ -232,83 +138,15 @@ class CIFilterPermsCRUD(DBMixin):
pass pass
def delete(self, **kwargs): def delete(self, **kwargs):
with redis_lock.Lock(rd.r, 'CMDB_FILTER_{}_{}'.format(kwargs['type_id'], kwargs['rid']), expire=10): obj = self.cls.get_by(type_id=kwargs.get('type_id'),
obj = self.cls.get_by(type_id=kwargs.get('type_id'), rid=kwargs.get('rid'),
rid=kwargs.get('rid'), first=True, to_dict=False)
id_filter=None,
first=True, to_dict=False)
if obj is not None: if obj is not None:
resource = None if current_app.config.get('USE_ACL'):
if current_app.config.get('USE_ACL'): ACLManager().del_resource(str(obj.id), ResourceTypeEnum.CI_FILTER)
resource = ACLManager().del_resource(str(obj.id), ResourceTypeEnum.CI_FILTER)
obj.soft_delete() obj.soft_delete()
return resource
def delete2(self, **kwargs):
with redis_lock.Lock(rd.r, 'CMDB_FILTER_{}_{}'.format(kwargs['type_id'], kwargs['rid']), expire=10):
obj = self.cls.get_by(type_id=kwargs.get('type_id'),
rid=kwargs.get('rid'),
ci_filter=None,
attr_filter=None,
first=True, to_dict=False)
request_id_filter = {}
for _id, v in (kwargs.get('id_filter') or {}).items():
key = ",".join(([v['parent_path']] if v.get('parent_path') else []) + [str(_id)])
request_id_filter[key] = v['name']
resource = None
if obj is not None:
id_filter = {}
for k, v in copy.deepcopy(obj.id_filter or {}).items(): # important
if k not in request_id_filter:
id_filter[k] = v
if not id_filter and current_app.config.get('USE_ACL'):
resource = ACLManager().del_resource(str(obj.id), ResourceTypeEnum.CI_FILTER, rebuild=False)
obj.soft_delete()
db.session.commit()
else:
obj.update(id_filter=id_filter)
self._revoke_children(kwargs.get('rid'), request_id_filter, rebuild=False)
self._revoke_parent(kwargs.get('rid'), kwargs.get('parent_path'))
return resource
def delete_id_filter_by_ci_id(self, ci_id):
items = self.cls.get_by(ci_filter=None, attr_filter=None, to_dict=False)
rebuild_roles = set()
for item in items:
id_filter = copy.deepcopy(item.id_filter)
changed = False
for node_path in item.id_filter:
if str(ci_id) in node_path:
id_filter.pop(node_path)
changed = True
if changed:
rebuild_roles.add(item.rid)
if not id_filter:
item.soft_delete(commit=False)
else:
item.update(id_filter=id_filter, commit=False)
db.session.commit()
if rebuild_roles:
from api.tasks.acl import role_rebuild
from api.lib.perm.acl.const import ACL_QUEUE
from api.lib.perm.acl.cache import AppCache
for rid in rebuild_roles:
role_rebuild.apply_async(args=(rid, AppCache.get('cmdb').id), queue=ACL_QUEUE)
def has_perm_for_ci(arg_name, resource_type, perm, callback=None, app=None): def has_perm_for_ci(arg_name, resource_type, perm, callback=None, app=None):

View File

@ -1,8 +1,8 @@
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
from collections import defaultdict
import copy import copy
import six import six
import toposort import toposort
from flask import abort from flask import abort
@ -14,22 +14,13 @@ from api.lib.cmdb.attribute import AttributeManager
from api.lib.cmdb.cache import AttributeCache from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import CITypeAttributesCache from api.lib.cmdb.cache import CITypeAttributesCache
from api.lib.cmdb.cache import CITypeCache from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.cache import CMDBCounterCache from api.lib.cmdb.const import PermEnum, ResourceTypeEnum, RoleEnum
from api.lib.cmdb.ci_type import CITypeAttributeManager
from api.lib.cmdb.const import BUILTIN_ATTRIBUTES
from api.lib.cmdb.const import ConstraintEnum
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.const import RoleEnum
from api.lib.cmdb.const import SysComputedAttributes
from api.lib.cmdb.perms import CIFilterPermsCRUD from api.lib.cmdb.perms import CIFilterPermsCRUD
from api.lib.cmdb.resp_format import ErrFormat from api.lib.cmdb.resp_format import ErrFormat
from api.lib.exception import AbortException from api.lib.exception import AbortException
from api.lib.perm.acl.acl import ACLManager from api.lib.perm.acl.acl import ACLManager
from api.models.cmdb import CITypeGroup from api.models.cmdb import CITypeAttribute
from api.models.cmdb import CITypeGroupItem
from api.models.cmdb import CITypeRelation from api.models.cmdb import CITypeRelation
from api.models.cmdb import PreferenceCITypeOrder
from api.models.cmdb import PreferenceRelationView from api.models.cmdb import PreferenceRelationView
from api.models.cmdb import PreferenceSearchOption from api.models.cmdb import PreferenceSearchOption
from api.models.cmdb import PreferenceShowAttributes from api.models.cmdb import PreferenceShowAttributes
@ -44,48 +35,15 @@ class PreferenceManager(object):
@staticmethod @staticmethod
def get_types(instance=False, tree=False): def get_types(instance=False, tree=False):
ci_type_order = sorted(PreferenceCITypeOrder.get_by(uid=current_user.uid, to_dict=False), key=lambda x: x.order)
type2group = {}
for i in db.session.query(CITypeGroupItem, CITypeGroup).join(
CITypeGroup, CITypeGroup.id == CITypeGroupItem.group_id).filter(
CITypeGroup.deleted.is_(False)).filter(CITypeGroupItem.deleted.is_(False)):
type2group[i.CITypeGroupItem.type_id] = i.CITypeGroup.to_dict()
types = db.session.query(PreferenceShowAttributes.type_id).filter( types = db.session.query(PreferenceShowAttributes.type_id).filter(
PreferenceShowAttributes.uid == current_user.uid).filter( PreferenceShowAttributes.uid == current_user.uid).filter(
PreferenceShowAttributes.deleted.is_(False)).group_by( PreferenceShowAttributes.deleted.is_(False)).group_by(
PreferenceShowAttributes.type_id).all() if instance else [] PreferenceShowAttributes.type_id).all() if instance else []
types = sorted(types, key=lambda x: {i.type_id: idx for idx, i in enumerate(
ci_type_order) if not i.is_tree}.get(x.type_id, 1))
group_types = []
other_types = []
group2idx = {}
type_ids = set()
for ci_type in types:
type_id = ci_type.type_id
type_ids.add(type_id)
type_dict = CITypeCache.get(type_id).to_dict()
if type_id not in type2group:
other_types.append(type_dict)
else:
group = type2group[type_id]
if group['id'] not in group2idx:
group_types.append(type2group[type_id])
group2idx[group['id']] = len(group_types) - 1
group_types[group2idx[group['id']]].setdefault('ci_types', []).append(type_dict)
if other_types:
group_types.append(dict(ci_types=other_types))
tree_types = PreferenceTreeView.get_by(uid=current_user.uid, to_dict=False) if tree else [] tree_types = PreferenceTreeView.get_by(uid=current_user.uid, to_dict=False) if tree else []
tree_types = sorted(tree_types, key=lambda x: {i.type_id: idx for idx, i in enumerate( type_ids = set([i.type_id for i in types + tree_types])
ci_type_order) if i.is_tree}.get(x.type_id, 1))
tree_types = [CITypeCache.get(_type.type_id).to_dict() for _type in tree_types] return [CITypeCache.get(type_id).to_dict() for type_id in type_ids]
for _type in tree_types:
type_ids.add(_type['id'])
return dict(group_types=group_types, tree_types=tree_types, type_ids=list(type_ids))
@staticmethod @staticmethod
def get_types2(instance=False, tree=False): def get_types2(instance=False, tree=False):
@ -98,83 +56,67 @@ class PreferenceManager(object):
:param tree: :param tree:
:return: :return:
""" """
result = dict(self=dict(instance=[], tree=[], type_id2subs_time=dict())) result = dict(self=dict(instance=[], tree=[], type_id2subs_time=dict()),
type_id2users=dict())
result.update(CMDBCounterCache.get_sub_counter())
ci_type_order = sorted(PreferenceCITypeOrder.get_by(uid=current_user.uid, to_dict=False), key=lambda x: x.order)
if instance: if instance:
types = db.session.query(PreferenceShowAttributes.type_id, types = db.session.query(PreferenceShowAttributes.type_id,
PreferenceShowAttributes.uid, PreferenceShowAttributes.created_at).filter( PreferenceShowAttributes.uid, PreferenceShowAttributes.created_at).filter(
PreferenceShowAttributes.deleted.is_(False)).filter( PreferenceShowAttributes.deleted.is_(False)).group_by(
PreferenceShowAttributes.uid == current_user.uid).group_by(
PreferenceShowAttributes.uid, PreferenceShowAttributes.type_id) PreferenceShowAttributes.uid, PreferenceShowAttributes.type_id)
for i in types: for i in types:
result['self']['instance'].append(i.type_id) if i.uid == current_user.uid:
if str(i.created_at) > str(result['self']['type_id2subs_time'].get(i.type_id, "")): result['self']['instance'].append(i.type_id)
result['self']['type_id2subs_time'][i.type_id] = i.created_at 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
instance_order = [i.type_id for i in ci_type_order if not i.is_tree] result['type_id2users'].setdefault(i.type_id, []).append(i.uid)
if len(instance_order) == len(result['self']['instance']):
result['self']['instance'] = instance_order
if tree: if tree:
types = PreferenceTreeView.get_by(uid=current_user.uid, to_dict=False) types = PreferenceTreeView.get_by(to_dict=False)
for i in types: for i in types:
result['self']['tree'].append(i.type_id) if i.uid == current_user.uid:
if str(i.created_at) > str(result['self']['type_id2subs_time'].get(i.type_id, "")): result['self']['tree'].append(i.type_id)
result['self']['type_id2subs_time'][i.type_id] = i.created_at 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
tree_order = [i.type_id for i in ci_type_order if i.is_tree] result['type_id2users'].setdefault(i.type_id, [])
if len(tree_order) == len(result['self']['tree']): if i.uid not in result['type_id2users'][i.type_id]:
result['self']['tree'] = tree_order result['type_id2users'][i.type_id].append(i.uid)
return result return result
@staticmethod @staticmethod
def get_show_attributes(type_id): def get_show_attributes(type_id):
_type = CITypeCache.get(type_id) or abort(404, ErrFormat.ci_type_not_found)
type_id = _type and _type.id
if not isinstance(type_id, six.integer_types): if not isinstance(type_id, six.integer_types):
_type = CITypeCache.get(type_id) _type = CITypeCache.get(type_id)
type_id = _type and _type.id type_id = _type and _type.id
attrs = PreferenceShowAttributes.get_by(uid=current_user.uid, type_id=type_id, to_dict=False) attrs = db.session.query(PreferenceShowAttributes, CITypeAttribute.order).join(
CITypeAttribute, CITypeAttribute.attr_id == PreferenceShowAttributes.attr_id).filter(
PreferenceShowAttributes.uid == current_user.uid).filter(
PreferenceShowAttributes.type_id == type_id).filter(
PreferenceShowAttributes.deleted.is_(False)).filter(CITypeAttribute.deleted.is_(False)).filter(
CITypeAttribute.type_id == type_id).all()
result = [] result = []
for i in sorted(attrs, key=lambda x: x.order): for i in sorted(attrs, key=lambda x: x.PreferenceShowAttributes.order):
if i.attr_id: item = i.PreferenceShowAttributes.attr.to_dict()
item = i.attr.to_dict() item.update(dict(is_fixed=i.PreferenceShowAttributes.is_fixed))
elif i.builtin_attr:
item = dict(name=i.builtin_attr, alias=BUILTIN_ATTRIBUTES[i.builtin_attr])
else:
item = dict(name="", alias="")
item.update(dict(is_fixed=i.is_fixed))
result.append(item) result.append(item)
is_subscribed = True is_subscribed = True
if not attrs: if not attrs:
result = CITypeAttributeManager.get_attributes_by_type_id(type_id, attrs = db.session.query(CITypeAttribute).filter(
choice_web_hook_parse=False, CITypeAttribute.type_id == type_id).filter(
choice_other_parse=False) CITypeAttribute.deleted.is_(False)).filter(
result = [i for i in result if i['default_show']] CITypeAttribute.default_show.is_(True)).order_by(CITypeAttribute.order)
result = [i.attr.to_dict() for i in attrs]
for i in BUILTIN_ATTRIBUTES:
result.append(dict(name=i, alias=BUILTIN_ATTRIBUTES[i]))
is_subscribed = False is_subscribed = False
for i in result: for i in result:
if i.get("is_choice"): if i["is_choice"]:
i.update(dict(choice_value=AttributeManager.get_choice_values( i.update(dict(choice_value=AttributeManager.get_choice_values(
i["id"], i["value_type"], i.get("choice_web_hook"), i.get("choice_other")))) i["id"], i["value_type"], i["choice_web_hook"])))
if (_type.name in SysComputedAttributes.type2attr and
i['name'] in SysComputedAttributes.type2attr[_type.name]):
i['sys_computed'] = True
else:
i['sys_computed'] = False
return is_subscribed, result return is_subscribed, result
@ -186,52 +128,29 @@ class PreferenceManager(object):
_attr, is_fixed = x _attr, is_fixed = x
else: else:
_attr, is_fixed = x, False _attr, is_fixed = x, False
attr = AttributeCache.get(_attr) or abort(404, ErrFormat.attribute_not_found.format("id={}".format(_attr)))
if _attr in BUILTIN_ATTRIBUTES:
attr = None
builtin_attr = _attr
else:
attr = AttributeCache.get(_attr) or abort(
404, ErrFormat.attribute_not_found.format("id={}".format(_attr)))
builtin_attr = None
existed = PreferenceShowAttributes.get_by(type_id=type_id, existed = PreferenceShowAttributes.get_by(type_id=type_id,
uid=current_user.uid, uid=current_user.uid,
attr_id=attr and attr.id, attr_id=attr.id,
builtin_attr=builtin_attr,
first=True, first=True,
to_dict=False) to_dict=False)
if existed is None: if existed is None:
PreferenceShowAttributes.create(type_id=type_id, PreferenceShowAttributes.create(type_id=type_id,
uid=current_user.uid, uid=current_user.uid,
attr_id=attr and attr.id, attr_id=attr.id,
builtin_attr=builtin_attr,
order=order, order=order,
is_fixed=is_fixed) is_fixed=is_fixed)
else: else:
existed.update(order=order, is_fixed=is_fixed) existed.update(order=order, is_fixed=is_fixed)
attr_dict = {(int(i[0]) if i[0].isdigit() else i[0]) if isinstance(i, list) else attr_dict = {int(i[0]) if isinstance(i, list) else int(i): j for i, j in attr_order}
(int(i) if i.isdigit() else i): j for i, j in attr_order}
for i in existed_all: for i in existed_all:
if (i.attr_id and i.attr_id not in attr_dict) or (i.builtin_attr and i.builtin_attr not in attr_dict): if i.attr_id not in attr_dict:
i.soft_delete() i.soft_delete()
if not existed_all and attr_order:
cls.add_ci_type_order_item(type_id, is_tree=False)
elif not PreferenceShowAttributes.get_by(type_id=type_id, uid=current_user.uid, to_dict=False):
cls.delete_ci_type_order_item(type_id, is_tree=False)
@staticmethod @staticmethod
def get_tree_view(): def get_tree_view():
ci_type_order = sorted(PreferenceCITypeOrder.get_by(uid=current_user.uid, is_tree=True, to_dict=False),
key=lambda x: x.order)
res = PreferenceTreeView.get_by(uid=current_user.uid, to_dict=True) res = PreferenceTreeView.get_by(uid=current_user.uid, to_dict=True)
if ci_type_order:
res = sorted(res, key=lambda x: {ii.type_id: idx for idx, ii in enumerate(
ci_type_order)}.get(x['type_id'], 1))
for item in res: for item in res:
if item["levels"]: if item["levels"]:
ci_type = CITypeCache.get(item['type_id']).to_dict() ci_type = CITypeCache.get(item['type_id']).to_dict()
@ -250,8 +169,8 @@ class PreferenceManager(object):
return res return res
@classmethod @staticmethod
def create_or_update_tree_view(cls, type_id, levels): def create_or_update_tree_view(type_id, levels):
attrs = CITypeAttributesCache.get(type_id) attrs = CITypeAttributesCache.get(type_id)
for idx, i in enumerate(levels): for idx, i in enumerate(levels):
for attr in attrs: for attr in attrs:
@ -263,12 +182,9 @@ class PreferenceManager(object):
if existed is not None: if existed is not None:
if not levels: if not levels:
existed.soft_delete() existed.soft_delete()
cls.delete_ci_type_order_item(type_id, is_tree=True)
return existed return existed
return existed.update(levels=levels) return existed.update(levels=levels)
elif levels: elif levels:
cls.add_ci_type_order_item(type_id, is_tree=True)
return PreferenceTreeView.create(levels=levels, type_id=type_id, uid=current_user.uid) return PreferenceTreeView.create(levels=levels, type_id=type_id, uid=current_user.uid)
@staticmethod @staticmethod
@ -287,14 +203,12 @@ class PreferenceManager(object):
else: else:
views = _views views = _views
view2cr_ids = defaultdict(list) view2cr_ids = dict()
name2view = dict()
result = dict() result = dict()
name2id = list() name2id = list()
for view in views: for view in views:
view2cr_ids[view['name']].extend(view['cr_ids']) view2cr_ids.setdefault(view['name'], []).extend(view['cr_ids'])
name2id.append([view['name'], view['id']]) name2id.append([view['name'], view['id']])
name2view[view['name']] = view
id2type = dict() id2type = dict()
for view_name in view2cr_ids: for view_name in view2cr_ids:
@ -315,31 +229,15 @@ class PreferenceManager(object):
if not parents: if not parents:
return return
for _l in leaf: for l in leaf:
_find_parent(_l) _find_parent(l)
for node_id in node2show_types: for node_id in node2show_types:
node2show_types[node_id] = [CITypeCache.get(i).to_dict() for i in set(node2show_types[node_id])] node2show_types[node_id] = [CITypeCache.get(i).to_dict() for i in set(node2show_types[node_id])]
topo_flatten = list(toposort.toposort_flatten(topo))
level2constraint = {}
for i, _ in enumerate(topo_flatten[1:]):
ctr = CITypeRelation.get_by(
parent_id=topo_flatten[i], child_id=topo_flatten[i + 1], first=True, to_dict=False)
level2constraint[i + 1] = ctr and ctr.constraint
if leaf2show_types.get(topo_flatten[-1]):
ctr = CITypeRelation.get_by(
parent_id=topo_flatten[-1],
child_id=leaf2show_types[topo_flatten[-1]][0], first=True, to_dict=False)
level2constraint[len(topo_flatten)] = ctr and ctr.constraint
result[view_name] = dict(topo=list(map(list, toposort.toposort(topo))), result[view_name] = dict(topo=list(map(list, toposort.toposort(topo))),
topo_flatten=topo_flatten, topo_flatten=list(toposort.toposort_flatten(topo)),
level2constraint=level2constraint,
leaf=leaf, leaf=leaf,
option=name2view[view_name]['option'],
is_public=name2view[view_name]['is_public'],
leaf2show_types=leaf2show_types, leaf2show_types=leaf2show_types,
node2show_types=node2show_types, node2show_types=node2show_types,
show_types=[CITypeCache.get(j).to_dict() show_types=[CITypeCache.get(j).to_dict()
@ -347,26 +245,18 @@ class PreferenceManager(object):
for type_id in id2type: for type_id in id2type:
id2type[type_id] = CITypeCache.get(type_id).to_dict() id2type[type_id] = CITypeCache.get(type_id).to_dict()
id2type[type_id]['unique_name'] = AttributeCache.get(id2type[type_id]['unique_id']).name
if id2type[type_id]['show_id']:
show_attr = AttributeCache.get(id2type[type_id]['show_id'])
id2type[type_id]['show_name'] = show_attr and show_attr.name
return result, id2type, sorted(name2id, key=lambda x: x[1]) return result, id2type, sorted(name2id, key=lambda x: x[1])
@classmethod @classmethod
def create_or_update_relation_view(cls, name=None, cr_ids=None, _id=None, is_public=False, option=None): def create_or_update_relation_view(cls, name, cr_ids, is_public=False):
if not cr_ids: if not cr_ids:
return abort(400, ErrFormat.preference_relation_view_node_required) return abort(400, ErrFormat.preference_relation_view_node_required)
if _id is None: existed = PreferenceRelationView.get_by(name=name, to_dict=False, first=True)
existed = PreferenceRelationView.get_by(name=name, to_dict=False, first=True)
else:
existed = PreferenceRelationView.get_by_id(_id)
current_app.logger.debug(existed) current_app.logger.debug(existed)
if existed is None: if existed is None:
PreferenceRelationView.create(name=name, cr_ids=cr_ids, uid=current_user.uid, PreferenceRelationView.create(name=name, cr_ids=cr_ids, uid=current_user.uid, is_public=is_public)
is_public=is_public, option=option)
if current_app.config.get("USE_ACL"): if current_app.config.get("USE_ACL"):
ACLManager().add_resource(name, ResourceTypeEnum.RELATION_VIEW) ACLManager().add_resource(name, ResourceTypeEnum.RELATION_VIEW)
@ -374,11 +264,6 @@ class PreferenceManager(object):
RoleEnum.CMDB_READ_ALL, RoleEnum.CMDB_READ_ALL,
ResourceTypeEnum.RELATION_VIEW, ResourceTypeEnum.RELATION_VIEW,
permissions=[PermEnum.READ]) permissions=[PermEnum.READ])
else:
if existed.name != name and current_app.config.get("USE_ACL"):
ACLManager().update_resource(existed.name, name, ResourceTypeEnum.RELATION_VIEW)
existed.update(name=name, cr_ids=cr_ids, is_public=is_public, option=option)
return cls.get_relation_view() return cls.get_relation_view()
@ -407,22 +292,14 @@ class PreferenceManager(object):
def add_search_option(**kwargs): def add_search_option(**kwargs):
kwargs['uid'] = current_user.uid kwargs['uid'] = current_user.uid
if kwargs['name'] in ('__recent__', '__favor__', '__relation_favor__'): existed = PreferenceSearchOption.get_by(uid=current_user.uid,
if kwargs['name'] == '__recent__': name=kwargs.get('name'),
for i in PreferenceSearchOption.get_by( prv_id=kwargs.get('prv_id'),
only_query=True, name=kwargs['name'], uid=current_user.uid).order_by( ptv_id=kwargs.get('ptv_id'),
PreferenceSearchOption.id.desc()).offset(20): type_id=kwargs.get('type_id'),
i.delete() )
if existed:
else: return abort(400, ErrFormat.preference_search_option_exists)
existed = PreferenceSearchOption.get_by(uid=current_user.uid,
name=kwargs.get('name'),
prv_id=kwargs.get('prv_id'),
ptv_id=kwargs.get('ptv_id'),
type_id=kwargs.get('type_id'),
)
if existed:
return abort(400, ErrFormat.preference_search_option_exists)
return PreferenceSearchOption.create(**kwargs) return PreferenceSearchOption.create(**kwargs)
@ -461,65 +338,3 @@ class PreferenceManager(object):
for i in PreferenceTreeView.get_by(type_id=type_id, uid=uid, to_dict=False): for i in PreferenceTreeView.get_by(type_id=type_id, uid=uid, to_dict=False):
i.soft_delete() i.soft_delete()
for i in PreferenceCITypeOrder.get_by(type_id=type_id, uid=uid, to_dict=False):
i.soft_delete()
@staticmethod
def can_edit_relation(parent_id, child_id):
views = PreferenceRelationView.get_by(to_dict=False)
for view in views:
has_m2m = False
last_node_id = None
for cr in view.cr_ids:
_rel = CITypeRelation.get_by(parent_id=cr['parent_id'], child_id=cr['child_id'],
first=True, to_dict=False)
if _rel and _rel.constraint == ConstraintEnum.Many2Many:
has_m2m = True
if parent_id == _rel.parent_id and child_id == _rel.child_id:
return False
if _rel:
last_node_id = _rel.child_id
if parent_id == last_node_id:
rels = CITypeRelation.get_by(parent_id=last_node_id, to_dict=False)
for rel in rels:
if rel.child_id == child_id and has_m2m:
return False
return True
@staticmethod
def add_ci_type_order_item(type_id, is_tree=False):
max_order = PreferenceCITypeOrder.get_by(
uid=current_user.uid, is_tree=is_tree, only_query=True).order_by(PreferenceCITypeOrder.order.desc()).first()
order = (max_order and max_order.order + 1) or 1
PreferenceCITypeOrder.create(type_id=type_id, is_tree=is_tree, uid=current_user.uid, order=order)
@staticmethod
def delete_ci_type_order_item(type_id, is_tree=False):
existed = PreferenceCITypeOrder.get_by(uid=current_user.uid, type_id=type_id, is_tree=is_tree,
first=True, to_dict=False)
existed and existed.soft_delete()
@staticmethod
def upsert_ci_type_order(type_ids, is_tree=False):
for idx, type_id in enumerate(type_ids):
order = idx + 1
existed = PreferenceCITypeOrder.get_by(uid=current_user.uid, type_id=type_id, is_tree=is_tree,
to_dict=False, first=True)
if existed is not None:
existed.update(order=order, flush=True)
else:
PreferenceCITypeOrder.create(uid=current_user.uid, type_id=type_id, is_tree=is_tree, order=order,
flush=True)
try:
db.session.commit()
except Exception as e:
db.session.rollback()
current_app.logger.error("upsert citype order failed: {}".format(e))
return abort(400, ErrFormat.unknown_error)

View File

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

View File

@ -1,178 +1,96 @@
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
from flask_babel import lazy_gettext as _l
from api.lib.resp_format import CommonErrFormat from api.lib.resp_format import CommonErrFormat
class ErrFormat(CommonErrFormat): class ErrFormat(CommonErrFormat):
ci_type_config = _l("CI Model") # 模型配置 invalid_relation_type = "无效的关系类型: {}"
ci_type_not_found = "模型不存在!"
argument_attributes_must_be_list = "参数 attributes 类型必须是列表"
argument_file_not_found = "文件似乎并未上传"
invalid_relation_type = _l("Invalid relation type: {}") # 无效的关系类型: {} attribute_not_found = "属性 {} 不存在!"
ci_type_not_found = _l("CIType is not found") # 模型不存在! attribute_is_unique_id = "该属性是模型的唯一标识,不能被删除!"
attribute_is_ref_by_type = "该属性被模型 {} 引用, 不能删除!"
attribute_value_type_cannot_change = "属性的值类型不允许修改!"
attribute_list_value_cannot_change = "多值不被允许修改!"
attribute_index_cannot_change = "修改索引 非管理员不被允许!"
attribute_index_change_failed = "索引切换失败!"
invalid_choice_values = "预定义值的类型不对!"
attribute_name_duplicate = "重复的属性名 {}"
add_attribute_failed = "创建属性 {} 失败!"
update_attribute_failed = "修改属性 {} 失败!"
cannot_edit_attribute = "您没有权限修改该属性!"
cannot_delete_attribute = "目前只允许 属性创建人、管理员 删除属性!"
attribute_name_cannot_be_builtin = "属性字段名不能是内置字段: id, _id, ci_id, type, _type, ci_type"
# 参数 attributes 类型必须是列表 ci_not_found = "CI {} 不存在"
argument_attributes_must_be_list = _l("The type of parameter attributes must be a list") unique_constraint = "多属性联合唯一校验不通过: {}"
argument_file_not_found = _l("The file doesn't seem to be uploaded") # 文件似乎并未上传 unique_value_not_found = "模型的主键 {} 不存在!"
unique_key_required = "主键字段 {} 缺失"
ci_is_already_existed = "CI 已经存在!"
relation_constraint = "关系约束: {}, 校验失败 "
relation_not_found = "CI关系: {} 不存在"
ci_search_Parentheses_invalid = "搜索表达式里小括号前不支持: 或、非"
attribute_not_found = _l("Attribute {} does not exist!") # 属性 {} 不存在! ci_type_not_found2 = "模型 {} 不存在"
# 该属性是模型的唯一标识,不能被删除! ci_type_is_already_existed = "模型 {} 已经存在"
attribute_is_unique_id = _l( unique_key_not_define = "主键未定义或者已被删除"
"This attribute is the unique identifier of the model and cannot be deleted!") only_owner_can_delete = "只有创建人才能删除它!"
attribute_is_ref_by_type = _l( ci_exists_and_cannot_delete_type = "因为CI已经存在不能删除模型"
"This attribute is referenced by model {} and cannot be deleted!") # 该属性被模型 {} 引用, 不能删除! ci_relation_view_exists_and_cannot_delete_type = "因为关系视图 {} 引用了该模型,不能删除模型"
attribute_value_type_cannot_change = _l( ci_type_group_not_found = "模型分组 {} 不存在"
"The value type of the attribute is not allowed to be modified!") # 属性的值类型不允许修改! ci_type_group_exists = "模型分组 {} 已经存在"
attribute_list_value_cannot_change = _l("Multiple values are not allowed to be modified!") # 多值不被允许修改! ci_type_relation_not_found = "模型关系 {} 不存在"
# 修改索引 非管理员不被允许! ci_type_attribute_group_duplicate = "属性分组 {} 已存在"
attribute_index_cannot_change = _l("Modifying the index is not allowed for non-administrators!") ci_type_attribute_group_not_found = "属性分组 {} 不存在"
attribute_index_change_failed = _l("Index switching failed!") # 索引切换失败! ci_type_group_attribute_not_found = "属性组<{0}> - 属性<{1}> 不存在"
invalid_choice_values = _l("The predefined value is of the wrong type!") # 预定义值的类型不对! unique_constraint_duplicate = "唯一约束已经存在!"
attribute_name_duplicate = _l("Duplicate attribute name {}") # 重复的属性名 {} unique_constraint_invalid = "唯一约束的属性不能是 JSON 和 多值"
add_attribute_failed = _l("Failed to create attribute {}!") # 创建属性 {} 失败! ci_type_trigger_duplicate = "重复的触发器"
update_attribute_failed = _l("Modify attribute {} failed!") # 修改属性 {} 失败! ci_type_trigger_not_found = "触发器 {} 不存在"
cannot_edit_attribute = _l("You do not have permission to modify this attribute!") # 您没有权限修改该属性!
cannot_delete_attribute = _l(
"Only creators and administrators are allowed to delete attributes!") # 目前只允许 属性创建人、管理员 删除属性!
# 属性字段名不能是内置字段: id, _id, ci_id, type, _type, ci_type
attribute_name_cannot_be_builtin = _l(
"Attribute field names cannot be built-in fields: id, _id, ci_id, type, _type, ci_type, ticket_id")
attribute_choice_other_invalid = _l(
"Predefined value: Other model request parameters are illegal!") # 预定义值: 其他模型请求参数不合法!
ci_not_found = _l("CI {} does not exist") # CI {} 不存在 record_not_found = "操作记录 {} 不存在"
unique_constraint = _l("Multiple attribute joint unique verification failed: {}") # 多属性联合唯一校验不通过: {} cannot_delete_unique = "不能删除唯一标识"
unique_value_not_found = _l("The model's primary key {} does not exist!") # 模型的主键 {} 不存在! cannot_delete_default_order_attr = "不能删除默认排序的属性"
unique_key_required = _l("Primary key {} is missing") # 主键字段 {} 缺失
ci_is_already_existed = _l("CI already exists!") # CI 已经存在!
ci_reference_not_found = _l("{}: CI reference {} does not exist!") # {}: CI引用 {} 不存在!
ci_reference_invalid = _l("{}: CI reference {} is illegal!") # {}, CI引用 {} 不合法!
relation_constraint = _l("Relationship constraint: {}, verification failed") # 关系约束: {}, 校验失败
# 多对多关系 限制: 模型 {} <-> {} 已经存在多对多关系!
m2m_relation_constraint = _l(
"Many-to-many relationship constraint: Model {} <-> {} already has a many-to-many relationship!")
relation_not_found = _l("CI relationship: {} does not exist") # CI关系: {} 不存在 preference_relation_view_node_required = "没有选择节点"
preference_search_option_not_found = "该搜索选项不存在!"
preference_search_option_exists = "该搜索选项命名重复!"
# 搜索表达式里小括号前不支持: 或、非 relation_type_exists = "关系类型 {} 已经存在"
ci_search_Parentheses_invalid = _l("In search expressions, not supported before parentheses: or, not") relation_type_not_found = "关系类型 {} 不存在"
ci_type_not_found2 = _l("Model {} does not exist") # 模型 {} 不存在 attribute_value_invalid = "无效的属性值: {}"
ci_type_is_already_existed = _l("Model {} already exists") # 模型 {} 已经存在 attribute_value_invalid2 = "{} 无效的值: {}"
unique_key_not_define = _l("The primary key is undefined or has been deleted") # 主键未定义或者已被删除 not_in_choice_values = "{} 不在预定义值里"
only_owner_can_delete = _l("Only the creator can delete it!") # 只有创建人才能删除它! attribute_value_unique_required = "属性 {} 的值必须是唯一的, 当前值 {} 已存在"
ci_exists_and_cannot_delete_type = _l( attribute_value_required = "属性 {} 值必须存在"
"The model cannot be deleted because the CI already exists") # 因为CI已经存在不能删除模型 attribute_value_unknown_error = "新增或者修改属性值未知错误: {}"
ci_exists_and_cannot_delete_inheritance = _l(
"The inheritance cannot be deleted because the CI already exists") # 因为CI已经存在不能删除继承关系
ci_type_inheritance_cannot_delete = _l("The model is inherited and cannot be deleted") # 该模型被继承, 不能删除
ci_type_referenced_cannot_delete = _l(
"The model is referenced by attribute {} and cannot be deleted") # 该模型被属性 {} 引用, 不能删除
# 因为关系视图 {} 引用了该模型,不能删除模型 custom_name_duplicate = "订制名重复"
ci_relation_view_exists_and_cannot_delete_type = _l(
"The model cannot be deleted because the model is referenced by the relational view {}")
ci_type_group_not_found = _l("Model group {} does not exist") # 模型分组 {} 不存在
ci_type_group_exists = _l("Model group {} already exists") # 模型分组 {} 已经存在
ci_type_relation_not_found = _l("Model relationship {} does not exist") # 模型关系 {} 不存在
ci_type_attribute_group_duplicate = _l("Attribute group {} already exists") # 属性分组 {} 已存在
ci_type_attribute_group_not_found = _l("Attribute group {} does not exist") # 属性分组 {} 不存在
# 属性组<{0}> - 属性<{1}> 不存在
ci_type_group_attribute_not_found = _l("Attribute group <{0}> - attribute <{1}> does not exist")
unique_constraint_duplicate = _l("The unique constraint already exists!") # 唯一约束已经存在!
# 唯一约束的属性不能是 JSON 和 多值
unique_constraint_invalid = _l("Uniquely constrained attributes cannot be JSON and multi-valued")
ci_type_trigger_duplicate = _l("Duplicated trigger") # 重复的触发器
ci_type_trigger_not_found = _l("Trigger {} does not exist") # 触发器 {} 不存在
ci_type_reconciliation_duplicate = _l("Duplicated reconciliation rule") # 重复的校验规则
ci_type_reconciliation_not_found = _l("Reconciliation rule {} does not exist") # 规则 {} 不存在
record_not_found = _l("Operation record {} does not exist") # 操作记录 {} 不存在 limit_ci_type = "模型数超过限制: {}"
cannot_delete_unique = _l("Unique identifier cannot be deleted") # 不能删除唯一标识 limit_ci = "CI数超过限制: {}"
cannot_delete_default_order_attr = _l("Cannot delete default sorted attributes") # 不能删除默认排序的属性
preference_relation_view_node_required = _l("No node selected") # 没有选择节点 adr_duplicate = "自动发现规则: {} 已经存在!"
preference_search_option_not_found = _l("This search option does not exist!") # 该搜索选项不存在! adr_not_found = "自动发现规则: {} 不存在!"
preference_search_option_exists = _l("This search option has a duplicate name!") # 该搜索选项命名重复! adr_referenced = "该自动发现规则被模型引用, 不能删除!"
ad_duplicate = "自动发现规则的应用不能重复定义!"
ad_not_found = "您要修改的自动发现: {} 不存在!"
ad_not_unique_key = "属性字段没有包括唯一标识: {}"
adc_not_found = "自动发现的实例不存在!"
adt_not_found = "模型并未关联该自动发现!"
adt_secret_no_permission = "只有创建人才能修改Secret!"
cannot_delete_adt = "该规则已经有自动发现的实例, 不能被删除!"
adr_default_ref_once = "该默认的自动发现规则 已经被模型 {} 引用!"
adr_unique_key_required = "unique_key方法必须返回非空字符串!"
adr_plugin_attributes_list_required = "attributes方法必须返回的是list"
adr_plugin_attributes_list_no_empty = "attributes方法返回的list不能为空!"
adt_target_all_no_permission = "只有管理员才可以定义执行机器为: 所有节点!"
adt_target_expr_no_permission = "执行机器权限检查不通过: {}"
relation_type_exists = _l("Relationship type {} already exists") # 关系类型 {} 已经存在 ci_filter_name_cannot_be_empty = "CI过滤授权 必须命名!"
relation_type_not_found = _l("Relationship type {} does not exist") # 关系类型 {} 不存在 ci_filter_perm_cannot_or_query = "CI过滤授权 暂时不支持 或 查询"
ci_filter_perm_attr_no_permission = "您没有属性 {} 的操作权限!"
attribute_value_invalid = _l("Invalid attribute value: {}") # 无效的属性值: {} ci_filter_perm_ci_no_permission = "您没有该CI的操作权限!"
attribute_value_invalid2 = _l("{} Invalid value: {}") # {} 无效的值: {}
not_in_choice_values = _l("{} is not in the predefined values") # {} 不在预定义值里
# 属性 {} 的值必须是唯一的, 当前值 {} 已存在
attribute_value_unique_required = _l("The value of attribute {} must be unique, {} already exists")
attribute_value_required = _l("Attribute {} value must exist") # 属性 {} 值必须存在
attribute_value_out_of_range = _l("Out of range value, the maximum value is 2147483647")
# 新增或者修改属性值未知错误: {}
attribute_value_unknown_error = _l("Unknown error when adding or modifying attribute value: {}")
custom_name_duplicate = _l("Duplicate custom name") # 订制名重复
limit_ci_type = _l("Number of models exceeds limit: {}") # 模型数超过限制: {}
limit_ci = _l("The number of CIs exceeds the limit: {}") # CI数超过限制: {}
adr_duplicate = _l("Auto-discovery rule: {} already exists!") # 自动发现规则: {} 已经存在!
adr_not_found = _l("Auto-discovery rule: {} does not exist!") # 自动发现规则: {} 不存在!
# 该自动发现规则被模型引用, 不能删除!
adr_referenced = _l("This auto-discovery rule is referenced by the model and cannot be deleted!")
# 自动发现规则的应用不能重复定义!
ad_duplicate = _l("The application of auto-discovery rules cannot be defined repeatedly!")
ad_not_found = _l("The auto-discovery you want to modify: {} does not exist!") # 您要修改的自动发现: {} 不存在!
ad_not_unique_key = _l("Attribute does not include unique identifier: {}") # 属性字段没有包括唯一标识: {}
adc_not_found = _l("The auto-discovery instance does not exist!") # 自动发现的实例不存在!
adt_not_found = _l("The model is not associated with this auto-discovery!") # 模型并未关联该自动发现!
adt_secret_no_permission = _l("Only the creator can modify the Secret!") # 只有创建人才能修改Secret!
# 该规则已经有自动发现的实例, 不能被删除!
cannot_delete_adt = _l("This rule already has auto-discovery instances and cannot be deleted!")
# 该默认的自动发现规则 已经被模型 {} 引用!
adr_default_ref_once = _l("The default auto-discovery rule is already referenced by model {}!")
# unique_key方法必须返回非空字符串!
adr_unique_key_required = _l("The unique_key method must return a non-empty string!")
# attributes方法必须返回的是list
adr_plugin_attributes_list_required = _l("The attributes method must return a list")
# attributes方法返回的list不能为空!
adr_plugin_attributes_list_no_empty = _l("The list returned by the attributes method cannot be empty!")
# 只有管理员才可以定义执行机器为: 所有节点!
adt_target_all_no_permission = _l("Only administrators can define execution targets as: all nodes!")
adt_target_expr_no_permission = _l("Execute targets permission check failed: {}") # 执行机器权限检查不通过: {}
ci_filter_name_cannot_be_empty = _l("CI filter authorization must be named!") # CI过滤授权 必须命名!
ci_filter_perm_cannot_or_query = _l(
"CI filter authorization is currently not supported or query") # CI过滤授权 暂时不支持 或 查询
# 您没有属性 {} 的操作权限!
ci_filter_perm_attr_no_permission = _l("You do not have permission to operate attribute {}!")
ci_filter_perm_ci_no_permission = _l("You do not have permission to operate this CI!") # 您没有该CI的操作权限!
password_save_failed = _l("Failed to save password: {}") # 保存密码失败: {}
password_load_failed = _l("Failed to get password: {}") # 获取密码失败: {}
cron_time_format_invalid = _l("Scheduling time format error") # 调度时间格式错误
reconciliation_title = _l("CMDB data reconciliation results") # CMDB数据合规检查结果
reconciliation_body = _l("Number of {} illegal: {}") # "{} 不合规数: {}"
topology_exists = _l("Topology view {} already exists") # 拓扑视图 {} 已经存在
topology_group_exists = _l("Topology group {} already exists") # 拓扑视图分组 {} 已经存在
# 因为该分组下定义了拓扑视图,不能删除
topo_view_exists_cannot_delete_group = _l("The group cannot be deleted because the topology view already exists")
relation_path_search_src_target_required = _l("Both the source model and the target model must be selected")
builtin_type_cannot_update_name = _l("The names of built-in models cannot be changed")
# # IPAM
ipam_subnet_model_not_found = _l("The subnet model {} does not exist")
ipam_address_model_not_found = _l("The IP Address model {} does not exist")
ipam_cidr_invalid_notation = _l("CIDR {} is an invalid notation")
ipam_cidr_invalid_subnet = _l("Invalid CIDR: {}, available subnets: {}")
ipam_subnet_prefix_length_invalid = _l("Invalid subnet prefix length: {}")
ipam_parent_subnet_node_cidr_cannot_empty = _l("parent node cidr must be required")
ipam_subnet_overlapped = _l("{} and {} overlap")
ipam_subnet_cannot_delete = _l("Cannot delete because child nodes exist")
ipam_subnet_not_found = _l("Subnet is not found")
ipam_scope_cannot_delete = _l("Cannot delete because child nodes exist")
# # DCIM
dcim_builtin_model_not_found = _l("The dcim model {} does not exist")
dcim_rack_u_slot_invalid = _l("Irregularities in Rack Units")
dcim_rack_u_count_invalid = _l("The device's position is greater than the rack unit height")

View File

@ -16,13 +16,10 @@ def search(query=None,
ret_key=RetKey.NAME, ret_key=RetKey.NAME,
count=1, count=1,
sort=None, sort=None,
excludes=None, excludes=None):
use_id_filter=False,
use_ci_filter=True):
if current_app.config.get("USE_ES"): if current_app.config.get("USE_ES"):
s = SearchFromES(query, fl, facet, page, ret_key, count, sort) s = SearchFromES(query, fl, facet, page, ret_key, count, sort)
else: else:
s = SearchFromDB(query, fl, facet, page, ret_key, count, sort, excludes=excludes, s = SearchFromDB(query, fl, facet, page, ret_key, count, sort, excludes=excludes)
use_id_filter=use_id_filter, use_ci_filter=use_ci_filter)
return s return s

View File

@ -7,7 +7,6 @@ QUERY_CIS_BY_VALUE_TABLE = """
attr.alias AS attr_alias, attr.alias AS attr_alias,
attr.value_type, attr.value_type,
attr.is_list, attr.is_list,
attr.is_password,
c_cis.type_id, c_cis.type_id,
{0}.ci_id, {0}.ci_id,
{0}.attr_id, {0}.attr_id,
@ -27,8 +26,7 @@ QUERY_CIS_BY_IDS = """
A.attr_alias, A.attr_alias,
A.value, A.value,
A.value_type, A.value_type,
A.is_list, A.is_list
A.is_password
FROM FROM
({1}) AS A {0} ({1}) AS A {0}
ORDER BY A.ci_id; ORDER BY A.ci_id;
@ -45,7 +43,7 @@ FACET_QUERY1 = """
FACET_QUERY = """ FACET_QUERY = """
SELECT {0}.value, SELECT {0}.value,
count(distinct {0}.ci_id) count({0}.ci_id)
FROM {0} FROM {0}
INNER JOIN ({1}) AS F ON F.ci_id={0}.ci_id INNER JOIN ({1}) AS F ON F.ci_id={0}.ci_id
WHERE {0}.attr_id={2:d} WHERE {0}.attr_id={2:d}
@ -56,13 +54,13 @@ QUERY_CI_BY_ATTR_NAME = """
SELECT {0}.ci_id SELECT {0}.ci_id
FROM {0} FROM {0}
WHERE {0}.attr_id={1:d} WHERE {0}.attr_id={1:d}
AND ({0}.value {2}) AND {0}.value {2}
""" """
QUERY_CI_BY_ID = """ QUERY_CI_BY_ID = """
SELECT c_cis.id as ci_id SELECT c_cis.id as ci_id
FROM c_cis FROM c_cis
WHERE c_cis.id {} WHERE c_cis.id={}
""" """
QUERY_CI_BY_TYPE = """ QUERY_CI_BY_TYPE = """
@ -107,12 +105,3 @@ FROM
WHERE c_value_index_datetime.value LIKE "{0}") AS {1} WHERE c_value_index_datetime.value LIKE "{0}") AS {1}
GROUP BY {1}.ci_id GROUP BY {1}.ci_id
""" """
QUERY_CI_BY_NO_ATTR_IN = """
SELECT *
FROM
(SELECT c_value_index_texts.ci_id
FROM c_value_index_texts
WHERE c_value_index_texts.value in ({0})) AS {1}
GROUP BY {1}.ci_id
"""

View File

@ -4,19 +4,16 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import copy import copy
import six
import time import time
from flask import abort
from flask import current_app from flask import current_app
from flask_login import current_user from flask_login import current_user
from jinja2 import Template from jinja2 import Template
from sqlalchemy import text
from api.extensions import db from api.extensions import db
from api.lib.cmdb.cache import AttributeCache from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import CITypeCache from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.ci import CIManager from api.lib.cmdb.ci import CIManager
from api.lib.cmdb.const import BUILTIN_ATTRIBUTES
from api.lib.cmdb.const import PermEnum from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import ResourceTypeEnum from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.const import RetKey from api.lib.cmdb.const import RetKey
@ -28,11 +25,9 @@ from api.lib.cmdb.search.ci.db.query_sql import FACET_QUERY
from api.lib.cmdb.search.ci.db.query_sql import QUERY_CI_BY_ATTR_NAME from api.lib.cmdb.search.ci.db.query_sql import QUERY_CI_BY_ATTR_NAME
from api.lib.cmdb.search.ci.db.query_sql import QUERY_CI_BY_ID from api.lib.cmdb.search.ci.db.query_sql import QUERY_CI_BY_ID
from api.lib.cmdb.search.ci.db.query_sql import QUERY_CI_BY_NO_ATTR from api.lib.cmdb.search.ci.db.query_sql import QUERY_CI_BY_NO_ATTR
from api.lib.cmdb.search.ci.db.query_sql import QUERY_CI_BY_NO_ATTR_IN
from api.lib.cmdb.search.ci.db.query_sql import QUERY_CI_BY_TYPE from api.lib.cmdb.search.ci.db.query_sql import QUERY_CI_BY_TYPE
from api.lib.cmdb.search.ci.db.query_sql import QUERY_UNION_CI_ATTRIBUTE_IS_NULL from api.lib.cmdb.search.ci.db.query_sql import QUERY_UNION_CI_ATTRIBUTE_IS_NULL
from api.lib.cmdb.utils import TableMap from api.lib.cmdb.utils import TableMap
from api.lib.cmdb.utils import ValueTypeMap
from api.lib.perm.acl.acl import ACLManager from api.lib.perm.acl.acl import ACLManager
from api.lib.perm.acl.acl import is_app_admin from api.lib.perm.acl.acl import is_app_admin
from api.lib.utils import handle_arg_list from api.lib.utils import handle_arg_list
@ -47,11 +42,7 @@ class Search(object):
count=1, count=1,
sort=None, sort=None,
ci_ids=None, ci_ids=None,
excludes=None, excludes=None):
parent_node_perm_passed=False,
use_id_filter=False,
use_ci_filter=True,
only_ids=False):
self.orig_query = query self.orig_query = query
self.fl = fl or [] self.fl = fl or []
self.excludes = excludes or [] self.excludes = excludes or []
@ -61,20 +52,12 @@ class Search(object):
self.count = count self.count = count
self.sort = sort self.sort = sort
self.ci_ids = ci_ids or [] self.ci_ids = ci_ids or []
self.raw_ci_ids = copy.deepcopy(self.ci_ids)
self.query_sql = "" self.query_sql = ""
self.type_id_list = [] self.type_id_list = []
self.only_type_query = False self.only_type_query = False
self.parent_node_perm_passed = parent_node_perm_passed
self.use_id_filter = use_id_filter
self.use_ci_filter = use_ci_filter
self.only_ids = only_ids
self.multi_type_has_ci_filter = False
self.valid_type_names = [] self.valid_type_names = []
self.type2filter_perms = dict() self.type2filter_perms = dict()
self.is_app_admin = is_app_admin('cmdb') or current_user.username == "worker"
self.is_app_admin = self.is_app_admin or (not self.use_ci_filter and not self.use_id_filter)
@staticmethod @staticmethod
def _operator_proc(key): def _operator_proc(key):
@ -108,137 +91,81 @@ class Search(object):
else: else:
raise SearchError(ErrFormat.attribute_not_found.format(key)) raise SearchError(ErrFormat.attribute_not_found.format(key))
def _type_query_handler(self, v, queries, is_sub=False): def _type_query_handler(self, v, queries):
new_v = v[1:-1].split(";") if v.startswith("(") and v.endswith(")") else [v] new_v = v[1:-1].split(";") if v.startswith("(") and v.endswith(")") else [v]
type_num = len(new_v)
type_id_list = []
for _v in new_v: for _v in new_v:
ci_type = CITypeCache.get(_v) ci_type = CITypeCache.get(_v)
if type_num == 1 and not self.sort and ci_type and ci_type.default_order_attr: if len(new_v) == 1 and not self.sort and ci_type and ci_type.default_order_attr:
self.sort = ci_type.default_order_attr self.sort = ci_type.default_order_attr
if ci_type is not None: if ci_type is not None:
if self.valid_type_names == "ALL" or ci_type.name in self.valid_type_names: if self.valid_type_names == "ALL" or ci_type.name in self.valid_type_names:
if not is_sub: self.type_id_list.append(str(ci_type.id))
self.type_id_list.append(str(ci_type.id)) if ci_type.id in self.type2filter_perms:
type_id_list.append(str(ci_type.id))
if ci_type.id in self.type2filter_perms and not is_sub:
ci_filter = self.type2filter_perms[ci_type.id].get('ci_filter') ci_filter = self.type2filter_perms[ci_type.id].get('ci_filter')
if ci_filter and self.use_ci_filter and not self.use_id_filter: if ci_filter:
sub = [] sub = []
ci_filter = Template(ci_filter).render(user=current_user) ci_filter = Template(ci_filter).render(user=current_user)
for i in ci_filter.split(','): for i in ci_filter.split(','):
if type_num == 1: if i.startswith("~") and not sub:
if i.startswith("~") and not sub: queries.append(i)
queries.append(i)
else:
sub.append(i)
else: else:
sub.append(i) sub.append(i)
if sub: if sub:
if type_num == 1: queries.append(dict(operator="&", queries=sub))
queries.append(dict(operator="&", queries=sub))
else:
if str(ci_type.id) in self.type_id_list:
self.type_id_list.remove(str(ci_type.id))
type_id_list.remove(str(ci_type.id))
sub.extend([i for i in queries[1:] if isinstance(i, (six.string_types, list))])
sub.insert(0, "_type:{}".format(ci_type.id))
queries.append(dict(operator="|", queries=sub))
self.multi_type_has_ci_filter = True
if self.type2filter_perms[ci_type.id].get('attr_filter'): if self.type2filter_perms[ci_type.id].get('attr_filter'):
if type_num == 1: if not self.fl:
if not self.fl: self.fl = set(self.type2filter_perms[ci_type.id]['attr_filter'])
self.fl = set(self.type2filter_perms[ci_type.id]['attr_filter'])
else:
fl = set(self.fl) & set(self.type2filter_perms[ci_type.id]['attr_filter'])
not fl and abort(400, ErrFormat.ci_filter_perm_attr_no_permission.format(self.fl))
self.fl = fl
else: else:
self.fl = self.fl or {} self.fl = set(self.fl) & set(self.type2filter_perms[ci_type.id]['attr_filter'])
if not self.fl or isinstance(self.fl, dict):
self.fl[ci_type.id] = set(self.type2filter_perms[ci_type.id]['attr_filter'])
if self.type2filter_perms[ci_type.id].get('id_filter') and self.use_id_filter:
if not self.raw_ci_ids:
self.ci_ids = list(self.type2filter_perms[ci_type.id]['id_filter'].keys())
if self.use_id_filter and not self.ci_ids and not self.is_app_admin:
self.raw_ci_ids = [0]
else: else:
raise SearchError(ErrFormat.no_permission.format(ci_type.alias, PermEnum.READ)) raise SearchError(ErrFormat.no_permission.format(ci_type.alias, PermEnum.READ))
else: else:
raise SearchError(ErrFormat.ci_type_not_found2.format(_v)) raise SearchError(ErrFormat.ci_type_not_found2.format(_v))
if type_num != len(self.type_id_list) and queries and queries[0].startswith('_type') and not is_sub: if self.type_id_list:
queries[0] = "_type:({})".format(";".join(self.type_id_list)) type_ids = ",".join(self.type_id_list)
if type_id_list:
type_ids = ",".join(type_id_list)
_query_sql = QUERY_CI_BY_TYPE.format(type_ids) _query_sql = QUERY_CI_BY_TYPE.format(type_ids)
if self.only_type_query or self.multi_type_has_ci_filter: if self.only_type_query:
return _query_sql return _query_sql
elif type_num > 1: # there must be instance-level access control else:
return "select c_cis.id as ci_id from c_cis where c_cis.id=0" return ""
return "" return ""
@staticmethod @staticmethod
def _id_query_handler(v): def _id_query_handler(v):
if ";" in v: return QUERY_CI_BY_ID.format(v)
return QUERY_CI_BY_ID.format("in {}".format(v.replace(';', ',')))
else:
return QUERY_CI_BY_ID.format("= {}".format(v))
@staticmethod @staticmethod
def _in_query_handler(attr, v, is_not): def _in_query_handler(attr, v, is_not):
new_v = v[1:-1].split(";") 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 table_name = TableMap(attr=attr).table_name
in_query = " OR {0}.value ".format(table_name).join(['{0} "{1}"'.format( in_query = " OR {0}.value ".format(table_name).join(['{0} "{1}"'.format(
"NOT LIKE" if is_not else "LIKE", "NOT LIKE" if is_not else "LIKE",
_v.replace("*", "%")) for _v in new_v]) _v.replace("*", "%")) for _v in new_v])
_query_sql = QUERY_CI_BY_ATTR_NAME.format(table_name, attr.id, in_query) _query_sql = QUERY_CI_BY_ATTR_NAME.format(table_name, attr.id, in_query)
return _query_sql return _query_sql
@staticmethod @staticmethod
def _range_query_handler(attr, v, is_not): def _range_query_handler(attr, v, is_not):
start, end = [x.strip() for x in v[1:-1].split("_TO_")] 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 table_name = TableMap(attr=attr).table_name
range_query = "{0} '{1}' AND '{2}'".format( range_query = "{0} '{1}' AND '{2}'".format(
"NOT BETWEEN" if is_not else "BETWEEN", "NOT BETWEEN" if is_not else "BETWEEN",
start.replace("*", "%"), end.replace("*", "%")) start.replace("*", "%"), end.replace("*", "%"))
_query_sql = QUERY_CI_BY_ATTR_NAME.format(table_name, attr.id, range_query) _query_sql = QUERY_CI_BY_ATTR_NAME.format(table_name, attr.id, range_query)
return _query_sql return _query_sql
@staticmethod @staticmethod
def _comparison_query_handler(attr, v): def _comparison_query_handler(attr, v):
table_name = TableMap(attr=attr).table_name table_name = TableMap(attr=attr).table_name
if v.startswith(">=") or v.startswith("<="): 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("*", "%")) comparison_query = "{0} '{1}'".format(v[:2], v[2:].replace("*", "%"))
else: 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("*", "%")) comparison_query = "{0} '{1}'".format(v[0], v[1:].replace("*", "%"))
_query_sql = QUERY_CI_BY_ATTR_NAME.format(table_name, attr.id, comparison_query) _query_sql = QUERY_CI_BY_ATTR_NAME.format(table_name, attr.id, comparison_query)
return _query_sql return _query_sql
@staticmethod @staticmethod
@ -250,7 +177,6 @@ class Search(object):
elif field.startswith("-"): elif field.startswith("-"):
field = field[1:] field = field[1:]
sort_type = "DESC" sort_type = "DESC"
return field, sort_type return field, sort_type
def __sort_by_id(self, sort_type, query_sql): def __sort_by_id(self, sort_type, query_sql):
@ -260,7 +186,7 @@ class Search(object):
return ret_sql.format(query_sql, "ORDER BY B.ci_id {1} LIMIT {0:d}, {2};".format( return ret_sql.format(query_sql, "ORDER BY B.ci_id {1} LIMIT {0:d}, {2};".format(
(self.page - 1) * self.count, sort_type, self.count)) (self.page - 1) * self.count, sort_type, self.count))
elif self.type_id_list and not self.multi_type_has_ci_filter: elif self.type_id_list:
self.query_sql = "SELECT B.ci_id FROM ({0}) AS B {1}".format( self.query_sql = "SELECT B.ci_id FROM ({0}) AS B {1}".format(
query_sql, query_sql,
"INNER JOIN c_cis on c_cis.id=B.ci_id WHERE c_cis.type_id IN ({0}) ".format( "INNER JOIN c_cis on c_cis.id=B.ci_id WHERE c_cis.type_id IN ({0}) ".format(
@ -285,7 +211,7 @@ class Search(object):
def __sort_by_type(self, sort_type, query_sql): def __sort_by_type(self, sort_type, query_sql):
ret_sql = "SELECT SQL_CALC_FOUND_ROWS DISTINCT B.ci_id FROM ({0}) AS B {1}" ret_sql = "SELECT SQL_CALC_FOUND_ROWS DISTINCT B.ci_id FROM ({0}) AS B {1}"
if self.type_id_list and not self.multi_type_has_ci_filter: if self.type_id_list:
self.query_sql = "SELECT B.ci_id FROM ({0}) AS B {1}".format( self.query_sql = "SELECT B.ci_id FROM ({0}) AS B {1}".format(
query_sql, query_sql,
"INNER JOIN c_cis on c_cis.id=B.ci_id WHERE c_cis.type_id IN ({0}) ".format( "INNER JOIN c_cis on c_cis.id=B.ci_id WHERE c_cis.type_id IN ({0}) ".format(
@ -309,23 +235,16 @@ class Search(object):
(self.page - 1) * self.count, sort_type, self.count)) (self.page - 1) * self.count, sort_type, self.count))
def __sort_by_field(self, field, sort_type, query_sql): def __sort_by_field(self, field, sort_type, query_sql):
if field not in BUILTIN_ATTRIBUTES: attr = AttributeCache.get(field)
attr_id = attr.id
attr = AttributeCache.get(field) table_name = TableMap(attr=attr).table_name
attr_id = attr.id _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
table_name = TableMap(attr=attr).table_name if self.only_type_query or not self.type_id_list:
_v_query_sql = """SELECT ALIAS.ci_id, {0}.value
FROM ({1}) AS ALIAS INNER JOIN {0} ON {0}.ci_id = ALIAS.ci_id
WHERE {0}.attr_id = {2}""".format(table_name, query_sql, attr_id)
new_table = _v_query_sql
else:
_v_query_sql = """SELECT c_cis.id AS ci_id, c_cis.{0} AS value
FROM c_cis INNER JOIN ({1}) AS ALIAS ON ALIAS.ci_id = c_cis.id""".format(
field[1:], query_sql)
new_table = _v_query_sql
if self.only_type_query or not self.type_id_list or self.multi_type_has_ci_filter:
return ("SELECT SQL_CALC_FOUND_ROWS DISTINCT C.ci_id FROM ({0}) AS C ORDER BY C.value {2} " 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)) "LIMIT {1:d}, {3};".format(new_table, (self.page - 1) * self.count, sort_type, self.count))
@ -363,9 +282,7 @@ class Search(object):
INNER JOIN ({2}) as {3} USING(ci_id)""".format(query_sql, alias, _query_sql, alias + "A") INNER JOIN ({2}) as {3} USING(ci_id)""".format(query_sql, alias, _query_sql, alias + "A")
elif operator == "|" or operator == "|~": elif operator == "|" or operator == "|~":
query_sql = "SELECT * FROM ({0}) as {1} UNION ALL SELECT * FROM ({2}) as {3}".format(query_sql, alias, query_sql = "SELECT * FROM ({0}) as {1} UNION ALL ({2})".format(query_sql, alias, _query_sql)
_query_sql,
alias + "A")
elif operator == "~": 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)
@ -378,8 +295,8 @@ class Search(object):
start = time.time() start = time.time()
execute = db.session.execute execute = db.session.execute
# current_app.logger.debug(v_query_sql) current_app.logger.debug(v_query_sql)
res = execute(text(v_query_sql)).fetchall() res = execute(v_query_sql).fetchall()
end_time = time.time() end_time = time.time()
current_app.logger.debug("query ci ids time is: {0}".format(end_time - start)) current_app.logger.debug("query ci ids time is: {0}".format(end_time - start))
@ -388,11 +305,6 @@ class Search(object):
return numfound, res return numfound, res
def __get_type2filter_perms(self):
res2 = ACLManager('cmdb').get_resources(ResourceTypeEnum.CI_FILTER)
if res2:
self.type2filter_perms = CIFilterPermsCRUD().get_by_ids(list(map(int, [i['name'] for i in res2])))
def __get_types_has_read(self): def __get_types_has_read(self):
""" """
:return: _type:(type1;type2) :return: _type:(type1;type2)
@ -402,23 +314,14 @@ class Search(object):
self.valid_type_names = {i['name'] for i in res if PermEnum.READ in i['permissions']} self.valid_type_names = {i['name'] for i in res if PermEnum.READ in i['permissions']}
self.__get_type2filter_perms() res2 = acl.get_resources(ResourceTypeEnum.CI_FILTER)
if res2:
for type_id in self.type2filter_perms: self.type2filter_perms = CIFilterPermsCRUD().get_by_ids(list(map(int, [i['name'] for i in res2])))
ci_type = CITypeCache.get(type_id)
if ci_type:
if self.type2filter_perms[type_id].get('id_filter'):
if self.use_id_filter:
self.valid_type_names.add(ci_type.name)
elif self.type2filter_perms[type_id].get('ci_filter'):
if self.use_ci_filter:
self.valid_type_names.add(ci_type.name)
else:
self.valid_type_names.add(ci_type.name)
return "_type:({})".format(";".join(self.valid_type_names)) return "_type:({})".format(";".join(self.valid_type_names))
def __confirm_type_first(self, queries): def __confirm_type_first(self, queries):
has_type = False has_type = False
result = [] result = []
@ -437,14 +340,11 @@ class Search(object):
if not q.startswith("("): if not q.startswith("("):
raise SearchError(ErrFormat.ci_search_Parentheses_invalid) raise SearchError(ErrFormat.ci_search_Parentheses_invalid)
if ":" not in q: # multi-line search operator, q = self._operator_proc(q)
result.append(q[1:-1].split(';')) if q.endswith(")"):
else: result.append(dict(operator=operator, queries=[q[1:-1]]))
operator, q = self._operator_proc(q)
if q.endswith(")"):
result.append(dict(operator=operator, queries=[q[1:-1]]))
sub = dict(operator=operator, queries=[q[1:]]) sub = dict(operator=operator, queries=[q[1:]])
elif q.endswith(")") and sub: elif q.endswith(")") and sub:
sub['queries'].append(q[:-1]) sub['queries'].append(q[:-1])
result.append(copy.deepcopy(sub)) result.append(copy.deepcopy(sub))
@ -454,10 +354,8 @@ class Search(object):
else: else:
result.append(q) result.append(q)
if self.parent_node_perm_passed: _is_app_admin = is_app_admin('cmdb') or current_user.username == "worker"
self.__get_type2filter_perms() if result and not has_type and not _is_app_admin:
self.valid_type_names = "ALL"
elif result and not has_type and not self.is_app_admin:
type_q = self.__get_types_has_read() type_q = self.__get_types_has_read()
if id_query: if id_query:
ci = CIManager.get_by_id(id_query) ci = CIManager.get_by_id(id_query)
@ -466,21 +364,23 @@ class Search(object):
result.insert(0, "_type:{}".format(ci.type_id)) result.insert(0, "_type:{}".format(ci.type_id))
else: else:
result.insert(0, type_q) result.insert(0, type_q)
elif self.is_app_admin: elif _is_app_admin:
self.valid_type_names = "ALL" self.valid_type_names = "ALL"
else: else:
self.__get_types_has_read() self.__get_types_has_read()
current_app.logger.warning(result)
return result return result
def __query_by_attr(self, q, queries, alias, is_sub=False): def __query_by_attr(self, q, queries, alias):
k = q.split(":")[0].strip() k = q.split(":")[0].strip()
v = "\:".join(q.split(":")[1:]).strip() v = "\:".join(q.split(":")[1:]).strip()
v = v.replace("'", "\\'") v = v.replace("'", "\\'")
v = v.replace('"', '\\"') v = v.replace('"', '\\"')
field, field_type, operator, attr = self._attr_name_proc(k) field, field_type, operator, attr = self._attr_name_proc(k)
if field == "_type": if field == "_type":
_query_sql = self._type_query_handler(v, queries, is_sub) _query_sql = self._type_query_handler(v, queries)
elif field == "_id": elif field == "_id":
_query_sql = self._id_query_handler(v) _query_sql = self._id_query_handler(v)
@ -491,12 +391,6 @@ class Search(object):
is_not = True if operator == "|~" else False is_not = True if operator == "|~" else False
if field_type == ValueTypeEnum.DATE and len(v) == 10:
v = "{} 00:00:00".format(v)
if field_type == ValueTypeEnum.BOOL and "*" not in str(v):
v = str(int(v in current_app.config.get('BOOL_TRUE')))
# in query # in query
if v.startswith("(") and v.endswith(")"): if v.startswith("(") and v.endswith(")"):
_query_sql = self._in_query_handler(attr, v, is_not) _query_sql = self._in_query_handler(attr, v, is_not)
@ -527,36 +421,26 @@ class Search(object):
return alias, _query_sql, operator return alias, _query_sql, operator
def __query_build_by_field(self, queries, is_first=True, only_type_query_special=True, alias='A', operator='&', def __query_build_by_field(self, queries, is_first=True, only_type_query_special=True, alias='A', operator='&'):
is_sub=False):
query_sql = "" query_sql = ""
for q in queries: for q in queries:
# current_app.logger.debug(q)
_query_sql = "" _query_sql = ""
if isinstance(q, dict): if isinstance(q, dict):
if len(q['queries']) == 1 and ";" in q['queries'][0]: alias, _query_sql, operator = self.__query_build_by_field(q['queries'], True, True, alias)
values = q['queries'][0].split(";") current_app.logger.info(_query_sql)
in_values = ",".join("'{0}'".format(v) for v in values) current_app.logger.info((operator, is_first, alias))
_query_sql = QUERY_CI_BY_NO_ATTR_IN.format(in_values, alias) operator = q['operator']
operator = q['operator']
else:
alias, _query_sql, operator = self.__query_build_by_field(q['queries'], True, True, alias,
is_sub=True)
operator = q['operator']
elif ":" in q and not q.startswith("*"): elif ":" in q and not q.startswith("*"):
alias, _query_sql, operator = self.__query_by_attr(q, queries, alias, is_sub) alias, _query_sql, operator = self.__query_by_attr(q, queries, alias)
elif q == "*": elif q == "*":
continue continue
elif q: elif q:
if not isinstance(q, list): q = q.replace("'", "\\'")
q = q.replace("'", "\\'") q = q.replace('"', '\\"')
q = q.replace('"', '\\"') q = q.replace("*", "%").replace('\\n', '%')
q = q.replace("*", "%").replace('\\n', '%') _query_sql = QUERY_CI_BY_NO_ATTR.format(q, alias)
_query_sql = QUERY_CI_BY_NO_ATTR.format(q, alias)
else:
_query_sql = QUERY_CI_BY_NO_ATTR_IN.format(",".join("'{0}'".format(v) for v in q), alias)
if is_first and _query_sql and not self.only_type_query: if is_first and _query_sql and not self.only_type_query:
query_sql = "SELECT * FROM ({0}) AS {1}".format(_query_sql, alias) query_sql = "SELECT * FROM ({0}) AS {1}".format(_query_sql, alias)
@ -575,7 +459,7 @@ class Search(object):
def _filter_ids(self, query_sql): def _filter_ids(self, query_sql):
if self.ci_ids: if self.ci_ids:
return "SELECT * FROM ({0}) AS IN_QUERY WHERE IN_QUERY.ci_id IN ({1})".format( return "SELECT * FROM ({0}) AS IN_QUERY WHERE IN_QUERY.ci_id IN ({1})".format(
query_sql, ",".join(list(set(map(str, self.ci_ids))))) query_sql, ",".join(list(map(str, self.ci_ids))))
return query_sql return query_sql
@ -600,15 +484,13 @@ class Search(object):
queries = handle_arg_list(self.orig_query) queries = handle_arg_list(self.orig_query)
queries = self._extra_handle_query_expr(queries) queries = self._extra_handle_query_expr(queries)
queries = self.__confirm_type_first(queries) queries = self.__confirm_type_first(queries)
current_app.logger.debug(queries)
_, query_sql, _ = self.__query_build_by_field(queries) _, query_sql, _ = self.__query_build_by_field(queries)
s = time.time() s = time.time()
if query_sql: if query_sql:
query_sql = self._filter_ids(query_sql) query_sql = self._filter_ids(query_sql)
if self.raw_ci_ids and not self.ci_ids:
return 0, []
self.query_sql = query_sql self.query_sql = query_sql
# current_app.logger.debug(query_sql) # current_app.logger.debug(query_sql)
numfound, res = self._execute_sql(query_sql) numfound, res = self._execute_sql(query_sql)
@ -624,35 +506,30 @@ class Search(object):
if k: if k:
table_name = TableMap(attr=attr).table_name table_name = TableMap(attr=attr).table_name
query_sql = FACET_QUERY.format(table_name, self.query_sql, attr.id) query_sql = FACET_QUERY.format(table_name, self.query_sql, attr.id)
result = db.session.execute(text(query_sql)).fetchall() # current_app.logger.debug(query_sql)
result = db.session.execute(query_sql).fetchall()
facet[k] = result facet[k] = result
facet_result = dict() facet_result = dict()
for k, v in facet.items(): for k, v in facet.items():
if not k.startswith('_'): if not k.startswith('_'):
attr = AttributeCache.get(k) a = getattr(AttributeCache.get(k), self.ret_key)
a = getattr(attr, self.ret_key) facet_result[a] = [(f[0], f[1], a) for f in v]
facet_result[a] = [(ValueTypeMap.serialize[attr.value_type](f[0]), f[1], a) for f in v]
return facet_result return facet_result
def _fl_build(self): def _fl_build(self):
if isinstance(self.fl, list): _fl = list()
_fl = list() for f in self.fl:
for f in self.fl: k, _, _, _ = self._attr_name_proc(f)
k, _, _, _ = self._attr_name_proc(f) if k:
if k: _fl.append(k)
_fl.append(k)
return _fl return _fl
else:
return self.fl
def search(self): def search(self):
numfound, ci_ids = self._query_build_raw() numfound, ci_ids = self._query_build_raw()
ci_ids = list(map(str, ci_ids)) ci_ids = list(map(str, ci_ids))
if self.only_ids:
return ci_ids
_fl = self._fl_build() _fl = self._fl_build()
@ -665,8 +542,6 @@ class Search(object):
if ci_ids: if ci_ids:
response = CIManager.get_cis_by_ids(ci_ids, ret_key=self.ret_key, fields=_fl, excludes=self.excludes) response = CIManager.get_cis_by_ids(ci_ids, ret_key=self.ret_key, fields=_fl, excludes=self.excludes)
for res in response: for res in response:
if not res:
continue
ci_type = res.get("ci_type") ci_type = res.get("ci_type")
if ci_type not in counter.keys(): if ci_type not in counter.keys():
counter[ci_type] = 0 counter[ci_type] = 0
@ -674,8 +549,3 @@ class Search(object):
total = len(response) total = len(response)
return response, counter, total, self.page, numfound, facet return response, counter, total, self.page, numfound, facet
def get_ci_ids(self):
_, ci_ids = self._query_build_raw()
return ci_ids

View File

@ -1,40 +1,24 @@
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
from collections import Counter
from collections import defaultdict
import copy
import json import json
import networkx as nx from collections import Counter
import sys
from flask import abort from flask import abort
from flask import current_app from flask import current_app
from flask_login import current_user
from api.extensions import rd from api.extensions import rd
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.ci import CIRelationManager from api.lib.cmdb.ci import CIRelationManager
from api.lib.cmdb.ci_type import CITypeRelationManager from api.lib.cmdb.ci_type import CITypeRelationManager
from api.lib.cmdb.const import ConstraintEnum
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION2
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.perms import CIFilterPermsCRUD
from api.lib.cmdb.resp_format import ErrFormat from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.search.ci.db.search import Search as SearchFromDB from api.lib.cmdb.search.ci.db.search import Search as SearchFromDB
from api.lib.cmdb.search.ci.es.search import Search as SearchFromES from api.lib.cmdb.search.ci.es.search import Search as SearchFromES
from api.lib.cmdb.utils import TableMap
from api.lib.cmdb.utils import ValueTypeMap
from api.lib.perm.acl.acl import ACLManager
from api.lib.perm.acl.acl import is_app_admin
from api.models.cmdb import CI from api.models.cmdb import CI
from api.models.cmdb import CITypeRelation
from api.models.cmdb import RelationType
class Search(object): class Search(object):
def __init__(self, root_id=None, def __init__(self, root_id,
level=None, level=None,
query=None, query=None,
fl=None, fl=None,
@ -42,11 +26,7 @@ class Search(object):
page=1, page=1,
count=None, count=None,
sort=None, sort=None,
reverse=False, reverse=False):
ancestor_ids=None,
descendant_ids=None,
has_m2m=None,
root_parent_path=None):
self.orig_query = query self.orig_query = query
self.fl = fl self.fl = fl
self.facet_field = facet_field self.facet_field = facet_field
@ -55,116 +35,36 @@ class Search(object):
self.sort = sort or ("ci_id" if current_app.config.get("USE_ES") else None) self.sort = sort or ("ci_id" if current_app.config.get("USE_ES") else None)
self.root_id = root_id self.root_id = root_id
self.level = level or 0 self.level = level
self.reverse = reverse self.reverse = reverse
self.level2constraint = CITypeRelationManager.get_level2constraint( def _get_ids(self):
root_id[0] if root_id and isinstance(root_id, list) else root_id,
level[0] if isinstance(level, list) and level else level)
self.ancestor_ids = ancestor_ids
self.descendant_ids = descendant_ids
self.root_parent_path = root_parent_path or []
self.has_m2m = has_m2m or False
if not self.has_m2m:
if self.ancestor_ids:
self.has_m2m = True
else:
level = level[0] if isinstance(level, list) and level else level
for _l, c in self.level2constraint.items():
if _l < int(level) and c == ConstraintEnum.Many2Many:
self.has_m2m = True
self.type2filter_perms = {}
self.is_app_admin = is_app_admin('cmdb') or current_user.username == "worker"
def _get_ids(self, ids):
merge_ids = [] merge_ids = []
key = [] ids = [self.root_id] if not isinstance(self.root_id, list) else self.root_id
_tmp = []
for level in range(1, sorted(self.level)[-1] + 1): for level in range(1, sorted(self.level)[-1] + 1):
if len(self.descendant_ids or []) >= level and self.type2filter_perms.get(self.descendant_ids[level - 1]): _tmp = list(map(lambda x: list(json.loads(x).keys()),
id_filter_limit, _ = self._get_ci_filter(self.type2filter_perms[self.descendant_ids[level - 1]]) filter(lambda x: x is not None, rd.get(ids, REDIS_PREFIX_CI_RELATION) or [])))
else:
id_filter_limit = {}
if not self.has_m2m:
key, prefix = list(map(str, ids)), REDIS_PREFIX_CI_RELATION
else:
if not self.ancestor_ids:
if level == 1:
key, prefix = list(map(str, ids)), REDIS_PREFIX_CI_RELATION
else:
key = list(set(["{},{}".format(i, j) for idx, i in enumerate(key) for j in _tmp[idx]]))
prefix = REDIS_PREFIX_CI_RELATION2
else:
if level == 1:
key, prefix = ["{},{}".format(self.ancestor_ids, i) for i in ids], REDIS_PREFIX_CI_RELATION2
else:
key = list(set(["{},{}".format(i, j) for idx, i in enumerate(key) for j in _tmp[idx]]))
prefix = REDIS_PREFIX_CI_RELATION2
if not key or id_filter_limit is None:
return []
res = [json.loads(x).items() for x in [i or '{}' for i in rd.get(key, prefix) or []]]
_tmp = [[i[0] for i in x if (not id_filter_limit or (
key[idx] not in id_filter_limit or int(i[0]) in id_filter_limit[key[idx]]) or
int(i[0]) in id_filter_limit)] for idx, x in enumerate(res)]
ids = [j for i in _tmp for j in i] ids = [j for i in _tmp for j in i]
if level in self.level: if level in self.level:
merge_ids.extend(ids) merge_ids.extend(ids)
return merge_ids return merge_ids
def _get_reverse_ids(self, ids): def _get_reverse_ids(self):
merge_ids = [] merge_ids = []
level2ids = {} ids = [self.root_id] if not isinstance(self.root_id, list) else self.root_id
for level in range(1, sorted(self.level)[-1] + 1): for level in range(1, sorted(self.level)[-1] + 1):
ids, _level2ids = CIRelationManager.get_ancestor_ids(ids, 1) ids = CIRelationManager.get_ancestor_ids(ids, 1)
if _level2ids.get(2):
level2ids[level + 1] = _level2ids[2]
if level in self.level: if level in self.level:
if level in level2ids and level2ids[level]: merge_ids.extend(ids)
merge_ids.extend(set(ids) & set(level2ids[level]))
else:
merge_ids.extend(ids)
return merge_ids return merge_ids
def _has_read_perm_from_parent_nodes(self): def search(self):
self.root_parent_path = list(map(str, self.root_parent_path))
if str(self.root_id).isdigit() and str(self.root_id) not in self.root_parent_path:
self.root_parent_path.append(str(self.root_id))
self.root_parent_path = set(self.root_parent_path)
if self.is_app_admin:
self.type2filter_perms = {}
return True
res = ACLManager().get_resources(ResourceTypeEnum.CI_FILTER) or {}
self.type2filter_perms = CIFilterPermsCRUD().get_by_ids(list(map(int, [i['name'] for i in res]))) or {}
for _, filters in self.type2filter_perms.items():
if set((filters.get('id_filter') or {}).keys()) & self.root_parent_path:
return True
return True
def search(self, only_ids=False):
use_ci_filter = len(self.descendant_ids or []) == self.level[0] - 1
parent_node_perm_passed = not self.is_app_admin and self._has_read_perm_from_parent_nodes()
ids = [self.root_id] if not isinstance(self.root_id, list) else self.root_id ids = [self.root_id] if not isinstance(self.root_id, list) else self.root_id
cis = [CI.get_by_id(_id) or abort(404, ErrFormat.ci_not_found.format("id={}".format(_id))) for _id in ids] cis = [CI.get_by_id(_id) or abort(404, ErrFormat.ci_not_found.format("id={}".format(_id))) for _id in ids]
merge_ids = self._get_ids(ids) if not self.reverse else self._get_reverse_ids(ids) merge_ids = self._get_ids() if not self.reverse else self._get_reverse_ids()
if not self.orig_query or ("_type:" not in self.orig_query if not self.orig_query or ("_type:" not in self.orig_query
and "type_id:" not in self.orig_query and "type_id:" not in self.orig_query
@ -176,11 +76,11 @@ class Search(object):
type_ids.extend(CITypeRelationManager.get_child_type_ids(ci.type_id, level)) type_ids.extend(CITypeRelationManager.get_child_type_ids(ci.type_id, level))
else: else:
type_ids.extend(CITypeRelationManager.get_parent_type_ids(ci.type_id, level)) type_ids.extend(CITypeRelationManager.get_parent_type_ids(ci.type_id, level))
type_ids = set(type_ids) type_ids = list(set(type_ids))
if self.orig_query: if self.orig_query:
self.orig_query = "_type:({0}),{1}".format(";".join(map(str, type_ids)), self.orig_query) self.orig_query = "_type:({0}),{1}".format(";".join(list(map(str, type_ids))), self.orig_query)
else: else:
self.orig_query = "_type:({0})".format(";".join(map(str, type_ids))) self.orig_query = "_type:({0})".format(";".join(list(map(str, type_ids))))
if not merge_ids: if not merge_ids:
# cis, counter, total, self.page, numfound, facet_ # cis, counter, total, self.page, numfound, facet_
@ -201,142 +101,33 @@ class Search(object):
page=self.page, page=self.page,
count=self.count, count=self.count,
sort=self.sort, sort=self.sort,
ci_ids=merge_ids, ci_ids=merge_ids).search()
parent_node_perm_passed=parent_node_perm_passed,
use_ci_filter=use_ci_filter,
only_ids=only_ids).search()
def _get_ci_filter(self, filter_perms, ci_filters=None):
ci_filters = ci_filters or []
if ci_filters:
result = {}
for item in ci_filters:
res = SearchFromDB('_type:{},{}'.format(item['type_id'], item['ci_filter']),
count=sys.maxsize, parent_node_perm_passed=True).get_ci_ids()
if res:
result[item['type_id']] = set(res)
return {}, result if result else None
result = dict()
if filter_perms.get('id_filter'):
for k in filter_perms['id_filter']:
node_path = k.split(',')
if len(node_path) == 1:
result[int(node_path[0])] = 1
elif not self.has_m2m:
result.setdefault(node_path[-2], set()).add(int(node_path[-1]))
else:
result.setdefault(','.join(node_path[:-1]), set()).add(int(node_path[-1]))
if result:
return result, None
else:
return None, None
return {}, None
def statistics(self, type_ids, need_filter=True):
self.level = int(self.level)
acl = ACLManager('cmdb')
type2filter_perms = dict()
if not self.is_app_admin:
res2 = acl.get_resources(ResourceTypeEnum.CI_FILTER)
if res2:
type2filter_perms = CIFilterPermsCRUD().get_by_ids(list(map(int, [i['name'] for i in res2])))
def statistics(self, type_ids):
_tmp = []
ids = [self.root_id] if not isinstance(self.root_id, list) else self.root_id ids = [self.root_id] if not isinstance(self.root_id, list) else self.root_id
_tmp, tmp_res = [], [] for l in range(0, int(self.level)):
level2ids = {} if not l:
for lv in range(1, self.level + 1): _tmp = list(map(lambda x: list(json.loads(x).items()),
level2ids[lv] = [] [i or '{}' for i in rd.get(ids, REDIS_PREFIX_CI_RELATION) or []]))
if need_filter:
id_filter_limit, ci_filter_limit = None, None
if len(self.descendant_ids or []) >= lv and type2filter_perms.get(self.descendant_ids[lv - 1]):
id_filter_limit, _ = self._get_ci_filter(type2filter_perms[self.descendant_ids[lv - 1]])
elif type_ids and self.level == lv:
ci_filters = [type2filter_perms[type_id] for type_id in type_ids if type_id in type2filter_perms]
if ci_filters:
id_filter_limit, ci_filter_limit = self._get_ci_filter({}, ci_filters=ci_filters)
else:
id_filter_limit = {}
else:
id_filter_limit = {}
else: else:
id_filter_limit, ci_filter_limit = {}, {}
if lv == 1:
if not self.has_m2m:
key, prefix = [str(i) for i in ids], REDIS_PREFIX_CI_RELATION
else:
key = ["{},{}".format(self.ancestor_ids, _id) for _id in ids]
if not self.ancestor_ids:
key, prefix = [str(i) for i in ids], REDIS_PREFIX_CI_RELATION
else:
prefix = REDIS_PREFIX_CI_RELATION2
level2ids[lv] = [[i] for i in key]
if not key or id_filter_limit is None:
_tmp = [[]] * len(ids)
continue
res = [json.loads(x).items() for x in [i or '{}' for i in rd.get(key, prefix) or []]]
_tmp = []
if type_ids and lv == self.level:
_tmp = [[i for i in x if i[1] in type_ids and
(not id_filter_limit or (key[idx] not in id_filter_limit or
int(i[0]) in id_filter_limit[key[idx]]) or
int(i[0]) in id_filter_limit)] for idx, x in enumerate(res)]
else:
_tmp = [[i for i in x if (not id_filter_limit or (key[idx] not in id_filter_limit or
int(i[0]) in id_filter_limit[key[idx]]) or
int(i[0]) in id_filter_limit)] for idx, x in enumerate(res)]
if ci_filter_limit:
_tmp = [[j for j in i if j[1] not in ci_filter_limit or int(j[0]) in ci_filter_limit[j[1]]]
for i in _tmp]
else:
for idx, item in enumerate(_tmp): for idx, item in enumerate(_tmp):
if item: if item:
if not self.has_m2m: if type_ids and l == self.level - 1:
key, prefix = [i[0] for i in item], REDIS_PREFIX_CI_RELATION __tmp = list(
map(lambda x: [(_id, type_id) for _id, type_id in json.loads(x).items()
if type_id in type_ids],
filter(lambda x: x is not None,
rd.get([i[0] for i in item], REDIS_PREFIX_CI_RELATION) or [])))
else: else:
key = list(set(['{},{}'.format(j, i[0]) for i in item for j in level2ids[lv - 1][idx]]))
prefix = REDIS_PREFIX_CI_RELATION2
level2ids[lv].append(key) __tmp = list(map(lambda x: list(json.loads(x).items()),
filter(lambda x: x is not None,
rd.get([i[0] for i in item], REDIS_PREFIX_CI_RELATION) or [])))
if key: _tmp[idx] = [j for i in __tmp for j in i]
res = [json.loads(x).items() for x in [i or '{}' for i in rd.get(key, prefix) or []]]
if type_ids and lv == self.level:
tmp_res = [[i for i in x if i[1] in type_ids and
(not id_filter_limit or (
key[idx] not in id_filter_limit or
int(i[0]) in id_filter_limit[key[idx]]) or
int(i[0]) in id_filter_limit)] for idx, x in enumerate(res)]
else:
tmp_res = [[i for i in x if (not id_filter_limit or (
key[idx] not in id_filter_limit or
int(i[0]) in id_filter_limit[key[idx]]) or
int(i[0]) in id_filter_limit)] for idx, x in
enumerate(res)]
if ci_filter_limit:
tmp_res = [[j for j in i if j[1] not in ci_filter_limit or
int(j[0]) in ci_filter_limit[j[1]]] for i in tmp_res]
else:
tmp_res = []
if tmp_res:
_tmp[idx] = [j for i in tmp_res for j in i]
else: else:
_tmp[idx] = [] _tmp[idx] = []
level2ids[lv].append([])
result = {str(_id): len(_tmp[idx]) for idx, _id in enumerate(ids)} result = {str(_id): len(_tmp[idx]) for idx, _id in enumerate(ids)}
@ -344,251 +135,3 @@ class Search(object):
detail={str(_id): dict(Counter([i[1] for i in _tmp[idx]]).items()) for idx, _id in enumerate(ids)}) detail={str(_id): dict(Counter([i[1] for i in _tmp[idx]]).items()) for idx, _id in enumerate(ids)})
return result return result
def search_full(self, type_ids):
def _get_id2name(_type_id):
ci_type = CITypeCache.get(_type_id)
attr = AttributeCache.get(ci_type.unique_id)
value_table = TableMap(attr=attr).table
serializer = ValueTypeMap.serialize[attr.value_type]
unique_value = {i.ci_id: serializer(i.value) for i in value_table.get_by(attr_id=attr.id, to_dict=False)}
attr = AttributeCache.get(ci_type.show_id)
if attr:
value_table = TableMap(attr=attr).table
serializer = ValueTypeMap.serialize[attr.value_type]
show_value = {i.ci_id: serializer(i.value) for i in value_table.get_by(attr_id=attr.id, to_dict=False)}
else:
show_value = unique_value
return show_value, unique_value
self.level = int(self.level)
acl = ACLManager('cmdb')
type2filter_perms = dict()
if not self.is_app_admin:
res2 = acl.get_resources(ResourceTypeEnum.CI_FILTER)
if res2:
type2filter_perms = CIFilterPermsCRUD().get_by_ids(list(map(int, [i['name'] for i in res2])))
ids = [self.root_id] if not isinstance(self.root_id, list) else self.root_id
level_ids = [str(i) for i in ids]
result = []
id2children = {}
id2name = _get_id2name(type_ids[0])
for i in level_ids:
item = dict(id=int(i),
type_id=type_ids[0],
isLeaf=False,
title=id2name[0].get(int(i)),
uniqueValue=id2name[1].get(int(i)),
children=[])
result.append(item)
id2children[str(i)] = item['children']
for lv in range(1, self.level):
type_id = type_ids[lv]
if len(type_ids or []) >= lv and type2filter_perms.get(type_id):
id_filter_limit, _ = self._get_ci_filter(type2filter_perms[type_id])
else:
id_filter_limit = {}
if self.has_m2m and lv != 1:
key, prefix = [i for i in level_ids], REDIS_PREFIX_CI_RELATION2
else:
key, prefix = [i.split(',')[-1] for i in level_ids], REDIS_PREFIX_CI_RELATION
res = [json.loads(x).items() for x in [i or '{}' for i in rd.get(key, prefix) or []]]
res = [[i for i in x if i[1] == type_id and (not id_filter_limit or (key[idx] not in id_filter_limit or
int(i[0]) in id_filter_limit[key[idx]]) or
int(i[0]) in id_filter_limit)] for idx, x in enumerate(res)]
_level_ids = []
id2name = _get_id2name(type_id)
for idx, node_path in enumerate(level_ids):
for child_id, _ in (res[idx] or []):
item = dict(id=int(child_id),
type_id=type_id,
isLeaf=True if lv == self.level - 1 else False,
title=id2name[0].get(int(child_id)),
uniqueValue=id2name[1].get(int(child_id)),
children=[])
id2children[node_path].append(item)
_node_path = "{},{}".format(node_path, child_id)
_level_ids.append(_node_path)
id2children[_node_path] = item['children']
level_ids = _level_ids
return result
@staticmethod
def _get_src_ids(src):
q = src.get('q') or ''
if not q.startswith('_type:'):
q = "_type:{},{}".format(src['type_id'], q)
return SearchFromDB(q, use_ci_filter=True, only_ids=True, count=100000).search()
@staticmethod
def _filter_target_ids(target_ids, type_ids, q):
if not q.startswith('_type:'):
q = "_type:({}),{}".format(";".join(map(str, type_ids)), q)
ci_ids = SearchFromDB(q, ci_ids=target_ids, use_ci_filter=True, only_ids=True, count=100000).search()
cis = CI.get_by(fl=['id', 'type_id'], only_query=True).filter(CI.id.in_(ci_ids))
return [(str(i.id), i.type_id) for i in cis]
@staticmethod
def _path2level(src_type_id, target_type_ids, path):
if not src_type_id or not target_type_ids:
return abort(400, ErrFormat.relation_path_search_src_target_required)
graph = nx.DiGraph()
graph.add_edges_from([(n, _path[idx + 1]) for _path in path for idx, n in enumerate(_path[:-1])])
relation_types = defaultdict(dict)
level2type = defaultdict(set)
type2show_key = dict()
for _path in path:
for idx, node in enumerate(_path[1:]):
level2type[idx + 1].add(node)
src = CITypeCache.get(_path[idx])
target = CITypeCache.get(node)
relation_type = RelationType.get_by(only_query=True).join(
CITypeRelation, CITypeRelation.relation_type_id == RelationType.id).filter(
CITypeRelation.parent_id == src.id).filter(CITypeRelation.child_id == target.id).first()
relation_types[src.alias].update({target.alias: relation_type.name})
if src.id not in type2show_key:
type2show_key[src.id] = AttributeCache.get(src.show_id or src.unique_id).name
if target.id not in type2show_key:
type2show_key[target.id] = AttributeCache.get(target.show_id or target.unique_id).name
nodes = graph.nodes()
return level2type, list(nodes), relation_types, type2show_key
def _build_graph(self, source_ids, source_type_id, level2type, target_type_ids, acl):
type2filter_perms = dict()
if not self.is_app_admin:
res2 = acl.get_resources(ResourceTypeEnum.CI_FILTER)
if res2:
type2filter_perms = CIFilterPermsCRUD().get_by_ids(list(map(int, [i['name'] for i in res2])))
target_type_ids = set(target_type_ids)
graph = nx.DiGraph()
target_ids = []
key = [(str(i), source_type_id) for i in source_ids]
graph.add_nodes_from(key)
for level in level2type:
filter_type_ids = level2type[level]
id_filter_limit = dict()
for _type_id in filter_type_ids:
if type2filter_perms.get(_type_id):
_id_filter_limit, _ = self._get_ci_filter(type2filter_perms[_type_id])
id_filter_limit.update(_id_filter_limit)
has_target = filter_type_ids & target_type_ids
res = [json.loads(x).items() for x in [i or '{}' for i in rd.get([i[0] for i in key],
REDIS_PREFIX_CI_RELATION) or []]]
_key = []
for idx, _id in enumerate(key):
valid_targets = [i for i in res[idx] if i[1] in filter_type_ids and
(not id_filter_limit or int(i[0]) in id_filter_limit)]
_key.extend(valid_targets)
graph.add_edges_from(zip([_id] * len(valid_targets), valid_targets))
if has_target:
target_ids.extend([j[0] for i in res for j in i if j[1] in target_type_ids])
key = copy.deepcopy(_key)
return graph, target_ids
@staticmethod
def _find_paths(graph, source_ids, source_type_id, target_ids, valid_path, max_depth=6):
paths = []
for source_id in source_ids:
_paths = nx.all_simple_paths(graph,
source=(source_id, source_type_id),
target=target_ids,
cutoff=max_depth)
for __path in _paths:
if tuple([i[1] for i in __path]) in valid_path:
paths.append([i[0] for i in __path])
return paths
@staticmethod
def _wrap_path_result(paths, types, valid_path, target_types, type2show_key):
ci_ids = [j for i in paths for j in i]
response, _, _, _, _, _ = SearchFromDB("_type:({})".format(";".join(map(str, types))),
use_ci_filter=False,
ci_ids=list(map(int, ci_ids)),
count=1000000).search()
id2ci = {str(i.get('_id')): i if i['_type'] in target_types else {
type2show_key[i['_type']]: i[type2show_key[i['_type']]],
"ci_type_alias": i["ci_type_alias"],
"_type": i["_type"],
} for i in response}
result = defaultdict(list)
counter = defaultdict(int)
for path in paths:
key = "-".join([id2ci.get(i, {}).get('ci_type_alias') or '' for i in path])
if tuple([id2ci.get(i, {}).get('_type') for i in path]) in valid_path:
counter[key] += 1
result[key].append(path)
return result, counter, id2ci
def search_by_path(self, source, target, path):
"""
:param source: {type_id: id, q: expr}
:param target: {type_ids: [id], q: expr}
:param path: [source_type_id, ..., target_type_id], use type id
:return:
"""
acl = ACLManager('cmdb')
if not self.is_app_admin:
res = {i['name'] for i in acl.get_resources(ResourceTypeEnum.CI_TYPE)}
for type_id in (source.get('type_id') and [source['type_id']] or []) + (target.get('type_ids') or []):
_type = CITypeCache.get(type_id)
if _type and _type.name not in res:
return abort(403, ErrFormat.no_permission.format(_type.alias, PermEnum.READ))
target['type_ids'] = [i[-1] for i in path]
level2type, types, relation_types, type2show_key = self._path2level(
source.get('type_id'), target.get('type_ids'), path)
if not level2type:
return [], {}, 0, self.page, 0, {}, {}
source_ids = self._get_src_ids(source)
graph, target_ids = self._build_graph(source_ids, source['type_id'], level2type, target['type_ids'], acl)
target_ids = self._filter_target_ids(target_ids, target['type_ids'], target.get('q') or '')
paths = self._find_paths(graph,
source_ids,
source['type_id'],
set(target_ids),
{tuple(i): 1 for i in path})
numfound = len(paths)
paths = paths[(self.page - 1) * self.count:self.page * self.count]
response, counter, id2ci = self._wrap_path_result(paths,
types,
{tuple(i): 1 for i in path},
set(target.get('type_ids') or []),
type2show_key)
return response, counter, len(paths), self.page, numfound, id2ci, relation_types, type2show_key

View File

@ -1,251 +0,0 @@
# -*- coding:utf-8 -*-
import json
from flask import abort
from flask import current_app
from flask_login import current_user
from werkzeug.exceptions import BadRequest
from api.extensions import rd
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.ci import CIRelationManager
from api.lib.cmdb.ci_type import CITypeRelationManager
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci import search as ci_search
from api.lib.perm.acl.acl import ACLManager
from api.lib.perm.acl.acl import is_app_admin
from api.models.cmdb import TopologyView
from api.models.cmdb import TopologyViewGroup
class TopologyViewManager(object):
group_cls = TopologyViewGroup
cls = TopologyView
@classmethod
def get_name_by_id(cls, _id):
res = cls.cls.get_by_id(_id)
return res and res.name
def get_view_by_id(self, _id):
res = self.cls.get_by_id(_id)
return res and res.to_dict() or {}
@classmethod
def add_group(cls, name, order):
if order is None:
cur_max_order = cls.group_cls.get_by(only_query=True).order_by(cls.group_cls.order.desc()).first()
cur_max_order = cur_max_order and cur_max_order.order or 0
order = cur_max_order + 1
cls.group_cls.get_by(name=name, first=True, to_dict=False) and abort(
400, ErrFormat.topology_group_exists.format(name))
return cls.group_cls.create(name=name, order=order)
def update_group(self, group_id, name, view_ids):
existed = self.group_cls.get_by_id(group_id) or abort(404, ErrFormat.not_found)
if name is not None and name != existed.name:
existed.update(name=name)
for idx, view_id in enumerate(view_ids):
view = self.cls.get_by_id(view_id)
if view is not None:
view.update(group_id=group_id, order=idx)
return existed.to_dict()
@classmethod
def delete_group(cls, _id):
existed = cls.group_cls.get_by_id(_id) or abort(404, ErrFormat.not_found)
if cls.cls.get_by(group_id=_id, first=True):
return abort(400, ErrFormat.topo_view_exists_cannot_delete_group)
existed.soft_delete()
@classmethod
def group_order(cls, group_ids):
for idx, group_id in enumerate(group_ids):
group = cls.group_cls.get_by_id(group_id)
group.update(order=idx + 1)
@classmethod
def add(cls, name, group_id, option, order=None, **kwargs):
cls.cls.get_by(name=name, first=True) and abort(400, ErrFormat.topology_exists.format(name))
if order is None:
cur_max_order = cls.cls.get_by(group_id=group_id, only_query=True).order_by(
cls.cls.order.desc()).first()
cur_max_order = cur_max_order and cur_max_order.order or 0
order = cur_max_order + 1
inst = cls.cls.create(name=name, group_id=group_id, option=option, order=order, **kwargs).to_dict()
if current_app.config.get('USE_ACL'):
try:
ACLManager().add_resource(name, ResourceTypeEnum.TOPOLOGY_VIEW)
except BadRequest:
pass
ACLManager().grant_resource_to_role(name,
current_user.username,
ResourceTypeEnum.TOPOLOGY_VIEW)
return inst
@classmethod
def update(cls, _id, **kwargs):
existed = cls.cls.get_by_id(_id) or abort(404, ErrFormat.not_found)
existed_name = existed.name
inst = existed.update(filter_none=False, **kwargs).to_dict()
if current_app.config.get('USE_ACL') and existed_name != kwargs.get('name') and kwargs.get('name'):
try:
ACLManager().update_resource(existed_name, kwargs['name'], ResourceTypeEnum.TOPOLOGY_VIEW)
except BadRequest:
pass
return inst
@classmethod
def delete(cls, _id):
existed = cls.cls.get_by_id(_id) or abort(404, ErrFormat.not_found)
existed.soft_delete()
if current_app.config.get("USE_ACL"):
ACLManager().del_resource(existed.name, ResourceTypeEnum.TOPOLOGY_VIEW)
@classmethod
def group_inner_order(cls, _ids):
for idx, _id in enumerate(_ids):
topology = cls.cls.get_by_id(_id)
topology.update(order=idx + 1)
@classmethod
def get_all(cls):
resources = None
if current_app.config.get('USE_ACL') and not is_app_admin('cmdb'):
resources = set([i.get('name') for i in ACLManager().get_resources(ResourceTypeEnum.TOPOLOGY_VIEW)])
groups = cls.group_cls.get_by(to_dict=True)
groups = sorted(groups, key=lambda x: x['order'])
group2pos = {group['id']: idx for idx, group in enumerate(groups)}
topo_views = sorted(cls.cls.get_by(to_dict=True), key=lambda x: x['order'])
other_group = dict(views=[])
for view in topo_views:
if resources is not None and view['name'] not in resources:
continue
if view['group_id']:
groups[group2pos[view['group_id']]].setdefault('views', []).append(view)
else:
other_group['views'].append(view)
if other_group['views']:
groups.append(other_group)
return groups
@staticmethod
def relation_from_ci_type(type_id):
nodes, edges = CITypeRelationManager.get_relations_by_type_id(type_id)
return dict(nodes=nodes, edges=edges)
def topology_view(self, view_id=None, preview=None):
if view_id is not None:
view = self.cls.get_by_id(view_id) or abort(404, ErrFormat.not_found)
central_node_type, central_node_instances, path = (view.central_node_type,
view.central_node_instances, view.path)
else:
central_node_type = preview.get('central_node_type')
central_node_instances = preview.get('central_node_instances')
path = preview.get('path')
nodes, links = [], []
_type = CITypeCache.get(central_node_type)
if not _type:
return dict(nodes=nodes, links=links)
type2meta = {_type.id: _type.icon}
root_ids = []
show_key = AttributeCache.get(_type.show_id or _type.unique_id)
q = (central_node_instances[2:] if central_node_instances.startswith('q=') else
central_node_instances)
s = ci_search(q, fl=['_id', show_key.name], use_id_filter=False, use_ci_filter=False, count=1000000)
try:
response, _, _, _, _, _ = s.search()
except SearchError as e:
current_app.logger.info(e)
return dict(nodes=nodes, links=links)
for i in response:
root_ids.append(i['_id'])
nodes.append(dict(id=str(i['_id']), name=i[show_key.name], type_id=central_node_type))
if not root_ids:
return dict(nodes=nodes, links=links)
prefix = REDIS_PREFIX_CI_RELATION
key = list(map(str, root_ids))
id2node = {}
for level in sorted([i for i in path.keys() if int(i) > 0]):
type_ids = {int(i) for i in path[level]}
res = [json.loads(x).items() for x in [i or '{}' for i in rd.get(key, prefix) or []]]
new_key = []
for idx, from_id in enumerate(key):
for to_id, type_id in res[idx]:
if type_id in type_ids:
links.append({'from': from_id, 'to': to_id})
id2node[to_id] = {'id': to_id, 'type_id': type_id}
new_key.append(to_id)
if type_id not in type2meta:
type2meta[type_id] = CITypeCache.get(type_id).icon
key = new_key
ci_ids = list(map(int, root_ids))
for level in sorted([i for i in path.keys() if int(i) < 0]):
type_ids = {int(i) for i in path[level]}
res = CIRelationManager.get_parent_ids(ci_ids)
_ci_ids = []
for to_id in res:
for from_id, type_id in res[to_id]:
if type_id in type_ids:
from_id, to_id = str(from_id), str(to_id)
links.append({'from': from_id, 'to': to_id})
id2node[from_id] = {'id': str(from_id), 'type_id': type_id}
_ci_ids.append(from_id)
if type_id not in type2meta:
type2meta[type_id] = CITypeCache.get(type_id).icon
ci_ids = _ci_ids
fl = set()
type_ids = {t for lv in path if lv != '0' for t in path[lv]}
type2show = {}
for type_id in type_ids:
ci_type = CITypeCache.get(type_id)
if ci_type:
attr = AttributeCache.get(ci_type.show_id or ci_type.unique_id)
if attr:
fl.add(attr.name)
type2show[type_id] = attr.name
if id2node:
s = ci_search("_id:({})".format(';'.join(id2node.keys())), fl=list(fl),
use_id_filter=False, use_ci_filter=False, count=1000000)
try:
response, _, _, _, _, _ = s.search()
except SearchError:
return dict(nodes=nodes, links=links)
for i in response:
id2node[str(i['_id'])]['name'] = i[type2show[str(i['_type'])]]
nodes.extend(id2node.values())
return dict(nodes=nodes, links=links, type2meta=type2meta)

View File

@ -7,52 +7,25 @@ import json
import re import re
import six import six
from flask import current_app
import api.models.cmdb as model import api.models.cmdb as model
from api.lib.cmdb.cache import AttributeCache from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.const import ValueTypeEnum from api.lib.cmdb.const import ValueTypeEnum
from api.lib.cmdb.resp_format import ErrFormat
TIME_RE = re.compile(r'(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d') TIME_RE = re.compile(r"^(20|21|22|23|[0-1]\d):[0-5]\d:[0-5]\d$")
class ValueDeserializeError(Exception):
pass
def string2int(x): def string2int(x):
v = int(float(x)) return int(float(x))
if v > 2147483647:
raise ValueDeserializeError(ErrFormat.attribute_value_out_of_range)
return v
def str2date(x):
try:
return datetime.datetime.strptime(x, "%Y-%m-%d").date()
except ValueError:
pass
try:
return datetime.datetime.strptime(x, "%Y-%m-%d %H:%M:%S").date()
except ValueError:
pass
def str2datetime(x): def str2datetime(x):
x = x.replace('T', ' ')
x = x.replace('Z', '')
try: try:
return datetime.datetime.strptime(x, "%Y-%m-%d %H:%M:%S") return datetime.datetime.strptime(x, "%Y-%m-%d")
except ValueError: except ValueError:
pass pass
return datetime.datetime.strptime(x, "%Y-%m-%d %H:%M") return datetime.datetime.strptime(x, "%Y-%m-%d %H:%M:%S")
class ValueTypeMap(object): class ValueTypeMap(object):
@ -62,9 +35,8 @@ class ValueTypeMap(object):
ValueTypeEnum.TEXT: lambda x: x, ValueTypeEnum.TEXT: lambda x: x,
ValueTypeEnum.TIME: lambda x: TIME_RE.findall(x)[0], ValueTypeEnum.TIME: lambda x: TIME_RE.findall(x)[0],
ValueTypeEnum.DATETIME: str2datetime, ValueTypeEnum.DATETIME: str2datetime,
ValueTypeEnum.DATE: str2date, ValueTypeEnum.DATE: str2datetime,
ValueTypeEnum.JSON: lambda x: json.loads(x) if isinstance(x, six.string_types) and x else x, ValueTypeEnum.JSON: lambda x: json.loads(x) if isinstance(x, six.string_types) and x else x,
ValueTypeEnum.BOOL: lambda x: x in current_app.config.get('BOOL_TRUE'),
} }
serialize = { serialize = {
@ -72,10 +44,9 @@ class ValueTypeMap(object):
ValueTypeEnum.FLOAT: float, ValueTypeEnum.FLOAT: float,
ValueTypeEnum.TEXT: lambda x: x if isinstance(x, six.string_types) else str(x), ValueTypeEnum.TEXT: lambda x: x if isinstance(x, six.string_types) else str(x),
ValueTypeEnum.TIME: lambda x: x if isinstance(x, six.string_types) else str(x), ValueTypeEnum.TIME: lambda x: x if isinstance(x, six.string_types) else str(x),
ValueTypeEnum.DATE: lambda x: x.strftime("%Y-%m-%d") if not isinstance(x, six.string_types) else x, ValueTypeEnum.DATE: lambda x: x.strftime("%Y-%m-%d"),
ValueTypeEnum.DATETIME: lambda x: x.strftime("%Y-%m-%d %H:%M:%S") if not isinstance(x, six.string_types) else x, ValueTypeEnum.DATETIME: lambda x: x.strftime("%Y-%m-%d %H:%M:%S"),
ValueTypeEnum.JSON: lambda x: json.loads(x) if isinstance(x, six.string_types) and x else x, ValueTypeEnum.JSON: lambda x: json.loads(x) if isinstance(x, six.string_types) and x else x,
ValueTypeEnum.BOOL: lambda x: x in current_app.config.get('BOOL_TRUE'),
} }
serialize2 = { serialize2 = {
@ -86,7 +57,6 @@ class ValueTypeMap(object):
ValueTypeEnum.DATE: lambda x: (x.decode() if not isinstance(x, six.string_types) else x).split()[0], ValueTypeEnum.DATE: lambda x: (x.decode() if not isinstance(x, six.string_types) else x).split()[0],
ValueTypeEnum.DATETIME: lambda x: x.decode() if not isinstance(x, six.string_types) else x, ValueTypeEnum.DATETIME: lambda x: x.decode() if not isinstance(x, six.string_types) else x,
ValueTypeEnum.JSON: lambda x: json.loads(x) if isinstance(x, six.string_types) and x else x, ValueTypeEnum.JSON: lambda x: json.loads(x) if isinstance(x, six.string_types) and x else x,
ValueTypeEnum.BOOL: lambda x: x in current_app.config.get('BOOL_TRUE'),
} }
choice = { choice = {
@ -94,8 +64,6 @@ class ValueTypeMap(object):
ValueTypeEnum.FLOAT: model.FloatChoice, ValueTypeEnum.FLOAT: model.FloatChoice,
ValueTypeEnum.TEXT: model.TextChoice, ValueTypeEnum.TEXT: model.TextChoice,
ValueTypeEnum.TIME: model.TextChoice, ValueTypeEnum.TIME: model.TextChoice,
ValueTypeEnum.DATE: model.TextChoice,
ValueTypeEnum.DATETIME: model.TextChoice,
} }
table = { table = {
@ -108,7 +76,6 @@ class ValueTypeMap(object):
'index_{0}'.format(ValueTypeEnum.TIME): model.CIIndexValueText, 'index_{0}'.format(ValueTypeEnum.TIME): model.CIIndexValueText,
'index_{0}'.format(ValueTypeEnum.FLOAT): model.CIIndexValueFloat, 'index_{0}'.format(ValueTypeEnum.FLOAT): model.CIIndexValueFloat,
'index_{0}'.format(ValueTypeEnum.JSON): model.CIValueJson, 'index_{0}'.format(ValueTypeEnum.JSON): model.CIValueJson,
'index_{0}'.format(ValueTypeEnum.BOOL): model.CIIndexValueInteger,
} }
table_name = { table_name = {
@ -121,7 +88,6 @@ class ValueTypeMap(object):
'index_{0}'.format(ValueTypeEnum.TIME): 'c_value_index_texts', 'index_{0}'.format(ValueTypeEnum.TIME): 'c_value_index_texts',
'index_{0}'.format(ValueTypeEnum.FLOAT): 'c_value_index_floats', 'index_{0}'.format(ValueTypeEnum.FLOAT): 'c_value_index_floats',
'index_{0}'.format(ValueTypeEnum.JSON): 'c_value_json', 'index_{0}'.format(ValueTypeEnum.JSON): 'c_value_json',
'index_{0}'.format(ValueTypeEnum.BOOL): 'c_value_index_integers',
} }
es_type = { es_type = {
@ -131,7 +97,7 @@ class ValueTypeMap(object):
ValueTypeEnum.DATE: 'text', ValueTypeEnum.DATE: 'text',
ValueTypeEnum.TIME: 'text', ValueTypeEnum.TIME: 'text',
ValueTypeEnum.FLOAT: 'float', ValueTypeEnum.FLOAT: 'float',
ValueTypeEnum.JSON: 'object', ValueTypeEnum.JSON: 'object'
} }
@ -144,9 +110,7 @@ class TableMap(object):
@property @property
def table(self): def table(self):
attr = AttributeCache.get(self.attr_name) if not self.attr else self.attr attr = AttributeCache.get(self.attr_name) if not self.attr else self.attr
if attr.is_password or attr.is_link: if attr.value_type != ValueTypeEnum.TEXT and attr.value_type != ValueTypeEnum.JSON:
self.is_index = False
elif attr.value_type not in {ValueTypeEnum.TEXT, ValueTypeEnum.JSON}:
self.is_index = True self.is_index = True
elif self.is_index is None: elif self.is_index is None:
self.is_index = attr.is_index self.is_index = attr.is_index
@ -158,9 +122,7 @@ class TableMap(object):
@property @property
def table_name(self): def table_name(self):
attr = AttributeCache.get(self.attr_name) if not self.attr else self.attr attr = AttributeCache.get(self.attr_name) if not self.attr else self.attr
if attr.is_password or attr.is_link: if attr.value_type != ValueTypeEnum.TEXT and attr.value_type != ValueTypeEnum.JSON:
self.is_index = False
elif attr.value_type not in {ValueTypeEnum.TEXT, ValueTypeEnum.JSON}:
self.is_index = True self.is_index = True
elif self.is_index is None: elif self.is_index is None:
self.is_index = attr.is_index self.is_index = attr.is_index

View File

@ -3,29 +3,27 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import importlib.util
import copy import copy
import jinja2 import imp
import os import os
import re
import tempfile import tempfile
import jinja2
from flask import abort from flask import abort
from flask import current_app from flask import current_app
from jinja2schema import infer from jinja2schema import infer
from jinja2schema import to_json_schema from jinja2schema import to_json_schema
from werkzeug.exceptions import BadRequest
from api.extensions import db from api.extensions import db
from api.lib.cmdb.attribute import AttributeManager from api.lib.cmdb.attribute import AttributeManager
from api.lib.cmdb.cache import AttributeCache from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import CITypeAttributeCache 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 OperateType
from api.lib.cmdb.const import ValueTypeEnum from api.lib.cmdb.const import ValueTypeEnum
from api.lib.cmdb.history import AttributeHistoryManger from api.lib.cmdb.history import AttributeHistoryManger
from api.lib.cmdb.resp_format import ErrFormat from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.utils import TableMap from api.lib.cmdb.utils import TableMap
from api.lib.cmdb.utils import ValueDeserializeError
from api.lib.cmdb.utils import ValueTypeMap from api.lib.cmdb.utils import ValueTypeMap
from api.lib.utils import handle_arg_list from api.lib.utils import handle_arg_list
from api.models.cmdb import CI from api.models.cmdb import CI
@ -47,7 +45,7 @@ class AttributeValueManager(object):
""" """
return AttributeCache.get(key) return AttributeCache.get(key)
def get_attr_values(self, fields, ci_id, ret_key="name", unique_key=None, use_master=False, enum_map=None): def get_attr_values(self, fields, ci_id, ret_key="name", unique_key=None, use_master=False):
""" """
:param fields: :param fields:
@ -55,7 +53,6 @@ class AttributeValueManager(object):
:param ret_key: It can be name or alias :param ret_key: It can be name or alias
:param unique_key: primary attribute :param unique_key: primary attribute
:param use_master: Only for master-slave read-write separation :param use_master: Only for master-slave read-write separation
:param enum_map:
:return: :return:
""" """
res = dict() res = dict()
@ -70,19 +67,12 @@ class AttributeValueManager(object):
use_master=use_master, use_master=use_master,
to_dict=False) to_dict=False)
field_name = getattr(attr, ret_key) field_name = getattr(attr, ret_key)
if attr.is_list: if attr.is_list:
res[field_name] = [ValueTypeMap.serialize[attr.value_type](i.value) for i in rs] res[field_name] = [ValueTypeMap.serialize[attr.value_type](i.value) for i in rs]
elif attr.is_password and rs:
res[field_name] = '******' if rs[0].value else ''
else: else:
res[field_name] = ValueTypeMap.serialize[attr.value_type](rs[0].value) if rs else None res[field_name] = ValueTypeMap.serialize[attr.value_type](rs[0].value) if rs else None
if enum_map and field_name in enum_map:
if attr.is_list:
res[field_name] = [enum_map[field_name].get(i, i) for i in res[field_name]]
else:
res[field_name] = enum_map[field_name].get(res[field_name], res[field_name])
if unique_key is not None and attr.id == unique_key.id and rs: if unique_key is not None and attr.id == unique_key.id and rs:
res['unique'] = unique_key.name res['unique'] = unique_key.name
res['unique_alias'] = unique_key.alias res['unique_alias'] = unique_key.alias
@ -90,31 +80,22 @@ class AttributeValueManager(object):
return res return res
@staticmethod @staticmethod
def _deserialize_value(alias, value_type, value): def _deserialize_value(value_type, value):
if not value: if not value:
return value return value
deserialize = ValueTypeMap.deserialize[value_type] deserialize = ValueTypeMap.deserialize[value_type]
try: try:
v = deserialize(value) v = deserialize(value)
if value_type in (ValueTypeEnum.DATE, ValueTypeEnum.DATETIME):
return str(v)
return v return v
except ValueDeserializeError as e:
return abort(400, ErrFormat.attribute_value_invalid2.format(alias, e))
except ValueError: except ValueError:
return abort(400, ErrFormat.attribute_value_invalid2.format(alias, value)) return abort(400, ErrFormat.attribute_value_invalid.format(value))
@staticmethod @staticmethod
def _check_is_choice(attr, value_type, value): def _check_is_choice(attr, value_type, value):
choice_values = AttributeManager.get_choice_values(attr.id, value_type, attr.choice_web_hook, attr.choice_other) choice_values = AttributeManager.get_choice_values(attr.id, value_type, attr.choice_web_hook)
if value_type == ValueTypeEnum.FLOAT: if str(value) not in list(map(str, [i[0] for i in choice_values])):
if float(value) not in list(map(float, [i[0] for i in choice_values])): return abort(400, ErrFormat.not_in_choice_values.format(value))
return abort(400, ErrFormat.not_in_choice_values.format(value))
else:
if str(value) not in list(map(str, [i[0] for i in choice_values])):
return abort(400, ErrFormat.not_in_choice_values.format(value))
@staticmethod @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):
@ -131,33 +112,19 @@ class AttributeValueManager(object):
if type_attr and type_attr.is_required and not value and value != 0: if type_attr and type_attr.is_required and not value and value != 0:
return abort(400, ErrFormat.attribute_value_required.format(attr.alias)) return abort(400, ErrFormat.attribute_value_required.format(attr.alias))
@staticmethod def _validate(self, attr, value, value_table, ci=None, type_id=None, ci_id=None, type_attr=None):
def check_re(expr, alias, value): ci = ci or {}
if not re.compile(expr).match(str(value)): v = self._deserialize_value(attr.value_type, value)
return abort(400, ErrFormat.attribute_value_invalid2.format(alias, value))
def _validate(self, attr, value, value_table, ci=None, type_id=None, ci_id=None, type_attr=None, unique_name=None): attr.is_choice and value and self._check_is_choice(attr, attr.value_type, v)
if not attr.is_reference: attr.is_unique and self._check_is_unique(
ci = ci or {}
v = self._deserialize_value(attr.alias, attr.value_type, value)
attr.is_choice and value and self._check_is_choice(attr, attr.value_type, v)
else:
v = value or None
(attr.is_unique or attr.name == unique_name) and self._check_is_unique(
value_table, attr, ci and ci.id or ci_id, ci and ci.type_id or type_id, v) 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 attr.is_reference:
return v
if v == "" and attr.value_type not in (ValueTypeEnum.TEXT,): if v == "" and attr.value_type not in (ValueTypeEnum.TEXT,):
v = None v = None
if attr.re_check and value:
self.check_re(attr.re_check, attr.alias, value)
return v return v
@staticmethod @staticmethod
@ -165,15 +132,14 @@ class AttributeValueManager(object):
return AttributeHistoryManger.add(record_id, ci_id, [(attr_id, operate_type, old, new)], type_id) return AttributeHistoryManger.add(record_id, ci_id, [(attr_id, operate_type, old, new)], type_id)
@staticmethod @staticmethod
def write_change2(changed, record_id=None, ticket_id=None): def _write_change2(changed):
record_id = None
for ci_id, attr_id, operate_type, old, new, type_id in changed: for ci_id, attr_id, operate_type, old, new, type_id in changed:
record_id = AttributeHistoryManger.add(record_id, ci_id, [(attr_id, operate_type, old, new)], type_id, record_id = AttributeHistoryManger.add(record_id, ci_id, [(attr_id, operate_type, old, new)], type_id,
ticket_id=ticket_id,
commit=False, flush=False) commit=False, flush=False)
try: try:
db.session.commit() db.session.commit()
except Exception as e: except Exception as e:
db.session.rollback()
current_app.logger.error("write change failed: {}".format(str(e))) current_app.logger.error("write change failed: {}".format(str(e)))
return record_id return record_id
@ -198,11 +164,11 @@ class AttributeValueManager(object):
try: try:
path = script_f.name path = script_f.name
name = os.path.basename(path)[:-3] dir_name, name = os.path.dirname(path), os.path.basename(path)[:-3]
spec = importlib.util.spec_from_file_location(name, path) fp, path, desc = imp.find_module(name, [dir_name])
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod) mod = imp.load_module(name, fp, path, desc)
if hasattr(mod, 'computed'): if hasattr(mod, 'computed'):
return mod.computed() return mod.computed()
@ -237,10 +203,7 @@ class AttributeValueManager(object):
if computed_value is not None: if computed_value is not None:
ci_dict[attr['name']] = computed_value ci_dict[attr['name']] = computed_value
def valid_attr_value(self, ci_dict, type_id, ci_id, name2attr, def valid_attr_value(self, ci_dict, type_id, ci_id, name2attr, alias2attr=None, ci_attr2type_attr=None):
alias2attr=None,
ci_attr2type_attr=None,
unique_name=None):
key2attr = dict() key2attr = dict()
alias2attr = alias2attr or {} alias2attr = alias2attr or {}
ci_attr2type_attr = ci_attr2type_attr or {} ci_attr2type_attr = ci_attr2type_attr or {}
@ -253,29 +216,17 @@ class AttributeValueManager(object):
try: try:
if attr.is_list: if attr.is_list:
if isinstance(value, dict):
if value.get('op') == "delete":
value['v'] = [ValueTypeMap.serialize[attr.value_type](
self._deserialize_value(attr.alias, attr.value_type, i))
for i in handle_arg_list(value['v'])]
continue
_value = value.get('v') or []
else:
_value = value
value_list = [self._validate(attr, i, value_table, ci=None, type_id=type_id, ci_id=ci_id, value_list = [self._validate(attr, i, value_table, ci=None, type_id=type_id, ci_id=ci_id,
type_attr=ci_attr2type_attr.get(attr.id)) type_attr=ci_attr2type_attr.get(attr.id))
for i in handle_arg_list(_value)] for i in handle_arg_list(value)]
ci_dict[key] = value_list if not isinstance(value, dict) else dict(op=value.get('op'), v=value_list) ci_dict[key] = value_list
if not value_list: if not value_list:
self._check_is_required(type_id, attr, '') self._check_is_required(type_id, attr, '')
else: else:
value = self._validate(attr, value, value_table, ci=None, type_id=type_id, ci_id=ci_id, value = self._validate(attr, value, value_table, ci=None, type_id=type_id, ci_id=ci_id,
type_attr=ci_attr2type_attr.get(attr.id), type_attr=ci_attr2type_attr.get(attr.id))
unique_name=unique_name)
ci_dict[key] = value ci_dict[key] = value
except BadRequest as e:
raise
except Exception as e: except Exception as e:
current_app.logger.warning(str(e)) current_app.logger.warning(str(e))
@ -284,17 +235,15 @@ class AttributeValueManager(object):
return key2attr return key2attr
def create_or_update_attr_value(self, ci, ci_dict, key2attr, ticket_id=None): def create_or_update_attr_value2(self, ci, ci_dict, key2attr):
""" """
add or update attribute value, then write history add or update attribute value, then write history
:param ci: instance object :param ci: instance object
:param ci_dict: attribute dict :param ci_dict: attribute dict
:param key2attr: attr key to attr :param key2attr: attr key to attr
:param ticket_id:
:return: :return:
""" """
changed = [] changed = []
has_dynamic = False
for key, value in ci_dict.items(): for key, value in ci_dict.items():
attr = key2attr.get(key) attr = key2attr.get(key)
if not attr: if not attr:
@ -303,90 +252,106 @@ class AttributeValueManager(object):
if attr.is_list: if attr.is_list:
existed_attrs = value_table.get_by(attr_id=attr.id, ci_id=ci.id, to_dict=False) existed_attrs = value_table.get_by(attr_id=attr.id, ci_id=ci.id, to_dict=False)
existed_values = [(ValueTypeMap.serialize[attr.value_type](i.value) if existed_values = [i.value for i in existed_attrs]
i.value or i.value == 0 else i.value) for i in existed_attrs] added = set(value) - set(existed_values)
deleted = set(existed_values) - set(value)
for v in added:
value_table.create(ci_id=ci.id, attr_id=attr.id, value=v, flush=False, commit=False)
changed.append((ci.id, attr.id, OperateType.ADD, None, v, ci.type_id))
if isinstance(value, dict): for v in deleted:
if value.get('op') == "add": existed_attr = existed_attrs[existed_values.index(v)]
for v in (value.get('v') or []): existed_attr.delete(flush=False, commit=False)
if v not in existed_values: changed.append((ci.id, attr.id, OperateType.DELETE, v, None, ci.type_id))
value_table.create(ci_id=ci.id, attr_id=attr.id, value=v, flush=False, commit=False)
if not attr.is_dynamic:
changed.append((ci.id, attr.id, OperateType.ADD, None, v, ci.type_id))
else:
has_dynamic = True
elif value.get('op') == "delete":
for v in (value.get('v') or []):
if v in existed_values:
existed_attrs[existed_values.index(v)].delete(flush=False, commit=False)
if not attr.is_dynamic:
changed.append((ci.id, attr.id, OperateType.DELETE, v, None, ci.type_id))
else:
has_dynamic = True
else:
# Comparison array starts from which position changes
min_len = min(len(value), len(existed_values))
index = 0
while index < min_len:
if value[index] != existed_values[index]:
break
index += 1
# Delete first and then add to ensure id sorting
for idx in range(index, len(existed_attrs)):
existed_attr = existed_attrs[idx]
existed_attr.delete(flush=False, commit=False)
if not attr.is_dynamic:
changed.append((ci.id, attr.id, OperateType.DELETE, existed_values[idx], None, ci.type_id))
else:
has_dynamic = True
for idx in range(index, len(value)):
value_table.create(ci_id=ci.id, attr_id=attr.id, value=value[idx], flush=False, commit=False)
if not attr.is_dynamic:
changed.append((ci.id, attr.id, OperateType.ADD, None, value[idx], ci.type_id))
else:
has_dynamic = True
else: else:
existed_attr = value_table.get_by(attr_id=attr.id, ci_id=ci.id, first=True, to_dict=False) 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 existed_value = existed_attr and existed_attr.value
existed_value = (ValueTypeMap.serialize[attr.value_type](existed_value) if
existed_value or existed_value == 0 else existed_value)
if existed_value is None and value is not None: if existed_value is None and value is not None:
value_table.create(ci_id=ci.id, attr_id=attr.id, value=value, flush=False, commit=False) value_table.create(ci_id=ci.id, attr_id=attr.id, value=value, flush=False, commit=False)
if not attr.is_dynamic: changed.append((ci.id, attr.id, OperateType.ADD, None, value, ci.type_id))
changed.append((ci.id, attr.id, OperateType.ADD, None, value, ci.type_id))
else:
has_dynamic = True
else: else:
if existed_value != value and existed_attr: if existed_value != value:
if value is None: if value is None:
existed_attr.delete(flush=False, commit=False) existed_attr.delete(flush=False, commit=False)
else: else:
existed_attr.update(value=value, flush=False, commit=False) existed_attr.update(value=value, flush=False, commit=False)
if not attr.is_dynamic: changed.append((ci.id, attr.id, OperateType.UPDATE, existed_value, value, ci.type_id))
changed.append((ci.id, attr.id, OperateType.UPDATE, existed_value, value, ci.type_id))
try:
db.session.commit()
except Exception as e:
db.session.rollback()
current_app.logger.warning(str(e))
return abort(400, ErrFormat.attribute_value_unknown_error.format(str(e)))
return self._write_change2(changed)
def create_or_update_attr_value(self, key, value, ci, _no_attribute_policy=ExistPolicy.IGNORE, record_id=None):
"""
add or update attribute value, then write history
:param key: id, name or alias
:param value:
:param ci: instance object
:param _no_attribute_policy: ignore or reject
:param record_id: op record
:return:
"""
attr = self._get_attr(key)
if attr is None:
if _no_attribute_policy == ExistPolicy.IGNORE:
return
if _no_attribute_policy == ExistPolicy.REJECT:
return abort(400, ErrFormat.attribute_not_found.format(key))
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: else:
has_dynamic = True existed_attr.update(value=value)
if changed or has_dynamic: record_id = self._write_change(ci.id, attr.id, OperateType.UPDATE,
try: existed_value, value, record_id, ci.type_id)
db.session.commit()
except Exception as e:
db.session.rollback()
current_app.logger.warning(str(e))
return abort(400, ErrFormat.attribute_value_unknown_error.format(e.args[0]))
return self.write_change2(changed, ticket_id=ticket_id), has_dynamic return record_id
else: except Exception as e:
return None, has_dynamic current_app.logger.warning(str(e))
return abort(400, ErrFormat.attribute_value_invalid2.format("{}({})".format(attr.alias, attr.name), value))
@staticmethod @staticmethod
def delete_attr_value(attr_id, ci_id, commit=True): def delete_attr_value(attr_id, ci_id):
attr = AttributeCache.get(attr_id) attr = AttributeCache.get(attr_id)
if attr is not None: if attr is not None:
value_table = TableMap(attr=attr).table value_table = TableMap(attr=attr).table
for item in value_table.get_by(attr_id=attr.id, ci_id=ci_id, to_dict=False): for item in value_table.get_by(attr_id=attr.id, ci_id=ci_id, to_dict=False):
item.delete(commit=commit) item.delete()

View File

@ -1,20 +1,13 @@
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
from flask import abort
from flask import current_app from flask import current_app
from api.lib.common_setting.resp_format import ErrFormat from api.lib.common_setting.resp_format import ErrFormat
from api.lib.perm.acl.app import AppCRUD
from api.lib.perm.acl.cache import RoleCache, AppCache from api.lib.perm.acl.cache import RoleCache, AppCache
from api.lib.perm.acl.permission import PermissionCRUD
from api.lib.perm.acl.resource import ResourceTypeCRUD, ResourceCRUD
from api.lib.perm.acl.role import RoleCRUD, RoleRelationCRUD from api.lib.perm.acl.role import RoleCRUD, RoleRelationCRUD
from api.lib.perm.acl.user import UserCRUD from api.lib.perm.acl.user import UserCRUD
def validate_app(app_id):
app = AppCache.get(app_id)
return app.id if app else None
class ACLManager(object): class ACLManager(object):
def __init__(self, app_name='acl', uid=None): def __init__(self, app_name='acl', uid=None):
self.log = current_app.logger self.log = current_app.logger
@ -85,69 +78,19 @@ class ACLManager(object):
return role.to_dict() return role.to_dict()
@staticmethod @staticmethod
def delete_role(_id): def delete_role(_id, payload):
RoleCRUD.delete_role(_id) RoleCRUD.delete_role(_id)
return dict(rid=_id) return dict(rid=_id)
def get_user_info(self, username): def get_user_info(self, username):
from api.lib.perm.acl.acl import ACLManager as ACL from api.lib.perm.acl.acl import ACLManager as ACL
user_info = ACL().get_user_info(username, self.app_name) user_info = ACL().get_user_info(username, self.app_name)
result = dict( result = dict(name=user_info.get('nickname') or username,
name=user_info.get('nickname') or username, username=user_info.get('username') or username,
username=user_info.get('username') or username, email=user_info.get('email'),
email=user_info.get('email'), uid=user_info.get('uid'),
uid=user_info.get('uid'), rid=user_info.get('rid'),
rid=user_info.get('rid'), role=dict(permissions=user_info.get('parents')),
role=dict(permissions=user_info.get('parents')), avatar=user_info.get('avatar'))
avatar=user_info.get('avatar')
)
return result return result
def validate_app(self):
return AppCache.get(self.app_name)
def get_all_resources_types(self, q=None, page=1, page_size=999999):
app_id = self.validate_app().id
numfound, res, id2perms = ResourceTypeCRUD.search(q, app_id, page, page_size)
return dict(
numfound=numfound,
groups=[i.to_dict() for i in res],
id2perms=id2perms
)
def create_resources_type(self, payload):
payload['app_id'] = self.validate_app().id
rt = ResourceTypeCRUD.add(**payload)
return rt.to_dict()
def update_resources_type(self, _id, payload):
rt = ResourceTypeCRUD.update(_id, **payload)
return rt.to_dict()
def create_resource(self, payload):
payload['app_id'] = self.validate_app().id
resource = ResourceCRUD.add(**payload)
return resource.to_dict()
def get_resource_by_type(self, q, u, rt_id, page=1, page_size=999999):
numfound, res = ResourceCRUD.search(q, u, self.validate_app().id, rt_id, page, page_size)
return res
@staticmethod
def grant_resource(rid, resource_id, perms):
PermissionCRUD.grant(rid, perms, resource_id=resource_id, group_id=None)
@staticmethod
def create_app(payload):
rt = AppCRUD.add(**payload)
return rt.to_dict()
def role_has_perms(self, rid, resource_name, resource_type_name, perm):
app_id = validate_app(self.app_name)
return RoleCRUD.has_permission(rid, resource_name, resource_type_name, app_id, perm)

View File

@ -1,24 +1,14 @@
import copy from flask import abort
import json
from flask import abort, current_app
from ldap3 import Connection
from ldap3 import Server
from ldap3.core.exceptions import LDAPBindError, LDAPSocketOpenError
from ldap3 import AUTO_BIND_NO_TLS
from api.extensions import db from api.extensions import db
from api.lib.common_setting.resp_format import ErrFormat from api.lib.common_setting.resp_format import ErrFormat
from api.models.common_setting import CommonData from api.models.common_setting import CommonData
from api.lib.utils import AESCrypto
from api.lib.common_setting.const import AuthCommonConfig, AuthenticateType, AuthCommonConfigAutoRedirect, TestType
class CommonDataCRUD(object): class CommonDataCRUD(object):
@staticmethod @staticmethod
def get_data_by_type(data_type): def get_data_by_type(data_type):
CommonDataCRUD.check_auth_type(data_type)
return CommonData.get_by(data_type=data_type) return CommonData.get_by(data_type=data_type)
@staticmethod @staticmethod
@ -28,8 +18,6 @@ class CommonDataCRUD(object):
@staticmethod @staticmethod
def create_new_data(data_type, **kwargs): def create_new_data(data_type, **kwargs):
try: try:
CommonDataCRUD.check_auth_type(data_type)
return CommonData.create(data_type=data_type, **kwargs) return CommonData.create(data_type=data_type, **kwargs)
except Exception as e: except Exception as e:
db.session.rollback() db.session.rollback()
@ -41,7 +29,6 @@ class CommonDataCRUD(object):
if not existed: if not existed:
abort(404, ErrFormat.common_data_not_found.format(_id)) abort(404, ErrFormat.common_data_not_found.format(_id))
try: try:
CommonDataCRUD.check_auth_type(existed.data_type)
return existed.update(**kwargs) return existed.update(**kwargs)
except Exception as e: except Exception as e:
db.session.rollback() db.session.rollback()
@ -53,230 +40,7 @@ class CommonDataCRUD(object):
if not existed: if not existed:
abort(404, ErrFormat.common_data_not_found.format(_id)) abort(404, ErrFormat.common_data_not_found.format(_id))
try: try:
CommonDataCRUD.check_auth_type(existed.data_type)
existed.soft_delete() existed.soft_delete()
except Exception as e: except Exception as e:
db.session.rollback() db.session.rollback()
abort(400, str(e)) abort(400, str(e))
@staticmethod
def check_auth_type(data_type):
if data_type in list(AuthenticateType.all()) + [AuthCommonConfig]:
abort(400, ErrFormat.common_data_not_support_auth_type.format(data_type))
@staticmethod
def set_auth_auto_redirect_enable(_value: int):
existed = CommonData.get_by(first=True, data_type=AuthCommonConfig, to_dict=False)
if not existed:
CommonDataCRUD.create_new_data(AuthCommonConfig, data={AuthCommonConfigAutoRedirect: _value})
else:
data = existed.data
data = copy.deepcopy(existed.data) if data else {}
data[AuthCommonConfigAutoRedirect] = _value
CommonDataCRUD.update_data(existed.id, data=data)
return True
@staticmethod
def get_auth_auto_redirect_enable():
existed = CommonData.get_by(first=True, data_type=AuthCommonConfig)
if not existed:
return 0
data = existed.get('data', {})
if not data:
return 0
return data.get(AuthCommonConfigAutoRedirect, 0)
class AuthenticateDataCRUD(object):
common_type_list = [AuthCommonConfig]
def __init__(self, _type):
self._type = _type
self.record = None
self.decrypt_data = {}
def get_support_type_list(self):
return list(AuthenticateType.all()) + self.common_type_list
def get(self):
if not self.decrypt_data:
self.decrypt_data = self.get_decrypt_data()
return self.decrypt_data
def get_by_key(self, _key):
if not self.decrypt_data:
self.decrypt_data = self.get_decrypt_data()
return self.decrypt_data.get(_key, None)
def get_record(self, to_dict=False) -> CommonData:
return CommonData.get_by(first=True, data_type=self._type, to_dict=to_dict)
def get_record_with_decrypt(self) -> dict:
record = CommonData.get_by(first=True, data_type=self._type, to_dict=True)
if not record:
return {}
data = self.get_decrypt_dict(record.get('data', ''))
record['data'] = data
return record
def get_decrypt_dict(self, data):
decrypt_str = self.decrypt(data)
try:
return json.loads(decrypt_str)
except Exception as e:
abort(400, str(e))
def get_decrypt_data(self) -> dict:
self.record = self.get_record()
if not self.record:
return self.get_from_config()
return self.get_decrypt_dict(self.record.data)
def get_from_config(self):
return current_app.config.get(self._type, {})
def check_by_type(self) -> None:
existed = self.get_record()
if existed:
abort(400, ErrFormat.common_data_already_existed.format(self._type))
def create(self, data) -> CommonData:
self.check_by_type()
encrypt = data.pop('encrypt', None)
if encrypt is False:
return CommonData.create(data_type=self._type, data=data)
encrypted_data = self.encrypt(data)
try:
return CommonData.create(data_type=self._type, data=encrypted_data)
except Exception as e:
db.session.rollback()
abort(400, str(e))
def update_by_record(self, record, data) -> CommonData:
encrypt = data.pop('encrypt', None)
if encrypt is False:
return record.update(data=data)
encrypted_data = self.encrypt(data)
try:
return record.update(data=encrypted_data)
except Exception as e:
db.session.rollback()
abort(400, str(e))
def update(self, _id, data) -> CommonData:
existed = CommonData.get_by(first=True, to_dict=False, id=_id)
if not existed:
abort(404, ErrFormat.common_data_not_found.format(_id))
return self.update_by_record(existed, data)
@staticmethod
def delete(_id) -> None:
existed = CommonData.get_by(first=True, to_dict=False, id=_id)
if not existed:
abort(404, ErrFormat.common_data_not_found.format(_id))
try:
existed.soft_delete()
except Exception as e:
db.session.rollback()
abort(400, str(e))
@staticmethod
def encrypt(data) -> str:
if type(data) is dict:
try:
data = json.dumps(data)
except Exception as e:
abort(400, str(e))
return AESCrypto().encrypt(data)
@staticmethod
def decrypt(data) -> str:
return AESCrypto().decrypt(data)
@staticmethod
def get_enable_list():
all_records = CommonData.query.filter(
CommonData.data_type.in_(AuthenticateType.all()),
CommonData.deleted == 0
).all()
enable_list = []
for auth_type in AuthenticateType.all():
record = list(filter(lambda x: x.data_type == auth_type, all_records))
if not record:
config = current_app.config.get(auth_type, None)
if not config:
continue
if config.get('enable', False):
enable_list.append(dict(
auth_type=auth_type,
))
continue
try:
decrypt_data = json.loads(AuthenticateDataCRUD.decrypt(record[0].data))
except Exception as e:
current_app.logger.error(e)
continue
if decrypt_data.get('enable', 0) == 1:
enable_list.append(dict(
auth_type=auth_type,
))
auth_auto_redirect = CommonDataCRUD.get_auth_auto_redirect_enable()
return dict(
enable_list=enable_list,
auth_auto_redirect=auth_auto_redirect,
)
def test(self, test_type, data):
type_lower = self._type.lower()
func_name = f'test_{type_lower}'
if hasattr(self, func_name):
try:
return getattr(self, f'test_{type_lower}')(test_type, data)
except Exception as e:
abort(400, str(e))
abort(400, ErrFormat.not_support_test.format(self._type))
@staticmethod
def test_ldap(test_type, data):
ldap_server = data.get('ldap_server')
ldap_user_dn = data.get('ldap_user_dn', '{}')
server = Server(ldap_server, connect_timeout=2)
if not server.check_availability():
raise Exception(ErrFormat.ldap_server_connect_not_available)
else:
if test_type == TestType.Connect:
return True
username = data.get('username', None)
if not username:
raise Exception(ErrFormat.ldap_test_username_required)
user = ldap_user_dn.format(username)
password = data.get('password', None)
try:
Connection(server, user=user, password=password, auto_bind=AUTO_BIND_NO_TLS)
except LDAPBindError:
ldap_domain = data.get('ldap_domain')
user_with_domain = f"{username}@{ldap_domain}"
try:
Connection(server, user=user_with_domain, password=password, auto_bind=AUTO_BIND_NO_TLS)
except Exception as e:
raise Exception(ErrFormat.ldap_test_unknown_error.format(str(e)))
except LDAPSocketOpenError:
raise Exception(ErrFormat.ldap_server_connect_timeout)
except Exception as e:
raise Exception(ErrFormat.ldap_test_unknown_error.format(str(e)))
return True

View File

@ -1,7 +1,5 @@
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
from urllib.parse import urlparse
from api.extensions import cache
from api.models.common_setting import CompanyInfo from api.models.common_setting import CompanyInfo
@ -13,51 +11,14 @@ class CompanyInfoCRUD(object):
@staticmethod @staticmethod
def create(**kwargs): def create(**kwargs):
CompanyInfoCRUD.check_data(**kwargs) return CompanyInfo.create(**kwargs)
res = CompanyInfo.create(**kwargs)
CompanyInfoCache.refresh(res.info)
return res
@staticmethod @staticmethod
def update(_id, **kwargs): def update(_id, **kwargs):
kwargs.pop('id', None) kwargs.pop('id', None)
existed = CompanyInfo.get_by_id(_id) existed = CompanyInfo.get_by_id(_id)
if not existed: if not existed:
existed = CompanyInfoCRUD.create(**kwargs) return CompanyInfoCRUD.create(**kwargs)
else: else:
CompanyInfoCRUD.check_data(**kwargs)
existed = existed.update(**kwargs) existed = existed.update(**kwargs)
CompanyInfoCache.refresh(existed.info) return existed
return existed
@staticmethod
def check_data(**kwargs):
info = kwargs.get('info', {})
info['messenger'] = CompanyInfoCRUD.check_messenger(info.get('messenger', None))
kwargs['info'] = info
@staticmethod
def check_messenger(messenger):
if not messenger:
return messenger
parsed_url = urlparse(messenger)
return f"{parsed_url.scheme}://{parsed_url.netloc}"
class CompanyInfoCache(object):
key = 'CompanyInfoCache::'
@classmethod
def get(cls):
info = cache.get(cls.key)
if not info:
res = CompanyInfo.get_by(first=True) or {}
info = res.get('info', {})
cache.set(cls.key, info)
return info
@classmethod
def refresh(cls, info):
cache.set(cls.key, info)

View File

@ -12,55 +12,3 @@ class OperatorType(BaseEnum):
LESS_THAN = 6 LESS_THAN = 6
IS_EMPTY = 7 IS_EMPTY = 7
IS_NOT_EMPTY = 8 IS_NOT_EMPTY = 8
BotNameMap = {
'wechatApp': 'wechatBot',
'feishuApp': 'feishuBot',
'dingdingApp': 'dingdingBot',
}
class AuthenticateType(BaseEnum):
CAS = 'CAS'
OAUTH2 = 'OAUTH2'
OIDC = 'OIDC'
LDAP = 'LDAP'
AuthCommonConfig = 'AuthCommonConfig'
AuthCommonConfigAutoRedirect = 'auto_redirect'
class TestType(BaseEnum):
Connect = 'connect'
Login = 'login'
MIMEExtMap = {
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx',
'application/msword': '.doc',
'application/vnd.ms-word.document.macroEnabled.12': '.docm',
'application/vnd.ms-excel': '.xls',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx',
'application/vnd.ms-excel.sheet.macroEnabled.12': '.xlsm',
'application/vnd.ms-powerpoint': '.ppt',
'application/vnd.openxmlformats-officedocument.presentationml.presentation': '.pptx',
'application/vnd.ms-powerpoint.presentation.macroEnabled.12': '.pptm',
'application/zip': '.zip',
'application/x-7z-compressed': '.7z',
'application/json': '.json',
'application/pdf': '.pdf',
'image/png': '.png',
'image/bmp': '.bmp',
'image/prs.btif': '.btif',
'image/gif': '.gif',
'image/jpeg': '.jpg',
'image/tiff': '.tif',
'image/vnd.microsoft.icon': '.ico',
'image/webp': '.webp',
'image/svg+xml': '.svg',
'image/vnd.adobe.photoshop': '.psd',
'text/plain': '.txt',
'text/csv': '.csv',
}

View File

@ -1,39 +0,0 @@
import functools
from flask import abort, session, current_app
from api.lib.common_setting.acl import ACLManager
from api.lib.common_setting.resp_format import ErrFormat
from api.lib.perm.acl.acl import is_app_admin
def perms_role_required(app_name, resource_type_name, resource_name, perm, role_name=None):
def decorator_perms_role_required(func):
@functools.wraps(func)
def wrapper_required(*args, **kwargs):
acl = ACLManager(app_name)
has_perms = False
try:
has_perms = acl.role_has_perms(session["acl"]['rid'], resource_name, resource_type_name, perm)
except Exception as e:
current_app.logger.error(f"acl role_has_perms err: {e}")
# resource_type not exist, continue check role
if role_name:
if role_name not in session.get("acl", {}).get("parentRoles", []) and not is_app_admin(app_name):
abort(403, ErrFormat.role_required.format(role_name))
return func(*args, **kwargs)
else:
abort(403, ErrFormat.resource_no_permission.format(resource_name, perm))
if not has_perms:
if role_name:
if role_name not in session.get("acl", {}).get("parentRoles", []) and not is_app_admin(app_name):
abort(403, ErrFormat.role_required.format(role_name))
else:
abort(403, ErrFormat.resource_no_permission.format(resource_name, perm))
return func(*args, **kwargs)
return wrapper_required
return decorator_perms_role_required

View File

@ -1,6 +1,6 @@
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
from flask import abort, current_app from flask import abort
from treelib import Tree from treelib import Tree
from wtforms import Form from wtforms import Form
from wtforms import IntegerField from wtforms import IntegerField
@ -9,7 +9,6 @@ from wtforms import validators
from api.extensions import db from api.extensions import db
from api.lib.common_setting.resp_format import ErrFormat from api.lib.common_setting.resp_format import ErrFormat
from api.lib.common_setting.acl import ACLManager
from api.lib.perm.acl.role import RoleCRUD from api.lib.perm.acl.role import RoleCRUD
from api.models.common_setting import Department, Employee from api.models.common_setting import Department, Employee
@ -24,15 +23,7 @@ def get_all_department_list(to_dict=True):
*criterion *criterion
).order_by(Department.department_id.asc()) ).order_by(Department.department_id.asc())
results = query.all() results = query.all()
if to_dict: return [r.to_dict() for r in results] if to_dict else results
datas = []
for r in results:
d = r.to_dict()
if r.department_id == 0:
d['department_name'] = ErrFormat.company_wide
datas.append(d)
return datas
return results
def get_all_employee_list(block=0, to_dict=True): def get_all_employee_list(block=0, to_dict=True):
@ -109,7 +100,6 @@ class DepartmentTree(object):
employees = self.get_employees_by_d_id(department_id) employees = self.get_employees_by_d_id(department_id)
top_d['employees'] = employees top_d['employees'] = employees
top_d['department_name'] = ErrFormat.company_wide
if len(sub_deps) == 0: if len(sub_deps) == 0:
top_d[sub_departments_column_name] = [] top_d[sub_departments_column_name] = []
d_list.append(top_d) d_list.append(top_d)
@ -162,10 +152,6 @@ class DepartmentForm(Form):
class DepartmentCRUD(object): class DepartmentCRUD(object):
@staticmethod
def get_department_by_id(d_id, to_dict=True):
return Department.get_by(first=True, department_id=d_id, to_dict=to_dict)
@staticmethod @staticmethod
def add(**kwargs): def add(**kwargs):
DepartmentCRUD.check_department_name_unique(kwargs['department_name']) DepartmentCRUD.check_department_name_unique(kwargs['department_name'])
@ -200,11 +186,10 @@ class DepartmentCRUD(object):
filter(lambda d: d['department_id'] == department_parent_id, allow_p_d_id_list)) filter(lambda d: d['department_id'] == department_parent_id, allow_p_d_id_list))
if len(target) == 0: if len(target) == 0:
try: try:
dep = Department.get_by( d = Department.get_by(
first=True, to_dict=False, department_id=department_parent_id) first=True, to_dict=False, department_id=department_parent_id)
name = dep.department_name if dep else ErrFormat.department_id_not_found.format(department_parent_id) name = d.department_name if d else ErrFormat.department_id_not_found.format(department_parent_id)
except Exception as e: except Exception as e:
current_app.logger.error(str(e))
name = ErrFormat.department_id_not_found.format(department_parent_id) name = ErrFormat.department_id_not_found.format(department_parent_id)
abort(400, ErrFormat.cannot_to_be_parent_department.format(name)) abort(400, ErrFormat.cannot_to_be_parent_department.format(name))
@ -255,7 +240,7 @@ class DepartmentCRUD(object):
return abort(400, ErrFormat.acl_update_role_failed.format(str(e))) return abort(400, ErrFormat.acl_update_role_failed.format(str(e)))
try: try:
return existed.update(**kwargs) existed.update(**kwargs)
except Exception as e: except Exception as e:
return abort(400, str(e)) return abort(400, str(e))
@ -268,7 +253,7 @@ class DepartmentCRUD(object):
try: try:
RoleCRUD.delete_role(existed.acl_rid) RoleCRUD.delete_role(existed.acl_rid)
except Exception as e: except Exception as e:
current_app.logger.error(str(e)) pass
return existed.soft_delete() return existed.soft_delete()
@ -283,7 +268,7 @@ class DepartmentCRUD(object):
try: try:
tree.remove_subtree(department_id) tree.remove_subtree(department_id)
except Exception as e: except Exception as e:
current_app.logger.error(str(e)) pass
[allow_d_id_list.append({'department_id': int(n.identifier), 'department_name': n.tag}) for n in [allow_d_id_list.append({'department_id': int(n.identifier), 'department_name': n.tag}) for n in
tree.all_nodes()] tree.all_nodes()]
@ -322,7 +307,6 @@ class DepartmentCRUD(object):
tree_list = [] tree_list = []
for top_d in top_deps: for top_d in top_deps:
top_d['department_name'] = ErrFormat.company_wide
tree = Tree() tree = Tree()
identifier_root = top_d['department_id'] identifier_root = top_d['department_id']
tree.create_node( tree.create_node(
@ -393,9 +377,6 @@ class DepartmentCRUD(object):
d['employee_count'] = len(list(filter(lambda e: e['department_id'] in d_ids, all_employee_list))) d['employee_count'] = len(list(filter(lambda e: e['department_id'] in d_ids, all_employee_list)))
if int(department_parent_id) == -1:
d['department_name'] = ErrFormat.company_wide
return all_departments, department_id_list return all_departments, department_id_list
@staticmethod @staticmethod
@ -409,151 +390,6 @@ class DepartmentCRUD(object):
[id_list.append(int(n.identifier)) [id_list.append(int(n.identifier))
for n in tmp_tree.all_nodes()] for n in tmp_tree.all_nodes()]
except Exception as e: except Exception as e:
current_app.logger.error(str(e)) pass
return id_list return id_list
class EditDepartmentInACL(object):
@staticmethod
def add_department_to_acl(department_id, op_uid):
db_department = DepartmentCRUD.get_department_by_id(department_id, to_dict=False)
if not db_department:
return
from api.models.acl import Role
role = Role.get_by(first=True, name=db_department.department_name, app_id=None)
acl = ACLManager('acl', str(op_uid))
if role is None:
payload = {
'app_id': 'acl',
'name': db_department.department_name,
}
role = acl.create_role(payload)
acl_rid = role.get('id') if role else 0
db_department.update(
acl_rid=acl_rid
)
info = f"add_department_to_acl, acl_rid: {acl_rid}"
current_app.logger.info(info)
return info
@staticmethod
def delete_department_from_acl(department_rids, op_uid):
acl = ACLManager('acl', str(op_uid))
result = []
for rid in department_rids:
try:
acl.delete_role(rid)
except Exception as e:
result.append(f"delete_department_in_acl, rid: {rid}, error: {e}")
continue
return result
@staticmethod
def edit_department_name_in_acl(d_rid: int, d_name: str, op_uid: int):
acl = ACLManager('acl', str(op_uid))
payload = {
'name': d_name
}
try:
acl.edit_role(d_rid, payload)
except Exception as e:
return f"edit_department_name_in_acl, rid: {d_rid}, error: {e}"
return f"edit_department_name_in_acl, rid: {d_rid}, success"
@classmethod
def remove_from_old_department_role(cls, e_list, acl):
result = []
for employee in e_list:
employee_acl_rid = employee.get('e_acl_rid')
if employee_acl_rid == 0:
result.append("employee_acl_rid == 0")
continue
cls.remove_single_employee_from_old_department(acl, employee, result)
@staticmethod
def remove_single_employee_from_old_department(acl, employee, result):
from api.models.acl import Role
old_department = DepartmentCRUD.get_department_by_id(employee.get('department_id'), False)
if not old_department:
return False
old_role = Role.get_by(first=True, name=old_department.department_name, app_id=None)
old_d_rid_in_acl = old_role.get('id') if old_role else 0
if old_d_rid_in_acl == 0:
return False
d_acl_rid = old_department.acl_rid if old_d_rid_in_acl == old_department.acl_rid else old_d_rid_in_acl
payload = {
'app_id': 'acl',
'parent_id': d_acl_rid,
}
try:
acl.remove_user_from_role(employee.get('e_acl_rid'), payload)
current_app.logger.info(f"remove {employee.get('e_acl_rid')} from {d_acl_rid}")
except Exception as e:
err = f"remove_user_from_role e_acl_rid: {employee.get('e_acl_rid')}, parent_id: {d_acl_rid}, err: {e}"
result.append(err)
return True
@staticmethod
def add_employee_to_new_department(acl, employee_acl_rid, new_department_acl_rid, result):
payload = {
'app_id': 'acl',
'child_ids': [employee_acl_rid],
}
try:
acl.add_user_to_role(new_department_acl_rid, payload)
current_app.logger.info(f"add {employee_acl_rid} to {new_department_acl_rid}")
except Exception as e:
result.append(
f"add_user_to_role employee_acl_rid: {employee_acl_rid}, parent_id: {new_department_acl_rid}, \
err: {e}")
@classmethod
def edit_employee_department_in_acl(cls, e_list: list, new_d_id: int, op_uid: int):
result = []
new_department = DepartmentCRUD.get_department_by_id(new_d_id, False)
if not new_department:
result.append(f"{new_d_id} new_department is None")
return result
from api.models.acl import Role
new_role = Role.get_by(first=True, name=new_department.department_name, app_id=None)
new_d_rid_in_acl = new_role.get('id') if new_role else 0
acl = ACLManager('acl', str(op_uid))
if new_d_rid_in_acl == 0:
# only remove from old department role
cls.remove_from_old_department_role(e_list, acl)
return
if new_d_rid_in_acl != new_department.acl_rid:
new_department.update(
acl_rid=new_d_rid_in_acl
)
new_department_acl_rid = new_department.acl_rid if new_d_rid_in_acl == new_department.acl_rid else \
new_d_rid_in_acl
for employee in e_list:
employee_acl_rid = employee.get('e_acl_rid')
if employee_acl_rid == 0:
result.append("employee_acl_rid == 0")
continue
cls.remove_single_employee_from_old_department(acl, employee, result)
# 在新部门中添加员工
cls.add_employee_to_new_department(acl, employee_acl_rid, new_department_acl_rid, result)
return result

View File

@ -1,10 +1,9 @@
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
import copy
import traceback import traceback
from datetime import datetime from datetime import datetime
import requests from flask import abort
from flask import abort, current_app
from flask_login import current_user from flask_login import current_user
from sqlalchemy import or_, literal_column, func, not_, and_ from sqlalchemy import or_, literal_column, func, not_, and_
from werkzeug.datastructures import MultiDict from werkzeug.datastructures import MultiDict
@ -15,13 +14,10 @@ from wtforms import validators
from api.extensions import db from api.extensions import db
from api.lib.common_setting.acl import ACLManager from api.lib.common_setting.acl import ACLManager
from api.lib.common_setting.const import OperatorType from api.lib.common_setting.const import COMMON_SETTING_QUEUE, OperatorType
from api.lib.perm.acl.const import ACL_QUEUE
from api.lib.common_setting.resp_format import ErrFormat from api.lib.common_setting.resp_format import ErrFormat
from api.models.common_setting import Employee, Department from api.models.common_setting import Employee, Department
from api.tasks.common_setting import refresh_employee_acl_info, edit_employee_department_in_acl
acl_user_columns = [ acl_user_columns = [
'email', 'email',
'mobile', 'mobile',
@ -124,25 +120,10 @@ class EmployeeCRUD(object):
employee = CreateEmployee().create_single(**data) employee = CreateEmployee().create_single(**data)
return employee.to_dict() return employee.to_dict()
@staticmethod
def add_employee_from_acl_created(**kwargs):
try:
kwargs['acl_uid'] = kwargs.pop('uid')
kwargs['acl_rid'] = kwargs.pop('rid')
kwargs['department_id'] = 0
Employee.create(
**kwargs
)
except Exception as e:
abort(400, str(e))
@staticmethod @staticmethod
def add(**kwargs): def add(**kwargs):
try: try:
res = CreateEmployee().create_single(**kwargs) return CreateEmployee().create_single(**kwargs)
refresh_employee_acl_info.apply_async(args=(res.employee_id,), queue=ACL_QUEUE)
return res
except Exception as e: except Exception as e:
abort(400, str(e)) abort(400, str(e))
@ -169,9 +150,10 @@ class EmployeeCRUD(object):
existed.update(**kwargs) existed.update(**kwargs)
if len(e_list) > 0: if len(e_list) > 0:
from api.tasks.common_setting import edit_employee_department_in_acl
edit_employee_department_in_acl.apply_async( edit_employee_department_in_acl.apply_async(
args=(e_list, new_department_id, current_user.uid), args=(e_list, new_department_id, current_user.uid),
queue=ACL_QUEUE queue=COMMON_SETTING_QUEUE
) )
return existed return existed
@ -182,7 +164,7 @@ class EmployeeCRUD(object):
def edit_employee_by_uid(_uid, **kwargs): def edit_employee_by_uid(_uid, **kwargs):
existed = EmployeeCRUD.get_employee_by_uid(_uid) existed = EmployeeCRUD.get_employee_by_uid(_uid)
try: try:
edit_acl_user(_uid, **kwargs) user = edit_acl_user(_uid, **kwargs)
for column in employee_pop_columns: for column in employee_pop_columns:
if kwargs.get(column): if kwargs.get(column):
@ -194,9 +176,9 @@ class EmployeeCRUD(object):
@staticmethod @staticmethod
def change_password_by_uid(_uid, password): def change_password_by_uid(_uid, password):
EmployeeCRUD.get_employee_by_uid(_uid) existed = EmployeeCRUD.get_employee_by_uid(_uid)
try: try:
edit_acl_user(_uid, password=password) user = edit_acl_user(_uid, password=password)
except Exception as e: except Exception as e:
return abort(400, str(e)) return abort(400, str(e))
@ -295,9 +277,7 @@ class EmployeeCRUD(object):
employees = [] employees = []
for r in pagination.items: for r in pagination.items:
d = r.Employee.to_dict() d = r.Employee.to_dict()
d['department_name'] = r.Department.department_name if r.Department else '' d['department_name'] = r.Department.department_name
if r.Employee.department_id == 0:
d['department_name'] = ErrFormat.company_wide
employees.append(d) employees.append(d)
return { return {
@ -365,11 +345,9 @@ class EmployeeCRUD(object):
if value and column == "last_login": if value and column == "last_login":
try: try:
return datetime.strptime(value, "%Y-%m-%d %H:%M:%S") value = datetime.strptime(value, "%Y-%m-%d %H:%M:%S")
except Exception as e: except Exception as e:
err = f"{ErrFormat.datetime_format_error.format(column)}: {str(e)}" abort(400, ErrFormat.datetime_format_error.format(column))
abort(400, err)
return value
@staticmethod @staticmethod
def get_attr_by_column(column): def get_attr_by_column(column):
@ -390,7 +368,7 @@ class EmployeeCRUD(object):
relation = condition.get("relation", None) relation = condition.get("relation", None)
value = condition.get("value", None) value = condition.get("value", None)
value = EmployeeCRUD.check_condition(column, operator, value, relation) EmployeeCRUD.check_condition(column, operator, value, relation)
a, o = EmployeeCRUD.get_expr_by_condition( a, o = EmployeeCRUD.get_expr_by_condition(
column, operator, value, relation) column, operator, value, relation)
and_list += a and_list += a
@ -443,7 +421,7 @@ class EmployeeCRUD(object):
employees = [] employees = []
for r in pagination.items: for r in pagination.items:
d = r.Employee.to_dict() d = r.Employee.to_dict()
d['department_name'] = r.Department.department_name if r.Department else '' d['department_name'] = r.Department.department_name
employees.append(d) employees.append(d)
return { return {
@ -478,7 +456,7 @@ class EmployeeCRUD(object):
Employee.deleted == 0, Employee.deleted == 0,
Employee.block == block, Employee.block == block,
] ]
if isinstance(department_id, list): if type(department_id) == list:
if len(department_id) == 0: if len(department_id) == 0:
return [] return []
else: else:
@ -496,226 +474,6 @@ class EmployeeCRUD(object):
return [r.to_dict() for r in results] return [r.to_dict() for r in results]
@staticmethod
def remove_bind_notice_by_uid(_platform, _uid):
existed = EmployeeCRUD.get_employee_by_uid(_uid)
employee_data = existed.to_dict()
notice_info = employee_data.get('notice_info', {})
notice_info = copy.deepcopy(notice_info) if notice_info else {}
notice_info[_platform] = ''
existed.update(
notice_info=notice_info
)
return ErrFormat.notice_remove_bind_success
@staticmethod
def bind_notice_by_uid(_platform, _uid):
existed = EmployeeCRUD.get_employee_by_uid(_uid)
mobile = existed.mobile
if not mobile or len(mobile) == 0:
abort(400, ErrFormat.notice_bind_err_with_empty_mobile)
from api.lib.common_setting.notice_config import NoticeConfigCRUD
messenger = NoticeConfigCRUD.get_messenger_url()
if not messenger or len(messenger) == 0:
abort(400, ErrFormat.notice_please_config_messenger_first)
url = f"{messenger}/v1/uid/getbyphone"
try:
payload = dict(
phone=mobile,
sender=_platform
)
res = requests.post(url, json=payload)
result = res.json()
if res.status_code != 200:
raise Exception(result.get('msg', ''))
target_id = result.get('uid', '')
employee_data = existed.to_dict()
notice_info = employee_data.get('notice_info', {})
notice_info = copy.deepcopy(notice_info) if notice_info else {}
notice_info[_platform] = '' if not target_id else target_id
existed.update(
notice_info=notice_info
)
return ErrFormat.notice_bind_success
except Exception as e:
return abort(400, ErrFormat.notice_bind_failed.format(str(e)))
@staticmethod
def get_employee_notice_by_ids(employee_ids):
criterion = [
Employee.employee_id.in_(employee_ids),
Employee.deleted == 0,
]
direct_columns = ['email', 'mobile']
employees = Employee.query.filter(
*criterion
).all()
results = []
for employee in employees:
d = employee.to_dict()
tmp = dict(
employee_id=employee.employee_id,
)
for column in direct_columns:
tmp[column] = d.get(column, '')
notice_info = d.get('notice_info', {})
notice_info = copy.deepcopy(notice_info) if notice_info else {}
tmp.update(**notice_info)
results.append(tmp)
return results
@staticmethod
def import_employee(employee_list):
res = CreateEmployee().batch_create(employee_list)
return res
@staticmethod
def batch_edit_employee_department(employee_id_list, column_value):
err_list = []
employee_list = []
for _id in employee_id_list:
try:
existed = EmployeeCRUD.get_employee_by_id(_id)
employee = dict(
e_acl_rid=existed.acl_rid,
department_id=existed.department_id
)
employee_list.append(employee)
existed.update(department_id=column_value)
except Exception as e:
err_list.append({
'employee_id': _id,
'err': str(e),
})
from api.lib.common_setting.department import EditDepartmentInACL
EditDepartmentInACL.edit_employee_department_in_acl(
employee_list, column_value, current_user.uid
)
return err_list
@staticmethod
def batch_edit_password_or_block_column(column_name, employee_id_list, column_value, is_acl=False):
if column_name == 'block':
err_list = []
success_list = []
for _id in employee_id_list:
try:
employee = EmployeeCRUD.edit_employee_block_column(
_id, is_acl, **{column_name: column_value})
success_list.append(employee)
except Exception as e:
err_list.append({
'employee_id': _id,
'err': str(e),
})
return err_list
else:
return EmployeeCRUD.batch_edit_column(column_name, employee_id_list, column_value, is_acl)
@staticmethod
def batch_edit_column(column_name, employee_id_list, column_value, is_acl=False):
err_list = []
for _id in employee_id_list:
try:
EmployeeCRUD.edit_employee_single_column(
_id, is_acl, **{column_name: column_value})
except Exception as e:
err_list.append({
'employee_id': _id,
'err': str(e),
})
return err_list
@staticmethod
def edit_employee_single_column(_id, is_acl=False, **kwargs):
existed = EmployeeCRUD.get_employee_by_id(_id)
if 'direct_supervisor_id' in kwargs.keys():
if kwargs['direct_supervisor_id'] == existed.direct_supervisor_id:
raise Exception(ErrFormat.direct_supervisor_is_not_self)
if is_acl:
return edit_acl_user(existed.acl_uid, **kwargs)
try:
for column in employee_pop_columns:
if kwargs.get(column):
kwargs.pop(column)
return existed.update(**kwargs)
except Exception as e:
return abort(400, str(e))
@staticmethod
def edit_employee_block_column(_id, is_acl=False, **kwargs):
existed = EmployeeCRUD.get_employee_by_id(_id)
value = get_block_value(kwargs.get('block'))
if value is True:
check_department_director_id_or_direct_supervisor_id(_id)
value = 1
else:
value = 0
if is_acl:
kwargs['block'] = value
edit_acl_user(existed.acl_uid, **kwargs)
existed.update(block=value)
data = existed.to_dict()
return data
@staticmethod
def batch_employee(column_name, column_value, employee_id_list):
if column_value is None:
abort(400, ErrFormat.value_is_required)
if column_name in ['password', 'block']:
return EmployeeCRUD.batch_edit_password_or_block_column(column_name, employee_id_list, column_value, True)
elif column_name in ['department_id']:
return EmployeeCRUD.batch_edit_employee_department(employee_id_list, column_value)
elif column_name in [
'direct_supervisor_id', 'position_name'
]:
return EmployeeCRUD.batch_edit_column(column_name, employee_id_list, column_value, False)
else:
abort(400, ErrFormat.column_name_not_support)
@staticmethod
def update_last_login_by_uid(uid, last_login=None):
employee = Employee.get_by(acl_uid=uid, first=True, to_dict=False)
if not employee:
return
if last_login:
try:
last_login = datetime.strptime(last_login, '%Y-%m-%d %H:%M:%S')
except Exception as e:
current_app.logger.error(f"strptime {last_login} err: {e}")
last_login = datetime.now()
else:
last_login = datetime.now()
try:
employee.update(
last_login=last_login
)
return last_login
except Exception as e:
current_app.logger.error(f"update last_login err: {e}")
return
def get_user_map(key='uid', acl=None): def get_user_map(key='uid', acl=None):
""" """
@ -756,7 +514,6 @@ class CreateEmployee(object):
try: try:
existed = self.check_acl_user(user_data) existed = self.check_acl_user(user_data)
if not existed: if not existed:
user_data['add_from'] = 'common'
return self.acl.create_user(user_data) return self.acl.create_user(user_data)
return existed return existed
except Exception as e: except Exception as e:
@ -789,14 +546,11 @@ class CreateEmployee(object):
if existed: if existed:
return existed return existed
res = Employee.create( return Employee.create(
**kwargs **kwargs
) )
refresh_employee_acl_info.apply_async(args=(res.employee_id,), queue=ACL_QUEUE)
return res
@staticmethod def get_department_by_name(self, d_name):
def get_department_by_name(d_name):
return Department.get_by(first=True, department_name=d_name) return Department.get_by(first=True, department_name=d_name)
def get_end_department_id(self, department_name_list, department_name_map): def get_end_department_id(self, department_name_list, department_name_map):
@ -900,75 +654,3 @@ class EmployeeUpdateByUidForm(Form):
avatar = StringField(validators=[]) avatar = StringField(validators=[])
sex = StringField(validators=[]) sex = StringField(validators=[])
mobile = StringField(validators=[]) mobile = StringField(validators=[])
class GrantEmployeeACLPerm(object):
"""
Grant ACL Permission After Create New Employee
"""
def __init__(self, acl=None):
self.perms_by_create_resources_type = ['read', 'grant', 'delete', 'update']
self.perms_by_common_grant = ['read']
self.resource_name_list = ['公司信息', '公司架构', '通知设置']
self.acl = acl if acl else self.check_app('backend')
self.resources_types = self.acl.get_all_resources_types()
self.resources_type = self.get_resources_type()
self.resource_list = self.acl.get_resource_by_type(None, None, self.resources_type['id'])
@staticmethod
def check_app(app_name):
acl = ACLManager(app_name)
payload = dict(
name=app_name,
description=app_name
)
app = acl.validate_app()
if not app:
acl.create_app(payload)
return acl
def get_resources_type(self):
results = list(filter(lambda t: t['name'] == '操作权限', self.resources_types['groups']))
if len(results) == 0:
payload = dict(
app_id=self.acl.app_name,
name='操作权限',
description='',
perms=self.perms_by_create_resources_type
)
resource_type = self.acl.create_resources_type(payload)
else:
resource_type = results[0]
resource_type_id = resource_type['id']
existed_perms = self.resources_types.get('id2perms', {}).get(resource_type_id, [])
existed_perms = [p['name'] for p in existed_perms]
new_perms = []
for perm in self.perms_by_create_resources_type:
if perm not in existed_perms:
new_perms.append(perm)
if len(new_perms) > 0:
resource_type['perms'] = existed_perms + new_perms
self.acl.update_resources_type(resource_type_id, resource_type)
return resource_type
def grant(self, rid_list):
[self.grant_by_rid(rid) for rid in rid_list if rid > 0]
def grant_by_rid(self, rid, is_admin=False):
for name in self.resource_name_list:
resource = list(filter(lambda r: r['name'] == name, self.resource_list))
if len(resource) == 0:
payload = dict(
type_id=self.resources_type['id'],
app_id=self.acl.app_name,
name=name,
)
resource = self.acl.create_resource(payload)
else:
resource = resource[0]
perms = self.perms_by_create_resources_type if is_admin else self.perms_by_common_grant
self.acl.grant_resource(rid, resource['id'], perms)

View File

@ -1,165 +0,0 @@
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 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

@ -1,84 +1,57 @@
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
from flask_babel import lazy_gettext as _l
from api.lib.resp_format import CommonErrFormat from api.lib.resp_format import CommonErrFormat
class ErrFormat(CommonErrFormat): class ErrFormat(CommonErrFormat):
company_info_is_already_existed = _l("Company info already existed") # 公司信息已存在!无法创建 company_info_is_already_existed = "公司信息已存在!无法创建"
no_file_part = _l("No file part") # 没有文件部分 no_file_part = "没有文件部分"
file_is_required = _l("File is required") # 文件是必须的 file_is_required = "文件是必须的"
file_not_found = _l("File not found") # 文件不存在
file_type_not_allowed = _l("File type not allowed") # 文件类型不允许
upload_failed = _l("Upload failed: {}") # 上传失败: {}
direct_supervisor_is_not_self = _l("Direct supervisor is not self") # 直属上级不能是自己 direct_supervisor_is_not_self = "直属上级不能是自己"
parent_department_is_not_self = _l("Parent department is not self") # 上级部门不能是自己 parent_department_is_not_self = "上级部门不能是自己"
employee_list_is_empty = _l("Employee list is empty") # 员工列表为空 employee_list_is_empty = "员工列表为空"
column_name_not_support = _l("Column name not support") # 不支持的列名 column_name_not_support = "不支持的列名"
password_is_required = _l("Password is required") # 密码是必须的 password_is_required = "密码不能为空"
employee_acl_rid_is_zero = _l("Employee acl rid is zero") # 员工ACL角色ID不能为0 employee_acl_rid_is_zero = "员工ACL角色ID不能为0"
generate_excel_failed = _l("Generate excel failed: {}") # 生成excel失败: {} generate_excel_failed = "生成excel失败: {}"
rename_columns_failed = _l("Rename columns failed: {}") # 重命名字段失败: {} rename_columns_failed = "字段转换为中文失败: {}"
cannot_block_this_employee_is_other_direct_supervisor = _l( cannot_block_this_employee_is_other_direct_supervisor = "该员工是其他员工的直属上级, 不能禁用"
"Cannot block this employee is other direct supervisor") # 该员工是其他员工的直属上级, 不能禁用 cannot_block_this_employee_is_department_manager = "该员工是部门负责人, 不能禁用"
cannot_block_this_employee_is_department_manager = _l( employee_id_not_found = "员工ID [{}] 不存在"
"Cannot block this employee is department manager") # 该员工是部门负责人, 不能禁用 value_is_required = "值是必须的"
employee_id_not_found = _l("Employee id [{}] not found") # 员工ID [{}] 不存在 email_already_exists = "邮箱 [{}] 已存在"
value_is_required = _l("Value is required") # 值是必须的 query_column_none_keep_value_empty = "查询 {} 空值时请保持value为空"
email_already_exists = _l("Email already exists") # 邮箱已存在 not_support_operator = "不支持的操作符: {}"
query_column_none_keep_value_empty = _l("Query {} none keep value empty") # 查询 {} 空值时请保持value为空" not_support_relation = "不支持的关系: {}"
not_support_operator = _l("Not support operator: {}") # 不支持的操作符: {} conditions_field_missing = "conditions内元素字段缺失请检查"
not_support_relation = _l("Not support relation: {}") # 不支持的关系: {} datetime_format_error = "{} 格式错误,应该为:%Y-%m-%d %H:%M:%S"
conditions_field_missing = _l("Conditions field missing") # conditions内元素字段缺失请检查 department_level_relation_error = "部门层级关系不正确"
datetime_format_error = _l("Datetime format error: {}") # {} 格式错误,应该为:%Y-%m-%d %H:%M:%S delete_reserved_department_name = "保留部门,无法删除!"
department_level_relation_error = _l("Department level relation error") # 部门层级关系不正确 department_id_is_required = "部门ID是必须的"
delete_reserved_department_name = _l("Delete reserved department name") # 保留部门,无法删除! department_list_is_required = "部门列表是必须的"
department_id_is_required = _l("Department id is required") # 部门ID是必须的 cannot_to_be_parent_department = "{} 不能设置为上级部门"
department_list_is_required = _l("Department list is required") # 部门列表是必须的 department_id_not_found = "部门ID [{}] 不存在"
cannot_to_be_parent_department = _l("{} Cannot to be parent department") # 不能设置为上级部门 parent_department_id_must_more_than_zero = "上级部门ID必须大于0"
department_id_not_found = _l("Department id [{}] not found") # 部门ID [{}] 不存在 department_name_already_exists = "部门名称 [{}] 已存在"
parent_department_id_must_more_than_zero = _l("Parent department id must more than zero") # 上级部门ID必须大于0 new_department_is_none = "新部门是空的"
department_name_already_exists = _l("Department name [{}] already exists") # 部门名称 [{}] 已存在
new_department_is_none = _l("New department is none") # 新部门是空的
acl_edit_user_failed = _l("ACL edit user failed: {}") # ACL 修改用户失败: {} acl_edit_user_failed = "ACL 修改用户失败: {}"
acl_uid_not_found = _l("ACL uid not found: {}") # ACL 用户UID [{}] 不存在 acl_uid_not_found = "ACL 用户UID [{}] 不存在"
acl_add_user_failed = _l("ACL add user failed: {}") # ACL 添加用户失败: {} acl_add_user_failed = "ACL 添加用户失败: {}"
acl_add_role_failed = _l("ACL add role failed: {}") # ACL 添加角色失败: {} acl_add_role_failed = "ACL 添加角色失败: {}"
acl_update_role_failed = _l("ACL update role failed: {}") # ACL 更新角色失败: {} acl_update_role_failed = "ACL 更新角色失败: {}"
acl_get_all_users_failed = _l("ACL get all users failed: {}") # ACL 获取所有用户失败: {} acl_get_all_users_failed = "ACL 获取所有用户失败: {}"
acl_remove_user_from_role_failed = _l("ACL remove user from role failed: {}") # ACL 从角色中移除用户失败: {} acl_remove_user_from_role_failed = "ACL 从角色中移除用户失败: {}"
acl_add_user_to_role_failed = _l("ACL add user to role failed: {}") # ACL 添加用户到角色失败: {} acl_add_user_to_role_failed = "ACL 添加用户到角色失败: {}"
acl_import_user_failed = _l("ACL import user failed: {}") # ACL 导入用户失败: {} acl_import_user_failed = "ACL 导入用户[{}]失败: {}"
nickname_is_required = _l("Nickname is required") # 昵称不能为空 nickname_is_required = "用户名不能为空"
username_is_required = _l("Username is required") # 用户名不能为空 username_is_required = "username不能为空"
email_is_required = _l("Email is required") # 邮箱不能为空 email_is_required = "邮箱不能为空"
email_format_error = _l("Email format error") # 邮箱格式错误 email_format_error = "邮箱格式错误"
email_send_timeout = _l("Email send timeout") # 邮件发送超时
common_data_not_found = _l("Common data not found {} ") # ID {} 找不到记录 common_data_not_found = "ID {} 找不到记录"
common_data_already_existed = _l("Common data {} already existed") # {} 已存在
notice_platform_existed = _l("Notice platform {} existed") # {} 已存在
notice_not_existed = _l("Notice {} not existed") # {} 配置项不存在
notice_please_config_messenger_first = _l("Notice please config messenger first") # 请先配置messenger URL
notice_bind_err_with_empty_mobile = _l("Notice bind err with empty mobile") # 绑定错误,手机号为空
notice_bind_failed = _l("Notice bind failed: {}") # 绑定失败: {}
notice_bind_success = _l("Notice bind success") # 绑定成功
notice_remove_bind_success = _l("Notice remove bind success") # 解绑成功
not_support_test = _l("Not support test type: {}") # 不支持的测试类型: {}
not_support_auth_type = _l("Not support auth type: {}") # 不支持的认证类型: {}
ldap_server_connect_timeout = _l("LDAP server connect timeout") # LDAP服务器连接超时
ldap_server_connect_not_available = _l("LDAP server connect not available") # LDAP服务器连接不可用
ldap_test_unknown_error = _l("LDAP test unknown error: {}") # LDAP测试未知错误: {}
common_data_not_support_auth_type = _l("Common data not support auth type: {}") # 通用数据不支持auth类型: {}
ldap_test_username_required = _l("LDAP test username required") # LDAP测试用户名必填
company_wide = _l("Company wide") # 全公司
resource_no_permission = _l("No permission to access resource {}, perm {} ") # 没有权限访问 {} 资源的 {} 权限"

View File

@ -1,68 +0,0 @@
class OperationPermission(object):
def __init__(self, resource_perms):
for _r in resource_perms:
setattr(self, _r['page'], _r['page'])
for _p in _r['perms']:
setattr(self, _p, _p)
class BaseApp(object):
resource_type_name = 'OperationPermission'
all_resource_perms = []
def __init__(self):
self.admin_name = None
self.roles = []
self.app_name = 'acl'
self.require_create_resource_type = self.resource_type_name
self.extra_create_resource_type_list = []
self.op = None
@staticmethod
def format_role(role_name, role_type, acl_rid, resource_perms, description=''):
return dict(
role_name=role_name,
role_type=role_type,
acl_rid=acl_rid,
description=description,
resource_perms=resource_perms,
)
class CMDBApp(BaseApp):
all_resource_perms = [
{"page": "Big_Screen", "page_cn": "大屏", "perms": ["read"]},
{"page": "Dashboard", "page_cn": "仪表盘", "perms": ["read"]},
{"page": "Resource_Search", "page_cn": "资源搜索", "perms": ["read"]},
{"page": "Auto_Discovery_Pool", "page_cn": "自动发现池", "perms": ["read"]},
{"page": "My_Subscriptions", "page_cn": "我的订阅", "perms": ["read"]},
{"page": "Bulk_Import", "page_cn": "批量导入", "perms": ["read"]},
{"page": "Model_Configuration", "page_cn": "模型配置",
"perms": ["read", "create_CIType", "create_CIType_group", "update_CIType_group",
"delete_CIType_group", "download_CIType"]},
{"page": "Backend_Management", "page_cn": "后台管理", "perms": ["read"]},
{"page": "Customized_Dashboard", "page_cn": "定制仪表盘", "perms": ["read"]},
{"page": "Service_Tree_Definition", "page_cn": "服务树定义", "perms": ["read"]},
{"page": "Model_Relationships", "page_cn": "模型关系", "perms": ["read"]},
{"page": "Operation_Audit", "page_cn": "操作审计", "perms": ["read"]},
{"page": "Relationship_Types", "page_cn": "关系类型", "perms": ["read"]},
{"page": "Auto_Discovery", "page_cn": "自动发现",
"perms": ["read", "create_plugin", "update_plugin", "delete_plugin"]
},
{"page": "TopologyView", "page_cn": "拓扑视图",
"perms": ["read", "create_topology_group", "update_topology_group", "delete_topology_group",
"create_topology_view"],
},
{"page": "IPAM", "page_cn": "IPAM", "perms": ["read"]},
{"page": "DCIM", "page_cn": "数据中心", "perms": ["read"]},
]
def __init__(self):
super().__init__()
self.admin_name = 'cmdb_admin'
self.app_name = 'cmdb'
self.op = OperationPermission(self.all_resource_perms)

View File

@ -1,14 +1,6 @@
import base64
import uuid import uuid
import os
from io import BytesIO
from flask import abort, current_app
import lz4.frame
from api.lib.common_setting.utils import get_cur_time_str from api.lib.common_setting.utils import get_cur_time_str
from api.models.common_setting import CommonFile
from api.lib.common_setting.resp_format import ErrFormat
def allowed_file(filename, allowed_extensions): def allowed_file(filename, allowed_extensions):
@ -22,73 +14,3 @@ def generate_new_file_name(name):
cur_str = get_cur_time_str('_') cur_str = get_cur_time_str('_')
return f"{prev_name}_{cur_str}_{uid}.{ext}" return f"{prev_name}_{cur_str}_{uid}.{ext}"
class CommonFileCRUD:
@staticmethod
def add_file(**kwargs):
return CommonFile.create(**kwargs)
@staticmethod
def get_file(file_name, to_str=False):
existed = CommonFile.get_by(file_name=file_name, first=True, to_dict=False)
if not existed:
abort(400, ErrFormat.file_not_found)
uncompressed_data = lz4.frame.decompress(existed.binary)
return base64.b64encode(uncompressed_data).decode('utf-8') if to_str else BytesIO(uncompressed_data)
@staticmethod
def sync_file_to_db():
for p in ['UPLOAD_DIRECTORY_FULL']:
upload_path = current_app.config.get(p, None)
if not upload_path:
continue
for root, dirs, files in os.walk(upload_path):
for file in files:
file_path = os.path.join(root, file)
if not os.path.isfile(file_path):
continue
existed = CommonFile.get_by(file_name=file, first=True, to_dict=False)
if existed:
continue
with open(file_path, 'rb') as f:
data = f.read()
compressed_data = lz4.frame.compress(data)
try:
CommonFileCRUD.add_file(
origin_name=file,
file_name=file,
binary=compressed_data
)
current_app.logger.info(f'sync file {file} to db')
except Exception as e:
current_app.logger.error(f'sync file {file} to db error: {e}')
def get_file_binary_str(self, file_name):
return self.get_file(file_name, True)
def save_str_to_file(self, file_name, str_data):
try:
self.get_file(file_name)
current_app.logger.info(f'file {file_name} already exists')
return
except Exception as e:
# file not found
pass
bytes_data = base64.b64decode(str_data)
compressed_data = lz4.frame.compress(bytes_data)
try:
self.add_file(
origin_name=file_name,
file_name=file_name,
binary=compressed_data
)
current_app.logger.info(f'save_str_to_file {file_name} success')
except Exception as e:
current_app.logger.error(f"save_str_to_file error: {e}")

View File

@ -1,10 +1,5 @@
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
from datetime import datetime from datetime import datetime
from flask import current_app
from sqlalchemy import inspect, text
from sqlalchemy.dialects.mysql import ENUM
from api.extensions import db
def get_cur_time_str(split_flag='-'): def get_cur_time_str(split_flag='-'):
@ -28,115 +23,3 @@ class BaseEnum(object):
if not attr.startswith("_") and not callable(getattr(cls, attr)) if not attr.startswith("_") and not callable(getattr(cls, attr))
} }
return cls._ALL_ return cls._ALL_
class CheckNewColumn(object):
def __init__(self):
self.engine = db.get_engine()
self.inspector = inspect(self.engine)
self.table_names = self.inspector.get_table_names()
@staticmethod
def get_model_by_table_name(_table_name):
registry = getattr(db.Model, 'registry', None)
class_registry = getattr(registry, '_class_registry', None)
for _model in class_registry.values():
if hasattr(_model, '__tablename__') and _model.__tablename__ == _table_name:
return _model
return None
def run(self):
for table_name in self.table_names:
self.check_by_table(table_name)
def check_by_table(self, table_name):
existed_columns = self.inspector.get_columns(table_name)
enum_columns = []
existed_column_name_list = []
for c in existed_columns:
if isinstance(c['type'], ENUM):
enum_columns.append(c['name'])
existed_column_name_list.append(c['name'])
model = self.get_model_by_table_name(table_name)
if model is None:
return
model_columns = getattr(getattr(getattr(model, '__table__'), 'columns'), '_all_columns')
for column in model_columns:
if column.name not in existed_column_name_list:
add_res = self.add_new_column(table_name, column)
if not add_res:
continue
current_app.logger.info(f"add new column [{column.name}] in table [{table_name}] success.")
if column.name in enum_columns:
enum_columns.remove(column.name)
self.add_new_index(table_name, column)
if len(enum_columns) > 0:
self.check_enum_column(enum_columns, existed_columns, model_columns, table_name)
def add_new_column(self, target_table_name, new_column):
try:
column_type = new_column.type.compile(self.engine.dialect)
default_value = new_column.default.arg if new_column.default else None
sql = "ALTER TABLE " + target_table_name + " ADD COLUMN " + f"`{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)
return True
except Exception as e:
err = f"add_new_column [{new_column.name}] to table [{target_table_name}] err: {e}"
current_app.logger.error(err)
return False
@staticmethod
def add_new_index(target_table_name, new_column):
try:
if new_column.index:
index_name = f"{target_table_name}_{new_column.name}"
sql = "CREATE INDEX " + f"{index_name}" + " ON " + target_table_name + " (" + new_column.name + ")"
db.session.execute(sql)
current_app.logger.info(f"add new index [{index_name}] in table [{target_table_name}] success.")
return True
except Exception as e:
err = f"add_new_index [{new_column.name}] to table [{target_table_name}] err: {e}"
current_app.logger.error(err)
return False
@staticmethod
def check_enum_column(enum_columns, existed_columns, model_columns, table_name):
for column_name in enum_columns:
try:
enum_column = list(filter(lambda x: x['name'] == column_name, existed_columns))[0]
old_enum_value = enum_column.get('type', {}).enums
target_column = list(filter(lambda x: x.name == column_name, model_columns))[0]
new_enum_value = target_column.type.enums
if set(old_enum_value) == set(new_enum_value):
continue
enum_values_str = ','.join(["'{}'".format(value) for value in new_enum_value])
sql = f"ALTER TABLE {table_name} MODIFY COLUMN" + f"`{column_name}`" + f" enum({enum_values_str})"
db.session.execute(sql)
current_app.logger.info(
f"modify column [{column_name}] ENUM: {new_enum_value} in table [{table_name}] success.")
except Exception as e:
current_app.logger.error(
f"modify column ENUM [{column_name}] in table [{table_name}] err: {e}")

View File

@ -10,18 +10,14 @@ from api.lib.exception import CommitException
class FormatMixin(object): class FormatMixin(object):
def to_dict(self): def to_dict(self):
res = dict() res = dict([(k, getattr(self, k) if not isinstance(
for k in getattr(self, "__mapper__").c.keys(): getattr(self, k), (datetime.datetime, datetime.date, datetime.time)) else str(
if k in {'password', '_password', 'secret', '_secret'}: getattr(self, k))) for k in getattr(self, "__mapper__").c.keys()])
continue # FIXME: getattr(cls, "__table__").columns k.name
if k.startswith('_'): res.pop('password', None)
k = k[1:] res.pop('_password', None)
res.pop('secret', None)
if not isinstance(getattr(self, k), (datetime.datetime, datetime.date, datetime.time)):
res[k] = getattr(self, k)
else:
res[k] = str(getattr(self, k))
return res return res
@ -94,7 +90,7 @@ class CRUDMixin(FormatMixin):
if any((isinstance(_id, six.string_types) and _id.isdigit(), if any((isinstance(_id, six.string_types) and _id.isdigit(),
isinstance(_id, (six.integer_types, float))), ): isinstance(_id, (six.integer_types, float))), ):
obj = getattr(cls, "query").get(int(_id)) obj = getattr(cls, "query").get(int(_id))
if obj and not getattr(obj, 'deleted', False): if obj and not obj.deleted:
return obj return obj
@classmethod @classmethod

View File

@ -4,14 +4,8 @@
from functools import wraps from functools import wraps
from flask import abort from flask import abort
from flask import current_app
from flask import request from flask import request
from sqlalchemy.exc import InvalidRequestError
from sqlalchemy.exc import OperationalError
from sqlalchemy.exc import PendingRollbackError
from sqlalchemy.exc import StatementError
from api.extensions import db
from api.lib.resp_format import CommonErrFormat from api.lib.resp_format import CommonErrFormat
@ -76,43 +70,3 @@ def args_validate(model_cls, exclude_args=None):
return wrapper return wrapper
return decorate return decorate
def reconnect_db(func):
@wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except (StatementError, OperationalError, InvalidRequestError) as e:
error_msg = str(e)
if 'Lost connection' in error_msg or 'reconnect until invalid transaction' in error_msg or \
'can be emitted within this transaction' in error_msg:
current_app.logger.info('[reconnect_db] lost connect rollback then retry')
db.session.rollback()
return func(*args, **kwargs)
else:
raise e
except Exception as e:
raise e
return wrapper
def _flush_db():
try:
db.session.commit()
except (StatementError, OperationalError, InvalidRequestError, PendingRollbackError):
db.session.rollback()
def flush_db(func):
@wraps(func)
def wrapper(*args, **kwargs):
_flush_db()
return func(*args, **kwargs)
return wrapper
def run_flush_db():
_flush_db()

View File

@ -1,7 +1,7 @@
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
from flask import current_app from flask import abort
from sqlalchemy import func from sqlalchemy import func
from api.extensions import db from api.extensions import db
@ -13,41 +13,25 @@ class DBMixin(object):
cls = None cls = None
@classmethod @classmethod
def search(cls, page, page_size, fl=None, only_query=False, reverse=False, count_query=False, def search(cls, page, page_size, fl=None, only_query=False, reverse=False, count_query=False, **kwargs):
last_size=None, **kwargs):
page = get_page(page) page = get_page(page)
page_size = get_page_size(page_size) page_size = get_page_size(page_size)
if fl is None: if fl is None:
query = db.session.query(cls.cls) query = db.session.query(cls.cls).filter(cls.cls.deleted.is_(False))
else: else:
query = db.session.query(*[getattr(cls.cls, i) for i in fl]) query = db.session.query(*[getattr(cls.cls, i) for i in fl]).filter(cls.cls.deleted.is_(False))
_query = None _query = None
if count_query: if count_query:
_query = db.session.query(func.count(cls.cls.id)) _query = db.session.query(func.count(cls.cls.id)).filter(cls.cls.deleted.is_(False))
if hasattr(cls.cls, 'deleted'):
query = query.filter(cls.cls.deleted.is_(False))
if _query:
_query = _query.filter(cls.cls.deleted.is_(False))
for k in kwargs: for k in kwargs:
if hasattr(cls.cls, k): if hasattr(cls.cls, k):
if isinstance(kwargs[k], list): query = query.filter(getattr(cls.cls, k) == kwargs[k])
query = query.filter(getattr(cls.cls, k).in_(kwargs[k])) if count_query:
if count_query: _query = _query.filter(getattr(cls.cls, k) == kwargs[k])
_query = _query.filter(getattr(cls.cls, k).in_(kwargs[k]))
else:
if "*" in str(kwargs[k]):
query = query.filter(getattr(cls.cls, k).ilike(kwargs[k].replace('*', '%')))
if count_query:
_query = _query.filter(getattr(cls.cls, k).ilike(kwargs[k].replace('*', '%')))
else:
query = query.filter(getattr(cls.cls, k) == kwargs[k])
if count_query:
_query = _query.filter(getattr(cls.cls, k) == kwargs[k])
if reverse in current_app.config.get('BOOL_TRUE'): if reverse:
query = query.order_by(cls.cls.id.desc()) query = query.order_by(cls.cls.id.desc())
if only_query and not count_query: if only_query and not count_query:
@ -56,15 +40,14 @@ class DBMixin(object):
return _query, query return _query, query
numfound = query.count() numfound = query.count()
if not last_size: return numfound, [i.to_dict() if fl is None else getattr(i, '_asdict')()
return numfound, [i.to_dict() if fl is None else getattr(i, '_asdict')() for i in query.offset((page - 1) * page_size).limit(page_size)]
for i in query.offset((page - 1) * page_size).limit(page_size)]
else: def _must_be_required(self, _id):
offset = numfound - last_size existed = self.cls.get_by_id(_id)
if offset < 0: existed or abort(404, "Factor [{}] does not exist".format(_id))
offset = 0
return numfound, [i.to_dict() if fl is None else getattr(i, '_asdict')() return existed
for i in query.offset(offset).limit(last_size)]
def _can_add(self, **kwargs): def _can_add(self, **kwargs):
raise NotImplementedError raise NotImplementedError

View File

@ -1,72 +0,0 @@
# -*- 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: '' if v is None else v 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

@ -6,7 +6,7 @@ from functools import wraps
from flask import abort from flask import abort
from flask import request from flask import request
from api.lib.perm.acl.cache import AppCache from api.lib.perm.acl.cache import AppCache, AppAccessTokenCache
from api.lib.perm.acl.resp_format import ErrFormat from api.lib.perm.acl.resp_format import ErrFormat

View File

@ -117,15 +117,15 @@ class ACLManager(object):
if group: if group:
PermissionCRUD.grant(role.id, permissions, group_id=group.id) PermissionCRUD.grant(role.id, permissions, group_id=group.id)
def grant_resource_to_role_by_rid(self, name, rid, resource_type_name=None, permissions=None, rebuild=True): def grant_resource_to_role_by_rid(self, name, rid, resource_type_name=None, permissions=None):
resource = self._get_resource(name, resource_type_name) resource = self._get_resource(name, resource_type_name)
if resource: if resource:
PermissionCRUD.grant(rid, permissions, resource_id=resource.id, rebuild=rebuild) PermissionCRUD.grant(rid, permissions, resource_id=resource.id)
else: else:
group = self._get_resource_group(name) group = self._get_resource_group(name)
if group: if group:
PermissionCRUD.grant(rid, permissions, group_id=group.id, rebuild=rebuild) PermissionCRUD.grant(rid, permissions, group_id=group.id)
def revoke_resource_from_role(self, name, role, resource_type_name=None, permissions=None): def revoke_resource_from_role(self, name, role, resource_type_name=None, permissions=None):
resource = self._get_resource(name, resource_type_name) resource = self._get_resource(name, resource_type_name)
@ -138,26 +138,26 @@ class ACLManager(object):
if group: if group:
PermissionCRUD.revoke(role.id, permissions, group_id=group.id) PermissionCRUD.revoke(role.id, permissions, group_id=group.id)
def revoke_resource_from_role_by_rid(self, name, rid, resource_type_name=None, permissions=None, rebuild=True): def revoke_resource_from_role_by_rid(self, name, rid, resource_type_name=None, permissions=None):
resource = self._get_resource(name, resource_type_name) resource = self._get_resource(name, resource_type_name)
if resource: if resource:
PermissionCRUD.revoke(rid, permissions, resource_id=resource.id, rebuild=rebuild) PermissionCRUD.revoke(rid, permissions, resource_id=resource.id)
else: else:
group = self._get_resource_group(name) group = self._get_resource_group(name)
if group: if group:
PermissionCRUD.revoke(rid, permissions, group_id=group.id, rebuild=rebuild) PermissionCRUD.revoke(rid, permissions, group_id=group.id)
def del_resource(self, name, resource_type_name=None, rebuild=True): def del_resource(self, name, resource_type_name=None):
resource = self._get_resource(name, resource_type_name) resource = self._get_resource(name, resource_type_name)
if resource: if resource:
return ResourceCRUD.delete(resource.id, rebuild=rebuild) ResourceCRUD.delete(resource.id)
def has_permission(self, resource_name, resource_type, perm, resource_id=None, rid=None): def has_permission(self, resource_name, resource_type, perm, resource_id=None):
if is_app_admin(self.app_id): if is_app_admin(self.app_id):
return True return True
role = self._get_role(current_user.username) if rid is None else RoleCache.get(rid) role = self._get_role(current_user.username)
role or abort(404, ErrFormat.role_not_found.format(current_user.username)) role or abort(404, ErrFormat.role_not_found.format(current_user.username))

View File

@ -1,19 +1,14 @@
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
import datetime
import itertools import itertools
import json import json
from enum import Enum from enum import Enum
from typing import List from typing import List
from flask import has_request_context from flask import has_request_context, request
from flask import request
from flask_login import current_user from flask_login import current_user
from sqlalchemy import func from sqlalchemy import func
from api.extensions import db
from api.lib.perm.acl import AppCache from api.lib.perm.acl import AppCache
from api.models.acl import AuditLoginLog
from api.models.acl import AuditPermissionLog from api.models.acl import AuditPermissionLog
from api.models.acl import AuditResourceLog from api.models.acl import AuditResourceLog
from api.models.acl import AuditRoleLog from api.models.acl import AuditRoleLog
@ -288,27 +283,6 @@ class AuditCRUD(object):
return data return data
@staticmethod
def search_login(_, q=None, page=1, page_size=10, start=None, end=None):
query = db.session.query(AuditLoginLog)
if start:
query = query.filter(AuditLoginLog.login_at >= start)
if end:
query = query.filter(AuditLoginLog.login_at <= end)
if q:
query = query.filter(AuditLoginLog.username == q)
records = query.order_by(
AuditLoginLog.id.desc()).offset((page - 1) * page_size).limit(page_size).all()
data = {
'data': [r.to_dict() for r in records],
}
return data
@classmethod @classmethod
def add_role_log(cls, app_id, operate_type: AuditOperateType, def add_role_log(cls, app_id, operate_type: AuditOperateType,
scope: AuditScope, link_id: int, origin: dict, current: dict, extra: dict, scope: AuditScope, link_id: int, origin: dict, current: dict, extra: dict,
@ -374,32 +348,3 @@ class AuditCRUD(object):
AuditTriggerLog.create(app_id=app_id, trigger_id=trigger_id, operate_uid=user_id, AuditTriggerLog.create(app_id=app_id, trigger_id=trigger_id, operate_uid=user_id,
operate_type=operate_type.value, operate_type=operate_type.value,
origin=origin, current=current, extra=extra, source=source.value) origin=origin, current=current, extra=extra, source=source.value)
@classmethod
def add_login_log(cls, username, is_ok, description, _id=None, logout_at=None, ip=None, browser=None):
if _id is not None:
existed = AuditLoginLog.get_by_id(_id)
if existed is not None:
existed.update(logout_at=logout_at)
return
payload = dict(username=username,
is_ok=is_ok,
description=description,
logout_at=logout_at,
ip=(ip or request.headers.get('X-Forwarded-For') or
request.headers.get('X-Real-IP') or request.remote_addr or '').split(',')[0],
browser=browser or request.headers.get('User-Agent'),
channel=request.values.get('channel', 'web'),
)
if logout_at is None:
payload['login_at'] = datetime.datetime.now()
try:
from api.lib.common_setting.employee import EmployeeCRUD
EmployeeCRUD.update_last_login_by_uid(current_user.uid)
except:
pass
return AuditLoginLog.create(**payload).id

View File

@ -2,12 +2,10 @@
import msgpack import msgpack
import redis_lock
from api.extensions import cache from api.extensions import cache
from api.extensions import db from api.extensions import db
from api.extensions import rd from api.lib.utils import Lock
from api.lib.decorator import flush_db
from api.models.acl import App from api.models.acl import App
from api.models.acl import Permission from api.models.acl import Permission
from api.models.acl import Resource from api.models.acl import Resource
@ -138,14 +136,14 @@ class HasResourceRoleCache(object):
@classmethod @classmethod
def add(cls, rid, app_id): def add(cls, rid, app_id):
with redis_lock.Lock(rd.r, 'HasResourceRoleCache', expire=10): with Lock('HasResourceRoleCache'):
c = cls.get(app_id) c = cls.get(app_id)
c[rid] = 1 c[rid] = 1
cache.set(cls.PREFIX_KEY.format(app_id), c, timeout=0) cache.set(cls.PREFIX_KEY.format(app_id), c, timeout=0)
@classmethod @classmethod
def remove(cls, rid, app_id): def remove(cls, rid, app_id):
with redis_lock.Lock(rd.r, 'HasResourceRoleCache', expire=10): with Lock('HasResourceRoleCache'):
c = cls.get(app_id) c = cls.get(app_id)
c.pop(rid, None) c.pop(rid, None)
cache.set(cls.PREFIX_KEY.format(app_id), c, timeout=0) cache.set(cls.PREFIX_KEY.format(app_id), c, timeout=0)
@ -158,10 +156,9 @@ class RoleRelationCache(object):
PREFIX_RESOURCES2 = "RoleRelationResources2::id::{0}::AppId::{1}" PREFIX_RESOURCES2 = "RoleRelationResources2::id::{0}::AppId::{1}"
@classmethod @classmethod
def get_parent_ids(cls, rid, app_id, force=False): def get_parent_ids(cls, rid, app_id):
parent_ids = cache.get(cls.PREFIX_PARENT.format(rid, app_id)) parent_ids = cache.get(cls.PREFIX_PARENT.format(rid, app_id))
if not parent_ids or force: if not parent_ids:
db.session.commit()
from api.lib.perm.acl.role import RoleRelationCRUD from api.lib.perm.acl.role import RoleRelationCRUD
parent_ids = RoleRelationCRUD.get_parent_ids(rid, app_id) parent_ids = RoleRelationCRUD.get_parent_ids(rid, app_id)
cache.set(cls.PREFIX_PARENT.format(rid, app_id), parent_ids, timeout=0) cache.set(cls.PREFIX_PARENT.format(rid, app_id), parent_ids, timeout=0)
@ -169,10 +166,9 @@ class RoleRelationCache(object):
return parent_ids return parent_ids
@classmethod @classmethod
def get_child_ids(cls, rid, app_id, force=False): def get_child_ids(cls, rid, app_id):
child_ids = cache.get(cls.PREFIX_CHILDREN.format(rid, app_id)) child_ids = cache.get(cls.PREFIX_CHILDREN.format(rid, app_id))
if not child_ids or force: if not child_ids:
db.session.commit()
from api.lib.perm.acl.role import RoleRelationCRUD from api.lib.perm.acl.role import RoleRelationCRUD
child_ids = RoleRelationCRUD.get_child_ids(rid, app_id) child_ids = RoleRelationCRUD.get_child_ids(rid, app_id)
cache.set(cls.PREFIX_CHILDREN.format(rid, app_id), child_ids, timeout=0) cache.set(cls.PREFIX_CHILDREN.format(rid, app_id), child_ids, timeout=0)
@ -180,16 +176,14 @@ class RoleRelationCache(object):
return child_ids return child_ids
@classmethod @classmethod
def get_resources(cls, rid, app_id, force=False): def get_resources(cls, rid, app_id):
""" """
:param rid: :param rid:
:param app_id: :param app_id:
:param force:
:return: {id2perms: {resource_id: [perm,]}, group2perms: {group_id: [perm, ]}} :return: {id2perms: {resource_id: [perm,]}, group2perms: {group_id: [perm, ]}}
""" """
resources = cache.get(cls.PREFIX_RESOURCES.format(rid, app_id)) resources = cache.get(cls.PREFIX_RESOURCES.format(rid, app_id))
if not resources or force: if not resources:
db.session.commit()
from api.lib.perm.acl.role import RoleCRUD from api.lib.perm.acl.role import RoleCRUD
resources = RoleCRUD.get_resources(rid, app_id) resources = RoleCRUD.get_resources(rid, app_id)
if resources['id2perms'] or resources['group2perms']: if resources['id2perms'] or resources['group2perms']:
@ -198,10 +192,9 @@ class RoleRelationCache(object):
return resources or {} return resources or {}
@classmethod @classmethod
def get_resources2(cls, rid, app_id, force=False): def get_resources2(cls, rid, app_id):
r_g = cache.get(cls.PREFIX_RESOURCES2.format(rid, app_id)) r_g = cache.get(cls.PREFIX_RESOURCES2.format(rid, app_id))
if not r_g or force: if not r_g:
db.session.commit()
res = cls.get_resources(rid, app_id) res = cls.get_resources(rid, app_id)
id2perms = res['id2perms'] id2perms = res['id2perms']
group2perms = res['group2perms'] group2perms = res['group2perms']
@ -228,30 +221,24 @@ class RoleRelationCache(object):
return msgpack.loads(r_g, raw=False) return msgpack.loads(r_g, raw=False)
@classmethod @classmethod
@flush_db
def rebuild(cls, rid, app_id): def rebuild(cls, rid, app_id):
if app_id is None: cls.clean(rid, app_id)
app_ids = [None] + [i.id for i in App.get_by(to_dict=False)] db.session.remove()
cls.get_parent_ids(rid, app_id)
cls.get_child_ids(rid, app_id)
resources = cls.get_resources(rid, app_id)
if resources.get('id2perms') or resources.get('group2perms'):
HasResourceRoleCache.add(rid, app_id)
else: else:
app_ids = [app_id] HasResourceRoleCache.remove(rid, app_id)
cls.get_resources2(rid, app_id)
for _app_id in app_ids:
cls.clean(rid, _app_id)
cls.get_parent_ids(rid, _app_id, force=True)
cls.get_child_ids(rid, _app_id, force=True)
resources = cls.get_resources(rid, _app_id, force=True)
if resources.get('id2perms') or resources.get('group2perms'):
HasResourceRoleCache.add(rid, _app_id)
else:
HasResourceRoleCache.remove(rid, _app_id)
cls.get_resources2(rid, _app_id, force=True)
@classmethod @classmethod
@flush_db
def rebuild2(cls, rid, app_id): def rebuild2(cls, rid, app_id):
cache.delete(cls.PREFIX_RESOURCES2.format(rid, app_id)) cache.delete(cls.PREFIX_RESOURCES2.format(rid, app_id))
cls.get_resources2(rid, app_id, force=True) db.session.remove()
cls.get_resources2(rid, app_id)
@classmethod @classmethod
def clean(cls, rid, app_id): def clean(cls, rid, app_id):

View File

@ -71,7 +71,7 @@ class PermissionCRUD(object):
@classmethod @classmethod
def get_all2(cls, resource_name, resource_type_name, app_id): def get_all2(cls, resource_name, resource_type_name, app_id):
rt = ResourceType.get_by(name=resource_type_name, app_id=app_id, first=True, to_dict=False) rt = ResourceType.get_by(name=resource_type_name, first=True, to_dict=False)
rt or abort(404, ErrFormat.resource_type_not_found.format(resource_type_name)) rt or abort(404, ErrFormat.resource_type_not_found.format(resource_type_name))
r = Resource.get_by(name=resource_name, resource_type_id=rt.id, app_id=app_id, first=True, to_dict=False) r = Resource.get_by(name=resource_name, resource_type_id=rt.id, app_id=app_id, first=True, to_dict=False)
@ -79,8 +79,7 @@ class PermissionCRUD(object):
return r and cls.get_all(r.id) return r and cls.get_all(r.id)
@staticmethod @staticmethod
def grant(rid, perms, resource_id=None, group_id=None, rebuild=True, def grant(rid, perms, resource_id=None, group_id=None, rebuild=True, source=AuditOperateSource.acl):
source=AuditOperateSource.acl, force_update=False):
app_id = None app_id = None
rt_id = None rt_id = None
@ -107,23 +106,8 @@ class PermissionCRUD(object):
if not perms: if not perms:
perms = [i.get('name') for i in ResourceTypeCRUD.get_perms(group.resource_type_id)] perms = [i.get('name') for i in ResourceTypeCRUD.get_perms(group.resource_type_id)]
if force_update:
revoke_role_permissions = []
existed_perms = RolePermission.get_by(rid=rid,
app_id=app_id,
group_id=group_id,
resource_id=resource_id,
to_dict=False)
for role_perm in existed_perms:
perm = PermissionCache.get(role_perm.perm_id, rt_id)
if perm and perm.name not in perms:
role_perm.soft_delete()
revoke_role_permissions.append(role_perm)
AuditCRUD.add_permission_log(app_id, AuditOperateType.revoke, rid, rt_id,
revoke_role_permissions, source=source)
_role_permissions = [] _role_permissions = []
for _perm in set(perms): for _perm in set(perms):
perm = PermissionCache.get(_perm, rt_id) perm = PermissionCache.get(_perm, rt_id)
if not perm: if not perm:
@ -290,14 +274,12 @@ class PermissionCRUD(object):
perm2resource.setdefault(_perm, []).append(resource_id) perm2resource.setdefault(_perm, []).append(resource_id)
for _perm in perm2resource: for _perm in perm2resource:
perm = PermissionCache.get(_perm, resource_type_id) perm = PermissionCache.get(_perm, resource_type_id)
if perm is None: existeds = RolePermission.get_by(rid=rid,
continue app_id=app_id,
exists = RolePermission.get_by(rid=rid, perm_id=perm.id,
app_id=app_id, __func_in___key_resource_id=perm2resource[_perm],
perm_id=perm.id, to_dict=False)
__func_in___key_resource_id=perm2resource[_perm], for existed in existeds:
to_dict=False)
for existed in exists:
existed.deleted = True existed.deleted = True
existed.deleted_at = datetime.datetime.now() existed.deleted_at = datetime.datetime.now()
db.session.add(existed) db.session.add(existed)

View File

@ -2,6 +2,7 @@
from flask import abort from flask import abort
from flask import current_app
from api.extensions import db from api.extensions import db
from api.lib.perm.acl.audit import AuditCRUD from api.lib.perm.acl.audit import AuditCRUD
@ -126,18 +127,11 @@ class ResourceTypeCRUD(object):
existed_ids = [i.id for i in existed] existed_ids = [i.id for i in existed]
current_ids = [] current_ids = []
rebuild_rids = set()
for i in existed: for i in existed:
if i.name not in perms: if i.name not in perms:
i.soft_delete(commit=False) i.soft_delete()
for rp in RolePermission.get_by(perm_id=i.id, to_dict=False):
rp.soft_delete(commit=False)
rebuild_rids.add((rp.app_id, rp.rid))
else: else:
current_ids.append(i.id) current_ids.append(i.id)
db.session.commit()
for _app_id, _rid in rebuild_rids:
role_rebuild.apply_async(args=(_rid, _app_id), queue=ACL_QUEUE)
for i in perms: for i in perms:
if i not in existed_names: if i not in existed_names:
@ -266,8 +260,7 @@ class ResourceCRUD(object):
numfound = query.count() numfound = query.count()
res = [i.to_dict() for i in query.offset((page - 1) * page_size).limit(page_size)] res = [i.to_dict() for i in query.offset((page - 1) * page_size).limit(page_size)]
for i in res: for i in res:
user = UserCache.get(i['uid']) if i['uid'] else '' i['user'] = UserCache.get(i['uid']).nickname if i['uid'] else ''
i['user'] = user and user.nickname
return numfound, res return numfound, res
@ -282,6 +275,7 @@ class ResourceCRUD(object):
from api.tasks.acl import apply_trigger from api.tasks.acl import apply_trigger
triggers = TriggerCRUD.match_triggers(app_id, r.name, r.resource_type_id, uid) triggers = TriggerCRUD.match_triggers(app_id, r.name, r.resource_type_id, uid)
current_app.logger.info(triggers)
for trigger in triggers: for trigger in triggers:
# auto trigger should be no uid # auto trigger should be no uid
apply_trigger.apply_async(args=(trigger.id,), apply_trigger.apply_async(args=(trigger.id,),
@ -315,12 +309,9 @@ class ResourceCRUD(object):
return resource return resource
@staticmethod @staticmethod
def delete(_id, rebuild=True, app_id=None): def delete(_id):
resource = Resource.get_by_id(_id) or abort(404, ErrFormat.resource_not_found.format("id={}".format(_id))) resource = Resource.get_by_id(_id) or abort(404, ErrFormat.resource_not_found.format("id={}".format(_id)))
if app_id is not None and resource.app_id != app_id:
return abort(404, ErrFormat.resource_not_found.format("id={}".format(_id)))
origin = resource.to_dict() origin = resource.to_dict()
resource.soft_delete() resource.soft_delete()
@ -331,15 +322,12 @@ class ResourceCRUD(object):
i.soft_delete() i.soft_delete()
rebuilds.append((i.rid, i.app_id)) rebuilds.append((i.rid, i.app_id))
if rebuild: for rid, app_id in set(rebuilds):
for rid, app_id in set(rebuilds): role_rebuild.apply_async(args=(rid, app_id), queue=ACL_QUEUE)
role_rebuild.apply_async(args=(rid, app_id), queue=ACL_QUEUE)
AuditCRUD.add_resource_log(resource.app_id, AuditOperateType.delete, AuditCRUD.add_resource_log(resource.app_id, AuditOperateType.delete,
AuditScope.resource, resource.id, origin, {}, {}) AuditScope.resource, resource.id, origin, {}, {})
return rebuilds
@classmethod @classmethod
def delete_by_name(cls, name, type_id, app_id): def delete_by_name(cls, name, type_id, app_id):
resource = Resource.get_by(name=name, resource_type_id=type_id, app_id=app_id) or abort( resource = Resource.get_by(name=name, resource_type_id=type_id, app_id=app_id) or abort(

View File

@ -1,50 +1,43 @@
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
from flask_babel import lazy_gettext as _l
from api.lib.resp_format import CommonErrFormat from api.lib.resp_format import CommonErrFormat
class ErrFormat(CommonErrFormat): class ErrFormat(CommonErrFormat):
login_succeed = _l("login successful") # 登录成功 auth_only_with_app_token_failed = "应用 Token验证失败"
ldap_connection_failed = _l("Failed to connect to LDAP service") # 连接LDAP服务失败 session_invalid = "您不是应用管理员 或者 session失效(尝试一下退出重新登录)"
invalid_password = _l("Password verification failed") # 密码验证失败
auth_only_with_app_token_failed = _l("Application Token verification failed") # 应用 Token验证失败
# 您不是应用管理员 或者 session失效(尝试一下退出重新登录)
session_invalid = _l(
"You are not the application administrator or the session has expired (try logging out and logging in again)")
resource_type_not_found = _l("Resource type {} does not exist!") # 资源类型 {} 不存在! resource_type_not_found = "资源类型 {} 不存在!"
resource_type_exists = _l("Resource type {} already exists!") # 资源类型 {} 已经存在! resource_type_exists = "资源类型 {} 已经存在!"
# 因为该类型下有资源的存在, 不能删除! resource_type_cannot_delete = "因为该类型下有资源的存在, 不能删除!"
resource_type_cannot_delete = _l("Because there are resources under this type, they cannot be deleted!")
user_not_found = _l("User {} does not exist!") # 用户 {} 不存在! user_not_found = "用户 {} 不存在!"
user_exists = _l("User {} already exists!") # 用户 {} 已经存在! user_exists = "用户 {} 已经存在!"
role_not_found = _l("Role {} does not exist!") # 角色 {} 不存在! role_not_found = "角色 {} 不存在!"
role_exists = _l("Role {} already exists!") # 角色 {} 已经存在! role_exists = "角色 {} 已经存在!"
global_role_not_found = _l("Global role {} does not exist!") # 全局角色 {} 不存在! global_role_not_found = "全局角色 {} 不存在!"
global_role_exists = _l("Global role {} already exists!") # 全局角色 {} 已经存在! global_role_exists = "全局角色 {} 已经存在!"
user_role_delete_invalid = "删除用户角色, 请在 用户管理 页面操作!"
resource_no_permission = _l("You do not have {} permission on resource: {}") # 您没有资源: {} 的 {} 权限 resource_no_permission = "您没有资源: {}{} 权限"
admin_required = _l("Requires administrator permissions") # 需要管理员权限 admin_required = "需要管理员权限"
role_required = _l("Requires role: {}") # 需要角色: {} role_required = "需要角色: {}"
# 删除用户角色, 请在 用户管理 页面操作!
user_role_delete_invalid = _l("To delete a user role, please operate on the User Management page!")
app_is_ready_existed = _l("Application {} already exists") # 应用 {} 已经存在 app_is_ready_existed = "应用 {} 已经存在"
app_not_found = _l("Application {} does not exist!") # 应用 {} 不存在! app_not_found = "应用 {} 不存在!"
app_secret_invalid = _l("The Secret is invalid") # 应用的Secret无效 app_secret_invalid = "应用的Secret无效"
resource_not_found = _l("Resource {} does not exist!") # 资源 {} 不存在! resource_not_found = "资源 {} 不存在!"
resource_exists = _l("Resource {} already exists!") # 资源 {} 已经存在! resource_exists = "资源 {} 已经存在!"
resource_group_not_found = _l("Resource group {} does not exist!") # 资源组 {} 不存在! resource_group_not_found = "资源组 {} 不存在!"
resource_group_exists = _l("Resource group {} already exists!") # 资源组 {} 已经存在! resource_group_exists = "资源组 {} 已经存在!"
inheritance_dead_loop = _l("Inheritance detected infinite loop") # 继承检测到了死循环 inheritance_dead_loop = "继承检测到了死循环"
role_relation_not_found = _l("Role relationship {} does not exist!") # 角色关系 {} 不存在! role_relation_not_found = "角色关系 {} 不存在!"
trigger_not_found = _l("Trigger {} does not exist!") # 触发器 {} 不存在! trigger_not_found = "触发器 {} 不存在!"
trigger_exists = _l("Trigger {} already exists!") # 触发器 {} 已经存在! trigger_exists = "触发器 {} 已经存在!"
trigger_disabled = _l("Trigger {} has been disabled!") # Trigger {} has been disabled! trigger_disabled = "触发器 {} 已经被禁用!"
invalid_password = "密码不正确!"

View File

@ -1,15 +1,18 @@
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
import redis_lock
import time
import six import six
from flask import abort from flask import abort
from flask import current_app from flask import current_app
from sqlalchemy import or_ from sqlalchemy import or_
from api.extensions import db from api.extensions import db
from api.extensions import rd
from api.lib.perm.acl.app import AppCRUD from api.lib.perm.acl.app import AppCRUD
from api.lib.perm.acl.audit import AuditCRUD, AuditOperateType, AuditScope from api.lib.perm.acl.audit import AuditCRUD
from api.lib.perm.acl.audit import AuditOperateType
from api.lib.perm.acl.audit import AuditScope
from api.lib.perm.acl.cache import AppCache from api.lib.perm.acl.cache import AppCache
from api.lib.perm.acl.cache import HasResourceRoleCache from api.lib.perm.acl.cache import HasResourceRoleCache
from api.lib.perm.acl.cache import RoleCache from api.lib.perm.acl.cache import RoleCache
@ -61,25 +64,23 @@ class RoleRelationCRUD(object):
id2parents = {} id2parents = {}
for i in res: for i in res:
parent = RoleCache.get(i.parent_id) id2parents.setdefault(rid2uid.get(i.child_id, i.child_id), []).append(RoleCache.get(i.parent_id).to_dict())
if parent:
id2parents.setdefault(rid2uid.get(i.child_id, i.child_id), []).append(parent.to_dict())
return id2parents return id2parents
@staticmethod @staticmethod
def get_parent_ids(rid, app_id): def get_parent_ids(rid, app_id):
if app_id is not None: if app_id is not None:
return [i.parent_id for i in RoleRelation.get_by(child_id=rid, app_id=app_id, to_dict=False)] + \ return ([i.parent_id for i in RoleRelation.get_by(child_id=rid, app_id=app_id, to_dict=False)] +
[i.parent_id for i in RoleRelation.get_by(child_id=rid, app_id=None, to_dict=False)] [i.parent_id for i in RoleRelation.get_by(child_id=rid, app_id=None, to_dict=False)])
else: else:
return [i.parent_id for i in RoleRelation.get_by(child_id=rid, app_id=app_id, to_dict=False)] return [i.parent_id for i in RoleRelation.get_by(child_id=rid, app_id=app_id, to_dict=False)]
@staticmethod @staticmethod
def get_child_ids(rid, app_id): def get_child_ids(rid, app_id):
if app_id is not None: if app_id is not None:
return [i.child_id for i in RoleRelation.get_by(parent_id=rid, app_id=app_id, to_dict=False)] + \ return ([i.child_id for i in RoleRelation.get_by(parent_id=rid, app_id=app_id, to_dict=False)] +
[i.child_id for i in RoleRelation.get_by(parent_id=rid, app_id=None, to_dict=False)] [i.child_id for i in RoleRelation.get_by(parent_id=rid, app_id=None, to_dict=False)])
else: else:
return [i.child_id for i in RoleRelation.get_by(parent_id=rid, app_id=app_id, to_dict=False)] return [i.child_id for i in RoleRelation.get_by(parent_id=rid, app_id=app_id, to_dict=False)]
@ -142,27 +143,24 @@ class RoleRelationCRUD(object):
@classmethod @classmethod
def add(cls, role, parent_id, child_ids, app_id): def add(cls, role, parent_id, child_ids, app_id):
with redis_lock.Lock(rd.r, "ROLE_RELATION_ADD", expire=10): result = []
db.session.commit() for child_id in child_ids:
existed = RoleRelation.get_by(parent_id=parent_id, child_id=child_id, app_id=app_id)
if existed:
continue
result = [] RoleRelationCache.clean(parent_id, app_id)
for child_id in child_ids: RoleRelationCache.clean(child_id, app_id)
existed = RoleRelation.get_by(parent_id=parent_id, child_id=child_id, app_id=app_id)
if existed:
continue
if parent_id in cls.recursive_child_ids(child_id, app_id): if parent_id in cls.recursive_child_ids(child_id, app_id):
return abort(400, ErrFormat.inheritance_dead_loop) return abort(400, ErrFormat.inheritance_dead_loop)
result.append(RoleRelation.create(parent_id=parent_id, child_id=child_id, app_id=app_id).to_dict()) if app_id is None:
for app in AppCRUD.get_all():
if app.name != "acl":
RoleRelationCache.clean(child_id, app.id)
RoleRelationCache.clean(parent_id, app_id) result.append(RoleRelation.create(parent_id=parent_id, child_id=child_id, app_id=app_id).to_dict())
RoleRelationCache.clean(child_id, app_id)
if app_id is None:
for app in AppCRUD.get_all():
if app.name != "acl":
RoleRelationCache.clean(child_id, app.id)
AuditCRUD.add_role_log(app_id, AuditOperateType.role_relation_add, AuditCRUD.add_role_log(app_id, AuditOperateType.role_relation_add,
AuditScope.role_relation, role.id, {}, {}, AuditScope.role_relation, role.id, {}, {},
@ -217,6 +215,7 @@ class RoleCRUD(object):
@staticmethod @staticmethod
def search(q, app_id, page=1, page_size=None, user_role=True, is_all=False, user_only=False): def search(q, app_id, page=1, page_size=None, user_role=True, is_all=False, user_only=False):
if user_only: # only user role if user_only: # only user role
query = db.session.query(Role).filter(Role.deleted.is_(False)).filter(Role.uid.isnot(None)) query = db.session.query(Role).filter(Role.deleted.is_(False)).filter(Role.uid.isnot(None))
@ -274,13 +273,6 @@ class RoleCRUD(object):
RoleCache.clean(rid) RoleCache.clean(rid)
role = role.update(**kwargs) 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, AuditCRUD.add_role_log(role.app_id, AuditOperateType.update,
AuditScope.role, role.id, origin, role.to_dict(), {}, AuditScope.role, role.id, origin, role.to_dict(), {},
) )
@ -299,11 +291,12 @@ class RoleCRUD(object):
from api.lib.perm.acl.acl import is_admin 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))) 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) not force and role.uid and abort(400, ErrFormat.user_role_delete_invalid)
if not role.app_id and not is_admin():
return abort(403, ErrFormat.admin_required)
origin = role.to_dict() origin = role.to_dict()
child_ids = [] child_ids = []
@ -312,18 +305,20 @@ class RoleCRUD(object):
for i in RoleRelation.get_by(parent_id=rid, to_dict=False): for i in RoleRelation.get_by(parent_id=rid, to_dict=False):
child_ids.append(i.child_id) child_ids.append(i.child_id)
i.soft_delete() i.soft_delete(commit=False)
for i in RoleRelation.get_by(child_id=rid, to_dict=False): for i in RoleRelation.get_by(child_id=rid, to_dict=False):
parent_ids.append(i.parent_id) parent_ids.append(i.parent_id)
i.soft_delete() i.soft_delete(commit=False)
role_permissions = [] role_permissions = []
for i in RolePermission.get_by(rid=rid, to_dict=False): for i in RolePermission.get_by(rid=rid, to_dict=False):
role_permissions.append(i.to_dict()) role_permissions.append(i.to_dict())
i.soft_delete() i.soft_delete(commit=False)
role.soft_delete() role.soft_delete(commit=False)
db.session.commit()
role_rebuild.apply_async(args=(recursive_child_ids, role.app_id), queue=ACL_QUEUE) role_rebuild.apply_async(args=(recursive_child_ids, role.app_id), queue=ACL_QUEUE)
@ -376,16 +371,16 @@ class RoleCRUD(object):
resource_type_id = resource_type and resource_type.id resource_type_id = resource_type and resource_type.id
result = dict(resources=dict(), groups=dict()) result = dict(resources=dict(), groups=dict())
# s = time.time() s = time.time()
parent_ids = RoleRelationCRUD.recursive_parent_ids(rid, app_id) parent_ids = RoleRelationCRUD.recursive_parent_ids(rid, app_id)
# current_app.logger.info('parent ids {0}: {1}'.format(parent_ids, time.time() - s)) current_app.logger.info('parent ids {0}: {1}'.format(parent_ids, time.time() - s))
for parent_id in parent_ids: for parent_id in parent_ids:
_resources, _groups = cls._extend_resources(parent_id, resource_type_id, app_id) _resources, _groups = cls._extend_resources(parent_id, resource_type_id, app_id)
# current_app.logger.info('middle1: {0}'.format(time.time() - s)) current_app.logger.info('middle1: {0}'.format(time.time() - s))
_merge(result['resources'], _resources) _merge(result['resources'], _resources)
# current_app.logger.info('middle2: {0}'.format(time.time() - s)) current_app.logger.info('middle2: {0}'.format(time.time() - s))
# current_app.logger.info(len(_groups)) current_app.logger.info(len(_groups))
if not group_flat: if not group_flat:
_merge(result['groups'], _groups) _merge(result['groups'], _groups)
else: else:
@ -396,7 +391,7 @@ class RoleCRUD(object):
item.setdefault('permissions', []) item.setdefault('permissions', [])
item['permissions'] = list(set(item['permissions'] + _groups[rg_id]['permissions'])) item['permissions'] = list(set(item['permissions'] + _groups[rg_id]['permissions']))
result['resources'][item['id']] = item result['resources'][item['id']] = item
# current_app.logger.info('End: {0}'.format(time.time() - s)) current_app.logger.info('End: {0}'.format(time.time() - s))
result['resources'] = list(result['resources'].values()) result['resources'] = list(result['resources'].values())
result['groups'] = list(result['groups'].values()) result['groups'] = list(result['groups'].values())

View File

@ -41,7 +41,6 @@ class UserCRUD(object):
@classmethod @classmethod
def add(cls, **kwargs): def add(cls, **kwargs):
add_from = kwargs.pop('add_from', None)
existed = User.get_by(username=kwargs['username']) existed = User.get_by(username=kwargs['username'])
existed and abort(400, ErrFormat.user_exists.format(kwargs['username'])) existed and abort(400, ErrFormat.user_exists.format(kwargs['username']))
@ -59,17 +58,11 @@ class UserCRUD(object):
kwargs['employee_id'] = '{0:04d}'.format(biggest_employee_id + 1) kwargs['employee_id'] = '{0:04d}'.format(biggest_employee_id + 1)
user = User.create(**kwargs) user = User.create(**kwargs)
role = RoleCRUD.add_role(user.username, uid=user.uid) RoleCRUD.add_role(user.username, uid=user.uid)
AuditCRUD.add_role_log(None, AuditOperateType.create, AuditCRUD.add_role_log(None, AuditOperateType.create,
AuditScope.user, user.uid, {}, user.to_dict(), {}, {} AuditScope.user, user.uid, {}, user.to_dict(), {}, {}
) )
if add_from != 'common':
from api.lib.common_setting.employee import EmployeeCRUD
payload = {column: getattr(user, column) for column in ['uid', 'username', 'nickname', 'email', 'block']}
payload['rid'] = role.id
EmployeeCRUD.add_employee_from_acl_created(**payload)
return user return user
@staticmethod @staticmethod

View File

@ -51,12 +51,12 @@ def _auth_with_key():
user, authenticated = User.query.authenticate_with_key(key, secret, req_args, path) user, authenticated = User.query.authenticate_with_key(key, secret, req_args, path)
if user and authenticated: if user and authenticated:
login_user(user) login_user(user)
# reset_session(user) reset_session(user)
return True return True
role, authenticated = Role.query.authenticate_with_key(key, secret, req_args, path) role, authenticated = Role.query.authenticate_with_key(key, secret, req_args, path)
if role and authenticated: if role and authenticated:
# reset_session(None, role=role.name) reset_session(None, role=role.name)
return True return True
return False return False
@ -93,9 +93,6 @@ def _auth_with_token():
def _auth_with_ip_white_list(): def _auth_with_ip_white_list():
if request.url.endswith("acl/users/info"):
return False
ip = request.headers.get('X-Real-IP') or request.remote_addr ip = request.headers.get('X-Real-IP') or request.remote_addr
key = request.values.get('_key') key = request.values.get('_key')
secret = request.values.get('_secret') secret = request.values.get('_secret')

View File

@ -1 +0,0 @@
# -*- coding:utf-8 -*-

View File

@ -1,67 +0,0 @@
# -*- coding:utf-8 -*-
import uuid
from flask import abort
from flask import current_app
from flask import session
from ldap3 import ALL
from ldap3 import AUTO_BIND_NO_TLS
from ldap3 import Connection
from ldap3 import Server
from ldap3.core.exceptions import LDAPBindError
from ldap3.core.exceptions import LDAPCertificateError
from ldap3.core.exceptions import LDAPSocketOpenError
from api.lib.common_setting.common_data import AuthenticateDataCRUD
from api.lib.common_setting.const import AuthenticateType
from api.lib.perm.acl.audit import AuditCRUD
from api.lib.perm.acl.resp_format import ErrFormat
from api.models.acl import User
def authenticate_with_ldap(username, password):
config = AuthenticateDataCRUD(AuthenticateType.LDAP).get()
server = Server(config.get('ldap_server'), get_info=ALL, connect_timeout=3)
if '@' in username:
email = username
who = config.get('ldap_user_dn').format(username.split('@')[0])
else:
who = config.get('ldap_user_dn').format(username)
email = "{}@{}".format(who, config.get('ldap_domain'))
username = username.split('@')[0]
user = User.query.get_by_username(username)
try:
if not password:
raise LDAPCertificateError
try:
conn = Connection(server, user=who, password=password, auto_bind=AUTO_BIND_NO_TLS)
except LDAPBindError:
conn = Connection(server,
user=f"{username}@{config.get('ldap_domain')}",
password=password,
auto_bind=AUTO_BIND_NO_TLS)
if conn.result['result'] != 0:
AuditCRUD.add_login_log(username, False, ErrFormat.invalid_password)
raise LDAPBindError
else:
_id = AuditCRUD.add_login_log(username, True, ErrFormat.login_succeed)
session['LOGIN_ID'] = _id
if not user:
from api.lib.perm.acl.user import UserCRUD
user = UserCRUD.add(username=username, email=email, password=uuid.uuid4().hex)
return user, True
except LDAPBindError as e:
current_app.logger.info(e)
return user, False
except LDAPSocketOpenError as e:
current_app.logger.info(e)
return abort(403, ErrFormat.ldap_connection_failed)

View File

@ -1,30 +0,0 @@
# -*- coding:utf-8 -*-
from flask import current_app
from . import routing
class OAuth2(object):
def __init__(self, app=None, url_prefix=None):
self._app = app
if app is not None:
self.init_app(app, url_prefix)
@staticmethod
def init_app(app, url_prefix=None):
# Configuration defaults
app.config.setdefault('OAUTH2_GRANT_TYPE', 'authorization_code')
app.config.setdefault('OAUTH2_RESPONSE_TYPE', 'code')
app.config.setdefault('OAUTH2_AFTER_LOGIN', '/')
app.config.setdefault('OIDC_GRANT_TYPE', 'authorization_code')
app.config.setdefault('OIDC_RESPONSE_TYPE', 'code')
app.config.setdefault('OIDC_AFTER_LOGIN', '/')
# Register Blueprint
app.register_blueprint(routing.blueprint, url_prefix=url_prefix)
@property
def app(self):
return self._app or current_app

View File

@ -1,139 +0,0 @@
# -*- coding:utf-8 -*-
import datetime
import secrets
import uuid
import requests
from flask import Blueprint
from flask import abort
from flask import current_app
from flask import redirect
from flask import request
from flask import session
from flask import url_for
from flask_login import login_user
from flask_login import logout_user
from six.moves.urllib.parse import urlencode
from six.moves.urllib.parse import urlparse
from api.lib.common_setting.common_data import AuthenticateDataCRUD
from api.lib.perm.acl.audit import AuditCRUD
from api.lib.perm.acl.cache import UserCache
from api.lib.perm.acl.resp_format import ErrFormat
blueprint = Blueprint('oauth2', __name__)
@blueprint.route('/api/<string:auth_type>/login')
def login(auth_type):
config = AuthenticateDataCRUD(auth_type.upper()).get()
if request.values.get("next"):
session["next"] = request.values.get("next")
session[f'{auth_type}_state'] = secrets.token_urlsafe(16)
auth_type = auth_type.upper()
redirect_uri = "{}://{}{}".format(urlparse(request.referrer).scheme,
urlparse(request.referrer).netloc,
url_for('oauth2.callback', auth_type=auth_type.lower()))
qs = urlencode({
'client_id': config['client_id'],
'redirect_uri': redirect_uri,
'response_type': current_app.config[f'{auth_type}_RESPONSE_TYPE'],
'scope': ' '.join(config['scopes'] or []),
'state': session[f'{auth_type.lower()}_state'],
})
return redirect("{}?{}".format(config['authorize_url'].split('?')[0], qs))
@blueprint.route('/api/<string:auth_type>/callback')
def callback(auth_type):
auth_type = auth_type.upper()
config = AuthenticateDataCRUD(auth_type).get()
redirect_url = session.get("next") or config.get('after_login') or '/'
if request.values['state'] != session.get(f'{auth_type.lower()}_state'):
return abort(401, "state is invalid")
if 'code' not in request.values:
return abort(401, 'code is invalid')
response = requests.post(config['token_url'], data={
'client_id': config['client_id'],
'client_secret': config['client_secret'],
'code': request.values['code'],
'grant_type': current_app.config[f'{auth_type}_GRANT_TYPE'],
'redirect_uri': url_for('oauth2.callback', auth_type=auth_type.lower(), _external=True),
}, headers={'Accept': 'application/json'})
if response.status_code != 200:
current_app.logger.error(response.text)
return abort(401)
access_token = response.json().get('access_token')
if not access_token:
return abort(401)
response = requests.get(config['user_info']['url'], headers={
'Authorization': 'Bearer {}'.format(access_token),
'Accept': 'application/json',
})
if response.status_code != 200:
return abort(401)
res = response.json()
email = res.get(config['user_info']['email'])
username = res.get(config['user_info']['username'])
avatar = res.get(config['user_info'].get('avatar'))
user = UserCache.get(username)
if user is None:
current_app.logger.info("create user: {}".format(username))
from api.lib.perm.acl.user import UserCRUD
user_dict = dict(username=username, email=email, avatar=avatar)
user_dict['password'] = uuid.uuid4().hex
user = UserCRUD.add(**user_dict)
# log the user in
login_user(user)
from api.lib.perm.acl.acl import ACLManager
user_info = ACLManager.get_user_info(username)
session["acl"] = dict(uid=user_info.get("uid"),
avatar=user.avatar if user else user_info.get("avatar"),
userId=user_info.get("uid"),
rid=user_info.get("rid"),
userName=user_info.get("username"),
nickName=user_info.get("nickname") or user_info.get("username"),
parentRoles=user_info.get("parents"),
childRoles=user_info.get("children"),
roleName=user_info.get("role"))
session["uid"] = user_info.get("uid")
_id = AuditCRUD.add_login_log(username, True, ErrFormat.login_succeed)
session['LOGIN_ID'] = _id
return redirect(redirect_url)
@blueprint.route('/api/<string:auth_type>/logout')
def logout(auth_type):
"acl" in session and session.pop("acl")
"uid" in session and session.pop("uid")
f'{auth_type}_state' in session and session.pop(f'{auth_type}_state')
"next" in session and session.pop("next")
redirect_url = url_for('oauth2.login', auth_type=auth_type, _external=True, next=request.referrer)
logout_user()
current_app.logger.debug('Redirecting to: {0}'.format(redirect_url))
AuditCRUD.add_login_log(None, None, None, _id=session.get('LOGIN_ID'), logout_at=datetime.datetime.now())
return redirect(redirect_url)

View File

@ -1,34 +1,27 @@
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
from flask_babel import lazy_gettext as _l
class CommonErrFormat(object): class CommonErrFormat(object):
unauthorized = _l("unauthorized") # 未认证 unauthorized = "未认证"
unknown_error = _l("unknown error") # 未知错误 unknown_error = "未知错误"
invalid_request = _l("Illegal request") # 不合法的请求 invalid_request = "不合法的请求"
invalid_operation = _l("Invalid operation") # 无效的操作 invalid_operation = "无效的操作"
not_found = _l("does not exist") # 不存在 not_found = "不存在"
circular_dependency_error = _l("There is a circular dependency!") # 存在循环依赖! unknown_search_error = "未知搜索错误"
unknown_search_error = _l("Unknown search error") # 未知搜索错误 invalid_json = "json格式似乎不正确了, 请仔细确认一下!"
# json格式似乎不正确了, 请仔细确认一下! datetime_argument_invalid = "参数 {} 格式不正确, 格式必须是: yyyy-mm-dd HH:MM:SS"
invalid_json = _l("The json format seems to be incorrect, please confirm carefully!")
# 参数 {} 格式不正确, 格式必须是: yyyy-mm-dd HH:MM:SS argument_value_required = "参数 {} 的值不能为空!"
datetime_argument_invalid = _l("The format of parameter {} is incorrect, the format must be: yyyy-mm-dd HH:MM:SS") argument_required = "请求缺少参数 {}"
argument_invalid = "参数 {} 的值无效"
argument_str_length_limit = "参数 {} 的长度必须 <= {}"
argument_value_required = _l("The value of parameter {} cannot be empty!") # 参数 {} 的值不能为空! role_required = "角色 {} 才能操作!"
argument_required = _l("The request is missing parameters {}") # 请求缺少参数 {} user_not_found = "用户 {} 不存在"
argument_invalid = _l("Invalid value for parameter {}") # 参数 {} 的值无效 no_permission = "您没有资源: {}{}权限!"
argument_str_length_limit = _l("The length of parameter {} must be <= {}") # 参数 {} 的长度必须 <= {} no_permission2 = "您没有操作权限!"
no_permission_only_owner = "只有创建人或者管理员才有权限!"
role_required = _l("Role {} can only operate!") # 角色 {} 才能操作!
user_not_found = _l("User {} does not exist") # 用户 {} 不存在
no_permission = _l("For resource: {}, you do not have {} permission!") # 您没有资源: {} 的{}权限!
no_permission2 = _l("You do not have permission to operate!") # 您没有操作权限!
no_permission_only_owner = _l("Only the creator or administrator has permission!") # 只有创建人或者管理员才有权限!

View File

@ -1 +0,0 @@
# -*- coding:utf-8 -*-

View File

@ -1,494 +0,0 @@
import json
import os
import secrets
import sys
import threading
from base64 import b64decode, b64encode
from Cryptodome.Protocol.SecretSharing import Shamir
from colorama import Back, Fore, Style, init as colorama_init
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes, padding
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from flask import current_app
global_iv_length = 16
global_key_shares = 5 # Number of generated key shares
global_key_threshold = 3 # Minimum number of shares required to rebuild the key
backend_root_key_name = "root_key"
backend_encrypt_key_name = "encrypt_key"
backend_root_key_salt_name = "root_key_salt"
backend_encrypt_key_salt_name = "encrypt_key_salt"
backend_seal_key = "seal_status"
success = "success"
seal_status = True
secrets_encrypt_key = ""
secrets_root_key = ""
def string_to_bytes(value):
if not value:
return ""
if isinstance(value, bytes):
return value
if sys.version_info.major == 2:
byte_string = value
else:
byte_string = value.encode("utf-8")
return byte_string
class Backend:
def __init__(self, backend=None):
self.backend = backend
# cache is a redis object
self.cache = backend.cache
def get(self, key):
return self.backend.get(key)
def add(self, key, value):
return self.backend.add(key, value)
def update(self, key, value):
return self.backend.update(key, value)
def get_shares(self, key):
return self.backend.get_shares(key)
def set_shares(self, key, value):
return self.backend.set_shares(key, value)
class KeyManage:
def __init__(self, trigger=None, backend=None):
self.trigger = trigger
self.backend = backend
self.share_key = "cmdb::secret::secrets_share"
if backend:
self.backend = Backend(backend)
def init_app(self, app, backend=None):
if (sys.argv[0].endswith("gunicorn") or
(len(sys.argv) > 1 and sys.argv[1] in ("run", "cmdb-password-data-migrate"))):
self.backend = backend
threading.Thread(target=self.watch_root_key, args=(app,), daemon=True).start()
self.trigger = app.config.get("INNER_TRIGGER_TOKEN")
if not self.trigger:
return
resp = self.auto_unseal()
self.print_response(resp)
def hash_root_key(self, value):
algorithm = hashes.SHA256()
salt = self.backend.get(backend_root_key_salt_name)
if not salt:
salt = secrets.token_hex(16)
msg, ok = self.backend.add(backend_root_key_salt_name, salt)
if not ok:
return msg, ok
kdf = PBKDF2HMAC(
algorithm=algorithm,
length=32,
salt=string_to_bytes(salt),
iterations=100000,
)
key = kdf.derive(string_to_bytes(value))
return b64encode(key).decode('utf-8'), True
def generate_encrypt_key(self, key):
algorithm = hashes.SHA256()
salt = self.backend.get(backend_encrypt_key_salt_name)
if not salt:
salt = secrets.token_hex(32)
kdf = PBKDF2HMAC(
algorithm=algorithm,
length=32,
salt=string_to_bytes(salt),
iterations=100000,
backend=default_backend()
)
key = kdf.derive(string_to_bytes(key))
msg, ok = self.backend.add(backend_encrypt_key_salt_name, salt)
if ok:
return b64encode(key).decode('utf-8'), ok
else:
return msg, ok
@classmethod
def generate_keys(cls, secret):
shares = Shamir.split(global_key_threshold, global_key_shares, secret, False)
new_shares = []
for share in shares:
t = [i for i in share[1]] + [ord(i) for i in "{:0>2}".format(share[0])]
new_shares.append(b64encode(bytes(t)))
return new_shares
def is_valid_root_key(self, root_key):
if not root_key:
return False
root_key_hash, ok = self.hash_root_key(root_key)
if not ok:
return root_key_hash, ok
backend_root_key_hash = self.backend.get(backend_root_key_name)
if not backend_root_key_hash:
return "should init firstly", False
elif backend_root_key_hash != root_key_hash:
return "invalid root key", False
else:
return "", True
def auth_root_secret(self, root_key, app):
with app.app_context():
msg, ok = self.is_valid_root_key(root_key)
if not ok:
return {
"message": msg,
"status": "failed"
}
encrypt_key_aes = self.backend.get(backend_encrypt_key_name)
if not encrypt_key_aes:
return {
"message": "encrypt key is empty",
"status": "failed"
}
secret_encrypt_key, ok = InnerCrypt.aes_decrypt(string_to_bytes(root_key), encrypt_key_aes)
if ok:
msg, ok = self.backend.update(backend_seal_key, "open")
if ok:
global secrets_encrypt_key, secrets_root_key
secrets_encrypt_key = secret_encrypt_key
secrets_root_key = root_key
self.backend.cache.set(self.share_key, json.dumps([]))
return {"message": success, "status": success}
return {"message": msg, "status": "failed"}
else:
return {
"message": secret_encrypt_key,
"status": "failed"
}
def parse_shares(self, shares, app):
if len(shares) >= global_key_threshold:
recovered_secret = Shamir.combine(shares[:global_key_threshold], False)
return self.auth_root_secret(b64encode(recovered_secret), app)
def unseal(self, key):
if not self.is_seal():
return {
"message": "current status is unseal, skip",
"status": "skip"
}
try:
t = [i for i in b64decode(key)]
v = (int("".join([chr(i) for i in t[-2:]])), bytes(t[:-2]))
shares = self.backend.get_shares(self.share_key)
if v not in shares:
shares.append(v)
self.set_shares(shares)
if len(shares) >= global_key_threshold:
return self.parse_shares(shares, current_app)
else:
return {
"message": "waiting for inputting other unseal key {0}/{1}".format(len(shares),
global_key_threshold),
"status": "waiting"
}
except Exception as e:
return {
"message": "invalid token: " + str(e),
"status": "failed"
}
def generate_unseal_keys(self):
info = self.backend.get(backend_root_key_name)
if info:
return "already exist", [], False
secret = AESGCM.generate_key(128)
shares = self.generate_keys(secret)
return b64encode(secret), shares, True
def init(self):
"""
init the master key, unseal key and store in backend
:return:
"""
root_key = self.backend.get(backend_root_key_name)
if root_key:
return {"message": "already init, skip", "status": "skip"}, False
else:
root_key, shares, status = self.generate_unseal_keys()
if not status:
return {"message": root_key, "status": "failed"}, False
# hash root key and store in backend
root_key_hash, ok = self.hash_root_key(root_key)
if not ok:
return {"message": root_key_hash, "status": "failed"}, False
msg, ok = self.backend.add(backend_root_key_name, root_key_hash)
if not ok:
return {"message": msg, "status": "failed"}, False
# generate encrypt key from root_key and store in backend
encrypt_key, ok = self.generate_encrypt_key(root_key)
if not ok:
return {"message": encrypt_key, "status": "failed"}
encrypt_key_aes, status = InnerCrypt.aes_encrypt(root_key, encrypt_key)
if not status:
return {"message": encrypt_key_aes, "status": "failed"}
msg, ok = self.backend.add(backend_encrypt_key_name, encrypt_key_aes)
if not ok:
return {"message": msg, "status": "failed"}, False
msg, ok = self.backend.add(backend_seal_key, "open")
if not ok:
return {"message": msg, "status": "failed"}, False
global secrets_encrypt_key, secrets_root_key
secrets_encrypt_key = encrypt_key
secrets_root_key = root_key
self.print_token(shares, root_token=root_key)
return {"message": "OK",
"details": {
"root_token": root_key,
"seal_tokens": shares,
}}, True
def auto_unseal(self):
if not self.trigger:
return {
"message": "trigger config is empty, skip",
"status": "skip"
}
if self.trigger.startswith("http"):
return {
"message": "todo in next step, skip",
"status": "skip"
}
# TODO
elif len(self.trigger.strip()) == 24:
res = self.auth_root_secret(self.trigger.encode(), current_app)
if res.get("status") == success:
return {
"message": success,
"status": success
}
else:
return {
"message": res.get("message"),
"status": "failed"
}
else:
return {
"message": "trigger config is invalid, skip",
"status": "skip"
}
def seal(self, root_key):
root_key = root_key.encode()
msg, ok = self.is_valid_root_key(root_key)
if not ok:
return {
"message": msg,
"status": "failed"
}
else:
msg, ok = self.backend.update(backend_seal_key, "block")
if not ok:
return {
"message": msg,
"status": "failed",
}
self.clear()
self.backend.cache.publish(self.share_key, "clear")
return {
"message": success,
"status": success
}
@staticmethod
def clear():
global secrets_encrypt_key, secrets_root_key
secrets_encrypt_key = ''
secrets_root_key = ''
def is_seal(self):
"""
If there is no initialization or the root key is inconsistent, it is considered to be in a sealed state..
:return:
"""
# secrets_root_key = current_app.config.get("secrets_root_key")
if not secrets_root_key:
return True
msg, ok = self.is_valid_root_key(secrets_root_key)
if not ok:
return True
status = self.backend.get(backend_seal_key)
return status == "block"
@classmethod
def print_token(cls, shares, root_token):
"""
data: {"message": "OK",
"details": {
"root_token": root_key,
"seal_tokens": shares,
}}
"""
colorama_init()
print(Style.BRIGHT, "Please be sure to store the Unseal Key in a secure location and avoid losing it."
" The Unseal Key is required to unseal the system every time when it restarts."
" Successful unsealing is necessary to enable the password feature." + Style.RESET_ALL)
for i, v in enumerate(shares):
print(
"unseal token " + str(i + 1) + ": " + Fore.RED + Back.BLACK + v.decode("utf-8") + Style.RESET_ALL)
print()
print(Fore.GREEN + "root token: " + root_token.decode("utf-8") + Style.RESET_ALL)
@classmethod
def print_response(cls, data):
status = data.get("status", "")
message = data.get("message", "")
status_colors = {
"skip": Style.BRIGHT,
"failed": Fore.RED,
"waiting": Fore.YELLOW,
}
print(status_colors.get(status, Fore.GREEN), message, Style.RESET_ALL)
def set_shares(self, values):
new_value = list()
for v in values:
new_value.append((v[0], b64encode(v[1]).decode("utf-8")))
self.backend.cache.publish(self.share_key, json.dumps(new_value))
self.backend.cache.set(self.share_key, json.dumps(new_value))
def watch_root_key(self, app):
pubsub = self.backend.cache.pubsub()
pubsub.subscribe(self.share_key)
new_value = set()
for message in pubsub.listen():
if message["type"] == "message":
if message["data"] == b"clear":
self.clear()
continue
try:
value = json.loads(message["data"].decode("utf-8"))
for v in value:
new_value.add((v[0], b64decode(v[1])))
except Exception as e:
return []
if len(new_value) >= global_key_threshold:
self.parse_shares(list(new_value), app)
new_value = set()
class InnerCrypt:
def __init__(self):
self.encrypt_key = b64decode(secrets_encrypt_key)
# self.encrypt_key = b64decode(secrets_encrypt_key, "".encode("utf-8"))
def encrypt(self, plaintext):
"""
encrypt method contain aes currently
"""
if not self.encrypt_key:
return ValueError("secret is disabled, please seal firstly"), False
return self.aes_encrypt(self.encrypt_key, plaintext)
def decrypt(self, ciphertext):
"""
decrypt method contain aes currently
"""
if not self.encrypt_key:
return ValueError("secret is disabled, please seal firstly"), False
return self.aes_decrypt(self.encrypt_key, ciphertext)
@classmethod
def aes_encrypt(cls, key, plaintext):
if isinstance(plaintext, str):
plaintext = string_to_bytes(plaintext)
iv = os.urandom(global_iv_length)
try:
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
encryptor = cipher.encryptor()
v_padder = padding.PKCS7(algorithms.AES.block_size).padder()
padded_plaintext = v_padder.update(plaintext) + v_padder.finalize()
ciphertext = encryptor.update(padded_plaintext) + encryptor.finalize()
return b64encode(iv + ciphertext).decode("utf-8"), True
except Exception as e:
return str(e), False
@classmethod
def aes_decrypt(cls, key, ciphertext):
try:
s = b64decode(ciphertext.encode("utf-8"))
iv = s[:global_iv_length]
ciphertext = s[global_iv_length:]
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
decrypter = cipher.decryptor()
decrypted_padded_plaintext = decrypter.update(ciphertext) + decrypter.finalize()
unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
plaintext = unpadder.update(decrypted_padded_plaintext) + unpadder.finalize()
return plaintext.decode('utf-8'), True
except Exception as e:
return str(e), False
if __name__ == "__main__":
km = KeyManage()
# info, shares, status = km.generate_unseal_keys()
# print(info, shares, status)
# print("..................")
# for i in shares:
# print(b64encode(i[1]).decode())
res1, ok1 = km.init()
if not ok1:
print(res1)
# for j in res["details"]["seal_tokens"]:
# r = km.unseal(j)
# if r["status"] != "waiting":
# if r["status"] != "success":
# print("r........", r)
# else:
# print(r)
# break
t_plaintext = b"Hello, World!" # The plaintext to encrypt
c = InnerCrypt()
t_ciphertext, status1 = c.encrypt(t_plaintext)
print("Ciphertext:", t_ciphertext)
decrypted_plaintext, status2 = c.decrypt(t_ciphertext)
print("Decrypted plaintext:", decrypted_plaintext)

View File

@ -1,63 +0,0 @@
import base64
import json
from api.models.cmdb import InnerKV
from api.extensions import rd
class InnerKVManger(object):
def __init__(self):
self.cache = rd.r
pass
@classmethod
def add(cls, key, value):
data = {"key": key, "value": value}
res = InnerKV.create(**data)
if res.key == key:
return "success", True
return "add failed", False
@classmethod
def get(cls, key):
res = InnerKV.get_by(first=True, to_dict=False, key=key)
if not res:
return None
return res.value
@classmethod
def update(cls, key, value):
res = InnerKV.get_by(first=True, to_dict=False, key=key)
if not res:
return cls.add(key, value)
t = res.update(value=value)
if t.key == key:
return "success", True
return "update failed", True
@classmethod
def get_shares(cls, key):
new_value = list()
v = rd.get_str(key)
if not v:
return new_value
try:
value = json.loads(v.decode("utf-8"))
for v in value:
new_value.append((v[0], base64.b64decode(v[1])))
except Exception as e:
return []
return new_value
@classmethod
def set_shares(cls, key, value):
new_value = list()
for v in value:
new_value.append((v[0], base64.b64encode(v[1]).decode("utf-8")))
rd.set_str(key, json.dumps(new_value))

View File

@ -1,141 +0,0 @@
from base64 import b64decode
from base64 import b64encode
import hvac
class VaultClient:
def __init__(self, base_url, token, mount_path='cmdb'):
self.client = hvac.Client(url=base_url, token=token)
self.mount_path = mount_path
def create_app_role(self, role_name, policies):
resp = self.client.create_approle(role_name, policies=policies)
return resp == 200
def delete_app_role(self, role_name):
resp = self.client.delete_approle(role_name)
return resp == 204
def update_app_role_policies(self, role_name, policies):
resp = self.client.update_approle_role(role_name, policies=policies)
return resp == 204
def get_app_role(self, role_name):
resp = self.client.get_approle(role_name)
resp.json()
if resp.status_code == 200:
return resp.json
else:
return {}
def enable_secrets_engine(self):
resp = self.client.sys.enable_secrets_engine('kv', path=self.mount_path)
resp_01 = self.client.sys.enable_secrets_engine('transit')
if resp.status_code == 200 and resp_01.status_code == 200:
return resp.json
else:
return {}
def encrypt(self, plaintext):
response = self.client.secrets.transit.encrypt_data(name='transit-key', plaintext=plaintext)
ciphertext = response['data']['ciphertext']
return ciphertext
# decrypt data
def decrypt(self, ciphertext):
response = self.client.secrets.transit.decrypt_data(name='transit-key', ciphertext=ciphertext)
plaintext = response['data']['plaintext']
return plaintext
def write(self, path, data, encrypt=None):
if encrypt:
for k, v in data.items():
data[k] = self.encrypt(self.encode_base64(v))
response = self.client.secrets.kv.v2.create_or_update_secret(
path=path,
secret=data,
mount_point=self.mount_path
)
return response
# read data
def read(self, path, decrypt=True):
try:
response = self.client.secrets.kv.v2.read_secret_version(
path=path, raise_on_deleted_version=False, mount_point=self.mount_path
)
except Exception as e:
return str(e), False
data = response['data']['data']
if decrypt:
try:
for k, v in data.items():
data[k] = self.decode_base64(self.decrypt(v))
except:
return data, True
return data, True
# update data
def update(self, path, data, overwrite=True, encrypt=True):
if encrypt:
for k, v in data.items():
data[k] = self.encrypt(self.encode_base64(v))
if overwrite:
response = self.client.secrets.kv.v2.create_or_update_secret(
path=path,
secret=data,
mount_point=self.mount_path
)
else:
response = self.client.secrets.kv.v2.patch(path=path, secret=data, mount_point=self.mount_path)
return response
# delete data
def delete(self, path):
response = self.client.secrets.kv.v2.delete_metadata_and_all_versions(
path=path,
mount_point=self.mount_path
)
return response
# Base64 encode
@classmethod
def encode_base64(cls, data):
encoded_bytes = b64encode(data.encode())
encoded_string = encoded_bytes.decode()
return encoded_string
# Base64 decode
@classmethod
def decode_base64(cls, encoded_string):
decoded_bytes = b64decode(encoded_string)
decoded_string = decoded_bytes.decode()
return decoded_string
if __name__ == "__main__":
_base_url = "http://localhost:8200"
_token = "your token"
_path = "test001"
# Example
sdk = VaultClient(_base_url, _token)
# sdk.enable_secrets_engine()
_data = {"key1": "value1", "key2": "value2", "key3": "value3"}
_data = sdk.update(_path, _data, overwrite=True, encrypt=True)
print(_data)
_data = sdk.read(_path, decrypt=True)
print(_data)

View File

@ -1,6 +1,9 @@
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
import base64 import base64
import sys
import time
from typing import Set
import elasticsearch import elasticsearch
import redis import redis
@ -9,9 +12,6 @@ from Crypto.Cipher import AES
from elasticsearch import Elasticsearch from elasticsearch import Elasticsearch
from flask import current_app from flask import current_app
from api.lib.secrets.inner import InnerCrypt
from api.lib.secrets.inner import KeyManage
class BaseEnum(object): class BaseEnum(object):
_ALL_ = set() # type: Set[str] _ALL_ = set() # type: Set[str]
@ -116,23 +116,6 @@ class RedisHandler(object):
except Exception as e: except Exception as e:
current_app.logger.error("delete redis key error, {0}".format(str(e))) current_app.logger.error("delete redis key error, {0}".format(str(e)))
def set_str(self, key, value, expired=None):
try:
if expired:
self.r.setex(key, expired, value)
else:
self.r.set(key, value)
except Exception as e:
current_app.logger.error("set redis error, {0}".format(str(e)))
def get_str(self, key):
try:
value = self.r.get(key)
except Exception as e:
current_app.logger.error("get redis error, {0}".format(str(e)))
return
return value
class ESHandler(object): class ESHandler(object):
def __init__(self, flask_app=None): def __init__(self, flask_app=None):
@ -227,6 +210,52 @@ class ESHandler(object):
return 0, [], {} return 0, [], {}
class Lock(object):
def __init__(self, name, timeout=10, app=None, need_lock=True):
self.lock_key = name
self.need_lock = need_lock
self.timeout = timeout
if not app:
app = current_app
self.app = app
try:
self.redis = redis.Redis(host=self.app.config.get('CACHE_REDIS_HOST'),
port=self.app.config.get('CACHE_REDIS_PORT'),
password=self.app.config.get('CACHE_REDIS_PASSWORD'))
except:
self.app.logger.error("cannot connect redis")
raise Exception("cannot connect redis")
def lock(self, timeout=None):
if not timeout:
timeout = self.timeout
retry = 0
while retry < 100:
timestamp = time.time() + timeout + 1
_lock = self.redis.setnx(self.lock_key, timestamp)
if _lock == 1 or (
time.time() > float(self.redis.get(self.lock_key) or sys.maxsize) and
time.time() > float(self.redis.getset(self.lock_key, timestamp) or sys.maxsize)):
break
else:
retry += 1
time.sleep(0.6)
if retry >= 100:
raise Exception("get lock failed...")
def release(self):
if time.time() < float(self.redis.get(self.lock_key)):
self.redis.delete(self.lock_key)
def __enter__(self):
if self.need_lock:
self.lock()
def __exit__(self, exc_type, exc_val, exc_tb):
if self.need_lock:
self.release()
class AESCrypto(object): class AESCrypto(object):
BLOCK_SIZE = 16 # Bytes BLOCK_SIZE = 16 # Bytes
pad = lambda s: s + ((AESCrypto.BLOCK_SIZE - len(s) % AESCrypto.BLOCK_SIZE) * pad = lambda s: s + ((AESCrypto.BLOCK_SIZE - len(s) % AESCrypto.BLOCK_SIZE) *
@ -257,33 +286,3 @@ class AESCrypto(object):
text_decrypted = cipher.decrypt(encode_bytes) text_decrypted = cipher.decrypt(encode_bytes)
return cls.unpad(text_decrypted).decode('utf8') return cls.unpad(text_decrypted).decode('utf8')
class Crypto(AESCrypto):
@classmethod
def encrypt(cls, data):
from api.lib.secrets.secrets import InnerKVManger
if not KeyManage(backend=InnerKVManger()).is_seal():
res, status = InnerCrypt().encrypt(data)
if status:
return res
return AESCrypto().encrypt(data)
@classmethod
def decrypt(cls, data):
from api.lib.secrets.secrets import InnerKVManger
if not KeyManage(backend=InnerKVManger()).is_seal():
try:
res, status = InnerCrypt().decrypt(data)
if status:
return res
except:
pass
try:
return AESCrypto().decrypt(data)
except:
return data

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