mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-12-15 16:42:34 +00:00
Compare commits
842 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
3cc9e0a66c | ||
|
|
ec1f6677ec | ||
|
|
ae98fb4332 | ||
|
|
29730757b8 | ||
|
|
1c2feedb27 | ||
|
|
b227d9cdc1 | ||
|
|
da6a0c286d | ||
|
|
13a042bc0f | ||
|
|
7f9644dbac | ||
|
|
ece9b16351 | ||
|
|
23b896b301 | ||
|
|
dd52baae12 | ||
|
|
eb1ca4c0f2 | ||
|
|
8e75e519fa | ||
|
|
389c6b5a84 | ||
|
|
6fc7a4cb21 | ||
|
|
68455156a3 | ||
|
|
2827a64095 | ||
|
|
c21505e92d | ||
|
|
7d855e5ad8 | ||
|
|
17956bf0db | ||
|
|
267a7fc9f7 | ||
|
|
9ef4762817 | ||
|
|
29457ad867 | ||
|
|
d5082d1379 | ||
|
|
c07536b26f | ||
|
|
678999b9f5 | ||
|
|
4295aec492 | ||
|
|
75a2b9eac2 | ||
|
|
2fc68ca6f1 | ||
|
|
56a156b717 | ||
|
|
7c59134635 | ||
|
|
f98f0e2c06 | ||
|
|
63746accf9 | ||
|
|
670e2d7352 | ||
|
|
db3b60faf9 | ||
|
|
f1b8c1965d | ||
|
|
aa2a77ee7e | ||
|
|
b107d15097 | ||
|
|
52b9c400eb | ||
|
|
851bd99eaf | ||
|
|
0c0eb843ab | ||
|
|
bd19f8afe8 | ||
|
|
bafffc95b8 | ||
|
|
14581ac775 | ||
|
|
6ddd48bfb1 | ||
|
|
ff1828bdd6 | ||
|
|
84e5177ce2 | ||
|
|
1c9be184fd | ||
|
|
57155c2469 | ||
|
|
f9578f4474 | ||
|
|
a37e14221e | ||
|
|
005272cdc0 | ||
|
|
f88cb3da20 | ||
|
|
9f42dfb26e | ||
|
|
f702fc7d93 | ||
|
|
05adb4e95f | ||
|
|
210a2d7fe6 | ||
|
|
c9dc1ea254 | ||
|
|
79b5618756 | ||
|
|
bf922459ff | ||
|
|
a27fb18a17 | ||
|
|
f62f750266 | ||
|
|
b072e98148 | ||
|
|
352bfeeb7a | ||
|
|
1d7bdd5f5f | ||
|
|
8702761303 | ||
|
|
ce3cc80037 | ||
|
|
9556d33d1c | ||
|
|
02650c9cdc | ||
|
|
75c6047236 | ||
|
|
e6c369cfd8 | ||
|
|
131496bfee | ||
|
|
c084412e53 | ||
|
|
ad81d6c28e | ||
|
|
08fb8f5d92 | ||
|
|
4df08f0521 | ||
|
|
c963937f00 | ||
|
|
b64ae358fb | ||
|
|
42c5783e43 | ||
|
|
a2350e7f1d | ||
|
|
b3114a1f3d | ||
|
|
b4cf540e51 | ||
|
|
deeb9cdfa6 | ||
|
|
7a6a1b9b59 | ||
|
|
15d1e021de | ||
|
|
f063832bc6 | ||
|
|
1d30c1900d | ||
|
|
78a227af3e | ||
|
|
e978308335 | ||
|
|
7193d7fc1b | ||
|
|
d6a95d3f1a | ||
|
|
829e1f4cac | ||
|
|
33dc7dec84 | ||
|
|
b90960e6e1 | ||
|
|
b365ba7982 | ||
|
|
9cfccf8a1b | ||
|
|
16f727c60d | ||
|
|
ac2ba63856 | ||
|
|
55c95c58f6 | ||
|
|
907703d911 | ||
|
|
e1919d0a62 | ||
|
|
4ac4b517f4 | ||
|
|
f296dce935 | ||
|
|
bc5a240121 | ||
|
|
b4498f2267 | ||
|
|
b2932803b0 | ||
|
|
cea336a8ce | ||
|
|
16864ca34e | ||
|
|
c5785e17aa | ||
|
|
f89c6124a6 | ||
|
|
47b1a13bea | ||
|
|
df52240227 | ||
|
|
4944ac8e75 | ||
|
|
a88db3d9db | ||
|
|
fd1b9d97c0 | ||
|
|
5e95f262b2 | ||
|
|
4493a82425 | ||
|
|
078751979e | ||
|
|
1fd98ae404 | ||
|
|
560de55c3c | ||
|
|
dd6e0f548c | ||
|
|
a07a90f1c9 | ||
|
|
64642ab48b | ||
|
|
12f51c9474 | ||
|
|
50106474d4 | ||
|
|
b3e2b30e71 | ||
|
|
7427f02e0b | ||
|
|
330c078597 | ||
|
|
eb05588776 | ||
|
|
98d3a36d98 | ||
|
|
fedf50f9dc | ||
|
|
1876b7dfba | ||
|
|
873d909879 | ||
|
|
15b8a2efb9 | ||
|
|
45444149dd | ||
|
|
a5485011c8 | ||
|
|
7c5592fa36 | ||
|
|
715b04e2b2 | ||
|
|
73086328e4 | ||
|
|
7f126ab652 | ||
|
|
5bcd122de9 | ||
|
|
a92aec2775 | ||
|
|
cc9554cdb4 | ||
|
|
100dd24ff5 | ||
|
|
3a812c2267 | ||
|
|
f47781a330 | ||
|
|
f96dd15f20 | ||
|
|
ffdc1eb198 | ||
|
|
91d1a62c98 | ||
|
|
793adbccab | ||
|
|
72ea036a29 | ||
|
|
9e322323f3 | ||
|
|
6951f4dd97 | ||
|
|
54887a3460 | ||
|
|
dafe71af42 | ||
|
|
82b9c2519b | ||
|
|
d563a11acf | ||
|
|
c5438fd65d | ||
|
|
66b8e59ae7 | ||
|
|
e424315d93 | ||
|
|
4c801bb828 | ||
|
|
1bba00412a | ||
|
|
bf40aa8df0 | ||
|
|
0c349f9350 | ||
|
|
4dc5d7d70e | ||
|
|
3fc30aa96c | ||
|
|
bb1349e962 | ||
|
|
c9ee8edeaf | ||
|
|
bfd8a9c66d | ||
|
|
030551d987 | ||
|
|
2f4096bf3d | ||
|
|
ebe129b3c2 | ||
|
|
61a8d6a5ca | ||
|
|
c6e7872ba8 | ||
|
|
096d4a4221 | ||
|
|
9b5cb8b0c5 | ||
|
|
bea5dadbac | ||
|
|
18f8864720 | ||
|
|
f71290d645 | ||
|
|
e69d283af9 | ||
|
|
31f5b2c06c | ||
|
|
611d5fc1fa | ||
|
|
8dba54e7c5 | ||
|
|
2ef487a92f | ||
|
|
aebf32d7be | ||
|
|
870e04feac | ||
|
|
4b2fbbfb84 | ||
|
|
0946645783 | ||
|
|
f3d2e5d1b3 | ||
|
|
6bfb026e47 | ||
|
|
aa428b0299 | ||
|
|
0a74602df3 | ||
|
|
05e7311b0b | ||
|
|
f40d51c84f | ||
|
|
46a2311e30 | ||
|
|
76e59f5bcd | ||
|
|
e731c01cc4 | ||
|
|
6067dbcf21 | ||
|
|
4057064b7f | ||
|
|
3f1e75c6f9 | ||
|
|
5434b65773 | ||
|
|
a01126c6c7 | ||
|
|
200ca65dc5 | ||
|
|
78a7bfbd30 | ||
|
|
fc58906bce | ||
|
|
e0d7a0e239 | ||
|
|
82077f4a0e | ||
|
|
c31b56ddd9 | ||
|
|
0a08ba3b9c | ||
|
|
2ea3ad4ca5 | ||
|
|
0d2fc27a97 | ||
|
|
f53cf8d544 | ||
|
|
596e5a6dd1 | ||
|
|
aeff0ab5f3 | ||
|
|
0b211d33b2 | ||
|
|
f4136decde | ||
|
|
74f22274b4 | ||
|
|
9e4874834f | ||
|
|
bd323d608e | ||
|
|
f9e41d71dc | ||
|
|
08775551c2 | ||
|
|
fb9e2ac9f6 | ||
|
|
edce831e46 | ||
|
|
12e1e559b5 | ||
|
|
fa02e3fb75 | ||
|
|
ed5a57042a | ||
|
|
6fbc4ce4fb | ||
|
|
eedaaddbf5 | ||
|
|
bae4387068 | ||
|
|
8de863525a | ||
|
|
6ce9815d51 | ||
|
|
9d201bbf98 | ||
|
|
96adaca3c3 | ||
|
|
cf65e9bfe3 | ||
|
|
b51ad05a4b | ||
|
|
ca178c534e | ||
|
|
d7948425df | ||
|
|
a0be7333fb | ||
|
|
fde8702916 | ||
|
|
bf0575b74d | ||
|
|
3dfb2d6af2 | ||
|
|
c25335cf95 | ||
|
|
ebbe49fab2 | ||
|
|
28ad362b8d | ||
|
|
b595ac9bc5 | ||
|
|
11f0024c33 | ||
|
|
dc3a9561c2 | ||
|
|
4f7ab69508 | ||
|
|
23b777b23b | ||
|
|
fe235823b4 | ||
|
|
2afeba8b5f | ||
|
|
87247da0ec | ||
|
|
d0ba67ed50 | ||
|
|
c163427db2 | ||
|
|
c7ecfd7943 | ||
|
|
db5ff0f922 | ||
|
|
c29381e657 | ||
|
|
ff62968bf6 | ||
|
|
4e1e60046a | ||
|
|
82c4705350 | ||
|
|
517c682201 | ||
|
|
bf92b61b78 | ||
|
|
f6d2ac9b7b | ||
|
|
98a8ef1526 | ||
|
|
4167987d67 | ||
|
|
ef41ba3117 | ||
|
|
458bee9a19 | ||
|
|
d22f60364a | ||
|
|
90321ab68c | ||
|
|
a5814c8100 | ||
|
|
87fd342b68 | ||
|
|
d544f7f936 | ||
|
|
fbe4f4c119 | ||
|
|
4d53b6f078 | ||
|
|
d2c65dd783 | ||
|
|
0c1463bc8b | ||
|
|
39b4145868 | ||
|
|
a57df0e05f | ||
|
|
36d98a237f | ||
|
|
cf719f492d | ||
|
|
69bcf3de2e | ||
|
|
d3cf2afb51 | ||
|
|
3c5ebeeb20 | ||
|
|
58f564b028 | ||
|
|
8fd65a372e | ||
|
|
f8f1aee997 | ||
|
|
9bb7c1adc1 | ||
|
|
d5cb0a2e59 | ||
|
|
7c4dee324f | ||
|
|
4c1821e42e | ||
|
|
f31ed0afb8 | ||
|
|
5e5f3ad383 | ||
|
|
f802fb645d | ||
|
|
93e8c9e534 | ||
|
|
0b405fdefa | ||
|
|
c995050ed7 | ||
|
|
0dbf2ab2e5 | ||
|
|
22cc8ffbdd | ||
|
|
6b8b0f4a0b | ||
|
|
b3502a9291 | ||
|
|
1c92516fd0 | ||
|
|
c3f02dbaab | ||
|
|
214c4d2f75 | ||
|
|
5a98b61c1e | ||
|
|
11ae5a4a4a | ||
|
|
5f3583279c | ||
|
|
950358f5ba | ||
|
|
84b3e29db6 | ||
|
|
a33d5cc53b | ||
|
|
3c7fa1b60b | ||
|
|
f4fc757379 | ||
|
|
7cf617c3fc | ||
|
|
87deb18791 | ||
|
|
82a6369568 | ||
|
|
3a10fd4160 | ||
|
|
a2f8f43321 | ||
|
|
6ebe8e16bd | ||
|
|
86522627d3 | ||
|
|
334e3bef0e | ||
|
|
0930716a54 | ||
|
|
f984acf1b2 | ||
|
|
87c7452d19 | ||
|
|
792fc3b0f0 | ||
|
|
673ebbec1b | ||
|
|
c2f78b1170 | ||
|
|
20f8c12576 | ||
|
|
1bc6492064 | ||
|
|
a7ae132b2b | ||
|
|
a978bf990d | ||
|
|
491f59026c | ||
|
|
a6531fedb5 | ||
|
|
e5250294b0 | ||
|
|
f3dc9b886b | ||
|
|
70664d435f | ||
|
|
ee97144a10 | ||
|
|
9870d3aa85 | ||
|
|
c3699889e2 | ||
|
|
237d51cff9 | ||
|
|
ccdb770972 | ||
|
|
8fcfcf7ad3 | ||
|
|
31280a3869 | ||
|
|
70a4f137b8 | ||
|
|
01ead7c8e1 | ||
|
|
ad3524f942 | ||
|
|
1a1f20920d | ||
|
|
b4d164954d | ||
|
|
52d0961519 | ||
|
|
e093475351 | ||
|
|
13822cbf6d | ||
|
|
b732f4e32c | ||
|
|
c0d1bf8ecc | ||
|
|
1f5cdc3fa4 | ||
|
|
21b17ca41a | ||
|
|
70713af95a | ||
|
|
2f2b0b28b3 | ||
|
|
7574966ccf | ||
|
|
3986bbf7ac | ||
|
|
4102dc6842 | ||
|
|
d1dc3342a2 | ||
|
|
dfedfc7e7a | ||
|
|
7ff6391801 | ||
|
|
5f96f8c229 | ||
|
|
7e7583e4fc | ||
|
|
74b4ff4993 | ||
|
|
ca50c0a9b3 | ||
|
|
cea6ea4826 | ||
|
|
3bd3564b2b | ||
|
|
9ee9be3339 | ||
|
|
ce530e417c | ||
|
|
c8cade622a | ||
|
|
e52178811c | ||
|
|
74ab311d3e |
@@ -9,8 +9,8 @@ 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 -i https://mirrors.aliyun.com/pypi/simple/ -r requirements.txt || pip install -r requirements.txt
|
||||
RUN cd /tmp/requirements && pip install --upgrade pip setuptools && pip install wheel && \
|
||||
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt || pip install -r requirements.txt
|
||||
RUN mkdir -p /root/.ssh/ && echo -e "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config
|
||||
|
||||
COPY . /opt/jumpserver
|
||||
|
||||
382
README.md
382
README.md
@@ -1,198 +1,230 @@
|
||||
# Jumpserver 多云环境下更好用的堡垒机
|
||||
# JumpServer 多云环境下更好用的堡垒机
|
||||
|
||||
[](https://www.python.org/)
|
||||
[](https://www.djangoproject.com/)
|
||||
[](https://www.ansible.com/)
|
||||
[](http://www.paramiko.org/)
|
||||
[](https://www.djangoproject.com/)
|
||||
|
||||
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 领域的的又一全新力作,欢迎关注和使用。
|
||||
|
||||
## 核心功能列表
|
||||
## 特色优势
|
||||
|
||||
<table class="subscription-level-table">
|
||||
<tr class="subscription-level-tr-border">
|
||||
<td class="features-first-td-background-style" rowspan="4">身份验证 Authentication</td>
|
||||
<td class="features-second-td-background-style" rowspan="3" >登录认证
|
||||
</td>
|
||||
<td class="features-third-td-background-style">资源统一登录和认证
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="subscription-level-tr-border">
|
||||
<td class="features-third-td-background-style">LDAP 认证
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="subscription-level-tr-border">
|
||||
<td class="features-third-td-background-style">支持 OpenID,实现单点登录
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="subscription-level-tr-border">
|
||||
<td class="features-second-td-background-style">多因子认证
|
||||
</td>
|
||||
<td class="features-third-td-background-style">MFA(Google Authenticator)
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="subscription-level-tr-border">
|
||||
<td class="features-first-td-background-style" rowspan="9">账号管理 Account</td>
|
||||
<td class="features-second-td-background-style" rowspan="2">集中账号管理
|
||||
</td>
|
||||
<td class="features-third-td-background-style">管理用户管理
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="subscription-level-tr-border">
|
||||
<td class="features-third-td-background-style">系统用户管理
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="subscription-level-tr-border">
|
||||
<td class="features-second-td-background-style" rowspan="4">统一密码管理
|
||||
</td>
|
||||
<td class="features-third-td-background-style">资产密码托管
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="subscription-level-tr-border">
|
||||
<td class="features-third-td-background-style">自动生成密码
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="subscription-level-tr-border">
|
||||
<td class="features-third-td-background-style">密码自动推送
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="subscription-level-tr-border">
|
||||
<td class="features-third-td-background-style">密码过期设置
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="subscription-level-tr-border">
|
||||
<td class="features-outline-td-background-style" rowspan="2">批量密码变更(X-PACK)
|
||||
</td>
|
||||
<td class="features-outline-td-background-style">定期批量修改密码
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="subscription-level-tr-border">
|
||||
<td class="features-outline-td-background-style">生成随机密码
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="subscription-level-tr-border">
|
||||
<td class="features-outline-td-background-style">多云环境的资产纳管(X-PACK)
|
||||
</td>
|
||||
<td class="features-outline-td-background-style">对私有云、公有云资产统一纳管
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="subscription-level-tr-border">
|
||||
<td class="features-first-td-background-style" rowspan="9">授权控制 Authorization</td>
|
||||
<td class="features-second-td-background-style" rowspan="3">资产授权管理
|
||||
</td>
|
||||
<td class="features-third-td-background-style">资产树
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="subscription-level-tr-border">
|
||||
<td class="features-third-td-background-style">资产或资产组灵活授权
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="subscription-level-tr-border">
|
||||
<td class="features-third-td-background-style">节点内资产自动继承授权
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="subscription-level-tr-border">
|
||||
<td class="features-outline-td-background-style">RemoteApp(X-PACK)
|
||||
</td>
|
||||
<td class="features-outline-td-background-style">实现更细粒度的应用级授权
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="subscription-level-tr-border">
|
||||
<td class="features-outline-td-background-style">组织管理(X-PACK)
|
||||
</td>
|
||||
<td class="features-outline-td-background-style">实现多租户管理,权限隔离
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="subscription-level-tr-border">
|
||||
<td class="features-second-td-background-style">多维度授权
|
||||
</td>
|
||||
<td class="features-third-td-background-style">可对用户、用户组或系统角色授权
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="subscription-level-tr-border">
|
||||
<td class="features-second-td-background-style">指令限制
|
||||
</td>
|
||||
<td class="features-third-td-background-style">限制特权指令使用,支持黑白名单
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="subscription-level-tr-border">
|
||||
<td class="features-second-td-background-style">统一文件传输
|
||||
</td>
|
||||
<td class="features-third-td-background-style">SFTP 文件上传/下载
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="subscription-level-tr-border">
|
||||
<td class="features-second-td-background-style">文件管理
|
||||
</td>
|
||||
<td class="features-third-td-background-style">Web SFTP 文件管理
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="subscription-level-tr-border">
|
||||
<td class="features-first-td-background-style" rowspan="6">安全审计 Audit</td>
|
||||
<td class="features-second-td-background-style" rowspan="2">会话管理
|
||||
</td>
|
||||
<td class="features-third-td-background-style">在线会话管理
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="subscription-level-tr-border">
|
||||
<td class="features-third-td-background-style">历史会话管理
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="subscription-level-tr-border">
|
||||
<td class="features-second-td-background-style" rowspan="2">录像管理
|
||||
</td>
|
||||
<td class="features-third-td-background-style">Linux 录像支持
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="subscription-level-tr-border">
|
||||
<td class="features-third-td-background-style">Windows 录像支持
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="subscription-level-tr-border">
|
||||
<td class="features-second-td-background-style">指令审计
|
||||
</td>
|
||||
<td class="features-third-td-background-style">指令记录
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="subscription-level-tr-border">
|
||||
<td class="features-second-td-background-style">文件传输审计
|
||||
</td>
|
||||
<td class="features-third-td-background-style">上传/下载记录审计
|
||||
</td>
|
||||
</tr>
|
||||
- 开源: 零门槛,线上快速获取和安装, 修复版本视情况而定;
|
||||
, 修复版本视情况而定- 分布式: 轻松支持大规模并发访问;
|
||||
- 无插件: 仅需浏览器,极致的 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="8">身份认证<br>Authentication</td>
|
||||
<td rowspan="5">登录认证</td>
|
||||
<td>资源统一登录与认证</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>LDAP/AD 认证</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>RADIUS 认证</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>OpenID 认证(实现单点登录)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>CAS 认证 (实现单点登录)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="2">MFA认证</td>
|
||||
<td>MFA 二次认证(Google Authenticator)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>RADIUS 二次认证</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>登录复核(X-PACK)</td>
|
||||
<td>用户登录行为受管理员的监管与控制</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="11">账号管理<br>Account</td>
|
||||
<td rowspan="2">集中账号</td>
|
||||
<td>管理用户管理</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>系统用户管理</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="4">统一密码</td>
|
||||
<td>资产密码托管</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>自动生成密码</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>自动推送密码</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>密码过期设置</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="2">批量改密(X-PACK)</td>
|
||||
<td>定期批量改密</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>多种密码策略</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>多云纳管(X-PACK)</td>
|
||||
<td>对私有云、公有云资产自动统一纳管</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>收集用户(X-PACK)</td>
|
||||
<td>自定义任务定期收集主机用户</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>密码匣子(X-PACK)</td>
|
||||
<td>统一对资产主机的用户密码进行查看、更新、测试操作</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="15">授权控制<br>Authorization</td>
|
||||
<td>多维授权</td>
|
||||
<td>对用户、用户组、资产、资产节点、应用以及系统用户进行授权</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="4">资产授权</td>
|
||||
<td>资产以树状结构进行展示</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>资产和节点均可灵活授权</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>节点内资产自动继承授权</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>子节点自动继承父节点授权</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="2">应用授权</td>
|
||||
<td>实现更细粒度的应用级授权</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MySQL 数据库应用、RemoteApp 远程应用(X-PACK)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>动作授权</td>
|
||||
<td>实现对授权资产的文件上传、下载以及连接动作的控制</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>时间授权</td>
|
||||
<td>实现对授权资源使用时间段的限制</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>特权指令</td>
|
||||
<td>实现对特权指令的使用(支持黑白名单)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>命令过滤</td>
|
||||
<td>实现对授权系统用户所执行的命令进行控制</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>文件传输</td>
|
||||
<td>SFTP 文件上传/下载</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>文件管理</td>
|
||||
<td>实现 Web SFTP 文件管理</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>工单管理(X-PACK)</td>
|
||||
<td>支持对用户登录请求行为进行控制</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>组织管理(X-PACK)</td>
|
||||
<td>实现多租户管理与权限隔离</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="7">安全审计<br>Audit</td>
|
||||
<td>操作审计</td>
|
||||
<td>用户操作行为审计</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="2">会话审计</td>
|
||||
<td>在线会话内容审计</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>历史会话内容审计</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="2">录像审计</td>
|
||||
<td>支持对 Linux、Windows 等资产操作的录像进行回放审计</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>支持对 RemoteApp(X-PACK)、MySQL 等应用操作的录像进行回放审计</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>指令审计</td>
|
||||
<td>支持对资产和应用等操作的命令进行审计</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>文件传输</td>
|
||||
<td>可对文件的上传、下载记录进行审计</td>
|
||||
</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
|
||||
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
from .remote_app import *
|
||||
from .database_app import *
|
||||
|
||||
20
apps/applications/api/database_app.py
Normal file
20
apps/applications/api/database_app.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# coding: utf-8
|
||||
#
|
||||
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
|
||||
from .. import models
|
||||
from .. import serializers
|
||||
from ..hands import IsOrgAdminOrAppUser
|
||||
|
||||
__all__ = [
|
||||
'DatabaseAppViewSet',
|
||||
]
|
||||
|
||||
|
||||
class DatabaseAppViewSet(OrgBulkModelViewSet):
|
||||
model = models.DatabaseApp
|
||||
filter_fields = ('name',)
|
||||
search_fields = filter_fields
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = serializers.DatabaseAppSerializer
|
||||
@@ -1,10 +1,8 @@
|
||||
# coding: utf-8
|
||||
#
|
||||
|
||||
|
||||
from rest_framework import generics
|
||||
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from orgs.mixins import generics
|
||||
from ..hands import IsOrgAdmin, IsAppUser
|
||||
from ..models import RemoteApp
|
||||
from ..serializers import RemoteAppSerializer, RemoteAppConnectionInfoSerializer
|
||||
@@ -16,14 +14,14 @@ __all__ = [
|
||||
|
||||
|
||||
class RemoteAppViewSet(OrgBulkModelViewSet):
|
||||
filter_fields = ('name',)
|
||||
model = RemoteApp
|
||||
filter_fields = ('name', 'type', 'comment')
|
||||
search_fields = filter_fields
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
queryset = RemoteApp.objects.all()
|
||||
serializer_class = RemoteAppSerializer
|
||||
|
||||
|
||||
class RemoteAppConnectionInfoApi(generics.RetrieveAPIView):
|
||||
queryset = RemoteApp.objects.all()
|
||||
model = RemoteApp
|
||||
permission_classes = (IsAppUser, )
|
||||
serializer_class = RemoteAppConnectionInfoSerializer
|
||||
|
||||
@@ -5,6 +5,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
# RemoteApp
|
||||
|
||||
REMOTE_APP_BOOT_PROGRAM_NAME = '||jmservisor'
|
||||
|
||||
REMOTE_APP_TYPE_CHROME = 'chrome'
|
||||
@@ -12,29 +13,6 @@ REMOTE_APP_TYPE_MYSQL_WORKBENCH = 'mysql_workbench'
|
||||
REMOTE_APP_TYPE_VMWARE_CLIENT = 'vmware_client'
|
||||
REMOTE_APP_TYPE_CUSTOM = 'custom'
|
||||
|
||||
REMOTE_APP_TYPE_CHOICES = (
|
||||
(
|
||||
_('Browser'),
|
||||
(
|
||||
(REMOTE_APP_TYPE_CHROME, 'Chrome'),
|
||||
)
|
||||
),
|
||||
(
|
||||
_('Database tools'),
|
||||
(
|
||||
(REMOTE_APP_TYPE_MYSQL_WORKBENCH, 'MySQL Workbench'),
|
||||
)
|
||||
),
|
||||
(
|
||||
_('Virtualization tools'),
|
||||
(
|
||||
(REMOTE_APP_TYPE_VMWARE_CLIENT, 'vSphere Client'),
|
||||
)
|
||||
),
|
||||
(REMOTE_APP_TYPE_CUSTOM, _('Custom')),
|
||||
|
||||
)
|
||||
|
||||
# Fields attribute write_only default => False
|
||||
|
||||
REMOTE_APP_TYPE_CHROME_FIELDS = [
|
||||
@@ -60,9 +38,26 @@ REMOTE_APP_TYPE_CUSTOM_FIELDS = [
|
||||
{'name': 'custom_password', 'write_only': True}
|
||||
]
|
||||
|
||||
REMOTE_APP_TYPE_MAP_FIELDS = {
|
||||
REMOTE_APP_TYPE_FIELDS_MAP = {
|
||||
REMOTE_APP_TYPE_CHROME: REMOTE_APP_TYPE_CHROME_FIELDS,
|
||||
REMOTE_APP_TYPE_MYSQL_WORKBENCH: REMOTE_APP_TYPE_MYSQL_WORKBENCH_FIELDS,
|
||||
REMOTE_APP_TYPE_VMWARE_CLIENT: REMOTE_APP_TYPE_VMWARE_CLIENT_FIELDS,
|
||||
REMOTE_APP_TYPE_CUSTOM: REMOTE_APP_TYPE_CUSTOM_FIELDS
|
||||
}
|
||||
|
||||
REMOTE_APP_TYPE_CHOICES = (
|
||||
(REMOTE_APP_TYPE_CHROME, 'Chrome'),
|
||||
(REMOTE_APP_TYPE_MYSQL_WORKBENCH, 'MySQL Workbench'),
|
||||
(REMOTE_APP_TYPE_VMWARE_CLIENT, 'vSphere Client'),
|
||||
(REMOTE_APP_TYPE_CUSTOM, _('Custom')),
|
||||
)
|
||||
|
||||
|
||||
# DatabaseApp
|
||||
|
||||
|
||||
DATABASE_APP_TYPE_MYSQL = 'mysql'
|
||||
|
||||
DATABASE_APP_TYPE_CHOICES = (
|
||||
(DATABASE_APP_TYPE_MYSQL, 'MySQL'),
|
||||
)
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
from .remote_app import *
|
||||
@@ -1,123 +0,0 @@
|
||||
# coding: utf-8
|
||||
#
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
from django import forms
|
||||
|
||||
from orgs.mixins.forms import OrgModelForm
|
||||
from assets.models import SystemUser
|
||||
|
||||
from ..models import RemoteApp
|
||||
from .. import const
|
||||
|
||||
|
||||
__all__ = [
|
||||
'RemoteAppCreateUpdateForm',
|
||||
]
|
||||
|
||||
|
||||
class RemoteAppTypeChromeForm(forms.ModelForm):
|
||||
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 RemoteAppTypeMySQLWorkbenchForm(forms.ModelForm):
|
||||
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 RemoteAppTypeVMwareForm(forms.ModelForm):
|
||||
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 RemoteAppTypeCustomForm(forms.ModelForm):
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
class RemoteAppTypeForms(
|
||||
RemoteAppTypeChromeForm,
|
||||
RemoteAppTypeMySQLWorkbenchForm,
|
||||
RemoteAppTypeVMwareForm,
|
||||
RemoteAppTypeCustomForm
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
class RemoteAppCreateUpdateForm(RemoteAppTypeForms, OrgModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
# 过滤RDP资产和系统用户
|
||||
super().__init__(*args, **kwargs)
|
||||
field_asset = self.fields['asset']
|
||||
field_asset.queryset = field_asset.queryset.has_protocol('rdp')
|
||||
|
||||
class Meta:
|
||||
model = RemoteApp
|
||||
fields = [
|
||||
'name', 'asset', 'type', 'path', 'comment'
|
||||
]
|
||||
widgets = {
|
||||
'asset': forms.Select(attrs={
|
||||
'class': 'select2', 'data-placeholder': _('Asset')
|
||||
}),
|
||||
}
|
||||
|
||||
def _clean_params(self):
|
||||
app_type = self.data.get('type')
|
||||
fields = const.REMOTE_APP_TYPE_MAP_FIELDS.get(app_type, [])
|
||||
params = {}
|
||||
for field in fields:
|
||||
name = field['name']
|
||||
value = self.cleaned_data[name]
|
||||
params.update({name: value})
|
||||
return params
|
||||
|
||||
def _save_params(self, instance):
|
||||
params = self._clean_params()
|
||||
instance.params = params
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
def save(self, commit=True):
|
||||
instance = super().save(commit=commit)
|
||||
instance = self._save_params(instance)
|
||||
return instance
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
|
||||
18
apps/applications/migrations/0003_auto_20191210_1659.py
Normal file
18
apps/applications/migrations/0003_auto_20191210_1659.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 2.1.11 on 2019-12-10 08:59
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('applications', '0002_remove_remoteapp_system_user'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='remoteapp',
|
||||
name='type',
|
||||
field=models.CharField(choices=[('chrome', 'Chrome'), ('mysql_workbench', 'MySQL Workbench'), ('vmware_client', 'vSphere Client'), ('custom', 'Custom')], default='chrome', max_length=128, verbose_name='App type'),
|
||||
),
|
||||
]
|
||||
38
apps/applications/migrations/0004_auto_20191218_1705.py
Normal file
38
apps/applications/migrations/0004_auto_20191218_1705.py
Normal file
@@ -0,0 +1,38 @@
|
||||
# Generated by Django 2.1.11 on 2019-12-18 09:05
|
||||
|
||||
from django.db import migrations, models
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('applications', '0003_auto_20191210_1659'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='DatabaseApp',
|
||||
fields=[
|
||||
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
|
||||
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
|
||||
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
|
||||
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=128, verbose_name='Name')),
|
||||
('type', models.CharField(choices=[('mysql', 'MySQL')], default='mysql', max_length=128, verbose_name='Type')),
|
||||
('host', models.CharField(db_index=True, max_length=128, verbose_name='Host')),
|
||||
('port', models.IntegerField(default=3306, verbose_name='Port')),
|
||||
('database', models.CharField(blank=True, db_index=True, max_length=128, null=True, verbose_name='Database')),
|
||||
('comment', models.TextField(blank=True, default='', max_length=128, verbose_name='Comment')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'DatabaseApp',
|
||||
'ordering': ('name',),
|
||||
},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='databaseapp',
|
||||
unique_together={('org_id', 'name')},
|
||||
),
|
||||
]
|
||||
@@ -1 +1,2 @@
|
||||
from .remote_app import *
|
||||
from .database_app import *
|
||||
|
||||
42
apps/applications/models/database_app.py
Normal file
42
apps/applications/models/database_app.py
Normal file
@@ -0,0 +1,42 @@
|
||||
# coding: utf-8
|
||||
#
|
||||
|
||||
import uuid
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orgs.mixins.models import OrgModelMixin
|
||||
from common.mixins import CommonModelMixin
|
||||
from .. import const
|
||||
|
||||
|
||||
__all__ = ['DatabaseApp']
|
||||
|
||||
|
||||
class DatabaseApp(CommonModelMixin, OrgModelMixin):
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
name = models.CharField(max_length=128, verbose_name=_('Name'))
|
||||
type = models.CharField(
|
||||
default=const.DATABASE_APP_TYPE_MYSQL,
|
||||
choices=const.DATABASE_APP_TYPE_CHOICES,
|
||||
max_length=128, verbose_name=_('Type')
|
||||
)
|
||||
host = models.CharField(
|
||||
max_length=128, verbose_name=_('Host'), db_index=True
|
||||
)
|
||||
port = models.IntegerField(default=3306, verbose_name=_('Port'))
|
||||
database = models.CharField(
|
||||
max_length=128, blank=True, null=True, verbose_name=_('Database'),
|
||||
db_index=True
|
||||
)
|
||||
comment = models.TextField(
|
||||
max_length=128, default='', blank=True, verbose_name=_('Comment')
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
unique_together = [('org_id', 'name'), ]
|
||||
verbose_name = _("DatabaseApp")
|
||||
ordering = ('name', )
|
||||
@@ -62,7 +62,7 @@ class RemoteApp(OrgModelMixin):
|
||||
_parameters.append(self.type)
|
||||
path = '\"%s\"' % self.path
|
||||
_parameters.append(path)
|
||||
for field in const.REMOTE_APP_TYPE_MAP_FIELDS[self.type]:
|
||||
for field in const.REMOTE_APP_TYPE_FIELDS_MAP[self.type]:
|
||||
value = self.params.get(field['name'])
|
||||
if value is None:
|
||||
continue
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
from .remote_app import *
|
||||
from .database_app import *
|
||||
|
||||
26
apps/applications/serializers/database_app.py
Normal file
26
apps/applications/serializers/database_app.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# coding: utf-8
|
||||
#
|
||||
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from common.serializers import AdaptedBulkListSerializer
|
||||
|
||||
from .. import models
|
||||
|
||||
__all__ = [
|
||||
'DatabaseAppSerializer',
|
||||
]
|
||||
|
||||
|
||||
class DatabaseAppSerializer(BulkOrgResourceModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.DatabaseApp
|
||||
list_serializer_class = AdaptedBulkListSerializer
|
||||
fields = [
|
||||
'id', 'name', 'type', 'get_type_display', 'host', 'port',
|
||||
'database', 'comment', 'created_by', 'date_created', 'date_updated',
|
||||
]
|
||||
read_only_fields = [
|
||||
'created_by', 'date_created', 'date_updated'
|
||||
'get_type_display',
|
||||
]
|
||||
@@ -1,10 +1,11 @@
|
||||
# coding: utf-8
|
||||
#
|
||||
|
||||
|
||||
import copy
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.serializers import AdaptedBulkListSerializer
|
||||
from common.fields.serializer import CustomMetaDictField
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
|
||||
from .. import const
|
||||
@@ -16,72 +17,54 @@ __all__ = [
|
||||
]
|
||||
|
||||
|
||||
class RemoteAppParamsDictField(serializers.DictField):
|
||||
"""
|
||||
RemoteApp field => params
|
||||
"""
|
||||
@staticmethod
|
||||
def filter_attribute(attribute, instance):
|
||||
"""
|
||||
过滤掉params字段值中write_only特性的key-value值
|
||||
For example, the chrome_password field is not returned when serializing
|
||||
{
|
||||
'chrome_target': 'http://www.jumpserver.org/',
|
||||
'chrome_username': 'admin',
|
||||
'chrome_password': 'admin',
|
||||
}
|
||||
"""
|
||||
for field in const.REMOTE_APP_TYPE_MAP_FIELDS[instance.type]:
|
||||
if field.get('write_only', False):
|
||||
attribute.pop(field['name'], None)
|
||||
return attribute
|
||||
|
||||
def get_attribute(self, instance):
|
||||
"""
|
||||
序列化时调用
|
||||
"""
|
||||
attribute = super().get_attribute(instance)
|
||||
attribute = self.filter_attribute(attribute, instance)
|
||||
return attribute
|
||||
|
||||
@staticmethod
|
||||
def filter_value(dictionary, value):
|
||||
"""
|
||||
过滤掉不属于当前app_type所包含的key-value值
|
||||
"""
|
||||
app_type = dictionary.get('type', const.REMOTE_APP_TYPE_CHROME)
|
||||
fields = const.REMOTE_APP_TYPE_MAP_FIELDS[app_type]
|
||||
fields_names = [field['name'] for field in fields]
|
||||
no_need_keys = [k for k in value.keys() if k not in fields_names]
|
||||
for k in no_need_keys:
|
||||
value.pop(k)
|
||||
return value
|
||||
|
||||
def get_value(self, dictionary):
|
||||
"""
|
||||
反序列化时调用
|
||||
"""
|
||||
value = super().get_value(dictionary)
|
||||
value = self.filter_value(dictionary, value)
|
||||
return value
|
||||
class RemoteAppParamsDictField(CustomMetaDictField):
|
||||
type_fields_map = const.REMOTE_APP_TYPE_FIELDS_MAP
|
||||
default_type = const.REMOTE_APP_TYPE_CHROME
|
||||
convert_key_remove_type_prefix = False
|
||||
convert_key_to_upper = False
|
||||
|
||||
|
||||
class RemoteAppSerializer(BulkOrgResourceModelSerializer):
|
||||
params = RemoteAppParamsDictField()
|
||||
type_fields_map = const.REMOTE_APP_TYPE_FIELDS_MAP
|
||||
|
||||
class Meta:
|
||||
model = RemoteApp
|
||||
list_serializer_class = AdaptedBulkListSerializer
|
||||
fields = [
|
||||
'id', 'name', 'asset', 'type', 'path', 'params',
|
||||
'comment', 'created_by', 'date_created', 'asset_info',
|
||||
'get_type_display',
|
||||
'id', 'name', 'asset', 'asset_info', 'type', 'get_type_display',
|
||||
'path', 'params', 'date_created', 'created_by', 'comment',
|
||||
]
|
||||
read_only_fields = [
|
||||
'created_by', 'date_created', 'asset_info',
|
||||
'get_type_display'
|
||||
]
|
||||
|
||||
def process_params(self, instance, validated_data):
|
||||
new_params = copy.deepcopy(validated_data.get('params', {}))
|
||||
tp = validated_data.get('type', '')
|
||||
|
||||
if tp != instance.type:
|
||||
return new_params
|
||||
|
||||
old_params = instance.params
|
||||
fields = self.type_fields_map.get(instance.type, [])
|
||||
for field in fields:
|
||||
if not field.get('write_only', False):
|
||||
continue
|
||||
field_name = field['name']
|
||||
new_value = new_params.get(field_name, '')
|
||||
old_value = old_params.get(field_name, '')
|
||||
field_value = new_value if new_value else old_value
|
||||
new_params[field_name] = field_value
|
||||
|
||||
return new_params
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
params = self.process_params(instance, validated_data)
|
||||
validated_data['params'] = params
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
|
||||
class RemoteAppConnectionInfoSerializer(serializers.ModelSerializer):
|
||||
parameter_remote_app = serializers.SerializerMethodField()
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
{% extends '_base_create_update.html' %}
|
||||
{% load static %}
|
||||
{% load bootstrap3 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block form %}
|
||||
<form id="appForm" method="post" class="form-horizontal">
|
||||
{% if form.non_field_errors %}
|
||||
<div class="alert alert-danger">
|
||||
{{ form.non_field_errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% csrf_token %}
|
||||
{% bootstrap_field form.name layout="horizontal" %}
|
||||
{% bootstrap_field form.asset layout="horizontal" %}
|
||||
{% bootstrap_field form.type layout="horizontal" %}
|
||||
{% bootstrap_field form.path layout="horizontal" %}
|
||||
|
||||
<div class="hr-line-dashed"></div>
|
||||
|
||||
{# chrome #}
|
||||
<div class="chrome-fields">
|
||||
{% bootstrap_field form.chrome_target layout="horizontal" %}
|
||||
{% bootstrap_field form.chrome_username layout="horizontal" %}
|
||||
{% bootstrap_field form.chrome_password layout="horizontal" %}
|
||||
</div>
|
||||
|
||||
{# mysql workbench #}
|
||||
<div class="mysql_workbench-fields">
|
||||
{% bootstrap_field form.mysql_workbench_ip layout="horizontal" %}
|
||||
{% bootstrap_field form.mysql_workbench_name layout="horizontal" %}
|
||||
{% bootstrap_field form.mysql_workbench_username layout="horizontal" %}
|
||||
{% bootstrap_field form.mysql_workbench_password layout="horizontal" %}
|
||||
</div>
|
||||
|
||||
{# vmware #}
|
||||
<div class="vmware_client-fields">
|
||||
{% bootstrap_field form.vmware_target layout="horizontal" %}
|
||||
{% bootstrap_field form.vmware_username layout="horizontal" %}
|
||||
{% bootstrap_field form.vmware_password layout="horizontal" %}
|
||||
</div>
|
||||
|
||||
{# custom #}
|
||||
<div class="custom-fields">
|
||||
{% bootstrap_field form.custom_cmdline layout="horizontal" %}
|
||||
{% bootstrap_field form.custom_target layout="horizontal" %}
|
||||
{% bootstrap_field form.custom_username layout="horizontal" %}
|
||||
{% bootstrap_field form.custom_password layout="horizontal" %}
|
||||
</div>
|
||||
|
||||
{% bootstrap_field form.comment 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 }}';
|
||||
var app_path_id = '#' + '{{ form.path.id_for_label }}';
|
||||
var all_type_fields = [
|
||||
'.chrome-fields',
|
||||
'.mysql_workbench-fields',
|
||||
'.vmware_client-fields',
|
||||
'.custom-fields'
|
||||
];
|
||||
var app_type_map_default_fields_value = {
|
||||
'chrome': {
|
||||
'app_path': 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe'
|
||||
},
|
||||
'mysql_workbench': {
|
||||
'app_path': 'C:\\Program Files\\MySQL\\MySQL Workbench 8.0 CE\\MySQLWorkbench.exe'
|
||||
},
|
||||
'vmware_client': {
|
||||
'app_path': 'C:\\Program Files (x86)\\VMware\\Infrastructure\\Virtual Infrastructure Client\\Launcher\\VpxClient.exe'
|
||||
},
|
||||
'custom': {'app_path': ''}
|
||||
};
|
||||
function getAppType(){
|
||||
return $(app_type_id+ " option:selected").val();
|
||||
}
|
||||
function initialDefaultValue(){
|
||||
var app_type = getAppType();
|
||||
var app_path = $(app_path_id).val();
|
||||
if(app_path){
|
||||
app_type_map_default_fields_value[app_type]['app_path'] = app_path
|
||||
}
|
||||
}
|
||||
function setDefaultValue(){
|
||||
// 设置类型相关字段的默认值
|
||||
var app_type = getAppType();
|
||||
var app_path = app_type_map_default_fields_value[app_type]['app_path'];
|
||||
$(app_path_id).val(app_path)
|
||||
}
|
||||
function hiddenFields(){
|
||||
var app_type = getAppType();
|
||||
$.each(all_type_fields, function(index, value){
|
||||
$(value).addClass('hidden')
|
||||
});
|
||||
$('.' + app_type + '-fields').removeClass('hidden');
|
||||
}
|
||||
function constructParams(data) {
|
||||
var typeList = ['chrome', 'mysql_workbench', 'vmware_client', 'custom'];
|
||||
var params = {};
|
||||
$.each(typeList, function(index, value){
|
||||
if (data.type === value){
|
||||
for (var k in data){
|
||||
if (k.startsWith(value)){
|
||||
params[k] = data[k]
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return params;
|
||||
}
|
||||
$(document).ready(function () {
|
||||
$('.select2').select2({
|
||||
closeOnSelect: true
|
||||
});
|
||||
initialDefaultValue();
|
||||
hiddenFields();
|
||||
setDefaultValue();
|
||||
})
|
||||
.on('change', app_type_id, function(){
|
||||
hiddenFields();
|
||||
setDefaultValue();
|
||||
})
|
||||
.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 type == "update" %}
|
||||
the_url = '{% url "api-applications:remote-app-detail" object.id %}';
|
||||
method = "PUT";
|
||||
{% endif %}
|
||||
var form = $("form");
|
||||
var data = form.serializeObject();
|
||||
data["params"] = constructParams(data);
|
||||
var props = {
|
||||
url: the_url,
|
||||
data: data,
|
||||
method: method,
|
||||
form: form,
|
||||
redirect_to: redirect_to
|
||||
};
|
||||
formSubmit(props);
|
||||
})
|
||||
;
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,105 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block custom_head_css_js %}
|
||||
<link href="{% static 'css/plugins/select2/select2.min.css' %}" rel="stylesheet">
|
||||
<script src="{% static 'js/plugins/select2/select2.full.min.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% 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,87 +0,0 @@
|
||||
{% extends '_base_list.html' %}
|
||||
{% load i18n static %}
|
||||
{% block help_message %}
|
||||
<div class="alert alert-info 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>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block table_search %}{% endblock %}
|
||||
{% block table_container %}
|
||||
<div class="uc pull-left m-r-5">
|
||||
<a href="{% url 'applications:remote-app-create' %}" class="btn btn-sm btn-primary"> {% trans "Create RemoteApp" %} </a>
|
||||
</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}
|
||||
],
|
||||
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,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 %}
|
||||
@@ -11,10 +11,12 @@ app_name = 'applications'
|
||||
|
||||
router = BulkRouter()
|
||||
router.register(r'remote-apps', api.RemoteAppViewSet, 'remote-app')
|
||||
router.register(r'database-apps', api.DatabaseAppViewSet, 'database-app')
|
||||
|
||||
urlpatterns = [
|
||||
path('remote-apps/<uuid:pk>/connection-info/', api.RemoteAppConnectionInfoApi.as_view(), name='remote-app-connection-info'),
|
||||
]
|
||||
|
||||
old_version_urlpatterns = [
|
||||
re_path('(?P<resource>remote-app)/.*', capi.redirect_plural_name_api)
|
||||
]
|
||||
|
||||
@@ -1,16 +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')
|
||||
|
||||
]
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
from .remote_app import *
|
||||
@@ -1,105 +0,0 @@
|
||||
# coding: utf-8
|
||||
#
|
||||
|
||||
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 django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.urls import reverse_lazy
|
||||
|
||||
|
||||
from common.permissions import PermissionsMixin, IsOrgAdmin, IsValidUser
|
||||
from common.const import create_success_msg, update_success_msg
|
||||
|
||||
from ..models import RemoteApp
|
||||
from .. import forms
|
||||
|
||||
|
||||
__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'),
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class RemoteAppCreateView(PermissionsMixin, SuccessMessageMixin, CreateView):
|
||||
template_name = 'applications/remote_app_create_update.html'
|
||||
model = RemoteApp
|
||||
form_class = forms.RemoteAppCreateUpdateForm
|
||||
success_url = reverse_lazy('applications:remote-app-list')
|
||||
permission_classes = [IsOrgAdmin]
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'app': _('Applications'),
|
||||
'action': _('Create RemoteApp'),
|
||||
'type': 'create'
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
def get_success_message(self, cleaned_data):
|
||||
return create_success_msg % ({'name': cleaned_data['name']})
|
||||
|
||||
|
||||
class RemoteAppUpdateView(PermissionsMixin, SuccessMessageMixin, UpdateView):
|
||||
template_name = 'applications/remote_app_create_update.html'
|
||||
model = RemoteApp
|
||||
form_class = forms.RemoteAppCreateUpdateForm
|
||||
success_url = reverse_lazy('applications:remote-app-list')
|
||||
permission_classes = [IsOrgAdmin]
|
||||
|
||||
def get_initial(self):
|
||||
return {k: v for k, v in self.object.params.items()}
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'app': _('Applications'),
|
||||
'action': _('Update RemoteApp'),
|
||||
'type': 'update'
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
def get_success_message(self, cleaned_data):
|
||||
return update_success_msg % ({'name': cleaned_data['name']})
|
||||
|
||||
|
||||
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)
|
||||
@@ -2,8 +2,10 @@ from .admin_user import *
|
||||
from .asset import *
|
||||
from .label import *
|
||||
from .system_user import *
|
||||
from .system_user_relation import *
|
||||
from .node import *
|
||||
from .domain import *
|
||||
from .cmd_filter import *
|
||||
from .asset_user import *
|
||||
from .gathered_user import *
|
||||
from .favorite_asset import *
|
||||
|
||||
@@ -1,25 +1,14 @@
|
||||
# ~*~ 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
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework import generics
|
||||
from django.utils.translation import ugettext as _
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from orgs.mixins import generics
|
||||
|
||||
from common.mixins import CommonApiMixin
|
||||
from common.utils import get_logger
|
||||
from ..hands import IsOrgAdmin
|
||||
from ..models import AdminUser, Asset
|
||||
@@ -39,22 +28,34 @@ class AdminUserViewSet(OrgBulkModelViewSet):
|
||||
"""
|
||||
Admin user api set, for add,delete,update,list,retrieve resource
|
||||
"""
|
||||
|
||||
model = AdminUser
|
||||
filter_fields = ("name", "username")
|
||||
search_fields = filter_fields
|
||||
queryset = AdminUser.objects.all()
|
||||
serializer_class = serializers.AdminUserSerializer
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
queryset = queryset.annotate(assets_amount=Count('assets'))
|
||||
return queryset
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
has_related_asset = instance.assets.exists()
|
||||
if has_related_asset:
|
||||
data = {'msg': _('Deleted failed, There are related assets')}
|
||||
return Response(data=data, status=status.HTTP_400_BAD_REQUEST)
|
||||
return super().destroy(request, *args, **kwargs)
|
||||
|
||||
|
||||
class AdminUserAuthApi(generics.UpdateAPIView):
|
||||
queryset = AdminUser.objects.all()
|
||||
model = AdminUser
|
||||
serializer_class = serializers.AdminUserAuthSerializer
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
|
||||
|
||||
class ReplaceNodesAdminUserApi(generics.UpdateAPIView):
|
||||
queryset = AdminUser.objects.all()
|
||||
model = AdminUser
|
||||
serializer_class = serializers.ReplaceNodeAdminUserSerializer
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
|
||||
@@ -79,7 +80,7 @@ class AdminUserTestConnectiveApi(generics.RetrieveAPIView):
|
||||
"""
|
||||
Test asset admin user assets_connectivity
|
||||
"""
|
||||
queryset = AdminUser.objects.all()
|
||||
model = AdminUser
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.TaskIDSerializer
|
||||
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
import random
|
||||
|
||||
from rest_framework import generics
|
||||
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
|
||||
|
||||
from common.utils import get_logger, get_object_or_none
|
||||
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser
|
||||
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, IsSuperUser
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from ..models import Asset, AdminUser, Node
|
||||
from orgs.mixins import generics
|
||||
from ..models import Asset, Node, Platform
|
||||
from .. import serializers
|
||||
from ..tasks import update_asset_hardware_info_manual, \
|
||||
test_asset_connectivity_manual
|
||||
from ..tasks import (
|
||||
update_asset_hardware_info_manual, test_asset_connectivity_manual
|
||||
)
|
||||
from ..filters import AssetByNodeFilterBackend, LabelFilterBackend
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
__all__ = [
|
||||
'AssetViewSet',
|
||||
'AssetRefreshHardwareApi', 'AssetAdminUserTestApi',
|
||||
'AssetGatewayApi',
|
||||
'AssetViewSet', 'AssetPlatformRetrieveApi',
|
||||
'AssetGatewayListApi', 'AssetPlatformViewSet',
|
||||
'AssetTaskCreateApi',
|
||||
]
|
||||
|
||||
|
||||
@@ -29,11 +29,17 @@ class AssetViewSet(OrgBulkModelViewSet):
|
||||
"""
|
||||
API endpoint that allows Asset to be viewed or edited.
|
||||
"""
|
||||
filter_fields = ("hostname", "ip", "systemuser__id", "admin_user__id")
|
||||
model = Asset
|
||||
filter_fields = (
|
||||
"hostname", "ip", "systemuser__id", "admin_user__id", "platform__base",
|
||||
"is_active"
|
||||
)
|
||||
search_fields = ("hostname", "ip")
|
||||
ordering_fields = ("hostname", "ip", "port", "cpu_cores")
|
||||
queryset = Asset.objects.all()
|
||||
serializer_class = serializers.AssetSerializer
|
||||
serializer_classes = {
|
||||
'default': serializers.AssetSerializer,
|
||||
'display': serializers.AssetDisplaySerializer,
|
||||
}
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
extra_filter_backends = [AssetByNodeFilterBackend, LabelFilterBackend]
|
||||
|
||||
@@ -53,49 +59,68 @@ class AssetViewSet(OrgBulkModelViewSet):
|
||||
self.set_assets_node(assets)
|
||||
|
||||
|
||||
class AssetRefreshHardwareApi(generics.RetrieveAPIView):
|
||||
"""
|
||||
Refresh asset hardware info
|
||||
"""
|
||||
queryset = Asset.objects.all()
|
||||
serializer_class = serializers.AssetSerializer
|
||||
class AssetPlatformRetrieveApi(RetrieveAPIView):
|
||||
queryset = Platform.objects.all()
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = serializers.PlatformSerializer
|
||||
|
||||
def get_object(self):
|
||||
asset_pk = self.kwargs.get('pk')
|
||||
asset = get_object_or_404(Asset, pk=asset_pk)
|
||||
return asset.platform
|
||||
|
||||
|
||||
class AssetPlatformViewSet(ModelViewSet):
|
||||
queryset = Platform.objects.all()
|
||||
permission_classes = (IsSuperUser,)
|
||||
serializer_class = serializers.PlatformSerializer
|
||||
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:
|
||||
self.permission_denied(
|
||||
request, message={"detail": "Internal platform"}
|
||||
)
|
||||
return super().check_object_permissions(request, obj)
|
||||
|
||||
|
||||
class AssetTaskCreateApi(generics.CreateAPIView):
|
||||
model = Asset
|
||||
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
|
||||
"""
|
||||
queryset = Asset.objects.all()
|
||||
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):
|
||||
queryset = Asset.objects.all()
|
||||
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,25 +1,24 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
import coreapi
|
||||
from django.conf import settings
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import generics
|
||||
from rest_framework import filters
|
||||
from rest_framework import generics, filters
|
||||
from rest_framework_bulk import BulkModelViewSet
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.http import Http404
|
||||
|
||||
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',
|
||||
]
|
||||
|
||||
|
||||
@@ -33,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):
|
||||
@@ -44,130 +50,108 @@ 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']
|
||||
permission_classes = [IsOrgAdminOrAppUser, NeedMFAVerify]
|
||||
class AssetUserAuthInfoViewSet(AssetUserViewSet):
|
||||
serializer_classes = {"default": serializers.AssetUserAuthInfoSerializer}
|
||||
http_method_names = ['get', 'post']
|
||||
permission_classes = [IsOrgAdminOrAppUser]
|
||||
|
||||
def get_permissions(self):
|
||||
if settings.SECURITY_VIEW_AUTH_NEED_MFA:
|
||||
self.permission_classes = [IsOrgAdminOrAppUser, NeedMFAVerify]
|
||||
return super().get_permissions()
|
||||
|
||||
|
||||
class AssetUserAuthInfoApi(generics.RetrieveAPIView):
|
||||
serializer_class = serializers.AssetUserAuthInfoSerializer
|
||||
permission_classes = [IsOrgAdminOrAppUser, NeedMFAVerify]
|
||||
|
||||
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
|
||||
|
||||
@@ -13,14 +13,15 @@ __all__ = ['CommandFilterViewSet', 'CommandFilterRuleViewSet']
|
||||
|
||||
|
||||
class CommandFilterViewSet(OrgBulkModelViewSet):
|
||||
model = CommandFilter
|
||||
filter_fields = ("name",)
|
||||
search_fields = filter_fields
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
queryset = CommandFilter.objects.all()
|
||||
serializer_class = serializers.CommandFilterSerializer
|
||||
|
||||
|
||||
class CommandFilterRuleViewSet(OrgBulkModelViewSet):
|
||||
model = CommandFilterRule
|
||||
filter_fields = ("content",)
|
||||
search_fields = filter_fields
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
|
||||
@@ -15,7 +15,9 @@ __all__ = ['DomainViewSet', 'GatewayViewSet', "GatewayTestConnectionApi"]
|
||||
|
||||
|
||||
class DomainViewSet(OrgBulkModelViewSet):
|
||||
queryset = Domain.objects.all()
|
||||
model = Domain
|
||||
filter_fields = ("name", )
|
||||
search_fields = filter_fields
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = serializers.DomainSerializer
|
||||
|
||||
@@ -26,16 +28,15 @@ class DomainViewSet(OrgBulkModelViewSet):
|
||||
|
||||
|
||||
class GatewayViewSet(OrgBulkModelViewSet):
|
||||
model = Gateway
|
||||
filter_fields = ("domain__name", "name", "username", "ip", "domain")
|
||||
search_fields = filter_fields
|
||||
queryset = Gateway.objects.all()
|
||||
search_fields = ("domain__name", "name", "username", "ip")
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.GatewaySerializer
|
||||
|
||||
|
||||
class GatewayTestConnectionApi(SingleObjectMixin, APIView):
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
model = Gateway
|
||||
object = None
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
@@ -45,4 +46,4 @@ class GatewayTestConnectionApi(SingleObjectMixin, APIView):
|
||||
if ok:
|
||||
return Response("ok")
|
||||
else:
|
||||
return Response({"failed": e}, status=404)
|
||||
return Response({"error": e}, status=400)
|
||||
|
||||
27
apps/assets/api/favorite_asset.py
Normal file
27
apps/assets/api/favorite_asset.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from rest_framework_bulk import BulkModelViewSet
|
||||
|
||||
from common.permissions import IsValidUser
|
||||
from orgs.utils import tmp_to_root_org
|
||||
from ..models import FavoriteAsset
|
||||
from ..serializers import FavoriteAssetSerializer
|
||||
|
||||
__all__ = ['FavoriteAssetViewSet']
|
||||
|
||||
|
||||
class FavoriteAssetViewSet(BulkModelViewSet):
|
||||
serializer_class = FavoriteAssetSerializer
|
||||
permission_classes = (IsValidUser,)
|
||||
filter_fields = ['asset']
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
with tmp_to_root_org():
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = FavoriteAsset.objects.filter(user=self.request.user)
|
||||
return queryset
|
||||
|
||||
def allow_bulk_destroy(self, qs, filtered):
|
||||
return filtered.count() == 1
|
||||
@@ -13,12 +13,10 @@ __all__ = ['GatheredUserViewSet']
|
||||
|
||||
|
||||
class GatheredUserViewSet(OrgModelViewSet):
|
||||
queryset = GatheredUser.objects.all()
|
||||
model = GatheredUser
|
||||
serializer_class = GatheredUserSerializer
|
||||
permission_classes = [IsOrgAdmin]
|
||||
extra_filter_backends = [AssetRelatedByNodeFilterBackend]
|
||||
|
||||
filter_fields = ['asset', 'username', 'present']
|
||||
filter_fields = ['asset', 'username', 'present', 'asset__ip', 'asset__hostname']
|
||||
search_fields = ['username', 'asset__ip', 'asset__hostname']
|
||||
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ __all__ = ['LabelViewSet']
|
||||
|
||||
|
||||
class LabelViewSet(OrgBulkModelViewSet):
|
||||
model = Label
|
||||
filter_fields = ("name", "value")
|
||||
search_fields = filter_fields
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
|
||||
@@ -1,31 +1,22 @@
|
||||
# ~*~ 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 rest_framework import generics
|
||||
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
|
||||
from orgs.mixins.api import OrgModelViewSet
|
||||
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
|
||||
from ..tasks import (
|
||||
update_node_assets_hardware_info_manual,
|
||||
test_node_assets_connectivity_manual,
|
||||
)
|
||||
from .. import serializers
|
||||
|
||||
|
||||
@@ -33,16 +24,16 @@ logger = get_logger(__file__)
|
||||
__all__ = [
|
||||
'NodeViewSet', 'NodeChildrenApi', 'NodeAssetsApi',
|
||||
'NodeAddAssetsApi', 'NodeRemoveAssetsApi', 'NodeReplaceAssetsApi',
|
||||
'NodeAddChildrenApi', 'RefreshNodeHardwareInfoApi',
|
||||
'TestNodeConnectiveApi', 'NodeListAsTreeApi',
|
||||
'NodeChildrenAsTreeApi', 'RefreshNodesCacheApi',
|
||||
'NodeAddChildrenApi', 'NodeListAsTreeApi',
|
||||
'NodeChildrenAsTreeApi',
|
||||
'NodeTaskCreateApi',
|
||||
]
|
||||
|
||||
|
||||
class NodeViewSet(OrgModelViewSet):
|
||||
model = Node
|
||||
filter_fields = ('value', 'key', 'id')
|
||||
search_fields = ('value', )
|
||||
queryset = Node.objects.all()
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.NodeSerializer
|
||||
|
||||
@@ -59,6 +50,13 @@ class NodeViewSet(OrgModelViewSet):
|
||||
raise ValidationError({"error": msg})
|
||||
return super().perform_update(serializer)
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
node = self.get_object()
|
||||
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)
|
||||
|
||||
|
||||
class NodeListAsTreeApi(generics.ListAPIView):
|
||||
"""
|
||||
@@ -72,6 +70,7 @@ class NodeListAsTreeApi(generics.ListAPIView):
|
||||
}
|
||||
]
|
||||
"""
|
||||
model = Node
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = TreeNodeSerializer
|
||||
|
||||
@@ -80,10 +79,6 @@ class NodeListAsTreeApi(generics.ListAPIView):
|
||||
queryset = [node.as_tree_node() for node in queryset]
|
||||
return queryset
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = Node.objects.all()
|
||||
return queryset
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
queryset = super().filter_queryset(queryset)
|
||||
queryset = self.to_tree_queryset(queryset)
|
||||
@@ -91,7 +86,6 @@ class NodeListAsTreeApi(generics.ListAPIView):
|
||||
|
||||
|
||||
class NodeChildrenApi(generics.ListCreateAPIView):
|
||||
queryset = Node.objects.all()
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.NodeSerializer
|
||||
instance = None
|
||||
@@ -155,6 +149,7 @@ class NodeChildrenAsTreeApi(NodeChildrenApi):
|
||||
]
|
||||
|
||||
"""
|
||||
model = Node
|
||||
serializer_class = TreeNodeSerializer
|
||||
http_method_names = ['get']
|
||||
|
||||
@@ -170,7 +165,7 @@ class NodeChildrenAsTreeApi(NodeChildrenApi):
|
||||
if not include_assets:
|
||||
return queryset
|
||||
assets = self.instance.get_assets().only(
|
||||
"id", "hostname", "ip", 'platform', "os",
|
||||
"id", "hostname", "ip", "os",
|
||||
"org_id", "protocols",
|
||||
)
|
||||
for asset in assets:
|
||||
@@ -197,7 +192,7 @@ class NodeAssetsApi(generics.ListAPIView):
|
||||
|
||||
|
||||
class NodeAddChildrenApi(generics.UpdateAPIView):
|
||||
queryset = Node.objects.all()
|
||||
model = Node
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.NodeAddChildrenSerializer
|
||||
instance = None
|
||||
@@ -214,8 +209,8 @@ class NodeAddChildrenApi(generics.UpdateAPIView):
|
||||
|
||||
|
||||
class NodeAddAssetsApi(generics.UpdateAPIView):
|
||||
model = Node
|
||||
serializer_class = serializers.NodeAssetsSerializer
|
||||
queryset = Node.objects.all()
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
instance = None
|
||||
|
||||
@@ -226,8 +221,8 @@ class NodeAddAssetsApi(generics.UpdateAPIView):
|
||||
|
||||
|
||||
class NodeRemoveAssetsApi(generics.UpdateAPIView):
|
||||
model = Node
|
||||
serializer_class = serializers.NodeAssetsSerializer
|
||||
queryset = Node.objects.all()
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
instance = None
|
||||
|
||||
@@ -242,8 +237,8 @@ class NodeRemoveAssetsApi(generics.UpdateAPIView):
|
||||
|
||||
|
||||
class NodeReplaceAssetsApi(generics.UpdateAPIView):
|
||||
model = Node
|
||||
serializer_class = serializers.NodeAssetsSerializer
|
||||
queryset = Node.objects.all()
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
instance = None
|
||||
|
||||
@@ -254,41 +249,41 @@ class NodeReplaceAssetsApi(generics.UpdateAPIView):
|
||||
asset.nodes.set([instance])
|
||||
|
||||
|
||||
class RefreshNodeHardwareInfoApi(APIView):
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
class NodeTaskCreateApi(generics.CreateAPIView):
|
||||
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 = _("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})
|
||||
|
||||
|
||||
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):
|
||||
serializer_class = serializers.NodeTaskSerializer
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
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)
|
||||
|
||||
@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,41 +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 import generics
|
||||
from rest_framework.response import Response
|
||||
|
||||
from common.serializers import CeleryTaskSerializer
|
||||
from common.utils import get_logger
|
||||
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser
|
||||
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',
|
||||
]
|
||||
|
||||
|
||||
@@ -43,24 +27,24 @@ class SystemUserViewSet(OrgBulkModelViewSet):
|
||||
"""
|
||||
System user api set, for add,delete,update,list,retrieve resource
|
||||
"""
|
||||
model = SystemUser
|
||||
filter_fields = ("name", "username")
|
||||
search_fields = filter_fields
|
||||
queryset = SystemUser.objects.all()
|
||||
serializer_class = serializers.SystemUserSerializer
|
||||
serializer_classes = {
|
||||
'default': serializers.SystemUserSerializer,
|
||||
'list': serializers.SystemUserListSerializer,
|
||||
}
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset().all()
|
||||
return queryset
|
||||
|
||||
|
||||
class SystemUserAuthInfoApi(generics.RetrieveUpdateDestroyAPIView):
|
||||
"""
|
||||
Get system user auth info
|
||||
"""
|
||||
queryset = SystemUser.objects.all()
|
||||
model = SystemUser
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = serializers.SystemUserAuthSerializer
|
||||
serializer_class = SystemUserWithAuthInfoSerializer
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
@@ -72,89 +56,62 @@ class SystemUserAssetAuthInfoApi(generics.RetrieveAPIView):
|
||||
"""
|
||||
Get system user with asset auth info
|
||||
"""
|
||||
queryset = SystemUser.objects.all()
|
||||
model = SystemUser
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = serializers.SystemUserAuthSerializer
|
||||
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
|
||||
"""
|
||||
queryset = SystemUser.objects.all()
|
||||
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
|
||||
"""
|
||||
queryset = SystemUser.objects.all()
|
||||
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):
|
||||
queryset = SystemUser.objects.all()
|
||||
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):
|
||||
queryset = SystemUser.objects.all()
|
||||
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):
|
||||
|
||||
136
apps/assets/api/system_user_relation.py
Normal file
136
apps/assets/api/system_user_relation.py
Normal file
@@ -0,0 +1,136 @@
|
||||
# -*- 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
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from orgs.utils import current_org
|
||||
from .. import models, serializers
|
||||
|
||||
__all__ = [
|
||||
'SystemUserAssetRelationViewSet', 'SystemUserNodeRelationViewSet',
|
||||
'SystemUserUserRelationViewSet',
|
||||
]
|
||||
|
||||
|
||||
class RelationMixin:
|
||||
def get_queryset(self):
|
||||
queryset = self.model.objects.all()
|
||||
org_id = current_org.org_id()
|
||||
if org_id is not None:
|
||||
queryset = queryset.filter(systemuser__org_id=org_id)
|
||||
queryset = queryset.annotate(systemuser_display=Concat(
|
||||
F('systemuser__name'), Value('('), F('systemuser__username'),
|
||||
Value(')')
|
||||
))
|
||||
return queryset
|
||||
|
||||
def send_post_add_signal(self, instance):
|
||||
if not isinstance(instance, list):
|
||||
instance = [instance]
|
||||
|
||||
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,)
|
||||
filter_fields = [
|
||||
'id', 'asset', 'systemuser',
|
||||
]
|
||||
search_fields = [
|
||||
"id", "asset__hostname", "asset__ip",
|
||||
"systemuser__name", "systemuser__username"
|
||||
]
|
||||
|
||||
def get_objects_attr(self):
|
||||
return models.Asset, 'asset'
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
queryset = queryset.annotate(
|
||||
asset_display=Concat(
|
||||
F('asset__hostname'), Value('('),
|
||||
F('asset__ip'), Value(')')
|
||||
)
|
||||
)
|
||||
return queryset
|
||||
|
||||
|
||||
class SystemUserNodeRelationViewSet(BaseRelationViewSet):
|
||||
serializer_class = serializers.SystemUserNodeRelationSerializer
|
||||
model = models.SystemUser.nodes.through
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
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,91 +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:
|
||||
in_kwargs[k] = v
|
||||
for k in in_kwargs:
|
||||
kwargs.pop(k)
|
||||
@abstractmethod
|
||||
def get_queryset(self):
|
||||
pass
|
||||
|
||||
if len(in_kwargs) == 0:
|
||||
return self
|
||||
for i in self:
|
||||
matched = True
|
||||
for k, v in in_kwargs.items():
|
||||
key = k.split('__')[0]
|
||||
attr = getattr(i, key, 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 not in v:
|
||||
matched = False
|
||||
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)
|
||||
if isinstance(value, uuid.UUID):
|
||||
value = str(value)
|
||||
real.append(value)
|
||||
return wanted == real
|
||||
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" and self._prefer != name:
|
||||
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
|
||||
|
||||
@@ -53,17 +53,19 @@ class AssetByNodeFilterBackend(filters.BaseFilterBackend):
|
||||
return queryset
|
||||
|
||||
if node is None:
|
||||
return queryset.none()
|
||||
return queryset
|
||||
query_all = self.is_query_all(request)
|
||||
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]
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from .asset import *
|
||||
from .label import *
|
||||
from .user import *
|
||||
from .domain import *
|
||||
from .cmd_filter import *
|
||||
@@ -1,186 +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, Node
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
__all__ = [
|
||||
'AssetCreateForm', 'AssetUpdateForm', 'AssetBulkUpdateForm', 'ProtocolForm',
|
||||
]
|
||||
|
||||
HELP_TEXTS_ASSET_HOSTNAME = _(
|
||||
'Only Numbers、letters、 chinese and characters ( {} ) are allowed'
|
||||
).format(" ".join(['.', '_', '@']))
|
||||
|
||||
|
||||
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 AssetCreateForm(OrgModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if self.data:
|
||||
return
|
||||
nodes_field = self.fields['nodes']
|
||||
if self.instance:
|
||||
nodes_field.choices = [(n.id, n.full_value) for n in
|
||||
self.instance.nodes.all()]
|
||||
else:
|
||||
nodes_field.choices = []
|
||||
|
||||
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',
|
||||
]
|
||||
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')
|
||||
}),
|
||||
}
|
||||
labels = {
|
||||
'nodes': _("Node"),
|
||||
}
|
||||
help_texts = {
|
||||
'hostname': HELP_TEXTS_ASSET_HOSTNAME,
|
||||
'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 AssetUpdateForm(OrgModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if self.data:
|
||||
return
|
||||
nodes_field = self.fields['nodes']
|
||||
if self.instance:
|
||||
nodes_field.choices = ((n.id, n.full_value) for n in
|
||||
self.instance.nodes.all())
|
||||
else:
|
||||
nodes_field.choices = []
|
||||
|
||||
class Meta:
|
||||
model = Asset
|
||||
fields = [
|
||||
'hostname', 'ip', 'protocols', 'nodes', 'is_active', 'platform',
|
||||
'public_ip', 'number', 'comment', 'admin_user', 'labels',
|
||||
'domain',
|
||||
]
|
||||
widgets = {
|
||||
'nodes': forms.SelectMultiple(attrs={
|
||||
'class': 'nodes-select2', 'data-placeholder': _('Node')
|
||||
}),
|
||||
'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')
|
||||
}),
|
||||
}
|
||||
labels = {
|
||||
'nodes': _("Node"),
|
||||
}
|
||||
help_texts = {
|
||||
'hostname': HELP_TEXTS_ASSET_HOSTNAME,
|
||||
'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.all(),
|
||||
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)
|
||||
# 重写其他字段为不再required
|
||||
for name, field in self.fields.items():
|
||||
if name != 'assets':
|
||||
field.required = False
|
||||
|
||||
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,75 +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.all(), 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):
|
||||
if kwargs.get('instance', None):
|
||||
initial = kwargs.get('initial', {})
|
||||
initial['assets'] = kwargs['instance'].assets.all()
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# 前端渲染优化, 防止过多资产
|
||||
assets_field = self.fields.get('assets')
|
||||
if not self.data:
|
||||
instance = kwargs.get('instance')
|
||||
if instance:
|
||||
assets_field.queryset = instance.assets.all()
|
||||
else:
|
||||
assets_field.queryset = Asset.objects.none()
|
||||
|
||||
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,42 +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.all(), 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):
|
||||
if kwargs.get('instance', None):
|
||||
initial = kwargs.get('initial', {})
|
||||
initial['assets'] = kwargs['instance'].assets.all()
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# 前端渲染优化, 防止过多资产
|
||||
assets_field = self.fields.get('assets')
|
||||
if not self.data:
|
||||
instance = kwargs.get('instance')
|
||||
if instance:
|
||||
assets_field.queryset = instance.assets.all()
|
||||
else:
|
||||
assets_field.queryset = Asset.objects.none()
|
||||
|
||||
def save(self, commit=True):
|
||||
label = super().save(commit=commit)
|
||||
assets = self.cleaned_data['assets']
|
||||
label.assets.set(assets)
|
||||
return label
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
|
||||
31
apps/assets/migrations/0042_favoriteasset.py
Normal file
31
apps/assets/migrations/0042_favoriteasset.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# Generated by Django 2.2.5 on 2019-10-16 08:38
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('assets', '0041_gathereduser'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='FavoriteAsset',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
|
||||
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
|
||||
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
|
||||
('asset', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='assets.Asset')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('user', 'asset')},
|
||||
},
|
||||
),
|
||||
]
|
||||
23
apps/assets/migrations/0043_auto_20191114_1111.py
Normal file
23
apps/assets/migrations/0043_auto_20191114_1111.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 2.2.5 on 2019-11-14 03:11
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0042_favoriteasset'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='gathereduser',
|
||||
name='date_last_login',
|
||||
field=models.DateTimeField(null=True, verbose_name='Date last login'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='gathereduser',
|
||||
name='ip_last_login',
|
||||
field=models.CharField(default='', max_length=39, verbose_name='IP last login'),
|
||||
),
|
||||
]
|
||||
48
apps/assets/migrations/0044_platform.py
Normal file
48
apps/assets/migrations/0044_platform.py
Normal file
@@ -0,0 +1,48 @@
|
||||
# Generated by Django 2.2.7 on 2019-12-06 07:26
|
||||
|
||||
import common.fields.model
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def create_internal_platform(apps, schema_editor):
|
||||
model = apps.get_model("assets", "Platform")
|
||||
db_alias = schema_editor.connection.alias
|
||||
type_platforms = (
|
||||
('Linux', 'Linux', None),
|
||||
('Unix', 'Unix', None),
|
||||
('MacOS', 'MacOS', None),
|
||||
('BSD', 'BSD', None),
|
||||
('Windows', 'Windows', None),
|
||||
('Windows2016', 'Windows', {'security': 'tls'}),
|
||||
('Other', 'Other', None),
|
||||
)
|
||||
for name, base, meta in type_platforms:
|
||||
model.objects.using(db_alias).create(
|
||||
name=name, base=base, internal=True, meta=meta
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0043_auto_20191114_1111'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Platform',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.SlugField(allow_unicode=True, unique=True, verbose_name='Name')),
|
||||
('base', models.CharField(choices=[('Linux', 'Linux'), ('Unix', 'Unix'), ('MacOS', 'MacOS'), ('BSD', 'BSD'), ('Windows', 'Windows'), ('Other', 'Other')], default='Linux', max_length=16, verbose_name='Base')),
|
||||
('charset', models.CharField(choices=[('utf8', 'UTF-8'), ('gbk', 'GBK')], default='utf8', max_length=8, verbose_name='Charset')),
|
||||
('meta', common.fields.model.JsonDictTextField(blank=True, null=True, verbose_name='Meta')),
|
||||
('internal', models.BooleanField(default=False, verbose_name='Internal')),
|
||||
('comment', models.TextField(blank=True, null=True, verbose_name='Comment')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Platform'
|
||||
}
|
||||
),
|
||||
migrations.RunPython(create_internal_platform)
|
||||
]
|
||||
47
apps/assets/migrations/0045_auto_20191206_1607.py
Normal file
47
apps/assets/migrations/0045_auto_20191206_1607.py
Normal file
@@ -0,0 +1,47 @@
|
||||
# Generated by Django 2.2.7 on 2019-12-06 08:07
|
||||
|
||||
import assets.models.asset
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
def migrate_platform_to_asset_type(apps, schema_editor):
|
||||
asset_model = apps.get_model("assets", "Asset")
|
||||
platform_model = apps.get_model("assets", "Platform")
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
platforms = platform_model.objects.using(db_alias).all()
|
||||
platforms_map = {p.name: p for p in platforms}
|
||||
for name, p in platforms_map.items():
|
||||
asset_model.objects.using(db_alias)\
|
||||
.filter(_platform=name)\
|
||||
.update(platform=p)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0044_platform'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='asset',
|
||||
old_name='platform',
|
||||
new_name='_platform',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='asset',
|
||||
name='platform',
|
||||
field=models.ForeignKey(
|
||||
default=assets.models.asset.Platform.default,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='assets', to='assets.Platform',
|
||||
verbose_name='Platform'),
|
||||
),
|
||||
migrations.RunPython(migrate_platform_to_asset_type),
|
||||
migrations.RemoveField(
|
||||
model_name='asset',
|
||||
name='_platform',
|
||||
),
|
||||
]
|
||||
18
apps/assets/migrations/0046_auto_20191218_1705.py
Normal file
18
apps/assets/migrations/0046_auto_20191218_1705.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 2.1.11 on 2019-12-18 09:05
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0045_auto_20191206_1607'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='systemuser',
|
||||
name='protocol',
|
||||
field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp'), ('telnet', 'telnet'), ('vnc', 'vnc'), ('mysql', 'mysql')], default='ssh', max_length=16, verbose_name='Protocol'),
|
||||
),
|
||||
]
|
||||
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'),
|
||||
),
|
||||
]
|
||||
@@ -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 *
|
||||
@@ -10,3 +12,4 @@ from .authbook import *
|
||||
from .utils import *
|
||||
from .authbook import *
|
||||
from .gathered_user import *
|
||||
from .favorite_asset import *
|
||||
|
||||
@@ -11,10 +11,13 @@ from collections import OrderedDict
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from .utils import Connectivity
|
||||
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']
|
||||
__all__ = ['Asset', 'ProtocolsMixin', 'Platform']
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -37,6 +40,13 @@ def default_node():
|
||||
return None
|
||||
|
||||
|
||||
class AssetManager(OrgManager):
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().annotate(
|
||||
platform_base=models.F('platform__base')
|
||||
)
|
||||
|
||||
|
||||
class AssetQuerySet(models.QuerySet):
|
||||
def active(self):
|
||||
return self.filter(is_active=True)
|
||||
@@ -119,6 +129,47 @@ class NodesRelationMixin:
|
||||
return nodes
|
||||
|
||||
|
||||
class Platform(models.Model):
|
||||
CHARSET_CHOICES = (
|
||||
('utf8', 'UTF-8'),
|
||||
('gbk', 'GBK'),
|
||||
)
|
||||
BASE_CHOICES = (
|
||||
('Linux', 'Linux'),
|
||||
('Unix', 'Unix'),
|
||||
('MacOS', 'MacOS'),
|
||||
('BSD', 'BSD'),
|
||||
('Windows', 'Windows'),
|
||||
('Other', 'Other'),
|
||||
)
|
||||
name = models.SlugField(verbose_name=_("Name"), unique=True, allow_unicode=True)
|
||||
base = models.CharField(choices=BASE_CHOICES, max_length=16, default='Linux', verbose_name=_("Base"))
|
||||
charset = models.CharField(default='utf8', choices=CHARSET_CHOICES, max_length=8, verbose_name=_("Charset"))
|
||||
meta = JsonDictTextField(blank=True, null=True, verbose_name=_("Meta"))
|
||||
internal = models.BooleanField(default=False, verbose_name=_("Internal"))
|
||||
comment = models.TextField(blank=True, null=True, verbose_name=_("Comment"))
|
||||
|
||||
@classmethod
|
||||
def default(cls):
|
||||
linux, created = cls.objects.get_or_create(
|
||||
defaults={'name': 'Linux'}, name='Linux'
|
||||
)
|
||||
return linux.id
|
||||
|
||||
def is_windows(self):
|
||||
return self.base.lower() in ('windows',)
|
||||
|
||||
def is_unixlike(self):
|
||||
return self.base.lower() in ("linux", "unix", "macos", "bsd")
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Platform")
|
||||
# ordering = ('name',)
|
||||
|
||||
|
||||
class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
|
||||
# Important
|
||||
PLATFORM_CHOICES = (
|
||||
@@ -138,9 +189,8 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
|
||||
choices=ProtocolsMixin.PROTOCOL_CHOICES,
|
||||
verbose_name=_('Protocol'))
|
||||
port = models.IntegerField(default=22, verbose_name=_('Port'))
|
||||
|
||||
protocols = models.CharField(max_length=128, default='ssh/22', blank=True, verbose_name=_("Protocols"))
|
||||
platform = models.CharField(max_length=128, choices=PLATFORM_CHOICES, default='Linux', verbose_name=_('Platform'))
|
||||
platform = models.ForeignKey(Platform, default=Platform.default, on_delete=models.PROTECT, verbose_name=_("Platform"), related_name='assets')
|
||||
domain = models.ForeignKey("assets.Domain", null=True, blank=True, related_name='assets', verbose_name=_("Domain"), on_delete=models.SET_NULL)
|
||||
nodes = models.ManyToManyField('assets.Node', default=default_node, related_name='assets', verbose_name=_("Nodes"))
|
||||
is_active = models.BooleanField(default=True, verbose_name=_('Is active'))
|
||||
@@ -175,7 +225,7 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
|
||||
date_created = models.DateTimeField(auto_now_add=True, null=True, blank=True, verbose_name=_('Date created'))
|
||||
comment = models.TextField(max_length=128, default='', blank=True, verbose_name=_('Comment'))
|
||||
|
||||
objects = OrgManager.from_queryset(AssetQuerySet)()
|
||||
objects = AssetManager.from_queryset(AssetQuerySet)()
|
||||
_connectivity = None
|
||||
|
||||
def __str__(self):
|
||||
@@ -190,20 +240,29 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
|
||||
return False, warning
|
||||
return True, warning
|
||||
|
||||
@lazyproperty
|
||||
def platform_base(self):
|
||||
return self.platform.base
|
||||
|
||||
@lazyproperty
|
||||
def admin_user_display(self):
|
||||
return self.admin_user.name
|
||||
|
||||
@lazyproperty
|
||||
def admin_user_username(self):
|
||||
"""求可连接性时,直接用用户名去取,避免再查一次admin user
|
||||
serializer 中直接通过annotate方式返回了这个
|
||||
"""
|
||||
return self.admin_user.username
|
||||
|
||||
def is_windows(self):
|
||||
if self.platform in ("Windows", "Windows2016"):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
return self.platform.is_windows()
|
||||
|
||||
def is_unixlike(self):
|
||||
if self.platform not in ("Windows", "Windows2016", "Other"):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
return self.platform.is_unixlike()
|
||||
|
||||
def is_support_ansible(self):
|
||||
return self.has_protocol('ssh') and self.platform not in ("Other",)
|
||||
return self.has_protocol('ssh') and self.platform_base not in ("Other",)
|
||||
|
||||
@property
|
||||
def cpu_info(self):
|
||||
@@ -228,9 +287,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
|
||||
@@ -243,7 +304,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,
|
||||
@@ -264,9 +325,9 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
|
||||
def as_tree_node(self, parent_node):
|
||||
from common.tree import TreeNode
|
||||
icon_skin = 'file'
|
||||
if self.platform.lower() == 'windows':
|
||||
if self.platform_base.lower() == 'windows':
|
||||
icon_skin = 'windows'
|
||||
elif self.platform.lower() == 'linux':
|
||||
elif self.platform_base.lower() == 'linux':
|
||||
icon_skin = 'linux'
|
||||
data = {
|
||||
'id': str(self.id),
|
||||
@@ -283,7 +344,7 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
|
||||
'hostname': self.hostname,
|
||||
'ip': self.ip,
|
||||
'protocols': self.protocols_as_list,
|
||||
'platform': self.platform,
|
||||
'platform': self.platform_base,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,32 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from django.db import models
|
||||
from django.db import models, transaction
|
||||
from django.db.models import Max
|
||||
from django.core.cache import cache
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orgs.mixins.models import OrgManager
|
||||
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,41 +37,42 @@ 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 set_version_and_latest(self):
|
||||
self.set_version()
|
||||
self.set_to_latest()
|
||||
|
||||
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']
|
||||
key_lock = 'KEY_LOCK_CREATE_AUTH_BOOK_{}_{}'.format(username, asset.id)
|
||||
with cache.lock(key_lock):
|
||||
with transaction.atomic():
|
||||
cls.objects.filter(
|
||||
username=username, asset=asset, is_latest=True
|
||||
).update(is_latest=False)
|
||||
max_version = cls.get_max_version(username, asset)
|
||||
kwargs.update({
|
||||
'version': max_version + 1,
|
||||
'is_latest': True
|
||||
})
|
||||
obj = cls.objects.create(**kwargs)
|
||||
return obj
|
||||
|
||||
@property
|
||||
def connectivity(self):
|
||||
return self.get_asset_connectivity(self.asset)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import io
|
||||
import os
|
||||
import uuid
|
||||
from hashlib import md5
|
||||
@@ -11,90 +12,29 @@ from django.utils.translation import ugettext_lazy as _
|
||||
from django.conf import settings
|
||||
|
||||
from common.utils import (
|
||||
get_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
|
||||
|
||||
signer = get_signer()
|
||||
|
||||
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"
|
||||
|
||||
@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_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()
|
||||
@@ -141,18 +81,10 @@ class AssetUser(OrgModelMixin):
|
||||
cache.set(cache_key, amount, self.ASSET_USER_CACHE_TIME)
|
||||
return amount
|
||||
|
||||
@property
|
||||
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)
|
||||
@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)
|
||||
@@ -165,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 = ''
|
||||
@@ -205,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"""
|
||||
@@ -229,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
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ from django.db import models
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from common.utils import lazyproperty
|
||||
from orgs.mixins.models import OrgModelMixin
|
||||
|
||||
|
||||
@@ -57,25 +58,30 @@ class CommandFilterRule(OrgModelMixin):
|
||||
date_updated = models.DateTimeField(auto_now=True)
|
||||
created_by = models.CharField(max_length=128, blank=True, default='', verbose_name=_('Created by'))
|
||||
|
||||
__pattern = None
|
||||
|
||||
class Meta:
|
||||
ordering = ('-priority', 'action')
|
||||
verbose_name = _("Command filter rule")
|
||||
|
||||
@property
|
||||
@lazyproperty
|
||||
def _pattern(self):
|
||||
if self.__pattern:
|
||||
return self.__pattern
|
||||
if self.type == 'command':
|
||||
regex = []
|
||||
for cmd in self.content.split('\r\n'):
|
||||
cmd = cmd.replace(' ', '\s+')
|
||||
regex.append(r'\b{0}\b'.format(cmd))
|
||||
self.__pattern = re.compile(r'{}'.format('|'.join(regex)))
|
||||
content = self.content.replace('\r\n', '\n')
|
||||
for cmd in content.split('\n'):
|
||||
cmd = re.escape(cmd)
|
||||
cmd = cmd.replace('\\ ', '\s+')
|
||||
if cmd[-1].isalpha():
|
||||
regex.append(r'\b{0}\b'.format(cmd))
|
||||
else:
|
||||
regex.append(r'\b{0}'.format(cmd))
|
||||
s = r'{}'.format('|'.join(regex))
|
||||
else:
|
||||
self.__pattern = re.compile(r'{0}'.format(self.content))
|
||||
return self.__pattern
|
||||
s = r'{0}'.format(self.content)
|
||||
try:
|
||||
_pattern = re.compile(s)
|
||||
except:
|
||||
_pattern = ''
|
||||
return _pattern
|
||||
|
||||
def match(self, data):
|
||||
found = self._pattern.search(data)
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
|
||||
import uuid
|
||||
import random
|
||||
import re
|
||||
|
||||
import paramiko
|
||||
|
||||
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']
|
||||
|
||||
@@ -39,7 +39,7 @@ class Domain(OrgModelMixin):
|
||||
return random.choice(self.gateways)
|
||||
|
||||
|
||||
class Gateway(AssetUser):
|
||||
class Gateway(BaseUser):
|
||||
PROTOCOL_SSH = 'ssh'
|
||||
PROTOCOL_RDP = 'rdp'
|
||||
PROTOCOL_CHOICES = (
|
||||
@@ -63,6 +63,9 @@ class Gateway(AssetUser):
|
||||
def test_connective(self, local_port=None):
|
||||
if local_port is None:
|
||||
local_port = self.port
|
||||
if self.password and not re.match(r'\w+$', self.password):
|
||||
return False, _("Password should not contain special characters")
|
||||
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
proxy = paramiko.SSHClient()
|
||||
|
||||
20
apps/assets/models/favorite_asset.py
Normal file
20
apps/assets/models/favorite_asset.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.db import models
|
||||
|
||||
from common.mixins.models import CommonModelMixin
|
||||
|
||||
|
||||
__all__ = ['FavoriteAsset']
|
||||
|
||||
|
||||
class FavoriteAsset(CommonModelMixin):
|
||||
user = models.ForeignKey('users.User', on_delete=models.CASCADE)
|
||||
asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('user', 'asset')
|
||||
|
||||
@classmethod
|
||||
def get_user_favorite_assets_id(cls, user):
|
||||
return cls.objects.filter(user=user).values_list('asset', flat=True)
|
||||
@@ -12,13 +12,12 @@ __all__ = ['GatheredUser']
|
||||
class GatheredUser(OrgModelMixin):
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, verbose_name=_("Asset"))
|
||||
username = models.CharField(max_length=32, blank=True, db_index=True,
|
||||
verbose_name=_('Username'))
|
||||
username = models.CharField(max_length=32, blank=True, db_index=True, verbose_name=_('Username'))
|
||||
present = models.BooleanField(default=True, verbose_name=_("Present"))
|
||||
date_created = models.DateTimeField(auto_now_add=True,
|
||||
verbose_name=_("Date created"))
|
||||
date_updated = models.DateTimeField(auto_now=True,
|
||||
verbose_name=_("Date updated"))
|
||||
date_last_login = models.DateTimeField(null=True, verbose_name=_("Date last login"))
|
||||
ip_last_login = models.CharField(max_length=39, default='', verbose_name=_("IP last login"))
|
||||
date_created = models.DateTimeField(auto_now_add=True, verbose_name=_("Date created"))
|
||||
date_updated = models.DateTimeField(auto_now=True, verbose_name=_("Date updated"))
|
||||
|
||||
@property
|
||||
def hostname(self):
|
||||
|
||||
@@ -6,13 +6,14 @@ import time
|
||||
|
||||
from django.db import models, transaction
|
||||
from django.db.models import Q
|
||||
from django.db.utils import IntegrityError
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import ugettext
|
||||
from django.core.cache import cache
|
||||
|
||||
from 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
|
||||
|
||||
|
||||
@@ -25,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 tree 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:
|
||||
@@ -324,6 +370,8 @@ class SomeNodesMixin:
|
||||
ungrouped_value = _('ungrouped')
|
||||
empty_key = '-11'
|
||||
empty_value = _("empty")
|
||||
favorite_key = '-12'
|
||||
favorite_value = _("favorite")
|
||||
|
||||
def is_default_node(self):
|
||||
return self.key == self.default_key
|
||||
@@ -334,6 +382,17 @@ class SomeNodesMixin:
|
||||
else:
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def get_next_org_root_node_key(cls):
|
||||
with tmp_to_org(Organization.root()):
|
||||
org_nodes_roots = cls.objects.filter(key__regex=r'^[0-9]+$')
|
||||
org_nodes_roots_keys = org_nodes_roots.values_list('key', flat=True)
|
||||
if not org_nodes_roots_keys:
|
||||
org_nodes_roots_keys = ['1']
|
||||
max_key = max([int(k) for k in org_nodes_roots_keys])
|
||||
key = str(max_key + 1) if max_key != 0 else '2'
|
||||
return key
|
||||
|
||||
@classmethod
|
||||
def create_org_root_node(cls):
|
||||
# 如果使用current_org 在set_current_org时会死循环
|
||||
@@ -341,14 +400,7 @@ class SomeNodesMixin:
|
||||
with transaction.atomic():
|
||||
if not ori_org.is_real():
|
||||
return cls.default_node()
|
||||
set_current_org(Organization.root())
|
||||
org_nodes_roots = cls.objects.filter(key__regex=r'^[0-9]+$')
|
||||
org_nodes_roots_keys = org_nodes_roots.values_list('key', flat=True)
|
||||
if not org_nodes_roots_keys:
|
||||
org_nodes_roots_keys = ['1']
|
||||
key = max([int(k) for k in org_nodes_roots_keys])
|
||||
key = str(key + 1) if key != 0 else '2'
|
||||
set_current_org(ori_org)
|
||||
key = cls.get_next_org_root_node_key()
|
||||
root = cls.objects.create(key=key, value=ori_org.name)
|
||||
return root
|
||||
|
||||
@@ -363,35 +415,71 @@ class SomeNodesMixin:
|
||||
@classmethod
|
||||
def ungrouped_node(cls):
|
||||
with tmp_to_org(Organization.system()):
|
||||
defaults = {'value': cls.ungrouped_key}
|
||||
defaults = {'value': cls.ungrouped_value}
|
||||
obj, created = cls.objects.get_or_create(
|
||||
defaults=defaults, key=cls.ungrouped_key
|
||||
)
|
||||
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()):
|
||||
defaults = {'value': cls.default_value}
|
||||
try:
|
||||
obj, created = cls.objects.get_or_create(
|
||||
defaults=defaults, key=cls.default_key,
|
||||
)
|
||||
except IntegrityError as e:
|
||||
logger.error("Create default node failed: {}".format(e))
|
||||
cls.modify_other_org_root_node_key()
|
||||
obj, created = cls.objects.get_or_create(
|
||||
defaults=defaults, key=cls.default_key,
|
||||
)
|
||||
return obj
|
||||
|
||||
@classmethod
|
||||
def favorite_node(cls):
|
||||
with tmp_to_org(Organization.system()):
|
||||
defaults = {'value': cls.favorite_value}
|
||||
obj, created = cls.objects.get_or_create(
|
||||
defaults=defaults, key=cls.default_key,
|
||||
defaults=defaults, key=cls.favorite_key
|
||||
)
|
||||
return obj
|
||||
|
||||
@classmethod
|
||||
def initial_some_nodes(cls):
|
||||
cls.default_node()
|
||||
cls.empty_node()
|
||||
cls.ungrouped_node()
|
||||
cls.favorite_node()
|
||||
|
||||
@classmethod
|
||||
def modify_other_org_root_node_key(cls):
|
||||
"""
|
||||
解决创建 default 节点失败的问题,
|
||||
因为在其他组织下存在 default 节点,故在 DEFAULT 组织下 get 不到 create 失败
|
||||
"""
|
||||
logger.info("Modify other org root node key")
|
||||
|
||||
with tmp_to_org(Organization.root()):
|
||||
node_key1 = cls.objects.filter(key='1').first()
|
||||
if not node_key1:
|
||||
logger.info("Not found node that `key` = 1")
|
||||
return
|
||||
if not node_key1.org.is_real():
|
||||
logger.info("Org is not real for node that `key` = 1")
|
||||
return
|
||||
|
||||
with transaction.atomic():
|
||||
with tmp_to_org(node_key1.org):
|
||||
org_root_node_new_key = cls.get_next_org_root_node_key()
|
||||
for n in cls.objects.all():
|
||||
old_key = n.key
|
||||
key_list = n.key.split(':')
|
||||
key_list[0] = org_root_node_new_key
|
||||
new_key = ':'.join(key_list)
|
||||
n.key = new_key
|
||||
n.save()
|
||||
logger.info('Modify key ( {} > {} )'.format(old_key, new_key))
|
||||
|
||||
|
||||
class Node(OrgModelMixin, SomeNodesMixin, TreeMixin, FamilyMixin, FullValueMixin, NodeAssetsMixin):
|
||||
@@ -412,11 +500,11 @@ class Node(OrgModelMixin, SomeNodesMixin, TreeMixin, FamilyMixin, FullValueMixin
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
||||
def __eq__(self, other):
|
||||
if not other:
|
||||
return False
|
||||
return self.id == other.id
|
||||
|
||||
# def __eq__(self, other):
|
||||
# if not other:
|
||||
# return False
|
||||
# return self.id == other.id
|
||||
#
|
||||
def __gt__(self, other):
|
||||
self_key = [int(k) for k in self.key.split(':')]
|
||||
other_key = [int(k) for k in other.key.split(':')]
|
||||
@@ -470,8 +558,13 @@ class Node(OrgModelMixin, SomeNodesMixin, TreeMixin, FamilyMixin, FullValueMixin
|
||||
tree_node = TreeNode(**data)
|
||||
return tree_node
|
||||
|
||||
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.children or self.get_assets():
|
||||
if self.has_children_or_has_assets():
|
||||
return
|
||||
return super().delete(using=using, keep_parents=keep_parents)
|
||||
|
||||
@@ -481,14 +574,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,23 +4,20 @@
|
||||
|
||||
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 get_signer
|
||||
from .base import AssetUser
|
||||
from common.utils import signer
|
||||
from .base import BaseUser
|
||||
from .asset import Asset
|
||||
|
||||
|
||||
__all__ = ['AdminUser', 'SystemUser']
|
||||
logger = logging.getLogger(__name__)
|
||||
signer = get_signer()
|
||||
|
||||
|
||||
class AdminUser(AssetUser):
|
||||
class AdminUser(BaseUser):
|
||||
"""
|
||||
A privileged user that ansible can use it to push system user and so on
|
||||
"""
|
||||
@@ -88,16 +85,18 @@ class AdminUser(AssetUser):
|
||||
continue
|
||||
|
||||
|
||||
class SystemUser(AssetUser):
|
||||
class SystemUser(BaseUser):
|
||||
PROTOCOL_SSH = 'ssh'
|
||||
PROTOCOL_RDP = 'rdp'
|
||||
PROTOCOL_TELNET = 'telnet'
|
||||
PROTOCOL_VNC = 'vnc'
|
||||
PROTOCOL_MYSQL = 'mysql'
|
||||
PROTOCOL_CHOICES = (
|
||||
(PROTOCOL_SSH, 'ssh'),
|
||||
(PROTOCOL_RDP, 'rdp'),
|
||||
(PROTOCOL_TELNET, 'telnet'),
|
||||
(PROTOCOL_VNC, 'vnc'),
|
||||
(PROTOCOL_MYSQL, 'mysql'),
|
||||
)
|
||||
|
||||
LOGIN_AUTO = 'auto'
|
||||
@@ -106,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'))
|
||||
@@ -116,9 +117,24 @@ 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):
|
||||
return self.nodes.all().count()
|
||||
|
||||
@property
|
||||
def login_mode_display(self):
|
||||
@@ -130,6 +146,23 @@ class SystemUser(AssetUser):
|
||||
else:
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_need_cmd_filter(self):
|
||||
return self.protocol not in [self.PROTOCOL_RDP, self.PROTOCOL_VNC]
|
||||
|
||||
@property
|
||||
def is_need_test_asset_connective(self):
|
||||
return self.protocol not in [self.PROTOCOL_MYSQL]
|
||||
|
||||
@property
|
||||
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
|
||||
|
||||
@@ -10,3 +10,4 @@ from .domain import *
|
||||
from .cmd_filter import *
|
||||
from .asset_user import *
|
||||
from .gathered_user import *
|
||||
from .favorite_asset import *
|
||||
|
||||
@@ -45,7 +45,7 @@ class ReplaceNodeAdminUserSerializer(serializers.ModelSerializer):
|
||||
管理用户更新关联到的集群
|
||||
"""
|
||||
nodes = serializers.PrimaryKeyRelatedField(
|
||||
many=True, queryset=Node.objects.all()
|
||||
many=True, queryset=Node.objects
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@@ -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,18 +1,20 @@
|
||||
# -*- 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
|
||||
from common.serializers import AdaptedBulkListSerializer
|
||||
from ..models import Asset, Node, Label
|
||||
from ..models import Asset, Node, Label, Platform
|
||||
from .base import ConnectivitySerializer
|
||||
|
||||
__all__ = [
|
||||
'AssetSerializer', 'AssetSimpleSerializer',
|
||||
'ProtocolsField',
|
||||
'AssetDisplaySerializer',
|
||||
'ProtocolsField', 'PlatformSerializer',
|
||||
'AssetDetailSerializer', 'AssetTaskSerializer',
|
||||
]
|
||||
|
||||
|
||||
@@ -61,30 +63,45 @@ class ProtocolsField(serializers.ListField):
|
||||
|
||||
|
||||
class AssetSerializer(BulkOrgResourceModelSerializer):
|
||||
platform = serializers.SlugRelatedField(
|
||||
slug_field='name', queryset=Platform.objects.all(), label=_("Platform")
|
||||
)
|
||||
protocols = ProtocolsField(label=_('Protocols'), required=False)
|
||||
connectivity = ConnectivitySerializer(read_only=True, label=_("Connectivity"))
|
||||
|
||||
"""
|
||||
资产的数据结构
|
||||
"""
|
||||
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', '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},
|
||||
@@ -92,22 +109,10 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
|
||||
'org_name': {'label': _('Org name')}
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def validate_hostname(hostname):
|
||||
pattern = r"^[\._@\w-]+$"
|
||||
res = re.match(pattern, hostname)
|
||||
if res is None:
|
||||
msg = _("* The hostname contains characters that are not allowed")
|
||||
raise serializers.ValidationError(msg)
|
||||
return hostname
|
||||
|
||||
@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')
|
||||
queryset = queryset.select_related('admin_user', 'domain', 'platform')
|
||||
return queryset
|
||||
|
||||
def compatible_with_old_protocol(self, validated_data):
|
||||
@@ -135,8 +140,49 @@ 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)
|
||||
|
||||
class Meta:
|
||||
model = Platform
|
||||
fields = [
|
||||
'id', 'name', 'base', 'charset',
|
||||
'internal', 'meta', 'comment'
|
||||
]
|
||||
|
||||
|
||||
class AssetDetailSerializer(AssetSerializer):
|
||||
platform = PlatformSerializer(read_only=True)
|
||||
|
||||
|
||||
class AssetSimpleSerializer(serializers.ModelSerializer):
|
||||
connectivity = ConnectivitySerializer(read_only=True, label=_("Connectivity"))
|
||||
|
||||
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_version_and_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,14 +80,8 @@ 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.all(), label=_("Asset"))
|
||||
asset = serializers.PrimaryKeyRelatedField(queryset=Asset.objects, label=_("Asset"))
|
||||
username = serializers.CharField(max_length=1024)
|
||||
|
||||
def create(self, validated_data):
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
23
apps/assets/serializers/favorite_asset.py
Normal file
23
apps/assets/serializers/favorite_asset.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from orgs.utils import tmp_to_root_org
|
||||
from common.serializers import AdaptedBulkListSerializer
|
||||
from common.mixins import BulkSerializerMixin
|
||||
from ..models import FavoriteAsset
|
||||
|
||||
|
||||
__all__ = ['FavoriteAssetSerializer']
|
||||
|
||||
|
||||
class FavoriteAssetSerializer(BulkSerializerMixin, serializers.ModelSerializer):
|
||||
user = serializers.HiddenField(
|
||||
default=serializers.CurrentUserDefault()
|
||||
)
|
||||
|
||||
class Meta:
|
||||
list_serializer_class = AdaptedBulkListSerializer
|
||||
model = FavoriteAsset
|
||||
fields = ['user', 'asset']
|
||||
@@ -12,10 +12,11 @@ class GatheredUserSerializer(OrgResourceModelSerializerMixin):
|
||||
model = GatheredUser
|
||||
fields = [
|
||||
'id', 'asset', 'hostname', 'ip', 'username',
|
||||
'date_last_login', 'ip_last_login',
|
||||
'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",
|
||||
]
|
||||
|
||||
|
||||
@@ -38,8 +38,10 @@ class NodeSerializer(BulkOrgResourceModelSerializer):
|
||||
return data
|
||||
|
||||
|
||||
class NodeAssetsSerializer(serializers.ModelSerializer):
|
||||
assets = serializers.PrimaryKeyRelatedField(many=True, queryset=Asset.objects.all())
|
||||
class NodeAssetsSerializer(BulkOrgResourceModelSerializer):
|
||||
assets = serializers.PrimaryKeyRelatedField(
|
||||
many=True, queryset=Asset.objects
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Node
|
||||
@@ -49,3 +51,12 @@ class NodeAssetsSerializer(serializers.ModelSerializer):
|
||||
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,12 +1,21 @@
|
||||
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 ..models import SystemUser
|
||||
from .base import AuthSerializer, AuthSerializerMixin
|
||||
from assets.models import Node
|
||||
from ..models import SystemUser, Asset
|
||||
from .base import AuthSerializerMixin
|
||||
|
||||
__all__ = [
|
||||
'SystemUserSerializer', 'SystemUserListSerializer',
|
||||
'SystemUserSimpleSerializer', 'SystemUserAssetRelationSerializer',
|
||||
'SystemUserNodeRelationSerializer', 'SystemUserTaskSerializer',
|
||||
'SystemUserUserRelationSerializer', 'SystemUserWithAuthInfoSerializer',
|
||||
]
|
||||
|
||||
|
||||
class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
@@ -19,15 +28,19 @@ 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',
|
||||
'auto_push', 'cmd_filters', 'sudo', 'shell', 'comment', 'nodes',
|
||||
'assets_amount', 'auto_generate_key'
|
||||
'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',
|
||||
'auto_generate_key', 'sftp_root',
|
||||
'assets_amount', 'date_created', 'created_by'
|
||||
]
|
||||
extra_kwargs = {
|
||||
'password': {"write_only": True},
|
||||
'public_key': {"write_only": True},
|
||||
'private_key': {"write_only": True},
|
||||
'nodes_amount': {'label': _('Node')},
|
||||
'assets_amount': {'label': _('Asset')},
|
||||
'login_mode_display': {'label': _('Login mode display')},
|
||||
'created_by': {'read_only': True},
|
||||
@@ -57,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)
|
||||
@@ -80,8 +119,12 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
|
||||
def validate(self, attrs):
|
||||
username = attrs.get("username", "manual")
|
||||
auto_gen_key = attrs.pop("auto_generate_key", False)
|
||||
protocol = attrs.get("protocol")
|
||||
auto_gen_key = attrs.get("auto_generate_key", False)
|
||||
|
||||
if protocol not in [SystemUser.PROTOCOL_RDP, SystemUser.PROTOCOL_SSH]:
|
||||
return attrs
|
||||
|
||||
if auto_gen_key:
|
||||
password = SystemUser.gen_password()
|
||||
attrs["password"] = password
|
||||
@@ -96,27 +139,44 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
public_key = ssh_pubkey_gen(private_key, password=password,
|
||||
username=username)
|
||||
attrs["public_key"] = public_key
|
||||
attrs.pop("auto_generate_key", None)
|
||||
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):
|
||||
"""
|
||||
系统用户认证信息
|
||||
"""
|
||||
|
||||
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',
|
||||
]
|
||||
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):
|
||||
@@ -128,4 +188,65 @@ class SystemUserSimpleSerializer(serializers.ModelSerializer):
|
||||
fields = ('id', 'name', 'username')
|
||||
|
||||
|
||||
class RelationMixin(BulkSerializerMixin, serializers.Serializer):
|
||||
systemuser_display = serializers.ReadOnlyField()
|
||||
|
||||
def get_field_names(self, declared_fields, info):
|
||||
fields = super().get_field_names(declared_fields, info)
|
||||
fields.extend(['systemuser', "systemuser_display"])
|
||||
return fields
|
||||
|
||||
class Meta:
|
||||
list_serializer_class = AdaptedBulkListSerializer
|
||||
|
||||
|
||||
class SystemUserAssetRelationSerializer(RelationMixin, serializers.ModelSerializer):
|
||||
asset_display = serializers.ReadOnlyField()
|
||||
|
||||
class Meta(RelationMixin.Meta):
|
||||
model = SystemUser.assets.through
|
||||
fields = [
|
||||
'id', "asset", "asset_display",
|
||||
]
|
||||
|
||||
|
||||
class SystemUserNodeRelationSerializer(RelationMixin, serializers.ModelSerializer):
|
||||
node_display = serializers.SerializerMethodField()
|
||||
|
||||
class Meta(RelationMixin.Meta):
|
||||
model = SystemUser.nodes.through
|
||||
fields = [
|
||||
'id', 'node', "node_display",
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.tree = Node.tree()
|
||||
|
||||
def get_node_display(self, obj):
|
||||
if hasattr(obj, 'node_key'):
|
||||
return self.tree.get_node_full_tag(obj.node_key)
|
||||
else:
|
||||
return obj.node.full_value
|
||||
|
||||
|
||||
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,12 +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 .models import Asset, SystemUser, Node
|
||||
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
|
||||
)
|
||||
@@ -93,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):
|
||||
"""
|
||||
@@ -112,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):
|
||||
"""
|
||||
@@ -120,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)
|
||||
@@ -131,16 +169,21 @@ def on_asset_nodes_add(sender, instance=None, action='', model=None, pk_set=None
|
||||
if action != "post_add":
|
||||
return
|
||||
logger.debug("Assets node add signal recv: {}".format(action))
|
||||
queryset = model.objects.filter(pk__in=pk_set).values_list('id', flat=True)
|
||||
if model == Node:
|
||||
nodes = queryset
|
||||
assets = [instance]
|
||||
nodes = model.objects.filter(pk__in=pk_set).values_list('key', flat=True)
|
||||
assets = [instance.id]
|
||||
else:
|
||||
nodes = [instance]
|
||||
assets = queryset
|
||||
# 节点资产发生变化时,将资产关联到节点关联的系统用户, 只关注新增的
|
||||
nodes = [instance.key]
|
||||
assets = model.objects.filter(pk__in=pk_set).values_list('id', flat=True)
|
||||
# 节点资产发生变化时,将资产关联到节点及祖先节点关联的系统用户, 只关注新增的
|
||||
nodes_ancestors_keys = set()
|
||||
node_tree = TreeService.new()
|
||||
for node in nodes:
|
||||
ancestors_keys = node_tree.ancestors_ids(nid=node)
|
||||
nodes_ancestors_keys.update(ancestors_keys)
|
||||
system_users = SystemUser.objects.filter(nodes__key__in=nodes_ancestors_keys)
|
||||
|
||||
system_users_assets = defaultdict(set)
|
||||
system_users = SystemUser.objects.filter(nodes__in=nodes)
|
||||
for system_user in system_users:
|
||||
system_users_assets[system_user].update(set(assets))
|
||||
for system_user, _assets in system_users_assets.items():
|
||||
@@ -189,3 +232,5 @@ def on_asset_nodes_remove(sender, instance=None, action='', model=None,
|
||||
def on_node_update_or_created(sender, **kwargs):
|
||||
# 刷新节点
|
||||
Node.refresh_nodes()
|
||||
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
|
||||
)
|
||||
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,
|
||||
@@ -94,6 +59,15 @@ GATHER_ASSET_USERS_TASKS = [
|
||||
"args": "database=passwd"
|
||||
},
|
||||
},
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
GATHER_ASSET_USERS_TASKS_WINDOWS = [
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,14 +2,15 @@
|
||||
|
||||
import re
|
||||
from collections import defaultdict
|
||||
|
||||
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']
|
||||
@@ -19,19 +20,25 @@ ignore_login_shell = re.compile(r'nologin$|sync$|shutdown$|halt$')
|
||||
|
||||
|
||||
def parse_linux_result_to_users(result):
|
||||
task_result = {}
|
||||
for task_name, raw in result.items():
|
||||
res = raw.get('ansible_facts', {}).get('getent_passwd')
|
||||
if res:
|
||||
task_result = res
|
||||
break
|
||||
if not task_result or not isinstance(task_result, dict):
|
||||
return []
|
||||
users = []
|
||||
for username, attr in task_result.items():
|
||||
users = defaultdict(dict)
|
||||
users_result = result.get('gather host users', {})\
|
||||
.get('ansible_facts', {})\
|
||||
.get('getent_passwd')
|
||||
if not isinstance(users_result, dict):
|
||||
users_result = {}
|
||||
for username, attr in users_result.items():
|
||||
if ignore_login_shell.search(attr[-1]):
|
||||
continue
|
||||
users.append(username)
|
||||
users[username] = {}
|
||||
last_login_result = result.get('get last login', {}).get('stdout_lines', [])
|
||||
for line in last_login_result:
|
||||
data = line.split('@')
|
||||
if len(data) != 3:
|
||||
continue
|
||||
username, ip, dt = data
|
||||
dt += ' +0800'
|
||||
date = timezone.datetime.strptime(dt, '%b %d %H:%M:%S %Y %z')
|
||||
users[username] = {"ip": ip, "date": date}
|
||||
return users
|
||||
|
||||
|
||||
@@ -45,7 +52,7 @@ def parse_windows_result_to_users(result):
|
||||
if not task_result:
|
||||
return []
|
||||
|
||||
users = []
|
||||
users = {}
|
||||
|
||||
for i in range(4):
|
||||
task_result.pop(0)
|
||||
@@ -55,7 +62,7 @@ def parse_windows_result_to_users(result):
|
||||
for line in task_result:
|
||||
user = space.split(line)
|
||||
if user[0]:
|
||||
users.append(user[0])
|
||||
users[user[0]] = {}
|
||||
return users
|
||||
|
||||
|
||||
@@ -82,19 +89,24 @@ def add_asset_users(assets, results):
|
||||
with tmp_to_org(asset.org_id):
|
||||
GatheredUser.objects.filter(asset=asset, present=True)\
|
||||
.update(present=False)
|
||||
for username in users:
|
||||
for username, data in users.items():
|
||||
defaults = {'asset': asset, 'username': username, 'present': True}
|
||||
if data.get("ip"):
|
||||
defaults["ip_last_login"] = data["ip"]
|
||||
if data.get("date"):
|
||||
defaults["date_last_login"] = data["date"]
|
||||
GatheredUser.objects.update_or_create(
|
||||
defaults=defaults, asset=asset, username=username,
|
||||
)
|
||||
|
||||
|
||||
@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 = {
|
||||
@@ -120,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,34 @@ 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)
|
||||
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 +67,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 +113,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,6 +0,0 @@
|
||||
{% extends '_import_modal.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block modal_title%}{% trans "Import admin user" %}{% endblock %}
|
||||
|
||||
{% block import_modal_download_template_url %}{% url "api-assets:admin-user-list" %}{% endblock %}
|
||||
@@ -1,4 +0,0 @@
|
||||
{% extends '_update_modal.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block modal_title%}{% trans "Update admin user" %}{% endblock %}
|
||||
@@ -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_otp" checked id="id_enable_otp"><label for="id_enable_otp">{% trans 'Enable-MFA' %}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
{% endblock %}
|
||||
{% block modal_confirm_id %}btn_asset_group_bulk_update{% endblock %}
|
||||
@@ -1,6 +0,0 @@
|
||||
{% extends '_import_modal.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block modal_title%}{% trans "Import assets" %}{% endblock %}
|
||||
|
||||
{% block import_modal_download_template_url %}{% url "api-assets:asset-list" %}{% endblock %}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user