mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-12-15 16:42:34 +00:00
Compare commits
492 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
10ff7730ec | ||
|
|
4f70e313b4 | ||
|
|
c225fd584d | ||
|
|
8ed6a64a9e | ||
|
|
d7e97629aa | ||
|
|
2c20889540 | ||
|
|
7d325856b9 | ||
|
|
28879d9314 | ||
|
|
32fab08ed3 | ||
|
|
8943850ca9 | ||
|
|
8fff57813a | ||
|
|
9128210e87 | ||
|
|
e3dd03f4c7 | ||
|
|
aabb2aff1f | ||
|
|
f8bbca38e3 | ||
|
|
12b180ddea | ||
|
|
4917769964 | ||
|
|
5868d56e18 | ||
|
|
bab76f3cda | ||
|
|
475c0e4187 | ||
|
|
3d070231f4 | ||
|
|
c5b0cafabd | ||
|
|
a69bba8702 | ||
|
|
cfd0098019 | ||
|
|
52f1dcf662 | ||
|
|
373c6c77e0 | ||
|
|
f3d052554d | ||
|
|
a57ce482dd | ||
|
|
a449d97f67 | ||
|
|
84e4238848 | ||
|
|
82dd1c35ea | ||
|
|
5ac974a44c | ||
|
|
c4caeb92ee | ||
|
|
f4799d90c0 | ||
|
|
f97685c788 | ||
|
|
0439376326 | ||
|
|
dd2413edd8 | ||
|
|
459c5c07c9 | ||
|
|
ef86a49c1e | ||
|
|
0ad389515b | ||
|
|
2432b9a553 | ||
|
|
12216a718a | ||
|
|
2190db1bb5 | ||
|
|
5d36537404 | ||
|
|
5a87634c26 | ||
|
|
e5eb84999a | ||
|
|
426a86b52d | ||
|
|
f84acfe282 | ||
|
|
c73b49fe30 | ||
|
|
98238f71ae | ||
|
|
66e45f1c80 | ||
|
|
93a400f6e6 | ||
|
|
535d7d8373 | ||
|
|
db268280b4 | ||
|
|
873789bdab | ||
|
|
6584890ab1 | ||
|
|
96d5c519ec | ||
|
|
6e91217303 | ||
|
|
5dd1dfc59e | ||
|
|
a53e930950 | ||
|
|
8f52f79d91 | ||
|
|
3af0e68c84 | ||
|
|
3ccf32ed48 | ||
|
|
d52ed2ffb9 | ||
|
|
38588151d1 | ||
|
|
2a95aca28f | ||
|
|
1915224063 | ||
|
|
da4f9efb42 | ||
|
|
579c2c1d7a | ||
|
|
2a86c3a376 | ||
|
|
5558e854de | ||
|
|
31b6c3b679 | ||
|
|
2209a2c8d2 | ||
|
|
f596b65ed7 | ||
|
|
2c9c64a13f | ||
|
|
1d49b3deca | ||
|
|
6701a1b604 | ||
|
|
b8ff3b38bf | ||
|
|
d3be16ffe8 | ||
|
|
e3648d11b1 | ||
|
|
d4037998c8 | ||
|
|
82de636b5c | ||
|
|
91f1280f97 | ||
|
|
7c82f5aa2b | ||
|
|
6a801eaf33 | ||
|
|
28da819735 | ||
|
|
cdf3cf3e8f | ||
|
|
118564577e | ||
|
|
47f2df0a5b | ||
|
|
e4aafc236d | ||
|
|
b1c530bba8 | ||
|
|
95aa9781c3 | ||
|
|
9f6540afe3 | ||
|
|
832bb832ce | ||
|
|
501329a8db | ||
|
|
8913aacd1e | ||
|
|
e461fbdf50 | ||
|
|
3941539408 | ||
|
|
605db2d905 | ||
|
|
1ef3f24465 | ||
|
|
4090a0b123 | ||
|
|
a55e28fc87 | ||
|
|
82cf53181f | ||
|
|
78232aa900 | ||
|
|
d2c93aff66 | ||
|
|
516e2309c0 | ||
|
|
4688e46f97 | ||
|
|
1299f3da75 | ||
|
|
fe502cbe41 | ||
|
|
09bfac34f1 | ||
|
|
12a86d7244 | ||
|
|
269eea8802 | ||
|
|
72aa265dd7 | ||
|
|
e26716e1e1 | ||
|
|
80b9db417c | ||
|
|
d944b5f4ff | ||
|
|
1b84afee0c | ||
|
|
172b6edd28 | ||
|
|
e6f248bfa0 | ||
|
|
1f037b1933 | ||
|
|
ae9bbd2683 | ||
|
|
a0085c4eab | ||
|
|
ddb71c43c4 | ||
|
|
8227f44058 | ||
|
|
e81762d692 | ||
|
|
5b8fa1809c | ||
|
|
90ba6442dd | ||
|
|
a28334b6d8 | ||
|
|
692dd6c8c4 | ||
|
|
15992ad5b3 | ||
|
|
072c3155ca | ||
|
|
9cb5985947 | ||
|
|
0fd2f18240 | ||
|
|
9ca8ab218c | ||
|
|
fcd8356e90 | ||
|
|
64d093e677 | ||
|
|
11493b9f3d | ||
|
|
5a9c91d9dd | ||
|
|
25dcb9510c | ||
|
|
a38a1868ca | ||
|
|
bec9b97092 | ||
|
|
d5b9596e7d | ||
|
|
af85d551ad | ||
|
|
ab8c57894e | ||
|
|
0e0c9275bd | ||
|
|
21b4a8600c | ||
|
|
4cf5573c36 | ||
|
|
962ea67b84 | ||
|
|
31720c9dcc | ||
|
|
54fe4835f6 | ||
|
|
91649a3908 | ||
|
|
0a242c3e81 | ||
|
|
25d1b3334f | ||
|
|
ffde306a04 | ||
|
|
f1e29a91f7 | ||
|
|
ec2b3b4cda | ||
|
|
1a9d9e4145 | ||
|
|
a14f121fad | ||
|
|
a25da8d479 | ||
|
|
15fe7f810b | ||
|
|
f6a4253936 | ||
|
|
c3c5801d2e | ||
|
|
f0d564180c | ||
|
|
8ee7230ead | ||
|
|
90f03dda62 | ||
|
|
4e7a5d8d4f | ||
|
|
2ed0927b18 | ||
|
|
e98235ca27 | ||
|
|
1b052a8729 | ||
|
|
34b188bbe7 | ||
|
|
3e6cd1c1d3 | ||
|
|
f8e248f0af | ||
|
|
b331730422 | ||
|
|
de3865fa1d | ||
|
|
1bc913ab13 | ||
|
|
2f11a70341 | ||
|
|
c277aec561 | ||
|
|
2a53a20808 | ||
|
|
674ad40f67 | ||
|
|
78089e01a3 | ||
|
|
1b71350199 | ||
|
|
5d08438dad | ||
|
|
31ba0564e4 | ||
|
|
ea5b7cd921 | ||
|
|
3e541162e3 | ||
|
|
6e19384231 | ||
|
|
19903c80c3 | ||
|
|
070af8c491 | ||
|
|
0fca33d874 | ||
|
|
08fdc57543 | ||
|
|
bb60d2a1d9 | ||
|
|
0014bd0cb9 | ||
|
|
9488c8bd97 | ||
|
|
1f30d459ae | ||
|
|
4e933fc1ca | ||
|
|
c0f3a1f64a | ||
|
|
0f70f5eccf | ||
|
|
eef942c155 | ||
|
|
061592fa6b | ||
|
|
c7a02586c1 | ||
|
|
ddcd4ebbfc | ||
|
|
9550ea62fb | ||
|
|
abcb589658 | ||
|
|
1bb366ad94 | ||
|
|
a5df7738f6 | ||
|
|
da858c8998 | ||
|
|
724a8f6324 | ||
|
|
437df9a533 | ||
|
|
f2c70d0bba | ||
|
|
ea913a5b6e | ||
|
|
c0cd8878dc | ||
|
|
15e995ade6 | ||
|
|
cadf42f3fa | ||
|
|
f588093cd3 | ||
|
|
7c12f8f462 | ||
|
|
6f5a92c21f | ||
|
|
17a76994dc | ||
|
|
39d793bc47 | ||
|
|
c3eafbee8c | ||
|
|
10f99be100 | ||
|
|
8eb6cfa9c9 | ||
|
|
f430c9e435 | ||
|
|
10c428a432 | ||
|
|
a30c603bdc | ||
|
|
39a75074af | ||
|
|
452ed2baf1 | ||
|
|
8c7240193a | ||
|
|
b622aca9af | ||
|
|
ce7edc1612 | ||
|
|
ebf1a9d5e2 | ||
|
|
23ef185b7e | ||
|
|
69f49f7776 | ||
|
|
6b16aa6bc0 | ||
|
|
43741dc9b2 | ||
|
|
18174e2867 | ||
|
|
3077d11483 | ||
|
|
fcd684e2db | ||
|
|
afcb6bd77c | ||
|
|
1c264399bb | ||
|
|
872e2546e9 | ||
|
|
8f347eee4d | ||
|
|
fa886b90c2 | ||
|
|
caf312c5be | ||
|
|
ac6168a06c | ||
|
|
eba9f2325a | ||
|
|
b46e772d09 | ||
|
|
183df82a75 | ||
|
|
98c91d0f18 | ||
|
|
e17d875206 | ||
|
|
4b1e84ed8a | ||
|
|
71ee33e3be | ||
|
|
5dd24f5cf9 | ||
|
|
2b6e818943 | ||
|
|
8c4e9720d3 | ||
|
|
d43709f584 | ||
|
|
89496baae5 | ||
|
|
ea6d995f55 | ||
|
|
cf6aba1f38 | ||
|
|
fdcda83c93 | ||
|
|
6e3369c944 | ||
|
|
d7e432a851 | ||
|
|
c0a153d13a | ||
|
|
2acc1dc875 | ||
|
|
32ed43ba7b | ||
|
|
e04e31eb30 | ||
|
|
ff747f9e42 | ||
|
|
c4bd093fd7 | ||
|
|
408b2d6dbd | ||
|
|
f1e5c7c2bb | ||
|
|
1d640eccf6 | ||
|
|
92fc0ceb16 | ||
|
|
217ea03c18 | ||
|
|
923f0ed477 | ||
|
|
3c6cfaa6cf | ||
|
|
0bfe255966 | ||
|
|
af5d531131 | ||
|
|
10d58ef424 | ||
|
|
64064cb526 | ||
|
|
46941037dd | ||
|
|
8ad71b6dd9 | ||
|
|
220ccda04d | ||
|
|
6d30fe797c | ||
|
|
3318df1771 | ||
|
|
0ccd806eca | ||
|
|
7ebe1c2916 | ||
|
|
08904c2a9f | ||
|
|
19e34270d1 | ||
|
|
afa515d570 | ||
|
|
bcba408517 | ||
|
|
80d94074e7 | ||
|
|
9347405f08 | ||
|
|
da4ad11a69 | ||
|
|
b81e424e80 | ||
|
|
1f15937139 | ||
|
|
f4fa011714 | ||
|
|
c5a9a85818 | ||
|
|
e23bfa0f69 | ||
|
|
451690fe8b | ||
|
|
5bea782b9f | ||
|
|
f26f7ca1e7 | ||
|
|
583d295fd1 | ||
|
|
b51af1f7d7 | ||
|
|
edcf9921fe | ||
|
|
eef172d0e2 | ||
|
|
5b407fe8bc | ||
|
|
1bb9048910 | ||
|
|
787cdbcadf | ||
|
|
b14ca14120 | ||
|
|
4b2fd0d0da | ||
|
|
3393f18399 | ||
|
|
7e1a379e47 | ||
|
|
213fdd461b | ||
|
|
148c7ffb43 | ||
|
|
75be45ce43 | ||
|
|
04eb670ada | ||
|
|
66f3706142 | ||
|
|
9ea98bf2b2 | ||
|
|
4695f80172 | ||
|
|
0452d53c3f | ||
|
|
ec30ef1f8b | ||
|
|
1c0ad08d80 | ||
|
|
e1ab453780 | ||
|
|
1a6597b572 | ||
|
|
865522953a | ||
|
|
4d4a107101 | ||
|
|
82f70cb0dc | ||
|
|
820186c6d0 | ||
|
|
4468e2d379 | ||
|
|
bd802e6a50 | ||
|
|
9362c272cb | ||
|
|
ee4534ac4b | ||
|
|
7ef09a4ca1 | ||
|
|
31daaed4cd | ||
|
|
71202e83f5 | ||
|
|
b1640e5592 | ||
|
|
076b7babcb | ||
|
|
8569910658 | ||
|
|
34c556d375 | ||
|
|
a43d6ad34d | ||
|
|
ca6825008b | ||
|
|
9c6f118dbd | ||
|
|
5730e60089 | ||
|
|
afc7f3bb9c | ||
|
|
c411b0a38e | ||
|
|
403b6fc563 | ||
|
|
55ae8bb5e6 | ||
|
|
dbcf785e42 | ||
|
|
e6cd126045 | ||
|
|
420f3c0c4c | ||
|
|
9b2c5cb305 | ||
|
|
907f0068db | ||
|
|
a16b3260ba | ||
|
|
1845821f6c | ||
|
|
27d906a877 | ||
|
|
431ba36a26 | ||
|
|
229c782157 | ||
|
|
5bacab7475 | ||
|
|
999286a089 | ||
|
|
68ccaf0cb3 | ||
|
|
8d58d58519 | ||
|
|
8efc0331de | ||
|
|
7c479c2479 | ||
|
|
96551856a2 | ||
|
|
b1f5cc7728 | ||
|
|
1a84661ca9 | ||
|
|
c87b9f203f | ||
|
|
9442acfb74 | ||
|
|
50bea55732 | ||
|
|
a4ece2b271 | ||
|
|
b460e4abaa | ||
|
|
087ba9ae95 | ||
|
|
5f2345852d | ||
|
|
ad1c17aa7b | ||
|
|
3a79bfd5f6 | ||
|
|
5ee8519274 | ||
|
|
ff546774e9 | ||
|
|
1f4fc9b6f0 | ||
|
|
f8142e23cd | ||
|
|
196f1654ab | ||
|
|
3b8a24eeb7 | ||
|
|
7dde15cb04 | ||
|
|
3e5d949610 | ||
|
|
25c3691f6b | ||
|
|
f3bc6c0b22 | ||
|
|
caf0d85939 | ||
|
|
a463f632e8 | ||
|
|
a0e6d09770 | ||
|
|
54623a5b06 | ||
|
|
7afff5e392 | ||
|
|
9a5fee5a4c | ||
|
|
a840e611cd | ||
|
|
566419cac4 | ||
|
|
7b362bfc76 | ||
|
|
f528dd4888 | ||
|
|
3c95c6fe11 | ||
|
|
71a72dd957 | ||
|
|
78ac1968dd | ||
|
|
93453cc8c3 | ||
|
|
11527b9033 | ||
|
|
2ff2266417 | ||
|
|
3320e6105c | ||
|
|
072e74ce49 | ||
|
|
492b1c4311 | ||
|
|
0babada459 | ||
|
|
7b0993959e | ||
|
|
701582fe38 | ||
|
|
b371676813 | ||
|
|
0cac8d66b3 | ||
|
|
3e0f5af848 | ||
|
|
245d28b03d | ||
|
|
75f4f6d0a2 | ||
|
|
f9e167cb0e | ||
|
|
f224e49de7 | ||
|
|
76ef9b292b | ||
|
|
1540cbdcaa | ||
|
|
5d129fd0da | ||
|
|
b529127461 | ||
|
|
d06ea2944e | ||
|
|
154aad1e22 | ||
|
|
17163dd909 | ||
|
|
b789a8bb05 | ||
|
|
9341ce9f84 | ||
|
|
195cbbbe42 | ||
|
|
6e5e340a25 | ||
|
|
eb74d13059 | ||
|
|
16f916c40a | ||
|
|
4dd6d4498b | ||
|
|
dd5bf546df | ||
|
|
d6debde566 | ||
|
|
efc66cc7ee | ||
|
|
b310731ba7 | ||
|
|
4bd2681bf0 | ||
|
|
fdd55511a6 | ||
|
|
0cff6ab29b | ||
|
|
cda677a30f | ||
|
|
5571651c02 | ||
|
|
2680396680 | ||
|
|
227f97c2f5 | ||
|
|
ad3231c8a3 | ||
|
|
e39d8dce3c | ||
|
|
0bdc425c55 | ||
|
|
0a7f63cc5e | ||
|
|
c6e0e9a79a | ||
|
|
7fde392774 | ||
|
|
3ee051303a | ||
|
|
9fa31be4bf | ||
|
|
ae2e4049db | ||
|
|
48b71bb11b | ||
|
|
e339ed1fb3 | ||
|
|
4517a92b2b | ||
|
|
ac902501ec | ||
|
|
363b5d04d9 | ||
|
|
dec89ae5ee | ||
|
|
5d37269a6c | ||
|
|
9a39ccd37d | ||
|
|
8c0bf0b71b | ||
|
|
5812c50a33 | ||
|
|
7b339df430 | ||
|
|
6bb13a26f5 | ||
|
|
b92137afd9 | ||
|
|
962763dc7b | ||
|
|
f4eca83a49 | ||
|
|
02135ea04f | ||
|
|
79eb838250 | ||
|
|
82710294f4 | ||
|
|
2d18acf6f7 | ||
|
|
f8c323cf5c | ||
|
|
b6f5b335bd | ||
|
|
f451f8a979 | ||
|
|
5c4dfabc48 | ||
|
|
0c0c0e6d6f | ||
|
|
20cf7c7c52 | ||
|
|
230d6137f3 | ||
|
|
aa9533eb5b | ||
|
|
efb5d4135a | ||
|
|
5f2c9c3801 | ||
|
|
1b1a686b96 | ||
|
|
e9fe5b3004 | ||
|
|
5001f48982 | ||
|
|
4eaaa2462b | ||
|
|
093e3924a2 | ||
|
|
8fd5e6521f | ||
|
|
6cdba2e8d2 | ||
|
|
4dcd4749c3 | ||
|
|
0d2b4d7ca3 | ||
|
|
42c5c02709 | ||
|
|
d1d73da322 | ||
|
|
88fcf8dbd7 | ||
|
|
396bc9b6ae | ||
|
|
cd7946f3f0 | ||
|
|
e7031d0ac1 | ||
|
|
f8dae2a3c9 |
17
.github/ISSUE_TEMPLATE.md
vendored
17
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,17 +0,0 @@
|
||||
[简述你的问题]
|
||||
|
||||
|
||||
##### 使用版本
|
||||
[请提供你使用的Jumpserver版本 1.x.x 注: 0.3.x不再提供支持]
|
||||
|
||||
##### 问题复现步骤
|
||||
1. [步骤1]
|
||||
2. [步骤2]
|
||||
|
||||
##### 具体表现[截图可能会更好些,最好能截全]
|
||||
|
||||
|
||||
##### 其他
|
||||
|
||||
|
||||
[注:] 完成后请关闭 issue
|
||||
9
.github/ISSUE_TEMPLATE/----.md
vendored
Normal file
9
.github/ISSUE_TEMPLATE/----.md
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
name: 需求建议
|
||||
about: 提出针对本项目的想法和建议
|
||||
title: "[Feature] "
|
||||
labels: 待处理, 需求
|
||||
assignees: 'ibuler'
|
||||
---
|
||||
|
||||
**请描述您的需求或者改进建议.**
|
||||
22
.github/ISSUE_TEMPLATE/bug---.md
vendored
Normal file
22
.github/ISSUE_TEMPLATE/bug---.md
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
name: Bug 提交
|
||||
about: 提交产品缺陷帮助我们更好的改进
|
||||
title: "[Bug] "
|
||||
labels: bug, 待处理
|
||||
assignees: wojiushixiaobai
|
||||
|
||||
---
|
||||
|
||||
**JumpServer 版本(v1.5.9以下不再支持)**
|
||||
|
||||
|
||||
**浏览器版本**
|
||||
|
||||
|
||||
**Bug 描述**
|
||||
|
||||
|
||||
**Bug 重现步骤(有截图更好)**
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
10
.github/ISSUE_TEMPLATE/question.md
vendored
Normal file
10
.github/ISSUE_TEMPLATE/question.md
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
name: 问题咨询
|
||||
about: 提出针对本项目安装部署、使用及其他方面的相关问题
|
||||
title: "[Question] "
|
||||
labels: 提问, 待处理
|
||||
assignees: wojiushixiaobai
|
||||
|
||||
---
|
||||
|
||||
**请描述您的问题.**
|
||||
44
.github/release-config.yml
vendored
Normal file
44
.github/release-config.yml
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
name-template: 'v$RESOLVED_VERSION'
|
||||
tag-template: 'v$RESOLVED_VERSION'
|
||||
categories:
|
||||
- title: '🌱 新功能 Features'
|
||||
labels:
|
||||
- 'feature'
|
||||
- 'enhancement'
|
||||
- 'feat'
|
||||
- '新功能'
|
||||
- title: '🚀 性能优化 Optimization'
|
||||
labels:
|
||||
- 'perf'
|
||||
- 'opt'
|
||||
- 'refactor'
|
||||
- 'Optimization'
|
||||
- '优化'
|
||||
- title: '🐛 Bug修复 Bug Fixes'
|
||||
labels:
|
||||
- 'fix'
|
||||
- 'bugfix'
|
||||
- 'bug'
|
||||
- title: '🧰 其它 Maintenance'
|
||||
labels:
|
||||
- 'chore'
|
||||
- 'docs'
|
||||
exclude-labels:
|
||||
- 'no'
|
||||
- '无需处理'
|
||||
- 'wontfix'
|
||||
change-template: '- $TITLE @$AUTHOR (#$NUMBER)'
|
||||
version-resolver:
|
||||
major:
|
||||
labels:
|
||||
- 'major'
|
||||
minor:
|
||||
labels:
|
||||
- 'minor'
|
||||
patch:
|
||||
labels:
|
||||
- 'patch'
|
||||
default: patch
|
||||
template: |
|
||||
## 版本变化 What’s Changed
|
||||
$CHANGES
|
||||
12
.github/workflows/jms-generic-action-handler.yml
vendored
Normal file
12
.github/workflows/jms-generic-action-handler.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
on: [push, pull_request, release]
|
||||
|
||||
name: JumpServer repos generic handler
|
||||
|
||||
jobs:
|
||||
generic_handler:
|
||||
name: Run generic handler
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: jumpserver/action-generic-handler@master
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PRIVATE_TOKEN }}
|
||||
46
.github/workflows/release-drafter.yml
vendored
Normal file
46
.github/workflows/release-drafter.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
on:
|
||||
push:
|
||||
# Sequence of patterns matched against refs/tags
|
||||
tags:
|
||||
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
|
||||
|
||||
name: Create Release And Upload assets
|
||||
|
||||
jobs:
|
||||
create-realese:
|
||||
name: Create Release
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Get version
|
||||
id: get_version
|
||||
run: |
|
||||
TAG=$(basename ${GITHUB_REF})
|
||||
VERSION=${TAG/v/}
|
||||
echo "::set-output name=TAG::$TAG"
|
||||
echo "::set-output name=VERSION::$VERSION"
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: release-drafter/release-drafter@v5
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
config-name: release-config.yml
|
||||
version: ${{ steps.get_version.outputs.TAG }}
|
||||
tag: ${{ steps.get_version.outputs.TAG }}
|
||||
|
||||
build-and-release:
|
||||
needs: create-realese
|
||||
name: Build and Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Build it and upload
|
||||
uses: jumpserver/action-build-upload-assets@master
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.create-realese.outputs.upload_url }}
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -35,4 +35,6 @@ docs/_build/
|
||||
xpack
|
||||
logs/*
|
||||
### Vagrant ###
|
||||
.vagrant/
|
||||
.vagrant/
|
||||
release/*
|
||||
releashe
|
||||
|
||||
33
Dockerfile
33
Dockerfile
@@ -1,19 +1,34 @@
|
||||
FROM registry.fit2cloud.com/public/python:v3
|
||||
FROM registry.fit2cloud.com/public/python:v3 as stage-build
|
||||
MAINTAINER Jumpserver Team <ibuler@qq.com>
|
||||
ARG VERSION
|
||||
ENV VERSION=$VERSION
|
||||
|
||||
WORKDIR /opt/jumpserver
|
||||
ADD . .
|
||||
RUN cd utils && bash -ixeu build.sh
|
||||
|
||||
|
||||
FROM registry.fit2cloud.com/public/python:v3
|
||||
ARG PIP_MIRROR=https://pypi.douban.com/simple
|
||||
ENV PIP_MIRROR=$PIP_MIRROR
|
||||
ARG MYSQL_MIRROR=https://mirrors.tuna.tsinghua.edu.cn/mysql/yum/mysql57-community-el6/
|
||||
ENV MYSQL_MIRROR=$MYSQL_MIRROR
|
||||
|
||||
WORKDIR /opt/jumpserver
|
||||
|
||||
COPY ./requirements ./requirements
|
||||
RUN useradd jumpserver
|
||||
|
||||
COPY ./requirements /tmp/requirements
|
||||
|
||||
RUN yum -y install epel-release && \
|
||||
echo -e "[mysql]\nname=mysql\nbaseurl=https://mirrors.tuna.tsinghua.edu.cn/mysql/yum/mysql57-community-el6/\ngpgcheck=0\nenabled=1" > /etc/yum.repos.d/mysql.repo
|
||||
RUN cd /tmp/requirements && yum -y install $(cat rpm_requirements.txt)
|
||||
RUN cd /tmp/requirements && pip install --upgrade pip setuptools && pip install wheel && \
|
||||
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt || pip install -r requirements.txt
|
||||
echo -e "[mysql]\nname=mysql\nbaseurl=${MYSQL_MIRROR}\ngpgcheck=0\nenabled=1" > /etc/yum.repos.d/mysql.repo
|
||||
RUN yum -y install $(cat requirements/rpm_requirements.txt)
|
||||
RUN pip install --upgrade pip setuptools==49.6.0 wheel -i ${PIP_MIRROR} && \
|
||||
pip config set global.index-url ${PIP_MIRROR}
|
||||
RUN pip install $(grep 'jms' requirements/requirements.txt) -i https://pypi.org/simple
|
||||
RUN pip install -r requirements/requirements.txt
|
||||
|
||||
COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/jumpserver
|
||||
RUN mkdir -p /root/.ssh/ && echo -e "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config
|
||||
|
||||
COPY . /opt/jumpserver
|
||||
RUN echo > config.yml
|
||||
VOLUME /opt/jumpserver/data
|
||||
VOLUME /opt/jumpserver/logs
|
||||
|
||||
31
README.md
31
README.md
@@ -2,6 +2,11 @@
|
||||
|
||||
[](https://www.python.org/)
|
||||
[](https://www.djangoproject.com/)
|
||||
[](https://hub.docker.com/u/jumpserver)
|
||||
|
||||
|Developer Wanted|
|
||||
|------------------|
|
||||
|JumpServer 正在寻找开发者,一起为改变世界做些贡献吧,哪怕一点点,联系我 <ibuler@fit2cloud.com> |
|
||||
|
||||
JumpServer 是全球首款开源的堡垒机,使用 GNU GPL v2.0 开源协议,是符合 4A 规范的运维安全审计系统。
|
||||
|
||||
@@ -22,6 +27,22 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
|
||||
- 云端存储: 审计录像云端存储,永不丢失;
|
||||
- 多租户: 一套系统,多个子公司和部门同时使用。
|
||||
|
||||
## 版本说明
|
||||
|
||||
自 v2.0.0 发布后, JumpServer 版本号命名将变更为:v大版本.功能版本.Bug修复版本。比如:
|
||||
|
||||
```
|
||||
v2.0.1 是 v2.0.0 之后的Bug修复版本;
|
||||
v2.1.0 是 v2.0.0 之后的功能版本。
|
||||
```
|
||||
|
||||
像其它优秀开源项目一样,JumpServer 每个月会发布一个功能版本,并同时维护 3 个功能版本。比如:
|
||||
|
||||
```
|
||||
在 v2.4 发布前,我们会同时维护 v2.1、v2.2、v2.3;
|
||||
在 v2.4 发布后,我们会同时维护 v2.2、v2.3、v2.4;v2.1 会停止维护。
|
||||
```
|
||||
|
||||
## 功能列表
|
||||
|
||||
<table>
|
||||
@@ -185,6 +206,16 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
|
||||
- [完整文档](https://docs.jumpserver.org)
|
||||
- [演示视频](https://jumpserver.oss-cn-hangzhou.aliyuncs.com/jms-media/%E3%80%90%E6%BC%94%E7%A4%BA%E8%A7%86%E9%A2%91%E3%80%91Jumpserver%20%E5%A0%A1%E5%9E%92%E6%9C%BA%20V1.5.0%20%E6%BC%94%E7%A4%BA%E8%A7%86%E9%A2%91%20-%20final.mp4)
|
||||
|
||||
## 组件项目
|
||||
- [Lina](https://github.com/jumpserver/lina) JumpServer Web UI 项目
|
||||
- [Luna](https://github.com/jumpserver/luna) JumpServer Web Terminal 项目
|
||||
- [Koko](https://github.com/jumpserver/koko) JumpServer 字符协议 Connector 项目,替代原来 Python 版本的 [Coco](https://github.com/jumpserver/coco)
|
||||
- [Guacamole](https://github.com/jumpserver/docker-guacamole) JumpServer 图形协议 Connector 项目,依赖 [Apache Guacamole](https://guacamole.apache.org/)
|
||||
|
||||
## JumpServer 企业版
|
||||
- [申请企业版试用](https://jinshuju.net/f/kyOYpi)
|
||||
> 注:企业版支持离线安装,申请通过后会提供高速下载链接。
|
||||
|
||||
## 案例研究
|
||||
|
||||
- [JumpServer 堡垒机护航顺丰科技超大规模资产安全运维](https://blog.fit2cloud.com/?p=1147);
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
from .remote_app import *
|
||||
from .database_app import *
|
||||
from .k8s_app import *
|
||||
|
||||
20
apps/applications/api/k8s_app.py
Normal file
20
apps/applications/api/k8s_app.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# coding: utf-8
|
||||
#
|
||||
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
|
||||
from .. import models
|
||||
from .. import serializers
|
||||
from ..hands import IsOrgAdminOrAppUser
|
||||
|
||||
__all__ = [
|
||||
'K8sAppViewSet',
|
||||
]
|
||||
|
||||
|
||||
class K8sAppViewSet(OrgBulkModelViewSet):
|
||||
model = models.K8sApp
|
||||
filter_fields = ('name',)
|
||||
search_fields = filter_fields
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = serializers.K8sAppSerializer
|
||||
@@ -15,7 +15,7 @@ __all__ = [
|
||||
|
||||
class RemoteAppViewSet(OrgBulkModelViewSet):
|
||||
model = RemoteApp
|
||||
filter_fields = ('name',)
|
||||
filter_fields = ('name', 'type', 'comment')
|
||||
search_fields = filter_fields
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = RemoteAppSerializer
|
||||
|
||||
@@ -23,6 +23,7 @@ REMOTE_APP_TYPE_CHROME_FIELDS = [
|
||||
REMOTE_APP_TYPE_MYSQL_WORKBENCH_FIELDS = [
|
||||
{'name': 'mysql_workbench_ip'},
|
||||
{'name': 'mysql_workbench_name'},
|
||||
{'name': 'mysql_workbench_port'},
|
||||
{'name': 'mysql_workbench_username'},
|
||||
{'name': 'mysql_workbench_password', 'write_only': True}
|
||||
]
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
from .remote_app import *
|
||||
from .database_app import *
|
||||
@@ -1,26 +0,0 @@
|
||||
# coding: utf-8
|
||||
#
|
||||
|
||||
|
||||
from django import forms
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from .. import models
|
||||
|
||||
__all__ = ['DatabaseAppMySQLForm']
|
||||
|
||||
|
||||
class BaseDatabaseAppForm(forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['type'].widget.attrs['disabled'] = True
|
||||
|
||||
class Meta:
|
||||
model = models.DatabaseApp
|
||||
fields = [
|
||||
'name', 'type', 'host', 'port', 'database', 'comment'
|
||||
]
|
||||
|
||||
|
||||
class DatabaseAppMySQLForm(BaseDatabaseAppForm):
|
||||
pass
|
||||
@@ -1,120 +0,0 @@
|
||||
# coding: utf-8
|
||||
#
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
from django import forms
|
||||
|
||||
from orgs.mixins.forms import OrgModelForm
|
||||
|
||||
from ..models import RemoteApp
|
||||
|
||||
|
||||
__all__ = [
|
||||
'RemoteAppChromeForm', 'RemoteAppMySQLWorkbenchForm',
|
||||
'RemoteAppVMwareForm', 'RemoteAppCustomForm'
|
||||
]
|
||||
|
||||
|
||||
class BaseRemoteAppForm(OrgModelForm):
|
||||
default_initial_data = {}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# 过滤RDP资产和系统用户
|
||||
super().__init__(*args, **kwargs)
|
||||
field_asset = self.fields['asset']
|
||||
field_asset.queryset = field_asset.queryset.has_protocol('rdp')
|
||||
self.fields['type'].widget.attrs['disabled'] = True
|
||||
self.fields.move_to_end('comment')
|
||||
self.initial_default()
|
||||
|
||||
def initial_default(self):
|
||||
for name, value in self.default_initial_data.items():
|
||||
field = self.fields.get(name)
|
||||
if not field:
|
||||
continue
|
||||
field.initial = value
|
||||
|
||||
class Meta:
|
||||
model = RemoteApp
|
||||
fields = [
|
||||
'name', 'asset', 'type', 'path', 'comment'
|
||||
]
|
||||
widgets = {
|
||||
'asset': forms.Select(attrs={
|
||||
'class': 'select2', 'data-placeholder': _('Asset')
|
||||
}),
|
||||
}
|
||||
|
||||
|
||||
class RemoteAppChromeForm(BaseRemoteAppForm):
|
||||
default_initial_data = {
|
||||
'path': r'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe'
|
||||
}
|
||||
|
||||
chrome_target = forms.CharField(
|
||||
max_length=128, label=_('Target URL'), required=False
|
||||
)
|
||||
chrome_username = forms.CharField(
|
||||
max_length=128, label=_('Login username'), required=False
|
||||
)
|
||||
chrome_password = forms.CharField(
|
||||
widget=forms.PasswordInput, strip=True,
|
||||
max_length=128, label=_('Login password'), required=False
|
||||
)
|
||||
|
||||
|
||||
class RemoteAppMySQLWorkbenchForm(BaseRemoteAppForm):
|
||||
default_initial_data = {
|
||||
'path': r'C:\Program Files\MySQL\MySQL Workbench 8.0 CE'
|
||||
r'\MySQLWorkbench.exe'
|
||||
}
|
||||
|
||||
mysql_workbench_ip = forms.CharField(
|
||||
max_length=128, label=_('Database IP'), required=False
|
||||
)
|
||||
mysql_workbench_name = forms.CharField(
|
||||
max_length=128, label=_('Database name'), required=False
|
||||
)
|
||||
mysql_workbench_username = forms.CharField(
|
||||
max_length=128, label=_('Database username'), required=False
|
||||
)
|
||||
mysql_workbench_password = forms.CharField(
|
||||
widget=forms.PasswordInput, strip=True,
|
||||
max_length=128, label=_('Database password'), required=False
|
||||
)
|
||||
|
||||
|
||||
class RemoteAppVMwareForm(BaseRemoteAppForm):
|
||||
default_initial_data = {
|
||||
'path': r'C:\Program Files (x86)\VMware\Infrastructure'
|
||||
r'\Virtual Infrastructure Client\Launcher\VpxClient.exe'
|
||||
}
|
||||
|
||||
vmware_target = forms.CharField(
|
||||
max_length=128, label=_('Target address'), required=False
|
||||
)
|
||||
vmware_username = forms.CharField(
|
||||
max_length=128, label=_('Login username'), required=False
|
||||
)
|
||||
vmware_password = forms.CharField(
|
||||
widget=forms.PasswordInput, strip=True,
|
||||
max_length=128, label=_('Login password'), required=False
|
||||
)
|
||||
|
||||
|
||||
class RemoteAppCustomForm(BaseRemoteAppForm):
|
||||
|
||||
custom_cmdline = forms.CharField(
|
||||
max_length=128, label=_('Operating parameter'), required=False
|
||||
)
|
||||
custom_target = forms.CharField(
|
||||
max_length=128, label=_('Target address'), required=False
|
||||
)
|
||||
custom_username = forms.CharField(
|
||||
max_length=128, label=_('Login username'), required=False
|
||||
)
|
||||
custom_password = forms.CharField(
|
||||
widget=forms.PasswordInput, strip=True,
|
||||
max_length=128, label=_('Login password'), required=False
|
||||
)
|
||||
|
||||
34
apps/applications/migrations/0005_k8sapp.py
Normal file
34
apps/applications/migrations/0005_k8sapp.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# Generated by Django 2.2.13 on 2020-08-07 07:13
|
||||
|
||||
from django.db import migrations, models
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('applications', '0004_auto_20191218_1705'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='K8sApp',
|
||||
fields=[
|
||||
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
|
||||
('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by')),
|
||||
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
|
||||
('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')),
|
||||
('name', models.CharField(max_length=128, verbose_name='Name')),
|
||||
('type', models.CharField(choices=[('k8s', 'Kubernetes')], default='k8s', max_length=128, verbose_name='Type')),
|
||||
('cluster', models.CharField(max_length=1024, verbose_name='Cluster')),
|
||||
('comment', models.TextField(blank=True, default='', max_length=128, verbose_name='Comment')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'KubernetesApp',
|
||||
'ordering': ('name',),
|
||||
'unique_together': {('org_id', 'name')},
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -1,2 +1,3 @@
|
||||
from .remote_app import *
|
||||
from .database_app import *
|
||||
from .k8s_app import *
|
||||
|
||||
27
apps/applications/models/k8s_app.py
Normal file
27
apps/applications/models/k8s_app.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from common.db import models
|
||||
from orgs.mixins.models import OrgModelMixin
|
||||
|
||||
|
||||
class K8sApp(OrgModelMixin, models.JMSModel):
|
||||
class TYPE(models.ChoiceSet):
|
||||
K8S = 'k8s', _('Kubernetes')
|
||||
|
||||
name = models.CharField(max_length=128, verbose_name=_('Name'))
|
||||
type = models.CharField(
|
||||
default=TYPE.K8S, choices=TYPE.choices,
|
||||
max_length=128, verbose_name=_('Type')
|
||||
)
|
||||
cluster = models.CharField(max_length=1024, verbose_name=_('Cluster'))
|
||||
comment = models.TextField(
|
||||
max_length=128, default='', blank=True, verbose_name=_('Comment')
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
unique_together = [('org_id', 'name'), ]
|
||||
verbose_name = _('KubernetesApp')
|
||||
ordering = ('name', )
|
||||
@@ -1,2 +1,3 @@
|
||||
from .remote_app import *
|
||||
from .database_app import *
|
||||
from .k8s_app import *
|
||||
|
||||
22
apps/applications/serializers/k8s_app.py
Normal file
22
apps/applications/serializers/k8s_app.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from .. import models
|
||||
|
||||
__all__ = [
|
||||
'K8sAppSerializer',
|
||||
]
|
||||
|
||||
|
||||
class K8sAppSerializer(BulkOrgResourceModelSerializer):
|
||||
type_display = serializers.CharField(source='get_type_display', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.K8sApp
|
||||
fields = [
|
||||
'id', 'name', 'type', 'type_display', 'comment', 'created_by',
|
||||
'date_created', 'date_updated', 'cluster'
|
||||
]
|
||||
read_only_fields = [
|
||||
'id', 'created_by', 'date_created', 'date_updated',
|
||||
]
|
||||
@@ -1,55 +0,0 @@
|
||||
{% extends '_base_create_update.html' %}
|
||||
{% load static %}
|
||||
{% load bootstrap3 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block form %}
|
||||
<form id="DatabaseAppForm" method="post" class="form-horizontal">
|
||||
{% bootstrap_form form layout="horizontal" %}
|
||||
<div class="hr-line-dashed"></div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-4 col-sm-offset-2">
|
||||
<button class="btn btn-default" type="reset"> {% trans 'Reset' %}</button>
|
||||
<button id="submit_button" class="btn btn-primary" type="submit">{% trans 'Submit' %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block custom_foot_js %}
|
||||
<script type="text/javascript">
|
||||
var app_type_id = '#' + '{{ form.type.id_for_label }}';
|
||||
|
||||
function getFormDataType(){
|
||||
return $(app_type_id+ " option:selected").val();
|
||||
}
|
||||
function getFormData(form){
|
||||
var data = form.serializeObject();
|
||||
data['type'] = getFormDataType();
|
||||
return data
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
})
|
||||
.on("submit", "form", function (evt) {
|
||||
evt.preventDefault();
|
||||
var the_url = '{% url "api-applications:database-app-list" %}';
|
||||
var redirect_to = '{% url "applications:database-app-list" %}';
|
||||
var method = "POST";
|
||||
{% if api_action == "update" %}
|
||||
the_url = '{% url "api-applications:database-app-detail" object.id %}';
|
||||
method = "PUT";
|
||||
{% endif %}
|
||||
var form = $("form");
|
||||
var data = getFormData(form);
|
||||
var props = {
|
||||
url: the_url,
|
||||
data: data,
|
||||
method: method,
|
||||
form: form,
|
||||
redirect_to: redirect_to
|
||||
};
|
||||
formSubmit(props);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,103 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="wrapper wrapper-content animated fadeInRight">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="panel-options">
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="active">
|
||||
<a href="{% url 'applications:database-app-detail' pk=database_app.id %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Detail' %} </a>
|
||||
</li>
|
||||
<li class="pull-right">
|
||||
<a class="btn btn-outline btn-default" href="{% url 'applications:database-app-update' pk=database_app.id %}"><i class="fa fa-edit"></i>{% trans 'Update' %}</a>
|
||||
</li>
|
||||
<li class="pull-right">
|
||||
<a class="btn btn-outline btn-danger btn-delete-application">
|
||||
<i class="fa fa-trash-o"></i>{% trans 'Delete' %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
<div class="col-sm-8" style="padding-left: 0;">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="ibox-title">
|
||||
<span class="label"><b>{{ database_app.name }}</b></span>
|
||||
<div class="ibox-tools">
|
||||
<a class="collapse-link">
|
||||
<i class="fa fa-chevron-up"></i>
|
||||
</a>
|
||||
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
|
||||
<i class="fa fa-wrench"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-user">
|
||||
</ul>
|
||||
<a class="close-link">
|
||||
<i class="fa fa-times"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ibox-content">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr class="no-borders-tr">
|
||||
<td>{% trans 'Name' %}:</td>
|
||||
<td><b>{{ database_app.name }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Type' %}:</td>
|
||||
<td><b>{{ database_app.get_type_display }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Host' %}:</td>
|
||||
<td><b>{{ database_app.host }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Port' %}:</td>
|
||||
<td><b>{{ database_app.port }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Database' %}:</td>
|
||||
<td><b>{{ database_app.database }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Date created' %}:</td>
|
||||
<td><b>{{ database_app.date_created }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Created by' %}:</td>
|
||||
<td><b>{{ database_app.created_by }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Comment' %}:</td>
|
||||
<td><b>{{ database_app.comment }}</b></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
})
|
||||
.on('click', '.btn-delete-application', function () {
|
||||
var $this = $(this);
|
||||
var name = "{{ database_app.name }}";
|
||||
var rid = "{{ database_app.id }}";
|
||||
var the_url = '{% url "api-applications:database-app-detail" pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', rid);
|
||||
var redirect_url = "{% url 'applications:database-app-list' %}";
|
||||
objectDelete($this, name, the_url, redirect_url);
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,88 +0,0 @@
|
||||
{% extends '_base_list.html' %}
|
||||
{% load i18n static %}
|
||||
{% block help_message %}
|
||||
{% endblock %}
|
||||
{% block table_search %}{% endblock %}
|
||||
{% block table_container %}
|
||||
<div class="btn-group uc pull-left m-r-5">
|
||||
<button data-toggle="dropdown" class="btn btn-primary btn-sm dropdown-toggle">
|
||||
{% trans "Create DatabaseApp" %}
|
||||
<span class="caret"></span></button>
|
||||
<ul class="dropdown-menu">
|
||||
{% for key, value in type_choices %}
|
||||
<li><a class="" href="{% url 'applications:database-app-create' %}?type={{ key }}">{{ value }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<table class="table table-striped table-bordered table-hover " id="database_app_list_table" >
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center">
|
||||
<input type="checkbox" id="check_all" class="ipt_check_all" >
|
||||
</th>
|
||||
<th class="text-center">{% trans 'Name' %}</th>
|
||||
<th class="text-center">{% trans 'Type' %}</th>
|
||||
<th class="text-center">{% trans 'Host' %}</th>
|
||||
<th class="text-center">{% trans 'Port' %}</th>
|
||||
<th class="text-center">{% trans 'Database' %}</th>
|
||||
<th class="text-center">{% trans 'Comment' %}</th>
|
||||
<th class="text-center">{% trans 'Action' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
||||
{% block content_bottom_left %}{% endblock %}
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
function initTable() {
|
||||
var options = {
|
||||
ele: $('#database_app_list_table'),
|
||||
columnDefs: [
|
||||
{targets: 1, createdCell: function (td, cellData, rowData) {
|
||||
cellData = htmlEscape(cellData);
|
||||
{% url 'applications:database-app-detail' pk=DEFAULT_PK as the_url %}
|
||||
var detail_btn = '<a href="{{ the_url }}">' + cellData + '</a>';
|
||||
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
|
||||
}},
|
||||
{targets: 2, createdCell: function (td, cellData, rowData) {
|
||||
$(td).html(rowData.get_type_display)
|
||||
}},
|
||||
{targets: 7, createdCell: function (td, cellData, rowData) {
|
||||
var update_btn = '<a href="{% url "applications:database-app-update" pk=DEFAULT_PK %}" class="btn btn-xs btn-info">{% trans "Update" %}</a>'.replace("{{ DEFAULT_PK }}", cellData);
|
||||
var del_btn = '<a class="btn btn-xs btn-danger m-l-xs btn-delete" data-rid="{{ DEFAULT_PK }}">{% trans "Delete" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
|
||||
$(td).html(update_btn + del_btn)
|
||||
}}
|
||||
],
|
||||
ajax_url: '{% url "api-applications:database-app-list" %}',
|
||||
columns: [
|
||||
{data: "id"},
|
||||
{data: "name" },
|
||||
{data: "type"},
|
||||
{data: "host"},
|
||||
{data: "port"},
|
||||
{data: "database"},
|
||||
{data: "comment"},
|
||||
{data: "id", orderable: false, width: "120px"}
|
||||
],
|
||||
op_html: $('#actions').html()
|
||||
};
|
||||
jumpserver.initServerSideDataTable(options);
|
||||
}
|
||||
$(document).ready(function(){
|
||||
initTable();
|
||||
})
|
||||
.on('click', '.btn-delete', function () {
|
||||
var $this = $(this);
|
||||
var $data_table = $('#database_app_list_table').DataTable();
|
||||
var name = $(this).closest("tr").find(":nth-child(2)").children('a').html();
|
||||
var rid = $this.data('rid');
|
||||
var the_url = '{% url "api-applications:database-app-detail" pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', rid);
|
||||
objectDelete($this, name, the_url);
|
||||
setTimeout( function () {
|
||||
$data_table.ajax.reload();
|
||||
}, 3000);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,71 +0,0 @@
|
||||
{% extends '_base_create_update.html' %}
|
||||
{% load static %}
|
||||
{% load bootstrap3 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block form %}
|
||||
<form id="RemoteAppForm" method="post" class="form-horizontal">
|
||||
{% bootstrap_form form layout="horizontal" %}
|
||||
<div class="hr-line-dashed"></div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-4 col-sm-offset-2">
|
||||
<button class="btn btn-default" type="reset"> {% trans 'Reset' %}</button>
|
||||
|
||||
<button id="submit_button" class="btn btn-primary" type="submit">{% trans 'Submit' %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block custom_foot_js %}
|
||||
<script type="text/javascript">
|
||||
var app_type_id = '#' + '{{ form.type.id_for_label }}';
|
||||
|
||||
function getFormDataType(){
|
||||
return $(app_type_id+ " option:selected").val();
|
||||
}
|
||||
function constructFormDataParams(data){
|
||||
var params = {};
|
||||
var type =data.type;
|
||||
for (var k in data){
|
||||
if (k.startsWith(type)){
|
||||
params[k] = data[k];
|
||||
delete data[k]
|
||||
}
|
||||
}
|
||||
return params
|
||||
}
|
||||
function getFormData(form){
|
||||
var data = form.serializeObject();
|
||||
data['type'] = getFormDataType();
|
||||
data['params'] = constructFormDataParams(data);
|
||||
return data
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
$('.select2').select2({
|
||||
closeOnSelect: true
|
||||
});
|
||||
}).on("submit", "form", function (evt) {
|
||||
evt.preventDefault();
|
||||
var the_url = '{% url "api-applications:remote-app-list" %}';
|
||||
var redirect_to = '{% url "applications:remote-app-list" %}';
|
||||
var method = "POST";
|
||||
{% if api_action == "update" %}
|
||||
the_url = '{% url "api-applications:remote-app-detail" object.id %}';
|
||||
method = "PUT";
|
||||
{% endif %}
|
||||
var form = $("form");
|
||||
var data = getFormData(form);
|
||||
var props = {
|
||||
url: the_url,
|
||||
data: data,
|
||||
method: method,
|
||||
form: form,
|
||||
redirect_to: redirect_to
|
||||
};
|
||||
formSubmit(props);
|
||||
})
|
||||
;
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,100 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="wrapper wrapper-content animated fadeInRight">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="panel-options">
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="active">
|
||||
<a href="{% url 'applications:remote-app-detail' pk=remote_app.id %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Detail' %} </a>
|
||||
</li>
|
||||
<li class="pull-right">
|
||||
<a class="btn btn-outline btn-default" href="{% url 'applications:remote-app-update' pk=remote_app.id %}"><i class="fa fa-edit"></i>{% trans 'Update' %}</a>
|
||||
</li>
|
||||
<li class="pull-right">
|
||||
<a class="btn btn-outline btn-danger btn-delete-application">
|
||||
<i class="fa fa-trash-o"></i>{% trans 'Delete' %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
<div class="col-sm-8" style="padding-left: 0;">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="ibox-title">
|
||||
<span class="label"><b>{{ remote_app.name }}</b></span>
|
||||
<div class="ibox-tools">
|
||||
<a class="collapse-link">
|
||||
<i class="fa fa-chevron-up"></i>
|
||||
</a>
|
||||
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
|
||||
<i class="fa fa-wrench"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-user">
|
||||
</ul>
|
||||
<a class="close-link">
|
||||
<i class="fa fa-times"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ibox-content">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr class="no-borders-tr">
|
||||
<td>{% trans 'Name' %}:</td>
|
||||
<td><b>{{ remote_app.name }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Asset' %}:</td>
|
||||
<td><b><a href="{% url 'assets:asset-detail' pk=remote_app.asset.id %}">{{ remote_app.asset.hostname }}</a></b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'App type' %}:</td>
|
||||
<td><b>{{ remote_app.get_type_display }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'App path' %}:</td>
|
||||
<td><b>{{ remote_app.path }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Date created' %}:</td>
|
||||
<td><b>{{ remote_app.date_created }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Created by' %}:</td>
|
||||
<td><b>{{ remote_app.created_by }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Comment' %}:</td>
|
||||
<td><b>{{ remote_app.comment }}</b></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
jumpserver.nodes_selected = {};
|
||||
$(document).ready(function () {
|
||||
})
|
||||
.on('click', '.btn-delete-application', function () {
|
||||
var $this = $(this);
|
||||
var name = "{{ remote_app.name }}";
|
||||
var rid = "{{ remote_app.id }}";
|
||||
var the_url = '{% url "api-applications:remote-app-detail" pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', rid);
|
||||
var redirect_url = "{% url 'applications:remote-app-list' %}";
|
||||
objectDelete($this, name, the_url, redirect_url);
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,92 +0,0 @@
|
||||
{% extends '_base_list.html' %}
|
||||
{% load i18n static %}
|
||||
{% block help_message %}
|
||||
{% trans 'Before using this feature, make sure that the application loader has been uploaded to the application server and successfully published as a RemoteApp application' %}
|
||||
<b><a href="https://github.com/jumpserver/Jmservisor/releases" target="view_window" >{% trans 'Download application loader' %}</a></b>
|
||||
{% endblock %}
|
||||
{% block table_search %}{% endblock %}
|
||||
{% block table_container %}
|
||||
<div class="btn-group uc pull-left m-r-5">
|
||||
<button data-toggle="dropdown" class="btn btn-primary btn-sm dropdown-toggle">
|
||||
{% trans "Create RemoteApp" %}
|
||||
<span class="caret"></span></button>
|
||||
<ul class="dropdown-menu">
|
||||
{% for key, value in type_choices %}
|
||||
<li><a class="" href="{% url 'applications:remote-app-create' %}?type={{ key }}">{{ value }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<table class="table table-striped table-bordered table-hover " id="remote_app_list_table" >
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center">
|
||||
<input type="checkbox" id="check_all" class="ipt_check_all" >
|
||||
</th>
|
||||
<th class="text-center">{% trans 'Name' %}</th>
|
||||
<th class="text-center">{% trans 'App type' %}</th>
|
||||
<th class="text-center">{% trans 'Asset' %}</th>
|
||||
<th class="text-center">{% trans 'Comment' %}</th>
|
||||
<th class="text-center">{% trans 'Action' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
||||
{% block content_bottom_left %}{% endblock %}
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
function initTable() {
|
||||
var options = {
|
||||
ele: $('#remote_app_list_table'),
|
||||
columnDefs: [
|
||||
{targets: 1, createdCell: function (td, cellData, rowData) {
|
||||
cellData = htmlEscape(cellData);
|
||||
{% url 'applications:remote-app-detail' pk=DEFAULT_PK as the_url %}
|
||||
var detail_btn = '<a href="{{ the_url }}">' + cellData + '</a>';
|
||||
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
|
||||
}},
|
||||
{targets: 3, createdCell: function (td, cellData, rowData) {
|
||||
var hostname = htmlEscape(cellData.hostname);
|
||||
var detail_btn = '<a href="{% url 'assets:asset-detail' pk=DEFAULT_PK %}">' + hostname + '</a>';
|
||||
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', cellData.id));
|
||||
}},
|
||||
{targets: 3, createdCell: function (td, cellData, rowData) {
|
||||
var comment = htmlEscape(cellData);
|
||||
$(td).html(comment)
|
||||
}},
|
||||
{targets: 5, createdCell: function (td, cellData, rowData) {
|
||||
var update_btn = '<a href="{% url "applications:remote-app-update" pk=DEFAULT_PK %}" class="btn btn-xs btn-info">{% trans "Update" %}</a>'.replace("{{ DEFAULT_PK }}", cellData);
|
||||
var del_btn = '<a class="btn btn-xs btn-danger m-l-xs btn-delete" data-rid="{{ DEFAULT_PK }}">{% trans "Delete" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
|
||||
$(td).html(update_btn + del_btn)
|
||||
}}
|
||||
],
|
||||
ajax_url: '{% url "api-applications:remote-app-list" %}',
|
||||
columns: [
|
||||
{data: "id"},
|
||||
{data: "name" },
|
||||
{data: "get_type_display", orderable: false},
|
||||
{data: "asset_info", orderable: false},
|
||||
{data: "comment"},
|
||||
{data: "id", orderable: false, width: "120px"}
|
||||
],
|
||||
op_html: $('#actions').html()
|
||||
};
|
||||
jumpserver.initServerSideDataTable(options);
|
||||
}
|
||||
$(document).ready(function(){
|
||||
initTable();
|
||||
})
|
||||
.on('click', '.btn-delete', function () {
|
||||
var $this = $(this);
|
||||
var $data_table = $('#remote_app_list_table').DataTable();
|
||||
var name = $(this).closest("tr").find(":nth-child(2)").children('a').html();
|
||||
var rid = $this.data('rid');
|
||||
var the_url = '{% url "api-applications:remote-app-detail" pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', rid);
|
||||
objectDelete($this, name, the_url);
|
||||
setTimeout( function () {
|
||||
$data_table.ajax.reload();
|
||||
}, 3000);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,83 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n static %}
|
||||
|
||||
{% block custom_head_css_js %}
|
||||
<script src="{% static 'js/jquery.form.min.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="mail-box-header">
|
||||
<table class="table table-striped table-bordered table-hover " id="database_app_list_table" >
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center">
|
||||
<input type="checkbox" id="check_all" class="ipt_check_all" >
|
||||
</th>
|
||||
<th class="text-center">{% trans 'Name' %}</th>
|
||||
<th class="text-center">{% trans 'Type' %}</th>
|
||||
<th class="text-center">{% trans 'Host' %}</th>
|
||||
<th class="text-center">{% trans 'Database' %}</th>
|
||||
<th class="text-center">{% trans 'Comment' %}</th>
|
||||
<th class="text-center">{% trans 'Action' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
var inited = false;
|
||||
var database_app_table, url;
|
||||
|
||||
function initTable() {
|
||||
if (inited){
|
||||
return
|
||||
} else {
|
||||
inited = true;
|
||||
}
|
||||
url = '{% url "api-perms:my-database-apps" %}';
|
||||
var options = {
|
||||
ele: $('#database_app_list_table'),
|
||||
columnDefs: [
|
||||
{targets: 1, createdCell: function (td, cellData, rowData) {
|
||||
var name = htmlEscape(cellData);
|
||||
$(td).html(name)
|
||||
}},
|
||||
{targets: 2, createdCell: function (td, cellData, rowData) {
|
||||
var type = htmlEscape(rowData.get_type_display);
|
||||
$(td).html(type);
|
||||
}},
|
||||
{targets: 3, createdCell: function (td, cellData, rowData) {
|
||||
var host = htmlEscape(cellData);
|
||||
$(td).html(host);
|
||||
}},
|
||||
{targets: 4, createdCell: function (td, cellData, rowData) {
|
||||
var database = htmlEscape(cellData);
|
||||
$(td).html(database);
|
||||
}},
|
||||
{targets: 6, createdCell: function (td, cellData, rowData) {
|
||||
var conn_btn = '<a href="{% url "luna-view" %}?type=database_app&login_to=' + cellData +'" class="btn btn-xs btn-primary" target="_blank">{% trans "Connect" %}</a>';
|
||||
$(td).html(conn_btn)
|
||||
}}
|
||||
],
|
||||
ajax_url: url,
|
||||
columns: [
|
||||
{data: "id"},
|
||||
{data: "name"},
|
||||
{data: "type"},
|
||||
{data: "host"},
|
||||
{data: "database"},
|
||||
{data: "comment", orderable: false},
|
||||
{data: "id", orderable: false}
|
||||
]
|
||||
};
|
||||
database_app_table = jumpserver.initServerSideDataTable(options);
|
||||
return database_app_table
|
||||
}
|
||||
$(document).ready(function(){
|
||||
initTable();
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,73 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n static %}
|
||||
|
||||
{% block custom_head_css_js %}
|
||||
<script src="{% static 'js/jquery.form.min.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="mail-box-header">
|
||||
<table class="table table-striped table-bordered table-hover " id="remote_app_list_table" >
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center">
|
||||
<input type="checkbox" id="check_all" class="ipt_check_all" >
|
||||
</th>
|
||||
<th class="text-center">{% trans 'Name' %}</th>
|
||||
<th class="text-center">{% trans 'App type' %}</th>
|
||||
<th class="text-center">{% trans 'Asset' %}</th>
|
||||
<th class="text-center">{% trans 'Comment' %}</th>
|
||||
<th class="text-center">{% trans 'Action' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
var inited = false;
|
||||
var remote_app_table, url;
|
||||
|
||||
function initTable() {
|
||||
if (inited){
|
||||
return
|
||||
} else {
|
||||
inited = true;
|
||||
}
|
||||
url = '{% url "api-perms:my-remote-apps" %}';
|
||||
var options = {
|
||||
ele: $('#remote_app_list_table'),
|
||||
columnDefs: [
|
||||
{targets: 1, createdCell: function (td, cellData, rowData) {
|
||||
var name = htmlEscape(cellData);
|
||||
$(td).html(name)
|
||||
}},
|
||||
{targets: 3, createdCell: function (td, cellData, rowData) {
|
||||
var hostname = htmlEscape(cellData.hostname);
|
||||
$(td).html(hostname);
|
||||
}},
|
||||
{targets: 5, createdCell: function (td, cellData, rowData) {
|
||||
var conn_btn = '<a href="{% url "luna-view" %}?type=remote_app&login_to=' + cellData +'" class="btn btn-xs btn-primary" target="_blank">{% trans "Connect" %}</a>'.replace("{{ DEFAULT_PK }}", cellData);
|
||||
$(td).html(conn_btn)
|
||||
}}
|
||||
],
|
||||
ajax_url: url,
|
||||
columns: [
|
||||
{data: "id"},
|
||||
{data: "name"},
|
||||
{data: "get_type_display", orderable: false},
|
||||
{data: "asset_info", orderable: false},
|
||||
{data: "comment", orderable: false},
|
||||
{data: "id", orderable: false}
|
||||
]
|
||||
};
|
||||
remote_app_table = jumpserver.initServerSideDataTable(options);
|
||||
return remote_app_table
|
||||
}
|
||||
$(document).ready(function(){
|
||||
initTable();
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -12,6 +12,7 @@ app_name = 'applications'
|
||||
router = BulkRouter()
|
||||
router.register(r'remote-apps', api.RemoteAppViewSet, 'remote-app')
|
||||
router.register(r'database-apps', api.DatabaseAppViewSet, 'database-app')
|
||||
router.register(r'k8s-apps', api.K8sAppViewSet, 'k8s-app')
|
||||
|
||||
urlpatterns = [
|
||||
path('remote-apps/<uuid:pk>/connection-info/', api.RemoteAppConnectionInfoApi.as_view(), name='remote-app-connection-info'),
|
||||
|
||||
@@ -1,23 +1,7 @@
|
||||
# coding:utf-8
|
||||
from django.urls import path
|
||||
from .. import views
|
||||
|
||||
app_name = 'applications'
|
||||
|
||||
urlpatterns = [
|
||||
# RemoteApp
|
||||
path('remote-app/', views.RemoteAppListView.as_view(), name='remote-app-list'),
|
||||
path('remote-app/create/', views.RemoteAppCreateView.as_view(), name='remote-app-create'),
|
||||
path('remote-app/<uuid:pk>/update/', views.RemoteAppUpdateView.as_view(), name='remote-app-update'),
|
||||
path('remote-app/<uuid:pk>/', views.RemoteAppDetailView.as_view(), name='remote-app-detail'),
|
||||
# User RemoteApp view
|
||||
path('user-remote-app/', views.UserRemoteAppListView.as_view(), name='user-remote-app-list'),
|
||||
|
||||
path('database-app/', views.DatabaseAppListView.as_view(), name='database-app-list'),
|
||||
path('database-app/create/', views.DatabaseAppCreateView.as_view(), name='database-app-create'),
|
||||
path('database-app/<uuid:pk>/update/', views.DatabaseAppUpdateView.as_view(), name='database-app-update'),
|
||||
path('database-app/<uuid:pk>/', views.DatabaseAppDetailView.as_view(), name='database-app-detail'),
|
||||
# User DatabaseApp view
|
||||
path('user-database-app/', views.UserDatabaseAppListView.as_view(), name='user-database-app-list'),
|
||||
|
||||
]
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
from .remote_app import *
|
||||
from .database_app import *
|
||||
@@ -1,115 +0,0 @@
|
||||
# coding: utf-8
|
||||
#
|
||||
|
||||
from django.http import Http404
|
||||
from django.views.generic import TemplateView
|
||||
from django.views.generic.edit import CreateView, UpdateView
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.views.generic.detail import DetailView
|
||||
|
||||
from common.permissions import PermissionsMixin, IsOrgAdmin, IsValidUser
|
||||
|
||||
from .. import models, const, forms
|
||||
|
||||
__all__ = [
|
||||
'DatabaseAppListView', 'DatabaseAppCreateView', 'DatabaseAppUpdateView',
|
||||
'DatabaseAppDetailView', 'UserDatabaseAppListView',
|
||||
]
|
||||
|
||||
|
||||
class DatabaseAppListView(PermissionsMixin, TemplateView):
|
||||
template_name = 'applications/database_app_list.html'
|
||||
permission_classes = [IsOrgAdmin]
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'app': _("Application"),
|
||||
'action': _('DatabaseApp list'),
|
||||
'type_choices': const.DATABASE_APP_TYPE_CHOICES
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class BaseDatabaseAppCreateUpdateView:
|
||||
template_name = 'applications/database_app_create_update.html'
|
||||
model = models.DatabaseApp
|
||||
permission_classes = [IsOrgAdmin]
|
||||
default_type = const.DATABASE_APP_TYPE_MYSQL
|
||||
form_class = forms.DatabaseAppMySQLForm
|
||||
form_class_choices = {
|
||||
const.DATABASE_APP_TYPE_MYSQL: forms.DatabaseAppMySQLForm,
|
||||
}
|
||||
|
||||
def get_initial(self):
|
||||
return {'type': self.get_type()}
|
||||
|
||||
def get_type(self):
|
||||
return self.default_type
|
||||
|
||||
def get_form_class(self):
|
||||
tp = self.get_type()
|
||||
form_class = self.form_class_choices.get(tp)
|
||||
if not form_class:
|
||||
raise Http404()
|
||||
return form_class
|
||||
|
||||
|
||||
class DatabaseAppCreateView(BaseDatabaseAppCreateUpdateView, CreateView):
|
||||
|
||||
def get_type(self):
|
||||
tp = self.request.GET.get("type")
|
||||
if tp:
|
||||
return tp.lower()
|
||||
return super().get_type()
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'app': _('Applications'),
|
||||
'action': _('Create DatabaseApp'),
|
||||
'api_action': 'create'
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class DatabaseAppUpdateView(BaseDatabaseAppCreateUpdateView, UpdateView):
|
||||
|
||||
def get_type(self):
|
||||
return self.object.type
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'app': _('Applications'),
|
||||
'action': _('Create DatabaseApp'),
|
||||
'api_action': 'update'
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class DatabaseAppDetailView(PermissionsMixin, DetailView):
|
||||
template_name = 'applications/database_app_detail.html'
|
||||
model = models.DatabaseApp
|
||||
context_object_name = 'database_app'
|
||||
permission_classes = [IsOrgAdmin]
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'app': _('Applications'),
|
||||
'action': _('DatabaseApp detail'),
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class UserDatabaseAppListView(PermissionsMixin, TemplateView):
|
||||
template_name = 'applications/user_database_app_list.html'
|
||||
permission_classes = [IsValidUser]
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'action': _('My DatabaseApp'),
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
@@ -1,128 +0,0 @@
|
||||
# coding: utf-8
|
||||
#
|
||||
|
||||
from django.http import Http404
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.generic import TemplateView
|
||||
from django.views.generic.edit import CreateView, UpdateView
|
||||
from django.views.generic.detail import DetailView
|
||||
|
||||
from common.permissions import PermissionsMixin, IsOrgAdmin, IsValidUser
|
||||
|
||||
from ..models import RemoteApp
|
||||
from .. import forms, const
|
||||
|
||||
|
||||
__all__ = [
|
||||
'RemoteAppListView', 'RemoteAppCreateView', 'RemoteAppUpdateView',
|
||||
'RemoteAppDetailView', 'UserRemoteAppListView',
|
||||
]
|
||||
|
||||
|
||||
class RemoteAppListView(PermissionsMixin, TemplateView):
|
||||
template_name = 'applications/remote_app_list.html'
|
||||
permission_classes = [IsOrgAdmin]
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'app': _('Applications'),
|
||||
'action': _('RemoteApp list'),
|
||||
'type_choices': const.REMOTE_APP_TYPE_CHOICES,
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class BaseRemoteAppCreateUpdateView:
|
||||
template_name = 'applications/remote_app_create_update.html'
|
||||
model = RemoteApp
|
||||
permission_classes = [IsOrgAdmin]
|
||||
default_type = const.REMOTE_APP_TYPE_CHROME
|
||||
form_class = forms.RemoteAppChromeForm
|
||||
form_class_choices = {
|
||||
const.REMOTE_APP_TYPE_CHROME: forms.RemoteAppChromeForm,
|
||||
const.REMOTE_APP_TYPE_MYSQL_WORKBENCH: forms.RemoteAppMySQLWorkbenchForm,
|
||||
const.REMOTE_APP_TYPE_VMWARE_CLIENT: forms.RemoteAppVMwareForm,
|
||||
const.REMOTE_APP_TYPE_CUSTOM: forms.RemoteAppCustomForm
|
||||
}
|
||||
|
||||
def get_initial(self):
|
||||
return {'type': self.get_type()}
|
||||
|
||||
def get_type(self):
|
||||
return self.default_type
|
||||
|
||||
def get_form_class(self):
|
||||
tp = self.get_type()
|
||||
form_class = self.form_class_choices.get(tp)
|
||||
if not form_class:
|
||||
raise Http404()
|
||||
return form_class
|
||||
|
||||
|
||||
class RemoteAppCreateView(BaseRemoteAppCreateUpdateView,
|
||||
PermissionsMixin, CreateView):
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'app': _('Applications'),
|
||||
'action': _('Create RemoteApp'),
|
||||
'api_action': 'create'
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
def get_type(self):
|
||||
tp = self.request.GET.get("type")
|
||||
if tp:
|
||||
return tp.lower()
|
||||
return super().get_type()
|
||||
|
||||
|
||||
class RemoteAppUpdateView(BaseRemoteAppCreateUpdateView,
|
||||
PermissionsMixin, UpdateView):
|
||||
|
||||
def get_initial(self):
|
||||
initial_data = super().get_initial()
|
||||
params = {k: v for k, v in self.object.params.items()}
|
||||
initial_data.update(params)
|
||||
return initial_data
|
||||
|
||||
def get_type(self):
|
||||
return self.object.type
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'app': _('Applications'),
|
||||
'action': _('Update RemoteApp'),
|
||||
'api_action': 'update'
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class RemoteAppDetailView(PermissionsMixin, DetailView):
|
||||
template_name = 'applications/remote_app_detail.html'
|
||||
model = RemoteApp
|
||||
context_object_name = 'remote_app'
|
||||
permission_classes = [IsOrgAdmin]
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'app': _('Applications'),
|
||||
'action': _('RemoteApp detail'),
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class UserRemoteAppListView(PermissionsMixin, TemplateView):
|
||||
template_name = 'applications/user_remote_app_list.html'
|
||||
permission_classes = [IsValidUser]
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'action': _('My RemoteApp'),
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
@@ -1,3 +1,4 @@
|
||||
from .mixin import *
|
||||
from .admin_user import *
|
||||
from .asset import *
|
||||
from .label import *
|
||||
|
||||
@@ -1,17 +1,4 @@
|
||||
# ~*~ coding: utf-8 ~*~
|
||||
# Copyright (C) 2014-2018 Beijing DuiZhan Technology Co.,Ltd. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the GNU General Public License v2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.gnu.org/licenses/gpl-2.0.html
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
from django.db import transaction
|
||||
from django.db.models import Count
|
||||
@@ -49,7 +36,7 @@ class AdminUserViewSet(OrgBulkModelViewSet):
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
queryset = queryset.annotate(_assets_amount=Count('assets'))
|
||||
queryset = queryset.annotate(assets_amount=Count('assets'))
|
||||
return queryset
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
import random
|
||||
|
||||
from rest_framework.response import Response
|
||||
from assets.api import FilterAssetByNodeMixin
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
from rest_framework.generics import RetrieveAPIView
|
||||
from django.shortcuts import get_object_or_404
|
||||
@@ -17,7 +14,7 @@ from .. import serializers
|
||||
from ..tasks import (
|
||||
update_asset_hardware_info_manual, test_asset_connectivity_manual
|
||||
)
|
||||
from ..filters import AssetByNodeFilterBackend, LabelFilterBackend
|
||||
from ..filters import FilterAssetByNodeFilterBackend, LabelFilterBackend, IpInFilterBackend
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
@@ -28,12 +25,15 @@ __all__ = [
|
||||
]
|
||||
|
||||
|
||||
class AssetViewSet(OrgBulkModelViewSet):
|
||||
class AssetViewSet(FilterAssetByNodeMixin, OrgBulkModelViewSet):
|
||||
"""
|
||||
API endpoint that allows Asset to be viewed or edited.
|
||||
"""
|
||||
model = Asset
|
||||
filter_fields = ("hostname", "ip", "systemuser__id", "admin_user__id")
|
||||
filter_fields = (
|
||||
"hostname", "ip", "systemuser__id", "admin_user__id", "platform__base",
|
||||
"is_active", 'ip'
|
||||
)
|
||||
search_fields = ("hostname", "ip")
|
||||
ordering_fields = ("hostname", "ip", "port", "cpu_cores")
|
||||
serializer_classes = {
|
||||
@@ -41,7 +41,7 @@ class AssetViewSet(OrgBulkModelViewSet):
|
||||
'display': serializers.AssetDisplaySerializer,
|
||||
}
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
extra_filter_backends = [AssetByNodeFilterBackend, LabelFilterBackend]
|
||||
extra_filter_backends = [FilterAssetByNodeFilterBackend, LabelFilterBackend, IpInFilterBackend]
|
||||
|
||||
def set_assets_node(self, assets):
|
||||
if not isinstance(assets, list):
|
||||
@@ -74,12 +74,16 @@ class AssetPlatformViewSet(ModelViewSet):
|
||||
queryset = Platform.objects.all()
|
||||
permission_classes = (IsSuperUser,)
|
||||
serializer_class = serializers.PlatformSerializer
|
||||
filterset_fields = ['name', 'base']
|
||||
filter_fields = ['name', 'base']
|
||||
search_fields = ['name']
|
||||
|
||||
def get_permissions(self):
|
||||
if self.request.method.lower() in ['get', 'options']:
|
||||
self.permission_classes = (IsOrgAdmin,)
|
||||
return super().get_permissions()
|
||||
|
||||
def check_object_permissions(self, request, obj):
|
||||
if request.method.lower() in ['delete', 'put', 'patch'] and \
|
||||
obj.internal:
|
||||
if request.method.lower() in ['delete', 'put', 'patch'] and obj.internal:
|
||||
self.permission_denied(
|
||||
request, message={"detail": "Internal platform"}
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import coreapi
|
||||
from django.conf import settings
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import generics, filters
|
||||
@@ -54,6 +55,15 @@ class AssetUserSearchBackend(filters.BaseFilterBackend):
|
||||
|
||||
|
||||
class AssetUserLatestFilterBackend(filters.BaseFilterBackend):
|
||||
def get_schema_fields(self, view):
|
||||
return [
|
||||
coreapi.Field(
|
||||
name='latest', location='query', required=False,
|
||||
type='string', example='1',
|
||||
description='Only the latest version'
|
||||
)
|
||||
]
|
||||
|
||||
def filter_queryset(self, request, queryset, view):
|
||||
latest = request.GET.get('latest') == '1'
|
||||
if latest:
|
||||
@@ -64,7 +74,7 @@ class AssetUserLatestFilterBackend(filters.BaseFilterBackend):
|
||||
class AssetUserViewSet(CommonApiMixin, BulkModelViewSet):
|
||||
serializer_classes = {
|
||||
'default': serializers.AssetUserWriteSerializer,
|
||||
'list': serializers.AssetUserReadSerializer,
|
||||
'display': serializers.AssetUserReadSerializer,
|
||||
'retrieve': serializers.AssetUserReadSerializer,
|
||||
}
|
||||
permission_classes = [IsOrgAdminOrAppUser]
|
||||
@@ -84,12 +94,15 @@ class AssetUserViewSet(CommonApiMixin, BulkModelViewSet):
|
||||
|
||||
def get_object(self):
|
||||
pk = self.kwargs.get("pk")
|
||||
if pk is None:
|
||||
return
|
||||
queryset = self.get_queryset()
|
||||
obj = queryset.get(id=pk)
|
||||
return obj
|
||||
|
||||
def get_exception_handler(self):
|
||||
def handler(e, context):
|
||||
logger.error(e, exc_info=True)
|
||||
return Response({"error": str(e)}, status=400)
|
||||
return handler
|
||||
|
||||
|
||||
@@ -18,5 +18,5 @@ class GatheredUserViewSet(OrgModelViewSet):
|
||||
permission_classes = [IsOrgAdmin]
|
||||
extra_filter_backends = [AssetRelatedByNodeFilterBackend]
|
||||
|
||||
filter_fields = ['asset', 'username', 'present']
|
||||
filter_fields = ['asset', 'username', 'present', 'asset__ip', 'asset__hostname', 'asset_id']
|
||||
search_fields = ['username', 'asset__ip', 'asset__hostname']
|
||||
|
||||
89
apps/assets/api/mixin.py
Normal file
89
apps/assets/api/mixin.py
Normal file
@@ -0,0 +1,89 @@
|
||||
from typing import List
|
||||
|
||||
from assets.models import Node, Asset
|
||||
from assets.pagination import AssetLimitOffsetPagination
|
||||
from common.utils import lazyproperty, dict_get_any, is_uuid, get_object_or_none
|
||||
from assets.utils import get_node, is_query_node_all_assets
|
||||
|
||||
|
||||
class SerializeToTreeNodeMixin:
|
||||
permission_classes = ()
|
||||
|
||||
def serialize_nodes(self, nodes: List[Node], with_asset_amount=False):
|
||||
if with_asset_amount:
|
||||
def _name(node: Node):
|
||||
return '{} ({})'.format(node.value, node.assets_amount)
|
||||
else:
|
||||
def _name(node: Node):
|
||||
return node.value
|
||||
data = [
|
||||
{
|
||||
'id': node.key,
|
||||
'name': _name(node),
|
||||
'title': _name(node),
|
||||
'pId': node.parent_key,
|
||||
'isParent': True,
|
||||
'open': node.is_org_root(),
|
||||
'meta': {
|
||||
'node': {
|
||||
"id": node.id,
|
||||
"key": node.key,
|
||||
"value": node.value,
|
||||
},
|
||||
'type': 'node'
|
||||
}
|
||||
}
|
||||
for node in nodes
|
||||
]
|
||||
return data
|
||||
|
||||
def get_platform(self, asset: Asset):
|
||||
default = 'file'
|
||||
icon = {'windows', 'linux'}
|
||||
platform = asset.platform_base.lower()
|
||||
if platform in icon:
|
||||
return platform
|
||||
return default
|
||||
|
||||
def serialize_assets(self, assets, node_key=None):
|
||||
if node_key is None:
|
||||
get_pid = lambda asset: getattr(asset, 'parent_key', '')
|
||||
else:
|
||||
get_pid = lambda asset: node_key
|
||||
|
||||
data = [
|
||||
{
|
||||
'id': str(asset.id),
|
||||
'name': asset.hostname,
|
||||
'title': asset.ip,
|
||||
'pId': get_pid(asset),
|
||||
'isParent': False,
|
||||
'open': False,
|
||||
'iconSkin': self.get_platform(asset),
|
||||
'chkDisabled': not asset.is_active,
|
||||
'meta': {
|
||||
'type': 'asset',
|
||||
'asset': {
|
||||
'id': asset.id,
|
||||
'hostname': asset.hostname,
|
||||
'ip': asset.ip,
|
||||
'protocols': asset.protocols_as_list,
|
||||
'platform': asset.platform_base,
|
||||
},
|
||||
}
|
||||
}
|
||||
for asset in assets
|
||||
]
|
||||
return data
|
||||
|
||||
|
||||
class FilterAssetByNodeMixin:
|
||||
pagination_class = AssetLimitOffsetPagination
|
||||
|
||||
@lazyproperty
|
||||
def is_query_node_all_assets(self):
|
||||
return is_query_node_all_assets(self.request)
|
||||
|
||||
@lazyproperty
|
||||
def node(self):
|
||||
return get_node(self.request)
|
||||
@@ -1,16 +1,24 @@
|
||||
# ~*~ coding: utf-8 ~*~
|
||||
from functools import partial
|
||||
from collections import namedtuple, defaultdict
|
||||
|
||||
from collections import namedtuple
|
||||
from rest_framework import status
|
||||
from rest_framework.serializers import ValidationError
|
||||
from rest_framework.response import Response
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.shortcuts import get_object_or_404, Http404
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.db.models.signals import m2m_changed
|
||||
|
||||
from common.exceptions import SomeoneIsDoingThis
|
||||
from common.const.signals import PRE_REMOVE, POST_REMOVE
|
||||
from assets.models import Asset
|
||||
from common.utils import get_logger, get_object_or_none
|
||||
from common.tree import TreeNodeSerializer
|
||||
from common.const.distributed_lock_key import UPDATE_NODE_TREE_LOCK_KEY
|
||||
from orgs.mixins.api import OrgModelViewSet
|
||||
from orgs.mixins import generics
|
||||
from orgs.lock import org_level_transaction_lock
|
||||
from ..hands import IsOrgAdmin
|
||||
from ..models import Node
|
||||
from ..tasks import (
|
||||
@@ -18,12 +26,13 @@ from ..tasks import (
|
||||
test_node_assets_connectivity_manual,
|
||||
)
|
||||
from .. import serializers
|
||||
from .mixin import SerializeToTreeNodeMixin
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
__all__ = [
|
||||
'NodeViewSet', 'NodeChildrenApi', 'NodeAssetsApi',
|
||||
'NodeAddAssetsApi', 'NodeRemoveAssetsApi', 'NodeReplaceAssetsApi',
|
||||
'NodeAddAssetsApi', 'NodeRemoveAssetsApi', 'MoveAssetsToNodeApi',
|
||||
'NodeAddChildrenApi', 'NodeListAsTreeApi',
|
||||
'NodeChildrenAsTreeApi',
|
||||
'NodeTaskCreateApi',
|
||||
@@ -136,7 +145,7 @@ class NodeChildrenApi(generics.ListCreateAPIView):
|
||||
return queryset
|
||||
|
||||
|
||||
class NodeChildrenAsTreeApi(NodeChildrenApi):
|
||||
class NodeChildrenAsTreeApi(SerializeToTreeNodeMixin, NodeChildrenApi):
|
||||
"""
|
||||
节点子节点作为树返回,
|
||||
[
|
||||
@@ -150,31 +159,23 @@ class NodeChildrenAsTreeApi(NodeChildrenApi):
|
||||
|
||||
"""
|
||||
model = Node
|
||||
serializer_class = TreeNodeSerializer
|
||||
http_method_names = ['get']
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
queryset = [node.as_tree_node() for node in queryset]
|
||||
queryset = self.add_assets_if_need(queryset)
|
||||
queryset = sorted(queryset)
|
||||
return queryset
|
||||
def list(self, request, *args, **kwargs):
|
||||
nodes = self.get_queryset().order_by('value')
|
||||
nodes = self.serialize_nodes(nodes, with_asset_amount=True)
|
||||
assets = self.get_assets()
|
||||
data = [*nodes, *assets]
|
||||
return Response(data=data)
|
||||
|
||||
def add_assets_if_need(self, queryset):
|
||||
def get_assets(self):
|
||||
include_assets = self.request.query_params.get('assets', '0') == '1'
|
||||
if not include_assets:
|
||||
return queryset
|
||||
return []
|
||||
assets = self.instance.get_assets().only(
|
||||
"id", "hostname", "ip", "os",
|
||||
"org_id", "protocols",
|
||||
)
|
||||
for asset in assets:
|
||||
queryset.append(asset.as_tree_node(self.instance))
|
||||
return queryset
|
||||
|
||||
def check_need_refresh_nodes(self):
|
||||
if self.request.query_params.get('refresh', '0') == '1':
|
||||
Node.refresh_nodes()
|
||||
return self.serialize_assets(assets, self.instance.key)
|
||||
|
||||
|
||||
class NodeAssetsApi(generics.ListAPIView):
|
||||
@@ -208,6 +209,8 @@ class NodeAddChildrenApi(generics.UpdateAPIView):
|
||||
return Response("OK")
|
||||
|
||||
|
||||
@method_decorator(org_level_transaction_lock(UPDATE_NODE_TREE_LOCK_KEY), name='patch')
|
||||
@method_decorator(org_level_transaction_lock(UPDATE_NODE_TREE_LOCK_KEY), name='put')
|
||||
class NodeAddAssetsApi(generics.UpdateAPIView):
|
||||
model = Node
|
||||
serializer_class = serializers.NodeAssetsSerializer
|
||||
@@ -220,6 +223,8 @@ class NodeAddAssetsApi(generics.UpdateAPIView):
|
||||
instance.assets.add(*tuple(assets))
|
||||
|
||||
|
||||
@method_decorator(org_level_transaction_lock(UPDATE_NODE_TREE_LOCK_KEY), name='patch')
|
||||
@method_decorator(org_level_transaction_lock(UPDATE_NODE_TREE_LOCK_KEY), name='put')
|
||||
class NodeRemoveAssetsApi(generics.UpdateAPIView):
|
||||
model = Node
|
||||
serializer_class = serializers.NodeAssetsSerializer
|
||||
@@ -228,15 +233,17 @@ class NodeRemoveAssetsApi(generics.UpdateAPIView):
|
||||
|
||||
def perform_update(self, serializer):
|
||||
assets = serializer.validated_data.get('assets')
|
||||
instance = self.get_object()
|
||||
if instance != Node.org_root():
|
||||
instance.assets.remove(*tuple(assets))
|
||||
else:
|
||||
assets = [asset for asset in assets if asset.nodes.count() > 1]
|
||||
instance.assets.remove(*tuple(assets))
|
||||
node = self.get_object()
|
||||
node.assets.remove(*assets)
|
||||
|
||||
# 把孤儿资产添加到 root 节点
|
||||
orphan_assets = Asset.objects.filter(id__in=[a.id for a in assets], nodes__isnull=True).distinct()
|
||||
Node.org_root().assets.add(*orphan_assets)
|
||||
|
||||
|
||||
class NodeReplaceAssetsApi(generics.UpdateAPIView):
|
||||
@method_decorator(org_level_transaction_lock(UPDATE_NODE_TREE_LOCK_KEY), name='patch')
|
||||
@method_decorator(org_level_transaction_lock(UPDATE_NODE_TREE_LOCK_KEY), name='put')
|
||||
class MoveAssetsToNodeApi(generics.UpdateAPIView):
|
||||
model = Node
|
||||
serializer_class = serializers.NodeAssetsSerializer
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
@@ -244,9 +251,39 @@ class NodeReplaceAssetsApi(generics.UpdateAPIView):
|
||||
|
||||
def perform_update(self, serializer):
|
||||
assets = serializer.validated_data.get('assets')
|
||||
instance = self.get_object()
|
||||
for asset in assets:
|
||||
asset.nodes.set([instance])
|
||||
node = self.get_object()
|
||||
self.remove_old_nodes(assets)
|
||||
node.assets.add(*assets)
|
||||
|
||||
def remove_old_nodes(self, assets):
|
||||
m2m_model = Asset.nodes.through
|
||||
|
||||
# 查询资产与节点关系表,查出要移动资产与节点的所有关系
|
||||
relates = m2m_model.objects.filter(asset__in=assets).values_list('asset_id', 'node_id')
|
||||
if relates:
|
||||
# 对关系以资产进行分组,用来发 `reverse=False` 信号
|
||||
asset_nodes_mapper = defaultdict(set)
|
||||
for asset_id, node_id in relates:
|
||||
asset_nodes_mapper[asset_id].add(node_id)
|
||||
|
||||
# 组建一个资产 id -> Asset 的 mapper
|
||||
asset_mapper = {asset.id: asset for asset in assets}
|
||||
|
||||
# 创建删除关系信号发送函数
|
||||
senders = []
|
||||
for asset_id, node_id_set in asset_nodes_mapper.items():
|
||||
senders.append(partial(m2m_changed.send, sender=m2m_model, instance=asset_mapper[asset_id],
|
||||
reverse=False, model=Node, pk_set=node_id_set))
|
||||
# 发送 pre 信号
|
||||
[sender(action=PRE_REMOVE) for sender in senders]
|
||||
num = len(relates)
|
||||
asset_ids, node_ids = zip(*relates)
|
||||
# 删除之前的关系
|
||||
rows, _i = m2m_model.objects.filter(asset_id__in=asset_ids, node_id__in=node_ids).delete()
|
||||
if rows != num:
|
||||
raise SomeoneIsDoingThis
|
||||
# 发送 post 信号
|
||||
[sender(action=POST_REMOVE) for sender in senders]
|
||||
|
||||
|
||||
class NodeTaskCreateApi(generics.CreateAPIView):
|
||||
@@ -267,7 +304,6 @@ class NodeTaskCreateApi(generics.CreateAPIView):
|
||||
|
||||
@staticmethod
|
||||
def refresh_nodes_cache():
|
||||
Node.refresh_nodes()
|
||||
Task = namedtuple('Task', ['id'])
|
||||
task = Task(id="0")
|
||||
return task
|
||||
|
||||
@@ -28,7 +28,7 @@ class SystemUserViewSet(OrgBulkModelViewSet):
|
||||
System user api set, for add,delete,update,list,retrieve resource
|
||||
"""
|
||||
model = SystemUser
|
||||
filter_fields = ("name", "username")
|
||||
filter_fields = ("name", "username", "protocol")
|
||||
search_fields = filter_fields
|
||||
serializer_class = serializers.SystemUserSerializer
|
||||
serializer_classes = {
|
||||
|
||||
@@ -65,7 +65,7 @@ class SystemUserAssetRelationViewSet(BaseRelationViewSet):
|
||||
serializer_class = serializers.SystemUserAssetRelationSerializer
|
||||
model = models.SystemUser.assets.through
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
filterset_fields = [
|
||||
filter_fields = [
|
||||
'id', 'asset', 'systemuser',
|
||||
]
|
||||
search_fields = [
|
||||
@@ -91,7 +91,7 @@ class SystemUserNodeRelationViewSet(BaseRelationViewSet):
|
||||
serializer_class = serializers.SystemUserNodeRelationSerializer
|
||||
model = models.SystemUser.nodes.through
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
filterset_fields = [
|
||||
filter_fields = [
|
||||
'id', 'node', 'systemuser',
|
||||
]
|
||||
search_fields = [
|
||||
@@ -112,7 +112,7 @@ class SystemUserUserRelationViewSet(BaseRelationViewSet):
|
||||
serializer_class = serializers.SystemUserUserRelationSerializer
|
||||
model = models.SystemUser.users.through
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
filterset_fields = [
|
||||
filter_fields = [
|
||||
'id', 'user', 'systemuser',
|
||||
]
|
||||
search_fields = [
|
||||
|
||||
6
apps/assets/exceptions.py
Normal file
6
apps/assets/exceptions.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from rest_framework import status
|
||||
from common.exceptions import JMSException
|
||||
|
||||
|
||||
class NodeIsBeingUpdatedByOthers(JMSException):
|
||||
status_code = status.HTTP_409_CONFLICT
|
||||
@@ -1,12 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
import coreapi
|
||||
from rest_framework.compat import coreapi, coreschema
|
||||
from rest_framework import filters
|
||||
from django.db.models import Q
|
||||
|
||||
from common.utils import dict_get_any, is_uuid, get_object_or_none
|
||||
from .models import Node, Label
|
||||
from .models import Label
|
||||
from assets.utils import is_query_node_all_assets, get_node
|
||||
|
||||
|
||||
class AssetByNodeFilterBackend(filters.BaseFilterBackend):
|
||||
@@ -21,51 +21,58 @@ class AssetByNodeFilterBackend(filters.BaseFilterBackend):
|
||||
for field in self.fields
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def is_query_all(request):
|
||||
query_all_arg = request.query_params.get('all')
|
||||
show_current_asset_arg = request.query_params.get('show_current_asset')
|
||||
def filter_node_related_all(self, queryset, node):
|
||||
return queryset.filter(
|
||||
Q(nodes__key__istartswith=f'{node.key}:') |
|
||||
Q(nodes__key=node.key)
|
||||
).distinct()
|
||||
|
||||
query_all = query_all_arg == '1'
|
||||
if show_current_asset_arg is not None:
|
||||
query_all = show_current_asset_arg != '1'
|
||||
return query_all
|
||||
|
||||
@staticmethod
|
||||
def get_query_node(request):
|
||||
node_id = dict_get_any(request.query_params, ['node', 'node_id'])
|
||||
if not node_id:
|
||||
return None, False
|
||||
|
||||
if is_uuid(node_id):
|
||||
node = get_object_or_none(Node, id=node_id)
|
||||
else:
|
||||
node = get_object_or_none(Node, key=node_id)
|
||||
return node, True
|
||||
|
||||
@staticmethod
|
||||
def perform_query(pattern, queryset):
|
||||
return queryset.filter(nodes__key__regex=pattern).distinct()
|
||||
def filter_node_related_direct(self, queryset, node):
|
||||
return queryset.filter(nodes__key=node.key).distinct()
|
||||
|
||||
def filter_queryset(self, request, queryset, view):
|
||||
node, has_query_arg = self.get_query_node(request)
|
||||
if not has_query_arg:
|
||||
return queryset
|
||||
|
||||
node = get_node(request)
|
||||
if node is None:
|
||||
return queryset
|
||||
query_all = self.is_query_all(request)
|
||||
|
||||
query_all = is_query_node_all_assets(request)
|
||||
if query_all:
|
||||
pattern = node.get_all_children_pattern(with_self=True)
|
||||
return self.filter_node_related_all(queryset, node)
|
||||
else:
|
||||
# pattern = node.get_children_key_pattern(with_self=True)
|
||||
# 只显示当前节点下资产
|
||||
pattern = r"^{}$".format(node.key)
|
||||
return self.perform_query(pattern, queryset)
|
||||
return self.filter_node_related_direct(queryset, node)
|
||||
|
||||
|
||||
class FilterAssetByNodeFilterBackend(filters.BaseFilterBackend):
|
||||
"""
|
||||
需要与 `assets.api.mixin.FilterAssetByNodeMixin` 配合使用
|
||||
"""
|
||||
fields = ['node', 'all']
|
||||
|
||||
def get_schema_fields(self, view):
|
||||
return [
|
||||
coreapi.Field(
|
||||
name=field, location='query', required=False,
|
||||
type='string', example='', description='', schema=None,
|
||||
)
|
||||
for field in self.fields
|
||||
]
|
||||
|
||||
def filter_queryset(self, request, queryset, view):
|
||||
node = view.node
|
||||
if node is None:
|
||||
return queryset
|
||||
query_all = view.is_query_node_all_assets
|
||||
if query_all:
|
||||
return queryset.filter(
|
||||
Q(nodes__key__istartswith=f'{node.key}:') |
|
||||
Q(nodes__key=node.key)
|
||||
).distinct()
|
||||
else:
|
||||
return queryset.filter(nodes__key=node.key).distinct()
|
||||
|
||||
|
||||
class LabelFilterBackend(filters.BaseFilterBackend):
|
||||
sep = '#'
|
||||
sep = ':'
|
||||
query_arg = 'label'
|
||||
|
||||
def get_schema_fields(self, view):
|
||||
@@ -84,6 +91,8 @@ class LabelFilterBackend(filters.BaseFilterBackend):
|
||||
|
||||
q = None
|
||||
for kv in labels_query:
|
||||
if '#' in kv:
|
||||
self.sep = '#'
|
||||
if self.sep not in kv:
|
||||
continue
|
||||
key, value = kv.strip().split(self.sep)[:2]
|
||||
@@ -111,7 +120,32 @@ class LabelFilterBackend(filters.BaseFilterBackend):
|
||||
|
||||
|
||||
class AssetRelatedByNodeFilterBackend(AssetByNodeFilterBackend):
|
||||
@staticmethod
|
||||
def perform_query(pattern, queryset):
|
||||
return queryset.filter(asset__nodes__key__regex=pattern).distinct()
|
||||
def filter_node_related_all(self, queryset, node):
|
||||
return queryset.filter(
|
||||
Q(asset__nodes__key__istartswith=f'{node.key}:') |
|
||||
Q(asset__nodes__key=node.key)
|
||||
).distinct()
|
||||
|
||||
def filter_node_related_direct(self, queryset, node):
|
||||
return queryset.filter(asset__nodes__key=node.key).distinct()
|
||||
|
||||
|
||||
class IpInFilterBackend(filters.BaseFilterBackend):
|
||||
def filter_queryset(self, request, queryset, view):
|
||||
ips = request.query_params.get('ips')
|
||||
if not ips:
|
||||
return queryset
|
||||
ip_list = [i.strip() for i in ips.split(',')]
|
||||
queryset = queryset.filter(ip__in=ip_list)
|
||||
return queryset
|
||||
|
||||
def get_schema_fields(self, view):
|
||||
return [
|
||||
coreapi.Field(
|
||||
name='ips', location='query', required=False, type='string',
|
||||
schema=coreschema.String(
|
||||
title='ips',
|
||||
description='ip in filter'
|
||||
)
|
||||
)
|
||||
]
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from .asset import *
|
||||
from .label import *
|
||||
from .user import *
|
||||
from .domain import *
|
||||
from .cmd_filter import *
|
||||
from .platform import *
|
||||
@@ -1,172 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from itertools import groupby
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from common.utils import get_logger
|
||||
from orgs.mixins.forms import OrgModelForm
|
||||
|
||||
from ..models import Asset, Platform
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
__all__ = [
|
||||
'AssetCreateUpdateForm', 'AssetBulkUpdateForm', 'ProtocolForm',
|
||||
]
|
||||
|
||||
|
||||
class ProtocolForm(forms.Form):
|
||||
name = forms.ChoiceField(
|
||||
choices=Asset.PROTOCOL_CHOICES, label=_("Name"), initial='ssh',
|
||||
widget=forms.Select(attrs={'class': 'form-control protocol-name'})
|
||||
)
|
||||
port = forms.IntegerField(
|
||||
max_value=65534, min_value=1, label=_("Port"), initial=22,
|
||||
widget=forms.TextInput(attrs={'class': 'form-control protocol-port'})
|
||||
)
|
||||
|
||||
|
||||
class AssetCreateUpdateForm(OrgModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.set_platform_to_name()
|
||||
self.set_fields_queryset()
|
||||
|
||||
def set_fields_queryset(self):
|
||||
nodes_field = self.fields['nodes']
|
||||
nodes_choices = []
|
||||
if self.instance:
|
||||
nodes_choices = [
|
||||
(n.id, n.full_value) for n in
|
||||
self.instance.nodes.all()
|
||||
]
|
||||
nodes_field.choices = nodes_choices
|
||||
|
||||
@staticmethod
|
||||
def sorted_platform(platform):
|
||||
if platform['base'] == 'Other':
|
||||
return 'zz'
|
||||
return platform['base']
|
||||
|
||||
def set_platform_to_name(self):
|
||||
choices = []
|
||||
platforms = Platform.objects.all().values('name', 'base')
|
||||
platforms_sorted = sorted(platforms, key=self.sorted_platform)
|
||||
platforms_grouped = groupby(platforms_sorted, key=lambda x: x['base'])
|
||||
for i in platforms_grouped:
|
||||
base = i[0]
|
||||
grouped = sorted(i[1], key=lambda x: x['name'])
|
||||
grouped = [(j['name'], j['name']) for j in grouped]
|
||||
choices.append(
|
||||
(base, grouped)
|
||||
)
|
||||
platform_field = self.fields['platform']
|
||||
platform_field.choices = choices
|
||||
if self.instance:
|
||||
self.initial['platform'] = self.instance.platform.name
|
||||
|
||||
def add_nodes_initial(self, node):
|
||||
nodes_field = self.fields['nodes']
|
||||
nodes_field.choices.append((node.id, node.full_value))
|
||||
nodes_field.initial = [node]
|
||||
|
||||
class Meta:
|
||||
model = Asset
|
||||
fields = [
|
||||
'hostname', 'ip', 'public_ip', 'protocols', 'comment',
|
||||
'nodes', 'is_active', 'admin_user', 'labels', 'platform',
|
||||
'domain', 'number',
|
||||
]
|
||||
widgets = {
|
||||
'nodes': forms.SelectMultiple(attrs={
|
||||
'class': 'nodes-select2', 'data-placeholder': _('Nodes')
|
||||
}),
|
||||
'admin_user': forms.Select(attrs={
|
||||
'class': 'select2', 'data-placeholder': _('Admin user')
|
||||
}),
|
||||
'labels': forms.SelectMultiple(attrs={
|
||||
'class': 'select2', 'data-placeholder': _('Label')
|
||||
}),
|
||||
'domain': forms.Select(attrs={
|
||||
'class': 'select2', 'data-placeholder': _('Domain')
|
||||
}),
|
||||
'platform': forms.Select(attrs={
|
||||
'class': 'select2', 'data-placeholder': _('Platform')
|
||||
}),
|
||||
}
|
||||
labels = {
|
||||
'nodes': _("Node"),
|
||||
}
|
||||
help_texts = {
|
||||
'admin_user': _(
|
||||
'root or other NOPASSWD sudo privilege user existed in asset,'
|
||||
'If asset is windows or other set any one, more see admin user left menu'
|
||||
),
|
||||
'platform': _("Windows 2016 RDP protocol is different, If is window 2016, set it"),
|
||||
'domain': _("If your have some network not connect with each other, you can set domain")
|
||||
}
|
||||
|
||||
|
||||
class AssetBulkUpdateForm(OrgModelForm):
|
||||
assets = forms.ModelMultipleChoiceField(
|
||||
required=True,
|
||||
label=_('Select assets'), queryset=Asset.objects,
|
||||
widget=forms.SelectMultiple(
|
||||
attrs={
|
||||
'class': 'select2',
|
||||
'data-placeholder': _('Select assets')
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Asset
|
||||
fields = [
|
||||
'assets', 'admin_user', 'labels', 'platform',
|
||||
'domain',
|
||||
]
|
||||
widgets = {
|
||||
'labels': forms.SelectMultiple(
|
||||
attrs={'class': 'select2', 'data-placeholder': _('Label')}
|
||||
),
|
||||
'nodes': forms.SelectMultiple(
|
||||
attrs={'class': 'select2', 'data-placeholder': _('Node')}
|
||||
),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.set_fields_queryset()
|
||||
|
||||
# 重写其他字段为不再required
|
||||
for name, field in self.fields.items():
|
||||
if name != 'assets':
|
||||
field.required = False
|
||||
|
||||
def set_fields_queryset(self):
|
||||
assets_field = self.fields['assets']
|
||||
if hasattr(self, 'data'):
|
||||
assets_field.queryset = Asset.objects.all()
|
||||
|
||||
def save(self, commit=True):
|
||||
changed_fields = []
|
||||
for field in self._meta.fields:
|
||||
if self.data.get(field) not in [None, '']:
|
||||
changed_fields.append(field)
|
||||
|
||||
cleaned_data = {k: v for k, v in self.cleaned_data.items()
|
||||
if k in changed_fields}
|
||||
assets = cleaned_data.pop('assets')
|
||||
labels = cleaned_data.pop('labels', [])
|
||||
nodes = cleaned_data.pop('nodes', None)
|
||||
assets = Asset.objects.filter(id__in=[asset.id for asset in assets])
|
||||
assets.update(**cleaned_data)
|
||||
|
||||
if labels:
|
||||
for asset in assets:
|
||||
asset.labels.set(labels)
|
||||
if nodes:
|
||||
for asset in assets:
|
||||
asset.nodes.set(nodes)
|
||||
return assets
|
||||
@@ -1,40 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
import re
|
||||
|
||||
from orgs.mixins.forms import OrgModelForm
|
||||
from ..models import CommandFilter, CommandFilterRule
|
||||
|
||||
__all__ = ['CommandFilterForm', 'CommandFilterRuleForm']
|
||||
|
||||
|
||||
class CommandFilterForm(OrgModelForm):
|
||||
class Meta:
|
||||
model = CommandFilter
|
||||
fields = ['name', 'comment']
|
||||
|
||||
|
||||
class CommandFilterRuleForm(OrgModelForm):
|
||||
invalid_pattern = re.compile(r'[\.\*\+\[\\\?\{\}\^\$\|\(\)\#\<\>]')
|
||||
|
||||
class Meta:
|
||||
model = CommandFilterRule
|
||||
fields = [
|
||||
'filter', 'type', 'content', 'priority', 'action', 'comment'
|
||||
]
|
||||
widgets = {
|
||||
'content': forms.Textarea(attrs={
|
||||
'placeholder': 'eg:\r\nreboot\r\nrm -rf'
|
||||
}),
|
||||
}
|
||||
|
||||
def clean_content(self):
|
||||
content = self.cleaned_data.get("content")
|
||||
if self.invalid_pattern.search(content):
|
||||
invalid_char = self.invalid_pattern.pattern.replace('\\', '')
|
||||
msg = _("Content should not be contain: {}").format(invalid_char)
|
||||
raise ValidationError(msg)
|
||||
return content
|
||||
@@ -1,79 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from orgs.mixins.forms import OrgModelForm
|
||||
from ..models import Domain, Asset, Gateway
|
||||
from .user import PasswordAndKeyAuthForm
|
||||
|
||||
__all__ = ['DomainForm', 'GatewayForm']
|
||||
|
||||
|
||||
class DomainForm(forms.ModelForm):
|
||||
assets = forms.ModelMultipleChoiceField(
|
||||
queryset=Asset.objects, label=_('Asset'), required=False,
|
||||
widget=forms.SelectMultiple(
|
||||
attrs={'class': 'select2', 'data-placeholder': _('Select assets')}
|
||||
)
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Domain
|
||||
fields = ['name', 'comment', 'assets']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.set_fields_queryset()
|
||||
|
||||
def set_fields_queryset(self):
|
||||
assets_field = self.fields.get('assets')
|
||||
|
||||
# 没有data代表是渲染表单, 有data代表是提交创建/更新表单
|
||||
if not self.data:
|
||||
# 有instance 代表渲染更新表单, 否则是创建表单
|
||||
# 前端渲染优化, 防止过多资产, 设置assets queryset为none
|
||||
if self.instance:
|
||||
assets_field.initial = self.instance.assets.all()
|
||||
assets_field.queryset = self.instance.assets.all()
|
||||
else:
|
||||
assets_field.queryset = Asset.objects.none()
|
||||
else:
|
||||
assets_field.queryset = Asset.objects.all()
|
||||
|
||||
def save(self, commit=True):
|
||||
instance = super().save(commit=commit)
|
||||
assets = self.cleaned_data['assets']
|
||||
instance.assets.set(assets)
|
||||
return instance
|
||||
|
||||
|
||||
class GatewayForm(PasswordAndKeyAuthForm, OrgModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
password_field = self.fields.get('password')
|
||||
password_field.help_text = _('Password should not contain special characters')
|
||||
protocol_field = self.fields.get('protocol')
|
||||
protocol_field.choices = [Gateway.PROTOCOL_CHOICES[0]]
|
||||
|
||||
def save(self, commit=True):
|
||||
# Because we define custom field, so we need rewrite :method: `save`
|
||||
instance = super().save()
|
||||
password = self.cleaned_data.get('password')
|
||||
private_key, public_key = super().gen_keys()
|
||||
instance.set_auth(password=password, private_key=private_key)
|
||||
return instance
|
||||
|
||||
class Meta:
|
||||
model = Gateway
|
||||
fields = [
|
||||
'name', 'ip', 'port', 'username', 'protocol', 'domain', 'password',
|
||||
'private_key', 'is_active', 'comment',
|
||||
]
|
||||
help_texts = {
|
||||
'protocol': _("SSH gateway support proxy SSH,RDP,VNC")
|
||||
}
|
||||
widgets = {
|
||||
'name': forms.TextInput(attrs={'placeholder': _('Name')}),
|
||||
'username': forms.TextInput(attrs={'placeholder': _('Username')}),
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from ..models import Label, Asset
|
||||
|
||||
__all__ = ['LabelForm']
|
||||
|
||||
|
||||
class LabelForm(forms.ModelForm):
|
||||
assets = forms.ModelMultipleChoiceField(
|
||||
queryset=Asset.objects.none(), label=_('Asset'), required=False,
|
||||
widget=forms.SelectMultiple(
|
||||
attrs={'class': 'select2', 'data-placeholder': _('Select assets')}
|
||||
)
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Label
|
||||
fields = ['name', 'value', 'assets']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.set_fields_queryset()
|
||||
|
||||
def set_fields_queryset(self):
|
||||
assets_field = self.fields.get('assets')
|
||||
|
||||
# 没有data代表是渲染表单, 有data代表是提交创建/更新表单
|
||||
if not self.data:
|
||||
# 有instance 代表渲染更新表单, 否则是创建表单
|
||||
# 前端渲染优化, 防止过多资产, 设置assets queryset为none
|
||||
if self.instance:
|
||||
assets_field.initial = self.instance.assets.all()
|
||||
assets_field.queryset = self.instance.assets.all()
|
||||
else:
|
||||
assets_field.queryset = Asset.objects.none()
|
||||
else:
|
||||
assets_field.queryset = Asset.objects.all()
|
||||
|
||||
def save(self, commit=True):
|
||||
label = super().save(commit=commit)
|
||||
assets = self.cleaned_data['assets']
|
||||
label.assets.set(assets)
|
||||
return label
|
||||
@@ -1,42 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from django import forms
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from ..models import Platform
|
||||
|
||||
|
||||
__all__ = ['PlatformForm', 'PlatformMetaForm']
|
||||
|
||||
|
||||
class PlatformMetaForm(forms.Form):
|
||||
SECURITY_CHOICES = (
|
||||
('rdp', "RDP"),
|
||||
('nla', "NLA"),
|
||||
('tls', 'TLS'),
|
||||
('any', "Any"),
|
||||
)
|
||||
CONSOLE_CHOICES = (
|
||||
(True, _('Yes')),
|
||||
(False, _('No')),
|
||||
)
|
||||
security = forms.ChoiceField(
|
||||
choices=SECURITY_CHOICES, initial='any', label=_("RDP security"),
|
||||
required=False,
|
||||
)
|
||||
console = forms.ChoiceField(
|
||||
choices=CONSOLE_CHOICES, initial=False, label=_("RDP console"),
|
||||
required=False,
|
||||
)
|
||||
|
||||
|
||||
class PlatformForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Platform
|
||||
fields = [
|
||||
'name', 'base', 'comment',
|
||||
]
|
||||
labels = {
|
||||
'base': _("Base platform")
|
||||
}
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from common.utils import validate_ssh_private_key, ssh_pubkey_gen, get_logger
|
||||
from orgs.mixins.forms import OrgModelForm
|
||||
from ..models import AdminUser, SystemUser
|
||||
|
||||
logger = get_logger(__file__)
|
||||
__all__ = [
|
||||
'FileForm', 'SystemUserForm', 'AdminUserForm', 'PasswordAndKeyAuthForm',
|
||||
]
|
||||
|
||||
|
||||
class FileForm(forms.Form):
|
||||
file = forms.FileField()
|
||||
|
||||
|
||||
class PasswordAndKeyAuthForm(forms.ModelForm):
|
||||
# Form field name can not start with `_`, so redefine it,
|
||||
password = forms.CharField(
|
||||
widget=forms.PasswordInput, max_length=128,
|
||||
strip=True, required=False,
|
||||
help_text=_('Password or private key passphrase'),
|
||||
label=_("Password"),
|
||||
)
|
||||
# Need use upload private key file except paste private key content
|
||||
private_key = forms.FileField(required=False, label=_("Private key"))
|
||||
|
||||
def clean_private_key(self):
|
||||
private_key_f = self.cleaned_data['private_key']
|
||||
password = self.cleaned_data['password']
|
||||
|
||||
if private_key_f:
|
||||
key_string = private_key_f.read()
|
||||
private_key_f.seek(0)
|
||||
key_string = key_string.decode()
|
||||
|
||||
if not validate_ssh_private_key(key_string, password):
|
||||
msg = _('Invalid private key, Only support '
|
||||
'RSA/DSA format key')
|
||||
raise forms.ValidationError(msg)
|
||||
return private_key_f
|
||||
|
||||
def validate_password_key(self):
|
||||
password = self.cleaned_data['password']
|
||||
private_key_f = self.cleaned_data.get('private_key', '')
|
||||
|
||||
if not password and not private_key_f:
|
||||
raise forms.ValidationError(_(
|
||||
'Password and private key file must be input one'
|
||||
))
|
||||
|
||||
def gen_keys(self):
|
||||
password = self.cleaned_data.get('password', '') or None
|
||||
private_key_f = self.cleaned_data['private_key']
|
||||
public_key = private_key = None
|
||||
|
||||
if private_key_f:
|
||||
private_key = private_key_f.read().strip().decode('utf-8')
|
||||
public_key = ssh_pubkey_gen(private_key=private_key, password=password)
|
||||
return private_key, public_key
|
||||
|
||||
|
||||
class AdminUserForm(PasswordAndKeyAuthForm):
|
||||
def save(self, commit=True):
|
||||
raise forms.ValidationError("Use api to save")
|
||||
|
||||
class Meta:
|
||||
model = AdminUser
|
||||
fields = ['name', 'username', 'password', 'private_key', 'comment']
|
||||
widgets = {
|
||||
'name': forms.TextInput(attrs={'placeholder': _('Name')}),
|
||||
'username': forms.TextInput(attrs={'placeholder': _('Username')}),
|
||||
}
|
||||
|
||||
|
||||
class SystemUserForm(OrgModelForm, PasswordAndKeyAuthForm):
|
||||
# Admin user assets define, let user select, save it in form not in view
|
||||
auto_generate_key = forms.BooleanField(initial=True, required=False)
|
||||
|
||||
def save(self, commit=True):
|
||||
raise forms.ValidationError("Use api to save")
|
||||
|
||||
class Meta:
|
||||
model = SystemUser
|
||||
fields = [
|
||||
'name', 'username', 'protocol', 'auto_generate_key',
|
||||
'password', 'private_key', 'auto_push', 'sudo',
|
||||
'username_same_with_user',
|
||||
'comment', 'shell', 'priority', 'login_mode', 'cmd_filters',
|
||||
'sftp_root',
|
||||
]
|
||||
widgets = {
|
||||
'name': forms.TextInput(attrs={'placeholder': _('Name')}),
|
||||
'username': forms.TextInput(attrs={'placeholder': _('Username')}),
|
||||
'cmd_filters': forms.SelectMultiple(attrs={
|
||||
'class': 'select2', 'data-placeholder': _('Command filter')
|
||||
}),
|
||||
}
|
||||
labels = {
|
||||
'username_same_with_user': _("Username same with user"),
|
||||
}
|
||||
help_texts = {
|
||||
'auto_push': _('Auto push system user to asset'),
|
||||
'priority': _('1-100, High level will be using login asset as default, '
|
||||
'if user was granted more than 2 system user'),
|
||||
'login_mode': _('If you choose manual login mode, you do not '
|
||||
'need to fill in the username and password.'),
|
||||
'sudo': _("Use comma split multi command, ex: /bin/whoami,/bin/ifconfig"),
|
||||
'sftp_root': _("SFTP root dir, tmp, home or custom"),
|
||||
'username_same_with_user': _("Username is dynamic, When connect asset, using current user's username"),
|
||||
# 'username_same_with_user': _("用户名是动态的,登录资产时使用当前用户的用户名登录"),
|
||||
}
|
||||
18
apps/assets/migrations/0050_auto_20200711_1740.py
Normal file
18
apps/assets/migrations/0050_auto_20200711_1740.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 2.2.10 on 2020-07-11 09:40
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0049_systemuser_sftp_root'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='asset',
|
||||
name='created_by',
|
||||
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'),
|
||||
),
|
||||
]
|
||||
22
apps/assets/migrations/0051_auto_20200713_1143.py
Normal file
22
apps/assets/migrations/0051_auto_20200713_1143.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# Generated by Django 2.2.10 on 2020-07-13 03:43
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0050_auto_20200711_1740'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='domain',
|
||||
name='name',
|
||||
field=models.CharField(max_length=128, verbose_name='Name'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='domain',
|
||||
unique_together={('org_id', 'name')},
|
||||
),
|
||||
]
|
||||
22
apps/assets/migrations/0052_auto_20200715_1535.py
Normal file
22
apps/assets/migrations/0052_auto_20200715_1535.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# Generated by Django 2.2.10 on 2020-07-15 07:35
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0051_auto_20200713_1143'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='commandfilter',
|
||||
name='name',
|
||||
field=models.CharField(max_length=64, verbose_name='Name'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='commandfilter',
|
||||
unique_together={('org_id', 'name')},
|
||||
),
|
||||
]
|
||||
34
apps/assets/migrations/0053_auto_20200723_1232.py
Normal file
34
apps/assets/migrations/0053_auto_20200723_1232.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# Generated by Django 2.2.10 on 2020-07-23 04:32
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0052_auto_20200715_1535'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='adminuser',
|
||||
name='username',
|
||||
field=models.CharField(blank=True, db_index=True, max_length=128, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='authbook',
|
||||
name='username',
|
||||
field=models.CharField(blank=True, db_index=True, max_length=128, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='gateway',
|
||||
name='username',
|
||||
field=models.CharField(blank=True, db_index=True, max_length=128, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='systemuser',
|
||||
name='username',
|
||||
field=models.CharField(blank=True, db_index=True, max_length=128, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
|
||||
),
|
||||
]
|
||||
23
apps/assets/migrations/0054_auto_20200807_1032.py
Normal file
23
apps/assets/migrations/0054_auto_20200807_1032.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 2.2.13 on 2020-08-07 02:32
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0053_auto_20200723_1232'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='systemuser',
|
||||
name='token',
|
||||
field=models.TextField(default='', verbose_name='Token'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='systemuser',
|
||||
name='protocol',
|
||||
field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp'), ('telnet', 'telnet'), ('vnc', 'vnc'), ('mysql', 'mysql'), ('k8s', 'k8s')], default='ssh', max_length=16, verbose_name='Protocol'),
|
||||
),
|
||||
]
|
||||
23
apps/assets/migrations/0055_auto_20200811_1845.py
Normal file
23
apps/assets/migrations/0055_auto_20200811_1845.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 2.2.13 on 2020-08-11 10:45
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0054_auto_20200807_1032'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='systemuser',
|
||||
name='home',
|
||||
field=models.CharField(blank=True, default='', max_length=4096, verbose_name='Home'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='systemuser',
|
||||
name='system_groups',
|
||||
field=models.CharField(blank=True, default='', max_length=4096, verbose_name='System groups'),
|
||||
),
|
||||
]
|
||||
23
apps/assets/migrations/0056_auto_20200904_1751.py
Normal file
23
apps/assets/migrations/0056_auto_20200904_1751.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 2.2.13 on 2020-09-04 09:51
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0055_auto_20200811_1845'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='node',
|
||||
name='assets_amount',
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='node',
|
||||
name='parent_key',
|
||||
field=models.CharField(db_index=True, default='', max_length=64, verbose_name='Parent key'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,38 @@
|
||||
# Generated by Django 2.2.13 on 2020-08-21 08:20
|
||||
|
||||
from django.db import migrations
|
||||
from django.db.models import Q
|
||||
|
||||
|
||||
def fill_node_value(apps, schema_editor):
|
||||
Node = apps.get_model('assets', 'Node')
|
||||
Asset = apps.get_model('assets', 'Asset')
|
||||
node_queryset = Node.objects.all()
|
||||
node_amount = node_queryset.count()
|
||||
width = len(str(node_amount))
|
||||
print('\n')
|
||||
for i, node in enumerate(node_queryset):
|
||||
print(f'\t{i+1:0>{width}}/{node_amount} compute node[{node.key}]`s assets_amount ...')
|
||||
assets_amount = Asset.objects.filter(
|
||||
Q(nodes__key__istartswith=f'{node.key}:') | Q(nodes=node)
|
||||
).distinct().count()
|
||||
key = node.key
|
||||
try:
|
||||
parent_key = key[:key.rindex(':')]
|
||||
except ValueError:
|
||||
parent_key = ''
|
||||
node.assets_amount = assets_amount
|
||||
node.parent_key = parent_key
|
||||
node.save()
|
||||
print(' ' + '.'*65, end='')
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0056_auto_20200904_1751'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(fill_node_value)
|
||||
]
|
||||
@@ -47,6 +47,10 @@ class AssetManager(OrgManager):
|
||||
)
|
||||
|
||||
|
||||
class AssetOrgManager(OrgManager):
|
||||
pass
|
||||
|
||||
|
||||
class AssetQuerySet(models.QuerySet):
|
||||
def active(self):
|
||||
return self.filter(is_active=True)
|
||||
@@ -221,11 +225,12 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
|
||||
hostname_raw = models.CharField(max_length=128, blank=True, null=True, verbose_name=_('Hostname raw'))
|
||||
|
||||
labels = models.ManyToManyField('assets.Label', blank=True, related_name='assets', verbose_name=_("Labels"))
|
||||
created_by = models.CharField(max_length=32, null=True, blank=True, verbose_name=_('Created by'))
|
||||
created_by = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Created by'))
|
||||
date_created = models.DateTimeField(auto_now_add=True, null=True, blank=True, verbose_name=_('Date created'))
|
||||
comment = models.TextField(max_length=128, default='', blank=True, verbose_name=_('Comment'))
|
||||
|
||||
objects = AssetManager.from_queryset(AssetQuerySet)()
|
||||
org_objects = AssetOrgManager.from_queryset(AssetQuerySet)()
|
||||
_connectivity = None
|
||||
|
||||
def __str__(self):
|
||||
@@ -350,36 +355,4 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
|
||||
class Meta:
|
||||
unique_together = [('org_id', 'hostname')]
|
||||
verbose_name = _("Asset")
|
||||
|
||||
@classmethod
|
||||
def generate_fake(cls, count=100):
|
||||
from .user import AdminUser, SystemUser
|
||||
from random import seed, choice
|
||||
from django.db import IntegrityError
|
||||
from .node import Node
|
||||
from orgs.utils import get_current_org
|
||||
from orgs.models import Organization
|
||||
org = get_current_org()
|
||||
if not org or not org.is_real():
|
||||
Organization.default().change_to()
|
||||
|
||||
nodes = list(Node.objects.all())
|
||||
seed()
|
||||
for i in range(count):
|
||||
ip = [str(i) for i in random.sample(range(255), 4)]
|
||||
asset = cls(ip='.'.join(ip),
|
||||
hostname='.'.join(ip),
|
||||
admin_user=choice(AdminUser.objects.all()),
|
||||
created_by='Fake')
|
||||
try:
|
||||
asset.save()
|
||||
asset.protocols = 'ssh/22'
|
||||
if nodes and len(nodes) > 3:
|
||||
_nodes = random.sample(nodes, 3)
|
||||
else:
|
||||
_nodes = [Node.default_node()]
|
||||
asset.nodes.set(_nodes)
|
||||
logger.debug('Generate fake asset : %s' % asset.ip)
|
||||
except IntegrityError:
|
||||
print('Error continue')
|
||||
continue
|
||||
ordering = ['-date_created']
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
from django.db import models, transaction
|
||||
from django.db.models import Max
|
||||
from django.core.cache import cache
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orgs.mixins.models import OrgManager
|
||||
@@ -59,19 +58,17 @@ class AuthBook(BaseUser):
|
||||
"""
|
||||
username = kwargs['username']
|
||||
asset = kwargs['asset']
|
||||
key_lock = 'KEY_LOCK_CREATE_AUTH_BOOK_{}_{}'.format(username, asset.id)
|
||||
with cache.lock(key_lock):
|
||||
with transaction.atomic():
|
||||
cls.objects.filter(
|
||||
username=username, asset=asset, is_latest=True
|
||||
).update(is_latest=False)
|
||||
max_version = cls.get_max_version(username, asset)
|
||||
kwargs.update({
|
||||
'version': max_version + 1,
|
||||
'is_latest': True
|
||||
})
|
||||
obj = cls.objects.create(**kwargs)
|
||||
return obj
|
||||
with transaction.atomic():
|
||||
# 使用select_for_update限制并发创建相同的username、asset条目
|
||||
instances = cls.objects.select_for_update().filter(username=username, asset=asset)
|
||||
instances.filter(is_latest=True).update(is_latest=False)
|
||||
max_version = cls.get_max_version(username, asset)
|
||||
kwargs.update({
|
||||
'version': max_version + 1,
|
||||
'is_latest': True
|
||||
})
|
||||
obj = cls.objects.create(**kwargs)
|
||||
return obj
|
||||
|
||||
@property
|
||||
def connectivity(self):
|
||||
|
||||
@@ -230,7 +230,7 @@ class AuthMixin:
|
||||
class BaseUser(OrgModelMixin, AuthMixin, ConnectivityMixin):
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
name = models.CharField(max_length=128, verbose_name=_('Name'))
|
||||
username = models.CharField(max_length=32, blank=True, verbose_name=_('Username'), validators=[alphanumeric], db_index=True)
|
||||
username = models.CharField(max_length=128, blank=True, verbose_name=_('Username'), validators=[alphanumeric], db_index=True)
|
||||
password = fields.EncryptCharField(max_length=256, blank=True, null=True, verbose_name=_('Password'))
|
||||
private_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH private key'))
|
||||
public_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH public key'))
|
||||
|
||||
@@ -38,27 +38,3 @@ class Cluster(models.Model):
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
verbose_name = _("Cluster")
|
||||
|
||||
@classmethod
|
||||
def generate_fake(cls, count=5):
|
||||
from random import seed, choice
|
||||
import forgery_py
|
||||
from django.db import IntegrityError
|
||||
|
||||
seed()
|
||||
for i in range(count):
|
||||
cluster = cls(name=forgery_py.name.full_name(),
|
||||
bandwidth='200M',
|
||||
contact=forgery_py.name.full_name(),
|
||||
phone=forgery_py.address.phone(),
|
||||
address=forgery_py.address.city() + forgery_py.address.street_address(),
|
||||
# operator=choice(['北京联通', '北京电信', 'BGP全网通']),
|
||||
operator=choice([_('Beijing unicom'), _('Beijing telecom'), _('BGP full netcom')]),
|
||||
comment=forgery_py.lorem_ipsum.sentence(),
|
||||
created_by='Fake')
|
||||
try:
|
||||
cluster.save()
|
||||
logger.debug('Generate fake asset group: %s' % cluster.name)
|
||||
except IntegrityError:
|
||||
print('Error continue')
|
||||
continue
|
||||
|
||||
@@ -18,7 +18,7 @@ __all__ = [
|
||||
|
||||
class CommandFilter(OrgModelMixin):
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
name = models.CharField(max_length=64, unique=True, verbose_name=_("Name"))
|
||||
name = models.CharField(max_length=64, verbose_name=_("Name"))
|
||||
is_active = models.BooleanField(default=True, verbose_name=_('Is active'))
|
||||
comment = models.TextField(blank=True, default='', verbose_name=_("Comment"))
|
||||
date_created = models.DateTimeField(auto_now_add=True)
|
||||
@@ -29,6 +29,7 @@ class CommandFilter(OrgModelMixin):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
unique_together = [('org_id', 'name')]
|
||||
verbose_name = _("Command filter")
|
||||
|
||||
|
||||
|
||||
@@ -17,13 +17,14 @@ __all__ = ['Domain', 'Gateway']
|
||||
|
||||
class Domain(OrgModelMixin):
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
name = models.CharField(max_length=128, unique=True, verbose_name=_('Name'))
|
||||
name = models.CharField(max_length=128, verbose_name=_('Name'))
|
||||
comment = models.TextField(blank=True, verbose_name=_('Comment'))
|
||||
date_created = models.DateTimeField(auto_now_add=True, null=True,
|
||||
verbose_name=_('Date created'))
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Domain")
|
||||
unique_together = [('org_id', 'name')]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@@ -18,3 +18,11 @@ class FavoriteAsset(CommonModelMixin):
|
||||
@classmethod
|
||||
def get_user_favorite_assets_id(cls, user):
|
||||
return cls.objects.filter(user=user).values_list('asset', flat=True)
|
||||
|
||||
@classmethod
|
||||
def get_user_favorite_assets(cls, user):
|
||||
from assets.models import Asset
|
||||
from perms.utils.user_asset_permission import get_user_granted_all_assets
|
||||
asset_ids = get_user_granted_all_assets(user).values_list('id', flat=True)
|
||||
query_name = cls.asset.field.related_query_name()
|
||||
return Asset.org_objects.filter(**{f'{query_name}__user_id': user.id}, id__in=asset_ids).distinct()
|
||||
|
||||
@@ -33,21 +33,3 @@ class AssetGroup(models.Model):
|
||||
def initial(cls):
|
||||
asset_group = cls(name=_('Default'), comment=_('Default asset group'))
|
||||
asset_group.save()
|
||||
|
||||
@classmethod
|
||||
def generate_fake(cls, count=100):
|
||||
from random import seed
|
||||
import forgery_py
|
||||
from django.db import IntegrityError
|
||||
|
||||
seed()
|
||||
for i in range(count):
|
||||
group = cls(name=forgery_py.name.full_name(),
|
||||
comment=forgery_py.lorem_ipsum.sentence(),
|
||||
created_by='Fake')
|
||||
try:
|
||||
group.save()
|
||||
logger.debug('Generate fake asset group: %s' % group.name)
|
||||
except IntegrityError:
|
||||
print('Error continue')
|
||||
continue
|
||||
|
||||
@@ -2,132 +2,35 @@
|
||||
#
|
||||
import uuid
|
||||
import re
|
||||
import time
|
||||
|
||||
from django.db import models, transaction
|
||||
from django.db.models import Q
|
||||
from django.db.utils import IntegrityError
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import ugettext
|
||||
from django.core.cache import cache
|
||||
from django.db.transaction import atomic
|
||||
|
||||
from common.utils import get_logger, lazyproperty
|
||||
from common.utils import get_logger
|
||||
from common.utils.common import lazyproperty
|
||||
from orgs.mixins.models import OrgModelMixin, OrgManager
|
||||
from orgs.utils import get_current_org, tmp_to_org, current_org
|
||||
from orgs.models import Organization
|
||||
|
||||
|
||||
__all__ = ['Node']
|
||||
__all__ = ['Node', 'FamilyMixin', 'compute_parent_key']
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def compute_parent_key(key):
|
||||
try:
|
||||
return key[:key.rindex(':')]
|
||||
except ValueError:
|
||||
return ''
|
||||
|
||||
|
||||
class NodeQuerySet(models.QuerySet):
|
||||
def delete(self):
|
||||
raise PermissionError("Bulk delete node deny")
|
||||
|
||||
|
||||
class TreeCache:
|
||||
updated_time_cache_key = 'NODE_TREE_UPDATED_AT_{}'
|
||||
cache_time = 3600
|
||||
assets_updated_time_cache_key = 'NODE_TREE_ASSETS_UPDATED_AT_{}'
|
||||
|
||||
def __init__(self, tree, org_id):
|
||||
now = time.time()
|
||||
self.created_time = now
|
||||
self.assets_created_time = now
|
||||
self.tree = tree
|
||||
self.org_id = org_id
|
||||
|
||||
def _has_changed(self, tp="tree"):
|
||||
if tp == "assets":
|
||||
key = self.assets_updated_time_cache_key.format(self.org_id)
|
||||
else:
|
||||
key = self.updated_time_cache_key.format(self.org_id)
|
||||
updated_time = cache.get(key, 0)
|
||||
if updated_time > self.created_time:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def set_changed(cls, tp="tree", t=None, org_id=None):
|
||||
if org_id is None:
|
||||
org_id = current_org.id
|
||||
if tp == "assets":
|
||||
key = cls.assets_updated_time_cache_key.format(org_id)
|
||||
else:
|
||||
key = cls.updated_time_cache_key.format(org_id)
|
||||
ttl = cls.cache_time
|
||||
if not t:
|
||||
t = time.time()
|
||||
cache.set(key, t, ttl)
|
||||
|
||||
def tree_has_changed(self):
|
||||
return self._has_changed("tree")
|
||||
|
||||
def set_tree_changed(self, t=None):
|
||||
logger.debug("Set tree tree changed")
|
||||
self.__class__.set_changed(t=t, tp="tree")
|
||||
|
||||
def assets_has_changed(self):
|
||||
return self._has_changed("assets")
|
||||
|
||||
def set_tree_assets_changed(self, t=None):
|
||||
logger.debug("Set tree assets changed")
|
||||
self.__class__.set_changed(t=t, tp="assets")
|
||||
|
||||
def get(self):
|
||||
if self.tree_has_changed():
|
||||
self.renew()
|
||||
return self.tree
|
||||
if self.assets_has_changed():
|
||||
self.tree.init_assets()
|
||||
return self.tree
|
||||
|
||||
def renew(self):
|
||||
new_obj = self.__class__.new(self.org_id)
|
||||
self.tree = new_obj.tree
|
||||
self.created_time = new_obj.created_time
|
||||
self.assets_created_time = new_obj.assets_created_time
|
||||
|
||||
@classmethod
|
||||
def new(cls, org_id=None):
|
||||
from ..utils import TreeService
|
||||
logger.debug("Create node tree")
|
||||
if not org_id:
|
||||
org_id = current_org.id
|
||||
with tmp_to_org(org_id):
|
||||
tree = TreeService.new()
|
||||
obj = cls(tree, org_id)
|
||||
obj.tree = tree
|
||||
return obj
|
||||
|
||||
|
||||
class TreeMixin:
|
||||
_org_tree_map = {}
|
||||
|
||||
@classmethod
|
||||
def tree(cls):
|
||||
org_id = current_org.org_id()
|
||||
t = cls.get_local_tree_cache(org_id)
|
||||
|
||||
if t is None:
|
||||
t = TreeCache.new()
|
||||
cls._org_tree_map[org_id] = t
|
||||
return t.get()
|
||||
|
||||
@classmethod
|
||||
def get_local_tree_cache(cls, org_id=None):
|
||||
t = cls._org_tree_map.get(org_id)
|
||||
return t
|
||||
|
||||
@classmethod
|
||||
def refresh_tree(cls, t=None):
|
||||
TreeCache.set_changed(tp="tree", t=t, org_id=current_org.id)
|
||||
|
||||
@classmethod
|
||||
def refresh_node_assets(cls, t=None):
|
||||
TreeCache.set_changed(tp="assets", t=t, org_id=current_org.id)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class FamilyMixin:
|
||||
@@ -138,16 +41,16 @@ class FamilyMixin:
|
||||
|
||||
@staticmethod
|
||||
def clean_children_keys(nodes_keys):
|
||||
nodes_keys = sorted(list(nodes_keys), key=lambda x: (len(x), x))
|
||||
sort_key = lambda k: [int(i) for i in k.split(':')]
|
||||
nodes_keys = sorted(list(nodes_keys), key=sort_key)
|
||||
|
||||
nodes_keys_clean = []
|
||||
for key in nodes_keys[::-1]:
|
||||
found = False
|
||||
for k in nodes_keys:
|
||||
if key.startswith(k + ':'):
|
||||
found = True
|
||||
break
|
||||
if not found:
|
||||
nodes_keys_clean.append(key)
|
||||
base_key = ''
|
||||
for key in nodes_keys:
|
||||
if key.startswith(base_key + ':'):
|
||||
continue
|
||||
nodes_keys_clean.append(key)
|
||||
base_key = key
|
||||
return nodes_keys_clean
|
||||
|
||||
@classmethod
|
||||
@@ -175,13 +78,16 @@ class FamilyMixin:
|
||||
return re.match(children_pattern, self.key)
|
||||
|
||||
def get_children(self, with_self=False):
|
||||
pattern = self.get_children_key_pattern(with_self=with_self)
|
||||
return Node.objects.filter(key__regex=pattern)
|
||||
q = Q(parent_key=self.key)
|
||||
if with_self:
|
||||
q |= Q(key=self.key)
|
||||
return Node.objects.filter(q)
|
||||
|
||||
def get_all_children(self, with_self=False):
|
||||
pattern = self.get_all_children_pattern(with_self=with_self)
|
||||
children = Node.objects.filter(key__regex=pattern)
|
||||
return children
|
||||
q = Q(key__istartswith=f'{self.key}:')
|
||||
if with_self:
|
||||
q |= Q(key=self.key)
|
||||
return Node.objects.filter(q)
|
||||
|
||||
@property
|
||||
def children(self):
|
||||
@@ -191,14 +97,30 @@ class FamilyMixin:
|
||||
def all_children(self):
|
||||
return self.get_all_children(with_self=False)
|
||||
|
||||
def create_child(self, value, _id=None):
|
||||
with transaction.atomic():
|
||||
def create_child(self, value=None, _id=None):
|
||||
with atomic(savepoint=False):
|
||||
child_key = self.get_next_child_key()
|
||||
if value is None:
|
||||
value = child_key
|
||||
child = self.__class__.objects.create(
|
||||
id=_id, key=child_key, value=value
|
||||
id=_id, key=child_key, value=value, parent_key=self.key,
|
||||
)
|
||||
return child
|
||||
|
||||
def get_or_create_child(self, value, _id=None):
|
||||
"""
|
||||
:return: Node, bool (created)
|
||||
"""
|
||||
children = self.get_children()
|
||||
exist = children.filter(value=value).exists()
|
||||
if exist:
|
||||
child = children.filter(value=value).first()
|
||||
created = False
|
||||
else:
|
||||
child = self.create_child(value, _id)
|
||||
created = True
|
||||
return child, created
|
||||
|
||||
def get_next_child_key(self):
|
||||
mark = self.child_mark
|
||||
self.child_mark += 1
|
||||
@@ -241,10 +163,13 @@ class FamilyMixin:
|
||||
ancestor_keys = self.get_ancestor_keys(with_self=with_self)
|
||||
return self.__class__.objects.filter(key__in=ancestor_keys)
|
||||
|
||||
@property
|
||||
def parent_key(self):
|
||||
parent_key = ":".join(self.key.split(":")[:-1])
|
||||
return parent_key
|
||||
# @property
|
||||
# def parent_key(self):
|
||||
# parent_key = ":".join(self.key.split(":")[:-1])
|
||||
# return parent_key
|
||||
|
||||
def compute_parent_key(self):
|
||||
return compute_parent_key(self.key)
|
||||
|
||||
def is_parent(self, other):
|
||||
return other.is_children(self)
|
||||
@@ -286,39 +211,33 @@ class FamilyMixin:
|
||||
return [*tuple(ancestors), self, *tuple(children)]
|
||||
|
||||
|
||||
class FullValueMixin:
|
||||
key = ''
|
||||
|
||||
@lazyproperty
|
||||
def full_value(self):
|
||||
if self.is_org_root():
|
||||
return self.value
|
||||
value = self.tree().get_node_full_tag(self.key)
|
||||
return value
|
||||
|
||||
|
||||
class NodeAssetsMixin:
|
||||
key = ''
|
||||
id = None
|
||||
|
||||
@lazyproperty
|
||||
def assets_amount(self):
|
||||
amount = self.tree().assets_amount(self.key)
|
||||
return amount
|
||||
|
||||
def get_all_assets(self):
|
||||
from .asset import Asset
|
||||
if self.is_org_root():
|
||||
return Asset.objects.filter(org_id=self.org_id)
|
||||
pattern = '^{0}$|^{0}:'.format(self.key)
|
||||
return Asset.objects.filter(nodes__key__regex=pattern).distinct()
|
||||
q = Q(nodes__key__startswith=f'{self.key}:') | Q(nodes__key=self.key)
|
||||
return Asset.objects.filter(q).distinct()
|
||||
|
||||
@classmethod
|
||||
def get_node_all_assets_by_key_v2(cls, key):
|
||||
# 最初的写法是:
|
||||
# Asset.objects.filter(Q(nodes__key__startswith=f'{node.key}:') | Q(nodes__id=node.id))
|
||||
# 可是 startswith 会导致表关联时 Asset 索引失效
|
||||
from .asset import Asset
|
||||
node_ids = cls.objects.filter(
|
||||
Q(key__startswith=f'{key}:') |
|
||||
Q(key=key)
|
||||
).values_list('id', flat=True).distinct()
|
||||
assets = Asset.objects.filter(
|
||||
nodes__id__in=list(node_ids)
|
||||
).distinct()
|
||||
return assets
|
||||
|
||||
def get_assets(self):
|
||||
from .asset import Asset
|
||||
if self.is_org_root():
|
||||
assets = Asset.objects.filter(Q(nodes=self) | Q(nodes__isnull=True))
|
||||
else:
|
||||
assets = Asset.objects.filter(nodes=self)
|
||||
assets = Asset.objects.filter(nodes=self)
|
||||
return assets.distinct()
|
||||
|
||||
def get_valid_assets(self):
|
||||
@@ -327,51 +246,54 @@ class NodeAssetsMixin:
|
||||
def get_all_valid_assets(self):
|
||||
return self.get_all_assets().valid()
|
||||
|
||||
@classmethod
|
||||
def _get_nodes_all_assets(cls, nodes_keys):
|
||||
"""
|
||||
当节点比较多的时候,这种正则方式性能差极了
|
||||
:param nodes_keys:
|
||||
:return:
|
||||
"""
|
||||
from .asset import Asset
|
||||
nodes_keys = cls.clean_children_keys(nodes_keys)
|
||||
nodes_children_pattern = set()
|
||||
for key in nodes_keys:
|
||||
children_pattern = cls.get_node_all_children_key_pattern(key)
|
||||
nodes_children_pattern.add(children_pattern)
|
||||
pattern = '|'.join(nodes_children_pattern)
|
||||
return Asset.objects.filter(nodes__key__regex=pattern).distinct()
|
||||
|
||||
@classmethod
|
||||
def get_nodes_all_assets_ids(cls, nodes_keys):
|
||||
nodes_keys = cls.clean_children_keys(nodes_keys)
|
||||
assets_ids = set()
|
||||
for key in nodes_keys:
|
||||
node_assets_ids = cls.tree().all_assets(key)
|
||||
assets_ids.update(set(node_assets_ids))
|
||||
assets_ids = cls.get_nodes_all_assets(nodes_keys).values_list('id', flat=True)
|
||||
return assets_ids
|
||||
|
||||
@classmethod
|
||||
def get_nodes_all_assets(cls, nodes_keys, extra_assets_ids=None):
|
||||
from .asset import Asset
|
||||
nodes_keys = cls.clean_children_keys(nodes_keys)
|
||||
assets_ids = cls.get_nodes_all_assets_ids(nodes_keys)
|
||||
q = Q()
|
||||
node_ids = ()
|
||||
for key in nodes_keys:
|
||||
q |= Q(key__startswith=f'{key}:')
|
||||
q |= Q(key=key)
|
||||
if q:
|
||||
node_ids = Node.objects.filter(q).distinct().values_list('id', flat=True)
|
||||
|
||||
q = Q(nodes__id__in=list(node_ids))
|
||||
if extra_assets_ids:
|
||||
assets_ids.update(set(extra_assets_ids))
|
||||
return Asset.objects.filter(id__in=assets_ids)
|
||||
q |= Q(id__in=extra_assets_ids)
|
||||
if q:
|
||||
return Asset.org_objects.filter(q).distinct()
|
||||
else:
|
||||
return Asset.objects.none()
|
||||
|
||||
|
||||
class SomeNodesMixin:
|
||||
key = ''
|
||||
default_key = '1'
|
||||
default_value = 'Default'
|
||||
ungrouped_key = '-10'
|
||||
ungrouped_value = _('ungrouped')
|
||||
empty_key = '-11'
|
||||
empty_value = _("empty")
|
||||
favorite_key = '-12'
|
||||
favorite_value = _("favorite")
|
||||
|
||||
@classmethod
|
||||
def default_node(cls):
|
||||
with tmp_to_org(Organization.default()):
|
||||
defaults = {'value': cls.default_value}
|
||||
try:
|
||||
obj, created = cls.objects.get_or_create(
|
||||
defaults=defaults, key=cls.default_key,
|
||||
)
|
||||
except IntegrityError as e:
|
||||
logger.error("Create default node failed: {}".format(e))
|
||||
cls.modify_other_org_root_node_key()
|
||||
obj, created = cls.objects.get_or_create(
|
||||
defaults=defaults, key=cls.default_key,
|
||||
)
|
||||
return obj
|
||||
|
||||
def is_default_node(self):
|
||||
return self.key == self.default_key
|
||||
@@ -406,51 +328,15 @@ class SomeNodesMixin:
|
||||
|
||||
@classmethod
|
||||
def org_root(cls):
|
||||
root = cls.objects.filter(key__regex=r'^[0-9]+$')
|
||||
root = cls.objects.filter(parent_key='').exclude(key__startswith='-')
|
||||
if root:
|
||||
return root[0]
|
||||
else:
|
||||
return cls.create_org_root_node()
|
||||
|
||||
@classmethod
|
||||
def ungrouped_node(cls):
|
||||
with tmp_to_org(Organization.system()):
|
||||
defaults = {'value': cls.ungrouped_value}
|
||||
obj, created = cls.objects.get_or_create(
|
||||
defaults=defaults, key=cls.ungrouped_key
|
||||
)
|
||||
return obj
|
||||
|
||||
@classmethod
|
||||
def default_node(cls):
|
||||
with tmp_to_org(Organization.default()):
|
||||
defaults = {'value': cls.default_value}
|
||||
try:
|
||||
obj, created = cls.objects.get_or_create(
|
||||
defaults=defaults, key=cls.default_key,
|
||||
)
|
||||
except IntegrityError as e:
|
||||
logger.error("Create default node failed: {}".format(e))
|
||||
cls.modify_other_org_root_node_key()
|
||||
obj, created = cls.objects.get_or_create(
|
||||
defaults=defaults, key=cls.default_key,
|
||||
)
|
||||
return obj
|
||||
|
||||
@classmethod
|
||||
def favorite_node(cls):
|
||||
with tmp_to_org(Organization.system()):
|
||||
defaults = {'value': cls.favorite_value}
|
||||
obj, created = cls.objects.get_or_create(
|
||||
defaults=defaults, key=cls.favorite_key
|
||||
)
|
||||
return obj
|
||||
|
||||
@classmethod
|
||||
def initial_some_nodes(cls):
|
||||
cls.default_node()
|
||||
cls.ungrouped_node()
|
||||
cls.favorite_node()
|
||||
|
||||
@classmethod
|
||||
def modify_other_org_root_node_key(cls):
|
||||
@@ -482,12 +368,15 @@ class SomeNodesMixin:
|
||||
logger.info('Modify key ( {} > {} )'.format(old_key, new_key))
|
||||
|
||||
|
||||
class Node(OrgModelMixin, SomeNodesMixin, TreeMixin, FamilyMixin, FullValueMixin, NodeAssetsMixin):
|
||||
class Node(OrgModelMixin, SomeNodesMixin, FamilyMixin, NodeAssetsMixin):
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
key = models.CharField(unique=True, max_length=64, verbose_name=_("Key")) # '1:1:1:1'
|
||||
value = models.CharField(max_length=128, verbose_name=_("Value"))
|
||||
child_mark = models.IntegerField(default=0)
|
||||
date_create = models.DateTimeField(auto_now_add=True)
|
||||
parent_key = models.CharField(max_length=64, verbose_name=_("Parent key"),
|
||||
db_index=True, default='')
|
||||
assets_amount = models.IntegerField(default=0)
|
||||
|
||||
objects = OrgManager.from_queryset(NodeQuerySet)()
|
||||
is_node = True
|
||||
@@ -522,18 +411,20 @@ class Node(OrgModelMixin, SomeNodesMixin, TreeMixin, FamilyMixin, FullValueMixin
|
||||
def name(self):
|
||||
return self.value
|
||||
|
||||
@lazyproperty
|
||||
def full_value(self):
|
||||
# 不要在列表中调用该属性
|
||||
values = self.__class__.objects.filter(
|
||||
key__in=self.get_ancestor_keys()
|
||||
).values_list('key', 'value')
|
||||
values = [v for k, v in sorted(values, key=lambda x: len(x[0]))]
|
||||
values.append(self.value)
|
||||
return ' / '.join(values)
|
||||
|
||||
@property
|
||||
def level(self):
|
||||
return len(self.key.split(':'))
|
||||
|
||||
@classmethod
|
||||
def refresh_nodes(cls):
|
||||
cls.refresh_tree()
|
||||
|
||||
@classmethod
|
||||
def refresh_assets(cls):
|
||||
cls.refresh_node_assets()
|
||||
|
||||
def as_tree_node(self):
|
||||
from common.tree import TreeNode
|
||||
name = '{} ({})'.format(self.value, self.assets_amount)
|
||||
@@ -567,20 +458,3 @@ class Node(OrgModelMixin, SomeNodesMixin, TreeMixin, FamilyMixin, FullValueMixin
|
||||
if self.has_children_or_has_assets():
|
||||
return
|
||||
return super().delete(using=using, keep_parents=keep_parents)
|
||||
|
||||
@classmethod
|
||||
def generate_fake(cls, count=100):
|
||||
import random
|
||||
org = get_current_org()
|
||||
if not org or not org.is_real():
|
||||
Organization.default().change_to()
|
||||
nodes = list(cls.objects.all())
|
||||
if count > 100:
|
||||
length = 100
|
||||
else:
|
||||
length = count
|
||||
|
||||
for i in range(length):
|
||||
node = random.choice(nodes)
|
||||
child = node.create_child('Node {}'.format(i))
|
||||
print("{}. {}".format(i, child))
|
||||
|
||||
@@ -9,6 +9,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
|
||||
from common.utils import signer
|
||||
from common.fields.model import JsonListCharField
|
||||
from .base import BaseUser
|
||||
from .asset import Asset
|
||||
|
||||
@@ -64,26 +65,6 @@ class AdminUser(BaseUser):
|
||||
unique_together = [('name', 'org_id')]
|
||||
verbose_name = _("Admin user")
|
||||
|
||||
@classmethod
|
||||
def generate_fake(cls, count=10):
|
||||
from random import seed
|
||||
import forgery_py
|
||||
from django.db import IntegrityError
|
||||
|
||||
seed()
|
||||
for i in range(count):
|
||||
obj = cls(name=forgery_py.name.full_name(),
|
||||
username=forgery_py.internet.user_name(),
|
||||
password=forgery_py.lorem_ipsum.word(),
|
||||
comment=forgery_py.lorem_ipsum.sentence(),
|
||||
created_by='Fake')
|
||||
try:
|
||||
obj.save()
|
||||
logger.debug('Generate fake asset group: %s' % obj.name)
|
||||
except IntegrityError:
|
||||
print('Error continue')
|
||||
continue
|
||||
|
||||
|
||||
class SystemUser(BaseUser):
|
||||
PROTOCOL_SSH = 'ssh'
|
||||
@@ -91,12 +72,14 @@ class SystemUser(BaseUser):
|
||||
PROTOCOL_TELNET = 'telnet'
|
||||
PROTOCOL_VNC = 'vnc'
|
||||
PROTOCOL_MYSQL = 'mysql'
|
||||
PROTOCOL_K8S = 'k8s'
|
||||
PROTOCOL_CHOICES = (
|
||||
(PROTOCOL_SSH, 'ssh'),
|
||||
(PROTOCOL_RDP, 'rdp'),
|
||||
(PROTOCOL_TELNET, 'telnet'),
|
||||
(PROTOCOL_VNC, 'vnc'),
|
||||
(PROTOCOL_MYSQL, 'mysql'),
|
||||
(PROTOCOL_K8S, 'k8s'),
|
||||
)
|
||||
|
||||
LOGIN_AUTO = 'auto'
|
||||
@@ -118,6 +101,9 @@ class SystemUser(BaseUser):
|
||||
login_mode = models.CharField(choices=LOGIN_MODE_CHOICES, default=LOGIN_AUTO, max_length=10, verbose_name=_('Login mode'))
|
||||
cmd_filters = models.ManyToManyField('CommandFilter', related_name='system_users', verbose_name=_("Command filter"), blank=True)
|
||||
sftp_root = models.CharField(default='tmp', max_length=128, verbose_name=_("SFTP Root"))
|
||||
token = models.TextField(default='', verbose_name=_('Token'))
|
||||
home = models.CharField(max_length=4096, default='', verbose_name=_('Home'), blank=True)
|
||||
system_groups = models.CharField(default='', max_length=4096, verbose_name=_('System groups'), blank=True)
|
||||
_prefer = 'system_user'
|
||||
|
||||
def __str__(self):
|
||||
@@ -193,23 +179,3 @@ class SystemUser(BaseUser):
|
||||
ordering = ['name']
|
||||
unique_together = [('name', 'org_id')]
|
||||
verbose_name = _("System user")
|
||||
|
||||
@classmethod
|
||||
def generate_fake(cls, count=10):
|
||||
from random import seed
|
||||
import forgery_py
|
||||
from django.db import IntegrityError
|
||||
|
||||
seed()
|
||||
for i in range(count):
|
||||
obj = cls(name=forgery_py.name.full_name(),
|
||||
username=forgery_py.internet.user_name(),
|
||||
password=forgery_py.lorem_ipsum.word(),
|
||||
comment=forgery_py.lorem_ipsum.sentence(),
|
||||
created_by='Fake')
|
||||
try:
|
||||
obj.save()
|
||||
logger.debug('Generate fake asset group: %s' % obj.name)
|
||||
except IntegrityError:
|
||||
print('Error continue')
|
||||
continue
|
||||
|
||||
39
apps/assets/pagination.py
Normal file
39
apps/assets/pagination.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from rest_framework.pagination import LimitOffsetPagination
|
||||
from rest_framework.request import Request
|
||||
|
||||
from assets.models import Node
|
||||
|
||||
|
||||
class AssetLimitOffsetPagination(LimitOffsetPagination):
|
||||
"""
|
||||
需要与 `assets.api.mixin.FilterAssetByNodeMixin` 配合使用
|
||||
"""
|
||||
def get_count(self, queryset):
|
||||
"""
|
||||
1. 如果查询节点下的所有资产,那 count 使用 Node.assets_amount
|
||||
2. 如果有其他过滤条件使用 super
|
||||
3. 如果只查询该节点下的资产使用 super
|
||||
"""
|
||||
exclude_query_params = {
|
||||
self.limit_query_param,
|
||||
self.offset_query_param,
|
||||
'node', 'all', 'show_current_asset',
|
||||
'node_id', 'display', 'draw', 'fields_size',
|
||||
}
|
||||
|
||||
for k, v in self._request.query_params.items():
|
||||
if k not in exclude_query_params and v is not None:
|
||||
return super().get_count(queryset)
|
||||
|
||||
is_query_all = self._view.is_query_node_all_assets
|
||||
if is_query_all:
|
||||
node = self._view.node
|
||||
if not node:
|
||||
node = Node.org_root()
|
||||
return node.assets_amount
|
||||
return super().get_count(queryset)
|
||||
|
||||
def paginate_queryset(self, queryset, request: Request, view=None):
|
||||
self._request = request
|
||||
self._view = view
|
||||
return super().paginate_queryset(queryset, request, view=None)
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from rest_framework import serializers
|
||||
from django.db.models import Prefetch, F
|
||||
from django.db.models import Prefetch, F, Count
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
@@ -67,27 +67,43 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
|
||||
slug_field='name', queryset=Platform.objects.all(), label=_("Platform")
|
||||
)
|
||||
protocols = ProtocolsField(label=_('Protocols'), required=False)
|
||||
domain_display = serializers.ReadOnlyField(source='domain.name')
|
||||
admin_user_display = serializers.ReadOnlyField(source='admin_user.name')
|
||||
|
||||
"""
|
||||
资产的数据结构
|
||||
"""
|
||||
class Meta:
|
||||
model = Asset
|
||||
list_serializer_class = AdaptedBulkListSerializer
|
||||
fields = [
|
||||
'id', 'ip', 'hostname', 'protocol', 'port',
|
||||
'protocols', 'platform', 'is_active', 'public_ip', 'domain',
|
||||
'admin_user', 'nodes', 'labels', 'number', 'vendor', 'model', 'sn',
|
||||
'cpu_model', 'cpu_count', 'cpu_cores', 'cpu_vcpus', 'memory',
|
||||
'disk_total', 'disk_info', 'os', 'os_version', 'os_arch',
|
||||
'hostname_raw', 'comment', 'created_by', 'date_created',
|
||||
'hardware_info',
|
||||
fields_mini = ['id', 'hostname', 'ip']
|
||||
fields_small = fields_mini + [
|
||||
'protocol', 'port', 'protocols', 'is_active', 'public_ip',
|
||||
'number', 'vendor', 'model', 'sn', 'cpu_model', 'cpu_count',
|
||||
'cpu_cores', 'cpu_vcpus', 'memory', 'disk_total', 'disk_info',
|
||||
'os', 'os_version', 'os_arch', 'hostname_raw', 'comment',
|
||||
'created_by', 'date_created', 'hardware_info',
|
||||
]
|
||||
read_only_fields = (
|
||||
fields_fk = [
|
||||
'admin_user', 'admin_user_display', 'domain', 'domain_display', 'platform'
|
||||
]
|
||||
fk_only_fields = {
|
||||
'platform': ['name']
|
||||
}
|
||||
fields_m2m = [
|
||||
'nodes', 'labels',
|
||||
]
|
||||
annotates_fields = {
|
||||
# 'admin_user_display': 'admin_user__name'
|
||||
}
|
||||
fields_as = list(annotates_fields.keys())
|
||||
fields = fields_small + fields_fk + fields_m2m + fields_as
|
||||
read_only_fields = [
|
||||
'vendor', 'model', 'sn', 'cpu_model', 'cpu_count',
|
||||
'cpu_cores', 'cpu_vcpus', 'memory', 'disk_total', 'disk_info',
|
||||
'os', 'os_version', 'os_arch', 'hostname_raw',
|
||||
'created_by', 'date_created',
|
||||
)
|
||||
] + fields_as
|
||||
|
||||
extra_kwargs = {
|
||||
'protocol': {'write_only': True},
|
||||
'port': {'write_only': True},
|
||||
@@ -98,11 +114,8 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
|
||||
@classmethod
|
||||
def setup_eager_loading(cls, queryset):
|
||||
""" Perform necessary eager loading of data. """
|
||||
queryset = queryset.prefetch_related(
|
||||
Prefetch('nodes', queryset=Node.objects.all().only('id')),
|
||||
Prefetch('labels', queryset=Label.objects.all().only('id')),
|
||||
).select_related('admin_user', 'domain', 'platform') \
|
||||
.annotate(platform_base=F('platform__base'))
|
||||
queryset = queryset.select_related('admin_user', 'domain', 'platform')
|
||||
queryset = queryset.prefetch_related('nodes', 'labels')
|
||||
return queryset
|
||||
|
||||
def compatible_with_old_protocol(self, validated_data):
|
||||
@@ -134,19 +147,13 @@ class AssetDisplaySerializer(AssetSerializer):
|
||||
connectivity = ConnectivitySerializer(read_only=True, label=_("Connectivity"))
|
||||
|
||||
class Meta(AssetSerializer.Meta):
|
||||
fields = [
|
||||
'id', 'ip', 'hostname', 'protocol', 'port',
|
||||
'protocols', 'is_active', 'public_ip',
|
||||
'number', 'vendor', 'model', 'sn',
|
||||
'cpu_model', 'cpu_count', 'cpu_cores', 'cpu_vcpus', 'memory',
|
||||
'disk_total', 'disk_info', 'os', 'os_version', 'os_arch',
|
||||
'hostname_raw', 'comment', 'created_by', 'date_created',
|
||||
'hardware_info', 'connectivity',
|
||||
fields = AssetSerializer.Meta.fields + [
|
||||
'connectivity',
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def setup_eager_loading(cls, queryset):
|
||||
""" Perform necessary eager loading of data. """
|
||||
queryset = super().setup_eager_loading(queryset)
|
||||
queryset = queryset\
|
||||
.annotate(admin_user_username=F('admin_user__username'))
|
||||
return queryset
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
#
|
||||
import re
|
||||
from rest_framework import serializers
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from common.fields import ChoiceDisplayField
|
||||
from common.serializers import AdaptedBulkListSerializer
|
||||
@@ -27,11 +26,20 @@ class CommandFilterSerializer(BulkOrgResourceModelSerializer):
|
||||
|
||||
|
||||
class CommandFilterRuleSerializer(BulkOrgResourceModelSerializer):
|
||||
serializer_choice_field = ChoiceDisplayField
|
||||
# serializer_choice_field = ChoiceDisplayField
|
||||
invalid_pattern = re.compile(r'[\.\*\+\[\\\?\{\}\^\$\|\(\)\#\<\>]')
|
||||
type_display = serializers.ReadOnlyField(source='get_type_display')
|
||||
action_display = serializers.ReadOnlyField(source='get_action_display')
|
||||
|
||||
class Meta:
|
||||
model = CommandFilterRule
|
||||
fields_mini = ['id']
|
||||
fields_small = fields_mini + [
|
||||
'type', 'type_display', 'content', 'priority',
|
||||
'action', 'action_display',
|
||||
'comment', 'created_by', 'date_created', 'date_updated'
|
||||
]
|
||||
fields_fk = ['filter']
|
||||
fields = '__all__'
|
||||
list_serializer_class = AdaptedBulkListSerializer
|
||||
|
||||
|
||||
@@ -15,11 +15,18 @@ class DomainSerializer(BulkOrgResourceModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Domain
|
||||
fields = [
|
||||
'id', 'name', 'asset_count', 'gateway_count', 'comment', 'assets',
|
||||
'date_created'
|
||||
fields_mini = ['id', 'name']
|
||||
fields_small = fields_mini + [
|
||||
'comment', 'date_created'
|
||||
]
|
||||
read_only_fields = ( 'asset_count', 'gateway_count', 'date_created')
|
||||
fields_m2m = [
|
||||
'asset_count', 'assets', 'gateway_count',
|
||||
]
|
||||
fields = fields_small + fields_m2m
|
||||
read_only_fields = ('asset_count', 'gateway_count', 'date_created')
|
||||
extra_kwargs = {
|
||||
'assets': {'required': False}
|
||||
}
|
||||
list_serializer_class = AdaptedBulkListSerializer
|
||||
|
||||
@staticmethod
|
||||
@@ -41,6 +48,16 @@ class GatewaySerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
'date_updated', 'created_by', 'comment',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.protocol_limit_to_ssh()
|
||||
|
||||
def protocol_limit_to_ssh(self):
|
||||
protocol_field = self.fields['protocol']
|
||||
choices = protocol_field.choices
|
||||
choices.pop('rdp')
|
||||
protocol_field._choices = choices
|
||||
|
||||
|
||||
class GatewayWithAuthSerializer(GatewaySerializer):
|
||||
def get_field_names(self, declared_fields, info):
|
||||
@@ -51,6 +68,8 @@ class GatewayWithAuthSerializer(GatewaySerializer):
|
||||
return fields
|
||||
|
||||
|
||||
|
||||
|
||||
class DomainWithGatewaySerializer(BulkOrgResourceModelSerializer):
|
||||
gateways = GatewayWithAuthSerializer(many=True, read_only=True)
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ class GatheredUserSerializer(OrgResourceModelSerializerMixin):
|
||||
'present', 'date_created', 'date_updated'
|
||||
]
|
||||
read_only_fields = fields
|
||||
labels = {
|
||||
'hostname': _("Hostname"),
|
||||
'ip': "IP"
|
||||
extra_kwargs = {
|
||||
'hostname': {'label': _("Hostname")},
|
||||
'ip': {'label': 'IP'},
|
||||
}
|
||||
|
||||
@@ -20,6 +20,9 @@ class LabelSerializer(BulkOrgResourceModelSerializer):
|
||||
read_only_fields = (
|
||||
'category', 'date_created', 'asset_count', 'get_category_display'
|
||||
)
|
||||
extra_kwargs = {
|
||||
'assets': {'required': False}
|
||||
}
|
||||
list_serializer_class = AdaptedBulkListSerializer
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -33,13 +33,15 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
'login_mode', 'login_mode_display',
|
||||
'priority', 'username_same_with_user',
|
||||
'auto_push', 'cmd_filters', 'sudo', 'shell', 'comment',
|
||||
'auto_generate_key', 'sftp_root',
|
||||
'assets_amount',
|
||||
'auto_generate_key', 'sftp_root', 'token',
|
||||
'assets_amount', 'date_created', 'created_by',
|
||||
'home', 'system_groups'
|
||||
]
|
||||
extra_kwargs = {
|
||||
'password': {"write_only": True},
|
||||
'public_key': {"write_only": True},
|
||||
'private_key': {"write_only": True},
|
||||
'token': {"write_only": True},
|
||||
'nodes_amount': {'label': _('Node')},
|
||||
'assets_amount': {'label': _('Asset')},
|
||||
'login_mode_display': {'label': _('Login mode display')},
|
||||
@@ -143,17 +145,25 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
|
||||
|
||||
class SystemUserListSerializer(SystemUserSerializer):
|
||||
|
||||
class Meta(SystemUserSerializer.Meta):
|
||||
fields = [
|
||||
'id', 'name', 'username', 'protocol',
|
||||
'password', 'public_key', 'private_key',
|
||||
'login_mode', 'login_mode_display',
|
||||
'priority', "username_same_with_user",
|
||||
'auto_push', 'sudo', 'shell', 'comment',
|
||||
"assets_amount",
|
||||
"assets_amount", 'home', 'system_groups',
|
||||
'auto_generate_key',
|
||||
'sftp_root',
|
||||
]
|
||||
|
||||
extra_kwargs = {
|
||||
'password': {"write_only": True},
|
||||
'public_key': {"write_only": True},
|
||||
'private_key': {"write_only": True},
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def setup_eager_loading(cls, queryset):
|
||||
""" Perform necessary eager loading of data. """
|
||||
@@ -169,7 +179,7 @@ class SystemUserWithAuthInfoSerializer(SystemUserSerializer):
|
||||
'login_mode', 'login_mode_display',
|
||||
'priority', 'username_same_with_user',
|
||||
'auto_push', 'sudo', 'shell', 'comment',
|
||||
'auto_generate_key', 'sftp_root',
|
||||
'auto_generate_key', 'sftp_root', 'token'
|
||||
]
|
||||
extra_kwargs = {
|
||||
'nodes_amount': {'label': _('Node')},
|
||||
@@ -219,15 +229,8 @@ class SystemUserNodeRelationSerializer(RelationMixin, serializers.ModelSerialize
|
||||
'id', 'node', "node_display",
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.tree = Node.tree()
|
||||
|
||||
def get_node_display(self, obj):
|
||||
if hasattr(obj, 'node_key'):
|
||||
return self.tree.get_node_full_tag(obj.node_key)
|
||||
else:
|
||||
return obj.node.full_value
|
||||
return obj.node.full_value
|
||||
|
||||
|
||||
class SystemUserUserRelationSerializer(RelationMixin, serializers.ModelSerializer):
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from collections import defaultdict
|
||||
from operator import add, sub
|
||||
|
||||
from assets.utils import is_asset_exists_in_node
|
||||
from django.db.models.signals import (
|
||||
post_save, m2m_changed, post_delete
|
||||
post_save, m2m_changed, pre_delete, post_delete
|
||||
)
|
||||
from django.db.models.aggregates import Count
|
||||
from django.db.models import Q, F
|
||||
from django.dispatch import receiver
|
||||
|
||||
from common.exceptions import M2MReverseNotAllowed
|
||||
from common.const.signals import PRE_ADD, POST_ADD, POST_REMOVE, PRE_CLEAR, PRE_REMOVE
|
||||
from common.utils import get_logger
|
||||
from common.decorator import on_transaction_commit
|
||||
from orgs.utils import tmp_to_root_org
|
||||
from .models import Asset, SystemUser, Node, AuthBook
|
||||
from .utils import TreeService
|
||||
from .models import Asset, SystemUser, Node, compute_parent_key
|
||||
from users.models import User
|
||||
from .tasks import (
|
||||
update_assets_hardware_info_util,
|
||||
test_asset_connectivity_util,
|
||||
@@ -54,15 +57,6 @@ def on_asset_created_or_update(sender, instance=None, created=False, **kwargs):
|
||||
instance.nodes.add(Node.org_root())
|
||||
|
||||
|
||||
@receiver(post_delete, sender=Asset)
|
||||
def on_asset_delete(sender, instance=None, **kwargs):
|
||||
"""
|
||||
当资产删除时,刷新节点,节点中存在节点和资产的关系
|
||||
"""
|
||||
logger.debug("Asset delete signal recv: {}".format(instance))
|
||||
Node.refresh_assets()
|
||||
|
||||
|
||||
@receiver(post_save, sender=SystemUser, dispatch_uid="jms")
|
||||
def on_system_user_update(sender, instance=None, created=True, **kwargs):
|
||||
"""
|
||||
@@ -82,7 +76,7 @@ def on_system_user_assets_change(sender, instance=None, action='', model=None, p
|
||||
"""
|
||||
当系统用户和资产关系发生变化时,应该重新推送系统用户到新添加的资产中
|
||||
"""
|
||||
if action != "post_add":
|
||||
if action != POST_ADD:
|
||||
return
|
||||
logger.debug("System user assets change signal recv: {}".format(instance))
|
||||
queryset = model.objects.filter(pk__in=pk_set)
|
||||
@@ -101,7 +95,7 @@ def on_system_user_users_change(sender, instance=None, action='', model=None, pk
|
||||
"""
|
||||
当系统用户和用户关系发生变化时,应该重新推送系统用户资产中
|
||||
"""
|
||||
if action != "post_add":
|
||||
if action != POST_ADD:
|
||||
return
|
||||
if not instance.username_same_with_user:
|
||||
return
|
||||
@@ -120,7 +114,7 @@ def on_system_user_nodes_change(sender, instance=None, action=None, model=None,
|
||||
"""
|
||||
当系统用户和节点关系发生变化时,应该将节点下资产关联到新的系统用户上
|
||||
"""
|
||||
if action != "post_add":
|
||||
if action != POST_ADD:
|
||||
return
|
||||
logger.info("System user nodes update signal recv: {}".format(instance))
|
||||
|
||||
@@ -135,102 +129,217 @@ def on_system_user_nodes_change(sender, instance=None, action=None, model=None,
|
||||
|
||||
|
||||
@receiver(m2m_changed, sender=SystemUser.groups.through)
|
||||
def on_system_user_groups_change(sender, instance=None, action=None, model=None,
|
||||
pk_set=None, reverse=False, **kwargs):
|
||||
def on_system_user_groups_change(instance, action, pk_set, reverse, **kwargs):
|
||||
"""
|
||||
当系统用户和用户组关系发生变化时,应该将组下用户关联到新的系统用户上
|
||||
"""
|
||||
if action != "post_add" or reverse:
|
||||
if action != POST_ADD:
|
||||
return
|
||||
if reverse:
|
||||
raise M2MReverseNotAllowed
|
||||
logger.info("System user groups update signal recv: {}".format(instance))
|
||||
groups = model.objects.filter(pk__in=pk_set).annotate(users_count=Count("users"))
|
||||
users = groups.filter(users_count__gt=0).values_list('users', flat=True)
|
||||
instance.users.add(*tuple(users))
|
||||
|
||||
users = User.objects.filter(groups__id__in=pk_set).distinct()
|
||||
instance.users.add(*users)
|
||||
|
||||
|
||||
@receiver(m2m_changed, sender=Asset.nodes.through)
|
||||
def on_asset_nodes_change(sender, instance=None, action='', **kwargs):
|
||||
def on_asset_nodes_add(instance, action, reverse, pk_set, **kwargs):
|
||||
"""
|
||||
资产节点发生变化时,刷新节点
|
||||
"""
|
||||
if action.startswith('post'):
|
||||
logger.debug("Asset nodes change signal recv: {}".format(instance))
|
||||
Node.refresh_assets()
|
||||
with tmp_to_root_org():
|
||||
Node.refresh_assets()
|
||||
本操作共访问 4 次数据库
|
||||
|
||||
|
||||
@receiver(m2m_changed, sender=Asset.nodes.through)
|
||||
def on_asset_nodes_add(sender, instance=None, action='', model=None, pk_set=None, **kwargs):
|
||||
"""
|
||||
当资产的节点发生变化时,或者 当节点的资产关系发生变化时,
|
||||
节点下新增的资产,添加到节点关联的系统用户中
|
||||
"""
|
||||
if action != "post_add":
|
||||
if action != POST_ADD:
|
||||
return
|
||||
logger.debug("Assets node add signal recv: {}".format(action))
|
||||
if model == Node:
|
||||
nodes = model.objects.filter(pk__in=pk_set).values_list('key', flat=True)
|
||||
assets = [instance.id]
|
||||
else:
|
||||
if reverse:
|
||||
nodes = [instance.key]
|
||||
assets = model.objects.filter(pk__in=pk_set).values_list('id', flat=True)
|
||||
asset_ids = pk_set
|
||||
else:
|
||||
nodes = Node.objects.filter(pk__in=pk_set).values_list('key', flat=True)
|
||||
asset_ids = [instance.id]
|
||||
|
||||
# 节点资产发生变化时,将资产关联到节点及祖先节点关联的系统用户, 只关注新增的
|
||||
nodes_ancestors_keys = set()
|
||||
node_tree = TreeService.new()
|
||||
for node in nodes:
|
||||
ancestors_keys = node_tree.ancestors_ids(nid=node)
|
||||
nodes_ancestors_keys.update(ancestors_keys)
|
||||
system_users = SystemUser.objects.filter(nodes__key__in=nodes_ancestors_keys)
|
||||
nodes_ancestors_keys.update(Node.get_node_ancestor_keys(node, with_self=True))
|
||||
|
||||
system_users_assets = defaultdict(set)
|
||||
for system_user in system_users:
|
||||
system_users_assets[system_user].update(set(assets))
|
||||
for system_user, _assets in system_users_assets.items():
|
||||
system_user.assets.add(*tuple(_assets))
|
||||
# 查询所有祖先节点关联的系统用户,都是要跟资产建立关系的
|
||||
system_user_ids = SystemUser.objects.filter(
|
||||
nodes__key__in=nodes_ancestors_keys
|
||||
).distinct().values_list('id', flat=True)
|
||||
|
||||
# 查询所有已存在的关系
|
||||
m2m_model = SystemUser.assets.through
|
||||
exist = set(m2m_model.objects.filter(
|
||||
systemuser_id__in=system_user_ids, asset_id__in=asset_ids
|
||||
).values_list('systemuser_id', 'asset_id'))
|
||||
# TODO 优化
|
||||
to_create = []
|
||||
for system_user_id in system_user_ids:
|
||||
asset_ids_to_push = []
|
||||
for asset_id in asset_ids:
|
||||
if (system_user_id, asset_id) in exist:
|
||||
continue
|
||||
asset_ids_to_push.append(asset_id)
|
||||
to_create.append(m2m_model(
|
||||
systemuser_id=system_user_id,
|
||||
asset_id=asset_id
|
||||
))
|
||||
push_system_user_to_assets.delay(system_user_id, asset_ids_to_push)
|
||||
m2m_model.objects.bulk_create(to_create)
|
||||
|
||||
|
||||
def _update_node_assets_amount(node: Node, asset_pk_set: set, operator=add):
|
||||
"""
|
||||
一个节点与多个资产关系变化时,更新计数
|
||||
|
||||
:param node: 节点实例
|
||||
:param asset_pk_set: 资产的`id`集合, 内部不会修改该值
|
||||
:param operator: 操作
|
||||
* -> Node
|
||||
# -> Asset
|
||||
|
||||
* [3]
|
||||
/ \
|
||||
* * [2]
|
||||
/ \
|
||||
* * [1]
|
||||
/ / \
|
||||
* [a] # # [b]
|
||||
|
||||
"""
|
||||
# 获取节点[1]祖先节点的 `key` 含自己,也就是[1, 2, 3]节点的`key`
|
||||
ancestor_keys = node.get_ancestor_keys(with_self=True)
|
||||
ancestors = Node.objects.filter(key__in=ancestor_keys).order_by('-key')
|
||||
to_update = []
|
||||
for ancestor in ancestors:
|
||||
# 迭代祖先节点的`key`,顺序是 [1] -> [2] -> [3]
|
||||
# 查询该节点及其后代节点是否包含要操作的资产,将包含的从要操作的
|
||||
# 资产集合中去掉,他们是重复节点,无论增加或删除都不会影响节点的资产数量
|
||||
|
||||
asset_pk_set -= set(Asset.objects.filter(
|
||||
id__in=asset_pk_set
|
||||
).filter(
|
||||
Q(nodes__key__istartswith=f'{ancestor.key}:') |
|
||||
Q(nodes__key=ancestor.key)
|
||||
).distinct().values_list('id', flat=True))
|
||||
if not asset_pk_set:
|
||||
# 要操作的资产集合为空,说明都是重复资产,不用改变节点资产数量
|
||||
# 而且既然它包含了,它的祖先节点肯定也包含了,所以祖先节点都不用
|
||||
# 处理了
|
||||
break
|
||||
ancestor.assets_amount = operator(F('assets_amount'), len(asset_pk_set))
|
||||
to_update.append(ancestor)
|
||||
Node.objects.bulk_update(to_update, fields=('assets_amount', 'parent_key'))
|
||||
|
||||
|
||||
def _remove_ancestor_keys(ancestor_key, tree_set):
|
||||
# 这里判断 `ancestor_key` 不能是空,防止数据错误导致的死循环
|
||||
# 判断是否在集合里,来区分是否已被处理过
|
||||
while ancestor_key and ancestor_key in tree_set:
|
||||
tree_set.remove(ancestor_key)
|
||||
ancestor_key = compute_parent_key(ancestor_key)
|
||||
|
||||
|
||||
def _update_nodes_asset_amount(node_keys, asset_pk, operator):
|
||||
"""
|
||||
一个资产与多个节点关系变化时,更新计数
|
||||
|
||||
:param node_keys: 节点 id 的集合
|
||||
:param asset_pk: 资产 id
|
||||
:param operator: 操作
|
||||
"""
|
||||
|
||||
# 所有相关节点的祖先节点,组成一棵局部树
|
||||
ancestor_keys = set()
|
||||
for key in node_keys:
|
||||
ancestor_keys.update(Node.get_node_ancestor_keys(key))
|
||||
|
||||
# 相关节点可能是其他相关节点的祖先节点,如果是从相关节点里干掉
|
||||
node_keys -= ancestor_keys
|
||||
|
||||
to_update_keys = []
|
||||
for key in node_keys:
|
||||
# 遍历相关节点,处理它及其祖先节点
|
||||
# 查询该节点是否包含待处理资产
|
||||
exists = is_asset_exists_in_node(asset_pk, key)
|
||||
parent_key = compute_parent_key(key)
|
||||
|
||||
if exists:
|
||||
# 如果资产在该节点,那么他及其祖先节点都不用处理
|
||||
_remove_ancestor_keys(parent_key, ancestor_keys)
|
||||
continue
|
||||
else:
|
||||
# 不存在,要更新本节点
|
||||
to_update_keys.append(key)
|
||||
# 这里判断 `parent_key` 不能是空,防止数据错误导致的死循环
|
||||
# 判断是否在集合里,来区分是否已被处理过
|
||||
while parent_key and parent_key in ancestor_keys:
|
||||
exists = is_asset_exists_in_node(asset_pk, parent_key)
|
||||
if exists:
|
||||
_remove_ancestor_keys(parent_key, ancestor_keys)
|
||||
break
|
||||
else:
|
||||
to_update_keys.append(parent_key)
|
||||
ancestor_keys.remove(parent_key)
|
||||
parent_key = compute_parent_key(parent_key)
|
||||
|
||||
Node.objects.filter(key__in=to_update_keys).update(
|
||||
assets_amount=operator(F('assets_amount'), 1)
|
||||
)
|
||||
|
||||
|
||||
@receiver(m2m_changed, sender=Asset.nodes.through)
|
||||
def on_asset_nodes_remove(sender, instance=None, action='', model=None,
|
||||
pk_set=None, **kwargs):
|
||||
def update_nodes_assets_amount(action, instance, reverse, pk_set, **kwargs):
|
||||
# 不允许 `pre_clear` ,因为该信号没有 `pk_set`
|
||||
# [官网](https://docs.djangoproject.com/en/3.1/ref/signals/#m2m-changed)
|
||||
refused = (PRE_CLEAR,)
|
||||
if action in refused:
|
||||
raise ValueError
|
||||
|
||||
"""
|
||||
监控资产删除节点关系, 或节点删除资产,避免产生游离资产
|
||||
"""
|
||||
if action not in ["post_remove", "pre_clear", "post_clear"]:
|
||||
mapper = {
|
||||
PRE_ADD: add,
|
||||
POST_REMOVE: sub
|
||||
}
|
||||
if action not in mapper:
|
||||
return
|
||||
if action == "pre_clear":
|
||||
if model == Node:
|
||||
instance._nodes = list(instance.nodes.all())
|
||||
else:
|
||||
instance._assets = list(instance.assets.all())
|
||||
return
|
||||
logger.debug("Assets node remove signal recv: {}".format(action))
|
||||
if action == "post_remove":
|
||||
queryset = model.objects.filter(pk__in=pk_set)
|
||||
|
||||
operator = mapper[action]
|
||||
|
||||
if reverse:
|
||||
node: Node = instance
|
||||
asset_pk_set = set(pk_set)
|
||||
_update_node_assets_amount(node, asset_pk_set, operator)
|
||||
else:
|
||||
if model == Node:
|
||||
queryset = instance._nodes
|
||||
else:
|
||||
queryset = instance._assets
|
||||
if model == Node:
|
||||
assets = [instance]
|
||||
else:
|
||||
assets = queryset
|
||||
if isinstance(assets, list):
|
||||
assets_not_has_node = []
|
||||
for asset in assets:
|
||||
if asset.nodes.all().count() == 0:
|
||||
assets_not_has_node.append(asset.id)
|
||||
else:
|
||||
assets_not_has_node = assets.annotate(nodes_count=Count('nodes'))\
|
||||
.filter(nodes_count=0).values_list('id', flat=True)
|
||||
Node.org_root().assets.add(*tuple(assets_not_has_node))
|
||||
asset_pk = instance.id
|
||||
# 与资产直接关联的节点
|
||||
node_keys = set(Node.objects.filter(id__in=pk_set).values_list('key', flat=True))
|
||||
_update_nodes_asset_amount(node_keys, asset_pk, operator)
|
||||
|
||||
|
||||
@receiver([post_save, post_delete], sender=Node)
|
||||
def on_node_update_or_created(sender, **kwargs):
|
||||
# 刷新节点
|
||||
Node.refresh_nodes()
|
||||
with tmp_to_root_org():
|
||||
Node.refresh_nodes()
|
||||
RELATED_NODE_IDS = '_related_node_ids'
|
||||
|
||||
|
||||
@receiver(pre_delete, sender=Asset)
|
||||
def on_asset_delete(instance: Asset, using, **kwargs):
|
||||
node_ids = set(Node.objects.filter(
|
||||
assets=instance
|
||||
).distinct().values_list('id', flat=True))
|
||||
setattr(instance, RELATED_NODE_IDS, node_ids)
|
||||
m2m_changed.send(
|
||||
sender=Asset.nodes.through, instance=instance, reverse=False,
|
||||
model=Node, pk_set=node_ids, using=using, action=PRE_REMOVE
|
||||
)
|
||||
|
||||
|
||||
@receiver(post_delete, sender=Asset)
|
||||
def on_asset_post_delete(instance: Asset, using, **kwargs):
|
||||
node_ids = getattr(instance, RELATED_NODE_IDS, None)
|
||||
if node_ids:
|
||||
m2m_changed.send(
|
||||
sender=Asset.nodes.through, instance=instance, reverse=False,
|
||||
model=Node, pk_set=node_ids, using=using, action=POST_REMOVE
|
||||
)
|
||||
|
||||
@@ -9,3 +9,4 @@ from .gather_asset_users import *
|
||||
from .gather_asset_hardware_info import *
|
||||
from .push_system_user import *
|
||||
from .system_user_connectivity import *
|
||||
from .nodes_amount import *
|
||||
|
||||
@@ -85,7 +85,7 @@ def test_asset_user_connectivity_util(asset_user, task_name):
|
||||
raw, summary = test_user_connectivity(
|
||||
task_name=task_name, asset=asset_user.asset,
|
||||
username=asset_user.username, password=asset_user.password,
|
||||
private_key=asset_user.private_key
|
||||
private_key=asset_user.private_key_file
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warn("Failed run adhoc {}, {}".format(task_name, e))
|
||||
|
||||
@@ -64,7 +64,7 @@ GATHER_ASSET_USERS_TASKS = [
|
||||
"action": {
|
||||
"module": "shell",
|
||||
"args": "users=$(getent passwd | grep -v 'nologin' | "
|
||||
"grep -v 'shudown' | awk -F: '{ print $1 }');for i in $users;do last -F $i -1 | "
|
||||
"grep -v 'shudown' | awk -F: '{ print $1 }');for i in $users;do last -w -F $i -1 | "
|
||||
"head -1 | grep -v '^$' | awk '{ print $1\"@\"$3\"@\"$5,$6,$7,$8 }';done"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,5 +129,5 @@ def update_assets_hardware_info_period():
|
||||
def update_node_assets_hardware_info_manual(node):
|
||||
task_name = _("Update node asset hardware information: {}").format(node.name)
|
||||
assets = node.get_all_assets()
|
||||
result = update_assets_hardware_info_util.delay(assets, task_name=task_name)
|
||||
result = update_assets_hardware_info_util(assets, task_name=task_name)
|
||||
return result
|
||||
|
||||
@@ -92,7 +92,7 @@ def add_asset_users(assets, results):
|
||||
for username, data in users.items():
|
||||
defaults = {'asset': asset, 'username': username, 'present': True}
|
||||
if data.get("ip"):
|
||||
defaults["ip_last_login"] = data["ip"]
|
||||
defaults["ip_last_login"] = data["ip"][:32]
|
||||
if data.get("date"):
|
||||
defaults["date_last_login"] = data["date"]
|
||||
GatheredUser.objects.update_or_create(
|
||||
|
||||
14
apps/assets/tasks/nodes_amount.py
Normal file
14
apps/assets/tasks/nodes_amount.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from celery import shared_task
|
||||
|
||||
from assets.utils import check_node_assets_amount
|
||||
from common.utils import get_logger
|
||||
from common.utils.timezone import now
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
@shared_task()
|
||||
def check_node_assets_amount_celery_task():
|
||||
logger.info(f'>>> {now()} begin check_node_assets_amount_celery_task ...')
|
||||
check_node_assets_amount()
|
||||
logger.info(f'>>> {now()} end check_node_assets_amount_celery_task ...')
|
||||
@@ -2,10 +2,13 @@
|
||||
|
||||
from itertools import groupby
|
||||
from celery import shared_task
|
||||
from common.db.utils import get_object_if_need, get_objects_if_need
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.db.models import Empty
|
||||
|
||||
from common.utils import encrypt_password, get_logger
|
||||
from orgs.utils import tmp_to_org, org_aware_func
|
||||
from assets.models import SystemUser, Asset
|
||||
from orgs.utils import org_aware_func
|
||||
from . import const
|
||||
from .utils import clean_ansible_task_hosts, group_asset_by_platform
|
||||
|
||||
@@ -17,20 +20,42 @@ __all__ = [
|
||||
]
|
||||
|
||||
|
||||
def _split_by_comma(raw: str):
|
||||
try:
|
||||
return [i.strip() for i in raw.split(',')]
|
||||
except AttributeError:
|
||||
return []
|
||||
|
||||
|
||||
def _dump_args(args: dict):
|
||||
return ' '.join([f'{k}={v}' for k, v in args.items() if v is not Empty])
|
||||
|
||||
|
||||
def get_push_unixlike_system_user_tasks(system_user, username=None):
|
||||
if username is None:
|
||||
username = system_user.username
|
||||
password = system_user.password
|
||||
public_key = system_user.public_key
|
||||
|
||||
groups = _split_by_comma(system_user.system_groups)
|
||||
|
||||
if groups:
|
||||
groups = '"%s"' % ','.join(groups)
|
||||
|
||||
add_user_args = {
|
||||
'name': username,
|
||||
'shell': system_user.shell or Empty,
|
||||
'state': 'present',
|
||||
'home': system_user.home or Empty,
|
||||
'groups': groups or Empty
|
||||
}
|
||||
|
||||
tasks = [
|
||||
{
|
||||
'name': 'Add user {}'.format(username),
|
||||
'action': {
|
||||
'module': 'user',
|
||||
'args': 'name={} shell={} state=present'.format(
|
||||
username, system_user.shell or '/bin/bash',
|
||||
),
|
||||
'args': _dump_args(add_user_args),
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -102,6 +127,11 @@ def get_push_windows_system_user_tasks(system_user, username=None):
|
||||
if username is None:
|
||||
username = system_user.username
|
||||
password = system_user.password
|
||||
groups = {'Users', 'Remote Desktop Users'}
|
||||
if system_user.system_groups:
|
||||
groups.update(_split_by_comma(system_user.system_groups))
|
||||
groups = ','.join(groups)
|
||||
|
||||
tasks = []
|
||||
if not password:
|
||||
return tasks
|
||||
@@ -116,9 +146,9 @@ def get_push_windows_system_user_tasks(system_user, username=None):
|
||||
'update_password=always '
|
||||
'password_expired=no '
|
||||
'password_never_expires=yes '
|
||||
'groups="Users,Remote Desktop Users" '
|
||||
'groups="{}" '
|
||||
'groups_action=add '
|
||||
''.format(username, username, password),
|
||||
''.format(username, username, password, groups),
|
||||
}
|
||||
}
|
||||
tasks.append(task)
|
||||
@@ -193,6 +223,7 @@ def push_system_user_util(system_user, assets, task_name, username=None):
|
||||
|
||||
@shared_task(queue="ansible")
|
||||
def push_system_user_to_assets_manual(system_user, username=None):
|
||||
system_user = get_object_if_need(SystemUser, system_user)
|
||||
assets = system_user.get_related_assets()
|
||||
task_name = _("Push system users to assets: {}").format(system_user.name)
|
||||
return push_system_user_util(system_user, assets, task_name=task_name, username=username)
|
||||
@@ -210,11 +241,11 @@ def push_system_user_a_asset_manual(system_user, asset, username=None):
|
||||
|
||||
@shared_task(queue="ansible")
|
||||
def push_system_user_to_assets(system_user, assets, username=None):
|
||||
system_user = get_object_if_need(SystemUser, system_user)
|
||||
task_name = _("Push system users to assets: {}").format(system_user.name)
|
||||
assets = get_objects_if_need(Asset, assets)
|
||||
return push_system_user_util(system_user, assets, task_name, username=username)
|
||||
|
||||
|
||||
|
||||
# @shared_task
|
||||
# @register_as_period_task(interval=3600)
|
||||
# @after_app_ready_start
|
||||
|
||||
@@ -31,7 +31,10 @@ def test_system_user_connectivity_util(system_user, assets, task_name):
|
||||
"""
|
||||
from ops.utils import update_or_create_ansible_task
|
||||
|
||||
hosts = clean_ansible_task_hosts(assets, system_user=system_user)
|
||||
# hosts = clean_ansible_task_hosts(assets, system_user=system_user)
|
||||
# TODO: 这里不传递系统用户,因为clean_ansible_task_hosts会通过system_user来判断是否可以推送,
|
||||
# 不符合测试可连接性逻辑, 后面需要优化此逻辑
|
||||
hosts = clean_ansible_task_hosts(assets)
|
||||
if not hosts:
|
||||
return {}
|
||||
platform_hosts_map = {}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
{% extends '_modal.html' %}
|
||||
{% load i18n %}
|
||||
{% block modal_id %}asset_group_bulk_update_modal{% endblock %}
|
||||
{% block modal_class %}modal-lg{% endblock %}
|
||||
{% block modal_title%}{% trans "Update asset group" %}{% endblock %}
|
||||
{% block modal_body %}
|
||||
{% load bootstrap3 %}
|
||||
<p class="text-success text-center">{% trans "Hint: only change the field you want to update." %}</p>
|
||||
<form method="post" class="form-horizontal" action="" id="fm_asset_group_bulk_update">
|
||||
<div class="form-group">
|
||||
<label for="assets" class="col-sm-2 control-label">{% trans 'Assets' %}</label>
|
||||
<div class="col-sm-9" id="select2-container">
|
||||
<select name="assets" id="select2_groups" data-placeholder="{% trans 'Select Asset' %}" class="select2 form-control m-b" multiple>
|
||||
{% for asset in assets %}
|
||||
<option value="{{ asset.id }}">{{ asset.ip }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="system_users" class="col-sm-2 control-label">{% trans 'System users' %}</label>
|
||||
<div class="col-sm-9" id="select2-container">
|
||||
<select name="system_users" id="select2_groups" data-placeholder="{% trans 'Select System Users' %}" class="select2 form-control m-b" multiple>
|
||||
{% for system_user in system_users %}
|
||||
<option value="{{ system_user.id }}">{{ system_user.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-9 col-lg-9 col-sm-offset-2">
|
||||
<div class="checkbox checkbox-success">
|
||||
<input type="checkbox" name="enable_mfa" checked id="id_enable_mfa"><label for="id_enable_mfa">{% trans 'Enable-MFA' %}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
{% endblock %}
|
||||
{% block modal_confirm_id %}btn_asset_group_bulk_update{% endblock %}
|
||||
@@ -1,224 +0,0 @@
|
||||
{% extends '_modal.html' %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
|
||||
{% block modal_class %}modal-lg{% endblock %}
|
||||
{% block modal_id %}asset_list_modal{% endblock %}
|
||||
{% block modal_title%}{% trans "Asset list" %}{% endblock %}
|
||||
{% block modal_body %}
|
||||
<link href="{% static 'css/plugins/ztree/awesomeStyle/awesome.css' %}" rel="stylesheet">
|
||||
<script type="text/javascript" src="{% static 'js/plugins/ztree/jquery.ztree.all.min.js' %}"></script>
|
||||
<script src="{% static 'js/jquery.form.min.js' %}"></script>
|
||||
<style>
|
||||
.inmodal .modal-header {
|
||||
padding: 10px 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#asset_modal_tree.ztree * {
|
||||
background-color: white;
|
||||
}
|
||||
#asset_modal_tree.ztree {
|
||||
background-color: white;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="wrapper wrapper-content">
|
||||
<div class="row">
|
||||
<div class="col-sm-3" id="split-left" style="padding-left: 3px;overflow: auto;max-height: 500px">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="ibox-content mailbox-content" style="padding-top: 0;padding-left: 1px">
|
||||
<div class="file-manager ">
|
||||
<div id="asset_modal_tree" class="ztree">
|
||||
{% trans 'Loading' %} ...
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-9 animated fadeInRight" id="split-right">
|
||||
<div class="mail-box-header">
|
||||
<table class="table table-striped table-bordered table-hover " id="asset_list_modal_table" style="width: 100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center"><input type="checkbox" class="ipt_check_all"></th>
|
||||
<th class="text-center">{% trans 'Hostname' %}</th>
|
||||
<th class="text-center">{% trans 'IP' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function syncTableSelectedAssetToSelect2(table) {
|
||||
var assets = table.selected;
|
||||
var options = [];
|
||||
var select2Id = assetModalOption.select2Id;
|
||||
$(select2Id + ' option').each(function (i, v) {
|
||||
options.push(v.value)
|
||||
});
|
||||
table.selected_rows.forEach(function (i) {
|
||||
var name = i.hostname + '(' + i.ip + ')';
|
||||
var option = new Option(name, i.id, false, true);
|
||||
|
||||
if (options.indexOf(i.id) === -1) {
|
||||
$(select2Id).append(option).trigger('change');
|
||||
}
|
||||
});
|
||||
$(select2Id).val(assets).trigger('change');
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 解决input框中的资产和弹出表格中资产的显示不一致
|
||||
function syncSelectedAssetsToModalTable(assetModalTable) {
|
||||
var select2Id = assetModalOption.select2Id;
|
||||
var inputAssets = $(select2Id).val();
|
||||
var selectedAssets = assetModalTable.selected.concat();
|
||||
|
||||
// input assets无,table assets选中,则取消勾选(再次click)
|
||||
if (selectedAssets.length !== 0) {
|
||||
$.each(selectedAssets, function (index, assetId) {
|
||||
if ($.inArray(assetId, inputAssets) === -1) {
|
||||
$('#' + assetId).trigger('click'); // 取消勾选
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// input assets有,table assets没选,则选中(click)
|
||||
if (inputAssets) {
|
||||
assetModalTable.selected = inputAssets;
|
||||
$.each(inputAssets, function (index, assetId) {
|
||||
var dom = document.getElementById(assetId);
|
||||
if (dom !== null) {
|
||||
var selected = dom.parentElement.parentElement.className.indexOf('selected')
|
||||
}
|
||||
if (selected === -1) {
|
||||
$('#' + assetId).trigger('click');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
defaultOnAssetModalConfirm = syncTableSelectedAssetToSelect2;
|
||||
defaultOnModalTableDone = syncSelectedAssetsToModalTable;
|
||||
|
||||
|
||||
var assetModalOption = {
|
||||
selectStyle: 'multi',
|
||||
select2Id: '#id_assets',
|
||||
onModalTableDone: defaultOnModalTableDone,
|
||||
onModalTreeDone: null,
|
||||
onModalConfirm: defaultOnAssetModalConfirm,
|
||||
};
|
||||
|
||||
var assetModalTable, assetModalTree = null;
|
||||
|
||||
function initAssetModalTable() {
|
||||
if(assetModalTable){
|
||||
return
|
||||
}
|
||||
if (assetModalOption.selectStyle === 'single') {
|
||||
$('.ipt_check_all').addClass('hidden')
|
||||
}
|
||||
var options = {
|
||||
ele: $('#asset_list_modal_table'),
|
||||
ajax_url: '{% url "api-assets:asset-list" %}?show_current_asset=1',
|
||||
columns: [
|
||||
{data: "id"}, {data: "hostname" }, {data: "ip" }
|
||||
],
|
||||
lengthMenu: [[10, 25, 50], [10, 25, 50]],
|
||||
pageLength: 10,
|
||||
select_style: assetModalOption.selectStyle,
|
||||
paging_numbers_length: 3
|
||||
};
|
||||
assetModalTable = jumpserver.initServerSideDataTable(options);
|
||||
if (assetModalOption.onModalTableDone) {
|
||||
assetModalOption.onModalTableDone(assetModalTable);
|
||||
}
|
||||
return assetModalTable
|
||||
}
|
||||
|
||||
function onModalTreeNodeSelected(event, treeNode) {
|
||||
var url = assetModalTable.ajax.url();
|
||||
url = setUrlParam(url, "node_id", treeNode.meta.node.id);
|
||||
url = setUrlParam(url, "show_current_asset", "");
|
||||
assetModalTable.ajax.url(url);
|
||||
assetModalTable.ajax.reload();
|
||||
}
|
||||
|
||||
|
||||
function initModalTree() {
|
||||
var url = '{% url 'api-assets:node-children-tree' %}?assets=0';
|
||||
var setting = {
|
||||
view: {
|
||||
dblClickExpand: false,
|
||||
showLine: true
|
||||
},
|
||||
data: {
|
||||
simpleData: {
|
||||
enable: true
|
||||
}
|
||||
},
|
||||
async: {
|
||||
enable: true,
|
||||
url: url,
|
||||
autoParam: ["id=key", "name=n", "level=lv"],
|
||||
type: 'get'
|
||||
},
|
||||
callback: {
|
||||
onSelected: onModalTreeNodeSelected
|
||||
}
|
||||
};
|
||||
$.get(url, function(data, status){
|
||||
$.fn.zTree.init($("#asset_modal_tree"), setting);
|
||||
assetModalTree = $.fn.zTree.getZTreeObj("assetTree2");
|
||||
if (assetModalOption.onModalTreeDone) {
|
||||
assetModalOption.onModalTreeDone(assetModalTree);
|
||||
}
|
||||
return assetModalTree;
|
||||
});
|
||||
}
|
||||
|
||||
function setAssetModalOptions(options) {
|
||||
assetModalOption = options;
|
||||
}
|
||||
|
||||
function initAssetTreeModel(selector) {
|
||||
$(selector).parent().find(".select2-selection").on('click', function (e) {
|
||||
if ($(e.target).attr('class') !== 'select2-selection__choice__remove'){
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
$("#asset_list_modal").modal();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
$(document).ready(function(){
|
||||
|
||||
}).on('show.bs.modal', function () {
|
||||
initAssetModalTable();
|
||||
initModalTree();
|
||||
}).on('click', '#btn_asset_modal_confirm', function () {
|
||||
if (assetModalOption.onModalConfirm) {
|
||||
assetModalOption.onModalConfirm(assetModalTable, assetModalTree);
|
||||
}
|
||||
$("#asset_list_modal").modal('hide');
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal_button %}
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
||||
{% block modal_confirm_id %}btn_asset_modal_confirm{% endblock %}
|
||||
|
||||
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
{% extends '_modal.html' %}
|
||||
{% load i18n %}
|
||||
{% block modal_id %}asset_user_auth_update_modal{% endblock %}
|
||||
{% block modal_title%}{% trans "Update asset user auth" %}{% endblock %}
|
||||
{% block modal_body %}
|
||||
<form class="form-horizontal" role="form" onkeydown="if(event.keyCode==13){ $('#btn_asset_user_auth_update_modal_confirm').trigger('click'); return false;}">
|
||||
{% csrf_token %}
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 control-label">{% trans "Hostname" %}: </label>
|
||||
<div class="col-sm-10">
|
||||
<p class="form-control-static" id="id_hostname_p"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 control-label">{% trans "Username" %}: </label>
|
||||
<div class="col-sm-10">
|
||||
<p class="form-control-static" id="id_username_p"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 control-label">{% trans "Password" %}: </label>
|
||||
<div class="col-sm-10">
|
||||
<input class="form-control" id="id_password_auth" type="password" name="password" placeholder="{% trans 'Please input password' %}"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 control-label">{% trans "Private key" %}: </label>
|
||||
<div class="col-sm-10">
|
||||
<div class="row bootstrap3-multi-input">
|
||||
<div class="col-xs-12">
|
||||
<input id="id_private_key" type="file" name="private_key"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<script>
|
||||
var authHostname, authUsername, authAssetId = null;
|
||||
|
||||
$(document).ready(function () {
|
||||
|
||||
}).on("show.bs.modal", "#asset_user_auth_update_modal", function () {
|
||||
$('#id_hostname_p').html(authHostname);
|
||||
$('#id_username_p').html(authUsername);
|
||||
$('#id_password_auth').parent().removeClass('has-error');
|
||||
$('#id_password_auth').val('');
|
||||
}).on('click', '#btn_asset_user_auth_update_modal_confirm', function(){
|
||||
var password = $('#id_password_auth').val();
|
||||
var privateKey = $('#id_private_key').prop('files');
|
||||
var hasPrivateKey = privateKey.length > 0;
|
||||
if (!password && !hasPrivateKey) {
|
||||
$('#id_password_auth').parent().addClass('has-error');
|
||||
return
|
||||
}
|
||||
var data = {
|
||||
'asset': authAssetId,
|
||||
'username': authUsername
|
||||
};
|
||||
if (password) {
|
||||
data["password"] = password
|
||||
}
|
||||
var props = {
|
||||
data: data,
|
||||
url: "{% url 'api-assets:asset-user-list' %}",
|
||||
form: $("form"),
|
||||
method: 'POST',
|
||||
success: function () {
|
||||
toastr.success("{% trans 'Update successfully!' %}");
|
||||
$("#asset_user_auth_update_modal").modal('hide');
|
||||
}
|
||||
};
|
||||
if (hasPrivateKey) {
|
||||
var reader = new FileReader();//新建一个FileReader
|
||||
reader.readAsText(privateKey[0], "UTF-8");//读取文件
|
||||
reader.onload = function(evt){ //读取完文件之后会回来这里
|
||||
data["private_key"] = evt.target.result;
|
||||
formSubmit(props);
|
||||
}
|
||||
}
|
||||
if (!hasPrivateKey) {
|
||||
formSubmit(props);
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% block modal_confirm_id %}btn_asset_user_auth_update_modal_confirm{% endblock %}
|
||||
@@ -1,102 +0,0 @@
|
||||
{% extends '_modal.html' %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% block modal_id %}asset_user_auth_view{% endblock %}
|
||||
{% block modal_title%}{% trans "Asset user auth" %}{% endblock %}
|
||||
{% block modal_body %}
|
||||
<style>
|
||||
.inmodal .modal-body {
|
||||
background: #fff;
|
||||
}
|
||||
</style>
|
||||
<form class="form-horizontal" action="" style="padding-top: 20px">
|
||||
<div class="auth-field">
|
||||
<div class="form-group">
|
||||
<label for="" class="col-sm-2 control-label">{% trans 'Hostname' %}:</label>
|
||||
<div class="col-sm-8">
|
||||
<p class="form-control-static" id="id_hostname_view"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="" class="col-sm-2 control-label">{% trans 'Username' %}:</label>
|
||||
<div class="col-sm-8" >
|
||||
<p class="form-control-static" id="id_username_view"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="" class="col-sm-2 control-label">{% trans 'Password' %}:</label>
|
||||
<div class="col-sm-8">
|
||||
<input id="id_password_view" type="password" class="form-control" value="" readonly style="border: none;padding-left: 0;background-color: #fff;width: 100%">
|
||||
</div>
|
||||
<div class="col-sm-2" style="padding-left: 2px">
|
||||
<a class="btn btn-white btn-sm btn-show-password"><i class="fa fa-eye"></i></a>
|
||||
<a class="btn btn-white btn-sm btn-copy-password"><i class="fa fa-copy"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<script src="{% static "js/plugins/clipboard/clipboard.min.js" %}"></script>
|
||||
<script>
|
||||
var showPassword = false;
|
||||
var authHostname = "";
|
||||
var authUsername = "";
|
||||
var mfaFor = "";
|
||||
var authUid = "";
|
||||
var authInfoDetailUrl = "{% url "api-assets:asset-user-auth-info-detail" pk=DEFAULT_PK %}";
|
||||
|
||||
function initClipboard() {
|
||||
var clipboard = new Clipboard('.btn-copy-password', {
|
||||
text: function (trigger) {
|
||||
return $("#id_password_view").val()
|
||||
}
|
||||
});
|
||||
clipboard.on("success", function (e) {
|
||||
toastr.success("{% trans "Copy success" %}")
|
||||
})
|
||||
}
|
||||
|
||||
function showAuth() {
|
||||
var url = authInfoDetailUrl.replace("{{ DEFAULT_PK }}", authUid);
|
||||
$("#id_username_view").html(authUsername);
|
||||
$("#id_hostname_view").html(authHostname);
|
||||
$("#id_password_view").val('');
|
||||
var success = function (data) {
|
||||
var password = data.password;
|
||||
$("#id_password_view").val(password);
|
||||
};
|
||||
var error = function() {
|
||||
var msg = "{% trans 'Get auth info error' %}";
|
||||
toastr.error(msg)
|
||||
};
|
||||
requestApi({
|
||||
url: url,
|
||||
method: "GET",
|
||||
success: success,
|
||||
flash_message: false,
|
||||
error: error
|
||||
})
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
initClipboard();
|
||||
}).on("click", ".btn-show-password", function () {
|
||||
showPassword = !showPassword;
|
||||
if (showPassword) {
|
||||
$("#id_password_view").attr("type", "text")
|
||||
} else {
|
||||
$("#id_password_view").attr("type", "password")
|
||||
}
|
||||
}).on("show.bs.modal", "#asset_user_auth_view", function () {
|
||||
showPassword = false;
|
||||
$("#id_password_view").attr("type", "password");
|
||||
showAuth();
|
||||
}).on("hide.bs.modal", "#asset_user_auth_view", function () {
|
||||
$("#id_username_view").html('');
|
||||
$("#id_hostname_view").html('');
|
||||
$("#id_password_view").val('');
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% block modal_button %}
|
||||
<button data-dismiss="modal" class="btn btn-white close_btn2" type="button">{% trans "Close" %}</button>
|
||||
{% endblock %}
|
||||
@@ -1,188 +0,0 @@
|
||||
{% load i18n %}
|
||||
<style>
|
||||
.btn-group>.btn+.dropdown-toggle {
|
||||
padding-right: 4px;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
table.dataTable tbody tr.selected a {
|
||||
color: rgb(103, 106, 108);;
|
||||
}
|
||||
</style>
|
||||
|
||||
<table class="table table-striped table-bordered table-hover" id="asset_user_list_table" style="width: 100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center">
|
||||
<input type="checkbox" id="check_all" class="ipt_check_all" >
|
||||
</th>
|
||||
<th class="text-center">{% trans 'Hostname' %}</th>
|
||||
<th class="text-center">{% trans 'IP' %}</th>
|
||||
<th class="text-center">{% trans 'Username' %}</th>
|
||||
<th class="text-center">{% trans 'Version' %}</th>
|
||||
{# <th class="text-center">{% trans 'Connectivity'%}</th>#}
|
||||
<th class="text-center">{% trans 'Datetime' %}</th>
|
||||
<th class="text-center">{% trans 'Action' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
{% include 'assets/_asset_user_auth_update_modal.html' %}
|
||||
{% include 'assets/_asset_user_auth_view_modal.html' %}
|
||||
{% include 'authentication/_mfa_confirm_modal.html' %}
|
||||
|
||||
<script>
|
||||
var defaultAssetUserListUrl = "{% url "api-assets:asset-user-list" %}";
|
||||
var defaultAssetUserDetail = "{% url "api-assets:asset-user-detail" pk=DEFAULT_PK %}";
|
||||
var assetUserTable;
|
||||
var defaultNeedPush = false;
|
||||
var lastMFATime = "{{ request.session.MFA_VERIFY_TIME }}";
|
||||
var testDatetime = "{% trans 'Test datetime: ' %}";
|
||||
var mfaVerifyTTL = "{{ SECURITY_MFA_VERIFY_TTL }}";
|
||||
var mfaNeedCheck = "{{ SECURITY_VIEW_AUTH_NEED_MFA }}" === "True";
|
||||
var onlyLatestEl = "<span style='padding-right:20px'><input type='checkbox' id='only_latest'> {% trans 'Only latest version' %}</span>";
|
||||
var onlyLatestChecked = false;
|
||||
var systemUserId = "";
|
||||
|
||||
function initAssetUserTable(option) {
|
||||
if (!option) {
|
||||
option = {}
|
||||
}
|
||||
var assetUserListUrl = option.assetUserListUrl || defaultAssetUserListUrl;
|
||||
var needPush = option.needPush === undefined ? defaultNeedPush : option.needPush;
|
||||
var options = {
|
||||
ele: $('#asset_user_list_table'),
|
||||
toggle: true,
|
||||
columnDefs: [
|
||||
{
|
||||
targets: 5, createdCell: function (td, cellData) {
|
||||
var data = toSafeLocalDateStr(cellData);
|
||||
$(td).html(data);
|
||||
},
|
||||
},
|
||||
{
|
||||
targets: 6, createdCell: function (td, cellData, rowData) {
|
||||
var viewBtn = '<button class="btn btn-xs btn-primary m-l-xs btn-view-auth" DATA>{% trans "View" %}</button>';
|
||||
var updateBtn = '<li><a class="btn-update-auth" DATA>{% trans 'Update' %}</a></li>';
|
||||
var testBtn = '<li><a class="btn-test-auth" DATA>{% trans 'Test' %}</a></li>';
|
||||
var pushBtn = '<li><a class="btn-push-auth" DATA>{% trans 'Push' %}</a></li>';
|
||||
var delBtn = '<li><a class="btn-del-auth" DATA>{% trans 'Delete' %}</a></li>';
|
||||
if (!needPush) {
|
||||
pushBtn = ''
|
||||
}
|
||||
|
||||
var data = "data-hostname=hostname123 data-username=username123 data-uid=uid123 data-asset=asset123";
|
||||
data = data.replaceAll("username123", rowData.username)
|
||||
.replaceAll("hostname123", rowData.hostname)
|
||||
.replaceAll("uid123", rowData.id)
|
||||
.replaceAll("asset123", rowData.asset);
|
||||
|
||||
var actions = '<div class="btn-group">' + viewBtn +
|
||||
' <button data-toggle="dropdown" class="btn btn-primary btn-xs dropdown-toggle">' +
|
||||
' <span class="caret"></span>' +
|
||||
' </button>' +
|
||||
' <ul class="dropdown-menu">' +
|
||||
updateBtn + delBtn + testBtn + pushBtn
|
||||
' </ul>' +
|
||||
' </div>';
|
||||
actions = actions.replaceAll("DATA", data);
|
||||
$(td).html(actions);
|
||||
},
|
||||
width: '70px'
|
||||
}
|
||||
],
|
||||
ajax_url: assetUserListUrl,
|
||||
columns: [
|
||||
{data: "id"}, {data: "hostname"}, {data: "ip"},
|
||||
{data: "username"}, {data: "version", orderable: false},
|
||||
{data: "date_created", orderable: false},
|
||||
{data: "asset", orderable: false}
|
||||
],
|
||||
op_html: $('#actions').html(),
|
||||
lb_html: onlyLatestEl,
|
||||
};
|
||||
assetUserTable = jumpserver.initServerSideDataTable(options);
|
||||
return assetUserTable
|
||||
}
|
||||
$(document).ready(function(){
|
||||
})
|
||||
.on('click', '.btn-view-auth', function () {
|
||||
// 通知给view auth modal
|
||||
authAssetId = $(this).data("asset");
|
||||
authHostname = $(this).data("hostname");
|
||||
authUsername = $(this).data('username');
|
||||
authUid = $(this).data("uid");
|
||||
if (!mfaNeedCheck){
|
||||
$("#asset_user_auth_view").modal('show');
|
||||
return
|
||||
}
|
||||
var now = new Date();
|
||||
var nowTime = now.getTime() / 1000;
|
||||
if ( !lastMFATime || nowTime - lastMFATime > mfaVerifyTTL ) {
|
||||
mfaFor = "viewAuth";
|
||||
$("#mfa_auth_confirm").modal("show");
|
||||
} else {
|
||||
$("#asset_user_auth_view").modal('show');
|
||||
}
|
||||
})
|
||||
.on("success", '#mfa_auth_confirm', function () {
|
||||
if (mfaFor !== "viewAuth") {
|
||||
return
|
||||
}
|
||||
$("#asset_user_auth_view").modal("show");
|
||||
})
|
||||
.on('click', '.btn-update-auth', function() {
|
||||
authUsername = $(this).data("username") ;
|
||||
authHostname = $(this).data("hostname");
|
||||
authAssetId = $(this).data("asset");
|
||||
$("#asset_user_auth_update_modal").modal('show');
|
||||
})
|
||||
.on("click", '.btn-test-auth', function () {
|
||||
authUid = $(this).data('uid');
|
||||
var theUrl = "{% url 'api-assets:asset-user-task-create'%}?id={{ DEFAULT_PK }}"
|
||||
.replace("{{ DEFAULT_PK }}", authUid);
|
||||
|
||||
var success = function (data) {
|
||||
var taskId = data.task;
|
||||
showCeleryTaskLog(taskId);
|
||||
};
|
||||
requestApi({
|
||||
url: theUrl,
|
||||
method: 'POST',
|
||||
data: JSON.stringify({action: 'test'}),
|
||||
success: success,
|
||||
flash_message: false
|
||||
});
|
||||
})
|
||||
.on('click', '.btn-del-auth', function () {
|
||||
var uid = $(this).data("uid");
|
||||
var theUrl = defaultAssetUserDetail.replace("{{ DEFAULT_PK }}", uid);
|
||||
requestApi({
|
||||
url: theUrl,
|
||||
method: "DELETE",
|
||||
success: function () {
|
||||
assetUserTable.ajax.reload(null, false);
|
||||
},
|
||||
success_message: "{% trans 'Delete success' %}"
|
||||
})
|
||||
|
||||
})
|
||||
.on("change", '#only_latest', function () {
|
||||
var checked = $("#only_latest").is(":checked");
|
||||
if (checked === onlyLatestChecked) {
|
||||
return
|
||||
}
|
||||
var ajaxUrl = assetUserTable.ajax.url();
|
||||
if (checked) {
|
||||
ajaxUrl = setUrlParam(ajaxUrl, 'latest', 1)
|
||||
} else {
|
||||
ajaxUrl = setUrlParam(ajaxUrl, 'latest', 0)
|
||||
}
|
||||
onlyLatestChecked = !onlyLatestChecked;
|
||||
assetUserTable.ajax.url(ajaxUrl);
|
||||
assetUserTable.ajax.reload();
|
||||
})
|
||||
|
||||
|
||||
</script>
|
||||
@@ -1,18 +0,0 @@
|
||||
{% extends '_modal.html' %}
|
||||
{% load i18n %}
|
||||
{% block modal_id %}gateway_test{% endblock %}
|
||||
{% block modal_title%}{% trans "Test gateway test connection" %}{% endblock %}
|
||||
{% block modal_body %}
|
||||
{% load bootstrap3 %}
|
||||
<form method="post" class="form-horizontal" action="" id="test_gateway_form" style="padding-top: 10px">
|
||||
<div class="form-group">
|
||||
<input id="gateway_id" name="gateway_id" hidden>
|
||||
<label for="port" class="col-sm-2 control-label">{% trans 'SSH Port' %}</label>
|
||||
<div class="col-sm-9" id="select2-container">
|
||||
<input id="ssh_test_port" name="port" class="form-control">
|
||||
<span class="help-block">{% trans 'If use nat, set the ssh real port' %}</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
{% block modal_confirm_id %}btn_gateway_test{% endblock %}
|
||||
@@ -1,68 +0,0 @@
|
||||
{% extends '_modal.html' %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
<style>
|
||||
.modal-body {
|
||||
background-color: white !important;
|
||||
}
|
||||
</style>
|
||||
{% block modal_id %}node_detail_modal{% endblock %}
|
||||
|
||||
{% block modal_title %}{% trans "Node detail" %}{% endblock %}
|
||||
|
||||
|
||||
{% block modal_body %}
|
||||
<form class="form-horizontal" action="" style="padding-top: 20px">
|
||||
<div>
|
||||
<div class="form-group">
|
||||
<label for="" class="col-sm-2 control-label">{% trans 'ID' %}</label>
|
||||
<div class="col-sm-8">
|
||||
<p class="form-control-static" id="id_node_detail_id_view"></p>
|
||||
</div>
|
||||
<div class="col-sm-2" style="padding-left: 2px">
|
||||
<a class="btn btn-white btn-sm btn-node-detail-copy-id"><i class="fa fa-copy"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="" class="col-sm-2 control-label">{% trans 'Name' %}</label>
|
||||
<div class="col-sm-8" >
|
||||
<p class="form-control-static" id="id_node_detail_name_view"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="" class="col-sm-2 control-label">{% trans 'Full name' %}</label>
|
||||
<div class="col-sm-8" >
|
||||
<p class="form-control-static" id="id_node_detail_full_name_view"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="" class="col-sm-2 control-label">{% trans 'Key' %}</label>
|
||||
<div class="col-sm-8">
|
||||
<p class="form-control-static" id="id_node_detail_key_view"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<script src="{% static "js/plugins/clipboard/clipboard.min.js" %}"></script>
|
||||
<script>
|
||||
|
||||
function initClipboard() {
|
||||
var clipboard = new Clipboard('.btn-node-detail-copy-id', {
|
||||
text: function (trigger) {
|
||||
return $("#id_node_detail_id_view").html()
|
||||
}
|
||||
});
|
||||
clipboard.on("success", function (e) {
|
||||
toastr.success("{% trans "Copy success" %}")
|
||||
})
|
||||
}
|
||||
$(document).ready(function () {
|
||||
initClipboard();
|
||||
})
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block modal_button %}
|
||||
<button data-dismiss="modal" class="btn btn-white" type="button">{% trans "Close" %}</button>
|
||||
{% endblock %}
|
||||
@@ -1,339 +0,0 @@
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
<link href="{% static 'css/plugins/ztree/awesomeStyle/awesome.css' %}" rel="stylesheet">
|
||||
{# <link href="https://cdn.datatables.net/1.10.19/css/jquery.dataTables.min.css" rel="stylesheet">#}
|
||||
<script type="text/javascript" src="{% static 'js/plugins/ztree/jquery.ztree.all.min.js' %}"></script>
|
||||
<style type="text/css">
|
||||
div#rMenu {
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
text-align: left;
|
||||
{#top: 100%;#}
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 999;
|
||||
{#float: left;#}
|
||||
padding: 0 0;
|
||||
margin: 2px 0 0;
|
||||
list-style: none;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
.dataTables_wrapper .dataTables_processing {
|
||||
opacity: .9;
|
||||
border: none;
|
||||
}
|
||||
div#rMenu li{
|
||||
margin: 1px 0;
|
||||
cursor: pointer;
|
||||
list-style: none outside none;
|
||||
}
|
||||
.dropdown a:hover {
|
||||
background-color: #f1f1f1
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="ibox treebox float-e-margins" style="overflow:auto;">
|
||||
<div class="ibox-content mailbox-content" style="padding-top: 0;padding-left: 1px">
|
||||
<div class="file-manager" id="tree-node-id">
|
||||
<div id="{% block treeID %}nodeTree{% endblock %}" class="ztree">
|
||||
{% trans 'Loading' %} ...
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="rMenu">
|
||||
<ul class="dropdown-menu menu-actions">
|
||||
<li class="divider"></li>
|
||||
<li id="m_create" tabindex="-1" onclick="addTreeNode();"><a><i class="fa fa-plus-square-o"></i> {% trans 'Add node' %}</a></li>
|
||||
<li id="m_del" tabindex="-1" onclick="editTreeNode();"><a><i class="fa fa-pencil-square-o"></i> {% trans 'Rename node' %}</a></li>
|
||||
<li id="m_del" tabindex="-1" onclick="removeTreeNode();"><a><i class="fa fa-minus-square"></i> {% trans 'Delete node' %}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<script>
|
||||
var zTree, rMenu = null;
|
||||
var current_node_id = null;
|
||||
var current_node = null;
|
||||
var showMenu = false;
|
||||
|
||||
|
||||
var treeUrl = '{% url 'api-assets:node-children-tree' %}?assets=0';
|
||||
// options:
|
||||
// {
|
||||
// "onSelected": func,
|
||||
// "showAssets": false,
|
||||
// "beforeAsync": func()
|
||||
// "showMenu": false,
|
||||
// "otherMenu": "",
|
||||
// "showAssets": false,
|
||||
// }
|
||||
var inited = false;
|
||||
function initNodeTree(options) {
|
||||
if (options.showAssets) {
|
||||
treeUrl = setUrlParam(treeUrl, 'assets', '1')
|
||||
}
|
||||
var setting = {
|
||||
view: {
|
||||
dblClickExpand: false,
|
||||
showLine: true
|
||||
},
|
||||
data: {
|
||||
simpleData: {
|
||||
enable: true
|
||||
}
|
||||
},
|
||||
async: {
|
||||
enable: true,
|
||||
url: treeUrl,
|
||||
autoParam: ["id=key", "name=n", "level=lv"],
|
||||
type: 'get'
|
||||
},
|
||||
edit: {
|
||||
enable: true,
|
||||
showRemoveBtn: false,
|
||||
showRenameBtn: false,
|
||||
drag: {
|
||||
isCopy: true,
|
||||
isMove: true
|
||||
}
|
||||
},
|
||||
callback: {
|
||||
onRightClick: OnRightClick,
|
||||
beforeClick: beforeClick,
|
||||
onRename: onRename,
|
||||
onSelected: options.onSelected || defaultCallback("On selected"),
|
||||
beforeDrag: beforeDrag,
|
||||
onDrag: onDrag,
|
||||
beforeDrop: beforeDrop,
|
||||
onDrop: onDrop,
|
||||
beforeAsync: options.beforeAsync || defaultCallback("Before async")
|
||||
}
|
||||
};
|
||||
$.get(treeUrl, function (data, status) {
|
||||
zTree = $.fn.zTree.init($("#nodeTree"), setting, data);
|
||||
rootNodeAddDom(zTree, function () {
|
||||
const url = '{% url 'api-assets:node-task-create' pk=DEFAULT_PK %}';
|
||||
requestApi({
|
||||
url: url,
|
||||
method: 'POST',
|
||||
data: {action: "refresh_cache"},
|
||||
flash_message: false,
|
||||
success: function () {
|
||||
initNodeTree(options);
|
||||
}
|
||||
});
|
||||
});
|
||||
inited = true;
|
||||
});
|
||||
|
||||
if (inited) {
|
||||
return
|
||||
}
|
||||
|
||||
if (options.showMenu) {
|
||||
showMenu = true;
|
||||
rMenu = $("#rMenu");
|
||||
}
|
||||
if (options.otherMenu) {
|
||||
$(".menu-actions").append(options.otherMenu)
|
||||
}
|
||||
return zTree
|
||||
}
|
||||
|
||||
function addTreeNode() {
|
||||
hideRMenu();
|
||||
var parentNode = zTree.getSelectedNodes()[0];
|
||||
if (!parentNode){
|
||||
return
|
||||
}
|
||||
var url = "{% url 'api-assets:node-children' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", parentNode.meta.node.id);
|
||||
$.post(url, {}, function (data, status){
|
||||
if (status === "success") {
|
||||
var newNode = {
|
||||
id: data["key"],
|
||||
name: data["value"],
|
||||
pId: parentNode.id,
|
||||
meta: {
|
||||
"node": data
|
||||
}
|
||||
};
|
||||
newNode.checked = zTree.getSelectedNodes()[0].checked;
|
||||
zTree.addNodes(parentNode, 0, newNode);
|
||||
var node = zTree.getNodeByParam('id', newNode.id, parentNode);
|
||||
zTree.editName(node);
|
||||
} else {
|
||||
alert("{% trans 'Create node failed' %}")
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function removeTreeNode() {
|
||||
hideRMenu();
|
||||
var current_node = zTree.getSelectedNodes()[0];
|
||||
if (!current_node){
|
||||
return
|
||||
}
|
||||
var url = "{% url 'api-assets:node-detail' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node_id);
|
||||
requestApi({
|
||||
url: url,
|
||||
method: "DELETE",
|
||||
success: function () {
|
||||
zTree.removeNode(current_node)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function editTreeNode() {
|
||||
hideRMenu();
|
||||
var current_node = zTree.getSelectedNodes()[0];
|
||||
if (!current_node){
|
||||
return
|
||||
}
|
||||
if (current_node) {
|
||||
current_node.name = current_node.meta.node.value;
|
||||
}
|
||||
zTree.editName(current_node);
|
||||
}
|
||||
|
||||
function OnRightClick(event, treeId, treeNode) {
|
||||
if (!showMenu) {
|
||||
return
|
||||
}
|
||||
if (!treeNode && event.target.tagName.toLowerCase() !== "button" && $(event.target).parents("a").length === 0) {
|
||||
zTree.cancelSelectedNode();
|
||||
showRMenu("root", event.clientX, event.clientY);
|
||||
} else if (treeNode && !treeNode.noR) {
|
||||
zTree.selectNode(treeNode);
|
||||
showRMenu("node", event.clientX, event.clientY);
|
||||
}
|
||||
}
|
||||
|
||||
function showRMenu(type, x, y) {
|
||||
var offset = $("#tree-node-id").offset();
|
||||
var scrollTop = document.querySelector('.treebox').scrollTop;
|
||||
x -= offset.left;
|
||||
y -= offset.top + scrollTop;
|
||||
x += document.body.scrollLeft;
|
||||
y += document.body.scrollTop + document.documentElement.scrollTop;
|
||||
rMenu.css({"top":y+"px", "left":x+"px", "visibility":"visible"});
|
||||
$("#rMenu ul").show();
|
||||
$("body").bind("mousedown", onBodyMouseDown);
|
||||
}
|
||||
|
||||
function beforeClick(treeId, treeNode, clickFlag) {
|
||||
return true;
|
||||
}
|
||||
|
||||
function hideRMenu() {
|
||||
if (rMenu) rMenu.css({"visibility": "hidden"});
|
||||
$("body").unbind("mousedown", onBodyMouseDown);
|
||||
}
|
||||
|
||||
function onBodyMouseDown(event){
|
||||
if (!(event.target.id === "rMenu" || $(event.target).parents("#rMenu").length>0)) {
|
||||
rMenu.css({"visibility" : "hidden"});
|
||||
}
|
||||
}
|
||||
|
||||
function onRename(event, treeId, treeNode, isCancel){
|
||||
var url = "{% url 'api-assets:node-detail' pk=DEFAULT_PK %}"
|
||||
.replace("{{ DEFAULT_PK }}", current_node_id);
|
||||
var data = {"value": treeNode.name};
|
||||
if (isCancel){
|
||||
return
|
||||
}
|
||||
requestApi({
|
||||
url: url,
|
||||
body: JSON.stringify(data),
|
||||
method: "PATCH",
|
||||
success_message: "{% trans 'Rename success' %}",
|
||||
success: function () {
|
||||
var assets_amount = treeNode.meta.node.assets_amount;
|
||||
if (!assets_amount) {
|
||||
assets_amount = 0;
|
||||
}
|
||||
treeNode.name = treeNode.name + ' (' + assets_amount + ')';
|
||||
zTree.updateNode(treeNode);
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
function beforeDrag() {
|
||||
return true
|
||||
}
|
||||
|
||||
function beforeDrop(treeId, treeNodes, targetNode, moveType) {
|
||||
var treeNodesNames = [];
|
||||
$.each(treeNodes, function (index, value) {
|
||||
treeNodesNames.push(value.name);
|
||||
});
|
||||
|
||||
var msg = "你想移动节点: `" + treeNodesNames.join(",") + "` 到 `" + targetNode.name + "` 下吗?";
|
||||
return confirm(msg);
|
||||
}
|
||||
|
||||
function onDrag(event, treeId, treeNodes) {
|
||||
}
|
||||
|
||||
function onDrop(event, treeId, treeNodes, targetNode, moveType) {
|
||||
var treeNodesIds = [];
|
||||
$.each(treeNodes, function (index, value) {
|
||||
treeNodesIds.push(value.meta.node.id);
|
||||
});
|
||||
|
||||
var the_url = "{% url 'api-assets:node-add-children' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", targetNode.meta.node.id);
|
||||
var body = {nodes: treeNodesIds};
|
||||
requestApi({
|
||||
url: the_url,
|
||||
method: "PUT",
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
}
|
||||
|
||||
function defaultCallback(action) {
|
||||
function logging() {
|
||||
console.log(action)
|
||||
}
|
||||
return logging
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (show === 0) {
|
||||
$("#split-left").hide(500, function () {
|
||||
$("#split-right").attr("class", "col-lg-12");
|
||||
$("#toggle-icon").attr("class", "fa fa-angle-right fa-x");
|
||||
show = 1;
|
||||
});
|
||||
} else {
|
||||
$("#split-right").attr("class", "col-lg-9");
|
||||
$("#toggle-icon").attr("class", "fa fa-angle-left fa-x");
|
||||
$("#split-left").show(500);
|
||||
show = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$(document).ready(function () {
|
||||
$('.treebox').css('height', window.innerHeight - 60);
|
||||
})
|
||||
.on('click', '.btn-show-current-asset', function(){
|
||||
hideRMenu();
|
||||
$(this).css('display', 'none');
|
||||
$('#show_all_asset').css('display', 'inline-block');
|
||||
setCookie('show_current_asset', '1');
|
||||
location.reload()
|
||||
})
|
||||
.on('click', '.btn-show-all-asset', function(){
|
||||
hideRMenu();
|
||||
$(this).css('display', 'none');
|
||||
$('#show_current_asset').css('display', 'inline-block');
|
||||
setCookie('show_current_asset', '');
|
||||
location.reload();
|
||||
}).on('click', '.tree-toggle-btn', function (e) {
|
||||
e.preventDefault();
|
||||
toggle();
|
||||
})
|
||||
|
||||
</script>
|
||||
@@ -1,282 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block content %}
|
||||
<div class="wrapper wrapper-content animated fadeInRight">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="ibox-title">
|
||||
<h5>{{ action }}</h5>
|
||||
<div class="ibox-tools">
|
||||
<a class="collapse-link">
|
||||
<i class="fa fa-chevron-up"></i>
|
||||
</a>
|
||||
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
|
||||
<i class="fa fa-wrench"></i>
|
||||
</a>
|
||||
<a class="close-link">
|
||||
<i class="fa fa-times"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ibox-content">
|
||||
<form enctype="multipart/form-data" method="post" class="form-horizontal" action="" >
|
||||
{% csrf_token %}
|
||||
{% if form.non_field_errors %}
|
||||
<div class="alert alert-danger">
|
||||
{{ form.non_field_errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<h3>{% trans 'Basic' %}</h3>
|
||||
{% bootstrap_field form.name layout="horizontal" %}
|
||||
{% bootstrap_field form.login_mode layout="horizontal" %}
|
||||
{% bootstrap_field form.username layout="horizontal" %}
|
||||
{% bootstrap_field form.username_same_with_user layout="horizontal" %}
|
||||
{% bootstrap_field form.priority layout="horizontal" %}
|
||||
{% bootstrap_field form.protocol layout="horizontal" %}
|
||||
|
||||
<h3 id="auth_title_id">{% trans 'Auth' %}</h3>
|
||||
{% block auth %}
|
||||
<div class="auto-generate">
|
||||
<div class="form-group">
|
||||
<label for="{{ form.auto_generate_key.id_for_label }}" class="col-sm-2 control-label">{% trans 'Auto generate key' %}</label>
|
||||
<div class="col-sm-8">
|
||||
{{ form.auto_generate_key}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="auth-fields">
|
||||
{% bootstrap_field form.password layout="horizontal" %}
|
||||
{% bootstrap_field form.private_key layout="horizontal" %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.auto_push.id_for_label }}" class="col-sm-2 control-label">{% trans 'Auto push' %}</label>
|
||||
<div class="col-sm-8">
|
||||
{{ form.auto_push}}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
<div id="command-filter-block">
|
||||
<h3>{% trans 'Command filter' %}</h3>
|
||||
{% bootstrap_field form.cmd_filters layout="horizontal" %}
|
||||
</div>
|
||||
<h3>{% trans 'Other' %}</h3>
|
||||
{% bootstrap_field form.sftp_root layout="horizontal" %}
|
||||
{% bootstrap_field form.sudo layout="horizontal" %}
|
||||
{% bootstrap_field form.shell layout="horizontal" %}
|
||||
{% bootstrap_field form.comment layout="horizontal" %}
|
||||
<div class="form-group">
|
||||
<div class="col-sm-4 col-sm-offset-2">
|
||||
<button class="btn btn-white" type="reset">{% trans 'Reset' %}</button>
|
||||
<button id="submit_button" class="btn btn-primary" type="submit">{% trans 'Submit' %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
var protocol_id = '#' + '{{ form.protocol.id_for_label }}';
|
||||
var login_mode_id = '#' + '{{ form.login_mode.id_for_label }}';
|
||||
|
||||
var auto_generate_key = '#'+'{{ form.auto_generate_key.id_for_label }}';
|
||||
var password_id = '#' + '{{ form.password.id_for_label }}';
|
||||
var private_key_id = '#' + '{{ form.private_key.id_for_label }}';
|
||||
var auto_push_id = '#' + '{{ form.auto_push.id_for_label }}';
|
||||
var command_filter_block_id = '#command-filter-block';
|
||||
var sudo_id = '#' + '{{ form.sudo.id_for_label }}';
|
||||
var shell_id = '#' + '{{ form.shell.id_for_label }}';
|
||||
|
||||
function autoLoginModeProtocol() {
|
||||
// 协议+自动登录模式字段控制
|
||||
$('#auth_title_id').removeClass('hidden');
|
||||
var protocol = $(protocol_id + " option:selected").text();
|
||||
if (['rdp'].indexOf(protocol) !== -1) {
|
||||
authFieldsDisplay();
|
||||
$(auto_generate_key).closest('.form-group').removeClass('hidden');
|
||||
$(private_key_id).closest('.form-group').addClass('hidden');
|
||||
$(password_id).closest('.form-group').removeClass('hidden');
|
||||
$(auto_push_id).closest('.form-group').removeClass('hidden');
|
||||
$(command_filter_block_id).addClass('hidden');
|
||||
$(sudo_id).closest('.form-group').addClass('hidden');
|
||||
$(shell_id).closest('.form-group').addClass('hidden');
|
||||
}
|
||||
else if (protocol === 'vnc') {
|
||||
$('.auth-fields').removeClass('hidden');
|
||||
$(auto_generate_key).closest('.form-group').addClass('hidden');
|
||||
$(private_key_id).closest('.form-group').addClass('hidden');
|
||||
$(password_id).closest('.form-group').removeClass('hidden');
|
||||
$(auto_push_id).closest('.form-group').addClass('hidden');
|
||||
$(command_filter_block_id).addClass('hidden');
|
||||
$(sudo_id).closest('.form-group').addClass('hidden');
|
||||
$(shell_id).closest('.form-group').addClass('hidden');
|
||||
}
|
||||
else if (protocol === 'mysql'){
|
||||
$('.auth-fields').removeClass('hidden');
|
||||
$(auto_generate_key).closest('.form-group').addClass('hidden');
|
||||
$(private_key_id).closest('.form-group').addClass('hidden');
|
||||
$(password_id).closest('.form-group').removeClass('hidden');
|
||||
$(auto_push_id).closest('.form-group').addClass('hidden');
|
||||
$(command_filter_block_id).removeClass('hidden');
|
||||
$(sudo_id).closest('.form-group').addClass('hidden');
|
||||
$(shell_id).closest('.form-group').addClass('hidden');
|
||||
}
|
||||
else if (protocol === 'telnet') {
|
||||
$('.auth-fields').removeClass('hidden');
|
||||
$(auto_generate_key).closest('.form-group').addClass('hidden');
|
||||
$(private_key_id).closest('.form-group').addClass('hidden');
|
||||
$(password_id).closest('.form-group').removeClass('hidden');
|
||||
$(auto_push_id).closest('.form-group').addClass('hidden');
|
||||
$(command_filter_block_id).removeClass('hidden');
|
||||
$(sudo_id).closest('.form-group').addClass('hidden');
|
||||
$(shell_id).closest('.form-group').addClass('hidden');
|
||||
}
|
||||
else {
|
||||
authFieldsDisplay();
|
||||
$(auto_generate_key).closest('.form-group').removeClass('hidden');
|
||||
$(private_key_id).closest('.form-group').removeClass('hidden');
|
||||
$(password_id).closest('.form-group').removeClass('hidden');
|
||||
$(auto_push_id).closest('.form-group').removeClass('hidden');
|
||||
$(command_filter_block_id).removeClass('hidden');
|
||||
$(sudo_id).closest('.form-group').removeClass('hidden');
|
||||
$(shell_id).closest('.form-group').removeClass('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function manualLoginModeProtocol() {
|
||||
// 协议+手动登录模式字段控制
|
||||
$('#auth_title_id').addClass('hidden');
|
||||
var protocol = $(protocol_id + " option:selected").text();
|
||||
if (['rdp'].indexOf(protocol) !== -1) {
|
||||
$('.auth-fields').addClass('hidden');
|
||||
$(auto_generate_key).closest('.form-group').addClass('hidden');
|
||||
$(password_id).closest('.form-group').addClass('hidden');
|
||||
$(private_key_id).closest('.form-group').addClass('hidden');
|
||||
$(auto_push_id).closest('.form-group').addClass('hidden');
|
||||
$(command_filter_block_id).addClass('hidden');
|
||||
$(sudo_id).closest('.form-group').addClass('hidden');
|
||||
$(shell_id).closest('.form-group').addClass('hidden');
|
||||
}
|
||||
else if (protocol === 'vnc'){
|
||||
$('.auth-fields').addClass('hidden');
|
||||
$(auto_generate_key).closest('.form-group').addClass('hidden');
|
||||
$(password_id).closest('.form-group').addClass('hidden');
|
||||
$(private_key_id).closest('.form-group').addClass('hidden');
|
||||
$(auto_push_id).closest('.form-group').addClass('hidden');
|
||||
$(command_filter_block_id).addClass('hidden');
|
||||
$(sudo_id).closest('.form-group').addClass('hidden');
|
||||
$(shell_id).closest('.form-group').addClass('hidden');
|
||||
}
|
||||
else if (protocol === 'mysql'){
|
||||
$('.auth-fields').addClass('hidden');
|
||||
$(auto_generate_key).closest('.form-group').addClass('hidden');
|
||||
$(password_id).closest('.form-group').addClass('hidden');
|
||||
$(private_key_id).closest('.form-group').addClass('hidden');
|
||||
$(auto_push_id).closest('.form-group').addClass('hidden');
|
||||
$(command_filter_block_id).removeClass('hidden');
|
||||
$(sudo_id).closest('.form-group').addClass('hidden');
|
||||
$(shell_id).closest('.form-group').addClass('hidden');
|
||||
}
|
||||
else if (protocol === 'telnet') {
|
||||
$('.auth-fields').addClass('hidden');
|
||||
$(auto_generate_key).closest('.form-group').addClass('hidden');
|
||||
$(password_id).closest('.form-group').addClass('hidden');
|
||||
$(private_key_id).closest('.form-group').addClass('hidden');
|
||||
$(auto_push_id).closest('.form-group').addClass('hidden');
|
||||
$(command_filter_block_id).removeClass('hidden');
|
||||
$(sudo_id).closest('.form-group').addClass('hidden');
|
||||
$(shell_id).closest('.form-group').addClass('hidden');
|
||||
}
|
||||
else {
|
||||
$('.auth-fields').addClass('hidden');
|
||||
$(auto_generate_key).closest('.form-group').addClass('hidden');
|
||||
$(password_id).closest('.form-group').addClass('hidden');
|
||||
$(private_key_id).closest('.form-group').addClass('hidden');
|
||||
$(auto_push_id).closest('.form-group').addClass('hidden');
|
||||
$(command_filter_block_id).removeClass('hidden');
|
||||
$(sudo_id).closest('.form-group').removeClass('hidden');
|
||||
$(shell_id).closest('.form-group').removeClass('hidden');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function authFieldsDisplay() {
|
||||
if ($(auto_generate_key).prop('checked')) {
|
||||
$('.auth-fields').addClass('hidden');
|
||||
} else {
|
||||
$('.auth-fields').removeClass('hidden');
|
||||
}
|
||||
}
|
||||
function fieldDisplay(){
|
||||
var login_mode = $(login_mode_id).val();
|
||||
if (login_mode === 'manual'){
|
||||
manualLoginModeProtocol();
|
||||
}
|
||||
else if(login_mode === 'auto'){
|
||||
autoLoginModeProtocol();
|
||||
}
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
$('.select2').select2();
|
||||
authFieldsDisplay();
|
||||
fieldDisplay();
|
||||
var checked = $("#id_username_same_with_user").prop('checked');
|
||||
if (checked) {
|
||||
$("#id_username").attr("disabled", true)
|
||||
}
|
||||
})
|
||||
.on('change', auto_generate_key, function(){
|
||||
authFieldsDisplay();
|
||||
})
|
||||
.on('change', login_mode_id, function(){
|
||||
fieldDisplay();
|
||||
})
|
||||
.on('change', protocol_id, function(){
|
||||
fieldDisplay();
|
||||
}).on("submit", "form", function (evt) {
|
||||
evt.preventDefault();
|
||||
{% block formUrl %}
|
||||
var the_url = '{% url 'api-assets:system-user-list' %}';
|
||||
var redirect_to = '{% url "assets:system-user-list" %}';
|
||||
var method = "POST";
|
||||
{% endblock %}
|
||||
var form = $("form");
|
||||
var data = form.serializeObject();
|
||||
|
||||
objectAttrsIsList(data, ['cmd_filters']);
|
||||
objectAttrsIsBool(data, ["auto_generate_key", "auto_push", "username_same_with_user"]);
|
||||
data["private_key"] = $("#id_private_key").data('file');
|
||||
|
||||
var props = {
|
||||
url: the_url,
|
||||
data: data,
|
||||
method: method,
|
||||
form: form,
|
||||
redirect_to: redirect_to
|
||||
};
|
||||
formSubmit(props);
|
||||
}).on('change', '#id_private_key', function () {
|
||||
readFile($(this)).on("onload", function (evt, data) {
|
||||
$(this).data("file", data)
|
||||
})
|
||||
}).on("change", '#id_username_same_with_user', function () {
|
||||
var checked = $(this).prop('checked');
|
||||
var usernameRef = $("#id_username");
|
||||
if (checked) {
|
||||
usernameRef.val('');
|
||||
usernameRef.attr("disabled", true)
|
||||
} else {
|
||||
usernameRef.attr("disabled", false)
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,24 +0,0 @@
|
||||
{% extends '_modal.html' %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
<style>
|
||||
.modal-body {
|
||||
background-color: white !important;
|
||||
}
|
||||
</style>
|
||||
{% block modal_id %}user_asset_detail_modal{% endblock %}
|
||||
|
||||
{% block modal_title %}{% trans "Asset detail" %}{% endblock %}
|
||||
|
||||
{% block modal_body %}
|
||||
<div class="ibox-content" style="background-color: inherit">
|
||||
<table class="table">
|
||||
<tbody id="asset_detail_tbody">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal_button %}
|
||||
<button data-dismiss="modal" class="btn btn-white" type="button">{% trans "Close" %}</button>
|
||||
{% endblock %}
|
||||
@@ -1,93 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="wrapper wrapper-content animated fadeInRight">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="panel-options">
|
||||
<ul class="nav nav-tabs">
|
||||
<li>
|
||||
<a href="{% url 'assets:admin-user-detail' pk=admin_user.pk %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Detail' %} </a>
|
||||
</li>
|
||||
<li class="active">
|
||||
<a href="{% url 'assets:admin-user-assets' pk=admin_user.pk %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Assets list' %} </a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
<div class="col-sm-8" style="padding-left: 0;">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="ibox-title">
|
||||
<span style="float: left">{% trans 'Asset list of ' %} <b>{{ admin_user.name }}</b></span>
|
||||
<div class="ibox-tools">
|
||||
<a class="collapse-link">
|
||||
<i class="fa fa-chevron-up"></i>
|
||||
</a>
|
||||
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
|
||||
<i class="fa fa-wrench"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-user">
|
||||
</ul>
|
||||
<a class="close-link">
|
||||
<i class="fa fa-times"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ibox-content">
|
||||
{% include 'assets/_asset_user_list.html' %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4" style="padding-left: 0;padding-right: 0">
|
||||
<div class="panel panel-primary">
|
||||
<div class="panel-heading">
|
||||
<i class="fa fa-info-circle"></i> {% trans 'Quick update' %}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr class="no-borders-tr">
|
||||
<td width="50%">{% trans 'Test connective' %}:</td>
|
||||
<td>
|
||||
<span style="float: right">
|
||||
<button type="button" class="btn btn-primary btn-xs btn-test-connective" style="width: 54px">{% trans 'Test' %}</button>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
|
||||
$(document).ready(function () {
|
||||
var assetUserListUrl = setUrlParam(defaultAssetUserListUrl, "prefer_id", "{{ admin_user.id }}");
|
||||
assetUserListUrl = setUrlParam(assetUserListUrl, "prefer", "admin_user");
|
||||
initAssetUserTable({assetUserListUrl: assetUserListUrl});
|
||||
})
|
||||
.on('click', '.btn-test-connective', function () {
|
||||
var the_url = "{% url 'api-assets:admin-user-connective' pk=admin_user.id %}";
|
||||
var success = function (data) {
|
||||
var task_id = data.task;
|
||||
showCeleryTaskLog(task_id);
|
||||
};
|
||||
requestApi({
|
||||
url: the_url,
|
||||
method: 'GET',
|
||||
success: success,
|
||||
flash_message: false
|
||||
});
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,86 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block content %}
|
||||
<div class="wrapper wrapper-content animated fadeInRight">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="ibox-title">
|
||||
<h5>{{ action }}</h5>
|
||||
<div class="ibox-tools">
|
||||
<a class="collapse-link">
|
||||
<i class="fa fa-chevron-up"></i>
|
||||
</a>
|
||||
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
|
||||
<i class="fa fa-wrench"></i>
|
||||
</a>
|
||||
<a class="close-link">
|
||||
<i class="fa fa-times"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ibox-content">
|
||||
{% if form.non_field_errors %}
|
||||
<div class="alert alert-danger">
|
||||
{{ form.non_field_errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<form enctype="multipart/form-data" method="post" class="form-horizontal" action="" >
|
||||
{% csrf_token %}
|
||||
{% bootstrap_field form.name layout="horizontal" %}
|
||||
{% bootstrap_field form.username layout="horizontal" %}
|
||||
{% bootstrap_field form.password layout="horizontal" %}
|
||||
{% bootstrap_field form.private_key layout="horizontal" %}
|
||||
{% bootstrap_field form.comment layout="horizontal" %}
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-4 col-sm-offset-2">
|
||||
<button class="btn btn-white" type="reset">{% trans 'Reset' %}</button>
|
||||
<button id="submit_button" class="btn btn-primary" type="submit">{% trans 'Submit' %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$('.select2').select2();
|
||||
})
|
||||
.on("submit", "form", function (evt) {
|
||||
evt.preventDefault();
|
||||
var the_url = '{% url 'api-assets:admin-user-list' %}';
|
||||
var redirect_to = '{% url "assets:admin-user-list" %}';
|
||||
var method = "POST";
|
||||
{% if type == "update" %}
|
||||
the_url = '{% url 'api-assets:admin-user-detail' pk=object.id %}';
|
||||
redirect_to = '{% url "assets:admin-user-list" %}';
|
||||
method = "PUT";
|
||||
{% endif %}
|
||||
var form = $("form");
|
||||
var data = form.serializeObject();
|
||||
data["private_key"] = $("#id_private_key").data('file');
|
||||
|
||||
var props = {
|
||||
url: the_url,
|
||||
data: data,
|
||||
method: method,
|
||||
form: form,
|
||||
redirect_to: redirect_to
|
||||
};
|
||||
formSubmit(props);
|
||||
})
|
||||
.on('change', '#id_private_key', function () {
|
||||
readFile($(this)).on("onload", function (evt, data) {
|
||||
$(this).data("file", data)
|
||||
})
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,166 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="wrapper wrapper-content animated fadeInRight">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="panel-options">
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="active">
|
||||
<a href="{% url 'assets:admin-user-detail' pk=admin_user.pk %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Detail' %} </a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'assets:admin-user-assets' pk=admin_user.pk %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Assets list' %} </a>
|
||||
</li>
|
||||
<li class="pull-right">
|
||||
<a class="btn btn-outline btn-default" href="{% url 'assets:admin-user-update' pk=admin_user.id %}"><i class="fa fa-edit"></i>{% trans 'Update' %}</a>
|
||||
</li>
|
||||
<li class="pull-right">
|
||||
<a class="btn btn-outline btn-danger btn-delete-admin-user">
|
||||
<i class="fa fa-trash-o"></i>{% trans 'Delete' %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
<div class="col-sm-8" style="padding-left: 0;">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="ibox-title">
|
||||
<span class="label"><b>{{ admin_user.name }}</b></span>
|
||||
<div class="ibox-tools">
|
||||
<a class="collapse-link">
|
||||
<i class="fa fa-chevron-up"></i>
|
||||
</a>
|
||||
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
|
||||
<i class="fa fa-wrench"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-user">
|
||||
</ul>
|
||||
<a class="close-link">
|
||||
<i class="fa fa-times"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ibox-content">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr class="no-borders-tr">
|
||||
<td>{% trans 'Name' %}:</td>
|
||||
<td><b>{{ admin_user.name }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Username' %}:</td>
|
||||
<td><b>{{ admin_user.username }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Date created' %}:</td>
|
||||
<td><b>{{ admin_user.date_created }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Created by' %}:</td>
|
||||
<td><b>{{ admin_user.created_by }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Comment' %}:</td>
|
||||
<td><b>{{ admin_user.comment }}</b></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4" style="padding-left: 0;padding-right: 0">
|
||||
<div class="panel panel-primary">
|
||||
<div class="panel-heading">
|
||||
<i class="fa fa-info-circle"></i> {% trans 'Replace node assets admin user with this' %}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<table class="table group_edit" id="table-clusters">
|
||||
<tbody>
|
||||
<form>
|
||||
<tr>
|
||||
<td colspan="2" class="no-borders">
|
||||
<select data-placeholder="{% trans 'Select nodes' %}" id="nodes_selected" class="nodes-select2" style="width: 100%" multiple="" tabindex="4">
|
||||
{% for node in nodes %}
|
||||
<option value="{{ node.id }}" id="opt_{{ node.id }}" >{{ node.full_value }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" class="no-borders">
|
||||
<button type="button" class="btn btn-primary btn-sm" id="btn-change-admin-user">{% trans 'Confirm' %}</button>
|
||||
</td>
|
||||
</tr>
|
||||
</form>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
function replaceNodeAssetsAdminUser(nodes) {
|
||||
var the_url = "{% url 'api-assets:replace-nodes-admin-user' pk=admin_user.id %}";
|
||||
var body = {
|
||||
nodes: nodes
|
||||
};
|
||||
var success = function(data) {
|
||||
// remove all the selected groups from select > option and rendered ul element;
|
||||
$('.select2-selection__rendered').empty();
|
||||
$('#nodes_selected').val('');
|
||||
$.map(jumpserver.nodes_selected, function(value, index) {
|
||||
$('#opt_' + index).remove();
|
||||
});
|
||||
// clear jumpserver.groups_selected
|
||||
jumpserver.nodes_selected = {};
|
||||
};
|
||||
requestApi({
|
||||
url: the_url,
|
||||
body: JSON.stringify(body),
|
||||
success: success
|
||||
});
|
||||
}
|
||||
|
||||
jumpserver.nodes_selected = {};
|
||||
$(document).ready(function () {
|
||||
nodesSelect2Init(".nodes-select2")
|
||||
.on('select2:select', function(evt) {
|
||||
var data = evt.params.data;
|
||||
jumpserver.nodes_selected[data.id] = data.text;
|
||||
}).on('select2:unselect', function(evt) {
|
||||
var data = evt.params.data;
|
||||
delete jumpserver.nodes_selected[data.id]
|
||||
});
|
||||
})
|
||||
.on('click', '.btn-delete-admin-user', function () {
|
||||
var $this = $(this);
|
||||
var name = "{{ admin_user.name }}";
|
||||
var uid = "{{ admin_user.id }}";
|
||||
var the_url = '{% url "api-assets:admin-user-detail" pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', uid);
|
||||
var redirect_url = "{% url 'assets:admin-user-list' %}";
|
||||
objectDelete($this, name, the_url, redirect_url);
|
||||
})
|
||||
.on('click', '#btn-change-admin-user', function () {
|
||||
if (Object.keys(jumpserver.nodes_selected).length === 0) {
|
||||
return false;
|
||||
}
|
||||
var nodes = [];
|
||||
$.map(jumpserver.nodes_selected, function(value, index) {
|
||||
nodes.push(index);
|
||||
});
|
||||
replaceNodeAssetsAdminUser(nodes);
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user