Compare commits

..

995 Commits

Author SHA1 Message Date
pycook 47332aca3c feat(api): Replace imp with importlib 2025-02-23 22:08:38 +08:00
LH_R f24cb55585 docs: add CODE_OF_CONDUCT 2025-02-13 16:48:07 +08:00
LH_R f1594550e0 docs: update README 2025-02-13 16:44:57 +08:00
pycook a025c844bc chore: release v2.5.1 2025-02-10 20:22:17 +08:00
pycook 1a03a0b800 fix(api): get citype 2025-02-10 20:20:22 +08:00
songlh e35efea712 feat(ui): login page - update background image 2025-01-22 16:39:13 +08:00
songlh 3f1e8beae8 docs: update README 2025-01-21 11:27:45 +08:00
songlh 3ea81987a1 docs: update README 2025-01-16 17:39:43 +08:00
pycook 6a20e2f578 fix(api): auto discovery rules sync 2025-01-14 16:24:20 +08:00
pycook f6de9b42ab chore: release v2.4.17 2024-12-26 15:30:08 +08:00
Leo Song 25f6bbcc3e
Merge pull request #664 from veops/dev_ui_241226
feat(ui): update relative views menu display
2024-12-26 13:59:26 +08:00
songlh bc3201656c feat(ui): update relative views menu display 2024-12-26 13:58:53 +08:00
pycook 89db5a060e fix(api): search for attr filter 2024-12-26 13:57:05 +08:00
Leo Song b669775cd6
Merge pull request #663 from veops/dev_ui_241224
feat(ui): update style
2024-12-24 15:23:33 +08:00
songlh 655b642930 feat(ui): update style 2024-12-24 15:22:36 +08:00
pycook b253fdfea0 fix(api): multi-line search 2024-12-23 16:52:06 +08:00
pycook d0129439cd fix(api): update CI for unique identifier 2024-12-23 16:36:45 +08:00
pycook aaa4ca1327 fix(api): date trigger 2024-12-23 16:32:27 +08:00
thexqn 747475b6a6
Fix bug 1217 (#656)
* fix(api): move soft delete of PreferenceShowAttributes to correct location

* chore: add manage.sh to .gitignore

* fix(api): correct type_id reference in CITypeAttributeManager for PreferenceShowAttributes soft delete

* Update .gitignore

---------

Co-authored-by: pycook <pycook@126.com>
2024-12-20 16:47:25 +08:00
thexqn ada23262bb
feat(search): implement column search mode and enhance search input functionality (#658)
* chore: update .gitignore to include manage.sh and .env files

* feat(api): add new SQL query for CI by no attribute in

* feat(api): enhance search functionality with new IN clause support for queries

* feat(lang): add new search tips and modes in English and Chinese language files

* feat(search): implement column search mode and enhance search input functionality
2024-12-18 17:21:24 +08:00
simontigers 0c57b2b83d
Merge pull request #660 from veops/config_ruff
fix: ci_type find_path _graph
2024-12-18 16:42:31 +08:00
simontigers 082724e7bd
fix: ci_type find_path _graph 2024-12-18 16:42:03 +08:00
simontigers d782ceddab
Merge pull request #659 from veops/config_ruff
fix: code linter
2024-12-18 14:35:56 +08:00
simontigers 510ea5dc2d fix: code linter 2024-12-18 06:24:38 +00:00
Leo Song 41ce5db1c7
Merge pull request #657 from veops/dev_ui_241217
fix(ui): ci - number type attr default value display error
2024-12-17 15:13:18 +08:00
songlh c3aab86844 fix(ui): ci - number type attr default value display error 2024-12-17 15:12:44 +08:00
simontigers d1e40b4e5e
Merge pull request #654 from veops/fix_acl_date_joined_column
fix: acl user date_joined timezone
2024-12-16 13:58:46 +08:00
simontigers ea4ea9d6b6
fix: acl user date_joined timezone 2024-12-16 13:58:20 +08:00
pycook 1c5d2c8e9e fix(api): date trigger 2024-12-13 16:57:24 +08:00
Leo Song 6bd3de8951
Merge pull request #652 from veops/dev_ui_241211
feat: update style
2024-12-11 15:48:23 +08:00
songlh a0ff3d69cb feat: update style 2024-12-11 15:47:45 +08:00
pycook fccf5db886 fix(ui): restore ui .env 2024-12-09 20:21:52 +08:00
pycook 95b55d2963 feat(api): set the default expire for redis lock 2024-12-09 19:50:04 +08:00
pycook 47ebe55291 chore: release v2.4.16 2024-12-06 16:52:13 +08:00
Leo Song f65c5a8c56
Merge pull request #651 from veops/dev_ui_241206
feat(ui): ipam - update address table display
2024-12-06 16:04:26 +08:00
songlh a6f0791852 feat(ui): ipam - update address table display 2024-12-06 16:03:11 +08:00
Leo Song 05181dbed3
Merge pull request #650 from veops/dev_ui_241206
feat(ui): ipam - update assign form field display
2024-12-06 15:54:02 +08:00
songlh f5dc53e5b8 feat(ui): ipam - update assign form field display 2024-12-06 15:53:19 +08:00
pycook 40b452cd4b Merge branch 'master' of github.com:veops/cmdb 2024-12-06 15:16:50 +08:00
pycook d322761ef7 feat(api): In the trigger, the date attribute can be used to test sending notifications 2024-12-06 15:16:35 +08:00
Leo Song ba7b78af63
Merge pull request #649 from veops/dev_ui_241206
feat(ui): ci relation update reference attr display
2024-12-06 13:37:39 +08:00
songlh f575291a1f feat(ui): ci relation update reference attr display 2024-12-06 13:36:12 +08:00
Leo Song 183b8c1f7d
Merge pull request #647 from veops/dev_ui_241205
Dev UI 241205
2024-12-05 14:37:57 +08:00
songlh 12743c20fe feat(ui): ipam - address table add view relation 2024-12-05 14:36:56 +08:00
songlh b7c3cce83b feat(ui): add trigger test send 2024-12-05 14:33:23 +08:00
pycook 191ce95717 chore: update readme 2024-12-03 18:45:41 +08:00
pycook 9f7a91ab26 feat(api): calc all racks free u count 2024-11-28 19:28:22 +08:00
pycook 9ffa9c943d Merge branch 'master' of github.com:veops/cmdb 2024-11-28 19:22:55 +08:00
pycook 9a2229ab10 fix(api): ci relation search 2024-11-28 19:22:26 +08:00
Leo Song d4518002f9 Merge pull request #646 from veops/dev_ui_241128
feat(ui): dcim - add calc free unit count btn
2024-11-28 18:24:59 +08:00
songlh 584215b569 feat(ui): dcim - add calc free unit count btn 2024-11-28 18:24:18 +08:00
pycook 52b0a41d16 fix(api): check rack u slot 2024-11-27 15:39:53 +08:00
pycook 8a16badf25 chore: release v2.4.15 2024-11-27 15:14:58 +08:00
Leo Song 1dc006426b Merge pull request #643 from veops/dev_ui_dcim
Dev UI dcim
2024-11-27 11:15:15 +08:00
songlh 1a2929c44d feat(ui): dcim - update rack list filter 2024-11-27 11:14:21 +08:00
songlh ccc45bec0f feat(ui): add dcim 2024-11-27 10:26:05 +08:00
pycook aa3beefe75 feat(api): dcim dev (#642) 2024-11-26 18:56:59 +08:00
pycook c4997458f4 feat(api): update ipam 2024-11-25 20:19:01 +08:00
Leo Song c70153c3a7 Merge pull request #639 from veops/dev_ui_ipam
feat(ui): ipam - add batch assign
2024-11-13 10:04:53 +08:00
songlh 6532a937bf feat(ui): ipam - add batch assign 2024-11-13 10:03:13 +08:00
Leo Song 7cb6bcae1e Merge pull request #638 from veops/dev_ui_ipam
fix(ui): ipam - filter search value error
2024-11-12 10:59:04 +08:00
songlh 6b16d393c7 fix(ui): ipam - filter search value error 2024-11-12 10:58:09 +08:00
pycook c03dc851a6 chore: release v2.4.14 2024-11-11 19:02:05 +08:00
pycook f7e748701d fix(api): ipam assign address 2024-11-11 18:56:09 +08:00
pycook 1bf8588984 Dev api ipam (#637)
* feat: ipam api

* fix: ipam
2024-11-11 18:17:37 +08:00
Leo Song 54d645b711 Merge pull request #636 from veops/dev_ui_ipam
feat(ui): add ipam
2024-11-11 16:50:35 +08:00
songlh c50133b3e4 feat(ui): add ipam 2024-11-11 16:49:53 +08:00
thexqn cce88bb4b0 fix(search): correct type_id usage in CI relation filtering (#633) 2024-11-11 15:53:24 +08:00
dagongren 1138783267 feat:add employee work_region (#634)
* feat:add employee work_region

* env
2024-11-07 11:52:55 +08:00
Zhuohao Li 48951ecd0a fix permission bug (#632)
不同的appid下可能有相同的resource type name.
2024-10-27 14:04:34 +08:00
pycook ce8ac744d6 feat(api): add builtin attributes (#631) 2024-10-22 18:21:07 +08:00
Leo Song c790aa3ab4 Merge pull request #630 from veops/dev_ui_241022
fix(ui): update userPanel style
2024-10-22 14:12:14 +08:00
songlh 2d09bd9c13 fix(ui): update userPanel style 2024-10-22 14:11:36 +08:00
Leo Song 2f3b5efea7 Merge pull request #629 from veops/dev_ui_241022
feat(ui): add userPanel component
2024-10-22 14:01:02 +08:00
songlh e6be756e42 feat(ui): add userPanel component 2024-10-22 13:59:38 +08:00
pycook 00ceee3408 feat(api): save relation search option 2024-10-18 11:03:31 +08:00
pycook 9afdec9ba3 chore: release v2.4.13 2024-10-18 09:52:26 +08:00
pycook 75c31d4256 Merge pull request #628 from veops/dev_api_relation_path_search
feat(api): relation path search
2024-10-17 19:47:34 +08:00
pycook a09336f00b feat(api): relation path search 2024-10-17 19:46:39 +08:00
Leo Song dbbff56395 Merge pull request #627 from veops/dev_ui_241017
feat(ui): add relation search
2024-10-17 17:56:13 +08:00
songlh 0ce42334f2 feat(ui): add relation search 2024-10-17 17:55:36 +08:00
pycook b967de2d10 Merge pull request #623 from veops/dev_api_relation_path_search
Dev api relation path search
2024-09-30 17:33:45 +08:00
pycook e369a55333 feat(api): add api /ci_type_relations/path 2024-09-26 20:32:21 +08:00
pycook 4b5d43de57 Merge pull request #622 from novohool/master
Update cache support for environment variables in settings.example.py
2024-09-26 18:09:54 +08:00
pycook bbcc0f986e feat(api): add relation path search 2024-09-26 17:59:08 +08:00
novohool 06e2924256 Update settings.example.py 2024-09-26 17:00:51 +08:00
pycook c986cfc6a6 fix(api): change records of attribute values for date and datetime 2024-09-25 19:37:08 +08:00
pycook 310bb6ea39 fix(api): search for multiple CIType 2024-09-24 17:46:27 +08:00
pycook 626aa7b094 fix(api): ci relations search 2024-09-23 19:46:43 +08:00
Leo Song 03b1139bb3 Merge pull request #619 from veops/dev_ui_240920
feat: update computed attr tip
2024-09-20 15:36:55 +08:00
songlh 4b300e772d feat: update computed attr tip 2024-09-20 15:36:19 +08:00
Leo Song 3beea17770 Merge pull request #617 from veops/dev_ui_240914
dev_ui_240914
2024-09-14 17:28:42 +08:00
songlh 26f0a65ae4 fix(ui): operation history search expand error 2024-09-14 17:27:57 +08:00
songlh 6ccf5b261d fix(ui): employeeTreeSelect display error 2024-09-14 17:26:33 +08:00
pycook afda71f135 Merge pull request #616 from thexqn/optimize_history
feat: Add show_attr value column to operation history table
2024-09-14 11:55:01 +08:00
thexqn 5ccbfec178 优化CITypeCache的调用方式 2024-09-14 11:30:45 +08:00
thexqn 179463e733 feat(cmdb): 添加操作历史表的唯一值列 (Add unique value column to operation history table) 2024-09-14 01:13:07 +08:00
thexqn d0779d17fa feat: Add unique value column to operation history table 2024-09-13 23:44:40 +08:00
Leo Song 12c7b564cd Merge pull request #615 from veops/dev_ui_240913
feat(ui): add employeeTreeSelect otherOptions prop
2024-09-13 18:36:48 +08:00
songlh 9a3897838d feat(ui): add employeeTreeSelect otherOptions prop 2024-09-13 18:36:24 +08:00
pycook 766609ad89 fix(api): remote ip for login log 2024-09-10 11:41:35 +08:00
pycook 70bdd8f151 feat(api): acl supports channel 2024-09-09 15:28:20 +08:00
Leo Song 4e363176fe Merge pull request #613 from veops/dev_ui_240909
feat(ui): add SplitPane calcBasedParent prop
2024-09-09 10:45:27 +08:00
songlh ef8ddeebe7 feat(ui): add SplitPane calcBasedParent prop 2024-09-09 10:44:58 +08:00
Leo Song 478e9519e8 Merge pull request #611 from veops/dev_ui_240903
feat: update icon select
2024-09-03 16:41:18 +08:00
songlh 5487d07b53 feat: update icon select 2024-09-03 16:40:46 +08:00
pycook ade4fc72e3 chore: release v2.4.12 2024-09-03 14:18:53 +08:00
Leo Song 13ed4671b0 Merge pull request #610 from veops/dev_ui_240903
fix(ui): build error
2024-09-03 13:15:43 +08:00
songlh c3b7303a08 fix(ui): build error 2024-09-03 13:14:56 +08:00
Leo Song d3080bad3c Merge pull request #609 from veops/dev_ui_240903
feat: update resource search
2024-09-03 11:30:08 +08:00
songlh 1f3df6921d feat: update resource search 2024-09-03 11:29:32 +08:00
pycook 6ccc010d08 perf(api): resource search supports recent searches and my favorites 2024-09-02 16:56:06 +08:00
Leo Song 69bde5ee29 Merge pull request #607 from veops/dev_ui_240828
fix(ui): ci choice attr error
2024-08-28 18:52:43 +08:00
songlh 52eef2d315 fix(ui): ci choice attr error 2024-08-28 18:52:20 +08:00
pycook 9bf621dc2c fix(api): CIType templates import 2024-08-28 17:52:15 +08:00
Leo Song ad91d208b8 Merge pull request #606 from veops/dev_ui_240828
feat(ui): update ui
2024-08-28 16:55:45 +08:00
songlh 6d404c2e3e feat(ui): update ui 2024-08-28 16:55:07 +08:00
Leo Song 17d75fb329 Merge pull request #605 from veops/dev_ui_240827
Dev UI 240827
2024-08-27 10:33:30 +08:00
songlh 97311b2b51 fix(ui): resource search export error 2024-08-27 10:32:53 +08:00
songlh f3b0efabb4 fix(ui): update create attr icon 2024-08-27 10:32:25 +08:00
pycook 7b65ab325e fix(api): custom dashboard for enum type 2024-08-26 22:31:58 +08:00
Leo Song 5d3221b93a Merge pull request #604 from veops/dev_ui_240826
feat: export remove reference attr
2024-08-26 22:22:43 +08:00
LH_R 785c63e397 feat: export remove reference attr 2024-08-26 22:21:25 +08:00
Leo Song 9244eea71b Merge pull request #603 from veops/dev_ui_240826
fix(ui): define value filter error
2024-08-26 21:40:03 +08:00
LH_R aaa3a1e829 fix(ui): define value filter error 2024-08-26 21:38:02 +08:00
pycook 61cf798a3a Merge branch 'master' of github.com:veops/cmdb 2024-08-26 19:50:44 +08:00
pycook 40d1a53537 fix(api): custom dashboard 2024-08-26 19:50:22 +08:00
Leo Song 5bc294e405 Merge pull request #602 from veops/dev_ui_240826
fix(ui): menu icon display
2024-08-26 19:49:46 +08:00
songlh 2717f65280 fix(ui): menu icon display 2024-08-26 19:47:23 +08:00
pycook f166824efb chore: release v2.4.11 2024-08-26 18:44:23 +08:00
Leo Song e235224e3c Merge pull request #601 from veops/dev_ui_240826
fix(ui): some bugs
2024-08-26 18:35:44 +08:00
songlh b417d98469 fix(ui): some bugs 2024-08-26 18:34:42 +08:00
Leo Song 65ecd827ba Merge pull request #600 from veops/dev_ui_240826
fix(ui): update builtIn params
2024-08-26 16:03:37 +08:00
songlh dec7435e9b fix(ui): update builtIn params 2024-08-26 16:02:05 +08:00
Leo Song ffcb533957 Merge pull request #599 from veops/dev_ui_240826
feat(ui) update CMDBFilterComp label
2024-08-26 15:16:07 +08:00
songlh 3c1a2fe3e4 feat(ui) update CMDBFilterComp label 2024-08-26 15:14:52 +08:00
Leo Song 24d9f3758e Merge pull request #598 from veops/dev_ui_240826
Dev UI 240826
2024-08-26 15:09:01 +08:00
songlh d18c1fba6e fix(ui): create ad plugin params 2024-08-26 15:08:19 +08:00
songlh 9729725c6a feat(ui): update ci type choice config 2024-08-26 15:05:11 +08:00
pycook e0c255ffa9 fix(api): in query 2024-08-26 13:29:03 +08:00
pycook b4241c92a0 Merge pull request #597 from veops/dev_api_0826
feat(api): enum supports
2024-08-26 12:15:05 +08:00
pycook b75143108f feat(api): enum supports 2024-08-26 12:14:14 +08:00
Leo Song 6ae4707323 Merge pull request #596 from thexqn/fix_order_bug
修复在继承模型的情况下,非继承属性与继承属性的排序以及其他分组的排序提示问题Fix order bug
2024-08-26 11:20:15 +08:00
thexqn 0a087df03f 清理多余的router-view 2024-08-23 16:55:21 +08:00
thexqn 0679fb96e9 feat: 修复在继承模型的情况下,非继承属性与继承属性的排序以及其他分组的排序的问题 2024-08-23 16:50:37 +08:00
thexqn 2a38401f5a feat: 修复在继承模型的情况下,非继承属性与继承属性的排序以及其他分组的排序的问题 2024-08-23 16:29:29 +08:00
thexqn 1e5ca4ff43 Merge branch 'veops:master' into master 2024-08-23 14:56:12 +08:00
Leo Song a23d091667 Merge pull request #594 from veops/dev_ui_240820
feat(ui): add bool and reference type
2024-08-20 15:31:45 +08:00
songlh 09b10eec45 feat(ui): add bool and reference type 2024-08-20 15:31:11 +08:00
pycook 23c3ac44bf Merge pull request #593 from veops/dev_api_0820
feat(api): supports bool and reference
2024-08-20 13:51:44 +08:00
pycook e893ea1b19 feat(api): supports bool and reference 2024-08-20 13:49:51 +08:00
kinyXu d80c1d7ad2 feat: add attribute sorted tips for non-inherited attributes 2024-08-20 11:48:44 +08:00
Leo Song 785b4d4a7d Merge pull request #591 from veops/dev_ui_240813
refactor(ui): ci table
2024-08-13 17:15:15 +08:00
songlh 0c1b017266 refactor(ui): ci table 2024-08-13 17:14:05 +08:00
pycook a77aefb436 Merge pull request #590 from lgphone/patch-1
bugfix: cmdb-api  auto_discovery add unique_value param
2024-08-07 16:05:56 +08:00
YangEver 6ad3d167e1 bugfix: cmdb-api auto_discovery add unique_value param
自动发现接口需要根据unique_value参数进行数据唯一性校验,此参数为必填项
2024-08-07 15:50:42 +08:00
Leo Song 5c8f050585 Merge pull request #589 from veops/dev_ui_240807
Dev UI 240807
2024-08-07 14:42:01 +08:00
songlh 41d7cdf4d2 fix(ui): dashboard chart config 2024-08-07 14:41:22 +08:00
songlh 95506e6f4e feat(ui): update common settings btn 2024-08-07 14:40:54 +08:00
pycook f280603d00 fix(api): Dashboard using display attributes 2024-08-06 19:59:16 +08:00
pycook 72beec08d7 chore: release v2.4.10 2024-07-31 16:42:26 +08:00
Leo Song c08e4529de Merge pull request #588 from veops/dev_ui_240731
feat(ui): update ci type
2024-07-31 16:01:17 +08:00
songlh b9c701bfb0 feat(ui): update ci type 2024-07-31 16:00:40 +08:00
pycook c0c84de600 fix(api): delete item for multi-value attributes 2024-07-30 20:05:21 +08:00
pycook 126eb03550 Merge pull request #587 from thexqn/master
修复在用了计算属性的情况下,批量上传功能可能出现的错误.
2024-07-30 09:18:30 +08:00
thexqn deea300620 修复在用了计算属性的情况下,批量上传功能可能出现的错误. 2024-07-30 01:17:53 +08:00
pycook ad1bb86daf feat(api): Multi-valued attribute values ​​support adding and deleting 2024-07-29 19:55:07 +08:00
pycook 5d3fe652b0 chore: release v2.4.9 2024-07-26 17:03:00 +08:00
Leo Song 9ace26cd04 Merge pull request #585 from veops/fix_ui_240726
fix: discovery card eye btn
2024-07-26 16:50:43 +08:00
songlh 021b53dad4 fix: discovery card eye btn 2024-07-26 16:49:28 +08:00
Leo Song 3720008c2f Merge pull request #584 from veops/dev_ui_240726
feat(ui): update auto discovery
2024-07-26 10:41:19 +08:00
songlh e4c3a4bee1 feat(ui): update auto discovery 2024-07-26 10:40:37 +08:00
pycook 1336a24044 perf(api): auto discovery (#582) 2024-07-25 17:45:26 +08:00
Leo Song 16c4a08b74 Merge pull request #581 from veops/dev_ui_240722
feat: add accounts config
2024-07-22 17:39:18 +08:00
songlh 88def811ec feat: add accounts config 2024-07-22 17:38:48 +08:00
Leo Song fa9bd5a926 Merge pull request #580 from veops/dev_ui_240716
feat: add history export
2024-07-16 13:46:40 +08:00
songlh 578b26737a feat: add history export 2024-07-16 13:45:31 +08:00
ivonGwy 00d468efc6 change pic 2024-07-15 16:23:34 +08:00
ivonGwy 2b30bd4486 change pic 2024-07-15 16:22:20 +08:00
pycook caf7642863 chore: update docker compose 2024-07-11 14:29:34 +08:00
pycook c748352d82 chore: release v2.4.8 2024-07-10 19:43:01 +08:00
Leo Song 6954c3bd7e Merge pull request #578 from veops/dev_ui_24071002
feat(ui): update
2024-07-10 19:19:08 +08:00
songlh 5cbcbaf93d feat(ui): update 2024-07-10 19:18:22 +08:00
pycook 82ceb75d5b fix(api): computed attributes for multi values (#577) 2024-07-10 19:18:03 +08:00
Leo Song ba1064495b Merge pull request #575 from veops/dev_ui_240710
fix: topoview search error
2024-07-10 10:12:11 +08:00
songlh 3b7c1adfb4 fix: topoview search error 2024-07-10 10:11:40 +08:00
Leo Song 4063b148a1 Merge pull request #574 from veops/dev_ui_240709
feat: update auto discovery
2024-07-09 09:45:25 +08:00
songlh 4e441ee7a9 feat: update auto discovery 2024-07-09 09:44:28 +08:00
pycook 341fb410f2 fix(api): auto discovery update
fix(api): auto discovery update
2024-07-08 18:03:21 +08:00
pycook fff6da943d perf(api): relationships built by attribute values (#572) 2024-07-08 11:42:18 +08:00
Leo Song 53dcf5d0d6 Merge pull request #571 from veops/dev_ui_240703
feat: add http attr mapping
2024-07-03 18:49:23 +08:00
songlh 3e9ae3e73a feat: add http attr mapping 2024-07-03 18:47:55 +08:00
pycook 39145989c3 fix(api): auto discovery configuration save password
fix(api): auto discovery configuration save password
2024-07-02 21:32:30 +08:00
pycook 3ad8378eab feat(api): auto discovery supports mapping (#569) 2024-07-02 20:19:50 +08:00
Leo Song d00544d92f Merge pull request #568 from veops/dev_ui_240628
dev_ui_240628
2024-06-28 17:43:30 +08:00
songlh 8343d4eee3 feat: update model export 2024-06-28 17:42:20 +08:00
songlh ecfc3e073d fix(ui): load ci type error 2024-06-28 17:42:10 +08:00
pycook 23ee5e75b9 chore: update docker compose 2024-06-27 21:33:19 +08:00
pycook bd61775048 chore: release v2.4.7 2024-06-27 21:30:02 +08:00
pycook 86afad1f68 perf(api): CIType templates download (#567) 2024-06-27 20:54:38 +08:00
Leo Song d017e5083a Merge pull request #566 from veops/dev_ui_0627
feat: update model config
2024-06-27 19:41:58 +08:00
songlh e6f2aadc13 feat: update model config 2024-06-27 19:41:24 +08:00
Leo Song 72c0fbcd11 Merge pull request #565 from veops/dev_ui_0625
fix(ui): auto discovery
2024-06-25 17:36:00 +08:00
songlh ca352e984b fix(ui): auto discovery 2024-06-25 17:35:29 +08:00
pycook 26266b2d6d chore: update ui dockerfile 2024-06-24 21:40:38 +08:00
Jared Tan 9a49636f63 chore: fix UI docker build and makes UI/API docker build parallel execution (#563)
* try fix UI docker build

* parallel execution

* polish

* polish

* polish

* update
2024-06-24 21:00:57 +08:00
Leo Song 20f1e82ffa Merge pull request #564 from veops/dev_ui_ad_0624
feat: update ad ui
2024-06-24 14:26:33 +08:00
songlh 244df5fa88 feat: update ad ui 2024-06-24 14:25:56 +08:00
pycook 4c652959d5 fix(api): commands cmdb-patch 2024-06-21 18:22:56 +08:00
pycook 4a42d26eef fix(api): auto discovery permission 2024-06-21 12:47:12 +08:00
Leo Song 3401fb6ac3 Merge pull request #562 from veops/fix_ui_2.4.6
fix(ui): some bugs
2024-06-21 11:49:53 +08:00
songlh 7c7b107180 fix(ui): some bugs 2024-06-21 11:49:12 +08:00
simontigers f1cb8569a2 Merge pull request #561 from veops/dev_common_perm
fix(api): auto_discovery add new perms
2024-06-21 10:25:35 +08:00
simontigers dd611a1523 fix(api): auto_discovery add new perms 2024-06-21 10:25:13 +08:00
Leo Song 6b3b00c9ea Merge pull request #560 from veops/fix_ui_topology
fix(ui): topology view error
2024-06-20 22:21:27 +08:00
LH_R a59e8b74ea fix(ui): topology view error 2024-06-20 22:20:25 +08:00
pycook 41131e82ce chore: release v2.4.6 2024-06-20 20:31:10 +08:00
pycook 5e0e64861f perf(api): auto discovery has been upgraded (#559) 2024-06-20 20:30:04 +08:00
Leo Song c5761fc805 Merge pull request #558 from veops/dev_ui_ad
fix: build error
2024-06-20 20:03:40 +08:00
LH_R e7f02817bf fix: build error 2024-06-20 19:54:15 +08:00
Leo Song 23f1edcc65 Merge pull request #557 from veops/dev_ui_ad
feat(ui): auto discovery
2024-06-20 17:29:06 +08:00
songlh b379ff74c0 feat(ui): auto discovery 2024-06-20 17:28:09 +08:00
pycook cbc58d4a4c chore: update Dockerfile-UI 2024-06-20 13:20:42 +08:00
pycook a4be8a77aa chore: update Dockerfile-UI 2024-06-20 11:07:57 +08:00
pycook 3a1920da32 chore: update Dockerfile-UI 2024-06-20 09:47:56 +08:00
Leo Song b5b8f899fe Merge pull request #555 from veops/fix_ui_lint
fix(ui): lint error
2024-06-18 11:42:53 +08:00
songlh 87d65a35bd fix(ui): lint error 2024-06-18 11:42:22 +08:00
Jared Tan 819cd2884b polish ci and remove es build (#553) 2024-06-18 10:31:33 +08:00
pycook 24e2f3fde4 feat(api): add table c_ad_ci_type_relations 2024-06-18 10:22:04 +08:00
Jared Tan ed23d6d930 add workflow (#552) 2024-06-18 09:29:00 +08:00
Leo Song f81cae18ee Merge pull request #551 from veops/fix_bug_538
fix: issue #538
2024-06-17 14:41:51 +08:00
songlh 81d25dbaed fix: issue #538 2024-06-17 14:41:24 +08:00
Leo Song fb1a2dd151 Merge pull request #550 from veops/fix_bug_operation_history
fix: operation history table
2024-06-14 17:27:49 +08:00
songlh 7908fd1b96 fix: operation history table 2024-06-14 17:27:13 +08:00
Leo Song 062c729d98 Merge pull request #542 from veops/fix_issue_540
fix: issue #540
2024-06-12 15:00:08 +08:00
songlh 0b683478cd fix: issue #540 2024-06-12 14:59:14 +08:00
Leo Song 8137a599d1 Merge pull request #539 from veops/fix_computed_code
fix: computed code area tab
2024-06-11 15:03:20 +08:00
songlh 3398fdf499 fix: computed code area tab 2024-06-11 15:02:37 +08:00
pycook ebe460934d chore(docker compose): add api health check 2024-06-09 20:58:27 +08:00
pycook 33602dcff5 feat(ui): update iconfont 2024-06-07 10:41:26 +08:00
pycook 5327fc3445 Dev dynamic attribute (#535)
* feat: dynamic attribute

* feat(api): dynamic attribute
2024-06-07 10:39:40 +08:00
pycook fa737e75c3 feat: dynamic attribute (#534) 2024-06-07 10:29:32 +08:00
Leo Song 1a774490ac Merge pull request #532 from veops/fix_bug_530
fix: ci topo expand error
2024-06-06 14:06:26 +08:00
songlh 5033c539de fix: ci topo expand error 2024-06-06 14:05:32 +08:00
Leo Song 104d163db8 Merge pull request #531 from veops/dev_ui_240606
feat: update topology view
2024-06-06 11:10:35 +08:00
songlh b29f498748 feat: update topology view 2024-06-06 11:08:58 +08:00
simontigers 5358fb41b2 Merge pull request #529 from veops/fix_decorator_perms_role_required
fix: decorator_perms_role_required
2024-06-04 19:23:58 +08:00
simontigers b7a6484579 fix: decorator_perms_role_required 2024-06-04 19:23:22 +08:00
Leo Song df3dc7cb1b Merge pull request #528 from veops/feat/dev_ui_240604
feat(ui): update model relation
2024-06-04 12:05:42 +08:00
songlh 4dfbf7cd62 feat(ui): update model relation 2024-06-04 12:04:26 +08:00
pycook 0209ba3778 feat(api): attribute association supports multiple groups (#527) 2024-06-04 11:34:54 +08:00
pycook 95eeda2ebe feat: update docker-compose 2024-05-30 13:18:28 +08:00
pycook 0aeac5b0df feat: put the mysql password in .env 2024-05-30 13:08:18 +08:00
pycook b0e7748ad0 fix(acl): add relation 2024-05-30 09:33:30 +08:00
pycook 9ccaaffa4a chore: release v2.4.5 2024-05-29 13:32:40 +08:00
pycook 58981a7301 fix(api): topology view read permission 2024-05-29 11:34:02 +08:00
pycook 29dab8ad06 feat(ui): update components CMDBExprDrawer 2024-05-28 20:16:39 +08:00
pycook f0713c5ac8 feat(ui): relation-graph upgrade to 2.1.42 2024-05-28 20:11:53 +08:00
pycook e4750d122e feat(ui): topology view (#525) 2024-05-28 20:03:10 +08:00
fxiang21 2da89ca1ef fix: cmdb-inner-secrets-init bug 2024-05-28 19:57:16 +08:00
pycook 5f6f22bff4 feat(api): i18n update 2024-05-28 18:08:15 +08:00
simontigers 9d0990b58b Merge pull request #524 from simontigers/common_cmdb_app_perm
fix: cmdb app perms
2024-05-28 17:55:08 +08:00
hu.sima 51027e9ac2 fix: cmdb app perms 2024-05-28 17:54:51 +08:00
pycook d276b3122e feat(api): topology view (#523)
* feat(api): topology views crud

* feat(api): topology view

* feat(api): topology view api done

* feat(api): topology view is done
2024-05-28 17:50:09 +08:00
simontigers e727391fed Merge pull request #521 from simontigers/common_cmdb_app_perm
feat: CMDB add TopologyView resource
2024-05-28 16:21:25 +08:00
simontigers 46f4cb5be0 feat: CMDB add TopologyView resource 2024-05-28 16:20:56 +08:00
pycook 59d948fb54 feat(ui): resource views router 2024-05-21 17:58:06 +08:00
pycook 11ff531730 fix(api): hot loading is blocked in development mode 2024-05-21 13:14:40 +08:00
pycook f89c18db51 Dev UI 240520 (#517)
* feat(ui): Model configuration supports search models

* fix(ui): Jump to the first subscription by default
2024-05-20 14:10:33 +08:00
thexqn a889fe503a chore: Update local.md with instructions for setting up MySQL and Redis services (#515) 2024-05-20 13:39:05 +08:00
pycook 55960aeb54 fix(api): exception when calling webhook (#516) 2024-05-20 13:22:54 +08:00
pycook 0a13ca82c6 feat(ui): Resources and Preference support grouping 2024-05-19 21:55:36 +08:00
pycook 6955714951 feat(api): my preference support grouping (#513) 2024-05-18 22:55:01 +08:00
pycook c92a3a5f31 fix(ui): some bugs (#512) 2024-05-17 12:07:56 +08:00
pycook a903738cdc Dev api 240517 (#511)
* fix(api): list values delete

* fix(acl): role rebuild cache
2024-05-17 11:20:53 +08:00
pycook 6b05ab1acc docs: update sql 2024-05-16 20:59:30 +08:00
pycook dc454f081d fix(ui): issue#490 2024-05-02 21:28:06 +08:00
pycook 618c68423a fix(api): unique constraint (#505) 2024-05-02 21:22:40 +08:00
pycook 123c35c890 fix(api): permissions for CIType group editing 2024-04-29 15:18:47 +08:00
pycook 3b4aa14bad docs: update build_api_key 2024-04-29 15:11:12 +08:00
pycook 2b639dd11f chore: release v2.4.4 2024-04-29 14:44:33 +08:00
pycook 3b95fb9bb5 feat(ui): baseline rollback (#502) 2024-04-29 10:10:07 +08:00
simontigers 657d57a742 Merge pull request #501 from simontigers/common_decorator_perms
fix: role base app perm
2024-04-29 09:27:36 +08:00
hu.sima c0fc534958 fix: role base app perm 2024-04-29 09:26:23 +08:00
simontigers 373dda6f41 Merge pull request #500 from simontigers/common_decorator_perms
fix(api): decorator_perms_role_required
2024-04-28 19:43:22 +08:00
hu.sima bd31043608 fix(api): decorator_perms_role_required 2024-04-28 19:41:50 +08:00
simontigers 3da43b6cef Merge pull request #499 from simontigers/common_decorator_perms
feat(api): role perm
2024-04-28 19:22:43 +08:00
hu.sima 03ec2e7d01 feat(api): role perm 2024-04-28 19:22:10 +08:00
pycook 7d9ef229c2 feat(api): ci baseline rollback (#498) 2024-04-28 19:19:14 +08:00
kdyq007 b4326722e6 feat(api): Add sorting function to ci list attribute (#495)
Co-authored-by: sherlock <sherlock@gmail.com>
2024-04-27 09:20:24 +08:00
pycook 69fb7f88ae feat(ui): CI change logs related itsm 2024-04-24 20:09:59 +08:00
dagongren d2b7161e39 feat:update cs && update style (#488) 2024-04-23 12:20:27 +08:00
dagongren d811f4d83f fix(cmdb-ui):service tree search (#487) 2024-04-19 13:32:12 +08:00
dagongren 644cd98af9 fix(cmdb-ui):fix service tree change table page (#486) 2024-04-19 11:46:51 +08:00
dagongren f07b87e568 style (#482) 2024-04-18 10:49:39 +08:00
pycook e06bf67b5e chore: add volumes cmdb_cache-data in docker-compose 2024-04-18 10:02:57 +08:00
pycook 5a2581d569 fix(api): commands cmdb-init-cache 2024-04-17 21:37:18 +08:00
dagongren f317e24ae8 fix(cmdb-ui):service tree key (#480) 2024-04-17 20:42:16 +08:00
pycook 7d1a05e487 chore: release v2.4.3 2024-04-17 19:35:35 +08:00
dagongren 0966d104a7 feat(cmdb-ui):citype show attr && service tree search (#479) 2024-04-17 17:59:21 +08:00
pycook 11dc7a6013 feat(api): custom attribute display (#478) 2024-04-17 17:50:46 +08:00
simontigers 00b022d620 Merge pull request #474 from simontigers/common_check_new_columns
Common check new columns
2024-04-16 15:35:15 +08:00
hu.sima e6ffcf9ebd fix(api): check new column support enum change 2024-04-16 15:34:03 +08:00
hu.sima e6eb1b8247 fix(api): secrets_shares Import ERROR 2024-04-16 15:33:36 +08:00
pycook 780dbbc280 feat(api): service tree search by keywords (#471) 2024-04-15 20:04:56 +08:00
loveiwei 0d7101c9f8 fix: support sealing and unsealing secret in multiple process(more than one workers started by gunicorn) (#469)
* fix: 解决在麒麟系统上使用docker安装时使用celery -D启动 celery 可能出现的问题

* fix: 解决在麒麟系统上使用docker安装时使用celery -D启动 celery 可能出现的问题

* fix: NoneType happend while unsealing the secret funtion, cancel the address check while unseal and seal

* fix: unseal secret function

* fix: remove depens_on in docker-compose

* fix: support sealing and unsealing secret in multiple process(more than one workers started by gunicorn)
2024-04-15 18:08:47 +08:00
dagongren 5008fe0491 fix(cmdb-ui):ci detail relation repeatly ciid (#468) 2024-04-15 13:50:50 +08:00
dagongren bb72881f3b style: global static.less (#467) 2024-04-12 15:18:52 +08:00
pycook 2f7896b3db release: 2.4.2 2024-04-03 15:55:13 +08:00
dagongren cf8ed6cda6 feat(cmdb-ui): attributes relation (#463) 2024-04-03 15:27:54 +08:00
pycook 9e62780d50 feat(api): rebuild relation by attribute (#462) 2024-04-03 15:13:43 +08:00
pycook a97d3d6198 feat(api): build relation by attributes (#461) 2024-04-02 09:19:51 +08:00
dagongren 9dfea3b478 feat:add icons (#460) 2024-04-01 17:37:00 +08:00
dagongren c252ef2d08 fix:topmenu shake & change logo (#459) 2024-04-01 15:11:24 +08:00
pycook 10406942a0 fix(api): import CIType 2024-03-29 15:50:07 +08:00
ivonGwy 33e58a658b fix: discover scripts (#458)
Co-authored-by: wang-liang0615 <dhuwl0615@163.com>
2024-03-29 15:48:46 +08:00
dagongren bb7fd13cb2 style and 文案变更 (#457) 2024-03-29 15:02:18 +08:00
pycook 7f5e5a0921 fix(api): CIType template import 2024-03-29 14:20:56 +08:00
pycook 9793734655 docs: docker-compose changed to docker compose 2024-03-29 13:27:23 +08:00
pycook 8f74be216e Merge branch 'master' of github.com:veops/cmdb 2024-03-29 13:14:00 +08:00
pycook a7c3a0a072 fix(acl): del resource 2024-03-29 13:13:38 +08:00
dagongren 0437da5797 i18n (#456) 2024-03-29 13:11:52 +08:00
pycook f0ac4d10ff release: v2.4.1 2024-03-29 12:47:23 +08:00
dagongren 85dcb997fb i18n (#455) 2024-03-29 12:23:23 +08:00
dagongren a4729a3c1d style && service tree define (#454) 2024-03-29 11:53:43 +08:00
pycook 6cda354c21 pref(api): error tips for out of range value (#453) 2024-03-29 11:46:50 +08:00
dagongren 702d8d65f0 fix:icon/filter/router...and some bugs (#451) 2024-03-29 10:50:14 +08:00
dagongren 9ce5a96232 icon font && opsTable (#450) 2024-03-28 20:59:32 +08:00
dagongren 79c9abe383 fix:cmdbgrant (#449) 2024-03-28 20:16:47 +08:00
dagongren e9d2365766 fix:Login (#448) 2024-03-28 19:56:54 +08:00
pycook 614766563e Dev api 240328 (#445)
* feat(api): login api supports parameter auth_with_ldap

* fix(api): transfer attribute
2024-03-28 19:12:47 +08:00
dagongren 472642c958 feat:ui 全面升级 (#444) 2024-03-28 18:38:15 +08:00
pycook 5ad73366ad fix(api): batch import ci relation 2024-03-26 20:38:39 +08:00
pycook 2c12f5fc6f fix(api): import CIType
fix(api): import CIType
2024-03-26 16:53:10 +08:00
pycook b08fa206e4 fix(api): revoke service tree node permissions
fix(api): revoke service tree node permissions
2024-03-26 12:05:22 +08:00
pycook dc569c32a5 feat(api): support service tree editing (#437) 2024-03-26 10:58:11 +08:00
simontigers ec55dadc57 Merge pull request #431 from simontigers/common_employee_edit_department_in_acl
fix(api): common_employee_edit department in acl role
2024-03-25 11:46:30 +08:00
hu.sima 24af71c1fc fix(api): common_employee_edit department in acl role 2024-03-25 11:46:04 +08:00
simontigers cf45f608d4 Merge pull request #430 from simontigers/common_file_ext_check
fix(api): check file ext with magic
2024-03-25 11:17:36 +08:00
hu.sima 53943f1244 fix(api): check file ext with magic 2024-03-25 11:16:04 +08:00
simontigers 6b2d4902af Merge pull request #429 from simontigers/common_check_new_columns
fix(api): common check new columns
2024-03-22 17:52:19 +08:00
simontigers 2b0261f055 fix(api): common check new columns 2024-03-22 16:48:16 +08:00
pycook b63ca2a059 Merge branch 'master' of github.com:veops/cmdb 2024-03-20 11:56:49 +08:00
pycook 4ebaf9c102 fix: custom dashboard 2024-03-20 11:56:39 +08:00
dagongren df5c62d98e style:update global.less (#426) 2024-03-19 10:01:38 +08:00
pycook 39e38b10cd release: 2.3.13 2024-03-18 20:35:51 +08:00
dagongren 02f332606e feat(cmdb-ui):service tree grant (#425) 2024-03-18 19:59:16 +08:00
pycook f30b8ecd3a Dev api 0308 (#424)
* feat(api): grant by node in relation view

* fix(api): When removing attributes, remove the unique constraint

* feat(api): grant by service tree
2024-03-18 19:57:25 +08:00
simontigers 7a170ab397 fix(api): edit employee depart with rid=0 (#420) 2024-03-12 17:46:50 +08:00
rustrover 34204ec4c6 fix: some typos (#415)
Signed-off-by: gcmutator <329964069@qq.com>
Co-authored-by: gcmutator <329964069@qq.com>
2024-03-11 15:04:38 +08:00
pycook f867ccbf94 fix(api): remove ACL resources when deleting CIType (#414) 2024-03-08 16:31:03 +08:00
pycook 60df081e49 fix(api): issule #412, unique value restrictions (#413) 2024-03-05 16:21:27 +08:00
pycook 2979d2056a feat(api): multi-id search (#411)
_id:(id1;id2)
2024-03-04 15:15:34 +08:00
simontigers 06af1f656d fix: deploy init common (#407) 2024-03-01 17:21:32 +08:00
pycook fe8582447e release: v2.3.12 2024-03-01 17:04:38 +08:00
dagongren d96d529aa9 fix(cmdb-ui):to lowercase (#406) 2024-03-01 13:52:30 +08:00
pycook aa000cabe2 feat(api): CIType inheritance (#405) 2024-03-01 13:51:13 +08:00
dagongren 27affe02a8 feat(cmdb-ui):ci type inherit (#404) 2024-03-01 13:39:20 +08:00
dagongren f010b9625e feat:ci detail share (#403) 2024-02-27 16:13:28 +08:00
dagongren ef1d0c34cf fix(cmdb-ui):triggers webhook headers (#402) 2024-02-26 13:46:40 +08:00
pycook 7474a92377 feat(api): Remove many-to-many restrictions (#401) 2024-02-26 10:17:53 +08:00
pycook 26c3404f28 fix(api): db-setup commands (#399)
fix(api): db-setup commands
2024-02-23 11:05:11 +08:00
dagongren d74f201710 fix(cmdb-ui):resource search common attrs (#397) 2024-02-22 16:19:12 +08:00
pycook 5b3fb7ee32 feat(acl): login channel add ssh options (#396) 2024-02-21 18:10:44 +08:00
simontigers 0f404fe9bf fix: grant common perm after create new employee (#394) 2024-02-04 13:48:02 +08:00
dagongren 498feee0a2 fix(cmdb-ui):fix multiple default value (#395) 2024-02-04 11:49:44 +08:00
wang-liang0615 2bd1a45ae6 fix(ui):login email-》username (#393) 2024-01-31 15:52:27 +08:00
pycook d4279600b5 docs: update init sql
docs: update init sql
2024-01-26 13:57:36 +08:00
pycook c52fa3bc80 feat(api): Auto-increment id can be used as primary key (#391) 2024-01-26 13:12:17 +08:00
simontigers 849af21855 fix: change common_setting task queue (#390) 2024-01-25 17:39:52 +08:00
pycook 861564c0da release: 2.3.11 2024-01-13 15:06:51 +08:00
pycook 9511e33736 ui: lint 2024-01-12 18:21:47 +08:00
wang-liang0615 b6ef49f139 lint regexSelect (#382) 2024-01-12 18:09:03 +08:00
wang-liang0615 9141c06530 feat:citype regex check & pref:edit is_list (#380) 2024-01-12 17:09:44 +08:00
pycook 9bc0ab6009 feat(api): password supports regular check 2024-01-12 16:56:10 +08:00
pycook 67081ef005 feat(api): Attributes support regular check (#379)
feat(api): Attributes support regular check
2024-01-12 13:05:37 +08:00
wang-liang0615 4111ac8d31 feat(cmdb-ui):preference citype order (#378) 2024-01-12 11:14:53 +08:00
pycook 7b593ce1bc Dev api 240111 (#377)
* feat(api): My subscription supports CIType sorting

* feat(api): db change
2024-01-11 18:01:37 +08:00
pycook e1b81561c9 perf(api): /api/v0.1/ci/adc/statistics
perf(api): /api/v0.1/ci/adc/statistics
2024-01-11 10:10:01 +08:00
pycook c53e5ecd30 fix(api): grant by attr (#373) 2024-01-10 16:52:27 +08:00
pycook 6f6da3c228 fix(api): commands add-user 2024-01-10 11:56:39 +08:00
pycook 95d85234c6 feat(db): set variable sql_mode
feat(db): set variable sql_mode
2024-01-10 10:28:15 +08:00
wang-liang0615 996151f0f4 fix(ui):some fix (#370)
* pref(ui):some bugfix & some style

* fix(ui):some fix
2024-01-10 09:46:02 +08:00
wang-liang0615 c2ba819076 pref(ui):some bugfix & some style (#369) 2024-01-09 14:48:13 +08:00
pycook 77d8a21bde fix(api): cmdb-init-acl commands (#368) 2024-01-09 10:04:53 +08:00
wang-liang0615 0f23feda2f fix(ui):logout (#365) 2024-01-04 16:12:20 +08:00
wang-liang0615 21bb741a02 refactor(ui):extract pager components (#364) 2024-01-04 13:41:07 +08:00
pycook 6c7bc690cc feat: update docker-compose 2024-01-04 10:26:47 +08:00
pycook 56dab953e7 release: v2.3.10 2024-01-03 19:23:42 +08:00
wang-liang0615 cdf7506b5d fix(acl-ui):operation history (#363) 2024-01-03 17:41:42 +08:00
wang-liang0615 f3ca1fbea3 fix(ui) (#362) 2024-01-03 17:09:25 +08:00
simontigers 6c9c987979 fix(api): common edit department return (#359)
fix(api): common edit department return (#359)
2024-01-03 16:42:14 +08:00
wang-liang0615 4e48dd2b37 format(ui) (#361) 2024-01-03 16:35:50 +08:00
wang-liang0615 1a8d54d4e2 feat(ui):i18n (#360)
* feat(ui):i18n

* i18n

* feat(ui):i18n
2024-01-03 16:03:15 +08:00
simontigers be0712f202 fix(api): common department allow parent (#358)
* fix(api): common department edit method

* fix(api): common department edit method

* fix(api): common department allow parent
2024-01-03 15:19:08 +08:00
wang-liang0615 ff8d4bd51b feat(ui):i18n (#357)
* feat(ui):i18n

* i18n
2024-01-03 14:58:47 +08:00
simontigers af148b7c8f fix(api): common department edit method (#356)
* fix(api): common department edit method

* fix(api): common department edit method
2024-01-03 14:31:54 +08:00
simontigers 5e79aab93d fix(api): common department edit method (#355) 2024-01-03 14:26:40 +08:00
pycook 99022bdabb docs: update install (#354) 2024-01-03 13:58:35 +08:00
wang-liang0615 19c6009c64 feat(ui):i18n (#352) 2024-01-03 13:29:38 +08:00
simontigers b3ef1aa5c1 fix(api): common i18n wide (#351)
* fix(api): common_i18n wide

* fix(api): department i18n
2024-01-03 13:26:58 +08:00
simontigers 2992bc2fae fix(api): common_i18n wide (#350) 2024-01-03 12:29:49 +08:00
wang-liang0615 5c2cb9073f feat(ui):add packages (#349) 2024-01-03 09:42:32 +08:00
wang-liang0615 ee50ea1cf3 i18n (#348) 2024-01-02 18:04:53 +08:00
wang-liang0615 11259b4067 feat(ui):i18n (#347)
* feat(acl-ui):i18n

* feat(base-ui):i18n

* feat(cmdb-ui):i18n
2024-01-02 17:53:07 +08:00
pycook a23bdab10e Dev api 231229 (#345)
* fix(api): predefined value for float

* feat(api): update public clouds config

* feat(api): commands add-user support is_admin
2023-12-29 13:44:23 +08:00
wang-liang0615 5c8e93e194 style(ui):global.less (#344) 2023-12-28 11:00:00 +08:00
wang-liang0615 f0bf740d70 fix(cmdb-ui):model relation table (#343) 2023-12-27 13:24:01 +08:00
wang-liang0615 98025ae47c feat:Batch import and download templates support predefined values (#342) 2023-12-27 13:18:05 +08:00
simontigers f65d81bf46 feat(api): common i18n (#340) 2023-12-26 10:06:15 +08:00
pycook 33f9f190e9 feat(api): i18n
feat(api): i18n
2023-12-25 21:51:44 +08:00
pycook 8646f4693a fix(api): CI revoke permission (#337) 2023-12-25 12:15:20 +08:00
kdyq007 d0575331d5 [更新] 修复 LDAP 登录失败的问题 (#336)
Co-authored-by: sherlock <sherlock@gmail.com>
2023-12-25 09:36:31 +08:00
pycook 885c346407 release: 2.3.9 2023-12-23 12:51:09 +08:00
pycook b887de2ab8 feat(api): update cmdb-init-acl commands (#335) 2023-12-23 12:44:01 +08:00
pycook 51c42f90be Merge branch 'master' of github.com:veops/cmdb 2023-12-23 12:31:52 +08:00
pycook a938477d85 fix(api): role grant 2023-12-23 12:30:52 +08:00
simontigers f965ad3bf3 feat(api): add update_last_login_by_uid (#333) 2023-12-22 18:43:20 +08:00
wang-liang0615 5fe6676d83 feat(cmdb-ui):ci type import&export,pref(cmdb-ui):download ci xlsx name, pref(cmdb-ui):ci detail history merge row method (#331)
* pref(cmdb-ui):download ci xlsx name

* pref(cmdb-ui):ci detail history merge row method

* feat(cmdb-ui):ci type import&export
2023-12-22 15:42:20 +08:00
pycook 41ad610c00 fix(api): ci relation search
fix(api): ci relation search
2023-12-22 15:35:02 +08:00
simontigers 7b8e120974 feat(api): add get_file_binary_str and save (#329) 2023-12-22 15:33:05 +08:00
simontigers 1ee8ed7c4f fix(api): refresh rid after create and import employee (#328) 2023-12-22 15:24:48 +08:00
pycook 1da629b877 pref(api): import and export of CIType templates
pref(api): import and export of CIType templates
2023-12-22 14:32:03 +08:00
pycook 5279d96c84 fix(api): add CI (#326) 2023-12-22 11:19:16 +08:00
simontigers 718335231c fix(api): svg upload (#321) 2023-12-21 18:58:35 +08:00
wang-liang0615 c361997591 fix(acl_ui):permission (#325) 2023-12-21 17:22:49 +08:00
wang-liang0615 3e2943b49e fix(ui):common double menu (#324) 2023-12-21 14:59:39 +08:00
wang-liang0615 75122f7a40 feat(ui):批量导入模型根据create权限过滤&&模型配置页面权限 (#323) 2023-12-21 14:23:38 +08:00
wang-liang0615 6674510697 fix:open triggerForm from attributeCard (#322) 2023-12-21 14:18:00 +08:00
pycook 4a0233df24 feat: Fixed db volume name 2023-12-21 10:26:51 +08:00
pycook 9afef06c54 fix(api): CAS authentication 2023-12-20 12:10:00 +08:00
wang-liang0615 37b3c1aa01 feat(ui):api_host annotation (#320) 2023-12-19 14:22:40 +08:00
pycook e597c2aee9 fix(api): oauth2.0 authentication 2023-12-19 13:07:21 +08:00
wang-liang0615 b61c14ba07 feat:add auth common api_host (#319) 2023-12-19 11:23:27 +08:00
pycook 3a760c3a80 fix(api): ldap authentication 2023-12-19 00:16:56 +08:00
pycook 92b54f045f release: v2.3.8 2023-12-18 20:08:19 +08:00
pycook 3e6ebec9af feat(ui): lint 2023-12-18 19:25:22 +08:00
simontigers 4a92d95d2f fix: auth config (#318) 2023-12-18 18:27:06 +08:00
simontigers 0806f2ed73 fix: auth config (#317) 2023-12-18 16:52:24 +08:00
wang-liang0615 ef052c4ab9 lint(ui) (#316) 2023-12-18 16:41:21 +08:00
wang-liang0615 3c315ff397 fix(ui):401 redirect && feat(ui):add auth ldap test (#315) 2023-12-18 16:30:02 +08:00
wang-liang0615 fc4bb7ffb4 pref(cmdb-ui):change adt key & add adt alias (#314) 2023-12-18 16:07:52 +08:00
pycook e1c797b6a1 pref(api): A CIType allows repeated binding of auto-discovery rules (#313) 2023-12-16 17:56:14 +08:00
wang-liang0615 faeb9a04f9 fix:is_list edit bug (#312)
* feat(ui):auth setting

* fix:is_list edit bug
2023-12-15 13:19:55 +08:00
simontigers c89ebf6518 fix(api): common_data (#311) 2023-12-15 10:56:04 +08:00
wang-liang0615 a21afbe909 feat(ui):auth setting (#310) 2023-12-15 10:33:38 +08:00
pycook 6aef26b82c pref(api): authentication and login log (#308)
* pref(api): authentication and login log

* feat(api): ldap and OAuth2.0
2023-12-14 19:53:08 +08:00
simontigers ee0b74bec7 feat(api): auth config api (#309) 2023-12-14 19:39:21 +08:00
wang-liang0615 32e073f3fd fix(cmdb-ui):batch upload cancel bug && download error (#306) 2023-12-13 14:50:53 +08:00
simontigers 67d64abf42 fix(api): get_employee_notice check data None (#305)
* fix(api): get_employee_notice check data None

* fix(api): remove path when save messager url
2023-12-13 09:45:42 +08:00
pycook 093065551b feat(api): support OAuth2.0 and OIDC authentication, it has been tested with casdoor
feat(api): support OAuth2.0 and OIDC authentication, it has been tested with casdoor
2023-12-12 20:29:57 +08:00
wang-liang0615 b84d5d717e pref(cmdb-ui):batch upload for date type (#301) 2023-12-12 14:53:12 +08:00
pycook 65664ae8f9 fix(api): time data format
fix(api): time data format
2023-12-12 14:37:21 +08:00
wang-liang0615 7f1d796fd1 Revert "pref(cmdb-ui):batch upload for date type (#298)" (#299)
This reverts commit cd319421d5.
2023-12-12 13:51:19 +08:00
wang-liang0615 6c45ca7cb0 pref(cmdb-ui):batch upload for date type (#298)
* fix(cmdb-ui):set localstorage '' after unsubscribe ci

* pref(cmdb-ui):batch upload for date type
2023-12-12 13:38:08 +08:00
pycook 64f891df31 perf(api): ci delete (#297) 2023-12-12 11:09:32 +08:00
wang-liang0615 beb2f01ec9 fix(cmdb-ui):set localstorage '' after unsubscribe ci (#296) 2023-12-12 09:38:50 +08:00
pycook 4b133c56fd feat(api): cas is compatible with casdoor
feat(api): cas is compatible with casdoor
2023-12-11 20:58:18 +08:00
pycook a1a6b11072 Dev api 231211 (#294)
* fix(api): cas authentication

* feat(api): add lz4 package
2023-12-11 19:30:09 +08:00
simontigers 379cabbfca feat(api): upload file save db (#292) 2023-12-11 18:22:33 +08:00
gmailnovo f1350a2940 feat: Handle '/dev/stdout' in Logger Configuration 2023-12-07 11:27:45 +08:00
simontigers 86fbd62048 fix(api): Common create employee (#287)
* fix(api): add add_from arg in create employee

* fix(api): add check in acl call common after add user
2023-12-06 17:12:37 +08:00
wang-liang0615 4d783235b7 pref(cmdb-ui):ci upload&delete concurrent 6 (#286) 2023-12-06 14:33:25 +08:00
pycook 34c846581e docs: update local install 2023-11-30 16:18:33 +08:00
simontigers 7c7b360ffc fix(api): common_data delele (#282) 2023-11-30 13:03:09 +08:00
pycook 4b8c5b8495 feat(api): only the role cmdb_admin can modify the CIType group (#280) 2023-11-29 17:40:12 +08:00
pycook c523ffaf65 fix(api): get relation history 2023-11-28 20:37:36 +08:00
pycook 315aa40b72 release: 2.3.7 2023-11-24 14:53:53 +08:00
pycook 7ef23bd779 feat(api): issue #212 (#279) 2023-11-24 10:26:48 +08:00
wang-liang0615 91555ffa64 feat(cmdb-ui):多对多关系&&仪表盘色卡调整 (#271) 2023-11-24 10:25:56 +08:00
loveiwei 2699c263d9 doc: change content in readme_en (#278) 2023-11-23 20:10:25 +08:00
loveiwei 8f3421bc29 feature: add a new management script, such as install,start,pause,del… (#277)
* feature: add a new management script, such as install,start,pause,delete,uninstall

* doc: add install.sh method in readme

* doc: add install.sh method in readme, change install.sh for macos support

* doc: add install.sh method in readme

* doc: add install.sh method in readme
2023-11-23 17:45:53 +08:00
loveiwei 17612105a5 Fix deploy 1700028675 (#272)
* fix: Solving the timezone issue in Redis, as well as the problem of MySQL logs always being in UTC timezone.

* fix: change the config path of slow_log into /tmp directory in mysqld.cnf file
2023-11-22 18:34:58 +08:00
pycook 21d445458d docs: local install (#270) 2023-11-16 20:54:08 +08:00
loveiwei 3b73971bf8 fix: Solving the timezone issue in Redis, as well as the problem of MySQL logs always being in UTC timezone. (#268) 2023-11-15 20:48:52 +08:00
wang-liang0615 8856115e7c feat(acl-ui):resources table resizable (#267) 2023-11-14 09:37:45 +08:00
pycook fb371a0d46 Dev api 231108 (#264)
* perf(api): commands add-user

* feat(api): add commands cmdb-agent-init
2023-11-09 11:32:54 +08:00
pycook 6aa02eed73 Dev api 1107 (#263)
feat: update cmdb.sql
2023-11-08 16:11:36 +08:00
wang-liang0615 e5d4015f7e ui package update (#261)
* update(cmdb-ui):update packages && delete yarn.lock

* update:gitignore
2023-11-08 16:06:48 +08:00
wang-liang0615 1b16a0b9c0 Revert "Dev UI 231108 (#256)" (#260)
This reverts commit 45d3f57228.
2023-11-08 14:43:37 +08:00
wang-liang0615 3d4c4d6b56 Dev UI 231108 (#256)
* pref(cmdb-ui):update packages

* pref(cmdb-ui):update packages
2023-11-08 14:32:17 +08:00
pycook a50f6d4fe4 feat(api): encrypting webhook configurations (#255) 2023-11-07 17:15:24 +08:00
pycook 07412169b3 fix(api): Code scanning alerts (#254) 2023-11-06 14:27:30 +08:00
pycook e577dce4ee Dev api 1103 (#252)
feat(api): update requirements
2023-11-06 13:24:08 +08:00
dependabot[bot] c394c41c3c build(deps): bump browserify-sign from 4.2.1 to 4.2.2 in /cmdb-ui (#241)
Bumps [browserify-sign](https://githubfast.com/crypto-browserify/browserify-sign) from 4.2.1 to 4.2.2.
- [Changelog](https://githubfast.com/browserify/browserify-sign/blob/main/CHANGELOG.md)
- [Commits](https://githubfast.com/crypto-browserify/browserify-sign/compare/v4.2.1...v4.2.2)

---
updated-dependencies:
- dependency-name: browserify-sign
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-06 13:13:29 +08:00
pycook 2554a7d1be fix(api): search sort misses cache (#251) 2023-11-03 12:01:44 +08:00
pycook 2c70cb49e1 fix(api commands): cmdb-password-data-migrate (#249) 2023-11-02 20:43:39 +08:00
simontigers 5e5ec3f887 style(common-setting): clean warning (#246) 2023-10-30 17:35:34 +08:00
pycook 5132481fef fix(api): secrets 2023-10-30 17:23:42 +08:00
pycook f75756b71a feat: add inner password storage and optimize flask command about inner cmdb (#248)
Co-authored-by: fxiang21 <fxiang21@126.com>
2023-10-30 16:48:53 +08:00
wang-liang0615 4440654a25 Dev UI 231030 (#247)
* config(ui):useEncryption default false

* fix(cmdb-ui):ident 4

* fix(cmdb-ui):relation views
2023-10-30 12:38:05 +08:00
pycook 072daeea35 Dev api password (#244)
* fix: delete CI password data

* fix(api): update CI password to flush cache
2023-10-29 11:42:07 +08:00
pycook 1ccbafce08 fix: delete CI password data (#243) 2023-10-29 10:53:29 +08:00
pycook e785d1a2f6 fix(cmdb-ui): CI update password 2023-10-28 23:07:01 +08:00
pycook 2d59757ba7 release: v2.3.6 2023-10-28 17:51:13 +08:00
pycook a94575df0d feat(cmdb-api): ci password 2023-10-28 16:55:02 +08:00
pycook 36a451686e feat(cmdb-api): CI password data store (#242)
* add secrets,for test

* feat: vault SDK (#238)

* feat: vault SDK

* docs: i18n

* perf(vault): format code

* feat(secrets): support vault

* feat: add inner password storage

* feat: secrets

* feat: add inner password storage

* feat: add secrets feature

* perf(secrets): review

---------

Co-authored-by: fxiang21 <fxiang21@126.com>
Co-authored-by: Mimo <osatmnzn@gmail.com>
2023-10-28 16:19:00 +08:00
wang-liang0615 c273ecdd29 feat:预定义值支持脚本&&密码存储&&一些bugfix (#239) 2023-10-27 11:10:43 +08:00
ivonGwy 2154834f44 Doc (#235)
* change assignees
2023-10-25 16:00:04 +08:00
ivonGwy c020d2fce8 Doc (#234)
* final template
2023-10-25 15:52:12 +08:00
ivonGwy 02b6e1d66b Doc (#232)
* fix bugs
2023-10-25 14:54:23 +08:00
ivonGwy be72a4579b Update issue templates 2023-10-25 14:46:44 +08:00
ivonGwy 3c660e0969 Doc (#231)
* add issue template
2023-10-25 14:36:36 +08:00
ivonGwy b44d9d7e47 Update issue templates 2023-10-25 14:06:24 +08:00
ivonGwy b31bbd3d63 Update issue templates 2023-10-25 14:04:53 +08:00
pycook be32b9b043 fix(api): add ci (#230) 2023-10-25 13:51:29 +08:00
ivonGwy 6c200f12a7 Doc (#229)
* change reandme
2023-10-25 13:19:30 +08:00
kdyq007 92dc81ec53 关闭前端密码加密;加强 ldap 用户验证 (#216)
* [更新] python-ldap 更新到 ldap3

* [更新] 关闭前端密码加密;加强 ldap 用户验证

* Update app.js

---------

Co-authored-by: sherlock <sherlock@gmail.com>
Co-authored-by: pycook <pycook@126.com>
2023-10-24 19:47:46 +08:00
pycook 4754bb59f4 feat: Predefined values support executing scripts (#227) 2023-10-24 19:32:43 +08:00
simontigers 9e180af9f4 fix: add_employee_from_acl (#225) 2023-10-24 14:20:40 +08:00
wang-liang0615 67779ccc67 fix:关系视图删除关系接口传参修改 (#224)
* fix:acl新增用户展示异常问题

* fix:关系视图删除关系接口传参修改
2023-10-24 06:04:21 +08:00
wang-liang0615 8c47105fbc fix:acl新增用户展示异常问题 (#223) 2023-10-24 05:59:08 +08:00
pycook 30ccb87499 feat: add cryptography to requirements 2023-10-23 14:37:01 +08:00
pycook 0ac14fa318 fix: acl cache 2023-10-23 13:57:06 +08:00
Evan Sung 819b994c2d fix(common): fix 'ACLManager' object has no attribute 'create_app' (#217) 2023-10-21 11:38:19 +08:00
Evan Sung 6047c59f51 fix(ci_cache): ci cache async args (#215) 2023-10-20 12:05:19 +08:00
kdyq007 c869827ffd feat: python-ldap 更新到 ldap3 (#214)
Co-authored-by: sherlock <sherlock@gmail.com>
2023-10-20 09:36:38 +08:00
pycook bdd2adcfc2 Dev api 20231019 (#210)
* fix(acl): get resources

* fix(celery worker): db server has gone away
2023-10-19 11:51:34 +08:00
wang-liang0615 e0e2ca6294 fix:ci relation add type filter (#208) 2023-10-18 14:06:28 +08:00
pycook 2a5091c51a fix: ci relation statistics 2023-10-18 13:35:01 +08:00
pycook 788dd684ab docs: api doc 2023-10-17 12:06:37 +08:00
wang-liang0615 ee1b068b62 feat:webhook body 支持非json (#203) 2023-10-17 10:44:38 +08:00
Evan Sung b08ed43105 Feature db migrate 20231013 (#202)
* feat(db): support flask migrate

* minor

---------

Co-authored-by: s01249 <songbing@smyfinancial.com>
2023-10-13 16:24:49 +08:00
Evan Sung 41d810642b feat(db): support flask migrate (#201)
Co-authored-by: s01249 <songbing@smyfinancial.com>
2023-10-13 15:55:26 +08:00
simontigers f033af2f58 fix: common perms (#200) 2023-10-12 16:02:35 +08:00
wang-liang0615 cbe579fdb4 pref:批量上传&资源管理小优化 (#199) 2023-10-12 15:06:39 +08:00
ivonGwy 4e1f25b389 Merge pull request #197 from veops/doc
Doc
2023-10-11 14:34:05 +08:00
ivonGwy a27b9a37df change wechat pic size 2023-10-11 14:33:19 +08:00
ivonGwy ebc8a1e254 change wechat pic 2023-10-11 14:28:49 +08:00
pycook e51fa08208 Dev api (#196)
* docs: update

* docs: README & Makefile
2023-10-11 13:40:15 +08:00
wang-liang0615 55c2c557fc Dev UI 231009 (#195)
* pref:用户密钥非必填

* fix:chartColor undefined
2023-10-11 09:12:04 +08:00
pycook 5aa36daaac docs: update (#194) 2023-10-10 16:53:30 +08:00
wang-liang0615 ba519a2c43 pref:用户密钥非必填 (#193) 2023-10-10 09:25:24 +08:00
pycook 9dbea9f403 release: v2.3.5 2023-10-09 20:55:30 +08:00
pycook d7bbc3ccf8 feat: get messenger url from common setting 2023-10-09 20:25:27 +08:00
wang-liang0615 501d86341a 前端更新 (#192)
* fix:add package

* fix:notice_info为null的情况

* fix:2 bugs

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

* fix:json 不支持预定义值

* fix:json 不支持预定义值

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

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

* fix:notice_info为null的情况

* fix:2 bugs

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

* fix:json 不支持预定义值

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

* fix:notice_info为null的情况

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

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

* feat:触发器

* add packages & 注释代码

* feat: webhook tips
2023-09-26 18:25:04 +08:00
wang-liang0615 ca0f332650 feat: webhook tips 2023-09-26 18:17:23 +08:00
wang-liang0615 43be749c58 add packages & 注释代码 2023-09-26 17:35:41 +08:00
wang-liang0615 788fefd9a1 feat:触发器 2023-09-26 17:01:31 +08:00
wang-liang0615 36a24d4a68 feat:新增api&适配 2023-09-26 16:26:25 +08:00
pycook c2066b53f1 fix: ci_cache 2023-09-25 15:46:07 +08:00
wang-liang0615 69d3d3b047 Merge pull request #178 from veops/dev_ui
前端更新:仪表盘优化
2023-09-25 14:52:09 +08:00
wang-liang0615 c236851d2f pref:仪表盘优化 2023-09-25 14:50:08 +08:00
wang-liang0615 249ba7ad5c Merge branch 'master' of github.com:veops/cmdb into dev_ui 2023-09-25 14:43:34 +08:00
pycook d8399f8723 refactor: CI triggers 2023-09-22 17:39:54 +08:00
simontigers 6e94c72031 fix: icon svg support (#177) 2023-09-20 15:56:57 +08:00
pycook 6e9871dfd8 fix date search 2023-09-18 18:15:02 +08:00
pycook e5ccb9a499 fix dashboard compute 2023-09-18 13:04:50 +08:00
pycook ad841f9732 release v2.3.3 2023-09-15 17:57:39 +08:00
pycook 47dbe5ba18 dashboard ui update 2023-09-15 17:36:10 +08:00
simontigers 737b29f7d6 feat: init resource for backend (#176) 2023-09-15 15:30:30 +08:00
pycook 8c703fb6d9 enhance dashboard 2023-09-15 15:26:20 +08:00
pycook afe365bd38 cmdb-api/api/lib/resp_format.py 2023-09-12 20:01:30 +08:00
pycook 5fa187bf46 Detect circular dependencies when adding CIType relationships 2023-09-12 20:00:56 +08:00
wang-liang0615 44f051dac9 计算属性 触发计算 (#174) 2023-09-11 19:16:05 +08:00
pycook 94cdb42477 fix upload template and add /api/v0.1/attributes/<int:attr_id>/calc_computed_attribute 2023-09-11 19:15:31 +08:00
wang-liang0615 cca8fccb55 Merge branch 'master' of github.com:veops/cmdb into dev_ui 2023-09-11 17:35:44 +08:00
wang-liang0615 eea379537f 计算属性 触发计算 2023-09-11 17:34:51 +08:00
pycook 2c74d107dd release 2.3.2 2023-09-07 13:44:51 +08:00
pycook 8f6b54cd12 Merge pull request #172 from veops/dev_ui
新建ci及批量导入时,新建关系
2023-09-07 11:04:49 +08:00
wang-liang0615 ae5636b702 新建ci及批量导入时,新建关系 2023-09-07 10:25:18 +08:00
pycook 9afb5179f7 Merge branch 'master' of github.com:veops/cmdb 2023-09-07 10:12:55 +08:00
pycook bba3a2b931 Add CI relationship when creating CI, the text value removes the escape 2023-09-07 10:12:42 +08:00
pycook 15f264681b Merge pull request #171 from ronething/fix/makefile
optimize: makefile help
2023-09-05 20:34:16 +08:00
ashing b099bc212b fix: review
Signed-off-by: ashing <axingfly@gmail.com>
2023-09-05 20:33:07 +08:00
ashing 3fd4a9fed6 fix: review
Signed-off-by: ashing <axingfly@gmail.com>
2023-09-05 20:29:29 +08:00
ashing ad0d32652e fix: review
Signed-off-by: ashing <axingfly@gmail.com>
2023-09-05 20:21:20 +08:00
ashing 8225159d5a optimize: makefile help
Signed-off-by: ashing <axingfly@gmail.com>
2023-09-05 20:06:31 +08:00
pycook 4a3db88ae6 Merge pull request #170 from ronething/feat/xx
feat: support docker deploy mysql and redis
2023-09-05 19:28:47 +08:00
ivonGwy e49ede0b3a Merge pull request #169 from veops/doc
add document link
2023-09-05 15:41:52 +08:00
ivonGwy 6fe6cf4600 add document link 2023-09-05 15:40:31 +08:00
ashing 4fb3138acd feat: support docker deploy mysql and redis
Signed-off-by: ashing <axingfly@gmail.com>
2023-09-05 15:26:50 +08:00
wang-liang0615 d1dc6c2fab Merge pull request #168 from veops/dev_ui
UI更新
2023-09-05 15:23:43 +08:00
wang-liang0615 9f8f3a29b7 Merge branch 'master' of github.com:veops/cmdb into dev_ui 2023-09-05 15:22:18 +08:00
wang-liang0615 063ed40440 模型关联 展示反向关系 2023-09-05 15:22:08 +08:00
pycook 96176c706a Merge branch 'master' of github.com:veops/cmdb 2023-09-05 14:49:53 +08:00
pycook 81faab5a20 move Dockerfile to docs 2023-09-05 14:49:34 +08:00
wang-liang0615 a39fd08e0d Merge pull request #167 from veops/dev_ui
sub menu color
2023-09-04 16:34:26 +08:00
wang-liang0615 cd954775bc sub menu color 2023-09-04 16:33:35 +08:00
wang-liang0615 5f550208ce Merge pull request #166 from veops/dev_ui
ui更新
2023-09-04 13:15:27 +08:00
wang-liang0615 88c9fb5ae3 Merge branch 'master' of github.com:veops/cmdb into dev_ui 2023-09-04 13:14:35 +08:00
wang-liang0615 2aa420ec07 sidebar 2023-09-04 13:14:11 +08:00
pycook 6e51e9ef4c import format 2023-09-02 12:09:41 +08:00
pycook 2f8856772b format 2023-09-01 18:07:44 +08:00
pycook c74fee8aa9 fix delete choice values 2023-08-31 16:02:24 +08:00
pycook e390b1ed7c fix delete choice values 2023-08-31 15:18:15 +08:00
wang-liang0615 ce5884b2b9 Merge pull request #165 from veops/dev_ui
proxy
2023-08-31 13:31:26 +08:00
wang-liang0615 7700afdf1c proxy 2023-08-31 13:28:15 +08:00
pycook 771439e008 Merge pull request #162 from simontigers/cmdb_icon_manage
feat: add cmdb custom icon manage
2023-08-31 11:15:09 +08:00
pycook 7918b81f33 Merge pull request #163 from veops/dev_ui
支持上传自定义图标
2023-08-31 11:14:42 +08:00
hu.sima 9547fce484 feat: add cmdb custom icon manage 2023-08-31 10:49:56 +08:00
wang-liang0615 d25c3760a1 支持上传自定义图标 2023-08-31 10:05:11 +08:00
pycook 6df845d662 fix update attribute 2023-08-30 13:34:10 +08:00
pycook 0ac5b9cfe4 Merge branch 'master' of github.com:veops/cmdb 2023-08-29 14:49:21 +08:00
pycook f74fd2ea8f The default value of USE_ACL is set to True 2023-08-29 14:49:09 +08:00
pycook 1e4033d0a6 Merge pull request #161 from simontigers/common_setting_format
fix: company info create
2023-08-29 11:01:25 +08:00
hu.sima 6ce139bec3 fix: company info create 2023-08-29 10:56:48 +08:00
pycook d763edc6bf Merge branch 'master' of github.com:veops/cmdb 2023-08-25 11:01:24 +08:00
pycook 11fe5ca457 update ad_ci when deleting ci 2023-08-25 10:59:38 +08:00
wang-liang0615 57b1b5fb67 Merge pull request #160 from veops/dev_ui
前端更新
2023-08-25 10:12:31 +08:00
wang-liang0615 349ddbd98a fix 新增类型回车键发送两次请求 2023-08-25 10:11:09 +08:00
wang-liang0615 700c411ec3 fix 新增类型回车键发送两次请求 2023-08-25 10:08:04 +08:00
pycook 4d36d448a7 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 0f1cb01f05 perf(ad_ci_relation): optimize ad_ci relation 2023-08-24 14:16:12 +08:00
pycook 7e06713b87 docker-compose add flask db-setup 2023-08-24 11:32:09 +08:00
pycook 41ee3b7f87 vxe-table-plugin-export-xlsx==2.0.0 2023-08-24 11:06:28 +08:00
pycook 36910d0052 add config CACHE_REDIS_PASSWORD and fix delete ci_type 2023-08-23 18:05:28 +08:00
pycook 52578d78d6 fix update ci 2023-08-22 11:34:40 +08:00
pycook 9b563f7b57 Register api and commands with absolute paths 2023-08-21 20:08:23 +08:00
pycook 96187bcd47 fix merge conflict 2023-08-21 11:55:49 +08:00
pycook 1ba3f85f7e fix g.user 2023-08-21 11:54:33 +08:00
pycook 3071983d6b version: 2.3.1 2023-08-20 11:24:53 +08:00
pycook 836892d909 lint 2023-08-20 11:23:55 +08:00
pycook a24ba8f7cd Merge pull request #157 from EvanSung/fix_20230817_guser_issue
fix(acl): g user issue
2023-08-17 22:12:45 +08:00
EvanSung d0d68c613b fix(acl): g user issue 2023-08-17 18:40:45 +08:00
pycook 9b976675c0 fix MyJSONEncoder 2023-08-16 21:28:27 +08:00
pycook 2200b122f0 Merge pull request #155 from veops/dev_ui
前端更新
2023-08-16 13:01:13 +08:00
pycook 190e82efe9 Merge branch 'master' of github.com:veops/cmdb 2023-08-16 13:00:44 +08:00
pycook 1dae48c583 Delete user without soft delete 2023-08-16 13:00:30 +08:00
wang-liang0615 5c25433e03 Merge branch 'master' of github.com:veops/cmdb into dev_ui 2023-08-16 10:09:47 +08:00
wang-liang0615 f8e94b7c70 delete user 2023-08-16 10:09:25 +08:00
pycook c64610e9ea 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 3f49586c9f fix: init-import-user-from-acl 2023-08-15 20:45:28 +08:00
pycook 91b886d632 Merge pull request #153 from simontigers/common_setting_format
fix: import_user_from_acl
2023-08-15 20:25:09 +08:00
hu.sima 7740031bd8 fix: import_user_from_acl 2023-08-15 20:19:45 +08:00
pycook 76a41a8e82 Merge branch 'master' of github.com:veops/cmdb 2023-08-15 19:48:11 +08:00
pycook 6843eb57c4 [update] delete roles, users, attributes 2023-08-15 19:47:59 +08:00
wang-liang0615 20caae8263 Merge pull request #152 from veops/dev_ui
前端更新
2023-08-15 19:47:03 +08:00
wang-liang0615 5e61da038f 属性库 2023-08-15 19:34:17 +08:00
wang-liang0615 348a34d862 属性库 2023-08-15 19:26:49 +08:00
wang-liang0615 2471af867a 属性库 2023-08-15 19:21:09 +08:00
wang-liang0615 775f65ba7b 属性库 2023-08-15 19:10:26 +08:00
wang-liang0615 ebd0b1dc2e 后台管理-模型关联 关系删除&&筛选 2023-08-15 15:02:46 +08:00
pycook 14b5119d94 update gitattributes 2023-08-15 13:41:45 +08:00
wang-liang0615 45167225c9 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 e576d41497 Merge pull request #148 from simontigers/common_setting_format
fix: default arg value
2023-08-10 19:31:18 +08:00
pycook 4c33d6dee9 Merge pull request #149 from veops/dev_ui
ui更新:password
2023-08-10 19:28:24 +08:00
EvanSung 74de3ec0b3 refactor(fe): reduce the width of resource mgt table 2023-08-10 19:23:41 +08:00
wang-liang0615 5e5ae296e0 Merge branch 'master' of github.com:veops/cmdb into dev_ui 2023-08-10 19:21:28 +08:00
wang-liang0615 e81606aa81 增加密码明文传输 2023-08-10 19:21:10 +08:00
hu.sima dc6da9ba2a fix: default arg value 2023-08-10 19:05:56 +08:00
pycook d40e69aa30 Merge pull request #147 from simontigers/common_setting_format
fix: remove useless
2023-08-10 19:01:25 +08:00
hu.sima 4cecdb10fb fix: remove useless 2023-08-10 18:55:32 +08:00
pycook ddf02213db Merge pull request #146 from simontigers/common_setting_format
Common setting format
2023-08-10 18:23:24 +08:00
hu.sima 28dea81036 fix: remove unused column 2023-08-10 16:29:52 +08:00
hu.sima 9756f70044 style: format common setting 2023-08-10 15:30:01 +08:00
pycook 88cea199c1 Merge pull request #145 from EvanSung/optimize_20230810_auth_require
optimize(auth): auth request json
2023-08-10 11:24:23 +08:00
EvanSung 684c1ab924 optimize(auth): auth request json 2023-08-10 10:43:59 +08:00
pycook cb01b577a5 fix celery config 2023-08-08 16:33:24 +08:00
pycook 694abf78b3 Merge branch 'master' of github.com:veops/cmdb 2023-08-08 13:16:14 +08:00
pycook 7e38dd8fab upgrade celery 2023-08-08 13:16:07 +08:00
pycook 7afefe1070 Merge pull request #144 from veops/dev_ui
UI更新:fix preferenceList=>attrList
2023-08-08 09:21:10 +08:00
wang-liang0615 31a4cb62d8 Merge branch 'master' of github.com:veops/cmdb into dev_ui 2023-08-08 09:11:24 +08:00
wang-liang0615 79a2fd32c1 fix preferenceList=>attrList 2023-08-08 09:11:03 +08:00
pycook ef09497d67 upgrade flask to 2.3.2 and replace g.user with current_user 2023-08-06 21:54:18 +08:00
pycook 51c6d50b38 Merge pull request #138 from lovvvve/fix_ldap
fix ldap login
2023-08-04 11:31:58 +08:00
pycook edb74d5790 Merge pull request #142 from veops/dev_ui
ci 批量更新和删除的异步处理
2023-08-04 09:27:55 +08:00
wang-liang0615 9b7d6d8f12 Merge branch 'master' of github.com:veops/cmdb into dev_ui 2023-08-03 16:54:47 +08:00
wang-liang0615 61e178530f ci 批量更新和删除的异步处理 2023-08-03 16:54:27 +08:00
pycook 3632f5f4a6 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 cd11b002ee fix(trigger): session invalid issue 2023-08-02 18:22:42 +08:00
lovvvve 883d7776e1 fix ldap login 2023-08-01 11:27:29 +00:00
pycook 695e9f7546 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 c3cbdc606f Merge pull request #135 from simontigers/remove_pandas
fix: remove pandas
2023-08-01 15:55:15 +08:00
hu.sima 5cb5650cde fix: remove pandas 2023-08-01 15:32:44 +08:00
dependabot[bot] 8d95092b52 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 458b1d144b Merge branch 'master' of github.com:veops/cmdb 2023-08-01 13:47:34 +08:00
pycook ec0f3d5a9f fix dependabot alerts 2023-08-01 13:47:11 +08:00
pycook 8c21df189f fix dependabot alerts 2023-08-01 13:46:47 +08:00
pycook a882a62d1c 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 d7d2e127d3 Merge pull request #132 from veops/dev_ui
删除角色相关
2023-07-31 19:54:19 +08:00
wang-liang0615 82f4bb5b65 删除角色相关 2023-07-31 19:52:06 +08:00
pycook 96ba04dc31 Merge branch 'master' of github.com:veops/cmdb 2023-07-31 18:39:46 +08:00
pycook 0263842b70 fix delete ci_type 2023-07-31 18:39:33 +08:00
pycook c0443a4e3f Merge pull request #131 from veops/dev_ui
前端acl
2023-07-28 18:03:36 +08:00
wang-liang0615 10b7f9dfcb common-setting 2023-07-27 15:47:13 +08:00
wang-liang0615 c2cca38f1c acl 2023-07-27 15:30:27 +08:00
wang-liang0615 742cd4ead1 fix acl change page size 2023-07-27 15:08:25 +08:00
dependabot[bot] 5e0ea75fae 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 7715d52e33 Merge pull request #129 from veops/dev_ui
前端更新
2023-07-25 18:19:47 +08:00
wang-liang0615 bd72abba10 Merge branch 'master' of github.com:veops/cmdb into dev_ui 2023-07-25 13:11:03 +08:00
wang-liang0615 8dc437bc81 授权高亮提示 2023-07-25 13:10:45 +08:00
pycook 5d299bd71a add command cmdb-index-table-upgrade 2023-07-25 10:31:30 +08:00
wang-liang0615 3d3453e257 style 新建属性行错乱 2023-07-25 10:18:22 +08:00
pycook c196c75985 废弃3个表: c_value_datetime c_value_floats c_value_integers, time类型属性值增加写入校验 2023-07-24 21:55:00 +08:00
pycook 7443f96813 禁止删除唯一标识的属性 2023-07-21 15:58:41 +08:00
pycook ea6c5c8566 Merge branch 'master' of github.com:veops/cmdb 2023-07-20 18:37:14 +08:00
pycook c4caa464d3 fix docker-compose 2023-07-20 18:36:32 +08:00
pycook 648c1cad68 Merge pull request #127 from veops/dev_ui
fix currentValueType
2023-07-20 15:39:11 +08:00
wang-liang0615 095d3b2cfb fix currentValueType 2023-07-20 15:30:12 +08:00
pycook d8a62fc885 更新架构图 2023-07-20 11:01:25 +08:00
pycook ccf96b17bc update readme 2023-07-20 11:01:25 +08:00
pycook ae99d7f909 update readme 2023-07-20 11:01:25 +08:00
pycook c570b5b61f lint 2023-07-20 11:01:25 +08:00
wang-liang0615 60aec1f9ef 角色授权 2023-07-20 11:01:25 +08:00
pycook 9ec590664b 清理空间 2023-07-20 11:01:25 +08:00
songbing01249 49d612d8db fix(ci_type_group_manager): fix resources issues 2023-07-20 11:01:25 +08:00
pycook b9d83f7539 update cmdb_api.md 2023-07-20 10:56:58 +08:00
wang-liang0615 158d9f8a76 删除fullscreen相关代码 2023-07-19 17:46:27 +08:00
wang-liang0615 4eff09900d format 2023-07-19 15:36:46 +08:00
wang-liang0615 23ae320223 ops table getVxetableRef 2023-07-19 14:39:57 +08:00
wang-liang0615 a22324ca49 编译 acl 2023-07-19 13:52:24 +08:00
wang-liang0615 d0e7198b9c Merge branch 'master' of github.com:veops/cmdb into dev_ui 2023-07-18 15:16:50 +08:00
wang-liang0615 14c6d7178c acl 样式升级 2023-07-18 15:14:35 +08:00
pycook 3e830f266b Merge pull request #119 from veops/dev_ui
模型属性 is_index
2023-07-17 18:15:28 +08:00
wang-liang0615 67d7062f39 Merge branch 'master' of github.com:veops/cmdb into dev_ui 2023-07-17 17:21:26 +08:00
wang-liang0615 e31fee9f6d 模型属性 is_index 2023-07-17 17:19:44 +08:00
pycook 61b5144362 update local.md 2023-07-17 13:32:34 +08:00
pycook 154f84d64e Merge pull request #118 from veops/dev_ui
Dev UI
2023-07-14 18:06:02 +08:00
wang-liang0615 b0c21fe735 删除usedfc 2023-07-14 15:20:58 +08:00
wang-liang0615 328105ed05 删除usedfc 2023-07-14 15:20:36 +08:00
wang-liang0615 1436ffb3b2 acl 前端 2023-07-14 14:34:35 +08:00
pycook abc83db77b PyJWT==2.4.0 2023-07-13 17:07:33 +08:00
pycook 806f5af830 Merge pull request #117 from lovvvve/patch-4
Update click_cmdb.py
2023-07-13 15:51:36 +08:00
lovvvve c6e150a3df Update click_cmdb.py
add-user remove --is_admin
2023-07-13 15:49:35 +08:00
pycook 41ae241deb Merge pull request #116 from lovvvve/patch-3
Update click_cmdb.py
2023-07-13 15:23:24 +08:00
lovvvve 7c83f9dca7 Update click_cmdb.py
add-user  remove --is_admin
2023-07-13 15:18:47 +08:00
pycook 77199c1b69 update README 2023-07-13 09:34:17 +08:00
pycook 15941f3ee3 Merge pull request #115 from veops/doc
change screenshot image
2023-07-12 17:22:58 +08:00
ivonGwy a644783b7c change screenshot image 2023-07-12 17:21:10 +08:00
pycook 2c9f33b851 Merge pull request #114 from veops/doc
change image size
2023-07-12 17:11:20 +08:00
ivonGwy 8b3f10dece change image size 2023-07-12 17:07:17 +08:00
pycook 9665294dd9 Merge pull request #113 from veops/doc
add qrcode for gzh
2023-07-12 16:49:12 +08:00
ivonGwy 2048202efb add qrcode for gzh 2023-07-12 16:12:17 +08:00
pycook 1861d24593 update requirements 2023-07-12 15:32:46 +08:00
pycook 3e51f89c5a update docker-compose 2023-07-12 11:59:51 +08:00
pycook e02d92c335 remove .gitattributes 2023-07-12 11:50:05 +08:00
pycook 7abc1ad581 md format 2023-07-12 10:14:47 +08:00
pycook e45070ebdc Update README.md 2023-07-12 10:09:11 +08:00
pycook 2ef4c438e2 Update README.md 2023-07-12 10:01:15 +08:00
pycook 64eab86497 docker-compose 构建后的默认账号密码 2023-07-11 19:40:40 +08:00
pycook 4495f006c0 docker-compose is ok 2023-07-11 18:22:17 +08:00
pycook 6c56757a9d docker-compose is ok 2023-07-11 18:12:22 +08:00
pycook 109d4f1a2e 前后端全面升级 2023-07-10 20:13:39 +08:00
pycook e3a3580206 友链Spug 2023-07-10 20:07:31 +08:00
pycook db5ff60aff 前后端全面升级 2023-07-10 20:07:20 +08:00
pycook c444fed436 Merge pull request #90 from lovvvve/patch-2
fix: 🐛 db search
2021-11-10 20:18:18 +08:00
lovvvve 98c7f97274 fix: 🐛 db search
Escape ":" character in SQLAlchemy
2021-11-10 18:56:42 +08:00
pycook 9c76b388c9 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 7936b9cb09 Update ci.py
cmdb-api:add attr check in ci_manager update method
2021-04-21 19:17:05 +08:00
pycook 5256f79ba1 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 3c6cc7f91b Update value.py
value type 是 int 或 float 时 value 值等于 0 是会删除 的 BUG
2021-01-28 16:57:20 +08:00
lovvvve 8a5dadcaa4 Update ci.py
兼容 py2
2021-01-28 16:56:04 +08:00
lovvvve 08fcac6a12 Update value.py 2021-01-28 16:46:19 +08:00
lovvvve 103596238e 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 889d9155bc 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 e6d3c34f75 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 97fc5896a5 Merge pull request #65 from shaohaojiecoder/stable
Stable
2020-12-14 09:20:15 +08:00
shaohaojiecoder c1beffac3b remove weeds 2020-12-13 16:58:06 +08:00
shaohaojiecoder 0cac7fa3f4 delay render 2020-12-13 16:42:17 +08:00
pycook fb9186aa1e yarn.lock update 2020-11-22 11:45:33 +08:00
pycook 9af0009516 Fix github security 2020-11-22 11:42:17 +08:00
pycook 5758b8e999 upgrade ui packages 2020-11-22 11:13:46 +08:00
pycook da29e72d17 Create codeql-analysis.yml 2020-11-22 10:45:45 +08:00
pycook 2bfb1839e5 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 3cc1915ff0 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 66aef97b06 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] 672660330f 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 d6731409b5 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 ed279416c8 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] e8ed71afbc 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] 51321ec86c 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] d73e5e8ef0 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] 53779e037e 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 5b73f69d60 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] 92637f0537 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 6f7b0a3b76 add command: add-user | del-user 2020-06-11 21:37:41 +08:00
pycook 647b11734f 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] e62d71c6c7 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 df90a54ae7 Merge pull request #48 from lovvvve/master
fix(sso login): sso login redirect
2020-06-01 12:05:05 +08:00
Lovvvve 4a1ed9be13 fix(sso login): sso login redirect 2020-06-01 12:01:55 +08:00
Lovvvve a44be48c7e fix(sso login): sso login redirect 2020-06-01 11:48:21 +08:00
pycook 0da1712343 fix merge conflict 2020-05-28 20:39:47 +08:00
pycook f5472c7fa8 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 3a9a966a60 update readme 2020-05-28 20:28:49 +08:00
pycook 91647767ed Fix: spelling mistakes 2020-05-28 20:28:49 +08:00
pycook e69404bada release version 2.1 2020-05-28 20:28:49 +08:00
pycook 332abe223d update readme 2020-04-10 17:22:33 +08:00
pycook 3bb32e1ead Merge branch 'master' of https://github.com/pycook/cmdb 2020-04-07 18:03:03 +08:00
pycook 66476ed254 Fix the judgment of app admin 2020-04-07 18:02:26 +08:00
pycook a4eb221199 Fix: spelling mistakes 2020-04-01 21:40:51 +08:00
pycook 3f53eab514 release version 2.1 2020-04-01 21:20:47 +08:00
pycook ba2a6a65b5 auth with ldap 2020-04-01 20:30:44 +08:00
pycook 486fcac138 UI: batch update relation 2020-04-01 11:09:41 +08:00
pycook cf5e4966c0 add .eslintrc.js 2020-03-26 17:35:26 +08:00
pycook 79536732c1 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 84b7b53a73 fix: delete attribute 2020-03-23 15:49:33 +08:00
dependabot[bot] 6b11b50bd5 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 ccccce262f update deps 2020-03-22 11:14:09 +08:00
pycook d4b3e259c8 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] 450508efee 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 8ea0379a0d Fix: permission management 2020-03-13 10:30:21 +08:00
pycook 6f320d4440 Merge branch 'develop' of https://github.com/pycook/cmdb into develop 2020-03-10 17:03:23 +08:00
pycook 8d7897665e 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 1505d9d948 chore: use wait script to hang api before cache/db/es started 2020-03-10 16:49:40 +08:00
pycook c0ff2ac5b9 Merge pull request #38 from AngrygrayWolf/dev
Change the requirements to support python3.8
2020-03-07 22:06:25 +08:00
what a9aff714d1 Change Pipfile 2020-03-07 21:51:37 +08:00
what 040986df20 Change the requirements to support python3.8 2020-03-07 21:20:46 +08:00
pycook 9ef6d3e955 fix case sensitive of ES search 2020-02-28 14:32:51 +08:00
pycook 28888a04ee Merge branch 'develop' of https://github.com/pycook/cmdb into develop 2020-02-23 23:13:59 +08:00
pycook e7c004fa19 alter table c_preference_relation_views column name varchar(64) 2020-02-23 23:13:22 +08:00
pycook 93c0eefe3c 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 84fcbaefb6 version 2.1 and update readme 2020-02-23 20:21:13 +08:00
pycook b9ef7856fe / redirect to /relation_views 2020-02-23 18:53:28 +08:00
pycook c3b5b30b1f fix i18n 2020-02-23 18:41:23 +08:00
pycook 9c47d9c733 add library future 2020-02-21 23:29:20 +08:00
pycook c305aaaef6 Remove Chinese comments 2020-02-21 23:14:26 +08:00
pycook 2ed10069f1 lint 2020-02-21 22:46:12 +08:00
pycook 8d3756566b The resource view is made into a two-level menu 2020-02-21 22:44:10 +08:00
penzai 91c844ba63 test: add ci and ci relation crud test cases 2020-02-18 22:05:13 +08:00
pycook 93ebc8d2e9 logo left justify 2020-02-16 19:18:51 +08:00
pycook 342a79ed0c Merge pull request #35 from shaohaojiecoder/i18n
I18n
2020-02-16 19:06:58 +08:00
pycook ddc3b564fb Merge pull request #34 from OhBonsai/develop
add test cases
2020-02-16 19:06:24 +08:00
shaohaojiecoder bacda2ed1e Merge branch 'develop' into i18n 2020-02-16 18:19:09 +08:00
shaohaojiecoder 61e6f36e0c add a log pic 2020-02-16 18:18:15 +08:00
penzai b798d5e2cc test: add some test cases 2020-02-16 18:03:33 +08:00
penzai 7b3f95b41d model: allow origin and ticket_id nullable in OperationRecord 2020-02-16 18:03:08 +08:00
penzai 82c921bb88 auth: add user in flask.g when auth by jwt 2020-02-16 18:02:24 +08:00
pycook d975aceaab ACL i18n 2020-02-16 17:36:03 +08:00
pycook a188218818 Merge pull request #33 from shaohaojiecoder/i18n
fix local storage for defalut lang
2020-02-16 14:51:59 +08:00
pycook 1484a308cf modeling i18n 2020-02-16 14:50:17 +08:00
shaohaojiecoder ea0a7df472 fix local storage for defalut lang 2020-02-16 13:47:30 +08:00
pycook b4de2bdb9e Merge pull request #32 from OhBonsai/develop
fix: recycle import by celery task
2020-02-16 10:38:27 +08:00
penzai d2f3589154 fix: recycle import by celery task 2020-02-16 09:39:33 +08:00
pycook 07d2af6671 i18n 2020-02-15 20:57:47 +08:00
pycook acf881dcb7 Merge pull request #31 from shaohaojiecoder/i18n
I18n
2020-02-11 09:50:50 +08:00
pycook cd0797e860 Merge branch 'develop' into i18n 2020-02-11 09:50:11 +08:00
shaohaojiecoder 328d678455 fix meta title 2020-02-09 19:50:23 +08:00
shaohaojiecoder 4e0dc1282f add i18n basic structure 2020-02-09 17:54:57 +08:00
shaohaojiecoder e336b31902 add something 2020-02-09 17:22:17 +08:00
shaohaojiecoder f6c3edfac1 add some 2020-02-08 22:27:56 +08:00
pycook e7f856fecd Define display fields 2020-02-08 17:39:42 +08:00
pycook 780c51c364 Define display fields 2020-02-08 17:36:54 +08:00
shaohaojiecoder ba142266fb add basic 2020-02-07 22:05:52 +08:00
pycook 87deba2e46 fix jwt decode 2020-02-06 09:59:24 +08:00
pycook 048f556936 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 90a97402ef Merge pull request #2 from pycook/master
merge from main repo
2020-02-04 10:56:03 +08:00
penzai 362122952b test: add ci_type test cases 2020-02-04 10:54:16 +08:00
penzai 00f4eeb90d 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 5da5d32b54 Merge branch 'master' of https://github.com/pycook/cmdb 2020-01-19 18:18:43 +08:00
pycook d49cd99881 Merge pull request #28 from shaohaojiecoder/master
fix drag group and attrs
2020-01-19 18:18:23 +08:00
haojie.shao 680a547df4 fix drag group and attrs 2020-01-19 18:14:53 +08:00
pycook 356cf58135 /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 28ff27c019 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 bb9d7da435 Merge remote-tracking branch 'origin/master' 2020-01-17 15:10:16 +08:00
penzai 1fa9bf8880 test: add basic test code and attribute create api test case 2020-01-17 15:08:46 +08:00
Bonsai 75bf42ea3c Merge pull request #1 from pycook/master
merge master
2020-01-17 10:05:05 +08:00
pycook 5931e9879b [fix] cycle import 2020-01-15 11:52:33 +08:00
pycook d82dc10451 flush cache when delete attribute 2020-01-15 09:06:31 +08:00
pycook 705f4916b9 [fix] delete CIType's attribute 2020-01-14 20:52:36 +08:00
pycook 96f4760a92 api docs update 2020-01-06 21:58:06 +08:00
pycook d8d48e6d13 api docs update 2020-01-06 21:54:33 +08:00
pycook 9d3d2e6b00 update README 2019-12-31 22:53:21 +08:00
pycook a3ff843f54 update README 2019-12-31 22:50:01 +08:00
pycook 4a421b4da2 Update README.md
update
2019-12-31 22:37:36 +08:00
pycook e4ecee8264 Merge pull request #24 from fxiang21/master
add Readme of English
2019-12-31 22:32:48 +08:00
fxiang21 1129e110dd add Readme of English 2019-12-31 22:25:42 +08:00
pycook c0a3e39367 Update README.md
[fix] flask db-setup
2019-12-31 11:06:59 +08:00
pycook 934f4cf93e [fix] fuzzy search 2019-12-25 13:36:43 +08:00
pycook 47b712620c [fix] security alerts 2019-12-25 10:19:03 +08:00
pycook 4e7e853173 [fix] validate attribute is required 2019-12-24 20:37:32 +08:00
pycook 03bc27087d disable eslint warning 2019-12-24 15:15:03 +08:00
pycook 317a16aa21 support JSON type 2019-12-23 18:51:33 +08:00
pycook 3a8d4ecebc fix sidebar menu in mobile 2019-12-23 11:58:41 +08:00
pycook b2b355b0dd add yarn.lock 2019-12-23 11:27:47 +08:00
pycook 60814cea2f flask init-acl 2019-12-20 12:57:39 +09:00
pycook 6801058a3f update makefile 2019-12-18 23:36:58 +09:00
pycook 92183423df Modify code organization 2019-12-18 23:33:22 +09:00
pycook ccf1d1c09a catch abort exception when getting relation views 2019-12-13 09:59:38 +08:00
pycook 13e9a0b7bd update 2019-12-12 21:45:19 +08:00
pycook e129b0a32e fix delete relation view 2019-12-12 21:36:33 +08:00
pycook 18122de03e [fix] update attribute which is list 2019-12-11 18:12:10 +08:00
pycook c07f39d669 release 2.0 2019-12-11 12:43:55 +08:00
pycook 81ecffa166 V2.0 2019-12-11 12:14:23 +08:00
pycook 4316880d02 fix relation tree 2019-12-10 15:35:59 +08:00
pycook 9af34d0beb 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 2eb6159e6e fix(ci_type api): fix the judgment condition of deleting ci_type 2019-12-10 14:31:27 +08:00
pycook fc11c5626e relation view bugfix 2019-12-08 00:20:55 +08:00
pycook 5669e253a9 acl done and bugfix 2019-12-06 22:33:31 +08:00
pycook 6973cc68ed ACL: permission management [doing] 2019-12-04 18:14:09 +08:00
pycook e73810b456 update readme 2019-12-04 09:26:01 +08:00
pycook 261f673e17 remove print 2019-12-03 22:13:14 +08:00
pycook b0e6248cd1 fix delete ci relation 2019-12-03 21:57:44 +08:00
pycook c663a2b783 fix get second cis api 2019-12-03 20:10:27 +08:00
pycook 9b9ae61505 relation view has been optimised 2019-12-03 19:10:54 +08:00
pycook 4987908368 version 1.5: update docker file 2019-11-30 23:07:12 +08:00
pycook d492e7188c cmdb.sql update 2019-11-29 22:21:41 +08:00
pycook 92c163971c relation view [done] 2019-11-29 18:11:18 +08:00
pycook e351c7cf36 relation view [doing] 2019-11-28 21:17:06 +08:00
pycook 38b4293a33 relation view define [done] 2019-11-27 18:25:53 +08:00
pycook 8ca93d72f7 GPLv2 2019-11-25 20:35:05 +08:00
pycook f7b94dbe6e change to GPLv2 2019-11-25 20:33:56 +08:00
pycook 92cb9633b0 License change to GPLv3 2019-11-25 19:42:37 +08:00
pycook 2f3b5a03ee UI: relation type define [done] 2019-11-25 19:23:51 +08:00
pycook 47cfce7ee9 /acl/resources add param resource_type_id 2019-11-24 22:33:57 +08:00
pycook 2210e9715b Merge pull request #20 from kdyq007/master
[更新] 新增角色、资源、权限页面
2019-11-24 17:29:58 +08:00
kdyq007 0151e0ff56 Merge branch 'master' of https://github.com/kdyq007/cmdb 2019-11-24 17:21:27 +08:00
kdyq007 bc63548dd8 Merge pull request #6 from pycook/master
fix acl api
2019-11-24 16:43:53 +08:00
pycook 39aaa916dc fix acl api 2019-11-24 16:35:28 +08:00
kdyq007 43bacef520 Merge pull request #5 from pycook/master
同步
2019-11-23 21:53:46 +08:00
pycook a822503b67 fix 2019-11-23 21:50:45 +08:00
pycook 77ed1ddb28 fix acl resource 2019-11-23 17:42:33 +08:00
kdyq007 2d17e29441 Merge pull request #4 from pycook/master
fix acl resource_type
2019-11-23 17:36:42 +08:00
pycook 6c4eefa9c0 fix acl resource_type 2019-11-23 17:24:43 +08:00
kdyq007 a492f97199 Merge pull request #3 from pycook/master
同步
2019-11-23 14:52:41 +08:00
pycook 4c7deee866 relative view api [done] 2019-11-22 18:18:22 +08:00
pycook 31a97d7ad0 Realize /api/v0.1/ci_relations/s [done] 2019-11-21 18:21:03 +08:00
kdyq007 c2e4090aea Merge branch 'master' of https://github.com/kdyq007/cmdb 2019-11-19 21:52:33 +08:00
kdyq007 40e49aa1e9 Merge pull request #2 from pycook/master
update
2019-11-19 21:52:02 +08:00
kdyq007 0bf8bfb113 [更新] 完成基础role和user管理 2019-11-19 21:49:51 +08:00
pycook bc7b51c544 fix get user by uid 2019-11-19 21:46:53 +08:00
pycook 1c6d056d58 acl: resource type api 2019-11-19 21:41:46 +08:00
pycook f3a0761ffc fix search 2019-11-19 18:32:35 +08:00
pycook 2ce8a6bdbf elastic search [done] 2019-11-19 18:16:31 +08:00
pycook 7ae58611c0 es search update 2019-11-18 22:05:59 +08:00
pycook 4ec3b310e4 search by elasticsearch [doing] 2019-11-18 20:02:25 +08:00
kdyq007 bd34023512 Merge pull request #1 from pycook/master
怎么玩的?反向pull request
2019-11-18 18:31:14 +08:00
pycook 2205c359b8 fix acl api 2019-11-18 12:02:02 +08:00
kdyq007 2723f187ac [更新] 完成roles基本接口 2019-11-17 17:09:24 +08:00
pycook bb91f21e40 pep8 2019-11-15 18:03:06 +08:00
pycook f1c2bcbd79 update acl 2019-11-15 16:54:56 +08:00
pycook a8255d2b69 Merge pull request #18 from kdyq007/master
[更新] 修改图片路径、压缩图片
2019-11-14 21:59:38 +08:00
qiqi 22c02f94e0 [更新] 优化格式 2019-11-14 21:51:58 +08:00
qiqi e1a5c01438 [更新] 更换图片位置、压缩图片 2019-11-14 21:48:36 +08:00
qiqi fd21dfe94d [更新] 优化说明文件格式 2019-11-14 21:00:24 +08:00
qiqi 4bc6492b21 [更新] 新增Q群 README.md 2019-11-14 20:55:48 +08:00
pycook eaa5cb8bf1 update acl 2019-11-14 18:35:31 +08:00
pycook 47c66be179 update readme 2019-11-13 14:02:02 +08:00
pycook 089a17ded8 python3.7 timezone fix 2019-11-13 13:56:44 +08:00
pycook cb0eb7eadd update acl 2019-11-13 13:25:42 +08:00
pycook 82a402883b docker images use aliyun 2019-11-13 11:56:17 +08:00
pycook 00f6e8e7d9 update Makefile and support for install by make 2019-11-12 11:55:04 +08:00
pycook 63cae8510f fix py3 2019-11-12 11:15:25 +08:00
pycook 62b56da0d8 merge Dockerfile 2019-11-12 10:40:37 +08:00
pycook 79a19d4b49 delete docs/Dockerfile 2019-11-11 23:12:50 +08:00
pycook f91c82b241 fix timezone 2019-11-11 23:11:12 +08:00
pycook 89ffec35e8 update README 2019-11-11 16:10:02 +08:00
pycook 2935ca5923 flask init-cache 2019-11-11 15:46:57 +08:00
pycook 46213dd758 Remove package-lock.json and remove some compile warnings 2019-11-11 13:16:07 +08:00
pycook aee66c075b add command init-cache 2019-11-11 11:27:43 +08:00
pycook 29e80f8597 Merge branch 'master' of https://github.com/pycook/cmdb 2019-11-11 09:20:07 +08:00
pycook 618cd7ff16 vue lint 2019-11-11 00:25:22 +08:00
pycook de22a4e04b gunicorn==19.5.0 2019-11-10 19:10:23 +08:00
pycook 3fdfa4dc92 Docker to production 2019-11-10 19:06:38 +08:00
pycook 7da14f6aa0 Partially completed backend development of permissions management 2019-11-08 17:42:13 +08:00
pycook 046d0e60b7 Update README.md
docker 一键安装说明补充
2019-11-08 15:26:22 +08:00
pycook dc18e3b82f fix py2.7 unicode encoding error 2019-11-08 15:15:31 +08:00
pycook cb8ef65e79 users drop is_admin 2019-11-08 14:58:21 +08:00
pycook 341a4359fa fix unicode encode error 2019-11-08 14:37:53 +08:00
fxiang21 f89169de96 移除多余的docker-start目录 2019-11-08 09:20:34 +08:00
fxiang21 47767e22d8 修复nginx转发问题 2019-11-08 09:20:27 +08:00
fxiang21 5f389459e8 添加容器化部署方式 2019-11-08 09:20:09 +08:00
pycook 02ea6afa9c code format 2019-11-07 19:18:31 +08:00
pycook 47692c16ef Update README.md
pipenv run flask run -h 0.0.0.0
2019-11-05 17:52:45 +08:00
pycook d9a2cff82a Update README.md
如果是非本机访问, 要修改ui/.env里VUE_APP_API_BASE_URL里的IP地址
2019-11-05 17:44:48 +08:00
pycook f05aec3d16 Update README.md
如果是非本机访问, 要修改ui/.env里VUE_APP_API_BASE_URL里的IP地址
2019-11-05 17:43:40 +08:00
pycook ed54fca4b8 update overview jpeg url 2019-11-01 11:37:16 +08:00
pycook 538cc3debf fix add integer list 2019-11-01 11:27:24 +08:00
pycook 2bfe6f0fe1 mkdir logs, ignore *.log 2019-11-01 10:45:35 +08:00
pycook 802a995cbb Update README.md
add cmdb.sql
2019-10-28 21:48:46 +08:00
pycook 8d13a7630f add docs/cmdb.sql 2019-10-28 21:46:41 +08:00
pycook c80291a1b3 fix date picker 2019-10-24 20:43:59 +08:00
pycook 77ad772d95 attribute alias must be unique 2019-10-24 20:43:58 +08:00
pycook 174375d3bf fix attributes paginate 2019-10-24 20:43:54 +08:00
pycook f00d97522a update README 2019-10-24 20:43:51 +08:00
pycook 93df278328 Update README.md
create tables fix
2019-10-24 20:43:51 +08:00
pycook f592af0ff4 attributes paginate and fix update value 2019-10-24 20:43:51 +08:00
pycook 872d9baa59 add docs 2019-10-24 20:43:51 +08:00
pycook 56baa2bedc ci search return unique key 2019-10-24 20:43:51 +08:00
pycook 2615fd7661 invalid username or password -> 403 2019-10-24 20:43:51 +08:00
pycook 9b609724bd cache强制unicode编码 2019-08-30 09:46:24 +08:00
pycook f3aacd72ee add .gitattributes 2019-08-28 21:08:28 +08:00
pycook 3bead5438d add ui 2019-08-28 20:51:51 +08:00
pycook 6ef1df9f4f remove ui 2019-08-28 20:48:04 +08:00
pycook c7ab0c2beb update README 2019-08-28 20:45:59 +08:00
pycook 20fd6393e4 升级后端并开源UI 2019-08-28 20:34:10 +08:00
pycook 02c01f60bf pep8 2016-06-27 10:50:32 +08:00
573 changed files with 86571 additions and 42021 deletions

6
.env Normal file
View File

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

0
.github/config.yml vendored
View File

View File

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

4
.gitignore vendored
View File

@ -43,7 +43,8 @@ cmdb-api/api/uploaded_files
cmdb-api/migrations/versions
# Translations
*.mo
#*.mo
messages.pot
# Mr Developer
.mr.developer.cfg
@ -70,6 +71,7 @@ settings.py
# UI
cmdb-ui/node_modules
cmdb-ui/dist
cmdb-ui/yarn.lock
# Log files
cmdb-ui/npm-debug.log*

13
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,13 @@
# 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,6 +1,4 @@
MYSQL_ROOT_PASSWORD ?= root
MYSQL_PORT ?= 3306
REDIS_PORT ?= 6379
include ./Makefile.variable
default: help
help: ## display this help
@ -50,3 +48,25 @@ clean: ## remove unwanted files like .pyc's
lint: ## check style with flake8
flake8 --exclude=env .
.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 \
.

21
Makefile.variable Normal file
View File

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

170
README.md
View File

@ -1,94 +1,138 @@
<p align="center">
<a href="https://veops.cn"><img src="docs/images/logo.png" alt="维易CMDB" width="300"/></a>
<a href="https://veops.cn">
<img src="https://github.com/user-attachments/assets/c5cfb272-899b-418d-9e69-8e1dd07db0f6" alt="维易CMDB"/>
</a>
</p>
<h3 align="center">简单、轻量、通用的运维配置管理数据库</h3>
<h4 align="center">简单、轻量、通用的运维配置管理数据库</h4>
<p align="center">
<a href="https://github.com/veops/cmdb/blob/master/LICENSE"><img src="https://img.shields.io/badge/License-AGPLv3-brightgreen" alt="License: GPLv3"></a>
<a href="https:https://github.com/sendya/ant-design-pro-vue"><img src="https://img.shields.io/badge/UI-Ant%20Design%20Pro%20Vue-brightgreen" alt="UI"></a>
<a href="https://github.com/pallets/flask"><img src="https://img.shields.io/badge/API-Flask-brightgreen" alt="API"></a>
<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>
------------------------------
[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
> **重要提示**: `master` 分支在开发过程中可能处于 _不稳定的状态_
> 请通过[releases](https://github.com/veops/cmdb/releases)获取
## 系统介绍
### 系统概览
维易CMDB是一个简洁、轻量且高度可定制的运维配置管理数据库CMDB。它支持灵活的模型配置和资源自动发现旨在为企业提供便捷的资产管理解决方案帮助运维团队高效地管理 IT 基础设施和服务。
<img src=docs/images/dashboard.png />
[查看更多展示](docs/screenshot.md)
### 相关文章
- <a href="https://mp.weixin.qq.com/s/v3eANth64UBW5xdyOkK3tg" target="_blank">概要设计</a>
- <a href="https://github.com/veops/cmdb/tree/master/docs/cmdb_api.md" target="_blank">API 文档</a>
- <a href="https://mp.weixin.qq.com/s/rQaf4AES7YJsyNQG_MKOLg" target="_blank">自动发现</a>
- 更多文章可以在公众号 **维易科技OneOps** 里查看
### 特点
- 灵活性
1. 配置灵活,不设定任何运维场景,有内置模板
2. 自动发现、入库 IT 资产
- 安全性
1. 细粒度权限控制
2. 完备操作日志
- 多应用
1. 丰富视图展示维度
2. API简单强大
3. 支持定义属性触发器、计算属性
- 产品文档:[https://veops.cn/docs/](https://veops.cn/docs/)
- 在线体验:[https://cmdb.veops.cn](https://cmdb.veops.cn)
- 用户名demo 或者 admin
- 密码123456
- **重要提示**`master` 分支在开发过程中可能处于**不稳定的状态**。请通过 [releases](https://github.com/veops/cmdb/releases) 获取最新稳定版本。
### 主要功能
- 模型属性支持索引、多值、默认排序、字体颜色,支持计算属性
- 支持自动发现、定时巡检、文件导入
- 支持资源、层级、关系视图展示
- 支持模型间关系配置和展示
- 细粒度访问控制,完备的操作日志
- 支持跨模型搜索
- **自定义模型和模型关系**:支持模型属性的自定义,包括下拉列表、字体颜色、计算属性等高级功能,满足不同业务需求。
- **自动发现资源**:支持计算机、网络设备、存储设备、数据库、中间件、公有云资源等自动发现。
- **多维度视图展示**:包括资源视图、层级视图、关系视图等,帮助运维人员全面管理资源。
- **细粒度权限控制**:通过精确的访问控制和完备的操作日志保障系统的安全性。
- **全面的资源搜索功能**:支持灵活的资源和关系搜索,快速定位和操作资源。
- **集成 IP 地址管理IPAM和数据中心基础设施管理DCIM**:简化网络资源和数据中心设备的管理。
更多详细功能,请移步 [维易科技官网](https://veops.cn) 进行了解。
### 系统优势
- 灵活性
+ 无需指定固定运维场景,支持自由配置并内置多种模板
+ 支持自动发现和入库 IT 资产,快速搭建资产管理系统
- 安全性
+ 细粒度的权限控制机制,确保资源管理的安全性
+ 完整的操作日志记录,便于审计和问题追踪
- 多应用
+ 提供多种视图展示方式,满足不同场景的需求
+ 强大的 API 接口,支持深度集成
+ 支持定义属性触发器和计算属性,增强数据处理能力
### 技术栈
### 更多功能
+ 后端Python [3.8-3.11]
+ 数据存储MySQL、Redis
+ 前端Vue.js
+ UI组件库Ant Design Vue
> 也欢迎移步[维易科技官网](https://veops.cn),发现更多免费运维系统。
### 系统概览
<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>
<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>
## 关注我们
欢迎 Star 加关注,第一时间获取更新动态!
![star us](https://github.com/user-attachments/assets/f9056d5a-171c-4f53-9fec-d40c9e5ff94d)
## 快速开始
### 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 的开源。
### Docker 一键快速构建
- 进入主目录(先安装 docker 环境, 注意要clone整个项目
<a href="https://github.com/veops/cmdb/graphs/contributors">
<img src="https://contrib.rocks/image?repo=veops/cmdb" />
</a>
```
docker-compose up -d
```
## 更多开源
- 浏览器打开: [http://127.0.0.1:8000](http://127.0.0.1:8000)
- username: demo 或者 admin
- password: 123456
- [OneTerm](https://github.com/veops/oneterm): 一款简单、轻量、灵活的堡垒机服务。
- [messenger](https://github.com/veops/messenger): 一个简单轻量的消息发送服务。
- [ACL](https://github.com/veops/acl): 一个简单通用的权限管理系统设计与实践。
### [本地开发环境搭建](docs/local.md)
## 相关文章
### [Makefile 安装](docs/makefile.md)
- <a href="https://mp.weixin.qq.com/s/v3eANth64UBW5xdyOkK3tg" target="_blank">尽可能通用的运维CMDB的设计与实践() - 概览</a>
- <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** 里查看
_**欢迎关注公众号(维易科技OneOps),关注后可加入微信群,进行产品和技术交流。**_
## 与我联系
![公众号: 维易科技OneOps](docs/images/wechat.png)
+ 邮箱: <a href="mailto:bd@veops.cn">bd@veops.cn</a>
+ 公众号:**维易科技OneOps**。关注后可以加入微信群,参与产品和技术交流
<img src="docs/images/wechat.png" alt="公众号: 维易科技OneOps" />

79
cmdb-api/.ruff.toml Normal file
View File

@ -0,0 +1,79 @@
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,35 +5,38 @@ name = "pypi"
[packages]
# Flask
Flask = "==2.3.2"
Werkzeug = "==2.3.6"
Flask = "==2.2.5"
Werkzeug = "==2.2.3"
click = ">=5.0"
# Api
Flask-RESTful = "==0.3.10"
# Database
Flask-SQLAlchemy = "==2.5.0"
Flask-SQLAlchemy = "==3.0.5"
SQLAlchemy = "==1.4.49"
PyMySQL = "==1.1.0"
redis = "==4.6.0"
python-redis-lock = "==4.0.0"
# Migrations
Flask-Migrate = "==2.5.2"
# Deployment
gunicorn = "==21.0.1"
supervisor = "==4.0.3"
# Auth
Flask-Login = "==0.6.2"
Flask-Login = ">=0.6.2"
Flask-Bcrypt = "==1.0.1"
Flask-Cors = ">=3.0.8"
ldap3 = "==2.9.1"
pycryptodome = "==3.12.0"
cryptography = ">=41.0.2"
# i18n
flask-babel = "==4.0.0"
# Caching
Flask-Caching = ">=1.0.0"
# Environment variable parsing
environs = "==4.2.0"
marshmallow = "==2.20.2"
# async tasks
celery = ">=5.3.1"
celery = "==5.3.1"
celery_once = "==3.0.1"
more-itertools = "==5.0.0"
kombu = ">=5.3.1"
@ -43,7 +46,7 @@ WTForms = "==3.0.0"
email-validator = "==1.3.1"
treelib = "==1.6.1"
flasgger = "==0.9.5"
Pillow = "==9.3.0"
Pillow = ">=10.0.1"
# other
six = "==1.16.0"
bs4 = ">=0.0.1"
@ -62,6 +65,11 @@ 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]
# Testing

View File

@ -12,14 +12,17 @@ from pathlib import Path
from flask import Flask
from flask import jsonify
from flask import make_response
from flask import request
from flask.blueprints import Blueprint
from flask.cli import click
from flask.json.provider import DefaultJSONProvider
from flask_babel.speaklater import LazyString
import api.views.entry
from api.extensions import (bcrypt, cache, celery, cors, db, es, login_manager, migrate, rd)
from api.extensions import (bcrypt, babel, 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
@ -71,7 +74,7 @@ class ReverseProxy(object):
class MyJSONEncoder(DefaultJSONProvider):
def default(self, o):
if isinstance(o, (decimal.Decimal, datetime.date, datetime.time)):
if isinstance(o, (decimal.Decimal, datetime.date, datetime.time, LazyString)):
return str(o)
if isinstance(o, datetime.datetime):
@ -96,6 +99,7 @@ def create_app(config_object="settings"):
register_shell_context(app)
register_commands(app)
CAS(app)
OAuth2(app)
app.wsgi_app = ReverseProxy(app.wsgi_app)
configure_upload_dir(app)
@ -115,7 +119,13 @@ def configure_upload_dir(app):
def register_extensions(app):
"""Register Flask extensions."""
def get_locale():
accept_languages = app.config.get('ACCEPT_LANGUAGES', ['en', 'zh'])
return request.accept_languages.best_match(accept_languages)
bcrypt.init_app(app)
babel.init_app(app, locale_selector=get_locale)
cache.init_app(app)
db.init_app(app)
cors.init_app(app)
@ -192,10 +202,11 @@ def configure_logger(app):
app.logger.addHandler(handler)
log_file = app.config['LOG_PATH']
file_handler = RotatingFileHandler(log_file,
maxBytes=2 ** 30,
backupCount=7)
file_handler.setLevel(getattr(logging, app.config['LOG_LEVEL']))
file_handler.setFormatter(formatter)
app.logger.addHandler(file_handler)
if log_file and log_file != "/dev/stdout":
file_handler = RotatingFileHandler(log_file,
maxBytes=2 ** 30,
backupCount=7)
file_handler.setLevel(getattr(logging, app.config['LOG_LEVEL']))
file_handler.setFormatter(formatter)
app.logger.addHandler(file_handler)
app.logger.setLevel(getattr(logging, app.config['LOG_LEVEL']))

View File

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

View File

@ -1,13 +1,13 @@
# -*- coding:utf-8 -*-
import click
import copy
import datetime
import json
import time
import click
import requests
import time
import uuid
from flask import current_app
from flask.cli import with_appcontext
from flask_login import login_user
@ -19,9 +19,11 @@ from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import REDIS_PREFIX_CI
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION2
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.const import RoleEnum
from api.lib.cmdb.const import ValueTypeEnum
from api.lib.cmdb.dcim.rack import RackManager
from api.lib.exception import AbortException
from api.lib.perm.acl.acl import ACLManager
from api.lib.perm.acl.acl import UserCache
@ -29,18 +31,20 @@ from api.lib.perm.acl.cache import AppCache
from api.lib.perm.acl.resource import ResourceCRUD
from api.lib.perm.acl.resource import ResourceTypeCRUD
from api.lib.perm.acl.role import RoleCRUD
from api.lib.perm.acl.user import UserCRUD
from api.lib.secrets.inner import KeyManage
from api.lib.secrets.inner import global_key_threshold
from api.lib.secrets.secrets import InnerKVManger
from api.models.acl import App
from api.models.acl import ResourceType
from api.models.cmdb import Attribute
from api.models.cmdb import AttributeHistory
from api.models.cmdb import CI
from api.models.cmdb import CIRelation
from api.models.cmdb import CIType
from api.models.cmdb import CITypeTrigger
from api.models.cmdb import OperationRecord
from api.models.cmdb import PreferenceRelationView
from api.tasks.cmdb import batch_ci_cache
@click.command()
@ -50,12 +54,20 @@ def cmdb_init_cache():
ci_relations = CIRelation.get_by(to_dict=False)
relations = dict()
relations2 = dict()
for cr in ci_relations:
relations.setdefault(cr.first_ci_id, {}).update({cr.second_ci_id: cr.second_ci.type_id})
if cr.ancestor_ids:
relations2.setdefault('{},{}'.format(cr.ancestor_ids, cr.first_ci_id), {}).update(
{cr.second_ci_id: cr.second_ci.type_id})
for i in relations:
relations[i] = json.dumps(relations[i])
for i in relations2:
relations2[i] = json.dumps(relations2[i])
if relations:
rd.create_or_update(relations, REDIS_PREFIX_CI_RELATION)
if relations2:
rd.create_or_update(relations2, REDIS_PREFIX_CI_RELATION2)
es = None
if current_app.config.get("USE_ES"):
@ -109,10 +121,20 @@ def cmdb_init_acl():
_app = AppCache.get('cmdb') or App.create(name='cmdb')
app_id = _app.id
current_app.test_request_context().push()
# 1. add resource type
for resource_type in ResourceTypeEnum.all():
try:
ResourceTypeCRUD.add(app_id, resource_type, '', PermEnum.all())
perms = PermEnum.all()
if resource_type in (ResourceTypeEnum.CI_FILTER, ResourceTypeEnum.PAGE):
perms = [PermEnum.READ]
elif resource_type == ResourceTypeEnum.CI_TYPE_RELATION:
perms = [PermEnum.ADD, PermEnum.DELETE, PermEnum.GRANT]
elif resource_type 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:
pass
@ -128,10 +150,10 @@ def cmdb_init_acl():
# 3. add resource and grant
ci_types = CIType.get_by(to_dict=False)
type_id = ResourceType.get_by(name=ResourceTypeEnum.CI, first=True, to_dict=False).id
resource_type_id = ResourceType.get_by(name=ResourceTypeEnum.CI, first=True, to_dict=False).id
for ci_type in ci_types:
try:
ResourceCRUD.add(ci_type.name, type_id, app_id)
ResourceCRUD.add(ci_type.name, resource_type_id, app_id)
except AbortException:
pass
@ -141,10 +163,10 @@ def cmdb_init_acl():
[PermEnum.READ])
relation_views = PreferenceRelationView.get_by(to_dict=False)
type_id = ResourceType.get_by(name=ResourceTypeEnum.RELATION_VIEW, first=True, to_dict=False).id
resource_type_id = ResourceType.get_by(name=ResourceTypeEnum.RELATION_VIEW, first=True, to_dict=False).id
for view in relation_views:
try:
ResourceCRUD.add(view.name, type_id, app_id)
ResourceCRUD.add(view.name, resource_type_id, app_id)
except AbortException:
pass
@ -154,57 +176,6 @@ def cmdb_init_acl():
[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()
@with_appcontext
def cmdb_counter():
@ -214,12 +185,34 @@ def cmdb_counter():
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:
try:
db.session.remove()
db.session.commit()
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:
import traceback
print(traceback.format_exc())
@ -235,6 +228,12 @@ def cmdb_trigger():
"""
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")
trigger2cis = dict()
trigger2completed = dict()
@ -263,10 +262,10 @@ def cmdb_trigger():
trigger2cis[trigger.id] = (trigger, ready_cis)
else:
cur = trigger2cis[trigger.id]
cur_ci_ids = {i.ci_id for i in cur[1]}
cur_ci_ids = {_ci.ci_id for _ci in cur[1]}
trigger2cis[trigger.id] = (
trigger, cur[1] + [i for i in ready_cis if i.ci_id not in cur_ci_ids
and i.ci_id not in trigger2completed.get(trigger.id, {})])
trigger, cur[1] + [_ci for _ci in ready_cis if _ci.ci_id not in cur_ci_ids
and _ci.ci_id not in trigger2completed.get(trigger.id, {})])
for tid in trigger2cis:
trigger, cis = trigger2cis[tid]
@ -329,7 +328,6 @@ def valid_address(address):
}
KeyManage.print_response(response)
return False
return True
@ -344,7 +342,7 @@ def cmdb_inner_secrets_init(address):
"""
init inner secrets for password feature
"""
res, ok = KeyManage(backend=InnerKVManger).init()
res, ok = KeyManage(backend=InnerKVManger()).init()
if not ok:
if res.get("status") == "failed":
KeyManage.print_response(res)
@ -354,7 +352,7 @@ def cmdb_inner_secrets_init(address):
if valid_address(address):
token = current_app.config.get("INNER_TRIGGER_TOKEN", "") if not token else token
if not token:
token = click.prompt(f'Enter root token', hide_input=True, confirmation_prompt=False)
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})
@ -378,13 +376,13 @@ def cmdb_inner_secrets_unseal(address):
"""
unseal the secrets feature
"""
if not valid_address(address):
return
# 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})
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"]:
@ -444,6 +442,7 @@ def cmdb_password_data_migrate():
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)
@ -454,6 +453,7 @@ def cmdb_password_data_migrate():
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 == '******':
@ -464,8 +464,124 @@ def cmdb_password_data_migrate():
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

@ -4,15 +4,13 @@ from flask.cli import with_appcontext
from werkzeug.datastructures import MultiDict
from api.lib.common_setting.acl import ACLManager
from api.lib.common_setting.employee import EmployeeAddForm
from api.lib.common_setting.employee import EmployeeAddForm, GrantEmployeeACLPerm
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
class InitEmployee(object):
"""
初始化员工
"""
def __init__(self):
self.log = current_app.logger
@ -58,7 +56,8 @@ class InitEmployee(object):
self.log.error(ErrFormat.acl_import_user_failed.format(user['username'], str(e)))
self.log.error(e)
def get_rid_by_uid(self, uid):
@staticmethod
def get_rid_by_uid(uid):
from api.models.acl import Role
role = Role.get_by(first=True, uid=uid)
return role['id'] if role is not None else 0
@ -71,7 +70,8 @@ class InitDepartment(object):
def init(self):
self.init_wide_company()
def hard_delete(self, department_id, department_name):
@staticmethod
def hard_delete(department_id, department_name):
existed_deleted_list = Department.query.filter(
Department.department_name == department_name,
Department.department_id == department_id,
@ -80,11 +80,12 @@ class InitDepartment(object):
for existed in existed_deleted_list:
existed.delete()
def get_department(self, department_name):
@staticmethod
def get_department(department_name):
return Department.query.filter(
Department.department_name == department_name,
Department.deleted == 0,
).order_by(Department.created_at.asc()).first()
).first()
def run(self, department_id, department_name, department_parent_id):
self.hard_delete(department_id, department_name)
@ -94,7 +95,7 @@ class InitDepartment(object):
if res.department_id == department_id:
return
else:
new_d = res.update(
res.update(
department_id=department_id,
department_parent_id=department_parent_id,
)
@ -108,11 +109,11 @@ class InitDepartment(object):
new_d = self.get_department(department_name)
if new_d.department_id != department_id:
new_d = new_d.update(
new_d.update(
department_id=department_id,
department_parent_id=department_parent_id,
)
self.log.info(f"初始化 {department_name} 部门成功.")
self.log.info(f"init {department_name} success.")
def run_common(self, department_id, department_name, department_parent_id):
try:
@ -123,19 +124,14 @@ class InitDepartment(object):
raise Exception(e)
def init_wide_company(self):
"""
创建 id 0, name 全公司 的部门
"""
department_id = 0
department_name = '全公司'
department_parent_id = -1
self.run_common(department_id, department_name, department_parent_id)
def create_acl_role_with_department(self):
"""
当前所有部门在ACL创建 role
"""
@staticmethod
def create_acl_role_with_department():
acl = ACLManager('acl')
role_name_map = {role['name']: role for role in acl.get_all_roles()}
@ -146,7 +142,7 @@ class InitDepartment(object):
continue
role = role_name_map.get(department.department_name)
if role is None:
if not role:
payload = {
'app_id': 'acl',
'name': department.department_name,
@ -163,70 +159,26 @@ class InitDepartment(object):
def init_backend_resource(self):
acl = self.check_app('backend')
resources_types = acl.get_all_resources_types()
perms = ['read', 'grant', 'delete', 'update']
acl_rid = self.get_admin_user_rid()
results = list(filter(lambda t: t['name'] == '操作权限', resources_types['groups']))
if len(results) == 0:
payload = dict(
app_id=acl.app_name,
name='操作权限',
description='',
perms=perms
)
resource_type = acl.create_resources_type(payload)
else:
resource_type = results[0]
resource_type_id = resource_type['id']
existed_perms = resources_types.get('id2perms', {}).get(resource_type_id, [])
existed_perms = [p['name'] for p in existed_perms]
new_perms = []
for perm in perms:
if perm not in existed_perms:
new_perms.append(perm)
if len(new_perms) > 0:
resource_type['perms'] = existed_perms + new_perms
acl.update_resources_type(resource_type_id, resource_type)
if acl_rid == 0:
return
GrantEmployeeACLPerm(acl).grant_by_rid(acl_rid, True)
resource_list = acl.get_resource_by_type(None, None, resource_type['id'])
for name in ['公司信息', '公司架构', '通知设置']:
target = list(filter(lambda r: r['name'] == name, resource_list))
if len(target) == 0:
payload = dict(
type_id=resource_type['id'],
app_id=acl.app_name,
name=name,
)
resource = acl.create_resource(payload)
else:
resource = target[0]
if acl_rid > 0:
acl.grant_resource(acl_rid, resource['id'], perms)
def check_app(self, app_name):
@staticmethod
def check_app(app_name):
acl = ACLManager(app_name)
payload = dict(
name=app_name,
description=app_name
)
try:
app = acl.validate_app()
if not app:
acl.create_app(payload)
return acl
except Exception as e:
current_app.logger.error(e)
if '不存在' in str(e):
acl.create_app(payload)
return acl
raise Exception(e)
app = acl.validate_app()
if not app:
acl.create_app(payload)
return acl
def get_admin_user_rid(self):
@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
@ -258,51 +210,21 @@ def common_check_new_columns():
"""
add new columns to tables
"""
from api.extensions import db
from sqlalchemy import inspect, text
CheckNewColumn().run()
def get_model_by_table_name(table_name):
for model in db.Model.registry._class_registry.values():
if hasattr(model, '__tablename__') and model.__tablename__ == table_name:
return model
return None
def add_new_column(table_name, new_column):
column_type = new_column.type.compile(engine.dialect)
default_value = new_column.default.arg if new_column.default else None
@click.command()
@with_appcontext
def common_sync_file_to_db():
from api.lib.common_setting.upload_file import CommonFileCRUD
CommonFileCRUD.sync_file_to_db()
sql = f"ALTER TABLE {table_name} ADD COLUMN {new_column.name} {column_type} "
if new_column.comment:
sql += f" comment '{new_column.comment}'"
if column_type == 'JSON':
pass
elif default_value:
if column_type.startswith('VAR') or column_type.startswith('Text'):
if default_value is None or len(default_value) == 0:
pass
else:
sql += f" DEFAULT {default_value}"
sql = text(sql)
db.session.execute(sql)
engine = db.get_engine()
inspector = inspect(engine)
table_names = inspector.get_table_names()
for table_name in table_names:
existed_columns = inspector.get_columns(table_name)
existed_column_name_list = [c['name'] for c in existed_columns]
model = get_model_by_table_name(table_name)
if model is None:
continue
model_columns = model.__table__.columns._all_columns
for column in model_columns:
if column.name not in existed_column_name_list:
try:
add_new_column(table_name, column)
current_app.logger.info(f"add new column [{column.name}] in table [{table_name}] success.")
except Exception as e:
current_app.logger.error(f"add new column [{column.name}] in table [{table_name}] err:")
current_app.logger.error(e)
@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

@ -5,9 +5,7 @@ from glob import glob
from subprocess import call
import click
from flask import current_app
from flask.cli import with_appcontext
from werkzeug.exceptions import MethodNotAllowed, NotFound
from api.extensions import db
@ -84,69 +82,59 @@ def clean():
os.remove(full_pathname)
@click.command()
@click.option("--url", default=None, help="Url to test (ex. /static/image.png)")
@click.option(
"--order", default="rule", help="Property on Rule to order by (default: rule)"
)
@with_appcontext
def urls(url, order):
"""Display all of the url matching routes for the project.
Borrowed from Flask-Script, converted to use Click.
"""
rows = []
column_headers = ("Rule", "Endpoint", "Arguments")
if url:
try:
rule, arguments = current_app.url_map.bind("localhost").match(
url, return_rule=True
)
rows.append((rule.rule, rule.endpoint, arguments))
column_length = 3
except (NotFound, MethodNotAllowed) as e:
rows.append(("<{}>".format(e), None, None))
column_length = 1
else:
rules = sorted(
current_app.url_map.iter_rules(), key=lambda rule: getattr(rule, order)
)
for rule in rules:
rows.append((rule.rule, rule.endpoint, None))
column_length = 2
str_template = ""
table_width = 0
if column_length >= 1:
max_rule_length = max(len(r[0]) for r in rows)
max_rule_length = max_rule_length if max_rule_length > 4 else 4
str_template += "{:" + str(max_rule_length) + "}"
table_width += max_rule_length
if column_length >= 2:
max_endpoint_length = max(len(str(r[1])) for r in rows)
max_endpoint_length = max_endpoint_length if max_endpoint_length > 8 else 8
str_template += " {:" + str(max_endpoint_length) + "}"
table_width += 2 + max_endpoint_length
if column_length >= 3:
max_arguments_length = max(len(str(r[2])) for r in rows)
max_arguments_length = max_arguments_length if max_arguments_length > 9 else 9
str_template += " {:" + str(max_arguments_length) + "}"
table_width += 2 + max_arguments_length
click.echo(str_template.format(*column_headers[:column_length]))
click.echo("-" * table_width)
for row in rows:
click.echo(str_template.format(*row[:column_length]))
@click.command()
@with_appcontext
def db_setup():
"""create tables
"""
db.create_all()
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

@ -2,6 +2,7 @@
from celery import Celery
from flask_babel import Babel
from flask_bcrypt import Bcrypt
from flask_caching import Cache
from flask_cors import CORS
@ -14,6 +15,7 @@ from api.lib.utils import ESHandler
from api.lib.utils import RedisHandler
bcrypt = Bcrypt()
babel = Babel()
login_manager = LoginManager()
db = SQLAlchemy(session_options={"autoflush": False})
migrate = Migrate()

View File

@ -81,8 +81,9 @@ class AttributeManager(object):
elif choice_other.get('script'):
try:
x = compile(choice_other['script'], '', "exec")
exec(x)
res = locals()['ChoiceValue']().values() or []
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))
@ -107,7 +108,8 @@ class AttributeManager(object):
return []
choice_values = choice_table.get_by(fl=["value", "option"], attr_id=attr_id)
return [[ValueTypeMap.serialize[value_type](choice_value['value']), choice_value['option']]
return [[ValueTypeMap.serialize[value_type](choice_value['value']), choice_value['option'] or
{"label": ValueTypeMap.serialize[value_type](choice_value['value'])}]
for choice_value in choice_values]
@staticmethod
@ -134,6 +136,15 @@ class AttributeManager(object):
choice_table and choice_table.get_by(attr_id=_id, only_query=True).delete()
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
def search_attributes(cls, name=None, alias=None, page=1, page_size=None):
"""
@ -166,29 +177,36 @@ class AttributeManager(object):
def get_attribute_by_name(self, name):
attr = Attribute.get_by(name=name, first=True)
if attr.get("is_choice"):
attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"],
attr["choice_web_hook"], attr.get("choice_other"))
attr["choice_value"] = self.get_choice_values(attr["id"],
attr["value_type"],
attr["choice_web_hook"],
attr.get("choice_other"))
return attr
def get_attribute_by_alias(self, alias):
attr = Attribute.get_by(alias=alias, first=True)
if attr.get("is_choice"):
attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"],
attr["choice_web_hook"], attr.get("choice_other"))
attr["choice_value"] = self.get_choice_values(attr["id"],
attr["value_type"],
attr["choice_web_hook"],
attr.get("choice_other"))
return attr
def get_attribute_by_id(self, _id):
attr = Attribute.get_by_id(_id).to_dict()
if attr.get("is_choice"):
attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"],
attr["choice_web_hook"], attr.get("choice_other"))
attr["choice_value"] = self.get_choice_values(attr["id"],
attr["value_type"],
attr["choice_web_hook"],
attr.get("choice_other"))
return attr
def get_attribute(self, key, choice_web_hook_parse=True, choice_other_parse=True):
attr = AttributeCache.get(key).to_dict()
attr = AttributeCache.get(key) or dict()
attr = attr and attr.to_dict()
if attr.get("is_choice"):
attr["choice_value"] = self.get_choice_values(
attr["id"],
@ -227,7 +245,7 @@ class AttributeManager(object):
is_choice = True if choice_value or kwargs.get('choice_web_hook') or kwargs.get('choice_other') else False
name = kwargs.pop("name")
if name in BUILTIN_KEYWORDS:
if name in BUILTIN_KEYWORDS or kwargs.get('alias') in BUILTIN_KEYWORDS:
return abort(400, ErrFormat.attribute_name_cannot_be_builtin)
while kwargs.get('choice_other'):
@ -336,9 +354,6 @@ class AttributeManager(object):
def update(self, _id, **kwargs):
attr = Attribute.get_by_id(_id) or abort(404, ErrFormat.attribute_not_found.format("id={}".format(_id)))
if not self._can_edit_attribute(attr):
return abort(403, ErrFormat.cannot_edit_attribute)
if kwargs.get("name"):
other = Attribute.get_by(name=kwargs['name'], first=True, to_dict=False)
if other and other.id != attr.id:
@ -379,6 +394,14 @@ class AttributeManager(object):
kwargs.get('is_computed') and self.can_create_computed_attribute()
is_changed = False
for k in kwargs:
if kwargs[k] != getattr(attr, k, None):
is_changed = True
if is_changed and not self._can_edit_attribute(attr):
return abort(403, ErrFormat.cannot_edit_attribute)
attr.update(flush=True, filter_none=False, **kwargs)
if is_choice and choice_value:
@ -392,7 +415,7 @@ class AttributeManager(object):
db.session.rollback()
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()
if not new['choice_web_hook'] and new['is_choice']:

View File

@ -1,44 +1,61 @@
# -*- coding:utf-8 -*-
import copy
import datetime
import json
import jsonpath
import os
from flask import abort
from flask import current_app
from flask_login import current_user
from sqlalchemy import func
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 CITypeCache
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.const import AutoDiscoveryType
from api.lib.cmdb.const import CMDB_QUEUE
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.custom_dashboard import SystemConfigManager
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci import search
from api.lib.cmdb.search.ci import search as ci_search
from api.lib.common_setting.role_perm_base import CMDBApp
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 validate_permission
from api.lib.utils import AESCrypto
from api.models.cmdb import AutoDiscoveryAccount
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 AutoDiscoveryExecHistory
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__))
app_cli = CMDBApp()
def parse_plugin_script(script):
attributes = []
try:
x = compile(script, '', "exec")
exec(x)
unique_key = locals()['AutoDiscovery']().unique_key
attrs = locals()['AutoDiscovery']().attributes() or []
local_ns = {}
exec(x, {}, local_ns)
unique_key = local_ns['AutoDiscovery']().unique_key
attrs = local_ns['AutoDiscovery']().attributes() or []
except Exception as e:
return abort(400, str(e))
@ -96,14 +113,30 @@ class AutoDiscoveryRuleCRUD(DBMixin):
else:
self.cls.create(**rule)
def _can_add(self, **kwargs):
def _can_add(self, valid=True, **kwargs):
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'):
if kwargs.get('is_plugin') and kwargs.get('plugin_script') and valid:
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
def _can_update(self, **kwargs):
def _can_update(self, valid=True, **kwargs):
existed = self.cls.get_by_id(kwargs['_id']) or abort(
404, ErrFormat.adr_not_found.format("id={}".format(kwargs['_id'])))
@ -115,6 +148,22 @@ class AutoDiscoveryRuleCRUD(DBMixin):
if other and other.id != existed.id:
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
def update(self, _id, **kwargs):
@ -122,21 +171,44 @@ class AutoDiscoveryRuleCRUD(DBMixin):
if kwargs.get('is_plugin') and kwargs.get('plugin_script'):
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)
def _can_delete(self, **kwargs):
if AutoDiscoveryCIType.get_by(adr_id=kwargs['_id'], first=True):
return abort(400, ErrFormat.adr_referenced)
return self._can_update(**kwargs)
existed = self.cls.get_by_id(kwargs['_id']) or abort(
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):
cls = AutoDiscoveryCIType
@classmethod
def get_all(cls):
return cls.cls.get_by(to_dict=False)
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.type_id in type_ids]
@classmethod
def get_by_id(cls, _id):
@ -147,25 +219,59 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
return cls.cls.get_by(type_id=type_id, to_dict=False)
@classmethod
def get(cls, ci_id, oneagent_id, last_update_at=None):
def get_ad_attributes(cls, type_id):
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 = []
rules = cls.cls.get_by(to_dict=True)
for rule in rules:
if rule.get('relation'):
if not rule['enabled']:
continue
if isinstance(rule.get("extra_option"), dict) and rule['extra_option'].get('secret'):
if not (current_user.username == "cmdb_agent" or current_user.uid == rule['uid']):
if isinstance(rule.get("extra_option"), dict):
decrypt_account(rule['extra_option'], rule['uid'])
if rule['extra_option'].get('_reference'):
rule['extra_option'].pop('password', None)
rule['extra_option'].pop('secret', None)
else:
rule['extra_option']['secret'] = AESCrypto.decrypt(rule['extra_option']['secret'])
rule['extra_option'].update(
AutoDiscoveryAccountCRUD().get_config_by_id(rule['extra_option']['_reference']))
if oneagent_id and rule['agent_id'] == oneagent_id:
result.append(rule)
elif rule['query_expr']:
query = rule['query_expr'].lstrip('q').lstrip('=')
s = search(query, fl=['_id'], count=1000000)
s = ci_search(query, fl=['_id'], count=1000000)
try:
response, _, _, _, _, _ = s.search()
except SearchError as e:
@ -176,25 +282,32 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
result.append(rule)
break
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'])
if not adr:
continue
if adr.type in (AutoDiscoveryType.SNMP, AutoDiscoveryType.HTTP):
continue
if not rule['updated_at']:
continue
result.append(rule)
ad_rules_updated_at = (SystemConfigManager.get('ad_rules_updated_at') or {}).get('option', {}).get('v') or ""
new_last_update_at = ""
for i in result:
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 "",
i['adr']['created_at'] or "", i['adr']['updated_at'] or ""])
i['adr']['created_at'] or "", i['adr']['updated_at'] or "", ad_rules_updated_at])
if 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:
return result, new_last_update_at
else:
@ -213,7 +326,7 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
agent_id = agent_id.strip()
q = "op_duty:{0},-rd_duty:{0},oneagent_id:{1}"
s = search(q.format(current_user.username, agent_id.strip()))
s = ci_search(q.format(current_user.username, agent_id.strip()))
try:
response, _, _, _, _, _ = s.search()
if response:
@ -222,7 +335,7 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
current_app.logger.warning(e)
return abort(400, str(e))
s = search(q.format(current_user.nickname, agent_id.strip()))
s = ci_search(q.format(current_user.nickname, agent_id.strip()))
try:
response, _, _, _, _, _ = s.search()
if response:
@ -236,7 +349,7 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
if query_expr.startswith('q='):
query_expr = query_expr[2:]
s = search(query_expr, count=1000000)
s = ci_search(query_expr, count=1000000)
try:
response, _, _, _, _, _ = s.search()
for i in response:
@ -250,26 +363,43 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
current_app.logger.warning(e)
return abort(400, str(e))
def _can_add(self, **kwargs):
self.cls.get_by(type_id=kwargs['type_id'], adr_id=kwargs.get('adr_id') or None) and abort(
400, ErrFormat.ad_duplicate)
# self.__valid_exec_target(kwargs.get('agent_id'), kwargs.get('query_expr'))
@staticmethod
def _can_add(**kwargs):
if kwargs.get('adr_id'):
adr = AutoDiscoveryRule.get_by_id(kwargs['adr_id']) or abort(
404, ErrFormat.adr_not_found.format("id={}".format(kwargs['adr_id'])))
if not adr.is_plugin:
other = self.cls.get_by(adr_id=adr.id, first=True, to_dict=False)
if other:
ci_type = CITypeCache.get(other.type_id)
return abort(400, ErrFormat.adr_default_ref_once.format(ci_type.alias))
if 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 kwargs.get('is_plugin') and kwargs.get('plugin_script'):
kwargs = check_plugin_script(**kwargs)
if isinstance(kwargs.get('extra_option'), dict) and kwargs['extra_option'].get('secret'):
kwargs['extra_option']['secret'] = AESCrypto.encrypt(kwargs['extra_option']['secret'])
encrypt_account(kwargs.get('extra_option'))
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
@ -279,11 +409,44 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
existed = self.cls.get_by_id(kwargs['_id']) or abort(
404, ErrFormat.ad_not_found.format("id={}".format(kwargs['_id'])))
self.__valid_exec_target(kwargs.get('agent_id'), kwargs.get('query_expr'))
adr = AutoDiscoveryRule.get_by_id(existed.adr_id) or abort(
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 current_user.uid != existed.uid:
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
@ -292,10 +455,22 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
if kwargs.get('is_plugin') and kwargs.get('plugin_script'):
kwargs = check_plugin_script(**kwargs)
if isinstance(kwargs.get('extra_option'), dict) and kwargs['extra_option'].get('secret'):
kwargs['extra_option']['secret'] = AESCrypto.encrypt(kwargs['extra_option']['secret'])
encrypt_account(kwargs.get('extra_option'))
return super(AutoDiscoveryCITypeCRUD, self).update(_id, filter_none=False, **kwargs)
inst = self._can_update(_id=_id, **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):
if AutoDiscoveryCICRUD.get_by_adt_id(kwargs['_id']):
@ -306,6 +481,61 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
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):
cls = AutoDiscoveryCI
@ -333,15 +563,14 @@ class AutoDiscoveryCICRUD(DBMixin):
@staticmethod
def get_attributes_by_type_id(type_id):
from api.lib.cmdb.cache import CITypeAttributesCache
attributes = [i[1] for i in CITypeAttributesCache.get2(type_id) or []]
from api.lib.cmdb.ci_type import CITypeAttributeManager
attributes = [i for i in CITypeAttributeManager.get_attributes_by_type_id(type_id) or []]
attr_names = set()
adts = AutoDiscoveryCITypeCRUD.get_by_type_id(type_id)
for adt in adts:
attr_names |= set((adt.attributes or {}).values())
return [attr.to_dict() for attr in attributes if attr.name in attr_names]
return [attr for attr in attributes if attr['name'] in attr_names]
@classmethod
def search(cls, page, page_size, fl=None, **kwargs):
@ -394,16 +623,24 @@ class AutoDiscoveryCICRUD(DBMixin):
changed = False
if existed is not None:
if existed.instance != kwargs['instance']:
instance = copy.deepcopy(existed.instance) or {}
instance.update(kwargs['instance'])
kwargs['instance'] = instance
existed.update(filter_none=False, **kwargs)
AutoDiscoveryExecHistoryCRUD().add(type_id=adt.type_id,
stdout="update resource: {}".format(kwargs.get('unique_value')))
changed = True
else:
existed = self.cls.create(**kwargs)
AutoDiscoveryExecHistoryCRUD().add(type_id=adt.type_id,
stdout="add resource: {}".format(kwargs.get('unique_value')))
changed = True
if adt.auto_accept and changed:
try:
self.accept(existed)
except Exception as e:
current_app.logger.error(e)
return abort(400, str(e))
elif changed:
existed.update(is_accept=False, accept_time=None, accept_by=None, filter_none=False)
@ -423,6 +660,13 @@ class AutoDiscoveryCICRUD(DBMixin):
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)
return inst
@ -438,6 +682,13 @@ class AutoDiscoveryCICRUD(DBMixin):
not is_app_admin("cmdb") and validate_permission(ci_type.name, ResourceTypeEnum.CI, PermEnum.DELETE, "cmdb")
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
@classmethod
@ -448,36 +699,24 @@ class AutoDiscoveryCICRUD(DBMixin):
adt = AutoDiscoveryCITypeCRUD.get_by_id(adc.adt_id) or abort(404, ErrFormat.adt_not_found)
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)
relation_adts = AutoDiscoveryCIType.get_by(type_id=adt.type_id, adr_id=None, to_dict=False)
for r_adt in relation_adts:
if not r_adt.relation or ci_id is None:
continue
for ad_key in r_adt.relation:
if not adc.instance.get(ad_key):
continue
cmdb_key = r_adt.relation[ad_key]
query = "_type:{},{}:{}".format(cmdb_key.get('type_name'), cmdb_key.get('attr_name'),
adc.instance.get(ad_key))
s = search(query)
try:
response, _, _, _, _, _ = s.search()
except SearchError as e:
current_app.logger.warning(e)
return abort(400, str(e))
ad_key2attr = adt.attributes or {}
if ad_key2attr:
ci_dict = {ad_key2attr[k]: None if not v and isinstance(v, (list, dict)) else v
for k, v in adc.instance.items() if k in ad_key2attr}
extra_option = adt.extra_option or {}
mapping, path_mapping = AutoDiscoveryHTTPManager.get_predefined_value_mapping(
extra_option.get('provider'), extra_option.get('category'))
if mapping:
ci_dict = {k: (mapping.get(k) or {}).get(str(v), v) for k, v in ci_dict.items()}
if path_mapping:
ci_dict = {k: jsonpath.jsonpath(v, path_mapping[k]) if k in path_mapping else v
for k, v in ci_dict.items()}
ci_id = CIManager.add(adc.type_id, is_auto_discovery=True, _is_admin=True, **ci_dict)
AutoDiscoveryExecHistoryCRUD().add(type_id=adt.type_id,
stdout="accept resource: {}".format(adc.unique_value))
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
build_relations_for_ad_accept.apply_async(args=(adc.to_dict(), ci_id, ad_key2attr), queue=CMDB_QUEUE)
adc.update(is_accept=True,
accept_by=nickname or current_user.nickname,
@ -488,17 +727,75 @@ class AutoDiscoveryCICRUD(DBMixin):
class AutoDiscoveryHTTPManager(object):
@staticmethod
def get_categories(name):
return (ClOUD_MAP.get(name) or {}).get('categories') or []
categories = (CLOUD_MAP.get(name) or {}) or []
for item in copy.deepcopy(categories):
item.pop('map', None)
item.pop('collect_key_map', None)
@staticmethod
def get_attributes(name, category):
tpt = ((ClOUD_MAP.get(name) or {}).get('map') or {}).get(category)
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 categories
def get_resources(self, name):
en_name = None
for i in DEFAULT_INNER:
if i['name'] == name:
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 []
@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):
@ -509,3 +806,191 @@ class AutoDiscoverySNMPManager(object):
return json.loads(f.read())
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,15 +2,38 @@
from api.lib.cmdb.const import AutoDiscoveryType
DEFAULT_HTTP = [
dict(name="阿里云", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-aliyun'}}),
dict(name="腾讯云", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-tengxunyun'}}),
dict(name="华为云", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-huaweiyun'}}),
dict(name="AWS", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-aws'}}),
PRIVILEGED_USERS = ("cmdb_agent", "worker", "admin")
DEFAULT_INNER = [
dict(name="阿里云", en="aliyun", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-aliyun'}, "en": "aliyun"}),
dict(name="腾讯云", en="tencentcloud", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-tengxunyun'}, "en": "tencentcloud"}),
dict(name="华为云", en="huaweicloud", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-huaweiyun'}, "en": "huaweicloud"}),
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,
option={'icon': {'name': 'caise-jiaohuanji'}}),
@ -22,32 +45,307 @@ DEFAULT_HTTP = [
option={'icon': {'name': 'caise-dayinji'}}),
]
ClOUD_MAP = {
"aliyun": {
"categories": ["云服务器 ECS"],
"map": {
"云服务器 ECS": "templates/aliyun_ecs.json",
}
},
"tencentcloud": {
"categories": ["云服务器 CVM"],
"map": {
"云服务器 CVM": "templates/tencent_cvm.json",
}
},
"huaweicloud": {
"categories": ["云服务器 ECS"],
"map": {
"云服务器 ECS": "templates/huaweicloud_ecs.json",
}
},
"aws": {
"categories": ["云服务器 EC2"],
"map": {
"云服务器 EC2": "templates/aws_ec2.json",
}
},
CLOUD_MAP = {
"aliyun": [
{
"category": "计算",
"items": ["云服务器 ECS", "云服务器 Disk"],
"map": {
"云服务器 ECS": {"template": "templates/aliyun_ecs.json", "mapping": "ecs"},
"云服务器 Disk": {"template": "templates/aliyun_ecs_disk.json", "mapping": "evs"},
},
"collect_key_map": {
"云服务器 ECS": "ali.ecs",
"云服务器 Disk": "ali.ecs_disk",
},
},
{
"category": "网络与CDN",
"items": [
"内容分发CDN",
"负载均衡SLB",
"专有网络VPC",
"交换机Switch",
],
"map": {
"内容分发CDN": {"template": "templates/aliyun_cdn.json", "mapping": "CDN"},
"负载均衡SLB": {"template": "templates/aliyun_slb.json", "mapping": "loadbalancer"},
"专有网络VPC": {"template": "templates/aliyun_vpc.json", "mapping": "vpc"},
"交换机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",
},
},
],
}

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@ -2,13 +2,27 @@
from __future__ import unicode_literals
from flask import current_app
from collections import defaultdict
import datetime
import os
import yaml
from flask import current_app
import json
from api.extensions import cache
from api.extensions import db
from api.lib.cmdb.custom_dashboard import CustomDashboardManager
from api.models.cmdb import Attribute
from api.models.cmdb import Attribute, AutoDiscoveryExecHistory
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 CIType
from api.models.cmdb import CITypeAttribute
from api.models.cmdb import PreferenceShowAttributes
from api.models.cmdb import PreferenceTreeView
from api.models.cmdb import RelationType
@ -226,7 +240,9 @@ class CITypeAttributeCache(object):
class CMDBCounterCache(object):
KEY = 'CMDB::Counter'
KEY = 'CMDB::Counter::dashboard'
KEY2 = 'CMDB::Counter::adc'
KEY3 = 'CMDB::Counter::sub'
@classmethod
def get(cls):
@ -239,7 +255,7 @@ class CMDBCounterCache(object):
@classmethod
def set(cls, result):
cache.set(cls.KEY, result, timeout=0)
cache.set(cls.KEY, json.loads(json.dumps(result)), timeout=0)
@classmethod
def reset(cls):
@ -261,7 +277,7 @@ class CMDBCounterCache(object):
cls.set(result)
return result
return json.loads(json.dumps(result))
@classmethod
def update(cls, custom, flush=True):
@ -283,27 +299,38 @@ class CMDBCounterCache(object):
result[custom['id']] = res
cls.set(result)
return res
return json.loads(json.dumps(res))
@staticmethod
def relation_counter(type_id, level, other_filer, type_ids):
@classmethod
def relation_counter(cls, type_id, level, other_filer, type_ids):
from api.lib.cmdb.search.ci_relation.search import Search as RelSearch
from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci import search
from api.lib.cmdb.attribute import AttributeManager
query = "_type:{}".format(type_id)
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 = [(str(i.get('_id')), i.get(i.get('unique'))) for i in type_names]
type_id_names = []
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'))
s = RelSearch([i[0] for i in type_id_names], level, other_filer or '')
type_id_names.append((str(i.get('_id')), enum_map.get(attr_value, attr_value)))
s = RelSearch([i[0] for i in type_id_names], level)
try:
stats = s.statistics(type_ids)
stats = s.statistics(type_ids, need_filter=False)
except SearchError as e:
current_app.logger.error(e)
return
@ -331,11 +358,12 @@ class CMDBCounterCache(object):
return result
@staticmethod
def attribute_counter(custom):
@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')
@ -351,16 +379,24 @@ class CMDBCounterCache(object):
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)
@ -370,13 +406,18 @@ class CMDBCounterCache(object):
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]:
result[ValueTypeMap.serialize2[attr2value_type[0]](str(i[0]))] = i[1]
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
for v in result:
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:
@ -384,18 +425,22 @@ class CMDBCounterCache(object):
except SearchError as e:
current_app.logger.error(e)
return
result[v] = dict()
result[enum_map1.get(v, v)] = dict()
origin_result[v] = dict()
for i in (list(facet.values()) or [[]])[0]:
result[v][ValueTypeMap.serialize2[attr2value_type[1]](str(i[0]))] = i[1]
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
for v1 in result:
if not isinstance(result[v1], dict):
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 result[v1]:
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)
@ -404,9 +449,10 @@ class CMDBCounterCache(object):
except SearchError as e:
current_app.logger.error(e)
return
result[v1][v2] = dict()
result[enum_map1.get(v1, v1)][enum_map2.get(v2, v2)] = dict()
for i in (list(facet.values()) or [[]])[0]:
result[v1][v2][ValueTypeMap.serialize2[attr2value_type[2]](str(i[0]))] = i[1]
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
@ -429,3 +475,124 @@ class CMDBCounterCache(object):
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,6 +1,8 @@
# -*- coding:utf-8 -*-
from flask_babel import lazy_gettext as _l
from api.lib.utils import BaseEnum
@ -14,6 +16,8 @@ class ValueTypeEnum(BaseEnum):
JSON = "6"
PASSWORD = TEXT
LINK = TEXT
BOOL = "7"
REFERENCE = INT
class ConstraintEnum(BaseEnum):
@ -41,20 +45,23 @@ class OperateType(BaseEnum):
class CITypeOperateType(BaseEnum):
ADD = "0" # 新增模型
UPDATE = "1" # 修改模型
DELETE = "2" # 删除模型
ADD_ATTRIBUTE = "3" # 新增属性
UPDATE_ATTRIBUTE = "4" # 修改属性
DELETE_ATTRIBUTE = "5" # 删除属性
ADD_TRIGGER = "6" # 新增触发器
UPDATE_TRIGGER = "7" # 修改触发器
DELETE_TRIGGER = "8" # 删除触发器
ADD_UNIQUE_CONSTRAINT = "9" # 新增联合唯一
UPDATE_UNIQUE_CONSTRAINT = "10" # 修改联合唯一
DELETE_UNIQUE_CONSTRAINT = "11" # 删除联合唯一
ADD_RELATION = "12" # 新增关系
DELETE_RELATION = "13" # 删除关系
ADD = "0" # add CIType
UPDATE = "1" # update CIType
DELETE = "2" # delete CIType
ADD_ATTRIBUTE = "3"
UPDATE_ATTRIBUTE = "4"
DELETE_ATTRIBUTE = "5"
ADD_TRIGGER = "6"
UPDATE_TRIGGER = "7"
DELETE_TRIGGER = "8"
ADD_UNIQUE_CONSTRAINT = "9"
UPDATE_UNIQUE_CONSTRAINT = "10"
DELETE_UNIQUE_CONSTRAINT = "11"
ADD_RELATION = "12"
DELETE_RELATION = "13"
ADD_RECONCILIATION = "14"
UPDATE_RECONCILIATION = "15"
DELETE_RECONCILIATION = "16"
class RetKey(BaseEnum):
@ -69,6 +76,8 @@ class ResourceTypeEnum(BaseEnum):
CI_TYPE_RELATION = "CITypeRelation" # create/delete/grant
RELATION_VIEW = "RelationView" # read/update/delete/grant
CI_FILTER = "CIFilter" # read
PAGE = "page" # read
TOPOLOGY_VIEW = "TopologyView" # read/update/delete/grant
class PermEnum(BaseEnum):
@ -88,7 +97,8 @@ class RoleEnum(BaseEnum):
class AutoDiscoveryType(BaseEnum):
AGENT = "agent"
SNMP = "snmp"
HTTP = "http"
HTTP = "http" # cloud
COMPONENTS = "components"
class AttributeDefaultValueEnum(BaseEnum):
@ -97,11 +107,52 @@ class AttributeDefaultValueEnum(BaseEnum):
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"
REDIS_PREFIX_CI = "ONE_CMDB"
REDIS_PREFIX_CI_RELATION = "CMDB_CI_RELATION"
REDIS_PREFIX_CI_RELATION2 = "CMDB_CI_RELATION2"
BUILTIN_KEYWORDS = {'id', '_id', 'ci_id', 'type', '_type', 'ci_type', '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_CI = None

View File

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

View File

@ -0,0 +1,33 @@
# -*- 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

@ -0,0 +1,17 @@
# -*- 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

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

@ -0,0 +1,19 @@
# -*- 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

@ -0,0 +1,182 @@
# -*- 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

@ -0,0 +1,29 @@
# -*- 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

@ -0,0 +1,56 @@
# -*- 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

@ -0,0 +1,85 @@
# -*- 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,9 +10,11 @@ from api.extensions import db
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import RelationTypeCache
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.resp_format import ErrFormat
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 AttributeHistory
from api.models.cmdb import CIRelationHistory
@ -21,12 +23,13 @@ from api.models.cmdb import CITypeHistory
from api.models.cmdb import CITypeTrigger
from api.models.cmdb import CITypeUniqueConstraint
from api.models.cmdb import OperationRecord
from api.lib.cmdb.utils import TableMap
class AttributeHistoryManger(object):
@staticmethod
def get_records_for_attributes(start, end, username, page, page_size, operate_type, type_id,
ci_id=None, attr_id=None):
ci_id=None, attr_id=None, ci_ids=None, more=False):
records = db.session.query(OperationRecord, AttributeHistory).join(
AttributeHistory, OperationRecord.id == AttributeHistory.record_id)
@ -48,6 +51,9 @@ class AttributeHistoryManger(object):
if ci_id is not None:
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:
records = records.filter(AttributeHistory.attr_id == attr_id)
@ -55,17 +61,39 @@ class AttributeHistoryManger(object):
total = len(records)
res = {}
show_attr_set = {}
show_attr_cache = {}
for record in records:
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['attr'] = AttributeCache.get(attr_hist['attr_id'])
if attr_hist['attr']:
attr_hist['attr_name'] = attr_hist['attr'].name
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")
if record_id not in res:
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"))
if record_dict["user"]:
record_dict['user'] = record_dict['user'].nickname
@ -135,7 +163,7 @@ class AttributeHistoryManger(object):
from api.lib.cmdb.ci import CIManager
cis = CIManager().get_cis_by_ids(list(ci_ids),
unique_required=True)
cis = {i['_id']: i for i in cis}
cis = {i['_id']: i for i in cis if i}
return total, res, cis
@ -161,12 +189,14 @@ class AttributeHistoryManger(object):
record = i.OperationRecord
item = dict(attr_name=attr.name,
attr_alias=attr.alias,
value_type=attr.value_type,
operate_type=hist.operate_type,
username=user and user.nickname,
old=hist.old,
new=hist.new,
created_at=record.created_at.strftime('%Y-%m-%d %H:%M:%S'),
record_id=record.id,
ticket_id=record.ticket_id,
hid=hist.id
)
result.append(item)
@ -200,9 +230,9 @@ class AttributeHistoryManger(object):
return username, timestamp, attr_dict, rel_dict
@staticmethod
def add(record_id, ci_id, history_list, type_id=None, flush=False, commit=True):
def add(record_id, ci_id, history_list, type_id=None, ticket_id=None, flush=False, commit=True):
if record_id is None:
record = OperationRecord.create(uid=current_user.uid, type_id=type_id)
record = OperationRecord.create(uid=current_user.uid, type_id=type_id, ticket_id=ticket_id)
record_id = record.id
for attr_id, operate_type, old, new in history_list or []:
@ -220,8 +250,8 @@ class AttributeHistoryManger(object):
class CIRelationHistoryManager(object):
@staticmethod
def add(rel_obj, operate_type=OperateType.ADD):
record = OperationRecord.create(uid=current_user.uid)
def add(rel_obj, operate_type=OperateType.ADD, uid=None):
record = OperationRecord.create(uid=uid or current_user.uid)
CIRelationHistory.create(relation_id=rel_obj.id,
record_id=record.id,
@ -270,7 +300,7 @@ class CITypeHistoryManager(object):
return numfound, result
@staticmethod
def add(operate_type, type_id, attr_id=None, trigger_id=None, unique_constraint_id=None, change=None):
def add(operate_type, type_id, attr_id=None, trigger_id=None, unique_constraint_id=None, change=None, rc_id=None):
if type_id is None and attr_id is not None:
from api.models.cmdb import CITypeAttribute
type_ids = [i.type_id for i in CITypeAttribute.get_by(attr_id=attr_id, to_dict=False)]
@ -283,6 +313,7 @@ class CITypeHistoryManager(object):
uid=current_user.uid,
attr_id=attr_id,
trigger_id=trigger_id,
rc_id=rc_id,
unique_constraint_id=unique_constraint_id,
change=change)
@ -294,7 +325,7 @@ class CITriggerHistoryManager(object):
def get(page, page_size, type_id=None, trigger_id=None, operate_type=None):
query = CITriggerHistory.get_by(only_query=True)
if type_id:
query = query.filter(CITriggerHistory.type_id == type_id)
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)

View File

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

View File

@ -0,0 +1,132 @@
# -*- 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

@ -0,0 +1,35 @@
# -*- 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

@ -0,0 +1,61 @@
# -*- 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

@ -0,0 +1,104 @@
# -*- 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

@ -0,0 +1,355 @@
# -*- 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,12 +1,15 @@
# -*- coding:utf-8 -*-
import copy
import functools
import redis_lock
from flask import abort
from flask import current_app
from flask import request
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.resp_format import ErrFormat
from api.lib.mixin import DBMixin
@ -24,7 +27,7 @@ class CIFilterPermsCRUD(DBMixin):
result = {}
for i in res:
if i['attr_filter']:
i['attr_filter'] = i['attr_filter'].split(',')
i['attr_filter'] = i['attr_filter'].split(',') + list(BUILTIN_ATTRIBUTES.keys())
if i['rid'] not in result:
result[i['rid']] = i
@ -40,6 +43,11 @@ class CIFilterPermsCRUD(DBMixin):
result[i['rid']]['ci_filter'] = ""
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
def get_by_ids(self, _ids, type_id=None):
@ -54,7 +62,7 @@ class CIFilterPermsCRUD(DBMixin):
result = {}
for i in res:
if i['attr_filter']:
i['attr_filter'] = i['attr_filter'].split(',')
i['attr_filter'] = i['attr_filter'].split(',') + list(BUILTIN_ATTRIBUTES.keys())
if i['type_id'] not in result:
result[i['type_id']] = i
@ -70,6 +78,11 @@ class CIFilterPermsCRUD(DBMixin):
result[i['type_id']]['ci_filter'] = ""
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
@classmethod
@ -82,6 +95,54 @@ class CIFilterPermsCRUD(DBMixin):
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 []
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):
ci_filter = kwargs.get('ci_filter')
attr_filter = kwargs.get('attr_filter') or ""
@ -102,34 +163,67 @@ class CIFilterPermsCRUD(DBMixin):
def add(self, **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)
obj = self.cls.get_by(type_id=kwargs.get('type_id'),
rid=kwargs.get('rid'),
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)
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']
obj.soft_delete()
else:
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
obj = self.cls.create(**kwargs)
else:
if not kwargs.get('ci_filter') and not kwargs.get('attr_filter') and not kwargs.get('id_filter'):
return
if current_app.config.get('USE_ACL'):
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)
if request_id_filter:
kwargs['id_filter'] = request_id_filter
return obj
obj = self.cls.create(**kwargs)
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):
pass
@ -138,15 +232,83 @@ class CIFilterPermsCRUD(DBMixin):
pass
def delete(self, **kwargs):
obj = self.cls.get_by(type_id=kwargs.get('type_id'),
rid=kwargs.get('rid'),
first=True, to_dict=False)
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'),
id_filter=None,
first=True, to_dict=False)
if obj is not None:
if current_app.config.get('USE_ACL'):
ACLManager().del_resource(str(obj.id), ResourceTypeEnum.CI_FILTER)
if obj is not None:
resource = None
if current_app.config.get('USE_ACL'):
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):

View File

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

@ -1,100 +1,178 @@
# -*- coding:utf-8 -*-
from flask_babel import lazy_gettext as _l
from api.lib.resp_format import CommonErrFormat
class ErrFormat(CommonErrFormat):
invalid_relation_type = "无效的关系类型: {}"
ci_type_not_found = "模型不存在!"
argument_attributes_must_be_list = "参数 attributes 类型必须是列表"
argument_file_not_found = "文件似乎并未上传"
ci_type_config = _l("CI Model") # 模型配置
attribute_not_found = "属性 {} 不存在!"
attribute_is_unique_id = "该属性是模型的唯一标识,不能被删除!"
attribute_is_ref_by_type = "该属性被模型 {} 引用, 不能删除!"
attribute_value_type_cannot_change = "属性的值类型不允许修改!"
attribute_list_value_cannot_change = "多值不被允许修改!"
attribute_index_cannot_change = "修改索引 非管理员不被允许!"
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"
attribute_choice_other_invalid = "预定义值: 其他模型请求参数不合法!"
invalid_relation_type = _l("Invalid relation type: {}") # 无效的关系类型: {}
ci_type_not_found = _l("CIType is not found") # 模型不存在!
ci_not_found = "CI {} 不存在"
unique_constraint = "多属性联合唯一校验不通过: {}"
unique_value_not_found = "模型的主键 {} 不存在!"
unique_key_required = "主键字段 {} 缺失"
ci_is_already_existed = "CI 已经存在!"
relation_constraint = "关系约束: {}, 校验失败 "
relation_not_found = "CI关系: {} 不存在"
ci_search_Parentheses_invalid = "搜索表达式里小括号前不支持: 或、非"
# 参数 attributes 类型必须是列表
argument_attributes_must_be_list = _l("The type of parameter attributes must be a list")
argument_file_not_found = _l("The file doesn't seem to be uploaded") # 文件似乎并未上传
ci_type_not_found2 = "模型 {} 不存在"
ci_type_is_already_existed = "模型 {} 已经存在"
unique_key_not_define = "主键未定义或者已被删除"
only_owner_can_delete = "只有创建人才能删除它!"
ci_exists_and_cannot_delete_type = "因为CI已经存在不能删除模型"
ci_relation_view_exists_and_cannot_delete_type = "因为关系视图 {} 引用了该模型,不能删除模型"
ci_type_group_not_found = "模型分组 {} 不存在"
ci_type_group_exists = "模型分组 {} 已经存在"
ci_type_relation_not_found = "模型关系 {} 不存在"
ci_type_attribute_group_duplicate = "属性分组 {} 已存在"
ci_type_attribute_group_not_found = "属性分组 {} 不存在"
ci_type_group_attribute_not_found = "属性组<{0}> - 属性<{1}> 不存在"
unique_constraint_duplicate = "唯一约束已经存在!"
unique_constraint_invalid = "唯一约束的属性不能是 JSON 和 多值"
ci_type_trigger_duplicate = "重复的触发器"
ci_type_trigger_not_found = "触发器 {} 不存在"
attribute_not_found = _l("Attribute {} does not exist!") # 属性 {} 不存在!
# 该属性是模型的唯一标识,不能被删除!
attribute_is_unique_id = _l(
"This attribute is the unique identifier of the model and cannot be deleted!")
attribute_is_ref_by_type = _l(
"This attribute is referenced by model {} and cannot be deleted!") # 该属性被模型 {} 引用, 不能删除!
attribute_value_type_cannot_change = _l(
"The value type of the attribute is not allowed to be modified!") # 属性的值类型不允许修改!
attribute_list_value_cannot_change = _l("Multiple values are not allowed to be modified!") # 多值不被允许修改!
# 修改索引 非管理员不被允许!
attribute_index_cannot_change = _l("Modifying the index is not allowed for non-administrators!")
attribute_index_change_failed = _l("Index switching failed!") # 索引切换失败!
invalid_choice_values = _l("The predefined value is of the wrong type!") # 预定义值的类型不对!
attribute_name_duplicate = _l("Duplicate attribute name {}") # 重复的属性名 {}
add_attribute_failed = _l("Failed to create attribute {}!") # 创建属性 {} 失败!
update_attribute_failed = _l("Modify attribute {} failed!") # 修改属性 {} 失败!
cannot_edit_attribute = _l("You do not have permission to modify this attribute!") # 您没有权限修改该属性!
cannot_delete_attribute = _l(
"Only creators and administrators are allowed to delete attributes!") # 目前只允许 属性创建人、管理员 删除属性!
# 属性字段名不能是内置字段: id, _id, ci_id, type, _type, ci_type
attribute_name_cannot_be_builtin = _l(
"Attribute field names cannot be built-in fields: id, _id, ci_id, type, _type, ci_type, ticket_id")
attribute_choice_other_invalid = _l(
"Predefined value: Other model request parameters are illegal!") # 预定义值: 其他模型请求参数不合法!
record_not_found = "操作记录 {} 不存在"
cannot_delete_unique = "不能删除唯一标识"
cannot_delete_default_order_attr = "不能删除默认排序的属性"
ci_not_found = _l("CI {} does not exist") # CI {} 不存在
unique_constraint = _l("Multiple attribute joint unique verification failed: {}") # 多属性联合唯一校验不通过: {}
unique_value_not_found = _l("The model's primary key {} does not exist!") # 模型的主键 {} 不存在!
unique_key_required = _l("Primary key {} is missing") # 主键字段 {} 缺失
ci_is_already_existed = _l("CI already exists!") # CI 已经存在!
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!")
preference_relation_view_node_required = "没有选择节点"
preference_search_option_not_found = "该搜索选项不存在!"
preference_search_option_exists = "该搜索选项命名重复!"
relation_not_found = _l("CI relationship: {} does not exist") # CI关系: {} 不存在
relation_type_exists = "关系类型 {} 已经存在"
relation_type_not_found = "关系类型 {} 不存在"
# 搜索表达式里小括号前不支持: 或、非
ci_search_Parentheses_invalid = _l("In search expressions, not supported before parentheses: or, not")
attribute_value_invalid = "无效的属性值: {}"
attribute_value_invalid2 = "{} 无效的值: {}"
not_in_choice_values = "{} 不在预定义值里"
attribute_value_unique_required = "属性 {} 的值必须是唯一的, 当前值 {} 已存在"
attribute_value_required = "属性 {} 值必须存在"
attribute_value_unknown_error = "新增或者修改属性值未知错误: {}"
ci_type_not_found2 = _l("Model {} does not exist") # 模型 {} 不存在
ci_type_is_already_existed = _l("Model {} already exists") # 模型 {} 已经存在
unique_key_not_define = _l("The primary key is undefined or has been deleted") # 主键未定义或者已被删除
only_owner_can_delete = _l("Only the creator can delete it!") # 只有创建人才能删除它!
ci_exists_and_cannot_delete_type = _l(
"The model cannot be deleted because the CI already exists") # 因为CI已经存在不能删除模型
ci_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") # 规则 {} 不存在
limit_ci_type = "模型数超过限制: {}"
limit_ci = "CI数超过限制: {}"
record_not_found = _l("Operation record {} does not exist") # 操作记录 {} 不存在
cannot_delete_unique = _l("Unique identifier cannot be deleted") # 不能删除唯一标识
cannot_delete_default_order_attr = _l("Cannot delete default sorted attributes") # 不能删除默认排序的属性
adr_duplicate = "自动发现规则: {} 已经存在!"
adr_not_found = "自动发现规则: {} 不存在!"
adr_referenced = "该自动发现规则被模型引用, 不能删除!"
ad_duplicate = "自动发现规则的应用不能重复定义!"
ad_not_found = "您要修改的自动发现: {} 不存在!"
ad_not_unique_key = "属性字段没有包括唯一标识: {}"
adc_not_found = "自动发现的实例不存在!"
adt_not_found = "模型并未关联该自动发现!"
adt_secret_no_permission = "只有创建人才能修改Secret!"
cannot_delete_adt = "该规则已经有自动发现的实例, 不能被删除!"
adr_default_ref_once = "该默认的自动发现规则 已经被模型 {} 引用!"
adr_unique_key_required = "unique_key方法必须返回非空字符串!"
adr_plugin_attributes_list_required = "attributes方法必须返回的是list"
adr_plugin_attributes_list_no_empty = "attributes方法返回的list不能为空!"
adt_target_all_no_permission = "只有管理员才可以定义执行机器为: 所有节点!"
adt_target_expr_no_permission = "执行机器权限检查不通过: {}"
preference_relation_view_node_required = _l("No node selected") # 没有选择节点
preference_search_option_not_found = _l("This search option does not exist!") # 该搜索选项不存在!
preference_search_option_exists = _l("This search option has a duplicate name!") # 该搜索选项命名重复!
ci_filter_name_cannot_be_empty = "CI过滤授权 必须命名!"
ci_filter_perm_cannot_or_query = "CI过滤授权 暂时不支持 或 查询"
ci_filter_perm_attr_no_permission = "您没有属性 {} 的操作权限!"
ci_filter_perm_ci_no_permission = "您没有该CI的操作权限!"
relation_type_exists = _l("Relationship type {} already exists") # 关系类型 {} 已经存在
relation_type_not_found = _l("Relationship type {} does not exist") # 关系类型 {} 不存在
password_save_failed = "保存密码失败: {}"
password_load_failed = "获取密码失败: {}"
attribute_value_invalid = _l("Invalid attribute value: {}") # 无效的属性值: {}
attribute_value_invalid2 = _l("{} Invalid value: {}") # {} 无效的值: {}
not_in_choice_values = _l("{} is not in the predefined values") # {} 不在预定义值里
# 属性 {} 的值必须是唯一的, 当前值 {} 已存在
attribute_value_unique_required = _l("The value of attribute {} must be unique, {} already exists")
attribute_value_required = _l("Attribute {} value must exist") # 属性 {} 值必须存在
attribute_value_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,10 +16,13 @@ def search(query=None,
ret_key=RetKey.NAME,
count=1,
sort=None,
excludes=None):
excludes=None,
use_id_filter=False,
use_ci_filter=True):
if current_app.config.get("USE_ES"):
s = SearchFromES(query, fl, facet, page, ret_key, count, sort)
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

View File

@ -56,13 +56,13 @@ QUERY_CI_BY_ATTR_NAME = """
SELECT {0}.ci_id
FROM {0}
WHERE {0}.attr_id={1:d}
AND {0}.value {2}
AND ({0}.value {2})
"""
QUERY_CI_BY_ID = """
SELECT c_cis.id as ci_id
FROM c_cis
WHERE c_cis.id={}
WHERE c_cis.id {}
"""
QUERY_CI_BY_TYPE = """
@ -107,3 +107,12 @@ FROM
WHERE c_value_index_datetime.value LIKE "{0}") AS {1}
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,16 +4,19 @@
from __future__ import unicode_literals
import copy
import six
import time
from flask import abort
from flask import current_app
from flask_login import current_user
from jinja2 import Template
from sqlalchemy import text
from api.extensions import db
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import CITypeCache
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 ResourceTypeEnum
from api.lib.cmdb.const import RetKey
@ -25,6 +28,7 @@ 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_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_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_UNION_CI_ATTRIBUTE_IS_NULL
from api.lib.cmdb.utils import TableMap
@ -43,7 +47,11 @@ class Search(object):
count=1,
sort=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.fl = fl or []
self.excludes = excludes or []
@ -53,12 +61,20 @@ class Search(object):
self.count = count
self.sort = sort
self.ci_ids = ci_ids or []
self.raw_ci_ids = copy.deepcopy(self.ci_ids)
self.query_sql = ""
self.type_id_list = []
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.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
def _operator_proc(key):
@ -92,52 +108,90 @@ class Search(object):
else:
raise SearchError(ErrFormat.attribute_not_found.format(key))
def _type_query_handler(self, v, queries):
def _type_query_handler(self, v, queries, is_sub=False):
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:
ci_type = CITypeCache.get(_v)
if len(new_v) == 1 and not self.sort and ci_type and ci_type.default_order_attr:
if type_num == 1 and not self.sort and ci_type and ci_type.default_order_attr:
self.sort = ci_type.default_order_attr
if ci_type is not None:
if self.valid_type_names == "ALL" or ci_type.name in self.valid_type_names:
self.type_id_list.append(str(ci_type.id))
if ci_type.id in self.type2filter_perms:
if not is_sub:
self.type_id_list.append(str(ci_type.id))
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')
if ci_filter:
if ci_filter and self.use_ci_filter and not self.use_id_filter:
sub = []
ci_filter = Template(ci_filter).render(user=current_user)
for i in ci_filter.split(','):
if i.startswith("~") and not sub:
queries.append(i)
if type_num == 1:
if i.startswith("~") and not sub:
queries.append(i)
else:
sub.append(i)
else:
sub.append(i)
if sub:
queries.append(dict(operator="&", queries=sub))
if type_num == 1:
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 not self.fl:
self.fl = set(self.type2filter_perms[ci_type.id]['attr_filter'])
if type_num == 1:
if not self.fl:
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:
self.fl = set(self.fl) & set(self.type2filter_perms[ci_type.id]['attr_filter'])
self.fl = self.fl or {}
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:
raise SearchError(ErrFormat.no_permission.format(ci_type.alias, PermEnum.READ))
else:
raise SearchError(ErrFormat.ci_type_not_found2.format(_v))
if self.type_id_list:
type_ids = ",".join(self.type_id_list)
if type_num != len(self.type_id_list) and queries and queries[0].startswith('_type') and not is_sub:
queries[0] = "_type:({})".format(";".join(self.type_id_list))
if type_id_list:
type_ids = ",".join(type_id_list)
_query_sql = QUERY_CI_BY_TYPE.format(type_ids)
if self.only_type_query:
if self.only_type_query or self.multi_type_has_ci_filter:
return _query_sql
else:
return ""
elif type_num > 1: # there must be instance-level access control
return "select c_cis.id as ci_id from c_cis where c_cis.id=0"
return ""
@staticmethod
def _id_query_handler(v):
return QUERY_CI_BY_ID.format(v)
if ";" in v:
return QUERY_CI_BY_ID.format("in {}".format(v.replace(';', ',')))
else:
return QUERY_CI_BY_ID.format("= {}".format(v))
@staticmethod
def _in_query_handler(attr, v, is_not):
@ -151,6 +205,7 @@ class Search(object):
"NOT LIKE" if is_not else "LIKE",
_v.replace("*", "%")) for _v in new_v])
_query_sql = QUERY_CI_BY_ATTR_NAME.format(table_name, attr.id, in_query)
return _query_sql
@staticmethod
@ -166,6 +221,7 @@ class Search(object):
"NOT BETWEEN" if is_not else "BETWEEN",
start.replace("*", "%"), end.replace("*", "%"))
_query_sql = QUERY_CI_BY_ATTR_NAME.format(table_name, attr.id, range_query)
return _query_sql
@staticmethod
@ -182,6 +238,7 @@ class Search(object):
comparison_query = "{0} '{1}'".format(v[0], v[1:].replace("*", "%"))
_query_sql = QUERY_CI_BY_ATTR_NAME.format(table_name, attr.id, comparison_query)
return _query_sql
@staticmethod
@ -193,6 +250,7 @@ class Search(object):
elif field.startswith("-"):
field = field[1:]
sort_type = "DESC"
return field, sort_type
def __sort_by_id(self, sort_type, query_sql):
@ -202,7 +260,7 @@ class Search(object):
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))
elif self.type_id_list:
elif self.type_id_list and not self.multi_type_has_ci_filter:
self.query_sql = "SELECT B.ci_id FROM ({0}) AS B {1}".format(
query_sql,
"INNER JOIN c_cis on c_cis.id=B.ci_id WHERE c_cis.type_id IN ({0}) ".format(
@ -227,7 +285,7 @@ class Search(object):
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}"
if self.type_id_list:
if self.type_id_list and not self.multi_type_has_ci_filter:
self.query_sql = "SELECT B.ci_id FROM ({0}) AS B {1}".format(
query_sql,
"INNER JOIN c_cis on c_cis.id=B.ci_id WHERE c_cis.type_id IN ({0}) ".format(
@ -251,16 +309,23 @@ class Search(object):
(self.page - 1) * self.count, sort_type, self.count))
def __sort_by_field(self, field, sort_type, query_sql):
attr = AttributeCache.get(field)
attr_id = attr.id
if field not in BUILTIN_ATTRIBUTES:
table_name = TableMap(attr=attr).table_name
_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
attr = AttributeCache.get(field)
attr_id = attr.id
if self.only_type_query or not self.type_id_list:
table_name = TableMap(attr=attr).table_name
_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} "
"LIMIT {1:d}, {3};".format(new_table, (self.page - 1) * self.count, sort_type, self.count))
@ -298,7 +363,9 @@ class Search(object):
INNER JOIN ({2}) as {3} USING(ci_id)""".format(query_sql, alias, _query_sql, alias + "A")
elif operator == "|" or operator == "|~":
query_sql = "SELECT * FROM ({0}) as {1} UNION ALL ({2})".format(query_sql, alias, _query_sql)
query_sql = "SELECT * FROM ({0}) as {1} UNION ALL SELECT * FROM ({2}) as {3}".format(query_sql, alias,
_query_sql,
alias + "A")
elif operator == "~":
query_sql = """SELECT * FROM ({0}) as {1} LEFT JOIN ({2}) as {3} USING(ci_id)
@ -312,7 +379,7 @@ class Search(object):
start = time.time()
execute = db.session.execute
# current_app.logger.debug(v_query_sql)
res = execute(v_query_sql).fetchall()
res = execute(text(v_query_sql)).fetchall()
end_time = time.time()
current_app.logger.debug("query ci ids time is: {0}".format(end_time - start))
@ -321,6 +388,11 @@ class Search(object):
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):
"""
:return: _type:(type1;type2)
@ -330,14 +402,23 @@ class Search(object):
self.valid_type_names = {i['name'] for i in res if PermEnum.READ in i['permissions']}
res2 = acl.get_resources(ResourceTypeEnum.CI_FILTER)
if res2:
self.type2filter_perms = CIFilterPermsCRUD().get_by_ids(list(map(int, [i['name'] for i in res2])))
self.__get_type2filter_perms()
for type_id in self.type2filter_perms:
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))
def __confirm_type_first(self, queries):
has_type = False
result = []
@ -356,11 +437,14 @@ class Search(object):
if not q.startswith("("):
raise SearchError(ErrFormat.ci_search_Parentheses_invalid)
operator, q = self._operator_proc(q)
if q.endswith(")"):
result.append(dict(operator=operator, queries=[q[1:-1]]))
if ":" not in q: # multi-line search
result.append(q[1:-1].split(';'))
else:
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:
sub['queries'].append(q[:-1])
result.append(copy.deepcopy(sub))
@ -370,8 +454,10 @@ class Search(object):
else:
result.append(q)
_is_app_admin = is_app_admin('cmdb') or current_user.username == "worker"
if result and not has_type and not _is_app_admin:
if self.parent_node_perm_passed:
self.__get_type2filter_perms()
self.valid_type_names = "ALL"
elif result and not has_type and not self.is_app_admin:
type_q = self.__get_types_has_read()
if id_query:
ci = CIManager.get_by_id(id_query)
@ -380,23 +466,21 @@ class Search(object):
result.insert(0, "_type:{}".format(ci.type_id))
else:
result.insert(0, type_q)
elif _is_app_admin:
elif self.is_app_admin:
self.valid_type_names = "ALL"
else:
self.__get_types_has_read()
current_app.logger.warning(result)
return result
def __query_by_attr(self, q, queries, alias):
def __query_by_attr(self, q, queries, alias, is_sub=False):
k = q.split(":")[0].strip()
v = "\:".join(q.split(":")[1:]).strip()
v = v.replace("'", "\\'")
v = v.replace('"', '\\"')
field, field_type, operator, attr = self._attr_name_proc(k)
if field == "_type":
_query_sql = self._type_query_handler(v, queries)
_query_sql = self._type_query_handler(v, queries, is_sub)
elif field == "_id":
_query_sql = self._id_query_handler(v)
@ -410,6 +494,9 @@ class Search(object):
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
if v.startswith("(") and v.endswith(")"):
_query_sql = self._in_query_handler(attr, v, is_not)
@ -440,26 +527,36 @@ class Search(object):
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 = ""
for q in queries:
# current_app.logger.debug(q)
_query_sql = ""
if isinstance(q, dict):
alias, _query_sql, operator = self.__query_build_by_field(q['queries'], True, True, alias)
current_app.logger.info(_query_sql)
current_app.logger.info((operator, is_first, alias))
operator = q['operator']
if len(q['queries']) == 1 and ";" in q['queries'][0]:
values = q['queries'][0].split(";")
in_values = ",".join("'{0}'".format(v) for v in values)
_query_sql = QUERY_CI_BY_NO_ATTR_IN.format(in_values, alias)
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("*"):
alias, _query_sql, operator = self.__query_by_attr(q, queries, alias)
alias, _query_sql, operator = self.__query_by_attr(q, queries, alias, is_sub)
elif q == "*":
continue
elif q:
q = q.replace("'", "\\'")
q = q.replace('"', '\\"')
q = q.replace("*", "%").replace('\\n', '%')
_query_sql = QUERY_CI_BY_NO_ATTR.format(q, alias)
if not isinstance(q, list):
q = q.replace("'", "\\'")
q = q.replace('"', '\\"')
q = q.replace("*", "%").replace('\\n', '%')
_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:
query_sql = "SELECT * FROM ({0}) AS {1}".format(_query_sql, alias)
@ -478,7 +575,7 @@ class Search(object):
def _filter_ids(self, query_sql):
if self.ci_ids:
return "SELECT * FROM ({0}) AS IN_QUERY WHERE IN_QUERY.ci_id IN ({1})".format(
query_sql, ",".join(list(map(str, self.ci_ids))))
query_sql, ",".join(list(set(map(str, self.ci_ids)))))
return query_sql
@ -503,13 +600,15 @@ class Search(object):
queries = handle_arg_list(self.orig_query)
queries = self._extra_handle_query_expr(queries)
queries = self.__confirm_type_first(queries)
current_app.logger.debug(queries)
_, query_sql, _ = self.__query_build_by_field(queries)
s = time.time()
if 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
# current_app.logger.debug(query_sql)
numfound, res = self._execute_sql(query_sql)
@ -525,7 +624,7 @@ class Search(object):
if k:
table_name = TableMap(attr=attr).table_name
query_sql = FACET_QUERY.format(table_name, self.query_sql, attr.id)
result = db.session.execute(query_sql).fetchall()
result = db.session.execute(text(query_sql)).fetchall()
facet[k] = result
facet_result = dict()
@ -538,17 +637,22 @@ class Search(object):
return facet_result
def _fl_build(self):
_fl = list()
for f in self.fl:
k, _, _, _ = self._attr_name_proc(f)
if k:
_fl.append(k)
if isinstance(self.fl, list):
_fl = list()
for f in self.fl:
k, _, _, _ = self._attr_name_proc(f)
if k:
_fl.append(k)
return _fl
return _fl
else:
return self.fl
def search(self):
numfound, ci_ids = self._query_build_raw()
ci_ids = list(map(str, ci_ids))
if self.only_ids:
return ci_ids
_fl = self._fl_build()
@ -561,6 +665,8 @@ class Search(object):
if ci_ids:
response = CIManager.get_cis_by_ids(ci_ids, ret_key=self.ret_key, fields=_fl, excludes=self.excludes)
for res in response:
if not res:
continue
ci_type = res.get("ci_type")
if ci_type not in counter.keys():
counter[ci_type] = 0
@ -568,3 +674,8 @@ class Search(object):
total = len(response)
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,24 +1,40 @@
# -*- coding:utf-8 -*-
import json
from collections import Counter
from collections import defaultdict
import copy
import json
import networkx as nx
import sys
from flask import abort
from flask import current_app
from flask_login import current_user
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 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_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.search.ci.db.search import Search as SearchFromDB
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 CITypeRelation
from api.models.cmdb import RelationType
class Search(object):
def __init__(self, root_id,
def __init__(self, root_id=None,
level=None,
query=None,
fl=None,
@ -26,7 +42,11 @@ class Search(object):
page=1,
count=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.fl = fl
self.facet_field = facet_field
@ -38,33 +58,113 @@ class Search(object):
self.level = level or 0
self.reverse = reverse
def _get_ids(self):
self.level2constraint = CITypeRelationManager.get_level2constraint(
root_id[0] if root_id and isinstance(root_id, list) else root_id,
level[0] if isinstance(level, list) and level else level)
self.ancestor_ids = ancestor_ids
self.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 = []
ids = [self.root_id] if not isinstance(self.root_id, list) else self.root_id
key = []
_tmp = []
for level in range(1, sorted(self.level)[-1] + 1):
_tmp = list(map(lambda x: list(json.loads(x).keys()),
filter(lambda x: x is not None, rd.get(ids, REDIS_PREFIX_CI_RELATION) or [])))
if len(self.descendant_ids or []) >= level and self.type2filter_perms.get(self.descendant_ids[level - 1]):
id_filter_limit, _ = self._get_ci_filter(self.type2filter_perms[self.descendant_ids[level - 1]])
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]
if level in self.level:
merge_ids.extend(ids)
return merge_ids
def _get_reverse_ids(self):
def _get_reverse_ids(self, ids):
merge_ids = []
ids = [self.root_id] if not isinstance(self.root_id, list) else self.root_id
level2ids = {}
for level in range(1, sorted(self.level)[-1] + 1):
ids = CIRelationManager.get_ancestor_ids(ids, 1)
ids, _level2ids = CIRelationManager.get_ancestor_ids(ids, 1)
if _level2ids.get(2):
level2ids[level + 1] = _level2ids[2]
if level in self.level:
merge_ids.extend(ids)
if level in level2ids and level2ids[level]:
merge_ids.extend(set(ids) & set(level2ids[level]))
else:
merge_ids.extend(ids)
return merge_ids
def search(self):
def _has_read_perm_from_parent_nodes(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
cis = [CI.get_by_id(_id) or abort(404, ErrFormat.ci_not_found.format("id={}".format(_id))) for _id in ids]
merge_ids = self._get_ids() if not self.reverse else self._get_reverse_ids()
merge_ids = self._get_ids(ids) if not self.reverse else self._get_reverse_ids(ids)
if not self.orig_query or ("_type:" not in self.orig_query
and "type_id:" not in self.orig_query
@ -76,11 +176,11 @@ class Search(object):
type_ids.extend(CITypeRelationManager.get_child_type_ids(ci.type_id, level))
else:
type_ids.extend(CITypeRelationManager.get_parent_type_ids(ci.type_id, level))
type_ids = list(set(type_ids))
type_ids = set(type_ids)
if self.orig_query:
self.orig_query = "_type:({0}),{1}".format(";".join(list(map(str, type_ids))), self.orig_query)
self.orig_query = "_type:({0}),{1}".format(";".join(map(str, type_ids)), self.orig_query)
else:
self.orig_query = "_type:({0})".format(";".join(list(map(str, type_ids))))
self.orig_query = "_type:({0})".format(";".join(map(str, type_ids)))
if not merge_ids:
# cis, counter, total, self.page, numfound, facet_
@ -101,39 +201,142 @@ class Search(object):
page=self.page,
count=self.count,
sort=self.sort,
ci_ids=merge_ids).search()
ci_ids=merge_ids,
parent_node_perm_passed=parent_node_perm_passed,
use_ci_filter=use_ci_filter,
only_ids=only_ids).search()
def statistics(self, type_ids):
self.level = int(self.level)
_tmp = []
ids = [self.root_id] if not isinstance(self.root_id, list) else self.root_id
for lv in range(0, self.level):
if not lv:
if type_ids and lv == self.level - 1:
_tmp = list(map(lambda x: [i for i in x if i[1] in type_ids],
(map(lambda x: list(json.loads(x).items()),
[i or '{}' for i in rd.get(ids, REDIS_PREFIX_CI_RELATION) or []]))))
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:
_tmp = list(map(lambda x: list(json.loads(x).items()),
[i or '{}' for i in rd.get(ids, REDIS_PREFIX_CI_RELATION) or []]))
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])))
ids = [self.root_id] if not isinstance(self.root_id, list) else self.root_id
_tmp, tmp_res = [], []
level2ids = {}
for lv in range(1, self.level + 1):
level2ids[lv] = []
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:
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):
if item:
if type_ids and lv == self.level - 1:
__tmp = list(
map(lambda x: [(_id, type_id) for _id, type_id in json.loads(x).items()
if type_id in type_ids],
filter(lambda x: x is not None,
rd.get([i[0] for i in item], REDIS_PREFIX_CI_RELATION) or [])))
if not self.has_m2m:
key, prefix = [i[0] for i in item], REDIS_PREFIX_CI_RELATION
else:
key = list(set(['{},{}'.format(j, i[0]) for i in item for j in level2ids[lv - 1][idx]]))
prefix = REDIS_PREFIX_CI_RELATION2
__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 [])))
level2ids[lv].append(key)
_tmp[idx] = [j for i in __tmp for j in i]
if key:
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:
_tmp[idx] = []
level2ids[lv].append([])
result = {str(_id): len(_tmp[idx]) for idx, _id in enumerate(ids)}
@ -141,3 +344,251 @@ class Search(object):
detail={str(_id): dict(Counter([i[1] for i in _tmp[idx]]).items()) for idx, _id in enumerate(ids)})
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

@ -0,0 +1,251 @@
# -*- 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,25 +7,52 @@ import json
import re
import six
from flask import current_app
import api.models.cmdb as model
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.const import ValueTypeEnum
from api.lib.cmdb.resp_format import ErrFormat
TIME_RE = re.compile(r"^20|21|22|23|[0-1]\d:[0-5]\d:[0-5]\d$")
TIME_RE = re.compile(r'(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d')
class ValueDeserializeError(Exception):
pass
def string2int(x):
return int(float(x))
v = int(float(x))
if v > 2147483647:
raise ValueDeserializeError(ErrFormat.attribute_value_out_of_range)
return v
def str2datetime(x):
def str2date(x):
try:
return datetime.datetime.strptime(x, "%Y-%m-%d").date()
except ValueError:
pass
return datetime.datetime.strptime(x, "%Y-%m-%d %H:%M:%S")
try:
return datetime.datetime.strptime(x, "%Y-%m-%d %H:%M:%S").date()
except ValueError:
pass
def str2datetime(x):
x = x.replace('T', ' ')
x = x.replace('Z', '')
try:
return datetime.datetime.strptime(x, "%Y-%m-%d %H:%M:%S")
except ValueError:
pass
return datetime.datetime.strptime(x, "%Y-%m-%d %H:%M")
class ValueTypeMap(object):
@ -35,8 +62,9 @@ class ValueTypeMap(object):
ValueTypeEnum.TEXT: lambda x: x,
ValueTypeEnum.TIME: lambda x: TIME_RE.findall(x)[0],
ValueTypeEnum.DATETIME: str2datetime,
ValueTypeEnum.DATE: str2datetime,
ValueTypeEnum.DATE: str2date,
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 = {
@ -47,6 +75,7 @@ class ValueTypeMap(object):
ValueTypeEnum.DATE: lambda x: x.strftime("%Y-%m-%d") if not isinstance(x, six.string_types) else x,
ValueTypeEnum.DATETIME: lambda x: x.strftime("%Y-%m-%d %H:%M:%S") if not isinstance(x, six.string_types) else x,
ValueTypeEnum.JSON: lambda x: json.loads(x) if isinstance(x, six.string_types) and x else x,
ValueTypeEnum.BOOL: lambda x: x in current_app.config.get('BOOL_TRUE'),
}
serialize2 = {
@ -57,6 +86,7 @@ class ValueTypeMap(object):
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.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 = {
@ -78,6 +108,7 @@ class ValueTypeMap(object):
'index_{0}'.format(ValueTypeEnum.TIME): model.CIIndexValueText,
'index_{0}'.format(ValueTypeEnum.FLOAT): model.CIIndexValueFloat,
'index_{0}'.format(ValueTypeEnum.JSON): model.CIValueJson,
'index_{0}'.format(ValueTypeEnum.BOOL): model.CIIndexValueInteger,
}
table_name = {
@ -90,6 +121,7 @@ class ValueTypeMap(object):
'index_{0}'.format(ValueTypeEnum.TIME): 'c_value_index_texts',
'index_{0}'.format(ValueTypeEnum.FLOAT): 'c_value_index_floats',
'index_{0}'.format(ValueTypeEnum.JSON): 'c_value_json',
'index_{0}'.format(ValueTypeEnum.BOOL): 'c_value_index_integers',
}
es_type = {

View File

@ -3,16 +3,18 @@
from __future__ import unicode_literals
import copy
import imp
import os
import tempfile
import importlib.util
import copy
import jinja2
import os
import re
import tempfile
from flask import abort
from flask import current_app
from jinja2schema import infer
from jinja2schema import to_json_schema
from werkzeug.exceptions import BadRequest
from api.extensions import db
from api.lib.cmdb.attribute import AttributeManager
@ -23,6 +25,7 @@ from api.lib.cmdb.const import ValueTypeEnum
from api.lib.cmdb.history import AttributeHistoryManger
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.utils import TableMap
from api.lib.cmdb.utils import ValueDeserializeError
from api.lib.cmdb.utils import ValueTypeMap
from api.lib.utils import handle_arg_list
from api.models.cmdb import CI
@ -44,7 +47,7 @@ class AttributeValueManager(object):
"""
return AttributeCache.get(key)
def get_attr_values(self, fields, ci_id, ret_key="name", unique_key=None, use_master=False):
def get_attr_values(self, fields, ci_id, ret_key="name", unique_key=None, use_master=False, enum_map=None):
"""
:param fields:
@ -52,6 +55,7 @@ class AttributeValueManager(object):
:param ret_key: It can be name or alias
:param unique_key: primary attribute
:param use_master: Only for master-slave read-write separation
:param enum_map:
:return:
"""
res = dict()
@ -73,6 +77,12 @@ class AttributeValueManager(object):
else:
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:
res['unique'] = unique_key.name
res['unique_alias'] = unique_key.alias
@ -80,22 +90,31 @@ class AttributeValueManager(object):
return res
@staticmethod
def _deserialize_value(value_type, value):
def _deserialize_value(alias, value_type, value):
if not value:
return value
deserialize = ValueTypeMap.deserialize[value_type]
try:
v = deserialize(value)
if value_type in (ValueTypeEnum.DATE, ValueTypeEnum.DATETIME):
return str(v)
return v
except ValueDeserializeError as e:
return abort(400, ErrFormat.attribute_value_invalid2.format(alias, e))
except ValueError:
return abort(400, ErrFormat.attribute_value_invalid.format(value))
return abort(400, ErrFormat.attribute_value_invalid2.format(alias, value))
@staticmethod
def _check_is_choice(attr, value_type, value):
choice_values = AttributeManager.get_choice_values(attr.id, value_type, attr.choice_web_hook, attr.choice_other)
if str(value) not in list(map(str, [i[0] for i in choice_values])):
return abort(400, ErrFormat.not_in_choice_values.format(value))
if value_type == ValueTypeEnum.FLOAT:
if float(value) not in list(map(float, [i[0] for i in choice_values])):
return abort(400, ErrFormat.not_in_choice_values.format(value))
else:
if str(value) not in list(map(str, [i[0] for i in choice_values])):
return abort(400, ErrFormat.not_in_choice_values.format(value))
@staticmethod
def _check_is_unique(value_table, attr, ci_id, type_id, value):
@ -112,19 +131,33 @@ class AttributeValueManager(object):
if type_attr and type_attr.is_required and not value and value != 0:
return abort(400, ErrFormat.attribute_value_required.format(attr.alias))
def _validate(self, attr, value, value_table, ci=None, type_id=None, ci_id=None, type_attr=None):
ci = ci or {}
v = self._deserialize_value(attr.value_type, value)
@staticmethod
def check_re(expr, alias, value):
if not re.compile(expr).match(str(value)):
return abort(400, ErrFormat.attribute_value_invalid2.format(alias, value))
attr.is_choice and value and self._check_is_choice(attr, attr.value_type, v)
attr.is_unique and self._check_is_unique(
def _validate(self, attr, value, value_table, ci=None, type_id=None, ci_id=None, type_attr=None, unique_name=None):
if not attr.is_reference:
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)
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,):
v = None
if attr.re_check and value:
self.check_re(attr.re_check, attr.alias, value)
return v
@staticmethod
@ -132,9 +165,10 @@ class AttributeValueManager(object):
return AttributeHistoryManger.add(record_id, ci_id, [(attr_id, operate_type, old, new)], type_id)
@staticmethod
def write_change2(changed, record_id=None):
def write_change2(changed, record_id=None, ticket_id=None):
for ci_id, attr_id, operate_type, old, new, type_id in changed:
record_id = AttributeHistoryManger.add(record_id, ci_id, [(attr_id, operate_type, old, new)], type_id,
ticket_id=ticket_id,
commit=False, flush=False)
try:
db.session.commit()
@ -164,11 +198,11 @@ class AttributeValueManager(object):
try:
path = script_f.name
dir_name, name = os.path.dirname(path), os.path.basename(path)[:-3]
name = os.path.basename(path)[:-3]
fp, path, desc = imp.find_module(name, [dir_name])
mod = imp.load_module(name, fp, path, desc)
spec = importlib.util.spec_from_file_location(name, path)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
if hasattr(mod, 'computed'):
return mod.computed()
@ -203,7 +237,10 @@ class AttributeValueManager(object):
if computed_value is not None:
ci_dict[attr['name']] = computed_value
def valid_attr_value(self, ci_dict, type_id, ci_id, name2attr, alias2attr=None, ci_attr2type_attr=None):
def valid_attr_value(self, ci_dict, type_id, ci_id, name2attr,
alias2attr=None,
ci_attr2type_attr=None,
unique_name=None):
key2attr = dict()
alias2attr = alias2attr or {}
ci_attr2type_attr = ci_attr2type_attr or {}
@ -216,17 +253,29 @@ class AttributeValueManager(object):
try:
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,
type_attr=ci_attr2type_attr.get(attr.id))
for i in handle_arg_list(value)]
ci_dict[key] = value_list
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)
if not value_list:
self._check_is_required(type_id, attr, '')
else:
value = self._validate(attr, value, value_table, ci=None, type_id=type_id, ci_id=ci_id,
type_attr=ci_attr2type_attr.get(attr.id))
type_attr=ci_attr2type_attr.get(attr.id),
unique_name=unique_name)
ci_dict[key] = value
except BadRequest as e:
raise
except Exception as e:
current_app.logger.warning(str(e))
@ -235,15 +284,17 @@ class AttributeValueManager(object):
return key2attr
def create_or_update_attr_value(self, ci, ci_dict, key2attr):
def create_or_update_attr_value(self, ci, ci_dict, key2attr, ticket_id=None):
"""
add or update attribute value, then write history
:param ci: instance object
:param ci_dict: attribute dict
:param key2attr: attr key to attr
:param ticket_id:
:return:
"""
changed = []
has_dynamic = False
for key, value in ci_dict.items():
attr = key2attr.get(key)
if not attr:
@ -252,46 +303,90 @@ class AttributeValueManager(object):
if attr.is_list:
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) - 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))
existed_values = [(ValueTypeMap.serialize[attr.value_type](i.value) if
i.value or i.value == 0 else i.value) for i in existed_attrs]
for v in deleted:
existed_attr = existed_attrs[existed_values.index(v)]
existed_attr.delete(flush=False, commit=False)
changed.append((ci.id, attr.id, OperateType.DELETE, v, None, ci.type_id))
if isinstance(value, dict):
if value.get('op') == "add":
for v in (value.get('v') or []):
if v not in existed_values:
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:
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 = (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:
value_table.create(ci_id=ci.id, attr_id=attr.id, value=value, flush=False, commit=False)
changed.append((ci.id, attr.id, OperateType.ADD, None, value, ci.type_id))
if not attr.is_dynamic:
changed.append((ci.id, attr.id, OperateType.ADD, None, value, ci.type_id))
else:
has_dynamic = True
else:
if existed_value != value:
if existed_value != value and existed_attr:
if value is None:
existed_attr.delete(flush=False, commit=False)
else:
existed_attr.update(value=value, flush=False, commit=False)
changed.append((ci.id, attr.id, OperateType.UPDATE, existed_value, value, ci.type_id))
if not attr.is_dynamic:
changed.append((ci.id, attr.id, OperateType.UPDATE, existed_value, value, ci.type_id))
else:
has_dynamic = True
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(e.args[0]))
if changed or has_dynamic:
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(e.args[0]))
return self.write_change2(changed)
return self.write_change2(changed, ticket_id=ticket_id), has_dynamic
else:
return None, has_dynamic
@staticmethod
def delete_attr_value(attr_id, ci_id):
def delete_attr_value(attr_id, ci_id, commit=True):
attr = AttributeCache.get(attr_id)
if attr is not None:
value_table = TableMap(attr=attr).table
for item in value_table.get_by(attr_id=attr.id, ci_id=ci_id, to_dict=False):
item.delete()
item.delete(commit=commit)

View File

@ -10,6 +10,11 @@ from api.lib.perm.acl.role import RoleCRUD, RoleRelationCRUD
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):
def __init__(self, app_name='acl', uid=None):
self.log = current_app.logger
@ -80,20 +85,22 @@ class ACLManager(object):
return role.to_dict()
@staticmethod
def delete_role(_id, payload):
def delete_role(_id):
RoleCRUD.delete_role(_id)
return dict(rid=_id)
def get_user_info(self, username):
from api.lib.perm.acl.acl import ACLManager as ACL
user_info = ACL().get_user_info(username, self.app_name)
result = dict(name=user_info.get('nickname') or username,
username=user_info.get('username') or username,
email=user_info.get('email'),
uid=user_info.get('uid'),
rid=user_info.get('rid'),
role=dict(permissions=user_info.get('parents')),
avatar=user_info.get('avatar'))
result = dict(
name=user_info.get('nickname') or username,
username=user_info.get('username') or username,
email=user_info.get('email'),
uid=user_info.get('uid'),
rid=user_info.get('rid'),
role=dict(permissions=user_info.get('parents')),
avatar=user_info.get('avatar')
)
return result
@ -131,7 +138,8 @@ class ACLManager(object):
numfound, res = ResourceCRUD.search(q, u, self.validate_app().id, rt_id, page, page_size)
return res
def grant_resource(self, rid, resource_id, perms):
@staticmethod
def grant_resource(rid, resource_id, perms):
PermissionCRUD.grant(rid, perms, resource_id=resource_id, group_id=None)
@staticmethod
@ -139,3 +147,7 @@ class ACLManager(object):
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,14 +1,24 @@
from flask import abort
import copy
import json
from flask import abort, current_app
from ldap3 import Connection
from ldap3 import Server
from ldap3.core.exceptions import LDAPBindError, LDAPSocketOpenError
from ldap3 import AUTO_BIND_NO_TLS
from api.extensions import db
from api.lib.common_setting.resp_format import ErrFormat
from api.models.common_setting import CommonData
from api.lib.utils import AESCrypto
from api.lib.common_setting.const import AuthCommonConfig, AuthenticateType, AuthCommonConfigAutoRedirect, TestType
class CommonDataCRUD(object):
@staticmethod
def get_data_by_type(data_type):
CommonDataCRUD.check_auth_type(data_type)
return CommonData.get_by(data_type=data_type)
@staticmethod
@ -18,6 +28,8 @@ class CommonDataCRUD(object):
@staticmethod
def create_new_data(data_type, **kwargs):
try:
CommonDataCRUD.check_auth_type(data_type)
return CommonData.create(data_type=data_type, **kwargs)
except Exception as e:
db.session.rollback()
@ -29,6 +41,7 @@ class CommonDataCRUD(object):
if not existed:
abort(404, ErrFormat.common_data_not_found.format(_id))
try:
CommonDataCRUD.check_auth_type(existed.data_type)
return existed.update(**kwargs)
except Exception as e:
db.session.rollback()
@ -40,7 +53,230 @@ class CommonDataCRUD(object):
if not existed:
abort(404, ErrFormat.common_data_not_found.format(_id))
try:
CommonDataCRUD.check_auth_type(existed.data_type)
existed.soft_delete()
except Exception as e:
db.session.rollback()
abort(400, str(e))
@staticmethod
def check_auth_type(data_type):
if data_type in list(AuthenticateType.all()) + [AuthCommonConfig]:
abort(400, ErrFormat.common_data_not_support_auth_type.format(data_type))
@staticmethod
def set_auth_auto_redirect_enable(_value: int):
existed = CommonData.get_by(first=True, data_type=AuthCommonConfig, to_dict=False)
if not existed:
CommonDataCRUD.create_new_data(AuthCommonConfig, data={AuthCommonConfigAutoRedirect: _value})
else:
data = existed.data
data = copy.deepcopy(existed.data) if data else {}
data[AuthCommonConfigAutoRedirect] = _value
CommonDataCRUD.update_data(existed.id, data=data)
return True
@staticmethod
def get_auth_auto_redirect_enable():
existed = CommonData.get_by(first=True, data_type=AuthCommonConfig)
if not existed:
return 0
data = existed.get('data', {})
if not data:
return 0
return data.get(AuthCommonConfigAutoRedirect, 0)
class AuthenticateDataCRUD(object):
common_type_list = [AuthCommonConfig]
def __init__(self, _type):
self._type = _type
self.record = None
self.decrypt_data = {}
def get_support_type_list(self):
return list(AuthenticateType.all()) + self.common_type_list
def get(self):
if not self.decrypt_data:
self.decrypt_data = self.get_decrypt_data()
return self.decrypt_data
def get_by_key(self, _key):
if not self.decrypt_data:
self.decrypt_data = self.get_decrypt_data()
return self.decrypt_data.get(_key, None)
def get_record(self, to_dict=False) -> CommonData:
return CommonData.get_by(first=True, data_type=self._type, to_dict=to_dict)
def get_record_with_decrypt(self) -> dict:
record = CommonData.get_by(first=True, data_type=self._type, to_dict=True)
if not record:
return {}
data = self.get_decrypt_dict(record.get('data', ''))
record['data'] = data
return record
def get_decrypt_dict(self, data):
decrypt_str = self.decrypt(data)
try:
return json.loads(decrypt_str)
except Exception as e:
abort(400, str(e))
def get_decrypt_data(self) -> dict:
self.record = self.get_record()
if not self.record:
return self.get_from_config()
return self.get_decrypt_dict(self.record.data)
def get_from_config(self):
return current_app.config.get(self._type, {})
def check_by_type(self) -> None:
existed = self.get_record()
if existed:
abort(400, ErrFormat.common_data_already_existed.format(self._type))
def create(self, data) -> CommonData:
self.check_by_type()
encrypt = data.pop('encrypt', None)
if encrypt is False:
return CommonData.create(data_type=self._type, data=data)
encrypted_data = self.encrypt(data)
try:
return CommonData.create(data_type=self._type, data=encrypted_data)
except Exception as e:
db.session.rollback()
abort(400, str(e))
def update_by_record(self, record, data) -> CommonData:
encrypt = data.pop('encrypt', None)
if encrypt is False:
return record.update(data=data)
encrypted_data = self.encrypt(data)
try:
return record.update(data=encrypted_data)
except Exception as e:
db.session.rollback()
abort(400, str(e))
def update(self, _id, data) -> CommonData:
existed = CommonData.get_by(first=True, to_dict=False, id=_id)
if not existed:
abort(404, ErrFormat.common_data_not_found.format(_id))
return self.update_by_record(existed, data)
@staticmethod
def delete(_id) -> None:
existed = CommonData.get_by(first=True, to_dict=False, id=_id)
if not existed:
abort(404, ErrFormat.common_data_not_found.format(_id))
try:
existed.soft_delete()
except Exception as e:
db.session.rollback()
abort(400, str(e))
@staticmethod
def encrypt(data) -> str:
if type(data) is dict:
try:
data = json.dumps(data)
except Exception as e:
abort(400, str(e))
return AESCrypto().encrypt(data)
@staticmethod
def decrypt(data) -> str:
return AESCrypto().decrypt(data)
@staticmethod
def get_enable_list():
all_records = CommonData.query.filter(
CommonData.data_type.in_(AuthenticateType.all()),
CommonData.deleted == 0
).all()
enable_list = []
for auth_type in AuthenticateType.all():
record = list(filter(lambda x: x.data_type == auth_type, all_records))
if not record:
config = current_app.config.get(auth_type, None)
if not config:
continue
if config.get('enable', False):
enable_list.append(dict(
auth_type=auth_type,
))
continue
try:
decrypt_data = json.loads(AuthenticateDataCRUD.decrypt(record[0].data))
except Exception as e:
current_app.logger.error(e)
continue
if decrypt_data.get('enable', 0) == 1:
enable_list.append(dict(
auth_type=auth_type,
))
auth_auto_redirect = CommonDataCRUD.get_auth_auto_redirect_enable()
return dict(
enable_list=enable_list,
auth_auto_redirect=auth_auto_redirect,
)
def test(self, test_type, data):
type_lower = self._type.lower()
func_name = f'test_{type_lower}'
if hasattr(self, func_name):
try:
return getattr(self, f'test_{type_lower}')(test_type, data)
except Exception as e:
abort(400, str(e))
abort(400, ErrFormat.not_support_test.format(self._type))
@staticmethod
def test_ldap(test_type, data):
ldap_server = data.get('ldap_server')
ldap_user_dn = data.get('ldap_user_dn', '{}')
server = Server(ldap_server, connect_timeout=2)
if not server.check_availability():
raise Exception(ErrFormat.ldap_server_connect_not_available)
else:
if test_type == TestType.Connect:
return True
username = data.get('username', None)
if not username:
raise Exception(ErrFormat.ldap_test_username_required)
user = ldap_user_dn.format(username)
password = data.get('password', None)
try:
Connection(server, user=user, password=password, auto_bind=AUTO_BIND_NO_TLS)
except LDAPBindError:
ldap_domain = data.get('ldap_domain')
user_with_domain = f"{username}@{ldap_domain}"
try:
Connection(server, user=user_with_domain, password=password, auto_bind=AUTO_BIND_NO_TLS)
except Exception as e:
raise Exception(ErrFormat.ldap_test_unknown_error.format(str(e)))
except LDAPSocketOpenError:
raise Exception(ErrFormat.ldap_server_connect_timeout)
except Exception as e:
raise Exception(ErrFormat.ldap_test_unknown_error.format(str(e)))
return True

View File

@ -1,4 +1,6 @@
# -*- coding:utf-8 -*-
from urllib.parse import urlparse
from api.extensions import cache
from api.models.common_setting import CompanyInfo
@ -11,6 +13,7 @@ class CompanyInfoCRUD(object):
@staticmethod
def create(**kwargs):
CompanyInfoCRUD.check_data(**kwargs)
res = CompanyInfo.create(**kwargs)
CompanyInfoCache.refresh(res.info)
return res
@ -22,10 +25,26 @@ class CompanyInfoCRUD(object):
if not existed:
existed = CompanyInfoCRUD.create(**kwargs)
else:
CompanyInfoCRUD.check_data(**kwargs)
existed = existed.update(**kwargs)
CompanyInfoCache.refresh(existed.info)
return existed
@staticmethod
def check_data(**kwargs):
info = kwargs.get('info', {})
info['messenger'] = CompanyInfoCRUD.check_messenger(info.get('messenger', None))
kwargs['info'] = info
@staticmethod
def check_messenger(messenger):
if not messenger:
return messenger
parsed_url = urlparse(messenger)
return f"{parsed_url.scheme}://{parsed_url.netloc}"
class CompanyInfoCache(object):
key = 'CompanyInfoCache::'
@ -41,4 +60,4 @@ class CompanyInfoCache(object):
@classmethod
def refresh(cls, info):
cache.set(cls.key, info)
cache.set(cls.key, info)

View File

@ -19,3 +19,48 @@ BotNameMap = {
'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

@ -0,0 +1,39 @@
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 -*-
from flask import abort
from flask import abort, current_app
from treelib import Tree
from wtforms import Form
from wtforms import IntegerField
@ -9,6 +9,7 @@ from wtforms import validators
from api.extensions import db
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.models.common_setting import Department, Employee
@ -23,7 +24,15 @@ def get_all_department_list(to_dict=True):
*criterion
).order_by(Department.department_id.asc())
results = query.all()
return [r.to_dict() for r in results] if to_dict else results
if to_dict:
datas = []
for r in results:
d = r.to_dict()
if r.department_id == 0:
d['department_name'] = ErrFormat.company_wide
datas.append(d)
return datas
return results
def get_all_employee_list(block=0, to_dict=True):
@ -100,6 +109,7 @@ class DepartmentTree(object):
employees = self.get_employees_by_d_id(department_id)
top_d['employees'] = employees
top_d['department_name'] = ErrFormat.company_wide
if len(sub_deps) == 0:
top_d[sub_departments_column_name] = []
d_list.append(top_d)
@ -152,6 +162,10 @@ class DepartmentForm(Form):
class DepartmentCRUD(object):
@staticmethod
def get_department_by_id(d_id, to_dict=True):
return Department.get_by(first=True, department_id=d_id, to_dict=to_dict)
@staticmethod
def add(**kwargs):
DepartmentCRUD.check_department_name_unique(kwargs['department_name'])
@ -186,10 +200,11 @@ class DepartmentCRUD(object):
filter(lambda d: d['department_id'] == department_parent_id, allow_p_d_id_list))
if len(target) == 0:
try:
d = Department.get_by(
dep = Department.get_by(
first=True, to_dict=False, department_id=department_parent_id)
name = d.department_name if d else ErrFormat.department_id_not_found.format(department_parent_id)
name = dep.department_name if dep else ErrFormat.department_id_not_found.format(department_parent_id)
except Exception as e:
current_app.logger.error(str(e))
name = ErrFormat.department_id_not_found.format(department_parent_id)
abort(400, ErrFormat.cannot_to_be_parent_department.format(name))
@ -240,7 +255,7 @@ class DepartmentCRUD(object):
return abort(400, ErrFormat.acl_update_role_failed.format(str(e)))
try:
existed.update(**kwargs)
return existed.update(**kwargs)
except Exception as e:
return abort(400, str(e))
@ -253,7 +268,7 @@ class DepartmentCRUD(object):
try:
RoleCRUD.delete_role(existed.acl_rid)
except Exception as e:
pass
current_app.logger.error(str(e))
return existed.soft_delete()
@ -268,7 +283,7 @@ class DepartmentCRUD(object):
try:
tree.remove_subtree(department_id)
except Exception as e:
pass
current_app.logger.error(str(e))
[allow_d_id_list.append({'department_id': int(n.identifier), 'department_name': n.tag}) for n in
tree.all_nodes()]
@ -307,6 +322,7 @@ class DepartmentCRUD(object):
tree_list = []
for top_d in top_deps:
top_d['department_name'] = ErrFormat.company_wide
tree = Tree()
identifier_root = top_d['department_id']
tree.create_node(
@ -377,6 +393,9 @@ class DepartmentCRUD(object):
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
@staticmethod
@ -390,6 +409,151 @@ class DepartmentCRUD(object):
[id_list.append(int(n.identifier))
for n in tmp_tree.all_nodes()]
except Exception as e:
pass
current_app.logger.error(str(e))
return id_list
class EditDepartmentInACL(object):
@staticmethod
def add_department_to_acl(department_id, op_uid):
db_department = DepartmentCRUD.get_department_by_id(department_id, to_dict=False)
if not db_department:
return
from api.models.acl import Role
role = Role.get_by(first=True, name=db_department.department_name, app_id=None)
acl = ACLManager('acl', str(op_uid))
if role is None:
payload = {
'app_id': 'acl',
'name': db_department.department_name,
}
role = acl.create_role(payload)
acl_rid = role.get('id') if role else 0
db_department.update(
acl_rid=acl_rid
)
info = f"add_department_to_acl, acl_rid: {acl_rid}"
current_app.logger.info(info)
return info
@staticmethod
def delete_department_from_acl(department_rids, op_uid):
acl = ACLManager('acl', str(op_uid))
result = []
for rid in department_rids:
try:
acl.delete_role(rid)
except Exception as e:
result.append(f"delete_department_in_acl, rid: {rid}, error: {e}")
continue
return result
@staticmethod
def edit_department_name_in_acl(d_rid: int, d_name: str, op_uid: int):
acl = ACLManager('acl', str(op_uid))
payload = {
'name': d_name
}
try:
acl.edit_role(d_rid, payload)
except Exception as e:
return f"edit_department_name_in_acl, rid: {d_rid}, error: {e}"
return f"edit_department_name_in_acl, rid: {d_rid}, success"
@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

@ -4,7 +4,7 @@ import traceback
from datetime import datetime
import requests
from flask import abort
from flask import abort, current_app
from flask_login import current_user
from sqlalchemy import or_, literal_column, func, not_, and_
from werkzeug.datastructures import MultiDict
@ -15,10 +15,13 @@ from wtforms import validators
from api.extensions import db
from api.lib.common_setting.acl import ACLManager
from api.lib.common_setting.const import COMMON_SETTING_QUEUE, OperatorType
from api.lib.common_setting.const import OperatorType
from api.lib.perm.acl.const import ACL_QUEUE
from api.lib.common_setting.resp_format import ErrFormat
from api.models.common_setting import Employee, Department
from api.tasks.common_setting import refresh_employee_acl_info, edit_employee_department_in_acl
acl_user_columns = [
'email',
'mobile',
@ -137,7 +140,9 @@ class EmployeeCRUD(object):
@staticmethod
def add(**kwargs):
try:
return CreateEmployee().create_single(**kwargs)
res = CreateEmployee().create_single(**kwargs)
refresh_employee_acl_info.apply_async(args=(res.employee_id,), queue=ACL_QUEUE)
return res
except Exception as e:
abort(400, str(e))
@ -164,10 +169,9 @@ class EmployeeCRUD(object):
existed.update(**kwargs)
if len(e_list) > 0:
from api.tasks.common_setting import edit_employee_department_in_acl
edit_employee_department_in_acl.apply_async(
args=(e_list, new_department_id, current_user.uid),
queue=COMMON_SETTING_QUEUE
queue=ACL_QUEUE
)
return existed
@ -178,7 +182,7 @@ class EmployeeCRUD(object):
def edit_employee_by_uid(_uid, **kwargs):
existed = EmployeeCRUD.get_employee_by_uid(_uid)
try:
user = edit_acl_user(_uid, **kwargs)
edit_acl_user(_uid, **kwargs)
for column in employee_pop_columns:
if kwargs.get(column):
@ -190,9 +194,9 @@ class EmployeeCRUD(object):
@staticmethod
def change_password_by_uid(_uid, password):
existed = EmployeeCRUD.get_employee_by_uid(_uid)
EmployeeCRUD.get_employee_by_uid(_uid)
try:
user = edit_acl_user(_uid, password=password)
edit_acl_user(_uid, password=password)
except Exception as e:
return abort(400, str(e))
@ -291,7 +295,9 @@ class EmployeeCRUD(object):
employees = []
for r in pagination.items:
d = r.Employee.to_dict()
d['department_name'] = r.Department.department_name
d['department_name'] = r.Department.department_name if r.Department else ''
if r.Employee.department_id == 0:
d['department_name'] = ErrFormat.company_wide
employees.append(d)
return {
@ -359,9 +365,11 @@ class EmployeeCRUD(object):
if value and column == "last_login":
try:
value = datetime.strptime(value, "%Y-%m-%d %H:%M:%S")
return datetime.strptime(value, "%Y-%m-%d %H:%M:%S")
except Exception as e:
abort(400, ErrFormat.datetime_format_error.format(column))
err = f"{ErrFormat.datetime_format_error.format(column)}: {str(e)}"
abort(400, err)
return value
@staticmethod
def get_attr_by_column(column):
@ -382,7 +390,7 @@ class EmployeeCRUD(object):
relation = condition.get("relation", None)
value = condition.get("value", None)
EmployeeCRUD.check_condition(column, operator, value, relation)
value = EmployeeCRUD.check_condition(column, operator, value, relation)
a, o = EmployeeCRUD.get_expr_by_condition(
column, operator, value, relation)
and_list += a
@ -435,7 +443,7 @@ class EmployeeCRUD(object):
employees = []
for r in pagination.items:
d = r.Employee.to_dict()
d['department_name'] = r.Department.department_name
d['department_name'] = r.Department.department_name if r.Department else ''
employees.append(d)
return {
@ -470,7 +478,7 @@ class EmployeeCRUD(object):
Employee.deleted == 0,
Employee.block == block,
]
if type(department_id) == list:
if isinstance(department_id, list):
if len(department_id) == 0:
return []
else:
@ -561,10 +569,153 @@ class EmployeeCRUD(object):
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):
"""
@ -605,6 +756,7 @@ class CreateEmployee(object):
try:
existed = self.check_acl_user(user_data)
if not existed:
user_data['add_from'] = 'common'
return self.acl.create_user(user_data)
return existed
except Exception as e:
@ -637,11 +789,14 @@ class CreateEmployee(object):
if existed:
return existed
return Employee.create(
res = Employee.create(
**kwargs
)
refresh_employee_acl_info.apply_async(args=(res.employee_id,), queue=ACL_QUEUE)
return res
def get_department_by_name(self, d_name):
@staticmethod
def get_department_by_name(d_name):
return Department.get_by(first=True, department_name=d_name)
def get_end_department_id(self, department_name_list, department_name_map):
@ -745,3 +900,75 @@ class EmployeeUpdateByUidForm(Form):
avatar = StringField(validators=[])
sex = 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

@ -2,7 +2,7 @@ import requests
from api.lib.common_setting.const import BotNameMap
from api.lib.common_setting.resp_format import ErrFormat
from api.models.common_setting import CompanyInfo, NoticeConfig
from api.models.common_setting import NoticeConfig
from wtforms import Form
from wtforms import StringField
from wtforms import validators
@ -162,4 +162,4 @@ class NoticeConfigUpdateForm(Form):
info = StringField(validators=[
validators.DataRequired(message="信息 不能为空"),
validators.Length(max=255),
])
])

View File

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

View File

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

View File

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

View File

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

View File

@ -58,7 +58,7 @@ def _request_messenger(subject, body, tos, sender, payload):
def notify_send(subject, body, methods, tos, payload=None):
payload = payload or {}
payload = {k: v or '' for k, v in payload.items()}
payload = {k: '' if v is None else v for k, v in payload.items()}
subject = Template(subject).render(payload)
body = Template(body).render(payload)

View File

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

View File

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

View File

@ -1,14 +1,19 @@
# -*- coding:utf-8 -*-
import datetime
import itertools
import json
from enum import Enum
from typing import List
from flask import has_request_context, request
from flask import has_request_context
from flask import request
from flask_login import current_user
from sqlalchemy import func
from api.extensions import db
from api.lib.perm.acl import AppCache
from api.models.acl import AuditLoginLog
from api.models.acl import AuditPermissionLog
from api.models.acl import AuditResourceLog
from api.models.acl import AuditRoleLog
@ -283,6 +288,27 @@ class AuditCRUD(object):
return data
@staticmethod
def search_login(_, q=None, page=1, page_size=10, start=None, end=None):
query = db.session.query(AuditLoginLog)
if start:
query = query.filter(AuditLoginLog.login_at >= start)
if end:
query = query.filter(AuditLoginLog.login_at <= end)
if q:
query = query.filter(AuditLoginLog.username == q)
records = query.order_by(
AuditLoginLog.id.desc()).offset((page - 1) * page_size).limit(page_size).all()
data = {
'data': [r.to_dict() for r in records],
}
return data
@classmethod
def add_role_log(cls, app_id, operate_type: AuditOperateType,
scope: AuditScope, link_id: int, origin: dict, current: dict, extra: dict,
@ -348,3 +374,32 @@ class AuditCRUD(object):
AuditTriggerLog.create(app_id=app_id, trigger_id=trigger_id, operate_uid=user_id,
operate_type=operate_type.value,
origin=origin, current=current, extra=extra, source=source.value)
@classmethod
def add_login_log(cls, username, is_ok, description, _id=None, logout_at=None, 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,10 +2,12 @@
import msgpack
import redis_lock
from api.extensions import cache
from api.extensions import db
from api.extensions import rd
from api.lib.decorator import flush_db
from api.lib.utils import Lock
from api.models.acl import App
from api.models.acl import Permission
from api.models.acl import Resource
@ -136,14 +138,14 @@ class HasResourceRoleCache(object):
@classmethod
def add(cls, rid, app_id):
with Lock('HasResourceRoleCache'):
with redis_lock.Lock(rd.r, 'HasResourceRoleCache', expire=10):
c = cls.get(app_id)
c[rid] = 1
cache.set(cls.PREFIX_KEY.format(app_id), c, timeout=0)
@classmethod
def remove(cls, rid, app_id):
with Lock('HasResourceRoleCache'):
with redis_lock.Lock(rd.r, 'HasResourceRoleCache', expire=10):
c = cls.get(app_id)
c.pop(rid, None)
cache.set(cls.PREFIX_KEY.format(app_id), c, timeout=0)
@ -156,9 +158,10 @@ class RoleRelationCache(object):
PREFIX_RESOURCES2 = "RoleRelationResources2::id::{0}::AppId::{1}"
@classmethod
def get_parent_ids(cls, rid, app_id):
def get_parent_ids(cls, rid, app_id, force=False):
parent_ids = cache.get(cls.PREFIX_PARENT.format(rid, app_id))
if not parent_ids:
if not parent_ids or force:
db.session.commit()
from api.lib.perm.acl.role import RoleRelationCRUD
parent_ids = RoleRelationCRUD.get_parent_ids(rid, app_id)
cache.set(cls.PREFIX_PARENT.format(rid, app_id), parent_ids, timeout=0)
@ -166,9 +169,10 @@ class RoleRelationCache(object):
return parent_ids
@classmethod
def get_child_ids(cls, rid, app_id):
def get_child_ids(cls, rid, app_id, force=False):
child_ids = cache.get(cls.PREFIX_CHILDREN.format(rid, app_id))
if not child_ids:
if not child_ids or force:
db.session.commit()
from api.lib.perm.acl.role import RoleRelationCRUD
child_ids = RoleRelationCRUD.get_child_ids(rid, app_id)
cache.set(cls.PREFIX_CHILDREN.format(rid, app_id), child_ids, timeout=0)
@ -176,14 +180,16 @@ class RoleRelationCache(object):
return child_ids
@classmethod
def get_resources(cls, rid, app_id):
def get_resources(cls, rid, app_id, force=False):
"""
:param rid:
:param app_id:
:param force:
:return: {id2perms: {resource_id: [perm,]}, group2perms: {group_id: [perm, ]}}
"""
resources = cache.get(cls.PREFIX_RESOURCES.format(rid, app_id))
if not resources:
if not resources or force:
db.session.commit()
from api.lib.perm.acl.role import RoleCRUD
resources = RoleCRUD.get_resources(rid, app_id)
if resources['id2perms'] or resources['group2perms']:
@ -192,9 +198,10 @@ class RoleRelationCache(object):
return resources or {}
@classmethod
def get_resources2(cls, rid, app_id):
def get_resources2(cls, rid, app_id, force=False):
r_g = cache.get(cls.PREFIX_RESOURCES2.format(rid, app_id))
if not r_g:
if not r_g or force:
db.session.commit()
res = cls.get_resources(rid, app_id)
id2perms = res['id2perms']
group2perms = res['group2perms']
@ -223,22 +230,28 @@ class RoleRelationCache(object):
@classmethod
@flush_db
def rebuild(cls, rid, app_id):
cls.clean(rid, app_id)
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)
if app_id is None:
app_ids = [None] + [i.id for i in App.get_by(to_dict=False)]
else:
HasResourceRoleCache.remove(rid, app_id)
cls.get_resources2(rid, app_id)
app_ids = [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
@flush_db
def rebuild2(cls, rid, app_id):
cache.delete(cls.PREFIX_RESOURCES2.format(rid, app_id))
cls.get_resources2(rid, app_id)
cls.get_resources2(rid, app_id, force=True)
@classmethod
def clean(cls, rid, app_id):

View File

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

View File

@ -2,7 +2,6 @@
from flask import abort
from flask import current_app
from api.extensions import db
from api.lib.perm.acl.audit import AuditCRUD
@ -127,11 +126,18 @@ class ResourceTypeCRUD(object):
existed_ids = [i.id for i in existed]
current_ids = []
rebuild_rids = set()
for i in existed:
if i.name not in perms:
i.soft_delete()
i.soft_delete(commit=False)
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:
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:
if i not in existed_names:
@ -276,7 +282,6 @@ class ResourceCRUD(object):
from api.tasks.acl import apply_trigger
triggers = TriggerCRUD.match_triggers(app_id, r.name, r.resource_type_id, uid)
current_app.logger.info(triggers)
for trigger in triggers:
# auto trigger should be no uid
apply_trigger.apply_async(args=(trigger.id,),
@ -310,9 +315,12 @@ class ResourceCRUD(object):
return resource
@staticmethod
def delete(_id):
def delete(_id, rebuild=True, app_id=None):
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()
resource.soft_delete()
@ -323,12 +331,15 @@ class ResourceCRUD(object):
i.soft_delete()
rebuilds.append((i.rid, i.app_id))
for rid, app_id in set(rebuilds):
role_rebuild.apply_async(args=(rid, app_id), queue=ACL_QUEUE)
if rebuild:
for rid, app_id in set(rebuilds):
role_rebuild.apply_async(args=(rid, app_id), queue=ACL_QUEUE)
AuditCRUD.add_resource_log(resource.app_id, AuditOperateType.delete,
AuditScope.resource, resource.id, origin, {}, {})
return rebuilds
@classmethod
def delete_by_name(cls, name, type_id, app_id):
resource = Resource.get_by(name=name, resource_type_id=type_id, app_id=app_id) or abort(

View File

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

View File

@ -1,14 +1,13 @@
# -*- coding:utf-8 -*-
import time
import redis_lock
import six
from flask import abort
from flask import current_app
from sqlalchemy import or_
from api.extensions import db
from api.extensions import rd
from api.lib.perm.acl.app import AppCRUD
from api.lib.perm.acl.audit import AuditCRUD, AuditOperateType, AuditScope
from api.lib.perm.acl.cache import AppCache
@ -62,7 +61,9 @@ class RoleRelationCRUD(object):
id2parents = {}
for i in res:
id2parents.setdefault(rid2uid.get(i.child_id, i.child_id), []).append(RoleCache.get(i.parent_id).to_dict())
parent = RoleCache.get(i.parent_id)
if parent:
id2parents.setdefault(rid2uid.get(i.child_id, i.child_id), []).append(parent.to_dict())
return id2parents
@ -141,24 +142,27 @@ class RoleRelationCRUD(object):
@classmethod
def add(cls, role, parent_id, child_ids, app_id):
result = []
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
with redis_lock.Lock(rd.r, "ROLE_RELATION_ADD", expire=10):
db.session.commit()
RoleRelationCache.clean(parent_id, app_id)
RoleRelationCache.clean(child_id, app_id)
result = []
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
if parent_id in cls.recursive_child_ids(child_id, app_id):
return abort(400, ErrFormat.inheritance_dead_loop)
if parent_id in cls.recursive_child_ids(child_id, app_id):
return abort(400, ErrFormat.inheritance_dead_loop)
if app_id is None:
for app in AppCRUD.get_all():
if app.name != "acl":
RoleRelationCache.clean(child_id, app.id)
result.append(RoleRelation.create(parent_id=parent_id, child_id=child_id, app_id=app_id).to_dict())
result.append(RoleRelation.create(parent_id=parent_id, child_id=child_id, app_id=app_id).to_dict())
RoleRelationCache.clean(parent_id, app_id)
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,
AuditScope.role_relation, role.id, {}, {},
@ -372,16 +376,16 @@ class RoleCRUD(object):
resource_type_id = resource_type and resource_type.id
result = dict(resources=dict(), groups=dict())
s = time.time()
# s = time.time()
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:
_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)
current_app.logger.info('middle2: {0}'.format(time.time() - s))
current_app.logger.info(len(_groups))
# current_app.logger.info('middle2: {0}'.format(time.time() - s))
# current_app.logger.info(len(_groups))
if not group_flat:
_merge(result['groups'], _groups)
else:
@ -392,7 +396,7 @@ class RoleCRUD(object):
item.setdefault('permissions', [])
item['permissions'] = list(set(item['permissions'] + _groups[rg_id]['permissions']))
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['groups'] = list(result['groups'].values())

View File

@ -41,6 +41,7 @@ class UserCRUD(object):
@classmethod
def add(cls, **kwargs):
add_from = kwargs.pop('add_from', None)
existed = User.get_by(username=kwargs['username'])
existed and abort(400, ErrFormat.user_exists.format(kwargs['username']))
@ -62,10 +63,12 @@ class UserCRUD(object):
AuditCRUD.add_role_log(None, AuditOperateType.create,
AuditScope.user, user.uid, {}, user.to_dict(), {}, {}
)
from api.lib.common_setting.employee import EmployeeCRUD
payload = {column: getattr(user, column) for column in ['uid', 'username', 'nickname', 'email', 'block']}
payload['rid'] = role.id
EmployeeCRUD.add_employee_from_acl_created(**payload)
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

View File

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

View File

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

View File

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

View File

@ -119,4 +119,4 @@ def create_cas_validate_url(cas_url, cas_route, service, ticket,
('service', service),
('ticket', ticket),
('renew', renew),
)
)

View File

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

View File

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

View File

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

View File

@ -0,0 +1,139 @@
# -*- coding:utf-8 -*-
import datetime
import secrets
import uuid
import requests
from flask import Blueprint
from flask import abort
from flask import current_app
from flask import redirect
from flask import request
from flask import session
from flask import url_for
from flask_login import login_user
from flask_login import logout_user
from six.moves.urllib.parse import urlencode
from six.moves.urllib.parse import urlparse
from api.lib.common_setting.common_data import AuthenticateDataCRUD
from api.lib.perm.acl.audit import AuditCRUD
from api.lib.perm.acl.cache import UserCache
from api.lib.perm.acl.resp_format import ErrFormat
blueprint = Blueprint('oauth2', __name__)
@blueprint.route('/api/<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,29 +1,34 @@
# -*- coding:utf-8 -*-
from flask_babel import lazy_gettext as _l
class CommonErrFormat(object):
unauthorized = "未认证"
unknown_error = "未知错误"
unauthorized = _l("unauthorized") # 未认证
unknown_error = _l("unknown error") # 未知错误
invalid_request = "不合法的请求"
invalid_operation = "无效的操作"
invalid_request = _l("Illegal request") # 不合法的请求
invalid_operation = _l("Invalid operation") # 无效的操作
not_found = "不存在"
not_found = _l("does not exist") # 不存在
circular_dependency_error = "存在循环依赖!"
circular_dependency_error = _l("There is a circular dependency!") # 存在循环依赖!
unknown_search_error = "未知搜索错误"
unknown_search_error = _l("Unknown search error") # 未知搜索错误
invalid_json = "json格式似乎不正确了, 请仔细确认一下!"
# json格式似乎不正确了, 请仔细确认一下!
invalid_json = _l("The json format seems to be incorrect, please confirm carefully!")
datetime_argument_invalid = "参数 {} 格式不正确, 格式必须是: yyyy-mm-dd HH:MM:SS"
# 参数 {} 格式不正确, 格式必须是: yyyy-mm-dd HH:MM:SS
datetime_argument_invalid = _l("The format of parameter {} is incorrect, the format must be: yyyy-mm-dd HH:MM:SS")
argument_value_required = "参数 {} 的值不能为空!"
argument_required = "请求缺少参数 {}"
argument_invalid = "参数 {} 的值无效"
argument_str_length_limit = "参数 {} 的长度必须 <= {}"
argument_value_required = _l("The value of parameter {} cannot be empty!") # 参数 {} 的值不能为空!
argument_required = _l("The request is missing parameters {}") # 请求缺少参数 {}
argument_invalid = _l("Invalid value for parameter {}") # 参数 {} 的值无效
argument_str_length_limit = _l("The length of parameter {} must be <= {}") # 参数 {} 的长度必须 <= {}
role_required = "角色 {} 才能操作!"
user_not_found = "用户 {} 不存在"
no_permission = "您没有资源: {}{}权限!"
no_permission2 = "您没有操作权限!"
no_permission_only_owner = "只有创建人或者管理员才有权限!"
role_required = _l("Role {} can only operate!") # 角色 {} 才能操作!
user_not_found = _l("User {} does not exist") # 用户 {} 不存在
no_permission = _l("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,19 +1,15 @@
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
from colorama import Fore
from colorama import Style
from colorama import init as colorama_init
from colorama import Back, Fore, Style, init as colorama_init
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives import padding
from cryptography.hazmat.primitives.ciphers import Cipher
from cryptography.hazmat.primitives.ciphers import algorithms
from cryptography.hazmat.primitives.ciphers import modes
from cryptography.hazmat.primitives 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
@ -27,24 +23,31 @@ 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)
@ -55,22 +58,33 @@ class Backend:
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] == "run"):
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
self.backend = backend
resp = self.auto_unseal()
self.print_response(resp)
@ -124,6 +138,8 @@ class KeyManage:
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
@ -135,35 +151,42 @@ class KeyManage:
else:
return "", True
def auth_root_secret(self, root_key):
msg, ok = self.is_valid_root_key(root_key)
if not ok:
return {
"message": msg,
"status": "failed"
}
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"
}
encrypt_key_aes = self.backend.get(backend_encrypt_key_name)
if not encrypt_key_aes:
return {
"message": "encrypt key is empty",
"status": "failed"
}
secrets_encrypt_key, ok = InnerCrypt.aes_decrypt(string_to_bytes(root_key), encrypt_key_aes)
if ok:
msg, ok = self.backend.update(backend_seal_key, "open")
secret_encrypt_key, ok = InnerCrypt.aes_decrypt(string_to_bytes(root_key), encrypt_key_aes)
if ok:
current_app.config["secrets_encrypt_key"] = secrets_encrypt_key
current_app.config["secrets_root_key"] = root_key
current_app.config["secrets_shares"] = []
return {"message": success, "status": success}
return {"message": msg, "status": "failed"}
else:
return {
"message": secrets_encrypt_key,
"status": "failed"
}
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():
@ -175,14 +198,12 @@ class KeyManage:
try:
t = [i for i in b64decode(key)]
v = (int("".join([chr(i) for i in t[-2:]])), bytes(t[:-2]))
shares = current_app.config.get("secrets_shares", [])
shares = self.backend.get_shares(self.share_key)
if v not in shares:
shares.append(v)
current_app.config["secrets_shares"] = shares
self.set_shares(shares)
if len(shares) >= global_key_threshold:
recovered_secret = Shamir.combine(shares[:global_key_threshold], False)
return self.auth_root_secret(b64encode(recovered_secret))
return self.parse_shares(shares, current_app)
else:
return {
"message": "waiting for inputting other unseal key {0}/{1}".format(len(shares),
@ -242,8 +263,11 @@ class KeyManage:
msg, ok = self.backend.add(backend_seal_key, "open")
if not ok:
return {"message": msg, "status": "failed"}, False
current_app.config["secrets_root_key"] = root_key
current_app.config["secrets_encrypt_key"] = encrypt_key
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",
@ -266,7 +290,7 @@ class KeyManage:
}
# TODO
elif len(self.trigger.strip()) == 24:
res = self.auth_root_secret(self.trigger.encode())
res = self.auth_root_secret(self.trigger.encode(), current_app)
if res.get("status") == success:
return {
"message": success,
@ -298,22 +322,31 @@ class KeyManage:
"message": msg,
"status": "failed",
}
current_app.config["secrets_root_key"] = ''
current_app.config["secrets_encrypt_key"] = ''
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.
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")
# 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 {"message": msg, "status": "failed"}
return True
status = self.backend.get(backend_seal_key)
return status == "block"
@ -349,22 +382,53 @@ class KeyManage:
}
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):
secrets_encrypt_key = current_app.config.get("secrets_encrypt_key", "")
self.encrypt_key = b64decode(secrets_encrypt_key.encode("utf-8"))
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
@ -381,6 +445,7 @@ class InnerCrypt:
return b64encode(iv + ciphertext).decode("utf-8"), True
except Exception as e:
return str(e), False
@classmethod

View File

@ -1,8 +1,13 @@
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
@ -33,3 +38,26 @@ class InnerKVManger(object):
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,9 +1,6 @@
# -*- coding:utf-8 -*-
import base64
import sys
import time
from typing import Set
import elasticsearch
import redis
@ -12,6 +9,9 @@ from Crypto.Cipher import AES
from elasticsearch import Elasticsearch
from flask import current_app
from api.lib.secrets.inner import InnerCrypt
from api.lib.secrets.inner import KeyManage
class BaseEnum(object):
_ALL_ = set() # type: Set[str]
@ -116,6 +116,23 @@ class RedisHandler(object):
except Exception as 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):
def __init__(self, flask_app=None):
@ -210,52 +227,6 @@ class ESHandler(object):
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):
BLOCK_SIZE = 16 # Bytes
pad = lambda s: s + ((AESCrypto.BLOCK_SIZE - len(s) % AESCrypto.BLOCK_SIZE) *
@ -286,3 +257,33 @@ class AESCrypto(object):
text_decrypted = cipher.decrypt(encode_bytes)
return cls.unpad(text_decrypted).decode('utf8')
class Crypto(AESCrypto):
@classmethod
def encrypt(cls, data):
from api.lib.secrets.secrets import InnerKVManger
if not KeyManage(backend=InnerKVManger()).is_seal():
res, status = InnerCrypt().encrypt(data)
if status:
return res
return AESCrypto().encrypt(data)
@classmethod
def decrypt(cls, data):
from api.lib.secrets.secrets import InnerKVManger
if not KeyManage(backend=InnerKVManger()).is_seal():
try:
res, status = InnerCrypt().decrypt(data)
if status:
return res
except:
pass
try:
return AESCrypto().decrypt(data)
except:
return data

View File

@ -88,11 +88,11 @@ def webhook_request(webhook, payload):
params = webhook.get('parameters') or None
if isinstance(params, dict):
params = json.loads(Template(json.dumps(params)).render(payload))
params = json.loads(Template(json.dumps(params)).render(payload).encode('utf-8'))
headers = json.loads(Template(json.dumps(webhook.get('headers') or {})).render(payload))
data = Template(json.dumps(webhook.get('body', ''))).render(payload)
data = Template(json.dumps(webhook.get('body', ''))).render(payload).encode('utf-8')
auth = _wrap_auth(**webhook.get('authorization', {}))
if (webhook.get('authorization', {}).get("type") or '').lower() == 'oauth2.0':

View File

@ -5,17 +5,18 @@ import copy
import hashlib
from datetime import datetime
from ldap3 import Server, Connection, ALL
from ldap3.core.exceptions import LDAPBindError, LDAPCertificateError
from flask import current_app
from flask import session
from flask_sqlalchemy import BaseQuery
from api.extensions import db
from api.lib.database import CRUDModel
from api.lib.database import Model
from api.lib.database import Model2
from api.lib.database import SoftDeleteMixin
from api.lib.perm.acl.const import ACL_QUEUE
from api.lib.perm.acl.const import OperateType
from api.lib.perm.acl.resp_format import ErrFormat
class App(Model):
@ -28,21 +29,26 @@ class App(Model):
class UserQuery(BaseQuery):
def _join(self, *args, **kwargs):
super(UserQuery, self)._join(*args, **kwargs)
def authenticate(self, login, password):
from api.lib.perm.acl.audit import AuditCRUD
user = self.filter(db.or_(User.username == login,
User.email == login)).filter(User.deleted.is_(False)).filter(User.block == 0).first()
if user:
current_app.logger.info(user)
authenticated = user.check_password(password)
if authenticated:
from api.tasks.acl import op_record
op_record.apply_async(args=(None, login, OperateType.LOGIN, ["ACL"]), queue=ACL_QUEUE)
_id = AuditCRUD.add_login_log(login, True, ErrFormat.login_succeed)
session['LOGIN_ID'] = _id
else:
AuditCRUD.add_login_log(login, False, ErrFormat.invalid_password)
else:
authenticated = False
AuditCRUD.add_login_log(login, False, ErrFormat.user_not_found.format(login))
current_app.logger.info(("login", login, user, authenticated))
return user, authenticated
def authenticate_with_key(self, key, secret, args, path):
@ -57,38 +63,6 @@ class UserQuery(BaseQuery):
return user, authenticated
def authenticate_with_ldap(self, username, password):
server = Server(current_app.config.get('LDAP_SERVER'), get_info=ALL)
if '@' in username:
email = username
who = current_app.config.get('LDAP_USER_DN').format(username.split('@')[0])
else:
who = current_app.config.get('LDAP_USER_DN').format(username)
email = "{}@{}".format(who, current_app.config.get('LDAP_DOMAIN'))
username = username.split('@')[0]
user = self.get_by_username(username)
try:
if not password:
raise LDAPCertificateError
conn = Connection(server, user=who, password=password)
conn.bind()
if conn.result['result'] != 0:
raise LDAPBindError
conn.unbind()
if not user:
from api.lib.perm.acl.user import UserCRUD
user = UserCRUD.add(username=username, email=email)
from api.tasks.acl import op_record
op_record.apply_async(args=(None, username, OperateType.LOGIN, ["ACL"]), queue=ACL_QUEUE)
return user, True
except LDAPBindError:
return user, False
def search(self, key):
query = self.filter(db.or_(User.email == key,
User.nickname.ilike('%' + key + '%'),
@ -131,13 +105,14 @@ class User(CRUDModel, SoftDeleteMixin):
_password = db.Column("password", db.String(80))
key = db.Column(db.String(32), nullable=False)
secret = db.Column(db.String(32), nullable=False)
date_joined = db.Column(db.DateTime, default=datetime.utcnow)
last_login = db.Column(db.DateTime, default=datetime.utcnow)
date_joined = db.Column(db.DateTime, default=datetime.now)
last_login = db.Column(db.DateTime, default=datetime.now)
block = db.Column(db.Boolean, default=False)
has_logined = db.Column(db.Boolean, default=False)
wx_id = db.Column(db.String(32))
employee_id = db.Column(db.String(16), index=True)
avatar = db.Column(db.String(128))
# apps = db.Column(db.JSON)
def __str__(self):
@ -168,11 +143,9 @@ class User(CRUDModel, SoftDeleteMixin):
class RoleQuery(BaseQuery):
def _join(self, *args, **kwargs):
super(RoleQuery, self)._join(*args, **kwargs)
def authenticate(self, login, password):
role = self.filter(Role.name == login).first()
role = self.filter(Role.name == login).filter(Role.deleted.is_(False)).first()
if role:
authenticated = role.check_password(password)
@ -377,3 +350,16 @@ class AuditTriggerLog(Model):
current = db.Column(db.JSON, default=dict(), comment='当前数据')
extra = db.Column(db.JSON, default=dict(), comment='权限名')
source = db.Column(db.String(16), default='', comment='来源')
class AuditLoginLog(Model2):
__tablename__ = "acl_audit_login_logs"
username = db.Column(db.String(64), index=True)
channel = db.Column(db.Enum('web', 'api', 'ssh'), default="web")
ip = db.Column(db.String(15))
browser = db.Column(db.String(256))
description = db.Column(db.String(128))
is_ok = db.Column(db.Boolean)
login_at = db.Column(db.DateTime)
logout_at = db.Column(db.DateTime)

View File

@ -2,7 +2,6 @@
import datetime
from sqlalchemy.dialects.mysql import DOUBLE
from api.extensions import db
@ -11,8 +10,11 @@ from api.lib.cmdb.const import CIStatusEnum
from api.lib.cmdb.const import CITypeOperateType
from api.lib.cmdb.const import ConstraintEnum
from api.lib.cmdb.const import OperateType
from api.lib.cmdb.const import RelationSourceEnum
from api.lib.cmdb.const import ValueTypeEnum
from api.lib.database import Model, Model2
from api.lib.database import Model
from api.lib.database import Model2
from api.lib.utils import Crypto
# template
@ -44,17 +46,31 @@ class CIType(Model):
name = db.Column(db.String(32), nullable=False)
alias = db.Column(db.String(32), nullable=False)
unique_id = db.Column(db.Integer, db.ForeignKey("c_attributes.id"), nullable=False)
show_id = db.Column(db.Integer, db.ForeignKey("c_attributes.id"))
enabled = db.Column(db.Boolean, default=True, nullable=False)
is_attached = db.Column(db.Boolean, default=False, nullable=False)
icon = db.Column(db.Text)
order = db.Column(db.SmallInteger, default=0, nullable=False)
default_order_attr = db.Column(db.String(33))
unique_key = db.relationship("Attribute", backref="c_ci_types.unique_id")
unique_key = db.relationship("Attribute", backref="c_ci_types.unique_id",
primaryjoin="Attribute.id==CIType.unique_id", foreign_keys=[unique_id])
show_key = db.relationship("Attribute", backref="c_ci_types.show_id",
primaryjoin="Attribute.id==CIType.show_id", foreign_keys=[show_id])
uid = db.Column(db.Integer, index=True)
class CITypeInheritance(Model):
__tablename__ = "c_ci_type_inheritance"
parent_id = db.Column(db.Integer, db.ForeignKey("c_ci_types.id"), nullable=False)
child_id = db.Column(db.Integer, db.ForeignKey("c_ci_types.id"), nullable=False)
parent = db.relationship("CIType", primaryjoin="CIType.id==CITypeInheritance.parent_id")
child = db.relationship("CIType", primaryjoin="CIType.id==CITypeInheritance.child_id")
class CITypeRelation(Model):
__tablename__ = "c_ci_type_relations"
@ -63,6 +79,12 @@ class CITypeRelation(Model):
relation_type_id = db.Column(db.Integer, db.ForeignKey("c_relation_types.id"), nullable=False)
constraint = db.Column(db.Enum(*ConstraintEnum.all()), default=ConstraintEnum.One2Many)
parent_attr_id = db.Column(db.Integer, db.ForeignKey("c_attributes.id")) # CMDB > 2.4.5: deprecated
child_attr_id = db.Column(db.Integer, db.ForeignKey("c_attributes.id")) # CMDB > 2.4.5: deprecated
parent_attr_ids = db.Column(db.JSON) # [parent_attr_id, ]
child_attr_ids = db.Column(db.JSON) # [child_attr_id, ]
parent = db.relationship("CIType", primaryjoin="CIType.id==CITypeRelation.parent_id")
child = db.relationship("CIType", primaryjoin="CIType.id==CITypeRelation.child_id")
relation_type = db.relationship("RelationType", backref="c_ci_type_relations.relation_type_id")
@ -82,6 +104,11 @@ class Attribute(Model):
is_link = db.Column(db.Boolean, default=False)
is_password = db.Column(db.Boolean, default=False)
is_sortable = db.Column(db.Boolean, default=False)
is_dynamic = db.Column(db.Boolean, default=False)
is_bool = db.Column(db.Boolean, default=False)
is_reference = db.Column(db.Boolean, default=False)
reference_type_id = db.Column(db.Integer, db.ForeignKey('c_ci_types.id'))
default = db.Column(db.JSON) # {"default": None}
@ -89,13 +116,39 @@ class Attribute(Model):
compute_expr = db.Column(db.Text)
compute_script = db.Column(db.Text)
choice_web_hook = db.Column(db.JSON)
_choice_web_hook = db.Column('choice_web_hook', db.JSON)
choice_other = db.Column(db.JSON)
re_check = db.Column(db.Text)
uid = db.Column(db.Integer, index=True)
option = db.Column(db.JSON)
def _get_webhook(self):
if self._choice_web_hook:
if self._choice_web_hook.get('headers') and "Cookie" in self._choice_web_hook['headers']:
self._choice_web_hook['headers']['Cookie'] = Crypto.decrypt(self._choice_web_hook['headers']['Cookie'])
if self._choice_web_hook.get('authorization'):
for k, v in self._choice_web_hook['authorization'].items():
self._choice_web_hook['authorization'][k] = Crypto.decrypt(v)
return self._choice_web_hook
def _set_webhook(self, data):
if data:
if data.get('headers') and "Cookie" in data['headers']:
data['headers']['Cookie'] = Crypto.encrypt(data['headers']['Cookie'])
if data.get('authorization'):
for k, v in data['authorization'].items():
data['authorization'][k] = Crypto.encrypt(v)
self._choice_web_hook = data
choice_web_hook = db.synonym("_choice_web_hook", descriptor=property(_get_webhook, _set_webhook))
class CITypeAttribute(Model):
__tablename__ = "c_ci_type_attributes"
@ -130,7 +183,25 @@ class CITypeTrigger(Model):
type_id = db.Column(db.Integer, db.ForeignKey('c_ci_types.id'), nullable=False)
attr_id = db.Column(db.Integer, db.ForeignKey("c_attributes.id"))
option = db.Column('notify', db.JSON)
_option = db.Column('notify', db.JSON)
def _get_option(self):
if self._option and self._option.get('webhooks'):
if self._option['webhooks'].get('authorization'):
for k, v in self._option['webhooks']['authorization'].items():
self._option['webhooks']['authorization'][k] = Crypto.decrypt(v)
return self._option
def _set_option(self, data):
if data and data.get('webhooks'):
if data['webhooks'].get('authorization'):
for k, v in data['webhooks']['authorization'].items():
data['webhooks']['authorization'][k] = Crypto.encrypt(v)
self._option = data
option = db.synonym("_option", descriptor=property(_get_option, _set_option))
class CITriggerHistory(Model):
@ -146,6 +217,26 @@ class CITriggerHistory(Model):
webhook = db.Column(db.Text)
class TopologyViewGroup(Model):
__tablename__ = 'c_topology_view_groups'
name = db.Column(db.String(64), index=True)
order = db.Column(db.Integer, default=0)
class TopologyView(Model):
__tablename__ = 'c_topology_views'
name = db.Column(db.String(64), index=True)
group_id = db.Column(db.Integer, db.ForeignKey('c_topology_view_groups.id'))
category = db.Column(db.String(32))
central_node_type = db.Column(db.Integer)
central_node_instances = db.Column(db.Text)
path = db.Column(db.JSON)
order = db.Column(db.Integer, default=0)
option = db.Column(db.JSON)
class CITypeUniqueConstraint(Model):
__tablename__ = "c_c_t_u_c"
@ -162,6 +253,7 @@ class CI(Model):
status = db.Column(db.Enum(*CIStatusEnum.all(), name="status"))
heartbeat = db.Column(db.DateTime, default=lambda: datetime.datetime.now())
is_auto_discovery = db.Column('a', db.Boolean, default=False)
updated_by = db.Column(db.String(64))
ci_type = db.relationship("CIType", backref="c_cis.type_id")
@ -173,6 +265,9 @@ class CIRelation(Model):
second_ci_id = db.Column(db.Integer, db.ForeignKey("c_cis.id"), nullable=False)
relation_type_id = db.Column(db.Integer, db.ForeignKey("c_relation_types.id"), nullable=False)
more = db.Column(db.Integer, db.ForeignKey("c_cis.id"))
source = db.Column(db.Enum(*RelationSourceEnum.all()), name="source")
ancestor_ids = db.Column(db.String(128), index=True)
first_ci = db.relationship("CI", primaryjoin="CI.id==CIRelation.first_ci_id")
second_ci = db.relationship("CI", primaryjoin="CI.id==CIRelation.second_ci_id")
@ -367,6 +462,7 @@ class CITypeHistory(Model):
attr_id = db.Column(db.Integer)
trigger_id = db.Column(db.Integer)
rc_id = db.Column(db.Integer)
unique_constraint_id = db.Column(db.Integer)
uid = db.Column(db.Integer, index=True)
@ -380,6 +476,7 @@ class PreferenceShowAttributes(Model):
uid = db.Column(db.Integer, index=True, nullable=False)
type_id = db.Column(db.Integer, db.ForeignKey("c_ci_types.id"), nullable=False)
attr_id = db.Column(db.Integer, db.ForeignKey("c_attributes.id"))
builtin_attr = db.Column(db.String(256), nullable=True)
order = db.Column(db.SmallInteger, default=0)
is_fixed = db.Column(db.Boolean, default=False)
@ -402,6 +499,7 @@ class PreferenceRelationView(Model):
name = db.Column(db.String(64), index=True, nullable=False)
cr_ids = db.Column(db.JSON) # [{parent_id: x, child_id: y}]
is_public = db.Column(db.Boolean, default=False)
option = db.Column(db.JSON)
class PreferenceSearchOption(Model):
@ -418,6 +516,15 @@ class PreferenceSearchOption(Model):
option = db.Column(db.JSON)
class PreferenceCITypeOrder(Model):
__tablename__ = "c_pcto"
uid = db.Column(db.Integer, index=True, nullable=False)
type_id = db.Column(db.Integer, db.ForeignKey('c_ci_types.id'))
order = db.Column(db.SmallInteger, default=0)
is_tree = db.Column(db.Boolean, default=False) # True is tree view, False is resource view
# custom
class CustomDashboard(Model):
__tablename__ = "c_c_d"
@ -429,6 +536,7 @@ class CustomDashboard(Model):
type_id = db.Column(db.Integer, db.ForeignKey('c_ci_types.id'))
attr_id = db.Column(db.Integer, db.ForeignKey('c_attributes.id'))
builtin_attr = db.Column(db.String(256), nullable=True)
level = db.Column(db.Integer)
options = db.Column(db.JSON)
@ -466,18 +574,28 @@ class AutoDiscoveryCIType(Model):
attributes = db.Column(db.JSON) # {ad_key: cmdb_key}
relation = db.Column(db.JSON) # [{ad_key: {type_id: x, attr_id: x}}]
relation = db.Column(db.JSON) # [{ad_key: {type_id: x, attr_id: x}}], CMDB > 2.4.5: deprecated
auto_accept = db.Column(db.Boolean, default=False)
agent_id = db.Column(db.String(8), index=True)
query_expr = db.Column(db.Text)
interval = db.Column(db.Integer) # seconds
interval = db.Column(db.Integer) # seconds, > 2.4.5: deprecated
cron = db.Column(db.String(128))
extra_option = db.Column(db.JSON)
uid = db.Column(db.Integer, index=True)
enabled = db.Column(db.Boolean, default=True)
class AutoDiscoveryCITypeRelation(Model):
__tablename__ = "c_ad_ci_type_relations"
ad_type_id = db.Column(db.Integer, db.ForeignKey('c_ci_types.id'), nullable=False)
ad_key = db.Column(db.String(128))
peer_type_id = db.Column(db.Integer, db.ForeignKey('c_ci_types.id'), nullable=False)
peer_attr_id = db.Column(db.Integer, db.ForeignKey('c_attributes.id'), nullable=False)
class AutoDiscoveryCI(Model):
@ -495,6 +613,45 @@ class AutoDiscoveryCI(Model):
accept_time = db.Column(db.DateTime)
class AutoDiscoveryRuleSyncHistory(Model2):
__tablename__ = "c_ad_rule_sync_histories"
adt_id = db.Column(db.Integer, db.ForeignKey('c_ad_ci_types.id'))
oneagent_id = db.Column(db.String(8))
oneagent_name = db.Column(db.String(64))
sync_at = db.Column(db.DateTime, default=datetime.datetime.now())
class AutoDiscoveryExecHistory(Model2):
__tablename__ = "c_ad_exec_histories"
type_id = db.Column(db.Integer, index=True)
stdout = db.Column(db.Text)
class AutoDiscoveryCounter(Model2):
__tablename__ = "c_ad_counter"
type_id = db.Column(db.Integer, index=True)
rule_count = db.Column(db.Integer, default=0)
exec_target_count = db.Column(db.Integer, default=0)
instance_count = db.Column(db.Integer, default=0)
accept_count = db.Column(db.Integer, default=0)
this_month_count = db.Column(db.Integer, default=0)
this_week_count = db.Column(db.Integer, default=0)
last_month_count = db.Column(db.Integer, default=0)
last_week_count = db.Column(db.Integer, default=0)
class AutoDiscoveryAccount(Model):
__tablename__ = "c_ad_accounts"
uid = db.Column(db.Integer, index=True)
name = db.Column(db.String(64))
adr_id = db.Column(db.Integer, db.ForeignKey('c_ad_rules.id'))
config = db.Column(db.JSON)
class CIFilterPerms(Model):
__tablename__ = "c_ci_filter_perms"
@ -502,6 +659,7 @@ class CIFilterPerms(Model):
type_id = db.Column(db.Integer, db.ForeignKey('c_ci_types.id'))
ci_filter = db.Column(db.Text)
attr_filter = db.Column(db.Text)
id_filter = db.Column(db.JSON) # {node_path: unique_value}
rid = db.Column(db.Integer, index=True)
@ -511,3 +669,52 @@ class InnerKV(Model):
key = db.Column(db.String(128), index=True)
value = db.Column(db.Text)
class IPAMSubnetScan(Model):
__tablename__ = "c_ipam_subnet_scans"
ci_id = db.Column(db.Integer, index=True, nullable=False)
scan_enabled = db.Column(db.Boolean, default=True)
rule_updated_at = db.Column(db.DateTime)
last_scan_time = db.Column(db.DateTime)
# scan rules
agent_id = db.Column(db.String(8), index=True)
cron = db.Column(db.String(128))
class IPAMSubnetScanHistory(Model2):
__tablename__ = "c_ipam_subnet_scan_histories"
subnet_scan_id = db.Column(db.Integer, index=True)
exec_id = db.Column(db.String(64), index=True)
cidr = db.Column(db.String(18), index=True)
start_at = db.Column(db.DateTime)
end_at = db.Column(db.DateTime)
status = db.Column(db.Integer, default=0) # 0 is ok
stdout = db.Column(db.Text)
ip_num = db.Column(db.Integer)
ips = db.Column(db.JSON) # keep only the last 10 records
class IPAMOperationHistory(Model2):
__tablename__ = "c_ipam_operation_histories"
from api.lib.cmdb.ipam.const import OperateTypeEnum
uid = db.Column(db.Integer, index=True)
cidr = db.Column(db.String(18), index=True)
operate_type = db.Column(db.Enum(*OperateTypeEnum.all()))
description = db.Column(db.Text)
class DCIMOperationHistory(Model2):
__tablename__ = "c_dcim_operation_histories"
from api.lib.cmdb.dcim.const import OperateTypeEnum
uid = db.Column(db.Integer, index=True)
rack_id = db.Column(db.Integer, index=True)
ci_id = db.Column(db.Integer, index=True)
operate_type = db.Column(db.Enum(*OperateTypeEnum.all()))

View File

@ -96,3 +96,11 @@ class NoticeConfig(Model):
platform = db.Column(db.VARCHAR(255), nullable=False)
info = db.Column(db.JSON)
class CommonFile(Model):
__tablename__ = 'common_file'
file_name = db.Column(db.VARCHAR(512), nullable=False, index=True)
origin_name = db.Column(db.VARCHAR(512), nullable=False)
binary = db.Column(db.LargeBinary(16777216), nullable=False)

View File

@ -3,12 +3,13 @@
import json
import re
from celery_once import QueueOnce
import redis_lock
from flask import current_app
from werkzeug.exceptions import BadRequest
from werkzeug.exceptions import NotFound
from api.extensions import celery
from api.extensions import rd
from api.lib.decorator import flush_db
from api.lib.decorator import reconnect_db
from api.lib.perm.acl.audit import AuditCRUD
@ -25,15 +26,14 @@ from api.models.acl import Role
from api.models.acl import Trigger
@celery.task(base=QueueOnce,
name="acl.role_rebuild",
queue=ACL_QUEUE,
once={"graceful": True, "unlock_before_run": True})
@celery.task(name="acl.role_rebuild", queue=ACL_QUEUE, )
@flush_db
@reconnect_db
def role_rebuild(rids, app_id):
rids = rids if isinstance(rids, list) else [rids]
for rid in rids:
RoleRelationCache.rebuild(rid, app_id)
with redis_lock.Lock(rd.r, "ROLE_REBUILD_{}_{}".format(rid, app_id), expire=10):
RoleRelationCache.rebuild(rid, app_id)
current_app.logger.info("Role {0} App {1} rebuild..........".format(rids, app_id))
@ -190,18 +190,18 @@ def cancel_trigger(_id, resource_id=None, operator_uid=None):
@celery.task(name="acl.op_record", queue=ACL_QUEUE)
@reconnect_db
def op_record(app, rolename, operate_type, obj):
def op_record(app, role_name, operate_type, obj):
if isinstance(app, int):
app = AppCache.get(app)
app = app and app.name
if isinstance(rolename, int):
u = UserCache.get(rolename)
if isinstance(role_name, int):
u = UserCache.get(role_name)
if u:
rolename = u.username
role_name = u.username
if not u:
r = RoleCache.get(rolename)
r = RoleCache.get(role_name)
if r:
rolename = r.name
role_name = r.name
OperateRecordCRUD.add(app, rolename, operate_type, obj)
OperateRecordCRUD.add(app, role_name, operate_type, obj)

View File

@ -1,10 +1,11 @@
# -*- coding:utf-8 -*-
# -*- coding:utf-8 -*-
import datetime
import json
import time
import redis_lock
from flask import current_app
from flask import has_request_context
from flask_login import login_user
import api.lib.cmdb.ci
@ -12,15 +13,23 @@ from api.extensions import celery
from api.extensions import db
from api.extensions import es
from api.extensions import rd
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import CITypeAttributesCache
from api.lib.cmdb.const import CMDB_QUEUE
from api.lib.cmdb.const import REDIS_PREFIX_CI
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION2
from api.lib.cmdb.const import RelationSourceEnum
from api.lib.cmdb.perms import CIFilterPermsCRUD
from api.lib.cmdb.utils import TableMap
from api.lib.decorator import flush_db
from api.lib.decorator import reconnect_db
from api.lib.perm.acl.cache import UserCache
from api.lib.utils import Lock
from api.lib.utils import handle_arg_list
from api.models.cmdb import Attribute
from api.models.cmdb import AutoDiscoveryCI
from api.models.cmdb import AutoDiscoveryCIType
from api.models.cmdb import AutoDiscoveryCITypeRelation
from api.models.cmdb import CI
from api.models.cmdb import CIRelation
from api.models.cmdb import CITypeAttribute
@ -31,8 +40,8 @@ from api.models.cmdb import CITypeAttribute
@reconnect_db
def ci_cache(ci_id, operate_type, record_id):
from api.lib.cmdb.ci import CITriggerManager
time.sleep(0.01)
from api.lib.cmdb.ci import CIRelationManager
from api.lib.cmdb.ci_type import CITypeAttributeManager
m = api.lib.cmdb.ci.CIManager()
ci_dict = m.get_ci_by_id_from_db(ci_id, need_children=False, use_master=False)
@ -45,18 +54,37 @@ def ci_cache(ci_id, operate_type, record_id):
current_app.logger.info("{0} flush..........".format(ci_id))
if operate_type:
current_app.test_request_context().push()
login_user(UserCache.get('worker'))
if not has_request_context():
current_app.test_request_context().push()
login_user(UserCache.get('worker'))
CITriggerManager.fire(operate_type, ci_dict, record_id)
_, enum_map = CITypeAttributeManager.get_attr_names_label_enum(ci_dict.get('_type'))
payload = dict()
for k, v in ci_dict.items():
if k in enum_map:
if isinstance(v, list):
payload[k] = [enum_map[k].get(i, i) for i in v]
else:
payload[k] = enum_map[k].get(v, v)
else:
payload[k] = v
CITriggerManager.fire(operate_type, payload, record_id)
ci_dict and CIRelationManager.build_by_attribute(ci_dict)
@celery.task(name="cmdb.rebuild_relation_for_attribute_changed", queue=CMDB_QUEUE)
@reconnect_db
def rebuild_relation_for_attribute_changed(ci_type_relation, uid):
from api.lib.cmdb.ci import CIRelationManager
CIRelationManager.rebuild_all_by_attribute(ci_type_relation, uid)
@celery.task(name="cmdb.batch_ci_cache", queue=CMDB_QUEUE)
@flush_db
@reconnect_db
def batch_ci_cache(ci_ids, ): # only for attribute change index
time.sleep(1)
for ci_id in ci_ids:
m = api.lib.cmdb.ci.CIManager()
ci_dict = m.get_ci_by_id_from_db(ci_id, need_children=False, use_master=False)
@ -71,7 +99,7 @@ def batch_ci_cache(ci_ids, ): # only for attribute change index
@celery.task(name="cmdb.ci_delete", queue=CMDB_QUEUE)
@reconnect_db
def ci_delete(ci_id):
def ci_delete(ci_id, type_id):
current_app.logger.info(ci_id)
if current_app.config.get("USE_ES"):
@ -79,9 +107,28 @@ def ci_delete(ci_id):
else:
rd.delete(ci_id, REDIS_PREFIX_CI)
instance = AutoDiscoveryCI.get_by(ci_id=ci_id, to_dict=False, first=True)
if instance is not None:
adt = AutoDiscoveryCIType.get_by_id(instance.adt_id)
if adt:
adt.update(updated_at=datetime.datetime.now())
instance.delete()
for attr in Attribute.get_by(reference_type_id=type_id, to_dict=False):
table = TableMap(attr=attr).table
for i in getattr(table, 'get_by')(attr_id=attr.id, value=ci_id, to_dict=False):
i.delete()
ci_cache(i.ci_id, None, None)
current_app.logger.info("{0} delete..........".format(ci_id))
@celery.task(name="cmdb.delete_id_filter", queue=CMDB_QUEUE)
@reconnect_db
def delete_id_filter(ci_id):
CIFilterPermsCRUD().delete_id_filter_by_ci_id(ci_id)
@celery.task(name="cmdb.ci_delete_trigger", queue=CMDB_QUEUE)
@reconnect_db
def ci_delete_trigger(trigger, operate_type, ci_dict):
@ -97,17 +144,30 @@ def ci_delete_trigger(trigger, operate_type, ci_dict):
@celery.task(name="cmdb.ci_relation_cache", queue=CMDB_QUEUE)
@flush_db
@reconnect_db
def ci_relation_cache(parent_id, child_id):
with Lock("CIRelation_{}".format(parent_id)):
def ci_relation_cache(parent_id, child_id, ancestor_ids):
with redis_lock.Lock(rd.r, "CIRelation_{}".format(parent_id), expire=10):
children = rd.get([parent_id], REDIS_PREFIX_CI_RELATION)[0]
children = json.loads(children) if children is not None else {}
cr = CIRelation.get_by(first_ci_id=parent_id, second_ci_id=child_id, first=True, to_dict=False)
cr = CIRelation.get_by(first_ci_id=parent_id, second_ci_id=child_id, ancestor_ids=ancestor_ids,
first=True, to_dict=False)
if str(child_id) not in children:
children[str(child_id)] = cr.second_ci.type_id
rd.create_or_update({parent_id: json.dumps(children)}, REDIS_PREFIX_CI_RELATION)
if ancestor_ids is not None:
key = "{},{}".format(ancestor_ids, parent_id)
grandson = rd.get([key], REDIS_PREFIX_CI_RELATION2)[0]
grandson = json.loads(grandson) if grandson is not None else {}
cr = CIRelation.get_by(first_ci_id=parent_id, second_ci_id=child_id, ancestor_ids=ancestor_ids,
first=True, to_dict=False)
if cr and str(cr.second_ci_id) not in grandson:
grandson[str(cr.second_ci_id)] = cr.second_ci.type_id
rd.create_or_update({key: json.dumps(grandson)}, REDIS_PREFIX_CI_RELATION2)
current_app.logger.info("ADD ci relation cache: {0} -> {1}".format(parent_id, child_id))
@ -126,8 +186,9 @@ def ci_relation_add(parent_dict, child_id, uid):
from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci import search
current_app.test_request_context().push()
login_user(UserCache.get(uid))
if not has_request_context():
current_app.test_request_context().push()
login_user(UserCache.get(uid))
for parent in parent_dict:
parent_ci_type_name, _attr_name = parent.strip()[1:].split('.', 1)
@ -149,20 +210,20 @@ def ci_relation_add(parent_dict, child_id, uid):
for ci in response:
try:
CIRelationManager.add(ci['_id'], child_id)
ci_relation_cache(ci['_id'], child_id)
ci_relation_cache(ci['_id'], child_id, None)
except Exception as e:
current_app.logger.warning(e)
finally:
try:
db.session.commit()
except:
pass
db.session.rollback()
@celery.task(name="cmdb.ci_relation_delete", queue=CMDB_QUEUE)
@reconnect_db
def ci_relation_delete(parent_id, child_id):
with Lock("CIRelation_{}".format(parent_id)):
def ci_relation_delete(parent_id, child_id, ancestor_ids):
with redis_lock.Lock(rd.r, "CIRelation_{}".format(parent_id), expire=10):
children = rd.get([parent_id], REDIS_PREFIX_CI_RELATION)[0]
children = json.loads(children) if children is not None else {}
@ -171,6 +232,16 @@ def ci_relation_delete(parent_id, child_id):
rd.create_or_update({parent_id: json.dumps(children)}, REDIS_PREFIX_CI_RELATION)
if ancestor_ids is not None:
key = "{},{}".format(ancestor_ids, parent_id)
grandson = rd.get([key], REDIS_PREFIX_CI_RELATION2)[0]
grandson = json.loads(grandson) if grandson is not None else {}
if str(child_id) in grandson:
grandson.pop(str(child_id))
rd.create_or_update({key: json.dumps(grandson)}, REDIS_PREFIX_CI_RELATION2)
current_app.logger.info("DELETE ci relation cache: {0} -> {1}".format(parent_id, child_id))
@ -204,11 +275,124 @@ def ci_type_attribute_order_rebuild(type_id, uid):
def calc_computed_attribute(attr_id, uid):
from api.lib.cmdb.ci import CIManager
current_app.test_request_context().push()
login_user(UserCache.get(uid))
if not has_request_context():
current_app.test_request_context().push()
login_user(UserCache.get(uid))
cim = CIManager()
for i in CITypeAttribute.get_by(attr_id=attr_id, to_dict=False):
cis = CI.get_by(type_id=i.type_id, to_dict=False)
for ci in cis:
cim.update(ci.id, {})
@celery.task(name="cmdb.write_ad_rule_sync_history", queue=CMDB_QUEUE)
@reconnect_db
def write_ad_rule_sync_history(rules, oneagent_id, oneagent_name, sync_at):
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryRuleSyncHistoryCRUD
for rule in rules:
AutoDiscoveryRuleSyncHistoryCRUD().upsert(adt_id=rule['id'],
oneagent_id=oneagent_id,
oneagent_name=oneagent_name,
sync_at=sync_at,
commit=False)
try:
db.session.commit()
except Exception as e:
current_app.logger.error("write auto discovery rule sync history failed: {}".format(e))
db.session.rollback()
@celery.task(name="cmdb.build_relations_for_ad_accept", queue=CMDB_QUEUE)
@reconnect_db
def build_relations_for_ad_accept(adc, ci_id, ad_key2attr):
from api.lib.cmdb.ci import CIRelationManager
from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci import search as ci_search
current_app.test_request_context().push()
login_user(UserCache.get('worker'))
relation_ads = AutoDiscoveryCITypeRelation.get_by(ad_type_id=adc['type_id'], to_dict=False)
for r_adt in relation_ads:
ad_key = r_adt.ad_key
if not adc['instance'].get(ad_key):
continue
ad_key_values = [adc['instance'].get(ad_key)] if not isinstance(
adc['instance'].get(ad_key), list) else adc['instance'].get(ad_key)
for ad_key_value in ad_key_values:
query = "_type:{},{}:{}".format(r_adt.peer_type_id, r_adt.peer_attr_id, ad_key_value)
s = ci_search(query, use_ci_filter=False, count=1000000)
try:
response, _, _, _, _, _ = s.search()
except SearchError as e:
current_app.logger.error("build_relations_for_ad_accept failed: {}".format(e))
return
for relation_ci in response:
relation_ci_id = relation_ci['_id']
try:
CIRelationManager.add(ci_id, relation_ci_id,
valid=False,
source=RelationSourceEnum.AUTO_DISCOVERY)
except:
try:
CIRelationManager.add(relation_ci_id, ci_id,
valid=False,
source=RelationSourceEnum.AUTO_DISCOVERY)
except:
pass
# build relations in reverse
relation_ads = AutoDiscoveryCITypeRelation.get_by(peer_type_id=adc['type_id'], to_dict=False)
attr2ad_key = {v: k for k, v in ad_key2attr.items()}
for r_adt in relation_ads:
attr = AttributeCache.get(r_adt.peer_attr_id)
ad_key = attr2ad_key.get(attr and attr.name)
if not ad_key:
continue
ad_value = adc['instance'].get(ad_key)
peer_ad_key = r_adt.ad_key
peer_instances = AutoDiscoveryCI.get_by(type_id=r_adt.ad_type_id, to_dict=False)
for peer_instance in peer_instances:
peer_ad_values = peer_instance.instance.get(peer_ad_key)
peer_ad_values = [peer_ad_values] if not isinstance(peer_ad_values, list) else peer_ad_values
if ad_value in peer_ad_values and peer_instance.ci_id:
try:
CIRelationManager.add(peer_instance.ci_id, ci_id,
valid=False,
source=RelationSourceEnum.AUTO_DISCOVERY)
except:
try:
CIRelationManager.add(ci_id, peer_instance.ci_id,
valid=False,
source=RelationSourceEnum.AUTO_DISCOVERY)
except:
pass
@celery.task(name="cmdb.dcim_calc_u_free_count", queue=CMDB_QUEUE)
@reconnect_db
def dcim_calc_u_free_count():
from api.lib.cmdb.ci import CIManager
from api.lib.cmdb.dcim.rack import RackManager
from api.lib.cmdb.dcim.const import RackBuiltinAttributes
if not has_request_context():
current_app.test_request_context().push()
login_user(UserCache.get('worker'))
try:
rack_m = RackManager()
except Exception:
return
racks = CI.get_by(type_id=rack_m.type_id, to_dict=False)
for rack in racks:
payload = {RackBuiltinAttributes.FREE_U_COUNT: rack_m.calc_u_free_count(rack.id)}
CIManager().update(rack.id, **payload)

View File

@ -1,24 +1,24 @@
# -*- coding:utf-8 -*-
import requests
from flask import current_app
from api.extensions import celery
from api.extensions import db
from api.lib.common_setting.acl import ACLManager
from api.lib.common_setting.const import COMMON_SETTING_QUEUE
from api.lib.perm.acl.const import ACL_QUEUE
from api.lib.common_setting.resp_format import ErrFormat
from api.models.common_setting import Department
from api.models.common_setting import Department, Employee
from api.lib.decorator import flush_db
from api.lib.decorator import reconnect_db
@celery.task(name="common_setting.edit_employee_department_in_acl", queue=COMMON_SETTING_QUEUE)
@celery.task(name="common_setting.edit_employee_department_in_acl", queue=ACL_QUEUE)
@flush_db
@reconnect_db
def edit_employee_department_in_acl(e_list, new_d_id, op_uid):
"""
:param e_list:{acl_rid: 11, department_id: 22}
:param new_d_id
:param op_uid
"""
db.session.remove()
result = []
new_department = Department.get_by(
first=True, department_id=new_d_id, to_dict=False)
@ -49,21 +49,20 @@ def edit_employee_department_in_acl(e_list, new_d_id, op_uid):
continue
old_d_rid_in_acl = role_map.get(old_department.department_name, 0)
if old_d_rid_in_acl == 0:
return
if old_d_rid_in_acl != old_department.acl_rid:
old_department.update(
acl_rid=old_d_rid_in_acl
)
d_acl_rid = old_department.acl_rid if old_d_rid_in_acl == old_department.acl_rid else old_d_rid_in_acl
payload = {
'app_id': 'acl',
'parent_id': d_acl_rid,
}
try:
acl.remove_user_from_role(employee_acl_rid, payload)
except Exception as e:
result.append(ErrFormat.acl_remove_user_from_role_failed.format(str(e)))
if old_d_rid_in_acl > 0:
if old_d_rid_in_acl != old_department.acl_rid:
old_department.update(
acl_rid=old_d_rid_in_acl
)
d_acl_rid = old_department.acl_rid if old_d_rid_in_acl == old_department.acl_rid else old_d_rid_in_acl
payload = {
'app_id': 'acl',
'parent_id': d_acl_rid,
}
try:
acl.remove_user_from_role(employee_acl_rid, payload)
except Exception as e:
result.append(ErrFormat.acl_remove_user_from_role_failed.format(str(e)))
payload = {
'app_id': 'acl',
@ -75,3 +74,57 @@ def edit_employee_department_in_acl(e_list, new_d_id, op_uid):
result.append(ErrFormat.acl_add_user_to_role_failed.format(str(e)))
return result
@celery.task(name="common_setting.refresh_employee_acl_info", queue=ACL_QUEUE)
@flush_db
@reconnect_db
def refresh_employee_acl_info(current_employee_id=None):
acl = ACLManager('acl')
role_map = {role['name']: role for role in acl.get_all_roles()}
criterion = [
Employee.deleted == 0
]
query = Employee.query.filter(*criterion).order_by(
Employee.created_at.desc()
)
current_employee_rid = 0
for em in query.all():
if current_employee_id and em.employee_id == current_employee_id:
current_employee_rid = em.acl_rid if em.acl_rid else 0
if em.acl_uid and em.acl_rid:
continue
role = role_map.get(em.username, None)
if not role:
continue
params = dict()
if not em.acl_uid:
params['acl_uid'] = role.get('uid', 0)
if not em.acl_rid:
params['acl_rid'] = role.get('id', 0)
if current_employee_id and em.employee_id == current_employee_id:
current_employee_rid = params['acl_rid'] if params.get('acl_rid', 0) else 0
try:
em.update(**params)
current_app.logger.info(
f"refresh_employee_acl_info success, employee_id: {em.employee_id}, uid: {em.acl_uid}, "
f"rid: {em.acl_rid}")
except Exception as e:
current_app.logger.error(str(e))
continue
if current_employee_rid and current_employee_rid > 0:
try:
from api.lib.common_setting.employee import GrantEmployeeACLPerm
GrantEmployeeACLPerm().grant_by_rid(current_employee_rid, False)
current_app.logger.info(f"GrantEmployeeACLPerm success, current_employee_rid: {current_employee_rid}")
except Exception as e:
current_app.logger.error(str(e))

Binary file not shown.

View File

@ -0,0 +1,943 @@
# Chinese translations for PROJECT.
# Copyright (C) 2023 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2023.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2024-11-26 18:54+0800\n"
"PO-Revision-Date: 2023-12-25 20:21+0800\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: zh\n"
"Language-Team: zh <LL@li.org>\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.16.0\n"
#: api/lib/resp_format.py:7
msgid "unauthorized"
msgstr "未认证"
#: api/lib/resp_format.py:8
msgid "unknown error"
msgstr "未知错误"
#: api/lib/resp_format.py:10
msgid "Illegal request"
msgstr "不合法的请求"
#: api/lib/resp_format.py:11
msgid "Invalid operation"
msgstr "无效的操作"
#: api/lib/resp_format.py:13
msgid "does not exist"
msgstr "不存在"
#: api/lib/resp_format.py:15
msgid "There is a circular dependency!"
msgstr "存在循环依赖!"
#: api/lib/resp_format.py:17
msgid "Unknown search error"
msgstr "未知搜索错误"
#: api/lib/resp_format.py:20
msgid "The json format seems to be incorrect, please confirm carefully!"
msgstr "# json格式似乎不正确了, 请仔细确认一下!"
#: api/lib/resp_format.py:23
msgid ""
"The format of parameter {} is incorrect, the format must be: yyyy-mm-dd "
"HH:MM:SS"
msgstr "参数 {} 格式不正确, 格式必须是: yyyy-mm-dd HH:MM:SS"
#: api/lib/resp_format.py:25
msgid "The value of parameter {} cannot be empty!"
msgstr "参数 {} 的值不能为空!"
#: api/lib/resp_format.py:26
msgid "The request is missing parameters {}"
msgstr "请求缺少参数 {}"
#: api/lib/resp_format.py:27
msgid "Invalid value for parameter {}"
msgstr "参数 {} 的值无效"
#: api/lib/resp_format.py:28
msgid "The length of parameter {} must be <= {}"
msgstr "参数 {} 的长度必须 <= {}"
#: api/lib/resp_format.py:30
msgid "Role {} can only operate!"
msgstr "角色 {} 才能操作!"
#: api/lib/resp_format.py:31
msgid "User {} does not exist"
msgstr "用户 {} 不存在"
#: api/lib/resp_format.py:32
msgid "For resource: {}, you do not have {} permission!"
msgstr "您没有资源: {} 的{}权限!"
#: api/lib/resp_format.py:33
msgid "You do not have permission to operate!"
msgstr "您没有操作权限!"
#: api/lib/resp_format.py:34
msgid "Only the creator or administrator has permission!"
msgstr "只有创建人或者管理员才有权限!"
#: api/lib/cmdb/const.py:133
msgid "Update Time"
msgstr "更新时间"
#: api/lib/cmdb/const.py:134
msgid "Updated By"
msgstr "更新人"
#: api/lib/cmdb/resp_format.py:9
msgid "CI Model"
msgstr "模型配置"
#: api/lib/cmdb/resp_format.py:11
msgid "Invalid relation type: {}"
msgstr "无效的关系类型: {}"
#: api/lib/cmdb/resp_format.py:12
msgid "CIType is not found"
msgstr "模型不存在!"
#: api/lib/cmdb/resp_format.py:15
msgid "The type of parameter attributes must be a list"
msgstr "参数 attributes 类型必须是列表"
#: api/lib/cmdb/resp_format.py:16
msgid "The file doesn't seem to be uploaded"
msgstr "文件似乎并未上传"
#: api/lib/cmdb/resp_format.py:18
msgid "Attribute {} does not exist!"
msgstr "属性 {} 不存在!"
#: api/lib/cmdb/resp_format.py:19
msgid ""
"This attribute is the unique identifier of the model and cannot be "
"deleted!"
msgstr "该属性是模型的唯一标识,不能被删除!"
#: api/lib/cmdb/resp_format.py:21
msgid "This attribute is referenced by model {} and cannot be deleted!"
msgstr "该属性被模型 {} 引用, 不能删除!"
#: api/lib/cmdb/resp_format.py:23
msgid "The value type of the attribute is not allowed to be modified!"
msgstr "属性的值类型不允许修改!"
#: api/lib/cmdb/resp_format.py:25
msgid "Multiple values are not allowed to be modified!"
msgstr "多值不被允许修改!"
#: api/lib/cmdb/resp_format.py:27
msgid "Modifying the index is not allowed for non-administrators!"
msgstr "修改索引 非管理员不被允许!"
#: api/lib/cmdb/resp_format.py:28
msgid "Index switching failed!"
msgstr "索引切换失败!"
#: api/lib/cmdb/resp_format.py:29
msgid "The predefined value is of the wrong type!"
msgstr "预定义值的类型不对!"
#: api/lib/cmdb/resp_format.py:30
msgid "Duplicate attribute name {}"
msgstr "重复的属性名 {}"
#: api/lib/cmdb/resp_format.py:31
msgid "Failed to create attribute {}!"
msgstr "创建属性 {} 失败!"
#: api/lib/cmdb/resp_format.py:32
msgid "Modify attribute {} failed!"
msgstr "修改属性 {} 失败!"
#: api/lib/cmdb/resp_format.py:33
msgid "You do not have permission to modify this attribute!"
msgstr "您没有权限修改该属性!"
#: api/lib/cmdb/resp_format.py:34
msgid "Only creators and administrators are allowed to delete attributes!"
msgstr "目前只允许 属性创建人、管理员 删除属性!"
#: api/lib/cmdb/resp_format.py:37
msgid ""
"Attribute field names cannot be built-in fields: id, _id, ci_id, type, "
"_type, ci_type, ticket_id"
msgstr "属性字段名不能是内置字段: id, _id, ci_id, type, _type, ci_type, ci_type, ticket_id"
#: api/lib/cmdb/resp_format.py:39
msgid "Predefined value: Other model request parameters are illegal!"
msgstr "预定义值: 其他模型请求参数不合法!"
#: api/lib/cmdb/resp_format.py:42
msgid "CI {} does not exist"
msgstr "CI {} 不存在"
#: api/lib/cmdb/resp_format.py:43
msgid "Multiple attribute joint unique verification failed: {}"
msgstr "多属性联合唯一校验不通过: {}"
#: api/lib/cmdb/resp_format.py:44
msgid "The model's primary key {} does not exist!"
msgstr "模型的主键 {} 不存在!"
#: api/lib/cmdb/resp_format.py:45
msgid "Primary key {} is missing"
msgstr "主键字段 {} 缺失"
#: api/lib/cmdb/resp_format.py:46
msgid "CI already exists!"
msgstr "CI 已经存在!"
#: api/lib/cmdb/resp_format.py:47
msgid "{}: CI reference {} does not exist!"
msgstr "{}: CI引用 {} 不存在!"
#: api/lib/cmdb/resp_format.py:48
msgid "{}: CI reference {} is illegal!"
msgstr "{}, CI引用 {} 不合法!"
#: api/lib/cmdb/resp_format.py:49
msgid "Relationship constraint: {}, verification failed"
msgstr "关系约束: {}, 校验失败"
#: api/lib/cmdb/resp_format.py:51
msgid ""
"Many-to-many relationship constraint: Model {} <-> {} already has a many-"
"to-many relationship!"
msgstr "多对多关系 限制: 模型 {} <-> {} 已经存在多对多关系!"
#: api/lib/cmdb/resp_format.py:54
msgid "CI relationship: {} does not exist"
msgstr "CI关系: {} 不存在"
#: api/lib/cmdb/resp_format.py:57
msgid "In search expressions, not supported before parentheses: or, not"
msgstr "搜索表达式里小括号前不支持: 或、非"
#: api/lib/cmdb/resp_format.py:59
msgid "Model {} does not exist"
msgstr "模型 {} 不存在"
#: api/lib/cmdb/resp_format.py:60
msgid "Model {} already exists"
msgstr "模型 {} 已经存在"
#: api/lib/cmdb/resp_format.py:61
msgid "The primary key is undefined or has been deleted"
msgstr "主键未定义或者已被删除"
#: api/lib/cmdb/resp_format.py:62
msgid "Only the creator can delete it!"
msgstr "只有创建人才能删除它!"
#: api/lib/cmdb/resp_format.py:63
msgid "The model cannot be deleted because the CI already exists"
msgstr "因为CI已经存在不能删除模型"
#: api/lib/cmdb/resp_format.py:65
msgid "The inheritance cannot be deleted because the CI already exists"
msgstr "因为CI已经存在不能删除继承关系"
#: api/lib/cmdb/resp_format.py:67
msgid "The model is inherited and cannot be deleted"
msgstr "该模型被继承, 不能删除"
#: api/lib/cmdb/resp_format.py:68
msgid "The model is referenced by attribute {} and cannot be deleted"
msgstr "该模型被属性 {} 引用, 不能删除"
#: api/lib/cmdb/resp_format.py:72
msgid ""
"The model cannot be deleted because the model is referenced by the "
"relational view {}"
msgstr "因为关系视图 {} 引用了该模型,不能删除模型"
#: api/lib/cmdb/resp_format.py:74
msgid "Model group {} does not exist"
msgstr "模型分组 {} 不存在"
#: api/lib/cmdb/resp_format.py:75
msgid "Model group {} already exists"
msgstr "模型分组 {} 已经存在"
#: api/lib/cmdb/resp_format.py:76
msgid "Model relationship {} does not exist"
msgstr "模型关系 {} 不存在"
#: api/lib/cmdb/resp_format.py:77
msgid "Attribute group {} already exists"
msgstr "属性分组 {} 已存在"
#: api/lib/cmdb/resp_format.py:78
msgid "Attribute group {} does not exist"
msgstr "属性分组 {} 不存在"
#: api/lib/cmdb/resp_format.py:80
msgid "Attribute group <{0}> - attribute <{1}> does not exist"
msgstr "属性组<{0}> - 属性<{1}> 不存在"
#: api/lib/cmdb/resp_format.py:81
msgid "The unique constraint already exists!"
msgstr "唯一约束已经存在!"
#: api/lib/cmdb/resp_format.py:83
msgid "Uniquely constrained attributes cannot be JSON and multi-valued"
msgstr "唯一约束的属性不能是 JSON 和 多值"
#: api/lib/cmdb/resp_format.py:84
msgid "Duplicated trigger"
msgstr "重复的触发器"
#: api/lib/cmdb/resp_format.py:85
msgid "Trigger {} does not exist"
msgstr "触发器 {} 不存在"
#: api/lib/cmdb/resp_format.py:86
msgid "Duplicated reconciliation rule"
msgstr ""
#: api/lib/cmdb/resp_format.py:87
msgid "Reconciliation rule {} does not exist"
msgstr "关系类型 {} 不存在"
#: api/lib/cmdb/resp_format.py:89
msgid "Operation record {} does not exist"
msgstr "操作记录 {} 不存在"
#: api/lib/cmdb/resp_format.py:90
msgid "Unique identifier cannot be deleted"
msgstr "不能删除唯一标识"
#: api/lib/cmdb/resp_format.py:91
msgid "Cannot delete default sorted attributes"
msgstr "不能删除默认排序的属性"
#: api/lib/cmdb/resp_format.py:93
msgid "No node selected"
msgstr "没有选择节点"
#: api/lib/cmdb/resp_format.py:94
msgid "This search option does not exist!"
msgstr "该搜索选项不存在!"
#: api/lib/cmdb/resp_format.py:95
msgid "This search option has a duplicate name!"
msgstr "该搜索选项命名重复!"
#: api/lib/cmdb/resp_format.py:97
msgid "Relationship type {} already exists"
msgstr "关系类型 {} 已经存在"
#: api/lib/cmdb/resp_format.py:98
msgid "Relationship type {} does not exist"
msgstr "关系类型 {} 不存在"
#: api/lib/cmdb/resp_format.py:100
msgid "Invalid attribute value: {}"
msgstr "无效的属性值: {}"
#: api/lib/cmdb/resp_format.py:101
msgid "{} Invalid value: {}"
msgstr "{} 无效的值: {}"
#: api/lib/cmdb/resp_format.py:102
msgid "{} is not in the predefined values"
msgstr "{} 不在预定义值里"
#: api/lib/cmdb/resp_format.py:104
msgid "The value of attribute {} must be unique, {} already exists"
msgstr "属性 {} 的值必须是唯一的, 当前值 {} 已存在"
#: api/lib/cmdb/resp_format.py:105
msgid "Attribute {} value must exist"
msgstr "属性 {} 值必须存在"
#: api/lib/cmdb/resp_format.py:106
msgid "Out of range value, the maximum value is 2147483647"
msgstr "超过最大值限制, 最大值是2147483647"
#: api/lib/cmdb/resp_format.py:108
msgid "Unknown error when adding or modifying attribute value: {}"
msgstr "新增或者修改属性值未知错误: {}"
#: api/lib/cmdb/resp_format.py:110
msgid "Duplicate custom name"
msgstr "订制名重复"
#: api/lib/cmdb/resp_format.py:112
msgid "Number of models exceeds limit: {}"
msgstr "模型数超过限制: {}"
#: api/lib/cmdb/resp_format.py:113
msgid "The number of CIs exceeds the limit: {}"
msgstr "CI数超过限制: {}"
#: api/lib/cmdb/resp_format.py:115
msgid "Auto-discovery rule: {} already exists!"
msgstr "自动发现规则: {} 已经存在!"
#: api/lib/cmdb/resp_format.py:116
msgid "Auto-discovery rule: {} does not exist!"
msgstr "自动发现规则: {} 不存在!"
#: api/lib/cmdb/resp_format.py:118
msgid "This auto-discovery rule is referenced by the model and cannot be deleted!"
msgstr "该自动发现规则被模型引用, 不能删除!"
#: api/lib/cmdb/resp_format.py:120
msgid "The application of auto-discovery rules cannot be defined repeatedly!"
msgstr "自动发现规则的应用不能重复定义!"
#: api/lib/cmdb/resp_format.py:121
msgid "The auto-discovery you want to modify: {} does not exist!"
msgstr "您要修改的自动发现: {} 不存在!"
#: api/lib/cmdb/resp_format.py:122
msgid "Attribute does not include unique identifier: {}"
msgstr "属性字段没有包括唯一标识: {}"
#: api/lib/cmdb/resp_format.py:123
msgid "The auto-discovery instance does not exist!"
msgstr "自动发现的实例不存在!"
#: api/lib/cmdb/resp_format.py:124
msgid "The model is not associated with this auto-discovery!"
msgstr "模型并未关联该自动发现!"
#: api/lib/cmdb/resp_format.py:125
msgid "Only the creator can modify the Secret!"
msgstr "只有创建人才能修改Secret!"
#: api/lib/cmdb/resp_format.py:127
msgid "This rule already has auto-discovery instances and cannot be deleted!"
msgstr "该规则已经有自动发现的实例, 不能被删除!"
#: api/lib/cmdb/resp_format.py:129
msgid "The default auto-discovery rule is already referenced by model {}!"
msgstr "该默认的自动发现规则 已经被模型 {} 引用!"
#: api/lib/cmdb/resp_format.py:131
msgid "The unique_key method must return a non-empty string!"
msgstr "unique_key方法必须返回非空字符串!"
#: api/lib/cmdb/resp_format.py:132
msgid "The attributes method must return a list"
msgstr "attributes方法必须返回的是list"
#: api/lib/cmdb/resp_format.py:134
msgid "The list returned by the attributes method cannot be empty!"
msgstr "attributes方法返回的list不能为空!"
#: api/lib/cmdb/resp_format.py:136
msgid "Only administrators can define execution targets as: all nodes!"
msgstr "只有管理员才可以定义执行机器为: 所有节点!"
#: api/lib/cmdb/resp_format.py:137
msgid "Execute targets permission check failed: {}"
msgstr "执行机器权限检查不通过: {}"
#: api/lib/cmdb/resp_format.py:139
msgid "CI filter authorization must be named!"
msgstr "CI过滤授权 必须命名!"
#: api/lib/cmdb/resp_format.py:140
msgid "CI filter authorization is currently not supported or query"
msgstr "CI过滤授权 暂时不支持 或 查询"
#: api/lib/cmdb/resp_format.py:143
msgid "You do not have permission to operate attribute {}!"
msgstr "您没有属性 {} 的操作权限!"
#: api/lib/cmdb/resp_format.py:144
msgid "You do not have permission to operate this CI!"
msgstr "您没有该CI的操作权限!"
#: api/lib/cmdb/resp_format.py:146
msgid "Failed to save password: {}"
msgstr "保存密码失败: {}"
#: api/lib/cmdb/resp_format.py:147
msgid "Failed to get password: {}"
msgstr "获取密码失败: {}"
#: api/lib/cmdb/resp_format.py:149
msgid "Scheduling time format error"
msgstr "{}格式错误,应该为:%Y-%m-%d %H:%M:%S"
#: api/lib/cmdb/resp_format.py:150
msgid "CMDB data reconciliation results"
msgstr "CMDB数据合规检查结果"
#: api/lib/cmdb/resp_format.py:151
msgid "Number of {} illegal: {}"
msgstr "{} 不合规数: {}"
#: api/lib/cmdb/resp_format.py:153
msgid "Topology view {} already exists"
msgstr "拓扑视图 {} 已经存在"
#: api/lib/cmdb/resp_format.py:154
msgid "Topology group {} already exists"
msgstr "拓扑视图分组 {} 已经存在"
#: api/lib/cmdb/resp_format.py:156
msgid "The group cannot be deleted because the topology view already exists"
msgstr "因为该分组下定义了拓扑视图,不能删除"
#: api/lib/cmdb/resp_format.py:158
msgid "Both the source model and the target model must be selected"
msgstr "源模型和目标模型不能为空!"
#: api/lib/cmdb/resp_format.py:160
msgid "The names of built-in models cannot be changed"
msgstr "内置模型的名字不能修改"
#: api/lib/cmdb/resp_format.py:162
msgid "The subnet model {} does not exist"
msgstr "子网模型 {} 不存在!"
#: api/lib/cmdb/resp_format.py:163
msgid "The IP Address model {} does not exist"
msgstr "IP地址模型 {} 不存在!"
#: api/lib/cmdb/resp_format.py:164
msgid "CIDR {} is an invalid notation"
msgstr "CIDR {} 写法不正确!"
#: api/lib/cmdb/resp_format.py:165
msgid "Invalid CIDR: {}, available subnets: {}"
msgstr "无效的CIDR: {}, 可用的子网: {}"
#: api/lib/cmdb/resp_format.py:166
msgid "Invalid subnet prefix length: {}"
msgstr "无效的子网前缀长度: {}"
#: api/lib/cmdb/resp_format.py:167
msgid "parent node cidr must be required"
msgstr "必须要有父节点"
#: api/lib/cmdb/resp_format.py:168
msgid "{} and {} overlap"
msgstr "{} 和 {} 有重叠"
#: api/lib/cmdb/resp_format.py:169 api/lib/cmdb/resp_format.py:171
msgid "Cannot delete because child nodes exist"
msgstr "因为子节点已经存在,不能删除"
#: api/lib/cmdb/resp_format.py:170
msgid "Subnet is not found"
msgstr "子网不存在"
#: api/lib/cmdb/resp_format.py:174
msgid "The dcim model {} does not exist"
msgstr "DCIM模型 {} 不存在!"
#: api/lib/cmdb/resp_format.py:175
msgid "Irregularities in Rack Units"
msgstr "机架U位异常!"
#: api/lib/cmdb/resp_format.py:176
msgid "The device's position is greater than the rack unit height"
msgstr "有设备的位置大于机柜的U数!"
#: api/lib/common_setting/resp_format.py:8
msgid "Company info already existed"
msgstr "公司信息已存在,无法创建!"
#: api/lib/common_setting/resp_format.py:10
msgid "No file part"
msgstr "没有文件部分"
#: api/lib/common_setting/resp_format.py:11
msgid "File is required"
msgstr "文件是必须的"
#: api/lib/common_setting/resp_format.py:12
msgid "File not found"
msgstr "文件不存在!"
#: api/lib/common_setting/resp_format.py:13
msgid "File type not allowed"
msgstr "文件类型不允许!"
#: api/lib/common_setting/resp_format.py:14
msgid "Upload failed: {}"
msgstr "上传失败: {}"
#: api/lib/common_setting/resp_format.py:16
msgid "Direct supervisor is not self"
msgstr "直属上级不能是自己"
#: api/lib/common_setting/resp_format.py:17
msgid "Parent department is not self"
msgstr "上级部门不能是自己"
#: api/lib/common_setting/resp_format.py:18
msgid "Employee list is empty"
msgstr "员工列表为空"
#: api/lib/common_setting/resp_format.py:20
msgid "Column name not support"
msgstr "不支持的列名"
#: api/lib/common_setting/resp_format.py:21
msgid "Password is required"
msgstr "密码是必须的"
#: api/lib/common_setting/resp_format.py:22
msgid "Employee acl rid is zero"
msgstr "员工ACL角色ID不能为0"
#: api/lib/common_setting/resp_format.py:24
msgid "Generate excel failed: {}"
msgstr "生成excel失败: {}"
#: api/lib/common_setting/resp_format.py:25
msgid "Rename columns failed: {}"
msgstr "重命名字段失败: {}"
#: api/lib/common_setting/resp_format.py:26
msgid "Cannot block this employee is other direct supervisor"
msgstr "该员工是其他员工的直属上级, 不能禁用"
#: api/lib/common_setting/resp_format.py:28
msgid "Cannot block this employee is department manager"
msgstr "该员工是部门负责人, 不能禁用"
#: api/lib/common_setting/resp_format.py:30
msgid "Employee id [{}] not found"
msgstr "员工ID [{}] 不存在!"
#: api/lib/common_setting/resp_format.py:31
msgid "Value is required"
msgstr "值是必须的"
#: api/lib/common_setting/resp_format.py:32
msgid "Email already exists"
msgstr "邮箱已存在!"
#: api/lib/common_setting/resp_format.py:33
msgid "Query {} none keep value empty"
msgstr "查询 {} 空值时请保持value为空"
#: api/lib/common_setting/resp_format.py:34
msgid "Not support operator: {}"
msgstr "不支持的操作符: {}"
#: api/lib/common_setting/resp_format.py:35
msgid "Not support relation: {}"
msgstr "不支持的关系: {}"
#: api/lib/common_setting/resp_format.py:36
msgid "Conditions field missing"
msgstr " conditions内元素字段缺失请检查"
#: api/lib/common_setting/resp_format.py:37
msgid "Datetime format error: {}"
msgstr "{}格式错误,应该为:%Y-%m-%d %H:%M:%S"
#: api/lib/common_setting/resp_format.py:38
msgid "Department level relation error"
msgstr "部门层级关系不正确"
#: api/lib/common_setting/resp_format.py:39
msgid "Delete reserved department name"
msgstr "保留部门,无法删除!"
#: api/lib/common_setting/resp_format.py:40
msgid "Department id is required"
msgstr "部门ID是必须的"
#: api/lib/common_setting/resp_format.py:41
msgid "Department list is required"
msgstr "部门列表是必须的"
#: api/lib/common_setting/resp_format.py:42
msgid "{} Cannot to be parent department"
msgstr "{} 不能设置为上级部门"
#: api/lib/common_setting/resp_format.py:43
msgid "Department id [{}] not found"
msgstr "部门ID [{}] 不存在"
#: api/lib/common_setting/resp_format.py:44
msgid "Parent department id must more than zero"
msgstr "上级部门ID必须大于0"
#: api/lib/common_setting/resp_format.py:45
msgid "Department name [{}] already exists"
msgstr "部门名称 [{}] 已存在"
#: api/lib/common_setting/resp_format.py:46
msgid "New department is none"
msgstr "新部门是空的"
#: api/lib/common_setting/resp_format.py:48
msgid "ACL edit user failed: {}"
msgstr "ACL 修改用户失败: {}"
#: api/lib/common_setting/resp_format.py:49
msgid "ACL uid not found: {}"
msgstr "ACL 用户UID [{}] 不存在"
#: api/lib/common_setting/resp_format.py:50
msgid "ACL add user failed: {}"
msgstr "ACL 添加用户失败: {}"
#: api/lib/common_setting/resp_format.py:51
msgid "ACL add role failed: {}"
msgstr "ACL 添加角色失败: {}"
#: api/lib/common_setting/resp_format.py:52
msgid "ACL update role failed: {}"
msgstr "ACL 更新角色失败: {}"
#: api/lib/common_setting/resp_format.py:53
msgid "ACL get all users failed: {}"
msgstr "ACL 获取所有用户失败: {}"
#: api/lib/common_setting/resp_format.py:54
msgid "ACL remove user from role failed: {}"
msgstr "ACL 从角色中移除用户失败: {}"
#: api/lib/common_setting/resp_format.py:55
msgid "ACL add user to role failed: {}"
msgstr "ACL 添加用户到角色失败: {}"
#: api/lib/common_setting/resp_format.py:56
msgid "ACL import user failed: {}"
msgstr "ACL 导入用户失败: {}"
#: api/lib/common_setting/resp_format.py:58
msgid "Nickname is required"
msgstr "昵称不能为空"
#: api/lib/common_setting/resp_format.py:59
msgid "Username is required"
msgstr "用户名不能为空"
#: api/lib/common_setting/resp_format.py:60
msgid "Email is required"
msgstr "邮箱不能为空"
#: api/lib/common_setting/resp_format.py:61
msgid "Email format error"
msgstr "邮箱格式错误"
#: api/lib/common_setting/resp_format.py:62
msgid "Email send timeout"
msgstr "邮件发送超时"
#: api/lib/common_setting/resp_format.py:64
msgid "Common data not found {} "
msgstr "ID {} 找不到记录"
#: api/lib/common_setting/resp_format.py:65
msgid "Common data {} already existed"
msgstr "{} 已经存在"
#: api/lib/common_setting/resp_format.py:66
msgid "Notice platform {} existed"
msgstr "{} 已经存在"
#: api/lib/common_setting/resp_format.py:67
msgid "Notice {} not existed"
msgstr "{} 配置项不存在"
#: api/lib/common_setting/resp_format.py:68
msgid "Notice please config messenger first"
msgstr "请先配置messenger URL"
#: api/lib/common_setting/resp_format.py:69
msgid "Notice bind err with empty mobile"
msgstr "绑定错误,手机号为空"
#: api/lib/common_setting/resp_format.py:70
msgid "Notice bind failed: {}"
msgstr "绑定失败: {}"
#: api/lib/common_setting/resp_format.py:71
msgid "Notice bind success"
msgstr "绑定成功"
#: api/lib/common_setting/resp_format.py:72
msgid "Notice remove bind success"
msgstr "解绑成功"
#: api/lib/common_setting/resp_format.py:74
msgid "Not support test type: {}"
msgstr "不支持的测试类型: {}"
#: api/lib/common_setting/resp_format.py:75
msgid "Not support auth type: {}"
msgstr "不支持的认证类型: {}"
#: api/lib/common_setting/resp_format.py:76
msgid "LDAP server connect timeout"
msgstr "LDAP服务器连接超时"
#: api/lib/common_setting/resp_format.py:77
msgid "LDAP server connect not available"
msgstr "LDAP服务器连接不可用"
#: api/lib/common_setting/resp_format.py:78
msgid "LDAP test unknown error: {}"
msgstr "LDAP测试未知错误: {}"
#: api/lib/common_setting/resp_format.py:79
msgid "Common data not support auth type: {}"
msgstr "通用数据不支持auth类型: {}"
#: api/lib/common_setting/resp_format.py:80
msgid "LDAP test username required"
msgstr "LDAP测试用户名必填"
#: api/lib/common_setting/resp_format.py:82
msgid "Company wide"
msgstr "全公司"
#: api/lib/common_setting/resp_format.py:84
msgid "No permission to access resource {}, perm {} "
msgstr "您没有资源: {} 的 {} 权限"
#: api/lib/perm/acl/resp_format.py:9
msgid "login successful"
msgstr "登录成功"
#: api/lib/perm/acl/resp_format.py:10
msgid "Failed to connect to LDAP service"
msgstr "连接LDAP服务失败"
#: api/lib/perm/acl/resp_format.py:11
msgid "Password verification failed"
msgstr "密码验证失败"
#: api/lib/perm/acl/resp_format.py:12
msgid "Application Token verification failed"
msgstr "应用 Token验证失败"
#: api/lib/perm/acl/resp_format.py:14
msgid ""
"You are not the application administrator or the session has expired (try"
" logging out and logging in again)"
msgstr "您不是应用管理员 或者 session失效(尝试一下退出重新登录)"
#: api/lib/perm/acl/resp_format.py:17
msgid "Resource type {} does not exist!"
msgstr "资源类型 {} 不存在!"
#: api/lib/perm/acl/resp_format.py:18
msgid "Resource type {} already exists!"
msgstr "资源类型 {} 已经存在!"
#: api/lib/perm/acl/resp_format.py:20
msgid "Because there are resources under this type, they cannot be deleted!"
msgstr "因为该类型下有资源的存在, 不能删除!"
#: api/lib/perm/acl/resp_format.py:22
msgid "User {} does not exist!"
msgstr "用户 {} 不存在!"
#: api/lib/perm/acl/resp_format.py:23
msgid "User {} already exists!"
msgstr "用户 {} 已经存在!"
#: api/lib/perm/acl/resp_format.py:24
msgid "Role {} does not exist!"
msgstr "角色 {} 不存在!"
#: api/lib/perm/acl/resp_format.py:25
msgid "Role {} already exists!"
msgstr "角色 {} 已经存在!"
#: api/lib/perm/acl/resp_format.py:26
msgid "Global role {} does not exist!"
msgstr "全局角色 {} 不存在!"
#: api/lib/perm/acl/resp_format.py:27
msgid "Global role {} already exists!"
msgstr "全局角色 {} 已经存在!"
#: api/lib/perm/acl/resp_format.py:29
msgid "You do not have {} permission on resource: {}"
msgstr "您没有资源: {} 的 {} 权限"
#: api/lib/perm/acl/resp_format.py:30
msgid "Requires administrator permissions"
msgstr "需要管理员权限"
#: api/lib/perm/acl/resp_format.py:31
msgid "Requires role: {}"
msgstr "需要角色: {}"
#: api/lib/perm/acl/resp_format.py:33
msgid "To delete a user role, please operate on the User Management page!"
msgstr "删除用户角色, 请在 用户管理 页面操作!"
#: api/lib/perm/acl/resp_format.py:35
msgid "Application {} already exists"
msgstr "应用 {} 已经存在"
#: api/lib/perm/acl/resp_format.py:36
msgid "Application {} does not exist!"
msgstr "应用 {} 不存在!"
#: api/lib/perm/acl/resp_format.py:37
msgid "The Secret is invalid"
msgstr "应用的Secret无效"
#: api/lib/perm/acl/resp_format.py:39
msgid "Resource {} does not exist!"
msgstr "资源 {} 不存在!"
#: api/lib/perm/acl/resp_format.py:40
msgid "Resource {} already exists!"
msgstr "资源 {} 已经存在!"
#: api/lib/perm/acl/resp_format.py:42
msgid "Resource group {} does not exist!"
msgstr "资源组 {} 不存在!"
#: api/lib/perm/acl/resp_format.py:43
msgid "Resource group {} already exists!"
msgstr "资源组 {} 已经存在!"
#: api/lib/perm/acl/resp_format.py:45
msgid "Inheritance detected infinite loop"
msgstr "继承检测到了死循环"
#: api/lib/perm/acl/resp_format.py:46
msgid "Role relationship {} does not exist!"
msgstr "角色关系 {} 不存在!"
#: api/lib/perm/acl/resp_format.py:48
msgid "Trigger {} does not exist!"
msgstr "触发器 {} 不存在!"
#: api/lib/perm/acl/resp_format.py:49
msgid "Trigger {} already exists!"
msgstr "触发器 {} 已经存在!"
#: api/lib/perm/acl/resp_format.py:50
msgid "Trigger {} has been disabled!"
msgstr "Trigger {} has been disabled!"
#~ msgid "Not a valid date value."
#~ msgstr ""

View File

@ -24,6 +24,7 @@ class AuditLogView(APIView):
'role': AuditCRUD.search_role,
'trigger': AuditCRUD.search_trigger,
'resource': AuditCRUD.search_resource,
'login': AuditCRUD.search_login,
}
if name not in func_map:
abort(400, f'wrong {name}, please use {func_map.keys()}')

View File

@ -1,22 +1,27 @@
# -*- coding:utf-8 -*-
import datetime
import jwt
import six
from flask import abort
from flask import current_app
from flask import request
from flask import session
from flask_login import login_user, logout_user
from flask_login import login_user
from flask_login import logout_user
from api.lib.common_setting.common_data import AuthenticateDataCRUD
from api.lib.common_setting.const import AuthenticateType
from api.lib.decorator import args_required
from api.lib.decorator import args_validate
from api.lib.perm.acl.acl import ACLManager
from api.lib.perm.acl.audit import AuditCRUD
from api.lib.perm.acl.cache import AppCache
from api.lib.perm.acl.cache import RoleCache
from api.lib.perm.acl.cache import User
from api.lib.perm.acl.cache import UserCache
from api.lib.perm.acl.resp_format import ErrFormat
from api.lib.perm.acl.role import RoleRelationCRUD
from api.lib.perm.auth import auth_abandoned
from api.lib.perm.auth import auth_with_app_token
from api.models.acl import Role
@ -34,8 +39,11 @@ class LoginView(APIView):
username = request.values.get("username") or request.values.get("email")
password = request.values.get("password")
_role = None
if current_app.config.get('AUTH_WITH_LDAP'):
user, authenticated = User.query.authenticate_with_ldap(username, password)
auth_with_ldap = request.values.get('auth_with_ldap', True)
config = AuthenticateDataCRUD(AuthenticateType.LDAP).get()
if (config.get('enabled') or config.get('enable')) and auth_with_ldap:
from api.lib.perm.authentication.ldap import authenticate_with_ldap
user, authenticated = authenticate_with_ldap(username, password)
else:
user, authenticated = User.query.authenticate(username, password)
if not user:
@ -117,10 +125,17 @@ class AuthWithKeyView(APIView):
if not user.get('username'):
user['username'] = user.get('name')
return self.jsonify(user=user,
authenticated=authenticated,
rid=role and role.id,
can_proxy=can_proxy)
result = dict(user=user,
authenticated=authenticated,
rid=role and role.id,
can_proxy=can_proxy)
if request.values.get('need_parentRoles') in current_app.config.get('BOOL_TRUE'):
app_id = AppCache.get(request.values.get('app_id'))
parent_ids = RoleRelationCRUD.recursive_parent_ids(role and role.id, app_id and app_id.id)
result['user']['parentRoles'] = [RoleCache.get(rid).name for rid in set(parent_ids) if RoleCache.get(rid)]
return self.jsonify(result)
class AuthWithTokenView(APIView):
@ -176,4 +191,9 @@ class LogoutView(APIView):
@auth_abandoned
def post(self):
logout_user()
AuditCRUD.add_login_log(None, None, None,
_id=session.get('LOGIN_ID') or request.values.get('LOGIN_ID'),
logout_at=datetime.datetime.now())
self.jsonify(code=200)

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