mirror of
https://github.com/veops/cmdb.git
synced 2025-09-18 02:26:53 +08:00
Compare commits
520 Commits
2.3.3
...
dependabot
Author | SHA1 | Date | |
---|---|---|---|
|
249e955f0d | ||
|
01fcd7f80e | ||
|
af254ddbeb | ||
|
422e89c6c6 | ||
|
c2c11b38ed | ||
|
45d3f57228 | ||
|
c711c3d3fd | ||
|
2ae4aeee67 | ||
|
46238b8b51 | ||
|
b1528ec511 | ||
|
8ef4edb7d2 | ||
|
0172206c7c | ||
|
9d5bdf1b0d | ||
|
c0726b228d | ||
|
5b314aa907 | ||
|
c9f0de9838 | ||
|
2db41dd992 | ||
|
89e492c1f3 | ||
|
1aeb9a2702 | ||
|
b090a88b76 | ||
|
d5c479f7e5 | ||
|
b342258e75 | ||
|
3d716eff3e | ||
|
9791a184e3 | ||
|
142b8c95c5 | ||
|
363b89011f | ||
|
321372fa88 | ||
|
fc27dc6e64 | ||
|
8ad98f4ba4 | ||
|
9b050fa7fa | ||
|
a30826827a | ||
|
691b50aec4 | ||
|
55ca2d5eb5 | ||
|
779cd1ea9e | ||
|
7d53180a2f | ||
|
b6c41c00dd | ||
|
fff5679f6e | ||
|
7556dfe56b | ||
|
44b6f2b2ad | ||
|
f5607d96f3 | ||
|
2e85a9971b | ||
|
8d177266dc | ||
|
42d870ea4e | ||
|
424cad2130 | ||
|
430308a679 | ||
|
cc7570a4b2 | ||
|
413c0dfdfe | ||
|
f8fee771c4 | ||
|
3d05cef7cd | ||
|
30477f736e | ||
|
0cd5c0277b | ||
|
4fcddd1010 | ||
|
b269ef894f | ||
|
ea13432e4c | ||
|
c8a68bd185 | ||
|
f9ee895f58 | ||
|
59d9f2c79a | ||
|
f629558d60 | ||
|
4cbf70e756 | ||
|
40b9bec122 | ||
|
17b27d492a | ||
|
dd8f66a3fa | ||
|
d501436e3d | ||
|
8c6389b4f8 | ||
|
63ed73fa14 | ||
|
ef90506916 | ||
|
c0541b7e50 | ||
|
388134b38c | ||
|
f57d640c5d | ||
|
42c506ded8 | ||
|
373346e71e | ||
|
983cb4041c | ||
|
718fca57d2 | ||
|
2b1b0333ff | ||
|
9bb787d3f4 | ||
|
9f5979c1b1 | ||
|
b58230fd56 | ||
|
8e6ce05c04 | ||
|
b8d93bc9eb | ||
|
968ef93153 | ||
|
e0a0113e69 | ||
|
7181f2879a | ||
|
ff1626ff07 | ||
|
dca8781a03 | ||
|
43c5d74661 | ||
|
b1d0bdb536 | ||
|
2420631ed5 | ||
|
deabebb922 | ||
|
cd451060e3 | ||
|
82d4c4f961 | ||
|
79f5dac661 | ||
|
1a7c3fcd21 | ||
|
606be7f8e8 | ||
|
7f66f7688b | ||
|
25ad2192fd | ||
|
881b8cc1fa | ||
|
68905281f2 | ||
|
3cb78f30d2 | ||
|
ebce839eaa | ||
|
3170048ea9 | ||
|
976cac6742 | ||
|
99e5a932ae | ||
|
058585504f | ||
|
6d50a13cf0 | ||
|
198ecad13a | ||
|
1660139b27 | ||
|
0155eb98a2 | ||
|
c0c05bca86 | ||
|
32227d375a | ||
|
eb9c20a295 | ||
|
b826de4195 | ||
|
c3b55c2850 | ||
|
33313b6206 | ||
|
00fbe15a19 | ||
|
9e5c926bfd | ||
|
f0467c3d3b | ||
|
6758b994f8 | ||
|
d8646f8e7c | ||
|
0fa95aed36 | ||
|
d82356444a | ||
|
b8fa68ec8d | ||
|
78c542e71d | ||
|
6594863934 | ||
|
61e8c61fc6 | ||
|
ec9b5a0fa0 | ||
|
3664994cc5 | ||
|
366311e59b | ||
|
03f2ff912d | ||
|
ff9af51465 | ||
|
04fc588935 | ||
|
f215e887c9 | ||
|
3e6955fc7a | ||
|
b1cfb88308 | ||
|
d1af0eba79 | ||
|
f41873dc8c | ||
|
63562007df | ||
|
a99ecb9ea5 | ||
|
08f9bd9071 | ||
|
17c851f354 | ||
|
3382195a25 | ||
|
cd70b16eb3 | ||
|
2a98c00d97 | ||
|
5044af5490 | ||
|
320dc07676 | ||
|
95d3b0233a | ||
|
093466ef06 | ||
|
ceacc0ab15 | ||
|
cb3a9d9432 | ||
|
23fd56cf28 | ||
|
dd1c783919 | ||
|
8296e9a552 | ||
|
590565bdf0 | ||
|
9e2bf3d1f0 | ||
|
7547b67805 | ||
|
4abe4d7e8f | ||
|
0ebd52f3fd | ||
|
4cc2e47f02 | ||
|
b16bfdfa03 | ||
|
ece24080d5 | ||
|
a0ea8a193b | ||
|
d8e09d449e | ||
|
44c99e0f2e | ||
|
7110b4bcb3 | ||
|
3989859945 | ||
|
29ed4dd708 | ||
|
dbcbe33ba0 | ||
|
ed340a1c33 | ||
|
17ee7622b4 | ||
|
51877778bf | ||
|
1de8b492ea | ||
|
1fe38c4ec3 | ||
|
92bff08b84 | ||
|
37d0dacd2e | ||
|
e22f45fd25 | ||
|
56ef0a055a | ||
|
522991e23b | ||
|
ff061d4d2e | ||
|
5b84a704b1 | ||
|
3ba83821f2 | ||
|
45b82aa52d | ||
|
c99790bda2 | ||
|
7272880062 | ||
|
8130dd92ab | ||
|
9ca2d38307 | ||
|
e61bbdbcb7 | ||
|
88960c3082 | ||
|
0aa433882e | ||
|
d4f5713e0a | ||
|
790204ea28 | ||
|
927ed393d3 | ||
|
bf92b89a5f | ||
|
7ddaa143da | ||
|
157361cc7a | ||
|
dcf768376a | ||
|
e339ad2c23 | ||
|
8b10c5c72d | ||
|
bd13f6b744 | ||
|
934d00e87d | ||
|
9d421993a0 | ||
|
6751f673d5 | ||
|
c420a2195a | ||
|
ec3a6e0b6e | ||
|
ab0a5399f7 | ||
|
33e63c762f | ||
|
53b2a228ae | ||
|
c687e7ad9b | ||
|
ea3cdbd5f5 | ||
|
1ebe74d966 | ||
|
1f935d25a2 | ||
|
14e4dbacac | ||
|
f7a9257e16 | ||
|
99562df6cd | ||
|
630013cb85 | ||
|
6351d3af64 | ||
|
2d96048828 | ||
|
3e67bb94bc | ||
|
a26c9ff542 | ||
|
252003f76d | ||
|
928149f2a0 | ||
|
0283af812c | ||
|
6ccb2e74e0 | ||
|
5016464016 | ||
|
b787bd9b7b | ||
|
11f69d8679 | ||
|
c3168035ed | ||
|
5140c0a58f | ||
|
ee97582579 | ||
|
83e9172722 | ||
|
341e687987 | ||
|
93d9804127 | ||
|
f3d42cb356 | ||
|
04b9a3c929 | ||
|
e98160b84a | ||
|
7c769a53da | ||
|
08dc343083 | ||
|
52e654ff60 | ||
|
c570b5b61f | ||
|
60aec1f9ef | ||
|
9ec590664b | ||
|
49d612d8db | ||
|
b9d83f7539 | ||
|
158d9f8a76 | ||
|
4eff09900d | ||
|
23ae320223 | ||
|
a22324ca49 | ||
|
d0e7198b9c | ||
|
14c6d7178c | ||
|
3e830f266b | ||
|
67d7062f39 | ||
|
e31fee9f6d | ||
|
61b5144362 | ||
|
154f84d64e | ||
|
b0c21fe735 | ||
|
328105ed05 | ||
|
1436ffb3b2 | ||
|
abc83db77b | ||
|
806f5af830 | ||
|
c6e150a3df | ||
|
41ae241deb | ||
|
7c83f9dca7 | ||
|
77199c1b69 | ||
|
15941f3ee3 | ||
|
a644783b7c | ||
|
2c9f33b851 | ||
|
8b3f10dece | ||
|
9665294dd9 | ||
|
2048202efb | ||
|
1861d24593 | ||
|
3e51f89c5a | ||
|
e02d92c335 | ||
|
7abc1ad581 | ||
|
e45070ebdc | ||
|
2ef4c438e2 | ||
|
64eab86497 | ||
|
4495f006c0 | ||
|
6c56757a9d | ||
|
109d4f1a2e | ||
|
e3a3580206 | ||
|
db5ff60aff | ||
|
c444fed436 | ||
|
98c7f97274 | ||
|
9c76b388c9 | ||
|
7936b9cb09 | ||
|
5256f79ba1 | ||
|
3c6cc7f91b | ||
|
8a5dadcaa4 | ||
|
08fcac6a12 | ||
|
103596238e | ||
|
889d9155bc | ||
|
e6d3c34f75 | ||
|
97fc5896a5 | ||
|
c1beffac3b | ||
|
0cac7fa3f4 | ||
|
fb9186aa1e | ||
|
9af0009516 | ||
|
5758b8e999 | ||
|
da29e72d17 | ||
|
2bfb1839e5 | ||
|
3cc1915ff0 | ||
|
66aef97b06 | ||
|
672660330f | ||
|
d6731409b5 | ||
|
ed279416c8 | ||
|
e8ed71afbc | ||
|
51321ec86c | ||
|
d73e5e8ef0 | ||
|
53779e037e | ||
|
5b73f69d60 | ||
|
92637f0537 | ||
|
6f7b0a3b76 | ||
|
647b11734f | ||
|
e62d71c6c7 | ||
|
df90a54ae7 | ||
|
4a1ed9be13 | ||
|
a44be48c7e | ||
|
0da1712343 | ||
|
f5472c7fa8 | ||
|
3a9a966a60 | ||
|
91647767ed | ||
|
e69404bada | ||
|
332abe223d | ||
|
3bb32e1ead | ||
|
66476ed254 | ||
|
a4eb221199 | ||
|
3f53eab514 | ||
|
ba2a6a65b5 | ||
|
486fcac138 | ||
|
cf5e4966c0 | ||
|
79536732c1 | ||
|
84b7b53a73 | ||
|
6b11b50bd5 | ||
|
ccccce262f | ||
|
d4b3e259c8 | ||
|
450508efee | ||
|
8ea0379a0d | ||
|
6f320d4440 | ||
|
8d7897665e | ||
|
1505d9d948 | ||
|
c0ff2ac5b9 | ||
|
a9aff714d1 | ||
|
040986df20 | ||
|
9ef6d3e955 | ||
|
28888a04ee | ||
|
e7c004fa19 | ||
|
93c0eefe3c | ||
|
84fcbaefb6 | ||
|
b9ef7856fe | ||
|
c3b5b30b1f | ||
|
9c47d9c733 | ||
|
c305aaaef6 | ||
|
2ed10069f1 | ||
|
8d3756566b | ||
|
91c844ba63 | ||
|
93ebc8d2e9 | ||
|
342a79ed0c | ||
|
ddc3b564fb | ||
|
bacda2ed1e | ||
|
61e6f36e0c | ||
|
b798d5e2cc | ||
|
7b3f95b41d | ||
|
82c921bb88 | ||
|
d975aceaab | ||
|
a188218818 | ||
|
1484a308cf | ||
|
ea0a7df472 | ||
|
b4de2bdb9e | ||
|
d2f3589154 | ||
|
07d2af6671 | ||
|
acf881dcb7 | ||
|
cd0797e860 | ||
|
328d678455 | ||
|
4e0dc1282f | ||
|
e336b31902 | ||
|
f6c3edfac1 | ||
|
e7f856fecd | ||
|
780c51c364 | ||
|
ba142266fb | ||
|
87deba2e46 | ||
|
048f556936 | ||
|
90a97402ef | ||
|
362122952b | ||
|
00f4eeb90d | ||
|
5da5d32b54 | ||
|
d49cd99881 | ||
|
680a547df4 | ||
|
356cf58135 | ||
|
28ff27c019 | ||
|
bb9d7da435 | ||
|
1fa9bf8880 | ||
|
75bf42ea3c | ||
|
5931e9879b | ||
|
d82dc10451 | ||
|
705f4916b9 | ||
|
96f4760a92 | ||
|
d8d48e6d13 | ||
|
9d3d2e6b00 | ||
|
a3ff843f54 | ||
|
4a421b4da2 | ||
|
e4ecee8264 | ||
|
1129e110dd | ||
|
c0a3e39367 | ||
|
934f4cf93e | ||
|
47b712620c | ||
|
4e7e853173 | ||
|
03bc27087d | ||
|
317a16aa21 | ||
|
3a8d4ecebc | ||
|
b2b355b0dd | ||
|
60814cea2f | ||
|
6801058a3f | ||
|
92183423df | ||
|
ccf1d1c09a | ||
|
13e9a0b7bd | ||
|
e129b0a32e | ||
|
18122de03e | ||
|
c07f39d669 | ||
|
81ecffa166 | ||
|
4316880d02 | ||
|
9af34d0beb | ||
|
2eb6159e6e | ||
|
fc11c5626e | ||
|
5669e253a9 | ||
|
6973cc68ed | ||
|
e73810b456 | ||
|
261f673e17 | ||
|
b0e6248cd1 | ||
|
c663a2b783 | ||
|
9b9ae61505 | ||
|
4987908368 | ||
|
d492e7188c | ||
|
92c163971c | ||
|
e351c7cf36 | ||
|
38b4293a33 | ||
|
8ca93d72f7 | ||
|
f7b94dbe6e | ||
|
92cb9633b0 | ||
|
2f3b5a03ee | ||
|
47cfce7ee9 | ||
|
2210e9715b | ||
|
0151e0ff56 | ||
|
bc63548dd8 | ||
|
39aaa916dc | ||
|
43bacef520 | ||
|
a822503b67 | ||
|
77ed1ddb28 | ||
|
2d17e29441 | ||
|
6c4eefa9c0 | ||
|
a492f97199 | ||
|
4c7deee866 | ||
|
31a97d7ad0 | ||
|
c2e4090aea | ||
|
40e49aa1e9 | ||
|
0bf8bfb113 | ||
|
bc7b51c544 | ||
|
1c6d056d58 | ||
|
f3a0761ffc | ||
|
2ce8a6bdbf | ||
|
7ae58611c0 | ||
|
4ec3b310e4 | ||
|
bd34023512 | ||
|
2205c359b8 | ||
|
2723f187ac | ||
|
bb91f21e40 | ||
|
f1c2bcbd79 | ||
|
a8255d2b69 | ||
|
22c02f94e0 | ||
|
e1a5c01438 | ||
|
fd21dfe94d | ||
|
4bc6492b21 | ||
|
eaa5cb8bf1 | ||
|
47c66be179 | ||
|
089a17ded8 | ||
|
cb0eb7eadd | ||
|
82a402883b | ||
|
00f6e8e7d9 | ||
|
63cae8510f | ||
|
62b56da0d8 | ||
|
79a19d4b49 | ||
|
f91c82b241 | ||
|
89ffec35e8 | ||
|
2935ca5923 | ||
|
46213dd758 | ||
|
aee66c075b | ||
|
29e80f8597 | ||
|
618cd7ff16 | ||
|
de22a4e04b | ||
|
3fdfa4dc92 | ||
|
7da14f6aa0 | ||
|
046d0e60b7 | ||
|
dc18e3b82f | ||
|
cb8ef65e79 | ||
|
341a4359fa | ||
|
f89169de96 | ||
|
47767e22d8 | ||
|
5f389459e8 | ||
|
02ea6afa9c | ||
|
47692c16ef | ||
|
d9a2cff82a | ||
|
f05aec3d16 | ||
|
ed54fca4b8 | ||
|
538cc3debf | ||
|
2bfe6f0fe1 | ||
|
802a995cbb | ||
|
8d13a7630f | ||
|
c80291a1b3 | ||
|
77ad772d95 | ||
|
174375d3bf | ||
|
f00d97522a | ||
|
93df278328 | ||
|
f592af0ff4 | ||
|
872d9baa59 | ||
|
56baa2bedc | ||
|
2615fd7661 | ||
|
9b609724bd | ||
|
f3aacd72ee | ||
|
3bead5438d | ||
|
6ef1df9f4f | ||
|
c7ab0c2beb | ||
|
20fd6393e4 | ||
|
02c01f60bf |
60
.github/ISSUE_TEMPLATE/1bug.yaml
vendored
Normal file
60
.github/ISSUE_TEMPLATE/1bug.yaml
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
name: Bug Report
|
||||
description: File a bug report
|
||||
title: "[Bug]: "
|
||||
labels: ["☢️ bug"]
|
||||
assignees:
|
||||
- Selina316
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
- type: input
|
||||
id: contact
|
||||
attributes:
|
||||
label: Contact Details
|
||||
description: How can we get in touch with you if we need more info?
|
||||
placeholder: ex. email@example.com
|
||||
validations:
|
||||
required: false
|
||||
- type: dropdown
|
||||
id: aspects
|
||||
attributes:
|
||||
label: This bug is related to UI or API?
|
||||
multiple: true
|
||||
options:
|
||||
- UI
|
||||
- API
|
||||
- type: textarea
|
||||
id: happened
|
||||
attributes:
|
||||
label: What happened?
|
||||
description: Also tell us, what did you expect to happen?
|
||||
placeholder: Tell us what you see!
|
||||
value: "A bug happened!"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: What version of our software are you running?
|
||||
value: "newest"
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: browsers
|
||||
attributes:
|
||||
label: What browsers are you seeing the problem on?
|
||||
multiple: true
|
||||
options:
|
||||
- Firefox
|
||||
- Chrome
|
||||
- Safari
|
||||
- Microsoft Edge
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Relevant log output
|
||||
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
||||
render: shell
|
44
.github/ISSUE_TEMPLATE/2feature.yaml
vendored
Normal file
44
.github/ISSUE_TEMPLATE/2feature.yaml
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
name: Feature wanted
|
||||
description: A new feature would be good
|
||||
title: "[Feature]: "
|
||||
labels: ["✏️ feature"]
|
||||
assignees:
|
||||
- pycook
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thank you for your feature suggestion; we will evaluate it carefully!
|
||||
- type: input
|
||||
id: contact
|
||||
attributes:
|
||||
label: Contact Details
|
||||
description: How can we get in touch with you if we need more info?
|
||||
placeholder: ex. email@example.com
|
||||
validations:
|
||||
required: false
|
||||
- type: dropdown
|
||||
id: aspects
|
||||
attributes:
|
||||
label: feature is related to UI or API aspects?
|
||||
multiple: true
|
||||
options:
|
||||
- UI
|
||||
- API
|
||||
- type: textarea
|
||||
id: feature
|
||||
attributes:
|
||||
label: What is your advice?
|
||||
description: Also tell us, what did you expect to happen?
|
||||
placeholder: Tell us what you want!
|
||||
value: "everyone wants this feature!"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: What version of our software are you running?
|
||||
value: "newest"
|
||||
validations:
|
||||
required: true
|
36
.github/ISSUE_TEMPLATE/3consultation.yaml
vendored
Normal file
36
.github/ISSUE_TEMPLATE/3consultation.yaml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
name: Help wanted
|
||||
description: I have a question
|
||||
title: "[help wanted]: "
|
||||
labels: ["help wanted"]
|
||||
assignees:
|
||||
- ivonGwy
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Please tell us what's you need!
|
||||
- type: input
|
||||
id: contact
|
||||
attributes:
|
||||
label: Contact Details
|
||||
description: How can we get in touch with you if we need more info?
|
||||
placeholder: ex. email@example.com
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: question
|
||||
attributes:
|
||||
label: What is your question?
|
||||
description: Also tell us, how can we help?
|
||||
placeholder: Tell us what you need!
|
||||
value: "i have a question!"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: What version of our software are you running?
|
||||
value: "newest"
|
||||
validations:
|
||||
required: true
|
60
.github/ISSUE_TEMPLATE/bug.yaml
vendored
Normal file
60
.github/ISSUE_TEMPLATE/bug.yaml
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
name: Bug Report
|
||||
description: File a bug report
|
||||
title: "[Bug]: "
|
||||
labels: ["bug"]
|
||||
assignees:
|
||||
- pycook
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
- type: input
|
||||
id: contact
|
||||
attributes:
|
||||
label: Contact Details
|
||||
description: How can we get in touch with you if we need more info?
|
||||
placeholder: ex. email@example.com
|
||||
validations:
|
||||
required: false
|
||||
- type: dropdown
|
||||
id: type
|
||||
attributes:
|
||||
label: bug is related to UI or API aspects?
|
||||
multiple: true
|
||||
options:
|
||||
- UI
|
||||
- API
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: What happened?
|
||||
description: Also tell us, what did you expect to happen?
|
||||
placeholder: Tell us what you see!
|
||||
value: "A bug happened!"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: What version of our software are you running?
|
||||
default: 2.3.5
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: browsers
|
||||
attributes:
|
||||
label: What browsers are you seeing the problem on?
|
||||
multiple: true
|
||||
options:
|
||||
- Firefox
|
||||
- Chrome
|
||||
- Safari
|
||||
- Microsoft Edge
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Relevant log output
|
||||
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
||||
render: shell
|
6
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
6
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: veops official website
|
||||
url: https://veops.cn/#hero
|
||||
about: you can contact us here.
|
||||
|
0
.github/ISSUE_TEMPLATE/consultation.yaml
vendored
Normal file
0
.github/ISSUE_TEMPLATE/consultation.yaml
vendored
Normal file
44
.github/ISSUE_TEMPLATE/feature.yaml
vendored
Normal file
44
.github/ISSUE_TEMPLATE/feature.yaml
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
name: Feature wanted
|
||||
description: A new feature would be good
|
||||
title: "[Feature]: "
|
||||
labels: ["feature"]
|
||||
assignees:
|
||||
- pycook
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thank you for your feature suggestion; we will evaluate it carefully!
|
||||
- type: input
|
||||
id: contact
|
||||
attributes:
|
||||
label: Contact Details
|
||||
description: How can we get in touch with you if we need more info?
|
||||
placeholder: ex. email@example.com
|
||||
validations:
|
||||
required: false
|
||||
- type: dropdown
|
||||
id: type
|
||||
attributes:
|
||||
label: feature is related to UI or API aspects?
|
||||
multiple: true
|
||||
options:
|
||||
- UI
|
||||
- API
|
||||
- type: textarea
|
||||
id: describe the feature
|
||||
attributes:
|
||||
label: What is your advice?
|
||||
description: Also tell us, what did you expect to happen?
|
||||
placeholder: Tell us what you want!
|
||||
value: "everyone wants this feature!"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: What version of our software are you running?
|
||||
default: 2.3.5
|
||||
validations:
|
||||
required: true
|
0
.github/config.yml
vendored
Normal file
0
.github/config.yml
vendored
Normal file
2
.gitignore
vendored
2
.gitignore
vendored
@@ -40,6 +40,7 @@ nosetests.xml
|
||||
.pytest_cache
|
||||
cmdb-api/test-output
|
||||
cmdb-api/api/uploaded_files
|
||||
cmdb-api/migrations/versions
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
@@ -69,6 +70,7 @@ settings.py
|
||||
# UI
|
||||
cmdb-ui/node_modules
|
||||
cmdb-ui/dist
|
||||
cmdb-ui/yarn.lock
|
||||
|
||||
# Log files
|
||||
cmdb-ui/npm-debug.log*
|
||||
|
4
Makefile
4
Makefile
@@ -9,7 +9,7 @@ help: ## display this help
|
||||
|
||||
env: ## create a development environment using pipenv
|
||||
sudo easy_install pip && \
|
||||
pip install pipenv -i https://pypi.douban.com/simple && \
|
||||
pip install pipenv -i https://repo.huaweicloud.com/repository/pypi/simple && \
|
||||
npm install yarn && \
|
||||
make deps
|
||||
.PHONY: env
|
||||
@@ -36,7 +36,7 @@ api: ## start api server
|
||||
.PHONY: api
|
||||
|
||||
worker: ## start async tasks worker
|
||||
cd cmdb-api && pipenv run celery -A celery_worker.celery worker -E -Q one_cmdb_async --concurrency=1 -D && pipenv run celery -A celery_worker.celery worker -E -Q acl_async --concurrency=1 -D
|
||||
cd cmdb-api && pipenv run celery -A celery_worker.celery worker -E -Q one_cmdb_async --autoscale=5,2 --logfile=one_cmdb_async.log -D && pipenv run celery -A celery_worker.celery worker -E -Q acl_async --autoscale=2,1 --logfile=one_acl_async.log -D
|
||||
.PHONY: worker
|
||||
|
||||
ui: ## start ui server
|
||||
|
54
README.md
54
README.md
@@ -1,12 +1,20 @@
|
||||

|
||||
|
||||
[](https://github.com/veops/cmdb/blob/master/LICENSE)
|
||||
[](https://github.com/sendya/ant-design-pro-vue)
|
||||
[](https://github.com/pallets/flask)
|
||||
<p align="center">
|
||||
<a href="https://veops.cn"><img src="docs/images/logo.png" alt="维易CMDB" width="300"/></a>
|
||||
</p>
|
||||
<h3 align="center">简单、轻量、通用的运维配置管理数据库</h3>
|
||||
<p align="center">
|
||||
<a href="https://github.com/veops/cmdb/blob/master/LICENSE"><img src="https://img.shields.io/badge/License-AGPLv3-brightgreen" alt="License: GPLv3"></a>
|
||||
<a href="https:https://github.com/sendya/ant-design-pro-vue"><img src="https://img.shields.io/badge/UI-Ant%20Design%20Pro%20Vue-brightgreen" alt="UI"></a>
|
||||
<a href="https://github.com/pallets/flask"><img src="https://img.shields.io/badge/API-Flask-brightgreen" alt="API"></a>
|
||||
</p>
|
||||
|
||||
|
||||
------------------------------
|
||||
|
||||
[English](docs/README_en.md) / [中文](README.md)
|
||||
- 产品文档:https://veops.cn/docs/
|
||||
- 在线体验: <a href="https://cmdb.veops.cn" target="_blank">CMDB</a>
|
||||
- 在线体验:<a href="https://cmdb.veops.cn" target="_blank">CMDB</a>
|
||||
- username: demo 或者 admin
|
||||
- password: 123456
|
||||
|
||||
@@ -15,45 +23,43 @@
|
||||
|
||||
## 系统介绍
|
||||
|
||||
### 整体架构
|
||||
### 系统概览
|
||||
|
||||
<img src=docs/images/view.jpg />
|
||||
<img src=docs/images/dashboard.png />
|
||||
|
||||
### 相关文档
|
||||
[查看更多展示](docs/screenshot.md)
|
||||
|
||||
- <a href="https://zhuanlan.zhihu.com/p/98453732" target="_blank">设计文档</a>
|
||||
### 相关文章
|
||||
|
||||
- <a href="https://mp.weixin.qq.com/s/v3eANth64UBW5xdyOkK3tg" target="_blank">概要设计</a>
|
||||
- <a href="https://github.com/veops/cmdb/tree/master/docs/cmdb_api.md" target="_blank">API 文档</a>
|
||||
- <a href="https://mp.weixin.qq.com/s/EflmmJ-qdUkddTx2hRt3pA" target="_blank">树形视图实践</a>
|
||||
- <a href="https://mp.weixin.qq.com/s/rQaf4AES7YJsyNQG_MKOLg" target="_blank">自动发现</a>
|
||||
- 更多文章可以在公众号 **维易科技OneOps** 里查看
|
||||
|
||||
### 特点
|
||||
|
||||
- 灵活性
|
||||
1. 规范并统一纳管复杂数据资产
|
||||
2. 自动发现、入库 IT 资产
|
||||
1. 配置灵活,不设定任何运维场景,有内置模板
|
||||
2. 自动发现、入库 IT 资产
|
||||
- 安全性
|
||||
1. 细粒度访问控制
|
||||
1. 细粒度权限控制
|
||||
2. 完备操作日志
|
||||
- 多应用
|
||||
1. 丰富视图展示维度
|
||||
2. 提供 Restful API
|
||||
3. 自定义字段触发器
|
||||
2. API简单强大
|
||||
3. 支持定义属性触发器、计算属性
|
||||
|
||||
### 主要功能
|
||||
|
||||
- 模型属性支持索引、多值、默认排序、字体颜色,支持计算属性
|
||||
- 支持自动发现、定时巡检、文件导入
|
||||
- 支持资源、树形、关系视图展示
|
||||
- 支持资源、层级、关系视图展示
|
||||
- 支持模型间关系配置和展示
|
||||
- 细粒度访问控制,完备的操作日志
|
||||
- 支持跨模型搜索
|
||||
|
||||
### 系统概览
|
||||
|
||||
- 服务树
|
||||
|
||||

|
||||
|
||||
[查看更多展示](docs/screenshot.md)
|
||||
|
||||
|
||||
### 更多功能
|
||||
@@ -67,7 +73,7 @@
|
||||
## 安装
|
||||
|
||||
### Docker 一键快速构建
|
||||
- 进入主目录(先安装 docker 环境)
|
||||
- 进入主目录(先安装 docker 环境, 注意要clone整个项目)
|
||||
|
||||
```
|
||||
docker-compose up -d
|
||||
@@ -83,6 +89,6 @@ docker-compose up -d
|
||||
|
||||
---
|
||||
|
||||
_**欢迎关注我们的公众号,点击联系我们,加入微信、QQ群(336164978),获得更多产品、行业相关资讯**_
|
||||
_**欢迎关注公众号(维易科技OneOps),关注后可加入微信群,进行产品和技术交流。**_
|
||||
|
||||

|
||||

|
||||
|
@@ -6,7 +6,7 @@ name = "pypi"
|
||||
[packages]
|
||||
# Flask
|
||||
Flask = "==2.3.2"
|
||||
Werkzeug = "==2.3.6"
|
||||
Werkzeug = ">=2.3.6"
|
||||
click = ">=5.0"
|
||||
# Api
|
||||
Flask-RESTful = "==0.3.10"
|
||||
@@ -21,33 +21,36 @@ Flask-Migrate = "==2.5.2"
|
||||
gunicorn = "==21.0.1"
|
||||
supervisor = "==4.0.3"
|
||||
# Auth
|
||||
Flask-Login = "==0.6.2"
|
||||
Flask-Login = ">=0.6.2"
|
||||
Flask-Bcrypt = "==1.0.1"
|
||||
Flask-Cors = ">=3.0.8"
|
||||
python-ldap = "==3.4.0"
|
||||
ldap3 = "==2.9.1"
|
||||
pycryptodome = "==3.12.0"
|
||||
cryptography = ">=41.0.2"
|
||||
# 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"
|
||||
kombu = ">=5.3.1"
|
||||
# common setting
|
||||
timeout-decorator = "==0.5.0"
|
||||
WTForms = "==3.0.0"
|
||||
email-validator = "==1.3.1"
|
||||
treelib = "==1.6.1"
|
||||
flasgger = "==0.9.5"
|
||||
Pillow = "==9.3.0"
|
||||
Pillow = ">=10.0.1"
|
||||
# other
|
||||
six = "==1.12.0"
|
||||
six = "==1.16.0"
|
||||
bs4 = ">=0.0.1"
|
||||
toposort = ">=1.5"
|
||||
requests = ">=2.22.0"
|
||||
requests_oauthlib = "==1.3.1"
|
||||
markdownify = "==0.11.6"
|
||||
PyJWT = "==2.4.0"
|
||||
elasticsearch = "==7.17.9"
|
||||
future = "==0.18.3"
|
||||
@@ -56,6 +59,9 @@ Jinja2 = "==3.1.2"
|
||||
jinja2schema = "==0.1.4"
|
||||
msgpack-python = "==0.5.6"
|
||||
alembic = "==1.7.7"
|
||||
hvac = "==2.0.0"
|
||||
colorama = ">=0.4.6"
|
||||
pycryptodomex = ">=3.19.0"
|
||||
|
||||
[dev-packages]
|
||||
# Testing
|
||||
@@ -72,4 +78,3 @@ flake8-isort = "==2.7.0"
|
||||
isort = "==4.3.21"
|
||||
pep8-naming = "==0.8.2"
|
||||
pydocstyle = "==3.0.0"
|
||||
|
||||
|
@@ -7,6 +7,7 @@ import os
|
||||
import sys
|
||||
from inspect import getmembers
|
||||
from logging.handlers import RotatingFileHandler
|
||||
from pathlib import Path
|
||||
|
||||
from flask import Flask
|
||||
from flask import jsonify
|
||||
@@ -17,11 +18,14 @@ from flask.json.provider import DefaultJSONProvider
|
||||
|
||||
import api.views.entry
|
||||
from api.extensions import (bcrypt, cache, celery, cors, db, es, login_manager, migrate, rd)
|
||||
from api.extensions import inner_secrets
|
||||
from api.flask_cas import CAS
|
||||
from api.lib.secrets.secrets import InnerKVManger
|
||||
from api.models.acl import User
|
||||
|
||||
HERE = os.path.abspath(os.path.dirname(__file__))
|
||||
PROJECT_ROOT = os.path.join(HERE, os.pardir)
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
@login_manager.user_loader
|
||||
@@ -76,15 +80,6 @@ class MyJSONEncoder(DefaultJSONProvider):
|
||||
return o
|
||||
|
||||
|
||||
def create_acl_app(config_object="settings"):
|
||||
app = Flask(__name__.split(".")[0])
|
||||
app.config.from_object(config_object)
|
||||
|
||||
register_extensions(app)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def create_app(config_object="settings"):
|
||||
"""Create application factory, as explained here: http://flask.pocoo.org/docs/patterns/appfactories/.
|
||||
|
||||
@@ -125,7 +120,7 @@ def register_extensions(app):
|
||||
db.init_app(app)
|
||||
cors.init_app(app)
|
||||
login_manager.init_app(app)
|
||||
migrate.init_app(app, db)
|
||||
migrate.init_app(app, db, directory=f"{BASE_DIR}/migrations")
|
||||
rd.init_app(app)
|
||||
if app.config.get('USE_ES'):
|
||||
es.init_app(app)
|
||||
@@ -133,6 +128,10 @@ def register_extensions(app):
|
||||
app.config.update(app.config.get("CELERY"))
|
||||
celery.conf.update(app.config)
|
||||
|
||||
if app.config.get('SECRETS_ENGINE') == 'inner':
|
||||
with app.app_context():
|
||||
inner_secrets.init_app(app, InnerKVManger())
|
||||
|
||||
|
||||
def register_blueprints(app):
|
||||
for item in getmembers(api.views.entry):
|
||||
|
@@ -1,6 +1,8 @@
|
||||
import click
|
||||
from flask.cli import with_appcontext
|
||||
|
||||
from api.lib.perm.acl.user import UserCRUD
|
||||
|
||||
|
||||
@click.command()
|
||||
@with_appcontext
|
||||
@@ -23,50 +25,18 @@ def init_acl():
|
||||
role_rebuild.apply_async(args=(role.id, app.id), queue=ACL_QUEUE)
|
||||
|
||||
|
||||
# @click.command()
|
||||
# @with_appcontext
|
||||
# def acl_clean():
|
||||
# from api.models.acl import Resource
|
||||
# from api.models.acl import Permission
|
||||
# from api.models.acl import RolePermission
|
||||
#
|
||||
# perms = RolePermission.get_by(to_dict=False)
|
||||
#
|
||||
# for r in perms:
|
||||
# perm = Permission.get_by_id(r.perm_id)
|
||||
# if perm and perm.app_id != r.app_id:
|
||||
# resource_id = r.resource_id
|
||||
# resource = Resource.get_by_id(resource_id)
|
||||
# perm_name = perm.name
|
||||
# existed = Permission.get_by(resource_type_id=resource.resource_type_id, name=perm_name, first=True,
|
||||
# to_dict=False)
|
||||
# if existed is not None:
|
||||
# other = RolePermission.get_by(rid=r.rid, perm_id=existed.id, resource_id=resource_id)
|
||||
# if not other:
|
||||
# r.update(perm_id=existed.id)
|
||||
# else:
|
||||
# r.soft_delete()
|
||||
# else:
|
||||
# r.soft_delete()
|
||||
#
|
||||
#
|
||||
# @click.command()
|
||||
# @with_appcontext
|
||||
# def acl_has_resource_role():
|
||||
# from api.models.acl import Role
|
||||
# from api.models.acl import App
|
||||
# from api.lib.perm.acl.cache import HasResourceRoleCache
|
||||
# from api.lib.perm.acl.role import RoleCRUD
|
||||
#
|
||||
# roles = Role.get_by(to_dict=False)
|
||||
# apps = App.get_by(to_dict=False)
|
||||
# for role in roles:
|
||||
# if role.app_id:
|
||||
# res = RoleCRUD.recursive_resources(role.id, role.app_id)
|
||||
# if res.get('resources') or res.get('groups'):
|
||||
# HasResourceRoleCache.add(role.id, role.app_id)
|
||||
# else:
|
||||
# for app in apps:
|
||||
# res = RoleCRUD.recursive_resources(role.id, app.id)
|
||||
# if res.get('resources') or res.get('groups'):
|
||||
# HasResourceRoleCache.add(role.id, app.id)
|
||||
@click.command()
|
||||
@with_appcontext
|
||||
def add_user():
|
||||
"""
|
||||
create a user
|
||||
|
||||
is_admin: default is False
|
||||
|
||||
"""
|
||||
|
||||
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)
|
||||
|
||||
UserCRUD.add(username=username, password=password, email=email)
|
||||
|
@@ -7,6 +7,7 @@ import json
|
||||
import time
|
||||
|
||||
import click
|
||||
import requests
|
||||
from flask import current_app
|
||||
from flask.cli import with_appcontext
|
||||
from flask_login import login_user
|
||||
@@ -15,7 +16,6 @@ import api.lib.cmdb.ci
|
||||
from api.extensions import db
|
||||
from api.extensions import rd
|
||||
from api.lib.cmdb.cache import AttributeCache
|
||||
from api.lib.cmdb.ci_type import CITypeTriggerManager
|
||||
from api.lib.cmdb.const import PermEnum
|
||||
from api.lib.cmdb.const import REDIS_PREFIX_CI
|
||||
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION
|
||||
@@ -24,12 +24,14 @@ from api.lib.cmdb.const import RoleEnum
|
||||
from api.lib.cmdb.const import ValueTypeEnum
|
||||
from api.lib.exception import AbortException
|
||||
from api.lib.perm.acl.acl import ACLManager
|
||||
from api.lib.perm.acl.acl import UserCache
|
||||
from api.lib.perm.acl.cache import AppCache
|
||||
from api.lib.perm.acl.cache import UserCache
|
||||
from api.lib.perm.acl.resource import ResourceCRUD
|
||||
from api.lib.perm.acl.resource import ResourceTypeCRUD
|
||||
from api.lib.perm.acl.role import RoleCRUD
|
||||
from api.lib.perm.acl.user import UserCRUD
|
||||
from api.lib.secrets.inner import KeyManage
|
||||
from api.lib.secrets.inner import global_key_threshold
|
||||
from api.lib.secrets.secrets import InnerKVManger
|
||||
from api.models.acl import App
|
||||
from api.models.acl import ResourceType
|
||||
from api.models.cmdb import Attribute
|
||||
@@ -54,6 +56,7 @@ def cmdb_init_cache():
|
||||
if relations:
|
||||
rd.create_or_update(relations, REDIS_PREFIX_CI_RELATION)
|
||||
|
||||
es = None
|
||||
if current_app.config.get("USE_ES"):
|
||||
from api.extensions import es
|
||||
from api.models.cmdb import Attribute
|
||||
@@ -124,10 +127,10 @@ def cmdb_init_acl():
|
||||
|
||||
# 3. add resource and grant
|
||||
ci_types = CIType.get_by(to_dict=False)
|
||||
type_id = ResourceType.get_by(name=ResourceTypeEnum.CI, first=True, to_dict=False).id
|
||||
resource_type_id = ResourceType.get_by(name=ResourceTypeEnum.CI, first=True, to_dict=False).id
|
||||
for ci_type in ci_types:
|
||||
try:
|
||||
ResourceCRUD.add(ci_type.name, type_id, app_id)
|
||||
ResourceCRUD.add(ci_type.name, resource_type_id, app_id)
|
||||
except AbortException:
|
||||
pass
|
||||
|
||||
@@ -137,10 +140,10 @@ def cmdb_init_acl():
|
||||
[PermEnum.READ])
|
||||
|
||||
relation_views = PreferenceRelationView.get_by(to_dict=False)
|
||||
type_id = ResourceType.get_by(name=ResourceTypeEnum.RELATION_VIEW, first=True, to_dict=False).id
|
||||
resource_type_id = ResourceType.get_by(name=ResourceTypeEnum.RELATION_VIEW, first=True, to_dict=False).id
|
||||
for view in relation_views:
|
||||
try:
|
||||
ResourceCRUD.add(view.name, type_id, app_id)
|
||||
ResourceCRUD.add(view.name, resource_type_id, app_id)
|
||||
except AbortException:
|
||||
pass
|
||||
|
||||
@@ -150,57 +153,6 @@ def cmdb_init_acl():
|
||||
[PermEnum.READ])
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option(
|
||||
'-u',
|
||||
'--user',
|
||||
help='username'
|
||||
)
|
||||
@click.option(
|
||||
'-p',
|
||||
'--password',
|
||||
help='password'
|
||||
)
|
||||
@click.option(
|
||||
'-m',
|
||||
'--mail',
|
||||
help='mail'
|
||||
)
|
||||
@with_appcontext
|
||||
def add_user(user, password, mail):
|
||||
"""
|
||||
create a user
|
||||
|
||||
is_admin: default is False
|
||||
|
||||
Example: flask add-user -u <username> -p <password> -m <mail>
|
||||
"""
|
||||
assert user is not None
|
||||
assert password is not None
|
||||
assert mail is not None
|
||||
UserCRUD.add(username=user, password=password, email=mail)
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option(
|
||||
'-u',
|
||||
'--user',
|
||||
help='username'
|
||||
)
|
||||
@with_appcontext
|
||||
def del_user(user):
|
||||
"""
|
||||
delete a user
|
||||
|
||||
Example: flask del-user -u <username>
|
||||
"""
|
||||
assert user is not None
|
||||
from api.models.acl import User
|
||||
|
||||
u = User.get_by(username=user, first=True, to_dict=False)
|
||||
u and UserCRUD.delete(u.uid)
|
||||
|
||||
|
||||
@click.command()
|
||||
@with_appcontext
|
||||
def cmdb_counter():
|
||||
@@ -227,50 +179,60 @@ def cmdb_counter():
|
||||
@with_appcontext
|
||||
def cmdb_trigger():
|
||||
"""
|
||||
Trigger execution
|
||||
Trigger execution for date attribute
|
||||
"""
|
||||
from api.lib.cmdb.ci import CITriggerManager
|
||||
|
||||
current_day = datetime.datetime.today().strftime("%Y-%m-%d")
|
||||
trigger2cis = dict()
|
||||
trigger2completed = dict()
|
||||
|
||||
i = 0
|
||||
while True:
|
||||
db.session.remove()
|
||||
if datetime.datetime.today().strftime("%Y-%m-%d") != current_day:
|
||||
trigger2cis = dict()
|
||||
trigger2completed = dict()
|
||||
current_day = datetime.datetime.today().strftime("%Y-%m-%d")
|
||||
try:
|
||||
db.session.remove()
|
||||
|
||||
if i == 360 or i == 0:
|
||||
i = 0
|
||||
try:
|
||||
triggers = CITypeTrigger.get_by(to_dict=False)
|
||||
if datetime.datetime.today().strftime("%Y-%m-%d") != current_day:
|
||||
trigger2cis = dict()
|
||||
trigger2completed = dict()
|
||||
current_day = datetime.datetime.today().strftime("%Y-%m-%d")
|
||||
|
||||
if i == 3 or i == 0:
|
||||
i = 0
|
||||
triggers = CITypeTrigger.get_by(to_dict=False, __func_isnot__key_attr_id=None)
|
||||
for trigger in triggers:
|
||||
ready_cis = CITypeTriggerManager.waiting_cis(trigger)
|
||||
try:
|
||||
ready_cis = CITriggerManager.waiting_cis(trigger)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
continue
|
||||
|
||||
if trigger.id not in trigger2cis:
|
||||
trigger2cis[trigger.id] = (trigger, ready_cis)
|
||||
else:
|
||||
cur = trigger2cis[trigger.id]
|
||||
cur_ci_ids = {i.ci_id for i in cur[1]}
|
||||
trigger2cis[trigger.id] = (trigger, cur[1] + [i for i in ready_cis if i.ci_id not in cur_ci_ids
|
||||
and i.ci_id not in trigger2completed[trigger.id]])
|
||||
trigger2cis[trigger.id] = (
|
||||
trigger, cur[1] + [i for i in ready_cis if i.ci_id not in cur_ci_ids
|
||||
and i.ci_id not in trigger2completed.get(trigger.id, {})])
|
||||
|
||||
except Exception as e:
|
||||
print(e)
|
||||
for tid in trigger2cis:
|
||||
trigger, cis = trigger2cis[tid]
|
||||
for ci in copy.deepcopy(cis):
|
||||
if CITriggerManager.trigger_notify(trigger, ci):
|
||||
trigger2completed.setdefault(trigger.id, set()).add(ci.ci_id)
|
||||
|
||||
for tid in trigger2cis:
|
||||
trigger, cis = trigger2cis[tid]
|
||||
for ci in copy.deepcopy(cis):
|
||||
if CITypeTriggerManager.trigger_notify(trigger, ci):
|
||||
trigger2completed.setdefault(trigger.id, set()).add(ci.ci_id)
|
||||
for _ci in cis:
|
||||
if _ci.ci_id == ci.ci_id:
|
||||
cis.remove(_ci)
|
||||
|
||||
for _ci in cis:
|
||||
if _ci.ci_id == ci.ci_id:
|
||||
cis.remove(_ci)
|
||||
|
||||
i += 1
|
||||
time.sleep(10)
|
||||
i += 1
|
||||
time.sleep(10)
|
||||
except Exception as e:
|
||||
import traceback
|
||||
print(traceback.format_exc())
|
||||
current_app.logger.error("cmdb trigger exception: {}".format(e))
|
||||
time.sleep(60)
|
||||
|
||||
|
||||
@click.command()
|
||||
@@ -302,3 +264,197 @@ def cmdb_index_table_upgrade():
|
||||
CIIndexValueDateTime.create(ci_id=i.ci_id, attr_id=i.attr_id, value=i.value, commit=False)
|
||||
i.delete(commit=False)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
def valid_address(address):
|
||||
if not address:
|
||||
return False
|
||||
|
||||
if not address.startswith(("http://127.0.0.1", "https://127.0.0.1")):
|
||||
response = {
|
||||
"message": "Address should start with http://127.0.0.1 or https://127.0.0.1",
|
||||
"status": "failed"
|
||||
}
|
||||
KeyManage.print_response(response)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option(
|
||||
'-a',
|
||||
'--address',
|
||||
help='inner cmdb api, http://127.0.0.1:8000',
|
||||
)
|
||||
@with_appcontext
|
||||
def cmdb_inner_secrets_init(address):
|
||||
"""
|
||||
init inner secrets for password feature
|
||||
"""
|
||||
res, ok = KeyManage(backend=InnerKVManger).init()
|
||||
if not ok:
|
||||
if res.get("status") == "failed":
|
||||
KeyManage.print_response(res)
|
||||
return
|
||||
|
||||
token = res.get("details", {}).get("root_token", "")
|
||||
if valid_address(address):
|
||||
token = current_app.config.get("INNER_TRIGGER_TOKEN", "") if not token else token
|
||||
if not token:
|
||||
token = click.prompt(f'Enter root token', hide_input=True, confirmation_prompt=False)
|
||||
assert token is not None
|
||||
resp = requests.post("{}/api/v0.1/secrets/auto_seal".format(address.strip("/")),
|
||||
headers={"Inner-Token": token})
|
||||
if resp.status_code == 200:
|
||||
KeyManage.print_response(resp.json())
|
||||
else:
|
||||
KeyManage.print_response({"message": resp.text or resp.status_code, "status": "failed"})
|
||||
else:
|
||||
KeyManage.print_response(res)
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option(
|
||||
'-a',
|
||||
'--address',
|
||||
help='inner cmdb api, http://127.0.0.1:8000',
|
||||
required=True,
|
||||
)
|
||||
@with_appcontext
|
||||
def cmdb_inner_secrets_unseal(address):
|
||||
"""
|
||||
unseal the secrets feature
|
||||
"""
|
||||
if not valid_address(address):
|
||||
return
|
||||
address = "{}/api/v0.1/secrets/unseal".format(address.strip("/"))
|
||||
for i in range(global_key_threshold):
|
||||
token = click.prompt(f'Enter unseal token {i + 1}', hide_input=True, confirmation_prompt=False)
|
||||
assert token is not None
|
||||
resp = requests.post(address, headers={"Unseal-Token": token})
|
||||
if resp.status_code == 200:
|
||||
KeyManage.print_response(resp.json())
|
||||
if resp.json().get("status") in ["success", "skip"]:
|
||||
return
|
||||
else:
|
||||
KeyManage.print_response({"message": resp.status_code, "status": "failed"})
|
||||
return
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option(
|
||||
'-a',
|
||||
'--address',
|
||||
help='inner cmdb api, http://127.0.0.1:8000',
|
||||
required=True,
|
||||
)
|
||||
@click.option(
|
||||
'-k',
|
||||
'--token',
|
||||
help='root token',
|
||||
prompt=True,
|
||||
hide_input=True,
|
||||
)
|
||||
@with_appcontext
|
||||
def cmdb_inner_secrets_seal(address, token):
|
||||
"""
|
||||
seal the secrets feature
|
||||
"""
|
||||
assert address is not None
|
||||
assert token is not None
|
||||
if not valid_address(address):
|
||||
return
|
||||
address = "{}/api/v0.1/secrets/seal".format(address.strip("/"))
|
||||
resp = requests.post(address, headers={
|
||||
"Inner-Token": token,
|
||||
})
|
||||
if resp.status_code == 200:
|
||||
KeyManage.print_response(resp.json())
|
||||
else:
|
||||
KeyManage.print_response({"message": resp.status_code, "status": "failed"})
|
||||
|
||||
|
||||
@click.command()
|
||||
@with_appcontext
|
||||
def cmdb_password_data_migrate():
|
||||
"""
|
||||
Migrate CI password data, version >= v2.3.6
|
||||
"""
|
||||
from api.models.cmdb import CIIndexValueText
|
||||
from api.models.cmdb import CIValueText
|
||||
from api.lib.secrets.inner import InnerCrypt
|
||||
from api.lib.secrets.vault import VaultClient
|
||||
|
||||
attrs = Attribute.get_by(to_dict=False)
|
||||
for attr in attrs:
|
||||
if attr.is_password:
|
||||
|
||||
value_table = CIIndexValueText if attr.is_index else CIValueText
|
||||
|
||||
failed = False
|
||||
for i in value_table.get_by(attr_id=attr.id, to_dict=False):
|
||||
if current_app.config.get("SECRETS_ENGINE", 'inner') == 'inner':
|
||||
_, status = InnerCrypt().decrypt(i.value)
|
||||
if status:
|
||||
continue
|
||||
|
||||
encrypt_value, status = InnerCrypt().encrypt(i.value)
|
||||
if status:
|
||||
CIValueText.create(ci_id=i.ci_id, attr_id=attr.id, value=encrypt_value)
|
||||
else:
|
||||
failed = True
|
||||
continue
|
||||
elif current_app.config.get("SECRETS_ENGINE") == 'vault':
|
||||
if i.value == '******':
|
||||
continue
|
||||
|
||||
vault = VaultClient(current_app.config.get('VAULT_URL'), current_app.config.get('VAULT_TOKEN'))
|
||||
try:
|
||||
vault.update("/{}/{}".format(i.ci_id, i.attr_id), dict(v=i.value))
|
||||
except Exception as e:
|
||||
print('save password to vault failed: {}'.format(e))
|
||||
failed = True
|
||||
continue
|
||||
else:
|
||||
continue
|
||||
|
||||
i.delete()
|
||||
|
||||
if not failed and attr.is_index:
|
||||
attr.update(is_index=False)
|
||||
|
||||
|
||||
@click.command()
|
||||
@with_appcontext
|
||||
def cmdb_agent_init():
|
||||
"""
|
||||
Initialize the agent's permissions and obtain the key and secret
|
||||
"""
|
||||
|
||||
from api.models.acl import User
|
||||
|
||||
user = User.get_by(username="cmdb_agent", first=True, to_dict=False)
|
||||
if user is None:
|
||||
click.echo(
|
||||
click.style('user cmdb_agent does not exist, please use flask add-user to create it first', fg='red'))
|
||||
return
|
||||
|
||||
# grant
|
||||
_app = AppCache.get('cmdb') or App.create(name='cmdb')
|
||||
app_id = _app.id
|
||||
|
||||
ci_types = CIType.get_by(to_dict=False)
|
||||
resource_type_id = ResourceType.get_by(name=ResourceTypeEnum.CI, first=True, to_dict=False).id
|
||||
for ci_type in ci_types:
|
||||
try:
|
||||
ResourceCRUD.add(ci_type.name, resource_type_id, app_id)
|
||||
except AbortException:
|
||||
pass
|
||||
|
||||
ACLManager().grant_resource_to_role(ci_type.name,
|
||||
"cmdb_agent",
|
||||
ResourceTypeEnum.CI,
|
||||
[PermEnum.READ, PermEnum.UPDATE, PermEnum.ADD, PermEnum.DELETE])
|
||||
|
||||
click.echo("Key : {}".format(click.style(user.key, bg='red')))
|
||||
click.echo("Secret: {}".format(click.style(user.secret, bg='red')))
|
||||
|
@@ -10,9 +10,6 @@ from api.models.common_setting import Employee, Department
|
||||
|
||||
|
||||
class InitEmployee(object):
|
||||
"""
|
||||
初始化员工
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.log = current_app.logger
|
||||
@@ -58,7 +55,8 @@ class InitEmployee(object):
|
||||
self.log.error(ErrFormat.acl_import_user_failed.format(user['username'], str(e)))
|
||||
self.log.error(e)
|
||||
|
||||
def get_rid_by_uid(self, uid):
|
||||
@staticmethod
|
||||
def get_rid_by_uid(uid):
|
||||
from api.models.acl import Role
|
||||
role = Role.get_by(first=True, uid=uid)
|
||||
return role['id'] if role is not None else 0
|
||||
@@ -71,7 +69,8 @@ class InitDepartment(object):
|
||||
def init(self):
|
||||
self.init_wide_company()
|
||||
|
||||
def hard_delete(self, department_id, department_name):
|
||||
@staticmethod
|
||||
def hard_delete(department_id, department_name):
|
||||
existed_deleted_list = Department.query.filter(
|
||||
Department.department_name == department_name,
|
||||
Department.department_id == department_id,
|
||||
@@ -80,11 +79,12 @@ class InitDepartment(object):
|
||||
for existed in existed_deleted_list:
|
||||
existed.delete()
|
||||
|
||||
def get_department(self, department_name):
|
||||
@staticmethod
|
||||
def get_department(department_name):
|
||||
return Department.query.filter(
|
||||
Department.department_name == department_name,
|
||||
Department.deleted == 0,
|
||||
).order_by(Department.created_at.asc()).first()
|
||||
).first()
|
||||
|
||||
def run(self, department_id, department_name, department_parent_id):
|
||||
self.hard_delete(department_id, department_name)
|
||||
@@ -94,7 +94,7 @@ class InitDepartment(object):
|
||||
if res.department_id == department_id:
|
||||
return
|
||||
else:
|
||||
new_d = res.update(
|
||||
res.update(
|
||||
department_id=department_id,
|
||||
department_parent_id=department_parent_id,
|
||||
)
|
||||
@@ -108,11 +108,11 @@ class InitDepartment(object):
|
||||
new_d = self.get_department(department_name)
|
||||
|
||||
if new_d.department_id != department_id:
|
||||
new_d = new_d.update(
|
||||
new_d.update(
|
||||
department_id=department_id,
|
||||
department_parent_id=department_parent_id,
|
||||
)
|
||||
self.log.info(f"初始化 {department_name} 部门成功.")
|
||||
self.log.info(f"init {department_name} success.")
|
||||
|
||||
def run_common(self, department_id, department_name, department_parent_id):
|
||||
try:
|
||||
@@ -123,19 +123,14 @@ class InitDepartment(object):
|
||||
raise Exception(e)
|
||||
|
||||
def init_wide_company(self):
|
||||
"""
|
||||
创建 id 0, name 全公司 的部门
|
||||
"""
|
||||
department_id = 0
|
||||
department_name = '全公司'
|
||||
department_parent_id = -1
|
||||
|
||||
self.run_common(department_id, department_name, department_parent_id)
|
||||
|
||||
def create_acl_role_with_department(self):
|
||||
"""
|
||||
当前所有部门,在ACL创建 role
|
||||
"""
|
||||
@staticmethod
|
||||
def create_acl_role_with_department():
|
||||
acl = ACLManager('acl')
|
||||
role_name_map = {role['name']: role for role in acl.get_all_roles()}
|
||||
|
||||
@@ -146,7 +141,7 @@ class InitDepartment(object):
|
||||
continue
|
||||
|
||||
role = role_name_map.get(department.department_name)
|
||||
if role is None:
|
||||
if not role:
|
||||
payload = {
|
||||
'app_id': 'acl',
|
||||
'name': department.department_name,
|
||||
@@ -165,50 +160,65 @@ class InitDepartment(object):
|
||||
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=['read', 'grant', 'delete', 'update']
|
||||
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)
|
||||
|
||||
for name in ['公司信息']:
|
||||
payload = dict(
|
||||
type_id=resource_type['id'],
|
||||
app_id=acl.app_name,
|
||||
name=name,
|
||||
)
|
||||
try:
|
||||
acl.create_resource(payload)
|
||||
except Exception as e:
|
||||
if '已经存在' in str(e):
|
||||
pass
|
||||
else:
|
||||
raise Exception(e)
|
||||
resource_list = acl.get_resource_by_type(None, None, resource_type['id'])
|
||||
|
||||
def check_app(self, app_name):
|
||||
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)
|
||||
|
||||
@staticmethod
|
||||
def check_app(app_name):
|
||||
acl = ACLManager(app_name)
|
||||
payload = dict(
|
||||
name=app_name,
|
||||
description=app_name
|
||||
)
|
||||
try:
|
||||
app = acl.validate_app()
|
||||
if app:
|
||||
return acl
|
||||
|
||||
app = acl.validate_app()
|
||||
if not app:
|
||||
acl.create_app(payload)
|
||||
except Exception as e:
|
||||
current_app.logger.error(e)
|
||||
if '不存在' in str(e):
|
||||
acl.create_app(payload)
|
||||
return acl
|
||||
raise Exception(e)
|
||||
return acl
|
||||
|
||||
@staticmethod
|
||||
def get_admin_user_rid():
|
||||
admin = Employee.get_by(first=True, username='admin', to_dict=False)
|
||||
return admin.acl_rid if admin else 0
|
||||
|
||||
|
||||
@click.command()
|
||||
@@ -230,3 +240,62 @@ def init_department():
|
||||
cli.init_wide_company()
|
||||
cli.create_acl_role_with_department()
|
||||
cli.init_backend_resource()
|
||||
|
||||
|
||||
@click.command()
|
||||
@with_appcontext
|
||||
def common_check_new_columns():
|
||||
"""
|
||||
add new columns to tables
|
||||
"""
|
||||
from api.extensions import db
|
||||
from sqlalchemy import inspect, text
|
||||
|
||||
def get_model_by_table_name(_table_name):
|
||||
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)
|
||||
|
@@ -84,66 +84,6 @@ def clean():
|
||||
os.remove(full_pathname)
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option("--url", default=None, help="Url to test (ex. /static/image.png)")
|
||||
@click.option(
|
||||
"--order", default="rule", help="Property on Rule to order by (default: rule)"
|
||||
)
|
||||
@with_appcontext
|
||||
def urls(url, order):
|
||||
"""Display all of the url matching routes for the project.
|
||||
|
||||
Borrowed from Flask-Script, converted to use Click.
|
||||
"""
|
||||
rows = []
|
||||
column_headers = ("Rule", "Endpoint", "Arguments")
|
||||
|
||||
if url:
|
||||
try:
|
||||
rule, arguments = current_app.url_map.bind("localhost").match(
|
||||
url, return_rule=True
|
||||
)
|
||||
rows.append((rule.rule, rule.endpoint, arguments))
|
||||
column_length = 3
|
||||
except (NotFound, MethodNotAllowed) as e:
|
||||
rows.append(("<{}>".format(e), None, None))
|
||||
column_length = 1
|
||||
else:
|
||||
rules = sorted(
|
||||
current_app.url_map.iter_rules(), key=lambda rule: getattr(rule, order)
|
||||
)
|
||||
for rule in rules:
|
||||
rows.append((rule.rule, rule.endpoint, None))
|
||||
column_length = 2
|
||||
|
||||
str_template = ""
|
||||
table_width = 0
|
||||
|
||||
if column_length >= 1:
|
||||
max_rule_length = max(len(r[0]) for r in rows)
|
||||
max_rule_length = max_rule_length if max_rule_length > 4 else 4
|
||||
str_template += "{:" + str(max_rule_length) + "}"
|
||||
table_width += max_rule_length
|
||||
|
||||
if column_length >= 2:
|
||||
max_endpoint_length = max(len(str(r[1])) for r in rows)
|
||||
max_endpoint_length = max_endpoint_length if max_endpoint_length > 8 else 8
|
||||
str_template += " {:" + str(max_endpoint_length) + "}"
|
||||
table_width += 2 + max_endpoint_length
|
||||
|
||||
if column_length >= 3:
|
||||
max_arguments_length = max(len(str(r[2])) for r in rows)
|
||||
max_arguments_length = max_arguments_length if max_arguments_length > 9 else 9
|
||||
str_template += " {:" + str(max_arguments_length) + "}"
|
||||
table_width += 2 + max_arguments_length
|
||||
|
||||
click.echo(str_template.format(*column_headers[:column_length]))
|
||||
click.echo("-" * table_width)
|
||||
|
||||
for row in rows:
|
||||
click.echo(str_template.format(*row[:column_length]))
|
||||
|
||||
|
||||
@click.command()
|
||||
@with_appcontext
|
||||
def db_setup():
|
||||
|
@@ -9,6 +9,7 @@ from flask_login import LoginManager
|
||||
from flask_migrate import Migrate
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
|
||||
from api.lib.secrets.inner import KeyManage
|
||||
from api.lib.utils import ESHandler
|
||||
from api.lib.utils import RedisHandler
|
||||
|
||||
@@ -21,3 +22,4 @@ celery = Celery()
|
||||
cors = CORS(supports_credentials=True)
|
||||
rd = RedisHandler()
|
||||
es = ESHandler()
|
||||
inner_secrets = KeyManage()
|
||||
|
@@ -1,6 +1,5 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
import requests
|
||||
from flask import abort
|
||||
from flask import current_app
|
||||
from flask import session
|
||||
@@ -23,6 +22,7 @@ from api.lib.cmdb.utils import ValueTypeMap
|
||||
from api.lib.decorator import kwargs_required
|
||||
from api.lib.perm.acl.acl import is_app_admin
|
||||
from api.lib.perm.acl.acl import validate_permission
|
||||
from api.lib.webhook import webhook_request
|
||||
from api.models.cmdb import Attribute
|
||||
from api.models.cmdb import CIType
|
||||
from api.models.cmdb import CITypeAttribute
|
||||
@@ -40,15 +40,11 @@ class AttributeManager(object):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def _get_choice_values_from_web_hook(choice_web_hook):
|
||||
url = choice_web_hook.get('url')
|
||||
ret_key = choice_web_hook.get('ret_key')
|
||||
headers = choice_web_hook.get('headers') or {}
|
||||
payload = choice_web_hook.get('payload') or {}
|
||||
method = (choice_web_hook.get('method') or 'GET').lower()
|
||||
def _get_choice_values_from_webhook(choice_webhook, payload=None):
|
||||
ret_key = choice_webhook.get('ret_key')
|
||||
|
||||
try:
|
||||
res = getattr(requests, method)(url, headers=headers, data=payload).json()
|
||||
res = webhook_request(choice_webhook, payload or {}).json()
|
||||
if ret_key:
|
||||
ret_key_list = ret_key.strip().split("##")
|
||||
for key in ret_key_list[:-1]:
|
||||
@@ -63,19 +59,57 @@ class AttributeManager(object):
|
||||
current_app.logger.error("get choice values failed: {}".format(e))
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def _get_choice_values_from_other(choice_other):
|
||||
from api.lib.cmdb.search import SearchError
|
||||
from api.lib.cmdb.search.ci import search
|
||||
|
||||
if choice_other.get('type_ids'):
|
||||
type_ids = choice_other.get('type_ids')
|
||||
attr_id = choice_other.get('attr_id')
|
||||
other_filter = choice_other.get('filter') or ''
|
||||
|
||||
query = "_type:({}),{}".format(";".join(map(str, type_ids)), other_filter)
|
||||
s = search(query, fl=[str(attr_id)], facet=[str(attr_id)], count=1)
|
||||
try:
|
||||
_, _, _, _, _, facet = s.search()
|
||||
return [[i[0], {}] for i in (list(facet.values()) or [[]])[0]]
|
||||
except SearchError as e:
|
||||
current_app.logger.error("get choice values from other ci failed: {}".format(e))
|
||||
return []
|
||||
|
||||
elif choice_other.get('script'):
|
||||
try:
|
||||
x = compile(choice_other['script'], '', "exec")
|
||||
local_ns = {}
|
||||
exec(x, {}, local_ns)
|
||||
res = local_ns['ChoiceValue']().values() or []
|
||||
return [[i, {}] for i in res]
|
||||
except Exception as e:
|
||||
current_app.logger.error("get choice values from script: {}".format(e))
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def get_choice_values(cls, attr_id, value_type, choice_web_hook, choice_web_hook_parse=True):
|
||||
def get_choice_values(cls, attr_id, value_type, choice_web_hook, choice_other,
|
||||
choice_web_hook_parse=True, choice_other_parse=True):
|
||||
if choice_web_hook:
|
||||
if choice_web_hook_parse:
|
||||
if isinstance(choice_web_hook, dict):
|
||||
return cls._get_choice_values_from_web_hook(choice_web_hook)
|
||||
if choice_web_hook_parse and isinstance(choice_web_hook, dict):
|
||||
return cls._get_choice_values_from_webhook(choice_web_hook)
|
||||
else:
|
||||
return []
|
||||
elif choice_other:
|
||||
if choice_other_parse and isinstance(choice_other, dict):
|
||||
return cls._get_choice_values_from_other(choice_other)
|
||||
else:
|
||||
return []
|
||||
|
||||
choice_table = ValueTypeMap.choice.get(value_type)
|
||||
if not choice_table:
|
||||
return []
|
||||
choice_values = choice_table.get_by(fl=["value", "option"], attr_id=attr_id)
|
||||
|
||||
return [[choice_value['value'], choice_value['option']] for choice_value in choice_values]
|
||||
return [[ValueTypeMap.serialize[value_type](choice_value['value']), choice_value['option']]
|
||||
for choice_value in choice_values]
|
||||
|
||||
@staticmethod
|
||||
def add_choice_values(_id, value_type, choice_values):
|
||||
@@ -122,7 +156,8 @@ class AttributeManager(object):
|
||||
res = list()
|
||||
for attr in attrs:
|
||||
attr["is_choice"] and attr.update(
|
||||
dict(choice_value=cls.get_choice_values(attr["id"], attr["value_type"], attr["choice_web_hook"])))
|
||||
dict(choice_value=cls.get_choice_values(attr["id"], attr["value_type"],
|
||||
attr["choice_web_hook"], attr.get("choice_other"))))
|
||||
attr['is_choice'] and attr.pop('choice_web_hook', None)
|
||||
|
||||
res.append(attr)
|
||||
@@ -132,29 +167,38 @@ class AttributeManager(object):
|
||||
def get_attribute_by_name(self, name):
|
||||
attr = Attribute.get_by(name=name, first=True)
|
||||
if attr.get("is_choice"):
|
||||
attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"], attr["choice_web_hook"])
|
||||
attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"],
|
||||
attr["choice_web_hook"], attr.get("choice_other"))
|
||||
|
||||
return attr
|
||||
|
||||
def get_attribute_by_alias(self, alias):
|
||||
attr = Attribute.get_by(alias=alias, first=True)
|
||||
if attr.get("is_choice"):
|
||||
attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"], attr["choice_web_hook"])
|
||||
attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"],
|
||||
attr["choice_web_hook"], attr.get("choice_other"))
|
||||
|
||||
return attr
|
||||
|
||||
def get_attribute_by_id(self, _id):
|
||||
attr = Attribute.get_by_id(_id).to_dict()
|
||||
if attr.get("is_choice"):
|
||||
attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"], attr["choice_web_hook"])
|
||||
attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"],
|
||||
attr["choice_web_hook"], attr.get("choice_other"))
|
||||
|
||||
return attr
|
||||
|
||||
def get_attribute(self, key, choice_web_hook_parse=True):
|
||||
def get_attribute(self, key, choice_web_hook_parse=True, choice_other_parse=True):
|
||||
attr = AttributeCache.get(key).to_dict()
|
||||
if attr.get("is_choice"):
|
||||
attr["choice_value"] = self.get_choice_values(
|
||||
attr["id"], attr["value_type"], attr["choice_web_hook"], choice_web_hook_parse=choice_web_hook_parse)
|
||||
attr["id"],
|
||||
attr["value_type"],
|
||||
attr["choice_web_hook"],
|
||||
attr.get("choice_other"),
|
||||
choice_web_hook_parse=choice_web_hook_parse,
|
||||
choice_other_parse=choice_other_parse,
|
||||
)
|
||||
|
||||
return attr
|
||||
|
||||
@@ -181,12 +225,22 @@ class AttributeManager(object):
|
||||
def add(cls, **kwargs):
|
||||
choice_value = kwargs.pop("choice_value", [])
|
||||
kwargs.pop("is_choice", None)
|
||||
is_choice = True if choice_value or kwargs.get('choice_web_hook') else False
|
||||
is_choice = True if choice_value or kwargs.get('choice_web_hook') or kwargs.get('choice_other') else False
|
||||
|
||||
name = kwargs.pop("name")
|
||||
if name in BUILTIN_KEYWORDS:
|
||||
return abort(400, ErrFormat.attribute_name_cannot_be_builtin)
|
||||
|
||||
while kwargs.get('choice_other'):
|
||||
if isinstance(kwargs['choice_other'], dict):
|
||||
if kwargs['choice_other'].get('script'):
|
||||
break
|
||||
|
||||
if kwargs['choice_other'].get('type_ids') and kwargs['choice_other'].get('attr_id'):
|
||||
break
|
||||
|
||||
return abort(400, ErrFormat.attribute_choice_other_invalid)
|
||||
|
||||
alias = kwargs.pop("alias", "")
|
||||
alias = name if not alias else alias
|
||||
Attribute.get_by(name=name, first=True) and abort(400, ErrFormat.attribute_name_duplicate.format(name))
|
||||
@@ -196,6 +250,8 @@ class AttributeManager(object):
|
||||
|
||||
kwargs.get('is_computed') and cls.can_create_computed_attribute()
|
||||
|
||||
kwargs.get('choice_other') and kwargs['choice_other'].get('script') and cls.can_create_computed_attribute()
|
||||
|
||||
attr = Attribute.create(flush=True,
|
||||
name=name,
|
||||
alias=alias,
|
||||
@@ -281,9 +337,6 @@ class AttributeManager(object):
|
||||
def update(self, _id, **kwargs):
|
||||
attr = Attribute.get_by_id(_id) or abort(404, ErrFormat.attribute_not_found.format("id={}".format(_id)))
|
||||
|
||||
if not self._can_edit_attribute(attr):
|
||||
return abort(403, ErrFormat.cannot_edit_attribute)
|
||||
|
||||
if kwargs.get("name"):
|
||||
other = Attribute.get_by(name=kwargs['name'], first=True, to_dict=False)
|
||||
if other and other.id != attr.id:
|
||||
@@ -301,12 +354,22 @@ class AttributeManager(object):
|
||||
|
||||
self._change_index(attr, attr.is_index, kwargs['is_index'])
|
||||
|
||||
while kwargs.get('choice_other'):
|
||||
if isinstance(kwargs['choice_other'], dict):
|
||||
if kwargs['choice_other'].get('script'):
|
||||
break
|
||||
|
||||
if kwargs['choice_other'].get('type_ids') and kwargs['choice_other'].get('attr_id'):
|
||||
break
|
||||
|
||||
return abort(400, ErrFormat.attribute_choice_other_invalid)
|
||||
|
||||
existed2 = attr.to_dict()
|
||||
if not existed2['choice_web_hook'] and existed2['is_choice']:
|
||||
existed2['choice_value'] = self.get_choice_values(attr.id, attr.value_type, attr.choice_web_hook)
|
||||
if not existed2['choice_web_hook'] and not existed2.get('choice_other') and existed2['is_choice']:
|
||||
existed2['choice_value'] = self.get_choice_values(attr.id, attr.value_type, None, None)
|
||||
|
||||
choice_value = kwargs.pop("choice_value", False)
|
||||
is_choice = True if choice_value or kwargs.get('choice_web_hook') else False
|
||||
is_choice = True if choice_value or kwargs.get('choice_web_hook') or kwargs.get('choice_other') else False
|
||||
kwargs['is_choice'] = is_choice
|
||||
|
||||
if kwargs.get('default') and not (isinstance(kwargs['default'], dict) and 'default' in kwargs['default']):
|
||||
@@ -314,6 +377,14 @@ class AttributeManager(object):
|
||||
|
||||
kwargs.get('is_computed') and self.can_create_computed_attribute()
|
||||
|
||||
is_changed = False
|
||||
for k in kwargs:
|
||||
if kwargs[k] != getattr(attr, k, None):
|
||||
is_changed = True
|
||||
|
||||
if is_changed and not self._can_edit_attribute(attr):
|
||||
return abort(403, ErrFormat.cannot_edit_attribute)
|
||||
|
||||
attr.update(flush=True, filter_none=False, **kwargs)
|
||||
|
||||
if is_choice and choice_value:
|
||||
|
@@ -36,9 +36,10 @@ def parse_plugin_script(script):
|
||||
attributes = []
|
||||
try:
|
||||
x = compile(script, '', "exec")
|
||||
exec(x)
|
||||
unique_key = locals()['AutoDiscovery']().unique_key
|
||||
attrs = locals()['AutoDiscovery']().attributes() or []
|
||||
local_ns = {}
|
||||
exec(x, {}, local_ns)
|
||||
unique_key = local_ns['AutoDiscovery']().unique_key
|
||||
attrs = local_ns['AutoDiscovery']().attributes() or []
|
||||
except Exception as e:
|
||||
return abort(400, str(e))
|
||||
|
||||
|
@@ -335,14 +335,20 @@ class CMDBCounterCache(object):
|
||||
def attribute_counter(custom):
|
||||
from api.lib.cmdb.search import SearchError
|
||||
from api.lib.cmdb.search.ci import search
|
||||
from api.lib.cmdb.utils import ValueTypeMap
|
||||
|
||||
custom.setdefault('options', {})
|
||||
type_id = custom.get('type_id')
|
||||
attr_id = custom.get('attr_id')
|
||||
type_ids = custom['options'].get('type_ids') or (type_id and [type_id])
|
||||
attr_ids = list(map(str, custom['options'].get('attr_ids') or (attr_id and [attr_id])))
|
||||
try:
|
||||
attr2value_type = [AttributeCache.get(i).value_type for i in attr_ids]
|
||||
except AttributeError:
|
||||
return
|
||||
|
||||
other_filter = custom['options'].get('filter')
|
||||
other_filter = "({})".format(other_filter) if other_filter else ''
|
||||
other_filter = "{}".format(other_filter) if other_filter else ''
|
||||
|
||||
if custom['options'].get('ret') == 'cis':
|
||||
query = "_type:({}),{}".format(";".join(map(str, type_ids)), other_filter)
|
||||
@@ -365,7 +371,7 @@ class CMDBCounterCache(object):
|
||||
current_app.logger.error(e)
|
||||
return
|
||||
for i in (list(facet.values()) or [[]])[0]:
|
||||
result[i[0]] = i[1]
|
||||
result[ValueTypeMap.serialize2[attr2value_type[0]](str(i[0]))] = i[1]
|
||||
if len(attr_ids) == 1:
|
||||
return result
|
||||
|
||||
@@ -380,7 +386,7 @@ class CMDBCounterCache(object):
|
||||
return
|
||||
result[v] = dict()
|
||||
for i in (list(facet.values()) or [[]])[0]:
|
||||
result[v][i[0]] = i[1]
|
||||
result[v][ValueTypeMap.serialize2[attr2value_type[1]](str(i[0]))] = i[1]
|
||||
|
||||
if len(attr_ids) == 2:
|
||||
return result
|
||||
@@ -400,7 +406,7 @@ class CMDBCounterCache(object):
|
||||
return
|
||||
result[v1][v2] = dict()
|
||||
for i in (list(facet.values()) or [[]])[0]:
|
||||
result[v1][v2][i[0]] = i[1]
|
||||
result[v1][v2][ValueTypeMap.serialize2[attr2value_type[2]](str(i[0]))] = i[1]
|
||||
|
||||
return result
|
||||
|
||||
|
@@ -4,6 +4,7 @@
|
||||
import copy
|
||||
import datetime
|
||||
import json
|
||||
import threading
|
||||
|
||||
from flask import abort
|
||||
from flask import current_app
|
||||
@@ -24,34 +25,45 @@ from api.lib.cmdb.const import CMDB_QUEUE
|
||||
from api.lib.cmdb.const import ConstraintEnum
|
||||
from api.lib.cmdb.const import ExistPolicy
|
||||
from api.lib.cmdb.const import OperateType
|
||||
from api.lib.cmdb.const import PermEnum, ResourceTypeEnum
|
||||
from api.lib.cmdb.const import PermEnum
|
||||
from api.lib.cmdb.const import REDIS_PREFIX_CI
|
||||
from api.lib.cmdb.const import ResourceTypeEnum
|
||||
from api.lib.cmdb.const import RetKey
|
||||
from api.lib.cmdb.const import ValueTypeEnum
|
||||
from api.lib.cmdb.history import AttributeHistoryManger
|
||||
from api.lib.cmdb.history import CIRelationHistoryManager
|
||||
from api.lib.cmdb.history import CITriggerHistoryManager
|
||||
from api.lib.cmdb.perms import CIFilterPermsCRUD
|
||||
from api.lib.cmdb.resp_format import ErrFormat
|
||||
from api.lib.cmdb.utils import TableMap
|
||||
from api.lib.cmdb.utils import ValueTypeMap
|
||||
from api.lib.cmdb.value import AttributeValueManager
|
||||
from api.lib.decorator import kwargs_required
|
||||
from api.lib.notify import notify_send
|
||||
from api.lib.perm.acl.acl import ACLManager
|
||||
from api.lib.perm.acl.acl import is_app_admin
|
||||
from api.lib.perm.acl.acl import validate_permission
|
||||
from api.lib.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
|
||||
from api.tasks.cmdb import ci_delete
|
||||
from api.tasks.cmdb import ci_delete_trigger
|
||||
from api.tasks.cmdb import ci_relation_add
|
||||
from api.tasks.cmdb import ci_relation_cache
|
||||
from api.tasks.cmdb import ci_relation_delete
|
||||
|
||||
PRIVILEGED_USERS = {"worker", "cmdb_agent", "agent"}
|
||||
PASSWORD_DEFAULT_SHOW = "******"
|
||||
|
||||
|
||||
class CIManager(object):
|
||||
@@ -315,6 +327,8 @@ class CIManager(object):
|
||||
ci_attr2type_attr = {type_attr.attr_id: type_attr for type_attr, _ in attrs}
|
||||
|
||||
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):
|
||||
existed = cls.ci_is_exist(unique_key, unique_value, ci_type.id)
|
||||
@@ -343,14 +357,23 @@ class CIManager(object):
|
||||
ci_dict.get(attr.name) is None and ci_dict.get(attr.alias) is None)):
|
||||
ci_dict[attr.name] = attr.default.get('default')
|
||||
|
||||
if type_attr.is_required and (attr.name not in ci_dict and attr.alias not in ci_dict):
|
||||
if (type_attr.is_required and not attr.is_computed and
|
||||
(attr.name not in ci_dict and attr.alias not in ci_dict)):
|
||||
return abort(400, ErrFormat.attribute_value_required.format(attr.name))
|
||||
else:
|
||||
for type_attr, attr in attrs:
|
||||
if attr.default and attr.default.get('default') == AttributeDefaultValueEnum.UPDATED_AT:
|
||||
ci_dict[attr.name] = now
|
||||
|
||||
computed_attrs = [attr.to_dict() for _, attr in attrs if attr.is_computed] or None
|
||||
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)
|
||||
elif attr.alias in ci_dict:
|
||||
password_dict[attr.id] = ci_dict.pop(attr.alias)
|
||||
|
||||
value_manager = AttributeValueManager()
|
||||
|
||||
@@ -378,16 +401,21 @@ class CIManager(object):
|
||||
key2attr = value_manager.valid_attr_value(ci_dict, ci_type.id, ci and ci.id,
|
||||
ci_type_attrs_name, ci_type_attrs_alias, ci_attr2type_attr)
|
||||
|
||||
operate_type = OperateType.UPDATE if ci is not None else OperateType.ADD
|
||||
try:
|
||||
ci = ci or CI.create(type_id=ci_type.id, is_auto_discovery=is_auto_discovery)
|
||||
record_id = value_manager.create_or_update_attr_value2(ci, ci_dict, key2attr)
|
||||
record_id = value_manager.create_or_update_attr_value(ci, ci_dict, key2attr)
|
||||
except BadRequest as e:
|
||||
if existed is None:
|
||||
cls.delete(ci.id)
|
||||
raise e
|
||||
|
||||
if password_dict:
|
||||
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
|
||||
ci_cache.apply_async([ci.id], queue=CMDB_QUEUE)
|
||||
ci_cache.apply_async(args=(ci.id, operate_type, record_id), queue=CMDB_QUEUE)
|
||||
|
||||
if ref_ci_dict: # add relations
|
||||
ci_relation_add.apply_async(args=(ref_ci_dict, ci.id, current_user.uid), queue=CMDB_QUEUE)
|
||||
@@ -405,7 +433,16 @@ class CIManager(object):
|
||||
if attr.default and attr.default.get('default') == AttributeDefaultValueEnum.UPDATED_AT:
|
||||
ci_dict[attr.name] = now
|
||||
|
||||
computed_attrs = [attr.to_dict() for _, attr in attrs if attr.is_computed] or None
|
||||
password_dict = dict()
|
||||
computed_attrs = list()
|
||||
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)
|
||||
elif attr.alias in ci_dict:
|
||||
password_dict[attr.id] = ci_dict.pop(attr.alias)
|
||||
|
||||
value_manager = AttributeValueManager()
|
||||
|
||||
@@ -414,6 +451,7 @@ 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):
|
||||
self._valid_unique_constraint(ci.type_id, ci_dict, ci_id)
|
||||
@@ -427,12 +465,16 @@ class CIManager(object):
|
||||
return abort(403, ErrFormat.ci_filter_perm_attr_no_permission.format(k))
|
||||
|
||||
try:
|
||||
record_id = value_manager.create_or_update_attr_value2(ci, ci_dict, key2attr)
|
||||
record_id = value_manager.create_or_update_attr_value(ci, ci_dict, key2attr)
|
||||
except BadRequest as e:
|
||||
raise e
|
||||
|
||||
if password_dict:
|
||||
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([ci_id], queue=CMDB_QUEUE)
|
||||
ci_cache.apply_async(args=(ci_id, OperateType.UPDATE, record_id), queue=CMDB_QUEUE)
|
||||
|
||||
ref_ci_dict = {k: v for k, v in ci_dict.items() if k.startswith("$") and "." in k}
|
||||
if ref_ci_dict:
|
||||
@@ -442,9 +484,10 @@ class CIManager(object):
|
||||
def update_unique_value(ci_id, unique_name, unique_value):
|
||||
ci = CI.get_by_id(ci_id) or abort(404, ErrFormat.ci_not_found.format("id={}".format(ci_id)))
|
||||
|
||||
AttributeValueManager().create_or_update_attr_value(unique_name, unique_value, ci)
|
||||
key2attr = {unique_name: AttributeCache.get(unique_name)}
|
||||
record_id = AttributeValueManager().create_or_update_attr_value(ci, {unique_name: unique_value}, key2attr)
|
||||
|
||||
ci_cache.apply_async([ci_id], queue=CMDB_QUEUE)
|
||||
ci_cache.apply_async(args=(ci_id, OperateType.UPDATE, record_id), queue=CMDB_QUEUE)
|
||||
|
||||
@classmethod
|
||||
def delete(cls, ci_id):
|
||||
@@ -455,6 +498,18 @@ class CIManager(object):
|
||||
ci_dict = cls.get_cis_by_ids([ci_id])
|
||||
ci_dict = ci_dict and ci_dict[0]
|
||||
|
||||
if ci_dict:
|
||||
triggers = CITriggerManager.get(ci_dict['_type'])
|
||||
for trigger in triggers:
|
||||
option = trigger['option']
|
||||
if not option.get('enable') or option.get('action') != OperateType.DELETE:
|
||||
continue
|
||||
|
||||
if option.get('filter') and not CITriggerManager.ci_filter(ci_dict.get('_id'), option['filter']):
|
||||
continue
|
||||
|
||||
ci_delete_trigger.apply_async(args=(trigger, OperateType.DELETE, ci_dict), queue=CMDB_QUEUE)
|
||||
|
||||
attrs = CITypeAttribute.get_by(type_id=ci.type_id, to_dict=False)
|
||||
attr_names = set([AttributeCache.get(attr.attr_id).name for attr in attrs])
|
||||
for attr_name in attr_names:
|
||||
@@ -477,9 +532,10 @@ class CIManager(object):
|
||||
|
||||
db.session.commit()
|
||||
|
||||
AttributeHistoryManger.add(None, ci_id, [(None, OperateType.DELETE, ci_dict, None)], ci.type_id)
|
||||
if ci_dict:
|
||||
AttributeHistoryManger.add(None, ci_id, [(None, OperateType.DELETE, ci_dict, None)], ci.type_id)
|
||||
|
||||
ci_delete.apply_async([ci.id], queue=CMDB_QUEUE)
|
||||
ci_delete.apply_async(args=(ci_id,), queue=CMDB_QUEUE)
|
||||
|
||||
return ci_id
|
||||
|
||||
@@ -579,10 +635,13 @@ class CIManager(object):
|
||||
_fields = list()
|
||||
for field in fields:
|
||||
attr = AttributeCache.get(field)
|
||||
if attr is not None:
|
||||
if attr is not None and not attr.is_password:
|
||||
_fields.append(str(attr.id))
|
||||
filter_fields_sql = "WHERE A.attr_id in ({0})".format(",".join(_fields))
|
||||
|
||||
ci2pos = {int(_id): _pos for _pos, _id in enumerate(ci_ids)}
|
||||
res = [None] * len(ci_ids)
|
||||
|
||||
ci_ids = ",".join(map(str, ci_ids))
|
||||
if value_tables is None:
|
||||
value_tables = ValueTypeMap.table_name.values()
|
||||
@@ -593,11 +652,10 @@ class CIManager(object):
|
||||
# current_app.logger.debug(query_sql)
|
||||
cis = db.session.execute(query_sql).fetchall()
|
||||
ci_set = set()
|
||||
res = list()
|
||||
ci_dict = dict()
|
||||
unique_id2obj = dict()
|
||||
excludes = excludes and set(excludes)
|
||||
for ci_id, type_id, attr_id, attr_name, attr_alias, value, value_type, is_list in cis:
|
||||
for ci_id, type_id, attr_id, attr_name, attr_alias, value, value_type, is_list, is_password in cis:
|
||||
if not fields and excludes and (attr_name in excludes or attr_alias in excludes):
|
||||
continue
|
||||
|
||||
@@ -613,7 +671,7 @@ class CIManager(object):
|
||||
ci_dict["unique"] = unique_id2obj[ci_type.unique_id] and unique_id2obj[ci_type.unique_id].name
|
||||
ci_dict["unique_alias"] = unique_id2obj[ci_type.unique_id] and unique_id2obj[ci_type.unique_id].alias
|
||||
ci_set.add(ci_id)
|
||||
res.append(ci_dict)
|
||||
res[ci2pos[ci_id]] = ci_dict
|
||||
|
||||
if ret_key == RetKey.NAME:
|
||||
attr_key = attr_name
|
||||
@@ -624,11 +682,14 @@ class CIManager(object):
|
||||
else:
|
||||
return abort(400, ErrFormat.argument_invalid.format("ret_key"))
|
||||
|
||||
value = ValueTypeMap.serialize2[value_type](value)
|
||||
if is_list:
|
||||
ci_dict.setdefault(attr_key, []).append(value)
|
||||
if is_password and value:
|
||||
ci_dict[attr_key] = PASSWORD_DEFAULT_SHOW
|
||||
else:
|
||||
ci_dict[attr_key] = value
|
||||
value = ValueTypeMap.serialize2[value_type](value)
|
||||
if is_list:
|
||||
ci_dict.setdefault(attr_key, []).append(value)
|
||||
else:
|
||||
ci_dict[attr_key] = value
|
||||
|
||||
return res
|
||||
|
||||
@@ -660,6 +721,84 @@ class CIManager(object):
|
||||
|
||||
return cls._get_cis_from_db(ci_ids, ret_key, fields, value_tables, excludes=excludes)
|
||||
|
||||
@classmethod
|
||||
def save_password(cls, ci_id, attr_id, value, record_id, type_id):
|
||||
changed = None
|
||||
encrypt_value = None
|
||||
value_table = ValueTypeMap.table[ValueTypeEnum.PASSWORD]
|
||||
if current_app.config.get('SECRETS_ENGINE') == 'inner':
|
||||
if value:
|
||||
encrypt_value, status = InnerCrypt().encrypt(value)
|
||||
if not status:
|
||||
current_app.logger.error('save password failed: {}'.format(encrypt_value))
|
||||
return abort(400, ErrFormat.password_save_failed.format(encrypt_value))
|
||||
else:
|
||||
encrypt_value = PASSWORD_DEFAULT_SHOW
|
||||
|
||||
existed = value_table.get_by(ci_id=ci_id, attr_id=attr_id, first=True, to_dict=False)
|
||||
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)]
|
||||
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)]
|
||||
else:
|
||||
existed.delete()
|
||||
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'))
|
||||
if value:
|
||||
try:
|
||||
vault.update("/{}/{}".format(ci_id, attr_id), dict(v=value))
|
||||
except Exception as e:
|
||||
current_app.logger.error('save password to vault failed: {}'.format(e))
|
||||
return abort(400, ErrFormat.password_save_failed.format('write vault failed'))
|
||||
else:
|
||||
try:
|
||||
vault.delete("/{}/{}".format(ci_id, attr_id))
|
||||
except Exception as e:
|
||||
current_app.logger.warning('delete password to vault failed: {}'.format(e))
|
||||
|
||||
if changed is not None:
|
||||
return AttributeValueManager.write_change2(changed, record_id)
|
||||
|
||||
@classmethod
|
||||
def load_password(cls, ci_id, attr_id):
|
||||
ci = CI.get_by_id(ci_id) or abort(404, ErrFormat.ci_not_found.format(ci_id))
|
||||
|
||||
limit_attrs = cls._valid_ci_for_no_read(ci, ci.ci_type)
|
||||
if limit_attrs:
|
||||
attr = AttributeCache.get(attr_id)
|
||||
if attr and attr.name not in limit_attrs:
|
||||
return abort(403, ErrFormat.no_permission2)
|
||||
|
||||
if current_app.config.get('SECRETS_ENGINE', 'inner') == 'inner':
|
||||
value_table = ValueTypeMap.table[ValueTypeEnum.PASSWORD]
|
||||
v = value_table.get_by(ci_id=ci_id, attr_id=attr_id, first=True, to_dict=False)
|
||||
|
||||
v = v and v.value
|
||||
if not v:
|
||||
return
|
||||
|
||||
decrypt_value, status = InnerCrypt().decrypt(v)
|
||||
if not status:
|
||||
current_app.logger.error('load password failed: {}'.format(decrypt_value))
|
||||
return abort(400, ErrFormat.password_load_failed.format(decrypt_value))
|
||||
|
||||
return decrypt_value
|
||||
|
||||
elif current_app.config.get('SECRETS_ENGINE') == 'vault':
|
||||
vault = VaultClient(current_app.config.get('VAULT_URL'), current_app.config.get('VAULT_TOKEN'))
|
||||
data, status = vault.read("/{}/{}".format(ci_id, attr_id))
|
||||
if not status:
|
||||
current_app.logger.error('read password from vault failed: {}'.format(data))
|
||||
return abort(400, ErrFormat.password_load_failed.format(data))
|
||||
|
||||
return data.get('v')
|
||||
|
||||
|
||||
class CIRelationManager(object):
|
||||
"""
|
||||
@@ -896,3 +1035,180 @@ class CIRelationManager(object):
|
||||
for parent_id in parents:
|
||||
for ci_id in ci_ids:
|
||||
cls.delete_2(parent_id, ci_id)
|
||||
|
||||
|
||||
class CITriggerManager(object):
|
||||
@staticmethod
|
||||
def get(type_id):
|
||||
db.session.remove()
|
||||
return CITypeTrigger.get_by(type_id=type_id, to_dict=True)
|
||||
|
||||
@staticmethod
|
||||
def _update_old_attr_value(record_id, ci_dict):
|
||||
attr_history = AttributeHistory.get_by(record_id=record_id, to_dict=False)
|
||||
attr_dict = dict()
|
||||
for attr_h in attr_history:
|
||||
attr_dict['old_{}'.format(AttributeCache.get(attr_h.attr_id).name)] = attr_h.old
|
||||
|
||||
ci_dict.update({'old_{}'.format(k): ci_dict[k] for k in ci_dict})
|
||||
|
||||
ci_dict.update(attr_dict)
|
||||
|
||||
@classmethod
|
||||
def _exec_webhook(cls, operate_type, webhook, ci_dict, trigger_id, trigger_name, record_id, ci_id=None, app=None):
|
||||
app = app or current_app
|
||||
|
||||
with app.app_context():
|
||||
if operate_type == OperateType.UPDATE:
|
||||
cls._update_old_attr_value(record_id, ci_dict)
|
||||
|
||||
if ci_id is not None:
|
||||
ci_dict = CIManager().get_ci_by_id_from_db(ci_id, need_children=False, use_master=False)
|
||||
|
||||
try:
|
||||
response = webhook_request(webhook, ci_dict).text
|
||||
is_ok = True
|
||||
except Exception as e:
|
||||
current_app.logger.warning("exec webhook failed: {}".format(e))
|
||||
response = e
|
||||
is_ok = False
|
||||
|
||||
CITriggerHistoryManager.add(operate_type,
|
||||
record_id,
|
||||
ci_dict.get('_id'),
|
||||
trigger_id,
|
||||
trigger_name,
|
||||
is_ok=is_ok,
|
||||
webhook=response)
|
||||
|
||||
return is_ok
|
||||
|
||||
@classmethod
|
||||
def _exec_notify(cls, operate_type, notify, ci_dict, trigger_id, trigger_name, record_id, ci_id=None, app=None):
|
||||
app = app or current_app
|
||||
|
||||
with app.app_context():
|
||||
|
||||
if ci_id is not None:
|
||||
ci_dict = CIManager().get_ci_by_id_from_db(ci_id, need_children=False, use_master=False)
|
||||
|
||||
if operate_type == OperateType.UPDATE:
|
||||
cls._update_old_attr_value(record_id, ci_dict)
|
||||
|
||||
is_ok = True
|
||||
response = ''
|
||||
for method in (notify.get('method') or []):
|
||||
try:
|
||||
res = notify_send(notify.get('subject'), notify.get('body'), [method],
|
||||
notify.get('tos'), ci_dict)
|
||||
response = "{}\n{}".format(response, res)
|
||||
except Exception as e:
|
||||
current_app.logger.warning("send notify failed: {}".format(e))
|
||||
response = "{}\n{}".format(response, e)
|
||||
is_ok = False
|
||||
|
||||
CITriggerHistoryManager.add(operate_type,
|
||||
record_id,
|
||||
ci_dict.get('_id'),
|
||||
trigger_id,
|
||||
trigger_name,
|
||||
is_ok=is_ok,
|
||||
notify=response.strip())
|
||||
|
||||
return is_ok
|
||||
|
||||
@staticmethod
|
||||
def ci_filter(ci_id, other_filter):
|
||||
from api.lib.cmdb.search import SearchError
|
||||
from api.lib.cmdb.search.ci import search
|
||||
|
||||
query = "{},_id:{}".format(other_filter, ci_id)
|
||||
|
||||
try:
|
||||
_, _, _, _, numfound, _ = search(query).search()
|
||||
return numfound
|
||||
except SearchError as e:
|
||||
current_app.logger.warning("ci search failed: {}".format(e))
|
||||
|
||||
@classmethod
|
||||
def fire(cls, operate_type, ci_dict, record_id):
|
||||
type_id = ci_dict.get('_type')
|
||||
triggers = cls.get(type_id) or []
|
||||
|
||||
for trigger in triggers:
|
||||
option = trigger['option']
|
||||
if not option.get('enable'):
|
||||
continue
|
||||
|
||||
if option.get('filter') and not cls.ci_filter(ci_dict.get('_id'), option['filter']):
|
||||
continue
|
||||
|
||||
if option.get('attr_ids') and isinstance(option['attr_ids'], list):
|
||||
if not (set(option['attr_ids']) &
|
||||
set([i.attr_id for i in AttributeHistory.get_by(record_id=record_id, to_dict=False)])):
|
||||
continue
|
||||
|
||||
if option.get('action') == operate_type:
|
||||
cls.fire_by_trigger(trigger, operate_type, ci_dict, record_id)
|
||||
|
||||
@classmethod
|
||||
def fire_by_trigger(cls, trigger, operate_type, ci_dict, record_id=None):
|
||||
option = trigger['option']
|
||||
|
||||
if option.get('webhooks'):
|
||||
cls._exec_webhook(operate_type, option['webhooks'], ci_dict, trigger['id'],
|
||||
option.get('name'), record_id)
|
||||
|
||||
elif option.get('notifies'):
|
||||
cls._exec_notify(operate_type, option['notifies'], ci_dict, trigger['id'],
|
||||
option.get('name'), record_id)
|
||||
|
||||
@classmethod
|
||||
def waiting_cis(cls, trigger):
|
||||
now = datetime.datetime.today()
|
||||
|
||||
config = trigger.option.get('notifies') or {}
|
||||
|
||||
delta_time = datetime.timedelta(days=(config.get('before_days', 0) or 0))
|
||||
|
||||
attr = AttributeCache.get(trigger.attr_id)
|
||||
|
||||
value_table = TableMap(attr=attr).table
|
||||
|
||||
values = value_table.get_by(attr_id=attr.id, to_dict=False)
|
||||
|
||||
result = []
|
||||
for v in values:
|
||||
if (isinstance(v.value, (datetime.date, datetime.datetime)) and
|
||||
(v.value - delta_time).strftime('%Y%m%d') == now.strftime("%Y%m%d")):
|
||||
|
||||
if trigger.option.get('filter') and not cls.ci_filter(v.ci_id, trigger.option['filter']):
|
||||
continue
|
||||
|
||||
result.append(v)
|
||||
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def trigger_notify(cls, trigger, ci):
|
||||
"""
|
||||
only for date attribute
|
||||
:param trigger:
|
||||
:param ci:
|
||||
:return:
|
||||
"""
|
||||
if (trigger.option.get('notifies', {}).get('notify_at') == datetime.datetime.now().strftime("%H:%M") or
|
||||
not trigger.option.get('notifies', {}).get('notify_at')):
|
||||
|
||||
if trigger.option.get('webhooks'):
|
||||
threading.Thread(target=cls._exec_webhook, args=(
|
||||
None, trigger.option['webhooks'], None, trigger.id, trigger.option.get('name'), None, ci.ci_id,
|
||||
current_app._get_current_object())).start()
|
||||
elif trigger.option.get('notifies'):
|
||||
threading.Thread(target=cls._exec_notify, args=(
|
||||
None, trigger.option['notifies'], None, trigger.id, trigger.option.get('name'), None, ci.ci_id,
|
||||
current_app._get_current_object())).start()
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
@@ -1,7 +1,6 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
import copy
|
||||
import datetime
|
||||
|
||||
import toposort
|
||||
from flask import abort
|
||||
@@ -25,7 +24,6 @@ from api.lib.cmdb.const import ValueTypeEnum
|
||||
from api.lib.cmdb.history import CITypeHistoryManager
|
||||
from api.lib.cmdb.relation_type import RelationTypeManager
|
||||
from api.lib.cmdb.resp_format import ErrFormat
|
||||
from api.lib.cmdb.utils import TableMap
|
||||
from api.lib.cmdb.value import AttributeValueManager
|
||||
from api.lib.decorator import kwargs_required
|
||||
from api.lib.perm.acl.acl import ACLManager
|
||||
@@ -354,19 +352,20 @@ class CITypeAttributeManager(object):
|
||||
return [AttributeCache.get(attr.attr_id).name for attr in CITypeAttributesCache.get(type_id)]
|
||||
|
||||
@staticmethod
|
||||
def get_attributes_by_type_id(type_id, choice_web_hook_parse=True):
|
||||
def get_attributes_by_type_id(type_id, choice_web_hook_parse=True, choice_other_parse=True):
|
||||
has_config_perm = ACLManager('cmdb').has_permission(
|
||||
CITypeManager.get_name_by_id(type_id), ResourceTypeEnum.CI, PermEnum.CONFIG)
|
||||
|
||||
attrs = CITypeAttributesCache.get(type_id)
|
||||
result = list()
|
||||
for attr in sorted(attrs, key=lambda x: (x.order, x.id)):
|
||||
attr_dict = AttributeManager().get_attribute(attr.attr_id, choice_web_hook_parse)
|
||||
attr_dict = AttributeManager().get_attribute(attr.attr_id, choice_web_hook_parse, choice_other_parse)
|
||||
attr_dict["is_required"] = attr.is_required
|
||||
attr_dict["order"] = attr.order
|
||||
attr_dict["default_show"] = attr.default_show
|
||||
if not has_config_perm:
|
||||
attr_dict.pop('choice_web_hook', None)
|
||||
attr_dict.pop('choice_other', None)
|
||||
|
||||
result.append(attr_dict)
|
||||
|
||||
@@ -374,13 +373,25 @@ class CITypeAttributeManager(object):
|
||||
|
||||
@staticmethod
|
||||
def get_common_attributes(type_ids):
|
||||
has_config_perm = False
|
||||
for type_id in type_ids:
|
||||
has_config_perm |= ACLManager('cmdb').has_permission(
|
||||
CITypeManager.get_name_by_id(type_id), ResourceTypeEnum.CI, PermEnum.CONFIG)
|
||||
|
||||
result = CITypeAttribute.get_by(__func_in___key_type_id=list(map(int, type_ids)), to_dict=False)
|
||||
attr2types = {}
|
||||
for i in result:
|
||||
attr2types.setdefault(i.attr_id, []).append(i.type_id)
|
||||
|
||||
return [AttributeCache.get(attr_id).to_dict() for attr_id in attr2types
|
||||
if len(attr2types[attr_id]) == len(type_ids)]
|
||||
attrs = []
|
||||
for attr_id in attr2types:
|
||||
if len(attr2types[attr_id]) == len(type_ids):
|
||||
attr = AttributeManager().get_attribute_by_id(attr_id)
|
||||
if not has_config_perm:
|
||||
attr.pop('choice_web_hook', None)
|
||||
attrs.append(attr)
|
||||
|
||||
return attrs
|
||||
|
||||
@staticmethod
|
||||
def _check(type_id, attr_ids):
|
||||
@@ -489,7 +500,7 @@ class CITypeAttributeManager(object):
|
||||
for ci in CI.get_by(type_id=type_id, to_dict=False):
|
||||
AttributeValueManager.delete_attr_value(attr_id, ci.id)
|
||||
|
||||
ci_cache.apply_async([ci.id], queue=CMDB_QUEUE)
|
||||
ci_cache.apply_async(args=(ci.id, None, None), queue=CMDB_QUEUE)
|
||||
|
||||
CITypeAttributeCache.clean(type_id, attr_id)
|
||||
|
||||
@@ -522,7 +533,7 @@ class CITypeAttributeManager(object):
|
||||
CITypeAttributesCache.clean(type_id)
|
||||
|
||||
from api.tasks.cmdb import ci_type_attribute_order_rebuild
|
||||
ci_type_attribute_order_rebuild.apply_async(args=(type_id,), queue=CMDB_QUEUE)
|
||||
ci_type_attribute_order_rebuild.apply_async(args=(type_id, current_user.uid), queue=CMDB_QUEUE)
|
||||
|
||||
|
||||
class CITypeRelationManager(object):
|
||||
@@ -582,7 +593,8 @@ class CITypeRelationManager(object):
|
||||
|
||||
def get_children(_id, level):
|
||||
children = CITypeRelation.get_by(parent_id=_id, to_dict=False)
|
||||
result[level + 1] = [i.child.to_dict() for i in children]
|
||||
if children:
|
||||
result.setdefault(level + 1, []).extend([i.child.to_dict() for i in children])
|
||||
|
||||
for i in children:
|
||||
if i.child_id != _id:
|
||||
@@ -846,7 +858,7 @@ class CITypeAttributeGroupManager(object):
|
||||
CITypeAttributesCache.clean(type_id)
|
||||
|
||||
from api.tasks.cmdb import ci_type_attribute_order_rebuild
|
||||
ci_type_attribute_order_rebuild.apply_async(args=(type_id,), queue=CMDB_QUEUE)
|
||||
ci_type_attribute_order_rebuild.apply_async(args=(type_id, current_user.uid), queue=CMDB_QUEUE)
|
||||
|
||||
|
||||
class CITypeTemplateManager(object):
|
||||
@@ -1091,7 +1103,7 @@ class CITypeTemplateManager(object):
|
||||
|
||||
for ci_type in tpt['ci_types']:
|
||||
tpt['type2attributes'][ci_type['id']] = CITypeAttributeManager.get_attributes_by_type_id(
|
||||
ci_type['id'], choice_web_hook_parse=False)
|
||||
ci_type['id'], choice_web_hook_parse=False, choice_other_parse=False)
|
||||
|
||||
tpt['type2attribute_group'][ci_type['id']] = CITypeAttributeGroupManager.get_by_type_id(ci_type['id'])
|
||||
|
||||
@@ -1165,16 +1177,18 @@ class CITypeUniqueConstraintManager(object):
|
||||
|
||||
class CITypeTriggerManager(object):
|
||||
@staticmethod
|
||||
def get(type_id):
|
||||
return CITypeTrigger.get_by(type_id=type_id, to_dict=True)
|
||||
def get(type_id, to_dict=True):
|
||||
return CITypeTrigger.get_by(type_id=type_id, to_dict=to_dict)
|
||||
|
||||
@staticmethod
|
||||
def add(type_id, attr_id, notify):
|
||||
CITypeTrigger.get_by(type_id=type_id, attr_id=attr_id) and abort(400, ErrFormat.ci_type_trigger_duplicate)
|
||||
def add(type_id, attr_id, option):
|
||||
for i in CITypeTrigger.get_by(type_id=type_id, attr_id=attr_id, to_dict=False):
|
||||
if i.option == option:
|
||||
return abort(400, ErrFormat.ci_type_trigger_duplicate)
|
||||
|
||||
not isinstance(notify, dict) and abort(400, ErrFormat.argument_invalid.format("notify"))
|
||||
not isinstance(option, dict) and abort(400, ErrFormat.argument_invalid.format("option"))
|
||||
|
||||
trigger = CITypeTrigger.create(type_id=type_id, attr_id=attr_id, notify=notify)
|
||||
trigger = CITypeTrigger.create(type_id=type_id, attr_id=attr_id, option=option)
|
||||
|
||||
CITypeHistoryManager.add(CITypeOperateType.ADD_TRIGGER,
|
||||
type_id,
|
||||
@@ -1184,12 +1198,12 @@ class CITypeTriggerManager(object):
|
||||
return trigger.to_dict()
|
||||
|
||||
@staticmethod
|
||||
def update(_id, notify):
|
||||
def update(_id, attr_id, option):
|
||||
existed = (CITypeTrigger.get_by_id(_id) or
|
||||
abort(404, ErrFormat.ci_type_trigger_not_found.format("id={}".format(_id))))
|
||||
|
||||
existed2 = existed.to_dict()
|
||||
new = existed.update(notify=notify)
|
||||
new = existed.update(attr_id=attr_id or None, option=option, filter_none=False)
|
||||
|
||||
CITypeHistoryManager.add(CITypeOperateType.UPDATE_TRIGGER,
|
||||
existed.type_id,
|
||||
@@ -1209,35 +1223,3 @@ class CITypeTriggerManager(object):
|
||||
existed.type_id,
|
||||
trigger_id=_id,
|
||||
change=existed.to_dict())
|
||||
|
||||
@staticmethod
|
||||
def waiting_cis(trigger):
|
||||
now = datetime.datetime.today()
|
||||
|
||||
delta_time = datetime.timedelta(days=(trigger.notify.get('before_days', 0) or 0))
|
||||
|
||||
attr = AttributeCache.get(trigger.attr_id)
|
||||
|
||||
value_table = TableMap(attr=attr).table
|
||||
|
||||
values = value_table.get_by(attr_id=attr.id, to_dict=False)
|
||||
|
||||
result = []
|
||||
for v in values:
|
||||
if (isinstance(v.value, (datetime.date, datetime.datetime)) and
|
||||
(v.value - delta_time).strftime('%Y%m%d') == now.strftime("%Y%m%d")):
|
||||
result.append(v)
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def trigger_notify(trigger, ci):
|
||||
if (trigger.notify.get('notify_at') == datetime.datetime.now().strftime("%H:%M") or
|
||||
not trigger.notify.get('notify_at')):
|
||||
from api.tasks.cmdb import trigger_notify
|
||||
|
||||
trigger_notify.apply_async(args=(trigger.notify, ci.ci_id), queue=CMDB_QUEUE)
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
@@ -12,6 +12,8 @@ class ValueTypeEnum(BaseEnum):
|
||||
DATE = "4"
|
||||
TIME = "5"
|
||||
JSON = "6"
|
||||
PASSWORD = TEXT
|
||||
LINK = TEXT
|
||||
|
||||
|
||||
class ConstraintEnum(BaseEnum):
|
||||
|
@@ -16,6 +16,7 @@ from api.lib.perm.acl.cache import UserCache
|
||||
from api.models.cmdb import Attribute
|
||||
from api.models.cmdb import AttributeHistory
|
||||
from api.models.cmdb import CIRelationHistory
|
||||
from api.models.cmdb import CITriggerHistory
|
||||
from api.models.cmdb import CITypeHistory
|
||||
from api.models.cmdb import CITypeTrigger
|
||||
from api.models.cmdb import CITypeUniqueConstraint
|
||||
@@ -286,3 +287,68 @@ class CITypeHistoryManager(object):
|
||||
change=change)
|
||||
|
||||
CITypeHistory.create(**payload)
|
||||
|
||||
|
||||
class CITriggerHistoryManager(object):
|
||||
@staticmethod
|
||||
def get(page, page_size, type_id=None, trigger_id=None, operate_type=None):
|
||||
query = CITriggerHistory.get_by(only_query=True)
|
||||
if type_id:
|
||||
query = query.filter(CITriggerHistory.type_id == type_id)
|
||||
|
||||
if trigger_id:
|
||||
query = query.filter(CITriggerHistory.trigger_id == trigger_id)
|
||||
|
||||
if operate_type:
|
||||
query = query.filter(CITriggerHistory.operate_type == operate_type)
|
||||
|
||||
numfound = query.count()
|
||||
|
||||
query = query.order_by(CITriggerHistory.id.desc())
|
||||
result = query.offset((page - 1) * page_size).limit(page_size)
|
||||
result = [i.to_dict() for i in result]
|
||||
for res in result:
|
||||
if res.get('trigger_id'):
|
||||
trigger = CITypeTrigger.get_by_id(res['trigger_id'])
|
||||
res['trigger'] = trigger and trigger.to_dict()
|
||||
|
||||
return numfound, result
|
||||
|
||||
@staticmethod
|
||||
def get_by_ci_id(ci_id):
|
||||
res = db.session.query(CITriggerHistory, CITypeTrigger).join(
|
||||
CITypeTrigger, CITypeTrigger.id == CITriggerHistory.trigger_id).filter(
|
||||
CITriggerHistory.ci_id == ci_id).order_by(CITriggerHistory.id.desc())
|
||||
|
||||
result = []
|
||||
id2trigger = dict()
|
||||
for i in res:
|
||||
hist = i.CITriggerHistory
|
||||
item = dict(is_ok=hist.is_ok,
|
||||
operate_type=hist.operate_type,
|
||||
notify=hist.notify,
|
||||
trigger_id=hist.trigger_id,
|
||||
trigger_name=hist.trigger_name,
|
||||
webhook=hist.webhook,
|
||||
created_at=hist.created_at.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
record_id=hist.record_id,
|
||||
hid=hist.id
|
||||
)
|
||||
if i.CITypeTrigger.id not in id2trigger:
|
||||
id2trigger[i.CITypeTrigger.id] = i.CITypeTrigger.to_dict()
|
||||
|
||||
result.append(item)
|
||||
|
||||
return dict(items=result, id2trigger=id2trigger)
|
||||
|
||||
@staticmethod
|
||||
def add(operate_type, record_id, ci_id, trigger_id, trigger_name, is_ok=False, notify=None, webhook=None):
|
||||
|
||||
CITriggerHistory.create(operate_type=operate_type,
|
||||
record_id=record_id,
|
||||
ci_id=ci_id,
|
||||
trigger_id=trigger_id,
|
||||
trigger_name=trigger_name,
|
||||
is_ok=is_ok,
|
||||
notify=notify,
|
||||
webhook=webhook)
|
||||
|
@@ -116,7 +116,7 @@ class PreferenceManager(object):
|
||||
for i in result:
|
||||
if i["is_choice"]:
|
||||
i.update(dict(choice_value=AttributeManager.get_choice_values(
|
||||
i["id"], i["value_type"], i["choice_web_hook"])))
|
||||
i["id"], i["value_type"], i["choice_web_hook"], i.get("choice_other"))))
|
||||
|
||||
return is_subscribed, result
|
||||
|
||||
|
@@ -23,6 +23,7 @@ class ErrFormat(CommonErrFormat):
|
||||
cannot_edit_attribute = "您没有权限修改该属性!"
|
||||
cannot_delete_attribute = "目前只允许 属性创建人、管理员 删除属性!"
|
||||
attribute_name_cannot_be_builtin = "属性字段名不能是内置字段: id, _id, ci_id, type, _type, ci_type"
|
||||
attribute_choice_other_invalid = "预定义值: 其他模型请求参数不合法!"
|
||||
|
||||
ci_not_found = "CI {} 不存在"
|
||||
unique_constraint = "多属性联合唯一校验不通过: {}"
|
||||
@@ -94,3 +95,6 @@ class ErrFormat(CommonErrFormat):
|
||||
ci_filter_perm_cannot_or_query = "CI过滤授权 暂时不支持 或 查询"
|
||||
ci_filter_perm_attr_no_permission = "您没有属性 {} 的操作权限!"
|
||||
ci_filter_perm_ci_no_permission = "您没有该CI的操作权限!"
|
||||
|
||||
password_save_failed = "保存密码失败: {}"
|
||||
password_load_failed = "获取密码失败: {}"
|
||||
|
@@ -7,6 +7,7 @@ QUERY_CIS_BY_VALUE_TABLE = """
|
||||
attr.alias AS attr_alias,
|
||||
attr.value_type,
|
||||
attr.is_list,
|
||||
attr.is_password,
|
||||
c_cis.type_id,
|
||||
{0}.ci_id,
|
||||
{0}.attr_id,
|
||||
@@ -26,7 +27,8 @@ QUERY_CIS_BY_IDS = """
|
||||
A.attr_alias,
|
||||
A.value,
|
||||
A.value_type,
|
||||
A.is_list
|
||||
A.is_list,
|
||||
A.is_password
|
||||
FROM
|
||||
({1}) AS A {0}
|
||||
ORDER BY A.ci_id;
|
||||
@@ -43,7 +45,7 @@ FACET_QUERY1 = """
|
||||
|
||||
FACET_QUERY = """
|
||||
SELECT {0}.value,
|
||||
count({0}.ci_id)
|
||||
count(distinct {0}.ci_id)
|
||||
FROM {0}
|
||||
INNER JOIN ({1}) AS F ON F.ci_id={0}.ci_id
|
||||
WHERE {0}.attr_id={2:d}
|
||||
|
@@ -9,6 +9,7 @@ import time
|
||||
from flask import current_app
|
||||
from flask_login import current_user
|
||||
from jinja2 import Template
|
||||
from sqlalchemy import text
|
||||
|
||||
from api.extensions import db
|
||||
from api.lib.cmdb.cache import AttributeCache
|
||||
@@ -28,6 +29,7 @@ from api.lib.cmdb.search.ci.db.query_sql import QUERY_CI_BY_NO_ATTR
|
||||
from api.lib.cmdb.search.ci.db.query_sql import QUERY_CI_BY_TYPE
|
||||
from api.lib.cmdb.search.ci.db.query_sql import QUERY_UNION_CI_ATTRIBUTE_IS_NULL
|
||||
from api.lib.cmdb.utils import TableMap
|
||||
from api.lib.cmdb.utils import ValueTypeMap
|
||||
from api.lib.perm.acl.acl import ACLManager
|
||||
from api.lib.perm.acl.acl import is_app_admin
|
||||
from api.lib.utils import handle_arg_list
|
||||
@@ -141,6 +143,10 @@ class Search(object):
|
||||
@staticmethod
|
||||
def _in_query_handler(attr, v, is_not):
|
||||
new_v = v[1:-1].split(";")
|
||||
|
||||
if attr.value_type == ValueTypeEnum.DATE:
|
||||
new_v = ["{} 00:00:00".format(i) for i in new_v if len(i) == 10]
|
||||
|
||||
table_name = TableMap(attr=attr).table_name
|
||||
in_query = " OR {0}.value ".format(table_name).join(['{0} "{1}"'.format(
|
||||
"NOT LIKE" if is_not else "LIKE",
|
||||
@@ -151,6 +157,11 @@ class Search(object):
|
||||
@staticmethod
|
||||
def _range_query_handler(attr, v, is_not):
|
||||
start, end = [x.strip() for x in v[1:-1].split("_TO_")]
|
||||
|
||||
if attr.value_type == ValueTypeEnum.DATE:
|
||||
start = "{} 00:00:00".format(start) if len(start) == 10 else start
|
||||
end = "{} 00:00:00".format(end) if len(end) == 10 else end
|
||||
|
||||
table_name = TableMap(attr=attr).table_name
|
||||
range_query = "{0} '{1}' AND '{2}'".format(
|
||||
"NOT BETWEEN" if is_not else "BETWEEN",
|
||||
@@ -162,8 +173,14 @@ class Search(object):
|
||||
def _comparison_query_handler(attr, v):
|
||||
table_name = TableMap(attr=attr).table_name
|
||||
if v.startswith(">=") or v.startswith("<="):
|
||||
if attr.value_type == ValueTypeEnum.DATE and len(v[2:]) == 10:
|
||||
v = "{} 00:00:00".format(v)
|
||||
|
||||
comparison_query = "{0} '{1}'".format(v[:2], v[2:].replace("*", "%"))
|
||||
else:
|
||||
if attr.value_type == ValueTypeEnum.DATE and len(v[1:]) == 10:
|
||||
v = "{} 00:00:00".format(v)
|
||||
|
||||
comparison_query = "{0} '{1}'".format(v[0], v[1:].replace("*", "%"))
|
||||
_query_sql = QUERY_CI_BY_ATTR_NAME.format(table_name, attr.id, comparison_query)
|
||||
return _query_sql
|
||||
@@ -295,8 +312,8 @@ class Search(object):
|
||||
|
||||
start = time.time()
|
||||
execute = db.session.execute
|
||||
current_app.logger.debug(v_query_sql)
|
||||
res = execute(v_query_sql).fetchall()
|
||||
# current_app.logger.debug(v_query_sql)
|
||||
res = execute(text(v_query_sql)).fetchall()
|
||||
end_time = time.time()
|
||||
current_app.logger.debug("query ci ids time is: {0}".format(end_time - start))
|
||||
|
||||
@@ -391,6 +408,9 @@ class Search(object):
|
||||
|
||||
is_not = True if operator == "|~" else False
|
||||
|
||||
if field_type == ValueTypeEnum.DATE and len(v) == 10:
|
||||
v = "{} 00:00:00".format(v)
|
||||
|
||||
# in query
|
||||
if v.startswith("(") and v.endswith(")"):
|
||||
_query_sql = self._in_query_handler(attr, v, is_not)
|
||||
@@ -506,15 +526,15 @@ class Search(object):
|
||||
if k:
|
||||
table_name = TableMap(attr=attr).table_name
|
||||
query_sql = FACET_QUERY.format(table_name, self.query_sql, attr.id)
|
||||
# current_app.logger.debug(query_sql)
|
||||
result = db.session.execute(query_sql).fetchall()
|
||||
result = db.session.execute(text(query_sql)).fetchall()
|
||||
facet[k] = result
|
||||
|
||||
facet_result = dict()
|
||||
for k, v in facet.items():
|
||||
if not k.startswith('_'):
|
||||
a = getattr(AttributeCache.get(k), self.ret_key)
|
||||
facet_result[a] = [(f[0], f[1], a) for f in v]
|
||||
attr = AttributeCache.get(k)
|
||||
a = getattr(attr, self.ret_key)
|
||||
facet_result[a] = [(ValueTypeMap.serialize[attr.value_type](f[0]), f[1], a) for f in v]
|
||||
|
||||
return facet_result
|
||||
|
||||
|
@@ -35,7 +35,7 @@ class Search(object):
|
||||
self.sort = sort or ("ci_id" if current_app.config.get("USE_ES") else None)
|
||||
|
||||
self.root_id = root_id
|
||||
self.level = level
|
||||
self.level = level or 0
|
||||
self.reverse = reverse
|
||||
|
||||
def _get_ids(self):
|
||||
@@ -104,16 +104,22 @@ class Search(object):
|
||||
ci_ids=merge_ids).search()
|
||||
|
||||
def statistics(self, type_ids):
|
||||
self.level = int(self.level)
|
||||
_tmp = []
|
||||
ids = [self.root_id] if not isinstance(self.root_id, list) else self.root_id
|
||||
for l in range(0, int(self.level)):
|
||||
if not l:
|
||||
_tmp = list(map(lambda x: list(json.loads(x).items()),
|
||||
[i or '{}' for i in rd.get(ids, REDIS_PREFIX_CI_RELATION) or []]))
|
||||
for lv in range(0, self.level):
|
||||
if not lv:
|
||||
if type_ids and lv == self.level - 1:
|
||||
_tmp = list(map(lambda x: [i for i in x if i[1] in type_ids],
|
||||
(map(lambda x: list(json.loads(x).items()),
|
||||
[i or '{}' for i in rd.get(ids, REDIS_PREFIX_CI_RELATION) or []]))))
|
||||
else:
|
||||
_tmp = list(map(lambda x: list(json.loads(x).items()),
|
||||
[i or '{}' for i in rd.get(ids, REDIS_PREFIX_CI_RELATION) or []]))
|
||||
else:
|
||||
for idx, item in enumerate(_tmp):
|
||||
if item:
|
||||
if type_ids and l == self.level - 1:
|
||||
if type_ids and lv == self.level - 1:
|
||||
__tmp = list(
|
||||
map(lambda x: [(_id, type_id) for _id, type_id in json.loads(x).items()
|
||||
if type_id in type_ids],
|
||||
|
@@ -12,7 +12,7 @@ import api.models.cmdb as model
|
||||
from api.lib.cmdb.cache import AttributeCache
|
||||
from api.lib.cmdb.const import ValueTypeEnum
|
||||
|
||||
TIME_RE = re.compile(r"^(20|21|22|23|[0-1]\d):[0-5]\d:[0-5]\d$")
|
||||
TIME_RE = re.compile(r"^20|21|22|23|[0-1]\d:[0-5]\d:[0-5]\d$")
|
||||
|
||||
|
||||
def string2int(x):
|
||||
@@ -21,7 +21,7 @@ def string2int(x):
|
||||
|
||||
def str2datetime(x):
|
||||
try:
|
||||
return datetime.datetime.strptime(x, "%Y-%m-%d")
|
||||
return datetime.datetime.strptime(x, "%Y-%m-%d").date()
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
@@ -44,8 +44,8 @@ class ValueTypeMap(object):
|
||||
ValueTypeEnum.FLOAT: float,
|
||||
ValueTypeEnum.TEXT: lambda x: x if isinstance(x, six.string_types) else str(x),
|
||||
ValueTypeEnum.TIME: lambda x: x if isinstance(x, six.string_types) else str(x),
|
||||
ValueTypeEnum.DATE: lambda x: x.strftime("%Y-%m-%d"),
|
||||
ValueTypeEnum.DATETIME: lambda x: x.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
ValueTypeEnum.DATE: lambda x: x.strftime("%Y-%m-%d") if not isinstance(x, six.string_types) else x,
|
||||
ValueTypeEnum.DATETIME: lambda x: x.strftime("%Y-%m-%d %H:%M:%S") if not isinstance(x, six.string_types) else x,
|
||||
ValueTypeEnum.JSON: lambda x: json.loads(x) if isinstance(x, six.string_types) and x else x,
|
||||
}
|
||||
|
||||
@@ -64,6 +64,8 @@ class ValueTypeMap(object):
|
||||
ValueTypeEnum.FLOAT: model.FloatChoice,
|
||||
ValueTypeEnum.TEXT: model.TextChoice,
|
||||
ValueTypeEnum.TIME: model.TextChoice,
|
||||
ValueTypeEnum.DATE: model.TextChoice,
|
||||
ValueTypeEnum.DATETIME: model.TextChoice,
|
||||
}
|
||||
|
||||
table = {
|
||||
@@ -97,7 +99,7 @@ class ValueTypeMap(object):
|
||||
ValueTypeEnum.DATE: 'text',
|
||||
ValueTypeEnum.TIME: 'text',
|
||||
ValueTypeEnum.FLOAT: 'float',
|
||||
ValueTypeEnum.JSON: 'object'
|
||||
ValueTypeEnum.JSON: 'object',
|
||||
}
|
||||
|
||||
|
||||
@@ -110,7 +112,9 @@ class TableMap(object):
|
||||
@property
|
||||
def table(self):
|
||||
attr = AttributeCache.get(self.attr_name) if not self.attr else self.attr
|
||||
if attr.value_type != ValueTypeEnum.TEXT and attr.value_type != ValueTypeEnum.JSON:
|
||||
if attr.is_password or attr.is_link:
|
||||
self.is_index = False
|
||||
elif attr.value_type not in {ValueTypeEnum.TEXT, ValueTypeEnum.JSON}:
|
||||
self.is_index = True
|
||||
elif self.is_index is None:
|
||||
self.is_index = attr.is_index
|
||||
@@ -122,7 +126,9 @@ class TableMap(object):
|
||||
@property
|
||||
def table_name(self):
|
||||
attr = AttributeCache.get(self.attr_name) if not self.attr else self.attr
|
||||
if attr.value_type != ValueTypeEnum.TEXT and attr.value_type != ValueTypeEnum.JSON:
|
||||
if attr.is_password or attr.is_link:
|
||||
self.is_index = False
|
||||
elif attr.value_type not in {ValueTypeEnum.TEXT, ValueTypeEnum.JSON}:
|
||||
self.is_index = True
|
||||
elif self.is_index is None:
|
||||
self.is_index = attr.is_index
|
||||
|
@@ -18,7 +18,6 @@ from api.extensions import db
|
||||
from api.lib.cmdb.attribute import AttributeManager
|
||||
from api.lib.cmdb.cache import AttributeCache
|
||||
from api.lib.cmdb.cache import CITypeAttributeCache
|
||||
from api.lib.cmdb.const import ExistPolicy
|
||||
from api.lib.cmdb.const import OperateType
|
||||
from api.lib.cmdb.const import ValueTypeEnum
|
||||
from api.lib.cmdb.history import AttributeHistoryManger
|
||||
@@ -67,9 +66,10 @@ class AttributeValueManager(object):
|
||||
use_master=use_master,
|
||||
to_dict=False)
|
||||
field_name = getattr(attr, ret_key)
|
||||
|
||||
if attr.is_list:
|
||||
res[field_name] = [ValueTypeMap.serialize[attr.value_type](i.value) for i in rs]
|
||||
elif attr.is_password and rs:
|
||||
res[field_name] = '******' if rs[0].value else ''
|
||||
else:
|
||||
res[field_name] = ValueTypeMap.serialize[attr.value_type](rs[0].value) if rs else None
|
||||
|
||||
@@ -93,7 +93,7 @@ class AttributeValueManager(object):
|
||||
|
||||
@staticmethod
|
||||
def _check_is_choice(attr, value_type, value):
|
||||
choice_values = AttributeManager.get_choice_values(attr.id, value_type, attr.choice_web_hook)
|
||||
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))
|
||||
|
||||
@@ -132,14 +132,14 @@ 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):
|
||||
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,
|
||||
commit=False, flush=False)
|
||||
try:
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
current_app.logger.error("write change failed: {}".format(str(e)))
|
||||
|
||||
return record_id
|
||||
@@ -235,7 +235,7 @@ class AttributeValueManager(object):
|
||||
|
||||
return key2attr
|
||||
|
||||
def create_or_update_attr_value2(self, ci, ci_dict, key2attr):
|
||||
def create_or_update_attr_value(self, ci, ci_dict, key2attr):
|
||||
"""
|
||||
add or update attribute value, then write history
|
||||
:param ci: instance object
|
||||
@@ -284,69 +284,9 @@ class AttributeValueManager(object):
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
current_app.logger.warning(str(e))
|
||||
return abort(400, ErrFormat.attribute_value_unknown_error.format(str(e)))
|
||||
return abort(400, ErrFormat.attribute_value_unknown_error.format(e.args[0]))
|
||||
|
||||
return self._write_change2(changed)
|
||||
|
||||
def create_or_update_attr_value(self, key, value, ci, _no_attribute_policy=ExistPolicy.IGNORE, record_id=None):
|
||||
"""
|
||||
add or update attribute value, then write history
|
||||
:param key: id, name or alias
|
||||
:param value:
|
||||
:param ci: instance object
|
||||
:param _no_attribute_policy: ignore or reject
|
||||
:param record_id: op record
|
||||
:return:
|
||||
"""
|
||||
attr = self._get_attr(key)
|
||||
if attr is None:
|
||||
if _no_attribute_policy == ExistPolicy.IGNORE:
|
||||
return
|
||||
if _no_attribute_policy == ExistPolicy.REJECT:
|
||||
return abort(400, ErrFormat.attribute_not_found.format(key))
|
||||
|
||||
value_table = TableMap(attr=attr).table
|
||||
|
||||
try:
|
||||
if attr.is_list:
|
||||
value_list = [self._validate(attr, i, value_table, ci) for i in handle_arg_list(value)]
|
||||
if not value_list:
|
||||
self._check_is_required(ci.type_id, attr, '')
|
||||
|
||||
existed_attrs = value_table.get_by(attr_id=attr.id, ci_id=ci.id, to_dict=False)
|
||||
existed_values = [i.value for i in existed_attrs]
|
||||
added = set(value_list) - set(existed_values)
|
||||
deleted = set(existed_values) - set(value_list)
|
||||
for v in added:
|
||||
value_table.create(ci_id=ci.id, attr_id=attr.id, value=v)
|
||||
record_id = self._write_change(ci.id, attr.id, OperateType.ADD, None, v, record_id, ci.type_id)
|
||||
|
||||
for v in deleted:
|
||||
existed_attr = existed_attrs[existed_values.index(v)]
|
||||
existed_attr.delete()
|
||||
record_id = self._write_change(ci.id, attr.id, OperateType.DELETE, v, None, record_id, ci.type_id)
|
||||
else:
|
||||
value = self._validate(attr, value, value_table, ci)
|
||||
existed_attr = value_table.get_by(attr_id=attr.id, ci_id=ci.id, first=True, to_dict=False)
|
||||
existed_value = existed_attr and existed_attr.value
|
||||
if existed_value is None and value is not None:
|
||||
value_table.create(ci_id=ci.id, attr_id=attr.id, value=value)
|
||||
|
||||
record_id = self._write_change(ci.id, attr.id, OperateType.ADD, None, value, record_id, ci.type_id)
|
||||
else:
|
||||
if existed_value != value:
|
||||
if value is None:
|
||||
existed_attr.delete()
|
||||
else:
|
||||
existed_attr.update(value=value)
|
||||
|
||||
record_id = self._write_change(ci.id, attr.id, OperateType.UPDATE,
|
||||
existed_value, value, record_id, ci.type_id)
|
||||
|
||||
return record_id
|
||||
except Exception as e:
|
||||
current_app.logger.warning(str(e))
|
||||
return abort(400, ErrFormat.attribute_value_invalid2.format("{}({})".format(attr.alias, attr.name), value))
|
||||
return self.write_change2(changed)
|
||||
|
||||
@staticmethod
|
||||
def delete_attr_value(attr_id, ci_id):
|
||||
|
@@ -1,12 +1,13 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
from flask import abort
|
||||
from flask import current_app
|
||||
|
||||
from api.lib.common_setting.resp_format import ErrFormat
|
||||
from api.lib.perm.acl.app import AppCRUD
|
||||
from api.lib.perm.acl.cache import RoleCache, AppCache
|
||||
from api.lib.perm.acl.permission import PermissionCRUD
|
||||
from api.lib.perm.acl.resource import ResourceTypeCRUD, ResourceCRUD
|
||||
from api.lib.perm.acl.role import RoleCRUD, RoleRelationCRUD
|
||||
from api.lib.perm.acl.user import UserCRUD
|
||||
from api.lib.perm.acl.resource import ResourceTypeCRUD, ResourceCRUD
|
||||
|
||||
|
||||
class ACLManager(object):
|
||||
@@ -79,20 +80,22 @@ class ACLManager(object):
|
||||
return role.to_dict()
|
||||
|
||||
@staticmethod
|
||||
def delete_role(_id, payload):
|
||||
def delete_role(_id):
|
||||
RoleCRUD.delete_role(_id)
|
||||
return dict(rid=_id)
|
||||
|
||||
def get_user_info(self, username):
|
||||
from api.lib.perm.acl.acl import ACLManager as ACL
|
||||
user_info = ACL().get_user_info(username, self.app_name)
|
||||
result = dict(name=user_info.get('nickname') or username,
|
||||
username=user_info.get('username') or username,
|
||||
email=user_info.get('email'),
|
||||
uid=user_info.get('uid'),
|
||||
rid=user_info.get('rid'),
|
||||
role=dict(permissions=user_info.get('parents')),
|
||||
avatar=user_info.get('avatar'))
|
||||
result = dict(
|
||||
name=user_info.get('nickname') or username,
|
||||
username=user_info.get('username') or username,
|
||||
email=user_info.get('email'),
|
||||
uid=user_info.get('uid'),
|
||||
rid=user_info.get('rid'),
|
||||
role=dict(permissions=user_info.get('parents')),
|
||||
avatar=user_info.get('avatar')
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
@@ -109,8 +112,32 @@ class ACLManager(object):
|
||||
id2perms=id2perms
|
||||
)
|
||||
|
||||
def create_resources_type(self, payload):
|
||||
payload['app_id'] = self.validate_app().id
|
||||
rt = ResourceTypeCRUD.add(**payload)
|
||||
|
||||
return rt.to_dict()
|
||||
|
||||
def update_resources_type(self, _id, payload):
|
||||
rt = ResourceTypeCRUD.update(_id, **payload)
|
||||
|
||||
return rt.to_dict()
|
||||
|
||||
def create_resource(self, payload):
|
||||
payload['app_id'] = self.validate_app().id
|
||||
resource = ResourceCRUD.add(**payload)
|
||||
|
||||
return resource.to_dict()
|
||||
|
||||
def get_resource_by_type(self, q, u, rt_id, page=1, page_size=999999):
|
||||
numfound, res = ResourceCRUD.search(q, u, self.validate_app().id, rt_id, page, page_size)
|
||||
return res
|
||||
|
||||
def grant_resource(self, rid, resource_id, perms):
|
||||
PermissionCRUD.grant(rid, perms, resource_id=resource_id, group_id=None)
|
||||
|
||||
@staticmethod
|
||||
def create_app(payload):
|
||||
rt = AppCRUD.add(**payload)
|
||||
|
||||
return rt.to_dict()
|
||||
|
@@ -1,5 +1,5 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
from api.extensions import cache
|
||||
from api.models.common_setting import CompanyInfo
|
||||
|
||||
|
||||
@@ -11,14 +11,34 @@ class CompanyInfoCRUD(object):
|
||||
|
||||
@staticmethod
|
||||
def create(**kwargs):
|
||||
return CompanyInfo.create(**kwargs)
|
||||
res = CompanyInfo.create(**kwargs)
|
||||
CompanyInfoCache.refresh(res.info)
|
||||
return res
|
||||
|
||||
@staticmethod
|
||||
def update(_id, **kwargs):
|
||||
kwargs.pop('id', None)
|
||||
existed = CompanyInfo.get_by_id(_id)
|
||||
if not existed:
|
||||
return CompanyInfoCRUD.create(**kwargs)
|
||||
existed = CompanyInfoCRUD.create(**kwargs)
|
||||
else:
|
||||
existed = existed.update(**kwargs)
|
||||
return existed
|
||||
CompanyInfoCache.refresh(existed.info)
|
||||
return existed
|
||||
|
||||
|
||||
class CompanyInfoCache(object):
|
||||
key = 'CompanyInfoCache::'
|
||||
|
||||
@classmethod
|
||||
def get(cls):
|
||||
info = cache.get(cls.key)
|
||||
if not info:
|
||||
res = CompanyInfo.get_by(first=True) or {}
|
||||
info = res.get('info', {})
|
||||
cache.set(cls.key, info)
|
||||
return info
|
||||
|
||||
@classmethod
|
||||
def refresh(cls, info):
|
||||
cache.set(cls.key, info)
|
@@ -12,3 +12,10 @@ class OperatorType(BaseEnum):
|
||||
LESS_THAN = 6
|
||||
IS_EMPTY = 7
|
||||
IS_NOT_EMPTY = 8
|
||||
|
||||
|
||||
BotNameMap = {
|
||||
'wechatApp': 'wechatBot',
|
||||
'feishuApp': 'feishuBot',
|
||||
'dingdingApp': 'dingdingBot',
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
from flask import abort
|
||||
from flask import abort, current_app
|
||||
from treelib import Tree
|
||||
from wtforms import Form
|
||||
from wtforms import IntegerField
|
||||
@@ -9,6 +9,7 @@ from wtforms import validators
|
||||
|
||||
from api.extensions import db
|
||||
from api.lib.common_setting.resp_format import ErrFormat
|
||||
from api.lib.common_setting.acl import ACLManager
|
||||
from api.lib.perm.acl.role import RoleCRUD
|
||||
from api.models.common_setting import Department, Employee
|
||||
|
||||
@@ -152,6 +153,10 @@ class DepartmentForm(Form):
|
||||
|
||||
class DepartmentCRUD(object):
|
||||
|
||||
@staticmethod
|
||||
def get_department_by_id(d_id, to_dict=True):
|
||||
return Department.get_by(first=True, department_id=d_id, to_dict=to_dict)
|
||||
|
||||
@staticmethod
|
||||
def add(**kwargs):
|
||||
DepartmentCRUD.check_department_name_unique(kwargs['department_name'])
|
||||
@@ -186,10 +191,11 @@ class DepartmentCRUD(object):
|
||||
filter(lambda d: d['department_id'] == department_parent_id, allow_p_d_id_list))
|
||||
if len(target) == 0:
|
||||
try:
|
||||
d = Department.get_by(
|
||||
dep = Department.get_by(
|
||||
first=True, to_dict=False, department_id=department_parent_id)
|
||||
name = d.department_name if d else ErrFormat.department_id_not_found.format(department_parent_id)
|
||||
name = dep.department_name if dep else ErrFormat.department_id_not_found.format(department_parent_id)
|
||||
except Exception as e:
|
||||
current_app.logger.error(str(e))
|
||||
name = ErrFormat.department_id_not_found.format(department_parent_id)
|
||||
abort(400, ErrFormat.cannot_to_be_parent_department.format(name))
|
||||
|
||||
@@ -253,7 +259,7 @@ class DepartmentCRUD(object):
|
||||
try:
|
||||
RoleCRUD.delete_role(existed.acl_rid)
|
||||
except Exception as e:
|
||||
pass
|
||||
current_app.logger.error(str(e))
|
||||
|
||||
return existed.soft_delete()
|
||||
|
||||
@@ -268,7 +274,7 @@ class DepartmentCRUD(object):
|
||||
try:
|
||||
tree.remove_subtree(department_id)
|
||||
except Exception as e:
|
||||
pass
|
||||
current_app.logger.error(str(e))
|
||||
|
||||
[allow_d_id_list.append({'department_id': int(n.identifier), 'department_name': n.tag}) for n in
|
||||
tree.all_nodes()]
|
||||
@@ -390,6 +396,125 @@ class DepartmentCRUD(object):
|
||||
[id_list.append(int(n.identifier))
|
||||
for n in tmp_tree.all_nodes()]
|
||||
except Exception as e:
|
||||
pass
|
||||
current_app.logger.error(str(e))
|
||||
|
||||
return id_list
|
||||
|
||||
|
||||
class EditDepartmentInACL(object):
|
||||
|
||||
@staticmethod
|
||||
def add_department_to_acl(department_id, op_uid):
|
||||
db_department = DepartmentCRUD.get_department_by_id(department_id, to_dict=False)
|
||||
if not db_department:
|
||||
return
|
||||
|
||||
from api.models.acl import Role
|
||||
role = Role.get_by(first=True, name=db_department.department_name, app_id=None)
|
||||
|
||||
acl = ACLManager('acl', str(op_uid))
|
||||
if role is None:
|
||||
payload = {
|
||||
'app_id': 'acl',
|
||||
'name': db_department.department_name,
|
||||
}
|
||||
role = acl.create_role(payload)
|
||||
|
||||
acl_rid = role.get('id') if role else 0
|
||||
|
||||
db_department.update(
|
||||
acl_rid=acl_rid
|
||||
)
|
||||
info = f"add_department_to_acl, acl_rid: {acl_rid}"
|
||||
current_app.logger.info(info)
|
||||
return info
|
||||
|
||||
@staticmethod
|
||||
def delete_department_from_acl(department_rids, op_uid):
|
||||
acl = ACLManager('acl', str(op_uid))
|
||||
|
||||
result = []
|
||||
|
||||
for rid in department_rids:
|
||||
try:
|
||||
acl.delete_role(rid)
|
||||
except Exception as e:
|
||||
result.append(f"delete_department_in_acl, rid: {rid}, error: {e}")
|
||||
continue
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def edit_department_name_in_acl(d_rid: int, d_name: str, op_uid: int):
|
||||
acl = ACLManager('acl', str(op_uid))
|
||||
payload = {
|
||||
'name': d_name
|
||||
}
|
||||
try:
|
||||
acl.edit_role(d_rid, payload)
|
||||
except Exception as e:
|
||||
return f"edit_department_name_in_acl, rid: {d_rid}, error: {e}"
|
||||
|
||||
return f"edit_department_name_in_acl, rid: {d_rid}, success"
|
||||
|
||||
@staticmethod
|
||||
def edit_employee_department_in_acl(e_list: list, new_d_id: int, op_uid: int):
|
||||
result = []
|
||||
new_department = DepartmentCRUD.get_department_by_id(new_d_id, False)
|
||||
if not new_department:
|
||||
result.append(f"{new_d_id} new_department is None")
|
||||
return result
|
||||
|
||||
from api.models.acl import Role
|
||||
new_role = Role.get_by(first=True, name=new_department.department_name, app_id=None)
|
||||
new_d_rid_in_acl = new_role.get('id') if new_role else 0
|
||||
if new_d_rid_in_acl == 0:
|
||||
return
|
||||
|
||||
if new_d_rid_in_acl != new_department.acl_rid:
|
||||
new_department.update(
|
||||
acl_rid=new_d_rid_in_acl
|
||||
)
|
||||
new_department_acl_rid = new_department.acl_rid if new_d_rid_in_acl == new_department.acl_rid else \
|
||||
new_d_rid_in_acl
|
||||
|
||||
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}")
|
||||
|
||||
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}")
|
||||
|
||||
return result
|
||||
|
@@ -1,8 +1,9 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
import copy
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
|
||||
import requests
|
||||
from flask import abort
|
||||
from flask_login import current_user
|
||||
from sqlalchemy import or_, literal_column, func, not_, and_
|
||||
@@ -120,6 +121,19 @@ class EmployeeCRUD(object):
|
||||
employee = CreateEmployee().create_single(**data)
|
||||
return employee.to_dict()
|
||||
|
||||
@staticmethod
|
||||
def add_employee_from_acl_created(**kwargs):
|
||||
try:
|
||||
kwargs['acl_uid'] = kwargs.pop('uid')
|
||||
kwargs['acl_rid'] = kwargs.pop('rid')
|
||||
kwargs['department_id'] = 0
|
||||
|
||||
Employee.create(
|
||||
**kwargs
|
||||
)
|
||||
except Exception as e:
|
||||
abort(400, str(e))
|
||||
|
||||
@staticmethod
|
||||
def add(**kwargs):
|
||||
try:
|
||||
@@ -164,7 +178,7 @@ class EmployeeCRUD(object):
|
||||
def edit_employee_by_uid(_uid, **kwargs):
|
||||
existed = EmployeeCRUD.get_employee_by_uid(_uid)
|
||||
try:
|
||||
user = edit_acl_user(_uid, **kwargs)
|
||||
edit_acl_user(_uid, **kwargs)
|
||||
|
||||
for column in employee_pop_columns:
|
||||
if kwargs.get(column):
|
||||
@@ -176,9 +190,9 @@ class EmployeeCRUD(object):
|
||||
|
||||
@staticmethod
|
||||
def change_password_by_uid(_uid, password):
|
||||
existed = EmployeeCRUD.get_employee_by_uid(_uid)
|
||||
EmployeeCRUD.get_employee_by_uid(_uid)
|
||||
try:
|
||||
user = edit_acl_user(_uid, password=password)
|
||||
edit_acl_user(_uid, password=password)
|
||||
except Exception as e:
|
||||
return abort(400, str(e))
|
||||
|
||||
@@ -345,9 +359,11 @@ class EmployeeCRUD(object):
|
||||
|
||||
if value and column == "last_login":
|
||||
try:
|
||||
value = datetime.strptime(value, "%Y-%m-%d %H:%M:%S")
|
||||
return datetime.strptime(value, "%Y-%m-%d %H:%M:%S")
|
||||
except Exception as e:
|
||||
abort(400, ErrFormat.datetime_format_error.format(column))
|
||||
err = f"{ErrFormat.datetime_format_error.format(column)}: {str(e)}"
|
||||
abort(400, err)
|
||||
return value
|
||||
|
||||
@staticmethod
|
||||
def get_attr_by_column(column):
|
||||
@@ -368,7 +384,7 @@ class EmployeeCRUD(object):
|
||||
relation = condition.get("relation", None)
|
||||
value = condition.get("value", None)
|
||||
|
||||
EmployeeCRUD.check_condition(column, operator, value, relation)
|
||||
value = EmployeeCRUD.check_condition(column, operator, value, relation)
|
||||
a, o = EmployeeCRUD.get_expr_by_condition(
|
||||
column, operator, value, relation)
|
||||
and_list += a
|
||||
@@ -474,6 +490,202 @@ class EmployeeCRUD(object):
|
||||
|
||||
return [r.to_dict() for r in results]
|
||||
|
||||
@staticmethod
|
||||
def remove_bind_notice_by_uid(_platform, _uid):
|
||||
existed = EmployeeCRUD.get_employee_by_uid(_uid)
|
||||
employee_data = existed.to_dict()
|
||||
|
||||
notice_info = employee_data.get('notice_info', {})
|
||||
notice_info = copy.deepcopy(notice_info) if notice_info else {}
|
||||
|
||||
notice_info[_platform] = ''
|
||||
|
||||
existed.update(
|
||||
notice_info=notice_info
|
||||
)
|
||||
return ErrFormat.notice_remove_bind_success
|
||||
|
||||
@staticmethod
|
||||
def bind_notice_by_uid(_platform, _uid):
|
||||
existed = EmployeeCRUD.get_employee_by_uid(_uid)
|
||||
mobile = existed.mobile
|
||||
if not mobile or len(mobile) == 0:
|
||||
abort(400, ErrFormat.notice_bind_err_with_empty_mobile)
|
||||
|
||||
from api.lib.common_setting.notice_config import NoticeConfigCRUD
|
||||
messenger = NoticeConfigCRUD.get_messenger_url()
|
||||
if not messenger or len(messenger) == 0:
|
||||
abort(400, ErrFormat.notice_please_config_messenger_first)
|
||||
|
||||
url = f"{messenger}/v1/uid/getbyphone"
|
||||
try:
|
||||
payload = dict(
|
||||
phone=mobile,
|
||||
sender=_platform
|
||||
)
|
||||
res = requests.post(url, json=payload)
|
||||
result = res.json()
|
||||
if res.status_code != 200:
|
||||
raise Exception(result.get('msg', ''))
|
||||
target_id = result.get('uid', '')
|
||||
|
||||
employee_data = existed.to_dict()
|
||||
|
||||
notice_info = employee_data.get('notice_info', {})
|
||||
notice_info = copy.deepcopy(notice_info) if notice_info else {}
|
||||
|
||||
notice_info[_platform] = '' if not target_id else target_id
|
||||
|
||||
existed.update(
|
||||
notice_info=notice_info
|
||||
)
|
||||
return ErrFormat.notice_bind_success
|
||||
|
||||
except Exception as e:
|
||||
return abort(400, ErrFormat.notice_bind_failed.format(str(e)))
|
||||
|
||||
@staticmethod
|
||||
def get_employee_notice_by_ids(employee_ids):
|
||||
criterion = [
|
||||
Employee.employee_id.in_(employee_ids),
|
||||
Employee.deleted == 0,
|
||||
]
|
||||
direct_columns = ['email', 'mobile']
|
||||
employees = Employee.query.filter(
|
||||
*criterion
|
||||
).all()
|
||||
results = []
|
||||
for employee in employees:
|
||||
d = employee.to_dict()
|
||||
tmp = dict(
|
||||
employee_id=employee.employee_id,
|
||||
)
|
||||
for column in direct_columns:
|
||||
tmp[column] = d.get(column, '')
|
||||
notice_info = d.get('notice_info', {})
|
||||
tmp.update(**notice_info)
|
||||
results.append(tmp)
|
||||
return results
|
||||
|
||||
@staticmethod
|
||||
def import_employee(employee_list):
|
||||
res = CreateEmployee().batch_create(employee_list)
|
||||
return res
|
||||
|
||||
@staticmethod
|
||||
def batch_edit_employee_department(employee_id_list, column_value):
|
||||
err_list = []
|
||||
employee_list = []
|
||||
for _id in employee_id_list:
|
||||
try:
|
||||
existed = EmployeeCRUD.get_employee_by_id(_id)
|
||||
employee = dict(
|
||||
e_acl_rid=existed.acl_rid,
|
||||
department_id=existed.department_id
|
||||
)
|
||||
employee_list.append(employee)
|
||||
existed.update(department_id=column_value)
|
||||
|
||||
except Exception as e:
|
||||
err_list.append({
|
||||
'employee_id': _id,
|
||||
'err': str(e),
|
||||
})
|
||||
from api.lib.common_setting.department import EditDepartmentInACL
|
||||
EditDepartmentInACL.edit_employee_department_in_acl(
|
||||
employee_list, column_value, current_user.uid
|
||||
)
|
||||
return err_list
|
||||
|
||||
@staticmethod
|
||||
def batch_edit_password_or_block_column(column_name, employee_id_list, column_value, is_acl=False):
|
||||
if column_name == 'block':
|
||||
err_list = []
|
||||
success_list = []
|
||||
for _id in employee_id_list:
|
||||
try:
|
||||
employee = EmployeeCRUD.edit_employee_block_column(
|
||||
_id, is_acl, **{column_name: column_value})
|
||||
success_list.append(employee)
|
||||
except Exception as e:
|
||||
err_list.append({
|
||||
'employee_id': _id,
|
||||
'err': str(e),
|
||||
})
|
||||
return err_list
|
||||
else:
|
||||
return EmployeeCRUD.batch_edit_column(column_name, employee_id_list, column_value, is_acl)
|
||||
|
||||
@staticmethod
|
||||
def batch_edit_column(column_name, employee_id_list, column_value, is_acl=False):
|
||||
err_list = []
|
||||
for _id in employee_id_list:
|
||||
try:
|
||||
EmployeeCRUD.edit_employee_single_column(
|
||||
_id, is_acl, **{column_name: column_value})
|
||||
except Exception as e:
|
||||
err_list.append({
|
||||
'employee_id': _id,
|
||||
'err': str(e),
|
||||
})
|
||||
|
||||
return err_list
|
||||
|
||||
@staticmethod
|
||||
def edit_employee_single_column(_id, is_acl=False, **kwargs):
|
||||
existed = EmployeeCRUD.get_employee_by_id(_id)
|
||||
if 'direct_supervisor_id' in kwargs.keys():
|
||||
if kwargs['direct_supervisor_id'] == existed.direct_supervisor_id:
|
||||
raise Exception(ErrFormat.direct_supervisor_is_not_self)
|
||||
|
||||
if is_acl:
|
||||
return edit_acl_user(existed.acl_uid, **kwargs)
|
||||
|
||||
try:
|
||||
for column in employee_pop_columns:
|
||||
if kwargs.get(column):
|
||||
kwargs.pop(column)
|
||||
|
||||
return existed.update(**kwargs)
|
||||
except Exception as e:
|
||||
return abort(400, str(e))
|
||||
|
||||
@staticmethod
|
||||
def edit_employee_block_column(_id, is_acl=False, **kwargs):
|
||||
existed = EmployeeCRUD.get_employee_by_id(_id)
|
||||
value = get_block_value(kwargs.get('block'))
|
||||
if value is True:
|
||||
check_department_director_id_or_direct_supervisor_id(_id)
|
||||
value = 1
|
||||
else:
|
||||
value = 0
|
||||
|
||||
if is_acl:
|
||||
kwargs['block'] = value
|
||||
edit_acl_user(existed.acl_uid, **kwargs)
|
||||
|
||||
existed.update(block=value)
|
||||
data = existed.to_dict()
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def batch_employee(column_name, column_value, employee_id_list):
|
||||
if column_value is None:
|
||||
abort(400, ErrFormat.value_is_required)
|
||||
if column_name in ['password', 'block']:
|
||||
return EmployeeCRUD.batch_edit_password_or_block_column(column_name, employee_id_list, column_value, True)
|
||||
|
||||
elif column_name in ['department_id']:
|
||||
return EmployeeCRUD.batch_edit_employee_department(employee_id_list, column_value)
|
||||
|
||||
elif column_name in [
|
||||
'direct_supervisor_id', 'position_name'
|
||||
]:
|
||||
return EmployeeCRUD.batch_edit_column(column_name, employee_id_list, column_value, False)
|
||||
|
||||
else:
|
||||
abort(400, ErrFormat.column_name_not_support)
|
||||
|
||||
|
||||
def get_user_map(key='uid', acl=None):
|
||||
"""
|
||||
@@ -550,7 +762,8 @@ class CreateEmployee(object):
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def get_department_by_name(self, d_name):
|
||||
@staticmethod
|
||||
def get_department_by_name(d_name):
|
||||
return Department.get_by(first=True, department_name=d_name)
|
||||
|
||||
def get_end_department_id(self, department_name_list, department_name_map):
|
||||
|
165
cmdb-api/api/lib/common_setting/notice_config.py
Normal file
165
cmdb-api/api/lib/common_setting/notice_config.py
Normal file
@@ -0,0 +1,165 @@
|
||||
import requests
|
||||
|
||||
from api.lib.common_setting.const import BotNameMap
|
||||
from api.lib.common_setting.resp_format import ErrFormat
|
||||
from api.models.common_setting import CompanyInfo, NoticeConfig
|
||||
from wtforms import Form
|
||||
from wtforms import StringField
|
||||
from wtforms import validators
|
||||
from flask import abort, current_app
|
||||
|
||||
|
||||
class NoticeConfigCRUD(object):
|
||||
|
||||
@staticmethod
|
||||
def add_notice_config(**kwargs):
|
||||
platform = kwargs.get('platform')
|
||||
NoticeConfigCRUD.check_platform(platform)
|
||||
info = kwargs.get('info', {})
|
||||
if 'name' not in info:
|
||||
info['name'] = platform
|
||||
kwargs['info'] = info
|
||||
try:
|
||||
NoticeConfigCRUD.update_messenger_config(**info)
|
||||
res = NoticeConfig.create(
|
||||
**kwargs
|
||||
)
|
||||
return res
|
||||
|
||||
except Exception as e:
|
||||
return abort(400, str(e))
|
||||
|
||||
@staticmethod
|
||||
def check_platform(platform):
|
||||
NoticeConfig.get_by(first=True, to_dict=False, platform=platform) and \
|
||||
abort(400, ErrFormat.notice_platform_existed.format(platform))
|
||||
|
||||
@staticmethod
|
||||
def edit_notice_config(_id, **kwargs):
|
||||
existed = NoticeConfigCRUD.get_notice_config_by_id(_id)
|
||||
try:
|
||||
info = kwargs.get('info', {})
|
||||
if 'name' not in info:
|
||||
info['name'] = existed.platform
|
||||
kwargs['info'] = info
|
||||
NoticeConfigCRUD.update_messenger_config(**info)
|
||||
|
||||
res = existed.update(**kwargs)
|
||||
return res
|
||||
except Exception as e:
|
||||
return abort(400, str(e))
|
||||
|
||||
@staticmethod
|
||||
def get_messenger_url():
|
||||
from api.lib.common_setting.company_info import CompanyInfoCache
|
||||
com_info = CompanyInfoCache.get()
|
||||
if not com_info:
|
||||
return
|
||||
messenger = com_info.get('messenger', '')
|
||||
if len(messenger) == 0:
|
||||
return
|
||||
if messenger[-1] == '/':
|
||||
messenger = messenger[:-1]
|
||||
return messenger
|
||||
|
||||
@staticmethod
|
||||
def update_messenger_config(**kwargs):
|
||||
try:
|
||||
messenger = NoticeConfigCRUD.get_messenger_url()
|
||||
if not messenger or len(messenger) == 0:
|
||||
raise Exception(ErrFormat.notice_please_config_messenger_first)
|
||||
|
||||
url = f"{messenger}/v1/senders"
|
||||
name = kwargs.get('name')
|
||||
bot_list = kwargs.pop('bot', None)
|
||||
for k, v in kwargs.items():
|
||||
if isinstance(v, bool):
|
||||
kwargs[k] = 'true' if v else 'false'
|
||||
else:
|
||||
kwargs[k] = str(v)
|
||||
|
||||
payload = {name: [kwargs]}
|
||||
current_app.logger.info(f"update_messenger_config: {url}, {payload}")
|
||||
res = requests.put(url, json=payload, timeout=2)
|
||||
current_app.logger.info(f"update_messenger_config: {res.status_code}, {res.text}")
|
||||
|
||||
if not bot_list or len(bot_list) == 0:
|
||||
return
|
||||
bot_name = BotNameMap.get(name)
|
||||
payload = {bot_name: bot_list}
|
||||
current_app.logger.info(f"update_messenger_config: {url}, {payload}")
|
||||
bot_res = requests.put(url, json=payload, timeout=2)
|
||||
current_app.logger.info(f"update_messenger_config: {bot_res.status_code}, {bot_res.text}")
|
||||
|
||||
except Exception as e:
|
||||
return abort(400, str(e))
|
||||
|
||||
@staticmethod
|
||||
def get_notice_config_by_id(_id):
|
||||
return NoticeConfig.get_by(first=True, to_dict=False, id=_id) or \
|
||||
abort(400,
|
||||
ErrFormat.notice_not_existed.format(_id))
|
||||
|
||||
@staticmethod
|
||||
def get_all():
|
||||
return NoticeConfig.get_by(to_dict=True)
|
||||
|
||||
@staticmethod
|
||||
def test_send_email(receive_address, **kwargs):
|
||||
messenger = NoticeConfigCRUD.get_messenger_url()
|
||||
if not messenger or len(messenger) == 0:
|
||||
abort(400, ErrFormat.notice_please_config_messenger_first)
|
||||
url = f"{messenger}/v1/message"
|
||||
|
||||
recipient_email = receive_address
|
||||
|
||||
subject = 'Test Email'
|
||||
body = 'This is a test email'
|
||||
payload = {
|
||||
"sender": 'email',
|
||||
"msgtype": "text/plain",
|
||||
"title": subject,
|
||||
"content": body,
|
||||
"tos": [recipient_email],
|
||||
}
|
||||
current_app.logger.info(f"test_send_email: {url}, {payload}")
|
||||
response = requests.post(url, json=payload)
|
||||
if response.status_code != 200:
|
||||
abort(400, response.text)
|
||||
|
||||
return 1
|
||||
|
||||
@staticmethod
|
||||
def get_app_bot():
|
||||
result = []
|
||||
for notice_app in NoticeConfig.get_by(to_dict=False):
|
||||
if notice_app.platform in ['email']:
|
||||
continue
|
||||
info = notice_app.info
|
||||
name = info.get('name', '')
|
||||
if name not in BotNameMap:
|
||||
continue
|
||||
result.append(dict(
|
||||
name=info.get('name', ''),
|
||||
label=info.get('label', ''),
|
||||
bot=info.get('bot', []),
|
||||
))
|
||||
return result
|
||||
|
||||
|
||||
class NoticeConfigForm(Form):
|
||||
platform = StringField(validators=[
|
||||
validators.DataRequired(message="平台 不能为空"),
|
||||
validators.Length(max=255),
|
||||
])
|
||||
info = StringField(validators=[
|
||||
validators.DataRequired(message="信息 不能为空"),
|
||||
validators.Length(max=255),
|
||||
])
|
||||
|
||||
|
||||
class NoticeConfigUpdateForm(Form):
|
||||
info = StringField(validators=[
|
||||
validators.DataRequired(message="信息 不能为空"),
|
||||
validators.Length(max=255),
|
||||
])
|
@@ -53,5 +53,13 @@ class ErrFormat(CommonErrFormat):
|
||||
username_is_required = "username不能为空"
|
||||
email_is_required = "邮箱不能为空"
|
||||
email_format_error = "邮箱格式错误"
|
||||
email_send_timeout = "邮件发送超时"
|
||||
|
||||
common_data_not_found = "ID {} 找不到记录"
|
||||
notice_platform_existed = "{} 已存在"
|
||||
notice_not_existed = "{} 配置项不存在"
|
||||
notice_please_config_messenger_first = "请先配置 messenger"
|
||||
notice_bind_err_with_empty_mobile = "绑定失败,手机号为空"
|
||||
notice_bind_failed = "绑定失败: {}"
|
||||
notice_bind_success = "绑定成功"
|
||||
notice_remove_bind_success = "解绑成功"
|
||||
|
@@ -10,14 +10,18 @@ from api.lib.exception import CommitException
|
||||
|
||||
class FormatMixin(object):
|
||||
def to_dict(self):
|
||||
res = dict([(k, getattr(self, k) if not isinstance(
|
||||
getattr(self, k), (datetime.datetime, datetime.date, datetime.time)) else str(
|
||||
getattr(self, k))) for k in getattr(self, "__mapper__").c.keys()])
|
||||
# FIXME: getattr(cls, "__table__").columns k.name
|
||||
res = dict()
|
||||
for k in getattr(self, "__mapper__").c.keys():
|
||||
if k in {'password', '_password', 'secret', '_secret'}:
|
||||
continue
|
||||
|
||||
res.pop('password', None)
|
||||
res.pop('_password', None)
|
||||
res.pop('secret', None)
|
||||
if k.startswith('_'):
|
||||
k = k[1:]
|
||||
|
||||
if not isinstance(getattr(self, k), (datetime.datetime, datetime.date, datetime.time)):
|
||||
res[k] = getattr(self, k)
|
||||
else:
|
||||
res[k] = str(getattr(self, k))
|
||||
|
||||
return res
|
||||
|
||||
|
@@ -4,8 +4,14 @@
|
||||
from functools import wraps
|
||||
|
||||
from flask import abort
|
||||
from flask import current_app
|
||||
from flask import request
|
||||
from sqlalchemy.exc import InvalidRequestError
|
||||
from sqlalchemy.exc import OperationalError
|
||||
from sqlalchemy.exc import PendingRollbackError
|
||||
from sqlalchemy.exc import StatementError
|
||||
|
||||
from api.extensions import db
|
||||
from api.lib.resp_format import CommonErrFormat
|
||||
|
||||
|
||||
@@ -70,3 +76,43 @@ def args_validate(model_cls, exclude_args=None):
|
||||
return wrapper
|
||||
|
||||
return decorate
|
||||
|
||||
|
||||
def reconnect_db(func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except (StatementError, OperationalError, InvalidRequestError) as e:
|
||||
error_msg = str(e)
|
||||
if 'Lost connection' in error_msg or 'reconnect until invalid transaction' in error_msg or \
|
||||
'can be emitted within this transaction' in error_msg:
|
||||
current_app.logger.info('[reconnect_db] lost connect rollback then retry')
|
||||
db.session.rollback()
|
||||
return func(*args, **kwargs)
|
||||
else:
|
||||
raise e
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def _flush_db():
|
||||
try:
|
||||
db.session.commit()
|
||||
except (StatementError, OperationalError, InvalidRequestError, PendingRollbackError):
|
||||
db.session.rollback()
|
||||
|
||||
|
||||
def flush_db(func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
_flush_db()
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def run_flush_db():
|
||||
_flush_db()
|
||||
|
72
cmdb-api/api/lib/notify.py
Normal file
72
cmdb-api/api/lib/notify.py
Normal file
@@ -0,0 +1,72 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
import json
|
||||
|
||||
import requests
|
||||
import six
|
||||
from flask import current_app
|
||||
from jinja2 import Template
|
||||
from markdownify import markdownify as md
|
||||
|
||||
from api.lib.common_setting.notice_config import NoticeConfigCRUD
|
||||
from api.lib.mail import send_mail
|
||||
|
||||
|
||||
def _request_messenger(subject, body, tos, sender, payload):
|
||||
params = dict(sender=sender, title=subject,
|
||||
tos=[to[sender] for to in tos if to.get(sender)])
|
||||
|
||||
if not params['tos']:
|
||||
raise Exception("no receivers")
|
||||
|
||||
flat_tos = []
|
||||
for i in params['tos']:
|
||||
if i.strip():
|
||||
to = Template(i).render(payload)
|
||||
if isinstance(to, list):
|
||||
flat_tos.extend(to)
|
||||
elif isinstance(to, six.string_types):
|
||||
flat_tos.append(to)
|
||||
params['tos'] = flat_tos
|
||||
|
||||
if sender == "email":
|
||||
params['msgtype'] = 'text/html'
|
||||
params['content'] = body
|
||||
else:
|
||||
params['msgtype'] = 'markdown'
|
||||
try:
|
||||
content = md("{}\n{}".format(subject or '', body or ''))
|
||||
except Exception as e:
|
||||
current_app.logger.warning("html2markdown failed: {}".format(e))
|
||||
content = "{}\n{}".format(subject or '', body or '')
|
||||
|
||||
params['content'] = json.dumps(dict(content=content))
|
||||
|
||||
url = current_app.config.get('MESSENGER_URL') or NoticeConfigCRUD.get_messenger_url()
|
||||
if not url:
|
||||
raise Exception("no messenger url")
|
||||
|
||||
if not url.endswith("message"):
|
||||
url = "{}/v1/message".format(url)
|
||||
|
||||
resp = requests.post(url, json=params)
|
||||
if resp.status_code != 200:
|
||||
raise Exception(resp.text)
|
||||
|
||||
return resp.text
|
||||
|
||||
|
||||
def notify_send(subject, body, methods, tos, payload=None):
|
||||
payload = payload or {}
|
||||
payload = {k: v or '' for k, v in payload.items()}
|
||||
subject = Template(subject).render(payload)
|
||||
body = Template(body).render(payload)
|
||||
|
||||
res = ''
|
||||
for method in methods:
|
||||
if method == "email" and not current_app.config.get('USE_MESSENGER', True):
|
||||
send_mail(None, [Template(to.get('email')).render(payload) for to in tos], subject, body)
|
||||
|
||||
res += (_request_messenger(subject, body, tos, method, payload) + "\n")
|
||||
|
||||
return res
|
@@ -4,7 +4,7 @@
|
||||
import msgpack
|
||||
|
||||
from api.extensions import cache
|
||||
from api.extensions import db
|
||||
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
|
||||
@@ -221,9 +221,9 @@ class RoleRelationCache(object):
|
||||
return msgpack.loads(r_g, raw=False)
|
||||
|
||||
@classmethod
|
||||
@flush_db
|
||||
def rebuild(cls, rid, app_id):
|
||||
cls.clean(rid, app_id)
|
||||
db.session.remove()
|
||||
|
||||
cls.get_parent_ids(rid, app_id)
|
||||
cls.get_child_ids(rid, app_id)
|
||||
@@ -235,9 +235,9 @@ class RoleRelationCache(object):
|
||||
cls.get_resources2(rid, app_id)
|
||||
|
||||
@classmethod
|
||||
@flush_db
|
||||
def rebuild2(cls, rid, app_id):
|
||||
cache.delete(cls.PREFIX_RESOURCES2.format(rid, app_id))
|
||||
db.session.remove()
|
||||
cls.get_resources2(rid, app_id)
|
||||
|
||||
@classmethod
|
||||
|
@@ -260,7 +260,8 @@ class ResourceCRUD(object):
|
||||
numfound = query.count()
|
||||
res = [i.to_dict() for i in query.offset((page - 1) * page_size).limit(page_size)]
|
||||
for i in res:
|
||||
i['user'] = UserCache.get(i['uid']).nickname if i['uid'] else ''
|
||||
user = UserCache.get(i['uid']) if i['uid'] else ''
|
||||
i['user'] = user and user.nickname
|
||||
|
||||
return numfound, res
|
||||
|
||||
@@ -275,7 +276,6 @@ class ResourceCRUD(object):
|
||||
|
||||
from api.tasks.acl import apply_trigger
|
||||
triggers = TriggerCRUD.match_triggers(app_id, r.name, r.resource_type_id, uid)
|
||||
current_app.logger.info(triggers)
|
||||
for trigger in triggers:
|
||||
# auto trigger should be no uid
|
||||
apply_trigger.apply_async(args=(trigger.id,),
|
||||
|
@@ -10,9 +10,7 @@ from sqlalchemy import or_
|
||||
|
||||
from api.extensions import db
|
||||
from api.lib.perm.acl.app import AppCRUD
|
||||
from api.lib.perm.acl.audit import AuditCRUD
|
||||
from api.lib.perm.acl.audit import AuditOperateType
|
||||
from api.lib.perm.acl.audit import AuditScope
|
||||
from api.lib.perm.acl.audit import AuditCRUD, AuditOperateType, AuditScope
|
||||
from api.lib.perm.acl.cache import AppCache
|
||||
from api.lib.perm.acl.cache import HasResourceRoleCache
|
||||
from api.lib.perm.acl.cache import RoleCache
|
||||
@@ -71,16 +69,16 @@ class RoleRelationCRUD(object):
|
||||
@staticmethod
|
||||
def get_parent_ids(rid, app_id):
|
||||
if app_id is not None:
|
||||
return ([i.parent_id for i in RoleRelation.get_by(child_id=rid, app_id=app_id, to_dict=False)] +
|
||||
[i.parent_id for i in RoleRelation.get_by(child_id=rid, app_id=None, to_dict=False)])
|
||||
return [i.parent_id for i in RoleRelation.get_by(child_id=rid, app_id=app_id, to_dict=False)] + \
|
||||
[i.parent_id for i in RoleRelation.get_by(child_id=rid, app_id=None, to_dict=False)]
|
||||
else:
|
||||
return [i.parent_id for i in RoleRelation.get_by(child_id=rid, app_id=app_id, to_dict=False)]
|
||||
|
||||
@staticmethod
|
||||
def get_child_ids(rid, app_id):
|
||||
if app_id is not None:
|
||||
return ([i.child_id for i in RoleRelation.get_by(parent_id=rid, app_id=app_id, to_dict=False)] +
|
||||
[i.child_id for i in RoleRelation.get_by(parent_id=rid, app_id=None, to_dict=False)])
|
||||
return [i.child_id for i in RoleRelation.get_by(parent_id=rid, app_id=app_id, to_dict=False)] + \
|
||||
[i.child_id for i in RoleRelation.get_by(parent_id=rid, app_id=None, to_dict=False)]
|
||||
else:
|
||||
return [i.child_id for i in RoleRelation.get_by(parent_id=rid, app_id=app_id, to_dict=False)]
|
||||
|
||||
@@ -215,7 +213,6 @@ class RoleCRUD(object):
|
||||
|
||||
@staticmethod
|
||||
def search(q, app_id, page=1, page_size=None, user_role=True, is_all=False, user_only=False):
|
||||
|
||||
if user_only: # only user role
|
||||
query = db.session.query(Role).filter(Role.deleted.is_(False)).filter(Role.uid.isnot(None))
|
||||
|
||||
@@ -273,6 +270,13 @@ class RoleCRUD(object):
|
||||
RoleCache.clean(rid)
|
||||
|
||||
role = role.update(**kwargs)
|
||||
|
||||
if origin['uid'] and kwargs.get('name') and kwargs.get('name') != origin['name']:
|
||||
from api.models.acl import User
|
||||
user = User.get_by(uid=origin['uid'], first=True, to_dict=False)
|
||||
if user:
|
||||
user.update(username=kwargs['name'])
|
||||
|
||||
AuditCRUD.add_role_log(role.app_id, AuditOperateType.update,
|
||||
AuditScope.role, role.id, origin, role.to_dict(), {},
|
||||
)
|
||||
@@ -291,12 +295,11 @@ class RoleCRUD(object):
|
||||
from api.lib.perm.acl.acl import is_admin
|
||||
|
||||
role = Role.get_by_id(rid) or abort(404, ErrFormat.role_not_found.format("rid={}".format(rid)))
|
||||
|
||||
not force and role.uid and abort(400, ErrFormat.user_role_delete_invalid)
|
||||
|
||||
if not role.app_id and not is_admin():
|
||||
return abort(403, ErrFormat.admin_required)
|
||||
|
||||
not force and role.uid and abort(400, ErrFormat.user_role_delete_invalid)
|
||||
|
||||
origin = role.to_dict()
|
||||
|
||||
child_ids = []
|
||||
@@ -305,20 +308,18 @@ class RoleCRUD(object):
|
||||
|
||||
for i in RoleRelation.get_by(parent_id=rid, to_dict=False):
|
||||
child_ids.append(i.child_id)
|
||||
i.soft_delete(commit=False)
|
||||
i.soft_delete()
|
||||
|
||||
for i in RoleRelation.get_by(child_id=rid, to_dict=False):
|
||||
parent_ids.append(i.parent_id)
|
||||
i.soft_delete(commit=False)
|
||||
i.soft_delete()
|
||||
|
||||
role_permissions = []
|
||||
for i in RolePermission.get_by(rid=rid, to_dict=False):
|
||||
role_permissions.append(i.to_dict())
|
||||
i.soft_delete(commit=False)
|
||||
i.soft_delete()
|
||||
|
||||
role.soft_delete(commit=False)
|
||||
|
||||
db.session.commit()
|
||||
role.soft_delete()
|
||||
|
||||
role_rebuild.apply_async(args=(recursive_child_ids, role.app_id), queue=ACL_QUEUE)
|
||||
|
||||
|
@@ -58,10 +58,14 @@ class UserCRUD(object):
|
||||
kwargs['employee_id'] = '{0:04d}'.format(biggest_employee_id + 1)
|
||||
user = User.create(**kwargs)
|
||||
|
||||
RoleCRUD.add_role(user.username, uid=user.uid)
|
||||
role = RoleCRUD.add_role(user.username, uid=user.uid)
|
||||
AuditCRUD.add_role_log(None, AuditOperateType.create,
|
||||
AuditScope.user, user.uid, {}, user.to_dict(), {}, {}
|
||||
)
|
||||
from api.lib.common_setting.employee import EmployeeCRUD
|
||||
payload = {column: getattr(user, column) for column in ['uid', 'username', 'nickname', 'email', 'block']}
|
||||
payload['rid'] = role.id
|
||||
EmployeeCRUD.add_employee_from_acl_created(**payload)
|
||||
|
||||
return user
|
||||
|
||||
|
1
cmdb-api/api/lib/secrets/__init__.py
Normal file
1
cmdb-api/api/lib/secrets/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# -*- coding:utf-8 -*-
|
430
cmdb-api/api/lib/secrets/inner.py
Normal file
430
cmdb-api/api/lib/secrets/inner.py
Normal file
@@ -0,0 +1,430 @@
|
||||
import os
|
||||
import secrets
|
||||
import sys
|
||||
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 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.ciphers.aead import AESGCM
|
||||
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||||
from flask import current_app
|
||||
|
||||
global_iv_length = 16
|
||||
global_key_shares = 5 # Number of generated key shares
|
||||
global_key_threshold = 3 # Minimum number of shares required to rebuild the key
|
||||
|
||||
backend_root_key_name = "root_key"
|
||||
backend_encrypt_key_name = "encrypt_key"
|
||||
backend_root_key_salt_name = "root_key_salt"
|
||||
backend_encrypt_key_salt_name = "encrypt_key_salt"
|
||||
backend_seal_key = "seal_status"
|
||||
success = "success"
|
||||
seal_status = True
|
||||
|
||||
|
||||
def string_to_bytes(value):
|
||||
if isinstance(value, bytes):
|
||||
return value
|
||||
if sys.version_info.major == 2:
|
||||
byte_string = value
|
||||
else:
|
||||
byte_string = value.encode("utf-8")
|
||||
|
||||
return byte_string
|
||||
|
||||
|
||||
class Backend:
|
||||
def __init__(self, backend=None):
|
||||
self.backend = backend
|
||||
|
||||
def get(self, key):
|
||||
return self.backend.get(key)
|
||||
|
||||
def add(self, key, value):
|
||||
return self.backend.add(key, value)
|
||||
|
||||
def update(self, key, value):
|
||||
return self.backend.update(key, value)
|
||||
|
||||
|
||||
class KeyManage:
|
||||
|
||||
def __init__(self, trigger=None, backend=None):
|
||||
self.trigger = trigger
|
||||
self.backend = backend
|
||||
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.trigger = app.config.get("INNER_TRIGGER_TOKEN")
|
||||
if not self.trigger:
|
||||
return
|
||||
|
||||
self.backend = backend
|
||||
resp = self.auto_unseal()
|
||||
self.print_response(resp)
|
||||
|
||||
def hash_root_key(self, value):
|
||||
algorithm = hashes.SHA256()
|
||||
salt = self.backend.get(backend_root_key_salt_name)
|
||||
if not salt:
|
||||
salt = secrets.token_hex(16)
|
||||
msg, ok = self.backend.add(backend_root_key_salt_name, salt)
|
||||
if not ok:
|
||||
return msg, ok
|
||||
|
||||
kdf = PBKDF2HMAC(
|
||||
algorithm=algorithm,
|
||||
length=32,
|
||||
salt=string_to_bytes(salt),
|
||||
iterations=100000,
|
||||
)
|
||||
key = kdf.derive(string_to_bytes(value))
|
||||
|
||||
return b64encode(key).decode('utf-8'), True
|
||||
|
||||
def generate_encrypt_key(self, key):
|
||||
algorithm = hashes.SHA256()
|
||||
salt = self.backend.get(backend_encrypt_key_salt_name)
|
||||
if not salt:
|
||||
salt = secrets.token_hex(32)
|
||||
|
||||
kdf = PBKDF2HMAC(
|
||||
algorithm=algorithm,
|
||||
length=32,
|
||||
salt=string_to_bytes(salt),
|
||||
iterations=100000,
|
||||
backend=default_backend()
|
||||
)
|
||||
key = kdf.derive(string_to_bytes(key))
|
||||
msg, ok = self.backend.add(backend_encrypt_key_salt_name, salt)
|
||||
if ok:
|
||||
return b64encode(key).decode('utf-8'), ok
|
||||
else:
|
||||
return msg, ok
|
||||
|
||||
@classmethod
|
||||
def generate_keys(cls, secret):
|
||||
shares = Shamir.split(global_key_threshold, global_key_shares, secret, False)
|
||||
new_shares = []
|
||||
for share in shares:
|
||||
t = [i for i in share[1]] + [ord(i) for i in "{:0>2}".format(share[0])]
|
||||
new_shares.append(b64encode(bytes(t)))
|
||||
|
||||
return new_shares
|
||||
|
||||
def is_valid_root_key(self, root_key):
|
||||
root_key_hash, ok = self.hash_root_key(root_key)
|
||||
if not ok:
|
||||
return root_key_hash, ok
|
||||
backend_root_key_hash = self.backend.get(backend_root_key_name)
|
||||
if not backend_root_key_hash:
|
||||
return "should init firstly", False
|
||||
elif backend_root_key_hash != root_key_hash:
|
||||
return "invalid root key", False
|
||||
else:
|
||||
return "", True
|
||||
|
||||
def auth_root_secret(self, root_key):
|
||||
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"
|
||||
}
|
||||
|
||||
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")
|
||||
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"
|
||||
}
|
||||
|
||||
def unseal(self, key):
|
||||
if not self.is_seal():
|
||||
return {
|
||||
"message": "current status is unseal, skip",
|
||||
"status": "skip"
|
||||
}
|
||||
|
||||
try:
|
||||
t = [i for i in b64decode(key)]
|
||||
v = (int("".join([chr(i) for i in t[-2:]])), bytes(t[:-2]))
|
||||
shares = current_app.config.get("secrets_shares", [])
|
||||
if v not in shares:
|
||||
shares.append(v)
|
||||
current_app.config["secrets_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))
|
||||
else:
|
||||
return {
|
||||
"message": "waiting for inputting other unseal key {0}/{1}".format(len(shares),
|
||||
global_key_threshold),
|
||||
"status": "waiting"
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"message": "invalid token: " + str(e),
|
||||
"status": "failed"
|
||||
}
|
||||
|
||||
def generate_unseal_keys(self):
|
||||
info = self.backend.get(backend_root_key_name)
|
||||
if info:
|
||||
return "already exist", [], False
|
||||
|
||||
secret = AESGCM.generate_key(128)
|
||||
shares = self.generate_keys(secret)
|
||||
|
||||
return b64encode(secret), shares, True
|
||||
|
||||
def init(self):
|
||||
"""
|
||||
init the master key, unseal key and store in backend
|
||||
:return:
|
||||
"""
|
||||
root_key = self.backend.get(backend_root_key_name)
|
||||
if root_key:
|
||||
return {"message": "already init, skip", "status": "skip"}, False
|
||||
else:
|
||||
root_key, shares, status = self.generate_unseal_keys()
|
||||
if not status:
|
||||
return {"message": root_key, "status": "failed"}, False
|
||||
|
||||
# hash root key and store in backend
|
||||
root_key_hash, ok = self.hash_root_key(root_key)
|
||||
if not ok:
|
||||
return {"message": root_key_hash, "status": "failed"}, False
|
||||
|
||||
msg, ok = self.backend.add(backend_root_key_name, root_key_hash)
|
||||
if not ok:
|
||||
return {"message": msg, "status": "failed"}, False
|
||||
|
||||
# generate encrypt key from root_key and store in backend
|
||||
encrypt_key, ok = self.generate_encrypt_key(root_key)
|
||||
if not ok:
|
||||
return {"message": encrypt_key, "status": "failed"}
|
||||
|
||||
encrypt_key_aes, status = InnerCrypt.aes_encrypt(root_key, encrypt_key)
|
||||
if not status:
|
||||
return {"message": encrypt_key_aes, "status": "failed"}
|
||||
|
||||
msg, ok = self.backend.add(backend_encrypt_key_name, encrypt_key_aes)
|
||||
if not ok:
|
||||
return {"message": msg, "status": "failed"}, False
|
||||
msg, ok = self.backend.add(backend_seal_key, "open")
|
||||
if not ok:
|
||||
return {"message": msg, "status": "failed"}, False
|
||||
current_app.config["secrets_root_key"] = root_key
|
||||
current_app.config["secrets_encrypt_key"] = encrypt_key
|
||||
self.print_token(shares, root_token=root_key)
|
||||
|
||||
return {"message": "OK",
|
||||
"details": {
|
||||
"root_token": root_key,
|
||||
"seal_tokens": shares,
|
||||
}}, True
|
||||
|
||||
def auto_unseal(self):
|
||||
if not self.trigger:
|
||||
return {
|
||||
"message": "trigger config is empty, skip",
|
||||
"status": "skip"
|
||||
}
|
||||
|
||||
if self.trigger.startswith("http"):
|
||||
return {
|
||||
"message": "todo in next step, skip",
|
||||
"status": "skip"
|
||||
}
|
||||
# TODO
|
||||
elif len(self.trigger.strip()) == 24:
|
||||
res = self.auth_root_secret(self.trigger.encode())
|
||||
if res.get("status") == success:
|
||||
return {
|
||||
"message": success,
|
||||
"status": success
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"message": res.get("message"),
|
||||
"status": "failed"
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"message": "trigger config is invalid, skip",
|
||||
"status": "skip"
|
||||
}
|
||||
|
||||
def seal(self, root_key):
|
||||
root_key = root_key.encode()
|
||||
msg, ok = self.is_valid_root_key(root_key)
|
||||
if not ok:
|
||||
return {
|
||||
"message": msg,
|
||||
"status": "failed"
|
||||
}
|
||||
else:
|
||||
msg, ok = self.backend.update(backend_seal_key, "block")
|
||||
if not ok:
|
||||
return {
|
||||
"message": msg,
|
||||
"status": "failed",
|
||||
}
|
||||
current_app.config["secrets_root_key"] = ''
|
||||
current_app.config["secrets_encrypt_key"] = ''
|
||||
return {
|
||||
"message": success,
|
||||
"status": success
|
||||
}
|
||||
|
||||
def is_seal(self):
|
||||
"""
|
||||
If there is no initialization or the root key is inconsistent, it is considered to be in a sealed state.
|
||||
:return:
|
||||
"""
|
||||
secrets_root_key = current_app.config.get("secrets_root_key")
|
||||
msg, ok = self.is_valid_root_key(secrets_root_key)
|
||||
if not ok:
|
||||
return {"message": msg, "status": "failed"}
|
||||
status = self.backend.get(backend_seal_key)
|
||||
return status == "block"
|
||||
|
||||
@classmethod
|
||||
def print_token(cls, shares, root_token):
|
||||
"""
|
||||
data: {"message": "OK",
|
||||
"details": {
|
||||
"root_token": root_key,
|
||||
"seal_tokens": shares,
|
||||
}}
|
||||
"""
|
||||
colorama_init()
|
||||
print(Style.BRIGHT, "Please be sure to store the Unseal Key in a secure location and avoid losing it."
|
||||
" The Unseal Key is required to unseal the system every time when it restarts."
|
||||
" Successful unsealing is necessary to enable the password feature." + Style.RESET_ALL)
|
||||
|
||||
for i, v in enumerate(shares):
|
||||
print(
|
||||
"unseal token " + str(i + 1) + ": " + Fore.RED + Back.BLACK + v.decode("utf-8") + Style.RESET_ALL)
|
||||
print()
|
||||
|
||||
print(Fore.GREEN + "root token: " + root_token.decode("utf-8") + Style.RESET_ALL)
|
||||
|
||||
@classmethod
|
||||
def print_response(cls, data):
|
||||
status = data.get("status", "")
|
||||
message = data.get("message", "")
|
||||
status_colors = {
|
||||
"skip": Style.BRIGHT,
|
||||
"failed": Fore.RED,
|
||||
"waiting": Fore.YELLOW,
|
||||
}
|
||||
print(status_colors.get(status, Fore.GREEN), message, Style.RESET_ALL)
|
||||
|
||||
|
||||
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"))
|
||||
|
||||
def encrypt(self, plaintext):
|
||||
"""
|
||||
encrypt method contain aes currently
|
||||
"""
|
||||
return self.aes_encrypt(self.encrypt_key, plaintext)
|
||||
|
||||
def decrypt(self, ciphertext):
|
||||
"""
|
||||
decrypt method contain aes currently
|
||||
"""
|
||||
return self.aes_decrypt(self.encrypt_key, ciphertext)
|
||||
|
||||
@classmethod
|
||||
def aes_encrypt(cls, key, plaintext):
|
||||
if isinstance(plaintext, str):
|
||||
plaintext = string_to_bytes(plaintext)
|
||||
iv = os.urandom(global_iv_length)
|
||||
try:
|
||||
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
|
||||
encryptor = cipher.encryptor()
|
||||
v_padder = padding.PKCS7(algorithms.AES.block_size).padder()
|
||||
padded_plaintext = v_padder.update(plaintext) + v_padder.finalize()
|
||||
ciphertext = encryptor.update(padded_plaintext) + encryptor.finalize()
|
||||
|
||||
return b64encode(iv + ciphertext).decode("utf-8"), True
|
||||
except Exception as e:
|
||||
return str(e), False
|
||||
|
||||
@classmethod
|
||||
def aes_decrypt(cls, key, ciphertext):
|
||||
try:
|
||||
s = b64decode(ciphertext.encode("utf-8"))
|
||||
iv = s[:global_iv_length]
|
||||
ciphertext = s[global_iv_length:]
|
||||
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
|
||||
decrypter = cipher.decryptor()
|
||||
decrypted_padded_plaintext = decrypter.update(ciphertext) + decrypter.finalize()
|
||||
unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
|
||||
plaintext = unpadder.update(decrypted_padded_plaintext) + unpadder.finalize()
|
||||
|
||||
return plaintext.decode('utf-8'), True
|
||||
except Exception as e:
|
||||
return str(e), False
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
km = KeyManage()
|
||||
# info, shares, status = km.generate_unseal_keys()
|
||||
# print(info, shares, status)
|
||||
# print("..................")
|
||||
# for i in shares:
|
||||
# print(b64encode(i[1]).decode())
|
||||
|
||||
res1, ok1 = km.init()
|
||||
if not ok1:
|
||||
print(res1)
|
||||
# for j in res["details"]["seal_tokens"]:
|
||||
# r = km.unseal(j)
|
||||
# if r["status"] != "waiting":
|
||||
# if r["status"] != "success":
|
||||
# print("r........", r)
|
||||
# else:
|
||||
# print(r)
|
||||
# break
|
||||
|
||||
t_plaintext = b"Hello, World!" # The plaintext to encrypt
|
||||
c = InnerCrypt()
|
||||
t_ciphertext, status1 = c.encrypt(t_plaintext)
|
||||
print("Ciphertext:", t_ciphertext)
|
||||
decrypted_plaintext, status2 = c.decrypt(t_ciphertext)
|
||||
print("Decrypted plaintext:", decrypted_plaintext)
|
35
cmdb-api/api/lib/secrets/secrets.py
Normal file
35
cmdb-api/api/lib/secrets/secrets.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from api.models.cmdb import InnerKV
|
||||
|
||||
|
||||
class InnerKVManger(object):
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def add(cls, key, value):
|
||||
data = {"key": key, "value": value}
|
||||
res = InnerKV.create(**data)
|
||||
if res.key == key:
|
||||
return "success", True
|
||||
|
||||
return "add failed", False
|
||||
|
||||
@classmethod
|
||||
def get(cls, key):
|
||||
res = InnerKV.get_by(first=True, to_dict=False, key=key)
|
||||
if not res:
|
||||
return None
|
||||
|
||||
return res.value
|
||||
|
||||
@classmethod
|
||||
def update(cls, key, value):
|
||||
res = InnerKV.get_by(first=True, to_dict=False, key=key)
|
||||
if not res:
|
||||
return cls.add(key, value)
|
||||
|
||||
t = res.update(value=value)
|
||||
if t.key == key:
|
||||
return "success", True
|
||||
|
||||
return "update failed", True
|
141
cmdb-api/api/lib/secrets/vault.py
Normal file
141
cmdb-api/api/lib/secrets/vault.py
Normal file
@@ -0,0 +1,141 @@
|
||||
from base64 import b64decode
|
||||
from base64 import b64encode
|
||||
|
||||
import hvac
|
||||
|
||||
|
||||
class VaultClient:
|
||||
def __init__(self, base_url, token, mount_path='cmdb'):
|
||||
self.client = hvac.Client(url=base_url, token=token)
|
||||
self.mount_path = mount_path
|
||||
|
||||
def create_app_role(self, role_name, policies):
|
||||
resp = self.client.create_approle(role_name, policies=policies)
|
||||
|
||||
return resp == 200
|
||||
|
||||
def delete_app_role(self, role_name):
|
||||
resp = self.client.delete_approle(role_name)
|
||||
|
||||
return resp == 204
|
||||
|
||||
def update_app_role_policies(self, role_name, policies):
|
||||
resp = self.client.update_approle_role(role_name, policies=policies)
|
||||
|
||||
return resp == 204
|
||||
|
||||
def get_app_role(self, role_name):
|
||||
resp = self.client.get_approle(role_name)
|
||||
resp.json()
|
||||
if resp.status_code == 200:
|
||||
return resp.json
|
||||
else:
|
||||
return {}
|
||||
|
||||
def enable_secrets_engine(self):
|
||||
resp = self.client.sys.enable_secrets_engine('kv', path=self.mount_path)
|
||||
resp_01 = self.client.sys.enable_secrets_engine('transit')
|
||||
|
||||
if resp.status_code == 200 and resp_01.status_code == 200:
|
||||
return resp.json
|
||||
else:
|
||||
return {}
|
||||
|
||||
def encrypt(self, plaintext):
|
||||
response = self.client.secrets.transit.encrypt_data(name='transit-key', plaintext=plaintext)
|
||||
ciphertext = response['data']['ciphertext']
|
||||
|
||||
return ciphertext
|
||||
|
||||
# decrypt data
|
||||
def decrypt(self, ciphertext):
|
||||
response = self.client.secrets.transit.decrypt_data(name='transit-key', ciphertext=ciphertext)
|
||||
plaintext = response['data']['plaintext']
|
||||
|
||||
return plaintext
|
||||
|
||||
def write(self, path, data, encrypt=None):
|
||||
if encrypt:
|
||||
for k, v in data.items():
|
||||
data[k] = self.encrypt(self.encode_base64(v))
|
||||
response = self.client.secrets.kv.v2.create_or_update_secret(
|
||||
path=path,
|
||||
secret=data,
|
||||
mount_point=self.mount_path
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
# read data
|
||||
def read(self, path, decrypt=True):
|
||||
try:
|
||||
response = self.client.secrets.kv.v2.read_secret_version(
|
||||
path=path, raise_on_deleted_version=False, mount_point=self.mount_path
|
||||
)
|
||||
except Exception as e:
|
||||
return str(e), False
|
||||
data = response['data']['data']
|
||||
if decrypt:
|
||||
try:
|
||||
for k, v in data.items():
|
||||
data[k] = self.decode_base64(self.decrypt(v))
|
||||
except:
|
||||
return data, True
|
||||
|
||||
return data, True
|
||||
|
||||
# update data
|
||||
def update(self, path, data, overwrite=True, encrypt=True):
|
||||
if encrypt:
|
||||
for k, v in data.items():
|
||||
data[k] = self.encrypt(self.encode_base64(v))
|
||||
if overwrite:
|
||||
response = self.client.secrets.kv.v2.create_or_update_secret(
|
||||
path=path,
|
||||
secret=data,
|
||||
mount_point=self.mount_path
|
||||
)
|
||||
else:
|
||||
response = self.client.secrets.kv.v2.patch(path=path, secret=data, mount_point=self.mount_path)
|
||||
|
||||
return response
|
||||
|
||||
# delete data
|
||||
def delete(self, path):
|
||||
response = self.client.secrets.kv.v2.delete_metadata_and_all_versions(
|
||||
path=path,
|
||||
mount_point=self.mount_path
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
# Base64 encode
|
||||
@classmethod
|
||||
def encode_base64(cls, data):
|
||||
encoded_bytes = b64encode(data.encode())
|
||||
encoded_string = encoded_bytes.decode()
|
||||
|
||||
return encoded_string
|
||||
|
||||
# Base64 decode
|
||||
@classmethod
|
||||
def decode_base64(cls, encoded_string):
|
||||
decoded_bytes = b64decode(encoded_string)
|
||||
decoded_string = decoded_bytes.decode()
|
||||
|
||||
return decoded_string
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
_base_url = "http://localhost:8200"
|
||||
_token = "your token"
|
||||
|
||||
_path = "test001"
|
||||
# Example
|
||||
sdk = VaultClient(_base_url, _token)
|
||||
# sdk.enable_secrets_engine()
|
||||
_data = {"key1": "value1", "key2": "value2", "key3": "value3"}
|
||||
_data = sdk.update(_path, _data, overwrite=True, encrypt=True)
|
||||
print(_data)
|
||||
_data = sdk.read(_path, decrypt=True)
|
||||
print(_data)
|
@@ -12,6 +12,9 @@ from Crypto.Cipher import AES
|
||||
from elasticsearch import Elasticsearch
|
||||
from flask import current_app
|
||||
|
||||
from api.lib.secrets.inner import InnerCrypt
|
||||
from api.lib.secrets.inner import KeyManage
|
||||
|
||||
|
||||
class BaseEnum(object):
|
||||
_ALL_ = set() # type: Set[str]
|
||||
@@ -286,3 +289,33 @@ class AESCrypto(object):
|
||||
text_decrypted = cipher.decrypt(encode_bytes)
|
||||
|
||||
return cls.unpad(text_decrypted).decode('utf8')
|
||||
|
||||
|
||||
class Crypto(AESCrypto):
|
||||
@classmethod
|
||||
def encrypt(cls, data):
|
||||
from api.lib.secrets.secrets import InnerKVManger
|
||||
|
||||
if not KeyManage(backend=InnerKVManger()).is_seal():
|
||||
res, status = InnerCrypt().encrypt(data)
|
||||
if status:
|
||||
return res
|
||||
|
||||
return AESCrypto().encrypt(data)
|
||||
|
||||
@classmethod
|
||||
def decrypt(cls, data):
|
||||
from api.lib.secrets.secrets import InnerKVManger
|
||||
|
||||
if not KeyManage(backend=InnerKVManger()).is_seal():
|
||||
try:
|
||||
res, status = InnerCrypt().decrypt(data)
|
||||
if status:
|
||||
return res
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
return AESCrypto().decrypt(data)
|
||||
except:
|
||||
return data
|
||||
|
109
cmdb-api/api/lib/webhook.py
Normal file
109
cmdb-api/api/lib/webhook.py
Normal file
@@ -0,0 +1,109 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
import json
|
||||
from functools import partial
|
||||
|
||||
import requests
|
||||
from jinja2 import Template
|
||||
from requests.auth import HTTPBasicAuth
|
||||
from requests_oauthlib import OAuth2Session
|
||||
|
||||
|
||||
class BearerAuth(requests.auth.AuthBase):
|
||||
def __init__(self, token):
|
||||
self.token = token
|
||||
|
||||
def __call__(self, r):
|
||||
r.headers["authorization"] = "Bearer {}".format(self.token)
|
||||
return r
|
||||
|
||||
|
||||
def _wrap_auth(**kwargs):
|
||||
auth_type = (kwargs.get('type') or "").lower()
|
||||
if auth_type == "basicauth":
|
||||
return HTTPBasicAuth(kwargs.get('username'), kwargs.get('password'))
|
||||
|
||||
elif auth_type == "bearer":
|
||||
return BearerAuth(kwargs.get('token'))
|
||||
|
||||
elif auth_type == 'oauth2.0':
|
||||
client_id = kwargs.get('client_id')
|
||||
client_secret = kwargs.get('client_secret')
|
||||
authorization_base_url = kwargs.get('authorization_base_url')
|
||||
token_url = kwargs.get('token_url')
|
||||
redirect_url = kwargs.get('redirect_url')
|
||||
scope = kwargs.get('scope')
|
||||
|
||||
oauth2_session = OAuth2Session(client_id, scope=scope or None)
|
||||
oauth2_session.authorization_url(authorization_base_url)
|
||||
|
||||
oauth2_session.fetch_token(token_url, client_secret=client_secret, authorization_response=redirect_url)
|
||||
|
||||
return oauth2_session
|
||||
|
||||
elif auth_type == "apikey":
|
||||
return HTTPBasicAuth(kwargs.get('key'), kwargs.get('value'))
|
||||
|
||||
|
||||
def webhook_request(webhook, payload):
|
||||
"""
|
||||
|
||||
:param webhook:
|
||||
{
|
||||
"url": "https://veops.cn"
|
||||
"method": "GET|POST|PUT|DELETE"
|
||||
"body": {},
|
||||
"headers": {
|
||||
"Content-Type": "Application/json"
|
||||
},
|
||||
"parameters": {
|
||||
"key": "value"
|
||||
},
|
||||
"authorization": {
|
||||
"type": "BasicAuth|Bearer|OAuth2.0|APIKey",
|
||||
"password": "mmmm", # BasicAuth
|
||||
"username": "bbb", # BasicAuth
|
||||
|
||||
"token": "xxx", # Bearer
|
||||
|
||||
"key": "xxx", # APIKey
|
||||
"value": "xxx", # APIKey
|
||||
|
||||
"client_id": "xxx", # OAuth2.0
|
||||
"client_secret": "xxx", # OAuth2.0
|
||||
"authorization_base_url": "xxx", # OAuth2.0
|
||||
"token_url": "xxx", # OAuth2.0
|
||||
"redirect_url": "xxx", # OAuth2.0
|
||||
"scope": "xxx" # OAuth2.0
|
||||
}
|
||||
}
|
||||
:param payload:
|
||||
:return:
|
||||
"""
|
||||
assert webhook.get('url') is not None
|
||||
|
||||
payload = {k: v or '' for k, v in payload.items()}
|
||||
|
||||
url = Template(webhook['url']).render(payload)
|
||||
|
||||
params = webhook.get('parameters') or None
|
||||
if isinstance(params, dict):
|
||||
params = json.loads(Template(json.dumps(params)).render(payload))
|
||||
|
||||
headers = json.loads(Template(json.dumps(webhook.get('headers') or {})).render(payload))
|
||||
|
||||
data = Template(json.dumps(webhook.get('body', ''))).render(payload)
|
||||
auth = _wrap_auth(**webhook.get('authorization', {}))
|
||||
|
||||
if (webhook.get('authorization', {}).get("type") or '').lower() == 'oauth2.0':
|
||||
request = getattr(auth, webhook.get('method', 'GET').lower())
|
||||
else:
|
||||
request = partial(requests.request, webhook.get('method', 'GET'))
|
||||
|
||||
return request(
|
||||
url,
|
||||
params=params,
|
||||
headers=headers or None,
|
||||
data=data,
|
||||
auth=auth
|
||||
)
|
@@ -5,7 +5,8 @@ import copy
|
||||
import hashlib
|
||||
from datetime import datetime
|
||||
|
||||
import ldap
|
||||
from ldap3 import Server, Connection, ALL
|
||||
from ldap3.core.exceptions import LDAPBindError, LDAPCertificateError
|
||||
from flask import current_app
|
||||
from flask_sqlalchemy import BaseQuery
|
||||
|
||||
@@ -57,9 +58,7 @@ class UserQuery(BaseQuery):
|
||||
return user, authenticated
|
||||
|
||||
def authenticate_with_ldap(self, username, password):
|
||||
ldap_conn = ldap.initialize(current_app.config.get('LDAP_SERVER'))
|
||||
ldap_conn.protocol_version = 3
|
||||
ldap_conn.set_option(ldap.OPT_REFERRALS, 0)
|
||||
server = Server(current_app.config.get('LDAP_SERVER'), get_info=ALL)
|
||||
if '@' in username:
|
||||
email = username
|
||||
who = current_app.config.get('LDAP_USER_DN').format(username.split('@')[0])
|
||||
@@ -70,11 +69,14 @@ class UserQuery(BaseQuery):
|
||||
username = username.split('@')[0]
|
||||
user = self.get_by_username(username)
|
||||
try:
|
||||
|
||||
if not password:
|
||||
raise ldap.INVALID_CREDENTIALS
|
||||
raise LDAPCertificateError
|
||||
|
||||
ldap_conn.simple_bind_s(who, password)
|
||||
conn = Connection(server, user=who, password=password)
|
||||
conn.bind()
|
||||
if conn.result['result'] != 0:
|
||||
raise LDAPBindError
|
||||
conn.unbind()
|
||||
|
||||
if not user:
|
||||
from api.lib.perm.acl.user import UserCRUD
|
||||
@@ -84,7 +86,7 @@ class UserQuery(BaseQuery):
|
||||
op_record.apply_async(args=(None, username, OperateType.LOGIN, ["ACL"]), queue=ACL_QUEUE)
|
||||
|
||||
return user, True
|
||||
except ldap.INVALID_CREDENTIALS:
|
||||
except LDAPBindError:
|
||||
return user, False
|
||||
|
||||
def search(self, key):
|
||||
|
@@ -12,7 +12,9 @@ from api.lib.cmdb.const import CITypeOperateType
|
||||
from api.lib.cmdb.const import ConstraintEnum
|
||||
from api.lib.cmdb.const import OperateType
|
||||
from api.lib.cmdb.const import ValueTypeEnum
|
||||
from api.lib.database import Model, Model2
|
||||
from api.lib.database import Model
|
||||
from api.lib.database import Model2
|
||||
from api.lib.utils import Crypto
|
||||
|
||||
|
||||
# template
|
||||
@@ -89,12 +91,37 @@ class Attribute(Model):
|
||||
compute_expr = db.Column(db.Text)
|
||||
compute_script = db.Column(db.Text)
|
||||
|
||||
choice_web_hook = db.Column(db.JSON)
|
||||
_choice_web_hook = db.Column('choice_web_hook', db.JSON)
|
||||
choice_other = db.Column(db.JSON)
|
||||
|
||||
uid = db.Column(db.Integer, index=True)
|
||||
|
||||
option = db.Column(db.JSON)
|
||||
|
||||
def _get_webhook(self):
|
||||
if self._choice_web_hook:
|
||||
if self._choice_web_hook.get('headers') and "Cookie" in self._choice_web_hook['headers']:
|
||||
self._choice_web_hook['headers']['Cookie'] = Crypto.decrypt(self._choice_web_hook['headers']['Cookie'])
|
||||
|
||||
if self._choice_web_hook.get('authorization'):
|
||||
for k, v in self._choice_web_hook['authorization'].items():
|
||||
self._choice_web_hook['authorization'][k] = Crypto.decrypt(v)
|
||||
|
||||
return self._choice_web_hook
|
||||
|
||||
def _set_webhook(self, data):
|
||||
if data:
|
||||
if data.get('headers') and "Cookie" in data['headers']:
|
||||
data['headers']['Cookie'] = Crypto.encrypt(data['headers']['Cookie'])
|
||||
|
||||
if data.get('authorization'):
|
||||
for k, v in data['authorization'].items():
|
||||
data['authorization'][k] = Crypto.encrypt(v)
|
||||
|
||||
self._choice_web_hook = data
|
||||
|
||||
choice_web_hook = db.synonym("_choice_web_hook", descriptor=property(_get_webhook, _set_webhook))
|
||||
|
||||
|
||||
class CITypeAttribute(Model):
|
||||
__tablename__ = "c_ci_type_attributes"
|
||||
@@ -125,16 +152,45 @@ class CITypeAttributeGroupItem(Model):
|
||||
|
||||
|
||||
class CITypeTrigger(Model):
|
||||
# __tablename__ = "c_ci_type_triggers"
|
||||
__tablename__ = "c_c_t_t"
|
||||
|
||||
type_id = db.Column(db.Integer, db.ForeignKey('c_ci_types.id'), nullable=False)
|
||||
attr_id = db.Column(db.Integer, db.ForeignKey("c_attributes.id"), nullable=False)
|
||||
notify = db.Column(db.JSON) # {subject: x, body: x, wx_to: [], mail_to: [], before_days: 0, notify_at: 08:00}
|
||||
attr_id = db.Column(db.Integer, db.ForeignKey("c_attributes.id"))
|
||||
_option = db.Column('notify', db.JSON)
|
||||
|
||||
def _get_option(self):
|
||||
if self._option and self._option.get('webhooks'):
|
||||
if self._option['webhooks'].get('authorization'):
|
||||
for k, v in self._option['webhooks']['authorization'].items():
|
||||
self._option['webhooks']['authorization'][k] = Crypto.decrypt(v)
|
||||
|
||||
return self._option
|
||||
|
||||
def _set_option(self, data):
|
||||
if data and data.get('webhooks'):
|
||||
if data['webhooks'].get('authorization'):
|
||||
for k, v in data['webhooks']['authorization'].items():
|
||||
data['webhooks']['authorization'][k] = Crypto.encrypt(v)
|
||||
|
||||
self._option = data
|
||||
|
||||
option = db.synonym("_option", descriptor=property(_get_option, _set_option))
|
||||
|
||||
|
||||
class CITriggerHistory(Model):
|
||||
__tablename__ = "c_ci_trigger_histories"
|
||||
|
||||
operate_type = db.Column(db.Enum(*OperateType.all(), name="operate_type"))
|
||||
record_id = db.Column(db.Integer, db.ForeignKey("c_records.id"))
|
||||
ci_id = db.Column(db.Integer, index=True, nullable=False)
|
||||
trigger_id = db.Column(db.Integer, db.ForeignKey("c_c_t_t.id"))
|
||||
trigger_name = db.Column(db.String(64))
|
||||
is_ok = db.Column(db.Boolean, default=False)
|
||||
notify = db.Column(db.Text)
|
||||
webhook = db.Column(db.Text)
|
||||
|
||||
|
||||
class CITypeUniqueConstraint(Model):
|
||||
# __tablename__ = "c_ci_type_unique_constraints"
|
||||
__tablename__ = "c_c_t_u_c"
|
||||
|
||||
type_id = db.Column(db.Integer, db.ForeignKey('c_ci_types.id'), nullable=False)
|
||||
@@ -363,7 +419,6 @@ class CITypeHistory(Model):
|
||||
|
||||
# preference
|
||||
class PreferenceShowAttributes(Model):
|
||||
# __tablename__ = "c_preference_show_attributes"
|
||||
__tablename__ = "c_psa"
|
||||
|
||||
uid = db.Column(db.Integer, index=True, nullable=False)
|
||||
@@ -377,7 +432,6 @@ class PreferenceShowAttributes(Model):
|
||||
|
||||
|
||||
class PreferenceTreeView(Model):
|
||||
# __tablename__ = "c_preference_tree_views"
|
||||
__tablename__ = "c_ptv"
|
||||
|
||||
uid = db.Column(db.Integer, index=True, nullable=False)
|
||||
@@ -386,7 +440,6 @@ class PreferenceTreeView(Model):
|
||||
|
||||
|
||||
class PreferenceRelationView(Model):
|
||||
# __tablename__ = "c_preference_relation_views"
|
||||
__tablename__ = "c_prv"
|
||||
|
||||
uid = db.Column(db.Integer, index=True, nullable=False)
|
||||
@@ -495,3 +548,10 @@ class CIFilterPerms(Model):
|
||||
attr_filter = db.Column(db.Text)
|
||||
|
||||
rid = db.Column(db.Integer, index=True)
|
||||
|
||||
|
||||
class InnerKV(Model):
|
||||
__tablename__ = "c_kv"
|
||||
|
||||
key = db.Column(db.String(128), index=True)
|
||||
value = db.Column(db.Text)
|
||||
|
@@ -47,6 +47,8 @@ class Employee(ModelWithoutPK):
|
||||
last_login = db.Column(db.TIMESTAMP, nullable=True)
|
||||
block = db.Column(db.Integer, default=0)
|
||||
|
||||
notice_info = db.Column(db.JSON, default={})
|
||||
|
||||
_department = db.relationship(
|
||||
'Department', backref='common_employee.department_id',
|
||||
lazy='joined'
|
||||
@@ -87,3 +89,10 @@ class CommonData(Model):
|
||||
|
||||
data_type = db.Column(db.VARCHAR(255), default='')
|
||||
data = db.Column(db.JSON)
|
||||
|
||||
|
||||
class NoticeConfig(Model):
|
||||
__tablename__ = "common_notice_config"
|
||||
|
||||
platform = db.Column(db.VARCHAR(255), nullable=False)
|
||||
info = db.Column(db.JSON)
|
||||
|
@@ -46,5 +46,4 @@ def register_resources(resource_path, rest_api):
|
||||
resource_cls.url_prefix = ("",)
|
||||
if isinstance(resource_cls.url_prefix, six.string_types):
|
||||
resource_cls.url_prefix = (resource_cls.url_prefix,)
|
||||
|
||||
rest_api.add_resource(resource_cls, *resource_cls.url_prefix)
|
||||
|
@@ -9,7 +9,8 @@ from werkzeug.exceptions import BadRequest
|
||||
from werkzeug.exceptions import NotFound
|
||||
|
||||
from api.extensions import celery
|
||||
from api.extensions import db
|
||||
from api.lib.decorator import flush_db
|
||||
from api.lib.decorator import reconnect_db
|
||||
from api.lib.perm.acl.audit import AuditCRUD
|
||||
from api.lib.perm.acl.audit import AuditOperateSource
|
||||
from api.lib.perm.acl.audit import AuditOperateType
|
||||
@@ -28,6 +29,7 @@ from api.models.acl import Trigger
|
||||
name="acl.role_rebuild",
|
||||
queue=ACL_QUEUE,
|
||||
once={"graceful": True, "unlock_before_run": True})
|
||||
@reconnect_db
|
||||
def role_rebuild(rids, app_id):
|
||||
rids = rids if isinstance(rids, list) else [rids]
|
||||
for rid in rids:
|
||||
@@ -37,6 +39,7 @@ def role_rebuild(rids, app_id):
|
||||
|
||||
|
||||
@celery.task(name="acl.update_resource_to_build_role", queue=ACL_QUEUE)
|
||||
@reconnect_db
|
||||
def update_resource_to_build_role(resource_id, app_id, group_id=None):
|
||||
rids = [i.id for i in Role.get_by(__func_isnot__key_uid=None, fl='id', to_dict=False)]
|
||||
rids += [i.id for i in Role.get_by(app_id=app_id, fl='id', to_dict=False)]
|
||||
@@ -52,9 +55,9 @@ def update_resource_to_build_role(resource_id, app_id, group_id=None):
|
||||
|
||||
|
||||
@celery.task(name="acl.apply_trigger", queue=ACL_QUEUE)
|
||||
@flush_db
|
||||
@reconnect_db
|
||||
def apply_trigger(_id, resource_id=None, operator_uid=None):
|
||||
db.session.remove()
|
||||
|
||||
from api.lib.perm.acl.permission import PermissionCRUD
|
||||
|
||||
trigger = Trigger.get_by_id(_id)
|
||||
@@ -118,9 +121,9 @@ def apply_trigger(_id, resource_id=None, operator_uid=None):
|
||||
|
||||
|
||||
@celery.task(name="acl.cancel_trigger", queue=ACL_QUEUE)
|
||||
@flush_db
|
||||
@reconnect_db
|
||||
def cancel_trigger(_id, resource_id=None, operator_uid=None):
|
||||
db.session.remove()
|
||||
|
||||
from api.lib.perm.acl.permission import PermissionCRUD
|
||||
|
||||
trigger = Trigger.get_by_id(_id)
|
||||
@@ -186,6 +189,7 @@ def cancel_trigger(_id, resource_id=None, operator_uid=None):
|
||||
|
||||
|
||||
@celery.task(name="acl.op_record", queue=ACL_QUEUE)
|
||||
@reconnect_db
|
||||
def op_record(app, rolename, operate_type, obj):
|
||||
if isinstance(app, int):
|
||||
app = AppCache.get(app)
|
||||
|
@@ -4,8 +4,6 @@
|
||||
import json
|
||||
import time
|
||||
|
||||
import jinja2
|
||||
import requests
|
||||
from flask import current_app
|
||||
from flask_login import login_user
|
||||
|
||||
@@ -18,7 +16,8 @@ from api.lib.cmdb.cache import CITypeAttributesCache
|
||||
from api.lib.cmdb.const import CMDB_QUEUE
|
||||
from api.lib.cmdb.const import REDIS_PREFIX_CI
|
||||
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION
|
||||
from api.lib.mail import send_mail
|
||||
from api.lib.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
|
||||
@@ -28,9 +27,12 @@ from api.models.cmdb import CITypeAttribute
|
||||
|
||||
|
||||
@celery.task(name="cmdb.ci_cache", queue=CMDB_QUEUE)
|
||||
def ci_cache(ci_id):
|
||||
@flush_db
|
||||
@reconnect_db
|
||||
def ci_cache(ci_id, operate_type, record_id):
|
||||
from api.lib.cmdb.ci import CITriggerManager
|
||||
|
||||
time.sleep(0.01)
|
||||
db.session.remove()
|
||||
|
||||
m = api.lib.cmdb.ci.CIManager()
|
||||
ci_dict = m.get_ci_by_id_from_db(ci_id, need_children=False, use_master=False)
|
||||
@@ -42,11 +44,18 @@ def ci_cache(ci_id):
|
||||
|
||||
current_app.logger.info("{0} flush..........".format(ci_id))
|
||||
|
||||
if operate_type:
|
||||
current_app.test_request_context().push()
|
||||
login_user(UserCache.get('worker'))
|
||||
|
||||
CITriggerManager.fire(operate_type, ci_dict, record_id)
|
||||
|
||||
|
||||
@celery.task(name="cmdb.batch_ci_cache", queue=CMDB_QUEUE)
|
||||
def batch_ci_cache(ci_ids):
|
||||
@flush_db
|
||||
@reconnect_db
|
||||
def batch_ci_cache(ci_ids, ): # only for attribute change index
|
||||
time.sleep(1)
|
||||
db.session.remove()
|
||||
|
||||
for ci_id in ci_ids:
|
||||
m = api.lib.cmdb.ci.CIManager()
|
||||
@@ -61,6 +70,7 @@ def batch_ci_cache(ci_ids):
|
||||
|
||||
|
||||
@celery.task(name="cmdb.ci_delete", queue=CMDB_QUEUE)
|
||||
@reconnect_db
|
||||
def ci_delete(ci_id):
|
||||
current_app.logger.info(ci_id)
|
||||
|
||||
@@ -72,10 +82,22 @@ def ci_delete(ci_id):
|
||||
current_app.logger.info("{0} delete..........".format(ci_id))
|
||||
|
||||
|
||||
@celery.task(name="cmdb.ci_relation_cache", queue=CMDB_QUEUE)
|
||||
def ci_relation_cache(parent_id, child_id):
|
||||
db.session.remove()
|
||||
@celery.task(name="cmdb.ci_delete_trigger", queue=CMDB_QUEUE)
|
||||
@reconnect_db
|
||||
def ci_delete_trigger(trigger, operate_type, ci_dict):
|
||||
current_app.logger.info('delete ci {} trigger'.format(ci_dict['_id']))
|
||||
from api.lib.cmdb.ci import CITriggerManager
|
||||
|
||||
current_app.test_request_context().push()
|
||||
login_user(UserCache.get('worker'))
|
||||
|
||||
CITriggerManager.fire_by_trigger(trigger, operate_type, ci_dict)
|
||||
|
||||
|
||||
@celery.task(name="cmdb.ci_relation_cache", queue=CMDB_QUEUE)
|
||||
@flush_db
|
||||
@reconnect_db
|
||||
def ci_relation_cache(parent_id, child_id):
|
||||
with Lock("CIRelation_{}".format(parent_id)):
|
||||
children = rd.get([parent_id], REDIS_PREFIX_CI_RELATION)[0]
|
||||
children = json.loads(children) if children is not None else {}
|
||||
@@ -90,6 +112,8 @@ def ci_relation_cache(parent_id, child_id):
|
||||
|
||||
|
||||
@celery.task(name="cmdb.ci_relation_add", queue=CMDB_QUEUE)
|
||||
@flush_db
|
||||
@reconnect_db
|
||||
def ci_relation_add(parent_dict, child_id, uid):
|
||||
"""
|
||||
:param parent_dict: key is '$parent_model.attr_name'
|
||||
@@ -105,8 +129,6 @@ def ci_relation_add(parent_dict, child_id, uid):
|
||||
current_app.test_request_context().push()
|
||||
login_user(UserCache.get(uid))
|
||||
|
||||
db.session.remove()
|
||||
|
||||
for parent in parent_dict:
|
||||
parent_ci_type_name, _attr_name = parent.strip()[1:].split('.', 1)
|
||||
attr_name = CITypeAttributeManager.get_attr_name(parent_ci_type_name, _attr_name)
|
||||
@@ -131,10 +153,14 @@ def ci_relation_add(parent_dict, child_id, uid):
|
||||
except Exception as e:
|
||||
current_app.logger.warning(e)
|
||||
finally:
|
||||
db.session.remove()
|
||||
try:
|
||||
db.session.commit()
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
@celery.task(name="cmdb.ci_relation_delete", queue=CMDB_QUEUE)
|
||||
@reconnect_db
|
||||
def ci_relation_delete(parent_id, child_id):
|
||||
with Lock("CIRelation_{}".format(parent_id)):
|
||||
children = rd.get([parent_id], REDIS_PREFIX_CI_RELATION)[0]
|
||||
@@ -149,15 +175,19 @@ def ci_relation_delete(parent_id, child_id):
|
||||
|
||||
|
||||
@celery.task(name="cmdb.ci_type_attribute_order_rebuild", queue=CMDB_QUEUE)
|
||||
def ci_type_attribute_order_rebuild(type_id):
|
||||
@flush_db
|
||||
@reconnect_db
|
||||
def ci_type_attribute_order_rebuild(type_id, uid):
|
||||
current_app.logger.info('rebuild attribute order')
|
||||
db.session.remove()
|
||||
|
||||
from api.lib.cmdb.ci_type import CITypeAttributeGroupManager
|
||||
|
||||
attrs = CITypeAttributesCache.get(type_id)
|
||||
id2attr = {attr.attr_id: attr for attr in attrs}
|
||||
|
||||
current_app.test_request_context().push()
|
||||
login_user(UserCache.get(uid))
|
||||
|
||||
res = CITypeAttributeGroupManager.get_by_type_id(type_id, True)
|
||||
order = 0
|
||||
for group in res:
|
||||
@@ -168,56 +198,17 @@ def ci_type_attribute_order_rebuild(type_id):
|
||||
order += 1
|
||||
|
||||
|
||||
@celery.task(name='cmdb.trigger_notify', queue=CMDB_QUEUE)
|
||||
def trigger_notify(notify, ci_id):
|
||||
from api.lib.perm.acl.cache import UserCache
|
||||
|
||||
def _wrap_mail(mail_to):
|
||||
if "@" not in mail_to:
|
||||
user = UserCache.get(mail_to)
|
||||
if user:
|
||||
return user.email
|
||||
|
||||
return mail_to
|
||||
|
||||
db.session.remove()
|
||||
|
||||
m = api.lib.cmdb.ci.CIManager()
|
||||
ci_dict = m.get_ci_by_id_from_db(ci_id, need_children=False, use_master=False)
|
||||
|
||||
subject = jinja2.Template(notify.get('subject') or "").render(ci_dict)
|
||||
body = jinja2.Template(notify.get('body') or "").render(ci_dict)
|
||||
|
||||
if notify.get('wx_to'):
|
||||
to_user = jinja2.Template('|'.join(notify['wx_to'])).render(ci_dict)
|
||||
url = current_app.config.get("WX_URI")
|
||||
data = {"to_user": to_user, "content": subject}
|
||||
try:
|
||||
requests.post(url, data=data)
|
||||
except Exception as e:
|
||||
current_app.logger.error(str(e))
|
||||
|
||||
if notify.get('mail_to'):
|
||||
try:
|
||||
if len(subject) > 700:
|
||||
subject = subject[:600] + "..." + subject[-100:]
|
||||
|
||||
send_mail("", [_wrap_mail(jinja2.Template(i).render(ci_dict))
|
||||
for i in notify['mail_to'] if i], subject, body)
|
||||
except Exception as e:
|
||||
current_app.logger.error("Send mail failed: {0}".format(str(e)))
|
||||
|
||||
|
||||
@celery.task(name="cmdb.calc_computed_attribute", queue=CMDB_QUEUE)
|
||||
@flush_db
|
||||
@reconnect_db
|
||||
def calc_computed_attribute(attr_id, uid):
|
||||
from api.lib.cmdb.ci import CIManager
|
||||
|
||||
db.session.remove()
|
||||
|
||||
current_app.test_request_context().push()
|
||||
login_user(UserCache.get(uid))
|
||||
|
||||
cim = CIManager()
|
||||
for i in CITypeAttribute.get_by(attr_id=attr_id, to_dict=False):
|
||||
cis = CI.get_by(type_id=i.type_id, to_dict=False)
|
||||
for ci in cis:
|
||||
CIManager.update(ci.id, {})
|
||||
cim.update(ci.id, {})
|
||||
|
@@ -84,11 +84,10 @@ class CIView(APIView):
|
||||
ci_dict = self._wrap_ci_dict()
|
||||
|
||||
manager = CIManager()
|
||||
current_app.logger.debug(ci_dict)
|
||||
ci_id = manager.add(ci_type,
|
||||
exist_policy=exist_policy or ExistPolicy.REJECT,
|
||||
_no_attribute_policy=_no_attribute_policy,
|
||||
_is_admin=request.values.pop('__is_admin', False),
|
||||
_is_admin=request.values.pop('__is_admin', None) or False,
|
||||
**ci_dict)
|
||||
|
||||
return self.jsonify(ci_id=ci_id)
|
||||
@@ -96,7 +95,6 @@ class CIView(APIView):
|
||||
@has_perm_for_ci("ci_id", ResourceTypeEnum.CI, PermEnum.UPDATE, CIManager.get_type)
|
||||
def put(self, ci_id=None):
|
||||
args = request.values
|
||||
current_app.logger.info(args)
|
||||
ci_type = args.get("ci_type")
|
||||
_no_attribute_policy = args.get("no_attribute_policy", ExistPolicy.IGNORE)
|
||||
|
||||
@@ -104,14 +102,14 @@ class CIView(APIView):
|
||||
manager = CIManager()
|
||||
if ci_id is not None:
|
||||
manager.update(ci_id,
|
||||
_is_admin=request.values.pop('__is_admin', False),
|
||||
_is_admin=request.values.pop('__is_admin', None) or False,
|
||||
**ci_dict)
|
||||
else:
|
||||
request.values.pop('exist_policy', None)
|
||||
ci_id = manager.add(ci_type,
|
||||
exist_policy=ExistPolicy.REPLACE,
|
||||
_no_attribute_policy=_no_attribute_policy,
|
||||
_is_admin=request.values.pop('__is_admin', False),
|
||||
_is_admin=request.values.pop('__is_admin', None) or False,
|
||||
**ci_dict)
|
||||
|
||||
return self.jsonify(ci_id=ci_id)
|
||||
@@ -185,8 +183,8 @@ class CIUnique(APIView):
|
||||
@has_perm_from_args("ci_id", ResourceTypeEnum.CI, PermEnum.UPDATE, CIManager.get_type_name)
|
||||
def put(self, ci_id):
|
||||
params = request.values
|
||||
unique_name = params.keys()[0]
|
||||
unique_value = params.values()[0]
|
||||
unique_name = list(params.keys())[0]
|
||||
unique_value = list(params.values())[0]
|
||||
|
||||
CIManager.update_unique_value(ci_id, unique_name, unique_value)
|
||||
|
||||
@@ -228,11 +226,11 @@ class CIFlushView(APIView):
|
||||
from api.tasks.cmdb import ci_cache
|
||||
from api.lib.cmdb.const import CMDB_QUEUE
|
||||
if ci_id is not None:
|
||||
ci_cache.apply_async([ci_id], queue=CMDB_QUEUE)
|
||||
ci_cache.apply_async(args=(ci_id, None, None), queue=CMDB_QUEUE)
|
||||
else:
|
||||
cis = CI.get_by(to_dict=False)
|
||||
for ci in cis:
|
||||
ci_cache.apply_async([ci.id], queue=CMDB_QUEUE)
|
||||
ci_cache.apply_async(args=(ci.id, None, None), queue=CMDB_QUEUE)
|
||||
|
||||
return self.jsonify(code=200)
|
||||
|
||||
@@ -242,3 +240,13 @@ class CIAutoDiscoveryStatisticsView(APIView):
|
||||
|
||||
def get(self):
|
||||
return self.jsonify(CIManager.get_ad_statistics())
|
||||
|
||||
|
||||
class CIPasswordView(APIView):
|
||||
url_prefix = "/ci/<int:ci_id>/attributes/<int:attr_id>/password"
|
||||
|
||||
def get(self, ci_id, attr_id):
|
||||
return self.jsonify(ci_id=ci_id, attr_id=attr_id, value=CIManager.load_password(ci_id, attr_id))
|
||||
|
||||
def post(self, ci_id, attr_id):
|
||||
return self.get(ci_id, attr_id)
|
||||
|
@@ -419,22 +419,22 @@ class CITypeTriggerView(APIView):
|
||||
return self.jsonify(CITypeTriggerManager.get(type_id))
|
||||
|
||||
@has_perm_from_args("type_id", ResourceTypeEnum.CI, PermEnum.CONFIG, CITypeManager.get_name_by_id)
|
||||
@args_required("attr_id")
|
||||
@args_required("notify")
|
||||
@args_required("option")
|
||||
def post(self, type_id):
|
||||
attr_id = request.values.get('attr_id')
|
||||
notify = request.values.get('notify')
|
||||
attr_id = request.values.get('attr_id') or None
|
||||
option = request.values.get('option')
|
||||
|
||||
return self.jsonify(CITypeTriggerManager().add(type_id, attr_id, notify))
|
||||
return self.jsonify(CITypeTriggerManager().add(type_id, attr_id, option))
|
||||
|
||||
@has_perm_from_args("type_id", ResourceTypeEnum.CI, PermEnum.CONFIG, CITypeManager.get_name_by_id)
|
||||
@args_required("notify")
|
||||
@args_required("option")
|
||||
def put(self, type_id, _id):
|
||||
assert type_id is not None
|
||||
|
||||
notify = request.values.get('notify')
|
||||
option = request.values.get('option')
|
||||
attr_id = request.values.get('attr_id')
|
||||
|
||||
return self.jsonify(CITypeTriggerManager().update(_id, notify))
|
||||
return self.jsonify(CITypeTriggerManager().update(_id, attr_id, option))
|
||||
|
||||
@has_perm_from_args("type_id", ResourceTypeEnum.CI, PermEnum.CONFIG, CITypeManager.get_name_by_id)
|
||||
def delete(self, type_id, _id):
|
||||
@@ -506,4 +506,3 @@ class CITypeFilterPermissionView(APIView):
|
||||
@auth_with_app_token
|
||||
def get(self, type_id):
|
||||
return self.jsonify(CIFilterPermsCRUD().get(type_id))
|
||||
|
||||
|
@@ -5,15 +5,18 @@ import datetime
|
||||
|
||||
from flask import abort
|
||||
from flask import request
|
||||
from flask import session
|
||||
|
||||
from api.lib.cmdb.ci import CIManager
|
||||
from api.lib.cmdb.const import PermEnum
|
||||
from api.lib.cmdb.const import ResourceTypeEnum
|
||||
from api.lib.cmdb.const import RoleEnum
|
||||
from api.lib.cmdb.history import AttributeHistoryManger
|
||||
from api.lib.cmdb.history import CITriggerHistoryManager
|
||||
from api.lib.cmdb.history import CITypeHistoryManager
|
||||
from api.lib.cmdb.resp_format import ErrFormat
|
||||
from api.lib.perm.acl.acl import has_perm_from_args
|
||||
from api.lib.perm.acl.acl import is_app_admin
|
||||
from api.lib.perm.acl.acl import role_required
|
||||
from api.lib.utils import get_page
|
||||
from api.lib.utils import get_page_size
|
||||
@@ -76,6 +79,39 @@ class CIHistoryView(APIView):
|
||||
return self.jsonify(result)
|
||||
|
||||
|
||||
class CITriggerHistoryView(APIView):
|
||||
url_prefix = ("/history/ci_triggers/<int:ci_id>", "/history/ci_triggers")
|
||||
|
||||
@has_perm_from_args("ci_id", ResourceTypeEnum.CI, PermEnum.READ, CIManager.get_type_name)
|
||||
def get(self, ci_id=None):
|
||||
if ci_id is not None:
|
||||
result = CITriggerHistoryManager.get_by_ci_id(ci_id)
|
||||
|
||||
return self.jsonify(result)
|
||||
|
||||
if RoleEnum.CONFIG not in session.get("acl", {}).get("parentRoles", []) and not is_app_admin("cmdb"):
|
||||
return abort(403, ErrFormat.role_required.format(RoleEnum.CONFIG))
|
||||
|
||||
type_id = request.values.get("type_id")
|
||||
trigger_id = request.values.get("trigger_id")
|
||||
operate_type = request.values.get("operate_type")
|
||||
|
||||
page = get_page(request.values.get('page', 1))
|
||||
page_size = get_page_size(request.values.get('page_size', 1))
|
||||
|
||||
numfound, result = CITriggerHistoryManager.get(page,
|
||||
page_size,
|
||||
type_id=type_id,
|
||||
trigger_id=trigger_id,
|
||||
operate_type=operate_type)
|
||||
|
||||
return self.jsonify(page=page,
|
||||
page_size=page_size,
|
||||
numfound=numfound,
|
||||
total=len(result),
|
||||
result=result)
|
||||
|
||||
|
||||
class CITypeHistoryView(APIView):
|
||||
url_prefix = "/history/ci_types"
|
||||
|
||||
|
37
cmdb-api/api/views/cmdb/inner_secrets.py
Normal file
37
cmdb-api/api/views/cmdb/inner_secrets.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from flask import request
|
||||
|
||||
from api.lib.perm.auth import auth_abandoned
|
||||
from api.lib.secrets.inner import KeyManage
|
||||
from api.lib.secrets.secrets import InnerKVManger
|
||||
from api.resource import APIView
|
||||
|
||||
|
||||
class InnerSecretUnSealView(APIView):
|
||||
url_prefix = "/secrets/unseal"
|
||||
|
||||
@auth_abandoned
|
||||
def post(self):
|
||||
unseal_key = request.headers.get("Unseal-Token")
|
||||
res = KeyManage(backend=InnerKVManger()).unseal(unseal_key)
|
||||
return self.jsonify(**res)
|
||||
|
||||
|
||||
class InnerSecretSealView(APIView):
|
||||
url_prefix = "/secrets/seal"
|
||||
|
||||
@auth_abandoned
|
||||
def post(self):
|
||||
unseal_key = request.headers.get("Inner-Token")
|
||||
res = KeyManage(backend=InnerKVManger()).seal(unseal_key)
|
||||
return self.jsonify(**res)
|
||||
|
||||
|
||||
class InnerSecretAutoSealView(APIView):
|
||||
url_prefix = "/secrets/auto_seal"
|
||||
|
||||
@auth_abandoned
|
||||
def post(self):
|
||||
root_key = request.headers.get("Inner-Token")
|
||||
res = KeyManage(trigger=root_key,
|
||||
backend=InnerKVManger()).auto_unseal()
|
||||
return self.jsonify(**res)
|
@@ -24,12 +24,12 @@ class DataView(APIView):
|
||||
class DataViewWithId(APIView):
|
||||
url_prefix = (f'{prefix}/<string:data_type>/<int:_id>',)
|
||||
|
||||
def put(self, data_type, _id):
|
||||
def put(self, _id):
|
||||
params = request.json
|
||||
res = CommonDataCRUD.update_data(_id, **params)
|
||||
|
||||
return self.jsonify(res.to_dict())
|
||||
|
||||
def delete(self, data_type, _id):
|
||||
def delete(self, _id):
|
||||
CommonDataCRUD.delete(_id)
|
||||
return self.jsonify({})
|
||||
|
@@ -1,9 +1,7 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
from flask import abort
|
||||
from flask import request
|
||||
|
||||
from api.lib.common_setting.company_info import CompanyInfoCRUD
|
||||
from api.lib.common_setting.resp_format import ErrFormat
|
||||
from api.resource import APIView
|
||||
|
||||
prefix = '/company'
|
||||
|
@@ -1,7 +1,5 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
import os
|
||||
|
||||
from flask import abort, current_app, send_from_directory
|
||||
from flask import abort
|
||||
from flask import request
|
||||
from werkzeug.datastructures import MultiDict
|
||||
|
||||
@@ -145,3 +143,26 @@ class EmployeePositionView(APIView):
|
||||
result = EmployeeCRUD.get_all_position()
|
||||
return self.jsonify(result)
|
||||
|
||||
|
||||
class GetEmployeeNoticeByIds(APIView):
|
||||
url_prefix = (f'{prefix}/get_notice_by_ids',)
|
||||
|
||||
def post(self):
|
||||
employee_ids = request.json.get('employee_ids', [])
|
||||
if not employee_ids:
|
||||
result = []
|
||||
else:
|
||||
result = EmployeeCRUD.get_employee_notice_by_ids(employee_ids)
|
||||
return self.jsonify(result)
|
||||
|
||||
|
||||
class EmployeeBindNoticeWithACLID(APIView):
|
||||
url_prefix = (f'{prefix}/by_uid/bind_notice/<string:platform>/<int:_uid>',)
|
||||
|
||||
def put(self, platform, _uid):
|
||||
data = EmployeeCRUD.bind_notice_by_uid(platform, _uid)
|
||||
return self.jsonify(info=data)
|
||||
|
||||
def delete(self, platform, _uid):
|
||||
data = EmployeeCRUD.remove_bind_notice_by_uid(platform, _uid)
|
||||
return self.jsonify(info=data)
|
||||
|
@@ -11,7 +11,7 @@ from api.resource import APIView
|
||||
prefix = '/file'
|
||||
|
||||
ALLOWED_EXTENSIONS = {
|
||||
'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif', 'xls', 'xlsx', 'doc', 'docx', 'ppt', 'pptx', 'csv'
|
||||
'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif', 'xls', 'xlsx', 'doc', 'docx', 'ppt', 'pptx', 'csv', 'svg'
|
||||
}
|
||||
|
||||
|
||||
|
79
cmdb-api/api/views/common_setting/notice_config.py
Normal file
79
cmdb-api/api/views/common_setting/notice_config.py
Normal file
@@ -0,0 +1,79 @@
|
||||
from flask import request, abort, current_app
|
||||
from werkzeug.datastructures import MultiDict
|
||||
|
||||
from api.lib.perm.auth import auth_with_app_token
|
||||
from api.models.common_setting import NoticeConfig
|
||||
from api.resource import APIView
|
||||
from api.lib.common_setting.notice_config import NoticeConfigForm, NoticeConfigUpdateForm, NoticeConfigCRUD
|
||||
from api.lib.decorator import args_required
|
||||
from api.lib.common_setting.resp_format import ErrFormat
|
||||
|
||||
prefix = '/notice_config'
|
||||
|
||||
|
||||
class NoticeConfigView(APIView):
|
||||
url_prefix = (f'{prefix}',)
|
||||
|
||||
@args_required('platform')
|
||||
@auth_with_app_token
|
||||
def get(self):
|
||||
platform = request.args.get('platform')
|
||||
res = NoticeConfig.get_by(first=True, to_dict=True, platform=platform) or {}
|
||||
return self.jsonify(res)
|
||||
|
||||
def post(self):
|
||||
form = NoticeConfigForm(MultiDict(request.json))
|
||||
if not form.validate():
|
||||
abort(400, ','.join(['{}: {}'.format(filed, ','.join(msg)) for filed, msg in form.errors.items()]))
|
||||
|
||||
data = NoticeConfigCRUD.add_notice_config(**form.data)
|
||||
return self.jsonify(data.to_dict())
|
||||
|
||||
|
||||
class NoticeConfigUpdateView(APIView):
|
||||
url_prefix = (f'{prefix}/<int:_id>',)
|
||||
|
||||
def put(self, _id):
|
||||
form = NoticeConfigUpdateForm(MultiDict(request.json))
|
||||
if not form.validate():
|
||||
abort(400, ','.join(['{}: {}'.format(filed, ','.join(msg)) for filed, msg in form.errors.items()]))
|
||||
|
||||
data = NoticeConfigCRUD.edit_notice_config(_id, **form.data)
|
||||
return self.jsonify(data.to_dict())
|
||||
|
||||
|
||||
class CheckEmailServer(APIView):
|
||||
url_prefix = (f'{prefix}/send_test_email',)
|
||||
|
||||
def post(self):
|
||||
receive_address = request.args.get('receive_address')
|
||||
info = request.values.get('info', {})
|
||||
|
||||
try:
|
||||
|
||||
result = NoticeConfigCRUD.test_send_email(receive_address, **info)
|
||||
return self.jsonify(result=result)
|
||||
except Exception as e:
|
||||
current_app.logger.error('test_send_email err:')
|
||||
current_app.logger.error(e)
|
||||
if 'Timed Out' in str(e):
|
||||
abort(400, ErrFormat.email_send_timeout)
|
||||
abort(400, f"{str(e)}")
|
||||
|
||||
|
||||
class NoticeConfigGetView(APIView):
|
||||
method_decorators = []
|
||||
url_prefix = (f'{prefix}/all',)
|
||||
|
||||
@auth_with_app_token
|
||||
def get(self):
|
||||
res = NoticeConfigCRUD.get_all()
|
||||
return self.jsonify(res)
|
||||
|
||||
|
||||
class NoticeAppBotView(APIView):
|
||||
url_prefix = (f'{prefix}/app_bot',)
|
||||
|
||||
def get(self):
|
||||
res = NoticeConfigCRUD.get_app_bot()
|
||||
return self.jsonify(res)
|
1
cmdb-api/migrations/README
Normal file
1
cmdb-api/migrations/README
Normal file
@@ -0,0 +1 @@
|
||||
Generic single-database configuration.
|
45
cmdb-api/migrations/alembic.ini
Normal file
45
cmdb-api/migrations/alembic.ini
Normal file
@@ -0,0 +1,45 @@
|
||||
# A generic, single database configuration.
|
||||
|
||||
[alembic]
|
||||
# template used to generate migration files
|
||||
# file_template = %%(rev)s_%%(slug)s
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# revision_environment = false
|
||||
|
||||
|
||||
# Logging configuration
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARN
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
110
cmdb-api/migrations/env.py
Normal file
110
cmdb-api/migrations/env.py
Normal file
@@ -0,0 +1,110 @@
|
||||
from __future__ import with_statement
|
||||
|
||||
import logging
|
||||
from logging.config import fileConfig
|
||||
|
||||
from sqlalchemy import engine_from_config
|
||||
from sqlalchemy import pool
|
||||
|
||||
from alembic import context
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
config = context.config
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
fileConfig(config.config_file_name)
|
||||
logger = logging.getLogger('alembic.env')
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
# from myapp import mymodel
|
||||
# target_metadata = mymodel.Base.metadata
|
||||
from flask import current_app
|
||||
config.set_main_option(
|
||||
'sqlalchemy.url', current_app.config.get(
|
||||
'SQLALCHEMY_DATABASE_URI').replace('%', '%%'))
|
||||
target_metadata = current_app.extensions['migrate'].db.metadata
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
# my_important_option = config.get_main_option("my_important_option")
|
||||
# ... etc.
|
||||
|
||||
# 添加要屏蔽的table列表
|
||||
exclude_tables = ["c_cfp"]
|
||||
|
||||
|
||||
def run_migrations_offline():
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
|
||||
"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(
|
||||
url=url, target_metadata=target_metadata, literal_binds=True,
|
||||
include_name=include_name
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online():
|
||||
"""Run migrations in 'online' mode.
|
||||
|
||||
In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
|
||||
# this callback is used to prevent an auto-migration from being generated
|
||||
# when there are no changes to the schema
|
||||
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
|
||||
def process_revision_directives(context, revision, directives):
|
||||
if getattr(config.cmd_opts, 'autogenerate', False):
|
||||
script = directives[0]
|
||||
if script.upgrade_ops.is_empty():
|
||||
directives[:] = []
|
||||
logger.info('No changes in schema detected.')
|
||||
|
||||
connectable = engine_from_config(
|
||||
config.get_section(config.config_ini_section),
|
||||
prefix='sqlalchemy.',
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
|
||||
with connectable.connect() as connection:
|
||||
context.configure(
|
||||
connection=connection,
|
||||
target_metadata=target_metadata,
|
||||
process_revision_directives=process_revision_directives,
|
||||
include_name=include_name,
|
||||
**current_app.extensions['migrate'].configure_args
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def include_name(name, type_, parent_names):
|
||||
if type_ == "table":
|
||||
return name not in exclude_tables
|
||||
elif parent_names.get("table_name") in exclude_tables:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
24
cmdb-api/migrations/script.py.mako
Normal file
24
cmdb-api/migrations/script.py.mako
Normal file
@@ -0,0 +1,24 @@
|
||||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = ${repr(up_revision)}
|
||||
down_revision = ${repr(down_revision)}
|
||||
branch_labels = ${repr(branch_labels)}
|
||||
depends_on = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade():
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade():
|
||||
${downgrades if downgrades else "pass"}
|
360
cmdb-api/migrations/versions/6a4df2623057_.py
Normal file
360
cmdb-api/migrations/versions/6a4df2623057_.py
Normal file
@@ -0,0 +1,360 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 6a4df2623057
|
||||
Revises:
|
||||
Create Date: 2023-10-13 15:17:00.066858
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '6a4df2623057'
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('common_data',
|
||||
sa.Column('deleted_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('deleted', sa.Boolean(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column('data_type', sa.VARCHAR(length=255), nullable=True),
|
||||
sa.Column('data', sa.JSON(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_common_data_deleted'), 'common_data', ['deleted'], unique=False)
|
||||
op.create_table('common_notice_config',
|
||||
sa.Column('deleted_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('deleted', sa.Boolean(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column('platform', sa.VARCHAR(length=255), nullable=False),
|
||||
sa.Column('info', sa.JSON(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_common_notice_config_deleted'), 'common_notice_config', ['deleted'], unique=False)
|
||||
op.add_column('c_attributes', sa.Column('choice_other', sa.JSON(), nullable=True))
|
||||
op.drop_index('idx_c_attributes_uid', table_name='c_attributes')
|
||||
op.create_index(op.f('ix_c_attributes_uid'), 'c_attributes', ['uid'], unique=False)
|
||||
op.drop_index('ix_c_custom_dashboard_deleted', table_name='c_c_d')
|
||||
op.create_index(op.f('ix_c_c_d_deleted'), 'c_c_d', ['deleted'], unique=False)
|
||||
op.drop_index('ix_c_ci_type_triggers_deleted', table_name='c_c_t_t')
|
||||
op.create_index(op.f('ix_c_c_t_t_deleted'), 'c_c_t_t', ['deleted'], unique=False)
|
||||
op.drop_index('ix_c_ci_type_unique_constraints_deleted', table_name='c_c_t_u_c')
|
||||
op.create_index(op.f('ix_c_c_t_u_c_deleted'), 'c_c_t_u_c', ['deleted'], unique=False)
|
||||
op.drop_index('c_ci_types_uid', table_name='c_ci_types')
|
||||
op.create_index(op.f('ix_c_ci_types_uid'), 'c_ci_types', ['uid'], unique=False)
|
||||
op.alter_column('c_prv', 'uid',
|
||||
existing_type=mysql.INTEGER(),
|
||||
nullable=False)
|
||||
op.drop_index('ix_c_preference_relation_views_deleted', table_name='c_prv')
|
||||
op.drop_index('ix_c_preference_relation_views_name', table_name='c_prv')
|
||||
op.create_index(op.f('ix_c_prv_deleted'), 'c_prv', ['deleted'], unique=False)
|
||||
op.create_index(op.f('ix_c_prv_name'), 'c_prv', ['name'], unique=False)
|
||||
op.create_index(op.f('ix_c_prv_uid'), 'c_prv', ['uid'], unique=False)
|
||||
op.drop_index('ix_c_preference_show_attributes_deleted', table_name='c_psa')
|
||||
op.drop_index('ix_c_preference_show_attributes_uid', table_name='c_psa')
|
||||
op.create_index(op.f('ix_c_psa_deleted'), 'c_psa', ['deleted'], unique=False)
|
||||
op.create_index(op.f('ix_c_psa_uid'), 'c_psa', ['uid'], unique=False)
|
||||
op.drop_index('ix_c_preference_tree_views_deleted', table_name='c_ptv')
|
||||
op.drop_index('ix_c_preference_tree_views_uid', table_name='c_ptv')
|
||||
op.create_index(op.f('ix_c_ptv_deleted'), 'c_ptv', ['deleted'], unique=False)
|
||||
op.create_index(op.f('ix_c_ptv_uid'), 'c_ptv', ['uid'], unique=False)
|
||||
op.alter_column('common_department', 'department_name',
|
||||
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255),
|
||||
comment=None,
|
||||
existing_comment='部门名称',
|
||||
existing_nullable=True)
|
||||
op.alter_column('common_department', 'department_director_id',
|
||||
existing_type=mysql.INTEGER(),
|
||||
comment=None,
|
||||
existing_comment='部门负责人ID',
|
||||
existing_nullable=True)
|
||||
op.alter_column('common_department', 'department_parent_id',
|
||||
existing_type=mysql.INTEGER(),
|
||||
comment=None,
|
||||
existing_comment='上级部门ID',
|
||||
existing_nullable=True)
|
||||
op.alter_column('common_department', 'sort_value',
|
||||
existing_type=mysql.INTEGER(),
|
||||
comment=None,
|
||||
existing_comment='排序值',
|
||||
existing_nullable=True)
|
||||
op.alter_column('common_department', 'acl_rid',
|
||||
existing_type=mysql.INTEGER(),
|
||||
comment=None,
|
||||
existing_comment='ACL中rid',
|
||||
existing_nullable=True)
|
||||
op.alter_column('common_employee', 'email',
|
||||
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255),
|
||||
comment=None,
|
||||
existing_comment='邮箱',
|
||||
existing_nullable=True)
|
||||
op.alter_column('common_employee', 'username',
|
||||
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255),
|
||||
comment=None,
|
||||
existing_comment='用户名',
|
||||
existing_nullable=True)
|
||||
op.alter_column('common_employee', 'nickname',
|
||||
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255),
|
||||
comment=None,
|
||||
existing_comment='姓名',
|
||||
existing_nullable=True)
|
||||
op.alter_column('common_employee', 'sex',
|
||||
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=64),
|
||||
comment=None,
|
||||
existing_comment='性别',
|
||||
existing_nullable=True)
|
||||
op.alter_column('common_employee', 'position_name',
|
||||
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255),
|
||||
comment=None,
|
||||
existing_comment='职位名称',
|
||||
existing_nullable=True)
|
||||
op.alter_column('common_employee', 'mobile',
|
||||
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255),
|
||||
comment=None,
|
||||
existing_comment='电话号码',
|
||||
existing_nullable=True)
|
||||
op.alter_column('common_employee', 'avatar',
|
||||
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255),
|
||||
comment=None,
|
||||
existing_comment='头像',
|
||||
existing_nullable=True)
|
||||
op.alter_column('common_employee', 'direct_supervisor_id',
|
||||
existing_type=mysql.INTEGER(),
|
||||
comment=None,
|
||||
existing_comment='直接上级ID',
|
||||
existing_nullable=True)
|
||||
op.alter_column('common_employee', 'department_id',
|
||||
existing_type=mysql.INTEGER(),
|
||||
comment=None,
|
||||
existing_comment='部门ID',
|
||||
existing_nullable=True)
|
||||
op.alter_column('common_employee', 'acl_uid',
|
||||
existing_type=mysql.INTEGER(),
|
||||
comment=None,
|
||||
existing_comment='ACL中uid',
|
||||
existing_nullable=True)
|
||||
op.alter_column('common_employee', 'acl_rid',
|
||||
existing_type=mysql.INTEGER(),
|
||||
comment=None,
|
||||
existing_comment='ACL中rid',
|
||||
existing_nullable=True)
|
||||
op.alter_column('common_employee', 'acl_virtual_rid',
|
||||
existing_type=mysql.INTEGER(),
|
||||
comment=None,
|
||||
existing_comment='ACL中虚拟角色rid',
|
||||
existing_nullable=True)
|
||||
op.alter_column('common_employee', 'last_login',
|
||||
existing_type=mysql.TIMESTAMP(),
|
||||
comment=None,
|
||||
existing_comment='上次登录时间',
|
||||
existing_nullable=True)
|
||||
op.alter_column('common_employee', 'block',
|
||||
existing_type=mysql.INTEGER(),
|
||||
comment=None,
|
||||
existing_comment='锁定状态',
|
||||
existing_nullable=True)
|
||||
op.alter_column('common_employee_info', 'info',
|
||||
existing_type=mysql.JSON(),
|
||||
comment=None,
|
||||
existing_comment='员工信息',
|
||||
existing_nullable=True)
|
||||
op.alter_column('common_employee_info', 'employee_id',
|
||||
existing_type=mysql.INTEGER(),
|
||||
comment=None,
|
||||
existing_comment='员工ID',
|
||||
existing_nullable=True)
|
||||
op.alter_column('common_internal_message', 'title',
|
||||
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255),
|
||||
comment=None,
|
||||
existing_comment='标题',
|
||||
existing_nullable=True)
|
||||
op.alter_column('common_internal_message', 'content',
|
||||
existing_type=mysql.TEXT(charset='utf8mb3', collation='utf8mb3_unicode_ci'),
|
||||
comment=None,
|
||||
existing_comment='内容',
|
||||
existing_nullable=True)
|
||||
op.alter_column('common_internal_message', 'path',
|
||||
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255),
|
||||
comment=None,
|
||||
existing_comment='跳转路径',
|
||||
existing_nullable=True)
|
||||
op.alter_column('common_internal_message', 'is_read',
|
||||
existing_type=mysql.TINYINT(display_width=1),
|
||||
comment=None,
|
||||
existing_comment='是否已读',
|
||||
existing_nullable=True)
|
||||
op.alter_column('common_internal_message', 'app_name',
|
||||
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=128),
|
||||
comment=None,
|
||||
existing_comment='应用名称',
|
||||
existing_nullable=False)
|
||||
op.alter_column('common_internal_message', 'category',
|
||||
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=128),
|
||||
comment=None,
|
||||
existing_comment='分类',
|
||||
existing_nullable=False)
|
||||
op.alter_column('common_internal_message', 'message_data',
|
||||
existing_type=mysql.JSON(),
|
||||
comment=None,
|
||||
existing_comment='数据',
|
||||
existing_nullable=True)
|
||||
op.drop_column('users', 'apps')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('users', sa.Column('apps', mysql.JSON(), nullable=True))
|
||||
op.alter_column('common_internal_message', 'message_data',
|
||||
existing_type=mysql.JSON(),
|
||||
comment='数据',
|
||||
existing_nullable=True)
|
||||
op.alter_column('common_internal_message', 'category',
|
||||
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=128),
|
||||
comment='分类',
|
||||
existing_nullable=False)
|
||||
op.alter_column('common_internal_message', 'app_name',
|
||||
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=128),
|
||||
comment='应用名称',
|
||||
existing_nullable=False)
|
||||
op.alter_column('common_internal_message', 'is_read',
|
||||
existing_type=mysql.TINYINT(display_width=1),
|
||||
comment='是否已读',
|
||||
existing_nullable=True)
|
||||
op.alter_column('common_internal_message', 'path',
|
||||
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255),
|
||||
comment='跳转路径',
|
||||
existing_nullable=True)
|
||||
op.alter_column('common_internal_message', 'content',
|
||||
existing_type=mysql.TEXT(charset='utf8mb3', collation='utf8mb3_unicode_ci'),
|
||||
comment='内容',
|
||||
existing_nullable=True)
|
||||
op.alter_column('common_internal_message', 'title',
|
||||
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255),
|
||||
comment='标题',
|
||||
existing_nullable=True)
|
||||
op.alter_column('common_employee_info', 'employee_id',
|
||||
existing_type=mysql.INTEGER(),
|
||||
comment='员工ID',
|
||||
existing_nullable=True)
|
||||
op.alter_column('common_employee_info', 'info',
|
||||
existing_type=mysql.JSON(),
|
||||
comment='员工信息',
|
||||
existing_nullable=True)
|
||||
op.alter_column('common_employee', 'block',
|
||||
existing_type=mysql.INTEGER(),
|
||||
comment='锁定状态',
|
||||
existing_nullable=True)
|
||||
op.alter_column('common_employee', 'last_login',
|
||||
existing_type=mysql.TIMESTAMP(),
|
||||
comment='上次登录时间',
|
||||
existing_nullable=True)
|
||||
op.alter_column('common_employee', 'acl_virtual_rid',
|
||||
existing_type=mysql.INTEGER(),
|
||||
comment='ACL中虚拟角色rid',
|
||||
existing_nullable=True)
|
||||
op.alter_column('common_employee', 'acl_rid',
|
||||
existing_type=mysql.INTEGER(),
|
||||
comment='ACL中rid',
|
||||
existing_nullable=True)
|
||||
op.alter_column('common_employee', 'acl_uid',
|
||||
existing_type=mysql.INTEGER(),
|
||||
comment='ACL中uid',
|
||||
existing_nullable=True)
|
||||
op.alter_column('common_employee', 'department_id',
|
||||
existing_type=mysql.INTEGER(),
|
||||
comment='部门ID',
|
||||
existing_nullable=True)
|
||||
op.alter_column('common_employee', 'direct_supervisor_id',
|
||||
existing_type=mysql.INTEGER(),
|
||||
comment='直接上级ID',
|
||||
existing_nullable=True)
|
||||
op.alter_column('common_employee', 'avatar',
|
||||
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255),
|
||||
comment='头像',
|
||||
existing_nullable=True)
|
||||
op.alter_column('common_employee', 'mobile',
|
||||
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255),
|
||||
comment='电话号码',
|
||||
existing_nullable=True)
|
||||
op.alter_column('common_employee', 'position_name',
|
||||
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255),
|
||||
comment='职位名称',
|
||||
existing_nullable=True)
|
||||
op.alter_column('common_employee', 'sex',
|
||||
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=64),
|
||||
comment='性别',
|
||||
existing_nullable=True)
|
||||
op.alter_column('common_employee', 'nickname',
|
||||
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255),
|
||||
comment='姓名',
|
||||
existing_nullable=True)
|
||||
op.alter_column('common_employee', 'username',
|
||||
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255),
|
||||
comment='用户名',
|
||||
existing_nullable=True)
|
||||
op.alter_column('common_employee', 'email',
|
||||
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255),
|
||||
comment='邮箱',
|
||||
existing_nullable=True)
|
||||
op.alter_column('common_department', 'acl_rid',
|
||||
existing_type=mysql.INTEGER(),
|
||||
comment='ACL中rid',
|
||||
existing_nullable=True)
|
||||
op.alter_column('common_department', 'sort_value',
|
||||
existing_type=mysql.INTEGER(),
|
||||
comment='排序值',
|
||||
existing_nullable=True)
|
||||
op.alter_column('common_department', 'department_parent_id',
|
||||
existing_type=mysql.INTEGER(),
|
||||
comment='上级部门ID',
|
||||
existing_nullable=True)
|
||||
op.alter_column('common_department', 'department_director_id',
|
||||
existing_type=mysql.INTEGER(),
|
||||
comment='部门负责人ID',
|
||||
existing_nullable=True)
|
||||
op.alter_column('common_department', 'department_name',
|
||||
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255),
|
||||
comment='部门名称',
|
||||
existing_nullable=True)
|
||||
op.drop_index(op.f('ix_c_ptv_uid'), table_name='c_ptv')
|
||||
op.drop_index(op.f('ix_c_ptv_deleted'), table_name='c_ptv')
|
||||
op.create_index('ix_c_preference_tree_views_uid', 'c_ptv', ['uid'], unique=False)
|
||||
op.create_index('ix_c_preference_tree_views_deleted', 'c_ptv', ['deleted'], unique=False)
|
||||
op.drop_index(op.f('ix_c_psa_uid'), table_name='c_psa')
|
||||
op.drop_index(op.f('ix_c_psa_deleted'), table_name='c_psa')
|
||||
op.create_index('ix_c_preference_show_attributes_uid', 'c_psa', ['uid'], unique=False)
|
||||
op.create_index('ix_c_preference_show_attributes_deleted', 'c_psa', ['deleted'], unique=False)
|
||||
op.drop_index(op.f('ix_c_prv_uid'), table_name='c_prv')
|
||||
op.drop_index(op.f('ix_c_prv_name'), table_name='c_prv')
|
||||
op.drop_index(op.f('ix_c_prv_deleted'), table_name='c_prv')
|
||||
op.create_index('ix_c_preference_relation_views_name', 'c_prv', ['name'], unique=False)
|
||||
op.create_index('ix_c_preference_relation_views_deleted', 'c_prv', ['deleted'], unique=False)
|
||||
op.alter_column('c_prv', 'uid',
|
||||
existing_type=mysql.INTEGER(),
|
||||
nullable=True)
|
||||
op.drop_index(op.f('ix_c_ci_types_uid'), table_name='c_ci_types')
|
||||
op.create_index('c_ci_types_uid', 'c_ci_types', ['uid'], unique=False)
|
||||
op.drop_index(op.f('ix_c_c_t_u_c_deleted'), table_name='c_c_t_u_c')
|
||||
op.create_index('ix_c_ci_type_unique_constraints_deleted', 'c_c_t_u_c', ['deleted'], unique=False)
|
||||
op.drop_index(op.f('ix_c_c_t_t_deleted'), table_name='c_c_t_t')
|
||||
op.create_index('ix_c_ci_type_triggers_deleted', 'c_c_t_t', ['deleted'], unique=False)
|
||||
op.drop_index(op.f('ix_c_c_d_deleted'), table_name='c_c_d')
|
||||
op.create_index('ix_c_custom_dashboard_deleted', 'c_c_d', ['deleted'], unique=False)
|
||||
op.drop_index(op.f('ix_c_attributes_uid'), table_name='c_attributes')
|
||||
op.create_index('idx_c_attributes_uid', 'c_attributes', ['uid'], unique=False)
|
||||
op.drop_column('c_attributes', 'choice_other')
|
||||
op.drop_index(op.f('ix_common_notice_config_deleted'), table_name='common_notice_config')
|
||||
op.drop_table('common_notice_config')
|
||||
op.drop_index(op.f('ix_common_data_deleted'), table_name='common_data')
|
||||
op.drop_table('common_data')
|
||||
# ### end Alembic commands ###
|
@@ -1,7 +1,7 @@
|
||||
-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
|
||||
@@ -12,35 +12,42 @@ Flask==2.3.2
|
||||
Flask-Bcrypt==1.0.1
|
||||
Flask-Caching==2.0.2
|
||||
Flask-Cors==4.0.0
|
||||
Flask-Login==0.6.2
|
||||
Flask-Login>=0.6.2
|
||||
Flask-Migrate==2.5.2
|
||||
Flask-RESTful==0.3.10
|
||||
Flask-SQLAlchemy==2.5.0
|
||||
future==0.18.3
|
||||
gunicorn==21.0.1
|
||||
hvac==2.0.0
|
||||
itsdangerous==2.1.2
|
||||
Jinja2==3.1.2
|
||||
jinja2schema==0.1.4
|
||||
jsonschema==4.18.0
|
||||
kombu==5.3.1
|
||||
kombu>=5.3.1
|
||||
Mako==1.2.4
|
||||
MarkupSafe==2.1.3
|
||||
marshmallow==2.20.2
|
||||
more-itertools==5.0.0
|
||||
msgpack-python==0.5.6
|
||||
Pillow==9.3.0
|
||||
pycryptodome==3.12.0
|
||||
Pillow>=10.0.1
|
||||
cryptography>=41.0.2
|
||||
PyJWT==2.4.0
|
||||
PyMySQL==1.1.0
|
||||
python-ldap==3.4.0
|
||||
ldap3==2.9.1
|
||||
PyYAML==6.0
|
||||
redis==4.6.0
|
||||
requests==2.31.0
|
||||
six==1.12.0
|
||||
requests_oauthlib==1.3.1
|
||||
markdownify==0.11.6
|
||||
six==1.16.0
|
||||
SQLAlchemy==1.4.49
|
||||
supervisor==4.0.3
|
||||
timeout-decorator==0.5.0
|
||||
toposort==1.10
|
||||
treelib==1.6.1
|
||||
Werkzeug==2.3.6
|
||||
Werkzeug>=2.3.6
|
||||
WTForms==3.0.0
|
||||
shamir~=17.12.0
|
||||
hvac~=2.0.0
|
||||
pycryptodomex>=3.19.0
|
||||
colorama>=0.4.6
|
||||
|
@@ -94,3 +94,12 @@ ES_HOST = '127.0.0.1'
|
||||
USE_ES = False
|
||||
|
||||
BOOL_TRUE = ['true', 'TRUE', 'True', True, '1', 1, "Yes", "YES", "yes", 'Y', 'y']
|
||||
|
||||
# # messenger
|
||||
USE_MESSENGER = True
|
||||
|
||||
# # secrets
|
||||
SECRETS_ENGINE = 'inner' # 'inner' or 'vault'
|
||||
VAULT_URL = ''
|
||||
VAULT_TOKEN = ''
|
||||
INNER_TRIGGER_TOKEN = ''
|
||||
|
@@ -17,8 +17,10 @@
|
||||
"@babel/plugin-syntax-import-meta": "^7.10.4",
|
||||
"@riophae/vue-treeselect": "^0.4.0",
|
||||
"@vue/composition-api": "^1.7.1",
|
||||
"@wangeditor/editor": "^5.1.23",
|
||||
"@wangeditor/editor-for-vue": "^1.0.0",
|
||||
"ant-design-vue": "^1.6.5",
|
||||
"axios": "0.18.0",
|
||||
"axios": "1.6.0",
|
||||
"babel-eslint": "^8.2.2",
|
||||
"butterfly-dag": "^4.3.26",
|
||||
"codemirror": "^5.65.13",
|
||||
@@ -37,6 +39,7 @@
|
||||
"moment": "^2.24.0",
|
||||
"nprogress": "^0.2.0",
|
||||
"relation-graph": "^1.1.0",
|
||||
"snabbdom": "^3.5.1",
|
||||
"sortablejs": "1.9.0",
|
||||
"viser-vue": "^2.4.8",
|
||||
"vue": "2.6.11",
|
||||
@@ -60,14 +63,15 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ant-design/colors": "^3.2.2",
|
||||
"@babel/core": "^7.23.2",
|
||||
"@babel/polyfill": "^7.2.5",
|
||||
"@babel/preset-env": "^7.23.2",
|
||||
"@vue/cli-plugin-babel": "4.5.17",
|
||||
"@vue/cli-plugin-eslint": "^4.0.5",
|
||||
"@vue/cli-plugin-unit-jest": "^4.0.5",
|
||||
"@vue/cli-service": "^4.0.5",
|
||||
"@vue/eslint-config-standard": "^4.0.0",
|
||||
"@vue/test-utils": "^1.0.0-beta.30",
|
||||
"babel-core": "7.0.0-bridge.0",
|
||||
"babel-jest": "^23.6.0",
|
||||
"babel-plugin-import": "^1.11.0",
|
||||
"babel-plugin-transform-remove-console": "^6.9.4",
|
||||
|
@@ -54,6 +54,90 @@
|
||||
<div class="content unicode" style="display: block;">
|
||||
<ul class="icon_lists dib-box">
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">icon-xianxing-password</div>
|
||||
<div class="code-name">&#xe894;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">icon-xianxing-link</div>
|
||||
<div class="code-name">&#xe895;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">itsm-oneclick download</div>
|
||||
<div class="code-name">&#xe892;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">itsm-package download</div>
|
||||
<div class="code-name">&#xe893;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">weixin</div>
|
||||
<div class="code-name">&#xe891;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">itsm-again</div>
|
||||
<div class="code-name">&#xe88f;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">itsm-next</div>
|
||||
<div class="code-name">&#xe890;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">wechatApp</div>
|
||||
<div class="code-name">&#xe88e;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">robot</div>
|
||||
<div class="code-name">&#xe88b;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">feishuApp</div>
|
||||
<div class="code-name">&#xe88c;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">dingdingApp</div>
|
||||
<div class="code-name">&#xe88d;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">email</div>
|
||||
<div class="code-name">&#xe88a;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">setting-feishu</div>
|
||||
<div class="code-name">&#xe887;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">setting-feishu-selected</div>
|
||||
<div class="code-name">&#xe888;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">cmdb-histogram</div>
|
||||
@@ -2100,6 +2184,12 @@
|
||||
<div class="code-name">&#xe738;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">ops-setting-notice-email-selected</div>
|
||||
<div class="code-name">&#xe889;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">ops-setting-notice</div>
|
||||
@@ -3954,9 +4044,9 @@
|
||||
<pre><code class="language-css"
|
||||
>@font-face {
|
||||
font-family: 'iconfont';
|
||||
src: url('iconfont.woff2?t=1694508259411') format('woff2'),
|
||||
url('iconfont.woff?t=1694508259411') format('woff'),
|
||||
url('iconfont.ttf?t=1694508259411') format('truetype');
|
||||
src: url('iconfont.woff2?t=1698273699449') format('woff2'),
|
||||
url('iconfont.woff?t=1698273699449') format('woff'),
|
||||
url('iconfont.ttf?t=1698273699449') format('truetype');
|
||||
}
|
||||
</code></pre>
|
||||
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
|
||||
@@ -3982,6 +4072,132 @@
|
||||
<div class="content font-class">
|
||||
<ul class="icon_lists dib-box">
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont icon-xianxing-password"></span>
|
||||
<div class="name">
|
||||
icon-xianxing-password
|
||||
</div>
|
||||
<div class="code-name">.icon-xianxing-password
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont icon-xianxing-link"></span>
|
||||
<div class="name">
|
||||
icon-xianxing-link
|
||||
</div>
|
||||
<div class="code-name">.icon-xianxing-link
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont a-itsm-oneclickdownload"></span>
|
||||
<div class="name">
|
||||
itsm-oneclick download
|
||||
</div>
|
||||
<div class="code-name">.a-itsm-oneclickdownload
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont a-itsm-packagedownload"></span>
|
||||
<div class="name">
|
||||
itsm-package download
|
||||
</div>
|
||||
<div class="code-name">.a-itsm-packagedownload
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont a-Frame4"></span>
|
||||
<div class="name">
|
||||
weixin
|
||||
</div>
|
||||
<div class="code-name">.a-Frame4
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont itsm-again"></span>
|
||||
<div class="name">
|
||||
itsm-again
|
||||
</div>
|
||||
<div class="code-name">.itsm-again
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont itsm-next"></span>
|
||||
<div class="name">
|
||||
itsm-next
|
||||
</div>
|
||||
<div class="code-name">.itsm-next
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont wechatApp"></span>
|
||||
<div class="name">
|
||||
wechatApp
|
||||
</div>
|
||||
<div class="code-name">.wechatApp
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont robot"></span>
|
||||
<div class="name">
|
||||
robot
|
||||
</div>
|
||||
<div class="code-name">.robot
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont feishuApp"></span>
|
||||
<div class="name">
|
||||
feishuApp
|
||||
</div>
|
||||
<div class="code-name">.feishuApp
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont dingdingApp"></span>
|
||||
<div class="name">
|
||||
dingdingApp
|
||||
</div>
|
||||
<div class="code-name">.dingdingApp
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont email"></span>
|
||||
<div class="name">
|
||||
email
|
||||
</div>
|
||||
<div class="code-name">.email
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont ops-setting-notice-feishu"></span>
|
||||
<div class="name">
|
||||
setting-feishu
|
||||
</div>
|
||||
<div class="code-name">.ops-setting-notice-feishu
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont ops-setting-notice-feishu-selected"></span>
|
||||
<div class="name">
|
||||
setting-feishu-selected
|
||||
</div>
|
||||
<div class="code-name">.ops-setting-notice-feishu-selected
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont cmdb-bar"></span>
|
||||
<div class="name">
|
||||
@@ -7051,6 +7267,15 @@
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont ops-setting-notice-email-selected-copy"></span>
|
||||
<div class="name">
|
||||
ops-setting-notice-email-selected
|
||||
</div>
|
||||
<div class="code-name">.ops-setting-notice-email-selected-copy
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont ops-setting-notice"></span>
|
||||
<div class="name">
|
||||
@@ -9832,6 +10057,118 @@
|
||||
<div class="content symbol">
|
||||
<ul class="icon_lists dib-box">
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-xianxing-password"></use>
|
||||
</svg>
|
||||
<div class="name">icon-xianxing-password</div>
|
||||
<div class="code-name">#icon-xianxing-password</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-xianxing-link"></use>
|
||||
</svg>
|
||||
<div class="name">icon-xianxing-link</div>
|
||||
<div class="code-name">#icon-xianxing-link</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#a-itsm-oneclickdownload"></use>
|
||||
</svg>
|
||||
<div class="name">itsm-oneclick download</div>
|
||||
<div class="code-name">#a-itsm-oneclickdownload</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#a-itsm-packagedownload"></use>
|
||||
</svg>
|
||||
<div class="name">itsm-package download</div>
|
||||
<div class="code-name">#a-itsm-packagedownload</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#a-Frame4"></use>
|
||||
</svg>
|
||||
<div class="name">weixin</div>
|
||||
<div class="code-name">#a-Frame4</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#itsm-again"></use>
|
||||
</svg>
|
||||
<div class="name">itsm-again</div>
|
||||
<div class="code-name">#itsm-again</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#itsm-next"></use>
|
||||
</svg>
|
||||
<div class="name">itsm-next</div>
|
||||
<div class="code-name">#itsm-next</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#wechatApp"></use>
|
||||
</svg>
|
||||
<div class="name">wechatApp</div>
|
||||
<div class="code-name">#wechatApp</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#robot"></use>
|
||||
</svg>
|
||||
<div class="name">robot</div>
|
||||
<div class="code-name">#robot</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#feishuApp"></use>
|
||||
</svg>
|
||||
<div class="name">feishuApp</div>
|
||||
<div class="code-name">#feishuApp</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#dingdingApp"></use>
|
||||
</svg>
|
||||
<div class="name">dingdingApp</div>
|
||||
<div class="code-name">#dingdingApp</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#email"></use>
|
||||
</svg>
|
||||
<div class="name">email</div>
|
||||
<div class="code-name">#email</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#ops-setting-notice-feishu"></use>
|
||||
</svg>
|
||||
<div class="name">setting-feishu</div>
|
||||
<div class="code-name">#ops-setting-notice-feishu</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#ops-setting-notice-feishu-selected"></use>
|
||||
</svg>
|
||||
<div class="name">setting-feishu-selected</div>
|
||||
<div class="code-name">#ops-setting-notice-feishu-selected</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#cmdb-bar"></use>
|
||||
@@ -12560,6 +12897,14 @@
|
||||
<div class="code-name">#ops-dot</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#ops-setting-notice-email-selected-copy"></use>
|
||||
</svg>
|
||||
<div class="name">ops-setting-notice-email-selected</div>
|
||||
<div class="code-name">#ops-setting-notice-email-selected-copy</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#ops-setting-notice"></use>
|
||||
|
@@ -1,8 +1,8 @@
|
||||
@font-face {
|
||||
font-family: "iconfont"; /* Project id 3857903 */
|
||||
src: url('iconfont.woff2?t=1694508259411') format('woff2'),
|
||||
url('iconfont.woff?t=1694508259411') format('woff'),
|
||||
url('iconfont.ttf?t=1694508259411') format('truetype');
|
||||
src: url('iconfont.woff2?t=1698273699449') format('woff2'),
|
||||
url('iconfont.woff?t=1698273699449') format('woff'),
|
||||
url('iconfont.ttf?t=1698273699449') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
@@ -13,6 +13,62 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-xianxing-password:before {
|
||||
content: "\e894";
|
||||
}
|
||||
|
||||
.icon-xianxing-link:before {
|
||||
content: "\e895";
|
||||
}
|
||||
|
||||
.a-itsm-oneclickdownload:before {
|
||||
content: "\e892";
|
||||
}
|
||||
|
||||
.a-itsm-packagedownload:before {
|
||||
content: "\e893";
|
||||
}
|
||||
|
||||
.a-Frame4:before {
|
||||
content: "\e891";
|
||||
}
|
||||
|
||||
.itsm-again:before {
|
||||
content: "\e88f";
|
||||
}
|
||||
|
||||
.itsm-next:before {
|
||||
content: "\e890";
|
||||
}
|
||||
|
||||
.wechatApp:before {
|
||||
content: "\e88e";
|
||||
}
|
||||
|
||||
.robot:before {
|
||||
content: "\e88b";
|
||||
}
|
||||
|
||||
.feishuApp:before {
|
||||
content: "\e88c";
|
||||
}
|
||||
|
||||
.dingdingApp:before {
|
||||
content: "\e88d";
|
||||
}
|
||||
|
||||
.email:before {
|
||||
content: "\e88a";
|
||||
}
|
||||
|
||||
.ops-setting-notice-feishu:before {
|
||||
content: "\e887";
|
||||
}
|
||||
|
||||
.ops-setting-notice-feishu-selected:before {
|
||||
content: "\e888";
|
||||
}
|
||||
|
||||
.cmdb-bar:before {
|
||||
content: "\e886";
|
||||
}
|
||||
@@ -1377,6 +1433,10 @@
|
||||
content: "\e738";
|
||||
}
|
||||
|
||||
.ops-setting-notice-email-selected-copy:before {
|
||||
content: "\e889";
|
||||
}
|
||||
|
||||
.ops-setting-notice:before {
|
||||
content: "\e72f";
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
@@ -5,6 +5,104 @@
|
||||
"css_prefix_text": "",
|
||||
"description": "",
|
||||
"glyphs": [
|
||||
{
|
||||
"icon_id": "37830610",
|
||||
"name": "icon-xianxing-password",
|
||||
"font_class": "icon-xianxing-password",
|
||||
"unicode": "e894",
|
||||
"unicode_decimal": 59540
|
||||
},
|
||||
{
|
||||
"icon_id": "37830609",
|
||||
"name": "icon-xianxing-link",
|
||||
"font_class": "icon-xianxing-link",
|
||||
"unicode": "e895",
|
||||
"unicode_decimal": 59541
|
||||
},
|
||||
{
|
||||
"icon_id": "37822199",
|
||||
"name": "itsm-oneclick download",
|
||||
"font_class": "a-itsm-oneclickdownload",
|
||||
"unicode": "e892",
|
||||
"unicode_decimal": 59538
|
||||
},
|
||||
{
|
||||
"icon_id": "37822198",
|
||||
"name": "itsm-package download",
|
||||
"font_class": "a-itsm-packagedownload",
|
||||
"unicode": "e893",
|
||||
"unicode_decimal": 59539
|
||||
},
|
||||
{
|
||||
"icon_id": "37772067",
|
||||
"name": "weixin",
|
||||
"font_class": "a-Frame4",
|
||||
"unicode": "e891",
|
||||
"unicode_decimal": 59537
|
||||
},
|
||||
{
|
||||
"icon_id": "37632784",
|
||||
"name": "itsm-again",
|
||||
"font_class": "itsm-again",
|
||||
"unicode": "e88f",
|
||||
"unicode_decimal": 59535
|
||||
},
|
||||
{
|
||||
"icon_id": "37632783",
|
||||
"name": "itsm-next",
|
||||
"font_class": "itsm-next",
|
||||
"unicode": "e890",
|
||||
"unicode_decimal": 59536
|
||||
},
|
||||
{
|
||||
"icon_id": "37590786",
|
||||
"name": "wechatApp",
|
||||
"font_class": "wechatApp",
|
||||
"unicode": "e88e",
|
||||
"unicode_decimal": 59534
|
||||
},
|
||||
{
|
||||
"icon_id": "37590798",
|
||||
"name": "robot",
|
||||
"font_class": "robot",
|
||||
"unicode": "e88b",
|
||||
"unicode_decimal": 59531
|
||||
},
|
||||
{
|
||||
"icon_id": "37590794",
|
||||
"name": "feishuApp",
|
||||
"font_class": "feishuApp",
|
||||
"unicode": "e88c",
|
||||
"unicode_decimal": 59532
|
||||
},
|
||||
{
|
||||
"icon_id": "37590791",
|
||||
"name": "dingdingApp",
|
||||
"font_class": "dingdingApp",
|
||||
"unicode": "e88d",
|
||||
"unicode_decimal": 59533
|
||||
},
|
||||
{
|
||||
"icon_id": "37590776",
|
||||
"name": "email",
|
||||
"font_class": "email",
|
||||
"unicode": "e88a",
|
||||
"unicode_decimal": 59530
|
||||
},
|
||||
{
|
||||
"icon_id": "37537876",
|
||||
"name": "setting-feishu",
|
||||
"font_class": "ops-setting-notice-feishu",
|
||||
"unicode": "e887",
|
||||
"unicode_decimal": 59527
|
||||
},
|
||||
{
|
||||
"icon_id": "37537859",
|
||||
"name": "setting-feishu-selected",
|
||||
"font_class": "ops-setting-notice-feishu-selected",
|
||||
"unicode": "e888",
|
||||
"unicode_decimal": 59528
|
||||
},
|
||||
{
|
||||
"icon_id": "37334642",
|
||||
"name": "cmdb-histogram",
|
||||
@@ -2392,6 +2490,13 @@
|
||||
"unicode": "e738",
|
||||
"unicode_decimal": 59192
|
||||
},
|
||||
{
|
||||
"icon_id": "37575490",
|
||||
"name": "ops-setting-notice-email-selected",
|
||||
"font_class": "ops-setting-notice-email-selected-copy",
|
||||
"unicode": "e889",
|
||||
"unicode_decimal": 59529
|
||||
},
|
||||
{
|
||||
"icon_id": "34108346",
|
||||
"name": "ops-setting-notice",
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -12,6 +12,9 @@ import zhCN from 'ant-design-vue/lib/locale-provider/zh_CN'
|
||||
import { AppDeviceEnquire } from '@/utils/mixin'
|
||||
import { debounce } from './utils/util'
|
||||
|
||||
import { h } from 'snabbdom'
|
||||
import { DomEditor, Boot } from '@wangeditor/editor'
|
||||
|
||||
export default {
|
||||
mixins: [AppDeviceEnquire],
|
||||
provide() {
|
||||
@@ -47,6 +50,134 @@ export default {
|
||||
this.$store.dispatch('setWindowSize')
|
||||
})
|
||||
)
|
||||
|
||||
// 注册富文本自定义元素
|
||||
const resume = {
|
||||
type: 'attachment',
|
||||
attachmentLabel: '',
|
||||
attachmentValue: '',
|
||||
children: [{ text: '' }], // void 元素必须有一个 children ,其中只有一个空字符串,重要!!!
|
||||
}
|
||||
|
||||
function withAttachment(editor) {
|
||||
// JS 语法
|
||||
const { isInline, isVoid } = editor
|
||||
const newEditor = editor
|
||||
|
||||
newEditor.isInline = (elem) => {
|
||||
const type = DomEditor.getNodeType(elem)
|
||||
if (type === 'attachment') return true // 针对 type: attachment ,设置为 inline
|
||||
return isInline(elem)
|
||||
}
|
||||
|
||||
newEditor.isVoid = (elem) => {
|
||||
const type = DomEditor.getNodeType(elem)
|
||||
if (type === 'attachment') return true // 针对 type: attachment ,设置为 void
|
||||
return isVoid(elem)
|
||||
}
|
||||
|
||||
return newEditor // 返回 newEditor ,重要!!!
|
||||
}
|
||||
Boot.registerPlugin(withAttachment)
|
||||
/**
|
||||
* 渲染“附件”元素到编辑器
|
||||
* @param elem 附件元素,即上文的 myResume
|
||||
* @param children 元素子节点,void 元素可忽略
|
||||
* @param editor 编辑器实例
|
||||
* @returns vnode 节点(通过 snabbdom.js 的 h 函数生成)
|
||||
*/
|
||||
function renderAttachment(elem, children, editor) {
|
||||
// JS 语法
|
||||
|
||||
// 获取“附件”的数据,参考上文 myResume 数据结构
|
||||
const { attachmentLabel = '', attachmentValue = '' } = elem
|
||||
|
||||
// 附件元素 vnode
|
||||
const attachVnode = h(
|
||||
// HTML tag
|
||||
'span',
|
||||
// HTML 属性、样式、事件
|
||||
{
|
||||
props: { contentEditable: false }, // HTML 属性,驼峰式写法
|
||||
style: {
|
||||
display: 'inline-block',
|
||||
margin: '0 3px',
|
||||
padding: '0 3px',
|
||||
backgroundColor: '#e6f7ff',
|
||||
border: '1px solid #91d5ff',
|
||||
borderRadius: '2px',
|
||||
color: '#1890ff',
|
||||
}, // style ,驼峰式写法
|
||||
on: {
|
||||
click() {
|
||||
console.log('clicked', attachmentValue)
|
||||
} /* 其他... */,
|
||||
},
|
||||
},
|
||||
// 子节点
|
||||
[attachmentLabel]
|
||||
)
|
||||
|
||||
return attachVnode
|
||||
}
|
||||
const renderElemConf = {
|
||||
type: 'attachment', // 新元素 type ,重要!!!
|
||||
renderElem: renderAttachment,
|
||||
}
|
||||
Boot.registerRenderElem(renderElemConf)
|
||||
|
||||
/**
|
||||
* 生成“附件”元素的 HTML
|
||||
* @param elem 附件元素,即上文的 myResume
|
||||
* @param childrenHtml 子节点的 HTML 代码,void 元素可忽略
|
||||
* @returns “附件”元素的 HTML 字符串
|
||||
*/
|
||||
function attachmentToHtml(elem, childrenHtml) {
|
||||
// JS 语法
|
||||
|
||||
// 获取附件元素的数据
|
||||
const { attachmentValue = '', attachmentLabel = '' } = elem
|
||||
|
||||
// 生成 HTML 代码
|
||||
const html = `<span data-w-e-type="attachment" data-w-e-is-void data-w-e-is-inline data-attachmentValue="${attachmentValue}" data-attachmentLabel="${attachmentLabel}">${attachmentLabel}</span>`
|
||||
|
||||
return html
|
||||
}
|
||||
const elemToHtmlConf = {
|
||||
type: 'attachment', // 新元素的 type ,重要!!!
|
||||
elemToHtml: attachmentToHtml,
|
||||
}
|
||||
Boot.registerElemToHtml(elemToHtmlConf)
|
||||
|
||||
/**
|
||||
* 解析 HTML 字符串,生成“附件”元素
|
||||
* @param domElem HTML 对应的 DOM Element
|
||||
* @param children 子节点
|
||||
* @param editor editor 实例
|
||||
* @returns “附件”元素,如上文的 myResume
|
||||
*/
|
||||
function parseAttachmentHtml(domElem, children, editor) {
|
||||
// JS 语法
|
||||
|
||||
// 从 DOM element 中获取“附件”的信息
|
||||
const attachmentValue = domElem.getAttribute('data-attachmentValue') || ''
|
||||
const attachmentLabel = domElem.getAttribute('data-attachmentLabel') || ''
|
||||
|
||||
// 生成“附件”元素(按照此前约定的数据结构)
|
||||
const myResume = {
|
||||
type: 'attachment',
|
||||
attachmentValue,
|
||||
attachmentLabel,
|
||||
children: [{ text: '' }], // void node 必须有 children ,其中有一个空字符串,重要!!!
|
||||
}
|
||||
|
||||
return myResume
|
||||
}
|
||||
const parseHtmlConf = {
|
||||
selector: 'span[data-w-e-type="attachment"]', // CSS 选择器,匹配特定的 HTML 标签
|
||||
parseElemHtml: parseAttachmentHtml,
|
||||
}
|
||||
Boot.registerParseElemHtml(parseHtmlConf)
|
||||
},
|
||||
beforeDestroy() {
|
||||
clearInterval(this.timer)
|
||||
|
@@ -79,13 +79,20 @@ export function updatePasswordByUid(uid, data) {
|
||||
})
|
||||
}
|
||||
|
||||
export function bindWxByUid(uid) {
|
||||
export function bindPlatformByUid(platform, uid) {
|
||||
return axios({
|
||||
url: `/common-setting/v1/employee/by_uid/bind_work_wechat/${uid}`,
|
||||
url: `/common-setting/v1/employee/by_uid/bind_notice/${platform}/${uid}`,
|
||||
method: 'put',
|
||||
})
|
||||
}
|
||||
|
||||
export function unbindPlatformByUid(platform, uid) {
|
||||
return axios({
|
||||
url: `/common-setting/v1/employee/by_uid/bind_notice/${platform}/${uid}`,
|
||||
method: 'delete',
|
||||
})
|
||||
}
|
||||
|
||||
export function getAllPosition() {
|
||||
return axios({
|
||||
url: `/common-setting/v1/employee/position`,
|
||||
@@ -117,3 +124,11 @@ export function getEmployeeListByFilter(data) {
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function getNoticeByEmployeeIds(data) {
|
||||
return axios({
|
||||
url: '/common-setting/v1/employee/get_notice_by_ids',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
40
cmdb-ui/src/api/noticeSetting.js
Normal file
40
cmdb-ui/src/api/noticeSetting.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import { axios } from '@/utils/request'
|
||||
|
||||
export function sendTestEmail(receive_address, data) {
|
||||
return axios({
|
||||
url: `/common-setting/v1/notice_config/send_test_email?receive_address=${receive_address}`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export const getNoticeConfigByPlatform = (platform) => {
|
||||
return axios({
|
||||
url: '/common-setting/v1/notice_config',
|
||||
method: 'get',
|
||||
params: { ...platform },
|
||||
})
|
||||
}
|
||||
|
||||
export const postNoticeConfigByPlatform = (data) => {
|
||||
return axios({
|
||||
url: '/common-setting/v1/notice_config',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export const putNoticeConfigByPlatform = (id, info) => {
|
||||
return axios({
|
||||
url: `/common-setting/v1/notice_config/${id}`,
|
||||
method: 'put',
|
||||
data: info
|
||||
})
|
||||
}
|
||||
|
||||
export const getNoticeConfigAppBot = () => {
|
||||
return axios({
|
||||
url: `/common-setting/v1/notice_config/app_bot`,
|
||||
method: 'get',
|
||||
})
|
||||
}
|
@@ -40,6 +40,8 @@
|
||||
}
|
||||
}
|
||||
"
|
||||
appendToBody
|
||||
:zIndex="1050"
|
||||
>
|
||||
<div
|
||||
:title="node.label"
|
||||
@@ -76,6 +78,8 @@
|
||||
}
|
||||
"
|
||||
@select="(value) => handleChangeExp(value, item, index)"
|
||||
appendToBody
|
||||
:zIndex="1050"
|
||||
>
|
||||
</treeselect>
|
||||
<treeselect
|
||||
@@ -97,6 +101,8 @@
|
||||
}
|
||||
}
|
||||
"
|
||||
appendToBody
|
||||
:zIndex="1050"
|
||||
>
|
||||
<div
|
||||
:title="node.label"
|
||||
@@ -135,6 +141,8 @@
|
||||
}
|
||||
}
|
||||
"
|
||||
appendToBody
|
||||
:zIndex="1050"
|
||||
>
|
||||
</treeselect>
|
||||
<a-input class="ops-input" v-model="item.value" size="small" style="width: 113px" />
|
||||
|
@@ -12,7 +12,10 @@
|
||||
<a-button type="primary" ghost>条件过滤<a-icon type="filter"/></a-button>
|
||||
</slot>
|
||||
<template slot="content">
|
||||
<Expression v-model="ruleList" :canSearchPreferenceAttrList="canSearchPreferenceAttrList" />
|
||||
<Expression
|
||||
v-model="ruleList"
|
||||
:canSearchPreferenceAttrList="canSearchPreferenceAttrList.filter((attr) => !attr.is_password)"
|
||||
/>
|
||||
<a-divider :style="{ margin: '10px 0' }" />
|
||||
<div style="width:534px">
|
||||
<a-space :style="{ display: 'flex', justifyContent: 'flex-end' }">
|
||||
@@ -22,7 +25,11 @@
|
||||
</div>
|
||||
</template>
|
||||
</a-popover>
|
||||
<Expression v-else v-model="ruleList" :canSearchPreferenceAttrList="canSearchPreferenceAttrList" />
|
||||
<Expression
|
||||
v-else
|
||||
v-model="ruleList"
|
||||
:canSearchPreferenceAttrList="canSearchPreferenceAttrList.filter((attr) => !attr.is_password)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -152,14 +159,15 @@ export default {
|
||||
})
|
||||
this.ruleList = [...expArray]
|
||||
} else if (open) {
|
||||
const _canSearchPreferenceAttrList = this.canSearchPreferenceAttrList.filter((attr) => !attr.is_password)
|
||||
this.ruleList = isInitOne
|
||||
? [
|
||||
{
|
||||
id: uuidv4(),
|
||||
type: 'and',
|
||||
property:
|
||||
this.canSearchPreferenceAttrList && this.canSearchPreferenceAttrList.length
|
||||
? this.canSearchPreferenceAttrList[0].name
|
||||
_canSearchPreferenceAttrList && _canSearchPreferenceAttrList.length
|
||||
? _canSearchPreferenceAttrList[0].name
|
||||
: undefined,
|
||||
exp: 'is',
|
||||
value: null,
|
||||
|
@@ -14,24 +14,6 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getPropertyStyle(attr) {
|
||||
switch (attr.value_type) {
|
||||
case '0':
|
||||
return { color: '#cf1322', backgroundColor: '#fff1f0' }
|
||||
case '1':
|
||||
return { color: '#d4b106', backgroundColor: '#feffe6' }
|
||||
case '2':
|
||||
return { color: '#d46b08', backgroundColor: '#fff7e6' }
|
||||
case '3':
|
||||
return { color: '#531dab', backgroundColor: '#f9f0ff' }
|
||||
case '4':
|
||||
return { color: '#389e0d', backgroundColor: '#f6ffed' }
|
||||
case '5':
|
||||
return { color: '#08979c', backgroundColor: '#e6fffb' }
|
||||
case '6':
|
||||
return { color: '#c41d7f', backgroundColor: '#fff0f6' }
|
||||
}
|
||||
},
|
||||
getPropertyIcon(attr) {
|
||||
switch (attr.value_type) {
|
||||
case '0':
|
||||
@@ -39,6 +21,12 @@ export default {
|
||||
case '1':
|
||||
return 'icon-xianxing-fudianshu'
|
||||
case '2':
|
||||
if (attr.is_password) {
|
||||
return 'icon-xianxing-password'
|
||||
}
|
||||
if (attr.is_link) {
|
||||
return 'icon-xianxing-link'
|
||||
}
|
||||
return 'icon-xianxing-wenben'
|
||||
case '3':
|
||||
return 'icon-xianxing-datetime'
|
||||
|
@@ -4,7 +4,7 @@ const appConfig = {
|
||||
buildAclToModules: true, // 是否在各个应用下 内联权限管理
|
||||
ssoLogoutURL: '/api/sso/logout',
|
||||
showDocs: false,
|
||||
useEncryption: true,
|
||||
useEncryption: false,
|
||||
}
|
||||
|
||||
export default appConfig
|
||||
|
@@ -99,7 +99,7 @@
|
||||
align="center"
|
||||
show-overflow>
|
||||
<template #default="{ row }">
|
||||
<span v-show="row.isGroup">
|
||||
<span v-show="isGroup">
|
||||
<a @click="handleDisplayMember(row)">成员</a>
|
||||
<a-divider type="vertical" />
|
||||
<a @click="handleGroupEdit(row)">编辑</a>
|
||||
|
@@ -35,8 +35,8 @@ export default {
|
||||
secret: '',
|
||||
},
|
||||
rules: {
|
||||
key: [{ required: true, message: 'key is required' }],
|
||||
secret: [{ required: true, message: 'secret is required' }],
|
||||
key: [{ required: false, message: 'key is required' }],
|
||||
secret: [{ required: false, message: 'secret is required' }],
|
||||
},
|
||||
visible: false,
|
||||
}
|
||||
|
@@ -111,10 +111,8 @@ export default {
|
||||
},
|
||||
async beforeMount() {
|
||||
this.loading = true
|
||||
await getOnDutyUser().then((res) => {
|
||||
this.onDutuUids = res.map((i) => i.uid)
|
||||
this.search()
|
||||
})
|
||||
await this.getOnDutyUser()
|
||||
this.search()
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
@@ -148,6 +146,11 @@ export default {
|
||||
inject: ['reload'],
|
||||
|
||||
methods: {
|
||||
async getOnDutyUser() {
|
||||
await getOnDutyUser().then((res) => {
|
||||
this.onDutuUids = res.map((i) => i.uid)
|
||||
})
|
||||
},
|
||||
search() {
|
||||
searchUser({ page_size: 10000 }).then((res) => {
|
||||
const ret = res.users.filter((u) => this.onDutuUids.includes(u.uid))
|
||||
@@ -162,8 +165,9 @@ export default {
|
||||
handleEdit(record) {
|
||||
this.$refs.userForm.handleEdit(record)
|
||||
},
|
||||
handleOk() {
|
||||
async handleOk() {
|
||||
this.searchName = ''
|
||||
await this.getOnDutyUser()
|
||||
this.search()
|
||||
},
|
||||
handleCreate() {
|
||||
|
@@ -168,3 +168,10 @@ export function calcComputedAttribute(attr_id) {
|
||||
method: 'PUT',
|
||||
})
|
||||
}
|
||||
|
||||
export function getAttrPassword(ci_id, attr_id) {
|
||||
return axios({
|
||||
url: `/v0.1/ci/${ci_id}/attributes/${attr_id}/password`,
|
||||
method: 'Get',
|
||||
})
|
||||
}
|
||||
|
@@ -1,13 +1,13 @@
|
||||
import { axios } from '@/utils/request'
|
||||
|
||||
export function getCIHistory (ciId) {
|
||||
export function getCIHistory(ciId) {
|
||||
return axios({
|
||||
url: `/v0.1/history/ci/${ciId}`,
|
||||
method: 'GET'
|
||||
})
|
||||
}
|
||||
|
||||
export function getCIHistoryTable (params) {
|
||||
export function getCIHistoryTable(params) {
|
||||
return axios({
|
||||
url: `/v0.1/history/records/attribute`,
|
||||
method: 'GET',
|
||||
@@ -15,7 +15,7 @@ export function getCIHistoryTable (params) {
|
||||
})
|
||||
}
|
||||
|
||||
export function getRelationTable (params) {
|
||||
export function getRelationTable(params) {
|
||||
return axios({
|
||||
url: `/v0.1/history/records/relation`,
|
||||
method: 'GET',
|
||||
@@ -23,7 +23,7 @@ export function getRelationTable (params) {
|
||||
})
|
||||
}
|
||||
|
||||
export function getCITypesTable (params) {
|
||||
export function getCITypesTable(params) {
|
||||
return axios({
|
||||
url: `/v0.1/history/ci_types`,
|
||||
method: 'GET',
|
||||
@@ -31,10 +31,26 @@ export function getCITypesTable (params) {
|
||||
})
|
||||
}
|
||||
|
||||
export function getUsers (params) {
|
||||
export function getUsers(params) {
|
||||
return axios({
|
||||
url: `/v1/acl/users/employee`,
|
||||
method: 'GET',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
export function getCiTriggers(params) {
|
||||
return axios({
|
||||
url: `/v0.1/history/ci_triggers`,
|
||||
method: 'GET',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
export function getCiTriggersByCiId(ci_id, params) {
|
||||
return axios({
|
||||
url: `/v0.1/history/ci_triggers/${ci_id}`,
|
||||
method: 'GET',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
BIN
cmdb-ui/src/modules/cmdb/assets/dashboard_empty.png
Normal file
BIN
cmdb-ui/src/modules/cmdb/assets/dashboard_empty.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 85 KiB |
@@ -0,0 +1,2 @@
|
||||
import NoticeContent from './index.vue'
|
||||
export default NoticeContent
|
199
cmdb-ui/src/modules/cmdb/components/noticeContent/index.vue
Normal file
199
cmdb-ui/src/modules/cmdb/components/noticeContent/index.vue
Normal file
@@ -0,0 +1,199 @@
|
||||
<template>
|
||||
<div class="notice-content">
|
||||
<div class="notice-content-main">
|
||||
<Toolbar
|
||||
:editor="editor"
|
||||
:defaultConfig="{
|
||||
excludeKeys: [
|
||||
'emotion',
|
||||
'group-image',
|
||||
'group-video',
|
||||
'insertTable',
|
||||
'codeBlock',
|
||||
'blockquote',
|
||||
'fullScreen',
|
||||
],
|
||||
}"
|
||||
mode="default"
|
||||
/>
|
||||
<Editor class="notice-content-editor" :defaultConfig="editorConfig" mode="simple" @onCreated="onCreated" />
|
||||
<div class="notice-content-sidebar">
|
||||
<template v-if="needOld">
|
||||
<div class="notice-content-sidebar-divider">变更前</div>
|
||||
<div
|
||||
@dblclick="dblclickSidebar(`old_${attr.name}`, attr.alias || attr.name)"
|
||||
class="notice-content-sidebar-item"
|
||||
v-for="attr in attrList"
|
||||
:key="`old_${attr.id}`"
|
||||
:title="attr.alias || attr.name"
|
||||
>
|
||||
{{ attr.alias || attr.name }}
|
||||
</div>
|
||||
<div class="notice-content-sidebar-divider">变更后</div>
|
||||
</template>
|
||||
<div
|
||||
@dblclick="dblclickSidebar(attr.name, attr.alias || attr.name)"
|
||||
class="notice-content-sidebar-item"
|
||||
v-for="attr in attrList"
|
||||
:key="attr.id"
|
||||
:title="attr.alias || attr.name"
|
||||
>
|
||||
{{ attr.alias || attr.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import '@wangeditor/editor/dist/css/style.css'
|
||||
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
|
||||
export default {
|
||||
name: 'NoticeContent',
|
||||
components: { Editor, Toolbar },
|
||||
props: {
|
||||
attrList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
needOld: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
editor: null,
|
||||
editorConfig: { placeholder: '请输入通知内容', readOnly: this.readOnly },
|
||||
content: '',
|
||||
defaultParams: [],
|
||||
value2LabelMap: {},
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
const editor = this.editor
|
||||
if (editor == null) return
|
||||
editor.destroy() // 组件销毁时,及时销毁编辑器
|
||||
},
|
||||
methods: {
|
||||
onCreated(editor) {
|
||||
this.editor = Object.seal(editor) // 一定要用 Object.seal() ,否则会报错
|
||||
},
|
||||
getContent() {
|
||||
const html = _.cloneDeep(this.editor.getHtml())
|
||||
const _html = html.replace(
|
||||
/<span data-w-e-type="attachment" (data-w-e-is-void|data-w-e-is-void="") (data-w-e-is-inline|data-w-e-is-inline="").*?<\/span>/gm,
|
||||
(value) => {
|
||||
const _match = value.match(/(?<=data-attachment(V|v)alue=").*?(?=")/)
|
||||
return `{{${_match[0]}}}`
|
||||
}
|
||||
)
|
||||
return { body_html: html, body: _html }
|
||||
},
|
||||
setContent(html) {
|
||||
this.editor.setHtml(html)
|
||||
},
|
||||
dblclickSidebar(value, label) {
|
||||
if (!this.readOnly) {
|
||||
this.editor.restoreSelection()
|
||||
|
||||
const node = {
|
||||
type: 'attachment',
|
||||
attachmentValue: value,
|
||||
attachmentLabel: `${label}`,
|
||||
children: [{ text: '' }],
|
||||
}
|
||||
this.editor.insertNode(node)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import '~@/style/static.less';
|
||||
.notice-content {
|
||||
width: 100%;
|
||||
& &-main {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
position: relative;
|
||||
.notice-content-editor {
|
||||
height: 300px;
|
||||
width: 75%;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-top: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
.notice-content-sidebar {
|
||||
width: 25%;
|
||||
position: absolute;
|
||||
height: 300px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-top: none;
|
||||
border-right: none;
|
||||
overflow: auto;
|
||||
.notice-content-sidebar-divider {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: #afafaf;
|
||||
background-color: #fff;
|
||||
line-height: 20px;
|
||||
padding-left: 12px;
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
border-top: 1px solid #d1d1d1;
|
||||
top: 50%;
|
||||
transition: translateY(-50%);
|
||||
}
|
||||
&::before {
|
||||
left: 3px;
|
||||
width: 5px;
|
||||
}
|
||||
&::after {
|
||||
right: 3px;
|
||||
width: 78px;
|
||||
}
|
||||
}
|
||||
.notice-content-sidebar-item:first-child {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.notice-content-sidebar-item {
|
||||
line-height: 1.5;
|
||||
padding: 4px 12px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
&:hover {
|
||||
background-color: #custom_colors[color_2];
|
||||
color: #custom_colors[color_1];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
@import '~@/style/static.less';
|
||||
|
||||
.notice-content {
|
||||
.w-e-bar {
|
||||
background-color: #custom_colors[color_2];
|
||||
}
|
||||
.w-e-text-placeholder {
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -2,32 +2,29 @@
|
||||
<div>
|
||||
<span v-if="!isShow && !isTableLoading">{{ showPassword }}</span>
|
||||
<span v-else>{{ password }}</span>
|
||||
<a
|
||||
:style="{ marginLeft: '10px' }"
|
||||
@click="
|
||||
() => {
|
||||
isShow = !isShow
|
||||
}
|
||||
"
|
||||
><a-icon
|
||||
:type="isShow ? 'eye-invisible' : 'eye'"
|
||||
/></a>
|
||||
<a :style="{ marginLeft: '10px' }" @click="getPassword"><a-icon :type="isShow ? 'eye-invisible' : 'eye'"/></a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import { getAttrPassword } from '../../api/CITypeAttr'
|
||||
export default {
|
||||
name: 'PasswordField',
|
||||
props: {
|
||||
password: {
|
||||
type: String,
|
||||
default: '',
|
||||
ci_id: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
attr_id: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isShow: false,
|
||||
password: '',
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -36,6 +33,18 @@ export default {
|
||||
},
|
||||
...mapState('cmdbStore', ['isTableLoading']),
|
||||
},
|
||||
methods: {
|
||||
getPassword() {
|
||||
if (this.isShow) {
|
||||
this.isShow = false
|
||||
} else {
|
||||
getAttrPassword(this.ci_id, this.attr_id).then((res) => {
|
||||
this.password = res.value
|
||||
this.isShow = true
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
|
144
cmdb-ui/src/modules/cmdb/components/webhook/authorization.vue
Normal file
144
cmdb-ui/src/modules/cmdb/components/webhook/authorization.vue
Normal file
@@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<div class="authorization-wrapper">
|
||||
<div class="authorization-header">
|
||||
<a-space>
|
||||
<span>Authorization Type</span>
|
||||
<a-select size="small" v-model="authorizationType" style="width: 200px" :showSearch="true">
|
||||
<a-select-option value="none">
|
||||
None
|
||||
</a-select-option>
|
||||
<a-select-option value="BasicAuth">
|
||||
Basic Auth
|
||||
</a-select-option>
|
||||
<a-select-option value="Bearer">
|
||||
Bearer
|
||||
</a-select-option>
|
||||
<a-select-option value="APIKey">
|
||||
APIKey
|
||||
</a-select-option>
|
||||
<a-select-option value="OAuth2.0">
|
||||
OAuth2.0
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-space>
|
||||
</div>
|
||||
<div style="margin-top:10px">
|
||||
<table v-if="authorizationType === 'BasicAuth'">
|
||||
<tr>
|
||||
<td><a-input class="authorization-input" v-model="BasicAuth.username" placeholder="用户名" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a-input class="authorization-input" v-model="BasicAuth.password" placeholder="密码" /></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table v-else-if="authorizationType === 'Bearer'">
|
||||
<tr>
|
||||
<td><a-input class="authorization-input" v-model="Bearer.token" placeholder="token" /></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table v-else-if="authorizationType === 'APIKey'">
|
||||
<tr>
|
||||
<td><a-input class="authorization-input" v-model="APIKey.key" placeholder="key" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a-input class="authorization-input" v-model="APIKey.value" placeholder="value" /></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table v-else-if="authorizationType === 'OAuth2.0'">
|
||||
<tr>
|
||||
<td><a-input class="authorization-input" v-model="OAuth2.client_id" placeholder="client_id" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a-input class="authorization-input" v-model="OAuth2.client_secret" placeholder="client_secret" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a-input
|
||||
class="authorization-input"
|
||||
v-model="OAuth2.authorization_base_url"
|
||||
placeholder="authorization_base_url"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a-input class="authorization-input" v-model="OAuth2.token_url" placeholder="token_url" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a-input class="authorization-input" v-model="OAuth2.redirect_url" placeholder="redirect_url" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a-input class="authorization-input" v-model="OAuth2.scope" placeholder="scope" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<a-empty
|
||||
v-else
|
||||
:image-style="{
|
||||
height: '60px',
|
||||
}"
|
||||
>
|
||||
<img slot="image" :src="require('@/assets/data_empty.png')" />
|
||||
<span slot="description"> 暂无请求认证 </span>
|
||||
</a-empty>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Authorization',
|
||||
data() {
|
||||
return {
|
||||
authorizationType: 'none',
|
||||
BasicAuth: {
|
||||
username: '',
|
||||
password: '',
|
||||
},
|
||||
Bearer: {
|
||||
token: '',
|
||||
},
|
||||
APIKey: {
|
||||
key: '',
|
||||
value: '',
|
||||
},
|
||||
OAuth2: {
|
||||
client_id: '',
|
||||
client_secret: '',
|
||||
authorization_base_url: '',
|
||||
token_url: '',
|
||||
redirect_url: '',
|
||||
scope: '',
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.authorization-wrapper {
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
table,
|
||||
td,
|
||||
th {
|
||||
border: 1px solid #f3f4f6;
|
||||
}
|
||||
.authorization-input {
|
||||
border: none;
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
92
cmdb-ui/src/modules/cmdb/components/webhook/body.vue
Normal file
92
cmdb-ui/src/modules/cmdb/components/webhook/body.vue
Normal file
@@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<div class="body-wrapper">
|
||||
<div class="body-header">
|
||||
<!-- <a-space>
|
||||
<span>Content Type</span>
|
||||
<a-select size="small" v-model="contentType" style="width: 200px" :showSearch="true">
|
||||
<a-select-option value="none">
|
||||
None
|
||||
</a-select-option>
|
||||
<a-select-opt-group v-for="item in segmentedContentTypes" :key="item.title" :label="item.title">
|
||||
<a-select-option v-for="ele in item.contentTypes" :key="ele" :value="ele">
|
||||
{{ ele }}
|
||||
</a-select-option>
|
||||
</a-select-opt-group>
|
||||
</a-select>
|
||||
</a-space> -->
|
||||
</div>
|
||||
<div style="margin-top:10px">
|
||||
<codemirror style="z-index: 9999" :options="cmOptions" v-model="jsonData"></codemirror>
|
||||
<!-- <a-empty
|
||||
v-else
|
||||
:image-style="{
|
||||
height: '60px',
|
||||
}"
|
||||
>
|
||||
<img slot="image" :src="require('@/assets/data_empty.png')" />
|
||||
<span slot="description"> 暂无请求体 </span>
|
||||
</a-empty> -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { codemirror } from 'vue-codemirror'
|
||||
import 'codemirror/lib/codemirror.css'
|
||||
|
||||
require('codemirror/mode/python/python.js')
|
||||
|
||||
export default {
|
||||
name: 'Body',
|
||||
components: { codemirror },
|
||||
data() {
|
||||
const segmentedContentTypes = [
|
||||
{
|
||||
title: 'text',
|
||||
contentTypes: [
|
||||
'application/json',
|
||||
'application/ld+json',
|
||||
'application/hal+json',
|
||||
'application/vnd.api+json',
|
||||
'application/xml',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'structured',
|
||||
contentTypes: ['application/x-www-form-urlencoded', 'multipart/form-data'],
|
||||
},
|
||||
{
|
||||
title: 'others',
|
||||
contentTypes: ['text/html', 'text/plain'],
|
||||
},
|
||||
]
|
||||
return {
|
||||
segmentedContentTypes,
|
||||
// contentType: 'none',
|
||||
jsonData: '',
|
||||
cmOptions: {
|
||||
lineNumbers: true,
|
||||
mode: 'python',
|
||||
height: '200px',
|
||||
tabSize: 4,
|
||||
lineWrapping: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
<style lang="less">
|
||||
.body-wrapper {
|
||||
div.jsoneditor-menu {
|
||||
display: none;
|
||||
}
|
||||
div.jsoneditor {
|
||||
border-color: #f3f4f6;
|
||||
.jsoneditor-outer {
|
||||
border-color: #f3f4f6;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user