Compare commits

...

205 Commits

Author SHA1 Message Date
pycook
d4b661c77f fix(api): commands cmdb-patch 2024-06-21 18:22:56 +08:00
pycook
75cd7bde77 fix(api): auto discovery permission 2024-06-21 12:47:12 +08:00
Leo Song
ec912d3a65 Merge pull request #562 from veops/fix_ui_2.4.6
fix(ui): some bugs
2024-06-21 11:49:53 +08:00
songlh
42f02b4986 fix(ui): some bugs 2024-06-21 11:49:12 +08:00
simontigers
a13b999820 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
5f53b0dd0e fix(api): auto_discovery add new perms 2024-06-21 10:25:13 +08:00
Leo Song
df22085ff9 Merge pull request #560 from veops/fix_ui_topology
fix(ui): topology view error
2024-06-20 22:21:27 +08:00
LH_R
06148b402d fix(ui): topology view error 2024-06-20 22:20:25 +08:00
pycook
3fe020505a chore: release v2.4.6 2024-06-20 20:31:10 +08:00
pycook
b34e83124f perf(api): auto discovery has been upgraded (#559) 2024-06-20 20:30:04 +08:00
Leo Song
cdc52d3f80 Merge pull request #558 from veops/dev_ui_ad
fix: build error
2024-06-20 20:03:40 +08:00
LH_R
b3a80d5678 fix: build error 2024-06-20 19:54:15 +08:00
Leo Song
a2e3061bba Merge pull request #557 from veops/dev_ui_ad
feat(ui): auto discovery
2024-06-20 17:29:06 +08:00
songlh
a8eb5126ea feat(ui): auto discovery 2024-06-20 17:28:09 +08:00
pycook
adac2129fc chore: update Dockerfile-UI 2024-06-20 13:20:42 +08:00
pycook
e660c901ce chore: update Dockerfile-UI 2024-06-20 11:07:57 +08:00
pycook
ff002c0a1e chore: update Dockerfile-UI 2024-06-20 09:47:56 +08:00
Leo Song
88593d6da7 Merge pull request #555 from veops/fix_ui_lint
fix(ui): lint error
2024-06-18 11:42:53 +08:00
songlh
6fa0dd5bc5 fix(ui): lint error 2024-06-18 11:42:22 +08:00
Jared Tan
3200942373 polish ci and remove es build (#553) 2024-06-18 10:31:33 +08:00
pycook
4fd705cc59 feat(api): add table c_ad_ci_type_relations 2024-06-18 10:22:04 +08:00
Jared Tan
74827ce187 add workflow (#552) 2024-06-18 09:29:00 +08:00
Leo Song
4ed1eb6062 Merge pull request #551 from veops/fix_bug_538
fix: issue #538
2024-06-17 14:41:51 +08:00
songlh
7792204658 fix: issue #538 2024-06-17 14:41:24 +08:00
Leo Song
8621108906 Merge pull request #550 from veops/fix_bug_operation_history
fix: operation history table
2024-06-14 17:27:49 +08:00
songlh
6437af19b9 fix: operation history table 2024-06-14 17:27:13 +08:00
Leo Song
735ddb334c Merge pull request #542 from veops/fix_issue_540
fix: issue #540
2024-06-12 15:00:08 +08:00
songlh
4a8032202e fix: issue #540 2024-06-12 14:59:14 +08:00
Leo Song
c7acea6422 Merge pull request #539 from veops/fix_computed_code
fix: computed code area tab
2024-06-11 15:03:20 +08:00
songlh
ac4c93de8e fix: computed code area tab 2024-06-11 15:02:37 +08:00
pycook
8d044cf935 chore(docker compose): add api health check 2024-06-09 20:58:27 +08:00
pycook
54747fa789 feat(ui): update iconfont 2024-06-07 10:41:26 +08:00
pycook
545f1bb30b Dev dynamic attribute (#535)
* feat: dynamic attribute

* feat(api): dynamic attribute
2024-06-07 10:39:40 +08:00
pycook
dc77bca17c feat: dynamic attribute (#534) 2024-06-07 10:29:32 +08:00
Leo Song
4973278c5a Merge pull request #532 from veops/fix_bug_530
fix: ci topo expand error
2024-06-06 14:06:26 +08:00
songlh
d1c9361e47 fix: ci topo expand error 2024-06-06 14:05:32 +08:00
Leo Song
28c57cacd9 Merge pull request #531 from veops/dev_ui_240606
feat: update topology view
2024-06-06 11:10:35 +08:00
songlh
711dcc4bd7 feat: update topology view 2024-06-06 11:08:58 +08:00
simontigers
491d3cce00 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
27354a3927 fix: decorator_perms_role_required 2024-06-04 19:23:22 +08:00
Leo Song
78495eb976 Merge pull request #528 from veops/feat/dev_ui_240604
feat(ui): update model relation
2024-06-04 12:05:42 +08:00
songlh
ae900c7d3b feat(ui): update model relation 2024-06-04 12:04:26 +08:00
pycook
50134e6a0b feat(api): attribute association supports multiple groups (#527) 2024-06-04 11:34:54 +08:00
pycook
65ef58dea9 feat: update docker-compose 2024-05-30 13:18:28 +08:00
pycook
0a2e7aa99f feat: put the mysql password in .env 2024-05-30 13:08:18 +08:00
pycook
8875e75883 fix(acl): add relation 2024-05-30 09:33:30 +08:00
pycook
2f03639c57 chore: release v2.4.5 2024-05-29 13:32:40 +08:00
pycook
49bc5d94a9 fix(api): topology view read permission 2024-05-29 11:34:02 +08:00
pycook
39354e1293 feat(ui): update components CMDBExprDrawer 2024-05-28 20:16:39 +08:00
pycook
d3714f3ecf feat(ui): relation-graph upgrade to 2.1.42 2024-05-28 20:11:53 +08:00
pycook
729a616282 feat(ui): topology view (#525) 2024-05-28 20:03:10 +08:00
fxiang21
2d3a290aa3 fix: cmdb-inner-secrets-init bug 2024-05-28 19:57:16 +08:00
pycook
9e885a5b12 feat(api): i18n update 2024-05-28 18:08:15 +08:00
simontigers
f5822d7cba Merge pull request #524 from simontigers/common_cmdb_app_perm
fix: cmdb app perms
2024-05-28 17:55:08 +08:00
hu.sima
21ea553e74 fix: cmdb app perms 2024-05-28 17:54:51 +08:00
pycook
e63038d1b6 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
d56806f511 Merge pull request #521 from simontigers/common_cmdb_app_perm
feat: CMDB add TopologyView resource
2024-05-28 16:21:25 +08:00
simontigers
7ac7fdc08e feat: CMDB add TopologyView resource 2024-05-28 16:20:56 +08:00
pycook
ba11707146 feat(ui): resource views router 2024-05-21 17:58:06 +08:00
pycook
d49dc8a067 fix(api): hot loading is blocked in development mode 2024-05-21 13:14:40 +08:00
pycook
6bfb34fe2a 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
2c7ed8c32d chore: Update local.md with instructions for setting up MySQL and Redis services (#515) 2024-05-20 13:39:05 +08:00
pycook
5b275af54e fix(api): exception when calling webhook (#516) 2024-05-20 13:22:54 +08:00
pycook
dde7ec6246 feat(ui): Resources and Preference support grouping 2024-05-19 21:55:36 +08:00
pycook
9181817e96 feat(api): my preference support grouping (#513) 2024-05-18 22:55:01 +08:00
pycook
46b54bb7f2 fix(ui): some bugs (#512) 2024-05-17 12:07:56 +08:00
pycook
fe63310c4e Dev api 240517 (#511)
* fix(api): list values delete

* fix(acl): role rebuild cache
2024-05-17 11:20:53 +08:00
pycook
27c733aa2c docs: update sql 2024-05-16 20:59:30 +08:00
pycook
2a8e9e684e fix(ui): issue#490 2024-05-02 21:28:06 +08:00
pycook
095190a785 fix(api): unique constraint (#505) 2024-05-02 21:22:40 +08:00
pycook
ef25c94b5d fix(api): permissions for CIType group editing 2024-04-29 15:18:47 +08:00
pycook
06ae1bcf13 docs: update build_api_key 2024-04-29 15:11:12 +08:00
pycook
9ead4e7d8d chore: release v2.4.4 2024-04-29 14:44:33 +08:00
pycook
994a28dd25 feat(ui): baseline rollback (#502) 2024-04-29 10:10:07 +08:00
simontigers
74b587e46c Merge pull request #501 from simontigers/common_decorator_perms
fix: role base app perm
2024-04-29 09:27:36 +08:00
hu.sima
091cd882bd fix: role base app perm 2024-04-29 09:26:23 +08:00
simontigers
73093db467 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
66e268ce68 fix(api): decorator_perms_role_required 2024-04-28 19:41:50 +08:00
simontigers
a41d1a5e97 Merge pull request #499 from simontigers/common_decorator_perms
feat(api): role perm
2024-04-28 19:22:43 +08:00
hu.sima
b4b728fe28 feat(api): role perm 2024-04-28 19:22:10 +08:00
pycook
d16462d8b7 feat(api): ci baseline rollback (#498) 2024-04-28 19:19:14 +08:00
kdyq007
de7d98c0b4 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
51332c7236 feat(ui): CI change logs related itsm 2024-04-24 20:09:59 +08:00
dagongren
bf1076fe4a feat:update cs && update style (#488) 2024-04-23 12:20:27 +08:00
dagongren
3454a98cfb fix(cmdb-ui):service tree search (#487) 2024-04-19 13:32:12 +08:00
dagongren
506dcbb40e fix(cmdb-ui):fix service tree change table page (#486) 2024-04-19 11:46:51 +08:00
dagongren
5ac4517187 style (#482) 2024-04-18 10:49:39 +08:00
pycook
761e98884b chore: add volumes cmdb_cache-data in docker-compose 2024-04-18 10:02:57 +08:00
pycook
073654624e fix(api): commands cmdb-init-cache 2024-04-17 21:37:18 +08:00
dagongren
df54244ff1 fix(cmdb-ui):service tree key (#480) 2024-04-17 20:42:16 +08:00
pycook
27e9919198 chore: release v2.4.3 2024-04-17 19:35:35 +08:00
dagongren
dc8b1a5de2 feat(cmdb-ui):citype show attr && service tree search (#479) 2024-04-17 17:59:21 +08:00
pycook
d8a7728f1d feat(api): custom attribute display (#478) 2024-04-17 17:50:46 +08:00
simontigers
82881965fb Merge pull request #474 from simontigers/common_check_new_columns
Common check new columns
2024-04-16 15:35:15 +08:00
hu.sima
1bb62022f1 fix(api): check new column support enum change 2024-04-16 15:34:03 +08:00
hu.sima
ed445a8d82 fix(api): secrets_shares Import ERROR 2024-04-16 15:33:36 +08:00
pycook
3626b1a97e feat(api): service tree search by keywords (#471) 2024-04-15 20:04:56 +08:00
loveiwei
32529fba9b 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
a042b4fe39 fix(cmdb-ui):ci detail relation repeatly ciid (#468) 2024-04-15 13:50:50 +08:00
dagongren
a0631414dc style: global static.less (#467) 2024-04-12 15:18:52 +08:00
pycook
5266cb5b88 release: 2.4.2 2024-04-03 15:55:13 +08:00
dagongren
c7d4bec988 feat(cmdb-ui): attributes relation (#463) 2024-04-03 15:27:54 +08:00
pycook
099ddd6ca9 feat(api): rebuild relation by attribute (#462) 2024-04-03 15:13:43 +08:00
pycook
bd813174b1 feat(api): build relation by attributes (#461) 2024-04-02 09:19:51 +08:00
dagongren
0a43680d6e feat:add icons (#460) 2024-04-01 17:37:00 +08:00
dagongren
976c6cfe91 fix:topmenu shake & change logo (#459) 2024-04-01 15:11:24 +08:00
pycook
cf594f04ba fix(api): import CIType 2024-03-29 15:50:07 +08:00
ivonGwy
4232094aed fix: discover scripts (#458)
Co-authored-by: wang-liang0615 <dhuwl0615@163.com>
2024-03-29 15:48:46 +08:00
dagongren
d08827d086 style and 文案变更 (#457) 2024-03-29 15:02:18 +08:00
pycook
d25ae532cd fix(api): CIType template import 2024-03-29 14:20:56 +08:00
pycook
9fbb6ee64d docs: docker-compose changed to docker compose 2024-03-29 13:27:23 +08:00
pycook
b62f0e96fd Merge branch 'master' of github.com:veops/cmdb 2024-03-29 13:14:00 +08:00
pycook
c1bcd0ce45 fix(acl): del resource 2024-03-29 13:13:38 +08:00
dagongren
c8b55c34eb i18n (#456) 2024-03-29 13:11:52 +08:00
pycook
4b5906770f release: v2.4.1 2024-03-29 12:47:23 +08:00
dagongren
4188ac7252 i18n (#455) 2024-03-29 12:23:23 +08:00
dagongren
2efbc6474a style && service tree define (#454) 2024-03-29 11:53:43 +08:00
pycook
03eac0c4d2 pref(api): error tips for out of range value (#453) 2024-03-29 11:46:50 +08:00
dagongren
2a861250eb fix:icon/filter/router...and some bugs (#451) 2024-03-29 10:50:14 +08:00
dagongren
8fc19d8b7c icon font && opsTable (#450) 2024-03-28 20:59:32 +08:00
dagongren
430d2ff6d0 fix:cmdbgrant (#449) 2024-03-28 20:16:47 +08:00
dagongren
2517009d70 fix:Login (#448) 2024-03-28 19:56:54 +08:00
dagongren
67da360d80 Dev UI 240328 (#447)
* feat:ui 全面升级

* feat:ui全面升级
2024-03-28 19:53:54 +08:00
dagongren
24c56fb259 feat:ui 全面升级 (#446)
* feat:ui 全面升级

* feat:ui全面升级
2024-03-28 19:47:46 +08:00
pycook
37d5da65de 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
2224ebd533 feat:ui 全面升级 (#444) 2024-03-28 18:38:15 +08:00
pycook
bf6331d215 fix(api): batch import ci relation 2024-03-26 20:38:39 +08:00
pycook
b18b90ab4e fix(api): import CIType
fix(api): import CIType
2024-03-26 16:53:10 +08:00
pycook
702e17a7a4 fix(api): revoke service tree node permissions
fix(api): revoke service tree node permissions
2024-03-26 12:05:22 +08:00
pycook
a7586aa140 feat(api): support service tree editing (#437) 2024-03-26 10:58:11 +08:00
simontigers
ad3f96431c 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
1515820713 fix(api): common_employee_edit department in acl role 2024-03-25 11:46:04 +08:00
simontigers
7728b57878 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
a419eefd72 fix(api): check file ext with magic 2024-03-25 11:16:04 +08:00
simontigers
a44e5f6cf1 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
7d46e92c2d fix(api): common check new columns 2024-03-22 16:48:16 +08:00
pycook
4117cf87ec Merge branch 'master' of github.com:veops/cmdb 2024-03-20 11:56:49 +08:00
pycook
9e0fe0b818 fix: custom dashboard 2024-03-20 11:56:39 +08:00
dagongren
2a8f1ab9a4 style:update global.less (#426) 2024-03-19 10:01:38 +08:00
pycook
c0fe99b8c7 release: 2.3.13 2024-03-18 20:35:51 +08:00
dagongren
42feb4b862 feat(cmdb-ui):service tree grant (#425) 2024-03-18 19:59:16 +08:00
pycook
482d34993b 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
7ff309b8b8 fix(api): edit employee depart with rid=0 (#420) 2024-03-12 17:46:50 +08:00
rustrover
98eb47d44f 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
9ab0f624ef fix(api): remove ACL resources when deleting CIType (#414) 2024-03-08 16:31:03 +08:00
pycook
3f3eda8b3c fix(api): issule #412, unique value restrictions (#413) 2024-03-05 16:21:27 +08:00
pycook
f788adc8cf feat(api): multi-id search (#411)
_id:(id1;id2)
2024-03-04 15:15:34 +08:00
simontigers
693ae4ff05 fix: deploy init common (#407) 2024-03-01 17:21:32 +08:00
pycook
a1a9d99eb4 release: v2.3.12 2024-03-01 17:04:38 +08:00
dagongren
e045e0fb43 fix(cmdb-ui):to lowercase (#406) 2024-03-01 13:52:30 +08:00
pycook
09376dbd2b feat(api): CIType inheritance (#405) 2024-03-01 13:51:13 +08:00
dagongren
7fda5a1e7b feat(cmdb-ui):ci type inherit (#404) 2024-03-01 13:39:20 +08:00
dagongren
113b84763f feat:ci detail share (#403) 2024-02-27 16:13:28 +08:00
dagongren
190170acad fix(cmdb-ui):triggers webhook headers (#402) 2024-02-26 13:46:40 +08:00
pycook
513d2af4b8 feat(api): Remove many-to-many restrictions (#401) 2024-02-26 10:17:53 +08:00
pycook
4588bd8996 fix(api): db-setup commands (#399)
fix(api): db-setup commands
2024-02-23 11:05:11 +08:00
dagongren
082da5fade fix(cmdb-ui):resource search common attrs (#397) 2024-02-22 16:19:12 +08:00
pycook
013b116eb5 feat(acl): login channel add ssh options (#396) 2024-02-21 18:10:44 +08:00
simontigers
208d29165b fix: grant common perm after create new employee (#394) 2024-02-04 13:48:02 +08:00
dagongren
d510330cde fix(cmdb-ui):fix multiple default value (#395) 2024-02-04 11:49:44 +08:00
wang-liang0615
ea4f0fc2a5 fix(ui):login email-》username (#393) 2024-01-31 15:52:27 +08:00
pycook
9bcdaacdc4 docs: update init sql
docs: update init sql
2024-01-26 13:57:36 +08:00
pycook
5045581ddf feat(api): Auto-increment id can be used as primary key (#391) 2024-01-26 13:12:17 +08:00
simontigers
232913172c fix: change common_setting task queue (#390) 2024-01-25 17:39:52 +08:00
pycook
157e1809ed release: 2.3.11 2024-01-13 15:06:51 +08:00
pycook
ab8ccf7d1b ui: lint 2024-01-12 18:21:47 +08:00
wang-liang0615
ae1f0f6b4f lint regexSelect (#382) 2024-01-12 18:09:03 +08:00
wang-liang0615
ff47e0ade6 feat:citype regex check & pref:edit is_list (#380) 2024-01-12 17:09:44 +08:00
pycook
0dd272fb04 feat(api): password supports regular check 2024-01-12 16:56:10 +08:00
pycook
6bc5c1516d feat(api): Attributes support regular check (#379)
feat(api): Attributes support regular check
2024-01-12 13:05:37 +08:00
wang-liang0615
4a4b9e6ef0 feat(cmdb-ui):preference citype order (#378) 2024-01-12 11:14:53 +08:00
pycook
6e3f9478b3 Dev api 240111 (#377)
* feat(api): My subscription supports CIType sorting

* feat(api): db change
2024-01-11 18:01:37 +08:00
pycook
691051c254 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
8f066e95a6 fix(api): grant by attr (#373) 2024-01-10 16:52:27 +08:00
pycook
75bca39bf6 fix(api): commands add-user 2024-01-10 11:56:39 +08:00
pycook
3360e4d0fe feat(db): set variable sql_mode
feat(db): set variable sql_mode
2024-01-10 10:28:15 +08:00
wang-liang0615
521fcd0ba2 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
fc113425cb pref(ui):some bugfix & some style (#369) 2024-01-09 14:48:13 +08:00
pycook
81a76a9632 fix(api): cmdb-init-acl commands (#368) 2024-01-09 10:04:53 +08:00
wang-liang0615
9ec105ca37 fix(ui):logout (#365) 2024-01-04 16:12:20 +08:00
wang-liang0615
1e1c92a3ef refactor(ui):extract pager components (#364) 2024-01-04 13:41:07 +08:00
pycook
0b3cad8215 feat: update docker-compose 2024-01-04 10:26:47 +08:00
pycook
ed41a72900 release: v2.3.10 2024-01-03 19:23:42 +08:00
wang-liang0615
27854d4e0a fix(acl-ui):operation history (#363) 2024-01-03 17:41:42 +08:00
wang-liang0615
0372459f9d fix(ui) (#362) 2024-01-03 17:09:25 +08:00
simontigers
f56378e2b5 fix(api): common edit department return (#359)
fix(api): common edit department return (#359)
2024-01-03 16:42:14 +08:00
wang-liang0615
00b276d5e7 format(ui) (#361) 2024-01-03 16:35:50 +08:00
wang-liang0615
3faefbe8d3 feat(ui):i18n (#360)
* feat(ui):i18n

* i18n

* feat(ui):i18n
2024-01-03 16:03:15 +08:00
simontigers
4261f6fb57 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
c98199e98e feat(ui):i18n (#357)
* feat(ui):i18n

* i18n
2024-01-03 14:58:47 +08:00
simontigers
82ea1ddc79 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
995e581315 fix(api): common department edit method (#355) 2024-01-03 14:26:40 +08:00
pycook
ec8f626b8f docs: update install (#354) 2024-01-03 13:58:35 +08:00
wang-liang0615
ec884c92e1 feat(ui):i18n (#352) 2024-01-03 13:29:38 +08:00
simontigers
9ee2776bdd fix(api): common i18n wide (#351)
* fix(api): common_i18n wide

* fix(api): department i18n
2024-01-03 13:26:58 +08:00
simontigers
7186bdac9c fix(api): common_i18n wide (#350) 2024-01-03 12:29:49 +08:00
wang-liang0615
0a9964375a feat(ui):add packages (#349) 2024-01-03 09:42:32 +08:00
wang-liang0615
bff45d00b6 i18n (#348) 2024-01-02 18:04:53 +08:00
wang-liang0615
e429ad59ff 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
ace160ae19 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
7036fe023c style(ui):global.less (#344) 2023-12-28 11:00:00 +08:00
wang-liang0615
341f5dba53 fix(cmdb-ui):model relation table (#343) 2023-12-27 13:24:01 +08:00
wang-liang0615
5883c1616e feat:Batch import and download templates support predefined values (#342) 2023-12-27 13:18:05 +08:00
simontigers
bee0a3a3e5 feat(api): common i18n (#340) 2023-12-26 10:06:15 +08:00
pycook
7a79a8bbf7 feat(api): i18n
feat(api): i18n
2023-12-25 21:51:44 +08:00
369 changed files with 41609 additions and 24459 deletions

6
.env Normal file
View File

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

0
.github/config.yml vendored
View File

View File

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

3
.gitignore vendored
View File

@@ -43,7 +43,8 @@ cmdb-api/api/uploaded_files
cmdb-api/migrations/versions
# Translations
*.mo
#*.mo
messages.pot
# Mr Developer
.mr.developer.cfg

View File

@@ -1,6 +1,4 @@
MYSQL_ROOT_PASSWORD ?= root
MYSQL_PORT ?= 3306
REDIS_PORT ?= 6379
include ./Makefile.variable
default: help
help: ## display this help
@@ -50,3 +48,25 @@ clean: ## remove unwanted files like .pyc's
lint: ## check style with flake8
flake8 --exclude=env .
.PHONY: lint
api-docker-build:
export DOCKER_CLI_EXPERIMENTAL=enabled ;\
! ( docker buildx ls | grep multi-platform-builder ) && docker buildx create --use --platform=$(BUILD_ARCH) --name multi-platform-builder ;\
docker buildx build \
--builder multi-platform-builder \
--platform=$(BUILD_ARCH) \
--tag $(REGISTRY)/cmdb-api:$(CMDB_DOCKER_VERSION) \
--tag $(REGISTRY)/cmdb-api:latest \
-f docker/Dockerfile-API \
.
ui-docker-build:
export DOCKER_CLI_EXPERIMENTAL=enabled ;\
! ( docker buildx ls | grep multi-platform-builder ) && docker buildx create --use --platform=$(BUILD_ARCH) --name multi-platform-builder ;\
docker buildx build \
--builder multi-platform-builder \
--platform=$(BUILD_ARCH) \
--tag $(REGISTRY)/cmdb-ui:$(CMDB_DOCKER_VERSION) \
--tag $(REGISTRY)/cmdb-ui:latest \
-f docker/Dockerfile-UI \
.

21
Makefile.variable Normal file
View File

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

View File

@@ -74,20 +74,20 @@
### Docker 一键快速构建
> 方法一
- 第一步: 先安装 docker 环境, 以及docker-compose
- 第一步: 先安装 Docker 环境, 以及Docker Compose (v2)
- 第二步: 拷贝项目
```shell
git clone https://github.com/veops/cmdb.git
```
- 第三步:进入主目录,执行:
```
docker-compose up -d
docker compose up -d
```
> 方法二, 该方法适用于linux系统
- 第一步: 先安装 docker 环境, 以及docker-compose
- 第一步: 先安装 Docker 环境, 以及Docker Compose (v2)
- 第二步: 直接使用项目根目录下的install.sh 文件进行 `安装`、`启动`、`暂停`、`查状态`、`删除`、`卸载`
```shell
curl -so install.sh https://raw.githubusercontent.com/veops/cmdb/master/install.sh
curl -so install.sh https://raw.githubusercontent.com/veops/cmdb/deploy_on_kylin_docker/install.sh
sh install.sh install
```

View File

@@ -5,8 +5,8 @@ name = "pypi"
[packages]
# Flask
Flask = "==2.3.2"
Werkzeug = ">=2.3.6"
Flask = "==2.2.5"
Werkzeug = "==2.2.3"
click = ">=5.0"
# Api
Flask-RESTful = "==0.3.10"
@@ -15,6 +15,7 @@ Flask-SQLAlchemy = "==2.5.0"
SQLAlchemy = "==1.4.49"
PyMySQL = "==1.1.0"
redis = "==4.6.0"
python-redis-lock = "==4.0.0"
# Migrations
Flask-Migrate = "==2.5.2"
# Deployment
@@ -27,13 +28,15 @@ Flask-Cors = ">=3.0.8"
ldap3 = "==2.9.1"
pycryptodome = "==3.12.0"
cryptography = ">=41.0.2"
# i18n
flask-babel = "==4.0.0"
# Caching
Flask-Caching = ">=1.0.0"
# Environment variable parsing
environs = "==4.2.0"
marshmallow = "==2.20.2"
# async tasks
celery = ">=5.3.1"
celery = "==5.3.1"
celery_once = "==3.0.1"
more-itertools = "==5.0.0"
kombu = ">=5.3.1"
@@ -63,6 +66,7 @@ hvac = "==2.0.0"
colorama = ">=0.4.6"
pycryptodomex = ">=3.19.0"
lz4 = ">=4.3.2"
python-magic = "==0.4.27"
[dev-packages]
# Testing

View File

@@ -12,12 +12,14 @@ from pathlib import Path
from flask import Flask
from flask import jsonify
from flask import make_response
from flask import request
from flask.blueprints import Blueprint
from flask.cli import click
from flask.json.provider import DefaultJSONProvider
from flask_babel.speaklater import LazyString
import api.views.entry
from api.extensions import (bcrypt, cache, celery, cors, db, es, login_manager, migrate, rd)
from api.extensions import (bcrypt, babel, cache, celery, cors, db, es, login_manager, migrate, rd)
from api.extensions import inner_secrets
from api.lib.perm.authentication.cas import CAS
from api.lib.perm.authentication.oauth2 import OAuth2
@@ -72,7 +74,7 @@ class ReverseProxy(object):
class MyJSONEncoder(DefaultJSONProvider):
def default(self, o):
if isinstance(o, (decimal.Decimal, datetime.date, datetime.time)):
if isinstance(o, (decimal.Decimal, datetime.date, datetime.time, LazyString)):
return str(o)
if isinstance(o, datetime.datetime):
@@ -117,7 +119,13 @@ def configure_upload_dir(app):
def register_extensions(app):
"""Register Flask extensions."""
def get_locale():
accept_languages = app.config.get('ACCEPT_LANGUAGES', ['en', 'zh'])
return request.accept_languages.best_match(accept_languages)
bcrypt.init_app(app)
babel.init_app(app, locale_selector=get_locale)
cache.init_app(app)
db.init_app(app)
cors.init_app(app)

View File

@@ -35,8 +35,22 @@ def add_user():
"""
from api.models.acl import App
from api.lib.perm.acl.cache import AppCache
from api.lib.perm.acl.cache import RoleCache
from api.lib.perm.acl.role import RoleCRUD
from api.lib.perm.acl.role import RoleRelationCRUD
username = click.prompt('Enter username', confirmation_prompt=False)
password = click.prompt('Enter password', hide_input=True, confirmation_prompt=True)
email = click.prompt('Enter email ', confirmation_prompt=False)
is_admin = click.prompt('Admin (Y/N) ', confirmation_prompt=False, type=bool, default=False)
UserCRUD.add(username=username, password=password, email=email)
if is_admin:
app = AppCache.get('acl') or App.create(name='acl')
acl_admin = RoleCache.get_by_name(app.id, 'acl_admin') or RoleCRUD.add_role('acl_admin', app.id, True)
rid = RoleCache.get_by_name(None, username).id
RoleRelationCRUD.add(acl_admin, acl_admin.id, [rid], app.id)

View File

@@ -5,6 +5,7 @@ import copy
import datetime
import json
import time
import uuid
import click
import requests
@@ -54,9 +55,12 @@ def cmdb_init_cache():
for cr in ci_relations:
relations.setdefault(cr.first_ci_id, {}).update({cr.second_ci_id: cr.second_ci.type_id})
if cr.ancestor_ids:
relations2.setdefault(cr.ancestor_ids, {}).update({cr.second_ci_id: cr.second_ci.type_id})
relations2.setdefault('{},{}'.format(cr.ancestor_ids, cr.first_ci_id), {}).update(
{cr.second_ci_id: cr.second_ci.type_id})
for i in relations:
relations[i] = json.dumps(relations[i])
for i in relations2:
relations2[i] = json.dumps(relations2[i])
if relations:
rd.create_or_update(relations, REDIS_PREFIX_CI_RELATION)
if relations2:
@@ -114,6 +118,8 @@ def cmdb_init_acl():
_app = AppCache.get('cmdb') or App.create(name='cmdb')
app_id = _app.id
current_app.test_request_context().push()
# 1. add resource type
for resource_type in ResourceTypeEnum.all():
try:
@@ -122,7 +128,7 @@ def cmdb_init_acl():
perms = [PermEnum.READ]
elif resource_type == ResourceTypeEnum.CI_TYPE_RELATION:
perms = [PermEnum.ADD, PermEnum.DELETE, PermEnum.GRANT]
elif resource_type == ResourceTypeEnum.RELATION_VIEW:
elif resource_type in (ResourceTypeEnum.RELATION_VIEW, ResourceTypeEnum.TOPOLOGY_VIEW):
perms = [PermEnum.READ, PermEnum.UPDATE, PermEnum.DELETE, PermEnum.GRANT]
ResourceTypeCRUD.add(app_id, resource_type, '', perms)
@@ -176,12 +182,32 @@ def cmdb_counter():
from api.lib.cmdb.cache import CMDBCounterCache
current_app.test_request_context().push()
if not UserCache.get('worker'):
from api.lib.perm.acl.user import UserCRUD
UserCRUD.add(username='worker', password=uuid.uuid4().hex, email='worker@xxx.com')
login_user(UserCache.get('worker'))
i = 0
today = datetime.date.today()
while True:
try:
db.session.remove()
CMDBCounterCache.reset()
if i % 5 == 0:
CMDBCounterCache.flush_adc_counter()
i = 0
if datetime.date.today() != today:
CMDBCounterCache.clear_ad_exec_history()
today = datetime.date.today()
CMDBCounterCache.flush_sub_counter()
i += 1
except:
import traceback
print(traceback.format_exc())
@@ -305,7 +331,7 @@ def cmdb_inner_secrets_init(address):
"""
init inner secrets for password feature
"""
res, ok = KeyManage(backend=InnerKVManger).init()
res, ok = KeyManage(backend=InnerKVManger()).init()
if not ok:
if res.get("status") == "failed":
KeyManage.print_response(res)
@@ -339,13 +365,13 @@ def cmdb_inner_secrets_unseal(address):
"""
unseal the secrets feature
"""
if not valid_address(address):
return
# if not valid_address(address):
# return
address = "{}/api/v0.1/secrets/unseal".format(address.strip("/"))
for i in range(global_key_threshold):
token = click.prompt(f'Enter unseal token {i + 1}', hide_input=True, confirmation_prompt=False)
assert token is not None
resp = requests.post(address, headers={"Unseal-Token": token})
resp = requests.post(address, headers={"Unseal-Token": token}, timeout=5)
if resp.status_code == 200:
KeyManage.print_response(resp.json())
if resp.json().get("status") in ["success", "skip"]:
@@ -472,3 +498,48 @@ def cmdb_agent_init():
click.echo("Key : {}".format(click.style(user.key, bg='red')))
click.echo("Secret: {}".format(click.style(user.secret, bg='red')))
@click.command()
@click.option(
'-v',
'--version',
help='input cmdb version, e.g. 2.4.6',
required=True,
)
@with_appcontext
def cmdb_patch(version):
"""
CMDB upgrade patch
"""
version = version[1:] if version.lower().startswith("v") else version
if version >= '2.4.6':
from api.models.cmdb import CITypeRelation
for cr in CITypeRelation.get_by(to_dict=False):
if hasattr(cr, 'parent_attr_id') and cr.parent_attr_id and not cr.parent_attr_ids:
parent_attr_ids, child_attr_ids = [cr.parent_attr_id], [cr.child_attr_id]
cr.update(parent_attr_ids=parent_attr_ids, child_attr_ids=child_attr_ids, commit=False)
db.session.commit()
from api.models.cmdb import AutoDiscoveryCIType, AutoDiscoveryCITypeRelation
from api.lib.cmdb.cache import CITypeCache, AttributeCache
for adt in AutoDiscoveryCIType.get_by(to_dict=False):
if adt.relation:
if not AutoDiscoveryCITypeRelation.get_by(ad_type_id=adt.type_id):
peer_type = CITypeCache.get(list(adt.relation.values())[0]['type_name'])
peer_type_id = peer_type and peer_type.id
peer_attr = AttributeCache.get(list(adt.relation.values())[0]['attr_name'])
peer_attr_id = peer_attr and peer_attr.id
if peer_type_id and peer_attr_id:
AutoDiscoveryCITypeRelation.create(ad_type_id=adt.type_id,
ad_key=list(adt.relation.keys())[0],
peer_type_id=peer_type_id,
peer_attr_id=peer_attr_id,
commit=False)
if hasattr(adt, 'interval') and adt.interval and not adt.cron:
adt.cron = "*/{} * * * *".format(adt.interval // 60)
db.session.commit()

View File

@@ -4,8 +4,9 @@ from flask.cli import with_appcontext
from werkzeug.datastructures import MultiDict
from api.lib.common_setting.acl import ACLManager
from api.lib.common_setting.employee import EmployeeAddForm
from api.lib.common_setting.employee import EmployeeAddForm, GrantEmployeeACLPerm
from api.lib.common_setting.resp_format import ErrFormat
from api.lib.common_setting.utils import CheckNewColumn
from api.models.common_setting import Employee, Department
@@ -158,50 +159,11 @@ class InitDepartment(object):
def init_backend_resource(self):
acl = self.check_app('backend')
resources_types = acl.get_all_resources_types()
perms = ['read', 'grant', 'delete', 'update']
acl_rid = self.get_admin_user_rid()
results = list(filter(lambda t: t['name'] == '操作权限', resources_types['groups']))
if len(results) == 0:
payload = dict(
app_id=acl.app_name,
name='操作权限',
description='',
perms=perms
)
resource_type = acl.create_resources_type(payload)
else:
resource_type = results[0]
resource_type_id = resource_type['id']
existed_perms = resources_types.get('id2perms', {}).get(resource_type_id, [])
existed_perms = [p['name'] for p in existed_perms]
new_perms = []
for perm in perms:
if perm not in existed_perms:
new_perms.append(perm)
if len(new_perms) > 0:
resource_type['perms'] = existed_perms + new_perms
acl.update_resources_type(resource_type_id, resource_type)
resource_list = acl.get_resource_by_type(None, None, resource_type['id'])
for name in ['公司信息', '公司架构', '通知设置']:
target = list(filter(lambda r: r['name'] == name, resource_list))
if len(target) == 0:
payload = dict(
type_id=resource_type['id'],
app_id=acl.app_name,
name=name,
)
resource = acl.create_resource(payload)
else:
resource = target[0]
if acl_rid > 0:
acl.grant_resource(acl_rid, resource['id'], perms)
if acl_rid == 0:
return
GrantEmployeeACLPerm(acl).grant_by_rid(acl_rid, True)
@staticmethod
def check_app(app_name):
@@ -248,57 +210,7 @@ def common_check_new_columns():
"""
add new columns to tables
"""
from api.extensions import db
from sqlalchemy import inspect, text
def get_model_by_table_name(_table_name):
registry = getattr(db.Model, 'registry', None)
class_registry = getattr(registry, '_class_registry', None)
for _model in class_registry.values():
if hasattr(_model, '__tablename__') and _model.__tablename__ == _table_name:
return _model
return None
def add_new_column(target_table_name, new_column):
column_type = new_column.type.compile(engine.dialect)
default_value = new_column.default.arg if new_column.default else None
sql = "ALTER TABLE " + target_table_name + " ADD COLUMN " + new_column.name + " " + column_type
if new_column.comment:
sql += f" comment '{new_column.comment}'"
if column_type == 'JSON':
pass
elif default_value:
if column_type.startswith('VAR') or column_type.startswith('Text'):
if default_value is None or len(default_value) == 0:
pass
else:
sql += f" DEFAULT {default_value}"
sql = text(sql)
db.session.execute(sql)
engine = db.get_engine()
inspector = inspect(engine)
table_names = inspector.get_table_names()
for table_name in table_names:
existed_columns = inspector.get_columns(table_name)
existed_column_name_list = [c['name'] for c in existed_columns]
model = get_model_by_table_name(table_name)
if model is None:
continue
model_columns = getattr(getattr(getattr(model, '__table__'), 'columns'), '_all_columns')
for column in model_columns:
if column.name not in existed_column_name_list:
try:
add_new_column(table_name, column)
current_app.logger.info(f"add new column [{column.name}] in table [{table_name}] success.")
except Exception as e:
current_app.logger.error(f"add new column [{column.name}] in table [{table_name}] err:")
current_app.logger.error(e)
CheckNewColumn().run()
@click.command()

View File

@@ -5,9 +5,7 @@ from glob import glob
from subprocess import call
import click
from flask import current_app
from flask.cli import with_appcontext
from werkzeug.exceptions import MethodNotAllowed, NotFound
from api.extensions import db
@@ -90,3 +88,53 @@ def db_setup():
"""create tables
"""
db.create_all()
try:
db.session.execute("set global sql_mode='STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,"
"ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION'")
db.session.commit()
except:
pass
try:
db.session.execute("set global tidb_enable_noop_functions='ON'")
db.session.commit()
except:
pass
@click.group()
def translate():
"""Translation and localization commands."""
@translate.command()
@click.argument('lang')
def init(lang):
"""Initialize a new language."""
if os.system('pybabel extract -F babel.cfg -k _l -o messages.pot .'):
raise RuntimeError('extract command failed')
if os.system(
'pybabel init -i messages.pot -d api/translations -l ' + lang):
raise RuntimeError('init command failed')
os.remove('messages.pot')
@translate.command()
def update():
"""Update all languages."""
if os.system('pybabel extract -F babel.cfg -k _l -o messages.pot .'):
raise RuntimeError('extract command failed')
if os.system('pybabel update -i messages.pot -d api/translations'):
raise RuntimeError('update command failed')
os.remove('messages.pot')
@translate.command()
def compile():
"""Compile all languages."""
if os.system('pybabel compile -d api/translations'):
raise RuntimeError('compile command failed')

View File

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

View File

@@ -1,34 +1,47 @@
# -*- coding:utf-8 -*-
import copy
import datetime
import json
import os
from flask import abort
from flask import current_app
from flask_login import current_user
from sqlalchemy import func
from api.extensions import db
from api.lib.cmdb.auto_discovery.const import ClOUD_MAP
from api.lib.cmdb.auto_discovery.const import DEFAULT_HTTP
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import CITypeAttributeCache
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.ci import CIManager
from api.lib.cmdb.ci import CIRelationManager
from api.lib.cmdb.ci_type import CITypeGroupManager
from api.lib.cmdb.const import AutoDiscoveryType
from api.lib.cmdb.const import CMDB_QUEUE
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci import search
from api.lib.cmdb.search.ci import search as ci_search
from api.lib.common_setting.role_perm_base import CMDBApp
from api.lib.mixin import DBMixin
from api.lib.perm.acl.acl import ACLManager
from api.lib.perm.acl.acl import is_app_admin
from api.lib.perm.acl.acl import validate_permission
from api.lib.utils import AESCrypto
from api.models.cmdb import AutoDiscoveryCI
from api.models.cmdb import AutoDiscoveryCIType
from api.models.cmdb import AutoDiscoveryCITypeRelation
from api.models.cmdb import AutoDiscoveryCounter
from api.models.cmdb import AutoDiscoveryExecHistory
from api.models.cmdb import AutoDiscoveryRule
from flask import abort
from flask import current_app
from flask_login import current_user
from sqlalchemy import func
from api.models.cmdb import AutoDiscoveryRuleSyncHistory
from api.tasks.cmdb import write_ad_rule_sync_history
PWD = os.path.abspath(os.path.dirname(__file__))
app_cli = CMDBApp()
def parse_plugin_script(script):
@@ -100,6 +113,22 @@ class AutoDiscoveryRuleCRUD(DBMixin):
self.cls.get_by(name=kwargs['name']) and abort(400, ErrFormat.adr_duplicate.format(kwargs['name']))
if kwargs.get('is_plugin') and kwargs.get('plugin_script'):
kwargs = check_plugin_script(**kwargs)
acl = ACLManager(app_cli.app_name)
has_perm = True
try:
if not acl.has_permission(app_cli.op.Auto_Discovery,
app_cli.resource_type_name,
app_cli.op.create_plugin) and not is_app_admin(app_cli.app_name):
has_perm = False
except Exception:
if not is_app_admin(app_cli.app_name):
return abort(403, ErrFormat.role_required.format(app_cli.admin_name))
if not has_perm:
return abort(403, ErrFormat.no_permission.format(
app_cli.op.Auto_Discovery, app_cli.op.create_plugin))
kwargs['owner'] = current_user.uid
return kwargs
@@ -115,6 +144,22 @@ class AutoDiscoveryRuleCRUD(DBMixin):
if other and other.id != existed.id:
return abort(400, ErrFormat.adr_duplicate.format(kwargs['name']))
if existed.is_plugin:
acl = ACLManager(app_cli.app_name)
has_perm = True
try:
if not acl.has_permission(app_cli.op.Auto_Discovery,
app_cli.resource_type_name,
app_cli.op.update_plugin) and not is_app_admin(app_cli.app_name):
has_perm = False
except Exception:
if not is_app_admin(app_cli.app_name):
return abort(403, ErrFormat.role_required.format(app_cli.admin_name))
if not has_perm:
return abort(403, ErrFormat.no_permission.format(
app_cli.op.Auto_Discovery, app_cli.op.update_plugin))
return existed
def update(self, _id, **kwargs):
@@ -122,13 +167,35 @@ class AutoDiscoveryRuleCRUD(DBMixin):
if kwargs.get('is_plugin') and kwargs.get('plugin_script'):
kwargs = check_plugin_script(**kwargs)
for item in AutoDiscoveryCIType.get_by(adr_id=_id, to_dict=False):
item.update(updated_at=datetime.datetime.now())
return super(AutoDiscoveryRuleCRUD, self).update(_id, filter_none=False, **kwargs)
def _can_delete(self, **kwargs):
if AutoDiscoveryCIType.get_by(adr_id=kwargs['_id'], first=True):
return abort(400, ErrFormat.adr_referenced)
return self._can_update(**kwargs)
existed = self.cls.get_by_id(kwargs['_id']) or abort(
404, ErrFormat.adr_not_found.format("id={}".format(kwargs['_id'])))
if existed.is_plugin:
acl = ACLManager(app_cli.app_name)
has_perm = True
try:
if not acl.has_permission(app_cli.op.Auto_Discovery,
app_cli.resource_type_name,
app_cli.op.delete_plugin) and not is_app_admin(app_cli.app_name):
has_perm = False
except Exception:
if not is_app_admin(app_cli.app_name):
return abort(403, ErrFormat.role_required.format(app_cli.admin_name))
if not has_perm:
return abort(403, ErrFormat.no_permission.format(
app_cli.op.Auto_Discovery, app_cli.op.delete_plugin))
return existed
class AutoDiscoveryCITypeCRUD(DBMixin):
@@ -147,14 +214,34 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
return cls.cls.get_by(type_id=type_id, to_dict=False)
@classmethod
def get(cls, ci_id, oneagent_id, last_update_at=None):
def get_ad_attributes(cls, type_id):
result = []
adts = cls.get_by_type_id(type_id)
for adt in adts:
adr = AutoDiscoveryRuleCRUD.get_by_id(adt.adr_id)
if not adr:
continue
if adr.type == "http":
for i in DEFAULT_HTTP:
if adr.name == i['name']:
attrs = AutoDiscoveryHTTPManager.get_attributes(
i['en'], (adt.extra_option or {}).get('category')) or []
result.extend([i.get('name') for i in attrs])
break
elif adr.type == "snmp":
attributes = AutoDiscoverySNMPManager.get_attributes()
result.extend([i.get('name') for i in (attributes or [])])
else:
result.extend([i.get('name') for i in (adr.attributes or [])])
return sorted(list(set(result)))
@classmethod
def get(cls, ci_id, oneagent_id, oneagent_name, last_update_at=None):
result = []
rules = cls.cls.get_by(to_dict=True)
for rule in rules:
if rule.get('relation'):
continue
if isinstance(rule.get("extra_option"), dict) and rule['extra_option'].get('secret'):
if not (current_user.username == "cmdb_agent" or current_user.uid == rule['uid']):
rule['extra_option'].pop('secret', None)
@@ -165,7 +252,7 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
result.append(rule)
elif rule['query_expr']:
query = rule['query_expr'].lstrip('q').lstrip('=')
s = search(query, fl=['_id'], count=1000000)
s = ci_search(query, fl=['_id'], count=1000000)
try:
response, _, _, _, _, _ = s.search()
except SearchError as e:
@@ -182,9 +269,6 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
if adr.type in (AutoDiscoveryType.SNMP, AutoDiscoveryType.HTTP):
continue
if not rule['updated_at']:
continue
result.append(rule)
new_last_update_at = ""
@@ -195,6 +279,9 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
if new_last_update_at < __last_update_at:
new_last_update_at = __last_update_at
write_ad_rule_sync_history.apply_async(args=(result, oneagent_id, oneagent_name, datetime.datetime.now()),
queue=CMDB_QUEUE)
if not last_update_at or new_last_update_at > last_update_at:
return result, new_last_update_at
else:
@@ -213,7 +300,7 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
agent_id = agent_id.strip()
q = "op_duty:{0},-rd_duty:{0},oneagent_id:{1}"
s = search(q.format(current_user.username, agent_id.strip()))
s = ci_search(q.format(current_user.username, agent_id.strip()))
try:
response, _, _, _, _, _ = s.search()
if response:
@@ -222,7 +309,7 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
current_app.logger.warning(e)
return abort(400, str(e))
s = search(q.format(current_user.nickname, agent_id.strip()))
s = ci_search(q.format(current_user.nickname, agent_id.strip()))
try:
response, _, _, _, _, _ = s.search()
if response:
@@ -236,7 +323,7 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
if query_expr.startswith('q='):
query_expr = query_expr[2:]
s = search(query_expr, count=1000000)
s = ci_search(query_expr, count=1000000)
try:
response, _, _, _, _, _ = s.search()
for i in response:
@@ -254,13 +341,21 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
def _can_add(**kwargs):
if kwargs.get('adr_id'):
AutoDiscoveryRule.get_by_id(kwargs['adr_id']) or abort(
adr = AutoDiscoveryRule.get_by_id(kwargs['adr_id']) or abort(
404, ErrFormat.adr_not_found.format("id={}".format(kwargs['adr_id'])))
# if not adr.is_plugin:
# other = self.cls.get_by(adr_id=adr.id, first=True, to_dict=False)
# if other:
# ci_type = CITypeCache.get(other.type_id)
# return abort(400, ErrFormat.adr_default_ref_once.format(ci_type.alias))
if adr.type == "http":
kwargs.setdefault('extra_option', dict)
en_name = None
for i in DEFAULT_HTTP:
if i['name'] == adr.name:
en_name = i['en']
break
if en_name and kwargs['extra_option'].get('category'):
for item in ClOUD_MAP[en_name]:
if item["collect_key_map"].get(kwargs['extra_option']['category']):
kwargs["extra_option"]["collect_key"] = item["collect_key_map"][
kwargs['extra_option']['category']]
break
if kwargs.get('is_plugin') and kwargs.get('plugin_script'):
kwargs = check_plugin_script(**kwargs)
@@ -268,6 +363,11 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
if isinstance(kwargs.get('extra_option'), dict) and kwargs['extra_option'].get('secret'):
kwargs['extra_option']['secret'] = AESCrypto.encrypt(kwargs['extra_option']['secret'])
ci_type = CITypeCache.get(kwargs['type_id'])
unique = AttributeCache.get(ci_type.unique_id)
if unique and unique.name not in (kwargs.get('attributes') or {}).values():
return abort(400, ErrFormat.ad_not_unique_key.format(unique.name))
kwargs['uid'] = current_user.uid
return kwargs
@@ -276,7 +376,29 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
existed = self.cls.get_by_id(kwargs['_id']) or abort(
404, ErrFormat.ad_not_found.format("id={}".format(kwargs['_id'])))
self.__valid_exec_target(kwargs.get('agent_id'), kwargs.get('query_expr'))
adr = AutoDiscoveryRule.get_by_id(existed.adr_id) or abort(
404, ErrFormat.adr_not_found.format("id={}".format(existed.adr_id)))
if adr.type == "http":
kwargs.setdefault('extra_option', dict)
en_name = None
for i in DEFAULT_HTTP:
if i['name'] == adr.name:
en_name = i['en']
break
if en_name and kwargs['extra_option'].get('category'):
for item in ClOUD_MAP[en_name]:
if item["collect_key_map"].get(kwargs['extra_option']['category']):
kwargs["extra_option"]["collect_key"] = item["collect_key_map"][
kwargs['extra_option']['category']]
break
if 'attributes' in kwargs:
self.__valid_exec_target(kwargs.get('agent_id'), kwargs.get('query_expr'))
ci_type = CITypeCache.get(existed.type_id)
unique = AttributeCache.get(ci_type.unique_id)
if unique and unique.name not in (kwargs.get('attributes') or {}).values():
return abort(400, ErrFormat.ad_not_unique_key.format(unique.name))
if isinstance(kwargs.get('extra_option'), dict) and kwargs['extra_option'].get('secret'):
if current_user.uid != existed.uid:
@@ -292,7 +414,15 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
if isinstance(kwargs.get('extra_option'), dict) and kwargs['extra_option'].get('secret'):
kwargs['extra_option']['secret'] = AESCrypto.encrypt(kwargs['extra_option']['secret'])
return super(AutoDiscoveryCITypeCRUD, self).update(_id, filter_none=False, **kwargs)
inst = self._can_update(_id=_id, **kwargs)
if inst.agent_id != kwargs.get('agent_id') or inst.query_expr != kwargs.get('query_expr'):
for item in AutoDiscoveryRuleSyncHistory.get_by(adt_id=inst.id, to_dict=False):
item.delete(commit=False)
db.session.commit()
obj = inst.update(_id=_id, filter_none=False, **kwargs)
return obj
def _can_delete(self, **kwargs):
if AutoDiscoveryCICRUD.get_by_adt_id(kwargs['_id']):
@@ -303,6 +433,56 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
return existed
def delete(self, _id):
inst = self._can_delete(_id=_id)
inst.soft_delete()
for item in AutoDiscoveryRuleSyncHistory.get_by(adt_id=inst.id, to_dict=False):
item.delete(commit=False)
db.session.commit()
attributes = self.get_ad_attributes(inst.type_id)
for item in AutoDiscoveryCITypeRelationCRUD.get_by_type_id(inst.type_id):
if item.ad_key not in attributes:
item.soft_delete()
return inst
class AutoDiscoveryCITypeRelationCRUD(DBMixin):
cls = AutoDiscoveryCITypeRelation
@classmethod
def get_by_type_id(cls, type_id, to_dict=False):
return cls.cls.get_by(ad_type_id=type_id, to_dict=to_dict)
def upsert(self, ad_type_id, relations):
existed = self.cls.get_by(ad_type_id=ad_type_id, to_dict=False)
existed = {(i.ad_key, i.peer_type_id, i.peer_attr_id): i for i in existed}
new = []
for r in relations:
k = (r.get('ad_key'), r.get('peer_type_id'), r.get('peer_attr_id'))
if len(list(filter(lambda x: x, k))) == 3 and k not in existed:
self.cls.create(ad_type_id=ad_type_id, **r)
new.append(k)
for deleted in set(existed.keys()) - set(new):
existed[deleted].soft_delete()
return self.get_by_type_id(ad_type_id, to_dict=True)
def _can_add(self, **kwargs):
pass
def _can_update(self, **kwargs):
pass
def _can_delete(self, **kwargs):
pass
class AutoDiscoveryCICRUD(DBMixin):
cls = AutoDiscoveryCI
@@ -330,15 +510,15 @@ class AutoDiscoveryCICRUD(DBMixin):
@staticmethod
def get_attributes_by_type_id(type_id):
from api.lib.cmdb.cache import CITypeAttributesCache
attributes = [i[1] for i in CITypeAttributesCache.get2(type_id) or []]
from api.lib.cmdb.ci_type import CITypeAttributeManager
attributes = [i for i in CITypeAttributeManager.get_attributes_by_type_id(type_id) or []]
attr_names = set()
adts = AutoDiscoveryCITypeCRUD.get_by_type_id(type_id)
for adt in adts:
attr_names |= set((adt.attributes or {}).values())
return [attr.to_dict() for attr in attributes if attr.name in attr_names]
return [attr for attr in attributes if attr['name'] in attr_names]
@classmethod
def search(cls, page, page_size, fl=None, **kwargs):
@@ -391,16 +571,24 @@ class AutoDiscoveryCICRUD(DBMixin):
changed = False
if existed is not None:
if existed.instance != kwargs['instance']:
instance = copy.deepcopy(existed.instance) or {}
instance.update(kwargs['instance'])
kwargs['instance'] = instance
existed.update(filter_none=False, **kwargs)
AutoDiscoveryExecHistoryCRUD().add(type_id=adt.type_id,
stdout="update resource: {}".format(kwargs.get('unique_value')))
changed = True
else:
existed = self.cls.create(**kwargs)
AutoDiscoveryExecHistoryCRUD().add(type_id=adt.type_id,
stdout="add resource: {}".format(kwargs.get('unique_value')))
changed = True
if adt.auto_accept and changed:
try:
self.accept(existed)
except Exception as e:
current_app.logger.error(e)
return abort(400, str(e))
elif changed:
existed.update(is_accept=False, accept_time=None, accept_by=None, filter_none=False)
@@ -420,6 +608,13 @@ class AutoDiscoveryCICRUD(DBMixin):
inst.delete()
adt = AutoDiscoveryCIType.get_by_id(inst.adt_id)
if adt:
adt.update(updated_at=datetime.datetime.now())
AutoDiscoveryExecHistoryCRUD().add(type_id=inst.type_id,
stdout="delete resource: {}".format(inst.unique_value))
self._after_delete(inst)
return inst
@@ -435,6 +630,13 @@ class AutoDiscoveryCICRUD(DBMixin):
not is_app_admin("cmdb") and validate_permission(ci_type.name, ResourceTypeEnum.CI, PermEnum.DELETE, "cmdb")
existed.delete()
adt = AutoDiscoveryCIType.get_by_id(existed.adt_id)
if adt:
adt.update(updated_at=datetime.datetime.now())
AutoDiscoveryExecHistoryCRUD().add(type_id=type_id,
stdout="delete resource: {}".format(unique_value))
# TODO: delete ci
@classmethod
@@ -447,32 +649,34 @@ class AutoDiscoveryCICRUD(DBMixin):
ci_id = None
if adt.attributes:
ci_dict = {adt.attributes[k]: v for k, v in adc.instance.items() if k in adt.attributes}
ci_id = CIManager.add(adc.type_id, is_auto_discovery=True, **ci_dict)
ci_id = CIManager.add(adc.type_id, is_auto_discovery=True, _is_admin=True, **ci_dict)
AutoDiscoveryExecHistoryCRUD().add(type_id=adt.type_id,
stdout="accept resource: {}".format(adc.unique_value))
relation_adts = AutoDiscoveryCIType.get_by(type_id=adt.type_id, adr_id=None, to_dict=False)
for r_adt in relation_adts:
if not r_adt.relation or ci_id is None:
relation_ads = AutoDiscoveryCITypeRelation.get_by(ad_type_id=adt.type_id, to_dict=False)
for r_adt in relation_ads:
ad_key = r_adt.ad_key
if not adc.instance.get(ad_key):
continue
for ad_key in r_adt.relation:
if not adc.instance.get(ad_key):
continue
cmdb_key = r_adt.relation[ad_key]
query = "_type:{},{}:{}".format(cmdb_key.get('type_name'), cmdb_key.get('attr_name'),
adc.instance.get(ad_key))
s = search(query)
ad_key_values = [adc.instance.get(ad_key)] if not isinstance(
adc.instance.get(ad_key), list) else adc.instance.get(ad_key)
for ad_key_value in ad_key_values:
query = "_type:{},{}:{}".format(r_adt.peer_type_id, r_adt.peer_attr_id, ad_key_value)
s = ci_search(query, use_ci_filter=False, count=1000000)
try:
response, _, _, _, _, _ = s.search()
except SearchError as e:
current_app.logger.warning(e)
return abort(400, str(e))
relation_ci_id = response and response[0]['_id']
if relation_ci_id:
for relation_ci in response:
relation_ci_id = relation_ci['_id']
try:
CIRelationManager.add(ci_id, relation_ci_id)
CIRelationManager.add(ci_id, relation_ci_id, valid=False)
except:
try:
CIRelationManager.add(relation_ci_id, ci_id)
CIRelationManager.add(relation_ci_id, ci_id, valid=False)
except:
pass
@@ -485,14 +689,35 @@ class AutoDiscoveryCICRUD(DBMixin):
class AutoDiscoveryHTTPManager(object):
@staticmethod
def get_categories(name):
return (ClOUD_MAP.get(name) or {}).get('categories') or []
categories = (ClOUD_MAP.get(name) or {}) or []
for item in copy.deepcopy(categories):
item.pop('map', None)
return categories
def get_resources(self, name):
en_name = None
for i in DEFAULT_HTTP:
if i['name'] == name:
en_name = i['en']
break
if en_name:
categories = self.get_categories(en_name)
return [j for i in categories for j in i['items']]
return []
@staticmethod
def get_attributes(name, category):
tpt = ((ClOUD_MAP.get(name) or {}).get('map') or {}).get(category)
if tpt and os.path.exists(os.path.join(PWD, tpt)):
with open(os.path.join(PWD, tpt)) as f:
return json.loads(f.read())
def get_attributes(provider, resource):
for item in (ClOUD_MAP.get(provider) or {}):
for _resource in (item.get('map') or {}):
if _resource == resource:
tpt = item['map'][_resource]
if tpt and os.path.exists(os.path.join(PWD, tpt)):
with open(os.path.join(PWD, tpt)) as f:
return json.loads(f.read())
return []
@@ -506,3 +731,62 @@ class AutoDiscoverySNMPManager(object):
return json.loads(f.read())
return []
class AutoDiscoveryRuleSyncHistoryCRUD(DBMixin):
cls = AutoDiscoveryRuleSyncHistory
def _can_add(self, **kwargs):
pass
def _can_update(self, **kwargs):
pass
def _can_delete(self, **kwargs):
pass
def upsert(self, **kwargs):
existed = self.cls.get_by(adt_id=kwargs.get('adt_id'),
oneagent_id=kwargs.get('oneagent_id'),
oneagent_name=kwargs.get('oneagent_name'),
first=True,
to_dict=False)
if existed is not None:
existed.update(**kwargs)
else:
self.cls.create(**kwargs)
class AutoDiscoveryExecHistoryCRUD(DBMixin):
cls = AutoDiscoveryExecHistory
def _can_add(self, **kwargs):
pass
def _can_update(self, **kwargs):
pass
def _can_delete(self, **kwargs):
pass
class AutoDiscoveryCounterCRUD(DBMixin):
cls = AutoDiscoveryCounter
def get(self, type_id):
res = self.cls.get_by(type_id=type_id, first=True, to_dict=True)
if res is None:
return dict(rule_count=0, exec_target_count=0, instance_count=0, accept_count=0,
this_month_count=0, this_week_count=0, last_month_count=0, last_week_count=0)
return res
def _can_add(self, **kwargs):
pass
def _can_update(self, **kwargs):
pass
def _can_delete(self, **kwargs):
pass

View File

@@ -3,13 +3,13 @@
from api.lib.cmdb.const import AutoDiscoveryType
DEFAULT_HTTP = [
dict(name="阿里云", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False,
dict(name="阿里云", en="aliyun", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-aliyun'}}),
dict(name="腾讯云", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False,
dict(name="腾讯云", en="tencentcloud", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-tengxunyun'}}),
dict(name="华为云", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False,
dict(name="华为云", en="huaweicloud", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-huaweiyun'}}),
dict(name="AWS", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False,
dict(name="AWS", en="aws", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-aws'}}),
dict(name="交换机", type=AutoDiscoveryType.SNMP, is_inner=True, is_plugin=False,
@@ -23,31 +23,47 @@ DEFAULT_HTTP = [
]
ClOUD_MAP = {
"aliyun": {
"categories": ["云服务器 ECS"],
"aliyun": [{
"category": "计算",
"items": ["云服务器 ECS"],
"map": {
"云服务器 ECS": "templates/aliyun_ecs.json",
},
"collect_key_map": {
"云服务器 ECS": "ali.ecs",
}
},
}],
"tencentcloud": {
"categories": ["云服务器 CVM"],
"tencentcloud": [{
"category": "计算",
"items": ["云服务器 CVM"],
"map": {
"云服务器 CVM": "templates/tencent_cvm.json",
},
"collect_key_map": {
"云服务器 CVM": "tencent.cvm",
}
},
}],
"huaweicloud": {
"categories": ["云服务器 ECS"],
"huaweicloud": [{
"category": "计算",
"items": ["云服务器 ECS"],
"map": {
"云服务器 ECS": "templates/huaweicloud_ecs.json",
},
"collect_key_map": {
"云服务器 ECS": "huawei.ecs",
}
},
}],
"aws": {
"categories": ["云服务器 EC2"],
"aws": [{
"category": "计算",
"items": ["云服务器 EC2"],
"map": {
"云服务器 EC2": "templates/aws_ec2.json",
},
"collect_key_map": {
"云服务器 EC2": "aws.ec2",
}
},
}],
}

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@@ -2,13 +2,24 @@
from __future__ import unicode_literals
import datetime
from flask import current_app
from api.extensions import cache
from api.extensions import db
from api.lib.cmdb.custom_dashboard import CustomDashboardManager
from api.models.cmdb import Attribute
from api.models.cmdb import Attribute, AutoDiscoveryExecHistory
from api.models.cmdb import AutoDiscoveryCI
from api.models.cmdb import AutoDiscoveryCIType
from api.models.cmdb import AutoDiscoveryCITypeRelation
from api.models.cmdb import AutoDiscoveryCounter
from api.models.cmdb import AutoDiscoveryRuleSyncHistory
from api.models.cmdb import CI
from api.models.cmdb import CIType
from api.models.cmdb import CITypeAttribute
from api.models.cmdb import PreferenceShowAttributes
from api.models.cmdb import PreferenceTreeView
from api.models.cmdb import RelationType
@@ -226,7 +237,9 @@ class CITypeAttributeCache(object):
class CMDBCounterCache(object):
KEY = 'CMDB::Counter'
KEY = 'CMDB::Counter::dashboard'
KEY2 = 'CMDB::Counter::adc'
KEY3 = 'CMDB::Counter::sub'
@classmethod
def get(cls):
@@ -303,7 +316,7 @@ class CMDBCounterCache(object):
s = RelSearch([i[0] for i in type_id_names], level, other_filer or '')
try:
stats = s.statistics(type_ids)
stats = s.statistics(type_ids, need_filter=False)
except SearchError as e:
current_app.logger.error(e)
return
@@ -429,3 +442,107 @@ class CMDBCounterCache(object):
return
return numfound
@classmethod
def flush_adc_counter(cls):
res = db.session.query(CI.type_id, CI.is_auto_discovery)
result = dict()
for i in res:
result.setdefault(i.type_id, dict(total=0, auto_discovery=0))
result[i.type_id]['total'] += 1
if i.is_auto_discovery:
result[i.type_id]['auto_discovery'] += 1
cache.set(cls.KEY2, result, timeout=0)
res = db.session.query(AutoDiscoveryCI.created_at,
AutoDiscoveryCI.updated_at,
AutoDiscoveryCI.adt_id,
AutoDiscoveryCI.type_id,
AutoDiscoveryCI.is_accept).filter(AutoDiscoveryCI.deleted.is_(False))
today = datetime.datetime.today()
this_month = datetime.datetime(today.year, today.month, 1)
last_month = this_month - datetime.timedelta(days=1)
last_month = datetime.datetime(last_month.year, last_month.month, 1)
this_week = today - datetime.timedelta(days=datetime.date.weekday(today))
this_week = datetime.datetime(this_week.year, this_week.month, this_week.day)
last_week = this_week - datetime.timedelta(days=7)
last_week = datetime.datetime(last_week.year, last_week.month, last_week.day)
result = dict()
for i in res:
if i.type_id not in result:
result[i.type_id] = dict(instance_count=0, accept_count=0,
this_month_count=0, this_week_count=0, last_month_count=0, last_week_count=0)
adts = AutoDiscoveryCIType.get_by(type_id=i.type_id, to_dict=False)
result[i.type_id]['rule_count'] = len(adts) + AutoDiscoveryCITypeRelation.get_by(
ad_type_id=i.type_id, only_query=True).count()
result[i.type_id]['exec_target_count'] = len(
set([i.oneagent_id for adt in adts for i in db.session.query(
AutoDiscoveryRuleSyncHistory.oneagent_id).filter(
AutoDiscoveryRuleSyncHistory.adt_id == adt.id)]))
result[i.type_id]['instance_count'] += 1
if i.is_accept:
result[i.type_id]['accept_count'] += 1
if last_month <= i.created_at < this_month:
result[i.type_id]['last_month_count'] += 1
elif i.created_at >= this_month:
result[i.type_id]['this_month_count'] += 1
if last_week <= i.created_at < this_week:
result[i.type_id]['last_week_count'] += 1
elif i.created_at >= this_week:
result[i.type_id]['this_week_count'] += 1
for type_id in result:
existed = AutoDiscoveryCounter.get_by(type_id=type_id, first=True, to_dict=False)
if existed is None:
AutoDiscoveryCounter.create(type_id=type_id, **result[type_id])
else:
existed.update(**result[type_id])
for i in AutoDiscoveryCounter.get_by(to_dict=False):
if i.type_id not in result:
i.delete()
@classmethod
def clear_ad_exec_history(cls):
ci_types = CIType.get_by(to_dict=False)
for ci_type in ci_types:
for i in AutoDiscoveryExecHistory.get_by(type_id=ci_type.id, only_query=True).order_by(
AutoDiscoveryExecHistory.id.desc()).offset(50000):
i.delete(commit=False)
db.session.commit()
@classmethod
def get_adc_counter(cls):
return cache.get(cls.KEY2) or cls.flush_adc_counter()
@classmethod
def flush_sub_counter(cls):
result = dict(type_id2users=dict())
types = db.session.query(PreferenceShowAttributes.type_id,
PreferenceShowAttributes.uid, PreferenceShowAttributes.created_at).filter(
PreferenceShowAttributes.deleted.is_(False)).group_by(
PreferenceShowAttributes.uid, PreferenceShowAttributes.type_id)
for i in types:
result['type_id2users'].setdefault(i.type_id, []).append(i.uid)
types = PreferenceTreeView.get_by(to_dict=False)
for i in types:
result['type_id2users'].setdefault(i.type_id, [])
if i.uid not in result['type_id2users'][i.type_id]:
result['type_id2users'][i.type_id].append(i.uid)
cache.set(cls.KEY3, result, timeout=0)
return result
@classmethod
def get_sub_counter(cls):
return cache.get(cls.KEY3) or cls.flush_sub_counter()

View File

@@ -6,6 +6,7 @@ import datetime
import json
import threading
import redis_lock
from flask import abort
from flask import current_app
from flask_login import current_user
@@ -14,8 +15,8 @@ from werkzeug.exceptions import BadRequest
from api.extensions import db
from api.extensions import rd
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import CITypeAttributesCache
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.cache import CMDBCounterCache
from api.lib.cmdb.ci_type import CITypeAttributeManager
from api.lib.cmdb.ci_type import CITypeManager
from api.lib.cmdb.ci_type import CITypeRelationManager
@@ -45,14 +46,12 @@ from api.lib.perm.acl.acl import is_app_admin
from api.lib.perm.acl.acl import validate_permission
from api.lib.secrets.inner import InnerCrypt
from api.lib.secrets.vault import VaultClient
from api.lib.utils import Lock
from api.lib.utils import handle_arg_list
from api.lib.webhook import webhook_request
from api.models.cmdb import AttributeHistory
from api.models.cmdb import AutoDiscoveryCI
from api.models.cmdb import CI
from api.models.cmdb import CIRelation
from api.models.cmdb import CITypeAttribute
from api.models.cmdb import CITypeRelation
from api.models.cmdb import CITypeTrigger
from api.tasks.cmdb import ci_cache
@@ -61,8 +60,8 @@ from api.tasks.cmdb import ci_delete_trigger
from api.tasks.cmdb import ci_relation_add
from api.tasks.cmdb import ci_relation_cache
from api.tasks.cmdb import ci_relation_delete
from api.tasks.cmdb import delete_id_filter
PRIVILEGED_USERS = {"worker", "cmdb_agent", "agent"}
PASSWORD_DEFAULT_SHOW = "******"
@@ -218,21 +217,13 @@ class CIManager(object):
@classmethod
def get_ad_statistics(cls):
res = CI.get_by(to_dict=False)
result = dict()
for i in res:
result.setdefault(i.type_id, dict(total=0, auto_discovery=0))
result[i.type_id]['total'] += 1
if i.is_auto_discovery:
result[i.type_id]['auto_discovery'] += 1
return result
return CMDBCounterCache.get_adc_counter()
@staticmethod
def ci_is_exist(unique_key, unique_value, type_id):
"""
:param unique_key: is a attribute
:param unique_key: is an attribute
:param unique_value:
:param type_id:
:return:
@@ -273,9 +264,11 @@ class CIManager(object):
for attr_id in constraint.attr_ids:
value_table = TableMap(attr_name=id2name[attr_id]).table
_ci_ids = set([i.ci_id for i in value_table.get_by(attr_id=attr_id,
to_dict=False,
value=ci_dict.get(id2name[attr_id]) or None)])
values = value_table.get_by(attr_id=attr_id,
value=ci_dict.get(id2name[attr_id]) or None,
only_query=True).join(
CI, CI.id == value_table.ci_id).filter(CI.type_id == type_id)
_ci_ids = set([i.ci_id for i in values])
if ci_ids is None:
ci_ids = _ci_ids
else:
@@ -287,16 +280,16 @@ class CIManager(object):
@staticmethod
def _auto_inc_id(attr):
db.session.remove()
db.session.commit()
value_table = TableMap(attr_name=attr.name).table
with Lock("auto_inc_id_{}".format(attr.name), need_lock=True):
with redis_lock.Lock(rd.r, "auto_inc_id_{}".format(attr.name)):
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:
return int(max_v.value) + 1
return 1
return 1
@classmethod
def add(cls, ci_type_name,
@@ -304,6 +297,7 @@ class CIManager(object):
_no_attribute_policy=ExistPolicy.IGNORE,
is_auto_discovery=False,
_is_admin=False,
ticket_id=None,
**ci_dict):
"""
add ci
@@ -312,19 +306,24 @@ class CIManager(object):
: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:
"""
now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
ci_type = CITypeManager.check_is_existed(ci_type_name)
raw_dict = copy.deepcopy(ci_dict)
unique_key = AttributeCache.get(ci_type.unique_id) or abort(
400, ErrFormat.unique_value_not_found.format("unique_id={}".format(ci_type.unique_id)))
unique_value = ci_dict.get(unique_key.name) or ci_dict.get(unique_key.alias) or ci_dict.get(unique_key.id)
unique_value = unique_value or abort(400, ErrFormat.unique_key_required.format(unique_key.name))
unique_value = None
if not (unique_key.default and unique_key.default.get('default') == AttributeDefaultValueEnum.AUTO_INC_ID and
not ci_dict.get(unique_key.name)): # primary key is not auto inc id
unique_value = ci_dict.get(unique_key.name) or ci_dict.get(unique_key.alias) or ci_dict.get(unique_key.id)
unique_value = unique_value or abort(400, ErrFormat.unique_key_required.format(unique_key.name))
attrs = CITypeAttributesCache.get2(ci_type_name)
attrs = CITypeAttributeManager.get_all_attributes(ci_type.id)
ci_type_attrs_name = {attr.name: attr for _, attr in attrs}
ci_type_attrs_alias = {attr.alias: attr for _, attr in attrs}
ci_attr2type_attr = {type_attr.attr_id: type_attr for type_attr, _ in attrs}
@@ -332,8 +331,15 @@ class CIManager(object):
ci = None
record_id = None
password_dict = {}
need_lock = current_user.username not in current_app.config.get('PRIVILEGED_USERS', PRIVILEGED_USERS)
with Lock(ci_type_name, need_lock=need_lock):
with redis_lock.Lock(rd.r, ci_type.name):
db.session.commit()
if (unique_key.default and unique_key.default.get('default') == AttributeDefaultValueEnum.AUTO_INC_ID and
not ci_dict.get(unique_key.name)):
ci_dict[unique_key.name] = cls._auto_inc_id(unique_key)
current_app.logger.info(ci_dict[unique_key.name])
unique_value = ci_dict[unique_key.name]
existed = cls.ci_is_exist(unique_key, unique_value, ci_type.id)
if existed is not None:
if exist_policy == ExistPolicy.REJECT:
@@ -354,7 +360,8 @@ class CIManager(object):
if attr.default.get('default') and attr.default.get('default') in (
AttributeDefaultValueEnum.CREATED_AT, AttributeDefaultValueEnum.UPDATED_AT):
ci_dict[attr.name] = now
elif attr.default.get('default') == AttributeDefaultValueEnum.AUTO_INC_ID:
elif (attr.default.get('default') == AttributeDefaultValueEnum.AUTO_INC_ID and
not ci_dict.get(attr.name)):
ci_dict[attr.name] = cls._auto_inc_id(attr)
elif ((attr.name not in ci_dict and attr.alias not in ci_dict) or (
ci_dict.get(attr.name) is None and ci_dict.get(attr.alias) is None)):
@@ -368,17 +375,20 @@ class CIManager(object):
if attr.default and attr.default.get('default') == AttributeDefaultValueEnum.UPDATED_AT:
ci_dict[attr.name] = now
value_manager = AttributeValueManager()
computed_attrs = []
for _, attr in attrs:
if attr.is_computed:
computed_attrs.append(attr.to_dict())
elif attr.is_password:
if attr.name in ci_dict:
password_dict[attr.id] = ci_dict.pop(attr.name)
password_dict[attr.id] = (ci_dict.pop(attr.name), attr.is_dynamic)
elif attr.alias in ci_dict:
password_dict[attr.id] = ci_dict.pop(attr.alias)
password_dict[attr.id] = (ci_dict.pop(attr.alias), attr.is_dynamic)
value_manager = AttributeValueManager()
if attr.re_check and password_dict.get(attr.id):
value_manager.check_re(attr.re_check, password_dict[attr.id][0])
if computed_attrs:
value_manager.handle_ci_compute_attributes(ci_dict, computed_attrs, ci)
@@ -386,7 +396,7 @@ class CIManager(object):
cls._valid_unique_constraint(ci_type.id, ci_dict, ci and ci.id)
ref_ci_dict = dict()
for k in ci_dict:
for k in copy.deepcopy(ci_dict):
if k.startswith("$") and "." in k:
ref_ci_dict[k] = ci_dict[k]
continue
@@ -398,7 +408,10 @@ class CIManager(object):
_attr_name = ((ci_type_attrs_name.get(k) and ci_type_attrs_name[k].name) or
(ci_type_attrs_alias.get(k) and ci_type_attrs_alias[k].name))
if limit_attrs and _attr_name not in limit_attrs:
return abort(403, ErrFormat.ci_filter_perm_attr_no_permission.format(k))
if k in raw_dict:
return abort(403, ErrFormat.ci_filter_perm_attr_no_permission.format(k))
else:
ci_dict.pop(k)
ci_dict = {k: v for k, v in ci_dict.items() if k in ci_type_attrs_name or k in ci_type_attrs_alias}
@@ -408,7 +421,8 @@ class CIManager(object):
operate_type = OperateType.UPDATE if ci is not None else OperateType.ADD
try:
ci = ci or CI.create(type_id=ci_type.id, is_auto_discovery=is_auto_discovery)
record_id = value_manager.create_or_update_attr_value(ci, ci_dict, key2attr)
record_id, has_dynamic = value_manager.create_or_update_attr_value(
ci, ci_dict, key2attr, ticket_id=ticket_id)
except BadRequest as e:
if existed is None:
cls.delete(ci.id)
@@ -418,7 +432,7 @@ class CIManager(object):
for attr_id in password_dict:
record_id = cls.save_password(ci.id, attr_id, password_dict[attr_id], record_id, ci_type.id)
if record_id: # has change
if record_id or has_dynamic: # has changed
ci_cache.apply_async(args=(ci.id, operate_type, record_id), queue=CMDB_QUEUE)
if ref_ci_dict: # add relations
@@ -426,17 +440,24 @@ class CIManager(object):
return ci.id
def update(self, ci_id, _is_admin=False, **ci_dict):
def update(self, ci_id, _is_admin=False, ticket_id=None, __sync=False, **ci_dict):
now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
ci = self.confirm_ci_existed(ci_id)
attrs = CITypeAttributesCache.get2(ci.type_id)
attrs = CITypeAttributeManager.get_all_attributes(ci.type_id)
ci_type_attrs_name = {attr.name: attr for _, attr in attrs}
ci_type_attrs_alias2name = {attr.alias: attr.name for _, attr in attrs}
ci_dict = {ci_type_attrs_alias2name[k] if k in ci_type_attrs_alias2name else k: v for k, v in ci_dict.items()}
raw_dict = copy.deepcopy(ci_dict)
ci_attr2type_attr = {type_attr.attr_id: type_attr for type_attr, _ in attrs}
for _, attr in attrs:
if attr.default and attr.default.get('default') == AttributeDefaultValueEnum.UPDATED_AT:
ci_dict[attr.name] = now
value_manager = AttributeValueManager()
password_dict = dict()
computed_attrs = list()
for _, attr in attrs:
@@ -444,11 +465,12 @@ class CIManager(object):
computed_attrs.append(attr.to_dict())
elif attr.is_password:
if attr.name in ci_dict:
password_dict[attr.id] = ci_dict.pop(attr.name)
password_dict[attr.id] = (ci_dict.pop(attr.name), attr.is_dynamic)
elif attr.alias in ci_dict:
password_dict[attr.id] = ci_dict.pop(attr.alias)
password_dict[attr.id] = (ci_dict.pop(attr.alias), attr.is_dynamic)
value_manager = AttributeValueManager()
if attr.re_check and password_dict.get(attr.id):
value_manager.check_re(attr.re_check, password_dict[attr.id][0])
if computed_attrs:
value_manager.handle_ci_compute_attributes(ci_dict, computed_attrs, ci)
@@ -456,20 +478,25 @@ class CIManager(object):
limit_attrs = self._valid_ci_for_no_read(ci) if not _is_admin else {}
record_id = None
need_lock = current_user.username not in current_app.config.get('PRIVILEGED_USERS', PRIVILEGED_USERS)
with Lock(ci.ci_type.name, need_lock=need_lock):
with redis_lock.Lock(rd.r, ci.ci_type.name):
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)
if limit_attrs:
for k in ci_dict:
for k in copy.deepcopy(ci_dict):
if k not in limit_attrs:
return abort(403, ErrFormat.ci_filter_perm_attr_no_permission.format(k))
if k in raw_dict:
return abort(403, ErrFormat.ci_filter_perm_attr_no_permission.format(k))
else:
ci_dict.pop(k)
try:
record_id = value_manager.create_or_update_attr_value(ci, ci_dict, key2attr)
record_id, has_dynamic = value_manager.create_or_update_attr_value(
ci, ci_dict, key2attr, ticket_id=ticket_id)
except BadRequest as e:
raise e
@@ -477,19 +504,25 @@ 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)
if record_id: # has change
ci_cache.apply_async(args=(ci_id, OperateType.UPDATE, record_id), queue=CMDB_QUEUE)
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)
else:
ci_cache(ci_id, OperateType.UPDATE, record_id)
ref_ci_dict = {k: v for k, v in ci_dict.items() if k.startswith("$") and "." in k}
if ref_ci_dict:
ci_relation_add.apply_async(args=(ref_ci_dict, ci.id), queue=CMDB_QUEUE)
if not __sync:
ci_relation_add.apply_async(args=(ref_ci_dict, ci.id), queue=CMDB_QUEUE)
else:
ci_relation_add(ref_ci_dict, ci.id)
@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)))
key2attr = {unique_name: AttributeCache.get(unique_name)}
record_id = AttributeValueManager().create_or_update_attr_value(ci, {unique_name: unique_value}, key2attr)
record_id, _ = AttributeValueManager().create_or_update_attr_value(ci, {unique_name: unique_value}, key2attr)
ci_cache.apply_async(args=(ci_id, OperateType.UPDATE, record_id), queue=CMDB_QUEUE)
@@ -514,8 +547,7 @@ class CIManager(object):
ci_delete_trigger.apply_async(args=(trigger, OperateType.DELETE, ci_dict), queue=CMDB_QUEUE)
attrs = CITypeAttribute.get_by(type_id=ci.type_id, to_dict=False)
attrs = [AttributeCache.get(attr.attr_id) for attr in attrs]
attrs = [i for _, i in CITypeAttributeManager.get_all_attributes(type_id=ci.type_id)]
for attr in attrs:
value_table = TableMap(attr=attr).table
for item in value_table.get_by(ci_id=ci_id, to_dict=False):
@@ -542,6 +574,7 @@ class CIManager(object):
AttributeHistoryManger.add(None, ci_id, [(None, OperateType.DELETE, ci_dict, None)], ci.type_id)
ci_delete.apply_async(args=(ci_id,), queue=CMDB_QUEUE)
delete_id_filter.apply_async(args=(ci_id,), queue=CMDB_QUEUE)
return ci_id
@@ -704,7 +737,7 @@ class CIManager(object):
fields=None, value_tables=None, unique_required=False, excludes=None):
"""
:param ci_ids: list of CI instance ID, eg. ['1', '2']
:param ci_ids: list of CI instance ID, e.g. ['1', '2']
:param ret_key: name, id or alias
:param fields:
:param value_tables:
@@ -729,6 +762,7 @@ class CIManager(object):
@classmethod
def save_password(cls, ci_id, attr_id, value, record_id, type_id):
value, is_dynamic = value
changed = None
encrypt_value = None
value_table = ValueTypeMap.table[ValueTypeEnum.PASSWORD]
@@ -745,14 +779,18 @@ class CIManager(object):
if existed is None:
if value:
value_table.create(ci_id=ci_id, attr_id=attr_id, value=encrypt_value)
changed = [(ci_id, attr_id, OperateType.ADD, '', PASSWORD_DEFAULT_SHOW, type_id)]
if not is_dynamic:
changed = [(ci_id, attr_id, OperateType.ADD, '', PASSWORD_DEFAULT_SHOW, type_id)]
elif existed.value != encrypt_value:
if value:
existed.update(ci_id=ci_id, attr_id=attr_id, value=encrypt_value)
changed = [(ci_id, attr_id, OperateType.UPDATE, PASSWORD_DEFAULT_SHOW, PASSWORD_DEFAULT_SHOW, type_id)]
if not is_dynamic:
changed = [(ci_id, attr_id, OperateType.UPDATE, PASSWORD_DEFAULT_SHOW,
PASSWORD_DEFAULT_SHOW, type_id)]
else:
existed.delete()
changed = [(ci_id, attr_id, OperateType.DELETE, PASSWORD_DEFAULT_SHOW, '', type_id)]
if not is_dynamic:
changed = [(ci_id, attr_id, OperateType.DELETE, PASSWORD_DEFAULT_SHOW, '', type_id)]
if current_app.config.get('SECRETS_ENGINE') == 'vault':
vault = VaultClient(current_app.config.get('VAULT_URL'), current_app.config.get('VAULT_TOKEN'))
@@ -805,6 +843,149 @@ class CIManager(object):
return data.get('v')
def baseline(self, ci_ids, before_date):
"""
return CI changes
:param ci_ids:
:param before_date:
:return:
"""
ci_list = self.get_cis_by_ids(ci_ids, ret_key=RetKey.ALIAS)
if not ci_list:
return dict()
ci2changed = dict()
changed = AttributeHistoryManger.get_records_for_attributes(
before_date, None, None, 1, 100000, None, None, ci_ids=ci_ids, more=True)[1]
for records in changed:
for change in records[1]:
if change['is_computed'] or change['is_password']:
continue
if change.get('default') and change['default'].get('default') == AttributeDefaultValueEnum.UPDATED_AT:
continue
ci2changed.setdefault(change['ci_id'], {})
item = (change['old'],
change['new'],
change.get('is_list'),
change.get('value_type'),
change['operate_type'])
if change.get('is_list'):
ci2changed[change['ci_id']].setdefault(change.get('attr_alias'), []).append(item)
else:
ci2changed[change['ci_id']].update({change.get('attr_alias'): item})
type2show_name = {}
result = []
for ci in ci_list:
list_attr2item = {}
for alias_name, v in (ci2changed.get(ci['_id']) or {}).items():
if not alias_name:
continue
if alias_name == ci.get('unique_alias'):
continue
if ci.get('_type') not in type2show_name:
ci_type = CITypeCache.get(ci.get('_type'))
show_id = ci_type.show_id or ci_type.unique_id
type2show_name[ci['_type']] = AttributeCache.get(show_id).alias
if isinstance(v, list):
for old, new, is_list, value_type, operate_type in v:
if alias_name not in list_attr2item:
list_attr2item[alias_name] = dict(instance=ci.get(type2show_name[ci['_type']]),
attr_name=alias_name,
value_type=value_type,
is_list=is_list,
ci_type=ci.get('ci_type'),
unique_alias=ci.get('unique_alias'),
unique_value=ci.get(ci['unique_alias']),
cur=copy.deepcopy(ci.get(alias_name)),
to=ci.get(alias_name) or [])
old = ValueTypeMap.deserialize[value_type](old) if old else old
new = ValueTypeMap.deserialize[value_type](new) if new else new
if operate_type == OperateType.ADD:
list_attr2item[alias_name]['to'].remove(new)
elif operate_type == OperateType.DELETE and old not in list_attr2item[alias_name]['to']:
list_attr2item[alias_name]['to'].append(old)
continue
old, value_type = v[0], v[3]
old = ValueTypeMap.deserialize[value_type](old) if old else old
if isinstance(old, (datetime.datetime, datetime.date)):
old = str(old)
if ci.get(alias_name) != old:
item = dict(instance=ci.get(type2show_name[ci['_type']]),
attr_name=alias_name,
value_type=value_type,
ci_type=ci.get('ci_type'),
unique_alias=ci.get('unique_alias'),
unique_value=ci.get(ci['unique_alias']),
cur=ci.get(alias_name),
to=old)
result.append(item)
for alias_name, item in list_attr2item.items():
if sorted(item['cur'] or []) != sorted(item['to'] or []):
result.append(item)
return result
def baseline_cis(self, ci_ids, before_date, fl=None):
"""
return CI changes
:param ci_ids:
:param before_date:
:param fl:
:return:
"""
ci_list = self.get_cis_by_ids(ci_ids, fields=fl)
if not ci_list:
return []
id2ci = {ci['_id']: ci for ci in ci_list}
changed = AttributeHistoryManger.get_records_for_attributes(
before_date, None, None, 1, 100000, None, None, ci_ids=ci_ids, more=True)[1]
for records in changed:
for change in records[1]:
if change['is_computed'] or change['is_password']:
continue
if change.get('default') and change['default'].get('default') == AttributeDefaultValueEnum.UPDATED_AT:
continue
if change['is_list']:
old, new, value_type, operate_type, ci_id, attr_name = (
change['old'], change['new'], change['value_type'], change['operate_type'],
change['ci_id'], change['attr_name'])
old = ValueTypeMap.deserialize[value_type](old) if old else old
new = ValueTypeMap.deserialize[value_type](new) if new else new
if operate_type == OperateType.ADD and new in (id2ci[ci_id][attr_name] or []):
id2ci[ci_id][attr_name].remove(new)
elif operate_type == OperateType.DELETE and old not in id2ci[ci_id][attr_name]:
id2ci[ci_id][attr_name].append(old)
else:
id2ci[change['ci_id']][change['attr_name']] = change['old']
return list(id2ci.values())
def rollback(self, ci_id, before_date):
baseline_ci = self.baseline([ci_id], before_date)
payload = dict()
for item in baseline_ci:
payload[item.get('attr_name')] = item.get('to')
if payload:
payload['ci_type'] = baseline_ci[0]['ci_type']
payload[baseline_ci[0]['unique_alias']] = baseline_ci[0]['unique_value']
self.update(ci_id, **payload)
return payload
class CIRelationManager(object):
"""
@@ -848,6 +1029,20 @@ class CIRelationManager(object):
return numfound, len(ci_ids), result
@staticmethod
def recursive_children(ci_id):
result = []
def _get_children(_id):
children = CIRelation.get_by(first_ci_id=_id, to_dict=False)
result.extend([i.second_ci_id for i in children])
for child in children:
_get_children(child.second_ci_id)
_get_children(ci_id)
return result
@staticmethod
def _sort_handler(sort_by, query_sql):
@@ -901,9 +1096,21 @@ class CIRelationManager(object):
return ci_ids, level2ids
@classmethod
def get_parent_ids(cls, ci_ids):
cis = db.session.query(CIRelation.first_ci_id, CIRelation.second_ci_id, CI.type_id).join(
CI, CI.id == CIRelation.first_ci_id).filter(
CIRelation.second_ci_id.in_(ci_ids)).filter(CIRelation.deleted.is_(False))
result = {}
for ci in cis:
result.setdefault(ci.second_ci_id, []).append((ci.first_ci_id, ci.type_id))
return result
@staticmethod
def _check_constraint(first_ci_id, first_type_id, second_ci_id, second_type_id, type_relation):
db.session.remove()
db.session.commit()
if type_relation.constraint == ConstraintEnum.Many2Many:
return
@@ -926,7 +1133,7 @@ class CIRelationManager(object):
return abort(400, ErrFormat.relation_constraint.format("1-N"))
@classmethod
def add(cls, first_ci_id, second_ci_id, more=None, relation_type_id=None, ancestor_ids=None):
def add(cls, first_ci_id, second_ci_id, more=None, relation_type_id=None, ancestor_ids=None, valid=True):
first_ci = CIManager.confirm_ci_existed(first_ci_id)
second_ci = CIManager.confirm_ci_existed(second_ci_id)
@@ -951,7 +1158,7 @@ class CIRelationManager(object):
relation_type_id or abort(404, ErrFormat.relation_not_found.format("{} -> {}".format(
first_ci.ci_type.name, second_ci.ci_type.name)))
if current_app.config.get('USE_ACL'):
if current_app.config.get('USE_ACL') and valid and current_user.username != 'worker':
resource_name = CITypeRelationManager.acl_resource_name(first_ci.ci_type.name,
second_ci.ci_type.name)
if not ACLManager().has_permission(
@@ -963,7 +1170,7 @@ class CIRelationManager(object):
else:
type_relation = CITypeRelation.get_by_id(relation_type_id)
with Lock("ci_relation_add_{}_{}".format(first_ci.type_id, second_ci.type_id), need_lock=True):
with redis_lock.Lock(rd.r, "ci_relation_add_{}_{}".format(first_ci.type_id, second_ci.type_id)):
cls._check_constraint(first_ci_id, first_ci.type_id, second_ci_id, second_ci.type_id, type_relation)
@@ -985,7 +1192,7 @@ class CIRelationManager(object):
def delete(cr_id):
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'):
if current_app.config.get('USE_ACL') and current_user.username != 'worker':
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,
@@ -999,6 +1206,7 @@ class CIRelationManager(object):
his_manager.add(cr, operate_type=OperateType.DELETE)
ci_relation_delete.apply_async(args=(cr.first_ci_id, cr.second_ci_id, cr.ancestor_ids), queue=CMDB_QUEUE)
delete_id_filter.apply_async(args=(cr.second_ci_id,), queue=CMDB_QUEUE)
return cr_id
@@ -1010,9 +1218,28 @@ class CIRelationManager(object):
to_dict=False,
first=True)
ci_relation_delete.apply_async(args=(first_ci_id, second_ci_id, ancestor_ids), queue=CMDB_QUEUE)
if cr is not None:
cls.delete(cr.id)
return cr and cls.delete(cr.id)
ci_relation_delete.apply_async(args=(first_ci_id, second_ci_id, ancestor_ids), queue=CMDB_QUEUE)
delete_id_filter.apply_async(args=(second_ci_id,), queue=CMDB_QUEUE)
return cr
@classmethod
def delete_3(cls, first_ci_id, second_ci_id):
cr = CIRelation.get_by(first_ci_id=first_ci_id,
second_ci_id=second_ci_id,
to_dict=False,
first=True)
if cr is not None:
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)
return cr
@classmethod
def batch_update(cls, ci_ids, parents, children, ancestor_ids=None):
@@ -1049,11 +1276,93 @@ class CIRelationManager(object):
for ci_id in ci_ids:
cls.delete_2(parent_id, ci_id, ancestor_ids=ancestor_ids)
@classmethod
def build_by_attribute(cls, ci_dict):
type_id = ci_dict['_type']
child_items = CITypeRelation.get_by(parent_id=type_id, only_query=True).filter(
CITypeRelation.parent_attr_ids.isnot(None))
for item in child_items:
relations = None
for parent_attr_id, child_attr_id in zip(item.parent_attr_ids, item.child_attr_ids):
_relations = set()
parent_attr = AttributeCache.get(parent_attr_id)
child_attr = AttributeCache.get(child_attr_id)
attr_value = ci_dict.get(parent_attr.name)
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))
if relations is None:
relations = _relations
else:
relations &= _relations
for parent_ci_id, child_ci_id in (relations or []):
CIRelationManager.add(parent_ci_id, child_ci_id, valid=False)
parent_items = CITypeRelation.get_by(child_id=type_id, only_query=True).filter(
CITypeRelation.child_attr_ids.isnot(None))
for item in parent_items:
relations = None
for parent_attr_id, child_attr_id in zip(item.parent_attr_ids, item.child_attr_ids):
_relations = set()
parent_attr = AttributeCache.get(parent_attr_id)
child_attr = AttributeCache.get(child_attr_id)
attr_value = ci_dict.get(child_attr.name)
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']))
if relations is None:
relations = _relations
else:
relations &= _relations
for parent_ci_id, child_ci_id in (relations or []):
CIRelationManager.add(parent_ci_id, child_ci_id, valid=False)
@classmethod
def rebuild_all_by_attribute(cls, ci_type_relation):
relations = None
for parent_attr_id, child_attr_id in zip(ci_type_relation['parent_attr_ids'] or [],
ci_type_relation['child_attr_ids'] or []):
_relations = set()
parent_attr = AttributeCache.get(parent_attr_id)
child_attr = AttributeCache.get(child_attr_id)
if not parent_attr or not child_attr:
continue
parent_value_table = TableMap(attr=parent_attr).table
child_value_table = TableMap(attr=child_attr).table
parent_values = parent_value_table.get_by(attr_id=parent_attr.id, only_query=True).join(
CI, CI.id == parent_value_table.ci_id).filter(CI.type_id == ci_type_relation['parent_id'])
child_values = child_value_table.get_by(attr_id=child_attr.id, only_query=True).join(
CI, CI.id == child_value_table.ci_id).filter(CI.type_id == ci_type_relation['child_id'])
child_value2ci_ids = {}
for child in child_values:
child_value2ci_ids.setdefault(child.value, []).append(child.ci_id)
for parent in parent_values:
for child_ci_id in child_value2ci_ids.get(parent.value, []):
_relations.add((parent.ci_id, child_ci_id))
if relations is None:
relations = _relations
else:
relations &= _relations
for parent_ci_id, child_ci_id in (relations or []):
try:
cls.add(parent_ci_id, child_ci_id, valid=False)
except:
pass
class CITriggerManager(object):
@staticmethod
def get(type_id):
db.session.remove()
db.session.commit()
return CITypeTrigger.get_by(type_id=type_id, to_dict=True)
@staticmethod

View File

@@ -5,7 +5,6 @@ import copy
import toposort
from flask import abort
from flask import current_app
from flask import session
from flask_login import current_user
from toposort import toposort_flatten
from werkzeug.exceptions import BadRequest
@@ -28,12 +27,15 @@ from api.lib.cmdb.perms import CIFilterPermsCRUD
from api.lib.cmdb.relation_type import RelationTypeManager
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.value import AttributeValueManager
from api.lib.common_setting.role_perm_base import CMDBApp
from api.lib.decorator import kwargs_required
from api.lib.perm.acl.acl import ACLManager
from api.lib.perm.acl.acl import is_app_admin
from api.lib.perm.acl.acl import validate_permission
from api.models.cmdb import Attribute
from api.models.cmdb import AutoDiscoveryCI
from api.models.cmdb import AutoDiscoveryCIType
from api.models.cmdb import AutoDiscoveryCITypeRelation
from api.models.cmdb import CI
from api.models.cmdb import CIFilterPerms
from api.models.cmdb import CIType
@@ -42,15 +44,18 @@ from api.models.cmdb import CITypeAttributeGroup
from api.models.cmdb import CITypeAttributeGroupItem
from api.models.cmdb import CITypeGroup
from api.models.cmdb import CITypeGroupItem
from api.models.cmdb import CITypeInheritance
from api.models.cmdb import CITypeRelation
from api.models.cmdb import CITypeTrigger
from api.models.cmdb import CITypeUniqueConstraint
from api.models.cmdb import CustomDashboard
from api.models.cmdb import PreferenceCITypeOrder
from api.models.cmdb import PreferenceRelationView
from api.models.cmdb import PreferenceSearchOption
from api.models.cmdb import PreferenceShowAttributes
from api.models.cmdb import PreferenceTreeView
from api.models.cmdb import RelationType
from api.models.cmdb import TopologyView
class CITypeManager(object):
@@ -74,17 +79,26 @@ class CITypeManager(object):
return CIType.get_by_id(ci_type.id)
def get_icons(self):
return {i.id: i.icon or i.name for i in db.session.query(
self.cls.id, self.cls.icon, self.cls.name).filter(self.cls.deleted.is_(False))}
@staticmethod
def get_ci_types(type_name=None):
def get_ci_types(type_name=None, like=True):
resources = None
if current_app.config.get('USE_ACL') and not is_app_admin('cmdb'):
resources = set([i.get('name') for i in ACLManager().get_resources(ResourceTypeEnum.CI_TYPE)])
ci_types = CIType.get_by() if type_name is None else CIType.get_by_like(name=type_name)
ci_types = CIType.get_by() if type_name is None else (
CIType.get_by_like(name=type_name) if like else CIType.get_by(name=type_name))
res = list()
for type_dict in ci_types:
attr = AttributeCache.get(type_dict["unique_id"])
type_dict["unique_key"] = attr and attr.name
if type_dict.get('show_id'):
attr = AttributeCache.get(type_dict["show_id"])
type_dict["show_name"] = attr and attr.name
type_dict['parent_ids'] = CITypeInheritanceManager.get_parents(type_dict['id'])
if resources is None or type_dict['name'] in resources:
res.append(type_dict)
@@ -119,7 +133,9 @@ class CITypeManager(object):
def add(cls, **kwargs):
if current_app.config.get('USE_ACL') and not is_app_admin('cmdb'):
if ErrFormat.ci_type_config not in {i['name'] for i in ACLManager().get_resources(ResourceTypeEnum.PAGE)}:
return abort(403, ErrFormat.no_permission2)
app_cli = CMDBApp()
validate_permission(app_cli.op.Model_Configuration, app_cli.resource_type_name,
app_cli.op.create_CIType, app_cli.app_name)
unique_key = kwargs.pop("unique_key", None) or kwargs.pop("unique_id", None)
unique_key = AttributeCache.get(unique_key) or abort(404, ErrFormat.unique_key_not_define)
@@ -131,8 +147,13 @@ class CITypeManager(object):
kwargs["unique_id"] = unique_key.id
kwargs['uid'] = current_user.uid
parent_ids = kwargs.pop('parent_ids', None)
ci_type = CIType.create(**kwargs)
CITypeInheritanceManager.add(parent_ids, ci_type.id)
CITypeAttributeManager.add(ci_type.id, [unique_key.id], is_required=True)
CITypeCache.clean(ci_type.name)
@@ -179,7 +200,7 @@ class CITypeManager(object):
CITypeAttributeManager.update(type_id, [attr])
ci_type2 = ci_type.to_dict()
new = ci_type.update(**kwargs)
new = ci_type.update(**kwargs, filter_none=False)
CITypeCache.clean(type_id)
@@ -208,27 +229,54 @@ class CITypeManager(object):
if CI.get_by(type_id=type_id, first=True, to_dict=False) is not None:
return abort(400, ErrFormat.ci_exists_and_cannot_delete_type)
if CITypeInheritance.get_by(parent_id=type_id, first=True):
return abort(400, ErrFormat.ci_type_inheritance_cannot_delete)
relation_views = PreferenceRelationView.get_by(to_dict=False)
for rv in relation_views:
for item in (rv.cr_ids or []):
if item.get('parent_id') == type_id or item.get('child_id') == type_id:
return abort(400, ErrFormat.ci_relation_view_exists_and_cannot_delete_type.format(rv.name))
for item in CITypeRelation.get_by(parent_id=type_id, to_dict=False):
item.soft_delete(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 current_app.config.get('USE_ACL'):
resource_name = CITypeRelationManager.acl_resource_name(item.parent.name, item.child.name)
ACLManager().del_resource(resource_name, ResourceTypeEnum.CI_TYPE_RELATION)
for item in CITypeRelation.get_by(child_id=type_id, to_dict=False):
item.soft_delete(commit=False)
for table in [PreferenceTreeView, PreferenceShowAttributes, PreferenceSearchOption, CustomDashboard,
CITypeGroupItem, CITypeAttributeGroup, CITypeAttribute, CITypeUniqueConstraint, CITypeTrigger,
AutoDiscoveryCIType, CIFilterPerms]:
AutoDiscoveryCIType, CIFilterPerms, PreferenceCITypeOrder]:
for item in table.get_by(type_id=type_id, to_dict=False):
item.soft_delete(commit=False)
for item in AutoDiscoveryCI.get_by(type_id=type_id, to_dict=False):
item.delete(commit=False)
for item in AutoDiscoveryCITypeRelation.get_by(ad_type_id=type_id, to_dict=False):
item.soft_delete(commit=False)
for item in AutoDiscoveryCITypeRelation.get_by(peer_type_id=type_id, to_dict=False):
item.soft_delete(commit=False)
for item in CITypeInheritance.get_by(parent_id=type_id, to_dict=False):
item.soft_delete(commit=False)
for item in CITypeInheritance.get_by(child_id=type_id, to_dict=False):
item.soft_delete(commit=False)
try:
from api.models.cmdb import CITypeReconciliation
for item in CITypeReconciliation.get_by(type_id=type_id, to_dict=False):
item.soft_delete(commit=False)
except Exception:
pass
for item in TopologyView.get_by(central_node_type=type_id, to_dict=False):
item.delete(commit=False)
db.session.commit()
ci_type.soft_delete()
@@ -241,6 +289,100 @@ class CITypeManager(object):
ACLManager().del_resource(ci_type.name, ResourceTypeEnum.CI)
class CITypeInheritanceManager(object):
cls = CITypeInheritance
@classmethod
def get_parents(cls, type_id):
return [i.parent_id for i in cls.cls.get_by(child_id=type_id, to_dict=False)]
@classmethod
def recursive_children(cls, type_id):
result = []
def _get_child(_id):
children = [i.child_id for i in cls.cls.get_by(parent_id=_id, to_dict=False)]
result.extend(children)
for child_id in children:
_get_child(child_id)
_get_child(type_id)
return result
@classmethod
def base(cls, type_id):
result = []
q = []
def _get_parents(_type_id):
parents = [i.parent_id for i in cls.cls.get_by(child_id=_type_id, to_dict=False)]
for i in parents[::-1]:
q.append(i)
try:
out = q.pop(0)
except IndexError:
return
result.append(out)
_get_parents(out)
_get_parents(type_id)
return result[::-1]
@classmethod
def add(cls, parent_ids, child_id):
rels = {}
for i in cls.cls.get_by(to_dict=False):
rels.setdefault(i.child_id, set()).add(i.parent_id)
try:
toposort_flatten(rels)
except toposort.CircularDependencyError as e:
current_app.logger.warning(str(e))
return abort(400, ErrFormat.circular_dependency_error)
for parent_id in parent_ids or []:
if parent_id == child_id:
return abort(400, ErrFormat.circular_dependency_error)
existed = cls.cls.get_by(parent_id=parent_id, child_id=child_id, first=True, to_dict=False)
if existed is None:
rels.setdefault(child_id, set()).add(parent_id)
try:
toposort_flatten(rels)
except toposort.CircularDependencyError as e:
current_app.logger.warning(str(e))
return abort(400, ErrFormat.circular_dependency_error)
cls.cls.create(parent_id=parent_id, child_id=child_id, commit=False)
db.session.commit()
@classmethod
def delete(cls, parent_id, child_id):
existed = cls.cls.get_by(parent_id=parent_id, child_id=child_id, first=True, to_dict=False)
if existed is not None:
children = cls.recursive_children(child_id) + [child_id]
for _id in children:
if CI.get_by(type_id=_id, to_dict=False, first=True) is not None:
return abort(400, ErrFormat.ci_exists_and_cannot_delete_inheritance)
attr_ids = set([i.id for _, i in CITypeAttributeManager.get_all_attributes(parent_id)])
for _id in children:
for attr_id in attr_ids:
for i in PreferenceShowAttributes.get_by(type_id=_id, attr_id=attr_id, to_dict=False):
i.soft_delete(commit=False)
db.session.commit()
existed.soft_delete()
class CITypeGroupManager(object):
cls = CITypeGroup
@@ -261,6 +403,7 @@ class CITypeGroupManager(object):
ci_type = CITypeCache.get(t['type_id']).to_dict()
if resources is None or (ci_type and ci_type['name'] in resources):
ci_type['permissions'] = resources[ci_type['name']] if resources is not None else None
ci_type['inherited'] = True if CITypeInheritanceManager.get_parents(ci_type['id']) else False
group.setdefault("ci_types", []).append(ci_type)
group_types.add(t["type_id"])
@@ -270,6 +413,7 @@ class CITypeGroupManager(object):
for ci_type in ci_types:
if ci_type["id"] not in group_types and (resources is None or ci_type['name'] in resources):
ci_type['permissions'] = resources.get(ci_type['name']) if resources is not None else None
ci_type['inherited'] = True if CITypeInheritanceManager.get_parents(ci_type['id']) else False
other_types['ci_types'].append(ci_type)
groups.append(other_types)
@@ -294,9 +438,6 @@ class CITypeGroupManager(object):
existed = CITypeGroup.get_by_id(gid) or abort(
404, ErrFormat.ci_type_group_not_found.format("id={}".format(gid)))
if name is not None and name != existed.name:
if RoleEnum.CONFIG not in session.get("acl", {}).get("parentRoles", []) and not is_app_admin("cmdb"):
return abort(403, ErrFormat.role_required.format(RoleEnum.CONFIG))
existed.update(name=name)
max_order = max([i.order or 0 for i in CITypeGroupItem.get_by(group_id=gid, to_dict=False)] or [0])
@@ -361,40 +502,62 @@ class CITypeAttributeManager(object):
return attr.name
@staticmethod
def get_attr_names_by_type_id(type_id):
return [AttributeCache.get(attr.attr_id).name for attr in CITypeAttributesCache.get(type_id)]
def get_all_attributes(type_id):
parent_ids = CITypeInheritanceManager.base(type_id)
result = []
for _type_id in parent_ids + [type_id]:
result.extend(CITypeAttributesCache.get2(_type_id))
return result
@classmethod
def get_attr_names_by_type_id(cls, type_id):
return [attr.name for _, attr in cls.get_all_attributes(type_id)]
@staticmethod
def get_attributes_by_type_id(type_id, choice_web_hook_parse=True, choice_other_parse=True):
has_config_perm = ACLManager('cmdb').has_permission(
CITypeManager.get_name_by_id(type_id), ResourceTypeEnum.CI, PermEnum.CONFIG)
attrs = CITypeAttributesCache.get(type_id)
result = list()
for attr in sorted(attrs, key=lambda x: (x.order, x.id)):
attr_dict = AttributeManager().get_attribute(attr.attr_id, choice_web_hook_parse, choice_other_parse)
attr_dict["is_required"] = attr.is_required
attr_dict["order"] = attr.order
attr_dict["default_show"] = attr.default_show
if not has_config_perm:
attr_dict.pop('choice_web_hook', None)
attr_dict.pop('choice_other', None)
parent_ids = CITypeInheritanceManager.base(type_id)
result.append(attr_dict)
result = list()
id2pos = dict()
type2name = {i: CITypeCache.get(i) for i in parent_ids}
for _type_id in parent_ids + [type_id]:
attrs = CITypeAttributesCache.get(_type_id)
for attr in sorted(attrs, key=lambda x: (x.order, x.id)):
attr_dict = AttributeManager().get_attribute(attr.attr_id, choice_web_hook_parse, choice_other_parse)
attr_dict["is_required"] = attr.is_required
attr_dict["order"] = attr.order
attr_dict["default_show"] = attr.default_show
attr_dict["inherited"] = False if _type_id == type_id else True
attr_dict["inherited_from"] = type2name.get(_type_id) and type2name[_type_id].alias
if not has_config_perm:
attr_dict.pop('choice_web_hook', None)
attr_dict.pop('choice_other', None)
if attr_dict['id'] not in id2pos:
id2pos[attr_dict['id']] = len(result)
result.append(attr_dict)
else:
result[id2pos[attr_dict['id']]] = attr_dict
return result
@staticmethod
def get_common_attributes(type_ids):
@classmethod
def get_common_attributes(cls, type_ids):
has_config_perm = False
for type_id in type_ids:
has_config_perm |= ACLManager('cmdb').has_permission(
CITypeManager.get_name_by_id(type_id), ResourceTypeEnum.CI, PermEnum.CONFIG)
result = CITypeAttribute.get_by(__func_in___key_type_id=list(map(int, type_ids)), to_dict=False)
result = {type_id: [i for _, i in cls.get_all_attributes(type_id)] for type_id in type_ids}
attr2types = {}
for i in result:
attr2types.setdefault(i.attr_id, []).append(i.type_id)
for type_id in result:
for i in result[type_id]:
attr2types.setdefault(i.id, []).append(type_id)
attrs = []
for attr_id in attr2types:
@@ -511,12 +674,49 @@ class CITypeAttributeManager(object):
existed.soft_delete()
for ci in CI.get_by(type_id=type_id, to_dict=False):
AttributeValueManager.delete_attr_value(attr_id, ci.id)
AttributeValueManager.delete_attr_value(attr_id, ci.id, commit=False)
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):
if attr_id in item.attr_ids:
attr_ids = copy.deepcopy(item.attr_ids)
attr_ids.remove(attr_id)
if attr_ids:
item.update(attr_ids=attr_ids, commit=False)
else:
item.soft_delete(commit=False)
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 (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 []):
item_dict = item.to_dict()
pop_idx = item.parent_attr_ids.index(attr_id)
elif item.child_id == type_id and attr_id in (item.child_attr_ids or []):
item_dict = item.to_dict()
pop_idx = item.child_attr_ids.index(attr_id)
else:
continue
item.update(parent_attr_ids=item_dict['parent_attr_ids'].pop(pop_idx),
child_attr_ids=item_dict['child_attr_ids'].pop(pop_idx),
commit=False)
db.session.commit()
CITypeAttributeCache.clean(type_id, attr_id)
if ci_type.show_id == attr_id:
ci_type.update(show_id=None, filter_none=False)
CITypeHistoryManager.add(CITypeOperateType.DELETE_ATTRIBUTE, type_id, attr_id=attr.id,
change=attr and attr.to_dict())
@@ -528,8 +728,22 @@ class CITypeAttributeManager(object):
attr_id = _from.get('attr_id')
from_group_id = _from.get('group_id')
to_group_id = _to.get('group_id')
from_group_name = _from.get('group_name')
to_group_name = _to.get('group_name')
order = _to.get('order')
if from_group_name:
from_group = CITypeAttributeGroup.get_by(type_id=type_id, name=from_group_name, first=True, to_dict=False)
from_group_id = from_group and from_group.id
if to_group_name:
to_group = CITypeAttributeGroup.get_by(type_id=type_id, name=to_group_name, first=True, to_dict=False)
to_group_id = to_group and to_group.id
if not to_group_id and CITypeInheritance.get_by(child_id=type_id, to_dict=False):
to_group = CITypeAttributeGroup.create(type_id=type_id, name=to_group_name)
to_group_id = to_group and to_group.id
if from_group_id != to_group_id:
if from_group_id is not None:
CITypeAttributeGroupManager.delete_item(from_group_id, attr_id)
@@ -557,14 +771,21 @@ class CITypeRelationManager(object):
@staticmethod
def get():
res = CITypeRelation.get_by(to_dict=False)
type2attributes = dict()
for idx, item in enumerate(res):
_item = item.to_dict()
res[idx] = _item
res[idx]['parent'] = item.parent.to_dict()
if item.parent_id not in type2attributes:
type2attributes[item.parent_id] = [i[1].to_dict() for i in
CITypeAttributeManager.get_all_attributes(item.parent_id)]
res[idx]['child'] = item.child.to_dict()
if item.child_id not in type2attributes:
type2attributes[item.child_id] = [i[1].to_dict() for i in
CITypeAttributeManager.get_all_attributes(item.child_id)]
res[idx]['relation_type'] = item.relation_type.to_dict()
return res
return res, type2attributes
@staticmethod
def get_child_type_ids(type_id, level):
@@ -596,6 +817,8 @@ class CITypeRelationManager(object):
ci_type_dict["relation_type"] = relation_inst.relation_type.name
ci_type_dict["constraint"] = relation_inst.constraint
ci_type_dict["parent_attr_ids"] = relation_inst.parent_attr_ids
ci_type_dict["child_attr_ids"] = relation_inst.child_attr_ids
return ci_type_dict
@@ -628,6 +851,72 @@ class CITypeRelationManager(object):
return [cls._wrap_relation_type_dict(parent.parent_id, parent) for parent in parents]
@staticmethod
def get_relations_by_type_id(type_id):
nodes, edges = [], []
node_ids, edge_tuples = set(), set()
ci_type = CITypeCache.get(type_id)
if ci_type is None:
return nodes, edges
ci_type_dict = ci_type.to_dict()
ci_type_dict.setdefault('level', [0])
nodes.append(ci_type_dict)
node_ids.add(ci_type.id)
def _find(_id, lv):
lv += 1
for i in CITypeRelation.get_by(parent_id=_id, to_dict=False):
if i.child_id not in node_ids:
node_ids.add(i.child_id)
node = i.child.to_dict()
node.setdefault('level', []).append(lv)
nodes.append(node)
edges.append(dict(from_id=i.parent_id, to_id=i.child_id, text=i.relation_type.name, reverse=False))
edge_tuples.add((i.parent_id, i.child_id))
_find(i.child_id, lv)
continue
elif (i.parent_id, i.child_id) not in edge_tuples:
edges.append(dict(from_id=i.parent_id, to_id=i.child_id, text=i.relation_type.name, reverse=False))
edge_tuples.add((i.parent_id, i.child_id))
_find(i.child_id, lv)
for _node in nodes:
if _node['id'] == i.child_id and lv not in _node['level']:
_node['level'].append(lv)
def _reverse_find(_id, lv):
lv -= 1
for i in CITypeRelation.get_by(child_id=_id, to_dict=False):
if i.parent_id not in node_ids:
node_ids.add(i.parent_id)
node = i.parent.to_dict()
node.setdefault('level', []).append(lv)
nodes.append(node)
edges.append(dict(from_id=i.parent_id, to_id=i.child_id, text=i.relation_type.name, reverse=True))
edge_tuples.add((i.parent_id, i.child_id))
_reverse_find(i.parent_id, lv)
continue
elif (i.parent_id, i.child_id) not in edge_tuples:
edges.append(dict(from_id=i.parent_id, to_id=i.child_id, text=i.relation_type.name, reverse=True))
edge_tuples.add((i.parent_id, i.child_id))
_reverse_find(i.parent_id, lv)
for _node in nodes:
if _node['id'] == i.child_id and lv not in _node['level']:
_node['level'].append(lv)
level = 0
_reverse_find(ci_type.id, level)
level = 0
_find(ci_type.id, level)
return nodes, edges
@staticmethod
def _get(parent_id, child_id):
return CITypeRelation.get_by(parent_id=parent_id,
@@ -640,7 +929,8 @@ class CITypeRelationManager(object):
return "{} -> {}".format(first_name, second_name)
@classmethod
def add(cls, parent, child, relation_type_id, constraint=ConstraintEnum.One2Many):
def add(cls, parent, child, relation_type_id, constraint=ConstraintEnum.One2Many,
parent_attr_ids=None, child_attr_ids=None):
p = CITypeManager.check_is_existed(parent)
c = CITypeManager.check_is_existed(child)
@@ -655,24 +945,22 @@ class CITypeRelationManager(object):
current_app.logger.warning(str(e))
return abort(400, ErrFormat.circular_dependency_error)
if constraint == ConstraintEnum.Many2Many:
other_c = CITypeRelation.get_by(parent_id=p.id, constraint=ConstraintEnum.Many2Many,
to_dict=False, first=True)
other_p = CITypeRelation.get_by(child_id=c.id, constraint=ConstraintEnum.Many2Many,
to_dict=False, first=True)
if other_c and other_c.child_id != c.id:
return abort(400, ErrFormat.m2m_relation_constraint.format(p.name, other_c.child.name))
if other_p and other_p.parent_id != p.id:
return abort(400, ErrFormat.m2m_relation_constraint.format(other_p.parent.name, c.name))
old_parent_attr_ids, old_child_attr_ids = None, None
existed = cls._get(p.id, c.id)
if existed is not None:
existed.update(relation_type_id=relation_type_id,
constraint=constraint)
old_parent_attr_ids = copy.deepcopy(existed.parent_attr_ids)
old_child_attr_ids = copy.deepcopy(existed.child_attr_ids)
existed = existed.update(relation_type_id=relation_type_id,
constraint=constraint,
parent_attr_ids=parent_attr_ids,
child_attr_ids=child_attr_ids,
filter_none=False)
else:
existed = CITypeRelation.create(parent_id=p.id,
child_id=c.id,
relation_type_id=relation_type_id,
parent_attr_ids=parent_attr_ids,
child_attr_ids=child_attr_ids,
constraint=constraint)
if current_app.config.get("USE_ACL"):
@@ -686,6 +974,11 @@ class CITypeRelationManager(object):
current_user.username,
ResourceTypeEnum.CI_TYPE_RELATION)
if ((parent_attr_ids and parent_attr_ids != old_parent_attr_ids) or
(child_attr_ids and child_attr_ids != old_child_attr_ids)):
from api.tasks.cmdb import rebuild_relation_for_attribute_changed
rebuild_relation_for_attribute_changed.apply_async(args=(existed.to_dict(),))
CITypeHistoryManager.add(CITypeOperateType.ADD_RELATION, p.id,
change=dict(parent=p.to_dict(), child=c.to_dict(), relation_type_id=relation_type_id))
@@ -738,25 +1031,66 @@ class CITypeAttributeGroupManager(object):
@staticmethod
def get_by_type_id(type_id, need_other=False):
groups = CITypeAttributeGroup.get_by(type_id=type_id)
groups = sorted(groups, key=lambda x: x["order"] or 0)
grouped = list()
parent_ids = CITypeInheritanceManager.base(type_id)
groups = []
id2type = {i: CITypeCache.get(i).alias for i in parent_ids}
for _type_id in parent_ids + [type_id]:
_groups = CITypeAttributeGroup.get_by(type_id=_type_id)
_groups = sorted(_groups, key=lambda x: x["order"] or 0)
for i in _groups:
if type_id != _type_id:
i['inherited'] = True
i['inherited_from'] = id2type[_type_id]
else:
i['inherited'] = False
groups.extend(_groups)
grouped = set()
attributes = CITypeAttributeManager.get_attributes_by_type_id(type_id)
id2attr = {i.get('id'): i for i in attributes}
group2pos = dict()
attr2pos = dict()
result = []
for group in groups:
items = CITypeAttributeGroupItem.get_by(group_id=group["id"], to_dict=False)
items = sorted(items, key=lambda x: x.order or 0)
group["attributes"] = [id2attr.get(i.attr_id) for i in items if i.attr_id in id2attr]
grouped.extend([i.attr_id for i in items])
if group['name'] not in group2pos:
group_pos = len(result)
group['attributes'] = []
result.append(group)
group2pos[group['name']] = group_pos
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)
if i.attr_id in attr2pos:
result[attr2pos[i.attr_id][0]]['attributes'].remove(attr2pos[i.attr_id][1])
attr2pos[i.attr_id] = [group_pos, attr]
group.pop('inherited_from', None)
grouped |= set([i.attr_id for i in items])
if need_other:
grouped = set(grouped)
other_attributes = [attr for attr in attributes if attr["id"] not in grouped]
groups.append(dict(attributes=other_attributes))
result.append(dict(attributes=other_attributes))
return groups
return result
@staticmethod
def create_or_update(type_id, name, attr_order, group_order=0, is_update=False):
@@ -890,10 +1224,16 @@ class CITypeAttributeGroupManager(object):
@classmethod
def transfer(cls, type_id, _from, _to):
current_app.logger.info("CIType[{0}] {1} -> {2}".format(type_id, _from, _to))
from_group = CITypeAttributeGroup.get_by_id(_from)
if isinstance(_from, int):
from_group = CITypeAttributeGroup.get_by_id(_from)
else:
from_group = CITypeAttributeGroup.get_by(name=_from, first=True, to_dict=False)
from_group or abort(404, ErrFormat.ci_type_attribute_group_not_found.format("id={}".format(_from)))
to_group = CITypeAttributeGroup.get_by_id(_to)
if isinstance(_to, int):
to_group = CITypeAttributeGroup.get_by_id(_to)
else:
to_group = CITypeAttributeGroup.get_by(name=_to, first=True, to_dict=False)
to_group or abort(404, ErrFormat.ci_type_attribute_group_not_found.format("id={}".format(_to)))
from_order, to_order = from_group.order, to_group.order
@@ -931,6 +1271,8 @@ class CITypeTemplateManager(object):
id2obj_dicts[added_id].get('child_id'),
id2obj_dicts[added_id].get('relation_type_id'),
id2obj_dicts[added_id].get('constraint'),
id2obj_dicts[added_id].get('parent_attr_ids'),
id2obj_dicts[added_id].get('child_attr_ids'),
)
else:
obj = cls.create(flush=True, **id2obj_dicts[added_id])
@@ -972,6 +1314,8 @@ class CITypeTemplateManager(object):
i.pop('choice_web_hook', None)
i.pop('choice_other', None)
i.pop('order', None)
i.pop('inherited', None)
i.pop('inherited_from', None)
choice_value = i.pop('choice_value', None)
if not choice_value:
i['is_choice'] = False
@@ -989,7 +1333,10 @@ class CITypeTemplateManager(object):
def _import_ci_types(self, ci_types, attr_id_map):
for i in ci_types:
i.pop("unique_key", None)
i.pop("show_name", None)
i['unique_id'] = attr_id_map.get(i['unique_id'], i['unique_id'])
if i.get('show_id'):
i['show_id'] = attr_id_map.get(i['show_id'], i['show_id'])
i['uid'] = current_user.uid
return self.__import(CIType, ci_types)
@@ -1083,6 +1430,8 @@ class CITypeTemplateManager(object):
_group = copy.deepcopy(group)
_group.pop('attributes', None)
_group.pop('id', None)
_group.pop('inherited', None)
_group.pop('inherited_from', None)
existed = CITypeAttributeGroup.get_by(name=_group['name'],
type_id=type_id_map.get(_group['type_id'], _group['type_id']),
first=True, to_dict=False)
@@ -1138,9 +1487,13 @@ class CITypeTemplateManager(object):
rule.pop("id", None)
rule.pop("created_at", None)
rule.pop("updated_at", None)
rule.pop("relation", None)
rule['uid'] = current_user.uid
if not rule.get('attributes'):
continue
existed = False
for i in AutoDiscoveryCIType.get_by(type_id=ci_type.id, adr_id=rule['adr_id'], to_dict=False):
if ((i.extra_option or {}).get('alias') or None) == (
@@ -1213,7 +1566,7 @@ class CITypeTemplateManager(object):
ci_types=CITypeManager.get_ci_types(),
ci_type_groups=CITypeGroupManager.get(),
relation_types=[i.to_dict() for i in RelationTypeManager.get_all()],
ci_type_relations=CITypeRelationManager.get(),
ci_type_relations=CITypeRelationManager.get()[0],
ci_type_auto_discovery_rules=list(),
type2attributes=dict(),
type2attribute_group=dict(),
@@ -1274,7 +1627,7 @@ class CITypeTemplateManager(object):
from api.lib.common_setting.upload_file import CommonFileCRUD
tpt = dict(
ci_types=CITypeManager.get_ci_types(type_name=ci_type.name),
ci_types=CITypeManager.get_ci_types(type_name=ci_type.name, like=False),
ci_type_auto_discovery_rules=list(),
type2attributes=dict(),
type2attribute_group=dict(),

View File

@@ -55,6 +55,9 @@ class CITypeOperateType(BaseEnum):
DELETE_UNIQUE_CONSTRAINT = "11" # 删除联合唯一
ADD_RELATION = "12" # 新增关系
DELETE_RELATION = "13" # 删除关系
ADD_RECONCILIATION = "14" # 新增数据合规
UPDATE_RECONCILIATION = "15" # 修改数据合规
DELETE_RECONCILIATION = "16" # 删除数据合规
class RetKey(BaseEnum):
@@ -70,6 +73,7 @@ class ResourceTypeEnum(BaseEnum):
RELATION_VIEW = "RelationView" # read/update/delete/grant
CI_FILTER = "CIFilter" # read
PAGE = "page" # read
TOPOLOGY_VIEW = "TopologyView" # read/update/delete/grant
class PermEnum(BaseEnum):
@@ -98,6 +102,12 @@ class AttributeDefaultValueEnum(BaseEnum):
AUTO_INC_ID = "$auto_inc_id"
class ExecuteStatusEnum(BaseEnum):
COMPLETED = '0'
FAILED = '1'
RUNNING = '2'
CMDB_QUEUE = "one_cmdb_async"
REDIS_PREFIX_CI = "ONE_CMDB"
REDIS_PREFIX_CI_RELATION = "CMDB_CI_RELATION"

View File

@@ -13,6 +13,7 @@ from api.lib.cmdb.const import OperateType
from api.lib.cmdb.perms import CIFilterPermsCRUD
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.perm.acl.cache import UserCache
from api.models.cmdb import CI
from api.models.cmdb import Attribute
from api.models.cmdb import AttributeHistory
from api.models.cmdb import CIRelationHistory
@@ -26,7 +27,7 @@ from api.models.cmdb import OperationRecord
class AttributeHistoryManger(object):
@staticmethod
def get_records_for_attributes(start, end, username, page, page_size, operate_type, type_id,
ci_id=None, attr_id=None):
ci_id=None, attr_id=None, ci_ids=None, more=False):
records = db.session.query(OperationRecord, AttributeHistory).join(
AttributeHistory, OperationRecord.id == AttributeHistory.record_id)
@@ -48,6 +49,9 @@ class AttributeHistoryManger(object):
if ci_id is not None:
records = records.filter(AttributeHistory.ci_id == ci_id)
if ci_ids and isinstance(ci_ids, list):
records = records.filter(AttributeHistory.ci_id.in_(ci_ids))
if attr_id is not None:
records = records.filter(AttributeHistory.attr_id == attr_id)
@@ -62,6 +66,12 @@ class AttributeHistoryManger(object):
if attr_hist['attr']:
attr_hist['attr_name'] = attr_hist['attr'].name
attr_hist['attr_alias'] = attr_hist['attr'].alias
if more:
attr_hist['is_list'] = attr_hist['attr'].is_list
attr_hist['is_computed'] = attr_hist['attr'].is_computed
attr_hist['is_password'] = attr_hist['attr'].is_password
attr_hist['default'] = attr_hist['attr'].default
attr_hist['value_type'] = attr_hist['attr'].value_type
attr_hist.pop("attr")
if record_id not in res:
@@ -161,12 +171,14 @@ class AttributeHistoryManger(object):
record = i.OperationRecord
item = dict(attr_name=attr.name,
attr_alias=attr.alias,
value_type=attr.value_type,
operate_type=hist.operate_type,
username=user and user.nickname,
old=hist.old,
new=hist.new,
created_at=record.created_at.strftime('%Y-%m-%d %H:%M:%S'),
record_id=record.id,
ticket_id=record.ticket_id,
hid=hist.id
)
result.append(item)
@@ -200,9 +212,9 @@ class AttributeHistoryManger(object):
return username, timestamp, attr_dict, rel_dict
@staticmethod
def add(record_id, ci_id, history_list, type_id=None, flush=False, commit=True):
def add(record_id, ci_id, history_list, type_id=None, ticket_id=None, flush=False, commit=True):
if record_id is None:
record = OperationRecord.create(uid=current_user.uid, type_id=type_id)
record = OperationRecord.create(uid=current_user.uid, type_id=type_id, ticket_id=ticket_id)
record_id = record.id
for attr_id, operate_type, old, new in history_list or []:
@@ -270,7 +282,7 @@ class CITypeHistoryManager(object):
return numfound, result
@staticmethod
def add(operate_type, type_id, attr_id=None, trigger_id=None, unique_constraint_id=None, change=None):
def add(operate_type, type_id, attr_id=None, trigger_id=None, unique_constraint_id=None, change=None, rc_id=None):
if type_id is None and attr_id is not None:
from api.models.cmdb import CITypeAttribute
type_ids = [i.type_id for i in CITypeAttribute.get_by(attr_id=attr_id, to_dict=False)]
@@ -283,6 +295,7 @@ class CITypeHistoryManager(object):
uid=current_user.uid,
attr_id=attr_id,
trigger_id=trigger_id,
rc_id=rc_id,
unique_constraint_id=unique_constraint_id,
change=change)
@@ -294,7 +307,7 @@ class CITriggerHistoryManager(object):
def get(page, page_size, type_id=None, trigger_id=None, operate_type=None):
query = CITriggerHistory.get_by(only_query=True)
if type_id:
query = query.filter(CITriggerHistory.type_id == type_id)
query = query.join(CI, CI.id == CITriggerHistory.ci_id).filter(CI.type_id == type_id)
if trigger_id:
query = query.filter(CITriggerHistory.trigger_id == trigger_id)

View File

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

View File

@@ -14,6 +14,8 @@ from api.lib.cmdb.attribute import AttributeManager
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import CITypeAttributesCache
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.cache import CMDBCounterCache
from api.lib.cmdb.ci_type import CITypeAttributeManager
from api.lib.cmdb.const import ConstraintEnum
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import ResourceTypeEnum
@@ -23,7 +25,10 @@ from api.lib.cmdb.resp_format import ErrFormat
from api.lib.exception import AbortException
from api.lib.perm.acl.acl import ACLManager
from api.models.cmdb import CITypeAttribute
from api.models.cmdb import CITypeGroup
from api.models.cmdb import CITypeGroupItem
from api.models.cmdb import CITypeRelation
from api.models.cmdb import PreferenceCITypeOrder
from api.models.cmdb import PreferenceRelationView
from api.models.cmdb import PreferenceSearchOption
from api.models.cmdb import PreferenceShowAttributes
@@ -38,15 +43,48 @@ class PreferenceManager(object):
@staticmethod
def get_types(instance=False, tree=False):
ci_type_order = sorted(PreferenceCITypeOrder.get_by(uid=current_user.uid, to_dict=False), key=lambda x: x.order)
type2group = {}
for i in db.session.query(CITypeGroupItem, CITypeGroup).join(
CITypeGroup, CITypeGroup.id == CITypeGroupItem.group_id).filter(
CITypeGroup.deleted.is_(False)).filter(CITypeGroupItem.deleted.is_(False)):
type2group[i.CITypeGroupItem.type_id] = i.CITypeGroup.to_dict()
types = db.session.query(PreferenceShowAttributes.type_id).filter(
PreferenceShowAttributes.uid == current_user.uid).filter(
PreferenceShowAttributes.deleted.is_(False)).group_by(
PreferenceShowAttributes.type_id).all() if instance else []
types = sorted(types, key=lambda x: {i.type_id: idx for idx, i in enumerate(
ci_type_order) if not i.is_tree}.get(x.type_id, 1))
group_types = []
other_types = []
group2idx = {}
type_ids = set()
for ci_type in types:
type_id = ci_type.type_id
type_ids.add(type_id)
type_dict = CITypeCache.get(type_id).to_dict()
if type_id not in type2group:
other_types.append(type_dict)
else:
group = type2group[type_id]
if group['id'] not in group2idx:
group_types.append(type2group[type_id])
group2idx[group['id']] = len(group_types) - 1
group_types[group2idx[group['id']]].setdefault('ci_types', []).append(type_dict)
if other_types:
group_types.append(dict(ci_types=other_types))
tree_types = PreferenceTreeView.get_by(uid=current_user.uid, to_dict=False) if tree else []
type_ids = set([i.type_id for i in types + tree_types])
tree_types = sorted(tree_types, key=lambda x: {i.type_id: idx for idx, i in enumerate(
ci_type_order) if i.is_tree}.get(x.type_id, 1))
return [CITypeCache.get(type_id).to_dict() for type_id in type_ids]
tree_types = [CITypeCache.get(_type.type_id).to_dict() for _type in tree_types]
for _type in tree_types:
type_ids.add(_type['id'])
return dict(group_types=group_types, tree_types=tree_types, type_ids=list(type_ids))
@staticmethod
def get_types2(instance=False, tree=False):
@@ -59,32 +97,36 @@ class PreferenceManager(object):
:param tree:
:return:
"""
result = dict(self=dict(instance=[], tree=[], type_id2subs_time=dict()),
type_id2users=dict())
result = dict(self=dict(instance=[], tree=[], type_id2subs_time=dict()))
result.update(CMDBCounterCache.get_sub_counter())
ci_type_order = sorted(PreferenceCITypeOrder.get_by(uid=current_user.uid, to_dict=False), key=lambda x: x.order)
if instance:
types = db.session.query(PreferenceShowAttributes.type_id,
PreferenceShowAttributes.uid, PreferenceShowAttributes.created_at).filter(
PreferenceShowAttributes.deleted.is_(False)).group_by(
PreferenceShowAttributes.deleted.is_(False)).filter(
PreferenceShowAttributes.uid == current_user.uid).group_by(
PreferenceShowAttributes.uid, PreferenceShowAttributes.type_id)
for i in types:
if i.uid == current_user.uid:
result['self']['instance'].append(i.type_id)
if str(i.created_at) > str(result['self']['type_id2subs_time'].get(i.type_id, "")):
result['self']['type_id2subs_time'][i.type_id] = i.created_at
result['self']['instance'].append(i.type_id)
if str(i.created_at) > str(result['self']['type_id2subs_time'].get(i.type_id, "")):
result['self']['type_id2subs_time'][i.type_id] = i.created_at
result['type_id2users'].setdefault(i.type_id, []).append(i.uid)
instance_order = [i.type_id for i in ci_type_order if not i.is_tree]
if len(instance_order) == len(result['self']['instance']):
result['self']['instance'] = instance_order
if tree:
types = PreferenceTreeView.get_by(to_dict=False)
types = PreferenceTreeView.get_by(uid=current_user.uid, to_dict=False)
for i in types:
if i.uid == current_user.uid:
result['self']['tree'].append(i.type_id)
if str(i.created_at) > str(result['self']['type_id2subs_time'].get(i.type_id, "")):
result['self']['type_id2subs_time'][i.type_id] = i.created_at
result['self']['tree'].append(i.type_id)
if str(i.created_at) > str(result['self']['type_id2subs_time'].get(i.type_id, "")):
result['self']['type_id2subs_time'][i.type_id] = i.created_at
result['type_id2users'].setdefault(i.type_id, [])
if i.uid not in result['type_id2users'][i.type_id]:
result['type_id2users'][i.type_id].append(i.uid)
tree_order = [i.type_id for i in ci_type_order if i.is_tree]
if len(tree_order) == len(result['self']['tree']):
result['self']['tree'] = tree_order
return result
@@ -98,8 +140,8 @@ class PreferenceManager(object):
CITypeAttribute, CITypeAttribute.attr_id == PreferenceShowAttributes.attr_id).filter(
PreferenceShowAttributes.uid == current_user.uid).filter(
PreferenceShowAttributes.type_id == type_id).filter(
PreferenceShowAttributes.deleted.is_(False)).filter(CITypeAttribute.deleted.is_(False)).filter(
CITypeAttribute.type_id == type_id).all()
PreferenceShowAttributes.deleted.is_(False)).filter(CITypeAttribute.deleted.is_(False)).group_by(
CITypeAttribute.attr_id).all()
result = []
for i in sorted(attrs, key=lambda x: x.PreferenceShowAttributes.order):
@@ -109,17 +151,16 @@ class PreferenceManager(object):
is_subscribed = True
if not attrs:
attrs = db.session.query(CITypeAttribute).filter(
CITypeAttribute.type_id == type_id).filter(
CITypeAttribute.deleted.is_(False)).filter(
CITypeAttribute.default_show.is_(True)).order_by(CITypeAttribute.order)
result = [i.attr.to_dict() for i in attrs]
result = CITypeAttributeManager.get_attributes_by_type_id(type_id,
choice_web_hook_parse=False,
choice_other_parse=False)
result = [i for i in result if i['default_show']]
is_subscribed = False
for i in result:
if i["is_choice"]:
i.update(dict(choice_value=AttributeManager.get_choice_values(
i["id"], i["value_type"], i["choice_web_hook"], i.get("choice_other"))))
i["id"], i["value_type"], i.get("choice_web_hook"), i.get("choice_other"))))
return is_subscribed, result
@@ -151,9 +192,22 @@ class PreferenceManager(object):
if i.attr_id not in attr_dict:
i.soft_delete()
if not existed_all and attr_order:
cls.add_ci_type_order_item(type_id, is_tree=False)
elif not PreferenceShowAttributes.get_by(type_id=type_id, uid=current_user.uid, to_dict=False):
cls.delete_ci_type_order_item(type_id, is_tree=False)
@staticmethod
def get_tree_view():
ci_type_order = sorted(PreferenceCITypeOrder.get_by(uid=current_user.uid, is_tree=True, to_dict=False),
key=lambda x: x.order)
res = PreferenceTreeView.get_by(uid=current_user.uid, to_dict=True)
if ci_type_order:
res = sorted(res, key=lambda x: {ii.type_id: idx for idx, ii in enumerate(
ci_type_order)}.get(x['type_id'], 1))
for item in res:
if item["levels"]:
ci_type = CITypeCache.get(item['type_id']).to_dict()
@@ -172,8 +226,8 @@ class PreferenceManager(object):
return res
@staticmethod
def create_or_update_tree_view(type_id, levels):
@classmethod
def create_or_update_tree_view(cls, type_id, levels):
attrs = CITypeAttributesCache.get(type_id)
for idx, i in enumerate(levels):
for attr in attrs:
@@ -185,9 +239,12 @@ class PreferenceManager(object):
if existed is not None:
if not levels:
existed.soft_delete()
cls.delete_ci_type_order_item(type_id, is_tree=True)
return existed
return existed.update(levels=levels)
elif levels:
cls.add_ci_type_order_item(type_id, is_tree=True)
return PreferenceTreeView.create(levels=levels, type_id=type_id, uid=current_user.uid)
@staticmethod
@@ -207,11 +264,13 @@ class PreferenceManager(object):
views = _views
view2cr_ids = dict()
name2view = dict()
result = dict()
name2id = list()
for view in views:
view2cr_ids.setdefault(view['name'], []).extend(view['cr_ids'])
name2id.append([view['name'], view['id']])
name2view[view['name']] = view
id2type = dict()
for view_name in view2cr_ids:
@@ -255,6 +314,8 @@ class PreferenceManager(object):
topo_flatten=topo_flatten,
level2constraint=level2constraint,
leaf=leaf,
option=name2view[view_name]['option'],
is_public=name2view[view_name]['is_public'],
leaf2show_types=leaf2show_types,
node2show_types=node2show_types,
show_types=[CITypeCache.get(j).to_dict()
@@ -262,18 +323,26 @@ class PreferenceManager(object):
for type_id in id2type:
id2type[type_id] = CITypeCache.get(type_id).to_dict()
id2type[type_id]['unique_name'] = AttributeCache.get(id2type[type_id]['unique_id']).name
if id2type[type_id]['show_id']:
show_attr = AttributeCache.get(id2type[type_id]['show_id'])
id2type[type_id]['show_name'] = show_attr and show_attr.name
return result, id2type, sorted(name2id, key=lambda x: x[1])
@classmethod
def create_or_update_relation_view(cls, name, cr_ids, is_public=False):
def create_or_update_relation_view(cls, name=None, cr_ids=None, _id=None, is_public=False, option=None):
if not cr_ids:
return abort(400, ErrFormat.preference_relation_view_node_required)
existed = PreferenceRelationView.get_by(name=name, to_dict=False, first=True)
if _id is None:
existed = PreferenceRelationView.get_by(name=name, to_dict=False, first=True)
else:
existed = PreferenceRelationView.get_by_id(_id)
current_app.logger.debug(existed)
if existed is None:
PreferenceRelationView.create(name=name, cr_ids=cr_ids, uid=current_user.uid, is_public=is_public)
PreferenceRelationView.create(name=name, cr_ids=cr_ids, uid=current_user.uid,
is_public=is_public, option=option)
if current_app.config.get("USE_ACL"):
ACLManager().add_resource(name, ResourceTypeEnum.RELATION_VIEW)
@@ -281,6 +350,11 @@ class PreferenceManager(object):
RoleEnum.CMDB_READ_ALL,
ResourceTypeEnum.RELATION_VIEW,
permissions=[PermEnum.READ])
else:
if existed.name != name and current_app.config.get("USE_ACL"):
ACLManager().update_resource(existed.name, name, ResourceTypeEnum.RELATION_VIEW)
existed.update(name=name, cr_ids=cr_ids, is_public=is_public, option=option)
return cls.get_relation_view()
@@ -356,6 +430,9 @@ class PreferenceManager(object):
for i in PreferenceTreeView.get_by(type_id=type_id, uid=uid, to_dict=False):
i.soft_delete()
for i in PreferenceCITypeOrder.get_by(type_id=type_id, uid=uid, to_dict=False):
i.soft_delete()
@staticmethod
def can_edit_relation(parent_id, child_id):
views = PreferenceRelationView.get_by(to_dict=False)
@@ -381,3 +458,36 @@ class PreferenceManager(object):
return False
return True
@staticmethod
def add_ci_type_order_item(type_id, is_tree=False):
max_order = PreferenceCITypeOrder.get_by(
uid=current_user.uid, is_tree=is_tree, only_query=True).order_by(PreferenceCITypeOrder.order.desc()).first()
order = (max_order and max_order.order + 1) or 1
PreferenceCITypeOrder.create(type_id=type_id, is_tree=is_tree, uid=current_user.uid, order=order)
@staticmethod
def delete_ci_type_order_item(type_id, is_tree=False):
existed = PreferenceCITypeOrder.get_by(uid=current_user.uid, type_id=type_id, is_tree=is_tree,
first=True, to_dict=False)
existed and existed.soft_delete()
@staticmethod
def upsert_ci_type_order(type_ids, is_tree=False):
for idx, type_id in enumerate(type_ids):
order = idx + 1
existed = PreferenceCITypeOrder.get_by(uid=current_user.uid, type_id=type_id, is_tree=is_tree,
to_dict=False, first=True)
if existed is not None:
existed.update(order=order, flush=True)
else:
PreferenceCITypeOrder.create(uid=current_user.uid, type_id=type_id, is_tree=is_tree, order=order,
flush=True)
try:
db.session.commit()
except Exception as e:
db.session.rollback()
current_app.logger.error("upsert citype order failed: {}".format(e))
return abort(400, ErrFormat.unknown_error)

View File

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

View File

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

View File

@@ -62,7 +62,7 @@ QUERY_CI_BY_ATTR_NAME = """
QUERY_CI_BY_ID = """
SELECT c_cis.id as ci_id
FROM c_cis
WHERE c_cis.id={}
WHERE c_cis.id {}
"""
QUERY_CI_BY_TYPE = """

View File

@@ -44,7 +44,11 @@ class Search(object):
count=1,
sort=None,
ci_ids=None,
excludes=None):
excludes=None,
parent_node_perm_passed=False,
use_id_filter=False,
use_ci_filter=True,
only_ids=False):
self.orig_query = query
self.fl = fl or []
self.excludes = excludes or []
@@ -54,12 +58,19 @@ class Search(object):
self.count = count
self.sort = sort
self.ci_ids = ci_ids or []
self.raw_ci_ids = copy.deepcopy(self.ci_ids)
self.query_sql = ""
self.type_id_list = []
self.only_type_query = False
self.parent_node_perm_passed = parent_node_perm_passed
self.use_id_filter = use_id_filter
self.use_ci_filter = use_ci_filter
self.only_ids = only_ids
self.valid_type_names = []
self.type2filter_perms = dict()
self.is_app_admin = is_app_admin('cmdb') or current_user.username == "worker"
self.is_app_admin = self.is_app_admin or (not self.use_ci_filter and not self.use_id_filter)
@staticmethod
def _operator_proc(key):
@@ -106,7 +117,7 @@ class Search(object):
self.type_id_list.append(str(ci_type.id))
if ci_type.id in self.type2filter_perms:
ci_filter = self.type2filter_perms[ci_type.id].get('ci_filter')
if ci_filter:
if ci_filter and self.use_ci_filter and not self.use_id_filter:
sub = []
ci_filter = Template(ci_filter).render(user=current_user)
for i in ci_filter.split(','):
@@ -122,6 +133,14 @@ class Search(object):
self.fl = set(self.type2filter_perms[ci_type.id]['attr_filter'])
else:
self.fl = set(self.fl) & set(self.type2filter_perms[ci_type.id]['attr_filter'])
if self.type2filter_perms[ci_type.id].get('id_filter') and self.use_id_filter:
if not self.raw_ci_ids:
self.ci_ids = list(self.type2filter_perms[ci_type.id]['id_filter'].keys())
if self.use_id_filter and not self.ci_ids and not self.is_app_admin:
self.raw_ci_ids = [0]
else:
raise SearchError(ErrFormat.no_permission.format(ci_type.alias, PermEnum.READ))
else:
@@ -138,7 +157,10 @@ class Search(object):
@staticmethod
def _id_query_handler(v):
return QUERY_CI_BY_ID.format(v)
if ";" in v:
return QUERY_CI_BY_ID.format("in {}".format(v.replace(';', ',')))
else:
return QUERY_CI_BY_ID.format("= {}".format(v))
@staticmethod
def _in_query_handler(attr, v, is_not):
@@ -152,6 +174,7 @@ class Search(object):
"NOT LIKE" if is_not else "LIKE",
_v.replace("*", "%")) for _v in new_v])
_query_sql = QUERY_CI_BY_ATTR_NAME.format(table_name, attr.id, in_query)
return _query_sql
@staticmethod
@@ -167,6 +190,7 @@ class Search(object):
"NOT BETWEEN" if is_not else "BETWEEN",
start.replace("*", "%"), end.replace("*", "%"))
_query_sql = QUERY_CI_BY_ATTR_NAME.format(table_name, attr.id, range_query)
return _query_sql
@staticmethod
@@ -183,6 +207,7 @@ class Search(object):
comparison_query = "{0} '{1}'".format(v[0], v[1:].replace("*", "%"))
_query_sql = QUERY_CI_BY_ATTR_NAME.format(table_name, attr.id, comparison_query)
return _query_sql
@staticmethod
@@ -194,6 +219,7 @@ class Search(object):
elif field.startswith("-"):
field = field[1:]
sort_type = "DESC"
return field, sort_type
def __sort_by_id(self, sort_type, query_sql):
@@ -322,6 +348,11 @@ class Search(object):
return numfound, res
def __get_type2filter_perms(self):
res2 = ACLManager('cmdb').get_resources(ResourceTypeEnum.CI_FILTER)
if res2:
self.type2filter_perms = CIFilterPermsCRUD().get_by_ids(list(map(int, [i['name'] for i in res2])))
def __get_types_has_read(self):
"""
:return: _type:(type1;type2)
@@ -331,14 +362,23 @@ class Search(object):
self.valid_type_names = {i['name'] for i in res if PermEnum.READ in i['permissions']}
res2 = acl.get_resources(ResourceTypeEnum.CI_FILTER)
if res2:
self.type2filter_perms = CIFilterPermsCRUD().get_by_ids(list(map(int, [i['name'] for i in res2])))
self.__get_type2filter_perms()
for type_id in self.type2filter_perms:
ci_type = CITypeCache.get(type_id)
if ci_type:
if self.type2filter_perms[type_id].get('id_filter'):
if self.use_id_filter:
self.valid_type_names.add(ci_type.name)
elif self.type2filter_perms[type_id].get('ci_filter'):
if self.use_ci_filter:
self.valid_type_names.add(ci_type.name)
else:
self.valid_type_names.add(ci_type.name)
return "_type:({})".format(";".join(self.valid_type_names))
def __confirm_type_first(self, queries):
has_type = False
result = []
@@ -371,8 +411,10 @@ class Search(object):
else:
result.append(q)
_is_app_admin = is_app_admin('cmdb') or current_user.username == "worker"
if result and not has_type and not _is_app_admin:
if self.parent_node_perm_passed:
self.__get_type2filter_perms()
self.valid_type_names = "ALL"
elif result and not has_type and not self.is_app_admin:
type_q = self.__get_types_has_read()
if id_query:
ci = CIManager.get_by_id(id_query)
@@ -381,13 +423,11 @@ class Search(object):
result.insert(0, "_type:{}".format(ci.type_id))
else:
result.insert(0, type_q)
elif _is_app_admin:
elif self.is_app_admin:
self.valid_type_names = "ALL"
else:
self.__get_types_has_read()
current_app.logger.warning(result)
return result
def __query_by_attr(self, q, queries, alias):
@@ -479,7 +519,7 @@ class Search(object):
def _filter_ids(self, query_sql):
if self.ci_ids:
return "SELECT * FROM ({0}) AS IN_QUERY WHERE IN_QUERY.ci_id IN ({1})".format(
query_sql, ",".join(list(map(str, self.ci_ids))))
query_sql, ",".join(list(set(map(str, self.ci_ids)))))
return query_sql
@@ -511,6 +551,9 @@ class Search(object):
s = time.time()
if query_sql:
query_sql = self._filter_ids(query_sql)
if self.raw_ci_ids and not self.ci_ids:
return 0, []
self.query_sql = query_sql
# current_app.logger.debug(query_sql)
numfound, res = self._execute_sql(query_sql)
@@ -550,6 +593,8 @@ class Search(object):
def search(self):
numfound, ci_ids = self._query_build_raw()
ci_ids = list(map(str, ci_ids))
if self.only_ids:
return ci_ids
_fl = self._fl_build()
@@ -569,3 +614,8 @@ class Search(object):
total = len(response)
return response, counter, total, self.page, numfound, facet
def get_ci_ids(self):
_, ci_ids = self._query_build_raw()
return ci_ids

View File

@@ -1,21 +1,30 @@
# -*- coding:utf-8 -*-
import json
import sys
from collections import Counter
from flask import abort
from flask import current_app
from flask_login import current_user
from api.extensions import rd
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.ci import CIRelationManager
from api.lib.cmdb.ci_type import CITypeRelationManager
from api.lib.cmdb.const import ConstraintEnum
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION2
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.perms import CIFilterPermsCRUD
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.search.ci.db.search import Search as SearchFromDB
from api.lib.cmdb.search.ci.es.search import Search as SearchFromES
from api.lib.cmdb.utils import TableMap
from api.lib.cmdb.utils import ValueTypeMap
from api.lib.perm.acl.acl import ACLManager
from api.lib.perm.acl.acl import is_app_admin
from api.models.cmdb import CI
from api.models.cmdb import CIRelation
class Search(object):
@@ -29,7 +38,9 @@ class Search(object):
sort=None,
reverse=False,
ancestor_ids=None,
has_m2m=None):
descendant_ids=None,
has_m2m=None,
root_parent_path=None):
self.orig_query = query
self.fl = fl
self.facet_field = facet_field
@@ -46,6 +57,8 @@ class Search(object):
level[0] if isinstance(level, list) and level else level)
self.ancestor_ids = ancestor_ids
self.descendant_ids = descendant_ids
self.root_parent_path = root_parent_path
self.has_m2m = has_m2m or False
if not self.has_m2m:
if self.ancestor_ids:
@@ -56,27 +69,23 @@ class Search(object):
if _l < int(level) and c == ConstraintEnum.Many2Many:
self.has_m2m = True
self.type2filter_perms = {}
self.is_app_admin = is_app_admin('cmdb') or current_user.username == "worker"
def _get_ids(self, ids):
if self.level[-1] == 1 and len(ids) == 1:
if self.ancestor_ids is None:
return [i.second_ci_id for i in CIRelation.get_by(first_ci_id=ids[0], to_dict=False)]
else:
seconds = {i.second_ci_id for i in CIRelation.get_by(first_ci_id=ids[0],
ancestor_ids=self.ancestor_ids,
to_dict=False)}
return list(seconds)
merge_ids = []
key = []
_tmp = []
for level in range(1, sorted(self.level)[-1] + 1):
if len(self.descendant_ids or []) >= level and self.type2filter_perms.get(self.descendant_ids[level - 1]):
id_filter_limit, _ = self._get_ci_filter(self.type2filter_perms[self.descendant_ids[level - 1]])
else:
id_filter_limit = {}
if not self.has_m2m:
_tmp = map(lambda x: json.loads(x).keys(),
filter(lambda x: x is not None, rd.get(ids, REDIS_PREFIX_CI_RELATION) or []))
ids = [j for i in _tmp for j in i]
key, prefix = ids, REDIS_PREFIX_CI_RELATION
key, prefix = list(map(str, ids)), REDIS_PREFIX_CI_RELATION
else:
if not self.ancestor_ids:
@@ -92,12 +101,16 @@ class Search(object):
key = list(set(["{},{}".format(i, j) for idx, i in enumerate(key) for j in _tmp[idx]]))
prefix = REDIS_PREFIX_CI_RELATION2
_tmp = list(map(lambda x: json.loads(x).keys() if x else [], rd.get(key, prefix) or []))
ids = [j for i in _tmp for j in i]
if not key:
if not key or id_filter_limit is None:
return []
res = [json.loads(x).items() for x in [i or '{}' for i in rd.get(key, prefix) or []]]
_tmp = [[i[0] for i in x if (not id_filter_limit or (
key[idx] not in id_filter_limit or int(i[0]) in id_filter_limit[key[idx]]) or
int(i[0]) in id_filter_limit)] for idx, x in enumerate(res)]
ids = [j for i in _tmp for j in i]
if level in self.level:
merge_ids.extend(ids)
@@ -120,7 +133,28 @@ class Search(object):
return merge_ids
def search(self):
def _has_read_perm_from_parent_nodes(self):
self.root_parent_path = list(map(str, self.root_parent_path))
if str(self.root_id).isdigit() and str(self.root_id) not in self.root_parent_path:
self.root_parent_path.append(str(self.root_id))
self.root_parent_path = set(self.root_parent_path)
if self.is_app_admin:
self.type2filter_perms = {}
return True
res = ACLManager().get_resources(ResourceTypeEnum.CI_FILTER) or {}
self.type2filter_perms = CIFilterPermsCRUD().get_by_ids(list(map(int, [i['name'] for i in res]))) or {}
for _, filters in self.type2filter_perms.items():
if set((filters.get('id_filter') or {}).keys()) & self.root_parent_path:
return True
return True
def search(self, only_ids=False):
use_ci_filter = len(self.descendant_ids or []) == self.level[0] - 1
parent_node_perm_passed = not self.is_app_admin and self._has_read_perm_from_parent_nodes()
ids = [self.root_id] if not isinstance(self.root_id, list) else self.root_id
cis = [CI.get_by_id(_id) or abort(404, ErrFormat.ci_not_found.format("id={}".format(_id))) for _id in ids]
@@ -161,42 +195,106 @@ class Search(object):
page=self.page,
count=self.count,
sort=self.sort,
ci_ids=merge_ids).search()
ci_ids=merge_ids,
parent_node_perm_passed=parent_node_perm_passed,
use_ci_filter=use_ci_filter,
only_ids=only_ids).search()
def statistics(self, type_ids):
def _get_ci_filter(self, filter_perms, ci_filters=None):
ci_filters = ci_filters or []
if ci_filters:
result = {}
for item in ci_filters:
res = SearchFromDB('_type:{},{}'.format(item['type_id'], item['ci_filter']),
count=sys.maxsize, parent_node_perm_passed=True).get_ci_ids()
if res:
result[item['type_id']] = set(res)
return {}, result if result else None
result = dict()
if filter_perms.get('id_filter'):
for k in filter_perms['id_filter']:
node_path = k.split(',')
if len(node_path) == 1:
result[int(node_path[0])] = 1
elif not self.has_m2m:
result.setdefault(node_path[-2], set()).add(int(node_path[-1]))
else:
result.setdefault(','.join(node_path[:-1]), set()).add(int(node_path[-1]))
if result:
return result, None
else:
return None, None
return {}, None
def statistics(self, type_ids, need_filter=True):
self.level = int(self.level)
acl = ACLManager('cmdb')
type2filter_perms = dict()
if not self.is_app_admin:
res2 = acl.get_resources(ResourceTypeEnum.CI_FILTER)
if res2:
type2filter_perms = CIFilterPermsCRUD().get_by_ids(list(map(int, [i['name'] for i in res2])))
ids = [self.root_id] if not isinstance(self.root_id, list) else self.root_id
_tmp = []
_tmp, tmp_res = [], []
level2ids = {}
for lv in range(1, self.level + 1):
level2ids[lv] = []
if need_filter:
id_filter_limit, ci_filter_limit = None, None
if len(self.descendant_ids or []) >= lv and type2filter_perms.get(self.descendant_ids[lv - 1]):
id_filter_limit, _ = self._get_ci_filter(type2filter_perms[self.descendant_ids[lv - 1]])
elif type_ids and self.level == lv:
ci_filters = [type2filter_perms[type_id] for type_id in type_ids if type_id in type2filter_perms]
if ci_filters:
id_filter_limit, ci_filter_limit = self._get_ci_filter({}, ci_filters=ci_filters)
else:
id_filter_limit = {}
else:
id_filter_limit = {}
else:
id_filter_limit, ci_filter_limit = {}, {}
if lv == 1:
if not self.has_m2m:
key, prefix = ids, REDIS_PREFIX_CI_RELATION
key, prefix = [str(i) for i in ids], REDIS_PREFIX_CI_RELATION
else:
key = ["{},{}".format(self.ancestor_ids, _id) for _id in ids]
if not self.ancestor_ids:
key, prefix = ids, REDIS_PREFIX_CI_RELATION
key, prefix = [str(i) for i in ids], REDIS_PREFIX_CI_RELATION
else:
key = ["{},{}".format(self.ancestor_ids, _id) for _id in ids]
prefix = REDIS_PREFIX_CI_RELATION2
level2ids[lv] = [[i] for i in key]
if not key:
_tmp = []
if not key or id_filter_limit is None:
_tmp = [[]] * len(ids)
continue
res = [json.loads(x).items() for x in [i or '{}' for i in rd.get(key, prefix) or []]]
_tmp = []
if type_ids and lv == self.level:
_tmp = list(map(lambda x: [i for i in x if i[1] in type_ids],
(map(lambda x: list(json.loads(x).items()),
[i or '{}' for i in rd.get(key, prefix) or []]))))
_tmp = [[i for i in x if i[1] in type_ids and
(not id_filter_limit or (key[idx] not in id_filter_limit or
int(i[0]) in id_filter_limit[key[idx]]) or
int(i[0]) in id_filter_limit)] for idx, x in enumerate(res)]
else:
_tmp = list(map(lambda x: list(json.loads(x).items()),
[i or '{}' for i in rd.get(key, prefix) or []]))
_tmp = [[i for i in x if (not id_filter_limit or (key[idx] not in id_filter_limit or
int(i[0]) in id_filter_limit[key[idx]]) or
int(i[0]) in id_filter_limit)] for idx, x in enumerate(res)]
if ci_filter_limit:
_tmp = [[j for j in i if j[1] not in ci_filter_limit or int(j[0]) in ci_filter_limit[j[1]]]
for i in _tmp]
else:
for idx, item in enumerate(_tmp):
if item:
if not self.has_m2m:
@@ -208,19 +306,28 @@ class Search(object):
level2ids[lv].append(key)
if key:
res = [json.loads(x).items() for x in [i or '{}' for i in rd.get(key, prefix) or []]]
if type_ids and lv == self.level:
__tmp = map(lambda x: [(_id, type_id) for _id, type_id in json.loads(x).items()
if type_id in type_ids],
filter(lambda x: x is not None,
rd.get(key, prefix) or []))
tmp_res = [[i for i in x if i[1] in type_ids and
(not id_filter_limit or (
key[idx] not in id_filter_limit or
int(i[0]) in id_filter_limit[key[idx]]) or
int(i[0]) in id_filter_limit)] for idx, x in enumerate(res)]
else:
__tmp = map(lambda x: list(json.loads(x).items()),
filter(lambda x: x is not None,
rd.get(key, prefix) or []))
else:
__tmp = []
tmp_res = [[i for i in x if (not id_filter_limit or (
key[idx] not in id_filter_limit or
int(i[0]) in id_filter_limit[key[idx]]) or
int(i[0]) in id_filter_limit)] for idx, x in
enumerate(res)]
_tmp[idx] = [j for i in __tmp for j in i]
if ci_filter_limit:
tmp_res = [[j for j in i if j[1] not in ci_filter_limit or
int(j[0]) in ci_filter_limit[j[1]]] for i in tmp_res]
else:
tmp_res = []
if tmp_res:
_tmp[idx] = [j for i in tmp_res for j in i]
else:
_tmp[idx] = []
level2ids[lv].append([])
@@ -231,3 +338,84 @@ class Search(object):
detail={str(_id): dict(Counter([i[1] for i in _tmp[idx]]).items()) for idx, _id in enumerate(ids)})
return result
def search_full(self, type_ids):
def _get_id2name(_type_id):
ci_type = CITypeCache.get(_type_id)
attr = AttributeCache.get(ci_type.unique_id)
value_table = TableMap(attr=attr).table
serializer = ValueTypeMap.serialize[attr.value_type]
unique_value = {i.ci_id: serializer(i.value) for i in value_table.get_by(attr_id=attr.id, to_dict=False)}
attr = AttributeCache.get(ci_type.show_id)
if attr:
value_table = TableMap(attr=attr).table
serializer = ValueTypeMap.serialize[attr.value_type]
show_value = {i.ci_id: serializer(i.value) for i in value_table.get_by(attr_id=attr.id, to_dict=False)}
else:
show_value = unique_value
return show_value, unique_value
self.level = int(self.level)
acl = ACLManager('cmdb')
type2filter_perms = dict()
if not self.is_app_admin:
res2 = acl.get_resources(ResourceTypeEnum.CI_FILTER)
if res2:
type2filter_perms = CIFilterPermsCRUD().get_by_ids(list(map(int, [i['name'] for i in res2])))
ids = [self.root_id] if not isinstance(self.root_id, list) else self.root_id
level_ids = [str(i) for i in ids]
result = []
id2children = {}
id2name = _get_id2name(type_ids[0])
for i in level_ids:
item = dict(id=int(i),
type_id=type_ids[0],
isLeaf=False,
title=id2name[0].get(int(i)),
uniqueValue=id2name[1].get(int(i)),
children=[])
result.append(item)
id2children[str(i)] = item['children']
for lv in range(1, self.level):
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]])
else:
id_filter_limit = {}
if self.has_m2m and lv != 1:
key, prefix = [i for i in level_ids], REDIS_PREFIX_CI_RELATION2
else:
key, prefix = [i.split(',')[-1] for i in level_ids], REDIS_PREFIX_CI_RELATION
res = [json.loads(x).items() for x in [i or '{}' for i in rd.get(key, prefix) or []]]
res = [[i for i in x if (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 []):
item = dict(id=int(child_id),
type_id=type_id,
isLeaf=True if lv == self.level - 1 else False,
title=id2name[0].get(int(child_id)),
uniqueValue=id2name[1].get(int(child_id)),
children=[])
id2children[node_path].append(item)
_node_path = "{},{}".format(node_path, child_id)
_level_ids.append(_node_path)
id2children[_node_path] = item['children']
level_ids = _level_ids
return result

View File

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

View File

@@ -11,12 +11,21 @@ import six
import api.models.cmdb as model
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.const import ValueTypeEnum
from api.lib.cmdb.resp_format import ErrFormat
TIME_RE = re.compile(r'(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d')
class ValueDeserializeError(Exception):
pass
def string2int(x):
return int(float(x))
v = int(float(x))
if v > 2147483647:
raise ValueDeserializeError(ErrFormat.attribute_value_out_of_range)
return v
def str2datetime(x):

View File

@@ -6,6 +6,7 @@ from __future__ import unicode_literals
import copy
import imp
import os
import re
import tempfile
import jinja2
@@ -13,6 +14,7 @@ from flask import abort
from flask import current_app
from jinja2schema import infer
from jinja2schema import to_json_schema
from werkzeug.exceptions import BadRequest
from api.extensions import db
from api.lib.cmdb.attribute import AttributeManager
@@ -23,6 +25,7 @@ from api.lib.cmdb.const import ValueTypeEnum
from api.lib.cmdb.history import AttributeHistoryManger
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.utils import TableMap
from api.lib.cmdb.utils import ValueDeserializeError
from api.lib.cmdb.utils import ValueTypeMap
from api.lib.utils import handle_arg_list
from api.models.cmdb import CI
@@ -80,7 +83,7 @@ class AttributeValueManager(object):
return res
@staticmethod
def _deserialize_value(value_type, value):
def _deserialize_value(alias, value_type, value):
if not value:
return value
@@ -88,14 +91,21 @@ class AttributeValueManager(object):
try:
v = deserialize(value)
return v
except ValueDeserializeError as e:
return abort(400, ErrFormat.attribute_value_invalid2.format(alias, e))
except ValueError:
return abort(400, ErrFormat.attribute_value_invalid.format(value))
@staticmethod
def _check_is_choice(attr, value_type, value):
choice_values = AttributeManager.get_choice_values(attr.id, value_type, attr.choice_web_hook, attr.choice_other)
if str(value) not in list(map(str, [i[0] for i in choice_values])):
return abort(400, ErrFormat.not_in_choice_values.format(value))
if value_type == ValueTypeEnum.FLOAT:
if float(value) not in list(map(float, [i[0] for i in choice_values])):
return abort(400, ErrFormat.not_in_choice_values.format(value))
else:
if str(value) not in list(map(str, [i[0] for i in choice_values])):
return abort(400, ErrFormat.not_in_choice_values.format(value))
@staticmethod
def _check_is_unique(value_table, attr, ci_id, type_id, value):
@@ -112,9 +122,14 @@ class AttributeValueManager(object):
if type_attr and type_attr.is_required and not value and value != 0:
return abort(400, ErrFormat.attribute_value_required.format(attr.alias))
@staticmethod
def check_re(expr, value):
if not re.compile(expr).match(str(value)):
return abort(400, ErrFormat.attribute_value_invalid.format(value))
def _validate(self, attr, value, value_table, ci=None, type_id=None, ci_id=None, type_attr=None):
ci = ci or {}
v = self._deserialize_value(attr.value_type, value)
v = self._deserialize_value(attr.alias, attr.value_type, value)
attr.is_choice and value and self._check_is_choice(attr, attr.value_type, v)
attr.is_unique and self._check_is_unique(
@@ -125,6 +140,9 @@ class AttributeValueManager(object):
if v == "" and attr.value_type not in (ValueTypeEnum.TEXT,):
v = None
if attr.re_check and value:
self.check_re(attr.re_check, value)
return v
@staticmethod
@@ -132,9 +150,10 @@ class AttributeValueManager(object):
return AttributeHistoryManger.add(record_id, ci_id, [(attr_id, operate_type, old, new)], type_id)
@staticmethod
def write_change2(changed, record_id=None):
def write_change2(changed, record_id=None, ticket_id=None):
for ci_id, attr_id, operate_type, old, new, type_id in changed:
record_id = AttributeHistoryManger.add(record_id, ci_id, [(attr_id, operate_type, old, new)], type_id,
ticket_id=ticket_id,
commit=False, flush=False)
try:
db.session.commit()
@@ -227,6 +246,8 @@ class AttributeValueManager(object):
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))
ci_dict[key] = value
except BadRequest as e:
raise
except Exception as e:
current_app.logger.warning(str(e))
@@ -235,15 +256,17 @@ class AttributeValueManager(object):
return key2attr
def create_or_update_attr_value(self, ci, ci_dict, key2attr):
def create_or_update_attr_value(self, ci, ci_dict, key2attr, ticket_id=None):
"""
add or update attribute value, then write history
:param ci: instance object
:param ci_dict: attribute dict
:param key2attr: attr key to attr
:param ticket_id:
:return:
"""
changed = []
has_dynamic = False
for key, value in ci_dict.items():
attr = key2attr.get(key)
if not attr:
@@ -252,46 +275,71 @@ class AttributeValueManager(object):
if attr.is_list:
existed_attrs = value_table.get_by(attr_id=attr.id, ci_id=ci.id, to_dict=False)
existed_values = [i.value for i in existed_attrs]
added = set(value) - set(existed_values)
deleted = set(existed_values) - set(value)
for v in added:
value_table.create(ci_id=ci.id, attr_id=attr.id, value=v, flush=False, commit=False)
changed.append((ci.id, attr.id, OperateType.ADD, None, v, ci.type_id))
existed_values = [(ValueTypeMap.serialize[attr.value_type](i.value) if
i.value or i.value == 0 else i.value) for i in existed_attrs]
for v in deleted:
existed_attr = existed_attrs[existed_values.index(v)]
# Comparison array starts from which position changes
min_len = min(len(value), len(existed_values))
index = 0
while index < min_len:
if value[index] != existed_values[index]:
break
index += 1
# Delete first and then add to ensure id sorting
for idx in range(index, len(existed_attrs)):
existed_attr = existed_attrs[idx]
existed_attr.delete(flush=False, commit=False)
changed.append((ci.id, attr.id, OperateType.DELETE, v, None, ci.type_id))
if not attr.is_dynamic:
changed.append((ci.id, attr.id, OperateType.DELETE, existed_values[idx], None, ci.type_id))
else:
has_dynamic = True
for idx in range(index, len(value)):
value_table.create(ci_id=ci.id, attr_id=attr.id, value=value[idx], flush=False, commit=False)
if not attr.is_dynamic:
changed.append((ci.id, attr.id, OperateType.ADD, None, value[idx], ci.type_id))
else:
has_dynamic = True
else:
existed_attr = value_table.get_by(attr_id=attr.id, ci_id=ci.id, first=True, to_dict=False)
existed_value = existed_attr and existed_attr.value
existed_value = (ValueTypeMap.serialize[attr.value_type](existed_value) if
existed_value or existed_value == 0 else existed_value)
if existed_value is None and value is not None:
value_table.create(ci_id=ci.id, attr_id=attr.id, value=value, flush=False, commit=False)
changed.append((ci.id, attr.id, OperateType.ADD, None, value, ci.type_id))
if not attr.is_dynamic:
changed.append((ci.id, attr.id, OperateType.ADD, None, value, ci.type_id))
else:
has_dynamic = True
else:
if existed_value != value:
if existed_value != value and existed_attr:
if value is None:
existed_attr.delete(flush=False, commit=False)
else:
existed_attr.update(value=value, flush=False, commit=False)
changed.append((ci.id, attr.id, OperateType.UPDATE, existed_value, value, ci.type_id))
if not attr.is_dynamic:
changed.append((ci.id, attr.id, OperateType.UPDATE, existed_value, value, ci.type_id))
else:
has_dynamic = True
try:
db.session.commit()
except Exception as e:
db.session.rollback()
current_app.logger.warning(str(e))
return abort(400, ErrFormat.attribute_value_unknown_error.format(e.args[0]))
if changed or has_dynamic:
try:
db.session.commit()
except Exception as e:
db.session.rollback()
current_app.logger.warning(str(e))
return abort(400, ErrFormat.attribute_value_unknown_error.format(e.args[0]))
return self.write_change2(changed)
return self.write_change2(changed, ticket_id=ticket_id), has_dynamic
else:
return None, has_dynamic
@staticmethod
def delete_attr_value(attr_id, ci_id):
def delete_attr_value(attr_id, ci_id, commit=True):
attr = AttributeCache.get(attr_id)
if attr is not None:
value_table = TableMap(attr=attr).table
for item in value_table.get_by(attr_id=attr.id, ci_id=ci_id, to_dict=False):
item.delete()
item.delete(commit=commit)

View File

@@ -10,6 +10,11 @@ from api.lib.perm.acl.role import RoleCRUD, RoleRelationCRUD
from api.lib.perm.acl.user import UserCRUD
def validate_app(app_id):
app = AppCache.get(app_id)
return app.id if app else None
class ACLManager(object):
def __init__(self, app_name='acl', uid=None):
self.log = current_app.logger
@@ -133,7 +138,8 @@ class ACLManager(object):
numfound, res = ResourceCRUD.search(q, u, self.validate_app().id, rt_id, page, page_size)
return res
def grant_resource(self, rid, resource_id, perms):
@staticmethod
def grant_resource(rid, resource_id, perms):
PermissionCRUD.grant(rid, perms, resource_id=resource_id, group_id=None)
@staticmethod
@@ -141,3 +147,7 @@ class ACLManager(object):
rt = AppCRUD.add(**payload)
return rt.to_dict()
def role_has_perms(self, rid, resource_name, resource_type_name, perm):
app_id = validate_app(self.app_name)
return RoleCRUD.has_permission(rid, resource_name, resource_type_name, app_id, perm)

View File

@@ -35,3 +35,32 @@ AuthCommonConfigAutoRedirect = 'auto_redirect'
class TestType(BaseEnum):
Connect = 'connect'
Login = 'login'
MIMEExtMap = {
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx',
'application/msword': '.doc',
'application/vnd.ms-word.document.macroEnabled.12': '.docm',
'application/vnd.ms-excel': '.xls',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx',
'application/vnd.ms-excel.sheet.macroEnabled.12': '.xlsm',
'application/vnd.ms-powerpoint': '.ppt',
'application/vnd.openxmlformats-officedocument.presentationml.presentation': '.pptx',
'application/vnd.ms-powerpoint.presentation.macroEnabled.12': '.pptm',
'application/zip': '.zip',
'application/x-7z-compressed': '.7z',
'application/json': '.json',
'application/pdf': '.pdf',
'image/png': '.png',
'image/bmp': '.bmp',
'image/prs.btif': '.btif',
'image/gif': '.gif',
'image/jpeg': '.jpg',
'image/tiff': '.tif',
'image/vnd.microsoft.icon': '.ico',
'image/webp': '.webp',
'image/svg+xml': '.svg',
'image/vnd.adobe.photoshop': '.psd',
'text/plain': '.txt',
'text/csv': '.csv',
}

View File

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

View File

@@ -24,7 +24,15 @@ def get_all_department_list(to_dict=True):
*criterion
).order_by(Department.department_id.asc())
results = query.all()
return [r.to_dict() for r in results] if to_dict else results
if to_dict:
datas = []
for r in results:
d = r.to_dict()
if r.department_id == 0:
d['department_name'] = ErrFormat.company_wide
datas.append(d)
return datas
return results
def get_all_employee_list(block=0, to_dict=True):
@@ -101,6 +109,7 @@ class DepartmentTree(object):
employees = self.get_employees_by_d_id(department_id)
top_d['employees'] = employees
top_d['department_name'] = ErrFormat.company_wide
if len(sub_deps) == 0:
top_d[sub_departments_column_name] = []
d_list.append(top_d)
@@ -246,7 +255,7 @@ class DepartmentCRUD(object):
return abort(400, ErrFormat.acl_update_role_failed.format(str(e)))
try:
existed.update(**kwargs)
return existed.update(**kwargs)
except Exception as e:
return abort(400, str(e))
@@ -313,6 +322,7 @@ class DepartmentCRUD(object):
tree_list = []
for top_d in top_deps:
top_d['department_name'] = ErrFormat.company_wide
tree = Tree()
identifier_root = top_d['department_id']
tree.create_node(
@@ -383,6 +393,9 @@ class DepartmentCRUD(object):
d['employee_count'] = len(list(filter(lambda e: e['department_id'] in d_ids, all_employee_list)))
if int(department_parent_id) == -1:
d['department_name'] = ErrFormat.company_wide
return all_departments, department_id_list
@staticmethod
@@ -457,8 +470,58 @@ class EditDepartmentInACL(object):
return f"edit_department_name_in_acl, rid: {d_rid}, success"
@classmethod
def remove_from_old_department_role(cls, e_list, acl):
result = []
for employee in e_list:
employee_acl_rid = employee.get('e_acl_rid')
if employee_acl_rid == 0:
result.append(f"employee_acl_rid == 0")
continue
cls.remove_single_employee_from_old_department(acl, employee, result)
@staticmethod
def edit_employee_department_in_acl(e_list: list, new_d_id: int, op_uid: int):
def remove_single_employee_from_old_department(acl, employee, result):
from api.models.acl import Role
old_department = DepartmentCRUD.get_department_by_id(employee.get('department_id'), False)
if not old_department:
return False
old_role = Role.get_by(first=True, name=old_department.department_name, app_id=None)
old_d_rid_in_acl = old_role.get('id') if old_role else 0
if old_d_rid_in_acl == 0:
return False
d_acl_rid = old_department.acl_rid if old_d_rid_in_acl == old_department.acl_rid else old_d_rid_in_acl
payload = {
'app_id': 'acl',
'parent_id': d_acl_rid,
}
try:
acl.remove_user_from_role(employee.get('e_acl_rid'), payload)
current_app.logger.info(f"remove {employee.get('e_acl_rid')} from {d_acl_rid}")
except Exception as e:
result.append(
f"remove_user_from_role employee_acl_rid: {employee.get('e_acl_rid')}, parent_id: {d_acl_rid}, err: {e}")
return True
@staticmethod
def add_employee_to_new_department(acl, employee_acl_rid, new_department_acl_rid, result):
payload = {
'app_id': 'acl',
'child_ids': [employee_acl_rid],
}
try:
acl.add_user_to_role(new_department_acl_rid, payload)
current_app.logger.info(f"add {employee_acl_rid} to {new_department_acl_rid}")
except Exception as e:
result.append(
f"add_user_to_role employee_acl_rid: {employee_acl_rid}, parent_id: {new_department_acl_rid}, \
err: {e}")
@classmethod
def edit_employee_department_in_acl(cls, e_list: list, new_d_id: int, op_uid: int):
result = []
new_department = DepartmentCRUD.get_department_by_id(new_d_id, False)
if not new_department:
@@ -468,7 +531,11 @@ class EditDepartmentInACL(object):
from api.models.acl import Role
new_role = Role.get_by(first=True, name=new_department.department_name, app_id=None)
new_d_rid_in_acl = new_role.get('id') if new_role else 0
acl = ACLManager('acl', str(op_uid))
if new_d_rid_in_acl == 0:
# only remove from old department role
cls.remove_from_old_department_role(e_list, acl)
return
if new_d_rid_in_acl != new_department.acl_rid:
@@ -478,43 +545,15 @@ class EditDepartmentInACL(object):
new_department_acl_rid = new_department.acl_rid if new_d_rid_in_acl == new_department.acl_rid else \
new_d_rid_in_acl
acl = ACLManager('acl', str(op_uid))
for employee in e_list:
old_department = DepartmentCRUD.get_department_by_id(employee.get('department_id'), False)
if not old_department:
continue
employee_acl_rid = employee.get('e_acl_rid')
if employee_acl_rid == 0:
result.append(f"employee_acl_rid == 0")
continue
old_role = Role.get_by(first=True, name=old_department.department_name, app_id=None)
old_d_rid_in_acl = old_role.get('id') if old_role else 0
if old_d_rid_in_acl == 0:
return
if old_d_rid_in_acl != old_department.acl_rid:
old_department.update(
acl_rid=old_d_rid_in_acl
)
d_acl_rid = old_department.acl_rid if old_d_rid_in_acl == old_department.acl_rid else old_d_rid_in_acl
payload = {
'app_id': 'acl',
'parent_id': d_acl_rid,
}
try:
acl.remove_user_from_role(employee_acl_rid, payload)
except Exception as e:
result.append(
f"remove_user_from_role employee_acl_rid: {employee_acl_rid}, parent_id: {d_acl_rid}, err: {e}")
cls.remove_single_employee_from_old_department(acl, employee, result)
payload = {
'app_id': 'acl',
'child_ids': [employee_acl_rid],
}
try:
acl.add_user_to_role(new_department_acl_rid, payload)
except Exception as e:
result.append(
f"add_user_to_role employee_acl_rid: {employee_acl_rid}, parent_id: {d_acl_rid}, err: {e}")
# 在新部门中添加员工
cls.add_employee_to_new_department(acl, employee_acl_rid, new_department_acl_rid, result)
return result

View File

@@ -16,7 +16,7 @@ from wtforms import validators
from api.extensions import db
from api.lib.common_setting.acl import ACLManager
from api.lib.common_setting.const import OperatorType
from api.lib.cmdb.const import CMDB_QUEUE
from api.lib.perm.acl.const import ACL_QUEUE
from api.lib.common_setting.resp_format import ErrFormat
from api.models.common_setting import Employee, Department
@@ -141,7 +141,7 @@ class EmployeeCRUD(object):
def add(**kwargs):
try:
res = CreateEmployee().create_single(**kwargs)
refresh_employee_acl_info.apply_async(args=(), queue=CMDB_QUEUE)
refresh_employee_acl_info.apply_async(args=(res.employee_id,), queue=ACL_QUEUE)
return res
except Exception as e:
abort(400, str(e))
@@ -171,7 +171,7 @@ class EmployeeCRUD(object):
if len(e_list) > 0:
edit_employee_department_in_acl.apply_async(
args=(e_list, new_department_id, current_user.uid),
queue=CMDB_QUEUE
queue=ACL_QUEUE
)
return existed
@@ -296,6 +296,8 @@ class EmployeeCRUD(object):
for r in pagination.items:
d = r.Employee.to_dict()
d['department_name'] = r.Department.department_name if r.Department else ''
if r.Employee.department_id == 0:
d['department_name'] = ErrFormat.company_wide
employees.append(d)
return {
@@ -575,7 +577,6 @@ class EmployeeCRUD(object):
@staticmethod
def import_employee(employee_list):
res = CreateEmployee().batch_create(employee_list)
refresh_employee_acl_info.apply_async(args=(), queue=CMDB_QUEUE)
return res
@staticmethod
@@ -786,9 +787,11 @@ class CreateEmployee(object):
if existed:
return existed
return Employee.create(
res = Employee.create(
**kwargs
)
refresh_employee_acl_info.apply_async(args=(res.employee_id,), queue=ACL_QUEUE)
return res
@staticmethod
def get_department_by_name(d_name):
@@ -895,3 +898,75 @@ class EmployeeUpdateByUidForm(Form):
avatar = StringField(validators=[])
sex = StringField(validators=[])
mobile = StringField(validators=[])
class GrantEmployeeACLPerm(object):
"""
Grant ACL Permission After Create New Employee
"""
def __init__(self, acl=None):
self.perms_by_create_resources_type = ['read', 'grant', 'delete', 'update']
self.perms_by_common_grant = ['read']
self.resource_name_list = ['公司信息', '公司架构', '通知设置']
self.acl = acl if acl else self.check_app('backend')
self.resources_types = self.acl.get_all_resources_types()
self.resources_type = self.get_resources_type()
self.resource_list = self.acl.get_resource_by_type(None, None, self.resources_type['id'])
@staticmethod
def check_app(app_name):
acl = ACLManager(app_name)
payload = dict(
name=app_name,
description=app_name
)
app = acl.validate_app()
if not app:
acl.create_app(payload)
return acl
def get_resources_type(self):
results = list(filter(lambda t: t['name'] == '操作权限', self.resources_types['groups']))
if len(results) == 0:
payload = dict(
app_id=self.acl.app_name,
name='操作权限',
description='',
perms=self.perms_by_create_resources_type
)
resource_type = self.acl.create_resources_type(payload)
else:
resource_type = results[0]
resource_type_id = resource_type['id']
existed_perms = self.resources_types.get('id2perms', {}).get(resource_type_id, [])
existed_perms = [p['name'] for p in existed_perms]
new_perms = []
for perm in self.perms_by_create_resources_type:
if perm not in existed_perms:
new_perms.append(perm)
if len(new_perms) > 0:
resource_type['perms'] = existed_perms + new_perms
self.acl.update_resources_type(resource_type_id, resource_type)
return resource_type
def grant(self, rid_list):
[self.grant_by_rid(rid) for rid in rid_list if rid > 0]
def grant_by_rid(self, rid, is_admin=False):
for name in self.resource_name_list:
resource = list(filter(lambda r: r['name'] == name, self.resource_list))
if len(resource) == 0:
payload = dict(
type_id=self.resources_type['id'],
app_id=self.acl.app_name,
name=name,
)
resource = self.acl.create_resource(payload)
else:
resource = resource[0]
perms = self.perms_by_create_resources_type if is_admin else self.perms_by_common_grant
self.acl.grant_resource(rid, resource['id'], perms)

View File

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

View File

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

View File

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

View File

@@ -8,6 +8,8 @@ from api.extensions import db
from api.lib.utils import get_page
from api.lib.utils import get_page_size
__author__ = 'pycook'
class DBMixin(object):
cls = None
@@ -17,13 +19,18 @@ class DBMixin(object):
page = get_page(page)
page_size = get_page_size(page_size)
if fl is None:
query = db.session.query(cls.cls).filter(cls.cls.deleted.is_(False))
query = db.session.query(cls.cls)
else:
query = db.session.query(*[getattr(cls.cls, i) for i in fl]).filter(cls.cls.deleted.is_(False))
query = db.session.query(*[getattr(cls.cls, i) for i in fl])
_query = None
if count_query:
_query = db.session.query(func.count(cls.cls.id)).filter(cls.cls.deleted.is_(False))
_query = db.session.query(func.count(cls.cls.id))
if hasattr(cls.cls, 'deleted'):
query = query.filter(cls.cls.deleted.is_(False))
if _query:
_query = _query.filter(cls.cls.deleted.is_(False))
for k in kwargs:
if hasattr(cls.cls, k):

View File

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

View File

@@ -389,6 +389,7 @@ class AuditCRUD(object):
logout_at=logout_at,
ip=request.headers.get('X-Real-IP') or request.remote_addr,
browser=request.headers.get('User-Agent'),
channel=request.values.get('channel', 'web'),
)
if logout_at is None:

View File

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

View File

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

View File

@@ -2,7 +2,6 @@
from flask import abort
from flask import current_app
from api.extensions import db
from api.lib.perm.acl.audit import AuditCRUD
@@ -127,11 +126,18 @@ class ResourceTypeCRUD(object):
existed_ids = [i.id for i in existed]
current_ids = []
rebuild_rids = set()
for i in existed:
if i.name not in perms:
i.soft_delete()
i.soft_delete(commit=False)
for rp in RolePermission.get_by(perm_id=i.id, to_dict=False):
rp.soft_delete(commit=False)
rebuild_rids.add((rp.app_id, rp.rid))
else:
current_ids.append(i.id)
db.session.commit()
for _app_id, _rid in rebuild_rids:
role_rebuild.apply_async(args=(_rid, _app_id), queue=ACL_QUEUE)
for i in perms:
if i not in existed_names:
@@ -309,9 +315,12 @@ class ResourceCRUD(object):
return resource
@staticmethod
def delete(_id):
def delete(_id, rebuild=True, app_id=None):
resource = Resource.get_by_id(_id) or abort(404, ErrFormat.resource_not_found.format("id={}".format(_id)))
if app_id is not None and resource.app_id != app_id:
return abort(404, ErrFormat.resource_not_found.format("id={}".format(_id)))
origin = resource.to_dict()
resource.soft_delete()
@@ -322,8 +331,9 @@ class ResourceCRUD(object):
i.soft_delete()
rebuilds.append((i.rid, i.app_id))
for rid, app_id in set(rebuilds):
role_rebuild.apply_async(args=(rid, app_id), queue=ACL_QUEUE)
if rebuild:
for rid, app_id in set(rebuilds):
role_rebuild.apply_async(args=(rid, app_id), queue=ACL_QUEUE)
AuditCRUD.add_resource_log(resource.app_id, AuditOperateType.delete,
AuditScope.resource, resource.id, origin, {}, {})

View File

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

View File

@@ -3,12 +3,14 @@
import time
import redis_lock
import six
from flask import abort
from flask import current_app
from sqlalchemy import or_
from api.extensions import db
from api.extensions import rd
from api.lib.perm.acl.app import AppCRUD
from api.lib.perm.acl.audit import AuditCRUD, AuditOperateType, AuditScope
from api.lib.perm.acl.cache import AppCache
@@ -62,7 +64,9 @@ class RoleRelationCRUD(object):
id2parents = {}
for i in res:
id2parents.setdefault(rid2uid.get(i.child_id, i.child_id), []).append(RoleCache.get(i.parent_id).to_dict())
parent = RoleCache.get(i.parent_id)
if parent:
id2parents.setdefault(rid2uid.get(i.child_id, i.child_id), []).append(parent.to_dict())
return id2parents
@@ -141,24 +145,27 @@ class RoleRelationCRUD(object):
@classmethod
def add(cls, role, parent_id, child_ids, app_id):
result = []
for child_id in child_ids:
existed = RoleRelation.get_by(parent_id=parent_id, child_id=child_id, app_id=app_id)
if existed:
continue
with redis_lock.Lock(rd.r, "ROLE_RELATION_ADD"):
db.session.commit()
RoleRelationCache.clean(parent_id, app_id)
RoleRelationCache.clean(child_id, app_id)
result = []
for child_id in child_ids:
existed = RoleRelation.get_by(parent_id=parent_id, child_id=child_id, app_id=app_id)
if existed:
continue
if parent_id in cls.recursive_child_ids(child_id, app_id):
return abort(400, ErrFormat.inheritance_dead_loop)
if parent_id in cls.recursive_child_ids(child_id, app_id):
return abort(400, ErrFormat.inheritance_dead_loop)
if app_id is None:
for app in AppCRUD.get_all():
if app.name != "acl":
RoleRelationCache.clean(child_id, app.id)
result.append(RoleRelation.create(parent_id=parent_id, child_id=child_id, app_id=app_id).to_dict())
result.append(RoleRelation.create(parent_id=parent_id, child_id=child_id, app_id=app_id).to_dict())
RoleRelationCache.clean(parent_id, app_id)
RoleRelationCache.clean(child_id, app_id)
if app_id is None:
for app in AppCRUD.get_all():
if app.name != "acl":
RoleRelationCache.clean(child_id, app.id)
AuditCRUD.add_role_log(app_id, AuditOperateType.role_relation_add,
AuditScope.role_relation, role.id, {}, {},
@@ -372,16 +379,16 @@ class RoleCRUD(object):
resource_type_id = resource_type and resource_type.id
result = dict(resources=dict(), groups=dict())
s = time.time()
# s = time.time()
parent_ids = RoleRelationCRUD.recursive_parent_ids(rid, app_id)
current_app.logger.info('parent ids {0}: {1}'.format(parent_ids, time.time() - s))
# current_app.logger.info('parent ids {0}: {1}'.format(parent_ids, time.time() - s))
for parent_id in parent_ids:
_resources, _groups = cls._extend_resources(parent_id, resource_type_id, app_id)
current_app.logger.info('middle1: {0}'.format(time.time() - s))
# current_app.logger.info('middle1: {0}'.format(time.time() - s))
_merge(result['resources'], _resources)
current_app.logger.info('middle2: {0}'.format(time.time() - s))
current_app.logger.info(len(_groups))
# current_app.logger.info('middle2: {0}'.format(time.time() - s))
# current_app.logger.info(len(_groups))
if not group_flat:
_merge(result['groups'], _groups)
else:
@@ -392,7 +399,7 @@ class RoleCRUD(object):
item.setdefault('permissions', [])
item['permissions'] = list(set(item['permissions'] + _groups[rg_id]['permissions']))
result['resources'][item['id']] = item
current_app.logger.info('End: {0}'.format(time.time() - s))
# current_app.logger.info('End: {0}'.format(time.time() - s))
result['resources'] = list(result['resources'].values())
result['groups'] = list(result['groups'].values())

View File

@@ -51,12 +51,12 @@ def _auth_with_key():
user, authenticated = User.query.authenticate_with_key(key, secret, req_args, path)
if user and authenticated:
login_user(user)
reset_session(user)
# reset_session(user)
return True
role, authenticated = Role.query.authenticate_with_key(key, secret, req_args, path)
if role and authenticated:
reset_session(None, role=role.name)
# reset_session(None, role=role.name)
return True
return False

View File

@@ -194,7 +194,7 @@ def validate(ticket):
def _parse_tag(string, tag):
"""
Used for parsing xml. Search string for the first occurence of
Used for parsing xml. Search string for the first occurrence of
<tag>.....</tag> and return text (stripped of leading and tailing
whitespace) between tags. Return "" if tag not found.
"""

View File

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

View File

@@ -1,19 +1,15 @@
import json
import os
import secrets
import sys
import threading
from base64 import b64decode, b64encode
from Cryptodome.Protocol.SecretSharing import Shamir
from colorama import Back
from colorama import Fore
from colorama import Style
from colorama import init as colorama_init
from colorama import Back, Fore, Style, init as colorama_init
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives import padding
from cryptography.hazmat.primitives.ciphers import Cipher
from cryptography.hazmat.primitives.ciphers import algorithms
from cryptography.hazmat.primitives.ciphers import modes
from cryptography.hazmat.primitives import hashes, padding
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from flask import current_app
@@ -27,11 +23,17 @@ backend_encrypt_key_name = "encrypt_key"
backend_root_key_salt_name = "root_key_salt"
backend_encrypt_key_salt_name = "encrypt_key_salt"
backend_seal_key = "seal_status"
success = "success"
seal_status = True
secrets_encrypt_key = ""
secrets_root_key = ""
def string_to_bytes(value):
if not value:
return ""
if isinstance(value, bytes):
return value
if sys.version_info.major == 2:
@@ -44,6 +46,8 @@ def string_to_bytes(value):
class Backend:
def __init__(self, backend=None):
self.backend = backend
# cache is a redis object
self.cache = backend.cache
def get(self, key):
return self.backend.get(key)
@@ -54,23 +58,33 @@ class Backend:
def update(self, key, value):
return self.backend.update(key, value)
def get_shares(self, key):
return self.backend.get_shares(key)
def set_shares(self, key, value):
return self.backend.set_shares(key, value)
class KeyManage:
def __init__(self, trigger=None, backend=None):
self.trigger = trigger
self.backend = backend
self.share_key = "cmdb::secret::secrets_share"
if backend:
self.backend = Backend(backend)
def init_app(self, app, backend=None):
if (sys.argv[0].endswith("gunicorn") or
(len(sys.argv) > 1 and sys.argv[1] in ("run", "cmdb-password-data-migrate"))):
self.backend = backend
threading.Thread(target=self.watch_root_key, args=(app,), daemon=True).start()
self.trigger = app.config.get("INNER_TRIGGER_TOKEN")
if not self.trigger:
return
self.backend = backend
resp = self.auto_unseal()
self.print_response(resp)
@@ -124,6 +138,8 @@ class KeyManage:
return new_shares
def is_valid_root_key(self, root_key):
if not root_key:
return False
root_key_hash, ok = self.hash_root_key(root_key)
if not ok:
return root_key_hash, ok
@@ -135,35 +151,42 @@ class KeyManage:
else:
return "", True
def auth_root_secret(self, root_key):
msg, ok = self.is_valid_root_key(root_key)
if not ok:
return {
"message": msg,
"status": "failed"
}
def auth_root_secret(self, root_key, app):
with app.app_context():
msg, ok = self.is_valid_root_key(root_key)
if not ok:
return {
"message": msg,
"status": "failed"
}
encrypt_key_aes = self.backend.get(backend_encrypt_key_name)
if not encrypt_key_aes:
return {
"message": "encrypt key is empty",
"status": "failed"
}
encrypt_key_aes = self.backend.get(backend_encrypt_key_name)
if not encrypt_key_aes:
return {
"message": "encrypt key is empty",
"status": "failed"
}
secrets_encrypt_key, ok = InnerCrypt.aes_decrypt(string_to_bytes(root_key), encrypt_key_aes)
if ok:
msg, ok = self.backend.update(backend_seal_key, "open")
secret_encrypt_key, ok = InnerCrypt.aes_decrypt(string_to_bytes(root_key), encrypt_key_aes)
if ok:
current_app.config["secrets_encrypt_key"] = secrets_encrypt_key
current_app.config["secrets_root_key"] = root_key
current_app.config["secrets_shares"] = []
return {"message": success, "status": success}
return {"message": msg, "status": "failed"}
else:
return {
"message": secrets_encrypt_key,
"status": "failed"
}
msg, ok = self.backend.update(backend_seal_key, "open")
if ok:
global secrets_encrypt_key, secrets_root_key
secrets_encrypt_key = secret_encrypt_key
secrets_root_key = root_key
self.backend.cache.set(self.share_key, json.dumps([]))
return {"message": success, "status": success}
return {"message": msg, "status": "failed"}
else:
return {
"message": secret_encrypt_key,
"status": "failed"
}
def parse_shares(self, shares, app):
if len(shares) >= global_key_threshold:
recovered_secret = Shamir.combine(shares[:global_key_threshold], False)
return self.auth_root_secret(b64encode(recovered_secret), app)
def unseal(self, key):
if not self.is_seal():
@@ -175,14 +198,12 @@ class KeyManage:
try:
t = [i for i in b64decode(key)]
v = (int("".join([chr(i) for i in t[-2:]])), bytes(t[:-2]))
shares = current_app.config.get("secrets_shares", [])
shares = self.backend.get_shares(self.share_key)
if v not in shares:
shares.append(v)
current_app.config["secrets_shares"] = shares
self.set_shares(shares)
if len(shares) >= global_key_threshold:
recovered_secret = Shamir.combine(shares[:global_key_threshold], False)
return self.auth_root_secret(b64encode(recovered_secret))
return self.parse_shares(shares, current_app)
else:
return {
"message": "waiting for inputting other unseal key {0}/{1}".format(len(shares),
@@ -242,8 +263,11 @@ class KeyManage:
msg, ok = self.backend.add(backend_seal_key, "open")
if not ok:
return {"message": msg, "status": "failed"}, False
current_app.config["secrets_root_key"] = root_key
current_app.config["secrets_encrypt_key"] = encrypt_key
global secrets_encrypt_key, secrets_root_key
secrets_encrypt_key = encrypt_key
secrets_root_key = root_key
self.print_token(shares, root_token=root_key)
return {"message": "OK",
@@ -266,7 +290,7 @@ class KeyManage:
}
# TODO
elif len(self.trigger.strip()) == 24:
res = self.auth_root_secret(self.trigger.encode())
res = self.auth_root_secret(self.trigger.encode(), current_app)
if res.get("status") == success:
return {
"message": success,
@@ -298,22 +322,31 @@ class KeyManage:
"message": msg,
"status": "failed",
}
current_app.config["secrets_root_key"] = ''
current_app.config["secrets_encrypt_key"] = ''
self.clear()
self.backend.cache.publish(self.share_key, "clear")
return {
"message": success,
"status": success
}
@staticmethod
def clear():
global secrets_encrypt_key, secrets_root_key
secrets_encrypt_key = ''
secrets_root_key = ''
def is_seal(self):
"""
If there is no initialization or the root key is inconsistent, it is considered to be in a sealed state.
If there is no initialization or the root key is inconsistent, it is considered to be in a sealed state..
:return:
"""
secrets_root_key = current_app.config.get("secrets_root_key")
# secrets_root_key = current_app.config.get("secrets_root_key")
if not secrets_root_key:
return True
msg, ok = self.is_valid_root_key(secrets_root_key)
if not ok:
return true
return True
status = self.backend.get(backend_seal_key)
return status == "block"
@@ -349,22 +382,53 @@ class KeyManage:
}
print(status_colors.get(status, Fore.GREEN), message, Style.RESET_ALL)
def set_shares(self, values):
new_value = list()
for v in values:
new_value.append((v[0], b64encode(v[1]).decode("utf-8")))
self.backend.cache.publish(self.share_key, json.dumps(new_value))
self.backend.cache.set(self.share_key, json.dumps(new_value))
def watch_root_key(self, app):
pubsub = self.backend.cache.pubsub()
pubsub.subscribe(self.share_key)
new_value = set()
for message in pubsub.listen():
if message["type"] == "message":
if message["data"] == b"clear":
self.clear()
continue
try:
value = json.loads(message["data"].decode("utf-8"))
for v in value:
new_value.add((v[0], b64decode(v[1])))
except Exception as e:
return []
if len(new_value) >= global_key_threshold:
self.parse_shares(list(new_value), app)
new_value = set()
class InnerCrypt:
def __init__(self):
secrets_encrypt_key = current_app.config.get("secrets_encrypt_key", "")
self.encrypt_key = b64decode(secrets_encrypt_key.encode("utf-8"))
self.encrypt_key = b64decode(secrets_encrypt_key)
# self.encrypt_key = b64decode(secrets_encrypt_key, "".encode("utf-8"))
def encrypt(self, plaintext):
"""
encrypt method contain aes currently
"""
if not self.encrypt_key:
return ValueError("secret is disabled, please seal firstly"), False
return self.aes_encrypt(self.encrypt_key, plaintext)
def decrypt(self, ciphertext):
"""
decrypt method contain aes currently
"""
if not self.encrypt_key:
return ValueError("secret is disabled, please seal firstly"), False
return self.aes_decrypt(self.encrypt_key, ciphertext)
@classmethod
@@ -381,6 +445,7 @@ class InnerCrypt:
return b64encode(iv + ciphertext).decode("utf-8"), True
except Exception as e:
return str(e), False
@classmethod

View File

@@ -1,8 +1,13 @@
import base64
import json
from api.models.cmdb import InnerKV
from api.extensions import rd
class InnerKVManger(object):
def __init__(self):
self.cache = rd.r
pass
@classmethod
@@ -33,3 +38,26 @@ class InnerKVManger(object):
return "success", True
return "update failed", True
@classmethod
def get_shares(cls, key):
new_value = list()
v = rd.get_str(key)
if not v:
return new_value
try:
value = json.loads(v.decode("utf-8"))
for v in value:
new_value.append((v[0], base64.b64decode(v[1])))
except Exception as e:
return []
return new_value
@classmethod
def set_shares(cls, key, value):
new_value = list()
for v in value:
new_value.append((v[0], base64.b64encode(v[1]).decode("utf-8")))
rd.set_str(key, json.dumps(new_value))

View File

@@ -1,8 +1,6 @@
# -*- coding:utf-8 -*-
import base64
import sys
import time
from typing import Set
import elasticsearch
@@ -119,6 +117,23 @@ class RedisHandler(object):
except Exception as e:
current_app.logger.error("delete redis key error, {0}".format(str(e)))
def set_str(self, key, value, expired=None):
try:
if expired:
self.r.setex(key, expired, value)
else:
self.r.set(key, value)
except Exception as e:
current_app.logger.error("set redis error, {0}".format(str(e)))
def get_str(self, key):
try:
value = self.r.get(key)
except Exception as e:
current_app.logger.error("get redis error, {0}".format(str(e)))
return
return value
class ESHandler(object):
def __init__(self, flask_app=None):
@@ -213,52 +228,6 @@ class ESHandler(object):
return 0, [], {}
class Lock(object):
def __init__(self, name, timeout=10, app=None, need_lock=True):
self.lock_key = name
self.need_lock = need_lock
self.timeout = timeout
if not app:
app = current_app
self.app = app
try:
self.redis = redis.Redis(host=self.app.config.get('CACHE_REDIS_HOST'),
port=self.app.config.get('CACHE_REDIS_PORT'),
password=self.app.config.get('CACHE_REDIS_PASSWORD'))
except:
self.app.logger.error("cannot connect redis")
raise Exception("cannot connect redis")
def lock(self, timeout=None):
if not timeout:
timeout = self.timeout
retry = 0
while retry < 100:
timestamp = time.time() + timeout + 1
_lock = self.redis.setnx(self.lock_key, timestamp)
if _lock == 1 or (
time.time() > float(self.redis.get(self.lock_key) or sys.maxsize) and
time.time() > float(self.redis.getset(self.lock_key, timestamp) or sys.maxsize)):
break
else:
retry += 1
time.sleep(0.6)
if retry >= 100:
raise Exception("get lock failed...")
def release(self):
if time.time() < float(self.redis.get(self.lock_key)):
self.redis.delete(self.lock_key)
def __enter__(self):
if self.need_lock:
self.lock()
def __exit__(self, exc_type, exc_val, exc_tb):
if self.need_lock:
self.release()
class AESCrypto(object):
BLOCK_SIZE = 16 # Bytes
pad = lambda s: s + ((AESCrypto.BLOCK_SIZE - len(s) % AESCrypto.BLOCK_SIZE) *

View File

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

View File

@@ -356,7 +356,7 @@ class AuditLoginLog(Model2):
__tablename__ = "acl_audit_login_logs"
username = db.Column(db.String(64), index=True)
channel = db.Column(db.Enum('web', 'api'), default="web")
channel = db.Column(db.Enum('web', 'api', 'ssh'), default="web")
ip = db.Column(db.String(15))
browser = db.Column(db.String(256))
description = db.Column(db.String(128))

View File

@@ -46,17 +46,31 @@ class CIType(Model):
name = db.Column(db.String(32), nullable=False)
alias = db.Column(db.String(32), nullable=False)
unique_id = db.Column(db.Integer, db.ForeignKey("c_attributes.id"), nullable=False)
show_id = db.Column(db.Integer, db.ForeignKey("c_attributes.id"))
enabled = db.Column(db.Boolean, default=True, nullable=False)
is_attached = db.Column(db.Boolean, default=False, nullable=False)
icon = db.Column(db.Text)
order = db.Column(db.SmallInteger, default=0, nullable=False)
default_order_attr = db.Column(db.String(33))
unique_key = db.relationship("Attribute", backref="c_ci_types.unique_id")
unique_key = db.relationship("Attribute", backref="c_ci_types.unique_id",
primaryjoin="Attribute.id==CIType.unique_id", foreign_keys=[unique_id])
show_key = db.relationship("Attribute", backref="c_ci_types.show_id",
primaryjoin="Attribute.id==CIType.show_id", foreign_keys=[show_id])
uid = db.Column(db.Integer, index=True)
class CITypeInheritance(Model):
__tablename__ = "c_ci_type_inheritance"
parent_id = db.Column(db.Integer, db.ForeignKey("c_ci_types.id"), nullable=False)
child_id = db.Column(db.Integer, db.ForeignKey("c_ci_types.id"), nullable=False)
parent = db.relationship("CIType", primaryjoin="CIType.id==CITypeInheritance.parent_id")
child = db.relationship("CIType", primaryjoin="CIType.id==CITypeInheritance.child_id")
class CITypeRelation(Model):
__tablename__ = "c_ci_type_relations"
@@ -65,6 +79,12 @@ class CITypeRelation(Model):
relation_type_id = db.Column(db.Integer, db.ForeignKey("c_relation_types.id"), nullable=False)
constraint = db.Column(db.Enum(*ConstraintEnum.all()), default=ConstraintEnum.One2Many)
parent_attr_id = db.Column(db.Integer, db.ForeignKey("c_attributes.id")) # CMDB > 2.4.5: deprecated
child_attr_id = db.Column(db.Integer, db.ForeignKey("c_attributes.id")) # CMDB > 2.4.5: deprecated
parent_attr_ids = db.Column(db.JSON) # [parent_attr_id, ]
child_attr_ids = db.Column(db.JSON) # [child_attr_id, ]
parent = db.relationship("CIType", primaryjoin="CIType.id==CITypeRelation.parent_id")
child = db.relationship("CIType", primaryjoin="CIType.id==CITypeRelation.child_id")
relation_type = db.relationship("RelationType", backref="c_ci_type_relations.relation_type_id")
@@ -84,6 +104,7 @@ class Attribute(Model):
is_link = db.Column(db.Boolean, default=False)
is_password = db.Column(db.Boolean, default=False)
is_sortable = db.Column(db.Boolean, default=False)
is_dynamic = db.Column(db.Boolean, default=False)
default = db.Column(db.JSON) # {"default": None}
@@ -94,6 +115,8 @@ class Attribute(Model):
_choice_web_hook = db.Column('choice_web_hook', db.JSON)
choice_other = db.Column(db.JSON)
re_check = db.Column(db.Text)
uid = db.Column(db.Integer, index=True)
option = db.Column(db.JSON)
@@ -190,6 +213,26 @@ class CITriggerHistory(Model):
webhook = db.Column(db.Text)
class TopologyViewGroup(Model):
__tablename__ = 'c_topology_view_groups'
name = db.Column(db.String(64), index=True)
order = db.Column(db.Integer, default=0)
class TopologyView(Model):
__tablename__ = 'c_topology_views'
name = db.Column(db.String(64), index=True)
group_id = db.Column(db.Integer, db.ForeignKey('c_topology_view_groups.id'))
category = db.Column(db.String(32))
central_node_type = db.Column(db.Integer)
central_node_instances = db.Column(db.Text)
path = db.Column(db.JSON)
order = db.Column(db.Integer, default=0)
option = db.Column(db.JSON)
class CITypeUniqueConstraint(Model):
__tablename__ = "c_c_t_u_c"
@@ -413,6 +456,7 @@ class CITypeHistory(Model):
attr_id = db.Column(db.Integer)
trigger_id = db.Column(db.Integer)
rc_id = db.Column(db.Integer)
unique_constraint_id = db.Column(db.Integer)
uid = db.Column(db.Integer, index=True)
@@ -448,6 +492,7 @@ class PreferenceRelationView(Model):
name = db.Column(db.String(64), index=True, nullable=False)
cr_ids = db.Column(db.JSON) # [{parent_id: x, child_id: y}]
is_public = db.Column(db.Boolean, default=False)
option = db.Column(db.JSON)
class PreferenceSearchOption(Model):
@@ -464,6 +509,15 @@ class PreferenceSearchOption(Model):
option = db.Column(db.JSON)
class PreferenceCITypeOrder(Model):
__tablename__ = "c_pcto"
uid = db.Column(db.Integer, index=True, nullable=False)
type_id = db.Column(db.Integer, db.ForeignKey('c_ci_types.id'))
order = db.Column(db.SmallInteger, default=0)
is_tree = db.Column(db.Boolean, default=False) # True is tree view, False is resource view
# custom
class CustomDashboard(Model):
__tablename__ = "c_c_d"
@@ -512,20 +566,29 @@ class AutoDiscoveryCIType(Model):
attributes = db.Column(db.JSON) # {ad_key: cmdb_key}
relation = db.Column(db.JSON) # [{ad_key: {type_id: x, attr_id: x}}]
relation = db.Column(db.JSON) # [{ad_key: {type_id: x, attr_id: x}}], CMDB > 2.4.5: deprecated
auto_accept = db.Column(db.Boolean, default=False)
agent_id = db.Column(db.String(8), index=True)
query_expr = db.Column(db.Text)
interval = db.Column(db.Integer) # seconds
interval = db.Column(db.Integer) # seconds, > 2.4.5: deprecated
cron = db.Column(db.String(128))
extra_option = db.Column(db.JSON)
uid = db.Column(db.Integer, index=True)
class AutoDiscoveryCITypeRelation(Model):
__tablename__ = "c_ad_ci_type_relations"
ad_type_id = db.Column(db.Integer, db.ForeignKey('c_ci_types.id'), nullable=False)
ad_key = db.Column(db.String(128))
peer_type_id = db.Column(db.Integer, db.ForeignKey('c_ci_types.id'), nullable=False)
peer_attr_id = db.Column(db.Integer, db.ForeignKey('c_attributes.id'), nullable=False)
class AutoDiscoveryCI(Model):
__tablename__ = "c_ad_ci"
@@ -541,6 +604,36 @@ class AutoDiscoveryCI(Model):
accept_time = db.Column(db.DateTime)
class AutoDiscoveryRuleSyncHistory(Model2):
__tablename__ = "c_ad_rule_sync_histories"
adt_id = db.Column(db.Integer, db.ForeignKey('c_ad_ci_types.id'))
oneagent_id = db.Column(db.String(8))
oneagent_name = db.Column(db.String(64))
sync_at = db.Column(db.DateTime, default=datetime.datetime.now())
class AutoDiscoveryExecHistory(Model2):
__tablename__ = "c_ad_exec_histories"
type_id = db.Column(db.Integer, index=True)
stdout = db.Column(db.Text)
class AutoDiscoveryCounter(Model2):
__tablename__ = "c_ad_counter"
type_id = db.Column(db.Integer, index=True)
rule_count = db.Column(db.Integer, default=0)
exec_target_count = db.Column(db.Integer, default=0)
instance_count = db.Column(db.Integer, default=0)
accept_count = db.Column(db.Integer, default=0)
this_month_count = db.Column(db.Integer, default=0)
this_week_count = db.Column(db.Integer, default=0)
last_month_count = db.Column(db.Integer, default=0)
last_week_count = db.Column(db.Integer, default=0)
class CIFilterPerms(Model):
__tablename__ = "c_ci_filter_perms"
@@ -548,6 +641,7 @@ class CIFilterPerms(Model):
type_id = db.Column(db.Integer, db.ForeignKey('c_ci_types.id'))
ci_filter = db.Column(db.Text)
attr_filter = db.Column(db.Text)
id_filter = db.Column(db.JSON) # {node_path: unique_value}
rid = db.Column(db.Integer, index=True)

View File

@@ -3,12 +3,13 @@
import json
import re
from celery_once import QueueOnce
import redis_lock
from flask import current_app
from werkzeug.exceptions import BadRequest
from werkzeug.exceptions import NotFound
from api.extensions import celery
from api.extensions import rd
from api.lib.decorator import flush_db
from api.lib.decorator import reconnect_db
from api.lib.perm.acl.audit import AuditCRUD
@@ -25,14 +26,14 @@ from api.models.acl import Role
from api.models.acl import Trigger
@celery.task(name="acl.role_rebuild",
queue=ACL_QUEUE,)
@celery.task(name="acl.role_rebuild", queue=ACL_QUEUE, )
@flush_db
@reconnect_db
def role_rebuild(rids, app_id):
rids = rids if isinstance(rids, list) else [rids]
for rid in rids:
RoleRelationCache.rebuild(rid, app_id)
with redis_lock.Lock(rd.r, "ROLE_REBUILD_{}_{}".format(rid, app_id)):
RoleRelationCache.rebuild(rid, app_id)
current_app.logger.info("Role {0} App {1} rebuild..........".format(rids, app_id))

View File

@@ -2,8 +2,9 @@
import json
import time
import datetime
import redis_lock
from flask import current_app
from flask_login import login_user
@@ -17,14 +18,16 @@ from api.lib.cmdb.const import CMDB_QUEUE
from api.lib.cmdb.const import REDIS_PREFIX_CI
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION2
from api.lib.cmdb.perms import CIFilterPermsCRUD
from api.lib.decorator import flush_db
from api.lib.decorator import reconnect_db
from api.lib.perm.acl.cache import UserCache
from api.lib.utils import Lock
from api.lib.utils import handle_arg_list
from api.models.cmdb import CI
from api.models.cmdb import CIRelation
from api.models.cmdb import CITypeAttribute
from api.models.cmdb import AutoDiscoveryCI
from api.models.cmdb import AutoDiscoveryCIType
@celery.task(name="cmdb.ci_cache", queue=CMDB_QUEUE)
@@ -32,8 +35,7 @@ from api.models.cmdb import CITypeAttribute
@reconnect_db
def ci_cache(ci_id, operate_type, record_id):
from api.lib.cmdb.ci import CITriggerManager
time.sleep(0.01)
from api.lib.cmdb.ci import CIRelationManager
m = api.lib.cmdb.ci.CIManager()
ci_dict = m.get_ci_by_id_from_db(ci_id, need_children=False, use_master=False)
@@ -51,13 +53,21 @@ def ci_cache(ci_id, operate_type, record_id):
CITriggerManager.fire(operate_type, ci_dict, record_id)
ci_dict and CIRelationManager.build_by_attribute(ci_dict)
@celery.task(name="cmdb.rebuild_relation_for_attribute_changed", queue=CMDB_QUEUE)
@reconnect_db
def rebuild_relation_for_attribute_changed(ci_type_relation):
from api.lib.cmdb.ci import CIRelationManager
CIRelationManager.rebuild_all_by_attribute(ci_type_relation)
@celery.task(name="cmdb.batch_ci_cache", queue=CMDB_QUEUE)
@flush_db
@reconnect_db
def batch_ci_cache(ci_ids, ): # only for attribute change index
time.sleep(1)
for ci_id in ci_ids:
m = api.lib.cmdb.ci.CIManager()
ci_dict = m.get_ci_by_id_from_db(ci_id, need_children=False, use_master=False)
@@ -80,9 +90,22 @@ def ci_delete(ci_id):
else:
rd.delete(ci_id, REDIS_PREFIX_CI)
instance = AutoDiscoveryCI.get_by(ci_id=ci_id, to_dict=False, first=True)
if instance is not None:
adt = AutoDiscoveryCIType.get_by_id(instance.adt_id)
if adt:
adt.update(updated_at=datetime.datetime.now())
instance.delete()
current_app.logger.info("{0} delete..........".format(ci_id))
@celery.task(name="cmdb.delete_id_filter", queue=CMDB_QUEUE)
@reconnect_db
def delete_id_filter(ci_id):
CIFilterPermsCRUD().delete_id_filter_by_ci_id(ci_id)
@celery.task(name="cmdb.ci_delete_trigger", queue=CMDB_QUEUE)
@reconnect_db
def ci_delete_trigger(trigger, operate_type, ci_dict):
@@ -99,19 +122,18 @@ def ci_delete_trigger(trigger, operate_type, ci_dict):
@flush_db
@reconnect_db
def ci_relation_cache(parent_id, child_id, ancestor_ids):
with Lock("CIRelation_{}".format(parent_id)):
if ancestor_ids is None:
children = rd.get([parent_id], REDIS_PREFIX_CI_RELATION)[0]
children = json.loads(children) if children is not None else {}
with redis_lock.Lock(rd.r, "CIRelation_{}".format(parent_id)):
children = rd.get([parent_id], REDIS_PREFIX_CI_RELATION)[0]
children = json.loads(children) if children is not None else {}
cr = CIRelation.get_by(first_ci_id=parent_id, second_ci_id=child_id, ancestor_ids=ancestor_ids,
first=True, to_dict=False)
if str(child_id) not in children:
children[str(child_id)] = cr.second_ci.type_id
cr = CIRelation.get_by(first_ci_id=parent_id, second_ci_id=child_id, ancestor_ids=ancestor_ids,
first=True, to_dict=False)
if str(child_id) not in children:
children[str(child_id)] = cr.second_ci.type_id
rd.create_or_update({parent_id: json.dumps(children)}, REDIS_PREFIX_CI_RELATION)
rd.create_or_update({parent_id: json.dumps(children)}, REDIS_PREFIX_CI_RELATION)
else:
if ancestor_ids is not None:
key = "{},{}".format(ancestor_ids, parent_id)
grandson = rd.get([key], REDIS_PREFIX_CI_RELATION2)[0]
grandson = json.loads(grandson) if grandson is not None else {}
@@ -177,17 +199,16 @@ 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 Lock("CIRelation_{}".format(parent_id)):
if ancestor_ids is None:
children = rd.get([parent_id], REDIS_PREFIX_CI_RELATION)[0]
children = json.loads(children) if children is not None else {}
with redis_lock.Lock(rd.r, "CIRelation_{}".format(parent_id)):
children = rd.get([parent_id], REDIS_PREFIX_CI_RELATION)[0]
children = json.loads(children) if children is not None else {}
if str(child_id) in children:
children.pop(str(child_id))
if str(child_id) in children:
children.pop(str(child_id))
rd.create_or_update({parent_id: json.dumps(children)}, REDIS_PREFIX_CI_RELATION)
rd.create_or_update({parent_id: json.dumps(children)}, REDIS_PREFIX_CI_RELATION)
else:
if ancestor_ids is not None:
key = "{},{}".format(ancestor_ids, parent_id)
grandson = rd.get([key], REDIS_PREFIX_CI_RELATION2)[0]
grandson = json.loads(grandson) if grandson is not None else {}
@@ -238,3 +259,21 @@ def calc_computed_attribute(attr_id, uid):
cis = CI.get_by(type_id=i.type_id, to_dict=False)
for ci in cis:
cim.update(ci.id, {})
@celery.task(name="cmdb.write_ad_rule_sync_history", queue=CMDB_QUEUE)
@reconnect_db
def write_ad_rule_sync_history(rules, oneagent_id, oneagent_name, sync_at):
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryRuleSyncHistoryCRUD
for rule in rules:
AutoDiscoveryRuleSyncHistoryCRUD().upsert(adt_id=rule['id'],
oneagent_id=oneagent_id,
oneagent_name=oneagent_name,
sync_at=sync_at,
commit=False)
try:
db.session.commit()
except Exception as e:
current_app.logger.error("write auto discovery rule sync history failed: {}".format(e))
db.session.rollback()

View File

@@ -3,14 +3,14 @@ from flask import current_app
from api.extensions import celery
from api.lib.common_setting.acl import ACLManager
from api.lib.cmdb.const import CMDB_QUEUE
from api.lib.perm.acl.const import ACL_QUEUE
from api.lib.common_setting.resp_format import ErrFormat
from api.models.common_setting import Department, Employee
from api.lib.decorator import flush_db
from api.lib.decorator import reconnect_db
@celery.task(name="common_setting.edit_employee_department_in_acl", queue=CMDB_QUEUE)
@celery.task(name="common_setting.edit_employee_department_in_acl", queue=ACL_QUEUE)
@flush_db
@reconnect_db
def edit_employee_department_in_acl(e_list, new_d_id, op_uid):
@@ -49,21 +49,20 @@ def edit_employee_department_in_acl(e_list, new_d_id, op_uid):
continue
old_d_rid_in_acl = role_map.get(old_department.department_name, 0)
if old_d_rid_in_acl == 0:
return
if old_d_rid_in_acl != old_department.acl_rid:
old_department.update(
acl_rid=old_d_rid_in_acl
)
d_acl_rid = old_department.acl_rid if old_d_rid_in_acl == old_department.acl_rid else old_d_rid_in_acl
payload = {
'app_id': 'acl',
'parent_id': d_acl_rid,
}
try:
acl.remove_user_from_role(employee_acl_rid, payload)
except Exception as e:
result.append(ErrFormat.acl_remove_user_from_role_failed.format(str(e)))
if old_d_rid_in_acl > 0:
if old_d_rid_in_acl != old_department.acl_rid:
old_department.update(
acl_rid=old_d_rid_in_acl
)
d_acl_rid = old_department.acl_rid if old_d_rid_in_acl == old_department.acl_rid else old_d_rid_in_acl
payload = {
'app_id': 'acl',
'parent_id': d_acl_rid,
}
try:
acl.remove_user_from_role(employee_acl_rid, payload)
except Exception as e:
result.append(ErrFormat.acl_remove_user_from_role_failed.format(str(e)))
payload = {
'app_id': 'acl',
@@ -77,10 +76,10 @@ def edit_employee_department_in_acl(e_list, new_d_id, op_uid):
return result
@celery.task(name="common_setting.refresh_employee_acl_info", queue=CMDB_QUEUE)
@celery.task(name="common_setting.refresh_employee_acl_info", queue=ACL_QUEUE)
@flush_db
@reconnect_db
def refresh_employee_acl_info():
def refresh_employee_acl_info(current_employee_id=None):
acl = ACLManager('acl')
role_map = {role['name']: role for role in acl.get_all_roles()}
@@ -90,8 +89,12 @@ def refresh_employee_acl_info():
query = Employee.query.filter(*criterion).order_by(
Employee.created_at.desc()
)
current_employee_rid = 0
for em in query.all():
if current_employee_id and em.employee_id == current_employee_id:
current_employee_rid = em.acl_rid if em.acl_rid else 0
if em.acl_uid and em.acl_rid:
continue
role = role_map.get(em.username, None)
@@ -105,6 +108,9 @@ def refresh_employee_acl_info():
if not em.acl_rid:
params['acl_rid'] = role.get('id', 0)
if current_employee_id and em.employee_id == current_employee_id:
current_employee_rid = params['acl_rid'] if params.get('acl_rid', 0) else 0
try:
em.update(**params)
current_app.logger.info(
@@ -113,3 +119,12 @@ def refresh_employee_acl_info():
except Exception as e:
current_app.logger.error(str(e))
continue
if current_employee_rid and current_employee_rid > 0:
try:
from api.lib.common_setting.employee import GrantEmployeeACLPerm
GrantEmployeeACLPerm().grant_by_rid(current_employee_rid, False)
current_app.logger.info(f"GrantEmployeeACLPerm success, current_employee_rid: {current_employee_rid}")
except Exception as e:
current_app.logger.error(str(e))

Binary file not shown.

View File

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

View File

@@ -38,8 +38,9 @@ class LoginView(APIView):
username = request.values.get("username") or request.values.get("email")
password = request.values.get("password")
_role = None
auth_with_ldap = request.values.get('auth_with_ldap', True)
config = AuthenticateDataCRUD(AuthenticateType.LDAP).get()
if config.get('enabled') or config.get('enable'):
if (config.get('enabled') or config.get('enable')) and auth_with_ldap:
from api.lib.perm.authentication.ldap import authenticate_with_ldap
user, authenticated = authenticate_with_ldap(username, password)
else:

View File

@@ -1,6 +1,7 @@
# -*- coding:utf-8 -*-
import copy
import json
import uuid
from io import BytesIO
from flask import abort
@@ -10,15 +11,19 @@ from flask_login import current_user
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryCICRUD
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryCITypeCRUD
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryCITypeRelationCRUD
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryCounterCRUD
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryExecHistoryCRUD
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryHTTPManager
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryRuleCRUD
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryRuleSyncHistoryCRUD
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoverySNMPManager
from api.lib.cmdb.auto_discovery.const import DEFAULT_HTTP
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci import search
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.perm.acl.acl import has_perm_from_args
@@ -37,14 +42,19 @@ class AutoDiscoveryRuleView(APIView):
rebuild = False
exists = {i['name'] for i in res}
for i in DEFAULT_HTTP:
for i in copy.deepcopy(DEFAULT_HTTP):
if i['name'] not in exists:
i.pop('en', None)
AutoDiscoveryRuleCRUD().add(**i)
rebuild = True
if rebuild:
_, res = AutoDiscoveryRuleCRUD.search(page=1, page_size=100000, **request.values)
for i in res:
if i['type'] == 'http':
i['resources'] = AutoDiscoveryHTTPManager().get_resources(i['name'])
return self.jsonify(res)
@args_required("name", value_required=True)
@@ -98,7 +108,8 @@ class AutoDiscoveryRuleTemplateFileView(APIView):
class AutoDiscoveryRuleHTTPView(APIView):
url_prefix = ("/adr/http/<string:name>/categories", "/adr/http/<string:name>/attributes",
url_prefix = ("/adr/http/<string:name>/categories",
"/adr/http/<string:name>/attributes",
"/adr/snmp/<string:name>/attributes")
def get(self, name):
@@ -106,16 +117,21 @@ class AutoDiscoveryRuleHTTPView(APIView):
return self.jsonify(AutoDiscoverySNMPManager.get_attributes())
if "attributes" in request.url:
category = request.values.get('category')
return self.jsonify(AutoDiscoveryHTTPManager.get_attributes(name, category))
resource = request.values.get('resource')
return self.jsonify(AutoDiscoveryHTTPManager.get_attributes(name, resource))
return self.jsonify(AutoDiscoveryHTTPManager.get_categories(name))
class AutoDiscoveryCITypeView(APIView):
url_prefix = ("/adt/ci_types/<int:type_id>", "/adt/<int:adt_id>")
url_prefix = ("/adt/ci_types/<int:type_id>",
"/adt/ci_types/<int:type_id>/attributes",
"/adt/<int:adt_id>")
def get(self, type_id):
if "attributes" in request.url:
return self.jsonify(AutoDiscoveryCITypeCRUD.get_ad_attributes(type_id))
_, res = AutoDiscoveryCITypeCRUD.search(page=1, page_size=100000, type_id=type_id, **request.values)
for i in res:
if isinstance(i.get("extra_option"), dict) and i['extra_option'].get('secret'):
@@ -146,6 +162,27 @@ class AutoDiscoveryCITypeView(APIView):
return self.jsonify(adt_id=adt_id)
class AutoDiscoveryCITypeRelationView(APIView):
url_prefix = ("/adt/ci_types/<int:type_id>/relations", "/adt/relations/<int:_id>")
def get(self, type_id):
_, res = AutoDiscoveryCITypeRelationCRUD.search(page=1, page_size=100000, ad_type_id=type_id, **request.values)
return self.jsonify(res)
@args_required("relations")
def post(self, type_id):
return self.jsonify(AutoDiscoveryCITypeRelationCRUD().upsert(type_id, request.values['relations']))
def put(self):
return self.post()
def delete(self, _id):
AutoDiscoveryCITypeRelationCRUD().delete(_id)
return self.jsonify(id=_id)
class AutoDiscoveryCIView(APIView):
url_prefix = ("/adc", "/adc/<int:adc_id>", "/adc/ci_types/<int:type_id>/attributes", "/adc/ci_types")
@@ -220,9 +257,8 @@ class AutoDiscoveryRuleSyncView(APIView):
oneagent_id = request.values.get('oneagent_id')
last_update_at = request.values.get('last_update_at')
query = "{},oneagent_id:{}".format(oneagent_name, oneagent_id)
current_app.logger.info(query)
s = search(query)
query = "oneagent_id:{}".format(oneagent_id)
s = ci_search(query)
try:
response, _, _, _, _, _ = s.search()
except SearchError as e:
@@ -230,7 +266,77 @@ class AutoDiscoveryRuleSyncView(APIView):
current_app.logger.error(traceback.format_exc())
return abort(400, str(e))
ci_id = response and response[0]["_id"]
rules, last_update_at = AutoDiscoveryCITypeCRUD.get(ci_id, oneagent_id, last_update_at)
for res in response:
if res.get('{}_name'.format(res['ci_type'])) == oneagent_name or oneagent_name == res.get('oneagent_name'):
ci_id = res["_id"]
rules, last_update_at = AutoDiscoveryCITypeCRUD.get(ci_id, oneagent_id, oneagent_name, last_update_at)
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)
return self.jsonify(rules=rules, last_update_at=last_update_at)
class AutoDiscoveryRuleSyncHistoryView(APIView):
url_prefix = ("/adt/<int:adt_id>/sync/histories",)
def get(self, adt_id):
page = get_page(request.values.pop('page', 1))
page_size = get_page_size(request.values.pop('page_size', None))
numfound, res = AutoDiscoveryRuleSyncHistoryCRUD.search(page=page,
page_size=page_size,
adt_id=adt_id,
**request.values)
return self.jsonify(page=page,
page_size=page_size,
numfound=numfound,
total=len(res),
result=res)
class AutoDiscoveryTestView(APIView):
url_prefix = ("/adt/<int:adt_id>/test", "/adt/test/<string:exec_id>/result")
def get(self, exec_id):
return self.jsonify(stdout="1\n2\n3", exec_id=exec_id)
def post(self, adt_id):
return self.jsonify(exec_id=uuid.uuid4().hex)
class AutoDiscoveryExecHistoryView(APIView):
url_prefix = ("/adc/exec/histories",)
@args_required('type_id')
def get(self):
page = get_page(request.values.pop('page', 1))
page_size = get_page_size(request.values.pop('page_size', None))
numfound, res = AutoDiscoveryExecHistoryCRUD.search(page=page,
page_size=page_size,
**request.values)
return self.jsonify(page=page,
page_size=page_size,
numfound=numfound,
total=len(res),
result=res)
@args_required('type_id')
@args_required('stdout')
def post(self):
AutoDiscoveryExecHistoryCRUD().add(type_id=request.values.get('type_id'),
stdout=request.values.get('stdout'))
return self.jsonify(code=200)
class AutoDiscoveryCounterView(APIView):
url_prefix = ("/adc/counter",)
@args_required('type_id')
def get(self):
type_id = request.values.get('type_id')
return self.jsonify(AutoDiscoveryCounterCRUD().get(type_id))

View File

@@ -11,12 +11,12 @@ from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.ci import CIManager
from api.lib.cmdb.ci import CIRelationManager
from api.lib.cmdb.const import ExistPolicy
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.const import ResourceTypeEnum, PermEnum
from api.lib.cmdb.const import RetKey
from api.lib.cmdb.perms import has_perm_for_ci
from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci import search
from api.lib.decorator import args_required
from api.lib.perm.acl.acl import has_perm_from_args
from api.lib.utils import get_page
from api.lib.utils import get_page_size
@@ -77,6 +77,7 @@ class CIView(APIView):
@has_perm_for_ci("ci_type", ResourceTypeEnum.CI, PermEnum.ADD, lambda x: CITypeCache.get(x))
def post(self):
ci_type = request.values.get("ci_type")
ticket_id = request.values.pop("ticket_id", None)
_no_attribute_policy = request.values.get("no_attribute_policy", ExistPolicy.IGNORE)
exist_policy = request.values.pop('exist_policy', None)
@@ -88,6 +89,7 @@ class CIView(APIView):
exist_policy=exist_policy or ExistPolicy.REJECT,
_no_attribute_policy=_no_attribute_policy,
_is_admin=request.values.pop('__is_admin', None) or False,
ticket_id=ticket_id,
**ci_dict)
return self.jsonify(ci_id=ci_id)
@@ -96,6 +98,7 @@ class CIView(APIView):
def put(self, ci_id=None):
args = request.values
ci_type = args.get("ci_type")
ticket_id = request.values.pop("ticket_id", None)
_no_attribute_policy = args.get("no_attribute_policy", ExistPolicy.IGNORE)
ci_dict = self._wrap_ci_dict()
@@ -103,6 +106,7 @@ class CIView(APIView):
if ci_id is not None:
manager.update(ci_id,
_is_admin=request.values.pop('__is_admin', None) or False,
ticket_id=ticket_id,
**ci_dict)
else:
request.values.pop('exist_policy', None)
@@ -110,6 +114,7 @@ class CIView(APIView):
exist_policy=ExistPolicy.REPLACE,
_no_attribute_policy=_no_attribute_policy,
_is_admin=request.values.pop('__is_admin', None) or False,
ticket_id=ticket_id,
**ci_dict)
return self.jsonify(ci_id=ci_id)
@@ -152,9 +157,10 @@ class CISearchView(APIView):
ret_key = RetKey.NAME
facet = handle_arg_list(request.values.get("facet", ""))
sort = request.values.get("sort")
use_id_filter = request.values.get("use_id_filter", False) in current_app.config.get('BOOL_TRUE')
start = time.time()
s = search(query, fl, facet, page, ret_key, count, sort, excludes)
s = search(query, fl, facet, page, ret_key, count, sort, excludes, use_id_filter=use_id_filter)
try:
response, counter, total, page, numfound, facet = s.search()
except SearchError as e:
@@ -221,7 +227,6 @@ class CIHeartbeatView(APIView):
class CIFlushView(APIView):
url_prefix = ("/ci/flush", "/ci/<int:ci_id>/flush")
# @auth_abandoned
def get(self, ci_id=None):
from api.tasks.cmdb import ci_cache
from api.lib.cmdb.const import CMDB_QUEUE
@@ -250,3 +255,23 @@ class CIPasswordView(APIView):
def post(self, ci_id, attr_id):
return self.get(ci_id, attr_id)
class CIBaselineView(APIView):
url_prefix = ("/ci/baseline", "/ci/<int:ci_id>/baseline/rollback")
@args_required("before_date")
def get(self):
ci_ids = handle_arg_list(request.values.get('ci_ids'))
before_date = request.values.get('before_date')
return self.jsonify(CIManager().baseline(list(map(int, ci_ids)), before_date))
@args_required("before_date")
def post(self, ci_id):
if 'rollback' in request.url:
before_date = request.values.get('before_date')
return self.jsonify(**CIManager().rollback(ci_id, before_date))
return self.get(ci_id)

View File

@@ -13,7 +13,6 @@ from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci_relation.search import Search
from api.lib.decorator import args_required
from api.lib.perm.auth import auth_abandoned
from api.lib.utils import get_page
from api.lib.utils import get_page_size
from api.lib.utils import handle_arg_list
@@ -31,11 +30,14 @@ class CIRelationSearchView(APIView):
level: default is 1
facet: statistic
"""
page = get_page(request.values.get("page", 1))
count = get_page_size(request.values.get("count") or request.values.get("page_size"))
root_id = request.values.get('root_id')
ancestor_ids = request.values.get('ancestor_ids') or None # only for many to many
root_parent_path = handle_arg_list(request.values.get('root_parent_path') or '')
descendant_ids = list(map(int, handle_arg_list(request.values.get('descendant_ids', []))))
level = list(map(int, handle_arg_list(request.values.get('level', '1'))))
query = request.values.get('q', "")
@@ -47,7 +49,8 @@ class CIRelationSearchView(APIView):
start = time.time()
s = Search(root_id, level, query, fl, facet, page, count, sort, reverse,
ancestor_ids=ancestor_ids, has_m2m=has_m2m)
ancestor_ids=ancestor_ids, has_m2m=has_m2m, root_parent_path=root_parent_path,
descendant_ids=descendant_ids)
try:
response, counter, total, page, numfound, facet = s.search()
except SearchError as e:
@@ -65,16 +68,16 @@ class CIRelationSearchView(APIView):
class CIRelationStatisticsView(APIView):
url_prefix = "/ci_relations/statistics"
@auth_abandoned
def get(self):
root_ids = list(map(int, handle_arg_list(request.values.get('root_ids'))))
level = request.values.get('level', 1)
type_ids = set(map(int, handle_arg_list(request.values.get('type_ids', []))))
ancestor_ids = request.values.get('ancestor_ids') or None # only for many to many
descendant_ids = list(map(int, handle_arg_list(request.values.get('descendant_ids', []))))
has_m2m = request.values.get("has_m2m") in current_app.config.get('BOOL_TRUE')
start = time.time()
s = Search(root_ids, level, ancestor_ids=ancestor_ids, has_m2m=has_m2m)
s = Search(root_ids, level, ancestor_ids=ancestor_ids, descendant_ids=descendant_ids, has_m2m=has_m2m)
try:
result = s.statistics(type_ids)
except SearchError as e:
@@ -84,6 +87,26 @@ class CIRelationStatisticsView(APIView):
return self.jsonify(result)
class CIRelationSearchFullView(APIView):
url_prefix = "/ci_relations/search/full"
def get(self):
root_ids = list(map(int, handle_arg_list(request.values.get('root_ids'))))
level = request.values.get('level', 1)
type_ids = list(map(int, handle_arg_list(request.values.get('type_ids', []))))
has_m2m = request.values.get("has_m2m") in current_app.config.get('BOOL_TRUE')
start = time.time()
s = Search(root_ids, level, has_m2m=has_m2m)
try:
result = s.search_full(type_ids)
except SearchError as e:
return abort(400, str(e))
current_app.logger.debug("search time is :{0}".format(time.time() - start))
return self.jsonify(result)
class GetSecondCIsView(APIView):
url_prefix = "/ci_relations/<int:first_ci_id>/second_cis"

View File

@@ -7,21 +7,23 @@ from io import BytesIO
from flask import abort
from flask import current_app
from flask import request
from flask import session
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.ci_type import CITypeAttributeGroupManager
from api.lib.cmdb.ci_type import CITypeAttributeManager
from api.lib.cmdb.ci_type import CITypeGroupManager
from api.lib.cmdb.ci_type import CITypeInheritanceManager
from api.lib.cmdb.ci_type import CITypeManager
from api.lib.cmdb.ci_type import CITypeTemplateManager
from api.lib.cmdb.ci_type import CITypeTriggerManager
from api.lib.cmdb.ci_type import CITypeUniqueConstraintManager
from api.lib.cmdb.const import PermEnum, ResourceTypeEnum, RoleEnum
from api.lib.cmdb.const import PermEnum, ResourceTypeEnum
from api.lib.cmdb.perms import CIFilterPermsCRUD
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
from api.lib.common_setting.role_perm_base import CMDBApp
from api.lib.decorator import args_required
from api.lib.decorator import args_validate
from api.lib.perm.acl.acl import ACLManager
@@ -35,17 +37,27 @@ from api.lib.perm.auth import auth_with_app_token
from api.lib.utils import handle_arg_list
from api.resource import APIView
app_cli = CMDBApp()
class CITypeView(APIView):
url_prefix = ("/ci_types", "/ci_types/<int:type_id>", "/ci_types/<string:type_name>")
url_prefix = ("/ci_types", "/ci_types/<int:type_id>", "/ci_types/<string:type_name>",
"/ci_types/icons")
def get(self, type_id=None, type_name=None):
if request.url.endswith("icons"):
return self.jsonify(CITypeManager().get_icons())
q = request.args.get("type_name")
if type_id is not None:
ci_types = [CITypeCache.get(type_id).to_dict()]
ci_type = CITypeCache.get(type_id).to_dict()
ci_type['parent_ids'] = CITypeInheritanceManager.get_parents(type_id)
ci_types = [ci_type]
elif type_name is not None:
ci_types = [CITypeCache.get(type_name).to_dict()]
ci_type = CITypeCache.get(type_name).to_dict()
ci_type['parent_ids'] = CITypeInheritanceManager.get_parents(ci_type['id'])
ci_types = [ci_type]
else:
ci_types = CITypeManager().get_ci_types(q)
count = len(ci_types)
@@ -53,7 +65,7 @@ class CITypeView(APIView):
return self.jsonify(numfound=count, ci_types=ci_types)
@args_required("name")
@args_validate(CITypeManager.cls)
@args_validate(CITypeManager.cls, exclude_args=['parent_ids'])
def post(self):
params = request.values
@@ -84,10 +96,29 @@ class CITypeView(APIView):
return self.jsonify(type_id=type_id)
class CITypeInheritanceView(APIView):
url_prefix = ("/ci_types/inheritance",)
@args_required("parent_ids")
@args_required("child_id")
@has_perm_from_args("child_id", ResourceTypeEnum.CI, PermEnum.CONFIG, CITypeManager.get_name_by_id)
def post(self):
CITypeInheritanceManager.add(request.values['parent_ids'], request.values['child_id'])
return self.jsonify(**request.values)
@args_required("parent_id")
@args_required("child_id")
@has_perm_from_args("child_id", ResourceTypeEnum.CI, PermEnum.CONFIG, CITypeManager.get_name_by_id)
def delete(self):
CITypeInheritanceManager.delete(request.values['parent_id'], request.values['child_id'])
return self.jsonify(**request.values)
class CITypeGroupView(APIView):
url_prefix = ("/ci_types/groups",
"/ci_types/groups/config",
"/ci_types/groups/order",
"/ci_types/groups/<int:gid>")
def get(self):
@@ -96,7 +127,8 @@ class CITypeGroupView(APIView):
return self.jsonify(CITypeGroupManager.get(need_other, config_required))
@role_required(RoleEnum.CONFIG)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.Model_Configuration,
app_cli.op.create_CIType_group, app_cli.admin_name)
@args_required("name")
@args_validate(CITypeGroupManager.cls)
def post(self):
@@ -107,15 +139,6 @@ class CITypeGroupView(APIView):
@args_validate(CITypeGroupManager.cls)
def put(self, gid=None):
if "/order" in request.url:
if RoleEnum.CONFIG not in session.get("acl", {}).get("parentRoles", []) and not is_app_admin("cmdb"):
return abort(403, ErrFormat.role_required.format(RoleEnum.CONFIG))
group_ids = request.values.get('group_ids')
CITypeGroupManager.order(group_ids)
return self.jsonify(group_ids=group_ids)
name = request.values.get('name') or abort(400, ErrFormat.argument_value_required.format("name"))
type_ids = request.values.get('type_ids')
@@ -123,7 +146,8 @@ class CITypeGroupView(APIView):
return self.jsonify(gid=gid)
@role_required(RoleEnum.CONFIG)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.Model_Configuration,
app_cli.op.delete_CIType_group, app_cli.admin_name)
def delete(self, gid):
type_ids = request.values.get("type_ids")
CITypeGroupManager.delete(gid, type_ids)
@@ -131,6 +155,18 @@ class CITypeGroupView(APIView):
return self.jsonify(gid=gid)
class CITypeGroupOrderView(APIView):
url_prefix = "/ci_types/groups/order"
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.Model_Configuration,
app_cli.op.update_CIType_group, app_cli.admin_name)
def put(self):
group_ids = request.values.get('group_ids')
CITypeGroupManager.order(group_ids)
return self.jsonify(group_ids=group_ids)
class CITypeQueryView(APIView):
url_prefix = "/ci_types/query"
@@ -248,8 +284,8 @@ class CITypeAttributeTransferView(APIView):
@args_required('from')
@args_required('to')
def post(self, type_id):
_from = request.values.get('from') # {'attr_id': xx, 'group_id': xx}
_to = request.values.get('to') # {'group_id': xx, 'order': xxx}
_from = request.values.get('from') # {'attr_id': xx, 'group_id': xx, 'group_name': xx}
_to = request.values.get('to') # {'group_id': xx, 'group_name': xx, 'order': xxx}
CITypeAttributeManager.transfer(type_id, _from, _to)
@@ -262,8 +298,8 @@ class CITypeAttributeGroupTransferView(APIView):
@args_required('from')
@args_required('to')
def post(self, type_id):
_from = request.values.get('from') # group_id
_to = request.values.get('to') # group_id
_from = request.values.get('from') # group_id or group_name
_to = request.values.get('to') # group_id or group_name
CITypeAttributeGroupManager.transfer(type_id, _from, _to)
@@ -296,7 +332,7 @@ class CITypeAttributeGroupView(APIView):
attr_order = list(zip(attrs, orders))
group = CITypeAttributeGroupManager.create_or_update(type_id, name, attr_order, order)
current_app.logger.warning(group.id)
return self.jsonify(group_id=group.id)
@has_perm_from_args("type_id", ResourceTypeEnum.CI, PermEnum.CONFIG, CITypeManager.get_name_by_id)
@@ -310,25 +346,29 @@ class CITypeAttributeGroupView(APIView):
attr_order = list(zip(attrs, orders))
CITypeAttributeGroupManager.update(group_id, name, attr_order, order)
return self.jsonify(group_id=group_id)
@has_perm_from_args("type_id", ResourceTypeEnum.CI, PermEnum.CONFIG, CITypeManager.get_name_by_id)
def delete(self, group_id):
CITypeAttributeGroupManager.delete(group_id)
return self.jsonify(group_id=group_id)
class CITypeTemplateView(APIView):
url_prefix = ("/ci_types/template/import", "/ci_types/template/export", "/ci_types/<int:type_id>/template/export")
@role_required(RoleEnum.CONFIG)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.Model_Configuration,
app_cli.op.download_CIType, app_cli.admin_name)
def get(self, type_id=None): # export
if type_id is not None:
return self.jsonify(dict(ci_type_template=CITypeTemplateManager.export_template_by_type(type_id)))
return self.jsonify(dict(ci_type_template=CITypeTemplateManager.export_template()))
@role_required(RoleEnum.CONFIG)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.Model_Configuration,
app_cli.op.download_CIType, app_cli.admin_name)
def post(self): # import
tpt = request.values.get('ci_type_template') or {}
@@ -348,7 +388,8 @@ class CITypeCanDefineComputed(APIView):
class CITypeTemplateFileView(APIView):
url_prefix = ("/ci_types/template/import/file", "/ci_types/template/export/file")
@role_required(RoleEnum.CONFIG)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.Model_Configuration,
app_cli.op.download_CIType, app_cli.admin_name)
def get(self): # export
tpt_json = CITypeTemplateManager.export_template()
tpt_json = dict(ci_type_template=tpt_json)
@@ -363,7 +404,8 @@ class CITypeTemplateFileView(APIView):
mimetype='application/json',
max_age=0)
@role_required(RoleEnum.CONFIG)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.Model_Configuration,
app_cli.op.download_CIType, app_cli.admin_name)
def post(self): # import
f = request.files.get('file')
@@ -463,18 +505,19 @@ class CITypeGrantView(APIView):
if not acl.has_permission(type_name, ResourceTypeEnum.CI_TYPE, PermEnum.GRANT) and not is_app_admin('cmdb'):
return abort(403, ErrFormat.no_permission.format(type_name, PermEnum.GRANT))
acl.grant_resource_to_role_by_rid(type_name, rid, ResourceTypeEnum.CI_TYPE, perms, rebuild=False)
if perms and not request.values.get('id_filter'):
acl.grant_resource_to_role_by_rid(type_name, rid, ResourceTypeEnum.CI_TYPE, perms, rebuild=False)
if request.values.get('ci_filter') or request.values.get('attr_filter'):
CIFilterPermsCRUD().add(type_id=type_id, rid=rid, **request.values)
else:
new_resource = None
if 'ci_filter' in request.values or 'attr_filter' in request.values or 'id_filter' in request.values:
new_resource = CIFilterPermsCRUD().add(type_id=type_id, rid=rid, **request.values)
if not new_resource:
from api.tasks.acl import role_rebuild
from api.lib.perm.acl.const import ACL_QUEUE
app_id = AppCache.get('cmdb').id
current_app.logger.info((rid, app_id))
role_rebuild.apply_async(args=(rid, app_id), queue=ACL_QUEUE)
current_app.logger.info('done')
return self.jsonify(code=200)
@@ -495,10 +538,18 @@ class CITypeRevokeView(APIView):
if not acl.has_permission(type_name, ResourceTypeEnum.CI_TYPE, PermEnum.GRANT) and not is_app_admin('cmdb'):
return abort(403, ErrFormat.no_permission.format(type_name, PermEnum.GRANT))
acl.revoke_resource_from_role_by_rid(type_name, rid, ResourceTypeEnum.CI_TYPE, perms, rebuild=False)
app_id = AppCache.get('cmdb').id
resource = None
if request.values.get('id_filter'):
CIFilterPermsCRUD().delete2(
type_id=type_id, rid=rid, id_filter=request.values['id_filter'],
parent_path=request.values.get('parent_path'))
return self.jsonify(type_id=type_id, rid=rid)
acl.revoke_resource_from_role_by_rid(type_name, rid, ResourceTypeEnum.CI_TYPE, perms, rebuild=False)
if PermEnum.READ in perms or not perms:
resource = CIFilterPermsCRUD().delete(type_id=type_id, rid=rid)

View File

@@ -11,6 +11,8 @@ 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
from api.lib.common_setting.role_perm_base import CMDBApp
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
@@ -18,6 +20,8 @@ from api.lib.perm.acl.acl import is_app_admin
from api.lib.perm.acl.acl import role_required
from api.resource import APIView
app_cli = CMDBApp()
class GetChildrenView(APIView):
url_prefix = ("/ci_type_relations/<int:parent_id>/children",
@@ -41,18 +45,22 @@ class GetParentsView(APIView):
class CITypeRelationView(APIView):
url_prefix = ("/ci_type_relations", "/ci_type_relations/<int:parent_id>/<int:child_id>")
@role_required(RoleEnum.CONFIG)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.Service_Tree_Definition,
app_cli.op.read, app_cli.admin_name)
def get(self):
res = CITypeRelationManager.get()
res, type2attributes = CITypeRelationManager.get()
return self.jsonify(res)
return self.jsonify(relations=res, type2attributes=type2attributes)
@has_perm_from_args("parent_id", ResourceTypeEnum.CI, PermEnum.CONFIG, CITypeManager.get_name_by_id)
@args_required("relation_type_id")
def post(self, parent_id, child_id):
relation_type_id = request.values.get("relation_type_id")
constraint = request.values.get("constraint")
ctr_id = CITypeRelationManager.add(parent_id, child_id, relation_type_id, constraint)
parent_attr_ids = request.values.get("parent_attr_ids")
child_attr_ids = request.values.get("child_attr_ids")
ctr_id = CITypeRelationManager.add(parent_id, child_id, relation_type_id, constraint,
parent_attr_ids, child_attr_ids)
return self.jsonify(ctr_id=ctr_id)
@@ -66,7 +74,8 @@ class CITypeRelationView(APIView):
class CITypeRelationDelete2View(APIView):
url_prefix = "/ci_type_relations/<int:ctr_id>"
@role_required(RoleEnum.CONFIG)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.Model_Relationships,
app_cli.op.read, app_cli.admin_name)
def delete(self, ctr_id):
CITypeRelationManager.delete(ctr_id)

View File

@@ -3,14 +3,16 @@
from flask import request
from api.lib.cmdb.const import RoleEnum
from api.lib.cmdb.custom_dashboard import CustomDashboardManager
from api.lib.cmdb.custom_dashboard import SystemConfigManager
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.decorator import args_validate
from api.lib.perm.acl.acl import role_required
from api.resource import APIView
app_cli = CMDBApp()
class CustomDashboardApiView(APIView):
url_prefix = ("/custom_dashboard", "/custom_dashboard/<int:_id>", "/custom_dashboard/batch",
@@ -19,7 +21,8 @@ class CustomDashboardApiView(APIView):
def get(self):
return self.jsonify(CustomDashboardManager.get())
@role_required(RoleEnum.CONFIG)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.Customized_Dashboard,
app_cli.op.read, app_cli.admin_name)
@args_validate(CustomDashboardManager.cls)
def post(self):
if request.url.endswith("/preview"):
@@ -32,7 +35,8 @@ class CustomDashboardApiView(APIView):
return self.jsonify(res)
@role_required(RoleEnum.CONFIG)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.Customized_Dashboard,
app_cli.op.read, app_cli.admin_name)
@args_validate(CustomDashboardManager.cls)
def put(self, _id=None):
if _id is not None:
@@ -47,7 +51,8 @@ class CustomDashboardApiView(APIView):
return self.jsonify(id2options=request.values.get('id2options'))
@role_required(RoleEnum.CONFIG)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.Customized_Dashboard,
app_cli.op.read, app_cli.admin_name)
def delete(self, _id):
CustomDashboardManager.delete(_id)
@@ -57,12 +62,14 @@ class CustomDashboardApiView(APIView):
class SystemConfigApiView(APIView):
url_prefix = ("/system_config",)
@role_required(RoleEnum.CONFIG)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.Service_Tree_Definition,
app_cli.op.read, app_cli.admin_name)
@args_required("name", value_required=True)
def get(self):
return self.jsonify(SystemConfigManager.get(request.values['name']))
@role_required(RoleEnum.CONFIG)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.Service_Tree_Definition,
app_cli.op.read, app_cli.admin_name)
@args_validate(SystemConfigManager.cls)
@args_required("name", value_required=True)
@args_required("option", value_required=True)
@@ -74,7 +81,8 @@ class SystemConfigApiView(APIView):
def put(self, _id=None):
return self.post()
@role_required(RoleEnum.CONFIG)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.Service_Tree_Definition,
app_cli.op.read, app_cli.admin_name)
@args_required("name")
def delete(self):
CustomDashboardManager.delete(request.values['name'])

View File

@@ -5,28 +5,29 @@ import datetime
from flask import abort
from flask import request
from flask import session
from api.lib.cmdb.ci import CIManager
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.const import RoleEnum
from api.lib.cmdb.history import AttributeHistoryManger
from api.lib.cmdb.history import CITriggerHistoryManager
from api.lib.cmdb.history import CITypeHistoryManager
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.common_setting.decorator import perms_role_required
from api.lib.common_setting.role_perm_base import CMDBApp
from api.lib.perm.acl.acl import has_perm_from_args
from api.lib.perm.acl.acl import is_app_admin
from api.lib.perm.acl.acl import role_required
from api.lib.utils import get_page
from api.lib.utils import get_page_size
from api.resource import APIView
app_cli = CMDBApp()
class RecordView(APIView):
url_prefix = ("/history/records/attribute", "/history/records/relation")
@role_required(RoleEnum.CONFIG)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.Operation_Audit,
app_cli.op.read, app_cli.admin_name)
def get(self):
page = get_page(request.values.get("page", 1))
page_size = get_page_size(request.values.get("page_size"))
@@ -80,18 +81,21 @@ class CIHistoryView(APIView):
class CITriggerHistoryView(APIView):
url_prefix = ("/history/ci_triggers/<int:ci_id>", "/history/ci_triggers")
url_prefix = ("/history/ci_triggers/<int:ci_id>",)
@has_perm_from_args("ci_id", ResourceTypeEnum.CI, PermEnum.READ, CIManager.get_type_name)
def get(self, ci_id=None):
if ci_id is not None:
result = CITriggerHistoryManager.get_by_ci_id(ci_id)
def get(self, ci_id):
result = CITriggerHistoryManager.get_by_ci_id(ci_id)
return self.jsonify(result)
return self.jsonify(result)
if RoleEnum.CONFIG not in session.get("acl", {}).get("parentRoles", []) and not is_app_admin("cmdb"):
return abort(403, ErrFormat.role_required.format(RoleEnum.CONFIG))
class CIsTriggerHistoryView(APIView):
url_prefix = ("/history/ci_triggers",)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.Operation_Audit,
app_cli.op.read, app_cli.admin_name)
def get(self):
type_id = request.values.get("type_id")
trigger_id = request.values.get("trigger_id")
operate_type = request.values.get("operate_type")
@@ -115,7 +119,8 @@ class CITriggerHistoryView(APIView):
class CITypeHistoryView(APIView):
url_prefix = "/history/ci_types"
@role_required(RoleEnum.CONFIG)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.Operation_Audit,
app_cli.op.read, app_cli.admin_name)
def get(self):
type_id = request.values.get("type_id")
username = request.values.get("username")

View File

@@ -2,25 +2,28 @@
from flask import abort
from flask import current_app
from flask import request
from api.lib.cmdb.ci_type import CITypeManager
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.const import RoleEnum
from api.lib.cmdb.perms import CIFilterPermsCRUD
from api.lib.cmdb.preference import PreferenceManager
from api.lib.cmdb.resp_format import ErrFormat
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.decorator import args_validate
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.perm.acl.acl import validate_permission
from api.lib.utils import handle_arg_list
from api.resource import APIView
app_cli = CMDBApp()
class PreferenceShowCITypesView(APIView):
url_prefix = ("/preference/ci_types", "/preference/ci_types2")
@@ -96,29 +99,38 @@ class PreferenceTreeApiView(APIView):
class PreferenceRelationApiView(APIView):
url_prefix = "/preference/relation/view"
url_prefix = ("/preference/relation/view", "/preference/relation/view/<int:_id>")
def get(self):
views, id2type, name2id = PreferenceManager.get_relation_view()
return self.jsonify(views=views, id2type=id2type, name2id=name2id)
@role_required(RoleEnum.CONFIG)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.Service_Tree_Definition,
app_cli.op.read, app_cli.admin_name)
@args_required("name")
@args_required("cr_ids")
@args_validate(PreferenceManager.pref_rel_cls)
def post(self):
name = request.values.get("name")
is_public = request.values.get("is_public") in current_app.config.get('BOOL_TRUE')
cr_ids = request.values.get("cr_ids")
views, id2type, name2id = PreferenceManager.create_or_update_relation_view(name, cr_ids)
option = request.values.get("option") or None
views, id2type, name2id = PreferenceManager.create_or_update_relation_view(name, cr_ids, is_public=is_public,
option=option)
return self.jsonify(views=views, id2type=id2type, name2id=name2id)
@role_required(RoleEnum.CONFIG)
def put(self):
return self.post()
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.Service_Tree_Definition,
app_cli.op.read, app_cli.admin_name)
@args_required("name")
def put(self, _id):
views, id2type, name2id = PreferenceManager.create_or_update_relation_view(_id=_id, **request.values)
@role_required(RoleEnum.CONFIG)
return self.jsonify(views=views, id2type=id2type, name2id=name2id)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.Service_Tree_Definition,
app_cli.op.read, app_cli.admin_name)
@args_required("name")
def delete(self):
name = request.values.get("name")
@@ -187,3 +199,15 @@ class PreferenceRelationRevokeView(APIView):
acl.revoke_resource_from_role_by_rid(name, rid, ResourceTypeEnum.RELATION_VIEW, perms)
return self.jsonify(code=200)
class PreferenceCITypeOrderView(APIView):
url_prefix = ("/preference/ci_types/order",)
def post(self):
type_ids = request.values.get("type_ids")
is_tree = request.values.get("is_tree") in current_app.config.get('BOOL_TRUE')
PreferenceManager.upsert_ci_type_order(type_ids, is_tree)
return self.jsonify(type_ids=type_ids, is_tree=is_tree)

View File

@@ -4,14 +4,16 @@
from flask import abort
from flask import request
from api.lib.cmdb.const import RoleEnum
from api.lib.cmdb.relation_type import RelationTypeManager
from api.lib.cmdb.resp_format import ErrFormat
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.decorator import args_validate
from api.lib.perm.acl.acl import role_required
from api.resource import APIView
app_cli = CMDBApp()
class RelationTypeView(APIView):
url_prefix = ("/relation_types", "/relation_types/<int:rel_id>")
@@ -19,7 +21,8 @@ class RelationTypeView(APIView):
def get(self):
return self.jsonify([i.to_dict() for i in RelationTypeManager.get_all()])
@role_required(RoleEnum.CONFIG)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.Relationship_Types,
app_cli.op.read, app_cli.admin_name)
@args_required("name")
@args_validate(RelationTypeManager.cls)
def post(self):
@@ -28,7 +31,8 @@ class RelationTypeView(APIView):
return self.jsonify(rel.to_dict())
@role_required(RoleEnum.CONFIG)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.Relationship_Types,
app_cli.op.read, app_cli.admin_name)
@args_required("name")
@args_validate(RelationTypeManager.cls)
def put(self, rel_id):
@@ -37,7 +41,8 @@ class RelationTypeView(APIView):
return self.jsonify(rel.to_dict())
@role_required(RoleEnum.CONFIG)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.Relationship_Types,
app_cli.op.read, app_cli.admin_name)
def delete(self, rel_id):
RelationTypeManager.delete(rel_id)

View File

@@ -0,0 +1,178 @@
# -*- coding:utf-8 -*-
from flask import abort
from flask import request
from api.lib.cmdb.const import PermEnum, ResourceTypeEnum
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.topology import TopologyViewManager
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.decorator import args_validate
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.resource import APIView
app_cli = CMDBApp()
class TopologyGroupView(APIView):
url_prefix = ('/topology_views/groups', '/topology_views/groups/<int:group_id>')
@args_required('name')
@args_validate(TopologyViewManager.group_cls)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.TopologyView,
app_cli.op.create_topology_group, app_cli.admin_name)
def post(self):
name = request.values.get('name')
order = request.values.get('order')
group = TopologyViewManager.add_group(name, order)
return self.jsonify(group.to_dict())
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.TopologyView,
app_cli.op.update_topology_group, app_cli.admin_name)
def put(self, group_id):
name = request.values.get('name')
view_ids = request.values.get('view_ids')
group = TopologyViewManager().update_group(group_id, name, view_ids)
return self.jsonify(**group)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.TopologyView,
app_cli.op.delete_topology_group, app_cli.admin_name)
def delete(self, group_id):
TopologyViewManager.delete_group(group_id)
return self.jsonify(group_id=group_id)
class TopologyGroupOrderView(APIView):
url_prefix = ('/topology_views/groups/order',)
@args_required('group_ids')
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.TopologyView,
app_cli.op.update_topology_group, app_cli.admin_name)
def post(self):
group_ids = request.values.get('group_ids')
TopologyViewManager.group_order(group_ids)
return self.jsonify(group_ids=group_ids)
def put(self):
return self.post()
class TopologyView(APIView):
url_prefix = ('/topology_views', '/topology_views/relations/ci_types/<int:type_id>', '/topology_views/<int:_id>')
def get(self, type_id=None, _id=None):
if type_id is not None:
return self.jsonify(TopologyViewManager.relation_from_ci_type(type_id))
if _id is not None:
return self.jsonify(TopologyViewManager().get_view_by_id(_id))
return self.jsonify(TopologyViewManager.get_all())
@args_required('name', 'central_node_type', 'central_node_instances', 'path', 'group_id')
@args_validate(TopologyViewManager.cls)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.TopologyView,
app_cli.op.create_topology_view, app_cli.admin_name)
def post(self):
name = request.values.pop('name')
group_id = request.values.pop('group_id', None)
option = request.values.pop('option', None)
order = request.values.pop('order', None)
topo_view = TopologyViewManager.add(name, group_id, option, order, **request.values)
return self.jsonify(topo_view)
@args_validate(TopologyViewManager.cls)
@has_perm_from_args("_id", ResourceTypeEnum.TOPOLOGY_VIEW, PermEnum.UPDATE, TopologyViewManager.get_name_by_id)
def put(self, _id):
topo_view = TopologyViewManager.update(_id, **request.values)
return self.jsonify(topo_view)
@has_perm_from_args("_id", ResourceTypeEnum.TOPOLOGY_VIEW, PermEnum.DELETE, TopologyViewManager.get_name_by_id)
def delete(self, _id):
TopologyViewManager.delete(_id)
return self.jsonify(code=200)
class TopologyOrderView(APIView):
url_prefix = ('/topology_views/order',)
@args_required('view_ids')
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.TopologyView,
app_cli.op.create_topology_view, app_cli.admin_name)
def post(self):
view_ids = request.values.get('view_ids')
TopologyViewManager.group_inner_order(view_ids)
return self.jsonify(view_ids=view_ids)
def put(self):
return self.post()
class TopologyViewPreview(APIView):
url_prefix = ('/topology_views/preview', '/topology_views/<int:_id>/view')
def get(self, _id=None):
if _id is not None:
acl = ACLManager('cmdb')
resource_name = TopologyViewManager.get_name_by_id(_id)
if (not acl.has_permission(resource_name, ResourceTypeEnum.TOPOLOGY_VIEW, PermEnum.READ) and
not is_app_admin('cmdb')):
return abort(403, ErrFormat.no_permission.format(resource_name, PermEnum.READ))
return self.jsonify(TopologyViewManager().topology_view(view_id=_id))
else:
return self.jsonify(TopologyViewManager().topology_view(preview=request.values))
def post(self, _id=None):
return self.get(_id)
class TopologyViewGrantView(APIView):
url_prefix = "/topology_views/<int:view_id>/roles/<int:rid>/grant"
def post(self, view_id, rid):
perms = request.values.pop('perms', None)
view_name = TopologyViewManager.get_name_by_id(view_id) or abort(404, ErrFormat.not_found)
acl = ACLManager('cmdb')
if not acl.has_permission(view_name, ResourceTypeEnum.TOPOLOGY_VIEW,
PermEnum.GRANT) and not is_app_admin('cmdb'):
return abort(403, ErrFormat.no_permission.format(view_name, PermEnum.GRANT))
acl.grant_resource_to_role_by_rid(view_name, rid, ResourceTypeEnum.TOPOLOGY_VIEW, perms, rebuild=True)
return self.jsonify(code=200)
class TopologyViewRevokeView(APIView):
url_prefix = "/topology_views/<int:view_id>/roles/<int:rid>/revoke"
@args_required('perms')
def post(self, view_id, rid):
perms = request.values.pop('perms', None)
view_name = TopologyViewManager.get_name_by_id(view_id) or abort(404, ErrFormat.not_found)
acl = ACLManager('cmdb')
if not acl.has_permission(view_name, ResourceTypeEnum.TOPOLOGY_VIEW,
PermEnum.GRANT) and not is_app_admin('cmdb'):
return abort(403, ErrFormat.no_permission.format(view_name, PermEnum.GRANT))
acl.revoke_resource_from_role_by_rid(view_name, rid, ResourceTypeEnum.TOPOLOGY_VIEW, perms, rebuild=True)
return self.jsonify(code=200)

View File

@@ -62,7 +62,7 @@ class DepartmentView(APIView):
class DepartmentIDView(APIView):
url_prefix = (f'{prefix}/<int:_id>',)
def get(self, _id):
def put(self, _id):
form = DepartmentForm(MultiDict(request.json))
if not form.validate():
abort(400, ','.join(['{}: {}'.format(filed, ','.join(msg))

View File

@@ -1,10 +1,10 @@
# -*- coding:utf-8 -*-
import os
from flask import request, abort, current_app, send_from_directory
from flask import request, abort, current_app
from werkzeug.utils import secure_filename
import lz4.frame
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.resource import APIView
@@ -45,32 +45,35 @@ class PostFileView(APIView):
if not file:
abort(400, ErrFormat.file_is_required)
extension = file.mimetype.split('/')[-1]
if '+' in extension:
extension = file.filename.split('.')[-1]
if file.filename == '':
filename = f'.{extension}'
else:
if extension not in file.filename:
filename = file.filename + f".{extension}"
else:
filename = file.filename
if allowed_file(filename, current_app.config.get('ALLOWED_EXTENSIONS', ALLOWED_EXTENSIONS)):
new_filename = generate_new_file_name(filename)
new_filename = secure_filename(new_filename)
file_content = file.read()
compressed_data = lz4.frame.compress(file_content)
try:
CommonFileCRUD.add_file(
origin_name=filename,
file_name=new_filename,
binary=compressed_data,
)
m_type = magic.from_buffer(file.read(2048), mime=True)
file.seek(0)
return self.jsonify(file_name=new_filename)
except Exception as e:
current_app.logger.error(e)
abort(400, ErrFormat.upload_failed.format(e))
if m_type == 'application/octet-stream':
m_type = file.mimetype
elif m_type == 'text/plain':
# https://github.com/ahupp/python-magic/issues/193
m_type = m_type if file.mimetype == m_type else file.mimetype
abort(400, ErrFormat.file_type_not_allowed.format(filename))
extension = MIMEExtMap.get(m_type, None)
if extension is None:
abort(400, f"不支持的文件类型: {m_type}")
filename = file.filename if file.filename and file.filename.endswith(extension) else file.filename + extension
new_filename = generate_new_file_name(filename)
new_filename = secure_filename(new_filename)
file_content = file.read()
compressed_data = lz4.frame.compress(file_content)
try:
CommonFileCRUD.add_file(
origin_name=filename,
file_name=new_filename,
binary=compressed_data,
)
return self.jsonify(file_name=new_filename)
except Exception as e:
current_app.logger.error(e)
abort(400, ErrFormat.upload_failed.format(e))

1
cmdb-api/babel.cfg Normal file
View File

@@ -0,0 +1 @@
[python: api/**.py]

View File

@@ -1,15 +1,16 @@
-i https://mirrors.aliyun.com/pypi/simple
alembic==1.7.7
bs4==0.0.1
celery>=5.3.1
celery==5.3.1
celery-once==3.0.1
click==8.1.3
elasticsearch==7.17.9
email-validator==1.3.1
environs==4.2.0
flasgger==0.9.5
Flask==2.3.2
Flask==2.2.5
Flask-Bcrypt==1.0.1
flask-babel==4.0.0
Flask-Caching==2.0.2
Flask-Cors==4.0.0
Flask-Login>=0.6.2
@@ -30,12 +31,14 @@ marshmallow==2.20.2
more-itertools==5.0.0
msgpack-python==0.5.6
Pillow>=10.0.1
pycryptodome==3.12.0
cryptography>=41.0.2
PyJWT==2.4.0
PyMySQL==1.1.0
ldap3==2.9.1
PyYAML==6.0.1
redis==4.6.0
python-redis-lock==4.0.0
requests==2.31.0
requests_oauthlib==1.3.1
markdownify==0.11.6
@@ -45,9 +48,10 @@ supervisor==4.0.3
timeout-decorator==0.5.0
toposort==1.10
treelib==1.6.1
Werkzeug>=2.3.6
Werkzeug==2.2.3
WTForms==3.0.0
shamir~=17.12.0
pycryptodomex>=3.19.0
colorama>=0.4.6
lz4>=4.3.2
lz4>=4.3.2
python-magic==0.4.27

View File

@@ -20,10 +20,16 @@ DEBUG_TB_INTERCEPT_REDIRECTS = False
ERROR_CODES = [400, 401, 403, 404, 405, 500, 502]
MYSQL_USER = env.str('MYSQL_USER', default='cmdb')
MYSQL_PASSWORD = env.str('MYSQL_PASSWORD', default='123456')
MYSQL_HOST = env.str('MYSQL_HOST', default='127.0.0.1')
MYSQL_PORT = env.int('MYSQL_PORT', default=3306)
MYSQL_DATABASE = env.str('MYSQL_DATABASE', default='cmdb')
# # database
SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://{user}:{password}@127.0.0.1:3306/{db}?charset=utf8'
SQLALCHEMY_DATABASE_URI = f'mysql+pymysql://{MYSQL_USER}:{MYSQL_PASSWORD}@' \
f'{MYSQL_HOST}:{MYSQL_PORT}/{MYSQL_DATABASE}?charset=utf8'
SQLALCHEMY_BINDS = {
'user': 'mysql+pymysql://{user}:{password}@127.0.0.1:3306/{db}?charset=utf8'
'user': SQLALCHEMY_DATABASE_URI
}
SQLALCHEMY_ECHO = False
SQLALCHEMY_TRACK_MODIFICATIONS = False

View File

@@ -13,7 +13,7 @@ const getAntdSerials = (color) => {
const themePluginOption = {
fileName: 'css/theme-colors-[contenthash:8].css',
matchColors: getAntdSerials('#1890ff'), // 主色系列
matchColors: getAntdSerials('#2f54eb'), // 主色系列
// 改变样式选择器,解决样式覆盖问题
changeSelector (selector) {
switch (selector) {

View File

@@ -27,7 +27,8 @@
"core-js": "^3.31.0",
"echarts": "^5.3.2",
"element-ui": "^2.15.10",
"exceljs": "^4.3.0",
"exceljs": "^4.4.0",
"file-saver": "^2.0.5",
"html2canvas": "^1.0.0-rc.5",
"is-buffer": "^2.0.5",
"jquery": "^3.6.0",
@@ -38,15 +39,18 @@
"md5": "^2.2.1",
"moment": "^2.24.0",
"nprogress": "^0.2.0",
"relation-graph": "^1.1.0",
"relation-graph": "^2.1.42",
"snabbdom": "^3.5.1",
"sortablejs": "1.9.0",
"style-resources-loader": "^1.5.0",
"viser-vue": "^2.4.8",
"vue": "2.6.11",
"vue-clipboard2": "^0.3.3",
"vue-cli-plugin-style-resources-loader": "^0.1.5",
"vue-codemirror": "^4.0.6",
"vue-cropper": "^0.6.2",
"vue-grid-layout": "2.3.12",
"vue-i18n": "8.28.2",
"vue-infinite-scroll": "^2.0.2",
"vue-json-editor": "^1.4.3",
"vue-ls": "^3.2.1",
@@ -55,7 +59,7 @@
"vue-template-compiler": "2.6.11",
"vuedraggable": "^2.23.0",
"vuex": "^3.1.1",
"vxe-table": "3.6.9",
"vxe-table": "3.7.10",
"vxe-table-plugin-export-xlsx": "2.0.0",
"xe-utils": "3",
"xlsx": "0.15.0",

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=1702544951995') format('woff2'),
url('iconfont.woff?t=1702544951995') format('woff'),
url('iconfont.ttf?t=1702544951995') format('truetype');
src: url('iconfont.woff2?t=1718872392430') format('woff2'),
url('iconfont.woff?t=1718872392430') format('woff'),
url('iconfont.ttf?t=1718872392430') format('truetype');
}
.iconfont {
@@ -13,6 +13,642 @@
-moz-osx-font-smoothing: grayscale;
}
.cmdb-manual_warehousing:before {
content: "\e95f";
}
.cmdb-not_warehousing:before {
content: "\e95d";
}
.cmdb-warehousing:before {
content: "\e95e";
}
.cmdb-prompt:before {
content: "\e95c";
}
.cmdb-arrow:before {
content: "\e95b";
}
.cmdb-automatic_inventory:before {
content: "\e95a";
}
.cmdb-week_additions:before {
content: "\e959";
}
.cmdb-month_additions:before {
content: "\e958";
}
.cmdb-rule:before {
content: "\e955";
}
.cmdb-executing_machine:before {
content: "\e956";
}
.cmdb-resource:before {
content: "\e957";
}
.cmdb-discovery_resources:before {
content: "\e954";
}
.cmdb-association:before {
content: "\e953";
}
.ops-is_dynamic-disabled:before {
content: "\e952";
}
.itsm-pdf:before {
content: "\e951";
}
.monitor-sqlserver:before {
content: "\e950";
}
.monitor-dig2:before {
content: "\e94d";
}
.monitor-base2:before {
content: "\e94e";
}
.monitor-foreground1:before {
content: "\e94f";
}
.monitor-log2:before {
content: "\e945";
}
.monitor-backgroud1:before {
content: "\e946";
}
.monitor-port1:before {
content: "\e947";
}
.monitor-ipmi2:before {
content: "\e948";
}
.monitor-process2:before {
content: "\e949";
}
.monitor-snmp2:before {
content: "\e94a";
}
.monitor-performance1:before {
content: "\e94b";
}
.monitor-testing1:before {
content: "\e94c";
}
.monitor-ping2:before {
content: "\e941";
}
.monitor-prometheus:before {
content: "\e942";
}
.monitor-websocket2:before {
content: "\e943";
}
.monitor-traceroute2:before {
content: "\e944";
}
.monitor-port:before {
content: "\e93c";
}
.monitor-base1:before {
content: "\e93d";
}
.monitor-backgroud:before {
content: "\e93e";
}
.monitor-dig1:before {
content: "\e93f";
}
.monitor-foreground:before {
content: "\e940";
}
.monitor-log1:before {
content: "\e934";
}
.monitor-process1:before {
content: "\e935";
}
.monitor-testing:before {
content: "\e936";
}
.monitor-snmp1:before {
content: "\e937";
}
.monitor-performance:before {
content: "\e938";
}
.monitor-traceroute1:before {
content: "\e939";
}
.monitor-ping1:before {
content: "\e93a";
}
.monitor-ipmi1:before {
content: "\e93b";
}
.a-monitor-prometheus1:before {
content: "\e932";
}
.monitor-websocket1:before {
content: "\e933";
}
.monitor-group_expansion1:before {
content: "\e930";
}
.monitor-group_collapse1:before {
content: "\e931";
}
.monitor-group_expansion:before {
content: "\e92e";
}
.monitor-group_collapse:before {
content: "\e92f";
}
.monitor-list_view:before {
content: "\e92d";
}
.monitor-group_view:before {
content: "\e92c";
}
.ops-topology_view:before {
content: "\e92b";
}
.monitor-host_analysis:before {
content: "\e92a";
}
.a-Group427319324:before {
content: "\e929";
}
.monitor-native:before {
content: "\e928";
}
.veops-filter2:before {
content: "\e927";
}
.ops-cmdb-data_companies-selected:before {
content: "\e601";
}
.ops-cmdb-data_companies:before {
content: "\e926";
}
.monitor-threshold_value:before {
content: "\e921";
}
.monitor-disposition:before {
content: "\e922";
}
.monitor-automatic_discovery:before {
content: "\e923";
}
.monitor-grouping_list:before {
content: "\e924";
}
.monitor-node_list:before {
content: "\e925";
}
.monitor-general_view:before {
content: "\e920";
}
.monitor-network_topology:before {
content: "\e91b";
}
.monitor-node_management:before {
content: "\e91c";
}
.monitor-alarm_policy:before {
content: "\e91d";
}
.monitor-alarm:before {
content: "\e91e";
}
.monitor-healing:before {
content: "\e91f";
}
.monitor-data_acquisition:before {
content: "\e8d4";
}
.monitor-analysis:before {
content: "\e91a";
}
.monitor-index:before {
content: "\e89b";
}
.monitor-user_defined:before {
content: "\e867";
}
.monitor-database:before {
content: "\e861";
}
.monitor-common:before {
content: "\e865";
}
.veops-edit:before {
content: "\e866";
}
.veops-empower:before {
content: "\e863";
}
.veops-share:before {
content: "\e864";
}
.veops-export:before {
content: "\e862";
}
.a-veops-import1:before {
content: "\e860";
}
.monitor-ip:before {
content: "\e807";
}
.monitor-director:before {
content: "\e803";
}
.monitor-host:before {
content: "\e804";
}
.a-cmdb-log1:before {
content: "\e802";
}
.monitor-add:before {
content: "\e7ff";
}
.monitor-down:before {
content: "\e7fc";
}
.monitor-up:before {
content: "\e7fd";
}
.itsm-unfold:before {
content: "\e7f9";
}
.itsm-stretch:before {
content: "\e7f8";
}
.monitor-data_comaparison2:before {
content: "\e7a1";
}
.monitor-data_comaparison1:before {
content: "\e7f7";
}
.a-monitor-online1:before {
content: "\e7a0";
}
.ops-setting-application-selected:before {
content: "\e919";
}
.ops-setting-application:before {
content: "\e918";
}
.ops-setting-basic:before {
content: "\e889";
}
.ops-setting-basic-selected:before {
content: "\e917";
}
.ops-setting-security:before {
content: "\e915";
}
.ops-setting-theme:before {
content: "\e916";
}
.veops-show:before {
content: "\e914";
}
.itsm-duration:before {
content: "\e913";
}
.itsm-workload:before {
content: "\e912";
}
.caise-VPC:before {
content: "\e910";
}
.caise-CDN:before {
content: "\e911";
}
.caise-OOS:before {
content: "\e90f";
}
.Google_Cloud_Platform:before {
content: "\e90b";
}
.Ctyun:before {
content: "\e90c";
}
.Alibaba_Cloud:before {
content: "\e90d";
}
.Azure:before {
content: "\e90e";
}
.ZStack:before {
content: "\e904";
}
.Tencent_Cloud:before {
content: "\e905";
}
.Nutanix:before {
content: "\e906";
}
.OpenStack:before {
content: "\e907";
}
.Huawei_Cloud:before {
content: "\e908";
}
.Bytecloud:before {
content: "\e909";
}
.UCloud:before {
content: "\e90a";
}
.AWS:before {
content: "\e901";
}
.ECloud:before {
content: "\e902";
}
.JDCloud:before {
content: "\e903";
}
.veops-more:before {
content: "\e900";
}
.duose-date:before {
content: "\e8ff";
}
.duose-shishu:before {
content: "\e8fd";
}
.duose-wenben:before {
content: "\e8fe";
}
.duose-json:before {
content: "\e8f7";
}
.duose-fudianshu:before {
content: "\e8f8";
}
.duose-time:before {
content: "\e8f9";
}
.duose-password:before {
content: "\e8fa";
}
.duose-link:before {
content: "\e8fb";
}
.duose-datetime:before {
content: "\e8fc";
}
.veops-setting2:before {
content: "\e8f6";
}
.veops-search:before {
content: "\e8f5";
}
.veops-delete:before {
content: "\e8f4";
}
.veops-refresh:before {
content: "\e8f3";
}
.veops-filter:before {
content: "\e8f2";
}
.veops-reduce:before {
content: "\e8ed";
}
.veops-increase:before {
content: "\e8ee";
}
.veops-configuration_table:before {
content: "\e8ef";
}
.veops-copy:before {
content: "\e8f0";
}
.veops-save:before {
content: "\e8f1";
}
.veops-setting:before {
content: "\e8ec";
}
.veops-default_avatar:before {
content: "\e8ea";
}
.veops-notice:before {
content: "\e8eb";
}
.itsm-quickStart:before {
content: "\e8e9";
}
.itsm-associatedWith:before {
content: "\e8e8";
}
.itsm-folder:before {
content: "\e8e7";
}
.report:before {
content: "\e8e5";
}
.folder:before {
content: "\e8e6";
}
.itsm-refresh:before {
content: "\e8e4";
}
.itsm-add_table:before {
content: "\e8e2";
}
.itsm-delete_page:before {
content: "\e8e3";
}
.oneterm-secret_key:before {
content: "\e8e0";
}
.oneterm-password:before {
content: "\e8e1";
}
.itsm-sla_timeout_not_handled:before {
content: "\e8dd";
}
.itsm-sla_not_timeout:before {
content: "\e8de";
}
.itsm-SLA:before {
content: "\e8df";
}
.itsm-sla_timeout_handled:before {
content: "\e8dc";
}
.itsm-sla_all:before {
content: "\e8da";
}
.itsm-generate_by_node_id:before {
content: "\e8db";
}
.cmdb-MySQL:before {
content: "\e8d9";
}
.OAUTH2:before {
content: "\e8d8";
}
@@ -29,11 +665,7 @@
content: "\e8d5";
}
.ops-setting-auth-selected:before {
content: "\e8d4";
}
.a-itsm-knowledge2:before {
.itsm-knowledge2:before {
content: "\e8d2";
}
@@ -261,10 +893,6 @@
content: "\e89c";
}
.ops-setting-duty-selected:before {
content: "\e89b";
}
.datainsight-sequential:before {
content: "\e899";
}
@@ -429,38 +1057,6 @@
content: "\e870";
}
.ops-itsm-ticketsetting-selected:before {
content: "\e860";
}
.ops-itsm-reports-selected:before {
content: "\e861";
}
.ops-itsm-servicecatalog-selected:before {
content: "\e862";
}
.ops-itsm-ticketmanage-selected:before {
content: "\e863";
}
.ops-itsm-knowledge-selected:before {
content: "\e864";
}
.ops-itsm-workstation-selected:before {
content: "\e865";
}
.ops-itsm-servicedesk-selected:before {
content: "\e866";
}
.ops-itsm-planticket-selected:before {
content: "\e867";
}
.ops-itsm-servicecatalog:before {
content: "\e868";
}
@@ -833,26 +1429,10 @@
content: "\e816";
}
.ops-cmdb-batch-selected:before {
content: "\e803";
}
.ops-cmdb-batch:before {
content: "\e80a";
}
.ops-cmdb-adc-selected:before {
content: "\e7f7";
}
.ops-cmdb-resource-selected:before {
content: "\e7f8";
}
.ops-cmdb-preference-selected:before {
content: "\e7f9";
}
.ops-cmdb-preference:before {
content: "\e7fa";
}
@@ -861,22 +1441,10 @@
content: "\e7fb";
}
.ops-cmdb-tree-selected:before {
content: "\e7fc";
}
.ops-cmdb-relation-selected:before {
content: "\e7fd";
}
.ops-cmdb-adc:before {
content: "\e7fe";
}
.ops-cmdb-search-selected:before {
content: "\e7ff";
}
.ops-cmdb-relation:before {
content: "\e800";
}
@@ -885,14 +1453,6 @@
content: "\e801";
}
.ops-cmdb-citype-selected:before {
content: "\e802";
}
.ops-cmdb-dashboard-selected:before {
content: "\e804";
}
.ops-cmdb-citype:before {
content: "\e805";
}
@@ -901,10 +1461,6 @@
content: "\e806";
}
.ops-cmdb-screen-selected:before {
content: "\e807";
}
.ops-cmdb-resource:before {
content: "\e808";
}
@@ -1253,14 +1809,6 @@
content: "\e7a6";
}
.ops-setting-role-selected:before {
content: "\e7a0";
}
.ops-setting-group-selected:before {
content: "\e7a1";
}
.ops-setting-role:before {
content: "\e7a2";
}
@@ -1701,18 +2249,10 @@
content: "\e738";
}
.ops-setting-notice-email-selected-copy:before {
content: "\e889";
}
.ops-setting-notice:before {
content: "\e72f";
}
.ops-setting-notice-selected:before {
content: "\e730";
}
.ops-setting-notice-email-selected:before {
content: "\e731";
}
@@ -1737,10 +2277,6 @@
content: "\e736";
}
.ops-setting-companyStructure-selected:before {
content: "\e72b";
}
.ops-setting-companyStructure:before {
content: "\e72c";
}
@@ -1749,10 +2285,6 @@
content: "\e72d";
}
.ops-setting-companyInfo-selected:before {
content: "\e72e";
}
.ops-email:before {
content: "\e61a";
}
@@ -2793,14 +3325,6 @@
content: "\e600";
}
.ops-dag-dashboard-selected:before {
content: "\e601";
}
.ops-dag-applet-selected:before {
content: "\e602";
}
.ops-dag-applet:before {
content: "\e603";
}
@@ -2809,62 +3333,26 @@
content: "\e604";
}
.ops-dag-terminal-selected:before {
content: "\e605";
}
.ops-dag-cron:before {
content: "\e606";
}
.ops-dag-cron-selected:before {
content: "\e608";
}
.ops-dag-history:before {
content: "\e609";
}
.ops-dag-history-selected:before {
content: "\e60a";
}
.ops-dag-dags-selected:before {
content: "\e60c";
}
.ops-dag-dagreview:before {
content: "\e60d";
}
.ops-dag-dagreview-selected:before {
content: "\e60e";
}
.ops-dag-panel:before {
content: "\e60f";
}
.ops-dag-panel-selected:before {
content: "\e615";
}
.ops-dag-variables:before {
content: "\e616";
}
.ops-dag-variables-selected:before {
content: "\e618";
}
.ops-dag-appletadmin:before {
content: "\e65c";
}
.ops-dag-appletadmin-selected:before {
content: "\e65d";
}
.ops-dag-dags:before {
content: "\e60b";
}

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,5 +1,5 @@
<template>
<a-config-provider :locale="locale">
<a-config-provider :locale="antdLocale">
<div id="app" :class="{ 'ops-fullscreen': isOpsFullScreen, 'ops-only-topmenu': isOpsOnlyTopMenu }">
<router-view v-if="alive" />
</div>
@@ -7,8 +7,9 @@
</template>
<script>
import { mapActions } from 'vuex'
import { mapState, mapActions, mapMutations } from 'vuex'
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'
@@ -24,20 +25,28 @@ export default {
},
data() {
return {
locale: zhCN,
alive: true,
timer: null,
}
},
computed: {
...mapState(['locale']),
antdLocale() {
if (this.locale === 'zh') {
return zhCN
}
return enUS
},
isOpsFullScreen() {
return this.$route.name === 'cmdb_screen'
return ['cmdb_screen'].includes(this.$route.name)
},
isOpsOnlyTopMenu() {
return ['fullscreen_index', 'setting_person'].includes(this.$route.name)
return ['fullscreen_index', 'setting_person', 'notice_center'].includes(this.$route.name)
},
},
created() {
this.SET_LOCALE(localStorage.getItem('ops_locale') || 'zh')
this.$i18n.locale = localStorage.getItem('ops_locale') || 'zh'
this.timer = setInterval(() => {
this.setTime(new Date().getTime())
}, 1000)
@@ -184,6 +193,7 @@ export default {
},
methods: {
...mapActions(['setTime']),
...mapMutations(['SET_LOCALE']),
reload() {
this.alive = false
this.$nextTick(() => {

View File

@@ -30,9 +30,9 @@ export function getAuthDataEnable() {
})
}
export function testLDAP(test_type, data) {
export function testLDAP(data) {
return axios({
url: `/common-setting/v1/auth_config/LDAP/test?test_type=${test_type}`,
url: `/common-setting/v1/auth_config/LDAP/test`,
method: 'post',
data,
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

@@ -1,14 +0,0 @@
<svg width="1em" height="1em" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1 0H2.5V1.25H1.25V2.5H0V1C0 0.447715 0.447715 0 1 0ZM0 7.5V9C0 9.55229 0.447715 10 1 10H2.5V8.75H1.25V7.5H0ZM8.75 7.5V8.75H7.5V10H9C9.55229 10 10 9.55228 10 9V7.5H8.75ZM10 2.5V1C10 0.447715 9.55228 0 9 0H7.5V1.25H8.75V2.5H10Z" fill="url(#paint0_linear_124_16807)"/>
<rect x="2.5" y="3.125" width="5" height="3.75" fill="url(#paint1_linear_124_16807)"/>
<defs>
<linearGradient id="paint0_linear_124_16807" x1="5" y1="0" x2="5" y2="10" gradientUnits="userSpaceOnUse">
<stop stop-color="#4F84FF"/>
<stop offset="1" stop-color="#85CBFF"/>
</linearGradient>
<linearGradient id="paint1_linear_124_16807" x1="5" y1="3.125" x2="5" y2="6.875" gradientUnits="userSpaceOnUse">
<stop stop-color="#4F84FF"/>
<stop offset="1" stop-color="#85CBFF"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 916 B

View File

@@ -1,14 +0,0 @@
<svg width="1em" height="1em" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="2.5" y="2.5" width="5" height="5" rx="0.5" fill="url(#paint0_linear_124_16808)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1 0C0.447715 0 0 0.447715 0 1V9C0 9.55229 0.447715 10 1 10H9C9.55229 10 10 9.55228 10 9V1C10 0.447715 9.55228 0 9 0H1ZM8.75 1.25H1.25V8.75H8.75V1.25Z" fill="url(#paint1_linear_124_16808)"/>
<defs>
<linearGradient id="paint0_linear_124_16808" x1="5" y1="2.5" x2="5" y2="7.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#5187FF"/>
<stop offset="1" stop-color="#84C9FF"/>
</linearGradient>
<linearGradient id="paint1_linear_124_16808" x1="5" y1="0" x2="5" y2="10" gradientUnits="userSpaceOnUse">
<stop stop-color="#5187FF"/>
<stop offset="1" stop-color="#84C9FF"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 840 B

View File

@@ -1,9 +0,0 @@
<svg width="1em" height="1em" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.56845 8.8409C3.06335 8.78963 1.86719 8.05799 2.06279 6.48243C2.1538 5.75105 2.64549 5.3214 3.34457 5.16041C3.67173 5.08909 4.00806 5.06954 4.34128 5.10247C4.40203 5.10811 4.44843 5.11401 4.47689 5.11837L4.51586 5.12631C4.64379 5.15574 4.77263 5.18104 4.90218 5.20219C5.26786 5.2651 5.63914 5.28941 6.0099 5.27474C6.8046 5.23219 7.21015 4.97429 7.23092 4.41672C7.25424 3.79429 6.76332 3.29619 5.86659 2.91832C5.52815 2.77793 5.17843 2.66645 4.82117 2.58506C4.70325 2.55755 4.58482 2.53328 4.46587 2.51226C4.30323 2.94847 3.9867 3.31016 3.57591 3.5292C3.16512 3.74824 2.68841 3.80952 2.23557 3.70149C1.90324 3.61651 1.60053 3.44214 1.36029 3.1973C1.12004 2.95245 0.951447 2.64649 0.872793 2.3126C0.794138 1.97872 0.808429 1.62967 0.914116 1.30333C1.0198 0.976995 1.21285 0.685836 1.4723 0.461451C1.73176 0.237065 2.04771 0.0880244 2.38588 0.0305017C2.72404 -0.0270211 3.07151 0.00917138 3.39056 0.135152C3.70961 0.261132 3.98807 0.472088 4.19571 0.745127C4.40335 1.01817 4.53225 1.34286 4.56841 1.68397C4.6812 1.70269 4.83374 1.73217 5.01524 1.77421C5.42003 1.86601 5.81625 1.99216 6.1996 2.15131C7.38191 2.64966 8.1156 3.39463 8.07638 4.4462C8.03639 5.53187 7.23425 6.04253 6.0563 6.10533C5.62418 6.12373 5.19132 6.09614 4.76503 6.02304C4.61925 5.99997 4.47398 5.9716 4.32923 5.93793C4.30731 5.93532 4.28534 5.9331 4.26335 5.93127C4.02033 5.90687 3.77501 5.92018 3.53606 5.97075C3.15153 6.05893 2.94311 6.24146 2.90056 6.58267C2.78725 7.49504 3.47915 7.94443 5.42694 8.00416C5.44492 7.65558 5.5586 7.3187 5.75548 7.03049C5.95237 6.74229 6.22485 6.51389 6.54303 6.37039C6.8612 6.22689 7.21277 6.17383 7.55912 6.21703C7.90548 6.26023 8.23323 6.39802 8.50641 6.61528C8.77959 6.83254 8.98763 7.12086 9.10769 7.4486C9.22775 7.77634 9.25519 8.13082 9.187 8.47314C9.11881 8.81545 8.95763 9.13235 8.72114 9.38907C8.48465 9.64578 8.18201 9.83237 7.84643 9.92836C7.39921 10.0556 6.92094 10.0153 6.50129 9.81515C6.08164 9.61495 5.74941 9.26855 5.56691 8.8409H5.56845Z" fill="url(#paint0_linear_124_16804)"/>
<defs>
<linearGradient id="paint0_linear_124_16804" x1="5.02318" y1="0.00390625" x2="5.02318" y2="10.0013" gradientUnits="userSpaceOnUse">
<stop stop-color="#497DFF"/>
<stop offset="1" stop-color="#8CD5FF"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -1,9 +0,0 @@
<svg width="1em" height="1em" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.01211 4.50621L2.7769 5.74077C2.712 5.80565 2.66051 5.88268 2.62538 5.96747C2.59025 6.05225 2.57217 6.14313 2.57217 6.2349C2.57217 6.32668 2.59025 6.41755 2.62538 6.50234C2.66051 6.58712 2.712 6.66416 2.7769 6.72904L3.27085 7.223C3.33573 7.28791 3.41276 7.3394 3.49754 7.37453C3.58232 7.40966 3.67319 7.42774 3.76496 7.42774C3.85674 7.42774 3.94761 7.40966 4.03239 7.37453C4.11717 7.3394 4.1942 7.28791 4.25908 7.223L5.49394 5.98775C5.6237 6.1175 5.72663 6.27155 5.79686 6.44109C5.86708 6.61063 5.90323 6.79234 5.90323 6.97585C5.90323 7.15935 5.86708 7.34106 5.79686 7.5106C5.72663 7.68014 5.6237 7.83419 5.49394 7.96394L3.76479 9.69316C3.56827 9.88963 3.30176 10 3.02387 10C2.74599 10 2.47948 9.88963 2.28296 9.69316L0.306832 7.71696C0.110368 7.52043 0 7.25391 0 6.97602C0 6.69813 0.110368 6.43161 0.306832 6.23508L2.03599 4.50586C2.16574 4.3761 2.31978 4.27317 2.48931 4.20294C2.65884 4.13271 2.84055 4.09657 3.02405 4.09657C3.20755 4.09657 3.38925 4.13271 3.55879 4.20294C3.72832 4.27317 3.88236 4.3761 4.01211 4.50586V4.50621ZM5.98789 5.49414L7.2231 4.25923C7.288 4.19435 7.33949 4.11732 7.37462 4.03253C7.40975 3.94775 7.42783 3.85687 7.42783 3.7651C7.42783 3.67332 7.40975 3.58245 7.37462 3.49766C7.33949 3.41288 7.288 3.33584 7.2231 3.27096L6.72915 2.777C6.66428 2.71209 6.58724 2.6606 6.50246 2.62547C6.41768 2.59034 6.32681 2.57226 6.23504 2.57226C6.14326 2.57226 6.05239 2.59034 5.96761 2.62547C5.88283 2.6606 5.8058 2.71209 5.74092 2.777L4.50606 4.01225C4.3763 3.8825 4.27337 3.72845 4.20314 3.55891C4.13292 3.38937 4.09677 3.20766 4.09677 3.02415C4.09677 2.84065 4.13292 2.65894 4.20314 2.4894C4.27337 2.31986 4.3763 2.16581 4.50606 2.03606L6.23521 0.306843C6.43173 0.110371 6.69824 0 6.97613 0C7.25401 0 7.52052 0.110371 7.71704 0.306843L9.69317 2.28304C9.88963 2.47957 10 2.74609 10 3.02398C10 3.30187 9.88963 3.56839 9.69317 3.76492L7.96401 5.49414C7.83426 5.6239 7.68022 5.72683 7.51069 5.79706C7.34116 5.86729 7.15945 5.90343 6.97595 5.90343C6.79245 5.90343 6.61075 5.86729 6.44121 5.79706C6.27168 5.72683 6.11764 5.6239 5.98789 5.49414ZM3.51817 5.9881L5.98789 3.51829C6.05339 3.45274 6.14225 3.4159 6.23491 3.41586C6.32758 3.41583 6.41646 3.45261 6.48201 3.51812C6.54755 3.58362 6.5844 3.67248 6.58443 3.76515C6.58446 3.85782 6.54768 3.9467 6.48218 4.01225L4.01211 6.48206C3.94661 6.54761 3.85775 6.58445 3.76509 6.58449C3.67242 6.58452 3.58354 6.54774 3.51799 6.48223C3.45245 6.41673 3.4156 6.32787 3.41557 6.2352C3.41554 6.14253 3.45232 6.05365 3.51782 5.9881H3.51817Z" fill="url(#paint0_linear_124_16775)"/>
<defs>
<linearGradient id="paint0_linear_124_16775" x1="5" y1="0" x2="5" y2="10" gradientUnits="userSpaceOnUse">
<stop stop-color="#5A85FF"/>
<stop offset="1" stop-color="#8DD8FF"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -1,9 +0,0 @@
<svg width="1em" height="1em" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.31822 4.16667H2.54549V2.5C2.54549 1.11458 3.63981 0 5.00003 0C6.36026 0 7.45458 1.11458 7.45458 2.5V4.16667H8.68185C8.90685 4.16667 9.09094 4.35417 9.09094 4.58333V9.58333C9.09094 9.8125 8.90685 10 8.68185 10H1.31822C1.09322 10 0.909124 9.8125 0.909124 9.58333V4.58333C0.909124 4.35417 1.09322 4.16667 1.31822 4.16667ZM5.00003 7.91667C5.45003 7.91667 5.81822 7.54167 5.81822 7.08333C5.81822 6.625 5.45003 6.25 5.00003 6.25C4.55003 6.25 4.18185 6.625 4.18185 7.08333C4.18185 7.54167 4.55003 7.91667 5.00003 7.91667ZM3.36367 4.16667H6.6364V2.5C6.6364 1.58333 5.90003 0.833333 5.00003 0.833333C4.10003 0.833333 3.36367 1.58333 3.36367 2.5V4.16667Z" fill="url(#paint0_linear_124_16805)"/>
<defs>
<linearGradient id="paint0_linear_124_16805" x1="5.00003" y1="0" x2="5.00003" y2="10" gradientUnits="userSpaceOnUse">
<stop stop-color="#4D82FF"/>
<stop offset="1" stop-color="#88CFFF"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1022 B

View File

@@ -1,9 +0,0 @@
<svg width="1em" height="1em" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.91242 9.46382C3.91242 9.57428 3.82288 9.66382 3.71242 9.66382H2.35075C2.2403 9.66382 2.15075 9.57428 2.15075 9.46382V3.55962C2.15075 3.44916 2.06121 3.35962 1.95075 3.35962H0.539905C0.354312 3.35962 0.268806 3.12879 0.40961 3.00788L3.58212 0.283626C3.71182 0.172253 3.91242 0.264405 3.91242 0.43536V9.46382ZM6.08758 0.567715C6.08758 0.457258 6.17712 0.367716 6.28758 0.367716H7.64925C7.7597 0.367716 7.84925 0.457259 7.84925 0.567716V6.4411C7.84925 6.55156 7.93879 6.6411 8.04925 6.6411H9.46001C9.64561 6.6411 9.73111 6.87195 9.59029 6.99285L6.41786 9.71645C6.28816 9.8278 6.08758 9.73565 6.08758 9.5647V0.567715Z" fill="url(#paint0_linear_124_16806)"/>
<defs>
<linearGradient id="paint0_linear_124_16806" x1="5" y1="0" x2="5" y2="10" gradientUnits="userSpaceOnUse">
<stop stop-color="#5A85FF"/>
<stop offset="1" stop-color="#8DD8FF"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 979 B

View File

@@ -1,9 +0,0 @@
<svg width="1em" height="1em" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.51961 6.8937V10H4.48V6.8937H1.76732C1.65823 6.8937 1.56223 6.85372 1.48369 6.77237C1.40522 6.69504 1.3621 6.5915 1.36369 6.48421C1.36369 5.95891 1.52769 5.48566 1.85895 5.06411C2.18986 4.64428 2.56258 4.43334 2.97893 4.43334V1.64277C2.75966 1.64277 2.57167 1.56142 2.41022 1.39873C2.25355 1.24349 2.16738 1.0362 2.17022 0.821384C2.17022 0.598718 2.25022 0.407762 2.41022 0.244037C2.56912 0.0827244 2.7593 0 2.97893 0H7.01959C7.23885 0 7.42685 0.0813456 7.5883 0.244037C7.74721 0.406728 7.82866 0.598718 7.82866 0.821384C7.82866 1.04405 7.74866 1.23501 7.58867 1.39873C7.4283 1.5628 7.23885 1.64277 7.01959 1.64277V4.43196C7.43594 4.43196 7.81012 4.64291 8.13956 5.06273C8.46631 5.47151 8.64098 5.97137 8.63628 6.48421C8.63628 6.59486 8.59701 6.6924 8.51665 6.77237C8.43665 6.85234 8.34211 6.8937 8.23302 6.8937H5.51998H5.51961Z" fill="url(#paint0_linear_124_16803)"/>
<defs>
<linearGradient id="paint0_linear_124_16803" x1="5.00001" y1="0" x2="5.00001" y2="10" gradientUnits="userSpaceOnUse">
<stop stop-color="#5A85FF"/>
<stop offset="1" stop-color="#8DD8FF"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

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