mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-12-15 16:42:34 +00:00
Compare commits
568 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a52b8a13fd | ||
|
|
8f0cd12a3c | ||
|
|
1551aa418f | ||
|
|
37b1038725 | ||
|
|
526babb724 | ||
|
|
a3c1d1e730 | ||
|
|
8905613e50 | ||
|
|
d6d85dff40 | ||
|
|
28b92a0d80 | ||
|
|
e4d2b5e98d | ||
|
|
1e1cd2e1aa | ||
|
|
5065490a25 | ||
|
|
070af8c491 | ||
|
|
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 | ||
|
|
3e993fd044 | ||
|
|
005573b53b | ||
|
|
e04e31eb30 | ||
|
|
ff747f9e42 | ||
|
|
c4bd093fd7 | ||
|
|
408b2d6dbd | ||
|
|
ebc63b9410 | ||
|
|
f1e5c7c2bb | ||
|
|
fcb0aefe3c | ||
|
|
29666cc8d3 | ||
|
|
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 | ||
|
|
fb9ac54843 | ||
|
|
927ca162d0 | ||
|
|
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 | ||
|
|
19926e67e1 | ||
|
|
af15622319 | ||
|
|
9850633350 | ||
|
|
5f2345852d | ||
|
|
ad1c17aa7b | ||
|
|
3a79bfd5f6 | ||
|
|
5ee8519274 | ||
|
|
ff546774e9 | ||
|
|
1f4fc9b6f0 | ||
|
|
f8142e23cd | ||
|
|
196f1654ab | ||
|
|
3b8a24eeb7 | ||
|
|
7dde15cb04 | ||
|
|
3e5d949610 | ||
|
|
25c3691f6b | ||
|
|
f3bc6c0b22 | ||
|
|
caf0d85939 | ||
|
|
a463f632e8 | ||
|
|
a0e6d09770 | ||
|
|
54623a5b06 | ||
|
|
7afff5e392 | ||
|
|
9a5fee5a4c | ||
|
|
a840e611cd | ||
|
|
566419cac4 | ||
|
|
7b362bfc76 | ||
|
|
f528dd4888 | ||
|
|
3c95c6fe11 | ||
|
|
0b17d55f30 | ||
|
|
3861943518 | ||
|
|
71a72dd957 | ||
|
|
78ac1968dd | ||
|
|
93453cc8c3 | ||
|
|
11527b9033 | ||
|
|
2ff2266417 | ||
|
|
1ef1bea703 | ||
|
|
6804f23b51 | ||
|
|
ea7e1f19b2 | ||
|
|
3320e6105c | ||
|
|
072e74ce49 | ||
|
|
492b1c4311 | ||
|
|
0babada459 | ||
|
|
7b0993959e | ||
|
|
701582fe38 | ||
|
|
d289960ff2 | ||
|
|
6adf37a30d | ||
|
|
b371676813 | ||
|
|
2e0eab9289 | ||
|
|
0cac8d66b3 | ||
|
|
2053e9210e | ||
|
|
7c44f74068 | ||
|
|
3e0f5af848 | ||
|
|
245d28b03d | ||
|
|
75f4f6d0a2 | ||
|
|
f9e167cb0e | ||
|
|
f224e49de7 | ||
|
|
4ec4869896 | ||
|
|
63ff868553 | ||
|
|
234130091b | ||
|
|
216e0f28b9 | ||
|
|
f3483484d7 | ||
|
|
d704a35ead | ||
|
|
b97deec1de | ||
|
|
5b6488e1b2 | ||
|
|
be3fdac8a9 | ||
|
|
96afd82341 | ||
|
|
728e4b7edd | ||
|
|
73c3de97b8 | ||
|
|
e98626988b | ||
|
|
01a52812f0 | ||
|
|
041d99f0be | ||
|
|
75edc26a10 | ||
|
|
2f7b169405 | ||
|
|
76ef9b292b | ||
|
|
1540cbdcaa | ||
|
|
eefe0709f8 | ||
|
|
5ae6e81a1d | ||
|
|
8a62488cb9 | ||
|
|
e951b64b0a | ||
|
|
5d129fd0da | ||
|
|
b529127461 | ||
|
|
2e40b9607e | ||
|
|
d878089ebd | ||
|
|
9d32285446 | ||
|
|
4658a4c90f | ||
|
|
d06ea2944e | ||
|
|
d50ea83f40 | ||
|
|
154aad1e22 | ||
|
|
17163dd909 | ||
|
|
b789a8bb05 | ||
|
|
9341ce9f84 | ||
|
|
4d0f7d0254 | ||
|
|
195cbbbe42 | ||
|
|
6e5e340a25 | ||
|
|
eb74d13059 | ||
|
|
16f916c40a | ||
|
|
4dd6d4498b | ||
|
|
dd5bf546df | ||
|
|
d6debde566 | ||
|
|
efc66cc7ee | ||
|
|
b310731ba7 | ||
|
|
4bd2681bf0 | ||
|
|
38e09753f4 | ||
|
|
98e3adbb11 | ||
|
|
c700b101c1 | ||
|
|
7b3647e78a | ||
|
|
f4f042c407 | ||
|
|
0a8eeca629 | ||
|
|
fdd55511a6 | ||
|
|
0cff6ab29b | ||
|
|
cda677a30f | ||
|
|
5571651c02 | ||
|
|
2680396680 | ||
|
|
227f97c2f5 | ||
|
|
ad3231c8a3 | ||
|
|
e39d8dce3c | ||
|
|
0bdc425c55 | ||
|
|
0a7f63cc5e | ||
|
|
c6e0e9a79a | ||
|
|
4cebfc7f6a | ||
|
|
7fde392774 | ||
|
|
3ee051303a | ||
|
|
9fa31be4bf | ||
|
|
ae2e4049db | ||
|
|
48b71bb11b | ||
|
|
7e4edc3c63 | ||
|
|
11b3c57c92 | ||
|
|
e339ed1fb3 | ||
|
|
2d48c8028b | ||
|
|
185c53d311 | ||
|
|
2f030c02ec | ||
|
|
f4457ff1e2 | ||
|
|
c6ed6d8acb | ||
|
|
331cfe2aed | ||
|
|
e48dbabef2 | ||
|
|
181973f235 | ||
|
|
440a2ad241 | ||
|
|
1936a6d5ee | ||
|
|
4517a92b2b | ||
|
|
44a2a51f59 | ||
|
|
2d4498578a | ||
|
|
ac902501ec | ||
|
|
363b5d04d9 | ||
|
|
dec89ae5ee | ||
|
|
5d37269a6c | ||
|
|
9a39ccd37d | ||
|
|
5896ea9c63 | ||
|
|
8323de1c07 | ||
|
|
8c0bf0b71b | ||
|
|
5812c50a33 | ||
|
|
7b339df430 | ||
|
|
aec78dc3c7 | ||
|
|
6bb13a26f5 | ||
|
|
b92137afd9 | ||
|
|
962763dc7b | ||
|
|
f4eca83a49 | ||
|
|
02135ea04f | ||
|
|
79eb838250 | ||
|
|
82710294f4 | ||
|
|
2d18acf6f7 | ||
|
|
a860bed34f | ||
|
|
f8c323cf5c | ||
|
|
b6f5b335bd | ||
|
|
f451f8a979 | ||
|
|
5c4dfabc48 | ||
|
|
72ccaf7b1c | ||
|
|
e4b788a012 | ||
|
|
6eaba4e2fb | ||
|
|
0c0c0e6d6f | ||
|
|
20cf7c7c52 | ||
|
|
230d6137f3 | ||
|
|
aa9533eb5b | ||
|
|
23f9454e5d | ||
|
|
9eee79f7d4 | ||
|
|
c0089a98f4 | ||
|
|
87242c13a1 | ||
|
|
184432a2a6 | ||
|
|
555861c319 | ||
|
|
a505995f49 | ||
|
|
b477b649f5 | ||
|
|
6e18383531 | ||
|
|
7833ff6671 | ||
|
|
5d433456d4 | ||
|
|
fc5ec3f21c | ||
|
|
efb5d4135a | ||
|
|
5f2c9c3801 | ||
|
|
56f38e57bc | ||
|
|
586c04cba6 | ||
|
|
306605915c | ||
|
|
272701a8fd | ||
|
|
9febe488b5 | ||
|
|
c8c6ba1c19 | ||
|
|
2df7bd8510 | ||
|
|
c16a986c4b | ||
|
|
44db0e8a5d | ||
|
|
91c994924f | ||
|
|
61407331bc | ||
|
|
1b1a686b96 | ||
|
|
cc30a20b7c | ||
|
|
0379e5160c | ||
|
|
386ce629ac | ||
|
|
5677bf0995 | ||
|
|
c17dc26f8c | ||
|
|
b4cc2bbff9 | ||
|
|
e74289b223 | ||
|
|
98afa032a7 | ||
|
|
0d3fab216b | ||
|
|
081f4b1c0a | ||
|
|
e9fe5b3004 | ||
|
|
1de2923dd3 | ||
|
|
6890d549ed | ||
|
|
9426f58a6b | ||
|
|
6a2a0013a8 | ||
|
|
864a4c0485 | ||
|
|
8fa08d7ea3 | ||
|
|
3648a1458b | ||
|
|
5001f48982 | ||
|
|
93bf15adc9 | ||
|
|
83eb8f77d1 | ||
|
|
4eaaa2462b | ||
|
|
0910236f12 | ||
|
|
77149bf36c | ||
|
|
6337d0d0a1 | ||
|
|
093e3924a2 | ||
|
|
e143408e57 | ||
|
|
dfdf33bbb5 | ||
|
|
8fd5e6521f | ||
|
|
6a2b9cd9bf | ||
|
|
55fa5800b1 | ||
|
|
6cdba2e8d2 | ||
|
|
043aa08f23 | ||
|
|
63519ec076 | ||
|
|
48fa26a3bc | ||
|
|
d969563e43 | ||
|
|
287236a447 | ||
|
|
4fb4a9f622 | ||
|
|
4dcd4749c3 | ||
|
|
508d1c2c1f | ||
|
|
600ea42633 | ||
|
|
41cbd3e0f6 | ||
|
|
e593ccb01c | ||
|
|
d5e1ca7908 | ||
|
|
06fb502a1d | ||
|
|
3f6d7637c3 | ||
|
|
0d2b4d7ca3 | ||
|
|
42c5c02709 | ||
|
|
d1d73da322 | ||
|
|
88fcf8dbd7 | ||
|
|
396bc9b6ae | ||
|
|
27ffed1be9 | ||
|
|
af9d42695f | ||
|
|
422b4424fe | ||
|
|
dc172e1ef0 | ||
|
|
cd7946f3f0 | ||
|
|
4994a4a387 | ||
|
|
e26eba9919 | ||
|
|
44cd82f3e1 | ||
|
|
adf14b0f2e | ||
|
|
f5a7c4131d | ||
|
|
e7031d0ac1 | ||
|
|
ef27188f86 | ||
|
|
a7734812fc | ||
|
|
fb62fbde6c | ||
|
|
521f4ea86b | ||
|
|
e9827c8b25 | ||
|
|
29b099efc0 | ||
|
|
e9fc56c056 | ||
|
|
e9103ee608 | ||
|
|
f8dae2a3c9 | ||
|
|
52c6244b2b | ||
|
|
7977294c5c | ||
|
|
e1331084e5 | ||
|
|
5268c0faae | ||
|
|
b2d9b69874 | ||
|
|
336589af98 | ||
|
|
5087c0e06f | ||
|
|
43ecd39d0e | ||
|
|
209b23c5ce | ||
|
|
7e7d4401e6 | ||
|
|
b4c3471d2c | ||
|
|
f4a822062f | ||
|
|
4583beaec0 | ||
|
|
42a6feb35e | ||
|
|
4503df910d | ||
|
|
a09b0c6c06 | ||
|
|
55c7e06185 | ||
|
|
a95a0da6f7 | ||
|
|
0fd43f48f0 | ||
|
|
dffd05cd20 | ||
|
|
681046119d | ||
|
|
b19c49da41 | ||
|
|
cc31c04b5f | ||
|
|
89f62d8e6b | ||
|
|
41b73c3701 | ||
|
|
cff3a790ef | ||
|
|
8328edd69c | ||
|
|
9bf5d6dd45 | ||
|
|
ac9a0c6d26 | ||
|
|
064cb16d25 | ||
|
|
f89b1fd44b | ||
|
|
b25096925b | ||
|
|
e8ff576324 | ||
|
|
5ac1467564 | ||
|
|
1c54e5acd8 | ||
|
|
b8d0272e37 | ||
|
|
0b7a90b83c | ||
|
|
7cf000262d | ||
|
|
fa7e0d84f9 | ||
|
|
98f6d0146c | ||
|
|
d0fac3f838 | ||
|
|
a41988d2b5 | ||
|
|
b801d2f2e9 | ||
|
|
6bed9210dd | ||
|
|
eac4630272 | ||
|
|
11a0d72b01 | ||
|
|
7dcd04ca1a | ||
|
|
ccfb151fb1 | ||
|
|
ae98eee0c7 | ||
|
|
829a4406a2 | ||
|
|
91c42e8530 | ||
|
|
250acea751 | ||
|
|
00d9f71384 | ||
|
|
ffce909ee3 | ||
|
|
0e62ea787c | ||
|
|
cedb862420 | ||
|
|
a6054ff6a5 | ||
|
|
a67b445026 | ||
|
|
8d33990050 | ||
|
|
516e75cbf4 | ||
|
|
15ca775005 | ||
|
|
41ca43bf33 | ||
|
|
c6a604fd5f | ||
|
|
8e84efb296 | ||
|
|
caee286973 | ||
|
|
672b82c3d6 | ||
|
|
55554a025f | ||
|
|
c91ce2b99f | ||
|
|
02a901467b | ||
|
|
00b3c7c945 | ||
|
|
2f9598ba49 | ||
|
|
3138abb00c | ||
|
|
3afb8647bd | ||
|
|
0682f4fc90 | ||
|
|
44ec69fdbd | ||
|
|
04945809a5 | ||
|
|
e38b113d7e | ||
|
|
eb448dc3f2 | ||
|
|
954a97bba7 | ||
|
|
41e03c629f | ||
|
|
1fd2e782f8 | ||
|
|
1f6a8e8f02 | ||
|
|
eca0a9a7d7 | ||
|
|
775f8f2ffd | ||
|
|
1e8ef8c925 | ||
|
|
c354b55f61 | ||
|
|
76ac0215fe | ||
|
|
4f93a3ca92 | ||
|
|
bcae30814d | ||
|
|
98bb6c63f5 | ||
|
|
8b8b11ce1e | ||
|
|
5f61f2b555 | ||
|
|
62d2e01cdf | ||
|
|
5e89ee9202 | ||
|
|
5d313a827b | ||
|
|
edf6baa52d | ||
|
|
3c69860b24 | ||
|
|
aa2255a87e |
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@@ -2,7 +2,7 @@
|
||||
|
||||
|
||||
##### 使用版本
|
||||
[请提供你使用的Jumpserver版本 1.x.x 注: 0.3.x不再提供支持]
|
||||
[请提供你使用的JumpServer版本 如 2.0.1 注: 1.4及以下版本不再提供支持]
|
||||
|
||||
##### 问题复现步骤
|
||||
1. [步骤1]
|
||||
|
||||
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
|
||||
|
||||
32
Dockerfile
32
Dockerfile
@@ -1,19 +1,33 @@
|
||||
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://mirrors.aliyun.com/pypi/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 -r requirements/requirements.txt || 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
|
||||
|
||||
86
README.md
86
README.md
@@ -1,26 +1,50 @@
|
||||
# Jumpserver 多云环境下更好用的堡垒机
|
||||
# JumpServer 多云环境下更好用的堡垒机
|
||||
|
||||
[](https://www.python.org/)
|
||||
[](https://www.djangoproject.com/)
|
||||
[](https://www.ansible.com/)
|
||||
[](http://www.paramiko.org/)
|
||||
[](https://www.djangoproject.com/)
|
||||
[](https://hub.docker.com/u/jumpserver)
|
||||
|
||||
Jumpserver 是全球首款完全开源的堡垒机,使用 GNU GPL v2.0 开源协议,是符合 4A 机制的运维安全审计系统。
|
||||
JumpServer 是全球首款开源的堡垒机,使用 GNU GPL v2.0 开源协议,是符合 4A 规范的运维安全审计系统。
|
||||
|
||||
Jumpserver 使用 Python / Django 进行开发,遵循 Web 2.0 规范,配备了业界领先的 Web Terminal 方案,交互界面美观、用户体验好。
|
||||
JumpServer 使用 Python / Django 为主进行开发,遵循 Web 2.0 规范,配备了业界领先的 Web Terminal 方案,交互界面美观、用户体验好。
|
||||
|
||||
Jumpserver 采纳分布式架构,支持多机房跨区域部署,支持横向扩展,无资产数量及并发限制。
|
||||
JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向扩展,无资产数量及并发限制。
|
||||
|
||||
改变世界,从一点点开始。
|
||||
|
||||
注: [KubeOperator](https://github.com/KubeOperator/KubeOperator) 是 Jumpserver 团队在 Kubernetes 领域的的又一全新力作,欢迎关注和使用。
|
||||
> 注: [KubeOperator](https://github.com/KubeOperator/KubeOperator) 是 JumpServer 团队在 Kubernetes 领域的的又一全新力作,欢迎关注和使用。
|
||||
|
||||
## 核心功能列表
|
||||
## 特色优势
|
||||
|
||||
- 开源: 零门槛,线上快速获取和安装, 修复版本视情况而定;
|
||||
, 修复版本视情况而定- 分布式: 轻松支持大规模并发访问;
|
||||
- 无插件: 仅需浏览器,极致的 Web Terminal 使用体验;
|
||||
- 多云支持: 一套系统,同时管理不同云上面的资产;
|
||||
- 云端存储: 审计录像云端存储,永不丢失;
|
||||
- 多租户: 一套系统,多个子公司和部门同时使用。
|
||||
|
||||
## 版本说明
|
||||
|
||||
自 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>
|
||||
<tr>
|
||||
<td rowspan="7">身份认证<br>Authentication</td>
|
||||
<td rowspan="4">登录认证</td>
|
||||
<td rowspan="8">身份认证<br>Authentication</td>
|
||||
<td rowspan="5">登录认证</td>
|
||||
<td>资源统一登录与认证</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -32,6 +56,9 @@ Jumpserver 采纳分布式架构,支持多机房跨区域部署,支持横向
|
||||
<tr>
|
||||
<td>OpenID 认证(实现单点登录)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>CAS 认证 (实现单点登录)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="2">MFA认证</td>
|
||||
<td>MFA 二次认证(Google Authenticator)</td>
|
||||
@@ -169,29 +196,36 @@ Jumpserver 采纳分布式架构,支持多机房跨区域部署,支持横向
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## 安装及使用指南
|
||||
|
||||
- [Docker 快速安装文档](http://docs.jumpserver.org/zh/docs/dockerinstall.html)
|
||||
- [Step by Step 安装文档](http://docs.jumpserver.org/zh/docs/step_by_step.html)
|
||||
- [完整文档](http://docs.jumpserver.org)
|
||||
|
||||
## 演示视频和截屏
|
||||
|
||||
我们提供了演示视频和系统截图可以让你快速了解 Jumpserver:
|
||||
## 快速开始
|
||||
|
||||
- [极速安装](https://docs.jumpserver.org/zh/master/install/setup_by_fast/)
|
||||
- [完整文档](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)
|
||||
- [系统截图](http://docs.jumpserver.org/zh/docs/snapshot.html)
|
||||
|
||||
## SDK
|
||||
## 案例研究
|
||||
|
||||
我们编写了一些SDK,供您的其它系统快速和 Jumpserver API 交互:
|
||||
- [JumpServer 堡垒机护航顺丰科技超大规模资产安全运维](https://blog.fit2cloud.com/?p=1147);
|
||||
- [JumpServer 堡垒机让“大智慧”的混合 IT 运维更智慧](https://blog.fit2cloud.com/?p=882);
|
||||
- [携程 JumpServer 堡垒机部署与运营实战](https://blog.fit2cloud.com/?p=851);
|
||||
- [小红书的JumpServer堡垒机大规模资产跨版本迁移之路](https://blog.fit2cloud.com/?p=516);
|
||||
- [JumpServer堡垒机助力中手游提升多云环境下安全运维能力](https://blog.fit2cloud.com/?p=732);
|
||||
- [中通快递:JumpServer主机安全运维实践](https://blog.fit2cloud.com/?p=708);
|
||||
- [东方明珠:JumpServer高效管控异构化、分布式云端资产](https://blog.fit2cloud.com/?p=687);
|
||||
- [江苏农信:JumpServer堡垒机助力行业云安全运维](https://blog.fit2cloud.com/?p=666)。
|
||||
|
||||
- [Python](https://github.com/jumpserver/jumpserver-python-sdk) Jumpserver 其它组件使用这个 SDK 完成交互
|
||||
- [Java](https://github.com/KaiJunYan/jumpserver-java-sdk.git) 恺珺同学提供的 Java 版本的 SDK
|
||||
## 安全说明
|
||||
|
||||
JumpServer是一款安全产品,请参考 [基本安全建议](https://docs.jumpserver.org/zh/master/install/install_security/) 部署安装.
|
||||
|
||||
如果你发现安全问题,可以直接联系我们:
|
||||
|
||||
- ibuler@fit2cloud.com
|
||||
- support@fit2cloud.com
|
||||
- 400-052-0755
|
||||
|
||||
## License & Copyright
|
||||
|
||||
Copyright (c) 2014-2019 飞致云 FIT2CLOUD, All rights reserved.
|
||||
Copyright (c) 2014-2020 飞致云 FIT2CLOUD, All rights reserved.
|
||||
|
||||
Licensed under The GNU General Public License version 2 (GPLv2) (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
Other module of this app shouldn't connect with other app.
|
||||
|
||||
:copyright: (c) 2014-2018 by Jumpserver Team.
|
||||
:copyright: (c) 2014-2018 by JumpServer Team.
|
||||
:license: GPL v2, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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,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 rest_framework.viewsets import ModelViewSet
|
||||
from rest_framework.generics import RetrieveAPIView
|
||||
from django.shortcuts import get_object_or_404
|
||||
@@ -17,14 +14,14 @@ from .. import serializers
|
||||
from ..tasks import (
|
||||
update_asset_hardware_info_manual, test_asset_connectivity_manual
|
||||
)
|
||||
from ..filters import AssetByNodeFilterBackend, LabelFilterBackend
|
||||
from ..filters import AssetByNodeFilterBackend, LabelFilterBackend, IpInFilterBackend
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
__all__ = [
|
||||
'AssetViewSet', 'AssetPlatformRetrieveApi',
|
||||
'AssetRefreshHardwareApi', 'AssetAdminUserTestApi',
|
||||
'AssetGatewayApi', 'AssetPlatformViewSet',
|
||||
'AssetGatewayListApi', 'AssetPlatformViewSet',
|
||||
'AssetTaskCreateApi',
|
||||
]
|
||||
|
||||
|
||||
@@ -33,12 +30,18 @@ class AssetViewSet(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_class = serializers.AssetSerializer
|
||||
serializer_classes = {
|
||||
'default': serializers.AssetSerializer,
|
||||
'display': serializers.AssetDisplaySerializer,
|
||||
}
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
extra_filter_backends = [AssetByNodeFilterBackend, LabelFilterBackend]
|
||||
extra_filter_backends = [AssetByNodeFilterBackend, LabelFilterBackend, IpInFilterBackend]
|
||||
|
||||
def set_assets_node(self, assets):
|
||||
if not isinstance(assets, list):
|
||||
@@ -71,62 +74,53 @@ 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"}
|
||||
)
|
||||
|
||||
return super().check_object_permissions(request, obj)
|
||||
|
||||
|
||||
class AssetRefreshHardwareApi(generics.RetrieveAPIView):
|
||||
"""
|
||||
Refresh asset hardware info
|
||||
"""
|
||||
class AssetTaskCreateApi(generics.CreateAPIView):
|
||||
model = Asset
|
||||
serializer_class = serializers.AssetSerializer
|
||||
serializer_class = serializers.AssetTaskSerializer
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
asset_id = kwargs.get('pk')
|
||||
asset = get_object_or_404(Asset, pk=asset_id)
|
||||
task = update_asset_hardware_info_manual.delay(asset)
|
||||
return Response({"task": task.id})
|
||||
def get_object(self):
|
||||
pk = self.kwargs.get("pk")
|
||||
instance = get_object_or_404(Asset, pk=pk)
|
||||
return instance
|
||||
|
||||
def perform_create(self, serializer):
|
||||
asset = self.get_object()
|
||||
action = serializer.validated_data["action"]
|
||||
if action == "refresh":
|
||||
task = update_asset_hardware_info_manual.delay(asset)
|
||||
else:
|
||||
task = test_asset_connectivity_manual.delay(asset)
|
||||
data = getattr(serializer, '_data', {})
|
||||
data["task"] = task.id
|
||||
setattr(serializer, '_data', data)
|
||||
|
||||
|
||||
class AssetAdminUserTestApi(generics.RetrieveAPIView):
|
||||
"""
|
||||
Test asset admin user assets_connectivity
|
||||
"""
|
||||
model = Asset
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.TaskIDSerializer
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
asset_id = kwargs.get('pk')
|
||||
asset = get_object_or_404(Asset, pk=asset_id)
|
||||
task = test_asset_connectivity_manual.delay(asset)
|
||||
return Response({"task": task.id})
|
||||
|
||||
|
||||
class AssetGatewayApi(generics.RetrieveAPIView):
|
||||
class AssetGatewayListApi(generics.ListAPIView):
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = serializers.GatewayWithAuthSerializer
|
||||
model = Asset
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
asset_id = kwargs.get('pk')
|
||||
def get_queryset(self):
|
||||
asset_id = self.kwargs.get('pk')
|
||||
asset = get_object_or_404(Asset, pk=asset_id)
|
||||
|
||||
if asset.domain and \
|
||||
asset.domain.gateways.filter(protocol='ssh').exists():
|
||||
gateway = random.choice(asset.domain.gateways.filter(protocol='ssh'))
|
||||
serializer = serializers.GatewayWithAuthSerializer(instance=gateway)
|
||||
return Response(serializer.data)
|
||||
else:
|
||||
return Response({"msg": "Not have gateway"}, status=404)
|
||||
if not asset.domain:
|
||||
return []
|
||||
queryset = asset.domain.gateways.filter(protocol='ssh')
|
||||
return queryset
|
||||
|
||||
@@ -1,26 +1,24 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import generics
|
||||
from rest_framework import filters
|
||||
from rest_framework_bulk import BulkModelViewSet
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.http import Http404
|
||||
import coreapi
|
||||
from django.conf import settings
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import generics, filters
|
||||
from rest_framework_bulk import BulkModelViewSet
|
||||
|
||||
from common.permissions import IsOrgAdminOrAppUser, NeedMFAVerify
|
||||
from common.utils import get_object_or_none, get_logger
|
||||
from common.mixins import CommonApiMixin
|
||||
from ..backends import AssetUserManager
|
||||
from ..models import Asset, Node, SystemUser, AdminUser
|
||||
from ..models import Asset, Node, SystemUser
|
||||
from .. import serializers
|
||||
from ..tasks import test_asset_users_connectivity_manual
|
||||
from ..tasks import (
|
||||
test_asset_users_connectivity_manual, push_system_user_a_asset_manual
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
'AssetUserViewSet', 'AssetUserAuthInfoApi', 'AssetUserTestConnectiveApi',
|
||||
'AssetUserExportViewSet',
|
||||
'AssetUserViewSet', 'AssetUserAuthInfoViewSet', 'AssetUserTaskCreateAPI',
|
||||
]
|
||||
|
||||
|
||||
@@ -34,10 +32,17 @@ class AssetUserFilterBackend(filters.BaseFilterBackend):
|
||||
value = request.GET.get(field)
|
||||
if not value:
|
||||
continue
|
||||
if field in ("node_id", "system_user_id", "admin_user_id"):
|
||||
if field == "node_id":
|
||||
value = get_object_or_none(Node, pk=value)
|
||||
kwargs["node"] = value
|
||||
continue
|
||||
elif field == "asset_id":
|
||||
field = "asset"
|
||||
kwargs[field] = value
|
||||
return queryset.filter(**kwargs)
|
||||
if kwargs:
|
||||
queryset = queryset.filter(**kwargs)
|
||||
logger.debug("Filter {}".format(kwargs))
|
||||
return queryset
|
||||
|
||||
|
||||
class AssetUserSearchBackend(filters.BaseFilterBackend):
|
||||
@@ -45,72 +50,75 @@ class AssetUserSearchBackend(filters.BaseFilterBackend):
|
||||
value = request.GET.get('search')
|
||||
if not value:
|
||||
return queryset
|
||||
_queryset = AssetUserManager.none()
|
||||
for field in view.search_fields:
|
||||
if field in ("node_id", "system_user_id", "admin_user_id"):
|
||||
continue
|
||||
_queryset |= queryset.filter(**{field: value})
|
||||
return _queryset.distinct()
|
||||
queryset = queryset.search(value)
|
||||
return queryset
|
||||
|
||||
|
||||
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:
|
||||
queryset = queryset.distinct()
|
||||
return queryset
|
||||
|
||||
|
||||
class AssetUserViewSet(CommonApiMixin, BulkModelViewSet):
|
||||
serializer_class = serializers.AssetUserSerializer
|
||||
serializer_classes = {
|
||||
'default': serializers.AssetUserWriteSerializer,
|
||||
'display': serializers.AssetUserReadSerializer,
|
||||
'retrieve': serializers.AssetUserReadSerializer,
|
||||
}
|
||||
permission_classes = [IsOrgAdminOrAppUser]
|
||||
http_method_names = ['get', 'post']
|
||||
filter_fields = [
|
||||
"id", "ip", "hostname", "username", "asset_id", "node_id",
|
||||
"system_user_id", "admin_user_id"
|
||||
"id", "ip", "hostname", "username",
|
||||
"asset_id", "node_id",
|
||||
"prefer", "prefer_id",
|
||||
]
|
||||
search_fields = filter_fields
|
||||
filter_backends = (
|
||||
filters.OrderingFilter,
|
||||
search_fields = ["ip", "hostname", "username"]
|
||||
filter_backends = [
|
||||
AssetUserFilterBackend, AssetUserSearchBackend,
|
||||
)
|
||||
AssetUserLatestFilterBackend,
|
||||
]
|
||||
|
||||
def allow_bulk_destroy(self, qs, filtered):
|
||||
return False
|
||||
|
||||
def get_queryset(self):
|
||||
# 尽可能先返回更少的数据
|
||||
username = self.request.GET.get('username')
|
||||
asset_id = self.request.GET.get('asset_id')
|
||||
node_id = self.request.GET.get('node_id')
|
||||
admin_user_id = self.request.GET.get("admin_user_id")
|
||||
system_user_id = self.request.GET.get("system_user_id")
|
||||
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
|
||||
|
||||
kwargs = {}
|
||||
assets = None
|
||||
def get_exception_handler(self):
|
||||
def handler(e, context):
|
||||
logger.error(e, exc_info=True)
|
||||
return Response({"error": str(e)}, status=400)
|
||||
return handler
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
manager = AssetUserManager()
|
||||
if system_user_id:
|
||||
system_user = get_object_or_404(SystemUser, id=system_user_id)
|
||||
assets = system_user.get_all_assets()
|
||||
username = system_user.username
|
||||
elif admin_user_id:
|
||||
admin_user = get_object_or_404(AdminUser, id=admin_user_id)
|
||||
assets = admin_user.assets.all()
|
||||
username = admin_user.username
|
||||
manager.prefer('admin_user')
|
||||
manager.delete(instance)
|
||||
|
||||
if asset_id:
|
||||
asset = get_object_or_404(Asset, id=asset_id)
|
||||
assets = [asset]
|
||||
elif node_id:
|
||||
node = get_object_or_404(Node, id=node_id)
|
||||
assets = node.get_all_assets()
|
||||
|
||||
if username:
|
||||
kwargs['username'] = username
|
||||
if assets is not None:
|
||||
kwargs['assets'] = assets
|
||||
|
||||
queryset = manager.filter(**kwargs)
|
||||
def get_queryset(self):
|
||||
manager = AssetUserManager()
|
||||
queryset = manager.all()
|
||||
return queryset
|
||||
|
||||
|
||||
class AssetUserExportViewSet(AssetUserViewSet):
|
||||
serializer_class = serializers.AssetUserExportSerializer
|
||||
http_method_names = ['get']
|
||||
class AssetUserAuthInfoViewSet(AssetUserViewSet):
|
||||
serializer_classes = {"default": serializers.AssetUserAuthInfoSerializer}
|
||||
http_method_names = ['get', 'post']
|
||||
permission_classes = [IsOrgAdminOrAppUser]
|
||||
|
||||
def get_permissions(self):
|
||||
@@ -119,66 +127,31 @@ class AssetUserExportViewSet(AssetUserViewSet):
|
||||
return super().get_permissions()
|
||||
|
||||
|
||||
class AssetUserAuthInfoApi(generics.RetrieveAPIView):
|
||||
serializer_class = serializers.AssetUserAuthInfoSerializer
|
||||
permission_classes = [IsOrgAdminOrAppUser]
|
||||
|
||||
def get_permissions(self):
|
||||
if settings.SECURITY_VIEW_AUTH_NEED_MFA:
|
||||
self.permission_classes = [IsOrgAdminOrAppUser, NeedMFAVerify]
|
||||
return super().get_permissions()
|
||||
|
||||
def get_object(self):
|
||||
query_params = self.request.query_params
|
||||
username = query_params.get('username')
|
||||
asset_id = query_params.get('asset_id')
|
||||
prefer = query_params.get("prefer")
|
||||
asset = get_object_or_none(Asset, pk=asset_id)
|
||||
try:
|
||||
manger = AssetUserManager()
|
||||
instance = manger.get(username, asset, prefer=prefer)
|
||||
except Exception as e:
|
||||
raise Http404("Not found")
|
||||
else:
|
||||
return instance
|
||||
|
||||
|
||||
class AssetUserTestConnectiveApi(generics.RetrieveAPIView):
|
||||
"""
|
||||
Test asset users connective
|
||||
"""
|
||||
class AssetUserTaskCreateAPI(generics.CreateAPIView):
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = serializers.TaskIDSerializer
|
||||
serializer_class = serializers.AssetUserTaskSerializer
|
||||
filter_backends = AssetUserViewSet.filter_backends
|
||||
filter_fields = AssetUserViewSet.filter_fields
|
||||
|
||||
def get_asset_users(self):
|
||||
username = self.request.GET.get('username')
|
||||
asset_id = self.request.GET.get('asset_id')
|
||||
prefer = self.request.GET.get("prefer")
|
||||
asset = get_object_or_none(Asset, pk=asset_id)
|
||||
manager = AssetUserManager()
|
||||
asset_users = manager.filter(username=username, assets=[asset], prefer=prefer)
|
||||
return asset_users
|
||||
queryset = manager.all()
|
||||
for cls in self.filter_backends:
|
||||
queryset = cls().filter_queryset(self.request, queryset, self)
|
||||
return list(queryset)
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
def perform_create(self, serializer):
|
||||
asset_users = self.get_asset_users()
|
||||
prefer = self.request.GET.get("prefer")
|
||||
kwargs = {}
|
||||
if prefer == "admin_user":
|
||||
kwargs["run_as_admin"] = True
|
||||
task = test_asset_users_connectivity_manual.delay(asset_users, **kwargs)
|
||||
return Response({"task": task.id})
|
||||
# action = serializer.validated_data["action"]
|
||||
# only this
|
||||
# if action == "test":
|
||||
task = test_asset_users_connectivity_manual.delay(asset_users)
|
||||
data = getattr(serializer, '_data', {})
|
||||
data["task"] = task.id
|
||||
setattr(serializer, '_data', data)
|
||||
return task
|
||||
|
||||
|
||||
class AssetUserPushApi(generics.CreateAPIView):
|
||||
"""
|
||||
Test asset users connective
|
||||
"""
|
||||
serializer_class = serializers.AssetUserPushSerializer
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
asset = serializer.validated_data["asset"]
|
||||
username = serializer.validated_data["username"]
|
||||
pass
|
||||
def get_exception_handler(self):
|
||||
def handler(e, context):
|
||||
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']
|
||||
|
||||
@@ -1,24 +1,11 @@
|
||||
# ~*~ 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 collections import namedtuple
|
||||
from rest_framework import status
|
||||
from rest_framework.serializers import ValidationError
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.shortcuts import get_object_or_404, Http404
|
||||
|
||||
from common.utils import get_logger, get_object_or_none
|
||||
from common.tree import TreeNodeSerializer
|
||||
@@ -27,7 +14,8 @@ from orgs.mixins import generics
|
||||
from ..hands import IsOrgAdmin
|
||||
from ..models import Node
|
||||
from ..tasks import (
|
||||
update_assets_hardware_info_util, test_asset_connectivity_util
|
||||
update_node_assets_hardware_info_manual,
|
||||
test_node_assets_connectivity_manual,
|
||||
)
|
||||
from .. import serializers
|
||||
|
||||
@@ -36,9 +24,9 @@ logger = get_logger(__file__)
|
||||
__all__ = [
|
||||
'NodeViewSet', 'NodeChildrenApi', 'NodeAssetsApi',
|
||||
'NodeAddAssetsApi', 'NodeRemoveAssetsApi', 'NodeReplaceAssetsApi',
|
||||
'NodeAddChildrenApi', 'RefreshNodeHardwareInfoApi',
|
||||
'TestNodeConnectiveApi', 'NodeListAsTreeApi',
|
||||
'NodeChildrenAsTreeApi', 'RefreshNodesCacheApi',
|
||||
'NodeAddChildrenApi', 'NodeListAsTreeApi',
|
||||
'NodeChildrenAsTreeApi',
|
||||
'NodeTaskCreateApi',
|
||||
]
|
||||
|
||||
|
||||
@@ -64,9 +52,9 @@ class NodeViewSet(OrgModelViewSet):
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
node = self.get_object()
|
||||
if node.has_children_or_contains_assets():
|
||||
msg = _("Deletion failed and the node contains children or assets")
|
||||
return Response(data={'msg': msg}, status=status.HTTP_403_FORBIDDEN)
|
||||
if node.has_children_or_has_assets():
|
||||
error = _("Deletion failed and the node contains children or assets")
|
||||
return Response(data={'error': error}, status=status.HTTP_403_FORBIDDEN)
|
||||
return super().destroy(request, *args, **kwargs)
|
||||
|
||||
|
||||
@@ -261,41 +249,41 @@ class NodeReplaceAssetsApi(generics.UpdateAPIView):
|
||||
asset.nodes.set([instance])
|
||||
|
||||
|
||||
class RefreshNodeHardwareInfoApi(APIView):
|
||||
class NodeTaskCreateApi(generics.CreateAPIView):
|
||||
model = Node
|
||||
serializer_class = serializers.NodeTaskSerializer
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
node_id = kwargs.get('pk')
|
||||
node = get_object_or_404(self.model, id=node_id)
|
||||
assets = node.get_all_assets()
|
||||
# task_name = _("更新节点资产硬件信息: {}".format(node.name))
|
||||
task_name = _("Update node asset hardware information: {}").format(node.name)
|
||||
task = update_assets_hardware_info_util.delay(assets, task_name=task_name)
|
||||
return Response({"task": task.id})
|
||||
def get_object(self):
|
||||
node_id = self.kwargs.get('pk')
|
||||
node = get_object_or_none(self.model, id=node_id)
|
||||
return node
|
||||
|
||||
@staticmethod
|
||||
def set_serializer_data(s, task):
|
||||
data = getattr(s, '_data', {})
|
||||
data["task"] = task.id
|
||||
setattr(s, '_data', data)
|
||||
|
||||
class TestNodeConnectiveApi(APIView):
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
model = Node
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
node_id = kwargs.get('pk')
|
||||
node = get_object_or_404(self.model, id=node_id)
|
||||
assets = node.get_all_assets()
|
||||
# task_name = _("测试节点下资产是否可连接: {}".format(node.name))
|
||||
task_name = _("Test if the assets under the node are connectable: {}".format(node.name))
|
||||
task = test_asset_connectivity_util.delay(assets, task_name=task_name)
|
||||
return Response({"task": task.id})
|
||||
|
||||
|
||||
class RefreshNodesCacheApi(APIView):
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
@staticmethod
|
||||
def refresh_nodes_cache():
|
||||
Node.refresh_nodes()
|
||||
return Response("Ok")
|
||||
Task = namedtuple('Task', ['id'])
|
||||
task = Task(id="0")
|
||||
return task
|
||||
|
||||
def perform_create(self, serializer):
|
||||
action = serializer.validated_data["action"]
|
||||
node = self.get_object()
|
||||
if action == "refresh_cache" and node is None:
|
||||
task = self.refresh_nodes_cache()
|
||||
self.set_serializer_data(serializer, task)
|
||||
return
|
||||
if node is None:
|
||||
raise Http404()
|
||||
if action == "refresh":
|
||||
task = update_node_assets_hardware_info_manual.delay(node)
|
||||
else:
|
||||
task = test_node_assets_connectivity_manual.delay(node)
|
||||
self.set_serializer_data(serializer, task)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
self.get(*args, **kwargs)
|
||||
return Response(status=204)
|
||||
|
||||
@@ -1,42 +1,25 @@
|
||||
# ~*~ 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.shortcuts import get_object_or_404
|
||||
from rest_framework.response import Response
|
||||
from django.db.models import Count
|
||||
|
||||
from common.serializers import CeleryTaskSerializer
|
||||
from common.utils import get_logger
|
||||
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, IsAppUser
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from orgs.mixins import generics
|
||||
from orgs.utils import tmp_to_org
|
||||
from ..models import SystemUser, Asset
|
||||
from .. import serializers
|
||||
from ..serializers import SystemUserWithAuthInfoSerializer
|
||||
from ..tasks import (
|
||||
push_system_user_to_assets_manual, test_system_user_connectivity_manual,
|
||||
push_system_user_a_asset_manual, test_system_user_connectivity_a_asset,
|
||||
push_system_user_a_asset_manual,
|
||||
)
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
__all__ = [
|
||||
'SystemUserViewSet', 'SystemUserAuthInfoApi', 'SystemUserAssetAuthInfoApi',
|
||||
'SystemUserPushApi', 'SystemUserTestConnectiveApi',
|
||||
'SystemUserAssetsListView', 'SystemUserPushToAssetApi',
|
||||
'SystemUserTestAssetConnectivityApi', 'SystemUserCommandFilterRuleListApi',
|
||||
|
||||
'SystemUserCommandFilterRuleListApi', 'SystemUserTaskApi',
|
||||
]
|
||||
|
||||
|
||||
@@ -45,16 +28,15 @@ 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 = {
|
||||
'default': serializers.SystemUserSerializer,
|
||||
'list': serializers.SystemUserListSerializer,
|
||||
}
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
queryset = queryset.annotate(_assets_amount=Count('assets'))
|
||||
return queryset
|
||||
|
||||
|
||||
class SystemUserAuthInfoApi(generics.RetrieveUpdateDestroyAPIView):
|
||||
"""
|
||||
@@ -62,7 +44,7 @@ class SystemUserAuthInfoApi(generics.RetrieveUpdateDestroyAPIView):
|
||||
"""
|
||||
model = SystemUser
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = serializers.SystemUserAuthSerializer
|
||||
serializer_class = SystemUserWithAuthInfoSerializer
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
@@ -75,88 +57,61 @@ class SystemUserAssetAuthInfoApi(generics.RetrieveAPIView):
|
||||
Get system user with asset auth info
|
||||
"""
|
||||
model = SystemUser
|
||||
permission_classes = (IsAppUser,)
|
||||
serializer_class = serializers.SystemUserAuthSerializer
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = SystemUserWithAuthInfoSerializer
|
||||
|
||||
def get_exception_handler(self):
|
||||
def handler(e, context):
|
||||
return Response({"error": str(e)}, status=400)
|
||||
return handler
|
||||
|
||||
def get_object(self):
|
||||
instance = super().get_object()
|
||||
aid = self.kwargs.get('aid')
|
||||
asset = get_object_or_404(Asset, pk=aid)
|
||||
instance.load_specific_asset_auth(asset)
|
||||
return instance
|
||||
username = instance.username
|
||||
if instance.username_same_with_user:
|
||||
username = self.request.query_params.get("username")
|
||||
asset_id = self.kwargs.get('aid')
|
||||
asset = get_object_or_404(Asset, pk=asset_id)
|
||||
|
||||
with tmp_to_org(asset.org_id):
|
||||
instance.load_asset_special_auth(asset=asset, username=username)
|
||||
return instance
|
||||
|
||||
|
||||
class SystemUserPushApi(generics.RetrieveAPIView):
|
||||
"""
|
||||
Push system user to cluster assets api
|
||||
"""
|
||||
model = SystemUser
|
||||
class SystemUserTaskApi(generics.CreateAPIView):
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = CeleryTaskSerializer
|
||||
serializer_class = serializers.SystemUserTaskSerializer
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
system_user = self.get_object()
|
||||
nodes = system_user.nodes.all()
|
||||
for node in nodes:
|
||||
system_user.assets.add(*tuple(node.get_all_assets()))
|
||||
task = push_system_user_to_assets_manual.delay(system_user)
|
||||
return Response({"task": task.id})
|
||||
def do_push(self, system_user, asset=None):
|
||||
if asset is None:
|
||||
task = push_system_user_to_assets_manual.delay(system_user)
|
||||
else:
|
||||
username = self.request.query_params.get('username')
|
||||
task = push_system_user_a_asset_manual.delay(
|
||||
system_user, asset, username=username
|
||||
)
|
||||
return task
|
||||
|
||||
|
||||
class SystemUserTestConnectiveApi(generics.RetrieveAPIView):
|
||||
"""
|
||||
Push system user to cluster assets api
|
||||
"""
|
||||
model = SystemUser
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = CeleryTaskSerializer
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
system_user = self.get_object()
|
||||
@staticmethod
|
||||
def do_test(system_user, asset=None):
|
||||
task = test_system_user_connectivity_manual.delay(system_user)
|
||||
return Response({"task": task.id})
|
||||
|
||||
|
||||
class SystemUserAssetsListView(generics.ListAPIView):
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.AssetSimpleSerializer
|
||||
filter_fields = ("hostname", "ip")
|
||||
http_method_names = ['get']
|
||||
search_fields = filter_fields
|
||||
return task
|
||||
|
||||
def get_object(self):
|
||||
pk = self.kwargs.get('pk')
|
||||
return get_object_or_404(SystemUser, pk=pk)
|
||||
|
||||
def get_queryset(self):
|
||||
def perform_create(self, serializer):
|
||||
action = serializer.validated_data["action"]
|
||||
asset = serializer.validated_data.get('asset')
|
||||
system_user = self.get_object()
|
||||
return system_user.assets.all()
|
||||
|
||||
|
||||
class SystemUserPushToAssetApi(generics.RetrieveAPIView):
|
||||
model = SystemUser
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.TaskIDSerializer
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
system_user = self.get_object()
|
||||
asset_id = self.kwargs.get('aid')
|
||||
asset = get_object_or_404(Asset, id=asset_id)
|
||||
task = push_system_user_a_asset_manual.delay(system_user, asset)
|
||||
return Response({"task": task.id})
|
||||
|
||||
|
||||
class SystemUserTestAssetConnectivityApi(generics.RetrieveAPIView):
|
||||
model = SystemUser
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.TaskIDSerializer
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
system_user = self.get_object()
|
||||
asset_id = self.kwargs.get('aid')
|
||||
asset = get_object_or_404(Asset, id=asset_id)
|
||||
task = test_system_user_connectivity_a_asset.delay(system_user, asset)
|
||||
return Response({"task": task.id})
|
||||
if action == 'push':
|
||||
task = self.do_push(system_user, asset)
|
||||
else:
|
||||
task = self.do_test(system_user, asset)
|
||||
data = getattr(serializer, '_data', {})
|
||||
data["task"] = task.id
|
||||
setattr(serializer, '_data', data)
|
||||
|
||||
|
||||
class SystemUserCommandFilterRuleListApi(generics.ListAPIView):
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from collections import defaultdict
|
||||
from django.db.models import F, Value
|
||||
from django.db.models.signals import m2m_changed
|
||||
from django.db.models.functions import Concat
|
||||
|
||||
from common.permissions import IsOrgAdmin
|
||||
@@ -8,10 +10,13 @@ from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from orgs.utils import current_org
|
||||
from .. import models, serializers
|
||||
|
||||
__all__ = ['SystemUserAssetRelationViewSet', 'SystemUserNodeRelationViewSet']
|
||||
__all__ = [
|
||||
'SystemUserAssetRelationViewSet', 'SystemUserNodeRelationViewSet',
|
||||
'SystemUserUserRelationViewSet',
|
||||
]
|
||||
|
||||
|
||||
class RelationMixin(OrgBulkModelViewSet):
|
||||
class RelationMixin:
|
||||
def get_queryset(self):
|
||||
queryset = self.model.objects.all()
|
||||
org_id = current_org.org_id()
|
||||
@@ -23,12 +28,44 @@ class RelationMixin(OrgBulkModelViewSet):
|
||||
))
|
||||
return queryset
|
||||
|
||||
def send_post_add_signal(self, instance):
|
||||
if not isinstance(instance, list):
|
||||
instance = [instance]
|
||||
|
||||
class SystemUserAssetRelationViewSet(RelationMixin):
|
||||
system_users_objects_map = defaultdict(list)
|
||||
model, object_field = self.get_objects_attr()
|
||||
|
||||
for i in instance:
|
||||
_id = getattr(i, object_field).id
|
||||
system_users_objects_map[i.systemuser].append(_id)
|
||||
|
||||
sender = self.get_sender()
|
||||
for system_user, objects in system_users_objects_map.items():
|
||||
m2m_changed.send(
|
||||
sender=sender, instance=system_user, action='post_add',
|
||||
reverse=False, model=model, pk_set=objects
|
||||
)
|
||||
|
||||
def get_sender(self):
|
||||
return self.model
|
||||
|
||||
def get_objects_attr(self):
|
||||
return models.Asset, 'asset'
|
||||
|
||||
def perform_create(self, serializer):
|
||||
instance = serializer.save()
|
||||
self.send_post_add_signal(instance)
|
||||
|
||||
|
||||
class BaseRelationViewSet(RelationMixin, OrgBulkModelViewSet):
|
||||
pass
|
||||
|
||||
|
||||
class SystemUserAssetRelationViewSet(BaseRelationViewSet):
|
||||
serializer_class = serializers.SystemUserAssetRelationSerializer
|
||||
model = models.SystemUser.assets.through
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
filterset_fields = [
|
||||
filter_fields = [
|
||||
'id', 'asset', 'systemuser',
|
||||
]
|
||||
search_fields = [
|
||||
@@ -36,6 +73,9 @@ class SystemUserAssetRelationViewSet(RelationMixin):
|
||||
"systemuser__name", "systemuser__username"
|
||||
]
|
||||
|
||||
def get_objects_attr(self):
|
||||
return models.Asset, 'asset'
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
queryset = queryset.annotate(
|
||||
@@ -47,19 +87,50 @@ class SystemUserAssetRelationViewSet(RelationMixin):
|
||||
return queryset
|
||||
|
||||
|
||||
class SystemUserNodeRelationViewSet(RelationMixin):
|
||||
class SystemUserNodeRelationViewSet(BaseRelationViewSet):
|
||||
serializer_class = serializers.SystemUserNodeRelationSerializer
|
||||
model = models.SystemUser.nodes.through
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
filterset_fields = [
|
||||
filter_fields = [
|
||||
'id', 'node', 'systemuser',
|
||||
]
|
||||
search_fields = [
|
||||
"node__value", "systemuser__name", "systemuser_username"
|
||||
]
|
||||
|
||||
def get_objects_attr(self):
|
||||
return models.Node, 'node'
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
queryset = queryset \
|
||||
.annotate(node_key=F('node__key'))
|
||||
return queryset
|
||||
|
||||
|
||||
class SystemUserUserRelationViewSet(BaseRelationViewSet):
|
||||
serializer_class = serializers.SystemUserUserRelationSerializer
|
||||
model = models.SystemUser.users.through
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
filter_fields = [
|
||||
'id', 'user', 'systemuser',
|
||||
]
|
||||
search_fields = [
|
||||
"user__username", "user__name",
|
||||
"systemuser__name", "systemuser__username",
|
||||
]
|
||||
|
||||
def get_objects_attr(self):
|
||||
from users.models import User
|
||||
return User, 'user'
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
queryset = queryset.annotate(
|
||||
user_display=Concat(
|
||||
F('user__name'), Value('('),
|
||||
F('user__username'), Value(')')
|
||||
)
|
||||
)
|
||||
return queryset
|
||||
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from ..models import AdminUser
|
||||
from .asset_user import AssetUserBackend
|
||||
|
||||
|
||||
class AdminUserBackend(AssetUserBackend):
|
||||
model = AdminUser
|
||||
backend = 'AdminUser'
|
||||
@@ -1,58 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from collections import defaultdict
|
||||
from .base import BaseBackend
|
||||
|
||||
|
||||
class AssetUserBackend(BaseBackend):
|
||||
model = None
|
||||
backend = "AssetUser"
|
||||
|
||||
@classmethod
|
||||
def filter_queryset_more(cls, queryset):
|
||||
return queryset
|
||||
|
||||
@classmethod
|
||||
def filter(cls, username=None, assets=None, **kwargs):
|
||||
queryset = cls.model.objects.all()
|
||||
prefer_id = kwargs.get('prefer_id')
|
||||
if prefer_id:
|
||||
queryset = queryset.filter(id=prefer_id)
|
||||
instances = cls.construct_authbook_objects(queryset, assets)
|
||||
return instances
|
||||
if username:
|
||||
queryset = queryset.filter(username=username)
|
||||
if assets:
|
||||
queryset = queryset.filter(assets__in=assets).distinct()
|
||||
|
||||
queryset = cls.filter_queryset_more(queryset)
|
||||
instances = cls.construct_authbook_objects(queryset, assets)
|
||||
return instances
|
||||
|
||||
@classmethod
|
||||
def construct_authbook_objects(cls, asset_users, assets):
|
||||
instances = []
|
||||
assets_user_assets_map = defaultdict(set)
|
||||
if isinstance(asset_users, list):
|
||||
assets_user_assets_map = {
|
||||
asset_user.id: asset_user.assets.values_list('id', flat=True)
|
||||
for asset_user in asset_users
|
||||
}
|
||||
else:
|
||||
assets_user_assets = asset_users.values_list('id', 'assets')
|
||||
for i, asset_id in assets_user_assets:
|
||||
assets_user_assets_map[i].add(asset_id)
|
||||
|
||||
for asset_user in asset_users:
|
||||
if not assets:
|
||||
related_assets = asset_user.assets.all()
|
||||
else:
|
||||
assets_map = {a.id: a for a in assets}
|
||||
related_assets = [
|
||||
assets_map.get(i) for i in assets_user_assets_map.get(asset_user.id) if i in assets_map
|
||||
]
|
||||
for asset in related_assets:
|
||||
instance = asset_user.construct_to_authbook(asset)
|
||||
instance.backend = cls.backend
|
||||
instances.append(instance)
|
||||
return instances
|
||||
@@ -1,94 +1,48 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import uuid
|
||||
from abc import abstractmethod
|
||||
|
||||
from ..models import Asset
|
||||
|
||||
|
||||
class BaseBackend:
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def filter(cls, username=None, assets=None, latest=True, prefer=None, prefer_id=None):
|
||||
"""
|
||||
:param username: 用户名
|
||||
:param assets: <Asset>对象
|
||||
:param latest: 是否是最新记录
|
||||
:param prefer: 优先使用
|
||||
:param prefer_id: 使用id
|
||||
:return: 元素为<AuthBook>的可迭代对象(<list> or <QuerySet>)
|
||||
"""
|
||||
def all(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def filter(self, username=None, hostname=None, ip=None, assets=None,
|
||||
node=None, prefer_id=None, **kwargs):
|
||||
pass
|
||||
|
||||
class AssetUserQuerySet(list):
|
||||
def order_by(self, *ordering):
|
||||
_ordering = []
|
||||
reverse = False
|
||||
for i in ordering:
|
||||
if i[0] == '-':
|
||||
reverse = True
|
||||
i = i[1:]
|
||||
_ordering.append(i)
|
||||
self.sort(key=lambda obj: [getattr(obj, j) for j in _ordering], reverse=reverse)
|
||||
return self
|
||||
@abstractmethod
|
||||
def search(self, item):
|
||||
pass
|
||||
|
||||
def filter_in(self, kwargs):
|
||||
in_kwargs = {}
|
||||
queryset = []
|
||||
for k, v in kwargs.items():
|
||||
if len(v) == 0:
|
||||
return self
|
||||
if k.find("__in") >= 0:
|
||||
_k = k.split('__')[0]
|
||||
in_kwargs[_k] = v
|
||||
else:
|
||||
in_kwargs[k] = v
|
||||
for k in in_kwargs:
|
||||
kwargs.pop(k, None)
|
||||
@abstractmethod
|
||||
def get_queryset(self):
|
||||
pass
|
||||
|
||||
if len(in_kwargs) == 0:
|
||||
return self
|
||||
for i in self:
|
||||
matched = False
|
||||
for k, v in in_kwargs.items():
|
||||
attr = getattr(i, k, None)
|
||||
# 如果属性或者value中是uuid,则转换成string
|
||||
if isinstance(v[0], uuid.UUID):
|
||||
v = [str(i) for i in v]
|
||||
if isinstance(attr, uuid.UUID):
|
||||
attr = str(attr)
|
||||
if attr in v:
|
||||
matched = True
|
||||
if matched:
|
||||
queryset.append(i)
|
||||
return AssetUserQuerySet(queryset)
|
||||
@abstractmethod
|
||||
def delete(self, union_id):
|
||||
pass
|
||||
|
||||
def filter_equal(self, kwargs):
|
||||
def filter_it(obj):
|
||||
wanted = []
|
||||
real = []
|
||||
for k, v in kwargs.items():
|
||||
wanted.append(v)
|
||||
value = getattr(obj, k, None)
|
||||
if isinstance(value, uuid.UUID):
|
||||
value = str(value)
|
||||
real.append(value)
|
||||
return wanted == real
|
||||
kwargs = {k: v for k, v in kwargs.items() if k.find('__in') == -1}
|
||||
if len(kwargs) > 0:
|
||||
queryset = AssetUserQuerySet([i for i in self if filter_it(i)])
|
||||
else:
|
||||
queryset = self
|
||||
return queryset
|
||||
@staticmethod
|
||||
def qs_to_values(qs):
|
||||
values = qs.values(
|
||||
'hostname', 'ip', "asset_id",
|
||||
'username', 'password', 'private_key', 'public_key',
|
||||
'score', 'version',
|
||||
"asset_username", "union_id",
|
||||
'date_created', 'date_updated',
|
||||
'org_id', 'backend',
|
||||
)
|
||||
return values
|
||||
|
||||
def filter(self, **kwargs):
|
||||
queryset = self.filter_in(kwargs).filter_equal(kwargs)
|
||||
return queryset
|
||||
|
||||
def distinct(self):
|
||||
items = list(set(self))
|
||||
self[:] = items
|
||||
return self
|
||||
|
||||
def __or__(self, other):
|
||||
self.extend(other)
|
||||
return self
|
||||
@staticmethod
|
||||
def make_assets_as_id(assets):
|
||||
if not assets:
|
||||
return []
|
||||
if isinstance(assets[0], Asset):
|
||||
assets = [a.id for a in assets]
|
||||
return assets
|
||||
|
||||
@@ -1,29 +1,318 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.utils.translation import ugettext as _
|
||||
from functools import reduce
|
||||
from django.db.models import F, CharField, Value, IntegerField, Q, Count
|
||||
from django.db.models.functions import Concat
|
||||
|
||||
from ..models import AuthBook
|
||||
from common.utils import get_object_or_none
|
||||
from orgs.utils import current_org
|
||||
from ..models import AuthBook, SystemUser, Asset, AdminUser
|
||||
from .base import BaseBackend
|
||||
|
||||
|
||||
class AuthBookBackend(BaseBackend):
|
||||
@classmethod
|
||||
def filter(cls, username=None, assets=None, latest=True, **kwargs):
|
||||
queryset = AuthBook.objects.all()
|
||||
if username is not None:
|
||||
queryset = queryset.filter(username=username)
|
||||
if assets:
|
||||
queryset = queryset.filter(asset__in=assets)
|
||||
if latest:
|
||||
queryset = queryset.latest_version()
|
||||
return queryset
|
||||
class DBBackend(BaseBackend):
|
||||
union_id_length = 2
|
||||
|
||||
@classmethod
|
||||
def create(cls, **kwargs):
|
||||
auth_info = {
|
||||
'password': kwargs.pop('password', ''),
|
||||
'public_key': kwargs.pop('public_key', ''),
|
||||
'private_key': kwargs.pop('private_key', '')
|
||||
}
|
||||
obj = AuthBook.objects.create(**kwargs)
|
||||
obj.set_auth(**auth_info)
|
||||
return obj
|
||||
def __init__(self, queryset=None):
|
||||
if queryset is None:
|
||||
queryset = self.all()
|
||||
self.queryset = queryset
|
||||
|
||||
def _clone(self):
|
||||
return self.__class__(self.queryset)
|
||||
|
||||
def all(self):
|
||||
return AuthBook.objects.none()
|
||||
|
||||
def count(self):
|
||||
return self.queryset.count()
|
||||
|
||||
def get_queryset(self):
|
||||
return self.queryset
|
||||
|
||||
def delete(self, union_id):
|
||||
cleaned_union_id = union_id.split('_')
|
||||
# 如果union_id通不过本检查,代表可能不是本backend, 应该返回空
|
||||
if not self._check_union_id(union_id, cleaned_union_id):
|
||||
return
|
||||
return self._perform_delete_by_union_id(cleaned_union_id)
|
||||
|
||||
def _perform_delete_by_union_id(self, union_id_cleaned):
|
||||
pass
|
||||
|
||||
def filter(self, assets=None, node=None, prefer=None, prefer_id=None,
|
||||
union_id=None, id__in=None, **kwargs):
|
||||
clone = self._clone()
|
||||
clone._filter_union_id(union_id)
|
||||
clone._filter_prefer(prefer, prefer_id)
|
||||
clone._filter_node(node)
|
||||
clone._filter_assets(assets)
|
||||
clone._filter_other(kwargs)
|
||||
clone._filter_id_in(id__in)
|
||||
return clone
|
||||
|
||||
def _filter_union_id(self, union_id):
|
||||
if not union_id:
|
||||
return
|
||||
cleaned_union_id = union_id.split('_')
|
||||
# 如果union_id通不过本检查,代表可能不是本backend, 应该返回空
|
||||
if not self._check_union_id(union_id, cleaned_union_id):
|
||||
self.queryset = self.queryset.none()
|
||||
return
|
||||
return self._perform_filter_union_id(union_id, cleaned_union_id)
|
||||
|
||||
def _check_union_id(self, union_id, cleaned_union_id):
|
||||
return union_id and len(cleaned_union_id) == self.union_id_length
|
||||
|
||||
def _perform_filter_union_id(self, union_id, union_id_cleaned):
|
||||
self.queryset = self.queryset.filter(union_id=union_id)
|
||||
|
||||
def _filter_assets(self, assets):
|
||||
assets_id = self.make_assets_as_id(assets)
|
||||
if assets_id:
|
||||
self.queryset = self.queryset.filter(asset_id__in=assets_id)
|
||||
|
||||
def _filter_node(self, node):
|
||||
pass
|
||||
|
||||
def _filter_id_in(self, ids):
|
||||
if ids and isinstance(ids, list):
|
||||
self.queryset = self.queryset.filter(union_id__in=ids)
|
||||
|
||||
@staticmethod
|
||||
def clean_kwargs(kwargs):
|
||||
return {k: v for k, v in kwargs.items() if v}
|
||||
|
||||
def _filter_other(self, kwargs):
|
||||
kwargs = self.clean_kwargs(kwargs)
|
||||
if kwargs:
|
||||
self.queryset = self.queryset.filter(**kwargs)
|
||||
|
||||
def _filter_prefer(self, prefer, prefer_id):
|
||||
pass
|
||||
|
||||
def search(self, item):
|
||||
qs = []
|
||||
for i in ['hostname', 'ip', 'username']:
|
||||
kwargs = {i + '__startswith': item}
|
||||
qs.append(Q(**kwargs))
|
||||
q = reduce(lambda x, y: x | y, qs)
|
||||
clone = self._clone()
|
||||
clone.queryset = clone.queryset.filter(q).distinct()
|
||||
return clone
|
||||
|
||||
|
||||
class SystemUserBackend(DBBackend):
|
||||
model = SystemUser.assets.through
|
||||
backend = 'system_user'
|
||||
prefer = backend
|
||||
base_score = 0
|
||||
union_id_length = 2
|
||||
|
||||
def _filter_prefer(self, prefer, prefer_id):
|
||||
if prefer and prefer != self.prefer:
|
||||
self.queryset = self.queryset.none()
|
||||
|
||||
if prefer_id:
|
||||
self.queryset = self.queryset.filter(systemuser__id=prefer_id)
|
||||
|
||||
def _perform_filter_union_id(self, union_id, union_id_cleaned):
|
||||
system_user_id, asset_id = union_id_cleaned
|
||||
self.queryset = self.queryset.filter(
|
||||
asset_id=asset_id, systemuser__id=system_user_id,
|
||||
)
|
||||
|
||||
def _perform_delete_by_union_id(self, union_id_cleaned):
|
||||
system_user_id, asset_id = union_id_cleaned
|
||||
system_user = get_object_or_none(SystemUser, pk=system_user_id)
|
||||
asset = get_object_or_none(Asset, pk=asset_id)
|
||||
if all((system_user, asset)):
|
||||
system_user.assets.remove(asset)
|
||||
|
||||
def _filter_node(self, node):
|
||||
if node:
|
||||
self.queryset = self.queryset.filter(asset__nodes__id=node.id)
|
||||
|
||||
def get_annotate(self):
|
||||
kwargs = dict(
|
||||
hostname=F("asset__hostname"),
|
||||
ip=F("asset__ip"),
|
||||
username=F("systemuser__username"),
|
||||
password=F("systemuser__password"),
|
||||
private_key=F("systemuser__private_key"),
|
||||
public_key=F("systemuser__public_key"),
|
||||
score=F("systemuser__priority") + self.base_score,
|
||||
version=Value(0, IntegerField()),
|
||||
date_created=F("systemuser__date_created"),
|
||||
date_updated=F("systemuser__date_updated"),
|
||||
asset_username=Concat(F("asset__id"), Value("_"),
|
||||
F("systemuser__username"),
|
||||
output_field=CharField()),
|
||||
union_id=Concat(F("systemuser_id"), Value("_"), F("asset_id"),
|
||||
output_field=CharField()),
|
||||
org_id=F("asset__org_id"),
|
||||
backend=Value(self.backend, CharField())
|
||||
)
|
||||
return kwargs
|
||||
|
||||
def get_filter(self):
|
||||
return dict(
|
||||
systemuser__username_same_with_user=False,
|
||||
)
|
||||
|
||||
def all(self):
|
||||
kwargs = self.get_annotate()
|
||||
filters = self.get_filter()
|
||||
qs = self.model.objects.all().annotate(**kwargs)
|
||||
if current_org.org_id() is not None:
|
||||
filters['org_id'] = current_org.org_id()
|
||||
qs = qs.filter(**filters)
|
||||
qs = self.qs_to_values(qs)
|
||||
return qs
|
||||
|
||||
|
||||
class DynamicSystemUserBackend(SystemUserBackend):
|
||||
backend = 'system_user_dynamic'
|
||||
prefer = 'system_user'
|
||||
union_id_length = 3
|
||||
|
||||
def get_annotate(self):
|
||||
kwargs = super().get_annotate()
|
||||
kwargs.update(dict(
|
||||
username=F("systemuser__users__username"),
|
||||
asset_username=Concat(
|
||||
F("asset__id"), Value("_"),
|
||||
F("systemuser__users__username"),
|
||||
output_field=CharField()
|
||||
),
|
||||
union_id=Concat(
|
||||
F("systemuser_id"), Value("_"), F("asset_id"),
|
||||
Value("_"), F("systemuser__users__id"),
|
||||
output_field=CharField()
|
||||
),
|
||||
users_count=Count('systemuser__users'),
|
||||
))
|
||||
return kwargs
|
||||
|
||||
def _perform_filter_union_id(self, union_id, union_id_cleaned):
|
||||
system_user_id, asset_id, user_id = union_id_cleaned
|
||||
self.queryset = self.queryset.filter(
|
||||
asset_id=asset_id, systemuser_id=system_user_id,
|
||||
union_id=union_id,
|
||||
)
|
||||
|
||||
def _perform_delete_by_union_id(self, union_id_cleaned):
|
||||
system_user_id, asset_id, user_id = union_id_cleaned
|
||||
system_user = get_object_or_none(SystemUser, pk=system_user_id)
|
||||
if not system_user:
|
||||
return
|
||||
system_user.users.remove(user_id)
|
||||
if system_user.users.count() == 0:
|
||||
system_user.assets.remove(asset_id)
|
||||
|
||||
def get_filter(self):
|
||||
return dict(
|
||||
users_count__gt=0,
|
||||
systemuser__username_same_with_user=True
|
||||
)
|
||||
|
||||
|
||||
class AdminUserBackend(DBBackend):
|
||||
model = Asset
|
||||
backend = 'admin_user'
|
||||
prefer = backend
|
||||
base_score = 200
|
||||
|
||||
def _filter_prefer(self, prefer, prefer_id):
|
||||
if prefer and prefer != self.backend:
|
||||
self.queryset = self.queryset.none()
|
||||
if prefer_id:
|
||||
self.queryset = self.queryset.filter(admin_user__id=prefer_id)
|
||||
|
||||
def _filter_node(self, node):
|
||||
if node:
|
||||
self.queryset = self.queryset.filter(nodes__id=node.id)
|
||||
|
||||
def _perform_filter_union_id(self, union_id, union_id_cleaned):
|
||||
admin_user_id, asset_id = union_id_cleaned
|
||||
self.queryset = self.queryset.filter(
|
||||
id=asset_id, admin_user_id=admin_user_id,
|
||||
)
|
||||
|
||||
def _perform_delete_by_union_id(self, union_id_cleaned):
|
||||
raise PermissionError(_("Could not remove asset admin user"))
|
||||
|
||||
def all(self):
|
||||
qs = self.model.objects.all().annotate(
|
||||
asset_id=F("id"),
|
||||
username=F("admin_user__username"),
|
||||
password=F("admin_user__password"),
|
||||
private_key=F("admin_user__private_key"),
|
||||
public_key=F("admin_user__public_key"),
|
||||
score=Value(self.base_score, IntegerField()),
|
||||
version=Value(0, IntegerField()),
|
||||
date_updated=F("admin_user__date_updated"),
|
||||
asset_username=Concat(F("id"), Value("_"), F("admin_user__username"), output_field=CharField()),
|
||||
union_id=Concat(F("admin_user_id"), Value("_"), F("id"), output_field=CharField()),
|
||||
backend=Value(self.backend, CharField()),
|
||||
)
|
||||
qs = self.qs_to_values(qs)
|
||||
return qs
|
||||
|
||||
|
||||
class AuthbookBackend(DBBackend):
|
||||
model = AuthBook
|
||||
backend = 'db'
|
||||
prefer = backend
|
||||
base_score = 400
|
||||
|
||||
def _filter_node(self, node):
|
||||
if node:
|
||||
self.queryset = self.queryset.filter(asset__nodes__id=node.id)
|
||||
|
||||
def _filter_prefer(self, prefer, prefer_id):
|
||||
if not prefer or not prefer_id:
|
||||
return
|
||||
if prefer.lower() == "admin_user":
|
||||
model = AdminUser
|
||||
elif prefer.lower() == "system_user":
|
||||
model = SystemUser
|
||||
else:
|
||||
self.queryset = self.queryset.none()
|
||||
return
|
||||
obj = get_object_or_none(model, pk=prefer_id)
|
||||
if obj is None:
|
||||
self.queryset = self.queryset.none()
|
||||
return
|
||||
username = obj.get_username()
|
||||
if isinstance(username, str):
|
||||
self.queryset = self.queryset.filter(username=username)
|
||||
# dynamic system user return more username
|
||||
else:
|
||||
self.queryset = self.queryset.filter(username__in=username)
|
||||
|
||||
def _perform_filter_union_id(self, union_id, union_id_cleaned):
|
||||
authbook_id, asset_id = union_id_cleaned
|
||||
self.queryset = self.queryset.filter(
|
||||
id=authbook_id, asset_id=asset_id,
|
||||
)
|
||||
|
||||
def _perform_delete_by_union_id(self, union_id_cleaned):
|
||||
authbook_id, asset_id = union_id_cleaned
|
||||
authbook = get_object_or_none(AuthBook, pk=authbook_id)
|
||||
if authbook.is_latest:
|
||||
raise PermissionError(_("Latest version could not be delete"))
|
||||
AuthBook.objects.filter(id=authbook_id).delete()
|
||||
|
||||
def all(self):
|
||||
qs = self.model.objects.all().annotate(
|
||||
hostname=F("asset__hostname"),
|
||||
ip=F("asset__ip"),
|
||||
score=F('version') + self.base_score,
|
||||
asset_username=Concat(F("asset__id"), Value("_"), F("username"), output_field=CharField()),
|
||||
union_id=Concat(F("id"), Value("_"), F("asset_id"), output_field=CharField()),
|
||||
backend=Value(self.backend, CharField()),
|
||||
)
|
||||
qs = self.qs_to_values(qs)
|
||||
return qs
|
||||
|
||||
@@ -1,110 +1,162 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from itertools import chain, groupby
|
||||
from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
|
||||
|
||||
from .base import AssetUserQuerySet
|
||||
from .db import AuthBookBackend
|
||||
from .system_user import SystemUserBackend
|
||||
from .admin_user import AdminUserBackend
|
||||
from orgs.utils import current_org
|
||||
from common.utils import get_logger, lazyproperty
|
||||
from common.struct import QuerySetChain
|
||||
|
||||
from ..models import AssetUser, AuthBook
|
||||
from .db import (
|
||||
AuthbookBackend, SystemUserBackend, AdminUserBackend,
|
||||
DynamicSystemUserBackend
|
||||
)
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class NotSupportError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class AssetUserManager:
|
||||
"""
|
||||
资产用户管理器
|
||||
"""
|
||||
class AssetUserQueryset:
|
||||
ObjectDoesNotExist = ObjectDoesNotExist
|
||||
MultipleObjectsReturned = MultipleObjectsReturned
|
||||
NotSupportError = NotSupportError
|
||||
MSG_NOT_EXIST = '{} Object matching query does not exist'
|
||||
MSG_MULTIPLE = '{} get() returned more than one object ' \
|
||||
'-- it returned {}!'
|
||||
|
||||
backends = (
|
||||
('db', AuthBookBackend),
|
||||
def __init__(self, backends=()):
|
||||
self.backends = backends
|
||||
self._distinct_queryset = None
|
||||
|
||||
def backends_queryset(self):
|
||||
return [b.get_queryset() for b in self.backends]
|
||||
|
||||
@lazyproperty
|
||||
def backends_counts(self):
|
||||
return [b.count() for b in self.backends]
|
||||
|
||||
def filter(self, hostname=None, ip=None, username=None,
|
||||
assets=None, asset=None, node=None,
|
||||
id=None, prefer_id=None, prefer=None, id__in=None):
|
||||
if not assets and asset:
|
||||
assets = [asset]
|
||||
|
||||
kwargs = dict(
|
||||
hostname=hostname, ip=ip, username=username,
|
||||
assets=assets, node=node, prefer=prefer, prefer_id=prefer_id,
|
||||
id__in=id__in, union_id=id,
|
||||
)
|
||||
logger.debug("Filter: {}".format(kwargs))
|
||||
backends = []
|
||||
for backend in self.backends:
|
||||
clone = backend.filter(**kwargs)
|
||||
backends.append(clone)
|
||||
return self._clone(backends)
|
||||
|
||||
def _clone(self, backends=None):
|
||||
if backends is None:
|
||||
backends = self.backends
|
||||
return self.__class__(backends)
|
||||
|
||||
def search(self, item):
|
||||
backends = []
|
||||
for backend in self.backends:
|
||||
new = backend.search(item)
|
||||
backends.append(new)
|
||||
return self._clone(backends)
|
||||
|
||||
def distinct(self):
|
||||
logger.debug("Distinct asset user queryset")
|
||||
queryset_chain = chain(*(backend.get_queryset() for backend in self.backends))
|
||||
queryset_sorted = sorted(
|
||||
queryset_chain,
|
||||
key=lambda item: (item["asset_username"], item["score"]),
|
||||
reverse=True,
|
||||
)
|
||||
results = groupby(queryset_sorted, key=lambda item: item["asset_username"])
|
||||
final = [next(result[1]) for result in results]
|
||||
self._distinct_queryset = final
|
||||
return self
|
||||
|
||||
def get(self, latest=False, **kwargs):
|
||||
queryset = self.filter(**kwargs)
|
||||
if latest:
|
||||
queryset = queryset.distinct()
|
||||
queryset = list(queryset)
|
||||
count = len(queryset)
|
||||
if count == 1:
|
||||
data = queryset[0]
|
||||
return data
|
||||
elif count > 1:
|
||||
msg = 'Should return 1 record, but get {}'.format(count)
|
||||
raise MultipleObjectsReturned(msg)
|
||||
else:
|
||||
msg = 'No record found(org is {})'.format(current_org.name)
|
||||
raise ObjectDoesNotExist(msg)
|
||||
|
||||
def get_latest(self, **kwargs):
|
||||
return self.get(latest=True, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def to_asset_user(data):
|
||||
obj = AssetUser()
|
||||
for k, v in data.items():
|
||||
setattr(obj, k, v)
|
||||
return obj
|
||||
|
||||
@property
|
||||
def queryset(self):
|
||||
if self._distinct_queryset is not None:
|
||||
return self._distinct_queryset
|
||||
return QuerySetChain(self.backends_queryset())
|
||||
|
||||
def count(self):
|
||||
if self._distinct_queryset is not None:
|
||||
return len(self._distinct_queryset)
|
||||
else:
|
||||
return sum(self.backends_counts)
|
||||
|
||||
def __getitem__(self, ndx):
|
||||
return self.queryset.__getitem__(ndx)
|
||||
|
||||
def __iter__(self):
|
||||
self._data = iter(self.queryset)
|
||||
return self
|
||||
|
||||
def __next__(self):
|
||||
return self.to_asset_user(next(self._data))
|
||||
|
||||
|
||||
class AssetUserManager:
|
||||
support_backends = (
|
||||
('db', AuthbookBackend),
|
||||
('system_user', SystemUserBackend),
|
||||
('admin_user', AdminUserBackend),
|
||||
('system_user_dynamic', DynamicSystemUserBackend),
|
||||
)
|
||||
|
||||
_prefer = "system_user"
|
||||
def __init__(self):
|
||||
self.backends = [backend() for name, backend in self.support_backends]
|
||||
self._queryset = AssetUserQueryset(self.backends)
|
||||
|
||||
def filter(self, username=None, assets=None, latest=True, prefer=None, prefer_id=None):
|
||||
if assets is not None and not assets:
|
||||
return AssetUserQuerySet([])
|
||||
def all(self):
|
||||
return self._queryset
|
||||
|
||||
if prefer:
|
||||
self._prefer = prefer
|
||||
|
||||
instances_map = {}
|
||||
instances = []
|
||||
for name, backend in self.backends:
|
||||
# if name != "db":
|
||||
# continue
|
||||
_instances = backend.filter(
|
||||
username=username, assets=assets, latest=latest,
|
||||
prefer=self._prefer, prefer_id=prefer_id,
|
||||
)
|
||||
instances_map[name] = _instances
|
||||
|
||||
# 如果不是获取最新版本,就不再merge
|
||||
if not latest:
|
||||
for _instances in instances_map.values():
|
||||
instances.extend(_instances)
|
||||
return AssetUserQuerySet(instances)
|
||||
|
||||
# merge的顺序
|
||||
ordering = ["db"]
|
||||
if self._prefer == "system_user":
|
||||
ordering.extend(["system_user", "admin_user"])
|
||||
def delete(self, obj):
|
||||
name_backends_map = dict(self.support_backends)
|
||||
backend_name = obj.backend
|
||||
backend_cls = name_backends_map.get(backend_name)
|
||||
union_id = obj.union_id
|
||||
if backend_cls:
|
||||
backend_cls().delete(union_id)
|
||||
else:
|
||||
ordering.extend(["admin_user", "system_user"])
|
||||
# 根据prefer决定优先使用系统用户或管理用户谁的
|
||||
ordering_instances = [instances_map.get(i, []) for i in ordering]
|
||||
instances = self._merge_instances(*ordering_instances)
|
||||
return AssetUserQuerySet(instances)
|
||||
|
||||
def get(self, username, asset, **kwargs):
|
||||
instances = self.filter(username, assets=[asset], **kwargs)
|
||||
if len(instances) == 1:
|
||||
return instances[0]
|
||||
elif len(instances) == 0:
|
||||
self.raise_does_not_exist(self.__class__.__name__)
|
||||
else:
|
||||
self.raise_multiple_return(self.__class__.__name__, len(instances))
|
||||
|
||||
def raise_does_not_exist(self, name):
|
||||
raise self.ObjectDoesNotExist(self.MSG_NOT_EXIST.format(name))
|
||||
|
||||
def raise_multiple_return(self, name, length):
|
||||
raise self.MultipleObjectsReturned(self.MSG_MULTIPLE.format(name, length))
|
||||
raise ObjectDoesNotExist("Not backend found")
|
||||
|
||||
@staticmethod
|
||||
def create(**kwargs):
|
||||
instance = AuthBookBackend.create(**kwargs)
|
||||
return instance
|
||||
# 使用create方法创建AuthBook对象,解决并发创建问题(添加锁机制)
|
||||
authbook = AuthBook.create(**kwargs)
|
||||
return authbook
|
||||
|
||||
def all(self):
|
||||
return self.filter()
|
||||
|
||||
def prefer(self, s):
|
||||
self._prefer = s
|
||||
return self
|
||||
|
||||
@staticmethod
|
||||
def none():
|
||||
return AssetUserQuerySet()
|
||||
|
||||
@staticmethod
|
||||
def _merge_instances(*args):
|
||||
instances = list(args[0])
|
||||
keywords = [obj.keyword for obj in instances]
|
||||
|
||||
for _instances in args[1:]:
|
||||
need_merge_instances = [obj for obj in _instances if obj.keyword not in keywords]
|
||||
need_merge_keywords = [obj.keyword for obj in need_merge_instances]
|
||||
instances.extend(need_merge_instances)
|
||||
keywords.extend(need_merge_keywords)
|
||||
return instances
|
||||
def __getattr__(self, item):
|
||||
return getattr(self._queryset, item)
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
import itertools
|
||||
|
||||
from assets.models import SystemUser
|
||||
from .asset_user import AssetUserBackend
|
||||
|
||||
|
||||
class SystemUserBackend(AssetUserBackend):
|
||||
model = SystemUser
|
||||
backend = 'SystemUser'
|
||||
|
||||
@classmethod
|
||||
def filter_queryset_more(cls, queryset):
|
||||
queryset = cls._distinct_system_users_by_username(queryset)
|
||||
return queryset
|
||||
|
||||
@classmethod
|
||||
def _distinct_system_users_by_username(cls, system_users):
|
||||
system_users = sorted(
|
||||
system_users,
|
||||
key=lambda su: (su.username, su.priority, su.date_updated),
|
||||
reverse=True,
|
||||
)
|
||||
results = itertools.groupby(system_users, key=lambda su: su.username)
|
||||
system_users = [next(result[1]) for result in results]
|
||||
return system_users
|
||||
|
||||
|
||||
@@ -3,14 +3,5 @@
|
||||
|
||||
# from django.conf import settings
|
||||
|
||||
from .db import AuthBookBackend
|
||||
# from .vault import VaultBackend
|
||||
|
||||
|
||||
def get_backend():
|
||||
default_backend = AuthBookBackend
|
||||
|
||||
# if settings.BACKEND_ASSET_USER_AUTH_VAULT:
|
||||
# return VaultBackend
|
||||
|
||||
return default_backend
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from .base import BaseBackend
|
||||
|
||||
|
||||
class VaultBackend(BaseBackend):
|
||||
|
||||
@classmethod
|
||||
def filter(cls, username=None, asset=None, latest=True):
|
||||
pass
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
import coreapi
|
||||
from rest_framework.compat import coreapi, coreschema
|
||||
from rest_framework import filters
|
||||
from django.db.models import Q
|
||||
|
||||
@@ -58,12 +58,14 @@ class AssetByNodeFilterBackend(filters.BaseFilterBackend):
|
||||
if query_all:
|
||||
pattern = node.get_all_children_pattern(with_self=True)
|
||||
else:
|
||||
pattern = node.get_children_key_pattern(with_self=True)
|
||||
# pattern = node.get_children_key_pattern(with_self=True)
|
||||
# 只显示当前节点下资产
|
||||
pattern = r"^{}$".format(node.key)
|
||||
return self.perform_query(pattern, queryset)
|
||||
|
||||
|
||||
class LabelFilterBackend(filters.BaseFilterBackend):
|
||||
sep = '#'
|
||||
sep = ':'
|
||||
query_arg = 'label'
|
||||
|
||||
def get_schema_fields(self, view):
|
||||
@@ -82,6 +84,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]
|
||||
@@ -113,3 +117,23 @@ class AssetRelatedByNodeFilterBackend(AssetByNodeFilterBackend):
|
||||
def perform_query(pattern, queryset):
|
||||
return queryset.filter(asset__nodes__key__regex=pattern).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,154 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
|
||||
def set_platform_to_name(self):
|
||||
platform_field = self.fields['platform']
|
||||
platform_field.to_field_name = 'name'
|
||||
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,107 +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',
|
||||
'comment', 'shell', 'priority', 'login_mode', 'cmd_filters',
|
||||
]
|
||||
widgets = {
|
||||
'name': forms.TextInput(attrs={'placeholder': _('Name')}),
|
||||
'username': forms.TextInput(attrs={'placeholder': _('Username')}),
|
||||
'cmd_filters': forms.SelectMultiple(attrs={
|
||||
'class': 'select2', 'data-placeholder': _('Command filter')
|
||||
}),
|
||||
}
|
||||
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")
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
Other module of this app shouldn't connect with other app.
|
||||
|
||||
:copyright: (c) 2014-2018 by Jumpserver Team.
|
||||
:copyright: (c) 2014-2018 by JumpServer Team.
|
||||
:license: GPL v2, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
|
||||
24
apps/assets/migrations/0047_assetuser.py
Normal file
24
apps/assets/migrations/0047_assetuser.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# Generated by Django 2.2.7 on 2020-01-06 07:34
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0046_auto_20191218_1705'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='AssetUser',
|
||||
fields=[
|
||||
],
|
||||
options={
|
||||
'proxy': True,
|
||||
'indexes': [],
|
||||
'constraints': [],
|
||||
},
|
||||
bases=('assets.authbook',),
|
||||
),
|
||||
]
|
||||
35
apps/assets/migrations/0048_auto_20191230_1512.py
Normal file
35
apps/assets/migrations/0048_auto_20191230_1512.py
Normal file
@@ -0,0 +1,35 @@
|
||||
# Generated by Django 2.2.7 on 2019-12-30 07:12
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('assets', '0047_assetuser'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='authbook',
|
||||
name='is_active',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='systemuser',
|
||||
name='username_same_with_user',
|
||||
field=models.BooleanField(default=False, verbose_name='Username same with user'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='systemuser',
|
||||
name='users',
|
||||
field=models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='Users'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='systemuser',
|
||||
name='groups',
|
||||
field=models.ManyToManyField(blank=True, to='users.UserGroup',
|
||||
verbose_name='User groups'),
|
||||
),
|
||||
]
|
||||
18
apps/assets/migrations/0049_systemuser_sftp_root.py
Normal file
18
apps/assets/migrations/0049_systemuser_sftp_root.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 2.2.7 on 2020-01-19 07:29
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0048_auto_20191230_1512'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='systemuser',
|
||||
name='sftp_root',
|
||||
field=models.CharField(default='tmp', max_length=128, verbose_name='SFTP Root'),
|
||||
),
|
||||
]
|
||||
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')},
|
||||
),
|
||||
]
|
||||
@@ -1,6 +1,8 @@
|
||||
from .base import *
|
||||
from .asset import *
|
||||
from .label import Label
|
||||
from .user import *
|
||||
from .asset_user import *
|
||||
from .cluster import *
|
||||
from .group import *
|
||||
from .domain import *
|
||||
|
||||
@@ -14,6 +14,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||
from common.fields.model import JsonDictTextField
|
||||
from common.utils import lazyproperty
|
||||
from orgs.mixins.models import OrgModelMixin, OrgManager
|
||||
from .base import ConnectivityMixin
|
||||
from .utils import Connectivity
|
||||
|
||||
__all__ = ['Asset', 'ProtocolsMixin', 'Platform']
|
||||
@@ -220,7 +221,7 @@ 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'))
|
||||
|
||||
@@ -243,6 +244,13 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
|
||||
def platform_base(self):
|
||||
return self.platform.base
|
||||
|
||||
@lazyproperty
|
||||
def admin_user_username(self):
|
||||
"""求可连接性时,直接用用户名去取,避免再查一次admin user
|
||||
serializer 中直接通过annotate方式返回了这个
|
||||
"""
|
||||
return self.admin_user.username
|
||||
|
||||
def is_windows(self):
|
||||
return self.platform.is_windows()
|
||||
|
||||
@@ -275,9 +283,11 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
|
||||
def connectivity(self):
|
||||
if self._connectivity:
|
||||
return self._connectivity
|
||||
if not self.admin_user:
|
||||
if not self.admin_user_username:
|
||||
return Connectivity.unknown()
|
||||
connectivity = self.admin_user.get_asset_connectivity(self)
|
||||
connectivity = ConnectivityMixin.get_asset_username_connectivity(
|
||||
self, self.admin_user_username
|
||||
)
|
||||
return connectivity
|
||||
|
||||
@connectivity.setter
|
||||
@@ -290,7 +300,7 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
|
||||
if not self.admin_user:
|
||||
return {}
|
||||
|
||||
self.admin_user.load_specific_asset_auth(self)
|
||||
self.admin_user.load_asset_special_auth(self)
|
||||
info = {
|
||||
'username': self.admin_user.username,
|
||||
'password': self.admin_user.password,
|
||||
|
||||
14
apps/assets/models/asset_user.py
Normal file
14
apps/assets/models/asset_user.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from .authbook import AuthBook
|
||||
|
||||
|
||||
class AssetUser(AuthBook):
|
||||
hostname = ""
|
||||
ip = ""
|
||||
backend = ""
|
||||
union_id = ""
|
||||
asset_username = ""
|
||||
|
||||
class Meta:
|
||||
proxy = True
|
||||
@@ -1,30 +1,31 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from django.db import models
|
||||
from django.db import models, transaction
|
||||
from django.db.models import Max
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orgs.mixins.models import OrgManager
|
||||
from .base import AssetUser
|
||||
from .base import BaseUser
|
||||
|
||||
__all__ = ['AuthBook']
|
||||
|
||||
|
||||
class AuthBookQuerySet(models.QuerySet):
|
||||
|
||||
def latest_version(self):
|
||||
return self.filter(is_latest=True).filter(is_active=True)
|
||||
def delete(self):
|
||||
if self.count() > 1:
|
||||
raise PermissionError(_("Bulk delete deny"))
|
||||
return super().delete()
|
||||
|
||||
|
||||
class AuthBookManager(OrgManager):
|
||||
pass
|
||||
|
||||
|
||||
class AuthBook(AssetUser):
|
||||
class AuthBook(BaseUser):
|
||||
asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, verbose_name=_('Asset'))
|
||||
is_latest = models.BooleanField(default=False, verbose_name=_('Latest version'))
|
||||
version = models.IntegerField(default=1, verbose_name=_('Version'))
|
||||
is_active = models.BooleanField(default=True, verbose_name=_("Is active"))
|
||||
|
||||
objects = AuthBookManager.from_queryset(AuthBookQuerySet)()
|
||||
backend = "db"
|
||||
@@ -35,37 +36,40 @@ class AuthBook(AssetUser):
|
||||
class Meta:
|
||||
verbose_name = _('AuthBook')
|
||||
|
||||
def set_to_latest(self):
|
||||
self.remove_pre_latest()
|
||||
self.is_latest = True
|
||||
self.save()
|
||||
|
||||
def get_pre_latest(self):
|
||||
pre_obj = self.__class__.objects.filter(
|
||||
username=self.username, asset=self.asset
|
||||
).latest_version().first()
|
||||
return pre_obj
|
||||
|
||||
def remove_pre_latest(self):
|
||||
pre_obj = self.get_pre_latest()
|
||||
if pre_obj:
|
||||
pre_obj.is_latest = False
|
||||
pre_obj.save()
|
||||
|
||||
def set_version(self):
|
||||
pre_obj = self.get_pre_latest()
|
||||
if pre_obj:
|
||||
self.version = pre_obj.version + 1
|
||||
else:
|
||||
self.version = 1
|
||||
self.save()
|
||||
|
||||
def get_related_assets(self):
|
||||
return [self.asset]
|
||||
|
||||
def generate_id_with_asset(self, asset):
|
||||
return self.id
|
||||
|
||||
@classmethod
|
||||
def get_max_version(cls, username, asset):
|
||||
version_max = cls.objects.filter(username=username, asset=asset) \
|
||||
.aggregate(Max('version'))
|
||||
version_max = version_max['version__max'] or 0
|
||||
return version_max
|
||||
|
||||
@classmethod
|
||||
def create(cls, **kwargs):
|
||||
"""
|
||||
使用并发锁机制创建AuthBook对象, (主要针对并发创建 username, asset 相同的对象时)
|
||||
并更新其他对象的 is_latest=False (其他对象: 与当前对象的 username, asset 相同)
|
||||
同时设置自己的 is_latest=True, version=max_version + 1
|
||||
"""
|
||||
username = kwargs['username']
|
||||
asset = kwargs['asset']
|
||||
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):
|
||||
return self.get_asset_connectivity(self.asset)
|
||||
|
||||
@@ -12,98 +12,29 @@ from django.utils.translation import ugettext_lazy as _
|
||||
from django.conf import settings
|
||||
|
||||
from common.utils import (
|
||||
signer, ssh_key_string_to_obj, ssh_key_gen, get_logger
|
||||
ssh_key_string_to_obj, ssh_key_gen, get_logger, lazyproperty
|
||||
)
|
||||
from common.validators import alphanumeric
|
||||
from common import fields
|
||||
from orgs.mixins.models import OrgModelMixin
|
||||
from .utils import private_key_validator, Connectivity
|
||||
from .utils import Connectivity
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
class AssetUser(OrgModelMixin):
|
||||
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)
|
||||
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'))
|
||||
comment = models.TextField(blank=True, verbose_name=_('Comment'))
|
||||
date_created = models.DateTimeField(auto_now_add=True, verbose_name=_("Date created"))
|
||||
date_updated = models.DateTimeField(auto_now=True, verbose_name=_("Date updated"))
|
||||
created_by = models.CharField(max_length=128, null=True, verbose_name=_('Created by'))
|
||||
|
||||
class ConnectivityMixin:
|
||||
CONNECTIVITY_ASSET_CACHE_KEY = "ASSET_USER_{}_{}_ASSET_CONNECTIVITY"
|
||||
CONNECTIVITY_AMOUNT_CACHE_KEY = "ASSET_USER_{}_{}_CONNECTIVITY_AMOUNT"
|
||||
ASSETS_AMOUNT_CACHE_KEY = "ASSET_USER_{}_ASSETS_AMOUNT"
|
||||
ASSET_USER_CACHE_TIME = 3600 * 24
|
||||
|
||||
_prefer = "system_user"
|
||||
_assets_amount = None
|
||||
|
||||
@property
|
||||
def private_key_obj(self):
|
||||
if self.private_key:
|
||||
return ssh_key_string_to_obj(self.private_key, password=self.password)
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def private_key_file(self):
|
||||
if not self.private_key_obj:
|
||||
return None
|
||||
project_dir = settings.PROJECT_DIR
|
||||
tmp_dir = os.path.join(project_dir, 'tmp')
|
||||
key_name = '.' + md5(self.private_key.encode('utf-8')).hexdigest()
|
||||
key_path = os.path.join(tmp_dir, key_name)
|
||||
if not os.path.exists(key_path):
|
||||
self.private_key_obj.write_private_key_file(key_path)
|
||||
os.chmod(key_path, 0o400)
|
||||
return key_path
|
||||
|
||||
@property
|
||||
def public_key_obj(self):
|
||||
if self.public_key:
|
||||
try:
|
||||
return sshpubkeys.SSHKey(self.public_key)
|
||||
except TabError:
|
||||
pass
|
||||
return None
|
||||
id = ''
|
||||
username = ''
|
||||
|
||||
@property
|
||||
def part_id(self):
|
||||
i = '-'.join(str(self.id).split('-')[:3])
|
||||
return i
|
||||
|
||||
def get_private_key(self):
|
||||
if not self.private_key_obj:
|
||||
return None
|
||||
string_io = io.StringIO()
|
||||
self.private_key_obj.write_private_key(string_io)
|
||||
private_key = string_io.getvalue()
|
||||
return private_key
|
||||
|
||||
def get_related_assets(self):
|
||||
assets = self.assets.all()
|
||||
return assets
|
||||
|
||||
def set_auth(self, password=None, private_key=None, public_key=None):
|
||||
update_fields = []
|
||||
if password:
|
||||
self.password = password
|
||||
update_fields.append('password')
|
||||
if private_key:
|
||||
self.private_key = private_key
|
||||
update_fields.append('private_key')
|
||||
if public_key:
|
||||
self.public_key = public_key
|
||||
update_fields.append('public_key')
|
||||
|
||||
if update_fields:
|
||||
self.save(update_fields=update_fields)
|
||||
|
||||
def set_connectivity(self, summary):
|
||||
unreachable = summary.get('dark', {}).keys()
|
||||
reachable = summary.get('contacted', {}).keys()
|
||||
@@ -150,20 +81,10 @@ class AssetUser(OrgModelMixin):
|
||||
cache.set(cache_key, amount, self.ASSET_USER_CACHE_TIME)
|
||||
return amount
|
||||
|
||||
@property
|
||||
def assets_amount(self):
|
||||
if self._assets_amount is not None:
|
||||
return self._assets_amount
|
||||
cache_key = self.ASSETS_AMOUNT_CACHE_KEY.format(self.id)
|
||||
cached = cache.get(cache_key)
|
||||
if not cached:
|
||||
cached = self.get_related_assets().count()
|
||||
cache.set(cache_key, cached, self.ASSET_USER_CACHE_TIME)
|
||||
return cached
|
||||
|
||||
def expire_assets_amount(self):
|
||||
cache_key = self.ASSETS_AMOUNT_CACHE_KEY.format(self.id)
|
||||
cache.delete(cache_key)
|
||||
@classmethod
|
||||
def get_asset_username_connectivity(cls, asset, username):
|
||||
key = cls.CONNECTIVITY_ASSET_CACHE_KEY.format(username, asset.id)
|
||||
return Connectivity.get(key)
|
||||
|
||||
def get_asset_connectivity(self, asset):
|
||||
key = self.get_asset_connectivity_key(asset)
|
||||
@@ -176,28 +97,103 @@ class AssetUser(OrgModelMixin):
|
||||
key = self.get_asset_connectivity_key(asset)
|
||||
Connectivity.set(key, c)
|
||||
|
||||
def get_asset_user(self, asset):
|
||||
|
||||
class AuthMixin:
|
||||
private_key = ''
|
||||
password = ''
|
||||
public_key = ''
|
||||
username = ''
|
||||
_prefer = 'system_user'
|
||||
|
||||
@property
|
||||
def private_key_obj(self):
|
||||
if self.private_key:
|
||||
key_obj = ssh_key_string_to_obj(self.private_key, password=self.password)
|
||||
return key_obj
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def private_key_file(self):
|
||||
if not self.private_key_obj:
|
||||
return None
|
||||
project_dir = settings.PROJECT_DIR
|
||||
tmp_dir = os.path.join(project_dir, 'tmp')
|
||||
key_name = '.' + md5(self.private_key.encode('utf-8')).hexdigest()
|
||||
key_path = os.path.join(tmp_dir, key_name)
|
||||
if not os.path.exists(key_path):
|
||||
self.private_key_obj.write_private_key_file(key_path)
|
||||
os.chmod(key_path, 0o400)
|
||||
return key_path
|
||||
|
||||
def get_private_key(self):
|
||||
if not self.private_key_obj:
|
||||
return None
|
||||
string_io = io.StringIO()
|
||||
self.private_key_obj.write_private_key(string_io)
|
||||
private_key = string_io.getvalue()
|
||||
return private_key
|
||||
|
||||
@property
|
||||
def public_key_obj(self):
|
||||
if self.public_key:
|
||||
try:
|
||||
return sshpubkeys.SSHKey(self.public_key)
|
||||
except TabError:
|
||||
pass
|
||||
return None
|
||||
|
||||
def set_auth(self, password=None, private_key=None, public_key=None):
|
||||
update_fields = []
|
||||
if password:
|
||||
self.password = password
|
||||
update_fields.append('password')
|
||||
if private_key:
|
||||
self.private_key = private_key
|
||||
update_fields.append('private_key')
|
||||
if public_key:
|
||||
self.public_key = public_key
|
||||
update_fields.append('public_key')
|
||||
|
||||
if update_fields:
|
||||
self.save(update_fields=update_fields)
|
||||
|
||||
def has_special_auth(self, asset=None):
|
||||
from .authbook import AuthBook
|
||||
queryset = AuthBook.objects.filter(username=self.username)
|
||||
if asset:
|
||||
queryset = queryset.filter(asset=asset)
|
||||
return queryset.exists()
|
||||
|
||||
def get_asset_user(self, asset, username=None):
|
||||
from ..backends import AssetUserManager
|
||||
if username is None:
|
||||
username = self.username
|
||||
try:
|
||||
manager = AssetUserManager().prefer(self._prefer)
|
||||
other = manager.get(username=self.username, asset=asset, prefer_id=self.id)
|
||||
manager = AssetUserManager()
|
||||
other = manager.get_latest(
|
||||
username=username, asset=asset,
|
||||
prefer_id=self.id, prefer=self._prefer,
|
||||
)
|
||||
return other
|
||||
except Exception as e:
|
||||
logger.error(e, exc_info=True)
|
||||
return None
|
||||
|
||||
def load_specific_asset_auth(self, asset):
|
||||
instance = self.get_asset_user(asset)
|
||||
def load_asset_special_auth(self, asset=None, username=None):
|
||||
if not asset:
|
||||
return self
|
||||
|
||||
instance = self.get_asset_user(asset, username=username)
|
||||
if instance:
|
||||
self._merge_auth(instance)
|
||||
|
||||
def _merge_auth(self, other):
|
||||
if other.password:
|
||||
self.password = other.password
|
||||
if other.public_key:
|
||||
self.public_key = other.public_key
|
||||
if other.private_key:
|
||||
if other.public_key or other.private_key:
|
||||
self.private_key = other.private_key
|
||||
self.public_key = other.public_key
|
||||
|
||||
def clear_auth(self):
|
||||
self.password = ''
|
||||
@@ -216,19 +212,57 @@ class AssetUser(OrgModelMixin):
|
||||
)
|
||||
return private_key, public_key
|
||||
|
||||
def auto_gen_auth(self):
|
||||
password = str(uuid.uuid4())
|
||||
private_key, public_key = ssh_key_gen(
|
||||
username=self.username
|
||||
)
|
||||
def auto_gen_auth(self, password=True, key=True):
|
||||
_password = None
|
||||
_private_key = None
|
||||
_public_key = None
|
||||
|
||||
if password:
|
||||
_password = self.gen_password()
|
||||
if key:
|
||||
_private_key, _public_key = self.gen_key(self.username)
|
||||
self.set_auth(
|
||||
password=password, private_key=private_key,
|
||||
public_key=public_key
|
||||
password=_password, private_key=_private_key,
|
||||
public_key=_public_key
|
||||
)
|
||||
|
||||
def auto_gen_auth_password(self):
|
||||
password = str(uuid.uuid4())
|
||||
self.set_auth(password=password)
|
||||
|
||||
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)
|
||||
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'))
|
||||
comment = models.TextField(blank=True, verbose_name=_('Comment'))
|
||||
date_created = models.DateTimeField(auto_now_add=True, verbose_name=_("Date created"))
|
||||
date_updated = models.DateTimeField(auto_now=True, verbose_name=_("Date updated"))
|
||||
created_by = models.CharField(max_length=128, null=True, verbose_name=_('Created by'))
|
||||
|
||||
ASSETS_AMOUNT_CACHE_KEY = "ASSET_USER_{}_ASSETS_AMOUNT"
|
||||
ASSET_USER_CACHE_TIME = 600
|
||||
|
||||
_prefer = "system_user"
|
||||
|
||||
def get_related_assets(self):
|
||||
assets = self.assets.filter(org_id=self.org_id)
|
||||
return assets
|
||||
|
||||
def get_username(self):
|
||||
return self.username
|
||||
|
||||
@lazyproperty
|
||||
def assets_amount(self):
|
||||
cache_key = self.ASSETS_AMOUNT_CACHE_KEY.format(self.id)
|
||||
cached = cache.get(cache_key)
|
||||
if not cached:
|
||||
cached = self.get_related_assets().count()
|
||||
cache.set(cache_key, cached, self.ASSET_USER_CACHE_TIME)
|
||||
return cached
|
||||
|
||||
def expire_assets_amount(self):
|
||||
cache_key = self.ASSETS_AMOUNT_CACHE_KEY.format(self.id)
|
||||
cache.delete(cache_key)
|
||||
|
||||
def _to_secret_json(self):
|
||||
"""Push system user use it"""
|
||||
@@ -240,26 +274,6 @@ class AssetUser(OrgModelMixin):
|
||||
'private_key': self.private_key_file,
|
||||
}
|
||||
|
||||
def generate_id_with_asset(self, asset):
|
||||
user_id = [self.part_id]
|
||||
asset_id = str(asset.id).split('-')[3:]
|
||||
ids = user_id + asset_id
|
||||
return '-'.join(ids)
|
||||
|
||||
def construct_to_authbook(self, asset):
|
||||
from . import AuthBook
|
||||
fields = [
|
||||
'name', 'username', 'comment', 'org_id',
|
||||
'password', 'private_key', 'public_key',
|
||||
'date_created', 'date_updated', 'created_by'
|
||||
]
|
||||
i = self.generate_id_with_asset(asset)
|
||||
obj = AuthBook(id=i, asset=asset, version=0, is_latest=True)
|
||||
for field in fields:
|
||||
value = getattr(self, field)
|
||||
setattr(obj, field, value)
|
||||
return obj
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
|
||||
@@ -10,20 +10,21 @@ from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orgs.mixins.models import OrgModelMixin
|
||||
from .base import AssetUser
|
||||
from .base import BaseUser
|
||||
|
||||
__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
|
||||
@@ -39,7 +40,7 @@ class Domain(OrgModelMixin):
|
||||
return random.choice(self.gateways)
|
||||
|
||||
|
||||
class Gateway(AssetUser):
|
||||
class Gateway(BaseUser):
|
||||
PROTOCOL_SSH = 'ssh'
|
||||
PROTOCOL_RDP = 'rdp'
|
||||
PROTOCOL_CHOICES = (
|
||||
|
||||
@@ -11,9 +11,9 @@ from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import ugettext
|
||||
from django.core.cache import cache
|
||||
|
||||
from common.utils import get_logger, timeit, lazyproperty
|
||||
from common.utils import get_logger, lazyproperty
|
||||
from orgs.mixins.models import OrgModelMixin, OrgManager
|
||||
from orgs.utils import set_current_org, get_current_org, tmp_to_org
|
||||
from orgs.utils import get_current_org, tmp_to_org, current_org
|
||||
from orgs.models import Organization
|
||||
|
||||
|
||||
@@ -26,63 +26,108 @@ class NodeQuerySet(models.QuerySet):
|
||||
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:
|
||||
tree_created_time = None
|
||||
tree_updated_time_cache_key = 'NODE_TREE_UPDATED_AT'
|
||||
tree_cache_time = 3600
|
||||
tree_assets_cache_key = 'NODE_TREE_ASSETS_UPDATED_AT'
|
||||
tree_assets_created_time = None
|
||||
_tree_service = None
|
||||
_org_tree_map = {}
|
||||
|
||||
@classmethod
|
||||
def tree(cls):
|
||||
from ..utils import TreeService
|
||||
tree_updated_time = cache.get(cls.tree_updated_time_cache_key, 0)
|
||||
now = time.time()
|
||||
# 什么时候重新初始化 _tree_service
|
||||
if not cls.tree_created_time or \
|
||||
tree_updated_time > cls.tree_created_time:
|
||||
logger.debug("Create node tree")
|
||||
tree = TreeService.new()
|
||||
cls.tree_created_time = now
|
||||
cls.tree_assets_created_time = now
|
||||
cls._tree_service = tree
|
||||
return tree
|
||||
# 是否要重新初始化节点资产
|
||||
node_assets_updated_time = cache.get(cls.tree_assets_cache_key, 0)
|
||||
if not cls.tree_assets_created_time or \
|
||||
node_assets_updated_time > cls.tree_assets_created_time:
|
||||
cls._tree_service.init_assets()
|
||||
cls.tree_assets_created_time = now
|
||||
logger.debug("Refresh node tree assets")
|
||||
return cls._tree_service
|
||||
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):
|
||||
logger.debug("Refresh node tree")
|
||||
key = cls.tree_updated_time_cache_key
|
||||
ttl = cls.tree_cache_time
|
||||
if not t:
|
||||
t = time.time()
|
||||
cache.set(key, t, ttl)
|
||||
TreeCache.set_changed(tp="tree", t=t, org_id=current_org.id)
|
||||
|
||||
@classmethod
|
||||
def refresh_node_assets(cls, t=None):
|
||||
logger.debug("Refresh node assets")
|
||||
key = cls.tree_assets_cache_key
|
||||
ttl = cls.tree_cache_time
|
||||
if not t:
|
||||
t = time.time()
|
||||
cache.set(key, t, ttl)
|
||||
|
||||
@staticmethod
|
||||
def refresh_user_tree_cache():
|
||||
"""
|
||||
当节点-节点关系,节点-资产关系发生变化时,应该刷新用户授权树缓存
|
||||
:return:
|
||||
"""
|
||||
from perms.utils.asset_permission import AssetPermissionUtilV2
|
||||
AssetPermissionUtilV2.expire_all_user_tree_cache()
|
||||
TreeCache.set_changed(tp="assets", t=t, org_id=current_org.id)
|
||||
|
||||
|
||||
class FamilyMixin:
|
||||
@@ -154,6 +199,20 @@ class FamilyMixin:
|
||||
)
|
||||
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
|
||||
@@ -376,15 +435,6 @@ class SomeNodesMixin:
|
||||
)
|
||||
return obj
|
||||
|
||||
@classmethod
|
||||
def empty_node(cls):
|
||||
with tmp_to_org(Organization.system()):
|
||||
defaults = {'value': cls.empty_value}
|
||||
obj, created = cls.objects.get_or_create(
|
||||
defaults=defaults, key=cls.empty_key
|
||||
)
|
||||
return obj
|
||||
|
||||
@classmethod
|
||||
def default_node(cls):
|
||||
with tmp_to_org(Organization.default()):
|
||||
@@ -413,7 +463,6 @@ class SomeNodesMixin:
|
||||
@classmethod
|
||||
def initial_some_nodes(cls):
|
||||
cls.default_node()
|
||||
cls.empty_node()
|
||||
cls.ungrouped_node()
|
||||
cls.favorite_node()
|
||||
|
||||
@@ -523,13 +572,13 @@ class Node(OrgModelMixin, SomeNodesMixin, TreeMixin, FamilyMixin, FullValueMixin
|
||||
tree_node = TreeNode(**data)
|
||||
return tree_node
|
||||
|
||||
def has_children_or_contains_assets(self):
|
||||
if self.children or self.get_assets():
|
||||
def has_children_or_has_assets(self):
|
||||
if self.children or self.get_assets().exists():
|
||||
return True
|
||||
return False
|
||||
|
||||
def delete(self, using=None, keep_parents=False):
|
||||
if self.has_children_or_contains_assets():
|
||||
if self.has_children_or_has_assets():
|
||||
return
|
||||
return super().delete(using=using, keep_parents=keep_parents)
|
||||
|
||||
@@ -539,14 +588,13 @@ class Node(OrgModelMixin, SomeNodesMixin, TreeMixin, FamilyMixin, FullValueMixin
|
||||
org = get_current_org()
|
||||
if not org or not org.is_real():
|
||||
Organization.default().change_to()
|
||||
i = 0
|
||||
while i < count:
|
||||
nodes = list(cls.objects.all())
|
||||
if count > 100:
|
||||
length = 100
|
||||
else:
|
||||
length = count
|
||||
nodes = list(cls.objects.all())
|
||||
if count > 100:
|
||||
length = 100
|
||||
else:
|
||||
length = count
|
||||
|
||||
for i in range(length):
|
||||
node = random.choice(nodes)
|
||||
node.create_child('Node {}'.format(i))
|
||||
for i in range(length):
|
||||
node = random.choice(nodes)
|
||||
child = node.create_child('Node {}'.format(i))
|
||||
print("{}. {}".format(i, child))
|
||||
|
||||
@@ -4,14 +4,12 @@
|
||||
|
||||
import logging
|
||||
|
||||
from functools import reduce
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
|
||||
from common.utils import signer
|
||||
from .base import AssetUser
|
||||
from .base import BaseUser
|
||||
from .asset import Asset
|
||||
|
||||
|
||||
@@ -19,7 +17,7 @@ __all__ = ['AdminUser', 'SystemUser']
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AdminUser(AssetUser):
|
||||
class AdminUser(BaseUser):
|
||||
"""
|
||||
A privileged user that ansible can use it to push system user and so on
|
||||
"""
|
||||
@@ -87,7 +85,7 @@ class AdminUser(AssetUser):
|
||||
continue
|
||||
|
||||
|
||||
class SystemUser(AssetUser):
|
||||
class SystemUser(BaseUser):
|
||||
PROTOCOL_SSH = 'ssh'
|
||||
PROTOCOL_RDP = 'rdp'
|
||||
PROTOCOL_TELNET = 'telnet'
|
||||
@@ -107,9 +105,11 @@ class SystemUser(AssetUser):
|
||||
(LOGIN_AUTO, _('Automatic login')),
|
||||
(LOGIN_MANUAL, _('Manually login'))
|
||||
)
|
||||
|
||||
username_same_with_user = models.BooleanField(default=False, verbose_name=_("Username same with user"))
|
||||
nodes = models.ManyToManyField('assets.Node', blank=True, verbose_name=_("Nodes"))
|
||||
assets = models.ManyToManyField('assets.Asset', blank=True, verbose_name=_("Assets"))
|
||||
users = models.ManyToManyField('users.User', blank=True, verbose_name=_("Users"))
|
||||
groups = models.ManyToManyField('users.UserGroup', blank=True, verbose_name=_("User groups"))
|
||||
priority = models.IntegerField(default=20, verbose_name=_("Priority"), validators=[MinValueValidator(1), MaxValueValidator(100)])
|
||||
protocol = models.CharField(max_length=16, choices=PROTOCOL_CHOICES, default='ssh', verbose_name=_('Protocol'))
|
||||
auto_push = models.BooleanField(default=True, verbose_name=_('Auto push'))
|
||||
@@ -117,9 +117,20 @@ class SystemUser(AssetUser):
|
||||
shell = models.CharField(max_length=64, default='/bin/bash', verbose_name=_('Shell'))
|
||||
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"))
|
||||
_prefer = 'system_user'
|
||||
|
||||
def __str__(self):
|
||||
return '{0.name}({0.username})'.format(self)
|
||||
username = self.username
|
||||
if self.username_same_with_user:
|
||||
username = 'dynamic'
|
||||
return '{0.name}({1})'.format(self, username)
|
||||
|
||||
def get_username(self):
|
||||
if self.username_same_with_user:
|
||||
return list(self.users.values_list('username', flat=True))
|
||||
else:
|
||||
return self.username
|
||||
|
||||
@property
|
||||
def nodes_amount(self):
|
||||
@@ -147,6 +158,11 @@ class SystemUser(AssetUser):
|
||||
def can_perm_to_asset(self):
|
||||
return self.protocol not in [self.PROTOCOL_MYSQL]
|
||||
|
||||
def _merge_auth(self, other):
|
||||
super()._merge_auth(other)
|
||||
if self.username_same_with_user:
|
||||
self.username = other.username
|
||||
|
||||
@property
|
||||
def cmd_filter_rules(self):
|
||||
from .cmd_filter import CommandFilterRule
|
||||
|
||||
@@ -77,7 +77,7 @@ class Connectivity:
|
||||
return cls(cls.UNKNOWN, timezone.now())
|
||||
|
||||
@classmethod
|
||||
def set(cls, key, value, ttl=0):
|
||||
def set(cls, key, value, ttl=None):
|
||||
cache.set(key, value, ttl)
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -55,3 +55,11 @@ class ReplaceNodeAdminUserSerializer(serializers.ModelSerializer):
|
||||
|
||||
class TaskIDSerializer(serializers.Serializer):
|
||||
task = serializers.CharField(read_only=True)
|
||||
|
||||
|
||||
class AssetUserTaskSerializer(serializers.Serializer):
|
||||
ACTION_CHOICES = (
|
||||
('test', 'test'),
|
||||
)
|
||||
action = serializers.ChoiceField(choices=ACTION_CHOICES, write_only=True)
|
||||
task = serializers.CharField(read_only=True)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import re
|
||||
from rest_framework import serializers
|
||||
from django.db.models import Prefetch
|
||||
from django.db.models import Prefetch, F, Count
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
@@ -12,8 +12,9 @@ from .base import ConnectivitySerializer
|
||||
|
||||
__all__ = [
|
||||
'AssetSerializer', 'AssetSimpleSerializer',
|
||||
'AssetDisplaySerializer',
|
||||
'ProtocolsField', 'PlatformSerializer',
|
||||
'AssetDetailSerializer',
|
||||
'AssetDetailSerializer', 'AssetTaskSerializer',
|
||||
]
|
||||
|
||||
|
||||
@@ -66,7 +67,8 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
|
||||
slug_field='name', queryset=Platform.objects.all(), label=_("Platform")
|
||||
)
|
||||
protocols = ProtocolsField(label=_('Protocols'), required=False)
|
||||
connectivity = ConnectivitySerializer(read_only=True, label=_("Connectivity"))
|
||||
domain_display = serializers.ReadOnlyField(source='domain.name')
|
||||
admin_user_display = serializers.ReadOnlyField(source='admin_user.name')
|
||||
|
||||
"""
|
||||
资产的数据结构
|
||||
@@ -74,21 +76,35 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
|
||||
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', 'connectivity',
|
||||
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},
|
||||
@@ -99,10 +115,7 @@ 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')
|
||||
queryset = queryset.select_related('admin_user', 'domain', 'platform')
|
||||
return queryset
|
||||
|
||||
def compatible_with_old_protocol(self, validated_data):
|
||||
@@ -130,6 +143,22 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
|
||||
class AssetDisplaySerializer(AssetSerializer):
|
||||
connectivity = ConnectivitySerializer(read_only=True, label=_("Connectivity"))
|
||||
|
||||
class Meta(AssetSerializer.Meta):
|
||||
fields = AssetSerializer.Meta.fields + [
|
||||
'connectivity',
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def setup_eager_loading(cls, queryset):
|
||||
""" Perform necessary eager loading of data. """
|
||||
queryset = queryset\
|
||||
.annotate(admin_user_username=F('admin_user__username'))
|
||||
return queryset
|
||||
|
||||
|
||||
class PlatformSerializer(serializers.ModelSerializer):
|
||||
meta = serializers.DictField(required=False, allow_null=True)
|
||||
|
||||
@@ -151,3 +180,12 @@ class AssetSimpleSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Asset
|
||||
fields = ['id', 'hostname', 'ip', 'connectivity', 'port']
|
||||
|
||||
|
||||
class AssetTaskSerializer(serializers.Serializer):
|
||||
ACTION_CHOICES = (
|
||||
('refresh', 'refresh'),
|
||||
('test', 'test'),
|
||||
)
|
||||
task = serializers.CharField(read_only=True)
|
||||
action = serializers.ChoiceField(choices=ACTION_CHOICES, write_only=True)
|
||||
|
||||
@@ -8,39 +8,23 @@ from common.serializers import AdaptedBulkListSerializer
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from ..models import AuthBook, Asset
|
||||
from ..backends import AssetUserManager
|
||||
|
||||
from .base import ConnectivitySerializer, AuthSerializerMixin
|
||||
|
||||
|
||||
__all__ = [
|
||||
'AssetUserSerializer', 'AssetUserAuthInfoSerializer',
|
||||
'AssetUserExportSerializer', 'AssetUserPushSerializer',
|
||||
'AssetUserWriteSerializer', 'AssetUserReadSerializer',
|
||||
'AssetUserAuthInfoSerializer', 'AssetUserPushSerializer',
|
||||
]
|
||||
|
||||
|
||||
class BasicAssetSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Asset
|
||||
fields = ['hostname', 'ip']
|
||||
|
||||
|
||||
class AssetUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
hostname = serializers.CharField(read_only=True, label=_("Hostname"))
|
||||
ip = serializers.CharField(read_only=True, label=_("IP"))
|
||||
connectivity = ConnectivitySerializer(read_only=True, label=_("Connectivity"))
|
||||
|
||||
backend = serializers.CharField(read_only=True, label=_("Backend"))
|
||||
|
||||
class AssetUserWriteSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
class Meta:
|
||||
model = AuthBook
|
||||
list_serializer_class = AdaptedBulkListSerializer
|
||||
read_only_fields = (
|
||||
'date_created', 'date_updated', 'created_by',
|
||||
'is_latest', 'version', 'connectivity',
|
||||
)
|
||||
fields = [
|
||||
"id", "hostname", "ip", "username", "password", "asset", "version",
|
||||
"is_latest", "connectivity", "backend",
|
||||
"date_created", "date_updated", "private_key", "public_key",
|
||||
'id', 'username', 'password', 'private_key', "public_key",
|
||||
'asset', 'comment',
|
||||
]
|
||||
extra_kwargs = {
|
||||
'username': {'required': True},
|
||||
@@ -53,11 +37,35 @@ class AssetUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
if not validated_data.get("name") and validated_data.get("username"):
|
||||
validated_data["name"] = validated_data["username"]
|
||||
instance = AssetUserManager.create(**validated_data)
|
||||
instance.set_to_latest()
|
||||
return instance
|
||||
|
||||
|
||||
class AssetUserExportSerializer(AssetUserSerializer):
|
||||
class AssetUserReadSerializer(AssetUserWriteSerializer):
|
||||
id = serializers.CharField(read_only=True, source='union_id', label=_("ID"))
|
||||
hostname = serializers.CharField(read_only=True, label=_("Hostname"))
|
||||
ip = serializers.CharField(read_only=True, label=_("IP"))
|
||||
asset = serializers.CharField(source='asset_id', label=_('Asset'))
|
||||
backend = serializers.CharField(read_only=True, label=_("Backend"))
|
||||
|
||||
class Meta(AssetUserWriteSerializer.Meta):
|
||||
read_only_fields = (
|
||||
'date_created', 'date_updated',
|
||||
'created_by', 'version',
|
||||
)
|
||||
fields = [
|
||||
'id', 'username', 'password', 'private_key', "public_key",
|
||||
'asset', 'hostname', 'ip', 'backend', 'version',
|
||||
'date_created', "date_updated", 'comment',
|
||||
]
|
||||
extra_kwargs = {
|
||||
'username': {'required': True},
|
||||
'password': {'write_only': True},
|
||||
'private_key': {'write_only': True},
|
||||
'public_key': {'write_only': True},
|
||||
}
|
||||
|
||||
|
||||
class AssetUserAuthInfoSerializer(AssetUserReadSerializer):
|
||||
password = serializers.CharField(
|
||||
max_length=256, allow_blank=True, allow_null=True,
|
||||
required=False, label=_('Password')
|
||||
@@ -72,12 +80,6 @@ class AssetUserExportSerializer(AssetUserSerializer):
|
||||
)
|
||||
|
||||
|
||||
class AssetUserAuthInfoSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = AuthBook
|
||||
fields = ['password', 'private_key', 'public_key']
|
||||
|
||||
|
||||
class AssetUserPushSerializer(serializers.Serializer):
|
||||
asset = serializers.PrimaryKeyRelatedField(queryset=Asset.objects, label=_("Asset"))
|
||||
username = serializers.CharField(max_length=1024)
|
||||
|
||||
@@ -5,6 +5,7 @@ from django.utils.translation import ugettext as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.utils import ssh_pubkey_gen, validate_ssh_private_key
|
||||
from ..models import AssetUser
|
||||
|
||||
|
||||
class AuthSerializer(serializers.ModelSerializer):
|
||||
@@ -60,9 +61,6 @@ class AuthSerializerMixin:
|
||||
if not value:
|
||||
validated_data.pop(field, None)
|
||||
|
||||
# print(validated_data)
|
||||
# raise serializers.ValidationError(">>>>>>")
|
||||
|
||||
def create(self, validated_data):
|
||||
self.clean_auth_fields(validated_data)
|
||||
return super().create(validated_data)
|
||||
@@ -70,3 +68,15 @@ class AuthSerializerMixin:
|
||||
def update(self, instance, validated_data):
|
||||
self.clean_auth_fields(validated_data)
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
|
||||
class AuthInfoSerializer(serializers.ModelSerializer):
|
||||
private_key = serializers.ReadOnlyField(source='get_private_key')
|
||||
|
||||
class Meta:
|
||||
model = AssetUser
|
||||
fields = [
|
||||
'username', 'password',
|
||||
'private_key', 'public_key',
|
||||
'date_updated',
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -8,7 +8,7 @@ from ..models import Asset, Node
|
||||
|
||||
__all__ = [
|
||||
'NodeSerializer', "NodeAddChildrenSerializer",
|
||||
"NodeAssetsSerializer",
|
||||
"NodeAssetsSerializer", "NodeTaskSerializer",
|
||||
]
|
||||
|
||||
|
||||
@@ -51,3 +51,12 @@ class NodeAssetsSerializer(BulkOrgResourceModelSerializer):
|
||||
class NodeAddChildrenSerializer(serializers.Serializer):
|
||||
nodes = serializers.ListField()
|
||||
|
||||
|
||||
class NodeTaskSerializer(serializers.Serializer):
|
||||
ACTION_CHOICES = (
|
||||
('refresh', 'refresh'),
|
||||
('test', 'test'),
|
||||
('refresh_cache', 'refresh_cache'),
|
||||
)
|
||||
task = serializers.CharField(read_only=True)
|
||||
action = serializers.ChoiceField(choices=ACTION_CHOICES, write_only=True)
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import re
|
||||
from rest_framework import serializers
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.db.models import Count
|
||||
|
||||
from common.serializers import AdaptedBulkListSerializer
|
||||
from common.mixins.serializers import BulkSerializerMixin
|
||||
from common.utils import ssh_pubkey_gen
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from assets.models import Node
|
||||
from ..models import SystemUser
|
||||
from .base import AuthSerializer, AuthSerializerMixin
|
||||
from ..models import SystemUser, Asset
|
||||
from .base import AuthSerializerMixin
|
||||
|
||||
__all__ = [
|
||||
'SystemUserSerializer', 'SystemUserAuthSerializer',
|
||||
'SystemUserSerializer', 'SystemUserListSerializer',
|
||||
'SystemUserSimpleSerializer', 'SystemUserAssetRelationSerializer',
|
||||
'SystemUserNodeRelationSerializer',
|
||||
'SystemUserNodeRelationSerializer', 'SystemUserTaskSerializer',
|
||||
'SystemUserUserRelationSerializer', 'SystemUserWithAuthInfoSerializer',
|
||||
]
|
||||
|
||||
|
||||
@@ -28,10 +28,13 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
model = SystemUser
|
||||
list_serializer_class = AdaptedBulkListSerializer
|
||||
fields = [
|
||||
'id', 'name', 'username', 'password', 'public_key', 'private_key',
|
||||
'login_mode', 'login_mode_display', 'priority', 'protocol',
|
||||
'id', 'name', 'username', 'protocol',
|
||||
'password', 'public_key', 'private_key',
|
||||
'login_mode', 'login_mode_display',
|
||||
'priority', 'username_same_with_user',
|
||||
'auto_push', 'cmd_filters', 'sudo', 'shell', 'comment',
|
||||
'assets_amount', 'nodes_amount', 'auto_generate_key'
|
||||
'auto_generate_key', 'sftp_root',
|
||||
'assets_amount', 'date_created', 'created_by'
|
||||
]
|
||||
extra_kwargs = {
|
||||
'password': {"write_only": True},
|
||||
@@ -67,17 +70,43 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
value = False
|
||||
return value
|
||||
|
||||
def validate_username_same_with_user(self, username_same_with_user):
|
||||
if not username_same_with_user:
|
||||
return username_same_with_user
|
||||
protocol = self.initial_data.get("protocol", "ssh")
|
||||
queryset = SystemUser.objects.filter(
|
||||
protocol=protocol, username_same_with_user=True
|
||||
)
|
||||
if self.instance:
|
||||
queryset = queryset.exclude(id=self.instance.id)
|
||||
exists = queryset.exists()
|
||||
if not exists:
|
||||
return username_same_with_user
|
||||
error = _("Username same with user with protocol {} only allow 1").format(protocol)
|
||||
raise serializers.ValidationError(error)
|
||||
|
||||
def validate_username(self, username):
|
||||
if username:
|
||||
return username
|
||||
login_mode = self.initial_data.get("login_mode")
|
||||
protocol = self.initial_data.get("protocol")
|
||||
username_same_with_user = self.initial_data.get("username_same_with_user")
|
||||
if username_same_with_user:
|
||||
return ''
|
||||
if login_mode == SystemUser.LOGIN_AUTO and \
|
||||
protocol != SystemUser.PROTOCOL_VNC:
|
||||
msg = _('* Automatic login mode must fill in the username.')
|
||||
raise serializers.ValidationError(msg)
|
||||
return username
|
||||
|
||||
def validate_sftp_root(self, value):
|
||||
if value in ['home', 'tmp']:
|
||||
return value
|
||||
if not value.startswith('/'):
|
||||
error = _("Path should starts with /")
|
||||
raise serializers.ValidationError(error)
|
||||
return value
|
||||
|
||||
def validate_password(self, password):
|
||||
super().validate_password(password)
|
||||
auto_gen_key = self.initial_data.get("auto_generate_key", False)
|
||||
@@ -112,29 +141,42 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
attrs["public_key"] = public_key
|
||||
return attrs
|
||||
|
||||
|
||||
class SystemUserListSerializer(SystemUserSerializer):
|
||||
class Meta(SystemUserSerializer.Meta):
|
||||
fields = [
|
||||
'id', 'name', 'username', 'protocol',
|
||||
'login_mode', 'login_mode_display',
|
||||
'priority', "username_same_with_user",
|
||||
'auto_push', 'sudo', 'shell', 'comment',
|
||||
"assets_amount",
|
||||
'auto_generate_key',
|
||||
'sftp_root',
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def setup_eager_loading(cls, queryset):
|
||||
""" Perform necessary eager loading of data. """
|
||||
queryset = queryset.prefetch_related('cmd_filters', 'nodes')
|
||||
queryset = queryset.annotate(assets_amount=Count("assets"))
|
||||
return queryset
|
||||
|
||||
|
||||
class SystemUserAuthSerializer(AuthSerializer):
|
||||
"""
|
||||
系统用户认证信息
|
||||
"""
|
||||
private_key = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = SystemUser
|
||||
class SystemUserWithAuthInfoSerializer(SystemUserSerializer):
|
||||
class Meta(SystemUserSerializer.Meta):
|
||||
fields = [
|
||||
"id", "name", "username", "protocol",
|
||||
"login_mode", "password", "private_key",
|
||||
'id', 'name', 'username', 'protocol',
|
||||
'password', 'public_key', 'private_key',
|
||||
'login_mode', 'login_mode_display',
|
||||
'priority', 'username_same_with_user',
|
||||
'auto_push', 'sudo', 'shell', 'comment',
|
||||
'auto_generate_key', 'sftp_root',
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def get_private_key(obj):
|
||||
return obj.get_private_key()
|
||||
extra_kwargs = {
|
||||
'nodes_amount': {'label': _('Node')},
|
||||
'assets_amount': {'label': _('Asset')},
|
||||
'login_mode_display': {'label': _('Login mode display')},
|
||||
'created_by': {'read_only': True},
|
||||
}
|
||||
|
||||
|
||||
class SystemUserSimpleSerializer(serializers.ModelSerializer):
|
||||
@@ -186,3 +228,25 @@ class SystemUserNodeRelationSerializer(RelationMixin, serializers.ModelSerialize
|
||||
return self.tree.get_node_full_tag(obj.node_key)
|
||||
else:
|
||||
return obj.node.full_value
|
||||
|
||||
|
||||
class SystemUserUserRelationSerializer(RelationMixin, serializers.ModelSerializer):
|
||||
user_display = serializers.ReadOnlyField()
|
||||
|
||||
class Meta(RelationMixin.Meta):
|
||||
model = SystemUser.users.through
|
||||
fields = [
|
||||
'id', "user", "user_display",
|
||||
]
|
||||
|
||||
|
||||
class SystemUserTaskSerializer(serializers.Serializer):
|
||||
ACTION_CHOICES = (
|
||||
("test", "test"),
|
||||
("push", "push"),
|
||||
)
|
||||
action = serializers.ChoiceField(choices=ACTION_CHOICES, write_only=True)
|
||||
asset = serializers.PrimaryKeyRelatedField(
|
||||
queryset=Asset.objects, allow_null=True, required=False, write_only=True
|
||||
)
|
||||
task = serializers.CharField(read_only=True)
|
||||
|
||||
@@ -7,13 +7,15 @@ from django.db.models.signals import (
|
||||
from django.db.models.aggregates import Count
|
||||
from django.dispatch import receiver
|
||||
|
||||
from common.utils import get_logger, timeit
|
||||
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 .tasks import (
|
||||
update_assets_hardware_info_util,
|
||||
test_asset_connectivity_util,
|
||||
push_system_user_to_assets_manual,
|
||||
push_system_user_to_assets,
|
||||
add_nodes_assets_to_system_users
|
||||
)
|
||||
@@ -94,6 +96,25 @@ def on_system_user_assets_change(sender, instance=None, action='', model=None, p
|
||||
push_system_user_to_assets.delay(system_user, assets)
|
||||
|
||||
|
||||
@receiver(m2m_changed, sender=SystemUser.users.through)
|
||||
def on_system_user_users_change(sender, instance=None, action='', model=None, pk_set=None, **kwargs):
|
||||
"""
|
||||
当系统用户和用户关系发生变化时,应该重新推送系统用户资产中
|
||||
"""
|
||||
if action != "post_add":
|
||||
return
|
||||
if not instance.username_same_with_user:
|
||||
return
|
||||
logger.debug("System user users change signal recv: {}".format(instance))
|
||||
queryset = model.objects.filter(pk__in=pk_set)
|
||||
if model == SystemUser:
|
||||
system_users = queryset
|
||||
else:
|
||||
system_users = [instance]
|
||||
for s in system_users:
|
||||
push_system_user_to_assets_manual.delay(s)
|
||||
|
||||
|
||||
@receiver(m2m_changed, sender=SystemUser.nodes.through)
|
||||
def on_system_user_nodes_change(sender, instance=None, action=None, model=None, pk_set=None, **kwargs):
|
||||
"""
|
||||
@@ -113,6 +134,20 @@ def on_system_user_nodes_change(sender, instance=None, action=None, model=None,
|
||||
add_nodes_assets_to_system_users.delay(nodes_keys, system_users)
|
||||
|
||||
|
||||
@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):
|
||||
"""
|
||||
当系统用户和用户组关系发生变化时,应该将组下用户关联到新的系统用户上
|
||||
"""
|
||||
if action != "post_add" or reverse:
|
||||
return
|
||||
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))
|
||||
|
||||
|
||||
@receiver(m2m_changed, sender=Asset.nodes.through)
|
||||
def on_asset_nodes_change(sender, instance=None, action='', **kwargs):
|
||||
"""
|
||||
@@ -121,6 +156,8 @@ def on_asset_nodes_change(sender, instance=None, action='', **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()
|
||||
|
||||
|
||||
@receiver(m2m_changed, sender=Asset.nodes.through)
|
||||
@@ -148,7 +185,9 @@ def on_asset_nodes_add(sender, instance=None, action='', model=None, pk_set=None
|
||||
|
||||
system_users_assets = defaultdict(set)
|
||||
for system_user in system_users:
|
||||
system_users_assets[system_user].update(set(assets))
|
||||
assets_has_set = system_user.assets.all().filter(id__in=assets).values_list('id', flat=True)
|
||||
assets_remain = set(assets) - set(assets_has_set)
|
||||
system_users_assets[system_user].update(assets_remain)
|
||||
for system_user, _assets in system_users_assets.items():
|
||||
system_user.assets.add(*tuple(_assets))
|
||||
|
||||
@@ -195,9 +234,5 @@ def on_asset_nodes_remove(sender, instance=None, action='', model=None,
|
||||
def on_node_update_or_created(sender, **kwargs):
|
||||
# 刷新节点
|
||||
Node.refresh_nodes()
|
||||
|
||||
|
||||
@receiver(post_save, sender=AuthBook)
|
||||
def on_authbook_created(sender, instance=None, created=True, **kwargs):
|
||||
if created and instance:
|
||||
instance.set_version()
|
||||
with tmp_to_root_org():
|
||||
Node.refresh_nodes()
|
||||
|
||||
@@ -4,11 +4,12 @@ from celery import shared_task
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.core.cache import cache
|
||||
|
||||
from orgs.utils import tmp_to_root_org, org_aware_func
|
||||
from common.utils import get_logger
|
||||
from ops.celery.decorator import register_as_period_task
|
||||
|
||||
from ..models import AdminUser
|
||||
from .utils import clean_hosts
|
||||
from .utils import clean_ansible_task_hosts
|
||||
from .asset_connectivity import test_asset_connectivity_util
|
||||
from . import const
|
||||
|
||||
@@ -20,7 +21,7 @@ __all__ = [
|
||||
]
|
||||
|
||||
|
||||
@shared_task(queue="ansible")
|
||||
@org_aware_func("admin_user")
|
||||
def test_admin_user_connectivity_util(admin_user, task_name):
|
||||
"""
|
||||
Test asset admin user can connect or not. Using ansible api do that
|
||||
@@ -29,7 +30,7 @@ def test_admin_user_connectivity_util(admin_user, task_name):
|
||||
:return:
|
||||
"""
|
||||
assets = admin_user.get_related_assets()
|
||||
hosts = clean_hosts(assets)
|
||||
hosts = clean_ansible_task_hosts(assets)
|
||||
if not hosts:
|
||||
return {}
|
||||
summary = test_asset_connectivity_util(hosts, task_name)
|
||||
@@ -51,10 +52,13 @@ def test_admin_user_connectivity_period():
|
||||
logger.debug("Test admin user connectivity, less than 40 minutes, skip")
|
||||
return
|
||||
cache.set(key, 1, 60*40)
|
||||
admin_users = AdminUser.objects.all()
|
||||
for admin_user in admin_users:
|
||||
task_name = _("Test admin user connectivity period: {}").format(admin_user.name)
|
||||
test_admin_user_connectivity_util(admin_user, task_name)
|
||||
with tmp_to_root_org():
|
||||
admin_users = AdminUser.objects.all()
|
||||
for admin_user in admin_users:
|
||||
task_name = _("Test admin user connectivity period: {}").format(
|
||||
admin_user.name
|
||||
)
|
||||
test_admin_user_connectivity_util(admin_user, task_name)
|
||||
cache.set(key, 1, 60*40)
|
||||
|
||||
|
||||
|
||||
@@ -1,55 +1,55 @@
|
||||
# ~*~ coding: utf-8 ~*~
|
||||
from itertools import groupby
|
||||
from collections import defaultdict
|
||||
from celery import shared_task
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from common.utils import get_logger
|
||||
from orgs.utils import org_aware_func
|
||||
from ..models.utils import Connectivity
|
||||
from . import const
|
||||
from .utils import clean_hosts
|
||||
from .utils import clean_ansible_task_hosts, group_asset_by_platform
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
__all__ = ['test_asset_connectivity_util', 'test_asset_connectivity_manual']
|
||||
__all__ = [
|
||||
'test_asset_connectivity_util', 'test_asset_connectivity_manual',
|
||||
'test_node_assets_connectivity_manual',
|
||||
]
|
||||
|
||||
|
||||
@shared_task(queue="ansible")
|
||||
@org_aware_func("assets")
|
||||
def test_asset_connectivity_util(assets, task_name=None):
|
||||
from ops.utils import update_or_create_ansible_task
|
||||
|
||||
if task_name is None:
|
||||
task_name = _("Test assets connectivity")
|
||||
|
||||
hosts = clean_hosts(assets)
|
||||
hosts = clean_ansible_task_hosts(assets)
|
||||
if not hosts:
|
||||
return {}
|
||||
platform_hosts_map = {}
|
||||
hosts_sorted = sorted(hosts, key=group_asset_by_platform)
|
||||
platform_hosts = groupby(hosts_sorted, key=group_asset_by_platform)
|
||||
for i in platform_hosts:
|
||||
platform_hosts_map[i[0]] = list(i[1])
|
||||
|
||||
hosts_category = {
|
||||
'linux': {
|
||||
'hosts': [],
|
||||
'tasks': const.TEST_ADMIN_USER_CONN_TASKS
|
||||
},
|
||||
'windows': {
|
||||
'hosts': [],
|
||||
'tasks': const.TEST_WINDOWS_ADMIN_USER_CONN_TASKS
|
||||
}
|
||||
platform_tasks_map = {
|
||||
"unixlike": const.PING_UNIXLIKE_TASKS,
|
||||
"windows": const.PING_WINDOWS_TASKS
|
||||
}
|
||||
for host in hosts:
|
||||
hosts_list = hosts_category['windows']['hosts'] if host.is_windows() \
|
||||
else hosts_category['linux']['hosts']
|
||||
hosts_list.append(host)
|
||||
|
||||
results_summary = dict(
|
||||
contacted=defaultdict(dict), dark=defaultdict(dict), success=True
|
||||
)
|
||||
created_by = assets[0].org_id
|
||||
for k, value in hosts_category.items():
|
||||
if not value['hosts']:
|
||||
for platform, _hosts in platform_hosts_map.items():
|
||||
if not _hosts:
|
||||
continue
|
||||
logger.debug("System user not has special auth")
|
||||
tasks = platform_tasks_map.get(platform)
|
||||
task, created = update_or_create_ansible_task(
|
||||
task_name=task_name, hosts=value['hosts'], tasks=value['tasks'],
|
||||
task_name=task_name, hosts=_hosts, tasks=tasks,
|
||||
pattern='all', options=const.TASK_OPTIONS, run_as_admin=True,
|
||||
created_by=created_by,
|
||||
)
|
||||
raw, summary = task.run()
|
||||
success = summary.get('success', False)
|
||||
@@ -59,6 +59,7 @@ def test_asset_connectivity_util(assets, task_name=None):
|
||||
results_summary['success'] &= success
|
||||
results_summary['contacted'].update(contacted)
|
||||
results_summary['dark'].update(dark)
|
||||
continue
|
||||
|
||||
for asset in assets:
|
||||
if asset.hostname in results_summary.get('dark', {}).keys():
|
||||
@@ -79,3 +80,12 @@ def test_asset_connectivity_manual(asset):
|
||||
return False, summary['dark']
|
||||
else:
|
||||
return True, ""
|
||||
|
||||
|
||||
@shared_task(queue="ansible")
|
||||
def test_node_assets_connectivity_manual(node):
|
||||
task_name = _("Test if the assets under the node are connectable: {}".format(node.name))
|
||||
assets = node.get_all_assets()
|
||||
result = test_asset_connectivity_util(assets, task_name=task_name)
|
||||
return result
|
||||
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
from celery import shared_task
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from common.utils import get_logger
|
||||
from common.utils import get_logger, get_object_or_none
|
||||
from orgs.utils import org_aware_func
|
||||
from ..models import Asset
|
||||
from . import const
|
||||
from .utils import check_asset_can_run_ansible
|
||||
|
||||
@@ -13,15 +15,16 @@ logger = get_logger(__file__)
|
||||
|
||||
__all__ = [
|
||||
'test_asset_user_connectivity_util', 'test_asset_users_connectivity_manual',
|
||||
'get_test_asset_user_connectivity_tasks',
|
||||
'get_test_asset_user_connectivity_tasks', 'test_user_connectivity',
|
||||
'run_adhoc',
|
||||
]
|
||||
|
||||
|
||||
def get_test_asset_user_connectivity_tasks(asset):
|
||||
if asset.is_unixlike():
|
||||
tasks = const.TEST_ASSET_USER_CONN_TASKS
|
||||
tasks = const.PING_UNIXLIKE_TASKS
|
||||
elif asset.is_windows():
|
||||
tasks = const.TEST_WINDOWS_ASSET_USER_CONN_TASKS
|
||||
tasks = const.PING_WINDOWS_TASKS
|
||||
else:
|
||||
msg = _(
|
||||
"The asset {} system platform {} does not "
|
||||
@@ -32,46 +35,98 @@ def get_test_asset_user_connectivity_tasks(asset):
|
||||
return tasks
|
||||
|
||||
|
||||
@shared_task(queue="ansible")
|
||||
def test_asset_user_connectivity_util(asset_user, task_name, run_as_admin=False):
|
||||
def run_adhoc(task_name, tasks, inventory):
|
||||
"""
|
||||
:param task_name
|
||||
:param tasks
|
||||
:param inventory
|
||||
"""
|
||||
from ops.ansible.runner import AdHocRunner
|
||||
runner = AdHocRunner(inventory, options=const.TASK_OPTIONS)
|
||||
result = runner.run(tasks, 'all', task_name)
|
||||
return result.results_raw, result.results_summary
|
||||
|
||||
|
||||
def test_user_connectivity(task_name, asset, username, password=None, private_key=None):
|
||||
"""
|
||||
:param task_name
|
||||
:param asset
|
||||
:param username
|
||||
:param password
|
||||
:param private_key
|
||||
"""
|
||||
from ops.inventory import JMSCustomInventory
|
||||
|
||||
tasks = get_test_asset_user_connectivity_tasks(asset)
|
||||
if not tasks:
|
||||
logger.debug("No tasks ")
|
||||
return {}, {}
|
||||
inventory = JMSCustomInventory(
|
||||
assets=[asset], username=username, password=password,
|
||||
private_key=private_key
|
||||
)
|
||||
raw, summary = run_adhoc(
|
||||
task_name=task_name, tasks=tasks, inventory=inventory
|
||||
)
|
||||
return raw, summary
|
||||
|
||||
|
||||
@org_aware_func("asset_user")
|
||||
def test_asset_user_connectivity_util(asset_user, task_name):
|
||||
"""
|
||||
:param asset_user: <AuthBook>对象
|
||||
:param task_name:
|
||||
:param run_as_admin:
|
||||
:return:
|
||||
"""
|
||||
from ops.utils import update_or_create_ansible_task
|
||||
|
||||
if not check_asset_can_run_ansible(asset_user.asset):
|
||||
return
|
||||
|
||||
tasks = get_test_asset_user_connectivity_tasks(asset_user.asset)
|
||||
if not tasks:
|
||||
logger.debug("No tasks ")
|
||||
try:
|
||||
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_file
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warn("Failed run adhoc {}, {}".format(task_name, e))
|
||||
return
|
||||
|
||||
args = (task_name,)
|
||||
kwargs = {
|
||||
'hosts': [asset_user.asset], 'tasks': tasks,
|
||||
'pattern': 'all', 'options': const.TASK_OPTIONS,
|
||||
'created_by': asset_user.org_id,
|
||||
}
|
||||
if run_as_admin:
|
||||
kwargs["run_as_admin"] = True
|
||||
else:
|
||||
kwargs["run_as"] = asset_user.username
|
||||
task, created = update_or_create_ansible_task(*args, **kwargs)
|
||||
raw, summary = task.run()
|
||||
asset_user.set_connectivity(summary)
|
||||
|
||||
|
||||
@shared_task(queue="ansible")
|
||||
def test_asset_users_connectivity_manual(asset_users, run_as_admin=False):
|
||||
def test_asset_users_connectivity_manual(asset_users):
|
||||
"""
|
||||
:param asset_users: <AuthBook>对象
|
||||
"""
|
||||
for asset_user in asset_users:
|
||||
task_name = _("Test asset user connectivity: {}").format(asset_user)
|
||||
test_asset_user_connectivity_util(asset_user, task_name, run_as_admin=run_as_admin)
|
||||
test_asset_user_connectivity_util(asset_user, task_name)
|
||||
|
||||
|
||||
@shared_task(queue="ansible")
|
||||
def push_asset_user_util(asset_user):
|
||||
"""
|
||||
:param asset_user: <Asset user>对象
|
||||
"""
|
||||
from .push_system_user import push_system_user_util
|
||||
if not asset_user.backend.startswith('system_user'):
|
||||
logger.error("Asset user is not from system user")
|
||||
return
|
||||
union_id = asset_user.union_id
|
||||
union_id_list = union_id.split('_')
|
||||
if len(union_id_list) < 2:
|
||||
logger.error("Asset user union id length less than 2")
|
||||
return
|
||||
system_user_id = union_id_list[0]
|
||||
asset_id = union_id_list[1]
|
||||
asset = get_object_or_none(Asset, pk=asset_id)
|
||||
system_user = None
|
||||
if not asset:
|
||||
return
|
||||
hosts = check_asset_can_run_ansible([asset])
|
||||
if asset.is_unixlike:
|
||||
pass
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -18,27 +18,10 @@ UPDATE_ASSETS_HARDWARE_TASKS = [
|
||||
}
|
||||
]
|
||||
|
||||
TEST_ADMIN_USER_CONN_TASKS = [
|
||||
{
|
||||
"name": "ping",
|
||||
"action": {
|
||||
"module": "ping",
|
||||
}
|
||||
}
|
||||
]
|
||||
TEST_WINDOWS_ADMIN_USER_CONN_TASKS = [
|
||||
{
|
||||
"name": "ping",
|
||||
"action": {
|
||||
"module": "win_ping",
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
ASSET_ADMIN_CONN_CACHE_KEY = "ASSET_ADMIN_USER_CONN_{}"
|
||||
|
||||
SYSTEM_USER_CONN_CACHE_KEY = "SYSTEM_USER_CONN_{}"
|
||||
TEST_SYSTEM_USER_CONN_TASKS = [
|
||||
PING_UNIXLIKE_TASKS = [
|
||||
{
|
||||
"name": "ping",
|
||||
"action": {
|
||||
@@ -46,7 +29,7 @@ TEST_SYSTEM_USER_CONN_TASKS = [
|
||||
}
|
||||
}
|
||||
]
|
||||
TEST_WINDOWS_SYSTEM_USER_CONN_TASKS = [
|
||||
PING_WINDOWS_TASKS = [
|
||||
{
|
||||
"name": "ping",
|
||||
"action": {
|
||||
@@ -55,24 +38,6 @@ TEST_WINDOWS_SYSTEM_USER_CONN_TASKS = [
|
||||
}
|
||||
]
|
||||
|
||||
TEST_ASSET_USER_CONN_TASKS = [
|
||||
{
|
||||
"name": "ping",
|
||||
"action": {
|
||||
"module": "ping",
|
||||
}
|
||||
}
|
||||
]
|
||||
TEST_WINDOWS_ASSET_USER_CONN_TASKS = [
|
||||
{
|
||||
"name": "ping",
|
||||
"action": {
|
||||
"module": "win_ping",
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
TASK_OPTIONS = {
|
||||
'timeout': 10,
|
||||
'forks': 10,
|
||||
@@ -98,7 +63,9 @@ GATHER_ASSET_USERS_TASKS = [
|
||||
"name": "get last login",
|
||||
"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 | head -1 | grep -v '^$' | awk '{ print $1\"@\"$3\"@\"$5,$6,$7,$8 }';done"
|
||||
"args": "users=$(getent passwd | grep -v 'nologin' | "
|
||||
"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"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -9,15 +9,16 @@ from django.utils.translation import ugettext as _
|
||||
from common.utils import (
|
||||
capacity_convert, sum_capacity, get_logger
|
||||
)
|
||||
from orgs.utils import org_aware_func
|
||||
from . import const
|
||||
from .utils import clean_hosts
|
||||
from .utils import clean_ansible_task_hosts
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
disk_pattern = re.compile(r'^hd|sd|xvd|vd|nv')
|
||||
__all__ = [
|
||||
'update_assets_hardware_info_util', 'update_asset_hardware_info_manual',
|
||||
'update_assets_hardware_info_period',
|
||||
'update_assets_hardware_info_period', 'update_node_assets_hardware_info_manual',
|
||||
]
|
||||
|
||||
|
||||
@@ -82,6 +83,7 @@ def set_assets_hardware_info(assets, result, **kwargs):
|
||||
|
||||
|
||||
@shared_task
|
||||
@org_aware_func("assets")
|
||||
def update_assets_hardware_info_util(assets, task_name=None):
|
||||
"""
|
||||
Using ansible api to update asset hardware info
|
||||
@@ -93,13 +95,13 @@ def update_assets_hardware_info_util(assets, task_name=None):
|
||||
if task_name is None:
|
||||
task_name = _("Update some assets hardware info")
|
||||
tasks = const.UPDATE_ASSETS_HARDWARE_TASKS
|
||||
hosts = clean_hosts(assets)
|
||||
hosts = clean_ansible_task_hosts(assets)
|
||||
if not hosts:
|
||||
return {}
|
||||
created_by = str(assets[0].org_id)
|
||||
task, created = update_or_create_ansible_task(
|
||||
task_name, hosts=hosts, tasks=tasks, created_by=created_by,
|
||||
pattern='all', options=const.TASK_OPTIONS, run_as_admin=True,
|
||||
task_name, hosts=hosts, tasks=tasks,
|
||||
pattern='all', options=const.TASK_OPTIONS,
|
||||
run_as_admin=True,
|
||||
)
|
||||
result = task.run()
|
||||
set_assets_hardware_info(assets, result)
|
||||
@@ -109,9 +111,7 @@ def update_assets_hardware_info_util(assets, task_name=None):
|
||||
@shared_task(queue="ansible")
|
||||
def update_asset_hardware_info_manual(asset):
|
||||
task_name = _("Update asset hardware info: {}").format(asset.hostname)
|
||||
update_assets_hardware_info_util(
|
||||
[asset], task_name=task_name
|
||||
)
|
||||
update_assets_hardware_info_util([asset], task_name=task_name)
|
||||
|
||||
|
||||
@shared_task(queue="ansible")
|
||||
@@ -123,3 +123,11 @@ def update_assets_hardware_info_period():
|
||||
if not const.PERIOD_TASK_ENABLED:
|
||||
logger.debug("Period task disabled, update assets hardware info pass")
|
||||
return
|
||||
|
||||
|
||||
@shared_task(queue="ansible")
|
||||
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)
|
||||
return result
|
||||
|
||||
@@ -7,10 +7,10 @@ from celery import shared_task
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils import timezone
|
||||
|
||||
from orgs.utils import tmp_to_org
|
||||
from orgs.utils import tmp_to_org, org_aware_func
|
||||
from common.utils import get_logger
|
||||
from ..models import GatheredUser, Node
|
||||
from .utils import clean_hosts
|
||||
from .utils import clean_ansible_task_hosts
|
||||
from . import const
|
||||
|
||||
__all__ = ['gather_asset_users', 'gather_nodes_asset_users']
|
||||
@@ -101,11 +101,12 @@ def add_asset_users(assets, results):
|
||||
|
||||
|
||||
@shared_task(queue="ansible")
|
||||
@org_aware_func("assets")
|
||||
def gather_asset_users(assets, task_name=None):
|
||||
from ops.utils import update_or_create_ansible_task
|
||||
if task_name is None:
|
||||
task_name = _("Gather assets users")
|
||||
assets = clean_hosts(assets)
|
||||
assets = clean_ansible_task_hosts(assets)
|
||||
if not assets:
|
||||
return
|
||||
hosts_category = {
|
||||
@@ -131,7 +132,7 @@ def gather_asset_users(assets, task_name=None):
|
||||
task, created = update_or_create_ansible_task(
|
||||
task_name=_task_name, hosts=value['hosts'], tasks=value['tasks'],
|
||||
pattern='all', options=const.TASK_OPTIONS,
|
||||
run_as_admin=True, created_by=value['hosts'][0].org_id,
|
||||
run_as_admin=True,
|
||||
)
|
||||
raw, summary = task.run()
|
||||
results[k].update(raw['ok'])
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
# ~*~ coding: utf-8 ~*~
|
||||
|
||||
from itertools import groupby
|
||||
from celery import shared_task
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from common.utils import encrypt_password, get_logger
|
||||
from orgs.utils import tmp_to_org, org_aware_func
|
||||
from . import const
|
||||
from .utils import clean_hosts_by_protocol, clean_hosts
|
||||
from .utils import clean_ansible_task_hosts, group_asset_by_platform
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
@@ -15,31 +17,34 @@ __all__ = [
|
||||
]
|
||||
|
||||
|
||||
def get_push_linux_system_user_tasks(system_user):
|
||||
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
|
||||
|
||||
tasks = [
|
||||
{
|
||||
'name': 'Add user {}'.format(system_user.username),
|
||||
'name': 'Add user {}'.format(username),
|
||||
'action': {
|
||||
'module': 'user',
|
||||
'args': 'name={} shell={} state=present'.format(
|
||||
system_user.username, system_user.shell,
|
||||
username, system_user.shell or '/bin/bash',
|
||||
),
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'Add group {}'.format(system_user.username),
|
||||
'name': 'Add group {}'.format(username),
|
||||
'action': {
|
||||
'module': 'group',
|
||||
'args': 'name={} state=present'.format(
|
||||
system_user.username,
|
||||
),
|
||||
'args': 'name={} state=present'.format(username),
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'Check home dir exists',
|
||||
'action': {
|
||||
'module': 'stat',
|
||||
'args': 'path=/home/{}'.format(system_user.username)
|
||||
'args': 'path=/home/{}'.format(username)
|
||||
},
|
||||
'register': 'home_existed'
|
||||
},
|
||||
@@ -47,29 +52,29 @@ def get_push_linux_system_user_tasks(system_user):
|
||||
'name': "Set home dir permission",
|
||||
'action': {
|
||||
'module': 'file',
|
||||
'args': "path=/home/{0} owner={0} group={0} mode=700".format(system_user.username)
|
||||
'args': "path=/home/{0} owner={0} group={0} mode=700".format(username)
|
||||
},
|
||||
'when': 'home_existed.stat.exists == true'
|
||||
}
|
||||
]
|
||||
if system_user.password:
|
||||
if password:
|
||||
tasks.append({
|
||||
'name': 'Set {} password'.format(system_user.username),
|
||||
'name': 'Set {} password'.format(username),
|
||||
'action': {
|
||||
'module': 'user',
|
||||
'args': 'name={} shell={} state=present password={}'.format(
|
||||
system_user.username, system_user.shell,
|
||||
encrypt_password(system_user.password, salt="K3mIlKK"),
|
||||
username, system_user.shell,
|
||||
encrypt_password(password, salt="K3mIlKK"),
|
||||
),
|
||||
}
|
||||
})
|
||||
if system_user.public_key:
|
||||
if public_key:
|
||||
tasks.append({
|
||||
'name': 'Set {} authorized key'.format(system_user.username),
|
||||
'name': 'Set {} authorized key'.format(username),
|
||||
'action': {
|
||||
'module': 'authorized_key',
|
||||
'args': "user={} state=present key='{}'".format(
|
||||
system_user.username, system_user.public_key
|
||||
username, public_key
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -81,26 +86,27 @@ def get_push_linux_system_user_tasks(system_user):
|
||||
sudo_tmp.append(s.strip(','))
|
||||
sudo = ','.join(sudo_tmp)
|
||||
tasks.append({
|
||||
'name': 'Set {} sudo setting'.format(system_user.username),
|
||||
'name': 'Set {} sudo setting'.format(username),
|
||||
'action': {
|
||||
'module': 'lineinfile',
|
||||
'args': "dest=/etc/sudoers state=present regexp='^{0} ALL=' "
|
||||
"line='{0} ALL=(ALL) NOPASSWD: {1}' "
|
||||
"validate='visudo -cf %s'".format(
|
||||
system_user.username, sudo,
|
||||
)
|
||||
"validate='visudo -cf %s'".format(username, sudo)
|
||||
}
|
||||
})
|
||||
|
||||
return tasks
|
||||
|
||||
|
||||
def get_push_windows_system_user_tasks(system_user):
|
||||
def get_push_windows_system_user_tasks(system_user, username=None):
|
||||
if username is None:
|
||||
username = system_user.username
|
||||
password = system_user.password
|
||||
tasks = []
|
||||
if not system_user.password:
|
||||
if not password:
|
||||
return tasks
|
||||
tasks.append({
|
||||
'name': 'Add user {}'.format(system_user.username),
|
||||
task = {
|
||||
'name': 'Add user {}'.format(username),
|
||||
'action': {
|
||||
'module': 'win_user',
|
||||
'args': 'fullname={} '
|
||||
@@ -112,84 +118,100 @@ def get_push_windows_system_user_tasks(system_user):
|
||||
'password_never_expires=yes '
|
||||
'groups="Users,Remote Desktop Users" '
|
||||
'groups_action=add '
|
||||
''.format(system_user.name,
|
||||
system_user.username,
|
||||
system_user.password),
|
||||
''.format(username, username, password),
|
||||
}
|
||||
})
|
||||
}
|
||||
tasks.append(task)
|
||||
return tasks
|
||||
|
||||
|
||||
def get_push_system_user_tasks(host, system_user):
|
||||
if host.is_unixlike():
|
||||
tasks = get_push_linux_system_user_tasks(system_user)
|
||||
elif host.is_windows():
|
||||
tasks = get_push_windows_system_user_tasks(system_user)
|
||||
else:
|
||||
msg = _(
|
||||
"The asset {} system platform {} does not "
|
||||
"support run Ansible tasks".format(host.hostname, host.platform)
|
||||
)
|
||||
logger.info(msg)
|
||||
tasks = []
|
||||
def get_push_system_user_tasks(system_user, platform="unixlike", username=None):
|
||||
"""
|
||||
:param system_user:
|
||||
:param platform:
|
||||
:param username: 当动态时,近推送某个
|
||||
:return:
|
||||
"""
|
||||
get_task_map = {
|
||||
"unixlike": get_push_unixlike_system_user_tasks,
|
||||
"windows": get_push_windows_system_user_tasks,
|
||||
}
|
||||
get_tasks = get_task_map.get(platform, get_push_unixlike_system_user_tasks)
|
||||
if not system_user.username_same_with_user:
|
||||
return get_tasks(system_user)
|
||||
tasks = []
|
||||
# 仅推送这个username
|
||||
if username is not None:
|
||||
tasks.extend(get_tasks(system_user, username))
|
||||
return tasks
|
||||
users = system_user.users.all().values_list('username', flat=True)
|
||||
print(_("System user is dynamic: {}").format(list(users)))
|
||||
for _username in users:
|
||||
tasks.extend(get_tasks(system_user, _username))
|
||||
return tasks
|
||||
|
||||
|
||||
@shared_task(queue="ansible")
|
||||
def push_system_user_util(system_user, assets, task_name):
|
||||
@org_aware_func("system_user")
|
||||
def push_system_user_util(system_user, assets, task_name, username=None):
|
||||
from ops.utils import update_or_create_ansible_task
|
||||
if not system_user.is_need_push():
|
||||
msg = _("Push system user task skip, auto push not enable or "
|
||||
"protocol is not ssh or rdp: {}").format(system_user.name)
|
||||
logger.info(msg)
|
||||
return {}
|
||||
|
||||
# Set root as system user is dangerous
|
||||
if system_user.username.lower() in ["root", "administrator"]:
|
||||
msg = _("For security, do not push user {}".format(system_user.username))
|
||||
logger.info(msg)
|
||||
return {}
|
||||
|
||||
hosts = clean_hosts(assets)
|
||||
hosts = clean_ansible_task_hosts(assets, system_user=system_user)
|
||||
if not hosts:
|
||||
return {}
|
||||
|
||||
hosts = clean_hosts_by_protocol(system_user, hosts)
|
||||
if not hosts:
|
||||
return {}
|
||||
platform_hosts_map = {}
|
||||
hosts_sorted = sorted(hosts, key=group_asset_by_platform)
|
||||
platform_hosts = groupby(hosts_sorted, key=group_asset_by_platform)
|
||||
for i in platform_hosts:
|
||||
platform_hosts_map[i[0]] = list(i[1])
|
||||
|
||||
for host in hosts:
|
||||
system_user.load_specific_asset_auth(host)
|
||||
tasks = get_push_system_user_tasks(host, system_user)
|
||||
if not tasks:
|
||||
continue
|
||||
def run_task(_tasks, _hosts):
|
||||
if not _tasks:
|
||||
return
|
||||
task, created = update_or_create_ansible_task(
|
||||
task_name=task_name, hosts=[host], tasks=tasks, pattern='all',
|
||||
task_name=task_name, hosts=_hosts, tasks=_tasks, pattern='all',
|
||||
options=const.TASK_OPTIONS, run_as_admin=True,
|
||||
created_by=system_user.org_id,
|
||||
)
|
||||
task.run()
|
||||
|
||||
for platform, _hosts in platform_hosts_map.items():
|
||||
if not _hosts:
|
||||
continue
|
||||
print(_("Start push system user for platform: [{}]").format(platform))
|
||||
print(_("Hosts count: {}").format(len(_hosts)))
|
||||
|
||||
if not system_user.has_special_auth():
|
||||
logger.debug("System user not has special auth")
|
||||
tasks = get_push_system_user_tasks(system_user, platform, username=username)
|
||||
run_task(tasks, _hosts)
|
||||
continue
|
||||
|
||||
for _host in _hosts:
|
||||
system_user.load_asset_special_auth(_host)
|
||||
tasks = get_push_system_user_tasks(system_user, platform, username=username)
|
||||
run_task(tasks, [_host])
|
||||
|
||||
|
||||
@shared_task(queue="ansible")
|
||||
def push_system_user_to_assets_manual(system_user):
|
||||
assets = system_user.get_all_assets()
|
||||
def push_system_user_to_assets_manual(system_user, username=None):
|
||||
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)
|
||||
return push_system_user_util(system_user, assets, task_name=task_name, username=username)
|
||||
|
||||
|
||||
@shared_task(queue="ansible")
|
||||
def push_system_user_a_asset_manual(system_user, asset):
|
||||
task_name = _("Push system users to asset: {} => {}").format(
|
||||
system_user.name, asset
|
||||
def push_system_user_a_asset_manual(system_user, asset, username=None):
|
||||
if username is None:
|
||||
username = system_user.username
|
||||
task_name = _("Push system users to asset: {}({}) => {}").format(
|
||||
system_user.name, username, asset
|
||||
)
|
||||
return push_system_user_util(system_user, [asset], task_name=task_name)
|
||||
return push_system_user_util(system_user, [asset], task_name=task_name, username=username)
|
||||
|
||||
|
||||
@shared_task(queue="ansible")
|
||||
def push_system_user_to_assets(system_user, assets):
|
||||
def push_system_user_to_assets(system_user, assets, username=None):
|
||||
task_name = _("Push system users to assets: {}").format(system_user.name)
|
||||
return push_system_user_util(system_user, assets, task_name)
|
||||
return push_system_user_util(system_user, assets, task_name, username=username)
|
||||
|
||||
|
||||
|
||||
@@ -199,4 +221,4 @@ def push_system_user_to_assets(system_user, assets):
|
||||
# @after_app_shutdown_clean_periodic
|
||||
# def push_system_user_period():
|
||||
# for system_user in SystemUser.objects.all():
|
||||
# push_system_user_related_nodes(system_user)
|
||||
# push_system_user_related_nodes(system_user)
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
|
||||
from itertools import groupby
|
||||
from collections import defaultdict
|
||||
|
||||
from celery import shared_task
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from common.utils import get_logger
|
||||
|
||||
from orgs.utils import tmp_to_org, org_aware_func
|
||||
from ..models import SystemUser
|
||||
from . import const
|
||||
from .utils import clean_hosts, clean_hosts_by_protocol
|
||||
from .utils import (
|
||||
clean_ansible_task_hosts, group_asset_by_platform
|
||||
)
|
||||
|
||||
logger = get_logger(__name__)
|
||||
__all__ = [
|
||||
@@ -16,7 +20,7 @@ __all__ = [
|
||||
]
|
||||
|
||||
|
||||
@shared_task(queue="ansible")
|
||||
@org_aware_func("system_user")
|
||||
def test_system_user_connectivity_util(system_user, assets, task_name):
|
||||
"""
|
||||
Test system cant connect his assets or not.
|
||||
@@ -27,41 +31,37 @@ def test_system_user_connectivity_util(system_user, assets, task_name):
|
||||
"""
|
||||
from ops.utils import update_or_create_ansible_task
|
||||
|
||||
hosts = clean_hosts(assets)
|
||||
# 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 = {}
|
||||
hosts_sorted = sorted(hosts, key=group_asset_by_platform)
|
||||
platform_hosts = groupby(hosts_sorted, key=group_asset_by_platform)
|
||||
for i in platform_hosts:
|
||||
platform_hosts_map[i[0]] = list(i[1])
|
||||
|
||||
hosts = clean_hosts_by_protocol(system_user, hosts)
|
||||
if not hosts:
|
||||
return {}
|
||||
|
||||
hosts_category = {
|
||||
'linux': {
|
||||
'hosts': [],
|
||||
'tasks': const.TEST_SYSTEM_USER_CONN_TASKS
|
||||
},
|
||||
'windows': {
|
||||
'hosts': [],
|
||||
'tasks': const.TEST_WINDOWS_SYSTEM_USER_CONN_TASKS
|
||||
}
|
||||
platform_tasks_map = {
|
||||
"unixlike": const.PING_UNIXLIKE_TASKS,
|
||||
"windows": const.PING_WINDOWS_TASKS
|
||||
}
|
||||
for host in hosts:
|
||||
hosts_list = hosts_category['windows']['hosts'] if host.is_windows() \
|
||||
else hosts_category['linux']['hosts']
|
||||
hosts_list.append(host)
|
||||
|
||||
results_summary = dict(
|
||||
contacted=defaultdict(dict), dark=defaultdict(dict), success=True
|
||||
)
|
||||
for k, value in hosts_category.items():
|
||||
if not value['hosts']:
|
||||
continue
|
||||
task, created = update_or_create_ansible_task(
|
||||
task_name=task_name, hosts=value['hosts'], tasks=value['tasks'],
|
||||
|
||||
def run_task(_tasks, _hosts, _username):
|
||||
old_name = "{}".format(system_user)
|
||||
new_name = "{}({})".format(system_user.name, _username)
|
||||
_task_name = task_name.replace(old_name, new_name)
|
||||
_task, created = update_or_create_ansible_task(
|
||||
task_name=_task_name, hosts=_hosts, tasks=_tasks,
|
||||
pattern='all', options=const.TASK_OPTIONS,
|
||||
run_as=system_user.username, created_by=system_user.org_id,
|
||||
run_as=_username,
|
||||
)
|
||||
raw, summary = task.run()
|
||||
raw, summary = _task.run()
|
||||
success = summary.get('success', False)
|
||||
contacted = summary.get('contacted', {})
|
||||
dark = summary.get('dark', {})
|
||||
@@ -70,23 +70,45 @@ def test_system_user_connectivity_util(system_user, assets, task_name):
|
||||
results_summary['contacted'].update(contacted)
|
||||
results_summary['dark'].update(dark)
|
||||
|
||||
for platform, _hosts in platform_hosts_map.items():
|
||||
if not _hosts:
|
||||
continue
|
||||
if platform not in ["unixlike", "windows"]:
|
||||
continue
|
||||
|
||||
tasks = platform_tasks_map[platform]
|
||||
print(_("Start test system user connectivity for platform: [{}]").format(platform))
|
||||
print(_("Hosts count: {}").format(len(_hosts)))
|
||||
# 用户名不是动态的,用户名则是一个
|
||||
if not system_user.username_same_with_user:
|
||||
logger.debug("System user not has special auth")
|
||||
run_task(tasks, _hosts, system_user.username)
|
||||
# 否则需要多个任务
|
||||
else:
|
||||
users = system_user.users.all().values_list('username', flat=True)
|
||||
print(_("System user is dynamic: {}").format(list(users)))
|
||||
for username in users:
|
||||
run_task(tasks, _hosts, username)
|
||||
|
||||
system_user.set_connectivity(results_summary)
|
||||
return results_summary
|
||||
|
||||
|
||||
@shared_task(queue="ansible")
|
||||
@org_aware_func("system_user")
|
||||
def test_system_user_connectivity_manual(system_user):
|
||||
task_name = _("Test system user connectivity: {}").format(system_user)
|
||||
assets = system_user.get_all_assets()
|
||||
return test_system_user_connectivity_util(system_user, assets, task_name)
|
||||
assets = system_user.get_related_assets()
|
||||
test_system_user_connectivity_util(system_user, assets, task_name)
|
||||
|
||||
|
||||
@shared_task(queue="ansible")
|
||||
@org_aware_func("system_user")
|
||||
def test_system_user_connectivity_a_asset(system_user, asset):
|
||||
task_name = _("Test system user connectivity: {} => {}").format(
|
||||
system_user, asset
|
||||
)
|
||||
return test_system_user_connectivity_util(system_user, [asset], task_name)
|
||||
test_system_user_connectivity_util(system_user, [asset], task_name)
|
||||
|
||||
|
||||
@shared_task(queue="ansible")
|
||||
@@ -94,8 +116,9 @@ def test_system_user_connectivity_period():
|
||||
if not const.PERIOD_TASK_ENABLED:
|
||||
logger.debug("Period task disabled, test system user connectivity pass")
|
||||
return
|
||||
system_users = SystemUser.objects.all()
|
||||
for system_user in system_users:
|
||||
queryset_map = SystemUser.objects.all_group_by_org()
|
||||
for org, system_user in queryset_map.items():
|
||||
task_name = _("Test system user connectivity period: {}").format(system_user)
|
||||
assets = system_user.get_all_assets()
|
||||
test_system_user_connectivity_util(system_user, assets, task_name)
|
||||
with tmp_to_org(org):
|
||||
assets = system_user.get_related_assets()
|
||||
test_system_user_connectivity_util(system_user, assets, task_name)
|
||||
|
||||
@@ -7,7 +7,8 @@ from common.utils import get_logger
|
||||
|
||||
logger = get_logger(__file__)
|
||||
__all__ = [
|
||||
'check_asset_can_run_ansible', 'clean_hosts', 'clean_hosts_by_protocol'
|
||||
'check_asset_can_run_ansible', 'clean_ansible_task_hosts',
|
||||
'group_asset_by_platform',
|
||||
]
|
||||
|
||||
|
||||
@@ -23,23 +24,43 @@ def check_asset_can_run_ansible(asset):
|
||||
return True
|
||||
|
||||
|
||||
def clean_hosts(assets):
|
||||
clean_assets = []
|
||||
def check_system_user_can_run_ansible(system_user):
|
||||
if not system_user.is_need_push():
|
||||
msg = _("Push system user task skip, auto push not enable or "
|
||||
"protocol is not ssh or rdp: {}").format(system_user.name)
|
||||
logger.info(msg)
|
||||
return False
|
||||
|
||||
# Push root as system user is dangerous
|
||||
if system_user.username.lower() in ["root", "administrator"]:
|
||||
msg = _("For security, do not push user {}".format(system_user.username))
|
||||
logger.info(msg)
|
||||
return False
|
||||
|
||||
# if system_user.protocol != "ssh":
|
||||
# msg = _("System user protocol not ssh: {}".format(system_user))
|
||||
# logger.info(msg)
|
||||
# return False
|
||||
return True
|
||||
|
||||
|
||||
def clean_ansible_task_hosts(assets, system_user=None):
|
||||
if system_user and not check_system_user_can_run_ansible(system_user):
|
||||
return []
|
||||
cleaned_assets = []
|
||||
for asset in assets:
|
||||
if not check_asset_can_run_ansible(asset):
|
||||
continue
|
||||
clean_assets.append(asset)
|
||||
if not clean_assets:
|
||||
cleaned_assets.append(asset)
|
||||
if not cleaned_assets:
|
||||
logger.info(_("No assets matched, stop task"))
|
||||
return clean_assets
|
||||
return cleaned_assets
|
||||
|
||||
|
||||
def clean_hosts_by_protocol(system_user, assets):
|
||||
hosts = [
|
||||
asset for asset in assets
|
||||
if asset.has_protocol(system_user.protocol)
|
||||
]
|
||||
if not hosts:
|
||||
msg = _("No assets matched related system user protocol, stop task")
|
||||
logger.info(msg)
|
||||
return hosts
|
||||
def group_asset_by_platform(asset):
|
||||
if asset.is_unixlike():
|
||||
return 'unixlike'
|
||||
elif asset.is_windows():
|
||||
return 'windows'
|
||||
else:
|
||||
return 'other'
|
||||
|
||||
@@ -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,98 +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 authAssetId = "";
|
||||
var authHostname = "";
|
||||
var authUsername = "";
|
||||
var mfaFor = "";
|
||||
|
||||
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 = "{% url "api-assets:asset-user-auth-info" %}?asset_id=" + authAssetId + "&username=" + authUsername;
|
||||
if (prefer) {
|
||||
url = setUrlParam(url, 'prefer', prefer)
|
||||
}
|
||||
$("#id_username_view").html(authUsername);
|
||||
$("#id_hostname_view").html(authHostname);
|
||||
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 () {
|
||||
showAuth();
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% block modal_button %}
|
||||
<button data-dismiss="modal" class="btn btn-white close_btn2" type="button">{% trans "Close" %}</button>
|
||||
{% endblock %}
|
||||
@@ -1,161 +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 assetUserListUrl = "{% url "api-assets:asset-user-list" %}";
|
||||
var assetUserTable;
|
||||
var needPush = false;
|
||||
var prefer = null;
|
||||
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";
|
||||
|
||||
function initAssetUserTable() {
|
||||
var options = {
|
||||
ele: $('#asset_user_list_table'),
|
||||
toggle: true,
|
||||
columnDefs: [
|
||||
{
|
||||
targets: 5, createdCell: function (td, cellData) {
|
||||
var innerHtml = "";
|
||||
if (cellData.status == 1) {
|
||||
innerHtml = '<i class="fa fa-circle text-navy"></i>'
|
||||
} else if (cellData.status == 0) {
|
||||
innerHtml = '<i class="fa fa-circle text-danger"></i>'
|
||||
} else {
|
||||
innerHtml = '<i class="fa fa-circle text-warning"></i>'
|
||||
}
|
||||
var dateManual = toSafeLocalDateStr(cellData.datetime);
|
||||
var dataContent = testDatetime + dateManual;
|
||||
innerHtml = "<a data-toggle='popover' data-content='" + dataContent + "'" + 'data-placement="auto bottom"' + ">" + innerHtml + "</a>";
|
||||
$(td).html(innerHtml);
|
||||
}
|
||||
},
|
||||
{
|
||||
targets: 6, createdCell: function (td, cellData) {
|
||||
var data = toSafeLocalDateStr(cellData);
|
||||
$(td).html(data);
|
||||
},
|
||||
},
|
||||
{
|
||||
targets: 7, createdCell: function (td, cellData, rowData) {
|
||||
var view_btn = '<button class="btn btn-xs btn-primary m-l-xs btn-view-auth" data-user="username123" data-hostname="hostname123" data-asset="asset123">{% trans "View" %}</button>'
|
||||
var update_btn = '<li><a class="btn-update-auth" data-user="username123" data-hostname="hostname123" data-asset="asset123">{% trans 'Update' %}</a></li>';
|
||||
var test_btn = '<li><a class="btn-test-auth" data-user="username123" data-hostname="hostname123" data-asset="asset123">{% trans 'Test' %}</a></li>';
|
||||
var push_btn = '<li><a class="btn-push-auth" data-user="username123" data-hostname="hostname123" data-asset="asset123">{% trans 'Push' %}</a></li>';
|
||||
if (needPush) {
|
||||
test_btn += push_btn;
|
||||
}
|
||||
var actions = '<div class="btn-group">' + view_btn +
|
||||
' <button data-toggle="dropdown" class="btn btn-primary btn-xs dropdown-toggle">' +
|
||||
' <span class="caret"></span>' +
|
||||
' </button>' +
|
||||
' <ul class="dropdown-menu">' +
|
||||
update_btn + test_btn +
|
||||
' </ul>' +
|
||||
' </div>';
|
||||
actions = actions.replaceAll("username123", rowData.username)
|
||||
.replaceAll("hostname123", rowData.hostname)
|
||||
.replaceAll("asset123", rowData.asset);
|
||||
$(td).html(actions);
|
||||
},
|
||||
width: '70px'
|
||||
}
|
||||
],
|
||||
ajax_url: assetUserListUrl,
|
||||
columns: [
|
||||
{data: "id"}, {data: "hostname"}, {data: "ip"},
|
||||
{data: "username"}, {data: "version", orderable: false},
|
||||
{data: "connectivity"},
|
||||
{data: "date_created", orderable: false},
|
||||
{data: "asset", orderable: false}
|
||||
],
|
||||
op_html: $('#actions').html()
|
||||
};
|
||||
table = jumpserver.initServerSideDataTable(options);
|
||||
return table
|
||||
}
|
||||
$(document).ready(function(){
|
||||
})
|
||||
.on('click', '.btn-view-auth', function () {
|
||||
authAssetId = $(this).data("asset") ;
|
||||
authHostname = $(this).data("hostname");
|
||||
authUsername = $(this).data('user');
|
||||
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("user") ;
|
||||
authHostname = $(this).data("hostname");
|
||||
authAssetId = $(this).data("asset");
|
||||
$("#asset_user_auth_update_modal").modal('show');
|
||||
})
|
||||
.on("click", '.btn-test-auth', function () {
|
||||
authUsername = $(this).data("user") ;
|
||||
authAssetId = $(this).data("asset");
|
||||
var the_url = "{% url 'api-assets:asset-user-connective' %}" + "?asset_id=" + authAssetId + "&username=" + authUsername;
|
||||
if (prefer) {
|
||||
the_url = setUrlParam(the_url, "prefer", prefer)
|
||||
}
|
||||
var success = function (data) {
|
||||
var task_id = data.task;
|
||||
showCeleryTaskLog(task_id);
|
||||
};
|
||||
requestApi({
|
||||
url: the_url,
|
||||
method: 'GET',
|
||||
success: success,
|
||||
flash_message: false
|
||||
});
|
||||
})
|
||||
|
||||
|
||||
</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,344 +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:refresh-nodes-cache' %}';
|
||||
requestApi({
|
||||
url: url,
|
||||
method: 'GET',
|
||||
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
|
||||
}
|
||||
if (current_node.children && current_node.children.length > 0) {
|
||||
toastr.error("{% trans 'Have child node, cancel' %}");
|
||||
} else if (current_node.meta.node.assets_amount !== 0) {
|
||||
toastr.error("{% trans 'Have assets, cancel' %}");
|
||||
} else {
|
||||
var url = "{% url 'api-assets:node-detail' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node_id);
|
||||
$.ajax({
|
||||
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,267 +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.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.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();
|
||||
})
|
||||
.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"]);
|
||||
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,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 () {
|
||||
assetUserListUrl = setUrlParam(assetUserListUrl, "admin_user_id", "{{ admin_user.id }}");
|
||||
prefer = "admin_user";
|
||||
initAssetUserTable();
|
||||
})
|
||||
.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>{{ asset_group.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 %}
|
||||
@@ -1,77 +0,0 @@
|
||||
{% extends '_base_list.html' %}
|
||||
{% load i18n static %}
|
||||
{% block help_message %}
|
||||
{% trans 'Admin users are asset (charged server) on the root, or have NOPASSWD: ALL sudo permissions users, '%}
|
||||
{% trans 'Jumpserver users of the system using the user to `push system user`, `get assets hardware information`, etc. '%}
|
||||
{% endblock %}
|
||||
{% block table_search %}
|
||||
{% include '_csv_import_export.html' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block table_container %}
|
||||
<div class="uc pull-left m-r-5">
|
||||
<a href="{% url "assets:admin-user-create" %}" class="btn btn-sm btn-primary"> {% trans "Create admin user" %} </a>
|
||||
</div>
|
||||
<table class="table table-striped table-bordered table-hover " id="admin_user_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 'Username' %}</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>
|
||||
var admin_user_table = 0;
|
||||
function initTable() {
|
||||
var options = {
|
||||
ele: $('#admin_user_list_table'),
|
||||
columnDefs: [
|
||||
{targets: 1, render: function (cellData, tp, rowData, meta) {
|
||||
cellData = htmlEscape(cellData);
|
||||
var detail_btn = '<a href="{% url "assets:admin-user-detail" pk=DEFAULT_PK %}">' + cellData + '</a>';
|
||||
return detail_btn.replace('{{ DEFAULT_PK }}', rowData.id);
|
||||
}},
|
||||
{targets: 5, createdCell: function (td, cellData, rowData) {
|
||||
var update_btn = '<a href="{% url "assets:admin-user-update" pk=DEFAULT_PK %}" class="btn btn-xs m-l-xs btn-info">{% trans "Update" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
|
||||
var del_btn = '<a class="btn btn-xs btn-danger m-l-xs btn_admin_user_delete" data-uid="{{ DEFAULT_PK }}">{% trans "Delete" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
|
||||
$(td).html(update_btn + del_btn)
|
||||
}}],
|
||||
ajax_url: '{% url "api-assets:admin-user-list" %}',
|
||||
columns: [
|
||||
{data: function(){return ""}}, {data: "name"}, {data: "username" }, {data: "assets_amount", orderable: false},
|
||||
{#{data: "connectivity_amount"}, {data: "connectivity_amount"}, {data: "connectivity_amount"},#}
|
||||
{data: "comment"}, {data: "id", orderable: false, width: "120px"}
|
||||
]
|
||||
};
|
||||
return jumpserver.initServerSideDataTable(options);
|
||||
}
|
||||
|
||||
$(document).ready(function(){
|
||||
admin_user_table = initTable();
|
||||
initCsvImportExport(admin_user_table, "{% trans "Admin user" %}")
|
||||
})
|
||||
.on('click', '.btn_admin_user_delete', function () {
|
||||
var $this = $(this);
|
||||
var $data_table = $("#admin_user_list_table").DataTable();
|
||||
var name = $(this).closest("tr").find(":nth-child(2)").children('a').html();
|
||||
var uid = $this.data('uid');
|
||||
var the_url = '{% url "api-assets:admin-user-detail" pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', uid);
|
||||
objectDelete($this, name, the_url);
|
||||
setTimeout( function () {
|
||||
$data_table.ajax.reload();
|
||||
}, 3000);
|
||||
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,94 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load common_tags %}
|
||||
{% 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:asset-detail' pk=asset.id %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Asset detail' %}</a>
|
||||
</li>
|
||||
<li class="active">
|
||||
<a href="{% url 'assets:asset-user-list' pk=asset.id %}" class="text-center"><i class="fa fa-bar-chart-o"></i> {% trans 'Asset user 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 users of' %} <b>{{ asset.hostname }} </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 modify' %}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
{% if asset.is_support_ansible %}
|
||||
<tr class="no-borders-tr">
|
||||
<td>{% trans 'Test connective' %}:</td>
|
||||
<td>
|
||||
<span class="pull-right">
|
||||
<button type="button" class="btn btn-primary btn-xs" id="btn-bulk-test-connective" style="width: 54px">{% trans 'Test' %}</button>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
assetUserListUrl = setUrlParam(assetUserListUrl, "asset_id", "{{ asset.id }}");
|
||||
initAssetUserTable()
|
||||
})
|
||||
|
||||
.on('click', '#btn-bulk-test-connective', function () {
|
||||
var the_url = "{% url 'api-assets:asset-user-connective' %}" + "?asset_id={{ asset.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,71 +0,0 @@
|
||||
{% extends '_base_create_update.html' %}
|
||||
{% load static %}
|
||||
{% load bootstrap3 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block form %}
|
||||
<div class="ydxbd" id="formlists" style="display: block;">
|
||||
<p id="tags_p" class="mgl-5 c02">{% trans 'Select properties that need to be modified' %}</p>
|
||||
<div class="tagBtnList">
|
||||
<a class="label label-primary" id="change_all" value="1">{% trans 'Select all' %}</a>
|
||||
{% for field in form %}
|
||||
{% if field.name != 'assets' %}
|
||||
<a data-id="{{ field.id_for_label }}" class="label label-default label-primary field-tag" value="1">{{ field.label }}</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<form method="post" class="form-horizontal" id="add_form">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form layout="horizontal" %}
|
||||
<div class="form-group abc">
|
||||
<div class="col-sm-4 col-sm-offset-2">
|
||||
<button class="btn btn-white" type="reset">{% trans 'Reset' %}</button>
|
||||
<button class="btn btn-primary" type="submit">{% trans 'Submit' %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% include 'assets/_asset_list_modal.html' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$('.select2').select2();
|
||||
initAssetTreeModel("#id_assets");
|
||||
}).on('click', '.field-tag', function() {
|
||||
changeField(this);
|
||||
}).on('click', '#change_all', function () {
|
||||
var tag_fields = $('.field-tag');
|
||||
var $this = $(this);
|
||||
var active = '1';
|
||||
if ($this.attr('value') == '0'){
|
||||
active = '0';
|
||||
$this.attr('value', '1').addClass('label-primary')
|
||||
} else {
|
||||
active = '1';
|
||||
$this.attr('value', '0').removeClass('label-primary')
|
||||
}
|
||||
$.each(tag_fields, function (k, v) {
|
||||
changeField(v, active)
|
||||
})
|
||||
});
|
||||
|
||||
function changeField(obj, active) {
|
||||
var $this = $(obj);
|
||||
var field_id = $this.data('id');
|
||||
if (!active) {
|
||||
active = $this.attr('value');
|
||||
}
|
||||
if (active == '0') {
|
||||
$this.attr('value', '1').addClass('label-primary');
|
||||
var form_groups = $('#add_form .form-group:not(.abc)');
|
||||
form_groups.filter(':has(#' + field_id + ')').show().find('select,input').prop('disabled', false)
|
||||
} else {
|
||||
$this.attr('value', '0').removeClass('label-primary');
|
||||
var form_groups = $('#add_form .form-group:not(.abc)');
|
||||
form_groups.filter(':has(#' + field_id + ')').hide().find('select,input').prop('disabled', true)
|
||||
}
|
||||
}
|
||||
</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