mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-12-21 03:26:03 +00:00
Compare commits
377 Commits
v3.10.5
...
rdp-patch-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab777aeb18 | ||
|
|
a497b3cf94 | ||
|
|
8548b73063 | ||
|
|
182320f492 | ||
|
|
40d326d6a6 | ||
|
|
b87554f9db | ||
|
|
3c255f9fa6 | ||
|
|
a6b5437f6a | ||
|
|
71c690ef9e | ||
|
|
bee07db900 | ||
|
|
115eb7c15a | ||
|
|
88810263cd | ||
|
|
3d95bc4656 | ||
|
|
69de08bb5d | ||
|
|
5c234fdd0c | ||
|
|
be5baa5a3f | ||
|
|
2f1a65f120 | ||
|
|
e6d02eaf4c | ||
|
|
6d6dec2752 | ||
|
|
c6c067c44b | ||
|
|
84ec1b047a | ||
|
|
e6dca2ec14 | ||
|
|
8793003d18 | ||
|
|
29fd6e0ae4 | ||
|
|
90587a83cc | ||
|
|
dfa0198742 | ||
|
|
9e857b54ed | ||
|
|
c34358509b | ||
|
|
a7c46109d9 | ||
|
|
48fa6172bd | ||
|
|
89aa87fd6b | ||
|
|
79d230755e | ||
|
|
99082f261e | ||
|
|
7e2100b435 | ||
|
|
185d4e9563 | ||
|
|
ecaa84790c | ||
|
|
30210dc0b9 | ||
|
|
ff699f4ee2 | ||
|
|
48239b0c63 | ||
|
|
f4f74909a8 | ||
|
|
cab1e0bf52 | ||
|
|
bf195c1599 | ||
|
|
7f5f7e81b8 | ||
|
|
99affad9b9 | ||
|
|
34eea024f8 | ||
|
|
1d1e4b90ed | ||
|
|
f5d40a787e | ||
|
|
77d8083c00 | ||
|
|
180303ccb4 | ||
|
|
9cd1619990 | ||
|
|
7d0a901522 | ||
|
|
5e9fabff1b | ||
|
|
1d36934111 | ||
|
|
25603e4758 | ||
|
|
3ae164d7e0 | ||
|
|
3ad64e142e | ||
|
|
0ff1413780 | ||
|
|
f5b64bed4e | ||
|
|
a559415b65 | ||
|
|
2e7bd076f4 | ||
|
|
11f6fe0bf9 | ||
|
|
ae94648e80 | ||
|
|
94e08f3d96 | ||
|
|
8bedef92f0 | ||
|
|
e5bb28231a | ||
|
|
b5aeb24ae9 | ||
|
|
674ea7142f | ||
|
|
5ab7b99b9d | ||
|
|
9cd163c99d | ||
|
|
e72073f0cc | ||
|
|
690f525afc | ||
|
|
6686afcec1 | ||
|
|
0918f5c6f6 | ||
|
|
891e3d5609 | ||
|
|
9fad591545 | ||
|
|
1ed1c3a536 | ||
|
|
63824d3491 | ||
|
|
96eadf060c | ||
|
|
2c9128b0e7 | ||
|
|
7d6fd0f881 | ||
|
|
4e996afd5e | ||
|
|
1ed745d042 | ||
|
|
39ebbfcf10 | ||
|
|
d0ec4f798b | ||
|
|
1712a9a104 | ||
|
|
951aafcabd | ||
|
|
e46da9d741 | ||
|
|
06aaf9e3d0 | ||
|
|
5ac9fb81dc | ||
|
|
fb907e250c | ||
|
|
34ea40a14d | ||
|
|
8c560b0317 | ||
|
|
df9cc4700b | ||
|
|
6aa1227e60 | ||
|
|
296c788e28 | ||
|
|
a1ae29d35e | ||
|
|
139ffd0b47 | ||
|
|
ff6c1aef7f | ||
|
|
9b6b48a7f1 | ||
|
|
2b133a8085 | ||
|
|
81b5f1ce93 | ||
|
|
c646084c51 | ||
|
|
5e69c03cb7 | ||
|
|
e2df85bddd | ||
|
|
d710697fa9 | ||
|
|
a955fcd682 | ||
|
|
1816d52d21 | ||
|
|
d7c26cab7d | ||
|
|
dc894fdc2d | ||
|
|
742ef89bef | ||
|
|
3d4fc56592 | ||
|
|
45291aba0c | ||
|
|
495ee99e29 | ||
|
|
223eb8ad38 | ||
|
|
370e959400 | ||
|
|
b82f007787 | ||
|
|
1faeb54673 | ||
|
|
04e102cb87 | ||
|
|
81027cd561 | ||
|
|
cf727d22c0 | ||
|
|
bb6d077645 | ||
|
|
a78ccc9667 | ||
|
|
d70351e6b3 | ||
|
|
4e76207adb | ||
|
|
7a12c3737f | ||
|
|
8450c49e25 | ||
|
|
ab6d8df2f0 | ||
|
|
550115c39f | ||
|
|
9c23512d91 | ||
|
|
30054b286a | ||
|
|
22d7385891 | ||
|
|
1701bedb41 | ||
|
|
165d030c8e | ||
|
|
9be77cf58f | ||
|
|
887724bad4 | ||
|
|
b283d88781 | ||
|
|
2977323800 | ||
|
|
4a520e9e10 | ||
|
|
44f29e166c | ||
|
|
f42113afb9 | ||
|
|
ff126f3459 | ||
|
|
66cd6e95a8 | ||
|
|
b28aec527f | ||
|
|
496903dfb2 | ||
|
|
0a0312695b | ||
|
|
3608b025e5 | ||
|
|
68244b2b37 | ||
|
|
948e9ecb4b | ||
|
|
7ad4d9116a | ||
|
|
9439035b86 | ||
|
|
2b220d3753 | ||
|
|
440a7ae9cc | ||
|
|
40a4efc992 | ||
|
|
15d4fafbdb | ||
|
|
48b037ac26 | ||
|
|
dfd133cf5a | ||
|
|
cdfb11549e | ||
|
|
0d825927e1 | ||
|
|
4e8d7df005 | ||
|
|
5d1829b998 | ||
|
|
75df845024 | ||
|
|
c103253867 | ||
|
|
81da9e018a | ||
|
|
7f90fccc4f | ||
|
|
4ebcba81e0 | ||
|
|
5616d31888 | ||
|
|
606d2c8933 | ||
|
|
a534c496d0 | ||
|
|
a11097fb5a | ||
|
|
d4c1f93ef6 | ||
|
|
9168e92669 | ||
|
|
bfd030d70f | ||
|
|
da0c017c4f | ||
|
|
5ffc0a9665 | ||
|
|
10e9026ec7 | ||
|
|
7c4c0b5924 | ||
|
|
42c3008ec9 | ||
|
|
2f6d743cf0 | ||
|
|
e8faaeb8fb | ||
|
|
0ea675f8d6 | ||
|
|
77caa5536f | ||
|
|
52c905832b | ||
|
|
94ee3169dc | ||
|
|
92b6286feb | ||
|
|
bce776bb63 | ||
|
|
dc39cbf037 | ||
|
|
38175d6b57 | ||
|
|
7408ed0f03 | ||
|
|
5135186961 | ||
|
|
5be399616b | ||
|
|
46a23afbec | ||
|
|
8c4add241d | ||
|
|
feee92daee | ||
|
|
42054c7989 | ||
|
|
9b20b67039 | ||
|
|
2acc84dc69 | ||
|
|
3383d0f314 | ||
|
|
c9858b5a84 | ||
|
|
25e21b185f | ||
|
|
720231f692 | ||
|
|
95f29a584e | ||
|
|
50cbb75b96 | ||
|
|
d418647774 | ||
|
|
6b5d4a4810 | ||
|
|
2cc67634a4 | ||
|
|
52922088a9 | ||
|
|
ef7329a721 | ||
|
|
ad0bc82539 | ||
|
|
1ecf8534f6 | ||
|
|
94286caec4 | ||
|
|
d4c8425218 | ||
|
|
59f9a4f369 | ||
|
|
64125051df | ||
|
|
660572a0ea | ||
|
|
c0273dc698 | ||
|
|
2782d4b5f1 | ||
|
|
d4f9e30306 | ||
|
|
1b221d1cb6 | ||
|
|
fbf42ebbf9 | ||
|
|
a0c4eae04c | ||
|
|
d1c293940a | ||
|
|
6f2d04a029 | ||
|
|
29dbc2e4d4 | ||
|
|
e8d717d174 | ||
|
|
138a3a2f46 | ||
|
|
cade2cfa13 | ||
|
|
ac988a76b4 | ||
|
|
5a9815481a | ||
|
|
bfbddfdead | ||
|
|
3cf526fdf3 | ||
|
|
f6a4ee54d0 | ||
|
|
5755d281d7 | ||
|
|
1569524583 | ||
|
|
7ba876eb0a | ||
|
|
a31ea77b3c | ||
|
|
44445a9482 | ||
|
|
b8449a6efa | ||
|
|
ccf6b00084 | ||
|
|
4423f842e0 | ||
|
|
7660e3228e | ||
|
|
482f5613e4 | ||
|
|
3cfb46f798 | ||
|
|
f0d1279a42 | ||
|
|
140118c9c6 | ||
|
|
637b9b1b15 | ||
|
|
969069dde0 | ||
|
|
84a71c8b3a | ||
|
|
f3bd727c32 | ||
|
|
2ac87e4ad6 | ||
|
|
3740a4ad6f | ||
|
|
3bc8db7c3d | ||
|
|
f3d19ad9f4 | ||
|
|
d2396afdd5 | ||
|
|
43f9c07838 | ||
|
|
6052306c04 | ||
|
|
6a12bc39e9 | ||
|
|
3f67b40975 | ||
|
|
0adc854721 | ||
|
|
ab76745a9f | ||
|
|
574639d5e1 | ||
|
|
fa5d9d3df4 | ||
|
|
0c31925131 | ||
|
|
94b5d8b9e9 | ||
|
|
bffc9f4b1d | ||
|
|
6b5d18222e | ||
|
|
2b05fd5276 | ||
|
|
3e46d72ba3 | ||
|
|
6502adb772 | ||
|
|
a8112c86e3 | ||
|
|
8911c9c649 | ||
|
|
3b70b4cf9e | ||
|
|
1e0ea3905e | ||
|
|
79f8480ae4 | ||
|
|
dec502e025 | ||
|
|
c7b5cc7d89 | ||
|
|
bc76ce50e1 | ||
|
|
be90bf6b28 | ||
|
|
dfa68d1ca8 | ||
|
|
0237edf6c1 | ||
|
|
6a87221c2a | ||
|
|
f0e87ef3f8 | ||
|
|
cd19a276c9 | ||
|
|
5ea4bba676 | ||
|
|
8c93d419fe | ||
|
|
2530827d07 | ||
|
|
8e54c446bc | ||
|
|
3456e9ac5b | ||
|
|
689f858f97 | ||
|
|
93eebd7876 | ||
|
|
82cc21ef59 | ||
|
|
e61f9efbf2 | ||
|
|
45bac09dc7 | ||
|
|
989a970a7c | ||
|
|
0296df0480 | ||
|
|
9776d35140 | ||
|
|
0aeea414f5 | ||
|
|
9817154234 | ||
|
|
39ae14877b | ||
|
|
9c238a9147 | ||
|
|
42d7e983e4 | ||
|
|
611d0b71e8 | ||
|
|
d78d55091c | ||
|
|
3b8aab8c25 | ||
|
|
2f16bdc4be | ||
|
|
22d70eb416 | ||
|
|
afa1ba4f6b | ||
|
|
39d3e5477c | ||
|
|
d499b94e04 | ||
|
|
7a6468530f | ||
|
|
02893c2a2b | ||
|
|
4470b68de9 | ||
|
|
d3d89b0853 | ||
|
|
681cecc52b | ||
|
|
3336a4526b | ||
|
|
bca0863952 | ||
|
|
bf1a29fac2 | ||
|
|
47ceaf967c | ||
|
|
00c5b3c0a2 | ||
|
|
3aeadc2f03 | ||
|
|
f0cbd77310 | ||
|
|
f11852c60d | ||
|
|
8b870678df | ||
|
|
470a088a9f | ||
|
|
ccd4f3ada4 | ||
|
|
ae7a562b85 | ||
|
|
be6d8566da | ||
|
|
f264bf03ff | ||
|
|
02c2ee8c54 | ||
|
|
d71374ca8a | ||
|
|
0589f7fe33 | ||
|
|
a5e8792092 | ||
|
|
15acfe84b0 | ||
|
|
08b483140c | ||
|
|
cf1e048328 | ||
|
|
a6228f145d | ||
|
|
b6ab3df038 | ||
|
|
e9f591b33b | ||
|
|
90d4914280 | ||
|
|
80a506e99f | ||
|
|
d8a891a7d7 | ||
|
|
d71c41e384 | ||
|
|
bb27ff7f8a | ||
|
|
0671e56d65 | ||
|
|
73a4ce0943 | ||
|
|
902fac61e9 | ||
|
|
dcd7f9f7e6 | ||
|
|
80035e7cb6 | ||
|
|
e2d14f5e4b | ||
|
|
a27cc22596 | ||
|
|
72362274ce | ||
|
|
cfb1d306a3 | ||
|
|
e5cb99d682 | ||
|
|
cbd812ab5f | ||
|
|
d0117b5a91 | ||
|
|
afe3777895 | ||
|
|
e45676edc4 | ||
|
|
60e4b19d07 | ||
|
|
86d76c53d6 | ||
|
|
b50f1a662d | ||
|
|
b3e4c10bc2 | ||
|
|
ba11e646d6 | ||
|
|
6de524c797 | ||
|
|
2e067a7950 | ||
|
|
a3658136e2 | ||
|
|
4108415894 | ||
|
|
ae2fdff9a7 | ||
|
|
b9422c096e | ||
|
|
b3e73605b0 | ||
|
|
6c89349194 | ||
|
|
670eac49b6 | ||
|
|
a7a099f290 | ||
|
|
5157514c62 | ||
|
|
533d2ab98a | ||
|
|
40730b741d | ||
|
|
786cb23f98 | ||
|
|
518ae3fa09 | ||
|
|
18707d365b |
11
.github/ISSUE_TEMPLATE/----.md
vendored
11
.github/ISSUE_TEMPLATE/----.md
vendored
@@ -1,11 +0,0 @@
|
||||
---
|
||||
name: 需求建议
|
||||
about: 提出针对本项目的想法和建议
|
||||
title: "[Feature] "
|
||||
labels: 类型:需求
|
||||
assignees:
|
||||
- ibuler
|
||||
- baijiangjie
|
||||
---
|
||||
|
||||
**请描述您的需求或者改进建议.**
|
||||
72
.github/ISSUE_TEMPLATE/1_bug_report.yml
vendored
Normal file
72
.github/ISSUE_TEMPLATE/1_bug_report.yml
vendored
Normal file
@@ -0,0 +1,72 @@
|
||||
name: '🐛 Bug Report'
|
||||
description: 'Report an Bug'
|
||||
title: '[Bug] '
|
||||
labels: ['🐛 Bug']
|
||||
assignees:
|
||||
- baijiangjie
|
||||
body:
|
||||
- type: input
|
||||
attributes:
|
||||
label: 'Product Version'
|
||||
description: The versions prior to v2.28 (inclusive) are no longer supported.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: 'Product Edition'
|
||||
options:
|
||||
- label: 'Community Edition'
|
||||
- label: 'Enterprise Edition'
|
||||
- label: 'Enterprise Trial Edition'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: 'Installation Method'
|
||||
options:
|
||||
- label: 'Online Installation (One-click command installation)'
|
||||
- label: 'Offline Package Installation'
|
||||
- label: 'All-in-One'
|
||||
- label: '1Panel'
|
||||
- label: 'Kubernetes'
|
||||
- label: 'Source Code'
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 'Environment Information'
|
||||
description: Please provide a clear and concise description outlining your environment information.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: '🐛 Bug Description'
|
||||
description:
|
||||
Please provide a clear and concise description of the defect. If the issue is complex, please provide detailed explanations. <br/>
|
||||
Unclear descriptions will not be processed. Please ensure you provide enough detail and information to support replicating and fixing the defect.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 'Recurrence Steps'
|
||||
description: Please provide a clear and concise description outlining how to reproduce the issue.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 'Expected Behavior'
|
||||
description: Please provide a clear and concise description of what you expect to happen.
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 'Additional Information'
|
||||
description: Please add any additional background information about the issue here.
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 'Attempted Solutions'
|
||||
description: If you have already attempted to solve the issue, please list the solutions you have tried here.
|
||||
72
.github/ISSUE_TEMPLATE/1_bug_report_cn.yml
vendored
Normal file
72
.github/ISSUE_TEMPLATE/1_bug_report_cn.yml
vendored
Normal file
@@ -0,0 +1,72 @@
|
||||
name: '🐛 反馈缺陷'
|
||||
description: '反馈一个缺陷'
|
||||
title: '[Bug] '
|
||||
labels: ['🐛 Bug']
|
||||
assignees:
|
||||
- baijiangjie
|
||||
body:
|
||||
- type: input
|
||||
attributes:
|
||||
label: '产品版本'
|
||||
description: 不再支持 v2.28(含)之前的版本。
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: '版本类型'
|
||||
options:
|
||||
- label: '社区版'
|
||||
- label: '企业版'
|
||||
- label: '企业试用版'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: '安装方式'
|
||||
options:
|
||||
- label: '在线安装 (一键命令安装)'
|
||||
- label: '离线包安装'
|
||||
- label: 'All-in-One'
|
||||
- label: '1Panel'
|
||||
- label: 'Kubernetes'
|
||||
- label: '源码安装'
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: '环境信息'
|
||||
description: 请提供一个清晰且简洁的描述,说明你的环境信息。
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: '🐛 缺陷描述'
|
||||
description: |
|
||||
请提供一个清晰且简洁的缺陷描述,如果问题比较复杂,也请详细说明。<br/>
|
||||
针对不清晰的描述信息将不予处理。请确保提供足够的细节和信息,以支持对缺陷进行复现和修复。
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: '复现步骤'
|
||||
description: 请提供一个清晰且简洁的描述,说明如何复现问题。
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: '期望结果'
|
||||
description: 请提供一个清晰且简洁的描述,说明你期望发生什么。
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: '补充信息'
|
||||
description: 在这里添加关于问题的任何其他背景信息。
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: '尝试过的解决方案'
|
||||
description: 如果你已经尝试解决问题,请在此列出你尝试过的解决方案。
|
||||
56
.github/ISSUE_TEMPLATE/2_feature_request.yml
vendored
Normal file
56
.github/ISSUE_TEMPLATE/2_feature_request.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
name: '⭐️ Feature Request'
|
||||
description: 'Suggest an idea'
|
||||
title: '[Feature] '
|
||||
labels: ['⭐️ Feature Request']
|
||||
assignees:
|
||||
- baijiangjie
|
||||
- ibuler
|
||||
body:
|
||||
- type: input
|
||||
attributes:
|
||||
label: 'Product Version'
|
||||
description: The versions prior to v2.28 (inclusive) are no longer supported.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: 'Product Edition'
|
||||
options:
|
||||
- label: 'Community Edition'
|
||||
- label: 'Enterprise Edition'
|
||||
- label: 'Enterprise Trial Edition'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: 'Installation Method'
|
||||
options:
|
||||
- label: 'Online Installation (One-click command installation)'
|
||||
- label: 'Offline Package Installation'
|
||||
- label: 'All-in-One'
|
||||
- label: '1Panel'
|
||||
- label: 'Kubernetes'
|
||||
- label: 'Source Code'
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: '⭐️ Feature Description'
|
||||
description: |
|
||||
Please add a clear and concise description of the problem you aim to solve with this feature request.<br/>
|
||||
Unclear descriptions will not be processed.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 'Proposed Solution'
|
||||
description: Please provide a clear and concise description of the solution you desire.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 'Additional Information'
|
||||
description: Please add any additional background information about the issue here.
|
||||
56
.github/ISSUE_TEMPLATE/2_feature_request_cn.yml
vendored
Normal file
56
.github/ISSUE_TEMPLATE/2_feature_request_cn.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
name: '⭐️ 功能需求'
|
||||
description: '提出需求或建议'
|
||||
title: '[Feature] '
|
||||
labels: ['⭐️ Feature Request']
|
||||
assignees:
|
||||
- baijiangjie
|
||||
- ibuler
|
||||
body:
|
||||
- type: input
|
||||
attributes:
|
||||
label: '产品版本'
|
||||
description: 不再支持 v2.28(含)之前的版本。
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: '版本类型'
|
||||
options:
|
||||
- label: '社区版'
|
||||
- label: '企业版'
|
||||
- label: '企业试用版'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: '安装方式'
|
||||
options:
|
||||
- label: '在线安装 (一键命令安装)'
|
||||
- label: '离线包安装'
|
||||
- label: 'All-in-One'
|
||||
- label: '1Panel'
|
||||
- label: 'Kubernetes'
|
||||
- label: '源码安装'
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: '⭐️ 需求描述'
|
||||
description: |
|
||||
请添加一个清晰且简洁的问题描述,阐述你希望通过这个功能需求解决的问题。<br/>
|
||||
针对不清晰的描述信息将不予处理。
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: '解决方案'
|
||||
description: 请清晰且简洁地描述你想要的解决方案。
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: '补充信息'
|
||||
description: 在这里添加关于问题的任何其他背景信息。
|
||||
60
.github/ISSUE_TEMPLATE/3_question.yml
vendored
Normal file
60
.github/ISSUE_TEMPLATE/3_question.yml
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
name: '🤔 Question'
|
||||
description: 'Pose a question'
|
||||
title: '[Question] '
|
||||
labels: ['🤔 Question']
|
||||
assignees:
|
||||
- baijiangjie
|
||||
body:
|
||||
- type: input
|
||||
attributes:
|
||||
label: 'Product Version'
|
||||
description: The versions prior to v2.28 (inclusive) are no longer supported.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: 'Product Edition'
|
||||
options:
|
||||
- label: 'Community Edition'
|
||||
- label: 'Enterprise Edition'
|
||||
- label: 'Enterprise Trial Edition'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: 'Installation Method'
|
||||
options:
|
||||
- label: 'Online Installation (One-click command installation)'
|
||||
- label: 'Offline Package Installation'
|
||||
- label: 'All-in-One'
|
||||
- label: '1Panel'
|
||||
- label: 'Kubernetes'
|
||||
- label: 'Source Code'
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 'Environment Information'
|
||||
description: Please provide a clear and concise description outlining your environment information.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: '🤔 Question Description'
|
||||
description: |
|
||||
Please provide a clear and concise description of the defect. If the issue is complex, please provide detailed explanations. <br/>
|
||||
Unclear descriptions will not be processed.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 'Expected Behavior'
|
||||
description: Please provide a clear and concise description of what you expect to happen.
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 'Additional Information'
|
||||
description: Please add any additional background information about the issue here.
|
||||
61
.github/ISSUE_TEMPLATE/3_question_cn.yml
vendored
Normal file
61
.github/ISSUE_TEMPLATE/3_question_cn.yml
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
name: '🤔 问题咨询'
|
||||
description: '提出一个问题'
|
||||
title: '[Question] '
|
||||
labels: ['🤔 Question']
|
||||
assignees:
|
||||
- baijiangjie
|
||||
body:
|
||||
- type: input
|
||||
attributes:
|
||||
label: '产品版本'
|
||||
description: 不再支持 v2.28(含)之前的版本。
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: '版本类型'
|
||||
options:
|
||||
- label: '社区版'
|
||||
- label: '企业版'
|
||||
- label: '企业试用版'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: '安装方式'
|
||||
options:
|
||||
- label: '在线安装 (一键命令安装)'
|
||||
- label: '离线包安装'
|
||||
- label: 'All-in-One'
|
||||
- label: '1Panel'
|
||||
- label: 'Kubernetes'
|
||||
- label: '源码安装'
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: '环境信息'
|
||||
description: 请在此详细描述你的环境信息,如操作系统、浏览器和部署架构等。
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: '🤔 问题描述'
|
||||
description: |
|
||||
请提供一个清晰且简洁的问题描述,如果问题比较复杂,也请详细说明。<br/>
|
||||
针对不清晰的描述信息将不予处理。
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: '期望结果'
|
||||
description: 请提供一个清晰且简洁的描述,说明你期望发生什么。
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: '补充信息'
|
||||
description: 在这里添加关于问题的任何其他背景信息。
|
||||
|
||||
22
.github/ISSUE_TEMPLATE/bug---.md
vendored
22
.github/ISSUE_TEMPLATE/bug---.md
vendored
@@ -1,22 +0,0 @@
|
||||
---
|
||||
name: Bug 提交
|
||||
about: 提交产品缺陷帮助我们更好的改进
|
||||
title: "[Bug] "
|
||||
labels: 类型:Bug
|
||||
assignees:
|
||||
- baijiangjie
|
||||
---
|
||||
|
||||
**JumpServer 版本( v2.28 之前的版本不再支持 )**
|
||||
|
||||
|
||||
**浏览器版本**
|
||||
|
||||
|
||||
**Bug 描述**
|
||||
|
||||
|
||||
**Bug 重现步骤(有截图更好)**
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
10
.github/ISSUE_TEMPLATE/question.md
vendored
10
.github/ISSUE_TEMPLATE/question.md
vendored
@@ -1,10 +0,0 @@
|
||||
---
|
||||
name: 问题咨询
|
||||
about: 提出针对本项目安装部署、使用及其他方面的相关问题
|
||||
title: "[Question] "
|
||||
labels: 类型:提问
|
||||
assignees:
|
||||
- baijiangjie
|
||||
---
|
||||
|
||||
**请描述您的问题.**
|
||||
4
.github/workflows/issue-close-require.yml
vendored
4
.github/workflows/issue-close-require.yml
vendored
@@ -12,7 +12,9 @@ jobs:
|
||||
uses: actions-cool/issues-helper@v2
|
||||
with:
|
||||
actions: 'close-issues'
|
||||
labels: '状态:待反馈'
|
||||
labels: '⏳ Pending feedback'
|
||||
inactive-day: 30
|
||||
body: |
|
||||
You haven't provided feedback for over 30 days.
|
||||
We will close this issue. If you have any further needs, you can reopen it or submit a new issue.
|
||||
您超过 30 天未反馈信息,我们将关闭该 issue,如有需求您可以重新打开或者提交新的 issue。
|
||||
|
||||
2
.github/workflows/issue-close.yml
vendored
2
.github/workflows/issue-close.yml
vendored
@@ -13,4 +13,4 @@ jobs:
|
||||
if: ${{ !github.event.issue.pull_request }}
|
||||
with:
|
||||
actions: 'remove-labels'
|
||||
labels: '状态:待处理,状态:待反馈'
|
||||
labels: '🔔 Pending processing,⏳ Pending feedback'
|
||||
8
.github/workflows/issue-comment.yml
vendored
8
.github/workflows/issue-comment.yml
vendored
@@ -13,13 +13,13 @@ jobs:
|
||||
uses: actions-cool/issues-helper@v2
|
||||
with:
|
||||
actions: 'add-labels'
|
||||
labels: '状态:待处理'
|
||||
labels: '🔔 Pending processing'
|
||||
|
||||
- name: Remove require reply label
|
||||
uses: actions-cool/issues-helper@v2
|
||||
with:
|
||||
actions: 'remove-labels'
|
||||
labels: '状态:待反馈'
|
||||
labels: '⏳ Pending feedback'
|
||||
|
||||
add-label-if-is-member:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -55,11 +55,11 @@ jobs:
|
||||
uses: actions-cool/issues-helper@v2
|
||||
with:
|
||||
actions: 'add-labels'
|
||||
labels: '状态:待反馈'
|
||||
labels: '⏳ Pending feedback'
|
||||
|
||||
- name: Remove require handle label
|
||||
if: contains(steps.member_names.outputs.data, github.event.comment.user.login)
|
||||
uses: actions-cool/issues-helper@v2
|
||||
with:
|
||||
actions: 'remove-labels'
|
||||
labels: '状态:待处理'
|
||||
labels: '🔔 Pending processing'
|
||||
|
||||
2
.github/workflows/issue-open.yml
vendored
2
.github/workflows/issue-open.yml
vendored
@@ -13,4 +13,4 @@ jobs:
|
||||
if: ${{ !github.event.issue.pull_request }}
|
||||
with:
|
||||
actions: 'add-labels'
|
||||
labels: '状态:待处理'
|
||||
labels: '🔔 Pending processing'
|
||||
45
.github/workflows/jms-build-test.yml
vendored
45
.github/workflows/jms-build-test.yml
vendored
@@ -1,26 +1,32 @@
|
||||
name: "Run Build Test"
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- pr@*
|
||||
- repr@*
|
||||
paths:
|
||||
- 'Dockerfile'
|
||||
- 'Dockerfile-*'
|
||||
- 'pyproject.toml'
|
||||
- 'poetry.lock'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: docker/setup-qemu-action@v3
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
- uses: docker/setup-qemu-action@v2
|
||||
- name: Check Dockerfile
|
||||
run: |
|
||||
test -f Dockerfile-ce || cp -f Dockerfile Dockerfile-ce
|
||||
|
||||
- uses: docker/setup-buildx-action@v2
|
||||
|
||||
- uses: docker/build-push-action@v3
|
||||
- name: Build CE Image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
tags: jumpserver/core-ce:test
|
||||
file: Dockerfile-ce
|
||||
tags: jumpserver/core-ce:test
|
||||
platforms: linux/amd64
|
||||
build-args: |
|
||||
APT_MIRROR=http://deb.debian.org
|
||||
PIP_MIRROR=https://pypi.org/simple
|
||||
@@ -28,9 +34,22 @@ jobs:
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- uses: LouisBrunner/checks-action@v1.5.0
|
||||
if: always()
|
||||
- name: Prepare EE Image
|
||||
run: |
|
||||
sed -i 's@^FROM registry.fit2cloud.com@# FROM registry.fit2cloud.com@g' Dockerfile-ee
|
||||
sed -i 's@^COPY --from=build-xpack@# COPY --from=build-xpack@g' Dockerfile-ee
|
||||
|
||||
- name: Build EE Image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
name: Check Build
|
||||
conclusion: ${{ job.status }}
|
||||
context: .
|
||||
push: false
|
||||
file: Dockerfile-ee
|
||||
tags: jumpserver/core-ee:test
|
||||
platforms: linux/amd64
|
||||
build-args: |
|
||||
APT_MIRROR=http://deb.debian.org
|
||||
PIP_MIRROR=https://pypi.org/simple
|
||||
PIP_JMS_MIRROR=https://pypi.org/simple
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
@@ -10,3 +10,4 @@ jobs:
|
||||
- uses: jumpserver/action-generic-handler@master
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PRIVATE_TOKEN }}
|
||||
I18N_TOKEN: ${{ secrets.I18N_TOKEN }}
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -43,3 +43,4 @@ releashe
|
||||
data/*
|
||||
test.py
|
||||
.history/
|
||||
.test/
|
||||
|
||||
137
Dockerfile
Normal file
137
Dockerfile
Normal file
@@ -0,0 +1,137 @@
|
||||
FROM python:3.11-slim-bullseye AS stage-1
|
||||
ARG TARGETARCH
|
||||
|
||||
ARG VERSION
|
||||
ENV VERSION=$VERSION
|
||||
|
||||
WORKDIR /opt/jumpserver
|
||||
ADD . .
|
||||
RUN echo > /opt/jumpserver/config.yml \
|
||||
&& cd utils && bash -ixeu build.sh
|
||||
|
||||
FROM python:3.11-slim-bullseye as stage-2
|
||||
ARG TARGETARCH
|
||||
|
||||
ARG BUILD_DEPENDENCIES=" \
|
||||
g++ \
|
||||
make \
|
||||
pkg-config"
|
||||
|
||||
ARG DEPENDENCIES=" \
|
||||
freetds-dev \
|
||||
libffi-dev \
|
||||
libjpeg-dev \
|
||||
libkrb5-dev \
|
||||
libldap2-dev \
|
||||
libpq-dev \
|
||||
libsasl2-dev \
|
||||
libssl-dev \
|
||||
libxml2-dev \
|
||||
libxmlsec1-dev \
|
||||
libxmlsec1-openssl \
|
||||
freerdp2-dev \
|
||||
libaio-dev"
|
||||
|
||||
ARG TOOLS=" \
|
||||
ca-certificates \
|
||||
curl \
|
||||
default-libmysqlclient-dev \
|
||||
default-mysql-client \
|
||||
git \
|
||||
git-lfs \
|
||||
unzip \
|
||||
xz-utils \
|
||||
wget"
|
||||
|
||||
ARG APT_MIRROR=http://mirrors.ustc.edu.cn
|
||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core-apt \
|
||||
--mount=type=cache,target=/var/lib/apt,sharing=locked,id=core-apt \
|
||||
sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \
|
||||
&& rm -f /etc/apt/apt.conf.d/docker-clean \
|
||||
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||
&& apt-get update \
|
||||
&& apt-get -y install --no-install-recommends ${BUILD_DEPENDENCIES} \
|
||||
&& apt-get -y install --no-install-recommends ${DEPENDENCIES} \
|
||||
&& apt-get -y install --no-install-recommends ${TOOLS} \
|
||||
&& echo "no" | dpkg-reconfigure dash
|
||||
|
||||
WORKDIR /opt/jumpserver
|
||||
|
||||
ARG PIP_MIRROR=https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
RUN --mount=type=cache,target=/root/.cache \
|
||||
--mount=type=bind,source=poetry.lock,target=/opt/jumpserver/poetry.lock \
|
||||
--mount=type=bind,source=pyproject.toml,target=/opt/jumpserver/pyproject.toml \
|
||||
set -ex \
|
||||
&& python3 -m venv /opt/py3 \
|
||||
&& pip install poetry -i ${PIP_MIRROR} \
|
||||
&& poetry config virtualenvs.create false \
|
||||
&& . /opt/py3/bin/activate \
|
||||
&& poetry install
|
||||
|
||||
FROM python:3.11-slim-bullseye
|
||||
ARG TARGETARCH
|
||||
ENV LANG=zh_CN.UTF-8 \
|
||||
PATH=/opt/py3/bin:$PATH
|
||||
|
||||
ARG DEPENDENCIES=" \
|
||||
libjpeg-dev \
|
||||
libpq-dev \
|
||||
libx11-dev \
|
||||
freerdp2-dev \
|
||||
libxmlsec1-openssl"
|
||||
|
||||
ARG TOOLS=" \
|
||||
ca-certificates \
|
||||
curl \
|
||||
default-libmysqlclient-dev \
|
||||
default-mysql-client \
|
||||
iputils-ping \
|
||||
locales \
|
||||
netcat-openbsd \
|
||||
nmap \
|
||||
openssh-client \
|
||||
patch \
|
||||
sshpass \
|
||||
telnet \
|
||||
vim \
|
||||
bubblewrap \
|
||||
wget"
|
||||
|
||||
ARG APT_MIRROR=http://mirrors.ustc.edu.cn
|
||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core-apt \
|
||||
--mount=type=cache,target=/var/lib/apt,sharing=locked,id=core-apt \
|
||||
sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \
|
||||
&& rm -f /etc/apt/apt.conf.d/docker-clean \
|
||||
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||
&& apt-get update \
|
||||
&& apt-get -y install --no-install-recommends ${DEPENDENCIES} \
|
||||
&& apt-get -y install --no-install-recommends ${TOOLS} \
|
||||
&& mkdir -p /root/.ssh/ \
|
||||
&& echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null\n\tCiphers +aes128-cbc\n\tKexAlgorithms +diffie-hellman-group1-sha1\n\tHostKeyAlgorithms +ssh-rsa" > /root/.ssh/config \
|
||||
&& echo "no" | dpkg-reconfigure dash \
|
||||
&& echo "zh_CN.UTF-8" | dpkg-reconfigure locales \
|
||||
&& sed -i "s@# export @export @g" ~/.bashrc \
|
||||
&& sed -i "s@# alias @alias @g" ~/.bashrc
|
||||
|
||||
ARG RECEPTOR_VERSION=v1.4.5
|
||||
RUN set -ex \
|
||||
&& wget -O /opt/receptor.tar.gz https://github.com/ansible/receptor/releases/download/${RECEPTOR_VERSION}/receptor_${RECEPTOR_VERSION/v/}_linux_${TARGETARCH}.tar.gz \
|
||||
&& tar -xf /opt/receptor.tar.gz -C /usr/local/bin/ \
|
||||
&& chown root:root /usr/local/bin/receptor \
|
||||
&& chmod 755 /usr/local/bin/receptor \
|
||||
&& rm -f /opt/receptor.tar.gz
|
||||
|
||||
COPY --from=stage-2 /opt/py3 /opt/py3
|
||||
COPY --from=stage-1 /opt/jumpserver/release/jumpserver /opt/jumpserver
|
||||
COPY --from=stage-1 /opt/jumpserver/release/jumpserver/apps/libs/ansible/ansible.cfg /etc/ansible/
|
||||
|
||||
WORKDIR /opt/jumpserver
|
||||
|
||||
ARG VERSION
|
||||
ENV VERSION=$VERSION
|
||||
|
||||
VOLUME /opt/jumpserver/data
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
ENTRYPOINT ["./entrypoint.sh"]
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM python:3.11-slim-bullseye as stage-1
|
||||
FROM python:3.11-slim-bullseye AS stage-1
|
||||
ARG TARGETARCH
|
||||
|
||||
ARG VERSION
|
||||
@@ -87,12 +87,14 @@ ARG TOOLS=" \
|
||||
default-mysql-client \
|
||||
iputils-ping \
|
||||
locales \
|
||||
netcat-openbsd \
|
||||
nmap \
|
||||
openssh-client \
|
||||
patch \
|
||||
sshpass \
|
||||
telnet \
|
||||
vim \
|
||||
bubblewrap \
|
||||
wget"
|
||||
|
||||
ARG APT_MIRROR=http://mirrors.ustc.edu.cn
|
||||
@@ -111,8 +113,17 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core-apt \
|
||||
&& sed -i "s@# export @export @g" ~/.bashrc \
|
||||
&& sed -i "s@# alias @alias @g" ~/.bashrc
|
||||
|
||||
ARG RECEPTOR_VERSION=v1.4.5
|
||||
RUN set -ex \
|
||||
&& wget -O /opt/receptor.tar.gz https://github.com/ansible/receptor/releases/download/${RECEPTOR_VERSION}/receptor_${RECEPTOR_VERSION/v/}_linux_${TARGETARCH}.tar.gz \
|
||||
&& tar -xf /opt/receptor.tar.gz -C /usr/local/bin/ \
|
||||
&& chown root:root /usr/local/bin/receptor \
|
||||
&& chmod 755 /usr/local/bin/receptor \
|
||||
&& rm -f /opt/receptor.tar.gz
|
||||
|
||||
COPY --from=stage-2 /opt/py3 /opt/py3
|
||||
COPY --from=stage-1 /opt/jumpserver/release/jumpserver /opt/jumpserver
|
||||
COPY --from=stage-1 /opt/jumpserver/release/jumpserver/apps/libs/ansible/ansible.cfg /etc/ansible/
|
||||
|
||||
WORKDIR /opt/jumpserver
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
ARG VERSION
|
||||
FROM registry.fit2cloud.com/jumpserver/xpack:${VERSION} as build-xpack
|
||||
FROM registry.fit2cloud.com/jumpserver/xpack:${VERSION} AS build-xpack
|
||||
FROM registry.fit2cloud.com/jumpserver/core-ce:${VERSION}
|
||||
|
||||
COPY --from=build-xpack /opt/xpack /opt/jumpserver/apps/xpack
|
||||
@@ -85,7 +85,7 @@ If you find a security problem, please contact us directly:
|
||||
- 400-052-0755
|
||||
|
||||
### License & Copyright
|
||||
Copyright (c) 2014-2022 FIT2CLOUD Tech, Inc., All rights reserved.
|
||||
Copyright (c) 2014-2024 FIT2CLOUD Tech, Inc., All rights reserved.
|
||||
|
||||
Licensed under The GNU General Public License version 3 (GPLv3) (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
|
||||
|
||||
|
||||
@@ -18,9 +18,8 @@ __all__ = [
|
||||
|
||||
class AccountBackupPlanViewSet(OrgBulkModelViewSet):
|
||||
model = AccountBackupAutomation
|
||||
filter_fields = ('name',)
|
||||
search_fields = filter_fields
|
||||
ordering = ('name',)
|
||||
filterset_fields = ('name',)
|
||||
search_fields = filterset_fields
|
||||
serializer_class = serializers.AccountBackupSerializer
|
||||
|
||||
|
||||
|
||||
@@ -20,8 +20,8 @@ __all__ = [
|
||||
class AutomationAssetsListApi(generics.ListAPIView):
|
||||
model = BaseAutomation
|
||||
serializer_class = serializers.AutomationAssetsSerializer
|
||||
filter_fields = ("name", "address")
|
||||
search_fields = filter_fields
|
||||
filterset_fields = ("name", "address")
|
||||
search_fields = filterset_fields
|
||||
|
||||
def get_object(self):
|
||||
pk = self.kwargs.get('pk')
|
||||
|
||||
@@ -6,9 +6,12 @@ from rest_framework.response import Response
|
||||
|
||||
from accounts import serializers
|
||||
from accounts.const import AutomationTypes
|
||||
from accounts.filters import ChangeSecretRecordFilterSet
|
||||
from accounts.models import ChangeSecretAutomation, ChangeSecretRecord
|
||||
from accounts.tasks import execute_automation_record_task
|
||||
from authentication.permissions import UserConfirmation, ConfirmType
|
||||
from orgs.mixins.api import OrgBulkModelViewSet, OrgGenericViewSet
|
||||
from rbac.permissions import RBACPermission
|
||||
from .base import (
|
||||
AutomationAssetsListApi, AutomationRemoveAssetApi, AutomationAddAssetApi,
|
||||
AutomationNodeAddRemoveApi, AutomationExecutionViewSet
|
||||
@@ -24,35 +27,54 @@ __all__ = [
|
||||
|
||||
class ChangeSecretAutomationViewSet(OrgBulkModelViewSet):
|
||||
model = ChangeSecretAutomation
|
||||
filter_fields = ('name', 'secret_type', 'secret_strategy')
|
||||
search_fields = filter_fields
|
||||
filterset_fields = ('name', 'secret_type', 'secret_strategy')
|
||||
search_fields = filterset_fields
|
||||
serializer_class = serializers.ChangeSecretAutomationSerializer
|
||||
|
||||
|
||||
class ChangeSecretRecordViewSet(mixins.ListModelMixin, OrgGenericViewSet):
|
||||
serializer_class = serializers.ChangeSecretRecordSerializer
|
||||
filterset_fields = ('asset_id', 'execution_id')
|
||||
filterset_class = ChangeSecretRecordFilterSet
|
||||
search_fields = ('asset__address',)
|
||||
tp = AutomationTypes.change_secret
|
||||
serializer_classes = {
|
||||
'default': serializers.ChangeSecretRecordSerializer,
|
||||
'secret': serializers.ChangeSecretRecordViewSecretSerializer,
|
||||
}
|
||||
rbac_perms = {
|
||||
'execute': 'accounts.add_changesecretexecution',
|
||||
'secret': 'accounts.view_changesecretrecord',
|
||||
}
|
||||
|
||||
def get_permissions(self):
|
||||
if self.action == 'secret':
|
||||
self.permission_classes = [
|
||||
RBACPermission,
|
||||
UserConfirmation.require(ConfirmType.MFA)
|
||||
]
|
||||
return super().get_permissions()
|
||||
|
||||
def get_queryset(self):
|
||||
return ChangeSecretRecord.objects.all()
|
||||
|
||||
@action(methods=['post'], detail=False, url_path='execute')
|
||||
def execute(self, request, *args, **kwargs):
|
||||
record_id = request.data.get('record_id')
|
||||
record = self.get_queryset().filter(pk=record_id)
|
||||
if not record:
|
||||
record_ids = request.data.get('record_ids')
|
||||
records = self.get_queryset().filter(id__in=record_ids)
|
||||
execution_count = records.values_list('execution_id', flat=True).distinct().count()
|
||||
if execution_count != 1:
|
||||
return Response(
|
||||
{'detail': 'record not found'},
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
{'detail': 'Only one execution is allowed to execute'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
task = execute_automation_record_task.delay(record_id, self.tp)
|
||||
task = execute_automation_record_task.delay(record_ids, self.tp)
|
||||
return Response({'task': task.id}, status=status.HTTP_200_OK)
|
||||
|
||||
@action(methods=['get'], detail=True, url_path='secret')
|
||||
def secret(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
serializer = self.get_serializer(instance)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class ChangSecretExecutionViewSet(AutomationExecutionViewSet):
|
||||
rbac_perms = (
|
||||
|
||||
@@ -20,8 +20,8 @@ __all__ = [
|
||||
|
||||
class GatherAccountsAutomationViewSet(OrgBulkModelViewSet):
|
||||
model = GatherAccountsAutomation
|
||||
filter_fields = ('name',)
|
||||
search_fields = filter_fields
|
||||
filterset_fields = ('name',)
|
||||
search_fields = filterset_fields
|
||||
serializer_class = serializers.GatherAccountAutomationSerializer
|
||||
|
||||
|
||||
|
||||
@@ -20,8 +20,8 @@ __all__ = [
|
||||
|
||||
class PushAccountAutomationViewSet(OrgBulkModelViewSet):
|
||||
model = PushAccountAutomation
|
||||
filter_fields = ('name', 'secret_type', 'secret_strategy')
|
||||
search_fields = filter_fields
|
||||
filterset_fields = ('name', 'secret_type', 'secret_strategy')
|
||||
search_fields = filterset_fields
|
||||
serializer_class = serializers.PushAccountAutomationSerializer
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ from django.conf import settings
|
||||
from rest_framework import serializers
|
||||
from xlsxwriter import Workbook
|
||||
|
||||
from accounts.const.automation import AccountBackupType
|
||||
from accounts.const import AccountBackupType
|
||||
from accounts.models.automations.backup_account import AccountBackupAutomation
|
||||
from accounts.notifications import AccountBackupExecutionTaskMsg, AccountBackupByObjStorageExecutionTaskMsg
|
||||
from accounts.serializers import AccountSecretSerializer
|
||||
|
||||
@@ -13,11 +13,13 @@
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_secret_type: "{{ jms_account.secret_type }}"
|
||||
login_private_key_path: "{{ jms_account.private_key_path }}"
|
||||
become: "{{ custom_become | default(False) }}"
|
||||
become_method: "{{ custom_become_method | default('su') }}"
|
||||
become_user: "{{ custom_become_user | default('') }}"
|
||||
become_password: "{{ custom_become_password | default('') }}"
|
||||
become_private_key_path: "{{ custom_become_private_key_path | default(None) }}"
|
||||
become: "{{ jms_custom_become | default(False) }}"
|
||||
become_method: "{{ jms_custom_become_method | default('su') }}"
|
||||
become_user: "{{ jms_custom_become_user | default('') }}"
|
||||
become_password: "{{ jms_custom_become_password | default('') }}"
|
||||
become_private_key_path: "{{ jms_custom_become_private_key_path | default(None) }}"
|
||||
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
||||
register: ping_info
|
||||
delegate_to: localhost
|
||||
|
||||
@@ -29,11 +31,11 @@
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_secret_type: "{{ jms_account.secret_type }}"
|
||||
login_private_key_path: "{{ jms_account.private_key_path }}"
|
||||
become: "{{ custom_become | default(False) }}"
|
||||
become_method: "{{ custom_become_method | default('su') }}"
|
||||
become_user: "{{ custom_become_user | default('') }}"
|
||||
become_password: "{{ custom_become_password | default('') }}"
|
||||
become_private_key_path: "{{ custom_become_private_key_path | default(None) }}"
|
||||
become: "{{ jms_custom_become | default(False) }}"
|
||||
become_method: "{{ jms_custom_become_method | default('su') }}"
|
||||
become_user: "{{ jms_custom_become_user | default('') }}"
|
||||
become_password: "{{ jms_custom_become_password | default('') }}"
|
||||
become_private_key_path: "{{ jms_custom_become_private_key_path | default(None) }}"
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret }}"
|
||||
commands: "{{ params.commands }}"
|
||||
@@ -54,4 +56,6 @@
|
||||
become_user: "{{ account.become.ansible_user | default('') }}"
|
||||
become_password: "{{ account.become.ansible_password | default('') }}"
|
||||
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
|
||||
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
||||
delegate_to: localhost
|
||||
|
||||
@@ -53,3 +53,5 @@
|
||||
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||
connection_options:
|
||||
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
|
||||
register: result
|
||||
failed_when: not result.is_available
|
||||
|
||||
@@ -36,7 +36,8 @@
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret }}"
|
||||
host: "%"
|
||||
priv: "{{ account.username + '.*:USAGE' if db_name == '' else db_name + '.*:ALL' }}"
|
||||
priv: "{{ omit if db_name == '' else db_name + '.*:ALL' }}"
|
||||
append_privs: "{{ db_name != '' | bool }}"
|
||||
ignore_errors: true
|
||||
when: db_info is succeeded
|
||||
|
||||
|
||||
@@ -39,3 +39,5 @@
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
db: "{{ jms_asset.spec_info.db_name }}"
|
||||
register: result
|
||||
failed_when: not result.is_available
|
||||
@@ -35,12 +35,24 @@
|
||||
- user_info.failed
|
||||
- params.groups
|
||||
|
||||
- name: "Set {{ account.username }} sudo setting"
|
||||
ansible.builtin.lineinfile:
|
||||
dest: /etc/sudoers
|
||||
state: present
|
||||
regexp: "^{{ account.username }} ALL="
|
||||
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
|
||||
validate: visudo -cf %s
|
||||
when:
|
||||
- user_info.failed or params.modify_sudo
|
||||
- params.sudo
|
||||
|
||||
- name: "Change {{ account.username }} password"
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret | password_hash('des') }}"
|
||||
update_password: always
|
||||
ignore_errors: true
|
||||
register: change_secret_result
|
||||
when: account.secret_type == "password"
|
||||
|
||||
- name: remove jumpserver ssh key
|
||||
@@ -57,19 +69,9 @@
|
||||
user: "{{ account.username }}"
|
||||
key: "{{ account.secret }}"
|
||||
exclusive: "{{ ssh_params.exclusive }}"
|
||||
register: change_secret_result
|
||||
when: account.secret_type == "ssh_key"
|
||||
|
||||
- name: "Set {{ account.username }} sudo setting"
|
||||
ansible.builtin.lineinfile:
|
||||
dest: /etc/sudoers
|
||||
state: present
|
||||
regexp: "^{{ account.username }} ALL="
|
||||
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
|
||||
validate: visudo -cf %s
|
||||
when:
|
||||
- user_info.failed
|
||||
- params.sudo
|
||||
|
||||
- name: Refresh connection
|
||||
ansible.builtin.meta: reset_connection
|
||||
|
||||
@@ -85,7 +87,10 @@
|
||||
become_user: "{{ account.become.ansible_user | default('') }}"
|
||||
become_password: "{{ account.become.ansible_password | default('') }}"
|
||||
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
|
||||
when: account.secret_type == "password"
|
||||
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
||||
when:
|
||||
- account.secret_type == "password"
|
||||
- check_conn_after_change or change_secret_result.failed | default(false)
|
||||
delegate_to: localhost
|
||||
|
||||
- name: "Verify {{ account.username }} SSH KEY (paramiko)"
|
||||
@@ -95,5 +100,8 @@
|
||||
login_user: "{{ account.username }}"
|
||||
login_private_key_path: "{{ account.private_key_path }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
|
||||
when: account.secret_type == "ssh_key"
|
||||
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
||||
when:
|
||||
- account.secret_type == "ssh_key"
|
||||
- check_conn_after_change or change_secret_result.failed | default(false)
|
||||
delegate_to: localhost
|
||||
|
||||
@@ -5,6 +5,12 @@ type:
|
||||
- AIX
|
||||
method: change_secret
|
||||
params:
|
||||
- name: modify_sudo
|
||||
type: bool
|
||||
label: "{{ 'Modify sudo label' | trans }}"
|
||||
default: False
|
||||
help_text: "{{ 'Modify params sudo help text' | trans }}"
|
||||
|
||||
- name: sudo
|
||||
type: str
|
||||
label: 'Sudo'
|
||||
@@ -34,6 +40,11 @@ i18n:
|
||||
ja: 'Ansible user モジュールを使用してアカウントのパスワード変更 (DES)'
|
||||
en: 'Using Ansible module user to change account secret (DES)'
|
||||
|
||||
Modify params sudo help text:
|
||||
zh: '如果用户存在,可以修改sudo权限'
|
||||
ja: 'ユーザーが存在する場合、sudo権限を変更できます'
|
||||
en: 'If the user exists, sudo permissions can be modified'
|
||||
|
||||
Params sudo help text:
|
||||
zh: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig'
|
||||
ja: 'コンマで区切って複数のコマンドを入力してください。例: /bin/whoami,/sbin/ifconfig'
|
||||
@@ -49,6 +60,11 @@ i18n:
|
||||
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
|
||||
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
|
||||
|
||||
Modify sudo label:
|
||||
zh: '修改 sudo 权限'
|
||||
ja: 'sudo 権限を変更'
|
||||
en: 'Modify sudo'
|
||||
|
||||
Params home label:
|
||||
zh: '家目录'
|
||||
ja: 'ホームディレクトリ'
|
||||
|
||||
@@ -35,12 +35,24 @@
|
||||
- user_info.failed
|
||||
- params.groups
|
||||
|
||||
- name: "Set {{ account.username }} sudo setting"
|
||||
ansible.builtin.lineinfile:
|
||||
dest: /etc/sudoers
|
||||
state: present
|
||||
regexp: "^{{ account.username }} ALL="
|
||||
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
|
||||
validate: visudo -cf %s
|
||||
when:
|
||||
- user_info.failed or params.modify_sudo
|
||||
- params.sudo
|
||||
|
||||
- name: "Change {{ account.username }} password"
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret | password_hash('sha512') }}"
|
||||
update_password: always
|
||||
ignore_errors: true
|
||||
register: change_secret_result
|
||||
when: account.secret_type == "password"
|
||||
|
||||
- name: remove jumpserver ssh key
|
||||
@@ -57,19 +69,9 @@
|
||||
user: "{{ account.username }}"
|
||||
key: "{{ account.secret }}"
|
||||
exclusive: "{{ ssh_params.exclusive }}"
|
||||
register: change_secret_result
|
||||
when: account.secret_type == "ssh_key"
|
||||
|
||||
- name: "Set {{ account.username }} sudo setting"
|
||||
ansible.builtin.lineinfile:
|
||||
dest: /etc/sudoers
|
||||
state: present
|
||||
regexp: "^{{ account.username }} ALL="
|
||||
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
|
||||
validate: visudo -cf %s
|
||||
when:
|
||||
- user_info.failed
|
||||
- params.sudo
|
||||
|
||||
- name: Refresh connection
|
||||
ansible.builtin.meta: reset_connection
|
||||
|
||||
@@ -85,7 +87,10 @@
|
||||
become_user: "{{ account.become.ansible_user | default('') }}"
|
||||
become_password: "{{ account.become.ansible_password | default('') }}"
|
||||
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
|
||||
when: account.secret_type == "password"
|
||||
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
||||
when:
|
||||
- account.secret_type == "password"
|
||||
- check_conn_after_change or change_secret_result.failed | default(false)
|
||||
delegate_to: localhost
|
||||
|
||||
- name: "Verify {{ account.username }} SSH KEY (paramiko)"
|
||||
@@ -95,5 +100,8 @@
|
||||
login_user: "{{ account.username }}"
|
||||
login_private_key_path: "{{ account.private_key_path }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
|
||||
when: account.secret_type == "ssh_key"
|
||||
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
||||
when:
|
||||
- account.secret_type == "ssh_key"
|
||||
- check_conn_after_change or change_secret_result.failed | default(false)
|
||||
delegate_to: localhost
|
||||
|
||||
@@ -6,6 +6,12 @@ type:
|
||||
- linux
|
||||
method: change_secret
|
||||
params:
|
||||
- name: modify_sudo
|
||||
type: bool
|
||||
label: "{{ 'Modify sudo label' | trans }}"
|
||||
default: False
|
||||
help_text: "{{ 'Modify params sudo help text' | trans }}"
|
||||
|
||||
- name: sudo
|
||||
type: str
|
||||
label: 'Sudo'
|
||||
@@ -36,6 +42,11 @@ i18n:
|
||||
ja: 'Ansible user モジュールを使用して アカウントのパスワード変更 (SHA512)'
|
||||
en: 'Using Ansible module user to change account secret (SHA512)'
|
||||
|
||||
Modify params sudo help text:
|
||||
zh: '如果用户存在,可以修改sudo权限'
|
||||
ja: 'ユーザーが存在する場合、sudo権限を変更できます'
|
||||
en: 'If the user exists, sudo permissions can be modified'
|
||||
|
||||
Params sudo help text:
|
||||
zh: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig'
|
||||
ja: 'コンマで区切って複数のコマンドを入力してください。例: /bin/whoami,/sbin/ifconfig'
|
||||
@@ -51,6 +62,11 @@ i18n:
|
||||
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
|
||||
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
|
||||
|
||||
Modify sudo label:
|
||||
zh: '修改 sudo 权限'
|
||||
ja: 'sudo 権限を変更'
|
||||
en: 'Modify sudo'
|
||||
|
||||
Params home label:
|
||||
zh: '家目录'
|
||||
ja: 'ホームディレクトリ'
|
||||
|
||||
@@ -28,4 +28,6 @@
|
||||
vars:
|
||||
ansible_user: "{{ account.username }}"
|
||||
ansible_password: "{{ account.secret }}"
|
||||
when: account.secret_type == "password"
|
||||
when:
|
||||
- account.secret_type == "password"
|
||||
- check_conn_after_change
|
||||
|
||||
@@ -31,5 +31,7 @@
|
||||
login_password: "{{ account.secret }}"
|
||||
login_secret_type: "{{ account.secret_type }}"
|
||||
login_private_key_path: "{{ account.private_key_path }}"
|
||||
when: account.secret_type == "password"
|
||||
when:
|
||||
- account.secret_type == "password"
|
||||
- check_conn_after_change
|
||||
delegate_to: localhost
|
||||
|
||||
@@ -7,9 +7,9 @@ from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from xlsxwriter import Workbook
|
||||
|
||||
from accounts.const import AutomationTypes, SecretType, SSHKeyStrategy, SecretStrategy
|
||||
from accounts.const import AutomationTypes, SecretType, SSHKeyStrategy, SecretStrategy, ChangeSecretRecordStatusChoice
|
||||
from accounts.models import ChangeSecretRecord
|
||||
from accounts.notifications import ChangeSecretExecutionTaskMsg
|
||||
from accounts.notifications import ChangeSecretExecutionTaskMsg, ChangeSecretFailedMsg
|
||||
from accounts.serializers import ChangeSecretRecordBackUpSerializer
|
||||
from assets.const import HostTypes
|
||||
from common.utils import get_logger
|
||||
@@ -27,7 +27,7 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.record_id = self.execution.snapshot.get('record_id')
|
||||
self.record_map = self.execution.snapshot.get('record_map', {})
|
||||
self.secret_type = self.execution.snapshot.get('secret_type')
|
||||
self.secret_strategy = self.execution.snapshot.get(
|
||||
'secret_strategy', SecretStrategy.custom
|
||||
@@ -123,14 +123,21 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
||||
print(f'new_secret is None, account: {account}')
|
||||
continue
|
||||
|
||||
if self.record_id is None:
|
||||
asset_account_id = f'{asset.id}-{account.id}'
|
||||
if asset_account_id not in self.record_map:
|
||||
recorder = ChangeSecretRecord(
|
||||
asset=asset, account=account, execution=self.execution,
|
||||
old_secret=account.secret, new_secret=new_secret,
|
||||
)
|
||||
records.append(recorder)
|
||||
else:
|
||||
recorder = ChangeSecretRecord.objects.get(id=self.record_id)
|
||||
record_id = self.record_map[asset_account_id]
|
||||
try:
|
||||
recorder = ChangeSecretRecord.objects.get(id=record_id)
|
||||
new_secret = recorder.new_secret
|
||||
except ChangeSecretRecord.DoesNotExist:
|
||||
print(f"Record {record_id} not found")
|
||||
continue
|
||||
|
||||
self.name_recorder_mapper[h['name']] = recorder
|
||||
|
||||
@@ -158,25 +165,43 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
||||
recorder = self.name_recorder_mapper.get(host)
|
||||
if not recorder:
|
||||
return
|
||||
recorder.status = 'success'
|
||||
recorder.status = ChangeSecretRecordStatusChoice.success.value
|
||||
recorder.date_finished = timezone.now()
|
||||
recorder.save()
|
||||
|
||||
account = recorder.account
|
||||
if not account:
|
||||
print("Account not found, deleted ?")
|
||||
return
|
||||
account.secret = recorder.new_secret
|
||||
account.date_updated = timezone.now()
|
||||
account.save(update_fields=['secret', 'date_updated'])
|
||||
|
||||
max_retries = 3
|
||||
retry_count = 0
|
||||
|
||||
while retry_count < max_retries:
|
||||
try:
|
||||
recorder.save()
|
||||
account.save(update_fields=['secret', 'version', 'date_updated'])
|
||||
break
|
||||
except Exception as e:
|
||||
retry_count += 1
|
||||
if retry_count == max_retries:
|
||||
self.on_host_error(host, str(e), result)
|
||||
else:
|
||||
print(f'retry {retry_count} times for {host} recorder save error: {e}')
|
||||
time.sleep(1)
|
||||
|
||||
def on_host_error(self, host, error, result):
|
||||
recorder = self.name_recorder_mapper.get(host)
|
||||
if not recorder:
|
||||
return
|
||||
recorder.status = 'failed'
|
||||
recorder.status = ChangeSecretRecordStatusChoice.failed.value
|
||||
recorder.date_finished = timezone.now()
|
||||
recorder.error = error
|
||||
try:
|
||||
recorder.save()
|
||||
except Exception as e:
|
||||
print(f"\033[31m Save {host} recorder error: {e} \033[0m\n")
|
||||
|
||||
def on_runner_failed(self, runner, e):
|
||||
logger.error("Account error: ", e)
|
||||
@@ -192,7 +217,7 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
||||
def get_summary(recorders):
|
||||
total, succeed, failed = 0, 0, 0
|
||||
for recorder in recorders:
|
||||
if recorder.status == 'success':
|
||||
if recorder.status == ChangeSecretRecordStatusChoice.success.value:
|
||||
succeed += 1
|
||||
else:
|
||||
failed += 1
|
||||
@@ -209,18 +234,35 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
||||
summary = self.get_summary(recorders)
|
||||
print(summary, end='')
|
||||
|
||||
if self.record_id:
|
||||
if self.record_map:
|
||||
return
|
||||
|
||||
self.send_recorder_mail(recorders, summary)
|
||||
failed_recorders = [
|
||||
r for r in recorders
|
||||
if r.status == ChangeSecretRecordStatusChoice.failed.value
|
||||
]
|
||||
|
||||
def send_recorder_mail(self, recorders, summary):
|
||||
recipients = self.execution.recipients
|
||||
if not recorders or not recipients:
|
||||
recipients = User.objects.filter(id__in=list(recipients.keys()))
|
||||
if not recipients:
|
||||
return
|
||||
|
||||
recipients = User.objects.filter(id__in=list(recipients.keys()))
|
||||
if failed_recorders:
|
||||
name = self.execution.snapshot.get('name')
|
||||
execution_id = str(self.execution.id)
|
||||
_ids = [r.id for r in failed_recorders]
|
||||
asset_account_errors = ChangeSecretRecord.objects.filter(
|
||||
id__in=_ids).values_list('asset__name', 'account__username', 'error')
|
||||
|
||||
for user in recipients:
|
||||
ChangeSecretFailedMsg(name, execution_id, user, asset_account_errors).publish()
|
||||
|
||||
if not recorders:
|
||||
return
|
||||
|
||||
self.send_recorder_mail(recipients, recorders, summary)
|
||||
|
||||
def send_recorder_mail(self, recipients, recorders, summary):
|
||||
name = self.execution.snapshot['name']
|
||||
path = os.path.join(os.path.dirname(settings.BASE_DIR), 'tmp')
|
||||
filename = os.path.join(path, f'{name}-{local_now_filename()}-{time.time()}.xlsx')
|
||||
|
||||
@@ -51,14 +51,22 @@ class GatherAccountsManager(AccountBasePlaybookManager):
|
||||
data = self.generate_data(asset, result)
|
||||
self.asset_account_info[asset] = data
|
||||
|
||||
@staticmethod
|
||||
def get_nested_info(data, *keys):
|
||||
for key in keys:
|
||||
data = data.get(key, {})
|
||||
if not data:
|
||||
break
|
||||
return data
|
||||
|
||||
def on_host_success(self, host, result):
|
||||
info = result.get('debug', {}).get('res', {}).get('info', {})
|
||||
info = self.get_nested_info(result, 'debug', 'res', 'info')
|
||||
asset = self.host_asset_mapper.get(host)
|
||||
if asset and info:
|
||||
result = self.filter_success_result(asset.type, info)
|
||||
self.collect_asset_account_info(asset, result)
|
||||
else:
|
||||
logger.error(f'Not found {host} info')
|
||||
print(f'\033[31m Not found {host} info \033[0m\n')
|
||||
|
||||
def update_or_create_accounts(self):
|
||||
for asset, data in self.asset_account_info.items():
|
||||
|
||||
@@ -53,3 +53,5 @@
|
||||
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||
connection_options:
|
||||
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
|
||||
register: result
|
||||
failed_when: not result.is_available
|
||||
|
||||
@@ -36,7 +36,8 @@
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret }}"
|
||||
host: "%"
|
||||
priv: "{{ account.username + '.*:USAGE' if db_name == '' else db_name + '.*:ALL' }}"
|
||||
priv: "{{ omit if db_name == '' else db_name + '.*:ALL' }}"
|
||||
append_privs: "{{ db_name != '' | bool }}"
|
||||
ignore_errors: true
|
||||
when: db_info is succeeded
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
role_attr_flags: LOGIN
|
||||
ignore_errors: true
|
||||
when: result is succeeded
|
||||
register: change_info
|
||||
|
||||
- name: Verify password
|
||||
community.postgresql.postgresql_ping:
|
||||
@@ -40,8 +39,5 @@
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
db: "{{ jms_asset.spec_info.db_name }}"
|
||||
when:
|
||||
- result is succeeded
|
||||
- change_info is succeeded
|
||||
register: result
|
||||
failed_when: not result.is_available
|
||||
|
||||
@@ -35,12 +35,24 @@
|
||||
- user_info.failed
|
||||
- params.groups
|
||||
|
||||
- name: "Set {{ account.username }} sudo setting"
|
||||
ansible.builtin.lineinfile:
|
||||
dest: /etc/sudoers
|
||||
state: present
|
||||
regexp: "^{{ account.username }} ALL="
|
||||
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
|
||||
validate: visudo -cf %s
|
||||
when:
|
||||
- user_info.failed or params.modify_sudo
|
||||
- params.sudo
|
||||
|
||||
- name: "Change {{ account.username }} password"
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret | password_hash('des') }}"
|
||||
update_password: always
|
||||
ignore_errors: true
|
||||
register: change_secret_result
|
||||
when: account.secret_type == "password"
|
||||
|
||||
- name: remove jumpserver ssh key
|
||||
@@ -57,19 +69,9 @@
|
||||
user: "{{ account.username }}"
|
||||
key: "{{ account.secret }}"
|
||||
exclusive: "{{ ssh_params.exclusive }}"
|
||||
register: change_secret_result
|
||||
when: account.secret_type == "ssh_key"
|
||||
|
||||
- name: "Set {{ account.username }} sudo setting"
|
||||
ansible.builtin.lineinfile:
|
||||
dest: /etc/sudoers
|
||||
state: present
|
||||
regexp: "^{{ account.username }} ALL="
|
||||
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
|
||||
validate: visudo -cf %s
|
||||
when:
|
||||
- user_info.failed
|
||||
- params.sudo
|
||||
|
||||
- name: Refresh connection
|
||||
ansible.builtin.meta: reset_connection
|
||||
|
||||
@@ -85,7 +87,10 @@
|
||||
become_user: "{{ account.become.ansible_user | default('') }}"
|
||||
become_password: "{{ account.become.ansible_password | default('') }}"
|
||||
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
|
||||
when: account.secret_type == "password"
|
||||
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
||||
when:
|
||||
- account.secret_type == "password"
|
||||
- check_conn_after_change or change_secret_result.failed | default(false)
|
||||
delegate_to: localhost
|
||||
|
||||
- name: "Verify {{ account.username }} SSH KEY (paramiko)"
|
||||
@@ -95,6 +100,9 @@
|
||||
login_user: "{{ account.username }}"
|
||||
login_private_key_path: "{{ account.private_key_path }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
|
||||
when: account.secret_type == "ssh_key"
|
||||
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
||||
when:
|
||||
- account.secret_type == "ssh_key"
|
||||
- check_conn_after_change or change_secret_result.failed | default(false)
|
||||
delegate_to: localhost
|
||||
|
||||
|
||||
@@ -5,6 +5,12 @@ type:
|
||||
- AIX
|
||||
method: push_account
|
||||
params:
|
||||
- name: modify_sudo
|
||||
type: bool
|
||||
label: "{{ 'Modify sudo label' | trans }}"
|
||||
default: False
|
||||
help_text: "{{ 'Modify params sudo help text' | trans }}"
|
||||
|
||||
- name: sudo
|
||||
type: str
|
||||
label: 'Sudo'
|
||||
@@ -34,6 +40,11 @@ i18n:
|
||||
ja: 'Ansible user モジュールを使用して Aix アカウントをプッシュする (DES)'
|
||||
en: 'Using Ansible module user to push account (DES)'
|
||||
|
||||
Modify params sudo help text:
|
||||
zh: '如果用户存在,可以修改sudo权限'
|
||||
ja: 'ユーザーが存在する場合、sudo権限を変更できます'
|
||||
en: 'If the user exists, sudo permissions can be modified'
|
||||
|
||||
Params sudo help text:
|
||||
zh: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig'
|
||||
ja: 'コンマで区切って複数のコマンドを入力してください。例: /bin/whoami,/sbin/ifconfig'
|
||||
@@ -49,6 +60,11 @@ i18n:
|
||||
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
|
||||
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
|
||||
|
||||
Modify sudo label:
|
||||
zh: '修改 sudo 权限'
|
||||
ja: 'sudo 権限を変更'
|
||||
en: 'Modify sudo'
|
||||
|
||||
Params home label:
|
||||
zh: '家目录'
|
||||
ja: 'ホームディレクトリ'
|
||||
|
||||
@@ -35,12 +35,24 @@
|
||||
- user_info.failed
|
||||
- params.groups
|
||||
|
||||
- name: "Set {{ account.username }} sudo setting"
|
||||
ansible.builtin.lineinfile:
|
||||
dest: /etc/sudoers
|
||||
state: present
|
||||
regexp: "^{{ account.username }} ALL="
|
||||
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
|
||||
validate: visudo -cf %s
|
||||
when:
|
||||
- user_info.failed or params.modify_sudo
|
||||
- params.sudo
|
||||
|
||||
- name: "Change {{ account.username }} password"
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret | password_hash('sha512') }}"
|
||||
update_password: always
|
||||
ignore_errors: true
|
||||
register: change_secret_result
|
||||
when: account.secret_type == "password"
|
||||
|
||||
- name: remove jumpserver ssh key
|
||||
@@ -57,19 +69,9 @@
|
||||
user: "{{ account.username }}"
|
||||
key: "{{ account.secret }}"
|
||||
exclusive: "{{ ssh_params.exclusive }}"
|
||||
register: change_secret_result
|
||||
when: account.secret_type == "ssh_key"
|
||||
|
||||
- name: "Set {{ account.username }} sudo setting"
|
||||
ansible.builtin.lineinfile:
|
||||
dest: /etc/sudoers
|
||||
state: present
|
||||
regexp: "^{{ account.username }} ALL="
|
||||
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
|
||||
validate: visudo -cf %s
|
||||
when:
|
||||
- user_info.failed
|
||||
- params.sudo
|
||||
|
||||
- name: Refresh connection
|
||||
ansible.builtin.meta: reset_connection
|
||||
|
||||
@@ -85,7 +87,10 @@
|
||||
become_user: "{{ account.become.ansible_user | default('') }}"
|
||||
become_password: "{{ account.become.ansible_password | default('') }}"
|
||||
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
|
||||
when: account.secret_type == "password"
|
||||
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
||||
when:
|
||||
- account.secret_type == "password"
|
||||
- check_conn_after_change or change_secret_result.failed | default(false)
|
||||
delegate_to: localhost
|
||||
|
||||
- name: "Verify {{ account.username }} SSH KEY (paramiko)"
|
||||
@@ -95,6 +100,9 @@
|
||||
login_user: "{{ account.username }}"
|
||||
login_private_key_path: "{{ account.private_key_path }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
|
||||
when: account.secret_type == "ssh_key"
|
||||
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
||||
when:
|
||||
- account.secret_type == "ssh_key"
|
||||
- check_conn_after_change or change_secret_result.failed | default(false)
|
||||
delegate_to: localhost
|
||||
|
||||
|
||||
@@ -6,6 +6,12 @@ type:
|
||||
- linux
|
||||
method: push_account
|
||||
params:
|
||||
- name: modify_sudo
|
||||
type: bool
|
||||
label: "{{ 'Modify sudo label' | trans }}"
|
||||
default: False
|
||||
help_text: "{{ 'Modify params sudo help text' | trans }}"
|
||||
|
||||
- name: sudo
|
||||
type: str
|
||||
label: 'Sudo'
|
||||
@@ -36,6 +42,11 @@ i18n:
|
||||
ja: 'Ansible user モジュールを使用してアカウントをプッシュする (sha512)'
|
||||
en: 'Using Ansible module user to push account (sha512)'
|
||||
|
||||
Modify params sudo help text:
|
||||
zh: '如果用户存在,可以修改sudo权限'
|
||||
ja: 'ユーザーが存在する場合、sudo権限を変更できます'
|
||||
en: 'If the user exists, sudo permissions can be modified'
|
||||
|
||||
Params sudo help text:
|
||||
zh: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig'
|
||||
ja: 'コンマで区切って複数のコマンドを入力してください。例: /bin/whoami,/sbin/ifconfig'
|
||||
@@ -51,6 +62,11 @@ i18n:
|
||||
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
|
||||
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
|
||||
|
||||
Modify sudo label:
|
||||
zh: '修改 sudo 权限'
|
||||
ja: 'sudo 権限を変更'
|
||||
en: 'Modify sudo'
|
||||
|
||||
Params home label:
|
||||
zh: '家目录'
|
||||
ja: 'ホームディレクトリ'
|
||||
|
||||
@@ -28,4 +28,6 @@
|
||||
vars:
|
||||
ansible_user: "{{ account.username }}"
|
||||
ansible_password: "{{ account.secret }}"
|
||||
when: account.secret_type == "password"
|
||||
when:
|
||||
- account.secret_type == "password"
|
||||
- check_conn_after_change
|
||||
|
||||
@@ -31,5 +31,7 @@
|
||||
login_password: "{{ account.secret }}"
|
||||
login_secret_type: "{{ account.secret_type }}"
|
||||
login_private_key_path: "{{ account.private_key_path }}"
|
||||
when: account.secret_type == "password"
|
||||
when:
|
||||
- account.secret_type == "password"
|
||||
- check_conn_after_change
|
||||
delegate_to: localhost
|
||||
|
||||
@@ -12,11 +12,13 @@
|
||||
path: "{{ user_home_dir.stdout }}"
|
||||
register: home_dir
|
||||
when: user_home_dir.stdout != ""
|
||||
ignore_errors: yes
|
||||
|
||||
- name: "Rename user home directory if it exists"
|
||||
ansible.builtin.command:
|
||||
cmd: "mv {{ user_home_dir.stdout }} {{ user_home_dir.stdout }}.bak"
|
||||
when: home_dir.stat | default(false) and user_home_dir.stdout != ""
|
||||
ignore_errors: yes
|
||||
|
||||
- name: "Remove account"
|
||||
ansible.builtin.user:
|
||||
|
||||
@@ -5,5 +5,3 @@
|
||||
ansible.windows.win_user:
|
||||
name: "{{ account.username }}"
|
||||
state: absent
|
||||
purge: yes
|
||||
force: yes
|
||||
@@ -60,8 +60,11 @@ class RemoveAccountManager(AccountBasePlaybookManager):
|
||||
if not tuple_asset_gather_account:
|
||||
return
|
||||
asset, gather_account = tuple_asset_gather_account
|
||||
try:
|
||||
Account.objects.filter(
|
||||
asset_id=asset.id,
|
||||
username=gather_account.username
|
||||
).delete()
|
||||
gather_account.delete()
|
||||
except Exception as e:
|
||||
print(f'\033[31m Delete account {gather_account.username} failed: {e} \033[0m\n')
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
vars:
|
||||
ansible_shell_type: sh
|
||||
ansible_connection: local
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
|
||||
tasks:
|
||||
- name: Verify account (pyfreerdp)
|
||||
|
||||
@@ -19,3 +19,5 @@
|
||||
become_user: "{{ account.become.ansible_user | default('') }}"
|
||||
become_password: "{{ account.become.ansible_password | default('') }}"
|
||||
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
|
||||
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
||||
|
||||
@@ -16,3 +16,5 @@
|
||||
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||
connection_options:
|
||||
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert }}"
|
||||
register: result
|
||||
failed_when: not result.is_available
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
ansible_user: "{{ account.username }}"
|
||||
ansible_password: "{{ account.secret }}"
|
||||
ansible_ssh_private_key_file: "{{ account.private_key_path }}"
|
||||
ansible_timeout: 30
|
||||
when: not account.become.ansible_become
|
||||
|
||||
- name: Verify account connectivity(Switch)
|
||||
@@ -20,4 +21,5 @@
|
||||
ansible_become_method: "{{ account.become.ansible_become_method }}"
|
||||
ansible_become_user: "{{ account.become.ansible_become_user }}"
|
||||
ansible_become_password: "{{ account.become.ansible_become_password }}"
|
||||
ansible_timeout: 30
|
||||
when: account.become.ansible_become
|
||||
|
||||
@@ -9,3 +9,4 @@
|
||||
vars:
|
||||
ansible_user: "{{ account.username }}"
|
||||
ansible_password: "{{ account.secret }}"
|
||||
ansible_timeout: 30
|
||||
|
||||
@@ -76,8 +76,14 @@ class VerifyAccountManager(AccountBasePlaybookManager):
|
||||
|
||||
def on_host_success(self, host, result):
|
||||
account = self.host_account_mapper.get(host)
|
||||
try:
|
||||
account.set_connectivity(Connectivity.OK)
|
||||
except Exception as e:
|
||||
print(f'\033[31m Update account {account.name} connectivity failed: {e} \033[0m\n')
|
||||
|
||||
def on_host_error(self, host, error, result):
|
||||
account = self.host_account_mapper.get(host)
|
||||
try:
|
||||
account.set_connectivity(Connectivity.ERR)
|
||||
except Exception as e:
|
||||
print(f'\033[31m Update account {account.name} connectivity failed: {e} \033[0m\n')
|
||||
|
||||
@@ -15,6 +15,7 @@ class AliasAccount(TextChoices):
|
||||
INPUT = '@INPUT', _('Manual input')
|
||||
USER = '@USER', _('Dynamic user')
|
||||
ANON = '@ANON', _('Anonymous account')
|
||||
SPEC = '@SPEC', _('Specified account')
|
||||
|
||||
@classmethod
|
||||
def virtual_choices(cls):
|
||||
|
||||
@@ -16,7 +16,7 @@ DEFAULT_PASSWORD_RULES = {
|
||||
__all__ = [
|
||||
'AutomationTypes', 'SecretStrategy', 'SSHKeyStrategy', 'Connectivity',
|
||||
'DEFAULT_PASSWORD_LENGTH', 'DEFAULT_PASSWORD_RULES', 'TriggerChoice',
|
||||
'PushAccountActionChoice', 'AccountBackupType'
|
||||
'PushAccountActionChoice', 'AccountBackupType', 'ChangeSecretRecordStatusChoice',
|
||||
]
|
||||
|
||||
|
||||
@@ -103,3 +103,9 @@ class AccountBackupType(models.TextChoices):
|
||||
email = 'email', _('Email')
|
||||
# 目前只支持sftp方式
|
||||
object_storage = 'object_storage', _('SFTP')
|
||||
|
||||
|
||||
class ChangeSecretRecordStatusChoice(models.TextChoices):
|
||||
failed = 'failed', _('Failed')
|
||||
success = 'success', _('Success')
|
||||
pending = 'pending', _('Pending')
|
||||
|
||||
@@ -5,7 +5,7 @@ from django_filters import rest_framework as drf_filters
|
||||
|
||||
from assets.models import Node
|
||||
from common.drf.filters import BaseFilterSet
|
||||
from .models import Account, GatheredAccount
|
||||
from .models import Account, GatheredAccount, ChangeSecretRecord
|
||||
|
||||
|
||||
class AccountFilterSet(BaseFilterSet):
|
||||
@@ -61,3 +61,13 @@ class GatheredAccountFilterSet(BaseFilterSet):
|
||||
class Meta:
|
||||
model = GatheredAccount
|
||||
fields = ['id', 'username']
|
||||
|
||||
|
||||
class ChangeSecretRecordFilterSet(BaseFilterSet):
|
||||
asset_name = drf_filters.CharFilter(field_name='asset__name', lookup_expr='icontains')
|
||||
account_username = drf_filters.CharFilter(field_name='account__username', lookup_expr='icontains')
|
||||
execution_id = drf_filters.CharFilter(field_name='execution_id', lookup_expr='exact')
|
||||
|
||||
class Meta:
|
||||
model = ChangeSecretRecord
|
||||
fields = ['id', 'status', 'asset_id', 'execution']
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
# Generated by Django 4.1.10 on 2023-08-01 09:12
|
||||
|
||||
from django.db import migrations, models
|
||||
import uuid
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
@@ -20,7 +21,7 @@ class Migration(migrations.Migration):
|
||||
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
|
||||
('alias', models.CharField(choices=[('@INPUT', 'Manual input'), ('@USER', 'Dynamic user'), ('@ANON', 'Anonymous account')], max_length=128, verbose_name='Alias')),
|
||||
('alias', models.CharField(choices=[('@INPUT', 'Manual input'), ('@USER', 'Dynamic user'), ('@ANON', 'Anonymous account'), ('@SPEC', 'Specified account')], max_length=128, verbose_name='Alias')),
|
||||
('secret_from_login', models.BooleanField(default=None, null=True, verbose_name='Secret from login')),
|
||||
],
|
||||
options={
|
||||
|
||||
@@ -53,7 +53,8 @@ class Account(AbsConnectivity, LabeledMixin, BaseAccount):
|
||||
on_delete=models.SET_NULL, verbose_name=_("Su from")
|
||||
)
|
||||
version = models.IntegerField(default=0, verbose_name=_('Version'))
|
||||
history = AccountHistoricalRecords(included_fields=['id', '_secret', 'secret_type', 'version'])
|
||||
history = AccountHistoricalRecords(included_fields=['id', '_secret', 'secret_type', 'version'],
|
||||
verbose_name=_("historical Account"))
|
||||
source = models.CharField(max_length=30, default=Source.LOCAL, verbose_name=_('Source'))
|
||||
source_id = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Source ID'))
|
||||
|
||||
@@ -119,7 +120,8 @@ class Account(AbsConnectivity, LabeledMixin, BaseAccount):
|
||||
return auth
|
||||
|
||||
auth.update(self.make_account_ansible_vars(su_from))
|
||||
become_method = platform.su_method if platform.su_method else 'sudo'
|
||||
|
||||
become_method = platform.ansible_become_method
|
||||
password = su_from.secret if become_method == 'sudo' else self.secret
|
||||
auth['ansible_become'] = True
|
||||
auth['ansible_become_method'] = become_method
|
||||
|
||||
@@ -8,7 +8,7 @@ from django.db import models
|
||||
from django.db.models import F
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from accounts.const.automation import AccountBackupType
|
||||
from accounts.const import AccountBackupType
|
||||
from common.const.choices import Trigger
|
||||
from common.db import fields
|
||||
from common.db.encoder import ModelJSONFieldEncoder
|
||||
|
||||
@@ -2,7 +2,7 @@ from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from accounts.const import (
|
||||
AutomationTypes
|
||||
AutomationTypes, ChangeSecretRecordStatusChoice
|
||||
)
|
||||
from common.db import fields
|
||||
from common.db.models import JMSBaseModel
|
||||
@@ -40,7 +40,10 @@ class ChangeSecretRecord(JMSBaseModel):
|
||||
new_secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('New secret'))
|
||||
date_started = models.DateTimeField(blank=True, null=True, verbose_name=_('Date started'))
|
||||
date_finished = models.DateTimeField(blank=True, null=True, verbose_name=_('Date finished'))
|
||||
status = models.CharField(max_length=16, default='pending', verbose_name=_('Status'))
|
||||
status = models.CharField(
|
||||
max_length=16, verbose_name=_('Status'),
|
||||
default=ChangeSecretRecordStatusChoice.pending.value
|
||||
)
|
||||
error = models.TextField(blank=True, null=True, verbose_name=_('Error'))
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -137,16 +137,13 @@ class BaseAccount(VaultModelMixin, JMSOrgBaseModel):
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def private_key_path(self):
|
||||
def get_private_key_path(self, path):
|
||||
if self.secret_type != SecretType.SSH_KEY \
|
||||
or not self.secret \
|
||||
or not self.private_key:
|
||||
return None
|
||||
project_dir = settings.PROJECT_DIR
|
||||
tmp_dir = os.path.join(project_dir, 'tmp')
|
||||
key_name = '.' + md5(self.private_key.encode('utf-8')).hexdigest()
|
||||
key_path = os.path.join(tmp_dir, key_name)
|
||||
key_path = os.path.join(path, key_name)
|
||||
if not os.path.exists(key_path):
|
||||
# https://github.com/ansible/ansible-runner/issues/544
|
||||
# ssh requires OpenSSH format keys to have a full ending newline.
|
||||
@@ -158,6 +155,12 @@ class BaseAccount(VaultModelMixin, JMSOrgBaseModel):
|
||||
os.chmod(key_path, 0o400)
|
||||
return key_path
|
||||
|
||||
@property
|
||||
def private_key_path(self):
|
||||
project_dir = settings.PROJECT_DIR
|
||||
tmp_dir = os.path.join(project_dir, 'tmp')
|
||||
return self.get_private_key_path(tmp_dir)
|
||||
|
||||
def get_private_key(self):
|
||||
if not self.private_key:
|
||||
return None
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from accounts.models import ChangeSecretRecord
|
||||
from common.tasks import send_mail_attachment_async, upload_backup_to_obj_storage
|
||||
from notifications.notifications import UserMessage
|
||||
from terminal.models.component.storage import ReplayStorage
|
||||
@@ -98,3 +99,35 @@ class GatherAccountChangeMsg(UserMessage):
|
||||
def gen_test_msg(cls):
|
||||
user = User.objects.first()
|
||||
return cls(user, {})
|
||||
|
||||
|
||||
class ChangeSecretFailedMsg(UserMessage):
|
||||
subject = _('Change secret or push account failed information')
|
||||
|
||||
def __init__(self, name, execution_id, user, asset_account_errors: list):
|
||||
self.name = name
|
||||
self.execution_id = execution_id
|
||||
self.asset_account_errors = asset_account_errors
|
||||
super().__init__(user)
|
||||
|
||||
def get_html_msg(self) -> dict:
|
||||
context = {
|
||||
'name': self.name,
|
||||
'recipient': self.user,
|
||||
'execution_id': self.execution_id,
|
||||
'asset_account_errors': self.asset_account_errors
|
||||
}
|
||||
message = render_to_string('accounts/change_secret_failed_info.html', context)
|
||||
|
||||
return {
|
||||
'subject': str(self.subject),
|
||||
'message': message
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def gen_test_msg(cls):
|
||||
name = 'test'
|
||||
user = User.objects.first()
|
||||
record = ChangeSecretRecord.objects.first()
|
||||
execution_id = str(record.execution_id)
|
||||
return cls(name, execution_id, user, [])
|
||||
|
||||
@@ -79,18 +79,28 @@ class AccountCreateUpdateSerializerMixin(serializers.Serializer):
|
||||
|
||||
@staticmethod
|
||||
def get_template_attr_for_account(template):
|
||||
# Set initial data from template
|
||||
field_names = [
|
||||
'name', 'username', 'secret',
|
||||
'secret_type', 'privileged', 'is_active'
|
||||
'name', 'username',
|
||||
'secret_type', 'secret',
|
||||
'privileged', 'is_active'
|
||||
]
|
||||
|
||||
field_map = {
|
||||
'push_params': 'params',
|
||||
'auto_push': 'push_now'
|
||||
}
|
||||
|
||||
field_names.extend(field_map.keys())
|
||||
|
||||
attrs = {}
|
||||
for name in field_names:
|
||||
value = getattr(template, name, None)
|
||||
if value is None:
|
||||
continue
|
||||
attrs[name] = value
|
||||
|
||||
attr_name = field_map.get(name, name)
|
||||
attrs[attr_name] = value
|
||||
|
||||
attrs['secret'] = template.get_secret()
|
||||
return attrs
|
||||
|
||||
@@ -173,6 +183,7 @@ class AccountCreateUpdateSerializerMixin(serializers.Serializer):
|
||||
params = validated_data.pop('params', None)
|
||||
self.clean_auth_fields(validated_data)
|
||||
instance, stat = self.do_create(validated_data)
|
||||
if instance.source == Source.LOCAL:
|
||||
self.push_account_if_need(instance, push_now, params, stat)
|
||||
return instance
|
||||
|
||||
@@ -275,8 +286,8 @@ class AssetAccountBulkSerializer(
|
||||
fields = [
|
||||
'name', 'username', 'secret', 'secret_type', 'passphrase',
|
||||
'privileged', 'is_active', 'comment', 'template',
|
||||
'on_invalid', 'push_now', 'assets', 'su_from_username',
|
||||
'source', 'source_id',
|
||||
'on_invalid', 'push_now', 'params', 'assets',
|
||||
'su_from_username', 'source', 'source_id',
|
||||
]
|
||||
extra_kwargs = {
|
||||
'name': {'required': False},
|
||||
@@ -414,16 +425,23 @@ class AssetAccountBulkSerializer(
|
||||
return results
|
||||
|
||||
@staticmethod
|
||||
def push_accounts_if_need(results, push_now):
|
||||
def push_accounts_if_need(results, push_now, params):
|
||||
if not push_now:
|
||||
return
|
||||
accounts = [str(v['instance']) for v in results if v.get('instance')]
|
||||
push_accounts_to_assets_task.delay(accounts)
|
||||
|
||||
account_ids = [v['instance'] for v in results if v.get('instance')]
|
||||
accounts = Account.objects.filter(id__in=account_ids, source=Source.LOCAL)
|
||||
if not accounts.exists():
|
||||
return
|
||||
|
||||
account_ids = [str(_id) for _id in accounts.values_list('id', flat=True)]
|
||||
push_accounts_to_assets_task.delay(account_ids, params)
|
||||
|
||||
def create(self, validated_data):
|
||||
params = validated_data.pop('params', None)
|
||||
push_now = validated_data.pop('push_now', False)
|
||||
results = self.perform_bulk_create(validated_data)
|
||||
self.push_accounts_if_need(results, push_now)
|
||||
self.push_accounts_if_need(results, push_now, params)
|
||||
for res in results:
|
||||
res['asset'] = str(res['asset'])
|
||||
return results
|
||||
@@ -431,8 +449,11 @@ class AssetAccountBulkSerializer(
|
||||
|
||||
class AccountSecretSerializer(SecretReadableMixin, AccountSerializer):
|
||||
class Meta(AccountSerializer.Meta):
|
||||
fields = AccountSerializer.Meta.fields + ['spec_info']
|
||||
extra_kwargs = {
|
||||
**AccountSerializer.Meta.extra_kwargs,
|
||||
'secret': {'write_only': False},
|
||||
'spec_info': {'label': _('Spec info')},
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -67,15 +67,14 @@ class BaseAccountSerializer(AuthValidateMixin, ResourceLabelsMixin, BulkOrgResou
|
||||
fields_mini = ['id', 'name', 'username']
|
||||
fields_small = fields_mini + [
|
||||
'secret_type', 'secret', 'passphrase',
|
||||
'privileged', 'is_active', 'spec_info',
|
||||
'privileged', 'is_active',
|
||||
]
|
||||
fields_other = ['created_by', 'date_created', 'date_updated', 'comment']
|
||||
fields = fields_small + fields_other + ['labels']
|
||||
read_only_fields = [
|
||||
'spec_info', 'date_verified', 'created_by', 'date_created',
|
||||
'date_verified', 'created_by', 'date_created',
|
||||
]
|
||||
extra_kwargs = {
|
||||
'spec_info': {'label': _('Spec info')},
|
||||
'username': {'help_text': _(
|
||||
"Tip: If no username is required for authentication, fill in `null`, "
|
||||
"If AD account, like `username@domain`"
|
||||
|
||||
@@ -35,6 +35,7 @@ class AccountTemplateSerializer(BaseAccountSerializer):
|
||||
'su_from'
|
||||
]
|
||||
extra_kwargs = {
|
||||
**BaseAccountSerializer.Meta.extra_kwargs,
|
||||
'secret_strategy': {'help_text': _('Secret generation strategy for account creation')},
|
||||
'auto_push': {'help_text': _('Whether to automatically push the account to the asset')},
|
||||
'platforms': {
|
||||
@@ -64,6 +65,9 @@ class AccountTemplateSerializer(BaseAccountSerializer):
|
||||
|
||||
class AccountTemplateSecretSerializer(SecretReadableMixin, AccountTemplateSerializer):
|
||||
class Meta(AccountTemplateSerializer.Meta):
|
||||
fields = AccountTemplateSerializer.Meta.fields + ['spec_info']
|
||||
extra_kwargs = {
|
||||
**AccountTemplateSerializer.Meta.extra_kwargs,
|
||||
'secret': {'write_only': False},
|
||||
'spec_info': {'label': _('Spec info')},
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ __all__ = [
|
||||
class BaseAutomationSerializer(PeriodTaskSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
assets = ObjectRelatedField(many=True, required=False, queryset=Asset.objects, label=_('Assets'))
|
||||
nodes = ObjectRelatedField(many=True, required=False, queryset=Node.objects, label=_('Nodes'))
|
||||
is_periodic = serializers.BooleanField(default=False, required=False, label=_("Periodic perform"))
|
||||
|
||||
class Meta:
|
||||
read_only_fields = [
|
||||
|
||||
@@ -4,7 +4,8 @@ from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from accounts.const import (
|
||||
AutomationTypes, SecretType, SecretStrategy, SSHKeyStrategy
|
||||
AutomationTypes, SecretType, SecretStrategy,
|
||||
SSHKeyStrategy, ChangeSecretRecordStatusChoice
|
||||
)
|
||||
from accounts.models import (
|
||||
Account, ChangeSecretAutomation,
|
||||
@@ -21,6 +22,7 @@ logger = get_logger(__file__)
|
||||
__all__ = [
|
||||
'ChangeSecretAutomationSerializer',
|
||||
'ChangeSecretRecordSerializer',
|
||||
'ChangeSecretRecordViewSecretSerializer',
|
||||
'ChangeSecretRecordBackUpSerializer',
|
||||
'ChangeSecretUpdateAssetSerializer',
|
||||
'ChangeSecretUpdateNodeSerializer',
|
||||
@@ -104,7 +106,10 @@ class ChangeSecretAutomationSerializer(AuthValidateMixin, BaseAutomationSerializ
|
||||
class ChangeSecretRecordSerializer(serializers.ModelSerializer):
|
||||
is_success = serializers.SerializerMethodField(label=_('Is success'))
|
||||
asset = ObjectRelatedField(queryset=Asset.objects, label=_('Asset'))
|
||||
account = ObjectRelatedField(queryset=Account.objects, label=_('Account'))
|
||||
account = ObjectRelatedField(
|
||||
queryset=Account.objects, label=_('Account'),
|
||||
attrs=("id", "name", "username")
|
||||
)
|
||||
execution = ObjectRelatedField(
|
||||
queryset=AutomationExecution.objects, label=_('Automation task execution')
|
||||
)
|
||||
@@ -119,7 +124,16 @@ class ChangeSecretRecordSerializer(serializers.ModelSerializer):
|
||||
|
||||
@staticmethod
|
||||
def get_is_success(obj):
|
||||
return obj.status == 'success'
|
||||
return obj.status == ChangeSecretRecordStatusChoice.success.value
|
||||
|
||||
|
||||
class ChangeSecretRecordViewSecretSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ChangeSecretRecord
|
||||
fields = [
|
||||
'id', 'old_secret', 'new_secret',
|
||||
]
|
||||
read_only_fields = fields
|
||||
|
||||
|
||||
class ChangeSecretRecordBackUpSerializer(serializers.ModelSerializer):
|
||||
@@ -145,7 +159,7 @@ class ChangeSecretRecordBackUpSerializer(serializers.ModelSerializer):
|
||||
|
||||
@staticmethod
|
||||
def get_is_success(obj):
|
||||
if obj.status == 'success':
|
||||
if obj.status == ChangeSecretRecordStatusChoice.success.value:
|
||||
return _("Success")
|
||||
return _("Failed")
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ from django.dispatch import receiver
|
||||
from django.utils.translation import gettext_noop
|
||||
|
||||
from accounts.backends import vault_client
|
||||
from accounts.const import Source
|
||||
from audits.const import ActivityChoices
|
||||
from audits.signal_handlers import create_activities
|
||||
from common.decorators import merge_delay_run
|
||||
@@ -32,7 +33,7 @@ def push_accounts_if_need(accounts=()):
|
||||
template_accounts = defaultdict(list)
|
||||
for ac in accounts:
|
||||
# 再强调一次吧
|
||||
if ac.source != 'template':
|
||||
if ac.source != Source.TEMPLATE:
|
||||
continue
|
||||
template_accounts[ac.source_id].append(ac)
|
||||
|
||||
@@ -61,7 +62,7 @@ def create_accounts_activities(account, action='create'):
|
||||
|
||||
@receiver(post_save, sender=Account)
|
||||
def on_account_create_by_template(sender, instance, created=False, **kwargs):
|
||||
if not created or instance.source != 'template':
|
||||
if not created:
|
||||
return
|
||||
push_accounts_if_need.delay(accounts=(instance,))
|
||||
create_accounts_activities(instance, action='create')
|
||||
|
||||
@@ -36,14 +36,14 @@ def execute_account_automation_task(pid, trigger, tp):
|
||||
instance.execute(trigger)
|
||||
|
||||
|
||||
def record_task_activity_callback(self, record_id, *args, **kwargs):
|
||||
def record_task_activity_callback(self, record_ids, *args, **kwargs):
|
||||
from accounts.models import ChangeSecretRecord
|
||||
with tmp_to_root_org():
|
||||
record = get_object_or_none(ChangeSecretRecord, id=record_id)
|
||||
if not record:
|
||||
records = ChangeSecretRecord.objects.filter(id__in=record_ids)
|
||||
if not records:
|
||||
return
|
||||
resource_ids = [record.id]
|
||||
org_id = record.execution.org_id
|
||||
resource_ids = [str(i.id) for i in records]
|
||||
org_id = records[0].execution.org_id
|
||||
return resource_ids, org_id
|
||||
|
||||
|
||||
@@ -51,22 +51,26 @@ def record_task_activity_callback(self, record_id, *args, **kwargs):
|
||||
queue='ansible', verbose_name=_('Execute automation record'),
|
||||
activity_callback=record_task_activity_callback
|
||||
)
|
||||
def execute_automation_record_task(record_id, tp):
|
||||
def execute_automation_record_task(record_ids, tp):
|
||||
from accounts.models import ChangeSecretRecord
|
||||
task_name = gettext_noop('Execute automation record')
|
||||
|
||||
with tmp_to_root_org():
|
||||
instance = get_object_or_none(ChangeSecretRecord, pk=record_id)
|
||||
if not instance:
|
||||
logger.error("No automation record found: {}".format(record_id))
|
||||
records = ChangeSecretRecord.objects.filter(id__in=record_ids)
|
||||
|
||||
if not records:
|
||||
logger.error('No automation record found: {}'.format(record_ids))
|
||||
return
|
||||
|
||||
task_name = gettext_noop('Execute automation record')
|
||||
record = records[0]
|
||||
record_map = {f'{record.asset_id}-{record.account_id}': str(record.id) for record in records}
|
||||
task_snapshot = {
|
||||
'secret': instance.new_secret,
|
||||
'secret_type': instance.execution.snapshot.get('secret_type'),
|
||||
'accounts': [str(instance.account_id)],
|
||||
'assets': [str(instance.asset_id)],
|
||||
'params': {},
|
||||
'record_id': record_id,
|
||||
'record_map': record_map,
|
||||
'secret': record.new_secret,
|
||||
'secret_type': record.execution.snapshot.get('secret_type'),
|
||||
'assets': [str(instance.asset_id) for instance in records],
|
||||
'accounts': [str(instance.account_id) for instance in records],
|
||||
}
|
||||
with tmp_to_org(instance.execution.org_id):
|
||||
with tmp_to_org(record.execution.org_id):
|
||||
quickstart_automation_by_snapshot(task_name, tp, task_snapshot)
|
||||
|
||||
@@ -55,7 +55,7 @@ def clean_historical_accounts():
|
||||
history_model = Account.history.model
|
||||
history_id_mapper = defaultdict(list)
|
||||
|
||||
ids = history_model.objects.values('id').annotate(count=Count('id', distinct=True)) \
|
||||
ids = history_model.objects.values('id').annotate(count=Count('id')) \
|
||||
.filter(count__gte=limit).values_list('id', flat=True)
|
||||
|
||||
if not ids:
|
||||
|
||||
@@ -29,7 +29,8 @@ def template_sync_related_accounts(template_id, user_id=None):
|
||||
name = template.name
|
||||
username = template.username
|
||||
secret_type = template.secret_type
|
||||
print(f'\033[32m>>> 开始同步模版名称、用户名、密钥类型到相关联的账号 ({datetime.now().strftime("%Y-%m-%d %H:%M:%S")})')
|
||||
print(
|
||||
f'\033[32m>>> 开始同步模板名称、用户名、密钥类型到相关联的账号 ({datetime.now().strftime("%Y-%m-%d %H:%M:%S")})')
|
||||
with tmp_to_org(org_id):
|
||||
for account in accounts:
|
||||
account.name = name
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{% load i18n %}
|
||||
|
||||
|
||||
<h3>{% trans 'Gather account change information' %}</h3>
|
||||
<table style="width: 100%; border-collapse: collapse; max-width: 100%; text-align: left; margin-top: 20px;">
|
||||
<caption></caption>
|
||||
<tr style="background-color: #f2f2f2;">
|
||||
<th style="border: 1px solid #ddd; padding: 10px; font-weight: bold;">{% trans 'Asset' %}</th>
|
||||
<th style="border: 1px solid #ddd; padding: 10px;">{% trans 'Asset' %}</th>
|
||||
<th style="border: 1px solid #ddd; padding: 10px;">{% trans 'Added account' %}</th>
|
||||
<th style="border: 1px solid #ddd; padding: 10px;">{% trans 'Deleted account' %}</th>
|
||||
</tr>
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
{% load i18n %}
|
||||
|
||||
<h3>{% trans 'Task name' %}: {{ name }}</h3>
|
||||
<h3>{% trans 'Task execution id' %}: {{ execution_id }}</h3>
|
||||
<p>{% trans 'Respectful' %} {{ recipient }}</p>
|
||||
<p>{% trans 'Hello! The following is the failure of changing the password of your assets or pushing the account. Please check and handle it in time.' %}</p>
|
||||
<table style="width: 100%; border-collapse: collapse; max-width: 100%; text-align: left; margin-top: 20px;">
|
||||
<caption></caption>
|
||||
<thead>
|
||||
<tr style="background-color: #f2f2f2;">
|
||||
<th style="border: 1px solid #ddd; padding: 10px;">{% trans 'Asset' %}</th>
|
||||
<th style="border: 1px solid #ddd; padding: 10px;">{% trans 'Account' %}</th>
|
||||
<th style="border: 1px solid #ddd; padding: 10px;">{% trans 'Error' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for asset_name, account_username, error in asset_account_errors %}
|
||||
<tr>
|
||||
<td style="border: 1px solid #ddd; padding: 10px;">{{ asset_name }}</td>
|
||||
<td style="border: 1px solid #ddd; padding: 10px;">{{ account_username }}</td>
|
||||
<td style="border: 1px solid #ddd; padding: 10px;">
|
||||
<div style="
|
||||
max-width: 90%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: block;"
|
||||
title="{{ error }}"
|
||||
>
|
||||
{{ error }}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -2,10 +2,11 @@ import copy
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from accounts.const import SecretType, DEFAULT_PASSWORD_RULES
|
||||
from common.utils import ssh_key_gen, random_string
|
||||
from common.utils import validate_ssh_private_key, parse_ssh_private_key_str
|
||||
from common.utils import (
|
||||
validate_ssh_private_key, parse_ssh_private_key_str, ssh_key_gen,
|
||||
random_string
|
||||
)
|
||||
|
||||
|
||||
class SecretGenerator:
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# coding: utf-8
|
||||
#
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
@@ -8,7 +10,7 @@ from common.utils.ip import is_ip_address, is_ip_network, is_ip_segment
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
__all__ = ['RuleSerializer', 'ip_group_child_validator', 'ip_group_help_text']
|
||||
__all__ = ['RuleSerializer', 'ip_group_child_validator', 'ip_group_help_text', 'address_validator']
|
||||
|
||||
|
||||
def ip_group_child_validator(ip_group_child):
|
||||
@@ -21,6 +23,19 @@ def ip_group_child_validator(ip_group_child):
|
||||
raise serializers.ValidationError(error)
|
||||
|
||||
|
||||
def address_validator(value):
|
||||
parsed = urlparse(value)
|
||||
is_basic_url = parsed.scheme in ('http', 'https') and parsed.netloc
|
||||
is_valid = value == '*' \
|
||||
or is_ip_address(value) \
|
||||
or is_ip_network(value) \
|
||||
or is_ip_segment(value) \
|
||||
or is_basic_url
|
||||
if not is_valid:
|
||||
error = _('address invalid: `{}`').format(value)
|
||||
raise serializers.ValidationError(error)
|
||||
|
||||
|
||||
ip_group_help_text = _(
|
||||
'With * indicating a match all. '
|
||||
'Such as: '
|
||||
|
||||
@@ -32,6 +32,7 @@ __all__ = [
|
||||
|
||||
class AssetFilterSet(BaseFilterSet):
|
||||
platform = django_filters.CharFilter(method='filter_platform')
|
||||
exclude_platform = django_filters.CharFilter(field_name="platform__name", lookup_expr='exact', exclude=True)
|
||||
domain = django_filters.CharFilter(method='filter_domain')
|
||||
type = django_filters.CharFilter(field_name="platform__type", lookup_expr="exact")
|
||||
category = django_filters.CharFilter(field_name="platform__category", lookup_expr="exact")
|
||||
@@ -92,7 +93,6 @@ class AssetViewSet(SuggestionMixin, OrgBulkModelViewSet):
|
||||
model = Asset
|
||||
filterset_class = AssetFilterSet
|
||||
search_fields = ("name", "address", "comment")
|
||||
ordering = ('name',)
|
||||
ordering_fields = ('name', 'address', 'connectivity', 'platform', 'date_updated', 'date_created')
|
||||
serializer_classes = (
|
||||
("default", serializers.AssetSerializer),
|
||||
@@ -292,6 +292,7 @@ class AssetsTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView):
|
||||
def check_permissions(self, request):
|
||||
action_perm_require = {
|
||||
"refresh": "assets.refresh_assethardwareinfo",
|
||||
"test": "assets.test_assetconnectivity",
|
||||
}
|
||||
_action = request.data.get("action")
|
||||
perm_required = action_perm_require.get(_action)
|
||||
|
||||
@@ -19,7 +19,6 @@ class DomainViewSet(OrgBulkModelViewSet):
|
||||
model = Domain
|
||||
filterset_fields = ("name",)
|
||||
search_fields = filterset_fields
|
||||
ordering = ('name',)
|
||||
serializer_classes = {
|
||||
'default': serializers.DomainSerializer,
|
||||
'list': serializers.DomainListSerializer,
|
||||
@@ -30,6 +29,10 @@ class DomainViewSet(OrgBulkModelViewSet):
|
||||
return serializers.DomainWithGatewaySerializer
|
||||
return super().get_serializer_class()
|
||||
|
||||
def partial_update(self, request, *args, **kwargs):
|
||||
kwargs['partial'] = True
|
||||
return self.update(request, *args, **kwargs)
|
||||
|
||||
|
||||
class GatewayViewSet(HostViewSet):
|
||||
perm_model = Gateway
|
||||
|
||||
@@ -22,6 +22,7 @@ from orgs.utils import current_org
|
||||
from rbac.permissions import RBACPermission
|
||||
from .. import serializers
|
||||
from ..models import Node
|
||||
from ..signal_handlers import update_nodes_assets_amount
|
||||
from ..tasks import (
|
||||
update_node_assets_hardware_info_manual,
|
||||
test_node_assets_connectivity_manual,
|
||||
@@ -94,6 +95,7 @@ class NodeAddChildrenApi(generics.UpdateAPIView):
|
||||
children = Node.objects.filter(id__in=node_ids)
|
||||
for node in children:
|
||||
node.parent = instance
|
||||
update_nodes_assets_amount.delay(ttl=5, node_ids=(instance.id,))
|
||||
return Response("OK")
|
||||
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ class AssetPlatformViewSet(JMSModelViewSet):
|
||||
}
|
||||
filterset_fields = ['name', 'category', 'type']
|
||||
search_fields = ['name']
|
||||
ordering = ['-internal', 'name']
|
||||
rbac_perms = {
|
||||
'categories': 'assets.view_platform',
|
||||
'type_constraints': 'assets.view_platform',
|
||||
|
||||
@@ -39,7 +39,6 @@ class NodeChildrenApi(generics.ListCreateAPIView):
|
||||
self.instance = self.get_object()
|
||||
|
||||
def perform_create(self, serializer):
|
||||
with NodeAddChildrenLock(self.instance):
|
||||
data = serializer.validated_data
|
||||
_id = data.get("id")
|
||||
value = data.get("value")
|
||||
@@ -49,6 +48,7 @@ class NodeChildrenApi(generics.ListCreateAPIView):
|
||||
raise JMSException(_('The same level node name cannot be the same'))
|
||||
else:
|
||||
value = self.instance.get_next_child_preset_name()
|
||||
with NodeAddChildrenLock(self.instance):
|
||||
node = self.instance.create_child(value=value, _id=_id)
|
||||
# 避免查询 full value
|
||||
node._full_value = node.value
|
||||
@@ -126,7 +126,7 @@ class NodeChildrenAsTreeApi(SerializeToTreeNodeMixin, NodeChildrenApi):
|
||||
include_assets = self.request.query_params.get('assets', '0') == '1'
|
||||
if not self.instance or not include_assets:
|
||||
return Asset.objects.none()
|
||||
if self.instance.is_org_root():
|
||||
if not self.request.GET.get('search') and self.instance.is_org_root():
|
||||
return Asset.objects.none()
|
||||
if query_all:
|
||||
assets = self.instance.get_all_assets()
|
||||
|
||||
@@ -11,8 +11,10 @@ from django.utils.translation import gettext as _
|
||||
from sshtunnel import SSHTunnelForwarder
|
||||
|
||||
from assets.automations.methods import platform_automation_methods
|
||||
from common.db.utils import safe_db_connection
|
||||
from common.utils import get_logger, lazyproperty, is_openssh_format_key, ssh_pubkey_gen
|
||||
from ops.ansible import JMSInventory, PlaybookRunner, DefaultCallback
|
||||
from ops.ansible import JMSInventory, DefaultCallback, SuperPlaybookRunner
|
||||
from ops.ansible.interface import interface
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -36,7 +38,7 @@ class SSHTunnelManager:
|
||||
info = self.file_to_json(runner.inventory)
|
||||
servers, not_valid = [], []
|
||||
for k, host in info['all']['hosts'].items():
|
||||
jms_asset, jms_gateway = host.get('jms_asset'), host.get('gateway')
|
||||
jms_asset, jms_gateway = host.get('jms_asset'), host.get('jms_gateway')
|
||||
if not jms_gateway:
|
||||
continue
|
||||
try:
|
||||
@@ -54,7 +56,9 @@ class SSHTunnelManager:
|
||||
not_valid.append(k)
|
||||
else:
|
||||
local_bind_port = server.local_bind_port
|
||||
host['ansible_host'] = jms_asset['address'] = host['login_host'] = '127.0.0.1'
|
||||
|
||||
host['ansible_host'] = jms_asset['address'] = host[
|
||||
'login_host'] = interface.get_gateway_proxy_host()
|
||||
host['ansible_port'] = jms_asset['port'] = host['login_port'] = local_bind_port
|
||||
servers.append(server)
|
||||
|
||||
@@ -110,11 +114,7 @@ class BasePlaybookManager:
|
||||
if not data:
|
||||
data = automation_params.get(method_id, {})
|
||||
params = serializer(data).data
|
||||
return {
|
||||
field_name: automation_params.get(field_name, '')
|
||||
if not params[field_name] else params[field_name]
|
||||
for field_name in params
|
||||
}
|
||||
return params
|
||||
|
||||
@property
|
||||
def platform_automation_methods(self):
|
||||
@@ -188,6 +188,7 @@ class BasePlaybookManager:
|
||||
host['error'] = _('{} disabled'.format(self.__class__.method_type()))
|
||||
return host
|
||||
|
||||
host['check_conn_after_change'] = settings.CHECK_CONN_AFTER_CHANGE
|
||||
host = self.convert_cert_to_file(host, kwargs.get('path_dir'))
|
||||
host['params'] = self.get_params(automation, method_type)
|
||||
return host
|
||||
@@ -269,7 +270,7 @@ class BasePlaybookManager:
|
||||
if not playbook_path:
|
||||
continue
|
||||
|
||||
runer = PlaybookRunner(
|
||||
runer = SuperPlaybookRunner(
|
||||
inventory_path,
|
||||
playbook_path,
|
||||
self.runtime_dir,
|
||||
@@ -297,12 +298,16 @@ class BasePlaybookManager:
|
||||
for host in hosts:
|
||||
result = cb.host_results.get(host)
|
||||
if state == 'ok':
|
||||
self.on_host_success(host, result)
|
||||
self.on_host_success(host, result.get('ok', ''))
|
||||
elif state == 'skipped':
|
||||
pass
|
||||
else:
|
||||
error = hosts.get(host)
|
||||
self.on_host_error(host, error, result)
|
||||
self.on_host_error(
|
||||
host, error,
|
||||
result.get('failures', '')
|
||||
or result.get('dark', '')
|
||||
)
|
||||
|
||||
def on_runner_failed(self, runner, e):
|
||||
print("Runner failed: {} {}".format(e, self))
|
||||
@@ -314,7 +319,7 @@ class BasePlaybookManager:
|
||||
def delete_runtime_dir(self):
|
||||
if settings.DEBUG_DEV:
|
||||
return
|
||||
shutil.rmtree(self.runtime_dir)
|
||||
shutil.rmtree(self.runtime_dir, ignore_errors=True)
|
||||
|
||||
def run(self, *args, **kwargs):
|
||||
print(">>> 任务准备阶段\n")
|
||||
@@ -333,7 +338,9 @@ class BasePlaybookManager:
|
||||
ssh_tunnel = SSHTunnelManager()
|
||||
ssh_tunnel.local_gateway_prepare(runner)
|
||||
try:
|
||||
kwargs.update({"clean_workspace": False})
|
||||
cb = runner.run(**kwargs)
|
||||
with safe_db_connection():
|
||||
self.on_runner_success(runner, cb)
|
||||
except Exception as e:
|
||||
self.on_runner_failed(runner, e)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from collections import Counter
|
||||
|
||||
__all__ = ['FormatAssetInfo']
|
||||
|
||||
|
||||
@@ -7,13 +9,28 @@ class FormatAssetInfo:
|
||||
self.tp = tp
|
||||
|
||||
@staticmethod
|
||||
def posix_format(info):
|
||||
for cpu_model in info.get('cpu_model', []):
|
||||
if cpu_model.endswith('GHz') or cpu_model.startswith("Intel"):
|
||||
break
|
||||
def get_cpu_model_count(cpus):
|
||||
try:
|
||||
if len(cpus) % 3 == 0:
|
||||
step = 3
|
||||
models = [cpus[i + 2] for i in range(0, len(cpus), step)]
|
||||
elif len(cpus) % 2 == 0:
|
||||
step = 2
|
||||
models = [cpus[i + 1] for i in range(0, len(cpus), step)]
|
||||
else:
|
||||
cpu_model = ''
|
||||
info['cpu_model'] = cpu_model[:48]
|
||||
raise ValueError("CPU list format not recognized")
|
||||
|
||||
model_counts = Counter(models)
|
||||
result = ', '.join([f"{model} x{count}" for model, count in model_counts.items()])
|
||||
except Exception as e:
|
||||
print(f"Error processing CPU model list: {e}")
|
||||
result = ''
|
||||
return result
|
||||
|
||||
def posix_format(self, info):
|
||||
cpus = self.get_cpu_model_count(info.get('cpu_model', []))
|
||||
|
||||
info['cpu_model'] = cpus
|
||||
info['cpu_count'] = info.get('cpu_count', 0)
|
||||
return info
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
vars:
|
||||
ansible_shell_type: sh
|
||||
ansible_connection: local
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
|
||||
tasks:
|
||||
- name: Test asset connection (pyfreerdp)
|
||||
|
||||
@@ -14,8 +14,11 @@
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_secret_type: "{{ jms_account.secret_type }}"
|
||||
login_private_key_path: "{{ jms_account.private_key_path }}"
|
||||
become: "{{ custom_become | default(False) }}"
|
||||
become_method: "{{ custom_become_method | default('su') }}"
|
||||
become_user: "{{ custom_become_user | default('') }}"
|
||||
become_password: "{{ custom_become_password | default('') }}"
|
||||
become_private_key_path: "{{ custom_become_private_key_path | default(None) }}"
|
||||
become: "{{ jms_custom_become | default(False) }}"
|
||||
become_method: "{{ jms_custom_become_method | default('su') }}"
|
||||
become_user: "{{ jms_custom_become_user | default('') }}"
|
||||
become_password: "{{ jms_custom_become_password | default('') }}"
|
||||
become_private_key_path: "{{ jms_custom_become_private_key_path | default(None) }}"
|
||||
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
||||
|
||||
|
||||
11
apps/assets/automations/ping/custom/telnet/main.yml
Normal file
11
apps/assets/automations/ping/custom/telnet/main.yml
Normal file
@@ -0,0 +1,11 @@
|
||||
- hosts: custom
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_connection: local
|
||||
ansible_shell_type: sh
|
||||
|
||||
tasks:
|
||||
- name: Test asset connection (telnet)
|
||||
telnet_ping:
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
16
apps/assets/automations/ping/custom/telnet/manifest.yml
Normal file
16
apps/assets/automations/ping/custom/telnet/manifest.yml
Normal file
@@ -0,0 +1,16 @@
|
||||
id: ping_by_telnet
|
||||
name: "{{ 'Ping by telnet' | trans }}"
|
||||
category:
|
||||
- device
|
||||
- host
|
||||
type:
|
||||
- all
|
||||
method: ping
|
||||
protocol: telnet
|
||||
priority: 50
|
||||
|
||||
i18n:
|
||||
Ping by telnet:
|
||||
zh: '使用 Python 模块 telnet 测试主机可连接性'
|
||||
en: 'Ping by telnet module'
|
||||
ja: 'Pythonモジュールtelnetを使用したホスト接続性のテスト'
|
||||
@@ -16,3 +16,5 @@
|
||||
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||
connection_options:
|
||||
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
|
||||
register: result
|
||||
failed_when: not result.is_available
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
- hosts: demo
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_timeout: 30
|
||||
tasks:
|
||||
- name: Posix ping
|
||||
ansible.builtin.ping:
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
- hosts: windows
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_timeout: 30
|
||||
tasks:
|
||||
- name: Refresh connection
|
||||
ansible.builtin.meta: reset_connection
|
||||
|
||||
@@ -25,14 +25,22 @@ class PingManager(BasePlaybookManager):
|
||||
|
||||
def on_host_success(self, host, result):
|
||||
asset, account = self.host_asset_and_account_mapper.get(host)
|
||||
try:
|
||||
asset.set_connectivity(Connectivity.OK)
|
||||
if not account:
|
||||
return
|
||||
account.set_connectivity(Connectivity.OK)
|
||||
except Exception as e:
|
||||
print(f'\033[31m Update account {account.name} or '
|
||||
f'update asset {asset.name} connectivity failed: {e} \033[0m\n')
|
||||
|
||||
def on_host_error(self, host, error, result):
|
||||
asset, account = self.host_asset_and_account_mapper.get(host)
|
||||
try:
|
||||
asset.set_connectivity(Connectivity.ERR)
|
||||
if not account:
|
||||
return
|
||||
account.set_connectivity(Connectivity.ERR)
|
||||
except Exception as e:
|
||||
print(f'\033[31m Update account {account.name} or '
|
||||
f'update asset {asset.name} connectivity failed: {e} \033[0m\n')
|
||||
|
||||
@@ -92,18 +92,26 @@ class PingGatewayManager:
|
||||
@staticmethod
|
||||
def on_host_success(gateway, account):
|
||||
print('\033[32m {} -> {}\033[0m\n'.format(gateway, account))
|
||||
try:
|
||||
gateway.set_connectivity(Connectivity.OK)
|
||||
if not account:
|
||||
return
|
||||
account.set_connectivity(Connectivity.OK)
|
||||
except Exception as e:
|
||||
print(f'\033[31m Update account {account.name} or '
|
||||
f'update asset {gateway.name} connectivity failed: {e} \033[0m\n')
|
||||
|
||||
@staticmethod
|
||||
def on_host_error(gateway, account, error):
|
||||
print('\033[31m {} -> {} 原因: {} \033[0m\n'.format(gateway, account, error))
|
||||
try:
|
||||
gateway.set_connectivity(Connectivity.ERR)
|
||||
if not account:
|
||||
return
|
||||
account.set_connectivity(Connectivity.ERR)
|
||||
except Exception as e:
|
||||
print(f'\033[31m Update account {account.name} or '
|
||||
f'update asset {gateway.name} connectivity failed: {e} \033[0m\n')
|
||||
|
||||
@staticmethod
|
||||
def before_runner_start():
|
||||
|
||||
@@ -2,5 +2,6 @@ from .automation import *
|
||||
from .base import *
|
||||
from .category import *
|
||||
from .host import *
|
||||
from .platform import *
|
||||
from .protocol import *
|
||||
from .types import *
|
||||
|
||||
@@ -8,6 +8,7 @@ class DatabaseTypes(BaseType):
|
||||
ORACLE = 'oracle', 'Oracle'
|
||||
SQLSERVER = 'sqlserver', 'SQLServer'
|
||||
DB2 = 'db2', 'DB2'
|
||||
DAMENG = 'dameng', 'Dameng'
|
||||
CLICKHOUSE = 'clickhouse', 'ClickHouse'
|
||||
MONGODB = 'mongodb', 'MongoDB'
|
||||
REDIS = 'redis', 'Redis'
|
||||
@@ -36,6 +37,7 @@ class DatabaseTypes(BaseType):
|
||||
'verify_account_enabled': True,
|
||||
'change_secret_enabled': True,
|
||||
'push_account_enabled': True,
|
||||
'remove_account_enabled': True,
|
||||
},
|
||||
cls.REDIS: {
|
||||
'ansible_enabled': False,
|
||||
@@ -55,6 +57,15 @@ class DatabaseTypes(BaseType):
|
||||
'change_secret_enabled': False,
|
||||
'push_account_enabled': False,
|
||||
},
|
||||
cls.DAMENG: {
|
||||
'ansible_enabled': False,
|
||||
'ping_enabled': False,
|
||||
'gather_facts_enabled': False,
|
||||
'gather_accounts_enabled': False,
|
||||
'verify_account_enabled': False,
|
||||
'change_secret_enabled': False,
|
||||
'push_account_enabled': False,
|
||||
},
|
||||
cls.CLICKHOUSE: {
|
||||
'ansible_enabled': False,
|
||||
'ping_enabled': False,
|
||||
@@ -84,6 +95,7 @@ class DatabaseTypes(BaseType):
|
||||
cls.ORACLE: [{'name': 'Oracle'}],
|
||||
cls.SQLSERVER: [{'name': 'SQLServer'}],
|
||||
cls.DB2: [{'name': 'DB2'}],
|
||||
cls.DAMENG: [{'name': 'Dameng'}],
|
||||
cls.CLICKHOUSE: [{'name': 'ClickHouse'}],
|
||||
cls.MONGODB: [{'name': 'MongoDB'}],
|
||||
cls.REDIS: [
|
||||
|
||||
@@ -19,7 +19,7 @@ class HostTypes(BaseType):
|
||||
'charset': 'utf-8', # default
|
||||
'domain_enabled': True,
|
||||
'su_enabled': True,
|
||||
'su_methods': ['sudo', 'su'],
|
||||
'su_methods': ['sudo', 'su', 'only_sudo', 'only_su'],
|
||||
},
|
||||
cls.WINDOWS: {
|
||||
'su_enabled': False,
|
||||
@@ -53,7 +53,8 @@ class HostTypes(BaseType):
|
||||
'gather_accounts_enabled': True,
|
||||
'verify_account_enabled': True,
|
||||
'change_secret_enabled': True,
|
||||
'push_account_enabled': True
|
||||
'push_account_enabled': True,
|
||||
'remove_account_enabled': True,
|
||||
},
|
||||
cls.WINDOWS: {
|
||||
'ansible_config': {
|
||||
|
||||
11
apps/assets/const/platform.py
Normal file
11
apps/assets/const/platform.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from django.db.models import TextChoices
|
||||
|
||||
|
||||
class SuMethodChoices(TextChoices):
|
||||
sudo = "sudo", "sudo su -"
|
||||
su = "su", "su - "
|
||||
only_sudo = "only_sudo", "sudo su"
|
||||
only_su = "only_su", "su"
|
||||
enable = "enable", "enable"
|
||||
super = "super", "super 15"
|
||||
super_level = "super_level", "super level 15"
|
||||
@@ -23,6 +23,7 @@ class Protocol(ChoicesMixin, models.TextChoices):
|
||||
postgresql = 'postgresql', 'PostgreSQL'
|
||||
sqlserver = 'sqlserver', 'SQLServer'
|
||||
db2 = 'db2', 'DB2'
|
||||
dameng = 'dameng', 'Dameng'
|
||||
clickhouse = 'clickhouse', 'ClickHouse'
|
||||
redis = 'redis', 'Redis'
|
||||
mongodb = 'mongodb', 'MongoDB'
|
||||
@@ -38,6 +39,14 @@ class Protocol(ChoicesMixin, models.TextChoices):
|
||||
cls.ssh: {
|
||||
'port': 22,
|
||||
'secret_types': ['password', 'ssh_key'],
|
||||
'setting': {
|
||||
'old_ssh_version': {
|
||||
'type': 'bool',
|
||||
'default': False,
|
||||
'label': _('Old SSH version'),
|
||||
'help_text': _('Old SSH version like openssh 5.x or 6.x')
|
||||
}
|
||||
}
|
||||
},
|
||||
cls.sftp: {
|
||||
'port': 22,
|
||||
@@ -177,6 +186,12 @@ class Protocol(ChoicesMixin, models.TextChoices):
|
||||
'secret_types': ['password'],
|
||||
'xpack': True,
|
||||
},
|
||||
cls.dameng: {
|
||||
'port': 5236,
|
||||
'required': True,
|
||||
'secret_types': ['password'],
|
||||
'xpack': True,
|
||||
},
|
||||
cls.clickhouse: {
|
||||
'port': 9000,
|
||||
'required': True,
|
||||
@@ -187,6 +202,20 @@ class Protocol(ChoicesMixin, models.TextChoices):
|
||||
'port': 27017,
|
||||
'required': True,
|
||||
'secret_types': ['password'],
|
||||
'setting': {
|
||||
'auth_source': {
|
||||
'type': 'str',
|
||||
'default': 'admin',
|
||||
'label': _('Auth source'),
|
||||
'help_text': _('The database to authenticate against')
|
||||
},
|
||||
'connection_options': {
|
||||
'type': 'str',
|
||||
'default': '',
|
||||
'label': _('Connection options'),
|
||||
'help_text': _('The connection specific options eg. retryWrites=false&retryReads=false')
|
||||
}
|
||||
}
|
||||
},
|
||||
cls.redis: {
|
||||
'port': 6379,
|
||||
@@ -266,22 +295,17 @@ class Protocol(ChoicesMixin, models.TextChoices):
|
||||
'setting': {
|
||||
'api_mode': {
|
||||
'type': 'choice',
|
||||
'default': 'gpt-3.5-turbo',
|
||||
'default': 'gpt-4o-mini',
|
||||
'label': _('API mode'),
|
||||
'choices': [
|
||||
('gpt-3.5-turbo', 'GPT-3.5 Turbo'),
|
||||
('gpt-3.5-turbo-16k', 'GPT-3.5 Turbo 16K'),
|
||||
('gpt-4o-mini', 'GPT-4o-mini'),
|
||||
('gpt-4o', 'GPT-4o'),
|
||||
('gpt-4-turbo', 'GPT-4 Turbo'),
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if settings.XPACK_LICENSE_IS_VALID:
|
||||
choices = protocols[cls.chatgpt]['setting']['api_mode']['choices']
|
||||
choices.extend([
|
||||
('gpt-4', 'GPT-4'),
|
||||
('gpt-4-32k', 'GPT-4 32K'),
|
||||
])
|
||||
return protocols
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Generated by Django 3.2.12 on 2022-07-11 06:13
|
||||
|
||||
import time
|
||||
import math
|
||||
from django.utils import timezone
|
||||
from itertools import groupby
|
||||
from django.db import migrations
|
||||
@@ -40,9 +41,13 @@ def migrate_asset_accounts(apps, schema_editor):
|
||||
if system_user:
|
||||
# 更新一次系统用户的认证属性
|
||||
account_values.update({attr: getattr(system_user, attr, '') for attr in all_attrs})
|
||||
account_values['created_by'] = str(system_user.id)
|
||||
account_values['privileged'] = system_user.type == 'admin' \
|
||||
or system_user.username in ['root', 'Administrator']
|
||||
if system_user.su_enabled and system_user.su_from:
|
||||
created_by = f'{str(system_user.id)}::{str(system_user.su_from.username)}'
|
||||
else:
|
||||
created_by = str(system_user.id)
|
||||
account_values['created_by'] = created_by
|
||||
|
||||
auth_book_auth = {attr: getattr(auth_book, attr, '') for attr in all_attrs if getattr(auth_book, attr, '')}
|
||||
# 最终优先使用 auth_book 的认证属性
|
||||
@@ -117,6 +122,70 @@ def migrate_asset_accounts(apps, schema_editor):
|
||||
print("\t - histories: {}".format(len(accounts_to_history)))
|
||||
|
||||
|
||||
def update_asset_accounts_su_from(apps, schema_editor):
|
||||
# Update accounts su_from
|
||||
print("\n\tStart update asset accounts su_from field")
|
||||
account_model = apps.get_model('accounts', 'Account')
|
||||
platform_model = apps.get_model('assets', 'Platform')
|
||||
asset_model = apps.get_model('assets', 'Asset')
|
||||
platform_ids = list(platform_model.objects.filter(su_enabled=True).values_list('id', flat=True))
|
||||
|
||||
count = 0
|
||||
step_size = 1000
|
||||
count_account = 0
|
||||
while True:
|
||||
start = time.time()
|
||||
asset_ids = asset_model.objects \
|
||||
.filter(platform_id__in=platform_ids) \
|
||||
.values_list('id', flat=True)[count:count + step_size]
|
||||
asset_ids = list(asset_ids)
|
||||
if not asset_ids:
|
||||
break
|
||||
count += len(asset_ids)
|
||||
|
||||
accounts = list(account_model.objects.filter(asset_id__in=asset_ids))
|
||||
|
||||
# {asset_id_account_username: account.id}}
|
||||
asset_accounts_mapper = {}
|
||||
for a in accounts:
|
||||
try:
|
||||
k = f'{a.asset_id}_{a.username}'
|
||||
asset_accounts_mapper[k] = str(a.id)
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
update_accounts = []
|
||||
for a in accounts:
|
||||
try:
|
||||
if not a.created_by:
|
||||
continue
|
||||
created_by_list = a.created_by.split('::')
|
||||
if len(created_by_list) != 2:
|
||||
continue
|
||||
su_from_username = created_by_list[1]
|
||||
if not su_from_username:
|
||||
continue
|
||||
k = f'{a.asset_id}_{su_from_username}'
|
||||
su_from_id = asset_accounts_mapper.get(k)
|
||||
if not su_from_id:
|
||||
continue
|
||||
a.su_from_id = su_from_id
|
||||
update_accounts.append(a)
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
count_account += len(update_accounts)
|
||||
|
||||
log_msg = "\t - [{}]: Update accounts su_from: {}-{} {:.2f}s"
|
||||
try:
|
||||
account_model.objects.bulk_update(update_accounts, ['su_from_id'])
|
||||
except Exception as e:
|
||||
status = 'Failed'
|
||||
else:
|
||||
status = 'Success'
|
||||
print(log_msg.format(status, count_account - len(update_accounts), count_account, time.time() - start))
|
||||
|
||||
|
||||
def migrate_db_accounts(apps, schema_editor):
|
||||
app_perm_model = apps.get_model('perms', 'ApplicationPermission')
|
||||
account_model = apps.get_model('accounts', 'Account')
|
||||
@@ -196,5 +265,6 @@ class Migration(migrations.Migration):
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(migrate_asset_accounts),
|
||||
migrations.RunPython(update_asset_accounts_su_from),
|
||||
migrations.RunPython(migrate_db_accounts),
|
||||
]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user