Compare commits

..

757 Commits

Author SHA1 Message Date
pycook
a4e686f9ae chore: release v2.5.4 2025-08-15 21:54:46 +08:00
LH_R
45bb3867da fix(ui): resource views - fix page display during initialization 2025-08-15 21:37:07 +08:00
LH_R
bb467030e2 feat(ui): attribute association - many-to-many cannot all be multi-value 2025-08-15 21:34:01 +08:00
pycook
0b1dfa4538 feat(api): multi-value attribute handling in relation building 2025-08-15 20:32:22 +08:00
LH_R
56a310c667 feat(ui): record the page path when logout 2025-08-15 16:09:41 +08:00
LH_R
290a79860c feat(ui): update tree views page empty data display 2025-08-15 16:07:35 +08:00
LH_R
0144ee6508 feat(ui): add automatic subscription #285 2025-08-15 16:06:23 +08:00
LH_R
035171cbe8 feat(ui): update resource view menu display and CMDB route redirection 2025-08-15 16:04:10 +08:00
LH_R
f7273c96dc feat(ui): support multi-value attribute associations 2025-08-15 15:57:59 +08:00
LH_R
73bdd99829 feat(ui): update iconfont 2025-08-15 15:52:39 +08:00
pycook
f46214aaf8 fix(api): click command: add-user #675 2025-08-14 20:46:57 +08:00
pycook
a809933a5f feat(api): Add auto subscription config 2025-08-14 20:19:49 +08:00
LH_R
5048f2a788 feat(ui): topology view & batch import - update CI Type select component 2025-07-09 16:23:18 +08:00
LH_R
40a53a0213 feat(ui): add CMDBTypeSelectAntd component 2025-07-07 12:17:30 +08:00
LH_R
1947af5693 feat(ui): update i18n 2025-06-23 16:26:49 +08:00
LH_R
6ec7caf5ea feat(ui): remove unused code and unused files 2025-06-23 16:26:38 +08:00
pycook
e93d894f04 chore: release v2.5.3 2025-06-20 21:30:16 +08:00
LH_R
081f35816f feat(ui): ci relation table - add upstream and downstream grouping 2025-06-20 16:27:51 +08:00
pycook
a8fadb2785 feat(api): add system language api 2025-06-19 12:46:24 +08:00
LH_R
72c37c995d feat(ui): update i18n 2025-06-18 15:35:45 +08:00
LH_R
f8fbbe4b9a feat(ui): i18n - init language add getSystemLanguage request 2025-06-17 21:19:51 +08:00
LH_R
155ba67ecc fix(ui): bug (#702) 2025-06-09 15:08:38 +08:00
LH_R
9c67b1e56a feat(ui): update iconfont 2025-06-08 23:45:44 +08:00
LH_R
88df3355d8 feat(ui): update adc permission 2025-05-16 17:34:14 +08:00
LH_R
549056a42d feat(ui): IPAM - ipSearch and subnetList add batch action group 2025-04-25 16:33:21 +08:00
LH_R
365fdf2bab feat(ui): RelationView - AddTableModal checkbox config add range 2025-04-25 16:32:38 +08:00
LH_R
6bf01786d8 fix(ui): CI - relation table repeat CIType 2025-04-25 16:32:06 +08:00
LH_R
e180f549c8 fix(ui): CI - ci detail title display 2025-04-25 16:31:43 +08:00
LH_R
3a7f4a31d0 fix(ui): Login - LDAP checkbox display condition 2025-04-22 21:33:17 +08:00
pycook
be4fc62218 chore: release v2.5.2 2025-04-17 21:19:46 +08:00
LH_R
ff4ce4dbe0 feat(ui): CI - update JSON type attr tooltip display 2025-04-16 20:54:11 +08:00
LH_R
dda1fce46a feat(ui): CI - relation table by subscribed attr 2025-04-16 20:53:54 +08:00
pycook
fbf59e7b44 perf(api): net device auto discovery 2025-04-16 19:55:02 +08:00
LH_R
4ae67d1f0f feat(ui): CI - hide groups without attr 2025-04-15 19:01:31 +08:00
LH_R
b56cf5bb3d feat(ui): CI - update default style of attr(JSON type) 2025-04-15 16:36:24 +08:00
Leo Song
53e8d34c68 Merge pull request #693 from veops/dev_ui_250411
fix(ui): dcim - device select name display
2025-04-11 15:51:50 +08:00
LH_R
c62e4032e3 fix(ui): dcim - device select name display 2025-04-11 15:51:12 +08:00
Leo Song
108c11071a Merge pull request #692 from veops/dev_ui_250410
dev_ ui_250410
2025-04-10 16:42:21 +08:00
LH_R
debb25f65b feat(ui): CI - update relation layout style 2025-04-10 16:38:42 +08:00
LH_R
f26dd65d07 feat(ui): CIType[relation] - update select filterOption 2025-04-10 16:14:00 +08:00
Leo Song
cb2726c890 Merge pull request #691 from veops/dev_ui_250409
feat(ui): CI Type[AD] - update snmp configuration style
2025-04-09 10:02:01 +08:00
LH_R
2003fd4a48 feat(ui): CI Type[AD] - update snmp configuration style 2025-04-09 10:01:35 +08:00
Leo Song
1bbf8c10b5 Merge pull request #689 from veops/dev_ui_250408
feat(ui): CI Type[AD] - update scanning configuration
2025-04-08 15:53:03 +08:00
LH_R
93e919b73f feat(ui): CI Type[AD] - update scanning configuration 2025-04-08 15:52:30 +08:00
thexqn
435bb2a2c8 fix(api): 使用 ast.literal_eval 代替 eval,取消不正确的计算属性值返回。 (#688)
* fix(api): 使用 ast.literal_eval 代替 eval,取消不正确的计算属性值返回。

* fix(api): 修复属性值计算逻辑,直接返回渲染结果。
2025-04-04 19:19:42 +08:00
pycook
5ceb8ff6f9 feat: update nginx configuration client_max_body_size 2025-03-26 21:02:28 +08:00
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
357 changed files with 36109 additions and 6448 deletions

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

195
README.md
View File

@@ -1,119 +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 一键快速构建
<a href="https://github.com/veops/cmdb/graphs/contributors">
<img src="https://contrib.rocks/image?repo=veops/cmdb" />
</a>
[//]: # (> 方法一)
- 第一步: 先安装 Docker 环境, 以及Docker Compose (v2)
- 第二步: 拷贝项目
```shell
git clone https://github.com/veops/cmdb.git
```
- 第三步:进入主目录,执行:
```
docker compose up -d
```
## 更多开源
[//]: # (> 方法二, 该方法适用于linux系统)
- [OneTerm](https://github.com/veops/oneterm): 一款简单、轻量、灵活的堡垒机服务。
- [messenger](https://github.com/veops/messenger): 一个简单轻量的消息发送服务。
- [ACL](https://github.com/veops/acl): 一个简单通用的权限管理系统设计与实践。
[//]: # (- 第一步: 先安装 Docker 环境, 以及Docker Compose &#40;v2&#41;)
## 相关文章
[//]: # (- 第二步: 直接使用项目根目录下的install.sh 文件进行 `安装`、`启动`、`暂停`、`查状态`、`删除`、`卸载`)
- <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>
[//]: # (```shell)
更多文章可以在公众号 **维易科技OneOps** 里查看
[//]: # (curl -so install.sh https://raw.githubusercontent.com/veops/cmdb/deploy_on_kylin_docker/install.sh)
## 与我联系
[//]: # (sh install.sh install)
[//]: # (```)
### [本地开发环境搭建](docs/local.md)
### [Makefile 安装](docs/makefile.md)
## 验证
- 浏览器打开: [http://127.0.0.1:8000](http://127.0.0.1:8000)
- username: demo 或者 admin
- password: 123456
---
_**欢迎关注公众号(维易科技OneOps),关注后可加入微信群,进行产品和技术交流。**_
<p align="center">
<img src="docs/images/wechat.png" alt="公众号: 维易科技OneOps" />
</p>
+ 邮箱: <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

@@ -11,7 +11,7 @@ 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"
@@ -68,6 +68,8 @@ 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

@@ -1,6 +1,8 @@
import click
from flask import current_app
from flask.cli import with_appcontext
from api.lib.exception import AbortException
from api.lib.perm.acl.user import UserCRUD
@@ -46,11 +48,18 @@ def add_user():
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)
current_app.test_request_context().push()
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
try:
RoleRelationCRUD.add(acl_admin, acl_admin.id, [rid], app.id)
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(None, '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)
except AbortException as e:
print(f"Failed: {e}")

View File

@@ -1,14 +1,13 @@
# -*- coding:utf-8 -*-
import click
import copy
import datetime
import json
import requests
import time
import uuid
import click
import requests
from flask import current_app
from flask.cli import with_appcontext
from flask_login import login_user
@@ -24,6 +23,7 @@ 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
@@ -37,11 +37,14 @@ 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()
@@ -193,7 +196,7 @@ def cmdb_counter():
today = datetime.date.today()
while True:
try:
db.session.remove()
db.session.commit()
CMDBCounterCache.reset()
@@ -207,6 +210,8 @@ def cmdb_counter():
CMDBCounterCache.flush_sub_counter()
RackManager().check_u_slot()
i += 1
except:
import traceback
@@ -223,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()
@@ -251,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]
@@ -341,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})
@@ -557,5 +568,20 @@ def cmdb_patch(version):
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

@@ -108,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
@@ -140,7 +141,7 @@ class AttributeManager(object):
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].get('label')}
return {i[0]: i[1]['label'] for i in choice_values if i[1] and i[1].get('label')}
return {}
@@ -414,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

@@ -12,6 +12,7 @@ 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 DEFAULT_INNER
from api.lib.cmdb.auto_discovery.const import NET_DEVICE_NAMES
from api.lib.cmdb.auto_discovery.const import PRIVILEGED_USERS
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import AutoDiscoveryMappingCache
@@ -252,6 +253,7 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
:return:
"""
result = []
db.session.commit()
rules = cls.cls.get_by(to_dict=True)
for rule in rules:
@@ -718,6 +720,12 @@ class AutoDiscoveryCICRUD(DBMixin):
build_relations_for_ad_accept.apply_async(args=(adc.to_dict(), ci_id, ad_key2attr), queue=CMDB_QUEUE)
ci_type = CITypeCache.get(adc.type_id)
if ci_type and ci_type.name in NET_DEVICE_NAMES and 'ports' in adc.instance:
from api.tasks.cmdb import add_net_device_ports
add_net_device_ports.apply_async(args=(ci_id, adc.instance['ports']),
queue=CMDB_QUEUE)
adc.update(is_accept=True,
accept_by=nickname or current_user.nickname,
accept_time=datetime.datetime.now(),

View File

@@ -4,6 +4,8 @@ from api.lib.cmdb.const import AutoDiscoveryType
PRIVILEGED_USERS = ("cmdb_agent", "worker", "admin")
NET_DEVICE_NAMES = {"switch", 'router', 'firewall', 'printer'}
DEFAULT_INNER = [
dict(name="阿里云", en="aliyun", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-aliyun'}, "en": "aliyun"}),
@@ -41,8 +43,12 @@ DEFAULT_INNER = [
option={'icon': {'name': 'caise-luyouqi'}}),
dict(name="防火墙", type=AutoDiscoveryType.SNMP, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-fanghuoqiang'}}),
dict(name="打印机", type=AutoDiscoveryType.SNMP, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-dayinji'}}),
# dict(name="打印机", type=AutoDiscoveryType.SNMP, is_inner=True, is_plugin=False,
# option={'icon': {'name': 'caise-dayinji'}}),
dict(name="光纤交换机", type=AutoDiscoveryType.SNMP, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-fiber'}}),
dict(name="F5", type=AutoDiscoveryType.SNMP, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-F5'}}),
]
CLOUD_MAP = {

View File

@@ -1,37 +1,74 @@
[{
"name":"manufacturer",
"type": "文本",
"example":"HUAWEI Technology Co.,Ltd",
"desc":"制造产商"
},{
"name":"sn",
"type": "文本",
"example":"102030059898",
"desc":"设备序列号"
},{
"name":"device_name",
"type": "文本",
"example":"USG6525E",
"desc":"设备名称"
},{
"name":"device_model",
"type": "文本",
"example":"2011.2.321.1.205",
"desc":"设备细分类型 结合相关产商获取相应的产品类型"
},{
"name":"description",
"type": "文本",
"example":"Huawei Vwersatile Routing Platform Software",
"desc":"设备描述"
},{
"name":"manager_ip",
"type": "文本",
"example":"192.168.1.1",
"desc":"管理ip"
}, {
"name":"ips",
"type": "文本、多值",
"example":"192.168.1.1, 192.168.1.2",
"desc":"ips"
}
[
{
"name": "manufacturer",
"type": "文本",
"example": "Huawei",
"desc": "制造产商"
},
{
"name": "sn",
"type": "文本",
"example": "102030059898",
"desc": "设备序列号"
},
{
"name": "name",
"type": "文本",
"example": "USG6525E",
"desc": "设备名称"
},
{
"name": "model",
"type": "文本",
"example": "2011.2.321.1.205",
"desc": "设备细分类型 结合相关产商获取相应的产品类型"
},
{
"name": "description",
"type": "文本",
"example": "Huawei Vwersatile Routing Platform Software",
"desc": "设备描述"
},
{
"name": "manager_ip",
"type": "文本",
"example": "192.168.1.1",
"desc": "管理ip"
},
{
"name": "ips",
"type": "文本、多值",
"example": "192.168.1.1, 192.168.1.2",
"desc": "ips"
},
{
"name": "uptime",
"type": "文本",
"example": "2023-04-15 10:00:00",
"desc": "启动时间"
},
{
"name": "snmp_version",
"type": "文本",
"example": "v2c",
"desc": "SNMP版本"
},
{
"name": "port_num",
"type": "整数",
"example": 24,
"desc": "端口数量"
},
{
"name": "ports",
"type": "json",
"example": "",
"desc": "设备的端口列表"
},
{
"name": "neighbors",
"type": "json",
"example": "",
"desc": "设备的邻居列表"
}
]

View File

@@ -512,7 +512,7 @@ class CMDBCounterCache(object):
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([i.oneagent_id for adt in adts for i in db.session.query(
set([j.oneagent_id for adt in adts for j in db.session.query(
AutoDiscoveryRuleSyncHistory.oneagent_id).filter(
AutoDiscoveryRuleSyncHistory.adt_id == adt.id)]))

View File

@@ -45,6 +45,7 @@ from api.lib.notify import notify_send
from api.lib.perm.acl.acl import ACLManager
from api.lib.perm.acl.acl import is_app_admin
from api.lib.perm.acl.acl import validate_permission
from api.lib.perm.acl.cache import UserCache
from api.lib.secrets.inner import InnerCrypt
from api.lib.secrets.vault import VaultClient
from api.lib.utils import handle_arg_list
@@ -113,7 +114,8 @@ class CIManager(object):
ci_type = CITypeCache.get(ci.type_id)
res["ci_type"] = ci_type.name
res.update(cls.get_cis_by_ids([str(ci_id)], fields=fields, ret_key=ret_key))
ci_list = cls.get_cis_by_ids([str(ci_id)], fields=fields, ret_key=ret_key)
ci_list and res.update(ci_list[0])
res['_type'] = ci_type.id
res['_id'] = ci_id
@@ -206,6 +208,8 @@ class CIManager(object):
res['_type'] = ci_type.id
res['ci_type_alias'] = ci_type.alias
res['_id'] = ci_id
res['_updated_at'] = str(ci.updated_at or '')
res['_updated_by'] = ci.updated_by
return res
@@ -291,7 +295,7 @@ class CIManager(object):
db.session.commit()
value_table = TableMap(attr_name=attr.name).table
with redis_lock.Lock(rd.r, "auto_inc_id_{}".format(attr.name)):
with redis_lock.Lock(rd.r, "auto_inc_id_{}".format(attr.name), expire=10):
max_v = value_table.get_by(attr_id=attr.id, only_query=True).order_by(
getattr(value_table, 'value').desc()).first()
if max_v is not None:
@@ -353,17 +357,31 @@ class CIManager(object):
is_auto_discovery=False,
_is_admin=False,
ticket_id=None,
_sync=False,
**ci_dict):
"""
add ci
:param ci_type_name:
:param exist_policy: replace or reject or need
:param _no_attribute_policy: ignore or reject
:param is_auto_discovery: default is False
:param _is_admin: default is False
:param ticket_id:
:param ci_dict:
:return:
Create a new Configuration Item (CI) or update existing based on unique constraints.
Handles complete CI creation workflow including validation, uniqueness checks,
password encryption, computed attributes, relationship creation, and caching.
Args:
ci_type_name (str): Name of the CI type to create
exist_policy (ExistPolicy): How to handle existing CIs (REPLACE/REJECT/NEED)
_no_attribute_policy (ExistPolicy): How to handle unknown attributes (IGNORE/REJECT)
is_auto_discovery (bool): Whether CI is created by auto-discovery process
_is_admin (bool): Whether to skip permission checks
ticket_id (int, optional): Associated ticket ID for audit trail
_sync (bool): Whether to execute cache/relation tasks synchronously
**ci_dict: CI attribute values as key-value pairs
Returns:
int: ID of the created or updated CI
Raises:
400: If unique constraints violated, required attributes missing, or validation fails
403: If user lacks permissions for restricted attributes
404: If CI type not found or referenced CI not exists
"""
now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
ci_type = CITypeManager.check_is_existed(ci_type_name)
@@ -387,7 +405,7 @@ class CIManager(object):
ci = None
record_id = None
password_dict = {}
with redis_lock.Lock(rd.r, ci_type.name):
with redis_lock.Lock(rd.r, ci_type.name, expire=10):
db.session.commit()
if (unique_key.default and unique_key.default.get('default') == AttributeDefaultValueEnum.AUTO_INC_ID and
@@ -492,14 +510,38 @@ class CIManager(object):
record_id = cls.save_password(ci.id, attr_id, password_dict[attr_id], record_id, ci_type.id)
if record_id or has_dynamic: # has changed
ci_cache.apply_async(args=(ci.id, operate_type, record_id), queue=CMDB_QUEUE)
if not _sync:
ci_cache.apply_async(args=(ci.id, operate_type, record_id), queue=CMDB_QUEUE)
else:
ci_cache(ci.id, operate_type, record_id)
if ref_ci_dict: # add relations
ci_relation_add.apply_async(args=(ref_ci_dict, ci.id, current_user.uid), queue=CMDB_QUEUE)
if not _sync:
ci_relation_add.apply_async(args=(ref_ci_dict, ci.id, current_user.uid), queue=CMDB_QUEUE)
else:
ci_relation_add(ref_ci_dict, ci.id, current_user.uid)
return ci.id
def update(self, ci_id, _is_admin=False, ticket_id=None, _sync=False, **ci_dict):
"""
Update an existing Configuration Item with new attribute values.
Performs comprehensive CI update including validation, constraint checks,
password handling, computed attributes processing, and change tracking.
Args:
ci_id (int): ID of the CI to update
_is_admin (bool): Whether to skip permission checks
ticket_id (int, optional): Associated ticket ID for audit trail
_sync (bool): Whether to execute cache/relation tasks synchronously
**ci_dict: CI attribute values to update as key-value pairs
Raises:
400: If unique constraints violated or validation fails
403: If user lacks permissions for restricted attributes
404: If CI not found
"""
now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
ci = self.confirm_ci_existed(ci_id)
ci_type = ci.ci_type
@@ -512,10 +554,14 @@ class CIManager(object):
raw_dict = copy.deepcopy(ci_dict)
ci_attr2type_attr = {type_attr.attr_id: type_attr for type_attr, _ in attrs}
unique_name = None
for _, attr in attrs:
if attr.default and attr.default.get('default') == AttributeDefaultValueEnum.UPDATED_AT:
ci_dict[attr.name] = now
if attr.id == ci_type.unique_id:
unique_name = attr.name
value_manager = AttributeValueManager()
password_dict = dict()
@@ -538,14 +584,15 @@ class CIManager(object):
limit_attrs = self._valid_ci_for_no_read(ci) if not _is_admin else {}
record_id = None
with redis_lock.Lock(rd.r, ci_type.name):
with redis_lock.Lock(rd.r, ci_type.name, expire=10):
db.session.commit()
self._valid_unique_constraint(ci.type_id, ci_dict, ci_id)
ci_dict = {k: v for k, v in ci_dict.items() if k in ci_type_attrs_name}
key2attr = value_manager.valid_attr_value(ci_dict, ci.type_id, ci.id, ci_type_attrs_name,
ci_attr2type_attr=ci_attr2type_attr)
ci_attr2type_attr=ci_attr2type_attr,
unique_name=unique_name)
if computed_attrs:
value_manager.handle_ci_compute_attributes(ci_dict, computed_attrs, ci)
@@ -568,6 +615,9 @@ class CIManager(object):
for attr_id in password_dict:
record_id = self.save_password(ci.id, attr_id, password_dict[attr_id], record_id, ci.type_id)
u = UserCache.get(current_user.uid)
ci.update(updated_at=now, updated_by=u and u.nickname)
if record_id or has_dynamic: # has changed
if not _sync:
ci_cache.apply_async(args=(ci_id, OperateType.UPDATE, record_id), queue=CMDB_QUEUE)
@@ -581,6 +631,9 @@ class CIManager(object):
else:
ci_relation_add(ref_ci_dict, ci.id)
u = UserCache.get(current_user.uid)
ci.update(updated_at=now, updated_by=u and u.nickname)
@staticmethod
def update_unique_value(ci_id, unique_name, unique_value):
ci = CI.get_by_id(ci_id) or abort(404, ErrFormat.ci_not_found.format("id={}".format(ci_id)))
@@ -709,13 +762,18 @@ class CIManager(object):
elif fields:
_res = []
for d in res:
if isinstance(fields, dict) and d.get("_type") not in fields:
_res.append(d)
continue
_d = dict()
_d["_id"], _d["_type"] = d.get("_id"), d.get("_type")
_d["ci_type"] = d.get("ci_type")
if unique_required:
_d[d.get('unique')] = d.get(d.get('unique'))
for field in fields + ['ci_type_alias', 'unique', 'unique_alias']:
_fields = list(fields.get(_d['_type']) or [] if isinstance(fields, dict) else fields)
for field in _fields + ['ci_type_alias', 'unique', 'unique_alias']:
_d[field] = d.get(field)
_res.append(_d)
return _res
@@ -732,9 +790,8 @@ class CIManager(object):
from api.lib.cmdb.search.ci.db.query_sql import QUERY_CIS_BY_IDS
from api.lib.cmdb.search.ci.db.query_sql import QUERY_CIS_BY_VALUE_TABLE
if not fields:
filter_fields_sql = ""
else:
filter_fields_sql = ""
if fields and isinstance(fields, list):
_fields = list()
for field in fields:
attr = AttributeCache.get(field)
@@ -776,6 +833,10 @@ class CIManager(object):
ci_set.add(ci_id)
res[ci2pos[ci_id]] = ci_dict
if isinstance(fields, dict) and fields.get(type_id):
if attr_name not in fields[type_id]:
continue
if ret_key == RetKey.NAME:
attr_key = attr_name
elif ret_key == RetKey.ALIAS:
@@ -813,7 +874,7 @@ class CIManager(object):
if not ci_ids:
return []
fields = [] if fields is None or not isinstance(fields, list) else fields
fields = [] if not fields else fields
ci_id_tuple = tuple(map(int, ci_ids))
res = cls._get_cis_from_cache(ci_id_tuple, ret_key, fields, unique_required, excludes=excludes)
@@ -1242,7 +1303,9 @@ class CIRelationManager(object):
else:
type_relation = CITypeRelation.get_by_id(relation_type_id)
with redis_lock.Lock(rd.r, "ci_relation_add_{}_{}".format(first_ci.type_id, second_ci.type_id)):
with redis_lock.Lock(rd.r,
"ci_relation_add_{}_{}".format(first_ci.type_id, second_ci.type_id),
expire=10):
cls._check_constraint(first_ci_id, first_ci.type_id, second_ci_id, second_ci.type_id, type_relation)
@@ -1263,10 +1326,10 @@ class CIRelationManager(object):
return existed.id
@staticmethod
def delete(cr_id, apply_async=True):
def delete(cr_id, apply_async=True, valid=True):
cr = CIRelation.get_by_id(cr_id) or abort(404, ErrFormat.relation_not_found.format("id={}".format(cr_id)))
if current_app.config.get('USE_ACL') and current_user.username != 'worker':
if current_app.config.get('USE_ACL') and current_user.username != 'worker' and valid:
resource_name = CITypeRelationManager.acl_resource_name(cr.first_ci.ci_type.name, cr.second_ci.ci_type.name)
if not ACLManager().has_permission(
resource_name,
@@ -1305,7 +1368,7 @@ class CIRelationManager(object):
return cr
@classmethod
def delete_3(cls, first_ci_id, second_ci_id, apply_async=True):
def delete_3(cls, first_ci_id, second_ci_id, apply_async=True, valid=True):
cr = CIRelation.get_by(first_ci_id=first_ci_id,
second_ci_id=second_ci_id,
to_dict=False,
@@ -1315,7 +1378,7 @@ class CIRelationManager(object):
# ci_relation_delete.apply_async(args=(first_ci_id, second_ci_id, cr.ancestor_ids), queue=CMDB_QUEUE)
# delete_id_filter.apply_async(args=(second_ci_id,), queue=CMDB_QUEUE)
cls.delete(cr.id, apply_async=apply_async)
cls.delete(cr.id, apply_async=apply_async, valid=valid)
return cr
@@ -1387,14 +1450,31 @@ class CIRelationManager(object):
parent_attr = AttributeCache.get(parent_attr_id)
child_attr = AttributeCache.get(child_attr_id)
attr_value = ci_dict.get(parent_attr.name)
if attr_value != 0 and not attr_value:
continue
value_table = TableMap(attr=child_attr).table
for child in value_table.get_by(attr_id=child_attr.id, value=attr_value, only_query=True).join(
CI, CI.id == value_table.ci_id).filter(CI.type_id == item.child_id):
_relations.add((ci_dict['_id'], child.ci_id))
attr_value_list = [attr_value] if not isinstance(attr_value, list) else attr_value
matching_cis = value_table.get_by(
attr_id=child_attr.id,
only_query=True
).join(
CI, CI.id == value_table.ci_id
).filter(
CI.type_id == item.child_id,
value_table.value.in_(attr_value_list)
).all()
for ci in matching_cis:
_relations.add((ci_dict['_id'], ci.ci_id))
if relations is None:
relations = _relations
else:
relations &= _relations
if item.constraint == ConstraintEnum.Many2Many:
relations |= _relations
else:
relations &= _relations
cls.delete_relations_by_source(RelationSourceEnum.ATTRIBUTE_VALUES,
first_ci_id=ci_dict['_id'],
@@ -1414,14 +1494,31 @@ class CIRelationManager(object):
parent_attr = AttributeCache.get(parent_attr_id)
child_attr = AttributeCache.get(child_attr_id)
attr_value = ci_dict.get(child_attr.name)
if attr_value != 0 and not attr_value:
continue
value_table = TableMap(attr=parent_attr).table
for parent in value_table.get_by(attr_id=parent_attr.id, value=attr_value, only_query=True).join(
CI, CI.id == value_table.ci_id).filter(CI.type_id == item.parent_id):
_relations.add((parent.ci_id, ci_dict['_id']))
attr_value_list = [attr_value] if not isinstance(attr_value, list) else attr_value
matching_cis = value_table.get_by(
attr_id=parent_attr.id,
only_query=True
).join(
CI, CI.id == value_table.ci_id
).filter(
CI.type_id == item.parent_id,
value_table.value.in_(attr_value_list)
).all()
for ci in matching_cis:
_relations.add((ci.ci_id, ci_dict['_id']))
if relations is None:
relations = _relations
else:
relations &= _relations
if item.constraint == ConstraintEnum.Many2Many:
relations |= _relations
else:
relations &= _relations
cls.delete_relations_by_source(RelationSourceEnum.ATTRIBUTE_VALUES,
second_ci_id=ci_dict['_id'],
@@ -1508,7 +1605,8 @@ class CITriggerManager(object):
ci_dict.update(attr_dict)
@classmethod
def _exec_webhook(cls, operate_type, webhook, ci_dict, trigger_id, trigger_name, record_id, ci_id=None, app=None):
def _exec_webhook(cls, operate_type, webhook, ci_dict, trigger_id, trigger_name, record_id,
ci_id=None, app=None, record_history=True):
app = app or current_app
with app.app_context():
@@ -1526,19 +1624,20 @@ class CITriggerManager(object):
current_app.logger.warning("exec webhook failed: {}".format(e))
response = e
is_ok = False
CITriggerHistoryManager.add(operate_type,
record_id,
ci_dict.get('_id'),
trigger_id,
trigger_name,
is_ok=is_ok,
webhook=response)
if record_history:
CITriggerHistoryManager.add(operate_type,
record_id,
ci_dict.get('_id'),
trigger_id,
trigger_name,
is_ok=is_ok,
webhook=response)
return is_ok
@classmethod
def _exec_notify(cls, operate_type, notify, ci_dict, trigger_id, trigger_name, record_id, ci_id=None, app=None):
def _exec_notify(cls, operate_type, notify, ci_dict, trigger_id, trigger_name, record_id,
ci_id=None, app=None, record_history=True):
app = app or current_app
with app.app_context():
@@ -1562,13 +1661,14 @@ class CITriggerManager(object):
response = "{}\n{}".format(response, e)
is_ok = False
CITriggerHistoryManager.add(operate_type,
record_id,
ci_dict.get('_id'),
trigger_id,
trigger_name,
is_ok=is_ok,
notify=response.strip())
if record_history:
CITriggerHistoryManager.add(operate_type,
record_id,
ci_dict.get('_id'),
trigger_id,
trigger_name,
is_ok=is_ok,
notify=response.strip())
return is_ok
@@ -1645,25 +1745,47 @@ class CITriggerManager(object):
return result
@classmethod
def trigger_notify(cls, trigger, ci):
def trigger_notify(cls, trigger, ci, only_test=False):
"""
only for date attribute
:param trigger:
:param ci:
:param only_test:
:return:
"""
if (trigger.option.get('notifies', {}).get('notify_at') == datetime.datetime.now().strftime("%H:%M") or
not trigger.option.get('notifies', {}).get('notify_at')):
not trigger.option.get('notifies', {}).get('notify_at')) or only_test:
if trigger.option.get('webhooks'):
threading.Thread(target=cls._exec_webhook, args=(
None, trigger.option['webhooks'], None, trigger.id, trigger.option.get('name'), None, ci.ci_id,
current_app._get_current_object())).start()
threading.Thread(
target=cls._exec_webhook,
args=(None, trigger.option['webhooks'], None, trigger.id,
trigger.option.get('name'), None,
ci and ci.ci_id,
current_app._get_current_object(), not only_test)).start()
elif trigger.option.get('notifies'):
threading.Thread(target=cls._exec_notify, args=(
None, trigger.option['notifies'], None, trigger.id, trigger.option.get('name'), None, ci.ci_id,
current_app._get_current_object())).start()
None, trigger.option['notifies'], None, trigger.id,
trigger.option.get('name'), None,
ci and ci.ci_id,
current_app._get_current_object(), not only_test)).start()
return True
return False
@classmethod
def trigger_notify_test(cls, type_id, trigger_id):
trigger = CITypeTrigger.get_by_id(trigger_id) or abort(
404, ErrFormat.ci_type_trigger_not_found.format(trigger_id))
ci_type = CITypeCache.get(type_id) or abort(404, ErrFormat.ci_type_not_found.format(type_id))
attr = AttributeCache.get(ci_type.unique_id)
value_table = TableMap(attr=attr).table
if not value_table:
return
value = value_table.get_by(attr_id=attr.id, only_query=True).join(
CI, value_table.ci_id == CI.id).filter(CI.type_id == type_id).first()
cls.trigger_notify(trigger, value, only_test=True)

View File

@@ -3,6 +3,7 @@
from collections import defaultdict
import copy
import networkx as nx
import toposort
from flask import abort
from flask import current_app
@@ -16,12 +17,14 @@ from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import CITypeAttributeCache
from api.lib.cmdb.cache import CITypeAttributesCache
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.const import BuiltinModelEnum
from api.lib.cmdb.const import CITypeOperateType
from api.lib.cmdb.const import CMDB_QUEUE
from api.lib.cmdb.const import ConstraintEnum
from api.lib.cmdb.const import PermEnum
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.const import ValueTypeEnum
from api.lib.cmdb.history import CITypeHistoryManager
from api.lib.cmdb.perms import CIFilterPermsCRUD
@@ -63,6 +66,7 @@ class CITypeManager(object):
"""
manage CIType
"""
cls = CIType
def __init__(self):
@@ -185,6 +189,9 @@ class CITypeManager(object):
ci_type = cls.check_is_existed(type_id)
if ci_type.name in BuiltinModelEnum.all() and kwargs.get('name', ci_type.name) != ci_type.name:
return abort(400, ErrFormat.builtin_type_cannot_update_name)
cls._validate_unique(type_id=type_id, name=kwargs.get('name'))
# cls._validate_unique(type_id=type_id, alias=kwargs.get('alias') or kwargs.get('name'))
@@ -414,7 +421,10 @@ class CITypeGroupManager(object):
group_types = set()
for group in groups:
for t in sorted(CITypeGroupItem.get_by(group_id=group['id']), key=lambda x: x['order'] or 0):
ci_type = CITypeCache.get(t['type_id']).to_dict()
ci_type = CITypeCache.get(t['type_id'])
if ci_type is None:
continue
ci_type = ci_type.to_dict()
if type_ids is not None and ci_type['id'] not in type_ids:
continue
if resources is None or (ci_type and ci_type['name'] in resources):
@@ -524,7 +534,14 @@ class CITypeAttributeManager(object):
for _type_id in parent_ids + [type_id]:
result.extend(CITypeAttributesCache.get2(_type_id))
return result
attr_ids = set()
result2 = []
for i in result:
if i[1].id not in attr_ids:
result2.append(i)
attr_ids.add(i[1].id)
return result2
@classmethod
def get_attr_names_by_type_id(cls, type_id):
@@ -705,9 +722,6 @@ class CITypeAttributeManager(object):
ci_cache.apply_async(args=(ci.id, None, None), queue=CMDB_QUEUE)
for item in PreferenceShowAttributes.get_by(type_id=type_id, attr_id=attr_id, to_dict=False):
item.soft_delete(commit=False)
child_ids = CITypeInheritanceManager.recursive_children(type_id)
for _type_id in [type_id] + child_ids:
for item in CITypeUniqueConstraint.get_by(type_id=_type_id, to_dict=False):
@@ -723,6 +737,9 @@ class CITypeAttributeManager(object):
item = CITypeTrigger.get_by(type_id=_type_id, attr_id=attr_id, to_dict=False, first=True)
item and item.soft_delete(commit=False)
for item in PreferenceShowAttributes.get_by(type_id=_type_id, attr_id=attr_id, to_dict=False):
item.soft_delete(commit=False)
for item in (CITypeRelation.get_by(parent_id=type_id, to_dict=False) +
CITypeRelation.get_by(child_id=type_id, to_dict=False)):
if item.parent_id == type_id and attr_id in (item.parent_attr_ids or []):
@@ -838,10 +855,35 @@ class CITypeRelationManager(object):
return ids
@staticmethod
def find_path(source_type_id, target_type_ids):
source_type_id = int(source_type_id)
target_type_ids = map(int, target_type_ids)
graph = nx.DiGraph()
def get_children(_id, _graph):
children = CITypeRelation.get_by(parent_id=_id, to_dict=False)
for i in children:
if i.child_id != _id:
_graph.add_edge(i.parent_id, i.child_id)
get_children(i.child_id, _graph)
get_children(source_type_id, graph)
paths = list(nx.all_simple_paths(graph, source_type_id, target_type_ids))
del graph
return paths
@staticmethod
def _wrap_relation_type_dict(type_id, relation_inst):
ci_type_dict = CITypeCache.get(type_id).to_dict()
ci_type_dict["ctr_id"] = relation_inst.id
show_key = AttributeCache.get(ci_type_dict.get('show_id') or ci_type_dict['unique_id'])
ci_type_dict["show_key"] = show_key and show_key.name
ci_type_dict["attributes"] = CITypeAttributeManager.get_attributes_by_type_id(ci_type_dict["id"])
attr_filter = CIFilterPermsCRUD.get_attr_filter(type_id)
if attr_filter:
@@ -1064,6 +1106,7 @@ class CITypeAttributeGroupManager(object):
@staticmethod
def get_by_type_id(type_id, need_other=False):
_type = CITypeCache.get(type_id) or abort(404, ErrFormat.ci_type_not_found)
parent_ids = CITypeInheritanceManager.base(type_id)
groups = []
@@ -1102,17 +1145,24 @@ class CITypeAttributeGroupManager(object):
else:
group_pos = group2pos[group['name']]
attr = None
for i in items:
if i.attr_id in id2attr:
attr = id2attr[i.attr_id]
attr['inherited'] = group['inherited']
attr['inherited_from'] = group.get('inherited_from')
result[group_pos]['attributes'].append(attr)
else:
continue
if i.attr_id in attr2pos:
result[attr2pos[i.attr_id][0]]['attributes'].remove(attr2pos[i.attr_id][1])
if (_type.name in SysComputedAttributes.type2attr and
attr['name'] in SysComputedAttributes.type2attr[_type.name]):
attr['sys_computed'] = True
else:
attr['sys_computed'] = False
attr2pos[i.attr_id] = [group_pos, attr]
group.pop('inherited_from', None)
@@ -1349,6 +1399,7 @@ class CITypeTemplateManager(object):
i.pop('order', None)
i.pop('choice_web_hook', None)
i.pop('choice_other', None)
i.pop('choice_builtin', None)
i.pop('order', None)
i.pop('inherited_from', None)
choice_value = i.pop('choice_value', None)
@@ -1399,6 +1450,10 @@ class CITypeTemplateManager(object):
payload = dict(group_id=group_id_map.get(group['id'], group['id']),
type_id=type_id_map.get(ci_type['id'], ci_type['id']),
order=order)
for i in CITypeGroupItem.get_by(type_id=payload['type_id'], to_dict=False):
if i.group_id != payload['group_id']:
i.soft_delete(flush=True)
existed = CITypeGroupItem.get_by(group_id=payload['group_id'], type_id=payload['type_id'],
first=True, to_dict=False)
if existed is None:
@@ -1503,7 +1558,10 @@ class CITypeTemplateManager(object):
if existed is None:
_group['type_id'] = type_id_map.get(_group['type_id'], _group['type_id'])
existed = CITypeAttributeGroup.create(flush=True, **_group)
try:
existed = CITypeAttributeGroup.create(flush=True, **_group)
except:
continue
for order, attr in enumerate(group['attributes'] or []):
item_existed = CITypeAttributeGroupItem.get_by(group_id=existed.id,

View File

@@ -1,6 +1,8 @@
# -*- coding:utf-8 -*-
from flask_babel import lazy_gettext as _l
from api.lib.utils import BaseEnum
@@ -110,17 +112,47 @@ class ExecuteStatusEnum(BaseEnum):
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_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
}
}
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,6 +10,7 @@ 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
@@ -22,6 +23,7 @@ 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):
@@ -59,8 +61,23 @@ 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']:
@@ -76,6 +93,7 @@ class AttributeHistoryManger(object):
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

View File

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

View File

@@ -0,0 +1,133 @@
# -*- 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=60, auto_renewal=True)):
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,7 +1,6 @@
# -*- coding:utf-8 -*-
import copy
import functools
import redis_lock
from flask import abort
from flask import current_app
@@ -10,6 +9,7 @@ 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
@@ -27,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
@@ -62,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
@@ -163,7 +163,7 @@ 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'])):
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'),
@@ -232,7 +232,7 @@ class CIFilterPermsCRUD(DBMixin):
pass
def delete(self, **kwargs):
with redis_lock.Lock(rd.r, 'CMDB_FILTER_{}_{}'.format(kwargs['type_id'], kwargs['rid'])):
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,
@@ -249,7 +249,7 @@ class CIFilterPermsCRUD(DBMixin):
def delete2(self, **kwargs):
with redis_lock.Lock(rd.r, 'CMDB_FILTER_{}_{}'.format(kwargs['type_id'], kwargs['rid'])):
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,

View File

@@ -1,10 +1,8 @@
# -*- coding:utf-8 -*-
import copy
from collections import defaultdict
import copy
import six
import toposort
from flask import abort
from flask import current_app
@@ -17,18 +15,21 @@ from api.lib.cmdb.cache import CITypeAttributesCache
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.cache import CMDBCounterCache
from api.lib.cmdb.ci_type import CITypeAttributeManager
from api.lib.cmdb.ci_type import CITypeManager
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 PreferenceAutoSubscriptionConfig
from api.models.cmdb import PreferenceCITypeOrder
from api.models.cmdb import PreferenceRelationView
from api.models.cmdb import PreferenceSearchOption
@@ -52,12 +53,24 @@ class PreferenceManager(object):
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))
if instance:
auto_types = PreferenceManager.get_auto_subscription_types(current_user.uid)
if auto_types is not None:
class TypeIdObj:
def __init__(self, type_id):
self.type_id = type_id
types = [TypeIdObj(t) for t in auto_types]
else:
types = db.session.query(PreferenceShowAttributes.type_id).filter(
PreferenceShowAttributes.uid == current_user.uid).filter(
PreferenceShowAttributes.deleted.is_(False)).group_by(
PreferenceShowAttributes.type_id).all()
else:
types = []
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 = {}
@@ -104,19 +117,26 @@ class PreferenceManager(object):
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)).filter(
PreferenceShowAttributes.uid == current_user.uid).group_by(
PreferenceShowAttributes.uid, PreferenceShowAttributes.type_id)
for i in types:
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
# Try auto subscription first, fallback to manual if not configured
auto_types = PreferenceManager.get_auto_subscription_types(current_user.uid)
if auto_types is not None:
result['self']['instance'] = auto_types
for type_id in auto_types:
result['self']['type_id2subs_time'][type_id] = ""
else:
types = db.session.query(PreferenceShowAttributes.type_id,
PreferenceShowAttributes.uid, PreferenceShowAttributes.created_at).filter(
PreferenceShowAttributes.deleted.is_(False)).filter(
PreferenceShowAttributes.uid == current_user.uid).group_by(
PreferenceShowAttributes.uid, PreferenceShowAttributes.type_id)
for i in types:
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
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
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(uid=current_user.uid, to_dict=False)
@@ -132,22 +152,83 @@ class PreferenceManager(object):
return result
@staticmethod
def get_show_attributes(type_id):
if not isinstance(type_id, six.integer_types):
_type = CITypeCache.get(type_id)
type_id = _type and _type.id
def get_auto_subscription_types(uid):
"""Get user's auto-subscribed CI types based on config rules"""
config = PreferenceAutoSubscriptionConfig.get_by(
uid=uid, enabled=True, first=True, to_dict=False
)
attrs = db.session.query(PreferenceShowAttributes, CITypeAttribute.order).join(
CITypeAttribute, CITypeAttribute.attr_id == PreferenceShowAttributes.attr_id).filter(
PreferenceShowAttributes.uid == current_user.uid).filter(
PreferenceShowAttributes.type_id == type_id).filter(
PreferenceShowAttributes.deleted.is_(False)).filter(CITypeAttribute.deleted.is_(False)).group_by(
CITypeAttribute.attr_id).all()
if not config:
return None
all_permitted_types = PreferenceManager._get_permitted_ci_types()
result_types = PreferenceManager._apply_subscription_config(config, all_permitted_types)
return result_types
@staticmethod
def _get_permitted_ci_types():
"""Get CI types that user has read permission for"""
from api.lib.perm.acl.acl import is_app_admin
if not current_app.config.get('USE_ACL') or is_app_admin('cmdb'):
return [t['id'] for t in CITypeManager.get_ci_types()]
# Regular user: filter by permissions
permitted_resources = ACLManager('cmdb').get_resources(ResourceTypeEnum.CI)
permitted_names = {r.get('name') for r in permitted_resources}
return [ci_type_dict['id'] for ci_type_dict in CITypeManager.get_ci_types()
if ci_type_dict['name'] in permitted_names]
@staticmethod
def _apply_subscription_config(config, all_permitted_types):
"""Apply subscription rules: 'all' mode excludes, 'none' mode includes"""
result_types = set()
if config.base_strategy == 'all':
# Start with all types, then exclude
result_types = set(all_permitted_types)
if config.group_ids:
exclude_group_type_ids = PreferenceManager._get_types_by_group_ids(config.group_ids)
result_types.difference_update(exclude_group_type_ids)
if config.type_ids:
result_types.difference_update(config.type_ids)
else: # base_strategy == 'none'
# Start empty, then include
if config.group_ids:
include_group_type_ids = PreferenceManager._get_types_by_group_ids(config.group_ids)
result_types.update(t for t in include_group_type_ids if t in all_permitted_types)
if config.type_ids:
result_types.update(t for t in config.type_ids if t in all_permitted_types)
return list(result_types)
@staticmethod
def _get_types_by_group_ids(group_ids):
return [i.type_id for i in CITypeGroupItem.get_by(
__func_in___key_group_id=group_ids, to_dict=False, fl=['type_id'])]
@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
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
@@ -156,13 +237,23 @@ class PreferenceManager(object):
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.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
@classmethod
@@ -173,24 +264,34 @@ 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:
@@ -384,14 +485,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)
@@ -492,3 +601,54 @@ class PreferenceManager(object):
db.session.rollback()
current_app.logger.error("upsert citype order failed: {}".format(e))
return abort(400, ErrFormat.unknown_error)
@staticmethod
def get_auto_subscription_config():
"""Get user's auto subscription configuration"""
config = PreferenceAutoSubscriptionConfig.get_by(
uid=current_user.uid, first=True, to_dict=True
)
return config
@staticmethod
def create_or_update_auto_subscription_config(base_strategy, group_ids=None, type_ids=None,
enabled=True, description=None):
"""Create or update user's auto subscription config"""
config = PreferenceAutoSubscriptionConfig.get_by(
uid=current_user.uid, first=True, to_dict=False
)
data = {
'base_strategy': base_strategy,
'group_ids': group_ids or [],
'type_ids': type_ids or [],
'enabled': enabled,
'description': description
}
if config:
return config.update(**data)
else:
data['uid'] = current_user.uid
return PreferenceAutoSubscriptionConfig.create(**data)
@staticmethod
def delete_auto_subscription_config():
"""Delete user's auto subscription configuration"""
config = PreferenceAutoSubscriptionConfig.get_by(
uid=current_user.uid, first=True, to_dict=False
)
if config:
config.soft_delete()
return True
@staticmethod
def toggle_auto_subscription_config(enabled):
"""Enable or disable user's auto subscription config"""
config = PreferenceAutoSubscriptionConfig.get_by(
uid=current_user.uid, first=True, to_dict=False
)
if not config:
return abort(404, "Auto subscription config not found")
return config.update(enabled=enabled)

View File

@@ -16,8 +16,9 @@ class ErrFormat(CommonErrFormat):
argument_file_not_found = _l("The file doesn't seem to be uploaded") # 文件似乎并未上传
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!") # 该属性是模型的唯一标识,不能被删除!
"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(
@@ -129,7 +130,8 @@ class ErrFormat(CommonErrFormat):
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!")
adr_plugin_attributes_list_required = _l("The attributes method must return a list") # attributes方法必须返回的是list
# 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!")
# 只有管理员才可以定义执行机器为: 所有节点!
@@ -154,3 +156,23 @@ class ErrFormat(CommonErrFormat):
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

@@ -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,8 +4,9 @@
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
@@ -15,6 +16,7 @@ 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
@@ -26,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
@@ -66,6 +69,7 @@ class Search(object):
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()
@@ -104,35 +108,58 @@ 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 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:
@@ -146,13 +173,17 @@ class Search(object):
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
@@ -229,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(
@@ -254,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(
@@ -278,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))
@@ -325,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)
@@ -397,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))
@@ -430,14 +473,14 @@ class Search(object):
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)
@@ -484,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)
@@ -547,7 +600,6 @@ 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)
@@ -585,13 +637,16 @@ 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()
@@ -610,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

View File

@@ -1,8 +1,11 @@
# -*- coding:utf-8 -*-
import json
import sys
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
@@ -13,6 +16,7 @@ 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
@@ -25,10 +29,12 @@ 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,
@@ -58,7 +64,7 @@ class Search(object):
self.ancestor_ids = ancestor_ids
self.descendant_ids = descendant_ids
self.root_parent_path = root_parent_path
self.root_parent_path = root_parent_path or []
self.has_m2m = has_m2m or False
if not self.has_m2m:
if self.ancestor_ids:
@@ -385,9 +391,10 @@ class Search(object):
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_ids[lv]):
id_filter_limit, _ = self._get_ci_filter(type2filter_perms[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 = {}
@@ -395,12 +402,12 @@ class Search(object):
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 (not id_filter_limit or (key[idx] not in id_filter_limit 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 = []
type_id = type_ids[lv]
id2name = _get_id2name(type_id)
for idx, node_path in enumerate(level_ids):
for child_id, _ in (res[idx] or []):
@@ -419,3 +426,169 @@ class Search(object):
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

@@ -55,7 +55,6 @@ def str2datetime(x):
return datetime.datetime.strptime(x, "%Y-%m-%d %H:%M")
class ValueTypeMap(object):
deserialize = {
ValueTypeEnum.INT: string2int,

View File

@@ -3,7 +3,7 @@
from __future__ import unicode_literals
import imp
import importlib.util
import copy
import jinja2
@@ -97,6 +97,8 @@ class AttributeValueManager(object):
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))
@@ -134,7 +136,7 @@ class AttributeValueManager(object):
if not re.compile(expr).match(str(value)):
return abort(400, ErrFormat.attribute_value_invalid2.format(alias, value))
def _validate(self, attr, value, value_table, ci=None, type_id=None, ci_id=None, type_attr=None):
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)
@@ -144,7 +146,7 @@ class AttributeValueManager(object):
else:
v = value or None
attr.is_unique and self._check_is_unique(
(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:
@@ -178,14 +180,15 @@ class AttributeValueManager(object):
@staticmethod
def _compute_attr_value_from_expr(expr, ci_dict):
t = jinja2.Template(expr).render(ci_dict)
try:
return eval(t)
result = jinja2.Template(expr).render(ci_dict)
return result
except Exception as e:
current_app.logger.warning(str(e))
return t
current_app.logger.warning(
f"Expression evaluation error - Expression: '{expr}'"
f"Input parameters: {ci_dict}, Error type: {type(e).__name__}, Error message: {str(e)}"
)
return None
@staticmethod
def _compute_attr_value_from_script(script, ci_dict):
script = jinja2.Template(script).render(ci_dict)
@@ -196,11 +199,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()
@@ -235,7 +238,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 {}
@@ -266,7 +272,8 @@ class AttributeValueManager(object):
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

View File

@@ -1,6 +1,6 @@
import functools
from flask import abort, session
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
@@ -15,6 +15,7 @@ def perms_role_required(app_name, resource_type_name, resource_name, perm, role_
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):

View File

@@ -476,7 +476,7 @@ class EditDepartmentInACL(object):
for employee in e_list:
employee_acl_rid = employee.get('e_acl_rid')
if employee_acl_rid == 0:
result.append(f"employee_acl_rid == 0")
result.append("employee_acl_rid == 0")
continue
cls.remove_single_employee_from_old_department(acl, employee, result)
@@ -501,8 +501,8 @@ class EditDepartmentInACL(object):
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:
result.append(
f"remove_user_from_role employee_acl_rid: {employee.get('e_acl_rid')}, parent_id: {d_acl_rid}, err: {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
@@ -548,7 +548,7 @@ class EditDepartmentInACL(object):
for employee in e_list:
employee_acl_rid = employee.get('e_acl_rid')
if employee_acl_rid == 0:
result.append(f"employee_acl_rid == 0")
result.append("employee_acl_rid == 0")
continue
cls.remove_single_employee_from_old_department(acl, employee, 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
@@ -478,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:
@@ -702,6 +702,7 @@ class EmployeeCRUD(object):
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()
@@ -712,6 +713,7 @@ class EmployeeCRUD(object):
)
return last_login
except Exception as e:
current_app.logger.error(f"update last_login err: {e}")
return

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

View File

@@ -48,11 +48,15 @@ class CMDBApp(BaseApp):
{"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": "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):

View File

@@ -1,6 +1,7 @@
# -*- coding:utf-8 -*-
from flask import current_app
from sqlalchemy import func
from api.extensions import db
@@ -32,11 +33,21 @@ class DBMixin(object):
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:

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

@@ -376,7 +376,7 @@ class AuditCRUD(object):
origin=origin, current=current, extra=extra, source=source.value)
@classmethod
def add_login_log(cls, username, is_ok, description, _id=None, logout_at=None):
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:
@@ -387,8 +387,9 @@ class AuditCRUD(object):
is_ok=is_ok,
description=description,
logout_at=logout_at,
ip=request.headers.get('X-Real-IP') or request.remote_addr,
browser=request.headers.get('User-Agent'),
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'),
)

View File

@@ -138,14 +138,14 @@ class HasResourceRoleCache(object):
@classmethod
def add(cls, rid, app_id):
with redis_lock.Lock(rd.r, '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 redis_lock.Lock(rd.r, '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)

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)

View File

@@ -1,8 +1,5 @@
# -*- coding:utf-8 -*-
import time
import redis_lock
import six
from flask import abort
@@ -145,7 +142,7 @@ class RoleRelationCRUD(object):
@classmethod
def add(cls, role, parent_id, child_ids, app_id):
with redis_lock.Lock(rd.r, "ROLE_RELATION_ADD"):
with redis_lock.Lock(rd.r, "ROLE_RELATION_ADD", expire=10):
db.session.commit()
result = []

View File

@@ -1,7 +1,6 @@
# -*- coding:utf-8 -*-
import base64
from typing import Set
import elasticsearch
import redis

View File

@@ -105,8 +105,8 @@ 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))

View File

@@ -253,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")
@@ -475,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)
@@ -523,6 +525,17 @@ class PreferenceCITypeOrder(Model):
is_tree = db.Column(db.Boolean, default=False) # True is tree view, False is resource view
class PreferenceAutoSubscriptionConfig(Model):
__tablename__ = "c_pasc"
uid = db.Column(db.Integer, index=True, nullable=False, unique=True)
base_strategy = db.Column(db.Enum('all', 'none'), default='none', nullable=False)
group_ids = db.Column(db.JSON)
type_ids = db.Column(db.JSON)
enabled = db.Column(db.Boolean, default=True, nullable=False)
description = db.Column(db.Text)
# custom
class CustomDashboard(Model):
__tablename__ = "c_c_d"
@@ -534,6 +547,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)
@@ -666,3 +680,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

@@ -32,7 +32,7 @@ from api.models.acl import Trigger
def role_rebuild(rids, app_id):
rids = rids if isinstance(rids, list) else [rids]
for rid in rids:
with redis_lock.Lock(rd.r, "ROLE_REBUILD_{}_{}".format(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))

View File

@@ -1,10 +1,11 @@
# -*- coding:utf-8 -*-
# -*- coding:utf-8 -*-
import datetime
import json
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
@@ -53,8 +54,9 @@ 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'))
_, enum_map = CITypeAttributeManager.get_attr_names_label_enum(ci_dict.get('_type'))
payload = dict()
@@ -143,7 +145,7 @@ def ci_delete_trigger(trigger, operate_type, ci_dict):
@flush_db
@reconnect_db
def ci_relation_cache(parent_id, child_id, ancestor_ids):
with redis_lock.Lock(rd.r, "CIRelation_{}".format(parent_id)):
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 {}
@@ -184,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)
@@ -220,7 +223,7 @@ def ci_relation_add(parent_dict, child_id, uid):
@celery.task(name="cmdb.ci_relation_delete", queue=CMDB_QUEUE)
@reconnect_db
def ci_relation_delete(parent_id, child_id, ancestor_ids):
with redis_lock.Lock(rd.r, "CIRelation_{}".format(parent_id)):
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 {}
@@ -272,8 +275,9 @@ 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):
@@ -370,3 +374,48 @@ def build_relations_for_ad_accept(adc, ci_id, ad_key2attr):
source=RelationSourceEnum.AUTO_DISCOVERY)
except:
pass
@celery.task(name="cmdb.add_net_device_ports", queue=CMDB_QUEUE)
@reconnect_db
def add_net_device_ports(ci_id, ports):
from api.lib.cmdb.ci import CIRelationManager
from api.lib.cmdb.ci import CIManager
from api.lib.cmdb.cache import CITypeCache
port_type = CITypeCache.get("net_port")
if not port_type:
current_app.logger.warning("CIType net port is not found")
return
for port in ports:
try:
port_id = CIManager.add(port_type.id, is_auto_discovery=True, _is_admin=True, **port)
CIRelationManager.add(ci_id, port_id,
valid=False,
source=RelationSourceEnum.AUTO_DISCOVERY)
except Exception as e:
current_app.logger.warning("add_net_device_ports failed: {}".format(e))
@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

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2024-08-20 13:47+0800\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"
@@ -92,6 +92,14 @@ msgstr "您没有操作权限!"
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 "模型配置"
@@ -474,11 +482,11 @@ msgstr "{}格式错误,应该为:%Y-%m-%d %H:%M:%S"
#: api/lib/cmdb/resp_format.py:150
msgid "CMDB data reconciliation results"
msgstr ""
msgstr "CMDB数据合规检查结果"
#: api/lib/cmdb/resp_format.py:151
msgid "Number of {} illegal: {}"
msgstr ""
msgstr "{} 不合规数: {}"
#: api/lib/cmdb/resp_format.py:153
msgid "Topology view {} already exists"
@@ -492,6 +500,62 @@ msgstr "拓扑视图分组 {} 已经存在"
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 "公司信息已存在,无法创建!"

View File

@@ -1,7 +1,6 @@
# -*- coding:utf-8 -*-
import datetime
import jwt
import six
from flask import abort
@@ -17,10 +16,12 @@ 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
@@ -124,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):
@@ -184,6 +192,8 @@ class LogoutView(APIView):
def post(self):
logout_user()
AuditCRUD.add_login_log(None, None, None, _id=session.get('LOGIN_ID'), logout_at=datetime.datetime.now())
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)

View File

@@ -11,6 +11,7 @@ from flask_login import current_user
from api.lib.decorator import args_required
from api.lib.decorator import args_validate
from api.lib.perm.acl.acl import ACLManager
from api.lib.perm.acl.acl import AuditCRUD
from api.lib.perm.acl.acl import role_required
from api.lib.perm.acl.cache import AppCache
from api.lib.perm.acl.cache import UserCache
@@ -48,6 +49,13 @@ class GetUserInfoView(APIView):
role=dict(permissions=user_info.get('parents')),
avatar=user_info.get('avatar'))
if request.values.get('channel'):
_id = AuditCRUD.add_login_log(name, True, ErrFormat.login_succeed,
ip=request.values.get('ip'),
browser=request.values.get('browser'))
session['LOGIN_ID'] = _id
result['LOGIN_ID'] = _id
current_app.logger.info("get user info for3: {}".format(result))
return self.jsonify(result=result)

View File

@@ -24,11 +24,13 @@ from api.lib.cmdb.auto_discovery.const import PRIVILEGED_USERS
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.ipam.subnet import SubnetManager
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.decorator import args_required
from api.lib.decorator import args_validate
from api.lib.exception import AbortException
from api.lib.perm.acl.acl import has_perm_from_args
from api.lib.utils import AESCrypto
from api.lib.utils import get_page
@@ -293,9 +295,16 @@ class AutoDiscoveryRuleSyncView(APIView):
return self.jsonify(rules=rules, last_update_at=last_update_at)
rules, last_update_at = AutoDiscoveryCITypeCRUD.get(None, oneagent_id, oneagent_name, last_update_at)
rules, last_update_at1 = AutoDiscoveryCITypeCRUD.get(None, oneagent_id, oneagent_name, last_update_at)
return self.jsonify(rules=rules, last_update_at=last_update_at)
try:
subnet_scan_rules, last_update_at2 = SubnetManager().scan_rules(oneagent_id, last_update_at)
except AbortException:
subnet_scan_rules, last_update_at2 = [], ""
return self.jsonify(rules=rules,
subnet_scan_rules=subnet_scan_rules,
last_update_at=max(last_update_at1 or "", last_update_at2 or ""))
class AutoDiscoveryRuleSyncHistoryView(APIView):

View File

@@ -2,7 +2,6 @@
import time
from flask import abort
from flask import current_app
from flask import request
@@ -65,6 +64,42 @@ class CIRelationSearchView(APIView):
result=response)
class CIRelationSearchPathView(APIView):
url_prefix = ("/ci_relations/path/s", "/ci_relations/path/search")
@args_required("source", "target", "path")
def post(self):
"""@params: page: page number
page_size | count: page size
source: source CIType, e.g. {type_id: 1, q: `search expr`}
target: target CIType, e.g. {type_ids: [2], q: `search expr`}
path: Path from the Source CIType to the Target CIType, e.g. [1, ..., 2]
"""
page = get_page(request.values.get("page", 1))
count = get_page_size(request.values.get("count") or request.values.get("page_size"))
source = request.values.get("source")
target = request.values.get("target")
path = request.values.get("path")
s = Search(page=page, count=count)
try:
(response, counter, total, page, numfound, id2ci,
relation_types, type2show_key) = s.search_by_path(source, target, path)
except SearchError as e:
return abort(400, str(e))
return self.jsonify(numfound=numfound,
total=total,
page=page,
counter=counter,
paths=response,
id2ci=id2ci,
relation_types=relation_types,
type2show_key=type2show_key)
class CIRelationStatisticsView(APIView):
url_prefix = "/ci_relations/statistics"

View File

@@ -2,14 +2,14 @@
import json
from io import BytesIO
from flask import abort
from flask import current_app
from flask import request
from io import BytesIO
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.ci import CITriggerManager
from api.lib.cmdb.ci_type import CITypeAttributeGroupManager
from api.lib.cmdb.ci_type import CITypeAttributeManager
from api.lib.cmdb.ci_type import CITypeGroupManager
@@ -64,9 +64,13 @@ class CITypeView(APIView):
ci_type['unique_name'] = ci_type['unique_id'] and AttributeCache.get(ci_type['unique_id']).name
ci_types.append(ci_type)
elif type_name is not None:
ci_type = CITypeCache.get(type_name).to_dict()
ci_type['parent_ids'] = CITypeInheritanceManager.get_parents(ci_type['id'])
ci_types = [ci_type]
ci_type = CITypeCache.get(type_name)
if ci_type is not None:
ci_type = ci_type.to_dict()
ci_type['parent_ids'] = CITypeInheritanceManager.get_parents(ci_type['id'])
ci_types = [ci_type]
else:
ci_types = []
else:
ci_types = CITypeManager().get_ci_types(q)
count = len(ci_types)
@@ -497,6 +501,16 @@ class CITypeTriggerView(APIView):
return self.jsonify(code=200)
class CITypeTriggerTestView(APIView):
url_prefix = ("/ci_types/<int:type_id>/triggers/<int:_id>/test_notify",)
@has_perm_from_args("type_id", ResourceTypeEnum.CI, PermEnum.CONFIG, CITypeManager.get_name_by_id)
def post(self, type_id, _id):
CITriggerManager().trigger_notify_test(type_id, _id)
return self.jsonify(code=200)
class CITypeGrantView(APIView):
url_prefix = "/ci_types/<int:type_id>/roles/<int:rid>/grant"

View File

@@ -8,7 +8,6 @@ from api.lib.cmdb.ci_type import CITypeManager
from api.lib.cmdb.ci_type import CITypeRelationManager
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.preference import PreferenceManager
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.common_setting.decorator import perms_role_required
@@ -17,7 +16,7 @@ from api.lib.decorator import args_required
from api.lib.perm.acl.acl import ACLManager
from api.lib.perm.acl.acl import has_perm_from_args
from api.lib.perm.acl.acl import is_app_admin
from api.lib.perm.acl.acl import role_required
from api.lib.utils import handle_arg_list
from api.resource import APIView
app_cli = CMDBApp()
@@ -42,6 +41,19 @@ class GetParentsView(APIView):
return self.jsonify(parents=CITypeRelationManager.get_parents(child_id))
class CITypeRelationPathView(APIView):
url_prefix = ("/ci_type_relations/path",)
@args_required("source_type_id", "target_type_ids")
def get(self):
source_type_id = request.values.get("source_type_id")
target_type_ids = handle_arg_list(request.values.get("target_type_ids"))
paths = CITypeRelationManager.find_path(source_type_id, target_type_ids)
return self.jsonify(paths=paths)
class CITypeRelationView(APIView):
url_prefix = ("/ci_type_relations", "/ci_type_relations/<int:parent_id>/<int:child_id>")

View File

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

View File

@@ -0,0 +1,30 @@
# -*- coding:utf-8 -*-
from flask import request
from api.lib.cmdb.dcim.history import OperateHistoryManager
from api.lib.common_setting.decorator import perms_role_required
from api.lib.common_setting.role_perm_base import CMDBApp
from api.lib.utils import get_page
from api.lib.utils import get_page_size
from api.lib.utils import handle_arg_list
from api.resource import APIView
app_cli = CMDBApp()
class DCIMOperateHistoryView(APIView):
url_prefix = ("/dcim/history/operate",)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
app_cli.op.read, app_cli.admin_name)
def get(self):
page = get_page(request.values.pop("page", 1))
page_size = get_page_size(request.values.pop("page_size", None))
operate_type = handle_arg_list(request.values.pop('operate_type', []))
if operate_type:
request.values["operate_type"] = operate_type
numfound, result, id2ci, type2show_key = OperateHistoryManager.search(page, page_size, **request.values)
return self.jsonify(numfound=numfound, result=result, id2ci=id2ci, type2show_key=type2show_key)

View File

@@ -0,0 +1,35 @@
# -*- coding:utf-8 -*-
from flask import request
from api.lib.cmdb.dcim.idc import IDCManager
from api.lib.common_setting.decorator import perms_role_required
from api.lib.common_setting.role_perm_base import CMDBApp
from api.resource import APIView
app_cli = CMDBApp()
class IDCView(APIView):
url_prefix = ("/dcim/idc", "/dcim/idc/<int:_id>")
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
app_cli.op.read, app_cli.admin_name)
def post(self):
parent_id = request.values.pop("parent_id")
return self.jsonify(ci_id=IDCManager().add(parent_id, **request.values))
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
app_cli.op.read, app_cli.admin_name)
def put(self, _id):
IDCManager().update(_id, **request.values)
return self.jsonify(ci_id=_id)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
app_cli.op.read, app_cli.admin_name)
def delete(self, _id):
IDCManager().delete(_id)
return self.jsonify(ci_id=_id)

View File

@@ -0,0 +1,102 @@
# -*- coding:utf-8 -*-
from flask import request
from api.lib.cmdb.const import CMDB_QUEUE
from api.lib.cmdb.dcim.const import RackBuiltinAttributes
from api.lib.cmdb.dcim.rack import RackManager
from api.lib.common_setting.decorator import perms_role_required
from api.lib.common_setting.role_perm_base import CMDBApp
from api.lib.decorator import args_required
from api.resource import APIView
from api.tasks.cmdb import dcim_calc_u_free_count
app_cli = CMDBApp()
class RackView(APIView):
url_prefix = ("/dcim/rack", "/dcim/rack/<int:_id>")
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
app_cli.op.read, app_cli.admin_name)
@args_required("parent_id")
def post(self):
parent_id = request.values.pop("parent_id")
return self.jsonify(ci_id=RackManager().add(parent_id, **request.values))
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
app_cli.op.read, app_cli.admin_name)
def put(self, _id):
RackManager().update(_id, **request.values)
return self.jsonify(ci_id=_id)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
app_cli.op.read, app_cli.admin_name)
def delete(self, _id):
RackManager().delete(_id)
return self.jsonify(ci_id=_id)
class RackDetailView(APIView):
url_prefix = ("/dcim/rack/<int:rack_id>/device/<int:device_id>",)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
app_cli.op.read, app_cli.admin_name)
@args_required(RackBuiltinAttributes.U_START)
def post(self, rack_id, device_id):
u_start = request.values.pop(RackBuiltinAttributes.U_START)
u_count = request.values.get(RackBuiltinAttributes.U_COUNT)
RackManager().add_device(rack_id, device_id, u_start, u_count)
return self.jsonify(rack_id=rack_id, device_id=device_id)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
app_cli.op.read, app_cli.admin_name)
@args_required("to_u_start")
def put(self, rack_id, device_id):
to_u_start = request.values.pop("to_u_start")
RackManager().move_device(rack_id, device_id, to_u_start)
return self.jsonify(rack_id=rack_id, device_id=device_id, to_u_start=to_u_start)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
app_cli.op.read, app_cli.admin_name)
def delete(self, rack_id, device_id):
RackManager().remove_device(rack_id, device_id)
return self.jsonify(code=200)
class RackDeviceMigrateView(APIView):
url_prefix = ("/dcim/rack/<int:rack_id>/device/<int:device_id>/migrate",)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
app_cli.op.read, app_cli.admin_name)
@args_required("to_rack_id")
@args_required("to_u_start")
def put(self, rack_id, device_id):
to_rack_id = request.values.pop("to_rack_id")
to_u_start = request.values.pop("to_u_start")
RackManager().migrate_device(rack_id, device_id, to_rack_id, to_u_start)
return self.jsonify(rack_id=rack_id,
device_id=device_id,
to_u_start=to_u_start,
to_rack_id=to_rack_id)
class RackCalcUFreeCountView(APIView):
url_prefix = ("/dcim/rack/calc_u_free_count",)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
app_cli.op.read, app_cli.admin_name)
def post(self):
dcim_calc_u_free_count.apply_async(queue=CMDB_QUEUE)
return self.jsonify(code=200)

View File

@@ -0,0 +1,33 @@
# -*- coding:utf-8 -*-
from flask import request
from api.lib.cmdb.dcim.region import RegionManager
from api.lib.common_setting.decorator import perms_role_required
from api.lib.common_setting.role_perm_base import CMDBApp
from api.resource import APIView
app_cli = CMDBApp()
class RegionView(APIView):
url_prefix = ("/dcim/region", "/dcim/region/<int:_id>")
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
app_cli.op.read, app_cli.admin_name)
def post(self):
return self.jsonify(ci_id=RegionManager().add(**request.values))
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
app_cli.op.read, app_cli.admin_name)
def put(self, _id):
RegionManager().update(_id, **request.values)
return self.jsonify(ci_id=_id)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
app_cli.op.read, app_cli.admin_name)
def delete(self, _id):
RegionManager().delete(_id)
return self.jsonify(ci_id=_id)

View File

@@ -0,0 +1,43 @@
# -*- coding:utf-8 -*-
from flask import request
from api.lib.cmdb.dcim.server_room import ServerRoomManager
from api.lib.common_setting.decorator import perms_role_required
from api.lib.common_setting.role_perm_base import CMDBApp
from api.lib.decorator import args_required
from api.resource import APIView
app_cli = CMDBApp()
class ServerRoomView(APIView):
url_prefix = ("/dcim/server_room", "/dcim/server_room/<int:_id>", "/dcim/server_room/<int:_id>/racks")
def get(self, _id):
q = request.values.get('q')
counter, result = ServerRoomManager.get_racks(_id, q)
return self.jsonify(counter=counter, result=result)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
app_cli.op.read, app_cli.admin_name)
@args_required("parent_id")
def post(self):
parent_id = request.values.pop("parent_id")
return self.jsonify(ci_id=ServerRoomManager().add(parent_id, **request.values))
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
app_cli.op.read, app_cli.admin_name)
def put(self, _id):
ServerRoomManager().update(_id, **request.values)
return self.jsonify(ci_id=_id)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
app_cli.op.read, app_cli.admin_name)
def delete(self, _id):
ServerRoomManager().delete(_id)
return self.jsonify(ci_id=_id)

View File

@@ -0,0 +1,19 @@
# -*- coding:utf-8 -*-
from api.lib.cmdb.dcim.tree_view import TreeViewManager
from api.lib.common_setting.decorator import perms_role_required
from api.lib.common_setting.role_perm_base import CMDBApp
from api.resource import APIView
app_cli = CMDBApp()
class DCIMTreeView(APIView):
url_prefix = "/dcim/tree_view"
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
app_cli.op.read, app_cli.admin_name)
def get(self):
result, type2name = TreeViewManager.get()
return self.jsonify(result=result, type2name=type2name)

View File

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

View File

@@ -0,0 +1,39 @@
# -*- coding:utf-8 -*-
from flask import request
from api.lib.cmdb.ipam.address import IpAddressManager
from api.lib.common_setting.decorator import perms_role_required
from api.lib.common_setting.role_perm_base import CMDBApp
from api.lib.decorator import args_required
from api.lib.utils import handle_arg_list
from api.resource import APIView
app_cli = CMDBApp()
class IPAddressView(APIView):
url_prefix = ("/ipam/address",)
@args_required("parent_id")
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM,
app_cli.op.read, app_cli.admin_name)
def get(self):
parent_id = request.args.get("parent_id")
numfound, result = IpAddressManager.list_ip_address(parent_id)
return self.jsonify(numfound=numfound, result=result)
@args_required("ips")
@args_required("assign_status", value_required=False)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM,
app_cli.op.read, app_cli.admin_name)
def post(self):
ips = handle_arg_list(request.values.pop("ips"))
parent_id = request.values.pop("parent_id", None)
cidr = request.values.pop("cidr", None)
IpAddressManager().assign_ips(ips, parent_id, cidr, **request.values)
return self.jsonify(code=200)

View File

@@ -0,0 +1,53 @@
# -*- coding:utf-8 -*-
from flask import request
from api.lib.cmdb.ipam.history import OperateHistoryManager
from api.lib.cmdb.ipam.history import ScanHistoryManager
from api.lib.common_setting.decorator import perms_role_required
from api.lib.common_setting.role_perm_base import CMDBApp
from api.lib.decorator import args_required
from api.lib.utils import get_page
from api.lib.utils import get_page_size
from api.lib.utils import handle_arg_list
from api.resource import APIView
app_cli = CMDBApp()
class IPAMOperateHistoryView(APIView):
url_prefix = ("/ipam/history/operate",)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM,
app_cli.op.read, app_cli.admin_name)
def get(self):
page = get_page(request.values.pop("page", 1))
page_size = get_page_size(request.values.pop("page_size", None))
operate_type = handle_arg_list(request.values.pop('operate_type', []))
if operate_type:
request.values["operate_type"] = operate_type
numfound, result = OperateHistoryManager.search(page, page_size, **request.values)
return self.jsonify(numfound=numfound, result=result)
class IPAMScanHistoryView(APIView):
url_prefix = ("/ipam/history/scan",)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM,
app_cli.op.read, app_cli.admin_name)
def get(self):
page = get_page(request.values.pop("page", 1))
page_size = get_page_size(request.values.pop("page_size", None))
numfound, result = ScanHistoryManager.search(page, page_size, **request.values)
return self.jsonify(numfound=numfound, result=result)
@args_required("exec_id")
def post(self):
ScanHistoryManager().add(**request.values)
return self.jsonify(code=200)

View File

@@ -0,0 +1,24 @@
# -*- coding:utf-8 -*-
from flask import request
from api.lib.cmdb.ipam.stats import Stats
from api.lib.common_setting.decorator import perms_role_required
from api.lib.common_setting.role_perm_base import CMDBApp
from api.lib.decorator import args_required
from api.resource import APIView
app_cli = CMDBApp()
class IPAMStatsView(APIView):
url_prefix = '/ipam/stats'
@args_required("parent_id")
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM,
app_cli.op.read, app_cli.admin_name)
def get(self):
parent_id = request.values.get("parent_id")
return self.jsonify(Stats().summary(parent_id))

View File

@@ -0,0 +1,75 @@
# -*- coding:utf-8 -*-
from flask import request
from api.lib.cmdb.ipam.subnet import SubnetManager
from api.lib.cmdb.ipam.subnet import SubnetScopeManager
from api.lib.common_setting.decorator import perms_role_required
from api.lib.common_setting.role_perm_base import CMDBApp
from api.lib.decorator import args_required
from api.resource import APIView
app_cli = CMDBApp()
class SubnetView(APIView):
url_prefix = ("/ipam/subnet", "/ipam/subnet/hosts", "/ipam/subnet/<int:_id>")
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM,
app_cli.op.read, app_cli.admin_name)
def get(self, _id=None):
if "hosts" in request.url:
return self.jsonify(SubnetManager.get_hosts(request.values.get('cidr')))
if _id is not None:
return self.jsonify(SubnetManager().get_by_id(_id))
result, type2name = SubnetManager().tree_view()
return self.jsonify(result=result, type2name=type2name)
@args_required("cidr")
@args_required("parent_id", value_required=False)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM,
app_cli.op.read, app_cli.admin_name)
def post(self):
cidr = request.values.pop("cidr")
parent_id = request.values.pop("parent_id")
agent_id = request.values.pop("agent_id", None)
cron = request.values.pop("cron", None)
return self.jsonify(SubnetManager().add(cidr, parent_id, agent_id, cron, **request.values))
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM,
app_cli.op.read, app_cli.admin_name)
def put(self, _id):
return self.jsonify(id=SubnetManager().update(_id, **request.values))
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM,
app_cli.op.read, app_cli.admin_name)
def delete(self, _id):
return self.jsonify(id=SubnetManager().delete(_id))
class SubnetScopeView(APIView):
url_prefix = ("/ipam/scope", "/ipam/scope/<int:_id>")
@args_required("parent_id", value_required=False)
@args_required("name")
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM,
app_cli.op.read, app_cli.admin_name)
def post(self):
parent_id = request.values.pop("parent_id")
name = request.values.pop("name")
return self.jsonify(SubnetScopeManager().add(parent_id, name))
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM,
app_cli.op.read, app_cli.admin_name)
def put(self, _id):
return self.jsonify(id=SubnetScopeManager().update(_id, **request.values))
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM,
app_cli.op.read, app_cli.admin_name)
def delete(self, _id):
return self.jsonify(id=SubnetScopeManager.delete(_id))

View File

@@ -211,3 +211,59 @@ class PreferenceCITypeOrderView(APIView):
PreferenceManager.upsert_ci_type_order(type_ids, is_tree)
return self.jsonify(type_ids=type_ids, is_tree=is_tree)
class PreferenceAutoSubscriptionView(APIView):
url_prefix = "/preference/auto_subscription"
def get(self):
config = PreferenceManager.get_auto_subscription_config()
return self.jsonify(config or {})
@args_required("base_strategy")
def put(self):
base_strategy = request.values.get("base_strategy")
group_ids = request.values.get("group_ids")
type_ids = request.values.get("type_ids")
enabled = request.values.get("enabled", 1) in current_app.config.get('BOOL_TRUE')
description = request.values.get("description")
if base_strategy not in ['all', 'none']:
return abort(400, "base_strategy must be 'all' or 'none'")
if group_ids:
try:
group_ids = [int(x) for x in group_ids.split(',') if x.strip()]
except ValueError:
return abort(400, "Invalid group_ids format")
if type_ids:
try:
type_ids = [int(x) for x in type_ids.split(',') if x.strip()]
except ValueError:
return abort(400, "Invalid type_ids format")
result = PreferenceManager.create_or_update_auto_subscription_config(
base_strategy=base_strategy,
group_ids=group_ids,
type_ids=type_ids,
enabled=enabled,
description=description
)
return self.jsonify(result.to_dict())
def delete(self):
PreferenceManager.delete_auto_subscription_config()
return self.jsonify(message="Auto subscription config deleted")
class PreferenceAutoSubscriptionToggleView(APIView):
url_prefix = "/preference/auto_subscription/toggle"
@args_required("enabled")
def patch(self):
enabled = request.values.get("enabled") in current_app.config.get('BOOL_TRUE')
result = PreferenceManager.toggle_auto_subscription_config(enabled)
return self.jsonify(result.to_dict())

View File

@@ -131,7 +131,7 @@ class EmployeeChangePasswordWithACLID(APIView):
if not password:
abort(400, ErrFormat.password_is_required)
data = EmployeeCRUD.change_password_by_uid(_uid, password)
EmployeeCRUD.change_password_by_uid(_uid, password)
return self.jsonify(200)

View File

@@ -6,7 +6,7 @@ import magic
from api.lib.common_setting.const import MIMEExtMap
from api.lib.common_setting.resp_format import ErrFormat
from api.lib.common_setting.upload_file import allowed_file, generate_new_file_name, CommonFileCRUD
from api.lib.common_setting.upload_file import generate_new_file_name, CommonFileCRUD
from api.resource import APIView
prefix = '/file'

View File

@@ -0,0 +1,37 @@
import os
from api.resource import APIView
from api.lib.perm.auth import auth_abandoned
prefix = "/system"
class SystemLanguageView(APIView):
url_prefix = (f"{prefix}/language",)
method_decorators = []
@auth_abandoned
def get(self):
"""Get system default language
Read from environment variable SYSTEM_DEFAULT_LANGUAGE, default to Chinese if not set
"""
default_language = os.environ.get("SYSTEM_DEFAULT_LANGUAGE", "")
return self.jsonify(
{
"language": default_language,
"language_name": self._get_language_name(default_language),
}
)
def _get_language_name(self, language_code):
"""Return language name based on language code"""
language_mapping = {
"zh-CN": "中文(简体)",
"zh-TW": "中文(繁体)",
"en-US": "English",
"ja-JP": "日本語",
"ko-KR": "한국어",
}
return language_mapping.get(language_code, "")

View File

@@ -16,7 +16,7 @@ Flask-Cors==4.0.0
Flask-Login>=0.6.2
Flask-Migrate==2.5.2
Flask-RESTful==0.3.10
Flask-SQLAlchemy==2.5.0
Flask-SQLAlchemy==3.0.5
future==0.18.3
gunicorn==21.0.1
hvac==2.0.0
@@ -56,3 +56,6 @@ colorama>=0.4.6
lz4>=4.3.2
python-magic==0.4.27
jsonpath==0.82.2
networkx>=3.1
ipaddress>=1.0.23
ruff==0.8.3

View File

@@ -39,9 +39,9 @@ SQLALCHEMY_ENGINE_OPTIONS = {
# # cache
CACHE_TYPE = 'redis'
CACHE_REDIS_HOST = '127.0.0.1'
CACHE_REDIS_PORT = 6379
CACHE_REDIS_PASSWORD = ''
CACHE_REDIS_HOST = env.str('CACHE_REDIS_HOST', default='redis')
CACHE_REDIS_PORT = env.str('CACHE_REDIS_PORT', default='6379')
CACHE_REDIS_PASSWORD = env.str('CACHE_REDIS_PASSWORD', default='')
CACHE_KEY_PREFIX = 'CMDB::'
CACHE_DEFAULT_TIMEOUT = 3000

View File

@@ -5,14 +5,6 @@ if (IS_PROD) {
plugins.push('transform-remove-console')
}
// lazy load ant-design-vue
// if your use import on Demand, Use this code
// plugins.push(['import', {
// 'libraryName': 'ant-design-vue',
// 'libraryDirectory': 'es',
// 'style': true // `style: true` 会加载 less 文件
// }])
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset',

View File

@@ -2,7 +2,7 @@ const ThemeColorReplacer = require('webpack-theme-color-replacer')
const generate = require('@ant-design/colors/lib/generate').default
const getAntdSerials = (color) => {
// 淡化(即lesstint
// Lighten (similar to less's tint)
const lightens = new Array(9).fill().map((t, i) => {
return ThemeColorReplacer.varyColor.lighten(color, i / 10)
})
@@ -13,8 +13,8 @@ const getAntdSerials = (color) => {
const themePluginOption = {
fileName: 'css/theme-colors-[contenthash:8].css',
matchColors: getAntdSerials('#2f54eb'), // 主色系列
// 改变样式选择器,解决样式覆盖问题
matchColors: getAntdSerials('#2f54eb'), // primary color series
// change style selectors to solve style override issues
changeSelector (selector) {
switch (selector) {
case '.ant-calendar-today .ant-calendar-date':

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
@font-face {
font-family: "iconfont"; /* Project id 3857903 */
src: url('iconfont.woff2?t=1724653006782') format('woff2'),
url('iconfont.woff?t=1724653006782') format('woff'),
url('iconfont.ttf?t=1724653006782') format('truetype');
src: url('iconfont.woff2?t=1755240492206') format('woff2'),
url('iconfont.woff?t=1755240492206') format('woff'),
url('iconfont.ttf?t=1755240492206') format('truetype');
}
.iconfont {
@@ -13,6 +13,582 @@
-moz-osx-font-smoothing: grayscale;
}
.auto:before {
content: "\ea28";
}
.oneterm-http:before {
content: "\ea26";
}
.oneterm-https:before {
content: "\ea27";
}
.ops-oneterm-access_period:before {
content: "\ea25";
}
.ops-oneterm-authorization:before {
content: "\ea24";
}
.onterm-symbolic_link:before {
content: "\ea23";
}
.oneterm-batch_execution:before {
content: "\ea20";
}
.ops-oneterm-file_log-selected:before {
content: "\ea21";
}
.ops-oneterm-file_log:before {
content: "\ea22";
}
.file:before {
content: "\ea1f";
}
.folder1:before {
content: "\ea1e";
}
.a-mongoDB1:before {
content: "\ea1b";
}
.a-postgreSQL1:before {
content: "\ea1c";
}
.a-telnet1:before {
content: "\ea1d";
}
.ops-oneterm-command_interception1:before {
content: "\ea17";
}
.quick_commands:before {
content: "\ea18";
}
.terminal_settings:before {
content: "\ea19";
}
.basic_settings:before {
content: "\ea1a";
}
.ops-oneterm-asset-management:before {
content: "\ea16";
}
.ai-seek:before {
content: "\ea15";
}
.ai-hate1:before {
content: "\ea13";
}
.ai-like1:before {
content: "\ea14";
}
.ai-like2:before {
content: "\ea11";
}
.ai-hate2:before {
content: "\ea12";
}
.ai-top_up:before {
content: "\ea10";
}
.ai-top_down:before {
content: "\ea0f";
}
.autoflow-script:before {
content: "\ea0d";
}
.autoflow-dag:before {
content: "\ea0e";
}
.itsm-default_line:before {
content: "\ea0c";
}
.veops-servicetree:before {
content: "\ea0b";
}
.veops-switch1:before {
content: "\ea0a";
}
.veops-label:before {
content: "\ea09";
}
.top_acl:before {
content: "\ea08";
}
.top_ticket:before {
content: "\ea06";
}
.top_agent:before {
content: "\ea07";
}
.itsm-table_download:before {
content: "\ea05";
}
.itsm-image_download:before {
content: "\ea04";
}
.veops-rear:before {
content: "\ea02";
}
.veops-front:before {
content: "\ea03";
}
.veops-xianggang:before {
content: "\ea01";
}
.a-veops-device2:before {
content: "\ea00";
}
.a-veops-room1:before {
content: "\e9ff";
}
.veops-IDC:before {
content: "\e9fe";
}
.veops-region:before {
content: "\e9fd";
}
.veops-device:before {
content: "\e9fb";
}
.veops-cabinet:before {
content: "\e9fc";
}
.veops-data_center:before {
content: "\e9f9";
}
.ops-setting-holidays:before {
content: "\e9fa";
}
.ops-itsm-logs:before {
content: "\e9f8";
}
.ops-setting-workday:before {
content: "\e9f6";
}
.ops-setting-holiday:before {
content: "\e9f7";
}
.ops-setting-festival:before {
content: "\e9f5";
}
.itsm-calc:before {
content: "\e9f4";
}
.itsm-reports_4:before {
content: "\e9f3";
}
.veops-folder:before {
content: "\e9f2";
}
.veops-entire_network_:before {
content: "\e9f1";
}
.veops-subnet:before {
content: "\e9f0";
}
.veops-map_view:before {
content: "\e9ef";
}
.veops-recycle:before {
content: "\e9ee";
}
.veops-catalog:before {
content: "\e9ed";
}
.veops-ipam:before {
content: "\e9ec";
}
.cmdb-calc:before {
content: "\e9eb";
}
.ai-users:before {
content: "\e9ea";
}
.ai-tokens:before {
content: "\e9e9";
}
.oneterm-mysql:before {
content: "\e9e8";
}
.oneterm-redis:before {
content: "\e9e7";
}
.veops-sign_out:before {
content: "\e9e6";
}
.veops-company:before {
content: "\e9e5";
}
.veops-emails:before {
content: "\e9e4";
}
.veops-switch:before {
content: "\e9e3";
}
.qiyeweixin:before {
content: "\e9e2";
}
.veops-progress:before {
content: "\e9e1";
}
.veops-completed:before {
content: "\e9e0";
}
.itsm-ticketTime:before {
content: "\e9df";
}
.veops-notification:before {
content: "\e9dc";
}
.a-veops-account1:before {
content: "\e9dd";
}
.veops-personal:before {
content: "\e9de";
}
.itsm-customer_satisfaction2:before {
content: "\e9da";
}
.itsm-over2:before {
content: "\e9db";
}
.veops-search1:before {
content: "\e9d9";
}
.itsm-customer_satisfaction:before {
content: "\e9d8";
}
.itsm-over:before {
content: "\e9d7";
}
.itsm-request:before {
content: "\e9d6";
}
.itsm-release:before {
content: "\e9d5";
}
.veops-link:before {
content: "\e9d4";
}
.oneterm-command_record:before {
content: "\e9d3";
}
.ai-question:before {
content: "\e9d2";
}
.ai-sending:before {
content: "\e9d1";
}
.ai-dialogue:before {
content: "\e9d0";
}
.ai-report2:before {
content: "\e9cf";
}
.ai-delete:before {
content: "\e9cd";
}
.caise-knowledge:before {
content: "\e9ce";
}
.ai-article:before {
content: "\e9cc";
}
.ai-model_setup1:before {
content: "\e9cb";
}
.ai-report:before {
content: "\e9ca";
}
.ai-customer_service:before {
content: "\e9c9";
}
.oneterm-connect1:before {
content: "\e9c6";
}
.oneterm-session1:before {
content: "\e9c7";
}
.oneterm-assets:before {
content: "\e9c8";
}
.a-oneterm-ssh1:before {
content: "\e9c3";
}
.a-oneterm-ssh2:before {
content: "\e9c4";
}
.oneterm-rdp:before {
content: "\e9c5";
}
.caise-websphere:before {
content: "\e9c2";
}
.caise-vps:before {
content: "\e9c1";
}
.caise-F5:before {
content: "\e9c0";
}
.caise-HAProxy:before {
content: "\e9bf";
}
.caise-JBoss:before {
content: "\e9be";
}
.caise-dongfangtong:before {
content: "\e9bd";
}
.caise-kafka:before {
content: "\e9b7";
}
.caise-weblogic:before {
content: "\e9b8";
}
.caise-TDSQL:before {
content: "\e9b9";
}
.caise-kingbase:before {
content: "\e9ba";
}
.caise-dameng:before {
content: "\e9bb";
}
.caise-TIDB:before {
content: "\e9bc";
}
.veops-expand:before {
content: "\e9b6";
}
.caise-public_cloud:before {
content: "\e9b1";
}
.caise-system:before {
content: "\e9b2";
}
.caise-IPAM:before {
content: "\e9b3";
}
.caise-hyperV:before {
content: "\e9b4";
}
.caise-data_center2:before {
content: "\e9b5";
}
.caise-hardware:before {
content: "\e9ad";
}
.caise-computer:before {
content: "\e9ae";
}
.caise-network_devices:before {
content: "\e9af";
}
.caise-storage_device:before {
content: "\e9b0";
}
.caise-load_balancing:before {
content: "\e9ab";
}
.caise-message_queue:before {
content: "\e9ac";
}
.caise-websever:before {
content: "\e9aa";
}
.caise-middleware:before {
content: "\e9a9";
}
.caise-database:before {
content: "\e9a7";
}
.caise-business:before {
content: "\e9a8";
}
.caise-virtualization:before {
content: "\e9a6";
}
.caise-storage_pool:before {
content: "\e9a4";
}
.caise-storage_volume1:before {
content: "\e9a5";
}
.ciase-aix:before {
content: "\e9a3";
}
.caise_pool:before {
content: "\e99b";
}
.caise-ip_address:before {
content: "\e99c";
}
.caise-computer_room:before {
content: "\e99d";
}
.caise-rack:before {
content: "\e99e";
}
.caise-pc:before {
content: "\e99f";
}
.caise-bandwidth_line:before {
content: "\e9a0";
}
.caise-fiber:before {
content: "\e9a1";
}
.caise-disk_array:before {
content: "\e9a2";
}
.veops-group:before {
content: "\e99a";
}
.veops-inheritance:before {
content: "\e999";
}
.veops-department:before {
content: "\e998";
}
@@ -641,11 +1217,11 @@
content: "\e914";
}
.itsm-duration:before {
.itsm-reports_3:before {
content: "\e913";
}
.itsm-workload:before {
.itsm-reports_2:before {
content: "\e912";
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -1,7 +1,6 @@
<!DOCTYPE html>
<html lang="zh-cmn-Hans">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
@@ -12,17 +11,19 @@
<% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.css) { %>
<link rel="stylesheet" href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" />
<% } %>
<script>
  const userAgent = navigator.userAgent
<script>
const userAgent = navigator.userAgent
const isEdge = userAgent.indexOf("Edge") > -1
  const isChrome = userAgent.indexOf("Chrome") > -1 && userAgent.indexOf("Safari") > -1 && !isEdge
  if (!isChrome) {
alert("推荐使用Chrome浏览器 其他环境下未做严格测试!")
const isChrome = userAgent.indexOf("Chrome") > -1 && userAgent.indexOf("Safari") > -1 && !isEdge
const lang = (navigator.language || navigator.userLanguage || '').toLowerCase()
if (!isChrome) {
if (lang.startsWith('zh')) {
alert("推荐使用Chrome浏览器其他环境下未做严格测试");
} else {
alert("It is recommended to use Chrome browser. Other environments are not strictly tested!");
}
}
</script>
</head>
<body>
<noscript>
@@ -30,10 +31,10 @@
</noscript>
<div id="app">
<div id="loading-mask">
<div class="loading-wrapper">
<span class="loading-dot loading-dot-spin"><i></i><i></i><i></i><i></i></span>
</div>
<div class="loading-wrapper">
<span class="loading-dot loading-dot-spin"><i></i><i></i><i></i><i></i></span>
</div>
</div>
</div>
<!-- require cdn assets js -->
<% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.js) { %>

View File

@@ -12,6 +12,7 @@ import zhCN from 'ant-design-vue/lib/locale-provider/zh_CN'
import enUS from 'ant-design-vue/lib/locale-provider/en_US'
import { AppDeviceEnquire } from '@/utils/mixin'
import { debounce } from './utils/util'
import { getSystemLanguage } from '@/api/system.js'
import { h } from 'snabbdom'
import { DomEditor, Boot } from '@wangeditor/editor'
@@ -45,8 +46,7 @@ export default {
},
},
created() {
this.SET_LOCALE(localStorage.getItem('ops_locale') || 'zh')
this.$i18n.locale = localStorage.getItem('ops_locale') || 'zh'
this.initLanguage()
this.timer = setInterval(() => {
this.setTime(new Date().getTime())
}, 1000)
@@ -60,133 +60,7 @@ export default {
})
)
// 注册富文本自定义元素
const resume = {
type: 'attachment',
attachmentLabel: '',
attachmentValue: '',
children: [{ text: '' }], // void 元素必须有一个 children 其中只有一个空字符串重要
}
function withAttachment(editor) {
// JS 语法
const { isInline, isVoid } = editor
const newEditor = editor
newEditor.isInline = (elem) => {
const type = DomEditor.getNodeType(elem)
if (type === 'attachment') return true // 针对 type: attachment 设置为 inline
return isInline(elem)
}
newEditor.isVoid = (elem) => {
const type = DomEditor.getNodeType(elem)
if (type === 'attachment') return true // 针对 type: attachment 设置为 void
return isVoid(elem)
}
return newEditor // 返回 newEditor 重要
}
Boot.registerPlugin(withAttachment)
/**
* 渲染附件元素到编辑器
* @param elem 附件元素即上文的 myResume
* @param children 元素子节点void 元素可忽略
* @param editor 编辑器实例
* @returns vnode 节点通过 snabbdom.js h 函数生成
*/
function renderAttachment(elem, children, editor) {
// JS 语法
// 获取附件的数据参考上文 myResume 数据结构
const { attachmentLabel = '', attachmentValue = '' } = elem
// 附件元素 vnode
const attachVnode = h(
// HTML tag
'span',
// HTML 属性样式事件
{
props: { contentEditable: false }, // HTML 属性驼峰式写法
style: {
display: 'inline-block',
margin: '0 3px',
padding: '0 3px',
backgroundColor: '#e6f7ff',
border: '1px solid #91d5ff',
borderRadius: '2px',
color: '#1890ff',
}, // style 驼峰式写法
on: {
click() {
console.log('clicked', attachmentValue)
} /* 其他... */,
},
},
// 子节点
[attachmentLabel]
)
return attachVnode
}
const renderElemConf = {
type: 'attachment', // 新元素 type 重要
renderElem: renderAttachment,
}
Boot.registerRenderElem(renderElemConf)
/**
* 生成附件元素的 HTML
* @param elem 附件元素即上文的 myResume
* @param childrenHtml 子节点的 HTML 代码void 元素可忽略
* @returns 附件元素的 HTML 字符串
*/
function attachmentToHtml(elem, childrenHtml) {
// JS 语法
// 获取附件元素的数据
const { attachmentValue = '', attachmentLabel = '' } = elem
// 生成 HTML 代码
const html = `<span data-w-e-type="attachment" data-w-e-is-void data-w-e-is-inline data-attachmentValue="${attachmentValue}" data-attachmentLabel="${attachmentLabel}">${attachmentLabel}</span>`
return html
}
const elemToHtmlConf = {
type: 'attachment', // 新元素的 type 重要
elemToHtml: attachmentToHtml,
}
Boot.registerElemToHtml(elemToHtmlConf)
/**
* 解析 HTML 字符串生成附件元素
* @param domElem HTML 对应的 DOM Element
* @param children 子节点
* @param editor editor 实例
* @returns 附件元素如上文的 myResume
*/
function parseAttachmentHtml(domElem, children, editor) {
// JS 语法
// DOM element 中获取附件的信息
const attachmentValue = domElem.getAttribute('data-attachmentValue') || ''
const attachmentLabel = domElem.getAttribute('data-attachmentLabel') || ''
// 生成附件元素按照此前约定的数据结构
const myResume = {
type: 'attachment',
attachmentValue,
attachmentLabel,
children: [{ text: '' }], // void node 必须有 children 其中有一个空字符串重要
}
return myResume
}
const parseHtmlConf = {
selector: 'span[data-w-e-type="attachment"]', // CSS 选择器匹配特定的 HTML 标签
parseElemHtml: parseAttachmentHtml,
}
Boot.registerParseElemHtml(parseHtmlConf)
this.handleEditor()
},
beforeDestroy() {
clearInterval(this.timer)
@@ -200,6 +74,141 @@ export default {
this.alive = true
})
},
async initLanguage() {
let saveLocale = localStorage.getItem('ops_locale')
if (!saveLocale) {
let requestLanguage = ''
try {
const languageRes = await getSystemLanguage()
requestLanguage = languageRes?.language || ''
} catch (e) {
console.error('getSystemLanguage error:', e)
}
// request language variable || user local system language
const userLanguage = requestLanguage || navigator.language || navigator.userLanguage
if (userLanguage.includes('zh')) {
saveLocale = 'zh'
} else {
saveLocale = 'en'
}
}
this.SET_LOCALE(saveLocale)
this.$i18n.locale = saveLocale
},
handleEditor() {
// register custom rich text element: attachment
function withAttachment(editor) {
const { isInline, isVoid } = editor
const newEditor = editor
newEditor.isInline = (elem) => {
const type = DomEditor.getNodeType(elem)
if (type === 'attachment') return true // For type: attachment, set to inline
return isInline(elem)
}
newEditor.isVoid = (elem) => {
const type = DomEditor.getNodeType(elem)
if (type === 'attachment') return true // For type: attachment set to void
return isVoid(elem)
}
return newEditor // Must return, important!!!
}
Boot.registerPlugin(withAttachment)
/**
* Render "attachment" element in editor
* @param elem Attachment element
* @param children Child nodes (ignored for void elements)
* @param editor Editor instance
* @returns vnode (generated by snabbdom's h function)
*/
function renderAttachment(elem, children, editor) {
const { attachmentLabel = '', attachmentValue = '' } = elem
const attachVnode = h(
// HTML tag
'span',
// HTML attr, style, event
{
props: { contentEditable: false },
style: {
display: 'inline-block',
margin: '0 3px',
padding: '0 3px',
backgroundColor: '#e6f7ff',
border: '1px solid #91d5ff',
borderRadius: '2px',
color: '#1890ff',
},
on: {
click() {
console.log('clicked', attachmentValue)
}
},
},
// child node
[attachmentLabel]
)
return attachVnode
}
const renderElemConf = {
type: 'attachment',
renderElem: renderAttachment,
}
Boot.registerRenderElem(renderElemConf)
/**
* Generate HTML for "attachment" element
* @param elem Attachment element
* @param childrenHtml Child HTML (ignored for void elements)
* @returns HTML string
*/
function attachmentToHtml(elem, childrenHtml) {
// Getting data for attached elements
const { attachmentValue = '', attachmentLabel = '' } = elem
// generate HTML
const html = `<span data-w-e-type="attachment" data-w-e-is-void data-w-e-is-inline data-attachmentValue="${attachmentValue}" data-attachmentLabel="${attachmentLabel}">${attachmentLabel}</span>`
return html
}
const elemToHtmlConf = {
type: 'attachment',
elemToHtml: attachmentToHtml,
}
Boot.registerElemToHtml(elemToHtmlConf)
/**
* Parse HTML to generate "attachment" element
* @param domElem DOM element
* @param children Children
* @param editor Editor instance
* @returns Attachment element
*/
function parseAttachmentHtml(domElem, children, editor) {
// Getting attachment information from DOM element
const attachmentValue = domElem.getAttribute('data-attachmentValue') || ''
const attachmentLabel = domElem.getAttribute('data-attachmentLabel') || ''
const myResume = {
type: 'attachment',
attachmentValue,
attachmentLabel,
children: [{ text: '' }], // The void node must have children with an empty string in it, important!!!!
}
return myResume
}
const parseHtmlConf = {
selector: 'span[data-w-e-type="attachment"]', // CSS selector to match specific HTML tags
parseElemHtml: parseAttachmentHtml,
}
Boot.registerParseElemHtml(parseHtmlConf)
}
},
}
</script>

View File

@@ -0,0 +1,8 @@
import { axios } from '@/utils/request'
export function getSystemLanguage() {
return axios({
url: '/common-setting/v1/system/language',
method: 'get',
})
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -1,41 +1,40 @@
import i18n from '@/lang'
export const ruleTypeList = () => {
return [
{ value: 'and', label: i18n.t('cmdbFilterComp.and') },
{ value: 'or', label: i18n.t('cmdbFilterComp.or') },
// { value: 'not', label: '非' },
]
}
export const expList = () => {
return [
{ value: 'is', label: i18n.t('cmdbFilterComp.is') },
{ value: '~is', label: i18n.t('cmdbFilterComp.~is') },
{ value: 'contain', label: i18n.t('cmdbFilterComp.contain') },
{ value: '~contain', label: i18n.t('cmdbFilterComp.~contain') },
{ value: 'start_with', label: i18n.t('cmdbFilterComp.start_with') },
{ value: '~start_with', label: i18n.t('cmdbFilterComp.~start_with') },
{ value: 'end_with', label: i18n.t('cmdbFilterComp.end_with') },
{ value: '~end_with', label: i18n.t('cmdbFilterComp.~end_with') },
{ value: '~value', label: i18n.t('cmdbFilterComp.~value') }, // 为空的定义有点绕
{ value: 'value', label: i18n.t('cmdbFilterComp.value') },
]
}
export const advancedExpList = () => {
return [
{ value: 'in', label: i18n.t('cmdbFilterComp.in') },
{ value: '~in', label: i18n.t('cmdbFilterComp.~in') },
{ value: 'range', label: i18n.t('cmdbFilterComp.range') },
{ value: '~range', label: i18n.t('cmdbFilterComp.~range') },
{ value: 'compare', label: i18n.t('cmdbFilterComp.compare') },
]
}
export const compareTypeList = [
{ value: '1', label: '>' },
{ value: '2', label: '>=' },
{ value: '3', label: '<' },
{ value: '4', label: '<=' },
]
import i18n from '@/lang'
export const ruleTypeList = () => {
return [
{ value: 'and', label: i18n.t('cmdbFilterComp.and') },
{ value: 'or', label: i18n.t('cmdbFilterComp.or') },
]
}
export const expList = () => {
return [
{ value: 'is', label: i18n.t('cmdbFilterComp.is') },
{ value: '~is', label: i18n.t('cmdbFilterComp.~is') },
{ value: 'contain', label: i18n.t('cmdbFilterComp.contain') },
{ value: '~contain', label: i18n.t('cmdbFilterComp.~contain') },
{ value: 'start_with', label: i18n.t('cmdbFilterComp.start_with') },
{ value: '~start_with', label: i18n.t('cmdbFilterComp.~start_with') },
{ value: 'end_with', label: i18n.t('cmdbFilterComp.end_with') },
{ value: '~end_with', label: i18n.t('cmdbFilterComp.~end_with') },
{ value: '~value', label: i18n.t('cmdbFilterComp.~value') },
{ value: 'value', label: i18n.t('cmdbFilterComp.value') },
]
}
export const advancedExpList = () => {
return [
{ value: 'in', label: i18n.t('cmdbFilterComp.in') },
{ value: '~in', label: i18n.t('cmdbFilterComp.~in') },
{ value: 'range', label: i18n.t('cmdbFilterComp.range') },
{ value: '~range', label: i18n.t('cmdbFilterComp.~range') },
{ value: 'compare', label: i18n.t('cmdbFilterComp.compare') },
]
}
export const compareTypeList = [
{ value: '1', label: '>' },
{ value: '2', label: '>=' },
{ value: '3', label: '<' },
{ value: '4', label: '<=' },
]

View File

@@ -106,7 +106,7 @@
<CIReferenceAttr
v-if="getAttr(item.property).is_reference && (item.exp === 'is' || item.exp === '~is')"
:style="{ width: '175px' }"
class="select-filter-component"
class="select-filter-component ops-select-bg"
:referenceTypeId="getAttr(item.property).reference_type_id"
:disabled="disabled"
v-model="item.value"
@@ -114,7 +114,7 @@
<a-select
v-else-if="getAttr(item.property).is_bool && (item.exp === 'is' || item.exp === '~is')"
v-model="item.value"
class="select-filter-component"
class="select-filter-component ops-select-bg"
:style="{ width: '175px' }"
:disabled="disabled"
:placeholder="$t('placeholder2')"
@@ -139,9 +139,9 @@
:normalizer="
(node) => {
return {
id: String(node[0] || ''),
label: node[1] ? node[1].label || node[0] : node[0],
children: node.children && node.children.length ? node.children : undefined,
id: node.id,
label: node.label,
children: node.children,
}
}
"
@@ -301,7 +301,7 @@ export default {
return [
{ value: 'is', label: this.$t('cmdbFilterComp.is') },
{ value: '~is', label: this.$t('cmdbFilterComp.~is') },
{ value: '~value', label: this.$t('cmdbFilterComp.~value') }, // 为空的定义有点绕
{ value: '~value', label: this.$t('cmdbFilterComp.~value') },
{ value: 'value', label: this.$t('cmdbFilterComp.value') },
]
}
@@ -352,8 +352,12 @@ export default {
},
getChoiceValueByProperty(property) {
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
if (_find) {
return _find.choice_value
if (_find?.choice_value?.length) {
return _find.choice_value.map((node) => ({
id: String(node?.[0] ?? ''),
label: node?.[1]?.label || node?.[0] || '',
children: node?.children?.length ? node.children : undefined
}))
}
return []
},
@@ -394,7 +398,6 @@ export default {
/deep/ .ant-select-selection {
height: 24px;
background: #f7f8fa;
line-height: 24px;
border: none;

View File

@@ -1,302 +1,304 @@
<template>
<div>
<a-popover
v-if="isDropdown"
v-model="visible"
trigger="click"
:placement="placement"
overlayClassName="table-filter"
@visibleChange="visibleChange"
>
<slot name="popover_item">
<a-button type="primary" ghost>{{ $t('cmdbFilterComp.conditionFilter') }}<a-icon type="filter"/></a-button>
</slot>
<template slot="content">
<Expression
:needAddHere="needAddHere"
v-model="ruleList"
:canSearchPreferenceAttrList="canSearchPreferenceAttrList.filter((attr) => !attr.is_password)"
:disabled="disabled"
/>
<a-divider :style="{ margin: '10px 0' }" />
<div style="width:554px">
<a-space :style="{ display: 'flex', justifyContent: 'flex-end' }">
<a-button type="primary" size="small" @click="handleSubmit">{{ $t('confirm') }}</a-button>
<a-button size="small" @click="handleClear">{{ $t('clear') }}</a-button>
</a-space>
</div>
</template>
</a-popover>
<Expression
:needAddHere="needAddHere"
v-else
v-model="ruleList"
:canSearchPreferenceAttrList="canSearchPreferenceAttrList.filter((attr) => !attr.is_password)"
:disabled="disabled"
/>
</div>
</template>
<script>
import { v4 as uuidv4 } from 'uuid'
import Expression from './expression.vue'
import { advancedExpList, compareTypeList } from './constants'
export default {
name: 'FilterComp',
components: { Expression },
props: {
canSearchPreferenceAttrList: {
type: Array,
required: true,
default: () => [],
},
expression: {
type: String,
default: '',
},
regQ: {
type: String,
default: '(?<=q=).+(?=&)|(?<=q=).+$',
},
placement: {
type: String,
default: 'bottomRight',
},
isDropdown: {
type: Boolean,
default: true,
},
needAddHere: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
},
data() {
return {
advancedExpList,
compareTypeList,
visible: false,
ruleList: [],
filterExp: '',
}
},
methods: {
visibleChange(open, isInitOne = true) {
// isInitOne 初始化exp为空时ruleList是否默认给一条
// const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
const exp = this.expression.match(new RegExp(this.regQ, 'g'))
? this.expression.match(new RegExp(this.regQ, 'g'))[0]
: null
if (open && exp) {
const expArray = exp.split(',').map((item) => {
let has_not = ''
const key = item.split(':')[0]
const val = item
.split(':')
.slice(1)
.join(':')
let type, property, exp, value, min, max, compareType
if (key.includes('-')) {
type = 'or'
if (key.includes('~')) {
property = key.substring(2)
has_not = '~'
} else {
property = key.substring(1)
}
} else {
type = 'and'
if (key.includes('~')) {
property = key.substring(1)
has_not = '~'
} else {
property = key
}
}
const in_reg = /(?<=\().+(?=\))/g
const range_reg = /(?<=\[).+(?=\])/g
const compare_reg = /(?<=>=|<=|>(?!=)|<(?!=)).+/
if (val === '*') {
exp = has_not + 'value'
value = ''
} else if (in_reg.test(val)) {
exp = has_not + 'in'
value = val.match(in_reg)[0]
} else if (range_reg.test(val)) {
exp = has_not + 'range'
value = val.match(range_reg)[0]
min = value.split('_TO_')[0]
max = value.split('_TO_')[1]
} else if (compare_reg.test(val)) {
exp = has_not + 'compare'
value = val.match(compare_reg)[0]
const _compareType = val.substring(0, val.match(compare_reg)['index'])
const idx = compareTypeList.findIndex((item) => item.label === _compareType)
compareType = compareTypeList[idx].value
} else if (!val.includes('*')) {
exp = has_not + 'is'
value = val
} else {
const resList = [
['contain', /(?<=\*).*(?=\*)/g],
['end_with', /(?<=\*).+/g],
['start_with', /.+(?=\*)/g],
]
for (let i = 0; i < 3; i++) {
const reg = resList[i]
if (reg[1].test(val)) {
exp = has_not + reg[0]
value = val.match(reg[1])[0]
break
}
}
}
return {
id: uuidv4(),
type,
property,
exp,
value,
min,
max,
compareType,
}
})
this.ruleList = [...expArray]
} else if (open) {
const _canSearchPreferenceAttrList = this.canSearchPreferenceAttrList.filter((attr) => !attr.is_password)
this.ruleList = isInitOne
? [
{
id: uuidv4(),
type: 'and',
property:
_canSearchPreferenceAttrList && _canSearchPreferenceAttrList.length
? _canSearchPreferenceAttrList[0].name
: undefined,
exp: 'is',
value: null,
},
]
: []
}
},
handleClear() {
this.ruleList = [
{
id: uuidv4(),
type: 'and',
property: this.canSearchPreferenceAttrList[0].name,
exp: 'is',
value: null,
},
]
this.filterExp = ''
this.visible = false
this.$emit('setExpFromFilter', this.filterExp)
},
handleSubmit() {
if (this.ruleList && this.ruleList.length) {
this.ruleList[0].type = 'and' // 增删后以防万一第一个不是and
this.filterExp = ''
const expList = this.ruleList.map((rule) => {
let singleRuleExp = ''
let _exp = rule.exp
if (rule.type === 'or') {
singleRuleExp += '-'
}
if (rule.exp.includes('~')) {
singleRuleExp += '~'
_exp = rule.exp.split('~')[1]
}
singleRuleExp += `${rule.property}:`
if (_exp === 'is') {
singleRuleExp += `${rule.value ?? ''}`
}
if (_exp === 'contain') {
singleRuleExp += `*${rule.value ?? ''}*`
}
if (_exp === 'start_with') {
singleRuleExp += `${rule.value ?? ''}*`
}
if (_exp === 'end_with') {
singleRuleExp += `*${rule.value ?? ''}`
}
if (_exp === 'value') {
singleRuleExp += `*`
}
if (_exp === 'in') {
singleRuleExp += `(${rule.value ?? ''})`
}
if (_exp === 'range') {
singleRuleExp += `[${rule.min}_TO_${rule.max}]`
}
if (_exp === 'compare') {
const idx = compareTypeList.findIndex((item) => item.value === rule.compareType)
singleRuleExp += `${compareTypeList[idx].label}${rule.value ?? ''}`
}
return singleRuleExp
})
this.filterExp = expList.join(',')
this.$emit('setExpFromFilter', this.filterExp)
} else {
this.$emit('setExpFromFilter', '')
}
this.visible = false
},
},
}
</script>
<style lang="less" scoped>
.table-filter {
.table-filter-add {
margin-top: 10px;
& > a {
padding: 2px 8px;
&:hover {
background-color: #f0faff;
border-radius: 5px;
}
}
}
.table-filter-extra-icon {
padding: 0px 2px;
&:hover {
display: inline-block;
border-radius: 5px;
background-color: #f0faff;
}
}
}
</style>
<style lang="less">
.table-filter-extra-operation {
.ant-popover-inner-content {
padding: 3px 4px;
.operation {
cursor: pointer;
width: 90px;
height: 30px;
line-height: 30px;
padding: 3px 4px;
border-radius: 5px;
transition: all 0.3s;
&:hover {
background-color: #f0faff;
}
> .anticon {
margin-right: 10px;
}
}
}
}
</style>
<template>
<div>
<a-popover
v-if="isDropdown"
v-model="visible"
trigger="click"
:placement="placement"
overlayClassName="table-filter"
@visibleChange="visibleChange"
>
<slot name="popover_item">
<a-button type="primary" ghost>{{ $t('cmdbFilterComp.conditionFilter') }}<a-icon type="filter"/></a-button>
</slot>
<template slot="content">
<Expression
:needAddHere="needAddHere"
v-model="ruleList"
:canSearchPreferenceAttrList="canSearchPreferenceAttrList.filter((attr) => !attr.is_password)"
:disabled="disabled"
/>
<a-divider :style="{ margin: '10px 0' }" />
<div style="width:554px">
<a-space :style="{ display: 'flex', justifyContent: 'flex-end' }">
<a-button type="primary" size="small" @click="handleSubmit">{{ $t('confirm') }}</a-button>
<a-button size="small" @click="handleClear">{{ $t('clear') }}</a-button>
</a-space>
</div>
</template>
</a-popover>
<Expression
:needAddHere="needAddHere"
v-else
v-model="ruleList"
:canSearchPreferenceAttrList="canSearchPreferenceAttrList.filter((attr) => !attr.is_password)"
:disabled="disabled"
/>
</div>
</template>
<script>
import { v4 as uuidv4 } from 'uuid'
import Expression from './expression.vue'
import { advancedExpList, compareTypeList } from './constants'
export default {
name: 'FilterComp',
components: { Expression },
props: {
canSearchPreferenceAttrList: {
type: Array,
required: true,
default: () => [],
},
expression: {
type: String,
default: '',
},
regQ: {
type: String,
default: '(?<=q=).+(?=&)|(?<=q=).+$',
},
placement: {
type: String,
default: 'bottomRight',
},
isDropdown: {
type: Boolean,
default: true,
},
needAddHere: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
},
data() {
return {
advancedExpList,
compareTypeList,
visible: false,
ruleList: [],
filterExp: '',
}
},
methods: {
/**
* @param isInitOne When the initialization exp is null, does the ruleList default to giving one
*/
visibleChange(open, isInitOne = true) {
// const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
const exp = this.expression.match(new RegExp(this.regQ, 'g'))
? this.expression.match(new RegExp(this.regQ, 'g'))[0]
: null
if (open && exp) {
const expArray = exp.split(',').map((item) => {
let has_not = ''
const key = item.split(':')[0]
const val = item
.split(':')
.slice(1)
.join(':')
let type, property, exp, value, min, max, compareType
if (key.includes('-')) {
type = 'or'
if (key.includes('~')) {
property = key.substring(2)
has_not = '~'
} else {
property = key.substring(1)
}
} else {
type = 'and'
if (key.includes('~')) {
property = key.substring(1)
has_not = '~'
} else {
property = key
}
}
const in_reg = /(?<=\().+(?=\))/g
const range_reg = /(?<=\[).+(?=\])/g
const compare_reg = /(?<=>=|<=|>(?!=)|<(?!=)).+/
if (val === '*') {
exp = has_not + 'value'
value = ''
} else if (in_reg.test(val)) {
exp = has_not + 'in'
value = val.match(in_reg)[0]
} else if (range_reg.test(val)) {
exp = has_not + 'range'
value = val.match(range_reg)[0]
min = value.split('_TO_')[0]
max = value.split('_TO_')[1]
} else if (compare_reg.test(val)) {
exp = has_not + 'compare'
value = val.match(compare_reg)[0]
const _compareType = val.substring(0, val.match(compare_reg)['index'])
const idx = compareTypeList.findIndex((item) => item.label === _compareType)
compareType = compareTypeList[idx].value
} else if (!val.includes('*')) {
exp = has_not + 'is'
value = val
} else {
const resList = [
['contain', /(?<=\*).*(?=\*)/g],
['end_with', /(?<=\*).+/g],
['start_with', /.+(?=\*)/g],
]
for (let i = 0; i < 3; i++) {
const reg = resList[i]
if (reg[1].test(val)) {
exp = has_not + reg[0]
value = val.match(reg[1])[0]
break
}
}
}
return {
id: uuidv4(),
type,
property,
exp,
value,
min,
max,
compareType,
}
})
this.ruleList = [...expArray]
} else if (open) {
const _canSearchPreferenceAttrList = this.canSearchPreferenceAttrList.filter((attr) => !attr.is_password)
this.ruleList = isInitOne
? [
{
id: uuidv4(),
type: 'and',
property:
_canSearchPreferenceAttrList && _canSearchPreferenceAttrList.length
? _canSearchPreferenceAttrList[0].name
: undefined,
exp: 'is',
value: null,
},
]
: []
}
},
handleClear() {
this.ruleList = [
{
id: uuidv4(),
type: 'and',
property: this.canSearchPreferenceAttrList[0].name,
exp: 'is',
value: null,
},
]
this.filterExp = ''
this.visible = false
this.$emit('setExpFromFilter', this.filterExp)
},
handleSubmit() {
if (this.ruleList && this.ruleList.length) {
this.ruleList[0].type = 'and' // after add/delete, just in case the first one is not 'and'
this.filterExp = ''
const expList = this.ruleList.map((rule) => {
let singleRuleExp = ''
let _exp = rule.exp
if (rule.type === 'or') {
singleRuleExp += '-'
}
if (rule.exp.includes('~')) {
singleRuleExp += '~'
_exp = rule.exp.split('~')[1]
}
singleRuleExp += `${rule.property}:`
if (_exp === 'is') {
singleRuleExp += `${rule.value ?? ''}`
}
if (_exp === 'contain') {
singleRuleExp += `*${rule.value ?? ''}*`
}
if (_exp === 'start_with') {
singleRuleExp += `${rule.value ?? ''}*`
}
if (_exp === 'end_with') {
singleRuleExp += `*${rule.value ?? ''}`
}
if (_exp === 'value') {
singleRuleExp += `*`
}
if (_exp === 'in') {
singleRuleExp += `(${rule.value ?? ''})`
}
if (_exp === 'range') {
singleRuleExp += `[${rule.min}_TO_${rule.max}]`
}
if (_exp === 'compare') {
const idx = compareTypeList.findIndex((item) => item.value === rule.compareType)
singleRuleExp += `${compareTypeList[idx].label}${rule.value ?? ''}`
}
return singleRuleExp
})
this.filterExp = expList.join(',')
this.$emit('setExpFromFilter', this.filterExp)
} else {
this.$emit('setExpFromFilter', '')
}
this.visible = false
},
},
}
</script>
<style lang="less" scoped>
.table-filter {
.table-filter-add {
margin-top: 10px;
& > a {
padding: 2px 8px;
&:hover {
background-color: @primary-color_5;
border-radius: 5px;
}
}
}
.table-filter-extra-icon {
padding: 0px 2px;
&:hover {
display: inline-block;
border-radius: 5px;
background-color: #f0faff;
}
}
}
</style>
<style lang="less">
.table-filter-extra-operation {
.ant-popover-inner-content {
padding: 3px 4px;
.operation {
cursor: pointer;
width: 90px;
height: 30px;
line-height: 30px;
padding: 3px 4px;
border-radius: 5px;
transition: all 0.3s;
&:hover {
background-color: #f0faff;
}
> .anticon {
margin-right: 10px;
}
}
}
}
</style>

View File

@@ -14,7 +14,7 @@
<script>
/**
* 元素折叠过度效果
* Collapse transition effect for elements
*/
export default {
name: 'CollapseTransition',
@@ -33,20 +33,17 @@ export default {
},
methods: {
collapseBeforeEnter(el) {
// console.log('11, collapseBeforeEnter');
this.oldPaddingBottom = el.style.paddingBottom
this.oldPaddingTop = el.style.paddingTop
// 过渡效果开始前设置元素的maxHeight为0让元素maxHeight有一个初始值
// set the element's maxHeight to 0 before the transition effect starts so that the element's maxHeight has an initial value
el.style.paddingTop = '0'
el.style.paddingBottom = '0'
el.style.maxHeight = '0'
},
collapseEnter(el, done) {
// console.log('22, collapseEnter');
//
this.oldOverflow = el.style.overflow
const elHeight = el.scrollHeight
// 过渡效果进入后将元素的maxHeight设置为元素本身的高度将元素maxHeight设置为auto不会有过渡效果
// After entering, set maxHeight to the element's height; setting maxHeight to auto will not have a transition effect
if (elHeight > 0) {
el.style.maxHeight = elHeight + 'px'
} else {
@@ -59,24 +56,20 @@ export default {
// done();
const onTransitionDone = function() {
done()
// console.log('enter onTransitionDone');
el.removeEventListener('transitionend', onTransitionDone, false)
el.removeEventListener('transitioncancel', onTransitionDone, false)
}
// 绑定元素的transition完成事件在transition完成后立即完成vue的过度动效
// Bind transition end event to finish Vue's transition immediately after the CSS transition
el.addEventListener('transitionend', onTransitionDone, false)
el.addEventListener('transitioncancel', onTransitionDone, false)
},
collapseAfterEnter(el) {
// console.log('33, collapseAfterEnter');
// 过渡效果完成后恢复元素的maxHeight
// Restore maxHeight after transition is complete
el.style.maxHeight = ''
el.style.overflow = this.oldOverflow
},
collapseBeforeLeave(el) {
// console.log('44, collapseBeforeLeave', el.scrollHeight);
this.oldPaddingBottom = el.style.paddingBottom
this.oldPaddingTop = el.style.paddingTop
this.oldOverflow = el.style.overflow
@@ -85,8 +78,6 @@ export default {
el.style.overflow = 'hidden'
},
collapseLeave(el, done) {
// console.log('55, collapseLeave', el.scrollHeight);
if (el.scrollHeight !== 0) {
el.style.maxHeight = '0'
el.style.paddingBottom = '0'
@@ -95,16 +86,14 @@ export default {
// done();
const onTransitionDone = function() {
done()
// console.log('leave onTransitionDone');
el.removeEventListener('transitionend', onTransitionDone, false)
el.removeEventListener('transitioncancel', onTransitionDone, false)
}
// 绑定元素的transition完成事件在transition完成后立即完成vue的过度动效
// Bind transition end event to finish Vue's transition immediately after the CSS transition
el.addEventListener('transitionend', onTransitionDone, false)
el.addEventListener('transitioncancel', onTransitionDone, false)
},
collapseAfterLeave(el) {
// console.log('66, collapseAfterLeave');
el.style.maxHeight = ''
el.style.overflow = this.oldOverflow
el.style.paddingBottom = this.oldPaddingBottom

View File

@@ -341,7 +341,7 @@ export default {
},
}
</script>
<style scoped>
<style lang="less" scoped>
.pop_btn {
text-align: right;
margin-top: 24px;

View File

@@ -1,9 +1,6 @@
/* eslint-disable */
/*
!!!!!!!
以下为凶残的cron表达式验证胆小肾虚及心脏病者慎入!!!
不听劝告者后果自负T T
!!!!!!!
cron表达式验证
cron表达式为秒
判断正误方法错误的话返回错误信息正确的话返回true
*/

View File

@@ -47,7 +47,7 @@ export const commonIconList = ['changyong-ubuntu',
export const linearIconList = [
{
value: 'database',
label: '数据库',
label: 'components.database',
list: [{
value: 'icon-xianxing-DB2',
label: 'DB2'
@@ -81,7 +81,7 @@ export const linearIconList = [
}]
}, {
value: 'system',
label: '操作系统',
label: 'components.system',
list: [{
value: 'icon-xianxing-Windows',
label: 'Windows'
@@ -106,7 +106,7 @@ export const linearIconList = [
}]
}, {
value: 'language',
label: '语言',
label: 'components.language',
list: [{
value: 'icon-xianxing-python',
label: 'python'
@@ -137,7 +137,7 @@ export const linearIconList = [
}]
}, {
value: 'status',
label: '状态',
label: 'components.status',
list: [{
value: 'icon-xianxing-yiwen',
label: '疑问'
@@ -177,7 +177,7 @@ export const linearIconList = [
}]
}, {
value: 'icon-xianxing-application',
label: '应用',
label: 'components.commonComponent',
list: [{
value: 'icon-xianxing-yilianjie',
label: '已连接'
@@ -310,7 +310,7 @@ export const linearIconList = [
}]
}, {
value: 'data',
label: '数据',
label: 'components.data',
list: [{
value: 'icon-xianxing-bingzhuangtu',
label: '饼状图'
@@ -387,7 +387,7 @@ export const linearIconList = [
export const fillIconList = [
{
value: 'database',
label: '数据库',
label: 'components.database',
list: [{
value: 'icon-shidi-DB2',
label: 'DB2'
@@ -421,7 +421,7 @@ export const fillIconList = [
}]
}, {
value: 'system',
label: '操作系统',
label: 'components.system',
list: [{
value: 'icon-shidi-Windows',
label: 'Windows'
@@ -446,7 +446,7 @@ export const fillIconList = [
}]
}, {
value: 'language',
label: '语言',
label: 'components.language',
list: [{
value: 'icon-shidi-python',
label: 'python'
@@ -477,7 +477,7 @@ export const fillIconList = [
}]
}, {
value: 'status',
label: '状态',
label: 'components.status',
list: [{
value: 'icon-shidi-yiwen',
label: '疑问'
@@ -517,7 +517,7 @@ export const fillIconList = [
}]
}, {
value: 'icon-shidi-application',
label: '应用',
label: 'components.commonComponent',
list: [{
value: 'icon-shidi-yilianjie',
label: '已连接'
@@ -650,7 +650,7 @@ export const fillIconList = [
}]
}, {
value: 'data',
label: '数据',
label: 'components.data',
list: [{
value: 'icon-shidi-bingzhuangtu',
label: '饼状图'
@@ -727,8 +727,20 @@ export const fillIconList = [
export const multicolorIconList = [
{
value: 'database',
label: '数据库',
label: 'components.database',
list: [{
value: 'caise-TIDB',
label: 'TIDB'
}, {
value: 'caise-dameng',
label: '达梦'
}, {
value: 'caise-kingbase',
label: 'KingBase'
}, {
value: 'caise-TDSQL',
label: 'TDSQL'
}, {
value: 'caise-DB2',
label: 'DB2'
}, {
@@ -761,7 +773,7 @@ export const multicolorIconList = [
}]
}, {
value: 'cloud',
label: '',
label: 'components.cloud',
list: [{
value: 'AWS',
label: 'AWS'
@@ -807,8 +819,11 @@ export const multicolorIconList = [
}]
}, {
value: 'system',
label: '操作系统',
label: 'components.system',
list: [{
value: 'ciase-aix',
label: 'aix'
}, {
value: 'caise-Windows',
label: 'Windows'
}, {
@@ -832,7 +847,7 @@ export const multicolorIconList = [
}]
}, {
value: 'language',
label: '语言',
label: 'components.language',
list: [{
value: 'caise-python',
label: 'python'
@@ -863,7 +878,7 @@ export const multicolorIconList = [
}]
}, {
value: 'status',
label: '状态',
label: 'components.status',
list: [{
value: 'caise-yiwen',
label: '疑问'
@@ -903,8 +918,113 @@ export const multicolorIconList = [
}]
}, {
value: 'caise-application',
label: '应用',
label: 'components.commonComponent',
list: [{
value: 'caise-websphere',
label: 'WebSphere'
}, {
value: 'caise-vps',
label: 'VPS'
}, {
value: 'caise-F5',
label: 'F5'
}, {
value: 'caise-HAProxy',
label: 'HAProxy'
}, {
value: 'caise-kafka',
label: 'kafka'
}, {
value: 'caise-dongfangtong',
label: '东方通'
}, {
value: 'cmdb-vcenter',
label: 'VCenter'
}, {
value: 'ops-KVM',
label: 'KVM'
}, {
value: 'caise-JBoss',
label: 'JBoss'
}, {
value: 'caise-weblogic',
label: 'WebLogic'
}, {
value: 'caise-disk_array',
label: '磁盘阵列'
}, {
value: 'caise-fiber',
label: '光纤交换机'
}, {
value: 'caise-bandwidth_line',
label: '带宽线路'
}, {
value: 'caise-pc',
label: 'PC'
}, {
value: 'caise-rack',
label: '机柜'
}, {
value: 'caise-computer_room',
label: '机房'
}, {
value: 'caise-ip_address',
label: 'ip地址'
}, {
value: 'caise_pool',
label: 'ip池'
}, {
value: 'caise-storage_volume1',
label: '存储卷'
}, {
value: 'caise-virtualization',
label: '虚拟化'
}, {
value: 'caise-business',
label: '业务'
}, {
value: 'caise-database',
label: '数据库'
}, {
value: 'caise-middleware',
label: '中间件'
}, {
value: 'caise-websever',
label: 'websever'
}, {
value: 'caise-message_queue',
label: '消息队列'
}, {
value: 'caise-load_balancing',
label: '负载均衡'
}, {
value: 'caise-storage_device',
label: '存储设备'
}, {
value: 'caise-network_devices',
label: '网络设备'
}, {
value: 'caise-computer',
label: '计算机'
}, {
value: 'caise-hardware',
label: '硬件设备'
}, {
value: 'caise-data_center2',
label: '数据中心'
}, {
value: 'caise-hyperV',
label: 'hyperV'
}, {
value: 'caise-IPAM',
label: 'IPAM'
}, {
value: 'caise-system',
label: '操作系统'
}, {
value: 'caise-public_cloud',
label: '公有云'
}, {
value: 'caise-data_center',
label: '数据中心'
}, {
@@ -1060,7 +1180,7 @@ export const multicolorIconList = [
}]
}, {
value: 'data',
label: '数据',
label: 'components.data',
list: [{
value: 'caise-bingzhuangtu',
label: '饼状图'

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